import fs from 'fs' import path from 'path' import yaml from 'js-yaml' class ChatGPTConfig { /** * 版本号 * @type {string} */ version = '3.0.0' /** * 基本配置 * @type {{ * toggleMode: 'at' | 'prefix', * debug: boolean, * }} */ basic = { // 触发方式,at触发或者前缀触发 toggleMode: 'at', // 触发前缀,仅在前缀触发时有效 togglePrefix: '#chat', // 是否开启调试模式 debug: false, // 一般命令的开头 commandPrefix: '#chatgpt' } /** * 伪人模式,基于框架实现,因此机器人开启前缀后依然需要带上前缀。 * @type {{ * enable: boolean, * hit: string[], * probability: number, * defaultPreset: string, * presetPrefix?: string, * presetMap: Array<{ * keywords: string[], * presetId: string, * priority: number, * recall?: boolean * }>, * maxTokens: number, * temperature: number, * sendReasoning: boolean * }} * }} */ bym = { // 开关 enable: false, // 伪人必定触发词 hit: ['bym'], // 不包含伪人必定触发词时的概率 probability: 0.02, // 伪人模式的默认预设 defaultPreset: '', // 伪人模式的预设前缀,会加在在所有其他预设前。例如此处可以用于配置通用的伪人发言风格(随意、模仿群友等),presetMap中专心配置角色设定即可 presetPrefix: '', // 包含关键词与预设的对应关系。包含特定触发词使用特定的预设,按照优先级排序 presetMap: [], // 如果大于0,会覆盖preset中的maxToken,用于控制伪人模式发言长度 maxTokens: 0, // 如果大于等于0,会覆盖preset中的temperature,用于控制伪人模式发言随机性 temperature: -1, // 是否发送思考内容 sendReasoning: false } /** * 模型和对话相关配置 * @type {{ * defaultModel: string, * embeddingModel: string, * defaultChatPresetId: string, * enableCustomPreset: boolean, * customPresetUserWhiteList: string[], * customPresetUserBlackList: string[], * promptBlockWords: string[], * responseBlockWords: string[], * blockStrategy: 'full' | 'mask', * blockWordMask: string, * enableGroupContext: boolean, * groupContextLength: number, * groupContextTemplatePrefix: string, * groupContextTemplateMessage: string, * groupContextTemplateSuffix: string * }} */ llm = { // 默认模型,初始化构建预设使用 defaultModel: '', // 嵌入模型 embeddingModel: 'gemini-embedding-exp-03-07', // 嵌入结果维度,0表示自动 dimensions: 0, // 默认对话预设ID defaultChatPresetId: '', // 是否启用允许其他人切换预设 enableCustomPreset: false, // 允许切换预设的用户白名单 customPresetUserWhiteList: [], // 禁止切换预设的用户黑名单 customPresetUserBlackList: [], // 用户对话屏蔽词 promptBlockWords: [], // 机器人回复屏蔽词 responseBlockWords: [], // 触发屏蔽词的策略,完全屏蔽或仅屏蔽关键词 blockStrategy: 'full', // 如果blockStrategy为mask,屏蔽词的替换字符 blockWordMask: '***', // 是否开启群组上下文 enableGroupContext: false, // 群组上下文长度 groupContextLength: 20, // 用于组装群聊上下文提示词的模板前缀 groupContextTemplatePrefix: 'Latest several messages in the group chat:\n' + '| 群名片 | 昵称 | qq号 | 群角色 | 群头衔 | 时间 | messageId | 消息内容 |\n' + '|---|---|---|---|---|---|---|---|', // 用于组装群聊上下文提示词的模板内容部分,每一条消息作为message,仿照示例填写 // eslint-disable-next-line no-template-curly-in-string groupContextTemplateMessage: '| ${message.sender.card} | ${message.sender.nickname} | ${message.sender.user_id} | ${message.sender.role} | ${message.sender.title} | ${message.time} | ${message.messageId} | ${message.raw_message} |', // 用于组装群聊上下文提示词的模板后缀 groupContextTemplateSuffix: '\n' } /** * 管理相关配置 * @type {{ * blackGroups: number[], * whiteGroups: number[], * blackUsers: string[], * whiteUsers: string[], * defaultRateLimit: number * }} */ management = { blackGroups: [], whiteGroups: [], blackUsers: [], whiteUsers: [], // 默认对话速率限制,0表示不限制,数字表示每分钟最多对话次数 defaultRateLimit: 0 } /** * chaite相关配置 * @type { * { dataDir: string, * processorsDirPath: string, * toolsDirPath: string, * cloudBaseUrl: string, * cloudApiKey: string, * authKey: string, * host: string, * port: number}} */ chaite = { // 数据目录,相对于插件下 dataDir: 'data', // 处理器目录,相对于插件下 processorsDirPath: 'utils/processors', // 工具目录,相对于插件目录下 toolsDirPath: 'utils/tools', // 云端API url cloudBaseUrl: 'https://api.chaite.cloud', // 云端API Key cloudApiKey: '', // jwt key,非必要勿修改,修改需重启 authKey: '', // 管理面板监听地址 host: '0.0.0.0', // 管理面板监听端口 port: 48370, // 存储实现 sqlite lowdb storage: 'sqlite' } constructor () { this.version = '3.0.0' this.watcher = null this.configPath = '' } /** * Start config file sync * call once! * @param {string} configDir Directory containing config files */ startSync (configDir) { const jsonPath = path.join(configDir, 'config.json') const yamlPath = path.join(configDir, 'config.yaml') // Determine which config file to use if (fs.existsSync(jsonPath)) { this.configPath = jsonPath } else if (fs.existsSync(yamlPath)) { this.configPath = yamlPath } else { this.configPath = jsonPath this.saveToFile() } // Load initial config this.loadFromFile() // Watch for file changes this.watcher = fs.watchFile(this.configPath, (curr, prev) => { if (curr.mtime !== prev.mtime) { this.loadFromFile() } }) const createDeepProxy = (obj, handler, seen = new WeakMap()) => { // 基本类型或非对象直接返回 if (obj === null || typeof obj !== 'object') return obj // 检查循环引用 if (seen.has(obj)) { return seen.get(obj) } // 创建代理对象 const proxy = new Proxy(obj, handler) // 记录已创建的代理,避免循环引用 seen.set(obj, proxy) // 处理子对象 for (let key of Object.keys(obj)) { if (typeof obj[key] === 'object' && obj[key] !== null) { obj[key] = createDeepProxy(obj[key], handler, seen) } } return proxy } const handler = { set: (target, prop, value) => { if (prop !== 'watcher' && prop !== 'configPath') { // 避免递归创建代理 if (typeof value === 'object' && value !== null) { // 检查 value 是否已经是代理 if (!value.__isProxy) { const newProxy = createDeepProxy(value, handler) // 标记为代理对象 Object.defineProperty(newProxy, '__isProxy', { value: true, enumerable: false, configurable: false }) target[prop] = newProxy } else { target[prop] = value } } else { target[prop] = value } // 避免在代理对象保存时再次触发 if (!target.__isSaving) { target.__isSaving = true try { this.saveToFile() } finally { target.__isSaving = false } } } return true } } // 为所有嵌套对象创建Proxy this.basic = createDeepProxy(this.basic, handler) this.bym = createDeepProxy(this.bym, handler) this.llm = createDeepProxy(this.llm, handler) this.management = createDeepProxy(this.management, handler) this.chaite = createDeepProxy(this.chaite, handler) // 返回最外层的Proxy return new Proxy(this, handler) } /** * Load config from file */ loadFromFile () { this.__isSaving = false; try { const content = fs.readFileSync(this.configPath, 'utf8') const loadedConfig = this.configPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content) // Deep merge function that preserves default values const deepMerge = (target, source) => { // Skip non-object properties or special properties if (!source || typeof source !== 'object' || !target || typeof target !== 'object') { return } for (const key in source) { // Skip internal properties if (key === 'watcher' || key === 'configPath') continue if (typeof source[key] === 'object' && source[key] !== null && typeof target[key] === 'object' && target[key] !== null) { // Recursively merge nested objects deepMerge(target[key], source[key]) } else if (source[key] !== undefined) { // Only update if the value exists in the loaded config target[key] = source[key] } // If source[key] is undefined, keep the default value in target } } // Apply loaded config to this object, preserving defaults deepMerge(this, loadedConfig) // Save the file to persist any new default values const hasChanges = JSON.stringify(this.toJSON()) !== content if (hasChanges) { this.saveToFile() } } catch (error) { console.error('Failed to load config:', error) } } /** * Save config to file */ saveToFile () { try { const config = { version: this.version, basic: this.basic, bym: this.bym, llm: this.llm, management: this.management, chaite: this.chaite } const content = this.configPath.endsWith('.json') ? JSON.stringify(config, null, 2) : yaml.dump(config) fs.writeFileSync(this.configPath, content, 'utf8') } catch (error) { console.error('Failed to save config:', error) } } toJSON () { return { version: this.version, basic: this.basic, bym: this.bym, llm: this.llm, management: this.management, chaite: this.chaite } } } export default new ChatGPTConfig()