168 lines
5.6 KiB
JavaScript
168 lines
5.6 KiB
JavaScript
// src/brain/memory-manager.js
|
||
// 记忆管理器 — 三层记忆架构
|
||
// 职责:短期/中期/长期记忆管理,记忆候选生成,写入判定
|
||
//
|
||
// 记忆分层:
|
||
// 短期(session):当前会话消息,存在前端 + 进程内存
|
||
// 中期(task):当前任务状态、开发者进度、决策上下文,存在进程内存
|
||
// 长期(persistent):身份信息、项目目标、关键决策,写入 brain 文件
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
// 中期记忆存储(进程内存,重启丢失)
|
||
const taskMemory = new Map();
|
||
|
||
// 记忆候选关键词 — 检测哪些信息值得生成记忆候选
|
||
const MEMORY_CANDIDATE_PATTERNS = [
|
||
{ type: 'identity_change', pattern: /身份|角色|权限|加入团队|离开|转岗/i, priority: 'high' },
|
||
{ type: 'project_goal', pattern: /目标|里程碑|milestone|deadline|截止|规划|roadmap/i, priority: 'high' },
|
||
{ type: 'module_ownership', pattern: /负责|接手|模块|m\d{2}-|归属|分工/i, priority: 'medium' },
|
||
{ type: 'decision', pattern: /决定|确定|方案|选择|采用|放弃|不再|改为/i, priority: 'high' },
|
||
{ type: 'todo', pattern: /待办|todo|下一步|接下来|计划|需要做/i, priority: 'medium' },
|
||
{ type: 'bug_fix', pattern: /修复|fix|解决|排查到|根因|原因是/i, priority: 'low' },
|
||
{ type: 'deployment', pattern: /上线|部署|发布|deploy|release|版本/i, priority: 'medium' },
|
||
];
|
||
|
||
/**
|
||
* 判定规则:是否值得写入长期记忆
|
||
* 不是什么都记——只记高优先级的、或中等优先级且重复出现的
|
||
*/
|
||
const WRITE_RULES = {
|
||
high: { minOccurrences: 1, description: '首次出现即记录' },
|
||
medium: { minOccurrences: 2, description: '重复提及 2 次后记录' },
|
||
low: { minOccurrences: 3, description: '反复提及 3 次后记录' },
|
||
};
|
||
|
||
/**
|
||
* 分析文本,生成记忆候选
|
||
* @param {string} text - 要分析的文本
|
||
* @param {string} source - 来源 (user/assistant/system)
|
||
* @returns {Array<{type: string, priority: string, excerpt: string, shouldPersist: boolean}>}
|
||
*/
|
||
function generateCandidates(text, source = 'user') {
|
||
if (!text || typeof text !== 'string') return [];
|
||
|
||
const candidates = [];
|
||
for (const { type, pattern, priority } of MEMORY_CANDIDATE_PATTERNS) {
|
||
if (pattern.test(text)) {
|
||
// 提取匹配上下文的前后50字符作为摘要
|
||
const match = text.match(pattern);
|
||
const idx = match ? match.index : 0;
|
||
const start = Math.max(0, idx - 30);
|
||
const end = Math.min(text.length, idx + 80);
|
||
const excerpt = text.slice(start, end).replace(/\n/g, ' ').trim();
|
||
|
||
// 检查中期记忆中的出现次数
|
||
const memKey = type + ':' + excerpt.slice(0, 20);
|
||
const taskEntry = taskMemory.get(memKey) || { count: 0 };
|
||
taskEntry.count++;
|
||
taskEntry.lastSeen = Date.now();
|
||
taskMemory.set(memKey, taskEntry);
|
||
|
||
const rule = WRITE_RULES[priority];
|
||
const shouldPersist = taskEntry.count >= rule.minOccurrences;
|
||
|
||
candidates.push({
|
||
type,
|
||
priority,
|
||
excerpt,
|
||
source,
|
||
shouldPersist,
|
||
occurrences: taskEntry.count,
|
||
threshold: rule.minOccurrences,
|
||
});
|
||
}
|
||
}
|
||
|
||
return candidates;
|
||
}
|
||
|
||
/**
|
||
* 保存记忆到中期存储(任务级别)
|
||
* @param {string} sessionId
|
||
* @param {string} key
|
||
* @param {*} value
|
||
*/
|
||
function setTaskMemory(sessionId, key, value) {
|
||
const sKey = sessionId + ':' + key;
|
||
taskMemory.set(sKey, { value, updatedAt: Date.now() });
|
||
}
|
||
|
||
/**
|
||
* 读取中期记忆
|
||
* @param {string} sessionId
|
||
* @param {string} key
|
||
* @returns {*}
|
||
*/
|
||
function getTaskMemory(sessionId, key) {
|
||
const entry = taskMemory.get(sessionId + ':' + key);
|
||
return entry ? entry.value : undefined;
|
||
}
|
||
|
||
/**
|
||
* 清理过期的中期记忆(超过 2 小时的条目)
|
||
*/
|
||
function cleanupTaskMemory() {
|
||
const cutoff = Date.now() - 2 * 60 * 60 * 1000;
|
||
for (const [key, entry] of taskMemory.entries()) {
|
||
const ts = entry.updatedAt || entry.lastSeen || 0;
|
||
if (ts < cutoff) {
|
||
taskMemory.delete(key);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 每 30 分钟清理一次过期中期记忆
|
||
const cleanupInterval = setInterval(cleanupTaskMemory, 30 * 60 * 1000);
|
||
// 允许进程在没有其他活动时优雅退出
|
||
if (cleanupInterval.unref) cleanupInterval.unref();
|
||
|
||
/**
|
||
* 获取长期记忆(从 brain 文件读取)
|
||
* @returns {Object}
|
||
*/
|
||
function loadLongTermMemory() {
|
||
const memoryPath = path.join(__dirname, '../../.github/brain/memory.json');
|
||
try {
|
||
return JSON.parse(fs.readFileSync(memoryPath, 'utf8'));
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取记忆层状态概览
|
||
*/
|
||
function getMemoryStatus() {
|
||
const longTerm = loadLongTermMemory();
|
||
return {
|
||
layers: {
|
||
short_term: { type: 'session', storage: 'frontend + process', description: '当前会话消息' },
|
||
mid_term: { type: 'task', storage: 'process memory', entries: taskMemory.size, description: '当前任务/开发者状态' },
|
||
long_term: { type: 'persistent', storage: 'brain files', loaded: !!longTerm, description: '身份/目标/决策/待办' },
|
||
},
|
||
write_rules: WRITE_RULES,
|
||
candidate_types: MEMORY_CANDIDATE_PATTERNS.map(p => ({ type: p.type, priority: p.priority })),
|
||
recovery_path: [
|
||
'.github/brain/memory.json',
|
||
'.github/brain/routing-map.json',
|
||
'.github/brain/wake-protocol.md',
|
||
'.github/persona-brain/dev-status.json',
|
||
],
|
||
};
|
||
}
|
||
|
||
module.exports = {
|
||
generateCandidates,
|
||
setTaskMemory,
|
||
getTaskMemory,
|
||
cleanupTaskMemory,
|
||
loadLongTermMemory,
|
||
getMemoryStatus,
|
||
MEMORY_CANDIDATE_PATTERNS,
|
||
WRITE_RULES,
|
||
};
|