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:
1 parent
37e65be823
commit
514d62644e
77 files changed
+2563
-931
No files matched your search
Vendored
+40
-86
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user