dsh-prime-memory 0.26.31 → 0.26.33
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/client.js +1760 -8
- package/dist/config.js +6 -0
- package/dist/contract.d.ts +66 -1
- package/dist/graph/constraints.d.ts +14 -0
- package/dist/graph/constraints.js +14 -0
- package/dist/graph/preview.d.ts +22 -0
- package/dist/graph/preview.js +25 -7
- package/dist/graph/record-links.d.ts +61 -0
- package/dist/graph/record-links.js +167 -0
- package/dist/graph/select.d.ts +30 -0
- package/dist/graph/select.js +124 -0
- package/dist/settings.js +3 -0
- package/dist/stats.d.ts +3 -2
- package/dist/stats.js +81 -5
- package/dist/store/graph-store.d.ts +37 -1
- package/dist/store/graph-store.js +120 -14
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -103,6 +103,12 @@ export function liveSettingsSchema() {
|
|
|
103
103
|
/** 时效分级门2 面板开关:与部署 cfg.validity.tieringEnabled 取或(任一为真即启用
|
|
104
104
|
* 注入侧 expired 过滤)。默认关;先面板验证再翻部署默认(照 keywordEnabled 先例)。 */
|
|
105
105
|
validityTiering: Schema.boolean().default(false),
|
|
106
|
+
/**
|
|
107
|
+
* 记忆库侧栏入口开关(概览页可拨):默认**开**(与其余进阶项默认关不同)。
|
|
108
|
+
* 必须在 live schema 里有名:schema 外的键写入会被丢弃,重启后读不回,
|
|
109
|
+
* 面板上拨了等于白拨(真机踩过的缺写点)。
|
|
110
|
+
*/
|
|
111
|
+
explorerEnabled: Schema.boolean().default(true),
|
|
106
112
|
/**
|
|
107
113
|
* 存量回填(keyword / redistill)LLM 批调用的**运行时并行度**覆盖:
|
|
108
114
|
* `0` = 跟随部署 `cfg.backfill.concurrency`;1–8 = 直接覆盖。
|
package/dist/contract.d.ts
CHANGED
|
@@ -131,6 +131,8 @@ export interface MemoryLiveSettings {
|
|
|
131
131
|
memportEnabled: boolean;
|
|
132
132
|
/** 时效分级门2 启用开关(概览区可拨):true = 被动注入剔除 expired。与部署 cfg.validity.tieringEnabled 取或;关=注入与改前逐字一致。 */
|
|
133
133
|
validityTiering: boolean;
|
|
134
|
+
/** 记忆库侧栏入口开关(概览区可拨):true = 在左侧栏注册「记忆库」入口行 + 主面板。默认 true。 */
|
|
135
|
+
explorerEnabled: boolean;
|
|
134
136
|
/** md 投射/后缀收敛的单次动作护栏运行时覆盖:0 = 跟随部署 `cfg.memExport.maxRecords`(默认 2000)。
|
|
135
137
|
* 存在的理由:一次点完多少条属于“回填历史时才改一下”的旋钮,不该要求改 YAML + 重启。 */
|
|
136
138
|
mdExportMaxRecords: number;
|
|
@@ -1335,12 +1337,15 @@ export interface GraphPreviewResponse {
|
|
|
1335
1337
|
missing?: number;
|
|
1336
1338
|
};
|
|
1337
1339
|
/** mode==='full' 才出现;全图星图轻量块(可见节点×active 边,截断标记 truncated)。
|
|
1338
|
-
* 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。
|
|
1340
|
+
* 不带此键时响应与旧形状逐字节一致——消费方按需读 `res.full?`。
|
|
1341
|
+
* 节点 `aging`(0.5~1,3D 亮度通道)与 `source`(出处场景名,provenance fallback)同为可选键:无值时**缺席**。 */
|
|
1339
1342
|
full?: {
|
|
1340
1343
|
nodes: Array<{
|
|
1341
1344
|
id: string;
|
|
1342
1345
|
name: string;
|
|
1343
1346
|
type: string;
|
|
1347
|
+
aging?: number;
|
|
1348
|
+
source?: string;
|
|
1344
1349
|
}>;
|
|
1345
1350
|
edges: Array<{
|
|
1346
1351
|
from: string;
|
|
@@ -1358,6 +1363,64 @@ export interface GraphCleanupDeadRequest {
|
|
|
1358
1363
|
export interface GraphCleanupDeadResponse {
|
|
1359
1364
|
removed: number;
|
|
1360
1365
|
}
|
|
1366
|
+
/**
|
|
1367
|
+
* dsh-memory/record-links(记忆库入口的关联跳转:记录 → 实体 → 关系 → 其它记录)。
|
|
1368
|
+
*
|
|
1369
|
+
* L1 记录本身**不是图顶点**(`l1_records` DDL 冻结、无 relation 列),所以关联一律
|
|
1370
|
+
* 经图投影的 `sourceRecordIds` **反查**得到——本端点是纯读面,不写图、不建边、
|
|
1371
|
+
* 不改投影。图谱未启用时返回全空结构(不抛),与 graph-preview 的降级口径一致。
|
|
1372
|
+
*/
|
|
1373
|
+
export interface RecordLinksRequest {
|
|
1374
|
+
/** 起点 L1 记录 id(≤200 字符;不存在 → `record: null` + 空集,不抛)。 */
|
|
1375
|
+
recordId: string;
|
|
1376
|
+
/** 邻居展开跳数,缺省 1,钳制 1..2(再深只会放大 hub 爆量,不给新信息)。 */
|
|
1377
|
+
hops?: number;
|
|
1378
|
+
/** 邻居与回落记录各自的条数上限,缺省 60,钳制 1..200。 */
|
|
1379
|
+
limit?: number;
|
|
1380
|
+
}
|
|
1381
|
+
export interface RecordLinksResponse {
|
|
1382
|
+
/** 层1 起点记录(悬挂 id → null)。 */
|
|
1383
|
+
record: UiRecord | null;
|
|
1384
|
+
/** 层2:该记录作为来源的实体节点(按 degree 降序)。 */
|
|
1385
|
+
entities: Array<{
|
|
1386
|
+
id: string;
|
|
1387
|
+
name: string;
|
|
1388
|
+
type: string;
|
|
1389
|
+
degree: number;
|
|
1390
|
+
causalDegree: number;
|
|
1391
|
+
/** 来源记录预览(保最新、≤5 条,已排除起点自身)。 */
|
|
1392
|
+
recordIds: string[];
|
|
1393
|
+
}>;
|
|
1394
|
+
/** 层3:实体的一度邻居。平行关系各占一条(同对节点两条关系 = 两行,不合并)。 */
|
|
1395
|
+
neighbors: Array<{
|
|
1396
|
+
id: string;
|
|
1397
|
+
name: string;
|
|
1398
|
+
type: string;
|
|
1399
|
+
relation: string;
|
|
1400
|
+
causal: boolean;
|
|
1401
|
+
/** 该邻居经由哪个层2 实体连出——多实体起点下用于回溯「经由谁」。 */
|
|
1402
|
+
viaEntityId: string;
|
|
1403
|
+
degree: number;
|
|
1404
|
+
/** 本邻居是第几跳(1 = 实体的直接邻居)。 */
|
|
1405
|
+
hop: number;
|
|
1406
|
+
/** 邻居的来源记录预览(≤5 条)——第三层「跳到记录」的落点。 */
|
|
1407
|
+
recordIds: string[];
|
|
1408
|
+
}>;
|
|
1409
|
+
/** 回落记录:邻居实体的来源记录(去重、排除起点自身、updatedAt 倒序、≤limit)。 */
|
|
1410
|
+
records: UiRecord[];
|
|
1411
|
+
/**
|
|
1412
|
+
* 取代链。方向语义由 marker 的位置决定,不能凭直觉写:
|
|
1413
|
+
* `dsh_superseded` **写在退场的旧行上**、`by` 指向新行——
|
|
1414
|
+
* 故 `prevIds` 是**数组**(本记录取代掉的旧行,`findSupersededTargets` 反查),
|
|
1415
|
+
* `nextId` 是**单值**(取代本记录的新行;本记录仍活跃时为 null)。
|
|
1416
|
+
*/
|
|
1417
|
+
supersede: {
|
|
1418
|
+
prevIds: string[];
|
|
1419
|
+
nextId: string | null;
|
|
1420
|
+
};
|
|
1421
|
+
/** 邻居或回落记录任一维触顶(渲染侧必须显式提示,不得静默少列)。 */
|
|
1422
|
+
truncated: boolean;
|
|
1423
|
+
}
|
|
1361
1424
|
/** dsh-memory/scenes(两族拼接的混合视图)。 */
|
|
1362
1425
|
export interface ScenesResponse {
|
|
1363
1426
|
items: Array<{
|
|
@@ -1954,6 +2017,7 @@ export interface DshMemoryRequestMap {
|
|
|
1954
2017
|
'dsh-memory/graph-search': GraphSearchRequest;
|
|
1955
2018
|
'dsh-memory/graph-node-get': GraphNodeGetRequest;
|
|
1956
2019
|
'dsh-memory/graph-preview': GraphPreviewRequest;
|
|
2020
|
+
'dsh-memory/record-links': RecordLinksRequest;
|
|
1957
2021
|
'dsh-memory/graph-cleanup-dead': GraphCleanupDeadRequest;
|
|
1958
2022
|
'dsh-memory/scenes': Record<string, never>;
|
|
1959
2023
|
'dsh-memory/persona': Record<string, never>;
|
|
@@ -2037,6 +2101,7 @@ export interface DshMemoryResponseMap {
|
|
|
2037
2101
|
'dsh-memory/graph-search': GraphSearchResponse;
|
|
2038
2102
|
'dsh-memory/graph-node-get': GraphNodeGetResponse;
|
|
2039
2103
|
'dsh-memory/graph-preview': GraphPreviewResponse;
|
|
2104
|
+
'dsh-memory/record-links': RecordLinksResponse;
|
|
2040
2105
|
'dsh-memory/graph-cleanup-dead': GraphCleanupDeadResponse;
|
|
2041
2106
|
'dsh-memory/scenes': ScenesResponse;
|
|
2042
2107
|
'dsh-memory/persona': PersonaResponse;
|
|
@@ -13,6 +13,15 @@ export declare const GRAPH_NODE_TYPE_SET: ReadonlySet<string>;
|
|
|
13
13
|
/** 记录生命周期四态(节点/fact/边通用)。 */
|
|
14
14
|
export declare const GRAPH_STATUSES: readonly GraphRecordStatus[];
|
|
15
15
|
export declare const GRAPH_STATUS_SET: ReadonlySet<string>;
|
|
16
|
+
/**
|
|
17
|
+
* 节点是否进入「可画 / 可检 / 可关联」的候选集——读侧可见性判据的**单源**。
|
|
18
|
+
*
|
|
19
|
+
* 预览榜单、全图元素选择、关联反查三处必须同式,否则会出现「画布上有这个节点、
|
|
20
|
+
* 关联清单里查不到」或反之的对不齐。superseded/archived 是历史而非当前状态。
|
|
21
|
+
*/
|
|
22
|
+
export declare function isGraphNodeVisible(n: {
|
|
23
|
+
status: GraphRecordStatus;
|
|
24
|
+
}): boolean;
|
|
16
25
|
/** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
|
|
17
26
|
export declare const GRAPH_CAUSAL_RELATIONS: readonly ["导致", "因为", "修复了", "取代"];
|
|
18
27
|
/** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
|
|
@@ -49,3 +58,8 @@ export declare const GRAPH_CAP: {
|
|
|
49
58
|
* 状态仍由 active facts 决定,不影响可追溯性主体。
|
|
50
59
|
*/
|
|
51
60
|
export declare const GRAPH_SOURCE_RECORDS_CAP = 128;
|
|
61
|
+
/**
|
|
62
|
+
* 星图老化剔除下限:`buildFullView` 收到 agingOf 时,aging 低于此值的节点不渲染(连悬挂边一起退)。
|
|
63
|
+
* 与 explorer 的 `agingWeightOf(..., floor=0.2)` 同值——下限即剔除线,低于它已经暗到看不出。
|
|
64
|
+
*/
|
|
65
|
+
export declare const GRAPH_AGING_CULL_FLOOR = 0.2;
|
|
@@ -4,6 +4,15 @@ export const GRAPH_NODE_TYPE_SET = new Set(GRAPH_NODE_TYPES);
|
|
|
4
4
|
/** 记录生命周期四态(节点/fact/边通用)。 */
|
|
5
5
|
export const GRAPH_STATUSES = ['active', 'superseded', 'disputed', 'archived'];
|
|
6
6
|
export const GRAPH_STATUS_SET = new Set(GRAPH_STATUSES);
|
|
7
|
+
/**
|
|
8
|
+
* 节点是否进入「可画 / 可检 / 可关联」的候选集——读侧可见性判据的**单源**。
|
|
9
|
+
*
|
|
10
|
+
* 预览榜单、全图元素选择、关联反查三处必须同式,否则会出现「画布上有这个节点、
|
|
11
|
+
* 关联清单里查不到」或反之的对不齐。superseded/archived 是历史而非当前状态。
|
|
12
|
+
*/
|
|
13
|
+
export function isGraphNodeVisible(n) {
|
|
14
|
+
return n.status === 'active' || n.status === 'disputed';
|
|
15
|
+
}
|
|
7
16
|
/** 因果边四词单源(投影提示词枚举、校验与面板预览加权共用——防两处词表漂移)。 */
|
|
8
17
|
export const GRAPH_CAUSAL_RELATIONS = ['导致', '因为', '修复了', '取代'];
|
|
9
18
|
/** 规范中文关系词(提示词给枚举;校验不做白名单——关系是开放词表,只做长度钳制)。
|
|
@@ -40,3 +49,8 @@ export const GRAPH_CAP = {
|
|
|
40
49
|
* 状态仍由 active facts 决定,不影响可追溯性主体。
|
|
41
50
|
*/
|
|
42
51
|
export const GRAPH_SOURCE_RECORDS_CAP = 128;
|
|
52
|
+
/**
|
|
53
|
+
* 星图老化剔除下限:`buildFullView` 收到 agingOf 时,aging 低于此值的节点不渲染(连悬挂边一起退)。
|
|
54
|
+
* 与 explorer 的 `agingWeightOf(..., floor=0.2)` 同值——下限即剔除线,低于它已经暗到看不出。
|
|
55
|
+
*/
|
|
56
|
+
export const GRAPH_AGING_CULL_FLOOR = 0.2;
|
package/dist/graph/preview.d.ts
CHANGED
|
@@ -71,6 +71,17 @@ export interface FullViewNode {
|
|
|
71
71
|
id: string;
|
|
72
72
|
name: string;
|
|
73
73
|
type: GraphNodeType;
|
|
74
|
+
/**
|
|
75
|
+
* 老化权重(0.5~1,与 UiRecord.agingWeight 同定义域)——3D 星图亮度通道的数据源。
|
|
76
|
+
* **可选键零漂移**:缺省(衰减关闭/无存活来源/调用方未注入 resolver)时该键**缺席**,
|
|
77
|
+
* 不是 undefined 占位——既有形状断言测试(三键)继续成立,老消费方 JSON 形状不变。
|
|
78
|
+
*/
|
|
79
|
+
aging?: number;
|
|
80
|
+
/**
|
|
81
|
+
* 出处标签(provenance fallback):来源记录的主场景名,如“字节Code M岗位”——
|
|
82
|
+
* 人物实体靠它回溯“在哪提到的”。同为可选键零漂移:无场景/未注入 resolver 时键缺席。
|
|
83
|
+
*/
|
|
84
|
+
source?: string;
|
|
74
85
|
}
|
|
75
86
|
/** 全图边轻量项(causal 由单源词表判出,渲染侧不再抄词表)。 */
|
|
76
87
|
export interface FullViewEdge {
|
|
@@ -88,6 +99,17 @@ export interface GraphFullView {
|
|
|
88
99
|
export interface FullViewCaps {
|
|
89
100
|
nodeCap?: number;
|
|
90
101
|
edgeCap?: number;
|
|
102
|
+
/**
|
|
103
|
+
* 节点老化解析器(nodeId → aging | null):由端点层查 L1 来源记录后注入,
|
|
104
|
+
* 本纯函数**不查库**。判据在调用方:节点无自己的老化——老化属于记录,
|
|
105
|
+
* 节点取全部存活来源中**最新**那条的 agingWeight(任一来源还新鲜,实体就不该显得旧)。
|
|
106
|
+
*/
|
|
107
|
+
agingOf?: (id: string) => number | null;
|
|
108
|
+
/**
|
|
109
|
+
* 出处解析器(nodeId → 场景名 | null):同 agingOf 一样由端点层查来源记录后注入,
|
|
110
|
+
* 本纯函数不查库。B 方案(投影期落库 context)上线后本项退为兜底。
|
|
111
|
+
*/
|
|
112
|
+
sourceOf?: (id: string) => string | null;
|
|
91
113
|
}
|
|
92
114
|
/**
|
|
93
115
|
* 全图星图数据块(纯函数,不查库):可见节点轻量投影 × active 边,
|
package/dist/graph/preview.js
CHANGED
|
@@ -9,13 +9,11 @@
|
|
|
9
9
|
* 判据与检索同源(search.ts):候选 = active|disputed 节点 × active 边;
|
|
10
10
|
* 确定性 = 同输入两次调用逐位一致(同分裁决链落到 id 升序兜底)。
|
|
11
11
|
*/
|
|
12
|
-
import { GRAPH_CAUSAL_RELATIONS } from './constraints.js';
|
|
12
|
+
import { GRAPH_AGING_CULL_FLOOR, GRAPH_CAUSAL_RELATIONS, isGraphNodeVisible } from './constraints.js';
|
|
13
13
|
import { GRAPH_PREVIEW_CAUSAL_WEIGHT, GRAPH_PREVIEW_FULL_EDGES, GRAPH_PREVIEW_FULL_NODES, GRAPH_PREVIEW_NEIGHBORS, GRAPH_PREVIEW_SEEDS, } from './types.js';
|
|
14
14
|
const CAUSAL_SET = new Set(GRAPH_CAUSAL_RELATIONS);
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
return n.status === 'active' || n.status === 'disputed';
|
|
18
|
-
}
|
|
15
|
+
/** 预览的节点可见性(与检索候选集同式):引用单源判据,不在此处重抄。 */
|
|
16
|
+
const visible = isGraphNodeVisible;
|
|
19
17
|
export function degreeMaps(nodes, edges) {
|
|
20
18
|
const candidate = new Set(nodes.filter(visible).map((n) => n.id));
|
|
21
19
|
const degree = new Map();
|
|
@@ -163,7 +161,17 @@ export function buildGraphPreview(nodes, edges, opts = {}) {
|
|
|
163
161
|
export function buildFullView(nodes, edges, caps = {}) {
|
|
164
162
|
const nodeCap = Math.max(1, Math.floor(caps.nodeCap ?? GRAPH_PREVIEW_FULL_NODES));
|
|
165
163
|
const edgeCap = Math.max(1, Math.floor(caps.edgeCap ?? GRAPH_PREVIEW_FULL_EDGES));
|
|
166
|
-
|
|
164
|
+
// 老化剔除线(仅当注入 agingOf 时生效):aging < 0.2 的节点及其悬挂边不渲染。
|
|
165
|
+
// aging=null(衰减关/无存活来源)不剔——不能因为拿不到数值就把节点删掉。
|
|
166
|
+
const cullByAging = caps.agingOf !== undefined;
|
|
167
|
+
const visibleNodes = nodes.filter((n) => {
|
|
168
|
+
if (!visible(n))
|
|
169
|
+
return false;
|
|
170
|
+
if (!cullByAging)
|
|
171
|
+
return true;
|
|
172
|
+
const a = caps.agingOf(n.id);
|
|
173
|
+
return a == null || a >= GRAPH_AGING_CULL_FLOOR;
|
|
174
|
+
});
|
|
167
175
|
const nodesTruncated = visibleNodes.length > nodeCap;
|
|
168
176
|
const drawn = visibleNodes.slice(0, nodeCap);
|
|
169
177
|
const keep = new Set(drawn.map((n) => n.id));
|
|
@@ -181,7 +189,17 @@ export function buildFullView(nodes, edges, caps = {}) {
|
|
|
181
189
|
outEdges.push({ from: e.fromNodeId, to: e.toNodeId, relation: e.relation, causal: CAUSAL_SET.has(e.relation) });
|
|
182
190
|
}
|
|
183
191
|
return {
|
|
184
|
-
nodes: drawn.map((n) =>
|
|
192
|
+
nodes: drawn.map((n) => {
|
|
193
|
+
const base = { id: n.id, name: n.name, type: n.type };
|
|
194
|
+
// 有值才挂键(可选键零漂移:null/无 resolver → 键缺席,老消费方看到的形状不变)。
|
|
195
|
+
const aging = caps.agingOf?.(n.id) ?? null;
|
|
196
|
+
if (aging != null)
|
|
197
|
+
base.aging = aging;
|
|
198
|
+
const source = caps.sourceOf?.(n.id) ?? null;
|
|
199
|
+
if (source != null)
|
|
200
|
+
base.source = source;
|
|
201
|
+
return base;
|
|
202
|
+
}),
|
|
185
203
|
edges: outEdges,
|
|
186
204
|
truncated: nodesTruncated || edgesTruncated,
|
|
187
205
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { GraphEdge, GraphNode, GraphNodeType } from './types.js';
|
|
2
|
+
/** 关联跳转的缺省条数上限(邻居与回落记录各吃一份)。 */
|
|
3
|
+
export declare const RECORD_LINKS_LIMIT_DEFAULT = 60;
|
|
4
|
+
/** 条数硬钳制上界(与 list-records 的单页上限同量级,再大没有可读性)。 */
|
|
5
|
+
export declare const RECORD_LINKS_LIMIT_MAX = 200;
|
|
6
|
+
/** 跳数上界:2 跳足够看清"经由谁连到谁";3 跳在百级图上是全图,在千级图上是噪声。 */
|
|
7
|
+
export declare const RECORD_LINKS_HOPS_MAX = 2;
|
|
8
|
+
/**
|
|
9
|
+
* 每个实体/邻居随带的来源记录预览条数(保最新)。
|
|
10
|
+
*
|
|
11
|
+
* 没有它,第三层「点邻居 → 跳到那条记录」就断了:面板拿到邻居 id 却不知道它
|
|
12
|
+
* 的来源是谁,只能再发一次 graph-node-get。截 5 条是为了不把整条来源链(上限 128)
|
|
13
|
+
* 塞进一个列表项。
|
|
14
|
+
*/
|
|
15
|
+
export declare const RECORD_LINKS_SOURCE_PREVIEW = 5;
|
|
16
|
+
export interface RecordLinkEntity {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: GraphNodeType;
|
|
20
|
+
/** active 边双向计数(与 preview.degreeMaps 同口径)。 */
|
|
21
|
+
degree: number;
|
|
22
|
+
causalDegree: number;
|
|
23
|
+
/** 来源记录预览(保最新、≤RECORD_LINKS_SOURCE_PREVIEW;起点自身已排除)。 */
|
|
24
|
+
recordIds: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface RecordLinkNeighbor {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: GraphNodeType;
|
|
30
|
+
/** 该条边的关系词(平行多边各占一条,不合并——合并就丢关系)。 */
|
|
31
|
+
relation: string;
|
|
32
|
+
causal: boolean;
|
|
33
|
+
/** 经由哪个上游节点连出(多层展开时形成"起点 → via → 邻居"的可回溯链)。 */
|
|
34
|
+
viaEntityId: string;
|
|
35
|
+
degree: number;
|
|
36
|
+
/** 本邻居是第几跳(1 = 实体的直接邻居)。 */
|
|
37
|
+
hop: number;
|
|
38
|
+
/** 邻居的来源记录预览(保最新、≤RECORD_LINKS_SOURCE_PREVIEW)——第三层跳转的落点。 */
|
|
39
|
+
recordIds: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface RecordLinksResult {
|
|
42
|
+
/** 层2:起点记录作为来源的实体节点(degree 降序)。 */
|
|
43
|
+
entities: RecordLinkEntity[];
|
|
44
|
+
/** 层3:实体邻居(因果优先 → degree 降 → updatedAt 新 → id 升)。 */
|
|
45
|
+
neighbors: RecordLinkNeighbor[];
|
|
46
|
+
/** 回落记录的 id 集(已去重、已排除起点自身、按"来源节点排名"顺序、≤limit)。 */
|
|
47
|
+
neighborRecordIds: string[];
|
|
48
|
+
/** 邻居或回落记录任一维触顶。 */
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface RecordLinksOpts {
|
|
52
|
+
/** 1..RECORD_LINKS_HOPS_MAX,缺省 1。 */
|
|
53
|
+
hops?: number;
|
|
54
|
+
/** 1..RECORD_LINKS_LIMIT_MAX,缺省 RECORD_LINKS_LIMIT_DEFAULT。 */
|
|
55
|
+
limit?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 主函数。`recordId` 命中零个实体时返回全空结构(不抛)——这条记录还没被投影进图
|
|
59
|
+
* (图谱默认关闭 / 投影未跑到 / 该记录无可抽实体),都是正常态而非错误态。
|
|
60
|
+
*/
|
|
61
|
+
export declare function buildRecordLinks(nodes: readonly GraphNode[], edges: readonly GraphEdge[], recordId: string, opts?: RecordLinksOpts): RecordLinksResult;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆库入口的关联跳转(读路径纯函数):L1 记录 → 实体 → 关系 → 其它记录。
|
|
3
|
+
*
|
|
4
|
+
* 为什么必须绕图投影:`l1_records` 的 DDL 是逐字节冻结的磁盘契约,记录行里
|
|
5
|
+
* **没有任何 relation/edges 字段**,L1 记录也不是图的顶点。所以"这条记忆和哪些
|
|
6
|
+
* 记忆相关"只能经 `graph_nodes/graph_edges` 的 `sourceRecordIds` **反查**得出——
|
|
7
|
+
* 关系是投影出来的,不是存出来的。反查一律在内存里做(`sourceRecordIds.includes`),
|
|
8
|
+
* 不走 SQL `LIKE '%"id"%'`:图谱量级为可重建投影(百~千级),全表读本就发生在上层
|
|
9
|
+
* `loadGraph`,内存扫描零成本;而 LIKE 匹配 JSON 文本有前缀误配风险。
|
|
10
|
+
*
|
|
11
|
+
* 三条不变式:
|
|
12
|
+
* 1) 只读:本文件不查库、不落库、不建边、不改投影——写面一律走既有 reproject 缝;
|
|
13
|
+
* 2) 判据与预览/检索同源:候选 = active|disputed 节点 × active 边(见 preview.visible);
|
|
14
|
+
* 3) 确定性 = 同输入两次调用逐位一致,同分裁决链落到 id 升序兜底。
|
|
15
|
+
*/
|
|
16
|
+
import { GRAPH_CAUSAL_RELATIONS, isGraphNodeVisible } from './constraints.js';
|
|
17
|
+
import { degreeMaps } from './preview.js';
|
|
18
|
+
const CAUSAL_SET = new Set(GRAPH_CAUSAL_RELATIONS);
|
|
19
|
+
/** 关联跳转的缺省条数上限(邻居与回落记录各吃一份)。 */
|
|
20
|
+
export const RECORD_LINKS_LIMIT_DEFAULT = 60;
|
|
21
|
+
/** 条数硬钳制上界(与 list-records 的单页上限同量级,再大没有可读性)。 */
|
|
22
|
+
export const RECORD_LINKS_LIMIT_MAX = 200;
|
|
23
|
+
/** 跳数上界:2 跳足够看清"经由谁连到谁";3 跳在百级图上是全图,在千级图上是噪声。 */
|
|
24
|
+
export const RECORD_LINKS_HOPS_MAX = 2;
|
|
25
|
+
/**
|
|
26
|
+
* 每个实体/邻居随带的来源记录预览条数(保最新)。
|
|
27
|
+
*
|
|
28
|
+
* 没有它,第三层「点邻居 → 跳到那条记录」就断了:面板拿到邻居 id 却不知道它
|
|
29
|
+
* 的来源是谁,只能再发一次 graph-node-get。截 5 条是为了不把整条来源链(上限 128)
|
|
30
|
+
* 塞进一个列表项。
|
|
31
|
+
*/
|
|
32
|
+
export const RECORD_LINKS_SOURCE_PREVIEW = 5;
|
|
33
|
+
/** 与预览同源的可见性判据(单源在 constraints)。 */
|
|
34
|
+
const visible = isGraphNodeVisible;
|
|
35
|
+
/** updatedAt 新(大)先;ISO 字符串可直接字典序比较。 */
|
|
36
|
+
function byRecency(a, b) {
|
|
37
|
+
if (a.updatedAt !== b.updatedAt)
|
|
38
|
+
return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
39
|
+
return a.id < b.id ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
/** 来源预览:保最新尾部截断,并排除起点自身(它已在层1 展示)。 */
|
|
42
|
+
function previewSources(node, excludeId) {
|
|
43
|
+
const keep = node.sourceRecordIds.filter((r) => r !== excludeId);
|
|
44
|
+
return keep.length > RECORD_LINKS_SOURCE_PREVIEW ? keep.slice(-RECORD_LINKS_SOURCE_PREVIEW) : keep;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 主函数。`recordId` 命中零个实体时返回全空结构(不抛)——这条记录还没被投影进图
|
|
48
|
+
* (图谱默认关闭 / 投影未跑到 / 该记录无可抽实体),都是正常态而非错误态。
|
|
49
|
+
*/
|
|
50
|
+
export function buildRecordLinks(nodes, edges, recordId, opts = {}) {
|
|
51
|
+
const empty = { entities: [], neighbors: [], neighborRecordIds: [], truncated: false };
|
|
52
|
+
const id = String(recordId ?? '').trim();
|
|
53
|
+
if (!id)
|
|
54
|
+
return empty;
|
|
55
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
56
|
+
const dm = degreeMaps(nodes, edges);
|
|
57
|
+
const limit = Math.min(Math.max(Math.floor(opts.limit ?? RECORD_LINKS_LIMIT_DEFAULT), 1), RECORD_LINKS_LIMIT_MAX);
|
|
58
|
+
const hops = Math.min(Math.max(Math.floor(opts.hops ?? 1), 1), RECORD_LINKS_HOPS_MAX);
|
|
59
|
+
const visEdges = edges.filter((e) => e.status === 'active');
|
|
60
|
+
// ── 层2:反查该记录作为来源的实体 ────────────────────────────
|
|
61
|
+
const entityNodes = nodes
|
|
62
|
+
.filter((n) => visible(n) && n.sourceRecordIds.includes(id))
|
|
63
|
+
.sort((a, b) => {
|
|
64
|
+
const da = dm.degree.get(a.id) ?? 0;
|
|
65
|
+
const db = dm.degree.get(b.id) ?? 0;
|
|
66
|
+
if (da !== db)
|
|
67
|
+
return db - da;
|
|
68
|
+
return byRecency(a, b);
|
|
69
|
+
});
|
|
70
|
+
if (entityNodes.length === 0)
|
|
71
|
+
return empty;
|
|
72
|
+
const entities = entityNodes.map((n) => ({
|
|
73
|
+
id: n.id,
|
|
74
|
+
name: n.name,
|
|
75
|
+
type: n.type,
|
|
76
|
+
degree: dm.degree.get(n.id) ?? 0,
|
|
77
|
+
causalDegree: dm.causal.get(n.id) ?? 0,
|
|
78
|
+
recordIds: previewSources(n, id),
|
|
79
|
+
}));
|
|
80
|
+
// ── 层3:沿 active 边逐跳展开(visited 防环;起点实体自身不重复入列) ──
|
|
81
|
+
let frontier = entityNodes;
|
|
82
|
+
/** 起点实体集:它们已在层2 展示过,不再当作「关联项」绕回来重复列出。 */
|
|
83
|
+
const entitySet = new Set(entityNodes.map((n) => n.id));
|
|
84
|
+
const seen = new Set([...entitySet]);
|
|
85
|
+
const rows = [];
|
|
86
|
+
/** 排名序的节点集(回落记录按它取,保证"越相关的邻居的来源越先出现")。 */
|
|
87
|
+
const rankedNodes = [...entityNodes];
|
|
88
|
+
for (let hop = 1; hop <= hops; hop += 1) {
|
|
89
|
+
const next = [];
|
|
90
|
+
for (const center of frontier) {
|
|
91
|
+
for (const e of visEdges) {
|
|
92
|
+
let otherId;
|
|
93
|
+
if (e.fromNodeId === center.id)
|
|
94
|
+
otherId = e.toNodeId;
|
|
95
|
+
else if (e.toNodeId === center.id)
|
|
96
|
+
otherId = e.fromNodeId;
|
|
97
|
+
else
|
|
98
|
+
continue;
|
|
99
|
+
const o = byId.get(otherId);
|
|
100
|
+
if (!o || !visible(o))
|
|
101
|
+
continue;
|
|
102
|
+
if (entitySet.has(o.id))
|
|
103
|
+
continue; // 环状图不把起点实体列成自己的邻居
|
|
104
|
+
rows.push({
|
|
105
|
+
id: o.id,
|
|
106
|
+
name: o.name,
|
|
107
|
+
type: o.type,
|
|
108
|
+
relation: e.relation,
|
|
109
|
+
causal: CAUSAL_SET.has(e.relation),
|
|
110
|
+
viaEntityId: center.id,
|
|
111
|
+
degree: dm.degree.get(o.id) ?? 0,
|
|
112
|
+
hop,
|
|
113
|
+
recordIds: previewSources(o, id),
|
|
114
|
+
_upd: o.updatedAt,
|
|
115
|
+
});
|
|
116
|
+
if (!seen.has(o.id)) {
|
|
117
|
+
seen.add(o.id);
|
|
118
|
+
next.push(o);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
rankedNodes.push(...next);
|
|
123
|
+
frontier = next;
|
|
124
|
+
if (next.length === 0)
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
rows.sort((a, b) => {
|
|
128
|
+
if (a.causal !== b.causal)
|
|
129
|
+
return a.causal ? -1 : 1;
|
|
130
|
+
if (a.hop !== b.hop)
|
|
131
|
+
return a.hop - b.hop;
|
|
132
|
+
if (a.degree !== b.degree)
|
|
133
|
+
return b.degree - a.degree;
|
|
134
|
+
if (a._upd !== b._upd)
|
|
135
|
+
return a._upd < b._upd ? -1 : 1;
|
|
136
|
+
if (a.id !== b.id)
|
|
137
|
+
return a.id < b.id ? -1 : 1;
|
|
138
|
+
return a.viaEntityId < b.viaEntityId ? -1 : 1;
|
|
139
|
+
});
|
|
140
|
+
const neighborsTruncated = rows.length > limit;
|
|
141
|
+
const kept = rows.slice(0, limit);
|
|
142
|
+
const neighbors = kept.map(({ _upd, ...row }) => row);
|
|
143
|
+
// ── 回落到记录:入选邻居 + 起点实体的来源记录,去重并排除起点自身 ──
|
|
144
|
+
const pickedIds = [];
|
|
145
|
+
const taken = new Set([id]);
|
|
146
|
+
let recordsTruncated = false;
|
|
147
|
+
for (const node of rankedNodes) {
|
|
148
|
+
for (const rid of node.sourceRecordIds) {
|
|
149
|
+
if (taken.has(rid))
|
|
150
|
+
continue;
|
|
151
|
+
if (pickedIds.length >= limit) {
|
|
152
|
+
recordsTruncated = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
taken.add(rid);
|
|
156
|
+
pickedIds.push(rid);
|
|
157
|
+
}
|
|
158
|
+
if (recordsTruncated)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
entities,
|
|
163
|
+
neighbors,
|
|
164
|
+
neighborRecordIds: pickedIds,
|
|
165
|
+
truncated: neighborsTruncated || recordsTruncated,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type FullViewEdge, type FullViewNode } from './preview.js';
|
|
2
|
+
import type { GraphEdge, GraphNode } from './types.js';
|
|
3
|
+
/** 骨架层占比(度量为 nodeCap)。 */
|
|
4
|
+
export declare const EXPLORER_QUOTA_SKELETON = 0.6;
|
|
5
|
+
/** 新鲜层占比(度量为 nodeCap)。 */
|
|
6
|
+
export declare const EXPLORER_QUOTA_FRESH = 0.2;
|
|
7
|
+
export interface ExplorerSelectCaps {
|
|
8
|
+
nodeCap?: number;
|
|
9
|
+
edgeCap?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ExplorerSelection {
|
|
12
|
+
nodes: FullViewNode[];
|
|
13
|
+
edges: FullViewEdge[];
|
|
14
|
+
/** 任一维触顶(渲染侧必须据此提示"仅画部分",不得静默少画)。 */
|
|
15
|
+
truncated: boolean;
|
|
16
|
+
/** 各层实际入选数(合计 === nodes.length;诊断与单测断言用)。 */
|
|
17
|
+
counts: {
|
|
18
|
+
skeleton: number;
|
|
19
|
+
fresh: number;
|
|
20
|
+
tail: number;
|
|
21
|
+
};
|
|
22
|
+
/** 截断前的可见节点总数(页面显示 "M/N 节点")。 */
|
|
23
|
+
totalVisible: number;
|
|
24
|
+
totalEdges: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 主函数。纯计算:不查库、不落库。`nodes`/`edges` 由调用方一次性装载
|
|
28
|
+
* (GraphStore.loadGraph),与 buildFullView 共享同一份入参。
|
|
29
|
+
*/
|
|
30
|
+
export declare function selectExplorerNodes(nodes: readonly GraphNode[], edges: readonly GraphEdge[], caps?: ExplorerSelectCaps): ExplorerSelection;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆库视图的元素选择(读路径纯函数):超上限时按**三层配额**选,而不是单一排序。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不按 degree 从高到低截断:图谱里的老 hub(如"用户本人"、长期项目)度数恒高,
|
|
5
|
+
* 单一排序会把全部名额吃在它们和它们的一度邻居上,**昨天刚沉淀的新记忆永远进不了图**
|
|
6
|
+
* ——而"我刚记的东西在哪"恰恰是查询入口最先被问的问题。故拆三层:
|
|
7
|
+
* 骨架层(60%) degree + causal×权重 —— 保证结构主干与因果链在画面里;
|
|
8
|
+
* 新鲜层(20%) updatedAt 倒序,含 degree=0 —— 保证新记忆一定看得见;
|
|
9
|
+
* 长尾层(余量) 剩余按 degree 补 —— 防止只剩几个毛球,保留图的连通观感。
|
|
10
|
+
*
|
|
11
|
+
* 未超限时**全选**,且输出顺序与入参一致(即 `loadGraph` 行序)——与既有
|
|
12
|
+
* `buildFullView` 的截断语义逐位对齐,老消费方不受影响。
|
|
13
|
+
*/
|
|
14
|
+
import { GRAPH_PREVIEW_CAUSAL_WEIGHT, GRAPH_PREVIEW_FULL_EDGES, GRAPH_PREVIEW_FULL_NODES } from './types.js';
|
|
15
|
+
import { GRAPH_CAUSAL_RELATIONS, isGraphNodeVisible } from './constraints.js';
|
|
16
|
+
import { degreeMaps } from './preview.js';
|
|
17
|
+
const CAUSAL_SET = new Set(GRAPH_CAUSAL_RELATIONS);
|
|
18
|
+
/** 骨架层占比(度量为 nodeCap)。 */
|
|
19
|
+
export const EXPLORER_QUOTA_SKELETON = 0.6;
|
|
20
|
+
/** 新鲜层占比(度量为 nodeCap)。 */
|
|
21
|
+
export const EXPLORER_QUOTA_FRESH = 0.2;
|
|
22
|
+
/** 与预览/关联同源的可见性判据(单源在 constraints)。 */
|
|
23
|
+
const visible = isGraphNodeVisible;
|
|
24
|
+
/**
|
|
25
|
+
* 主函数。纯计算:不查库、不落库。`nodes`/`edges` 由调用方一次性装载
|
|
26
|
+
* (GraphStore.loadGraph),与 buildFullView 共享同一份入参。
|
|
27
|
+
*/
|
|
28
|
+
export function selectExplorerNodes(nodes, edges, caps = {}) {
|
|
29
|
+
const nodeCap = Math.max(1, Math.floor(caps.nodeCap ?? GRAPH_PREVIEW_FULL_NODES));
|
|
30
|
+
const edgeCap = Math.max(1, Math.floor(caps.edgeCap ?? GRAPH_PREVIEW_FULL_EDGES));
|
|
31
|
+
const dm = degreeMaps(nodes, edges);
|
|
32
|
+
const visNodes = nodes.filter(visible);
|
|
33
|
+
const scoreOf = (n) => (dm.degree.get(n.id) ?? 0) + (dm.causal.get(n.id) ?? 0) * GRAPH_PREVIEW_CAUSAL_WEIGHT;
|
|
34
|
+
// ── 节点选择 ──────────────────────────────────────────────
|
|
35
|
+
const chosen = new Set();
|
|
36
|
+
let skeleton = 0;
|
|
37
|
+
let fresh = 0;
|
|
38
|
+
let tail = 0;
|
|
39
|
+
let nodesTruncated = false;
|
|
40
|
+
if (visNodes.length <= nodeCap) {
|
|
41
|
+
for (const n of visNodes)
|
|
42
|
+
chosen.add(n.id);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
nodesTruncated = true;
|
|
46
|
+
const byScore = (a, b) => {
|
|
47
|
+
const s = scoreOf(b) - scoreOf(a);
|
|
48
|
+
if (s !== 0)
|
|
49
|
+
return s;
|
|
50
|
+
return a.id < b.id ? -1 : 1;
|
|
51
|
+
};
|
|
52
|
+
const byFresh = (a, b) => {
|
|
53
|
+
if (a.updatedAt !== b.updatedAt)
|
|
54
|
+
return a.updatedAt < b.updatedAt ? 1 : -1;
|
|
55
|
+
return a.id < b.id ? -1 : 1;
|
|
56
|
+
};
|
|
57
|
+
// 每层至少留 1 个名额:cap 很小时 floor(cap×0.2)=0 会让新鲜层直接消失,
|
|
58
|
+
// 于是“刚记的东西看不见”这个要防的问题反而在最需要它的窄预算下发生。
|
|
59
|
+
const skeletonQuota = Math.max(1, Math.floor(nodeCap * EXPLORER_QUOTA_SKELETON));
|
|
60
|
+
const freshQuota = Math.max(1, Math.floor(nodeCap * EXPLORER_QUOTA_FRESH));
|
|
61
|
+
// 1) 骨架层:score 降序(孤立点 score=0 自然沉底,不会白占主干名额)
|
|
62
|
+
for (const n of [...visNodes].sort(byScore)) {
|
|
63
|
+
if (skeleton >= skeletonQuota)
|
|
64
|
+
break;
|
|
65
|
+
if (chosen.has(n.id))
|
|
66
|
+
continue;
|
|
67
|
+
chosen.add(n.id);
|
|
68
|
+
skeleton += 1;
|
|
69
|
+
}
|
|
70
|
+
// 2) 新鲜层:updatedAt 倒序,含 degree=0 的新点
|
|
71
|
+
for (const n of [...visNodes].sort(byFresh)) {
|
|
72
|
+
if (fresh >= freshQuota)
|
|
73
|
+
break;
|
|
74
|
+
if (chosen.has(n.id))
|
|
75
|
+
continue;
|
|
76
|
+
chosen.add(n.id);
|
|
77
|
+
fresh += 1;
|
|
78
|
+
}
|
|
79
|
+
// 3) 长尾层:剩余名额按 score 补满
|
|
80
|
+
for (const n of [...visNodes].sort(byScore)) {
|
|
81
|
+
if (chosen.size >= nodeCap)
|
|
82
|
+
break;
|
|
83
|
+
if (chosen.has(n.id))
|
|
84
|
+
continue;
|
|
85
|
+
chosen.add(n.id);
|
|
86
|
+
tail += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 输出保持入参顺序(loadGraph 行序),与 buildFullView 的截断语义同形
|
|
90
|
+
const outNodes = visNodes
|
|
91
|
+
.filter((n) => chosen.has(n.id))
|
|
92
|
+
.map((n) => ({ id: n.id, name: n.name, type: n.type }));
|
|
93
|
+
// ── 边选择:两端入选 → 因果优先 → 两端 score 和降序 → id 升序兜底 ──
|
|
94
|
+
const candidates = edges.filter((e) => e.status === 'active' && chosen.has(e.fromNodeId) && chosen.has(e.toNodeId));
|
|
95
|
+
const totalEdges = candidates.length;
|
|
96
|
+
const ranked = [...candidates].sort((a, b) => {
|
|
97
|
+
const ca = CAUSAL_SET.has(a.relation) ? 1 : 0;
|
|
98
|
+
const cb = CAUSAL_SET.has(b.relation) ? 1 : 0;
|
|
99
|
+
if (ca !== cb)
|
|
100
|
+
return cb - ca;
|
|
101
|
+
const sa = (dm.degree.get(a.fromNodeId) ?? 0) + (dm.degree.get(a.toNodeId) ?? 0)
|
|
102
|
+
+ ((dm.causal.get(a.fromNodeId) ?? 0) + (dm.causal.get(a.toNodeId) ?? 0)) * GRAPH_PREVIEW_CAUSAL_WEIGHT;
|
|
103
|
+
const sb = (dm.degree.get(b.fromNodeId) ?? 0) + (dm.degree.get(b.toNodeId) ?? 0)
|
|
104
|
+
+ ((dm.causal.get(b.fromNodeId) ?? 0) + (dm.causal.get(b.toNodeId) ?? 0)) * GRAPH_PREVIEW_CAUSAL_WEIGHT;
|
|
105
|
+
if (sa !== sb)
|
|
106
|
+
return sb - sa;
|
|
107
|
+
return a.id < b.id ? -1 : 1;
|
|
108
|
+
});
|
|
109
|
+
const edgesTruncated = ranked.length > edgeCap;
|
|
110
|
+
const outEdges = ranked.slice(0, edgeCap).map((e) => ({
|
|
111
|
+
from: e.fromNodeId,
|
|
112
|
+
to: e.toNodeId,
|
|
113
|
+
relation: e.relation,
|
|
114
|
+
causal: CAUSAL_SET.has(e.relation),
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
nodes: outNodes,
|
|
118
|
+
edges: outEdges,
|
|
119
|
+
truncated: nodesTruncated || edgesTruncated,
|
|
120
|
+
counts: { skeleton, fresh, tail },
|
|
121
|
+
totalVisible: visNodes.length,
|
|
122
|
+
totalEdges,
|
|
123
|
+
};
|
|
124
|
+
}
|
package/dist/settings.js
CHANGED
|
@@ -90,6 +90,8 @@ const ALWAYS_ON = {
|
|
|
90
90
|
memportEnabled: false,
|
|
91
91
|
// 时效分级门2 灰度开关:默认关(注入与改前逐字一致)
|
|
92
92
|
validityTiering: false,
|
|
93
|
+
// 记忆库侧栏入口:默认开(用户装了插件就能看到)
|
|
94
|
+
explorerEnabled: true,
|
|
93
95
|
// md 投射护栏运行时覆盖:0 = 跟随部署 cfg.memExport.maxRecords(面板上可改)
|
|
94
96
|
mdExportMaxRecords: 0,
|
|
95
97
|
// 回填并行度运行时覆盖:0 = 跟随部署 cfg.backfill.concurrency(概览页滑块可改)
|
|
@@ -339,6 +341,7 @@ function resolveSettings(value) {
|
|
|
339
341
|
mdSyncEnabled: v.mdSyncEnabled === true,
|
|
340
342
|
memportEnabled: v.memportEnabled === true,
|
|
341
343
|
validityTiering: v.validityTiering === true,
|
|
344
|
+
explorerEnabled: v.explorerEnabled !== false, // 默认 true,只有显式 false 才关
|
|
342
345
|
// md 投射护栏钳 0~10000(与 live schema 同上限);0 = 跟随部署默认
|
|
343
346
|
mdExportMaxRecords: Math.min(10_000, Math.max(0, num(v.mdExportMaxRecords))),
|
|
344
347
|
// 回填并行度钳 0~8(与 live schema 同上限);0 = 跟随部署 cfg.backfill.concurrency
|