234 lines
6.6 KiB
TypeScript
234 lines
6.6 KiB
TypeScript
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<void> {
|
||
if (this.loaded) return
|
||
await this.load()
|
||
// 首次加载后,执行一次和保存配置相同的逻辑:启动已启用的 bot
|
||
void reconnectAllPlatforms(this.config)
|
||
}
|
||
|
||
async load(): Promise<void> {
|
||
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<void> {
|
||
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<RemoteControlConfig>, options?: { skipEmit?: boolean }): Promise<void> {
|
||
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<void> {
|
||
// 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'
|