sage-mem 0.8.0 → 0.9.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 +28 -0
- package/lib/autodream/config.js +13 -0
- package/lib/autodream/engine.js +78 -1
- package/lib/autodream/report.js +29 -0
- package/lib/autodream/settings.js +2 -0
- package/lib/autodream-audit.js +46 -5
- package/lib/autodream-tools.js +10 -11
- package/lib/client.js +1521 -100
- package/lib/index.js +436 -11
- package/lib/memory/access.js +220 -0
- package/lib/memory/archive.js +91 -0
- package/lib/memory/scan.js +130 -7
- package/lib/typert.host.js +287 -1
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -205,7 +205,12 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
205
205
|
return { W: W, H: H, pts: pts };
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
/**
|
|
209
|
+
* 一颗星此刻画不画:归档开关 → 类型开关 → 搜索词,三层依次收窄。
|
|
210
|
+
* 归档开关放最前面:关掉「显示档案馆」时,归档星连图例计数都不该占到。
|
|
211
|
+
*/
|
|
212
|
+
function starVisible(p, kindsOn, q, showArchived) {
|
|
213
|
+
if (p.star.archived === true && showArchived === false) return false;
|
|
209
214
|
if (!kindsOn[p.star.kind]) return false;
|
|
210
215
|
if (q) {
|
|
211
216
|
const hay = (p.star.title + " " + p.star.desc + " " + p.star.file).toLowerCase();
|
|
@@ -215,9 +220,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
/** 可见性标记 + 同类最近邻星座连线。 */
|
|
218
|
-
function buildVisibility(lay, kindsOn, q) {
|
|
223
|
+
function buildVisibility(lay, kindsOn, q, showArchived) {
|
|
219
224
|
const flags = [];
|
|
220
|
-
for (let i = 0; i < lay.pts.length; i++) flags.push(starVisible(lay.pts[i], kindsOn, q));
|
|
225
|
+
for (let i = 0; i < lay.pts.length; i++) flags.push(starVisible(lay.pts[i], kindsOn, q, showArchived));
|
|
221
226
|
const byKind = {};
|
|
222
227
|
for (let i = 0; i < lay.pts.length; i++) {
|
|
223
228
|
if (!flags[i]) continue;
|
|
@@ -389,6 +394,12 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
389
394
|
".smap-search::placeholder{color:rgba(219,228,245,.35);}",
|
|
390
395
|
".smap-btn{background:rgba(255,255,255,.07);border:1px solid rgba(150,170,230,.25);border-radius:8px;color:#dbe4f5;font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
|
|
391
396
|
".smap-btn:hover{background:rgba(255,255,255,.13);}",
|
|
397
|
+
// 开态(档案馆开关)与主按钮(恢复):沿用同一套夜色调,不新增色板。
|
|
398
|
+
// 「显示档案馆」的开/关要一眼看出来:开 = 亮边 + 蓝调底 + 亮字,关 = 整体压暗。
|
|
399
|
+
".smap-btn.on{border-color:rgba(150,170,230,.78);background:rgba(122,158,255,.20);color:#eef3ff;}",
|
|
400
|
+
'.smap-ar-toggle[aria-pressed="false"]{opacity:.60;}',
|
|
401
|
+
".smap-btn:disabled{opacity:.5;cursor:default;}",
|
|
402
|
+
".smap-btn-primary{border-color:rgba(150,170,230,.55);background:rgba(122,158,255,.20);}",
|
|
392
403
|
".smap-wrap{position:relative;flex:1;min-height:0;}",
|
|
393
404
|
".smap-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;border-radius:10px;cursor:crosshair;}",
|
|
394
405
|
".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:rgba(10,14,30,.95);border:1px solid rgba(150,170,230,.35);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.45);}",
|
|
@@ -399,6 +410,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
399
410
|
".smap-panel-head{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px;}",
|
|
400
411
|
".smap-panel-title{font-weight:600;font-size:14px;flex:1;}",
|
|
401
412
|
".smap-panel-meta{font-size:11px;opacity:.6;margin-bottom:8px;}",
|
|
413
|
+
".smap-panel-arch{display:flex;flex-direction:column;gap:6px;border:1px solid rgba(150,170,230,.28);border-radius:8px;padding:8px 10px;background:rgba(255,255,255,.03);}",
|
|
402
414
|
".smap-meta-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:-1px;}",
|
|
403
415
|
".smap-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:12px;align-items:center;}",
|
|
404
416
|
".smap-lg-item{cursor:pointer;user-select:none;}",
|
|
@@ -426,6 +438,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
426
438
|
".smap-timeline{position:absolute;inset:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:4px 6px 4px 2px;}",
|
|
427
439
|
".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:rgba(255,255,255,.035);border:1px solid rgba(150,170,230,.16);border-radius:8px;padding:7px 10px;cursor:pointer;flex:none;}",
|
|
428
440
|
".smap-tl-item:hover{background:rgba(255,255,255,.08);border-color:rgba(150,170,230,.35);}",
|
|
441
|
+
".smap-tl-item--arch{opacity:.75;}",
|
|
442
|
+
".smap-ar-badge{font-size:10.5px;opacity:.65;border:1px solid rgba(150,170,230,.3);border-radius:5px;padding:0 5px;flex:none;align-self:center;}",
|
|
429
443
|
".smap-tl-date{font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;flex:none;}",
|
|
430
444
|
".smap-tl-title{font-size:13px;font-weight:600;flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
431
445
|
".smap-tl-desc{font-size:12px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}",
|
|
@@ -448,6 +462,22 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
448
462
|
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
|
449
463
|
}
|
|
450
464
|
|
|
465
|
+
// 归档星(暗星)的用色:把类型色往石板灰上压 —— 它得「还在图上、一眼数得出来」,
|
|
466
|
+
// 又要一眼看出不是活记忆。亮度靠 shader 那一档(0.52),这里负责色相:冷、灰、不饱和。
|
|
467
|
+
const ARCHIVE_GREY = [0.46, 0.50, 0.58];
|
|
468
|
+
function dimRgb(hex) {
|
|
469
|
+
const c = hexToRgb(hex);
|
|
470
|
+
return [
|
|
471
|
+
c[0] * 0.26 + ARCHIVE_GREY[0] * 0.74,
|
|
472
|
+
c[1] * 0.26 + ARCHIVE_GREY[1] * 0.74,
|
|
473
|
+
c[2] * 0.26 + ARCHIVE_GREY[2] * 0.74,
|
|
474
|
+
];
|
|
475
|
+
}
|
|
476
|
+
function dimCss(hex) {
|
|
477
|
+
const c = dimRgb(hex);
|
|
478
|
+
return "rgb(" + Math.round(c[0] * 255) + "," + Math.round(c[1] * 255) + "," + Math.round(c[2] * 255) + ")";
|
|
479
|
+
}
|
|
480
|
+
|
|
451
481
|
const SMAP_BG_VS = "attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }";
|
|
452
482
|
// 底色(重做过一版)。旧版是四层 fbm 噪声算出来的蓝紫星云,还带 uTime 动画 ——
|
|
453
483
|
// 「会呼吸的彩色底」正是廉价感的一大来源,而且每帧都要重算噪声。
|
|
@@ -509,11 +539,15 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
509
539
|
" if (vMeta1.z < -0.5){",
|
|
510
540
|
" alpha = smoothstep(0.55, 0.0, d) * vColor.a;",
|
|
511
541
|
" } else {",
|
|
512
|
-
"
|
|
542
|
+
" // 归档星(vMeta1.y = -1)单独一档 0.52:核心小而暗;外圈的存在感由另画的一层",
|
|
543
|
+
" // 淡底盘负责(见 uploadStars),所以这里不用改辉光形状。",
|
|
544
|
+
" float aFresh = vMeta1.y < -0.5 ? 0.52 : mix(0.60, 1.0, vMeta1.y);",
|
|
545
|
+
" float hov = abs(vMeta1.z - uHoverIdx) < 0.5 ? 1.0 : 0.0;",
|
|
546
|
+
" if (hov > 0.5 && vMeta1.y < -0.5) aFresh = 1.0;", // 悬停把暗星临时点亮
|
|
513
547
|
" float core = smoothstep(0.30, 0.04, d);",
|
|
514
548
|
" float glow = exp(-d * 3.8) * 0.30;",
|
|
515
549
|
" alpha = (core + glow) * aFresh;",
|
|
516
|
-
" if (
|
|
550
|
+
" if (hov > 0.5){",
|
|
517
551
|
" alpha += smoothstep(0.10, 0.0, abs(d - 0.62)) * 0.55;",
|
|
518
552
|
" }",
|
|
519
553
|
" alpha = clamp(alpha, 0.0, 1.0);",
|
|
@@ -648,9 +682,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
648
682
|
if (!visBox.flags[i]) continue;
|
|
649
683
|
const p = layout.pts[i];
|
|
650
684
|
const meta = KIND_META[p.star.kind] || KIND_META.special;
|
|
685
|
+
const arch = p.star.archived === true;
|
|
651
686
|
const fresh = p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS ? 1 : 0;
|
|
652
687
|
const birth = p.star.mtimeMs > 0 ? p.star.mtimeMs : 0;
|
|
653
|
-
|
|
688
|
+
if (arch) {
|
|
689
|
+
// 归档星 = 暗星,两层画:
|
|
690
|
+
// ① 一层很淡的冷色底盘(走 dust 分支:不带星芒、不参与 hover),
|
|
691
|
+
// 负责「深空底上一眼数得出来」——只画一个小点是数不出来的;
|
|
692
|
+
// ② 一颗小核心(走普通分支,fresh 传 -1 = 归档档 0.52),负责
|
|
693
|
+
// 「看得出是颗星」,并且 hover 时整颗点亮 + 出环。
|
|
694
|
+
pushStarQuad(arr, p.x, p.y, p.r * 6.0, dimRgb(meta.color), 0.26, p.tw, 0, -1, birth);
|
|
695
|
+
pushStarQuad(arr, p.x, p.y, p.r * 4.4, dimRgb(meta.color), 1, p.tw, -1, i, birth);
|
|
696
|
+
} else {
|
|
697
|
+
pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(meta.color), 1, p.tw, fresh, i, birth);
|
|
698
|
+
}
|
|
654
699
|
}
|
|
655
700
|
gl.bindBuffer(gl.ARRAY_BUFFER, starBuf);
|
|
656
701
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.DYNAMIC_DRAW);
|
|
@@ -800,6 +845,35 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
800
845
|
if (rv <= 0) continue;
|
|
801
846
|
}
|
|
802
847
|
const tw = 0.72 + 0.28 * Math.sin(phase * 2.1 + p.tw * Math.PI * 2);
|
|
848
|
+
if (p.star.archived === true) {
|
|
849
|
+
// 暗星(canvas2d 兜底路径):与 webgl 同一档 —— 核心小而暗、外面一圈很淡的冷辉光。
|
|
850
|
+
// 有辉光才「数得出来」;hover 时整颗点亮 + 描边环,这是它能被发现的关键。
|
|
851
|
+
const hot = hoverBox.idx === i;
|
|
852
|
+
const dc = dimRgb(meta.color);
|
|
853
|
+
const rgba = (a) => "rgba(" + Math.round(dc[0] * 255) + "," + Math.round(dc[1] * 255) + "," + Math.round(dc[2] * 255) + "," + a.toFixed(3) + ")";
|
|
854
|
+
const haloR = p.r * 3.4;
|
|
855
|
+
const hg = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, haloR);
|
|
856
|
+
hg.addColorStop(0, rgba((hot ? 0.50 : 0.26) * rv));
|
|
857
|
+
hg.addColorStop(1, rgba(0));
|
|
858
|
+
c2d.fillStyle = hg;
|
|
859
|
+
c2d.beginPath();
|
|
860
|
+
c2d.arc(p.x, p.y, haloR, 0, Math.PI * 2);
|
|
861
|
+
c2d.fill();
|
|
862
|
+
c2d.globalAlpha = (hot ? 0.95 : 0.52) * rv;
|
|
863
|
+
c2d.fillStyle = dimCss(meta.color);
|
|
864
|
+
c2d.beginPath();
|
|
865
|
+
c2d.arc(p.x, p.y, Math.max(1.2, p.r * 0.7), 0, Math.PI * 2);
|
|
866
|
+
c2d.fill();
|
|
867
|
+
c2d.globalAlpha = 1;
|
|
868
|
+
if (hot) {
|
|
869
|
+
c2d.strokeStyle = dimCss(meta.color);
|
|
870
|
+
c2d.lineWidth = 1;
|
|
871
|
+
c2d.beginPath();
|
|
872
|
+
c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
|
|
873
|
+
c2d.stroke();
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
803
877
|
if (p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS) {
|
|
804
878
|
const pr = p.r + 5 + 3 * (0.5 + 0.5 * Math.sin(phase * 1.4 + p.tw * 6.28));
|
|
805
879
|
c2d.strokeStyle = hexA(meta.color, 0.4 * rv);
|
|
@@ -890,6 +964,14 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
890
964
|
const replayDateS = react.useState(null);
|
|
891
965
|
const replayDate = replayDateS[0];
|
|
892
966
|
const setReplayDate = replayDateS[1];
|
|
967
|
+
// 档案馆开关:默认**开** —— 关着的默认值等于用户不知道图上有归档星。
|
|
968
|
+
// 关掉只改本地可见性,不重新取数(数据一次拿全,过滤在客户端做)。
|
|
969
|
+
const showArchS = react.useState(true);
|
|
970
|
+
const showArchive = showArchS[0];
|
|
971
|
+
const setShowArchive = showArchS[1];
|
|
972
|
+
const restoringS = react.useState(null);
|
|
973
|
+
const restoring = restoringS[0];
|
|
974
|
+
const setRestoring = restoringS[1];
|
|
893
975
|
|
|
894
976
|
const hoverBox = react.useRef({ idx: -1 }).current;
|
|
895
977
|
const visBox = react.useRef({ flags: [], lines: [] }).current;
|
|
@@ -921,7 +1003,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
921
1003
|
setBusy(false);
|
|
922
1004
|
setErr("拉取超时:starmap 服务 20 秒无响应——Host 端服务大概率未激活,需要重启 DSH 让插件行重新装载");
|
|
923
1005
|
}, 20000);
|
|
924
|
-
remote.listStars().then((r) => {
|
|
1006
|
+
remote.listStars(true).then((r) => {
|
|
925
1007
|
if (!alive || settled) return;
|
|
926
1008
|
settled = true;
|
|
927
1009
|
clearTimeout(guard);
|
|
@@ -1000,7 +1082,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1000
1082
|
react.useEffect(() => {
|
|
1001
1083
|
if (!data || !layout || !kindsOn) return;
|
|
1002
1084
|
const q = query.trim().toLowerCase();
|
|
1003
|
-
const vis = buildVisibility(layout, kindsOn, q);
|
|
1085
|
+
const vis = buildVisibility(layout, kindsOn, q, showArchive);
|
|
1004
1086
|
visBox.flags = vis.flags;
|
|
1005
1087
|
visBox.lines = vis.lines;
|
|
1006
1088
|
let anyVis = false;
|
|
@@ -1012,7 +1094,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1012
1094
|
if (drawBox.markDirty) drawBox.markDirty();
|
|
1013
1095
|
else drawBox.renderer.render((Date.now() - drawBox.t0) / 1000);
|
|
1014
1096
|
}
|
|
1015
|
-
}, [data, layout, kindsOn, query]);
|
|
1097
|
+
}, [data, layout, kindsOn, query, showArchive]);
|
|
1016
1098
|
|
|
1017
1099
|
function hitTest(e) {
|
|
1018
1100
|
if (!layout) return -1;
|
|
@@ -1021,14 +1103,17 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1021
1103
|
const sx = (e.clientX - rect.left) * (layout.W / rect.width);
|
|
1022
1104
|
const sy = (e.clientY - rect.top) * (layout.H / rect.height);
|
|
1023
1105
|
let best = -1;
|
|
1024
|
-
let bestD =
|
|
1106
|
+
let bestD = Infinity;
|
|
1025
1107
|
for (let i = 0; i < layout.pts.length; i++) {
|
|
1026
1108
|
if (!visBox.flags[i]) continue;
|
|
1027
1109
|
const p = layout.pts[i];
|
|
1028
1110
|
const dx = p.x - sx;
|
|
1029
1111
|
const dy = p.y - sy;
|
|
1030
1112
|
const d = dx * dx + dy * dy;
|
|
1031
|
-
|
|
1113
|
+
// 每颗星有自己的热区:暗星画得小,热区反而要更大(16px),否则
|
|
1114
|
+
// 就成了「看得见但点不中」。活星仍是 12px。
|
|
1115
|
+
const lim = p.star.archived === true ? 256 : 144;
|
|
1116
|
+
if (d < lim && d < bestD) { bestD = d; best = i; }
|
|
1032
1117
|
}
|
|
1033
1118
|
return best;
|
|
1034
1119
|
}
|
|
@@ -1060,8 +1145,14 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1060
1145
|
setHover(null);
|
|
1061
1146
|
}
|
|
1062
1147
|
|
|
1148
|
+
/**
|
|
1149
|
+
* 打开详情。归档星不在这里展开正文:starmap.readFile 只读记忆根目录,
|
|
1150
|
+
* 归档那份在 archive/ 下取不到 —— 与其弹一句「读取失败」,不如面板直接
|
|
1151
|
+
* 给归档信息 + 「恢复」;恢复之后远程自然读得到它。
|
|
1152
|
+
*/
|
|
1063
1153
|
function openStar(st) {
|
|
1064
1154
|
setSelected({ star: st, text: null });
|
|
1155
|
+
if (st.archived === true) return;
|
|
1065
1156
|
const remote = ctx.get("remote.starmap");
|
|
1066
1157
|
if (!remote) return;
|
|
1067
1158
|
remote.readFile(st.file).then((r) => {
|
|
@@ -1077,6 +1168,26 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1077
1168
|
});
|
|
1078
1169
|
}
|
|
1079
1170
|
|
|
1171
|
+
/** 从星图直接恢复一颗归档星:成功后重新取数,星回到活星区。 */
|
|
1172
|
+
function restoreStar(st) {
|
|
1173
|
+
const remote = ctx.get("remote.memory");
|
|
1174
|
+
if (!remote || typeof remote.restore !== "function") {
|
|
1175
|
+
setErr("宿主侧没有 restore 接口(插件版本较旧)");
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
setErr("");
|
|
1179
|
+
setRestoring(st.file);
|
|
1180
|
+
remote.restore(st.file).then((r) => {
|
|
1181
|
+
setRestoring(null);
|
|
1182
|
+
if (r && r.ok === false) { setErr(r.error || "恢复被拒绝"); return; }
|
|
1183
|
+
setSelected(null);
|
|
1184
|
+
refresh();
|
|
1185
|
+
}, (e) => {
|
|
1186
|
+
setRestoring(null);
|
|
1187
|
+
setErr(detail(e));
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1080
1191
|
function onClick(e) {
|
|
1081
1192
|
const best = hitTest(e);
|
|
1082
1193
|
if (best < 0 || !layout) return;
|
|
@@ -1091,7 +1202,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1091
1202
|
setSelected(null);
|
|
1092
1203
|
setErr("");
|
|
1093
1204
|
setBusy(true);
|
|
1094
|
-
|
|
1205
|
+
// 连归档星一起取:暗星是背景,不该为它再跑一次请求。
|
|
1206
|
+
remote.listStars(true).then((r) => {
|
|
1095
1207
|
setBusy(false);
|
|
1096
1208
|
const d = unwrap(r);
|
|
1097
1209
|
if (d && Array.isArray(d.stars)) setData(d);
|
|
@@ -1152,15 +1264,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1152
1264
|
replayBox.raf = window.requestAnimationFrame(tick);
|
|
1153
1265
|
}
|
|
1154
1266
|
|
|
1267
|
+
// 归档星数(数据一次取全,含归档):头部开关与图例计数都要用。
|
|
1268
|
+
const archivedN = data ? data.stars.filter((s) => s.archived === true).length : 0;
|
|
1269
|
+
|
|
1155
1270
|
let body;
|
|
1156
1271
|
if (err) {
|
|
1157
1272
|
body = h("div", { className: "smap-status" }, "点亮失败:" + err);
|
|
1158
1273
|
} else if (!data || !layout || !kindsOn) {
|
|
1159
1274
|
body = h("div", { className: "smap-status" }, busy ? "正在点亮星空…" : "整理星图…");
|
|
1160
1275
|
} else {
|
|
1276
|
+
// 「显示档案馆」关掉时,图例计数也只算活星 —— 屏幕上没有的东西不该占数字。
|
|
1277
|
+
const viewStars = showArchive ? data.stars : data.stars.filter((s) => s.archived !== true);
|
|
1161
1278
|
const legendKids = [];
|
|
1162
1279
|
for (const k of KIND_ORDER) {
|
|
1163
|
-
const n =
|
|
1280
|
+
const n = viewStars.filter((s) => s.kind === k).length;
|
|
1164
1281
|
if (n === 0) continue;
|
|
1165
1282
|
legendKids.push(h("span", {
|
|
1166
1283
|
key: k,
|
|
@@ -1173,12 +1290,14 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1173
1290
|
}
|
|
1174
1291
|
let totalBytes = 0;
|
|
1175
1292
|
let newest = 0;
|
|
1176
|
-
for (const s of
|
|
1293
|
+
for (const s of viewStars) {
|
|
1177
1294
|
totalBytes += s.bytes || 0;
|
|
1178
1295
|
if (s.mtimeMs > newest) newest = s.mtimeMs;
|
|
1179
1296
|
}
|
|
1180
1297
|
legendKids.push(h("span", { key: "count", className: "smap-count" },
|
|
1181
|
-
|
|
1298
|
+
viewStars.length + " 条 · " + fmtBytes(totalBytes) + (newest ? " · 最近更新 " + fmtDate(newest) : "") +
|
|
1299
|
+
(showArchive && archivedN ? " · 档案馆 " + archivedN + " 颗暗星" : "") +
|
|
1300
|
+
" · 单击看" + (showArchive && archivedN ? "详情" : "全文")));
|
|
1182
1301
|
|
|
1183
1302
|
const tip = hover ? (() => {
|
|
1184
1303
|
const meta = KIND_META[hover.star.kind] || KIND_META.special;
|
|
@@ -1187,6 +1306,12 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1187
1306
|
h("div", { key: "k", className: "smap-tip-kind" },
|
|
1188
1307
|
meta.label + " · " + hover.star.file + " · " + fmtBytes(hover.star.bytes)),
|
|
1189
1308
|
];
|
|
1309
|
+
if (hover.star.archived === true) {
|
|
1310
|
+
// 悬浮也要说清它是档案馆里的:暗星外表已经不同,别让人猜。
|
|
1311
|
+
kids.push(h("div", { key: "arch", className: "smap-tip-kind" },
|
|
1312
|
+
"已归档" + (hover.star.archivedAt ? " · " + hover.star.archivedAt : "") +
|
|
1313
|
+
(hover.star.archivedReason ? " · " + hover.star.archivedReason : "")));
|
|
1314
|
+
}
|
|
1190
1315
|
if (hover.star.desc) {
|
|
1191
1316
|
const d = hover.star.desc.length > 90 ? hover.star.desc.slice(0, 90) + "…" : hover.star.desc;
|
|
1192
1317
|
kids.push(h("div", { key: "d", className: "smap-tip-desc" }, d));
|
|
@@ -1204,9 +1329,27 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1204
1329
|
h("div", { key: "meta", className: "smap-panel-meta" },
|
|
1205
1330
|
h("span", { className: "smap-meta-dot", style: { background: meta.color } }),
|
|
1206
1331
|
meta.label + " · " + selected.star.file + " · " + fmtBytes(selected.star.bytes) +
|
|
1207
|
-
|
|
1332
|
+
// 归档星的 mtime 是「当年写下」的时间,跟「什么时候收起来的」不是一回事 ——
|
|
1333
|
+
// 这里只留「已归档」,具体时间在下面的归档块里给。
|
|
1334
|
+
(selected.star.archived === true
|
|
1335
|
+
? " · 已归档"
|
|
1336
|
+
: (selected.star.mtimeMs ? " · " + fmtDate(selected.star.mtimeMs) : ""))),
|
|
1208
1337
|
];
|
|
1209
|
-
if (selected.
|
|
1338
|
+
if (selected.star.archived === true) {
|
|
1339
|
+
// 归档星:不展开正文(远程读不到 archive/),给归档信息 + 一键恢复。
|
|
1340
|
+
// 恢复了就能正常读 —— 面板给出的是一条出路,不是一个死状态。
|
|
1341
|
+
panelKids.push(h("div", { key: "arch", className: "smap-panel-arch" }, [
|
|
1342
|
+
h("div", { key: "at" }, "已归档于 " + (selected.star.archivedAt || "(时间未知)")),
|
|
1343
|
+
h("div", { key: "why" }, "理由:" + (selected.star.archivedReason || "(没写)")),
|
|
1344
|
+
h("div", { key: "hint", className: "smap-panel-meta" },
|
|
1345
|
+
"归档的记忆不参与检索与星图;原文仍在记忆目录的 archive/ 下,恢复后即可正常阅读。"),
|
|
1346
|
+
h("button", {
|
|
1347
|
+
key: "restore", className: "smap-btn smap-btn-primary",
|
|
1348
|
+
disabled: restoring === selected.star.file,
|
|
1349
|
+
onClick: () => restoreStar(selected.star),
|
|
1350
|
+
}, restoring === selected.star.file ? "恢复中…" : "恢复到活动记忆"),
|
|
1351
|
+
]));
|
|
1352
|
+
} else if (selected.text === null) panelKids.push(h("div", { key: "loading", className: "smap-status" }, "展开中…"));
|
|
1210
1353
|
else panelKids.push(h("div", { key: "body" }, renderMarkdown(selected.text)));
|
|
1211
1354
|
panel = h("div", { className: "smap-panel" }, panelKids);
|
|
1212
1355
|
}
|
|
@@ -1215,19 +1358,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1215
1358
|
if (viewMode === "timeline") {
|
|
1216
1359
|
const q2 = query.trim().toLowerCase();
|
|
1217
1360
|
const sorted = data.stars
|
|
1218
|
-
.filter((s) => starVisible({ star: s }, kindsOn, q2))
|
|
1361
|
+
.filter((s) => starVisible({ star: s }, kindsOn, q2, showArchive))
|
|
1219
1362
|
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0) || a.file.localeCompare(b.file));
|
|
1220
1363
|
mainView = h("div", { className: "smap-wrap", key: "wrap" },
|
|
1221
1364
|
h("div", { className: "smap-timeline" },
|
|
1222
1365
|
sorted.length === 0
|
|
1223
1366
|
? h("div", { className: "smap-status" }, "这片天区安静了")
|
|
1224
1367
|
: sorted.map((s) => h("div", {
|
|
1225
|
-
className: "smap-tl-item",
|
|
1368
|
+
className: "smap-tl-item" + (s.archived === true ? " smap-tl-item--arch" : ""),
|
|
1226
1369
|
key: s.file,
|
|
1227
1370
|
onClick: () => openStar(s),
|
|
1228
|
-
title: "展开全文",
|
|
1371
|
+
title: s.archived === true ? "归档案:点开看归档信息并恢复" : "展开全文",
|
|
1229
1372
|
},
|
|
1230
|
-
h("span", { className: "smap-meta-dot", style: { background: (KIND_META[s.kind] || KIND_META.special).color, flex: "none", alignSelf: "center" } }),
|
|
1373
|
+
h("span", { className: "smap-meta-dot", style: { background: s.archived === true ? dimCss((KIND_META[s.kind] || KIND_META.special).color) : (KIND_META[s.kind] || KIND_META.special).color, flex: "none", alignSelf: "center" } }),
|
|
1374
|
+
s.archived === true ? h("span", { className: "smap-ar-badge" }, "已归档") : null,
|
|
1231
1375
|
h("span", { className: "smap-tl-date", title: s.mtimeMs ? fmtDate(s.mtimeMs) : "" }, s.mtimeMs ? fmtRel(s.mtimeMs) : "—"),
|
|
1232
1376
|
h("span", { className: "smap-tl-title" }, s.title),
|
|
1233
1377
|
h("span", { className: "smap-tl-desc" }, s.desc || "")))),
|
|
@@ -1252,13 +1396,24 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1252
1396
|
}
|
|
1253
1397
|
|
|
1254
1398
|
return h("div", { className: "smap-card" },
|
|
1399
|
+
// 卡片里不再重复「记忆星图 / sage-mem memory」这一行 —— 标题已经在面板顶部
|
|
1400
|
+
// 那条共用头部带里(「我在哪」),这里只留控件,少一行噪音。
|
|
1255
1401
|
h("div", { className: "smap-head" },
|
|
1256
|
-
h("span", { className: "smap-title" }, "记忆星图"),
|
|
1257
|
-
h("span", { className: "smap-sub" }, "sage-mem memory · 每颗星是一条记忆"),
|
|
1258
1402
|
h("span", { className: "smap-spacer" }),
|
|
1259
1403
|
h("span", { className: "smap-viewtoggle" },
|
|
1260
1404
|
h("button", { className: "smap-vt-btn" + (viewMode === "sky" ? " on" : ""), onClick: () => setViewMode("sky"), title: "空间视角:星座分布" }, "星空"),
|
|
1261
1405
|
h("button", { className: "smap-vt-btn" + (viewMode === "timeline" ? " on" : ""), onClick: () => setViewMode("timeline"), title: "时间视角:最近更新在前" }, "时间线")),
|
|
1406
|
+
// 档案馆开关:默认开。关掉只是本地不再画暗星,不重新取数。
|
|
1407
|
+
// 只留文字,不挂数字(计数归脚注的「档案馆 N 颗暗星」);开/关靠 on 态
|
|
1408
|
+
// 与 aria-pressed 的样式差,别让人猜现在是开还是关。
|
|
1409
|
+
h("button", {
|
|
1410
|
+
className: "smap-btn smap-ar-toggle" + (showArchive ? " on" : ""),
|
|
1411
|
+
"aria-pressed": showArchive ? "true" : "false",
|
|
1412
|
+
onClick: () => setShowArchive(!showArchive),
|
|
1413
|
+
title: showArchive
|
|
1414
|
+
? "开着:已归档的记忆画成暗星。点一下只留活星(不重新取数)"
|
|
1415
|
+
: "关着:图上只有活星。点一下把已归档的记忆也画成暗星",
|
|
1416
|
+
}, "显示档案馆"),
|
|
1262
1417
|
viewMode === "sky" ? h("button", {
|
|
1263
1418
|
className: "smap-btn" + (replaying ? " smap-replay-on" : ""),
|
|
1264
1419
|
onClick: () => (replaying ? stopReplay() : startReplay()),
|
|
@@ -1366,6 +1521,28 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1366
1521
|
const starmapListSchema = { parse(v) { if (!v || typeof v !== "object") throw new Error("expected object"); if (!Array.isArray(v.stars)) throw new Error("expected stars array"); v.stars.forEach((f) => starmapStarSchema.parse(f)); return v; } };
|
|
1367
1522
|
const starmapReadSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
1368
1523
|
|
|
1524
|
+
// ── 归档区(v0.9.0 档案馆)──
|
|
1525
|
+
// 客户端只声明 remote 面:面板上的「已归档」区由后续 UI 任务接。
|
|
1526
|
+
// 结果 codec 与宿主 manifest 逐字段对齐(ArchiveResult 的 file / error 都可选,
|
|
1527
|
+
// 成功回 file、失败回 error,所以这里只硬校 ok)。
|
|
1528
|
+
const boolSchema = { parse(v) { isBool(v); return v; } };
|
|
1529
|
+
const archiveSchema = parseSchema({ "ok": isBool });
|
|
1530
|
+
const archivedFileSchema = parseSchema({
|
|
1531
|
+
"file": isStr, "type": isStr, "description": isStr, "size": isNum,
|
|
1532
|
+
"archivedAt": isStr, "archivedReason": isStr,
|
|
1533
|
+
});
|
|
1534
|
+
const archivedListSchema = {
|
|
1535
|
+
parse(v) {
|
|
1536
|
+
if (!v || typeof v !== "object") throw new Error("sage-mem: expected object");
|
|
1537
|
+
if (!Array.isArray(v.files)) throw new Error("sage-mem: expected files array");
|
|
1538
|
+
v.files.forEach((f) => archivedFileSchema.parse(f));
|
|
1539
|
+
return v;
|
|
1540
|
+
},
|
|
1541
|
+
};
|
|
1542
|
+
// 归档文件的读 / 写(B1b):形状与活跃记忆那套的 readSchema / writeSchema 对称。
|
|
1543
|
+
const archivedReadSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
1544
|
+
const archivedWriteSchema = parseSchema({ "ok": isBool, "file": isStr });
|
|
1545
|
+
|
|
1369
1546
|
// ── autodream 的 codec ──
|
|
1370
1547
|
// 刻意写得宽:这几个方法返回的是配置 / 状态对象,字段会随版本增删。
|
|
1371
1548
|
// 用 parseSchema 逐字段硬写一遍的代价是 —— host 那边加一个字段,client 的旧
|
|
@@ -1434,13 +1611,145 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1434
1611
|
result: { mode: "strict", typeSymbol: "sage-mem/types#DeleteResult", schema: deleteSchema, create: () => deleteSchema },
|
|
1435
1612
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1436
1613
|
},
|
|
1614
|
+
{
|
|
1615
|
+
id: "sage-mem#memory/archive",
|
|
1616
|
+
service: "memory",
|
|
1617
|
+
namespace: "memory",
|
|
1618
|
+
method: "archive",
|
|
1619
|
+
invocation: { kind: "direct" },
|
|
1620
|
+
parameters: [
|
|
1621
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1622
|
+
{ name: "reason", wire: "reason", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveReason", schema: strSchema, create: () => strSchema } },
|
|
1623
|
+
],
|
|
1624
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveResult", schema: archiveSchema, create: () => archiveSchema },
|
|
1625
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
id: "sage-mem#memory/restore",
|
|
1629
|
+
service: "memory",
|
|
1630
|
+
namespace: "memory",
|
|
1631
|
+
method: "restore",
|
|
1632
|
+
invocation: { kind: "direct" },
|
|
1633
|
+
parameters: [
|
|
1634
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1635
|
+
],
|
|
1636
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveResult", schema: archiveSchema, create: () => archiveSchema },
|
|
1637
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
id: "sage-mem#memory/listArchived",
|
|
1641
|
+
service: "memory",
|
|
1642
|
+
namespace: "memory",
|
|
1643
|
+
method: "listArchived",
|
|
1644
|
+
invocation: { kind: "direct" },
|
|
1645
|
+
parameters: [],
|
|
1646
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedList", schema: archivedListSchema, create: () => archivedListSchema },
|
|
1647
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
id: "sage-mem#memory/readArchived",
|
|
1651
|
+
service: "memory",
|
|
1652
|
+
namespace: "memory",
|
|
1653
|
+
method: "readArchived",
|
|
1654
|
+
invocation: { kind: "direct" },
|
|
1655
|
+
parameters: [
|
|
1656
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedFileName", schema: strSchema, create: () => strSchema } },
|
|
1657
|
+
],
|
|
1658
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedFileContent", schema: archivedReadSchema, create: () => archivedReadSchema },
|
|
1659
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
id: "sage-mem#memory/writeArchived",
|
|
1663
|
+
service: "memory",
|
|
1664
|
+
namespace: "memory",
|
|
1665
|
+
method: "writeArchived",
|
|
1666
|
+
invocation: { kind: "direct" },
|
|
1667
|
+
parameters: [
|
|
1668
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedFileName", schema: strSchema, create: () => strSchema } },
|
|
1669
|
+
{ name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedFileContent", schema: strSchema, create: () => strSchema } },
|
|
1670
|
+
],
|
|
1671
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedWriteResult", schema: archivedWriteSchema, create: () => archivedWriteSchema },
|
|
1672
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1673
|
+
},
|
|
1674
|
+
// ── 面板 API(D1):体检 / 归档候选 / 设置 / 保留名原样读写 ──
|
|
1675
|
+
// 结果形状都在演进(审计清单、候选表、设置项),顶层用 objCodec 接住、
|
|
1676
|
+
// 内部宽松 —— 与 autodream 那几个 codec 同一套理由:host 加字段不该让旧界面整体报错。
|
|
1677
|
+
{
|
|
1678
|
+
id: "sage-mem#memory/audit",
|
|
1679
|
+
service: "memory",
|
|
1680
|
+
namespace: "memory",
|
|
1681
|
+
method: "audit",
|
|
1682
|
+
invocation: { kind: "direct" },
|
|
1683
|
+
parameters: [],
|
|
1684
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#AuditReport", schema: objCodec, create: () => objCodec },
|
|
1685
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
id: "sage-mem#memory/archiveCandidates",
|
|
1689
|
+
service: "memory",
|
|
1690
|
+
namespace: "memory",
|
|
1691
|
+
method: "archiveCandidates",
|
|
1692
|
+
invocation: { kind: "direct" },
|
|
1693
|
+
parameters: [],
|
|
1694
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveCandidates", schema: objCodec, create: () => objCodec },
|
|
1695
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
id: "sage-mem#memory/getSettings",
|
|
1699
|
+
service: "memory",
|
|
1700
|
+
namespace: "memory",
|
|
1701
|
+
method: "getSettings",
|
|
1702
|
+
invocation: { kind: "direct" },
|
|
1703
|
+
parameters: [],
|
|
1704
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#MemorySettings", schema: objCodec, create: () => objCodec },
|
|
1705
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
id: "sage-mem#memory/setSettings",
|
|
1709
|
+
service: "memory",
|
|
1710
|
+
namespace: "memory",
|
|
1711
|
+
method: "setSettings",
|
|
1712
|
+
invocation: { kind: "direct" },
|
|
1713
|
+
parameters: [
|
|
1714
|
+
{ name: "patch", wire: "patch", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#MemorySettingsPatch", schema: objCodec, create: () => objCodec } },
|
|
1715
|
+
],
|
|
1716
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#MemorySettingsResult", schema: objCodec, create: () => objCodec },
|
|
1717
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
id: "sage-mem#memory/readRaw",
|
|
1721
|
+
service: "memory",
|
|
1722
|
+
namespace: "memory",
|
|
1723
|
+
method: "readRaw",
|
|
1724
|
+
invocation: { kind: "direct" },
|
|
1725
|
+
parameters: [
|
|
1726
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ReservedFileName", schema: strSchema, create: () => strSchema } },
|
|
1727
|
+
],
|
|
1728
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#RawFileContent", schema: objCodec, create: () => objCodec },
|
|
1729
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
id: "sage-mem#memory/writeRaw",
|
|
1733
|
+
service: "memory",
|
|
1734
|
+
namespace: "memory",
|
|
1735
|
+
method: "writeRaw",
|
|
1736
|
+
invocation: { kind: "direct" },
|
|
1737
|
+
parameters: [
|
|
1738
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ReservedFileName", schema: strSchema, create: () => strSchema } },
|
|
1739
|
+
{ name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#RawFileContent", schema: strSchema, create: () => strSchema } },
|
|
1740
|
+
],
|
|
1741
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#RawWriteResult", schema: objCodec, create: () => objCodec },
|
|
1742
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1743
|
+
},
|
|
1437
1744
|
{
|
|
1438
1745
|
id: "sage-mem#starmap/listStars",
|
|
1439
1746
|
service: "starmap",
|
|
1440
1747
|
namespace: "starmap",
|
|
1441
1748
|
method: "listStars",
|
|
1442
1749
|
invocation: { kind: "direct" },
|
|
1443
|
-
parameters: [
|
|
1750
|
+
parameters: [
|
|
1751
|
+
{ name: "includeArchived", wire: "includeArchived", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#StarmapIncludeArchived", schema: boolSchema, create: () => boolSchema } },
|
|
1752
|
+
],
|
|
1444
1753
|
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapList", schema: starmapListSchema, create: () => starmapListSchema },
|
|
1445
1754
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1446
1755
|
},
|
|
@@ -1628,6 +1937,79 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1628
1937
|
".smem-narrative{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere;}",
|
|
1629
1938
|
'.smem-narrative[data-clamp="1"]{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer;}',
|
|
1630
1939
|
".smem-card-foot{display:flex;justify-content:flex-end;}",
|
|
1940
|
+
// ── 档案馆(文件列表)──
|
|
1941
|
+
// 归档条目整体「退役」:虚线边框 + 灰底 + 内容压灰。
|
|
1942
|
+
// ⚠️ 灰的只许是**内容**(下面这几条只点标题/类型章/标签/正文的字色)。
|
|
1943
|
+
// 绝不给整张卡片加 opacity / filter —— 那会把里面的「编辑 / 恢复」按钮一起灰掉,
|
|
1944
|
+
// 用户会以为按钮不可按(同类误读已经栽过一次)。
|
|
1945
|
+
".smem-card--archived{border-style:dashed;background:var(--dsw-alias-bg-layer-2);}",
|
|
1946
|
+
".smem-card--archived .smem-card-title,.smem-card--archived .smem-badge,.smem-card--archived .smem-tag,.smem-card--archived .smem-narrative{color:var(--dsw-alias-label-secondary);}",
|
|
1947
|
+
".smem-card--archived .smem-badge,.smem-card--archived .smem-tag{opacity:.78;}",
|
|
1948
|
+
// 归档文件的「原样文本」编辑器:等宽字体,正文一字不改。
|
|
1949
|
+
// 选择器写两个类是为了压过后面那条 .smem-textarea{min-height:72px}——
|
|
1950
|
+
// 同一个权重时后定义的赢,单类写法会被它盖掉(截图走查时发现的)。
|
|
1951
|
+
".smem-ar-edit{border-top:1px dashed var(--dsw-alias-border-l2);padding-top:8px;display:flex;flex-direction:column;gap:6px;}",
|
|
1952
|
+
".smem-textarea.smem-ar-editor{min-height:220px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;}",
|
|
1953
|
+
".smem-ar-hint{font-size:11.5px;color:var(--dsw-alias-label-secondary);}",
|
|
1954
|
+
".smem-ar-meta{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;}",
|
|
1955
|
+
".smem-ar-reason{font-size:12px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-border-l2);padding-left:8px;white-space:pre-wrap;overflow-wrap:anywhere;}",
|
|
1956
|
+
".smem-ar-confirm{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:1.55;}",
|
|
1957
|
+
".smem-ar-banner{font-size:12px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 10px;}",
|
|
1958
|
+
// ── 工具行 + 工具面板(体检 / 归档候选 / 保留名原样编辑)──
|
|
1959
|
+
// 三个新入口收在一行**轻量**文字按钮里:不跟主工具栏抢重量级,也不各占一块版面。
|
|
1960
|
+
// 面板一次只开一个(Section 里一个 tool 状态),否则文件列表会被顶到屏幕外。
|
|
1961
|
+
".smem-tools{display:flex;align-items:center;gap:4px;flex-wrap:wrap;}",
|
|
1962
|
+
".smem-tool-btn{border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-family:inherit;cursor:pointer;padding:2px 8px;border-radius:6px;}",
|
|
1963
|
+
".smem-tool-btn:hover{color:var(--dsw-alias-label-primary);}",
|
|
1964
|
+
'.smem-tool-btn[data-on="1"]{color:var(--dsw-alias-brand-primary,#4d76e6);background:var(--dsw-alias-bg-layer-2);}',
|
|
1965
|
+
// 工具抽屉:关着时**零占位**(工具栏里只有一个「工具 ▾」),点开才占版面。
|
|
1966
|
+
// ⚠️ 可发现性不靠抽屉本身,靠头部带常显的「体检 N 个问题」徽标 —— 那是整个
|
|
1967
|
+
// 抽屉方案能成立的前提(否则三个能力就退回「藏起来」了)。
|
|
1968
|
+
".smem-drawer{position:relative;flex:none;}",
|
|
1969
|
+
".smem-drawer-pop{position:absolute;right:0;top:calc(100% + 6px);z-index:6;min-width:236px;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.10);padding:6px;display:flex;flex-direction:column;gap:2px;}",
|
|
1970
|
+
".smem-drawer-item{display:flex;align-items:center;gap:8px;border:0;background:transparent;font-family:inherit;font-size:12.5px;color:var(--dsw-alias-label-primary);text-align:left;padding:6px 8px;border-radius:8px;cursor:pointer;}",
|
|
1971
|
+
".smem-drawer-item:hover{background:var(--dsw-alias-bg-layer-2);}",
|
|
1972
|
+
'.smem-drawer-item[data-on="1"]{background:var(--dsw-alias-bg-layer-2);font-weight:600;}',
|
|
1973
|
+
".smem-drawer-item .smem-status{margin-left:auto;}",
|
|
1974
|
+
".smem-status--bad{color:var(--dsw-alias-state-error-primary,#dc2626);font-weight:600;}",
|
|
1975
|
+
// 头部带:三个 tab 共用的一条「我在哪 + 现在什么状态 + 主操作」。
|
|
1976
|
+
".smem-head-job{font-size:12.5px;font-weight:600;}",
|
|
1977
|
+
".smem-head-actions{margin-left:auto;display:flex;align-items:center;gap:6px;}",
|
|
1978
|
+
".smem-tool-panel{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:8px;}",
|
|
1979
|
+
// 体检 / 候选是「翻清单」的面板:定高 + 内部滚动,列表不被顶出首屏。
|
|
1980
|
+
// 原样编辑器(.smem-ar-editor)不设上限 —— 那是专心改一份文件的界面,给它完整高度。
|
|
1981
|
+
".smem-tool-panel--cap{max-height:320px;overflow-y:auto;}",
|
|
1982
|
+
".smem-tool-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1983
|
+
".smem-tool-title{font-weight:600;font-size:13px;}",
|
|
1984
|
+
".smem-tool-badge{display:inline-flex;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:0 6px;white-space:nowrap;}",
|
|
1985
|
+
".smem-tool-badge--bad{color:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626);}",
|
|
1986
|
+
".smem-tool-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1987
|
+
// 体检:硬问题用红标题分节;提示区单独一块虚线框(**不染红**)。
|
|
1988
|
+
".smem-audit-sec{display:flex;flex-direction:column;gap:3px;}",
|
|
1989
|
+
".smem-audit-h{font-size:12px;font-weight:600;}",
|
|
1990
|
+
".smem-audit-h--hard{color:var(--dsw-alias-state-error-primary,#dc2626);}",
|
|
1991
|
+
".smem-audit-row{font-size:12px;color:var(--dsw-alias-label-secondary);padding-left:10px;overflow-wrap:anywhere;}",
|
|
1992
|
+
".smem-audit-hint{border:1px dashed var(--dsw-alias-border-l2);border-radius:8px;padding:7px 9px;display:flex;flex-direction:column;gap:3px;}",
|
|
1993
|
+
".smem-empty--ok{color:var(--dsw-alias-state-success-primary,#16a34a);border-color:var(--dsw-alias-state-success-primary,#16a34a);}",
|
|
1994
|
+
// 归档候选:每一条都把「为什么建议归档」写出来。
|
|
1995
|
+
".smem-cand-list{display:flex;flex-direction:column;gap:6px;}",
|
|
1996
|
+
".smem-cand-note{font-size:11.5px;color:var(--dsw-alias-label-secondary);line-height:1.5;}",
|
|
1997
|
+
".smem-cand{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:7px 9px;display:flex;flex-direction:column;gap:5px;}",
|
|
1998
|
+
".smem-cand-main{display:flex;align-items:center;gap:8px;flex-wrap:wrap;cursor:pointer;}",
|
|
1999
|
+
".smem-cand-reason{font-size:12px;color:var(--dsw-alias-label-secondary);border-left:2px solid var(--dsw-alias-border-l2);padding-left:8px;overflow-wrap:anywhere;}",
|
|
2000
|
+
".smem-cand-act{display:flex;justify-content:flex-end;}",
|
|
2001
|
+
".smem-cand-confirm{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;border-top:1px dashed var(--dsw-alias-border-l2);padding-top:6px;}",
|
|
2002
|
+
".smem-raw-names{display:flex;gap:6px;flex-wrap:wrap;}",
|
|
2003
|
+
// 自动做梦的二级分组:导航用**下划线**(跟筛选 chip、动作按钮区分开,三种层级三种形状)。
|
|
2004
|
+
".smem-subtabs{display:flex;gap:16px;border-bottom:1px solid var(--dsw-alias-border-l2);margin:2px 0 8px;}",
|
|
2005
|
+
".smem-subtab{background:none;border:0;padding:5px 1px 7px;font-size:12.5px;font-family:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;position:relative;}",
|
|
2006
|
+
".smem-subtab--on{color:var(--dsw-alias-label-primary);font-weight:600;}",
|
|
2007
|
+
'.smem-subtab--on:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary);}',
|
|
2008
|
+
".smem-subtab-n{opacity:.6;margin-left:5px;font-variant-numeric:tabular-nums;}",
|
|
2009
|
+
// 五个注入参数排两列:一行放两个,扫读反而更清楚(吸收方案 A 的做法)。
|
|
2010
|
+
".smem-ad-grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(330px,1fr));gap:4px 18px;}",
|
|
2011
|
+
".smem-ad-field{display:flex;align-items:center;gap:8px;min-width:0;}",
|
|
2012
|
+
".smem-ad-field .smem-autodream-key{min-width:96px;}",
|
|
1631
2013
|
".smem-actions{display:flex;gap:6px;}",
|
|
1632
2014
|
".smem-form{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;}",
|
|
1633
2015
|
".smem-form-title{font-weight:600;font-size:13px;}",
|
|
@@ -1688,6 +2070,10 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1688
2070
|
".smem-ad-run:hover{border-color:var(--dsw-alias-border-l2);}",
|
|
1689
2071
|
'.smem-ad-run[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);}',
|
|
1690
2072
|
".smem-ad-md{max-height:360px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2);font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.6;}",
|
|
2073
|
+
// ── 注入参数 / 保留名名单(自动做梦 tab)──
|
|
2074
|
+
".smem-ad-warn{color:var(--dsw-alias-state-warning-primary,#d97706);}",
|
|
2075
|
+
".smem-chip--fixed{opacity:.72;cursor:default;}",
|
|
2076
|
+
".smem-chip-x{border:0;background:transparent;color:inherit;cursor:pointer;font-size:11px;font-family:inherit;padding:0 0 0 5px;}",
|
|
1691
2077
|
].join("\n");
|
|
1692
2078
|
|
|
1693
2079
|
const TYPE_META = {
|
|
@@ -1995,16 +2381,296 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1995
2381
|
}, String(item.description))
|
|
1996
2382
|
: null,
|
|
1997
2383
|
h("div", { className: "smem-card-foot", key: "foot" },
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2384
|
+
props.archiving
|
|
2385
|
+
? h("div", { className: "smem-ar-confirm", key: "ar" }, [
|
|
2386
|
+
h("div", { key: "what" }, "把「" + item.file + "」收进档案馆:它不再参与检索与星图,文件移进 archive/,随时可以恢复。"),
|
|
2387
|
+
// 「可留空」必须跟输入框贴在一起、并且独立成行 —— 它是这一块最容易
|
|
2388
|
+
// 被误读成「必须填才能按」的地方。
|
|
2389
|
+
h("label", { className: "smem-label", key: "lb" }, "归档理由(可留空)"),
|
|
2390
|
+
h("input", {
|
|
2391
|
+
className: "smem-input", key: "in",
|
|
2392
|
+
value: props.archiveReason || "",
|
|
2393
|
+
placeholder: "例如:结论已被 project_beta 取代",
|
|
2394
|
+
onChange: function (e) { props.onArchiveReason(e.target.value); },
|
|
2395
|
+
}),
|
|
2396
|
+
h("div", { className: "smem-ar-hint", key: "hint" },
|
|
2397
|
+
"写下当时的判断,以后翻到它时就知道这条还值不值得捞回来。"),
|
|
2398
|
+
h("div", { className: "smem-row", key: "btns" }, [
|
|
2399
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", disabled: props.busy, onClick: function () { props.onConfirmArchive(item.file); } }, "确认归档"),
|
|
2400
|
+
h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelArchive }, "取消"),
|
|
2401
|
+
]),
|
|
2402
|
+
])
|
|
2403
|
+
: h("div", { className: "smem-actions", key: "acts" },
|
|
2404
|
+
props.deleting
|
|
2405
|
+
? [
|
|
2406
|
+
h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
|
|
2407
|
+
h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
|
|
2408
|
+
]
|
|
2409
|
+
: [
|
|
2410
|
+
h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
|
|
2411
|
+
// 归档排在「编辑」和「删除」之间:比编辑轻、比删除温和,且随时可逆。
|
|
2412
|
+
h("button", { type: "button", className: "smem-btn", key: "arch", title: "收进档案馆:不再参与检索与星图,随时可以恢复", onClick: function () { props.onAskArchive(item.file); } }, "归档"),
|
|
2413
|
+
h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
|
|
2414
|
+
])),
|
|
2415
|
+
]);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* 归档区的一条。与活动卡片共用骨架(类型章 / 标题 / 标签 / 描述),刻意的差异:
|
|
2420
|
+
* 虚线边框 + 整体压灰(退役感)、多一行归档留痕、动作是「编辑 / 恢复」。
|
|
2421
|
+
*
|
|
2422
|
+
* ⚠️ 灰的只有**内容**(`.smem-card--archived` 那几条只点标题/类型章/标签/正文的字色)。
|
|
2423
|
+
* 按钮一律保持原样 —— 按钮一灰,用户就以为不能按。
|
|
2424
|
+
*
|
|
2425
|
+
* ⚠️ 编辑走**原样文本**:readArchived 拿到的全文直接进 textarea,保存时原样交回
|
|
2426
|
+
* writeArchived。绝不走活跃记忆那套 buildFrontmatter 重建 —— 重建会吞字段,
|
|
2427
|
+
* 归档文件里还有 archived_at / archived_reason 这种留痕,更动不得。
|
|
2428
|
+
* 也不提供删除:归档 = 不删,host 侧也没有那条 remote。
|
|
2429
|
+
*/
|
|
2430
|
+
function ArchivedCard(props) {
|
|
2431
|
+
const item = props.item;
|
|
2432
|
+
const meta = TYPE_META[item.type] || TYPE_META.reference;
|
|
2433
|
+
return h("div", { className: "smem-card smem-card--archived", key: item.file }, [
|
|
2434
|
+
h("div", { className: "smem-card-head", key: "head" }, [
|
|
2435
|
+
h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
|
|
2436
|
+
h("span", { className: "smem-card-title", key: "title" }, item.file),
|
|
2437
|
+
h("span", { className: "smem-ar-meta", key: "at", title: "归档时间" }, "已归档 " + (item.archivedAt || "(时间未知)")),
|
|
2438
|
+
h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
|
|
2439
|
+
]),
|
|
2440
|
+
(item.tags && item.tags.length)
|
|
2441
|
+
? h("div", { className: "smem-taglist", key: "tags" }, item.tags.map(function (t) {
|
|
2442
|
+
return h("span", { className: "smem-tag", key: t }, "#" + t);
|
|
2443
|
+
}))
|
|
2444
|
+
: null,
|
|
2445
|
+
// 描述留在活动卡片里的同一个位置(标签下面),归档留痕块再往下 ——
|
|
2446
|
+
// 否则「归档理由」像卡片结尾,后面孤零零一行描述没人认得它是什么。
|
|
2447
|
+
item.description
|
|
2448
|
+
? h("div", { className: "smem-narrative", key: "desc" }, String(item.description))
|
|
2449
|
+
: null,
|
|
2450
|
+
h("div", { className: "smem-ar-reason", key: "why" }, "归档理由:" + (item.archivedReason || "(没写)")),
|
|
2451
|
+
props.editing
|
|
2452
|
+
? h("div", { className: "smem-ar-edit", key: "edit" }, [
|
|
2453
|
+
h("div", { className: "smem-ar-hint", key: "note" },
|
|
2454
|
+
"原样文本编辑:下面就是这份文件的全文(含 frontmatter 与归档留痕),保存时原样写回,不会被重新生成。"),
|
|
2455
|
+
props.loading
|
|
2456
|
+
? h("div", { className: "smem-status", key: "loading" }, "读取中…")
|
|
2457
|
+
: h("textarea", {
|
|
2458
|
+
className: "smem-textarea smem-ar-editor", key: "ta",
|
|
2459
|
+
value: props.text || "",
|
|
2460
|
+
spellCheck: false,
|
|
2461
|
+
onChange: function (e) { props.onText(e.target.value); },
|
|
2462
|
+
}),
|
|
2463
|
+
h("div", { className: "smem-row", key: "btns" }, [
|
|
2464
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(item.file); } }, props.saving ? "保存中…" : "保存"),
|
|
2465
|
+
h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, "取消"),
|
|
2466
|
+
]),
|
|
2467
|
+
])
|
|
2468
|
+
: h("div", { className: "smem-card-foot", key: "foot" },
|
|
2469
|
+
h("div", { className: "smem-actions", key: "acts" }, [
|
|
2470
|
+
h("button", {
|
|
2471
|
+
type: "button", className: "smem-btn", key: "edit",
|
|
2472
|
+
disabled: props.busy,
|
|
2473
|
+
onClick: function () { props.onEdit(item.file); },
|
|
2474
|
+
}, "编辑"),
|
|
2475
|
+
h("button", {
|
|
2476
|
+
type: "button", className: "smem-btn smem-btn-primary", key: "restore",
|
|
2477
|
+
disabled: props.busy,
|
|
2478
|
+
onClick: function () { props.onRestore(item.file); },
|
|
2479
|
+
}, props.restoring ? "恢复中…" : "恢复"),
|
|
2480
|
+
])),
|
|
2481
|
+
]);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// ── 自动归档三档的**唯一**一套措辞 ──
|
|
2485
|
+
// 文件列表的「归档候选」与自动做梦 tab 的开关都从这里取词:同一件事在两处出现,
|
|
2486
|
+
// 用两套说法等于让用户以为它们是两回事。
|
|
2487
|
+
const AUTO_ARCHIVE_LABEL = { off: "关闭", report: "只出报告", auto: "满足条件自动跑" };
|
|
2488
|
+
const AUTO_ARCHIVE_COST = {
|
|
2489
|
+
off: "关闭:一个文件都不动 —— 候选只列在清单里,等你手点。",
|
|
2490
|
+
report: "只出报告:候选写进整理报告,一个文件都不动。默认档,最保守。",
|
|
2491
|
+
auto: "满足条件自动跑:会真的把候选移进 archive/(只移不删,随时可恢复);" +
|
|
2492
|
+
"若「改动方式」是只出报告,这一档会自动降级成只出报告。",
|
|
2493
|
+
};
|
|
2494
|
+
const autoArchiveOf = (v) => (v === "off" || v === "auto" ? v : "report");
|
|
2495
|
+
|
|
2496
|
+
/** 头部带上的「我在哪」:三个 tab 的中文名(与 .smem-tabs 上的字面一致)。 */
|
|
2497
|
+
const TAB_LABEL = { files: "文件列表", star: "记忆星图", autodream: "自动做梦" };
|
|
2498
|
+
|
|
2499
|
+
/**
|
|
2500
|
+
* 工具面板头部的「收起」按钮:抽屉里开的工具要能一键收回去 ——
|
|
2501
|
+
* 面板不收,列表就还在下面被顶着。
|
|
2502
|
+
*/
|
|
2503
|
+
const toolClose = (props) => (props.onClose
|
|
2504
|
+
? h("button", { type: "button", className: "smem-btn", key: "close", onClick: props.onClose }, "收起")
|
|
2505
|
+
: null);
|
|
2506
|
+
|
|
2507
|
+
/**
|
|
2508
|
+
* 「体检」结果面板。
|
|
2509
|
+
*
|
|
2510
|
+
* 只读:这里**不提供任何「顺手修一下」的按钮** —— 修哪条、怎么修由用户决定,
|
|
2511
|
+
* 走列表里那条记忆自己的编辑入口。
|
|
2512
|
+
*
|
|
2513
|
+
* 硬问题与提示**分开渲染**:`archivedLinks` 是提示(归档不是删除,目标还在档案馆里),
|
|
2514
|
+
* 跟问题一样染红会让人去「修」一条本来正确的链接 —— 那正是审计里那条分界线。
|
|
2515
|
+
*/
|
|
2516
|
+
function AuditPanel(props) {
|
|
2517
|
+
const r = props.report || {};
|
|
2518
|
+
const rows = (list, render) => (list || []).map(function (x, i) {
|
|
2519
|
+
return h("div", { className: "smem-audit-row", key: String(x.file || "?") + "-" + i }, render(x));
|
|
2520
|
+
});
|
|
2521
|
+
const section = (title, list, render) => ((list && list.length)
|
|
2522
|
+
? h("div", { className: "smem-audit-sec", key: title }, [
|
|
2523
|
+
h("div", { className: "smem-audit-h smem-audit-h--hard", key: "h" }, title + "(" + list.length + ")"),
|
|
2524
|
+
rows(list, render),
|
|
2525
|
+
])
|
|
2526
|
+
: null);
|
|
2527
|
+
const hints = r.archivedLinks || [];
|
|
2528
|
+
return h("div", { className: "smem-tool-panel smem-tool-panel--cap", key: "audit" }, [
|
|
2529
|
+
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
2530
|
+
h("span", { className: "smem-tool-title", key: "t" }, "体检"),
|
|
2531
|
+
h("span", { className: "smem-muted", key: "s" },
|
|
2532
|
+
"扫了 " + String(r.fileCount == null ? "?" : r.fileCount) + " 个记忆 · 索引 " +
|
|
2533
|
+
String(r.indexEntries == null ? "?" : r.indexEntries) + " 条 · 档案馆 " +
|
|
2534
|
+
String(r.archivedCount == null ? "?" : r.archivedCount) + " 条"),
|
|
2535
|
+
h("span", { className: "smem-tool-badge" + (r.problems ? " smem-tool-badge--bad" : ""), key: "p" },
|
|
2536
|
+
r.problems ? ("问题 " + r.problems) : "没有问题"),
|
|
2537
|
+
h("button", { type: "button", className: "smem-btn", key: "again", disabled: props.busy, onClick: props.onRun }, props.busy ? "体检中…" : "重新体检"),
|
|
2538
|
+
toolClose(props),
|
|
2539
|
+
]),
|
|
2540
|
+
r.problems
|
|
2541
|
+
? h("div", { className: "smem-audit-secs", key: "secs" }, [
|
|
2542
|
+
section("索引悬空(索引里有、文件没了)", r.dangling, (x) => x.file),
|
|
2543
|
+
section("漏索引(文件在、索引里没有)", r.unlisted, (x) => x.file),
|
|
2544
|
+
section("type 与前缀对不上", r.typeMismatch, (x) => x.file + " · type=" + x.type + ",文件名前缀=" + x.prefix),
|
|
2545
|
+
section("行尾是 CRLF", r.crlf, (x) => x.file),
|
|
2546
|
+
section("双链断链", r.brokenLinks, (x) => x.file + " → [[" + x.target + "]]"),
|
|
2547
|
+
section("未加引号的「空格+#」", r.hashHazards, (x) => x.file + " · " + x.line),
|
|
2548
|
+
section("没有 frontmatter", r.noFrontmatter, (x) => x.file),
|
|
2549
|
+
section("读不出来", r.unreadable, (x) => x.file),
|
|
2550
|
+
])
|
|
2551
|
+
: h("div", { className: "smem-empty smem-empty--ok", key: "ok" },
|
|
2552
|
+
"没有发现问题 —— 索引、双链、frontmatter 与行尾都对得上。"),
|
|
2553
|
+
hints.length
|
|
2554
|
+
? h("div", { className: "smem-audit-hint", key: "hint" }, [
|
|
2555
|
+
h("div", { className: "smem-audit-h", key: "h" }, "指向已归档(提示,不计入问题)"),
|
|
2556
|
+
h("div", { className: "smem-ar-hint", key: "n" },
|
|
2557
|
+
"归档不是删除:这些链接的目标还在档案馆里躺着,恢复它链接就活了 —— 不用改。"),
|
|
2558
|
+
rows(hints, (x) => x.file + " → [[" + x.target + "]]"),
|
|
2559
|
+
])
|
|
2560
|
+
: null,
|
|
2561
|
+
h("div", { className: "smem-ar-hint", key: "ro" }, "这里只报告、不修改 —— 要改哪条,用那条记忆自己的编辑入口。"),
|
|
2562
|
+
]);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
/**
|
|
2566
|
+
* 「归档候选」面板。
|
|
2567
|
+
*
|
|
2568
|
+
* 每一条都必须把**为什么建议归档**写出来(`reason` 就是干这个的)——
|
|
2569
|
+
* 只列文件名等于让人凭感觉点。执行前一律过一道确认,逐条与批量都要过。
|
|
2570
|
+
*/
|
|
2571
|
+
function CandidatePanel(props) {
|
|
2572
|
+
const c = props.data || {};
|
|
2573
|
+
const cands = Array.isArray(c.candidates) ? c.candidates : [];
|
|
2574
|
+
const th = c.thresholds || {};
|
|
2575
|
+
const pickedN = cands.filter((x) => props.picked[x.file]).length;
|
|
2576
|
+
const mode = autoArchiveOf(c.autoArchive);
|
|
2577
|
+
const allPicked = cands.length > 0 && pickedN === cands.length;
|
|
2578
|
+
return h("div", { className: "smem-tool-panel smem-tool-panel--cap", key: "cand" }, [
|
|
2579
|
+
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
2580
|
+
h("span", { className: "smem-tool-title", key: "t" }, "归档候选"),
|
|
2581
|
+
h("span", { className: "smem-muted", key: "s" },
|
|
2582
|
+
"自动归档:" + AUTO_ARCHIVE_LABEL[mode] + " · 阈值:项目 " + String(th.project == null ? "?" : th.project) + " 天 / 参考 " +
|
|
2583
|
+
String(th.reference == null ? "?" : th.reference) + " 天 / 用户 " +
|
|
2584
|
+
String(th.user == null ? "?" : th.user) + " 天"),
|
|
2585
|
+
h("span", { className: "smem-tool-badge", key: "n" }, "候选 " + cands.length),
|
|
2586
|
+
h("button", { type: "button", className: "smem-btn", key: "again", disabled: props.busy, onClick: props.onLoad }, props.busy ? "刷新中…" : "刷新"),
|
|
2587
|
+
toolClose(props),
|
|
2588
|
+
]),
|
|
2589
|
+
cands.length === 0
|
|
2590
|
+
? h("div", { className: "smem-empty", key: "empty" },
|
|
2591
|
+
"按当前阈值没有可归档的 —— 要么闲置天数还没到该类型的阈值,要么台账里还没有访问记录。")
|
|
2592
|
+
: h("div", { className: "smem-cand-list", key: "list" }, [
|
|
2593
|
+
h("div", { className: "smem-cand-note", key: "note" },
|
|
2594
|
+
"候选只按「多久没被注入过」算。归档 = 移进 archive/(不删),随时可以恢复。"),
|
|
2595
|
+
cands.map((x) => h("div", { className: "smem-cand", key: x.file }, [
|
|
2596
|
+
h("label", { className: "smem-cand-main", key: "m" }, [
|
|
2597
|
+
h("input", { type: "checkbox", key: "cb", checked: !!props.picked[x.file], onChange: function () { props.onPick(x.file); } }),
|
|
2598
|
+
h("span", { className: "smem-card-title", key: "f" }, x.file),
|
|
2599
|
+
h("span", { className: "smem-badge", key: "ty" }, TYPE_LABEL[x.type] || x.type || "—"),
|
|
2600
|
+
h("span", { className: "smem-muted", key: "d" }, "闲置 " + String(x.days) + " 天 / 阈值 " + String(x.limit) + " 天"),
|
|
2601
|
+
]),
|
|
2602
|
+
h("div", { className: "smem-cand-reason", key: "why" },
|
|
2603
|
+
"为什么建议归档:" + (x.reason || ("闲置 " + String(x.days) + " 天,超过该类型阈值 " + String(x.limit) + " 天"))),
|
|
2604
|
+
props.confirm === x.file
|
|
2605
|
+
? h("div", { className: "smem-cand-confirm", key: "cf" }, [
|
|
2606
|
+
h("span", { key: "t" }, "确认把「" + x.file + "」移进档案馆?它不再参与检索与星图,随时可以恢复。"),
|
|
2607
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "yes", disabled: props.busy, onClick: function () { props.onArchiveOne(x); } }, "确认归档"),
|
|
2608
|
+
h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelConfirm }, "取消"),
|
|
2609
|
+
])
|
|
2610
|
+
: h("div", { className: "smem-cand-act", key: "act" },
|
|
2611
|
+
h("button", { type: "button", className: "smem-btn", key: "one", disabled: props.busy, onClick: function () { props.onAskOne(x.file); } }, "归档这一条")),
|
|
2612
|
+
])),
|
|
2613
|
+
]),
|
|
2614
|
+
cands.length
|
|
2615
|
+
? h("div", { className: "smem-tool-foot", key: "foot" }, [
|
|
2616
|
+
h("button", { type: "button", className: "smem-btn", key: "all", disabled: props.busy, onClick: props.onPickAll }, allPicked ? "全不选" : "全选"),
|
|
2617
|
+
props.confirm === "__batch__"
|
|
2618
|
+
? [
|
|
2619
|
+
h("span", { className: "smem-ar-hint", key: "t" }, "确认把选中的 " + pickedN + " 条一起移进档案馆?"),
|
|
2620
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "yes", disabled: props.busy || !pickedN, onClick: props.onArchivePicked }, "确认归档"),
|
|
2621
|
+
h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelConfirm }, "取消"),
|
|
2622
|
+
]
|
|
2623
|
+
: h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "go", disabled: props.busy || !pickedN, onClick: props.onAskPicked }, "执行归档(选中 " + pickedN + " 条)"),
|
|
2624
|
+
])
|
|
2625
|
+
: null,
|
|
2626
|
+
]);
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/**
|
|
2630
|
+
* 保留名(索引 / 流水)的**原样文本**编辑。
|
|
2631
|
+
*
|
|
2632
|
+
* readRaw 拿到的全文直接进 textarea,保存时逐字节交回 writeRaw —— 不解析、不重建。
|
|
2633
|
+
* 这是那次数据事故(表单重建 frontmatter 把 baseline / node_type 抹掉)的解药,
|
|
2634
|
+
* 所以这里刻意不复用上面的记忆表单。
|
|
2635
|
+
*/
|
|
2636
|
+
function RawPanel(props) {
|
|
2637
|
+
const names = Array.isArray(props.reserved) ? props.reserved : [];
|
|
2638
|
+
return h("div", { className: "smem-tool-panel", key: "raw" }, [
|
|
2639
|
+
h("div", { className: "smem-tool-head", key: "head" }, [
|
|
2640
|
+
h("span", { className: "smem-tool-title", key: "t" }, "保留名原样编辑"),
|
|
2641
|
+
h("span", { className: "smem-muted", key: "s" },
|
|
2642
|
+
"索引与流水这类文件不走记忆表单:读到的全文直接给你,保存时逐字节写回," +
|
|
2643
|
+
"不解析、也不重建 frontmatter(普通记忆才走表单)。"),
|
|
2644
|
+
]),
|
|
2645
|
+
names.length
|
|
2646
|
+
? h("div", { className: "smem-raw-names", key: "names" }, names.map((n) =>
|
|
2647
|
+
h("button", {
|
|
2648
|
+
type: "button", key: n, className: "smem-chip",
|
|
2649
|
+
"data-on": props.name === n ? "1" : "0",
|
|
2650
|
+
onClick: function () { props.onOpen(n); },
|
|
2651
|
+
}, n)))
|
|
2652
|
+
: h("div", { className: "smem-ar-hint", key: "nonames" }, "宿主没给出保留名清单。"),
|
|
2653
|
+
props.name
|
|
2654
|
+
? h("div", { className: "smem-ar-edit", key: "edit" }, [
|
|
2655
|
+
h("div", { className: "smem-ar-hint", key: "n" }, "正在编辑 " + props.name + " —— 保存就是覆盖这一份文件。"),
|
|
2656
|
+
props.isNew
|
|
2657
|
+
? h("div", { className: "smem-ar-hint smem-ad-warn", key: "new" },
|
|
2658
|
+
"⚠ 这个文件还不存在 —— 保存会新建它。")
|
|
2659
|
+
: null,
|
|
2660
|
+
props.loading
|
|
2661
|
+
? h("div", { className: "smem-status", key: "l" }, "读取中…")
|
|
2662
|
+
: h("textarea", {
|
|
2663
|
+
className: "smem-textarea smem-ar-editor", key: "ta",
|
|
2664
|
+
value: props.text || "",
|
|
2665
|
+
spellCheck: false,
|
|
2666
|
+
onChange: function (e) { props.onText(e.target.value); },
|
|
2667
|
+
}),
|
|
2668
|
+
h("div", { className: "smem-row", key: "btns" }, [
|
|
2669
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: props.busy || props.loading, onClick: function () { props.onSave(props.name); } }, props.saving ? "保存中…" : (props.isNew ? "新建并保存" : "保存")),
|
|
2670
|
+
h("button", { type: "button", className: "smem-btn", key: "close", disabled: props.busy, onClick: props.onClose }, "关闭"),
|
|
2671
|
+
]),
|
|
2672
|
+
])
|
|
2673
|
+
: h("div", { className: "smem-ar-hint", key: "pick" }, "选一个文件名开始编辑。"),
|
|
2008
2674
|
]);
|
|
2009
2675
|
}
|
|
2010
2676
|
|
|
@@ -2053,6 +2719,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2053
2719
|
*/
|
|
2054
2720
|
function AutodreamPanel(props) {
|
|
2055
2721
|
const remote = props.ctx && props.ctx.get ? props.ctx.get("remote.autodream") : null;
|
|
2722
|
+
// 注入设置走 memory 那半边(autodream 只管整理那趟)
|
|
2723
|
+
const memRemote = props.ctx && props.ctx.get ? props.ctx.get("remote.memory") : null;
|
|
2056
2724
|
const sCfg = react.useState(null); const cfg = sCfg[0], setCfg = sCfg[1];
|
|
2057
2725
|
const sSt = react.useState(null); const st = sSt[0], setSt = sSt[1];
|
|
2058
2726
|
const sErr = react.useState(null); const err = sErr[0], setErr = sErr[1];
|
|
@@ -2074,6 +2742,14 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2074
2742
|
const sRbMsg = react.useState(null); const rbMsg = sRbMsg[0], setRbMsg = sRbMsg[1];
|
|
2075
2743
|
const sListErr = react.useState(null); const listErr = sListErr[0], setListErr = sListErr[1];
|
|
2076
2744
|
const sWasRunning = react.useState(false); const wasRunning = sWasRunning[0], setWasRunning = sWasRunning[1];
|
|
2745
|
+
// ── 记忆注入设置(memory.getSettings / setSettings)──
|
|
2746
|
+
// limits = **当前生效**(模块加载时固化);pendingLimits = **重启后才生效**。
|
|
2747
|
+
// 两个都必须在界面上,且看得出差别 —— 只显示一个就等于骗人。
|
|
2748
|
+
const sMemSet = react.useState(null); const memSet = sMemSet[0], setMemSet = sMemSet[1];
|
|
2749
|
+
const sLimitsDraft = react.useState(null); const limitsDraft = sLimitsDraft[0], setLimitsDraft = sLimitsDraft[1];
|
|
2750
|
+
const sSaveBusy = react.useState(false); const saveBusy = sSaveBusy[0], setSaveBusy = sSaveBusy[1];
|
|
2751
|
+
const sAutoSaving = react.useState(false); const autoSaving = sAutoSaving[0], setAutoSaving = sAutoSaving[1];
|
|
2752
|
+
const sReservedNew = react.useState(""); const reservedNew = sReservedNew[0], setReservedNew = sReservedNew[1];
|
|
2077
2753
|
|
|
2078
2754
|
// 派生量放在 effect 之前:下面几个 effect 的依赖数组要在渲染期就能取到值。
|
|
2079
2755
|
const c = cfg || {};
|
|
@@ -2081,6 +2757,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2081
2757
|
const last = (st && st.lastResult) || null;
|
|
2082
2758
|
const route = (st && st.route) || null;
|
|
2083
2759
|
const rollbackScope = c.rollbackScope === "all" ? "all" : "files";
|
|
2760
|
+
// 保留名名单:reserved = 生效清单(含内置两个),reservedExtra = 用户自己声明的那部分。
|
|
2761
|
+
// 内置的两个 = 生效清单里不属于 reservedExtra 的那些 —— 不写死名字,host 那边改了这边跟着变。
|
|
2762
|
+
const extraReserved = Array.isArray(memSet && memSet.reservedExtra) ? memSet.reservedExtra : [];
|
|
2763
|
+
const extraLower = extraReserved.map(function (x) { return String(x).toLowerCase(); });
|
|
2764
|
+
const fixedReserved = ((memSet && memSet.reserved) || []).filter(function (n) {
|
|
2765
|
+
return extraLower.indexOf(String(n).toLowerCase()) < 0;
|
|
2766
|
+
});
|
|
2084
2767
|
|
|
2085
2768
|
react.useEffect(function () {
|
|
2086
2769
|
if (!remote) { setErr("remote.autodream 不可用 —— 插件的 宿主侧可能没加载成功,看 DSH 启动日志"); return undefined; }
|
|
@@ -2095,6 +2778,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2095
2778
|
pull();
|
|
2096
2779
|
refreshLists();
|
|
2097
2780
|
loadModels();
|
|
2781
|
+
loadMemSettings();
|
|
2098
2782
|
const id = setInterval(pull, 2000);
|
|
2099
2783
|
return function () { alive = false; clearInterval(id); };
|
|
2100
2784
|
}, [remote]);
|
|
@@ -2137,6 +2821,128 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2137
2821
|
}, function (m) { setErr(m); });
|
|
2138
2822
|
}
|
|
2139
2823
|
|
|
2824
|
+
// ── 注入设置:5 项上限 + 保留名名单 ─────────────────────────────
|
|
2825
|
+
// 边界与 host 的 LIMIT_SPECS 一一对应(那边是权威,这里只是提前拦明显笔误)。
|
|
2826
|
+
const LIMIT_FIELDS = [
|
|
2827
|
+
{ key: "maxResults", label: "一次注入条数", lo: 1, hi: 30, unit: "条" },
|
|
2828
|
+
{ key: "maxChars", label: "单条字符上限", lo: 200, hi: 20000, unit: "字符" },
|
|
2829
|
+
{ key: "maxBaseline", label: "baseline 名额", lo: 1, hi: 50, unit: "条" },
|
|
2830
|
+
{ key: "maxSessionBytes", label: "单会话字节预算", lo: 4096, hi: 524288, unit: "字节" },
|
|
2831
|
+
{ key: "staleDays", label: "新鲜度门槛", lo: 0, hi: 365, unit: "天" },
|
|
2832
|
+
];
|
|
2833
|
+
|
|
2834
|
+
/** 把一份 limits 对象摊成输入框用的字符串草稿(输入期间允许非法,保存时再校验)。 */
|
|
2835
|
+
function limitsDraftOf(src) {
|
|
2836
|
+
const out = {};
|
|
2837
|
+
for (const f of LIMIT_FIELDS) out[f.key] = src && src[f.key] != null ? String(src[f.key]) : "";
|
|
2838
|
+
return out;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function loadMemSettings(onOk) {
|
|
2842
|
+
if (!memRemote || typeof memRemote.getSettings !== "function") return;
|
|
2843
|
+
Promise.resolve(memRemote.getSettings()).then(function (res) {
|
|
2844
|
+
const d = unwrap(res) || {};
|
|
2845
|
+
setMemSet(d);
|
|
2846
|
+
setLimitsDraft(limitsDraftOf(d.pendingLimits || d.limits || {}));
|
|
2847
|
+
if (onOk) onOk(d);
|
|
2848
|
+
}, function (e) { setErr(detail(e)); });
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
/**
|
|
2852
|
+
* 自动归档三档:保存后**回读校验**。
|
|
2853
|
+
*
|
|
2854
|
+
* `setConfig` 对非法值沿用的是「忽略、保留原值」的语义(没有 `{ok:false}` 通道),
|
|
2855
|
+
* 所以不能只看它返回的 config —— 必须再 `getConfig()` 一次,确认那个值真的落在
|
|
2856
|
+
* 配置里。没变就报错:**不能让用户点了保存以为成了、实际什么都没发生**。
|
|
2857
|
+
*/
|
|
2858
|
+
function saveAutoArchive(v) {
|
|
2859
|
+
setErr(null); setNotice(""); setAutoSaving(true);
|
|
2860
|
+
callRemote("setConfig", [{ autoArchive: v }], function (d) {
|
|
2861
|
+
if (d && d.config && d.config.autoArchive === v) {
|
|
2862
|
+
setCfg(d.config);
|
|
2863
|
+
setAutoSaving(false);
|
|
2864
|
+
setNotice("自动归档已设为「" + AUTO_ARCHIVE_LABEL[v] + "」");
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
// 返回的 config 里没看到新值 → 回读一次再定论
|
|
2868
|
+
callRemote("getConfig", [], function (d2) {
|
|
2869
|
+
const next = d2 && d2.config ? d2.config : null;
|
|
2870
|
+
setAutoSaving(false);
|
|
2871
|
+
if (next) setCfg(next);
|
|
2872
|
+
const got = next ? autoArchiveOf(next.autoArchive) : null;
|
|
2873
|
+
if (got === v) { setNotice("自动归档已设为「" + AUTO_ARCHIVE_LABEL[v] + "」"); return; }
|
|
2874
|
+
setErr("保存没生效:配置里仍是「" + (got ? AUTO_ARCHIVE_LABEL[got] : "读不出来") +
|
|
2875
|
+
"」。setConfig 对非法值会忽略并保留原值 —— 请重试,或看 DSH 启动日志。");
|
|
2876
|
+
}, function (m) { setAutoSaving(false); setErr("保存后回读失败:" + m); });
|
|
2877
|
+
}, function (m) { setAutoSaving(false); setErr(m); });
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/** 保存 5 项上限。越界/非整数**本地先拦**(host 也是直接拒绝),别让它白跑一趟。 */
|
|
2881
|
+
function saveLimits() {
|
|
2882
|
+
if (!memRemote || typeof memRemote.setSettings !== "function") { setErr("宿主侧还没有 setSettings 接口(插件版本较旧)"); return; }
|
|
2883
|
+
const draft = limitsDraft || {};
|
|
2884
|
+
const limits = {};
|
|
2885
|
+
const bad = [];
|
|
2886
|
+
for (const f of LIMIT_FIELDS) {
|
|
2887
|
+
const raw = String(draft[f.key] == null ? "" : draft[f.key]).trim();
|
|
2888
|
+
const n = Number(raw);
|
|
2889
|
+
if (raw === "" || !Number.isFinite(n) || !Number.isInteger(n)) { bad.push(f.label + "要整数"); continue; }
|
|
2890
|
+
if (n < f.lo || n > f.hi) { bad.push(f.label + " 要在 " + f.lo + "–" + f.hi + " 之间"); continue; }
|
|
2891
|
+
limits[f.key] = n;
|
|
2892
|
+
}
|
|
2893
|
+
if (bad.length) { setErr("没保存 —— " + bad.join(";")); return; }
|
|
2894
|
+
setSaveBusy(true); setErr(null); setNotice("");
|
|
2895
|
+
Promise.resolve(memRemote.setSettings({ limits: limits })).then(function (out) {
|
|
2896
|
+
setSaveBusy(false);
|
|
2897
|
+
// setSettings 是 {ok:true, settings, restartRequired} 形状:不要过 unwrap
|
|
2898
|
+
// (unwrap 只认 {ok, value},这里会把 settings 吃掉)。
|
|
2899
|
+
if (out && out.ok === false) { setErr(out.error || "保存被拒绝"); return; }
|
|
2900
|
+
if (out && out.settings) { setMemSet(out.settings); setLimitsDraft(limitsDraftOf(out.settings.pendingLimits)); }
|
|
2901
|
+
else loadMemSettings();
|
|
2902
|
+
setNotice("已保存 —— 需重启 DSH 才生效");
|
|
2903
|
+
}, function (e) { setSaveBusy(false); setErr(detail(e)); });
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* 保存保留名名单(用户声明的那部分)。
|
|
2908
|
+
*
|
|
2909
|
+
* 两个内置名(memory.md / session-log.md)**不在**这份名单里,也不在界面上给删除入口 ——
|
|
2910
|
+
* 那是生效清单里固定的两个,删不掉。这里管的只是用户自己加的那些。
|
|
2911
|
+
*/
|
|
2912
|
+
function saveReserved(list) {
|
|
2913
|
+
if (!memRemote || typeof memRemote.setSettings !== "function") { setErr("宿主侧还没有 setSettings 接口(插件版本较旧)"); return; }
|
|
2914
|
+
setSaveBusy(true); setErr(null); setNotice("");
|
|
2915
|
+
Promise.resolve(memRemote.setSettings({ reserved: list })).then(function (out) {
|
|
2916
|
+
setSaveBusy(false);
|
|
2917
|
+
if (out && out.ok === false) { setErr(out.error || "名单被拒绝"); return; }
|
|
2918
|
+
if (out && out.settings) setMemSet(out.settings);
|
|
2919
|
+
else loadMemSettings();
|
|
2920
|
+
setNotice("保留名名单已保存 —— 需重启 DSH 才生效");
|
|
2921
|
+
}, function (e) { setSaveBusy(false); setErr(detail(e)); });
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function addReserved() {
|
|
2925
|
+
const name = String(reservedNew || "").trim();
|
|
2926
|
+
const extra = Array.isArray(memSet && memSet.reservedExtra) ? memSet.reservedExtra : [];
|
|
2927
|
+
if (!name) { setErr("先写一个文件名,例如 project_notes.md"); return; }
|
|
2928
|
+
// 与 host 的校验同一套:单个 .md 文件名、不许带路径
|
|
2929
|
+
if (!name.endsWith(".md") || name.includes("/") || name.includes("\\") || name.startsWith("..")) {
|
|
2930
|
+
setErr("保留名要是一个 .md 文件名(不能带路径):" + name);
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
if (extra.some(function (x) { return String(x).toLowerCase() === name.toLowerCase(); })) {
|
|
2934
|
+
setErr("名单里已经有 " + name + " 了");
|
|
2935
|
+
return;
|
|
2936
|
+
}
|
|
2937
|
+
setReservedNew("");
|
|
2938
|
+
saveReserved(extra.concat([name]));
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
function removeReserved(name) {
|
|
2942
|
+
const extra = Array.isArray(memSet && memSet.reservedExtra) ? memSet.reservedExtra : [];
|
|
2943
|
+
saveReserved(extra.filter(function (x) { return x !== name; }));
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2140
2946
|
/** 报告 / 回滚点 / 整理记录三份列表一起刷。 */
|
|
2141
2947
|
function refreshLists() {
|
|
2142
2948
|
callRemote("listReports", [], function (d) {
|
|
@@ -2226,7 +3032,24 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2226
3032
|
return encodeRoute(r.provider, r.model) === modelValue;
|
|
2227
3033
|
});
|
|
2228
3034
|
|
|
3035
|
+
// 二级分组:**运行 / 设置 / 记录**。
|
|
3036
|
+
// 这三类东西原来混在一条滚动里(实测约 1760px):跑一次要用的、要调的、回头看历史的
|
|
3037
|
+
// 全叠在一起,找什么东西都得滚。分段之后每段都落在一屏内(标尺:可视高度约 1000px)。
|
|
3038
|
+
// ⚠️ 分段状态放在 Section 里(props.seg),因为头部带的「待重启生效」徽标要能一步切到「设置」。
|
|
3039
|
+
const segOf = (props.seg === "settings" || props.seg === "records") ? props.seg : "run";
|
|
3040
|
+
const subtab = (name, label, note) => h("button", {
|
|
3041
|
+
type: "button", key: name, className: "smem-subtab" + (segOf === name ? " smem-subtab--on" : ""),
|
|
3042
|
+
"aria-current": segOf === name ? "true" : "false",
|
|
3043
|
+
onClick: function () { if (props.onSeg) props.onSeg(name); },
|
|
3044
|
+
}, [label, note ? h("span", { className: "smem-subtab-n", key: "n" }, note) : null]);
|
|
3045
|
+
|
|
2229
3046
|
return h("div", { className: "smem-autodream" }, [
|
|
3047
|
+
h("div", { className: "smem-subtabs", key: "segs" }, [
|
|
3048
|
+
subtab("run", "运行"),
|
|
3049
|
+
subtab("settings", "设置"),
|
|
3050
|
+
subtab("records", "记录", String(snaps.length + runs.length + reports.length)),
|
|
3051
|
+
]),
|
|
3052
|
+
segOf === "run" ? h("div", { className: "smem-seg", key: "seg-run" }, [
|
|
2230
3053
|
h("div", { className: "smem-autodream-sec", key: "top" }, [
|
|
2231
3054
|
h("div", { className: "smem-autodream-row", key: "r" }, [
|
|
2232
3055
|
h("button", {
|
|
@@ -2322,30 +3145,6 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2322
3145
|
? h("div", { className: "smem-autodream-note", key: "mnote" }, String(models.note))
|
|
2323
3146
|
: null,
|
|
2324
3147
|
]),
|
|
2325
|
-
h("details", { className: "smem-autodream-adv", key: "adv", open: true }, [
|
|
2326
|
-
h("summary", { key: "s" }, "高级设置"),
|
|
2327
|
-
h("div", { key: "d" }, [
|
|
2328
|
-
c.trigger === "auto" ? h("div", { className: "smem-autodream-row", key: "gate" }, [
|
|
2329
|
-
h("span", { className: "smem-autodream-key", key: "k" }, "自动触发"),
|
|
2330
|
-
h("span", { className: "smem-status", key: "a" }, "距上次 ≥"),
|
|
2331
|
-
h("input", { className: "smem-num", key: "h", type: "number", min: "1", max: "720", value: c.minHours, onChange: function (e) { patch({ minHours: Number(e.target.value) }); } }),
|
|
2332
|
-
h("span", { className: "smem-status", key: "b" }, "小时,且期间 ≥"),
|
|
2333
|
-
h("input", { className: "smem-num", key: "m", type: "number", min: "1", max: "200", value: c.minSessions, onChange: function (e) { patch({ minSessions: Number(e.target.value) }); } }),
|
|
2334
|
-
h("span", { className: "smem-status", key: "c" }, "个会话有更新"),
|
|
2335
|
-
]) : null,
|
|
2336
|
-
h("div", { className: "smem-autodream-row", key: "steps" }, [
|
|
2337
|
-
h("span", { className: "smem-autodream-key", key: "k" }, "最多轮数"),
|
|
2338
|
-
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "80", value: c.maxSteps, onChange: function (e) { patch({ maxSteps: Number(e.target.value) }); } }),
|
|
2339
|
-
h("span", { className: "smem-status", key: "s" }, "一轮 = 一次模型调用 + 它要的工具调用"),
|
|
2340
|
-
]),
|
|
2341
|
-
h("div", { className: "smem-autodream-row", key: "keep" }, [
|
|
2342
|
-
h("span", { className: "smem-autodream-key", key: "k" }, "保留快照"),
|
|
2343
|
-
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "50", value: c.maxSnapshotKeep, onChange: function (e) { patch({ maxSnapshotKeep: Number(e.target.value) }); } }),
|
|
2344
|
-
h("span", { className: "smem-status", key: "s" }, "份"),
|
|
2345
|
-
]),
|
|
2346
|
-
h("div", { className: "smem-status", key: "paths" }, "配置与状态:" + ((st && st.paths && st.paths.configPath) || "—")),
|
|
2347
|
-
]),
|
|
2348
|
-
]),
|
|
2349
3148
|
h("div", { className: "smem-autodream-sec", key: "run" }, [
|
|
2350
3149
|
h("div", { className: "smem-autodream-row", key: "r" }, [
|
|
2351
3150
|
h("button", {
|
|
@@ -2373,8 +3172,128 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2373
3172
|
(last.report ? " · 报告 " + last.report : ""))
|
|
2374
3173
|
: null,
|
|
2375
3174
|
]),
|
|
3175
|
+
]) : null,
|
|
3176
|
+
segOf === "settings" ? h("div", { className: "smem-seg", key: "seg-set" }, [
|
|
3177
|
+
h("div", { className: "smem-autodream-sec", key: "autoarchive" }, [
|
|
3178
|
+
h("div", { className: "smem-autodream-row", key: "r" }, [
|
|
3179
|
+
h("span", { className: "smem-autodream-key", key: "k" }, "自动归档"),
|
|
3180
|
+
h(Seg, {
|
|
3181
|
+
key: "v", value: autoArchiveOf(c.autoArchive),
|
|
3182
|
+
options: [
|
|
3183
|
+
{ value: "off", label: AUTO_ARCHIVE_LABEL.off },
|
|
3184
|
+
{ value: "report", label: AUTO_ARCHIVE_LABEL.report },
|
|
3185
|
+
{ value: "auto", label: AUTO_ARCHIVE_LABEL.auto },
|
|
3186
|
+
],
|
|
3187
|
+
// 点一档就存一档,但**存完要回读**:setConfig 对非法值是静默忽略,
|
|
3188
|
+
// 只信返回值不够(见 saveAutoArchive 的注释)。
|
|
3189
|
+
onChange: function (v) { if (v !== autoArchiveOf(c.autoArchive)) saveAutoArchive(v); },
|
|
3190
|
+
}),
|
|
3191
|
+
autoSaving ? h("span", { className: "smem-status", key: "s" }, "保存中…") : null,
|
|
3192
|
+
]),
|
|
3193
|
+
h("div", { className: "smem-autodream-note", key: "cost" }, AUTO_ARCHIVE_COST[autoArchiveOf(c.autoArchive)]),
|
|
3194
|
+
autoArchiveOf(c.autoArchive) === "auto" && !c.apply
|
|
3195
|
+
? h("div", { className: "smem-autodream-note smem-ad-warn", key: "deg" },
|
|
3196
|
+
"注意:现在「改动方式」是只出报告 —— 这一档会自动降级成只出报告,不会动文件。")
|
|
3197
|
+
: null,
|
|
3198
|
+
]),
|
|
3199
|
+
h("div", { className: "smem-autodream-sec", key: "limits" }, [
|
|
3200
|
+
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
3201
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, "记忆注入参数"),
|
|
3202
|
+
memSet && memSet.restartRequired
|
|
3203
|
+
? h("span", { className: "smem-ad-badge smem-ad-badge--warn", key: "w" }, "有改动待生效")
|
|
3204
|
+
: null,
|
|
3205
|
+
]),
|
|
3206
|
+
memSet && memSet.restartRequired
|
|
3207
|
+
? h("div", { className: "smem-autodream-note smem-ad-warn", key: "rw" },
|
|
3208
|
+
"⚠ 这些值在 DSH 启动时就固化了 —— 要重启 DSH 才生效。下面每行都写清「当前 → 重启后」。")
|
|
3209
|
+
: h("div", { className: "smem-autodream-note", key: "rn" },
|
|
3210
|
+
"这些值在 DSH 启动时固化:保存后要重启 DSH 才生效 —— 所以每行都会标出「当前生效」与「重启后」。"),
|
|
3211
|
+
// 五个参数排两列(一行两个):同样的信息,行数少一半,扫读反而更清楚。
|
|
3212
|
+
h("div", { className: "smem-ad-grid2", key: "grid" }, LIMIT_FIELDS.map(function (f) {
|
|
3213
|
+
const cur = memSet && memSet.limits ? memSet.limits[f.key] : null;
|
|
3214
|
+
const pend = memSet && memSet.pendingLimits ? memSet.pendingLimits[f.key] : null;
|
|
3215
|
+
const diff = cur != null && pend != null && cur !== pend;
|
|
3216
|
+
return h("div", { className: "smem-ad-field", key: f.key }, [
|
|
3217
|
+
h("span", { className: "smem-autodream-key", key: "k", title: f.unit + " · 范围 " + f.lo + "–" + f.hi }, f.label),
|
|
3218
|
+
h("input", {
|
|
3219
|
+
className: "smem-num", key: "v", type: "number",
|
|
3220
|
+
min: String(f.lo), max: String(f.hi),
|
|
3221
|
+
title: "范围 " + f.lo + "–" + f.hi + " " + f.unit,
|
|
3222
|
+
value: limitsDraft && limitsDraft[f.key] != null ? limitsDraft[f.key] : "",
|
|
3223
|
+
onChange: function (e) {
|
|
3224
|
+
const nx = Object.assign({}, limitsDraft);
|
|
3225
|
+
nx[f.key] = e.target.value;
|
|
3226
|
+
setLimitsDraft(nx);
|
|
3227
|
+
},
|
|
3228
|
+
}),
|
|
3229
|
+
diff
|
|
3230
|
+
? h("span", { className: "smem-ad-badge smem-ad-badge--warn", key: "d" }, "当前 " + String(cur) + " → 重启后 " + String(pend))
|
|
3231
|
+
: h("span", { className: "smem-status", key: "d" }, "范围 " + f.lo + "–" + f.hi + (cur == null ? "" : (" · 当前生效 " + String(cur)))),
|
|
3232
|
+
]);
|
|
3233
|
+
})),
|
|
3234
|
+
h("div", { className: "smem-autodream-row", key: "save" }, [
|
|
3235
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "s", disabled: saveBusy, onClick: saveLimits }, saveBusy ? "保存中…" : "保存注入参数"),
|
|
3236
|
+
h("span", { className: "smem-status", key: "n" }, "保存后需重启 DSH 才生效"),
|
|
3237
|
+
]),
|
|
3238
|
+
]),
|
|
3239
|
+
h("div", { className: "smem-autodream-sec", key: "reserved" }, [
|
|
3240
|
+
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
3241
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, "保留名保护名单"),
|
|
3242
|
+
h("span", { className: "smem-status", key: "c" }, "名单里的文件不参与注入"),
|
|
3243
|
+
]),
|
|
3244
|
+
h("div", { className: "smem-autodream-note", key: "n" },
|
|
3245
|
+
"内置的两个固定不可删;下面可以再加自己的(只写文件名,不要带路径)。改完需重启 DSH 才生效。"),
|
|
3246
|
+
h("div", { className: "smem-raw-names", key: "fixed" }, fixedReserved.map(function (n) {
|
|
3247
|
+
return h("span", { className: "smem-chip smem-chip--fixed", key: n, title: "内置固定,不可删除" }, n + " · 固定");
|
|
3248
|
+
})),
|
|
3249
|
+
h("div", { className: "smem-raw-names", key: "extra" },
|
|
3250
|
+
extraReserved.length
|
|
3251
|
+
? extraReserved.map(function (n) {
|
|
3252
|
+
return h("span", { className: "smem-chip", key: n, "data-on": "1" }, [
|
|
3253
|
+
n,
|
|
3254
|
+
h("button", { type: "button", className: "smem-chip-x", key: "x", title: "从名单里移除 " + n, onClick: function () { removeReserved(n); } }, "✕"),
|
|
3255
|
+
]);
|
|
3256
|
+
})
|
|
3257
|
+
: h("span", { className: "smem-status", key: "none" }, "(还没有自己加的保留名)")),
|
|
3258
|
+
h("div", { className: "smem-autodream-row", key: "add" }, [
|
|
3259
|
+
h("input", {
|
|
3260
|
+
className: "smem-input", key: "i",
|
|
3261
|
+
style: { flex: "1", minWidth: "150px" },
|
|
3262
|
+
value: reservedNew,
|
|
3263
|
+
placeholder: "例如 project_notes.md(要 .md 文件名)",
|
|
3264
|
+
onChange: function (e) { setReservedNew(e.target.value); },
|
|
3265
|
+
}),
|
|
3266
|
+
h("button", { type: "button", className: "smem-btn", key: "a", disabled: saveBusy, onClick: addReserved }, "加入名单"),
|
|
3267
|
+
]),
|
|
3268
|
+
]),
|
|
3269
|
+
h("details", { className: "smem-autodream-adv", key: "adv", open: true }, [
|
|
3270
|
+
h("summary", { key: "s" }, "高级设置"),
|
|
3271
|
+
h("div", { key: "d" }, [
|
|
3272
|
+
c.trigger === "auto" ? h("div", { className: "smem-autodream-row", key: "gate" }, [
|
|
3273
|
+
h("span", { className: "smem-autodream-key", key: "k" }, "自动触发"),
|
|
3274
|
+
h("span", { className: "smem-status", key: "a" }, "距上次 ≥"),
|
|
3275
|
+
h("input", { className: "smem-num", key: "h", type: "number", min: "1", max: "720", value: c.minHours, onChange: function (e) { patch({ minHours: Number(e.target.value) }); } }),
|
|
3276
|
+
h("span", { className: "smem-status", key: "b" }, "小时,且期间 ≥"),
|
|
3277
|
+
h("input", { className: "smem-num", key: "m", type: "number", min: "1", max: "200", value: c.minSessions, onChange: function (e) { patch({ minSessions: Number(e.target.value) }); } }),
|
|
3278
|
+
h("span", { className: "smem-status", key: "c" }, "个会话有更新"),
|
|
3279
|
+
]) : null,
|
|
3280
|
+
h("div", { className: "smem-autodream-row", key: "steps" }, [
|
|
3281
|
+
h("span", { className: "smem-autodream-key", key: "k" }, "最多轮数"),
|
|
3282
|
+
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "80", value: c.maxSteps, onChange: function (e) { patch({ maxSteps: Number(e.target.value) }); } }),
|
|
3283
|
+
h("span", { className: "smem-status", key: "s" }, "一轮 = 一次模型调用 + 它要的工具调用"),
|
|
3284
|
+
]),
|
|
3285
|
+
h("div", { className: "smem-autodream-row", key: "keep" }, [
|
|
3286
|
+
h("span", { className: "smem-autodream-key", key: "k" }, "保留快照"),
|
|
3287
|
+
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "50", value: c.maxSnapshotKeep, onChange: function (e) { patch({ maxSnapshotKeep: Number(e.target.value) }); } }),
|
|
3288
|
+
h("span", { className: "smem-status", key: "s" }, "份"),
|
|
3289
|
+
]),
|
|
3290
|
+
h("div", { className: "smem-status", key: "paths" }, "配置与状态:" + ((st && st.paths && st.paths.configPath) || "—")),
|
|
3291
|
+
]),
|
|
3292
|
+
]),
|
|
3293
|
+
]) : null,
|
|
2376
3294
|
// ── 回滚点(新)──────────────────────────────────────────────
|
|
2377
3295
|
// 只改这一个文件、只用既有 h() 写法;数据全走 remote.autodream。
|
|
3296
|
+
segOf === "records" ? h("div", { className: "smem-seg", key: "seg-rec" }, [
|
|
2378
3297
|
h("div", { className: "smem-autodream-sec", key: "snap" }, [
|
|
2379
3298
|
h("div", { className: "smem-autodream-row", key: "h" }, [
|
|
2380
3299
|
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, "回滚点"),
|
|
@@ -2485,6 +3404,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2485
3404
|
})),
|
|
2486
3405
|
open && body ? h("pre", { className: "smem-report-pre", key: "body" }, body) : null,
|
|
2487
3406
|
]),
|
|
3407
|
+
]) : null,
|
|
2488
3408
|
listErr && !err ? h("div", { className: "smem-err", key: "lerr" }, "⚠ " + listErr) : null,
|
|
2489
3409
|
err ? h("div", { className: "smem-err", key: "err" }, "⚠ " + err) : null,
|
|
2490
3410
|
notice && !err ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
@@ -2545,6 +3465,65 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2545
3465
|
const dirOk = sOk[0], setDirOk = sOk[1];
|
|
2546
3466
|
const sTab = react.useState("files");
|
|
2547
3467
|
const tab = sTab[0], setTab = sTab[1];
|
|
3468
|
+
// ── 档案馆(v0.9.0)──
|
|
3469
|
+
// 归档不是「另一个视图」,而是和类型 / 标签并列的**筛选维度**:
|
|
3470
|
+
// 状态(全部 / 活动中 / 已归档)。三组筛选组内 OR、组间 AND,再与搜索框 AND。
|
|
3471
|
+
const sStatus = react.useState("all");
|
|
3472
|
+
const statusFilter = sStatus[0], setStatusFilter = sStatus[1];
|
|
3473
|
+
const sArchive = react.useState([]);
|
|
3474
|
+
const archiveItems = sArchive[0], setArchiveItems = sArchive[1];
|
|
3475
|
+
const sArchiving = react.useState(null);
|
|
3476
|
+
const archiving = sArchiving[0], setArchiving = sArchiving[1];
|
|
3477
|
+
const sArchiveReason = react.useState("");
|
|
3478
|
+
const archiveReason = sArchiveReason[0], setArchiveReason = sArchiveReason[1];
|
|
3479
|
+
const sRestoring = react.useState(null);
|
|
3480
|
+
const restoring = sRestoring[0], setRestoring = sRestoring[1];
|
|
3481
|
+
// 归档文件的「原样文本」编辑器:正在编辑谁、编辑框里是什么、读/存进行中
|
|
3482
|
+
const sArchEdit = react.useState(null);
|
|
3483
|
+
const archEdit = sArchEdit[0], setArchEdit = sArchEdit[1];
|
|
3484
|
+
const sArchText = react.useState("");
|
|
3485
|
+
const archText = sArchText[0], setArchText = sArchText[1];
|
|
3486
|
+
const sArchLoad = react.useState(false);
|
|
3487
|
+
const archLoad = sArchLoad[0], setArchLoad = sArchLoad[1];
|
|
3488
|
+
const sArchSaving = react.useState(false);
|
|
3489
|
+
const archSaving = sArchSaving[0], setArchSaving = sArchSaving[1];
|
|
3490
|
+
// ── 工具面板(体检 / 归档候选 / 保留名原样编辑)──
|
|
3491
|
+
// 三个入口收进「工具 ▾」抽屉:关着时零占位,点开才占版面。
|
|
3492
|
+
// 面板一次只开一个(tool 状态),列表始终留在下面。
|
|
3493
|
+
// ⚠️ 抽屉本身不负责「可发现」——那是头部带上常显徽标的活。所以 audit / settings
|
|
3494
|
+
// 在面板挂载时就各拉一次(都只读):体检有几个问题、有没有待重启项,不打开也看得见。
|
|
3495
|
+
const sDrawer = react.useState(false);
|
|
3496
|
+
const drawer = sDrawer[0], setDrawer = sDrawer[1];
|
|
3497
|
+
// 自动做梦的二级分段状态放在这里(不放面板内部):头部带的「待重启生效」徽标
|
|
3498
|
+
// 点一下就切到自动做梦·设置那一段,需要跨组件指路。
|
|
3499
|
+
const sAdSeg = react.useState("run");
|
|
3500
|
+
const adSeg = sAdSeg[0], setAdSeg = sAdSeg[1];
|
|
3501
|
+
const sTool = react.useState(null);
|
|
3502
|
+
const tool = sTool[0], setTool = sTool[1];
|
|
3503
|
+
const sAudit = react.useState(null);
|
|
3504
|
+
const audit = sAudit[0], setAudit = sAudit[1];
|
|
3505
|
+
const sAuditBusy = react.useState(false);
|
|
3506
|
+
const auditBusy = sAuditBusy[0], setAuditBusy = sAuditBusy[1];
|
|
3507
|
+
const sCand = react.useState(null);
|
|
3508
|
+
const cand = sCand[0], setCand = sCand[1];
|
|
3509
|
+
const sCandBusy = react.useState(false);
|
|
3510
|
+
const candBusy = sCandBusy[0], setCandBusy = sCandBusy[1];
|
|
3511
|
+
const sPicked = react.useState({});
|
|
3512
|
+
const picked = sPicked[0], setPicked = sPicked[1];
|
|
3513
|
+
const sCandConfirm = react.useState(null);
|
|
3514
|
+
const candConfirm = sCandConfirm[0], setCandConfirm = sCandConfirm[1];
|
|
3515
|
+
const sSettings = react.useState(null);
|
|
3516
|
+
const settings = sSettings[0], setSettings = sSettings[1];
|
|
3517
|
+
const sRawName = react.useState(null);
|
|
3518
|
+
const rawName = sRawName[0], setRawName = sRawName[1];
|
|
3519
|
+
const sRawText = react.useState("");
|
|
3520
|
+
const rawText = sRawText[0], setRawText = sRawText[1];
|
|
3521
|
+
const sRawIsNew = react.useState(false);
|
|
3522
|
+
const rawIsNew = sRawIsNew[0], setRawIsNew = sRawIsNew[1];
|
|
3523
|
+
const sRawBusy = react.useState(false);
|
|
3524
|
+
const rawBusy = sRawBusy[0], setRawBusy = sRawBusy[1];
|
|
3525
|
+
const sRawLoading = react.useState(false);
|
|
3526
|
+
const rawLoading = sRawLoading[0], setRawLoading = sRawLoading[1];
|
|
2548
3527
|
const renderSlot = props.renderSlot;
|
|
2549
3528
|
|
|
2550
3529
|
function load() {
|
|
@@ -2562,9 +3541,45 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2562
3541
|
})
|
|
2563
3542
|
.catch(function (e) { setError(String((e && e.message) || e)); setDirOk(false); })
|
|
2564
3543
|
.then(function () { setLoading(false); });
|
|
3544
|
+
// 归档区与活动列表分开拉:归档区读不到不该拖垮活动列表。
|
|
3545
|
+
// 旧版宿主没有 listArchived → 界面退化成一个空档案馆,不报错。
|
|
3546
|
+
if (typeof remote.listArchived !== "function") return;
|
|
3547
|
+
Promise.resolve()
|
|
3548
|
+
.then(function () { return remote.listArchived(); })
|
|
3549
|
+
.then(function (res) {
|
|
3550
|
+
const d = unwrap(res);
|
|
3551
|
+
setArchiveItems(d && Array.isArray(d.files) ? d.files : []);
|
|
3552
|
+
})
|
|
3553
|
+
.catch(function () { setArchiveItems([]); });
|
|
2565
3554
|
}
|
|
2566
3555
|
|
|
2567
|
-
react.useEffect(function () {
|
|
3556
|
+
react.useEffect(function () {
|
|
3557
|
+
load();
|
|
3558
|
+
// 头部带的两个徽标要有数据才立得住:体检(只读扫描)与待重启项。
|
|
3559
|
+
runAudit();
|
|
3560
|
+
loadSettings();
|
|
3561
|
+
}, []);
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* 抽屉「点外部 / Esc 关闭」。
|
|
3565
|
+
*
|
|
3566
|
+
* ⚠️ 监听**只在展开时挂**(依赖 [drawer]):关着时一个 document 监听都没有 ——
|
|
3567
|
+
* 「点外部关闭」绝不能变成「点哪儿都没反应」。cleanup 里成对摘掉,不留悬挂监听。
|
|
3568
|
+
* 用 click 而不是 pointerdown:宿主的弹窗里 pointerdown 可能被别的控件的拖拽吃掉。
|
|
3569
|
+
* 点抽屉里的条目也一样会关(条目自己的 onClick 先把它设成 false,这里再设一次无害)。
|
|
3570
|
+
*/
|
|
3571
|
+
react.useEffect(function () {
|
|
3572
|
+
if (!drawer) return undefined;
|
|
3573
|
+
if (typeof document === "undefined" || !document.addEventListener) return undefined;
|
|
3574
|
+
function onOutside() { setDrawer(false); }
|
|
3575
|
+
function onKey(e) { if (e && (e.key === "Escape" || e.keyCode === 27)) setDrawer(false); }
|
|
3576
|
+
document.addEventListener("click", onOutside);
|
|
3577
|
+
document.addEventListener("keydown", onKey);
|
|
3578
|
+
return function () {
|
|
3579
|
+
document.removeEventListener("click", onOutside);
|
|
3580
|
+
document.removeEventListener("keydown", onKey);
|
|
3581
|
+
};
|
|
3582
|
+
}, [drawer]);
|
|
2568
3583
|
|
|
2569
3584
|
function closeForm() {
|
|
2570
3585
|
setFormOpen(false); setEditingFile(null); setFormName(""); setFormDesc(""); setFormBody(""); setFormType("feedback");
|
|
@@ -2660,13 +3675,251 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2660
3675
|
.then(function () { setBusy(false); });
|
|
2661
3676
|
}
|
|
2662
3677
|
|
|
3678
|
+
/**
|
|
3679
|
+
* 归档:把一条记忆移进档案馆。
|
|
3680
|
+
*
|
|
3681
|
+
* 与删除的区别要在界面上说清 —— 归档是「静默失效且可回退」,删除是真的没了。
|
|
3682
|
+
* 所以这一问问的是「为什么」(留痕给以后的自己看),不是「确定吗」。
|
|
3683
|
+
*/
|
|
3684
|
+
function confirmArchive(file) {
|
|
3685
|
+
if (typeof remote.archive !== "function") {
|
|
3686
|
+
setError("宿主侧还没有 archive 接口(插件版本较旧)");
|
|
3687
|
+
return;
|
|
3688
|
+
}
|
|
3689
|
+
setBusy(true); setError(null); setNotice("");
|
|
3690
|
+
remote.archive(file, archiveReason.trim())
|
|
3691
|
+
.then(function (r) {
|
|
3692
|
+
if (r && r.ok === false) { setError(r.error || "归档被拒绝"); return; }
|
|
3693
|
+
setArchiving(null);
|
|
3694
|
+
setArchiveReason("");
|
|
3695
|
+
setNotice("已归档 " + file + " —— 它现在在「已归档」视图里,随时可以恢复");
|
|
3696
|
+
load();
|
|
3697
|
+
})
|
|
3698
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3699
|
+
.then(function () { setBusy(false); });
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
/** 从档案馆捞回来:文件移回记忆目录,归档留痕被抹掉。 */
|
|
3703
|
+
function restoreFile(file) {
|
|
3704
|
+
if (typeof remote.restore !== "function") {
|
|
3705
|
+
setError("宿主侧还没有 restore 接口(插件版本较旧)");
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
setBusy(true); setError(null); setNotice("");
|
|
3709
|
+
setRestoring(file);
|
|
3710
|
+
remote.restore(file)
|
|
3711
|
+
.then(function (r) {
|
|
3712
|
+
if (r && r.ok === false) { setError(r.error || "恢复被拒绝"); return; }
|
|
3713
|
+
setNotice("已恢复 " + file + " —— 它回到了活动记忆里");
|
|
3714
|
+
load();
|
|
3715
|
+
})
|
|
3716
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3717
|
+
.then(function () { setBusy(false); setRestoring(null); });
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
/**
|
|
3721
|
+
* 打开归档文件的**原样文本**编辑器。
|
|
3722
|
+
*
|
|
3723
|
+
* 读回来的 content 一字不改地放进编辑框;保存时也一字不改地交回去。
|
|
3724
|
+
* 这是刻意的:活跃记忆那套表单会用 buildFrontmatter 重建 frontmatter,
|
|
3725
|
+
* 它会吞掉自己不认识的字段(曾经的数据事故);归档文件里还多两行
|
|
3726
|
+
* archived_at / archived_reason,重建就是丢留痕。
|
|
3727
|
+
*/
|
|
3728
|
+
function openArchEdit(file) {
|
|
3729
|
+
if (typeof remote.readArchived !== "function") {
|
|
3730
|
+
setError("宿主侧还没有 readArchived 接口(插件版本较旧)");
|
|
3731
|
+
return;
|
|
3732
|
+
}
|
|
3733
|
+
setError(null); setNotice("");
|
|
3734
|
+
setArchEdit(file);
|
|
3735
|
+
setArchText("");
|
|
3736
|
+
setArchLoad(true);
|
|
3737
|
+
remote.readArchived(file)
|
|
3738
|
+
.then(function (r) {
|
|
3739
|
+
const d = unwrap(r);
|
|
3740
|
+
setArchText(d && typeof d.content === "string" ? d.content : "");
|
|
3741
|
+
setArchLoad(false);
|
|
3742
|
+
})
|
|
3743
|
+
.catch(function (e) {
|
|
3744
|
+
setError(String((e && e.message) || e));
|
|
3745
|
+
setArchLoad(false);
|
|
3746
|
+
setArchEdit(null);
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
function saveArchEdit(file) {
|
|
3751
|
+
if (typeof remote.writeArchived !== "function") {
|
|
3752
|
+
setError("宿主侧还没有 writeArchived 接口(插件版本较旧)");
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
setArchSaving(true); setError(null); setNotice("");
|
|
3756
|
+
// 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
|
|
3757
|
+
remote.writeArchived(file, archText)
|
|
3758
|
+
.then(function (r) {
|
|
3759
|
+
if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
|
|
3760
|
+
setArchEdit(null);
|
|
3761
|
+
setNotice("已保存 " + file);
|
|
3762
|
+
load();
|
|
3763
|
+
})
|
|
3764
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3765
|
+
.then(function () { setArchSaving(false); });
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
function cancelArchEdit() { setArchEdit(null); setArchText(""); }
|
|
3769
|
+
|
|
3770
|
+
// ── 工具面板的三个数据通道 ──────────────────────────────────────
|
|
3771
|
+
|
|
3772
|
+
function runAudit() {
|
|
3773
|
+
if (typeof remote.audit !== "function") { setError("宿主侧还没有 audit 接口(插件版本较旧)"); return; }
|
|
3774
|
+
setAuditBusy(true); setError(null);
|
|
3775
|
+
remote.audit()
|
|
3776
|
+
.then(function (r) { setAudit(unwrap(r) || null); })
|
|
3777
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3778
|
+
.then(function () { setAuditBusy(false); });
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
function loadCandidates() {
|
|
3782
|
+
if (typeof remote.archiveCandidates !== "function") { setError("宿主侧还没有 archiveCandidates 接口(插件版本较旧)"); return; }
|
|
3783
|
+
setCandBusy(true); setError(null);
|
|
3784
|
+
remote.archiveCandidates()
|
|
3785
|
+
.then(function (r) {
|
|
3786
|
+
const d = unwrap(r) || {};
|
|
3787
|
+
setCand(d);
|
|
3788
|
+
// 候选没了就把选中项一起清掉,免得留下指向不存在文件的对勾
|
|
3789
|
+
const keep = {};
|
|
3790
|
+
(d.candidates || []).forEach(function (x) { if (picked[x.file]) keep[x.file] = true; });
|
|
3791
|
+
setPicked(keep);
|
|
3792
|
+
setCandConfirm(null);
|
|
3793
|
+
})
|
|
3794
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3795
|
+
.then(function () { setCandBusy(false); });
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
function loadSettings() {
|
|
3799
|
+
if (typeof remote.getSettings !== "function") { setError("宿主侧还没有 getSettings 接口(插件版本较旧)"); return; }
|
|
3800
|
+
remote.getSettings()
|
|
3801
|
+
.then(function (r) { const d = unwrap(r) || {}; setSettings(d); })
|
|
3802
|
+
.catch(function (e) { setError(String((e && e.message) || e)); });
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
/**
|
|
3806
|
+
* 打开 / 收起一个工具面板。数据只在第一次打开时拉;已经拉过就复用
|
|
3807
|
+
* (「刷新」「重新体检」是显式动作,别替用户反复扫目录)。
|
|
3808
|
+
*/
|
|
3809
|
+
function toggleTool(name) {
|
|
3810
|
+
const next = tool === name ? null : name;
|
|
3811
|
+
setTool(next);
|
|
3812
|
+
setError(null);
|
|
3813
|
+
if (next === "audit" && !audit) runAudit();
|
|
3814
|
+
if (next === "candidates" && !cand) loadCandidates();
|
|
3815
|
+
if (next === "raw" && !settings) loadSettings();
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
/** 打开某个工具(头部带的徽标点进来时用:不切换、直接开)。 */
|
|
3819
|
+
function openTool(name) {
|
|
3820
|
+
setTool(name);
|
|
3821
|
+
setError(null);
|
|
3822
|
+
if (name === "audit" && !audit) runAudit();
|
|
3823
|
+
if (name === "candidates" && !cand) loadCandidates();
|
|
3824
|
+
if (name === "raw" && !settings) loadSettings();
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
/**
|
|
3828
|
+
* 执行归档(逐条 / 批量都走这里)。reason 用候选算出来的那个
|
|
3829
|
+
* —— 归档留痕要写清「为什么」,不能把模型/界面的判断丢掉。
|
|
3830
|
+
*/
|
|
3831
|
+
function archiveCandidates(list) {
|
|
3832
|
+
if (!list.length) return;
|
|
3833
|
+
if (typeof remote.archive !== "function") { setError("宿主侧还没有 archive 接口(插件版本较旧)"); return; }
|
|
3834
|
+
setBusy(true); setError(null); setNotice("");
|
|
3835
|
+
const done = [];
|
|
3836
|
+
const failed = [];
|
|
3837
|
+
const step = function (i) {
|
|
3838
|
+
if (i >= list.length) {
|
|
3839
|
+
setBusy(false);
|
|
3840
|
+
setCandConfirm(null);
|
|
3841
|
+
setPicked({});
|
|
3842
|
+
setNotice(failed.length
|
|
3843
|
+
? ("已归档 " + done.length + " 条," + failed.length + " 条没成功:" + failed.join("、"))
|
|
3844
|
+
: ("已归档 " + done.length + " 条 —— 它们现在在档案馆里,随时可以恢复"));
|
|
3845
|
+
load();
|
|
3846
|
+
loadCandidates();
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
const c = list[i];
|
|
3850
|
+
Promise.resolve()
|
|
3851
|
+
.then(function () { return remote.archive(c.file, c.reason); })
|
|
3852
|
+
.then(function (r) {
|
|
3853
|
+
if (r && r.ok === false) failed.push(c.file); else done.push(c.file);
|
|
3854
|
+
}, function () { failed.push(c.file); })
|
|
3855
|
+
.then(function () { step(i + 1); });
|
|
3856
|
+
};
|
|
3857
|
+
step(0);
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
function openRaw(name) {
|
|
3861
|
+
if (typeof remote.readRaw !== "function") { setError("宿主侧还没有 readRaw 接口(插件版本较旧)"); return; }
|
|
3862
|
+
setError(null); setNotice("");
|
|
3863
|
+
setRawName(name);
|
|
3864
|
+
setRawText("");
|
|
3865
|
+
setRawIsNew(false);
|
|
3866
|
+
setRawLoading(true);
|
|
3867
|
+
remote.readRaw(name)
|
|
3868
|
+
.then(function (r) {
|
|
3869
|
+
const d = unwrap(r);
|
|
3870
|
+
setRawText(d && typeof d.content === "string" ? d.content : "");
|
|
3871
|
+
setRawLoading(false);
|
|
3872
|
+
})
|
|
3873
|
+
.catch(function (e) {
|
|
3874
|
+
const msg = String((e && e.message) || e);
|
|
3875
|
+
// 文件还不存在 → 允许**新建**(索引/流水这类文件此前只能靠文件工具建,
|
|
3876
|
+
// 那正是「只有模型能操作」的一处)。但必须让用户**看见**这是新建:
|
|
3877
|
+
// 静默开一个空编辑框,用户会以为原文件是空的,一存就把内容顶掉。
|
|
3878
|
+
if (/ENOENT|no such file|not exist/i.test(msg)) {
|
|
3879
|
+
setRawText("");
|
|
3880
|
+
setRawIsNew(true);
|
|
3881
|
+
setRawLoading(false);
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
setError("读不到 " + name + ":" + msg);
|
|
3885
|
+
setRawLoading(false);
|
|
3886
|
+
setRawName(null);
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
function saveRaw(name) {
|
|
3891
|
+
if (typeof remote.writeRaw !== "function") { setError("宿主侧还没有 writeRaw 接口(插件版本较旧)"); return; }
|
|
3892
|
+
setRawBusy(true); setError(null); setNotice("");
|
|
3893
|
+
// 原样交回:不 trim、不补 frontmatter、不做任何「智能」处理。
|
|
3894
|
+
remote.writeRaw(name, rawText)
|
|
3895
|
+
.then(function (r) {
|
|
3896
|
+
if (r && r.ok === false) { setError(r.error || "保存被拒绝"); return; }
|
|
3897
|
+
setNotice((rawIsNew ? "已新建 " : "已保存 ") + name);
|
|
3898
|
+
setRawIsNew(false);
|
|
3899
|
+
})
|
|
3900
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
3901
|
+
.then(function () { setRawBusy(false); });
|
|
3902
|
+
}
|
|
2663
3903
|
// ── 筛选条的数据 ──
|
|
3904
|
+
// 状态是筛选维度之一:先按状态取一批,类型 / 标签 / 搜索都在这一批里继续收窄。
|
|
3905
|
+
// 组内 OR、组间 AND —— 与「类型 × 标签」原有的语义完全一致。
|
|
3906
|
+
// 归档条目在这里打一个本地 archived 标记(宿主 listArchived 的形状不动),
|
|
3907
|
+
// 列表渲染靠它决定用哪张卡;顺序是**活动在前、归档在后**。
|
|
3908
|
+
const archivedTagged = archiveItems.map(function (x) {
|
|
3909
|
+
return Object.assign({}, x, { archived: true });
|
|
3910
|
+
});
|
|
3911
|
+
const statusItems = statusFilter === "archived"
|
|
3912
|
+
? archivedTagged
|
|
3913
|
+
: statusFilter === "active"
|
|
3914
|
+
? items
|
|
3915
|
+
: items.concat(archivedTagged);
|
|
3916
|
+
const totalCount = items.length + archiveItems.length;
|
|
2664
3917
|
// 计数一律按**全量**算,不随当前筛选变化 —— 否则一筛下去别的类型就都显示 0,
|
|
2665
3918
|
// 用户没法知道「换个类型还有多少条」。
|
|
2666
3919
|
const typeCounts = {};
|
|
2667
3920
|
const tagCounts = {};
|
|
2668
|
-
for (let i = 0; i <
|
|
2669
|
-
const it =
|
|
3921
|
+
for (let i = 0; i < statusItems.length; i++) {
|
|
3922
|
+
const it = statusItems[i];
|
|
2670
3923
|
const t = it.type || "reference";
|
|
2671
3924
|
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
2672
3925
|
const tg = Array.isArray(it.tags) ? it.tags : [];
|
|
@@ -2686,7 +3939,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2686
3939
|
});
|
|
2687
3940
|
|
|
2688
3941
|
const q = query.trim().toLowerCase();
|
|
2689
|
-
const filtered =
|
|
3942
|
+
const filtered = statusItems.filter(function (it) {
|
|
2690
3943
|
if (typeFilter.length && typeFilter.indexOf(it.type || "reference") < 0) return false;
|
|
2691
3944
|
if (tagFilter.length) {
|
|
2692
3945
|
const tg = Array.isArray(it.tags) ? it.tags : [];
|
|
@@ -2701,7 +3954,85 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2701
3954
|
(Array.isArray(it.tags) ? it.tags.join(" ") : "")).toLowerCase();
|
|
2702
3955
|
return hay.indexOf(q) >= 0;
|
|
2703
3956
|
});
|
|
2704
|
-
const hasFilter = !!(typeFilter.length || tagFilter.length || q);
|
|
3957
|
+
const hasFilter = !!(statusFilter !== "all" || typeFilter.length || tagFilter.length || q);
|
|
3958
|
+
// 两个计数永远一起露脸:用户不必先筛状态才知道档案馆里有没有东西。
|
|
3959
|
+
const headerCount = (hasFilter ? "筛出 " + String(filtered.length) + " / " + String(statusItems.length) + " · " : "") +
|
|
3960
|
+
"活动中 " + String(items.length) + " · 已归档 " + String(archiveItems.length);
|
|
3961
|
+
// 头部带的「待重启生效」徽标:有多少项要重启才生效,从 limits / pendingLimits 的差算出来
|
|
3962
|
+
// (reserved 名单的变化 getSettings 只给 restartRequired 布尔值,数不出来就不写数)。
|
|
3963
|
+
const LIMIT_KEYS = ["maxResults", "maxChars", "maxBaseline", "maxSessionBytes", "staleDays"];
|
|
3964
|
+
const pendingDiff = (settings && settings.limits && settings.pendingLimits)
|
|
3965
|
+
? LIMIT_KEYS.filter(function (k) { return settings.limits[k] !== settings.pendingLimits[k]; }).length
|
|
3966
|
+
: 0;
|
|
3967
|
+
const restartPending = !!(settings && settings.restartRequired);
|
|
3968
|
+
|
|
3969
|
+
// 「文件列表」这一屏:状态筛选决定看哪一批,灰色卡片与留痕块自带「退役」语义。
|
|
3970
|
+
// banner 只在**专门看归档**时出现,切到全部时不重复说一遍。
|
|
3971
|
+
let filesView;
|
|
3972
|
+
if (loading && totalCount === 0) {
|
|
3973
|
+
filesView = h("div", { className: "smem-muted", key: "loading" }, "加载中…");
|
|
3974
|
+
} else if (totalCount === 0) {
|
|
3975
|
+
filesView = h("div", { className: "smem-empty", key: "empty" },
|
|
3976
|
+
"还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。");
|
|
3977
|
+
} else if (statusFilter === "archived" && archiveItems.length === 0) {
|
|
3978
|
+
filesView = h("div", { className: "smem-empty", key: "empty" },
|
|
3979
|
+
"档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。");
|
|
3980
|
+
} else if (statusFilter === "active" && items.length === 0) {
|
|
3981
|
+
filesView = h("div", { className: "smem-empty", key: "empty" },
|
|
3982
|
+
"活动记忆是空的 —— 状态筛到「已归档」还能捞出 " + archiveItems.length + " 条。");
|
|
3983
|
+
} else if (filtered.length === 0) {
|
|
3984
|
+
filesView = h("div", { className: "smem-empty", key: "empty" }, "没有符合搜索或筛选条件的文件。");
|
|
3985
|
+
} else {
|
|
3986
|
+
const list = h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
3987
|
+
if (it.archived === true) {
|
|
3988
|
+
return h(ArchivedCard, {
|
|
3989
|
+
key: it.file,
|
|
3990
|
+
item: it,
|
|
3991
|
+
busy: busy,
|
|
3992
|
+
restoring: restoring === it.file,
|
|
3993
|
+
editing: archEdit === it.file,
|
|
3994
|
+
loading: archLoad && archEdit === it.file,
|
|
3995
|
+
saving: archSaving && archEdit === it.file,
|
|
3996
|
+
text: archText,
|
|
3997
|
+
onText: setArchText,
|
|
3998
|
+
onEdit: openArchEdit,
|
|
3999
|
+
onSave: saveArchEdit,
|
|
4000
|
+
onCancel: cancelArchEdit,
|
|
4001
|
+
onRestore: restoreFile,
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
return h(Card, {
|
|
4005
|
+
key: it.file,
|
|
4006
|
+
item: it,
|
|
4007
|
+
expanded: !!expanded[it.file],
|
|
4008
|
+
deleting: deleting === it.file,
|
|
4009
|
+
archiving: archiving === it.file,
|
|
4010
|
+
archiveReason: archiveReason,
|
|
4011
|
+
busy: busy,
|
|
4012
|
+
onArchiveReason: setArchiveReason,
|
|
4013
|
+
onAskArchive: function (file) { setArchiving(file); setArchiveReason(""); setDeleting(null); },
|
|
4014
|
+
onCancelArchive: function () { setArchiving(null); setArchiveReason(""); },
|
|
4015
|
+
onConfirmArchive: confirmArchive,
|
|
4016
|
+
onToggle: function (file) {
|
|
4017
|
+
const nx = {};
|
|
4018
|
+
nx[file] = !expanded[file];
|
|
4019
|
+
setExpanded(Object.assign({}, expanded, nx));
|
|
4020
|
+
},
|
|
4021
|
+
onEdit: openEdit,
|
|
4022
|
+
onAskDelete: function (file) { setDeleting(file); setArchiving(null); },
|
|
4023
|
+
onCancelDelete: function () { setDeleting(null); },
|
|
4024
|
+
onConfirmDelete: confirmDelete,
|
|
4025
|
+
});
|
|
4026
|
+
}));
|
|
4027
|
+
filesView = statusFilter === "archived"
|
|
4028
|
+
? [
|
|
4029
|
+
h("div", { className: "smem-ar-banner", key: "banner" },
|
|
4030
|
+
"档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
|
|
4031
|
+
"「编辑」是原样文本编辑,改完存回同一份文件。"),
|
|
4032
|
+
list,
|
|
4033
|
+
]
|
|
4034
|
+
: list;
|
|
4035
|
+
}
|
|
2705
4036
|
|
|
2706
4037
|
return h("div", { className: "smem-root" }, [
|
|
2707
4038
|
h("div", { className: "smem-tabs", key: "tabs" }, [
|
|
@@ -2709,30 +4040,143 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2709
4040
|
h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", onClick: function () { setTab("star"); } }, "记忆星图"),
|
|
2710
4041
|
h("button", { type: "button", className: "smem-tab" + (tab === "autodream" ? " smem-tab--on" : ""), key: "d", onClick: function () { setTab("autodream"); } }, "自动做梦"),
|
|
2711
4042
|
]),
|
|
4043
|
+
// 三个 tab 共用的一条头部带:我在哪 · 目录状态 · 活动/归档计数 · 体检徽标 · 待重启徽标 · 主操作。
|
|
4044
|
+
// 层级形状固定:导航=下划线(.smem-tabs)、筛选=chip、动作=按钮,三种不混用。
|
|
2712
4045
|
h("div", { className: "smem-head", key: "head" }, [
|
|
2713
|
-
h("span", { className: "smem-title", key: "t" }, "
|
|
4046
|
+
h("span", { className: "smem-title", key: "t" }, "记忆管理"),
|
|
4047
|
+
h("span", { className: "smem-head-job", key: "job" }, TAB_LABEL[tab] || ""),
|
|
2714
4048
|
h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
|
|
2715
4049
|
h("span", { className: "smem-muted", key: "st" }, dirOk ? "目录可读" : "目录不可读"),
|
|
2716
|
-
h("span", { className: "smem-badge", key: "cnt" },
|
|
2717
|
-
|
|
2718
|
-
|
|
4050
|
+
h("span", { className: "smem-badge", key: "cnt" }, headerCount),
|
|
4051
|
+
// 体检徽标:**抽屉关着也一直看得见**。整个「工具收进抽屉」的方案就靠它成立 ——
|
|
4052
|
+
// 没有这个徽标,三个能力就退回「藏起来」了(上一轮刚把它们做成用户自己的入口)。
|
|
4053
|
+
h("button", {
|
|
4054
|
+
type: "button", key: "aud",
|
|
4055
|
+
className: "smem-tool-badge" + (audit && audit.problems ? " smem-tool-badge--bad" : ""),
|
|
4056
|
+
title: audit ? (audit.problems ? "打开体检看是哪几条" : "体检没有发现问题") : "跑一次体检(只读)",
|
|
4057
|
+
onClick: function () { openTool("audit"); },
|
|
4058
|
+
}, audit ? (audit.problems ? ("体检 " + audit.problems + " 个问题") : "体检 通过") : "体检 · 未跑"),
|
|
4059
|
+
restartPending
|
|
4060
|
+
? h("button", {
|
|
4061
|
+
type: "button", key: "rs", className: "smem-tool-badge smem-ad-badge--warn",
|
|
4062
|
+
title: "有改动要重启 DSH 才生效",
|
|
4063
|
+
onClick: function () { setTab("autodream"); setAdSeg("settings"); },
|
|
4064
|
+
}, (pendingDiff ? (pendingDiff + " 项") : "") + "待重启生效")
|
|
4065
|
+
: null,
|
|
4066
|
+
// 主操作:只放当前 tab 的那一个(星图/自动做梦的主操作在各自面板里,不重复摆一份)。
|
|
4067
|
+
h("span", { className: "smem-head-actions", key: "act" }, tab === "files" ? [
|
|
4068
|
+
h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
|
|
4069
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
|
|
4070
|
+
] : null),
|
|
2719
4071
|
]),
|
|
2720
|
-
//
|
|
2721
|
-
// 放在 tab 分支外面,会让星图和自动做梦页也顶着一排用不上的控件
|
|
2722
|
-
// (走查时发现:「+ 添加记忆」出现在星图页上)。
|
|
4072
|
+
// 工具栏只剩「搜索 + 工具 ▾」:刷新与添加已经收进头部带的主操作位。
|
|
2723
4073
|
tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
|
|
2724
4074
|
h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
|
|
2725
|
-
|
|
2726
|
-
h("
|
|
4075
|
+
// 工具抽屉:关着时这里只有一个按钮,零占位;面板一次只开一个。
|
|
4076
|
+
h("span", { className: "smem-drawer", key: "drawer" }, [
|
|
4077
|
+
h("button", {
|
|
4078
|
+
type: "button", key: "btn",
|
|
4079
|
+
className: "smem-btn" + (drawer ? " smem-btn-primary" : ""),
|
|
4080
|
+
"aria-expanded": drawer ? "true" : "false",
|
|
4081
|
+
title: "体检 / 归档候选 / 保留名编辑",
|
|
4082
|
+
onClick: function () { setDrawer(!drawer); },
|
|
4083
|
+
}, "工具 ▾"),
|
|
4084
|
+
drawer ? h("div", { className: "smem-drawer-pop", key: "pop" }, [
|
|
4085
|
+
h("button", {
|
|
4086
|
+
type: "button", className: "smem-drawer-item", key: "a",
|
|
4087
|
+
"data-on": tool === "audit" ? "1" : "0",
|
|
4088
|
+
onClick: function () { toggleTool("audit"); setDrawer(false); },
|
|
4089
|
+
}, ["体检", h("span", { className: "smem-status" + (audit && audit.problems ? " smem-status--bad" : ""), key: "s" }, audit ? (audit.problems ? (audit.problems + " 个问题") : "通过") : "未跑")]),
|
|
4090
|
+
h("button", {
|
|
4091
|
+
type: "button", className: "smem-drawer-item", key: "c",
|
|
4092
|
+
"data-on": tool === "candidates" ? "1" : "0",
|
|
4093
|
+
onClick: function () { toggleTool("candidates"); setDrawer(false); },
|
|
4094
|
+
}, ["归档候选", h("span", { className: "smem-status", key: "s" }, cand ? (cand.count + " 条") : "未取")]),
|
|
4095
|
+
h("button", {
|
|
4096
|
+
type: "button", className: "smem-drawer-item", key: "r",
|
|
4097
|
+
"data-on": tool === "raw" ? "1" : "0",
|
|
4098
|
+
onClick: function () { toggleTool("raw"); setDrawer(false); },
|
|
4099
|
+
}, ["保留名编辑", h("span", { className: "smem-status", key: "s" }, settings && settings.reserved ? (settings.reserved.length + " 个") : "")]),
|
|
4100
|
+
]) : null,
|
|
4101
|
+
]),
|
|
2727
4102
|
]) : null,
|
|
2728
|
-
tab === "files" &&
|
|
4103
|
+
tab === "files" && tool === "audit"
|
|
4104
|
+
? h(AuditPanel, { report: audit, busy: auditBusy, onRun: runAudit, onClose: function () { setTool(null); } })
|
|
4105
|
+
: null,
|
|
4106
|
+
tab === "files" && tool === "candidates"
|
|
4107
|
+
? h(CandidatePanel, {
|
|
4108
|
+
data: cand,
|
|
4109
|
+
busy: candBusy || busy,
|
|
4110
|
+
picked: picked,
|
|
4111
|
+
confirm: candConfirm,
|
|
4112
|
+
onLoad: loadCandidates,
|
|
4113
|
+
onClose: function () { setTool(null); },
|
|
4114
|
+
onPick: function (file) {
|
|
4115
|
+
const nx = Object.assign({}, picked);
|
|
4116
|
+
if (nx[file]) delete nx[file]; else nx[file] = true;
|
|
4117
|
+
setPicked(nx);
|
|
4118
|
+
},
|
|
4119
|
+
onPickAll: function () {
|
|
4120
|
+
const list = (cand && cand.candidates) || [];
|
|
4121
|
+
const all = list.length > 0 && list.every(function (x) { return picked[x.file]; });
|
|
4122
|
+
const nx = {};
|
|
4123
|
+
if (!all) list.forEach(function (x) { nx[x.file] = true; });
|
|
4124
|
+
setPicked(nx);
|
|
4125
|
+
},
|
|
4126
|
+
onAskOne: function (file) { setCandConfirm(file); },
|
|
4127
|
+
onAskPicked: function () { setCandConfirm("__batch__"); },
|
|
4128
|
+
onCancelConfirm: function () { setCandConfirm(null); },
|
|
4129
|
+
onArchiveOne: function (x) { archiveCandidates([x]); },
|
|
4130
|
+
onArchivePicked: function () {
|
|
4131
|
+
const list = ((cand && cand.candidates) || []).filter(function (x) { return picked[x.file]; });
|
|
4132
|
+
archiveCandidates(list);
|
|
4133
|
+
},
|
|
4134
|
+
})
|
|
4135
|
+
: null,
|
|
4136
|
+
tab === "files" && tool === "raw"
|
|
4137
|
+
? h(RawPanel, {
|
|
4138
|
+
reserved: settings && settings.reserved,
|
|
4139
|
+
name: rawName,
|
|
4140
|
+
text: rawText,
|
|
4141
|
+
isNew: rawIsNew,
|
|
4142
|
+
loading: rawLoading,
|
|
4143
|
+
busy: rawBusy,
|
|
4144
|
+
saving: rawBusy,
|
|
4145
|
+
onOpen: openRaw,
|
|
4146
|
+
onText: setRawText,
|
|
4147
|
+
onSave: saveRaw,
|
|
4148
|
+
onClose: function () { setRawName(null); setRawText(""); setRawIsNew(false); },
|
|
4149
|
+
})
|
|
4150
|
+
: null,
|
|
4151
|
+
// 三排筛选:状态 / 类型 / 标签。组内 OR、组间 AND,再与搜索框 AND。
|
|
4152
|
+
tab === "files" && totalCount
|
|
2729
4153
|
? h("div", { className: "smem-filters", key: "filters" }, [
|
|
4154
|
+
h("span", { className: "smem-filters-label", key: "lst" }, "状态"),
|
|
4155
|
+
h("button", {
|
|
4156
|
+
type: "button", key: "st-all", className: "smem-chip",
|
|
4157
|
+
"data-on": statusFilter === "all" ? "1" : "0",
|
|
4158
|
+
title: "活动与已归档一起看(归档条走灰色)",
|
|
4159
|
+
onClick: function () { setStatusFilter("all"); },
|
|
4160
|
+
}, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(totalCount))]),
|
|
4161
|
+
h("button", {
|
|
4162
|
+
type: "button", key: "st-active", className: "smem-chip",
|
|
4163
|
+
"data-on": statusFilter === "active" ? "1" : "0",
|
|
4164
|
+
title: "正在参与检索与星图的记忆",
|
|
4165
|
+
onClick: function () { setStatusFilter("active"); },
|
|
4166
|
+
}, ["活动中", h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
|
|
4167
|
+
h("button", {
|
|
4168
|
+
type: "button", key: "st-arch", className: "smem-chip",
|
|
4169
|
+
"data-on": statusFilter === "archived" ? "1" : "0",
|
|
4170
|
+
title: "已收进档案馆:不参与检索与星图,可编辑 / 恢复",
|
|
4171
|
+
onClick: function () { setStatusFilter("archived"); },
|
|
4172
|
+
}, ["已归档", h("span", { className: "smem-chip-n", key: "n" }, String(archiveItems.length))]),
|
|
4173
|
+
h("span", { className: "smem-sep", key: "sep0" }),
|
|
2730
4174
|
h("span", { className: "smem-filters-label", key: "lt" }, "类型"),
|
|
2731
4175
|
h("button", {
|
|
2732
4176
|
type: "button", key: "all", className: "smem-chip",
|
|
2733
4177
|
"data-on": typeFilter.length ? "0" : "1",
|
|
2734
4178
|
onClick: function () { setTypeFilter([]); },
|
|
2735
|
-
}, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(
|
|
4179
|
+
}, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(statusItems.length))]),
|
|
2736
4180
|
typeList.map(function (t) {
|
|
2737
4181
|
return h("button", {
|
|
2738
4182
|
type: "button", key: t, className: "smem-chip",
|
|
@@ -2752,7 +4196,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2752
4196
|
hasFilter
|
|
2753
4197
|
? h("button", {
|
|
2754
4198
|
type: "button", key: "clear", className: "smem-btn",
|
|
2755
|
-
onClick: function () { setTypeFilter([]); setTagFilter([]); setQuery(""); },
|
|
4199
|
+
onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); },
|
|
2756
4200
|
}, "清除筛选")
|
|
2757
4201
|
: null,
|
|
2758
4202
|
])
|
|
@@ -2784,34 +4228,11 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2784
4228
|
])
|
|
2785
4229
|
: null,
|
|
2786
4230
|
tab === "autodream"
|
|
2787
|
-
? h(AutodreamPanel, { key: "autodream", ctx: props.ctx })
|
|
4231
|
+
? h(AutodreamPanel, { key: "autodream", ctx: props.ctx, seg: adSeg, onSeg: setAdSeg })
|
|
2788
4232
|
: tab === "star"
|
|
2789
4233
|
? h("div", { className: "smem-starmap", key: "starmap" },
|
|
2790
4234
|
h(StarmapRuntime.StarMap, { ctx: props.ctx }))
|
|
2791
|
-
:
|
|
2792
|
-
? h("div", { className: "smem-muted", key: "loading" }, "加载中…")
|
|
2793
|
-
: filtered.length === 0
|
|
2794
|
-
? h("div", { className: "smem-empty", key: "empty" },
|
|
2795
|
-
items.length === 0
|
|
2796
|
-
? "还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。"
|
|
2797
|
-
: "没有符合搜索条件的文件。")
|
|
2798
|
-
: h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
2799
|
-
return h(Card, {
|
|
2800
|
-
key: it.file,
|
|
2801
|
-
item: it,
|
|
2802
|
-
expanded: !!expanded[it.file],
|
|
2803
|
-
deleting: deleting === it.file,
|
|
2804
|
-
onToggle: function (file) {
|
|
2805
|
-
const nx = {};
|
|
2806
|
-
nx[file] = !expanded[file];
|
|
2807
|
-
setExpanded(Object.assign({}, expanded, nx));
|
|
2808
|
-
},
|
|
2809
|
-
onEdit: openEdit,
|
|
2810
|
-
onAskDelete: function (file) { setDeleting(file); },
|
|
2811
|
-
onCancelDelete: function () { setDeleting(null); },
|
|
2812
|
-
onConfirmDelete: confirmDelete,
|
|
2813
|
-
});
|
|
2814
|
-
})),
|
|
4235
|
+
: filesView,
|
|
2815
4236
|
]);
|
|
2816
4237
|
}
|
|
2817
4238
|
|