@modusensus/dsh-mneme 0.8.9 → 0.8.11
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/README.md +4 -3
- package/assets/icon.svg +3 -0
- package/lib/api-standalone.js +11 -1
- package/lib/api.js +6 -1
- package/lib/client.js +208 -59
- package/lib/config.js +26 -0
- package/lib/index.js +10 -4
- package/lib/inject.js +68 -9
- package/lib/service.js +28 -3
- package/lib/settings.js +9 -1
- package/lib/tools.js +17 -8
- package/lib/write-admission.js +165 -22
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +6 -2
- package/scripts/benchmark-recall.js +57 -7
- package/src/api-standalone.js +11 -1
- package/src/api.js +6 -1
- package/src/config.js +26 -0
- package/src/index.js +10 -4
- package/src/inject.js +68 -9
- package/src/service.js +28 -3
- package/src/settings.js +9 -1
- package/src/tools.js +17 -8
- package/src/write-admission.js +165 -22
- package/test/api.test.js +17 -2
- package/test/client.test.js +245 -45
- package/test/epistemic.test.js +27 -0
- package/test/helpers/write-admission-samples.js +308 -0
- package/test/inject.test.js +137 -0
- package/test/recall-anti-update.test.js +82 -0
- package/test/write-admission.test.js +208 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/slow-stack/mneme)
|
|
9
9
|
[](https://github.com/slow-stack/mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -432,6 +432,7 @@ dsh web
|
|
|
432
432
|
| `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(`autoDream` / `autoSummarize` / `sleep` / `entityExtract`,对应 `operation_type` 为 `dream_consolidate` / `dream_summarize` / `dream_narrative`、`summarize_compress`、`sleep_conflict` / `sleep_pattern`、`entity_extract`)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
|
|
433
433
|
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL);去重键扩展含作用域三元——跨作用域同标题不再物理合并;检索按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见)。也走 feature_flags 白名单(面板可启停) |
|
|
434
434
|
| `strictScope` | `false` | 作用域硬过滤(v0.8.0 引入,v0.8.1 起只认显式声明;依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,**显式声明**收窄到他者作用域的记忆完全不可见;载体自动标注只降权保留可见——真正的物理隔离请用 sensitivity。会话身份解析不到时 fail-closed 只挡显式行。关闭时全部为软隔离(降权保留可见)。也走 feature_flags 白名单 |
|
|
435
|
+
| `writeAdmission` | `{enabled:false, enforce:false}` | 写入准入(issue #254,默认关):把「这条该不该进库」前移到 LLM 之前。第 1 级是零 LLM 的确定性判据——空白 / 纯噪声 + 密钥 / PII(判据来源是 #164 A2,经 `sensitiveScan` 注入,本批只定义接口)。`enabled` 跑判据并落审计行(`llm_audit_logs` 的 `metadata.deny`),`enforce` 才真的拒(`store.save` 之前返回,`memory_save` 返回 `action:"denied"` + `reason`);只开 `enabled` = 仅告警、写入不拦。去重键命中不进第 1 级(归 write-update 放行),G1/G2 阈值只计量。也走 feature_flags 白名单 |
|
|
435
436
|
| `conflictFreezeEnabled` | `false` | 冲突冻结(v0.4.4):dream 发现矛盾对不自动裁决,冻结进冲突队列;状态页「冲突队列」支持并排对比与人工确认(保留 A / 保留 B / 仅标记已处理,v0.8.0) |
|
|
436
437
|
| `conflictFreezeMaxPending` | `100` | 冲突冻结队列最大挂起数(1-1000):超出后不再入队(防队列无限膨胀) |
|
|
437
438
|
| `escapePromptVariables` | `true` | 注入文本花括号转义(v0.8.0 恢复,issue #162):注入边界把 `{{...}}` 转义,防止 hot memory / 记忆原文里的 Go template / Vue 语法触发宿主 interpolate 抛错卡死会话(v0.7.4 曾修复、v0.7.11 误删) |
|
|
@@ -583,7 +584,7 @@ src/
|
|
|
583
584
|
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
|
|
584
585
|
└── index.js # 插件接线
|
|
585
586
|
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
|
|
586
|
-
test/ #
|
|
587
|
+
test/ # 1431 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
587
588
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
|
|
588
589
|
```
|
|
589
590
|
|
|
@@ -592,7 +593,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
592
593
|
```bash
|
|
593
594
|
cd dsh-mneme
|
|
594
595
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
595
|
-
npm test # 运行
|
|
596
|
+
npm test # 运行 1431 个测试
|
|
596
597
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
597
598
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
598
599
|
```
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16" fill="none" stroke="#4176e6" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97"/>
|
|
3
|
+
</svg>
|
package/lib/api-standalone.js
CHANGED
|
@@ -386,7 +386,7 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
try {
|
|
389
|
-
const
|
|
389
|
+
const result = service.saveWithDedupe({
|
|
390
390
|
type: body.type,
|
|
391
391
|
title: body.title,
|
|
392
392
|
content: body.content,
|
|
@@ -398,6 +398,16 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
398
398
|
...(body.agent_scope !== undefined ? { agent_scope: body.agent_scope, agent_scope_source: "explicit" } : {}),
|
|
399
399
|
...(body.workspace_scope !== undefined ? { workspace_scope: body.workspace_scope, workspace_scope_source: "explicit" } : {})
|
|
400
400
|
});
|
|
401
|
+
const { action } = result;
|
|
402
|
+
// #254 写入准入:这条 HTTP 路径今天不带会话身份,evaluate 在无 sessionKey
|
|
403
|
+
// 时直接早退,所以拦不到;仍然显式分支——将来真接上会话身份时,落到
|
|
404
|
+
// `toApiList([null])` 上会变成 500,把「被拒」伪装成「服务器出错」,而
|
|
405
|
+
// 这两件事对调用方的处置完全不同(改内容 vs 重试)。
|
|
406
|
+
if (action === "denied") {
|
|
407
|
+
sendJson(res, 422, { error: "write-rejected", reason: result.reason });
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const { memory } = result;
|
|
401
411
|
// action 随行透出(created/merged):memory_save 工具语义对齐所需,
|
|
402
412
|
// 附加键对既有消费方(CLI add 等)向后兼容。
|
|
403
413
|
sendJson(res, action === "created" ? 201 : 200, { ...service.toApiList([memory])[0], action });
|
package/lib/api.js
CHANGED
|
@@ -135,9 +135,14 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
135
135
|
];
|
|
136
136
|
// 嵌套键在 kv 里按点号平铺("memoryQualityFilter.enabled"),运行时 cfg 里
|
|
137
137
|
// 是嵌套对象,effective 从对象子字段取值;其余键照旧从 cfg 顶层取。
|
|
138
|
+
// 这张表必须与 settings.js 白名单里的点号键一一对应:#254 加键时漏了这里,
|
|
139
|
+
// effective 会静默少两个键(configFlagValue 取不到 undefined 就不写进去),
|
|
140
|
+
// 面板上看不出来、PUT 也存不进去——test/api.test.js 的计数锁就是钉这件事的。
|
|
138
141
|
const NESTED_FLAG_PATHS = {
|
|
139
142
|
"memoryQualityFilter.enabled": ["memoryQualityFilter", "enabled"],
|
|
140
|
-
"llmAudit.enabled": ["llmAudit", "enabled"]
|
|
143
|
+
"llmAudit.enabled": ["llmAudit", "enabled"],
|
|
144
|
+
"writeAdmission.enabled": ["writeAdmission", "enabled"],
|
|
145
|
+
"writeAdmission.enforce": ["writeAdmission", "enforce"]
|
|
141
146
|
};
|
|
142
147
|
function configFlagValue(key) {
|
|
143
148
|
const path = NESTED_FLAG_PATHS[key];
|
package/lib/client.js
CHANGED
|
@@ -68,19 +68,37 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
if (ops.filter((o) => o.kind !== "same").length > 200) return null;
|
|
69
69
|
return ops;
|
|
70
70
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
71
|
+
// --- mneme mark:插件自己的标识图标 -------------------------------------
|
|
72
|
+
// 不再借宿主的 IconArchiveOutline*,两个原因:
|
|
73
|
+
// · 语义撞车——「归档盒」和记忆库里的归档动作是同一个符号,用户反馈看不出
|
|
74
|
+
// 这是记忆插件(issue 走维护者自开通道);
|
|
75
|
+
// · 宿主耦合——宿主图标名会换代(#287:Outline20 → OutlineRegular/Medium),
|
|
76
|
+
// 只认某一代就取到 undefined,h(undefined) 即 React #130 把整个 slot entry
|
|
77
|
+
// 崩掉。自绘后这一整类问题消失,也不再需要「探测多代名字 + 缺名降级」。
|
|
78
|
+
// 形状:单线螺旋(记忆痕迹 / 召回回路)。规格逐条对齐宿主图标,才不像外来户:
|
|
79
|
+
// 16 栅格、fill:none、stroke:currentColor、描边 1(宿主 Regular 档)、圆头圆角。
|
|
80
|
+
// 路径是 6 段三次贝塞尔拟合同一螺旋(拟合版与逐点采样版逐像素一致,已核验),
|
|
81
|
+
// 所以源码里只有 200 字符,不是一百多个采样点。
|
|
82
|
+
// 改形状只需换 MNEME_MARK_D,并同步 assets/icon.svg(插件列表那份,同一个 d)。
|
|
83
|
+
const MNEME_MARK_D = "M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97";
|
|
84
|
+
|
|
85
|
+
/** mneme mark 的内联 SVG;currentColor + 描边 1,随主题与字号走。 */
|
|
86
|
+
const MnemeMark = ({ size = 16, className }) => h("svg", {
|
|
87
|
+
width: size,
|
|
88
|
+
height: size,
|
|
89
|
+
className,
|
|
90
|
+
viewBox: "0 0 16 16",
|
|
91
|
+
fill: "none",
|
|
92
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
93
|
+
"aria-hidden": "true",
|
|
94
|
+
stroke: "currentColor",
|
|
95
|
+
strokeWidth: 1,
|
|
96
|
+
strokeLinecap: "round",
|
|
97
|
+
strokeLinejoin: "round"
|
|
98
|
+
}, h("path", { d: MNEME_MARK_D }));
|
|
99
|
+
|
|
100
|
+
/** 渲染 mneme mark:四个入口(浮层标题栏 / footer 回退 / 侧边栏 portal / better-sidebar tab)共用同一颗。 */
|
|
101
|
+
const renderMnemeMark = (props) => h(MnemeMark, props);
|
|
84
102
|
|
|
85
103
|
// Portal target for the hero fallback surface. The host whitelists
|
|
86
104
|
// react-dom for its own bundles (dsh-client-ui-trajectory requires it);
|
|
@@ -415,6 +433,8 @@ window.__ModuleLoader__.load({
|
|
|
415
433
|
"memory.features.autoSummarize.hint": "对话结束自动提炼记忆条目",
|
|
416
434
|
"memory.features.hotMemoryEnabled": "热记忆",
|
|
417
435
|
"memory.features.hotMemoryEnabled.hint": "最近几轮对话原文随注入携带,不写入长期记忆",
|
|
436
|
+
"memory.features.injectTimePrefix": "对话开始注入当前时间",
|
|
437
|
+
"memory.features.injectTimePrefix.hint": "每次新会话的首轮注入一次日期与星期(默认关)",
|
|
418
438
|
"memory.features.entityExtractionEnabled": "实体抽取",
|
|
419
439
|
"memory.features.entityExtractionEnabled.hint": "从记忆中提取人物/项目/概念,图谱随之生长",
|
|
420
440
|
"memory.features.entityExtractionProvider": "实体抽取 Provider",
|
|
@@ -466,6 +486,13 @@ window.__ModuleLoader__.load({
|
|
|
466
486
|
"memory.features.memoryQualityFilter.enabled.hint": "低价值记忆自动归档,注入按质量降权",
|
|
467
487
|
"memory.features.llmAudit.enabled": "后台调用审计",
|
|
468
488
|
"memory.features.llmAudit.enabled.hint": "记录巩固/总结的后台模型调用与 token 消耗",
|
|
489
|
+
// #254:写入准入两层。检测关时整条路径不碰;检测开、拦截关 = 仅告警 +
|
|
490
|
+
// 留审计;两个都开才真拦。文案要写清「只留审计」与「真拦下」的区别,
|
|
491
|
+
// 否则操作者会以为打开检测就已经在拦。
|
|
492
|
+
"memory.features.writeAdmission.enabled": "写入准入判定",
|
|
493
|
+
"memory.features.writeAdmission.enabled.hint": "写入前判空白/噪声,命中留审计(不拦)",
|
|
494
|
+
"memory.features.writeAdmission.enforce": "写入准入拦截",
|
|
495
|
+
"memory.features.writeAdmission.enforce.hint": "真拦下命中的写入;关闭时只留审计不拦",
|
|
469
496
|
"memory.features.bm25SearchEnabled": "BM25 关键词检索",
|
|
470
497
|
"memory.features.bm25SearchEnabled.hint": "传统关键词打分检索,与向量召回互补",
|
|
471
498
|
"memory.features.conflictFreezeEnabled": "冲突冻结",
|
|
@@ -839,6 +866,8 @@ window.__ModuleLoader__.load({
|
|
|
839
866
|
"memory.features.autoSummarize.hint": "Distill memory entries when a conversation ends",
|
|
840
867
|
"memory.features.hotMemoryEnabled": "Hot memory",
|
|
841
868
|
"memory.features.hotMemoryEnabled.hint": "Carry the last few turns verbatim; never written to long-term memory",
|
|
869
|
+
"memory.features.injectTimePrefix": "Inject current time at conversation start",
|
|
870
|
+
"memory.features.injectTimePrefix.hint": "Adds the date and weekday once at each new session's first turn (off by default)",
|
|
842
871
|
"memory.features.entityExtractionEnabled": "Entity extraction",
|
|
843
872
|
"memory.features.entityExtractionEnabled.hint": "Extract people / projects / concepts so the graph grows by itself",
|
|
844
873
|
"memory.features.entityExtractionProvider": "Entity extraction provider",
|
|
@@ -890,6 +919,12 @@ window.__ModuleLoader__.load({
|
|
|
890
919
|
"memory.features.memoryQualityFilter.enabled.hint": "Archive low-value memories automatically; injection downranks by quality",
|
|
891
920
|
"memory.features.llmAudit.enabled": "Background call audit",
|
|
892
921
|
"memory.features.llmAudit.enabled.hint": "Log background model calls and token usage from consolidation / summaries",
|
|
922
|
+
// #254 — two layers: enabled runs the check (audit only), enforce actually
|
|
923
|
+
// blocks. The hints must keep that difference explicit.
|
|
924
|
+
"memory.features.writeAdmission.enabled": "Write admission checks",
|
|
925
|
+
"memory.features.writeAdmission.enabled.hint": "Flag blank / noise writes before storing, with an audit row (never blocks)",
|
|
926
|
+
"memory.features.writeAdmission.enforce": "Write admission enforcement",
|
|
927
|
+
"memory.features.writeAdmission.enforce.hint": "Actually reject flagged writes; off keeps the audit row only",
|
|
893
928
|
"memory.features.bm25SearchEnabled": "BM25 keyword search",
|
|
894
929
|
"memory.features.bm25SearchEnabled.hint": "Classic keyword scoring, complementary to vector recall",
|
|
895
930
|
"memory.features.conflictFreezeEnabled": "Conflict freezing",
|
|
@@ -1311,11 +1346,26 @@ window.__ModuleLoader__.load({
|
|
|
1311
1346
|
".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
|
|
1312
1347
|
".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
|
|
1313
1348
|
".mneme-heat--cold .mneme-heatpct{display:none}",
|
|
1314
|
-
//
|
|
1349
|
+
// 侧边栏入口两种形态的样式(挂载与搬移逻辑见 SidebarTopEntry 的头注释)。
|
|
1350
|
+
// 展开态兜底:逐条镜像宿主 panelRow(36px 行高 / 透明底 / 2px 外边距 /
|
|
1351
|
+
// 7px 8px 内边距)——宿主面板行类名在或不在,观感都是「插件 / 自动化任务」
|
|
1352
|
+
// 那一档,不会再退回 v0.8.10 那种高亮大按钮。
|
|
1353
|
+
// 收起态:按钮带宿主 newSession 类名,标题栏固定几何(position:fixed、
|
|
1354
|
+
// 顶部居中、28px 圆钮、次级文字色、hover 填充)整套继承宿主规则,我们只用
|
|
1355
|
+
// 一条更具体的规则把 left 从宿主的 48px 推到 84px——即「新会话」右侧,
|
|
1356
|
+
// 不再像 v0.8.10 那样压在它身上(用户实测确认的遮挡 bug)。
|
|
1357
|
+
// 84 = 48(宿主给「新会话」的 left)+ 28(钮宽)+ 8(间距);120 = 84 + 28 + 8,
|
|
1358
|
+
// 让「应用 / 编辑」菜单顺延到我们右侧。宿主侧出处:SidebarRoot.module.css 的
|
|
1359
|
+
// `.collapsed .newSession{left:48px}` 与
|
|
1360
|
+
// `html[data-windows-titlebar]:has([data-sidebar-collapsed=true]){--dsh-windows-menu-start:84px}`。
|
|
1315
1361
|
".mneme-topentry{display:flex;flex-direction:column;position:relative}",
|
|
1316
|
-
".mneme-topentry-native{
|
|
1362
|
+
".mneme-topentry-native{box-sizing:border-box;min-height:36px;border:none;border-radius:var(--dsw-radius-md);background:0 0;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;align-items:center;gap:8px;margin:0 2px;padding:7px 8px;line-height:22px;display:flex}",
|
|
1317
1363
|
".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
1364
|
+
".mneme-topentry-native:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}",
|
|
1318
1365
|
".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
|
|
1366
|
+
"html[data-windows-titlebar] .mneme-topentry .mneme-topentry-rail{left:84px}",
|
|
1367
|
+
// 菜单让位只在「收起 + 入口确实挂在标题栏里」时生效;展开态完全不碰宿主变量。
|
|
1368
|
+
"html[data-windows-titlebar]:has([data-sidebar-collapsed=true]):has([data-plugin-entry=\"@modusensus/dsh-mneme\"]){--dsh-windows-menu-start:120px}",
|
|
1319
1369
|
// --- 功能开关:一行一开关,Claude 式安静排版 ---
|
|
1320
1370
|
".mneme-featgroup{flex:none;font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);margin:16px 0 2px}",
|
|
1321
1371
|
".mneme-featrow{display:flex;align-items:center;gap:14px;padding:11px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}",
|
|
@@ -2072,7 +2122,7 @@ window.__ModuleLoader__.load({
|
|
|
2072
2122
|
// test/inject-parent-gate.test.js 钉住。
|
|
2073
2123
|
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
|
|
2074
2124
|
const FEATURE_GROUPS = [
|
|
2075
|
-
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "memoryQualityFilter.enabled", "llmAudit.enabled"] },
|
|
2125
|
+
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce"] },
|
|
2076
2126
|
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled"] },
|
|
2077
2127
|
{ key: "group.dream", items: ["autoDream", "sleepModeEnabled"] },
|
|
2078
2128
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
@@ -3014,6 +3064,10 @@ window.__ModuleLoader__.load({
|
|
|
3014
3064
|
// MemoryExplorer 内部状态(选中行、过滤)由各自的跳转回调处理。
|
|
3015
3065
|
function openLibrary() {
|
|
3016
3066
|
setOverlayOpen(true);
|
|
3067
|
+
// 打开面板 = 用户正准备处理冻结冲突:顺手把入口红点重取一次,别让他
|
|
3068
|
+
// 对着一个已经过期的计数(裁决走别的路径、或后台 autoDream 刚写过库时,
|
|
3069
|
+
// 计数不该等 60s 兜底轮询才对齐)。
|
|
3070
|
+
conflictBadgeCount.refresh();
|
|
3017
3071
|
}
|
|
3018
3072
|
|
|
3019
3073
|
// --- Hero fallback overlay state (module-level pub/sub) ---
|
|
@@ -3081,7 +3135,7 @@ window.__ModuleLoader__.load({
|
|
|
3081
3135
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
3082
3136
|
h("div", { className: "mneme-overlaybar" },
|
|
3083
3137
|
h("span", { className: "mneme-overlaytitle" },
|
|
3084
|
-
|
|
3138
|
+
renderMnemeMark({ size: 15 }),
|
|
3085
3139
|
t("memory.view.label")
|
|
3086
3140
|
),
|
|
3087
3141
|
h("button", {
|
|
@@ -4907,32 +4961,80 @@ window.__ModuleLoader__.load({
|
|
|
4907
4961
|
);
|
|
4908
4962
|
}
|
|
4909
4963
|
|
|
4910
|
-
// --- Sidebar entry:
|
|
4964
|
+
// --- Sidebar entry: 侧边栏里的记忆入口 ---
|
|
4911
4965
|
// 注册仍在 sidebar.footer.action(list 插槽)——它既是 React 树的挂载
|
|
4912
|
-
// 锚点,也是 portal
|
|
4913
|
-
//
|
|
4914
|
-
//
|
|
4915
|
-
//
|
|
4916
|
-
//
|
|
4917
|
-
//
|
|
4918
|
-
//
|
|
4966
|
+
// 锚点,也是 portal 失效时的原位回退;常驻 sheet 也从这个插槽挂载。
|
|
4967
|
+
// 入口形状跟着宿主 v0.2 的侧边栏走,分两态:
|
|
4968
|
+
// · 展开态:宿主把「插件 / 自动化任务」这类全局面板渲染成 nav.panelList
|
|
4969
|
+
// 里的 panelRow(36px 行高、透明底、图标 + 标题)。我们把按钮 portal 进
|
|
4970
|
+
// 同一组并原样套用宿主 panelRow 的实时类名,于是它是该组的第三行——低调,
|
|
4971
|
+
// 而不是 v0.8.10 那样长得像第二个高亮「新会话」按钮。
|
|
4972
|
+
// · 收起态(Windows 标题栏):panelList / regionArea / footArea 被宿主
|
|
4973
|
+
// display:none,只有开关与「新会话」被改成固定在标题栏上。我们把按钮搬回
|
|
4974
|
+
// 标题栏、并排在「新会话」右侧(84px / 120px 的算法见上面 CSS 注释),
|
|
4975
|
+
// 三颗图标各就各位——v0.8.10 把入口留在宿主的 48px 上把它盖住了。
|
|
4976
|
+
// 承载类名一律从活元素读取(宿主/皮肤会异步改写哈希类名),不硬编码;
|
|
4977
|
+
// 面板列表不存在(没装任何面板插件)时退回「新会话」行之后。
|
|
4919
4978
|
// 宿主结构异常时约 2 秒后放弃 portal,footer 回退按钮保持可用。
|
|
4920
|
-
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts
|
|
4921
|
-
//
|
|
4922
|
-
//
|
|
4923
|
-
|
|
4924
|
-
|
|
4979
|
+
// #177:未处理冲突数 badge——数据走 dream-status 的 pendingConflicts(与状态卡
|
|
4980
|
+
// 同一端点)。挂侧边栏入口按钮右上角(portal 与回退两处都挂),让用户不进面板
|
|
4981
|
+
// 也知道有冻结要裁决。
|
|
4982
|
+
// 刷新时机(用户反馈「裁决完红点消失得很慢」后重做):
|
|
4983
|
+
// · 裁决成功时冲突队列广播 mneme:conflicts-changed → 立刻重取,不必等下一轮;
|
|
4984
|
+
// 此前这条广播只有状态卡订阅,红点漏订,才慢到要等 60s 兜底轮询;
|
|
4985
|
+
// · openLibrary 打开面板时重取(见上);
|
|
4986
|
+
// · 60s 兜底轮询——后台 autoDream 自己产生/消解冲突时没人广播,靠它浮现。
|
|
4987
|
+
// 计数是模块级单例:入口按钮的 portal 与 footer 回退是两个组件,原先各起一个
|
|
4988
|
+
// 60s 轮询(两份请求、两个互不同步的计数)。现在共享一个 store:一次取回广播给
|
|
4989
|
+
// 全部订阅者,in-flight 请求也去重,最后一个订阅者离开才停表。
|
|
4990
|
+
const conflictBadgeCount = (() => {
|
|
4991
|
+
const listeners = new Set();
|
|
4992
|
+
let value = 0;
|
|
4993
|
+
let timer = null;
|
|
4994
|
+
let inflight = null;
|
|
4995
|
+
const publish = (next) => {
|
|
4996
|
+
if (next === value) return;
|
|
4997
|
+
value = next;
|
|
4998
|
+
listeners.forEach((fn) => {
|
|
4999
|
+
try { fn(value); } catch { /* 单个订阅者出错不拖垮其余 */ }
|
|
5000
|
+
});
|
|
5001
|
+
};
|
|
5002
|
+
const refresh = () => {
|
|
5003
|
+
if (inflight) return inflight;
|
|
5004
|
+
inflight = apiFetch("/api/dsh-mneme/dream-status")
|
|
5005
|
+
.then((res) => (res.ok ? res.json() : { pendingConflicts: 0 }))
|
|
5006
|
+
.then((d) => { publish(Number((d && d.pendingConflicts) ?? 0) || 0); })
|
|
5007
|
+
.catch(() => { publish(0); })
|
|
5008
|
+
.finally(() => { inflight = null; });
|
|
5009
|
+
return inflight;
|
|
5010
|
+
};
|
|
5011
|
+
return {
|
|
5012
|
+
get: () => value,
|
|
5013
|
+
refresh,
|
|
5014
|
+
subscribe: (fn) => {
|
|
5015
|
+
listeners.add(fn);
|
|
5016
|
+
fn(value);
|
|
5017
|
+
if (listeners.size === 1) {
|
|
5018
|
+
refresh();
|
|
5019
|
+
timer = setInterval(refresh, 60_000);
|
|
5020
|
+
}
|
|
5021
|
+
return () => {
|
|
5022
|
+
listeners.delete(fn);
|
|
5023
|
+
if (listeners.size === 0 && timer !== null) { clearInterval(timer); timer = null; }
|
|
5024
|
+
};
|
|
5025
|
+
}
|
|
5026
|
+
};
|
|
5027
|
+
})();
|
|
5028
|
+
function useConflictBadgeCount() {
|
|
5029
|
+
const [pending, setPending] = useState(() => conflictBadgeCount.get());
|
|
4925
5030
|
useEffect(() => {
|
|
4926
|
-
|
|
4927
|
-
const
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5031
|
+
const unsubscribe = conflictBadgeCount.subscribe(setPending);
|
|
5032
|
+
const onChanged = () => { conflictBadgeCount.refresh(); };
|
|
5033
|
+
window.addEventListener("mneme:conflicts-changed", onChanged);
|
|
5034
|
+
return () => {
|
|
5035
|
+
unsubscribe();
|
|
5036
|
+
window.removeEventListener("mneme:conflicts-changed", onChanged);
|
|
4932
5037
|
};
|
|
4933
|
-
tick();
|
|
4934
|
-
const timer = setInterval(tick, 60_000);
|
|
4935
|
-
return () => { cancelled = true; clearInterval(timer); };
|
|
4936
5038
|
}, []);
|
|
4937
5039
|
return pending;
|
|
4938
5040
|
}
|
|
@@ -4942,7 +5044,7 @@ window.__ModuleLoader__.load({
|
|
|
4942
5044
|
pending > 99 ? "99+" : String(pending));
|
|
4943
5045
|
}
|
|
4944
5046
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
4945
|
-
const pending = useConflictBadgeCount(
|
|
5047
|
+
const pending = useConflictBadgeCount();
|
|
4946
5048
|
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
4947
5049
|
// 直接放里面会被裁剪——包一层定位容器,badge 挂在容器上。
|
|
4948
5050
|
return h("div", { className: "mneme-triggerwrap" },
|
|
@@ -4956,7 +5058,7 @@ window.__ModuleLoader__.load({
|
|
|
4956
5058
|
onClick: openLibrary,
|
|
4957
5059
|
"data-mneme-overlay-opener": "true"
|
|
4958
5060
|
},
|
|
4959
|
-
|
|
5061
|
+
renderMnemeMark({ size: wide ? 16 : 18 }),
|
|
4960
5062
|
wide && h("span", { className: "mneme-trigger-label" }, t("memory.panel.open"))
|
|
4961
5063
|
),
|
|
4962
5064
|
h(ConflictBadge, { pending })
|
|
@@ -4965,14 +5067,25 @@ window.__ModuleLoader__.load({
|
|
|
4965
5067
|
|
|
4966
5068
|
function SidebarTopEntry({ wide, t, fallback }) {
|
|
4967
5069
|
const [host, setHost] = useState(null);
|
|
4968
|
-
const [
|
|
4969
|
-
const pending = useConflictBadgeCount(
|
|
5070
|
+
const [cls, setCls] = useState({ row: "", glyph: "", title: "" });
|
|
5071
|
+
const pending = useConflictBadgeCount();
|
|
4970
5072
|
useEffect(() => {
|
|
4971
5073
|
if (!reactDom || typeof document === "undefined") return undefined;
|
|
4972
5074
|
let tries = 0, timer = null, created = null, mo = null;
|
|
5075
|
+
const findRegion = () => {
|
|
5076
|
+
const region = document.querySelector('[class*="regionArea"]');
|
|
5077
|
+
return region && region.parentElement ? region : null;
|
|
5078
|
+
};
|
|
4973
5079
|
const findNative = (region) =>
|
|
4974
5080
|
region.parentElement.querySelector('[class*="newSession"]')
|
|
4975
5081
|
|| region.previousElementSibling;
|
|
5082
|
+
const findPanelList = (region) =>
|
|
5083
|
+
region.parentElement.querySelector('[class*="panelList"]');
|
|
5084
|
+
// 宿主自己的面板行:排除我们自己 portal 进去的那颗按钮(它也带 panelRow)。
|
|
5085
|
+
const findPanelRow = (region) => {
|
|
5086
|
+
const list = findPanelList(region);
|
|
5087
|
+
return list ? list.querySelector('[class*="panelRow"]:not(.mneme-topentry-native)') : null;
|
|
5088
|
+
};
|
|
4976
5089
|
// 占位锚点:新会话行(现役外壳里 newSession 按钮嵌在 logoRow 内,
|
|
4977
5090
|
// 旧外壳是根的直接子按钮)。判定与生态插件的 sidebar-entry-core
|
|
4978
5091
|
// 一致;锚点不可靠时回退到 regionArea 之前的旧位置。
|
|
@@ -4991,32 +5104,61 @@ window.__ModuleLoader__.load({
|
|
|
4991
5104
|
// insertBefore(x, x) 在 Chromium 里不是 no-op,会触发 mutation 造成
|
|
4992
5105
|
// 观察器自激风暴、冻死整个 SPA。
|
|
4993
5106
|
const place = (region) => {
|
|
5107
|
+
if (!created) return;
|
|
4994
5108
|
const parent = region.parentElement;
|
|
4995
5109
|
if (!parent) return;
|
|
5110
|
+
// 展开态落进宿主的面板列表(紧跟「插件 / 自动化任务」);宿主没有面板
|
|
5111
|
+
// 列表时退回「新会话」行之后,由 .mneme-topentry-native 兜住低调行样式。
|
|
5112
|
+
const list = wide ? findPanelList(region) : null;
|
|
5113
|
+
if (list) {
|
|
5114
|
+
if (created.parentElement === list && created === list.lastElementChild) return;
|
|
5115
|
+
list.appendChild(created);
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
4996
5118
|
const anchor = findAnchor(region);
|
|
4997
5119
|
const target = anchor ? anchor.nextSibling : region;
|
|
4998
5120
|
if (target === created) return;
|
|
4999
5121
|
if (created.parentElement === parent && created.nextSibling === target) return;
|
|
5000
5122
|
parent.insertBefore(created, target);
|
|
5001
5123
|
};
|
|
5124
|
+
// 类名一律从活元素读。展开态借宿主面板行的盒模型,并剔掉激活态修饰类
|
|
5125
|
+
// (否则我们的行会跟着宿主当前选中的面板一起常亮);收起态借「新会话」
|
|
5126
|
+
// 按钮的类名拿整套标题栏固定几何。两处都取不到时留空,由我们的类兜底。
|
|
5127
|
+
const readCls = (region) => {
|
|
5128
|
+
if (!wide) return { row: findNative(region)?.className || "", glyph: "", title: "" };
|
|
5129
|
+
const row = findPanelRow(region);
|
|
5130
|
+
const btn = row || findNative(region);
|
|
5131
|
+
const glyph = row && row.querySelector('[class*="panelGlyph"]');
|
|
5132
|
+
const title = row && row.querySelector('[class*="panelTitle"]');
|
|
5133
|
+
return {
|
|
5134
|
+
row: (btn?.className || "")
|
|
5135
|
+
.split(/\s+/)
|
|
5136
|
+
.filter((name) => name && !name.includes("panelActive"))
|
|
5137
|
+
.join(" "),
|
|
5138
|
+
glyph: (glyph && glyph.className) || "",
|
|
5139
|
+
title: (title && title.className) || ""
|
|
5140
|
+
};
|
|
5141
|
+
};
|
|
5142
|
+
const sync = (region) => {
|
|
5143
|
+
const next = readCls(region);
|
|
5144
|
+
setCls((prev) => (prev.row === next.row && prev.glyph === next.glyph && prev.title === next.title ? prev : next));
|
|
5145
|
+
};
|
|
5002
5146
|
const attempt = () => {
|
|
5003
|
-
const region =
|
|
5004
|
-
if (region
|
|
5005
|
-
//
|
|
5006
|
-
//
|
|
5007
|
-
//
|
|
5008
|
-
// 否则 portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
|
|
5147
|
+
const region = findRegion();
|
|
5148
|
+
if (region) {
|
|
5149
|
+
// 宿主切换展开/收起时会重排 DOM(面板列表 → 标题栏固定位),所以每次
|
|
5150
|
+
// 变动都重新落位并重读类名;宿主/皮肤异步改写类名也靠这条同步,否则
|
|
5151
|
+
// portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
|
|
5009
5152
|
created = document.createElement("div");
|
|
5010
5153
|
created.dataset.pluginEntry = "@modusensus/dsh-mneme";
|
|
5011
5154
|
place(region);
|
|
5012
5155
|
setHost(created);
|
|
5013
|
-
|
|
5156
|
+
sync(region);
|
|
5014
5157
|
mo = new MutationObserver(() => {
|
|
5015
|
-
const cur =
|
|
5016
|
-
if (!cur
|
|
5158
|
+
const cur = findRegion();
|
|
5159
|
+
if (!cur) return;
|
|
5017
5160
|
place(cur);
|
|
5018
|
-
|
|
5019
|
-
setNativeCls((prev) => (prev === cls ? prev : cls));
|
|
5161
|
+
sync(cur);
|
|
5020
5162
|
});
|
|
5021
5163
|
mo.observe(region.parentElement, {
|
|
5022
5164
|
attributes: true,
|
|
@@ -5035,13 +5177,19 @@ window.__ModuleLoader__.load({
|
|
|
5035
5177
|
if (mo) mo.disconnect();
|
|
5036
5178
|
if (created && created.parentElement) created.parentElement.removeChild(created);
|
|
5037
5179
|
};
|
|
5038
|
-
}, []);
|
|
5180
|
+
}, [wide]);
|
|
5039
5181
|
if (!host) return fallback;
|
|
5182
|
+
// 图标与「插件 / 自动化任务」同档(16px);收起态沿用标题栏里原来的 18px。
|
|
5183
|
+
const icon = renderMnemeMark({ size: wide ? 16 : 18 });
|
|
5040
5184
|
return reactDom.createPortal(
|
|
5041
5185
|
h("div", { className: "mneme-topentry" },
|
|
5042
5186
|
h("button", {
|
|
5043
5187
|
type: "button",
|
|
5044
|
-
|
|
5188
|
+
// 展开态:宿主面板行类名 + 我们的同款兜底;收起态:宿主「新会话」类名
|
|
5189
|
+
// + 只覆盖 left 的 rail 规则(整套标题栏几何继承宿主)。
|
|
5190
|
+
className: wide
|
|
5191
|
+
? `${cls.row} mneme-topentry-native`.trim()
|
|
5192
|
+
: `${cls.row} mneme-topentry-rail`.trim(),
|
|
5045
5193
|
"aria-label": pending > 0
|
|
5046
5194
|
? `${t("memory.sidebar.aria")} · ${t("memory.status.conflicts")} ${pending}`
|
|
5047
5195
|
: t("memory.sidebar.aria"),
|
|
@@ -5049,8 +5197,9 @@ window.__ModuleLoader__.load({
|
|
|
5049
5197
|
onClick: openLibrary,
|
|
5050
5198
|
"data-mneme-overlay-opener": "true"
|
|
5051
5199
|
},
|
|
5052
|
-
|
|
5053
|
-
|
|
5200
|
+
// 图标套上宿主面板行的槽位类名,视觉与同组入口完全一致;取不到时裸渲染。
|
|
5201
|
+
cls.glyph ? h("span", { className: cls.glyph, "aria-hidden": "true" }, icon) : icon,
|
|
5202
|
+
wide && h("span", { className: `${cls.title} mneme-topentry-label`.trim() }, t("memory.panel.open")),
|
|
5054
5203
|
h(ConflictBadge, { pending })
|
|
5055
5204
|
)
|
|
5056
5205
|
), host);
|
|
@@ -5111,7 +5260,7 @@ window.__ModuleLoader__.load({
|
|
|
5111
5260
|
reg.registerTab({
|
|
5112
5261
|
id: TAB_ID,
|
|
5113
5262
|
title: () => t("memory.view.label"),
|
|
5114
|
-
icon: (size) =>
|
|
5263
|
+
icon: (size) => renderMnemeMark({ size }),
|
|
5115
5264
|
order: 60,
|
|
5116
5265
|
component: () => h(MemoryExplorer, { t })
|
|
5117
5266
|
});
|
package/lib/config.js
CHANGED
|
@@ -71,6 +71,9 @@ export const Config = z.object({
|
|
|
71
71
|
// 提示:上限/原长/全文 memory_get 指引(BUDGET_EXCEEDED 原则——绝不静默)。
|
|
72
72
|
// 默认 300 = 与既有行为一致。
|
|
73
73
|
injectContentMaxChars: z.natural().min(60).max(4000).default(300),
|
|
74
|
+
// Issue #34(v0.7.2 引入;v0.7.11 误删后随 #333 恢复):对话开始注入一次当前
|
|
75
|
+
// 时间。默认关 = 与既有行为一致;格式与键名不变,存量 feature_flags 恢复即生效。
|
|
76
|
+
injectTimePrefix: z.boolean().default(false),
|
|
74
77
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
75
78
|
// Issue #239(第 5 项):注入条数的查询自适应(默认关)。确定性强的话题收缩注入
|
|
76
79
|
// 条数(减半、下限 1),模糊话题(回指/时间线索,或极短查询)维持
|
|
@@ -588,6 +591,29 @@ export const Config = z.object({
|
|
|
588
591
|
retentionDays: z.natural().min(1).max(3650).default(90)
|
|
589
592
|
}).default({}),
|
|
590
593
|
|
|
594
|
+
// --- write admission (#254) ------------------------------------------------
|
|
595
|
+
// 写入准入:把「这条该不该进库」前移到 LLM 之前。与 memoryQualityFilter 的分工
|
|
596
|
+
// 是时点不是判据——那个是**写入后**打分归档(扣分可以错,只影响排序与归档),
|
|
597
|
+
// 这个是**写入前**判定(硬拒不能错)。
|
|
598
|
+
//
|
|
599
|
+
// 两个开关都默认关(验收第 1 条「默认关零行为变化」):
|
|
600
|
+
// enabled — 跑第 1 级确定性判据。关时 evaluate 的返回与只计量那一阶段逐字段
|
|
601
|
+
// 一致,写入路径完全不碰。
|
|
602
|
+
// enforce — 命中时真的拒绝。关时判据照跑、审计照留,决策回落 allow(仅告警)。
|
|
603
|
+
// 读审计时「metadata.deny 非空且 metadata.decision=allow」就是这一档的指纹。
|
|
604
|
+
// 分层是为了能把「先观察」和「开始拦」分成两次上线:enforce 打开前先在真实流量
|
|
605
|
+
// 里看误杀面,而不是靠回滚开关当保险。
|
|
606
|
+
//
|
|
607
|
+
// 第 1 级的判据面刻意收窄到两类:密钥/PII(#164 A2 是判据来源,经
|
|
608
|
+
// createWriteAdmission 的 sensitiveScan 注入;本批只定义它怎么被消费)与空白/
|
|
609
|
+
// 纯噪声文本。去重键命中**不**进第 1 级——按维护者 09-22 的判定归 write-update
|
|
610
|
+
// 放行(2606.24535 的失效模式:同步近重复门跑在异步矛盾检测之前,把本该被裁决
|
|
611
|
+
// 的矛盾直接拒掉);G1/G2 的阈值继续只计量,等遥测有分布再单独提。
|
|
612
|
+
writeAdmission: z.object({
|
|
613
|
+
enabled: z.boolean().default(false),
|
|
614
|
+
enforce: z.boolean().default(false)
|
|
615
|
+
}).default({}),
|
|
616
|
+
|
|
591
617
|
// --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
|
|
592
618
|
// Separate retrieval evaluation snapshots from the production recall audit.
|
|
593
619
|
// When false (default) evaluateRetrieval still computes precision/recall/mrr
|
package/lib/index.js
CHANGED
|
@@ -272,10 +272,16 @@ export const apply = (ctx, config) => {
|
|
|
272
272
|
documentIndex.remove();
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
275
|
+
// 写入准入实例(#254)。两个开关 writeAdmission.enabled / .enforce 都默认关;
|
|
276
|
+
// 关时它的返回与只计量那一阶段逐字段一致,写入路径完全不碰。既有的
|
|
277
|
+
// llmAudit.enabled 关掉时它同样既不判也不写(那个开关连审计行的启动期清理一起
|
|
278
|
+
// 关掉,在无保留期的表里按写入频次增长是不能接受的)。
|
|
279
|
+
//
|
|
280
|
+
// sensitiveScan 是密钥 / PII 那一类判据的注入点。按 #254 验收第 4 条它是 #164 A2
|
|
281
|
+
// 的判据来源(A2 记在维护者排期里),所以本批不实现它,只把接口形状定在这里——
|
|
282
|
+
// 接上时只改这一行:
|
|
283
|
+
// createWriteAdmission({ ..., sensitiveScan: createSensitiveScan({ config: cfg }) })
|
|
284
|
+
// 缺省 null 时第 1 级只跑空白 / 噪声两类判据,其余一切照旧。
|
|
279
285
|
const writeAdmission = createWriteAdmission({ store, config: cfg, logger: ctx.logger });
|
|
280
286
|
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, documentIndex, writeAdmission });
|
|
281
287
|
|