-
Notifications
You must be signed in to change notification settings - Fork 129
Expand file tree
/
Copy pathconfig.js
More file actions
590 lines (543 loc) · 18.2 KB
/
config.js
File metadata and controls
590 lines (543 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
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,
* sendReasoning: boolean,
* }}
*/
basic = {
// 触发方式,at触发或者前缀触发
toggleMode: 'at',
// 触发前缀,仅在前缀触发时有效
togglePrefix: '#chat',
// 是否开启调试模式
debug: false,
// 一般命令的开头
commandPrefix: '#chatgpt',
// 是否发送思考内容
sendReasoning: true
}
/**
* 伪人模式,基于框架实现,因此机器人开启前缀后依然需要带上前缀。
* @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: '<settings>\n' +
// eslint-disable-next-line no-template-curly-in-string
'You are a member of a chat group, whose name is ${group.name}, and the group id is ${group.id}.\n' +
'</settings>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',
// 触发器目录,相对于插件目录下
triggersDir: 'utils/triggers',
// 工具目录,相对于插件目录下
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'
}
/**
* 记忆系统配置
* @type {{
* database: string,
* vectorDimensions: number,
* group: {
* enable: boolean,
* enabledGroups: string[],
* extractionModel: string,
* extractionPresetId: string,
* minMessageCount: number,
* maxMessageWindow: number,
* retrievalMode: 'vector' | 'keyword' | 'hybrid',
* hybridPrefer: 'vector-first' | 'keyword-first',
* historyPollInterval: number,
* historyBatchSize: number,
* promptHeader: string,
* promptItemTemplate: string,
* promptFooter: string,
* extractionSystemPrompt: string,
* extractionUserPrompt: string,
* vectorMaxDistance: number,
* textMaxBm25Score: number,
* maxFactsPerInjection: number,
* minImportanceForInjection: number
* },
* user: {
* enable: boolean,
* whitelist: string[],
* blacklist: string[],
* extractionModel: string,
* extractionPresetId: string,
* maxItemsPerInjection: number,
* maxRelevantItemsPerQuery: number,
* minImportanceForInjection: number,
* promptHeader: string,
* promptItemTemplate: string,
* promptFooter: string,
* extractionSystemPrompt: string,
* extractionUserPrompt: string
* },
* extensions: {
* simple: {
* enable: boolean,
* libraryPath: string,
* dictPath: string,
* useJieba: boolean
* }
* }
* }}
*/
memory = {
database: 'data/memory.db',
vectorDimensions: 1536,
group: {
enable: false,
enabledGroups: [],
extractionModel: '',
extractionPresetId: '',
minMessageCount: 80,
maxMessageWindow: 300,
retrievalMode: 'hybrid',
hybridPrefer: 'vector-first',
historyPollInterval: 300,
historyBatchSize: 120,
promptHeader: '# 以下是一些该群聊中可能相关的事实,你可以参考,但不要主动透露这些事实。',
promptItemTemplate: '- ${fact}${topicSuffix}${timeSuffix}',
promptFooter: '',
extractionSystemPrompt: `You are a knowledge extraction assistant that specialises in summarising long-term facts from group chat transcripts.
Read the provided conversation and identify statements that should be stored as long-term knowledge for the group.
Return a JSON array. Each element must contain:
{
"fact": 事实内容,必须完整包含事件的各个要素而不能是简单的短语(比如谁参与了事件、做了什么事情、背景时间是什么)(同一件事情尽可能整合为同一条而非拆分,以便利于检索),
"topic": 主题关键词,字符串,如 "活动"、"成员信息",
"importance": 一个介于0和1之间的小数,数值越大表示越重要,
"source_message_ids": 原始消息ID数组,
"source_messages": 对应原始消息的简要摘录或合并文本,
"involved_users": 出现或相关的用户ID数组
}
Only include meaningful, verifiable group-specific information that is useful for future conversations. Do not record incomplete information. Do not include general knowledge or unrelated facts. Do not wrap the JSON array in code fences.`,
extractionUserPrompt: `以下是群聊中的一些消息,请根据系统说明提取值得长期记忆的事实,以JSON数组形式返回,不要输出额外说明。
\${messages}`,
vectorMaxDistance: 0,
textMaxBm25Score: 0,
maxFactsPerInjection: 5,
minImportanceForInjection: 0.3
},
user: {
enable: false,
whitelist: [],
blacklist: [],
extractionModel: '',
extractionPresetId: '',
maxItemsPerInjection: 5,
maxRelevantItemsPerQuery: 3,
minImportanceForInjection: 0,
promptHeader: '# 用户画像',
promptItemTemplate: '- ${value}${timeSuffix}',
promptFooter: '',
extractionSystemPrompt: `You are an assistant that extracts long-term personal preferences or persona details about a user.
Given a conversation snippet between the user and the bot, identify durable information such as preferences, nicknames, roles, speaking style, habits, or other facts that remain valid over time.
Return a JSON array of **strings**, and nothing else, without any other characters including \`\`\` or \`\`\`json. Each string must be a short sentence (in the same language as the conversation) describing one piece of long-term memory. Do not include keys, JSON objects, or additional metadata. Ignore temporary topics or uncertain information.`,
extractionUserPrompt: `下面是用户与机器人的对话,请根据系统提示提取可长期记忆的个人信息。
\${messages}`
},
extensions: {
simple: {
enable: false,
libraryPath: '',
dictPath: '',
useJieba: false
}
}
}
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')
if (fs.existsSync(jsonPath)) {
this.configPath = jsonPath
} else if (fs.existsSync(yamlPath)) {
this.configPath = yamlPath
} else {
this.configPath = jsonPath
this.saveToFile()
}
// 加载初始配置
this.loadFromFile()
// 文件变更标志和保存定时器
this._saveOrigin = null
this._saveTimer = null
// 监听文件变化
this.watcher = fs.watchFile(this.configPath, (curr, prev) => {
if (curr.mtime !== prev.mtime && this._saveOrigin !== 'code') {
this.loadFromFile()
}
})
// 处理所有嵌套对象
return this._createProxyRecursively(this)
}
// 递归创建代理
_createProxyRecursively (obj, path = []) {
if (obj === null || typeof obj !== 'object' || obj instanceof Date) {
return obj
}
// 处理数组和对象
if (Array.isArray(obj)) {
// 创建一个新数组,递归地处理每个元素
const proxiedArray = [...obj].map((item, index) =>
this._createProxyRecursively(item, [...path, index])
)
// 代理数组,捕获数组方法调用
return new Proxy(proxiedArray, {
set: (target, prop, value) => {
// 处理数字属性(数组索引)和数组长度
if (typeof prop !== 'symbol' &&
((!isNaN(prop) && prop !== 'length') ||
prop === 'length')) {
// 直接设置值
target[prop] = value
// 触发保存
this._triggerSave('array')
} else {
target[prop] = value
}
return true
},
// 拦截数组方法调用
get: (target, prop) => {
const val = target[prop]
// 处理数组修改方法
if (typeof val === 'function' &&
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].includes(prop)) {
return function (...args) {
const result = Array.prototype[prop].apply(target, args)
// 方法调用后触发保存
this._triggerSave('array-method')
return result
}.bind(this)
}
return val
}
})
} else {
// 对普通对象的处理
const proxiedObj = {}
// 处理所有属性
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// 跳过内部属性
if (key === 'watcher' || key === 'configPath' ||
key.startsWith('_save') || key === '_isSaving') {
proxiedObj[key] = obj[key]
} else {
// 递归处理嵌套对象
proxiedObj[key] = this._createProxyRecursively(
obj[key], [...path, key]
)
}
}
}
// 创建对象的代理
return new Proxy(proxiedObj, {
set: (target, prop, value) => {
// 跳过内部属性的处理
if (prop === 'watcher' || prop === 'configPath' ||
prop.startsWith('_save') || prop === '_isSaving') {
target[prop] = value
return true
}
// 设置新值,如果是对象则递归创建代理
if (value !== null && typeof value === 'object') {
target[prop] = this._createProxyRecursively(
value, [...path, prop]
)
} else {
target[prop] = value
}
// 触发保存
this._triggerSave('object')
return true
}
})
}
}
loadFromFile () {
try {
if (!fs.existsSync(this.configPath)) {
// 如果文件不存在,直接返回
return
}
const content = fs.readFileSync(this.configPath, 'utf8')
const loadedConfig = this.configPath.endsWith('.json')
? JSON.parse(content)
: yaml.load(content)
// 处理加载的配置并和默认值合并
if (loadedConfig) {
const mergeResult = this._mergeConfig(loadedConfig)
if (mergeResult.changed) {
logger?.debug?.('[Config] merged new defaults into persisted config; scheduling save')
this._triggerSave('code')
}
}
logger.debug('Config loaded successfully')
} catch (error) {
logger.error('Failed to load config:', error)
}
}
_mergeConfig (loadedConfig) {
let changed = false
const mergeInto = (target, source) => {
if (!source || typeof source !== 'object') {
return target
}
if (!target || typeof target !== 'object') {
target = Array.isArray(source) ? [] : {}
}
const result = Array.isArray(source) ? [] : { ...target }
if (Array.isArray(source)) {
return source.slice()
}
const targetKeys = target && typeof target === 'object'
? Object.keys(target)
: []
for (const key of targetKeys) {
if (!Object.prototype.hasOwnProperty.call(source, key)) {
changed = true
}
}
for (const key of Object.keys(source)) {
const sourceValue = source[key]
const targetValue = target[key]
if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue)) {
result[key] = mergeInto(targetValue, sourceValue)
} else {
if (targetValue === undefined || targetValue !== sourceValue) {
changed = true
}
result[key] = sourceValue
}
}
return result
}
const sections = ['version', 'basic', 'bym', 'llm', 'management', 'chaite', 'memory']
for (const key of sections) {
const loadedValue = loadedConfig[key]
if (loadedValue === undefined) {
continue
}
if (typeof loadedValue === 'object' && loadedValue !== null) {
const merged = mergeInto(this[key], loadedValue)
if (merged !== this[key]) {
this[key] = merged
}
} else {
if (this[key] !== loadedValue) {
changed = true
}
this[key] = loadedValue
}
}
return { changed }
}
// 合并触发保存,防抖处理
_triggerSave (origin) {
// 清除之前的定时器
if (this._saveTimer) {
clearTimeout(this._saveTimer)
}
const originLabel = origin || 'code'
this._saveOrigin = originLabel
this._saveTimer = setTimeout(() => {
this.saveToFile(originLabel)
this._saveOrigin = null
}, 200)
}
saveToFile (origin = 'code') {
if (origin !== 'code') {
this._saveOrigin = 'external'
}
logger.debug('Saving config to file...')
try {
const config = {
version: this.version,
basic: this.basic,
bym: this.bym,
llm: this.llm,
management: this.management,
chaite: this.chaite,
memory: this.memory
}
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,
memory: this.memory
}
}
}
export default new ChatGPTConfig()