fix(service-party-app): 修复 gRPC 响应字段名 snake_case 问题

问题:
- proto-loader 使用 keepCase: true,导致 gRPC 响应字段为 snake_case
- TypeScript 接口使用 camelCase,导致字段不匹配
- joinSession 响应的 session_info.threshold_t 和 threshold_n 无法读取
- 导致 activeKeygenSession.threshold 为 {t: 0, n: 0}
- TSS 进程收到错误的 threshold 参数导致 exit code 1

修复:
- grpc-client.ts 接口改为 snake_case 以匹配 proto 定义
- main.ts 更新为使用 snake_case 字段名
- SessionEvent 处理转换为 camelCase 再传递给 handleSessionStart

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-29 12:14:30 -08:00
parent 1b48c05aa7
commit 989364969d
2 changed files with 80 additions and 57 deletions

View File

@ -608,36 +608,44 @@ async function connectAndRegisterToMessageRouter() {
}); });
// 监听会话事件并处理 // 监听会话事件并处理
// Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader
grpcClient.on('sessionEvent', async (event: { grpcClient.on('sessionEvent', async (event: {
eventId: string; event_id: string;
eventType: string; event_type: string;
sessionId: string; session_id: string;
thresholdN: number; threshold_n: number;
thresholdT: number; threshold_t: number;
selectedParties: string[]; selected_parties: string[];
joinTokens: Record<string, string>; join_tokens: Record<string, string>;
messageHash?: Buffer; message_hash?: Buffer;
}) => { }) => {
debugLog.info('grpc', `Received session event: ${event.eventType} for session ${event.sessionId}`); debugLog.info('grpc', `Received session event: ${event.event_type} for session ${event.session_id}`);
const eventData: SessionEventData = { const eventData: SessionEventData = {
type: event.eventType, type: event.event_type,
sessionId: event.sessionId, sessionId: event.session_id,
thresholdN: event.thresholdN, thresholdN: event.threshold_n,
thresholdT: event.thresholdT, thresholdT: event.threshold_t,
selectedParties: event.selectedParties, selectedParties: event.selected_parties,
}; };
// 缓存事件(解决前端可能还未订阅的时序问题) // 缓存事件(解决前端可能还未订阅的时序问题)
cacheSessionEvent(event.sessionId, eventData); cacheSessionEvent(event.session_id, eventData);
// 转发事件到前端 // 转发事件到前端
mainWindow?.webContents.send(`session:events:${event.sessionId}`, eventData); mainWindow?.webContents.send(`session:events:${event.session_id}`, eventData);
// 根据事件类型处理 // 根据事件类型处理
if (event.eventType === 'session_started' || event.eventType === 'all_joined') { if (event.event_type === 'session_started' || event.event_type === 'all_joined') {
// 当会话开始或所有参与方都加入时,检查是否需要触发 keygen // 当会话开始或所有参与方都加入时,检查是否需要触发 keygen
await handleSessionStart(event); // Convert snake_case to camelCase for handleSessionStart
await handleSessionStart({
eventType: event.event_type,
sessionId: event.session_id,
thresholdN: event.threshold_n,
thresholdT: event.threshold_t,
selectedParties: event.selected_parties,
});
} }
}); });
} catch (error) { } catch (error) {
@ -680,16 +688,17 @@ function setupIpcHandlers() {
const result = await grpcClient?.joinSession(sessionId, partyId, joinToken); const result = await grpcClient?.joinSession(sessionId, partyId, joinToken);
if (result?.success) { if (result?.success) {
// 设置活跃的 keygen 会话信息 // 设置活跃的 keygen 会话信息
const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.otherParties?.map((p: { partyId: string; partyIndex: number }, idx: number) => ({ // Note: gRPC response uses snake_case field names due to keepCase: true in proto-loader
partyId: p.partyId, const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({
partyIndex: p.partyIndex, partyId: p.party_id,
partyIndex: p.party_index,
name: `参与方 ${idx + 1}`, // 暂时使用默认名称 name: `参与方 ${idx + 1}`, // 暂时使用默认名称
})) || []; })) || [];
// 添加自己到参与者列表 // 添加自己到参与者列表
participants.push({ participants.push({
partyId: partyId, partyId: partyId,
partyIndex: result.partyIndex, partyIndex: result.party_index,
name: '我', name: '我',
}); });
@ -698,13 +707,13 @@ function setupIpcHandlers() {
activeKeygenSession = { activeKeygenSession = {
sessionId: sessionId, sessionId: sessionId,
partyIndex: result.partyIndex, partyIndex: result.party_index,
participants: participants, participants: participants,
threshold: { threshold: {
t: result.sessionInfo?.thresholdT || 0, t: result.session_info?.threshold_t || 0,
n: result.sessionInfo?.thresholdN || 0, n: result.session_info?.threshold_n || 0,
}, },
walletName: walletName || result.sessionInfo?.sessionId || 'Shared Wallet', walletName: walletName || result.session_info?.session_id || 'Shared Wallet',
encryptionPassword: '', // 不使用加密密码 encryptionPassword: '', // 不使用加密密码
}; };
@ -768,18 +777,19 @@ function setupIpcHandlers() {
if (joinResult?.success) { if (joinResult?.success) {
// 设置活跃的 keygen 会话信息 // 设置活跃的 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 }> = []; const participants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
// 添加发起方(自己) // 添加发起方(自己)
participants.push({ participants.push({
partyId: partyId, partyId: partyId,
partyIndex: joinResult.partyIndex, partyIndex: joinResult.party_index,
name: params.initiatorName || '发起者', name: params.initiatorName || '发起者',
}); });
activeKeygenSession = { activeKeygenSession = {
sessionId: result.session_id, sessionId: result.session_id,
partyIndex: joinResult.partyIndex, partyIndex: joinResult.party_index,
participants: participants, participants: participants,
threshold: { threshold: {
t: params.thresholdT, t: params.thresholdT,

View File

@ -44,48 +44,65 @@ function loadProtoDefinition(): protoLoader.PackageDefinition {
return packageDefinition; return packageDefinition;
} }
// Note: field names must match proto definitions with keepCase: true
// Proto uses snake_case: session_id, session_type, threshold_n, threshold_t
interface SessionInfo { interface SessionInfo {
sessionId: string; session_id: string;
sessionType: string; session_type: string;
thresholdN: number; threshold_n: number;
thresholdT: number; threshold_t: number;
messageHash?: Buffer; message_hash?: Buffer;
keygenSessionId?: string; keygen_session_id?: string;
status?: string;
} }
interface PartyInfo { interface PartyInfo {
partyId: string; party_id: string;
partyIndex: number; party_index: number;
} }
interface JoinSessionResponse { interface JoinSessionResponse {
success: boolean; success: boolean;
sessionInfo?: SessionInfo; session_info?: SessionInfo;
partyIndex: number; party_index: number;
otherParties: PartyInfo[]; other_parties: PartyInfo[];
} }
interface MPCMessage { interface MPCMessage {
messageId: string; message_id: string;
sessionId: string; session_id: string;
fromParty: string; from_party: string;
isBroadcast: boolean; is_broadcast: boolean;
roundNumber: number; round_number: number;
payload: Buffer; payload: Buffer;
createdAt: string; created_at: string;
} }
interface SessionEvent { interface SessionEvent {
eventId: string; event_id: string;
eventType: string; event_type: string;
sessionId: string; session_id: string;
thresholdN: number; threshold_n: number;
thresholdT: number; threshold_t: number;
selectedParties: string[]; selected_parties: string[];
joinTokens: Record<string, string>; join_tokens: Record<string, string>;
messageHash?: Buffer; message_hash?: Buffer;
} }
// Raw proto response (snake_case)
interface RegisteredPartyProto {
party_id: string;
role: string;
online: boolean;
registered_at: string;
last_seen_at: string;
}
interface GetRegisteredPartiesResponse {
parties: RegisteredPartyProto[];
}
// Converted response (camelCase) - used by callers
interface RegisteredParty { interface RegisteredParty {
partyId: string; partyId: string;
role: string; role: string;
@ -94,10 +111,6 @@ interface RegisteredParty {
lastSeenAt: string; lastSeenAt: string;
} }
interface GetRegisteredPartiesResponse {
parties: RegisteredParty[];
}
// 重连配置 // 重连配置
interface ReconnectConfig { interface ReconnectConfig {
maxRetries: number; maxRetries: number;