forked from datawhale/whale-town-end
- 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
/**
|
||
* Zulip配置模块
|
||
*
|
||
* 功能描述:
|
||
* - 定义Zulip集成系统的配置接口
|
||
* - 提供配置验证功能
|
||
* - 支持环境变量和配置文件两种配置方式
|
||
* - 实现配置热重载
|
||
*
|
||
* 配置来源优先级:
|
||
* 1. 环境变量(最高优先级)
|
||
* 2. 配置文件
|
||
* 3. 默认值(最低优先级)
|
||
*
|
||
* 依赖模块:
|
||
* - @nestjs/config: NestJS配置模块
|
||
*
|
||
* @author angjustinl
|
||
* @version 1.0.0
|
||
* @since 2025-12-25
|
||
*/
|
||
|
||
import { registerAs } from '@nestjs/config';
|
||
|
||
/**
|
||
* Zulip服务器配置接口
|
||
*/
|
||
export interface ZulipServerConfig {
|
||
/** Zulip服务器URL */
|
||
serverUrl: string;
|
||
/** Zulip机器人邮箱 */
|
||
botEmail: string;
|
||
/** Zulip机器人API Key */
|
||
botApiKey: string;
|
||
}
|
||
|
||
/**
|
||
* WebSocket配置接口
|
||
*/
|
||
export interface WebSocketConfig {
|
||
/** WebSocket端口 */
|
||
port: number;
|
||
/** WebSocket命名空间 */
|
||
namespace: string;
|
||
/** 心跳间隔(毫秒) */
|
||
pingInterval: number;
|
||
/** 心跳超时(毫秒) */
|
||
pingTimeout: number;
|
||
}
|
||
|
||
/**
|
||
* 消息配置接口
|
||
*/
|
||
export interface MessageConfig {
|
||
/** 消息频率限制(条/分钟) */
|
||
rateLimit: number;
|
||
/** 消息最大长度 */
|
||
maxLength: number;
|
||
/** 是否启用内容过滤 */
|
||
contentFilterEnabled: boolean;
|
||
/** 敏感词列表文件路径 */
|
||
sensitiveWordsPath?: string;
|
||
}
|
||
|
||
/**
|
||
* 会话配置接口
|
||
*/
|
||
export interface SessionConfig {
|
||
/** 会话超时时间(分钟) */
|
||
timeout: number;
|
||
/** 清理间隔(分钟) */
|
||
cleanupInterval: number;
|
||
/** 最大连接数 */
|
||
maxConnections: number;
|
||
}
|
||
|
||
/**
|
||
* 错误处理配置接口
|
||
*/
|
||
export interface ErrorHandlingConfig {
|
||
/** 是否启用降级模式 */
|
||
degradedModeEnabled: boolean;
|
||
/** 是否启用自动重连 */
|
||
autoReconnectEnabled: boolean;
|
||
/** 最大重连尝试次数 */
|
||
maxReconnectAttempts: number;
|
||
/** 重连基础延迟(毫秒) */
|
||
reconnectBaseDelay: number;
|
||
/** API超时时间(毫秒) */
|
||
apiTimeout: number;
|
||
/** 最大重试次数 */
|
||
maxRetries: number;
|
||
}
|
||
|
||
/**
|
||
* 监控配置接口
|
||
*/
|
||
export interface MonitoringConfig {
|
||
/** 健康检查间隔(毫秒) */
|
||
healthCheckInterval: number;
|
||
/** 错误率阈值(0-1) */
|
||
errorRateThreshold: number;
|
||
/** API响应时间阈值(毫秒) */
|
||
responseTimeThreshold: number;
|
||
/** 内存使用阈值(0-1) */
|
||
memoryThreshold: number;
|
||
}
|
||
|
||
/**
|
||
* 安全配置接口
|
||
*/
|
||
export interface SecurityConfig {
|
||
/** API Key加密密钥(生产环境必须配置) */
|
||
apiKeyEncryptionKey?: string;
|
||
/** 允许的Stream列表(空表示允许所有) */
|
||
allowedStreams: string[];
|
||
}
|
||
|
||
/**
|
||
* 完整的Zulip配置接口
|
||
*/
|
||
export interface ZulipConfiguration {
|
||
/** Zulip服务器配置 */
|
||
server: ZulipServerConfig;
|
||
/** WebSocket配置 */
|
||
websocket: WebSocketConfig;
|
||
/** 消息配置 */
|
||
message: MessageConfig;
|
||
/** 会话配置 */
|
||
session: SessionConfig;
|
||
/** 错误处理配置 */
|
||
errorHandling: ErrorHandlingConfig;
|
||
/** 监控配置 */
|
||
monitoring: MonitoringConfig;
|
||
/** 安全配置 */
|
||
security: SecurityConfig;
|
||
}
|
||
|
||
/**
|
||
* 配置验证结果接口
|
||
*/
|
||
export interface ConfigValidationResult {
|
||
/** 是否有效 */
|
||
valid: boolean;
|
||
/** 错误信息列表 */
|
||
errors: string[];
|
||
/** 警告信息列表 */
|
||
warnings: string[];
|
||
}
|
||
|
||
/**
|
||
* 默认配置值
|
||
*/
|
||
export const DEFAULT_ZULIP_CONFIG: ZulipConfiguration = {
|
||
server: {
|
||
serverUrl: 'https://your-zulip-server.com',
|
||
botEmail: 'bot@example.com',
|
||
botApiKey: '',
|
||
},
|
||
websocket: {
|
||
port: 3000,
|
||
namespace: '/game',
|
||
pingInterval: 25000,
|
||
pingTimeout: 5000,
|
||
},
|
||
message: {
|
||
rateLimit: 10,
|
||
maxLength: 10000,
|
||
contentFilterEnabled: true,
|
||
},
|
||
session: {
|
||
timeout: 30,
|
||
cleanupInterval: 5,
|
||
maxConnections: 1000,
|
||
},
|
||
errorHandling: {
|
||
degradedModeEnabled: false,
|
||
autoReconnectEnabled: true,
|
||
maxReconnectAttempts: 5,
|
||
reconnectBaseDelay: 5000,
|
||
apiTimeout: 30000,
|
||
maxRetries: 3,
|
||
},
|
||
monitoring: {
|
||
healthCheckInterval: 60000,
|
||
errorRateThreshold: 0.1,
|
||
responseTimeThreshold: 5000,
|
||
memoryThreshold: 0.9,
|
||
},
|
||
security: {
|
||
allowedStreams: [],
|
||
},
|
||
};
|
||
|
||
|
||
/**
|
||
* 从环境变量加载Zulip配置
|
||
*
|
||
* 功能描述:
|
||
* 从环境变量读取配置值,未设置的使用默认值
|
||
*
|
||
* @returns ZulipConfiguration 完整的Zulip配置对象
|
||
*/
|
||
export function loadZulipConfigFromEnv(): ZulipConfiguration {
|
||
return {
|
||
server: {
|
||
serverUrl: process.env.ZULIP_SERVER_URL || DEFAULT_ZULIP_CONFIG.server.serverUrl,
|
||
botEmail: process.env.ZULIP_BOT_EMAIL || DEFAULT_ZULIP_CONFIG.server.botEmail,
|
||
botApiKey: process.env.ZULIP_BOT_API_KEY || DEFAULT_ZULIP_CONFIG.server.botApiKey,
|
||
},
|
||
websocket: {
|
||
port: parseInt(process.env.WEBSOCKET_PORT || String(DEFAULT_ZULIP_CONFIG.websocket.port), 10),
|
||
namespace: process.env.WEBSOCKET_NAMESPACE || DEFAULT_ZULIP_CONFIG.websocket.namespace,
|
||
pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL || String(DEFAULT_ZULIP_CONFIG.websocket.pingInterval), 10),
|
||
pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT || String(DEFAULT_ZULIP_CONFIG.websocket.pingTimeout), 10),
|
||
},
|
||
message: {
|
||
rateLimit: parseInt(process.env.ZULIP_MESSAGE_RATE_LIMIT || String(DEFAULT_ZULIP_CONFIG.message.rateLimit), 10),
|
||
maxLength: parseInt(process.env.ZULIP_MESSAGE_MAX_LENGTH || String(DEFAULT_ZULIP_CONFIG.message.maxLength), 10),
|
||
contentFilterEnabled: process.env.ZULIP_CONTENT_FILTER_ENABLED !== 'false',
|
||
sensitiveWordsPath: process.env.ZULIP_SENSITIVE_WORDS_PATH,
|
||
},
|
||
session: {
|
||
timeout: parseInt(process.env.ZULIP_SESSION_TIMEOUT || String(DEFAULT_ZULIP_CONFIG.session.timeout), 10),
|
||
cleanupInterval: parseInt(process.env.ZULIP_CLEANUP_INTERVAL || String(DEFAULT_ZULIP_CONFIG.session.cleanupInterval), 10),
|
||
maxConnections: parseInt(process.env.ZULIP_MAX_CONNECTIONS || String(DEFAULT_ZULIP_CONFIG.session.maxConnections), 10),
|
||
},
|
||
errorHandling: {
|
||
degradedModeEnabled: process.env.ZULIP_DEGRADED_MODE_ENABLED === 'true',
|
||
autoReconnectEnabled: process.env.ZULIP_AUTO_RECONNECT_ENABLED !== 'false',
|
||
maxReconnectAttempts: parseInt(process.env.ZULIP_MAX_RECONNECT_ATTEMPTS || String(DEFAULT_ZULIP_CONFIG.errorHandling.maxReconnectAttempts), 10),
|
||
reconnectBaseDelay: parseInt(process.env.ZULIP_RECONNECT_BASE_DELAY || String(DEFAULT_ZULIP_CONFIG.errorHandling.reconnectBaseDelay), 10),
|
||
apiTimeout: parseInt(process.env.ZULIP_API_TIMEOUT || String(DEFAULT_ZULIP_CONFIG.errorHandling.apiTimeout), 10),
|
||
maxRetries: parseInt(process.env.ZULIP_MAX_RETRIES || String(DEFAULT_ZULIP_CONFIG.errorHandling.maxRetries), 10),
|
||
},
|
||
monitoring: {
|
||
healthCheckInterval: parseInt(process.env.MONITORING_HEALTH_CHECK_INTERVAL || String(DEFAULT_ZULIP_CONFIG.monitoring.healthCheckInterval), 10),
|
||
errorRateThreshold: parseFloat(process.env.MONITORING_ERROR_RATE_THRESHOLD || String(DEFAULT_ZULIP_CONFIG.monitoring.errorRateThreshold)),
|
||
responseTimeThreshold: parseInt(process.env.MONITORING_RESPONSE_TIME_THRESHOLD || String(DEFAULT_ZULIP_CONFIG.monitoring.responseTimeThreshold), 10),
|
||
memoryThreshold: parseFloat(process.env.MONITORING_MEMORY_THRESHOLD || String(DEFAULT_ZULIP_CONFIG.monitoring.memoryThreshold)),
|
||
},
|
||
security: {
|
||
apiKeyEncryptionKey: process.env.ZULIP_API_KEY_ENCRYPTION_KEY,
|
||
allowedStreams: (process.env.ZULIP_ALLOWED_STREAMS || '').split(',').filter(s => s.trim()),
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 验证Zulip配置
|
||
*
|
||
* 功能描述:
|
||
* 验证配置的完整性和有效性,返回验证结果
|
||
*
|
||
* 验证规则:
|
||
* 1. 必填字段不能为空
|
||
* 2. 数值字段必须在有效范围内
|
||
* 3. URL格式必须正确
|
||
* 4. 生产环境必须配置API Key加密密钥
|
||
*
|
||
* @param config Zulip配置对象
|
||
* @param isProduction 是否为生产环境
|
||
* @returns ConfigValidationResult 验证结果
|
||
*/
|
||
export function validateZulipConfig(
|
||
config: ZulipConfiguration,
|
||
isProduction: boolean = false
|
||
): ConfigValidationResult {
|
||
const errors: string[] = [];
|
||
const warnings: string[] = [];
|
||
|
||
// 验证服务器配置
|
||
if (!config.server.serverUrl) {
|
||
errors.push('缺少Zulip服务器URL (ZULIP_SERVER_URL)');
|
||
} else if (!isValidUrl(config.server.serverUrl)) {
|
||
errors.push('Zulip服务器URL格式无效');
|
||
}
|
||
|
||
if (!config.server.botEmail) {
|
||
warnings.push('未配置Zulip机器人邮箱 (ZULIP_BOT_EMAIL)');
|
||
} else if (!isValidEmail(config.server.botEmail)) {
|
||
errors.push('Zulip机器人邮箱格式无效');
|
||
}
|
||
|
||
if (!config.server.botApiKey) {
|
||
warnings.push('未配置Zulip机器人API Key (ZULIP_BOT_API_KEY),将使用本地模式');
|
||
}
|
||
|
||
// 验证WebSocket配置
|
||
if (config.websocket.port < 1 || config.websocket.port > 65535) {
|
||
errors.push('WebSocket端口必须在1-65535范围内');
|
||
}
|
||
|
||
if (!config.websocket.namespace || !config.websocket.namespace.startsWith('/')) {
|
||
errors.push('WebSocket命名空间必须以/开头');
|
||
}
|
||
|
||
// 验证消息配置
|
||
if (config.message.rateLimit < 1) {
|
||
errors.push('消息频率限制必须大于0');
|
||
}
|
||
|
||
if (config.message.maxLength < 1 || config.message.maxLength > 100000) {
|
||
errors.push('消息最大长度必须在1-100000范围内');
|
||
}
|
||
|
||
// 验证会话配置
|
||
if (config.session.timeout < 1) {
|
||
errors.push('会话超时时间必须大于0');
|
||
}
|
||
|
||
if (config.session.cleanupInterval < 1) {
|
||
errors.push('清理间隔必须大于0');
|
||
}
|
||
|
||
if (config.session.maxConnections < 1) {
|
||
errors.push('最大连接数必须大于0');
|
||
}
|
||
|
||
// 验证错误处理配置
|
||
if (config.errorHandling.maxReconnectAttempts < 0) {
|
||
errors.push('最大重连尝试次数不能为负数');
|
||
}
|
||
|
||
if (config.errorHandling.apiTimeout < 1000) {
|
||
warnings.push('API超时时间过短,建议至少1000毫秒');
|
||
}
|
||
|
||
// 验证监控配置
|
||
if (config.monitoring.errorRateThreshold < 0 || config.monitoring.errorRateThreshold > 1) {
|
||
errors.push('错误率阈值必须在0-1范围内');
|
||
}
|
||
|
||
if (config.monitoring.memoryThreshold < 0 || config.monitoring.memoryThreshold > 1) {
|
||
errors.push('内存使用阈值必须在0-1范围内');
|
||
}
|
||
|
||
// 生产环境特殊验证
|
||
if (isProduction) {
|
||
if (!config.security.apiKeyEncryptionKey) {
|
||
errors.push('生产环境必须配置API Key加密密钥 (ZULIP_API_KEY_ENCRYPTION_KEY)');
|
||
} else if (config.security.apiKeyEncryptionKey.length < 32) {
|
||
errors.push('API Key加密密钥长度必须至少32字符');
|
||
}
|
||
|
||
if (!config.server.botApiKey) {
|
||
errors.push('生产环境必须配置Zulip机器人API Key');
|
||
}
|
||
}
|
||
|
||
return {
|
||
valid: errors.length === 0,
|
||
errors,
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 验证URL格式
|
||
*
|
||
* @param url URL字符串
|
||
* @returns boolean 是否有效
|
||
*/
|
||
function isValidUrl(url: string): boolean {
|
||
try {
|
||
new URL(url);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证邮箱格式
|
||
*
|
||
* @param email 邮箱字符串
|
||
* @returns boolean 是否有效
|
||
*/
|
||
function isValidEmail(email: string): boolean {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(email);
|
||
}
|
||
|
||
/**
|
||
* NestJS配置工厂函数
|
||
*
|
||
* 功能描述:
|
||
* 用于@nestjs/config模块的配置注册
|
||
*
|
||
* 使用方式:
|
||
* ConfigModule.forRoot({
|
||
* load: [zulipConfig],
|
||
* })
|
||
*/
|
||
export const zulipConfig = registerAs('zulip', () => {
|
||
return loadZulipConfigFromEnv();
|
||
});
|