sage-mem 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/lib/client.js +632 -66
- package/lib/dream-audit.js +211 -0
- package/lib/dream-prompt.js +123 -0
- package/lib/dream-tools.js +366 -0
- package/lib/dream.js +887 -0
- package/lib/index.js +123 -3
- package/lib/typert.host.js +196 -0
- package/package.json +18 -5
package/lib/client.js
CHANGED
|
@@ -108,12 +108,16 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// ── 常量与纯函数 ─────────────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// 配色(2026-09-27 重做):原来五个色都是高饱和高亮度(#ffd27d / #6fe3ff /
|
|
113
|
+
// #b18cff / #7dffb0),配上当时的星芒与脉冲环,整体读起来像贴纸。
|
|
114
|
+
// 现在统一降饱和、压亮度 —— 深空画布上够辨认,又不抢眼。
|
|
111
115
|
const KIND_META = {
|
|
112
|
-
user: { label: "用户", color: "#
|
|
113
|
-
feedback: { label: "反馈", color: "#
|
|
114
|
-
project: { label: "项目", color: "#
|
|
115
|
-
reference: { label: "参考", color: "#
|
|
116
|
-
special: { label: "特殊", color: "#
|
|
116
|
+
user: { label: "用户", color: "#e0c07a" },
|
|
117
|
+
feedback: { label: "反馈", color: "#7ec8da" },
|
|
118
|
+
project: { label: "项目", color: "#ac9edc" },
|
|
119
|
+
reference: { label: "参考", color: "#92ceaa" },
|
|
120
|
+
special: { label: "特殊", color: "#bcc3d0" },
|
|
117
121
|
};
|
|
118
122
|
const KIND_ORDER = ["user", "feedback", "project", "reference", "special"];
|
|
119
123
|
const FRESH_MS = 7 * 86400000;
|
|
@@ -369,7 +373,13 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
369
373
|
".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:linear-gradient(160deg,#0d1226,#111a33);border:1px solid rgba(140,160,220,.22);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
|
|
370
374
|
"@keyframes smap-fade-in{from{opacity:0}to{opacity:1}}",
|
|
371
375
|
"@keyframes smap-pop-in{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}}",
|
|
372
|
-
|
|
376
|
+
// 2026-09-27(B 方案「自洽夜空」):星图整块含 header 与图例,同属一张深色画布。
|
|
377
|
+
//
|
|
378
|
+
// 不这么做的下场是实测出来的:宿主切到**浅色主题**后,这个组件里硬编码的
|
|
379
|
+
// 浅色文字(#dbe4f5 系)落在白色卡片上,标题、搜索框、图例几乎看不见 ——
|
|
380
|
+
// 截图里就是一片「重影」。星图本来就不打算跟随宿主主题(它是一张夜空),
|
|
381
|
+
// 那就把边界画清楚:整块深色,而不是只有中间那半块。
|
|
382
|
+
".smap-card{background:#0c0e12;border-radius:10px;padding:12px 14px;color:#e3e7ef;display:flex;flex-direction:column;flex:1;min-height:0;}",
|
|
373
383
|
".smap-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;}",
|
|
374
384
|
".smap-title{font-size:15px;font-weight:600;letter-spacing:.06em;}",
|
|
375
385
|
".smap-sub{font-size:11px;opacity:.55;}",
|
|
@@ -439,35 +449,18 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
439
449
|
}
|
|
440
450
|
|
|
441
451
|
const SMAP_BG_VS = "attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }";
|
|
452
|
+
// 底色(2026-09-27 重做)。旧版是四层 fbm 噪声算出来的蓝紫星云,还带 uTime 动画 ——
|
|
453
|
+
// 「会呼吸的彩色底」正是廉价感的一大来源,而且每帧都要重算噪声。
|
|
454
|
+
// 现在只剩一条静态径向渐变:中心略亮的中性深灰,边缘更暗。uTime 保留声明
|
|
455
|
+
// 只为让 uniform 列表不变(用不到时 getUniformLocation 返回 null,调用是安全空操作)。
|
|
442
456
|
const SMAP_BG_FS = [
|
|
443
457
|
"precision highp float;",
|
|
444
458
|
"uniform vec2 uRes;",
|
|
445
459
|
"uniform float uTime;",
|
|
446
|
-
"float hash21(vec2 p){ p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }",
|
|
447
|
-
"float vnoise(vec2 p){",
|
|
448
|
-
" vec2 i = floor(p); vec2 f = fract(p);",
|
|
449
|
-
" f = f * f * (3.0 - 2.0 * f);",
|
|
450
|
-
" float a = hash21(i); float b = hash21(i + vec2(1.0, 0.0));",
|
|
451
|
-
" float c = hash21(i + vec2(0.0, 1.0)); float e = hash21(i + vec2(1.0, 1.0));",
|
|
452
|
-
" return mix(mix(a, b, f.x), mix(c, e, f.x), f.y);",
|
|
453
|
-
"}",
|
|
454
|
-
"float fbm(vec2 p){",
|
|
455
|
-
" float v = 0.0; float amp = 0.55;",
|
|
456
|
-
" for (int i = 0; i < 4; i++){ v += amp * vnoise(p); p = p * 2.03 + vec2(17.7, 9.2); amp *= 0.5; }",
|
|
457
|
-
" return v;",
|
|
458
|
-
"}",
|
|
459
460
|
"void main(){",
|
|
460
|
-
" float fy = gl_FragCoord.y / uRes.y;",
|
|
461
|
-
" vec3 col = mix(vec3(0.063, 0.090, 0.192), vec3(0.043, 0.063, 0.149), fy);",
|
|
462
461
|
" vec2 suv = gl_FragCoord.xy / uRes;",
|
|
463
|
-
" float
|
|
464
|
-
"
|
|
465
|
-
" vec2 np = vec2(suv.x * 3.1 + suv.y * 1.35, suv.y * 2.15 - suv.x * 0.75);",
|
|
466
|
-
" float neb = fbm(np + vec2(uTime * 0.010, -uTime * 0.006));",
|
|
467
|
-
" vec3 dustCol = mix(vec3(0.36, 0.44, 0.78), vec3(0.72, 0.58, 0.46), fbm(np * 0.63 + 4.7));",
|
|
468
|
-
" col += dustCol * bandMask * (neb * neb) * 0.30;",
|
|
469
|
-
" float dark = fbm(np * 0.42 + 13.1);",
|
|
470
|
-
" col *= 1.0 - dark * bandMask * 0.22;",
|
|
462
|
+
" float r = length((suv - vec2(0.5, 0.44)) * vec2(1.0, 1.28));",
|
|
463
|
+
" vec3 col = mix(vec3(0.105, 0.117, 0.145), vec3(0.043, 0.050, 0.065), clamp(r * 1.30, 0.0, 1.0));",
|
|
471
464
|
" gl_FragColor = vec4(col, 1.0);",
|
|
472
465
|
"}",
|
|
473
466
|
].join("\n");
|
|
@@ -491,6 +484,11 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
491
484
|
"}",
|
|
492
485
|
].join("\n");
|
|
493
486
|
|
|
487
|
+
// 星点(2026-09-27 重做)。去掉了三样东西,它们就是「塑料感」的来源:
|
|
488
|
+
// 1. 衍射星芒(横竖两条十字 spike)—— 用户明确要求去掉
|
|
489
|
+
// 2. 脉冲扩散环(fract(uTime*0.3) 画出来的同心圆)—— 看起来像贴纸上的亮环
|
|
490
|
+
// 3. 呼吸闪烁(sin(uTime) 调制整体透明度)—— 满屏一起闪,廉价
|
|
491
|
+
// 留下的是「锐利实心核心 + 收敛辉光」,亮度按 vMeta1.y(是否新鲜)分档。
|
|
494
492
|
const SMAP_STAR_FS = [
|
|
495
493
|
"precision mediump float;",
|
|
496
494
|
"varying vec2 vLocal;",
|
|
@@ -509,27 +507,16 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
509
507
|
" float d = length(vLocal);",
|
|
510
508
|
" float alpha = 0.0;",
|
|
511
509
|
" if (vMeta1.z < -0.5){",
|
|
512
|
-
"
|
|
513
|
-
" float slow = 0.5 + 0.5 * sin(uTime * 0.8 + vMeta1.x * 6.2831853);",
|
|
514
|
-
" alpha = core * vColor.a * (0.45 + 0.55 * slow);",
|
|
510
|
+
" alpha = smoothstep(0.55, 0.0, d) * vColor.a;",
|
|
515
511
|
" } else {",
|
|
516
|
-
" float
|
|
517
|
-
" float core = smoothstep(0.30, 0.
|
|
518
|
-
" float
|
|
519
|
-
"
|
|
520
|
-
" float spikes = max(sh, sv); spikes = spikes * spikes * spikes * 0.6;",
|
|
521
|
-
" float glow = exp(-d * 3.2) * 0.5;",
|
|
522
|
-
" alpha = core + spikes * core + glow;",
|
|
523
|
-
" if (vMeta1.y > 0.5){",
|
|
524
|
-
" float ph = fract(uTime * 0.30 + vMeta1.x * 0.61803399);",
|
|
525
|
-
" float rr = mix(0.20, 0.88, ph);",
|
|
526
|
-
" alpha += smoothstep(0.075, 0.0, abs(d - rr)) * (1.0 - ph) * 0.85;",
|
|
527
|
-
" }",
|
|
512
|
+
" float aFresh = mix(0.60, 1.0, vMeta1.y);",
|
|
513
|
+
" float core = smoothstep(0.30, 0.04, d);",
|
|
514
|
+
" float glow = exp(-d * 3.8) * 0.30;",
|
|
515
|
+
" alpha = (core + glow) * aFresh;",
|
|
528
516
|
" if (abs(vMeta1.z - uHoverIdx) < 0.5){",
|
|
529
|
-
" alpha += smoothstep(0.10, 0.0, abs(d - 0.
|
|
530
|
-
" alpha += smoothstep(0.05, 0.0, abs(d - 0.66)) * 0.9;",
|
|
517
|
+
" alpha += smoothstep(0.10, 0.0, abs(d - 0.62)) * 0.55;",
|
|
531
518
|
" }",
|
|
532
|
-
" alpha = clamp(alpha, 0.0, 1.
|
|
519
|
+
" alpha = clamp(alpha, 0.0, 1.0);",
|
|
533
520
|
" }",
|
|
534
521
|
" alpha *= revealFactor();",
|
|
535
522
|
" gl_FragColor = vec4(vColor.rgb, alpha);",
|
|
@@ -906,7 +893,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
906
893
|
|
|
907
894
|
const hoverBox = react.useRef({ idx: -1 }).current;
|
|
908
895
|
const visBox = react.useRef({ flags: [], lines: [] }).current;
|
|
909
|
-
const drawBox = react.useRef({ renderer: null, t0: 0 }).current;
|
|
896
|
+
const drawBox = react.useRef({ renderer: null, t0: 0, markDirty: null }).current;
|
|
910
897
|
const replayBox = react.useRef({ raf: 0, active: false }).current;
|
|
911
898
|
|
|
912
899
|
// Esc 分层:详情面板开着时先关面板(capture 阶段拦截),再按才落到 overlay 关星空。
|
|
@@ -970,13 +957,40 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
970
957
|
drawBox.renderer = renderer;
|
|
971
958
|
drawBox.t0 = Date.now();
|
|
972
959
|
renderer.render(0);
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
960
|
+
|
|
961
|
+
// ── 按需重绘(2026-09-27 重做)──
|
|
962
|
+
// 旧版是一个常驻 rAF 循环:只要星图页开着就永远满帧重画,后台标签页也一样,
|
|
963
|
+
// 而画面其实早就静止了。新的规矩只有三种情况会画:
|
|
964
|
+
// 1. 入场展示动画期间(约 1.3s,星点按诞生时间依次点亮)
|
|
965
|
+
// 2. 有人调 markDirty()(hover / 数据或筛选变化 / 尺寸变化)
|
|
966
|
+
// 3. 诞生回放进行中
|
|
967
|
+
// 其余时间一帧都不画。另外帧率封顶 30fps,页面不可见时直接跳过。
|
|
968
|
+
const FRAME_MS = 1000 / 30;
|
|
969
|
+
const ENTRANCE_MS = 1300;
|
|
970
|
+
let raf = 0;
|
|
971
|
+
let dirty = true;
|
|
972
|
+
let lastDraw = 0;
|
|
973
|
+
function schedule() {
|
|
974
|
+
if (!raf) raf = window.requestAnimationFrame(tick);
|
|
975
|
+
}
|
|
976
|
+
function tick(ts) {
|
|
977
|
+
raf = 0;
|
|
978
|
+
if (!drawBox.renderer) return;
|
|
979
|
+
if (ts - drawBox.t0 < ENTRANCE_MS || replayBox.active) dirty = true;
|
|
980
|
+
if (!dirty) return;
|
|
981
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
982
|
+
if (ts - lastDraw < FRAME_MS) { schedule(); return; }
|
|
983
|
+
dirty = false;
|
|
984
|
+
lastDraw = ts;
|
|
985
|
+
drawBox.renderer.render((ts - drawBox.t0) / 1000);
|
|
986
|
+
}
|
|
987
|
+
drawBox.markDirty = function () { dirty = true; schedule(); };
|
|
988
|
+
schedule();
|
|
989
|
+
|
|
977
990
|
return () => {
|
|
978
991
|
window.cancelAnimationFrame(raf);
|
|
979
992
|
drawBox.renderer = null;
|
|
993
|
+
drawBox.markDirty = null;
|
|
980
994
|
replayBox.active = false;
|
|
981
995
|
window.cancelAnimationFrame(replayBox.raf);
|
|
982
996
|
if (renderer.destroy) renderer.destroy();
|
|
@@ -994,7 +1008,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
994
1008
|
setEmptySky(!anyVis);
|
|
995
1009
|
if (drawBox.renderer) {
|
|
996
1010
|
if (drawBox.renderer.setGeometry) drawBox.renderer.setGeometry();
|
|
997
|
-
|
|
1011
|
+
// 交给按需重绘那一套:下一帧画(<33ms),不再这里直接 render
|
|
1012
|
+
if (drawBox.markDirty) drawBox.markDirty();
|
|
1013
|
+
else drawBox.renderer.render((Date.now() - drawBox.t0) / 1000);
|
|
998
1014
|
}
|
|
999
1015
|
}, [data, layout, kindsOn, query]);
|
|
1000
1016
|
|
|
@@ -1024,6 +1040,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1024
1040
|
if (best !== hoverBox.idx) {
|
|
1025
1041
|
hoverBox.idx = best;
|
|
1026
1042
|
if (drawBox.renderer && drawBox.renderer.setHighlight) drawBox.renderer.setHighlight(best);
|
|
1043
|
+
if (drawBox.markDirty) drawBox.markDirty();
|
|
1027
1044
|
}
|
|
1028
1045
|
if (best >= 0) {
|
|
1029
1046
|
const p = layout.pts[best];
|
|
@@ -1039,6 +1056,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1039
1056
|
function onLeave() {
|
|
1040
1057
|
hoverBox.idx = -1;
|
|
1041
1058
|
if (drawBox.renderer && drawBox.renderer.setHighlight) drawBox.renderer.setHighlight(-1);
|
|
1059
|
+
if (drawBox.markDirty) drawBox.markDirty();
|
|
1042
1060
|
setHover(null);
|
|
1043
1061
|
}
|
|
1044
1062
|
|
|
@@ -1098,6 +1116,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1098
1116
|
replayBox.active = false;
|
|
1099
1117
|
window.cancelAnimationFrame(replayBox.raf);
|
|
1100
1118
|
if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(null);
|
|
1119
|
+
if (drawBox.markDirty) drawBox.markDirty();
|
|
1101
1120
|
setReplaying(false);
|
|
1102
1121
|
setReplayDate(null);
|
|
1103
1122
|
}
|
|
@@ -1347,6 +1366,30 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1347
1366
|
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; } };
|
|
1348
1367
|
const starmapReadSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
1349
1368
|
|
|
1369
|
+
// ── dream 的 codec ──
|
|
1370
|
+
// 刻意写得宽:这几个方法返回的是配置 / 状态对象,字段会随版本增删。
|
|
1371
|
+
// 用 parseSchema 逐字段硬写一遍的代价是 —— host 那边加一个字段,client 的旧
|
|
1372
|
+
// schema 就把整条调用当「缺字段」拒掉(parseSchema 会 throw missing field),
|
|
1373
|
+
// 于是插件一升级、界面就整体报错。所以顶层类型严格、内部宽松。
|
|
1374
|
+
const objCodec = {
|
|
1375
|
+
parse(v) {
|
|
1376
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) throw new Error("sage-mem: expected object");
|
|
1377
|
+
return v;
|
|
1378
|
+
},
|
|
1379
|
+
};
|
|
1380
|
+
const arrCodec = {
|
|
1381
|
+
parse(v) {
|
|
1382
|
+
if (!Array.isArray(v)) throw new Error("sage-mem: expected array");
|
|
1383
|
+
return v;
|
|
1384
|
+
},
|
|
1385
|
+
};
|
|
1386
|
+
const boolCodec = {
|
|
1387
|
+
parse(v) {
|
|
1388
|
+
if (typeof v !== "boolean") throw new Error("sage-mem: expected boolean");
|
|
1389
|
+
return v;
|
|
1390
|
+
},
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1350
1393
|
const TYPERT_REMOTE = {
|
|
1351
1394
|
package: "sage-mem",
|
|
1352
1395
|
descriptors: [
|
|
@@ -1419,6 +1462,89 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1419
1462
|
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapRead", schema: starmapReadSchema, create: () => starmapReadSchema },
|
|
1420
1463
|
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1421
1464
|
},
|
|
1465
|
+
// ── dream(夜间整理)──
|
|
1466
|
+
// 2026-09-27 踩到的坑:namespace 不会自动出现。dsh-api-remotes 里那份
|
|
1467
|
+
// remote 清单是**官方构建期生成的产物**,只有 memory / starmap 在里面;
|
|
1468
|
+
// 后加的 dream 不在,于是 `ctx.get("remote.dream")` 永远 undefined ——
|
|
1469
|
+
// 界面上表现为「remote.dream 不可用」,而 host 半日志明确写着
|
|
1470
|
+
// 「dream gateway mounted ok」(服务是真的挂上了,只是 client 不知道)。
|
|
1471
|
+
// 修法就是这七条:和 memory / starmap 一样,自己声明 + $mount。
|
|
1472
|
+
{
|
|
1473
|
+
id: "sage-mem#dream/getConfig",
|
|
1474
|
+
service: "dream",
|
|
1475
|
+
namespace: "dream",
|
|
1476
|
+
method: "getConfig",
|
|
1477
|
+
invocation: { kind: "direct" },
|
|
1478
|
+
parameters: [],
|
|
1479
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamConfigResult", schema: objCodec, create: () => objCodec },
|
|
1480
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
id: "sage-mem#dream/setConfig",
|
|
1484
|
+
service: "dream",
|
|
1485
|
+
namespace: "dream",
|
|
1486
|
+
method: "setConfig",
|
|
1487
|
+
invocation: { kind: "direct" },
|
|
1488
|
+
parameters: [
|
|
1489
|
+
{ name: "patch", wire: "patch", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#DreamPatch", schema: objCodec, create: () => objCodec } },
|
|
1490
|
+
],
|
|
1491
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamConfigResult", schema: objCodec, create: () => objCodec },
|
|
1492
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
id: "sage-mem#dream/status",
|
|
1496
|
+
service: "dream",
|
|
1497
|
+
namespace: "dream",
|
|
1498
|
+
method: "status",
|
|
1499
|
+
invocation: { kind: "direct" },
|
|
1500
|
+
parameters: [],
|
|
1501
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamStatus", schema: objCodec, create: () => objCodec },
|
|
1502
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
id: "sage-mem#dream/runNow",
|
|
1506
|
+
service: "dream",
|
|
1507
|
+
namespace: "dream",
|
|
1508
|
+
method: "runNow",
|
|
1509
|
+
invocation: { kind: "direct" },
|
|
1510
|
+
parameters: [
|
|
1511
|
+
{ name: "force", wire: "force", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#Boolean", schema: boolCodec, create: () => boolCodec } },
|
|
1512
|
+
],
|
|
1513
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamRunResult", schema: objCodec, create: () => objCodec },
|
|
1514
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
id: "sage-mem#dream/listReports",
|
|
1518
|
+
service: "dream",
|
|
1519
|
+
namespace: "dream",
|
|
1520
|
+
method: "listReports",
|
|
1521
|
+
invocation: { kind: "direct" },
|
|
1522
|
+
parameters: [],
|
|
1523
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamReportList", schema: arrCodec, create: () => arrCodec },
|
|
1524
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
id: "sage-mem#dream/readReport",
|
|
1528
|
+
service: "dream",
|
|
1529
|
+
namespace: "dream",
|
|
1530
|
+
method: "readReport",
|
|
1531
|
+
invocation: { kind: "direct" },
|
|
1532
|
+
parameters: [
|
|
1533
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1534
|
+
],
|
|
1535
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#FileContent", schema: objCodec, create: () => objCodec },
|
|
1536
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
id: "sage-mem#dream/listSnapshots",
|
|
1540
|
+
service: "dream",
|
|
1541
|
+
namespace: "dream",
|
|
1542
|
+
method: "listSnapshots",
|
|
1543
|
+
invocation: { kind: "direct" },
|
|
1544
|
+
parameters: [],
|
|
1545
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#DreamSnapshotList", schema: arrCodec, create: () => arrCodec },
|
|
1546
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1547
|
+
},
|
|
1422
1548
|
],
|
|
1423
1549
|
};
|
|
1424
1550
|
|
|
@@ -1430,6 +1556,14 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1430
1556
|
".smem-dot--bad{background:var(--dsw-alias-state-error-primary,#ef4444);}",
|
|
1431
1557
|
".smem-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
1432
1558
|
".smem-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1559
|
+
// 筛选条:类型 + 自定义标签两组 chip,与搜索框是 AND 关系。
|
|
1560
|
+
// 只消费宿主变量,浅色深色同一套语言(这是融入型界面,见 frontend-craft)。
|
|
1561
|
+
".smem-filters{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}",
|
|
1562
|
+
".smem-filters-label{font-size:11.5px;color:var(--dsw-alias-label-secondary);flex:none;}",
|
|
1563
|
+
".smem-sep{width:1px;height:15px;background:var(--dsw-alias-border-l2);flex:none;margin:0 3px;}",
|
|
1564
|
+
".smem-chip-n{opacity:.62;margin-left:5px;font-variant-numeric:tabular-nums;}",
|
|
1565
|
+
".smem-taglist{display:flex;gap:4px;flex-wrap:wrap;}",
|
|
1566
|
+
".smem-tag{font-size:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));border-radius:5px;padding:1px 6px;white-space:nowrap;}",
|
|
1433
1567
|
".smem-search{flex:1;min-width:160px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:5px 10px;font-size:12px;outline:none;font-family:inherit;}",
|
|
1434
1568
|
".smem-search:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
1435
1569
|
".smem-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
|
|
@@ -1467,7 +1601,30 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1467
1601
|
".smem-tab{background:none;border:0;padding:8px 2px 10px;font-size:13px;font-family:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;position:relative;}",
|
|
1468
1602
|
'.smem-tab--on{color:var(--dsw-alias-label-primary);}',
|
|
1469
1603
|
'.smem-tab--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);}',
|
|
1470
|
-
|
|
1604
|
+
// 高度预算压到 62vh(原 70vh):设置弹窗本身高度有限,
|
|
1605
|
+
// 70vh 在 1050px 视口下会把星图底部的图例和计数挤出面板之外(实测截图被裁)。
|
|
1606
|
+
".smem-starmap{display:flex;flex-direction:column;height:min(62vh,660px);min-height:400px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;}",
|
|
1607
|
+
// ── 「梦境」面板 ──
|
|
1608
|
+
// 融入型:全部走宿主变量,控件复用已有的 chip / input / btn 语言,不另造一套。
|
|
1609
|
+
".smem-dream{display:flex;flex-direction:column;gap:12px;}",
|
|
1610
|
+
".smem-dream-sec{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:9px;}",
|
|
1611
|
+
".smem-dream-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap;}",
|
|
1612
|
+
".smem-dream-key{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none;min-width:78px;}",
|
|
1613
|
+
".smem-switch{border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:999px;width:38px;height:21px;position:relative;cursor:pointer;flex:none;padding:0;}",
|
|
1614
|
+
'.smem-switch::after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:left .15s,background .15s;}',
|
|
1615
|
+
'.smem-switch[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);background:var(--dsw-alias-brand-primary,#4d76e6);}',
|
|
1616
|
+
'.smem-switch[data-on="1"]::after{left:19px;background:#fff;}',
|
|
1617
|
+
".smem-num{width:66px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;padding:4px 8px;font-size:12px;font-family:inherit;outline:none;}",
|
|
1618
|
+
".smem-num:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
1619
|
+
".smem-status{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
1620
|
+
".smem-dream-note{font-size:11.5px;color:var(--dsw-alias-label-secondary);line-height:1.5;}",
|
|
1621
|
+
".smem-dream-list{display:flex;flex-direction:column;gap:6px;}",
|
|
1622
|
+
".smem-report{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:6px 9px;font-size:12px;cursor:pointer;background:transparent;color:inherit;text-align:left;font-family:inherit;}",
|
|
1623
|
+
".smem-report:hover{border-color:var(--dsw-alias-border-l2);}",
|
|
1624
|
+
'.smem-report[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);}',
|
|
1625
|
+
".smem-report-pre{max-height:320px;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;}",
|
|
1626
|
+
".smem-dream-adv>summary{cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
1627
|
+
".smem-dream-adv>div{display:flex;flex-direction:column;gap:8px;padding-top:9px;}",
|
|
1471
1628
|
].join("\n");
|
|
1472
1629
|
|
|
1473
1630
|
const TYPE_META = {
|
|
@@ -1477,6 +1634,26 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1477
1634
|
reference: ["🔗", "参考"],
|
|
1478
1635
|
};
|
|
1479
1636
|
const TYPE_ORDER = ["user", "feedback", "project", "reference"];
|
|
1637
|
+
// 筛选条上的显示名:emoji 在筛选条里太吵,只用文字;special 是星图的兜底类,
|
|
1638
|
+
// 文件列表里出现它说明 frontmatter 写了 type: special(本机有 2 条)。
|
|
1639
|
+
const TYPE_LABEL = { user: "用户", feedback: "反馈", project: "项目", reference: "参考", special: "特殊" };
|
|
1640
|
+
const TYPE_SORT = ["user", "feedback", "project", "reference", "special"];
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* 把输入框里的一串标签切成数组。
|
|
1644
|
+
*
|
|
1645
|
+
* 分隔符收半角逗号、全角逗号、顿号三种 —— 中文输入法下这三种都会自然敲出来。
|
|
1646
|
+
* 顺手剥掉 YAML 里有语法含义的字符([ ] " ' # :),因为标签是按 flow 写法
|
|
1647
|
+
* 落进 frontmatter 的,带着这些字符写进去会让整行解析错位。
|
|
1648
|
+
*/
|
|
1649
|
+
function parseTagInput(s) {
|
|
1650
|
+
return String(s == null ? "" : s)
|
|
1651
|
+
.split(/[,,、]+/)
|
|
1652
|
+
.map(function (t) { return t.replace(/[\[\]"'#:]/g, "").trim(); })
|
|
1653
|
+
.filter(Boolean)
|
|
1654
|
+
.slice(0, 20)
|
|
1655
|
+
.map(function (t) { return t.slice(0, 40); });
|
|
1656
|
+
}
|
|
1480
1657
|
|
|
1481
1658
|
// ── frontmatter 纯逻辑(顶层自包含普通函数,不碰 React state)─────
|
|
1482
1659
|
//
|
|
@@ -1553,6 +1730,32 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1553
1730
|
return m ? stripQuotes(m[1]) : "";
|
|
1554
1731
|
}
|
|
1555
1732
|
|
|
1733
|
+
/**
|
|
1734
|
+
* 取顶层 tags 的值。三种写法都认,与 host 半 parseTags 保持同一套规则:
|
|
1735
|
+
* tags: ["a", "b"] (flow,本插件自己写出去的就是这种)
|
|
1736
|
+
* tags:\n - a\n - b (block,手写文件常见)
|
|
1737
|
+
* tags: a, b (松散逗号串)
|
|
1738
|
+
*/
|
|
1739
|
+
function detectTags(text) {
|
|
1740
|
+
const fm = fmInner(text);
|
|
1741
|
+
const flow = fm.match(/^tags[ \t]*:[ \t]*\[([^\]]*)\]/m);
|
|
1742
|
+
if (flow) return flow[1].split(",").map(stripQuotes).filter(Boolean);
|
|
1743
|
+
const block = fm.match(/^tags[ \t]*:[ \t]*\r?\n((?:[ \t]+-[^\r\n]*\r?\n?)+)/m);
|
|
1744
|
+
if (block) {
|
|
1745
|
+
return block[1].split(/\r?\n/).map(function (l) {
|
|
1746
|
+
return stripQuotes(l.replace(/^[ \t]*-[ \t]*/, ""));
|
|
1747
|
+
}).filter(Boolean);
|
|
1748
|
+
}
|
|
1749
|
+
const inline = fm.match(/^tags[ \t]*:[ \t]*(.+?)[ \t\r]*$/m);
|
|
1750
|
+
if (inline && inline[1].trim()) return inline[1].split(",").map(stripQuotes).filter(Boolean);
|
|
1751
|
+
return [];
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/** 渲染 tags 成一行 YAML flow(带引号:标签可能含空格或中文标点)。 */
|
|
1755
|
+
function renderTags(tags) {
|
|
1756
|
+
return "tags: [" + tags.map(function (t) { return '"' + yamlQuote(t) + '"'; }).join(", ") + "]";
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1556
1759
|
/**
|
|
1557
1760
|
* 就地合并 frontmatter,返回新的完整文件内容。
|
|
1558
1761
|
* patch = { type, description, addMissingType }
|
|
@@ -1587,12 +1790,34 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1587
1790
|
|
|
1588
1791
|
const wantDesc = typeof p.description === "string" && p.description !== oldDesc;
|
|
1589
1792
|
const wantType = typeof p.type === "string" && p.type !== "" && p.type !== oldType;
|
|
1793
|
+
// tags:只有传了数组才管(undefined = 调用方不关心,一行都不动);空数组 = 明确要求移除。
|
|
1794
|
+
// 相同则整行逐字保留 —— 与 description 一样的规矩,别把用户手写的
|
|
1795
|
+
// `tags:\n - a` block 形式悄悄改写成 flow 形式。
|
|
1796
|
+
const wantTags = Array.isArray(p.tags);
|
|
1797
|
+
const tagsSame = wantTags && JSON.stringify(detectTags(parts.fm)) === JSON.stringify(p.tags);
|
|
1798
|
+
let tagsIdx = -1;
|
|
1799
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
1800
|
+
if (/^tags[ \t]*:/.test(srcLines[i])) { tagsIdx = i; break; }
|
|
1801
|
+
}
|
|
1590
1802
|
const out = [];
|
|
1591
1803
|
let descDone = !wantDesc;
|
|
1592
1804
|
let typeDone = !wantType || typeIdx < 0;
|
|
1805
|
+
let tagsDone = !wantTags || tagsSame;
|
|
1806
|
+
let tagsBlockChild = false;
|
|
1593
1807
|
let metadataAt = -1;
|
|
1594
1808
|
for (let i = 0; i < srcLines.length; i++) {
|
|
1595
1809
|
const line = srcLines[i];
|
|
1810
|
+
// block 形式 tags 的缩进子行:紧跟在被替换/移除的 tags 行之后,一起处理掉
|
|
1811
|
+
if (tagsBlockChild) {
|
|
1812
|
+
if (/^[ \t]*-[^\r\n]*$/.test(line)) continue;
|
|
1813
|
+
tagsBlockChild = false;
|
|
1814
|
+
}
|
|
1815
|
+
if (!tagsDone && i === tagsIdx) {
|
|
1816
|
+
if (p.tags.length) out.push(renderTags(p.tags));
|
|
1817
|
+
tagsDone = true;
|
|
1818
|
+
tagsBlockChild = true;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1596
1821
|
if (/^metadata[ \t]*:/.test(line)) metadataAt = out.length;
|
|
1597
1822
|
if (!descDone && /^description[ \t]*:/.test(line)) {
|
|
1598
1823
|
out.push('description: "' + yamlQuote(p.description) + '"');
|
|
@@ -1631,6 +1856,19 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1631
1856
|
}
|
|
1632
1857
|
if (at >= 0) out.splice(at, 0, rendered); else out.unshift(rendered);
|
|
1633
1858
|
}
|
|
1859
|
+
// 原本没有 tags 行 → 只在这次确实要写标签时插一行(空数组不插,避免留下一行空 tags)
|
|
1860
|
+
if (wantTags && tagsIdx < 0 && p.tags.length) {
|
|
1861
|
+
let at = -1;
|
|
1862
|
+
for (let i = 0; i < out.length; i++) {
|
|
1863
|
+
if (/^description[ \t]*:/.test(out[i])) { at = i + 1; break; }
|
|
1864
|
+
}
|
|
1865
|
+
if (at < 0) {
|
|
1866
|
+
for (let i = 0; i < out.length; i++) {
|
|
1867
|
+
if (/^name[ \t]*:/.test(out[i])) { at = i + 1; break; }
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (at >= 0) out.splice(at, 0, renderTags(p.tags)); else out.unshift(renderTags(p.tags));
|
|
1871
|
+
}
|
|
1634
1872
|
|
|
1635
1873
|
// 原文件是 CRLF 就照样回写 CRLF(LF 文件回写 \n)——保证逐字节 round-trip。
|
|
1636
1874
|
// 组件把原文重建成 "---\n" + fm + "\n---\n" + body,所以还要从 fm / body 里看 CRLF。
|
|
@@ -1642,10 +1880,11 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1642
1880
|
* 新建记忆的 frontmatter(只用于「添加记忆」或原本没有 frontmatter 的文件;
|
|
1643
1881
|
* 编辑已有文件一律走 mergeFrontmatter,绝不重建)。
|
|
1644
1882
|
*/
|
|
1645
|
-
function buildFrontmatter(type, desc, fileName) {
|
|
1883
|
+
function buildFrontmatter(type, desc, fileName, tags) {
|
|
1646
1884
|
const slug = String(fileName == null ? "" : fileName).replace(/\.md$/i, "");
|
|
1647
|
-
|
|
1648
|
-
|
|
1885
|
+
const tagLine = Array.isArray(tags) && tags.length ? "\n" + renderTags(tags) : "";
|
|
1886
|
+
return "---\nname: \"" + yamlQuote(slug) + "\"\ndescription: \"" + yamlQuote(desc) + "\"" +
|
|
1887
|
+
tagLine + "\nmetadata:\n node_type: memory\n type: " + type + "\n---\n\n";
|
|
1649
1888
|
}
|
|
1650
1889
|
|
|
1651
1890
|
function fmtSize(n) {
|
|
@@ -1678,6 +1917,11 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1678
1917
|
h("span", { className: "smem-card-title", key: "title" }, item.file),
|
|
1679
1918
|
h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
|
|
1680
1919
|
]),
|
|
1920
|
+
(item.tags && item.tags.length)
|
|
1921
|
+
? h("div", { className: "smem-taglist", key: "tags" }, item.tags.map(function (t) {
|
|
1922
|
+
return h("span", { className: "smem-tag", key: t }, "#" + t);
|
|
1923
|
+
}))
|
|
1924
|
+
: null,
|
|
1681
1925
|
item.description
|
|
1682
1926
|
? h("div", {
|
|
1683
1927
|
className: "smem-narrative",
|
|
@@ -1701,6 +1945,232 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1701
1945
|
]);
|
|
1702
1946
|
}
|
|
1703
1947
|
|
|
1948
|
+
/** 单选按钮组。复用已有的 chip 语言,不另造一套视觉。 */
|
|
1949
|
+
function Seg(props) {
|
|
1950
|
+
return h("div", { className: "smem-chips" }, props.options.map(function (o) {
|
|
1951
|
+
return h("button", {
|
|
1952
|
+
type: "button",
|
|
1953
|
+
key: o.value,
|
|
1954
|
+
className: "smem-chip",
|
|
1955
|
+
"data-on": props.value === o.value ? "1" : "0",
|
|
1956
|
+
onClick: function () { if (props.value !== o.value) props.onChange(o.value); },
|
|
1957
|
+
}, o.label);
|
|
1958
|
+
}));
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/**
|
|
1962
|
+
* 「梦境」面板 —— 夜间记忆整理(移植 Claude Code 的 autoDream)。
|
|
1963
|
+
*
|
|
1964
|
+
* 数据全走 remote.dream(host 半的 DreamGateway)。两条实现约定:
|
|
1965
|
+
* 1. **轮询而不是等待**。「立即整理」启动后台任务后立刻返回 —— 一趟可能跑
|
|
1966
|
+
* 几分钟,把 remote 调用挂在那儿等会撞上前端超时。进度靠每 2 秒拉一次
|
|
1967
|
+
* status() 显示。
|
|
1968
|
+
* 2. **不替用户做判断**。触发方式 / 改动方式 / 输入源三件事各自明说代价
|
|
1969
|
+
* (比如「记忆 + 会话记录」会把私密对话送进模型),默认全是最保守的那一档。
|
|
1970
|
+
*/
|
|
1971
|
+
function DreamPanel(props) {
|
|
1972
|
+
const remote = props.ctx && props.ctx.get ? props.ctx.get("remote.dream") : null;
|
|
1973
|
+
const sCfg = react.useState(null); const cfg = sCfg[0], setCfg = sCfg[1];
|
|
1974
|
+
const sSt = react.useState(null); const st = sSt[0], setSt = sSt[1];
|
|
1975
|
+
const sErr = react.useState(null); const err = sErr[0], setErr = sErr[1];
|
|
1976
|
+
const sNotice = react.useState(""); const notice = sNotice[0], setNotice = sNotice[1];
|
|
1977
|
+
const sBusy = react.useState(false); const busy = sBusy[0], setBusy = sBusy[1];
|
|
1978
|
+
const sReports = react.useState([]); const reports = sReports[0], setReports = sReports[1];
|
|
1979
|
+
const sOpen = react.useState(null); const open = sOpen[0], setOpen = sOpen[1];
|
|
1980
|
+
const sBody = react.useState(""); const body = sBody[0], setBody = sBody[1];
|
|
1981
|
+
const sModelEdit = react.useState(false); const modelEdit = sModelEdit[0], setModelEdit = sModelEdit[1];
|
|
1982
|
+
const sProv = react.useState(""); const prov = sProv[0], setProv = sProv[1];
|
|
1983
|
+
const sModel = react.useState(""); const model = sModel[0], setModel = sModel[1];
|
|
1984
|
+
|
|
1985
|
+
react.useEffect(function () {
|
|
1986
|
+
if (!remote) { setErr("remote.dream 不可用 —— 插件的 host 半可能没加载成功,看 DSH 启动日志"); return undefined; }
|
|
1987
|
+
let alive = true;
|
|
1988
|
+
function pull() {
|
|
1989
|
+
remote.status().then(function (r) {
|
|
1990
|
+
if (!alive) return;
|
|
1991
|
+
const d = unwrap(r);
|
|
1992
|
+
if (d) { setSt(d); if (d.config) setCfg(d.config); }
|
|
1993
|
+
setErr(null);
|
|
1994
|
+
}).catch(function (e) { if (alive) setErr(detail(e)); });
|
|
1995
|
+
}
|
|
1996
|
+
pull();
|
|
1997
|
+
remote.listReports().then(function (r) { if (alive) setReports(unwrap(r) || []); }).catch(function () {});
|
|
1998
|
+
const id = setInterval(pull, 2000);
|
|
1999
|
+
return function () { alive = false; clearInterval(id); };
|
|
2000
|
+
}, [remote]);
|
|
2001
|
+
|
|
2002
|
+
function patch(p) {
|
|
2003
|
+
setErr(null); setNotice("");
|
|
2004
|
+
remote.setConfig(p).then(function (r) {
|
|
2005
|
+
const d = unwrap(r);
|
|
2006
|
+
if (d && d.config) setCfg(d.config);
|
|
2007
|
+
}).catch(function (e) { setErr(detail(e)); });
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function run() {
|
|
2011
|
+
setErr(null); setNotice(""); setBusy(true);
|
|
2012
|
+
remote.runNow(true).then(function (r) {
|
|
2013
|
+
const d = unwrap(r);
|
|
2014
|
+
if (d && d.ok === false) setErr(d.error || "启动失败");
|
|
2015
|
+
else setNotice("已开始整理 —— 进度每 2 秒刷新一次");
|
|
2016
|
+
return remote.listReports().then(function (rr) { setReports(unwrap(rr) || []); });
|
|
2017
|
+
}).catch(function (e) { setErr(detail(e)); }).then(function () { setBusy(false); });
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function openReport(name) {
|
|
2021
|
+
if (name === null) { setOpen(null); setBody(""); return; }
|
|
2022
|
+
setOpen(name); setBody("载入中…");
|
|
2023
|
+
remote.readReport(name).then(function (r) {
|
|
2024
|
+
const d = unwrap(r);
|
|
2025
|
+
setBody((d && d.content) || "(空)");
|
|
2026
|
+
}).catch(function (e) { setBody("读取失败:" + detail(e)); });
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const c = cfg || {};
|
|
2030
|
+
const running = !!(st && st.running);
|
|
2031
|
+
const last = (st && st.lastResult) || null;
|
|
2032
|
+
|
|
2033
|
+
return h("div", { className: "smem-dream" }, [
|
|
2034
|
+
h("div", { className: "smem-dream-sec", key: "top" }, [
|
|
2035
|
+
h("div", { className: "smem-dream-row", key: "r" }, [
|
|
2036
|
+
h("button", {
|
|
2037
|
+
type: "button", key: "sw", className: "smem-switch",
|
|
2038
|
+
"data-on": c.enabled ? "1" : "0",
|
|
2039
|
+
title: c.enabled ? "点击停用" : "点击启用",
|
|
2040
|
+
"aria-label": "启用或停用夜间整理",
|
|
2041
|
+
onClick: function () { patch({ enabled: !c.enabled }); },
|
|
2042
|
+
}),
|
|
2043
|
+
h("span", { key: "t", style: { fontWeight: "600" } }, "夜间整理(dream)"),
|
|
2044
|
+
h("span", { className: "smem-status", key: "s" },
|
|
2045
|
+
running ? ("运行中 · " + String((st && st.phase) || "准备中")) : (c.enabled ? "已启用 · 空闲" : "未启用")),
|
|
2046
|
+
]),
|
|
2047
|
+
h("div", { className: "smem-dream-note", key: "n" },
|
|
2048
|
+
"把记忆目录通读一遍:合并重复的、删掉被推翻的、修索引与断链、把相对日期换成绝对日期。门控、锁、快照、审计都照 Claude Code autoDream 那套做。"),
|
|
2049
|
+
]),
|
|
2050
|
+
h("div", { className: "smem-dream-sec", key: "mode" }, [
|
|
2051
|
+
h("div", { className: "smem-dream-row", key: "a" }, [
|
|
2052
|
+
h("span", { className: "smem-dream-key", key: "k" }, "触发"),
|
|
2053
|
+
h(Seg, {
|
|
2054
|
+
key: "v", value: c.trigger,
|
|
2055
|
+
options: [{ value: "manual", label: "只手动" }, { value: "auto", label: "满足条件自动跑" }],
|
|
2056
|
+
onChange: function (v) { patch({ trigger: v }); },
|
|
2057
|
+
}),
|
|
2058
|
+
]),
|
|
2059
|
+
h("div", { className: "smem-dream-row", key: "b" }, [
|
|
2060
|
+
h("span", { className: "smem-dream-key", key: "k" }, "改动方式"),
|
|
2061
|
+
h(Seg, {
|
|
2062
|
+
key: "v", value: c.apply ? "apply" : "report",
|
|
2063
|
+
options: [{ value: "report", label: "只出报告" }, { value: "apply", label: "直接改写(先快照)" }],
|
|
2064
|
+
onChange: function (v) { patch({ apply: v === "apply" }); },
|
|
2065
|
+
}),
|
|
2066
|
+
]),
|
|
2067
|
+
h("div", { className: "smem-dream-row", key: "c" }, [
|
|
2068
|
+
h("span", { className: "smem-dream-key", key: "k" }, "输入源"),
|
|
2069
|
+
h(Seg, {
|
|
2070
|
+
key: "v", value: c.source,
|
|
2071
|
+
options: [{ value: "memory", label: "仅记忆目录" }, { value: "memory+sessions", label: "记忆 + 会话记录" }],
|
|
2072
|
+
onChange: function (v) { patch({ source: v }); },
|
|
2073
|
+
}),
|
|
2074
|
+
]),
|
|
2075
|
+
h("div", { className: "smem-dream-note", key: "w" },
|
|
2076
|
+
c.source === "memory+sessions"
|
|
2077
|
+
? "会解压会话记录做定向搜索 —— 私密对话会进入模型上下文,token 成本也更高。"
|
|
2078
|
+
: "只看记忆文件本身,不碰会话记录。"),
|
|
2079
|
+
h("div", { className: "smem-dream-note", key: "w2" },
|
|
2080
|
+
c.apply
|
|
2081
|
+
? "改写前会自动把整个记忆目录快照一份(memory 上一层 .sage-mem/snapshots/),改坏可回退。"
|
|
2082
|
+
: "只读模式:这一趟连写工具都不会挂给模型,所以它不可能改到任何文件。"),
|
|
2083
|
+
]),
|
|
2084
|
+
h("details", { className: "smem-dream-adv", key: "adv" }, [
|
|
2085
|
+
h("summary", { key: "s" }, "高级设置"),
|
|
2086
|
+
h("div", { key: "d" }, [
|
|
2087
|
+
c.trigger === "auto" ? h("div", { className: "smem-dream-row", key: "gate" }, [
|
|
2088
|
+
h("span", { className: "smem-dream-key", key: "k" }, "自动触发"),
|
|
2089
|
+
h("span", { className: "smem-status", key: "a" }, "距上次 ≥"),
|
|
2090
|
+
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) }); } }),
|
|
2091
|
+
h("span", { className: "smem-status", key: "b" }, "小时,且期间 ≥"),
|
|
2092
|
+
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) }); } }),
|
|
2093
|
+
h("span", { className: "smem-status", key: "c" }, "个会话有更新"),
|
|
2094
|
+
]) : null,
|
|
2095
|
+
h("div", { className: "smem-dream-row", key: "model" }, [
|
|
2096
|
+
h("span", { className: "smem-dream-key", key: "k" }, "模型"),
|
|
2097
|
+
modelEdit
|
|
2098
|
+
? [
|
|
2099
|
+
h("input", { className: "smem-input", key: "p", style: { flex: "1", minWidth: "110px" }, value: prov, placeholder: "provider(留空=默认)", onChange: function (e) { setProv(e.target.value); } }),
|
|
2100
|
+
h("input", { className: "smem-input", key: "m", style: { flex: "1", minWidth: "110px" }, value: model, placeholder: "model(留空=默认)", onChange: function (e) { setModel(e.target.value); } }),
|
|
2101
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "ok", onClick: function () { patch({ provider: prov, model: model }); setModelEdit(false); setNotice("模型已保存"); } }, "保存"),
|
|
2102
|
+
h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { setModelEdit(false); } }, "取消"),
|
|
2103
|
+
]
|
|
2104
|
+
: [
|
|
2105
|
+
h("span", { className: "smem-status", key: "cur" },
|
|
2106
|
+
st && st.route ? (st.route.provider + " / " + st.route.model + (st.route.fromDefault ? "(跟随聊天页默认)" : "")) : "(拿不到默认模型)"),
|
|
2107
|
+
h("button", {
|
|
2108
|
+
type: "button", className: "smem-btn", key: "e",
|
|
2109
|
+
onClick: function () { setProv(c.provider || ""); setModel(c.model || ""); setModelEdit(true); },
|
|
2110
|
+
}, "改"),
|
|
2111
|
+
],
|
|
2112
|
+
]),
|
|
2113
|
+
h("div", { className: "smem-dream-row", key: "steps" }, [
|
|
2114
|
+
h("span", { className: "smem-dream-key", key: "k" }, "最多轮数"),
|
|
2115
|
+
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) }); } }),
|
|
2116
|
+
h("span", { className: "smem-status", key: "s" }, "一轮 = 一次模型调用 + 它要的工具调用"),
|
|
2117
|
+
]),
|
|
2118
|
+
h("div", { className: "smem-dream-row", key: "keep" }, [
|
|
2119
|
+
h("span", { className: "smem-dream-key", key: "k" }, "保留快照"),
|
|
2120
|
+
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) }); } }),
|
|
2121
|
+
h("span", { className: "smem-status", key: "s" }, "份"),
|
|
2122
|
+
]),
|
|
2123
|
+
h("div", { className: "smem-status", key: "paths" }, "配置与状态:" + ((st && st.paths && st.paths.configPath) || "—")),
|
|
2124
|
+
]),
|
|
2125
|
+
]),
|
|
2126
|
+
h("div", { className: "smem-dream-sec", key: "run" }, [
|
|
2127
|
+
h("div", { className: "smem-dream-row", key: "r" }, [
|
|
2128
|
+
h("button", {
|
|
2129
|
+
type: "button", className: "smem-btn smem-btn-primary", key: "go",
|
|
2130
|
+
disabled: busy || running || !remote,
|
|
2131
|
+
onClick: run,
|
|
2132
|
+
}, running ? "正在整理…" : "立即整理"),
|
|
2133
|
+
h("span", { className: "smem-status", key: "s" },
|
|
2134
|
+
running
|
|
2135
|
+
? ("已跑 " + ((st && st.steps && st.steps.length) || 0) + " 轮 · " + String((st && st.phase) || ""))
|
|
2136
|
+
: (st && st.hoursSince != null ? ("距上次整理 " + st.hoursSince.toFixed(1) + " 小时") : "从未整理过")),
|
|
2137
|
+
]),
|
|
2138
|
+
running && st && st.steps && st.steps.length
|
|
2139
|
+
? h("div", { className: "smem-dream-note", key: "prog" }, st.steps.map(function (x) {
|
|
2140
|
+
return "第 " + x.step + " 轮:" + (x.tools.length ? x.tools.join("、") : "结束");
|
|
2141
|
+
}).join(" · "))
|
|
2142
|
+
: null,
|
|
2143
|
+
last
|
|
2144
|
+
? h("div", { className: "smem-status", key: "last" },
|
|
2145
|
+
"上次 " + String(last.atHuman || "") + " · " + (last.ok
|
|
2146
|
+
? ("成功" + (last.apply ? ",改写了 " + ((last.touched || []).length) + " 个文件" : "(只出报告)") +
|
|
2147
|
+
" · 结构问题 " + last.problemsBefore + " → " + last.problemsAfter +
|
|
2148
|
+
" · token " + last.tokensIn + " / " + last.tokensOut)
|
|
2149
|
+
: ("失败:" + (last.error || "未知"))) +
|
|
2150
|
+
(last.report ? " · 报告 " + last.report : ""))
|
|
2151
|
+
: null,
|
|
2152
|
+
]),
|
|
2153
|
+
h("div", { className: "smem-dream-sec", key: "rep" }, [
|
|
2154
|
+
h("div", { className: "smem-dream-row", key: "h" }, [
|
|
2155
|
+
h("span", { key: "t", style: { fontWeight: "600", fontSize: "12.5px" } }, "历史报告"),
|
|
2156
|
+
h("span", { className: "smem-status", key: "c" }, String(reports.length) + " 份"),
|
|
2157
|
+
]),
|
|
2158
|
+
reports.length === 0
|
|
2159
|
+
? h("div", { className: "smem-status", key: "e" }, "还没有报告 —— 跑一次就有了。报告写在 memory/dream/ 下,不会混进记忆列表。")
|
|
2160
|
+
: h("div", { className: "smem-dream-list", key: "l" }, reports.slice(0, 12).map(function (r) {
|
|
2161
|
+
return h("button", {
|
|
2162
|
+
type: "button", key: r.name, className: "smem-report",
|
|
2163
|
+
"data-on": open === r.name ? "1" : "0",
|
|
2164
|
+
onClick: function () { openReport(open === r.name ? null : r.name); },
|
|
2165
|
+
}, String(r.atHuman || "") + " · " + r.name + " · " + fmtSize(r.bytes) + (open === r.name ? " · 收起" : " · 查看"));
|
|
2166
|
+
})),
|
|
2167
|
+
open && body ? h("pre", { className: "smem-report-pre", key: "body" }, body) : null,
|
|
2168
|
+
]),
|
|
2169
|
+
err ? h("div", { className: "smem-err", key: "err" }, "⚠ " + err) : null,
|
|
2170
|
+
notice && !err ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
2171
|
+
]);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
1704
2174
|
function Section(props) {
|
|
1705
2175
|
const remote = props.ctx.get("remote.memory");
|
|
1706
2176
|
const sItems = react.useState([]);
|
|
@@ -1729,6 +2199,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1729
2199
|
const formDesc = sFormDesc[0], setFormDesc = sFormDesc[1];
|
|
1730
2200
|
const sFormBody = react.useState("");
|
|
1731
2201
|
const formBody = sFormBody[0], setFormBody = sFormBody[1];
|
|
2202
|
+
const sFormTags = react.useState("");
|
|
2203
|
+
const formTags = sFormTags[0], setFormTags = sFormTags[1];
|
|
2204
|
+
// 筛选:空数组 = 不筛。类型与标签各自组内 OR、两组之间 AND,再与搜索框 AND。
|
|
2205
|
+
const sTypeFilter = react.useState([]);
|
|
2206
|
+
const typeFilter = sTypeFilter[0], setTypeFilter = sTypeFilter[1];
|
|
2207
|
+
const sTagFilter = react.useState([]);
|
|
2208
|
+
const tagFilter = sTagFilter[0], setTagFilter = sTagFilter[1];
|
|
2209
|
+
function toggleIn(setter, value) {
|
|
2210
|
+
setter(function (cur) {
|
|
2211
|
+
return cur.indexOf(value) >= 0
|
|
2212
|
+
? cur.filter(function (x) { return x !== value; })
|
|
2213
|
+
: cur.concat([value]);
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
1732
2216
|
// 打开编辑时的原 frontmatter 原文(null = 该文件没有 frontmatter)
|
|
1733
2217
|
const sFormFm = react.useState(null);
|
|
1734
2218
|
const formFm = sFormFm[0], setFormFm = sFormFm[1];
|
|
@@ -1764,7 +2248,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1764
2248
|
|
|
1765
2249
|
function closeForm() {
|
|
1766
2250
|
setFormOpen(false); setEditingFile(null); setFormName(""); setFormDesc(""); setFormBody(""); setFormType("feedback");
|
|
1767
|
-
setFormFm(null); setFormBaseType("");
|
|
2251
|
+
setFormFm(null); setFormBaseType(""); setFormTags("");
|
|
1768
2252
|
}
|
|
1769
2253
|
|
|
1770
2254
|
function openEdit(file) {
|
|
@@ -1786,6 +2270,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1786
2270
|
setFormBaseType(shown);
|
|
1787
2271
|
setFormDesc(extractDescription(parts.fm));
|
|
1788
2272
|
setFormBody(parts.body);
|
|
2273
|
+
setFormTags(detectTags(parts.fm).join(", "));
|
|
1789
2274
|
// 关键:原 frontmatter 原文留着,保存时「就地合并」而不是重建
|
|
1790
2275
|
setFormFm(parts.hasFm ? parts.fm : null);
|
|
1791
2276
|
setFormOpen(true);
|
|
@@ -1803,6 +2288,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1803
2288
|
setFormDesc("");
|
|
1804
2289
|
setFormBody("");
|
|
1805
2290
|
setFormFm(null);
|
|
2291
|
+
setFormTags("");
|
|
1806
2292
|
setFormOpen(true);
|
|
1807
2293
|
}
|
|
1808
2294
|
|
|
@@ -1813,18 +2299,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1813
2299
|
const name = (formName.trim() || (type + "_note.md"));
|
|
1814
2300
|
const safeName = name.endsWith(".md") ? name : name + ".md";
|
|
1815
2301
|
const desc = formDesc.trim();
|
|
2302
|
+
const tags = parseTagInput(formTags);
|
|
1816
2303
|
let content;
|
|
1817
2304
|
if (formFm === null) {
|
|
1818
2305
|
// 没有 frontmatter(新增记忆 / 索引文件)→ 生成一份
|
|
1819
|
-
content = buildFrontmatter(type, desc, safeName) + formBody;
|
|
2306
|
+
content = buildFrontmatter(type, desc, safeName, tags) + formBody;
|
|
1820
2307
|
} else {
|
|
1821
|
-
// 有 frontmatter → 就地合并:除 description / type 外一字不动
|
|
2308
|
+
// 有 frontmatter → 就地合并:除 description / type / tags 外一字不动
|
|
1822
2309
|
const original = "---\n" + formFm + "\n---\n" + formBody;
|
|
1823
2310
|
content = mergeFrontmatter(original, {
|
|
1824
2311
|
type: type,
|
|
1825
2312
|
description: desc,
|
|
1826
2313
|
// 原文没有 type 行时,只有用户确实改过类型才补写一行
|
|
1827
2314
|
addMissingType: type !== formBaseType,
|
|
2315
|
+
tags: tags,
|
|
1828
2316
|
});
|
|
1829
2317
|
}
|
|
1830
2318
|
remote.writeFile(safeName, content)
|
|
@@ -1852,29 +2340,103 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1852
2340
|
.then(function () { setBusy(false); });
|
|
1853
2341
|
}
|
|
1854
2342
|
|
|
2343
|
+
// ── 筛选条的数据 ──
|
|
2344
|
+
// 计数一律按**全量**算,不随当前筛选变化 —— 否则一筛下去别的类型就都显示 0,
|
|
2345
|
+
// 用户没法知道「换个类型还有多少条」。
|
|
2346
|
+
const typeCounts = {};
|
|
2347
|
+
const tagCounts = {};
|
|
2348
|
+
for (let i = 0; i < items.length; i++) {
|
|
2349
|
+
const it = items[i];
|
|
2350
|
+
const t = it.type || "reference";
|
|
2351
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
2352
|
+
const tg = Array.isArray(it.tags) ? it.tags : [];
|
|
2353
|
+
for (let j = 0; j < tg.length; j++) tagCounts[tg[j]] = (tagCounts[tg[j]] || 0) + 1;
|
|
2354
|
+
}
|
|
2355
|
+
// 类型顺序:认识的类型按固定次序在前(用户/反馈/项目/参考/特殊),
|
|
2356
|
+
// 遇到 frontmatter 里写了别的 type 的,按名字排在后面 —— 不丢任何一个。
|
|
2357
|
+
const typeList = Object.keys(typeCounts).sort(function (a, b) {
|
|
2358
|
+
const ia = TYPE_SORT.indexOf(a), ib = TYPE_SORT.indexOf(b);
|
|
2359
|
+
if (ia >= 0 && ib >= 0) return ia - ib;
|
|
2360
|
+
if (ia >= 0) return -1;
|
|
2361
|
+
if (ib >= 0) return 1;
|
|
2362
|
+
return a < b ? -1 : 1;
|
|
2363
|
+
});
|
|
2364
|
+
const tagList = Object.keys(tagCounts).sort(function (a, b) {
|
|
2365
|
+
return tagCounts[b] - tagCounts[a] || (a < b ? -1 : 1);
|
|
2366
|
+
});
|
|
2367
|
+
|
|
1855
2368
|
const q = query.trim().toLowerCase();
|
|
1856
2369
|
const filtered = items.filter(function (it) {
|
|
2370
|
+
if (typeFilter.length && typeFilter.indexOf(it.type || "reference") < 0) return false;
|
|
2371
|
+
if (tagFilter.length) {
|
|
2372
|
+
const tg = Array.isArray(it.tags) ? it.tags : [];
|
|
2373
|
+
let hit = false;
|
|
2374
|
+
for (let i = 0; i < tagFilter.length; i++) {
|
|
2375
|
+
if (tg.indexOf(tagFilter[i]) >= 0) { hit = true; break; }
|
|
2376
|
+
}
|
|
2377
|
+
if (!hit) return false;
|
|
2378
|
+
}
|
|
1857
2379
|
if (!q) return true;
|
|
1858
|
-
const hay = ((it.file || "") + "\n" + (it.description || "") + "\n" + (it.type || "")
|
|
2380
|
+
const hay = ((it.file || "") + "\n" + (it.description || "") + "\n" + (it.type || "") + "\n" +
|
|
2381
|
+
(Array.isArray(it.tags) ? it.tags.join(" ") : "")).toLowerCase();
|
|
1859
2382
|
return hay.indexOf(q) >= 0;
|
|
1860
2383
|
});
|
|
2384
|
+
const hasFilter = !!(typeFilter.length || tagFilter.length || q);
|
|
1861
2385
|
|
|
1862
2386
|
return h("div", { className: "smem-root" }, [
|
|
1863
2387
|
h("div", { className: "smem-tabs", key: "tabs" }, [
|
|
1864
2388
|
h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", onClick: function () { setTab("files"); } }, "文件列表"),
|
|
1865
2389
|
h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", onClick: function () { setTab("star"); } }, "记忆星图"),
|
|
2390
|
+
h("button", { type: "button", className: "smem-tab" + (tab === "dream" ? " smem-tab--on" : ""), key: "d", onClick: function () { setTab("dream"); } }, "夜间整理"),
|
|
1866
2391
|
]),
|
|
1867
2392
|
h("div", { className: "smem-head", key: "head" }, [
|
|
1868
2393
|
h("span", { className: "smem-title", key: "t" }, "记忆文件(sage-mem)"),
|
|
1869
2394
|
h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
|
|
1870
2395
|
h("span", { className: "smem-muted", key: "st" }, dirOk ? "目录可读" : "目录不可读"),
|
|
1871
|
-
h("span", { className: "smem-badge", key: "cnt" },
|
|
2396
|
+
h("span", { className: "smem-badge", key: "cnt" }, hasFilter
|
|
2397
|
+
? "筛出 " + String(filtered.length) + " / " + String(items.length)
|
|
2398
|
+
: "共 " + String(items.length) + " 个文件"),
|
|
1872
2399
|
]),
|
|
1873
|
-
|
|
1874
|
-
|
|
2400
|
+
// 搜索 / 刷新 / 添加 / 筛选都只属于「文件列表」这个视图。
|
|
2401
|
+
// 放在 tab 分支外面,会让星图和夜间整理页也顶着一排用不上的控件
|
|
2402
|
+
// (2026-09-27 截图走查时发现:「+ 添加记忆」出现在星图页上)。
|
|
2403
|
+
tab === "files" ? h("div", { className: "smem-toolbar", key: "bar" }, [
|
|
2404
|
+
h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型 / 标签…", onChange: function (e) { setQuery(e.target.value); } }),
|
|
1875
2405
|
h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
|
|
1876
2406
|
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
|
|
1877
|
-
]),
|
|
2407
|
+
]) : null,
|
|
2408
|
+
tab === "files" && items.length
|
|
2409
|
+
? h("div", { className: "smem-filters", key: "filters" }, [
|
|
2410
|
+
h("span", { className: "smem-filters-label", key: "lt" }, "类型"),
|
|
2411
|
+
h("button", {
|
|
2412
|
+
type: "button", key: "all", className: "smem-chip",
|
|
2413
|
+
"data-on": typeFilter.length ? "0" : "1",
|
|
2414
|
+
onClick: function () { setTypeFilter([]); },
|
|
2415
|
+
}, ["全部", h("span", { className: "smem-chip-n", key: "n" }, String(items.length))]),
|
|
2416
|
+
typeList.map(function (t) {
|
|
2417
|
+
return h("button", {
|
|
2418
|
+
type: "button", key: t, className: "smem-chip",
|
|
2419
|
+
"data-on": typeFilter.indexOf(t) >= 0 ? "1" : "0",
|
|
2420
|
+
onClick: function () { toggleIn(setTypeFilter, t); },
|
|
2421
|
+
}, [TYPE_LABEL[t] || t, h("span", { className: "smem-chip-n", key: "n" }, String(typeCounts[t]))]);
|
|
2422
|
+
}),
|
|
2423
|
+
tagList.length ? h("span", { className: "smem-sep", key: "sep" }) : null,
|
|
2424
|
+
tagList.length ? h("span", { className: "smem-filters-label", key: "lgt" }, "标签") : null,
|
|
2425
|
+
tagList.map(function (g) {
|
|
2426
|
+
return h("button", {
|
|
2427
|
+
type: "button", key: "tag-" + g, className: "smem-chip",
|
|
2428
|
+
"data-on": tagFilter.indexOf(g) >= 0 ? "1" : "0",
|
|
2429
|
+
onClick: function () { toggleIn(setTagFilter, g); },
|
|
2430
|
+
}, ["#" + g, h("span", { className: "smem-chip-n", key: "n" }, String(tagCounts[g]))]);
|
|
2431
|
+
}),
|
|
2432
|
+
hasFilter
|
|
2433
|
+
? h("button", {
|
|
2434
|
+
type: "button", key: "clear", className: "smem-btn",
|
|
2435
|
+
onClick: function () { setTypeFilter([]); setTagFilter([]); setQuery(""); },
|
|
2436
|
+
}, "清除筛选")
|
|
2437
|
+
: null,
|
|
2438
|
+
])
|
|
2439
|
+
: null,
|
|
1878
2440
|
error ? h("div", { className: "smem-err", key: "err" }, "⚠ " + error) : null,
|
|
1879
2441
|
notice && !error ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
1880
2442
|
formOpen
|
|
@@ -1891,6 +2453,8 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1891
2453
|
h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_traveler.md" }),
|
|
1892
2454
|
h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
|
|
1893
2455
|
h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
|
|
2456
|
+
h("label", { className: "smem-label", key: "lt" }, "自定义标签(可选,逗号分隔;会写进 frontmatter 的 tags,列表页可按它筛)"),
|
|
2457
|
+
h("input", { className: "smem-input", key: "tt", value: formTags, onChange: function (e) { setFormTags(e.target.value); }, placeholder: "例如 发布, 脱敏, dsh" }),
|
|
1894
2458
|
h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
|
|
1895
2459
|
h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
|
|
1896
2460
|
h("div", { className: "smem-row", key: "btns" }, [
|
|
@@ -1899,7 +2463,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1899
2463
|
]),
|
|
1900
2464
|
])
|
|
1901
2465
|
: null,
|
|
1902
|
-
tab === "
|
|
2466
|
+
tab === "dream"
|
|
2467
|
+
? h(DreamPanel, { key: "dream", ctx: props.ctx })
|
|
2468
|
+
: tab === "star"
|
|
1903
2469
|
? h("div", { className: "smem-starmap", key: "starmap" },
|
|
1904
2470
|
h(StarmapRuntime.StarMap, { ctx: props.ctx }))
|
|
1905
2471
|
: loading && items.length === 0
|