@aalis/plugin-user-relation 0.4.1

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actions.d.ts +13 -0
  3. package/dist/actions.d.ts.map +1 -0
  4. package/dist/actions.js +705 -0
  5. package/dist/actions.js.map +1 -0
  6. package/dist/commands.d.ts +46 -0
  7. package/dist/commands.d.ts.map +1 -0
  8. package/dist/commands.js +601 -0
  9. package/dist/commands.js.map +1 -0
  10. package/dist/consolidate-llm.d.ts +150 -0
  11. package/dist/consolidate-llm.d.ts.map +1 -0
  12. package/dist/consolidate-llm.js +373 -0
  13. package/dist/consolidate-llm.js.map +1 -0
  14. package/dist/extractor.d.ts +308 -0
  15. package/dist/extractor.d.ts.map +1 -0
  16. package/dist/extractor.js +1356 -0
  17. package/dist/extractor.js.map +1 -0
  18. package/dist/index.d.ts +33 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +626 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/middleware.d.ts +41 -0
  23. package/dist/middleware.d.ts.map +1 -0
  24. package/dist/middleware.js +281 -0
  25. package/dist/middleware.js.map +1 -0
  26. package/dist/rename-watcher.d.ts +16 -0
  27. package/dist/rename-watcher.d.ts.map +1 -0
  28. package/dist/rename-watcher.js +16 -0
  29. package/dist/rename-watcher.js.map +1 -0
  30. package/dist/service.d.ts +1108 -0
  31. package/dist/service.d.ts.map +1 -0
  32. package/dist/service.js +4462 -0
  33. package/dist/service.js.map +1 -0
  34. package/dist/store.d.ts +89 -0
  35. package/dist/store.d.ts.map +1 -0
  36. package/dist/store.js +207 -0
  37. package/dist/store.js.map +1 -0
  38. package/dist/tools.d.ts +48 -0
  39. package/dist/tools.d.ts.map +1 -0
  40. package/dist/tools.js +1630 -0
  41. package/dist/tools.js.map +1 -0
  42. package/dist/types.d.ts +432 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +94 -0
  45. package/dist/types.js.map +1 -0
  46. package/dist/utils.d.ts +419 -0
  47. package/dist/utils.d.ts.map +1 -0
  48. package/dist/utils.js +1630 -0
  49. package/dist/utils.js.map +1 -0
  50. package/package.json +57 -0
@@ -0,0 +1,705 @@
1
+ import { scoreToTier } from './service.js';
2
+ /** 关系类型 → 中文显示(WebUI 渲染用;不影响存储里的英文 key) */
3
+ const RELATION_LABEL_ZH = {
4
+ // person-event role
5
+ initiator: '发起者',
6
+ participant: '参与者',
7
+ witness: '旁观者',
8
+ target: '被指向',
9
+ reporter: '转述者',
10
+ // person-entity role
11
+ enthusiast: '爱好者',
12
+ owner: '拥有者',
13
+ creator: '创作者',
14
+ critic: '批评者',
15
+ visitor: '访客',
16
+ mentioned: '仅提及',
17
+ // person-person relation
18
+ friend: '朋友',
19
+ cp: 'CP',
20
+ rival: '对手',
21
+ mentor: '师徒',
22
+ colleague: '同事',
23
+ familiar: '熟人',
24
+ antagonist: '对头',
25
+ admirer: '仰慕者',
26
+ // event-event relation
27
+ 'caused-by': '因→果',
28
+ follows: '随后',
29
+ 'part-of': '属于',
30
+ related: '相关',
31
+ // event-entity relation
32
+ about: '关于',
33
+ uses: '使用',
34
+ 'located-at': '位于',
35
+ produced: '产出',
36
+ // entity-entity relation
37
+ contains: '包含',
38
+ 'variant-of': '变体',
39
+ opposite: '对立',
40
+ // entity kind / sentiment(少量备用)
41
+ positive: '正向',
42
+ negative: '负向',
43
+ neutral: '中性',
44
+ mixed: '复杂',
45
+ };
46
+ function labelZh(raw) {
47
+ if (!raw)
48
+ return raw;
49
+ return RELATION_LABEL_ZH[raw] ?? raw;
50
+ }
51
+ function svc(ctx) {
52
+ return ctx.getService('user-relation');
53
+ }
54
+ function previewEvidence(e) {
55
+ if (e.evidence.length === 0)
56
+ return '';
57
+ const recent = [...e.evidence].sort((a, b) => b.extractedAt - a.extractedAt)[0];
58
+ return recent.quote
59
+ ? `「${recent.quote.slice(0, 30)}${recent.quote.length > 30 ? '…' : ''}」`
60
+ : `${recent.messageIds.length} 条证据`;
61
+ }
62
+ function formatDate(ts) {
63
+ return new Date(ts).toISOString().replace('T', ' ').slice(0, 16);
64
+ }
65
+ /** 给任意对象补充时间戳的可读字符串字段(保留原数字字段不动)。 */
66
+ function withReadableDates(o) {
67
+ const out = { ...o };
68
+ for (const key of ['firstSeenAt', 'lastSeenAt', 'lastReinforcedAt', 'lastMentionedAt']) {
69
+ const v = o[key];
70
+ if (typeof v === 'number' && Number.isFinite(v)) {
71
+ out[`${key}Text`] = formatDate(v);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ /** evidence 列表的统一展开(按时间倒序 + 附时间文本,保留 messageIds/quote/sessionId 原样)。 */
77
+ function expandEvidence(list) {
78
+ return [...list]
79
+ .sort((a, b) => b.extractedAt - a.extractedAt)
80
+ .map(ev => ({
81
+ quote: ev.quote,
82
+ messageIds: ev.messageIds,
83
+ sessionId: ev.sessionId,
84
+ extractedAt: ev.extractedAt,
85
+ extractedAtText: formatDate(ev.extractedAt),
86
+ }));
87
+ }
88
+ export const actions = {
89
+ // ───── 表格数据源 ─────
90
+ async listPersons(ctx) {
91
+ const s = svc(ctx);
92
+ if (!s)
93
+ return [];
94
+ const snap = await s.loadAll();
95
+ return snap.persons
96
+ .sort((a, b) => b.lastSeenAt - a.lastSeenAt)
97
+ .map((p) => ({
98
+ id: p.id,
99
+ platform: p.platform,
100
+ userId: p.userId,
101
+ displayName: p.displayName ?? '',
102
+ firstSeenAt: formatDate(p.firstSeenAt),
103
+ lastSeenAt: formatDate(p.lastSeenAt),
104
+ }));
105
+ },
106
+ async listEvents(ctx) {
107
+ const s = svc(ctx);
108
+ if (!s)
109
+ return [];
110
+ const snap = await s.loadAll();
111
+ return snap.events
112
+ .sort((a, b) => b.lastReinforcedAt - a.lastReinforcedAt)
113
+ .map((e) => ({
114
+ id: e.id,
115
+ title: e.title,
116
+ category: e.category ?? '',
117
+ sessionScope: e.sessionScope ?? 'global',
118
+ summary: e.summary ?? '',
119
+ evidenceCount: e.evidence.length,
120
+ preview: previewEvidence(e),
121
+ lastReinforcedAt: formatDate(e.lastReinforcedAt),
122
+ }));
123
+ },
124
+ async listEntities(ctx) {
125
+ const s = svc(ctx);
126
+ if (!s)
127
+ return [];
128
+ const snap = await s.loadAll();
129
+ return snap.entities
130
+ .sort((a, b) => b.lastReinforcedAt - a.lastReinforcedAt)
131
+ .map((e) => ({
132
+ id: e.id,
133
+ name: e.name,
134
+ entityKind: e.entityKind,
135
+ aliases: (e.aliases ?? []).join(', '),
136
+ summary: e.summary ?? '',
137
+ evidenceCount: e.evidence.length,
138
+ lastReinforcedAt: formatDate(e.lastReinforcedAt),
139
+ }));
140
+ },
141
+ // ───── 关系图(Cytoscape elements) ─────
142
+ async getRelationGraph(ctx, args) {
143
+ const s = svc(ctx);
144
+ if (!s)
145
+ return { nodes: [], edges: [] };
146
+ // 焦点可为 person(`platform:userId`,含冒号) / event / entity(UUID)。
147
+ // 不再以「包含冒号」来过滤——event/entity UUID 不含冒号也应被接受。
148
+ const focusIdRaw = typeof args.focusId === 'string' ? args.focusId.trim() : '';
149
+ const focusId = focusIdRaw || undefined;
150
+ const maxDepth = numArg(args.maxDepth, 2);
151
+ const maxBreadth = numArg(args.maxBreadth, 10);
152
+ let persons;
153
+ let events;
154
+ let entities;
155
+ let edges;
156
+ let focusEdge;
157
+ // 全图 snapshot:用于给每个返回节点附 compositeScore + tier(基于全图位置而非子图)。
158
+ const fullSnap = await s.loadAll();
159
+ if (focusId) {
160
+ // 先检测 focusId 是否为某条边的 id:若是 → 取边两端点作为起点 + 1 跳邻域
161
+ const edgeMatch = fullSnap.edges.find(e => e.id === focusId);
162
+ if (edgeMatch) {
163
+ focusEdge = edgeMatch;
164
+ const endpointIds = edgeEndpointIds(edgeMatch);
165
+ const sub = await s.traverseSubgraph({ startNodeIds: endpointIds, maxDepth, maxBreadth });
166
+ persons = sub.persons;
167
+ events = sub.events;
168
+ entities = sub.entities;
169
+ edges = sub.edges;
170
+ if (!edges.some(e => e.id === edgeMatch.id))
171
+ edges.push(edgeMatch);
172
+ }
173
+ else {
174
+ const sub = await s.traverseSubgraph({ startNodeIds: [focusId], maxDepth, maxBreadth });
175
+ persons = sub.persons;
176
+ events = sub.events;
177
+ entities = sub.entities;
178
+ edges = sub.edges;
179
+ }
180
+ }
181
+ else {
182
+ // 全图模式:直接全量返回,由前端 / 焦点功能进行筛选。
183
+ // 历史上这里做过「按 lastSeenAt 截断 + 过滤未触达事件/实体」的优化,
184
+ // 但会导致「person A 在图里但其关系对端 B 被截断 → A 看似孤儿」的视觉错觉
185
+ // (真正的孤儿在压缩阶段已由 pruneOrphans 删除)。压缩流程已收紧,全图直出更诚实。
186
+ persons = fullSnap.persons;
187
+ events = fullSnap.events;
188
+ entities = fullSnap.entities;
189
+ edges = fullSnap.edges;
190
+ }
191
+ const personLabel = (p) => p.displayName?.trim() || p.userId;
192
+ const truncate = (text, max) => (text.length > max ? `${text.slice(0, max)}…` : text);
193
+ // 给每个返回节点附 compositeScore + tier:用全图位置计算百分位,避免子图局部错觉。
194
+ const scoreOf = (id) => s._computeSingleNodeScore(id, fullSnap);
195
+ const allScored = [];
196
+ for (const p of fullSnap.persons) {
197
+ const sc = scoreOf(p.id);
198
+ if (sc)
199
+ allScored.push({ id: p.id, kind: 'person', score: sc.compositeScore });
200
+ }
201
+ for (const e of fullSnap.events) {
202
+ const sc = scoreOf(e.id);
203
+ if (sc)
204
+ allScored.push({ id: e.id, kind: 'event', score: sc.compositeScore });
205
+ }
206
+ for (const e of fullSnap.entities) {
207
+ const sc = scoreOf(e.id);
208
+ if (sc)
209
+ allScored.push({ id: e.id, kind: 'entity', score: sc.compositeScore });
210
+ }
211
+ const tierByNodeId = new Map();
212
+ for (const kind of ['person', 'event', 'entity']) {
213
+ const sameKind = allScored.filter(s => s.kind === kind).sort((a, b) => b.score - a.score);
214
+ for (let i = 0; i < sameKind.length; i++) {
215
+ const rank = i + 1;
216
+ const percentile = sameKind.length > 1 ? (sameKind.length - rank) / (sameKind.length - 1) : 1;
217
+ tierByNodeId.set(sameKind[i].id, {
218
+ compositeScore: sameKind[i].score,
219
+ tier: scoreToTier(sameKind[i].score, percentile),
220
+ });
221
+ }
222
+ }
223
+ const getScoreFields = (id) => {
224
+ const t = tierByNodeId.get(id);
225
+ return t ? { compositeScore: t.compositeScore, tier: t.tier } : { compositeScore: undefined, tier: undefined };
226
+ };
227
+ const globalPrRanks = {};
228
+ {
229
+ const prEntries = [];
230
+ for (const p of fullSnap.persons) {
231
+ if (typeof p.lastPageRank === 'number' && Number.isFinite(p.lastPageRank))
232
+ prEntries.push({ id: p.id, kind: 'person', pr: p.lastPageRank });
233
+ }
234
+ for (const e of fullSnap.events) {
235
+ if (typeof e.lastPageRank === 'number' && Number.isFinite(e.lastPageRank))
236
+ prEntries.push({ id: e.id, kind: 'event', pr: e.lastPageRank });
237
+ }
238
+ for (const e of fullSnap.entities) {
239
+ if (typeof e.lastPageRank === 'number' && Number.isFinite(e.lastPageRank))
240
+ prEntries.push({ id: e.id, kind: 'entity', pr: e.lastPageRank });
241
+ }
242
+ const globalSorted = [...prEntries].sort((a, b) => b.pr - a.pr);
243
+ const globalTotal = globalSorted.length;
244
+ const globalRankById = new Map();
245
+ for (let i = 0; i < globalSorted.length; i++)
246
+ globalRankById.set(globalSorted[i].id, i + 1);
247
+ const byKind = new Map();
248
+ for (const e of prEntries) {
249
+ const arr = byKind.get(e.kind) ?? [];
250
+ arr.push(e);
251
+ byKind.set(e.kind, arr);
252
+ }
253
+ for (const [, arr] of byKind) {
254
+ arr.sort((a, b) => b.pr - a.pr);
255
+ for (let i = 0; i < arr.length; i++) {
256
+ globalPrRanks[arr[i].id] = {
257
+ kindRank: i + 1,
258
+ kindTotal: arr.length,
259
+ globalRank: globalRankById.get(arr[i].id) ?? 0,
260
+ globalTotal,
261
+ };
262
+ }
263
+ }
264
+ }
265
+ // 防御:过滤掉引用了缺失节点的“幽灵边”——避免前端 cytoscape 抛
266
+ // “Can not create edge with nonexistent source/target”导致整个图黑屏。
267
+ // 历史上某些级联删除/合并路径可能漏清边;这里只做防御性兜底,不修复存储。
268
+ {
269
+ const validNodeIds = new Set();
270
+ for (const p of persons)
271
+ validNodeIds.add(p.id);
272
+ for (const e of events)
273
+ validNodeIds.add(e.id);
274
+ for (const e of entities)
275
+ validNodeIds.add(e.id);
276
+ const before = edges.length;
277
+ const kept = [];
278
+ const danglingPairs = [];
279
+ for (const e of edges) {
280
+ const [src, tgt] = edgeEndpointIds(e);
281
+ if (validNodeIds.has(src) && validNodeIds.has(tgt)) {
282
+ kept.push(e);
283
+ }
284
+ else {
285
+ danglingPairs.push(`${e.id}(${src}→${tgt})`);
286
+ }
287
+ }
288
+ if (danglingPairs.length > 0) {
289
+ const logger = ctx.logger;
290
+ const sample = danglingPairs.slice(0, 5).join(', ');
291
+ const more = danglingPairs.length > 5 ? `,… 共 ${danglingPairs.length} 条` : '';
292
+ logger?.warn(`[user-relation] getRelationGraph: 跳过 ${danglingPairs.length}/${before} 条幽灵边(节点缺失): ${sample}${more}`);
293
+ }
294
+ edges = kept;
295
+ }
296
+ return {
297
+ focusId,
298
+ focusEdge: focusEdge
299
+ ? {
300
+ id: focusEdge.id,
301
+ kind: focusEdge.kind,
302
+ weight: focusEdge.weight,
303
+ description: focusEdge.description,
304
+ firstSeenAt: focusEdge.firstSeenAt,
305
+ lastReinforcedAt: focusEdge.lastReinforcedAt,
306
+ evidence: focusEdge.evidence,
307
+ endpoints: edgeEndpointIds(focusEdge),
308
+ // 按 kind 暴露的额外语义字段(在前端面板里展示)
309
+ relation: focusEdge.kind === 'person-event' || focusEdge.kind === 'person-entity'
310
+ ? undefined
311
+ : focusEdge.relationType,
312
+ role: focusEdge.kind === 'person-event' || focusEdge.kind === 'person-entity' ? focusEdge.role : undefined,
313
+ sentiment: focusEdge.kind === 'person-event' || focusEdge.kind === 'person-entity' ? focusEdge.sentiment : undefined,
314
+ directed: focusEdge.kind === 'person-event' || focusEdge.kind === 'person-entity' ? undefined : focusEdge.directed,
315
+ }
316
+ : undefined,
317
+ stats: {
318
+ persons: persons.length,
319
+ events: events.length,
320
+ entities: entities.length,
321
+ edges: edges.length,
322
+ },
323
+ globalPrRanks,
324
+ nodes: [
325
+ ...persons.map(p => ({
326
+ data: {
327
+ id: p.id,
328
+ label: personLabel(p),
329
+ kind: 'person',
330
+ platform: p.platform,
331
+ userId: p.userId,
332
+ displayName: p.displayName,
333
+ lastPageRank: p.lastPageRank,
334
+ ...getScoreFields(p.id),
335
+ },
336
+ })),
337
+ ...events.map(e => ({
338
+ data: {
339
+ id: e.id,
340
+ label: truncate(e.title, 18),
341
+ kind: 'event',
342
+ category: e.category,
343
+ title: e.title,
344
+ summary: e.summary,
345
+ sessionScope: e.sessionScope ?? 'global',
346
+ lastPageRank: e.lastPageRank,
347
+ ...getScoreFields(e.id),
348
+ },
349
+ })),
350
+ ...entities.map(e => ({
351
+ data: {
352
+ id: e.id,
353
+ label: truncate(e.name, 16),
354
+ kind: 'entity',
355
+ entityKind: e.entityKind,
356
+ name: e.name,
357
+ summary: e.summary,
358
+ lastPageRank: e.lastPageRank,
359
+ ...getScoreFields(e.id),
360
+ },
361
+ })),
362
+ ],
363
+ edges: edges.map(e => {
364
+ if (e.kind === 'person-event') {
365
+ return {
366
+ data: {
367
+ id: e.id,
368
+ source: e.fromPersonId,
369
+ target: e.toEventId,
370
+ kind: 'person-event',
371
+ label: labelZh(e.role),
372
+ role: e.role,
373
+ weight: e.weight,
374
+ sentiment: e.sentiment,
375
+ description: e.description,
376
+ },
377
+ };
378
+ }
379
+ if (e.kind === 'person-entity') {
380
+ return {
381
+ data: {
382
+ id: e.id,
383
+ source: e.fromPersonId,
384
+ target: e.toEntityId,
385
+ kind: 'person-entity',
386
+ label: labelZh(e.role),
387
+ role: e.role,
388
+ weight: e.weight,
389
+ sentiment: e.sentiment,
390
+ description: e.description,
391
+ },
392
+ };
393
+ }
394
+ if (e.kind === 'event-event') {
395
+ return {
396
+ data: {
397
+ id: e.id,
398
+ source: e.fromEventId,
399
+ target: e.toEventId,
400
+ kind: 'event-event',
401
+ label: labelZh(e.relationType),
402
+ relationType: e.relationType,
403
+ directed: e.directed,
404
+ weight: e.weight,
405
+ description: e.description,
406
+ },
407
+ };
408
+ }
409
+ if (e.kind === 'event-entity') {
410
+ return {
411
+ data: {
412
+ id: e.id,
413
+ source: e.fromEventId,
414
+ target: e.toEntityId,
415
+ kind: 'event-entity',
416
+ label: labelZh(e.relationType),
417
+ relationType: e.relationType,
418
+ directed: true,
419
+ weight: e.weight,
420
+ description: e.description,
421
+ },
422
+ };
423
+ }
424
+ if (e.kind === 'entity-entity') {
425
+ return {
426
+ data: {
427
+ id: e.id,
428
+ source: e.fromEntityId,
429
+ target: e.toEntityId,
430
+ kind: 'entity-entity',
431
+ label: labelZh(e.relationType),
432
+ relationType: e.relationType,
433
+ directed: e.directed,
434
+ weight: e.weight,
435
+ description: e.description,
436
+ },
437
+ };
438
+ }
439
+ return {
440
+ data: {
441
+ id: e.id,
442
+ source: e.fromPersonId,
443
+ target: e.toPersonId,
444
+ kind: 'person-person',
445
+ label: labelZh(e.relationType),
446
+ relationType: e.relationType,
447
+ directed: e.directed,
448
+ weight: e.weight,
449
+ description: e.description,
450
+ },
451
+ };
452
+ }),
453
+ };
454
+ },
455
+ async getGraphNodeDetail(ctx, args) {
456
+ const s = svc(ctx);
457
+ if (!s)
458
+ return { error: 'service 不可用' };
459
+ const nodeId = String(args.nodeId ?? '');
460
+ const kind = String(args.kind ?? '');
461
+ if (kind === 'person') {
462
+ if (!nodeId.includes(':'))
463
+ return { error: '无效 personId' };
464
+ const nb = await s.getNeighborhood(nodeId);
465
+ return {
466
+ person: withReadableDates(nb.person),
467
+ eventCount: nb.events.length,
468
+ edgeCount: nb.edges.length,
469
+ recentEvents: nb.events.slice(0, 5).map(e => withReadableDates({
470
+ id: e.id,
471
+ title: e.title,
472
+ category: e.category,
473
+ summary: e.summary,
474
+ weight: e.weight,
475
+ mentionCount: e.mentionCount,
476
+ firstSeenAt: e.firstSeenAt,
477
+ lastReinforcedAt: e.lastReinforcedAt,
478
+ lastMentionedAt: e.lastMentionedAt,
479
+ evidenceCount: e.evidence.length,
480
+ evidencePreview: previewEvidence(e),
481
+ })),
482
+ edges: nb.edges.slice(0, 10).map(e => {
483
+ // 通用骨架:把所有边都 spread 出来,并补可读时间 + 中文 role/relation
484
+ const base = withReadableDates({
485
+ ...e,
486
+ evidenceCount: e.evidence.length,
487
+ evidencePreview: previewEvidence(e),
488
+ });
489
+ if (e.kind === 'person-event' || e.kind === 'person-entity') {
490
+ base.roleZh = labelZh(e.role);
491
+ }
492
+ else if ('relationType' in e && e.relationType) {
493
+ base.relationZh = labelZh(e.relationType);
494
+ }
495
+ return base;
496
+ }),
497
+ };
498
+ }
499
+ if (kind === 'event') {
500
+ const e = await s.getEvent(nodeId);
501
+ if (!e)
502
+ return { error: '事件不存在' };
503
+ return {
504
+ ...withReadableDates(e),
505
+ evidenceCount: e.evidence.length,
506
+ evidence: expandEvidence(e.evidence),
507
+ };
508
+ }
509
+ if (kind === 'entity') {
510
+ const e = await s.getEntity(nodeId);
511
+ if (!e)
512
+ return { error: '实体不存在' };
513
+ return {
514
+ ...withReadableDates(e),
515
+ aliases: e.aliases ?? [],
516
+ evidenceCount: e.evidence.length,
517
+ evidence: expandEvidence(e.evidence),
518
+ };
519
+ }
520
+ return { error: `未知 kind: ${kind}` };
521
+ },
522
+ // ───── stat / info ─────
523
+ async getStats(ctx) {
524
+ const s = svc(ctx);
525
+ if (!s)
526
+ return { value: 0 };
527
+ const snap = await s.loadAll();
528
+ const pe = snap.edges.filter(e => e.kind === 'person-event').length;
529
+ const pp = snap.edges.filter(e => e.kind === 'person-person').length;
530
+ const pent = snap.edges.filter(e => e.kind === 'person-entity').length;
531
+ const ee = snap.edges.filter(e => e.kind === 'event-event').length;
532
+ const eent = snap.edges.filter(e => e.kind === 'event-entity').length;
533
+ const entent = snap.edges.filter(e => e.kind === 'entity-entity').length;
534
+ return {
535
+ value: snap.persons.length,
536
+ detail: `人物 ${snap.persons.length} / 事件 ${snap.events.length} / 实体 ${snap.entities.length} / 人-事 ${pe} / 人-人 ${pp} / 人-实体 ${pent} / 事-事 ${ee} / 事-实体 ${eent} / 实体-实体 ${entent}`,
537
+ };
538
+ },
539
+ // ───── 详情 ─────
540
+ async getPerson(ctx, args) {
541
+ const s = svc(ctx);
542
+ if (!s)
543
+ return { error: 'service 不可用' };
544
+ const id = String(args.id ?? '');
545
+ const [platform = '', userId = ''] = id.split(':');
546
+ if (!platform || !userId)
547
+ return { error: '无效 personId' };
548
+ const nb = await s.getNeighborhood(id);
549
+ return {
550
+ person: nb.person,
551
+ events: nb.events,
552
+ edges: nb.edges,
553
+ };
554
+ },
555
+ async getEvent(ctx, args) {
556
+ const s = svc(ctx);
557
+ if (!s)
558
+ return { error: 'service 不可用' };
559
+ const e = await s.getEvent(String(args.id ?? ''));
560
+ if (!e)
561
+ return { error: '事件不存在' };
562
+ return e;
563
+ },
564
+ // ───── 操作类 ─────
565
+ async deletePerson(ctx, args) {
566
+ const s = svc(ctx);
567
+ if (!s)
568
+ return { error: 'service 不可用' };
569
+ const id = String(args.id ?? '');
570
+ const [platform = '', userId = ''] = id.split(':');
571
+ if (!platform || !userId)
572
+ return { error: '无效 personId' };
573
+ await s.deletePerson(platform, userId);
574
+ return { ok: true };
575
+ },
576
+ async deleteEvent(ctx, args) {
577
+ const s = svc(ctx);
578
+ if (!s)
579
+ return { error: 'service 不可用' };
580
+ await s.deleteEvent(String(args.id ?? ''));
581
+ return { ok: true };
582
+ },
583
+ async deleteEntity(ctx, args) {
584
+ const s = svc(ctx);
585
+ if (!s)
586
+ return { error: 'service 不可用' };
587
+ await s.deleteEntity(String(args.id ?? ''));
588
+ return { ok: true };
589
+ },
590
+ async deleteEdge(ctx, args) {
591
+ const s = svc(ctx);
592
+ if (!s)
593
+ return { error: 'service 不可用' };
594
+ await s.deleteEdge(String(args.id ?? ''));
595
+ return { ok: true };
596
+ },
597
+ async triggerExtraction(ctx, args) {
598
+ const s = svc(ctx);
599
+ if (!s)
600
+ return { error: 'service 不可用' };
601
+ const sessionId = String(args.sessionId ?? '').trim();
602
+ if (!sessionId)
603
+ return { error: '请输入 sessionId' };
604
+ return s.triggerExtraction(sessionId);
605
+ },
606
+ // ───── 多层查询(webui view + 调试用,参数走 view.* 范畴的默认值/上限由 index.ts 注入) ─────
607
+ async expandPerson(ctx, args) {
608
+ const s = svc(ctx);
609
+ if (!s)
610
+ return { error: 'service 不可用' };
611
+ const personId = String(args.personId ?? args.id ?? '').trim();
612
+ if (!personId.includes(':'))
613
+ return { error: 'personId 格式应为 platform:userId' };
614
+ const maxDepth = numArg(args.maxDepth, 2);
615
+ const maxBreadth = numArg(args.maxBreadth, 10);
616
+ const sub = await s.traverseSubgraph({
617
+ startNodeIds: [personId],
618
+ maxDepth,
619
+ maxBreadth,
620
+ });
621
+ return {
622
+ personId,
623
+ maxDepth,
624
+ maxBreadth,
625
+ stats: {
626
+ persons: sub.persons.length,
627
+ events: sub.events.length,
628
+ edges: sub.edges.length,
629
+ },
630
+ persons: sub.persons,
631
+ events: sub.events,
632
+ edges: sub.edges,
633
+ };
634
+ },
635
+ async findPath(ctx, args) {
636
+ const s = svc(ctx);
637
+ if (!s)
638
+ return { error: 'service 不可用' };
639
+ const from = String(args.fromPersonId ?? args.from ?? '').trim();
640
+ const to = String(args.toPersonId ?? args.to ?? '').trim();
641
+ if (!from.includes(':') || !to.includes(':'))
642
+ return { error: 'person id 格式应为 platform:userId' };
643
+ const maxDepth = numArg(args.maxDepth, 3);
644
+ const path = await s.findPath(from, to, maxDepth);
645
+ if (!path)
646
+ return { found: false, from, to, maxDepth };
647
+ return {
648
+ found: true,
649
+ length: path.edges.length,
650
+ nodes: path.nodes,
651
+ edges: path.edges,
652
+ };
653
+ },
654
+ async searchEvents(ctx, args) {
655
+ const s = svc(ctx);
656
+ if (!s)
657
+ return { error: 'service 不可用' };
658
+ const keyword = typeof args.keyword === 'string' ? args.keyword : undefined;
659
+ const days = numArgOptional(args.days);
660
+ const limit = numArg(args.limit, 20);
661
+ const events = await s.searchEvents({ keyword, days, limit });
662
+ return {
663
+ count: events.length,
664
+ events,
665
+ };
666
+ },
667
+ };
668
+ function numArg(v, fallback) {
669
+ if (typeof v === 'number' && Number.isFinite(v))
670
+ return v;
671
+ if (typeof v === 'string' && v.trim() !== '') {
672
+ const n = Number(v);
673
+ if (Number.isFinite(n))
674
+ return n;
675
+ }
676
+ return fallback;
677
+ }
678
+ function numArgOptional(v) {
679
+ if (typeof v === 'number' && Number.isFinite(v))
680
+ return v;
681
+ if (typeof v === 'string' && v.trim() !== '') {
682
+ const n = Number(v);
683
+ if (Number.isFinite(n))
684
+ return n;
685
+ }
686
+ return undefined;
687
+ }
688
+ /** 给定一条边,返回它的两个端点 id(统一为字符串数组) */
689
+ function edgeEndpointIds(e) {
690
+ switch (e.kind) {
691
+ case 'person-event':
692
+ return [e.fromPersonId, e.toEventId];
693
+ case 'person-person':
694
+ return [e.fromPersonId, e.toPersonId];
695
+ case 'person-entity':
696
+ return [e.fromPersonId, e.toEntityId];
697
+ case 'event-event':
698
+ return [e.fromEventId, e.toEventId];
699
+ case 'event-entity':
700
+ return [e.fromEventId, e.toEntityId];
701
+ case 'entity-entity':
702
+ return [e.fromEntityId, e.toEntityId];
703
+ }
704
+ }
705
+ //# sourceMappingURL=actions.js.map