dsh-recall-plugin 2.3.1 → 2.3.2
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 +30 -0
- package/README.en.md +4 -4
- package/README.md +8 -9
- package/lib/client.js +625 -476
- package/lib/config.js +53 -96
- package/lib/diagnostics.js +27 -52
- package/lib/dump-parse.js +79 -61
- package/lib/errors.js +41 -55
- package/lib/index.js +345 -446
- package/lib/maintenance.js +204 -275
- package/lib/routes-core.js +89 -140
- package/lib/routes-manage.js +571 -624
- package/lib/scripts.posix.js +452 -638
- package/lib/scripts.pwsh.js +537 -778
- package/lib/session-info.js +42 -57
- package/lib/snapshots.js +400 -588
- package/lib/store.js +364 -563
- package/package.json +7 -3
package/lib/routes-core.js
CHANGED
|
@@ -1,180 +1,129 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* 端点。依赖经 deps 注入(rt/snaps/state/cfg/enqueue/agentBusy/rescueRollback
|
|
6
|
-
* 等),无模块级可变状态(HMR 假设)。preview/execute 内部跑 git add -A,
|
|
7
|
-
* 经 enqueue 排进与快照/gc 同一条串行队列,避免 index.lock 竞态。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { ENV_HINTS } from './diagnostics.js'
|
|
11
|
-
import { parseTreeId } from './snapshots.js'
|
|
12
|
-
|
|
13
|
-
export function createRoutesCore(deps) {
|
|
14
|
-
// ctx 不解构(A4):本域所有服务访问都已由 rt/snaps 封装,直接摸 ctx 会
|
|
15
|
-
// 绕过工厂分层——留空位只会诱导未来代码破坏依赖注入约定
|
|
16
|
-
const { rt, snaps, state, cfg, supported, enqueue, agentBusy, rescueRollback, E } = deps
|
|
17
|
-
|
|
1
|
+
import { ENV_HINTS } from "./diagnostics.js";
|
|
2
|
+
import { parseTreeId } from "./snapshots.js";
|
|
3
|
+
function createRoutesCore(deps) {
|
|
4
|
+
const { rt, snaps, state, cfg, supported, enqueue, agentBusy, rescueRollback, E } = deps;
|
|
18
5
|
return {
|
|
19
|
-
|
|
6
|
+
"init": async (args) => {
|
|
20
7
|
if (!supported) {
|
|
21
|
-
return { ok: false, root: null, notice: { unsupported: true } }
|
|
8
|
+
return { ok: false, root: null, notice: { unsupported: true } };
|
|
22
9
|
}
|
|
23
|
-
const sessionId = args && args.sessionId ? String(args.sessionId) : null
|
|
24
|
-
const root = await rt.resolveRoot(sessionId)
|
|
25
|
-
let notice = null
|
|
10
|
+
const sessionId = args && args.sessionId ? String(args.sessionId) : null;
|
|
11
|
+
const root = await rt.resolveRoot(sessionId);
|
|
12
|
+
let notice = null;
|
|
26
13
|
if (root) {
|
|
27
|
-
let store = await rt.resolveStore(root)
|
|
28
|
-
store = await rt.tryUpgradeToHome(root)
|
|
29
|
-
await rt.ensureGit(root, store)
|
|
30
|
-
await snaps.loadIndex(root, sessionId)
|
|
31
|
-
await snaps.rebuildOrphans(root, sessionId)
|
|
32
|
-
rt.cleanupLegacy(root)
|
|
33
|
-
// 降级状态随 init 下发,Client 弹一次性提示(每次页面加载各弹一次):
|
|
34
|
-
// gitMissing=未检测到 git CLI(撤回按钮不出现);homeFallback=home
|
|
35
|
-
// 不可写,快照降级存进项目内 .dsh-recall-snapshots。
|
|
14
|
+
let store = await rt.resolveStore(root);
|
|
15
|
+
store = await rt.tryUpgradeToHome(root);
|
|
16
|
+
await rt.ensureGit(root, store);
|
|
17
|
+
await snaps.loadIndex(root, sessionId);
|
|
18
|
+
await snaps.rebuildOrphans(root, sessionId);
|
|
19
|
+
rt.cleanupLegacy(root);
|
|
36
20
|
notice = {
|
|
37
|
-
gitMissing: state.gitExe ===
|
|
21
|
+
gitMissing: state.gitExe === "",
|
|
38
22
|
homeFallback: store ? !store.home : false
|
|
39
|
-
}
|
|
23
|
+
};
|
|
40
24
|
}
|
|
41
|
-
|
|
42
|
-
// init 是每会话必经的预热通道
|
|
43
|
-
return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft, archiveOriginal: cfg.archiveOriginal } }
|
|
25
|
+
return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft, archiveOriginal: cfg.archiveOriginal } };
|
|
44
26
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
// 终止轮询并 toast,不再空等 20 次;has 时附带 skipped 让用户知道
|
|
51
|
-
// fail-open 跳过了哪些路径
|
|
52
|
-
const feedback = await snaps.feedbackFor(args && args.sessionId, id)
|
|
53
|
-
return { has: Boolean(snap), time: snap ? snap.time : null, id, ...feedback }
|
|
27
|
+
"snapshot-info": async (args) => {
|
|
28
|
+
const id = args && args.messageId ? String(args.messageId) : "";
|
|
29
|
+
const snap = state.snapshots.get(id);
|
|
30
|
+
const feedback = await snaps.feedbackFor(args ? args.sessionId : null, id);
|
|
31
|
+
return { has: Boolean(snap), time: snap ? snap.time : null, id, ...feedback };
|
|
54
32
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (result === null) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message: '该消息没有可用的项目快照' }
|
|
66
|
-
const snap2 = state.snapshots.get(id)
|
|
67
|
-
const cutSeq = await snaps.resolveCutSeq(sessionId, id)
|
|
68
|
-
// PF-1:treeId 是 preview 时 add -A 后的 index 树指纹,Client 确认时
|
|
69
|
-
// 透传回 execute——Host 与安全快照指纹比对即可判定「预览后文件是否
|
|
70
|
-
// 变化」,省掉 execute 侧整条重复 diff。旧版 Client 不认识该字段,
|
|
71
|
-
// 无值时 execute 退回 previewTotal 校验(向后兼容)。
|
|
72
|
-
return { ok: true, changes: result.changes, total: result.total, truncated: result.truncated, treeId: result.treeId || null, time: snap2 ? snap2.time : null, root: snap2 ? snap2.root : null, cutSeq }
|
|
33
|
+
"preview": async (args) => {
|
|
34
|
+
const id = args && args.messageId ? String(args.messageId) : "";
|
|
35
|
+
const sessionId = args && args.sessionId ? String(args.sessionId) : null;
|
|
36
|
+
const snap = state.snapshots.get(id);
|
|
37
|
+
if (agentBusy(sessionId, snap ? snap.root : null)) return { ok: false, code: E.RECALL_AGENT_BUSY, message: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE" };
|
|
38
|
+
const result = await enqueue(() => snaps.diffFor(id));
|
|
39
|
+
if (result === null) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167" };
|
|
40
|
+
const snap2 = state.snapshots.get(id);
|
|
41
|
+
const cutSeq = await snaps.resolveCutSeq(sessionId, id);
|
|
42
|
+
return { ok: true, changes: result.changes, total: result.total, truncated: result.truncated, treeId: result.treeId || null, time: snap2 ? snap2.time : null, root: snap2 ? snap2.root : null, cutSeq };
|
|
73
43
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const sessionId = args && args.sessionId ? String(args.sessionId) : null
|
|
44
|
+
"execute": async (args) => {
|
|
45
|
+
const id = args && args.messageId ? String(args.messageId) : "";
|
|
46
|
+
const sessionId = args && args.sessionId ? String(args.sessionId) : null;
|
|
78
47
|
const result = await enqueue(async () => {
|
|
79
|
-
const snap = state.snapshots.get(id)
|
|
80
|
-
if (!snap) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message:
|
|
81
|
-
const store = state.stores.get(snap.root)
|
|
82
|
-
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
// - 新版 Client 透传 previewTreeId(preview 时 add -A 后的 index 树
|
|
88
|
-
// 指纹):与下方安全快照输出的指纹比对,内容级一致判定,且免掉
|
|
89
|
-
// 一整条重复 diff 进程(一次撤回 4 条重进程 → 3 条)。
|
|
90
|
-
// - 旧版 Client 只带 previewTotal:退回条目总数校验(多付一次 diff,
|
|
91
|
-
// 同数不同文件的边缘情形由安全快照兜底)。
|
|
92
|
-
// - 都不带(直调 API):不校验,与 P0-3 同款可选语义。
|
|
93
|
-
const previewTreeId = args && typeof args.previewTreeId === 'string' && args.previewTreeId ? args.previewTreeId : null
|
|
94
|
-
if (!previewTreeId && args && typeof args.previewTotal === 'number') {
|
|
95
|
-
const fresh = await snaps.diffFor(id)
|
|
48
|
+
const snap = state.snapshots.get(id);
|
|
49
|
+
if (!snap) return { ok: false, code: E.RECALL_NO_SNAPSHOT, message: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167" };
|
|
50
|
+
const store = state.stores.get(snap.root);
|
|
51
|
+
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528" };
|
|
52
|
+
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" };
|
|
53
|
+
const previewTreeId = args && typeof args.previewTreeId === "string" && args.previewTreeId ? args.previewTreeId : null;
|
|
54
|
+
if (!previewTreeId && args && typeof args.previewTotal === "number") {
|
|
55
|
+
const fresh = await snaps.diffFor(id);
|
|
96
56
|
if (!fresh || fresh.total !== args.previewTotal) {
|
|
97
|
-
return { ok: false, code: E.RECALL_STALE, message:
|
|
57
|
+
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" };
|
|
98
58
|
}
|
|
99
59
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// (列表不展示),但孤儿重建/手动 git tag 仍能找到它,误回退后
|
|
104
|
-
// 用户可让插件从该 tag 恢复,堵住唯一的不可逆操作缺口。
|
|
105
|
-
// 失败时 safetyOk 置 false:后续回退若也失败将无救援点(H1),
|
|
106
|
-
// 行为退化为现状(fail-loud),不更差。
|
|
107
|
-
// PF-1:安全快照输出的树指纹就是「执行时刻的工作区状态」——与
|
|
108
|
-
// previewTreeId 不一致 → STALE(此时安全快照已打下,isSafetySnapshotId
|
|
109
|
-
// 让它不进索引,反而是额外的救援点)。安全快照失败时无指纹可比对,
|
|
110
|
-
// 跳过指纹校验继续回退(不阻断主流程的既有语义)。
|
|
111
|
-
const safetyId = 'pre-rollback-' + Date.now()
|
|
112
|
-
let safetyOk = false
|
|
113
|
-
let safetyTreeId = null
|
|
60
|
+
const safetyId = "pre-rollback-" + Date.now();
|
|
61
|
+
let safetyOk = false;
|
|
62
|
+
let safetyTreeId = null;
|
|
114
63
|
try {
|
|
115
|
-
const out = await rt.runShell(rt.scripts.snapshotScript(snap.root, store, state.gitExe, safetyId, cfg.baseExcludes), { timeoutMs:
|
|
116
|
-
safetyOk = true
|
|
117
|
-
safetyTreeId = parseTreeId(out)
|
|
64
|
+
const out = await rt.runShell(rt.scripts.snapshotScript(snap.root, store, state.gitExe || "", safetyId, cfg.baseExcludes), { timeoutMs: 6e5, stdoutMaxBytes: 65536 });
|
|
65
|
+
safetyOk = true;
|
|
66
|
+
safetyTreeId = parseTreeId(out);
|
|
118
67
|
} catch (error) {
|
|
119
|
-
|
|
120
|
-
rt.recordError('recall safety snapshot failed: ' + String(error))
|
|
68
|
+
rt.recordError("recall safety snapshot failed: " + String(error));
|
|
121
69
|
}
|
|
122
70
|
if (previewTreeId && safetyTreeId && safetyTreeId !== previewTreeId) {
|
|
123
|
-
return { ok: false, code: E.RECALL_STALE, message:
|
|
71
|
+
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" };
|
|
124
72
|
}
|
|
125
|
-
const rolled = await snaps.rollbackFor(id)
|
|
126
|
-
if (rolled.ok) return rolled
|
|
127
|
-
// 回退失败(rollbackFor 返回 partial,工作区可能半回退):用安全快照
|
|
128
|
-
// 救援(H1)。rescueRollback 是 snapshots.js 模块级纯逻辑,副作用经
|
|
129
|
-
// deps 注入,三分支(无救援点/救援成功/救援失败)单测直接钉。
|
|
73
|
+
const rolled = await snaps.rollbackFor(id);
|
|
74
|
+
if (rolled.ok) return rolled;
|
|
130
75
|
return rescueRollback(
|
|
131
|
-
{ runShell: rt.runShell, scripts: rt.scripts, gitExe: state.gitExe, recordError: rt.recordError },
|
|
76
|
+
{ runShell: rt.runShell, scripts: rt.scripts, gitExe: state.gitExe || "", recordError: rt.recordError },
|
|
132
77
|
{ root: snap.root, store, safetyId, safetyOk, rollbackError: rolled.error }
|
|
133
|
-
)
|
|
134
|
-
})
|
|
135
|
-
if (!result.ok) return result
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return { ok: true, count: result.count, cutSeq }
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
if (!result.ok) return result;
|
|
81
|
+
const cutSeq = await snaps.resolveCutSeq(sessionId, id);
|
|
82
|
+
return { ok: true, count: result.count, cutSeq };
|
|
139
83
|
},
|
|
140
|
-
|
|
141
84
|
// 设置页排障:最近错误(Host 侧 console.error 的页面可见副本)。
|
|
142
85
|
// M1-D3/D5:条目自带 count/kind(recordError 富集)——count 在服务端
|
|
143
86
|
// 拼成「(×N)」展示文本,设置页按 message 渲染即显示重复计数,零
|
|
144
87
|
// Client 改动;hint 是分类后的可行动提示(API 自描述,本次无客户端
|
|
145
88
|
// 消费,设置页未来展示零成本)。storeBase(M2-D3)暴露快照存储根,
|
|
146
89
|
// 供设置页未来展示「快照存在哪里」,失败为 null。
|
|
147
|
-
|
|
148
|
-
const storeBase = await rt.resolveHomeContainer()
|
|
149
|
-
if (args && args.op ===
|
|
150
|
-
state.errors.length = 0
|
|
151
|
-
return { ok: true, errors: [], storeBase }
|
|
90
|
+
"status": async (args) => {
|
|
91
|
+
const storeBase = await rt.resolveHomeContainer();
|
|
92
|
+
if (args && args.op === "clear") {
|
|
93
|
+
state.errors.length = 0;
|
|
94
|
+
return { ok: true, errors: [], storeBase };
|
|
152
95
|
}
|
|
153
96
|
const errors = state.errors.slice(-20).reverse().map((e) => ({
|
|
154
97
|
...e,
|
|
155
|
-
message: e.message + (e.count > 1 ?
|
|
156
|
-
hint: ENV_HINTS[e.kind]
|
|
157
|
-
}))
|
|
158
|
-
return { ok: true, errors, storeBase }
|
|
98
|
+
message: e.message + (e.count > 1 ? "\uFF08\xD7" + e.count + "\uFF09" : ""),
|
|
99
|
+
hint: e.kind ? ENV_HINTS[e.kind] : null
|
|
100
|
+
}));
|
|
101
|
+
return { ok: true, errors, storeBase };
|
|
159
102
|
},
|
|
160
|
-
|
|
161
103
|
// F1:client fork 成功后上报撤回链(childId ↔ parentId),Host 持久化到
|
|
162
104
|
// lineage.json 供快照管理树聚族展示「版本家族」。root 优先按 fork 源
|
|
163
105
|
// parentId 解析(fork 时它仍是 live 会话;归档只隐藏列表、对象在内存),
|
|
164
106
|
// 失败回退 childId。
|
|
165
|
-
|
|
166
|
-
const childId = args && args.childId ? String(args.childId) :
|
|
167
|
-
const parentId = args && args.parentId ? String(args.parentId) :
|
|
168
|
-
if (!childId || !parentId) return { ok: false, code: E.RECALL_BAD_TYPE, message:
|
|
169
|
-
const root =
|
|
170
|
-
if (!root) return { ok: false, code: E.RECALL_NO_ROOT, message:
|
|
171
|
-
let store = state.stores.get(root)
|
|
107
|
+
"lineage-record": async (args) => {
|
|
108
|
+
const childId = args && args.childId ? String(args.childId) : "";
|
|
109
|
+
const parentId = args && args.parentId ? String(args.parentId) : "";
|
|
110
|
+
if (!childId || !parentId) return { ok: false, code: E.RECALL_BAD_TYPE, message: "\u7F3A\u5C11\u4F1A\u8BDD ID" };
|
|
111
|
+
const root = await rt.resolveRoot(parentId) || await rt.resolveRoot(childId);
|
|
112
|
+
if (!root) return { ok: false, code: E.RECALL_NO_ROOT, message: "\u65E0\u6CD5\u89E3\u6790\u5DE5\u4F5C\u533A" };
|
|
113
|
+
let store = state.stores.get(root) || null;
|
|
172
114
|
if (!store) {
|
|
173
|
-
try {
|
|
115
|
+
try {
|
|
116
|
+
store = await rt.resolveStore(root);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
store = null;
|
|
119
|
+
}
|
|
174
120
|
}
|
|
175
|
-
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message:
|
|
176
|
-
await snaps.recordLineage(root, childId, parentId)
|
|
177
|
-
return { ok: true }
|
|
121
|
+
if (!store) return { ok: false, code: E.RECALL_NO_STORE, message: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528" };
|
|
122
|
+
await snaps.recordLineage(root, childId, parentId);
|
|
123
|
+
return { ok: true };
|
|
178
124
|
}
|
|
179
|
-
}
|
|
125
|
+
};
|
|
180
126
|
}
|
|
127
|
+
export {
|
|
128
|
+
createRoutesCore
|
|
129
|
+
};
|