dsh-recall-plugin 2.4.6 → 2.4.8
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/CHANGELOG.md +46 -0
- package/README.en.md +126 -35
- package/README.md +129 -40
- package/lib/client.js +824 -199
- package/lib/config.js +11 -4
- package/lib/errors.js +3 -0
- package/lib/exclude-patterns.js +2 -1
- package/lib/index.js +35 -5
- package/lib/intent-journal.js +108 -0
- package/lib/maintenance.js +9 -4
- package/lib/routes-core.js +89 -6
- package/lib/routes-manage.js +24 -6
- package/lib/scripts.posix.js +4 -0
- package/lib/scripts.pwsh.js +4 -0
- package/lib/snapshots.js +74 -4
- package/lib/store.js +7 -5
- package/package.json +8 -2
package/lib/snapshots.js
CHANGED
|
@@ -140,6 +140,20 @@ function parseTagsWithTime(text) {
|
|
|
140
140
|
}
|
|
141
141
|
return out;
|
|
142
142
|
}
|
|
143
|
+
function judgeStoreFormat(raw, supported) {
|
|
144
|
+
if (raw === null) return { ok: false, v: null, reason: "unreadable" };
|
|
145
|
+
const t = String(raw == null ? "" : raw).trim();
|
|
146
|
+
let v;
|
|
147
|
+
if (!t) {
|
|
148
|
+
v = 1;
|
|
149
|
+
} else {
|
|
150
|
+
if (!/^\d+$/.test(t)) return { ok: false, v: null, reason: "corrupt" };
|
|
151
|
+
v = parseInt(t, 10);
|
|
152
|
+
if (v < 1) return { ok: false, v: null, reason: "corrupt" };
|
|
153
|
+
}
|
|
154
|
+
if (v > supported) return { ok: false, v, reason: "future" };
|
|
155
|
+
return { ok: true, v, reason: t ? "ok" : "absent" };
|
|
156
|
+
}
|
|
143
157
|
function createSnapshots(ctx, rt, config) {
|
|
144
158
|
const sessions = ctx.sessions;
|
|
145
159
|
const state = rt.state;
|
|
@@ -149,9 +163,58 @@ function createSnapshots(ctx, rt, config) {
|
|
|
149
163
|
const FUSE_BACKOFF_BASE_MS = 5 * 60 * 1e3;
|
|
150
164
|
const FUSE_BACKOFF_CAP_MS = 60 * 60 * 1e3;
|
|
151
165
|
const snapFailures = /* @__PURE__ */ new Map();
|
|
166
|
+
const SUPPORTED_FORMAT = 1;
|
|
167
|
+
const FORMAT_OK_TTL_MS = 3e4;
|
|
168
|
+
const formatOkAt = /* @__PURE__ */ new Map();
|
|
169
|
+
const formatBlockedAt = /* @__PURE__ */ new Map();
|
|
170
|
+
function formatFile(store) {
|
|
171
|
+
return store.dir + (rt.isWin ? "\\" : "/") + "format";
|
|
172
|
+
}
|
|
173
|
+
async function readFormatRaw(store) {
|
|
174
|
+
try {
|
|
175
|
+
return S.stripBom(await rt.runShell(S.fileReadCmd(formatFile(store)), { stdoutMaxBytes: 4096 }));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function storeFormatVerdict(store) {
|
|
181
|
+
const at = formatOkAt.get(store.dir);
|
|
182
|
+
if (at !== void 0 && Date.now() - at < FORMAT_OK_TTL_MS) return { ok: true, v: SUPPORTED_FORMAT, reason: "ok" };
|
|
183
|
+
const verdict = judgeStoreFormat(await readFormatRaw(store), SUPPORTED_FORMAT);
|
|
184
|
+
if (verdict.ok && verdict.reason === "ok") formatOkAt.set(store.dir, Date.now());
|
|
185
|
+
return verdict;
|
|
186
|
+
}
|
|
187
|
+
function formatBlockedThrottled(store, verdict) {
|
|
188
|
+
const last = formatBlockedAt.get(store.dir) || 0;
|
|
189
|
+
if (Date.now() - last < 5 * 60 * 1e3) return;
|
|
190
|
+
formatBlockedAt.set(store.dir, Date.now());
|
|
191
|
+
const detail = verdict.reason === "future" ? "\u78C1\u76D8\u683C\u5F0F\u6807\u8BB0\u4E3A v" + verdict.v + "\uFF08\u672C\u63D2\u4EF6\u652F\u6301 v" + SUPPORTED_FORMAT + "\uFF09\uFF1A\u5199\u5165\u5DF2\u6682\u505C\uFF08\u5FEB\u7167/\u64A4\u56DE/\u6E05\u7406\uFF09\uFF0C\u5217\u8868\u4ECD\u53EF\u67E5\u770B\uFF1B\u8BF7\u5347\u7EA7\u63D2\u4EF6" : verdict.reason === "corrupt" ? "\u78C1\u76D8\u683C\u5F0F\u6807\u8BB0\u5185\u5BB9\u975E\u6CD5\uFF08\u5E94\u4E3A " + SUPPORTED_FORMAT + "\uFF09\uFF1A\u5199\u5165\u5DF2\u6682\u505C\uFF08\u5FEB\u7167/\u64A4\u56DE/\u6E05\u7406\uFF09\uFF1B\u8BF7\u68C0\u67E5\u8BE5 store \u76EE\u5F55\u4E0B\u7684 format \u6587\u4EF6" : "\u8BFB\u4E0D\u5230\u5FEB\u7167\u5E93\u7684\u78C1\u76D8\u683C\u5F0F\u6807\u8BB0\uFF08\u73AF\u5883\u672A\u5C31\u7EEA\u6216\u6587\u4EF6\u4E0D\u53EF\u8BFB\uFF09\uFF1A\u672C\u6B21\u5199\u5165\u5DF2\u6682\u505C\uFF0C\u4E0B\u4E00\u6761\u6D88\u606F\u81EA\u52A8\u91CD\u8BD5\uFF1B\u6301\u7EED\u51FA\u73B0\u8BF7\u68C0\u67E5\u8BE5 store \u76EE\u5F55\u6743\u9650";
|
|
192
|
+
rt.recordError("recall store format blocked: " + detail);
|
|
193
|
+
}
|
|
194
|
+
async function guardStoreFormat(store) {
|
|
195
|
+
const verdict = await storeFormatVerdict(store);
|
|
196
|
+
if (verdict.ok) return true;
|
|
197
|
+
formatBlockedThrottled(store, verdict);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
async function stampStoreFormat(store, verdict) {
|
|
201
|
+
if (!verdict.ok || verdict.reason !== "absent") return;
|
|
202
|
+
try {
|
|
203
|
+
await rt.writeTextViaShell(formatFile(store), String(SUPPORTED_FORMAT));
|
|
204
|
+
formatOkAt.set(store.dir, Date.now());
|
|
205
|
+
} catch (error) {
|
|
206
|
+
rt.recordError("recall format stamp failed: " + String(error));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
152
209
|
async function saveIndex(root, sessionId) {
|
|
153
210
|
const store = state.stores.get(root);
|
|
154
211
|
if (!store) return;
|
|
212
|
+
const verdict = await storeFormatVerdict(store);
|
|
213
|
+
if (!verdict.ok) {
|
|
214
|
+
formatBlockedThrottled(store, verdict);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await stampStoreFormat(store, verdict);
|
|
155
218
|
const entries = Array.from(state.snapshots.entries()).filter(([, s]) => s.root === root).map(([id, s]) => {
|
|
156
219
|
const rec = { id, time: s.time, root: s.root, sessionId: s.sessionId };
|
|
157
220
|
const fb = state.snapFeedback.get(id);
|
|
@@ -168,6 +231,7 @@ function createSnapshots(ctx, rt, config) {
|
|
|
168
231
|
if (state.indexLoaded.has(root)) return;
|
|
169
232
|
const store = state.stores.get(root);
|
|
170
233
|
if (!store) return;
|
|
234
|
+
if (!await guardStoreFormat(store)) return;
|
|
171
235
|
let raw = "";
|
|
172
236
|
let truncated = false;
|
|
173
237
|
try {
|
|
@@ -259,6 +323,7 @@ function createSnapshots(ctx, rt, config) {
|
|
|
259
323
|
async function recordLineage(root, childId, parentId) {
|
|
260
324
|
const store = state.stores.get(root);
|
|
261
325
|
if (!store) return;
|
|
326
|
+
if (!await guardStoreFormat(store)) return;
|
|
262
327
|
const sep = rt.isWin ? "\\" : "/";
|
|
263
328
|
const existing = await loadLineage(root);
|
|
264
329
|
if (!existing.some((e) => e.childId === childId && e.parentId === parentId)) {
|
|
@@ -313,6 +378,7 @@ function createSnapshots(ctx, rt, config) {
|
|
|
313
378
|
if (buildArtifactRootSegment(root, BASE(), rt.isWin)) return;
|
|
314
379
|
let store = await rt.resolveStore(root);
|
|
315
380
|
store = await rt.tryUpgradeToHome(root);
|
|
381
|
+
if (!await guardStoreFormat(store)) return;
|
|
316
382
|
const g = await rt.ensureGit(root, store);
|
|
317
383
|
if (!g.ok) {
|
|
318
384
|
setFeedback(messageId, { failed: true, ...buildFeedbackError(g.error || "\u672A\u77E5\u539F\u56E0") });
|
|
@@ -336,7 +402,10 @@ function createSnapshots(ctx, rt, config) {
|
|
|
336
402
|
const keep = rec && (rec.failed || Array.isArray(rec.skipped) && rec.skipped.length);
|
|
337
403
|
if (keep) state.snapFeedback.set(id, rec);
|
|
338
404
|
else state.snapFeedback.delete(id);
|
|
339
|
-
if (state.snapFeedback.size > 200)
|
|
405
|
+
if (state.snapFeedback.size > 200) {
|
|
406
|
+
const oldest = state.snapFeedback.keys().next().value;
|
|
407
|
+
if (oldest !== void 0) state.snapFeedback.delete(oldest);
|
|
408
|
+
}
|
|
340
409
|
}
|
|
341
410
|
async function feedbackFor(sessionId, messageId) {
|
|
342
411
|
const rec = state.snapFeedback.get(String(messageId || ""));
|
|
@@ -391,8 +460,8 @@ function createSnapshots(ctx, rt, config) {
|
|
|
391
460
|
if (!m) {
|
|
392
461
|
return { ok: false, partial: true, error: "\u56DE\u9000\u811A\u672C\u672A\u6B63\u5E38\u5B8C\u6210\uFF08\u5DE5\u4F5C\u533A\u53EF\u80FD\u5904\u4E8E\u534A\u56DE\u9000\u72B6\u6001\uFF09\uFF1A" + text.slice(0, 300) };
|
|
393
462
|
}
|
|
394
|
-
const deleted = parseInt(m[1], 10);
|
|
395
|
-
const restored = parseInt(m[2], 10);
|
|
463
|
+
const deleted = parseInt(m[1] ?? "", 10);
|
|
464
|
+
const restored = parseInt(m[2] ?? "", 10);
|
|
396
465
|
return { ok: true, count: (Number.isNaN(deleted) ? 0 : deleted) + (Number.isNaN(restored) ? 0 : restored) };
|
|
397
466
|
} catch (error) {
|
|
398
467
|
const re = error;
|
|
@@ -461,11 +530,12 @@ function createSnapshots(ctx, rt, config) {
|
|
|
461
530
|
return [];
|
|
462
531
|
}
|
|
463
532
|
}
|
|
464
|
-
return { saveIndex, loadIndex, readExclude, writeExclude, rebuildOrphans, captureSnapshot, diffFor, rollbackFor, resolveCutSeq, resolveStaleQueueItemIds, feedbackFor, loadLineage, recordLineage };
|
|
533
|
+
return { saveIndex, loadIndex, readExclude, writeExclude, rebuildOrphans, captureSnapshot, diffFor, rollbackFor, resolveCutSeq, resolveStaleQueueItemIds, feedbackFor, loadLineage, recordLineage, guardStoreFormat };
|
|
465
534
|
}
|
|
466
535
|
export {
|
|
467
536
|
createSnapshots,
|
|
468
537
|
isSafetySnapshotId,
|
|
538
|
+
judgeStoreFormat,
|
|
469
539
|
parseChanges,
|
|
470
540
|
parseDiffOutput,
|
|
471
541
|
parseSkipped,
|
package/lib/store.js
CHANGED
|
@@ -29,7 +29,7 @@ async function resolvePosixHomeBase(deps, inputs) {
|
|
|
29
29
|
}
|
|
30
30
|
function parseCleanupResult(out) {
|
|
31
31
|
const m = String(out || "").match(/CLEANUP_OTHER_INSTANCE\s+(\d+)/);
|
|
32
|
-
if (m) return { otherPid: parseInt(m[1], 10), skippedFresh: false };
|
|
32
|
+
if (m) return { otherPid: parseInt(m[1] ?? "", 10), skippedFresh: false };
|
|
33
33
|
if (String(out || "").indexOf("CLEANUP_SKIPPED_FRESH_LOCK") >= 0) return { otherPid: null, skippedFresh: true };
|
|
34
34
|
return { otherPid: null, skippedFresh: false };
|
|
35
35
|
}
|
|
@@ -84,8 +84,10 @@ async function runViaSpawn(spawn, req) {
|
|
|
84
84
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
85
85
|
chunks.push(buf);
|
|
86
86
|
total += buf.length;
|
|
87
|
-
while (chunks.length > 1
|
|
88
|
-
|
|
87
|
+
while (chunks.length > 1) {
|
|
88
|
+
const head = chunks[0];
|
|
89
|
+
if (!head || total - head.length <= maxBytes) break;
|
|
90
|
+
total -= head.length;
|
|
89
91
|
chunks.shift();
|
|
90
92
|
}
|
|
91
93
|
},
|
|
@@ -275,7 +277,7 @@ function createRuntime(ctx, config, deps) {
|
|
|
275
277
|
}
|
|
276
278
|
function extractGitDir(command) {
|
|
277
279
|
const m = String(command).match(/(?:^|\n)[ \t]*(?:\$g|g)[ \t]*=[ \t]*'([^']+)/);
|
|
278
|
-
return m ? m[1] : null;
|
|
280
|
+
return m ? m[1] ?? null : null;
|
|
279
281
|
}
|
|
280
282
|
async function cleanupAfterGitFailure(command) {
|
|
281
283
|
if (!command || String(command).indexOf("RECALL_CLEANUP") >= 0) return;
|
|
@@ -487,7 +489,7 @@ function createRuntime(ctx, config, deps) {
|
|
|
487
489
|
const out = scripts.stripBom(await runShell(scripts.ensureGitScript(store, gitExe, config.baseExcludes), { stdoutMaxBytes: 4096 }));
|
|
488
490
|
state.gitReady.add(store.git);
|
|
489
491
|
const m = out.match(/GIT_OK\s+(\d+)/);
|
|
490
|
-
state.gcLastAt.set(store.git, m ? parseInt(m[1], 10) * 1e3 : Date.now());
|
|
492
|
+
state.gcLastAt.set(store.git, m ? parseInt(m[1] ?? "", 10) * 1e3 : Date.now());
|
|
491
493
|
return { ok: true };
|
|
492
494
|
} catch (error) {
|
|
493
495
|
recordError("recall ensureGit failed: " + String(error));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recall-plugin",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.8",
|
|
4
4
|
"description": "在 DSH 中撤回自己发过的消息:工作区文件与对话历史一起回到那条消息发出之前,消息的文本与附件自动放回输入框,改完即可重发。文件走独立影子 git 快照(不碰项目自身的 git),对话走官方 sessions.fork,原会话归档可找回。",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"check:upgrade": "node scripts/check-upgrade.mjs",
|
|
35
35
|
"build": "node scripts/build-host.mjs && node scripts/build-client.mjs",
|
|
36
36
|
"test": "vitest run tests/unit",
|
|
37
|
+
"test:client": "vitest run --config vitest.client.config.ts",
|
|
37
38
|
"test:probe": "vitest run tests/probe",
|
|
38
39
|
"verify:host": "node scripts/verify-host.mjs",
|
|
39
40
|
"typecheck": "tsc --noEmit"
|
|
@@ -65,7 +66,8 @@
|
|
|
65
66
|
"0.1.7-alpha.2": "compatible",
|
|
66
67
|
"0.1.7-rc.1": "compatible",
|
|
67
68
|
"0.1.7-rc.2": "compatible",
|
|
68
|
-
"0.2.0-rc.1": "compatible"
|
|
69
|
+
"0.2.0-rc.1": "compatible",
|
|
70
|
+
"0.2.0-rc.2": "compatible"
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
},
|
|
@@ -90,7 +92,11 @@
|
|
|
90
92
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
91
93
|
"@types/node": "^20.19.43",
|
|
92
94
|
"@types/react": "^18.3.31",
|
|
95
|
+
"@types/react-dom": "^18.3.7",
|
|
93
96
|
"esbuild": "^0.28.1",
|
|
97
|
+
"jsdom": "^29.1.1",
|
|
98
|
+
"react": "^18.3.1",
|
|
99
|
+
"react-dom": "^18.3.1",
|
|
94
100
|
"typescript": "^5.9.3",
|
|
95
101
|
"vitest": "^4.1.11"
|
|
96
102
|
}
|