Commit 2e6f62a519aca4f3e0d99110a2455adfd3c1739e

Authored by yibo
1 parent ef48bf0b

feat: implement Memory Phase 2 — hippocampus retrieval + cerebellum plans

- Hippocampus keyword matching engine (extractKeywords + score)
- Cerebellum plan storage (saveCerebellumPlan)
- Auto-sync hippocampus-index.json + hippocampus.md
- POST /:agentId/memory/cerebellum API endpoint
- buildSystemPrompt injects matched plans from hippocampus
- Use count & lastUsed tracking on plan access
- Chinese n-gram + English word tokenizer with stop words
- Fix: score formula uses entry keyword count as denominator
- Fix: useCount updates original index entry not spread copy
- Verified: OKR plan matched + injected into CEO prompt
data/agents/ceo/memory/cerebellum/blm战略规划.md 0 → 100644
  1 +# 方案: BLM战略规划
  2 +
  3 +> 创建时间: 2026-03-25
  4 +
  5 +## 适用条件
  6 +- 需要做年度或长期战略规划
  7 +- 需要分析业绩/机会差距
  8 +
  9 +## 执行步骤
  10 +1. 双差分析:业绩差距+机会差距
  11 +2. 战略意图四维度(愿景→目标→近期→指标)
  12 +3. 市场洞察(PEST+5Forces)
  13 +4. 创新焦点识别
  14 +5. 业务设计五要素(客户×价值×盈利×范围×控制)
  15 +6. 执行体系(关键任务+组织+人才+氛围)
  16 +
  17 +## 注意事项
  18 +- 差距是战略的起点不是终点
  19 +- 业务设计必须有战略控制点
data/agents/ceo/memory/cerebellum/okr目标制定.md 0 → 100644
  1 +# 方案: OKR目标制定
  2 +
  3 +> 创建时间: 2026-03-25
  4 +
  5 +## 适用条件
  6 +- 需要制定季度/年度目标
  7 +- 需要将战略分解为可衡量指标
  8 +
  9 +## 执行步骤
  10 +1. 从战略目标出发识别3-5个Objective
  11 +2. 每个Objective制定2-4个Key Result
  12 +3. 用SMART原则检查每个KR
  13 +4. 层层对齐:公司OKR→部门OKR→个人OKR
  14 +5. 设定每周跟踪和月度复盘节奏
  15 +
  16 +## 注意事项
  17 +- O要有鼓舞性,KR要可量化
  18 +- 目标完成率70%为最佳(太高说明目标太保守)
  19 +- 避免把KR当成任务清单
data/agents/ceo/memory/hippocampus-index.json 0 → 100644
  1 +[
  2 + {
  3 + "scenario": "OKR目标制定",
  4 + "keywords": [
  5 + "OKR",
  6 + "目标",
  7 + "制定",
  8 + "关键结果",
  9 + "objective",
  10 + "key result"
  11 + ],
  12 + "planFile": "okr目标制定.md",
  13 + "confidence": 0.85,
  14 + "useCount": 1,
  15 + "lastUsed": "2026-03-25",
  16 + "createdAt": "2026-03-25"
  17 + },
  18 + {
  19 + "scenario": "BLM战略规划",
  20 + "keywords": [
  21 + "BLM",
  22 + "战略",
  23 + "规划",
  24 + "差距分析",
  25 + "业务设计",
  26 + "战略制定"
  27 + ],
  28 + "planFile": "blm战略规划.md",
  29 + "confidence": 0.9,
  30 + "useCount": 0,
  31 + "lastUsed": null,
  32 + "createdAt": "2026-03-25"
  33 + }
  34 +]
0 \ No newline at end of file 35 \ No newline at end of file
data/agents/ceo/memory/hippocampus.md 0 → 100644
  1 +# 海马体 — 经验记忆索引
  2 +
  3 +> 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。
  4 +> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。
  5 +
  6 +| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 |
  7 +|---|---------|-----------|---------|--------|---------|----------|
  8 +| 1 | OKR目标制定 | OKR, 目标, 制定, 关键结果, objective, key result | okr目标制定.md | 0.85 | 0 | - |
  9 +| 2 | BLM战略规划 | BLM, 战略, 规划, 差距分析, 业务设计, 战略制定 | blm战略规划.md | 0.9 | 0 | - |
data/agents/ceo/memory/memory.md
1 # 我的记忆 1 # 我的记忆
2 2
3 ## 关键事实 3 ## 关键事实
4 -- [2026-03-25] [TASK from master] → ## 战略规划的 3 个核心要点 4 +- [2026-03-25] [TASK from master] → **BLM 模型的核心思想**:以差距分析为起点,通过"战略制定"(市场洞察、战略意图、创新焦点、业务设计五要素)与"战略执行"(关键任务、组织、人才、文化)的双轮驱动,实现业绩差距和机会差距的闭合,
  5 +- [2026-03-25] [TASK from master] → 是的,我记得刚才我们讨论的是 **BLM 模型**(Business Leadership Model,业务领先模型)。
  6 +- [2026-03-25] [TASK from master] → ...馈收集机制 | 编辑器产品化 |
5 7
6 ## 已验证的模式 8 ## 已验证的模式
7 (尚无记录) 9 (尚无记录)
data/agents/ceo/todo.md
@@ -10,3 +10,6 @@ @@ -10,3 +10,6 @@
10 | T-mn4aq3tt | 【完成首个项目剧本大纲】首个互动剧项目《深渊回响》已立项,现在需要完成详细剧本大纲。具体要求:1) 参考 workspace/project_001_proposal.md 中的项目设定;2) 设计完整的故事结构:开端(研究站失联)、发展(救援队员逐步发现真相)、高潮(核心秘密揭露)、结局(多重分支结局);3) 设计核心角色及其对话风格;4) 设计关键互动节点和分支逻辑;5) 将剧本大纲输出到 workspace/project_001_script_outline.md;6) 完成后向我汇报 | ✅ 完成 | master-001 | P1 | 2026-03-24 07:31 | - | 10 | T-mn4aq3tt | 【完成首个项目剧本大纲】首个互动剧项目《深渊回响》已立项,现在需要完成详细剧本大纲。具体要求:1) 参考 workspace/project_001_proposal.md 中的项目设定;2) 设计完整的故事结构:开端(研究站失联)、发展(救援队员逐步发现真相)、高潮(核心秘密揭露)、结局(多重分支结局);3) 设计核心角色及其对话风格;4) 设计关键互动节点和分支逻辑;5) 将剧本大纲输出到 workspace/project_001_script_outline.md;6) 完成后向我汇报 | ✅ 完成 | master-001 | P1 | 2026-03-24 07:31 | - |
11 | T-mn4b2s1a | 【完成技术架构搭建】首个互动剧项目《深渊回响》剧本大纲已完成,现在需要完成技术架构搭建。具体要求:1) 参考 workspace/project_001_proposal.md 和 workspace/project_001_script_outline.md;2) 设计技术架构方案,包括:AI对话引擎选型、分支逻辑系统、用户状态管理、内容存储方案;3) 明确技术选型理由和依赖库;4) 输出技术架构文档到 workspace/project_001_tech_architecture.md;5) 完成后向我汇报,标志着 NarrativeAI Inc. 公司设立任务全部完成 | ✅ 完成 | master-001 | P0 | 2026-03-24 07:41 | - | 11 | T-mn4b2s1a | 【完成技术架构搭建】首个互动剧项目《深渊回响》剧本大纲已完成,现在需要完成技术架构搭建。具体要求:1) 参考 workspace/project_001_proposal.md 和 workspace/project_001_script_outline.md;2) 设计技术架构方案,包括:AI对话引擎选型、分支逻辑系统、用户状态管理、内容存储方案;3) 明确技术选型理由和依赖库;4) 输出技术架构文档到 workspace/project_001_tech_architecture.md;5) 完成后向我汇报,标志着 NarrativeAI Inc. 公司设立任务全部完成 | ✅ 完成 | master-001 | P0 | 2026-03-24 07:41 | - |
12 | T-mn5g4hlb | 列出你知道的关于战略规划的3个核心要点,简要回答即可,不需要使用工具。 | ✅ 完成 | master | P1 | 2026-03-25 02:50 | - | 12 | T-mn5g4hlb | 列出你知道的关于战略规划的3个核心要点,简要回答即可,不需要使用工具。 | ✅ 完成 | master | P1 | 2026-03-25 02:50 | - |
  13 +| T-mn5g8r4s | 请用一句话总结BLM模型的核心思想。不需要使用任何工具,直接回答即可。 | ✅ 完成 | master | P2 | 2026-03-25 02:53 | - |
  14 +| T-mn5g9cri | 你还记得刚才我们讨论了什么模型吗?如果你的记忆中有相关信息,请引用它。不需要使用工具。 | ✅ 完成 | master | P2 | 2026-03-25 02:54 | - |
  15 +| T-mn5glp6q | 帮我制定下季度的OKR目标,公司方向是AI互动剧。简要列出结构即可,不需要工具。 | ✅ 完成 | master | P2 | 2026-03-25 03:03 | - |
server/routes/agent-chat.js
@@ -20,7 +20,7 @@ const { ClaudeCliProtocol } = require('@mas/acp-core'); @@ -20,7 +20,7 @@ const { ClaudeCliProtocol } = require('@mas/acp-core');
20 const { Parser } = require('../src/core/parser'); 20 const { Parser } = require('../src/core/parser');
21 const AgentManager = require('../src/core/agent-manager'); 21 const AgentManager = require('../src/core/agent-manager');
22 const { GoalsService } = require('../services/goals'); 22 const { GoalsService } = require('../services/goals');
23 -const { saveSessionSummary, loadRecentMemory } = require('../services/memory-service'); 23 +const { saveSessionSummary, loadRecentMemory, retrieveRelevantPlans } = require('../services/memory-service');
24 const systemConfig = require('../config'); 24 const systemConfig = require('../config');
25 const router = express.Router(); 25 const router = express.Router();
26 26
@@ -293,9 +293,16 @@ function buildSystemPrompt(agentDir) { @@ -293,9 +293,16 @@ function buildSystemPrompt(agentDir) {
293 if (memory.trim()) parts.push('---\n\n# 我的记忆\n\n' + memory.trim()); 293 if (memory.trim()) parts.push('---\n\n# 我的记忆\n\n' + memory.trim());
294 294
295 // short-term — 近期对话摘要 (Phase 1 MVP) 295 // short-term — 近期对话摘要 (Phase 1 MVP)
296 - const recentMemory = loadRecentMemory(config?.id || path.basename(agentDir)); 296 + const agentIdForMemory = config?.id || path.basename(agentDir);
  297 + const recentMemory = loadRecentMemory(agentIdForMemory);
297 if (recentMemory) parts.push('---\n\n' + recentMemory); 298 if (recentMemory) parts.push('---\n\n' + recentMemory);
298 299
  300 + // hippocampus — 海马体检索相关经验 (Phase 2)
  301 + // 使用当前任务文本作为检索 key(从 todo.md 或最后一条消息提取)
  302 + const todoContent = loadFile(agentDir, 'todo.md');
  303 + const hippocampusPlans = retrieveRelevantPlans(agentIdForMemory, todoContent);
  304 + if (hippocampusPlans) parts.push('---\n\n' + hippocampusPlans);
  305 +
299 // skills 306 // skills
300 let mainSkill = loadFile(agentDir, '.claude/skills/main_skill/SKILL.md'); 307 let mainSkill = loadFile(agentDir, '.claude/skills/main_skill/SKILL.md');
301 if (mainSkill.trim()) { 308 if (mainSkill.trim()) {
@@ -623,6 +630,23 @@ router.get('/list', (req, res) => { @@ -623,6 +630,23 @@ router.get('/list', (req, res) => {
623 } catch (err) { res.status(500).json({ error: err.message }); } 630 } catch (err) { res.status(500).json({ error: err.message }); }
624 }); 631 });
625 632
  633 +// ── Phase 2: 小脑方案固化 API ──
  634 +router.post('/:agentId/memory/cerebellum', (req, res) => {
  635 + const { saveCerebellumPlan } = require('../services/memory-service');
  636 + const { scenario, keywords, steps, conditions, notes, rawContent, confidence } = req.body;
  637 + if (!scenario) return res.status(400).json({ error: 'scenario is required' });
  638 +
  639 + const result = saveCerebellumPlan(req.params.agentId, {
  640 + scenario, keywords, steps, conditions, notes, rawContent,
  641 + confidence: confidence || 0.7,
  642 + });
  643 + if (result) {
  644 + res.json({ success: true, path: result });
  645 + } else {
  646 + res.status(500).json({ error: 'Failed to save cerebellum plan' });
  647 + }
  648 +});
  649 +
626 router.get('/:agentId/info', (req, res) => { 650 router.get('/:agentId/info', (req, res) => {
627 const agentDir = path.join(DATA_DIR, req.params.agentId); 651 const agentDir = path.join(DATA_DIR, req.params.agentId);
628 if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' }); 652 if (!fs.existsSync(agentDir)) return res.status(404).json({ error: 'Agent not found' });
server/scripts/test-memory.js 0 → 100644
  1 +#!/usr/bin/env node
  2 +/**
  3 + * Memory System Phase 1 MVP — 集成测试
  4 + *
  5 + * 测试流程:
  6 + * 1. 清理 CEO 的 short-term 和 memory.md
  7 + * 2. 发送第一次对话 → 验证 session 摘要和 memory.md 写入
  8 + * 3. 发送第二次对话 → 验证 prompt 中包含近期记忆
  9 + */
  10 +
  11 +const http = require('http');
  12 +const fs = require('fs');
  13 +const path = require('path');
  14 +
  15 +const BASE = 'http://localhost:3000';
  16 +const AGENT = 'ceo';
  17 +const AGENT_DIR = path.resolve(__dirname, '../../data/agents', AGENT);
  18 +const SHORT_TERM_DIR = path.join(AGENT_DIR, 'memory', 'short-term');
  19 +const MEMORY_MD = path.join(AGENT_DIR, 'memory', 'memory.md');
  20 +
  21 +let passed = 0, failed = 0;
  22 +
  23 +function assert(condition, msg) {
  24 + if (condition) { passed++; console.log(` ✅ ${msg}`); }
  25 + else { failed++; console.log(` ❌ ${msg}`); }
  26 +}
  27 +
  28 +function post(url, body) {
  29 + return new Promise((resolve, reject) => {
  30 + const data = JSON.stringify(body);
  31 + const u = new URL(url);
  32 + const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST',
  33 + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }
  34 + }, res => {
  35 + let buf = '';
  36 + res.on('data', c => buf += c);
  37 + res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } });
  38 + });
  39 + req.on('error', reject);
  40 + req.write(data);
  41 + req.end();
  42 + });
  43 +}
  44 +
  45 +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  46 +
  47 +function waitForCompletion(agentId, convId, timeoutMs = 120000) {
  48 + const metaPath = path.join(AGENT_DIR, '..', agentId, 'conversations', convId, 'meta.json');
  49 + const start = Date.now();
  50 + return new Promise((resolve, reject) => {
  51 + const check = () => {
  52 + if (Date.now() - start > timeoutMs) return reject(new Error('Timeout'));
  53 + try {
  54 + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
  55 + if (meta.status === 'completed' || meta.status === 'failed') return resolve(meta);
  56 + } catch {}
  57 + setTimeout(check, 2000);
  58 + };
  59 + check();
  60 + });
  61 +}
  62 +
  63 +async function main() {
  64 + console.log('\n🧪 Memory System Phase 1 — 集成测试\n');
  65 + console.log('═'.repeat(50));
  66 +
  67 + // ── Step 0: 清理 ──
  68 + console.log('\n📋 Step 0: 清理历史数据');
  69 + // 清理 short-term
  70 + if (fs.existsSync(SHORT_TERM_DIR)) {
  71 + const files = fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-'));
  72 + files.forEach(f => fs.unlinkSync(path.join(SHORT_TERM_DIR, f)));
  73 + console.log(` 🗑️ 清理了 ${files.length} 个 session 文件`);
  74 + }
  75 + // 重置 memory.md
  76 + fs.writeFileSync(MEMORY_MD, '# 我的记忆\n\n## 关键事实\n(尚无记录)\n\n## 已验证的模式\n(尚无记录)\n\n## 失败教训\n(尚无记录)\n', 'utf-8');
  77 + console.log(' 🗑️ memory.md 已重置');
  78 +
  79 + // ── Step 1: 第一次对话 ──
  80 + console.log('\n📋 Step 1: 发送第一次对话');
  81 + const resp1 = await post(`${BASE}/api/agent/task/dispatch`, {
  82 + from: 'master', to: AGENT,
  83 + task: '请用一句话总结BLM模型的核心思想。不需要使用任何工具,直接回答即可。',
  84 + priority: 'P2', autoExecute: true,
  85 + });
  86 + assert(resp1.success, `dispatch 成功 (taskId: ${resp1.taskId})`);
  87 + const convId1 = resp1.executionConvId;
  88 +
  89 + console.log(' ⏳ 等待对话完成...');
  90 + const meta1 = await waitForCompletion(AGENT, convId1);
  91 + assert(meta1.status === 'completed', `对话完成 (status: ${meta1.status})`);
  92 +
  93 + // 等待 memory 写入(异步)
  94 + await sleep(2000);
  95 +
  96 + // 验证 short-term
  97 + const sessions1 = fs.existsSync(SHORT_TERM_DIR)
  98 + ? fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-'))
  99 + : [];
  100 + assert(sessions1.length === 1, `short-term 有 ${sessions1.length} 个 session 文件`);
  101 +
  102 + if (sessions1.length > 0) {
  103 + const sessionContent = fs.readFileSync(path.join(SHORT_TERM_DIR, sessions1[0]), 'utf-8');
  104 + assert(sessionContent.includes('# Session 摘要'), 'session 包含标题');
  105 + assert(sessionContent.includes('BLM') || sessionContent.includes('战略'), 'session 包含任务关键词');
  106 + assert(sessionContent.includes('工具调用'), 'session 包含 toolCall 统计');
  107 + console.log(` 📄 Session 文件: ${sessions1[0]} (${sessionContent.length} 字)`);
  108 + }
  109 +
  110 + // 验证 memory.md
  111 + const memContent1 = fs.readFileSync(MEMORY_MD, 'utf-8');
  112 + const hasNewFact = !memContent1.includes('(尚无记录)') || memContent1.match(/- \[202/);
  113 + assert(hasNewFact, `memory.md 有新事实记录`);
  114 + console.log(` 📝 memory.md: ${memContent1.length} 字`);
  115 +
  116 + // ── Step 2: 第二次对话 ──
  117 + console.log('\n📋 Step 2: 发送第二次对话 (验证近期记忆注入)');
  118 + const resp2 = await post(`${BASE}/api/agent/task/dispatch`, {
  119 + from: 'master', to: AGENT,
  120 + task: '你还记得刚才我们讨论了什么模型吗?如果你的记忆中有相关信息,请引用它。不需要使用工具。',
  121 + priority: 'P2', autoExecute: true,
  122 + });
  123 + assert(resp2.success, `第二次 dispatch 成功 (taskId: ${resp2.taskId})`);
  124 + const convId2 = resp2.executionConvId;
  125 +
  126 + console.log(' ⏳ 等待第二次对话完成...');
  127 + const meta2 = await waitForCompletion(AGENT, convId2);
  128 + assert(meta2.status === 'completed', `第二次对话完成 (status: ${meta2.status})`);
  129 +
  130 + await sleep(2000);
  131 +
  132 + // 验证 short-term 增长
  133 + const sessions2 = fs.readdirSync(SHORT_TERM_DIR).filter(f => f.startsWith('session-'));
  134 + assert(sessions2.length === 2, `short-term 有 ${sessions2.length} 个 session 文件 (期望 2)`);
  135 +
  136 + // 验证 memory.md 条目增长
  137 + const memContent2 = fs.readFileSync(MEMORY_MD, 'utf-8');
  138 + const factCount = (memContent2.match(/- \[202/g) || []).length;
  139 + assert(factCount >= 2, `memory.md 有 ${factCount} 条事实 (期望 ≥2)`);
  140 +
  141 + // 检查最后的 messages.log 是否表明 Agent "记住了"
  142 + try {
  143 + const logPath = path.join(AGENT_DIR, 'conversations', convId2, 'messages.log');
  144 + const log2 = fs.readFileSync(logPath, 'utf-8');
  145 + const mentionsBLM = log2.includes('BLM') || log2.includes('记忆') || log2.includes('之前');
  146 + assert(mentionsBLM, `Agent 在第二次对话中引用了之前的信息`);
  147 + } catch { console.log(' ⚠️ 无法检查 messages.log'); }
  148 +
  149 + // ── 结果 ──
  150 + console.log('\n' + '═'.repeat(50));
  151 + console.log(`\n🏁 测试结果: ${passed} 通过, ${failed} 失败\n`);
  152 +
  153 +
  154 + if (failed > 0) process.exit(1);
  155 +}
  156 +
  157 +main().catch(err => { console.error('❌ 测试崩溃:', err); process.exit(1); });
server/services/memory-service.js
@@ -6,9 +6,10 @@ @@ -6,9 +6,10 @@
6 * - 对话结束 → 关键事实追加 → memory.md 6 * - 对话结束 → 关键事实追加 → memory.md
7 * - 新对话开始 → 加载最近 N 个 session 摘要注入 prompt 7 * - 新对话开始 → 加载最近 N 个 session 摘要注入 prompt
8 * 8 *
9 - * Phase 2 (TODO):  
10 - * - 海马体关键词匹配  
11 - * - 小脑方案固化 9 + * Phase 2:
  10 + * - 海马体关键词匹配 → 检索相关小脑方案
  11 + * - 小脑方案固化 → cerebellum/ 存储成功方案
  12 + * - 海马体索引自动更新
12 */ 13 */
13 14
14 const fs = require('fs'); 15 const fs = require('fs');
@@ -18,6 +19,8 @@ const DATA_DIR = path.resolve(__dirname, '../../data/agents'); @@ -18,6 +19,8 @@ const DATA_DIR = path.resolve(__dirname, '../../data/agents');
18 const MAX_SHORT_TERM = 10; // 最多保留最近 N 个 session 摘要 19 const MAX_SHORT_TERM = 10; // 最多保留最近 N 个 session 摘要
19 const MAX_MEMORY_MD_SIZE = 3000; // memory.md 最大字符数 20 const MAX_MEMORY_MD_SIZE = 3000; // memory.md 最大字符数
20 const INJECT_SESSION_COUNT = 5; // prompt 注入最近 N 个 session 21 const INJECT_SESSION_COUNT = 5; // prompt 注入最近 N 个 session
  22 +const HIPPOCAMPUS_MIN_SCORE = 0.3; // 海马体匹配最低分
  23 +const MAX_CEREBELLUM_INJECT = 2; // 最多注入 N 个小脑方案
21 24
22 // ════════════════════════════════════════ 25 // ════════════════════════════════════════
23 // 1. 对话结束 → 自动保存 Session 摘要 26 // 1. 对话结束 → 自动保存 Session 摘要
@@ -190,7 +193,295 @@ function appendToMemoryMd(agentId, task, result, meta) { @@ -190,7 +193,295 @@ function appendToMemoryMd(agentId, task, result, meta) {
190 } 193 }
191 194
192 // ════════════════════════════════════════ 195 // ════════════════════════════════════════
193 -// 内部工具函数 196 +// 4. Phase 2: 海马体检索 (Hippocampus)
  197 +// ════════════════════════════════════════
  198 +
  199 +/**
  200 + * 根据任务文本检索海马体,返回匹配的小脑方案
  201 + *
  202 + * @param {string} agentId
  203 + * @param {string} taskText - 用户的任务文本
  204 + * @returns {string} 格式化的小脑方案文本(用于注入 prompt)
  205 + */
  206 +function retrieveRelevantPlans(agentId, taskText) {
  207 + try {
  208 + const agentDir = path.join(DATA_DIR, agentId);
  209 + const index = loadHippocampusIndex(agentDir);
  210 + if (index.length === 0) return '';
  211 +
  212 + // 从任务文本提取关键词
  213 + const taskKeywords = extractKeywords(taskText);
  214 + if (taskKeywords.length === 0) return '';
  215 +
  216 + // 计算每个索引条目的匹配分数
  217 + const scored = index.map(entry => {
  218 + const entryKeywords = (entry.keywords || []).map(k => k.toLowerCase());
  219 + const hits = taskKeywords.filter(kw =>
  220 + entryKeywords.some(ek => ek.includes(kw) || kw.includes(ek))
  221 + );
  222 + const score = entryKeywords.length > 0
  223 + ? hits.length / entryKeywords.length // 命中了 entry 的多少比例关键词
  224 + : 0;
  225 + return { ...entry, score, hits };
  226 + });
  227 +
  228 + // 过滤 + 排序
  229 + const matches = scored
  230 + .filter(e => e.score >= HIPPOCAMPUS_MIN_SCORE)
  231 + .sort((a, b) => {
  232 + // 先按分数,再按置信度,最后按使用次数
  233 + if (b.score !== a.score) return b.score - a.score;
  234 + if ((b.confidence || 0) !== (a.confidence || 0)) return (b.confidence || 0) - (a.confidence || 0);
  235 + return (b.useCount || 0) - (a.useCount || 0);
  236 + })
  237 + .slice(0, MAX_CEREBELLUM_INJECT);
  238 +
  239 + if (matches.length === 0) return '';
  240 +
  241 + // 加载对应的小脑方案文件
  242 + const cerebellumDir = path.join(agentDir, 'memory', 'cerebellum');
  243 + const plans = [];
  244 +
  245 + for (const match of matches) {
  246 + const planPath = path.join(cerebellumDir, match.planFile);
  247 + if (fs.existsSync(planPath)) {
  248 + const planContent = fs.readFileSync(planPath, 'utf-8').trim();
  249 + plans.push(`### 经验方案: ${match.scenario} (置信度 ${Math.round((match.confidence || 0) * 100)}%, 使用 ${match.useCount || 0} 次)\n\n${planContent}`);
  250 +
  251 + // 更新原始索引中的使用次数 + 最后使用时间
  252 + const origEntry = index.find(e => e.planFile === match.planFile);
  253 + if (origEntry) {
  254 + origEntry.useCount = (origEntry.useCount || 0) + 1;
  255 + origEntry.lastUsed = new Date().toISOString().substring(0, 10);
  256 + }
  257 + }
  258 + }
  259 +
  260 + if (plans.length > 0) {
  261 + // 保存更新后的使用统计
  262 + saveHippocampusIndex(agentDir, index);
  263 + console.log(`[Memory] 🐴 海马体命中 ${plans.length} 个方案 (agent: ${agentId})`);
  264 + }
  265 +
  266 + if (plans.length === 0) return '';
  267 +
  268 + return [
  269 + `# 经验参考(海马体检索)`,
  270 + '',
  271 + '> 以下是你过去处理类似任务的成功经验。可以参考,但不必完全照搬。',
  272 + '',
  273 + ...plans,
  274 + ].join('\n');
  275 + } catch (err) {
  276 + console.error(`[Memory] ⚠️ 海马体检索失败:`, err.message);
  277 + return '';
  278 + }
  279 +}
  280 +
  281 +// ════════════════════════════════════════
  282 +// 5. Phase 2: 小脑固化 (Cerebellum)
  283 +// ════════════════════════════════════════
  284 +
  285 +/**
  286 + * 保存一个成功方案到小脑,并更新海马体索引
  287 + *
  288 + * @param {string} agentId
  289 + * @param {object} plan - { scenario, keywords, steps, notes }
  290 + * @returns {string|null} 保存成功返回文件路径
  291 + */
  292 +function saveCerebellumPlan(agentId, plan) {
  293 + try {
  294 + const agentDir = path.join(DATA_DIR, agentId);
  295 + const cerebellumDir = path.join(agentDir, 'memory', 'cerebellum');
  296 +
  297 + if (!fs.existsSync(cerebellumDir)) {
  298 + fs.mkdirSync(cerebellumDir, { recursive: true });
  299 + }
  300 +
  301 + // 生成文件名: 基于 scenario 的 slug
  302 + const slug = (plan.scenario || 'plan')
  303 + .toLowerCase()
  304 + .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
  305 + .replace(/^-|-$/g, '')
  306 + .substring(0, 40);
  307 + const filename = `${slug}.md`;
  308 + const filepath = path.join(cerebellumDir, filename);
  309 +
  310 + // 构造方案内容
  311 + const content = [
  312 + `# 方案: ${plan.scenario}`,
  313 + '',
  314 + `> 创建时间: ${new Date().toISOString().substring(0, 10)}`,
  315 + '',
  316 + ];
  317 +
  318 + if (plan.conditions) {
  319 + content.push('## 适用条件', ...plan.conditions.map(c => `- ${c}`), '');
  320 + }
  321 +
  322 + if (plan.steps) {
  323 + content.push('## 执行步骤', ...plan.steps.map((s, i) => `${i + 1}. ${s}`), '');
  324 + }
  325 +
  326 + if (plan.notes) {
  327 + content.push('## 注意事项', ...plan.notes.map(n => `- ${n}`), '');
  328 + }
  329 +
  330 + if (plan.rawContent) {
  331 + content.push('## 详细内容', plan.rawContent, '');
  332 + }
  333 +
  334 + fs.writeFileSync(filepath, content.join('\n'), 'utf-8');
  335 +
  336 + // 更新海马体索引
  337 + const index = loadHippocampusIndex(agentDir);
  338 +
  339 + // 检查是否已有同名条目
  340 + const existing = index.findIndex(e => e.planFile === filename);
  341 + const entry = {
  342 + scenario: plan.scenario,
  343 + keywords: plan.keywords || [],
  344 + planFile: filename,
  345 + confidence: plan.confidence || 0.7,
  346 + useCount: 0,
  347 + lastUsed: null,
  348 + createdAt: new Date().toISOString().substring(0, 10),
  349 + };
  350 +
  351 + if (existing >= 0) {
  352 + // 更新已有条目(保留使用次数)
  353 + entry.useCount = index[existing].useCount || 0;
  354 + entry.lastUsed = index[existing].lastUsed;
  355 + index[existing] = entry;
  356 + } else {
  357 + index.push(entry);
  358 + }
  359 +
  360 + saveHippocampusIndex(agentDir, index);
  361 +
  362 + // 同步更新 hippocampus.md (人类可读版)
  363 + syncHippocampusMd(agentDir, index);
  364 +
  365 + console.log(`[Memory] 🧩 小脑方案已固化: ${agentId}/cerebellum/${filename}`);
  366 + return filepath;
  367 + } catch (err) {
  368 + console.error(`[Memory] ⚠️ 小脑方案保存失败:`, err.message);
  369 + return null;
  370 + }
  371 +}
  372 +
  373 +// ════════════════════════════════════════
  374 +// 海马体索引 JSON 读写
  375 +// ════════════════════════════════════════
  376 +
  377 +/**
  378 + * 加载海马体索引 (JSON 格式,比 md 表格更可靠)
  379 + * 文件: memory/hippocampus-index.json
  380 + */
  381 +function loadHippocampusIndex(agentDir) {
  382 + const indexPath = path.join(agentDir, 'memory', 'hippocampus-index.json');
  383 + try {
  384 + return JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
  385 + } catch {
  386 + return [];
  387 + }
  388 +}
  389 +
  390 +/** 保存海马体索引 */
  391 +function saveHippocampusIndex(agentDir, index) {
  392 + const indexPath = path.join(agentDir, 'memory', 'hippocampus-index.json');
  393 + const dir = path.dirname(indexPath);
  394 + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  395 + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
  396 +}
  397 +
  398 +/**
  399 + * 同步 hippocampus.md — 人类可读的 markdown 表格
  400 + * (JSON 是程序真正使用的,md 只是给人看)
  401 + */
  402 +function syncHippocampusMd(agentDir, index) {
  403 + const mdPath = path.join(agentDir, 'memory', 'hippocampus.md');
  404 + const rows = index.map((e, i) =>
  405 + `| ${i + 1} | ${e.scenario} | ${(e.keywords || []).join(', ')} | ${e.planFile} | ${e.confidence} | ${e.useCount || 0} | ${e.lastUsed || '-'} |`
  406 + );
  407 +
  408 + const content = [
  409 + '# 海马体 — 经验记忆索引',
  410 + '',
  411 + '> 记录过往处理过的情景模式。遇到相似任务时,优先调用小脑中的固化方案。',
  412 + '> ⚠️ 此文件由系统自动生成,勿手动编辑。真实数据在 hippocampus-index.json。',
  413 + '',
  414 + '| # | 情景模式 | 匹配关键词 | 方案文件 | 置信度 | 使用次数 | 上次使用 |',
  415 + '|---|---------|-----------|---------|--------|---------|----------|',
  416 + ...(rows.length > 0 ? rows : ['| | (暂无经验记录) | | | | | |']),
  417 + '',
  418 + ].join('\n');
  419 +
  420 + fs.writeFileSync(mdPath, content, 'utf-8');
  421 +}
  422 +
  423 +// ════════════════════════════════════════
  424 +// 关键词提取
  425 +// ════════════════════════════════════════
  426 +
  427 +/** 中文停用词表(常见无意义词) */
  428 +const STOP_WORDS = new Set([
  429 + '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个',
  430 + '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好',
  431 + '自己', '这', '他', '她', '它', '那', '些', '所以', '因为', '但是', '而且', '或者',
  432 + '如果', '虽然', '所有', '可以', '应该', '需要', '能够', '已经', '正在', '请',
  433 + '把', '被', '让', '给', '从', '对', '将', '于', '之', '等', '以', '及', '与',
  434 + 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
  435 + 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
  436 + 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
  437 + 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'about', 'and', 'or',
  438 + 'but', 'not', 'this', 'that', 'it', 'its', 'my', 'your', 'his', 'her',
  439 +]);
  440 +
  441 +/**
  442 + * 从文本中提取关键词
  443 + * 策略: 中文按 2-4 字切片 + 英文按空格分词 + 去停用词
  444 + */
  445 +function extractKeywords(text) {
  446 + if (!text || text.length < 2) return [];
  447 +
  448 + const keywords = new Set();
  449 +
  450 + // 1. 英文词 (≥2字母)
  451 + const englishWords = text.match(/[a-zA-Z]{2,}/g) || [];
  452 + englishWords.forEach(w => {
  453 + const lower = w.toLowerCase();
  454 + if (!STOP_WORDS.has(lower) && lower.length >= 3) {
  455 + keywords.add(lower);
  456 + }
  457 + });
  458 +
  459 + // 2. 中文:提取连续中文字符序列,再分割成 2-4 字的 ngram
  460 + const chineseSegments = text.match(/[\u4e00-\u9fff]{2,}/g) || [];
  461 + for (const seg of chineseSegments) {
  462 + // 完整段 (如果 ≤6 字)
  463 + if (seg.length <= 6 && !STOP_WORDS.has(seg)) {
  464 + keywords.add(seg);
  465 + }
  466 + // 2-gram
  467 + for (let i = 0; i <= seg.length - 2; i++) {
  468 + const bi = seg.substring(i, i + 2);
  469 + if (!STOP_WORDS.has(bi)) keywords.add(bi);
  470 + }
  471 + // 3-gram (如果段够长)
  472 + if (seg.length >= 3) {
  473 + for (let i = 0; i <= seg.length - 3; i++) {
  474 + const tri = seg.substring(i, i + 3);
  475 + if (!STOP_WORDS.has(tri)) keywords.add(tri);
  476 + }
  477 + }
  478 + }
  479 +
  480 + return [...keywords].slice(0, 30); // 最多 30 个关键词
  481 +}
  482 +
  483 +// ════════════════════════════════════════
  484 +// Phase 1 内部工具函数
194 // ════════════════════════════════════════ 485 // ════════════════════════════════════════
195 486
196 /** 从 title 提取任务描述 */ 487 /** 从 title 提取任务描述 */
@@ -314,7 +605,15 @@ function truncateMemory(content) { @@ -314,7 +605,15 @@ function truncateMemory(content) {
314 } 605 }
315 606
316 module.exports = { 607 module.exports = {
  608 + // Phase 1
317 saveSessionSummary, 609 saveSessionSummary,
318 loadRecentMemory, 610 loadRecentMemory,
319 appendToMemoryMd, 611 appendToMemoryMd,
  612 + // Phase 2
  613 + retrieveRelevantPlans,
  614 + saveCerebellumPlan,
  615 + extractKeywords,
  616 + // 内部 (测试用)
  617 + loadHippocampusIndex,
  618 + saveHippocampusIndex,
320 }; 619 };