@modusensus/dsh-mneme 0.10.1 → 0.12.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 +17 -6
- package/bin/dsh-mneme-choro.mjs +254 -0
- package/docs/CONFIGURATION.md +10 -1
- package/lib/api-standalone.js +17 -21
- package/lib/api.js +414 -12
- package/lib/bootstrap-http.js +105 -0
- package/lib/bootstrap.js +5 -1
- package/lib/choro-command.js +104 -0
- package/lib/client.js +1637 -110
- package/lib/config-keys.js +144 -0
- package/lib/config.js +60 -1
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/index.js +87 -5
- package/lib/inject.js +18 -4
- package/lib/lang.js +82 -0
- package/lib/profile-facts.js +356 -0
- package/lib/quality-filter.js +48 -12
- package/lib/service.js +112 -4
- package/lib/settings.js +13 -1
- package/lib/store.js +978 -27
- package/lib/tools.js +244 -1
- package/lib/user-settings.js +63 -7
- 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 +360 -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/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +17 -21
- package/src/api.js +414 -12
- 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 +60 -1
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/index.js +87 -5
- package/src/inject.js +18 -4
- package/src/lang.js +82 -0
- package/src/profile-facts.js +356 -0
- package/src/quality-filter.js +48 -12
- package/src/service.js +112 -4
- package/src/settings.js +13 -1
- package/src/store.js +978 -27
- package/src/tools.js +244 -1
- package/src/user-settings.js +63 -7
- 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 +360 -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 +9 -1
- package/test/bootstrap.test.js +8 -0
- package/test/choro-command.test.js +218 -0
- package/test/client.test.js +802 -21
- package/test/config-keys-routes.test.js +233 -0
- package/test/config.test.js +79 -0
- package/test/contrast.test.js +14 -7
- package/test/document.test.js +32 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/inject.test.js +51 -0
- package/test/llm-audit.test.js +29 -1
- package/test/persona-settings.test.js +31 -0
- package/test/profile-facts.test.js +492 -0
- 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/session-gate.test.js +1 -1
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/wiki-api.test.js +273 -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 +270 -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 +176 -0
package/lib/client.js
CHANGED
|
@@ -289,6 +289,19 @@ window.__ModuleLoader__.load({
|
|
|
289
289
|
return fetch(path, { ...opts, headers });
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// 通用失效广播:任何改库动作(裁决/删除/编辑/归档/导入/接收/建条/回收…)
|
|
293
|
+
// 成功后派发一次。此前跨组件失效只有 mneme:conflicts-changed 一个事件、
|
|
294
|
+
// 三个状态侧监听者——记忆列表与冲突徽章、实体页、归档工作台全都不在名单里,
|
|
295
|
+
// 用户裁决完冲突要关掉面板重开才能看到败者归档。数据视图一律订阅本事件
|
|
296
|
+
// 立即重取;各动作自己的本地刷新(reloadKey / 列表过滤)保留,偶发的重复
|
|
297
|
+
// GET 换「永远不陈旧」,划算。与 mneme:conflicts-changed 并存不合并:后者
|
|
298
|
+
// 是裁决专属的精准信号,语义更窄,已有监听者不必搬家。
|
|
299
|
+
function broadcastStoreChanged(reason) {
|
|
300
|
+
try {
|
|
301
|
+
window.dispatchEvent(new CustomEvent("mneme:store-changed", { detail: { reason: reason || "" } }));
|
|
302
|
+
} catch { /* 非浏览器环境 */ }
|
|
303
|
+
}
|
|
304
|
+
|
|
292
305
|
// ⚠️ 'betterSidebar' 绝不能进模块级 inject(issue #88 实锤):loader 对
|
|
293
306
|
// inject 声明的服务是硬等待——未安装 better-sidebar 的环境里整个 entry
|
|
294
307
|
// 永远 pending("1 entry did not activate" → Failed to load plugins)。
|
|
@@ -301,6 +314,24 @@ window.__ModuleLoader__.load({
|
|
|
301
314
|
zh: {
|
|
302
315
|
"memory.panel.empty": "暂无记忆条目",
|
|
303
316
|
"memory.panel.open": "记忆",
|
|
317
|
+
"wiki.panel.open": "仓库地志",
|
|
318
|
+
"wiki.rebuild": "重建骨架",
|
|
319
|
+
"wiki.rebuilding": "重建中…",
|
|
320
|
+
"wiki.rebuild.done": "重建完成",
|
|
321
|
+
"wiki.rebuild.failed": "重建失败",
|
|
322
|
+
"wiki.rebuild.hint": "重跑 T0 确定性骨架(秒级;T1 语义富化请用 CLI --t1)。注意:富化页会退回骨架,语义段需重新生成",
|
|
323
|
+
"wiki.rebuild.modules": "个模块",
|
|
324
|
+
"wiki.rebuild.pages": "页",
|
|
325
|
+
"wiki.empty.noRepo": "还没有登记的仓库。先在仓库目录里跑 `dsh-mneme-choro <仓库路径> --apply` 生成骨架页并登记,然后回到这里。",
|
|
326
|
+
"wiki.empty.disabled": "chorography(仓库地志)未启用:在「设置」里打开「仓库地志」后重启 DSH 生效。",
|
|
327
|
+
"wiki.empty.pickRepo": "已登记多个仓库,选一个:",
|
|
328
|
+
"wiki.search.placeholder": "在页面里检索…",
|
|
329
|
+
"wiki.state.loading": "加载中…",
|
|
330
|
+
"wiki.state.pickPage": "从左侧选一个模块页。",
|
|
331
|
+
"wiki.state.pageMissing": "索引还在、磁盘页面缺失:在仓库目录重跑一次 `dsh-mneme-choro <仓库路径> --apply` 即可重建。",
|
|
332
|
+
"wiki.state.unavailable": "chorography 查询装配不可用(宿主环境缺件)。",
|
|
333
|
+
"wiki.state.error": "加载失败",
|
|
334
|
+
"wiki.tree.title": "模块",
|
|
304
335
|
"memory.tab.all": "全部",
|
|
305
336
|
"memory.tab.preference": "偏好",
|
|
306
337
|
"memory.tab.project": "项目",
|
|
@@ -322,16 +353,21 @@ window.__ModuleLoader__.load({
|
|
|
322
353
|
"memory.settings.profileSaved": "画像已保存",
|
|
323
354
|
"memory.settings.profileExtract": "从记忆提取草稿",
|
|
324
355
|
"memory.settings.profileExtracting": "正在提取…",
|
|
325
|
-
"memory.settings.profileExtractHint": "
|
|
356
|
+
"memory.settings.profileExtractHint": "会花一次模型调用,只在你点这个按钮时发生;提取出来的是草稿副本,只在这里展示——不会写进记忆库,也不会动你自己手写的「用户画像」。想留下它,点「复制草稿」。",
|
|
326
357
|
"memory.settings.profileExtractDisabled": "没有提取:功能开关里的「画像提取草稿」还没打开。",
|
|
327
358
|
"memory.settings.profileExtractEmpty": "库里没有可用的记忆条目,这次没有能提炼的内容(空输入下硬写一段画像只会是编的)。",
|
|
328
359
|
"memory.settings.profileExtractNoRoute": "没有可用的模型:先在宿主里选一个默认模型(或在配置里填 dreamProvider / dreamModel),再点一次。",
|
|
329
360
|
"memory.settings.profileSaveFailed": "画像没存进去",
|
|
330
361
|
"memory.settings.profileExtractFailed": "提取没成:模型没按格式返回,或这次调用失败了。你现有的画像一个字都没被改动。",
|
|
331
|
-
"memory.settings.profileDraftTitle": "
|
|
362
|
+
"memory.settings.profileDraftTitle": "草稿副本(不会自动保存)",
|
|
332
363
|
"memory.settings.profileDraftRows": "依据记忆条数",
|
|
364
|
+
"memory.status.entitiesSampled": "分类计数只统计最近 500 个实体;总数是全库",
|
|
365
|
+
"memory.status.entitiesUnknown": "总数读不到;下面是最近 500 个实体的分类计数",
|
|
333
366
|
"memory.settings.profileDraftTruncated": "条目较多,这次只读了前面一部分",
|
|
334
|
-
"memory.settings.
|
|
367
|
+
"memory.settings.profileDraftCopy": "复制草稿",
|
|
368
|
+
"memory.settings.profileDraftCopied": "已复制",
|
|
369
|
+
"memory.settings.profileDraftCopyHint": "「用户画像」那一栏只由你自己填写;要把它写进去,请粘贴后自己点保存。",
|
|
370
|
+
"memory.settings.profileDraftCopyFailed": "复制没成功:请选中上面的草稿手动复制。",
|
|
335
371
|
"memory.settings.profileDraftDiscard": "丢弃草稿",
|
|
336
372
|
"memory.settings.persona": "人格定义(beta)",
|
|
337
373
|
"memory.settings.personaHint": "给这位 Agent 一张角色卡:名字、性格、回复要求。开关打开且下方「人格定义」总闸开启时,每轮对话都会注入。",
|
|
@@ -363,12 +399,55 @@ window.__ModuleLoader__.load({
|
|
|
363
399
|
"memory.settings.personaViewSourceNone": "按当前寻址口径,这个身份不会注入任何人格——默认卡只兜「解析不到会话身份」的会话。",
|
|
364
400
|
"memory.settings.personaViewGateOff": "总闸「人格定义(beta)」现在是关的:下面这段里不会出现人格,只有画像与规则。",
|
|
365
401
|
"memory.settings.personaViewRefresh": "刷新预览",
|
|
402
|
+
"memory.settings.factsTitle": "自动事实(Agent 眼中的你)",
|
|
403
|
+
"memory.settings.factsHint": "Agent 在巩固(dream)后从新记忆里提炼的「对你的了解」,按会话身份分卡,与人格卡同域,注在预览里手填画像之后。这里可以逐条修正;删除只是归档,随时可恢复。你自己填的用户画像不受影响。",
|
|
404
|
+
"memory.settings.factsEmpty": "这张卡还没有事实。有新的对话记忆并跑过巩固后,Agent 对你的了解会出现在这里;也可以在下面手动添加。",
|
|
405
|
+
"memory.settings.factsUnavailable": "读不到事实(请求失败或后端版本过旧)。这里不显示猜测的内容。",
|
|
406
|
+
"memory.settings.factsAddPlaceholder": "例如:用户偏好简洁直给的回答",
|
|
407
|
+
"memory.settings.factsAdd": "添加事实",
|
|
408
|
+
"memory.settings.factsRowEdit": "编辑这条事实",
|
|
409
|
+
"memory.settings.factsRowSave": "保存",
|
|
410
|
+
"memory.settings.factsRowCancel": "取消",
|
|
411
|
+
"memory.settings.factsRowDelete": "删除这条事实(归档,可恢复)",
|
|
412
|
+
"memory.settings.factsApplyDone": "事实已更新",
|
|
413
|
+
"memory.settings.factsApplyError": "没存进去:请求失败了,请再试一次。",
|
|
414
|
+
"memory.settings.factsApplyFailures": "{n} 条没存进去",
|
|
415
|
+
"memory.settings.factsApplyDups": "跳过 {n} 条重复",
|
|
416
|
+
"memory.settings.factsGateOff": "功能开关里的「自动事实」没开:dream 轮不会自动维护这份清单,但这里的增改与下面的手动更新照常可用。",
|
|
417
|
+
"memory.settings.factsRun": "立即更新",
|
|
418
|
+
"memory.settings.factsRunning": "正在更新…",
|
|
419
|
+
"memory.settings.factsRunOk": "已更新 {n} 条事实",
|
|
420
|
+
"memory.settings.factsRunNoop": "没有新记忆,事实已是最新",
|
|
421
|
+
"memory.settings.factsRunDisabled": "功能开关里的「自动事实」还没打开,先到功能开关区打开它。",
|
|
422
|
+
"memory.settings.factsRunNoRoute": "没有可用的模型:先在宿主里选一个默认模型(或在配置里填 dreamProvider / dreamModel),再点一次。",
|
|
423
|
+
"memory.settings.factsRunFailed": "更新没成:模型没按格式返回,或这次调用失败了。稍后可重试。",
|
|
424
|
+
"memory.settings.factsJournalTitle": "最近变更",
|
|
425
|
+
"memory.settings.factsJournalEmpty": "还没有变更记录",
|
|
426
|
+
"memory.settings.factsOpAdd": "新增",
|
|
427
|
+
"memory.settings.factsOpUpdate": "修改",
|
|
428
|
+
"memory.settings.factsOpArchive": "归档",
|
|
429
|
+
"memory.settings.factsActorAuto": "自动",
|
|
430
|
+
"memory.settings.factsActorManual": "手动",
|
|
431
|
+
"memory.settings.factsRunLast": "最近一轮:",
|
|
432
|
+
"memory.settings.factsRunCounts": "输入 {input} 条 · 应用 {ops} 项操作",
|
|
433
|
+
"memory.settings.factsRunStatusOk": "成功",
|
|
434
|
+
"memory.settings.factsRunStatusNoop": "无变更",
|
|
435
|
+
"memory.settings.factsRunStatusFailed": "失败",
|
|
366
436
|
"memory.features.personaEnabled": "人格定义(beta)",
|
|
367
437
|
"memory.features.personaEnabled.hint": "把「设置 → 个性化」里的人格卡注入到每轮对话。默认关(beta);总闸与人格卡内的开关同时打开才生效。",
|
|
368
438
|
"memory.features.personaStrictAddressing": "严格人格寻址",
|
|
369
439
|
"memory.features.personaStrictAddressing.hint": "开:会话身份没有对应人格卡、或那张卡被关掉时,就不注入人格——默认卡只兜「解析不到会话身份」的会话。宿主里跑着自带身份的 bot(DS Bot 那类)时关掉这种兜底,免得两句「你是谁」撞在同一轮。默认关 = 未命中就落默认卡(历史行为)。",
|
|
370
440
|
"memory.features.profileExtractEnabled": "画像提取草稿",
|
|
371
|
-
"memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 →
|
|
441
|
+
"memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 → Agent 眼中的你」卡上的「从记忆提取草稿」按钮:读一批活跃记忆、花一次模型调用,产一段「助手眼中的用户」草稿。草稿只在那张卡上展示副本,可一键复制,不会写进记忆库、也不会覆盖你自己填的「用户画像」。轻量档预设关(用户显式勾选仍然赢)。",
|
|
442
|
+
"memory.features.profileFactsEnabled": "自动事实",
|
|
443
|
+
"memory.features.profileFactsEnabled.hint": "每次 dream(巩固)轮后,把这一窗口里沉淀的新记忆再读一遍,按会话身份卡维护一份「Agent 眼中的你」事实清单(设置 → 个性化 → 自动事实),注入在手填画像之后。事实住独立的表,不会覆盖你自己填的「用户画像」;面板上可逐条修正或删除(删除=归档,可恢复)。轻量档预设关(用户显式勾选仍然赢)。",
|
|
444
|
+
"memory.features.chorographyEnabled": "仓库地志(chorography)",
|
|
445
|
+
"memory.features.chorographyEnabled.hint": "读一遍仓库写成一份带页码的志书:确定性骨架零模型、可以随时重建;语义段由 CLI 的 headless 会话逐模块补。打开后有四个入口——模型侧三个只读工具、面板「仓库地志」浏览页、每轮的开局提纲注入、/choro 命令。关着时它们全部不存在(模型工具列表里根本没有,注入块不渲染,/choro 不注册)。默认关。",
|
|
446
|
+
"memory.features.chorographyInject": "开局注入档位",
|
|
447
|
+
"memory.features.chorographyInject.hint": "每轮往上下文里塞地志提纲的份量:light 只列文件数最多的 6 个模块(约 8 行)、full 列全部模块、off 不注入(工具与面板页仍可用)。只注模块名 / 文件数 / 页状态 / 页相对路径,不含正文;正文要模型自己用 choro_read_page 取。轻量档预设压成 off。",
|
|
448
|
+
"memory.features.chorographyInject.off": "off(不注入)",
|
|
449
|
+
"memory.features.chorographyInject.light": "light(前 6 个模块)",
|
|
450
|
+
"memory.features.chorographyInject.full": "full(全部模块)",
|
|
372
451
|
"memory.settings.rules": "规则",
|
|
373
452
|
"memory.settings.rulesHint": "给 Agent 立下必须遵守的规矩,随时增删,下一轮即生效。",
|
|
374
453
|
"memory.settings.ruleAdd": "添加规则",
|
|
@@ -390,6 +469,7 @@ window.__ModuleLoader__.load({
|
|
|
390
469
|
"memory.explorer.tabSettings": "设置",
|
|
391
470
|
"memory.explorer.tabStatus": "状态",
|
|
392
471
|
"memory.entities.count": "{n} 个实体",
|
|
472
|
+
"memory.entities.countSampled": "列表只列最近 {shown} 个;上方总数是全库",
|
|
393
473
|
"memory.entities.pick": "从左侧选择一个实体,查看它的属性与关系",
|
|
394
474
|
// 搜索框有字但一条没筛出来时用它;与 entities.none(库里真没实体)分开,
|
|
395
475
|
// 否则「暂无实体」会把打错一个字误导成「你还没开抽取」。
|
|
@@ -535,6 +615,15 @@ window.__ModuleLoader__.load({
|
|
|
535
615
|
"memory.settings.mcp.snippet": "{ \"mcpServers\": { \"dsh-mneme\": { \"command\": \"dsh-mneme-mcp\", \"env\": { \"DSH_MNEME_TOKEN\": \"<外部访问 API 的 token>\" } } } }",
|
|
536
616
|
"memory.settings.mcp.copy": "复制",
|
|
537
617
|
"memory.settings.mcp.copied": "已复制",
|
|
618
|
+
"memory.settings.configKeys.title": "配置键覆盖面",
|
|
619
|
+
"memory.settings.configKeys.desc": "配置文件里的键,面板能改多少、哪些只能手改。只列键名,不显示任何值。",
|
|
620
|
+
"memory.settings.configKeys.summary": "schema 里共 {schema} 个键:面板能改 {panel} 个,{file} 个只能改配置文件,另有 {undefaulted} 个登记了却没有默认值。",
|
|
621
|
+
"memory.settings.configKeys.fileOnlyHead": "只能改配置文件({n} 个)",
|
|
622
|
+
"memory.settings.configKeys.undefaultedHead": "面板登记了、但运行时没有默认值({n} 个)",
|
|
623
|
+
"memory.settings.configKeys.undefaultedHint": "这些键在白名单里,所以面板上有输入框;但 schema 没给默认值,留空时「当前生效值」里就没有它。填一个值才会生效。",
|
|
624
|
+
"memory.settings.configKeys.show": "展开清单",
|
|
625
|
+
"memory.settings.configKeys.hide": "收起清单",
|
|
626
|
+
"memory.settings.configKeys.loadFailed": "没能取到这份清单(接口暂时不可用)——这不等于「没有只能改文件的键」。",
|
|
538
627
|
"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
628
|
"memory.settings.extapi.savedHint": "已保存,重启 DSH 后生效",
|
|
540
629
|
"memory.settings.extapi.invalidPort": "端口需为 1-65535 的数字",
|
|
@@ -814,6 +903,19 @@ window.__ModuleLoader__.load({
|
|
|
814
903
|
"memory.explorer.imported": "已合并 {n} 条编辑",
|
|
815
904
|
"memory.explorer.importFailed": "导入失败",
|
|
816
905
|
"memory.explorer.importNoFile": "请先选择文件",
|
|
906
|
+
"memory.explorer.bootstrap": "从目录引导…",
|
|
907
|
+
"memory.explorer.bootstrapTitle": "从目录引导",
|
|
908
|
+
"memory.explorer.bootstrapHint": "读这个目录里的确定性产物(脚本命令、README、贡献规范、CI 工作流、顶层结构、最近提交),一条规则一条记忆。不调用模型,重复执行只会合并更新、不会重复入库。",
|
|
909
|
+
"memory.explorer.bootstrapDir": "目录路径",
|
|
910
|
+
"memory.explorer.bootstrapConfirm": "开始引导",
|
|
911
|
+
"memory.explorer.bootstraping": "引导中…",
|
|
912
|
+
"memory.explorer.bootstrapDone": "新建 {n} 条、并入 {m} 条",
|
|
913
|
+
"memory.explorer.bootstrapNoDir": "请先填写目录路径",
|
|
914
|
+
"memory.explorer.bootstrapFailed": "引导失败",
|
|
915
|
+
"memory.explorer.bootstrapErr.missing-dir": "没给目录路径",
|
|
916
|
+
"memory.explorer.bootstrapErr.dir-not-found": "这个路径不存在",
|
|
917
|
+
"memory.explorer.bootstrapErr.not-a-directory": "这个路径不是目录",
|
|
918
|
+
"memory.explorer.bootstrapErr.bootstrap-unavailable": "当前装配下这条能力没接线",
|
|
817
919
|
"memory.explorer.sharePack": "分享记忆包…",
|
|
818
920
|
"memory.explorer.shareTitle": "分享记忆包",
|
|
819
921
|
"memory.explorer.shareHint": "把本机活跃记忆打成一个可外发的包:密钥/PII 整条不发,本机路径抹成 ~,归档与遗忘的不进。",
|
|
@@ -1096,6 +1198,13 @@ window.__ModuleLoader__.load({
|
|
|
1096
1198
|
"memory.status.token.heat": "8 周热力 · 每天花了多少",
|
|
1097
1199
|
"memory.status.token.heatLess": "少",
|
|
1098
1200
|
"memory.status.token.heatMore": "多",
|
|
1201
|
+
"memory.status.token.sink": "8 周沉淀 · 进来的 vs 稳住的",
|
|
1202
|
+
"memory.status.token.sinkAdded": "新增",
|
|
1203
|
+
"memory.status.token.sinkMerged": "巩固",
|
|
1204
|
+
"memory.status.token.sinkArchived": "归档",
|
|
1205
|
+
"memory.status.token.sinkGap": "巩固/新增",
|
|
1206
|
+
"memory.status.token.sinkEmpty": "最近还没有记录",
|
|
1207
|
+
"memory.status.token.sinkFoot": "巩固按去重记录数计,同一条被多轮合并只算一次",
|
|
1099
1208
|
"memory.status.token.last7": "近 7 天",
|
|
1100
1209
|
"memory.status.token.meterOnly": "(只计量)",
|
|
1101
1210
|
"memory.status.token.detail": "详细状态卡(原「库内一览 / 后台运转」9 张,点击展开)",
|
|
@@ -1110,6 +1219,24 @@ window.__ModuleLoader__.load({
|
|
|
1110
1219
|
en: {
|
|
1111
1220
|
"memory.panel.empty": "No memories yet",
|
|
1112
1221
|
"memory.panel.open": "Memory",
|
|
1222
|
+
"wiki.panel.open": "Repo Atlas",
|
|
1223
|
+
"wiki.rebuild": "Rebuild skeleton",
|
|
1224
|
+
"wiki.rebuilding": "Rebuilding…",
|
|
1225
|
+
"wiki.rebuild.done": "Rebuilt",
|
|
1226
|
+
"wiki.rebuild.failed": "Rebuild failed",
|
|
1227
|
+
"wiki.rebuild.hint": "Re-runs the deterministic T0 skeleton (seconds; T1 semantic enrichment stays on the CLI --t1). Note: enriched pages fall back to skeleton — regenerate semantics afterwards",
|
|
1228
|
+
"wiki.rebuild.modules": "modules",
|
|
1229
|
+
"wiki.rebuild.pages": "pages",
|
|
1230
|
+
"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.",
|
|
1231
|
+
"wiki.empty.disabled": "chorography (Repo Atlas) is disabled: turn it on under Settings and restart DSH to apply.",
|
|
1232
|
+
"wiki.empty.pickRepo": "Multiple repositories registered — pick one:",
|
|
1233
|
+
"wiki.search.placeholder": "Search pages…",
|
|
1234
|
+
"wiki.state.loading": "Loading…",
|
|
1235
|
+
"wiki.state.pickPage": "Pick a module page from the tree.",
|
|
1236
|
+
"wiki.state.pageMissing": "Index exists but the page file is missing: re-run `dsh-mneme-choro <repo-path> --apply` to rebuild.",
|
|
1237
|
+
"wiki.state.unavailable": "chorography queries are unavailable (host wiring missing).",
|
|
1238
|
+
"wiki.state.error": "Failed to load",
|
|
1239
|
+
"wiki.tree.title": "Modules",
|
|
1113
1240
|
"memory.tab.all": "All",
|
|
1114
1241
|
"memory.tab.preference": "Preferences",
|
|
1115
1242
|
"memory.tab.project": "Projects",
|
|
@@ -1131,16 +1258,21 @@ window.__ModuleLoader__.load({
|
|
|
1131
1258
|
"memory.settings.profileSaved": "Profile saved",
|
|
1132
1259
|
"memory.settings.profileExtract": "Draft from memory",
|
|
1133
1260
|
"memory.settings.profileExtracting": "Extracting…",
|
|
1134
|
-
"memory.settings.profileExtractHint": "Costs one model call, and only when you press this button. The result is a draft
|
|
1261
|
+
"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
1262
|
"memory.settings.profileExtractDisabled": "Nothing extracted: the “Profile draft from memory” feature switch is still off.",
|
|
1136
1263
|
"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
1264
|
"memory.settings.profileExtractNoRoute": "No model available: pick a default model in the host (or set dreamProvider / dreamModel), then try again.",
|
|
1138
1265
|
"memory.settings.profileSaveFailed": "Profile was not saved",
|
|
1139
1266
|
"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 (
|
|
1267
|
+
"memory.settings.profileDraftTitle": "Draft copy (never saved automatically)",
|
|
1141
1268
|
"memory.settings.profileDraftRows": "Based on entries",
|
|
1269
|
+
"memory.status.entitiesSampled": "Per-type counts cover the newest 500 entities only; the total is the whole library",
|
|
1270
|
+
"memory.status.entitiesUnknown": "Total unavailable; the counts below cover the newest 500 entities",
|
|
1142
1271
|
"memory.settings.profileDraftTruncated": "many entries — only the first batch was read",
|
|
1143
|
-
"memory.settings.
|
|
1272
|
+
"memory.settings.profileDraftCopy": "Copy draft",
|
|
1273
|
+
"memory.settings.profileDraftCopied": "Copied",
|
|
1274
|
+
"memory.settings.profileDraftCopyHint": "The User Profile field is yours to write; to keep this text, paste it there and save it yourself.",
|
|
1275
|
+
"memory.settings.profileDraftCopyFailed": "Copy failed: select the draft above and copy it manually.",
|
|
1144
1276
|
"memory.settings.profileDraftDiscard": "Discard draft",
|
|
1145
1277
|
"memory.settings.persona": "Persona (beta)",
|
|
1146
1278
|
"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.",
|
|
@@ -1172,12 +1304,55 @@ window.__ModuleLoader__.load({
|
|
|
1172
1304
|
"memory.settings.personaViewSourceNone": "Under the current addressing mode this identity injects no persona at all — the default card only covers sessions with no resolvable identity.",
|
|
1173
1305
|
"memory.settings.personaViewGateOff": "The master switch “Persona (beta)” is off: the block below contains profile and rules only.",
|
|
1174
1306
|
"memory.settings.personaViewRefresh": "Refresh preview",
|
|
1307
|
+
"memory.settings.factsTitle": "Auto facts (you, as the agent sees you)",
|
|
1308
|
+
"memory.settings.factsHint": "What the agent learned about you, distilled from new memories after each dream run — split into cards by session identity (same domain as persona cards) and injected after your handwritten profile in the preview. Fix entries here one by one; deleting only archives a fact and can be undone. The User Profile you wrote yourself is never touched.",
|
|
1309
|
+
"memory.settings.factsEmpty": "No facts on this card yet. After new conversation memories go through a dream run, what the agent picked up about you shows up here; you can also add one below.",
|
|
1310
|
+
"memory.settings.factsUnavailable": "Could not read the facts (request failed or backend too old). Nothing is guessed here.",
|
|
1311
|
+
"memory.settings.factsAddPlaceholder": "e.g. The user prefers short, direct answers",
|
|
1312
|
+
"memory.settings.factsAdd": "Add fact",
|
|
1313
|
+
"memory.settings.factsRowEdit": "Edit this fact",
|
|
1314
|
+
"memory.settings.factsRowSave": "Save",
|
|
1315
|
+
"memory.settings.factsRowCancel": "Cancel",
|
|
1316
|
+
"memory.settings.factsRowDelete": "Delete this fact (archived, restorable)",
|
|
1317
|
+
"memory.settings.factsApplyDone": "Facts updated",
|
|
1318
|
+
"memory.settings.factsApplyError": "Not saved: the request failed. Please try again.",
|
|
1319
|
+
"memory.settings.factsApplyFailures": "{n} not saved",
|
|
1320
|
+
"memory.settings.factsApplyDups": "skipped {n} duplicates",
|
|
1321
|
+
"memory.settings.factsGateOff": "The “Auto facts” feature switch is off: dream runs will not maintain this list, but editing here and the manual update below keep working.",
|
|
1322
|
+
"memory.settings.factsRun": "Update now",
|
|
1323
|
+
"memory.settings.factsRunning": "Updating…",
|
|
1324
|
+
"memory.settings.factsRunOk": "Updated {n} facts",
|
|
1325
|
+
"memory.settings.factsRunNoop": "No new memories — facts are already up to date",
|
|
1326
|
+
"memory.settings.factsRunDisabled": "The “Auto facts” feature switch is off; turn it on in the feature switches section first.",
|
|
1327
|
+
"memory.settings.factsRunNoRoute": "No model available: pick a default model in the host (or set dreamProvider / dreamModel), then try again.",
|
|
1328
|
+
"memory.settings.factsRunFailed": "Update failed: the model did not return the required shape, or the call errored. You can retry later.",
|
|
1329
|
+
"memory.settings.factsJournalTitle": "Recent changes",
|
|
1330
|
+
"memory.settings.factsJournalEmpty": "No changes recorded yet",
|
|
1331
|
+
"memory.settings.factsOpAdd": "Add",
|
|
1332
|
+
"memory.settings.factsOpUpdate": "Edit",
|
|
1333
|
+
"memory.settings.factsOpArchive": "Archive",
|
|
1334
|
+
"memory.settings.factsActorAuto": "auto",
|
|
1335
|
+
"memory.settings.factsActorManual": "manual",
|
|
1336
|
+
"memory.settings.factsRunLast": "Last run: ",
|
|
1337
|
+
"memory.settings.factsRunCounts": "input {input} · applied {ops} ops",
|
|
1338
|
+
"memory.settings.factsRunStatusOk": "ok",
|
|
1339
|
+
"memory.settings.factsRunStatusNoop": "no change",
|
|
1340
|
+
"memory.settings.factsRunStatusFailed": "failed",
|
|
1175
1341
|
"memory.features.personaEnabled": "Persona (beta)",
|
|
1176
1342
|
"memory.features.personaEnabled.hint": "Inject the persona card (Settings → Personalization) into every turn. Off by default (beta); both this switch and the card's own toggle must be on.",
|
|
1177
1343
|
"memory.features.personaStrictAddressing": "Strict persona addressing",
|
|
1178
1344
|
"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
1345
|
"memory.features.profileExtractEnabled": "Profile draft from memory",
|
|
1180
|
-
"memory.features.profileExtractEnabled.hint": "Adds the \"Draft from memory\" button to Settings → Personalization
|
|
1346
|
+
"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).",
|
|
1347
|
+
"memory.features.profileFactsEnabled": "Auto facts",
|
|
1348
|
+
"memory.features.profileFactsEnabled.hint": "After each dream (consolidation) run, the memories settled in that window are read once more to maintain a per-identity-card list of facts the agent holds about you (Settings → Personalization → Auto facts), injected after the handwritten profile. Facts live in their own tables and never overwrite the User Profile you wrote yourself; any fact can be fixed or removed in the panel (removing = archiving, restorable). Off in the lightweight preset (an explicit choice still wins).",
|
|
1349
|
+
"memory.features.chorographyEnabled": "Repo atlas (chorography)",
|
|
1350
|
+
"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.",
|
|
1351
|
+
"memory.features.chorographyInject": "Opening injection tier",
|
|
1352
|
+
"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.",
|
|
1353
|
+
"memory.features.chorographyInject.off": "off (no injection)",
|
|
1354
|
+
"memory.features.chorographyInject.light": "light (top 6 modules)",
|
|
1355
|
+
"memory.features.chorographyInject.full": "full (all modules)",
|
|
1181
1356
|
"memory.settings.rules": "Rules",
|
|
1182
1357
|
"memory.settings.rulesHint": "Rules the agent must follow — add or remove anytime, effective next turn.",
|
|
1183
1358
|
"memory.settings.ruleAdd": "Add Rule",
|
|
@@ -1199,6 +1374,7 @@ window.__ModuleLoader__.load({
|
|
|
1199
1374
|
"memory.explorer.tabSettings": "Settings",
|
|
1200
1375
|
"memory.explorer.tabStatus": "Status",
|
|
1201
1376
|
"memory.entities.count": "{n} entities",
|
|
1377
|
+
"memory.entities.countSampled": "The list shows the newest {shown} only; the total above is the whole library",
|
|
1202
1378
|
"memory.entities.pick": "Select an entity to see its attributes and relations",
|
|
1203
1379
|
"memory.entities.noMatch": "No entities match that search — try another word",
|
|
1204
1380
|
"memory.entities.none": "No entities yet — turn on entity extraction and people / projects / concepts will accumulate here",
|
|
@@ -1338,6 +1514,15 @@ window.__ModuleLoader__.load({
|
|
|
1338
1514
|
"memory.settings.mcp.snippet": "{ \"mcpServers\": { \"dsh-mneme\": { \"command\": \"dsh-mneme-mcp\", \"env\": { \"DSH_MNEME_TOKEN\": \"<token from External API above>\" } } } }",
|
|
1339
1515
|
"memory.settings.mcp.copy": "Copy",
|
|
1340
1516
|
"memory.settings.mcp.copied": "Copied",
|
|
1517
|
+
"memory.settings.configKeys.title": "Config key coverage",
|
|
1518
|
+
"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.",
|
|
1519
|
+
"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.",
|
|
1520
|
+
"memory.settings.configKeys.fileOnlyHead": "File-only ({n})",
|
|
1521
|
+
"memory.settings.configKeys.undefaultedHead": "Registered for the panel but has no runtime default ({n})",
|
|
1522
|
+
"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.",
|
|
1523
|
+
"memory.settings.configKeys.show": "Show the list",
|
|
1524
|
+
"memory.settings.configKeys.hide": "Hide the list",
|
|
1525
|
+
"memory.settings.configKeys.loadFailed": "Couldn't load this list (endpoint unavailable) — that is not the same as \"no file-only keys\".",
|
|
1341
1526
|
"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
1527
|
"memory.settings.extapi.savedHint": "Saved. Takes effect after restarting DSH",
|
|
1343
1528
|
"memory.settings.extapi.invalidPort": "Port must be a number between 1 and 65535",
|
|
@@ -1600,6 +1785,19 @@ window.__ModuleLoader__.load({
|
|
|
1600
1785
|
"memory.explorer.imported": "Merged {n} edits",
|
|
1601
1786
|
"memory.explorer.importFailed": "Import failed",
|
|
1602
1787
|
"memory.explorer.importNoFile": "Choose a file first",
|
|
1788
|
+
"memory.explorer.bootstrap": "Bootstrap from a directory…",
|
|
1789
|
+
"memory.explorer.bootstrapTitle": "Bootstrap from a directory",
|
|
1790
|
+
"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.",
|
|
1791
|
+
"memory.explorer.bootstrapDir": "Directory path",
|
|
1792
|
+
"memory.explorer.bootstrapConfirm": "Bootstrap",
|
|
1793
|
+
"memory.explorer.bootstraping": "Bootstrapping…",
|
|
1794
|
+
"memory.explorer.bootstrapDone": "Created {n}, merged {m}",
|
|
1795
|
+
"memory.explorer.bootstrapNoDir": "Enter a directory path first",
|
|
1796
|
+
"memory.explorer.bootstrapFailed": "Bootstrap failed",
|
|
1797
|
+
"memory.explorer.bootstrapErr.missing-dir": "No directory path was given",
|
|
1798
|
+
"memory.explorer.bootstrapErr.dir-not-found": "That path does not exist",
|
|
1799
|
+
"memory.explorer.bootstrapErr.not-a-directory": "That path is not a directory",
|
|
1800
|
+
"memory.explorer.bootstrapErr.bootstrap-unavailable": "This capability is not wired in the current assembly",
|
|
1603
1801
|
"memory.explorer.sharePack": "Share memories…",
|
|
1604
1802
|
"memory.explorer.shareTitle": "Memory share pack",
|
|
1605
1803
|
"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 +2080,13 @@ window.__ModuleLoader__.load({
|
|
|
1882
2080
|
"memory.status.token.heat": "8-week heat · daily spend",
|
|
1883
2081
|
"memory.status.token.heatLess": "less",
|
|
1884
2082
|
"memory.status.token.heatMore": "more",
|
|
2083
|
+
"memory.status.token.sink": "8-week consolidation · in vs kept",
|
|
2084
|
+
"memory.status.token.sinkAdded": "added",
|
|
2085
|
+
"memory.status.token.sinkMerged": "consolidated",
|
|
2086
|
+
"memory.status.token.sinkArchived": "archived",
|
|
2087
|
+
"memory.status.token.sinkGap": "consolidated/added",
|
|
2088
|
+
"memory.status.token.sinkEmpty": "nothing recorded yet",
|
|
2089
|
+
"memory.status.token.sinkFoot": "consolidated counts distinct records; re-merging the same record counts once",
|
|
1885
2090
|
"memory.status.token.last7": "last 7 days",
|
|
1886
2091
|
"memory.status.token.meterOnly": "(metered only)",
|
|
1887
2092
|
"memory.status.token.detail": "Detailed status cards (the original 9 under Library / Background activity, click to expand)",
|
|
@@ -1968,10 +2173,12 @@ window.__ModuleLoader__.load({
|
|
|
1968
2173
|
// / -warning / -success / -warn / --dsw-alias-border),
|
|
1969
2174
|
// 于是长年吃的是写在 fallback 里的硬编码色。这里按宿主真实 token 现算一组
|
|
1970
2175
|
// 面板自有语义色:混色基准全是宿主 token,宿主换肤时跟着走,不引入新字面量。
|
|
1971
|
-
//
|
|
2176
|
+
// 必须挂满五个根——面板不只长在 .mneme-x 下:记忆库 sheet portal 到 body
|
|
1972
2177
|
// (.mneme-overlay)、侧边栏入口在宿主插槽(.mneme-triggerwrap)、顶部入口
|
|
1973
|
-
// portal 到宿主侧边栏(.mneme-topentry
|
|
1974
|
-
|
|
2178
|
+
// portal 到宿主侧边栏(.mneme-topentry)。第五个根是宿主内整页面板
|
|
2179
|
+
// (.mneme-panel)配套的侧栏图标容器:那颗图标长在宿主的 panelGlyph 里,
|
|
2180
|
+
// 不在 .mneme-x 的子树里,而它右上角的未处理冲突 badge 要吃 --mneme-danger。
|
|
2181
|
+
".mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry,.mneme-paneliglyph{" +
|
|
1975
2182
|
"--mneme-fg-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 60%,transparent);" +
|
|
1976
2183
|
"--mneme-graphic-dim:color-mix(in srgb,var(--dsw-alias-label-primary) 50%,transparent);" +
|
|
1977
2184
|
"--mneme-accent:color-mix(in srgb,var(--dsw-alias-state-business-primary) 85%,var(--dsw-alias-label-primary));" +
|
|
@@ -2011,6 +2218,9 @@ window.__ModuleLoader__.load({
|
|
|
2011
2218
|
// 低于详情抽屉(6),下拉菜单/导入对话框随容器整体上浮。
|
|
2012
2219
|
".mneme-xtools{position:absolute;right:14px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:8px;padding:0;z-index:3}",
|
|
2013
2220
|
".mneme-xcount{flex:none;font-size:12px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
2221
|
+
// 目录计数下面那行小字:说清「总数是全库、列表只列最近 N 个」,两个口径
|
|
2222
|
+
// 不一样不能只靠一个数字冒充(实体页此前就吃到这个亏)。
|
|
2223
|
+
".mneme-xcountnote{font-size:11px;line-height:15px;color:var(--mneme-fg-dim);white-space:nowrap}",
|
|
2014
2224
|
// --- three-column browse layout: hairline separators, no outer box ---
|
|
2015
2225
|
".mneme-xmain{flex:1;min-height:0;display:flex;flex-direction:row;overflow:hidden}",
|
|
2016
2226
|
".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 +2374,62 @@ window.__ModuleLoader__.load({
|
|
|
2164
2374
|
".mneme-set-cmddesc{font-size:12px;line-height:17px;color:var(--mneme-fg-dim);margin-top:1px;word-break:break-word}",
|
|
2165
2375
|
// --- settings sub-view ---
|
|
2166
2376
|
".mneme-set{flex:1;min-height:0;overflow-y:auto;padding:8px 24px 48px;box-sizing:border-box}",
|
|
2377
|
+
// --- 宿主中列整页面板(sidebar.panellist + main 成对座位)---
|
|
2378
|
+
// 宿主把 main 槽渲染在三列 grid 的中列里,那一层只给了 min-width:0 与
|
|
2379
|
+
// overflow:hidden,没有任何尺寸约束——注册者必须自己铺满并自管滚动,
|
|
2380
|
+
// 否则内容会撑破中列(右栏打开时中列更窄,宽度还会变)。
|
|
2381
|
+
".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)}",
|
|
2382
|
+
// 侧栏图标容器:badge 用绝对定位挂在它上面。宿主 panelRow 没有
|
|
2383
|
+
// overflow:hidden,所以 -3px 的偏移不会被裁;这里的 position:relative
|
|
2384
|
+
// 只提供定位上下文,不接管尺寸(由宿主的 panelGlyph 决定)。
|
|
2385
|
+
".mneme-paneliglyph{position:relative;display:inline-flex;align-items:center;justify-content:center}",
|
|
2386
|
+
// --- chorography 面板(仓库地志):顶栏检索 + 左树右文。颜色全部引用
|
|
2387
|
+
// 既有语义层(--dsw-alias-* 正文/边框、--mneme-fg-dim 弱化),不引入新
|
|
2388
|
+
// 字面量色——contrast.test.js 会重算每一条 color 声明。内容渲染挂在
|
|
2389
|
+
// .mneme-panel 根内(#562 的中列整页容器),不新增渲染根。
|
|
2390
|
+
".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)}",
|
|
2391
|
+
".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}",
|
|
2392
|
+
// 重建骨架按钮(收尾档 P34)与回执行:语义色层,无新字面量色。
|
|
2393
|
+
".mneme-wikibtn{flex:none;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);cursor:pointer;font-family:inherit;font-size:12px;line-height:26px;transition:border-color .12s}",
|
|
2394
|
+
".mneme-wikibtn:hover{border-color:var(--dsw-alias-state-business-primary)}",
|
|
2395
|
+
".mneme-wikibtn:disabled{opacity:.5;cursor:default}",
|
|
2396
|
+
".mneme-wikirebuildnote{flex:1;min-width:0;font-size:11.5px;line-height:16px;color:var(--mneme-fg-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
2397
|
+
".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}",
|
|
2398
|
+
".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)}",
|
|
2399
|
+
".mneme-wikibody{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
2400
|
+
".mneme-wikitree{flex:none;width:232px;overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l1);padding:10px 6px}",
|
|
2401
|
+
".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}",
|
|
2402
|
+
".mneme-wikitreeitem:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
2403
|
+
".mneme-wikitreeitem.mneme-active{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);font-weight:500}",
|
|
2404
|
+
".mneme-wikipage{flex:1;min-width:0;overflow-y:auto;padding:18px 22px 44px;color:var(--dsw-alias-label-primary)}",
|
|
2405
|
+
".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}",
|
|
2406
|
+
".mneme-wikipage h2{font-size:17px;border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:6px}",
|
|
2407
|
+
".mneme-wikipage h3{font-size:15px}",
|
|
2408
|
+
".mneme-wikipage h4,.mneme-wikipage h5{font-size:13.5px}",
|
|
2409
|
+
".mneme-wikipage p{margin:6px 0;font-size:13px;line-height:21px;color:var(--dsw-alias-label-secondary)}",
|
|
2410
|
+
".mneme-wikipage ul,.mneme-wikipage ol{margin:6px 0;padding-left:22px}",
|
|
2411
|
+
".mneme-wikipage li{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);margin:3px 0}",
|
|
2412
|
+
".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}",
|
|
2413
|
+
".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}",
|
|
2414
|
+
// 表格(v0.11.1 美化):真 table 替代竖线等宽行;文件清单第一列路径走
|
|
2415
|
+
// code 底色,行数列右对齐等宽数字;斑马纹用 3% 前景混色,不引新字面量色。
|
|
2416
|
+
".mneme-wikitable{width:100%;border-collapse:collapse;margin:10px 0 14px;font-size:12.5px}",
|
|
2417
|
+
".mneme-wikitable th{padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-weight:500;text-align:left;white-space:nowrap}",
|
|
2418
|
+
".mneme-wikitable td{padding:5px 10px;border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);vertical-align:top}",
|
|
2419
|
+
".mneme-wikitable tbody tr:nth-child(even){background:color-mix(in srgb,var(--dsw-alias-label-primary) 3%,transparent)}",
|
|
2420
|
+
".mneme-wikitable tbody tr:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 7%,transparent)}",
|
|
2421
|
+
".mneme-wikitable td .mneme-wikicode{background:none;padding:0}",
|
|
2422
|
+
".mneme-wikinum{text-align:right;font-variant-numeric:tabular-nums}",
|
|
2423
|
+
".mneme-wikiquote{margin:8px 0 14px;padding:8px 12px;border-left:3px solid var(--dsw-alias-border-l2);color:var(--mneme-fg-dim);font-size:12px;line-height:18px}",
|
|
2424
|
+
".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}",
|
|
2425
|
+
".mneme-wikiemptytitle{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
2426
|
+
".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)}",
|
|
2427
|
+
".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}",
|
|
2428
|
+
".mneme-wikihit:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
2429
|
+
".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}",
|
|
2430
|
+
// 相对定位由 .mneme-panel 提供?它没有——检索浮层需要定位上下文,给页面列加
|
|
2431
|
+
// position:relative(不能动 .mneme-panel 本身:记忆面板还挂在同一根上)。
|
|
2432
|
+
".mneme-wikipage{position:relative}",
|
|
2167
2433
|
// --- 记忆库页面:居中 sheet,非全屏 ---
|
|
2168
2434
|
// 从对话直接打开一页记忆库:背板压暗 + 居中圆角卡片(上限 1180×880),
|
|
2169
2435
|
// 对话仍留在背板之后,任何状态下(含新会话 hero、无 tab 环)都可用。
|
|
@@ -2463,6 +2729,25 @@ window.__ModuleLoader__.load({
|
|
|
2463
2729
|
".mneme-heatcell.l4{background:var(--mneme-heat-1)}",
|
|
2464
2730
|
".mneme-heatlegend{display:flex;align-items:center;gap:3px;margin-top:8px;font-size:10.5px;color:var(--mneme-fg-dim)}",
|
|
2465
2731
|
".mneme-heatlegend .mneme-heatcell{width:10px;height:10px}",
|
|
2732
|
+
// 热力右侧的空白:热力只有 56 格 × 11px ≈ 745px 宽,而容器可用宽 968px,
|
|
2733
|
+
// 右边常年空着约 220px。原先三块图各自独占整行,空白是「每块只占半行」造成的,
|
|
2734
|
+
// 不是某个空 div——所以这里把热力与沉淀图并成两列(左侧热力按内容宽 auto,
|
|
2735
|
+
// 右侧吃剩余),容器窄到放不下两列时回落单列竖排。
|
|
2736
|
+
".mneme-flowrow{display:grid;grid-template-columns:auto minmax(0,1fr);gap:20px;align-items:start}",
|
|
2737
|
+
"@container (max-width:960px){.mneme-flowrow{grid-template-columns:minmax(0,1fr)}}",
|
|
2738
|
+
".mneme-flowheat{min-width:0}",
|
|
2739
|
+
".mneme-flowsink{min-width:0;display:flex;flex-direction:column}",
|
|
2740
|
+
// 8 周沉淀只画两条(进来的 / 稳住的):220px 宽里三序列会细到看不清,
|
|
2741
|
+
// 归档数改在下方图例里给总量。
|
|
2742
|
+
".mneme-sinkchart{display:flex;align-items:flex-end;gap:4px;height:96px;margin-top:12px}",
|
|
2743
|
+
".mneme-sinkcol{flex:1;min-width:0;display:flex;align-items:flex-end;gap:2px;height:100%}",
|
|
2744
|
+
".mneme-sinkbar{flex:1;min-width:0;border-radius:2px 2px 0 0;min-height:1px}",
|
|
2745
|
+
".mneme-sinkbar--added{background:var(--mneme-chart-3)}",
|
|
2746
|
+
".mneme-sinkbar--merged{background:var(--mneme-chart-2)}",
|
|
2747
|
+
".mneme-sinklabels{display:flex;gap:4px;margin-top:4px;font-size:9.5px;color:var(--mneme-fg-dim);font-variant-numeric:tabular-nums}",
|
|
2748
|
+
".mneme-sinklabels span{flex:1;min-width:0;text-align:center;white-space:nowrap}",
|
|
2749
|
+
".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)}",
|
|
2750
|
+
".mneme-sinklegend b{font-weight:600;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}",
|
|
2466
2751
|
// 原 9 卡降级折叠区:默认收起,展开才挂载子卡(见 TokenFlowSection 注释)。
|
|
2467
2752
|
".mneme-legacy{max-width:1000px;margin:0 auto 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 16px}",
|
|
2468
2753
|
".mneme-legacy summary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
@@ -2506,6 +2791,11 @@ window.__ModuleLoader__.load({
|
|
|
2506
2791
|
".mneme-set-tocchip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
2507
2792
|
".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
2793
|
".mneme-set-hint{font-size:12px;color:var(--mneme-fg-dim)}",
|
|
2794
|
+
// 配置键覆盖清单(#386 第 7 条):一行一排 chip,长键名换行不截断——
|
|
2795
|
+
// 这张表的用处就是让「memoryQualityFilter.archiveThreshold」这样的名字能被
|
|
2796
|
+
// 完整抄进配置文件,省略号会直接毁掉它的用途。
|
|
2797
|
+
".mneme-keylist{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}",
|
|
2798
|
+
".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
2799
|
// --- 重要性编辑行:与查看态同一份 Lucide 星形(此前是文本星号下拉,角更尖锐且与其他星不统一) ---
|
|
2510
2800
|
".mneme-staredit{display:inline-flex;align-items:center;gap:1px;justify-self:start}",
|
|
2511
2801
|
".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 +3121,12 @@ window.__ModuleLoader__.load({
|
|
|
2831
3121
|
// ④ 关系图谱 the same ego graph the old graph-only tab drew
|
|
2832
3122
|
function EntityPanel({ t, focusEntity, onJumpMemory }) {
|
|
2833
3123
|
const [entities, setEntities] = useState(null);
|
|
3124
|
+
// 目录只取最近 500 个(limit=500),可「一共抽出多少实体」问的是全库。
|
|
3125
|
+
// 后端回 total + totalExact(见 src/api.js 的 /entities),这里必须用全库数,
|
|
3126
|
+
// 否则左侧栏永远显示「500 个实体」——那正是「实体太少」这个观感的另一半来源:
|
|
3127
|
+
// 状态页已经在 #559 修正,实体页这一处当时漏了。null = 全库数读不到(老宿主
|
|
3128
|
+
// 或计数失败),此时退回切片长度并如实标成切片口径,不冒充全库。
|
|
3129
|
+
const [entityTotal, setEntityTotal] = useState(null);
|
|
2834
3130
|
const [railQuery, setRailQuery] = useState("");
|
|
2835
3131
|
const [entityName, setEntityName] = useState(focusEntity || "");
|
|
2836
3132
|
const [depth, setDepth] = useState(1);
|
|
@@ -2848,14 +3144,29 @@ window.__ModuleLoader__.load({
|
|
|
2848
3144
|
const dragRef = useRef(null); // { id, moved }
|
|
2849
3145
|
const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
|
|
2850
3146
|
const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
|
|
3147
|
+
// 通用失效广播的重取游标:记忆行的增删改会连带实体属性/关联记忆/关系边,
|
|
3148
|
+
// 目录与各层数据的 effect 依赖都带上它(见下方订阅 effect)。
|
|
3149
|
+
const [dataTick, setDataTick] = useState(0);
|
|
2851
3150
|
|
|
2852
3151
|
useEffect(() => {
|
|
2853
3152
|
let cancelled = false;
|
|
2854
3153
|
apiFetch("/api/dsh-mneme/entities?limit=500")
|
|
2855
3154
|
.then((r) => (r.ok ? r.json() : { entities: [] }))
|
|
2856
|
-
.then((j) => {
|
|
2857
|
-
|
|
3155
|
+
.then((j) => {
|
|
3156
|
+
if (cancelled) return;
|
|
3157
|
+
setEntities(Array.isArray(j.entities) ? j.entities : []);
|
|
3158
|
+
setEntityTotal(j.totalExact === true && typeof j.total === "number" ? j.total : null);
|
|
3159
|
+
})
|
|
3160
|
+
.catch(() => { if (!cancelled) { setEntities([]); setEntityTotal(null); } });
|
|
2858
3161
|
return () => { cancelled = true; };
|
|
3162
|
+
}, [dataTick]);
|
|
3163
|
+
|
|
3164
|
+
// 通用失效广播订阅:实体抽取随写入发生,本页签对别处的改库(删除记忆、
|
|
3165
|
+
// 裁决冲突、导入)无从感知——不订阅的话,切回来之前图谱一直是旧的。
|
|
3166
|
+
useEffect(() => {
|
|
3167
|
+
const onChanged = () => setDataTick((k) => k + 1);
|
|
3168
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
3169
|
+
return () => window.removeEventListener("mneme:store-changed", onChanged);
|
|
2859
3170
|
}, []);
|
|
2860
3171
|
|
|
2861
3172
|
useEffect(() => {
|
|
@@ -2889,7 +3200,7 @@ window.__ModuleLoader__.load({
|
|
|
2889
3200
|
}
|
|
2890
3201
|
}, []);
|
|
2891
3202
|
|
|
2892
|
-
useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
|
|
3203
|
+
useEffect(() => { load(entityName, depth); }, [load, entityName, depth, dataTick]);
|
|
2893
3204
|
|
|
2894
3205
|
// Layer data for the selected entity: current attrs + related memories.
|
|
2895
3206
|
useEffect(() => {
|
|
@@ -2906,7 +3217,7 @@ window.__ModuleLoader__.load({
|
|
|
2906
3217
|
.then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
|
|
2907
3218
|
.catch(() => {});
|
|
2908
3219
|
return () => { cancelled = true; };
|
|
2909
|
-
}, [entityName]);
|
|
3220
|
+
}, [entityName, dataTick]);
|
|
2910
3221
|
|
|
2911
3222
|
// issue #24 块4 复核面:只在展开时取,收起时不发请求。
|
|
2912
3223
|
const loadBoosted = useCallback(async () => {
|
|
@@ -3106,8 +3417,13 @@ window.__ModuleLoader__.load({
|
|
|
3106
3417
|
value: railQuery,
|
|
3107
3418
|
onChange: (e) => setRailQuery(e.target.value)
|
|
3108
3419
|
}),
|
|
3420
|
+
// 大数字用全库数;全库数读不到(老宿主 / 计数失败)时退回切片长度,
|
|
3421
|
+
// 但那只是「这次取回多少」,所以要多一行脚注说清列表本身只列最近 500 个。
|
|
3109
3422
|
entities !== null && entities.length > 0 && h("div", { className: "mneme-xcount" },
|
|
3110
|
-
t("memory.entities.count").replace("{n}", String(entities.length)))
|
|
3423
|
+
t("memory.entities.count").replace("{n}", String(entityTotal !== null ? entityTotal : entities.length))),
|
|
3424
|
+
entities !== null && entityTotal !== null && entityTotal > entities.length
|
|
3425
|
+
&& h("div", { className: "mneme-xcountnote" },
|
|
3426
|
+
t("memory.entities.countSampled").replace("{shown}", String(entities.length)))
|
|
3111
3427
|
),
|
|
3112
3428
|
h("div", { className: "mneme-entrailbody" },
|
|
3113
3429
|
entities === null && h("div", { className: "mneme-xempty" }, "…"),
|
|
@@ -3393,7 +3709,16 @@ window.__ModuleLoader__.load({
|
|
|
3393
3709
|
// 人格定义页(beta):注入总闸。分组随 enhance(行为开关,非座位闸门);
|
|
3394
3710
|
// 人格内容在设置页「个性化」区的人格卡。
|
|
3395
3711
|
// 严格寻址是同一条链上的第二个口径开关(只在总闸打开时起作用)。
|
|
3396
|
-
"personaEnabled", "personaStrictAddressing", "profileExtractEnabled"
|
|
3712
|
+
"personaEnabled", "personaStrictAddressing", "profileExtractEnabled",
|
|
3713
|
+
// 自动事实(profile_facts):dream 后的自动维护闸。展示与人工修正不受它管
|
|
3714
|
+
// (GET/apply 在 flag 关时照常放行,存量事实要能看能改),它只决定「自动做梦
|
|
3715
|
+
// 维护」与面板「立即更新」这两条 LLM 路径开不开。
|
|
3716
|
+
"profileFactsEnabled",
|
|
3717
|
+
// chorography(仓库地志)的暴露闸(M1 就有的键,M3 才上 UI):它是「是否
|
|
3718
|
+
// 启用这个子系统」而不是「每轮付多少」,所以和上面这些行为开关同组;
|
|
3719
|
+
// 每轮付出多少由紧随组尾的 choroSub 档位管。放组末是因为子块按组尾渲染
|
|
3720
|
+
// ——档位下拉要贴着它的总闸。
|
|
3721
|
+
"chorographyEnabled"] },
|
|
3397
3722
|
{ key: "group.dream", items: ["sleepModeEnabled"] },
|
|
3398
3723
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
|
3399
3724
|
{ key: "group.scope", items: ["scopeEnabled", "strictScope"] }
|
|
@@ -3417,6 +3742,9 @@ window.__ModuleLoader__.load({
|
|
|
3417
3742
|
// 蒸馏思考强度(issue #315):比 entity 多一个 off(显式关思考,思考型模型
|
|
3418
3743
|
// 蒸馏防推理烧预算)。后端枚举含 off,面板必须能选到,否则操作者用不上。
|
|
3419
3744
|
const SUMMARIZE_REASONING = ["off", "none", "low", "medium", "high"];
|
|
3745
|
+
// 地志注入档位(M3):与后端 settings.js 的 FEATURE_FLAG_ENUMS.chorographyInject
|
|
3746
|
+
// 同一份取值。light 是默认档,off 才是不注入。
|
|
3747
|
+
const CHORO_TIERS = ["off", "light", "full"];
|
|
3420
3748
|
|
|
3421
3749
|
function FeatureRow({ name, hint, on, disabled, onToggle, sub, tag }) {
|
|
3422
3750
|
return h("div", { className: "mneme-featrow", style: sub ? { paddingLeft: 18, opacity: 0.86 } : undefined },
|
|
@@ -3514,6 +3842,10 @@ window.__ModuleLoader__.load({
|
|
|
3514
3842
|
}
|
|
3515
3843
|
setState(await res.json());
|
|
3516
3844
|
setSaveError("");
|
|
3845
|
+
// 功能开关即改即存:别的卡在读同一份 effective 快照(画像事实卡的
|
|
3846
|
+
// 「立即更新」按钮禁用态)——广播让它们重读,不然按提示开了开关还得
|
|
3847
|
+
// 关面板重开才解禁,正是本车道广播工作项要消灭的那类体验。
|
|
3848
|
+
try { window.dispatchEvent(new CustomEvent("mneme:features-changed")); } catch { /* 非浏览器环境 */ }
|
|
3517
3849
|
announce(t("memory.features.restartHint"));
|
|
3518
3850
|
setSavedTick(true);
|
|
3519
3851
|
setTimeout(() => setSavedTick(false), 1800);
|
|
@@ -3834,6 +4166,24 @@ window.__ModuleLoader__.load({
|
|
|
3834
4166
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.distillRecords.retentionDays.hint"))
|
|
3835
4167
|
);
|
|
3836
4168
|
|
|
4169
|
+
// chorography M3:开局注入的档位。挂在 chorographyEnabled 下(总闸关着时
|
|
4170
|
+
// 不展开——没启用子系统就没有「注入多少」可谈)。枚举键没有通用渲染器,
|
|
4171
|
+
// 与上面几个子块同款手写下拉。轻量档(lightMode)在消费侧压成 off,
|
|
4172
|
+
// 面板不因此灰掉:那是运行时预设,不是用户选择。
|
|
4173
|
+
const choroSub = eff.chorographyEnabled && h("div", { className: "mneme-featsub" },
|
|
4174
|
+
h("div", { className: "mneme-featnum" },
|
|
4175
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.features.chorographyInject")),
|
|
4176
|
+
h("select", {
|
|
4177
|
+
className: "mneme-select",
|
|
4178
|
+
value: eff.chorographyInject || "light",
|
|
4179
|
+
disabled: busy,
|
|
4180
|
+
onChange: (e) => put({ chorographyInject: e.target.value })
|
|
4181
|
+
},
|
|
4182
|
+
CHORO_TIERS.map((r) => h("option", { key: r, value: r }, t(`memory.features.chorographyInject.${r}`))))
|
|
4183
|
+
),
|
|
4184
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.chorographyInject.hint"))
|
|
4185
|
+
);
|
|
4186
|
+
|
|
3837
4187
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
3838
4188
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
3839
4189
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -3868,7 +4218,7 @@ window.__ModuleLoader__.load({
|
|
|
3868
4218
|
// 子块按组尾追加。distillSub 紧跟在 core 组最后一行
|
|
3869
4219
|
// (distillRecords.enabled)之后,天数行才贴着它的总闸。
|
|
3870
4220
|
g.key === "group.core" && h(react.Fragment, null, distillSub, summarizeSub),
|
|
3871
|
-
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub),
|
|
4221
|
+
g.key === "group.enhance" && h(react.Fragment, null, embedSub, entitySub, choroSub),
|
|
3872
4222
|
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
3873
4223
|
);
|
|
3874
4224
|
const firstScreen = FEATURE_GROUPS.filter((g) => FEATURE_FIRST_SCREEN.includes(g.key));
|
|
@@ -3963,6 +4313,15 @@ window.__ModuleLoader__.load({
|
|
|
3963
4313
|
// 话——把「没启用」「库里没东西」「模型没配合」压成同一句「没有草稿」,用户就只能瞎猜。
|
|
3964
4314
|
const [profileDraft, setProfileDraft] = react.useState(null);
|
|
3965
4315
|
const [profileExtracting, setProfileExtracting] = react.useState(false);
|
|
4316
|
+
const [profileDraftCopied, setProfileDraftCopied] = react.useState(false);
|
|
4317
|
+
// 复制失败必须**看得见**:announce() 那个 live region 是 1px 裁剪元素,只有屏幕
|
|
4318
|
+
// 阅读器读得到。视力正常的用户看到按钮文字没变、页面也没变化,就会以为复制成功,
|
|
4319
|
+
// 粘贴时拿到旧的剪贴板内容——比不提示更糟。
|
|
4320
|
+
const [profileDraftCopyFailed, setProfileDraftCopyFailed] = react.useState(false);
|
|
4321
|
+
// 复制的每一趟都要能认出「这条回调还属于当前这份草稿吗」。剪贴板是异步的,复制 A 的
|
|
4322
|
+
// promise 还没落地时又提取出草稿 B,A 的回调就会在 B 上打勾或报错——B 一个字都没被
|
|
4323
|
+
// 复制过。用递增序号做票据:换草稿或重新复制都换票,旧票的回调直接丢弃。
|
|
4324
|
+
const profileCopyTicket = react.useRef(0);
|
|
3966
4325
|
// 保存失败要可见(persona 那条一直有,画像这条以前是静默 catch)。
|
|
3967
4326
|
const [profileError, setProfileError] = react.useState("");
|
|
3968
4327
|
const [cmdError, setCmdError] = react.useState("");
|
|
@@ -4186,6 +4545,10 @@ window.__ModuleLoader__.load({
|
|
|
4186
4545
|
async function extractProfileDraft() {
|
|
4187
4546
|
setProfileExtracting(true);
|
|
4188
4547
|
setProfileDraft(null);
|
|
4548
|
+
// 新草稿一出现,上一趟复制的回调立刻作废,连它留下的两种提示也一并清掉。
|
|
4549
|
+
profileCopyTicket.current += 1;
|
|
4550
|
+
setProfileDraftCopied(false);
|
|
4551
|
+
setProfileDraftCopyFailed(false);
|
|
4189
4552
|
try {
|
|
4190
4553
|
const res = await apiFetch("/api/dsh-mneme/profile/extract", {
|
|
4191
4554
|
method: "POST",
|
|
@@ -4217,16 +4580,59 @@ window.__ModuleLoader__.load({
|
|
|
4217
4580
|
}
|
|
4218
4581
|
}
|
|
4219
4582
|
|
|
4220
|
-
//
|
|
4221
|
-
//
|
|
4222
|
-
//
|
|
4223
|
-
//
|
|
4224
|
-
|
|
4583
|
+
// 草稿只展示副本(桉桉 2026-11-04 定的口径)。这一段以前叫「采用为画像」,
|
|
4584
|
+
// 点一下就直接 PUT /profile —— 把用户手写的那份覆盖掉,且没有撤销。现在改成
|
|
4585
|
+
// 复制:要落进「用户画像」必须由用户自己粘贴再点保存,手写原文一个字都不会被
|
|
4586
|
+
// 后台改掉。剪贴板不可用(无权限 / 非安全上下文)时不许假装成功——退回
|
|
4587
|
+
// execCommand 那条老路,仍然失败就明确让她手动选中复制。
|
|
4588
|
+
function copyProfileDraft() {
|
|
4225
4589
|
if (profileDraft?.kind !== "draft") return;
|
|
4226
|
-
const text = profileDraft.text;
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4590
|
+
const text = profileDraft.text || "";
|
|
4591
|
+
// 每次尝试先清掉上次的失败提示:失败过以后再成功,红字不能留在屏上。
|
|
4592
|
+
setProfileDraftCopyFailed(false);
|
|
4593
|
+
// 这一趟的票。回调落地时票已经不是当前那张,就说明界面早已换了一份草稿。
|
|
4594
|
+
const ticket = ++profileCopyTicket.current;
|
|
4595
|
+
const current = () => ticket === profileCopyTicket.current;
|
|
4596
|
+
const ok = () => {
|
|
4597
|
+
if (!current()) return;
|
|
4598
|
+
setProfileDraftCopied(true);
|
|
4599
|
+
announce(t("memory.settings.profileDraftCopied"));
|
|
4600
|
+
setTimeout(() => { if (current()) setProfileDraftCopied(false); }, 1500);
|
|
4601
|
+
};
|
|
4602
|
+
// 剪贴板失败要说话:这个方法唯一的产出就是"文本进了剪贴板",静默失败
|
|
4603
|
+
// 会让用户以为复制成功、粘贴时却拿到旧内容(草稿是只读 <pre>,选中再
|
|
4604
|
+
// 复制是她能自己走的退路,得明确告诉她走这条)。
|
|
4605
|
+
// 失败还要顺手撤掉「已复制」:成功态能挂 1.5 秒,这期间重试失败会同时出现
|
|
4606
|
+
// 「已复制」和「复制没成功」两句相反的话。
|
|
4607
|
+
const failed = () => {
|
|
4608
|
+
if (!current()) return;
|
|
4609
|
+
setProfileDraftCopied(false);
|
|
4610
|
+
setProfileDraftCopyFailed(true);
|
|
4611
|
+
announce(t("memory.settings.profileDraftCopyFailed"));
|
|
4612
|
+
};
|
|
4613
|
+
if (navigator.clipboard?.writeText) {
|
|
4614
|
+
navigator.clipboard.writeText(text).then(ok, () => copyProfileDraftFallback(text) ? ok() : failed());
|
|
4615
|
+
} else {
|
|
4616
|
+
copyProfileDraftFallback(text) ? ok() : failed();
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
// 没有 Clipboard API(非安全上下文 / 内嵌 webview 未授权)时的退路:
|
|
4621
|
+
// 临时 textarea + execCommand("copy")。失败返回 false,由调用方说话。
|
|
4622
|
+
function copyProfileDraftFallback(text) {
|
|
4623
|
+
try {
|
|
4624
|
+
const ta = document.createElement("textarea");
|
|
4625
|
+
ta.value = text;
|
|
4626
|
+
ta.setAttribute("readonly", "");
|
|
4627
|
+
ta.style.position = "fixed";
|
|
4628
|
+
ta.style.opacity = "0";
|
|
4629
|
+
document.body.appendChild(ta);
|
|
4630
|
+
ta.select();
|
|
4631
|
+
const done = document.execCommand("copy");
|
|
4632
|
+
document.body.removeChild(ta);
|
|
4633
|
+
return done === true;
|
|
4634
|
+
} catch {
|
|
4635
|
+
return false;
|
|
4230
4636
|
}
|
|
4231
4637
|
}
|
|
4232
4638
|
|
|
@@ -4289,6 +4695,203 @@ window.__ModuleLoader__.load({
|
|
|
4289
4695
|
savePersonaCards(personaDefault, next);
|
|
4290
4696
|
};
|
|
4291
4697
|
|
|
4698
|
+
// 「Agent 眼中的你」自动事实(profile_facts 三表):展示与人工修正独立于手填
|
|
4699
|
+
// 画像(#497 口径:user_settings 神圣,这里一个字都不写它)。增改删全走
|
|
4700
|
+
// POST /apply(删除=归档,可恢复),与 dream 自动轮共用后端同一份 applyOps
|
|
4701
|
+
// 与同一本账——前端不复述任何判据,也不自己维护第二份事实列表。
|
|
4702
|
+
const [factsCards, setFactsCards] = react.useState([]); // 卡清单是全局的,不随选中卡变化
|
|
4703
|
+
const [factsRows, setFactsRows] = react.useState(null); // null = 还没读到/读失败(fail-closed,不拿旧卡的行冒充这张卡)
|
|
4704
|
+
const [factsJournal, setFactsJournal] = react.useState([]);
|
|
4705
|
+
const [factsRuns, setFactsRuns] = react.useState([]);
|
|
4706
|
+
const [factsAgent, setFactsAgent] = react.useState(""); // "" = default 卡
|
|
4707
|
+
const [factsEditId, setFactsEditId] = react.useState(null);
|
|
4708
|
+
const [factsEditDraft, setFactsEditDraft] = react.useState("");
|
|
4709
|
+
const [factsNew, setFactsNew] = react.useState("");
|
|
4710
|
+
const [factsBusy, setFactsBusy] = react.useState(false); // apply 提交中
|
|
4711
|
+
const [factsRunning, setFactsRunning] = react.useState(false); // 手动维护轮进行中
|
|
4712
|
+
// apply 的一次性回执:{kind, text}。failures / dupsSkipped 必须如实摆出来——
|
|
4713
|
+
// 把失败装成成功,人会以为删掉的事实在下一轮不会再来。
|
|
4714
|
+
const [factsNotice, setFactsNotice] = react.useState(null);
|
|
4715
|
+
// 手动维护轮的结局文案(profileDraft 的 kind 分支同款):把「没启用」「没配模型」
|
|
4716
|
+
// 「模型不配合」压成一句「失败」,用户就只能瞎猜该去修什么。
|
|
4717
|
+
const [factsRunEnd, setFactsRunEnd] = react.useState(null);
|
|
4718
|
+
// 总闸快照只管「立即更新」按钮的可用性:null = 还没读到(读失败不猜,按钮保持
|
|
4719
|
+
// 可用,点下去由后端 409 disabled 兜底);事实的展示与人工修正不受这个闸管。
|
|
4720
|
+
const [factsFlag, setFactsFlag] = react.useState(null);
|
|
4721
|
+
// 与 loadPreview 同一套序号守卫:先切 A 再切 B,A 的响应后到不能盖掉 B 的。
|
|
4722
|
+
const factsSeq = react.useRef(0);
|
|
4723
|
+
const loadFacts = react.useCallback(async (agentKey) => {
|
|
4724
|
+
const seq = ++factsSeq.current;
|
|
4725
|
+
try {
|
|
4726
|
+
const res = await apiFetch(`/api/dsh-mneme/profile-facts?agent=${encodeURIComponent(agentKey)}`);
|
|
4727
|
+
const body = await res.json().catch(() => null);
|
|
4728
|
+
if (seq !== factsSeq.current) return;
|
|
4729
|
+
if (res.ok && body && Array.isArray(body.facts)) {
|
|
4730
|
+
setFactsCards(Array.isArray(body.cards) ? body.cards.filter((k) => typeof k === "string") : []);
|
|
4731
|
+
setFactsRows(body.facts);
|
|
4732
|
+
setFactsJournal(Array.isArray(body.journal) ? body.journal : []);
|
|
4733
|
+
setFactsRuns(Array.isArray(body.runs) ? body.runs : []);
|
|
4734
|
+
} else {
|
|
4735
|
+
// 503 unavailable / 后端过旧:行清空走「读不到」文案;卡清单是全局的,
|
|
4736
|
+
// 保留上次读到的,选择器不至于闪空。
|
|
4737
|
+
setFactsRows(null);
|
|
4738
|
+
setFactsJournal([]);
|
|
4739
|
+
setFactsRuns([]);
|
|
4740
|
+
}
|
|
4741
|
+
} catch {
|
|
4742
|
+
if (seq === factsSeq.current) {
|
|
4743
|
+
setFactsRows(null);
|
|
4744
|
+
setFactsJournal([]);
|
|
4745
|
+
setFactsRuns([]);
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
}, []);
|
|
4749
|
+
// 切卡先清行再取:上一张卡的事实留在屏上会被当成这张的(同 loadPreview 的
|
|
4750
|
+
// 单一所有者纪律——这里没有直接写数据,都交给 loadFacts)。维护轮结局一并
|
|
4751
|
+
// 清:A 卡跑完的「已更新 N 条」不该显示在 B 卡下(评审 P3:跨卡残留)。
|
|
4752
|
+
react.useEffect(() => {
|
|
4753
|
+
setFactsRows(null);
|
|
4754
|
+
setFactsEditId(null);
|
|
4755
|
+
setFactsEditDraft("");
|
|
4756
|
+
setFactsNotice(null);
|
|
4757
|
+
setFactsRunEnd(null);
|
|
4758
|
+
loadFacts(factsAgent);
|
|
4759
|
+
}, [factsAgent, loadFacts]);
|
|
4760
|
+
// 总闸快照:features 由功能开关卡管写,这里只读不写、失败不猜。功能开关卡
|
|
4761
|
+
// 即改即存并广播 mneme:features-changed——不监听的话,按提示开了「自动事实」
|
|
4762
|
+
// 还得关面板重开才解禁(正是广播工作项要消灭的那类体验)。
|
|
4763
|
+
react.useEffect(() => {
|
|
4764
|
+
let cancelled = false;
|
|
4765
|
+
const loadFlag = () => {
|
|
4766
|
+
apiFetch("/api/dsh-mneme/features")
|
|
4767
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
4768
|
+
.then((j) => {
|
|
4769
|
+
if (cancelled) return;
|
|
4770
|
+
const eff = (j && j.effective) || {};
|
|
4771
|
+
setFactsFlag(eff.profileFactsEnabled === true);
|
|
4772
|
+
})
|
|
4773
|
+
.catch(() => {}); // 读失败保持 null:按钮不锁死,409 兜底
|
|
4774
|
+
};
|
|
4775
|
+
loadFlag();
|
|
4776
|
+
window.addEventListener("mneme:features-changed", loadFlag);
|
|
4777
|
+
return () => { cancelled = true; window.removeEventListener("mneme:features-changed", loadFlag); };
|
|
4778
|
+
}, []);
|
|
4779
|
+
// 人工修正统一入口:一张卡上的小批量 ops 一次 POST,回来重取 GET 让账本跟上,
|
|
4780
|
+
// 并刷新注入预览(事实段是注入文本的一部分,预览别停在编辑前)。
|
|
4781
|
+
async function applyFactsOps(ops) {
|
|
4782
|
+
if (factsBusy) return null;
|
|
4783
|
+
setFactsBusy(true);
|
|
4784
|
+
try {
|
|
4785
|
+
const res = await apiFetch("/api/dsh-mneme/profile-facts/apply", {
|
|
4786
|
+
method: "POST",
|
|
4787
|
+
headers: { "Content-Type": "application/json" },
|
|
4788
|
+
body: JSON.stringify({ agent: factsAgent, ops })
|
|
4789
|
+
});
|
|
4790
|
+
const body = await res.json().catch(() => null);
|
|
4791
|
+
if (!res.ok || !body || !Array.isArray(body.facts)) {
|
|
4792
|
+
setFactsNotice({ kind: "fail", text: t("memory.settings.factsApplyError") });
|
|
4793
|
+
announce(t("memory.settings.factsApplyError"));
|
|
4794
|
+
return null;
|
|
4795
|
+
}
|
|
4796
|
+
const fails = Array.isArray(body.failures) ? body.failures : [];
|
|
4797
|
+
const dups = Number(body.dupsSkipped) || 0;
|
|
4798
|
+
let text = t("memory.settings.factsApplyDone");
|
|
4799
|
+
let kind = "ok";
|
|
4800
|
+
if (fails.length || dups > 0) {
|
|
4801
|
+
const parts = [];
|
|
4802
|
+
if (fails.length) parts.push(t("memory.settings.factsApplyFailures").replace("{n}", String(fails.length)));
|
|
4803
|
+
if (dups > 0) parts.push(t("memory.settings.factsApplyDups").replace("{n}", String(dups)));
|
|
4804
|
+
text = parts.join(" · ");
|
|
4805
|
+
kind = fails.length ? "fail" : "dup";
|
|
4806
|
+
}
|
|
4807
|
+
setFactsNotice({ kind, text });
|
|
4808
|
+
announce(text);
|
|
4809
|
+
await loadFacts(factsAgent);
|
|
4810
|
+
loadPreview(personaEditing);
|
|
4811
|
+
return body;
|
|
4812
|
+
} catch {
|
|
4813
|
+
setFactsNotice({ kind: "fail", text: t("memory.settings.factsApplyError") });
|
|
4814
|
+
announce(t("memory.settings.factsApplyError"));
|
|
4815
|
+
return null;
|
|
4816
|
+
} finally {
|
|
4817
|
+
setFactsBusy(false);
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
async function addFact() {
|
|
4821
|
+
const content = factsNew.trim();
|
|
4822
|
+
if (!content) return;
|
|
4823
|
+
const body = await applyFactsOps([{ op: "add", content }]);
|
|
4824
|
+
// 与既有活跃事实重复的 add 会被后端跳过并计数:输入框照常清空,跳了几条
|
|
4825
|
+
// 由提示条说明,别让人以为没点上。
|
|
4826
|
+
if (body) setFactsNew("");
|
|
4827
|
+
}
|
|
4828
|
+
function startFactEdit(f) {
|
|
4829
|
+
setFactsEditId(f.id);
|
|
4830
|
+
setFactsEditDraft(typeof f.content === "string" ? f.content : "");
|
|
4831
|
+
}
|
|
4832
|
+
function cancelFactEdit() {
|
|
4833
|
+
setFactsEditId(null);
|
|
4834
|
+
setFactsEditDraft("");
|
|
4835
|
+
}
|
|
4836
|
+
async function saveFactEdit() {
|
|
4837
|
+
if (factsEditId === null) return;
|
|
4838
|
+
const content = factsEditDraft.trim();
|
|
4839
|
+
if (!content) return;
|
|
4840
|
+
const editId = factsEditId;
|
|
4841
|
+
const body = await applyFactsOps([{ op: "update", id: editId, content }]);
|
|
4842
|
+
// 只有这条真的写进去了才收起编辑框;它自己的 op 失败时留着草稿,别让人重打。
|
|
4843
|
+
if (body && !(Array.isArray(body.failures) && body.failures.some((x) => x && x.id === editId))) {
|
|
4844
|
+
cancelFactEdit();
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
async function archiveFact(id) {
|
|
4848
|
+
await applyFactsOps([{ op: "archive", id }]);
|
|
4849
|
+
}
|
|
4850
|
+
// 手动维护轮:结局按后端 error 码分派——no-route 重跑也一样、unparseable 等
|
|
4851
|
+
// 可重试、disabled 是闸没开,三种话不能混。跑完重取 GET 并刷新预览。
|
|
4852
|
+
async function runFacts() {
|
|
4853
|
+
if (factsRunning || factsBusy) return;
|
|
4854
|
+
setFactsRunning(true);
|
|
4855
|
+
setFactsRunEnd(null);
|
|
4856
|
+
try {
|
|
4857
|
+
const res = await apiFetch("/api/dsh-mneme/profile-facts/run", {
|
|
4858
|
+
method: "POST",
|
|
4859
|
+
headers: { "Content-Type": "application/json" },
|
|
4860
|
+
body: JSON.stringify({ agent: factsAgent })
|
|
4861
|
+
});
|
|
4862
|
+
const body = await res.json().catch(() => null);
|
|
4863
|
+
if (res.ok && body && typeof body.status === "string") {
|
|
4864
|
+
const n = Number(body.opsApplied) || 0;
|
|
4865
|
+
setFactsRunEnd(body.status === "noop" ? { kind: "noop" } : { kind: "ok", n });
|
|
4866
|
+
announce(t(body.status === "noop" ? "memory.settings.factsRunNoop" : "memory.settings.factsRunOk").replace("{n}", String(n)));
|
|
4867
|
+
} else {
|
|
4868
|
+
const error = body && typeof body.error === "string" ? body.error : "";
|
|
4869
|
+
const kind = error === "disabled" ? "disabled" : error === "no-route" ? "noroute" : "failed";
|
|
4870
|
+
setFactsRunEnd({ kind });
|
|
4871
|
+
announce(t(kind === "disabled" ? "memory.settings.factsRunDisabled"
|
|
4872
|
+
: kind === "noroute" ? "memory.settings.factsRunNoRoute"
|
|
4873
|
+
: "memory.settings.factsRunFailed"));
|
|
4874
|
+
}
|
|
4875
|
+
await loadFacts(factsAgent);
|
|
4876
|
+
loadPreview(personaEditing);
|
|
4877
|
+
} catch {
|
|
4878
|
+
setFactsRunEnd({ kind: "failed" });
|
|
4879
|
+
announce(t("memory.settings.factsRunFailed"));
|
|
4880
|
+
} finally {
|
|
4881
|
+
setFactsRunning(false);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
// 账本行的摘要截断:before/after 各 ~40 字符,长了加省略号——账本给「扫一眼」
|
|
4885
|
+
// 用,整句塞进来会把最近变更撑成一堵墙。
|
|
4886
|
+
const clipFactText = (s) => {
|
|
4887
|
+
const v = String(s ?? "");
|
|
4888
|
+
return v.length > 40 ? v.slice(0, 40) + "…" : v;
|
|
4889
|
+
};
|
|
4890
|
+
const factOpLabel = (op) => (op === "add" ? t("memory.settings.factsOpAdd")
|
|
4891
|
+
: op === "update" ? t("memory.settings.factsOpUpdate")
|
|
4892
|
+
: op === "archive" ? t("memory.settings.factsOpArchive")
|
|
4893
|
+
: String(op ?? ""));
|
|
4894
|
+
|
|
4292
4895
|
async function putRules(next) {
|
|
4293
4896
|
await apiFetch("/api/dsh-mneme/rules", {
|
|
4294
4897
|
method: "PUT",
|
|
@@ -4521,35 +5124,7 @@ window.__ModuleLoader__.load({
|
|
|
4521
5124
|
saved && h("span", { className: "mneme-saved" }, t("memory.settings.profileSaved")),
|
|
4522
5125
|
profileError ? h("span", { className: "mneme-saved", style: { color: "var(--dsw-danger, #d33)" } },
|
|
4523
5126
|
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
|
|
5127
|
+
)
|
|
4553
5128
|
),
|
|
4554
5129
|
// 人格卡(beta,v1.2 多卡)— who the agent is:多张角色卡按会话身份注入。
|
|
4555
5130
|
// 卡内开关只是该卡 enabled 半边;总闸 personaEnabled 在功能开关组,双闸同真。
|
|
@@ -4681,7 +5256,186 @@ window.__ModuleLoader__.load({
|
|
|
4681
5256
|
className: "mneme-btn",
|
|
4682
5257
|
onClick: () => loadPreview(personaEditing)
|
|
4683
5258
|
}, t("memory.settings.personaViewRefresh"))
|
|
4684
|
-
)
|
|
5259
|
+
),
|
|
5260
|
+
// 从记忆提取草稿(#497)。**放在这张卡上而不是「用户画像」卡**:画像那一栏是
|
|
5261
|
+
// 用户自己手填的地方;这一段是从记忆里反推出来的「Agent 眼中的你」,属于可
|
|
5262
|
+
// 沉淀的那一侧。它只出**副本**——不写 user_settings,也不碰手写画像(桉桉
|
|
5263
|
+
// 2026-11-04 定的口径:只展示副本)。要改成手写画像只能自己复制再粘过去。
|
|
5264
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
5265
|
+
h("button", {
|
|
5266
|
+
type: "button",
|
|
5267
|
+
className: "mneme-btn",
|
|
5268
|
+
disabled: profileExtracting,
|
|
5269
|
+
onClick: extractProfileDraft
|
|
5270
|
+
}, t("memory.settings.profileExtract")),
|
|
5271
|
+
profileExtracting ? h("span", { className: "mneme-saved" }, t("memory.settings.profileExtracting")) : null
|
|
5272
|
+
),
|
|
5273
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractHint")),
|
|
5274
|
+
profileDraft?.kind === "disabled" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractDisabled")) : null,
|
|
5275
|
+
profileDraft?.kind === "empty" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractEmpty")) : null,
|
|
5276
|
+
profileDraft?.kind === "noroute" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractNoRoute")) : null,
|
|
5277
|
+
profileDraft?.kind === "failed" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.profileExtractFailed")) : null,
|
|
5278
|
+
profileDraft?.kind === "draft" ? h("div", null,
|
|
5279
|
+
h("div", { className: "mneme-featnumlabel" }, t("memory.settings.profileDraftTitle")),
|
|
5280
|
+
h("div", { className: "mneme-set-desc" },
|
|
5281
|
+
t("memory.settings.profileDraftRows") + ": " + profileDraft.rowsUsed
|
|
5282
|
+
+ (profileDraft.truncated ? " · " + t("memory.settings.profileDraftTruncated") : "")),
|
|
5283
|
+
h("pre", { className: "mneme-set-input mneme-set-input--area", style: { whiteSpace: "pre-wrap", minHeight: 0 } }, profileDraft.text),
|
|
5284
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 6 } },
|
|
5285
|
+
h("button", { type: "button", className: "mneme-btn", onClick: copyProfileDraft },
|
|
5286
|
+
profileDraftCopied ? t("memory.settings.profileDraftCopied") : t("memory.settings.profileDraftCopy")),
|
|
5287
|
+
h("button", { type: "button", className: "mneme-btn", onClick: () => {
|
|
5288
|
+
profileCopyTicket.current += 1;
|
|
5289
|
+
setProfileDraft(null);
|
|
5290
|
+
setProfileDraftCopied(false);
|
|
5291
|
+
setProfileDraftCopyFailed(false);
|
|
5292
|
+
} }, t("memory.settings.profileDraftDiscard")),
|
|
5293
|
+
h("span", { className: "mneme-set-desc" }, t("memory.settings.profileDraftCopyHint")),
|
|
5294
|
+
profileDraftCopyFailed ? h("span", { className: "mneme-set-desc", role: "alert", style: { color: "var(--mneme-danger)" } },
|
|
5295
|
+
t("memory.settings.profileDraftCopyFailed")) : null
|
|
5296
|
+
)
|
|
5297
|
+
) : null
|
|
5298
|
+
),
|
|
5299
|
+
// 自动事实(profile_facts):dream 后自动维护的「Agent 眼中的你」,按会话身份
|
|
5300
|
+
// 分卡。放在注入预览卡之后——预览里事实段的来源就在这里,看预览的人顺势就能
|
|
5301
|
+
// 找到改它的地方;规则是用户立的另一域(另一张卡),不混。
|
|
5302
|
+
h("section", { className: "mneme-set-card" },
|
|
5303
|
+
h("div", { className: "mneme-set-title" }, t("memory.settings.factsTitle")),
|
|
5304
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.factsHint")),
|
|
5305
|
+
// 卡选择器:default + 各身份卡,人格卡同款 chip 形态;切换即重取(effect 里清行)。
|
|
5306
|
+
h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6 } },
|
|
5307
|
+
h("button", {
|
|
5308
|
+
type: "button",
|
|
5309
|
+
className: "mneme-btn" + (factsAgent === "" ? " mneme-btn--active" : ""),
|
|
5310
|
+
onClick: () => setFactsAgent("")
|
|
5311
|
+
}, t("memory.settings.personaDefaultCard")),
|
|
5312
|
+
// 选中卡即使这轮从清单里消失(事实全归档且无同 scope 记忆)也不让它从
|
|
5313
|
+
// 选择器上失踪——正在编辑的人不能失去落点。
|
|
5314
|
+
[...new Set([factsAgent, ...factsCards])].filter((k) => k && k.trim()).map((k) =>
|
|
5315
|
+
h("button", {
|
|
5316
|
+
key: k,
|
|
5317
|
+
type: "button",
|
|
5318
|
+
className: "mneme-btn" + (factsAgent === k ? " mneme-btn--active" : ""),
|
|
5319
|
+
onClick: () => setFactsAgent(k)
|
|
5320
|
+
}, k))
|
|
5321
|
+
),
|
|
5322
|
+
factsRows === null
|
|
5323
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.factsUnavailable"))
|
|
5324
|
+
: h("div", null,
|
|
5325
|
+
// 只列活跃行:归档的行留在账本里可追溯,不再回到清单上(恢复=重新添加)。
|
|
5326
|
+
factsRows.filter((f) => f && f.status === "active").length === 0
|
|
5327
|
+
? h("div", { className: "mneme-xempty" }, t("memory.settings.factsEmpty"))
|
|
5328
|
+
: null,
|
|
5329
|
+
factsRows.filter((f) => f && f.status === "active").map((f) =>
|
|
5330
|
+
factsEditId === f.id
|
|
5331
|
+
? h("div", { key: f.id, className: "mneme-set-row" },
|
|
5332
|
+
h("input", {
|
|
5333
|
+
className: "mneme-set-input",
|
|
5334
|
+
style: { marginBottom: 0, flex: "1 1 auto" },
|
|
5335
|
+
value: factsEditDraft,
|
|
5336
|
+
autoFocus: true,
|
|
5337
|
+
onChange: (e) => setFactsEditDraft(e.target.value),
|
|
5338
|
+
onKeyDown: (e) => {
|
|
5339
|
+
if (e.key === "Enter") saveFactEdit();
|
|
5340
|
+
if (e.key === "Escape") cancelFactEdit();
|
|
5341
|
+
}
|
|
5342
|
+
}),
|
|
5343
|
+
h("button", { type: "button", className: "mneme-btn", disabled: factsBusy, onClick: saveFactEdit },
|
|
5344
|
+
t("memory.settings.factsRowSave")),
|
|
5345
|
+
h("button", { type: "button", className: "mneme-btn", disabled: factsBusy, onClick: cancelFactEdit },
|
|
5346
|
+
t("memory.settings.factsRowCancel"))
|
|
5347
|
+
)
|
|
5348
|
+
: h("div", { key: f.id, className: "mneme-set-row" },
|
|
5349
|
+
h("span", { className: "mneme-set-ruletext", style: { flex: "1 1 auto" } }, f.content),
|
|
5350
|
+
// ✎ 与 × 都走 mneme-set-del(悬停显形):低频动作不常驻,
|
|
5351
|
+
// 与规则卡同一套视觉语言。
|
|
5352
|
+
h("button", {
|
|
5353
|
+
type: "button",
|
|
5354
|
+
className: "mneme-set-del",
|
|
5355
|
+
title: t("memory.settings.factsRowEdit"),
|
|
5356
|
+
"aria-label": t("memory.settings.factsRowEdit"),
|
|
5357
|
+
onClick: () => startFactEdit(f)
|
|
5358
|
+
}, "✎"),
|
|
5359
|
+
// 删除不二次确认:archive 可恢复(账本里留 before),打断比误删贵。
|
|
5360
|
+
h("button", {
|
|
5361
|
+
type: "button",
|
|
5362
|
+
className: "mneme-set-del",
|
|
5363
|
+
title: t("memory.settings.factsRowDelete"),
|
|
5364
|
+
"aria-label": t("memory.settings.factsRowDelete"),
|
|
5365
|
+
onClick: () => archiveFact(f.id)
|
|
5366
|
+
}, "×")
|
|
5367
|
+
)
|
|
5368
|
+
),
|
|
5369
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 6 } },
|
|
5370
|
+
h("input", {
|
|
5371
|
+
className: "mneme-set-input",
|
|
5372
|
+
style: { marginBottom: 0 },
|
|
5373
|
+
value: factsNew,
|
|
5374
|
+
placeholder: t("memory.settings.factsAddPlaceholder"),
|
|
5375
|
+
onChange: (e) => setFactsNew(e.target.value),
|
|
5376
|
+
onKeyDown: (e) => { if (e.key === "Enter") addFact(); }
|
|
5377
|
+
}),
|
|
5378
|
+
h("button", { type: "button", className: "mneme-btn", disabled: factsBusy, onClick: addFact },
|
|
5379
|
+
t("memory.settings.factsAdd"))
|
|
5380
|
+
),
|
|
5381
|
+
// apply 回执:失败(role=alert)与跳重如实摆出来,全成才说成功。
|
|
5382
|
+
factsNotice
|
|
5383
|
+
? h("div", {
|
|
5384
|
+
className: "mneme-set-desc",
|
|
5385
|
+
role: factsNotice.kind === "fail" ? "alert" : undefined,
|
|
5386
|
+
style: { color: factsNotice.kind === "fail" ? "var(--mneme-danger)" : undefined }
|
|
5387
|
+
}, factsNotice.text)
|
|
5388
|
+
: null,
|
|
5389
|
+
// 手动维护轮:总闸快照关着就禁用(features 只读不写);快照没读到时
|
|
5390
|
+
// 不锁——点下去由后端 409 disabled 给出同款解释。
|
|
5391
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
|
|
5392
|
+
h("button", {
|
|
5393
|
+
type: "button",
|
|
5394
|
+
className: "mneme-btn",
|
|
5395
|
+
disabled: factsRunning || factsFlag === false,
|
|
5396
|
+
onClick: runFacts
|
|
5397
|
+
}, factsRunning ? t("memory.settings.factsRunning") : t("memory.settings.factsRun")),
|
|
5398
|
+
factsRunning ? h("span", { className: "mneme-saved" }, t("memory.settings.factsRunning")) : null
|
|
5399
|
+
),
|
|
5400
|
+
factsFlag === false ? h("div", { className: "mneme-set-desc" }, t("memory.settings.factsGateOff")) : null,
|
|
5401
|
+
// 五种结局各一句话(profileDraft 的 kind 分支同款)。
|
|
5402
|
+
factsRunEnd?.kind === "ok" ? h("div", { className: "mneme-set-desc" },
|
|
5403
|
+
t("memory.settings.factsRunOk").replace("{n}", String(factsRunEnd.n))) : null,
|
|
5404
|
+
factsRunEnd?.kind === "noop" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.factsRunNoop")) : null,
|
|
5405
|
+
factsRunEnd?.kind === "disabled" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.factsRunDisabled")) : null,
|
|
5406
|
+
factsRunEnd?.kind === "noroute" ? h("div", { className: "mneme-set-desc" }, t("memory.settings.factsRunNoRoute")) : null,
|
|
5407
|
+
factsRunEnd?.kind === "failed" ? h("div", { className: "mneme-set-desc", role: "alert", style: { color: "var(--mneme-danger)" } },
|
|
5408
|
+
t("memory.settings.factsRunFailed")) : null,
|
|
5409
|
+
// 最近变更(账本尾 8 条,后端按新→旧给):时间 + op + 内容变化 + actor 徽章。
|
|
5410
|
+
h("div", { className: "mneme-featnumlabel", style: { marginTop: 12 } }, t("memory.settings.factsJournalTitle")),
|
|
5411
|
+
factsJournal.length === 0 ? h("div", { className: "mneme-xempty" }, t("memory.settings.factsJournalEmpty")) : null,
|
|
5412
|
+
factsJournal.slice(0, 8).map((j) =>
|
|
5413
|
+
h("div", { key: j.id, className: "mneme-set-row", style: { flexWrap: "wrap", gap: 6 } },
|
|
5414
|
+
h("span", { className: "mneme-set-desc", title: j.at }, formatRelativeTime(j.at, t)),
|
|
5415
|
+
h("span", { className: "mneme-set-ruletext" }, factOpLabel(j.op)),
|
|
5416
|
+
h("span", { className: "mneme-set-desc", style: { flex: "1 1 160px" } },
|
|
5417
|
+
// add 只展示新值;archive 只展示旧值(op 标签已经说了归档);
|
|
5418
|
+
// update 才有真正的 before→after。
|
|
5419
|
+
j.op === "add" ? "+ " + clipFactText(j.after)
|
|
5420
|
+
: j.op === "archive" ? clipFactText(j.before)
|
|
5421
|
+
: clipFactText(j.before) + " → " + clipFactText(j.after)),
|
|
5422
|
+
h("span", { className: "mneme-badge " + (j.actor === "manual" ? "mneme-badge--scope" : "mneme-badge--default") },
|
|
5423
|
+
t(j.actor === "manual" ? "memory.settings.factsActorManual" : "memory.settings.factsActorAuto"))
|
|
5424
|
+
)
|
|
5425
|
+
),
|
|
5426
|
+
// 最近一轮维护(回执尾 1 条,后端按新→旧给):状态 + 输入/应用计数。
|
|
5427
|
+
factsRuns[0]
|
|
5428
|
+
? h("div", { className: "mneme-set-desc" },
|
|
5429
|
+
t("memory.settings.factsRunLast")
|
|
5430
|
+
+ (factsRuns[0].status === "ok" ? t("memory.settings.factsRunStatusOk")
|
|
5431
|
+
: factsRuns[0].status === "noop" ? t("memory.settings.factsRunStatusNoop")
|
|
5432
|
+
: factsRuns[0].status === "failed" ? t("memory.settings.factsRunStatusFailed")
|
|
5433
|
+
: String(factsRuns[0].status ?? ""))
|
|
5434
|
+
+ " · " + t("memory.settings.factsRunCounts")
|
|
5435
|
+
.replace("{input}", String(Number(factsRuns[0].input_count) || 0))
|
|
5436
|
+
.replace("{ops}", String(Number(factsRuns[0].ops_applied) || 0)))
|
|
5437
|
+
: null
|
|
5438
|
+
)
|
|
4685
5439
|
),
|
|
4686
5440
|
// 规则 — numbered rows, hover reveals the delete affordance
|
|
4687
5441
|
h("section", { className: "mneme-set-card" },
|
|
@@ -4922,6 +5676,10 @@ window.__ModuleLoader__.load({
|
|
|
4922
5676
|
),
|
|
4923
5677
|
apiTokenSaved && h("div", { style: { marginTop: 8 } }, h("span", { className: "mneme-saved" }, t("memory.settings.apiTokenSaved")))
|
|
4924
5678
|
),
|
|
5679
|
+
// 配置键覆盖面(issue #386 第 7 条):只读核对表,放在 connect 组尾——
|
|
5680
|
+
// 它与上面的 API Token / 独立服务同属「我在这台机器上能调什么」那一类,
|
|
5681
|
+
// 而不是功能开关(那些在 personal/core/enhance 各组里改)。
|
|
5682
|
+
h(ConfigKeysCard, { t }),
|
|
4925
5683
|
// —— 帮助:支持与鼓励(图标行 + 检测更新 + star,兼当「关于」收尾) ——
|
|
4926
5684
|
groupHead("help"),
|
|
4927
5685
|
// 反馈三入口:新建 issue 预填(版本+平台)/ 邮件 / 浏览已知问题。
|
|
@@ -4997,6 +5755,16 @@ window.__ModuleLoader__.load({
|
|
|
4997
5755
|
// 上下文裁不住它;运行时拒绝 react-dom 时就地渲染(position:fixed 仍然成立)。
|
|
4998
5756
|
function MemoryOverlay({ t }) {
|
|
4999
5757
|
const [open, setOpen] = useOverlayOpen();
|
|
5758
|
+
// 库内是否有请求在飞(目前只有「从目录引导」会置位)。Esc / 背板 / ✕ 三条
|
|
5759
|
+
// 关闭路径都要看它:关掉弹层会把 MemoryExplorer 连同引导弹层一起卸载,
|
|
5760
|
+
// 而 POST 还在飞,回来那句「新建几条、并入几条」就再没有地方显示——
|
|
5761
|
+
// 库写进去了,用户却以为没成功(评审 #4453225573)。
|
|
5762
|
+
const [explorerBusy, setExplorerBusy] = useState(false);
|
|
5763
|
+
// 用 ref 读而不是进 effect 依赖:那个 effect 在 [open] 变化时会
|
|
5764
|
+
// closeBtnRef.current.focus(),若把 explorerBusy 也列进依赖,每次请求
|
|
5765
|
+
// 起落都会重跑一次并抢焦点——用户正打字输目录时会被踢出输入框。
|
|
5766
|
+
const explorerBusyRef = useRef(false);
|
|
5767
|
+
useEffect(() => { explorerBusyRef.current = explorerBusy; }, [explorerBusy]);
|
|
5000
5768
|
// #178:弹层焦点管理——打开时把焦点移入面板(关闭按钮为入口),
|
|
5001
5769
|
// Tab 循环圈在面板内(Tab 从最后一个元素出去回到关闭按钮),
|
|
5002
5770
|
// Esc/关闭时把焦点还给触发元素(侧边栏入口)。
|
|
@@ -5005,7 +5773,8 @@ window.__ModuleLoader__.load({
|
|
|
5005
5773
|
useEffect(() => {
|
|
5006
5774
|
if (!open) return undefined;
|
|
5007
5775
|
const onKey = (e) => {
|
|
5008
|
-
|
|
5776
|
+
// 库内请求在飞时不响应 Esc:见上面 explorerBusy 的注释。
|
|
5777
|
+
if (e.key === "Escape") { if (!explorerBusyRef.current) setOpen(false); return; }
|
|
5009
5778
|
if (e.key !== "Tab" || !panelRef.current) return;
|
|
5010
5779
|
const focusables = panelRef.current.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])');
|
|
5011
5780
|
if (focusables.length === 0) return;
|
|
@@ -5033,7 +5802,7 @@ window.__ModuleLoader__.load({
|
|
|
5033
5802
|
const tree = h(react.Fragment, null,
|
|
5034
5803
|
h("div", {
|
|
5035
5804
|
className: "mneme-backdrop",
|
|
5036
|
-
onClick: () => setOpen(false),
|
|
5805
|
+
onClick: () => { if (!explorerBusyRef.current) setOpen(false); },
|
|
5037
5806
|
"aria-hidden": "true"
|
|
5038
5807
|
}),
|
|
5039
5808
|
h("div", { className: "mneme-overlay", role: "region", "aria-label": t("memory.view.label"), ref: panelRef },
|
|
@@ -5046,12 +5815,15 @@ window.__ModuleLoader__.load({
|
|
|
5046
5815
|
type: "button",
|
|
5047
5816
|
className: "mneme-footbtn",
|
|
5048
5817
|
ref: closeBtnRef,
|
|
5818
|
+
// 忙碌时不禁用、只标 aria-disabled:真的 disabled 会把焦点从按钮上
|
|
5819
|
+
// 拿走,而 #178 的 Tab 环靠 closeBtnRef 当入口,焦点落空就出不去。
|
|
5820
|
+
"aria-disabled": String(explorerBusy),
|
|
5049
5821
|
"aria-label": t("memory.overlay.close"),
|
|
5050
5822
|
title: t("memory.overlay.close"),
|
|
5051
|
-
onClick: () => setOpen(false)
|
|
5823
|
+
onClick: () => { if (!explorerBusyRef.current) setOpen(false); }
|
|
5052
5824
|
}, "✕")
|
|
5053
5825
|
),
|
|
5054
|
-
h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t }))
|
|
5826
|
+
h("div", { className: "mneme-overlaybody" }, h(MemoryExplorer, { t, onBusyChange: setExplorerBusy }))
|
|
5055
5827
|
)
|
|
5056
5828
|
);
|
|
5057
5829
|
if (reactDom && typeof document !== "undefined") return reactDom.createPortal(tree, document.body);
|
|
@@ -5123,7 +5895,7 @@ window.__ModuleLoader__.load({
|
|
|
5123
5895
|
|
|
5124
5896
|
// 实体 — directory snapshot grouped by entity type.
|
|
5125
5897
|
function EntitiesStatusCard({ t }) {
|
|
5126
|
-
const [state, setState] = useState({ loading: true, error: false, total: 0, byType: [] });
|
|
5898
|
+
const [state, setState] = useState({ loading: true, error: false, total: 0, totalKnown: true, byType: [] });
|
|
5127
5899
|
useEffect(() => {
|
|
5128
5900
|
let cancelled = false;
|
|
5129
5901
|
const order = ["organization", "person", "project", "technology", "concept"];
|
|
@@ -5140,9 +5912,16 @@ window.__ModuleLoader__.load({
|
|
|
5140
5912
|
const byType = order.concat("other")
|
|
5141
5913
|
.filter((k) => counts.has(k))
|
|
5142
5914
|
.map((k) => [k, counts.get(k)]);
|
|
5143
|
-
|
|
5915
|
+
// 大数字用后端回的全库 total;分类型小行仍来自本次切片(最近 500 个),
|
|
5916
|
+
// 两个口径不一样,切片写进 foot 里说清楚——以前 total 取 list.length,
|
|
5917
|
+
// 一万六千个实体永远显示成 500,看着像"实体太少"。
|
|
5918
|
+
// totalExact 为假(计数失败 / 老宿主)时 total 只是切片长度,不能当全库数
|
|
5919
|
+
// 显示:那正是原来「永远 500」的错法。这时大数字显示“—”,脚注说明总数读不到。
|
|
5920
|
+
const totalKnown = j.totalExact === true && typeof j.total === "number";
|
|
5921
|
+
const realTotal = totalKnown ? j.total : list.length;
|
|
5922
|
+
setState({ loading: false, error: false, total: realTotal, totalKnown, sampled: list.length < realTotal, byType });
|
|
5144
5923
|
})
|
|
5145
|
-
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, byType: [] }); });
|
|
5924
|
+
.catch(() => { if (!cancelled) setState({ loading: false, error: true, total: 0, totalKnown: true, byType: [] }); });
|
|
5146
5925
|
return () => { cancelled = true; };
|
|
5147
5926
|
}, []);
|
|
5148
5927
|
const rows = state.byType.map(([ty, n]) => ({ label: entityTypeLabel(t, ty), value: String(n) }));
|
|
@@ -5151,8 +5930,11 @@ window.__ModuleLoader__.load({
|
|
|
5151
5930
|
title: t("memory.status.entities"),
|
|
5152
5931
|
loading: state.loading,
|
|
5153
5932
|
error: state.error,
|
|
5154
|
-
num: state.total.toLocaleString(),
|
|
5155
|
-
rows
|
|
5933
|
+
num: state.totalKnown === false ? "—" : state.total.toLocaleString(),
|
|
5934
|
+
rows,
|
|
5935
|
+
foot: state.totalKnown === false
|
|
5936
|
+
? t("memory.status.entitiesUnknown")
|
|
5937
|
+
: state.sampled === true ? t("memory.status.entitiesSampled") : null
|
|
5156
5938
|
});
|
|
5157
5939
|
}
|
|
5158
5940
|
|
|
@@ -5433,6 +6215,8 @@ window.__ModuleLoader__.load({
|
|
|
5433
6215
|
setResult(r.body);
|
|
5434
6216
|
setArmed(false);
|
|
5435
6217
|
setRunning(false);
|
|
6218
|
+
// 回收确认模式真的删了行:主列表/实体页/工作台的计数都变了,广播重取。
|
|
6219
|
+
broadcastStoreChanged("reclaim");
|
|
5436
6220
|
})
|
|
5437
6221
|
.catch(() => { setResultError(t("memory.status.maint.failed")); setRunning(false); });
|
|
5438
6222
|
};
|
|
@@ -5613,6 +6397,76 @@ window.__ModuleLoader__.load({
|
|
|
5613
6397
|
});
|
|
5614
6398
|
}
|
|
5615
6399
|
|
|
6400
|
+
// --- 配置键覆盖面(issue #386 第 7 条)---
|
|
6401
|
+
// 面板能改的键(settings.js 白名单)与 schema 里真有的键是两张表,从没被摆在一起
|
|
6402
|
+
// 对过。这条卡把它们交/差出来,回答一个具体问题:**「我在配置文件里看到的这个键,
|
|
6403
|
+
// 为什么在面板上找不到」**——答案要么是「它只能改文件」(fileOnly),要么是「它
|
|
6404
|
+
// 登记了却没默认值」(undefaulted,见 config-keys.js 对 dreamProvider 的说明)。
|
|
6405
|
+
//
|
|
6406
|
+
// 只显示键名,一个值都不显示:apiToken 的名字出现在「仅配置文件」里正是这条 issue
|
|
6407
|
+
// 想让人看见的(否则用户不知道它存在),值则一律不出服务端。
|
|
6408
|
+
//
|
|
6409
|
+
// 默认收起:这是一张核对表,不是日常要看的东西。做只读清单而不是补一批输入框,
|
|
6410
|
+
// 也是维护者在 issue 里给的接手口径(「做成只读清单就能先关掉大部分价值」)。
|
|
6411
|
+
function ConfigKeysCard({ t }) {
|
|
6412
|
+
const [state, setState] = useState({ loading: true, data: null, failed: false });
|
|
6413
|
+
const [open, setOpen] = useState(false);
|
|
6414
|
+
useEffect(() => {
|
|
6415
|
+
let cancelled = false;
|
|
6416
|
+
apiFetch("/api/dsh-mneme/config-keys")
|
|
6417
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
6418
|
+
.then((j) => { if (!cancelled) setState({ loading: false, data: j, failed: false }); })
|
|
6419
|
+
.catch(() => { if (!cancelled) setState({ loading: false, data: null, failed: true }); });
|
|
6420
|
+
return () => { cancelled = true; };
|
|
6421
|
+
}, []);
|
|
6422
|
+
const d = state.data;
|
|
6423
|
+
const counts = (d && d.counts) || null;
|
|
6424
|
+
const fileOnly = (d && d.fileOnly) || [];
|
|
6425
|
+
const undefaulted = (d && d.undefaulted) || [];
|
|
6426
|
+
const title = t("memory.settings.configKeys.title");
|
|
6427
|
+
const body = state.loading
|
|
6428
|
+
? h("div", { className: "mneme-set-hint" }, "…")
|
|
6429
|
+
// 取数失败与「真的没有只读键」必须分开说:把失败画成 0 条,人会以为清单就是空的。
|
|
6430
|
+
: state.failed || !counts
|
|
6431
|
+
? h("div", { className: "mneme-set-hint", style: { color: "var(--mneme-danger)" } },
|
|
6432
|
+
t("memory.settings.configKeys.loadFailed"))
|
|
6433
|
+
: h(react.Fragment, null,
|
|
6434
|
+
h("div", { className: "mneme-set-hint", style: { marginTop: 4 } },
|
|
6435
|
+
t("memory.settings.configKeys.summary")
|
|
6436
|
+
.replace("{schema}", String(counts.schemaLeaf))
|
|
6437
|
+
.replace("{panel}", String(counts.panelEditable))
|
|
6438
|
+
.replace("{file}", String(counts.fileOnly))
|
|
6439
|
+
// 三类的数字必须都来自服务端算出来的桶:写成 schema − panel 会与展开后
|
|
6440
|
+
// 「只能改文件」那段对不上(评审 #4452245244),因为 schema 之外还多出
|
|
6441
|
+
// 白名单里登记了却没默认值的那几个键。
|
|
6442
|
+
.replace("{undefaulted}", String(counts.undefaulted))),
|
|
6443
|
+
open && h("div", { style: { marginTop: 10 } },
|
|
6444
|
+
// 两段各自的标题也带上条数,收起时只看 summary 就够判断要不要展开。
|
|
6445
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 2 } },
|
|
6446
|
+
t("memory.settings.configKeys.fileOnlyHead")
|
|
6447
|
+
.replace("{n}", String(fileOnly.length))),
|
|
6448
|
+
h("div", { className: "mneme-keylist" },
|
|
6449
|
+
fileOnly.map((k) => h("code", { key: k, className: "mneme-keychip" }, k))),
|
|
6450
|
+
undefaulted.length > 0 && h(react.Fragment, null,
|
|
6451
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 10 } },
|
|
6452
|
+
t("memory.settings.configKeys.undefaultedHead")
|
|
6453
|
+
.replace("{n}", String(undefaulted.length))),
|
|
6454
|
+
h("div", { className: "mneme-keylist" },
|
|
6455
|
+
undefaulted.map((k) => h("code", { key: k, className: "mneme-keychip" }, k))),
|
|
6456
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } },
|
|
6457
|
+
t("memory.settings.configKeys.undefaultedHint"))
|
|
6458
|
+
)
|
|
6459
|
+
),
|
|
6460
|
+
h("button", { type: "button", className: "mneme-btn", style: { marginTop: 10 }, onClick: () => setOpen(!open) },
|
|
6461
|
+
open ? t("memory.settings.configKeys.hide") : t("memory.settings.configKeys.show"))
|
|
6462
|
+
);
|
|
6463
|
+
return h("section", { className: "mneme-set-card" },
|
|
6464
|
+
h("div", { className: "mneme-set-title" }, title),
|
|
6465
|
+
h("div", { className: "mneme-set-desc" }, t("memory.settings.configKeys.desc")),
|
|
6466
|
+
body
|
|
6467
|
+
);
|
|
6468
|
+
}
|
|
6469
|
+
|
|
5616
6470
|
// --- Token 去向三屏(2026-10 可观测性改造)---
|
|
5617
6471
|
// 原 LlmStatusCard 只有一个「LLM 消耗」总数,且 by_source 把两个最大成本项
|
|
5618
6472
|
// dream_consolidate / dream_summarize 合并成 autoDream(2026-10 审计里巩固
|
|
@@ -5654,6 +6508,11 @@ window.__ModuleLoader__.load({
|
|
|
5654
6508
|
function TokenFlowSection({ t }) {
|
|
5655
6509
|
const [stats, setStats] = useState(null);
|
|
5656
6510
|
const [heatStats, setHeatStats] = useState(null);
|
|
6511
|
+
// 沉淀曲线(/semantic/consolidation-stats)与 token 审计不同源:前者读
|
|
6512
|
+
// memories.created_at / receipt_chain / archived_at,与「花了多少 token」
|
|
6513
|
+
// 无关,所以是独立请求、独立 error 位——一个失败不该把另一个图也拖成错误态。
|
|
6514
|
+
const [sinkStats, setSinkStats] = useState(null);
|
|
6515
|
+
const [sinkError, setSinkError] = useState(false);
|
|
5657
6516
|
const [statsError, setStatsError] = useState(false);
|
|
5658
6517
|
// #521 评审:待确认冲突与「上一轮跳过明细」同源(/dream-status),一次请求两用。
|
|
5659
6518
|
// 跳过明细必须出现在首屏——它原先只挂在折叠区里的 DreamStatusCards 上,而那个
|
|
@@ -5680,6 +6539,14 @@ window.__ModuleLoader__.load({
|
|
|
5680
6539
|
setHeatStats(j && typeof j === "object" ? (j.stats || j) : {});
|
|
5681
6540
|
})
|
|
5682
6541
|
.catch(() => { if (!cancelled) setHeatStats({}); });
|
|
6542
|
+
// 沉淀曲线同样要 56 天窗口,理由与热力一致(7 天的 by_day 撑不满 8 周)。
|
|
6543
|
+
apiFetch("/api/dsh-mneme/semantic/consolidation-stats?days=56")
|
|
6544
|
+
.then((res) => { if (!res.ok) throw new Error("http"); return res.json(); })
|
|
6545
|
+
.then((j) => {
|
|
6546
|
+
if (cancelled) return;
|
|
6547
|
+
setSinkStats(j && typeof j === "object" ? j : {});
|
|
6548
|
+
})
|
|
6549
|
+
.catch(() => { if (!cancelled) setSinkError(true); });
|
|
5683
6550
|
// KPI 第 4 格的待确认冲突与 DreamStatusCards 同源(/dream-status),
|
|
5684
6551
|
// 同样监听 mneme:conflicts-changed 重取(#295),两处计数永不漂移。
|
|
5685
6552
|
const loadConflicts = () => {
|
|
@@ -5703,7 +6570,14 @@ window.__ModuleLoader__.load({
|
|
|
5703
6570
|
};
|
|
5704
6571
|
loadConflicts();
|
|
5705
6572
|
window.addEventListener("mneme:conflicts-changed", loadConflicts);
|
|
5706
|
-
|
|
6573
|
+
// 通用失效广播:KPI 第 4 格的待确认计数不只随裁决变——涉及冲突的
|
|
6574
|
+
// 记忆被删除/恢复也一样,两个事件都听,取数只有一次 GET。
|
|
6575
|
+
window.addEventListener("mneme:store-changed", loadConflicts);
|
|
6576
|
+
return () => {
|
|
6577
|
+
cancelled = true;
|
|
6578
|
+
window.removeEventListener("mneme:conflicts-changed", loadConflicts);
|
|
6579
|
+
window.removeEventListener("mneme:store-changed", loadConflicts);
|
|
6580
|
+
};
|
|
5707
6581
|
}, []);
|
|
5708
6582
|
|
|
5709
6583
|
const loading = !stats && !statsError;
|
|
@@ -5799,6 +6673,42 @@ window.__ModuleLoader__.load({
|
|
|
5799
6673
|
for (const d of [1, 3, 5]) rows[(d - startDow + 7) % 7] = ["一", "三", "五"][[1, 3, 5].indexOf(d)];
|
|
5800
6674
|
return rows;
|
|
5801
6675
|
})();
|
|
6676
|
+
// 8 周沉淀:56 天原始序列在热力右侧那条约 220px 的竖列里会细到看不出,
|
|
6677
|
+
// 所以按周聚成 8 桶(右端对齐今天 UTC,与 7 天柱状同一套「补连续区间」
|
|
6678
|
+
// 口径,缺周填 0)。三条线只画两条——新增与巩固是这张图要表达的剪刀差,
|
|
6679
|
+
// 归档数改在图例里给总量(它从 10-12 才开始非零,混进同一坐标会误导)。
|
|
6680
|
+
// sinkStats 为 null(未到)与 {}(已知为空)必须分开:前者不画图,后者
|
|
6681
|
+
// 画空态,否则加载态会先画 8 根零高柱子。
|
|
6682
|
+
const sinkDays = (() => {
|
|
6683
|
+
if (!sinkStats) return null;
|
|
6684
|
+
const rows = Array.isArray(sinkStats.by_day) ? sinkStats.by_day : [];
|
|
6685
|
+
const byDay = new Map(rows.map((d) => [String((d && d.day) ?? ""), d]));
|
|
6686
|
+
const out = [];
|
|
6687
|
+
for (let w = 7; w >= 0; w--) {
|
|
6688
|
+
let added = 0;
|
|
6689
|
+
let merged = 0;
|
|
6690
|
+
let archived = 0;
|
|
6691
|
+
let first = "";
|
|
6692
|
+
for (let i = 6; i >= 0; i--) {
|
|
6693
|
+
const key = new Date(endDay.getTime() - (w * 7 + i) * 86400000).toISOString().slice(0, 10);
|
|
6694
|
+
if (!first) first = key;
|
|
6695
|
+
const row = byDay.get(key);
|
|
6696
|
+
added += Number((row && row.added) ?? 0);
|
|
6697
|
+
merged += Number((row && row.merged) ?? 0);
|
|
6698
|
+
archived += Number((row && row.archived) ?? 0);
|
|
6699
|
+
}
|
|
6700
|
+
out.push({ key: first, added, merged, archived });
|
|
6701
|
+
}
|
|
6702
|
+
return out;
|
|
6703
|
+
})();
|
|
6704
|
+
const sinkMax = Math.max(1, ...((sinkDays ?? []).map((d) => d.added)));
|
|
6705
|
+
const sinkSum = (field) => (sinkDays ?? []).reduce((a, d) => a + d[field], 0);
|
|
6706
|
+
const sinkAddedTotal = sinkSum("added");
|
|
6707
|
+
const sinkMergedTotal = sinkSum("merged");
|
|
6708
|
+
const sinkArchivedTotal = sinkSum("archived");
|
|
6709
|
+
// 巩固/新增的比值取自总量而非逐周:单周可能新增为 0(无分母),总量口径
|
|
6710
|
+
// 才是「这个库到底在膨胀还是在巩固」的答案。
|
|
6711
|
+
const sinkGap = sinkAddedTotal > 0 ? `${((sinkMergedTotal / sinkAddedTotal) * 100).toFixed(0)}%` : "—";
|
|
5802
6712
|
const kpi = (title, num, cap) =>
|
|
5803
6713
|
h(StatusCard, { t, title, loading, error: statsError, num, cap });
|
|
5804
6714
|
const segClass = (s) =>
|
|
@@ -5892,29 +6802,76 @@ window.__ModuleLoader__.load({
|
|
|
5892
6802
|
h("div", { className: "mneme-barlabels" },
|
|
5893
6803
|
days.map((d) => h("span", { key: d.day }, d.day.slice(5)))
|
|
5894
6804
|
),
|
|
5895
|
-
|
|
5896
|
-
//
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
6805
|
+
// 热力与沉淀并排两列:热力只有约 745px,右侧原本整块空着。两列
|
|
6806
|
+
// 在窄容器里回落单列(见 .mneme-flowrow 的 @container)。
|
|
6807
|
+
h("div", { className: "mneme-flowrow" },
|
|
6808
|
+
h("div", { className: "mneme-flowheat" },
|
|
6809
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.heat")),
|
|
6810
|
+
// 月份行与格子列同用列主序网格对齐:每月占的列数 = 该月在这个
|
|
6811
|
+
// 56 天窗口里的列数(按列的起始日期算),标签只在该列起点画一次。
|
|
6812
|
+
h("div", { className: "mneme-heatmonths" },
|
|
6813
|
+
monthLabels.map((m) => h("span", { key: m.key, style: { gridColumn: `${m.from} / span ${m.span}` } }, m.label))
|
|
6814
|
+
),
|
|
6815
|
+
h("div", { className: "mneme-heatwrap" },
|
|
6816
|
+
h("div", { className: "mneme-heatrows", "aria-hidden": "true" },
|
|
6817
|
+
heatRowLabels.map((d, i) => h("span", { key: i }, d))
|
|
6818
|
+
),
|
|
6819
|
+
h("div", { className: "mneme-heatgrid" },
|
|
6820
|
+
heatCells.map((c) => h("div", {
|
|
6821
|
+
key: c.key,
|
|
6822
|
+
className: heatLevel(c.total, heatQ) ? `mneme-heatcell l${heatLevel(c.total, heatQ)}` : "mneme-heatcell",
|
|
6823
|
+
title: `${c.key} · ${c.total.toLocaleString()}`
|
|
6824
|
+
}))
|
|
6825
|
+
)
|
|
6826
|
+
),
|
|
6827
|
+
h("div", { className: "mneme-heatlegend" },
|
|
6828
|
+
h("span", null, t("memory.status.token.heatLess")),
|
|
6829
|
+
h("span", { className: "mneme-heatcell" }),
|
|
6830
|
+
[1, 2, 3, 4].map((lv) => h("span", { key: lv, className: `mneme-heatcell l${lv}` })),
|
|
6831
|
+
h("span", null, t("memory.status.token.heatMore"))
|
|
6832
|
+
)
|
|
5904
6833
|
),
|
|
5905
|
-
h("div", { className: "mneme-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
6834
|
+
h("div", { className: "mneme-flowsink" },
|
|
6835
|
+
h("h3", { className: "mneme-xcolhead" }, t("memory.status.token.sink")),
|
|
6836
|
+
sinkError
|
|
6837
|
+
? h("div", { className: "mneme-statuscap", style: { color: "var(--mneme-danger)" } }, t("memory.status.error"))
|
|
6838
|
+
: !sinkDays
|
|
6839
|
+
// 未到:什么都不画。画空态会把「还没加载完」说成「最近没有记录」。
|
|
6840
|
+
? null
|
|
6841
|
+
: sinkAddedTotal + sinkMergedTotal + sinkArchivedTotal === 0
|
|
6842
|
+
? h("div", { className: "mneme-statuscap", style: { marginTop: "12px" } }, t("memory.status.token.sinkEmpty"))
|
|
6843
|
+
: h("div", { className: "mneme-sinkchart" },
|
|
6844
|
+
(sinkDays ?? []).map((d) => h("div", { className: "mneme-sinkcol", key: d.key },
|
|
6845
|
+
// 两根柱同一基准(本期最大新增),高度差就是剪刀差本身;
|
|
6846
|
+
// 巩固可能远小于新增,故给 1px 下限免得整根消失。
|
|
6847
|
+
h("div", {
|
|
6848
|
+
className: "mneme-sinkbar mneme-sinkbar--added",
|
|
6849
|
+
style: { height: `${Math.max((d.added / sinkMax) * 100, d.added > 0 ? 1 : 0)}%` },
|
|
6850
|
+
title: `${d.key} · ${t("memory.status.token.sinkAdded")} ${d.added}`
|
|
6851
|
+
}),
|
|
6852
|
+
h("div", {
|
|
6853
|
+
className: "mneme-sinkbar mneme-sinkbar--merged",
|
|
6854
|
+
style: { height: `${Math.max((d.merged / sinkMax) * 100, d.merged > 0 ? 1 : 0)}%` },
|
|
6855
|
+
title: `${d.key} · ${t("memory.status.token.sinkMerged")} ${d.merged}`
|
|
6856
|
+
})
|
|
6857
|
+
))
|
|
6858
|
+
),
|
|
6859
|
+
sinkDays && !sinkError
|
|
6860
|
+
? h("div", { className: "mneme-sinklabels" },
|
|
6861
|
+
sinkDays.map((d) => h("span", { key: d.key }, d.key.slice(5)))
|
|
6862
|
+
)
|
|
6863
|
+
: null,
|
|
6864
|
+
// 图例只在有数据时给:加载中把三个 0 画出来等于谎报「最近没东西进库」。
|
|
6865
|
+
sinkDays && !sinkError
|
|
6866
|
+
? h("div", { className: "mneme-sinklegend" },
|
|
6867
|
+
h("span", null, `${t("memory.status.token.sinkAdded")} `, h("b", null, sinkAddedTotal.toLocaleString())),
|
|
6868
|
+
h("span", null, `${t("memory.status.token.sinkMerged")} `, h("b", null, sinkMergedTotal.toLocaleString())),
|
|
6869
|
+
h("span", null, `${t("memory.status.token.sinkArchived")} `, h("b", null, sinkArchivedTotal.toLocaleString())),
|
|
6870
|
+
h("span", null, `${t("memory.status.token.sinkGap")} `, h("b", null, sinkGap))
|
|
6871
|
+
)
|
|
6872
|
+
: null,
|
|
6873
|
+
h("div", { className: "mneme-statuscap", style: { marginTop: "6px", fontSize: "11px" } }, t("memory.status.token.sinkFoot"))
|
|
5911
6874
|
)
|
|
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
6875
|
)
|
|
5919
6876
|
)
|
|
5920
6877
|
);
|
|
@@ -5967,9 +6924,15 @@ window.__ModuleLoader__.load({
|
|
|
5967
6924
|
};
|
|
5968
6925
|
loadOnce();
|
|
5969
6926
|
// #295 评审:裁决完成后队列会广播 mneme:conflicts-changed,这里重取
|
|
5970
|
-
//
|
|
6927
|
+
// 计数,状态卡的「待确认冲突」不再停留旧值。涉及冲突的记忆被删除/
|
|
6928
|
+
// 恢复同样改变计数,通用失效广播一并听。
|
|
5971
6929
|
window.addEventListener("mneme:conflicts-changed", loadOnce);
|
|
5972
|
-
|
|
6930
|
+
window.addEventListener("mneme:store-changed", loadOnce);
|
|
6931
|
+
return () => {
|
|
6932
|
+
cancelled = true;
|
|
6933
|
+
window.removeEventListener("mneme:conflicts-changed", loadOnce);
|
|
6934
|
+
window.removeEventListener("mneme:store-changed", loadOnce);
|
|
6935
|
+
};
|
|
5973
6936
|
}, []);
|
|
5974
6937
|
// #495:这张卡的标题是「最近巩固」,取数就该是巩固轮,而不是 runs[0]。
|
|
5975
6938
|
// organize 与巩固共用 dream_runs,一次 agent 主动整理会写两行(dryRun 报告 +
|
|
@@ -6123,6 +7086,14 @@ window.__ModuleLoader__.load({
|
|
|
6123
7086
|
.catch(() => { setItems([]); setLoadError(true); });
|
|
6124
7087
|
};
|
|
6125
7088
|
useEffect(() => { load(); }, []);
|
|
7089
|
+
// 参与记忆在别处被删除/编辑时(通用失效广播),队列联表出的那一侧会失真
|
|
7090
|
+
// ——重取,宁可多一次 GET 也不把「已失联」装成「还在」。
|
|
7091
|
+
useEffect(() => {
|
|
7092
|
+
const onChanged = () => load(true);
|
|
7093
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
7094
|
+
return () => window.removeEventListener("mneme:store-changed", onChanged);
|
|
7095
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
7096
|
+
}, []);
|
|
6126
7097
|
// #177:空队列不再整块消失——空态教育卡要渲染(原 return null 已移除)。
|
|
6127
7098
|
if (!items) return null;
|
|
6128
7099
|
const resolve = (id, winner) => {
|
|
@@ -6143,6 +7114,10 @@ window.__ModuleLoader__.load({
|
|
|
6143
7114
|
// 读一次 dream-status,不广播的话状态卡停留在旧计数。同页两个组件
|
|
6144
7115
|
// 用一个 window 事件对齐(比提升状态到 StatusPanel 少动三处)。
|
|
6145
7116
|
try { window.dispatchEvent(new CustomEvent("mneme:conflicts-changed")); } catch { /* 非浏览器环境 */ }
|
|
7117
|
+
// 裁决会归档败者、给胜者追加已否决注记——记忆列表、实体页、归档
|
|
7118
|
+
// 工作台的数据全变了,走通用失效广播让它们立刻重取(用户反馈:
|
|
7119
|
+
// 以前要关面板重开才能看到裁决落地)。
|
|
7120
|
+
broadcastStoreChanged("conflict-resolve");
|
|
6146
7121
|
load(true);
|
|
6147
7122
|
})
|
|
6148
7123
|
.catch(() => {})
|
|
@@ -6310,6 +7285,13 @@ window.__ModuleLoader__.load({
|
|
|
6310
7285
|
return () => { cancelled = true; };
|
|
6311
7286
|
}, [wbReload]);
|
|
6312
7287
|
useEffect(() => { load(); }, [load]);
|
|
7288
|
+
// 工作台三份列表(活动流/沉淀/归档)都读库:别处改库(裁决/删除/导入…)
|
|
7289
|
+
// 广播过来就整组重取——此前只有本组件自己的恢复按钮会刷新它。
|
|
7290
|
+
useEffect(() => {
|
|
7291
|
+
const onChanged = () => setWbReload((k) => k + 1);
|
|
7292
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
7293
|
+
return () => window.removeEventListener("mneme:store-changed", onChanged);
|
|
7294
|
+
}, []);
|
|
6313
7295
|
|
|
6314
7296
|
const restore = (id) => {
|
|
6315
7297
|
setRestoring(id);
|
|
@@ -6322,6 +7304,8 @@ window.__ModuleLoader__.load({
|
|
|
6322
7304
|
.then(() => {
|
|
6323
7305
|
setArchived((cur) => (cur || []).filter((m) => m.id !== id));
|
|
6324
7306
|
setArchivedTotal((n) => Math.max(0, n - 1));
|
|
7307
|
+
// 恢复让行回到主列表:记忆页与实体页不知道这次 /update,广播让它们重取。
|
|
7308
|
+
broadcastStoreChanged("memory-restore");
|
|
6325
7309
|
})
|
|
6326
7310
|
.catch(() => {})
|
|
6327
7311
|
.finally(() => setRestoring(""));
|
|
@@ -7011,6 +7995,98 @@ window.__ModuleLoader__.load({
|
|
|
7011
7995
|
);
|
|
7012
7996
|
}
|
|
7013
7997
|
|
|
7998
|
+
// --- 从目录引导弹层(issue #386 第 6 条)---
|
|
7999
|
+
// 后端能力早在 #220 就实现了(src/bootstrap.js:六类确定性产物,零 LLM,吃
|
|
8000
|
+
// saveWithDedupe 的 (type,title,scope) 去重),但只有 Bearer 数据面一条入口,
|
|
8001
|
+
// 面板够不着——用户照文档找过来,发现产品界面里没有这个东西。
|
|
8002
|
+
//
|
|
8003
|
+
// 与 ImportDialog 的两处刻意的不同:
|
|
8004
|
+
// 1. 成功路径**不**重复 setBusy(false)。这里没有「同键再提交一次会追加正文」的
|
|
8005
|
+
// 风险(bootstrap 是 _overwrite 刷新),但 900ms 的提示阅读时间同样成立;
|
|
8006
|
+
// 更要紧的是这一段里组件仍挂着,按钮若重新可点,人会以为「没反应」再点一次。
|
|
8007
|
+
// 定时器句柄存进 ref,卸载时清掉——否则第二次提交的结果会写进已卸载的组件
|
|
8008
|
+
// (#386 第 6 条评审)。
|
|
8009
|
+
// 2. 错误按服务端的码分开说。「目录不存在」「不是目录」「能力没接线」三种情况下
|
|
8010
|
+
// 用户该做的事完全不同,统一成一句「引导失败」等于让他自己猜。
|
|
8011
|
+
function BootstrapDialog({ t, onClose, onImported, onBusyChange }) {
|
|
8012
|
+
const [dir, setDir] = useState("");
|
|
8013
|
+
const [busy, setBusy] = useState(false);
|
|
8014
|
+
const [msg, setMsg] = useState("");
|
|
8015
|
+
const [err, setErr] = useState("");
|
|
8016
|
+
const timerRef = useRef(null);
|
|
8017
|
+
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
|
8018
|
+
// 忙碌状态要让外面也知道:菜单那个「⋯」在请求进行中若把弹层卸掉,等于绕过
|
|
8019
|
+
// 了下面那两个 disabled 的按钮,请求还在飞、界面已经没了(评审 #4452245292)。
|
|
8020
|
+
useEffect(() => { if (onBusyChange) onBusyChange(busy); }, [busy, onBusyChange]);
|
|
8021
|
+
useEffect(() => () => { if (onBusyChange) onBusyChange(false); }, [onBusyChange]);
|
|
8022
|
+
const submit = () => {
|
|
8023
|
+
if (busy) return;
|
|
8024
|
+
const target = String(dir || "").trim();
|
|
8025
|
+
if (!target) { setErr(t("memory.explorer.bootstrapNoDir")); return; }
|
|
8026
|
+
setBusy(true);
|
|
8027
|
+
// 同步告诉父组件,别等下面那个 effect:effect 要等这次渲染提交后才跑,
|
|
8028
|
+
// 中间那一拍父组件的 bootstrapBusy 还是 false,此时点「⋯」仍会把弹层卸掉,
|
|
8029
|
+
// 请求的成功或失败就再没有地方显示(评审 #4452539705)。
|
|
8030
|
+
if (onBusyChange) onBusyChange(true);
|
|
8031
|
+
setErr("");
|
|
8032
|
+
apiFetch("/api/dsh-mneme/bootstrap", {
|
|
8033
|
+
method: "POST",
|
|
8034
|
+
headers: { "Content-Type": "application/json" },
|
|
8035
|
+
// 传原串(不 trim):前后空格在多数文件系统里是合法路径的一部分,
|
|
8036
|
+
// 服务端也只是 trim 判空、回传原值。
|
|
8037
|
+
body: JSON.stringify({ dir })
|
|
8038
|
+
})
|
|
8039
|
+
.then((res) => res.json().then((d) => ({ ok: res.ok, d })))
|
|
8040
|
+
.then(({ ok, d }) => {
|
|
8041
|
+
if (!ok) {
|
|
8042
|
+
const code = d && d.error;
|
|
8043
|
+
const key = "memory.explorer.bootstrapErr." + String(code || "");
|
|
8044
|
+
const localized = t(key);
|
|
8045
|
+
// t() 对未知键原样返回键名——那就退回通用文案,别把键名甩给用户。
|
|
8046
|
+
setErr(localized === key ? t("memory.explorer.bootstrapFailed") : localized);
|
|
8047
|
+
setBusy(false);
|
|
8048
|
+
return;
|
|
8049
|
+
}
|
|
8050
|
+
setMsg(t("memory.explorer.bootstrapDone")
|
|
8051
|
+
.replace("{n}", String((d && d.created) ?? 0))
|
|
8052
|
+
.replace("{m}", String((d && d.merged) ?? 0)));
|
|
8053
|
+
// onImported 同步调,只把 onClose 留在定时器里:切视图会卸载本组件并清掉
|
|
8054
|
+
// timerRef,若刷新也排在那 900ms 之后,这一步就静默丢了——库已经写进去了,
|
|
8055
|
+
// 列表却还是旧的(评审 #4452539711)。
|
|
8056
|
+
onImported();
|
|
8057
|
+
timerRef.current = setTimeout(() => onClose(), 900);
|
|
8058
|
+
})
|
|
8059
|
+
.catch(() => { setBusy(false); setErr(t("memory.explorer.bootstrapFailed")); });
|
|
8060
|
+
};
|
|
8061
|
+
return h("div", { className: "mneme-impdialog", role: "region", "aria-label": t("memory.explorer.bootstrapTitle") },
|
|
8062
|
+
h("div", { className: "mneme-set-title" }, t("memory.explorer.bootstrapTitle")),
|
|
8063
|
+
h("div", { className: "mneme-featsubhint", style: { marginTop: 4 } }, t("memory.explorer.bootstrapHint")),
|
|
8064
|
+
h("div", { className: "mneme-improw" },
|
|
8065
|
+
h("span", { className: "mneme-featnumlabel" }, t("memory.explorer.bootstrapDir")),
|
|
8066
|
+
// 用文本框而不是 <input webkitdirectory>:后者给的是「选了哪个目录里的
|
|
8067
|
+
// 一批文件」,拿不到目录本身,而 bootstrap 要的正是那个目录。
|
|
8068
|
+
h("input", {
|
|
8069
|
+
className: "mneme-set-input",
|
|
8070
|
+
style: { marginBottom: 0, flex: 1, minWidth: 0 },
|
|
8071
|
+
type: "text",
|
|
8072
|
+
value: dir,
|
|
8073
|
+
placeholder: "/path/to/project",
|
|
8074
|
+
// 请求进行中锁住输入框:否则用户改成 B,回来的提示就贴在 B 旁边,
|
|
8075
|
+
// 而它说的是 A 的结果(评审 #4452245279)。
|
|
8076
|
+
disabled: busy,
|
|
8077
|
+
onChange: (e) => { setDir(e.target.value); setErr(""); }
|
|
8078
|
+
})
|
|
8079
|
+
),
|
|
8080
|
+
err && h("div", { className: "mneme-featsubhint", style: { color: "var(--mneme-danger)", marginTop: 8 } }, err),
|
|
8081
|
+
msg && h("div", { className: "mneme-saved", style: { marginTop: 8 } }, msg),
|
|
8082
|
+
h("div", { className: "mneme-improw", style: { justifyContent: "flex-end" } },
|
|
8083
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: onClose }, t("memory.explorer.importCancel")),
|
|
8084
|
+
h("button", { type: "button", className: "mneme-btn", disabled: busy, onClick: submit },
|
|
8085
|
+
busy ? t("memory.explorer.bootstraping") : t("memory.explorer.bootstrapConfirm"))
|
|
8086
|
+
)
|
|
8087
|
+
);
|
|
8088
|
+
}
|
|
8089
|
+
|
|
7014
8090
|
// --- 分享包弹层 ---
|
|
7015
8091
|
// 这一面的支点只有一句:**看见 report 之前不给下载**。服务端一次 POST 就把包和报告
|
|
7016
8092
|
// 一起交回,所以这一格拦不住靠协议、只能靠界面自己:CLI 那侧没人拦得住,面板至少要让
|
|
@@ -7454,7 +8530,7 @@ window.__ModuleLoader__.load({
|
|
|
7454
8530
|
);
|
|
7455
8531
|
}
|
|
7456
8532
|
|
|
7457
|
-
function MemoryExplorer({ t }) {
|
|
8533
|
+
function MemoryExplorer({ t, onBusyChange }) {
|
|
7458
8534
|
const [view, setView] = useState("memory"); // memory | entity | status | settings
|
|
7459
8535
|
const [items, setItems] = useState([]); // loaded browse pages, chrono desc
|
|
7460
8536
|
const [total, setTotal] = useState(0); // server count for the current type filter
|
|
@@ -7490,6 +8566,21 @@ window.__ModuleLoader__.load({
|
|
|
7490
8566
|
const [pendingIds, setPendingIds] = useState(() => new Set());
|
|
7491
8567
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
7492
8568
|
const [importOpen, setImportOpen] = useState(false);
|
|
8569
|
+
const [bootstrapOpen, setBootstrapOpen] = useState(false);
|
|
8570
|
+
// 引导请求是否在飞:「⋯」按钮在忙碌时必须不关弹层(评审 #4452245292)。
|
|
8571
|
+
const [bootstrapBusy, setBootstrapBusy] = useState(false);
|
|
8572
|
+
// 一路直接抬到外层 MemoryOverlay:它要在 Esc / 背板 / ✕ 三条关闭路径上一起挡
|
|
8573
|
+
// (评审 #4453225573)。刻意**不**用 useEffect 转报——那要等渲染提交后才跑,
|
|
8574
|
+
// 中间那一拍外层守卫读到的是 false,正是 #4452539705 那条的老毛病。
|
|
8575
|
+
// 走 BootstrapDialog 的回调,它的 submit 里是同步调的。
|
|
8576
|
+
// better-sidebar 那个 tab 注册时不传 onBusyChange,所以这里必须容忍 undefined。
|
|
8577
|
+
// 身份必须稳定:BootstrapDialog 的卸载 effect 依赖 [onBusyChange],父组件每
|
|
8578
|
+
// 重渲染一次就会先跑 cleanup 调 onBusyChange(false)——而父组件恰好是在
|
|
8579
|
+
// setBootstrapBusy(true) 之后重渲染的,那一抹等于把刚置上的忙碌标记当场抹掉。
|
|
8580
|
+
const reportBootstrapBusy = useCallback((v) => {
|
|
8581
|
+
setBootstrapBusy(v);
|
|
8582
|
+
if (onBusyChange) onBusyChange(v);
|
|
8583
|
+
}, [onBusyChange]);
|
|
7493
8584
|
const [createOpen, setCreateOpen] = useState(false);
|
|
7494
8585
|
const [shareOpen, setShareOpen] = useState(false);
|
|
7495
8586
|
const [receiveOpen, setReceiveOpen] = useState(false);
|
|
@@ -7519,6 +8610,16 @@ window.__ModuleLoader__.load({
|
|
|
7519
8610
|
return () => { cancelled = true; };
|
|
7520
8611
|
}, [reloadKey]);
|
|
7521
8612
|
|
|
8613
|
+
// 通用失效广播订阅:任何改库动作(裁决/删除/编辑/导入/恢复…)都让首页
|
|
8614
|
+
// 与冲突徽章立刻重取。此前列表只靠弹层成功回调、手动刷新与 30s 首页合并
|
|
8615
|
+
// 续命——裁决冲突后徽章与败者归档要关面板重开才更新(用户反馈的病灶)。
|
|
8616
|
+
// reloadKey 同时驱动浏览列表、搜索与上面这份 pendingIds,一个游标全带上。
|
|
8617
|
+
useEffect(() => {
|
|
8618
|
+
const onChanged = () => setReloadKey((k) => k + 1);
|
|
8619
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
8620
|
+
return () => window.removeEventListener("mneme:store-changed", onChanged);
|
|
8621
|
+
}, []);
|
|
8622
|
+
|
|
7522
8623
|
// One query string behind every browse-list fetch (initial page,
|
|
7523
8624
|
// loadMore, auto-refresh): the type filter, importance floor and time
|
|
7524
8625
|
// window always travel together, so counts and pagination stay
|
|
@@ -7593,7 +8694,10 @@ window.__ModuleLoader__.load({
|
|
|
7593
8694
|
.catch(() => { if (!cancelled) setRemoteItems([]); });
|
|
7594
8695
|
}, 250);
|
|
7595
8696
|
return () => { cancelled = true; clearTimeout(timer); };
|
|
7596
|
-
|
|
8697
|
+
// reloadKey 必须进依赖:导入/引导/接收成功时只递增它,而搜索态下 items 走
|
|
8698
|
+
// remoteItems 那条路——少了这一格,搜索词不变就永远看不到刚进来的新行
|
|
8699
|
+
// (评审 #4452245305)。
|
|
8700
|
+
}, [query, semantic, searchTopK, reloadKey]);
|
|
7597
8701
|
|
|
7598
8702
|
// Live view: while the memory tab is open, quietly re-fetch page 1
|
|
7599
8703
|
// every 30s (and on every activation) so fresh memories surface
|
|
@@ -7712,6 +8816,8 @@ window.__ModuleLoader__.load({
|
|
|
7712
8816
|
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== id) : cur));
|
|
7713
8817
|
setTotal((n) => Math.max(0, n - 1));
|
|
7714
8818
|
setSelectedId(null);
|
|
8819
|
+
// 删除同样广播:冲突队列的联表侧、实体页的关联记忆都引用这条 id。
|
|
8820
|
+
broadcastStoreChanged("memory-delete");
|
|
7715
8821
|
setToastMsg(t("memory.explorer.deleted"));
|
|
7716
8822
|
setTimeout(() => setToastMsg(""), 2500);
|
|
7717
8823
|
})
|
|
@@ -7724,7 +8830,10 @@ window.__ModuleLoader__.load({
|
|
|
7724
8830
|
|
|
7725
8831
|
// 编辑保存 / 归档的本地落账:替换或移除对应行;镜像由后端重渲染。
|
|
7726
8832
|
// 归档从所有视图移除(列表默认只看未归档),并清空选中。
|
|
8833
|
+
// 调用点在服务端提交成功之后,所以这里同时是通用失效广播的发布点:
|
|
8834
|
+
// 其它页签(实体/状态工作台/冲突队列)不知道这次 /update,不广播就陈旧。
|
|
7727
8835
|
const applyMemoryUpdate = (updated, { archived = false, restored = false } = {}) => {
|
|
8836
|
+
broadcastStoreChanged(archived ? "memory-archive" : restored ? "memory-restore" : "memory-edit");
|
|
7728
8837
|
if (archived) {
|
|
7729
8838
|
setItems((cur) => cur.filter((m) => m.id !== updated.id));
|
|
7730
8839
|
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
|
|
@@ -7785,7 +8894,18 @@ window.__ModuleLoader__.load({
|
|
|
7785
8894
|
setSelectedId(target.id);
|
|
7786
8895
|
};
|
|
7787
8896
|
|
|
8897
|
+
// 引导请求在飞时不许离开记忆视图(评审 #4453639221):`.mneme-xtools` 整块挂在
|
|
8898
|
+
// `view === "memory"` 之下,BootstrapDialog 就在里面。任何离开这个视图的动作都会
|
|
8899
|
+
// 连它一起卸载——请求还在飞、界面已经没了,那句「新建几条、并入几条」就再没有
|
|
8900
|
+
// 地方显示。这里只拦「离开」的路径:jumpToMemory / browseWithFilter 是回到
|
|
8901
|
+
// memory,不拦。
|
|
8902
|
+
const leaveMemory = (key) => {
|
|
8903
|
+
if (bootstrapBusy) return;
|
|
8904
|
+
setView(key);
|
|
8905
|
+
};
|
|
8906
|
+
|
|
7788
8907
|
const openGraphFor = (name) => {
|
|
8908
|
+
if (bootstrapBusy) return;
|
|
7789
8909
|
setGraphFocus(name || "");
|
|
7790
8910
|
setView("entity");
|
|
7791
8911
|
};
|
|
@@ -7814,9 +8934,14 @@ window.__ModuleLoader__.load({
|
|
|
7814
8934
|
h("button", {
|
|
7815
8935
|
key: s.key,
|
|
7816
8936
|
type: "button",
|
|
8937
|
+
// 引导请求在飞时四个视图标签一律禁用(评审 #4453639221):离开 memory
|
|
8938
|
+
// 会把整个 `.mneme-xtools` 连同里面的 BootstrapDialog 卸掉。onClick 仍
|
|
8939
|
+
// 走 leaveMemory 而不是 setView——disabled 只是第一道,第二道在函数里,
|
|
8940
|
+
// 免得将来有人给标签换一种可点的实现就绕过去了。
|
|
8941
|
+
disabled: bootstrapBusy,
|
|
7817
8942
|
className: view === s.key ? "mneme-vtab mneme-active" : "mneme-vtab",
|
|
7818
8943
|
"aria-pressed": String(view === s.key),
|
|
7819
|
-
onClick: () =>
|
|
8944
|
+
onClick: () => leaveMemory(s.key)
|
|
7820
8945
|
},
|
|
7821
8946
|
h(Icon, { name: s.key === "memory" ? "database" : s.key === "entity" ? "waypoints" : s.key === "status" ? "activity" : "settings", size: 14, className: "mneme-vtabico" }),
|
|
7822
8947
|
s.label
|
|
@@ -7843,39 +8968,66 @@ window.__ModuleLoader__.load({
|
|
|
7843
8968
|
"aria-label": t("memory.explorer.more"),
|
|
7844
8969
|
"aria-expanded": String(menuOpen),
|
|
7845
8970
|
title: t("memory.explorer.more"),
|
|
7846
|
-
|
|
8971
|
+
// 引导请求在飞时整个「⋯」都不给开:菜单与引导弹层共用同一个
|
|
8972
|
+
// 绝对定位锚点、同一层 z-index,弹层排在菜单之后渲染,会把
|
|
8973
|
+
// 菜单顶上的两项导出盖住——它们没有 disabled,却点不到
|
|
8974
|
+
// (评审 #4453225585)。不开菜单,就不存在「可点但被遮住」。
|
|
8975
|
+
// 这也顺带把 #4452245292 那条锁得更死:菜单开不了,就没有
|
|
8976
|
+
// 任何路径能在请求中途把引导弹层卸掉。
|
|
8977
|
+
disabled: bootstrapBusy,
|
|
8978
|
+
onClick: () => {
|
|
8979
|
+
setMenuOpen(!menuOpen);
|
|
8980
|
+
setImportOpen(false);
|
|
8981
|
+
// 引导弹层只在不忙的时候才关:它内部已经用 busy 禁用了取消与提交,
|
|
8982
|
+
// 这里若照旧卸载,等于给了一条绕过那两道锁的路(评审 #4452245292)。
|
|
8983
|
+
if (!bootstrapBusy) setBootstrapOpen(false);
|
|
8984
|
+
setCreateOpen(false);
|
|
8985
|
+
setShareOpen(false);
|
|
8986
|
+
setReceiveOpen(false);
|
|
8987
|
+
}
|
|
7847
8988
|
}, "⋯"),
|
|
7848
8989
|
menuOpen && h("div", { className: "mneme-menu", role: "menu" },
|
|
7849
|
-
|
|
8990
|
+
// 引导请求在飞时,会挂弹层的那几项一律 disabled:这些弹层共用同一个
|
|
8991
|
+
// 绝对定位锚点,后渲染的会盖住引导弹层,它那句「新建几条、并入几条」
|
|
8992
|
+
// 就没地方看了(评审 #4452539738)。导出两项不挂弹层,照旧可点。
|
|
8993
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setCreateOpen(true); setMenuOpen(false); } },
|
|
7850
8994
|
t("memory.explorer.newMemory")),
|
|
7851
8995
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("json"); } },
|
|
7852
8996
|
t("memory.explorer.exportJson")),
|
|
7853
8997
|
h("button", { type: "button", role: "menuitem", onClick: () => { setMenuOpen(false); exportData("markdown"); } },
|
|
7854
8998
|
t("memory.explorer.exportMarkdown")),
|
|
7855
|
-
h("button", { type: "button", role: "menuitem", onClick: () => { setImportOpen(true); setMenuOpen(false); } },
|
|
8999
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setImportOpen(true); setMenuOpen(false); } },
|
|
7856
9000
|
t("memory.explorer.importMd")),
|
|
7857
|
-
h("button", { type: "button", role: "menuitem", onClick: () => {
|
|
9001
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setBootstrapOpen(true); setMenuOpen(false); } },
|
|
9002
|
+
t("memory.explorer.bootstrap")),
|
|
9003
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setShareOpen(true); setMenuOpen(false); } },
|
|
7858
9004
|
t("memory.explorer.sharePack")),
|
|
7859
|
-
h("button", { type: "button", role: "menuitem", onClick: () => { setReceiveOpen(true); setMenuOpen(false); } },
|
|
9005
|
+
h("button", { type: "button", role: "menuitem", disabled: bootstrapBusy, onClick: () => { setReceiveOpen(true); setMenuOpen(false); } },
|
|
7860
9006
|
t("memory.explorer.receivePack")),
|
|
7861
9007
|
),
|
|
7862
9008
|
importOpen && h(ImportDialog, {
|
|
7863
9009
|
t,
|
|
7864
9010
|
onClose: () => setImportOpen(false),
|
|
7865
|
-
onImported: () => setReloadKey((k) => k + 1)
|
|
9011
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
9012
|
+
}),
|
|
9013
|
+
bootstrapOpen && h(BootstrapDialog, {
|
|
9014
|
+
t,
|
|
9015
|
+
onClose: () => setBootstrapOpen(false),
|
|
9016
|
+
onBusyChange: reportBootstrapBusy,
|
|
9017
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
7866
9018
|
}),
|
|
7867
9019
|
shareOpen && h(ShareDialog, { t, onClose: () => setShareOpen(false) }),
|
|
7868
9020
|
receiveOpen && h(ReceiveDialog, {
|
|
7869
9021
|
t,
|
|
7870
9022
|
onClose: () => setReceiveOpen(false),
|
|
7871
|
-
onImported: () => setReloadKey((k) => k + 1)
|
|
9023
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
7872
9024
|
}),
|
|
7873
9025
|
createOpen && h(CreateDialog, {
|
|
7874
9026
|
t,
|
|
7875
9027
|
onClose: () => setCreateOpen(false),
|
|
7876
9028
|
// 新建后既刷新列表也选中新行:并入既有条目时 action 是 merged,
|
|
7877
9029
|
// 返回的 id 就是被并入那一行——两种情况都该把它滚进视野。
|
|
7878
|
-
onCreated: (d) => { setReloadKey((k) => k + 1); if (d && d.id) setSelectedId(d.id); }
|
|
9030
|
+
onCreated: (d) => { setReloadKey((k) => k + 1); broadcastStoreChanged("create"); if (d && d.id) setSelectedId(d.id); }
|
|
7879
9031
|
})
|
|
7880
9032
|
)
|
|
7881
9033
|
),
|
|
@@ -8121,6 +9273,7 @@ window.__ModuleLoader__.load({
|
|
|
8121
9273
|
// (toast 形状照详情抽屉里 restored 那一条的先例,不带行号免得漂)。
|
|
8122
9274
|
setSelectedId(null);
|
|
8123
9275
|
setReloadKey((k) => k + 1);
|
|
9276
|
+
broadcastStoreChanged("pack-undo");
|
|
8124
9277
|
setToastMsg(t("memory.explorer.undoPackDone").replace("{n}", String(n)));
|
|
8125
9278
|
setTimeout(() => setToastMsg(""), 2500);
|
|
8126
9279
|
}
|
|
@@ -8141,8 +9294,10 @@ window.__ModuleLoader__.load({
|
|
|
8141
9294
|
// 入口形状跟着宿主 v0.2 的侧边栏走,分两态:
|
|
8142
9295
|
// · 展开态:宿主把「插件 / 自动化任务」这类全局面板渲染成 nav.panelList
|
|
8143
9296
|
// 里的 panelRow(36px 行高、透明底、图标 + 标题)。我们把按钮 portal 进
|
|
8144
|
-
// 同一组并原样套用宿主 panelRow
|
|
8145
|
-
// 而不是 v0.8.10
|
|
9297
|
+
// 同一组并原样套用宿主 panelRow 的实时类名,于是它是该组的一行——低调,
|
|
9298
|
+
// 而不是 v0.8.10 那样长得像第二个高亮「新会话」按钮。落点是「更多」行
|
|
9299
|
+
// 之前而不是组尾:宿主 0.2.2 起把「更多」追加进同一个 nav,组尾已不再是
|
|
9300
|
+
// 面板行,落在它下面会被展开的菜单盖住(#573)。
|
|
8146
9301
|
// · 收起态(Windows 标题栏):panelList / regionArea / footArea 被宿主
|
|
8147
9302
|
// display:none,只有开关与「新会话」被改成固定在标题栏上。我们把按钮搬回
|
|
8148
9303
|
// 标题栏、并排在「新会话」右侧(84px / 120px 的算法见上面 CSS 注释),
|
|
@@ -8205,9 +9360,13 @@ window.__ModuleLoader__.load({
|
|
|
8205
9360
|
const unsubscribe = conflictBadgeCount.subscribe(setPending);
|
|
8206
9361
|
const onChanged = () => { conflictBadgeCount.refresh(); };
|
|
8207
9362
|
window.addEventListener("mneme:conflicts-changed", onChanged);
|
|
9363
|
+
// 涉及冲突的记忆在别处被删除/恢复时 pending 集同样会变——裁决广播
|
|
9364
|
+
// 之外的第二条失效路径(此前只能等 60s 兜底轮询)。
|
|
9365
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
8208
9366
|
return () => {
|
|
8209
9367
|
unsubscribe();
|
|
8210
9368
|
window.removeEventListener("mneme:conflicts-changed", onChanged);
|
|
9369
|
+
window.removeEventListener("mneme:store-changed", onChanged);
|
|
8211
9370
|
};
|
|
8212
9371
|
}, []);
|
|
8213
9372
|
return pending;
|
|
@@ -8217,6 +9376,33 @@ window.__ModuleLoader__.load({
|
|
|
8217
9376
|
return h("span", { className: "mneme-entrybadge", "aria-hidden": "true" },
|
|
8218
9377
|
pending > 99 ? "99+" : String(pending));
|
|
8219
9378
|
}
|
|
9379
|
+
// --- 宿主内整页面板(新版宿主):侧栏图标 + 中列整页 ---
|
|
9380
|
+
// 首选承载方式:记忆库直接长在宿主中列里,与会话平级,不再叠一层背板
|
|
9381
|
+
// 浮层。老宿主没声明 sidebar.panellist 时退回 footer.action + 浮层(见 apply)。
|
|
9382
|
+
function SidebarPanelGlyph({ size }) {
|
|
9383
|
+
// 红点复用模块级单例:图标与回落按钮共享同一个计数、同一条 60s 兜底轮询。
|
|
9384
|
+
const pending = useConflictBadgeCount();
|
|
9385
|
+
// 宿主 PanelRow 已经自带 button / aria-label / Tooltip / active 高亮,
|
|
9386
|
+
// 这里只交一颗图形——再造一层按钮会套出两颗可聚焦元素,Tab 要停两次,
|
|
9387
|
+
// 而且 host 的 selectPanel 也轮不到我们调。
|
|
9388
|
+
return h("span", { className: "mneme-paneliglyph" },
|
|
9389
|
+
renderMnemeMark({ size: size || 18 }),
|
|
9390
|
+
h(ConflictBadge, { pending })
|
|
9391
|
+
);
|
|
9392
|
+
}
|
|
9393
|
+
|
|
9394
|
+
function MnemeMainPanel({ t }) {
|
|
9395
|
+
// 进面板 = 用户准备处理冻结冲突:重取一次红点计数。理由与浮层时代
|
|
9396
|
+
// openLibrary 里那次重取相同——裁决走别的路径或后台刚写过库时,计数
|
|
9397
|
+
// 不该等 60s 兜底轮询才对齐。
|
|
9398
|
+
useEffect(() => { conflictBadgeCount.refresh(); }, []);
|
|
9399
|
+
// 不自带「回到会话」按钮:宿主的 openSession / 新建会话都会
|
|
9400
|
+
// selectPanel(null),用户点侧栏任何会话行就回得去(baleen 同样不做)。
|
|
9401
|
+
return h("div", { className: "mneme-panel" },
|
|
9402
|
+
h(MemoryExplorer, { t })
|
|
9403
|
+
);
|
|
9404
|
+
}
|
|
9405
|
+
|
|
8220
9406
|
function SidebarFallbackTrigger({ wide, t }) {
|
|
8221
9407
|
const pending = useConflictBadgeCount();
|
|
8222
9408
|
// #295 评审:.mneme-trigger 自身 overflow:hidden 且无定位上下文,badge
|
|
@@ -8260,6 +9446,12 @@ window.__ModuleLoader__.load({
|
|
|
8260
9446
|
const list = findPanelList(region);
|
|
8261
9447
|
return list ? list.querySelector('[class*="panelRow"]:not(.mneme-topentry-native)') : null;
|
|
8262
9448
|
};
|
|
9449
|
+
// 面板列表里的「更多」行(宿主 PanelMoreMenu 的锚点)。宿主 0.2.2 起先渲染
|
|
9450
|
+
// 面板行、再把「更多」追加进同一个 nav,于是列表最后一项不再是面板行——
|
|
9451
|
+
// 我们的入口若仍 appendChild 到末尾就会落在它下面,菜单一展开就盖住入口
|
|
9452
|
+
// (#573)。按 [class*=] 前缀找,和其余几处一样不写死哈希类名;老宿主
|
|
9453
|
+
// 没有这行时返回 null,退回追加到末尾的老行为。
|
|
9454
|
+
const findMoreAnchor = (list) => list.querySelector('[class*="moreAnchor"]');
|
|
8263
9455
|
// 占位锚点:新会话行(现役外壳里 newSession 按钮嵌在 logoRow 内,
|
|
8264
9456
|
// 旧外壳是根的直接子按钮)。判定与生态插件的 sidebar-entry-core
|
|
8265
9457
|
// 一致;锚点不可靠时回退到 regionArea 之前的旧位置。
|
|
@@ -8285,6 +9477,14 @@ window.__ModuleLoader__.load({
|
|
|
8285
9477
|
// 列表时退回「新会话」行之后,由 .mneme-topentry-native 兜住低调行样式。
|
|
8286
9478
|
const list = wide ? findPanelList(region) : null;
|
|
8287
9479
|
if (list) {
|
|
9480
|
+
// 落在面板组末尾、但在「更多」行之前(#573);没有「更多」行时组尾
|
|
9481
|
+
// 就是终点,退回老行为。
|
|
9482
|
+
const more = findMoreAnchor(list);
|
|
9483
|
+
if (more) {
|
|
9484
|
+
if (created.parentElement === list && created.nextElementSibling === more) return;
|
|
9485
|
+
list.insertBefore(created, more);
|
|
9486
|
+
return;
|
|
9487
|
+
}
|
|
8288
9488
|
if (created.parentElement === list && created === list.lastElementChild) return;
|
|
8289
9489
|
list.appendChild(created);
|
|
8290
9490
|
return;
|
|
@@ -9477,13 +10677,290 @@ window.__ModuleLoader__.load({
|
|
|
9477
10677
|
const t = ctx.locale.bind(NS);
|
|
9478
10678
|
ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-mneme: dictionaries");
|
|
9479
10679
|
|
|
9480
|
-
//
|
|
9481
|
-
//
|
|
9482
|
-
//
|
|
9483
|
-
//
|
|
9484
|
-
//
|
|
9485
|
-
|
|
9486
|
-
|
|
10680
|
+
// --- 入口座位 ---
|
|
10681
|
+
// --- chorography(仓库地志)面板(M1,spec §7)---------------------
|
|
10682
|
+
// 浏览页:左模块树 / 右正文 / 顶部检索。数据从 /choro/* 四条读路由来,
|
|
10683
|
+
// 真源是仓库磁盘的 MD。座位无条件注册(P11):inject 同名二次注册的
|
|
10684
|
+
// cordis 语义没验证过,不赌;chorographyEnabled 关着时页面渲染「未启用」
|
|
10685
|
+
// 空态(路由 409 disabled),开关生效走既有「重启生效」纪律——座位常挂
|
|
10686
|
+
// 的收敛形态就是 D2 拆双 Cordis 条目时的样子。
|
|
10687
|
+
function WikiGlyph({ size }) {
|
|
10688
|
+
// 图形走 currentColor,粗细对比交给宿主的 active 态;不引入新色值。
|
|
10689
|
+
return h("span", { className: "mneme-paneliglyph", "aria-hidden": "true" },
|
|
10690
|
+
h("svg", {
|
|
10691
|
+
width: size || 18, height: size || 18, viewBox: "0 0 24 24",
|
|
10692
|
+
fill: "none", stroke: "currentColor", strokeWidth: 1.8,
|
|
10693
|
+
strokeLinecap: "round", strokeLinejoin: "round"
|
|
10694
|
+
},
|
|
10695
|
+
h("path", { d: "M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2z" }),
|
|
10696
|
+
h("path", { d: "M9 4v14" }),
|
|
10697
|
+
h("path", { d: "M15 6v14" })
|
|
10698
|
+
)
|
|
10699
|
+
);
|
|
10700
|
+
}
|
|
10701
|
+
|
|
10702
|
+
// 只读 MD 渲染(P13):React 文本节点天然转义,不经过 innerHTML,无原始
|
|
10703
|
+
// HTML 通道。frontmatter 折叠不显示;表格按等宽行渲染(知识库页的表都是
|
|
10704
|
+
// 两三列的清单,保真优先于交互);`code` 拆成等宽 span。
|
|
10705
|
+
function renderWikiMarkdown(content) {
|
|
10706
|
+
const lines = String(content ?? "").split("\n");
|
|
10707
|
+
let i = 0;
|
|
10708
|
+
// frontmatter:首行是 --- 时吃到下一行 ---
|
|
10709
|
+
if (lines[0] && lines[0].trim() === "---") {
|
|
10710
|
+
i = 1;
|
|
10711
|
+
while (i < lines.length && lines[i].trim() !== "---") i++;
|
|
10712
|
+
i++;
|
|
10713
|
+
}
|
|
10714
|
+
const out = [];
|
|
10715
|
+
const inline = (text) => String(text).split(/`([^`]+)`/).map((part, idx) =>
|
|
10716
|
+
idx % 2 === 1 ? h("code", { key: idx, className: "mneme-wikicode" }, part) : part
|
|
10717
|
+
);
|
|
10718
|
+
let list = null; // "ul" | "ol" | null
|
|
10719
|
+
const flushList = () => {
|
|
10720
|
+
if (list) { out.push(h(list, { key: out.length }, listItems)); listItems = []; list = null; }
|
|
10721
|
+
};
|
|
10722
|
+
let listItems = [];
|
|
10723
|
+
let inCode = false;
|
|
10724
|
+
let codeLines = [];
|
|
10725
|
+
// --- 表格(v0.11.1 美化):连续 `|` 行解析成真 table,不再按等宽行画竖线 ---
|
|
10726
|
+
const splitRow = (line) => String(line).trim().replace(/^\|/, "").replace(/\|$/, "")
|
|
10727
|
+
.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
|
|
10728
|
+
const isSepRow = (cells) => cells.length > 0 && cells.every((c) => /^:?-{3,}:?$/.test(c));
|
|
10729
|
+
// 路径形单元格(含 / 且以扩展名结尾)渲染成等宽 code——文件清单的第一列
|
|
10730
|
+
// 是相对路径,等宽 + 深一点的底色让路径在表里一眼可辨。
|
|
10731
|
+
const pathLike = (text) => /^(?!https?:)(?:[\w.-]+\/)*[\w.-]+\.[A-Za-z]{1,5}$/.test(String(text).trim());
|
|
10732
|
+
const cellNodes = (text, key) => {
|
|
10733
|
+
const trimmed = String(text).trim();
|
|
10734
|
+
if (pathLike(trimmed)) return h("code", { key, className: "mneme-wikicode" }, trimmed);
|
|
10735
|
+
return inline(trimmed);
|
|
10736
|
+
};
|
|
10737
|
+
let tableRows = null;
|
|
10738
|
+
const flushTable = () => {
|
|
10739
|
+
if (!tableRows) return;
|
|
10740
|
+
const rows = tableRows;
|
|
10741
|
+
tableRows = null;
|
|
10742
|
+
if (rows.length === 0) return;
|
|
10743
|
+
const headerCells = splitRow(rows[0]);
|
|
10744
|
+
const sepIndex = rows.length > 1 && isSepRow(splitRow(rows[1])) ? 1 : -1;
|
|
10745
|
+
const aligns = sepIndex >= 0
|
|
10746
|
+
? splitRow(rows[sepIndex]).map((c) => (c.endsWith(":") ? "right" : c.startsWith(":") ? "center" : null))
|
|
10747
|
+
: headerCells.map(() => null);
|
|
10748
|
+
const bodyStart = sepIndex >= 0 ? sepIndex + 1 : 0;
|
|
10749
|
+
const head = h("thead", { key: "h" }, h("tr", null,
|
|
10750
|
+
headerCells.map((c, idx) => h("th", {
|
|
10751
|
+
key: idx, style: aligns[idx] ? { textAlign: aligns[idx] } : undefined,
|
|
10752
|
+
}, cellNodes(c, idx)))));
|
|
10753
|
+
const body = h("tbody", { key: "b" }, rows.slice(bodyStart).map((row, rIdx) => {
|
|
10754
|
+
const cells = splitRow(row);
|
|
10755
|
+
return h("tr", { key: rIdx }, headerCells.map((_, idx) => h("td", {
|
|
10756
|
+
key: idx,
|
|
10757
|
+
className: aligns[idx] === "right" ? "mneme-wikinum" : undefined,
|
|
10758
|
+
style: aligns[idx] && aligns[idx] !== "right" ? { textAlign: aligns[idx] } : undefined,
|
|
10759
|
+
}, cellNodes(cells[idx] ?? "", idx))));
|
|
10760
|
+
}));
|
|
10761
|
+
out.push(h("table", { key: out.length, className: "mneme-wikitable" }, head, body));
|
|
10762
|
+
};
|
|
10763
|
+
for (; i < lines.length; i++) {
|
|
10764
|
+
const line = lines[i];
|
|
10765
|
+
if (line.trim().startsWith("```")) {
|
|
10766
|
+
if (inCode) {
|
|
10767
|
+
out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10768
|
+
codeLines = []; inCode = false;
|
|
10769
|
+
} else { flushList(); flushTable(); inCode = true; }
|
|
10770
|
+
continue;
|
|
10771
|
+
}
|
|
10772
|
+
if (inCode) { codeLines.push(line); continue; }
|
|
10773
|
+
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
10774
|
+
if (heading) {
|
|
10775
|
+
flushList(); flushTable();
|
|
10776
|
+
const level = Math.min(heading[1].length + 1, 5);
|
|
10777
|
+
out.push(h(`h${level}`, { key: out.length }, inline(heading[2])));
|
|
10778
|
+
continue;
|
|
10779
|
+
}
|
|
10780
|
+
const ul = line.match(/^\s*[-*]\s+(.*)$/);
|
|
10781
|
+
const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
10782
|
+
if (ul || ol) {
|
|
10783
|
+
flushTable();
|
|
10784
|
+
const want = ul ? "ul" : "ol";
|
|
10785
|
+
if (list !== want) flushList();
|
|
10786
|
+
list = want;
|
|
10787
|
+
listItems.push(h("li", { key: listItems.length }, inline(ul ? ul[1] : ol[1])));
|
|
10788
|
+
continue;
|
|
10789
|
+
}
|
|
10790
|
+
if (/^\s*\|/.test(line)) { flushList(); tableRows = tableRows ?? []; tableRows.push(line); continue; }
|
|
10791
|
+
flushList(); flushTable();
|
|
10792
|
+
if (/^>\s?/.test(line)) { out.push(h("blockquote", { key: out.length, className: "mneme-wikiquote" }, inline(line.replace(/^>\s?/, "")))); continue; }
|
|
10793
|
+
if (line.trim() === "") continue;
|
|
10794
|
+
out.push(h("p", { key: out.length }, inline(line)));
|
|
10795
|
+
}
|
|
10796
|
+
flushList(); flushTable();
|
|
10797
|
+
if (inCode && codeLines.length > 0) out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10798
|
+
return out;
|
|
10799
|
+
}
|
|
10800
|
+
|
|
10801
|
+
function WikiMainPanel({ t }) {
|
|
10802
|
+
const [state, setState] = useState({ phase: "loading", repos: [], repo: null, modules: [], edges: [], page: null, hits: [], q: "", selected: null, disabled: false, error: null, rebuilding: false, rebuildNote: null });
|
|
10803
|
+
// apiFetch 是裸 fetch:非 2xx 不抛错,返回 Response。这里统一解 JSON 并把
|
|
10804
|
+
// 状态码带回来——409 disabled(用户没开)/ 503 unavailable(装配缺件)
|
|
10805
|
+
// 各有各的空态文案(#497 的语义分叉在这里接住)。
|
|
10806
|
+
const readJson = async (res) => {
|
|
10807
|
+
let data = {};
|
|
10808
|
+
try { data = await res.json(); } catch { /* 空响应体 */ }
|
|
10809
|
+
return { status: res.status, data };
|
|
10810
|
+
};
|
|
10811
|
+
const load = useCallback(async (repoRef) => {
|
|
10812
|
+
try {
|
|
10813
|
+
const { status, data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/repos`));
|
|
10814
|
+
if (status === 409) { setState((s) => ({ ...s, phase: "disabled" })); return; }
|
|
10815
|
+
if (status === 503) { setState((s) => ({ ...s, phase: "unavailable" })); return; }
|
|
10816
|
+
const repos = data.repos ?? [];
|
|
10817
|
+
if (repos.length === 0) { setState((s) => ({ ...s, phase: "no-repo", repos: [] })); return; }
|
|
10818
|
+
const ref = repoRef ?? (repos.length === 1 ? repos[0].id : null);
|
|
10819
|
+
if (ref === null) { setState((s) => ({ ...s, phase: "pick-repo", repos })); return; }
|
|
10820
|
+
const mod = await readJson(await apiFetch(`/api/dsh-mneme/choro/modules?repo=${encodeURIComponent(ref)}`));
|
|
10821
|
+
if (mod.status !== 200) { setState((s) => ({ ...s, phase: "error", error: `modules ${mod.status}` })); return; }
|
|
10822
|
+
setState((s) => ({ ...s, phase: "ready", repos, repo: mod.data.repo, modules: mod.data.modules ?? [], edges: mod.data.edges ?? [], page: null, selected: null }));
|
|
10823
|
+
} catch (err) {
|
|
10824
|
+
setState((s) => ({ ...s, phase: "error", error: String(err?.message ?? err) }));
|
|
10825
|
+
}
|
|
10826
|
+
}, []);
|
|
10827
|
+
useEffect(() => { load(); }, [load]);
|
|
10828
|
+
const openPage = useCallback(async (slug) => {
|
|
10829
|
+
const repo = state.repo;
|
|
10830
|
+
if (!repo) return;
|
|
10831
|
+
setState((s) => ({ ...s, selected: slug, page: null }));
|
|
10832
|
+
try {
|
|
10833
|
+
const { status, data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/page?repo=${encodeURIComponent(repo.id)}&slug=${encodeURIComponent(slug)}`));
|
|
10834
|
+
if (status !== 200 || data.error) { setState((s) => ({ ...s, page: { error: data.error ?? `page ${status}` } })); return; }
|
|
10835
|
+
setState((s) => ({ ...s, page: data.page }));
|
|
10836
|
+
} catch (err) {
|
|
10837
|
+
setState((s) => ({ ...s, page: { error: String(err?.message ?? err) } }));
|
|
10838
|
+
}
|
|
10839
|
+
}, [state.repo]);
|
|
10840
|
+
const runSearch = useCallback(async (q) => {
|
|
10841
|
+
setState((s) => ({ ...s, q }));
|
|
10842
|
+
const repo = state.repo;
|
|
10843
|
+
if (!repo || !q.trim()) { setState((s) => ({ ...s, hits: [] })); return; }
|
|
10844
|
+
try {
|
|
10845
|
+
const { data } = await readJson(await apiFetch(`/api/dsh-mneme/choro/search?repo=${encodeURIComponent(repo.id)}&q=${encodeURIComponent(q)}`));
|
|
10846
|
+
setState((s) => ({ ...s, hits: data.hits ?? [] }));
|
|
10847
|
+
} catch { setState((s) => ({ ...s, hits: [] })); }
|
|
10848
|
+
}, [state.repo]);
|
|
10849
|
+
// 重建骨架(收尾档 P34):POST /choro/run(token-write),T0 apply 秒级
|
|
10850
|
+
// 同步回执。T1 语义富化是长任务,不上 HTTP——按钮旁的空态/提示会引导去
|
|
10851
|
+
// CLI(t1Loss 警告也从服务端原样透出)。
|
|
10852
|
+
const rebuild = useCallback(async () => {
|
|
10853
|
+
const repo = state.repo;
|
|
10854
|
+
if (!repo || state.rebuilding) return;
|
|
10855
|
+
setState((s) => ({ ...s, rebuilding: true, rebuildNote: null }));
|
|
10856
|
+
try {
|
|
10857
|
+
const { status, data } = await readJson(await apiFetch("/api/dsh-mneme/choro/run", {
|
|
10858
|
+
method: "POST",
|
|
10859
|
+
headers: { "Content-Type": "application/json" },
|
|
10860
|
+
body: JSON.stringify({ repo: repo.id })
|
|
10861
|
+
}));
|
|
10862
|
+
if (status !== 200 || !data.ok) {
|
|
10863
|
+
setState((s) => ({ ...s, rebuilding: false, rebuildNote: `${t("wiki.rebuild.failed")}: ${data.detail ?? data.error ?? status}` }));
|
|
10864
|
+
return;
|
|
10865
|
+
}
|
|
10866
|
+
const r = data.receipt ?? {};
|
|
10867
|
+
const loss = (r.warnings ?? []).find((w) => /T1|语义/.test(w));
|
|
10868
|
+
setState((s) => ({
|
|
10869
|
+
...s,
|
|
10870
|
+
rebuilding: false,
|
|
10871
|
+
rebuildNote: `${t("wiki.rebuild.done")}: ${r.moduleCount ?? "?"} ${t("wiki.rebuild.modules")} / ${r.pageCount ?? "?"} ${t("wiki.rebuild.pages")}${loss ? ` — ${loss}` : ""}`
|
|
10872
|
+
}));
|
|
10873
|
+
await load(repo.id);
|
|
10874
|
+
} catch (err) {
|
|
10875
|
+
setState((s) => ({ ...s, rebuilding: false, rebuildNote: `${t("wiki.rebuild.failed")}: ${String(err?.message ?? err)}` }));
|
|
10876
|
+
}
|
|
10877
|
+
}, [state.repo, state.rebuilding, load, t]);
|
|
10878
|
+
|
|
10879
|
+
if (state.phase === "loading") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.state.loading"));
|
|
10880
|
+
if (state.phase === "disabled") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.empty.disabled"));
|
|
10881
|
+
if (state.phase === "unavailable") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.state.unavailable"));
|
|
10882
|
+
if (state.phase === "error") return h("div", { className: "mneme-panel mneme-wikiempty" }, `${t("wiki.state.error")}: ${state.error}`);
|
|
10883
|
+
if (state.phase === "no-repo") {
|
|
10884
|
+
return h("div", { className: "mneme-panel mneme-wikiempty" },
|
|
10885
|
+
h("div", { className: "mneme-wikiemptytitle" }, t("wiki.panel.open")),
|
|
10886
|
+
h("p", null, t("wiki.empty.noRepo")));
|
|
10887
|
+
}
|
|
10888
|
+
if (state.phase === "pick-repo") {
|
|
10889
|
+
return h("div", { className: "mneme-panel mneme-wikiempty" },
|
|
10890
|
+
h("div", { className: "mneme-wikiemptytitle" }, t("wiki.empty.pickRepo")),
|
|
10891
|
+
state.repos.map((r) => h("button", {
|
|
10892
|
+
key: r.id, type: "button", className: "mneme-wikitreeitem",
|
|
10893
|
+
onClick: () => load(r.id)
|
|
10894
|
+
}, `${r.name}`)));
|
|
10895
|
+
}
|
|
10896
|
+
const tree = [];
|
|
10897
|
+
for (const mod of state.modules) {
|
|
10898
|
+
const depth = (mod.slug.match(/-/g) || []).length;
|
|
10899
|
+
tree.push(h("button", {
|
|
10900
|
+
key: mod.slug,
|
|
10901
|
+
type: "button",
|
|
10902
|
+
className: `mneme-wikitreeitem${state.selected === mod.slug ? " mneme-active" : ""}`,
|
|
10903
|
+
style: { paddingLeft: `${8 + Math.min(depth, 4) * 14}px` },
|
|
10904
|
+
title: mod.title,
|
|
10905
|
+
onClick: () => openPage(mod.slug)
|
|
10906
|
+
}, mod.slug));
|
|
10907
|
+
}
|
|
10908
|
+
const pageBody = state.page
|
|
10909
|
+
? (state.page.error
|
|
10910
|
+
? h("div", { className: "mneme-wikiempty" }, state.page.error === "page-missing" ? t("wiki.state.pageMissing") : state.page.error)
|
|
10911
|
+
: renderWikiMarkdown(state.page.content))
|
|
10912
|
+
: h("div", { className: "mneme-wikiempty" }, t("wiki.state.pickPage"));
|
|
10913
|
+
return h("div", { className: "mneme-panel" },
|
|
10914
|
+
h("div", { className: "mneme-wikibar" },
|
|
10915
|
+
h("span", { className: "mneme-wikirepo" }, state.repo ? `${state.repo.name}` : ""),
|
|
10916
|
+
h("input", {
|
|
10917
|
+
className: "mneme-wikisearch",
|
|
10918
|
+
placeholder: t("wiki.search.placeholder"),
|
|
10919
|
+
value: state.q,
|
|
10920
|
+
onChange: (e) => runSearch(e.target.value),
|
|
10921
|
+
"aria-label": t("wiki.search.placeholder")
|
|
10922
|
+
}),
|
|
10923
|
+
h("button", {
|
|
10924
|
+
type: "button",
|
|
10925
|
+
className: "mneme-wikibtn",
|
|
10926
|
+
disabled: state.rebuilding,
|
|
10927
|
+
title: t("wiki.rebuild.hint"),
|
|
10928
|
+
onClick: rebuild
|
|
10929
|
+
}, state.rebuilding ? t("wiki.rebuilding") : t("wiki.rebuild")),
|
|
10930
|
+
state.rebuildNote && h("span", { className: "mneme-wikirebuildnote" }, state.rebuildNote)),
|
|
10931
|
+
h("div", { className: "mneme-wikibody" },
|
|
10932
|
+
h("nav", { className: "mneme-wikitree", "aria-label": t("wiki.tree.title") }, tree),
|
|
10933
|
+
h("div", { className: "mneme-wikipage" },
|
|
10934
|
+
state.hits.length > 0 && h("div", { className: "mneme-wikihits" },
|
|
10935
|
+
state.hits.map((hit, idx) => h("button", {
|
|
10936
|
+
key: `${hit.slug}-${hit.line}-${idx}`,
|
|
10937
|
+
type: "button",
|
|
10938
|
+
className: "mneme-wikihit",
|
|
10939
|
+
onClick: () => { setState((s) => ({ ...s, hits: [] })); openPage(hit.slug); }
|
|
10940
|
+
}, `${hit.slug} · ${hit.section} · L${hit.line}: ${hit.excerpt}`)),
|
|
10941
|
+
h("button", { type: "button", className: "mneme-wikihitsclose", onClick: () => setState((s) => ({ ...s, hits: [] })) }, "×")),
|
|
10942
|
+
pageBody)));
|
|
10943
|
+
}
|
|
10944
|
+
|
|
10945
|
+
// 两套宿主形态,二选一,靠宿主是否声明了 sidebar.panellist 判定:
|
|
10946
|
+
// · 宿主内整页面板(首选):sidebar.panellist 出图标、main 出中列整页。
|
|
10947
|
+
// 图标长在宿主自己的面板列表里(PanelRow 自己处理点击与 aria-current),
|
|
10948
|
+
// 插件只管渲染一颗 glyph,不自己造按钮、不自己调 selectPanel。
|
|
10949
|
+
// · 老宿主回落:没有这两个座位时,footer.action 出按钮 + 居中 sheet,
|
|
10950
|
+
// 并保留 SidebarTopEntry 的 DOM portal(尽量把入口放到面板列表位置)。
|
|
10951
|
+
// 两套都挂同一个模块级 overlay 状态,但不会同时装:panellist 一旦出现就
|
|
10952
|
+
// 先把回落拆掉,否则侧栏会冒出两个记忆入口。
|
|
10953
|
+
// 判定 + 装载都收在两个闭包里:谁都可以问「宿主有没有这套座位」,
|
|
10954
|
+
// 也都可以在条件变化后重装回落——但同一时刻至多一份注册在册。
|
|
10955
|
+
const hostHasPanelSeat = () => typeof ctx.slots.spec === "function"
|
|
10956
|
+
&& ctx.slots.spec("sidebar.panellist") !== undefined;
|
|
10957
|
+
let footerSeat = null;
|
|
10958
|
+
const dropFooterSeat = () => {
|
|
10959
|
+
if (footerSeat) { footerSeat(); footerSeat = null; }
|
|
10960
|
+
};
|
|
10961
|
+
const mountFooterSeat = () => {
|
|
10962
|
+
if (footerSeat || hostHasPanelSeat()) return;
|
|
10963
|
+
footerSeat = ctx.slots.register({
|
|
9487
10964
|
name: "sidebar.footer.action",
|
|
9488
10965
|
id: "dsh-mneme",
|
|
9489
10966
|
order: 0,
|
|
@@ -9496,6 +10973,56 @@ window.__ModuleLoader__.load({
|
|
|
9496
10973
|
}),
|
|
9497
10974
|
h(MemoryOverlay, { t })
|
|
9498
10975
|
));
|
|
10976
|
+
};
|
|
10977
|
+
|
|
10978
|
+
ctx.slots.inject("sidebar.panellist", () => {
|
|
10979
|
+
dropFooterSeat();
|
|
10980
|
+
const disposeGlyph = ctx.slots.register({
|
|
10981
|
+
name: "sidebar.panellist",
|
|
10982
|
+
id: "dsh-mneme",
|
|
10983
|
+
// 排在宿主自己的面板之后(baleen 占 45):记忆是高频入口,但排在
|
|
10984
|
+
// 已有面板之后不抢位。order 只影响列表排序,不影响可发现性。
|
|
10985
|
+
order: 60,
|
|
10986
|
+
label: () => t("memory.panel.open")
|
|
10987
|
+
}, (props) => h(SidebarPanelGlyph, { size: props && props.size }));
|
|
10988
|
+
// main 的 key 必须与上面侧栏的 id 逐字相同:宿主点面板行时调
|
|
10989
|
+
// layout.selectPanel(id),它按 options.key 在 main 槽里找 entry,
|
|
10990
|
+
// 对不上直接抛 "main panel ... is not registered"。
|
|
10991
|
+
const disposeMain = ctx.slots.register({
|
|
10992
|
+
name: "main",
|
|
10993
|
+
key: "dsh-mneme"
|
|
10994
|
+
}, () => h(MnemeMainPanel, { t }));
|
|
10995
|
+
// chorography 第二个座位(M1):成对规则同上——id 与 key 逐字同为
|
|
10996
|
+
// "dsh-mneme-wiki"。座位无条件注册(P11),内容层自己渲染空态;
|
|
10997
|
+
// D2 拆双 Cordis 条目后这里自然变成可独立禁用的形态。
|
|
10998
|
+
const disposeWikiGlyph = ctx.slots.register({
|
|
10999
|
+
name: "sidebar.panellist",
|
|
11000
|
+
id: "dsh-mneme-wiki",
|
|
11001
|
+
order: 61,
|
|
11002
|
+
label: () => t("wiki.panel.open")
|
|
11003
|
+
}, (props) => h(WikiGlyph, { size: props && props.size }));
|
|
11004
|
+
const disposeWikiMain = ctx.slots.register({
|
|
11005
|
+
name: "main",
|
|
11006
|
+
key: "dsh-mneme-wiki"
|
|
11007
|
+
}, () => h(WikiMainPanel, { t }));
|
|
11008
|
+
return () => {
|
|
11009
|
+
disposeGlyph();
|
|
11010
|
+
disposeMain();
|
|
11011
|
+
disposeWikiGlyph();
|
|
11012
|
+
disposeWikiMain();
|
|
11013
|
+
// 座位被撤(宿主折叠该槽 / 换宿主形态)时把回落装回去:两条路径都
|
|
11014
|
+
// 只靠各自的 inject 触发,谁不动谁就永远停在「没有入口」的状态。
|
|
11015
|
+
mountFooterSeat();
|
|
11016
|
+
};
|
|
11017
|
+
});
|
|
11018
|
+
|
|
11019
|
+
// 老宿主回落:footer.action 这一格既能当锚点(SidebarTopEntry 靠它挂
|
|
11020
|
+
// portal)也能当兜底按钮,新宿主上则整格不注册——否则侧栏两颗记忆入口。
|
|
11021
|
+
ctx.slots.inject("sidebar.footer.action", () => {
|
|
11022
|
+
mountFooterSeat();
|
|
11023
|
+
return () => {
|
|
11024
|
+
dropFooterSeat();
|
|
11025
|
+
};
|
|
9499
11026
|
});
|
|
9500
11027
|
|
|
9501
11028
|
// --- better-sidebar 生态 tab(可选软依赖)---
|