@yun-zero/claw-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +68 -0
- package/README.md +323 -0
- package/dist/config/llm.d.ts +13 -0
- package/dist/config/llm.d.ts.map +1 -0
- package/dist/config/llm.js +96 -0
- package/dist/config/llm.js.map +1 -0
- package/dist/config/plugin.d.ts +15 -0
- package/dist/config/plugin.d.ts.map +1 -0
- package/dist/config/plugin.js +32 -0
- package/dist/config/plugin.js.map +1 -0
- package/dist/db/entityRepository.d.ts +21 -0
- package/dist/db/entityRepository.d.ts.map +1 -0
- package/dist/db/entityRepository.js +55 -0
- package/dist/db/entityRepository.js.map +1 -0
- package/dist/db/repository.d.ts +22 -0
- package/dist/db/repository.d.ts.map +1 -0
- package/dist/db/repository.js +77 -0
- package/dist/db/repository.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/todoRepository.d.ts +26 -0
- package/dist/db/todoRepository.d.ts.map +1 -0
- package/dist/db/todoRepository.js +54 -0
- package/dist/db/todoRepository.js.map +1 -0
- package/dist/hooks/bootstrap.d.ts +3 -0
- package/dist/hooks/bootstrap.d.ts.map +1 -0
- package/dist/hooks/bootstrap.js +28 -0
- package/dist/hooks/bootstrap.js.map +1 -0
- package/dist/hooks/message.d.ts +18 -0
- package/dist/hooks/message.d.ts.map +1 -0
- package/dist/hooks/message.js +52 -0
- package/dist/hooks/message.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +360 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/plugin.d.ts +18 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +62 -0
- package/dist/plugin.js.map +1 -0
- package/dist/services/entityGraphService.d.ts +87 -0
- package/dist/services/entityGraphService.d.ts.map +1 -0
- package/dist/services/entityGraphService.js +271 -0
- package/dist/services/entityGraphService.js.map +1 -0
- package/dist/services/memory.d.ts +26 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +281 -0
- package/dist/services/memory.js.map +1 -0
- package/dist/services/memoryIndex.d.ts +34 -0
- package/dist/services/memoryIndex.d.ts.map +1 -0
- package/dist/services/memoryIndex.js +100 -0
- package/dist/services/memoryIndex.js.map +1 -0
- package/dist/services/metadataExtractor.d.ts +16 -0
- package/dist/services/metadataExtractor.d.ts.map +1 -0
- package/dist/services/metadataExtractor.js +75 -0
- package/dist/services/metadataExtractor.js.map +1 -0
- package/dist/services/retrieval.d.ts +24 -0
- package/dist/services/retrieval.d.ts.map +1 -0
- package/dist/services/retrieval.js +40 -0
- package/dist/services/retrieval.js.map +1 -0
- package/dist/services/scheduler.d.ts +122 -0
- package/dist/services/scheduler.d.ts.map +1 -0
- package/dist/services/scheduler.js +434 -0
- package/dist/services/scheduler.js.map +1 -0
- package/dist/services/summarizer.d.ts +43 -0
- package/dist/services/summarizer.d.ts.map +1 -0
- package/dist/services/summarizer.js +252 -0
- package/dist/services/summarizer.js.map +1 -0
- package/dist/services/tagService.d.ts +64 -0
- package/dist/services/tagService.d.ts.map +1 -0
- package/dist/services/tagService.js +281 -0
- package/dist/services/tagService.js.map +1 -0
- package/dist/tools/memory.d.ts +3 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +114 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/plans/2026-03-02-claw-memory-design.md +445 -0
- package/docs/plans/2026-03-02-incremental-summary-design.md +157 -0
- package/docs/plans/2026-03-02-incremental-summary-implementation.md +468 -0
- package/docs/plans/2026-03-02-memory-index-design.md +163 -0
- package/docs/plans/2026-03-02-memory-index-implementation.md +836 -0
- package/docs/plans/2026-03-02-mvp-implementation.md +1703 -0
- package/docs/plans/2026-03-02-testing-implementation.md +395 -0
- package/docs/plans/2026-03-02-testing-plan.md +93 -0
- package/docs/plans/2026-03-03-claw-memory-openclaw-plugin-design.md +285 -0
- package/docs/plans/2026-03-03-claw-memory-plugin-implementation.md +642 -0
- package/docs/plans/2026-03-03-entity-graph-design.md +121 -0
- package/docs/plans/2026-03-03-entity-graph-implementation.md +687 -0
- package/docs/plans/2026-03-03-llm-generic-config-design.md +43 -0
- package/docs/plans/2026-03-03-llm-generic-config-implementation.md +186 -0
- package/docs/plans/2026-03-03-memory-e2e-stress-test-design.md +110 -0
- package/docs/plans/2026-03-03-memory-e2e-stress-test-implementation.md +464 -0
- package/docs/plans/2026-03-03-minimax-llm-fix.md +156 -0
- package/docs/plans/2026-03-03-scheduler-design.md +165 -0
- package/docs/plans/2026-03-03-scheduler-implementation.md +777 -0
- package/docs/plans/2026-03-03-tags-visualization-design.md +73 -0
- package/docs/plans/2026-03-03-tags-visualization-implementation.md +539 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +41 -0
- package/src/config/llm.ts +129 -0
- package/src/config/plugin.ts +47 -0
- package/src/db/entityRepository.ts +80 -0
- package/src/db/repository.ts +106 -0
- package/src/db/schema.ts +121 -0
- package/src/db/todoRepository.ts +76 -0
- package/src/hooks/bootstrap.ts +36 -0
- package/src/hooks/message.ts +84 -0
- package/src/index.ts +50 -0
- package/src/plugin.ts +85 -0
- package/src/services/entityGraphService.ts +367 -0
- package/src/services/memory.ts +338 -0
- package/src/services/memoryIndex.ts +140 -0
- package/src/services/metadataExtractor.ts +89 -0
- package/src/services/retrieval.ts +71 -0
- package/src/services/scheduler.ts +529 -0
- package/src/services/summarizer.ts +318 -0
- package/src/services/tagService.ts +335 -0
- package/src/tools/memory.ts +137 -0
- package/src/types.ts +139 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
# 记忆索引与自动提取功能实现计划
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** 实现 LLM 自动提取元数据、记忆索引 API、待办事项功能,支持 OpenClaw Hook 在对话开始时获取记忆摘要
|
|
6
|
+
|
|
7
|
+
**Architecture:** 分三个阶段实现:1) LLM 自动提取元数据 2) 记忆索引 API 3) 待办事项功能。每个阶段使用 TDD,先写测试再实现。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, better-sqlite3, LLM API (OpenAI/Anthropic)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 阶段一:LLM 自动提取元数据
|
|
14
|
+
|
|
15
|
+
### Task 1: 创建 MetadataExtractor 服务
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- Create: `src/services/metadataExtractor.ts`
|
|
19
|
+
- Modify: `src/services/memory.ts`
|
|
20
|
+
- Test: `tests/services/metadataExtractor.test.ts`
|
|
21
|
+
|
|
22
|
+
**Step 1: 编写失败的测试**
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// tests/services/metadataExtractor.test.ts
|
|
26
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
27
|
+
import { MetadataExtractor } from '../../src/services/metadataExtractor.js';
|
|
28
|
+
|
|
29
|
+
describe('MetadataExtractor', () => {
|
|
30
|
+
let extractor: MetadataExtractor;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
extractor = new MetadataExtractor();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should extract metadata from content', async () => {
|
|
37
|
+
const content = '用户讨论了 React Hooks 的使用,包括 useState 和 useEffect';
|
|
38
|
+
const result = await extractor.extract(content);
|
|
39
|
+
|
|
40
|
+
expect(result).toHaveProperty('tags');
|
|
41
|
+
expect(result).toHaveProperty('keywords');
|
|
42
|
+
expect(result).toHaveProperty('subjects');
|
|
43
|
+
expect(result).toHaveProperty('importance');
|
|
44
|
+
expect(result).toHaveProperty('summary');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Step 2: 运行测试验证失败**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm test tests/services/metadataExtractor.test.ts
|
|
53
|
+
```
|
|
54
|
+
Expected: FAIL - MetadataExtractor not found
|
|
55
|
+
|
|
56
|
+
**Step 3: 最小实现**
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// src/services/metadataExtractor.ts
|
|
60
|
+
export interface ExtractedMetadata {
|
|
61
|
+
tags: string[];
|
|
62
|
+
keywords: string[];
|
|
63
|
+
subjects: string[];
|
|
64
|
+
importance: number;
|
|
65
|
+
summary: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class MetadataExtractor {
|
|
69
|
+
async extract(content: string): Promise<ExtractedMetadata> {
|
|
70
|
+
// TODO: 实现 LLM 提取逻辑
|
|
71
|
+
return {
|
|
72
|
+
tags: [],
|
|
73
|
+
keywords: [],
|
|
74
|
+
subjects: [],
|
|
75
|
+
importance: 0.5,
|
|
76
|
+
summary: ''
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Step 4: 运行测试验证通过**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm test tests/services/metadataExtractor.test.ts
|
|
86
|
+
```
|
|
87
|
+
Expected: PASS
|
|
88
|
+
|
|
89
|
+
**Step 5: 提交**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git add src/services/metadataExtractor.ts tests/services/metadataExtractor.test.ts
|
|
93
|
+
git commit -m "feat: add MetadataExtractor service stub"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### Task 2: 集成 LLM 提取逻辑
|
|
99
|
+
|
|
100
|
+
**Files:**
|
|
101
|
+
- Modify: `src/services/metadataExtractor.ts:15-60`
|
|
102
|
+
- Test: `tests/services/metadataExtractor.test.ts`
|
|
103
|
+
|
|
104
|
+
**Step 1: 更新测试添加 LLM 模拟**
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// 在 metadataExtractor.test.ts 添加
|
|
108
|
+
it('should call LLM and parse response', async () => {
|
|
109
|
+
const mockLLMResponse = {
|
|
110
|
+
tags: ['技术/前端/React'],
|
|
111
|
+
keywords: ['useState', 'useEffect'],
|
|
112
|
+
subjects: ['React Hooks'],
|
|
113
|
+
importance: 0.8,
|
|
114
|
+
summary: '讨论 React Hooks 使用'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Mock LLM call
|
|
118
|
+
vi.spyOn(extractor, 'callLLM').mockResolvedValue(mockLLMResponse);
|
|
119
|
+
|
|
120
|
+
const result = await extractor.extract('讨论 React Hooks');
|
|
121
|
+
expect(result.tags).toContain('技术/前端/React');
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Step 2: 运行测试**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm test tests/services/metadataExtractor.test.ts
|
|
129
|
+
```
|
|
130
|
+
Expected: FAIL - callLLM not implemented
|
|
131
|
+
|
|
132
|
+
**Step 3: 实现 LLM 提取**
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// src/services/metadataExtractor.ts 完整实现
|
|
136
|
+
import { generateSummaryWithLLM } from '../config/llm.js';
|
|
137
|
+
|
|
138
|
+
const EXTRACTION_PROMPT = `请从以下对话内容中提取结构化元数据:
|
|
139
|
+
|
|
140
|
+
内容:{content}
|
|
141
|
+
|
|
142
|
+
请以 JSON 格式返回:
|
|
143
|
+
{
|
|
144
|
+
"tags": ["一级分类/二级分类/三级分类"],
|
|
145
|
+
"keywords": ["关键词1", "关键词2"],
|
|
146
|
+
"subjects": ["主题1", "主题2"],
|
|
147
|
+
"importance": 0.0-1.0,
|
|
148
|
+
"summary": "一句话摘要"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
注意:
|
|
152
|
+
- tags 使用层级结构,如 "技术/前端/React"
|
|
153
|
+
- importance 表示内容重要性,0.0-1.0
|
|
154
|
+
- 只返回 JSON,不要其他内容`;
|
|
155
|
+
|
|
156
|
+
export class MetadataExtractor {
|
|
157
|
+
async extract(content: string): Promise<ExtractedMetadata> {
|
|
158
|
+
const prompt = EXTRACTION_PROMPT.replace('{content}', content);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const response = await generateSummaryWithLLM(prompt);
|
|
162
|
+
return this.parseLLMResponse(response);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn('LLM extraction failed:', error);
|
|
165
|
+
return this.fallbackExtract(content);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private parseLLMResponse(response: string): ExtractedMetadata {
|
|
170
|
+
try {
|
|
171
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
172
|
+
if (jsonMatch) {
|
|
173
|
+
return JSON.parse(jsonMatch[0]);
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
return this.fallbackExtract('');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private fallbackExtract(content: string): ExtractedMetadata {
|
|
180
|
+
return {
|
|
181
|
+
tags: [],
|
|
182
|
+
keywords: [],
|
|
183
|
+
subjects: [],
|
|
184
|
+
importance: 0.5,
|
|
185
|
+
summary: content.substring(0, 100)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Step 4: 运行测试**
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm test tests/services/metadataExtractor.test.ts
|
|
195
|
+
```
|
|
196
|
+
Expected: PASS
|
|
197
|
+
|
|
198
|
+
**Step 5: 提交**
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git add src/services/metadataExtractor.ts
|
|
202
|
+
git commit -m "feat: implement LLM metadata extraction"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### Task 3: 修改 MemoryService 集成提取
|
|
208
|
+
|
|
209
|
+
**Files:**
|
|
210
|
+
- Modify: `src/services/memory.ts:28-50`
|
|
211
|
+
- Test: `tests/services/memory.test.ts`
|
|
212
|
+
|
|
213
|
+
**Step 1: 添加集成测试**
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// tests/services/memory.test.ts 添加
|
|
217
|
+
it('should extract metadata with LLM when saving memory', async () => {
|
|
218
|
+
vi.spyOn(extractor, 'extract').mockResolvedValue({
|
|
219
|
+
tags: ['技术/前端'],
|
|
220
|
+
keywords: ['React'],
|
|
221
|
+
subjects: ['React学习'],
|
|
222
|
+
importance: 0.8,
|
|
223
|
+
summary: '测试'
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await service.saveMemory({
|
|
227
|
+
content: '讨论 React Hooks',
|
|
228
|
+
metadata: {}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result).toBeDefined();
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Step 2: 运行测试**
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm test tests/services/memory.test.ts
|
|
239
|
+
```
|
|
240
|
+
Expected: FAIL - extractor not integrated
|
|
241
|
+
|
|
242
|
+
**Step 3: 修改 MemoryService**
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// src/services/memory.ts 导入
|
|
246
|
+
import { MetadataExtractor } from './metadataExtractor.js';
|
|
247
|
+
|
|
248
|
+
export class MemoryService {
|
|
249
|
+
private extractor: MetadataExtractor;
|
|
250
|
+
|
|
251
|
+
constructor(...) {
|
|
252
|
+
// ... existing code
|
|
253
|
+
this.extractor = new MetadataExtractor();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async saveMemory(input: SaveMemoryInput): Promise<Memory> {
|
|
257
|
+
// 调用 LLM 提取元数据
|
|
258
|
+
const extracted = await this.extractor.extract(input.content);
|
|
259
|
+
|
|
260
|
+
// 合并:LLM 提取的覆盖用户传入的
|
|
261
|
+
const metadata = {
|
|
262
|
+
tags: extracted.tags,
|
|
263
|
+
keywords: extracted.keywords,
|
|
264
|
+
subjects: extracted.subjects,
|
|
265
|
+
importance: extracted.importance,
|
|
266
|
+
summary: extracted.summary
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ... 后续处理不变
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Step 4: 运行测试**
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
npm test tests/services/memory.test.ts
|
|
278
|
+
```
|
|
279
|
+
Expected: PASS
|
|
280
|
+
|
|
281
|
+
**Step 5: 提交**
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
git add src/services/memory.ts tests/services/memory.test.ts
|
|
285
|
+
git commit -m "feat: integrate LLM metadata extraction in saveMemory"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 阶段二:记忆索引 API
|
|
291
|
+
|
|
292
|
+
### Task 4: 添加 todos 表 schema
|
|
293
|
+
|
|
294
|
+
**Files:**
|
|
295
|
+
- Modify: `src/db/schema.ts:69-85`
|
|
296
|
+
- Test: `tests/db/schema.test.ts`
|
|
297
|
+
|
|
298
|
+
**Step 1: 编写失败的测试**
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// tests/db/schema.test.ts 添加
|
|
302
|
+
it('should create todos table', () => {
|
|
303
|
+
const stmt = db.prepare(`
|
|
304
|
+
SELECT name FROM sqlite_master WHERE type='table' AND name='todos'
|
|
305
|
+
`);
|
|
306
|
+
const result = stmt.get();
|
|
307
|
+
expect(result).toBeDefined();
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Step 2: 运行测试**
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
npm test tests/db/schema.test.ts
|
|
315
|
+
```
|
|
316
|
+
Expected: FAIL - todos table not found
|
|
317
|
+
|
|
318
|
+
**Step 3: 添加 schema**
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// src/db/schema.ts 添加
|
|
322
|
+
db.exec(`
|
|
323
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
324
|
+
id TEXT PRIMARY KEY,
|
|
325
|
+
content TEXT NOT NULL,
|
|
326
|
+
period TEXT NOT NULL,
|
|
327
|
+
period_date TEXT,
|
|
328
|
+
created_at TEXT NOT NULL,
|
|
329
|
+
completed_at TEXT,
|
|
330
|
+
memory_id TEXT
|
|
331
|
+
)
|
|
332
|
+
`);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Step 4: 运行测试**
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
npm test tests/db/schema.test.ts
|
|
339
|
+
```
|
|
340
|
+
Expected: PASS
|
|
341
|
+
|
|
342
|
+
**Step 5: 提交**
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
git add src/db/schema.ts
|
|
346
|
+
git commit -m "feat: add todos table schema"
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### Task 5: 创建 TodoRepository
|
|
352
|
+
|
|
353
|
+
**Files:**
|
|
354
|
+
- Create: `src/db/todoRepository.ts`
|
|
355
|
+
- Test: `tests/db/todoRepository.test.ts`
|
|
356
|
+
|
|
357
|
+
**Step 1: 编写测试**
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
// tests/db/todoRepository.test.ts
|
|
361
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
362
|
+
import Database from 'better-sqlite3';
|
|
363
|
+
import { initializeDatabase } from '../../src/db/schema.js';
|
|
364
|
+
import { TodoRepository } from '../../src/db/todoRepository.js';
|
|
365
|
+
|
|
366
|
+
describe('TodoRepository', () => {
|
|
367
|
+
let db: Database.Database;
|
|
368
|
+
let repo: TodoRepository;
|
|
369
|
+
|
|
370
|
+
beforeEach(() => {
|
|
371
|
+
db = new Database(':memory:');
|
|
372
|
+
initializeDatabase(db);
|
|
373
|
+
repo = new TodoRepository(db);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should create a todo', () => {
|
|
377
|
+
const todo = repo.create({
|
|
378
|
+
content: '完成某事',
|
|
379
|
+
period: 'week',
|
|
380
|
+
periodDate: '2026-03-02'
|
|
381
|
+
});
|
|
382
|
+
expect(todo.id).toBeDefined();
|
|
383
|
+
expect(todo.content).toBe('完成某事');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should list todos by period', () => {
|
|
387
|
+
repo.create({ content: '任务1', period: 'week', periodDate: '2026-03-02' });
|
|
388
|
+
repo.create({ content: '任务2', period: 'week', periodDate: '2026-03-02' });
|
|
389
|
+
|
|
390
|
+
const todos = repo.findByPeriod('week', '2026-03-02');
|
|
391
|
+
expect(todos.length).toBe(2);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should mark todo as completed', () => {
|
|
395
|
+
const todo = repo.create({ content: '任务', period: 'day', periodDate: '2026-03-02' });
|
|
396
|
+
repo.markCompleted(todo.id);
|
|
397
|
+
|
|
398
|
+
const updated = repo.findById(todo.id);
|
|
399
|
+
expect(updated.completedAt).toBeDefined();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Step 2: 运行测试**
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
npm test tests/db/todoRepository.test.ts
|
|
408
|
+
```
|
|
409
|
+
Expected: FAIL - TodoRepository not found
|
|
410
|
+
|
|
411
|
+
**Step 3: 实现 TodoRepository**
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// src/db/todoRepository.ts
|
|
415
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
416
|
+
|
|
417
|
+
export interface Todo {
|
|
418
|
+
id: string;
|
|
419
|
+
content: string;
|
|
420
|
+
period: 'day' | 'week' | 'month';
|
|
421
|
+
periodDate: string;
|
|
422
|
+
createdAt: Date;
|
|
423
|
+
completedAt: Date | null;
|
|
424
|
+
memoryId: string | null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface CreateTodoInput {
|
|
428
|
+
content: string;
|
|
429
|
+
period: 'day' | 'week' | 'month';
|
|
430
|
+
periodDate: string;
|
|
431
|
+
memoryId?: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export class TodoRepository {
|
|
435
|
+
constructor(private db: Database.Database) {}
|
|
436
|
+
|
|
437
|
+
create(input: CreateTodoInput): Todo {
|
|
438
|
+
const id = uuidv4();
|
|
439
|
+
const createdAt = new Date().toISOString();
|
|
440
|
+
|
|
441
|
+
this.db.prepare(`
|
|
442
|
+
INSERT INTO todos (id, content, period, period_date, created_at, memory_id)
|
|
443
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
444
|
+
`).run(id, input.content, input.period, input.periodDate, createdAt, input.memoryId || null);
|
|
445
|
+
|
|
446
|
+
return { id, content: input.content, period: input.period, periodDate: input.periodDate, createdAt: new Date(createdAt), completedAt: null, memoryId: input.memoryId || null };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
findByPeriod(period: string, periodDate: string): Todo[] {
|
|
450
|
+
return this.db.prepare(`
|
|
451
|
+
SELECT * FROM todos WHERE period = ? AND period_date = ?
|
|
452
|
+
ORDER BY created_at DESC
|
|
453
|
+
`).all(period, periodDate) as Todo[];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
findById(id: string): Todo | null {
|
|
457
|
+
return this.db.prepare('SELECT * FROM todos WHERE id = ?').get(id) as Todo | null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
markCompleted(id: string): void {
|
|
461
|
+
this.db.prepare(`
|
|
462
|
+
UPDATE todos SET completed_at = ? WHERE id = ?
|
|
463
|
+
`).run(new Date().toISOString(), id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Step 4: 运行测试**
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
npm test tests/db/todoRepository.test.ts
|
|
472
|
+
```
|
|
473
|
+
Expected: PASS
|
|
474
|
+
|
|
475
|
+
**Step 5: 提交**
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
git add src/db/todoRepository.ts tests/db/todoRepository.test.ts
|
|
479
|
+
git commit -m "feat: add TodoRepository for todo management"
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
### Task 6: 创建 get_memory_index MCP 工具
|
|
485
|
+
|
|
486
|
+
**Files:**
|
|
487
|
+
- Modify: `src/mcp/tools.ts`
|
|
488
|
+
- Create: `src/services/memoryIndex.ts`
|
|
489
|
+
- Test: `tests/mcp/memoryIndex.test.ts`
|
|
490
|
+
|
|
491
|
+
**Step 1: 编写测试**
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// tests/mcp/memoryIndex.test.ts
|
|
495
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
496
|
+
import Database from 'better-sqlite3';
|
|
497
|
+
import { initializeDatabase } from '../../src/db/schema.js';
|
|
498
|
+
import { getMemoryIndex } from '../../src/services/memoryIndex.js';
|
|
499
|
+
|
|
500
|
+
describe('getMemoryIndex', () => {
|
|
501
|
+
let db: Database.Database;
|
|
502
|
+
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
db = new Database(':memory:');
|
|
505
|
+
initializeDatabase(db);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should return memory index with all sections', async () => {
|
|
509
|
+
// 创建测试数据...
|
|
510
|
+
|
|
511
|
+
const result = await getMemoryIndex(db, {
|
|
512
|
+
period: 'week',
|
|
513
|
+
includeTodos: true,
|
|
514
|
+
includeRecent: true
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(result).toHaveProperty('period');
|
|
518
|
+
expect(result).toHaveProperty('activeAreas');
|
|
519
|
+
expect(result).toHaveProperty('todos');
|
|
520
|
+
expect(result).toHaveProperty('recentActivity');
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Step 2: 运行测试**
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
npm test tests/mcp/memoryIndex.test.ts
|
|
529
|
+
```
|
|
530
|
+
Expected: FAIL - getMemoryIndex not found
|
|
531
|
+
|
|
532
|
+
**Step 3: 实现 memoryIndex 服务**
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// src/services/memoryIndex.ts
|
|
536
|
+
import { TodoRepository } from '../db/todoRepository.js';
|
|
537
|
+
import { MemoryRepository } from '../db/repository.js';
|
|
538
|
+
import { EntityRepository } from '../db/entityRepository.js';
|
|
539
|
+
|
|
540
|
+
export interface MemoryIndexOptions {
|
|
541
|
+
period: 'day' | 'week' | 'month';
|
|
542
|
+
date?: string;
|
|
543
|
+
includeTodos?: boolean;
|
|
544
|
+
includeRecent?: boolean;
|
|
545
|
+
recentLimit?: number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export interface MemoryIndex {
|
|
549
|
+
period: { start: string; end: string };
|
|
550
|
+
activeAreas: {
|
|
551
|
+
tags: { name: string; count: number }[];
|
|
552
|
+
keywords: string[];
|
|
553
|
+
};
|
|
554
|
+
todos: { id: string; content: string; period: string }[];
|
|
555
|
+
recentActivity: { date: string; summary: string }[];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function getMemoryIndex(db: Database.Database, options: MemoryIndexOptions): Promise<MemoryIndex> {
|
|
559
|
+
const { startDate, endDate } = calculatePeriodRange(options.period, options.date);
|
|
560
|
+
|
|
561
|
+
// 获取活跃领域
|
|
562
|
+
const tagRepo = new EntityRepository(db);
|
|
563
|
+
const tags = getTopTags(db, startDate, endDate, 10);
|
|
564
|
+
const keywords = getTopKeywords(db, startDate, endDate, 10);
|
|
565
|
+
|
|
566
|
+
// 获取待办
|
|
567
|
+
const todoRepo = new TodoRepository(db);
|
|
568
|
+
const todos = options.includeTodos
|
|
569
|
+
? todoRepo.findByPeriod(options.period, endDate).filter(t => !t.completedAt)
|
|
570
|
+
: [];
|
|
571
|
+
|
|
572
|
+
// 获取最近动态
|
|
573
|
+
const memoryRepo = new MemoryRepository(db);
|
|
574
|
+
const recentMemories = options.includeRecent
|
|
575
|
+
? memoryRepo.findByDateRange(startDate, endDate, options.recentLimit || 5)
|
|
576
|
+
: [];
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
period: { start: startDate, end: endDate },
|
|
580
|
+
activeAreas: { tags, keywords },
|
|
581
|
+
todos: todos.map(t => ({ id: t.id, content: t.content, period: t.period })),
|
|
582
|
+
recentActivity: recentMemories.map(m => ({
|
|
583
|
+
date: m.createdAt.toISOString().split('T')[0],
|
|
584
|
+
summary: m.summary || ''
|
|
585
|
+
}))
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function calculatePeriodRange(period: string, date?: string) {
|
|
590
|
+
const endDate = date || new Date().toISOString().split('T')[0];
|
|
591
|
+
const startDate = new Date(endDate);
|
|
592
|
+
|
|
593
|
+
switch (period) {
|
|
594
|
+
case 'day': break;
|
|
595
|
+
case 'week': startDate.setDate(startDate.getDate() - 7); break;
|
|
596
|
+
case 'month': startDate.setMonth(startDate.getMonth() - 1); break;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
startDate: startDate.toISOString().split('T')[0],
|
|
601
|
+
endDate
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getTopTags(db: Database.Database, startDate: string, endDate: string, limit: number) {
|
|
606
|
+
return db.prepare(`
|
|
607
|
+
SELECT e.name, COUNT(me.memory_id) as count
|
|
608
|
+
FROM memory_entities me
|
|
609
|
+
JOIN entities e ON me.entity_id = e.id
|
|
610
|
+
JOIN memories m ON me.memory_id = m.id
|
|
611
|
+
WHERE e.type = 'tag' AND date(m.created_at) BETWEEN ? AND ?
|
|
612
|
+
GROUP BY e.id ORDER BY count DESC LIMIT ?
|
|
613
|
+
`).all(startDate, endDate, limit) as { name: string; count: number }[];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getTopKeywords(db: Database.Database, startDate: string, endDate: string, limit: number) {
|
|
617
|
+
const rows = db.prepare(`
|
|
618
|
+
SELECT e.name FROM memory_entities me
|
|
619
|
+
JOIN entities e ON me.entity_id = e.id
|
|
620
|
+
JOIN memories m ON me.memory_id = m.id
|
|
621
|
+
WHERE e.type = 'keyword' AND date(m.created_at) BETWEEN ? AND ?
|
|
622
|
+
GROUP BY e.id ORDER BY COUNT(*) DESC LIMIT ?
|
|
623
|
+
`).all(startDate, endDate, limit) as { name: string }[];
|
|
624
|
+
return rows.map(r => r.name);
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Step 4: 添加 MCP 工具**
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// src/mcp/tools.ts 添加
|
|
632
|
+
import { getMemoryIndex } from '../services/memoryIndex.js';
|
|
633
|
+
|
|
634
|
+
export function createGetMemoryIndexTool(db: Database.Database): MCPTool {
|
|
635
|
+
return {
|
|
636
|
+
name: 'get_memory_index',
|
|
637
|
+
description: 'Get memory index summary for conversation context',
|
|
638
|
+
inputSchema: {
|
|
639
|
+
type: 'object' as const,
|
|
640
|
+
properties: {
|
|
641
|
+
period: {
|
|
642
|
+
type: 'string',
|
|
643
|
+
enum: ['day', 'week', 'month'],
|
|
644
|
+
default: 'week'
|
|
645
|
+
},
|
|
646
|
+
date: { type: 'string' },
|
|
647
|
+
includeTodos: { type: 'boolean', default: true },
|
|
648
|
+
includeRecent: { type: 'boolean', default: true },
|
|
649
|
+
recentLimit: { type: 'number', default: 5 }
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
handler: async (params) => {
|
|
653
|
+
return await getMemoryIndex(db, params);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Step 5: 运行测试**
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
npm test tests/mcp/memoryIndex.test.ts
|
|
663
|
+
```
|
|
664
|
+
Expected: PASS
|
|
665
|
+
|
|
666
|
+
**Step 6: 提交**
|
|
667
|
+
|
|
668
|
+
```bash
|
|
669
|
+
git add src/services/memoryIndex.ts src/mcp/tools.ts tests/mcp/memoryIndex.test.ts
|
|
670
|
+
git commit -m "feat: add get_memory_index MCP tool"
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## 阶段三:待办事项 MCP 工具
|
|
676
|
+
|
|
677
|
+
### Task 7: 添加待办事项 MCP 工具
|
|
678
|
+
|
|
679
|
+
**Files:**
|
|
680
|
+
- Modify: `src/mcp/tools.ts`
|
|
681
|
+
- Test: `tests/mcp/todo.test.ts`
|
|
682
|
+
|
|
683
|
+
**Step 1: 编写测试**
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// tests/mcp/todo.test.ts
|
|
687
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
688
|
+
import Database from 'better-sqlite3';
|
|
689
|
+
import { initializeDatabase } from '../../src/db/schema.js';
|
|
690
|
+
import { createAddTodoTool, createListTodosTool, createCompleteTodoTool } from '../../src/mcp/tools.js';
|
|
691
|
+
|
|
692
|
+
describe('Todo MCP Tools', () => {
|
|
693
|
+
let db: Database.Database;
|
|
694
|
+
|
|
695
|
+
beforeEach(() => {
|
|
696
|
+
db = new Database(':memory:');
|
|
697
|
+
initializeDatabase(db);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should add todo', async () => {
|
|
701
|
+
const addTool = createAddTodoTool(db);
|
|
702
|
+
const result = await addTool.handler({ content: '新任务', period: 'week', periodDate: '2026-03-02' });
|
|
703
|
+
expect(result.id).toBeDefined();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('should list todos', async () => {
|
|
707
|
+
const addTool = createAddTodoTool(db);
|
|
708
|
+
const listTool = createListTodosTool(db);
|
|
709
|
+
|
|
710
|
+
await addTool.handler({ content: '任务1', period: 'week', periodDate: '2026-03-02' });
|
|
711
|
+
await addTool.handler({ content: '任务2', period: 'week', periodDate: '2026-03-02' });
|
|
712
|
+
|
|
713
|
+
const result = await listTool.handler({ period: 'week', periodDate: '2026-03-02' });
|
|
714
|
+
expect(result.todos.length).toBe(2);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should complete todo', async () => {
|
|
718
|
+
const addTool = createAddTodoTool(db);
|
|
719
|
+
const completeTool = createCompleteTodoTool(db);
|
|
720
|
+
|
|
721
|
+
const created = await addTool.handler({ content: '任务', period: 'day', periodDate: '2026-03-02' });
|
|
722
|
+
await completeTool.handler({ id: created.id });
|
|
723
|
+
|
|
724
|
+
const listTool = createListTodosTool(db);
|
|
725
|
+
const result = await listTool.handler({ period: 'day', periodDate: '2026-03-02' });
|
|
726
|
+
expect(result.todos[0].completedAt).toBeDefined();
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Step 2: 运行测试**
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
npm test tests/mcp/todo.test.ts
|
|
735
|
+
```
|
|
736
|
+
Expected: FAIL - tools not implemented
|
|
737
|
+
|
|
738
|
+
**Step 3: 实现工具**
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
// src/mcp/tools.ts 添加
|
|
742
|
+
import { TodoRepository, CreateTodoInput } from '../db/todoRepository.js';
|
|
743
|
+
|
|
744
|
+
export function createAddTodoTool(db: Database.Database): MCPTool {
|
|
745
|
+
return {
|
|
746
|
+
name: 'add_todo',
|
|
747
|
+
description: 'Add a todo item',
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: 'object' as const,
|
|
750
|
+
properties: {
|
|
751
|
+
content: { type: 'string' },
|
|
752
|
+
period: { type: 'string', enum: ['day', 'week', 'month'] },
|
|
753
|
+
periodDate: { type: 'string' },
|
|
754
|
+
memoryId: { type: 'string' }
|
|
755
|
+
},
|
|
756
|
+
required: ['content', 'period', 'periodDate']
|
|
757
|
+
},
|
|
758
|
+
handler: async (params) => {
|
|
759
|
+
const repo = new TodoRepository(db);
|
|
760
|
+
const todo = repo.create(params as CreateTodoInput);
|
|
761
|
+
return todo;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export function createListTodosTool(db: Database.Database): MCPTool {
|
|
767
|
+
return {
|
|
768
|
+
name: 'list_todos',
|
|
769
|
+
description: 'List todo items',
|
|
770
|
+
inputSchema: {
|
|
771
|
+
type: 'object' as const,
|
|
772
|
+
properties: {
|
|
773
|
+
period: { type: 'string', enum: ['day', 'week', 'month'] },
|
|
774
|
+
periodDate: { type: 'string' },
|
|
775
|
+
includeCompleted: { type: 'boolean', default: false }
|
|
776
|
+
},
|
|
777
|
+
required: ['period', 'periodDate']
|
|
778
|
+
},
|
|
779
|
+
handler: async (params) => {
|
|
780
|
+
const repo = new TodoRepository(db);
|
|
781
|
+
let todos = repo.findByPeriod(params.period, params.periodDate);
|
|
782
|
+
if (!params.includeCompleted) {
|
|
783
|
+
todos = todos.filter(t => !t.completedAt);
|
|
784
|
+
}
|
|
785
|
+
return { todos };
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function createCompleteTodoTool(db: Database.Database): MCPTool {
|
|
791
|
+
return {
|
|
792
|
+
name: 'complete_todo',
|
|
793
|
+
description: 'Mark a todo as completed',
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: 'object' as const,
|
|
796
|
+
properties: {
|
|
797
|
+
id: { type: 'string' }
|
|
798
|
+
},
|
|
799
|
+
required: ['id']
|
|
800
|
+
},
|
|
801
|
+
handler: async (params) => {
|
|
802
|
+
const repo = new TodoRepository(db);
|
|
803
|
+
repo.markCompleted(params.id);
|
|
804
|
+
return { success: true };
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Step 4: 运行测试**
|
|
811
|
+
|
|
812
|
+
```bash
|
|
813
|
+
npm test tests/mcp/todo.test.ts
|
|
814
|
+
```
|
|
815
|
+
Expected: PASS
|
|
816
|
+
|
|
817
|
+
**Step 5: 提交**
|
|
818
|
+
|
|
819
|
+
```bash
|
|
820
|
+
git add src/mcp/tools.ts tests/mcp/todo.test.ts
|
|
821
|
+
git commit -m "feat: add todo MCP tools (add_todo, list_todos, complete_todo)"
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## 总结
|
|
827
|
+
|
|
828
|
+
完成所有任务后,运行完整测试:
|
|
829
|
+
|
|
830
|
+
```bash
|
|
831
|
+
npm test
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
预期:所有测试通过
|
|
835
|
+
|
|
836
|
+
---
|