sage-mem 2.5.1 → 2.6.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.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +453 -18
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
14
14
|
|
|
15
|
-
**Current release: `2.
|
|
15
|
+
**Current release: `2.6.0`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
|
|
16
16
|
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
17
17
|
|
|
18
18
|
If it is useful to you, a Star is the most direct feedback.
|
|
@@ -191,7 +191,7 @@ Since 1.9.0 the memory-management surface follows the harness language (**Settin
|
|
|
191
191
|
|
|
192
192
|

|
|
193
193
|
|
|
194
|
-
*Health — one screen answers "is anything wrong with this library": the **conflict queue on top** (a finite list that needs your decision: which of two contradictory statements is void) and the **audit below** (dangling index entries, missing index entries, broken links and the rest, reported in red by category). "Points to an archived memory" is only a hint — it does not count as a problem, and there is no one-click fix
|
|
194
|
+
*Health — one screen answers "is anything wrong with this library": the **conflict queue on top** (a finite list that needs your decision: which of two contradictory statements is void) and the **audit below** (dangling index entries, missing index entries, broken links and the rest, reported in red by category). "Points to an archived memory" is only a hint — it does not count as a problem, and there is **no one-click fix**: which one to change is your call. The audit only reports and never modifies. When there is really something to deal with, the badge gets a **「复制修复提示词」(copy fix prompt)** button next to it (2.6.0): it turns what this screen needs into one prompt in your clipboard — **the audit problems and the pending conflicts are written as two separate sections** (fix these / only advise on those), so you can paste it to the agent and let it work. A receipt appears at the bottom afterwards — it closes itself in 10 seconds, closes as soon as you switch tabs, and you can dismiss it by hand.*
|
|
195
195
|
|
|
196
196
|

|
|
197
197
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
> File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
|
|
16
16
|
|
|
17
|
-
**当前发布版本:`2.
|
|
17
|
+
**当前发布版本:`2.6.0`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
|
|
18
18
|
[契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
|
|
19
19
|
|
|
20
20
|
如果它对你有用,给个 Star 是最直接的反馈。
|
|
@@ -191,7 +191,7 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
191
191
|
|
|
192
192
|

|
|
193
193
|
|
|
194
|
-
*库健康 ——
|
|
194
|
+
*库健康 —— 一屏回答「这个库有没有事」:**冲突队列在上**(要你裁决的有限队列:两条说法互斥,选哪一方作废),**体检在下**(索引悬空、漏索引、双链断链等结构问题按类报红)。「指向已归档」只作提示、不计入问题;也**不提供一键修补** —— 改哪条由你决定。体检只报告、不修改。真有事要处理时,徽标后面会多一颗**「复制修复提示词」**(2.6.0):把这一屏要处理的事整理成一段提示词放进剪贴板 —— **体检问题与待裁决冲突分两段写**(前者照清单修、后者只给建议),粘给智能体就能照着办;复制成功后下方弹一条提示,10 秒自收、换页签即收、也能自己关掉。*
|
|
195
195
|
|
|
196
196
|

|
|
197
197
|
|
package/lib/client.js
CHANGED
|
@@ -2551,6 +2551,17 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2551
2551
|
".smem-modal-body{font-size:12.5px;line-height:1.65;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:8px;}",
|
|
2552
2552
|
".smem-modal-foot{display:flex;gap:8px;justify-content:flex-end;margin-top:14px;}",
|
|
2553
2553
|
"@media (prefers-reduced-motion:reduce){.smem-modal,.smem-modal-scrim{animation:none;}}",
|
|
2554
|
+
// ── 提示条(2.6.0):复制成功的回执,铺在视口下方中央 ──
|
|
2555
|
+
// **容器不吃点击**(pointer-events:none):回执不该拦住下一次操作;只有 × 收回
|
|
2556
|
+
// 点击,让用户能手动关掉。z-index 压在模态(50)之下、抽屉(40)同层之上。
|
|
2557
|
+
".smem-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:41;pointer-events:none;display:flex;align-items:center;gap:10px;max-width:min(560px,92vw);padding:10px 10px 10px 16px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 12px 32px rgba(0,0,0,.38);font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);animation:smem-pop 200ms cubic-bezier(.2,0,0,1);}",
|
|
2558
|
+
".smem-toast-check{flex:none;color:var(--dsw-alias-state-success-primary,#16a34a);font-weight:700;}",
|
|
2559
|
+
".smem-toast-text{min-width:0;overflow-wrap:anywhere;}",
|
|
2560
|
+
".smem-toast-x{pointer-events:auto;flex:none;width:22px;height:22px;padding:0;border-radius:6px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1;font-family:inherit;cursor:pointer;}",
|
|
2561
|
+
".smem-toast-x:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);}",
|
|
2562
|
+
"@media (prefers-reduced-motion:reduce){.smem-toast{animation:none;}}",
|
|
2563
|
+
// 兜底模态里的提示词:等宽、尽量多显示几行。
|
|
2564
|
+
".smem-prompt-ta{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11.5px;line-height:1.5;height:38vh;min-height:160px;white-space:pre;resize:vertical;}",
|
|
2554
2565
|
// ── 编辑抽屉(2.5.0,D39):替掉 1.0 起的页面内联表单 `.smem-form` ──
|
|
2555
2566
|
// 抽屉从右侧滑入,遮罩自带淡入。两处都用 `animation` 而不是 transition:mount 即播,
|
|
2556
2567
|
// 不依赖二次渲染(页面不可见时 rAF 不触发)。
|
|
@@ -2577,7 +2588,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2577
2588
|
".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
|
|
2578
2589
|
// ── 模块卡头(2.5.0,D39)────────────────────────────────────────────
|
|
2579
2590
|
// 一屏两张模块卡,就必须看得出「这张卡到哪儿为止」——
|
|
2580
|
-
// 否则下面那张会被读成「当前这一段的内容」(2026-11-08
|
|
2591
|
+
// 否则下面那张会被读成「当前这一段的内容」(2026-11-08 维护时一眼看到的正是这个)。
|
|
2581
2592
|
// 所以每张卡自己带边框;卡头(开关 + 标题 + 状态)在分段之外,底下一条线把它和内容分开。
|
|
2582
2593
|
".smem-module-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;}",
|
|
2583
2594
|
".smem-module-head{padding-bottom:10px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:12px;}",
|
|
@@ -2687,7 +2698,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2687
2698
|
".smem-plugin-card-err{font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#ef4444);overflow-wrap:anywhere;}",
|
|
2688
2699
|
// ── 底部一行仓库链接(引流,不抢视线)──
|
|
2689
2700
|
// ── 求 Star 的角标(2.5.0,D39):不占正文位置,hover 才展开半句话 ──
|
|
2690
|
-
// 2026-11-09
|
|
2701
|
+
// 2026-11-09 调(原话:「太不显眼了」):既然它固定在右下角、不压任何内容,
|
|
2691
2702
|
// 就没必要靠 0.62 的透明度自我隐形 —— 那是给「怕打扰阅读」的按下去的做法,
|
|
2692
2703
|
// 而这一颗星本来就该被看见。现在:不透明、底色抬到 layer-2、字色从 tertiary 抬到 secondary、
|
|
2693
2704
|
// 加一层轻阴影让它离底;hover 才把「喜欢这个插件的话」展开(不常显,仍不占阅读视线)。
|
|
@@ -3476,6 +3487,27 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3476
3487
|
]);
|
|
3477
3488
|
}
|
|
3478
3489
|
|
|
3490
|
+
/**
|
|
3491
|
+
* 一条**不挡操作**的提示条(2.6.0)。
|
|
3492
|
+
*
|
|
3493
|
+
* 为什么不用模态:复制成功是一次「做完了」的回执,不该拦着人继续点 ——
|
|
3494
|
+
* 所以它铺在视口下方中央、容器 `pointer-events:none`(点得穿),只有右上那颗
|
|
3495
|
+
* × 收 `pointer-events:auto`,让用户能手动关掉(`onClose`)。
|
|
3496
|
+
*
|
|
3497
|
+
* 自动消失的计时器由调用方(Section)持有:它才知道「换页签就该收」。
|
|
3498
|
+
*/
|
|
3499
|
+
function Toast(props) {
|
|
3500
|
+
return h("div", { className: "smem-toast", role: "status", "aria-live": "polite" }, [
|
|
3501
|
+
h("span", { className: "smem-toast-check", key: "c", "aria-hidden": "true" }, "✓"),
|
|
3502
|
+
h("span", { className: "smem-toast-text", key: "t" }, String(props.text == null ? "" : props.text)),
|
|
3503
|
+
h("button", {
|
|
3504
|
+
type: "button", key: "x", className: "smem-toast-x",
|
|
3505
|
+
title: props.closeTitle || "", "aria-label": props.closeTitle || "",
|
|
3506
|
+
onClick: props.onClose,
|
|
3507
|
+
}, "×"),
|
|
3508
|
+
]);
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3479
3511
|
function AuditPanel(props) {
|
|
3480
3512
|
const r = props.report || {};
|
|
3481
3513
|
/**
|
|
@@ -3493,6 +3525,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3493
3525
|
])
|
|
3494
3526
|
: null);
|
|
3495
3527
|
const hints = r.archivedLinks || [];
|
|
3528
|
+
// 分节表(字段 / 标题 / 条目格式)与「复制修复提示词」共用同一份,定义在
|
|
3529
|
+
// `client/56-health-prompt.js` 的 `AUDIT_SECTIONS` —— 面板上看到的每一行,
|
|
3530
|
+
// 就是提示词里读到的那一行。这里只负责把它渲染成「标题(说明)」。
|
|
3531
|
+
const sectionTitle = (s) => (s.sub ? s.title + "(" + s.sub + ")" : s.title);
|
|
3496
3532
|
return h("div", { className: "smem-tool-panel" + (standalone ? " smem-tool-panel--full" : " smem-tool-panel--cap"), key: "audit" }, [
|
|
3497
3533
|
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
3498
3534
|
h("span", { className: "smem-tool-title", key: "t" }, "体检"),
|
|
@@ -3506,19 +3542,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3506
3542
|
standalone ? null : toolClose(props),
|
|
3507
3543
|
]),
|
|
3508
3544
|
r.problems
|
|
3509
|
-
? h("div", { key: "secs" },
|
|
3510
|
-
section(
|
|
3511
|
-
|
|
3512
|
-
section("type 与前缀对不上", r.typeMismatch, (x) => x.file + " · type=" + x.type + ",文件名前缀=" + x.prefix),
|
|
3513
|
-
section("行尾是 CRLF", r.crlf, (x) => x.file),
|
|
3514
|
-
section("双链断链", r.brokenLinks, (x) => x.file + " → [[" + x.target + "]]"),
|
|
3515
|
-
section("未加引号的「空格+#」", r.hashHazards, (x) => x.file + " · " + x.line),
|
|
3516
|
-
section("没有 frontmatter", r.noFrontmatter, (x) => x.file),
|
|
3517
|
-
section("读不出来", r.unreadable, (x) => x.file),
|
|
3518
|
-
// 归档中途崩溃留下的私有名残留:它不是一条记忆(扫描/注入都不认),
|
|
3519
|
-
// 但里面可能是用户唯一一份内容 —— 所以要在体检里露头,而不是隐形。
|
|
3520
|
-
section("归档残留(内容可能还在,请确认后改回 .md 或删除)", r.strays, (x) => x.file),
|
|
3521
|
-
])
|
|
3545
|
+
? h("div", { key: "secs" }, AUDIT_SECTIONS.map(function (s) {
|
|
3546
|
+
return section(sectionTitle(s), r[s.field], s.item);
|
|
3547
|
+
}))
|
|
3522
3548
|
: h("div", { className: "smem-empty smem-empty--ok", key: "ok" },
|
|
3523
3549
|
"没有发现问题 —— 索引、双链、frontmatter 与行尾都对得上。"),
|
|
3524
3550
|
hints.length
|
|
@@ -3526,7 +3552,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3526
3552
|
h("div", { className: "smem-audit-h", key: "h" }, "指向已归档(提示,不计入问题)"),
|
|
3527
3553
|
h("div", { className: "smem-ar-hint", key: "n" },
|
|
3528
3554
|
"归档不是删除:这些链接的目标还在档案馆里躺着,恢复它链接就活了 —— 不用改。"),
|
|
3529
|
-
rows(hints,
|
|
3555
|
+
rows(hints, AUDIT_ARCHIVED_LINKS.item),
|
|
3530
3556
|
])
|
|
3531
3557
|
: null,
|
|
3532
3558
|
h("div", { className: "smem-ar-hint", key: "ro" }, "这里只报告、不修改 —— 要改哪条,用那条记忆自己的编辑入口。"),
|
|
@@ -4517,6 +4543,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4517
4543
|
"archive.err.thresholdBad": "{label}的阈值要是 1~3650 之间的天数(现在是「{value}」)",
|
|
4518
4544
|
"archive.notice.thresholdsSaved": "归档条件已保存",
|
|
4519
4545
|
"archive.err.thresholdStill": "{label} 仍是 {value}",
|
|
4546
|
+
|
|
4547
|
+
// ── 库健康 · 复制修复提示词(2.6.0)──
|
|
4548
|
+
// 只有按钮 / title / 提示条 / 兜底模态走 i18n;提示词**正文**仍是中文
|
|
4549
|
+
// (体检卡与冲突卡本身还没 i18n,见 D31 / D40)。
|
|
4550
|
+
"header.copyFixPrompt": "复制修复提示词",
|
|
4551
|
+
"header.copyHealthTitle": "把这 {count} 件事整理成提示词 —— 粘给智能体(体检问题照修,冲突只给建议)",
|
|
4552
|
+
"toast.copied": "复制修复任务成功,请在会话中发送给智能体",
|
|
4553
|
+
"prompt.copyFailedTitle": "复制没成功 —— 手动复制这段提示词",
|
|
4554
|
+
"prompt.copyFailedHint": "点一下文本框全选,再按 Ctrl+C 复制",
|
|
4555
|
+
"prompt.close": "关闭",
|
|
4520
4556
|
};
|
|
4521
4557
|
|
|
4522
4558
|
const EN = {
|
|
@@ -5014,6 +5050,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5014
5050
|
"archive.err.thresholdBad": "{label} threshold must be between 1 and 3650 days (currently “{value}”)",
|
|
5015
5051
|
"archive.notice.thresholdsSaved": "Archive conditions saved",
|
|
5016
5052
|
"archive.err.thresholdStill": "{label} is still {value}",
|
|
5053
|
+
|
|
5054
|
+
// ── Health · copy the fix prompt (2.6.0) ──
|
|
5055
|
+
"header.copyFixPrompt": "Copy fix prompt",
|
|
5056
|
+
"header.copyHealthTitle": "Turn these {count} items into a prompt you can paste to the agent (it fixes the audit problems and only advises on conflicts)",
|
|
5057
|
+
"toast.copied": "Fix task copied — paste it to the agent in the conversation",
|
|
5058
|
+
"prompt.copyFailedTitle": "Copy failed — copy this prompt by hand",
|
|
5059
|
+
"prompt.copyFailedHint": "Click the box to select it all, then press Ctrl+C",
|
|
5060
|
+
"prompt.close": "Close",
|
|
5017
5061
|
};
|
|
5018
5062
|
|
|
5019
5063
|
/**
|
|
@@ -5078,6 +5122,334 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5078
5122
|
else install();
|
|
5079
5123
|
return true;
|
|
5080
5124
|
}
|
|
5125
|
+
// ── 库健康:把「要处理的事」整理成能给智能体的提示词(2.6.0)──────────
|
|
5126
|
+
//
|
|
5127
|
+
// 为什么是「复制」而不是「一键修」:体检与冲突都是只读面 —— 改哪一条、标哪一方
|
|
5128
|
+
// 由人定。智能体该拿到的是一份**可执行的清单**,而不是让面板替人去改文件。
|
|
5129
|
+
//
|
|
5130
|
+
// 为什么生成放在客户端、不新增 remote:整段文本只由「界面已经拿到的体检 / 冲突
|
|
5131
|
+
// 数据」拼出来,宿主半身一个字段都不用加 —— 升级界面不需要重启宿主,也不牵动
|
|
5132
|
+
// typert 契约(那套 manifest 按包名缓存、进程内不失效,加 wire 参数最容易出事)。
|
|
5133
|
+
//
|
|
5134
|
+
// **一颗按钮、两段内容**(2026-11-10 定):界面只留一颗「复制修复提示词」,
|
|
5135
|
+
// 但提示词里必须把 A(体检问题:要修)与 B(待裁决的矛盾:只判断)**分开写清楚** ——
|
|
5136
|
+
// 两段的纪律是相反的,混成一段智能体很容易把「只判断」做成「顺手改了」。
|
|
5137
|
+
//
|
|
5138
|
+
// 提示词正文**暂只出中文**:体检卡与冲突卡本身仍是硬编码中文(D31 遗留),
|
|
5139
|
+
// 半翻译的提示词只会更怪。按钮文案 / title / 提示条走 i18n,正文等 i18n 批次
|
|
5140
|
+
// 连同那两卡一起翻(见 docs/decisions.md D40)。
|
|
5141
|
+
|
|
5142
|
+
/** 每类最多列多少条:再多就让智能体自己看面板,不把剪贴板撑爆。 */
|
|
5143
|
+
const AUDIT_PROMPT_MAX_ITEMS = 50;
|
|
5144
|
+
|
|
5145
|
+
/**
|
|
5146
|
+
* 体检的九类硬问题(**唯一一份**:体检面板的分节与提示词共用)。
|
|
5147
|
+
*
|
|
5148
|
+
* 面板(`client/50-shared-ui.js` 的 `AuditPanel`)用 `title` + `sub` 渲染成
|
|
5149
|
+
* 「索引悬空(索引里有、文件没了)」,提示词用 `title` + `fix` 渲染成
|
|
5150
|
+
* 「【索引悬空】…怎么改」。**`item` 是两处共用的一份**:面板上看到的每一行,
|
|
5151
|
+
* 就是粘给智能体的那一行 —— 改一处不会只改半边。
|
|
5152
|
+
*
|
|
5153
|
+
* 顺序 = `lib/autodream/audit.js` 里 problems 的计数顺序,别按字母重排。
|
|
5154
|
+
*/
|
|
5155
|
+
const AUDIT_SECTIONS = [
|
|
5156
|
+
{
|
|
5157
|
+
field: "dangling", title: "索引悬空", sub: "索引里有、文件没了",
|
|
5158
|
+
fix: "索引里有、文件不存在 —— 从索引删掉这一行;若是文件被误删,把文件补回来",
|
|
5159
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5160
|
+
},
|
|
5161
|
+
{
|
|
5162
|
+
field: "unlisted", title: "漏索引", sub: "文件在、索引里没有",
|
|
5163
|
+
fix: "文件在、索引里没有 —— 在索引里补一行 `- [标题](文件.md) — 一句话`",
|
|
5164
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5165
|
+
},
|
|
5166
|
+
{
|
|
5167
|
+
field: "typeMismatch", title: "type 与前缀对不上", sub: "",
|
|
5168
|
+
fix: "本库以前缀为准,改 frontmatter 的 type(确实该换类型的再改文件名)",
|
|
5169
|
+
item: function (x) {
|
|
5170
|
+
return String(x.file == null ? "" : x.file) +
|
|
5171
|
+
" · type=" + String(x.type == null ? "" : x.type) +
|
|
5172
|
+
",文件名前缀=" + String(x.prefix == null ? "" : x.prefix);
|
|
5173
|
+
},
|
|
5174
|
+
},
|
|
5175
|
+
{
|
|
5176
|
+
field: "crlf", title: "行尾是 CRLF", sub: "",
|
|
5177
|
+
fix: "转成 LF,正文一个字节都不要改",
|
|
5178
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5179
|
+
},
|
|
5180
|
+
{
|
|
5181
|
+
field: "brokenLinks", title: "双链断链", sub: "",
|
|
5182
|
+
fix: "改指向真实存在的文件名,或去掉链接只留文字",
|
|
5183
|
+
item: function (x) {
|
|
5184
|
+
return String(x.file == null ? "" : x.file) + " → [[" + String(x.target == null ? "" : x.target) + "]]";
|
|
5185
|
+
},
|
|
5186
|
+
},
|
|
5187
|
+
{
|
|
5188
|
+
field: "hashHazards", title: "未加引号的「空格+#」", sub: "",
|
|
5189
|
+
fix: "这个值会被 YAML 当注释吞掉 —— 给它加引号",
|
|
5190
|
+
item: function (x) {
|
|
5191
|
+
return String(x.file == null ? "" : x.file) + " · " + String(x.line == null ? "" : x.line);
|
|
5192
|
+
},
|
|
5193
|
+
},
|
|
5194
|
+
{
|
|
5195
|
+
field: "noFrontmatter", title: "没有 frontmatter", sub: "",
|
|
5196
|
+
fix: "按该文件的类型补 name / description / metadata",
|
|
5197
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5198
|
+
},
|
|
5199
|
+
{
|
|
5200
|
+
field: "unreadable", title: "读不出来", sub: "",
|
|
5201
|
+
fix: "先报给我,不要凭猜重写",
|
|
5202
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5203
|
+
},
|
|
5204
|
+
{
|
|
5205
|
+
// 归档中途崩溃留下的私有名残留:它不是一条记忆(扫描 / 注入都不认),
|
|
5206
|
+
// 但里面可能是用户唯一一份内容 —— 所以要在体检里露头,而不是隐形。
|
|
5207
|
+
field: "strays", title: "归档残留", sub: "内容可能还在,请确认后改回 .md 或删除",
|
|
5208
|
+
fix: "归档中途崩溃留下的私有名,内容可能还在 —— 先问我,再决定改回 .md 还是删除",
|
|
5209
|
+
item: function (x) { return String(x.file == null ? "" : x.file); },
|
|
5210
|
+
},
|
|
5211
|
+
];
|
|
5212
|
+
|
|
5213
|
+
/** 「指向已归档」是提示、不计入 problems —— 面板与提示词各用一句自己的标题,条目格式共用。 */
|
|
5214
|
+
const AUDIT_ARCHIVED_LINKS = {
|
|
5215
|
+
field: "archivedLinks",
|
|
5216
|
+
item: function (x) {
|
|
5217
|
+
return String(x.file == null ? "" : x.file) + " → [[" + String(x.target == null ? "" : x.target) + "]]";
|
|
5218
|
+
},
|
|
5219
|
+
};
|
|
5220
|
+
|
|
5221
|
+
/** 提示词里那一节的标题与口径(面板上写的是「不计入问题」,提示词里写「不是错误」)。 */
|
|
5222
|
+
const AUDIT_PROMPT_HINT = {
|
|
5223
|
+
field: AUDIT_ARCHIVED_LINKS.field,
|
|
5224
|
+
title: "指向已归档(提示,不是错误)",
|
|
5225
|
+
fix: "归档不是删除,目标还在档案馆里 —— 能改成指向现有活动记忆就改;改不了就保持原样,不要为此恢复归档,也不要删掉链接所在的记忆",
|
|
5226
|
+
item: AUDIT_ARCHIVED_LINKS.item,
|
|
5227
|
+
};
|
|
5228
|
+
|
|
5229
|
+
/** 冲突类型 → 中文标签(提示词正文只出中文,所以不走界面那份 i18n 键)。 */
|
|
5230
|
+
const CONFLICT_KIND_ZH = {
|
|
5231
|
+
contradiction: "矛盾",
|
|
5232
|
+
stale_version: "版本冲突", staleVersion: "版本冲突",
|
|
5233
|
+
external_change: "外部改动", externalChange: "外部改动",
|
|
5234
|
+
unresolved_merge: "合并未完成", unresolvedMerge: "合并未完成",
|
|
5235
|
+
};
|
|
5236
|
+
|
|
5237
|
+
/** 统计行里的数字:缺字段写 `?`,别把它渲染成 0(0 是「查过、确实没有」)。 */
|
|
5238
|
+
function auditPromptNum(v) {
|
|
5239
|
+
return typeof v === "number" && isFinite(v) ? String(v) : "?";
|
|
5240
|
+
}
|
|
5241
|
+
|
|
5242
|
+
/**
|
|
5243
|
+
* 把一类条目写进 lines;返回这一波一共列了几条。
|
|
5244
|
+
*
|
|
5245
|
+
* 空类**整节不出现**(面板上不显示的分类,提示词里也不该占位),超过上限截断
|
|
5246
|
+
* 并留一句「还有 N 条」—— 悄悄截断会让智能体以为改完了。
|
|
5247
|
+
*/
|
|
5248
|
+
function appendPromptGroups(lines, groups, report) {
|
|
5249
|
+
let listed = 0;
|
|
5250
|
+
groups.forEach(function (g) {
|
|
5251
|
+
const raw = report && Array.isArray(report[g.field]) ? report[g.field] : [];
|
|
5252
|
+
const items = raw.filter(function (x) { return x && typeof x === "object"; });
|
|
5253
|
+
if (!items.length) return;
|
|
5254
|
+
lines.push("");
|
|
5255
|
+
lines.push("【" + g.title + "】" + g.fix + "(" + items.length + " 条)");
|
|
5256
|
+
items.slice(0, AUDIT_PROMPT_MAX_ITEMS).forEach(function (x) {
|
|
5257
|
+
lines.push("- " + g.item(x));
|
|
5258
|
+
});
|
|
5259
|
+
if (items.length > AUDIT_PROMPT_MAX_ITEMS) {
|
|
5260
|
+
lines.push("- …(还有 " + (items.length - AUDIT_PROMPT_MAX_ITEMS) + " 条,见面板)");
|
|
5261
|
+
}
|
|
5262
|
+
listed += items.length;
|
|
5263
|
+
});
|
|
5264
|
+
return listed;
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
/**
|
|
5268
|
+
* A 段(体检问题)的正文行。
|
|
5269
|
+
*
|
|
5270
|
+
* `problems <= 0` 时返回 null:整段不出现,一颗空按钮也不该冒出来。
|
|
5271
|
+
* 返回的是**正文**(统计 + 口径 + 分类 + 收尾),不带自己的开场白 ——
|
|
5272
|
+
* 开场白由 `buildHealthFixPrompt` 统一写,免得合成一颗按钮之后出现两句「请修复」。
|
|
5273
|
+
*/
|
|
5274
|
+
function auditFixLines(report) {
|
|
5275
|
+
const r = report && typeof report === "object" ? report : null;
|
|
5276
|
+
if (!r) return null;
|
|
5277
|
+
const problems = typeof r.problems === "number" && isFinite(r.problems) ? r.problems : 0;
|
|
5278
|
+
if (problems <= 0) return null;
|
|
5279
|
+
|
|
5280
|
+
const lines = [];
|
|
5281
|
+
// 不在这里写「记忆目录」—— 那是整段提示词的开头(`buildHealthFixPrompt` 统一写一次),
|
|
5282
|
+
// 两段各写一遍会在合成后的文本里出现两次同样的路径。
|
|
5283
|
+
lines.push(
|
|
5284
|
+
"扫描 " + auditPromptNum(r.fileCount) + " 个记忆 · 索引 " + auditPromptNum(r.indexEntries) +
|
|
5285
|
+
" 条 · 档案馆 " + auditPromptNum(r.archivedCount) + " 条 · 问题合计 " + problems,
|
|
5286
|
+
);
|
|
5287
|
+
lines.push("");
|
|
5288
|
+
lines.push("修复口径:");
|
|
5289
|
+
// 工具名写成条件句:聊天会话里的智能体通常只有 search_memory / read_memory,
|
|
5290
|
+
// write_memory / archive_memory 属 autodream 工具面 —— 不能假设它一定拿得到。
|
|
5291
|
+
lines.push("1. 能直接改就改(有 sage-mem 的 write_memory / archive_memory 就用它们;没有就直接编辑文件),改完把每条的处理结果列给我。");
|
|
5292
|
+
lines.push("2. 只改清单里指出的那一点:正文与其它 frontmatter 字段不要顺手动,行尾保持 LF。");
|
|
5293
|
+
lines.push("3. 不删记忆:要下线用归档,不要删文件;「归档残留」这一类先问我再动。");
|
|
5294
|
+
lines.push("4. 拿不准的(读不出来、type 该怎么统一)先说不确定,不要猜着改。");
|
|
5295
|
+
|
|
5296
|
+
const listed = appendPromptGroups(lines, AUDIT_SECTIONS, r);
|
|
5297
|
+
const hinted = appendPromptGroups(lines, [AUDIT_PROMPT_HINT], r);
|
|
5298
|
+
lines.push("");
|
|
5299
|
+
lines.push("改完请自己再核对一遍(能跑 audit_memory 就跑),然后告诉我每条改了什么。");
|
|
5300
|
+
return { lines: lines, count: listed, hints: hinted };
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
/**
|
|
5304
|
+
* B 段(待裁决的矛盾)的正文行。
|
|
5305
|
+
*
|
|
5306
|
+
* 只取**待裁决**的记录;一份都没有就返回 null。措辞刻意把裁决权留在人手里
|
|
5307
|
+
* (标失效只写一行 frontmatter,真正点按钮的是我)—— 提示词不是授权书。
|
|
5308
|
+
*/
|
|
5309
|
+
function conflictFixLines(data) {
|
|
5310
|
+
const d = data && typeof data === "object" ? data : null;
|
|
5311
|
+
const all = d && Array.isArray(d.items) ? d.items : [];
|
|
5312
|
+
const items = all.filter(function (x) { return x && typeof x === "object" && x.status === "open"; });
|
|
5313
|
+
if (!items.length) return null;
|
|
5314
|
+
|
|
5315
|
+
const lines = [];
|
|
5316
|
+
lines.push("请对每一条:读这两份正文,用一两句话说清「哪一份过时 / 两边说的其实是不同的事」,并给出你的建议 —— 标哪一方失效,还是两边都留着。拿不准就直说不确定。");
|
|
5317
|
+
lines.push("");
|
|
5318
|
+
items.slice(0, AUDIT_PROMPT_MAX_ITEMS).forEach(function (x, i) {
|
|
5319
|
+
const kind = CONFLICT_KIND_ZH[String(x.kind)] || String(x.kind || "矛盾");
|
|
5320
|
+
const pair = x.otherFile
|
|
5321
|
+
? String(x.file == null ? "" : x.file) + " ↔ " + String(x.otherFile)
|
|
5322
|
+
: String(x.file == null ? "" : x.file);
|
|
5323
|
+
lines.push((i + 1) + ". " + kind + " · " + pair + "(记录 " + String(x.id == null ? "" : x.id) + ")");
|
|
5324
|
+
if (x.note) lines.push(" 备注:" + String(x.note));
|
|
5325
|
+
});
|
|
5326
|
+
if (items.length > AUDIT_PROMPT_MAX_ITEMS) {
|
|
5327
|
+
lines.push("…(还有 " + (items.length - AUDIT_PROMPT_MAX_ITEMS) + " 条,见面板)");
|
|
5328
|
+
}
|
|
5329
|
+
return { lines: lines, count: items.length };
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5332
|
+
/**
|
|
5333
|
+
* 「库健康」一屏 → 一段提示词(**全场唯一入口**,对应界面上那一颗按钮)。
|
|
5334
|
+
*
|
|
5335
|
+
* 两段**分开写**:A 是体检问题(要修),B 是待裁决的矛盾(只判断)。开头的
|
|
5336
|
+
* 一句话先说清这次有哪几段、没什么 —— 智能体不该靠猜「为什么 B 不见了」。
|
|
5337
|
+
* 两边都没有时返回 null:界面据此不渲染那颗按钮。
|
|
5338
|
+
*/
|
|
5339
|
+
function buildHealthFixPrompt(input) {
|
|
5340
|
+
const src = input && typeof input === "object" ? input : {};
|
|
5341
|
+
const audit = src.report;
|
|
5342
|
+
const a = auditFixLines(audit);
|
|
5343
|
+
const b = conflictFixLines(src.conflicts);
|
|
5344
|
+
if (!a && !b) return null;
|
|
5345
|
+
|
|
5346
|
+
const dir = audit && typeof audit.dir === "string" ? audit.dir : "";
|
|
5347
|
+
const lines = [];
|
|
5348
|
+
lines.push("sage-mem「库健康」这一屏要处理的事,分两部分;两部分的纪律不一样,别混。");
|
|
5349
|
+
lines.push("");
|
|
5350
|
+
if (a && b) {
|
|
5351
|
+
lines.push("这次两部分都有:A 是体检发现的结构问题(**要修**),B 是待裁决的矛盾(**只做判断,不要改盘上的文件**)。");
|
|
5352
|
+
} else if (a) {
|
|
5353
|
+
lines.push("这次只有 A:体检发现的结构问题(**要修**);当前没有待裁决的矛盾。");
|
|
5354
|
+
} else {
|
|
5355
|
+
lines.push("这次只有 B:待裁决的矛盾(**只做判断,不要改盘上的文件**);体检没有发现问题。");
|
|
5356
|
+
}
|
|
5357
|
+
if (dir) lines.push("记忆目录:" + dir);
|
|
5358
|
+
|
|
5359
|
+
if (a) {
|
|
5360
|
+
lines.push("");
|
|
5361
|
+
lines.push("════ A · 体检问题:按下面的清单修 ════");
|
|
5362
|
+
Array.prototype.push.apply(lines, a.lines);
|
|
5363
|
+
}
|
|
5364
|
+
if (b) {
|
|
5365
|
+
lines.push("");
|
|
5366
|
+
lines.push("════ B · 待裁决的矛盾:只给判断,不要改盘 ════");
|
|
5367
|
+
lines.push("这些要我来裁决 —— 你只做判断,不要改盘上的文件,也不要把任何一方标失效。面板里的「标失效」只往 frontmatter 加一行 invalid_at、不动正文;点哪颗按钮由我来。");
|
|
5368
|
+
Array.prototype.push.apply(lines, b.lines);
|
|
5369
|
+
}
|
|
5370
|
+
return {
|
|
5371
|
+
text: lines.join("\n"),
|
|
5372
|
+
count: (a ? a.count : 0) + (b ? b.count : 0),
|
|
5373
|
+
hasAudit: !!a,
|
|
5374
|
+
hasConflict: !!b,
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
/**
|
|
5379
|
+
* 写剪贴板:优先异步 Clipboard API,失败降级到隐藏 textarea + execCommand。
|
|
5380
|
+
*
|
|
5381
|
+
* 两条都不承诺「一定成功」—— 局域网 http 部署不是安全上下文,两条都可能被拒;
|
|
5382
|
+
* 调用方拿到 `false` 必须把文本摊出来让人手抄(`PromptFallbackModal`),
|
|
5383
|
+
* 绝不静默失败(D25:点了没反应是最坏的失败形态)。
|
|
5384
|
+
*/
|
|
5385
|
+
function copyPromptText(text) {
|
|
5386
|
+
const value = String(text == null ? "" : text);
|
|
5387
|
+
let clip = null;
|
|
5388
|
+
try {
|
|
5389
|
+
if (typeof navigator !== "undefined" && navigator && navigator.clipboard &&
|
|
5390
|
+
typeof navigator.clipboard.writeText === "function") {
|
|
5391
|
+
clip = navigator.clipboard;
|
|
5392
|
+
}
|
|
5393
|
+
} catch (e) { clip = null; }
|
|
5394
|
+
if (clip) {
|
|
5395
|
+
return Promise.resolve()
|
|
5396
|
+
.then(function () { return clip.writeText(value); })
|
|
5397
|
+
.then(function () { return true; })
|
|
5398
|
+
.catch(function () { return legacyCopyPrompt(value); });
|
|
5399
|
+
}
|
|
5400
|
+
return Promise.resolve(legacyCopyPrompt(value));
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
/** 降级路径:老的 textarea + execCommand(已废弃但仍是 http 页面唯一的出路)。 */
|
|
5404
|
+
function legacyCopyPrompt(value) {
|
|
5405
|
+
try {
|
|
5406
|
+
if (typeof document === "undefined" || !document || !document.body) return false;
|
|
5407
|
+
const ta = document.createElement("textarea");
|
|
5408
|
+
ta.value = value;
|
|
5409
|
+
ta.setAttribute("readonly", "readonly");
|
|
5410
|
+
ta.style.position = "fixed";
|
|
5411
|
+
ta.style.left = "-9999px";
|
|
5412
|
+
ta.style.top = "0";
|
|
5413
|
+
document.body.appendChild(ta);
|
|
5414
|
+
if (typeof ta.select === "function") ta.select();
|
|
5415
|
+
const ok = typeof document.execCommand === "function" && document.execCommand("copy") === true;
|
|
5416
|
+
document.body.removeChild(ta);
|
|
5417
|
+
return ok;
|
|
5418
|
+
} catch (e) {
|
|
5419
|
+
return false;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
/**
|
|
5424
|
+
* 复制失败时的兜底:把提示词摊在一只只读 textarea 里,点一下即全选。
|
|
5425
|
+
*
|
|
5426
|
+
* 用户手动关(× / 关闭 / 点遮罩)—— 与面板里其它模态同一套壳。
|
|
5427
|
+
*/
|
|
5428
|
+
function PromptFallbackModal(props) {
|
|
5429
|
+
return h("div", { className: "smem-modal-host" }, [
|
|
5430
|
+
h("div", { className: "smem-modal-scrim", key: "scrim", onClick: props.onClose }),
|
|
5431
|
+
h("div", {
|
|
5432
|
+
className: "smem-modal", key: "m",
|
|
5433
|
+
role: "dialog", "aria-modal": "true",
|
|
5434
|
+
"aria-label": t("prompt.copyFailedTitle"),
|
|
5435
|
+
}, [
|
|
5436
|
+
h("div", { className: "smem-modal-title", key: "t" }, t("prompt.copyFailedTitle")),
|
|
5437
|
+
h("div", { className: "smem-modal-body", key: "b" }, [
|
|
5438
|
+
h("textarea", {
|
|
5439
|
+
className: "smem-textarea smem-prompt-ta", key: "ta",
|
|
5440
|
+
readOnly: true, rows: 12, value: String(props.text == null ? "" : props.text),
|
|
5441
|
+
onFocus: function (e) {
|
|
5442
|
+
if (e && e.target && typeof e.target.select === "function") e.target.select();
|
|
5443
|
+
},
|
|
5444
|
+
}),
|
|
5445
|
+
h("div", { className: "smem-ar-hint", key: "h" }, t("prompt.copyFailedHint")),
|
|
5446
|
+
]),
|
|
5447
|
+
h("div", { className: "smem-modal-foot", key: "f" }, [
|
|
5448
|
+
h("button", { type: "button", className: "smem-btn", key: "c", onClick: props.onClose }, t("prompt.close")),
|
|
5449
|
+
]),
|
|
5450
|
+
]),
|
|
5451
|
+
]);
|
|
5452
|
+
}
|
|
5081
5453
|
/**
|
|
5082
5454
|
* 「自动做梦」面板 —— 让记忆自己整理一遍(引擎在 宿主侧 lib/autodream.js)。
|
|
5083
5455
|
*
|
|
@@ -5445,7 +5817,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5445
5817
|
// 模块卡头(2.5.0 修):开关 + 标题 + 状态**必须活在分段之外**。
|
|
5446
5818
|
// 它回答的是「这是哪个模块、现在是开是关」,和「这次看哪一段」是两个维度;
|
|
5447
5819
|
// 原先它长在「运行」段里,切到设置 / 记录就一起消失 —— 于是下面那张
|
|
5448
|
-
//
|
|
5820
|
+
// 自动归档的卡看起来像是当前这一段的内容(2026-11-08 一眼看到的就是这个)。
|
|
5449
5821
|
h("div", { className: "smem-autodream-sec smem-module-head", key: "top" }, [
|
|
5450
5822
|
h("div", { className: "smem-autodream-row", key: "r" }, [
|
|
5451
5823
|
h(Switch, {
|
|
@@ -5953,7 +6325,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5953
6325
|
// 根节点带一个稳定的类名(2.5.0):它现在与「自动做梦」同屏堆叠,
|
|
5954
6326
|
// 断言与样式都得能把这一张卡与旁边那张分开 —— 没有类名就只能按整棵树找文本。
|
|
5955
6327
|
//
|
|
5956
|
-
// 2.5.0
|
|
6328
|
+
// 2.5.0 补(2026-11-08 提的结构):这张卡也要有自己的「运行 / 设置」分段。
|
|
5957
6329
|
// 并屏之后两张卡形状必须一样 —— 否则「设置」那一段里出现阈值、档位、说明混在一起,
|
|
5958
6330
|
// 读起来像没分过类。分法照自动做梦那张卡:
|
|
5959
6331
|
// 运行 = 它做什么 + 现在什么档(要看状态、要改档位的人在这一段)
|
|
@@ -6349,6 +6721,11 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6349
6721
|
const audit = sAudit[0], setAudit = sAudit[1];
|
|
6350
6722
|
const sAuditBusy = react.useState(false);
|
|
6351
6723
|
const auditBusy = sAuditBusy[0], setAuditBusy = sAuditBusy[1];
|
|
6724
|
+
// 2.6.0:复制修复提示词的回执(null = 不显示)与复制失败时的兜底模态。
|
|
6725
|
+
const sToast = react.useState(null);
|
|
6726
|
+
const toast = sToast[0], setToast = sToast[1];
|
|
6727
|
+
const sPromptFallback = react.useState(null);
|
|
6728
|
+
const promptFallback = sPromptFallback[0], setPromptFallback = sPromptFallback[1];
|
|
6352
6729
|
// 更新提示:只在**确实有新版本**时才被赋值(查不到/限流/网络错都不赋值)
|
|
6353
6730
|
const sUpdate = react.useState(null);
|
|
6354
6731
|
const updateInfo = sUpdate[0], setUpdateInfo = sUpdate[1];
|
|
@@ -6462,6 +6839,24 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6462
6839
|
}
|
|
6463
6840
|
}, []);
|
|
6464
6841
|
|
|
6842
|
+
/**
|
|
6843
|
+
* 复制提示词的回执(2.6.0):10 秒后自动收掉。
|
|
6844
|
+
*
|
|
6845
|
+
* 计时器挂在 `toast` 上:再复制一次只是换文本 → 依赖变 → 旧计时器清掉、重新计时。
|
|
6846
|
+
* 卸载时 cleanup 清掉,不留悬挂定时器(面板关掉之后再去 setState 是纯净的噪音)。
|
|
6847
|
+
*/
|
|
6848
|
+
react.useEffect(function () {
|
|
6849
|
+
if (!toast) return undefined;
|
|
6850
|
+
const id = setTimeout(function () { setToast(null); }, 10000);
|
|
6851
|
+
return function () { clearTimeout(id); };
|
|
6852
|
+
}, [toast]);
|
|
6853
|
+
|
|
6854
|
+
/**
|
|
6855
|
+
* 换页签立刻收掉回执 —— 用 effect 盯 `tab`,而不是在每个 setTab 的调用点里塞
|
|
6856
|
+
* `setToast(null)`:入口只会越来越多(徽标、抽屉、侧栏),漏一个就是「切了还在」。
|
|
6857
|
+
*/
|
|
6858
|
+
react.useEffect(function () { setToast(null); }, [tab]);
|
|
6859
|
+
|
|
6465
6860
|
/**
|
|
6466
6861
|
* 抽屉「点外部 / Esc 关闭」。
|
|
6467
6862
|
*
|
|
@@ -6892,6 +7287,25 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6892
7287
|
.then(function () { setAuditBusy(false); });
|
|
6893
7288
|
}
|
|
6894
7289
|
|
|
7290
|
+
/**
|
|
7291
|
+
* 把「库健康这一屏要处理的事」复制成一段提示词(2.6.0)。
|
|
7292
|
+
*
|
|
7293
|
+
* 界面上只有**一颗**按钮,提示词里却是**两段**(A 体检问题要修 / B 待裁决只判断)——
|
|
7294
|
+
* 分不分得清由 `buildHealthFixPrompt` 负责,这里只管复制与失败兜底。
|
|
7295
|
+
*
|
|
7296
|
+
* `built` 在两边都没事时是 null;按钮本就不会渲染,这里再挡一次,防将来有别的入口。
|
|
7297
|
+
* 复制失败**不静默**:把提示词摊进模态框让人手抄(局域网 http 不是安全上下文,
|
|
7298
|
+
* Clipboard API 与 execCommand 都可能被拒 —— 「点了没反应」是最坏的形态)。
|
|
7299
|
+
*/
|
|
7300
|
+
function copyHealthPrompt() {
|
|
7301
|
+
const built = buildHealthFixPrompt({ report: audit, conflicts: conf });
|
|
7302
|
+
if (!built) return;
|
|
7303
|
+
copyPromptText(built.text).then(function (ok) {
|
|
7304
|
+
if (ok) { setToast(t("toast.copied")); return; }
|
|
7305
|
+
setPromptFallback(built.text);
|
|
7306
|
+
});
|
|
7307
|
+
}
|
|
7308
|
+
|
|
6895
7309
|
function loadCandidates() {
|
|
6896
7310
|
if (typeof remote.archiveCandidates !== "function") { setError(t("error.noArchiveCandidates")); return; }
|
|
6897
7311
|
setCandBusy(true); setError(null);
|
|
@@ -7344,6 +7758,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7344
7758
|
// 冲突徽标数的是**待裁决**(open)条数 —— 与冲突页签里的「N 条待裁决」同源。
|
|
7345
7759
|
// -1 = 还没取到队列(显示「未载」,不假装 0 条)。
|
|
7346
7760
|
const conflictOpen = conf && conf.counts && typeof conf.counts.open === "number" ? conf.counts.open : -1;
|
|
7761
|
+
// 「复制修复提示词」那颗按钮的分母:体检问题 + 待裁决冲突。两边都没有(=0)时按钮不渲染;
|
|
7762
|
+
// 冲突队列还没载入(-1)不算进分母 —— 免得按钮先冒出来又消失。
|
|
7763
|
+
const healthWork = (audit && typeof audit.problems === "number" && audit.problems > 0 ? audit.problems : 0)
|
|
7764
|
+
+ (conflictOpen > 0 ? conflictOpen : 0);
|
|
7347
7765
|
|
|
7348
7766
|
// 「文件列表」这一屏:状态筛选决定看哪一批,灰色卡片与留痕块自带「退役」语义。
|
|
7349
7767
|
// banner 只在**专门看归档**时出现,切到全部时不重复说一遍。
|
|
@@ -7407,6 +7825,17 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7407
7825
|
: conflictOpen > 0 ? t("header.conflictOpen", { count: conflictOpen })
|
|
7408
7826
|
: t("header.conflictEmpty"))
|
|
7409
7827
|
: null,
|
|
7828
|
+
// 2.6.0:**一颗**按钮收在两组徽标之后(2026-11-10 定:保持界面简洁)。
|
|
7829
|
+
// 只在真有内容时出现(体检有问题 或 冲突待裁决);提示词里 A / B 两段分开写,
|
|
7830
|
+
// 所以一颗按钮不会把「要修」与「只判断」混成一件事。
|
|
7831
|
+
(healthWork > 0)
|
|
7832
|
+
? h("button", {
|
|
7833
|
+
type: "button", className: "smem-btn", key: "cp",
|
|
7834
|
+
title: t("header.copyHealthTitle", { count: healthWork }),
|
|
7835
|
+
"aria-label": t("header.copyHealthTitle", { count: healthWork }),
|
|
7836
|
+
onClick: copyHealthPrompt,
|
|
7837
|
+
}, t("header.copyFixPrompt"))
|
|
7838
|
+
: null,
|
|
7410
7839
|
restartPending
|
|
7411
7840
|
? h("button", {
|
|
7412
7841
|
type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
|
|
@@ -7640,6 +8069,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
7640
8069
|
h(AuditPanel, { key: "audit", report: audit, busy: auditBusy, onRun: runAudit, standalone: true }),
|
|
7641
8070
|
])
|
|
7642
8071
|
: h("div", { className: "smem-view", key: "files" }, filesView),
|
|
8072
|
+
// 2.6.0:复制成功的回执铺在视口下方中央(`position:fixed`,不吃点击,10s 自收,
|
|
8073
|
+
// 也能自己点 × 关)。渲染在 `.smem-root` 直属层 —— 别塞进带进场动画的
|
|
8074
|
+
// `.smem-view`(那个祖先有 transform,`fixed` 会被改写定位)。
|
|
8075
|
+
toast ? h(Toast, { key: "toast", text: toast, closeTitle: t("prompt.close"), onClose: function () { setToast(null); } }) : null,
|
|
8076
|
+
// 复制失败时把提示词摊出来手抄;用户手动关(× / 关闭 / 点遮罩)。
|
|
8077
|
+
promptFallback ? h(PromptFallbackModal, { key: "prompt-fallback", text: promptFallback, onClose: function () { setPromptFallback(null); } }) : null,
|
|
7643
8078
|
// 底部一行仓库链接(四个 tab 都在,不抢视线)
|
|
7644
8079
|
h(StarLine, { key: "star-line" }),
|
|
7645
8080
|
]);
|
package/package.json
CHANGED