737 lines
21 KiB
TypeScript
737 lines
21 KiB
TypeScript
import * as grpc from '@grpc/grpc-js';
|
|
import * as protoLoader from '@grpc/proto-loader';
|
|
import * as path from 'path';
|
|
import { EventEmitter } from 'events';
|
|
import { app } from 'electron';
|
|
|
|
// 定义 proto 包结构类型
|
|
interface ProtoPackage {
|
|
mpc?: {
|
|
router?: {
|
|
v1?: {
|
|
MessageRouter?: grpc.ServiceClientConstructor;
|
|
};
|
|
};
|
|
};
|
|
}
|
|
|
|
// 延迟加载的 Proto 定义
|
|
let packageDefinition: protoLoader.PackageDefinition | null = null;
|
|
|
|
// Proto 文件路径 - 在打包后需要从 app.asar.unpacked 或 resources 目录加载
|
|
function getProtoPath(): string {
|
|
// 开发环境
|
|
if (!app.isPackaged) {
|
|
return path.join(__dirname, '../../proto/message_router.proto');
|
|
}
|
|
// 生产环境 - proto 文件需要解包
|
|
return path.join(process.resourcesPath, 'proto/message_router.proto');
|
|
}
|
|
|
|
// 延迟加载 Proto 定义
|
|
function loadProtoDefinition(): protoLoader.PackageDefinition {
|
|
if (!packageDefinition) {
|
|
const protoPath = getProtoPath();
|
|
console.log('Loading proto from:', protoPath);
|
|
packageDefinition = protoLoader.loadSync(protoPath, {
|
|
keepCase: true,
|
|
longs: String,
|
|
enums: String,
|
|
defaults: true,
|
|
oneofs: true,
|
|
});
|
|
}
|
|
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 {
|
|
session_id: string;
|
|
session_type: string;
|
|
threshold_n: number;
|
|
threshold_t: number;
|
|
message_hash?: Buffer;
|
|
keygen_session_id?: string;
|
|
status?: string;
|
|
}
|
|
|
|
interface PartyInfo {
|
|
party_id: string;
|
|
party_index: number;
|
|
}
|
|
|
|
interface JoinSessionResponse {
|
|
success: boolean;
|
|
session_info?: SessionInfo;
|
|
party_index: number;
|
|
other_parties: PartyInfo[];
|
|
}
|
|
|
|
interface MPCMessage {
|
|
message_id: string;
|
|
session_id: string;
|
|
from_party: string;
|
|
is_broadcast: boolean;
|
|
round_number: number;
|
|
payload: Buffer;
|
|
created_at: string;
|
|
}
|
|
|
|
interface SessionEvent {
|
|
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;
|
|
}
|
|
|
|
// 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 {
|
|
partyId: string;
|
|
role: string;
|
|
online: boolean;
|
|
registeredAt: string;
|
|
lastSeenAt: string;
|
|
}
|
|
|
|
// 重连配置
|
|
interface ReconnectConfig {
|
|
maxRetries: number;
|
|
initialDelayMs: number;
|
|
maxDelayMs: number;
|
|
backoffMultiplier: number;
|
|
}
|
|
|
|
const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
|
|
maxRetries: 10,
|
|
initialDelayMs: 1000,
|
|
maxDelayMs: 30000,
|
|
backoffMultiplier: 2,
|
|
};
|
|
|
|
/**
|
|
* gRPC 客户端 - 连接到 Message Router
|
|
*
|
|
* 连接地址格式:
|
|
* - 开发环境: localhost:50051 (不加密)
|
|
* - 生产环境: mpc-grpc.szaiai.com:443 (TLS 加密)
|
|
*
|
|
* 特性:
|
|
* - 自动重连机制(指数退避)
|
|
* - 事件流断开后自动重新订阅
|
|
* - 心跳失败后自动重连
|
|
*/
|
|
export class GrpcClient extends EventEmitter {
|
|
private client: grpc.Client | null = null;
|
|
private connected = false;
|
|
private partyId: string | null = null;
|
|
private partyRole: string | null = null;
|
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
private messageStream: grpc.ClientReadableStream<MPCMessage> | null = null;
|
|
private eventStream: grpc.ClientReadableStream<SessionEvent> | null = null;
|
|
|
|
// 重连相关
|
|
private reconnectConfig: ReconnectConfig;
|
|
private currentAddress: string | null = null;
|
|
private currentUseTLS: boolean | undefined;
|
|
private isReconnecting = false;
|
|
private reconnectAttempts = 0;
|
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
private shouldReconnect = true;
|
|
|
|
// 消息流状态(用于重连后恢复)
|
|
private activeMessageSubscription: { sessionId: string; partyId: string } | null = null;
|
|
private eventStreamSubscribed = false;
|
|
|
|
// 心跳失败计数
|
|
private heartbeatFailCount = 0;
|
|
private readonly MAX_HEARTBEAT_FAILS = 3;
|
|
|
|
constructor(reconnectConfig?: Partial<ReconnectConfig>) {
|
|
super();
|
|
this.reconnectConfig = { ...DEFAULT_RECONNECT_CONFIG, ...reconnectConfig };
|
|
}
|
|
|
|
/**
|
|
* 连接到 Message Router
|
|
* @param address 完整地址,格式: host:port (例如 mpc-grpc.szaiai.com:443 或 localhost:50051)
|
|
* @param useTLS 是否使用 TLS 加密 (默认: 自动检测,端口 443 使用 TLS)
|
|
*/
|
|
async connect(address: string, useTLS?: boolean): Promise<void> {
|
|
// 保存连接参数用于重连
|
|
this.currentAddress = address;
|
|
this.currentUseTLS = useTLS;
|
|
this.shouldReconnect = true;
|
|
|
|
return this.doConnect(address, useTLS);
|
|
}
|
|
|
|
private async doConnect(address: string, useTLS?: boolean): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const definition = loadProtoDefinition();
|
|
const proto = grpc.loadPackageDefinition(definition) as ProtoPackage;
|
|
const MessageRouter = proto.mpc?.router?.v1?.MessageRouter;
|
|
|
|
if (!MessageRouter) {
|
|
reject(new Error('Failed to load MessageRouter service definition'));
|
|
return;
|
|
}
|
|
|
|
// 解析地址,如果没有端口则默认使用 443
|
|
let targetAddress = address;
|
|
if (!address.includes(':')) {
|
|
targetAddress = `${address}:443`;
|
|
}
|
|
|
|
// 自动检测是否使用 TLS: 端口 443 或显式指定
|
|
const port = parseInt(targetAddress.split(':')[1], 10);
|
|
const shouldUseTLS = useTLS !== undefined ? useTLS : (port === 443);
|
|
|
|
// 创建凭证
|
|
const credentials = shouldUseTLS
|
|
? grpc.credentials.createSsl() // TLS 加密 (生产环境)
|
|
: grpc.credentials.createInsecure(); // 不加密 (开发环境)
|
|
|
|
console.log(`[gRPC] Connecting to Message Router: ${targetAddress} (TLS: ${shouldUseTLS})`);
|
|
|
|
this.client = new MessageRouter(
|
|
targetAddress,
|
|
credentials
|
|
) as grpc.Client;
|
|
|
|
// 等待连接就绪
|
|
const deadline = new Date();
|
|
deadline.setSeconds(deadline.getSeconds() + 10);
|
|
|
|
(this.client as grpc.Client & { waitForReady: (deadline: Date, callback: (err?: Error) => void) => void })
|
|
.waitForReady(deadline, (err?: Error) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
this.connected = true;
|
|
this.reconnectAttempts = 0; // 重置重连计数
|
|
this.heartbeatFailCount = 0;
|
|
console.log('[gRPC] Connected successfully');
|
|
this.emit('connected');
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 断开连接(不会自动重连)
|
|
*/
|
|
disconnect(): void {
|
|
this.shouldReconnect = false;
|
|
this.cleanupConnection();
|
|
}
|
|
|
|
/**
|
|
* 清理连接资源
|
|
*/
|
|
private cleanupConnection(): void {
|
|
if (this.reconnectTimeout) {
|
|
clearTimeout(this.reconnectTimeout);
|
|
this.reconnectTimeout = null;
|
|
}
|
|
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
this.heartbeatInterval = null;
|
|
}
|
|
|
|
if (this.messageStream) {
|
|
try {
|
|
this.messageStream.cancel();
|
|
} catch (e) {
|
|
// 忽略取消错误
|
|
}
|
|
this.messageStream = null;
|
|
}
|
|
|
|
if (this.eventStream) {
|
|
try {
|
|
this.eventStream.cancel();
|
|
} catch (e) {
|
|
// 忽略取消错误
|
|
}
|
|
this.eventStream = null;
|
|
}
|
|
|
|
if (this.client) {
|
|
try {
|
|
(this.client as grpc.Client & { close: () => void }).close();
|
|
} catch (e) {
|
|
// 忽略关闭错误
|
|
}
|
|
this.client = null;
|
|
}
|
|
|
|
this.connected = false;
|
|
}
|
|
|
|
/**
|
|
* 触发重连
|
|
*/
|
|
private async triggerReconnect(reason: string): Promise<void> {
|
|
if (!this.shouldReconnect || this.isReconnecting || !this.currentAddress) {
|
|
return;
|
|
}
|
|
|
|
console.log(`[gRPC] Triggering reconnect: ${reason}`);
|
|
this.isReconnecting = true;
|
|
this.connected = false;
|
|
this.emit('disconnected', reason);
|
|
|
|
// 清理现有连接
|
|
this.cleanupConnection();
|
|
|
|
// 计算延迟时间(指数退避)
|
|
const delay = Math.min(
|
|
this.reconnectConfig.initialDelayMs * Math.pow(this.reconnectConfig.backoffMultiplier, this.reconnectAttempts),
|
|
this.reconnectConfig.maxDelayMs
|
|
);
|
|
|
|
console.log(`[gRPC] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.reconnectConfig.maxRetries})`);
|
|
|
|
this.reconnectTimeout = setTimeout(async () => {
|
|
this.reconnectAttempts++;
|
|
|
|
if (this.reconnectAttempts > this.reconnectConfig.maxRetries) {
|
|
console.error('[gRPC] Max reconnect attempts reached');
|
|
this.isReconnecting = false;
|
|
this.emit('reconnectFailed', 'Max retries exceeded');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.doConnect(this.currentAddress!, this.currentUseTLS);
|
|
|
|
// 重新注册
|
|
if (this.partyId && this.partyRole) {
|
|
console.log(`[gRPC] Re-registering as party: ${this.partyId}`);
|
|
await this.registerParty(this.partyId, this.partyRole);
|
|
}
|
|
|
|
// 重新订阅事件流
|
|
if (this.eventStreamSubscribed && this.partyId) {
|
|
console.log('[gRPC] Re-subscribing to session events');
|
|
this.subscribeSessionEvents(this.partyId);
|
|
}
|
|
|
|
// 重新订阅消息流
|
|
if (this.activeMessageSubscription) {
|
|
console.log(`[gRPC] Re-subscribing to messages for session: ${this.activeMessageSubscription.sessionId}`);
|
|
this.subscribeMessages(this.activeMessageSubscription.sessionId, this.activeMessageSubscription.partyId);
|
|
}
|
|
|
|
this.isReconnecting = false;
|
|
this.emit('reconnected');
|
|
} catch (err) {
|
|
console.error(`[gRPC] Reconnect attempt ${this.reconnectAttempts} failed:`, (err as Error).message);
|
|
this.isReconnecting = false;
|
|
// 继续尝试重连
|
|
this.triggerReconnect('Previous reconnect attempt failed');
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
/**
|
|
* 检查是否已连接
|
|
*/
|
|
isConnected(): boolean {
|
|
return this.connected;
|
|
}
|
|
|
|
/**
|
|
* 获取当前 Party ID
|
|
*/
|
|
getPartyId(): string | null {
|
|
return this.partyId;
|
|
}
|
|
|
|
/**
|
|
* 注册为参与方
|
|
*/
|
|
async registerParty(partyId: string, role: string): Promise<void> {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
(this.client as grpc.Client & { registerParty: (req: unknown, callback: (err: Error | null, res: { success: boolean }) => void) => void })
|
|
.registerParty(
|
|
{
|
|
party_id: partyId,
|
|
party_role: role,
|
|
version: '1.0.0',
|
|
},
|
|
(err: Error | null, response: { success: boolean }) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else if (!response.success) {
|
|
reject(new Error('Registration failed'));
|
|
} else {
|
|
this.partyId = partyId;
|
|
this.partyRole = role;
|
|
this.startHeartbeat();
|
|
resolve();
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 开始心跳(带重连逻辑)
|
|
*/
|
|
private startHeartbeat(): void {
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
}
|
|
|
|
this.heartbeatFailCount = 0;
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
if (this.client && this.partyId) {
|
|
(this.client as grpc.Client & { heartbeat: (req: unknown, callback: (err: Error | null) => void) => void })
|
|
.heartbeat(
|
|
{ party_id: this.partyId },
|
|
(err: Error | null) => {
|
|
if (err) {
|
|
this.heartbeatFailCount++;
|
|
console.error(`[gRPC] Heartbeat failed (${this.heartbeatFailCount}/${this.MAX_HEARTBEAT_FAILS}):`, err.message);
|
|
this.emit('connectionError', err);
|
|
|
|
// 连续失败多次后触发重连
|
|
if (this.heartbeatFailCount >= this.MAX_HEARTBEAT_FAILS) {
|
|
this.triggerReconnect('Heartbeat failed');
|
|
}
|
|
} else {
|
|
// 心跳成功,重置失败计数
|
|
this.heartbeatFailCount = 0;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}, 30000); // 每 30 秒一次
|
|
}
|
|
|
|
/**
|
|
* 加入会话
|
|
*/
|
|
async joinSession(sessionId: string, partyId: string, joinToken: string): Promise<JoinSessionResponse> {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
(this.client as grpc.Client & { joinSession: (req: unknown, callback: (err: Error | null, res: JoinSessionResponse) => void) => void })
|
|
.joinSession(
|
|
{
|
|
session_id: sessionId,
|
|
party_id: partyId,
|
|
join_token: joinToken,
|
|
},
|
|
(err: Error | null, response: JoinSessionResponse) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(response);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 订阅会话事件(带自动重连)
|
|
*/
|
|
subscribeSessionEvents(partyId: string): void {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
// 标记已订阅(用于重连后恢复)
|
|
this.eventStreamSubscribed = true;
|
|
|
|
// 取消现有流 - 先移除事件监听器再取消,防止旧流的 end 事件触发重连
|
|
if (this.eventStream) {
|
|
const oldStream = this.eventStream;
|
|
oldStream.removeAllListeners();
|
|
try {
|
|
oldStream.cancel();
|
|
} catch (e) {
|
|
// 忽略
|
|
}
|
|
}
|
|
|
|
this.eventStream = (this.client as grpc.Client & { subscribeSessionEvents: (req: unknown) => grpc.ClientReadableStream<SessionEvent> })
|
|
.subscribeSessionEvents({ party_id: partyId });
|
|
|
|
// 保存当前流的引用,用于在事件处理器中检查是否是当前活跃的流
|
|
const currentStream = this.eventStream;
|
|
|
|
this.eventStream.on('data', (event: SessionEvent) => {
|
|
this.emit('sessionEvent', event);
|
|
});
|
|
|
|
this.eventStream.on('error', (err: Error) => {
|
|
console.error('[gRPC] Session event stream error:', err.message);
|
|
this.emit('streamError', err);
|
|
|
|
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
|
|
if (currentStream !== this.eventStream) {
|
|
console.log('[gRPC] Ignoring error from old event stream');
|
|
return;
|
|
}
|
|
|
|
// 非主动取消的错误触发重连
|
|
if (!err.message.includes('CANCELLED') && this.shouldReconnect) {
|
|
this.triggerReconnect('Event stream error');
|
|
}
|
|
});
|
|
|
|
this.eventStream.on('end', () => {
|
|
console.log('[gRPC] Session event stream ended');
|
|
this.emit('streamEnd');
|
|
|
|
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
|
|
if (currentStream !== this.eventStream) {
|
|
console.log('[gRPC] Ignoring end from old event stream');
|
|
return;
|
|
}
|
|
|
|
// 流结束也触发重连
|
|
if (this.shouldReconnect && this.eventStreamSubscribed) {
|
|
this.triggerReconnect('Event stream ended');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 取消订阅会话事件
|
|
*/
|
|
unsubscribeSessionEvents(): void {
|
|
this.eventStreamSubscribed = false;
|
|
if (this.eventStream) {
|
|
try {
|
|
this.eventStream.cancel();
|
|
} catch (e) {
|
|
// 忽略
|
|
}
|
|
this.eventStream = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 订阅 MPC 消息(带自动重连)
|
|
*/
|
|
subscribeMessages(sessionId: string, partyId: string): void {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
if (!this.connected) {
|
|
throw new Error('gRPC client not connected');
|
|
}
|
|
|
|
// 保存订阅状态(用于重连后恢复)
|
|
this.activeMessageSubscription = { sessionId, partyId };
|
|
|
|
// 取消现有流 - 先移除事件监听器再取消,防止旧流的 end 事件触发重连
|
|
if (this.messageStream) {
|
|
const oldStream = this.messageStream;
|
|
oldStream.removeAllListeners();
|
|
try {
|
|
oldStream.cancel();
|
|
} catch (e) {
|
|
console.log('[gRPC] Ignored error while canceling old message stream:', (e as Error).message);
|
|
}
|
|
this.messageStream = null;
|
|
}
|
|
|
|
try {
|
|
this.messageStream = (this.client as grpc.Client & { subscribeMessages: (req: unknown) => grpc.ClientReadableStream<MPCMessage> })
|
|
.subscribeMessages({
|
|
session_id: sessionId,
|
|
party_id: partyId,
|
|
});
|
|
} catch (e) {
|
|
console.error('[gRPC] Failed to create message stream:', (e as Error).message);
|
|
this.activeMessageSubscription = null;
|
|
throw e;
|
|
}
|
|
|
|
// 保存当前流的引用,用于在事件处理器中检查是否是当前活跃的流
|
|
const currentStream = this.messageStream;
|
|
|
|
this.messageStream.on('data', (message: MPCMessage) => {
|
|
this.emit('mpcMessage', message);
|
|
});
|
|
|
|
this.messageStream.on('error', (err: Error) => {
|
|
console.error('[gRPC] Message stream error:', err.message);
|
|
this.emit('messageStreamError', err);
|
|
|
|
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
|
|
if (currentStream !== this.messageStream) {
|
|
console.log('[gRPC] Ignoring error from old message stream');
|
|
return;
|
|
}
|
|
|
|
// 非主动取消的错误触发重连
|
|
if (!err.message.includes('CANCELLED') && this.shouldReconnect && this.activeMessageSubscription) {
|
|
this.triggerReconnect('Message stream error');
|
|
}
|
|
});
|
|
|
|
this.messageStream.on('end', () => {
|
|
console.log('[gRPC] Message stream ended');
|
|
this.emit('messageStreamEnd');
|
|
|
|
// 只有当前活跃的流才触发重连,防止旧流的事件干扰
|
|
if (currentStream !== this.messageStream) {
|
|
console.log('[gRPC] Ignoring end from old message stream');
|
|
return;
|
|
}
|
|
|
|
// 流结束也触发重连
|
|
if (this.shouldReconnect && this.activeMessageSubscription) {
|
|
this.triggerReconnect('Message stream ended');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 取消订阅 MPC 消息
|
|
*/
|
|
unsubscribeMessages(): void {
|
|
this.activeMessageSubscription = null;
|
|
if (this.messageStream) {
|
|
try {
|
|
this.messageStream.cancel();
|
|
} catch (e) {
|
|
// 忽略
|
|
}
|
|
this.messageStream = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 发送 MPC 消息
|
|
*/
|
|
async routeMessage(
|
|
sessionId: string,
|
|
fromParty: string,
|
|
toParties: string[],
|
|
roundNumber: number,
|
|
payload: Buffer
|
|
): Promise<string> {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
(this.client as grpc.Client & { routeMessage: (req: unknown, callback: (err: Error | null, res: { message_id: string }) => void) => void })
|
|
.routeMessage(
|
|
{
|
|
session_id: sessionId,
|
|
from_party: fromParty,
|
|
to_parties: toParties,
|
|
round_number: roundNumber,
|
|
payload: payload,
|
|
},
|
|
(err: Error | null, response: { message_id: string }) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(response.message_id);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 报告完成
|
|
*/
|
|
async reportCompletion(sessionId: string, partyId: string, publicKey: Buffer): Promise<boolean> {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
(this.client as grpc.Client & { reportCompletion: (req: unknown, callback: (err: Error | null, res: { all_completed: boolean }) => void) => void })
|
|
.reportCompletion(
|
|
{
|
|
session_id: sessionId,
|
|
party_id: partyId,
|
|
public_key: publicKey,
|
|
},
|
|
(err: Error | null, response: { all_completed: boolean }) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(response.all_completed);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 获取已注册的参与方列表
|
|
* @param roleFilter 可选,按角色过滤 (persistent/delegate/temporary)
|
|
* @param onlyOnline 可选,只返回在线的参与方
|
|
*/
|
|
async getRegisteredParties(roleFilter?: string, onlyOnline?: boolean): Promise<RegisteredParty[]> {
|
|
if (!this.client) {
|
|
throw new Error('Not connected');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
(this.client as grpc.Client & { getRegisteredParties: (req: unknown, callback: (err: Error | null, res: GetRegisteredPartiesResponse) => void) => void })
|
|
.getRegisteredParties(
|
|
{
|
|
role_filter: roleFilter || '',
|
|
only_online: onlyOnline || false,
|
|
},
|
|
(err: Error | null, response: GetRegisteredPartiesResponse) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
// 转换字段名从 snake_case 到 camelCase
|
|
const parties = (response.parties || []).map((p: { party_id?: string; partyId?: string; role?: string; party_role?: string; online?: boolean; registered_at?: string; registeredAt?: string; last_seen_at?: string; lastSeenAt?: string }) => ({
|
|
partyId: p.party_id || p.partyId || '',
|
|
role: p.role || p.party_role || '',
|
|
online: p.online || false,
|
|
registeredAt: p.registered_at || p.registeredAt || '',
|
|
lastSeenAt: p.last_seen_at || p.lastSeenAt || '',
|
|
}));
|
|
resolve(parties);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
}
|