diff --git a/config/config.js b/config/config.js index 6d3e466..e93bec1 100644 --- a/config/config.js +++ b/config/config.js @@ -250,7 +250,7 @@ class ChatGPTConfig { historyPollInterval: 300, historyBatchSize: 120, promptHeader: '# 以下是一些该群聊中可能相关的事实,你可以参考,但不要主动透露这些事实。', - promptItemTemplate: '- ${fact}${topicSuffix}', + 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. @@ -282,7 +282,7 @@ Only include meaningful, verifiable group-specific information that is useful fo maxRelevantItemsPerQuery: 3, minImportanceForInjection: 0, promptHeader: '# 用户画像', - promptItemTemplate: '- ${value}', + 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. @@ -453,20 +453,13 @@ Return a JSON array of **strings**, and nothing else, without any other characte ? JSON.parse(content) : yaml.load(content) - // 只更新存在的配置项 + // 处理加载的配置并和默认值合并 if (loadedConfig) { - Object.keys(loadedConfig).forEach(key => { - if (['version', 'basic', 'bym', 'llm', 'management', 'chaite', 'memory'].includes(key)) { - if (typeof loadedConfig[key] === 'object' && loadedConfig[key] !== null) { - // 对象的合并 - if (!this[key]) this[key] = {} - Object.assign(this[key], loadedConfig[key]) - } else { - // 基本类型直接赋值 - this[key] = loadedConfig[key] - } - } - }) + 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') @@ -475,6 +468,68 @@ Return a JSON array of **strings**, and nothing else, without any other characte } } + _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) { // 清除之前的定时器 @@ -482,20 +537,18 @@ Return a JSON array of **strings**, and nothing else, without any other characte clearTimeout(this._saveTimer) } - // 记录保存来源 - this._saveOrigin = origin || 'code' - - // 设置定时器延迟保存 + const originLabel = origin || 'code' + this._saveOrigin = originLabel this._saveTimer = setTimeout(() => { - this.saveToFile() - // 保存完成后延迟一下再清除来源标记 - setTimeout(() => { - this._saveOrigin = null - }, 100) + this.saveToFile(originLabel) + this._saveOrigin = null }, 200) } - saveToFile () { + saveToFile (origin = 'code') { + if (origin !== 'code') { + this._saveOrigin = 'external' + } logger.debug('Saving config to file...') try { const config = { diff --git a/models/memory/database.js b/models/memory/database.js index c19acfd..63fbe3d 100644 --- a/models/memory/database.js +++ b/models/memory/database.js @@ -610,15 +610,7 @@ function ensureVectorTable (db) { let tablePresent = tableExists let needsTableReset = false - if (storedModel && storedModel !== currentModel) { - needsTableReset = true - } else if (!storedModel && tableExists) { - // Unknown model metadata but table exists; keep it as-is. - dimension = storedDimension - } - if (tableExists && storedDimension <= 0) { - logger?.warn?.('[Memory] vec_group_facts exists but stored dimension is invalid, rebuilding table') needsTableReset = true } @@ -628,11 +620,11 @@ function ensureVectorTable (db) { tablePresent = false dimension = 0 } catch (err) { - logger?.warn?.('[Memory] failed to drop vec_group_facts during model change:', err) + logger?.warn?.('[Memory] failed to drop vec_group_facts during dimension change:', err) } } - if (!tablePresent) { +if (!tablePresent) { if (dimension <= 0) { dimension = parseDimension(preferredDimension) } @@ -640,30 +632,36 @@ function ensureVectorTable (db) { try { createVectorTable(db, dimension) tablePresent = true + setMetaValue(db, META_VECTOR_MODEL_KEY, currentModel) + setMetaValue(db, META_VECTOR_DIM_KEY, String(dimension)) + cachedVectorDimension = dimension + cachedVectorModel = currentModel + return cachedVectorDimension } catch (err) { logger?.error?.('[Memory] failed to (re)create vec_group_facts table:', err) dimension = 0 } } - } else if (dimension > 0 && preferredDimension > 0 && dimension !== preferredDimension) { - logger?.debug?.('[Memory] vector table dimension (%s) differs from preferred (%s); keeping existing table', dimension, preferredDimension) } - const metaDimensionValue = dimension > 0 ? String(dimension) : '0' - setMetaValue(db, META_VECTOR_MODEL_KEY, currentModel) - setMetaValue(db, META_VECTOR_DIM_KEY, metaDimensionValue) + if (tablePresent && storedDimension > 0) { + cachedVectorDimension = storedDimension + cachedVectorModel = storedModel || currentModel + return cachedVectorDimension + } - cachedVectorDimension = dimension > 0 ? dimension : 0 + // At this point we failed to determine a valid dimension, set metadata to 0 to avoid loops. + setMetaValue(db, META_VECTOR_MODEL_KEY, currentModel) + setMetaValue(db, META_VECTOR_DIM_KEY, '0') + cachedVectorDimension = 0 cachedVectorModel = currentModel return cachedVectorDimension } - export function resetVectorTableDimension (dimension) { if (!Number.isFinite(dimension) || dimension <= 0) { throw new Error(`Invalid vector dimension: ${dimension}`) } const db = getMemoryDatabase() - logger?.info?.('[Memory] resetting group vector table dimension to %s', dimension) try { db.exec('DROP TABLE IF EXISTS vec_group_facts') } catch (err) { @@ -726,7 +724,12 @@ export function getMemoryDatabase () { } export function getVectorDimension () { - if (cachedVectorDimension) { + const currentModel = ChatGPTConfig.llm?.embeddingModel || '' + if (cachedVectorModel && cachedVectorModel !== currentModel) { + cachedVectorDimension = null + cachedVectorModel = null + } + if (cachedVectorDimension !== null) { return cachedVectorDimension } const db = getMemoryDatabase() diff --git a/models/memory/prompt.js b/models/memory/prompt.js index 1e1460b..7093e26 100644 --- a/models/memory/prompt.js +++ b/models/memory/prompt.js @@ -24,6 +24,8 @@ function formatUserMemories (memories, config) { segments.push(header) } memories.forEach((item, index) => { + const timestamp = item.updated_at || item.created_at || '' + const timeSuffix = timestamp ? `(记录时间:${timestamp})` : '' const context = { index, order: index + 1, @@ -33,7 +35,10 @@ function formatUserMemories (memories, config) { sourceId: item.source_message_id || '', groupId: item.group_id || '', createdAt: item.created_at || '', - updatedAt: item.updated_at || '' + updatedAt: item.updated_at || '', + timestamp, + time: timestamp, + timeSuffix } const line = renderTemplate(itemTemplate, context) if (line) { @@ -61,6 +66,8 @@ function formatGroupFacts (facts, config) { } facts.forEach((item, index) => { const topicSuffix = item.topic ? `(${item.topic})` : '' + const timestamp = item.updated_at || item.created_at || '' + const timeSuffix = timestamp ? `(记录时间:${timestamp})` : '' const context = { index, order: index + 1, @@ -70,6 +77,9 @@ function formatGroupFacts (facts, config) { importance: item.importance ?? '', createdAt: item.created_at || '', updatedAt: item.updated_at || '', + timestamp, + time: timestamp, + timeSuffix, distance: item.distance ?? '', bm25: item.bm25_score ?? '', sourceMessages: item.source_messages || '',