@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.
Files changed (131) hide show
  1. package/.claude/settings.local.json +68 -0
  2. package/README.md +323 -0
  3. package/dist/config/llm.d.ts +13 -0
  4. package/dist/config/llm.d.ts.map +1 -0
  5. package/dist/config/llm.js +96 -0
  6. package/dist/config/llm.js.map +1 -0
  7. package/dist/config/plugin.d.ts +15 -0
  8. package/dist/config/plugin.d.ts.map +1 -0
  9. package/dist/config/plugin.js +32 -0
  10. package/dist/config/plugin.js.map +1 -0
  11. package/dist/db/entityRepository.d.ts +21 -0
  12. package/dist/db/entityRepository.d.ts.map +1 -0
  13. package/dist/db/entityRepository.js +55 -0
  14. package/dist/db/entityRepository.js.map +1 -0
  15. package/dist/db/repository.d.ts +22 -0
  16. package/dist/db/repository.d.ts.map +1 -0
  17. package/dist/db/repository.js +77 -0
  18. package/dist/db/repository.js.map +1 -0
  19. package/dist/db/schema.d.ts +5 -0
  20. package/dist/db/schema.d.ts.map +1 -0
  21. package/dist/db/schema.js +112 -0
  22. package/dist/db/schema.js.map +1 -0
  23. package/dist/db/todoRepository.d.ts +26 -0
  24. package/dist/db/todoRepository.d.ts.map +1 -0
  25. package/dist/db/todoRepository.js +54 -0
  26. package/dist/db/todoRepository.js.map +1 -0
  27. package/dist/hooks/bootstrap.d.ts +3 -0
  28. package/dist/hooks/bootstrap.d.ts.map +1 -0
  29. package/dist/hooks/bootstrap.js +28 -0
  30. package/dist/hooks/bootstrap.js.map +1 -0
  31. package/dist/hooks/message.d.ts +18 -0
  32. package/dist/hooks/message.d.ts.map +1 -0
  33. package/dist/hooks/message.js +52 -0
  34. package/dist/hooks/message.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +46 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mcp/tools.d.ts +26 -0
  40. package/dist/mcp/tools.d.ts.map +1 -0
  41. package/dist/mcp/tools.js +360 -0
  42. package/dist/mcp/tools.js.map +1 -0
  43. package/dist/plugin.d.ts +18 -0
  44. package/dist/plugin.d.ts.map +1 -0
  45. package/dist/plugin.js +62 -0
  46. package/dist/plugin.js.map +1 -0
  47. package/dist/services/entityGraphService.d.ts +87 -0
  48. package/dist/services/entityGraphService.d.ts.map +1 -0
  49. package/dist/services/entityGraphService.js +271 -0
  50. package/dist/services/entityGraphService.js.map +1 -0
  51. package/dist/services/memory.d.ts +26 -0
  52. package/dist/services/memory.d.ts.map +1 -0
  53. package/dist/services/memory.js +281 -0
  54. package/dist/services/memory.js.map +1 -0
  55. package/dist/services/memoryIndex.d.ts +34 -0
  56. package/dist/services/memoryIndex.d.ts.map +1 -0
  57. package/dist/services/memoryIndex.js +100 -0
  58. package/dist/services/memoryIndex.js.map +1 -0
  59. package/dist/services/metadataExtractor.d.ts +16 -0
  60. package/dist/services/metadataExtractor.d.ts.map +1 -0
  61. package/dist/services/metadataExtractor.js +75 -0
  62. package/dist/services/metadataExtractor.js.map +1 -0
  63. package/dist/services/retrieval.d.ts +24 -0
  64. package/dist/services/retrieval.d.ts.map +1 -0
  65. package/dist/services/retrieval.js +40 -0
  66. package/dist/services/retrieval.js.map +1 -0
  67. package/dist/services/scheduler.d.ts +122 -0
  68. package/dist/services/scheduler.d.ts.map +1 -0
  69. package/dist/services/scheduler.js +434 -0
  70. package/dist/services/scheduler.js.map +1 -0
  71. package/dist/services/summarizer.d.ts +43 -0
  72. package/dist/services/summarizer.d.ts.map +1 -0
  73. package/dist/services/summarizer.js +252 -0
  74. package/dist/services/summarizer.js.map +1 -0
  75. package/dist/services/tagService.d.ts +64 -0
  76. package/dist/services/tagService.d.ts.map +1 -0
  77. package/dist/services/tagService.js +281 -0
  78. package/dist/services/tagService.js.map +1 -0
  79. package/dist/tools/memory.d.ts +3 -0
  80. package/dist/tools/memory.d.ts.map +1 -0
  81. package/dist/tools/memory.js +114 -0
  82. package/dist/tools/memory.js.map +1 -0
  83. package/dist/types.d.ts +128 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +6 -0
  86. package/dist/types.js.map +1 -0
  87. package/docs/plans/2026-03-02-claw-memory-design.md +445 -0
  88. package/docs/plans/2026-03-02-incremental-summary-design.md +157 -0
  89. package/docs/plans/2026-03-02-incremental-summary-implementation.md +468 -0
  90. package/docs/plans/2026-03-02-memory-index-design.md +163 -0
  91. package/docs/plans/2026-03-02-memory-index-implementation.md +836 -0
  92. package/docs/plans/2026-03-02-mvp-implementation.md +1703 -0
  93. package/docs/plans/2026-03-02-testing-implementation.md +395 -0
  94. package/docs/plans/2026-03-02-testing-plan.md +93 -0
  95. package/docs/plans/2026-03-03-claw-memory-openclaw-plugin-design.md +285 -0
  96. package/docs/plans/2026-03-03-claw-memory-plugin-implementation.md +642 -0
  97. package/docs/plans/2026-03-03-entity-graph-design.md +121 -0
  98. package/docs/plans/2026-03-03-entity-graph-implementation.md +687 -0
  99. package/docs/plans/2026-03-03-llm-generic-config-design.md +43 -0
  100. package/docs/plans/2026-03-03-llm-generic-config-implementation.md +186 -0
  101. package/docs/plans/2026-03-03-memory-e2e-stress-test-design.md +110 -0
  102. package/docs/plans/2026-03-03-memory-e2e-stress-test-implementation.md +464 -0
  103. package/docs/plans/2026-03-03-minimax-llm-fix.md +156 -0
  104. package/docs/plans/2026-03-03-scheduler-design.md +165 -0
  105. package/docs/plans/2026-03-03-scheduler-implementation.md +777 -0
  106. package/docs/plans/2026-03-03-tags-visualization-design.md +73 -0
  107. package/docs/plans/2026-03-03-tags-visualization-implementation.md +539 -0
  108. package/openclaw.plugin.json +11 -0
  109. package/package.json +41 -0
  110. package/src/config/llm.ts +129 -0
  111. package/src/config/plugin.ts +47 -0
  112. package/src/db/entityRepository.ts +80 -0
  113. package/src/db/repository.ts +106 -0
  114. package/src/db/schema.ts +121 -0
  115. package/src/db/todoRepository.ts +76 -0
  116. package/src/hooks/bootstrap.ts +36 -0
  117. package/src/hooks/message.ts +84 -0
  118. package/src/index.ts +50 -0
  119. package/src/plugin.ts +85 -0
  120. package/src/services/entityGraphService.ts +367 -0
  121. package/src/services/memory.ts +338 -0
  122. package/src/services/memoryIndex.ts +140 -0
  123. package/src/services/metadataExtractor.ts +89 -0
  124. package/src/services/retrieval.ts +71 -0
  125. package/src/services/scheduler.ts +529 -0
  126. package/src/services/summarizer.ts +318 -0
  127. package/src/services/tagService.ts +335 -0
  128. package/src/tools/memory.ts +137 -0
  129. package/src/types.ts +139 -0
  130. package/tsconfig.json +20 -0
  131. package/vitest.config.ts +16 -0
@@ -0,0 +1,338 @@
1
+ import Database from 'better-sqlite3';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { readFileSync } from 'fs';
4
+ import { writeFile, mkdir } from 'fs/promises';
5
+ import { dirname, join } from 'path';
6
+ import { MemoryRepository, CreateMemoryInput } from '../db/repository.js';
7
+ import { EntityRepository } from '../db/entityRepository.js';
8
+ import { calculateWeight, DEFAULT_TIME_DECAY, type SearchOptions } from './retrieval.js';
9
+ import { SummarizerService } from './summarizer.js';
10
+ import { generateSummaryWithLLM } from '../config/llm.js';
11
+ import { MetadataExtractor } from './metadataExtractor.js';
12
+ import type { Memory, SaveMemoryInput, GetContextInput, TimeBucket, WeeklyReport, IntegratedSummary } from '../types.js';
13
+
14
+ export class MemoryService {
15
+ private db: Database.Database;
16
+ private memoryRepo: MemoryRepository;
17
+ private entityRepo: EntityRepository;
18
+ private summarizer: SummarizerService;
19
+ private extractor: MetadataExtractor;
20
+ private dataDir: string;
21
+
22
+ constructor(db: Database.Database, dataDir: string = './memories') {
23
+ this.db = db;
24
+ this.memoryRepo = new MemoryRepository(db);
25
+ this.entityRepo = new EntityRepository(db);
26
+ this.summarizer = new SummarizerService(db);
27
+ this.extractor = new MetadataExtractor();
28
+ this.dataDir = dataDir;
29
+ }
30
+
31
+ getLatestIntegratedSummary(): IntegratedSummary | null {
32
+ return this.memoryRepo.getLatestIntegratedSummary();
33
+ }
34
+
35
+ async saveMemory(input: SaveMemoryInput): Promise<Memory> {
36
+ const { content, metadata } = input;
37
+
38
+ // 生成文件路径
39
+ const date = new Date();
40
+ const datePath = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`;
41
+ const fileName = `${uuidv4()}.md`;
42
+ const contentPath = join(this.dataDir, datePath, fileName);
43
+
44
+ // 保存内容到文件
45
+ await this.saveContentToFile(contentPath, content);
46
+
47
+ // 获取已有的整体摘要
48
+ const existingSummary = this.getLatestIntegratedSummary();
49
+
50
+ // 调用 LLM 提取元数据(传入已有摘要)
51
+ const extracted = content.trim()
52
+ ? await this.extractor.extract(content, existingSummary || undefined)
53
+ : { tags: [], keywords: [], subjects: [], importance: 0.5, summary: '', integratedSummary: existingSummary || { active_areas: [], key_topics: [], recent_summary: '' } };
54
+
55
+ // 合并:LLM 提取的覆盖用户传入的
56
+ const mergedMetadata = {
57
+ tags: extracted.tags,
58
+ keywords: extracted.keywords,
59
+ subjects: extracted.subjects,
60
+ importance: extracted.importance,
61
+ summary: extracted.summary,
62
+ ...metadata
63
+ };
64
+
65
+ // 创建记忆记录
66
+ const clampedImportance = this.clampImportance(mergedMetadata.importance);
67
+ const memoryInput: CreateMemoryInput = {
68
+ contentPath,
69
+ summary: mergedMetadata.summary || undefined,
70
+ integratedSummary: extracted.integratedSummary,
71
+ importance: clampedImportance ?? 0.5,
72
+ tokenCount: this.estimateTokens(content)
73
+ };
74
+
75
+ const memory = this.memoryRepo.create(memoryInput);
76
+
77
+ // 处理实体关联
78
+ await this.processEntities(memory.id, mergedMetadata);
79
+
80
+ return memory;
81
+ }
82
+
83
+ async searchMemory(options: SearchOptions): Promise<Memory[]> {
84
+ const { query, timeRange, tags, limit = 10 } = options;
85
+
86
+ // 构建时间过滤条件
87
+ const dateFilter = this.buildDateFilter(timeRange);
88
+
89
+ // 获取所有候选记忆
90
+ let memories = this.memoryRepo.findAll(100);
91
+
92
+ // 应用查询过滤(如果提供了查询字符串)
93
+ if (query && query.trim() !== '') {
94
+ const searchTerm = query.toLowerCase();
95
+ memories = memories.filter(m => {
96
+ const summaryMatch = m.summary?.toLowerCase().includes(searchTerm);
97
+ // 读取内容文件进行匹配
98
+ let contentMatch = false;
99
+ try {
100
+ const content = this.readContentFromFile(m.contentPath);
101
+ contentMatch = content.toLowerCase().includes(searchTerm);
102
+ } catch {
103
+ // 如果读取失败,忽略内容匹配
104
+ }
105
+ return summaryMatch || contentMatch;
106
+ });
107
+ }
108
+
109
+ // 应用时间过滤
110
+ if (dateFilter) {
111
+ memories = memories.filter(m => {
112
+ const created = m.createdAt.toISOString().split('T')[0];
113
+ return created >= dateFilter.start && created <= dateFilter.end;
114
+ });
115
+ }
116
+
117
+ // 计算权重并排序
118
+ const weightedMemories = memories.map(memory => ({
119
+ memory,
120
+ weight: calculateWeight({
121
+ entityMatch: 0,
122
+ timeDecay: DEFAULT_TIME_DECAY,
123
+ memoryDate: memory.createdAt.toISOString().split('T')[0],
124
+ tagMatch: 0,
125
+ importance: memory.importance
126
+ })
127
+ }));
128
+
129
+ // 按权重排序
130
+ weightedMemories.sort((a, b) => b.weight - a.weight);
131
+
132
+ // 返回结果
133
+ return weightedMemories.slice(0, limit).map(w => w.memory);
134
+ }
135
+
136
+ async getContext(input: GetContextInput): Promise<string> {
137
+ const { query, maxTokens = 8000 } = input;
138
+
139
+ const memories = await this.searchMemory({
140
+ query,
141
+ limit: 20,
142
+ maxTokens
143
+ });
144
+
145
+ // 累积内容直到达到 token 限制
146
+ let totalTokens = 0;
147
+ const contextParts: string[] = [];
148
+
149
+ for (const memory of memories) {
150
+ const content = this.readContentFromFile(memory.contentPath);
151
+ const tokens = this.estimateTokens(content);
152
+
153
+ if (totalTokens + tokens > maxTokens) {
154
+ break;
155
+ }
156
+
157
+ contextParts.push(content);
158
+ totalTokens += tokens;
159
+
160
+ // 更新访问计数
161
+ this.memoryRepo.updateLastAccessed(memory.id);
162
+ }
163
+
164
+ return contextParts.join('\n\n---\n\n');
165
+ }
166
+
167
+ async getSummary(period: 'day' | 'week' | 'month', date?: string): Promise<TimeBucket> {
168
+ const targetDate = date ? new Date(date) : new Date();
169
+ const { startDate, endDate } = this.calculatePeriodRange(period, targetDate);
170
+
171
+ // 聚合数据
172
+ const report: WeeklyReport = await this.summarizer.aggregateMemories(startDate, endDate);
173
+
174
+ // 生成 LLM 总结
175
+ try {
176
+ const reportString = this.summarizer.reportToString(report);
177
+ report.summary = await generateSummaryWithLLM(reportString);
178
+ } catch (error) {
179
+ console.warn('Failed to generate LLM summary:', error);
180
+ report.summary = '总结生成失败,请配置有效的 LLM API Key。';
181
+ }
182
+
183
+ // 保存到 time_buckets 表
184
+ this.saveTimeBucket(startDate, report);
185
+
186
+ return {
187
+ date: startDate,
188
+ memoryCount: report.basic.totalMemories,
189
+ summary: report.summary,
190
+ summaryGeneratedAt: new Date(),
191
+ keyTopics: report.topics.keyTopics,
192
+ createdAt: new Date()
193
+ };
194
+ }
195
+
196
+ private calculatePeriodRange(period: 'day' | 'week' | 'month', date: Date): { startDate: string; endDate: string } {
197
+ const endDate = date.toISOString().split('T')[0];
198
+ const startDate = new Date(date);
199
+
200
+ switch (period) {
201
+ case 'day':
202
+ // 返回当天
203
+ break;
204
+ case 'week':
205
+ startDate.setDate(startDate.getDate() - 7);
206
+ break;
207
+ case 'month':
208
+ startDate.setMonth(startDate.getMonth() - 1);
209
+ break;
210
+ }
211
+
212
+ return {
213
+ startDate: startDate.toISOString().split('T')[0],
214
+ endDate
215
+ };
216
+ }
217
+
218
+ private saveTimeBucket(date: string, report: WeeklyReport): void {
219
+ const stmt = this.db.prepare(`
220
+ INSERT OR REPLACE INTO time_buckets (date, memory_count, summary, summary_generated_at, key_topics, created_at)
221
+ VALUES (?, ?, ?, ?, ?, ?)
222
+ `);
223
+
224
+ stmt.run(
225
+ date,
226
+ report.basic.totalMemories,
227
+ report.summary,
228
+ new Date().toISOString(),
229
+ JSON.stringify(report.topics.keyTopics),
230
+ new Date().toISOString()
231
+ );
232
+ }
233
+
234
+ private async saveContentToFile(path: string, content: string): Promise<void> {
235
+ const dir = dirname(path);
236
+ await mkdir(dir, { recursive: true });
237
+ await writeFile(path, content, 'utf-8');
238
+ }
239
+
240
+ private readContentFromFile(path: string): string {
241
+ try {
242
+ return readFileSync(path, 'utf-8');
243
+ } catch {
244
+ return '';
245
+ }
246
+ }
247
+
248
+ private async processEntities(memoryId: string, metadata: SaveMemoryInput['metadata']): Promise<void> {
249
+ // 处理标签
250
+ if (metadata.tags) {
251
+ for (const tag of metadata.tags) {
252
+ const entity = this.entityRepo.findOrCreate({
253
+ name: tag,
254
+ type: 'tag',
255
+ level: tag.split('/').length - 1
256
+ });
257
+
258
+ this.db.prepare(`
259
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
260
+ VALUES (?, ?, ?)
261
+ `).run(memoryId, entity.id, 1.0);
262
+ }
263
+ }
264
+
265
+ // 处理主题
266
+ if (metadata.subjects) {
267
+ for (const subject of metadata.subjects) {
268
+ const entity = this.entityRepo.findOrCreate({
269
+ name: subject,
270
+ type: 'subject'
271
+ });
272
+
273
+ this.db.prepare(`
274
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
275
+ VALUES (?, ?, ?)
276
+ `).run(memoryId, entity.id, 0.8);
277
+ }
278
+ }
279
+
280
+ // 处理关键词
281
+ if (metadata.keywords) {
282
+ for (const keyword of metadata.keywords) {
283
+ const entity = this.entityRepo.findOrCreate({
284
+ name: keyword,
285
+ type: 'keyword'
286
+ });
287
+
288
+ this.db.prepare(`
289
+ INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, relevance)
290
+ VALUES (?, ?, ?)
291
+ `).run(memoryId, entity.id, 0.6);
292
+ }
293
+ }
294
+ }
295
+
296
+ private buildDateFilter(timeRange?: SearchOptions['timeRange']): { start: string; end: string } | null {
297
+ if (!timeRange || timeRange === 'all') return null;
298
+
299
+ const now = new Date();
300
+ const end = now.toISOString().split('T')[0];
301
+ let start: string;
302
+
303
+ switch (timeRange) {
304
+ case 'today':
305
+ start = end;
306
+ break;
307
+ case 'week':
308
+ now.setDate(now.getDate() - 7);
309
+ start = now.toISOString().split('T')[0];
310
+ break;
311
+ case 'month':
312
+ now.setMonth(now.getMonth() - 1);
313
+ start = now.toISOString().split('T')[0];
314
+ break;
315
+ case 'year':
316
+ now.setFullYear(now.getFullYear() - 1);
317
+ start = now.toISOString().split('T')[0];
318
+ break;
319
+ default:
320
+ return null;
321
+ }
322
+
323
+ return { start, end };
324
+ }
325
+
326
+ private clampImportance(value: number | undefined): number | undefined {
327
+ if (value === undefined) {
328
+ return undefined;
329
+ }
330
+ return Math.max(0, Math.min(1, value));
331
+ }
332
+
333
+ private estimateTokens(text: string): number {
334
+ const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
335
+ const otherChars = text.length - chineseChars;
336
+ return Math.ceil(chineseChars / 1.5 + otherChars / 4);
337
+ }
338
+ }
@@ -0,0 +1,140 @@
1
+ import Database from 'better-sqlite3';
2
+ import { TodoRepository } from '../db/todoRepository.js';
3
+ import { MemoryRepository } from '../db/repository.js';
4
+ import { EntityRepository } from '../db/entityRepository.js';
5
+ import type { IntegratedSummary } from '../types.js';
6
+
7
+ export interface MemoryIndexOptions {
8
+ period: 'day' | 'week' | 'month';
9
+ date?: string;
10
+ includeTodos?: boolean;
11
+ includeRecent?: boolean;
12
+ recentLimit?: number;
13
+ }
14
+
15
+ export interface MemoryIndex {
16
+ period: { start: string; end: string };
17
+ activeAreas: {
18
+ tags: { name: string; count: number }[];
19
+ keywords: string[];
20
+ };
21
+ todos: { id: string; content: string; period: string }[];
22
+ recentActivity: { date: string; summary: string }[];
23
+ integratedSummary?: IntegratedSummary;
24
+ }
25
+
26
+ export async function getMemoryIndex(db: Database.Database, options: MemoryIndexOptions): Promise<MemoryIndex> {
27
+ const { startDate, endDate } = calculatePeriodRange(options.period, options.date);
28
+
29
+ // 并行获取活跃领域
30
+ const [tags, keywords] = await Promise.all([
31
+ Promise.resolve(getTopTags(db, startDate, endDate, 10)),
32
+ Promise.resolve(getTopKeywords(db, startDate, endDate, 10))
33
+ ]);
34
+
35
+ // 并行获取待办和最近动态
36
+ const [todos, recentActivity] = await Promise.all([
37
+ options.includeTodos
38
+ ? getTodos(db, options.period, endDate)
39
+ : Promise.resolve([]),
40
+ options.includeRecent
41
+ ? getRecentActivity(db, startDate, endDate, options.recentLimit || 5)
42
+ : Promise.resolve([])
43
+ ]);
44
+
45
+ // 获取缓存的整体摘要(从最新的 memory)
46
+ const memoryRepo = new MemoryRepository(db);
47
+ const rawSummary = memoryRepo.getLatestIntegratedSummary();
48
+ // Convert null to undefined to match the interface type
49
+ const integratedSummary = rawSummary === null ? undefined : rawSummary;
50
+
51
+ return {
52
+ period: { start: startDate, end: endDate },
53
+ activeAreas: { tags, keywords },
54
+ todos,
55
+ recentActivity,
56
+ integratedSummary
57
+ };
58
+ }
59
+
60
+ async function getTodos(db: Database.Database, period: string, endDate: string) {
61
+ const todoRepo = new TodoRepository(db);
62
+ const allTodos = todoRepo.findByPeriod(period, endDate);
63
+ return allTodos
64
+ .filter(t => !t.completedAt)
65
+ .map(t => ({ id: t.id, content: t.content, period: t.period }));
66
+ }
67
+
68
+ async function getRecentActivity(db: Database.Database, startDate: string, endDate: string, limit: number) {
69
+ const recentMemories = findMemoriesByDateRange(db, startDate, endDate, limit);
70
+ return recentMemories.map(m => ({
71
+ date: m.createdAt.toISOString().split('T')[0],
72
+ summary: m.summary || ''
73
+ }));
74
+ }
75
+
76
+ function calculatePeriodRange(period: string, date?: string) {
77
+ const endDate = date || new Date().toISOString().split('T')[0];
78
+ const startDate = new Date(endDate);
79
+
80
+ switch (period) {
81
+ case 'day': break;
82
+ case 'week': startDate.setDate(startDate.getDate() - 7); break;
83
+ case 'month': startDate.setMonth(startDate.getMonth() - 1); break;
84
+ }
85
+
86
+ return {
87
+ startDate: startDate.toISOString().split('T')[0],
88
+ endDate
89
+ };
90
+ }
91
+
92
+ function getTopTags(db: Database.Database, startDate: string, endDate: string, limit: number) {
93
+ const rows = db.prepare(`
94
+ SELECT e.name, COUNT(me.memory_id) as count
95
+ FROM memory_entities me
96
+ JOIN entities e ON me.entity_id = e.id
97
+ JOIN memories m ON me.memory_id = m.id
98
+ WHERE e.type = 'tag' AND date(m.created_at) BETWEEN ? AND ?
99
+ GROUP BY e.id ORDER BY count DESC LIMIT ?
100
+ `).all(startDate, endDate, limit) as { name: string; count: number }[];
101
+
102
+ return rows;
103
+ }
104
+
105
+ function getTopKeywords(db: Database.Database, startDate: string, endDate: string, limit: number) {
106
+ const rows = db.prepare(`
107
+ SELECT e.name FROM memory_entities me
108
+ JOIN entities e ON me.entity_id = e.id
109
+ JOIN memories m ON me.memory_id = m.id
110
+ WHERE e.type = 'keyword' AND date(m.created_at) BETWEEN ? AND ?
111
+ GROUP BY e.id ORDER BY COUNT(*) DESC LIMIT ?
112
+ `).all(startDate, endDate, limit) as { name: string }[];
113
+ return rows.map(r => r.name);
114
+ }
115
+
116
+ interface MemoryRow {
117
+ id: string;
118
+ contentPath: string;
119
+ summary: string | null;
120
+ createdAt: Date;
121
+ importance: number;
122
+ }
123
+
124
+ function findMemoriesByDateRange(db: Database.Database, startDate: string, endDate: string, limit: number): MemoryRow[] {
125
+ const rows = db.prepare(`
126
+ SELECT id, content_path, summary, created_at, importance
127
+ FROM memories
128
+ WHERE date(created_at) BETWEEN ? AND ?
129
+ ORDER BY created_at DESC
130
+ LIMIT ?
131
+ `).all(startDate, endDate, limit) as any[];
132
+
133
+ return rows.map(row => ({
134
+ id: row.id,
135
+ contentPath: row.content_path,
136
+ summary: row.summary,
137
+ createdAt: new Date(row.created_at),
138
+ importance: row.importance
139
+ }));
140
+ }
@@ -0,0 +1,89 @@
1
+ import { generateSummaryWithLLM } from '../config/llm.js';
2
+ import type { IntegratedSummary } from '../types.js';
3
+
4
+ export interface ExtractedMetadata {
5
+ tags: string[];
6
+ keywords: string[];
7
+ subjects: string[];
8
+ importance: number;
9
+ summary: string;
10
+ integratedSummary: IntegratedSummary;
11
+ }
12
+
13
+ const EXTRACTION_PROMPT = `请从以下对话内容中提取结构化元数据,并整合已有的整体摘要:
14
+
15
+ 当前对话内容:
16
+ {content}
17
+
18
+ 已有整体摘要(请在此基础上增量更新):
19
+ {existing_summary}
20
+
21
+ 请以 JSON 格式返回:
22
+ {
23
+ "tags": ["一级分类/二级分类"],
24
+ "keywords": ["关键词1", "关键词2"],
25
+ "subjects": ["主题1"],
26
+ "importance": 0.0-1.0,
27
+ "summary": "当前对话的一句话摘要",
28
+ "integrated_summary": {
29
+ "active_areas": ["领域名 (出现次数)"],
30
+ "key_topics": ["主题1", "主题2"],
31
+ "recent_summary": "整体摘要自然语言描述"
32
+ }
33
+ }
34
+
35
+ 注意:
36
+ - tags 使用层级结构
37
+ - integrated_summary 需整合历史信息,在已有基础上增加新领域
38
+ - 只返回 JSON,不要其他内容`;
39
+
40
+ export class MetadataExtractor {
41
+ async extract(content: string, existingSummary?: IntegratedSummary): Promise<ExtractedMetadata> {
42
+ let existingSummaryText = '无';
43
+ if (existingSummary) {
44
+ existingSummaryText = `活跃领域: ${existingSummary.active_areas.join(', ')}\n关键词: ${existingSummary.key_topics.join(', ')}\n近期摘要: ${existingSummary.recent_summary}`;
45
+ }
46
+
47
+ const prompt = EXTRACTION_PROMPT
48
+ .replace('{content}', content)
49
+ .replace('{existing_summary}', existingSummaryText);
50
+
51
+ try {
52
+ const response = await generateSummaryWithLLM(prompt);
53
+ return this.parseLLMResponse(response);
54
+ } catch (error) {
55
+ console.warn('LLM extraction failed:', error);
56
+ return this.fallbackExtract(content);
57
+ }
58
+ }
59
+
60
+ async callLLM(prompt: string): Promise<ExtractedMetadata> {
61
+ const response = await generateSummaryWithLLM(prompt);
62
+ return this.parseLLMResponse(response);
63
+ }
64
+
65
+ private parseLLMResponse(response: string): ExtractedMetadata {
66
+ try {
67
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
68
+ if (jsonMatch) {
69
+ return JSON.parse(jsonMatch[0]);
70
+ }
71
+ } catch {}
72
+ return this.fallbackExtract('');
73
+ }
74
+
75
+ private fallbackExtract(content: string): ExtractedMetadata {
76
+ return {
77
+ tags: [],
78
+ keywords: [],
79
+ subjects: [],
80
+ importance: 0.5,
81
+ summary: content.substring(0, 100),
82
+ integratedSummary: {
83
+ active_areas: [],
84
+ key_topics: [],
85
+ recent_summary: ''
86
+ }
87
+ };
88
+ }
89
+ }
@@ -0,0 +1,71 @@
1
+ import type { Memory, Entity } from '../types.js';
2
+
3
+ export interface TimeDecayConfig {
4
+ today: number;
5
+ week: number;
6
+ month: number;
7
+ year: number;
8
+ older: number;
9
+ }
10
+
11
+ export interface WeightInput {
12
+ entityMatch: number;
13
+ timeDecay: TimeDecayConfig;
14
+ memoryDate: string;
15
+ tagMatch: number;
16
+ importance: number;
17
+ }
18
+
19
+ export const DEFAULT_TIME_DECAY: TimeDecayConfig = {
20
+ today: 30,
21
+ week: 20,
22
+ month: 10,
23
+ year: 5,
24
+ older: 0
25
+ };
26
+
27
+ export function calculateWeight(input: WeightInput): number {
28
+ const { entityMatch, timeDecay, memoryDate, tagMatch, importance } = input;
29
+
30
+ // 实体匹配权重 (0-40)
31
+ const entityWeight = Math.min(entityMatch * 10, 40);
32
+
33
+ // 时间衰减权重 (0-30)
34
+ const timeWeight = getTimeWeight(memoryDate, timeDecay);
35
+
36
+ // 标签层级权重 (0-20)
37
+ const tagWeight = Math.min(tagMatch * 2, 20);
38
+
39
+ // 重要性权重 (0-10)
40
+ const importanceWeight = importance * 10;
41
+
42
+ // 总分 = 实体匹配 × 0.4 + 时间衰减 × 0.3 + 标签层级 × 0.2 + 重要性 × 0.1
43
+ // 归一化到 0-100
44
+ const total =
45
+ entityWeight * 0.4 +
46
+ timeWeight * 0.3 +
47
+ tagWeight * 0.2 +
48
+ importanceWeight * 0.1;
49
+
50
+ return Math.round(total * 10) / 10;
51
+ }
52
+
53
+ function getTimeWeight(memoryDate: string, config: TimeDecayConfig): number {
54
+ const today = new Date();
55
+ const memory = new Date(memoryDate);
56
+ const diffDays = Math.floor((today.getTime() - memory.getTime()) / (1000 * 60 * 60 * 24));
57
+
58
+ if (diffDays === 0) return config.today;
59
+ if (diffDays <= 7) return config.week;
60
+ if (diffDays <= 30) return config.month;
61
+ if (diffDays <= 365) return config.year;
62
+ return config.older;
63
+ }
64
+
65
+ export interface SearchOptions {
66
+ query: string;
67
+ timeRange?: 'today' | 'week' | 'month' | 'year' | 'all';
68
+ tags?: string[];
69
+ limit?: number;
70
+ maxTokens?: number;
71
+ }