import { EventEmitter } from 'node:events' import fs from 'node:fs/promises' import path from 'node:path' import type { RemoteControlConfig, ConfigChangeEvent } from './types' const DEFAULT_CONFIG: RemoteControlConfig = { discord: { enabled: false, botToken: '', }, dingtalk: { enabled: false, clientId: '', clientSecret: '', }, lark: { enabled: false, appId: '', appSecret: '', }, telegram: { enabled: false, botToken: '', }, slack: { enabled: false, botToken: '', appToken: '', }, } function maskValue(value: string): string { if (!value || value.length < 4) return '••••' return '••••••••' + value.slice(-4) } export class ConfigManager extends EventEmitter { private static instance: ConfigManager private config: RemoteControlConfig private configPath: string private constructor() { super() this.config = { ...DEFAULT_CONFIG } this.configPath = path.join(process.cwd(), 'remote-control', 'data', 'config.json') } private loaded = false static getInstance(): ConfigManager { if (!ConfigManager.instance) { ConfigManager.instance = new ConfigManager() } return ConfigManager.instance } async ensureLoaded(): Promise { if (this.loaded) return await this.load() // 首次加载后,执行一次和保存配置相同的逻辑:启动已启用的 bot void reconnectAllPlatforms(this.config) } async load(): Promise { try { const data = await fs.readFile(this.configPath, 'utf-8') this.config = { ...DEFAULT_CONFIG, ...JSON.parse(data) } } catch (error) { this.config = { ...DEFAULT_CONFIG } await this.save() } this.loaded = true } private async save(): Promise { const dir = path.dirname(this.configPath) await fs.mkdir(dir, { recursive: true }) await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8') } get(): RemoteControlConfig { return { ...this.config } } getMasked(): RemoteControlConfig { return { discord: { ...this.config.discord, botToken: maskValue(this.config.discord.botToken), }, dingtalk: { ...this.config.dingtalk, clientSecret: maskValue(this.config.dingtalk.clientSecret), }, lark: { ...this.config.lark, appSecret: maskValue(this.config.lark.appSecret), }, telegram: { ...this.config.telegram, botToken: maskValue(this.config.telegram.botToken), }, slack: { ...this.config.slack, botToken: maskValue(this.config.slack.botToken), appToken: maskValue(this.config.slack.appToken), }, } } async update(newConfig: Partial, options?: { skipEmit?: boolean }): Promise { const previousConfig = { ...this.config } if (newConfig.discord) { this.config.discord = { ...this.config.discord, ...newConfig.discord } } if (newConfig.dingtalk) { this.config.dingtalk = { ...this.config.dingtalk, ...newConfig.dingtalk } } if (newConfig.lark) { this.config.lark = { ...this.config.lark, ...newConfig.lark } } if (newConfig.telegram) { this.config.telegram = { ...this.config.telegram, ...newConfig.telegram } } if (newConfig.slack) { this.config.slack = { ...this.config.slack, ...newConfig.slack } } await this.save() if (!options?.skipEmit) { const changedPlatforms = this.getChangedPlatforms(previousConfig, this.config) this.emit('config-changed', { previousConfig, newConfig: this.config, changedPlatforms, } as ConfigChangeEvent) } } private getChangedPlatforms( prev: RemoteControlConfig, next: RemoteControlConfig ): ('discord' | 'dingtalk' | 'lark' | 'telegram' | 'slack')[] { const changed: ('discord' | 'dingtalk' | 'lark' | 'telegram' | 'slack')[] = [] if (JSON.stringify(prev.discord) !== JSON.stringify(next.discord)) { changed.push('discord') } if (JSON.stringify(prev.dingtalk) !== JSON.stringify(next.dingtalk)) { changed.push('dingtalk') } if (JSON.stringify(prev.lark) !== JSON.stringify(next.lark)) { changed.push('lark') } if (JSON.stringify(prev.telegram) !== JSON.stringify(next.telegram)) { changed.push('telegram') } if (JSON.stringify(prev.slack) !== JSON.stringify(next.slack)) { changed.push('slack') } return changed } } /** * 停止所有 bot,然后启动已启用的 bot。 * 服务启动和保存配置时复用同一逻辑。 */ export async function reconnectAllPlatforms( config: RemoteControlConfig, errors?: string[], ): Promise { // Discord try { const bot = await import('@/remote-control/bots/discord') await bot.stopBot() if (config.discord.enabled && config.discord.botToken) { await bot.startBot(config.discord.botToken) } } catch (err) { if (config.discord.enabled) errors?.push(`Discord: ${err instanceof Error ? err.message : String(err)}`) } // DingTalk try { const bot = await import('@/remote-control/bots/dingtalk') await bot.stopBot() if (config.dingtalk.enabled && config.dingtalk.clientId && config.dingtalk.clientSecret) { await bot.startBot(config.dingtalk.clientId, config.dingtalk.clientSecret) } } catch (err) { if (config.dingtalk.enabled) errors?.push(`钉钉: ${err instanceof Error ? err.message : String(err)}`) } // Lark try { const bot = await import('@/remote-control/bots/lark') await bot.stopBot() if (config.lark.enabled && config.lark.appId && config.lark.appSecret) { await bot.startBot(config.lark.appId, config.lark.appSecret) } } catch (err) { if (config.lark.enabled) errors?.push(`飞书: ${err instanceof Error ? err.message : String(err)}`) } // Telegram try { const bot = await import('@/remote-control/bots/telegram') await bot.stopBot() if (config.telegram.enabled && config.telegram.botToken) { await bot.startBot(config.telegram.botToken) } } catch (err) { if (config.telegram.enabled) errors?.push(`Telegram: ${err instanceof Error ? err.message : String(err)}`) } // Slack try { const bot = await import('@/remote-control/bots/slack') await bot.stopBot() if (config.slack.enabled && config.slack.botToken && config.slack.appToken) { await bot.startBot(config.slack.botToken, config.slack.appToken) } } catch (err) { if (config.slack.enabled) errors?.push(`Slack: ${err instanceof Error ? err.message : String(err)}`) } } export type { RemoteControlConfig, ConfigChangeEvent } from './types'