v2.0.0: 主题系统+SSH私钥解析修复+会话持久化+连接池优化+UI打磨

核心修复:
- ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2,
  解决 'Cannot parse privateKey: Unsupported key format' 错误
- useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey
- sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题
- database-service.ts: updateSession() 补充 private_key_path 字段持久化
- SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa'

UI/UX:
- 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计
- macOS 标题栏支持双击最大化(window:toggleMaximize IPC)
- macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,)
- WelcomeView 4卡片布局,移除 Settings 按钮
- TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线

架构优化:
- SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard)
- 连接池竞态修复,支持快速多标签连接
- IPC 接口规范化,安全错误处理
- Vite 代码分割优化

质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass
This commit is contained in:
Logan.Li committed 2026-04-22 11:15:41 +08:00
1 parent 37e65be823
commit 514d62644e
77 files changed
+2563 -931

No files matched your search

+40 -86
View File
@@ -1,93 +1,43 @@
// Type declarations for Electron IPC API exposed via preload
interface SSHConfig {
host: string
port?: number
username: string
password?: string
privateKey?: string
passphrase?: string
agent?: string
readyTimeout?: number
keepaliveInterval?: number
}
interface Session {
id: string
name: string
host: string
port: number
username: string
authType: 'password' | 'key' | 'agent'
password?: string
privateKey?: string
passphrase?: string
group?: string
tags?: string[]
}
interface IPCResponse<T = any> {
success: boolean
error?: string
connectionId?: string
shellId?: string
sftpId?: string
sessions?: Session[]
session?: Session
files?: any[]
result?: any
value?: any
path?: string
boundPort?: number
}
// All types imported from shared/types for consistency
interface ElectronAPI {
// SSH
sshConnect: (config: SSHConfig) => Promise<IPCResponse>
sshDisconnect: (connectionId: string) => Promise<IPCResponse>
sshExec: (connectionId: string, command: string) => Promise<IPCResponse>
sshCreateShell: (connectionId: string, options?: any, shellId?: string) => Promise<IPCResponse>
sshShellWrite: (shellId: string, data: string) => Promise<IPCResponse>
sshShellResize: (shellId: string, cols: number, rows: number) => Promise<IPCResponse>
sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) => Promise<IPCResponse>
sshForwardRemote: (connectionId: string, remotePort: number, localHost: string, localPort: number) => Promise<IPCResponse>
sshForwardDynamic: (connectionId: string, localPort: number) => Promise<IPCResponse>
sshConnect: (config: import('../shared/types/ipc.types').SSHConnectRequest) => Promise<import('../shared/types/ipc.types').SSHConnectResponse>
sshDisconnect: (connectionId: string) => Promise<{ success: boolean; error?: string }>
sshExec: (connectionId: string, command: string) => Promise<{ success: boolean; result?: string; error?: string }>
sshCreateShell: (connectionId: string, options?: import('../shared/types/ipc.types').SSHCreateShellRequest, shellId?: string) => Promise<{ success: boolean; shellId?: string; error?: string }>
sshShellWrite: (shellId: string, data: string | Uint8Array) => Promise<{ success: boolean; error?: string }>
sshShellResize: (shellId: string, cols: number, rows: number) => Promise<{ success: boolean; error?: string }>
sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) => Promise<import('../shared/types/ipc.types').SSHForwardResponse>
sshForwardRemote: (connectionId: string, remotePort: number, localHost: string, localPort: number) => Promise<import('../shared/types/ipc.types').SSHForwardResponse>
sshForwardDynamic: (connectionId: string, localPort: number) => Promise<import('../shared/types/ipc.types').SSHForwardResponse>
// Sessions
sessionGetAll: () => Promise<IPCResponse>
sessionGetById: (id: string) => Promise<IPCResponse>
sessionCreate: (session: Partial<Session>) => Promise<IPCResponse>
sessionUpdate: (id: string, updates: Partial<Session>) => Promise<IPCResponse>
sessionDelete: (id: string) => Promise<IPCResponse>
sessionGetAll: () => Promise<{ success: boolean; sessions?: import('../shared/types/session.types').Session[]; error?: string }>
sessionGetById: (id: string) => Promise<{ success: boolean; session?: import('../shared/types/session.types').Session; error?: string }>
sessionCreate: (session: Omit<import('../shared/types/session.types').Session, 'id'>) => Promise<import('../shared/types/ipc.types').SessionCreateResponse>
sessionUpdate: (id: string, updates: Partial<import('../shared/types/session.types').Session>) => Promise<import('../shared/types/ipc.types').SessionUpdateResponse>
sessionDelete: (id: string) => Promise<{ success: boolean; error?: string }>
// SFTP
sftpConnect: (connectionId: string) => Promise<IPCResponse>
sftpListDirectory: (sftpId: string, path: string) => Promise<IPCResponse>
sftpUpload: (sftpId: string, localPath: string, remotePath: string) => Promise<IPCResponse>
sftpDownload: (sftpId: string, remotePath: string, localPath: string) => Promise<IPCResponse>
sftpRename: (sftpId: string, oldPath: string, newPath: string) => Promise<IPCResponse>
sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => Promise<IPCResponse>
sftpMkdir: (sftpId: string, path: string) => Promise<IPCResponse>
sftpStat: (sftpId: string, filePath: string) => Promise<IPCResponse & {
stats?: {
size: number
mode: number
mtime: number
atime: number
isDirectory: boolean
isFile: boolean
isSymbolicLink: boolean
}
}>
sftpChmod: (sftpId: string, filePath: string, mode: number) => Promise<IPCResponse>
sftpConnect: (connectionId: string) => Promise<{ success: boolean; sftpId?: string; error?: string }>
sftpListDirectory: (sftpId: string, path: string) => Promise<{ success: boolean; files?: unknown[]; error?: string }>
sftpUpload: (sftpId: string, localPath: string, remotePath: string) => Promise<{ success: boolean; error?: string }>
sftpDownload: (sftpId: string, remotePath: string, localPath: string) => Promise<{ success: boolean; error?: string }>
sftpRename: (sftpId: string, oldPath: string, newPath: string) => Promise<{ success: boolean; error?: string }>
sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => Promise<{ success: boolean; error?: string }>
sftpMkdir: (sftpId: string, path: string) => Promise<{ success: boolean; error?: string }>
sftpStat: (sftpId: string, filePath: string) => Promise<{ success: boolean; stats?: unknown; error?: string }>
sftpChmod: (sftpId: string, filePath: string, mode: number) => Promise<{ success: boolean; error?: string }>
// Settings
settingsGet: (key: string) => Promise<IPCResponse>
settingsSet: (key: string, value: any) => Promise<IPCResponse>
settingsGet: (key: string) => Promise<{ success: boolean; value?: unknown; error?: string }>
settingsSet: (key: string, value: unknown) => Promise<{ success: boolean; error?: string }>
// Dialogs
showSaveDialog: (options: any) => Promise<{ canceled: boolean; filePath?: string }>
showOpenDialog: (options: any) => Promise<{ canceled: boolean; filePaths?: string[] }>
showSaveDialog: (options: import('../shared/types/ipc.types').DialogOptions) => Promise<import('../shared/types/ipc.types').DialogResult>
showOpenDialog: (options: import('../shared/types/ipc.types').DialogOptions) => Promise<import('../shared/types/ipc.types').DialogResult>
// File operations
readFile: (filePath: string) => Promise<{ success: boolean; data?: string; error?: string }>
@@ -96,23 +46,27 @@ interface ElectronAPI {
// Local filesystem
getHomePath: () => Promise<{ success: boolean; path?: string; error?: string }>
getDownloadsPath: () => Promise<string>
localReadDir: (dirPath: string) => Promise<IPCResponse>
localGetStats: (filePath: string) => Promise<IPCResponse>
localDelete: (filePath: string) => Promise<IPCResponse>
localRename: (oldPath: string, newPath: string) => Promise<IPCResponse>
localMkdir: (dirPath: string) => Promise<IPCResponse>
localReadDir: (dirPath: string) => Promise<unknown>
localGetStats: (filePath: string) => Promise<unknown>
localDelete: (filePath: string) => Promise<unknown>
localRename: (oldPath: string, newPath: string) => Promise<unknown>
localMkdir: (dirPath: string) => Promise<unknown>
// App
getVersion: () => Promise<string>
// Window
toggleMaximize: () => Promise<void>
// Events
onMenuNewSession: (callback: () => void) => void
onMenuQuickConnect: (callback: () => void) => void
onMenuImportSessions: (callback: () => void) => void
onMenuExportSessions: (callback: () => void) => void
onShellData: (callback: (event: any, connectionId: string, shellId: string, data: Buffer) => void) => void
onSFTPProgress: (callback: (event: any, sftpId: string, progress: any) => void) => void
removeListener: (channel: string, callback: (...args: any[]) => void) => void
onMenuOpenSettings: (callback: () => void) => void
onShellData: (callback: (event: Electron.IpcRendererEvent, connectionId: string, shellId: string, data: Buffer) => void) => void
onSFTPProgress: (callback: (event: Electron.IpcRendererEvent, sftpId: string, progress: { transferred: number; total: number; percentage: number }) => void) => void
removeListener: (channel: string, callback: (...args: unknown[]) => void) => void
removeAllListeners: (channel: string) => void
}