@modusensus/dsh-mneme 0.11.0 → 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 +10 -6
- package/docs/CONFIGURATION.md +2 -0
- package/lib/api.js +172 -6
- package/lib/client.js +619 -18
- package/lib/config.js +32 -1
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/index.js +40 -4
- package/lib/inject.js +4 -2
- package/lib/lang.js +28 -0
- package/lib/profile-facts.js +356 -0
- package/lib/service.js +109 -4
- package/lib/settings.js +5 -0
- package/lib/store.js +483 -34
- package/lib/tools.js +140 -23
- package/lib/user-settings.js +63 -7
- package/lib/wiki/queries.js +149 -9
- package/package.json +1 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api.js +172 -6
- package/src/config.js +32 -1
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/index.js +40 -4
- package/src/inject.js +4 -2
- package/src/lang.js +28 -0
- package/src/profile-facts.js +356 -0
- package/src/service.js +109 -4
- package/src/settings.js +5 -0
- package/src/store.js +483 -34
- package/src/tools.js +140 -23
- package/src/user-settings.js +63 -7
- package/src/wiki/queries.js +149 -9
- package/test/api.test.js +3 -1
- package/test/client.test.js +147 -2
- package/test/config.test.js +79 -0
- 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/persona-settings.test.js +31 -0
- package/test/profile-facts.test.js +492 -0
- 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 +121 -1
- package/test/wiki-queries.test.js +84 -1
- package/test/wiki-tools.test.js +131 -76
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)。
|
|
@@ -302,6 +315,13 @@ window.__ModuleLoader__.load({
|
|
|
302
315
|
"memory.panel.empty": "暂无记忆条目",
|
|
303
316
|
"memory.panel.open": "记忆",
|
|
304
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": "页",
|
|
305
325
|
"wiki.empty.noRepo": "还没有登记的仓库。先在仓库目录里跑 `dsh-mneme-choro <仓库路径> --apply` 生成骨架页并登记,然后回到这里。",
|
|
306
326
|
"wiki.empty.disabled": "chorography(仓库地志)未启用:在「设置」里打开「仓库地志」后重启 DSH 生效。",
|
|
307
327
|
"wiki.empty.pickRepo": "已登记多个仓库,选一个:",
|
|
@@ -379,12 +399,48 @@ window.__ModuleLoader__.load({
|
|
|
379
399
|
"memory.settings.personaViewSourceNone": "按当前寻址口径,这个身份不会注入任何人格——默认卡只兜「解析不到会话身份」的会话。",
|
|
380
400
|
"memory.settings.personaViewGateOff": "总闸「人格定义(beta)」现在是关的:下面这段里不会出现人格,只有画像与规则。",
|
|
381
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": "失败",
|
|
382
436
|
"memory.features.personaEnabled": "人格定义(beta)",
|
|
383
437
|
"memory.features.personaEnabled.hint": "把「设置 → 个性化」里的人格卡注入到每轮对话。默认关(beta);总闸与人格卡内的开关同时打开才生效。",
|
|
384
438
|
"memory.features.personaStrictAddressing": "严格人格寻址",
|
|
385
439
|
"memory.features.personaStrictAddressing.hint": "开:会话身份没有对应人格卡、或那张卡被关掉时,就不注入人格——默认卡只兜「解析不到会话身份」的会话。宿主里跑着自带身份的 bot(DS Bot 那类)时关掉这种兜底,免得两句「你是谁」撞在同一轮。默认关 = 未命中就落默认卡(历史行为)。",
|
|
386
440
|
"memory.features.profileExtractEnabled": "画像提取草稿",
|
|
387
441
|
"memory.features.profileExtractEnabled.hint": "打开「设置 → 个性化 → Agent 眼中的你」卡上的「从记忆提取草稿」按钮:读一批活跃记忆、花一次模型调用,产一段「助手眼中的用户」草稿。草稿只在那张卡上展示副本,可一键复制,不会写进记忆库、也不会覆盖你自己填的「用户画像」。轻量档预设关(用户显式勾选仍然赢)。",
|
|
442
|
+
"memory.features.profileFactsEnabled": "自动事实",
|
|
443
|
+
"memory.features.profileFactsEnabled.hint": "每次 dream(巩固)轮后,把这一窗口里沉淀的新记忆再读一遍,按会话身份卡维护一份「Agent 眼中的你」事实清单(设置 → 个性化 → 自动事实),注入在手填画像之后。事实住独立的表,不会覆盖你自己填的「用户画像」;面板上可逐条修正或删除(删除=归档,可恢复)。轻量档预设关(用户显式勾选仍然赢)。",
|
|
388
444
|
"memory.features.chorographyEnabled": "仓库地志(chorography)",
|
|
389
445
|
"memory.features.chorographyEnabled.hint": "读一遍仓库写成一份带页码的志书:确定性骨架零模型、可以随时重建;语义段由 CLI 的 headless 会话逐模块补。打开后有四个入口——模型侧三个只读工具、面板「仓库地志」浏览页、每轮的开局提纲注入、/choro 命令。关着时它们全部不存在(模型工具列表里根本没有,注入块不渲染,/choro 不注册)。默认关。",
|
|
390
446
|
"memory.features.chorographyInject": "开局注入档位",
|
|
@@ -1164,6 +1220,13 @@ window.__ModuleLoader__.load({
|
|
|
1164
1220
|
"memory.panel.empty": "No memories yet",
|
|
1165
1221
|
"memory.panel.open": "Memory",
|
|
1166
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",
|
|
1167
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.",
|
|
1168
1231
|
"wiki.empty.disabled": "chorography (Repo Atlas) is disabled: turn it on under Settings and restart DSH to apply.",
|
|
1169
1232
|
"wiki.empty.pickRepo": "Multiple repositories registered — pick one:",
|
|
@@ -1241,12 +1304,48 @@ window.__ModuleLoader__.load({
|
|
|
1241
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.",
|
|
1242
1305
|
"memory.settings.personaViewGateOff": "The master switch “Persona (beta)” is off: the block below contains profile and rules only.",
|
|
1243
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",
|
|
1244
1341
|
"memory.features.personaEnabled": "Persona (beta)",
|
|
1245
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.",
|
|
1246
1343
|
"memory.features.personaStrictAddressing": "Strict persona addressing",
|
|
1247
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.",
|
|
1248
1345
|
"memory.features.profileExtractEnabled": "Profile draft from memory",
|
|
1249
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).",
|
|
1250
1349
|
"memory.features.chorographyEnabled": "Repo atlas (chorography)",
|
|
1251
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.",
|
|
1252
1351
|
"memory.features.chorographyInject": "Opening injection tier",
|
|
@@ -2290,6 +2389,11 @@ window.__ModuleLoader__.load({
|
|
|
2290
2389
|
// .mneme-panel 根内(#562 的中列整页容器),不新增渲染根。
|
|
2291
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)}",
|
|
2292
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}",
|
|
2293
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}",
|
|
2294
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)}",
|
|
2295
2399
|
".mneme-wikibody{flex:1;min-height:0;display:flex;overflow:hidden}",
|
|
@@ -2307,8 +2411,16 @@ window.__ModuleLoader__.load({
|
|
|
2307
2411
|
".mneme-wikipage li{font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);margin:3px 0}",
|
|
2308
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}",
|
|
2309
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}",
|
|
2310
|
-
|
|
2311
|
-
|
|
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}",
|
|
2312
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}",
|
|
2313
2425
|
".mneme-wikiemptytitle{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
2314
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)}",
|
|
@@ -3032,6 +3144,9 @@ window.__ModuleLoader__.load({
|
|
|
3032
3144
|
const dragRef = useRef(null); // { id, moved }
|
|
3033
3145
|
const viewRef = useRef({ x: 0, y: 0, k: 1 }); // pan/zoom viewport, viewBox units
|
|
3034
3146
|
const [frame, setFrame] = useState(0); // re-render tick driven by the simulation
|
|
3147
|
+
// 通用失效广播的重取游标:记忆行的增删改会连带实体属性/关联记忆/关系边,
|
|
3148
|
+
// 目录与各层数据的 effect 依赖都带上它(见下方订阅 effect)。
|
|
3149
|
+
const [dataTick, setDataTick] = useState(0);
|
|
3035
3150
|
|
|
3036
3151
|
useEffect(() => {
|
|
3037
3152
|
let cancelled = false;
|
|
@@ -3044,6 +3159,14 @@ window.__ModuleLoader__.load({
|
|
|
3044
3159
|
})
|
|
3045
3160
|
.catch(() => { if (!cancelled) { setEntities([]); setEntityTotal(null); } });
|
|
3046
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);
|
|
3047
3170
|
}, []);
|
|
3048
3171
|
|
|
3049
3172
|
useEffect(() => {
|
|
@@ -3077,7 +3200,7 @@ window.__ModuleLoader__.load({
|
|
|
3077
3200
|
}
|
|
3078
3201
|
}, []);
|
|
3079
3202
|
|
|
3080
|
-
useEffect(() => { load(entityName, depth); }, [load, entityName, depth]);
|
|
3203
|
+
useEffect(() => { load(entityName, depth); }, [load, entityName, depth, dataTick]);
|
|
3081
3204
|
|
|
3082
3205
|
// Layer data for the selected entity: current attrs + related memories.
|
|
3083
3206
|
useEffect(() => {
|
|
@@ -3094,7 +3217,7 @@ window.__ModuleLoader__.load({
|
|
|
3094
3217
|
.then((j) => { if (!cancelled) setRelated(Array.isArray(j.items) ? j.items : []); })
|
|
3095
3218
|
.catch(() => {});
|
|
3096
3219
|
return () => { cancelled = true; };
|
|
3097
|
-
}, [entityName]);
|
|
3220
|
+
}, [entityName, dataTick]);
|
|
3098
3221
|
|
|
3099
3222
|
// issue #24 块4 复核面:只在展开时取,收起时不发请求。
|
|
3100
3223
|
const loadBoosted = useCallback(async () => {
|
|
@@ -3587,6 +3710,10 @@ window.__ModuleLoader__.load({
|
|
|
3587
3710
|
// 人格内容在设置页「个性化」区的人格卡。
|
|
3588
3711
|
// 严格寻址是同一条链上的第二个口径开关(只在总闸打开时起作用)。
|
|
3589
3712
|
"personaEnabled", "personaStrictAddressing", "profileExtractEnabled",
|
|
3713
|
+
// 自动事实(profile_facts):dream 后的自动维护闸。展示与人工修正不受它管
|
|
3714
|
+
// (GET/apply 在 flag 关时照常放行,存量事实要能看能改),它只决定「自动做梦
|
|
3715
|
+
// 维护」与面板「立即更新」这两条 LLM 路径开不开。
|
|
3716
|
+
"profileFactsEnabled",
|
|
3590
3717
|
// chorography(仓库地志)的暴露闸(M1 就有的键,M3 才上 UI):它是「是否
|
|
3591
3718
|
// 启用这个子系统」而不是「每轮付多少」,所以和上面这些行为开关同组;
|
|
3592
3719
|
// 每轮付出多少由紧随组尾的 choroSub 档位管。放组末是因为子块按组尾渲染
|
|
@@ -3715,6 +3842,10 @@ window.__ModuleLoader__.load({
|
|
|
3715
3842
|
}
|
|
3716
3843
|
setState(await res.json());
|
|
3717
3844
|
setSaveError("");
|
|
3845
|
+
// 功能开关即改即存:别的卡在读同一份 effective 快照(画像事实卡的
|
|
3846
|
+
// 「立即更新」按钮禁用态)——广播让它们重读,不然按提示开了开关还得
|
|
3847
|
+
// 关面板重开才解禁,正是本车道广播工作项要消灭的那类体验。
|
|
3848
|
+
try { window.dispatchEvent(new CustomEvent("mneme:features-changed")); } catch { /* 非浏览器环境 */ }
|
|
3718
3849
|
announce(t("memory.features.restartHint"));
|
|
3719
3850
|
setSavedTick(true);
|
|
3720
3851
|
setTimeout(() => setSavedTick(false), 1800);
|
|
@@ -4564,6 +4695,203 @@ window.__ModuleLoader__.load({
|
|
|
4564
4695
|
savePersonaCards(personaDefault, next);
|
|
4565
4696
|
};
|
|
4566
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
|
+
|
|
4567
4895
|
async function putRules(next) {
|
|
4568
4896
|
await apiFetch("/api/dsh-mneme/rules", {
|
|
4569
4897
|
method: "PUT",
|
|
@@ -4968,6 +5296,147 @@ window.__ModuleLoader__.load({
|
|
|
4968
5296
|
)
|
|
4969
5297
|
) : null
|
|
4970
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
|
+
)
|
|
5439
|
+
),
|
|
4971
5440
|
// 规则 — numbered rows, hover reveals the delete affordance
|
|
4972
5441
|
h("section", { className: "mneme-set-card" },
|
|
4973
5442
|
h("div", { className: "mneme-set-title" }, t("memory.settings.rules")),
|
|
@@ -5746,6 +6215,8 @@ window.__ModuleLoader__.load({
|
|
|
5746
6215
|
setResult(r.body);
|
|
5747
6216
|
setArmed(false);
|
|
5748
6217
|
setRunning(false);
|
|
6218
|
+
// 回收确认模式真的删了行:主列表/实体页/工作台的计数都变了,广播重取。
|
|
6219
|
+
broadcastStoreChanged("reclaim");
|
|
5749
6220
|
})
|
|
5750
6221
|
.catch(() => { setResultError(t("memory.status.maint.failed")); setRunning(false); });
|
|
5751
6222
|
};
|
|
@@ -6099,7 +6570,14 @@ window.__ModuleLoader__.load({
|
|
|
6099
6570
|
};
|
|
6100
6571
|
loadConflicts();
|
|
6101
6572
|
window.addEventListener("mneme:conflicts-changed", loadConflicts);
|
|
6102
|
-
|
|
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
|
+
};
|
|
6103
6581
|
}, []);
|
|
6104
6582
|
|
|
6105
6583
|
const loading = !stats && !statsError;
|
|
@@ -6446,9 +6924,15 @@ window.__ModuleLoader__.load({
|
|
|
6446
6924
|
};
|
|
6447
6925
|
loadOnce();
|
|
6448
6926
|
// #295 评审:裁决完成后队列会广播 mneme:conflicts-changed,这里重取
|
|
6449
|
-
//
|
|
6927
|
+
// 计数,状态卡的「待确认冲突」不再停留旧值。涉及冲突的记忆被删除/
|
|
6928
|
+
// 恢复同样改变计数,通用失效广播一并听。
|
|
6450
6929
|
window.addEventListener("mneme:conflicts-changed", loadOnce);
|
|
6451
|
-
|
|
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
|
+
};
|
|
6452
6936
|
}, []);
|
|
6453
6937
|
// #495:这张卡的标题是「最近巩固」,取数就该是巩固轮,而不是 runs[0]。
|
|
6454
6938
|
// organize 与巩固共用 dream_runs,一次 agent 主动整理会写两行(dryRun 报告 +
|
|
@@ -6602,6 +7086,14 @@ window.__ModuleLoader__.load({
|
|
|
6602
7086
|
.catch(() => { setItems([]); setLoadError(true); });
|
|
6603
7087
|
};
|
|
6604
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
|
+
}, []);
|
|
6605
7097
|
// #177:空队列不再整块消失——空态教育卡要渲染(原 return null 已移除)。
|
|
6606
7098
|
if (!items) return null;
|
|
6607
7099
|
const resolve = (id, winner) => {
|
|
@@ -6622,6 +7114,10 @@ window.__ModuleLoader__.load({
|
|
|
6622
7114
|
// 读一次 dream-status,不广播的话状态卡停留在旧计数。同页两个组件
|
|
6623
7115
|
// 用一个 window 事件对齐(比提升状态到 StatusPanel 少动三处)。
|
|
6624
7116
|
try { window.dispatchEvent(new CustomEvent("mneme:conflicts-changed")); } catch { /* 非浏览器环境 */ }
|
|
7117
|
+
// 裁决会归档败者、给胜者追加已否决注记——记忆列表、实体页、归档
|
|
7118
|
+
// 工作台的数据全变了,走通用失效广播让它们立刻重取(用户反馈:
|
|
7119
|
+
// 以前要关面板重开才能看到裁决落地)。
|
|
7120
|
+
broadcastStoreChanged("conflict-resolve");
|
|
6625
7121
|
load(true);
|
|
6626
7122
|
})
|
|
6627
7123
|
.catch(() => {})
|
|
@@ -6789,6 +7285,13 @@ window.__ModuleLoader__.load({
|
|
|
6789
7285
|
return () => { cancelled = true; };
|
|
6790
7286
|
}, [wbReload]);
|
|
6791
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
|
+
}, []);
|
|
6792
7295
|
|
|
6793
7296
|
const restore = (id) => {
|
|
6794
7297
|
setRestoring(id);
|
|
@@ -6801,6 +7304,8 @@ window.__ModuleLoader__.load({
|
|
|
6801
7304
|
.then(() => {
|
|
6802
7305
|
setArchived((cur) => (cur || []).filter((m) => m.id !== id));
|
|
6803
7306
|
setArchivedTotal((n) => Math.max(0, n - 1));
|
|
7307
|
+
// 恢复让行回到主列表:记忆页与实体页不知道这次 /update,广播让它们重取。
|
|
7308
|
+
broadcastStoreChanged("memory-restore");
|
|
6804
7309
|
})
|
|
6805
7310
|
.catch(() => {})
|
|
6806
7311
|
.finally(() => setRestoring(""));
|
|
@@ -8105,6 +8610,16 @@ window.__ModuleLoader__.load({
|
|
|
8105
8610
|
return () => { cancelled = true; };
|
|
8106
8611
|
}, [reloadKey]);
|
|
8107
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
|
+
|
|
8108
8623
|
// One query string behind every browse-list fetch (initial page,
|
|
8109
8624
|
// loadMore, auto-refresh): the type filter, importance floor and time
|
|
8110
8625
|
// window always travel together, so counts and pagination stay
|
|
@@ -8301,6 +8816,8 @@ window.__ModuleLoader__.load({
|
|
|
8301
8816
|
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== id) : cur));
|
|
8302
8817
|
setTotal((n) => Math.max(0, n - 1));
|
|
8303
8818
|
setSelectedId(null);
|
|
8819
|
+
// 删除同样广播:冲突队列的联表侧、实体页的关联记忆都引用这条 id。
|
|
8820
|
+
broadcastStoreChanged("memory-delete");
|
|
8304
8821
|
setToastMsg(t("memory.explorer.deleted"));
|
|
8305
8822
|
setTimeout(() => setToastMsg(""), 2500);
|
|
8306
8823
|
})
|
|
@@ -8313,7 +8830,10 @@ window.__ModuleLoader__.load({
|
|
|
8313
8830
|
|
|
8314
8831
|
// 编辑保存 / 归档的本地落账:替换或移除对应行;镜像由后端重渲染。
|
|
8315
8832
|
// 归档从所有视图移除(列表默认只看未归档),并清空选中。
|
|
8833
|
+
// 调用点在服务端提交成功之后,所以这里同时是通用失效广播的发布点:
|
|
8834
|
+
// 其它页签(实体/状态工作台/冲突队列)不知道这次 /update,不广播就陈旧。
|
|
8316
8835
|
const applyMemoryUpdate = (updated, { archived = false, restored = false } = {}) => {
|
|
8836
|
+
broadcastStoreChanged(archived ? "memory-archive" : restored ? "memory-restore" : "memory-edit");
|
|
8317
8837
|
if (archived) {
|
|
8318
8838
|
setItems((cur) => cur.filter((m) => m.id !== updated.id));
|
|
8319
8839
|
setRemoteItems((cur) => (cur ? cur.filter((m) => m.id !== updated.id) : cur));
|
|
@@ -8488,26 +9008,26 @@ window.__ModuleLoader__.load({
|
|
|
8488
9008
|
importOpen && h(ImportDialog, {
|
|
8489
9009
|
t,
|
|
8490
9010
|
onClose: () => setImportOpen(false),
|
|
8491
|
-
onImported: () => setReloadKey((k) => k + 1)
|
|
9011
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
8492
9012
|
}),
|
|
8493
9013
|
bootstrapOpen && h(BootstrapDialog, {
|
|
8494
9014
|
t,
|
|
8495
9015
|
onClose: () => setBootstrapOpen(false),
|
|
8496
9016
|
onBusyChange: reportBootstrapBusy,
|
|
8497
|
-
onImported: () => setReloadKey((k) => k + 1)
|
|
9017
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
8498
9018
|
}),
|
|
8499
9019
|
shareOpen && h(ShareDialog, { t, onClose: () => setShareOpen(false) }),
|
|
8500
9020
|
receiveOpen && h(ReceiveDialog, {
|
|
8501
9021
|
t,
|
|
8502
9022
|
onClose: () => setReceiveOpen(false),
|
|
8503
|
-
onImported: () => setReloadKey((k) => k + 1)
|
|
9023
|
+
onImported: () => { setReloadKey((k) => k + 1); broadcastStoreChanged("import"); }
|
|
8504
9024
|
}),
|
|
8505
9025
|
createOpen && h(CreateDialog, {
|
|
8506
9026
|
t,
|
|
8507
9027
|
onClose: () => setCreateOpen(false),
|
|
8508
9028
|
// 新建后既刷新列表也选中新行:并入既有条目时 action 是 merged,
|
|
8509
9029
|
// 返回的 id 就是被并入那一行——两种情况都该把它滚进视野。
|
|
8510
|
-
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); }
|
|
8511
9031
|
})
|
|
8512
9032
|
)
|
|
8513
9033
|
),
|
|
@@ -8753,6 +9273,7 @@ window.__ModuleLoader__.load({
|
|
|
8753
9273
|
// (toast 形状照详情抽屉里 restored 那一条的先例,不带行号免得漂)。
|
|
8754
9274
|
setSelectedId(null);
|
|
8755
9275
|
setReloadKey((k) => k + 1);
|
|
9276
|
+
broadcastStoreChanged("pack-undo");
|
|
8756
9277
|
setToastMsg(t("memory.explorer.undoPackDone").replace("{n}", String(n)));
|
|
8757
9278
|
setTimeout(() => setToastMsg(""), 2500);
|
|
8758
9279
|
}
|
|
@@ -8839,9 +9360,13 @@ window.__ModuleLoader__.load({
|
|
|
8839
9360
|
const unsubscribe = conflictBadgeCount.subscribe(setPending);
|
|
8840
9361
|
const onChanged = () => { conflictBadgeCount.refresh(); };
|
|
8841
9362
|
window.addEventListener("mneme:conflicts-changed", onChanged);
|
|
9363
|
+
// 涉及冲突的记忆在别处被删除/恢复时 pending 集同样会变——裁决广播
|
|
9364
|
+
// 之外的第二条失效路径(此前只能等 60s 兜底轮询)。
|
|
9365
|
+
window.addEventListener("mneme:store-changed", onChanged);
|
|
8842
9366
|
return () => {
|
|
8843
9367
|
unsubscribe();
|
|
8844
9368
|
window.removeEventListener("mneme:conflicts-changed", onChanged);
|
|
9369
|
+
window.removeEventListener("mneme:store-changed", onChanged);
|
|
8845
9370
|
};
|
|
8846
9371
|
}, []);
|
|
8847
9372
|
return pending;
|
|
@@ -10197,19 +10722,57 @@ window.__ModuleLoader__.load({
|
|
|
10197
10722
|
let listItems = [];
|
|
10198
10723
|
let inCode = false;
|
|
10199
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
|
+
};
|
|
10200
10763
|
for (; i < lines.length; i++) {
|
|
10201
10764
|
const line = lines[i];
|
|
10202
10765
|
if (line.trim().startsWith("```")) {
|
|
10203
10766
|
if (inCode) {
|
|
10204
10767
|
out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10205
10768
|
codeLines = []; inCode = false;
|
|
10206
|
-
} else { flushList(); inCode = true; }
|
|
10769
|
+
} else { flushList(); flushTable(); inCode = true; }
|
|
10207
10770
|
continue;
|
|
10208
10771
|
}
|
|
10209
10772
|
if (inCode) { codeLines.push(line); continue; }
|
|
10210
10773
|
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
10211
10774
|
if (heading) {
|
|
10212
|
-
flushList();
|
|
10775
|
+
flushList(); flushTable();
|
|
10213
10776
|
const level = Math.min(heading[1].length + 1, 5);
|
|
10214
10777
|
out.push(h(`h${level}`, { key: out.length }, inline(heading[2])));
|
|
10215
10778
|
continue;
|
|
@@ -10217,25 +10780,26 @@ window.__ModuleLoader__.load({
|
|
|
10217
10780
|
const ul = line.match(/^\s*[-*]\s+(.*)$/);
|
|
10218
10781
|
const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
|
|
10219
10782
|
if (ul || ol) {
|
|
10783
|
+
flushTable();
|
|
10220
10784
|
const want = ul ? "ul" : "ol";
|
|
10221
10785
|
if (list !== want) flushList();
|
|
10222
10786
|
list = want;
|
|
10223
10787
|
listItems.push(h("li", { key: listItems.length }, inline(ul ? ul[1] : ol[1])));
|
|
10224
10788
|
continue;
|
|
10225
10789
|
}
|
|
10226
|
-
flushList();
|
|
10227
|
-
|
|
10790
|
+
if (/^\s*\|/.test(line)) { flushList(); tableRows = tableRows ?? []; tableRows.push(line); continue; }
|
|
10791
|
+
flushList(); flushTable();
|
|
10228
10792
|
if (/^>\s?/.test(line)) { out.push(h("blockquote", { key: out.length, className: "mneme-wikiquote" }, inline(line.replace(/^>\s?/, "")))); continue; }
|
|
10229
10793
|
if (line.trim() === "") continue;
|
|
10230
10794
|
out.push(h("p", { key: out.length }, inline(line)));
|
|
10231
10795
|
}
|
|
10232
|
-
flushList();
|
|
10796
|
+
flushList(); flushTable();
|
|
10233
10797
|
if (inCode && codeLines.length > 0) out.push(h("pre", { key: out.length, className: "mneme-wikipre" }, codeLines.join("\n")));
|
|
10234
10798
|
return out;
|
|
10235
10799
|
}
|
|
10236
10800
|
|
|
10237
10801
|
function WikiMainPanel({ t }) {
|
|
10238
|
-
const [state, setState] = useState({ phase: "loading", repos: [], repo: null, modules: [], edges: [], page: null, hits: [], q: "", selected: null, disabled: false, error: null });
|
|
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 });
|
|
10239
10803
|
// apiFetch 是裸 fetch:非 2xx 不抛错,返回 Response。这里统一解 JSON 并把
|
|
10240
10804
|
// 状态码带回来——409 disabled(用户没开)/ 503 unavailable(装配缺件)
|
|
10241
10805
|
// 各有各的空态文案(#497 的语义分叉在这里接住)。
|
|
@@ -10282,6 +10846,35 @@ window.__ModuleLoader__.load({
|
|
|
10282
10846
|
setState((s) => ({ ...s, hits: data.hits ?? [] }));
|
|
10283
10847
|
} catch { setState((s) => ({ ...s, hits: [] })); }
|
|
10284
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]);
|
|
10285
10878
|
|
|
10286
10879
|
if (state.phase === "loading") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.state.loading"));
|
|
10287
10880
|
if (state.phase === "disabled") return h("div", { className: "mneme-panel mneme-wikiempty" }, t("wiki.empty.disabled"));
|
|
@@ -10326,7 +10919,15 @@ window.__ModuleLoader__.load({
|
|
|
10326
10919
|
value: state.q,
|
|
10327
10920
|
onChange: (e) => runSearch(e.target.value),
|
|
10328
10921
|
"aria-label": t("wiki.search.placeholder")
|
|
10329
|
-
})
|
|
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)),
|
|
10330
10931
|
h("div", { className: "mneme-wikibody" },
|
|
10331
10932
|
h("nav", { className: "mneme-wikitree", "aria-label": t("wiki.tree.title") }, tree),
|
|
10332
10933
|
h("div", { className: "mneme-wikipage" },
|