forked from datawhale/whale-town-end
refactor:重构Zulip模块按业务功能模块化架构
- 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
This commit is contained in:
397
src/core/zulip/config/zulip.config.ts
Normal file
397
src/core/zulip/config/zulip.config.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
Reference in New Issue
Block a user