@modusensus/dsh-mneme 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -10
- package/bin/cli.mjs +215 -2
- package/docs/CONFIGURATION.md +25 -2
- package/docs/DAEMON.md +13 -2
- package/docs/DISTILL.md +150 -4
- package/docs/SHARE.md +552 -0
- package/docs/SLEEP.md +21 -7
- package/docs/STORAGE.md +2 -1
- package/lib/api-standalone.js +294 -8
- package/lib/api.js +217 -3
- package/lib/client.js +919 -39
- package/lib/config.js +33 -1
- package/lib/continuity.js +11 -3
- package/lib/document.js +15 -2
- package/lib/dream/decisions.js +176 -30
- package/lib/dream/sleep.js +530 -94
- package/lib/dream.js +174 -21
- package/lib/fence.js +24 -0
- package/lib/index.js +15 -6
- package/lib/inject.js +9 -4
- package/lib/lang.js +38 -16
- package/lib/memory-source.js +9 -0
- package/lib/organize.js +5 -1
- package/lib/pipeline-nudge-auto.js +3 -0
- package/lib/pipeline-nudge.js +41 -8
- package/lib/pipeline-runner.js +15 -0
- package/lib/pipeline.js +62 -1
- package/lib/pre-inject-gate.js +14 -0
- package/lib/profile-extract.js +4 -1
- package/lib/provenance-http.js +251 -0
- package/lib/serve.js +13 -0
- package/lib/service.js +97 -4
- package/lib/session-gate.js +91 -0
- package/lib/settings.js +13 -1
- package/lib/share-http.js +281 -0
- package/lib/share-import.js +234 -0
- package/lib/share.js +335 -0
- package/lib/store.js +797 -28
- package/lib/summarize.js +209 -23
- package/lib/tools.js +26 -6
- package/package.json +4 -2
- package/src/api-standalone.js +294 -8
- package/src/api.js +217 -3
- package/src/config.js +33 -1
- package/src/continuity.js +11 -3
- package/src/document.js +15 -2
- package/src/dream/decisions.js +176 -30
- package/src/dream/sleep.js +530 -94
- package/src/dream.js +174 -21
- package/src/fence.js +24 -0
- package/src/index.js +15 -6
- package/src/inject.js +9 -4
- package/src/lang.js +38 -16
- package/src/memory-source.js +9 -0
- package/src/organize.js +5 -1
- package/src/pipeline-nudge-auto.js +3 -0
- package/src/pipeline-nudge.js +41 -8
- package/src/pipeline-runner.js +15 -0
- package/src/pipeline.js +62 -1
- package/src/pre-inject-gate.js +14 -0
- package/src/profile-extract.js +4 -1
- package/src/provenance-http.js +251 -0
- package/src/serve.js +13 -0
- package/src/service.js +97 -4
- package/src/session-gate.js +91 -0
- package/src/settings.js +13 -1
- package/src/share-http.js +281 -0
- package/src/share-import.js +234 -0
- package/src/share.js +335 -0
- package/src/store.js +797 -28
- package/src/summarize.js +209 -23
- package/src/tools.js +26 -6
- package/test/api.test.js +523 -6
- package/test/audit.test.js +2 -2
- package/test/cli-receive.test.js +390 -0
- package/test/cli-share.test.js +437 -0
- package/test/client-share.test.js +225 -0
- package/test/client.test.js +354 -0
- package/test/config.test.js +8 -0
- package/test/conflict-reviewed.test.js +79 -0
- package/test/continuity.test.js +29 -1
- package/test/dedupe-candidate-window.test.js +180 -0
- package/test/dependency-overrides.test.js +78 -0
- package/test/differentiate-feedback.test.js +248 -0
- package/test/dream-candidate-hybrid.test.js +2 -2
- package/test/dream-decision-evidence.test.js +314 -0
- package/test/dream-fence.test.js +207 -0
- package/test/dream.test.js +87 -5
- package/test/helpers/dream-mock.js +6 -2
- package/test/inject-pools.test.js +2 -1
- package/test/llm-message-source.test.js +1 -1
- package/test/mint-source.test.js +84 -1
- package/test/pipeline-nudge-auto.test.js +26 -2
- package/test/pipeline-nudge.test.js +87 -0
- package/test/pipeline-r1r2.test.js +129 -0
- package/test/pipeline-runner.test.js +26 -0
- package/test/policy-epoch.test.js +2 -2
- package/test/prompt-stance.test.js +128 -0
- package/test/provenance-http.test.js +367 -0
- package/test/readme-claims.test.js +45 -0
- package/test/reasoning-effort.test.js +10 -10
- package/test/release-docs.test.js +7 -1
- package/test/scope-candidates.test.js +7 -4
- package/test/scope-state.test.js +130 -1
- package/test/security-doc.test.js +43 -4
- package/test/serve.test.js +62 -0
- package/test/session-gate.test.js +264 -0
- package/test/settings.test.js +4 -0
- package/test/share-foreign-rows.test.js +144 -0
- package/test/share-http.test.js +144 -0
- package/test/share-import.test.js +577 -0
- package/test/share-imported-guard.test.js +311 -0
- package/test/share-imported-identity.test.js +111 -0
- package/test/share-pack.test.js +511 -0
- package/test/share-receive-http.test.js +756 -0
- package/test/share-routes.test.js +337 -0
- package/test/share-store.test.js +133 -0
- package/test/share-validate.test.js +101 -0
- package/test/sleep-action-set.test.js +16 -11
- package/test/sleep-catchup.test.js +315 -0
- package/test/sleep-failure-backoff.test.js +156 -0
- package/test/sleep.test.js +293 -0
- package/test/standalone-api.test.js +574 -5
- package/test/store.test.js +220 -0
- package/test/summarize-parse-diagnostics.test.js +75 -0
- package/test/summarize.test.js +187 -2
package/lib/client.js
CHANGED
|
@@ -240,6 +240,42 @@ window.__ModuleLoader__.load({
|
|
|
240
240
|
t("memory.explorer.scopeBadge"));
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* 「标题 + 类型」两列的小清单。抽屉的整包退出清单与接收弹层的撞名清单是同一个形状
|
|
245
|
+
* (`{type,title}`),第三份手写 `.map` 就是第三处会各自漂移的地方——所以这里只有一份。
|
|
246
|
+
* 分享弹层的跳过清单**不走它**:那一列的第二格是 reason 标签,不是类型。
|
|
247
|
+
*/
|
|
248
|
+
function TypeRowList({ t, rows }) {
|
|
249
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
250
|
+
if (!list.length) return null;
|
|
251
|
+
return h("div", { className: "mneme-sharelist" },
|
|
252
|
+
list.map((row, i) => h("div", { key: `${row.type}-${i}`, className: "mneme-sharow" },
|
|
253
|
+
h("span", { className: "mneme-sharowtitle" }, row.title || ""),
|
|
254
|
+
h("span", { className: "mneme-sharowreason" }, typeLabel(t, row.type)))));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 外来行的来源徽标。别名不单独存列:`importedTagFor` 打的那条 `from-<别名>` 标签就是唯一读数,
|
|
259
|
+
* 所以这里只认标签,不猜别的来源。先判 `source === "imported"` 再判标签——
|
|
260
|
+
* 一个人手工给自己的一条打了 from-x 不算外来行,徽标会把它误报成别人的。
|
|
261
|
+
* 搜索态不出现这个徽标,且这是对的:那张 DTO 的 source 是召回渠道(src/service.js:939),
|
|
262
|
+
* 判据在那一侧读不出来。不许为了让搜索结果显示徽标而改成「认标签就算」——那会把
|
|
263
|
+
* 手工标签误报成外来行,也是 memory-source.js 那条契约的反面。
|
|
264
|
+
* from-shared 是空别名的中性值,照原样显示,不兜成「未知来源」。
|
|
265
|
+
*/
|
|
266
|
+
const ForeignBadge = ({ m, t }) => {
|
|
267
|
+
// 判据先于标签:`isImported` 就是 src/memory-source.js 那条契约的读数,
|
|
268
|
+
// 手工打了 from-x 标签的本机行不算外来行。正向字面量也要留着——test/client-share.test.js
|
|
269
|
+
// 的锁按 `m.source === "imported"` 钉这条契约,写成反向判断会让那条锁无处可钉。
|
|
270
|
+
const isImported = !!m && m.source === "imported";
|
|
271
|
+
if (!isImported) return null;
|
|
272
|
+
const tags = Array.isArray(m.tags) ? m.tags : [];
|
|
273
|
+
const tag = tags.find((x) => typeof x === "string" && x.startsWith("from-"));
|
|
274
|
+
if (!tag) return null;
|
|
275
|
+
return h("span", { className: "mneme-badge mneme-badge--foreign", title: tag },
|
|
276
|
+
t("memory.explorer.foreignBadge").replace("{alias}", tag.slice(5)));
|
|
277
|
+
};
|
|
278
|
+
|
|
243
279
|
// Unified API fetcher: attaches the optional apiToken (set in the settings
|
|
244
280
|
// view, persisted in localStorage) as a Bearer header. When no token has
|
|
245
281
|
// been configured the header is omitted and the API stays open (default).
|
|
@@ -277,6 +313,9 @@ window.__ModuleLoader__.load({
|
|
|
277
313
|
"memory.settings.group.search": "搜索",
|
|
278
314
|
"memory.settings.group.connect": "连接与安全",
|
|
279
315
|
"memory.settings.group.help": "帮助",
|
|
316
|
+
"memory.settings.toc.label": "目录",
|
|
317
|
+
"memory.settings.toc.aria": "设置分组目录",
|
|
318
|
+
"memory.settings.toc.jump": "跳到「{name}」",
|
|
280
319
|
"memory.settings.profile": "用户画像",
|
|
281
320
|
"memory.settings.profileHint": "写一段自我介绍(角色、背景、偏好),Agent 每轮对话都会自动带上。",
|
|
282
321
|
"memory.settings.profileSave": "保存画像",
|
|
@@ -311,7 +350,7 @@ window.__ModuleLoader__.load({
|
|
|
311
350
|
"memory.settings.personaDefaultCard": "默认卡(兜底)",
|
|
312
351
|
"memory.settings.personaAdd": "添加卡",
|
|
313
352
|
"memory.settings.personaAddPlaceholder": "输入 agent 身份名(如 nyx),按它寻址注入",
|
|
314
|
-
"memory.settings.personaEditingDefault": "
|
|
353
|
+
"memory.settings.personaEditingDefault": "正在编辑:默认卡——解析不到身份的会话一定用它;身份解析到了却没建卡(或卡被关掉)的会话,在默认口径下也落到这里,开着「严格人格寻址」则不注入人格",
|
|
315
354
|
"memory.settings.personaEditingFor": "正在编辑身份卡:",
|
|
316
355
|
"memory.settings.personaDeleteCard": "删除这张卡",
|
|
317
356
|
"memory.settings.personaView": "Agent 眼中的你(注入生效文本)",
|
|
@@ -454,6 +493,11 @@ window.__ModuleLoader__.load({
|
|
|
454
493
|
"memory.settings.mode.standard": "标准",
|
|
455
494
|
"memory.settings.mode.savedHint": "已保存,重启 DSH 后生效",
|
|
456
495
|
"memory.settings.mode.offList": "已关闭:巩固(autoDream)· 实体抽取 · 语义搜索",
|
|
496
|
+
// #514:用户把宿主「极简(minimal)」与插件「轻量」混为一谈,以为关掉人格是
|
|
497
|
+
// 轻量模式干的。这个词条把两件事分开说清楚,不再让用户自己猜。
|
|
498
|
+
"memory.settings.mode.hostPresetHint": "注意区分:这里的「轻量 / 标准」是插件的记忆引擎档位,与 DSH 宿主的「极简模式(minimal 预设)」是两回事——轻量档不会关闭人格定义。",
|
|
499
|
+
"memory.status.minimalPresetTitle": "当前会话用的是宿主的「极简模式」",
|
|
500
|
+
"memory.status.minimalPresetBody": "极简预设(minimal)把 system prompt 整体接管了,记忆、用户画像、人格定义都不会送达模型。这是宿主的设计边界,插件改不了。想要它们生效:把 DSH 切到标准模式(或在 ~/.dsh/settings.yaml 设 agent-presets.default: standard);过渡方案是把画像与规则写进 AGENTS.md——那条走指令路径,不受这个压制。",
|
|
457
501
|
"memory.settings.extapi.title": "外部访问 API",
|
|
458
502
|
"memory.settings.extapi.desc": "开放一个本机接口,让其他工具(插件、命令行、桌面应用)也能读写这份记忆库,默认只允许本机连接。改动保存后重启 DSH 生效。",
|
|
459
503
|
"memory.settings.extapi.enabled": "启用",
|
|
@@ -753,6 +797,61 @@ window.__ModuleLoader__.load({
|
|
|
753
797
|
"memory.explorer.imported": "已合并 {n} 条编辑",
|
|
754
798
|
"memory.explorer.importFailed": "导入失败",
|
|
755
799
|
"memory.explorer.importNoFile": "请先选择文件",
|
|
800
|
+
"memory.explorer.sharePack": "分享记忆包…",
|
|
801
|
+
"memory.explorer.shareTitle": "分享记忆包",
|
|
802
|
+
"memory.explorer.shareHint": "把本机活跃记忆打成一个可外发的包:密钥/PII 整条不发,本机路径抹成 ~,归档与遗忘的不进。",
|
|
803
|
+
"memory.explorer.shareAlias": "分享别名",
|
|
804
|
+
"memory.explorer.shareAliasBlank": "留空 = from-shared",
|
|
805
|
+
"memory.explorer.shareType": "只发这一类",
|
|
806
|
+
"memory.explorer.shareBuild": "生成",
|
|
807
|
+
"memory.explorer.shareBuilding": "生成中…",
|
|
808
|
+
"memory.explorer.shareFailed": "生成失败",
|
|
809
|
+
"memory.explorer.shareIncluded": "进包 {n} 条",
|
|
810
|
+
"memory.explorer.shareDeduped": "另有 {n} 条同键折叠,够格进包 {m} 条",
|
|
811
|
+
"memory.explorer.shareSkipped": "跳过 {n} 条",
|
|
812
|
+
"memory.explorer.share.skip.archived": "归档",
|
|
813
|
+
"memory.explorer.share.skip.forgotten": "已遗忘",
|
|
814
|
+
"memory.explorer.share.skip.readonly_type": "只读指针行",
|
|
815
|
+
"memory.explorer.share.skip.excluded_type": "不外发的类型",
|
|
816
|
+
"memory.explorer.share.skip.blank_or_noise": "空白或噪声",
|
|
817
|
+
"memory.explorer.share.skip.sensitive": "密钥或 PII",
|
|
818
|
+
"memory.explorer.shareSkipList": "跳过清单({n} 条)",
|
|
819
|
+
"memory.explorer.shareSkipListCapped": "清单只列前 {n} 条,上面计数是全量",
|
|
820
|
+
"memory.explorer.shareNoTitle": "(标题不外发)",
|
|
821
|
+
"memory.explorer.shareTooLarge": "包体 {bytes} 字节,超过 {limit} 字节水位",
|
|
822
|
+
"memory.explorer.shareEmpty": "没有够格外发的条目,包是空的",
|
|
823
|
+
"memory.explorer.shareDownload": "下载包文件",
|
|
824
|
+
"memory.explorer.receivePack": "导入分享包…",
|
|
825
|
+
"memory.explorer.receiveTitle": "导入分享包",
|
|
826
|
+
"memory.explorer.receiveHint": "选她发来的包文件。先干跑预览,点确认才写库;导入的行进外来区,不参与本机的巩固、降级或整理。",
|
|
827
|
+
"memory.explorer.receivePick": "选择包文件…",
|
|
828
|
+
"memory.explorer.receiveNotJson": "这个文件不是 JSON",
|
|
829
|
+
"memory.explorer.receiveNotPack": "这个文件不是分享包(顶层应该是一个 JSON 对象)",
|
|
830
|
+
"memory.explorer.receivePreview": "先干跑预览",
|
|
831
|
+
"memory.explorer.receiveConfirm": "确认导入",
|
|
832
|
+
"memory.explorer.receiveFailed": "导入失败",
|
|
833
|
+
"memory.explorer.receiveFrom": "来自 {alias}",
|
|
834
|
+
"memory.explorer.receiveAnonymous": "未署名",
|
|
835
|
+
"memory.explorer.receiveWouldCreate": "将新建 {n} 条",
|
|
836
|
+
"memory.explorer.receiveDidCreate": "已新建 {n} 条",
|
|
837
|
+
"memory.explorer.receiveExisting": "{n} 条已在库中",
|
|
838
|
+
"memory.explorer.receiveUnchanged": "{n} 条正文相同",
|
|
839
|
+
"memory.explorer.receiveConflicts": "撞名 {n} 条(不合并,请自己看)",
|
|
840
|
+
"memory.explorer.receivePackCount": "包内 {n} 条",
|
|
841
|
+
"memory.explorer.receiveStillDry": "没有写库:这一枪仍是干跑",
|
|
842
|
+
"memory.explorer.filterForeign": "外来",
|
|
843
|
+
"memory.explorer.filterForeignHint": "只看别人分享进来的记忆(source=imported)。这些行不参与本机的巩固、降级或整理。",
|
|
844
|
+
"memory.explorer.filterForeignSearchHint": "搜索时这一档不按:搜索结果里没有可用的来源读数。要看外来区请先清掉搜索词。",
|
|
845
|
+
"memory.explorer.foreignBadge": "来自 {alias}",
|
|
846
|
+
"memory.explorer.foreignPack": "所属分享包",
|
|
847
|
+
"memory.explorer.undoPack": "收起这个包",
|
|
848
|
+
"memory.explorer.undoPackPreview": "将删除这个包的 {n} 条外来记忆(物理删除)",
|
|
849
|
+
"memory.explorer.undoPackConfirm": "确认删除 {n} 条",
|
|
850
|
+
"memory.explorer.undoPackDone": "已收起 {n} 条",
|
|
851
|
+
"memory.explorer.undoPackFailed": "收起失败",
|
|
852
|
+
"memory.explorer.undoPackBusy": "收起中…",
|
|
853
|
+
"memory.explorer.undoPackStillDry": "没有删除:这一枪仍是干跑",
|
|
854
|
+
"memory.explorer.undoPackGone": "这个包在本库里已经没有行了",
|
|
756
855
|
"memory.explorer.newMemory": "新增记忆…",
|
|
757
856
|
"memory.explorer.newTitle": "新增记忆",
|
|
758
857
|
"memory.explorer.newHint": "手动写一条记忆。同类型同标题会并入既有条目,而不是长出第二行。",
|
|
@@ -793,6 +892,12 @@ window.__ModuleLoader__.load({
|
|
|
793
892
|
"memory.explorer.detail.quality": "质量分",
|
|
794
893
|
"memory.explorer.detail.entities": "关联实体",
|
|
795
894
|
"memory.explorer.detail.entitiesEmpty": "开启实体抽取后,相关人物/项目会出现在这里",
|
|
895
|
+
"memory.explorer.detail.provenance": "蒸发归因",
|
|
896
|
+
"memory.explorer.detail.provenanceEmpty": "这条记忆没有归因:早于改造的历史行不补、也不猜",
|
|
897
|
+
"memory.explorer.detail.provenanceMint": "铸造自",
|
|
898
|
+
"memory.explorer.detail.provenanceWindow": "蒸馏窗口",
|
|
899
|
+
"memory.explorer.detail.provenanceNoRun": "无运行归因(历史行)",
|
|
900
|
+
"memory.explorer.detail.provenanceRecords": "{n} 条原文",
|
|
796
901
|
"memory.explorer.detail.closeAria": "关闭详情",
|
|
797
902
|
"memory.explorer.detail.editTitle": "标题",
|
|
798
903
|
"memory.explorer.detail.editContent": "内容",
|
|
@@ -800,8 +905,13 @@ window.__ModuleLoader__.load({
|
|
|
800
905
|
"memory.explorer.detail.starLabel": "设为 {n} 星",
|
|
801
906
|
"memory.status.dream": "最近巩固",
|
|
802
907
|
"memory.status.dreamNever": "尚未运行",
|
|
908
|
+
"memory.status.dreamOrganizeOnly": "最近一次后台运行是 agent 主动整理({time}),不计入巩固",
|
|
803
909
|
"memory.status.conflicts": "待确认冲突",
|
|
804
910
|
"memory.status.conflictsHint": "冻结的矛盾记忆,等待人工确认",
|
|
911
|
+
"memory.status.skippedTitle": "本轮跳过 {n} 条决定",
|
|
912
|
+
"memory.status.skippedHint": "这些决定被校验器拦下,未执行。同类原因已合并计数——出现一整类动作全部被拦,通常意味着提示词与护栏没对齐,值得看一眼。",
|
|
913
|
+
"memory.status.skippedMore": "另有 {n} 类",
|
|
914
|
+
"memory.status.skippedNone": "本轮没有跳过任何决定",
|
|
805
915
|
"memory.status.injectSuppressed": "极简模式下注入按宿主设计关闭",
|
|
806
916
|
"memory.status.injectSuppressedHint": "当前会话使用 minimal 预设:记忆注入 / 用户画像 / 热记忆不会送达模型(宿主设计,非插件缺陷)。切到标准模式,或在 ~/.dsh/settings.yaml 设 agent-presets.default: standard;过渡方案:把画像与规则写进 AGENTS.md",
|
|
807
917
|
"memory.status.injectPreview": "注入预览",
|
|
@@ -995,6 +1105,9 @@ window.__ModuleLoader__.load({
|
|
|
995
1105
|
"memory.settings.group.search": "Search",
|
|
996
1106
|
"memory.settings.group.connect": "Connections & safety",
|
|
997
1107
|
"memory.settings.group.help": "Help",
|
|
1108
|
+
"memory.settings.toc.label": "Contents",
|
|
1109
|
+
"memory.settings.toc.aria": "Settings section index",
|
|
1110
|
+
"memory.settings.toc.jump": "Jump to {name}",
|
|
998
1111
|
"memory.settings.profile": "User Profile",
|
|
999
1112
|
"memory.settings.profileHint": "Describe yourself once — the agent reads it every turn.",
|
|
1000
1113
|
"memory.settings.profileSave": "Save Profile",
|
|
@@ -1029,7 +1142,7 @@ window.__ModuleLoader__.load({
|
|
|
1029
1142
|
"memory.settings.personaDefaultCard": "Default (fallback)",
|
|
1030
1143
|
"memory.settings.personaAdd": "Add card",
|
|
1031
1144
|
"memory.settings.personaAddPlaceholder": "Agent identity (e.g. nyx) used to address injection",
|
|
1032
|
-
"memory.settings.personaEditingDefault": "Editing: default card — used by sessions
|
|
1145
|
+
"memory.settings.personaEditingDefault": "Editing: default card — always used by sessions with no resolvable identity; a session whose identity has no card (or whose card is off) also lands here under the default mode, while strict persona addressing injects nothing instead",
|
|
1033
1146
|
"memory.settings.personaEditingFor": "Editing identity card:",
|
|
1034
1147
|
"memory.settings.personaDeleteCard": "Delete this card",
|
|
1035
1148
|
"memory.settings.personaView": "You, as the agent sees you (injected text)",
|
|
@@ -1168,6 +1281,9 @@ window.__ModuleLoader__.load({
|
|
|
1168
1281
|
"memory.settings.mode.standard": "Standard",
|
|
1169
1282
|
"memory.settings.mode.savedHint": "Saved. Takes effect after restarting DSH",
|
|
1170
1283
|
"memory.settings.mode.offList": "Off: consolidation (autoDream) · entity extraction · semantic search",
|
|
1284
|
+
"memory.settings.mode.hostPresetHint": "Two different things: Light/Standard here is this plugin's memory engine tier; the host's minimal preset is separate — Light never disables persona.",
|
|
1285
|
+
"memory.status.minimalPresetTitle": "This session runs the host's minimal preset",
|
|
1286
|
+
"memory.status.minimalPresetBody": "The minimal preset takes over the whole system prompt, so memory, profile and persona never reach the model. That is a host design boundary the plugin cannot change. To get them back: switch DSH to standard mode (or set agent-presets.default: standard in ~/.dsh/settings.yaml); as an interim workaround put profile and rules in AGENTS.md — that path is not suppressed.",
|
|
1171
1287
|
"memory.settings.extapi.title": "External API",
|
|
1172
1288
|
"memory.settings.extapi.desc": "Opens a local API so other tools (plugins, CLIs, desktop apps) can also read and write this memory store; binds to localhost only by default. Changes take effect after restarting DSH.",
|
|
1173
1289
|
"memory.settings.extapi.enabled": "Enable",
|
|
@@ -1450,6 +1566,61 @@ window.__ModuleLoader__.load({
|
|
|
1450
1566
|
"memory.explorer.imported": "Merged {n} edits",
|
|
1451
1567
|
"memory.explorer.importFailed": "Import failed",
|
|
1452
1568
|
"memory.explorer.importNoFile": "Choose a file first",
|
|
1569
|
+
"memory.explorer.sharePack": "Share memories…",
|
|
1570
|
+
"memory.explorer.shareTitle": "Memory share pack",
|
|
1571
|
+
"memory.explorer.shareHint": "Pack your active memories for someone else: rows holding secrets are dropped whole, home paths become ~, archived and forgotten rows stay out.",
|
|
1572
|
+
"memory.explorer.shareAlias": "Share alias",
|
|
1573
|
+
"memory.explorer.shareAliasBlank": "blank = from-shared",
|
|
1574
|
+
"memory.explorer.shareType": "Only this type",
|
|
1575
|
+
"memory.explorer.shareBuild": "Build",
|
|
1576
|
+
"memory.explorer.shareBuilding": "Building…",
|
|
1577
|
+
"memory.explorer.shareFailed": "Could not build the pack",
|
|
1578
|
+
"memory.explorer.shareIncluded": "{n} rows in the pack",
|
|
1579
|
+
"memory.explorer.shareDeduped": "{n} folded as duplicates; {m} rows qualified",
|
|
1580
|
+
"memory.explorer.shareSkipped": "{n} rows skipped",
|
|
1581
|
+
"memory.explorer.share.skip.archived": "Archived",
|
|
1582
|
+
"memory.explorer.share.skip.forgotten": "Forgotten",
|
|
1583
|
+
"memory.explorer.share.skip.readonly_type": "Read-only pointers",
|
|
1584
|
+
"memory.explorer.share.skip.excluded_type": "Excluded types",
|
|
1585
|
+
"memory.explorer.share.skip.blank_or_noise": "Blank or noise",
|
|
1586
|
+
"memory.explorer.share.skip.sensitive": "Secrets or PII",
|
|
1587
|
+
"memory.explorer.shareSkipList": "Skip list ({n})",
|
|
1588
|
+
"memory.explorer.shareSkipListCapped": "List shows the first {n}; the counts above are complete",
|
|
1589
|
+
"memory.explorer.shareNoTitle": "(title withheld)",
|
|
1590
|
+
"memory.explorer.shareTooLarge": "Pack is {bytes} bytes, over the {limit}-byte water mark",
|
|
1591
|
+
"memory.explorer.shareEmpty": "Nothing qualified - the pack is empty",
|
|
1592
|
+
"memory.explorer.shareDownload": "Download pack file",
|
|
1593
|
+
"memory.explorer.receivePack": "Import a share pack…",
|
|
1594
|
+
"memory.explorer.receiveTitle": "Import a share pack",
|
|
1595
|
+
"memory.explorer.receiveHint": "Pick the file she sent. The first pass is a dry run; nothing is written until Confirm. Imported rows join the foreign area and never take part in this machine's consolidation, demotion or tidying.",
|
|
1596
|
+
"memory.explorer.receivePick": "Choose pack file…",
|
|
1597
|
+
"memory.explorer.receiveNotJson": "That file is not JSON",
|
|
1598
|
+
"memory.explorer.receiveNotPack": "That file is not a share pack (the top level should be one JSON object)",
|
|
1599
|
+
"memory.explorer.receivePreview": "Preview (dry run)",
|
|
1600
|
+
"memory.explorer.receiveConfirm": "Confirm import",
|
|
1601
|
+
"memory.explorer.receiveFailed": "Import failed",
|
|
1602
|
+
"memory.explorer.receiveFrom": "From {alias}",
|
|
1603
|
+
"memory.explorer.receiveAnonymous": "unnamed",
|
|
1604
|
+
"memory.explorer.receiveWouldCreate": "Would create {n}",
|
|
1605
|
+
"memory.explorer.receiveDidCreate": "Created {n}",
|
|
1606
|
+
"memory.explorer.receiveExisting": "{n} already in the store",
|
|
1607
|
+
"memory.explorer.receiveUnchanged": "{n} identical already",
|
|
1608
|
+
"memory.explorer.receiveConflicts": "{n} title clashes (not merged - check them yourself)",
|
|
1609
|
+
"memory.explorer.receivePackCount": "{n} rows in the pack",
|
|
1610
|
+
"memory.explorer.receiveStillDry": "Nothing was written: that call stayed dry",
|
|
1611
|
+
"memory.explorer.filterForeign": "Foreign",
|
|
1612
|
+
"memory.explorer.filterForeignHint": "Only rows shared in by someone else (source=imported). They never join this machine's consolidation, demotion or tidying.",
|
|
1613
|
+
"memory.explorer.filterForeignSearchHint": "Not applied while searching: search results carry no usable provenance readout. Clear the query to browse the foreign area.",
|
|
1614
|
+
"memory.explorer.foreignBadge": "From {alias}",
|
|
1615
|
+
"memory.explorer.foreignPack": "Share pack",
|
|
1616
|
+
"memory.explorer.undoPack": "Withdraw this pack",
|
|
1617
|
+
"memory.explorer.undoPackPreview": "This would delete {n} foreign rows from that pack (hard delete)",
|
|
1618
|
+
"memory.explorer.undoPackConfirm": "Delete {n} rows",
|
|
1619
|
+
"memory.explorer.undoPackDone": "Withdrew {n} rows",
|
|
1620
|
+
"memory.explorer.undoPackFailed": "Withdrawal failed",
|
|
1621
|
+
"memory.explorer.undoPackBusy": "Withdrawing…",
|
|
1622
|
+
"memory.explorer.undoPackStillDry": "Nothing was deleted: that call stayed dry",
|
|
1623
|
+
"memory.explorer.undoPackGone": "That pack has no rows left in this store",
|
|
1453
1624
|
"memory.explorer.newMemory": "New memory…",
|
|
1454
1625
|
"memory.explorer.newTitle": "New memory",
|
|
1455
1626
|
"memory.explorer.newHint": "Write one memory by hand. Same type + same title merges into the existing entry instead of minting a second row.",
|
|
@@ -1490,6 +1661,12 @@ window.__ModuleLoader__.load({
|
|
|
1490
1661
|
"memory.explorer.detail.quality": "Quality",
|
|
1491
1662
|
"memory.explorer.detail.entities": "Related entities",
|
|
1492
1663
|
"memory.explorer.detail.entitiesEmpty": "Turn on entity extraction and related people / projects appear here",
|
|
1664
|
+
"memory.explorer.detail.provenance": "Distillation provenance",
|
|
1665
|
+
"memory.explorer.detail.provenanceEmpty": "No attribution for this memory: rows older than the change are left blank, never guessed",
|
|
1666
|
+
"memory.explorer.detail.provenanceMint": "Minted by",
|
|
1667
|
+
"memory.explorer.detail.provenanceWindow": "Distill window",
|
|
1668
|
+
"memory.explorer.detail.provenanceNoRun": "No run attribution (legacy row)",
|
|
1669
|
+
"memory.explorer.detail.provenanceRecords": "{n} records",
|
|
1493
1670
|
"memory.explorer.detail.closeAria": "Close details",
|
|
1494
1671
|
"memory.explorer.detail.editTitle": "Title",
|
|
1495
1672
|
"memory.explorer.detail.editContent": "Content",
|
|
@@ -1497,8 +1674,13 @@ window.__ModuleLoader__.load({
|
|
|
1497
1674
|
"memory.explorer.detail.starLabel": "Set importance to {n} stars",
|
|
1498
1675
|
"memory.status.dream": "Last consolidation",
|
|
1499
1676
|
"memory.status.dreamNever": "Not yet run",
|
|
1677
|
+
"memory.status.dreamOrganizeOnly": "The last background run was an agent-initiated organize ({time}), not a consolidation",
|
|
1500
1678
|
"memory.status.conflicts": "Pending conflicts",
|
|
1501
1679
|
"memory.status.conflictsHint": "Frozen contradictory memories awaiting confirmation",
|
|
1680
|
+
"memory.status.skippedTitle": "{n} decisions skipped this run",
|
|
1681
|
+
"memory.status.skippedHint": "These decisions were blocked by the validator and not applied. Identical reasons are counted together — a whole action type being blocked usually means the prompt and the guard disagree, which is worth a look.",
|
|
1682
|
+
"memory.status.skippedMore": "{n} more kinds",
|
|
1683
|
+
"memory.status.skippedNone": "No decisions were skipped this run",
|
|
1502
1684
|
"memory.status.injectSuppressed": "Injection disabled by host minimal preset",
|
|
1503
1685
|
"memory.status.injectSuppressedHint": "This session uses the minimal preset: memory injection / profile / hot memory never reach the model (by host design, not a plugin defect). Switch to standard mode, or set agent-presets.default: standard in ~/.dsh/settings.yaml. Interim: put profile and rules in AGENTS.md",
|
|
1504
1686
|
"memory.status.injectPreview": "Injection preview",
|
|
@@ -1847,6 +2029,13 @@ window.__ModuleLoader__.load({
|
|
|
1847
2029
|
// --- graph sub-view (fills the content area under the tabs) ---
|
|
1848
2030
|
".mneme-graph{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;padding:12px 16px 16px;box-sizing:border-box}",
|
|
1849
2031
|
".mneme-graphbar{display:flex;gap:8px;align-items:center;flex:none;margin-bottom:10px}",
|
|
2032
|
+
// touch-action 保持 none:图谱的拖拽与缩放只挂了 mousedown/mousemove/wheel,
|
|
2033
|
+
// 全仓 onPointerDown/onTouchStart 命中 0,所以在触屏上它本来就不是一个可交手势的
|
|
2034
|
+
// 面。改成 pan-y 会让纵向拖拽退回页面滚动——而浏览器在 touch-action:none 下会派发
|
|
2035
|
+
// 兼容鼠标事件,触屏纵向拖拽究竟走哪条路,没有真机无法定论;这一版不做无法复验的
|
|
2036
|
+
// 行为变更,留给出指针事件重写的那条车道。高度也不在这里限:480 主断点已压到
|
|
2037
|
+
// min(320px,52vh),把 52vh 写进基础规则会在常见窗口高度(≈680px)下把桌面也压成
|
|
2038
|
+
// 353px,等于顺手改了大屏观感。
|
|
1850
2039
|
".mneme-graphsvg{flex:1;min-height:180px;width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-button-elevated-fill);cursor:grab;touch-action:none}",
|
|
1851
2040
|
".mneme-gnode{cursor:pointer}",
|
|
1852
2041
|
".mneme-gnode circle{stroke:var(--dsw-alias-bg-layer-2);stroke-width:2;transition:stroke-width .12s}",
|
|
@@ -1890,7 +2079,9 @@ window.__ModuleLoader__.load({
|
|
|
1890
2079
|
".mneme-layer{margin-top:24px}",
|
|
1891
2080
|
".mneme-layerhead{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:10px}",
|
|
1892
2081
|
".mneme-layercount{font-size:12px;font-weight:400;color:var(--mneme-fg-dim)}",
|
|
1893
|
-
|
|
2082
|
+
// 属性网格同理:min(200px,100%) 的 100% 兜底压在基础规则上,实体页 detail 在
|
|
2083
|
+
// 481–519px 只剩 257–276px(rail 仍占 224px 横排),裸 200px 下限会溢出。
|
|
2084
|
+
".mneme-attrgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr));gap:8px}",
|
|
1894
2085
|
".mneme-attrcard{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}",
|
|
1895
2086
|
".mneme-attrkey{font-size:11px;color:var(--mneme-fg-dim);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
1896
2087
|
".mneme-attrval{font-size:13px;line-height:19px;color:var(--dsw-alias-label-primary);word-break:break-word}",
|
|
@@ -2005,8 +2196,12 @@ window.__ModuleLoader__.load({
|
|
|
2005
2196
|
".mneme-seg button{border:none;background:none;color:var(--mneme-fg-dim);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:5px 10px;display:inline-flex;align-items:center;gap:5px}",
|
|
2006
2197
|
".mneme-seg button:hover{color:var(--dsw-alias-label-primary)}",
|
|
2007
2198
|
".mneme-seg button.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
2008
|
-
// 卡片网格:响应式 auto-fill
|
|
2009
|
-
|
|
2199
|
+
// 卡片网格:响应式 auto-fill,卡片悬停轻浮起。
|
|
2200
|
+
// 轨道下限用 min(252px,100%) 而不是裸 252px:容器(.mneme-xcontent)只有
|
|
2201
|
+
// 265–294px 时裸下限仍要求 300px 轨道,网格横向溢出 2–11px 被外层 overflow:hidden
|
|
2202
|
+
// 切掉。这个洞出现在 481–519px——480 主断点之外、两栏又放不下的中间带,所以
|
|
2203
|
+
// 兜底必须落在基础规则上,只在断点里写等于给中间带留缺口。
|
|
2204
|
+
".mneme-cards{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(min(252px,100%),1fr));gap:14px;padding:20px 24px 32px;align-content:start}",
|
|
2010
2205
|
".mneme-card{display:flex;flex-direction:column;gap:7px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 13px;background:var(--dsw-alias-bg-layer-1);cursor:pointer;font-family:inherit;text-align:left;transition:border-color .12s,box-shadow .12s,transform .12s}",
|
|
2011
2206
|
".mneme-card:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,var(--dsw-alias-border-l2));box-shadow:0 6px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);transform:translateY(-1px)}",
|
|
2012
2207
|
".mneme-card.mneme-active{border-color:var(--dsw-alias-state-business-primary)}",
|
|
@@ -2104,6 +2299,22 @@ window.__ModuleLoader__.load({
|
|
|
2104
2299
|
".mneme-menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
2105
2300
|
".mneme-impdialog{position:absolute;right:0;top:calc(100% + 6px);width:300px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 12px 32px color-mix(in srgb,var(--dsw-alias-label-primary) 16%,transparent);padding:14px;z-index:20;animation:mneme-fadein .1s ease-out}",
|
|
2106
2301
|
".mneme-improw{display:flex;align-items:center;gap:8px;margin-top:10px}",
|
|
2302
|
+
// 分享/接收弹层:宽度与滚动共用 impdialog 的锚,正文一律 --mneme-* 语义层
|
|
2303
|
+
// (纪律 10:宿主 label-tertiary/label-dimmed 是分隔线层,不是正文色)。
|
|
2304
|
+
".mneme-sharedialog{width:340px;max-height:min(72vh,560px);overflow-y:auto}",
|
|
2305
|
+
".mneme-sharebody{margin-top:10px;display:flex;flex-direction:column;gap:2px}",
|
|
2306
|
+
".mneme-sharebreak{display:flex;flex-wrap:wrap;gap:8px;margin-top:4px}",
|
|
2307
|
+
".mneme-sharebreakitem{font-size:11px;line-height:15px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
2308
|
+
".mneme-sharelist{margin-top:4px;max-height:132px;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px}",
|
|
2309
|
+
".mneme-sharow{display:flex;justify-content:space-between;gap:8px;font-size:11.5px;line-height:16px}",
|
|
2310
|
+
".mneme-sharowtitle{color:var(--dsw-alias-label-primary);min-width:0;word-break:break-word}",
|
|
2311
|
+
".mneme-sharowreason{flex:none;color:var(--mneme-fg-dim)}",
|
|
2312
|
+
// 外来徽标:只有文字色与描边,**不给底色**。`.mneme-badge--scope` / `--frozen` 那两类
|
|
2313
|
+
// 有自己的 10–12% 色晕,而 `test/contrast.test.js` 的正文对比度是按**该条规则自己的
|
|
2314
|
+
// 背景**合成的:`--mneme-ok` 在画布上是 4.62,铺上 12% 绿晕就掉到 4.17(<4.5)。
|
|
2315
|
+
// 与其为它造一个新语义令牌或改用别的色,不如让这一颗走无晕的形状——来源读数本来就在
|
|
2316
|
+
// 文字里(`from-<别名>`),不靠底色说话。
|
|
2317
|
+
".mneme-badge--foreign{color:var(--mneme-ok);border:1px solid color-mix(in srgb,var(--mneme-ok) 34%,transparent)}",
|
|
2107
2318
|
// 瞬时操作提示(删除/归档):挂在浏览层底部居中,不被抽屉卸载吞掉
|
|
2108
2319
|
".mneme-x{position:relative}",
|
|
2109
2320
|
".mneme-toast{position:absolute;left:50%;bottom:20px;transform:translateX(-50%);background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);font-size:12.5px;line-height:18px;padding:7px 16px;border-radius:10px;z-index:30;box-shadow:0 8px 24px color-mix(in srgb,var(--dsw-alias-label-primary) 25%,transparent);animation:mneme-fadein .15s ease-out}",
|
|
@@ -2238,9 +2449,20 @@ window.__ModuleLoader__.load({
|
|
|
2238
2449
|
// self-contained unit inside the stacked settings view ---
|
|
2239
2450
|
".mneme-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;margin-bottom:10px}",
|
|
2240
2451
|
// 设置页分组标题(个性化 / 记忆引擎 / 搜索 / 连接与安全 / 帮助):
|
|
2241
|
-
//
|
|
2242
|
-
|
|
2243
|
-
".mneme-set-grouphead:
|
|
2452
|
+
// 先给「这一段在管什么」,卡片才是具体选项;scroll-margin-top 是给 #518 的粘顶目录(实测约 74px 高)留的跳转余量
|
|
2453
|
+
// ——没有它,点 chip 滚过去的标题就停在目录背面
|
|
2454
|
+
".mneme-set-grouphead{font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin:24px 0 10px;scroll-margin-top:84px}",
|
|
2455
|
+
".mneme-set-toc+.mneme-set-grouphead{margin-top:8px}", // nav 才是第一个孩子::first-child 自 #518 起永不命中
|
|
2456
|
+
// 设置页目录(#518):分组标题 + 一栏跳转 chip。粘顶是因为设置页是整块
|
|
2457
|
+
// 滚动的(.mneme-set 自己 overflow-y),长页里目录滚走了就等于没有——用户
|
|
2458
|
+
// 提的诉求正是「方便直接跳转」,跳不回去的目录不算数。
|
|
2459
|
+
// 窄屏收成横向滚动:换行会让粘顶条吃掉半屏高度,而设置页本身已经很挤。
|
|
2460
|
+
".mneme-set-toc{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-base,var(--dsw-alias-bg-layer-1));padding:8px 0 10px;margin-bottom:4px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
2461
|
+
".mneme-set-toclabel{font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--mneme-fg-dim);margin-bottom:6px}",
|
|
2462
|
+
".mneme-set-toclist{display:flex;gap:6px;overflow-x:auto;padding:3px}", // 不 wrap:chip flex:none,窄屏横向滚;3px 给 focus ring 留位(滚动容器会裁掉描边)
|
|
2463
|
+
".mneme-set-tocchip{flex:none;height:26px;padding:0 10px;border-radius:13px;border:1px solid var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:24px;transition:border-color .12s,color .12s}",
|
|
2464
|
+
".mneme-set-tocchip:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}",
|
|
2465
|
+
".mneme-set-tocchip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2244
2466
|
".mneme-set-token{font-family:monospace;font-size:12px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);word-break:break-all;user-select:all}",
|
|
2245
2467
|
".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2246
2468
|
// --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) ---
|
|
@@ -2432,6 +2654,48 @@ window.__ModuleLoader__.load({
|
|
|
2432
2654
|
// 而「为什么禁用」本来就由 title 说明(读不到会话工作目录)。
|
|
2433
2655
|
".mneme-pipe-distillBtn:disabled{cursor:not-allowed;border-style:dashed;color:var(--mneme-fg-dim)}",
|
|
2434
2656
|
".mneme-pipe-distillNote{color:var(--mneme-fg-dim);line-height:17px;min-width:0}",
|
|
2657
|
+
// ===== 移动端适配(mobile-adapt 车道)=====
|
|
2658
|
+
// 为什么主通道是 @container 而不是 @media:面板有两种形态(overlay 弹层与
|
|
2659
|
+
// better-sidebar 工作台 tab),容器宽度与视口宽度并不相等;`.mneme-x` 是
|
|
2660
|
+
// container-type:inline-size 的容器根(1950 行),两种形态下容器查询都成立。
|
|
2661
|
+
// @media 只负责 `.mneme-overlay` 自己的外框——它不在 `.mneme-x` 之内,
|
|
2662
|
+
// 容器查询够不到它,这一条只能走视口。
|
|
2663
|
+
// 纪律:以下几块内一律不写 color/fill/stroke/background。contrast.test.js 的
|
|
2664
|
+
// 规则提取器按「单层花括号」匹配(/^([^{]+)\{([^}]*)\}$/),双花括号的
|
|
2665
|
+
// @container / @media 整串不匹配而被跳过——在这样的块里写颜色等于绕过 #178
|
|
2666
|
+
// 的 AA 主锁,属于静默失效,不是优化。布局属性不受影响。
|
|
2667
|
+
"@media (max-width: 640px){.mneme-overlay{width:calc(100vw - 16px);height:calc(100dvh - 16px);border-radius:12px}}",
|
|
2668
|
+
// 触屏没有 hover:设置页的删除钮 base 是 opacity:0、只靠 :hover/:focus-within
|
|
2669
|
+
// 恢复(2088-2089),手机上等于永久不可见。#457 与 #459 两次回滚都是因为把
|
|
2670
|
+
// hover 配色一起改了,所以这里只加「常显」,不碰颜色。min-height:24px 是
|
|
2671
|
+
// WCAG 2.5.8 AA 的触控目标下限,顺手补到常显按钮上。
|
|
2672
|
+
"@media (hover:none){.mneme-set-del{opacity:1;padding:0 8px;min-height:24px}.mneme-footbtn{min-height:24px}}",
|
|
2673
|
+
// 主断点 480:骨架换向,不是继续收窄数值。临界宽度实算——卡片要 252+48=300、
|
|
2674
|
+
// 左栏 216+1,面板得 ≥517px;实体属性网格要 248、rail 224+1,得 ≥473px。
|
|
2675
|
+
// 目标区间上沿只有 430px,靠收窄永远追不上,只能改骨架。
|
|
2676
|
+
// 左右分栏在窄容器里改上下:左栏/rail 限高可滚,不允许自然撑开把主区挤掉;
|
|
2677
|
+
// 不做折叠按钮(要 JS 状态 + 新 i18n 键,成本高于收益)。
|
|
2678
|
+
// 抽屉改满宽不丢关闭入口:抽屉自带 ✕(.mneme-drawerbar);锚点是
|
|
2679
|
+
// .mneme-xmain 自己的 position:relative(2252),换向后仍然成立。
|
|
2680
|
+
// 实体页换向的前提:rail 侧本就 .mneme-entrailbody{overflow-y:auto} +
|
|
2681
|
+
// .mneme-enttype{sticky}(2046/2047),叠放后天然可滚、零 JS、不加文案键。
|
|
2682
|
+
// 图谱只在 CSS 层止血(高度 + touch-action + 标签字号):它能拖能缩依赖
|
|
2683
|
+
// onMouseDown/onMouseMove/wheel,全仓 onPointerDown/onTouchStart 命中 0,
|
|
2684
|
+
// 那是能力缺失不是样式问题,归后续车道。
|
|
2685
|
+
// 页签的横向滚动也归这一档:4 个带图标的 tab 约 436px,而本档给 .mneme-xbar 留的
|
|
2686
|
+
// 可用宽度是容器宽减 62px(左 10 + 右 52 让位给绝对定位的 ⋯),480px 时只剩 418px
|
|
2687
|
+
// ——整段 361–480px 都会把页签顶到 ⋯ 底下,只写 360 档等于把症状留在主断点全程。
|
|
2688
|
+
// menu / impdialog / sharedialog 三处窄屏宽度必须带 box-sizing:border-box:全文件没有
|
|
2689
|
+
// *{box-sizing} 兜底(实数 0 处),声明的那个数是内容宽,外宽还要再加 padding+border
|
|
2690
|
+
// ——impdialog +30px、menu +12px,320px 视口下比屏幕还宽,会被 overlay 裁掉。
|
|
2691
|
+
// sharedialog 自己不带 padding:它和 impdialog 同挂一条 div(见 JSX 里的
|
|
2692
|
+
// `className:"mneme-impdialog mneme-sharedialog"`),盒模型由 impdialog 带过来,
|
|
2693
|
+
// 所以它同样 +30px;宽度取哪一条由源码顺序决定,故两条都得是 border-box。
|
|
2694
|
+
"@container (max-width: 480px){.mneme-xbar{justify-content:flex-start;padding:0 52px 0 10px}.mneme-vtab{padding:0 10px}.mneme-vtabs{min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.mneme-vtabs::-webkit-scrollbar{height:0}.mneme-xmain{flex-direction:column}.mneme-xside{width:100%;flex:0 1 auto;min-height:0;max-height:min(42vh,300px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2);overscroll-behavior:contain}.mneme-xbrowsebar{padding:8px 12px}.mneme-filterbar{padding:8px 12px}.mneme-drawer{width:100%}.mneme-drawerbody{padding:16px 14px 24px}.mneme-cards{padding:14px 12px 24px;overscroll-behavior:contain}.mneme-ent{flex-direction:column}.mneme-entrail{width:100%;flex:0 1 auto;min-height:0;max-height:min(40vh,280px);border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2)}.mneme-entrailbody{overscroll-behavior:contain}.mneme-entdetail{padding:16px 14px 24px}.mneme-set{padding:8px 14px 40px}.mneme-status{padding:16px 14px 32px}.mneme-menu{box-sizing:border-box;min-width:0;width:min(200px,calc(100cqw - 32px))}.mneme-impdialog{box-sizing:border-box;width:min(300px,calc(100cqw - 32px))}.mneme-sharedialog{box-sizing:border-box;width:min(340px,calc(100cqw - 32px))}.mneme-graph{padding:10px 12px 12px}.mneme-graphsvg{max-height:min(320px,52vh);touch-action:pan-y}.mneme-glabel{font-size:14px}.mneme-graphside{position:static;width:auto;max-width:100%;max-height:min(200px,30vh);margin-top:8px}}",
|
|
2695
|
+
// 360 档只做两件事:隐掉 14px 的 tab 图标(.mneme-vtab 带图标时约 109px/个、
|
|
2696
|
+
// 4 个约 436px,320px 容器原地就横向溢出),再把正文 padding 收到 12–14px。
|
|
2697
|
+
// 页签横向滚动不在这一档、属于 480 主断点,理由见 480 档那条算式。
|
|
2698
|
+
"@container (max-width: 360px){.mneme-vtabico{display:none}.mneme-vtab{padding:0 8px;font-size:13px}.mneme-xbar{padding:0 46px 0 8px}.mneme-cards{grid-template-columns:1fr;padding:12px 10px 20px}.mneme-xside{max-height:min(46vh,260px)}.mneme-entrail{max-height:min(44vh,240px)}.mneme-entdetail{padding:14px 12px 22px}.mneme-set{padding:8px 12px 36px}.mneme-status{padding:14px 12px 30px}}",
|
|
2435
2699
|
].join("\n");
|
|
2436
2700
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${CSS_TAG}"]`) === null) {
|
|
2437
2701
|
const tag = document.createElement("style");
|
|
@@ -2525,7 +2789,10 @@ window.__ModuleLoader__.load({
|
|
|
2525
2789
|
// ③ 关联记忆 memories recalled through the entity: prefix search
|
|
2526
2790
|
// ④ 关系图谱 the same ego graph the old graph-only tab drew
|
|
2527
2791
|
function EntityPanel({ t, focusEntity, onJumpMemory }) {
|
|
2528
|
-
const [entities, setEntities] = useState(null);
|
|
2792
|
+
const [entities, setEntities] = useState(null);
|
|
2793
|
+
// #512:这条记忆的归因——窗口级(哪一段原文蒸出来的)与行级(哪一轮铸造的)。
|
|
2794
|
+
// null = 还没读到(或读失败),两者要分得开,不能被当成「没有归因」。
|
|
2795
|
+
const [provenance, setProvenance] = useState(null); // null = still loading
|
|
2529
2796
|
const [railQuery, setRailQuery] = useState("");
|
|
2530
2797
|
const [entityName, setEntityName] = useState(focusEntity || "");
|
|
2531
2798
|
const [depth, setDepth] = useState(1);
|
|
@@ -3546,6 +3813,31 @@ window.__ModuleLoader__.load({
|
|
|
3546
3813
|
return "Unknown";
|
|
3547
3814
|
};
|
|
3548
3815
|
|
|
3816
|
+
// #182 / #514 注入状态探测:一处取数,两处消费(状态页告示卡、设置页提示)。
|
|
3817
|
+
// 分成两份读同一接口会让两处的「已知/未知」判断各自漂移——#514 里用户就是在
|
|
3818
|
+
// 设置页开关人格,却只在状态页才看得到原因。普通函数声明(会提升),这里紧
|
|
3819
|
+
// 挨着设置页放,是为了让源码顺序和「先谁用」对得上。
|
|
3820
|
+
function useInjectStatus() {
|
|
3821
|
+
const [state, setState] = useState({ loading: true, suppressed: false, agentPreset: null });
|
|
3822
|
+
useEffect(() => {
|
|
3823
|
+
let cancelled = false;
|
|
3824
|
+
apiFetch("/api/dsh-mneme/inject-status")
|
|
3825
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
3826
|
+
.then((j) => {
|
|
3827
|
+
if (!cancelled) {
|
|
3828
|
+
setState({
|
|
3829
|
+
loading: false,
|
|
3830
|
+
suppressed: !!(j && j.suppressed),
|
|
3831
|
+
agentPreset: (j && j.agentPreset) || null
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
})
|
|
3835
|
+
.catch(() => { if (!cancelled) setState({ loading: false, suppressed: false, agentPreset: null }); });
|
|
3836
|
+
return () => { cancelled = true; };
|
|
3837
|
+
}, []);
|
|
3838
|
+
return state;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3549
3841
|
function SettingsContent({ t }) {
|
|
3550
3842
|
const [profile, setProfile] = react.useState("");
|
|
3551
3843
|
const [rules, setRules] = react.useState([]);
|
|
@@ -3568,6 +3860,10 @@ window.__ModuleLoader__.load({
|
|
|
3568
3860
|
const [personaNewName, setPersonaNewName] = react.useState("");
|
|
3569
3861
|
const [personaSaved, setPersonaSaved] = react.useState(false);
|
|
3570
3862
|
const [personaError, setPersonaError] = react.useState("");
|
|
3863
|
+
// #514:人是在这一页开关人格的,所以「为什么关了没反应」的答案必须在
|
|
3864
|
+
// 这一页。与状态页告示卡共用 useInjectStatus,两边口径不会各自漂。
|
|
3865
|
+
const injectStatus = useInjectStatus();
|
|
3866
|
+
const minimalWarn = !injectStatus.loading && injectStatus.suppressed;
|
|
3571
3867
|
// 注入预览:文本由后端渲染(GET /persona 的 preview 字段),前端只显示、不自己拼。
|
|
3572
3868
|
// 原先这里是一段前端手拼的 profile + rules 列表,注释却写着「注入生效文本」——
|
|
3573
3869
|
// 它既没有表头、没有「- 用户画像:」前缀,也完全没有人格三行,两份渲染代码必然漂。
|
|
@@ -4059,9 +4355,43 @@ window.__ModuleLoader__.load({
|
|
|
4059
4355
|
);
|
|
4060
4356
|
}
|
|
4061
4357
|
|
|
4358
|
+
// 目录(#518):五项分组标题是设置页唯一的骨架,所以索引直接由这张表
|
|
4359
|
+
// 生成,而不是再手写一遍标题——两份清单迟早会漂,而漂了的目录会把人
|
|
4360
|
+
// 领到空处。tocRefs 是滚动目标,与渲染顺序一一对应(同一个 map 里产出)。
|
|
4361
|
+
const tocGroups = [
|
|
4362
|
+
{ key: "personal", label: t("memory.settings.group.personal") },
|
|
4363
|
+
{ key: "engine", label: t("memory.settings.group.engine") },
|
|
4364
|
+
{ key: "search", label: t("memory.settings.group.search") },
|
|
4365
|
+
{ key: "connect", label: t("memory.settings.group.connect") },
|
|
4366
|
+
{ key: "help", label: t("memory.settings.group.help") }
|
|
4367
|
+
];
|
|
4368
|
+
const tocRefs = useRef({});
|
|
4369
|
+
function jumpToGroup(key) {
|
|
4370
|
+
const node = tocRefs.current?.[key];
|
|
4371
|
+
// 偏移不在这里算:标题自己带 scroll-margin-top(见 .mneme-set-grouphead)。
|
|
4372
|
+
// 两处各量一次粘顶条高度必然对不上,跳转会把目标停在目录背后。
|
|
4373
|
+
if (node && node.scrollIntoView) node.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
4374
|
+
}
|
|
4375
|
+
const groupHead = (key) => h("div", {
|
|
4376
|
+
className: "mneme-set-grouphead",
|
|
4377
|
+
ref: (node) => { if (node) tocRefs.current[key] = node; }
|
|
4378
|
+
}, t("memory.settings.group." + key));
|
|
4379
|
+
|
|
4062
4380
|
return h("div", null,
|
|
4381
|
+
h("nav", { className: "mneme-set-toc", "aria-label": t("memory.settings.toc.aria") },
|
|
4382
|
+
h("div", { className: "mneme-set-toclabel" }, t("memory.settings.toc.label")),
|
|
4383
|
+
h("div", { className: "mneme-set-toclist" },
|
|
4384
|
+
...tocGroups.map((group) => h("button", {
|
|
4385
|
+
key: group.key,
|
|
4386
|
+
type: "button",
|
|
4387
|
+
className: "mneme-set-tocchip",
|
|
4388
|
+
title: t("memory.settings.toc.jump").replace("{name}", group.label),
|
|
4389
|
+
onClick: () => jumpToGroup(group.key)
|
|
4390
|
+
}, group.label))
|
|
4391
|
+
)
|
|
4392
|
+
),
|
|
4063
4393
|
// —— 个性化:画像 / 人格 / 规则 / 自定义指令(Agent 怎么对你) ——
|
|
4064
|
-
|
|
4394
|
+
groupHead("personal"),
|
|
4065
4395
|
// 用户画像 — who the agent is talking to
|
|
4066
4396
|
h("section", { className: "mneme-set-card" },
|
|
4067
4397
|
h("div", { className: "mneme-set-title" }, t("memory.settings.profile")),
|
|
@@ -4112,6 +4442,14 @@ window.__ModuleLoader__.load({
|
|
|
4112
4442
|
h("section", { className: "mneme-set-card" },
|
|
4113
4443
|
h("div", { className: "mneme-set-title" }, t("memory.settings.persona")),
|
|
4114
4444
|
h("div", { className: "mneme-set-desc" }, t("memory.settings.personaHint")),
|
|
4445
|
+
// #514:人格卡是最容易撞上宿主极简压制的地方——这里的提示比运行模式卡
|
|
4446
|
+
// 那句更要紧,因为用户就是在这里点开关的。
|
|
4447
|
+
minimalWarn && h("div", {
|
|
4448
|
+
style: { marginBottom: 10, padding: "8px 10px", borderLeft: "3px solid var(--mneme-warn)", fontSize: 12, lineHeight: "18px" }
|
|
4449
|
+
},
|
|
4450
|
+
h("div", { style: { fontWeight: 600, color: "var(--dsw-alias-label-primary)" } }, t("memory.status.minimalPresetTitle")),
|
|
4451
|
+
h("div", { style: { marginTop: 4, color: "var(--mneme-fg-dim)" } }, t("memory.status.minimalPresetBody"))
|
|
4452
|
+
),
|
|
4115
4453
|
// 卡列表:default + 各 byAgent 卡,点选切换编辑位;default 标注兜底位
|
|
4116
4454
|
h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 } },
|
|
4117
4455
|
h("button", {
|
|
@@ -4292,7 +4630,7 @@ window.__ModuleLoader__.load({
|
|
|
4292
4630
|
)
|
|
4293
4631
|
),
|
|
4294
4632
|
// —— 记忆引擎:运行模式(粗粒度)在前,功能开关(细粒度)紧随 ——
|
|
4295
|
-
|
|
4633
|
+
groupHead("engine"),
|
|
4296
4634
|
// 运行模式 — light vs standard chip radios; each click PUTs and the
|
|
4297
4635
|
// change only lands after a DSH restart (green saved hint says so).
|
|
4298
4636
|
h("section", { className: "mneme-set-card" },
|
|
@@ -4302,6 +4640,10 @@ window.__ModuleLoader__.load({
|
|
|
4302
4640
|
mode === null && !modeError
|
|
4303
4641
|
? h("div", { className: "mneme-set-hint" }, "…")
|
|
4304
4642
|
: h(react.Fragment, null,
|
|
4643
|
+
// #514:用户以为「轻量」关掉了人格。两个词都会在界面上出现,不先
|
|
4644
|
+
// 分开说,用户永远会把宿主的 minimal 记成插件的 light。
|
|
4645
|
+
h("div", { className: "mneme-set-hint", style: { marginBottom: 8 } },
|
|
4646
|
+
t("memory.settings.mode.hostPresetHint")),
|
|
4305
4647
|
h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
4306
4648
|
h("button", {
|
|
4307
4649
|
className: mode === "light" ? "mneme-chip mneme-active" : "mneme-chip",
|
|
@@ -4317,12 +4659,20 @@ window.__ModuleLoader__.load({
|
|
|
4317
4659
|
),
|
|
4318
4660
|
mode === "light" && h("div", { className: "mneme-set-hint", style: { marginTop: 8 } },
|
|
4319
4661
|
t("memory.settings.mode.offList"))
|
|
4320
|
-
)
|
|
4662
|
+
),
|
|
4663
|
+
// #514:suppressed 只在「autoInject 开着 + 宿主确实报了 minimal」时为真,
|
|
4664
|
+
// 所以这里不是吓唬人——关掉注入的用户、preset 未知的宿主都不会看到它。
|
|
4665
|
+
minimalWarn && h("div", {
|
|
4666
|
+
style: { marginTop: 10, padding: "8px 10px", borderLeft: "3px solid var(--mneme-warn)", fontSize: 12, lineHeight: "18px" }
|
|
4667
|
+
},
|
|
4668
|
+
h("div", { style: { fontWeight: 600, color: "var(--dsw-alias-label-primary)" } }, t("memory.status.minimalPresetTitle")),
|
|
4669
|
+
h("div", { style: { marginTop: 4, color: "var(--mneme-fg-dim)" } }, t("memory.status.minimalPresetBody"))
|
|
4670
|
+
)
|
|
4321
4671
|
),
|
|
4322
4672
|
// 功能开关 — 后端能力的细粒度闸门;改动即时保存,重启 DSH 后生效。
|
|
4323
4673
|
h(FeaturesCard, { t }),
|
|
4324
4674
|
// —— 搜索:语义召回 ——
|
|
4325
|
-
|
|
4675
|
+
groupHead("search"),
|
|
4326
4676
|
// 向量搜索 — semantic recall over an embeddings API
|
|
4327
4677
|
h("section", { className: "mneme-set-card" },
|
|
4328
4678
|
h("div", { className: "mneme-set-title" }, t("memory.settings.vectorTitle")),
|
|
@@ -4342,7 +4692,7 @@ window.__ModuleLoader__.load({
|
|
|
4342
4692
|
)
|
|
4343
4693
|
),
|
|
4344
4694
|
// —— 连接与安全:外部访问 API / 面板令牌 ——
|
|
4345
|
-
|
|
4695
|
+
groupHead("connect"),
|
|
4346
4696
|
// 外部访问 API — standalone HTTP service for plugins/CLI/desktop tools;
|
|
4347
4697
|
// the token is generated and kept by the backend, so it is read-only
|
|
4348
4698
|
// here with a copy affordance. Changes need a DSH restart.
|
|
@@ -4459,7 +4809,7 @@ window.__ModuleLoader__.load({
|
|
|
4459
4809
|
apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
|
|
4460
4810
|
),
|
|
4461
4811
|
// —— 帮助:支持与鼓励(图标行 + 检测更新 + star,兼当「关于」收尾) ——
|
|
4462
|
-
|
|
4812
|
+
groupHead("help"),
|
|
4463
4813
|
// 反馈三入口:新建 issue 预填(版本+平台)/ 邮件 / 浏览已知问题。
|
|
4464
4814
|
// 纯前端链接零后端成本;插件版本来自 /info,平台取 userAgent。GitHub
|
|
4465
4815
|
// 仓库当前没有 issue 模板,故用 issues/new?title=&body= 直接预填。
|
|
@@ -5191,7 +5541,10 @@ window.__ModuleLoader__.load({
|
|
|
5191
5541
|
const [stats, setStats] = useState(null);
|
|
5192
5542
|
const [heatStats, setHeatStats] = useState(null);
|
|
5193
5543
|
const [statsError, setStatsError] = useState(false);
|
|
5194
|
-
|
|
5544
|
+
// #521 评审:待确认冲突与「上一轮跳过明细」同源(/dream-status),一次请求两用。
|
|
5545
|
+
// 跳过明细必须出现在首屏——它原先只挂在折叠区里的 DreamStatusCards 上,而那个
|
|
5546
|
+
// 折叠区默认收起、收起时连子卡都不挂载,等于「一整类动作 100% 被拦」仍旧看不见。
|
|
5547
|
+
const [conf, setConf] = useState({ loading: true, error: false, pending: 0, skipped: [] });
|
|
5195
5548
|
const [focusOp, setFocusOp] = useState(null);
|
|
5196
5549
|
useEffect(() => {
|
|
5197
5550
|
let cancelled = false;
|
|
@@ -5220,9 +5573,19 @@ window.__ModuleLoader__.load({
|
|
|
5220
5573
|
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
5221
5574
|
.then((d) => {
|
|
5222
5575
|
if (cancelled) return;
|
|
5223
|
-
|
|
5576
|
+
// #495:这块 KPI 说的是「上一轮巩固跳过了什么」,所以要的是**巩固轮**。
|
|
5577
|
+
// organize 回执同样带 skipped,但它的跳过不是巩固决策(例如「与既有
|
|
5578
|
+
// 记忆重复」),混进来会让人以为巩固那一整类动作被拦。后端 lastConsolidation
|
|
5579
|
+
// 只含 auto/sleep;旧服务端没有该键时退回 lastRun,保持原行为。
|
|
5580
|
+
const last = (d && (d.lastConsolidation || d.lastRun)) || null;
|
|
5581
|
+
setConf({
|
|
5582
|
+
loading: false,
|
|
5583
|
+
error: false,
|
|
5584
|
+
pending: Number((d && d.pendingConflicts) ?? 0),
|
|
5585
|
+
skipped: Array.isArray(last && last.skippedReasons) ? last.skippedReasons : []
|
|
5586
|
+
});
|
|
5224
5587
|
})
|
|
5225
|
-
.catch(() => { if (!cancelled) setConf({ loading: false, error: true, pending: 0 }); });
|
|
5588
|
+
.catch(() => { if (!cancelled) setConf({ loading: false, error: true, pending: 0, skipped: [] }); });
|
|
5226
5589
|
};
|
|
5227
5590
|
loadConflicts();
|
|
5228
5591
|
window.addEventListener("mneme:conflicts-changed", loadConflicts);
|
|
@@ -5355,6 +5718,7 @@ window.__ModuleLoader__.load({
|
|
|
5355
5718
|
cap: t("memory.status.token.conflictsFoot")
|
|
5356
5719
|
})
|
|
5357
5720
|
),
|
|
5721
|
+
h(SkippedReasonsNotice, { t, buckets: conf.skipped, loading: conf.loading, error: conf.error }),
|
|
5358
5722
|
h("div", { className: "mneme-tokenflow" },
|
|
5359
5723
|
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.donut")),
|
|
5360
5724
|
statsError
|
|
@@ -5442,12 +5806,34 @@ window.__ModuleLoader__.load({
|
|
|
5442
5806
|
);
|
|
5443
5807
|
}
|
|
5444
5808
|
|
|
5809
|
+
// #521 评审:跳过明细只留一份实现。它原先只挂在折叠区里的 DreamStatusCards 上,
|
|
5810
|
+
// 而那个折叠区默认收起、收起时连子卡都不挂载——首屏照样看不出「一整类动作 100%
|
|
5811
|
+
// 被拦」,等于把 #521 的病换个地方犯。现在由 TokenFlowSection 的 KPI 带下方渲染,
|
|
5812
|
+
// 折叠区不再重复一份。
|
|
5813
|
+
function SkippedReasonsNotice({ t, buckets, loading, error }) {
|
|
5814
|
+
const list = Array.isArray(buckets) ? buckets : [];
|
|
5815
|
+
if (loading || error || list.length === 0) return null;
|
|
5816
|
+
const total = list.reduce((sum, b) => sum + (Number(b.count) || 0), 0);
|
|
5817
|
+
return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--mneme-warn)" } },
|
|
5818
|
+
h("div", { className: "mneme-statusnum", style: { fontSize: "16px", lineHeight: "24px" } },
|
|
5819
|
+
t("memory.status.skippedTitle").replace("{n}", String(total))),
|
|
5820
|
+
h("div", { className: "mneme-statuscap", style: { marginBottom: "6px" } }, t("memory.status.skippedHint")),
|
|
5821
|
+
h("ul", { style: { margin: 0, paddingLeft: "18px", fontSize: "12px", lineHeight: "18px" } },
|
|
5822
|
+
list.slice(0, 4).map((bucket, i) =>
|
|
5823
|
+
h("li", { key: i, style: { color: "var(--mneme-fg-dim)" } },
|
|
5824
|
+
(bucket.action ? bucket.action + " · " : "") + bucket.reason + " ×" + String(bucket.count)))),
|
|
5825
|
+
list.length > 4
|
|
5826
|
+
? h("div", { className: "mneme-statuscap", style: { marginTop: "6px" } },
|
|
5827
|
+
t("memory.status.skippedMore").replace("{n}", String(list.length - 4)))
|
|
5828
|
+
: null
|
|
5829
|
+
);
|
|
5830
|
+
}
|
|
5445
5831
|
// 巩固状态卡(×2):最近一次 autoDream 运行 + 待确认冲突计数。数据来自
|
|
5446
5832
|
// /dream-status,一次取回两张卡共用;失败只影响这两张卡自身。
|
|
5447
5833
|
// showConflicts=false 用于 2026-10 改造后的降级折叠区:待确认冲突已升入
|
|
5448
5834
|
// KPI 带,这里不再重复渲染冲突卡,只留最近运行卡。
|
|
5449
5835
|
function DreamStatusCards({ t, onGotoQueue, showConflicts = true }) {
|
|
5450
|
-
const [state, setState] = useState({ loading: true, error: false, lastRun: null, pending: 0 });
|
|
5836
|
+
const [state, setState] = useState({ loading: true, error: false, lastRun: null, lastConsolidation: null, pending: 0 });
|
|
5451
5837
|
useEffect(() => {
|
|
5452
5838
|
let cancelled = false;
|
|
5453
5839
|
const loadOnce = () => {
|
|
@@ -5459,10 +5845,11 @@ window.__ModuleLoader__.load({
|
|
|
5459
5845
|
loading: false,
|
|
5460
5846
|
error: false,
|
|
5461
5847
|
lastRun: (d && d.lastRun) || null,
|
|
5848
|
+
lastConsolidation: (d && d.lastConsolidation) || null,
|
|
5462
5849
|
pending: Number((d && d.pendingConflicts) ?? 0)
|
|
5463
5850
|
});
|
|
5464
5851
|
})
|
|
5465
|
-
.catch(() => { if (!cancelled) setState({ loading: false, error: true, lastRun: null, pending: 0 }); });
|
|
5852
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, lastRun: null, lastConsolidation: null, pending: 0 }); });
|
|
5466
5853
|
};
|
|
5467
5854
|
loadOnce();
|
|
5468
5855
|
// #295 评审:裁决完成后队列会广播 mneme:conflicts-changed,这里重取
|
|
@@ -5470,17 +5857,34 @@ window.__ModuleLoader__.load({
|
|
|
5470
5857
|
window.addEventListener("mneme:conflicts-changed", loadOnce);
|
|
5471
5858
|
return () => { cancelled = true; window.removeEventListener("mneme:conflicts-changed", loadOnce); };
|
|
5472
5859
|
}, []);
|
|
5473
|
-
|
|
5860
|
+
// #495:这张卡的标题是「最近巩固」,取数就该是巩固轮,而不是 runs[0]。
|
|
5861
|
+
// organize 与巩固共用 dream_runs,一次 agent 主动整理会写两行(dryRun 报告 +
|
|
5862
|
+
// apply 回执),把它顶到首位后这张卡就变成「整理回执」了。
|
|
5863
|
+
// 卡面数字只认巩固轮:lastConsolidation 由后端排除 organize 后取一条。旧服务端
|
|
5864
|
+
// 没有这个键时退回 lastRun,但退回的那条若是 organize 就仍然不算「最近巩固」。
|
|
5865
|
+
const organizedOnly = state.lastRun && state.lastRun.run_type === "organize" ? state.lastRun : null;
|
|
5866
|
+
const run = state.lastConsolidation
|
|
5867
|
+
|| (state.lastRun && !organizedOnly ? state.lastRun : null);
|
|
5474
5868
|
const num = run ? formatRelativeTime(run.created_at, t) : t("memory.status.dreamNever");
|
|
5869
|
+
// issue 给的两条路里选「标注」:最近一次后台运行是 organize 时,用脚注点名它,
|
|
5870
|
+
// 而不是把它从审计面抹掉。它没有挤掉巩固数(卡面仍是巩固轮),只是补一句说明
|
|
5871
|
+
// ——否则读者会以为整理就是那次巩固。
|
|
5872
|
+
const organizeFoot = organizedOnly && !run
|
|
5873
|
+
? t("memory.status.dreamOrganizeOnly").replace("{time}", formatRelativeTime(organizedOnly.created_at, t))
|
|
5874
|
+
: null;
|
|
5475
5875
|
const cap = run
|
|
5476
5876
|
? `${run.status || "—"}${run.model ? ` · ${run.model}` : ""}`
|
|
5477
|
-
|
|
5877
|
+
// 只有 organize 跑过时不能沿用 offList 那句「已关闭:巩固…」——autoDream 可能
|
|
5878
|
+
// 开着,只是还没轮到。这时只留脚注说明最近那一条是什么。
|
|
5879
|
+
: organizeFoot ? null : t("memory.settings.mode.offList");
|
|
5478
5880
|
// #178:pending>0 时冲突卡可激活(role=button + 可聚焦 + 回车/空格),
|
|
5479
5881
|
// 激活直达队列——StatusPanel 把 onGotoQueue 传进来(一步 prop,无线程)。
|
|
5480
5882
|
const pending = state.pending > 0;
|
|
5481
5883
|
const gotoQueue = () => { if (pending && onGotoQueue) onGotoQueue(); };
|
|
5884
|
+
// #521 的跳过明细不在这里渲染:它已提到 TokenFlowSection 的 KPI 带下方,
|
|
5885
|
+
// 首屏可见。这张卡在折叠区里,展开后再重复一遍只会让人以为有两批跳过。
|
|
5482
5886
|
return h(react.Fragment, null,
|
|
5483
|
-
h(StatusCard, { t, title: t("memory.status.dream"), loading: state.loading, error: state.error, num, cap }),
|
|
5887
|
+
h(StatusCard, { t, title: t("memory.status.dream"), loading: state.loading, error: state.error, num, cap, foot: organizeFoot }),
|
|
5484
5888
|
showConflicts
|
|
5485
5889
|
? h("div", {
|
|
5486
5890
|
role: pending ? "button" : undefined,
|
|
@@ -5508,15 +5912,7 @@ window.__ModuleLoader__.load({
|
|
|
5508
5912
|
// 主动提示,把「为什么没注入」从逐帧解压日志降为看一眼状态页。只在
|
|
5509
5913
|
// suppressed=true 时渲染——standard 会话与 preset 未知的宿主零打扰。
|
|
5510
5914
|
function InjectStatusCard({ t }) {
|
|
5511
|
-
const
|
|
5512
|
-
useEffect(() => {
|
|
5513
|
-
let cancelled = false;
|
|
5514
|
-
apiFetch("/api/dsh-mneme/inject-status")
|
|
5515
|
-
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
5516
|
-
.then((j) => { if (!cancelled) setState({ loading: false, suppressed: !!(j && j.suppressed) }); })
|
|
5517
|
-
.catch(() => { if (!cancelled) setState({ loading: false, suppressed: false }); });
|
|
5518
|
-
return () => { cancelled = true; };
|
|
5519
|
-
}, []);
|
|
5915
|
+
const state = useInjectStatus();
|
|
5520
5916
|
if (state.loading || !state.suppressed) return null;
|
|
5521
5917
|
return h("div", { className: "mneme-statuscard", style: { borderColor: "var(--mneme-warn)" } },
|
|
5522
5918
|
h("div", { className: "mneme-statusnum", style: { fontSize: "16px", lineHeight: "24px" } }, t("memory.status.injectSuppressed")),
|
|
@@ -6085,7 +6481,7 @@ window.__ModuleLoader__.load({
|
|
|
6085
6481
|
decidedAt ?? null
|
|
6086
6482
|
].filter(Boolean).join(" · ");
|
|
6087
6483
|
|
|
6088
|
-
function MemoryDrawer({ t, memory, conflict, deleting, deleteError, onClose, onDelete, onSaved }) {
|
|
6484
|
+
function MemoryDrawer({ t, memory, conflict, deleting, deleteError, onClose, onDelete, onSaved, onUndone }) {
|
|
6089
6485
|
const [editing, setEditing] = useState(false);
|
|
6090
6486
|
const [title, setTitle] = useState(memory.title || "");
|
|
6091
6487
|
const [content, setContent] = useState(memory.content || "");
|
|
@@ -6102,6 +6498,14 @@ window.__ModuleLoader__.load({
|
|
|
6102
6498
|
const [saveError, setSaveError] = useState(false);
|
|
6103
6499
|
const [copied, setCopied] = useState(false);
|
|
6104
6500
|
const [entities, setEntities] = useState(null);
|
|
6501
|
+
// 整包退出的两段:先干跑清点(点按钮那一发),再确认物删。撤是**物理删除**不是归档
|
|
6502
|
+
// (归档行仍占 share_external_id 的 UNIQUE 位,撤了会导不回来,理由在
|
|
6503
|
+
// src/share-import.js 的 deleteSharePack),所以这里比删除那条路多一站清点。
|
|
6504
|
+
const [undoPreview, setUndoPreview] = useState(null);
|
|
6505
|
+
const [undoBusy, setUndoBusy] = useState(false);
|
|
6506
|
+
const [undoError, setUndoError] = useState(false);
|
|
6507
|
+
// 确认那一发却回干跑时唯一出声的一格(见 undoPack 的注释)。
|
|
6508
|
+
const [undoStillDry, setUndoStillDry] = useState(false);
|
|
6105
6509
|
|
|
6106
6510
|
useEffect(() => {
|
|
6107
6511
|
let cancelled = false;
|
|
@@ -6112,6 +6516,21 @@ window.__ModuleLoader__.load({
|
|
|
6112
6516
|
return () => { cancelled = true; };
|
|
6113
6517
|
}, [memory.id]);
|
|
6114
6518
|
|
|
6519
|
+
// #512:归因单独取一次。读失败/未开启时保持 null——「没读到」不等于
|
|
6520
|
+
// 「没有归因」,界面上不能把这两件事显示成同一句话。
|
|
6521
|
+
useEffect(() => {
|
|
6522
|
+
let cancelled = false;
|
|
6523
|
+
setProvenance(null);
|
|
6524
|
+
apiFetch("/api/dsh-mneme/memory/provenance?id=" + encodeURIComponent(memory.id))
|
|
6525
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
6526
|
+
.then((d) => {
|
|
6527
|
+
if (cancelled) return;
|
|
6528
|
+
if (d && Array.isArray(d.windows) && Array.isArray(d.mints)) setProvenance(d);
|
|
6529
|
+
})
|
|
6530
|
+
.catch(() => { if (!cancelled) setProvenance(null); });
|
|
6531
|
+
return () => { cancelled = true; };
|
|
6532
|
+
}, [memory.id]);
|
|
6533
|
+
|
|
6115
6534
|
const postUpdate = (patch, opts) => {
|
|
6116
6535
|
setBusy(true);
|
|
6117
6536
|
setSaveError(false);
|
|
@@ -6132,6 +6551,34 @@ window.__ModuleLoader__.load({
|
|
|
6132
6551
|
.finally(() => setBusy(false));
|
|
6133
6552
|
};
|
|
6134
6553
|
|
|
6554
|
+
const undoPack = (confirm) => {
|
|
6555
|
+
setUndoBusy(true);
|
|
6556
|
+
setUndoError(false);
|
|
6557
|
+
setUndoStillDry(false);
|
|
6558
|
+
apiFetch("/api/dsh-mneme/receive/undo", {
|
|
6559
|
+
method: "POST",
|
|
6560
|
+
headers: { "Content-Type": "application/json" },
|
|
6561
|
+
// 不带 confirm 就是干跑:第一发只清点,第二发才删。packId 只从行上取
|
|
6562
|
+
// (面板行带这个字段是因为「收起这个包」按 pack_id 整批删),面板不自己拼也不猜。
|
|
6563
|
+
body: JSON.stringify(confirm
|
|
6564
|
+
? { packId: memory.share_pack_id, confirm: true }
|
|
6565
|
+
: { packId: memory.share_pack_id })
|
|
6566
|
+
})
|
|
6567
|
+
.then(async (res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
6568
|
+
.then((d) => {
|
|
6569
|
+
// 只认答复的 dry_run,**不按「我点的是哪一颗」推断**:确认那一发如果回 dry_run:true
|
|
6570
|
+
// (服务端与面板版本不一致的形态),悄悄把预览重画一遍就等于「按了删除却没删也不说」
|
|
6571
|
+
// ——接收弹层那一侧修过同一个形状,这里不许退回去。
|
|
6572
|
+
if (d.dry_run === false) onUndone(d.deleted ?? 0);
|
|
6573
|
+
else {
|
|
6574
|
+
setUndoPreview(d);
|
|
6575
|
+
if (confirm) setUndoStillDry(true);
|
|
6576
|
+
}
|
|
6577
|
+
})
|
|
6578
|
+
.catch(() => setUndoError(true))
|
|
6579
|
+
.finally(() => setUndoBusy(false));
|
|
6580
|
+
};
|
|
6581
|
+
|
|
6135
6582
|
const save = () => {
|
|
6136
6583
|
const t2 = title.trim();
|
|
6137
6584
|
if (!t2) return;
|
|
@@ -6246,6 +6693,11 @@ window.__ModuleLoader__.load({
|
|
|
6246
6693
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.updated")),
|
|
6247
6694
|
h("span", { className: "mneme-dmetaval", title: formatDate(memory.updated_at) },
|
|
6248
6695
|
`${formatRelativeTime(memory.updated_at, t)}(${formatDateShort(memory.updated_at)})`),
|
|
6696
|
+
// 包号给人核对:与 `dsh-mneme undo-share <包文件>` 用的是同一个 pack_id,
|
|
6697
|
+
// 两条路撤的是同一批行。
|
|
6698
|
+
memory.share_pack_id && h(react.Fragment, null,
|
|
6699
|
+
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.foreignPack")),
|
|
6700
|
+
h("span", { className: "mneme-dmetaval", title: memory.share_pack_id }, memory.share_pack_id)),
|
|
6249
6701
|
Array.isArray(memory.tags) && memory.tags.length > 0 && h(react.Fragment, null,
|
|
6250
6702
|
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.tags")),
|
|
6251
6703
|
h("span", { className: "mneme-dmetaval" }, memory.tags.join(" · ")))
|
|
@@ -6259,6 +6711,25 @@ window.__ModuleLoader__.load({
|
|
|
6259
6711
|
h("span", { className: "mneme-xdot", style: { color: typeColor(e.type) }, "aria-hidden": "true" }),
|
|
6260
6712
|
e.name
|
|
6261
6713
|
)))),
|
|
6714
|
+
provenance !== null && h(react.Fragment, null,
|
|
6715
|
+
h("div", { className: "mneme-xcolhead", style: { padding: "16px 0 0" } }, t("memory.explorer.detail.provenance")),
|
|
6716
|
+
(provenance.windows.length === 0 && provenance.mints.length === 0)
|
|
6717
|
+
? h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.detail.provenanceEmpty"))
|
|
6718
|
+
: h("div", { className: "mneme-dmeta" },
|
|
6719
|
+
provenance.mints.map((m, i) => h(react.Fragment, { key: "mint-" + m.run_id + "-" + i },
|
|
6720
|
+
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.provenanceMint")),
|
|
6721
|
+
h("span", { className: "mneme-dmetaval", title: m.run_id },
|
|
6722
|
+
[m.run_type, m.trigger_source, formatDateShort(m.created_at)].filter(Boolean).join(" · ")))),
|
|
6723
|
+
provenance.windows.map((w, i) => h(react.Fragment, { key: "win-" + w.session_id + "-" + w.seq_start + "-" + i },
|
|
6724
|
+
h("span", { className: "mneme-dmetakey" }, t("memory.explorer.detail.provenanceWindow")),
|
|
6725
|
+
h("span", { className: "mneme-dmetaval", title: w.session_id },
|
|
6726
|
+
[w.session_id + " · seq " + w.seq_start + "–" + w.seq_end,
|
|
6727
|
+
w.distill_run_id
|
|
6728
|
+
? [w.run_type, w.run_status].filter(Boolean).join(" / ")
|
|
6729
|
+
: t("memory.explorer.detail.provenanceNoRun"),
|
|
6730
|
+
t("memory.explorer.detail.provenanceRecords").replace("{n}", String(w.records ? w.records.length : 0))
|
|
6731
|
+
].join(" · ")))))
|
|
6732
|
+
),
|
|
6262
6733
|
editing && h("div", { className: "mneme-xcolhead", style: { padding: "16px 0 4px" } }, t("memory.explorer.detail.editContent")),
|
|
6263
6734
|
editing
|
|
6264
6735
|
? h("textarea", { className: "mneme-dcontent-input", value: content, onChange: (e) => setContent(e.target.value) })
|
|
@@ -6321,7 +6792,41 @@ window.__ModuleLoader__.load({
|
|
|
6321
6792
|
: h("button", { type: "button", className: "mneme-btn mneme-btndanger", onClick: () => setConfirmDelete(true) },
|
|
6322
6793
|
t("memory.explorer.delete")),
|
|
6323
6794
|
deleteError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } },
|
|
6324
|
-
t("memory.explorer.deleteFailed"))
|
|
6795
|
+
t("memory.explorer.deleteFailed")),
|
|
6796
|
+
memory.share_pack_id && h(react.Fragment, null,
|
|
6797
|
+
undoPreview
|
|
6798
|
+
? (undoPreview.deleted ?? 0) === 0
|
|
6799
|
+
// 「这个包已经不在本库里了」是她真正需要的那一句,而不是
|
|
6800
|
+
// 「将删除 0 条 + 一颗确认删除 0 条的键」——后者真实但没用。
|
|
6801
|
+
? h("span", { style: { fontSize: 12, color: "var(--mneme-warn)" } },
|
|
6802
|
+
t("memory.explorer.undoPackGone"))
|
|
6803
|
+
: h(react.Fragment, null,
|
|
6804
|
+
h("span", { style: { fontSize: 12, color: "var(--mneme-warn)" } },
|
|
6805
|
+
t("memory.explorer.undoPackPreview").replace("{n}", String(undoPreview.deleted ?? 0))),
|
|
6806
|
+
// sample 是**清点时**的前 20 条({type,title},序为 updated_at DESC),
|
|
6807
|
+
// deleted 是这一笔真带走几行。两个数并发下可以差一条,谁也不是对方的
|
|
6808
|
+
// 依据(src/share-import.js 的 deleteSharePack 那段),所以这里只把
|
|
6809
|
+
// sample 当「我要收起的到底是哪一批」的核对清单,不参与条数。
|
|
6810
|
+
// 清单形状走 TypeRowList:接收弹层的撞名清单与它是同一个形状,
|
|
6811
|
+
// 第三份手写 .map 就是第三处会各自漂移的地方。
|
|
6812
|
+
TypeRowList({ t, rows: undoPreview.sample }),
|
|
6813
|
+
h("button", { type: "button", className: "mneme-btn mneme-btndangerconfirm", disabled: undoBusy, onClick: () => undoPack(true) },
|
|
6814
|
+
t("memory.explorer.undoPackConfirm").replace("{n}", String(undoPreview.deleted ?? 0))),
|
|
6815
|
+
// 取消回到「一颗干净的收起键」:清点结果与那句「仍是干跑」是同一轮的
|
|
6816
|
+
// 两样东西,只清一样会让旧判词挂在空按钮旁边。
|
|
6817
|
+
h("button", { type: "button", className: "mneme-btn", disabled: undoBusy, onClick: () => { setUndoPreview(null); setUndoStillDry(false); } },
|
|
6818
|
+
t("memory.explorer.cancel"))
|
|
6819
|
+
)
|
|
6820
|
+
: h("button", { type: "button", className: "mneme-btn mneme-btndanger", disabled: undoBusy, onClick: () => undoPack(false) },
|
|
6821
|
+
t("memory.explorer.undoPack"))),
|
|
6822
|
+
// 撤销同样是往返一秒量级的枪(清点那一发与接收侧的干跑走的是同一档耗时):
|
|
6823
|
+
// 只把键按灰而不说在忙,看起来就像没反应。这一句说的是**收起**,不是导入——
|
|
6824
|
+
// 物删在飞的时候写成「导入中」是把两件相反的事挂在同一行字上。
|
|
6825
|
+
undoBusy && h("span", { style: { fontSize: 12 } }, t("memory.explorer.undoPackBusy")),
|
|
6826
|
+
undoStillDry && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } },
|
|
6827
|
+
t("memory.explorer.undoPackStillDry")),
|
|
6828
|
+
undoError && h("span", { style: { fontSize: 12, color: "var(--mneme-danger)" } },
|
|
6829
|
+
t("memory.explorer.undoPackFailed"))
|
|
6325
6830
|
)
|
|
6326
6831
|
)
|
|
6327
6832
|
);
|
|
@@ -6385,6 +6890,338 @@ window.__ModuleLoader__.load({
|
|
|
6385
6890
|
);
|
|
6386
6891
|
}
|
|
6387
6892
|
|
|
6893
|
+
// --- 分享包弹层 ---
|
|
6894
|
+
// 这一面的支点只有一句:**看见 report 之前不给下载**。服务端一次 POST 就把包和报告
|
|
6895
|
+
// 一起交回,所以这一格拦不住靠协议、只能靠界面自己:CLI 那侧没人拦得住,面板至少要让
|
|
6896
|
+
// 「随手把整库外发」停一步。八个类型下拉、六个跳过原因都只许是 src/share.js 的投影,
|
|
6897
|
+
// 一条判据都不在这里复述(#411 口径)。
|
|
6898
|
+
const SHARE_TYPE_OPTIONS = ["preference", "project", "decision", "history", "pitfall", "constraint", "rejected_solution", "pattern"];
|
|
6899
|
+
// 顺序即 share.js 的判序(classifyRowForShare 从上往下命中);报告按这个顺序排,
|
|
6900
|
+
// 跨文件锁在 test/client-share.test.js 里同时锁内容与顺序。
|
|
6901
|
+
const SHARE_SKIP_REASONS = ["archived", "forgotten", "readonly_type", "excluded_type", "blank_or_noise", "sensitive"];
|
|
6902
|
+
|
|
6903
|
+
/**
|
|
6904
|
+
* 服务端 Content-Disposition 里的文件名。拼法只住在 src/share-http.js 的
|
|
6905
|
+
* shareFilename(日期 + pack_id 前 8 位),面板复制一份就是第二个口径:旧服务端没带
|
|
6906
|
+
* 这一格时宁可短(pack_id 兜底),也不许在这里再拼一次日期。
|
|
6907
|
+
*/
|
|
6908
|
+
const dispositionName = (value) => {
|
|
6909
|
+
const m = /filename="?([^";]+)"?/.exec(value || "");
|
|
6910
|
+
return m ? m[1] : "";
|
|
6911
|
+
};
|
|
6912
|
+
|
|
6913
|
+
// 出包报告。两笔账必须同屏:进包条数与「够格」条数是两个数(包内同键折叠),
|
|
6914
|
+
// 跳过计数是全量而跳过清单截到 50——只报清单会让人以为跳过的就清单里那些。
|
|
6915
|
+
function ShareReport({ t, report, listOpen, setListOpen }) {
|
|
6916
|
+
const skipped = report.skipped || {};
|
|
6917
|
+
const list = Array.isArray(report.skip_list) ? report.skip_list : [];
|
|
6918
|
+
const totalSkipped = SHARE_SKIP_REASONS.reduce((n, k) => n + (skipped[k] || 0), 0);
|
|
6919
|
+
return h("div", { className: "mneme-sharebody" },
|
|
6920
|
+
h("div", { className: "mneme-saved" }, t("memory.explorer.shareIncluded").replace("{n}", String(report.included ?? 0))),
|
|
6921
|
+
(report.deduped || 0) > 0 && h("div", { className: "mneme-featsubhint" },
|
|
6922
|
+
t("memory.explorer.shareDeduped")
|
|
6923
|
+
.replace("{n}", String(report.deduped))
|
|
6924
|
+
.replace("{m}", String(report.included + report.deduped))),
|
|
6925
|
+
totalSkipped > 0 && h("div", { className: "mneme-featsubhint" },
|
|
6926
|
+
t("memory.explorer.shareSkipped").replace("{n}", String(totalSkipped))),
|
|
6927
|
+
totalSkipped > 0 && h("div", { className: "mneme-sharebreak" },
|
|
6928
|
+
SHARE_SKIP_REASONS.filter((k) => (skipped[k] || 0) > 0).map((k) =>
|
|
6929
|
+
h("span", { key: k, className: "mneme-sharebreakitem" },
|
|
6930
|
+
`${t(`memory.explorer.share.skip.${k}`)} ${skipped[k]}`))),
|
|
6931
|
+
list.length > 0 && h("div", null,
|
|
6932
|
+
h("button", {
|
|
6933
|
+
type: "button",
|
|
6934
|
+
className: "mneme-footbtn",
|
|
6935
|
+
"aria-expanded": String(listOpen),
|
|
6936
|
+
onClick: () => setListOpen(!listOpen)
|
|
6937
|
+
}, t("memory.explorer.shareSkipList").replace("{n}", String(list.length))),
|
|
6938
|
+
listOpen && h("div", { className: "mneme-sharelist" },
|
|
6939
|
+
list.map((row, i) => h("div", { key: `${i}-${row.reason}`, className: "mneme-sharow" },
|
|
6940
|
+
// 命中密钥/PII 的那几条服务端就不给标题(空串),这里不许拿正文或摘要兜底:
|
|
6941
|
+
// 摘要是递给对面的离线爆破 oracle(src/share.js 的 skipTitle 那段)。
|
|
6942
|
+
h("span", { className: "mneme-sharowtitle" }, row.title || t("memory.explorer.shareNoTitle")),
|
|
6943
|
+
h("span", { className: "mneme-sharowreason" }, t(`memory.explorer.share.skip.${row.reason}`)))),
|
|
6944
|
+
// 截断的判据用「清单比账短」而不是硬写 50:服务端的水位改了,这里跟着走。
|
|
6945
|
+
totalSkipped > list.length && h("div", { className: "mneme-sharow" },
|
|
6946
|
+
h("span", { className: "mneme-sharowreason" },
|
|
6947
|
+
t("memory.explorer.shareSkipListCapped").replace("{n}", String(list.length))))))
|
|
6948
|
+
);
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
function ShareDialog({ t, onClose }) {
|
|
6952
|
+
const [alias, setAlias] = useState("");
|
|
6953
|
+
const [type, setType] = useState(""); // "" = 整库口径:这个键压根不发
|
|
6954
|
+
const [busy, setBusy] = useState(false);
|
|
6955
|
+
const [pack, setPack] = useState(null);
|
|
6956
|
+
const [fileName, setFileName] = useState("");
|
|
6957
|
+
const [tooLarge, setTooLarge] = useState(null);
|
|
6958
|
+
const [err, setErr] = useState("");
|
|
6959
|
+
const [listOpen, setListOpen] = useState(false);
|
|
6960
|
+
|
|
6961
|
+
const build = () => {
|
|
6962
|
+
setBusy(true);
|
|
6963
|
+
setErr("");
|
|
6964
|
+
setPack(null);
|
|
6965
|
+
setTooLarge(null);
|
|
6966
|
+
setFileName("");
|
|
6967
|
+
setListOpen(false);
|
|
6968
|
+
const body = {};
|
|
6969
|
+
if (alias.trim()) body.alias = alias.trim();
|
|
6970
|
+
if (type) body.type = type;
|
|
6971
|
+
apiFetch("/api/dsh-mneme/share", {
|
|
6972
|
+
method: "POST",
|
|
6973
|
+
headers: { "Content-Type": "application/json" },
|
|
6974
|
+
body: JSON.stringify(body)
|
|
6975
|
+
})
|
|
6976
|
+
.then(async (res) => {
|
|
6977
|
+
const d = await res.json().catch(() => null);
|
|
6978
|
+
// 413 的答复不是包:{error, bytes, limitBytes, hint, report}。报告要给,
|
|
6979
|
+
// 包体一个字节都不许在内存里留着——服务端正是因为它太大才拒的。
|
|
6980
|
+
if (res.status === 413) { setTooLarge(d || {}); setBusy(false); return; }
|
|
6981
|
+
if (!res.ok) {
|
|
6982
|
+
// 与接收弹层同一份形状:服务端只回一个码(empty-pack / invalid-alias / …),
|
|
6983
|
+
// detail 有就带上——那是「哪一格不合格」的唯一读数,面板不自己编第二套文案。
|
|
6984
|
+
// 不这么做的话,日后 shareReply 新增的码在这里全是「生成失败」六个字。
|
|
6985
|
+
setErr(`${t("memory.explorer.shareFailed")}(${(d && d.error) || res.status})`
|
|
6986
|
+
+ (d && d.detail ? `:${d.detail}` : ""));
|
|
6987
|
+
setBusy(false);
|
|
6988
|
+
return;
|
|
6989
|
+
}
|
|
6990
|
+
// 200 但读不出报告**不是成功**:这一支不给码(没有码可给),只说失败。
|
|
6991
|
+
if (!d || !d.report) { setErr(t("memory.explorer.shareFailed")); setBusy(false); return; }
|
|
6992
|
+
setPack(d);
|
|
6993
|
+
setFileName(dispositionName(res.headers.get("content-disposition")));
|
|
6994
|
+
setBusy(false);
|
|
6995
|
+
})
|
|
6996
|
+
.catch(() => { setErr(t("memory.explorer.shareFailed")); setBusy(false); });
|
|
6997
|
+
};
|
|
6998
|
+
|
|
6999
|
+
const rep = pack && pack.report;
|
|
7000
|
+
// included 为 0 时不给下载:那种包对面吃的是 empty-pack(400),给一个必败的文件
|
|
7001
|
+
// 不如在这里说清「没有够格外发的条目」。
|
|
7002
|
+
const canDownload = !!rep && rep.included > 0;
|
|
7003
|
+
|
|
7004
|
+
const download = () => {
|
|
7005
|
+
// 面板写的字节必须与 `src/share.js` 量水位的、`bin/cli.mjs` 落盘的**同一份形状**
|
|
7006
|
+
// (pretty-print 只属于 `/export` 那个备份面:人读的档,不是要重投的包)。
|
|
7007
|
+
const blob = new Blob([JSON.stringify(pack)], { type: "application/json" });
|
|
7008
|
+
const url = URL.createObjectURL(blob);
|
|
7009
|
+
const a = document.createElement("a");
|
|
7010
|
+
a.href = url;
|
|
7011
|
+
a.download = fileName || `dsh-mneme-share-${pack.pack_id}.json`;
|
|
7012
|
+
document.body.appendChild(a);
|
|
7013
|
+
a.click();
|
|
7014
|
+
a.remove();
|
|
7015
|
+
URL.revokeObjectURL(url);
|
|
7016
|
+
};
|
|
7017
|
+
|
|
7018
|
+
return h("div", { className: "mneme-impdialog mneme-sharedialog", role: "region", "aria-label": t("memory.explorer.shareTitle") },
|
|
7019
|
+
h("div", { className: "mneme-set-title" }, t("memory.explorer.shareTitle")),
|
|
7020
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.shareHint")),
|
|
7021
|
+
h("div", { className: "mneme-improw" },
|
|
7022
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.shareAlias")),
|
|
7023
|
+
// 尺子与服务端同一把:importedTagFor 把整串标签(含 from-)截到 32,留给别名的
|
|
7024
|
+
// 就是 27。不拦的话她打的第 28 个字起在**对面**被静默吃掉,而这条标签是外来行
|
|
7025
|
+
// 唯一的来源读数。数字由 test/client-share.test.js 跨文件钉住。
|
|
7026
|
+
h("input", {
|
|
7027
|
+
type: "text",
|
|
7028
|
+
className: "mneme-select",
|
|
7029
|
+
value: alias,
|
|
7030
|
+
maxLength: 27,
|
|
7031
|
+
placeholder: t("memory.explorer.shareAliasBlank"),
|
|
7032
|
+
onChange: (e) => setAlias(e.target.value)
|
|
7033
|
+
})
|
|
7034
|
+
),
|
|
7035
|
+
h("div", { className: "mneme-improw" },
|
|
7036
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.shareType")),
|
|
7037
|
+
h("select", { className: "mneme-select", value: type, onChange: (e) => setType(e.target.value) },
|
|
7038
|
+
h("option", { value: "" }, t("memory.tab.all")),
|
|
7039
|
+
SHARE_TYPE_OPTIONS.map((k) => h("option", { key: k, value: k }, typeLabel(t, k))))
|
|
7040
|
+
),
|
|
7041
|
+
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
7042
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose },
|
|
7043
|
+
t("memory.explorer.importCancel")),
|
|
7044
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: build },
|
|
7045
|
+
busy ? t("memory.explorer.shareBuilding") : t("memory.explorer.shareBuild"))
|
|
7046
|
+
),
|
|
7047
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
7048
|
+
tooLarge && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-warn)", marginTop: 8 } },
|
|
7049
|
+
// 提示语服务端给(SHARE_ERROR_HINT 只许有一份),面板只补自己那句字节数。
|
|
7050
|
+
// 它写着 --type/--tag 的 CLI 口吻,这一格已知:docs/SHARE.md 的『面板』那一节记着。
|
|
7051
|
+
`${t("memory.explorer.shareTooLarge")
|
|
7052
|
+
.replace("{bytes}", String(tooLarge.bytes ?? "?"))
|
|
7053
|
+
.replace("{limit}", String(tooLarge.limitBytes ?? "?"))} ${tooLarge.hint || ""}`),
|
|
7054
|
+
tooLarge && tooLarge.report && h(ShareReport, { t, report: tooLarge.report, listOpen, setListOpen }),
|
|
7055
|
+
rep && h(ShareReport, { t, report: rep, listOpen, setListOpen }),
|
|
7056
|
+
canDownload && h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
7057
|
+
h("button", { type: "button", className: "mneme-btn", onClick: download },
|
|
7058
|
+
t("memory.explorer.shareDownload"))),
|
|
7059
|
+
// 没有可下载的东西就一定给一句原因:`included` 为 0 与「这答复没有条数」是两种失败,
|
|
7060
|
+
// 但对她的界面是同一种——数字后面没有键。canDownload 是唯一的门,这里就只认它。
|
|
7061
|
+
rep && !canDownload && h("div", { className: "mneme-featsubhint", style: { marginTop: 8 } },
|
|
7062
|
+
t("memory.explorer.shareEmpty")),
|
|
7063
|
+
);
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
// 干跑与真导入**共用这一份读数**:两者的键完全相同,区别只有 dry_run 那一格。
|
|
7067
|
+
// 拆成两个组件就会各写一套文案,而「将新建」与「已新建」的唯一真凭是 dry_run。
|
|
7068
|
+
function ReceiveSummary({ t, d }) {
|
|
7069
|
+
const conflicts = Array.isArray(d.title_conflicts) ? d.title_conflicts : [];
|
|
7070
|
+
const alias = d.alias || t("memory.explorer.receiveAnonymous");
|
|
7071
|
+
return h("div", { className: "mneme-sharebody" },
|
|
7072
|
+
h("div", { className: "mneme-featsubhint" },
|
|
7073
|
+
`${t("memory.explorer.receiveFrom").replace("{alias}", alias)} · ${d.pack_id || ""}`),
|
|
7074
|
+
// dry_run 缺格(旧服务端)时按「没写」处理:宁可少报一次写入,不可虚报。
|
|
7075
|
+
h("div", { className: "mneme-saved" },
|
|
7076
|
+
t(d.dry_run === false ? "memory.explorer.receiveDidCreate" : "memory.explorer.receiveWouldCreate")
|
|
7077
|
+
.replace("{n}", String(d.created ?? 0))),
|
|
7078
|
+
(d.skipped_existing || 0) > 0 && h("div", { className: "mneme-featsubhint" },
|
|
7079
|
+
t("memory.explorer.receiveExisting").replace("{n}", String(d.skipped_existing))),
|
|
7080
|
+
(d.unchanged || 0) > 0 && h("div", { className: "mneme-featsubhint" },
|
|
7081
|
+
t("memory.explorer.receiveUnchanged").replace("{n}", String(d.unchanged))),
|
|
7082
|
+
// 撞名是 advisory 读数:只对「本轮真正会新建的那些」判过,重吃同一个包时恒空
|
|
7083
|
+
//(src/share-import.js:181-186)。所以这里**只有非空才出声**,绝不写「没有冲突」;
|
|
7084
|
+
// 屏上没有这一行不等于没有撞名,等于「本轮没有新的要报」。
|
|
7085
|
+
conflicts.length > 0 && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-warn)" } },
|
|
7086
|
+
t("memory.explorer.receiveConflicts").replace("{n}", String(conflicts.length))),
|
|
7087
|
+
TypeRowList({ t, rows: conflicts }),
|
|
7088
|
+
// `conflicts_checked` 是服务端给的**包内条数**,不是「比过了几条」(同一处注释写明:
|
|
7089
|
+
// 第二次吃同一个包时 title_conflicts 恒空,而这一格仍是全量)。所以文案只说
|
|
7090
|
+
// 「包内 N 条」:它是上一行为什么可能沉默的尺,不许读成「比对了 N 条都没撞」。
|
|
7091
|
+
h("div", { className: "mneme-featsubhint" },
|
|
7092
|
+
t("memory.explorer.receivePackCount").replace("{n}", String(d.conflicts_checked ?? 0)))
|
|
7093
|
+
);
|
|
7094
|
+
}
|
|
7095
|
+
|
|
7096
|
+
function ReceiveDialog({ t, onClose, onImported }) {
|
|
7097
|
+
const [file, setFile] = useState(null);
|
|
7098
|
+
const [pack, setPack] = useState(null);
|
|
7099
|
+
const [preview, setPreview] = useState(null);
|
|
7100
|
+
const [result, setResult] = useState(null);
|
|
7101
|
+
const [busy, setBusy] = useState(false);
|
|
7102
|
+
const [err, setErr] = useState("");
|
|
7103
|
+
const fileRef = useRef(null);
|
|
7104
|
+
|
|
7105
|
+
// 一发 POST 两种用法(干跑 / 确认)都从这走,失败文案只有一份。返回 null 表示
|
|
7106
|
+
// 「已经有一句话摆上屏了」——所以每一条不回 null 的路径都必须自己先 setErr,
|
|
7107
|
+
// 否则调用方读到 null 就是什么都不做,现场变成「按了没反应」。
|
|
7108
|
+
const post = (body) => apiFetch("/api/dsh-mneme/receive", {
|
|
7109
|
+
method: "POST",
|
|
7110
|
+
headers: { "Content-Type": "application/json" },
|
|
7111
|
+
body: JSON.stringify(body)
|
|
7112
|
+
})
|
|
7113
|
+
.then(async (res) => {
|
|
7114
|
+
const d = await res.json().catch(() => null);
|
|
7115
|
+
setBusy(false);
|
|
7116
|
+
if (!res.ok) {
|
|
7117
|
+
// 闸门只回一个码(invalid-kind / external-id-mismatch / …),detail 有就带上:
|
|
7118
|
+
// 那是「哪一格不合格」的唯一读数,面板不自己编第二套文案。
|
|
7119
|
+
setErr(`${t("memory.explorer.receiveFailed")}(${(d && d.error) || res.status})`
|
|
7120
|
+
+ (d && d.detail ? `:${d.detail}` : ""));
|
|
7121
|
+
return null;
|
|
7122
|
+
}
|
|
7123
|
+
// 200 但正文读不出(前置的 HTML 拦截页、空 body 的代理、被截断的响应)
|
|
7124
|
+
// **不是成功**:这里曾经直接把 null 交回调用方,于是确认那一发悄悄没走完,
|
|
7125
|
+
// 而屏上上一块的「将新建 N 条」还留着——看起来就像她没点确认。
|
|
7126
|
+
if (!d || typeof d !== "object") {
|
|
7127
|
+
setErr(t("memory.explorer.receiveFailed"));
|
|
7128
|
+
return null;
|
|
7129
|
+
}
|
|
7130
|
+
return d;
|
|
7131
|
+
})
|
|
7132
|
+
.catch(() => { setErr(t("memory.explorer.receiveFailed")); setBusy(false); return null; });
|
|
7133
|
+
|
|
7134
|
+
const pick = (f) => {
|
|
7135
|
+
setFile(f || null);
|
|
7136
|
+
setErr("");
|
|
7137
|
+
setPreview(null);
|
|
7138
|
+
setResult(null);
|
|
7139
|
+
setPack(null);
|
|
7140
|
+
if (!f) return;
|
|
7141
|
+
f.text().then((text) => {
|
|
7142
|
+
// 坏 JSON 在本地就拦。「这个文件根本不是 JSON」与「这个包不合格」是两件事,
|
|
7143
|
+
// 后者由服务端的码说(invalid-kind / invalid-entry / …)。
|
|
7144
|
+
// 顶层不是对象的也一样在这里说清:`null`、`0`、`[]` 会让预览键永远灰着且不解释,
|
|
7145
|
+
// 那种「按不动也没话说」的界面比一句错话更坏。
|
|
7146
|
+
let parsed;
|
|
7147
|
+
try {
|
|
7148
|
+
parsed = JSON.parse(text);
|
|
7149
|
+
} catch {
|
|
7150
|
+
setErr(t("memory.explorer.receiveNotJson"));
|
|
7151
|
+
return;
|
|
7152
|
+
}
|
|
7153
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7154
|
+
setErr(t("memory.explorer.receiveNotPack"));
|
|
7155
|
+
return;
|
|
7156
|
+
}
|
|
7157
|
+
setPack(parsed);
|
|
7158
|
+
}).catch(() => setErr(t("memory.explorer.receiveFailed")));
|
|
7159
|
+
};
|
|
7160
|
+
|
|
7161
|
+
const runPreview = () => {
|
|
7162
|
+
setBusy(true);
|
|
7163
|
+
setErr("");
|
|
7164
|
+
setResult(null);
|
|
7165
|
+
// 不带 confirm = 干跑(服务端缺省即预览,spec §5 沿用 reclaim 的脾气)。
|
|
7166
|
+
post({ pack }).then((d) => { if (d) setPreview(d); });
|
|
7167
|
+
};
|
|
7168
|
+
|
|
7169
|
+
const runApply = () => {
|
|
7170
|
+
setBusy(true);
|
|
7171
|
+
setErr("");
|
|
7172
|
+
post({ pack, confirm: true }).then((d) => {
|
|
7173
|
+
if (!d) return;
|
|
7174
|
+
setResult(d);
|
|
7175
|
+
// 写完才刷新列表;答复说没写就不刷(dry_run 那一格是唯一凭据)。
|
|
7176
|
+
// 同一支里顺手撤掉预览:写成功之后「将新建 40 条」与「已新建 40 条」是同屏的两种时态,
|
|
7177
|
+
// 上面那一句此时不是信息。回干跑时**保留**预览——那一发确实没写,屏上还该留着清点。
|
|
7178
|
+
if (d.dry_run === false) { setPreview(null); onImported(); }
|
|
7179
|
+
});
|
|
7180
|
+
};
|
|
7181
|
+
|
|
7182
|
+
return h("div", { className: "mneme-impdialog mneme-sharedialog", role: "region", "aria-label": t("memory.explorer.receiveTitle") },
|
|
7183
|
+
h("div", { className: "mneme-set-title" }, t("memory.explorer.receiveTitle")),
|
|
7184
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.receiveHint")),
|
|
7185
|
+
h("div", { className: "mneme-improw" },
|
|
7186
|
+
// 忙的时候不许再选一个文件:预览那一发还在飞,选第二个会清掉 pack/preview,
|
|
7187
|
+
// 而飞回来的那一块数字说的是**前一个**包——确认键亮着、写的却是后一个文件。
|
|
7188
|
+
// 这条窗口按实测是 0.5–3 秒量级,不是理论风险。
|
|
7189
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: () => fileRef.current?.click() },
|
|
7190
|
+
file ? file.name : t("memory.explorer.receivePick")),
|
|
7191
|
+
h("input", {
|
|
7192
|
+
ref: fileRef,
|
|
7193
|
+
type: "file",
|
|
7194
|
+
accept: ".json,application/json",
|
|
7195
|
+
style: { display: "none" },
|
|
7196
|
+
// 选完就清空控件的值:否则她改完同一个文件再选一次,浏览器不发 change,
|
|
7197
|
+
// 面板会拿着上一版的包继续预览——同一颗键,两个事实。
|
|
7198
|
+
onChange: (e) => { pick((e.target.files && e.target.files[0]) || null); e.target.value = ""; }
|
|
7199
|
+
})
|
|
7200
|
+
),
|
|
7201
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
7202
|
+
// 入库是逐条先查 + 逐条 INSERT + 排队嵌入:实测 494 µs/条,单批一千多条要跑一秒开外,
|
|
7203
|
+
// 整库量级近 3 秒。busy 必须挂在预览与确认两发上,不然按钮看起来像没反应。
|
|
7204
|
+
busy && h("div", { className: "mneme-featsubhint", style: { marginTop: 8 } }, t("memory.explorer.importing")),
|
|
7205
|
+
preview && h(ReceiveSummary, { t, d: preview }),
|
|
7206
|
+
result && h(ReceiveSummary, { t, d: result }),
|
|
7207
|
+
// 按了确认却回个「仍是干跑」(服务端只认 confirm 的真值 true,缺省、字符串
|
|
7208
|
+
// "true"、别的旋钮一律不写)——这一句是唯一出声的地方:沉默会让人以为已经写进去了。
|
|
7209
|
+
// 它挂在 result 那一支而不是 ReceiveSummary 里:预览本来就该是干跑,那里再说一遍
|
|
7210
|
+
// 只是噪声。
|
|
7211
|
+
result && result.dry_run !== false && h("div", {
|
|
7212
|
+
className: "mneme-featsubhint", style: { color: "var(--mneme-warn)" }
|
|
7213
|
+
}, t("memory.explorer.receiveStillDry")),
|
|
7214
|
+
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
7215
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose },
|
|
7216
|
+
t("memory.explorer.importCancel")),
|
|
7217
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy || !pack, onClick: runPreview },
|
|
7218
|
+
t("memory.explorer.receivePreview")),
|
|
7219
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy || !preview, onClick: runApply },
|
|
7220
|
+
t("memory.explorer.receiveConfirm"))
|
|
7221
|
+
)
|
|
7222
|
+
);
|
|
7223
|
+
}
|
|
7224
|
+
|
|
6388
7225
|
// #386 第 5 条:面板手动新增一条记忆。字段与 memory_save 工具对齐
|
|
6389
7226
|
//(type/title/content/importance/tags),但**不放 scope/sensitivity**:归属的
|
|
6390
7227
|
// 显式声明走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖
|
|
@@ -6527,11 +7364,14 @@ window.__ModuleLoader__.load({
|
|
|
6527
7364
|
// 方案 A:沉淀/归档筛选 chip——状态页「查看全部」也会带着它们跳转过来。
|
|
6528
7365
|
const [depositedOnly, setDepositedOnly] = useState(false); // 只看 autoDream 巩固
|
|
6529
7366
|
const [archivedOnly, setArchivedOnly] = useState(false); // 只看已归档
|
|
7367
|
+
const [foreignOnly, setForeignOnly] = useState(false); // 只看外来行(source=imported)
|
|
6530
7368
|
const [heatSort, setHeatSort] = useState(false); // 卡片页内热度降序(阶段二补口)
|
|
6531
7369
|
const [pendingIds, setPendingIds] = useState(() => new Set());
|
|
6532
7370
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
6533
7371
|
const [importOpen, setImportOpen] = useState(false);
|
|
6534
7372
|
const [createOpen, setCreateOpen] = useState(false);
|
|
7373
|
+
const [shareOpen, setShareOpen] = useState(false);
|
|
7374
|
+
const [receiveOpen, setReceiveOpen] = useState(false);
|
|
6535
7375
|
const itemRefs = useRef(new Map());
|
|
6536
7376
|
const moreRef = useRef(null);
|
|
6537
7377
|
|
|
@@ -6569,7 +7409,8 @@ window.__ModuleLoader__.load({
|
|
|
6569
7409
|
+ (minImp ? `&minImportance=${minImp}` : "")
|
|
6570
7410
|
+ (dateFromIso ? `&updatedFrom=${encodeURIComponent(dateFromIso)}` : "")
|
|
6571
7411
|
+ (depositedOnly ? "&deposited=only" : "")
|
|
6572
|
-
+ (archivedOnly ? "&archived=only" : "")
|
|
7412
|
+
+ (archivedOnly ? "&archived=only" : "")
|
|
7413
|
+
+ (foreignOnly ? "&source=imported" : "");
|
|
6573
7414
|
const listUrl = (offset) => `/api/dsh-mneme/list?limit=${PAGE_SIZE}&offset=${offset}${filterQS}&order=chrono`;
|
|
6574
7415
|
|
|
6575
7416
|
// Browse = paged, pure-chronological pages (order=chrono). The default
|
|
@@ -6881,7 +7722,7 @@ window.__ModuleLoader__.load({
|
|
|
6881
7722
|
"aria-label": t("memory.explorer.more"),
|
|
6882
7723
|
"aria-expanded": String(menuOpen),
|
|
6883
7724
|
title: t("memory.explorer.more"),
|
|
6884
|
-
onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); setCreateOpen(false); }
|
|
7725
|
+
onClick: () => { setMenuOpen(!menuOpen); setImportOpen(false); setCreateOpen(false); setShareOpen(false); setReceiveOpen(false); }
|
|
6885
7726
|
}, "⋯"),
|
|
6886
7727
|
menuOpen && h("div", { className: "mneme-menu", role: "menu" },
|
|
6887
7728
|
h("button", { type: "button", role: "menuitem", onClick: () => { setCreateOpen(true); setMenuOpen(false); } },
|
|
@@ -6891,13 +7732,23 @@ window.__ModuleLoader__.load({
|
|
|
6891
7732
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("markdown"); } },
|
|
6892
7733
|
t("memory.explorer.exportMarkdown")),
|
|
6893
7734
|
h("button", { type: "button", role: "menuitem", onClick: () => { setImportOpen(true); setMenuOpen(false); } },
|
|
6894
|
-
t("memory.explorer.importMd"))
|
|
7735
|
+
t("memory.explorer.importMd")),
|
|
7736
|
+
h("button", { type: "button", role: "menuitem", onClick: () => { setShareOpen(true); setMenuOpen(false); } },
|
|
7737
|
+
t("memory.explorer.sharePack")),
|
|
7738
|
+
h("button", { type: "button", role: "menuitem", onClick: () => { setReceiveOpen(true); setMenuOpen(false); } },
|
|
7739
|
+
t("memory.explorer.receivePack")),
|
|
6895
7740
|
),
|
|
6896
7741
|
importOpen && h(ImportDialog, {
|
|
6897
7742
|
t,
|
|
6898
7743
|
onClose: () => setImportOpen(false),
|
|
6899
7744
|
onImported: () => setReloadKey((k) => k + 1)
|
|
6900
7745
|
}),
|
|
7746
|
+
shareOpen && h(ShareDialog, { t, onClose: () => setShareOpen(false) }),
|
|
7747
|
+
receiveOpen && h(ReceiveDialog, {
|
|
7748
|
+
t,
|
|
7749
|
+
onClose: () => setReceiveOpen(false),
|
|
7750
|
+
onImported: () => setReloadKey((k) => k + 1)
|
|
7751
|
+
}),
|
|
6901
7752
|
createOpen && h(CreateDialog, {
|
|
6902
7753
|
t,
|
|
6903
7754
|
onClose: () => setCreateOpen(false),
|
|
@@ -6973,6 +7824,25 @@ window.__ModuleLoader__.load({
|
|
|
6973
7824
|
title: t("memory.explorer.filterArchivedHint"),
|
|
6974
7825
|
onClick: () => setArchivedOnly(!archivedOnly)
|
|
6975
7826
|
}, t("memory.explorer.filterArchived")),
|
|
7827
|
+
h("button", {
|
|
7828
|
+
className: foreignOnly ? "mneme-chip mneme-active" : "mneme-chip",
|
|
7829
|
+
// 搜索态不许**开启**这一档:/search 的 DTO 读不出行来源(`source` 被
|
|
7830
|
+
// searchMemories 盖成召回渠道,见 src/service.js:939),按一个假信号筛比筛不动
|
|
7831
|
+
// 更坏。但已经开着的时候永远许关掉——把它整个 disabled 会把她锁在一个解不开
|
|
7832
|
+
// 的状态里,只能先去清搜索词。
|
|
7833
|
+
disabled: !!query.trim() && !foreignOnly,
|
|
7834
|
+
title: query.trim()
|
|
7835
|
+
? t("memory.explorer.filterForeignSearchHint")
|
|
7836
|
+
: t("memory.explorer.filterForeignHint"),
|
|
7837
|
+
"aria-pressed": String(foreignOnly),
|
|
7838
|
+
onClick: () => setForeignOnly(!foreignOnly)
|
|
7839
|
+
}, t("memory.explorer.filterForeign")),
|
|
7840
|
+
// 「为什么这一档在搜索时不按」不能只挂在 title 上:disabled 的按钮不发 focus,
|
|
7841
|
+
// 触屏与键盘用户拿不到提示,而她看到的是一个**亮着**的筛选压在一堆没筛过的
|
|
7842
|
+
// 结果上。所以开着外来档又在搜索时,屏上多一行看得见的说明。
|
|
7843
|
+
foreignOnly && !!query.trim() && h("div", {
|
|
7844
|
+
className: "mneme-featsubhint", style: { marginTop: 4 }
|
|
7845
|
+
}, t("memory.explorer.filterForeignSearchHint")),
|
|
6976
7846
|
heatAvailable && h("button", {
|
|
6977
7847
|
className: heatSort ? "mneme-chip mneme-active" : "mneme-chip",
|
|
6978
7848
|
title: t("memory.explorer.heatSortHint"),
|
|
@@ -7030,6 +7900,7 @@ window.__ModuleLoader__.load({
|
|
|
7030
7900
|
h(ImportanceStars, { value: m.importance || 0, size: 12 }),
|
|
7031
7901
|
h(HeatBadge, { value: m.heat }),
|
|
7032
7902
|
h(ScopeBadge, { m, t }),
|
|
7903
|
+
h(ForeignBadge, { m, t }),
|
|
7033
7904
|
m.source && h("span", { className: "mneme-cardsrc", title: m.source }, m.source)
|
|
7034
7905
|
)
|
|
7035
7906
|
)),
|
|
@@ -7092,7 +7963,8 @@ window.__ModuleLoader__.load({
|
|
|
7092
7963
|
h("span", { className: "mneme-xdot", style: { color: memoryTypeColor(m.type) }, title: typeLabel(t, m.type), "aria-hidden": "true" }),
|
|
7093
7964
|
h("span", { className: "mneme-xtime" }, time),
|
|
7094
7965
|
h("span", { className: "mneme-xname" }, m.title || m.content?.slice(0, 40)),
|
|
7095
|
-
h(ScopeBadge, { m, t })
|
|
7966
|
+
h(ScopeBadge, { m, t }),
|
|
7967
|
+
h(ForeignBadge, { m, t })
|
|
7096
7968
|
);
|
|
7097
7969
|
})
|
|
7098
7970
|
))
|
|
@@ -7122,7 +7994,15 @@ window.__ModuleLoader__.load({
|
|
|
7122
7994
|
deleteError,
|
|
7123
7995
|
onClose: () => setSelectedId(null),
|
|
7124
7996
|
onDelete: deleteById,
|
|
7125
|
-
onSaved: applyMemoryUpdate
|
|
7997
|
+
onSaved: applyMemoryUpdate,
|
|
7998
|
+
onUndone: (n) => {
|
|
7999
|
+
// 撤掉的是整包:选中那一行已经不存在,关抽屉 + 刷新列表 + 一句 toast
|
|
8000
|
+
// (toast 形状照详情抽屉里 restored 那一条的先例,不带行号免得漂)。
|
|
8001
|
+
setSelectedId(null);
|
|
8002
|
+
setReloadKey((k) => k + 1);
|
|
8003
|
+
setToastMsg(t("memory.explorer.undoPackDone").replace("{n}", String(n)));
|
|
8004
|
+
setTimeout(() => setToastMsg(""), 2500);
|
|
8005
|
+
}
|
|
7126
8006
|
})
|
|
7127
8007
|
),
|
|
7128
8008
|
view === "entity" && h(EntityPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
|