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/config.js
CHANGED
|
@@ -40,7 +40,11 @@ const Config = Schema.object({
|
|
|
40
40
|
refillDraft: withVolatile(Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\uFF08\u6587\u672C\u4E0E\u9644\u4EF6\uFF09\u56DE\u586B\u5230\u8F93\u5165\u6846")),
|
|
41
41
|
snapshotEnabled: withVolatile(Schema.boolean().default(true).description("\u542F\u7528\u6D88\u606F\u5FEB\u7167\uFF08\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\uFF0C\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09")),
|
|
42
42
|
archiveOriginal: withVolatile(Schema.boolean().default(true).description("\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD\uFF08\u5173\u95ED\u540E\u539F\u4F1A\u8BDD\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF09")),
|
|
43
|
-
retentionDays: withVolatile(Schema.number().default(0).description("\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528"))
|
|
43
|
+
retentionDays: withVolatile(Schema.number().default(0).description("\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\uFF1B0 \u8868\u793A\u4E0D\u542F\u7528")),
|
|
44
|
+
// A4:界面语言。union 三值而不是自由字符串——非法配置在加载时响亮失败
|
|
45
|
+
//(合规清单 #3);description 保持中文是 v1 明示限制:它由宿主官方设置
|
|
46
|
+
// 表单在 host 侧渲染,插件无法本地化(自定义配置卡片不受影响,见 README)
|
|
47
|
+
locale: withVolatile(Schema.union(["auto", "zh", "en"]).default("auto").description("\u754C\u9762\u8BED\u8A00\uFF1Aauto \u8DDF\u968F\u7CFB\u7EDF\uFF0C\u53EF\u9009 zh / en\uFF08\u4FDD\u5B58\u540E\u751F\u6548\uFF09"))
|
|
44
48
|
});
|
|
45
49
|
function isLegacySettingsFace(settings) {
|
|
46
50
|
const svc = settings;
|
|
@@ -65,7 +69,8 @@ function resolveSettingsNs(ctx, settings) {
|
|
|
65
69
|
for (const id of entryIds) {
|
|
66
70
|
if (known.indexOf(id) >= 0) return id;
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
const first = entryIds[0];
|
|
73
|
+
return first !== void 0 ? first : null;
|
|
69
74
|
}
|
|
70
75
|
function describeNamespaces(settings) {
|
|
71
76
|
const svc = settings;
|
|
@@ -119,7 +124,8 @@ const DEFAULTS = {
|
|
|
119
124
|
refillDraft: true,
|
|
120
125
|
snapshotEnabled: true,
|
|
121
126
|
archiveOriginal: true,
|
|
122
|
-
retentionDays: 0
|
|
127
|
+
retentionDays: 0,
|
|
128
|
+
locale: "auto"
|
|
123
129
|
};
|
|
124
130
|
function createConfig(raw) {
|
|
125
131
|
const cfg = raw && typeof raw === "object" ? raw : {};
|
|
@@ -139,7 +145,8 @@ function createConfig(raw) {
|
|
|
139
145
|
const archiveOriginal = typeof cfg.archiveOriginal === "boolean" ? cfg.archiveOriginal : true;
|
|
140
146
|
const rawDays = typeof cfg.retentionDays === "number" ? cfg.retentionDays : parseInt(String(cfg.retentionDays == null ? "" : cfg.retentionDays), 10);
|
|
141
147
|
const retentionDays = Number.isFinite(rawDays) ? Math.max(0, rawDays) : 0;
|
|
142
|
-
|
|
148
|
+
const locale = cfg.locale === "zh" || cfg.locale === "en" ? cfg.locale : "auto";
|
|
149
|
+
return { gcSnaps, gcHours, maxFileBytes, maxSnapshotsPerWorkspace, baseExcludes, refillDraft, snapshotEnabled, archiveOriginal, retentionDays, locale };
|
|
143
150
|
}
|
|
144
151
|
export {
|
|
145
152
|
Config,
|
package/lib/errors.js
CHANGED
|
@@ -14,6 +14,7 @@ const RECALL_NO_ROOT = "NO_ROOT";
|
|
|
14
14
|
const RECALL_NO_SESSION = "NO_SESSION";
|
|
15
15
|
const RECALL_PARTIAL_DELETE = "PARTIAL_DELETE";
|
|
16
16
|
const RECALL_UNKNOWN_OP = "UNKNOWN_OP";
|
|
17
|
+
const RECALL_FORMAT_BLOCKED = "FORMAT_BLOCKED";
|
|
17
18
|
const RECALL_UNKNOWN_ENDPOINT = "UNKNOWN_ENDPOINT";
|
|
18
19
|
const RECALL_INDEX_CORRUPT = "INDEX_CORRUPT";
|
|
19
20
|
const ALL_CODES = Object.freeze([
|
|
@@ -33,6 +34,7 @@ const ALL_CODES = Object.freeze([
|
|
|
33
34
|
RECALL_NO_SESSION,
|
|
34
35
|
RECALL_PARTIAL_DELETE,
|
|
35
36
|
RECALL_UNKNOWN_OP,
|
|
37
|
+
RECALL_FORMAT_BLOCKED,
|
|
36
38
|
RECALL_UNKNOWN_ENDPOINT,
|
|
37
39
|
RECALL_INDEX_CORRUPT
|
|
38
40
|
]);
|
|
@@ -43,6 +45,7 @@ export {
|
|
|
43
45
|
RECALL_BODY_TOO_LARGE,
|
|
44
46
|
RECALL_EMPTY_PATCH,
|
|
45
47
|
RECALL_ERROR,
|
|
48
|
+
RECALL_FORMAT_BLOCKED,
|
|
46
49
|
RECALL_INDEX_CORRUPT,
|
|
47
50
|
RECALL_NO_ROOT,
|
|
48
51
|
RECALL_NO_SESSION,
|
package/lib/exclude-patterns.js
CHANGED
|
@@ -20,7 +20,8 @@ function buildArtifactRootSegment(root, base, isWin) {
|
|
|
20
20
|
const pool = isWin ? names.map((n) => n.toLowerCase()) : names;
|
|
21
21
|
for (const seg of segs) {
|
|
22
22
|
const i = pool.indexOf(isWin ? seg.toLowerCase() : seg);
|
|
23
|
-
|
|
23
|
+
const hit = i >= 0 ? names[i] : void 0;
|
|
24
|
+
if (hit !== void 0) return hit;
|
|
24
25
|
}
|
|
25
26
|
return null;
|
|
26
27
|
}
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createMaintenance } from "./maintenance.js";
|
|
|
7
7
|
import { createSessionInfo, titleFromEvents, messageTextFromEvents } from "./session-info.js";
|
|
8
8
|
import { createRoutesCore } from "./routes-core.js";
|
|
9
9
|
import { createRoutesManage } from "./routes-manage.js";
|
|
10
|
+
import { createIntentJournal } from "./intent-journal.js";
|
|
10
11
|
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
11
12
|
import * as E from "./errors.js";
|
|
12
13
|
const name = "dsh-recall-plugin";
|
|
@@ -65,7 +66,7 @@ function apply(ctx, config) {
|
|
|
65
66
|
const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
|
|
66
67
|
while (index < tasks.length) {
|
|
67
68
|
const task = tasks[index++];
|
|
68
|
-
await task();
|
|
69
|
+
if (task) await task();
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
await Promise.all(workers);
|
|
@@ -215,6 +216,26 @@ function apply(ctx, config) {
|
|
|
215
216
|
}
|
|
216
217
|
return records;
|
|
217
218
|
}
|
|
219
|
+
const intentJournal = createIntentJournal({
|
|
220
|
+
runShell: rt.runShell,
|
|
221
|
+
writeTextViaShell: rt.writeTextViaShell,
|
|
222
|
+
scripts: rt.scripts,
|
|
223
|
+
isWin: rt.isWin,
|
|
224
|
+
recordError: rt.recordError,
|
|
225
|
+
workspaceMatchesTag: async (messageId) => {
|
|
226
|
+
const d = await snaps.diffFor(messageId);
|
|
227
|
+
return Boolean(d && d.total === 0 && d.changes.length === 0);
|
|
228
|
+
},
|
|
229
|
+
resetToSafety: async (store, safetyId, root) => {
|
|
230
|
+
try {
|
|
231
|
+
const out = await rt.runShell(rt.scripts.rescueScript(root, store, state.gitExe || "", "snap-" + safetyId), { timeoutMs: 6e5, stdoutMaxBytes: 65536 });
|
|
232
|
+
return String(out || "").indexOf("RESCUE_OK") >= 0;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
agentBusy: (root) => agentBusy(null, root)
|
|
238
|
+
});
|
|
218
239
|
const deps = {
|
|
219
240
|
ctx,
|
|
220
241
|
rt,
|
|
@@ -245,14 +266,18 @@ function apply(ctx, config) {
|
|
|
245
266
|
readSettings: () => readSettings(),
|
|
246
267
|
DEFAULTS,
|
|
247
268
|
rescueRollback,
|
|
248
|
-
|
|
269
|
+
intentJournal,
|
|
270
|
+
E,
|
|
271
|
+
// issue #19 撤回终态事件广播:闭包注入保持 ctx 不解构纪律,routes 域
|
|
272
|
+
// 不直接持有 ctx;单测可注入桩断言事件名与 payload
|
|
273
|
+
emitEvent: (event, payload) => ctx.emit(event, payload)
|
|
249
274
|
};
|
|
250
275
|
const endpoints = {
|
|
251
276
|
...createRoutesCore(deps),
|
|
252
277
|
...createRoutesManage(deps)
|
|
253
278
|
};
|
|
254
279
|
const handleRecall = async (request) => {
|
|
255
|
-
const name2 = new URL(request.url).pathname.replace(/^\/api\/recall\/?/, "").split("/")[0];
|
|
280
|
+
const name2 = new URL(request.url).pathname.replace(/^\/api\/recall\/?/, "").split("/")[0] ?? "";
|
|
256
281
|
const endpoint = endpoints[name2];
|
|
257
282
|
if (!endpoint) {
|
|
258
283
|
return jsonResponse({ ok: false, code: E.RECALL_UNKNOWN_ENDPOINT, message: "unknown endpoint: " + name2 });
|
|
@@ -319,10 +344,15 @@ function apply(ctx, config) {
|
|
|
319
344
|
}
|
|
320
345
|
}
|
|
321
346
|
for (const [cwd, sessionId] of warmupRoots) {
|
|
322
|
-
Promise.resolve(rt.resolveStore(cwd)).then(() => rt.tryUpgradeToHome(cwd)).then((store) => rt.ensureGit(cwd, store)).then(() => snaps.loadIndex(cwd, sessionId)).then(() => snaps.rebuildOrphans(cwd, sessionId)).then(
|
|
347
|
+
Promise.resolve(rt.resolveStore(cwd)).then(() => rt.tryUpgradeToHome(cwd)).then((store) => rt.ensureGit(cwd, store)).then(() => snaps.loadIndex(cwd, sessionId)).then(() => snaps.rebuildOrphans(cwd, sessionId)).then(async () => {
|
|
348
|
+
const st = state.stores.get(cwd);
|
|
349
|
+
if (st && await snaps.guardStoreFormat(st)) await intentJournal.recover(st);
|
|
350
|
+
}).then(() => rt.cleanupLegacy(cwd)).catch(() => {
|
|
323
351
|
});
|
|
324
352
|
}
|
|
325
|
-
})()
|
|
353
|
+
})().catch((error) => {
|
|
354
|
+
console.error("recall warmup skipped:", error);
|
|
355
|
+
});
|
|
326
356
|
}
|
|
327
357
|
export {
|
|
328
358
|
Config,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const INTENT_VERSION = 1;
|
|
2
|
+
function describeIntent(intent) {
|
|
3
|
+
return "message=" + intent.messageId + ", phase=" + intent.phase + ", safety=snap-" + intent.safetyId;
|
|
4
|
+
}
|
|
5
|
+
function createIntentJournal(deps) {
|
|
6
|
+
const handled = /* @__PURE__ */ new Set();
|
|
7
|
+
function intentFile(store) {
|
|
8
|
+
return store.dir + (deps.isWin ? "\\" : "/") + "recall-intent.json";
|
|
9
|
+
}
|
|
10
|
+
async function readRaw(store) {
|
|
11
|
+
let raw = "";
|
|
12
|
+
try {
|
|
13
|
+
raw = deps.scripts.stripBom(await deps.runShell(deps.scripts.fileReadCmd(intentFile(store)), { timeoutMs: 3e4, stdoutMaxBytes: 65536 })).trim();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return { ok: false, intent: null };
|
|
16
|
+
}
|
|
17
|
+
if (!raw) return { ok: true, intent: null };
|
|
18
|
+
try {
|
|
19
|
+
const obj = JSON.parse(raw);
|
|
20
|
+
if (!obj || obj.v !== INTENT_VERSION || obj.op !== "execute" || typeof obj.messageId !== "string" || !obj.messageId || typeof obj.safetyId !== "string" || !obj.safetyId || obj.phase !== "rollback" && obj.phase !== "rescue") {
|
|
21
|
+
return { ok: true, intent: null };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
intent: {
|
|
26
|
+
v: INTENT_VERSION,
|
|
27
|
+
op: "execute",
|
|
28
|
+
messageId: obj.messageId,
|
|
29
|
+
root: typeof obj.root === "string" ? obj.root : "",
|
|
30
|
+
safetyId: obj.safetyId,
|
|
31
|
+
safetyOk: obj.safetyOk !== false,
|
|
32
|
+
phase: obj.phase,
|
|
33
|
+
time: typeof obj.time === "number" ? obj.time : 0
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { ok: true, intent: null };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function write(store, text) {
|
|
41
|
+
try {
|
|
42
|
+
await deps.writeTextViaShell(intentFile(store), text);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
deps.recordError("recall intent journal write failed: " + String(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function read(store) {
|
|
48
|
+
return readRaw(store).then((r) => r.intent);
|
|
49
|
+
}
|
|
50
|
+
async function begin(store, root, fields) {
|
|
51
|
+
const intent = {
|
|
52
|
+
v: INTENT_VERSION,
|
|
53
|
+
op: "execute",
|
|
54
|
+
messageId: fields.messageId,
|
|
55
|
+
root,
|
|
56
|
+
safetyId: fields.safetyId,
|
|
57
|
+
safetyOk: fields.safetyOk,
|
|
58
|
+
phase: "rollback",
|
|
59
|
+
time: Date.now()
|
|
60
|
+
};
|
|
61
|
+
await write(store, JSON.stringify(intent));
|
|
62
|
+
}
|
|
63
|
+
async function advance(store, phase) {
|
|
64
|
+
const cur = await read(store);
|
|
65
|
+
if (cur) await write(store, JSON.stringify(Object.assign({}, cur, { phase })));
|
|
66
|
+
}
|
|
67
|
+
function clear(store) {
|
|
68
|
+
return write(store, "");
|
|
69
|
+
}
|
|
70
|
+
async function recover(store) {
|
|
71
|
+
if (handled.has(store.dir)) return false;
|
|
72
|
+
const got = await readRaw(store);
|
|
73
|
+
if (!got.ok) return false;
|
|
74
|
+
const intent = got.intent;
|
|
75
|
+
if (!intent) {
|
|
76
|
+
handled.add(store.dir);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (await deps.workspaceMatchesTag(intent.messageId)) {
|
|
80
|
+
await clear(store);
|
|
81
|
+
handled.add(store.dir);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (!intent.safetyOk) {
|
|
85
|
+
deps.recordError("recall interrupted rollback detected, no safety snapshot: " + describeIntent(intent) + "\uFF08\u8BB0\u5F55\u4FDD\u7559\u4E8E " + intentFile(store) + "\uFF09");
|
|
86
|
+
handled.add(store.dir);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (deps.agentBusy(intent.root)) {
|
|
90
|
+
deps.recordError("recall interrupted rollback deferred (agent busy): " + describeIntent(intent));
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const reset = await deps.resetToSafety(store, intent.safetyId, intent.root);
|
|
94
|
+
if (!reset) {
|
|
95
|
+
deps.recordError("recall interrupted rollback rescue failed: " + describeIntent(intent) + "\uFF08\u8BB0\u5F55\u4FDD\u7559\u4E8E " + intentFile(store) + "\uFF09");
|
|
96
|
+
handled.add(store.dir);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
await clear(store);
|
|
100
|
+
handled.add(store.dir);
|
|
101
|
+
deps.recordError("recovered interrupted rollback: " + describeIntent(intent) + " \u2014 \u5DE5\u4F5C\u533A\u5DF2\u590D\u4F4D\u5230\u5B89\u5168\u5FEB\u7167");
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return { begin, advance, clear, read, file: intentFile, recover };
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
createIntentJournal
|
|
108
|
+
};
|
package/lib/maintenance.js
CHANGED
|
@@ -5,8 +5,9 @@ function selectOverLimitVictims(snapshots, limit) {
|
|
|
5
5
|
const byRoot = /* @__PURE__ */ new Map();
|
|
6
6
|
for (const [id, s] of snapshots.entries()) {
|
|
7
7
|
if (!s || !s.root) continue;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
const list = byRoot.get(s.root);
|
|
9
|
+
if (list) list.push({ id, time: s.time });
|
|
10
|
+
else byRoot.set(s.root, [{ id, time: s.time }]);
|
|
10
11
|
}
|
|
11
12
|
const victims = /* @__PURE__ */ new Map();
|
|
12
13
|
for (const [root, list] of byRoot) {
|
|
@@ -37,8 +38,9 @@ function createMaintenance(ctx, rt, snaps, config, deps = {}) {
|
|
|
37
38
|
const byRoot = /* @__PURE__ */ new Map();
|
|
38
39
|
for (const [id, s] of state.snapshots.entries()) {
|
|
39
40
|
if (!s || s.sessionId !== sessionId) continue;
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const list = byRoot.get(s.root);
|
|
42
|
+
if (list) list.push(id);
|
|
43
|
+
else byRoot.set(s.root, [id]);
|
|
42
44
|
}
|
|
43
45
|
let purged = 0;
|
|
44
46
|
for (const [root, ids] of byRoot) {
|
|
@@ -51,6 +53,7 @@ function createMaintenance(ctx, rt, snaps, config, deps = {}) {
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
if (!store || !state.gitExe) continue;
|
|
56
|
+
if (!await snaps.guardStoreFormat(store)) continue;
|
|
54
57
|
try {
|
|
55
58
|
for (let i = 0; i < ids.length; i += 100) {
|
|
56
59
|
await rt.runShell(S.purgeTagsScript(store, state.gitExe, ids.slice(i, i + 100).map((id) => "snap-" + id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
|
@@ -99,6 +102,7 @@ function createMaintenance(ctx, rt, snaps, config, deps = {}) {
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
if (!store || !state.gitExe) continue;
|
|
105
|
+
if (!await snaps.guardStoreFormat(store)) continue;
|
|
102
106
|
try {
|
|
103
107
|
for (let i = 0; i < victims.length; i += 100) {
|
|
104
108
|
await rt.runShell(S.purgeTagsScript(store, state.gitExe, victims.slice(i, i + 100).map((v) => "snap-" + v.id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
|
@@ -127,6 +131,7 @@ function createMaintenance(ctx, rt, snaps, config, deps = {}) {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
if (!store || !state.gitExe) continue;
|
|
134
|
+
if (!await snaps.guardStoreFormat(store)) continue;
|
|
130
135
|
try {
|
|
131
136
|
for (let i = 0; i < victims.length; i += 100) {
|
|
132
137
|
await rt.runShell(S.purgeTagsScript(store, state.gitExe, victims.slice(i, i + 100).map((v) => "snap-" + v.id)), { timeoutMs: 12e4, stdoutMaxBytes: 4096 });
|
package/lib/routes-core.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ENV_HINTS } from "./diagnostics.js";
|
|
2
2
|
import { parseTreeId } from "./snapshots.js";
|
|
3
3
|
import { buildArtifactRootSegment, buildRootNotice } from "./exclude-patterns.js";
|
|
4
|
+
const RECALL_EVENT_COMPLETE_HOST = "dsh-recall/complete";
|
|
5
|
+
const RECALL_EVENT_FAILED_HOST = "dsh-recall/failed";
|
|
4
6
|
function createRoutesCore(deps) {
|
|
5
|
-
const { rt, snaps, state, cfg, supported, enqueue, agentBusy, rescueRollback, E } = deps;
|
|
7
|
+
const { rt, snaps, state, cfg, supported, enqueue, agentBusy, rescueRollback, intentJournal, emitEvent, E } = deps;
|
|
6
8
|
return {
|
|
7
9
|
"init": async (args) => {
|
|
8
10
|
if (!supported) {
|
|
@@ -17,15 +19,19 @@ function createRoutesCore(deps) {
|
|
|
17
19
|
await rt.ensureGit(root, store);
|
|
18
20
|
await snaps.loadIndex(root, sessionId);
|
|
19
21
|
await snaps.rebuildOrphans(root, sessionId);
|
|
22
|
+
if (store && await snaps.guardStoreFormat(store)) await intentJournal.recover(store);
|
|
20
23
|
rt.cleanupLegacy(root);
|
|
21
24
|
notice = {
|
|
22
25
|
gitMissing: state.gitExe === "",
|
|
23
26
|
homeFallback: store ? !store.home : false
|
|
24
27
|
};
|
|
25
28
|
const artifactSeg = buildArtifactRootSegment(root, cfg.baseExcludes, rt.isWin);
|
|
26
|
-
if (artifactSeg)
|
|
29
|
+
if (artifactSeg) {
|
|
30
|
+
notice.buildRootArtifactSeg = artifactSeg;
|
|
31
|
+
notice.buildRootNotice = buildRootNotice(artifactSeg);
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
|
-
return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft, archiveOriginal: cfg.archiveOriginal } };
|
|
34
|
+
return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft, archiveOriginal: cfg.archiveOriginal, locale: cfg.locale } };
|
|
29
35
|
},
|
|
30
36
|
"snapshot-info": async (args) => {
|
|
31
37
|
const id = args && args.messageId ? String(args.messageId) : "";
|
|
@@ -38,7 +44,9 @@ function createRoutesCore(deps) {
|
|
|
38
44
|
time: snap ? snap.time : null,
|
|
39
45
|
id,
|
|
40
46
|
...feedback,
|
|
41
|
-
|
|
47
|
+
// A4:段名与中文 notice 并存下发(client 优先段名本地取词,见 init 同处注释)
|
|
48
|
+
notice: artifactSeg ? buildRootNotice(artifactSeg) : void 0,
|
|
49
|
+
artifactSeg: artifactSeg || void 0
|
|
42
50
|
};
|
|
43
51
|
},
|
|
44
52
|
"preview": async (args) => {
|
|
@@ -69,6 +77,9 @@ function createRoutesCore(deps) {
|
|
|
69
77
|
if (!snap) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167" };
|
|
70
78
|
const store = state.stores.get(snap.root);
|
|
71
79
|
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528" };
|
|
80
|
+
if (!await snaps.guardStoreFormat(store)) {
|
|
81
|
+
return { ok: false, code: E.RECALL_FORMAT_BLOCKED, message: "\u78C1\u76D8\u683C\u5F0F\u4E0D\u53D7\u652F\u6301\uFF0C\u5DF2\u505C\u6B62\u5199\u5165\uFF1B\u8BE6\u89C1\u300C\u6700\u8FD1\u9519\u8BEF\u300D" };
|
|
82
|
+
}
|
|
72
83
|
if (agentBusy(sessionId, snap.root)) return { ok: false, code: E.RECALL_AGENT_BUSY, message: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE" };
|
|
73
84
|
const previewTreeId = args && typeof args.previewTreeId === "string" && args.previewTreeId ? args.previewTreeId : null;
|
|
74
85
|
if (!previewTreeId && args && typeof args.previewTotal === "number") {
|
|
@@ -90,12 +101,20 @@ function createRoutesCore(deps) {
|
|
|
90
101
|
if (previewTreeId && safetyTreeId && safetyTreeId !== previewTreeId) {
|
|
91
102
|
return { ok: false, code: E.RECALL_STALE, message: "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4" };
|
|
92
103
|
}
|
|
104
|
+
await intentJournal.begin(store, snap.root, { messageId: id, safetyId, safetyOk });
|
|
93
105
|
const rolled = await snaps.rollbackFor(id);
|
|
94
|
-
if (rolled.ok)
|
|
95
|
-
|
|
106
|
+
if (rolled.ok) {
|
|
107
|
+
await intentJournal.clear(store);
|
|
108
|
+
return rolled;
|
|
109
|
+
}
|
|
110
|
+
await intentJournal.advance(store, "rescue");
|
|
111
|
+
const rescueResult = await rescueRollback(
|
|
96
112
|
{ runShell: rt.runShell, scripts: rt.scripts, gitExe: state.gitExe || "", recordError: rt.recordError },
|
|
97
113
|
{ root: snap.root, store, safetyId, safetyOk, rollbackError: rolled.error }
|
|
98
114
|
);
|
|
115
|
+
await intentJournal.clear(store);
|
|
116
|
+
if (rescueResult && rescueResult.message) rescueResult.message += "\uFF08\u4E2D\u65AD\u70B9\u8BB0\u5F55\uFF1A" + intentJournal.file(store) + "\uFF09";
|
|
117
|
+
return rescueResult;
|
|
99
118
|
});
|
|
100
119
|
if (!result.ok) return result;
|
|
101
120
|
const cutSeq = await snaps.resolveCutSeq(sessionId, id);
|
|
@@ -142,9 +161,73 @@ function createRoutesCore(deps) {
|
|
|
142
161
|
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528" };
|
|
143
162
|
await snaps.recordLineage(root, childId, parentId);
|
|
144
163
|
return { ok: true };
|
|
164
|
+
},
|
|
165
|
+
// issue #19:client 撤回终态上报 → cordis 事件广播(dsh-recall/complete
|
|
166
|
+
// | dsh-recall/failed)。不进串行队列(零 git 操作)、不依赖 store——
|
|
167
|
+
// lineage-record 的 NO_STORE 早退在这里是缺陷(原会话已归档时解析不到
|
|
168
|
+
// store 会吞掉事件),端点只做 enrich 与转发。恒 { ok: true }:事件
|
|
169
|
+
// 发送失败不影响撤回主流程(client 侧 fire-and-forget,连响应都不等)。
|
|
170
|
+
"notify": async (args) => {
|
|
171
|
+
const status = args && args.status;
|
|
172
|
+
const sessionId = args && args.sessionId ? String(args.sessionId) : "";
|
|
173
|
+
const messageId = args && args.messageId ? String(args.messageId) : "";
|
|
174
|
+
if (status !== "complete" && status !== "failed" || !sessionId || !messageId) {
|
|
175
|
+
return { ok: false, code: E.RECALL_BAD_TYPE, message: "\u64A4\u56DE\u4E0A\u62A5\u7F3A\u5C11\u5FC5\u8981\u53C2\u6570" };
|
|
176
|
+
}
|
|
177
|
+
const scope = args && args.scope === "session-only" ? "session-only" : "both";
|
|
178
|
+
const cutSeq = args && typeof args.cutSeq === "number" ? args.cutSeq : null;
|
|
179
|
+
let root = null;
|
|
180
|
+
try {
|
|
181
|
+
root = await rt.resolveRoot(sessionId) || null;
|
|
182
|
+
} catch {
|
|
183
|
+
root = null;
|
|
184
|
+
}
|
|
185
|
+
let eventName;
|
|
186
|
+
let payload;
|
|
187
|
+
if (status === "complete") {
|
|
188
|
+
eventName = RECALL_EVENT_COMPLETE_HOST;
|
|
189
|
+
payload = {
|
|
190
|
+
version: 1,
|
|
191
|
+
sessionId,
|
|
192
|
+
childSessionId: args && args.childSessionId ? String(args.childSessionId) : null,
|
|
193
|
+
scope,
|
|
194
|
+
cutSeq,
|
|
195
|
+
messageId,
|
|
196
|
+
root,
|
|
197
|
+
count: args && typeof args.count === "number" ? args.count : 0,
|
|
198
|
+
chatReverted: Boolean(args && args.chatReverted),
|
|
199
|
+
archiveRequested: Boolean(args && args.archiveRequested),
|
|
200
|
+
time: Date.now()
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
eventName = RECALL_EVENT_FAILED_HOST;
|
|
204
|
+
const code = args && typeof args.code === "string" && args.code ? args.code : void 0;
|
|
205
|
+
payload = {
|
|
206
|
+
version: 1,
|
|
207
|
+
stage: args && args.stage === "fork" ? "fork" : "execute",
|
|
208
|
+
sessionId,
|
|
209
|
+
messageId,
|
|
210
|
+
scope,
|
|
211
|
+
cutSeq,
|
|
212
|
+
root,
|
|
213
|
+
...code ? { code } : {},
|
|
214
|
+
error: args && typeof args.error === "string" && args.error ? args.error : "unknown recall failure",
|
|
215
|
+
time: Date.now()
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
try {
|
|
220
|
+
emitEvent(eventName, payload);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
rt.recordError("recall event emit failed: " + String(error));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return { ok: true };
|
|
145
226
|
}
|
|
146
227
|
};
|
|
147
228
|
}
|
|
148
229
|
export {
|
|
230
|
+
RECALL_EVENT_COMPLETE_HOST,
|
|
231
|
+
RECALL_EVENT_FAILED_HOST,
|
|
149
232
|
createRoutesCore
|
|
150
233
|
};
|
package/lib/routes-manage.js
CHANGED
|
@@ -109,8 +109,9 @@ function createRoutesManage(deps) {
|
|
|
109
109
|
const byRoot = /* @__PURE__ */ new Map();
|
|
110
110
|
for (const rec of records.values()) {
|
|
111
111
|
if (!match(rec) || !rec.root) continue;
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
const list = byRoot.get(rec.root);
|
|
113
|
+
if (list) list.push(rec.id);
|
|
114
|
+
else byRoot.set(rec.root, [rec.id]);
|
|
114
115
|
}
|
|
115
116
|
let deleted = 0;
|
|
116
117
|
await enqueue(async () => {
|
|
@@ -124,6 +125,7 @@ function createRoutesManage(deps) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
if (!store) continue;
|
|
128
|
+
if (!await snaps.guardStoreFormat(store)) continue;
|
|
127
129
|
try {
|
|
128
130
|
if (state.gitExe) {
|
|
129
131
|
const tags = rootIds.map((id) => "snap-" + id);
|
|
@@ -181,6 +183,10 @@ function createRoutesManage(deps) {
|
|
|
181
183
|
let clearedStores = 0;
|
|
182
184
|
let failed = 0;
|
|
183
185
|
for (const { store, root } of stores.values()) {
|
|
186
|
+
if (!await snaps.guardStoreFormat(store)) {
|
|
187
|
+
failed += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
184
190
|
try {
|
|
185
191
|
const output = await rt.runShell(rt.scripts.listTagsScript(store, gitExe), { timeoutMs: 12e4, stdoutMaxBytes: 4194304 });
|
|
186
192
|
const tags = rt.scripts.stripBom(output).split(/\r?\n/).map((tag) => tag.trim()).filter((tag) => tag.indexOf("snap-") === 0);
|
|
@@ -279,7 +285,9 @@ function createRoutesManage(deps) {
|
|
|
279
285
|
refillDraft: cfg.refillDraft,
|
|
280
286
|
snapshotEnabled: cfg.snapshotEnabled,
|
|
281
287
|
archiveOriginal: cfg.archiveOriginal,
|
|
282
|
-
retentionDays: cfg.retentionDays
|
|
288
|
+
retentionDays: cfg.retentionDays,
|
|
289
|
+
// A4:界面语言偏好随 values 下发(设置卡片据此渲染下拉并即时切语言)
|
|
290
|
+
locale: cfg.locale
|
|
283
291
|
},
|
|
284
292
|
overridden,
|
|
285
293
|
envLocks,
|
|
@@ -314,6 +322,11 @@ function createRoutesManage(deps) {
|
|
|
314
322
|
if (!Array.isArray(patch.baseExcludes)) return { ok: false, code: E.RECALL_BAD_TYPE, message: "baseExcludes \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4" };
|
|
315
323
|
clean.baseExcludes = patch.baseExcludes.filter((p) => typeof p === "string" && p.trim());
|
|
316
324
|
}
|
|
325
|
+
if (patch.locale !== void 0) {
|
|
326
|
+
const v = String(patch.locale);
|
|
327
|
+
if (v !== "auto" && v !== "zh" && v !== "en") return { ok: false, code: E.RECALL_BAD_TYPE, message: "\u754C\u9762\u8BED\u8A00\u5FC5\u987B\u662F auto / zh / en \u4E4B\u4E00" };
|
|
328
|
+
clean.locale = v;
|
|
329
|
+
}
|
|
317
330
|
if (!Object.keys(clean).length) return { ok: false, code: E.RECALL_EMPTY_PATCH, message: "\u6CA1\u6709\u53EF\u5199\u5165\u7684\u914D\u7F6E\u5B57\u6BB5" };
|
|
318
331
|
let settings = null;
|
|
319
332
|
try {
|
|
@@ -378,14 +391,16 @@ function createRoutesManage(deps) {
|
|
|
378
391
|
return { ok: true, titles: out };
|
|
379
392
|
}
|
|
380
393
|
if (op === "messages") {
|
|
381
|
-
const
|
|
394
|
+
const reqsRaw = args && args.requests;
|
|
395
|
+
const reqs = Array.isArray(reqsRaw) ? reqsRaw.slice(0, 200) : [];
|
|
382
396
|
const bySession = /* @__PURE__ */ new Map();
|
|
383
397
|
for (const r of reqs) {
|
|
384
398
|
const sid = r && r.sessionId ? String(r.sessionId) : null;
|
|
385
399
|
const mid = r && r.messageId ? String(r.messageId) : null;
|
|
386
400
|
if (!sid || !mid) continue;
|
|
387
|
-
|
|
388
|
-
|
|
401
|
+
const midList = bySession.get(sid);
|
|
402
|
+
if (midList) midList.push(mid);
|
|
403
|
+
else bySession.set(sid, [mid]);
|
|
389
404
|
}
|
|
390
405
|
const texts = {};
|
|
391
406
|
await runLimited(Array.from(bySession.entries()).map(([sid, mids]) => async () => {
|
|
@@ -492,6 +507,9 @@ function createRoutesManage(deps) {
|
|
|
492
507
|
}
|
|
493
508
|
}
|
|
494
509
|
if (!store) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message: "\u8BE5\u5FEB\u7167\u4E0D\u5B58\u5728" };
|
|
510
|
+
if (!await snaps.guardStoreFormat(store)) {
|
|
511
|
+
return { ok: false, code: E.RECALL_FORMAT_BLOCKED, message: "\u78C1\u76D8\u683C\u5F0F\u4E0D\u53D7\u652F\u6301\uFF0C\u5DF2\u505C\u6B62\u5199\u5165\uFF1B\u8BE6\u89C1\u300C\u6700\u8FD1\u9519\u8BEF\u300D" };
|
|
512
|
+
}
|
|
495
513
|
const finalStore = store;
|
|
496
514
|
const finalRoot = snapRoot;
|
|
497
515
|
await enqueue(async () => {
|
package/lib/scripts.posix.js
CHANGED
|
@@ -363,6 +363,9 @@ function indexReadCmd(dir) {
|
|
|
363
363
|
function lineageReadCmd(dir) {
|
|
364
364
|
return "cat " + psq(dir + "/lineage.json") + " 2>/dev/null || true";
|
|
365
365
|
}
|
|
366
|
+
function fileReadCmd(file) {
|
|
367
|
+
return "cat " + psq(file) + " 2>/dev/null || true";
|
|
368
|
+
}
|
|
366
369
|
function legacyRmScript(path) {
|
|
367
370
|
return "rm -rf -- " + psq(path);
|
|
368
371
|
}
|
|
@@ -441,6 +444,7 @@ export {
|
|
|
441
444
|
ensureGitScript,
|
|
442
445
|
excludeDumpScript,
|
|
443
446
|
excludeReadCmd,
|
|
447
|
+
fileReadCmd,
|
|
444
448
|
fileWriteStdinCmd,
|
|
445
449
|
gcScript,
|
|
446
450
|
indexReadCmd,
|
package/lib/scripts.pwsh.js
CHANGED
|
@@ -420,6 +420,9 @@ function indexReadCmd(dir) {
|
|
|
420
420
|
function lineageReadCmd(dir) {
|
|
421
421
|
return "Get-Content -LiteralPath " + psq(dir + "\\lineage.json") + " -Raw -Encoding UTF8 -ErrorAction SilentlyContinue";
|
|
422
422
|
}
|
|
423
|
+
function fileReadCmd(file) {
|
|
424
|
+
return "if (Test-Path -LiteralPath " + psq(file) + ") { Get-Content -LiteralPath " + psq(file) + " -Raw -Encoding UTF8 -ErrorAction SilentlyContinue }";
|
|
425
|
+
}
|
|
423
426
|
function legacyRmScript(path) {
|
|
424
427
|
return "Remove-Item -Recurse -Force -LiteralPath " + psq(path) + " -ErrorAction SilentlyContinue";
|
|
425
428
|
}
|
|
@@ -525,6 +528,7 @@ export {
|
|
|
525
528
|
ensureGitScript,
|
|
526
529
|
excludeDumpScript,
|
|
527
530
|
excludeReadCmd,
|
|
531
|
+
fileReadCmd,
|
|
528
532
|
fileWriteStdinCmd,
|
|
529
533
|
gcScript,
|
|
530
534
|
homeDirScript,
|