rwadurian/backend/mpc-system/services/service-party-app/electron/main.ts

1682 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 } 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';
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<typeof express.application.listen> | 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;
// 会话事件缓存 - 解决前端订阅时可能错过事件的时序问题
// 当事件到达时,前端可能还在页面导航中,尚未订阅
interface SessionEventData {
type: string;
sessionId: string;
thresholdN?: number;
thresholdT?: number;
selectedParties?: string[];
publicKey?: string;
shareId?: string;
allCompleted?: boolean;
error?: string;
}
const sessionEventCache = new Map<string, SessionEventData[]>();
const SESSION_EVENT_CACHE_MAX_AGE = 60000; // 60秒后清理缓存
const sessionEventCacheTimestamps = new Map<string, number>();
// 添加事件到缓存
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 交易服务
kavaTxService = new KavaTxService(KAVA_MAINNET_TX_CONFIG);
// 初始化 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;
}
}
// 连接并注册到 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<string, string>;
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);
// 根据事件类型处理
if (event.event_type === 'all_joined') {
// 收到 all_joined 事件表示所有参与方都已加入
// 此时启动 5 分钟倒计时,在此期间完成 keygen 启动
debugLog.info('main', `Received all_joined event for session ${event.session_id}, starting 5-minute keygen countdown`);
// 使用 setImmediate 确保不阻塞事件处理
setImmediate(() => {
checkAndTriggerKeygen(event.session_id);
});
} else if (event.event_type === 'session_started') {
// session_started 事件表示 keygen 可以开始了(所有人已准备就绪)
// 直接触发 keygen
await handleSessionStart({
eventType: event.event_type,
sessionId: event.session_id,
thresholdN: event.threshold_n,
thresholdT: event.threshold_t,
selectedParties: event.selected_parties,
});
}
});
} 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) {
debugLog.info('tss', `Preparing for keygen: subscribing to messages for session ${sessionId}`);
(tssHandler as { prepareForKeygen: (sessionId: string, partyId: string) => void }).prepareForKeygen(sessionId, partyId);
}
// 方案 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) {
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);
}
// 方案 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?.parties?.length || 0,
},
currentParticipants: result?.joined_count || 0,
status: result?.status,
parties: result?.parties,
},
};
} 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';
});
// ===========================================================================
// 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,
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 };
}
});
// ===========================================================================
// 对话框相关
// ===========================================================================
// 选择目录
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', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
// 清理资源
grpcClient?.disconnect();
database?.close();
httpServer?.close();
kavaTxService?.disconnect();
});