sage-mem 2.1.19 → 2.1.21
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +87 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
14
14
|
|
|
15
|
-
**Current release: `2.1.
|
|
15
|
+
**Current release: `2.1.21`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
|
|
16
16
|
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
17
17
|
|
|
18
18
|
If it is useful to you, a Star is the most direct feedback.
|
package/README.zh.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
> File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
|
|
16
16
|
|
|
17
|
-
**当前发布版本:`2.1.
|
|
17
|
+
**当前发布版本:`2.1.21`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
|
|
18
18
|
[契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
|
|
19
19
|
|
|
20
20
|
如果它对你有用,给个 Star 是最直接的反馈。
|
package/lib/client.js
CHANGED
|
@@ -978,6 +978,22 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
978
978
|
return makeCanvas2DRenderer(canvasEl, layout, visBox, hoverBox);
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
+
/**
|
|
982
|
+
* 星图动画的统一时钟(Q15)。
|
|
983
|
+
*
|
|
984
|
+
* rAF 回调的时间戳与 `performance.now()` 是**同一个原点**(单调、从页面加载起算),
|
|
985
|
+
* 而 `Date.now()` 是 epoch 毫秒 —— 两套混用会让相位变成大负数。旧版入场动画就是这个
|
|
986
|
+
* bug:`t0 = Date.now()` 去减 rAF 的 `ts`,`ts - t0` 恒为大负数 → 永远「入场没结束」,
|
|
987
|
+
* 但每帧画完又不接着排帧,于是入场只画了一帧、相位还是负的。
|
|
988
|
+
*
|
|
989
|
+
* 星图里所有「过了多少毫秒」都走这里(freshness 那种墙钟判断除外,那本来就该用 Date.now)。
|
|
990
|
+
*/
|
|
991
|
+
function starmapNow() {
|
|
992
|
+
return (typeof performance !== "undefined" && performance && typeof performance.now === "function")
|
|
993
|
+
? performance.now()
|
|
994
|
+
: Date.now();
|
|
995
|
+
}
|
|
996
|
+
|
|
981
997
|
// ── 星图主体(canvas + 图例 + 详情面板)──
|
|
982
998
|
function StarMap(props) {
|
|
983
999
|
const ctx = props.ctx || appCtx;
|
|
@@ -1108,16 +1124,18 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1108
1124
|
if (!data || !layout || !canvasEl) return undefined;
|
|
1109
1125
|
const renderer = createRenderer(canvasEl, layout, visBox, hoverBox);
|
|
1110
1126
|
drawBox.renderer = renderer;
|
|
1111
|
-
|
|
1127
|
+
// t0 与 rAF 的 ts 必须同一时钟(见 starmapNow)。旧版用 Date.now 当 t0,
|
|
1128
|
+
// 于是相位是 epoch 级的大负数,入场窗口永远关不上。
|
|
1129
|
+
drawBox.t0 = starmapNow();
|
|
1112
1130
|
renderer.render(0);
|
|
1113
1131
|
|
|
1114
1132
|
// ── 按需重绘(重做过一版)──
|
|
1115
1133
|
// 旧版是一个常驻 rAF 循环:只要星图页开着就永远满帧重画,后台标签页也一样,
|
|
1116
1134
|
// 而画面其实早就静止了。新的规矩只有三种情况会画:
|
|
1117
1135
|
// 1. 入场展示动画期间(约 1.3s,星点按诞生时间依次点亮)
|
|
1118
|
-
// 2. 有人调 markDirty()(hover / 数据或筛选变化 /
|
|
1136
|
+
// 2. 有人调 markDirty()(hover / 数据或筛选变化 / 尺寸变化 / 回放)
|
|
1119
1137
|
// 3. 诞生回放进行中
|
|
1120
|
-
// 其余时间一帧都不画。另外帧率封顶 30fps
|
|
1138
|
+
// 其余时间一帧都不画。另外帧率封顶 30fps,页面不可见时暂停(回来再补一帧)。
|
|
1121
1139
|
const FRAME_MS = 1000 / 30;
|
|
1122
1140
|
const ENTRANCE_MS = 1300;
|
|
1123
1141
|
let raf = 0;
|
|
@@ -1126,26 +1144,51 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1126
1144
|
function schedule() {
|
|
1127
1145
|
if (!raf) raf = window.requestAnimationFrame(tick);
|
|
1128
1146
|
}
|
|
1147
|
+
/** 还需要连续帧的两种状态:入场动画没走完 / 诞生回放激活。 */
|
|
1148
|
+
function animating(ts) {
|
|
1149
|
+
return (ts - drawBox.t0 < ENTRANCE_MS) || replayBox.active === true;
|
|
1150
|
+
}
|
|
1129
1151
|
function tick(ts) {
|
|
1130
1152
|
raf = 0;
|
|
1131
1153
|
if (!drawBox.renderer) return;
|
|
1132
|
-
if (ts
|
|
1154
|
+
if (animating(ts)) dirty = true;
|
|
1155
|
+
// 页面不可见:这一帧不画,也**不**再排下一帧(旧版直接 return,结果从后台
|
|
1156
|
+
// 切回来就再也不画了)。visibilitychange 回到可见时会补一帧。
|
|
1157
|
+
if (typeof document !== "undefined" && document && document.hidden) {
|
|
1158
|
+
dirty = true;
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1133
1161
|
if (!dirty) return;
|
|
1134
|
-
if (typeof document !== "undefined" && document.hidden) return;
|
|
1135
1162
|
if (ts - lastDraw < FRAME_MS) { schedule(); return; }
|
|
1136
1163
|
dirty = false;
|
|
1137
1164
|
lastDraw = ts;
|
|
1138
1165
|
drawBox.renderer.render((ts - drawBox.t0) / 1000);
|
|
1166
|
+
// 入场 / 回放没结束就接着排下一帧;静止之后一帧都不排(不允许常驻空转)。
|
|
1167
|
+
if (animating(ts)) schedule();
|
|
1139
1168
|
}
|
|
1140
1169
|
drawBox.markDirty = function () { dirty = true; schedule(); };
|
|
1170
|
+
// 从后台标签页回来:补一帧(隐藏期间不排帧,靠这个事件唤醒)。
|
|
1171
|
+
const onVisibility = function () {
|
|
1172
|
+
if (typeof document === "undefined" || !document) return;
|
|
1173
|
+
if (document.hidden) { dirty = true; return; }
|
|
1174
|
+
dirty = true;
|
|
1175
|
+
schedule();
|
|
1176
|
+
};
|
|
1177
|
+
if (typeof document !== "undefined" && document && typeof document.addEventListener === "function") {
|
|
1178
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
1179
|
+
}
|
|
1141
1180
|
schedule();
|
|
1142
1181
|
|
|
1143
1182
|
return () => {
|
|
1144
1183
|
window.cancelAnimationFrame(raf);
|
|
1184
|
+
if (typeof document !== "undefined" && document && typeof document.removeEventListener === "function") {
|
|
1185
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
1186
|
+
}
|
|
1145
1187
|
drawBox.renderer = null;
|
|
1146
1188
|
drawBox.markDirty = null;
|
|
1147
1189
|
replayBox.active = false;
|
|
1148
1190
|
window.cancelAnimationFrame(replayBox.raf);
|
|
1191
|
+
replayBox.raf = 0;
|
|
1149
1192
|
if (renderer.destroy) renderer.destroy();
|
|
1150
1193
|
};
|
|
1151
1194
|
}, [data, layout, canvasEl, themeKey]);
|
|
@@ -1163,7 +1206,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1163
1206
|
if (drawBox.renderer.setGeometry) drawBox.renderer.setGeometry();
|
|
1164
1207
|
// 交给按需重绘那一套:下一帧画(<33ms),不再这里直接 render
|
|
1165
1208
|
if (drawBox.markDirty) drawBox.markDirty();
|
|
1166
|
-
else drawBox.renderer.render((
|
|
1209
|
+
else drawBox.renderer.render((starmapNow() - drawBox.t0) / 1000);
|
|
1167
1210
|
}
|
|
1168
1211
|
}, [data, layout, kindsOn, query, showArchive]);
|
|
1169
1212
|
|
|
@@ -1298,7 +1341,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1298
1341
|
function stopReplay() {
|
|
1299
1342
|
replayBox.active = false;
|
|
1300
1343
|
window.cancelAnimationFrame(replayBox.raf);
|
|
1344
|
+
replayBox.raf = 0;
|
|
1301
1345
|
if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(null);
|
|
1346
|
+
// 停回放要回到「全亮」:markDirty 让绘制循环补这一帧。
|
|
1302
1347
|
if (drawBox.markDirty) drawBox.markDirty();
|
|
1303
1348
|
setReplaying(false);
|
|
1304
1349
|
setReplayDate(null);
|
|
@@ -1316,17 +1361,22 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1316
1361
|
window.cancelAnimationFrame(replayBox.raf);
|
|
1317
1362
|
const span = Math.max(mx - mn, 60000) + 2400;
|
|
1318
1363
|
const dur = 18000;
|
|
1319
|
-
|
|
1364
|
+
let t0 = null;
|
|
1320
1365
|
replayBox.active = true;
|
|
1321
1366
|
setReplaying(true);
|
|
1367
|
+
// 关键:回放期间由「按需绘制」那套逐帧重画。setReveal 只改渲染器的变量,
|
|
1368
|
+
// 不标脏 / 不唤醒绘制循环的话屏幕上什么都不动(旧版就是这样,回放是空的)。
|
|
1369
|
+
if (drawBox.markDirty) drawBox.markDirty();
|
|
1322
1370
|
let lastLabel = 0;
|
|
1323
|
-
const tick = () => {
|
|
1371
|
+
const tick = (ts) => {
|
|
1324
1372
|
if (!replayBox.active) return;
|
|
1325
|
-
|
|
1373
|
+
// 进度也用 rAF 的单调时间戳,和绘制循环同一个时钟(旧版用 Date.now 又混了一次)。
|
|
1374
|
+
if (t0 === null) t0 = ts;
|
|
1375
|
+
const p = Math.min(1, (ts - t0) / dur);
|
|
1326
1376
|
const cur = mn - 1200 + span * p;
|
|
1327
1377
|
if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(cur);
|
|
1328
|
-
if (
|
|
1329
|
-
lastLabel =
|
|
1378
|
+
if (ts - lastLabel > 250) {
|
|
1379
|
+
lastLabel = ts;
|
|
1330
1380
|
setReplayDate(Math.min(mx, cur));
|
|
1331
1381
|
}
|
|
1332
1382
|
if (p >= 1) { stopReplay(); return; }
|
|
@@ -2758,6 +2808,20 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2758
2808
|
}, props.label);
|
|
2759
2809
|
}
|
|
2760
2810
|
|
|
2811
|
+
/**
|
|
2812
|
+
* 活动卡片「展开态」的合并(Q16,2.1.21)。
|
|
2813
|
+
*
|
|
2814
|
+
* 为什么是个纯函数:连续点两张卡(或同一帧里点两次)时,事件回调手里那个
|
|
2815
|
+
* `expanded` 可能还是上一帧的快照 —— 拿快照拼新对象会把另一笔改动覆盖掉。
|
|
2816
|
+
* 所以 Section 走**函数式 setState**,把「怎么合并」收敛到这里:
|
|
2817
|
+
* 只翻转目标文件那一条,其余原样保留。测试也直接喂 prev 来验这条语义。
|
|
2818
|
+
*/
|
|
2819
|
+
function mergeExpanded(prev, file) {
|
|
2820
|
+
const next = Object.assign({}, prev);
|
|
2821
|
+
next[file] = !(prev && prev[file]);
|
|
2822
|
+
return next;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2761
2825
|
function Card(props) {
|
|
2762
2826
|
const item = props.item;
|
|
2763
2827
|
const meta = TYPE_META[item.type] || TYPE_META.reference;
|
|
@@ -5590,7 +5654,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5590
5654
|
// 只负责「按状态/筛选把卡片摆出来 + 四种空态」;状态、筛选语义、动作回调全部由 Section 传。
|
|
5591
5655
|
// 抽出的判据:它是 Section 里最大的一块纯渲染,而卡片组件(Card / ArchivedCard)本来就在 50 共享层。
|
|
5592
5656
|
function FilesView(props) {
|
|
5593
|
-
const { loading, totalCount, openAdd, createIndexSkeleton, statusFilter, setStatusFilter, items, archiveItems, filtered, setTypeFilter, setTagFilter, setQuery, busy, restoring, archDelete, archEdit, archLoad, archSaving, archText, setArchText, openArchEdit, saveArchEdit, cancelArchEdit, restoreFile, setArchDelete, setArchEdit, deleteArchivedFile, expanded, deleting, archiving, archiveReason, setArchiveReason, setArchiving, setDeleting, confirmArchive, toggleLock, openEdit, confirmDelete } = props;
|
|
5657
|
+
const { loading, totalCount, openAdd, createIndexSkeleton, statusFilter, setStatusFilter, items, archiveItems, filtered, setTypeFilter, setTagFilter, setQuery, busy, restoring, archDelete, archEdit, archLoad, archSaving, archText, setArchText, openArchEdit, saveArchEdit, cancelArchEdit, restoreFile, setArchDelete, setArchEdit, deleteArchivedFile, expanded, onToggleExpanded, deleting, archiving, archiveReason, setArchiveReason, setArchiving, setDeleting, confirmArchive, toggleLock, openEdit, confirmDelete } = props;
|
|
5594
5658
|
let filesView;
|
|
5595
5659
|
if (loading && totalCount === 0) {
|
|
5596
5660
|
filesView = h("div", { className: "smem-skeleton", key: "loading", "data-loading": "1" }, [0, 1, 2].map(function (i) {
|
|
@@ -5654,11 +5718,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5654
5718
|
onCancelArchive: function () { setArchiving(null); setArchiveReason(""); },
|
|
5655
5719
|
onConfirmArchive: confirmArchive,
|
|
5656
5720
|
onToggleLock: toggleLock,
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
},
|
|
5721
|
+
// 展开 / 收起是**业务回调**(Q16):卡片只上报「点了哪张」,
|
|
5722
|
+
// 合并逻辑归 Section。旧版这里引用旧 Section 闭包里的 `setExpanded` ——
|
|
5723
|
+
// 拆件后它不在本函数作用域,点击即 ReferenceError: setExpanded is not defined。
|
|
5724
|
+
onToggle: onToggleExpanded,
|
|
5662
5725
|
onEdit: openEdit,
|
|
5663
5726
|
onAskDelete: function (file) { setDeleting(file); setArchiving(null); },
|
|
5664
5727
|
onCancelDelete: function () { setDeleting(null); },
|
|
@@ -5778,6 +5841,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
5778
5841
|
const query = sQuery[0], setQuery = sQuery[1];
|
|
5779
5842
|
const sExpanded = react.useState({});
|
|
5780
5843
|
const expanded = sExpanded[0], setExpanded = sExpanded[1];
|
|
5844
|
+
// Q16:卡片「展开 / 收起」是业务回调(FilesView 只上报点了哪张卡)。
|
|
5845
|
+
// 必须走**函数式** setState:事件里捕获的 expanded 可能是上一帧快照,
|
|
5846
|
+
// 直接 Object.assign({}, expanded, …) 会把连续点击的上一笔改动覆盖掉。
|
|
5847
|
+
function onToggleExpanded(file) {
|
|
5848
|
+
setExpanded(function (prev) { return mergeExpanded(prev, file); });
|
|
5849
|
+
}
|
|
5781
5850
|
const sDeleting = react.useState(null);
|
|
5782
5851
|
const deleting = sDeleting[0], setDeleting = sDeleting[1];
|
|
5783
5852
|
const sFormOpen = react.useState(false);
|
|
@@ -6790,7 +6859,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
6790
6859
|
archText: archText, setArchText: setArchText,
|
|
6791
6860
|
openArchEdit: openArchEdit, saveArchEdit: saveArchEdit, cancelArchEdit: cancelArchEdit,
|
|
6792
6861
|
restoreFile: restoreFile, setArchDelete: setArchDelete, setArchEdit: setArchEdit, deleteArchivedFile: deleteArchivedFile,
|
|
6793
|
-
expanded: expanded, deleting: deleting, archiving: archiving,
|
|
6862
|
+
expanded: expanded, onToggleExpanded: onToggleExpanded, deleting: deleting, archiving: archiving,
|
|
6794
6863
|
archiveReason: archiveReason, setArchiveReason: setArchiveReason,
|
|
6795
6864
|
setArchiving: setArchiving, setDeleting: setDeleting,
|
|
6796
6865
|
confirmArchive: confirmArchive, toggleLock: toggleLock,
|
package/package.json
CHANGED