forked from datawhale/whale-town-end
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:
331
src/business/zulip/services/stream-initializer.service.ts
Normal file
331
src/business/zulip/services/stream-initializer.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user