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
+73
-31
@@ -1,32 +1,64 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import type {
|
||||
SSHConnectRequest,
|
||||
SSHConnectResponse,
|
||||
SSHCreateShellRequest,
|
||||
SessionCreateResponse,
|
||||
SessionUpdateResponse,
|
||||
DialogOptions,
|
||||
DialogResult,
|
||||
ZmodemProgress,
|
||||
ZmodemComplete,
|
||||
} from '../shared/types/ipc.types'
|
||||
import type { Session } from '../shared/types/session.types'
|
||||
|
||||
// 暴露 API 到渲染进程
|
||||
const api = {
|
||||
// SSH 相关
|
||||
sshConnect: (config: any) => ipcRenderer.invoke('ssh:connect', config),
|
||||
sshDisconnect: (connectionId: string) => ipcRenderer.invoke('ssh:disconnect', connectionId),
|
||||
sshExec: (connectionId: string, command: string) => ipcRenderer.invoke('ssh:exec', connectionId, command),
|
||||
sshCreateShell: (connectionId: string, options?: any, shellId?: string) => ipcRenderer.invoke('ssh:createShell', connectionId, options, shellId),
|
||||
sshShellWrite: (shellId: string, data: string | Uint8Array) => ipcRenderer.invoke('ssh:shellWrite', shellId, data),
|
||||
sshShellResize: (shellId: string, cols: number, rows: number) => ipcRenderer.invoke('ssh:shellResize', shellId, cols, rows),
|
||||
sshConnect: (config: SSHConnectRequest): Promise<SSHConnectResponse> =>
|
||||
ipcRenderer.invoke('ssh:connect', config),
|
||||
sshDisconnect: (connectionId: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('ssh:disconnect', connectionId),
|
||||
sshExec: (connectionId: string, command: string): Promise<{ success: boolean; result?: string; error?: string }> =>
|
||||
ipcRenderer.invoke('ssh:exec', connectionId, command),
|
||||
sshCreateShell: (connectionId: string, options?: SSHCreateShellRequest, shellId?: string): Promise<{ success: boolean; shellId?: string; error?: string }> =>
|
||||
ipcRenderer.invoke('ssh:createShell', connectionId, options, shellId),
|
||||
sshShellWrite: (shellId: string, data: string | Uint8Array): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('ssh:shellWrite', shellId, data),
|
||||
sshShellResize: (shellId: string, cols: number, rows: number): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('ssh:shellResize', shellId, cols, rows),
|
||||
|
||||
// 会话管理
|
||||
sessionGetAll: () => ipcRenderer.invoke('session:getAll'),
|
||||
sessionGetById: (id: string) => ipcRenderer.invoke('session:getById', id),
|
||||
sessionCreate: (session: any) => ipcRenderer.invoke('session:create', session),
|
||||
sessionUpdate: (id: string, updates: any) => ipcRenderer.invoke('session:update', id, updates),
|
||||
sessionDelete: (id: string) => ipcRenderer.invoke('session:delete', id),
|
||||
sessionGetAll: (): Promise<{ success: boolean; sessions?: Session[]; error?: string }> =>
|
||||
ipcRenderer.invoke('session:getAll'),
|
||||
sessionGetById: (id: string): Promise<{ success: boolean; session?: Session; error?: string }> =>
|
||||
ipcRenderer.invoke('session:getById', id),
|
||||
sessionCreate: (session: Omit<Session, 'id'>): Promise<SessionCreateResponse> =>
|
||||
ipcRenderer.invoke('session:create', session),
|
||||
sessionUpdate: (id: string, updates: Partial<Session>): Promise<SessionUpdateResponse> =>
|
||||
ipcRenderer.invoke('session:update', id, updates),
|
||||
sessionDelete: (id: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('session:delete', id),
|
||||
|
||||
// SFTP
|
||||
sftpConnect: (connectionId: string) => ipcRenderer.invoke('sftp:connect', connectionId),
|
||||
sftpListDirectory: (sftpId: string, path: string) => ipcRenderer.invoke('sftp:listDirectory', sftpId, path),
|
||||
sftpUpload: (sftpId: string, localPath: string, remotePath: string) => ipcRenderer.invoke('sftp:upload', sftpId, localPath, remotePath),
|
||||
sftpDownload: (sftpId: string, remotePath: string, localPath: string) => ipcRenderer.invoke('sftp:download', sftpId, remotePath, localPath),
|
||||
sftpRename: (sftpId: string, oldPath: string, newPath: string) => ipcRenderer.invoke('sftp:rename', sftpId, oldPath, newPath),
|
||||
sftpDelete: (sftpId: string, path: string, isDirectory: boolean) => ipcRenderer.invoke('sftp:delete', sftpId, path, isDirectory),
|
||||
sftpMkdir: (sftpId: string, path: string) => ipcRenderer.invoke('sftp:mkdir', sftpId, path),
|
||||
sftpStat: (sftpId: string, filePath: string) => ipcRenderer.invoke('sftp:stat', sftpId, filePath),
|
||||
sftpChmod: (sftpId: string, filePath: string, mode: number) => ipcRenderer.invoke('sftp:chmod', sftpId, filePath, mode),
|
||||
sftpConnect: (connectionId: string): Promise<{ success: boolean; sftpId?: string; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:connect', connectionId),
|
||||
sftpListDirectory: (sftpId: string, path: string): Promise<{ success: boolean; files?: unknown[]; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:listDirectory', sftpId, path),
|
||||
sftpUpload: (sftpId: string, localPath: string, remotePath: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:upload', sftpId, localPath, remotePath),
|
||||
sftpDownload: (sftpId: string, remotePath: string, localPath: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:download', sftpId, remotePath, localPath),
|
||||
sftpRename: (sftpId: string, oldPath: string, newPath: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:rename', sftpId, oldPath, newPath),
|
||||
sftpDelete: (sftpId: string, path: string, isDirectory: boolean): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:delete', sftpId, path, isDirectory),
|
||||
sftpMkdir: (sftpId: string, path: string): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:mkdir', sftpId, path),
|
||||
sftpStat: (sftpId: string, filePath: string): Promise<{ success: boolean; stats?: unknown; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:stat', sftpId, filePath),
|
||||
sftpChmod: (sftpId: string, filePath: string, mode: number): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('sftp:chmod', sftpId, filePath, mode),
|
||||
|
||||
// 端口转发
|
||||
sshForwardLocal: (connectionId: string, localPort: number, remoteHost: string, remotePort: number) =>
|
||||
@@ -37,12 +69,16 @@ const api = {
|
||||
ipcRenderer.invoke('ssh:forwardDynamic', connectionId, localPort),
|
||||
|
||||
// 设置
|
||||
settingsGet: (key: string) => ipcRenderer.invoke('settings:get', key),
|
||||
settingsSet: (key: string, value: any) => ipcRenderer.invoke('settings:set', key, value),
|
||||
settingsGet: (key: string): Promise<{ success: boolean; value?: unknown; error?: string }> =>
|
||||
ipcRenderer.invoke('settings:get', key),
|
||||
settingsSet: (key: string, value: unknown): Promise<{ success: boolean; error?: string }> =>
|
||||
ipcRenderer.invoke('settings:set', key, value),
|
||||
|
||||
// 对话框
|
||||
showSaveDialog: (options: any) => ipcRenderer.invoke('dialog:showSaveDialog', options),
|
||||
showOpenDialog: (options: any) => ipcRenderer.invoke('dialog:showOpenDialog', options),
|
||||
showSaveDialog: (options: DialogOptions): Promise<DialogResult> =>
|
||||
ipcRenderer.invoke('dialog:showSaveDialog', options),
|
||||
showOpenDialog: (options: DialogOptions): Promise<DialogResult> =>
|
||||
ipcRenderer.invoke('dialog:showOpenDialog', options),
|
||||
|
||||
// 文件操作
|
||||
readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath),
|
||||
@@ -57,22 +93,25 @@ const api = {
|
||||
localRename: (oldPath: string, newPath: string) => ipcRenderer.invoke('local:rename', oldPath, newPath),
|
||||
localMkdir: (dirPath: string) => ipcRenderer.invoke('local:mkdir', dirPath),
|
||||
|
||||
// Zmodem rz/sz
|
||||
// Zmodem
|
||||
zmodemSend: (shellId: string) => ipcRenderer.invoke('zmodem:send', shellId),
|
||||
zmodemReceive: (shellId: string) => ipcRenderer.invoke('zmodem:receive', shellId),
|
||||
zmodemCancel: (sessionId: string) => ipcRenderer.invoke('zmodem:cancel', sessionId),
|
||||
|
||||
// Zmodem events
|
||||
onZmodemProgress: (callback: (event: any, sessionId: string, progress: any) => void) => {
|
||||
onZmodemProgress: (callback: (event: Electron.IpcRendererEvent, sessionId: string, progress: ZmodemProgress) => void) => {
|
||||
ipcRenderer.on('zmodem:progress', callback)
|
||||
},
|
||||
onZmodemComplete: (callback: (event: any, sessionId: string, result: any) => void) => {
|
||||
onZmodemComplete: (callback: (event: Electron.IpcRendererEvent, sessionId: string, result: ZmodemComplete) => void) => {
|
||||
ipcRenderer.on('zmodem:complete', callback)
|
||||
},
|
||||
|
||||
// 应用信息
|
||||
getVersion: () => ipcRenderer.invoke('app:getVersion'),
|
||||
|
||||
// Window
|
||||
toggleMaximize: () => ipcRenderer.invoke('window:toggleMaximize'),
|
||||
|
||||
// 事件监听
|
||||
onMenuNewSession: (callback: () => void) => {
|
||||
ipcRenderer.on('menu-new-session', callback)
|
||||
@@ -86,13 +125,16 @@ const api = {
|
||||
onMenuExportSessions: (callback: () => void) => {
|
||||
ipcRenderer.on('menu-export-sessions', callback)
|
||||
},
|
||||
onShellData: (callback: (event: any, connectionId: string, shellId: string, data: Buffer) => void) => {
|
||||
onMenuOpenSettings: (callback: () => void) => {
|
||||
ipcRenderer.on('menu-open-settings', callback)
|
||||
},
|
||||
onShellData: (callback: (event: Electron.IpcRendererEvent, connectionId: string, shellId: string, data: Buffer) => void) => {
|
||||
ipcRenderer.on('ssh:shellData', callback)
|
||||
},
|
||||
onSFTPProgress: (callback: (event: any, sftpId: string, progress: any) => void) => {
|
||||
onSFTPProgress: (callback: (event: Electron.IpcRendererEvent, sftpId: string, progress: { transferred: number; total: number; percentage: number }) => void) => {
|
||||
ipcRenderer.on('sftp:progress', callback)
|
||||
},
|
||||
removeListener: (channel: string, callback: (...args: any[]) => void) => {
|
||||
removeListener: (channel: string, callback: (...args: unknown[]) => void) => {
|
||||
ipcRenderer.removeListener(channel, callback)
|
||||
},
|
||||
removeAllListeners: (channel: string) => {
|
||||
|
||||
Reference in new issue
Block a user