@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.
- package/LICENSE +21 -0
- package/dist/actions.d.ts +13 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +705 -0
- package/dist/actions.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +601 -0
- package/dist/commands.js.map +1 -0
- package/dist/consolidate-llm.d.ts +150 -0
- package/dist/consolidate-llm.d.ts.map +1 -0
- package/dist/consolidate-llm.js +373 -0
- package/dist/consolidate-llm.js.map +1 -0
- package/dist/extractor.d.ts +308 -0
- package/dist/extractor.d.ts.map +1 -0
- package/dist/extractor.js +1356 -0
- package/dist/extractor.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +626 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +41 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +281 -0
- package/dist/middleware.js.map +1 -0
- package/dist/rename-watcher.d.ts +16 -0
- package/dist/rename-watcher.d.ts.map +1 -0
- package/dist/rename-watcher.js +16 -0
- package/dist/rename-watcher.js.map +1 -0
- package/dist/service.d.ts +1108 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +4462 -0
- package/dist/service.js.map +1 -0
- package/dist/store.d.ts +89 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +207 -0
- package/dist/store.js.map +1 -0
- package/dist/tools.d.ts +48 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1630 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +432 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +94 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +419 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1630 -0
- package/dist/utils.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelationService —— 关系图的应用层 API。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 暴露给其他插件 / webui / agent middleware 的稳定查询接口
|
|
6
|
+
* - 处理"upsert 时合并 evidence + 衰减/强化 weight"的语义
|
|
7
|
+
* - 生成稳定的 ID(事件 / 边)
|
|
8
|
+
*
|
|
9
|
+
* 不处理:
|
|
10
|
+
* - LLM 提取本身 → M2 的 extractor.ts
|
|
11
|
+
* - WebUI 端点 → M4 的 actions
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@aalis/core';
|
|
14
|
+
import type { ModelRef } from '@aalis/plugin-llm-api';
|
|
15
|
+
import type { RelationStore } from './store.js';
|
|
16
|
+
import type { CommunityMembership, EntityEntityEdge, EntityKind, EntityNode, EventEntityEdge, EventEventEdge, EventNode, EvidenceRef, PersonEntityEdge, PersonEventEdge, PersonNode, PersonPersonEdge, RelationEdge, RelationGraphSnapshot, ScoreMode } from './types.js';
|
|
17
|
+
import { type WeightDecayCfg } from './utils.js';
|
|
18
|
+
export type TriggerExtractionFn = (sessionId: string) => Promise<{
|
|
19
|
+
status: 'ok' | 'skipped' | 'error';
|
|
20
|
+
reason?: string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare class RelationService {
|
|
23
|
+
private readonly store;
|
|
24
|
+
/** 可选 ctx:仅用于写 logger 审计(deleteNode / mergeNodes / changeEntityKind 等 agent 写入路径)。测试不传则 fallback 到 console。 */
|
|
25
|
+
private readonly ctx?;
|
|
26
|
+
/** 由 extractor 注入;actions 层通过 triggerExtraction() 调用 */
|
|
27
|
+
private triggerExtractionHandler?;
|
|
28
|
+
/** 最近一次 consolidate() 完成的时间戳(ms);未运行时为 undefined */
|
|
29
|
+
private _lastConsolidateAt?;
|
|
30
|
+
/** 最近一次 consolidate() 结果的简短摘要 */
|
|
31
|
+
private _lastConsolidateResultSummary?;
|
|
32
|
+
/** 最近一次 consolidate() 的触发来源:manual | eviction | api */
|
|
33
|
+
private _lastConsolidateTrigger?;
|
|
34
|
+
constructor(store: RelationStore,
|
|
35
|
+
/** 可选 ctx:仅用于写 logger 审计(deleteNode / mergeNodes / changeEntityKind 等 agent 写入路径)。测试不传则 fallback 到 console。 */
|
|
36
|
+
ctx?: Context | undefined);
|
|
37
|
+
/** 写 audit 日志;ctx 存在走 logger.warn,否则 fallback console.warn(主要照顾单元测试)。 */
|
|
38
|
+
private _audit;
|
|
39
|
+
/** 查询最近一次 consolidation 运行时间、触发源与结果摘要 */
|
|
40
|
+
getLastConsolidateInfo(): {
|
|
41
|
+
lastRunAt?: number;
|
|
42
|
+
summary?: string;
|
|
43
|
+
trigger?: 'manual' | 'eviction' | 'api';
|
|
44
|
+
};
|
|
45
|
+
static personId(platform: string, userId: string): string;
|
|
46
|
+
/** 由 extractor 在 start() 后注入 */
|
|
47
|
+
setTriggerExtractionHandler(fn: TriggerExtractionFn): void;
|
|
48
|
+
/** 手动触发某 session 的 LLM 提取;extractor 未挂载时返回 error */
|
|
49
|
+
triggerExtraction(sessionId: string): Promise<{
|
|
50
|
+
status: 'ok' | 'skipped' | 'error';
|
|
51
|
+
reason?: string;
|
|
52
|
+
}>;
|
|
53
|
+
observePerson(platform: string, userId: string, displayName?: string): Promise<PersonNode>;
|
|
54
|
+
getPerson(platform: string, userId: string): Promise<PersonNode | undefined>;
|
|
55
|
+
/**
|
|
56
|
+
* 同步平台 displayName 到 Person 节点:仅当节点已存在且 displayName 与传入不同时才 upsert。
|
|
57
|
+
* 不创建新节点(避免水群幽灵);不动 mentionCount / firstSeenAt / lastMentionedAt(与
|
|
58
|
+
* 「显式提及」语义区分);仅刷新 lastSeenAt。返回是否真的发生了改名。
|
|
59
|
+
*
|
|
60
|
+
* 调用方:rename-watcher(订阅 inbound:message:archived,从 metadata.nickname 同步)。
|
|
61
|
+
*/
|
|
62
|
+
syncDisplayName(platform: string, userId: string, displayName: string): Promise<boolean>;
|
|
63
|
+
deletePerson(platform: string, userId: string): Promise<{
|
|
64
|
+
deletedEdges: number;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* 统一节点查找入口:给定任意节点 ID(person `<platform>:<userId>` 或 event/entity UUID)
|
|
68
|
+
* 返回 { kind, name };不存在返回 null。供 tools 层做存在性校验+友好报错使用。
|
|
69
|
+
*/
|
|
70
|
+
findNodeById(id: string): Promise<{
|
|
71
|
+
kind: 'person' | 'event' | 'entity';
|
|
72
|
+
name: string;
|
|
73
|
+
} | null>;
|
|
74
|
+
/**
|
|
75
|
+
* 新建事件。严格按 normalized title 去重:若已存在同名事件,**强制合并**到旧节点
|
|
76
|
+
* (追加 evidence、累加权重 += 0.3、occurrences 追加当前时间戳),返回旧节点。
|
|
77
|
+
* 这样保证「同一件事被反复提及」不会产生重复 event,但通过 occurrences[] 保留时间维度。
|
|
78
|
+
*/
|
|
79
|
+
createEvent(input: Omit<EventNode, 'id' | 'firstSeenAt' | 'lastReinforcedAt'>): Promise<EventNode>;
|
|
80
|
+
/**
|
|
81
|
+
* 按 normalized title 精确匹配(不区分大小写、压缩空白)查找已有事件。
|
|
82
|
+
* 用于 createEvent 入口去重。
|
|
83
|
+
*
|
|
84
|
+
* 若传入 scope:遵循「同名 + 同 scope 才是同事件」原则;只接受
|
|
85
|
+
* (a) 两者 scope 相同,或 (b) 旧节点 scope 为 undefined(老数据通配)。
|
|
86
|
+
* 不传 scope:只看 title,保留老行为(供手动调用 / 测试 / 迁移)。
|
|
87
|
+
*/
|
|
88
|
+
findEventByTitle(title: string, scope?: string): Promise<EventNode | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* 强化已有事件:追加 evidence、更新 lastReinforcedAt,可选更新 summary/title/category。
|
|
91
|
+
*
|
|
92
|
+
* 跨 sessionScope 软护栏:如果新 evidence 全部来自与 existing.sessionScope 不同的会话,
|
|
93
|
+
* 且 existing 既不是 'global' 也不是未限定 scope,则记录审计但**继续执行**(warn 不阻断)。
|
|
94
|
+
* 与 addEventEventEdge 的 is-alias-of 跨 scope 硬阻断对应——reinforce 走 warn,
|
|
95
|
+
* 因为它在不少正常路径(如 entity 共现、is-alias-of 后回写)也会自然跨 scope 触发。
|
|
96
|
+
*/
|
|
97
|
+
reinforceEvent(eventId: string, patch: {
|
|
98
|
+
title?: string;
|
|
99
|
+
summary?: string;
|
|
100
|
+
category?: EventNode['category'];
|
|
101
|
+
evidence?: EvidenceRef[];
|
|
102
|
+
}): Promise<EventNode | undefined>;
|
|
103
|
+
getEvent(eventId: string): Promise<EventNode | undefined>;
|
|
104
|
+
deleteEvent(eventId: string): Promise<{
|
|
105
|
+
deletedEdges: number;
|
|
106
|
+
}>;
|
|
107
|
+
/**
|
|
108
|
+
* 新建实体。严格按 (entityKind, normalized name) 去重:若已存在同 kind 同名实体,
|
|
109
|
+
* **强制合并**到旧节点(追加 evidence、合并 aliases、累加权重 += 0.3),返回旧节点。
|
|
110
|
+
*/
|
|
111
|
+
createEntity(input: Omit<EntityNode, 'id' | 'firstSeenAt' | 'lastReinforcedAt'>): Promise<EntityNode>;
|
|
112
|
+
/**
|
|
113
|
+
* 强化已有实体:追加 evidence、更新 lastReinforcedAt,可选更新字段。
|
|
114
|
+
*
|
|
115
|
+
* **静默改 kind / 改 name 已被禁用**(详见 patch 字段注释),LLM 反复输出"重名异 kind"
|
|
116
|
+
* 时不会把已有节点偷偷翻转身份,只会留下 audit 日志。如确需 rename/换 kind,请走
|
|
117
|
+
* rename-watcher / consolidate verify / 显式 merge 工具。
|
|
118
|
+
*/
|
|
119
|
+
reinforceEntity(entityId: string, patch: {
|
|
120
|
+
/**
|
|
121
|
+
* 仅作 audit 比对;reinforceEntity **不会**通过此字段改名。传入与 existing.name normalize
|
|
122
|
+
* 后等价时,允许把 display 写法换成 patch 写法(洗写法);不等价则记审计后保留 existing.name。
|
|
123
|
+
*/
|
|
124
|
+
name?: string;
|
|
125
|
+
aliases?: string[];
|
|
126
|
+
summary?: string;
|
|
127
|
+
/**
|
|
128
|
+
* 仅作 audit 比对;reinforceEntity **不会**通过此字段改 kind。
|
|
129
|
+
* topic/work/place/thing 是底层分类,改 kind 等于换实体,必须走 consolidate verify
|
|
130
|
+
* 或显式管理工具。
|
|
131
|
+
*/
|
|
132
|
+
entityKind?: EntityNode['entityKind'];
|
|
133
|
+
evidence?: EvidenceRef[];
|
|
134
|
+
}): Promise<EntityNode | undefined>;
|
|
135
|
+
getEntity(entityId: string): Promise<EntityNode | undefined>;
|
|
136
|
+
deleteEntity(entityId: string): Promise<{
|
|
137
|
+
deletedEdges: number;
|
|
138
|
+
}>;
|
|
139
|
+
/**
|
|
140
|
+
* 按 name / aliases 精确匹配(不区分大小写)查找已有实体。
|
|
141
|
+
* 用于抽取阶段去重 —— LLM 提取出"三角洲"时优先复用已存在的同名实体。
|
|
142
|
+
*/
|
|
143
|
+
findEntityByName(name: string): Promise<EntityNode | undefined>;
|
|
144
|
+
/**
|
|
145
|
+
* 按 (entityKind, normalized name) 精确匹配查找已有实体;用于 createEntity 入口去重。
|
|
146
|
+
* 比 findEntityByName 更严格(要求 kind 一致),避免「同名不同类」误合并(如游戏《北京》vs 地点北京)。
|
|
147
|
+
*/
|
|
148
|
+
findEntityByKindAndName(kind: EntityNode['entityKind'], name: string): Promise<EntityNode | undefined>;
|
|
149
|
+
addPersonEntityEdge(input: {
|
|
150
|
+
fromPersonId: string;
|
|
151
|
+
toEntityId: string;
|
|
152
|
+
role: PersonEntityEdge['role'];
|
|
153
|
+
sentiment?: PersonEntityEdge['sentiment'];
|
|
154
|
+
weight?: number;
|
|
155
|
+
description?: string;
|
|
156
|
+
evidence?: EvidenceRef[];
|
|
157
|
+
}): Promise<PersonEntityEdge>;
|
|
158
|
+
findPersonEntityEdge(fromPersonId: string, toEntityId: string, role: PersonEntityEdge['role']): Promise<PersonEntityEdge | undefined>;
|
|
159
|
+
addEventEventEdge(input: {
|
|
160
|
+
fromEventId: string;
|
|
161
|
+
toEventId: string;
|
|
162
|
+
relationType: string;
|
|
163
|
+
directed?: boolean;
|
|
164
|
+
weight?: number;
|
|
165
|
+
description?: string;
|
|
166
|
+
evidence?: EvidenceRef[];
|
|
167
|
+
}): Promise<EventEventEdge>;
|
|
168
|
+
findEventEventEdge(fromEventId: string, toEventId: string, relationType: string, directed: boolean): Promise<EventEventEdge | undefined>;
|
|
169
|
+
addEventEntityEdge(input: {
|
|
170
|
+
fromEventId: string;
|
|
171
|
+
toEntityId: string;
|
|
172
|
+
relationType: string;
|
|
173
|
+
weight?: number;
|
|
174
|
+
description?: string;
|
|
175
|
+
evidence?: EvidenceRef[];
|
|
176
|
+
}): Promise<EventEntityEdge>;
|
|
177
|
+
findEventEntityEdge(fromEventId: string, toEntityId: string, relationType: string): Promise<EventEntityEdge | undefined>;
|
|
178
|
+
addEntityEntityEdge(input: {
|
|
179
|
+
fromEntityId: string;
|
|
180
|
+
toEntityId: string;
|
|
181
|
+
relationType: string;
|
|
182
|
+
directed?: boolean;
|
|
183
|
+
weight?: number;
|
|
184
|
+
description?: string;
|
|
185
|
+
evidence?: EvidenceRef[];
|
|
186
|
+
}): Promise<EntityEntityEdge>;
|
|
187
|
+
findEntityEntityEdge(fromEntityId: string, toEntityId: string, relationType: string, directed: boolean): Promise<EntityEntityEdge | undefined>;
|
|
188
|
+
addPersonEventEdge(input: {
|
|
189
|
+
fromPersonId: string;
|
|
190
|
+
toEventId: string;
|
|
191
|
+
role: PersonEventEdge['role'];
|
|
192
|
+
sentiment?: PersonEventEdge['sentiment'];
|
|
193
|
+
weight?: number;
|
|
194
|
+
description?: string;
|
|
195
|
+
evidence?: EvidenceRef[];
|
|
196
|
+
}): Promise<PersonEventEdge>;
|
|
197
|
+
addPersonPersonEdge(input: {
|
|
198
|
+
fromPersonId: string;
|
|
199
|
+
toPersonId: string;
|
|
200
|
+
relationType: string;
|
|
201
|
+
directed?: boolean;
|
|
202
|
+
hierarchy?: PersonPersonEdge['hierarchy'];
|
|
203
|
+
weight?: number;
|
|
204
|
+
description?: string;
|
|
205
|
+
evidence?: EvidenceRef[];
|
|
206
|
+
}): Promise<PersonPersonEdge>;
|
|
207
|
+
findPersonEventEdge(fromPersonId: string, toEventId: string, role: PersonEventEdge['role']): Promise<PersonEventEdge | undefined>;
|
|
208
|
+
/**
|
|
209
|
+
* 查找等价的人-人边。对于对称关系 (directed=false),(A→B, friend) 与 (B→A, friend)
|
|
210
|
+
* 视为同一条边;只看其中一种方向即可命中。
|
|
211
|
+
*/
|
|
212
|
+
findPersonPersonEdge(fromPersonId: string, toPersonId: string, relationType: string, directed: boolean): Promise<PersonPersonEdge | undefined>;
|
|
213
|
+
deleteEdge(edgeId: string): Promise<void>;
|
|
214
|
+
loadAll(): Promise<RelationGraphSnapshot>;
|
|
215
|
+
/**
|
|
216
|
+
* 廉价预判:图中任一类节点 / 边的当前数量是否已达到 `evictByQuota` 的滞回触发阈值
|
|
217
|
+
* (`count >= ceil(cap · (1 + hysteresisPct))`),即下一次 `evictByQuota` **真的会删东西**。
|
|
218
|
+
*
|
|
219
|
+
* 用于 extractor 的"写后顺手老化"路径节流:避免每次提取都跑 PageRank / consolidate。
|
|
220
|
+
* 公式与 {@link evictByQuota} 内部判定一致;任一类超阈即返回 true(与 evict 的分类独立判定对齐)。
|
|
221
|
+
*
|
|
222
|
+
* `cap <= 0` 的类视为不限,跳过检查;`evictionEnabled` 由调用方负责。
|
|
223
|
+
*/
|
|
224
|
+
isOverQuota(quota: {
|
|
225
|
+
maxPersons?: number;
|
|
226
|
+
maxEvents?: number;
|
|
227
|
+
maxEntities?: number;
|
|
228
|
+
maxEdges?: number;
|
|
229
|
+
hysteresisPct?: number;
|
|
230
|
+
}): Promise<boolean>;
|
|
231
|
+
/**
|
|
232
|
+
* 清理孤儿节点:删除所有"没有任何边引用"的 person / event / entity。
|
|
233
|
+
*
|
|
234
|
+
* 设计原则(v3,最简):
|
|
235
|
+
* - **没有任何边端点引用 = 孤儿**,三类节点一视同仁。边的 6 种 kind 中只要节点
|
|
236
|
+
* 出现在任一 from/to 字段上就算"被引用"。
|
|
237
|
+
* - **person 孤儿也清**:observePerson 按 (platform, userId) upsert,删掉的"水群幽灵"
|
|
238
|
+
* 下次发言时会自动重建,所以删除安全。
|
|
239
|
+
* - **零保护**:weight/evidence 门槛属于配额淘汰阶段的事,与孤儿无关;
|
|
240
|
+
* 孤儿的语义就是"没人指向",无条件清。
|
|
241
|
+
* - **零参数**:刻意不暴露任何 opts,避免重新引入误用。
|
|
242
|
+
*
|
|
243
|
+
* 返回被删除的 id 列表,便于 caller 打日志/报告。
|
|
244
|
+
*/
|
|
245
|
+
pruneOrphans(): Promise<{
|
|
246
|
+
deletedPersons: number;
|
|
247
|
+
deletedEvents: number;
|
|
248
|
+
deletedEntities: number;
|
|
249
|
+
deletedPersonIds: string[];
|
|
250
|
+
deletedEventIds: string[];
|
|
251
|
+
deletedEntityIds: string[];
|
|
252
|
+
deletedDanglingEdges: number;
|
|
253
|
+
}>;
|
|
254
|
+
/**
|
|
255
|
+
* Eager 时间衰减回写:把所有 event/entity 节点与所有边的 `weight` 字段
|
|
256
|
+
* 物理改写为当前 `effectiveWeight`,并把 `lastReinforcedAt` 重置为 now
|
|
257
|
+
* (作为"新的衰减基准"——否则下次 rewrite 会基于同一基准再次衰减,
|
|
258
|
+
* raw 被反复折半到 0)。
|
|
259
|
+
*
|
|
260
|
+
* 设计动机(lazy → eager 切换):
|
|
261
|
+
* - 原 lazy 模式 DB 永远存 raw 累积值,effectiveWeight 仅查询时实时算。
|
|
262
|
+
* 问题:raw=1.0 的边被衰减到 effW=0.3 后再次 reinforce 一次,
|
|
263
|
+
* `reinforceWeight(1.0, 0.1) = 1.0`——**永远卡死在 1**,effW 从 0.3
|
|
264
|
+
* 瞬间跳回 1.0,离散跳跃,不符合"老朋友重逢慢慢回温"的人类直觉。
|
|
265
|
+
* - Eager 回写后:raw 物理变成 0.3,下次 reinforce 从 0.3 出发 → 0.37 →
|
|
266
|
+
* 0.43 …**渐进恢复**,活跃关系靠持续 reinforce 维持高位、长期不活跃的
|
|
267
|
+
* 关系自然回落到 floor 附近。每日压缩前调用一次,DB 字段就能反映
|
|
268
|
+
* "当下真实强度",便于调试 / 观察 / 跨时间快照对比。
|
|
269
|
+
*
|
|
270
|
+
* 语义合并(务实简化,避免新增 `lastDecayedAt` 字段):
|
|
271
|
+
* - `lastReinforcedAt` 在 eager 模式下含义统一为"上次 weight 字段被更新的
|
|
272
|
+
* 时间"——reinforce* 方法与 rewriteWeights 都写它。物理上是衰减基准。
|
|
273
|
+
* - 调用方应理解:稳态下"长期不活跃节点"的 lastReinforcedAt 会被每日
|
|
274
|
+
* rewrite 推到最近一次压缩时间,age 分子≈0;但它们的 effW 已收敛到 floor,
|
|
275
|
+
* ageScore 排序退化为 `1 / (floor × PR)`——PR 边缘的依旧最先被淘汰。
|
|
276
|
+
*
|
|
277
|
+
* 副作用与正交性:
|
|
278
|
+
* - Person 节点不参与(无 weight 字段,由 mentionCount / lastSeenAt 表达活跃度)。
|
|
279
|
+
* - `halfLifeDays <= 0`(衰减关闭)时 short-circuit 返回,零写盘开销;
|
|
280
|
+
* 单测默认配置 `{ halfLifeDays: 0 }` 走此路径,**不影响现有测试行为**。
|
|
281
|
+
* - 增量阈值 `|new - raw| < 1e-6` 跳过,避免对几乎无变化的节点做无谓写盘
|
|
282
|
+
* (也保证幂等:rewrite 后第二次立即调用本方法所有节点都命中阈值跳过)。
|
|
283
|
+
* - 活跃节点保护:被 reinforce 过的节点 lastReinforcedAt > 上次 rewrite 时间,
|
|
284
|
+
* age 更小受保护——"最近活跃的更新鲜,更应保留"。
|
|
285
|
+
*
|
|
286
|
+
* 调用方:
|
|
287
|
+
* - `evictByQuota` 入口自动调用一次(与每日 scheduler 压缩对齐)。
|
|
288
|
+
* - `/relation rewrite-weights` 手动命令。
|
|
289
|
+
*
|
|
290
|
+
* 返回各类写回计数,便于日志 / 测试断言。
|
|
291
|
+
*/
|
|
292
|
+
rewriteWeights(decay: WeightDecayCfg, opts?: {
|
|
293
|
+
now?: number;
|
|
294
|
+
}): Promise<{
|
|
295
|
+
events: number;
|
|
296
|
+
entities: number;
|
|
297
|
+
edges: number;
|
|
298
|
+
skipped: boolean;
|
|
299
|
+
}>;
|
|
300
|
+
/**
|
|
301
|
+
* 自动老化:按配额淘汰过多节点。模仿 profile 的"写后顺手扫"风格,不开独立调度器。
|
|
302
|
+
*
|
|
303
|
+
* 优先级(每次仅在超额时执行):
|
|
304
|
+
* 1. **孤儿节点**先删(无任何边引用的 person / event / entity;委托 `pruneOrphans()`)。
|
|
305
|
+
* 孤儿清理与配额无关,旧账噪声任何时候都清。
|
|
306
|
+
* 2. 仍超额时按 `(now - lastReinforcedAt) / (max(effW,0.05) · max(PR,ε))` **降序**删;
|
|
307
|
+
* 即"老旧 + 低权重 + 在 PageRank 上无人指向"的优先丢。
|
|
308
|
+
* 3. **不再有硬豁免**(evidence≥3 / effW≥0.8):避免老节点永久占住名额。
|
|
309
|
+
* 重要性完全由 effW + PageRank 表达:高 evidence/weight 节点自然在打分尾部,
|
|
310
|
+
* 并且随时间衰减后仍可以让出名额。Person 节点同样进入排序,
|
|
311
|
+
* 依靠 PR 个性化向量的人偶偏置(person seed=2 / entity=1.5 / event=1)自然偏保护。
|
|
312
|
+
* 4. **滞回(hysteresis)**:仅当 count > quota·(1+hysteresisPct) 时才触发,
|
|
313
|
+
* 触发后一次性裁到 floor(quota·targetPct)。默认 hysteresis=0.2, target=0.8 ——
|
|
314
|
+
* quota=500 时会在 600 触发并裁到 400,相当于一次清理 ~200 条;不会每写一条就裁。
|
|
315
|
+
* 5. 边也按配额删——保留 `weight · 端点PR平均` 最高的,让"弱权但连接重要节点"的边受保护。
|
|
316
|
+
*
|
|
317
|
+
* 副作用:每次调用都会把 PageRank 写回三类节点的 `lastPageRank` / `lastPageRankAt`,
|
|
318
|
+
* 用于 WebUI 展示"图重要性"。
|
|
319
|
+
*
|
|
320
|
+
* PageRank 个性化向量按 kind 加权(默认 person=2 / entity=1.5 / event=1),从而"重要性 人>物>事"
|
|
321
|
+
* 直接体现为分数偏置:人物附近的事件/实体更难被淘汰。
|
|
322
|
+
* 另外 utils.computePageRank 在 person→event / person→entity 单向边上加了半权反向虚拟边(系数 0.5),
|
|
323
|
+
* 让"参与重要事件 / 关注热门实体"的人 PR 能拉开差距,避免无 person-person 边的人退化到 seed 常数。
|
|
324
|
+
*
|
|
325
|
+
* 返回各类删除计数,便于日志/测试断言。
|
|
326
|
+
*/
|
|
327
|
+
evictByQuota(quota: {
|
|
328
|
+
/** 人物节点总数上限。0 = 不限(允许人物无限增长)。 */
|
|
329
|
+
maxPersons?: number;
|
|
330
|
+
maxEvents: number;
|
|
331
|
+
maxEntities: number;
|
|
332
|
+
maxEdges: number;
|
|
333
|
+
/** PageRank 阻尼,默认 0.85 */
|
|
334
|
+
pagerankDamping?: number;
|
|
335
|
+
/** PageRank 最大迭代次数,默认 20 */
|
|
336
|
+
pagerankIterations?: number;
|
|
337
|
+
/** PageRank 收敛阈值(L1 误差),默认 1e-4 */
|
|
338
|
+
pagerankEpsilon?: number;
|
|
339
|
+
/** 滞回百分比;count > quota·(1+hysteresisPct) 才触发淘汰。默认 0.2 */
|
|
340
|
+
hysteresisPct?: number;
|
|
341
|
+
/** 触发后裁到 floor(quota·targetPct)。默认 0.8 */
|
|
342
|
+
targetPct?: number;
|
|
343
|
+
/** PageRank 个性化向量种子权(人/物/事),默认 2/1.5/1 */
|
|
344
|
+
personSeed?: number;
|
|
345
|
+
entitySeed?: number;
|
|
346
|
+
eventSeed?: number;
|
|
347
|
+
/**
|
|
348
|
+
* person→event / person→entity 单向边反向虚拟边权重系数。
|
|
349
|
+
* 0 = 不加反向边;默认 0.5。
|
|
350
|
+
*/
|
|
351
|
+
reverseEdgeFactor?: number;
|
|
352
|
+
/**
|
|
353
|
+
* 是否启用 component-size 缩放(Component-weighted Personalized PageRank):
|
|
354
|
+
* 孤立小连通分量节点 PR 按 sqrt(componentSize/n) 压低,避免
|
|
355
|
+
* "1人1物1事" 这种三角小环被反向虚拟边 + dangling 重分布抬高。默认 true。
|
|
356
|
+
*/
|
|
357
|
+
pagerankComponentScale?: boolean;
|
|
358
|
+
/** 时间衰减配置:用于把 raw weight 折算成有效 weight。halfLifeDays<=0 时退化为原 raw 行为。 */
|
|
359
|
+
decay?: WeightDecayCfg;
|
|
360
|
+
/**
|
|
361
|
+
* 社群发现算法;默认 'louvain'。
|
|
362
|
+
* - 'louvain':标准硬划分,快、主社群清晰;每人恒为 1 个社群。
|
|
363
|
+
* - 'leiden':Louvain + 内部连通性 refinement;修复 Louvain 社区内部可能不连通的问题。
|
|
364
|
+
* - 'slpa':Speaker-Listener Label Propagation,**原生重叠社区**;跨群人物能获得多个社群隶属度。
|
|
365
|
+
*/
|
|
366
|
+
communityAlgorithm?: 'louvain' | 'leiden' | 'slpa';
|
|
367
|
+
}): Promise<{
|
|
368
|
+
deletedPersons: number;
|
|
369
|
+
deletedEvents: number;
|
|
370
|
+
deletedEntities: number;
|
|
371
|
+
deletedEdges: number;
|
|
372
|
+
/** 孤儿阶段被删的 id 列表(前 50 个),便于日志/诊断 */
|
|
373
|
+
orphanSamples: {
|
|
374
|
+
persons: string[];
|
|
375
|
+
events: string[];
|
|
376
|
+
entities: string[];
|
|
377
|
+
};
|
|
378
|
+
}>;
|
|
379
|
+
/** 查询某人涉及的所有事件 + 实体 + 直连人际关系(深度 1 快捷方法) */
|
|
380
|
+
getNeighborhood(personId: string): Promise<{
|
|
381
|
+
person: PersonNode | undefined;
|
|
382
|
+
events: EventNode[];
|
|
383
|
+
entities: EntityNode[];
|
|
384
|
+
edges: RelationEdge[];
|
|
385
|
+
}>;
|
|
386
|
+
/**
|
|
387
|
+
* 按 BFS 抽取以指定 person 为起点的子图。
|
|
388
|
+
*
|
|
389
|
+
* - **maxDepth**:探求层数(0 = 仅起点;1 = 起点 + 直接邻居;以此类推)。
|
|
390
|
+
* 人 → 事件 / 人 → 人 各算 1 跳;事件 → 人也算 1 跳,因此 depth=2 可触达"同事件其他参与者"。
|
|
391
|
+
* - **maxBreadth**:单个节点在 BFS 中最多展开的邻居数,按边 weight 降序选取。
|
|
392
|
+
* - **visited**:以 nodeId 集合去重,防止环 / 重复展开(同一节点最多被加入队列一次)。
|
|
393
|
+
*
|
|
394
|
+
* 返回子图包含访问过的节点之间的全部已存在边(不仅 BFS 树边),便于上层渲染完整局部结构。
|
|
395
|
+
*/
|
|
396
|
+
traverseSubgraph(opts: {
|
|
397
|
+
/** 起点节点 id 列表,按 snapshot 自动推断 kind(person / event / entity) */
|
|
398
|
+
startNodeIds: string[];
|
|
399
|
+
maxDepth: number;
|
|
400
|
+
maxBreadth: number;
|
|
401
|
+
}): Promise<{
|
|
402
|
+
persons: PersonNode[];
|
|
403
|
+
events: EventNode[];
|
|
404
|
+
entities: EntityNode[];
|
|
405
|
+
edges: RelationEdge[];
|
|
406
|
+
}>;
|
|
407
|
+
/**
|
|
408
|
+
* 寻找两个人之间的最短关系链。BFS,事件节点作为中间桥(A→事件→B 算 2 跳)。
|
|
409
|
+
* - maxDepth:路径最大边数;超过返回 null。
|
|
410
|
+
* - 返回 { nodes, edges } 节点列表按路径顺序排列;找不到返回 null。
|
|
411
|
+
*/
|
|
412
|
+
findPath(fromNodeId: string, toNodeId: string, maxDepth: number): Promise<{
|
|
413
|
+
nodes: Array<PersonNode | EventNode | EntityNode>;
|
|
414
|
+
edges: RelationEdge[];
|
|
415
|
+
} | null>;
|
|
416
|
+
/**
|
|
417
|
+
* 计算两节点间联系强度(方向感知版)。
|
|
418
|
+
*
|
|
419
|
+
* **方向语义模型**:
|
|
420
|
+
* - **桥型边**(person-event / person-entity / event-entity):事件/实体没有主观能动,
|
|
421
|
+
* 仅作中介出现 → 邻接表里总是双向(无视 edge.directed)
|
|
422
|
+
* - **主体间边**(person-person / event-event / entity-entity):
|
|
423
|
+
* - `directed=false` → 双向(如 event "related" event)
|
|
424
|
+
* - `directed=true` → 严格 from→to 单向(如 A "admirer" B:B 不一定认识 A)
|
|
425
|
+
*
|
|
426
|
+
* **mode 参数**:
|
|
427
|
+
* - `'symmetric'`(默认)= **联系紧密度**。跑 a→b 与 b→a 两遍取 max。
|
|
428
|
+
* 语义:"存在任意方向的关系连通"。单方面声明至少会从一侧贡献。
|
|
429
|
+
* - `'directed'` = **关注/影响传播度**。仅跑 fromNodeId → toNodeId 一次。
|
|
430
|
+
* 语义:"从 A 出发能否通过主动声明触达 B"。适用于"A 都关心了谁/A 的影响波及谁"。
|
|
431
|
+
*
|
|
432
|
+
* **kindMultiplier**(待数据观察调整,目前为直觉估计):
|
|
433
|
+
* - person-person = 1.0(社会语义最强)
|
|
434
|
+
* - person-event = 0.8(事件 = 真实互动)
|
|
435
|
+
* - person-entity = 0.5(兴趣共鸣 < 真实互动)
|
|
436
|
+
* - event-event = 0.4
|
|
437
|
+
* - event-entity = 0.4
|
|
438
|
+
* - entity-entity = 0.3(内容关联,非社会信号)
|
|
439
|
+
*
|
|
440
|
+
* **算法**:限深简单路径枚举(Katz 风格) + Adamic-Adar 共同邻居
|
|
441
|
+
* - contrib = β^|p| × Π w_e × (len==1 ? 1.5 : 1) — 直接连接 boost
|
|
442
|
+
* - common = Σ 1/log(deg(C)+1.7) — 惩罚高度共同节点(群聊噪声)
|
|
443
|
+
* - raw = katz + 0.3 × common;score = tanh(raw) ∈ [0, 1]
|
|
444
|
+
*
|
|
445
|
+
* **未来扩展点**(在 opts 里预留):hierarchy 反向降权、relationType 加权、时间衰减…
|
|
446
|
+
*/
|
|
447
|
+
scoreBetween(fromNodeId: string, toNodeId: string, opts?: {
|
|
448
|
+
maxDepth?: number;
|
|
449
|
+
beta?: number;
|
|
450
|
+
topPaths?: number;
|
|
451
|
+
/** 'symmetric'(默认)= 联系紧密度;'directed' = 关注/影响传播度 */
|
|
452
|
+
mode?: ScoreMode;
|
|
453
|
+
/**
|
|
454
|
+
* 内部优化:复用调用方已加载的 snapshot,跳过本函数内的 `store.loadAll()`。
|
|
455
|
+
* 仅在 consolidate 等批量场景使用——上层保证 snapshot 在批处理期间不变。
|
|
456
|
+
* 公共 API 调用者不要传,让本函数自己加载以获取最新数据。
|
|
457
|
+
*/
|
|
458
|
+
_snapshot?: RelationGraphSnapshot;
|
|
459
|
+
}): Promise<{
|
|
460
|
+
fromId: string;
|
|
461
|
+
toId: string;
|
|
462
|
+
mode: ScoreMode;
|
|
463
|
+
score: number;
|
|
464
|
+
rawScore: number;
|
|
465
|
+
katzScore: number;
|
|
466
|
+
commonNeighborsScore: number;
|
|
467
|
+
pathsConsidered: number;
|
|
468
|
+
shortestLength: number | null;
|
|
469
|
+
directlyConnected: boolean;
|
|
470
|
+
/** 仅 symmetric 模式同时利用;directed 模式 backward* 固定 0 */
|
|
471
|
+
forwardKatzScore: number;
|
|
472
|
+
backwardKatzScore: number;
|
|
473
|
+
topPaths: Array<{
|
|
474
|
+
direction: 'forward' | 'backward';
|
|
475
|
+
nodes: Array<PersonNode | EventNode | EntityNode>;
|
|
476
|
+
edges: RelationEdge[];
|
|
477
|
+
length: number;
|
|
478
|
+
weightProduct: number;
|
|
479
|
+
contribution: number;
|
|
480
|
+
}>;
|
|
481
|
+
commonNeighbors: Array<{
|
|
482
|
+
node: PersonNode | EventNode | EntityNode;
|
|
483
|
+
degree: number;
|
|
484
|
+
aaContribution: number;
|
|
485
|
+
}>;
|
|
486
|
+
}>;
|
|
487
|
+
/**
|
|
488
|
+
* 按关键词搜索事件(substring,标题 + summary,不区分大小写)。
|
|
489
|
+
* - days:仅返回 lastReinforcedAt 在 N 天内的事件;0/未传 → 不限
|
|
490
|
+
* - limit:返回上限(默认 20)
|
|
491
|
+
*/
|
|
492
|
+
searchEvents(opts: {
|
|
493
|
+
keyword?: string;
|
|
494
|
+
days?: number;
|
|
495
|
+
limit?: number;
|
|
496
|
+
}): Promise<EventNode[]>;
|
|
497
|
+
/**
|
|
498
|
+
* 按关键词搜索人物。匹配 displayName / userId / aliases / id(substring,不区分大小写)。
|
|
499
|
+
* - platform:可选,仅返回该平台下的人物
|
|
500
|
+
* - limit:返回上限(默认 20)
|
|
501
|
+
*/
|
|
502
|
+
searchPersons(opts: {
|
|
503
|
+
keyword?: string;
|
|
504
|
+
platform?: string;
|
|
505
|
+
limit?: number;
|
|
506
|
+
}): Promise<PersonNode[]>;
|
|
507
|
+
/**
|
|
508
|
+
* 按关键词搜索实体。匹配 name / aliases / summary / id(substring,不区分大小写)。
|
|
509
|
+
* - kind:可选,仅返回指定 entityKind
|
|
510
|
+
* - limit:返回上限(默认 20)
|
|
511
|
+
*/
|
|
512
|
+
searchEntities(opts: {
|
|
513
|
+
keyword?: string;
|
|
514
|
+
kind?: EntityNode['entityKind'];
|
|
515
|
+
limit?: number;
|
|
516
|
+
}): Promise<EntityNode[]>;
|
|
517
|
+
/**
|
|
518
|
+
* 列出符合过滤条件的边。所有过滤器是 AND 关系;不传任何过滤器 = 返回全部(受 limit 限制)。
|
|
519
|
+
* - kinds:边大类(person-event / person-person / person-entity / event-event / event-entity / entity-entity)
|
|
520
|
+
* - relationTypes:仅对带 relationType 的边生效(person-person / event-event / event-entity / entity-entity)
|
|
521
|
+
* - roles:仅对带 role 的边生效(person-event / person-entity)
|
|
522
|
+
* - nodeId:边的任一端等于该 id(用于"这条边和某节点相关")
|
|
523
|
+
* - fromId/toId:方向敏感(注意无向边的 from/to 由 LLM 提取时给定,未必符合直觉)
|
|
524
|
+
* - days:仅返回 lastReinforcedAt 在 N 天内的;0/未传 → 不限
|
|
525
|
+
* - limit:返回上限(默认 50)
|
|
526
|
+
* 按 lastReinforcedAt 降序。
|
|
527
|
+
*/
|
|
528
|
+
listEdges(opts: {
|
|
529
|
+
kinds?: RelationEdge['kind'][];
|
|
530
|
+
relationTypes?: string[];
|
|
531
|
+
roles?: string[];
|
|
532
|
+
nodeId?: string;
|
|
533
|
+
fromId?: string;
|
|
534
|
+
toId?: string;
|
|
535
|
+
days?: number;
|
|
536
|
+
limit?: number;
|
|
537
|
+
}): Promise<RelationEdge[]>;
|
|
538
|
+
/**
|
|
539
|
+
* 时间线:给定节点,返回与其相关的事件按时间倒序排列。
|
|
540
|
+
* - 节点是 person → 返回该人参与的事件(按 personEvent.lastReinforcedAt 降序)
|
|
541
|
+
* - 节点是 entity → 返回涉及该实体的事件(按 eventEntity.lastReinforcedAt 降序)
|
|
542
|
+
* - 节点是 event → 返回该事件 + 由 event-event 边相连的相关事件
|
|
543
|
+
* 返回每个事件附带触达它的边信息(用于追溯"为什么相关")。
|
|
544
|
+
*/
|
|
545
|
+
getTimeline(opts: {
|
|
546
|
+
nodeId: string;
|
|
547
|
+
days?: number;
|
|
548
|
+
limit?: number;
|
|
549
|
+
}): Promise<Array<{
|
|
550
|
+
event: EventNode;
|
|
551
|
+
viaEdge: RelationEdge;
|
|
552
|
+
}>>;
|
|
553
|
+
consolidate(opts?: {
|
|
554
|
+
autoLink?: boolean;
|
|
555
|
+
/** 可选:传入后 consolidate 末尾会调用 LLM 做别名核验与摘要重写 */
|
|
556
|
+
llm?: {
|
|
557
|
+
ctx: Context;
|
|
558
|
+
modelRef: ModelRef;
|
|
559
|
+
disableThinking?: boolean;
|
|
560
|
+
};
|
|
561
|
+
/** 调用来源标识,仅用于 getLastConsolidateInfo() 报告。默认 api。 */
|
|
562
|
+
triggerSource?: 'manual' | 'eviction' | 'api';
|
|
563
|
+
/**
|
|
564
|
+
* (1.5) 宽召回 LLM 性能优化:
|
|
565
|
+
* - skipLowScorePairs:若 true,pair 双方 compositeScore 均 < lowScoreThreshold 时跳过 LLM 核验
|
|
566
|
+
* (两端都是 edge tier,合并价值低,不值得花 LLM 调用),默认 true。
|
|
567
|
+
* - lowScoreThreshold:阈值,默认 0.2(与 scoreToTier 的 edge 边界一致)。设 0 = 不跳过。
|
|
568
|
+
*/
|
|
569
|
+
skipLowScorePairs?: boolean;
|
|
570
|
+
lowScoreThreshold?: number;
|
|
571
|
+
/**
|
|
572
|
+
* Entity 宽召回的 embedding cos 阈值,默认 0.86。
|
|
573
|
+
* 仅在 `ctx.getService('embedding')` 可用时生效;name+summary embed 后 cos≥该值即作为额外候选。
|
|
574
|
+
* 设 0 = 关闭(依然走 substring/alias 路径)。
|
|
575
|
+
*/
|
|
576
|
+
entityCosThreshold?: number;
|
|
577
|
+
/**
|
|
578
|
+
* 可选 ctx。若传入则 consolidate 会顺带做一次「伪 person 自动清理」:
|
|
579
|
+
* platform 不在 `getPlatformNames(ctx)` 运行时白名单内(或 userId 命中
|
|
580
|
+
* 通用占位 self/me/bot/assistant)的 person,连同级联边一起删除。
|
|
581
|
+
* 与写入守卫 `isPlaceholderSelfPersonId` 共用同一谓词,口径一致。
|
|
582
|
+
*
|
|
583
|
+
* 警告:临时禁用了某个 adapter 时(白名单收缩),这里会把对应平台的
|
|
584
|
+
* **真实历史 person** 误判为 fake。`getPlatformNames(ctx)` 为空时本步骤
|
|
585
|
+
* 自动跳过以保护历史数据。
|
|
586
|
+
*/
|
|
587
|
+
ctx?: Context;
|
|
588
|
+
}): Promise<{
|
|
589
|
+
aliasCandidates: Array<{
|
|
590
|
+
aId: string;
|
|
591
|
+
bId: string;
|
|
592
|
+
aKind: 'person' | 'entity';
|
|
593
|
+
bKind: 'person' | 'entity';
|
|
594
|
+
reason: string;
|
|
595
|
+
}>;
|
|
596
|
+
aliasEdgesCreated: number;
|
|
597
|
+
partOfEdgesCreated: number;
|
|
598
|
+
eventEdgesNormalized: number;
|
|
599
|
+
entityHierarchyCandidates: number;
|
|
600
|
+
entityHierarchyEdgesCreated: number;
|
|
601
|
+
llmVerified?: number;
|
|
602
|
+
llmRejected?: number;
|
|
603
|
+
summariesRewritten?: number;
|
|
604
|
+
lateralParentCandidates: number;
|
|
605
|
+
lateralParentsCreated: number;
|
|
606
|
+
lateralEdgesCreated: number;
|
|
607
|
+
/** 自动清理删掉的伪 person 数(platform 不在白名单或 userId 为通用占位)。 */
|
|
608
|
+
fakePersonsDeleted: number;
|
|
609
|
+
/** 自动清理时级联删的 person-* 边总数。 */
|
|
610
|
+
fakePersonEdgesDeleted: number;
|
|
611
|
+
/** event 重复合并:召回候选数(pair 数,dry-run 报告用) */
|
|
612
|
+
eventDuplicateCandidates: number;
|
|
613
|
+
/** event 重复合并:实际合并的 event 数(被合并到 canonical 的别名 event 数) */
|
|
614
|
+
eventDuplicatesMerged: number;
|
|
615
|
+
}>;
|
|
616
|
+
/**
|
|
617
|
+
* 内部入口:执行一次 event 重复检测;可选 dryRun 仅返回候选不合并。
|
|
618
|
+
*
|
|
619
|
+
* 召回融合公式(任一阈值达成即进 LLM,OR 关系):
|
|
620
|
+
* - 有 embedding 时:fused = 0.7·cos + 0.3·struct ≥ fusedThreshold(默认 0.7);
|
|
621
|
+
* 任一端 embedding 失败 → 该项不参与 fused,仅看 jaccard / struct
|
|
622
|
+
* - 永远兜底:jaccard(chars) ≥ jaccardThreshold(默认 0.4) **或** struct ≥ structuralThreshold(默认 0.5)
|
|
623
|
+
*
|
|
624
|
+
* cos 与 struct 权重对换的原因:标题/语义高度相似但孤立(无共邻边)的事件对在旧公式
|
|
625
|
+
* 0.3·cos+0.7·struct 下永远 fused≈0 → 阈值不达 → 不进 LLM。文本相似度才是 event 同一性
|
|
626
|
+
* 的主要信号,结构作为加成。
|
|
627
|
+
*
|
|
628
|
+
* sessionScope 隔离:
|
|
629
|
+
* - 同 scope 池内才比对(含「都 'global'」、「同 sessionId」、「都 undefined → 兜底当 'global'」);
|
|
630
|
+
* - 跨 scope 永不比对,与 L537 mergeAlias 硬护栏一致
|
|
631
|
+
*
|
|
632
|
+
* Lazy embed:当 EventNode.embeddingHash !== computeEventEmbeddingHash(title, summary) 时,
|
|
633
|
+
* 实时调 embedding.embed() 并写回(持久化),下次 consolidate 复用。
|
|
634
|
+
*/
|
|
635
|
+
private _consolidateEventDuplicates;
|
|
636
|
+
/**
|
|
637
|
+
* 公开 API:dry-run 查找 event 重复(不执行任何合并)。
|
|
638
|
+
* 给 `/relation event-duplicates` 命令 / webui 调用使用。
|
|
639
|
+
*
|
|
640
|
+
* 与 consolidate 行为口径完全一致:sessionScope 同池 + 加权融合阈值 + mergeReject 缓存复用 + LLM 终判。
|
|
641
|
+
* 但不写图,仅返回候选 + LLM 判定。
|
|
642
|
+
*/
|
|
643
|
+
findEventDuplicates(opts?: {
|
|
644
|
+
llm?: {
|
|
645
|
+
ctx: Context;
|
|
646
|
+
modelRef: ModelRef;
|
|
647
|
+
disableThinking?: boolean;
|
|
648
|
+
};
|
|
649
|
+
fusedThreshold?: number;
|
|
650
|
+
jaccardThreshold?: number;
|
|
651
|
+
structuralThreshold?: number;
|
|
652
|
+
}): Promise<{
|
|
653
|
+
candidates: Array<{
|
|
654
|
+
aId: string;
|
|
655
|
+
bId: string;
|
|
656
|
+
aTitle: string;
|
|
657
|
+
bTitle: string;
|
|
658
|
+
sessionScope: string;
|
|
659
|
+
cosineScore: number | null;
|
|
660
|
+
jaccardScore: number;
|
|
661
|
+
structuralScore: number;
|
|
662
|
+
fusedScore: number | null;
|
|
663
|
+
llmVerdict?: {
|
|
664
|
+
isSame: boolean;
|
|
665
|
+
reason: string;
|
|
666
|
+
};
|
|
667
|
+
cacheHit?: boolean;
|
|
668
|
+
}>;
|
|
669
|
+
llmVerified: number;
|
|
670
|
+
llmRejected: number;
|
|
671
|
+
llmRejectCacheHits: number;
|
|
672
|
+
embeddingAvailable: boolean;
|
|
673
|
+
}>;
|
|
674
|
+
renameNode(opts: {
|
|
675
|
+
kind: 'event' | 'entity';
|
|
676
|
+
id: string;
|
|
677
|
+
newName: string;
|
|
678
|
+
/** 调用来源标识:'llm' / 'manual' / 'consolidate' 等;默认 'manual' */
|
|
679
|
+
by?: string;
|
|
680
|
+
/** 改名理由(≤80 字),写入 audit log */
|
|
681
|
+
reason?: string;
|
|
682
|
+
}): Promise<{
|
|
683
|
+
from: string;
|
|
684
|
+
to: string;
|
|
685
|
+
aliasesAdded: boolean;
|
|
686
|
+
}>;
|
|
687
|
+
/**
|
|
688
|
+
* 关系边修正:LLM 发现某条边过弱 / 过强 / 是幻觉时调用。
|
|
689
|
+
*
|
|
690
|
+
* 设计原则(小破坏性 + 高效修正):
|
|
691
|
+
* - **阶梯保护**:weight 越高越难物理删除,避免误删强关系
|
|
692
|
+
* - weight ≥ 0.5:只允许 weaken;想 remove 需先反复 weaken 到 < 0.5(或 force=true)
|
|
693
|
+
* - 0.3 ≤ weight < 0.5:可 weaken 或 remove
|
|
694
|
+
* - weight < 0.3:自由(含 strengthen 重建)
|
|
695
|
+
* - **alias 边禁操作**:is-alias-of / alt-account-of 是结构性边,
|
|
696
|
+
* 修改会破坏 mergeAlias 不变量。需取消别名请走未来的 splitAlias 工具。
|
|
697
|
+
* - **必填 reason**(≤80 字),写入 weightHistory[] 留痕
|
|
698
|
+
* - **物理删除清理干净**:deleteEdge 直接落盘,不留墓碑(避免脏数据)
|
|
699
|
+
*
|
|
700
|
+
* 不接受 multiplier > 1 的 weaken / multiplier < 1 的 strengthen
|
|
701
|
+
* (语义错位会让 LLM 误用)。
|
|
702
|
+
*/
|
|
703
|
+
correctEdge(opts: {
|
|
704
|
+
edgeId: string;
|
|
705
|
+
action: 'weaken' | 'strengthen' | 'remove';
|
|
706
|
+
/** weaken 默认 0.5;strengthen 默认 1.5;remove 忽略 */
|
|
707
|
+
multiplier?: number;
|
|
708
|
+
reason: string;
|
|
709
|
+
/** 调用来源标识,默认 'llm' */
|
|
710
|
+
by?: string;
|
|
711
|
+
/** true → 跳过阶梯保护(仅 manual / 系统纠错使用) */
|
|
712
|
+
force?: boolean;
|
|
713
|
+
}): Promise<{
|
|
714
|
+
action: 'weakened' | 'strengthened' | 'removed';
|
|
715
|
+
edgeId: string;
|
|
716
|
+
from: number;
|
|
717
|
+
to: number;
|
|
718
|
+
edge?: RelationEdge;
|
|
719
|
+
}>;
|
|
720
|
+
mergeAlias(opts: {
|
|
721
|
+
aliasId: string;
|
|
722
|
+
canonicalId: string;
|
|
723
|
+
kind: 'person' | 'entity' | 'event';
|
|
724
|
+
/** 若为 true,不做启发式校正,强制按传入方向 */
|
|
725
|
+
noCanonicalCorrection?: boolean;
|
|
726
|
+
}): Promise<{
|
|
727
|
+
effectiveCanonicalId: string;
|
|
728
|
+
effectiveAliasId: string;
|
|
729
|
+
edgesRewritten: number;
|
|
730
|
+
edgesMerged: number;
|
|
731
|
+
edgesDeleted: number;
|
|
732
|
+
swapped: boolean;
|
|
733
|
+
/** alias 节点是否被物理删除(即"真合并"是否完成)。person 当前总为 true(无 aliases 字段需合并) */
|
|
734
|
+
aliasDeleted: boolean;
|
|
735
|
+
}>;
|
|
736
|
+
/**
|
|
737
|
+
* 物理删除 event / entity 节点(级联删边)。Person 节点禁用。
|
|
738
|
+
* 保护门:weight ≥ 0.8 或 evidence.length ≥ 5 直接拒绝。
|
|
739
|
+
*/
|
|
740
|
+
deleteNode(opts: {
|
|
741
|
+
kind: 'event' | 'entity';
|
|
742
|
+
id: string;
|
|
743
|
+
reason: string;
|
|
744
|
+
by?: string;
|
|
745
|
+
}): Promise<{
|
|
746
|
+
kind: 'event' | 'entity';
|
|
747
|
+
id: string;
|
|
748
|
+
deletedEdges: number;
|
|
749
|
+
}>;
|
|
750
|
+
private _assertNodeDeletable;
|
|
751
|
+
/**
|
|
752
|
+
* consolidate hierarchy 守门用:若 child→parent 的 entity-entity[part-of] 边缺失则新建;
|
|
753
|
+
* 已存在(任一方向:part-of 或反向 contains)则跳过,返回 false。
|
|
754
|
+
* 不强化既有边、不做证据合并——hierarchy 守门只负责"不被错误地 alias-merge 掉",
|
|
755
|
+
* 后续走正规 inferEntityHierarchy / extractor 写边路径补全/强化。
|
|
756
|
+
*/
|
|
757
|
+
private _upsertPartOfEdgeIfAbsent;
|
|
758
|
+
/**
|
|
759
|
+
* 物理删除一条边(带保护门,供 agent 调用)。alias 边(is-alias-of / alt-account-of)禁删;
|
|
760
|
+
* weight ≥ 0.8 或 evidence ≥ 5 拒绝(请先 correctEdge weaken)。
|
|
761
|
+
*
|
|
762
|
+
* 注:与旧 deleteEdge(edgeId)(无保护,供 consolidate 内部使用)区别开。
|
|
763
|
+
*/
|
|
764
|
+
deleteEdgeWithGuard(opts: {
|
|
765
|
+
edgeId: string;
|
|
766
|
+
reason: string;
|
|
767
|
+
by?: string;
|
|
768
|
+
}): Promise<{
|
|
769
|
+
edgeId: string;
|
|
770
|
+
kind: string;
|
|
771
|
+
relationType: string;
|
|
772
|
+
weight: number;
|
|
773
|
+
}>;
|
|
774
|
+
/**
|
|
775
|
+
* 物理合并:把 aliasIds 全部并入 canonicalId,并物理删除 aliasIds。
|
|
776
|
+
* 仅支持 event / entity(person 合并请走 mergeAlias,保留 alias 标记边)。
|
|
777
|
+
*
|
|
778
|
+
* 内部分两步:
|
|
779
|
+
* 1) 复用 mergeAlias 把每个 alias 的边改写到 canonical(保留同名 alias 标记边以便回溯);
|
|
780
|
+
* 2) 物理删除 alias 节点本身(cascade 顺手清理残留的 alias 标记边)。
|
|
781
|
+
*/
|
|
782
|
+
mergeNodes(opts: {
|
|
783
|
+
kind: 'event' | 'entity';
|
|
784
|
+
canonicalId: string;
|
|
785
|
+
aliasIds: string[];
|
|
786
|
+
reason: string;
|
|
787
|
+
by?: string;
|
|
788
|
+
}): Promise<{
|
|
789
|
+
canonicalId: string;
|
|
790
|
+
mergedAliasIds: string[];
|
|
791
|
+
totalEdgesRewritten: number;
|
|
792
|
+
totalEdgesMerged: number;
|
|
793
|
+
totalEdgesDeleted: number;
|
|
794
|
+
}>;
|
|
795
|
+
/**
|
|
796
|
+
* 修改 entity 的 kind(topic/place/thing/work)。轻量操作,仅写入字段 + audit。
|
|
797
|
+
* 不变更 id,所有引用边 0 风险。
|
|
798
|
+
*/
|
|
799
|
+
changeEntityKind(opts: {
|
|
800
|
+
entityId: string;
|
|
801
|
+
newKind: EntityKind;
|
|
802
|
+
reason: string;
|
|
803
|
+
by?: string;
|
|
804
|
+
}): Promise<{
|
|
805
|
+
entityId: string;
|
|
806
|
+
from: EntityKind;
|
|
807
|
+
to: EntityKind;
|
|
808
|
+
}>;
|
|
809
|
+
/**
|
|
810
|
+
* 从 entity 的 aliases[] 中剥离一个错误绑定的别名(轻量纠错)。
|
|
811
|
+
*
|
|
812
|
+
* 使用场景:consolidate 把一个不该并入 canonical 的别名错误合并了,
|
|
813
|
+
* 导致 canonical 的 aliases 里出现了一个本不属于它的名字(如把母概念错并入子概念,
|
|
814
|
+
* 母概念的名字残留为子概念的 alias)。本方法只把该名字从 aliases 中移除——
|
|
815
|
+
* **不会**重建出当初被合并掉的 entity 节点(那个节点已被物理删除,证据已迁移)。
|
|
816
|
+
* 之后 extractor 在新对话中再次看到该名字时,会自然地新建一个新 entity 节点。
|
|
817
|
+
*
|
|
818
|
+
* 不变更:name / 边 / evidence / nameHistory(不动 nameHistory:renameNode 才追写)。
|
|
819
|
+
* 别名匹配按 trim 后字符串等价(不区分大小写不在此处处理;如有需要由调用方先归一)。
|
|
820
|
+
*/
|
|
821
|
+
splitAlias(opts: {
|
|
822
|
+
entityId: string;
|
|
823
|
+
aliasName: string;
|
|
824
|
+
reason: string;
|
|
825
|
+
by?: string;
|
|
826
|
+
}): Promise<{
|
|
827
|
+
entityId: string;
|
|
828
|
+
removed: string;
|
|
829
|
+
remainingAliases: string[];
|
|
830
|
+
}>;
|
|
831
|
+
/**
|
|
832
|
+
* 计算单个节点的「综合活跃度评分 + 排名 + 分级」,供 agent 快速判断节点份量。
|
|
833
|
+
*
|
|
834
|
+
* 返回字段语义:
|
|
835
|
+
* - compositeScore: 0..1 综合分(pagerank 0.4 + edgeWeight 0.3 + recency 0.2 + degree 0.1)
|
|
836
|
+
* - tier: 'core' | 'active' | 'normal' | 'edge',绝对分 + 同 kind 百分位双门槛分级
|
|
837
|
+
* - rankInKind / rankInGlobal: 'k/N' 字符串,按 compositeScore 降序,1=最高
|
|
838
|
+
* - percentileInKind / percentileInGlobal: 0..1,0.95=前 5%,越大越中心
|
|
839
|
+
* - pagerankFresh: false=节点从未参与过 PR 计算(lastPageRankAt=0),pagerank=0 不代表"边缘"
|
|
840
|
+
* - 其它字段:相关邻居计数 / 入边权 / pagerank 快照 / evidence 数 / 距上次强化天数
|
|
841
|
+
*
|
|
842
|
+
* 复杂度:O(N) 全图扫描,N=节点总数(几百到几千可接受;如果发现卡顿可加节点缓存)。
|
|
843
|
+
*/
|
|
844
|
+
computeNodeScore(nodeId: string): Promise<{
|
|
845
|
+
nodeId: string;
|
|
846
|
+
kind: 'person' | 'event' | 'entity';
|
|
847
|
+
name: string;
|
|
848
|
+
relatedPeople: number;
|
|
849
|
+
relatedEvents: number;
|
|
850
|
+
relatedEntities: number;
|
|
851
|
+
maxIncomingEdgeWeight: number;
|
|
852
|
+
avgIncomingEdgeWeight: number;
|
|
853
|
+
pagerank: number;
|
|
854
|
+
pagerankFresh: boolean;
|
|
855
|
+
evidenceCount: number;
|
|
856
|
+
daysSinceLastReinforced: number;
|
|
857
|
+
compositeScore: number;
|
|
858
|
+
tier: 'core' | 'active' | 'normal' | 'edge';
|
|
859
|
+
rankInKind: string;
|
|
860
|
+
rankInGlobal: string;
|
|
861
|
+
percentileInKind: number;
|
|
862
|
+
percentileInGlobal: number;
|
|
863
|
+
} | null>;
|
|
864
|
+
/**
|
|
865
|
+
* 内部:单节点综合分计算(不含排名)。抽出复用:computeNodeScore 与 actions graph_data。
|
|
866
|
+
*/
|
|
867
|
+
_computeSingleNodeScore(nodeId: string, snap: {
|
|
868
|
+
persons: PersonNode[];
|
|
869
|
+
events: EventNode[];
|
|
870
|
+
entities: EntityNode[];
|
|
871
|
+
edges: RelationEdge[];
|
|
872
|
+
}): {
|
|
873
|
+
nodeId: string;
|
|
874
|
+
kind: 'person' | 'event' | 'entity';
|
|
875
|
+
name: string;
|
|
876
|
+
relatedPeople: number;
|
|
877
|
+
relatedEvents: number;
|
|
878
|
+
relatedEntities: number;
|
|
879
|
+
maxIncomingEdgeWeight: number;
|
|
880
|
+
avgIncomingEdgeWeight: number;
|
|
881
|
+
pagerank: number;
|
|
882
|
+
pagerankFresh: boolean;
|
|
883
|
+
evidenceCount: number;
|
|
884
|
+
daysSinceLastReinforced: number;
|
|
885
|
+
compositeScore: number;
|
|
886
|
+
} | null;
|
|
887
|
+
/**
|
|
888
|
+
* 计算节点的「邻居剖面」:返回每类邻居 (人/事件/实体) 的 **总数 + top-K {name, weight}**。
|
|
889
|
+
* 用于 consolidate verifyAliasPair / verifyEventPair 给 LLM 提供更丰富的邻居证据。
|
|
890
|
+
*
|
|
891
|
+
* - weight 取节点对之间所有边的 weight 之和(同一对实体可能既是 mentioned 又是 enthusiast)
|
|
892
|
+
* - 排序:weight 倒序;同权按 name 升序保证可复现
|
|
893
|
+
* - 名字截断到 24 字以控制 prompt 体积
|
|
894
|
+
* - 命中不存在的 otherId(被删/孤立)→ 跳过
|
|
895
|
+
*/
|
|
896
|
+
_computeNeighborProfile(nodeId: string, snap: {
|
|
897
|
+
persons: PersonNode[];
|
|
898
|
+
events: EventNode[];
|
|
899
|
+
entities: EntityNode[];
|
|
900
|
+
edges: RelationEdge[];
|
|
901
|
+
}, topK?: number): {
|
|
902
|
+
peopleCount: number;
|
|
903
|
+
eventCount: number;
|
|
904
|
+
entityCount: number;
|
|
905
|
+
topPeople: Array<{
|
|
906
|
+
name: string;
|
|
907
|
+
weight: number;
|
|
908
|
+
}>;
|
|
909
|
+
topEvents: Array<{
|
|
910
|
+
name: string;
|
|
911
|
+
weight: number;
|
|
912
|
+
}>;
|
|
913
|
+
topEntities: Array<{
|
|
914
|
+
name: string;
|
|
915
|
+
weight: number;
|
|
916
|
+
}>;
|
|
917
|
+
};
|
|
918
|
+
/**
|
|
919
|
+
* 计算节点的「方向性出入度剖面」:返回 outByType / inByType / dominance / fanIdolHint。
|
|
920
|
+
*
|
|
921
|
+
* 仅统计 **有向的主体边**(person-person / event-event / entity-entity 且 directed=true)。
|
|
922
|
+
* 桥型边(person-event / person-entity / event-entity)按设计天然双向,是"参与"不是"指代",
|
|
923
|
+
* 不计入此剖面。
|
|
924
|
+
*
|
|
925
|
+
* 返回的 `outByType` 含义:节点作为 from 端发出的边("我主动指向谁"),按 relationType 分桶;
|
|
926
|
+
* `inByType`:节点作为 to 端接收的边("谁指向我")。每个桶含 count / totalWeight / top-K 对端节点。
|
|
927
|
+
*
|
|
928
|
+
* `dominance` 启发式判断:
|
|
929
|
+
* - outTotal - inTotal >= 2 且 outTotal/inTotal >= 1.5 → 'outgoing'(更偏向"主动方",如典型粉丝/学生)
|
|
930
|
+
* - inTotal - outTotal >= 2 且 inTotal/outTotal >= 1.5 → 'incoming'(更偏向"被指方",如典型偶像/导师)
|
|
931
|
+
* - 否则 'balanced'
|
|
932
|
+
*
|
|
933
|
+
* `fanIdolHint` 专门拎出 admirer 关系:fansCount = 入度 admirer(多少人 admire 我),
|
|
934
|
+
* idolsCount = 出度 admirer(我 admire 多少人)。verdict 给出粗判。
|
|
935
|
+
*/
|
|
936
|
+
computeDirectionalDegree(nodeId: string, options?: {
|
|
937
|
+
topPerType?: number;
|
|
938
|
+
}): Promise<{
|
|
939
|
+
nodeId: string;
|
|
940
|
+
kind: 'person' | 'event' | 'entity';
|
|
941
|
+
name: string;
|
|
942
|
+
outTotal: number;
|
|
943
|
+
inTotal: number;
|
|
944
|
+
outByType: Record<string, {
|
|
945
|
+
count: number;
|
|
946
|
+
totalWeight: number;
|
|
947
|
+
top: Array<{
|
|
948
|
+
otherId: string;
|
|
949
|
+
otherName: string;
|
|
950
|
+
weight: number;
|
|
951
|
+
kind: 'person' | 'event' | 'entity';
|
|
952
|
+
}>;
|
|
953
|
+
}>;
|
|
954
|
+
inByType: Record<string, {
|
|
955
|
+
count: number;
|
|
956
|
+
totalWeight: number;
|
|
957
|
+
top: Array<{
|
|
958
|
+
otherId: string;
|
|
959
|
+
otherName: string;
|
|
960
|
+
weight: number;
|
|
961
|
+
kind: 'person' | 'event' | 'entity';
|
|
962
|
+
}>;
|
|
963
|
+
}>;
|
|
964
|
+
dominance: 'outgoing' | 'incoming' | 'balanced';
|
|
965
|
+
fanIdolHint: {
|
|
966
|
+
fansCount: number;
|
|
967
|
+
idolsCount: number;
|
|
968
|
+
verdict: 'idol-leaning' | 'fan-leaning' | 'mutual' | 'none';
|
|
969
|
+
};
|
|
970
|
+
} | null>;
|
|
971
|
+
/**
|
|
972
|
+
* 同社群活跃成员(Louvain 社群标签由 evictByQuota 写入;未跑过则返回空)。
|
|
973
|
+
*
|
|
974
|
+
* 用途:profile 注入 / agent 工具——「跟 X 同一个圈子的高活跃成员是谁」。
|
|
975
|
+
*
|
|
976
|
+
* - personId 必须是 `<platform>:<userId>` 完整 ID;
|
|
977
|
+
* - 仅返回 person 类型的同社群成员(事件/实体也有 communityId 但用户视角无意义);
|
|
978
|
+
* - 按 lastPageRank desc 排序,截断到 limit;
|
|
979
|
+
* - 自己不会出现在结果里;
|
|
980
|
+
* - 如果 personId 没有 communityId(节点太新或从未跑过 evict)→ communitySize=0、peers=[]。
|
|
981
|
+
*/
|
|
982
|
+
getCommunityPeers(personId: string, limit?: number): Promise<{
|
|
983
|
+
personId: string;
|
|
984
|
+
communityId: string | null;
|
|
985
|
+
communitySize: number;
|
|
986
|
+
peers: Array<{
|
|
987
|
+
id: string;
|
|
988
|
+
displayName: string;
|
|
989
|
+
pagerank: number;
|
|
990
|
+
communityId: string;
|
|
991
|
+
}>;
|
|
992
|
+
}>;
|
|
993
|
+
/**
|
|
994
|
+
* 两人是否同社群 + 各自社群信息(agent 工具 community_bridge 用)。
|
|
995
|
+
*
|
|
996
|
+
* 不算路径——find_path 已有同等能力,重复实现徒增维护。
|
|
997
|
+
*/
|
|
998
|
+
getCommunityBridge(personAId: string, personBId: string): Promise<{
|
|
999
|
+
a: {
|
|
1000
|
+
id: string;
|
|
1001
|
+
displayName: string;
|
|
1002
|
+
communityId: string | null;
|
|
1003
|
+
communitySize: number;
|
|
1004
|
+
};
|
|
1005
|
+
b: {
|
|
1006
|
+
id: string;
|
|
1007
|
+
displayName: string;
|
|
1008
|
+
communityId: string | null;
|
|
1009
|
+
communitySize: number;
|
|
1010
|
+
};
|
|
1011
|
+
sameCommunity: boolean;
|
|
1012
|
+
}>;
|
|
1013
|
+
/**
|
|
1014
|
+
* 全图社群概览:按 community 分组,每组列 top 成员/话题/事件,再算 modularity Q 和"桥梁人"。
|
|
1015
|
+
*
|
|
1016
|
+
* 设计要点:
|
|
1017
|
+
* - `algorithm` 不传默认实时跑一遍 Louvain;传 'leiden' 跑 Leiden-lite;传 'slpa' 跑 SLPA(原生重叠)。
|
|
1018
|
+
* **不读节点上的 communityId 缓存**,保证每次调用结果与当前快照严格一致(即使 evictByQuota 还没跑)。
|
|
1019
|
+
* - `sessionScope` 是**后过滤**:先在全图上跑社群算法(保证社群划分准确),再只统计 evidence 含该 scope
|
|
1020
|
+
* 的节点,避免把"跨群关系"切断。
|
|
1021
|
+
* - 每个节点的**分组归属**始终按"主社群"(memberships[0].id,即 SLPA 下 weight 最高的 label);
|
|
1022
|
+
* 保持 topMembers/Topics/Events 语义清晰,避免一个节点在多社群重复出现稀释 LLM 注意力。
|
|
1023
|
+
* - `topN`:每个社群展示的成员/话题/事件条数。默认动态:log2 自适应。传 0 = 不限。
|
|
1024
|
+
* - `bridges`:跨社群联系最广的 top-K person。`crossCommunityDegree` = 邻居中不在该 person 自身**任一**社群
|
|
1025
|
+
* 隶属里的"外社群"个数(SLPA 下自然把跨群人物的多归属考虑进去);`communityWeights` 给出按"外社群"分组的
|
|
1026
|
+
* 邻居边权累计(边越重 / 邻居越多 → weight 越大),供 LLM 判断该桥梁人物的跨群强度分布。
|
|
1027
|
+
*
|
|
1028
|
+
* Q(modularity)值粗判:Q > 0.3 = 圈子分明;0.1 ~ 0.3 = 一般;< 0.1 = 接近随机划分。
|
|
1029
|
+
* SLPA 下 Q 用"主社群"作为硬划分近似计算,仅作参考——重叠社区没有标准 modularity 定义。
|
|
1030
|
+
*/
|
|
1031
|
+
getCommunityOverview(opts?: {
|
|
1032
|
+
sessionScope?: string;
|
|
1033
|
+
/** 0 = 不限 */
|
|
1034
|
+
topN?: number;
|
|
1035
|
+
/** 不传则跑 louvain;可临时切 leiden 或 slpa */
|
|
1036
|
+
algorithm?: 'louvain' | 'leiden' | 'slpa';
|
|
1037
|
+
/**
|
|
1038
|
+
* Louvain/Leiden 分辨率 γ:默认 1.0(标准模块度)。
|
|
1039
|
+
* - γ > 1:划得更细 → 社群数变多、单社群更小
|
|
1040
|
+
* - γ < 1:划得更粗 → 社群数变少、单社群更大
|
|
1041
|
+
* 常用范围 0.5 ~ 3.0。超出 [0.01, 100] 会被夹紧。
|
|
1042
|
+
* **SLPA 不使用该参数**(SLPA 的粒度由阈值 r 控制,本服务暂不暴露)。
|
|
1043
|
+
*
|
|
1044
|
+
* 传 `'auto'` 启用图规模自适应(推荐):γ = clamp(0.6, 2.5, 0.5 + log10(n / 30))。
|
|
1045
|
+
* 详见 `computeAdaptiveResolution`。数值默认仍为 1.0,向后兼容。
|
|
1046
|
+
*/
|
|
1047
|
+
resolution?: number | 'auto';
|
|
1048
|
+
}): Promise<{
|
|
1049
|
+
algorithm: 'louvain' | 'leiden' | 'slpa';
|
|
1050
|
+
/**
|
|
1051
|
+
* 本次实际生效的 γ。SLPA 下为 `null`(不使用)。
|
|
1052
|
+
* `resolutionMode='auto'` 时为 `computeAdaptiveResolution(snap)` 的输出;
|
|
1053
|
+
* `'explicit'` 时为调用方传入的数值;`'default'` 时为 1.0。
|
|
1054
|
+
*/
|
|
1055
|
+
effectiveResolution: number | null;
|
|
1056
|
+
resolutionMode: 'auto' | 'explicit' | 'default';
|
|
1057
|
+
numCommunities: number;
|
|
1058
|
+
/** SLPA 下基于主社群的硬划分近似 modularity,仅供参考。 */
|
|
1059
|
+
modularity: number;
|
|
1060
|
+
totalPersonsInScope: number;
|
|
1061
|
+
totalEventsInScope: number;
|
|
1062
|
+
totalEntitiesInScope: number;
|
|
1063
|
+
communities: Array<{
|
|
1064
|
+
communityId: string;
|
|
1065
|
+
size: number;
|
|
1066
|
+
topMembers: Array<{
|
|
1067
|
+
id: string;
|
|
1068
|
+
displayName: string;
|
|
1069
|
+
pagerank: number;
|
|
1070
|
+
}>;
|
|
1071
|
+
topTopics: Array<{
|
|
1072
|
+
id: string;
|
|
1073
|
+
name: string;
|
|
1074
|
+
pagerank: number;
|
|
1075
|
+
}>;
|
|
1076
|
+
topEvents: Array<{
|
|
1077
|
+
id: string;
|
|
1078
|
+
title: string;
|
|
1079
|
+
weight: number;
|
|
1080
|
+
sessionScope?: string;
|
|
1081
|
+
}>;
|
|
1082
|
+
}>;
|
|
1083
|
+
bridges: Array<{
|
|
1084
|
+
id: string;
|
|
1085
|
+
displayName: string;
|
|
1086
|
+
/** 该 person 自身的主社群(SLPA 下即权重最高的 label) */
|
|
1087
|
+
communityId: string;
|
|
1088
|
+
/** SLPA 下该 person 的全部社群隶属度(按 weight 降序);louvain/leiden 永远单元素 */
|
|
1089
|
+
communityMemberships: CommunityMembership[];
|
|
1090
|
+
/** 邻居中不在自身任一社群隶属里的"外社群"个数(向下兼容老消费方) */
|
|
1091
|
+
crossCommunityDegree: number;
|
|
1092
|
+
/**
|
|
1093
|
+
* 按"外社群"分组的邻居边权累计(按 weight 降序)。weight = ∑(邻居边权 × 邻居在该外社群的隶属度)。
|
|
1094
|
+
* 用这个字段判断桥梁人物在每个跨群方向上的强度分布;degree 只告诉数量,weights 告诉力度。
|
|
1095
|
+
*/
|
|
1096
|
+
communityWeights: Array<{
|
|
1097
|
+
communityId: string;
|
|
1098
|
+
weight: number;
|
|
1099
|
+
}>;
|
|
1100
|
+
}>;
|
|
1101
|
+
}>;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* compositeScore + 同 kind 百分位双门槛分级。绝对分给"够亮"的小图节点保底,
|
|
1105
|
+
* 百分位给"大图但绝对分都低"的相对核心节点保底。
|
|
1106
|
+
*/
|
|
1107
|
+
export declare function scoreToTier(score: number, percentile: number): 'core' | 'active' | 'normal' | 'edge';
|
|
1108
|
+
//# sourceMappingURL=service.d.ts.map
|