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,331 @@
/**
* Stream初始化服务
*
* 功能描述:
* - 在系统启动时检查并创建所有地图对应的Zulip Streams
* - 确保所有配置的Streams在Zulip服务器上存在
* - 提供Stream创建和验证功能
*
* 主要方法:
* - initializeStreams(): 初始化所有Streams
* - checkStreamExists(): 检查Stream是否存在
* - createStream(): 创建Stream
*
* 使用场景:
* - 系统启动时自动初始化
* - 配置更新后重新初始化
*
* @author 开发团队
* @version 1.0.0
* @since 2025-12-25
*/
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigManagerService } from './config-manager.service';
@Injectable()
export class StreamInitializerService implements OnModuleInit {
private readonly logger = new Logger(StreamInitializerService.name);
private initializationComplete = false;
constructor(
private readonly configManager: ConfigManagerService,
) {
this.logger.log('StreamInitializerService初始化完成');
}
/**
* 模块初始化时自动执行
*/
async onModuleInit(): Promise<void> {
// 延迟5秒执行确保其他服务已初始化
setTimeout(async () => {
await this.initializeStreams();
}, 5000);
}
/**
* 初始化所有Streams
*
* 功能描述:
* 检查配置中的所有Streams是否存在不存在则创建
*
* @returns Promise<{success: boolean, created: string[], existing: string[], failed: string[]}>
*/
async initializeStreams(): Promise<{
success: boolean;
created: string[];
existing: string[];
failed: string[];
}> {
this.logger.log('开始初始化Zulip Streams', {
operation: 'initializeStreams',
timestamp: new Date().toISOString(),
});
const created: string[] = [];
const existing: string[] = [];
const failed: string[] = [];
try {
// 获取所有地图配置
const mapConfigs = this.configManager.getAllMapConfigs();
if (mapConfigs.length === 0) {
this.logger.warn('没有找到地图配置跳过Stream初始化', {
operation: 'initializeStreams',
});
return { success: true, created, existing, failed };
}
// 获取所有唯一的Stream名称
const streamNames = new Set<string>();
mapConfigs.forEach(config => {
streamNames.add(config.zulipStream);
});
this.logger.log(`找到 ${streamNames.size} 个需要检查的Streams`, {
operation: 'initializeStreams',
streamCount: streamNames.size,
streams: Array.from(streamNames),
});
// 检查并创建每个Stream
for (const streamName of streamNames) {
try {
const exists = await this.checkStreamExists(streamName);
if (exists) {
existing.push(streamName);
this.logger.log(`Stream已存在: ${streamName}`, {
operation: 'initializeStreams',
streamName,
});
} else {
const createResult = await this.createStream(streamName);
if (createResult) {
created.push(streamName);
this.logger.log(`Stream创建成功: ${streamName}`, {
operation: 'initializeStreams',
streamName,
});
} else {
failed.push(streamName);
this.logger.warn(`Stream创建失败: ${streamName}`, {
operation: 'initializeStreams',
streamName,
});
}
}
} catch (error) {
const err = error as Error;
failed.push(streamName);
this.logger.error(`处理Stream失败: ${streamName}`, {
operation: 'initializeStreams',
streamName,
error: err.message,
});
}
}
this.initializationComplete = true;
const success = failed.length === 0;
this.logger.log('Stream初始化完成', {
operation: 'initializeStreams',
success,
totalStreams: streamNames.size,
created: created.length,
existing: existing.length,
failed: failed.length,
timestamp: new Date().toISOString(),
});
return { success, created, existing, failed };
} catch (error) {
const err = error as Error;
this.logger.error('Stream初始化失败', {
operation: 'initializeStreams',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return { success: false, created, existing, failed };
}
}
/**
* 检查Stream是否存在
*
* 功能描述:
* 使用Bot API Key检查指定的Stream是否在Zulip服务器上存在
*
* @param streamName Stream名称
* @returns Promise<boolean> 是否存在
*/
private async checkStreamExists(streamName: string): Promise<boolean> {
try {
// 获取Zulip配置
const zulipConfig = this.configManager.getZulipConfig();
if (!zulipConfig.zulipBotApiKey) {
this.logger.warn('Bot API Key未配置跳过Stream检查', {
operation: 'checkStreamExists',
streamName,
});
return false;
}
// 动态导入zulip-js
const zulipModule: any = await import('zulip-js');
const zulipFactory = zulipModule.default || zulipModule;
// 创建Bot客户端
const client = await zulipFactory({
username: zulipConfig.zulipBotEmail,
apiKey: zulipConfig.zulipBotApiKey,
realm: zulipConfig.zulipServerUrl,
});
// 获取所有Streams
const result = await client.streams.retrieve();
if (result.result === 'success' && result.streams) {
const exists = result.streams.some(
(stream: any) => stream.name.toLowerCase() === streamName.toLowerCase()
);
return exists;
}
return false;
} catch (error) {
const err = error as Error;
this.logger.error('检查Stream失败', {
operation: 'checkStreamExists',
streamName,
error: err.message,
});
return false;
}
}
/**
* 创建Stream
*
* 功能描述:
* 使用Bot API Key在Zulip服务器上创建新的Stream
*
* @param streamName Stream名称
* @param description Stream描述可选
* @returns Promise<boolean> 是否创建成功
*/
private async createStream(
streamName: string,
description?: string
): Promise<boolean> {
try {
// 获取Zulip配置
const zulipConfig = this.configManager.getZulipConfig();
if (!zulipConfig.zulipBotApiKey) {
this.logger.warn('Bot API Key未配置无法创建Stream', {
operation: 'createStream',
streamName,
});
return false;
}
// 动态导入zulip-js
const zulipModule: any = await import('zulip-js');
const zulipFactory = zulipModule.default || zulipModule;
// 创建Bot客户端
const client = await zulipFactory({
username: zulipConfig.zulipBotEmail,
apiKey: zulipConfig.zulipBotApiKey,
realm: zulipConfig.zulipServerUrl,
});
// 查找对应的地图配置以获取描述
const mapConfig = this.configManager.getMapConfigByStream(streamName);
const streamDescription = description ||
(mapConfig ? `${mapConfig.mapName} - ${mapConfig.description || 'Game chat channel'}` :
`Game chat channel for ${streamName}`);
// 使用callEndpoint创建Stream
const result = await client.callEndpoint(
'/users/me/subscriptions',
'POST',
{
subscriptions: JSON.stringify([
{
name: streamName,
description: streamDescription
}
])
}
);
if (result.result === 'success') {
this.logger.log('Stream创建成功', {
operation: 'createStream',
streamName,
description: streamDescription,
});
return true;
} else {
this.logger.warn('Stream创建失败', {
operation: 'createStream',
streamName,
error: result.msg,
});
return false;
}
} catch (error) {
const err = error as Error;
this.logger.error('创建Stream异常', {
operation: 'createStream',
streamName,
error: err.message,
}, err.stack);
return false;
}
}
/**
* 检查初始化是否完成
*
* @returns boolean 是否完成
*/
isInitializationComplete(): boolean {
return this.initializationComplete;
}
/**
* 手动触发Stream初始化
*
* 功能描述:
* 允许手动触发Stream初始化用于配置更新后重新初始化
*
* @returns Promise<{success: boolean, created: string[], existing: string[], failed: string[]}>
*/
async reinitializeStreams(): Promise<{
success: boolean;
created: string[];
existing: string[];
failed: string[];
}> {
this.logger.log('手动触发Stream重新初始化', {
operation: 'reinitializeStreams',
timestamp: new Date().toISOString(),
});
this.initializationComplete = false;
return await this.initializeStreams();
}
}