#!/usr/bin/env node /** * Memory System — 完整端到端测试 * * 用真实 Agent 对话验证三个 Phase 的完整效果: * * Phase 1 验证: * ① 对话完成 → short-term/ 自动出现 session 文件 * ② memory.md 自动追加关键事实 * ③ 第二次对话 → Agent 能引用第一次的内容(近期记忆注入) * * Phase 2 验证: * ④ 保存小脑方案 → hippocampus-index.json 更新 * ⑤ 发送匹配任务 → 海马体命中方案 → prompt 体积增大 * ⑥ 使用次数自动递增 * * Phase 3 验证: * ⑦ 复杂任务(多 tool call)→ 自动触发反思 → reflections/ 出现日志 * ⑧ memory.md "已验证模式" / "失败教训" 段被更新 * ⑨ 过期 short-term 合并 → long-term/ 出现合并记录 * ⑩ 日报 API → daily/{today}.md 生成 */ const http = require('http'); const fs = require('fs'); const path = require('path'); const BASE = 'http://localhost:3000'; const AGENT = 'poet'; // 用 poet 做测试(轻量级、不影响 ceo 数据) const DATA_DIR = path.resolve(__dirname, '../../data/agents'); const AGENT_DIR = path.join(DATA_DIR, AGENT); const MEM = path.join(AGENT_DIR, 'memory'); let passed = 0, failed = 0, total = 0; function test(condition, msg) { total++; if (condition) { passed++; console.log(` ✅ ${msg}`); } else { failed++; console.log(` ❌ FAIL: ${msg}`); } } // ── HTTP 工具 ── function post(urlStr, body) { return new Promise((resolve, reject) => { const data = JSON.stringify(body); const u = new URL(urlStr); const req = http.request({ hostname: u.hostname, port: u.port, path: u.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } }, res => { let buf = ''; res.on('data', c => buf += c); res.on('end', () => { try { resolve(JSON.parse(buf)); } catch { resolve(buf); } }); }); req.on('error', reject); req.write(data); req.end(); }); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function waitForStatus(agentId, convId, timeoutMs = 120000) { const start = Date.now(); return new Promise((resolve, reject) => { const check = () => { if (Date.now() - start > timeoutMs) return reject(new Error(`Timeout waiting for ${convId}`)); try { const metaPath = path.join(DATA_DIR, agentId, 'conversations', convId, 'meta.json'); const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); if (meta.status === 'completed' || meta.status === 'failed') return resolve(meta); } catch {} setTimeout(check, 2000); }; check(); }); } function readFile(p) { try { return fs.readFileSync(p, 'utf-8'); } catch { return ''; } } function listDir(p) { try { return fs.readdirSync(p); } catch { return []; } } // ════════════════════════════════════════ // Clean Slate // ════════════════════════════════════════ function cleanMemory() { const dirs = ['short-term', 'long-term', 'reflections', 'cerebellum', 'daily']; for (const d of dirs) { const dir = path.join(MEM, d); if (fs.existsSync(dir)) { for (const f of fs.readdirSync(dir)) { if (f === '.' || f === '..') continue; try { fs.unlinkSync(path.join(dir, f)); } catch {} } } } // 重置 memory.md fs.writeFileSync(path.join(MEM, 'memory.md'), '# 我的记忆\n\n## 关键事实\n(尚无记录)\n\n## 已验证的模式\n(尚无记录)\n\n## 失败教训\n(尚无记录)\n', 'utf-8'); // 清 hippocampus const hippIdx = path.join(MEM, 'hippocampus-index.json'); if (fs.existsSync(hippIdx)) fs.unlinkSync(hippIdx); } // ════════════════════════════════════════ // Main Test // ════════════════════════════════════════ async function main() { console.log('\n🧠 Memory System — 端到端集成测试'); console.log('═'.repeat(56)); console.log(` Agent: ${AGENT}`); console.log(` Server: ${BASE}`); console.log('═'.repeat(56)); // ── 0. Clean ── console.log('\n🗑️ Step 0: 清理历史数据'); cleanMemory(); console.log(' Done.\n'); // ═══════════════════════════════════ // PHASE 1: 基础记忆读写 // ═══════════════════════════════════ console.log('┌──────────────────────────────────┐'); console.log('│ PHASE 1: 基础记忆读写 │'); console.log('└──────────────────────────────────┘'); // ── 1a. 第一次对话 ── console.log('\n📋 1a. 发送第一次对话:让 Poet 写一首关于春天的五言绝句'); const r1 = await post(`${BASE}/api/agent/task/dispatch`, { from: 'master', to: AGENT, task: '请写一首关于春天的五言绝句,主题是踏青。不需要使用任何工具,直接创作即可。', priority: 'P2', autoExecute: true, }); test(r1.success, `Dispatch 成功 (taskId: ${r1.taskId})`); const conv1 = r1.executionConvId; console.log(' ⏳ 等待对话完成...'); const meta1 = await waitForStatus(AGENT, conv1); test(meta1.status === 'completed', `对话完成 (toolCalls: ${meta1.stats?.toolCallCount})`); await sleep(2000); // ① session 文件 const sessions1 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-')); test(sessions1.length >= 1, `① short-term/ 有 ${sessions1.length} 个 session 文件`); // ② memory.md 更新 const mem1 = readFile(path.join(MEM, 'memory.md')); const hasFact = mem1.includes('[2026-') || !mem1.includes('(尚无记录)'); test(hasFact, `② memory.md 已更新关键事实`); // ── 1b. 第二次对话(验证近期记忆注入) ── console.log('\n📋 1b. 发送第二次对话:要求 Agent 回忆之前写了什么'); const r2 = await post(`${BASE}/api/agent/task/dispatch`, { from: 'master', to: AGENT, task: '你还记得刚才写了什么内容吗?如果你的近期记忆中有相关信息,请引用它回答。不需要使用工具。', priority: 'P2', autoExecute: true, }); test(r2.success, `Dispatch 成功 (taskId: ${r2.taskId})`); const conv2 = r2.executionConvId; console.log(' ⏳ 等待对话完成...'); const meta2 = await waitForStatus(AGENT, conv2); test(meta2.status === 'completed', `对话完成`); await sleep(2000); // ③ 验证 Agent 引用了之前的内容 const log2Path = path.join(AGENT_DIR, 'conversations', conv2, 'messages.log'); const log2 = readFile(log2Path); const remembered = log2.includes('春') || log2.includes('踏青') || log2.includes('诗') || log2.includes('记忆'); test(remembered, `③ Agent 在第二次对话中引用了之前的春天/踏青内容`); const sessions2 = listDir(path.join(MEM, 'short-term')).filter(f => f.startsWith('session-')); test(sessions2.length >= 2, `short-term/ 现在有 ${sessions2.length} 个 session 文件`); // ═══════════════════════════════════ // PHASE 2: 海马体检索 + 小脑 // ═══════════════════════════════════ console.log('\n┌──────────────────────────────────┐'); console.log('│ PHASE 2: 海马体检索 + 小脑固化 │'); console.log('└──────────────────────────────────┘'); // ④ 保存小脑方案 console.log('\n📋 2a. 保存一个"写诗"小脑方案'); const planRes = await post(`${BASE}/api/agent/${AGENT}/memory/cerebellum`, { scenario: '写古典诗词', keywords: ['写诗', '诗词', '古诗', '五言', '七言', '绝句', '律诗', '创作'], conditions: ['需要创作古典诗词', '需要遵循格律'], steps: [ '确认体裁(五绝/七绝/五律/七律/词)', '确认主题和意境', '起句: 点明时间/地点/情境', '承句: 展开描写,丰富画面', '转句: 转折或深化情感', '合句: 收束全诗,升华主题', ], notes: ['五绝20字,七绝28字', '注意平仄和押韵', '避免直白说理,用意象表达'], confidence: 0.9, }); test(planRes.success, `④ 小脑方案保存成功`); // 验证 hippocampus 索引 const hippIdx = readFile(path.join(MEM, 'hippocampus-index.json')); test(hippIdx.includes('写古典诗词'), `hippocampus-index.json 包含新条目`); // ⑤ 发送匹配任务 → 验证海马体命中 console.log('\n📋 2b. 发送"写诗"任务 → 验证海马体检索'); const r3 = await post(`${BASE}/api/agent/task/dispatch`, { from: 'master', to: AGENT, task: '请写一首七言绝句,主题是中秋月夜。不需要使用工具,直接创作。', priority: 'P2', autoExecute: true, }); test(r3.success, `Dispatch 成功 (taskId: ${r3.taskId})`); const conv3 = r3.executionConvId; console.log(' ⏳ 等待对话完成...'); await waitForStatus(AGENT, conv3); await sleep(2000); // ⑥ 验证使用次数递增 // 海马体 useCount 在 buildSystemPrompt 时更新,需等服务端写完文件 await sleep(5000); const hippRaw = readFile(path.join(MEM, 'hippocampus-index.json')); console.log(` 🔍 hippocampus raw: ${hippRaw.substring(0, 200)}`); const hippIdxAfter = JSON.parse(hippRaw || '[]'); const poetryEntry = hippIdxAfter.find(e => e.scenario === '写古典诗词'); console.log(` 🔍 poetryEntry: ${JSON.stringify(poetryEntry)}`); test(poetryEntry && poetryEntry.useCount >= 1, `⑥ 海马体使用次数: ${poetryEntry?.useCount} (期望 ≥1)`); test(poetryEntry && poetryEntry.lastUsed !== null, `lastUsed 已更新: ${poetryEntry?.lastUsed}`); // ═══════════════════════════════════ // PHASE 3: 反思 + 合并 + 日报 // ═══════════════════════════════════ console.log('\n┌──────────────────────────────────┐'); console.log('│ PHASE 3: 反思 + 合并 + 日报 │'); console.log('└──────────────────────────────────┘'); // ⑦ 模拟复杂对话触发反思 console.log('\n📋 3a. 模拟复杂对话 → 验证自动反思'); const { autoReflect } = require('../../server/services/memory-service'); const complexMeta = { status: 'completed', stats: { toolCallCount: 8, messageCount: 4 }, createdAt: new Date(Date.now() - 180000).toISOString(), updatedAt: new Date().toISOString(), title: '为国庆节创作一组主题诗集,包含5首诗', }; const complexOutput = `创作完成!以下是创作步骤: 1. 确定国庆主题和5个子题材(国旗、山河、英雄、和平、未来) 2. 分别确定每首诗的体裁(五绝、七绝、五律、七律、词) 3. 创作第一首《旗颂》五言绝句 4. 创作第二首《山河赋》七言律诗 5. 创作第三首《英雄辞》五言律诗 6. 创作第四首《和平吟》七言绝句 7. 创作第五首《展望》词·水调歌头 8. 整体校对格律和意境 产出文件: - workspace/poet/guoqing-collection.md - workspace/poet/guoqing-notes.md`; const reflectResult = autoReflect(AGENT, 'test-complex', complexMeta, complexOutput); test(reflectResult !== null, `⑦ 复杂对话触发了反思`); test(reflectResult?.patterns?.length >= 1, `提取了 ${reflectResult?.patterns?.length} 个模式`); test(reflectResult?.plan !== null, `自动固化了方案`); // 验证 reflections 目录 const reflectFiles = listDir(path.join(MEM, 'reflections')).filter(f => f.startsWith('reflect-')); test(reflectFiles.length >= 1, `reflections/ 有 ${reflectFiles.length} 个日志`); // ⑧ memory.md 更新 const mem3 = readFile(path.join(MEM, 'memory.md')); test(!mem3.includes('已验证的模式\n(尚无记录)'), `⑧ memory.md "已验证模式" 段有内容`); // 模拟失败对话 console.log('\n📋 3b. 模拟失败对话 → 验证教训提取'); const failReflect = autoReflect(AGENT, 'test-fail', { status: 'failed', stats: { toolCallCount: 4, messageCount: 2 }, createdAt: new Date(Date.now() - 60000).toISOString(), updatedAt: new Date().toISOString(), title: '翻译古诗为英文但格式错误', }, 'error: 输入格式无法解析 Error: Invalid poem format at line 3'); test(failReflect?.lessons?.length >= 1, `失败对话提取了 ${failReflect?.lessons?.length} 个教训`); const mem3b = readFile(path.join(MEM, 'memory.md')); test(!mem3b.includes('失败教训\n(尚无记录)'), `memory.md "失败教训" 段有内容`); // ⑨ 记忆合并 console.log('\n📋 3c. 记忆合并 (short-term → long-term)'); // 创建 3 个"过期"session(8天前) const shortDir = path.join(MEM, 'short-term'); const oldTs = Date.now() - 8 * 24 * 60 * 60 * 1000; for (let i = 0; i < 3; i++) { fs.writeFileSync(path.join(shortDir, `session-${oldTs + i}.md`), `# Session 摘要\n\n- **时间**: 2026-03-17 0${i}:00:00\n\n## 任务\n旧任务-${i}: 写一首关于秋天的诗\n\n## 结果\n秋诗完成-${i}\n`, 'utf-8'); } const shortBefore = listDir(shortDir).filter(f => f.startsWith('session-')); console.log(` short-term/ 合并前: ${shortBefore.length} 文件`); const consolidateRes = await post(`${BASE}/api/agent/${AGENT}/memory/consolidate`, {}); test(consolidateRes.merged === 3, `⑨ 合并了 ${consolidateRes.merged} 个过期 session (期望 3)`); const ltFiles = listDir(path.join(MEM, 'long-term')); test(ltFiles.some(f => f.startsWith('consolidate-')), `long-term/ 有合并记录`); const shortAfter = listDir(shortDir).filter(f => { const ts = parseInt(f.replace('session-', '').replace('.md', '')); return !isNaN(ts) && ts < oldTs + 5; }); test(shortAfter.length === 0, `过期 session 已从 short-term/ 删除`); // ⑩ 日报 console.log('\n📋 3d. 日报生成'); const dailyRes = await post(`${BASE}/api/agent/${AGENT}/memory/daily-report`, {}); test(dailyRes.success, `⑩ 日报生成成功`); const today = new Date().toISOString().substring(0, 10); const dailyContent = readFile(path.join(MEM, 'daily', `${today}.md`)); test(dailyContent.includes('# 日报'), `daily/${today}.md 包含日报标题`); test(dailyContent.includes('对话数'), `日报包含统计数据`); test(dailyContent.includes('反思摘要') || dailyContent.includes('反思数'), `日报包含反思信息`); // ═══════════════════════════════════ // 最终汇总 // ═══════════════════════════════════ console.log('\n' + '═'.repeat(56)); console.log('\n📊 Memory 目录最终状态:'); const finalState = { 'short-term/': listDir(path.join(MEM, 'short-term')).filter(f => f.endsWith('.md')).length, 'long-term/': listDir(path.join(MEM, 'long-term')).length, 'cerebellum/': listDir(path.join(MEM, 'cerebellum')).length, 'reflections/': listDir(path.join(MEM, 'reflections')).length, 'daily/': listDir(path.join(MEM, 'daily')).filter(f => f.endsWith('.md')).length, 'memory.md': `${readFile(path.join(MEM, 'memory.md')).length} 字`, 'hippocampus': hippIdxAfter.length + ' 条索引', }; for (const [k, v] of Object.entries(finalState)) { console.log(` 📁 ${k.padEnd(18)} ${typeof v === 'number' ? v + ' 文件' : v}`); } console.log('\n' + '═'.repeat(56)); const status = failed === 0 ? '🎉 ALL PASSED' : '⚠️ SOME FAILED'; console.log(`\n${status}: ${passed}/${total} 通过, ${failed} 失败\n`); if (failed > 0) process.exit(1); } main().catch(err => { console.error('💥 测试崩溃:', err); process.exit(1); });