@modusensus/dsh-mneme 0.10.1 → 0.11.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 +10 -3
- package/bin/dsh-mneme-choro.mjs +254 -0
- package/docs/CONFIGURATION.md +8 -1
- package/lib/api-standalone.js +17 -21
- package/lib/api.js +247 -11
- package/lib/bootstrap-http.js +105 -0
- package/lib/bootstrap.js +5 -1
- package/lib/choro-command.js +104 -0
- package/lib/client.js +1028 -102
- package/lib/config-keys.js +144 -0
- package/lib/config.js +28 -0
- package/lib/index.js +50 -4
- package/lib/inject.js +15 -3
- package/lib/lang.js +54 -0
- package/lib/quality-filter.js +48 -12
- package/lib/service.js +3 -0
- package/lib/settings.js +8 -1
- package/lib/store.js +502 -0
- package/lib/tools.js +127 -1
- package/lib/wiki/inject.js +164 -0
- package/lib/wiki/partition.js +200 -0
- package/lib/wiki/plan.js +214 -0
- package/lib/wiki/queries.js +220 -0
- package/lib/wiki/render.js +187 -0
- package/lib/wiki/scan.js +371 -0
- package/lib/wiki/t0.js +463 -0
- package/lib/wiki/t1.js +496 -0
- package/lib/write-admission.js +5 -3
- package/package.json +4 -2
- package/src/api-standalone.js +17 -21
- package/src/api.js +247 -11
- package/src/bootstrap-http.js +105 -0
- package/src/bootstrap.js +5 -1
- package/src/choro-command.js +104 -0
- package/src/config-keys.js +144 -0
- package/src/config.js +28 -0
- package/src/index.js +50 -4
- package/src/inject.js +15 -3
- package/src/lang.js +54 -0
- package/src/quality-filter.js +48 -12
- package/src/service.js +3 -0
- package/src/settings.js +8 -1
- package/src/store.js +502 -0
- package/src/tools.js +127 -1
- package/src/wiki/inject.js +164 -0
- package/src/wiki/partition.js +200 -0
- package/src/wiki/plan.js +214 -0
- package/src/wiki/queries.js +220 -0
- package/src/wiki/render.js +187 -0
- package/src/wiki/scan.js +371 -0
- package/src/wiki/t0.js +463 -0
- package/src/wiki/t1.js +496 -0
- package/src/write-admission.js +5 -3
- package/test/api-routes-gaps.test.js +131 -1
- package/test/api.test.js +7 -1
- package/test/bootstrap.test.js +8 -0
- package/test/choro-command.test.js +218 -0
- package/test/client.test.js +655 -19
- package/test/config-keys-routes.test.js +233 -0
- package/test/contrast.test.js +14 -7
- package/test/inject.test.js +51 -0
- package/test/llm-audit.test.js +29 -1
- package/test/quality-backfill.test.js +290 -0
- package/test/quality-filter.test.js +113 -4
- package/test/service.test.js +1 -1
- package/test/wiki-api.test.js +153 -0
- package/test/wiki-inject.test.js +265 -0
- package/test/wiki-partition.test.js +112 -0
- package/test/wiki-plan.test.js +80 -0
- package/test/wiki-queries.test.js +187 -0
- package/test/wiki-scan.test.js +208 -0
- package/test/wiki-store.test.js +190 -0
- package/test/wiki-t0.test.js +380 -0
- package/test/wiki-t1.test.js +235 -0
- package/test/wiki-tools.test.js +121 -0
package/lib/client.js
CHANGED
|
@@ -301,6 +301,17 @@ window.__ModuleLoader__.load({
|
|
|
301
301
|
zh: {
|
|
302
302
|
"memory.panel.empty": "暂无记忆条目",
|
|
303
303
|
"memory.panel.open": "记忆",
|
|
304
|
+
"wiki.panel.open": "仓库地志",
|
|
305
|
+
"wiki.empty.noRepo": "还没有登记的仓库。先在仓库目录里跑 `dsh-mneme-choro <仓库路径> --apply` 生成骨架页并登记,然后回到这里。",
|
|
306
|
+
"wiki.empty.disabled": "chorography(仓库地志)未启用:在「设置」里打开「仓库地志」后重启 DSH 生效。",
|
|
307
|
+
"wiki.empty.pickRepo": "已登记多个仓库,选一个:",
|
|
308
|
+
"wiki.search.placeholder": "在页面里检索…",
|
|
309
|
+
"wiki.state.loading": "加载中…",
|
|
310
|
+
"wiki.state.pickPage": "从左侧选一个模块页。",
|
|
311
|
+
"wiki.state.pageMissing": "索引还在、磁盘页面缺失:在仓库目录重跑一次 `dsh-mneme-choro <仓库路径> --apply` 即可重建。",
|
|
312
|
+
"wiki.state.unavailable": "chorography 查询装配不可用(宿主环境缺件)。",
|
|
313
|
+
"wiki.state.error": "加载失败",
|
|
314
|
+
"wiki.tree.title": "模块",
|
|
304
315
|
"memory.tab.all": "全部",
|
|
305
316
|
"memory.tab.preference": "偏好",
|
|
306
317
|
"memory.tab.project": "项目",
|
|
@@ -322,16 +333,21 @@ window.__ModuleLoader__.load({
|
|
|
322
333
|
"memory.settings.profileSaved": "画像已保存",
|
|
323
334
|
"memory.settings.profileExtract": "从记忆提取草稿",
|
|
324
335
|
"memory.settings.profileExtracting": "正在提取…",
|
|
325
|
-
"memory.settings.profileExtractHint": "
|
|
336
|
+
"memory.settings.profileExtractHint": "会花一次模型调用,只在你点这个按钮时发生;提取出来的是草稿副本,只在这里展示——不会写进记忆库,也不会动你自己手写的「用户画像」。想留下它,点「复制草稿」。",
|
|
326
337
|
"memory.settings.profileExtractDisabled": "没有提取:功能开关里的「画像提取草稿」还没打开。",
|
|
327
338
|
"memory.settings.profileExtractEmpty": "库里没有可用的记忆条目,这次没有能提炼的内容(空输入下硬写一段画像只会是编的)。",
|
|
328
339
|
"memory.settings.profileExtractNoRoute": "没有可用的模型:先在宿主里选一个默认模型(或在配置里填 dreamProvider / dreamModel),再点一次。",
|
|
329
340
|
"memory.settings.profileSaveFailed": "画像没存进去",
|
|
330
341
|
"memory.settings.profileExtractFailed": "提取没成:模型没按格式返回,或这次调用失败了。你现有的画像一个字都没被改动。",
|
|
331
|
-
"memory.settings.profileDraftTitle": "
|
|
342
|
+
"memory.settings.profileDraftTitle": "草稿副本(不会自动保存)",
|
|
332
343
|
"memory.settings.profileDraftRows": "依据记忆条数",
|
|
344
|
+
"memory.status.entitiesSampled": "分类计数只统计最近 500 个实体;总数是全库",
|
|
345
|
+
"memory.status.entitiesUnknown": "总数读不到;下面是最近 500 个实体的分类计数",
|
|
333
346
|
"memory.settings.profileDraftTruncated": "条目较多,这次只读了前面一部分",
|
|
334
|
-
"memory.settings.
|
|
347
|
+
"memory.settings.profileDraftCopy": "复制草稿",
|
|
348
|
+
"memory.settings.profileDraftCopied": "已复制",
|
|
349
|
+
"memory.settings.profileDraftCopyHint": "「用户画像」那一栏只由你自己填写;要把它写进去,请粘贴后自己点保存。",
|
|
350
|
+
"memory.settings.profileDraftCopyFailed": "复制没成功:请选中上面的草稿手动复制。",
|
|
335
351
|
"memory.settings.profileDraftDiscard": "丢弃草稿",
|
|
336
352
|
"memory.settings.persona": "人格定义(beta)",
|
|
337
353
|
"memory.settings.personaHint": "给这位 Agent 一张角色卡:名字、性格、回复要求。开关打开且下方「人格定义」总闸开启时,每轮对话都会注入。",
|
|
@@ -368,7 +384,14 @@ window.__ModuleLoader__.load({
|
|
|
368
384
|
"memory.features.personaStrictAddressing": "严格人格寻址",
|
|
369
385
|
"memory.features.personaStrictAddressing.hint": "开:会话身份没有对应人格卡、或那张卡被关掉时,就不注入人格——默认卡只兜「解析不到会话身份」的会话。宿主里跑着自带身份的 bot(DS Bot 那类)时关掉这种兜底,免得两句「你是谁」撞在同一轮。默认关 = 未命中就落默认卡(历史行为)。",
|
|
370
386
|
"memory.features.profileExtractEnabled": "画像提取草稿",
|
|
371
|
-
"memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 →
|
|
387
|
+
"memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 → Agent 眼中的你」卡上的「从记忆提取草稿」按钮:读一批活跃记忆、花一次模型调用,产一段「助手眼中的用户」草稿。草稿只在那张卡上展示副本,可一键复制,不会写进记忆库、也不会覆盖你自己填的「用户画像」。轻量档预设关(用户显式勾选仍然赢)。",
|
|
388
|
+
"memory.features.chorographyEnabled": "仓库地志(chorography)",
|
|
389
|
+
"memory.features.chorographyEnabled.hint": "读一遍仓库写成一份带页码的志书:确定性骨架零模型、可以随时重建;语义段由 CLI 的 headless 会话逐模块补。打开后有四个入口——模型侧三个只读工具、面板「仓库地志」浏览页、每轮的开局提纲注入、/choro 命令。关着时它们全部不存在(模型工具列表里根本没有,注入块不渲染,/choro 不注册)。默认关。",
|
|
390
|
+
"memory.features.chorographyInject": "开局注入档位",
|
|
391
|
+
"memory.features.chorographyInject.hint": "每轮往上下文里塞地志提纲的份量:light 只列文件数最多的 6 个模块(约 8 行)、full 列全部模块、off 不注入(工具与面板页仍可用)。只注模块名 / 文件数 / 页状态 / 页相对路径,不含正文;正文要模型自己用 choro_read_page 取。轻量档预设压成 off。",
|
|
392
|
+
"memory.features.chorographyInject.off": "off(不注入)",
|
|
393
|
+
"memory.features.chorographyInject.light": "light(前 6 个模块)",
|
|
394
|
+
"memory.features.chorographyInject.full": "full(全部模块)",
|
|
372
395
|
"memory.settings.rules": "规则",
|
|
373
396
|
"memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
|
|
374
397
|
"memory.settings.ruleAdd": "添加规则",
|
|
@@ -390,6 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
390
413
|
"memory.explorer.tabSettings": "设置",
|
|
391
414
|
"memory.explorer.tabStatus": "状态",
|
|
392
415
|
"memory.entities.count": "{n} 个实体",
|
|
416
|
+
"memory.entities.countSampled": "列表只列最近 {shown} 个;上方总数是全库",
|
|
393
417
|
"memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
|
|
394
418
|
// 搜索框有字但一条没筛出来时用它;与 entities.none(库里真没实体)分开,
|
|
395
419
|
// 否则「暂无实体」会把打错一个字误导成「你还没开抽取」。
|
|
@@ -535,6 +559,15 @@ window.__ModuleLoader__.load({
|
|
|
535
559
|
"memory.settings.mcp.snippet": "{ \"mcpServers\": { \"dsh-mneme\": { \"command\": \"dsh-mneme-mcp\", \"env\": { \"DSH_MNEME_TOKEN\": \"<外部访问 API 的 token>\" } } } }",
|
|
536
560
|
"memory.settings.mcp.copy": "复制",
|
|
537
561
|
"memory.settings.mcp.copied": "已复制",
|
|
562
|
+
"memory.settings.configKeys.title": "配置键覆盖面",
|
|
563
|
+
"memory.settings.configKeys.desc": "配置文件里的键,面板能改多少、哪些只能手改。只列键名,不显示任何值。",
|
|
564
|
+
"memory.settings.configKeys.summary": "schema 里共 {schema} 个键:面板能改 {panel} 个,{file} 个只能改配置文件,另有 {undefaulted} 个登记了却没有默认值。",
|
|
565
|
+
"memory.settings.configKeys.fileOnlyHead": "只能改配置文件({n} 个)",
|
|
566
|
+
"memory.settings.configKeys.undefaultedHead": "面板登记了、但运行时没有默认值({n} 个)",
|
|
567
|
+
"memory.settings.configKeys.undefaultedHint": "这些键在白名单里,所以面板上有输入框;但 schema 没给默认值,留空时「当前生效值」里就没有它。填一个值才会生效。",
|
|
568
|
+
"memory.settings.configKeys.show": "展开清单",
|
|
569
|
+
"memory.settings.configKeys.hide": "收起清单",
|
|
570
|
+
"memory.settings.configKeys.loadFailed": "没能取到这份清单(接口暂时不可用)——这不等于「没有只能改文件的键」。",
|
|
538
571
|
"memory.settings.mcp.hint": "数据面走「外部访问 API」(默认 127.0.0.1:8790,Bearer),token 就是上面那张;可用 DSH_MNEME_URL / DSH_MNEME_TOKEN 环境变量或 ~/.dsh-mneme/cli.json 覆盖。command 需在 PATH 上(随 @modusensus/dsh-mneme 安装自带)。",
|
|
539
572
|
"memory.settings.extapi.savedHint": "已保存,重启 DSH 后生效",
|
|
540
573
|
"memory.settings.extapi.invalidPort": "端口需为 1-65535 的数字",
|
|
@@ -814,6 +847,19 @@ window.__ModuleLoader__.load({
|
|
|
814
847
|
"memory.explorer.imported": "已合并 {n} 条编辑",
|
|
815
848
|
"memory.explorer.importFailed": "导入失败",
|
|
816
849
|
"memory.explorer.importNoFile": "请先选择文件",
|
|
850
|
+
"memory.explorer.bootstrap": "从目录引导…",
|
|
851
|
+
"memory.explorer.bootstrapTitle": "从目录引导",
|
|
852
|
+
"memory.explorer.bootstrapHint": "读这个目录里的确定性产物(脚本命令、README、贡献规范、CI 工作流、顶层结构、最近提交),一条规则一条记忆。不调用模型,重复执行只会合并更新、不会重复入库。",
|
|
853
|
+
"memory.explorer.bootstrapDir": "目录路径",
|
|
854
|
+
"memory.explorer.bootstrapConfirm": "开始引导",
|
|
855
|
+
"memory.explorer.bootstraping": "引导中…",
|
|
856
|
+
"memory.explorer.bootstrapDone": "新建 {n} 条、并入 {m} 条",
|
|
857
|
+
"memory.explorer.bootstrapNoDir": "请先填写目录路径",
|
|
858
|
+
"memory.explorer.bootstrapFailed": "引导失败",
|
|
859
|
+
"memory.explorer.bootstrapErr.missing-dir": "没给目录路径",
|
|
860
|
+
"memory.explorer.bootstrapErr.dir-not-found": "这个路径不存在",
|
|
861
|
+
"memory.explorer.bootstrapErr.not-a-directory": "这个路径不是目录",
|
|
862
|
+
"memory.explorer.bootstrapErr.bootstrap-unavailable": "当前装配下这条能力没接线",
|
|
817
863
|
"memory.explorer.sharePack": "分享记忆包…",
|
|
818
864
|
"memory.explorer.shareTitle": "分享记忆包",
|
|
819
865
|
"memory.explorer.shareHint": "把本机活跃记忆打成一个可外发的包:密钥/PII 整条不发,本机路径抹成 ~,归档与遗忘的不进。",
|
|
@@ -1096,6 +1142,13 @@ window.__ModuleLoader__.load({
|
|
|
1096
1142
|
"memory.status.token.heat": "8 周热力 · 每天花了多少",
|
|
1097
1143
|
"memory.status.token.heatLess": "少",
|
|
1098
1144
|
"memory.status.token.heatMore": "多",
|
|
1145
|
+
"memory.status.token.sink": "8 周沉淀 · 进来的 vs 稳住的",
|
|
1146
|
+
"memory.status.token.sinkAdded": "新增",
|
|
1147
|
+
"memory.status.token.sinkMerged": "巩固",
|
|
1148
|
+
"memory.status.token.sinkArchived": "归档",
|
|
1149
|
+
"memory.status.token.sinkGap": "巩固/新增",
|
|
1150
|
+
"memory.status.token.sinkEmpty": "最近还没有记录",
|
|
1151
|
+
"memory.status.token.sinkFoot": "巩固按去重记录数计,同一条被多轮合并只算一次",
|
|
1099
1152
|
"memory.status.token.last7": "近 7 天",
|
|
1100
1153
|
"memory.status.token.meterOnly": "(只计量)",
|
|
1101
1154
|
"memory.status.token.detail": "详细状态卡(原「库内一览 / 后台运转」9 张,点击展开)",
|
|
@@ -1110,6 +1163,17 @@ window.__ModuleLoader__.load({
|
|
|
1110
1163
|
en: {
|
|
1111
1164
|
"memory.panel.empty": "No memories yet",
|
|
1112
1165
|
"memory.panel.open": "Memory",
|
|
1166
|
+
"wiki.panel.open": "Repo Atlas",
|
|
1167
|
+
"wiki.empty.noRepo": "No repository registered yet. Run `dsh-mneme-choro <repo-path> --apply` inside the repository to generate skeleton pages and register it, then come back here.",
|
|
1168
|
+
"wiki.empty.disabled": "chorography (Repo Atlas) is disabled: turn it on under Settings and restart DSH to apply.",
|
|
1169
|
+
"wiki.empty.pickRepo": "Multiple repositories registered — pick one:",
|
|
1170
|
+
"wiki.search.placeholder": "Search pages…",
|
|
1171
|
+
"wiki.state.loading": "Loading…",
|
|
1172
|
+
"wiki.state.pickPage": "Pick a module page from the tree.",
|
|
1173
|
+
"wiki.state.pageMissing": "Index exists but the page file is missing: re-run `dsh-mneme-choro <repo-path> --apply` to rebuild.",
|
|
1174
|
+
"wiki.state.unavailable": "chorography queries are unavailable (host wiring missing).",
|
|
1175
|
+
"wiki.state.error": "Failed to load",
|
|
1176
|
+
"wiki.tree.title": "Modules",
|
|
1113
1177
|
"memory.tab.all": "All",
|
|
1114
1178
|
"memory.tab.preference": "Preferences",
|
|
1115
1179
|
"memory.tab.project": "Projects",
|
|
@@ -1131,16 +1195,21 @@ window.__ModuleLoader__.load({
|
|
|
1131
1195
|
"memory.settings.profileSaved": "Profile saved",
|
|
1132
1196
|
"memory.settings.profileExtract": "Draft from memory",
|
|
1133
1197
|
"memory.settings.profileExtracting": "Extracting…",
|
|
1134
|
-
"memory.settings.profileExtractHint": "Costs one model call, and only when you press this button. The result is a draft
|
|
1198
|
+
"memory.settings.profileExtractHint": "Costs one model call, and only when you press this button. The result is a draft copy shown here only — it is never written to the memory store and never touches the User Profile you wrote yourself. Press “Copy draft” to keep it.",
|
|
1135
1199
|
"memory.settings.profileExtractDisabled": "Nothing extracted: the “Profile draft from memory” feature switch is still off.",
|
|
1136
1200
|
"memory.settings.profileExtractEmpty": "No usable memory entries in the store, so there was nothing to draft from (writing a profile out of an empty input would only be invention).",
|
|
1137
1201
|
"memory.settings.profileExtractNoRoute": "No model available: pick a default model in the host (or set dreamProvider / dreamModel), then try again.",
|
|
1138
1202
|
"memory.settings.profileSaveFailed": "Profile was not saved",
|
|
1139
1203
|
"memory.settings.profileExtractFailed": "Extraction failed: the model did not return the required shape, or the call errored. Your existing profile is untouched.",
|
|
1140
|
-
"memory.settings.profileDraftTitle": "Draft (
|
|
1204
|
+
"memory.settings.profileDraftTitle": "Draft copy (never saved automatically)",
|
|
1141
1205
|
"memory.settings.profileDraftRows": "Based on entries",
|
|
1206
|
+
"memory.status.entitiesSampled": "Per-type counts cover the newest 500 entities only; the total is the whole library",
|
|
1207
|
+
"memory.status.entitiesUnknown": "Total unavailable; the counts below cover the newest 500 entities",
|
|
1142
1208
|
"memory.settings.profileDraftTruncated": "many entries — only the first batch was read",
|
|
1143
|
-
"memory.settings.
|
|
1209
|
+
"memory.settings.profileDraftCopy": "Copy draft",
|
|
1210
|
+
"memory.settings.profileDraftCopied": "Copied",
|
|
1211
|
+
"memory.settings.profileDraftCopyHint": "The User Profile field is yours to write; to keep this text, paste it there and save it yourself.",
|
|
1212
|
+
"memory.settings.profileDraftCopyFailed": "Copy failed: select the draft above and copy it manually.",
|
|
1144
1213
|
"memory.settings.profileDraftDiscard": "Discard draft",
|
|
1145
1214
|
"memory.settings.persona": "Persona (beta)",
|
|
1146
1215
|
"memory.settings.personaHint": "Give this agent a character card: name, personality, reply style. Injected every turn when the toggle below and the \"Persona\" master switch are both on.",
|
|
@@ -1177,7 +1246,14 @@ window.__ModuleLoader__.load({
|
|
|
1177
1246
|
"memory.features.personaStrictAddressing": "Strict persona addressing",
|
|
1178
1247
|
"memory.features.personaStrictAddressing.hint": "On: when a session's identity has no persona card — or that card is switched off — no persona is injected, so the default card only covers sessions with no resolvable identity. Turn this on when the host runs a bot that brings its own identity (DS Bot and friends), otherwise two \"you are ...\" claims land in the same turn. Off (default): unresolved identities fall back to the default card, as before.",
|
|
1179
1248
|
"memory.features.profileExtractEnabled": "Profile draft from memory",
|
|
1180
|
-
"memory.features.profileExtractEnabled.hint": "Adds the \"Draft from memory\" button to Settings → Personalization
|
|
1249
|
+
"memory.features.profileExtractEnabled.hint": "Adds the \"Draft from memory\" button to the \"You in the agent's eyes\" card under Settings → Personalization: reads a batch of active memories, spends one model call, and returns an \"as the assistant sees them\" draft. The draft is only shown there as a copy you can copy out — it is never written to the memory store and never overwrites the User Profile you wrote yourself. Off in the lightweight preset (an explicit choice still wins).",
|
|
1250
|
+
"memory.features.chorographyEnabled": "Repo atlas (chorography)",
|
|
1251
|
+
"memory.features.chorographyEnabled.hint": "Reads a repository once and writes it up as a paginated atlas: the deterministic skeleton costs no model and can be rebuilt any time; the semantic sections are added per module by a headless session in the CLI. Turning it on enables four entry points — three read-only tools for the model, the atlas panel page, the per-turn outline injection, and the /choro command. With it off none of them exist (the tools are absent from the model's tool list, the injection block is not rendered, /choro is not registered). Off by default.",
|
|
1252
|
+
"memory.features.chorographyInject": "Opening injection tier",
|
|
1253
|
+
"memory.features.chorographyInject.hint": "How much of the atlas outline goes into context every turn: light lists the six modules with the most files (about 8 lines), full lists every module, off injects nothing (tools and the panel page still work). Only module name / file count / page status / page path are injected, never page bodies — the model fetches those with choro_read_page. The lightweight preset forces off.",
|
|
1254
|
+
"memory.features.chorographyInject.off": "off (no injection)",
|
|
1255
|
+
"memory.features.chorographyInject.light": "light (top 6 modules)",
|
|
1256
|
+
"memory.features.chorographyInject.full": "full (all modules)",
|
|
1181
1257
|
"memory.settings.rules": "Rules",
|
|
1182
1258
|
"memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
|
|
1183
1259
|
"memory.settings.ruleAdd": "Add Rule",
|
|
@@ -1199,6 +1275,7 @@ window.__ModuleLoader__.load({
|
|
|
1199
1275
|
"memory.explorer.tabSettings": "Settings",
|
|
1200
1276
|
"memory.explorer.tabStatus": "Status",
|
|
1201
1277
|
"memory.entities.count": "{n} entities",
|
|
1278
|
+
"memory.entities.countSampled": "The list shows the newest {shown} only; the total above is the whole library",
|
|
1202
1279
|
"memory.entities.pick": "Select an entity to see its attributes and relations",
|
|
1203
1280
|
"memory.entities.noMatch": "No entities match that search — try another word",
|
|
1204
1281
|
"memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
|
|
@@ -1338,6 +1415,15 @@ window.__ModuleLoader__.load({
|
|
|
1338
1415
|
"memory.settings.mcp.snippet": "{ \"mcpServers\": { \"dsh-mneme\": { \"command\": \"dsh-mneme-mcp\", \"env\": { \"DSH_MNEME_TOKEN\": \"<token from External API above>\" } } } }",
|
|
1339
1416
|
"memory.settings.mcp.copy": "Copy",
|
|
1340
1417
|
"memory.settings.mcp.copied": "Copied",
|
|
1418
|
+
"memory.settings.configKeys.title": "Config key coverage",
|
|
1419
|
+
"memory.settings.configKeys.desc": "How many config-file keys the panel can change, and which ones are file-only. Key names only — no values are shown.",
|
|
1420
|
+
"memory.settings.configKeys.summary": "The schema has {schema} keys: the panel can change {panel}, {file} are file-only, and {undefaulted} are registered without a runtime default.",
|
|
1421
|
+
"memory.settings.configKeys.fileOnlyHead": "File-only ({n})",
|
|
1422
|
+
"memory.settings.configKeys.undefaultedHead": "Registered for the panel but has no runtime default ({n})",
|
|
1423
|
+
"memory.settings.configKeys.undefaultedHint": "These keys are on the whitelist, so the panel shows an input for them, but the schema gives no default — while unset they are absent from the effective snapshot. Set a value for them to take effect.",
|
|
1424
|
+
"memory.settings.configKeys.show": "Show the list",
|
|
1425
|
+
"memory.settings.configKeys.hide": "Hide the list",
|
|
1426
|
+
"memory.settings.configKeys.loadFailed": "Couldn't load this list (endpoint unavailable) — that is not the same as \"no file-only keys\".",
|
|
1341
1427
|
"memory.settings.mcp.hint": "Data plane is the external API (default 127.0.0.1:8790, Bearer) — the token is the one above; override with DSH_MNEME_URL / DSH_MNEME_TOKEN env vars or ~/.dsh-mneme/cli.json. The command must be on PATH (installed with @modusensus/dsh-mneme).",
|
|
1342
1428
|
"memory.settings.extapi.savedHint": "Saved. Takes effect after restarting DSH",
|
|
1343
1429
|
"memory.settings.extapi.invalidPort": "Port must be a number between 1 and 65535",
|
|
@@ -1600,6 +1686,19 @@ window.__ModuleLoader__.load({
|
|
|
1600
1686
|
"memory.explorer.imported": "Merged {n} edits",
|
|
1601
1687
|
"memory.explorer.importFailed": "Import failed",
|
|
1602
1688
|
"memory.explorer.importNoFile": "Choose a file first",
|
|
1689
|
+
"memory.explorer.bootstrap": "Bootstrap from a directory…",
|
|
1690
|
+
"memory.explorer.bootstrapTitle": "Bootstrap from a directory",
|
|
1691
|
+
"memory.explorer.bootstrapHint": "Reads the deterministic artifacts there (script commands, README, contributing rules, CI workflows, top-level layout, recent commits) — one rule, one memory. No model call; running it again merges instead of duplicating.",
|
|
1692
|
+
"memory.explorer.bootstrapDir": "Directory path",
|
|
1693
|
+
"memory.explorer.bootstrapConfirm": "Bootstrap",
|
|
1694
|
+
"memory.explorer.bootstraping": "Bootstrapping…",
|
|
1695
|
+
"memory.explorer.bootstrapDone": "Created {n}, merged {m}",
|
|
1696
|
+
"memory.explorer.bootstrapNoDir": "Enter a directory path first",
|
|
1697
|
+
"memory.explorer.bootstrapFailed": "Bootstrap failed",
|
|
1698
|
+
"memory.explorer.bootstrapErr.missing-dir": "No directory path was given",
|
|
1699
|
+
"memory.explorer.bootstrapErr.dir-not-found": "That path does not exist",
|
|
1700
|
+
"memory.explorer.bootstrapErr.not-a-directory": "That path is not a directory",
|
|
1701
|
+
"memory.explorer.bootstrapErr.bootstrap-unavailable": "This capability is not wired in the current assembly",
|
|
1603
1702
|
"memory.explorer.sharePack": "Share memories…",
|
|
1604
1703
|
"memory.explorer.shareTitle": "Memory share pack",
|
|
1605
1704
|
"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.",
|
|
@@ -1882,6 +1981,13 @@ window.__ModuleLoader__.load({
|
|
|
1882
1981
|
"memory.status.token.heat": "8-week heat · daily spend",
|
|
1883
1982
|
"memory.status.token.heatLess": "less",
|
|
1884
1983
|
"memory.status.token.heatMore": "more",
|
|
1984
|
+
"memory.status.token.sink": "8-week consolidation · in vs kept",
|
|
1985
|
+
"memory.status.token.sinkAdded": "added",
|
|
1986
|
+
"memory.status.token.sinkMerged": "consolidated",
|
|
1987
|
+
"memory.status.token.sinkArchived": "archived",
|
|
1988
|
+
"memory.status.token.sinkGap": "consolidated/added",
|
|
1989
|
+
"memory.status.token.sinkEmpty": "nothing recorded yet",
|
|
1990
|
+
"memory.status.token.sinkFoot": "consolidated counts distinct records; re-merging the same record counts once",
|
|
1885
1991
|
"memory.status.token.last7": "last 7 days",
|
|
1886
1992
|
"memory.status.token.meterOnly": "(metered only)",
|
|
1887
1993
|
"memory.status.token.detail": "Detailed status cards (the original 9 under Library / Background activity, click to expand)",
|
|
@@ -1968,10 +2074,12 @@ window.__ModuleLoader__.load({
|
|
|
1968
2074
|
// / -warning / -success / -warn / --dsw-alias-border),
|
|
1969
2075
|
// 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组
|
|
1970
2076
|
// 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。
|
|
1971
|
-
//
|
|
2077
|
+
// 必须挂满五个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body
|
|
1972
2078
|
// (.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口
|
|
1973
|
-
// portal 到宿主侧边栏(.mneme-topentry
|
|
1974
|
-
|
|
2079
|
+
// portal 到宿主侧边栏(.mneme-topentry)。第五个根是宿主内整页面板
|
|
2080
|
+
// (.mneme-panel)配套的侧栏图标容器:那颗图标长在宿主的 panelGlyph 里,
|
|
2081
|
+
// 不在 .mneme-x 的子树里,而它右上角的未处理冲突 badge 要吃 --mneme-danger。
|
|
2082
|
+
".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry,.mneme-paneliglyph{" +
|
|
1975
2083
|
"--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
|
|
1976
2084
|
"--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
1977
2085
|
"--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
@@ -2011,6 +2119,9 @@ window.__ModuleLoader__.load({
|
|
|
2011
2119
|
// 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。
|
|
2012
2120
|
".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}",
|
|
2013
2121
|
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
2122
|
+
// 目录计数下面那行小字:说清「总数是全库、列表只列最近 N 个」,两个口径
|
|
2123
|
+
// 不一样不能只靠一个数字冒充(实体页此前就吃到这个亏)。
|
|
2124
|
+
".mneme-xcountnote{font-size:11px;line-height:15px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
2014
2125
|
// --- three-column browse layout: hairline separators, no outer box ---
|
|
2015
2126
|
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
2016
2127
|
".mneme-xside{flex:none;width:264px;min-width:0;min-height:0;overflow-y:auto;overflow-x:hidden;padding:16px 14px;border-right:1px solid var(--dsw-alias-border-l2);box-sizing:border-box;display:flex;flex-direction:column;gap:8px}",
|
|
@@ -2164,6 +2275,49 @@ window.__ModuleLoader__.load({
|
|
|
2164
2275
|
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
|
|
2165
2276
|
// --- settings sub-view ---
|
|
2166
2277
|
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
2278
|
+
// --- 宿主中列整页面板(sidebar.panellist + main 成对座位)---
|
|
2279
|
+
// 宿主把 main 槽渲染在三列 grid 的中列里,那一层只给了 min-width:0 与
|
|
2280
|
+
// overflow:hidden,没有任何尺寸约束——注册者必须自己铺满并自管滚动,
|
|
2281
|
+
// 否则内容会撑破中列(右栏打开时中列更窄,宽度还会变)。
|
|
2282
|
+
".mneme-panel{box-sizing:border-box;flex:1;min-width:0;min-height:0;width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}",
|
|
2283
|
+
// 侧栏图标容器:badge 用绝对定位挂在它上面。宿主 panelRow 没有
|
|
2284
|
+
// overflow:hidden,所以 -3px 的偏移不会被裁;这里的 position:relative
|
|
2285
|
+
// 只提供定位上下文,不接管尺寸(由宿主的 panelGlyph 决定)。
|
|
2286
|
+
".mneme-paneliglyph{position:relative;display:inline-flex;align-items:center;justify-content:center}",
|
|
2287
|
+
// --- chorography 面板(仓库地志):顶栏检索 + 左树右文。颜色全部引用
|
|
2288
|
+
// 既有语义层(--dsw-alias-* 正文/边框、--mneme-fg-dim 弱化),不引入新
|
|
2289
|
+
// 字面量色——contrast.test.js 会重算每一条 color 声明。内容渲染挂在
|
|
2290
|
+
// .mneme-panel 根内(#562 的中列整页容器),不新增渲染根。
|
|
2291
|
+
".mneme-wikibar{flex:none;display:flex;align-items:center;gap:10px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}",
|
|
2292
|
+
".mneme-wikirepo{flex:none;max-width:40%;font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
2293
|
+
".mneme-wikisearch{flex:1;min-width:0;height:28px;padding:0 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:12.5px;outline:none;transition:border-color .12s}",
|
|
2294
|
+
".mneme-wikisearch:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-business-primary) 15%,transparent)}",
|
|
2295
|
+
".mneme-wikibody{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
2296
|
+
".mneme-wikitree{flex:none;width:232px;overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l1);padding:10px 6px}",
|
|
2297
|
+
".mneme-wikitreeitem{display:block;width:100%;text-align:left;border:none;background:none;cursor:pointer;font-family:inherit;font-size:12.5px;line-height:18px;color:var(--dsw-alias-label-secondary);padding:5px 8px;border-radius:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .12s}",
|
|
2298
|
+
".mneme-wikitreeitem:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
2299
|
+
".mneme-wikitreeitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
2300
|
+
".mneme-wikipage{flex:1;min-width:0;overflow-y:auto;padding:18px 22px 44px;color:var(--dsw-alias-label-primary)}",
|
|
2301
|
+
".mneme-wikipage h2,.mneme-wikipage h3,.mneme-wikipage h4,.mneme-wikipage h5{margin:18px 0 8px;color:var(--dsw-alias-label-primary);line-height:1.35}",
|
|
2302
|
+
".mneme-wikipage h2{font-size:17px;border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:6px}",
|
|
2303
|
+
".mneme-wikipage h3{font-size:15px}",
|
|
2304
|
+
".mneme-wikipage h4,.mneme-wikipage h5{font-size:13.5px}",
|
|
2305
|
+
".mneme-wikipage p{margin:6px 0;font-size:13px;line-height:21px;color:var(--dsw-alias-label-secondary)}",
|
|
2306
|
+
".mneme-wikipage ul,.mneme-wikipage ol{margin:6px 0;padding-left:22px}",
|
|
2307
|
+
".mneme-wikipage li{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);margin:3px 0}",
|
|
2308
|
+
".mneme-wikipre{margin:10px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));overflow-x:auto;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:pre}",
|
|
2309
|
+
".mneme-wikicode{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-active);border-radius:4px;padding:1px 5px}",
|
|
2310
|
+
".mneme-wikitablerow{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;line-height:19px;color:var(--dsw-alias-label-secondary);white-space:pre;overflow-x:auto}",
|
|
2311
|
+
".mneme-wikiquote{margin:8px 0;padding:6px 12px;border-left:3px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:19px}",
|
|
2312
|
+
".mneme-wikiempty{padding:26px 22px;font-size:13px;line-height:21px;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:8px;overflow-y:auto}",
|
|
2313
|
+
".mneme-wikiemptytitle{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
2314
|
+
".mneme-wikihits{position:absolute;inset-inline:22px;top:8px;z-index:3;display:flex;flex-direction:column;gap:4px;max-height:46%;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:8px;box-shadow:0 10px 28px color-mix(in srgb,var(--dsw-alias-label-primary) 14%,transparent)}",
|
|
2315
|
+
".mneme-wikihit{text-align:left;border:none;background:none;cursor:pointer;font-family:inherit;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);padding:5px 7px;border-radius:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
2316
|
+
".mneme-wikihit:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
2317
|
+
".mneme-wikihitsclose{flex:none;align-self:flex-end;border:none;background:none;cursor:pointer;color:var(--mneme-fg-dim);font-size:15px;line-height:16px;padding:0 4px}",
|
|
2318
|
+
// 相对定位由 .mneme-panel 提供?它没有——检索浮层需要定位上下文,给页面列加
|
|
2319
|
+
// position:relative(不能动 .mneme-panel 本身:记忆面板还挂在同一根上)。
|
|
2320
|
+
".mneme-wikipage{position:relative}",
|
|
2167
2321
|
// --- 记忆库页面:居中 sheet,非全屏 ---
|
|
2168
2322
|
// 从对话直接打开一页记忆库:背板压暗 + 居中圆角卡片(上限 1180×880),
|
|
2169
2323
|
// 对话仍留在背板之后,任何状态下(含新会话 hero、无 tab 环)都可用。
|
|
@@ -2463,6 +2617,25 @@ window.__ModuleLoader__.load({
|
|
|
2463
2617
|
".mneme-heatcell.l4{background:var(--mneme-heat-1)}",
|
|
2464
2618
|
".mneme-heatlegend{display:flex;align-items:center;gap:3px;margin-top:8px;font-size:10.5px;color:var(--mneme-fg-dim)}",
|
|
2465
2619
|
".mneme-heatlegend .mneme-heatcell{width:10px;height:10px}",
|
|
2620
|
+
// 热力右侧的空白:热力只有 56 格 × 11px ≈ 745px 宽,而容器可用宽 968px,
|
|
2621
|
+
// 右边常年空着约 220px。原先三块图各自独占整行,空白是「每块只占半行」造成的,
|
|
2622
|
+
// 不是某个空 div——所以这里把热力与沉淀图并成两列(左侧热力按内容宽 auto,
|
|
2623
|
+
// 右侧吃剩余),容器窄到放不下两列时回落单列竖排。
|
|
2624
|
+
".mneme-flowrow{display:grid;grid-template-columns:auto minmax(0,1fr);gap:20px;align-items:start}",
|
|
2625
|
+
"@container (max-width:960px){.mneme-flowrow{grid-template-columns:minmax(0,1fr)}}",
|
|
2626
|
+
".mneme-flowheat{min-width:0}",
|
|
2627
|
+
".mneme-flowsink{min-width:0;display:flex;flex-direction:column}",
|
|
2628
|
+
// 8 周沉淀只画两条(进来的 / 稳住的):220px 宽里三序列会细到看不清,
|
|
2629
|
+
// 归档数改在下方图例里给总量。
|
|
2630
|
+
".mneme-sinkchart{display:flex;align-items:flex-end;gap:4px;height:96px;margin-top:12px}",
|
|
2631
|
+
".mneme-sinkcol{flex:1;min-width:0;display:flex;align-items:flex-end;gap:2px;height:100%}",
|
|
2632
|
+
".mneme-sinkbar{flex:1;min-width:0;border-radius:2px 2px 0 0;min-height:1px}",
|
|
2633
|
+
".mneme-sinkbar--added{background:var(--mneme-chart-3)}",
|
|
2634
|
+
".mneme-sinkbar--merged{background:var(--mneme-chart-2)}",
|
|
2635
|
+
".mneme-sinklabels{display:flex;gap:4px;margin-top:4px;font-size:9.5px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
2636
|
+
".mneme-sinklabels span{flex:1;min-width:0;text-align:center;white-space:nowrap}",
|
|
2637
|
+
".mneme-sinklegend{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;margin-top:8px;font-size:10.5px;color:var(--mneme-fg-dim)}",
|
|
2638
|
+
".mneme-sinklegend b{font-weight:600;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}",
|
|
2466
2639
|
// 原 9 卡降级折叠区:默认收起,展开才挂载子卡(见 TokenFlowSection 注释)。
|
|
2467
2640
|
".mneme-legacy{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 16px}",
|
|
2468
2641
|
".mneme-legacy summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
@@ -2506,6 +2679,11 @@ window.__ModuleLoader__.load({
|
|
|
2506
2679
|
".mneme-set-tocchip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2507
2680
|
".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}",
|
|
2508
2681
|
".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2682
|
+
// 配置键覆盖清单(#386 第 7 条):一行一排 chip,长键名换行不截断——
|
|
2683
|
+
// 这张表的用处就是让「memoryQualityFilter.archiveThreshold」这样的名字能被
|
|
2684
|
+
// 完整抄进配置文件,省略号会直接毁掉它的用途。
|
|
2685
|
+
".mneme-keylist{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}",
|
|
2686
|
+
".mneme-keychip{font-family:monospace;font-size:11px;padding:2px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--mneme-fg-dim);word-break:break-all;user-select:all}",
|
|
2509
2687
|
// --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) ---
|
|
2510
2688
|
".mneme-staredit{display:inline-flex;align-items:center;gap:1px;justify-self:start}",
|
|
2511
2689
|
".mneme-starbtn{border:none;background:none;cursor:pointer;padding:2px 1px;display:inline-flex;color:var(--dsw-alias-label-secondary);transition:transform .12s}",
|
|
@@ -2831,6 +3009,12 @@ window.__ModuleLoader__.load({
|
|
|
2831
3009
|
// ④ 关系图谱 the same ego graph the old graph-only tab drew
|
|
2832
3010
|
function EntityPanel({ t, focusEntity, onJumpMemory }) {
|
|
2833
3011
|
const [entities, setEntities] = useState(null);
|
|
3012
|
+
// 目录只取最近 500 个(limit=500),可「一共抽出多少实体」问的是全库。
|
|
3013
|
+
// 后端回 total + totalExact(见 src/api.js 的 /entities),这里必须用全库数,
|
|
3014
|
+
// 否则左侧栏永远显示「500 个实体」——那正是「实体太少」这个观感的另一半来源:
|
|
3015
|
+
// 状态页已经在 #559 修正,实体页这一处当时漏了。null = 全库数读不到(老宿主
|
|
3016
|
+
// 或计数失败),此时退回切片长度并如实标成切片口径,不冒充全库。
|
|
3017
|
+
const [entityTotal, setEntityTotal] = useState(null);
|
|
2834
3018
|
const [railQuery, setRailQuery] = useState("");
|
|
2835
3019
|
const [entityName, setEntityName] = useState(focusEntity || "");
|
|
2836
3020
|
const [depth, setDepth] = useState(1);
|
|
@@ -2853,8 +3037,12 @@ window.__ModuleLoader__.load({
|
|
|
2853
3037
|
let cancelled = false;
|
|
2854
3038
|
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
2855
3039
|
.then((r) => (r.ok ? r.json() : { entities: [] }))
|
|
2856
|
-
.then((j) => {
|
|
2857
|
-
|
|
3040
|
+
.then((j) => {
|
|
3041
|
+
if (cancelled) return;
|
|
3042
|
+
setEntities(Array.isArray(j.entities) ? j.entities : []);
|
|
3043
|
+
setEntityTotal(j.totalExact === true && typeof j.total === "number" ? j.total : null);
|
|
3044
|
+
})
|
|
3045
|
+
.catch(() => { if (!cancelled) { setEntities([]); setEntityTotal(null); } });
|
|
2858
3046
|
return () => { cancelled = true; };
|
|
2859
3047
|
}, []);
|
|
2860
3048
|
|
|
@@ -3106,8 +3294,13 @@ window.__ModuleLoader__.load({
|
|
|
3106
3294
|
value: railQuery,
|
|
3107
3295
|
onChange: (e) => setRailQuery(e.target.value)
|
|
3108
3296
|
}),
|
|
3297
|
+
// 大数字用全库数;全库数读不到(老宿主 / 计数失败)时退回切片长度,
|
|
3298
|
+
// 但那只是「这次取回多少」,所以要多一行脚注说清列表本身只列最近 500 个。
|
|
3109
3299
|
entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
|
|
3110
|
-
t("memory.entities.count").replace("{n}", String(entities.length)))
|
|
3300
|
+
t("memory.entities.count").replace("{n}", String(entityTotal !== null ? entityTotal : entities.length))),
|
|
3301
|
+
entities !== null && entityTotal !== null && entityTotal > entities.length
|
|
3302
|
+
&& h("div", { className: "mneme-xcountnote" },
|
|
3303
|
+
t("memory.entities.countSampled").replace("{shown}", String(entities.length)))
|
|
3111
3304
|
),
|
|
3112
3305
|
h("div", { className: "mneme-entrailbody" },
|
|
3113
3306
|
entities === null && h("div", { className: "mneme-xempty" }, "…"),
|
|
@@ -3393,7 +3586,12 @@ window.__ModuleLoader__.load({
|
|
|
3393
3586
|
// 人格定义页(beta):注入总闸。分组随 enhance(行为开关,非座位闸门);
|
|
3394
3587
|
// 人格内容在设置页「个性化」区的人格卡。
|
|
3395
3588
|
// 严格寻址是同一条链上的第二个口径开关(只在总闸打开时起作用)。
|
|
3396
|
-
"personaEnabled", "personaStrictAddressing", "profileExtractEnabled"
|
|
3589
|
+
"personaEnabled", "personaStrictAddressing", "profileExtractEnabled",
|
|
3590
|
+
// chorography(仓库地志)的暴露闸(M1 就有的键,M3 才上 UI):它是「是否
|
|
3591
|
+
// 启用这个子系统」而不是「每轮付多少」,所以和上面这些行为开关同组;
|
|
3592
|
+
// 每轮付出多少由紧随组尾的 choroSub 档位管。放组末是因为子块按组尾渲染
|
|
3593
|
+
// ——档位下拉要贴着它的总闸。
|
|
3594
|
+
"chorographyEnabled"] },
|
|
3397
3595
|
{ key: "group.dream", items: ["sleepModeEnabled"] },
|
|
3398
3596
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
3399
3597
|
{ key: "group.scope", items: ["scopeEnabled", "strictScope"] }
|
|
@@ -3417,6 +3615,9 @@ window.__ModuleLoader__.load({
|
|
|
3417
3615
|
// 蒸馏思考强度(issue #315):比 entity 多一个 off(显式关思考,思考型模型
|
|
3418
3616
|
// 蒸馏防推理烧预算)。后端枚举含 off,面板必须能选到,否则操作者用不上。
|
|
3419
3617
|
const SUMMARIZE_REASONING = ["off", "none", "low", "medium", "high"];
|
|
3618
|
+
// 地志注入档位(M3):与后端 settings.js 的 FEATURE_FLAG_ENUMS.chorographyInject
|
|
3619
|
+
// 同一份取值。light 是默认档,off 才是不注入。
|
|
3620
|
+
const CHORO_TIERS = ["off", "light", "full"];
|
|
3420
3621
|
|
|
3421
3622
|
function FeatureRow({ name, hint, on, disabled, onToggle, sub, tag }) {
|
|
3422
3623
|
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
@@ -3834,6 +4035,24 @@ window.__ModuleLoader__.load({
|
|
|
3834
4035
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.distillRecords.retentionDays.hint"))
|
|
3835
4036
|
);
|
|
3836
4037
|
|
|
4038
|
+
// chorography M3:开局注入的档位。挂在 chorographyEnabled 下(总闸关着时
|
|
4039
|
+
// 不展开——没启用子系统就没有「注入多少」可谈)。枚举键没有通用渲染器,
|
|
4040
|
+
// 与上面几个子块同款手写下拉。轻量档(lightMode)在消费侧压成 off,
|
|
4041
|
+
// 面板不因此灰掉:那是运行时预设,不是用户选择。
|
|
4042
|
+
const choroSub = eff.chorographyEnabled && h("div", { className: "mneme-featsub" },
|
|
4043
|
+
h("div", { className: "mneme-featnum" },
|
|
4044
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.features.chorographyInject")),
|
|
4045
|
+
h("select", {
|
|
4046
|
+
className: "mneme-select",
|
|
4047
|
+
value: eff.chorographyInject || "light",
|
|
4048
|
+
disabled: busy,
|
|
4049
|
+
onChange: (e) => put({ chorographyInject: e.target.value })
|
|
4050
|
+
},
|
|
4051
|
+
CHORO_TIERS.map((r) => h("option", { key: r, value: r }, t(`memory.features.chorographyInject.${r}`))))
|
|
4052
|
+
),
|
|
4053
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.chorographyInject.hint"))
|
|
4054
|
+
);
|
|
4055
|
+
|
|
3837
4056
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
3838
4057
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
3839
4058
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -3868,7 +4087,7 @@ window.__ModuleLoader__.load({
|
|
|
3868
4087
|
// 子块按组尾追加。distillSub 紧跟在 core 组最后一行
|
|
3869
4088
|
// (distillRecords.enabled)之后,天数行才贴着它的总闸。
|
|
3870
4089
|
g.key === "group.core" && h(react.Fragment, null, distillSub, summarizeSub),
|
|
3871
|
-
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
4090
|
+
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub, choroSub),
|
|
3872
4091
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
3873
4092
|
);
|
|
3874
4093
|
const firstScreen = FEATURE_GROUPS.filter((g) => FEATURE_FIRST_SCREEN.includes(g.key));
|
|
@@ -3963,6 +4182,15 @@ window.__ModuleLoader__.load({
|
|
|
3963
4182
|
// 话——把「没启用」「库里没东西」「模型没配合」压成同一句「没有草稿」,用户就只能瞎猜。
|
|
3964
4183
|
const [profileDraft, setProfileDraft] = react.useState(null);
|
|
3965
4184
|
const [profileExtracting, setProfileExtracting] = react.useState(false);
|
|
4185
|
+
const [profileDraftCopied, setProfileDraftCopied] = react.useState(false);
|
|
4186
|
+
// 复制失败必须**看得见**:announce() 那个 live region 是 1px 裁剪元素,只有屏幕
|
|
4187
|
+
// 阅读器读得到。视力正常的用户看到按钮文字没变、页面也没变化,就会以为复制成功,
|
|
4188
|
+
// 粘贴时拿到旧的剪贴板内容——比不提示更糟。
|
|
4189
|
+
const [profileDraftCopyFailed, setProfileDraftCopyFailed] = react.useState(false);
|
|
4190
|
+
// 复制的每一趟都要能认出「这条回调还属于当前这份草稿吗」。剪贴板是异步的,复制 A 的
|
|
4191
|
+
// promise 还没落地时又提取出草稿 B,A 的回调就会在 B 上打勾或报错——B 一个字都没被
|
|
4192
|
+
// 复制过。用递增序号做票据:换草稿或重新复制都换票,旧票的回调直接丢弃。
|
|
4193
|
+
const profileCopyTicket = react.useRef(0);
|
|
3966
4194
|
// 保存失败要可见(persona 那条一直有,画像这条以前是静默 catch)。
|
|
3967
4195
|
const [profileError, setProfileError] = react.useState("");
|
|
3968
4196
|
const [cmdError, setCmdError] = react.useState("");
|
|
@@ -4186,6 +4414,10 @@ window.__ModuleLoader__.load({
|
|
|
4186
4414
|
async function extractProfileDraft() {
|
|
4187
4415
|
setProfileExtracting(true);
|
|
4188
4416
|
setProfileDraft(null);
|
|
4417
|
+
// 新草稿一出现,上一趟复制的回调立刻作废,连它留下的两种提示也一并清掉。
|
|
4418
|
+
profileCopyTicket.current += 1;
|
|
4419
|
+
setProfileDraftCopied(false);
|
|
4420
|
+
setProfileDraftCopyFailed(false);
|
|
4189
4421
|
try {
|
|
4190
4422
|
const res = await apiFetch("/api/dsh-mneme/profile/extract", {
|
|
4191
4423
|
method: "POST",
|
|
@@ -4217,16 +4449,59 @@ window.__ModuleLoader__.load({
|
|
|
4217
4449
|
}
|
|
4218
4450
|
}
|
|
4219
4451
|
|
|
4220
|
-
//
|
|
4221
|
-
//
|
|
4222
|
-
//
|
|
4223
|
-
//
|
|
4224
|
-
|
|
4452
|
+
// 草稿只展示副本(桉桉 2026-11-04 定的口径)。这一段以前叫「采用为画像」,
|
|
4453
|
+
// 点一下就直接 PUT /profile —— 把用户手写的那份覆盖掉,且没有撤销。现在改成
|
|
4454
|
+
// 复制:要落进「用户画像」必须由用户自己粘贴再点保存,手写原文一个字都不会被
|
|
4455
|
+
// 后台改掉。剪贴板不可用(无权限 / 非安全上下文)时不许假装成功——退回
|
|
4456
|
+
// execCommand 那条老路,仍然失败就明确让她手动选中复制。
|
|
4457
|
+
function copyProfileDraft() {
|
|
4225
4458
|
if (profileDraft?.kind !== "draft") return;
|
|
4226
|
-
const text = profileDraft.text;
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4459
|
+
const text = profileDraft.text || "";
|
|
4460
|
+
// 每次尝试先清掉上次的失败提示:失败过以后再成功,红字不能留在屏上。
|
|
4461
|
+
setProfileDraftCopyFailed(false);
|
|
4462
|
+
// 这一趟的票。回调落地时票已经不是当前那张,就说明界面早已换了一份草稿。
|
|
4463
|
+
const ticket = ++profileCopyTicket.current;
|
|
4464
|
+
const current = () => ticket === profileCopyTicket.current;
|
|
4465
|
+
const ok = () => {
|
|
4466
|
+
if (!current()) return;
|
|
4467
|
+
setProfileDraftCopied(true);
|
|
4468
|
+
announce(t("memory.settings.profileDraftCopied"));
|
|
4469
|
+
setTimeout(() => { if (current()) setProfileDraftCopied(false); }, 1500);
|
|
4470
|
+
};
|
|
4471
|
+
// 剪贴板失败要说话:这个方法唯一的产出就是"文本进了剪贴板",静默失败
|
|
4472
|
+
// 会让用户以为复制成功、粘贴时却拿到旧内容(草稿是只读 <pre>,选中再
|
|
4473
|
+
// 复制是她能自己走的退路,得明确告诉她走这条)。
|
|
4474
|
+
// 失败还要顺手撤掉「已复制」:成功态能挂 1.5 秒,这期间重试失败会同时出现
|
|
4475
|
+
// 「已复制」和「复制没成功」两句相反的话。
|
|
4476
|
+
const failed = () => {
|
|
4477
|
+
if (!current()) return;
|
|
4478
|
+
setProfileDraftCopied(false);
|
|
4479
|
+
setProfileDraftCopyFailed(true);
|
|
4480
|
+
announce(t("memory.settings.profileDraftCopyFailed"));
|
|
4481
|
+
};
|
|
4482
|
+
if (navigator.clipboard?.writeText) {
|
|
4483
|
+
navigator.clipboard.writeText(text).then(ok, () => copyProfileDraftFallback(text) ? ok() : failed());
|
|
4484
|
+
} else {
|
|
4485
|
+
copyProfileDraftFallback(text) ? ok() : failed();
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// 没有 Clipboard API(非安全上下文 / 内嵌 webview 未授权)时的退路:
|
|
4490
|
+
// 临时 textarea + execCommand("copy")。失败返回 false,由调用方说话。
|
|
4491
|
+
function copyProfileDraftFallback(text) {
|
|
4492
|
+
try {
|
|
4493
|
+
const ta = document.createElement("textarea");
|
|
4494
|
+
ta.value = text;
|
|
4495
|
+
ta.setAttribute("readonly", "");
|
|
4496
|
+
ta.style.position = "fixed";
|
|
4497
|
+
ta.style.opacity = "0";
|
|
4498
|
+
document.body.appendChild(ta);
|
|
4499
|
+
ta.select();
|
|
4500
|
+
const done = document.execCommand("copy");
|
|
4501
|
+
document.body.removeChild(ta);
|
|
4502
|
+
return done === true;
|
|
4503
|
+
} catch {
|
|
4504
|
+
return false;
|
|
4230
4505
|
}
|
|
4231
4506
|
}
|
|
4232
4507
|
|
|
@@ -4521,35 +4796,7 @@ window.__ModuleLoader__.load({
|
|
|
4521
4796
|
saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved")),
|
|
4522
4797
|
profileError ? h("span", { className: "mneme-saved", style: { color: "var(--dsw-danger, #d33)" } },
|
|
4523
4798
|
t("memory.settings.profileSaveFailed") + " (" + profileError + ")") : null
|
|
4524
|
-
)
|
|
4525
|
-
// 从记忆提取草稿(#497)。两种结局都要显眼:草稿(要用户点「采用」才落库)
|
|
4526
|
-
// 与失败原因(开关没开 / 没内容可提 / 模型没配合)。手写的原文在这之上一直
|
|
4527
|
-
// 可见——采用前它不会被任何东西覆盖。
|
|
4528
|
-
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
|
|
4529
|
-
h("button", {
|
|
4530
|
-
type: "button",
|
|
4531
|
-
className: "mneme-btn",
|
|
4532
|
-
disabled: profileExtracting,
|
|
4533
|
-
onClick: extractProfileDraft
|
|
4534
|
-
}, t("memory.settings.profileExtract")),
|
|
4535
|
-
profileExtracting ? h("span", { className: "mneme-saved" }, t("memory.settings.profileExtracting")) : null
|
|
4536
|
-
),
|
|
4537
|
-
h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractHint")),
|
|
4538
|
-
profileDraft?.kind === "disabled" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractDisabled")) : null,
|
|
4539
|
-
profileDraft?.kind === "empty" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractEmpty")) : null,
|
|
4540
|
-
profileDraft?.kind === "noroute" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractNoRoute")) : null,
|
|
4541
|
-
profileDraft?.kind === "failed" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractFailed")) : null,
|
|
4542
|
-
profileDraft?.kind === "draft" ? h("div", null,
|
|
4543
|
-
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.profileDraftTitle")),
|
|
4544
|
-
h("div", { className: "mneme-set-desc" },
|
|
4545
|
-
t("memory.settings.profileDraftRows") + ": " + profileDraft.rowsUsed
|
|
4546
|
-
+ (profileDraft.truncated ? " · " + t("memory.settings.profileDraftTruncated") : "")),
|
|
4547
|
-
h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } }, profileDraft.text),
|
|
4548
|
-
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 6 } },
|
|
4549
|
-
h("button", { type: "button", className: "mneme-btn", onClick: applyProfileDraft }, t("memory.settings.profileDraftApply")),
|
|
4550
|
-
h("button", { type: "button", className: "mneme-btn", onClick: () => setProfileDraft(null) }, t("memory.settings.profileDraftDiscard"))
|
|
4551
|
-
)
|
|
4552
|
-
) : null
|
|
4799
|
+
)
|
|
4553
4800
|
),
|
|
4554
4801
|
// 人格卡(beta,v1.2 多卡)— who the agent is:多张角色卡按会话身份注入。
|
|
4555
4802
|
// 卡内开关只是该卡 enabled 半边;总闸 personaEnabled 在功能开关组,双闸同真。
|
|
@@ -4681,7 +4928,45 @@ window.__ModuleLoader__.load({
|
|
|
4681
4928
|
className: "mneme-btn",
|
|
4682
4929
|
onClick: () => loadPreview(personaEditing)
|
|
4683
4930
|
}, t("memory.settings.personaViewRefresh"))
|
|
4684
|
-
)
|
|
4931
|
+
),
|
|
4932
|
+
// 从记忆提取草稿(#497)。**放在这张卡上而不是「用户画像」卡**:画像那一栏是
|
|
4933
|
+
// 用户自己手填的地方;这一段是从记忆里反推出来的「Agent 眼中的你」,属于可
|
|
4934
|
+
// 沉淀的那一侧。它只出**副本**——不写 user_settings,也不碰手写画像(桉桉
|
|
4935
|
+
// 2026-11-04 定的口径:只展示副本)。要改成手写画像只能自己复制再粘过去。
|
|
4936
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
4937
|
+
h("button", {
|
|
4938
|
+
type: "button",
|
|
4939
|
+
className: "mneme-btn",
|
|
4940
|
+
disabled: profileExtracting,
|
|
4941
|
+
onClick: extractProfileDraft
|
|
4942
|
+
}, t("memory.settings.profileExtract")),
|
|
4943
|
+
profileExtracting ? h("span", { className: "mneme-saved" }, t("memory.settings.profileExtracting")) : null
|
|
4944
|
+
),
|
|
4945
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractHint")),
|
|
4946
|
+
profileDraft?.kind === "disabled" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractDisabled")) : null,
|
|
4947
|
+
profileDraft?.kind === "empty" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractEmpty")) : null,
|
|
4948
|
+
profileDraft?.kind === "noroute" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractNoRoute")) : null,
|
|
4949
|
+
profileDraft?.kind === "failed" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractFailed")) : null,
|
|
4950
|
+
profileDraft?.kind === "draft" ? h("div", null,
|
|
4951
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.profileDraftTitle")),
|
|
4952
|
+
h("div", { className: "mneme-set-desc" },
|
|
4953
|
+
t("memory.settings.profileDraftRows") + ": " + profileDraft.rowsUsed
|
|
4954
|
+
+ (profileDraft.truncated ? " · " + t("memory.settings.profileDraftTruncated") : "")),
|
|
4955
|
+
h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } }, profileDraft.text),
|
|
4956
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 6 } },
|
|
4957
|
+
h("button", { type: "button", className: "mneme-btn", onClick: copyProfileDraft },
|
|
4958
|
+
profileDraftCopied ? t("memory.settings.profileDraftCopied") : t("memory.settings.profileDraftCopy")),
|
|
4959
|
+
h("button", { type: "button", className: "mneme-btn", onClick: () => {
|
|
4960
|
+
profileCopyTicket.current += 1;
|
|
4961
|
+
setProfileDraft(null);
|
|
4962
|
+
setProfileDraftCopied(false);
|
|
4963
|
+
setProfileDraftCopyFailed(false);
|
|
4964
|
+
} }, t("memory.settings.profileDraftDiscard")),
|
|
4965
|
+
h("span", { className: "mneme-set-desc" }, t("memory.settings.profileDraftCopyHint")),
|
|
4966
|
+
profileDraftCopyFailed ? h("span", { className: "mneme-set-desc", role: "alert", style: { color: "var(--mneme-danger)" } },
|
|
4967
|
+
t("memory.settings.profileDraftCopyFailed")) : null
|
|
4968
|
+
)
|
|
4969
|
+
) : null
|
|
4685
4970
|
),
|
|
4686
4971
|
// 规则 — numbered rows, hover reveals the delete affordance
|
|
4687
4972
|
h("section", { className: "mneme-set-card" },
|
|
@@ -4922,6 +5207,10 @@ window.__ModuleLoader__.load({
|
|
|
4922
5207
|
),
|
|
4923
5208
|
apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
|
|
4924
5209
|
),
|
|
5210
|
+
// 配置键覆盖面(issue #386 第 7 条):只读核对表,放在 connect 组尾——
|
|
5211
|
+
// 它与上面的 API Token / 独立服务同属「我在这台机器上能调什么」那一类,
|
|
5212
|
+
// 而不是功能开关(那些在 personal/core/enhance 各组里改)。
|
|
5213
|
+
h(ConfigKeysCard, { t }),
|
|
4925
5214
|
// —— 帮助:支持与鼓励(图标行 + 检测更新 + star,兼当「关于」收尾) ——
|
|
4926
5215
|
groupHead("help"),
|
|
4927
5216
|
// 反馈三入口:新建 issue 预填(版本+平台)/ 邮件 / 浏览已知问题。
|
|
@@ -4997,6 +5286,16 @@ window.__ModuleLoader__.load({
|
|
|
4997
5286
|
// 上下文裁不住它;运行时拒绝 react-dom 时就地渲染(position:fixed 仍然成立)。
|
|
4998
5287
|
function MemoryOverlay({ t }) {
|
|
4999
5288
|
const [open, setOpen] = useOverlayOpen();
|
|
5289
|
+
// 库内是否有请求在飞(目前只有「从目录引导」会置位)。Esc / 背板 / ✕ 三条
|
|
5290
|
+
// 关闭路径都要看它:关掉弹层会把 MemoryExplorer 连同引导弹层一起卸载,
|
|
5291
|
+
// 而 POST 还在飞,回来那句「新建几条、并入几条」就再没有地方显示——
|
|
5292
|
+
// 库写进去了,用户却以为没成功(评审 #4453225573)。
|
|
5293
|
+
const [explorerBusy, setExplorerBusy] = useState(false);
|
|
5294
|
+
// 用 ref 读而不是进 effect 依赖:那个 effect 在 [open] 变化时会
|
|
5295
|
+
// closeBtnRef.current.focus(),若把 explorerBusy 也列进依赖,每次请求
|
|
5296
|
+
// 起落都会重跑一次并抢焦点——用户正打字输目录时会被踢出输入框。
|
|
5297
|
+
const explorerBusyRef = useRef(false);
|
|
5298
|
+
useEffect(() => { explorerBusyRef.current = explorerBusy; }, [explorerBusy]);
|
|
5000
5299
|
// #178:弹层焦点管理——打开时把焦点移入面板(关闭按钮为入口),
|
|
5001
5300
|
// Tab 循环圈在面板内(Tab 从最后一个元素出去回到关闭按钮),
|
|
5002
5301
|
// Esc/关闭时把焦点还给触发元素(侧边栏入口)。
|
|
@@ -5005,7 +5304,8 @@ window.__ModuleLoader__.load({
|
|
|
5005
5304
|
useEffect(() => {
|
|
5006
5305
|
if (!open) return undefined;
|
|
5007
5306
|
const onKey = (e) => {
|
|
5008
|
-
|
|
5307
|
+
// 库内请求在飞时不响应 Esc:见上面 explorerBusy 的注释。
|
|
5308
|
+
if (e.key === "Escape") { if (!explorerBusyRef.current) setOpen(false); return; }
|
|
5009
5309
|
if (e.key !== "Tab" || !panelRef.current) return;
|
|
5010
5310
|
const focusables = panelRef.current.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])');
|
|
5011
5311
|
if (focusables.length === 0) return;
|
|
@@ -5033,7 +5333,7 @@ window.__ModuleLoader__.load({
|
|
|
5033
5333
|
const tree = h(react.Fragment, null,
|
|
5034
5334
|
h("div", {
|
|
5035
5335
|
className: "mneme-backdrop",
|
|
5036
|
-
onClick: () => setOpen(false),
|
|
5336
|
+
onClick: () => { if (!explorerBusyRef.current) setOpen(false); },
|
|
5037
5337
|
"aria-hidden": "true"
|
|
5038
5338
|
}),
|
|
5039
5339
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
@@ -5046,12 +5346,15 @@ window.__ModuleLoader__.load({
|
|
|
5046
5346
|
type: "button",
|
|
5047
5347
|
className: "mneme-footbtn",
|
|
5048
5348
|
ref: closeBtnRef,
|
|
5349
|
+
// 忙碌时不禁用、只标 aria-disabled:真的 disabled 会把焦点从按钮上
|
|
5350
|
+
// 拿走,而 #178 的 Tab 环靠 closeBtnRef 当入口,焦点落空就出不去。
|
|
5351
|
+
"aria-disabled": String(explorerBusy),
|
|
5049
5352
|
"aria-label": t("memory.overlay.close"),
|
|
5050
5353
|
title: t("memory.overlay.close"),
|
|
5051
|
-
onClick: () => setOpen(false)
|
|
5354
|
+
onClick: () => { if (!explorerBusyRef.current) setOpen(false); }
|
|
5052
5355
|
}, "✕")
|
|
5053
5356
|
),
|
|
5054
|
-
h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t }))
|
|
5357
|
+
h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t, onBusyChange: setExplorerBusy }))
|
|
5055
5358
|
)
|
|
5056
5359
|
);
|
|
5057
5360
|
if (reactDom && typeof document !== "undefined") return reactDom.createPortal(tree, document.body);
|
|
@@ -5123,7 +5426,7 @@ window.__ModuleLoader__.load({
|
|
|
5123
5426
|
|
|
5124
5427
|
// 实体 — directory snapshot grouped by entity type.
|
|
5125
5428
|
function EntitiesStatusCard({ t }) {
|
|
5126
|
-
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
5429
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, totalKnown: true, byType: [] });
|
|
5127
5430
|
useEffect(() => {
|
|
5128
5431
|
let cancelled = false;
|
|
5129
5432
|
const order = ["organization", "person", "project", "technology", "concept"];
|
|
@@ -5140,9 +5443,16 @@ window.__ModuleLoader__.load({
|
|
|
5140
5443
|
const byType = order.concat("other")
|
|
5141
5444
|
.filter((k) => counts.has(k))
|
|
5142
5445
|
.map((k) => [k, counts.get(k)]);
|
|
5143
|
-
|
|
5446
|
+
// 大数字用后端回的全库 total;分类型小行仍来自本次切片(最近 500 个),
|
|
5447
|
+
// 两个口径不一样,切片写进 foot 里说清楚——以前 total 取 list.length,
|
|
5448
|
+
// 一万六千个实体永远显示成 500,看着像"实体太少"。
|
|
5449
|
+
// totalExact 为假(计数失败 / 老宿主)时 total 只是切片长度,不能当全库数
|
|
5450
|
+
// 显示:那正是原来「永远 500」的错法。这时大数字显示“—”,脚注说明总数读不到。
|
|
5451
|
+
const totalKnown = j.totalExact === true && typeof j.total === "number";
|
|
5452
|
+
const realTotal = totalKnown ? j.total : list.length;
|
|
5453
|
+
setState({ loading: false, error: false, total: realTotal, totalKnown, sampled: list.length < realTotal, byType });
|
|
5144
5454
|
})
|
|
5145
|
-
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
5455
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, totalKnown: true, byType: [] }); });
|
|
5146
5456
|
return () => { cancelled = true; };
|
|
5147
5457
|
}, []);
|
|
5148
5458
|
const rows = state.byType.map(([ty, n]) => ({ label: entityTypeLabel(t, ty), value: String(n) }));
|
|
@@ -5151,8 +5461,11 @@ window.__ModuleLoader__.load({
|
|
|
5151
5461
|
title: t("memory.status.entities"),
|
|
5152
5462
|
loading: state.loading,
|
|
5153
5463
|
error: state.error,
|
|
5154
|
-
num: state.total.toLocaleString(),
|
|
5155
|
-
rows
|
|
5464
|
+
num: state.totalKnown === false ? "—" : state.total.toLocaleString(),
|
|
5465
|
+
rows,
|
|
5466
|
+
foot: state.totalKnown === false
|
|
5467
|
+
? t("memory.status.entitiesUnknown")
|
|
5468
|
+
: state.sampled === true ? t("memory.status.entitiesSampled") : null
|
|
5156
5469
|
});
|
|
5157
5470
|
}
|
|
5158
5471
|
|
|
@@ -5613,6 +5926,76 @@ window.__ModuleLoader__.load({
|
|
|
5613
5926
|
});
|
|
5614
5927
|
}
|
|
5615
5928
|
|
|
5929
|
+
// --- 配置键覆盖面(issue #386 第 7 条)---
|
|
5930
|
+
// 面板能改的键(settings.js 白名单)与 schema 里真有的键是两张表,从没被摆在一起
|
|
5931
|
+
// 对过。这条卡把它们交/差出来,回答一个具体问题:**「我在配置文件里看到的这个键,
|
|
5932
|
+
// 为什么在面板上找不到」**——答案要么是「它只能改文件」(fileOnly),要么是「它
|
|
5933
|
+
// 登记了却没默认值」(undefaulted,见 config-keys.js 对 dreamProvider 的说明)。
|
|
5934
|
+
//
|
|
5935
|
+
// 只显示键名,一个值都不显示:apiToken 的名字出现在「仅配置文件」里正是这条 issue
|
|
5936
|
+
// 想让人看见的(否则用户不知道它存在),值则一律不出服务端。
|
|
5937
|
+
//
|
|
5938
|
+
// 默认收起:这是一张核对表,不是日常要看的东西。做只读清单而不是补一批输入框,
|
|
5939
|
+
// 也是维护者在 issue 里给的接手口径(「做成只读清单就能先关掉大部分价值」)。
|
|
5940
|
+
function ConfigKeysCard({ t }) {
|
|
5941
|
+
const [state, setState] = useState({ loading: true, data: null, failed: false });
|
|
5942
|
+
const [open, setOpen] = useState(false);
|
|
5943
|
+
useEffect(() => {
|
|
5944
|
+
let cancelled = false;
|
|
5945
|
+
apiFetch("/api/dsh-mneme/config-keys")
|
|
5946
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
5947
|
+
.then((j) => { if (!cancelled) setState({ loading: false, data: j, failed: false }); })
|
|
5948
|
+
.catch(() => { if (!cancelled) setState({ loading: false, data: null, failed: true }); });
|
|
5949
|
+
return () => { cancelled = true; };
|
|
5950
|
+
}, []);
|
|
5951
|
+
const d = state.data;
|
|
5952
|
+
const counts = (d && d.counts) || null;
|
|
5953
|
+
const fileOnly = (d && d.fileOnly) || [];
|
|
5954
|
+
const undefaulted = (d && d.undefaulted) || [];
|
|
5955
|
+
const title = t("memory.settings.configKeys.title");
|
|
5956
|
+
const body = state.loading
|
|
5957
|
+
? h("div", { className: "mneme-set-hint" }, "…")
|
|
5958
|
+
// 取数失败与「真的没有只读键」必须分开说:把失败画成 0 条,人会以为清单就是空的。
|
|
5959
|
+
: state.failed || !counts
|
|
5960
|
+
? h("div", { className: "mneme-set-hint", style: { color: "var(--mneme-danger)" } },
|
|
5961
|
+
t("memory.settings.configKeys.loadFailed"))
|
|
5962
|
+
: h(react.Fragment, null,
|
|
5963
|
+
h("div", { className: "mneme-set-hint", style: { marginTop: 4 } },
|
|
5964
|
+
t("memory.settings.configKeys.summary")
|
|
5965
|
+
.replace("{schema}", String(counts.schemaLeaf))
|
|
5966
|
+
.replace("{panel}", String(counts.panelEditable))
|
|
5967
|
+
.replace("{file}", String(counts.fileOnly))
|
|
5968
|
+
// 三类的数字必须都来自服务端算出来的桶:写成 schema − panel 会与展开后
|
|
5969
|
+
// 「只能改文件」那段对不上(评审 #4452245244),因为 schema 之外还多出
|
|
5970
|
+
// 白名单里登记了却没默认值的那几个键。
|
|
5971
|
+
.replace("{undefaulted}", String(counts.undefaulted))),
|
|
5972
|
+
open && h("div", { style: { marginTop: 10 } },
|
|
5973
|
+
// 两段各自的标题也带上条数,收起时只看 summary 就够判断要不要展开。
|
|
5974
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 2 } },
|
|
5975
|
+
t("memory.settings.configKeys.fileOnlyHead")
|
|
5976
|
+
.replace("{n}", String(fileOnly.length))),
|
|
5977
|
+
h("div", { className: "mneme-keylist" },
|
|
5978
|
+
fileOnly.map((k) => h("code", { key: k, className: "mneme-keychip" }, k))),
|
|
5979
|
+
undefaulted.length > 0 && h(react.Fragment, null,
|
|
5980
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 10 } },
|
|
5981
|
+
t("memory.settings.configKeys.undefaultedHead")
|
|
5982
|
+
.replace("{n}", String(undefaulted.length))),
|
|
5983
|
+
h("div", { className: "mneme-keylist" },
|
|
5984
|
+
undefaulted.map((k) => h("code", { key: k, className: "mneme-keychip" }, k))),
|
|
5985
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } },
|
|
5986
|
+
t("memory.settings.configKeys.undefaultedHint"))
|
|
5987
|
+
)
|
|
5988
|
+
),
|
|
5989
|
+
h("button", { type: "button", className: "mneme-btn", style: { marginTop: 10 }, onClick: () => setOpen(!open) },
|
|
5990
|
+
open ? t("memory.settings.configKeys.hide") : t("memory.settings.configKeys.show"))
|
|
5991
|
+
);
|
|
5992
|
+
return h("section", { className: "mneme-set-card" },
|
|
5993
|
+
h("div", { className: "mneme-set-title" }, title),
|
|
5994
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.configKeys.desc")),
|
|
5995
|
+
body
|
|
5996
|
+
);
|
|
5997
|
+
}
|
|
5998
|
+
|
|
5616
5999
|
// --- Token 去向三屏(2026-10 可观测性改造)---
|
|
5617
6000
|
// 原 LlmStatusCard 只有一个「LLM 消耗」总数,且 by_source 把两个最大成本项
|
|
5618
6001
|
// dream_consolidate / dream_summarize 合并成 autoDream(2026-10 审计里巩固
|
|
@@ -5654,6 +6037,11 @@ window.__ModuleLoader__.load({
|
|
|
5654
6037
|
function TokenFlowSection({ t }) {
|
|
5655
6038
|
const [stats, setStats] = useState(null);
|
|
5656
6039
|
const [heatStats, setHeatStats] = useState(null);
|
|
6040
|
+
// 沉淀曲线(/semantic/consolidation-stats)与 token 审计不同源:前者读
|
|
6041
|
+
// memories.created_at / receipt_chain / archived_at,与「花了多少 token」
|
|
6042
|
+
// 无关,所以是独立请求、独立 error 位——一个失败不该把另一个图也拖成错误态。
|
|
6043
|
+
const [sinkStats, setSinkStats] = useState(null);
|
|
6044
|
+
const [sinkError, setSinkError] = useState(false);
|
|
5657
6045
|
const [statsError, setStatsError] = useState(false);
|
|
5658
6046
|
// #521 评审:待确认冲突与「上一轮跳过明细」同源(/dream-status),一次请求两用。
|
|
5659
6047
|
// 跳过明细必须出现在首屏——它原先只挂在折叠区里的 DreamStatusCards 上,而那个
|
|
@@ -5680,6 +6068,14 @@ window.__ModuleLoader__.load({
|
|
|
5680
6068
|
setHeatStats(j && typeof j === "object" ? (j.stats || j) : {});
|
|
5681
6069
|
})
|
|
5682
6070
|
.catch(() => { if (!cancelled) setHeatStats({}); });
|
|
6071
|
+
// 沉淀曲线同样要 56 天窗口,理由与热力一致(7 天的 by_day 撑不满 8 周)。
|
|
6072
|
+
apiFetch("/api/dsh-mneme/semantic/consolidation-stats?days=56")
|
|
6073
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
6074
|
+
.then((j) => {
|
|
6075
|
+
if (cancelled) return;
|
|
6076
|
+
setSinkStats(j && typeof j === "object" ? j : {});
|
|
6077
|
+
})
|
|
6078
|
+
.catch(() => { if (!cancelled) setSinkError(true); });
|
|
5683
6079
|
// KPI 第 4 格的待确认冲突与 DreamStatusCards 同源(/dream-status),
|
|
5684
6080
|
// 同样监听 mneme:conflicts-changed 重取(#295),两处计数永不漂移。
|
|
5685
6081
|
const loadConflicts = () => {
|
|
@@ -5799,6 +6195,42 @@ window.__ModuleLoader__.load({
|
|
|
5799
6195
|
for (const d of [1, 3, 5]) rows[(d - startDow + 7) % 7] = ["一", "三", "五"][[1, 3, 5].indexOf(d)];
|
|
5800
6196
|
return rows;
|
|
5801
6197
|
})();
|
|
6198
|
+
// 8 周沉淀:56 天原始序列在热力右侧那条约 220px 的竖列里会细到看不出,
|
|
6199
|
+
// 所以按周聚成 8 桶(右端对齐今天 UTC,与 7 天柱状同一套「补连续区间」
|
|
6200
|
+
// 口径,缺周填 0)。三条线只画两条——新增与巩固是这张图要表达的剪刀差,
|
|
6201
|
+
// 归档数改在图例里给总量(它从 10-12 才开始非零,混进同一坐标会误导)。
|
|
6202
|
+
// sinkStats 为 null(未到)与 {}(已知为空)必须分开:前者不画图,后者
|
|
6203
|
+
// 画空态,否则加载态会先画 8 根零高柱子。
|
|
6204
|
+
const sinkDays = (() => {
|
|
6205
|
+
if (!sinkStats) return null;
|
|
6206
|
+
const rows = Array.isArray(sinkStats.by_day) ? sinkStats.by_day : [];
|
|
6207
|
+
const byDay = new Map(rows.map((d) => [String((d && d.day) ?? ""), d]));
|
|
6208
|
+
const out = [];
|
|
6209
|
+
for (let w = 7; w >= 0; w--) {
|
|
6210
|
+
let added = 0;
|
|
6211
|
+
let merged = 0;
|
|
6212
|
+
let archived = 0;
|
|
6213
|
+
let first = "";
|
|
6214
|
+
for (let i = 6; i >= 0; i--) {
|
|
6215
|
+
const key = new Date(endDay.getTime() - (w * 7 + i) * 86400000).toISOString().slice(0, 10);
|
|
6216
|
+
if (!first) first = key;
|
|
6217
|
+
const row = byDay.get(key);
|
|
6218
|
+
added += Number((row && row.added) ?? 0);
|
|
6219
|
+
merged += Number((row && row.merged) ?? 0);
|
|
6220
|
+
archived += Number((row && row.archived) ?? 0);
|
|
6221
|
+
}
|
|
6222
|
+
out.push({ key: first, added, merged, archived });
|
|
6223
|
+
}
|
|
6224
|
+
return out;
|
|
6225
|
+
})();
|
|
6226
|
+
const sinkMax = Math.max(1, ...((sinkDays ?? []).map((d) => d.added)));
|
|
6227
|
+
const sinkSum = (field) => (sinkDays ?? []).reduce((a, d) => a + d[field], 0);
|
|
6228
|
+
const sinkAddedTotal = sinkSum("added");
|
|
6229
|
+
const sinkMergedTotal = sinkSum("merged");
|
|
6230
|
+
const sinkArchivedTotal = sinkSum("archived");
|
|
6231
|
+
// 巩固/新增的比值取自总量而非逐周:单周可能新增为 0(无分母),总量口径
|
|
6232
|
+
// 才是「这个库到底在膨胀还是在巩固」的答案。
|
|
6233
|
+
const sinkGap = sinkAddedTotal > 0 ? `${((sinkMergedTotal / sinkAddedTotal) * 100).toFixed(0)}%` : "—";
|
|
5802
6234
|
const kpi = (title, num, cap) =>
|
|
5803
6235
|
h(StatusCard, { t, title, loading, error: statsError, num, cap });
|
|
5804
6236
|
const segClass = (s) =>
|
|
@@ -5892,29 +6324,76 @@ window.__ModuleLoader__.load({
|
|
|
5892
6324
|
h("div", { className: "mneme-barlabels" },
|
|
5893
6325
|
days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
|
|
5894
6326
|
),
|
|
5895
|
-
|
|
5896
|
-
//
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
6327
|
+
// 热力与沉淀并排两列:热力只有约 745px,右侧原本整块空着。两列
|
|
6328
|
+
// 在窄容器里回落单列(见 .mneme-flowrow 的 @container)。
|
|
6329
|
+
h("div", { className: "mneme-flowrow" },
|
|
6330
|
+
h("div", { className: "mneme-flowheat" },
|
|
6331
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
|
|
6332
|
+
// 月份行与格子列同用列主序网格对齐:每月占的列数 = 该月在这个
|
|
6333
|
+
// 56 天窗口里的列数(按列的起始日期算),标签只在该列起点画一次。
|
|
6334
|
+
h("div", { className: "mneme-heatmonths" },
|
|
6335
|
+
monthLabels.map((m) => h("span", { key: m.key, style: { gridColumn: `${m.from} / span ${m.span}` } }, m.label))
|
|
6336
|
+
),
|
|
6337
|
+
h("div", { className: "mneme-heatwrap" },
|
|
6338
|
+
h("div", { className: "mneme-heatrows", "aria-hidden": "true" },
|
|
6339
|
+
heatRowLabels.map((d, i) => h("span", { key: i }, d))
|
|
6340
|
+
),
|
|
6341
|
+
h("div", { className: "mneme-heatgrid" },
|
|
6342
|
+
heatCells.map((c) => h("div", {
|
|
6343
|
+
key: c.key,
|
|
6344
|
+
className: heatLevel(c.total, heatQ) ? `mneme-heatcell l${heatLevel(c.total, heatQ)}` : "mneme-heatcell",
|
|
6345
|
+
title: `${c.key} · ${c.total.toLocaleString()}`
|
|
6346
|
+
}))
|
|
6347
|
+
)
|
|
6348
|
+
),
|
|
6349
|
+
h("div", { className: "mneme-heatlegend" },
|
|
6350
|
+
h("span", null, t("memory.status.token.heatLess")),
|
|
6351
|
+
h("span", { className: "mneme-heatcell" }),
|
|
6352
|
+
[1, 2, 3, 4].map((lv) => h("span", { key: lv, className: `mneme-heatcell l${lv}` })),
|
|
6353
|
+
h("span", null, t("memory.status.token.heatMore"))
|
|
6354
|
+
)
|
|
5904
6355
|
),
|
|
5905
|
-
h("div", { className: "mneme-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
6356
|
+
h("div", { className: "mneme-flowsink" },
|
|
6357
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.sink")),
|
|
6358
|
+
sinkError
|
|
6359
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
|
|
6360
|
+
: !sinkDays
|
|
6361
|
+
// 未到:什么都不画。画空态会把「还没加载完」说成「最近没有记录」。
|
|
6362
|
+
? null
|
|
6363
|
+
: sinkAddedTotal + sinkMergedTotal + sinkArchivedTotal === 0
|
|
6364
|
+
? h("div", { className: "mneme-statuscap", style: { marginTop: "12px" } }, t("memory.status.token.sinkEmpty"))
|
|
6365
|
+
: h("div", { className: "mneme-sinkchart" },
|
|
6366
|
+
(sinkDays ?? []).map((d) => h("div", { className: "mneme-sinkcol", key: d.key },
|
|
6367
|
+
// 两根柱同一基准(本期最大新增),高度差就是剪刀差本身;
|
|
6368
|
+
// 巩固可能远小于新增,故给 1px 下限免得整根消失。
|
|
6369
|
+
h("div", {
|
|
6370
|
+
className: "mneme-sinkbar mneme-sinkbar--added",
|
|
6371
|
+
style: { height: `${Math.max((d.added / sinkMax) * 100, d.added > 0 ? 1 : 0)}%` },
|
|
6372
|
+
title: `${d.key} · ${t("memory.status.token.sinkAdded")} ${d.added}`
|
|
6373
|
+
}),
|
|
6374
|
+
h("div", {
|
|
6375
|
+
className: "mneme-sinkbar mneme-sinkbar--merged",
|
|
6376
|
+
style: { height: `${Math.max((d.merged / sinkMax) * 100, d.merged > 0 ? 1 : 0)}%` },
|
|
6377
|
+
title: `${d.key} · ${t("memory.status.token.sinkMerged")} ${d.merged}`
|
|
6378
|
+
})
|
|
6379
|
+
))
|
|
6380
|
+
),
|
|
6381
|
+
sinkDays && !sinkError
|
|
6382
|
+
? h("div", { className: "mneme-sinklabels" },
|
|
6383
|
+
sinkDays.map((d) => h("span", { key: d.key }, d.key.slice(5)))
|
|
6384
|
+
)
|
|
6385
|
+
: null,
|
|
6386
|
+
// 图例只在有数据时给:加载中把三个 0 画出来等于谎报「最近没东西进库」。
|
|
6387
|
+
sinkDays && !sinkError
|
|
6388
|
+
? h("div", { className: "mneme-sinklegend" },
|
|
6389
|
+
h("span", null, `${t("memory.status.token.sinkAdded")} `, h("b", null, sinkAddedTotal.toLocaleString())),
|
|
6390
|
+
h("span", null, `${t("memory.status.token.sinkMerged")} `, h("b", null, sinkMergedTotal.toLocaleString())),
|
|
6391
|
+
h("span", null, `${t("memory.status.token.sinkArchived")} `, h("b", null, sinkArchivedTotal.toLocaleString())),
|
|
6392
|
+
h("span", null, `${t("memory.status.token.sinkGap")} `, h("b", null, sinkGap))
|
|
6393
|
+
)
|
|
6394
|
+
: null,
|
|
6395
|
+
h("div", { className: "mneme-statuscap", style: { marginTop: "6px", fontSize: "11px" } }, t("memory.status.token.sinkFoot"))
|
|
5911
6396
|
)
|
|
5912
|
-
),
|
|
5913
|
-
h("div", { className: "mneme-heatlegend" },
|
|
5914
|
-
h("span", null, t("memory.status.token.heatLess")),
|
|
5915
|
-
h("span", { className: "mneme-heatcell" }),
|
|
5916
|
-
[1, 2, 3, 4].map((lv) => h("span", { key: lv, className: `mneme-heatcell l${lv}` })),
|
|
5917
|
-
h("span", null, t("memory.status.token.heatMore"))
|
|
5918
6397
|
)
|
|
5919
6398
|
)
|
|
5920
6399
|
);
|
|
@@ -7011,6 +7490,98 @@ window.__ModuleLoader__.load({
|
|
|
7011
7490
|
);
|
|
7012
7491
|
}
|
|
7013
7492
|
|
|
7493
|
+
// --- 从目录引导弹层(issue #386 第 6 条)---
|
|
7494
|
+
// 后端能力早在 #220 就实现了(src/bootstrap.js:六类确定性产物,零 LLM,吃
|
|
7495
|
+
// saveWithDedupe 的 (type,title,scope) 去重),但只有 Bearer 数据面一条入口,
|
|
7496
|
+
// 面板够不着——用户照文档找过来,发现产品界面里没有这个东西。
|
|
7497
|
+
//
|
|
7498
|
+
// 与 ImportDialog 的两处刻意的不同:
|
|
7499
|
+
// 1. 成功路径**不**重复 setBusy(false)。这里没有「同键再提交一次会追加正文」的
|
|
7500
|
+
// 风险(bootstrap 是 _overwrite 刷新),但 900ms 的提示阅读时间同样成立;
|
|
7501
|
+
// 更要紧的是这一段里组件仍挂着,按钮若重新可点,人会以为「没反应」再点一次。
|
|
7502
|
+
// 定时器句柄存进 ref,卸载时清掉——否则第二次提交的结果会写进已卸载的组件
|
|
7503
|
+
// (#386 第 6 条评审)。
|
|
7504
|
+
// 2. 错误按服务端的码分开说。「目录不存在」「不是目录」「能力没接线」三种情况下
|
|
7505
|
+
// 用户该做的事完全不同,统一成一句「引导失败」等于让他自己猜。
|
|
7506
|
+
function BootstrapDialog({ t, onClose, onImported, onBusyChange }) {
|
|
7507
|
+
const [dir, setDir] = useState("");
|
|
7508
|
+
const [busy, setBusy] = useState(false);
|
|
7509
|
+
const [msg, setMsg] = useState("");
|
|
7510
|
+
const [err, setErr] = useState("");
|
|
7511
|
+
const timerRef = useRef(null);
|
|
7512
|
+
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
|
7513
|
+
// 忙碌状态要让外面也知道:菜单那个「⋯」在请求进行中若把弹层卸掉,等于绕过
|
|
7514
|
+
// 了下面那两个 disabled 的按钮,请求还在飞、界面已经没了(评审 #4452245292)。
|
|
7515
|
+
useEffect(() => { if (onBusyChange) onBusyChange(busy); }, [busy, onBusyChange]);
|
|
7516
|
+
useEffect(() => () => { if (onBusyChange) onBusyChange(false); }, [onBusyChange]);
|
|
7517
|
+
const submit = () => {
|
|
7518
|
+
if (busy) return;
|
|
7519
|
+
const target = String(dir || "").trim();
|
|
7520
|
+
if (!target) { setErr(t("memory.explorer.bootstrapNoDir")); return; }
|
|
7521
|
+
setBusy(true);
|
|
7522
|
+
// 同步告诉父组件,别等下面那个 effect:effect 要等这次渲染提交后才跑,
|
|
7523
|
+
// 中间那一拍父组件的 bootstrapBusy 还是 false,此时点「⋯」仍会把弹层卸掉,
|
|
7524
|
+
// 请求的成功或失败就再没有地方显示(评审 #4452539705)。
|
|
7525
|
+
if (onBusyChange) onBusyChange(true);
|
|
7526
|
+
setErr("");
|
|
7527
|
+
apiFetch("/api/dsh-mneme/bootstrap", {
|
|
7528
|
+
method: "POST",
|
|
7529
|
+
headers: { "Content-Type": "application/json" },
|
|
7530
|
+
// 传原串(不 trim):前后空格在多数文件系统里是合法路径的一部分,
|
|
7531
|
+
// 服务端也只是 trim 判空、回传原值。
|
|
7532
|
+
body: JSON.stringify({ dir })
|
|
7533
|
+
})
|
|
7534
|
+
.then((res) => res.json().then((d) => ({ ok: res.ok, d })))
|
|
7535
|
+
.then(({ ok, d }) => {
|
|
7536
|
+
if (!ok) {
|
|
7537
|
+
const code = d && d.error;
|
|
7538
|
+
const key = "memory.explorer.bootstrapErr." + String(code || "");
|
|
7539
|
+
const localized = t(key);
|
|
7540
|
+
// t() 对未知键原样返回键名——那就退回通用文案,别把键名甩给用户。
|
|
7541
|
+
setErr(localized === key ? t("memory.explorer.bootstrapFailed") : localized);
|
|
7542
|
+
setBusy(false);
|
|
7543
|
+
return;
|
|
7544
|
+
}
|
|
7545
|
+
setMsg(t("memory.explorer.bootstrapDone")
|
|
7546
|
+
.replace("{n}", String((d && d.created) ?? 0))
|
|
7547
|
+
.replace("{m}", String((d && d.merged) ?? 0)));
|
|
7548
|
+
// onImported 同步调,只把 onClose 留在定时器里:切视图会卸载本组件并清掉
|
|
7549
|
+
// timerRef,若刷新也排在那 900ms 之后,这一步就静默丢了——库已经写进去了,
|
|
7550
|
+
// 列表却还是旧的(评审 #4452539711)。
|
|
7551
|
+
onImported();
|
|
7552
|
+
timerRef.current = setTimeout(() => onClose(), 900);
|
|
7553
|
+
})
|
|
7554
|
+
.catch(() => { setBusy(false); setErr(t("memory.explorer.bootstrapFailed")); });
|
|
7555
|
+
};
|
|
7556
|
+
return h("div", { className: "mneme-impdialog", role: "region", "aria-label": t("memory.explorer.bootstrapTitle") },
|
|
7557
|
+
h("div", { className: "mneme-set-title" }, t("memory.explorer.bootstrapTitle")),
|
|
7558
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.bootstrapHint")),
|
|
7559
|
+
h("div", { className: "mneme-improw" },
|
|
7560
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.bootstrapDir")),
|
|
7561
|
+
// 用文本框而不是 <input webkitdirectory>:后者给的是「选了哪个目录里的
|
|
7562
|
+
// 一批文件」,拿不到目录本身,而 bootstrap 要的正是那个目录。
|
|
7563
|
+
h("input", {
|
|
7564
|
+
className: "mneme-set-input",
|
|
7565
|
+
style: { marginBottom: 0, flex: 1, minWidth: 0 },
|
|
7566
|
+
type: "text",
|
|
7567
|
+
value: dir,
|
|
7568
|
+
placeholder: "/path/to/project",
|
|
7569
|
+
// 请求进行中锁住输入框:否则用户改成 B,回来的提示就贴在 B 旁边,
|
|
7570
|
+
// 而它说的是 A 的结果(评审 #4452245279)。
|
|
7571
|
+
disabled: busy,
|
|
7572
|
+
onChange: (e) => { setDir(e.target.value); setErr(""); }
|
|
7573
|
+
})
|
|
7574
|
+
),
|
|
7575
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
7576
|
+
msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
|
|
7577
|
+
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
7578
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
|
|
7579
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: submit },
|
|
7580
|
+
busy ? t("memory.explorer.bootstraping") : t("memory.explorer.bootstrapConfirm"))
|
|
7581
|
+
)
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
7584
|
+
|
|
7014
7585
|
// --- 分享包弹层 ---
|
|
7015
7586
|
// 这一面的支点只有一句:**看见 report 之前不给下载**。服务端一次 POST 就把包和报告
|
|
7016
7587
|
// 一起交回,所以这一格拦不住靠协议、只能靠界面自己:CLI 那侧没人拦得住,面板至少要让
|
|
@@ -7454,7 +8025,7 @@ window.__ModuleLoader__.load({
|
|
|
7454
8025
|
);
|
|
7455
8026
|
}
|
|
7456
8027
|
|
|
7457
|
-
function MemoryExplorer({ t }) {
|
|
8028
|
+
function MemoryExplorer({ t, onBusyChange }) {
|
|
7458
8029
|
const [view, setView] = useState("memory"); // memory | entity | status | settings
|
|
7459
8030
|
const [items, setItems] = useState([]); // loaded browse pages, chrono desc
|
|
7460
8031
|
const [total, setTotal] = useState(0); // server count for the current type filter
|
|
@@ -7490,6 +8061,21 @@ window.__ModuleLoader__.load({
|
|
|
7490
8061
|
const [pendingIds, setPendingIds] = useState(() => new Set());
|
|
7491
8062
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
7492
8063
|
const [importOpen, setImportOpen] = useState(false);
|
|
8064
|
+
const [bootstrapOpen, setBootstrapOpen] = useState(false);
|
|
8065
|
+
// 引导请求是否在飞:「⋯」按钮在忙碌时必须不关弹层(评审 #4452245292)。
|
|
8066
|
+
const [bootstrapBusy, setBootstrapBusy] = useState(false);
|
|
8067
|
+
// 一路直接抬到外层 MemoryOverlay:它要在 Esc / 背板 / ✕ 三条关闭路径上一起挡
|
|
8068
|
+
// (评审 #4453225573)。刻意**不**用 useEffect 转报——那要等渲染提交后才跑,
|
|
8069
|
+
// 中间那一拍外层守卫读到的是 false,正是 #4452539705 那条的老毛病。
|
|
8070
|
+
// 走 BootstrapDialog 的回调,它的 submit 里是同步调的。
|
|
8071
|
+
// better-sidebar 那个 tab 注册时不传 onBusyChange,所以这里必须容忍 undefined。
|
|
8072
|
+
// 身份必须稳定:BootstrapDialog 的卸载 effect 依赖 [onBusyChange],父组件每
|
|
8073
|
+
// 重渲染一次就会先跑 cleanup 调 onBusyChange(false)——而父组件恰好是在
|
|
8074
|
+
// setBootstrapBusy(true) 之后重渲染的,那一抹等于把刚置上的忙碌标记当场抹掉。
|
|
8075
|
+
const reportBootstrapBusy = useCallback((v) => {
|
|
8076
|
+
setBootstrapBusy(v);
|
|
8077
|
+
if (onBusyChange) onBusyChange(v);
|
|
8078
|
+
}, [onBusyChange]);
|
|
7493
8079
|
const [createOpen, setCreateOpen] = useState(false);
|
|
7494
8080
|
const [shareOpen, setShareOpen] = useState(false);
|
|
7495
8081
|
const [receiveOpen, setReceiveOpen] = useState(false);
|
|
@@ -7593,7 +8179,10 @@ window.__ModuleLoader__.load({
|
|
|
7593
8179
|
.catch(() => { if (!cancelled) setRemoteItems([]); });
|
|
7594
8180
|
}, 250);
|
|
7595
8181
|
return () => { cancelled = true; clearTimeout(timer); };
|
|
7596
|
-
|
|
8182
|
+
// reloadKey 必须进依赖:导入/引导/接收成功时只递增它,而搜索态下 items 走
|
|
8183
|
+
// remoteItems 那条路——少了这一格,搜索词不变就永远看不到刚进来的新行
|
|
8184
|
+
// (评审 #4452245305)。
|
|
8185
|
+
}, [query, semantic, searchTopK, reloadKey]);
|
|
7597
8186
|
|
|
7598
8187
|
// Live view: while the memory tab is open, quietly re-fetch page 1
|
|
7599
8188
|
// every 30s (and on every activation) so fresh memories surface
|
|
@@ -7785,7 +8374,18 @@ window.__ModuleLoader__.load({
|
|
|
7785
8374
|
setSelectedId(target.id);
|
|
7786
8375
|
};
|
|
7787
8376
|
|
|
8377
|
+
// 引导请求在飞时不许离开记忆视图(评审 #4453639221):`.mneme-xtools` 整块挂在
|
|
8378
|
+
// `view === "memory"` 之下,BootstrapDialog 就在里面。任何离开这个视图的动作都会
|
|
8379
|
+
// 连它一起卸载——请求还在飞、界面已经没了,那句「新建几条、并入几条」就再没有
|
|
8380
|
+
// 地方显示。这里只拦「离开」的路径:jumpToMemory / browseWithFilter 是回到
|
|
8381
|
+
// memory,不拦。
|
|
8382
|
+
const leaveMemory = (key) => {
|
|
8383
|
+
if (bootstrapBusy) return;
|
|
8384
|
+
setView(key);
|
|
8385
|
+
};
|
|
8386
|
+
|
|
7788
8387
|
const openGraphFor = (name) => {
|
|
8388
|
+
if (bootstrapBusy) return;
|
|
7789
8389
|
setGraphFocus(name || "");
|
|
7790
8390
|
setView("entity");
|
|
7791
8391
|
};
|
|
@@ -7814,9 +8414,14 @@ window.__ModuleLoader__.load({
|
|
|
7814
8414
|
h("button", {
|
|
7815
8415
|
key: s.key,
|
|
7816
8416
|
type: "button",
|
|
8417
|
+
// 引导请求在飞时四个视图标签一律禁用(评审 #4453639221):离开 memory
|
|
8418
|
+
// 会把整个 `.mneme-xtools` 连同里面的 BootstrapDialog 卸掉。onClick 仍
|
|
8419
|
+
// 走 leaveMemory 而不是 setView——disabled 只是第一道,第二道在函数里,
|
|
8420
|
+
// 免得将来有人给标签换一种可点的实现就绕过去了。
|
|
8421
|
+
disabled: bootstrapBusy,
|
|
7817
8422
|
className: view === s.key ? "mneme-vtab mneme-active" : "mneme-vtab",
|
|
7818
8423
|
"aria-pressed": String(view === s.key),
|
|
7819
|
-
onClick: () =>
|
|
8424
|
+
onClick: () => leaveMemory(s.key)
|
|
7820
8425
|
},
|
|
7821
8426
|
h(Icon, { name: s.key === "memory" ? "database" : s.key === "entity" ? "waypoints" : s.key === "status" ? "activity" : "settings", size: 14, className: "mneme-vtabico" }),
|
|
7822
8427
|
s.label
|
|
@@ -7843,20 +8448,41 @@ window.__ModuleLoader__.load({
|
|
|
7843
8448
|
"aria-label": t("memory.explorer.more"),
|
|
7844
8449
|
"aria-expanded": String(menuOpen),
|
|
7845
8450
|
title: t("memory.explorer.more"),
|
|
7846
|
-
|
|
8451
|
+
// 引导请求在飞时整个「⋯」都不给开:菜单与引导弹层共用同一个
|
|
8452
|
+
// 绝对定位锚点、同一层 z-index,弹层排在菜单之后渲染,会把
|
|
8453
|
+
// 菜单顶上的两项导出盖住——它们没有 disabled,却点不到
|
|
8454
|
+
// (评审 #4453225585)。不开菜单,就不存在「可点但被遮住」。
|
|
8455
|
+
// 这也顺带把 #4452245292 那条锁得更死:菜单开不了,就没有
|
|
8456
|
+
// 任何路径能在请求中途把引导弹层卸掉。
|
|
8457
|
+
disabled: bootstrapBusy,
|
|
8458
|
+
onClick: () => {
|
|
8459
|
+
setMenuOpen(!menuOpen);
|
|
8460
|
+
setImportOpen(false);
|
|
8461
|
+
// 引导弹层只在不忙的时候才关:它内部已经用 busy 禁用了取消与提交,
|
|
8462
|
+
// 这里若照旧卸载,等于给了一条绕过那两道锁的路(评审 #4452245292)。
|
|
8463
|
+
if (!bootstrapBusy) setBootstrapOpen(false);
|
|
8464
|
+
setCreateOpen(false);
|
|
8465
|
+
setShareOpen(false);
|
|
8466
|
+
setReceiveOpen(false);
|
|
8467
|
+
}
|
|
7847
8468
|
}, "⋯"),
|
|
7848
8469
|
menuOpen && h("div", { className: "mneme-menu", role: "menu" },
|
|
7849
|
-
|
|
8470
|
+
// 引导请求在飞时,会挂弹层的那几项一律 disabled:这些弹层共用同一个
|
|
8471
|
+
// 绝对定位锚点,后渲染的会盖住引导弹层,它那句「新建几条、并入几条」
|
|
8472
|
+
// 就没地方看了(评审 #4452539738)。导出两项不挂弹层,照旧可点。
|
|
8473
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setCreateOpen(true); setMenuOpen(false); } },
|
|
7850
8474
|
t("memory.explorer.newMemory")),
|
|
7851
8475
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("json"); } },
|
|
7852
8476
|
t("memory.explorer.exportJson")),
|
|
7853
8477
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("markdown"); } },
|
|
7854
8478
|
t("memory.explorer.exportMarkdown")),
|
|
7855
|
-
h("button", { type: "button", role: "menuitem", onClick: () => { setImportOpen(true); setMenuOpen(false); } },
|
|
8479
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setImportOpen(true); setMenuOpen(false); } },
|
|
7856
8480
|
t("memory.explorer.importMd")),
|
|
7857
|
-
h("button", { type: "button", role: "menuitem", onClick: () => {
|
|
8481
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setBootstrapOpen(true); setMenuOpen(false); } },
|
|
8482
|
+
t("memory.explorer.bootstrap")),
|
|
8483
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setShareOpen(true); setMenuOpen(false); } },
|
|
7858
8484
|
t("memory.explorer.sharePack")),
|
|
7859
|
-
h("button", { type: "button", role: "menuitem", onClick: () => { setReceiveOpen(true); setMenuOpen(false); } },
|
|
8485
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setReceiveOpen(true); setMenuOpen(false); } },
|
|
7860
8486
|
t("memory.explorer.receivePack")),
|
|
7861
8487
|
),
|
|
7862
8488
|
importOpen && h(ImportDialog, {
|
|
@@ -7864,6 +8490,12 @@ window.__ModuleLoader__.load({
|
|
|
7864
8490
|
onClose: () => setImportOpen(false),
|
|
7865
8491
|
onImported: () => setReloadKey((k) => k + 1)
|
|
7866
8492
|
}),
|
|
8493
|
+
bootstrapOpen && h(BootstrapDialog, {
|
|
8494
|
+
t,
|
|
8495
|
+
onClose: () => setBootstrapOpen(false),
|
|
8496
|
+
onBusyChange: reportBootstrapBusy,
|
|
8497
|
+
onImported: () => setReloadKey((k) => k + 1)
|
|
8498
|
+
}),
|
|
7867
8499
|
shareOpen && h(ShareDialog, { t, onClose: () => setShareOpen(false) }),
|
|
7868
8500
|
receiveOpen && h(ReceiveDialog, {
|
|
7869
8501
|
t,
|
|
@@ -8141,8 +8773,10 @@ window.__ModuleLoader__.load({
|
|
|
8141
8773
|
// 入口形状跟着宿主 v0.2 的侧边栏走,分两态:
|
|
8142
8774
|
// · 展开态:宿主把「插件 / 自动化任务」这类全局面板渲染成 nav.panelList
|
|
8143
8775
|
// 里的 panelRow(36px 行高、透明底、图标 + 标题)。我们把按钮 portal 进
|
|
8144
|
-
// 同一组并原样套用宿主 panelRow
|
|
8145
|
-
// 而不是 v0.8.10
|
|
8776
|
+
// 同一组并原样套用宿主 panelRow 的实时类名,于是它是该组的一行——低调,
|
|
8777
|
+
// 而不是 v0.8.10 那样长得像第二个高亮「新会话」按钮。落点是「更多」行
|
|
8778
|
+
// 之前而不是组尾:宿主 0.2.2 起把「更多」追加进同一个 nav,组尾已不再是
|
|
8779
|
+
// 面板行,落在它下面会被展开的菜单盖住(#573)。
|
|
8146
8780
|
// · 收起态(Windows 标题栏):panelList / regionArea / footArea 被宿主
|
|
8147
8781
|
// display:none,只有开关与「新会话」被改成固定在标题栏上。我们把按钮搬回
|
|
8148
8782
|
// 标题栏、并排在「新会话」右侧(84px / 120px 的算法见上面 CSS 注释),
|
|
@@ -8217,6 +8851,33 @@ window.__ModuleLoader__.load({
|
|
|
8217
8851
|
return h("span", { className: "mneme-entrybadge", "aria-hidden": "true" },
|
|
8218
8852
|
pending > 99 ? "99+" : String(pending));
|
|
8219
8853
|
}
|
|
8854
|
+
// --- 宿主内整页面板(新版宿主):侧栏图标 + 中列整页 ---
|
|
8855
|
+
// 首选承载方式:记忆库直接长在宿主中列里,与会话平级,不再叠一层背板
|
|
8856
|
+
// 浮层。老宿主没声明 sidebar.panellist 时退回 footer.action + 浮层(见 apply)。
|
|
8857
|
+
function SidebarPanelGlyph({ size }) {
|
|
8858
|
+
// 红点复用模块级单例:图标与回落按钮共享同一个计数、同一条 60s 兜底轮询。
|
|
8859
|
+
const pending = useConflictBadgeCount();
|
|
8860
|
+
// 宿主 PanelRow 已经自带 button / aria-label / Tooltip / active 高亮,
|
|
8861
|
+
// 这里只交一颗图形——再造一层按钮会套出两颗可聚焦元素,Tab 要停两次,
|
|
8862
|
+
// 而且 host 的 selectPanel 也轮不到我们调。
|
|
8863
|
+
return h("span", { className: "mneme-paneliglyph" },
|
|
8864
|
+
renderMnemeMark({ size: size || 18 }),
|
|
8865
|
+
h(ConflictBadge, { pending })
|
|
8866
|
+
);
|
|
8867
|
+
}
|
|
8868
|
+
|
|
8869
|
+
function MnemeMainPanel({ t }) {
|
|
8870
|
+
// 进面板 = 用户准备处理冻结冲突:重取一次红点计数。理由与浮层时代
|
|
8871
|
+
// openLibrary 里那次重取相同——裁决走别的路径或后台刚写过库时,计数
|
|
8872
|
+
// 不该等 60s 兜底轮询才对齐。
|
|
8873
|
+
useEffect(() => { conflictBadgeCount.refresh(); }, []);
|
|
8874
|
+
// 不自带「回到会话」按钮:宿主的 openSession / 新建会话都会
|
|
8875
|
+
// selectPanel(null),用户点侧栏任何会话行就回得去(baleen 同样不做)。
|
|
8876
|
+
return h("div", { className: "mneme-panel" },
|
|
8877
|
+
h(MemoryExplorer, { t })
|
|
8878
|
+
);
|
|
8879
|
+
}
|
|
8880
|
+
|
|
8220
8881
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
8221
8882
|
const pending = useConflictBadgeCount();
|
|
8222
8883
|
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
@@ -8260,6 +8921,12 @@ window.__ModuleLoader__.load({
|
|
|
8260
8921
|
const list = findPanelList(region);
|
|
8261
8922
|
return list ? list.querySelector('[class*="panelRow"]:not(.mneme-topentry-native)') : null;
|
|
8262
8923
|
};
|
|
8924
|
+
// 面板列表里的「更多」行(宿主 PanelMoreMenu 的锚点)。宿主 0.2.2 起先渲染
|
|
8925
|
+
// 面板行、再把「更多」追加进同一个 nav,于是列表最后一项不再是面板行——
|
|
8926
|
+
// 我们的入口若仍 appendChild 到末尾就会落在它下面,菜单一展开就盖住入口
|
|
8927
|
+
// (#573)。按 [class*=] 前缀找,和其余几处一样不写死哈希类名;老宿主
|
|
8928
|
+
// 没有这行时返回 null,退回追加到末尾的老行为。
|
|
8929
|
+
const findMoreAnchor = (list) => list.querySelector('[class*="moreAnchor"]');
|
|
8263
8930
|
// 占位锚点:新会话行(现役外壳里 newSession 按钮嵌在 logoRow 内,
|
|
8264
8931
|
// 旧外壳是根的直接子按钮)。判定与生态插件的 sidebar-entry-core
|
|
8265
8932
|
// 一致;锚点不可靠时回退到 regionArea 之前的旧位置。
|
|
@@ -8285,6 +8952,14 @@ window.__ModuleLoader__.load({
|
|
|
8285
8952
|
// 列表时退回「新会话」行之后,由 .mneme-topentry-native 兜住低调行样式。
|
|
8286
8953
|
const list = wide ? findPanelList(region) : null;
|
|
8287
8954
|
if (list) {
|
|
8955
|
+
// 落在面板组末尾、但在「更多」行之前(#573);没有「更多」行时组尾
|
|
8956
|
+
// 就是终点,退回老行为。
|
|
8957
|
+
const more = findMoreAnchor(list);
|
|
8958
|
+
if (more) {
|
|
8959
|
+
if (created.parentElement === list && created.nextElementSibling === more) return;
|
|
8960
|
+
list.insertBefore(created, more);
|
|
8961
|
+
return;
|
|
8962
|
+
}
|
|
8288
8963
|
if (created.parentElement === list && created === list.lastElementChild) return;
|
|
8289
8964
|
list.appendChild(created);
|
|
8290
8965
|
return;
|
|
@@ -9477,13 +10152,214 @@ window.__ModuleLoader__.load({
|
|
|
9477
10152
|
const t = ctx.locale.bind(NS);
|
|
9478
10153
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
9479
10154
|
|
|
9480
|
-
//
|
|
9481
|
-
//
|
|
9482
|
-
//
|
|
9483
|
-
//
|
|
9484
|
-
//
|
|
9485
|
-
|
|
9486
|
-
|
|
10155
|
+
// --- 入口座位 ---
|
|
10156
|
+
// --- chorography(仓库地志)面板(M1,spec §7)---------------------
|
|
10157
|
+
// 浏览页:左模块树 / 右正文 / 顶部检索。数据从 /choro/* 四条读路由来,
|
|
10158
|
+
// 真源是仓库磁盘的 MD。座位无条件注册(P11):inject 同名二次注册的
|
|
10159
|
+
// cordis 语义没验证过,不赌;chorographyEnabled 关着时页面渲染「未启用」
|
|
10160
|
+
// 空态(路由 409 disabled),开关生效走既有「重启生效」纪律——座位常挂
|
|
10161
|
+
// 的收敛形态就是 D2 拆双 Cordis 条目时的样子。
|
|
10162
|
+
function WikiGlyph({ size }) {
|
|
10163
|
+
// 图形走 currentColor,粗细对比交给宿主的 active 态;不引入新色值。
|
|
10164
|
+
return h("span", { className: "mneme-paneliglyph", "aria-hidden": "true" },
|
|
10165
|
+
h("svg", {
|
|
10166
|
+
width: size || 18, height: size || 18, viewBox: "0 0 24 24",
|
|
10167
|
+
fill: "none", stroke: "currentColor", strokeWidth: 1.8,
|
|
10168
|
+
strokeLinecap: "round", strokeLinejoin: "round"
|
|
10169
|
+
},
|
|
10170
|
+
h("path", { d: "M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2z" }),
|
|
10171
|
+
h("path", { d: "M9 4v14" }),
|
|
10172
|
+
h("path", { d: "M15 6v14" })
|
|
10173
|
+
)
|
|
10174
|
+
);
|
|
10175
|
+
}
|
|
10176
|
+
|
|
10177
|
+
// 只读 MD 渲染(P13):React 文本节点天然转义,不经过 innerHTML,无原始
|
|
10178
|
+
// HTML 通道。frontmatter 折叠不显示;表格按等宽行渲染(知识库页的表都是
|
|
10179
|
+
// 两三列的清单,保真优先于交互);`code` 拆成等宽 span。
|
|
10180
|
+
function renderWikiMarkdown(content) {
|
|
10181
|
+
const lines = String(content ?? "").split("\n");
|
|
10182
|
+
let i = 0;
|
|
10183
|
+
// frontmatter:首行是 --- 时吃到下一行 ---
|
|
10184
|
+
if (lines[0] && lines[0].trim() === "---") {
|
|
10185
|
+
i = 1;
|
|
10186
|
+
while (i < lines.length && lines[i].trim() !== "---") i++;
|
|
10187
|
+
i++;
|
|
10188
|
+
}
|
|
10189
|
+
const out = [];
|
|
10190
|
+
const inline = (text) => String(text).split(/`([^`]+)`/).map((part, idx) =>
|
|
10191
|
+
idx % 2 === 1 ? h("code", { key: idx, className: "mneme-wikicode" }, part) : part
|
|
10192
|
+
);
|
|
10193
|
+
let list = null; // "ul" | "ol" | null
|
|
10194
|
+
const flushList = () => {
|
|
10195
|
+
if (list) { out.push(h(list, { key: out.length }, listItems)); listItems = []; list = null; }
|
|
10196
|
+
};
|
|
10197
|
+
let listItems = [];
|
|
10198
|
+
let inCode = false;
|
|
10199
|
+
let codeLines = [];
|
|
10200
|
+
for (; i < lines.length; i++) {
|
|
10201
|
+
const line = lines[i];
|
|
10202
|
+
if (line.trim().startsWith("```")) {
|
|
10203
|
+
if (inCode) {
|
|
10204
|
+
out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10205
|
+
codeLines = []; inCode = false;
|
|
10206
|
+
} else { flushList(); inCode = true; }
|
|
10207
|
+
continue;
|
|
10208
|
+
}
|
|
10209
|
+
if (inCode) { codeLines.push(line); continue; }
|
|
10210
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
10211
|
+
if (heading) {
|
|
10212
|
+
flushList();
|
|
10213
|
+
const level = Math.min(heading[1].length + 1, 5);
|
|
10214
|
+
out.push(h(`h${level}`, { key: out.length }, inline(heading[2])));
|
|
10215
|
+
continue;
|
|
10216
|
+
}
|
|
10217
|
+
const ul = line.match(/^\s*[-*]\s+(.*)$/);
|
|
10218
|
+
const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
10219
|
+
if (ul || ol) {
|
|
10220
|
+
const want = ul ? "ul" : "ol";
|
|
10221
|
+
if (list !== want) flushList();
|
|
10222
|
+
list = want;
|
|
10223
|
+
listItems.push(h("li", { key: listItems.length }, inline(ul ? ul[1] : ol[1])));
|
|
10224
|
+
continue;
|
|
10225
|
+
}
|
|
10226
|
+
flushList();
|
|
10227
|
+
if (/^\s*\|/.test(line)) { out.push(h("div", { key: out.length, className: "mneme-wikitablerow" }, inline(line.trim()))); continue; }
|
|
10228
|
+
if (/^>\s?/.test(line)) { out.push(h("blockquote", { key: out.length, className: "mneme-wikiquote" }, inline(line.replace(/^>\s?/, "")))); continue; }
|
|
10229
|
+
if (line.trim() === "") continue;
|
|
10230
|
+
out.push(h("p", { key: out.length }, inline(line)));
|
|
10231
|
+
}
|
|
10232
|
+
flushList();
|
|
10233
|
+
if (inCode && codeLines.length > 0) out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10234
|
+
return out;
|
|
10235
|
+
}
|
|
10236
|
+
|
|
10237
|
+
function WikiMainPanel({ t }) {
|
|
10238
|
+
const [state, setState] = useState({ phase: "loading", repos: [], repo: null, modules: [], edges: [], page: null, hits: [], q: "", selected: null, disabled: false, error: null });
|
|
10239
|
+
// apiFetch 是裸 fetch:非 2xx 不抛错,返回 Response。这里统一解 JSON 并把
|
|
10240
|
+
// 状态码带回来——409 disabled(用户没开)/ 503 unavailable(装配缺件)
|
|
10241
|
+
// 各有各的空态文案(#497 的语义分叉在这里接住)。
|
|
10242
|
+
const readJson = async (res) => {
|
|
10243
|
+
let data = {};
|
|
10244
|
+
try { data = await res.json(); } catch { /* 空响应体 */ }
|
|
10245
|
+
return { status: res.status, data };
|
|
10246
|
+
};
|
|
10247
|
+
const load = useCallback(async (repoRef) => {
|
|
10248
|
+
try {
|
|
10249
|
+
const { status, data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/repos`));
|
|
10250
|
+
if (status === 409) { setState((s) => ({ ...s, phase: "disabled" })); return; }
|
|
10251
|
+
if (status === 503) { setState((s) => ({ ...s, phase: "unavailable" })); return; }
|
|
10252
|
+
const repos = data.repos ?? [];
|
|
10253
|
+
if (repos.length === 0) { setState((s) => ({ ...s, phase: "no-repo", repos: [] })); return; }
|
|
10254
|
+
const ref = repoRef ?? (repos.length === 1 ? repos[0].id : null);
|
|
10255
|
+
if (ref === null) { setState((s) => ({ ...s, phase: "pick-repo", repos })); return; }
|
|
10256
|
+
const mod = await readJson(await apiFetch(`/api/dsh-mneme/choro/modules?repo=${encodeURIComponent(ref)}`));
|
|
10257
|
+
if (mod.status !== 200) { setState((s) => ({ ...s, phase: "error", error: `modules ${mod.status}` })); return; }
|
|
10258
|
+
setState((s) => ({ ...s, phase: "ready", repos, repo: mod.data.repo, modules: mod.data.modules ?? [], edges: mod.data.edges ?? [], page: null, selected: null }));
|
|
10259
|
+
} catch (err) {
|
|
10260
|
+
setState((s) => ({ ...s, phase: "error", error: String(err?.message ?? err) }));
|
|
10261
|
+
}
|
|
10262
|
+
}, []);
|
|
10263
|
+
useEffect(() => { load(); }, [load]);
|
|
10264
|
+
const openPage = useCallback(async (slug) => {
|
|
10265
|
+
const repo = state.repo;
|
|
10266
|
+
if (!repo) return;
|
|
10267
|
+
setState((s) => ({ ...s, selected: slug, page: null }));
|
|
10268
|
+
try {
|
|
10269
|
+
const { status, data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/page?repo=${encodeURIComponent(repo.id)}&slug=${encodeURIComponent(slug)}`));
|
|
10270
|
+
if (status !== 200 || data.error) { setState((s) => ({ ...s, page: { error: data.error ?? `page ${status}` } })); return; }
|
|
10271
|
+
setState((s) => ({ ...s, page: data.page }));
|
|
10272
|
+
} catch (err) {
|
|
10273
|
+
setState((s) => ({ ...s, page: { error: String(err?.message ?? err) } }));
|
|
10274
|
+
}
|
|
10275
|
+
}, [state.repo]);
|
|
10276
|
+
const runSearch = useCallback(async (q) => {
|
|
10277
|
+
setState((s) => ({ ...s, q }));
|
|
10278
|
+
const repo = state.repo;
|
|
10279
|
+
if (!repo || !q.trim()) { setState((s) => ({ ...s, hits: [] })); return; }
|
|
10280
|
+
try {
|
|
10281
|
+
const { data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/search?repo=${encodeURIComponent(repo.id)}&q=${encodeURIComponent(q)}`));
|
|
10282
|
+
setState((s) => ({ ...s, hits: data.hits ?? [] }));
|
|
10283
|
+
} catch { setState((s) => ({ ...s, hits: [] })); }
|
|
10284
|
+
}, [state.repo]);
|
|
10285
|
+
|
|
10286
|
+
if (state.phase === "loading") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.state.loading"));
|
|
10287
|
+
if (state.phase === "disabled") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.empty.disabled"));
|
|
10288
|
+
if (state.phase === "unavailable") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.state.unavailable"));
|
|
10289
|
+
if (state.phase === "error") return h("div", { className: "mneme-panel mneme-wikiempty" }, `${t("wiki.state.error")}: ${state.error}`);
|
|
10290
|
+
if (state.phase === "no-repo") {
|
|
10291
|
+
return h("div", { className: "mneme-panel mneme-wikiempty" },
|
|
10292
|
+
h("div", { className: "mneme-wikiemptytitle" }, t("wiki.panel.open")),
|
|
10293
|
+
h("p", null, t("wiki.empty.noRepo")));
|
|
10294
|
+
}
|
|
10295
|
+
if (state.phase === "pick-repo") {
|
|
10296
|
+
return h("div", { className: "mneme-panel mneme-wikiempty" },
|
|
10297
|
+
h("div", { className: "mneme-wikiemptytitle" }, t("wiki.empty.pickRepo")),
|
|
10298
|
+
state.repos.map((r) => h("button", {
|
|
10299
|
+
key: r.id, type: "button", className: "mneme-wikitreeitem",
|
|
10300
|
+
onClick: () => load(r.id)
|
|
10301
|
+
}, `${r.name}`)));
|
|
10302
|
+
}
|
|
10303
|
+
const tree = [];
|
|
10304
|
+
for (const mod of state.modules) {
|
|
10305
|
+
const depth = (mod.slug.match(/-/g) || []).length;
|
|
10306
|
+
tree.push(h("button", {
|
|
10307
|
+
key: mod.slug,
|
|
10308
|
+
type: "button",
|
|
10309
|
+
className: `mneme-wikitreeitem${state.selected === mod.slug ? " mneme-active" : ""}`,
|
|
10310
|
+
style: { paddingLeft: `${8 + Math.min(depth, 4) * 14}px` },
|
|
10311
|
+
title: mod.title,
|
|
10312
|
+
onClick: () => openPage(mod.slug)
|
|
10313
|
+
}, mod.slug));
|
|
10314
|
+
}
|
|
10315
|
+
const pageBody = state.page
|
|
10316
|
+
? (state.page.error
|
|
10317
|
+
? h("div", { className: "mneme-wikiempty" }, state.page.error === "page-missing" ? t("wiki.state.pageMissing") : state.page.error)
|
|
10318
|
+
: renderWikiMarkdown(state.page.content))
|
|
10319
|
+
: h("div", { className: "mneme-wikiempty" }, t("wiki.state.pickPage"));
|
|
10320
|
+
return h("div", { className: "mneme-panel" },
|
|
10321
|
+
h("div", { className: "mneme-wikibar" },
|
|
10322
|
+
h("span", { className: "mneme-wikirepo" }, state.repo ? `${state.repo.name}` : ""),
|
|
10323
|
+
h("input", {
|
|
10324
|
+
className: "mneme-wikisearch",
|
|
10325
|
+
placeholder: t("wiki.search.placeholder"),
|
|
10326
|
+
value: state.q,
|
|
10327
|
+
onChange: (e) => runSearch(e.target.value),
|
|
10328
|
+
"aria-label": t("wiki.search.placeholder")
|
|
10329
|
+
})),
|
|
10330
|
+
h("div", { className: "mneme-wikibody" },
|
|
10331
|
+
h("nav", { className: "mneme-wikitree", "aria-label": t("wiki.tree.title") }, tree),
|
|
10332
|
+
h("div", { className: "mneme-wikipage" },
|
|
10333
|
+
state.hits.length > 0 && h("div", { className: "mneme-wikihits" },
|
|
10334
|
+
state.hits.map((hit, idx) => h("button", {
|
|
10335
|
+
key: `${hit.slug}-${hit.line}-${idx}`,
|
|
10336
|
+
type: "button",
|
|
10337
|
+
className: "mneme-wikihit",
|
|
10338
|
+
onClick: () => { setState((s) => ({ ...s, hits: [] })); openPage(hit.slug); }
|
|
10339
|
+
}, `${hit.slug} · ${hit.section} · L${hit.line}: ${hit.excerpt}`)),
|
|
10340
|
+
h("button", { type: "button", className: "mneme-wikihitsclose", onClick: () => setState((s) => ({ ...s, hits: [] })) }, "×")),
|
|
10341
|
+
pageBody)));
|
|
10342
|
+
}
|
|
10343
|
+
|
|
10344
|
+
// 两套宿主形态,二选一,靠宿主是否声明了 sidebar.panellist 判定:
|
|
10345
|
+
// · 宿主内整页面板(首选):sidebar.panellist 出图标、main 出中列整页。
|
|
10346
|
+
// 图标长在宿主自己的面板列表里(PanelRow 自己处理点击与 aria-current),
|
|
10347
|
+
// 插件只管渲染一颗 glyph,不自己造按钮、不自己调 selectPanel。
|
|
10348
|
+
// · 老宿主回落:没有这两个座位时,footer.action 出按钮 + 居中 sheet,
|
|
10349
|
+
// 并保留 SidebarTopEntry 的 DOM portal(尽量把入口放到面板列表位置)。
|
|
10350
|
+
// 两套都挂同一个模块级 overlay 状态,但不会同时装:panellist 一旦出现就
|
|
10351
|
+
// 先把回落拆掉,否则侧栏会冒出两个记忆入口。
|
|
10352
|
+
// 判定 + 装载都收在两个闭包里:谁都可以问「宿主有没有这套座位」,
|
|
10353
|
+
// 也都可以在条件变化后重装回落——但同一时刻至多一份注册在册。
|
|
10354
|
+
const hostHasPanelSeat = () => typeof ctx.slots.spec === "function"
|
|
10355
|
+
&& ctx.slots.spec("sidebar.panellist") !== undefined;
|
|
10356
|
+
let footerSeat = null;
|
|
10357
|
+
const dropFooterSeat = () => {
|
|
10358
|
+
if (footerSeat) { footerSeat(); footerSeat = null; }
|
|
10359
|
+
};
|
|
10360
|
+
const mountFooterSeat = () => {
|
|
10361
|
+
if (footerSeat || hostHasPanelSeat()) return;
|
|
10362
|
+
footerSeat = ctx.slots.register({
|
|
9487
10363
|
name: "sidebar.footer.action",
|
|
9488
10364
|
id: "dsh-mneme",
|
|
9489
10365
|
order: 0,
|
|
@@ -9496,6 +10372,56 @@ window.__ModuleLoader__.load({
|
|
|
9496
10372
|
}),
|
|
9497
10373
|
h(MemoryOverlay, { t })
|
|
9498
10374
|
));
|
|
10375
|
+
};
|
|
10376
|
+
|
|
10377
|
+
ctx.slots.inject("sidebar.panellist", () => {
|
|
10378
|
+
dropFooterSeat();
|
|
10379
|
+
const disposeGlyph = ctx.slots.register({
|
|
10380
|
+
name: "sidebar.panellist",
|
|
10381
|
+
id: "dsh-mneme",
|
|
10382
|
+
// 排在宿主自己的面板之后(baleen 占 45):记忆是高频入口,但排在
|
|
10383
|
+
// 已有面板之后不抢位。order 只影响列表排序,不影响可发现性。
|
|
10384
|
+
order: 60,
|
|
10385
|
+
label: () => t("memory.panel.open")
|
|
10386
|
+
}, (props) => h(SidebarPanelGlyph, { size: props && props.size }));
|
|
10387
|
+
// main 的 key 必须与上面侧栏的 id 逐字相同:宿主点面板行时调
|
|
10388
|
+
// layout.selectPanel(id),它按 options.key 在 main 槽里找 entry,
|
|
10389
|
+
// 对不上直接抛 "main panel ... is not registered"。
|
|
10390
|
+
const disposeMain = ctx.slots.register({
|
|
10391
|
+
name: "main",
|
|
10392
|
+
key: "dsh-mneme"
|
|
10393
|
+
}, () => h(MnemeMainPanel, { t }));
|
|
10394
|
+
// chorography 第二个座位(M1):成对规则同上——id 与 key 逐字同为
|
|
10395
|
+
// "dsh-mneme-wiki"。座位无条件注册(P11),内容层自己渲染空态;
|
|
10396
|
+
// D2 拆双 Cordis 条目后这里自然变成可独立禁用的形态。
|
|
10397
|
+
const disposeWikiGlyph = ctx.slots.register({
|
|
10398
|
+
name: "sidebar.panellist",
|
|
10399
|
+
id: "dsh-mneme-wiki",
|
|
10400
|
+
order: 61,
|
|
10401
|
+
label: () => t("wiki.panel.open")
|
|
10402
|
+
}, (props) => h(WikiGlyph, { size: props && props.size }));
|
|
10403
|
+
const disposeWikiMain = ctx.slots.register({
|
|
10404
|
+
name: "main",
|
|
10405
|
+
key: "dsh-mneme-wiki"
|
|
10406
|
+
}, () => h(WikiMainPanel, { t }));
|
|
10407
|
+
return () => {
|
|
10408
|
+
disposeGlyph();
|
|
10409
|
+
disposeMain();
|
|
10410
|
+
disposeWikiGlyph();
|
|
10411
|
+
disposeWikiMain();
|
|
10412
|
+
// 座位被撤(宿主折叠该槽 / 换宿主形态)时把回落装回去:两条路径都
|
|
10413
|
+
// 只靠各自的 inject 触发,谁不动谁就永远停在「没有入口」的状态。
|
|
10414
|
+
mountFooterSeat();
|
|
10415
|
+
};
|
|
10416
|
+
});
|
|
10417
|
+
|
|
10418
|
+
// 老宿主回落:footer.action 这一格既能当锚点(SidebarTopEntry 靠它挂
|
|
10419
|
+
// portal)也能当兜底按钮,新宿主上则整格不注册——否则侧栏两颗记忆入口。
|
|
10420
|
+
ctx.slots.inject("sidebar.footer.action", () => {
|
|
10421
|
+
mountFooterSeat();
|
|
10422
|
+
return () => {
|
|
10423
|
+
dropFooterSeat();
|
|
10424
|
+
};
|
|
9499
10425
|
});
|
|
9500
10426
|
|
|
9501
10427
|
// --- better-sidebar 生态 tab(可选软依赖)---
|