sage-mem 2.9.0 → 2.10.2
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 +59 -11
- package/README.md +59 -11
- package/lib/assembly/tools-mount.js +5 -2
- package/lib/autodream/audit.js +58 -1
- package/lib/autodream/prompt.js +30 -2
- package/lib/autodream/report.js +136 -1
- package/lib/autodream/size-tools.js +222 -0
- package/lib/autodream/tools.js +28 -2
- package/lib/autodream/util.js +3 -1
- package/lib/client.js +401 -66
- package/lib/memory/scan.js +6 -1
- package/lib/memory/size-policy.js +218 -0
- package/lib/memory/trim-archive.js +251 -0
- package/lib/memory/trim-plan.js +331 -0
- package/lib/memory/write-protocol.js +31 -2
- package/lib/shared/codes.js +13 -0
- package/lib/shared/limits.js +47 -0
- package/lib/tools/memory-tools.js +215 -6
- package/lib/typert.host.js +14 -0
- package/package.json +1 -1
- package/skills/sage-mem/SKILL.md +37 -2
package/lib/client.js
CHANGED
|
@@ -4601,6 +4601,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4601
4601
|
"ad.err.reservedDup": "名单里已经有 {name} 了",
|
|
4602
4602
|
"ad.note.modelCatalogUnavailable": "模型目录不可用({error})—— 请在下面手填 provider 与 model。",
|
|
4603
4603
|
|
|
4604
|
+
// ── 2.10.1 体积治理:不参与整理的文件(65-tidy-scope.js)──
|
|
4605
|
+
"ad.tidyScope.title": "不参与整理的文件",
|
|
4606
|
+
"ad.tidyScope.count": "已保护 {on} / 共 {total} 个",
|
|
4607
|
+
"ad.tidyScope.note1": "勾上的文件在体积治理时不会被裁剪或压缩 —— 自动做梦与 trim_memory 都按这份名单跳过。",
|
|
4608
|
+
"ad.tidyScope.note2": "内置的两个(索引 / 流水)不能取消;保存后需重启 DSH 才生效。",
|
|
4609
|
+
"ad.tidyScope.builtin": "内置 · 不可取消",
|
|
4610
|
+
"ad.tidyScope.missing": "文件不存在",
|
|
4611
|
+
"ad.tidyScope.locked": "已锁定",
|
|
4612
|
+
"ad.tidyScope.search": "搜索文件名…",
|
|
4613
|
+
"ad.tidyScope.noMatch": "没有匹配的文件名",
|
|
4614
|
+
"ad.tidyScope.save": "保存「不参与整理的文件」",
|
|
4615
|
+
"ad.tidyScope.saving": "保存中…",
|
|
4616
|
+
"ad.tidyScope.saved": "已保存不参与整理的文件 —— 需重启 DSH 才生效",
|
|
4617
|
+
"ad.tidyScope.savedNow": "已保存(宿主说这次不用重启)",
|
|
4618
|
+
"ad.tidyScope.restartHint": "保存后需重启 DSH 才生效",
|
|
4619
|
+
"ad.tidyScope.dirty": "有未保存的改动",
|
|
4620
|
+
"ad.tidyScope.noSettings": "宿主没有提供设置接口(版本偏旧)—— 这一块只能看,不能改",
|
|
4621
|
+
"ad.tidyScope.errList": "读不到文件列表:{error}",
|
|
4622
|
+
"ad.tidyScope.notApplied": "宿主回执里的名单与刚才保存的不一致 —— 刷新后再试一次",
|
|
4623
|
+
|
|
4604
4624
|
// ── 2.1.0: auto-archive settings (62-archive-settings.js) ──
|
|
4605
4625
|
"archive.title": "自动归档做什么",
|
|
4606
4626
|
"archive.intro1": "闲置太久没被注入过的记忆,会被移进 archive/ —— 不删除,随时能恢复。这一页决定「多久算闲置」,以及要不要真的动手。",
|
|
@@ -5134,6 +5154,26 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5134
5154
|
"ad.err.reservedDup": "{name} is already in the list",
|
|
5135
5155
|
"ad.note.modelCatalogUnavailable": "The model catalog is unavailable ({error}) — enter provider and model below by hand.",
|
|
5136
5156
|
|
|
5157
|
+
// ── 2.10.1 size governance: files excluded from tidying (65-tidy-scope.js) ──
|
|
5158
|
+
"ad.tidyScope.title": "Files excluded from tidying",
|
|
5159
|
+
"ad.tidyScope.count": "{on} of {total} protected",
|
|
5160
|
+
"ad.tidyScope.note1": "Checked files are never trimmed or compressed by size governance — Autodream and trim_memory both skip this list.",
|
|
5161
|
+
"ad.tidyScope.note2": "The two built-in names (index / session log) cannot be unchecked; restart DSH for a save to take effect.",
|
|
5162
|
+
"ad.tidyScope.builtin": "built-in, cannot uncheck",
|
|
5163
|
+
"ad.tidyScope.missing": "file not found",
|
|
5164
|
+
"ad.tidyScope.locked": "locked",
|
|
5165
|
+
"ad.tidyScope.search": "Search file names…",
|
|
5166
|
+
"ad.tidyScope.noMatch": "No matching file names",
|
|
5167
|
+
"ad.tidyScope.save": "Save excluded-from-tidying files",
|
|
5168
|
+
"ad.tidyScope.saving": "Saving…",
|
|
5169
|
+
"ad.tidyScope.saved": "Saved — restart DSH for it to take effect",
|
|
5170
|
+
"ad.tidyScope.savedNow": "Saved (the host says no restart is needed)",
|
|
5171
|
+
"ad.tidyScope.restartHint": "Restart DSH for a save to take effect",
|
|
5172
|
+
"ad.tidyScope.dirty": "Unsaved changes",
|
|
5173
|
+
"ad.tidyScope.noSettings": "The host has no settings API (older version) — this block is read-only",
|
|
5174
|
+
"ad.tidyScope.errList": "Could not read the file list: {error}",
|
|
5175
|
+
"ad.tidyScope.notApplied": "The list the host returned differs from what was just saved — reload and try again",
|
|
5176
|
+
|
|
5137
5177
|
// ── 2.1.0: auto-archive settings (62-archive-settings.js) ──
|
|
5138
5178
|
"archive.title": "What auto-archive does",
|
|
5139
5179
|
"archive.intro1": "Memories that have not been injected for too long are moved into archive/ — never deleted, always recoverable. This page decides how long counts as idle, and whether it may actually act.",
|
|
@@ -5618,10 +5658,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5618
5658
|
const sMemSet = react.useState(null); const memSet = sMemSet[0], setMemSet = sMemSet[1];
|
|
5619
5659
|
const sLimitsDraft = react.useState(null); const limitsDraft = sLimitsDraft[0], setLimitsDraft = sLimitsDraft[1];
|
|
5620
5660
|
const sSaveBusy = react.useState(false); const saveBusy = sSaveBusy[0], setSaveBusy = sSaveBusy[1];
|
|
5621
|
-
const sReservedNew = react.useState(""); const reservedNew = sReservedNew[0], setReservedNew = sReservedNew[1];
|
|
5622
5661
|
// ── 2.7.0 界面入口三选一(客户端行为:改完立刻生效,不需要重启)──
|
|
5623
5662
|
const sEntryBusy = react.useState(false); const entryBusy = sEntryBusy[0], setEntryBusy = sEntryBusy[1];
|
|
5624
5663
|
const sEntryErr = react.useState(""); const entryErr = sEntryErr[0], setEntryErr = sEntryErr[1];
|
|
5664
|
+
// ── 2.10.1「不参与整理的文件」那一块的界面状态 ──
|
|
5665
|
+
// **放在宿主里**是有意的:chips 与勾选面板必须是同一份选择(见 65 分片的头注),
|
|
5666
|
+
// 而且这个 hook 的位置在「运行 / 设置 / 记录」三段里恒定 —— 子面板只在设置段出现,
|
|
5667
|
+
// 它自己持状态就会在两段之间错位(宿主渲染器按位置存 hook 状态)。
|
|
5668
|
+
const sTidy = react.useState(null); const tidyState = sTidy[0], setTidyState = sTidy[1];
|
|
5625
5669
|
|
|
5626
5670
|
// 派生量放在 effect 之前:下面几个 effect 的依赖数组要在渲染期就能取到值。
|
|
5627
5671
|
const c = cfg || {};
|
|
@@ -5629,13 +5673,9 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5629
5673
|
const last = (st && st.lastResult) || null;
|
|
5630
5674
|
const route = (st && st.route) || null;
|
|
5631
5675
|
const rollbackScope = c.rollbackScope === "all" ? "all" : "files";
|
|
5632
|
-
//
|
|
5633
|
-
//
|
|
5634
|
-
|
|
5635
|
-
const extraLower = extraReserved.map(function (x) { return String(x).toLowerCase(); });
|
|
5636
|
-
const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
|
|
5637
|
-
return extraLower.indexOf(String(n).toLowerCase()) < 0;
|
|
5638
|
-
});
|
|
5676
|
+
// 2.10.1:保留名名单(保护名单)整块交给 65-tidy-scope.js —— 它同时管
|
|
5677
|
+
// 「chips 上立即落盘」与「勾选面板按保存落盘」,状态只有一份 draft。
|
|
5678
|
+
// 这里不再派生 extraReserved / fixedReserved:**两套状态是这一块以前最脏的地方**。
|
|
5639
5679
|
// 2.7.1(Q04 尾巴):宿主从 2.1.11 起就在 getSettings 里带 `limitDiagnostics`
|
|
5640
5680
|
// —— 坏 JSON / 逐项越界 / 环境变量写错都在这份数组里。以前它只进日志,
|
|
5641
5681
|
// 面板上看不见,于是「我改的值怎么没生效」只能靠猜。现在就地摊开逐条列。
|
|
@@ -5660,6 +5700,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5660
5700
|
refreshLists();
|
|
5661
5701
|
loadModels();
|
|
5662
5702
|
loadMemSettings();
|
|
5703
|
+
loadTidyFiles();
|
|
5663
5704
|
const id = setInterval(pull, 2000);
|
|
5664
5705
|
return function () { alive = false; clearInterval(id); };
|
|
5665
5706
|
}, [remote]);
|
|
@@ -5749,6 +5790,30 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5749
5790
|
}, function (e) { setErr(detail(e)); });
|
|
5750
5791
|
}
|
|
5751
5792
|
|
|
5793
|
+
/** 合并写入「不参与整理的文件」那份界面状态(子面板每次只patch 它关心的键)。 */
|
|
5794
|
+
function patchTidy(p) {
|
|
5795
|
+
setTidyState(function (cur) {
|
|
5796
|
+
const base = cur && typeof cur === "object" ? cur : {};
|
|
5797
|
+
return Object.assign({}, base, p);
|
|
5798
|
+
});
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
/**
|
|
5802
|
+
* 拉一次文件名清单(勾选面板的候选行)。
|
|
5803
|
+
*
|
|
5804
|
+
* 与 getSettings 分开拉:设置读不到(旧宿主)时勾选面板仍要能列出文件并说清「只能看」;
|
|
5805
|
+
* 反过来文件列表读不到也不该让保护名单跟着变空。
|
|
5806
|
+
*/
|
|
5807
|
+
function loadTidyFiles() {
|
|
5808
|
+
if (!memRemote || typeof memRemote.listFiles !== "function") { patchTidy({ files: [] }); return; }
|
|
5809
|
+
Promise.resolve(memRemote.listFiles()).then(function (res) {
|
|
5810
|
+
const d = unwrap(res);
|
|
5811
|
+
patchTidy({ files: Array.isArray(d) ? d : [] });
|
|
5812
|
+
}, function (e) {
|
|
5813
|
+
patchTidy({ files: [], err: t("ad.tidyScope.errList", { error: String((e && e.message) || e) }) });
|
|
5814
|
+
});
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5752
5817
|
/** 保存 5 项上限。越界/非整数**本地先拦**(host 也是直接拒绝),别让它白跑一趟。 */
|
|
5753
5818
|
function saveLimits() {
|
|
5754
5819
|
if (!memRemote || typeof memRemote.setSettings !== "function") { setErr(t("ad.err.noSetSettings")); return; }
|
|
@@ -5802,44 +5867,14 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5802
5867
|
}
|
|
5803
5868
|
|
|
5804
5869
|
/**
|
|
5805
|
-
*
|
|
5870
|
+
* 保存保护名单的入口**已经搬进 65-tidy-scope.js**(`TidyScopePanel` 的 `put`)。
|
|
5806
5871
|
*
|
|
5807
|
-
*
|
|
5808
|
-
*
|
|
5872
|
+
* 为什么搬:2.10.1 起这份名单同时决定「注入时跳过」与「体积治理时跳过」,
|
|
5873
|
+
* 于是它需要两种编辑方式(chips 上点一下 / 勾选面板一次改多个)。两处各自
|
|
5874
|
+
* 持一份状态会打架 —— 现在只有 `tidyState` 一份(就在上面的 hook 里),
|
|
5875
|
+
* 子面板只读它、只按 props 回调改它;这一层负责把宿主回执里的 settings
|
|
5876
|
+
* 放回 memSet(注入参数等其它块靠它保持新鲜)。
|
|
5809
5877
|
*/
|
|
5810
|
-
function saveReserved(list) {
|
|
5811
|
-
if (!memRemote || typeof memRemote.setSettings !== "function") { setErr(t("ad.err.noSetSettings")); return; }
|
|
5812
|
-
setSaveBusy(true); setErr(null); setNotice("");
|
|
5813
|
-
Promise.resolve(memRemote.setSettings({ reserved: list })).then(function (out) {
|
|
5814
|
-
setSaveBusy(false);
|
|
5815
|
-
if (out && out.ok === false) { setErr(out.error || t("ad.err.reservedRejected")); return; }
|
|
5816
|
-
if (out && out.settings) setMemSet(out.settings);
|
|
5817
|
-
else loadMemSettings();
|
|
5818
|
-
setNotice(t("ad.notice.reservedSaved"));
|
|
5819
|
-
}, function (e) { setSaveBusy(false); setErr(detail(e)); });
|
|
5820
|
-
}
|
|
5821
|
-
|
|
5822
|
-
function addReserved() {
|
|
5823
|
-
const name = String(reservedNew || "").trim();
|
|
5824
|
-
const extra = Array.isArray(memSet && memSet.reservedExtra) ? memSet.reservedExtra : [];
|
|
5825
|
-
if (!name) { setErr(t("ad.err.reservedNeedName")); return; }
|
|
5826
|
-
// 与 host 的校验同一套:单个 .md 文件名、不许带路径
|
|
5827
|
-
if (!name.endsWith(".md") || name.includes("/") || name.includes("\\") || name.startsWith("..")) {
|
|
5828
|
-
setErr(t("ad.err.reservedBadName", { name: name }));
|
|
5829
|
-
return;
|
|
5830
|
-
}
|
|
5831
|
-
if (extra.some(function (x) { return String(x).toLowerCase() === name.toLowerCase(); })) {
|
|
5832
|
-
setErr(t("ad.err.reservedDup", { name: name }));
|
|
5833
|
-
return;
|
|
5834
|
-
}
|
|
5835
|
-
setReservedNew("");
|
|
5836
|
-
saveReserved(extra.concat([name]));
|
|
5837
|
-
}
|
|
5838
|
-
|
|
5839
|
-
function removeReserved(name) {
|
|
5840
|
-
const extra = Array.isArray(memSet && memSet.reservedExtra) ? memSet.reservedExtra : [];
|
|
5841
|
-
saveReserved(extra.filter(function (x) { return x !== name; }));
|
|
5842
|
-
}
|
|
5843
5878
|
|
|
5844
5879
|
/** 报告 / 回滚点 / 整理记录三份列表一起刷。 */
|
|
5845
5880
|
function refreshLists() {
|
|
@@ -6174,28 +6209,16 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6174
6209
|
h("span", { className: "smem-status", key: "c" }, t("ad.reserved.subtitle")),
|
|
6175
6210
|
]),
|
|
6176
6211
|
h("div", { className: "smem-autodream-note", key: "n" }, t("ad.reserved.note")),
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
})
|
|
6188
|
-
: h("span", { className: "smem-status", key: "none" }, t("ad.reserved.empty"))),
|
|
6189
|
-
h("div", { className: "smem-autodream-row", key: "add" }, [
|
|
6190
|
-
h("input", {
|
|
6191
|
-
className: "smem-input", key: "i",
|
|
6192
|
-
style: { flex: "1", minWidth: "150px" },
|
|
6193
|
-
value: reservedNew,
|
|
6194
|
-
placeholder: t("ad.reserved.placeholder"),
|
|
6195
|
-
onChange: function (e) { setReservedNew(e.target.value); },
|
|
6196
|
-
}),
|
|
6197
|
-
h("button", { type: "button", className: "smem-btn", key: "a", disabled: saveBusy, onClick: addReserved }, t("ad.action.addToList")),
|
|
6198
|
-
]),
|
|
6212
|
+
// 2.10.1(契约 §6):整块(chips + 手输入口 + 勾选面板 + 回执)由 65 分片渲染。
|
|
6213
|
+
// 这里只把宿主回执收到的 settings 接回来 —— 面板内保存完不会再各自刷新一遍。
|
|
6214
|
+
h(TidyScopePanel, {
|
|
6215
|
+
key: "tidy",
|
|
6216
|
+
ctx: props.ctx,
|
|
6217
|
+
settings: memSet,
|
|
6218
|
+
state: tidyState,
|
|
6219
|
+
onState: setTidyState,
|
|
6220
|
+
onSettings: function (s) { if (s) setMemSet(s); else loadMemSettings(); },
|
|
6221
|
+
}),
|
|
6199
6222
|
]),
|
|
6200
6223
|
h("details", { className: "smem-autodream-adv", key: "adv", open: true }, [
|
|
6201
6224
|
h("summary", { key: "s" }, t("ad.advanced")),
|
|
@@ -6731,6 +6754,318 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6731
6754
|
notice ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
6732
6755
|
]);
|
|
6733
6756
|
}
|
|
6757
|
+
/**
|
|
6758
|
+
* 「不参与整理的文件」勾选面板(2.10.1 体积治理 · 契约 §6)。
|
|
6759
|
+
*
|
|
6760
|
+
* 这一块原来是「一行说明 + 一个输名字的输入框」(自动做梦 → 设置 → 保留名保护名单)。
|
|
6761
|
+
* 名单当时只有一个作用:注入时跳过。手输名字够用,因为写错了的后果只是「多注入一条」。
|
|
6762
|
+
* 2.10.1 起它多了一层作用 —— 自动做梦的体积治理与 `trim_memory` **都按这份名单跳过**,
|
|
6763
|
+
* 于是 `project_note.md` / `project_notes.md` 这种一位之差从「无害」变成「该保护的那条
|
|
6764
|
+
* 被裁了,而你当时以为自己写了」。结论:名字不该靠手打,要从**目录里现成的文件名**里勾。
|
|
6765
|
+
*
|
|
6766
|
+
* 界面上每句话都对应一条事实,不是修饰:
|
|
6767
|
+
* 1. 两个内置名(索引 / 流水)**不可取消** —— 它们各有自己的规则,取消保护只会让
|
|
6768
|
+
* 「被当作普通记忆处理」成为可能。
|
|
6769
|
+
* 2. 保存后**要重启 DSH** —— 名单在模块加载时读进内存(宿主的 `RESERVED_FILES`),
|
|
6770
|
+
* 写盘只是写盘。界面上永远不写「已生效」。
|
|
6771
|
+
* 3. 宿主没有设置接口时**只读、换文案、不报错** —— 旧宿主不该因为少一个接口就红一片。
|
|
6772
|
+
*
|
|
6773
|
+
* 状态只有一份,且**放在宿主里**(`AutodreamPanel` 的 `tidyState`,见 60 分片):
|
|
6774
|
+
* chips、「加入名单」输入框、勾选框画的全是同一份选择,所以不会出现
|
|
6775
|
+
* 「chips 说勾着、框里没勾」。这也是本仓库既有的分工 —— `72-files-view.js` 就是
|
|
6776
|
+
* 「纯渲染,状态在上层」。这个分片因此**不持有任何 hook**:它只把 props 画出来,
|
|
6777
|
+
* 把动作交给 `onState` / `onSettings`。
|
|
6778
|
+
*
|
|
6779
|
+
* 「选择」用**增量**表达(`add` / `drop` 两个数组,相对宿主已保存的 `reservedExtra`):
|
|
6780
|
+
* 这样保存成功后把两个数组清空就等于「已对齐」,不需要第三个影子状态去同步,
|
|
6781
|
+
* 也就没有「同步晚一拍」的那类 bug。
|
|
6782
|
+
*/
|
|
6783
|
+
|
|
6784
|
+
/** 名字比较键:大小写无关(Windows 上 `MEMORY.md` 与 `memory.md` 是同一个文件)。 */
|
|
6785
|
+
function tidyKey(name) {
|
|
6786
|
+
return String(name == null ? "" : name).toLowerCase();
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
/**
|
|
6790
|
+
* 归一化一份名字列表:去空、去重(大小写无关)、保序。
|
|
6791
|
+
* 宿主那边也会做同样的事;这里做是为了「勾选 → 保存 → 回执校验」三处用同一把尺子。
|
|
6792
|
+
*/
|
|
6793
|
+
function tidyList(v) {
|
|
6794
|
+
const out = [];
|
|
6795
|
+
const seen = {};
|
|
6796
|
+
(Array.isArray(v) ? v : []).forEach(function (x) {
|
|
6797
|
+
const s = String(x == null ? "" : x).trim();
|
|
6798
|
+
if (!s) return;
|
|
6799
|
+
const k = tidyKey(s);
|
|
6800
|
+
if (seen[k]) return;
|
|
6801
|
+
seen[k] = true;
|
|
6802
|
+
out.push(s);
|
|
6803
|
+
});
|
|
6804
|
+
return out;
|
|
6805
|
+
}
|
|
6806
|
+
|
|
6807
|
+
/**
|
|
6808
|
+
* 勾选列表的行:内置 → 目录里的文件 → 已声明但文件已不在的。
|
|
6809
|
+
*
|
|
6810
|
+
* 顺序固定是**有意的**:勾一个文件不该让它在列表里换位置(那会让人以为点错了)。
|
|
6811
|
+
* 「已声明但文件已不在」的单列一类 —— 名单里留着一个不存在的名字是合法的
|
|
6812
|
+
* (文件是后来被改名 / 归档的),但必须看得出来,否则用户永远查不出「为什么没保护上」。
|
|
6813
|
+
*/
|
|
6814
|
+
function tidyRows(fixed, names, files) {
|
|
6815
|
+
const rows = [];
|
|
6816
|
+
const seen = {};
|
|
6817
|
+
const push = function (name, kind, meta) {
|
|
6818
|
+
const k = tidyKey(name);
|
|
6819
|
+
if (!k || seen[k]) return;
|
|
6820
|
+
seen[k] = true;
|
|
6821
|
+
rows.push({ name: String(name), kind: kind, meta: meta || null });
|
|
6822
|
+
};
|
|
6823
|
+
fixed.forEach(function (n) { push(n, "fixed", null); });
|
|
6824
|
+
files.forEach(function (f) { if (f && f.file) push(f.file, "file", f); });
|
|
6825
|
+
// 用户声明的(含本趟新勾的)——已在上面出现过的不再重复
|
|
6826
|
+
tidyList(names).forEach(function (n) { push(n, "extra", null); });
|
|
6827
|
+
return rows;
|
|
6828
|
+
}
|
|
6829
|
+
|
|
6830
|
+
function TidyScopePanel(props) {
|
|
6831
|
+
const memRemote = stableRemote(props.ctx, "remote.memory");
|
|
6832
|
+
const settings = props.settings || null;
|
|
6833
|
+
// 宿主里那份界面状态(`{}` 兜底:空 / 旧值都当「什么都没做」)。
|
|
6834
|
+
const view = props.state && typeof props.state === "object" ? props.state : {};
|
|
6835
|
+
const patch = function (p) {
|
|
6836
|
+
if (typeof props.onState === "function") props.onState(Object.assign({}, view, p));
|
|
6837
|
+
};
|
|
6838
|
+
|
|
6839
|
+
// 能力分两层(与设置页其它块同一条):整个 remote 没有 / 只有读没有写。
|
|
6840
|
+
const canRead = !!(memRemote && typeof memRemote.getSettings === "function");
|
|
6841
|
+
const canWrite = !!(memRemote && typeof memRemote.setSettings === "function");
|
|
6842
|
+
|
|
6843
|
+
// 每个字段都重新认一遍类型:这是「渲染不得抛」的最后一道(host 回执 / 旧状态都可能歪)。
|
|
6844
|
+
const files = Array.isArray(view.files) ? view.files : [];
|
|
6845
|
+
const query = typeof view.query === "string" ? view.query : "";
|
|
6846
|
+
const newName = typeof view.newName === "string" ? view.newName : "";
|
|
6847
|
+
const err = typeof view.err === "string" && view.err ? view.err : null;
|
|
6848
|
+
const notice = typeof view.notice === "string" && view.notice ? view.notice : null;
|
|
6849
|
+
const busy = view.busy === true;
|
|
6850
|
+
const add = tidyList(view.add);
|
|
6851
|
+
const drop = (Array.isArray(view.drop) ? view.drop : []).map(tidyKey);
|
|
6852
|
+
|
|
6853
|
+
const extras = tidyList(settings && settings.reservedExtra);
|
|
6854
|
+
const extraKeys = extras.map(tidyKey);
|
|
6855
|
+
// 内置名 = 生效名单里不属于「用户声明」的那些。**不写死两个名字**:
|
|
6856
|
+
// 宿主改了名字 / 多了一个内置名,这里跟着变。
|
|
6857
|
+
const fixed = tidyList(settings && settings.reserved).filter(function (n) {
|
|
6858
|
+
return extraKeys.indexOf(tidyKey(n)) < 0;
|
|
6859
|
+
});
|
|
6860
|
+
// 当前选择 = 已保存的名单 - 本趟取消的 + 本趟勾上的。
|
|
6861
|
+
const selected = tidyList(extras.filter(function (n) {
|
|
6862
|
+
return drop.indexOf(tidyKey(n)) < 0;
|
|
6863
|
+
}).concat(add));
|
|
6864
|
+
const selectedKey = selected.slice().sort().join(",");
|
|
6865
|
+
const rows = tidyRows(fixed, selected, files);
|
|
6866
|
+
const q = String(query || "").trim().toLowerCase();
|
|
6867
|
+
// 搜索只筛「可勾的那些」:内置名恒定显示 —— 它们不是搜索结果,是这一块的前提。
|
|
6868
|
+
const shown = rows.filter(function (r) {
|
|
6869
|
+
return r.kind === "fixed" || !q || r.name.toLowerCase().indexOf(q) >= 0;
|
|
6870
|
+
});
|
|
6871
|
+
const picked = fixed.length + selected.length;
|
|
6872
|
+
// 与已保存的不一致才提示「未保存」(两个串都由同一把尺子拼出来)。
|
|
6873
|
+
const changed = selectedKey !== extras.slice().sort().join(",");
|
|
6874
|
+
|
|
6875
|
+
/**
|
|
6876
|
+
* 落盘一份名单(chips 的立即写 与 保存按钮 共用)。
|
|
6877
|
+
*
|
|
6878
|
+
* `setSettings` 回的是 `{ ok, settings, restartRequired }` —— **不要过 unwrap**
|
|
6879
|
+
* (unwrap 只认 `{ok, value}`,会把 settings 整个吃掉)。回执里带回来的名单
|
|
6880
|
+
* 要跟发出去的对一遍:对不上就不许说「已保存」。
|
|
6881
|
+
*/
|
|
6882
|
+
function put(next, okMsg) {
|
|
6883
|
+
if (!canWrite) { patch({ err: t("ad.tidyScope.noSettings") }); return; }
|
|
6884
|
+
if (busy) return;
|
|
6885
|
+
const list = tidyList(next);
|
|
6886
|
+
patch({ busy: true, err: null, notice: null });
|
|
6887
|
+
Promise.resolve(memRemote.setSettings({ reserved: list })).then(function (out) {
|
|
6888
|
+
if (out && out.ok === false) {
|
|
6889
|
+
patch({ busy: false, err: out.error || t("ad.err.reservedRejected") });
|
|
6890
|
+
return;
|
|
6891
|
+
}
|
|
6892
|
+
const s = out && out.settings ? out.settings : null;
|
|
6893
|
+
if (typeof props.onSettings === "function") props.onSettings(s);
|
|
6894
|
+
const back = s ? tidyList(s.reservedExtra).slice().sort().join(",") : list.slice().sort().join(",");
|
|
6895
|
+
if (s && back !== list.slice().sort().join(",")) {
|
|
6896
|
+
// 宿主说了算:回执里的名单不是刚发的那份,就不谎报「已保存」。
|
|
6897
|
+
patch({ busy: false, err: t("ad.tidyScope.notApplied"), add: [], drop: [] });
|
|
6898
|
+
return;
|
|
6899
|
+
}
|
|
6900
|
+
patch({
|
|
6901
|
+
busy: false,
|
|
6902
|
+
err: null,
|
|
6903
|
+
// 落盘成功 → 增量清零(选择重新以宿主那份为基准)
|
|
6904
|
+
add: [],
|
|
6905
|
+
drop: [],
|
|
6906
|
+
notice: out && out.restartRequired === false ? t("ad.tidyScope.savedNow") : (okMsg || t("ad.tidyScope.saved")),
|
|
6907
|
+
});
|
|
6908
|
+
}, function (e) {
|
|
6909
|
+
patch({ busy: false, err: String((e && e.message) || e) });
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
/** 勾 / 取消一个名字:只改增量,落盘交给「保存」(一次能改好几个)。 */
|
|
6914
|
+
function toggle(name, on) {
|
|
6915
|
+
const k = tidyKey(name);
|
|
6916
|
+
if (on) patch({ add: tidyList(add.concat([name])), drop: drop.filter(function (x) { return x !== k; }) });
|
|
6917
|
+
else patch({ add: add.filter(function (x) { return tidyKey(x) !== k; }), drop: drop.concat([k]) });
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
/** chips 上的 ✕:旧入口,点一下立刻落盘(沿用 2.1.0 起的行为)。 */
|
|
6921
|
+
function removeName(name) {
|
|
6922
|
+
const k = tidyKey(name);
|
|
6923
|
+
put(selected.filter(function (x) { return tidyKey(x) !== k; }), t("ad.notice.reservedSaved"));
|
|
6924
|
+
}
|
|
6925
|
+
|
|
6926
|
+
/** 「加入名单」:与宿主同一套校验(单个 .md 文件名、不许带路径、不许重复)。 */
|
|
6927
|
+
function addName() {
|
|
6928
|
+
const name = String(newName || "").trim();
|
|
6929
|
+
if (!name) { patch({ err: t("ad.err.reservedNeedName") }); return; }
|
|
6930
|
+
if (!name.endsWith(".md") || name.includes("/") || name.includes("\\") || name.startsWith("..")) {
|
|
6931
|
+
patch({ err: t("ad.err.reservedBadName", { name: name }) });
|
|
6932
|
+
return;
|
|
6933
|
+
}
|
|
6934
|
+
if (selected.some(function (x) { return tidyKey(x) === tidyKey(name); })) {
|
|
6935
|
+
patch({ err: t("ad.err.reservedDup", { name: name }) });
|
|
6936
|
+
return;
|
|
6937
|
+
}
|
|
6938
|
+
patch({ newName: "" });
|
|
6939
|
+
put(selected.concat([name]), t("ad.notice.reservedSaved"));
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
/** 一行元信息:内置说「不可取消」,文件说体积与类型,已声明但没文件就直说。 */
|
|
6943
|
+
function metaText(r) {
|
|
6944
|
+
if (r.kind === "fixed") return t("ad.tidyScope.builtin");
|
|
6945
|
+
if (!r.meta) return t("ad.tidyScope.missing");
|
|
6946
|
+
const size = typeof r.meta.size === "number" ? fmtSize(r.meta.size) : "";
|
|
6947
|
+
const kind = r.meta.type ? typeLabel(r.meta.type) : "";
|
|
6948
|
+
const base = [size, kind].filter(Boolean).join(" · ");
|
|
6949
|
+
return r.meta.locked ? base + " · " + t("ad.tidyScope.locked") : base;
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6952
|
+
const isOn = function (name) {
|
|
6953
|
+
const k = tidyKey(name);
|
|
6954
|
+
return fixed.some(function (x) { return tidyKey(x) === k; }) ||
|
|
6955
|
+
selected.some(function (x) { return tidyKey(x) === k; });
|
|
6956
|
+
};
|
|
6957
|
+
|
|
6958
|
+
const rowEl = function (r) {
|
|
6959
|
+
const on = isOn(r.name);
|
|
6960
|
+
const locked = r.kind === "fixed";
|
|
6961
|
+
// 行用现成的 `.smem-check`(复选框 + 手型 + accent-color 都已经在库里),
|
|
6962
|
+
// 只把「内置不可取消」的视觉差别用内联样式补上 —— 不为这一块再长一段 CSS。
|
|
6963
|
+
return h("label", {
|
|
6964
|
+
className: "smem-check smem-tidy-row",
|
|
6965
|
+
key: "row-" + r.name,
|
|
6966
|
+
"data-on": on ? "1" : "0",
|
|
6967
|
+
style: locked
|
|
6968
|
+
? { opacity: 0.72, cursor: "default" }
|
|
6969
|
+
: (canWrite ? null : { cursor: "default" }),
|
|
6970
|
+
}, [
|
|
6971
|
+
h("input", {
|
|
6972
|
+
type: "checkbox", key: "c", className: "smem-tidy-check",
|
|
6973
|
+
checked: on,
|
|
6974
|
+
disabled: locked || busy || !canWrite,
|
|
6975
|
+
title: locked ? t("ad.tidyScope.builtin") : "",
|
|
6976
|
+
onChange: function (e) { toggle(r.name, !!(e && e.target && e.target.checked)); },
|
|
6977
|
+
}),
|
|
6978
|
+
h("span", { key: "n", style: { minWidth: "150px" } }, r.name),
|
|
6979
|
+
h("span", { className: "smem-status", key: "m" }, metaText(r)),
|
|
6980
|
+
]);
|
|
6981
|
+
};
|
|
6982
|
+
|
|
6983
|
+
return h("div", { className: "smem-tidy-scope", key: "tidy" }, [
|
|
6984
|
+
// ── 已选 chips(内置固定 + 当前选择可删)──
|
|
6985
|
+
h("div", { className: "smem-raw-names", key: "fixed" }, fixed.map(function (n) {
|
|
6986
|
+
return h("span", { className: "smem-chip smem-chip--fixed", key: n, title: t("ad.reserved.fixedTitle") }, n + t("ad.reserved.fixedSuffix"));
|
|
6987
|
+
})),
|
|
6988
|
+
h("div", { className: "smem-raw-names", key: "extra" },
|
|
6989
|
+
selected.length
|
|
6990
|
+
? selected.map(function (n) {
|
|
6991
|
+
return h("span", { className: "smem-chip", key: n, "data-on": "1" }, [
|
|
6992
|
+
n,
|
|
6993
|
+
h("button", {
|
|
6994
|
+
type: "button", className: "smem-chip-x", key: "x",
|
|
6995
|
+
title: t("ad.reserved.removeTitle", { name: n }),
|
|
6996
|
+
disabled: busy || !canWrite,
|
|
6997
|
+
onClick: function () { removeName(n); },
|
|
6998
|
+
}, "✕"),
|
|
6999
|
+
]);
|
|
7000
|
+
})
|
|
7001
|
+
: h("span", { className: "smem-status", key: "none" }, t("ad.reserved.empty"))),
|
|
7002
|
+
// 旧入口保留在 chips 上:手输一个还没落到目录里的名字仍然有用(先保护、后建文件)。
|
|
7003
|
+
h("div", { className: "smem-autodream-row", key: "add" }, [
|
|
7004
|
+
h("input", {
|
|
7005
|
+
className: "smem-input", key: "i",
|
|
7006
|
+
style: { flex: "1", minWidth: "150px" },
|
|
7007
|
+
value: newName,
|
|
7008
|
+
disabled: busy || !canWrite,
|
|
7009
|
+
placeholder: t("ad.reserved.placeholder"),
|
|
7010
|
+
onChange: function (e) { patch({ newName: (e && e.target && e.target.value) || "" }); },
|
|
7011
|
+
}),
|
|
7012
|
+
h("button", {
|
|
7013
|
+
type: "button", className: "smem-btn", key: "a",
|
|
7014
|
+
disabled: busy || !canWrite, onClick: addName,
|
|
7015
|
+
}, t("ad.action.addToList")),
|
|
7016
|
+
]),
|
|
7017
|
+
|
|
7018
|
+
// ── 勾选面板:从目录里现成的文件名里勾 ──
|
|
7019
|
+
h("div", { className: "smem-autodream-row", key: "head" }, [
|
|
7020
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, t("ad.tidyScope.title")),
|
|
7021
|
+
h("span", { className: "smem-status", key: "c" }, t("ad.tidyScope.count", { on: String(picked), total: String(rows.length) })),
|
|
7022
|
+
]),
|
|
7023
|
+
h("div", { className: "smem-autodream-note", key: "n1" }, t("ad.tidyScope.note1")),
|
|
7024
|
+
h("div", { className: "smem-autodream-note", key: "n2" }, t("ad.tidyScope.note2")),
|
|
7025
|
+
!canRead
|
|
7026
|
+
? h("div", { className: "smem-autodream-note smem-ad-warn", key: "nosettings" }, t("ad.tidyScope.noSettings"))
|
|
7027
|
+
: null,
|
|
7028
|
+
canRead && !settings
|
|
7029
|
+
? h("div", { className: "smem-status", key: "loading" }, t("ad.loading"))
|
|
7030
|
+
: null,
|
|
7031
|
+
canRead
|
|
7032
|
+
? h("div", { className: "smem-autodream-row", key: "search" }, [
|
|
7033
|
+
h("input", {
|
|
7034
|
+
className: "smem-search", key: "q", type: "search",
|
|
7035
|
+
value: query,
|
|
7036
|
+
placeholder: t("ad.tidyScope.search"),
|
|
7037
|
+
onChange: function (e) { patch({ query: (e && e.target && e.target.value) || "" }); },
|
|
7038
|
+
}),
|
|
7039
|
+
])
|
|
7040
|
+
: null,
|
|
7041
|
+
canRead
|
|
7042
|
+
? h("div", {
|
|
7043
|
+
key: "list",
|
|
7044
|
+
// 滚动的是这一块而不是整页:名单可能几十条,把保存按钮推下去就找不到了。
|
|
7045
|
+
style: {
|
|
7046
|
+
maxHeight: "220px", overflowY: "auto",
|
|
7047
|
+
border: "1px solid var(--dsw-alias-border-l1)", borderRadius: "8px",
|
|
7048
|
+
padding: "6px 10px",
|
|
7049
|
+
},
|
|
7050
|
+
className: "smem-autodream-list",
|
|
7051
|
+
}, shown.length
|
|
7052
|
+
? shown.map(rowEl)
|
|
7053
|
+
: [h("div", { className: "smem-status", key: "nomatch" }, t("ad.tidyScope.noMatch"))])
|
|
7054
|
+
: null,
|
|
7055
|
+
// ── 保存 ──
|
|
7056
|
+
h("div", { className: "smem-autodream-row", key: "save" }, [
|
|
7057
|
+
h("button", {
|
|
7058
|
+
type: "button", className: "smem-btn smem-btn-primary", key: "s",
|
|
7059
|
+
disabled: busy || !canWrite, onClick: function () { put(selected, t("ad.tidyScope.saved")); },
|
|
7060
|
+
}, busy ? t("ad.tidyScope.saving") : t("ad.tidyScope.save")),
|
|
7061
|
+
h("span", { className: "smem-status", key: "h" },
|
|
7062
|
+
changed ? t("ad.tidyScope.dirty") : t("ad.tidyScope.restartHint")),
|
|
7063
|
+
]),
|
|
7064
|
+
// 回执与错误留在这块的最下面:不管按的是 chips 还是保存按钮,结果都在同一处。
|
|
7065
|
+
err ? h("div", { className: "smem-err", key: "err" }, "⚠ " + err) : null,
|
|
7066
|
+
notice ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
7067
|
+
]);
|
|
7068
|
+
}
|
|
6734
7069
|
// ── 文件列表这一屏(1.5.3 从 Section 里抽出;D23 计划的 72 片)──────────
|
|
6735
7070
|
// 只负责「按状态/筛选把卡片摆出来 + 四种空态」;状态、筛选语义、动作回调全部由 Section 传。
|
|
6736
7071
|
// 抽出的判据:它是 Section 里最大的一块纯渲染,而卡片组件(Card / ArchivedCard)本来就在 50 共享层。
|
package/lib/memory/scan.js
CHANGED
|
@@ -581,7 +581,12 @@ async function readStarDir(dir, ctx, opts = {}) {
|
|
|
581
581
|
return []
|
|
582
582
|
}
|
|
583
583
|
const names = entries
|
|
584
|
-
|
|
584
|
+
// 2.10.1:跳过点开头的名字。归档区里会出现 `.retired-<stamp>-<hex>.md` 这类
|
|
585
|
+
// **退役件**(裁段归档成功、但改主文件失败时,归档件被移动过去保命)。
|
|
586
|
+
// 它们不是归档条目、也不该进星图:一次失败清理若被列成「一条归档记忆」,
|
|
587
|
+
// 人会以为那是历史资产而不是事故现场。整文件归档用的 `.archiving-*`(不带 .md)
|
|
588
|
+
// 本来就被 isMemoryEntry 挡掉,这里补的是后缀带 .md 的那一类。
|
|
589
|
+
.filter(e => e.isFile() && !e.name.startsWith('.') && isMemoryEntry(e.name))
|
|
585
590
|
.map(e => e.name)
|
|
586
591
|
const reservedIds = await reservedIdentitySet()
|
|
587
592
|
const loaded = await Promise.all(names.map(async (name) => {
|