import { app, BrowserWindow, ipcMain, shell, dialog } from 'electron'; import * as path from 'path'; import * as fs from 'fs'; import * as crypto from 'crypto'; import express from 'express'; import { GrpcClient } from './modules/grpc-client'; import { DatabaseManager } from './modules/database'; import { addressDerivationService, CHAIN_CONFIGS } from './modules/address-derivation'; import { KavaTxService, KAVA_MAINNET_TX_CONFIG, KAVA_TESTNET_TX_CONFIG } from './modules/kava-tx-service'; import { AccountClient } from './modules/account-client'; import { TSSHandler, MockTSSHandler, KeygenResult } from './modules/tss-handler'; // 内置 HTTP 服务器端口 const HTTP_PORT = 3456; // 是否使用 Mock TSS Handler (开发模式) // 注意:即使是开发模式,默认也使用真实的 TSS Handler // 只有显式设置 USE_MOCK_TSS=true 才会使用 Mock Handler const USE_MOCK_TSS = process.env.USE_MOCK_TSS === 'true'; // =========================================================================== // 调试日志系统 // =========================================================================== let debugLogEnabled = false; type LogLevel = 'info' | 'warn' | 'error' | 'debug'; type LogSource = 'main' | 'grpc' | 'tss' | 'account' | 'renderer' | 'kava'; function sendDebugLog(level: LogLevel, source: LogSource, message: string) { if (debugLogEnabled && mainWindow) { mainWindow.webContents.send('debug:log', { level, source, message }); } // 同时输出到控制台 const prefix = `[${source.toUpperCase()}]`; switch (level) { case 'error': console.error(prefix, message); break; case 'warn': console.warn(prefix, message); break; case 'debug': console.debug(prefix, message); break; default: console.log(prefix, message); } } // 创建日志辅助函数 const debugLog = { info: (source: LogSource, message: string) => sendDebugLog('info', source, message), warn: (source: LogSource, message: string) => sendDebugLog('warn', source, message), error: (source: LogSource, message: string) => sendDebugLog('error', source, message), debug: (source: LogSource, message: string) => sendDebugLog('debug', source, message), }; let mainWindow: BrowserWindow | null = null; let grpcClient: GrpcClient | null = null; let database: DatabaseManager | null = null; let httpServer: ReturnType | null = null; let kavaTxService: KavaTxService | null = null; let accountClient: AccountClient | null = null; let tssHandler: TSSHandler | MockTSSHandler | null = null; // 当前正在进行的 Keygen 会话信息 interface ActiveKeygenSession { sessionId: string; partyIndex: number; participants: Array<{ partyId: string; partyIndex: number; name: string }>; threshold: { t: number; n: number }; walletName: string; encryptionPassword: string; } let activeKeygenSession: ActiveKeygenSession | null = null; // Keygen 幂等性保护:追踪正在进行的 keygen 会话 ID let keygenInProgressSessionId: string | null = null; // =========================================================================== // Co-Sign 相关状态 // =========================================================================== // 当前正在进行的 Co-Sign 会话信息 interface ActiveCoSignSession { sessionId: string; partyIndex: number; participants: Array<{ partyId: string; partyIndex: number; name: string }>; threshold: { t: number; n: number }; walletName: string; messageHash: string; shareId: string; sharePassword: string; } let activeCoSignSession: ActiveCoSignSession | null = null; // Co-Sign 幂等性保护:追踪正在进行的签名会话 ID let signInProgressSessionId: string | null = null; // 会话事件缓存 - 解决前端订阅时可能错过事件的时序问题 // 当事件到达时,前端可能还在页面导航中,尚未订阅 interface SessionEventData { type: string; sessionId: string; thresholdN?: number; thresholdT?: number; selectedParties?: string[]; publicKey?: string; shareId?: string; allCompleted?: boolean; error?: string; } const sessionEventCache = new Map(); const SESSION_EVENT_CACHE_MAX_AGE = 60000; // 60秒后清理缓存 const sessionEventCacheTimestamps = new Map(); // 添加事件到缓存 function cacheSessionEvent(sessionId: string, event: SessionEventData) { if (!sessionEventCache.has(sessionId)) { sessionEventCache.set(sessionId, []); } sessionEventCache.get(sessionId)!.push(event); sessionEventCacheTimestamps.set(sessionId, Date.now()); // 清理过期缓存 cleanExpiredEventCache(); } // 获取并清除缓存的事件 function getAndClearCachedEvents(sessionId: string): SessionEventData[] { const events = sessionEventCache.get(sessionId) || []; sessionEventCache.delete(sessionId); sessionEventCacheTimestamps.delete(sessionId); return events; } // 清理过期缓存 function cleanExpiredEventCache() { const now = Date.now(); for (const [sessionId, timestamp] of sessionEventCacheTimestamps.entries()) { if (now - timestamp > SESSION_EVENT_CACHE_MAX_AGE) { sessionEventCache.delete(sessionId); sessionEventCacheTimestamps.delete(sessionId); } } } // 检查并触发 keygen(在收到 all_joined 事件后调用) // 5 分钟超时是指:所有人加入后,启动 keygen 的超时时间 // 注意:此函数应该在收到 all_joined 事件后才调用,而不是 joinSession 后立即调用 async function checkAndTriggerKeygen(sessionId: string) { console.log('[KEYGEN] checkAndTriggerKeygen called with sessionId:', sessionId); if (!activeKeygenSession || activeKeygenSession.sessionId !== sessionId) { console.log('[KEYGEN] No matching active keygen session for', sessionId); return; } console.log('[KEYGEN] Active session found:', { sessionId: activeKeygenSession.sessionId, partyIndex: activeKeygenSession.partyIndex, threshold: activeKeygenSession.threshold, participantCount: activeKeygenSession.participants.length, }); // 如果 TSS 已经在运行,不重复触发 if (tssHandler?.getIsRunning()) { console.log('[KEYGEN] TSS already running, skip check'); return; } const pollIntervalMs = 2000; // 2秒轮询间隔 const maxWaitMs = 5 * 60 * 1000; // 5分钟超时(所有人加入后的启动超时) const startTime = Date.now(); console.log('[KEYGEN] Starting to poll session status...'); debugLog.info('main', `Starting to poll session status for ${sessionId}`); while (Date.now() - startTime < maxWaitMs) { // 检查会话是否仍然有效 if (!activeKeygenSession || activeKeygenSession.sessionId !== sessionId) { debugLog.warn('main', 'Active keygen session changed, stopping poll'); return; } // 如果 TSS 已经在运行(可能被其他事件触发),停止轮询 if (tssHandler?.getIsRunning()) { debugLog.info('main', 'TSS started running, stopping poll'); return; } try { const status = await accountClient?.getSessionStatus(sessionId); if (!status) { debugLog.warn('main', 'Failed to get session status, will retry'); await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); continue; } // 优先使用后端返回的 threshold 值 const thresholdN = status.threshold_n || status.total_parties || activeKeygenSession.threshold.n; const thresholdT = status.threshold_t || activeKeygenSession.threshold.t; const expectedN = thresholdN; const currentParticipants = status.participants?.length || 0; debugLog.debug('main', `Session ${sessionId} status: ${status.status}, participants: ${currentParticipants}/${expectedN}, threshold: ${thresholdT}-of-${thresholdN}`); // 检查是否满足启动条件 const hasAllParticipants = currentParticipants >= expectedN; const statusReady = status.status === 'in_progress' || status.status === 'all_joined' || status.status === 'waiting_for_keygen'; console.log('[KEYGEN] Check conditions:', { hasAllParticipants, statusReady, currentParticipants, expectedN, status: status.status, }); if (hasAllParticipants && statusReady) { console.log('[KEYGEN] Conditions met! Triggering keygen...'); debugLog.info('main', `All ${expectedN} participants joined (status: ${status.status}), triggering keygen...`); // 使用后端返回的 participants 信息(包含正确的 party_index) if (status.participants && status.participants.length > 0) { const myPartyId = grpcClient?.getPartyId(); const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; for (const p of status.participants) { const existing = activeKeygenSession.participants.find(ep => ep.partyId === p.party_id); updatedParticipants.push({ partyId: p.party_id, partyIndex: p.party_index, name: existing?.name || (p.party_id === myPartyId ? '我' : `参与方 ${p.party_index + 1}`), }); } activeKeygenSession.participants = updatedParticipants; const myInfo = updatedParticipants.find(p => p.partyId === myPartyId); if (myInfo) { activeKeygenSession.partyIndex = myInfo.partyIndex; } debugLog.info('main', `Updated participants from server: ${JSON.stringify(updatedParticipants.map(p => ({ partyId: p.partyId.substring(0, 8) + '...', partyIndex: p.partyIndex, })))}`); } // 更新 activeKeygenSession 的 threshold(使用后端返回的正确值) activeKeygenSession.threshold = { t: thresholdT, n: thresholdN }; const selectedParties = activeKeygenSession.participants.map(p => p.partyId); await handleSessionStart({ eventType: 'session_started', sessionId: sessionId, thresholdN: thresholdN, thresholdT: thresholdT, selectedParties: selectedParties, }); return; // 成功触发,退出循环 } // 等待下次轮询 await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } catch (error) { debugLog.error('main', `Failed to check session status: ${error}, will retry`); await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } } // 超时 - 所有人已加入但 5 分钟内未能启动 keygen debugLog.error('main', `Timeout: failed to start keygen within 5 minutes after all_joined for session ${sessionId}`); // 通知前端超时 if (mainWindow) { mainWindow.webContents.send(`session:events:${sessionId}`, { type: 'keygen_start_timeout', error: '启动密钥生成超时,请重试', }); } } // 创建主窗口 function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, titleBarStyle: 'hiddenInset', show: false, }); // 开发模式下加载 Vite 开发服务器 if (process.env.NODE_ENV === 'development') { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { // 生产模式下加载打包后的文件 mainWindow.loadFile(path.join(__dirname, '../dist/index.html')); } mainWindow.once('ready-to-show', () => { mainWindow?.show(); }); mainWindow.on('closed', () => { mainWindow = null; }); // 处理外部链接 mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); } // 启动内置 HTTP 服务器 function startHttpServer() { const expressApp = express(); expressApp.use(express.json()); expressApp.use(express.static(path.join(__dirname, '../dist'))); // API 路由 expressApp.get('/api/status', (_req, res) => { res.json({ connected: grpcClient?.isConnected() ?? false, partyId: grpcClient?.getPartyId() ?? null, }); }); // 所有其他路由返回 index.html (SPA) expressApp.get('*', (_req, res) => { res.sendFile(path.join(__dirname, '../dist/index.html')); }); httpServer = expressApp.listen(HTTP_PORT, '127.0.0.1', () => { console.log(`HTTP server running at http://127.0.0.1:${HTTP_PORT}`); }); } // 生成或获取持久化的 partyId function getOrCreatePartyId(db: DatabaseManager): string { const settings = db.getAllSettings(); let partyId = settings['party_id']; if (!partyId) { // 生成一个新的 UUID 作为 partyId partyId = crypto.randomUUID(); db.setSetting('party_id', partyId); debugLog.info('main', `Generated new partyId: ${partyId}`); } else { debugLog.info('main', `Loaded existing partyId: ${partyId}`); } return partyId; } // 初始化服务 async function initServices() { // 初始化数据库 (必须首先初始化) database = new DatabaseManager(); // 等待数据库初始化完成(加载 WASM 和创建表) await database.waitForReady(); debugLog.info('main', 'Database initialized'); // 初始化 gRPC 客户端 grpcClient = new GrpcClient(); // 清理过期的已处理消息记录(防止数据库膨胀) database.cleanupOldProcessedMessages(); debugLog.debug('main', 'Cleaned up old processed messages'); // 初始化 TSS Handler if (USE_MOCK_TSS) { debugLog.info('tss', 'Using Mock TSS Handler (development mode)'); tssHandler = new MockTSSHandler(grpcClient); } else { debugLog.info('tss', 'Using real TSS Handler'); tssHandler = new TSSHandler(grpcClient, database); } // 设置 TSS 进度事件监听 tssHandler.on('progress', (progress: { round: number; totalRounds: number }) => { debugLog.info('tss', `Keygen progress: round ${progress.round}/${progress.totalRounds}`); // 通知前端更新进度 if (activeKeygenSession) { mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, { type: 'progress', round: progress.round, totalRounds: progress.totalRounds, }); } }); tssHandler.on('error', (error: Error) => { debugLog.error('tss', `TSS error: ${error.message}`); if (activeKeygenSession) { mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, { type: 'failed', error: error.message, }); } }); // 初始化 Kava 交易服务 (从数据库读取网络设置,默认主网) const kavaNetwork = database.getSetting('kava_network') || 'mainnet'; const kavaConfig = kavaNetwork === 'mainnet' ? KAVA_MAINNET_TX_CONFIG : KAVA_TESTNET_TX_CONFIG; kavaTxService = new KavaTxService(kavaConfig); debugLog.info('kava', `Kava network: ${kavaNetwork}`); // 初始化 Account 服务 HTTP 客户端 // 从数据库读取 Account 服务 URL,默认使用生产环境地址 const settings = database.getAllSettings(); const accountServiceUrl = settings['account_service_url'] || 'https://rwaapi.szaiai.com'; accountClient = new AccountClient(accountServiceUrl); debugLog.info('account', `Account service URL: ${accountServiceUrl}`); // 设置 IPC 处理器 setupIpcHandlers(); // 启动时自动连接并注册到 Message Router (非阻塞) connectAndRegisterToMessageRouter().catch((err) => { debugLog.error('grpc', `Background connection failed: ${err.message}`); }); } // 处理会话开始事件 - 触发 Keygen async function handleSessionStart(event: { eventType: string; sessionId: string; thresholdN: number; thresholdT: number; selectedParties: string[]; }) { console.log('[KEYGEN] handleSessionStart called:', { eventType: event.eventType, sessionId: event.sessionId, thresholdN: event.thresholdN, thresholdT: event.thresholdT, selectedParties: event.selectedParties?.length, }); if (!activeKeygenSession) { console.log('[KEYGEN] No active keygen session, ignoring'); debugLog.debug('main', 'No active keygen session, ignoring session start event'); return; } if (activeKeygenSession.sessionId !== event.sessionId) { debugLog.debug('main', `Session ID mismatch: expected ${activeKeygenSession.sessionId}, got ${event.sessionId}`); return; } // 幂等性保护:检查是否已经在执行 keygen if (keygenInProgressSessionId === event.sessionId) { debugLog.debug('main', `Keygen already in progress for session ${event.sessionId}, skipping duplicate trigger`); return; } // 再次检查 TSS 是否在运行(双重保护) if (tssHandler?.getIsRunning()) { debugLog.debug('main', 'TSS already running, skipping'); return; } if (!tssHandler) { debugLog.error('tss', 'TSS handler not initialized'); mainWindow?.webContents.send(`session:events:${event.sessionId}`, { type: 'failed', error: 'TSS handler not initialized', }); return; } // 标记 keygen 开始 keygenInProgressSessionId = event.sessionId; // 从事件中更新参与者列表(如果事件包含完整列表) // 注意:activeKeygenSession.participants 可能已经包含正确的 partyIndex(从 checkAndTriggerKeygen 更新) // 只有当事件来自 session_started 事件时才需要重新构建 if (event.selectedParties && event.selectedParties.length > 0) { const myPartyId = grpcClient?.getPartyId(); const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; event.selectedParties.forEach((partyId) => { // 查找已有的参与者信息(优先使用已有的 partyIndex) const existing = activeKeygenSession!.participants.find(p => p.partyId === partyId); if (existing) { // 保留已有的 partyIndex updatedParticipants.push({ partyId: partyId, partyIndex: existing.partyIndex, name: existing.name || (partyId === myPartyId ? '我' : `参与方 ${existing.partyIndex + 1}`), }); } else { // 找不到已有信息,这不应该发生 debugLog.warn('main', `Party ${partyId} not found in existing participants, using fallback index`); updatedParticipants.push({ partyId: partyId, partyIndex: updatedParticipants.length, name: partyId === myPartyId ? '我' : `参与方 ${updatedParticipants.length + 1}`, }); } }); // 按 partyIndex 排序 updatedParticipants.sort((a, b) => a.partyIndex - b.partyIndex); activeKeygenSession.participants = updatedParticipants; // 更新自己的 partyIndex const myInfo = updatedParticipants.find(p => p.partyId === myPartyId); if (myInfo) { activeKeygenSession.partyIndex = myInfo.partyIndex; } debugLog.info('main', `Updated participants: ${JSON.stringify(updatedParticipants.map(p => ({ partyId: p.partyId.substring(0, 8) + '...', partyIndex: p.partyIndex, name: p.name, })))}`); } console.log('[KEYGEN] Calling tssHandler.participateKeygen with:', { sessionId: activeKeygenSession.sessionId, partyId: grpcClient?.getPartyId(), partyIndex: activeKeygenSession.partyIndex, participants: activeKeygenSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })), threshold: activeKeygenSession.threshold, }); debugLog.info('tss', `Starting keygen for session ${event.sessionId}...`); try { const result = await tssHandler.participateKeygen( activeKeygenSession.sessionId, grpcClient?.getPartyId() || '', activeKeygenSession.partyIndex, activeKeygenSession.participants, activeKeygenSession.threshold, activeKeygenSession.encryptionPassword ); if (result.success) { debugLog.info('tss', 'Keygen completed successfully'); await handleKeygenComplete(result); } else { debugLog.error('tss', `Keygen failed: ${result.error}`); mainWindow?.webContents.send(`session:events:${activeKeygenSession.sessionId}`, { type: 'failed', error: result.error || 'Keygen failed', }); // 清除幂等性标志 keygenInProgressSessionId = null; } } catch (error) { debugLog.error('tss', `Keygen error: ${(error as Error).message}`); mainWindow?.webContents.send(`session:events:${activeKeygenSession?.sessionId}`, { type: 'failed', error: (error as Error).message, }); // 清除幂等性标志 keygenInProgressSessionId = null; } } // 处理 Keygen 完成 - 保存 share 并报告完成 async function handleKeygenComplete(result: KeygenResult) { if (!activeKeygenSession || !database || !grpcClient) { debugLog.error('main', 'Missing required components for keygen completion'); return; } const sessionId = activeKeygenSession.sessionId; const partyId = grpcClient.getPartyId(); try { // 1. 保存 share 到本地数据库 const publicKeyHex = result.publicKey.toString('hex'); // 转换 participants 格式:从 { partyId, partyIndex, name } 到 { partyId, name } const participantsForSave = activeKeygenSession.participants.map(p => ({ partyId: p.partyId, name: p.name, })); const saved = database.saveShare({ sessionId: sessionId, walletName: activeKeygenSession.walletName, partyId: partyId || '', partyIndex: result.partyIndex, thresholdT: activeKeygenSession.threshold.t, thresholdN: activeKeygenSession.threshold.n, publicKeyHex: publicKeyHex, rawShare: result.encryptedShare.toString('base64'), participants: participantsForSave, }, activeKeygenSession.encryptionPassword); debugLog.info('main', `Share saved to local database: ${saved?.id}`); // 2. 报告完成给 session-coordinator const allCompleted = await grpcClient.reportCompletion( sessionId, partyId || '', result.publicKey ); debugLog.info('grpc', `Reported completion to session-coordinator, all_completed: ${allCompleted}`); // 3. 通知前端 mainWindow?.webContents.send(`session:events:${sessionId}`, { type: 'completed', publicKey: publicKeyHex, shareId: saved?.id, allCompleted: allCompleted, }); // 4. 清理活跃会话和幂等性标志 activeKeygenSession = null; keygenInProgressSessionId = null; debugLog.info('main', 'Keygen session completed and cleaned up'); } catch (error) { debugLog.error('main', `Failed to handle keygen completion: ${error}`); mainWindow?.webContents.send(`session:events:${sessionId}`, { type: 'failed', error: (error as Error).message, }); // 清除幂等性标志 keygenInProgressSessionId = null; } } // =========================================================================== // Co-Sign 相关处理函数 // =========================================================================== // 检查并触发 Co-Sign(在收到 all_joined 事件后调用) async function checkAndTriggerCoSign(sessionId: string) { console.log('[CO-SIGN] checkAndTriggerCoSign called with sessionId:', sessionId); if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) { console.log('[CO-SIGN] No matching active co-sign session for', sessionId); return; } console.log('[CO-SIGN] Active session found:', { sessionId: activeCoSignSession.sessionId, partyIndex: activeCoSignSession.partyIndex, threshold: activeCoSignSession.threshold, participantCount: activeCoSignSession.participants.length, }); // 如果 TSS 已经在运行,不重复触发 if (tssHandler?.getIsRunning()) { console.log('[CO-SIGN] TSS already running, skip check'); return; } const pollIntervalMs = 2000; // 2秒轮询间隔 const maxWaitMs = 5 * 60 * 1000; // 5分钟超时 const startTime = Date.now(); console.log('[CO-SIGN] Starting to poll session status...'); debugLog.info('main', `Starting to poll co-sign session status for ${sessionId}`); while (Date.now() - startTime < maxWaitMs) { // 检查会话是否仍然有效 if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) { debugLog.warn('main', 'Active co-sign session changed, stopping poll'); return; } // 如果 TSS 已经在运行,停止轮询 if (tssHandler?.getIsRunning()) { debugLog.info('main', 'TSS started running, stopping poll'); return; } try { // 获取签名会话状态 const status = await accountClient?.getSignSessionStatus(sessionId); if (!status) { debugLog.warn('main', 'Failed to get sign session status, will retry'); await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); continue; } const expectedT = activeCoSignSession.threshold.t; const currentParticipants = status.joined_count || 0; debugLog.debug('main', `Sign session ${sessionId} status: ${status.status}, participants: ${currentParticipants}/${expectedT}`); // 检查是否满足启动条件 const hasAllParticipants = currentParticipants >= expectedT; const statusReady = status.status === 'in_progress' || status.status === 'all_joined' || status.status === 'waiting_for_sign'; console.log('[CO-SIGN] Check conditions:', { hasAllParticipants, statusReady, currentParticipants, expectedT, status: status.status, }); if (hasAllParticipants && statusReady) { console.log('[CO-SIGN] Conditions met! Triggering sign...'); debugLog.info('main', `All ${expectedT} participants joined (status: ${status.status}), triggering sign...`); // 更新参与者列表 if (status.parties && status.parties.length > 0) { const myPartyId = grpcClient?.getPartyId(); const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; for (const p of status.parties) { const existing = activeCoSignSession.participants.find(ep => ep.partyId === p.party_id); updatedParticipants.push({ partyId: p.party_id, partyIndex: p.party_index, name: existing?.name || (p.party_id === myPartyId ? '我' : `参与方 ${p.party_index + 1}`), }); } activeCoSignSession.participants = updatedParticipants; const myInfo = updatedParticipants.find(p => p.partyId === myPartyId); if (myInfo) { activeCoSignSession.partyIndex = myInfo.partyIndex; } } const selectedParties = activeCoSignSession.participants.map(p => p.partyId); await handleCoSignStart({ eventType: 'session_started', sessionId: sessionId, thresholdT: activeCoSignSession.threshold.t, thresholdN: activeCoSignSession.threshold.n, selectedParties: selectedParties, }); return; } await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } catch (error) { debugLog.error('main', `Failed to check sign session status: ${error}, will retry`); await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); } } // 超时 debugLog.error('main', `Timeout: failed to start sign within 5 minutes for session ${sessionId}`); if (mainWindow) { mainWindow.webContents.send(`cosign:events:${sessionId}`, { type: 'sign_start_timeout', error: '启动签名超时,请重试', }); } } // 处理 Co-Sign 会话开始事件 - 触发签名 async function handleCoSignStart(event: { eventType: string; sessionId: string; thresholdT: number; thresholdN: number; selectedParties: string[]; }) { console.log('[CO-SIGN] handleCoSignStart called:', { eventType: event.eventType, sessionId: event.sessionId, thresholdT: event.thresholdT, thresholdN: event.thresholdN, selectedParties: event.selectedParties?.length, }); if (!activeCoSignSession) { console.log('[CO-SIGN] No active co-sign session, ignoring'); debugLog.debug('main', 'No active co-sign session, ignoring sign start event'); return; } if (activeCoSignSession.sessionId !== event.sessionId) { debugLog.debug('main', `Session ID mismatch: expected ${activeCoSignSession.sessionId}, got ${event.sessionId}`); return; } // 幂等性保护 if (signInProgressSessionId === event.sessionId) { debugLog.debug('main', `Sign already in progress for session ${event.sessionId}, skipping duplicate trigger`); return; } if (tssHandler?.getIsRunning()) { debugLog.debug('main', 'TSS already running, skipping'); return; } if (!tssHandler || !('participateSign' in tssHandler)) { debugLog.error('tss', 'TSS handler not initialized or does not support signing'); mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, { type: 'failed', error: 'TSS handler not initialized', }); return; } // 标记签名开始 signInProgressSessionId = event.sessionId; // 打印当前 activeCoSignSession.participants 状态 console.log('[CO-SIGN] Current activeCoSignSession.participants before update:', activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex, })) ); console.log('[CO-SIGN] event.selectedParties:', event.selectedParties?.map(id => id.substring(0, 8))); // 从 event.selectedParties 更新参与者列表 // 优先使用 activeCoSignSession.participants 中的 partyIndex(来自 signingParties 或 other_parties) if (event.selectedParties && event.selectedParties.length > 0) { const myPartyId = grpcClient?.getPartyId(); const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; event.selectedParties.forEach((partyId) => { // 查找已有的参与者信息 const existingParticipant = activeCoSignSession?.participants.find(p => p.partyId === partyId); if (existingParticipant) { // 使用已有的 partyIndex updatedParticipants.push({ partyId: partyId, partyIndex: existingParticipant.partyIndex, name: partyId === myPartyId ? '我' : existingParticipant.name, }); } else { // 找不到已有信息,这不应该发生 - 记录警告 console.warn(`[CO-SIGN] WARNING: Party ${partyId.substring(0, 8)} not found in activeCoSignSession.participants!`); // 不使用 fallback index,直接跳过,这会导致参与者数量不足,签名会失败 // 这样可以更早发现问题 } }); // 检查参与者数量是否符合预期 if (updatedParticipants.length !== event.selectedParties.length) { console.error(`[CO-SIGN] ERROR: Participant count mismatch! Expected ${event.selectedParties.length}, got ${updatedParticipants.length}`); } // 按 partyIndex 排序 updatedParticipants.sort((a, b) => a.partyIndex - b.partyIndex); activeCoSignSession.participants = updatedParticipants; console.log('[CO-SIGN] Updated participants from session_started event:', updatedParticipants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex, }))); } // 获取 share 数据 const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword); if (!share) { debugLog.error('main', 'Failed to get share data'); mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, { type: 'failed', error: 'Failed to get share data', }); signInProgressSessionId = null; return; } console.log('[CO-SIGN] Calling tssHandler.participateSign with:', { sessionId: activeCoSignSession.sessionId, partyId: grpcClient?.getPartyId(), partyIndex: activeCoSignSession.partyIndex, participants: activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })), threshold: activeCoSignSession.threshold, messageHash: activeCoSignSession.messageHash.substring(0, 16) + '...', }); debugLog.info('tss', `Starting sign for session ${event.sessionId}...`); try { const result = await (tssHandler as TSSHandler).participateSign( activeCoSignSession.sessionId, grpcClient?.getPartyId() || '', activeCoSignSession.partyIndex, activeCoSignSession.participants, activeCoSignSession.threshold, activeCoSignSession.messageHash, share.raw_share, activeCoSignSession.sharePassword ); if (result.success) { debugLog.info('tss', 'Sign completed successfully'); await handleCoSignComplete(result); } else { debugLog.error('tss', `Sign failed: ${result.error}`); mainWindow?.webContents.send(`cosign:events:${activeCoSignSession.sessionId}`, { type: 'failed', error: result.error || 'Sign failed', }); signInProgressSessionId = null; } } catch (error) { debugLog.error('tss', `Sign error: ${(error as Error).message}`); mainWindow?.webContents.send(`cosign:events:${activeCoSignSession?.sessionId}`, { type: 'failed', error: (error as Error).message, }); signInProgressSessionId = null; } } // 处理 Co-Sign 完成 - 保存签名并报告完成 async function handleCoSignComplete(result: { success: boolean; signature: Buffer; error?: string }) { if (!activeCoSignSession || !database || !grpcClient) { debugLog.error('main', 'Missing required components for sign completion'); return; } const sessionId = activeCoSignSession.sessionId; const partyId = grpcClient.getPartyId(); try { const signatureHex = result.signature.toString('hex'); // 1. 更新签名历史 database.updateSigningHistory(sessionId, { status: 'completed', signature: signatureHex, }); debugLog.info('main', `Signature saved: ${signatureHex.substring(0, 32)}...`); // 2. 报告完成给 session-coordinator const allCompleted = await grpcClient.reportCompletion( sessionId, partyId || '', result.signature ); debugLog.info('grpc', `Reported sign completion to session-coordinator, all_completed: ${allCompleted}`); // 3. 通知前端 mainWindow?.webContents.send(`cosign:events:${sessionId}`, { type: 'completed', signature: signatureHex, allCompleted: allCompleted, }); // 4. 清理活跃会话和幂等性标志 activeCoSignSession = null; signInProgressSessionId = null; debugLog.info('main', 'Co-Sign session completed and cleaned up'); } catch (error) { debugLog.error('main', `Failed to handle sign completion: ${error}`); mainWindow?.webContents.send(`cosign:events:${sessionId}`, { type: 'failed', error: (error as Error).message, }); signInProgressSessionId = null; } } // 连接并注册到 Message Router async function connectAndRegisterToMessageRouter() { if (!grpcClient || !database) { debugLog.error('grpc', 'gRPC client or database not initialized'); return; } try { const settings = database.getAllSettings(); const routerUrl = settings['message_router_url'] || 'mpc-grpc.szaiai.com:443'; const partyId = getOrCreatePartyId(database); const role = 'temporary'; // Service-Party-App 使用 temporary 角色 debugLog.info('grpc', `Connecting to Message Router: ${routerUrl}...`); await grpcClient.connect(routerUrl); debugLog.info('grpc', 'Connected to Message Router'); debugLog.info('grpc', `Registering as party: ${partyId} (role: ${role})...`); await grpcClient.registerParty(partyId, role); debugLog.info('grpc', 'Registered to Message Router successfully'); // 订阅会话事件 grpcClient.subscribeSessionEvents(partyId); debugLog.info('grpc', 'Subscribed to session events'); // 监听连接状态变化 grpcClient.on('disconnected', (reason: string) => { debugLog.warn('grpc', `Disconnected from Message Router: ${reason}`); mainWindow?.webContents.send('grpc:connectionStatus', { connected: false, reason }); }); grpcClient.on('reconnected', () => { debugLog.info('grpc', 'Reconnected to Message Router'); mainWindow?.webContents.send('grpc:connectionStatus', { connected: true }); }); grpcClient.on('reconnectFailed', (reason: string) => { debugLog.error('grpc', `Failed to reconnect: ${reason}`); mainWindow?.webContents.send('grpc:connectionStatus', { connected: false, error: reason }); }); // 监听会话事件并处理 // Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader grpcClient.on('sessionEvent', async (event: { event_id: string; event_type: string; session_id: string; threshold_n: number; threshold_t: number; selected_parties: string[]; join_tokens: Record; message_hash?: Buffer; }) => { debugLog.info('grpc', `Received session event: ${event.event_type} for session ${event.session_id}`); const eventData: SessionEventData = { type: event.event_type, sessionId: event.session_id, thresholdN: event.threshold_n, thresholdT: event.threshold_t, selectedParties: event.selected_parties, }; // 缓存事件(解决前端可能还未订阅的时序问题) cacheSessionEvent(event.session_id, eventData); // 转发事件到前端 mainWindow?.webContents.send(`session:events:${event.session_id}`, eventData); // 根据事件类型处理 - 区分 Keygen 和 Co-Sign 会话 const isCoSignSession = activeCoSignSession?.sessionId === event.session_id; const isKeygenSession = activeKeygenSession?.sessionId === event.session_id; if (event.event_type === 'all_joined') { // 收到 all_joined 事件表示所有参与方都已加入 debugLog.info('main', `Received all_joined event for session ${event.session_id}, isCoSign=${isCoSignSession}, isKeygen=${isKeygenSession}`); if (isCoSignSession) { // Co-Sign 会话:转发到 cosign 频道并触发签名 mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); setImmediate(() => { checkAndTriggerCoSign(event.session_id); }); } else if (isKeygenSession) { // Keygen 会话:启动 5 分钟倒计时 setImmediate(() => { checkAndTriggerKeygen(event.session_id); }); } } else if (event.event_type === 'session_started') { // session_started 事件表示可以开始了 if (isCoSignSession) { // Co-Sign 会话 mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); await handleCoSignStart({ eventType: event.event_type, sessionId: event.session_id, thresholdN: event.threshold_n, thresholdT: event.threshold_t, selectedParties: event.selected_parties, }); } else if (isKeygenSession) { // Keygen 会话 await handleSessionStart({ eventType: event.event_type, sessionId: event.session_id, thresholdN: event.threshold_n, thresholdT: event.threshold_t, selectedParties: event.selected_parties, }); } } else if (event.event_type === 'participant_joined') { // 参与者加入事件 - 也需要区分会话类型 if (isCoSignSession) { mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); } } }); } catch (error) { console.error('Failed to connect/register to Message Router:', (error as Error).message); // 不抛出错误,允许应用继续启动,用户可以稍后手动重试 } } // 设置 IPC 通信处理器 function setupIpcHandlers() { // =========================================================================== // gRPC 相关 // =========================================================================== // gRPC 连接 ipcMain.handle('grpc:connect', async (_event, { url }) => { try { await grpcClient?.connect(url); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 注册为参与方 ipcMain.handle('grpc:register', async (_event, { partyId, role }) => { try { await grpcClient?.registerParty(partyId, role); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 加入会话 ipcMain.handle('grpc:joinSession', async (_event, { sessionId, partyId, joinToken, walletName }) => { try { debugLog.info('grpc', `Joining session: sessionId=${sessionId}, partyId=${partyId}, has_token=${!!joinToken}, token_length=${joinToken?.length || 0}`); const result = await grpcClient?.joinSession(sessionId, partyId, joinToken); if (result?.success) { // 设置活跃的 keygen 会话信息 // Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({ partyId: p.party_id, partyIndex: p.party_index, name: `参与方 ${idx + 1}`, // 暂时使用默认名称 })) || []; // 添加自己到参与者列表 participants.push({ partyId: partyId, partyIndex: result.party_index, name: '我', }); // 按 partyIndex 排序 participants.sort((a, b) => a.partyIndex - b.partyIndex); activeKeygenSession = { sessionId: sessionId, partyIndex: result.party_index, participants: participants, threshold: { t: result.session_info?.threshold_t || 0, n: result.session_info?.threshold_n || 0, }, walletName: walletName || result.session_info?.session_id || 'Shared Wallet', encryptionPassword: '', // 不使用加密密码 }; console.log('Active keygen session set:', { sessionId: activeKeygenSession.sessionId, partyIndex: activeKeygenSession.partyIndex, participantCount: activeKeygenSession.participants.length, threshold: activeKeygenSession.threshold, }); // 关键步骤:立即预订阅消息流 // 这确保在其他方开始发送 TSS 消息时,我们已经准备好接收和缓冲 // 即使 keygen 进程还没启动,消息也不会丢失 if (tssHandler && 'prepareForKeygen' in tssHandler) { try { debugLog.info('tss', `Preparing for keygen: subscribing to messages for session ${sessionId}`); (tssHandler as { prepareForKeygen: (sessionId: string, partyId: string) => void }).prepareForKeygen(sessionId, partyId); } catch (prepareErr) { debugLog.error('tss', `Failed to prepare for keygen: ${(prepareErr as Error).message}`); return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` }; } } // 方案 B: 检查 JoinSession 响应中的 session 状态 // 如果 session 已经是 in_progress,说明我们是最后一个加入的 // 此时 session_started 事件可能已经在 JoinSession 返回前到达(并被忽略) // 所以我们需要直接触发 keygen,而不是等待 session_started 事件 // // 注意:只检查 status === 'in_progress' 就足够了 // 因为 session 只有在所有参与者都加入后才会变成 in_progress // 不需要额外检查参与者数量 const sessionStatus = result.session_info?.status; debugLog.info('main', `JoinSession response: status=${sessionStatus}`); if (sessionStatus === 'in_progress') { // Session 已经开始,说明我们是最后一个加入的 // 直接触发 keygen,不需要等待 session_started 事件 debugLog.info('main', `Session already in_progress, triggering keygen immediately (Solution B)`); // 使用 setImmediate 确保 activeKeygenSession 已完全设置 setImmediate(async () => { const selectedParties = activeKeygenSession?.participants.map(p => p.partyId) || []; await handleSessionStart({ eventType: 'session_started', sessionId: sessionId, thresholdN: result.session_info?.threshold_n || activeKeygenSession?.threshold.n || 0, thresholdT: result.session_info?.threshold_t || activeKeygenSession?.threshold.t || 0, selectedParties: selectedParties, }); }); } else { // Session 还未开始,等待 session_started 事件 debugLog.info('main', `Joined session ${sessionId}, waiting for session_started event to start keygen`); } } return { success: true, data: result }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // gRPC - 创建会话 (通过 Account 服务 HTTP API) ipcMain.handle('grpc:createSession', async (_event, params) => { try { // 获取当前 party ID const partyId = grpcClient?.getPartyId(); if (!partyId) { return { success: false, error: '请先连接到消息路由器' }; } // 动态计算 server-party 数量: persistent = n - t // 例如: 2-of-3 -> persistent=1, 3-of-5 -> persistent=2, 4-of-7 -> persistent=3 // 这样平台备份方数量等于"允许丢失的份额数",确保用户丢失密钥后仍可恢复 const persistentCount = params.thresholdN - params.thresholdT; const externalCount = params.thresholdT; // 用户持有的份额数量 const result = await accountClient?.createKeygenSession({ wallet_name: params.walletName, threshold_t: params.thresholdT, threshold_n: params.thresholdN, initiator_party_id: partyId, initiator_name: params.initiatorName || '发起者', persistent_count: persistentCount, external_count: externalCount, expires_in_seconds: 86400, // 24 小时 }); if (!result?.session_id) { return { success: false, error: '创建会话失败: 未返回会话ID' }; } // 发起方自动加入会话 // 优先使用 join_tokens map,如果没有则使用 wildcard token (join_token) const joinToken = result.join_tokens?.[partyId] || result.join_tokens?.['*'] || result.join_token; if (joinToken) { console.log('Initiator auto-joining session...'); const joinResult = await grpcClient?.joinSession(result.session_id, partyId, joinToken); if (joinResult?.success) { // 设置活跃的 keygen 会话信息 // Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader const participants: Array<{ partyId: string; partyIndex: number; name: string }> = []; // 添加发起方(自己) participants.push({ partyId: partyId, partyIndex: joinResult.party_index, name: params.initiatorName || '发起者', }); activeKeygenSession = { sessionId: result.session_id, partyIndex: joinResult.party_index, participants: participants, threshold: { t: params.thresholdT, n: params.thresholdN, }, walletName: params.walletName, encryptionPassword: '', // 不使用加密密码 }; console.log('Initiator active keygen session set:', { sessionId: activeKeygenSession.sessionId, partyIndex: activeKeygenSession.partyIndex, participantCount: activeKeygenSession.participants.length, threshold: activeKeygenSession.threshold, }); // 关键步骤:立即预订阅消息流 if (tssHandler && 'prepareForKeygen' in tssHandler) { try { debugLog.info('tss', `Initiator preparing for keygen: subscribing to messages for session ${result.session_id}`); (tssHandler as { prepareForKeygen: (sessionId: string, partyId: string) => void }).prepareForKeygen(result.session_id, partyId); } catch (prepareErr) { debugLog.error('tss', `Failed to prepare for keygen: ${(prepareErr as Error).message}`); return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` }; } } // 方案 B: 检查 JoinSession 响应中的 session 状态 // 发起方通常是第一个加入的,所以 session 不太可能已经是 in_progress // 但为了代码一致性,这里也添加检查 const sessionStatus = joinResult.session_info?.status; debugLog.info('main', `Initiator JoinSession response: status=${sessionStatus}`); if (sessionStatus === 'in_progress') { debugLog.info('main', `Session already in_progress, triggering keygen immediately (Solution B)`); setImmediate(async () => { const selectedParties = activeKeygenSession?.participants.map(p => p.partyId) || []; await handleSessionStart({ eventType: 'session_started', sessionId: result.session_id, thresholdN: joinResult.session_info?.threshold_n || params.thresholdN, thresholdT: joinResult.session_info?.threshold_t || params.thresholdT, selectedParties: selectedParties, }); }); } else { debugLog.info('main', `Initiator joined session ${result.session_id}, waiting for session_started event to start keygen`); } } else { console.warn('Initiator failed to join session'); } } else { console.warn('No join token found for initiator partyId:', partyId); } return { success: true, sessionId: result?.session_id, inviteCode: result?.invite_code, walletName: result?.wallet_name, expiresAt: result?.expires_at, joinTokens: result?.join_tokens, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // gRPC - 验证邀请码 (通过 Account 服务 HTTP API) ipcMain.handle('grpc:validateInviteCode', async (_event, { code }) => { try { debugLog.info('account', `Validating invite code: ${code}`); const result = await accountClient?.getSessionByInviteCode(code); debugLog.info('account', `Got session for invite code: session_id=${result?.session_id}, has_join_token=${!!result?.join_token}, token_length=${result?.join_token?.length || 0}`); return { success: true, sessionInfo: { sessionId: result?.session_id, walletName: result?.wallet_name, threshold: { t: result?.threshold_t, n: result?.threshold_n, }, status: result?.status, currentParticipants: result?.completed_parties || result?.joined_parties || 0, totalParticipants: result?.total_parties || result?.threshold_n || 0, }, joinToken: result?.join_token, }; } catch (error) { debugLog.error('account', `Failed to validate invite code: ${(error as Error).message}`); return { success: false, error: (error as Error).message }; } }); // gRPC - 获取会话状态 (通过 Account 服务 HTTP API) ipcMain.handle('grpc:getSessionStatus', async (_event, { sessionId }) => { try { const result = await accountClient?.getSessionStatus(sessionId); // 优先使用后端返回的 threshold,否则从 activeKeygenSession 获取 const threshold = { t: result?.threshold_t || activeKeygenSession?.threshold?.t || 0, n: result?.threshold_n || result?.total_parties || activeKeygenSession?.threshold?.n || 0, }; const participants = result?.participants?.map((p, idx) => ({ partyId: p.party_id, partyIndex: p.party_index, name: activeKeygenSession?.participants?.find(ap => ap.partyId === p.party_id)?.name || `参与方 ${idx + 1}`, status: p.status, joinedAt: new Date().toISOString(), })) || []; return { success: true, session: { sessionId: result?.session_id, walletName: activeKeygenSession?.walletName || '', status: result?.status, completedParties: result?.completed_parties, totalParties: result?.total_parties, sessionType: result?.session_type, publicKey: result?.public_key, threshold: threshold, participants: participants, currentRound: 0, totalRounds: 4, }, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // gRPC - 测试连接 ipcMain.handle('grpc:testConnection', async (_event, { url }) => { try { // 地址格式: host:port (例如 mpc-grpc.szaiai.com:443) await grpcClient?.connect(url); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // gRPC - 获取已注册的参与方列表 ipcMain.handle('grpc:getRegisteredParties', async (_event, { roleFilter, onlyOnline }) => { try { const parties = await grpcClient?.getRegisteredParties(roleFilter, onlyOnline); return { success: true, parties: parties || [] }; } catch (error) { return { success: false, error: (error as Error).message, parties: [] }; } }); // gRPC - 获取当前 partyId ipcMain.handle('grpc:getPartyId', async () => { try { const partyId = grpcClient?.getPartyId(); return { success: true, partyId: partyId || null }; } catch (error) { return { success: false, error: (error as Error).message, partyId: null }; } }); // gRPC - 检查连接状态 ipcMain.handle('grpc:isConnected', async () => { try { const connected = grpcClient?.isConnected() || false; return { success: true, connected }; } catch (error) { return { success: false, error: (error as Error).message, connected: false }; } }); // gRPC - 验证签名会话 (通过 Account 服务 HTTP API) ipcMain.handle('grpc:validateSigningSession', async (_event, { code }) => { try { const result = await accountClient?.getSignSessionByInviteCode(code); return { success: true, session: { sessionId: result?.session_id, keygenSessionId: result?.keygen_session_id, walletName: result?.wallet_name, messageHash: result?.message_hash, threshold: { t: result?.threshold_t, n: result?.threshold_n, }, currentParticipants: result?.joined_count || 0, status: result?.status, parties: result?.parties, initiator: '发起者', }, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // gRPC - 加入签名会话 ipcMain.handle('grpc:joinSigningSession', async (_event, params) => { try { // 从本地 SQLite 获取 share 数据 const share = database?.getShare(params.shareId, params.password); if (!share) { return { success: false, error: 'Share not found or incorrect password' }; } // 加入签名会话 (通过 gRPC) // TODO: 实际加入会话逻辑需要使用 gRPC client // 这里先返回成功,表示验证通过 return { success: true, partyId: share.party_id, partyIndex: share.party_index, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // =========================================================================== // Account 服务相关 (HTTP API) // =========================================================================== // 创建签名会话 ipcMain.handle('account:createSignSession', async (_event, params) => { try { // 从本地 SQLite 获取 share 数据 const share = database?.getShare(params.shareId, params.password); if (!share) { return { success: false, error: 'Share not found or incorrect password' }; } // 解析 participants_json 获取参与方列表 const participants = JSON.parse(share.participants_json || '[]'); const parties = participants.map((p: { partyId: string }, index: number) => ({ party_id: p.partyId, party_index: index, })); const result = await accountClient?.createSignSession({ keygen_session_id: share.session_id, wallet_name: share.wallet_name, message_hash: params.messageHash, parties: parties, threshold_t: share.threshold_t, initiator_name: params.initiatorName, }); return { success: true, sessionId: result?.session_id, inviteCode: result?.invite_code, expiresAt: result?.expires_at, joinToken: result?.join_token, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取签名会话状态 (通过邀请码) ipcMain.handle('account:getSignSessionByInviteCode', async (_event, { inviteCode }) => { try { const result = await accountClient?.getSignSessionByInviteCode(inviteCode); return { success: true, session: { sessionId: result?.session_id, keygenSessionId: result?.keygen_session_id, walletName: result?.wallet_name, messageHash: result?.message_hash, thresholdT: result?.threshold_t, status: result?.status, inviteCode: result?.invite_code, expiresAt: result?.expires_at, parties: result?.parties, joinedCount: result?.joined_count, }, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // Account 服务健康检查 ipcMain.handle('account:healthCheck', async () => { try { const result = await accountClient?.healthCheck(); return { success: true, data: result }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 测试 Account 服务连接 ipcMain.handle('account:testConnection', async () => { try { const connected = await accountClient?.testConnection(); return { success: connected }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 更新 Account 服务 URL ipcMain.handle('account:updateUrl', async (_event, { url }) => { try { accountClient?.setBaseUrl(url); database?.setSetting('account_service_url', url); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取 Account 服务 URL ipcMain.handle('account:getUrl', async () => { return accountClient?.getBaseUrl() || 'https://rwaapi.szaiai.com'; }); // =========================================================================== // Co-Sign 相关 IPC 处理器 // =========================================================================== // 创建 Co-Sign 会话 ipcMain.handle('cosign:createSession', async (_event, params: { shareId: string; sharePassword: string; messageHash: string; initiatorName?: string; }) => { try { // 获取当前 party ID const partyId = grpcClient?.getPartyId(); if (!partyId) { return { success: false, error: '请先连接到消息路由器' }; } // 从本地获取 share 信息 const share = database?.getShare(params.shareId, params.sharePassword); if (!share) { return { success: false, error: 'Share 不存在或密码错误' }; } // 从后端获取 keygen 会话的参与者信息(包含正确的 party_index) const keygenStatus = await accountClient?.getSessionStatus(share.session_id); if (!keygenStatus?.participants || keygenStatus.participants.length === 0) { return { success: false, error: '无法获取 keygen 会话的参与者信息' }; } // 过滤掉 co-managed-party-*(服务器持久方),只保留 temporary/external 用户方 // 只有用户方持有签名私钥份额,co-managed-party 是备份/恢复用的 const signingParties = keygenStatus.participants .filter(p => !p.party_id.startsWith('co-managed-party-')) .map(p => ({ party_id: p.party_id, party_index: p.party_index, })); console.log('[CO-SIGN] Signing parties (excluding co-managed-party):', { total_keygen_participants: keygenStatus.participants.length, signing_parties_count: signingParties.length, signing_parties: signingParties.map(p => ({ id: p.party_id, index: p.party_index })), }); if (signingParties.length < share.threshold_t) { return { success: false, error: `签名参与方不足: 需要 ${share.threshold_t} 个,但只有 ${signingParties.length} 个用户方` }; } // 创建签名会话 const result = await accountClient?.createSignSession({ keygen_session_id: share.session_id, wallet_name: share.wallet_name, message_hash: params.messageHash, parties: signingParties, threshold_t: share.threshold_t, initiator_name: params.initiatorName || '发起者', }); if (!result?.session_id) { return { success: false, error: '创建签名会话失败: 未返回会话ID' }; } // 创建签名历史记录 database?.createSigningHistory({ shareId: params.shareId, sessionId: result.session_id, messageHash: params.messageHash, }); // 发起方自动加入会话 // 支持新格式 join_tokens (map[partyID]token) 和旧格式 join_token (单一通配符 token) const joinToken = result.join_tokens?.[partyId] || (result as { join_token?: string }).join_token; if (joinToken) { console.log('[CO-SIGN] Initiator auto-joining session...'); const joinResult = await grpcClient?.joinSession(result.session_id, partyId, joinToken); if (joinResult?.success) { // 设置活跃的 Co-Sign 会话信息 // 使用 signingParties 初始化完整的参与者列表(包含正确的 partyIndex) const signParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = signingParties.map(p => ({ partyId: p.party_id, partyIndex: p.party_index, name: p.party_id === partyId ? (params.initiatorName || '发起者') : `参与方 ${p.party_index + 1}`, })); console.log('[CO-SIGN] Initiator signParticipants (from signingParties):', signParticipants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex, }))); activeCoSignSession = { sessionId: result.session_id, partyIndex: joinResult.party_index, participants: signParticipants, threshold: { t: share.threshold_t, n: share.threshold_n, }, walletName: share.wallet_name, messageHash: params.messageHash, shareId: params.shareId, sharePassword: params.sharePassword, }; console.log('[CO-SIGN] Initiator active session set:', { sessionId: activeCoSignSession.sessionId, partyIndex: activeCoSignSession.partyIndex, threshold: activeCoSignSession.threshold, }); // 预订阅消息流 if (tssHandler && 'prepareForSign' in tssHandler) { try { debugLog.info('tss', `Initiator preparing for sign: subscribing to messages for session ${result.session_id}`); (tssHandler as TSSHandler).prepareForSign(result.session_id, partyId); } catch (prepareErr) { debugLog.error('tss', `Failed to prepare for sign: ${(prepareErr as Error).message}`); return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` }; } } // 检查会话状态 const sessionStatus = joinResult.session_info?.status; debugLog.info('main', `Initiator JoinSession response: status=${sessionStatus}`); if (sessionStatus === 'in_progress') { debugLog.info('main', 'Session already in_progress, triggering sign immediately'); // 从 joinResult.other_parties 获取其他参与者,加上发起者自己 // 因为此时 activeCoSignSession.participants 只有发起者自己 const otherParties = joinResult.other_parties || []; const selectedParties = [partyId, ...otherParties.map((p: { party_id: string }) => p.party_id)]; console.log('[CO-SIGN] Initiator using other_parties + self:', selectedParties.map((id: string) => id.substring(0, 8))); setImmediate(async () => { await handleCoSignStart({ eventType: 'session_started', sessionId: result.session_id, thresholdT: share.threshold_t, thresholdN: share.threshold_n, selectedParties: selectedParties, }); }); } } else { console.warn('[CO-SIGN] Initiator failed to join session'); } } return { success: true, sessionId: result.session_id, inviteCode: result.invite_code, walletName: share.wallet_name, expiresAt: result.expires_at, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 验证 Co-Sign 邀请码 ipcMain.handle('cosign:validateInviteCode', async (_event, { code }) => { try { debugLog.info('account', `Validating co-sign invite code: ${code}`); const result = await accountClient?.getSignSessionByInviteCode(code); if (!result?.session_id) { return { success: false, error: '无效的邀请码' }; } return { success: true, sessionInfo: { sessionId: result.session_id, keygenSessionId: result.keygen_session_id, walletName: result.wallet_name, messageHash: result.message_hash, threshold: { t: result.threshold_t, n: result.threshold_n, }, status: result.status, currentParticipants: result.joined_count || 0, parties: result.parties, }, joinToken: result.join_token, }; } catch (error) { debugLog.error('account', `Failed to validate co-sign invite code: ${(error as Error).message}`); return { success: false, error: (error as Error).message }; } }); // 加入 Co-Sign 会话 ipcMain.handle('cosign:joinSession', async (_event, params: { sessionId: string; shareId: string; sharePassword: string; joinToken: string; walletName?: string; messageHash: string; threshold: { t: number; n: number }; parties?: Array<{ party_id: string; party_index: number }>; }) => { try { const partyId = grpcClient?.getPartyId(); if (!partyId) { return { success: false, error: '请先连接到消息路由器' }; } // 验证 share const share = database?.getShare(params.shareId, params.sharePassword); if (!share) { return { success: false, error: 'Share 不存在或密码错误' }; } debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, partyId=${partyId}`); const result = await grpcClient?.joinSession(params.sessionId, partyId, params.joinToken); if (result?.success) { // 设置活跃的 Co-Sign 会话 // 优先使用 params.parties(来自 validateInviteCode,包含所有预期参与者) // 而不是 result.other_parties(只包含已加入的参与者) let participants: Array<{ partyId: string; partyIndex: number; name: string }>; if (params.parties && params.parties.length > 0) { // 使用完整的 parties 列表 participants = params.parties.map(p => ({ partyId: p.party_id, partyIndex: p.party_index, name: p.party_id === partyId ? '我' : `参与方 ${p.party_index + 1}`, })); console.log('[CO-SIGN] Participant using params.parties (complete list):', participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex, }))); } else { // Fallback: 使用 other_parties + 自己(可能不完整) console.warn('[CO-SIGN] WARNING: params.parties not available, using other_parties (may be incomplete)'); participants = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({ partyId: p.party_id, partyIndex: p.party_index, name: `参与方 ${idx + 1}`, })) || []; // 添加自己 participants.push({ partyId: partyId, partyIndex: result.party_index, name: '我', }); } // 按 partyIndex 排序 participants.sort((a, b) => a.partyIndex - b.partyIndex); activeCoSignSession = { sessionId: params.sessionId, partyIndex: result.party_index, participants: participants, threshold: params.threshold, walletName: params.walletName || share.wallet_name, messageHash: params.messageHash, shareId: params.shareId, sharePassword: params.sharePassword, }; console.log('[CO-SIGN] Active session set:', { sessionId: activeCoSignSession.sessionId, partyIndex: activeCoSignSession.partyIndex, participantCount: activeCoSignSession.participants.length, threshold: activeCoSignSession.threshold, }); // 创建签名历史记录 database?.createSigningHistory({ shareId: params.shareId, sessionId: params.sessionId, messageHash: params.messageHash, }); // 预订阅消息流 if (tssHandler && 'prepareForSign' in tssHandler) { try { debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}`); (tssHandler as TSSHandler).prepareForSign(params.sessionId, partyId); } catch (prepareErr) { debugLog.error('tss', `Failed to prepare for sign: ${(prepareErr as Error).message}`); return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` }; } } // 检查会话状态 const sessionStatus = result.session_info?.status; debugLog.info('main', `JoinSession response: status=${sessionStatus}`); if (sessionStatus === 'in_progress') { debugLog.info('main', 'Session already in_progress, triggering sign immediately'); // 使用 activeCoSignSession.participants(已从 other_parties + 自己构建) const selectedParties = activeCoSignSession?.participants.map(p => p.partyId) || []; console.log('[CO-SIGN] Participant using activeCoSignSession.participants:', selectedParties.map((id: string) => id.substring(0, 8))); setImmediate(async () => { await handleCoSignStart({ eventType: 'session_started', sessionId: params.sessionId, thresholdT: params.threshold.t, thresholdN: params.threshold.n, selectedParties: selectedParties, }); }); } return { success: true, data: result }; } else { return { success: false, error: '加入会话失败' }; } } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取 Co-Sign 会话状态 ipcMain.handle('cosign:getSessionStatus', async (_event, { sessionId }) => { try { const result = await accountClient?.getSignSessionStatus(sessionId); // API 返回的是 participants 字段 const apiParticipants = (result as { participants?: Array<{ party_id: string; party_index: number; status: string }> })?.participants || []; return { success: true, session: { sessionId: result?.session_id, status: result?.status, joinedCount: result?.joined_count, inviteCode: (result as { invite_code?: string })?.invite_code || '', threshold: { t: result?.threshold_t || activeCoSignSession?.threshold?.t || 0, n: result?.threshold_n || activeCoSignSession?.threshold?.n || 0, }, participants: apiParticipants.map((p, idx) => ({ partyId: p.party_id, partyIndex: p.party_index, name: activeCoSignSession?.participants?.find(ap => ap.partyId === p.party_id)?.name || `参与方 ${idx + 1}`, status: p.status || 'waiting', })), messageHash: activeCoSignSession?.messageHash || '', walletName: activeCoSignSession?.walletName || '', }, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 订阅 Co-Sign 会话事件 ipcMain.on('cosign:subscribeSessionEvents', (_event, { sessionId }) => { debugLog.debug('main', `Frontend subscribing to co-sign session events: ${sessionId}`); // 获取并发送缓存的事件 const cachedEvents = getAndClearCachedEvents(sessionId); if (cachedEvents.length > 0) { debugLog.info('main', `Sending ${cachedEvents.length} cached events to frontend for co-sign session ${sessionId}`); for (const event of cachedEvents) { mainWindow?.webContents.send(`cosign:events:${sessionId}`, event); } } }); // 取消订阅 Co-Sign 会话事件 ipcMain.on('cosign:unsubscribeSessionEvents', (_event, { sessionId }) => { debugLog.debug('main', `Frontend unsubscribing from co-sign session events: ${sessionId}`); }); // =========================================================================== // Share 存储相关 (SQLite) // =========================================================================== // 保存 share ipcMain.handle('storage:saveShare', async (_event, { share, password }) => { try { const saved = database?.saveShare({ sessionId: share.sessionId, walletName: share.walletName, partyId: share.partyId, partyIndex: share.partyIndex, thresholdT: share.threshold.t, thresholdN: share.threshold.n, publicKeyHex: share.publicKey, rawShare: share.rawShare, participants: share.participants || [], }, password); // 自动派生 Kava 地址 if (saved && share.publicKey) { try { const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava'); database?.saveDerivedAddress({ shareId: saved.id, chain: 'kava', derivationPath: kavaAddress.derivationPath, address: kavaAddress.address, publicKeyHex: share.publicKey, }); } catch (err) { console.error('Failed to derive Kava address:', err); } } return { success: true, data: saved }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取 share 列表 ipcMain.handle('storage:listShares', async () => { try { const shares = database?.listShares() ?? []; // 转换为前端期望的格式 const formatted = shares.map(share => ({ id: share.id, sessionId: share.session_id, walletName: share.wallet_name, partyId: share.party_id, partyIndex: share.party_index, threshold: { t: share.threshold_t, n: share.threshold_n, }, publicKey: share.public_key_hex, createdAt: share.created_at, lastUsedAt: share.last_used_at, metadata: { participants: JSON.parse(share.participants_json || '[]'), }, })); return { success: true, data: formatted }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取单个 share (解密) ipcMain.handle('storage:getShare', async (_event, { id, password }) => { try { const share = database?.getShare(id, password); if (!share) return null; return { id: share.id, sessionId: share.session_id, walletName: share.wallet_name, partyId: share.party_id, partyIndex: share.party_index, threshold: { t: share.threshold_t, n: share.threshold_n, }, publicKey: share.public_key_hex, rawShare: share.raw_share, createdAt: share.created_at, lastUsedAt: share.last_used_at, participants: JSON.parse(share.participants_json || '[]'), }; } catch (error) { return null; } }); // 删除 share ipcMain.handle('storage:deleteShare', async (_event, { id }) => { try { database?.deleteShare(id); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 导出 share ipcMain.handle('storage:exportShare', async (_event, { id, password }) => { try { const data = database?.exportShare(id, password); return { success: true, data }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 导入 share ipcMain.handle('storage:importShare', async (_event, { filePath, password }) => { try { const data = fs.readFileSync(filePath); const share = database?.importShare(data, password); return { success: true, share }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // =========================================================================== // 地址派生相关 // =========================================================================== // 派生地址 ipcMain.handle('address:derive', async (_event, { shareId, chain, password }) => { try { const share = database?.getShare(shareId, password); if (!share) { return { success: false, error: 'Share not found' }; } const derived = addressDerivationService.deriveAddress(share.public_key_hex, chain); // 保存到数据库 const saved = database?.saveDerivedAddress({ shareId, chain, derivationPath: derived.derivationPath, address: derived.address, publicKeyHex: share.public_key_hex, }); return { success: true, data: { ...derived, id: saved?.id } }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 派生所有支持的链地址 ipcMain.handle('address:deriveAll', async (_event, { shareId, password }) => { try { const share = database?.getShare(shareId, password); if (!share) { return { success: false, error: 'Share not found' }; } const addresses = addressDerivationService.deriveAllAddresses(share.public_key_hex); // 保存所有地址 for (const addr of addresses) { database?.saveDerivedAddress({ shareId, chain: addr.chain, derivationPath: addr.derivationPath, address: addr.address, publicKeyHex: share.public_key_hex, }); } return { success: true, data: addresses }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取 share 的所有派生地址 ipcMain.handle('address:list', async (_event, { shareId }) => { try { const addresses = database?.getAddressesByShare(shareId) ?? []; return { success: true, data: addresses.map(addr => ({ id: addr.id, shareId: addr.share_id, chain: addr.chain, chainName: CHAIN_CONFIGS[addr.chain]?.name || addr.chain, derivationPath: addr.derivation_path, address: addr.address, publicKeyHex: addr.public_key_hex, createdAt: addr.created_at, })), }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取支持的链列表 ipcMain.handle('address:getSupportedChains', async () => { return addressDerivationService.getSupportedChains(); }); // =========================================================================== // 签名历史相关 // =========================================================================== // 获取签名历史 ipcMain.handle('signing:getHistory', async (_event, { shareId }) => { try { const history = database?.getSigningHistoryByShare(shareId) ?? []; return { success: true, data: history.map(h => ({ id: h.id, shareId: h.share_id, sessionId: h.session_id, messageHash: h.message_hash, signature: h.signature, status: h.status, errorMessage: h.error_message, createdAt: h.created_at, completedAt: h.completed_at, })), }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // =========================================================================== // 设置相关 // =========================================================================== // 获取设置 ipcMain.handle('storage:getSettings', async () => { try { const settings = database?.getAllSettings() ?? {}; return { messageRouterUrl: settings['message_router_url'] || 'mpc-grpc.szaiai.com:443', autoBackup: settings['auto_backup'] === 'true', backupPath: settings['backup_path'] || '', }; } catch (error) { return null; } }); // 保存设置 ipcMain.handle('storage:saveSettings', async (_event, { settings }) => { try { if (settings.messageRouterUrl !== undefined) { database?.setSetting('message_router_url', settings.messageRouterUrl); } if (settings.autoBackup !== undefined) { database?.setSetting('auto_backup', settings.autoBackup ? 'true' : 'false'); } if (settings.backupPath !== undefined) { database?.setSetting('backup_path', settings.backupPath); } return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // =========================================================================== // Kava 区块链相关 // =========================================================================== // 查询 Kava 余额 ipcMain.handle('kava:getBalance', async (_event, { address }) => { try { const balance = await kavaTxService?.getKavaBalance(address); return { success: true, data: balance }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 查询所有余额 ipcMain.handle('kava:getAllBalances', async (_event, { address }) => { try { const balances = await kavaTxService?.getAllBalances(address); return { success: true, data: balances }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 查询账户信息 ipcMain.handle('kava:getAccountInfo', async (_event, { address }) => { try { const info = await kavaTxService?.getAccountInfo(address); return { success: true, data: info }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 构建转账交易 (待签名) ipcMain.handle('kava:buildSendTx', async (_event, { fromAddress, toAddress, amount, publicKeyHex, memo }) => { try { const unsignedTx = await kavaTxService?.buildSendTx( fromAddress, toAddress, amount, publicKeyHex, memo || '' ); return { success: true, data: unsignedTx }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 完成签名并广播交易 ipcMain.handle('kava:completeTxAndBroadcast', async (_event, { unsignedTx, signatureHex }) => { try { // 完成交易签名 const signedTx = await kavaTxService?.completeTx(unsignedTx, signatureHex); if (!signedTx) { return { success: false, error: 'Failed to complete transaction' }; } // 广播交易 const result = await kavaTxService?.broadcastTx(signedTx); return { success: true, data: result }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 查询交易状态 ipcMain.handle('kava:getTxStatus', async (_event, { txHash }) => { try { const status = await kavaTxService?.getTxStatus(txHash); return { success: true, data: status }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取 Kava 配置 ipcMain.handle('kava:getConfig', async () => { return kavaTxService?.getConfig(); }); // 更新 Kava 配置 ipcMain.handle('kava:updateConfig', async (_event, { config }) => { try { kavaTxService?.updateConfig(config); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // Kava 健康检查 ipcMain.handle('kava:healthCheck', async () => { try { const result = await kavaTxService?.healthCheck(); return { success: result?.ok ?? false, data: result }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 切换 Kava 网络 (主网/测试网) ipcMain.handle('kava:switchNetwork', async (_event, { network }) => { try { if (network === 'testnet') { kavaTxService?.switchToTestnet(); database?.setSetting('kava_network', 'testnet'); } else { kavaTxService?.switchToMainnet(); database?.setSetting('kava_network', 'mainnet'); } return { success: true, network }; } catch (error) { return { success: false, error: (error as Error).message }; } }); // 获取当前 Kava 网络 ipcMain.handle('kava:getNetwork', async () => { const isTestnet = kavaTxService?.isTestnet() ?? false; return { network: isTestnet ? 'testnet' : 'mainnet' }; }); // =========================================================================== // 对话框相关 // =========================================================================== // 选择目录 ipcMain.handle('dialog:selectDirectory', async () => { const result = await dialog.showOpenDialog(mainWindow!, { properties: ['openDirectory'], }); return result.canceled ? null : result.filePaths[0]; }); // 选择文件 ipcMain.handle('dialog:selectFile', async (_event, { filters }) => { const result = await dialog.showOpenDialog(mainWindow!, { properties: ['openFile'], filters: filters || [], }); return result.canceled ? null : result.filePaths[0]; }); // 保存文件 ipcMain.handle('dialog:saveFile', async (_event, { defaultPath, filters }) => { const result = await dialog.showSaveDialog(mainWindow!, { defaultPath, filters: filters || [], }); return result.canceled ? null : result.filePath; }); // =========================================================================== // 调试相关 // =========================================================================== // 订阅日志 ipcMain.on('debug:subscribe', () => { debugLogEnabled = true; debugLog.info('main', 'Debug console connected'); }); // 取消订阅日志 ipcMain.on('debug:unsubscribe', () => { debugLogEnabled = false; }); // 接收来自渲染进程的日志 ipcMain.on('debug:log', (_event, { level, source, message }) => { sendDebugLog(level as LogLevel, source as LogSource, message); }); // =========================================================================== // 会话事件订阅(带缓存事件发送) // =========================================================================== // 前端订阅会话事件时,立即发送缓存的事件 ipcMain.on('grpc:subscribeSessionEvents', (_event, { sessionId }) => { debugLog.debug('main', `Frontend subscribing to session events: ${sessionId}`); // 获取并发送缓存的事件 const cachedEvents = getAndClearCachedEvents(sessionId); if (cachedEvents.length > 0) { debugLog.info('main', `Sending ${cachedEvents.length} cached events to frontend for session ${sessionId}`); for (const event of cachedEvents) { mainWindow?.webContents.send(`session:events:${sessionId}`, event); } } }); // 前端取消订阅 ipcMain.on('grpc:unsubscribeSessionEvents', (_event, { sessionId }) => { debugLog.debug('main', `Frontend unsubscribing from session events: ${sessionId}`); }); } // 应用生命周期 app.whenReady().then(async () => { await initServices(); // HTTP 服务器仅在开发模式下启动 if (process.env.NODE_ENV === 'development') { startHttpServer(); } createWindow(); app.on('activate', async () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); // macOS: 重新激活时检查并恢复 gRPC 连接 if (grpcClient && !grpcClient.isConnected()) { debugLog.info('grpc', 'App activated, reconnecting to Message Router...'); try { await connectAndRegisterToMessageRouter(); } catch (err) { debugLog.error('grpc', `Failed to reconnect on activate: ${(err as Error).message}`); } } } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', () => { // 清理资源 grpcClient?.disconnect(); database?.close(); httpServer?.close(); kavaTxService?.disconnect(); });