@aalis/plugin-user-relation 0.11.0 → 0.12.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/dist/consolidate-llm.d.ts.map +1 -1
- package/dist/consolidate-llm.js +2 -0
- package/dist/consolidate-llm.js.map +1 -1
- package/dist/extractor.d.ts.map +1 -1
- package/dist/extractor.js +3 -0
- package/dist/extractor.js.map +1 -1
- package/dist/service.d.ts +9 -5
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +87 -36
- package/dist/service.js.map +1 -1
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -37,6 +37,31 @@ export class RelationService {
|
|
|
37
37
|
else
|
|
38
38
|
console.warn(msg);
|
|
39
39
|
}
|
|
40
|
+
// ─── 派生回写统一写口(落笔核实况)─────────────────────────────
|
|
41
|
+
// consolidate 的批量范式(决策与应用共用 pass 开头快照)与「真合并物理删节点」
|
|
42
|
+
// 组合出过一类事故:真合并删除节点后,同 pass 后续阶段拿快照旧拷贝做补丁式回写
|
|
43
|
+
// (embedding hash / summary / PageRank),把死节点整个写回(复活)。僵尸节点使
|
|
44
|
+
// consolidate 每轮重判同一批对、反复铸 part-of 边(2026-08「表情包星形」事故)。
|
|
45
|
+
// 凡「以快照旧拷贝为基底 spread + 补丁」的节点回写一律走这两个写口:
|
|
46
|
+
// 1. 落笔前单点核实节点在库中仍存活,死了就跳过——「删了就是删了」由此机器保证;
|
|
47
|
+
// 2. 以**库中活文档**为基底套补丁(而非旧拷贝)——否则回写还会用陈旧字段
|
|
48
|
+
// 压掉并发合并刚并入的 aliases 等新状态。
|
|
49
|
+
// 豁免路径:facade 新建/强化、renameNode 等「先单点读活文档再改写」的读改写路径;
|
|
50
|
+
// evictByQuota 内 decay 回写(其快照在本函数自身删除动作之前 fresh 加载,无复活窗)。
|
|
51
|
+
async writeBackEntityIfLive(node, patch) {
|
|
52
|
+
const live = await this.store.getEntity(node.id);
|
|
53
|
+
if (!live)
|
|
54
|
+
return false;
|
|
55
|
+
await this.store.upsertEntity({ ...live, ...patch });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
async writeBackEventIfLive(node, patch) {
|
|
59
|
+
const live = await this.store.getEvent(node.id);
|
|
60
|
+
if (!live)
|
|
61
|
+
return false;
|
|
62
|
+
await this.store.upsertEvent({ ...live, ...patch });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
40
65
|
/** 查询最近一次 consolidation 运行时间、触发源与结果摘要 */
|
|
41
66
|
getLastConsolidateInfo() {
|
|
42
67
|
return {
|
|
@@ -1287,8 +1312,8 @@ export class RelationService {
|
|
|
1287
1312
|
const cid = list && list.length > 0 ? list[0].id : undefined;
|
|
1288
1313
|
if (score === undefined && cid === undefined)
|
|
1289
1314
|
continue;
|
|
1290
|
-
|
|
1291
|
-
|
|
1315
|
+
// 派生回写走活性写口(after 虽为 fresh 加载,统一收口以防未来插入删除动作后复活)
|
|
1316
|
+
await this.writeBackEventIfLive(ev, {
|
|
1292
1317
|
...(score !== undefined ? { lastPageRank: score, lastPageRankAt: now } : {}),
|
|
1293
1318
|
...(cid !== undefined ? { communityId: cid, communityIdAt: now, communityMemberships: list } : {}),
|
|
1294
1319
|
});
|
|
@@ -1299,8 +1324,7 @@ export class RelationService {
|
|
|
1299
1324
|
const cid = list && list.length > 0 ? list[0].id : undefined;
|
|
1300
1325
|
if (score === undefined && cid === undefined)
|
|
1301
1326
|
continue;
|
|
1302
|
-
await this.
|
|
1303
|
-
...en,
|
|
1327
|
+
await this.writeBackEntityIfLive(en, {
|
|
1304
1328
|
...(score !== undefined ? { lastPageRank: score, lastPageRankAt: now } : {}),
|
|
1305
1329
|
...(cid !== undefined ? { communityId: cid, communityIdAt: now, communityMemberships: list } : {}),
|
|
1306
1330
|
});
|
|
@@ -2104,10 +2128,14 @@ export class RelationService {
|
|
|
2104
2128
|
shouldMerge = false;
|
|
2105
2129
|
if (v.hierarchy) {
|
|
2106
2130
|
const { parentId, childId } = v.hierarchy;
|
|
2107
|
-
const
|
|
2131
|
+
const partOfOutcome = await this._ensureHierarchyEdge(childId, parentId, `consolidate LLM 判定 hierarchy:${v.reason}`);
|
|
2108
2132
|
if (opts.llm.ctx.logger) {
|
|
2109
|
-
|
|
2110
|
-
|
|
2133
|
+
const outcomeText = partOfOutcome === 'built'
|
|
2134
|
+
? '(已新建 part-of 边)'
|
|
2135
|
+
: partOfOutcome === 'exists'
|
|
2136
|
+
? '(part-of 边已存在)'
|
|
2137
|
+
: '(端点已被合并,跳过落边)';
|
|
2138
|
+
opts.llm.ctx.logger.info(`[user-relation] consolidate LLM 判定 hierarchy ${childId} part-of ${parentId}: ${v.reason}${outcomeText}`);
|
|
2111
2139
|
}
|
|
2112
2140
|
}
|
|
2113
2141
|
else if (opts.llm.ctx.logger) {
|
|
@@ -2222,8 +2250,15 @@ export class RelationService {
|
|
|
2222
2250
|
embedCache.set(en.id, null);
|
|
2223
2251
|
return null;
|
|
2224
2252
|
}
|
|
2253
|
+
// 落笔核实况:en 是 pass 快照旧拷贝,节点可能已被本 pass 真合并删除——
|
|
2254
|
+
// 死节点不写回(防复活)、不参与相似度召回,也**不写向量**:向量命名空间
|
|
2255
|
+
// 只有按节点 key 的级联删除,死 uuid 的向量一旦写入永不可回收(~96KB/条)。
|
|
2256
|
+
// hash 先于向量落盘的窗口由自愈路径兜底(hash 命中但取不到向量 → 重算)。
|
|
2257
|
+
if (!(await this.writeBackEntityIfLive(en, { embeddingHash: expectedHash }))) {
|
|
2258
|
+
embedCache.set(en.id, null);
|
|
2259
|
+
return null;
|
|
2260
|
+
}
|
|
2225
2261
|
await this.store.upsertVector('entity', en.id, vec, expectedHash);
|
|
2226
|
-
await this.store.upsertEntity({ ...en, embeddingHash: expectedHash });
|
|
2227
2262
|
en.embeddingHash = expectedHash;
|
|
2228
2263
|
embedCache.set(en.id, vec);
|
|
2229
2264
|
return vec;
|
|
@@ -2462,10 +2497,14 @@ export class RelationService {
|
|
|
2462
2497
|
// 避免母概念(如「三角洲行动」)被并入子概念(如「三角洲行动·绝密航天」)。
|
|
2463
2498
|
if (v.hierarchy) {
|
|
2464
2499
|
const { parentId, childId } = v.hierarchy;
|
|
2465
|
-
const
|
|
2500
|
+
const partOfOutcome = await this._ensureHierarchyEdge(childId, parentId, `consolidate LLM 判定 hierarchy:${v.reason}`);
|
|
2466
2501
|
if (opts.llm.ctx.logger) {
|
|
2467
|
-
|
|
2468
|
-
|
|
2502
|
+
const outcomeText = partOfOutcome === 'built'
|
|
2503
|
+
? '(已新建 part-of 边)'
|
|
2504
|
+
: partOfOutcome === 'exists'
|
|
2505
|
+
? '(part-of 边已存在)'
|
|
2506
|
+
: '(端点已被合并,跳过落边)';
|
|
2507
|
+
opts.llm.ctx.logger.info(`[user-relation] consolidate LLM 判定 hierarchy(宽召回)${childId} part-of ${parentId}: ${v.reason}${outcomeText}`);
|
|
2469
2508
|
}
|
|
2470
2509
|
}
|
|
2471
2510
|
else if (opts.llm.ctx.logger) {
|
|
@@ -2564,9 +2603,15 @@ export class RelationService {
|
|
|
2564
2603
|
}
|
|
2565
2604
|
}
|
|
2566
2605
|
}
|
|
2606
|
+
// ─── 旧账整理共用快照刷新:(3)/(3b)/(3c) 一律基于**合并后的实况**重载 ───
|
|
2607
|
+
// 旧形态吃 pass 开头快照,而 (1)/(1.5) 真合并已「保 id 改端点」重写/删除了边——
|
|
2608
|
+
// 按旧端点重建 dedup key 会「删活边 + 把死端点写回」(对抗审计端到端复现:
|
|
2609
|
+
// 事件对已删实体 X 的 part-of/about 两条边被折叠成指向死 X 的悬空边,
|
|
2610
|
+
// 活的 事件→canonical 挂载净丢失,随后被 pruneOrphans 当幽灵边清掉)。
|
|
2611
|
+
const normSnapshot = await this.store.loadAll();
|
|
2567
2612
|
// ─── (3) PersonEventEdge 旧账整理:对每对 (person,event) 跑一次吸收规则
|
|
2568
2613
|
const pairs = new Map();
|
|
2569
|
-
for (const e of
|
|
2614
|
+
for (const e of normSnapshot.edges) {
|
|
2570
2615
|
if (e.kind !== 'person-event')
|
|
2571
2616
|
continue;
|
|
2572
2617
|
const k = `${e.fromPersonId}|${e.toEventId}`;
|
|
@@ -2599,7 +2644,7 @@ export class RelationService {
|
|
|
2599
2644
|
// ─── (3b) PersonEntityEdge 旧账整理:同一 (person,entity) 多条不同 role 行 → 留最强 role
|
|
2600
2645
|
// 存量里可能有未做 (from,to) 级别去重的边;此处按当前 addPersonEntityEdge 规则修旧账。
|
|
2601
2646
|
const peEntityPairs = new Map();
|
|
2602
|
-
for (const e of
|
|
2647
|
+
for (const e of normSnapshot.edges) {
|
|
2603
2648
|
if (e.kind !== 'person-entity')
|
|
2604
2649
|
continue;
|
|
2605
2650
|
const k = `${e.fromPersonId}|${e.toEntityId}`;
|
|
@@ -2652,7 +2697,7 @@ export class RelationService {
|
|
|
2652
2697
|
// · 若同强度有多条 → 取 weight 最大者
|
|
2653
2698
|
// · weight 取所有被合并边的最大值(保留强化记录)
|
|
2654
2699
|
const eePairs = new Map();
|
|
2655
|
-
for (const e of
|
|
2700
|
+
for (const e of normSnapshot.edges) {
|
|
2656
2701
|
if (e.kind !== 'event-entity')
|
|
2657
2702
|
continue;
|
|
2658
2703
|
const k = `${e.fromEventId}|${e.toEntityId}`;
|
|
@@ -2750,8 +2795,10 @@ export class RelationService {
|
|
|
2750
2795
|
relatedPersons: relatedPersons.slice(0, 8),
|
|
2751
2796
|
}, llmDisableThinking);
|
|
2752
2797
|
if (newSummary && newSummary !== ent.summary) {
|
|
2753
|
-
|
|
2754
|
-
|
|
2798
|
+
// 落笔核实况:ent 来自阶段快照,可能已被真合并删除(死节点不复活)
|
|
2799
|
+
if (await this.writeBackEntityIfLive(ent, { summary: newSummary, lastReinforcedAt: Date.now() })) {
|
|
2800
|
+
summariesRewritten++;
|
|
2801
|
+
}
|
|
2755
2802
|
}
|
|
2756
2803
|
}
|
|
2757
2804
|
}
|
|
@@ -3101,7 +3148,11 @@ export class RelationService {
|
|
|
3101
3148
|
}
|
|
3102
3149
|
// 写回持久化:向量入独立命名空间,节点只更新 hash
|
|
3103
3150
|
await this.store.upsertVector('event', ev.id, vec, expectedHash);
|
|
3104
|
-
|
|
3151
|
+
// 落笔核实况:ev 是 pass 快照旧拷贝,死节点不写回(防复活)、不参与召回
|
|
3152
|
+
if (!(await this.writeBackEventIfLive(ev, { embeddingHash: expectedHash }))) {
|
|
3153
|
+
eventVecCache.set(ev.id, null);
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3105
3156
|
ev.embeddingHash = expectedHash;
|
|
3106
3157
|
embeddedCount++;
|
|
3107
3158
|
eventVecCache.set(ev.id, vec);
|
|
@@ -3578,9 +3629,10 @@ export class RelationService {
|
|
|
3578
3629
|
}
|
|
3579
3630
|
}
|
|
3580
3631
|
catch (err) {
|
|
3581
|
-
// 真合并失败不应阻塞调用链(边已经 rewire 成功,最差情况退化为旧的"路由+壳子"
|
|
3582
|
-
//
|
|
3583
|
-
|
|
3632
|
+
// 真合并失败不应阻塞调用链(边已经 rewire 成功,最差情况退化为旧的"路由+壳子"行为)。
|
|
3633
|
+
// 必须走 _audit 进日志文件:这条曾是 console.warn,排障期间完全隐形(僵尸节点
|
|
3634
|
+
// 复活事故的诊断成本直接来源之一),静默失败不可接受。
|
|
3635
|
+
this._audit(`[user-relation] mergeAlias: 删除 alias 壳子失败 alias=${aliasId} canonical=${canonicalId} kind=${opts.kind}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3584
3636
|
}
|
|
3585
3637
|
return {
|
|
3586
3638
|
effectiveCanonicalId: canonicalId,
|
|
@@ -3635,33 +3687,32 @@ export class RelationService {
|
|
|
3635
3687
|
}
|
|
3636
3688
|
}
|
|
3637
3689
|
/**
|
|
3638
|
-
*
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3641
|
-
*
|
|
3690
|
+
* hierarchy 判定落边的唯一入口。曾经的形态(查 pass 快照 + store 直写)出过双重事故:
|
|
3691
|
+
* 旧快照查重看不见本 pass 刚建的边 → 同一对同 pass 重复铸边(实测同边 ×9);
|
|
3692
|
+
* 直写绕过 addEntityEntityEdge 门面 → 写入层实时查重完全失效。
|
|
3693
|
+
* 现约束:一切以**库中实时状态**为准——先核两端节点仍存活(stale 工单里的已合并
|
|
3694
|
+
* 节点不再落边),再实时查 part-of/contains 双向已存在,写入走门面复用其
|
|
3695
|
+
* 实时查重 + 强化语义。
|
|
3642
3696
|
*/
|
|
3643
|
-
async
|
|
3644
|
-
const
|
|
3697
|
+
async _ensureHierarchyEdge(childId, parentId, description) {
|
|
3698
|
+
const [child, parent] = await Promise.all([this.store.getEntity(childId), this.store.getEntity(parentId)]);
|
|
3699
|
+
if (!child || !parent)
|
|
3700
|
+
return 'skipped-dead';
|
|
3701
|
+
const snapshot = await this.store.loadAll();
|
|
3702
|
+
const existing = snapshot.edges.some(e => e.kind === 'entity-entity' &&
|
|
3645
3703
|
(e.relationType === 'part-of' || e.relationType === 'contains') &&
|
|
3646
3704
|
((e.fromEntityId === childId && e.toEntityId === parentId) ||
|
|
3647
3705
|
(e.fromEntityId === parentId && e.toEntityId === childId)));
|
|
3648
3706
|
if (existing)
|
|
3649
|
-
return
|
|
3650
|
-
|
|
3651
|
-
await this.store.upsertEdge({
|
|
3652
|
-
id: globalThis.crypto.randomUUID(),
|
|
3653
|
-
kind: 'entity-entity',
|
|
3707
|
+
return 'exists';
|
|
3708
|
+
await this.addEntityEntityEdge({
|
|
3654
3709
|
fromEntityId: childId,
|
|
3655
3710
|
toEntityId: parentId,
|
|
3656
3711
|
relationType: 'part-of',
|
|
3657
|
-
directed: true,
|
|
3658
3712
|
weight: 0.6,
|
|
3659
3713
|
description: description.slice(0, 80),
|
|
3660
|
-
firstSeenAt: now,
|
|
3661
|
-
lastReinforcedAt: now,
|
|
3662
|
-
evidence: [],
|
|
3663
3714
|
});
|
|
3664
|
-
return
|
|
3715
|
+
return 'built';
|
|
3665
3716
|
}
|
|
3666
3717
|
/**
|
|
3667
3718
|
* 物理删除一条边(带保护门,供 agent 调用)。alias 边(is-alias-of / alt-account-of)禁删;
|