/** * Zulip客户端核心服务 * * 功能描述: * - 封装Zulip REST API调用 * - 实现API Key验证和错误处理 * - 提供消息发送、事件队列管理等核心功能 * * 主要方法: * - initialize(): 初始化Zulip客户端并验证API Key * - sendMessage(): 发送消息到指定Stream/Topic * - registerQueue(): 注册事件队列 * - deregisterQueue(): 注销事件队列 * - getEvents(): 获取事件队列中的事件 * * 使用场景: * - 用户登录时创建和验证Zulip客户端 * - 消息发送和接收 * - 事件队列管理 * * @author 开发团队 * @version 1.0.0 * @since 2025-12-25 */ import { Injectable, Logger } from '@nestjs/common'; import { ZulipAPI, Internal, Enums } from '../interfaces/zulip.interfaces'; /** * Zulip客户端配置接口 */ export interface ZulipClientConfig { username: string; apiKey: string; realm: string; } /** * Zulip客户端实例接口 */ export interface ZulipClientInstance { userId: string; config: ZulipClientConfig; client: any; // zulip-js客户端实例 queueId?: string; lastEventId: number; createdAt: Date; lastActivity: Date; isValid: boolean; } /** * 发送消息结果接口 */ export interface SendMessageResult { success: boolean; messageId?: number; error?: string; } /** * 事件队列注册结果接口 */ export interface RegisterQueueResult { success: boolean; queueId?: string; lastEventId?: number; error?: string; } /** * 获取事件结果接口 */ export interface GetEventsResult { success: boolean; events?: ZulipAPI.Event[]; error?: string; } @Injectable() export class ZulipClientService { private readonly logger = new Logger(ZulipClientService.name); constructor() { this.logger.log('ZulipClientService初始化完成'); } /** * 创建并初始化Zulip客户端 * * 功能描述: * 使用提供的配置创建zulip-js客户端实例,并验证API Key的有效性 * * 业务逻辑: * 1. 验证配置参数的完整性 * 2. 创建zulip-js客户端实例 * 3. 调用API验证凭证有效性 * 4. 返回初始化后的客户端实例 * * @param userId 用户ID * @param config Zulip客户端配置 * @returns Promise 初始化后的客户端实例 * * @throws Error 当配置无效或API Key验证失败时 */ async createClient(userId: string, config: ZulipClientConfig): Promise { const startTime = Date.now(); this.logger.log('开始创建Zulip客户端', { operation: 'createClient', userId, realm: config.realm, timestamp: new Date().toISOString(), }); try { // 1. 验证配置参数 this.validateConfig(config); // 2. 动态导入zulip-js const zulipInit = await this.loadZulipModule(); // 3. 创建zulip-js客户端实例 const client = await zulipInit({ username: config.username, apiKey: config.apiKey, realm: config.realm, }); // 4. 验证API Key有效性 - 通过获取用户信息 const profile = await client.users.me.getProfile(); if (profile.result !== 'success') { throw new Error(`API Key验证失败: ${profile.msg || '未知错误'}`); } const clientInstance: ZulipClientInstance = { userId, config, client, lastEventId: -1, createdAt: new Date(), lastActivity: new Date(), isValid: true, }; const duration = Date.now() - startTime; this.logger.log('Zulip客户端创建成功', { operation: 'createClient', userId, realm: config.realm, userEmail: profile.email, duration, timestamp: new Date().toISOString(), }); return clientInstance; } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('创建Zulip客户端失败', { operation: 'createClient', userId, realm: config.realm, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); throw new Error(`创建Zulip客户端失败: ${err.message}`); } } /** * 验证API Key有效性 * * 功能描述: * 通过调用Zulip API验证API Key是否有效 * * @param clientInstance Zulip客户端实例 * @returns Promise API Key是否有效 */ async validateApiKey(clientInstance: ZulipClientInstance): Promise { this.logger.log('验证API Key有效性', { operation: 'validateApiKey', userId: clientInstance.userId, timestamp: new Date().toISOString(), }); try { const profile = await clientInstance.client.users.me.getProfile(); const isValid = profile.result === 'success'; clientInstance.isValid = isValid; clientInstance.lastActivity = new Date(); this.logger.log('API Key验证完成', { operation: 'validateApiKey', userId: clientInstance.userId, isValid, timestamp: new Date().toISOString(), }); return isValid; } catch (error) { const err = error as Error; this.logger.error('API Key验证失败', { operation: 'validateApiKey', userId: clientInstance.userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); clientInstance.isValid = false; return false; } } /** * 发送消息到指定Stream/Topic * * 功能描述: * 使用Zulip客户端发送消息到指定的Stream和Topic * * 业务逻辑: * 1. 验证客户端实例有效性 * 2. 构建消息请求参数 * 3. 调用Zulip API发送消息 * 4. 处理响应并返回结果 * * @param clientInstance Zulip客户端实例 * @param stream 目标Stream名称 * @param topic 目标Topic名称 * @param content 消息内容 * @returns Promise 发送结果 */ async sendMessage( clientInstance: ZulipClientInstance, stream: string, topic: string, content: string, ): Promise { const startTime = Date.now(); this.logger.log('发送消息到Zulip', { operation: 'sendMessage', userId: clientInstance.userId, stream, topic, contentLength: content.length, timestamp: new Date().toISOString(), }); try { // 1. 验证客户端有效性 if (!clientInstance.isValid) { throw new Error('Zulip客户端无效'); } // 2. 构建消息参数 const params = { type: 'stream', to: stream, subject: topic, content: content, }; // 3. 发送消息 const response = await clientInstance.client.messages.send(params); // 4. 更新最后活动时间 clientInstance.lastActivity = new Date(); const duration = Date.now() - startTime; if (response.result === 'success') { this.logger.log('消息发送成功', { operation: 'sendMessage', userId: clientInstance.userId, stream, topic, messageId: response.id, duration, timestamp: new Date().toISOString(), }); return { success: true, messageId: response.id, }; } else { this.logger.warn('消息发送失败', { operation: 'sendMessage', userId: clientInstance.userId, stream, topic, error: response.msg, duration, timestamp: new Date().toISOString(), }); return { success: false, error: response.msg || '消息发送失败', }; } } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('发送消息异常', { operation: 'sendMessage', userId: clientInstance.userId, stream, topic, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, error: err.message, }; } } /** * 注册事件队列 * * 功能描述: * 向Zulip服务器注册事件队列,用于接收消息通知 * * 业务逻辑: * 1. 验证客户端实例有效性 * 2. 构建队列注册参数 * 3. 调用Zulip API注册队列 * 4. 保存队列ID到客户端实例 * * @param clientInstance Zulip客户端实例 * @param eventTypes 要订阅的事件类型列表 * @returns Promise 注册结果 */ async registerQueue( clientInstance: ZulipClientInstance, eventTypes: string[] = ['message'], ): Promise { const startTime = Date.now(); this.logger.log('注册Zulip事件队列', { operation: 'registerQueue', userId: clientInstance.userId, eventTypes, timestamp: new Date().toISOString(), }); try { // 1. 验证客户端有效性 if (!clientInstance.isValid) { throw new Error('Zulip客户端无效'); } // 2. 构建注册参数 const params = { event_types: eventTypes, }; // 3. 注册队列 const response = await clientInstance.client.queues.register(params); const duration = Date.now() - startTime; if (response.result === 'success') { // 4. 保存队列信息 clientInstance.queueId = response.queue_id; clientInstance.lastEventId = response.last_event_id; clientInstance.lastActivity = new Date(); this.logger.log('事件队列注册成功', { operation: 'registerQueue', userId: clientInstance.userId, queueId: response.queue_id, lastEventId: response.last_event_id, duration, timestamp: new Date().toISOString(), }); return { success: true, queueId: response.queue_id, lastEventId: response.last_event_id, }; } else { this.logger.warn('事件队列注册失败', { operation: 'registerQueue', userId: clientInstance.userId, error: response.msg, duration, timestamp: new Date().toISOString(), }); return { success: false, error: response.msg || '事件队列注册失败', }; } } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('注册事件队列异常', { operation: 'registerQueue', userId: clientInstance.userId, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, error: err.message, }; } } /** * 注销事件队列 * * 功能描述: * 注销已注册的Zulip事件队列 * * @param clientInstance Zulip客户端实例 * @returns Promise 是否成功注销 */ async deregisterQueue(clientInstance: ZulipClientInstance): Promise { this.logger.log('注销Zulip事件队列', { operation: 'deregisterQueue', userId: clientInstance.userId, queueId: clientInstance.queueId, timestamp: new Date().toISOString(), }); try { if (!clientInstance.queueId) { this.logger.log('无事件队列需要注销', { operation: 'deregisterQueue', userId: clientInstance.userId, }); return true; } const response = await clientInstance.client.queues.deregister({ queue_id: clientInstance.queueId, }); if (response.result === 'success') { clientInstance.queueId = undefined; clientInstance.lastEventId = -1; this.logger.log('事件队列注销成功', { operation: 'deregisterQueue', userId: clientInstance.userId, timestamp: new Date().toISOString(), }); return true; } else { this.logger.warn('事件队列注销失败', { operation: 'deregisterQueue', userId: clientInstance.userId, error: response.msg, timestamp: new Date().toISOString(), }); return false; } } catch (error) { const err = error as Error; // 如果是JSON解析错误,说明队列可能已经过期或被删除,这是正常的 if (err.message.includes('invalid json response') || err.message.includes('Unexpected token')) { this.logger.debug('事件队列可能已过期,跳过注销', { operation: 'deregisterQueue', userId: clientInstance.userId, queueId: clientInstance.queueId, }); // 清理本地状态 clientInstance.queueId = undefined; clientInstance.lastEventId = -1; return true; } this.logger.error('注销事件队列异常', { operation: 'deregisterQueue', userId: clientInstance.userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); // 即使注销失败,也清理本地状态 clientInstance.queueId = undefined; clientInstance.lastEventId = -1; return false; } } /** * 获取事件队列中的事件 * * 功能描述: * 从Zulip事件队列中获取新事件 * * @param clientInstance Zulip客户端实例 * @param dontBlock 是否不阻塞等待新事件 * @returns Promise 获取结果 */ async getEvents( clientInstance: ZulipClientInstance, dontBlock: boolean = false, ): Promise { this.logger.debug('获取Zulip事件', { operation: 'getEvents', userId: clientInstance.userId, queueId: clientInstance.queueId, lastEventId: clientInstance.lastEventId, dontBlock, timestamp: new Date().toISOString(), }); try { if (!clientInstance.queueId) { throw new Error('事件队列未注册'); } const params = { queue_id: clientInstance.queueId, last_event_id: clientInstance.lastEventId, dont_block: dontBlock, }; const response = await clientInstance.client.events.retrieve(params); if (response.result === 'success') { // 更新最后事件ID if (response.events && response.events.length > 0) { const lastEvent = response.events[response.events.length - 1]; clientInstance.lastEventId = lastEvent.id; } clientInstance.lastActivity = new Date(); this.logger.debug('获取事件成功', { operation: 'getEvents', userId: clientInstance.userId, eventCount: response.events?.length || 0, timestamp: new Date().toISOString(), }); return { success: true, events: response.events || [], }; } else { this.logger.warn('获取事件失败', { operation: 'getEvents', userId: clientInstance.userId, error: response.msg, timestamp: new Date().toISOString(), }); return { success: false, error: response.msg || '获取事件失败', }; } } catch (error) { const err = error as Error; this.logger.error('获取事件异常', { operation: 'getEvents', userId: clientInstance.userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); return { success: false, error: err.message, }; } } /** * 销毁客户端实例 * * 功能描述: * 清理客户端资源,注销事件队列 * * @param clientInstance Zulip客户端实例 * @returns Promise */ async destroyClient(clientInstance: ZulipClientInstance): Promise { this.logger.log('销毁Zulip客户端', { operation: 'destroyClient', userId: clientInstance.userId, queueId: clientInstance.queueId, timestamp: new Date().toISOString(), }); try { // 注销事件队列 if (clientInstance.queueId) { await this.deregisterQueue(clientInstance); } // 标记客户端为无效 clientInstance.isValid = false; clientInstance.client = null; this.logger.log('Zulip客户端销毁完成', { operation: 'destroyClient', userId: clientInstance.userId, timestamp: new Date().toISOString(), }); } catch (error) { const err = error as Error; this.logger.error('销毁Zulip客户端异常', { operation: 'destroyClient', userId: clientInstance.userId, error: err.message, timestamp: new Date().toISOString(), }, err.stack); // 即使出错也标记为无效 clientInstance.isValid = false; clientInstance.client = null; } } /** * 验证配置参数 * * @param config Zulip客户端配置 * @throws Error 当配置无效时 * @private */ private validateConfig(config: ZulipClientConfig): void { if (!config.username || typeof config.username !== 'string') { throw new Error('无效的username配置'); } if (!config.apiKey || typeof config.apiKey !== 'string') { throw new Error('无效的apiKey配置'); } if (!config.realm || typeof config.realm !== 'string') { throw new Error('无效的realm配置'); } // 验证realm是否为有效URL try { new URL(config.realm); } catch { throw new Error('realm必须是有效的URL'); } } /** * 动态加载zulip-js模块 * * @returns Promise zulip-js初始化函数 * @private */ private async loadZulipModule(): Promise { try { // 使用动态导入加载zulip-js const zulipModule = await import('zulip-js'); return zulipModule.default || zulipModule; } catch (error) { const err = error as Error; this.logger.error('加载zulip-js模块失败', { operation: 'loadZulipModule', error: err.message, timestamp: new Date().toISOString(), }, err.stack); throw new Error(`加载zulip-js模块失败: ${err.message}`); } } }