@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,687 @@
1
+ # 实体关系图查询实现计划
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** 实现实体关系图查询功能,包括 MCP 工具和静态 HTML 可视化
6
+
7
+ **Architecture:** 使用 BFS 算法进行多跳查询,D3.js 力导向图实现可视化,复用现有 entity_relations 表
8
+
9
+ **Tech Stack:** TypeScript, D3.js, SQLite
10
+
11
+ ---
12
+
13
+ ## 准备工作
14
+
15
+ ### Task 1: 创建开发分支
16
+
17
+ **Step 1: 创建并切换到新分支**
18
+
19
+ ```bash
20
+ cd /home/ubuntu/openclaw/claw-memory
21
+ git checkout -b feature/entity-graph
22
+ ```
23
+
24
+ **Step 2: 验证分支**
25
+
26
+ ```bash
27
+ git branch --show-current
28
+ ```
29
+
30
+ Expected: `feature/entity-graph`
31
+
32
+ ---
33
+
34
+ ## Task 2: 创建实体关系服务类
35
+
36
+ **Files:**
37
+ - Create: `src/services/entityGraphService.ts`
38
+
39
+ **Step 1: 创建基础结构**
40
+
41
+ ```typescript
42
+ // src/services/entityGraphService.ts
43
+ import { getDatabase } from '../db/schema.js';
44
+
45
+ export interface EntityNode {
46
+ id: string;
47
+ name: string;
48
+ type: string;
49
+ }
50
+
51
+ export interface EntityEdge {
52
+ source: string;
53
+ target: string;
54
+ type: string;
55
+ weight: number;
56
+ }
57
+
58
+ export interface GraphData {
59
+ nodes: EntityNode[];
60
+ edges: EntityEdge[];
61
+ }
62
+
63
+ export interface RelationStats {
64
+ most_connected: Array<{ entity: string; count: number }>;
65
+ relation_types: Record<string, number>;
66
+ total_relations: number;
67
+ }
68
+
69
+ export class EntityGraphService {
70
+ private db: ReturnType<typeof getDatabase>;
71
+
72
+ constructor() {
73
+ this.db = getDatabase();
74
+ }
75
+ }
76
+ ```
77
+
78
+ **Step 2: 提交**
79
+
80
+ ```bash
81
+ git add src/services/entityGraphService.ts
82
+ git commit -m "feat: add EntityGraphService class skeleton"
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Task 3: 实现 getEntityRelations 方法
88
+
89
+ **Files:**
90
+ - Modify: `src/services/entityGraphService.ts`
91
+
92
+ **Step 1: 实现 getEntityRelations**
93
+
94
+ ```typescript
95
+ async getEntityRelations(entityName: string): Promise<{
96
+ entity: string;
97
+ relations: Array<{ target: string; type: string; weight: number }>;
98
+ }> {
99
+ // 查找实体
100
+ const entity = this.db.prepare(`
101
+ SELECT id, name FROM entities WHERE name = ? LIMIT 1
102
+ `).get(entityName) as { id: string; name: string } | undefined;
103
+
104
+ if (!entity) {
105
+ return { entity: entityName, relations: [] };
106
+ }
107
+
108
+ // 查询直接关联(作为 source)
109
+ const asSource = this.db.prepare(`
110
+ SELECT e.name as target, er.relation_type as type, er.weight
111
+ FROM entity_relations er
112
+ JOIN entities e ON er.target_id = e.id
113
+ WHERE er.source_id = ?
114
+ `).all(entity.id) as Array<{ target: string; type: string; weight: number }>;
115
+
116
+ // 查询直接关联(作为 target)
117
+ const asTarget = this.db.prepare(`
118
+ SELECT e.name as target, er.relation_type as type, er.weight
119
+ FROM entity_relations er
120
+ JOIN entities e ON er.source_id = e.id
121
+ WHERE er.target_id = ?
122
+ `).all(entity.id) as Array<{ target: string; type: string; weight: number }>;
123
+
124
+ // 合并结果
125
+ const relations = [...asSource, ...asTarget];
126
+
127
+ return {
128
+ entity: entity.name,
129
+ relations
130
+ };
131
+ }
132
+ ```
133
+
134
+ **Step 2: 测试运行**
135
+
136
+ ```bash
137
+ npx ts-node -e "
138
+ import { EntityGraphService } from './src/services/entityGraphService.js';
139
+ const svc = new EntityGraphService();
140
+ svc.getEntityRelations('技术').then(r => console.log(JSON.stringify(r, null, 2))).catch(e => console.error(e));
141
+ "
142
+ ```
143
+
144
+ Expected: 返回关联数据(可能为空)
145
+
146
+ **Step 3: 提交**
147
+
148
+ ```bash
149
+ git add src/services/entityGraphService.ts
150
+ git commit -m "feat: implement getEntityRelations method"
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Task 4: 实现 queryEntityGraph 方法(多跳查询)
156
+
157
+ **Files:**
158
+ - Modify: `src/services/entityGraphService.ts`
159
+
160
+ **Step 1: 实现 BFS 多跳查询**
161
+
162
+ ```typescript
163
+ async queryEntityGraph(
164
+ startEntity: string,
165
+ endEntity?: string,
166
+ maxHops: number = 2
167
+ ): Promise<GraphData> {
168
+ // 限制跳数
169
+ maxHops = Math.min(Math.max(1, maxHops), 5);
170
+
171
+ // 查找起点实体
172
+ const start = this.db.prepare(`
173
+ SELECT id, name, type FROM entities WHERE name = ? LIMIT 1
174
+ `).get(startEntity) as { id: string; name: string; type: string } | undefined;
175
+
176
+ if (!start) {
177
+ return { nodes: [], edges: [] };
178
+ }
179
+
180
+ // BFS 查询
181
+ const visited = new Set<string>();
182
+ const nodes: EntityNode[] = [];
183
+ const edges: EntityEdge[] = [];
184
+ const queue: Array<{ id: string; name: string; type: string; hop: number }> = [];
185
+
186
+ queue.push({ id: start.id, name: start.name, type: start.type, hop: 0 });
187
+ visited.add(start.id);
188
+ nodes.push({ id: start.id, name: start.name, type: start.type });
189
+
190
+ while (queue.length > 0) {
191
+ const current = queue.shift()!;
192
+
193
+ if (current.hop >= maxHops) continue;
194
+
195
+ // 查询关联(作为 source)
196
+ const asSource = this.db.prepare(`
197
+ SELECT e.id, e.name, e.type, er.relation_type, er.weight
198
+ FROM entity_relations er
199
+ JOIN entities e ON er.target_id = e.id
200
+ WHERE er.source_id = ?
201
+ `).all(current.id) as Array<{ id: string; name: string; type: string; relation_type: string; weight: number }>;
202
+
203
+ // 查询关联(作为 target)
204
+ const asTarget = this.db.prepare(`
205
+ SELECT e.id, e.name, e.type, er.relation_type, er.weight
206
+ FROM entity_relations er
207
+ JOIN entities e ON er.source_id = e.id
208
+ WHERE er.target_id = ?
209
+ `).all(current.id) as Array<{ id: string; name: string; type: string; relation_type: string; weight: number }>;
210
+
211
+ const allRelations = [
212
+ ...asSource.map(r => ({ ...r, targetId: current.id, targetName: current.name })),
213
+ ...asTarget.map(r => ({ ...r, targetId: r.id, targetName: r.name, isReverse: true }))
214
+ ];
215
+
216
+ for (const rel of allRelations) {
217
+ if (!visited.has(rel.id) || (endEntity && rel.name === endEntity)) {
218
+ if (!visited.has(rel.id)) {
219
+ visited.add(rel.id);
220
+ queue.push({ id: rel.id, name: rel.name, type: rel.type, hop: current.hop + 1 });
221
+ nodes.push({ id: rel.id, name: rel.name, type: rel.type });
222
+ }
223
+
224
+ // 添加边
225
+ edges.push({
226
+ source: current.name,
227
+ target: rel.name,
228
+ type: rel.relation_type,
229
+ weight: rel.weight
230
+ });
231
+
232
+ // 如果找到终点,提前结束
233
+ if (endEntity && rel.name === endEntity) {
234
+ return { nodes, edges };
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ return { nodes, edges };
241
+ }
242
+ ```
243
+
244
+ **Step 2: 测试运行**
245
+
246
+ ```bash
247
+ npx ts-node -e "
248
+ import { EntityGraphService } from './src/services/entityGraphService.js';
249
+ const svc = new EntityGraphService();
250
+ svc.queryEntityGraph('技术', undefined, 2).then(r => console.log('Nodes:', r.nodes.length, 'Edges:', r.edges.length)).catch(e => console.error(e));
251
+ "
252
+ ```
253
+
254
+ **Step 3: 提交**
255
+
256
+ ```bash
257
+ git add src/services/entityGraphService.ts
258
+ git commit -m "feat: implement queryEntityGraph method with BFS"
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Task 5: 实现 getRelationStats 方法
264
+
265
+ **Files:**
266
+ - Modify: `src/services/entityGraphService.ts`
267
+
268
+ **Step 1: 实现 getRelationStats**
269
+
270
+ ```typescript
271
+ async getRelationStats(): Promise<RelationStats> {
272
+ // 关联最多的实体
273
+ const mostConnected = this.db.prepare(`
274
+ SELECT e.name as entity, COUNT(*) as count
275
+ FROM entity_relations er
276
+ JOIN entities e ON er.source_id = e.id OR er.target_id = e.id
277
+ GROUP BY e.id
278
+ ORDER BY count DESC
279
+ LIMIT 10
280
+ `).all() as Array<{ entity: string; count: number }>;
281
+
282
+ // 关系类型分布
283
+ const typeStats = this.db.prepare(`
284
+ SELECT relation_type, COUNT(*) as count
285
+ FROM entity_relations
286
+ GROUP BY relation_type
287
+ `).all() as Array<{ relation_type: string; count: number }>;
288
+
289
+ const relation_types: Record<string, number> = {};
290
+ for (const s of typeStats) {
291
+ relation_types[s.relation_type] = s.count;
292
+ }
293
+
294
+ // 总关系数
295
+ const total = this.db.prepare(`
296
+ SELECT COUNT(*) as count FROM entity_relations
297
+ `).get() as { count: number };
298
+
299
+ return {
300
+ most_connected: mostConnected,
301
+ relation_types,
302
+ total_relations: total.count
303
+ };
304
+ }
305
+ ```
306
+
307
+ **Step 2: 测试运行**
308
+
309
+ ```bash
310
+ npx ts-node -e "
311
+ import { EntityGraphService } from './src/services/entityGraphService.js';
312
+ const svc = new EntityGraphService();
313
+ svc.getRelationStats().then(r => console.log(JSON.stringify(r, null, 2))).catch(e => console.error(e));
314
+ "
315
+ ```
316
+
317
+ **Step 3: 提交**
318
+
319
+ ```bash
320
+ git add src/services/entityGraphService.ts
321
+ git commit -m "feat: implement getRelationStats method"
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Task 6: 添加 MCP 工具
327
+
328
+ **Files:**
329
+ - Modify: `src/mcp/tools.ts`
330
+
331
+ **Step 1: 添加 get_entity_relations**
332
+
333
+ ```typescript
334
+ {
335
+ name: 'get_entity_relations',
336
+ description: 'Get direct relations of an entity',
337
+ inputSchema: {
338
+ type: 'object',
339
+ properties: {
340
+ entity_name: { type: 'string', description: 'Entity name to query' }
341
+ },
342
+ required: ['entity_name']
343
+ }
344
+ },
345
+ ```
346
+
347
+ **Step 2: 添加 query_entity_graph**
348
+
349
+ ```typescript
350
+ {
351
+ name: 'query_entity_graph',
352
+ description: 'Query entity graph with multi-hop traversal',
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ start_entity: { type: 'string', description: 'Start entity name' },
357
+ end_entity: { type: 'string', description: 'End entity name (optional)' },
358
+ max_hops: { type: 'number', description: 'Max hops (default 2, max 5)', default: 2 }
359
+ },
360
+ required: ['start_entity']
361
+ }
362
+ },
363
+ ```
364
+
365
+ **Step 3: 添加 get_relation_stats**
366
+
367
+ ```typescript
368
+ {
369
+ name: 'get_relation_stats',
370
+ description: 'Get relationship statistics',
371
+ inputSchema: {
372
+ type: 'object',
373
+ properties: {}
374
+ }
375
+ },
376
+ ```
377
+
378
+ **Step 4: 实现工具处理函数**
379
+
380
+ 在工具处理逻辑中添加:
381
+
382
+ ```typescript
383
+ case 'get_entity_relations': {
384
+ const result = await entityGraphService.getEntityRelations(args.entity_name);
385
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
386
+ }
387
+
388
+ case 'query_entity_graph': {
389
+ const result = await entityGraphService.queryEntityGraph(
390
+ args.start_entity,
391
+ args.end_entity,
392
+ args.max_hops || 2
393
+ );
394
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
395
+ }
396
+
397
+ case 'get_relation_stats': {
398
+ const result = await entityGraphService.getRelationStats();
399
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
400
+ }
401
+ ```
402
+
403
+ **Step 5: 提交**
404
+
405
+ ```bash
406
+ git add src/mcp/tools.ts
407
+ git commit -m "feat: add entity graph MCP tools"
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Task 7: 实现 HTML 可视化生成器
413
+
414
+ **Files:**
415
+ - Modify: `src/services/entityGraphService.ts`
416
+
417
+ **Step 1: 添加 generateGraphHtml 方法**
418
+
419
+ ```typescript
420
+ generateGraphHtml(graph: GraphData): string {
421
+ const nodesJson = JSON.stringify(graph.nodes.map(n => ({
422
+ id: n.name,
423
+ group: n.type
424
+ })));
425
+
426
+ const linksJson = JSON.stringify(graph.edges.map(e => ({
427
+ source: e.source,
428
+ target: e.target,
429
+ type: e.type,
430
+ value: e.weight
431
+ })));
432
+
433
+ return `<!DOCTYPE html>
434
+ <html>
435
+ <head>
436
+ <meta charset="UTF-8">
437
+ <title>实体关系图 - ${graph.nodes.length} 节点</title>
438
+ <script src="https://d3js.org/d3.v7.min.js"></script>
439
+ <style>
440
+ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
441
+ #graph { width: 100vw; height: 100vh; }
442
+ .node { cursor: pointer; }
443
+ .node circle { stroke: #fff; stroke-width: 2px; }
444
+ .node text { font-size: 12px; }
445
+ .link { stroke: #999; stroke-opacity: 0.6; }
446
+ .tooltip { position: absolute; background: white; padding: 8px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
447
+ #info { position: absolute; top: 10px; left: 10px; background: white; padding: 10px; border-radius: 4px; }
448
+ </style>
449
+ </head>
450
+ <body>
451
+ <div id="info">
452
+ <h3>实体关系图</h3>
453
+ <p>节点: ${graph.nodes.length}</p>
454
+ <p>边: ${graph.edges.length}</p>
455
+ </div>
456
+ <div id="graph"></div>
457
+ <script>
458
+ const nodes = ${nodesJson};
459
+ const links = ${linksJson};
460
+
461
+ const width = window.innerWidth;
462
+ const height = window.innerHeight;
463
+
464
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
465
+
466
+ const simulation = d3.forceSimulation(nodes)
467
+ .force("link", d3.forceLink(links).id(d => d.id).distance(100))
468
+ .force("charge", d3.forceManyBody().strength(-300))
469
+ .force("center", d3.forceCenter(width / 2, height / 2));
470
+
471
+ const svg = d3.select("#graph").append("svg")
472
+ .attr("width", width)
473
+ .attr("height", height)
474
+ .call(d3.zoom().on("zoom", (event) => {
475
+ g.attr("transform", event.transform);
476
+ }));
477
+
478
+ const g = svg.append("g");
479
+
480
+ const link = g.append("g")
481
+ .selectAll("line")
482
+ .data(links)
483
+ .enter().append("line")
484
+ .attr("class", "link")
485
+ .attr("stroke-width", d => Math.sqrt(d.value) * 2);
486
+
487
+ const node = g.append("g")
488
+ .selectAll("g")
489
+ .data(nodes)
490
+ .enter().append("g")
491
+ .attr("class", "node")
492
+ .call(d3.drag()
493
+ .on("start", dragstarted)
494
+ .on("drag", dragged)
495
+ .on("end", dragended));
496
+
497
+ node.append("circle")
498
+ .attr("r", 15)
499
+ .attr("fill", d => color(d.group));
500
+
501
+ node.append("text")
502
+ .text(d => d.id)
503
+ .attr("x", 18)
504
+ .attr("y", 4);
505
+
506
+ node.on("click", (event, d) => {
507
+ const connected = links.filter(l => l.source.id === d.id || l.target.id === d.id);
508
+ alert("关联: " + connected.map(l => l.source.id + " - " + l.target.id).join(", "));
509
+ });
510
+
511
+ simulation.on("tick", () => {
512
+ link
513
+ .attr("x1", d => d.source.x)
514
+ .attr("y1", d => d.source.y)
515
+ .attr("x2", d => d.target.x)
516
+ .attr("y2", d => d.target.y);
517
+
518
+ node.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
519
+ });
520
+
521
+ function dragstarted(event, d) {
522
+ if (!event.active) simulation.alphaTarget(0.3).restart();
523
+ d.fx = d.x;
524
+ d.fy = d.y;
525
+ }
526
+
527
+ function dragged(event, d) {
528
+ d.fx = event.x;
529
+ d.fy = event.y;
530
+ }
531
+
532
+ function dragended(event, d) {
533
+ if (!event.active) simulation.alphaTarget(0);
534
+ d.fx = null;
535
+ d.fy = null;
536
+ }
537
+ </script>
538
+ </body>
539
+ </html>`;
540
+ }
541
+ ```
542
+
543
+ **Step 2: 添加 generateStatsHtml 方法**
544
+
545
+ ```typescript
546
+ generateStatsHtml(stats: RelationStats): string {
547
+ return `<!DOCTYPE html>
548
+ <html>
549
+ <head>
550
+ <meta charset="UTF-8">
551
+ <title>关系统计</title>
552
+ <style>
553
+ body { font-family: -apple-system, sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
554
+ h1, h2 { color: #333; }
555
+ .card { background: #f5f5f5; padding: 20px; margin: 10px 0; border-radius: 8px; }
556
+ .bar { background: #4a90d9; color: white; padding: 10px; margin: 5px 0; border-radius: 4px; }
557
+ table { width: 100%; border-collapse: collapse; }
558
+ th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
559
+ </style>
560
+ </head>
561
+ <body>
562
+ <h1>实体关系统计</h1>
563
+
564
+ <div class="card">
565
+ <h2>总关系数</h2>
566
+ <p style="font-size: 36px; font-weight: bold; color: #4a90d9;">${stats.total_relations}</p>
567
+ </div>
568
+
569
+ <div class="card">
570
+ <h2>关联最多的实体</h2>
571
+ <table>
572
+ <tr><th>实体</th><th>关联数</th></tr>
573
+ ${stats.most_connected.map(m => `<tr><td>${m.entity}</td><td>${m.count}</td></tr>`).join('')}
574
+ </table>
575
+ </div>
576
+
577
+ <div class="card">
578
+ <h2>关系类型分布</h2>
579
+ ${Object.entries(stats.relation_types).map(([type, count]) =>
580
+ `<div class="bar" style="width: ${count * 10}px">${type}: ${count}</div>`
581
+ ).join('')}
582
+ </div>
583
+ </body>
584
+ </html>`;
585
+ }
586
+ ```
587
+
588
+ **Step 3: 提交**
589
+
590
+ ```bash
591
+ git add src/services/entityGraphService.ts
592
+ git commit -m "feat: add HTML generators for entity graph visualization"
593
+ ```
594
+
595
+ ---
596
+
597
+ ## Task 8: 添加 CLI 命令
598
+
599
+ **Files:**
600
+ - Modify: `src/index.ts`
601
+
602
+ **Step 1: 导入 EntityGraphService**
603
+
604
+ ```typescript
605
+ import { EntityGraphService } from './services/entityGraphService.js';
606
+ import { writeFile } from 'fs/promises';
607
+ ```
608
+
609
+ **Step 2: 添加 relations 子命令**
610
+
611
+ ```typescript
612
+ .command('relations <action>')
613
+ .description('实体关系命令')
614
+ .option('-e, --entity <name>', '实体名称')
615
+ .option('-o, --output <file>', '输出文件')
616
+ .option('--hops <n>', '最大跳数', '2')
617
+ .action(async (action, options) => {
618
+ const svc = new EntityGraphService();
619
+
620
+ if (action === 'graph') {
621
+ const graph = await svc.queryEntityGraph(
622
+ options.entity || '技术',
623
+ undefined,
624
+ parseInt(options.hops)
625
+ );
626
+ const html = svc.generateGraphHtml(graph);
627
+ const output = options.output || 'entity-graph.html';
628
+ await writeFile(output, html);
629
+ console.log(`关系图已生成: ${output}`);
630
+ } else if (action === 'stats') {
631
+ const stats = await svc.getRelationStats();
632
+ const html = svc.generateStatsHtml(stats);
633
+ const output = options.output || 'relation-stats.html';
634
+ await writeFile(output, html);
635
+ console.log(`统计数据已生成: ${output}`);
636
+ }
637
+ });
638
+ ```
639
+
640
+ **Step 3: 测试编译**
641
+
642
+ ```bash
643
+ npm run build
644
+ ```
645
+
646
+ **Step 4: 提交**
647
+
648
+ ```bash
649
+ git add src/index.ts
650
+ git commit -m "feat: add relations CLI commands"
651
+ ```
652
+
653
+ ---
654
+
655
+ ## Task 9: 最终测试
656
+
657
+ **Step 1: 测试 CLI 命令**
658
+
659
+ ```bash
660
+ node dist/index.js relations graph -e "技术" --hops 2 -o /tmp/test-graph.html
661
+ ls -la /tmp/test-graph.html
662
+ ```
663
+
664
+ **Step 2: 测试 stats 命令**
665
+
666
+ ```bash
667
+ node dist/index.js relations stats -o /tmp/test-stats.html
668
+ ls -la /tmp/test-stats.html
669
+ ```
670
+
671
+ ---
672
+
673
+ ## Task 10: 合并到主分支
674
+
675
+ **Step 1: 切换到主分支并合并**
676
+
677
+ ```bash
678
+ git checkout main
679
+ git merge feature/entity-graph
680
+ git push origin main
681
+ ```
682
+
683
+ **Step 2: 删除功能分支(可选)**
684
+
685
+ ```bash
686
+ git branch -d feature/entity-graph
687
+ ```