feat(zulip): 添加全面的 Zulip 集成系统

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

View File

@@ -0,0 +1,704 @@
/**
* 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}`);
}
}
}