feat: v0.3.0 版本 - SSH终端、SFTP、端口转发功能实现

核心功能:
- SSH终端: 密码/密钥认证、XTerm.js终端、多主题配色、IP高亮
- SFTP管理: 双栏文件管理、上传下载、传输队列
- 端口转发: 本地/远程转发、SOCKS5动态代理、规则管理

架构改进:
- 拆分大型组件 (SFTPView/TabBar/Sidebar)
- 添加统一日志和错误处理
- 修复React严格模式下的终端黑屏问题

UI更新:
- 新增端口转发视图
- 更新UI预览文档
- 左侧栏右键菜单支持SFTP快捷打开
This commit is contained in:
Logan.Li committed 2026-04-06 01:20:47 +08:00
1 parent cf1c054381
commit 484d850abd
105 files changed
+25330 -64

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
/**
* App IPC Handlers
* 处理应用级别信息
*/
import { ipcMain, IpcMainInvokeEvent, app } from 'electron'
// IPC channel names
export const APP_CHANNELS = {
GET_VERSION: 'app:getVersion',
GET_DOWNLOADS_PATH: 'app:getDownloadsPath',
} as const
export function registerAppIPCHandlers(): void {
// Get app version
ipcMain.handle(APP_CHANNELS.GET_VERSION, () => {
return app.getVersion()
})
// Get downloads directory path
ipcMain.handle(APP_CHANNELS.GET_DOWNLOADS_PATH, () => {
return app.getPath('downloads')
})
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Dialog IPC Handlers
* 处理系统对话框
*/
import { ipcMain, IpcMainInvokeEvent, dialog } from 'electron'
// IPC channel names
export const DIALOG_CHANNELS = {
SHOW_SAVE: 'dialog:showSaveDialog',
SHOW_OPEN: 'dialog:showOpenDialog',
} as const
export interface DialogIPCOptions {
mainWindowGetter: () => Electron.BrowserWindow | null
}
export function registerDialogIPCHandlers(options: DialogIPCOptions): void {
const { mainWindowGetter } = options
// Show save dialog
ipcMain.handle(DIALOG_CHANNELS.SHOW_SAVE, async (_event: IpcMainInvokeEvent, options: Electron.SaveDialogOptions) => {
const mainWindow = mainWindowGetter()
if (!mainWindow) {
return { canceled: true }
}
return dialog.showSaveDialog(mainWindow, options)
})
// Show open dialog
ipcMain.handle(DIALOG_CHANNELS.SHOW_OPEN, async (_event: IpcMainInvokeEvent, options: Electron.OpenDialogOptions) => {
const mainWindow = mainWindowGetter()
if (!mainWindow) {
return { canceled: true }
}
return dialog.showOpenDialog(mainWindow, options)
})
}
+161
View File
@@ -0,0 +1,161 @@
/**
* File IPC Handlers
* 处理本地文件操作和远程文件读写
*/
import { ipcMain, IpcMainInvokeEvent, app } from 'electron'
import fs from 'fs'
import path from 'path'
// IPC channel names
export const FILE_CHANNELS = {
// Remote file operations (for zmodem)
READ: 'file:read',
WRITE: 'file:write',
// Local file system operations
LOCAL_GET_HOME: 'local:getHome',
LOCAL_READ_DIR: 'local:readDir',
LOCAL_GET_STATS: 'local:getStats',
LOCAL_DELETE: 'local:delete',
LOCAL_RENAME: 'local:rename',
LOCAL_MKDIR: 'local:mkdir',
} as const
export function registerFileIPCHandlers(): void {
// Read file (for rz)
ipcMain.handle(FILE_CHANNELS.READ, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const data = fs.readFileSync(filePath)
return { success: true, data: data.toString('base64') }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Write file (for sz)
ipcMain.handle(FILE_CHANNELS.WRITE, async (_event: IpcMainInvokeEvent, filePath: string, data: string) => {
try {
const buffer = Buffer.from(data, 'base64')
fs.writeFileSync(filePath, buffer)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get user home directory
ipcMain.handle(FILE_CHANNELS.LOCAL_GET_HOME, async () => {
try {
const homePath = app.getPath('home')
return { success: true, path: homePath }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Read directory contents
ipcMain.handle(FILE_CHANNELS.LOCAL_READ_DIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
const files = entries.map((entry) => {
const fullPath = path.join(dirPath, entry.name)
let stats: fs.Stats | null = null
try {
stats = fs.lstatSync(fullPath)
} catch {
// Skip files we can't stat
}
return {
name: entry.name,
path: fullPath,
type: entry.isDirectory() ? 'directory' : entry.isSymbolicLink() ? 'symlink' : 'file',
size: stats?.size || 0,
modified: stats?.mtime ? Math.floor(stats.mtime.getTime() / 1000) : 0,
permissions: formatPermissions(stats?.mode || 0),
}
})
// Sort: directories first, then by name
files.sort((a, b) => {
if (a.type !== 'file' && b.type === 'file') return -1
if (a.type === 'file' && b.type !== 'file') return 1
return a.name.localeCompare(b.name)
})
return { success: true, files }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get file stats
ipcMain.handle(FILE_CHANNELS.LOCAL_GET_STATS, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const stats = fs.lstatSync(filePath)
return {
success: true,
stats: {
name: path.basename(filePath),
path: filePath,
type: stats.isDirectory() ? 'directory' : stats.isSymbolicLink() ? 'symlink' : 'file',
size: stats.size,
modified: Math.floor(stats.mtime.getTime() / 1000),
permissions: formatPermissions(stats.mode),
},
}
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Delete file or directory
ipcMain.handle(FILE_CHANNELS.LOCAL_DELETE, async (_event: IpcMainInvokeEvent, filePath: string) => {
try {
const stats = fs.lstatSync(filePath)
if (stats.isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true })
} else {
fs.unlinkSync(filePath)
}
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Rename file or directory
ipcMain.handle(FILE_CHANNELS.LOCAL_RENAME, async (_event: IpcMainInvokeEvent, oldPath: string, newPath: string) => {
try {
fs.renameSync(oldPath, newPath)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Create directory
ipcMain.handle(FILE_CHANNELS.LOCAL_MKDIR, async (_event: IpcMainInvokeEvent, dirPath: string) => {
try {
fs.mkdirSync(dirPath, { recursive: true })
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
}
// Helper function to format Unix permissions
function formatPermissions(mode: number): string {
const perms: string[] = []
const types = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
// Owner
perms.push(types[(mode >> 6) & 7])
// Group
perms.push(types[(mode >> 3) & 7])
// Other
perms.push(types[mode & 7])
return perms.join('')
}
+68
View File
@@ -0,0 +1,68 @@
/**
* IPC Handler Registration
* 统一注册所有 IPC 处理器
*/
import type { SSHService } from '../services/ssh-service'
import type { DatabaseService } from '../services/database-service'
import type { ZmodemService } from '../services/zmodem-service'
import type { BrowserWindow } from 'electron'
import { createLogger } from '../utils/logger'
const logger = createLogger('IPC')
import { registerSSHIPCHandlers } from './ssh.ipc'
import { registerSFTPIPCHandlers } from './sftp.ipc'
import { registerSessionIPCHandlers } from './session.ipc'
import { registerSettingsIPCHandlers } from './settings.ipc'
import { registerDialogIPCHandlers } from './dialog.ipc'
import { registerFileIPCHandlers } from './file.ipc'
import { registerZmodemIPCHandlers } from './zmodem.ipc'
import { registerAppIPCHandlers } from './app.ipc'
export interface IPCOptions {
sshService: SSHService
databaseService: DatabaseService
zmodemService: ZmodemService
mainWindowGetter: () => BrowserWindow | null
}
export function registerAllIPCHandlers(options: IPCOptions): void {
const { sshService, databaseService, zmodemService, mainWindowGetter } = options
// SSH handlers
registerSSHIPCHandlers({ sshService, zmodemService, mainWindowGetter })
// SFTP handlers
registerSFTPIPCHandlers({ sshService, mainWindowGetter })
// Session handlers
registerSessionIPCHandlers({ dbService: databaseService })
// Settings handlers
registerSettingsIPCHandlers({ dbService: databaseService })
// Dialog handlers
registerDialogIPCHandlers({ mainWindowGetter })
// File handlers
registerFileIPCHandlers()
// Zmodem handlers
registerZmodemIPCHandlers({ sshService, zmodemService, mainWindowGetter })
// App handlers
registerAppIPCHandlers()
logger.info('[IPC] All handlers registered')
}
// Re-export channel constants for external use
export { SSH_CHANNELS } from './ssh.ipc'
export { SFTP_CHANNELS } from './sftp.ipc'
export { SESSION_CHANNELS } from './session.ipc'
export { SETTINGS_CHANNELS } from './settings.ipc'
export { DIALOG_CHANNELS } from './dialog.ipc'
export { FILE_CHANNELS } from './file.ipc'
export { ZMODEM_CHANNELS } from './zmodem.ipc'
export { APP_CHANNELS } from './app.ipc'
+74
View File
@@ -0,0 +1,74 @@
/**
* Session IPC Handlers
* 处理会话的 CRUD 操作
*/
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import type { DatabaseService } from '../services/database-service'
// IPC channel names
export const SESSION_CHANNELS = {
CREATE: 'session:create',
GET_ALL: 'session:getAll',
GET_BY_ID: 'session:getById',
UPDATE: 'session:update',
DELETE: 'session:delete',
} as const
export interface SessionIPCOptions {
dbService: DatabaseService
}
export function registerSessionIPCHandlers(options: SessionIPCOptions): void {
const { dbService } = options
// Create session
ipcMain.handle(SESSION_CHANNELS.CREATE, async (_event: IpcMainInvokeEvent, session) => {
try {
const result = dbService.createSession(session)
return { success: true, session: result }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get all sessions
ipcMain.handle(SESSION_CHANNELS.GET_ALL, async () => {
try {
const sessions = dbService.getAllSessions()
return { success: true, sessions }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Get session by ID
ipcMain.handle(SESSION_CHANNELS.GET_BY_ID, async (_event: IpcMainInvokeEvent, id: string) => {
try {
const session = dbService.getSessionById(id)
return { success: true, session }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Update session
ipcMain.handle(SESSION_CHANNELS.UPDATE, async (_event: IpcMainInvokeEvent, id: string, updates: any) => {
try {
const result = dbService.updateSession(id, updates)
return { success: true, session: result }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Delete session
ipcMain.handle(SESSION_CHANNELS.DELETE, async (_event: IpcMainInvokeEvent, id: string) => {
try {
dbService.deleteSession(id)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Settings IPC Handlers
* 处理应用设置读写
*/
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import type { DatabaseService } from '../services/database-service'
// IPC channel names
export const SETTINGS_CHANNELS = {
GET: 'settings:get',
SET: 'settings:set',
} as const
export interface SettingsIPCOptions {
dbService: DatabaseService
}
export function registerSettingsIPCHandlers(options: SettingsIPCOptions): void {
const { dbService } = options
// Get setting
ipcMain.handle(SETTINGS_CHANNELS.GET, async (_event: IpcMainInvokeEvent, key: string) => {
try {
const value = dbService.getSetting(key)
return { success: true, value }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// Set setting
ipcMain.handle(SETTINGS_CHANNELS.SET, async (_event: IpcMainInvokeEvent, key: string, value: unknown) => {
try {
dbService.setSetting(key, value)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
}
+196
View File
@@ -0,0 +1,196 @@
/**
* SFTP IPC Handlers
* 处理 SFTP 连接和文件操作相关 IPC
*/
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import type { SSHService } from '../services/ssh-service'
// IPC channel names
export const SFTP_CHANNELS = {
CONNECT: 'sftp:connect',
LIST_DIRECTORY: 'sftp:listDirectory',
UPLOAD: 'sftp:upload',
DOWNLOAD: 'sftp:download',
RENAME: 'sftp:rename',
DELETE: 'sftp:delete',
MKDIR: 'sftp:mkdir',
PROGRESS: 'sftp:progress',
} as const
export interface SFTPIPCOptions {
sshService: SSHService
mainWindowGetter: () => Electron.BrowserWindow | null
}
// ===== 彩色调试日志 =====
const DEBUG = true
function logHeader(msg: string) {
console.log(`%c${msg}`, 'background: #16a085; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;')
}
function logInfo(label: string, value: any) {
console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #1abc9c; font-weight: bold;')
}
function logSuccess(msg: string) {
console.log(`%c✓ ${msg}`, 'color: #27ae60; font-weight: bold;')
}
function logError(msg: string) {
console.log(`%c✗ ${msg}`, 'color: #e74c3c; font-weight: bold;')
}
export function registerSFTPIPCHandlers(options: SFTPIPCOptions): void {
const { sshService, mainWindowGetter } = options
// SFTP connect
ipcMain.handle(SFTP_CHANNELS.CONNECT, async (_event: IpcMainInvokeEvent, connectionId: string) => {
logHeader('📁 SFTP IPC CONNECT')
logInfo('connectionId', connectionId)
try {
const sftp = await sshService.createSFTP(connectionId)
logSuccess(`SFTP connected: ${sftp.id}`)
return { success: true, sftpId: sftp.id }
} catch (error: any) {
logError(`SFTP connect failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP list directory
ipcMain.handle(SFTP_CHANNELS.LIST_DIRECTORY, async (_event: IpcMainInvokeEvent, sftpId: string, dirPath: string) => {
logHeader('📁 SFTP IPC LIST')
logInfo('sftpId', sftpId)
logInfo('dirPath', dirPath)
try {
const files = await sshService.sftpListDirectory(sftpId, dirPath)
logSuccess(`Listed ${files.length} items`)
return { success: true, files }
} catch (error: any) {
logError(`List failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP upload file
ipcMain.handle(SFTP_CHANNELS.UPLOAD, async (
_event: IpcMainInvokeEvent,
sftpId: string,
localPath: string,
remotePath: string
) => {
logHeader('📤 SFTP IPC UPLOAD')
logInfo('sftpId', sftpId)
logInfo('localPath', localPath)
logInfo('remotePath', remotePath)
try {
const mainWindow = mainWindowGetter()
await sshService.sftpUpload(sftpId, localPath, remotePath, (transferred, total) => {
mainWindow?.webContents.send(SFTP_CHANNELS.PROGRESS, sftpId, {
transferred,
total,
percentage: Math.round((transferred / total) * 100),
})
})
logSuccess('Upload complete')
return { success: true }
} catch (error: any) {
logError(`Upload failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP download file
ipcMain.handle(SFTP_CHANNELS.DOWNLOAD, async (
_event: IpcMainInvokeEvent,
sftpId: string,
remotePath: string,
localPath: string
) => {
logHeader('📥 SFTP IPC DOWNLOAD')
logInfo('sftpId', sftpId)
logInfo('remotePath', remotePath)
logInfo('localPath', localPath)
try {
const mainWindow = mainWindowGetter()
await sshService.sftpDownload(sftpId, remotePath, localPath, (transferred, total) => {
mainWindow?.webContents.send(SFTP_CHANNELS.PROGRESS, sftpId, {
transferred,
total,
percentage: Math.round((transferred / total) * 100),
})
})
logSuccess('Download complete')
return { success: true }
} catch (error: any) {
logError(`Download failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP rename
ipcMain.handle(SFTP_CHANNELS.RENAME, async (
_event: IpcMainInvokeEvent,
sftpId: string,
oldPath: string,
newPath: string
) => {
logHeader('📁 SFTP IPC RENAME')
logInfo('sftpId', sftpId)
logInfo('oldPath', oldPath)
logInfo('newPath', newPath)
try {
await sshService.sftpRename(sftpId, oldPath, newPath)
logSuccess('Rename complete')
return { success: true }
} catch (error: any) {
logError(`Rename failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP delete
ipcMain.handle(SFTP_CHANNELS.DELETE, async (
_event: IpcMainInvokeEvent,
sftpId: string,
filePath: string,
isDirectory: boolean
) => {
logHeader('📁 SFTP IPC DELETE')
logInfo('sftpId', sftpId)
logInfo('filePath', filePath)
logInfo('isDirectory', isDirectory)
try {
await sshService.sftpDelete(sftpId, filePath, isDirectory)
logSuccess('Delete complete')
return { success: true }
} catch (error: any) {
logError(`Delete failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SFTP mkdir
ipcMain.handle(SFTP_CHANNELS.MKDIR, async (
_event: IpcMainInvokeEvent,
sftpId: string,
dirPath: string
) => {
logHeader('📁 SFTP IPC MKDIR')
logInfo('sftpId', sftpId)
logInfo('dirPath', dirPath)
try {
await sshService.sftpMkdir(sftpId, dirPath)
logSuccess('Mkdir complete')
return { success: true }
} catch (error: any) {
logError(`Mkdir failed: ${error.message}`)
return { success: false, error: error.message }
}
})
}
+222
View File
@@ -0,0 +1,222 @@
/**
* SSH IPC Handlers
* 处理 SSH 连接、Shell、会话、端口转发相关 IPC
*/
import { ipcMain, IpcMainInvokeEvent } from 'electron'
import type { SSHService } from '../services/ssh-service'
import type { ZmodemService } from '../services/zmodem-service'
import { createLogger } from '../utils/logger'
const logger = createLogger('SSH_IPC')
// ===== 彩色调试日志 =====
const DEBUG = true
function debug(...args: any[]) {
if (DEBUG) console.log('[SSH_IPC]', ...args)
}
function logHeader(msg: string) {
console.log(`%c${msg}`, 'background: #9b59b6; color: #fff; padding: 4px 8px; border-radius: 4px; font-weight: bold;')
}
function logInfo(label: string, value: any) {
console.log(`%c${label}:%c ${value}`, 'background: #2c3e50; color: #fff; padding: 2px 6px; border-radius: 3px;', 'color: #3498db; font-weight: bold;')
}
function logSuccess(msg: string) {
console.log(`%c✓ ${msg}`, 'color: #27ae60; font-weight: bold;')
}
function logError(msg: string) {
console.log(`%c✗ ${msg}`, 'color: #e74c3c; font-weight: bold;')
}
// IPC channel names
export const SSH_CHANNELS = {
CONNECT: 'ssh:connect',
DISCONNECT: 'ssh:disconnect',
EXEC: 'ssh:exec',
CREATE_SHELL: 'ssh:createShell',
SHELL_WRITE: 'ssh:shellWrite',
SHELL_RESIZE: 'ssh:shellResize',
FORWARD_LOCAL: 'ssh:forwardLocal',
FORWARD_REMOTE: 'ssh:forwardRemote',
FORWARD_DYNAMIC: 'ssh:forwardDynamic',
SHELL_DATA: 'ssh:shellData',
} as const
export interface SSHIPCOptions {
sshService: SSHService
zmodemService: ZmodemService
mainWindowGetter: () => Electron.BrowserWindow | null
}
export function registerSSHIPCHandlers(options: SSHIPCOptions): void {
const { sshService, zmodemService, mainWindowGetter } = options
// SSH connect
ipcMain.handle(SSH_CHANNELS.CONNECT, async (_event: IpcMainInvokeEvent, config) => {
try {
const connection = await sshService.connect(config)
return { success: true, connectionId: connection.id }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// SSH disconnect
ipcMain.handle(SSH_CHANNELS.DISCONNECT, async (_event: IpcMainInvokeEvent, connectionId: string) => {
try {
await sshService.disconnect(connectionId)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// SSH exec command
ipcMain.handle(SSH_CHANNELS.EXEC, async (_event: IpcMainInvokeEvent, connectionId: string, command: string) => {
try {
const result = await sshService.exec(connectionId, command)
return { success: true, result }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// SSH create shell
ipcMain.handle(SSH_CHANNELS.CREATE_SHELL, async (event: IpcMainInvokeEvent, connectionId: string, options?: { cols?: number; rows?: number }, shellId?: string) => {
console.log('=== SSH_IPC CREATE_SHELL ===')
console.log(' connectionId:', connectionId)
console.log(' options:', options)
console.log(' preferredShellId:', shellId)
try {
const preferredShellId = shellId
logger.info('[SSH_IPC] Creating shell for connection:', connectionId, 'preferredId:', preferredShellId)
debug('>>> Calling sshService.createShell...')
const shell = await sshService.createShell(connectionId, options || {}, (shellId, data) => {
// 检查 sender 是否已被销毁
if (event.sender.isDestroyed()) {
console.log('=== SHELL DATA CALLBACK - SKIPPED (sender destroyed) ===')
return
}
const text = data.toString('utf-8')
console.log('=== SHELL DATA CALLBACK ===')
console.log(' shellId:', shellId)
console.log(' data length:', data.length)
console.log(' data preview:', text.substring(0, 100).replace(/\n/g, '\\n'))
// 打印所有收到的数据前100字符用于调试
if (text.length > 0) {
logger.info(`[SSH_IPC] Shell data from ${shellId}:`, text.substring(0, 100).replace(/\n/g, '\\n'))
}
try {
console.log('>>> Sending SHELL_DATA to renderer...')
event.sender.send(SSH_CHANNELS.SHELL_DATA, connectionId, shellId, data)
console.log('>>> SHELL_DATA sent successfully')
} catch (err) {
console.error('>>> FAILED to send SHELL_DATA:', err)
}
zmodemService.feedShellData(shellId, data)
}, preferredShellId)
logger.info('[SSH_IPC] Shell created:', shell.id)
debug('>>> Shell created successfully:', shell.id)
return { success: true, shellId: shell.id }
} catch (error: any) {
logger.error('[SSH_IPC] Shell creation failed:', error)
debug('>>> Shell creation FAILED:', error.message)
return { success: false, error: error.message }
}
})
// SSH shell write
ipcMain.handle(SSH_CHANNELS.SHELL_WRITE, async (_event: IpcMainInvokeEvent, shellId: string, data: string) => {
try {
sshService.shellWrite(shellId, data)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// SSH shell resize
ipcMain.handle(SSH_CHANNELS.SHELL_RESIZE, async (_event: IpcMainInvokeEvent, shellId: string, cols: number, rows: number) => {
try {
sshService.shellResize(shellId, cols, rows)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
// SSH local port forward
ipcMain.handle(SSH_CHANNELS.FORWARD_LOCAL, async (
_event: IpcMainInvokeEvent,
connectionId: string,
localPort: number,
remoteHost: string,
remotePort: number
) => {
logHeader('🔀 IPC FORWARD_LOCAL')
logInfo('connectionId', connectionId)
logInfo('localPort', localPort)
logInfo('remoteHost', remoteHost)
logInfo('remotePort', remotePort)
try {
const result = await sshService.forwardLocal(connectionId, localPort, remoteHost, remotePort)
logSuccess(`Local forward bound to port ${result.boundPort}`)
return { success: true, boundPort: result.boundPort }
} catch (error: any) {
logError(`Local forward failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SSH remote port forward
ipcMain.handle(SSH_CHANNELS.FORWARD_REMOTE, async (
_event: IpcMainInvokeEvent,
connectionId: string,
remotePort: number,
localHost: string,
localPort: number
) => {
logHeader('🔀 IPC FORWARD_REMOTE')
logInfo('connectionId', connectionId)
logInfo('remotePort', remotePort)
logInfo('localHost', localHost)
logInfo('localPort', localPort)
try {
const result = await sshService.forwardRemote(connectionId, remotePort, localHost, localPort)
logSuccess('Remote forward established')
return { success: true }
} catch (error: any) {
logError(`Remote forward failed: ${error.message}`)
return { success: false, error: error.message }
}
})
// SSH dynamic port forward (SOCKS5)
ipcMain.handle(SSH_CHANNELS.FORWARD_DYNAMIC, async (
_event: IpcMainInvokeEvent,
connectionId: string,
localPort: number
) => {
logHeader('🧦 IPC FORWARD_DYNAMIC (SOCKS5)')
logInfo('connectionId', connectionId)
logInfo('localPort', localPort)
try {
const result = await sshService.forwardDynamic(connectionId, localPort)
logSuccess(`SOCKS5 proxy started on port ${result.boundPort}`)
return { success: true, boundPort: result.boundPort }
} catch (error: any) {
logError(`Dynamic forward failed: ${error.message}`)
return { success: false, error: error.message }
}
})
}
+141
View File
@@ -0,0 +1,141 @@
/**
* Zmodem IPC Handlers
* 处理 rz/sz 文件传输
*/
import { ipcMain, IpcMainInvokeEvent, app, dialog } from 'electron'
import fs from 'fs'
import path from 'path'
import type { SSHService } from '../services/ssh-service'
import type { ZmodemService } from '../services/zmodem-service'
import { createLogger } from '../utils/logger'
const logger = createLogger('Zmodem_IPC')
// IPC channel names
export const ZMODEM_CHANNELS = {
SEND: 'zmodem:send',
RECEIVE: 'zmodem:receive',
CANCEL: 'zmodem:cancel',
PROGRESS: 'zmodem:progress',
COMPLETE: 'zmodem:complete',
} as const
export interface ZmodemIPCOptions {
sshService: SSHService
zmodemService: ZmodemService
mainWindowGetter: () => Electron.BrowserWindow | null
}
export function registerZmodemIPCHandlers(options: ZmodemIPCOptions): void {
const { sshService, zmodemService, mainWindowGetter } = options
// Zmodem send (rz) - send file to server
ipcMain.handle(ZMODEM_CHANNELS.SEND, async (_event: IpcMainInvokeEvent, shellId: string) => {
try {
const mainWindow = mainWindowGetter()
if (!mainWindow) {
return { success: false, error: 'Window not available' }
}
// Open file dialog to select file
const result = await dialog.showOpenDialog(mainWindow, {
title: '选择要发送的文件',
properties: ['openFile'],
})
if (result.canceled || !result.filePaths.length) {
return { success: false, canceled: true }
}
const filePath = result.filePaths[0]
const fileName = path.basename(filePath)
// Read file
const fileData = fs.readFileSync(filePath)
// Get shell write function
const shell = sshService.getShell(shellId)
if (!shell) {
return { success: false, error: 'Shell not found' }
}
// Send file via Zmodem protocol
const sessionId = zmodemService.sendFile(
shellId,
fileName,
fileData,
(data) => {
shell.channel.write(data)
},
(progress) => {
mainWindow.webContents.send(ZMODEM_CHANNELS.PROGRESS, sessionId, progress)
},
(success, error) => {
mainWindow.webContents.send(ZMODEM_CHANNELS.COMPLETE, sessionId, { success, error })
}
)
return { success: true, sessionId, fileName }
} catch (error: any) {
logger.error('Zmodem send error:', error)
return { success: false, error: error.message }
}
})
// Zmodem receive (sz) - receive file from server
ipcMain.handle(ZMODEM_CHANNELS.RECEIVE, async (_event: IpcMainInvokeEvent, shellId: string) => {
try {
const mainWindow = mainWindowGetter()
if (!mainWindow) {
return { success: false, error: 'Window not available' }
}
// Get shell to monitor for incoming data
const shell = sshService.getShell(shellId)
if (!shell) {
return { success: false, error: 'Shell not found' }
}
// Open save dialog to select destination
const result = await dialog.showSaveDialog(mainWindow, {
title: '保存文件',
defaultPath: app.getPath('downloads'),
})
if (result.canceled || !result.filePath) {
return { success: false, canceled: true }
}
// Start Zmodem receive session
const sessionId = zmodemService.receiveFile(
shellId,
result.filePath,
(data) => {
shell.channel.write(data)
},
(progress) => {
mainWindow.webContents.send(ZMODEM_CHANNELS.PROGRESS, sessionId, progress)
},
(success, error) => {
mainWindow.webContents.send(ZMODEM_CHANNELS.COMPLETE, sessionId, { success, error })
}
)
return { success: true, sessionId }
} catch (error: any) {
logger.error('Zmodem receive error:', error)
return { success: false, error: error.message }
}
})
// Zmodem cancel transfer
ipcMain.handle(ZMODEM_CHANNELS.CANCEL, async (_event: IpcMainInvokeEvent, sessionId: string) => {
try {
zmodemService.cancelSession(sessionId)
return { success: true }
} catch (error: any) {
return { success: false, error: error.message }
}
})
}