Files
whale-town-end/src/business/zulip/services/zulip-client.service.ts
angjustinl 55cfda0532 feat(zulip): 添加全面的 Zulip 集成系统
* **新增 Zulip 模块**:包含完整的集成服务,涵盖客户端池(client pool)、会话管理及事件处理。
* **新增 WebSocket 网关**:用于处理 Zulip 的实时事件监听与双向通信。
* **新增安全服务**:支持 API 密钥加密存储及凭据的安全管理。
* **新增配置管理服务**:支持配置热加载(hot-reload),实现动态配置更新。
* **新增错误处理与监控服务**:提升系统的可靠性与可观测性。
* **新增消息过滤服务**:用于内容校验及速率限制(流控)。
* **新增流初始化与会话清理服务**:优化资源管理与回收。
* **完善测试覆盖**:包含单元测试及端到端(e2e)集成测试。
* **完善详细文档**:包括 API 参考手册、配置指南及集成概述。
* **新增地图配置系统**:实现游戏地点与 Zulip Stream(频道)及 Topic(话题)的逻辑映射。
* **新增环境变量配置**:涵盖 Zulip 服务器地址、身份验证及监控相关设置。
* **更新 App 模块**:注册并启用新的 Zulip 集成模块。
* **更新 Redis 接口**:以支持增强型的会话管理功能。
* **实现 WebSocket 协议支持**:确保与 Zulip 之间的实时双向通信。
2025-12-25 22:22:30 +08:00

705 lines
18 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.
/**
* 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<ZulipClientInstance> 初始化后的客户端实例
*
* @throws Error 当配置无效或API Key验证失败时
*/
async createClient(userId: string, config: ZulipClientConfig): Promise<ZulipClientInstance> {
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<boolean> API Key是否有效
*/
async validateApiKey(clientInstance: ZulipClientInstance): Promise<boolean> {
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<SendMessageResult> 发送结果
*/
async sendMessage(
clientInstance: ZulipClientInstance,
stream: string,
topic: string,
content: string,
): Promise<SendMessageResult> {
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<RegisterQueueResult> 注册结果
*/
async registerQueue(
clientInstance: ZulipClientInstance,
eventTypes: string[] = ['message'],
): Promise<RegisterQueueResult> {
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<boolean> 是否成功注销
*/
async deregisterQueue(clientInstance: ZulipClientInstance): Promise<boolean> {
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<GetEventsResult> 获取结果
*/
async getEvents(
clientInstance: ZulipClientInstance,
dontBlock: boolean = false,
): Promise<GetEventsResult> {
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<void>
*/
async destroyClient(clientInstance: ZulipClientInstance): Promise<void> {
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<any> zulip-js初始化函数
* @private
*/
private async loadZulipModule(): Promise<any> {
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}`);
}
}
}