// IPC 通道处理器 import { ipcMain, BrowserWindow, nativeTheme, dialog } from 'electron' import store, { type ConnectionConfig } from './store' import { encrypt, decrypt } from './credential' import * as redis from './redis' // Helper: wrap handler with command logging function withLog(cmd: string, fn: (...args: any[]) => any) { return async (_e: any, ...args: any[]) => { const start = Date.now() const connId = args[0] || 'unknown' try { const result = await fn(_e, ...args) redis.log(connId, cmd, Date.now() - start) return result } catch (err) { redis.log(connId, cmd, Date.now() - start) throw err } } } export function registerIpcHandlers(): void { // Window ipcMain.on('window:minimize', () => BrowserWindow.getFocusedWindow()?.minimize()) ipcMain.on('window:maximize', () => { const win = BrowserWindow.getFocusedWindow() if (win) win.isMaximized() ? win.unmaximize() : win.maximize() }) ipcMain.on('window:close', () => BrowserWindow.getFocusedWindow()?.close()) // Theme ipcMain.handle('theme:get', () => nativeTheme.shouldUseDarkColors) ipcMain.on('theme:set', (_e, theme: 'system' | 'dark' | 'light') => { nativeTheme.themeSource = theme }) // Dialog ipcMain.handle('dialog:openFile', async (_e, options: Electron.OpenDialogOptions) => { const win = BrowserWindow.getFocusedWindow() if (!win) return null const result = await dialog.showOpenDialog(win, options) if (result.canceled || result.filePaths.length === 0) return null const fs = await import('fs') try { return { path: result.filePaths[0], content: fs.readFileSync(result.filePaths[0], 'utf-8') } } catch { return null } }) // Credential ipcMain.handle('credential:encrypt', (_e, text: string) => encrypt(text)) ipcMain.handle('credential:decrypt', (_e, encoded: string) => decrypt(encoded)) // Storage - connections ipcMain.handle('storage:getConnections', async () => { const connections = store.get('connections', []) // Decrypt sensitive fields for (const conn of connections) { if (conn.auth && conn.auth.startsWith('enc:')) { try { conn.auth = await decrypt(conn.auth.slice(4)) } catch { /* keep as-is */ } } if (conn.sshPassword && conn.sshPassword.startsWith('enc:')) { try { conn.sshPassword = await decrypt(conn.sshPassword.slice(4)) } catch { /* keep as-is */ } } if (conn.sshPassphrase && conn.sshPassphrase.startsWith('enc:')) { try { conn.sshPassphrase = await decrypt(conn.sshPassphrase.slice(4)) } catch { /* keep as-is */ } } } return connections }) ipcMain.handle('storage:saveConnection', async (_e, conn: ConnectionConfig) => { // Encrypt sensitive fields before storing (create copy to avoid mutation) const encrypted: ConnectionConfig = { ...conn } if (encrypted.auth && !encrypted.auth.startsWith('enc:')) { encrypted.auth = 'enc:' + await encrypt(encrypted.auth) } if (encrypted.sshPassword && !encrypted.sshPassword.startsWith('enc:')) { encrypted.sshPassword = 'enc:' + await encrypt(encrypted.sshPassword) } if (encrypted.sshPassphrase && !encrypted.sshPassphrase.startsWith('enc:')) { encrypted.sshPassphrase = 'enc:' + await encrypt(encrypted.sshPassphrase) } const connections = store.get('connections', []) const idx = connections.findIndex((c: ConnectionConfig) => c.id === encrypted.id) if (idx >= 0) connections[idx] = encrypted else connections.push(encrypted) store.set('connections', connections) return connections }) ipcMain.handle('storage:deleteConnection', (_e, id: string) => { const connections = store.get('connections', []).filter((c: ConnectionConfig) => c.id !== id) store.set('connections', connections) return connections }) ipcMain.handle('storage:reorderConnections', (_e, ids: string[]) => { const connections = store.get('connections', []) const reordered = ids.map((id, i) => { const conn = connections.find((c: ConnectionConfig) => c.id === id) if (!conn) return null conn.order = i return conn }).filter((c): c is ConnectionConfig => c !== null) store.set('connections', reordered) return reordered }) // Storage - settings ipcMain.handle('storage:getSettings', () => store.get('settings')) ipcMain.handle('storage:saveSettings', (_e, settings) => { store.set('settings', settings) }) // Redis - 连接 ipcMain.handle('redis:connect', async (_e, id: string, opts) => { try { await redis.connect(id, opts) return { success: true } } catch (err: any) { return { success: false, error: err.message } } }) ipcMain.handle('redis:disconnect', async (_e, id: string) => { await redis.disconnect(id) }) ipcMain.handle('redis:ping', async (_e, id: string) => { return redis.ping(id) }) ipcMain.handle('redis:isConnected', (_e, id: string) => { return redis.isClientConnected(id) }) // Blocked dangerous commands — blocked in redis:execute but allowed via redis:executeUnsafe (CLI) const BLOCKED_COMMANDS = new Set([ 'FLUSHALL', 'FLUSHDB', 'SHUTDOWN', 'DEBUG', ]) // Redis - 服务器 (safe execute — blocks dangerous commands) ipcMain.handle('redis:execute', withLog('EXEC', async (_e, id: string, command: string, args: string[]) => { const cmdUpper = command.toUpperCase().split(/\s+/)[0] if (BLOCKED_COMMANDS.has(cmdUpper)) { throw new Error(`Command "${command}" is blocked for safety. Use CLI for administrative commands.`) } return redis.execute(id, command, args) })) // Redis - unsafe execute (bypasses block — for CLI use only) ipcMain.handle('redis:executeUnsafe', withLog('EXEC', async (_e, id: string, command: string, args: string[]) => { return redis.execute(id, command, args) })) ipcMain.handle('redis:getInfo', async (_e, id: string) => { return redis.getServerInfo(id) }) ipcMain.handle('redis:selectDb', withLog('SELECT', async (_e, id: string, db: number) => { await redis.selectDb(id, db) })) ipcMain.handle('redis:dbSize', async (_e, id: string) => { return redis.dbSize(id) }) ipcMain.handle('redis:slowLogGet', async (_e, id: string, count: number) => { return redis.slowLogGet(id, count) }) ipcMain.handle('redis:slowLogLen', async (_e, id: string) => { return redis.slowLogLen(id) }) // Redis - Key 操作 ipcMain.handle('redis:scanKeys', async (_e, id: string, cursor: string, pattern: string, count: number) => { return redis.scanKeys(id, cursor, pattern, count) }) ipcMain.handle('redis:getKeyType', async (_e, id: string, key: string) => { return redis.getKeyType(id, key) }) ipcMain.handle('redis:getKeyTTL', async (_e, id: string, key: string) => { return redis.getKeyTTL(id, key) }) ipcMain.handle('redis:setTTL', withLog('EXPIRE', async (_e, id: string, key: string, ttl: number) => { await redis.setKeyTTL(id, key, ttl) })) ipcMain.handle('redis:deleteKey', withLog('DEL', async (_e, id: string, key: string) => { await redis.deleteKey(id, key) })) ipcMain.handle('redis:renameKey', withLog('RENAME', async (_e, id: string, oldKey: string, newKey: string) => { await redis.renameKey(id, oldKey, newKey) })) ipcMain.handle('redis:existsKey', async (_e, id: string, key: string) => { return redis.existsKey(id, key) }) ipcMain.handle('redis:batchDelete', withLog('DEL batch', async (_e, id: string, keys: string[]) => { return redis.batchDelete(id, keys) })) ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => { return redis.memoryUsage(id, key) }) ipcMain.handle('redis:batchMemoryUsage', async (_e, id: string, keys: string[]) => { return redis.batchKeyMemoryUsage(id, keys) }) // Redis - String ipcMain.handle('redis:getString', async (_e, id: string, key: string) => { return redis.getStringValue(id, key) }) ipcMain.handle('redis:setString', withLog('SET', async (_e, id: string, key: string, value: string) => { await redis.setStringValue(id, key, value) })) // ReJson ipcMain.handle('redis:rejsonGet', async (_e, id: string, key: string, path: string) => { return redis.rejsonGet(id, key, path) }) ipcMain.handle('redis:rejsonSet', withLog('JSON.SET', async (_e, id: string, key: string, path: string, value: string) => { await redis.rejsonSet(id, key, path, value) })) // Redis - Hash ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => { return redis.getHashFields(id, key, cursor, count) }) ipcMain.handle('redis:hashSet', withLog('HSET', async (_e, id: string, key: string, field: string, value: string) => { await redis.setHashField(id, key, field, value) })) ipcMain.handle('redis:hashDel', withLog('HDEL', async (_e, id: string, key: string, field: string) => { await redis.deleteHashField(id, key, field) })) // Redis - List ipcMain.handle('redis:listRange', async (_e, id: string, key: string, start: number, stop: number) => { return redis.listRange(id, key, start, stop) }) ipcMain.handle('redis:listLength', async (_e, id: string, key: string) => { return redis.listLength(id, key) }) ipcMain.handle('redis:listPush', withLog('RPUSH', async (_e, id: string, key: string, values: string[]) => { return redis.listPush(id, key, ...values) })) ipcMain.handle('redis:listSet', withLog('LSET', async (_e, id: string, key: string, index: number, value: string) => { await redis.listSet(id, key, index, value) })) ipcMain.handle('redis:listRemove', withLog('LREM', async (_e, id: string, key: string, count: number, value: string) => { return redis.listRemove(id, key, count, value) })) // Redis - Set ipcMain.handle('redis:setMembers', async (_e, id: string, key: string) => { return redis.setMembers(id, key) }) ipcMain.handle('redis:setSize', async (_e, id: string, key: string) => { return redis.setSize(id, key) }) ipcMain.handle('redis:setAdd', withLog('SADD', async (_e, id: string, key: string, members: string[]) => { return redis.setAdd(id, key, ...members) })) ipcMain.handle('redis:setRemove', withLog('SREM', async (_e, id: string, key: string, members: string[]) => { return redis.setRemove(id, key, ...members) })) // Redis - Zset ipcMain.handle('redis:zsetRange', async (_e, id: string, key: string, start: number, stop: number, withScores: boolean) => { return redis.zsetRange(id, key, start, stop, withScores) }) ipcMain.handle('redis:zsetSize', async (_e, id: string, key: string) => { return redis.zsetSize(id, key) }) ipcMain.handle('redis:zsetAdd', withLog('ZADD', async (_e, id: string, key: string, score: number, member: string) => { return redis.zsetAdd(id, key, score, member) })) ipcMain.handle('redis:zsetRemove', withLog('ZREM', async (_e, id: string, key: string, members: string[]) => { return redis.zsetRemove(id, key, ...members) })) // Redis - Stream ipcMain.handle('redis:streamInfo', async (_e, id: string, key: string) => { return redis.streamInfo(id, key) }) ipcMain.handle('redis:streamRange', async (_e, id: string, key: string, start: string, end: string, count: number) => { return redis.streamRange(id, key, start, end, count) }) ipcMain.handle('redis:streamAdd', withLog('XADD', async (_e, id: string, key: string, streamId: string, fieldValues: string[]) => { return redis.streamAdd(id, key, streamId, fieldValues) })) ipcMain.handle('redis:streamTrim', withLog('XTRIM', async (_e, id: string, key: string, maxLen: number) => { return redis.streamTrim(id, key, maxLen) })) ipcMain.handle('redis:streamDel', withLog('XDEL', async (_e, id: string, key: string, ids: string[]) => { return redis.streamDel(id, key, ...ids) })) // Redis - Stream Consumer Groups ipcMain.handle('redis:streamGroups', async (_e, id: string, key: string) => { return redis.streamGroups(id, key) }) ipcMain.handle('redis:streamConsumers', async (_e, id: string, key: string, group: string) => { return redis.streamConsumers(id, key, group) }) ipcMain.handle('redis:streamGroupCreate', withLog('XGROUP CREATE', async (_e, id: string, key: string, group: string, startId: string, mkstream: boolean) => { return redis.streamGroupCreate(id, key, group, startId, mkstream) })) ipcMain.handle('redis:streamGroupDestroy', withLog('XGROUP DESTROY', async (_e, id: string, key: string, group: string) => { return redis.streamGroupDestroy(id, key, group) })) ipcMain.handle('redis:streamAck', withLog('XACK', async (_e, id: string, key: string, group: string, ids: string[]) => { return redis.streamAck(id, key, group, ...ids) })) // Command Log ipcMain.handle('redis:getCommandLog', () => { return redis.getLog() }) ipcMain.handle('redis:clearCommandLog', () => { redis.clearLog() }) // Format decode ipcMain.handle('redis:decodeValue', (_e, value: string, format: string) => { return redis.decodeValue(value, format as any) }) ipcMain.handle('redis:detectFormat', (_e, value: string) => { return redis.detectFormat(value) }) ipcMain.handle('redis:customFormat', (_e, value: string, command: string, key: string) => { return redis.customFormat(value, command, key) }) // Pub/Sub ipcMain.handle('redis:subscribe', async (_e, id: string, channels: string[], isPattern: boolean) => { try { redis.startSubscribe(id, channels, isPattern, _e.sender) return { success: true } } catch (err: any) { return { success: false, error: err.message } } }) ipcMain.handle('redis:unsubscribe', async (_e, id: string) => { redis.stopSubscribe(id) }) // Monitor ipcMain.handle('redis:monitor', async (_e, id: string) => { try { redis.startMonitor(id, _e.sender) return { success: true } } catch (err: any) { return { success: false, error: err.message } } }) ipcMain.handle('redis:monitorStop', async (_e, id: string) => { redis.stopMonitor(id) }) }