dsh-vscode-mode 0.1.1
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 +95 -0
- package/assets/compare_idle.png +0 -0
- package/assets/compare_select.png +0 -0
- package/assets/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
- package/assets/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
- package/assets/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
- package/assets/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
- package/assets/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
- package/assets/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
- package/assets/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
- package/assets/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
- package/assets/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
- package/assets/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/bat-C397hTD6.js +1 -0
- package/assets/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
- package/assets/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
- package/assets/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
- package/assets/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
- package/assets/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
- package/assets/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
- package/assets/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
- package/assets/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
- package/assets/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
- package/assets/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
- package/assets/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
- package/assets/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
- package/assets/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
- package/assets/vendor/monaco/vs/ecl-30fUercY.js +1 -0
- package/assets/vendor/monaco/vs/editor/editor.main.css +1 -0
- package/assets/vendor/monaco/vs/editor/editor.main.js +1 -0
- package/assets/vendor/monaco/vs/editor/editor.worker.js +1 -0
- package/assets/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
- package/assets/vendor/monaco/vs/editor.js +1 -0
- package/assets/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
- package/assets/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
- package/assets/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
- package/assets/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
- package/assets/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
- package/assets/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
- package/assets/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
- package/assets/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
- package/assets/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
- package/assets/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
- package/assets/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
- package/assets/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
- package/assets/vendor/monaco/vs/index-049xrXrp.js +1 -0
- package/assets/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
- package/assets/vendor/monaco/vs/index.js +1 -0
- package/assets/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
- package/assets/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
- package/assets/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
- package/assets/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
- package/assets/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
- package/assets/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
- package/assets/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
- package/assets/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
- package/assets/vendor/monaco/vs/language/css/css.worker.js +66 -0
- package/assets/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/html/html.worker.js +443 -0
- package/assets/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/json/json.worker.js +17 -0
- package/assets/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
- package/assets/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
- package/assets/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
- package/assets/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
- package/assets/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
- package/assets/vendor/monaco/vs/loader.js +1368 -0
- package/assets/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
- package/assets/vendor/monaco/vs/lua-DtygF91M.js +1 -0
- package/assets/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
- package/assets/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
- package/assets/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
- package/assets/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
- package/assets/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
- package/assets/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
- package/assets/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
- package/assets/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
- package/assets/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
- package/assets/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
- package/assets/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
- package/assets/vendor/monaco/vs/nls.messages-loader.js +1 -0
- package/assets/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
- package/assets/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
- package/assets/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
- package/assets/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
- package/assets/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
- package/assets/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
- package/assets/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
- package/assets/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
- package/assets/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
- package/assets/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
- package/assets/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
- package/assets/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
- package/assets/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
- package/assets/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
- package/assets/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
- package/assets/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
- package/assets/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
- package/assets/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
- package/assets/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
- package/assets/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
- package/assets/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
- package/assets/vendor/monaco/vs/sb-3GYllVck.js +1 -0
- package/assets/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
- package/assets/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
- package/assets/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
- package/assets/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
- package/assets/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
- package/assets/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
- package/assets/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
- package/assets/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
- package/assets/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
- package/assets/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
- package/assets/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
- package/assets/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
- package/assets/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
- package/assets/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
- package/assets/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
- package/assets/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
- package/assets/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
- package/assets/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
- package/assets/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
- package/assets/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
- package/assets/vendor/monaco/vs/workers-BBttULjf.js +1 -0
- package/assets/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
- package/assets/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1914 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1298 -0
- package/lib/index.js.map +1 -0
- package/package.json +60 -0
- package/src/capture.ts +73 -0
- package/src/client/events.ts +36 -0
- package/src/client/index.ts +37 -0
- package/src/client/monaco/diffRender.ts +162 -0
- package/src/client/monaco/loader.ts +71 -0
- package/src/client/rpc.ts +28 -0
- package/src/client/state/records.ts +90 -0
- package/src/client/state/regions.ts +81 -0
- package/src/client/styles/css.d.ts +2 -0
- package/src/client/styles/editor.css +87 -0
- package/src/client/ui/DiffBadge.ts +51 -0
- package/src/client/ui/DiffBarEmpty.ts +37 -0
- package/src/client/ui/DiffBox.ts +79 -0
- package/src/client/ui/DiffLauncher.ts +124 -0
- package/src/client/ui/EditorView.ts +577 -0
- package/src/client/ui/QuickOpen.ts +91 -0
- package/src/client/ui/shared.ts +28 -0
- package/src/index.ts +43 -0
- package/src/model.ts +197 -0
- package/src/registry.ts +35 -0
- package/src/revert.ts +107 -0
- package/src/routes.ts +138 -0
- package/src/rpc.ts +295 -0
- package/src/shared/rpc.ts +67 -0
- package/src/shared/types.ts +121 -0
- package/src/store.ts +244 -0
- package/src/workspace.ts +74 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/shared/rpc.ts
|
|
5
|
+
/** webServer 精确路由。 */
|
|
6
|
+
const RPC_PATH = "/edrv/rpc";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/routes.ts
|
|
9
|
+
/**
|
|
10
|
+
* dsh-vscode-mode host — webServer 路由注册(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)。
|
|
11
|
+
* 迁移自原 src/index.ts 的路由部分,路径/行为一字不改。
|
|
12
|
+
* 作者 ddj 2026-08-20
|
|
13
|
+
*/
|
|
14
|
+
const IMG_ROUTES = [{
|
|
15
|
+
url: "/edrv/assets/compare-idle.png",
|
|
16
|
+
file: "compare_idle.png"
|
|
17
|
+
}, {
|
|
18
|
+
url: "/edrv/assets/compare-select.png",
|
|
19
|
+
file: "compare_select.png"
|
|
20
|
+
}];
|
|
21
|
+
/** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
|
|
22
|
+
const VENDOR_PREFIX = "/edrv/vendor";
|
|
23
|
+
const VENDOR_MIME = {
|
|
24
|
+
".js": "text/javascript; charset=utf-8",
|
|
25
|
+
".css": "text/css; charset=utf-8",
|
|
26
|
+
".json": "application/json",
|
|
27
|
+
".map": "application/json",
|
|
28
|
+
".ts": "text/plain; charset=utf-8",
|
|
29
|
+
".svg": "image/svg+xml",
|
|
30
|
+
".png": "image/png",
|
|
31
|
+
".gif": "image/gif",
|
|
32
|
+
".ico": "image/x-icon",
|
|
33
|
+
".woff": "font/woff",
|
|
34
|
+
".woff2": "font/woff2",
|
|
35
|
+
".ttf": "font/ttf",
|
|
36
|
+
".html": "text/html; charset=utf-8",
|
|
37
|
+
".md": "text/markdown"
|
|
38
|
+
};
|
|
39
|
+
/** 图标目录:默认插件包内 assets/(随包发布);config.imageDir 可覆盖(用于自定义换图)。 */
|
|
40
|
+
function imageDirOf(config) {
|
|
41
|
+
const cfg = config;
|
|
42
|
+
if (cfg && typeof cfg.imageDir === "string" && cfg.imageDir) return cfg.imageDir.replace(/\\/g, "/");
|
|
43
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
|
|
44
|
+
}
|
|
45
|
+
/** Monaco AMD 静态目录(随包发布)。 */
|
|
46
|
+
const vendorDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "vendor");
|
|
47
|
+
/**
|
|
48
|
+
* 注册全部 webServer 路由。
|
|
49
|
+
* @author ddj 2026年08月20号
|
|
50
|
+
* @param ctx DSH 上下文
|
|
51
|
+
* @param config 插件配置(可选 imageDir 等)
|
|
52
|
+
* @param handleRpc RPC 分发入口(method,args → RpcResult)
|
|
53
|
+
*/
|
|
54
|
+
function registerRoutes(ctx, config, handleRpc) {
|
|
55
|
+
const imgDir = imageDirOf(config);
|
|
56
|
+
const web = ctx.get("webServer");
|
|
57
|
+
if (!web) return;
|
|
58
|
+
ctx.effect(() => web.register({
|
|
59
|
+
kind: "exact",
|
|
60
|
+
path: RPC_PATH,
|
|
61
|
+
handler: async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
65
|
+
let input = {};
|
|
66
|
+
try {
|
|
67
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
68
|
+
} catch (error) {
|
|
69
|
+
input = {};
|
|
70
|
+
}
|
|
71
|
+
const result = await handleRpc(typeof input.method === "string" ? input.method : "", input.args ?? {});
|
|
72
|
+
res.statusCode = 200;
|
|
73
|
+
res.setHeader("content-type", "application/json");
|
|
74
|
+
res.end(JSON.stringify(result));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
res.statusCode = 500;
|
|
77
|
+
res.end(JSON.stringify({
|
|
78
|
+
ok: false,
|
|
79
|
+
error: String(error)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}), "edrv: /edrv/rpc route");
|
|
84
|
+
for (const r of IMG_ROUTES) ctx.effect(() => web.register({
|
|
85
|
+
kind: "exact",
|
|
86
|
+
path: r.url,
|
|
87
|
+
handler: async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const body = await readFile(imgDir + "/" + r.file);
|
|
90
|
+
res.statusCode = 200;
|
|
91
|
+
res.setHeader("content-type", "image/png");
|
|
92
|
+
res.setHeader("cache-control", "no-cache");
|
|
93
|
+
res.end(body);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.statusCode = 404;
|
|
96
|
+
res.end();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}), "edrv: static " + r.file);
|
|
100
|
+
ctx.effect(() => web.register({
|
|
101
|
+
kind: "prefix",
|
|
102
|
+
path: VENDOR_PREFIX,
|
|
103
|
+
handler: async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
106
|
+
res.statusCode = 405;
|
|
107
|
+
res.end();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const pathname = new URL(req.url ?? "/", "http://x").pathname;
|
|
111
|
+
const rel = decodeURIComponent(pathname.slice(12)).replace(/^\/+/, "");
|
|
112
|
+
if (!rel) {
|
|
113
|
+
res.statusCode = 404;
|
|
114
|
+
res.end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const target = resolve(normalize(join(vendorDir, rel)));
|
|
118
|
+
if (target !== vendorDir && !target.startsWith(vendorDir + sep)) {
|
|
119
|
+
res.statusCode = 403;
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const body = await readFile(target);
|
|
124
|
+
res.statusCode = 200;
|
|
125
|
+
res.setHeader("content-type", VENDOR_MIME[extname(target)] ?? "application/octet-stream");
|
|
126
|
+
res.setHeader("cache-control", "public, max-age=3600");
|
|
127
|
+
res.end(body);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
res.statusCode = 404;
|
|
130
|
+
res.end();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}), "edrv: /edrv/vendor prefix");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 归一化一条持久化记录(容忍旧版/损坏字段)。
|
|
137
|
+
* @author ddj 2026年08月20号
|
|
138
|
+
* @param raw 反序列化的原始记录
|
|
139
|
+
* @returns 合法记录或 null
|
|
140
|
+
*/
|
|
141
|
+
function normalizeRecord(raw) {
|
|
142
|
+
if (!raw || typeof raw !== "object") return null;
|
|
143
|
+
const r = raw;
|
|
144
|
+
if (typeof r.callId !== "string" || typeof r.path !== "string") return null;
|
|
145
|
+
const hunks = Array.isArray(r.hunks) ? r.hunks.filter((h) => h && typeof h.newText === "string") : [];
|
|
146
|
+
const toolName = r.toolName === "write" ? "write" : "edit";
|
|
147
|
+
return {
|
|
148
|
+
callId: r.callId,
|
|
149
|
+
toolName,
|
|
150
|
+
path: r.path,
|
|
151
|
+
before: typeof r.before === "string" ? r.before : null,
|
|
152
|
+
create: r.create === true,
|
|
153
|
+
callHunk: r.callHunk && typeof r.callHunk.oldText === "string" ? {
|
|
154
|
+
oldText: r.callHunk.oldText,
|
|
155
|
+
newText: r.callHunk.newText
|
|
156
|
+
} : null,
|
|
157
|
+
hunks: hunks.map((h) => ({
|
|
158
|
+
oldText: typeof h.oldText === "string" ? h.oldText : null,
|
|
159
|
+
newText: h.newText
|
|
160
|
+
})),
|
|
161
|
+
decisions: r.decisions && typeof r.decisions === "object" ? r.decisions : {
|
|
162
|
+
call: "pending",
|
|
163
|
+
perHunk: hunks.map(() => "pending")
|
|
164
|
+
},
|
|
165
|
+
note: typeof r.note === "string" ? r.note : null,
|
|
166
|
+
superseded: r.superseded === true,
|
|
167
|
+
archived: r.archived === true,
|
|
168
|
+
batch: Number.isInteger(r.batch) ? r.batch : 0,
|
|
169
|
+
at: typeof r.at === "string" ? r.at : (/* @__PURE__ */ new Date()).toISOString()
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 写决策到记录(scope=call 整体 / hunk 单个块)。
|
|
174
|
+
* @author ddj 2026年08月20号
|
|
175
|
+
* @param record 记录
|
|
176
|
+
* @param scope 作用域
|
|
177
|
+
* @param idx hunk 下标(仅 scope=hunk 时生效)
|
|
178
|
+
* @param value 决策值
|
|
179
|
+
* @returns 更新后的记录(原对象就地修改)
|
|
180
|
+
*/
|
|
181
|
+
function markDecision(record, scope, idx, value) {
|
|
182
|
+
const dec = record.decisions;
|
|
183
|
+
if (scope === "hunk" && Array.isArray(dec.perHunk) && Number.isInteger(idx)) {
|
|
184
|
+
const i = idx;
|
|
185
|
+
if (i >= 0 && i < dec.perHunk.length) dec.perHunk[i] = value;
|
|
186
|
+
} else {
|
|
187
|
+
dec.call = value;
|
|
188
|
+
if (Array.isArray(dec.perHunk)) dec.perHunk = dec.perHunk.map(() => value);
|
|
189
|
+
}
|
|
190
|
+
record.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
191
|
+
return record;
|
|
192
|
+
}
|
|
193
|
+
/** 记录是否已无任何待决策差异(superseded 或全部已决策)。 */
|
|
194
|
+
function recordResolved(record) {
|
|
195
|
+
if (record.superseded === true) return true;
|
|
196
|
+
const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
|
|
197
|
+
if (perHunk.length) return perHunk.every((v) => v === "accepted" || v === "rejected");
|
|
198
|
+
return record.decisions.call === "accepted" || record.decisions.call === "rejected";
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 记录决策摘要(采纳/拒绝/待处理计数,superseded 标记)。
|
|
202
|
+
* @author ddj 2026年08月20号
|
|
203
|
+
* @param record 记录
|
|
204
|
+
* @returns 摘要
|
|
205
|
+
*/
|
|
206
|
+
function recSummary(record) {
|
|
207
|
+
const perHunk = Array.isArray(record.decisions.perHunk) ? record.decisions.perHunk : [];
|
|
208
|
+
const n = Math.max(1, Array.isArray(record.hunks) ? record.hunks.length : 1);
|
|
209
|
+
let accepted = 0;
|
|
210
|
+
let rejected = 0;
|
|
211
|
+
let pending = 0;
|
|
212
|
+
if (perHunk.length) for (const v of perHunk) if (v === "accepted") accepted++;
|
|
213
|
+
else if (v === "rejected") rejected++;
|
|
214
|
+
else pending++;
|
|
215
|
+
else if (record.decisions.call === "accepted") accepted = n;
|
|
216
|
+
else if (record.decisions.call === "rejected") rejected = n;
|
|
217
|
+
else pending = n;
|
|
218
|
+
return {
|
|
219
|
+
accepted,
|
|
220
|
+
rejected,
|
|
221
|
+
pending,
|
|
222
|
+
superseded: record.superseded === true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 重建"本批次修改前"内容:把仍待处理(pending)的差异块按 新→旧 顺序从当前内容反解
|
|
227
|
+
* (newText → oldText)。已采纳/已拒绝的块跳过。反解失败的块标记为 stale。
|
|
228
|
+
* @author ddj 2026年08月20号
|
|
229
|
+
* @param records 该文件的活动记录
|
|
230
|
+
* @param content 当前磁盘内容
|
|
231
|
+
* @returns 重建内容与 stale 定位
|
|
232
|
+
*/
|
|
233
|
+
function reconstructOriginal(records, content) {
|
|
234
|
+
const pending = [];
|
|
235
|
+
for (const rec of records) {
|
|
236
|
+
const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : [];
|
|
237
|
+
for (let i = 0; i < rec.hunks.length; i++) if ((perHunk.length ? perHunk[i] : rec.decisions.call) === "pending") pending.push({
|
|
238
|
+
rec,
|
|
239
|
+
idx: i
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
pending.sort((a, b) => a.rec.at < b.rec.at ? 1 : a.rec.at > b.rec.at ? -1 : 0);
|
|
243
|
+
let out = content;
|
|
244
|
+
const stale = [];
|
|
245
|
+
for (const p of pending) {
|
|
246
|
+
const h = p.rec.hunks[p.idx];
|
|
247
|
+
const precise = p.rec.toolName === "edit" && p.rec.hunks.length <= 1 && p.rec.callHunk ? p.rec.callHunk : h;
|
|
248
|
+
const newText = precise.newText ?? "";
|
|
249
|
+
const at = out.indexOf(newText);
|
|
250
|
+
if (at < 0) {
|
|
251
|
+
stale.push({
|
|
252
|
+
callId: p.rec.callId,
|
|
253
|
+
idx: p.idx
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const oldText = precise.oldText === null ? "" : precise.oldText;
|
|
258
|
+
out = out.slice(0, at) + oldText + out.slice(at + newText.length);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
content: out,
|
|
262
|
+
stale
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** 文件当前最大批次号(无记录为 0)。 */
|
|
266
|
+
function fileMaxBatch(records, path) {
|
|
267
|
+
let m = 0;
|
|
268
|
+
for (const r of records.values()) if (r.path === path && Number.isInteger(r.batch) && r.batch > m) m = r.batch;
|
|
269
|
+
return m;
|
|
270
|
+
}
|
|
271
|
+
/** 记录桶裁剪:超出 MAX_RECORDS 时剔除最旧的。 */
|
|
272
|
+
function prune(map, max = 200) {
|
|
273
|
+
if (map.size <= max) return;
|
|
274
|
+
const sorted = [...map.values()].sort((a, b) => a.at < b.at ? -1 : 1);
|
|
275
|
+
for (let i = 0; i < sorted.length - max; i++) map.delete(sorted[i].callId);
|
|
276
|
+
}
|
|
277
|
+
/** 按批次分组记录。 */
|
|
278
|
+
function groupByBatch(recs) {
|
|
279
|
+
const map = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const r of recs) {
|
|
281
|
+
const k = Number.isInteger(r.batch) ? r.batch : null;
|
|
282
|
+
if (!map.has(k)) map.set(k, []);
|
|
283
|
+
map.get(k).push(r);
|
|
284
|
+
}
|
|
285
|
+
return map;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 归档条目构造(一条批次一条)。
|
|
289
|
+
* @author ddj 2026年08月20号
|
|
290
|
+
* @param recs 同批次记录(非空)
|
|
291
|
+
* @param cwd 工作区
|
|
292
|
+
* @param reason 归档原因
|
|
293
|
+
*/
|
|
294
|
+
function archiveEntryFor(recs, cwd, reason) {
|
|
295
|
+
const first = recs[0];
|
|
296
|
+
return {
|
|
297
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
298
|
+
cwd,
|
|
299
|
+
path: first.path,
|
|
300
|
+
batch: Number.isInteger(first.batch) ? first.batch : null,
|
|
301
|
+
reason,
|
|
302
|
+
records: recs.map((r) => ({
|
|
303
|
+
callId: r.callId,
|
|
304
|
+
toolName: r.toolName,
|
|
305
|
+
path: r.path,
|
|
306
|
+
create: r.create === true,
|
|
307
|
+
callHunk: r.callHunk,
|
|
308
|
+
hunks: r.hunks,
|
|
309
|
+
decisions: r.decisions,
|
|
310
|
+
note: r.note ?? null,
|
|
311
|
+
before: typeof r.before === "string" ? r.before : null,
|
|
312
|
+
superseded: r.superseded === true,
|
|
313
|
+
batch: Number.isInteger(r.batch) ? r.batch : null,
|
|
314
|
+
at: r.at,
|
|
315
|
+
summary: recSummary(r)
|
|
316
|
+
}))
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/store.ts
|
|
321
|
+
const SIDECAR = ".dsh-edit-review.json";
|
|
322
|
+
const SIDECAR_ARCHIVE = ".dsh-edit-review-archive.json";
|
|
323
|
+
/** 会话沙箱策略(policyOf):可能不存在,调用方需容忍 undefined。 */
|
|
324
|
+
function policyOf(ctx, session) {
|
|
325
|
+
const svc = ctx.get("sandboxPolicy");
|
|
326
|
+
if (!svc) return void 0;
|
|
327
|
+
return session ? svc.resolve({ session }) : svc.resolve();
|
|
328
|
+
}
|
|
329
|
+
/** 按会话 cwd 解析目标路径。 */
|
|
330
|
+
async function resolveTarget(ctx, session, path) {
|
|
331
|
+
const fs = ctx.get("fs");
|
|
332
|
+
const cwd = session?.header?.cwd;
|
|
333
|
+
return fs.resolve(path, cwd ? { cwd } : {});
|
|
334
|
+
}
|
|
335
|
+
/** 读取工作区主 sidecar 文本(缺失/失败 → null)。 */
|
|
336
|
+
async function readSidecarText(ctx, cwd) {
|
|
337
|
+
const fs = ctx.get("fs");
|
|
338
|
+
if (!fs || !cwd) return null;
|
|
339
|
+
try {
|
|
340
|
+
return await fs.readText(await fs.resolve(SIDECAR, { cwd }));
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 解析主 sidecar:v2 直接返回;v1 迁移为 v2 结构(按会话 cwd 分桶)。
|
|
347
|
+
* @author ddj 2026年08月20号
|
|
348
|
+
* @param text sidecar 文本
|
|
349
|
+
* @returns v2 数据或 null
|
|
350
|
+
*/
|
|
351
|
+
function parseSidecar(text) {
|
|
352
|
+
if (!text) return null;
|
|
353
|
+
try {
|
|
354
|
+
const data = JSON.parse(text);
|
|
355
|
+
if (data && typeof data === "object" && data.version === 2 && data.workspaces && typeof data.workspaces === "object") return data;
|
|
356
|
+
if (data && data.version === 1 && data.sessions && typeof data.sessions === "object") {
|
|
357
|
+
const workspaces = {};
|
|
358
|
+
for (const bucket of Object.values(data.sessions)) {
|
|
359
|
+
const b = bucket;
|
|
360
|
+
if (typeof b.cwd === "string" && b.records) workspaces[b.cwd] = {
|
|
361
|
+
at: typeof b.at === "number" ? b.at : Date.now(),
|
|
362
|
+
records: b.records
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
version: 2,
|
|
367
|
+
updatedAt: typeof data.updatedAt === "string" ? data.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
368
|
+
workspaces
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/** 加载某工作区的记录桶(内存 Map;缺失 → 空)。 */
|
|
377
|
+
async function loadBucket(ctx, cwd) {
|
|
378
|
+
const data = parseSidecar(await readSidecarText(ctx, cwd));
|
|
379
|
+
const map = /* @__PURE__ */ new Map();
|
|
380
|
+
if (data && data.workspaces[cwd] && typeof data.workspaces[cwd].records === "object") for (const rec of Object.values(data.workspaces[cwd].records)) {
|
|
381
|
+
const n = normalizeRecord(rec);
|
|
382
|
+
if (n) map.set(n.callId, n);
|
|
383
|
+
}
|
|
384
|
+
return map;
|
|
385
|
+
}
|
|
386
|
+
/** 保存某工作区的记录桶(写前合并,不覆盖其他工作区)。 */
|
|
387
|
+
async function saveBucket(ctx, cwd, recsMap, session) {
|
|
388
|
+
const fs = ctx.get("fs");
|
|
389
|
+
if (!fs || !cwd) return;
|
|
390
|
+
try {
|
|
391
|
+
const target = await fs.resolve(SIDECAR, { cwd });
|
|
392
|
+
const existing = parseSidecar(await readSidecarText(ctx, cwd)) ?? {
|
|
393
|
+
version: 2,
|
|
394
|
+
updatedAt: "",
|
|
395
|
+
workspaces: {}
|
|
396
|
+
};
|
|
397
|
+
existing.workspaces[cwd] = {
|
|
398
|
+
at: Date.now(),
|
|
399
|
+
records: Object.fromEntries(recsMap)
|
|
400
|
+
};
|
|
401
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
402
|
+
await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session));
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.error("edrv saveBucket failed", error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 读取记录目标文件内容(按 path 缓存一次;缺失/过大/失败 → null/'' 占位)。
|
|
409
|
+
* @author ddj 2026年08月20号
|
|
410
|
+
* @param cache path → 内容(null=缺失/读取失败,''=过大跳过)
|
|
411
|
+
*/
|
|
412
|
+
async function readForCache(ctx, session, cache, path) {
|
|
413
|
+
if (cache.has(path)) return cache.get(path) ?? null;
|
|
414
|
+
const fs = ctx.get("fs");
|
|
415
|
+
if (!fs) {
|
|
416
|
+
cache.set(path, null);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const target = await resolveTarget(ctx, session, path);
|
|
421
|
+
const info = await fs.stat(target);
|
|
422
|
+
if (!info || info.type !== "file") {
|
|
423
|
+
cache.set(path, null);
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
if ((info.size ?? 0) > 8388608) {
|
|
427
|
+
cache.set(path, "");
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
const content = await fs.readText(target);
|
|
431
|
+
cache.set(path, content);
|
|
432
|
+
return content;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
cache.set(path, null);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* 判断记录是否已无任何可操作差异(stale):待决策 hunk 的新文本在磁盘均找不到,
|
|
440
|
+
* 或新建文件已不存在,或全为空差异。满足则应由 host 自动归档。
|
|
441
|
+
* @author ddj 2026年08月20号
|
|
442
|
+
*/
|
|
443
|
+
async function recordIsStale(ctx, session, cache, rec) {
|
|
444
|
+
if (rec.superseded === true) return true;
|
|
445
|
+
if (!Array.isArray(rec.hunks) || !rec.hunks.length) return true;
|
|
446
|
+
const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : [];
|
|
447
|
+
let hasLocatablePending = false;
|
|
448
|
+
for (let i = 0; i < rec.hunks.length; i++) {
|
|
449
|
+
const st = perHunk.length ? perHunk[i] : rec.decisions.call;
|
|
450
|
+
if (st === "accepted" || st === "rejected") continue;
|
|
451
|
+
if (rec.create === true) {
|
|
452
|
+
if (await readForCache(ctx, session, cache, rec.path) !== null) {
|
|
453
|
+
hasLocatablePending = true;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const precise = rec.toolName === "edit" && rec.callHunk ? rec.callHunk : rec.hunks[i];
|
|
459
|
+
const oldText = precise?.oldText ?? null;
|
|
460
|
+
const newText = precise?.newText ?? null;
|
|
461
|
+
if (oldText !== null && oldText === newText) continue;
|
|
462
|
+
if (newText === null || newText === void 0) continue;
|
|
463
|
+
const content = await readForCache(ctx, session, cache, rec.path);
|
|
464
|
+
if (content !== null && content.indexOf(newText) >= 0) {
|
|
465
|
+
hasLocatablePending = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return !hasLocatablePending;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* edrv.list 全量轮询时自动清理 stale 记录:标记 superseded 并归档,
|
|
473
|
+
* 防止"幽灵差异"长期留在审查列表且无法操作。
|
|
474
|
+
* @author ddj 2026年08月20号
|
|
475
|
+
* @returns 自动归档条数
|
|
476
|
+
*/
|
|
477
|
+
async function autoArchiveStale(ctx, session, cwd, bucket) {
|
|
478
|
+
if (!ctx.get("fs")) return 0;
|
|
479
|
+
const cache = /* @__PURE__ */ new Map();
|
|
480
|
+
const stale = [];
|
|
481
|
+
for (const rec of bucket.values()) {
|
|
482
|
+
if (rec.archived || rec.superseded === true) continue;
|
|
483
|
+
if (await recordIsStale(ctx, session, cache, rec)) stale.push(rec);
|
|
484
|
+
}
|
|
485
|
+
if (!stale.length) return 0;
|
|
486
|
+
for (const r of stale) {
|
|
487
|
+
r.superseded = true;
|
|
488
|
+
r.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
|
+
}
|
|
490
|
+
await archiveRecords(ctx, session, cwd, bucket, stale, "差异无法定位(已被后续修改覆盖),自动归档");
|
|
491
|
+
return stale.length;
|
|
492
|
+
}
|
|
493
|
+
/** 读取归档 sidecar 文本(缺失/失败 → null)。 */
|
|
494
|
+
async function readArchiveText(ctx, cwd) {
|
|
495
|
+
const fs = ctx.get("fs");
|
|
496
|
+
if (!fs || !cwd) return null;
|
|
497
|
+
try {
|
|
498
|
+
return await fs.readText(await fs.resolve(SIDECAR_ARCHIVE, { cwd }));
|
|
499
|
+
} catch (error) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/** 解析归档 sidecar(损坏 → 空数组)。 */
|
|
504
|
+
function parseArchive(text) {
|
|
505
|
+
if (!text) return [];
|
|
506
|
+
try {
|
|
507
|
+
const data = JSON.parse(text);
|
|
508
|
+
if (data && typeof data === "object" && Array.isArray(data.batches)) return data.batches;
|
|
509
|
+
} catch (error) {}
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
/** 追加归档条目(按 cwd+path+batch 合并,同 callId 去重)。 */
|
|
513
|
+
async function appendArchiveEntries(ctx, cwd, entries, session) {
|
|
514
|
+
const fs = ctx.get("fs");
|
|
515
|
+
if (!fs || !cwd) return;
|
|
516
|
+
try {
|
|
517
|
+
const target = await fs.resolve(SIDECAR_ARCHIVE, { cwd });
|
|
518
|
+
const existing = parseArchive(await readArchiveText(ctx, cwd));
|
|
519
|
+
for (const e of entries) {
|
|
520
|
+
const idx = existing.findIndex((x) => x.cwd === e.cwd && x.path === e.path && x.batch === e.batch);
|
|
521
|
+
if (idx >= 0) {
|
|
522
|
+
const exist = existing[idx];
|
|
523
|
+
const seen = new Set((exist.records || []).map((r) => r.callId));
|
|
524
|
+
for (const rec of e.records) if (!seen.has(String(rec.callId))) {
|
|
525
|
+
exist.records.push(rec);
|
|
526
|
+
seen.add(String(rec.callId));
|
|
527
|
+
}
|
|
528
|
+
exist.lastAt = e.at;
|
|
529
|
+
exist.reason = e.reason;
|
|
530
|
+
} else existing.push(e);
|
|
531
|
+
}
|
|
532
|
+
const data = {
|
|
533
|
+
version: 1,
|
|
534
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
535
|
+
batches: existing
|
|
536
|
+
};
|
|
537
|
+
await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session));
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error("edrv appendArchiveEntries failed", error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** 归档记录:标记 archived、按批次写入归档、落盘工作区桶。 */
|
|
543
|
+
async function archiveRecords(ctx, session, cwd, bucket, recs, reason) {
|
|
544
|
+
if (!recs.length) return;
|
|
545
|
+
const fresh = recs.filter((r) => !r.archived);
|
|
546
|
+
for (const r of recs) r.archived = true;
|
|
547
|
+
const entries = [];
|
|
548
|
+
for (const list of groupByBatch(recs).values()) entries.push(archiveEntryFor(list, cwd, reason));
|
|
549
|
+
await appendArchiveEntries(ctx, cwd, entries, session);
|
|
550
|
+
if (fresh.length) await saveBucket(ctx, cwd, bucket, session);
|
|
551
|
+
}
|
|
552
|
+
//#endregion
|
|
553
|
+
//#region src/registry.ts
|
|
554
|
+
/** 按 sessionId 取会话;缺省时仅当唯一活跃会话才返回。 */
|
|
555
|
+
function sessionOf(ctx, sessionId) {
|
|
556
|
+
const sessions = ctx.get("sessions");
|
|
557
|
+
if (!sessions) return void 0;
|
|
558
|
+
if (sessionId) return sessions.get(sessionId);
|
|
559
|
+
const live = sessions.list().filter((s) => s.id);
|
|
560
|
+
return live.length === 1 ? live[0] : void 0;
|
|
561
|
+
}
|
|
562
|
+
/** 会话工作区 cwd。 */
|
|
563
|
+
function cwdOf(session) {
|
|
564
|
+
return session?.header?.cwd ?? null;
|
|
565
|
+
}
|
|
566
|
+
/** 取(或加载)某工作区记录桶。 */
|
|
567
|
+
function bucketOf(registry, ctx, cwd) {
|
|
568
|
+
const existing = registry.get(cwd);
|
|
569
|
+
if (existing) return Promise.resolve(existing);
|
|
570
|
+
return loadBucket(ctx, cwd).then((map) => {
|
|
571
|
+
registry.set(cwd, map);
|
|
572
|
+
return map;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/capture.ts
|
|
577
|
+
/**
|
|
578
|
+
* 捕获一次工具执行结果(tools/result):edit/write 成功时构造记录、
|
|
579
|
+
* 递增文件批次、融合归档被后续修改取代的旧差异、落盘。
|
|
580
|
+
* @author ddj 2026年08月20号
|
|
581
|
+
* @param ctx DSH 上下文
|
|
582
|
+
* @param registry 工作区记录桶注册表
|
|
583
|
+
* @param exec 工具执行描述(name/arguments/callId/agent.session)
|
|
584
|
+
* @param result 执行结果(value/meta/isError)
|
|
585
|
+
*/
|
|
586
|
+
async function captureToolResult(ctx, registry, exec, result) {
|
|
587
|
+
const session = exec?.agent?.session;
|
|
588
|
+
if (!session || exec?.name !== "edit" && exec?.name !== "write") return;
|
|
589
|
+
if (result?.isError || !result?.value) return;
|
|
590
|
+
try {
|
|
591
|
+
const cwd = cwdOf(session);
|
|
592
|
+
if (!cwd) return;
|
|
593
|
+
const value = result.value;
|
|
594
|
+
const args = exec.arguments || {};
|
|
595
|
+
const path = typeof value.path === "string" ? value.path : args.file_path;
|
|
596
|
+
if (!path) return;
|
|
597
|
+
const metaDiffs = Array.isArray(result.meta?.diffs) ? result.meta.diffs : [];
|
|
598
|
+
const before = typeof value.before === "string" ? value.before : null;
|
|
599
|
+
const create = exec.name === "write" && value.before === null;
|
|
600
|
+
const callHunk = exec.name === "edit" && typeof args.old_string === "string" ? {
|
|
601
|
+
oldText: args.old_string,
|
|
602
|
+
newText: args.new_string
|
|
603
|
+
} : null;
|
|
604
|
+
const synthHunk = create ? {
|
|
605
|
+
oldText: null,
|
|
606
|
+
newText: typeof args.content === "string" ? args.content : typeof value.after === "string" ? value.after : ""
|
|
607
|
+
} : null;
|
|
608
|
+
const hunks = metaDiffs.length ? metaDiffs : synthHunk ? [synthHunk] : callHunk ? [callHunk] : [];
|
|
609
|
+
if (!hunks.length) return;
|
|
610
|
+
const record = {
|
|
611
|
+
callId: exec.callId,
|
|
612
|
+
toolName: exec.name,
|
|
613
|
+
path,
|
|
614
|
+
before,
|
|
615
|
+
create,
|
|
616
|
+
callHunk,
|
|
617
|
+
hunks,
|
|
618
|
+
decisions: {
|
|
619
|
+
call: "pending",
|
|
620
|
+
perHunk: hunks.map(() => "pending")
|
|
621
|
+
},
|
|
622
|
+
note: create ? "新建文件:全部拒绝将删除该文件" : before === null ? "未捕获修改前内容(大文件)" : null,
|
|
623
|
+
superseded: false,
|
|
624
|
+
archived: false,
|
|
625
|
+
batch: 0,
|
|
626
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
627
|
+
};
|
|
628
|
+
let bucket = registry.get(cwd);
|
|
629
|
+
if (!bucket) {
|
|
630
|
+
bucket = await loadBucket(ctx, cwd);
|
|
631
|
+
registry.set(cwd, bucket);
|
|
632
|
+
}
|
|
633
|
+
record.batch = fileMaxBatch(bucket, path) + 1;
|
|
634
|
+
bucket.set(exec.callId, record);
|
|
635
|
+
const prior = [];
|
|
636
|
+
for (const r of bucket.values()) if (r.path === path && r.callId !== exec.callId && !r.archived && (r.batch ?? 0) < record.batch) prior.push(r);
|
|
637
|
+
if (prior.length) await archiveRecords(ctx, session, cwd, bucket, prior, prior.every(recordResolved) ? "已处理且被后续修改取代" : "被后续修改融合(内容已含其效果)");
|
|
638
|
+
prune(bucket);
|
|
639
|
+
await saveBucket(ctx, cwd, bucket, session);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
console.error("edrv capture failed", error);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/workspace.ts
|
|
646
|
+
/** 文件清单缓存 TTL 与扫描上限(避免大工作区反复全量扫描)。 */
|
|
647
|
+
const FILE_INDEX_TTL = 6e4;
|
|
648
|
+
const SCAN_CAP = 6e3;
|
|
649
|
+
/** cwd → { at, files }(随会话销毁清理)。 */
|
|
650
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
651
|
+
/**
|
|
652
|
+
* 扫描工作区文件清单(快速打开/搜索用):subprocess(Windows powershell / POSIX find),
|
|
653
|
+
* 排除常见重目录,容量上限 SCAN_CAP;结果缓存(TTL FILE_INDEX_TTL)。失败返回 null。
|
|
654
|
+
* @author ddj 2026年08月20号
|
|
655
|
+
* @param ctx DSH 上下文
|
|
656
|
+
* @param session 会话(取工作区根)
|
|
657
|
+
* @param cwd 工作区目录
|
|
658
|
+
* @returns 绝对路径清单;不可用(无 subprocess/fs)或扫描失败返回 null
|
|
659
|
+
*/
|
|
660
|
+
async function listWorkspaceFiles(ctx, session, cwd) {
|
|
661
|
+
const cached = fileIndex.get(cwd);
|
|
662
|
+
if (cached && Date.now() - cached.at < FILE_INDEX_TTL) return cached.files;
|
|
663
|
+
const sub = ctx.get("subprocess");
|
|
664
|
+
const fs = ctx.get("fs");
|
|
665
|
+
if (!sub || !fs) return null;
|
|
666
|
+
const policy = policyOf(ctx, session);
|
|
667
|
+
let root;
|
|
668
|
+
try {
|
|
669
|
+
const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", {});
|
|
670
|
+
root = fs.processPath(rootTarget);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
root = cwd;
|
|
673
|
+
}
|
|
674
|
+
const lines = [];
|
|
675
|
+
const run = async (argv) => {
|
|
676
|
+
const handle = sub.spawn({
|
|
677
|
+
argv,
|
|
678
|
+
stdio: {
|
|
679
|
+
stdout: { maxBytes: 8 << 20 },
|
|
680
|
+
stderr: { maxBytes: 65536 },
|
|
681
|
+
stdin: "ignore"
|
|
682
|
+
},
|
|
683
|
+
graceMs: 2e4
|
|
684
|
+
});
|
|
685
|
+
const outcome = await handle.done;
|
|
686
|
+
const code = outcome?.exitCode ?? outcome?.code;
|
|
687
|
+
if (code !== 0) throw new Error("scan exit " + code);
|
|
688
|
+
const text = handle.collected?.stdout?.readFrom(0).text ?? "";
|
|
689
|
+
for (const ln of text.split(/\r?\n/)) {
|
|
690
|
+
const s = ln.trim();
|
|
691
|
+
if (s) lines.push(s);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
try {
|
|
695
|
+
if (process.platform === "win32") await run([
|
|
696
|
+
"powershell",
|
|
697
|
+
"-NoProfile",
|
|
698
|
+
"-NonInteractive",
|
|
699
|
+
"-Command",
|
|
700
|
+
"Get-ChildItem -LiteralPath \"" + String(root).replace(/"/g, "\"\"") + "\" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '(node_modules|\\.git|\\.tmp|\\.cache|dist|build|vendor|coverage|__pycache__)' } | Select-Object -First 6000 | ForEach-Object { $_.FullName }"
|
|
701
|
+
]);
|
|
702
|
+
else await run([
|
|
703
|
+
"/bin/sh",
|
|
704
|
+
"-c",
|
|
705
|
+
"find " + JSON.stringify(root) + " -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.tmp/*' -not -path '*/.cache/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/vendor/*' -not -path '*/coverage/*' 2>/dev/null | head -n 6000"
|
|
706
|
+
]);
|
|
707
|
+
} catch (error) {
|
|
708
|
+
console.error("edrv scan failed", error);
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const files = lines.slice(0, SCAN_CAP);
|
|
712
|
+
if (files.length) fileIndex.set(cwd, {
|
|
713
|
+
at: Date.now(),
|
|
714
|
+
files
|
|
715
|
+
});
|
|
716
|
+
return files;
|
|
717
|
+
}
|
|
718
|
+
/** 会话销毁时清理文件索引缓存。 */
|
|
719
|
+
function dropFileIndex(cwd) {
|
|
720
|
+
fileIndex.delete(cwd);
|
|
721
|
+
}
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/revert.ts
|
|
724
|
+
/**
|
|
725
|
+
* 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
|
|
726
|
+
* @author ddj 2026年08月20号
|
|
727
|
+
* @returns 成功或失败原因
|
|
728
|
+
*/
|
|
729
|
+
async function deleteCreated(ctx, session, record) {
|
|
730
|
+
const sub = ctx.get("subprocess");
|
|
731
|
+
const fs = ctx.get("fs");
|
|
732
|
+
if (!sub || !fs) return {
|
|
733
|
+
ok: false,
|
|
734
|
+
error: "回滚不可用:缺少 subprocess/fs"
|
|
735
|
+
};
|
|
736
|
+
const policy = policyOf(ctx, session);
|
|
737
|
+
const rootTarget = await fs.resolve(policy?.workspaceRoot ?? ".", {});
|
|
738
|
+
const target = await resolveTarget(ctx, session, record.path);
|
|
739
|
+
if (!fs.contains(rootTarget, target)) return {
|
|
740
|
+
ok: false,
|
|
741
|
+
error: "拒绝删除:目标不在会话工作区内"
|
|
742
|
+
};
|
|
743
|
+
const p = fs.processPath(target);
|
|
744
|
+
const attempt = async (argv) => {
|
|
745
|
+
const handle = sub.spawn({
|
|
746
|
+
argv,
|
|
747
|
+
stdio: {
|
|
748
|
+
stdout: { maxBytes: 65536 },
|
|
749
|
+
stderr: { maxBytes: 65536 },
|
|
750
|
+
stdin: "ignore"
|
|
751
|
+
},
|
|
752
|
+
graceMs: 1e4
|
|
753
|
+
});
|
|
754
|
+
const outcome = await handle.done;
|
|
755
|
+
const code = outcome?.exitCode ?? outcome?.code;
|
|
756
|
+
if (code !== 0) {
|
|
757
|
+
const err = handle.collected?.stderr?.readFrom(0).text ?? "";
|
|
758
|
+
throw new Error("exit " + code + " " + err);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
try {
|
|
762
|
+
await attempt([
|
|
763
|
+
"powershell",
|
|
764
|
+
"-NoProfile",
|
|
765
|
+
"-NonInteractive",
|
|
766
|
+
"-Command",
|
|
767
|
+
"Remove-Item -LiteralPath \"" + p + "\" -Force"
|
|
768
|
+
]);
|
|
769
|
+
return { ok: true };
|
|
770
|
+
} catch (error) {
|
|
771
|
+
try {
|
|
772
|
+
await attempt([
|
|
773
|
+
"/bin/rm",
|
|
774
|
+
"-f",
|
|
775
|
+
"--",
|
|
776
|
+
p
|
|
777
|
+
]);
|
|
778
|
+
return { ok: true };
|
|
779
|
+
} catch (error2) {
|
|
780
|
+
return {
|
|
781
|
+
ok: false,
|
|
782
|
+
error: "删除失败(文件仍存在):" + String(error) + " / " + String(error2)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/** 整调用回滚:write/编辑用 before 整文件恢复;新建文件转 deleteCreated。 */
|
|
788
|
+
async function revertCall(ctx, session, record) {
|
|
789
|
+
const fs = ctx.get("fs");
|
|
790
|
+
if (!fs) return {
|
|
791
|
+
ok: false,
|
|
792
|
+
error: "回滚不可用:缺少 fs"
|
|
793
|
+
};
|
|
794
|
+
if (record.before === null) {
|
|
795
|
+
if (!record.create) return {
|
|
796
|
+
ok: false,
|
|
797
|
+
error: "无法回滚:缺少修改前内容(大文件或旧记录)"
|
|
798
|
+
};
|
|
799
|
+
return deleteCreated(ctx, session, record);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const target = await resolveTarget(ctx, session, record.path);
|
|
803
|
+
await fs.writeText(target, record.before, void 0, void 0, policyOf(ctx, session));
|
|
804
|
+
return { ok: true };
|
|
805
|
+
} catch (error) {
|
|
806
|
+
return {
|
|
807
|
+
ok: false,
|
|
808
|
+
error: "回滚失败:" + String(error)
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** 单 hunk 回滚:用 callHunk(edit 单 hunk 精确串)经 fs.editText 反替换。 */
|
|
813
|
+
async function revertHunk(ctx, session, record, idx) {
|
|
814
|
+
const fs = ctx.get("fs");
|
|
815
|
+
if (!fs) return {
|
|
816
|
+
ok: false,
|
|
817
|
+
error: "回滚不可用:缺少 fs"
|
|
818
|
+
};
|
|
819
|
+
const hunk = record.hunks[idx];
|
|
820
|
+
const precise = record.toolName === "edit" && record.hunks.length <= 1 && record.callHunk ? record.callHunk : hunk;
|
|
821
|
+
if (!precise) return {
|
|
822
|
+
ok: false,
|
|
823
|
+
error: "找不到该差异块"
|
|
824
|
+
};
|
|
825
|
+
try {
|
|
826
|
+
const target = await resolveTarget(ctx, session, record.path);
|
|
827
|
+
await fs.editText(target, {
|
|
828
|
+
oldString: precise.newText,
|
|
829
|
+
newString: precise.oldText === null ? "" : precise.oldText
|
|
830
|
+
}, void 0, void 0, policyOf(ctx, session));
|
|
831
|
+
return { ok: true };
|
|
832
|
+
} catch (error) {
|
|
833
|
+
return {
|
|
834
|
+
ok: false,
|
|
835
|
+
error: "回滚失败:该区域可能已被后续修改影响(" + String(error) + ")"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* 恢复文件到记录对应状态(批次回滚用):返回错误文案或 null(成功)。
|
|
841
|
+
* @author ddj 2026年08月20号
|
|
842
|
+
*/
|
|
843
|
+
async function restoreFile(ctx, session, rec) {
|
|
844
|
+
const fs = ctx.get("fs");
|
|
845
|
+
if (!fs) return "缺少 fs";
|
|
846
|
+
if (rec.before === null || rec.before === void 0) {
|
|
847
|
+
if (rec.create === true) {
|
|
848
|
+
const out = await deleteCreated(ctx, session, rec);
|
|
849
|
+
return out.ok ? null : out.error;
|
|
850
|
+
}
|
|
851
|
+
return "缺少修改前内容(大文件或旧记录),无法回滚";
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
const target = await resolveTarget(ctx, session, rec.path);
|
|
855
|
+
await fs.writeText(target, rec.before, void 0, void 0, policyOf(ctx, session));
|
|
856
|
+
return null;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
return "回滚失败:" + String(error);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/rpc.ts
|
|
863
|
+
/** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
|
|
864
|
+
const debugWriteQueues = /* @__PURE__ */ new Map();
|
|
865
|
+
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
866
|
+
function recView(record) {
|
|
867
|
+
return {
|
|
868
|
+
callId: record.callId,
|
|
869
|
+
toolName: record.toolName,
|
|
870
|
+
path: record.path,
|
|
871
|
+
beforeLen: typeof record.before === "string" ? record.before.length : 0,
|
|
872
|
+
create: record.create === true,
|
|
873
|
+
callHunk: record.callHunk,
|
|
874
|
+
hunks: record.hunks,
|
|
875
|
+
decisions: record.decisions,
|
|
876
|
+
note: record.note ?? null,
|
|
877
|
+
superseded: record.superseded === true,
|
|
878
|
+
at: record.at
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
|
|
882
|
+
async function requireSession(ctx, sessionId) {
|
|
883
|
+
const session = sessionOf(ctx, sessionId);
|
|
884
|
+
if (!session) return { err: "会话不存在" };
|
|
885
|
+
const cwd = cwdOf(session);
|
|
886
|
+
if (!cwd) return { err: "会话无工作区" };
|
|
887
|
+
return {
|
|
888
|
+
session,
|
|
889
|
+
cwd
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
893
|
+
function buildHandlers(ctx, registry) {
|
|
894
|
+
return {
|
|
895
|
+
"edrv.list": async (args) => {
|
|
896
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
897
|
+
if ("err" in sc) return {
|
|
898
|
+
ok: false,
|
|
899
|
+
error: sc.err
|
|
900
|
+
};
|
|
901
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
902
|
+
const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null;
|
|
903
|
+
if (!want) await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
904
|
+
const out = [];
|
|
905
|
+
for (const rec of bucket.values()) {
|
|
906
|
+
if (!want && rec.archived) continue;
|
|
907
|
+
if (want && !want.has(rec.callId)) continue;
|
|
908
|
+
out.push(recView(rec));
|
|
909
|
+
}
|
|
910
|
+
out.sort((a, b) => a.at < b.at ? -1 : 1);
|
|
911
|
+
return {
|
|
912
|
+
ok: true,
|
|
913
|
+
records: out
|
|
914
|
+
};
|
|
915
|
+
},
|
|
916
|
+
"edrv.accept": async (args) => {
|
|
917
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
918
|
+
if ("err" in sc) return {
|
|
919
|
+
ok: false,
|
|
920
|
+
error: sc.err
|
|
921
|
+
};
|
|
922
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
923
|
+
const record = bucket.get(args.callId);
|
|
924
|
+
if (!record) return {
|
|
925
|
+
ok: false,
|
|
926
|
+
error: "记录不存在"
|
|
927
|
+
};
|
|
928
|
+
markDecision(record, args.scope ?? "call", args.hunkIndex, "accepted");
|
|
929
|
+
if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理");
|
|
930
|
+
else await saveBucket(ctx, sc.cwd, bucket, sc.session);
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
record: recView(record)
|
|
934
|
+
};
|
|
935
|
+
},
|
|
936
|
+
"edrv.reject": async (args) => {
|
|
937
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
938
|
+
if ("err" in sc) return {
|
|
939
|
+
ok: false,
|
|
940
|
+
error: sc.err
|
|
941
|
+
};
|
|
942
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
943
|
+
const record = bucket.get(args.callId);
|
|
944
|
+
if (!record) return {
|
|
945
|
+
ok: false,
|
|
946
|
+
error: "记录不存在"
|
|
947
|
+
};
|
|
948
|
+
const scope = args.scope ?? "call";
|
|
949
|
+
const outcome = scope === "hunk" ? await revertHunk(ctx, sc.session, record, args.hunkIndex ?? -1) : await revertCall(ctx, sc.session, record);
|
|
950
|
+
if (!outcome.ok) return {
|
|
951
|
+
ok: false,
|
|
952
|
+
error: outcome.error
|
|
953
|
+
};
|
|
954
|
+
markDecision(record, scope, args.hunkIndex, "rejected");
|
|
955
|
+
record.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
956
|
+
if (recordResolved(record)) await archiveRecords(ctx, sc.session, sc.cwd, bucket, [record], "已处理(回滚)");
|
|
957
|
+
else await saveBucket(ctx, sc.cwd, bucket, sc.session);
|
|
958
|
+
return {
|
|
959
|
+
ok: true,
|
|
960
|
+
record: recView(record)
|
|
961
|
+
};
|
|
962
|
+
},
|
|
963
|
+
"edrv.read": async (args) => {
|
|
964
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
965
|
+
if ("err" in sc) return {
|
|
966
|
+
ok: false,
|
|
967
|
+
error: sc.err
|
|
968
|
+
};
|
|
969
|
+
const fs = ctx.get("fs");
|
|
970
|
+
if (!fs) return {
|
|
971
|
+
ok: false,
|
|
972
|
+
error: "缺少 fs"
|
|
973
|
+
};
|
|
974
|
+
try {
|
|
975
|
+
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
976
|
+
const info = await fs.stat(target);
|
|
977
|
+
if (!info || info.type !== "file") return {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: "文件不存在"
|
|
980
|
+
};
|
|
981
|
+
if ((info.size ?? 0) > 8388608) return {
|
|
982
|
+
ok: false,
|
|
983
|
+
error: "文件过大(>8MB),不支持整文件预览"
|
|
984
|
+
};
|
|
985
|
+
const content = await fs.readText(target);
|
|
986
|
+
return {
|
|
987
|
+
ok: true,
|
|
988
|
+
content,
|
|
989
|
+
size: content.length
|
|
990
|
+
};
|
|
991
|
+
} catch (error) {
|
|
992
|
+
return {
|
|
993
|
+
ok: false,
|
|
994
|
+
error: "读取失败:" + String(error)
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
"edrv.original": async (args) => {
|
|
999
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1000
|
+
if ("err" in sc) return {
|
|
1001
|
+
ok: false,
|
|
1002
|
+
error: sc.err
|
|
1003
|
+
};
|
|
1004
|
+
const fs = ctx.get("fs");
|
|
1005
|
+
if (!fs) return {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
error: "缺少 fs"
|
|
1008
|
+
};
|
|
1009
|
+
try {
|
|
1010
|
+
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
1011
|
+
const info = await fs.stat(target);
|
|
1012
|
+
if (!info || info.type !== "file") return {
|
|
1013
|
+
ok: false,
|
|
1014
|
+
error: "文件不存在"
|
|
1015
|
+
};
|
|
1016
|
+
if ((info.size ?? 0) > 8388608) return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
error: "文件过大(>8MB),不支持差异重建"
|
|
1019
|
+
};
|
|
1020
|
+
const content = await fs.readText(target);
|
|
1021
|
+
const records = [...(await bucketOf(registry, ctx, sc.cwd)).values()].filter((r) => r.path === args.path && !r.archived).sort((a, b) => a.at < b.at ? -1 : 1);
|
|
1022
|
+
if (!records.length) return {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
content,
|
|
1025
|
+
size: content.length,
|
|
1026
|
+
stale: [],
|
|
1027
|
+
fallback: false
|
|
1028
|
+
};
|
|
1029
|
+
const rebuilt = reconstructOriginal(records, content);
|
|
1030
|
+
if (rebuilt.stale.length && records[0].before !== null) return {
|
|
1031
|
+
ok: true,
|
|
1032
|
+
content: records[0].before,
|
|
1033
|
+
size: records[0].before.length,
|
|
1034
|
+
stale: rebuilt.stale,
|
|
1035
|
+
fallback: true
|
|
1036
|
+
};
|
|
1037
|
+
return {
|
|
1038
|
+
ok: true,
|
|
1039
|
+
content: rebuilt.content,
|
|
1040
|
+
size: rebuilt.content.length,
|
|
1041
|
+
stale: rebuilt.stale,
|
|
1042
|
+
fallback: false
|
|
1043
|
+
};
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
return {
|
|
1046
|
+
ok: false,
|
|
1047
|
+
error: "重建失败:" + String(error)
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
"edrv.save": async (args) => {
|
|
1052
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1053
|
+
if ("err" in sc) return {
|
|
1054
|
+
ok: false,
|
|
1055
|
+
error: sc.err
|
|
1056
|
+
};
|
|
1057
|
+
const fs = ctx.get("fs");
|
|
1058
|
+
if (!fs) return {
|
|
1059
|
+
ok: false,
|
|
1060
|
+
error: "缺少 fs"
|
|
1061
|
+
};
|
|
1062
|
+
try {
|
|
1063
|
+
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
1064
|
+
await fs.writeText(target, args.content, void 0, void 0, policyOf(ctx, sc.session));
|
|
1065
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
1066
|
+
let changed = false;
|
|
1067
|
+
for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded !== true) {
|
|
1068
|
+
rec.superseded = true;
|
|
1069
|
+
rec.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1070
|
+
changed = true;
|
|
1071
|
+
}
|
|
1072
|
+
if (changed) {
|
|
1073
|
+
const done = [];
|
|
1074
|
+
for (const rec of bucket.values()) if (rec.path === args.path && rec.superseded) done.push(rec);
|
|
1075
|
+
if (done.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, done, "被手动编辑覆盖");
|
|
1076
|
+
else await saveBucket(ctx, sc.cwd, bucket, sc.session);
|
|
1077
|
+
}
|
|
1078
|
+
return { ok: true };
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
return {
|
|
1081
|
+
ok: false,
|
|
1082
|
+
error: "保存失败:" + String(error)
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
"edrv.archiveList": async (args) => {
|
|
1087
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1088
|
+
if ("err" in sc) return {
|
|
1089
|
+
ok: false,
|
|
1090
|
+
error: sc.err
|
|
1091
|
+
};
|
|
1092
|
+
const entries = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd).map((b) => {
|
|
1093
|
+
const recs = Array.isArray(b.records) ? b.records : [];
|
|
1094
|
+
const sum = recs.reduce((s, r) => {
|
|
1095
|
+
const sm = r.summary || {
|
|
1096
|
+
accepted: 0,
|
|
1097
|
+
rejected: 0,
|
|
1098
|
+
pending: 0,
|
|
1099
|
+
superseded: false
|
|
1100
|
+
};
|
|
1101
|
+
s.accepted += sm.accepted || 0;
|
|
1102
|
+
s.rejected += sm.rejected || 0;
|
|
1103
|
+
s.pending += sm.pending || 0;
|
|
1104
|
+
if (sm.superseded) s.superseded++;
|
|
1105
|
+
return s;
|
|
1106
|
+
}, {
|
|
1107
|
+
accepted: 0,
|
|
1108
|
+
rejected: 0,
|
|
1109
|
+
pending: 0,
|
|
1110
|
+
superseded: 0
|
|
1111
|
+
});
|
|
1112
|
+
return {
|
|
1113
|
+
at: b.at,
|
|
1114
|
+
lastAt: b.lastAt || b.at,
|
|
1115
|
+
path: b.path,
|
|
1116
|
+
batch: b.batch ?? null,
|
|
1117
|
+
reason: b.reason ?? null,
|
|
1118
|
+
nRecords: recs.length,
|
|
1119
|
+
summary: sum
|
|
1120
|
+
};
|
|
1121
|
+
});
|
|
1122
|
+
entries.sort((a, b) => Number(b.batch ?? -1) - Number(a.batch ?? -1) || (a.at < b.at ? 1 : -1));
|
|
1123
|
+
return {
|
|
1124
|
+
ok: true,
|
|
1125
|
+
entries
|
|
1126
|
+
};
|
|
1127
|
+
},
|
|
1128
|
+
"edrv.archiveRead": async (args) => {
|
|
1129
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1130
|
+
if ("err" in sc) return {
|
|
1131
|
+
ok: false,
|
|
1132
|
+
error: sc.err
|
|
1133
|
+
};
|
|
1134
|
+
const batchPath = args.path;
|
|
1135
|
+
return {
|
|
1136
|
+
ok: true,
|
|
1137
|
+
batches: parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && (!batchPath || b.path === batchPath))
|
|
1138
|
+
};
|
|
1139
|
+
},
|
|
1140
|
+
"edrv.rollback": async (args) => {
|
|
1141
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1142
|
+
if ("err" in sc) return {
|
|
1143
|
+
ok: false,
|
|
1144
|
+
error: sc.err
|
|
1145
|
+
};
|
|
1146
|
+
if (!ctx.get("fs")) return {
|
|
1147
|
+
ok: false,
|
|
1148
|
+
error: "缺少 fs"
|
|
1149
|
+
};
|
|
1150
|
+
const batch = args.batch;
|
|
1151
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
1152
|
+
let affected = [];
|
|
1153
|
+
let beforeRec = null;
|
|
1154
|
+
if (batch !== void 0) {
|
|
1155
|
+
const all = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && b.path === args.path);
|
|
1156
|
+
const recs = [];
|
|
1157
|
+
for (const b of all) if (b.batch === batch) for (const r of b.records || []) recs.push(r);
|
|
1158
|
+
if (!recs.length) return {
|
|
1159
|
+
ok: false,
|
|
1160
|
+
error: "归档中找不到该批次"
|
|
1161
|
+
};
|
|
1162
|
+
recs.sort((a, b2) => a.at < b2.at ? -1 : 1);
|
|
1163
|
+
beforeRec = recs[0];
|
|
1164
|
+
affected = recs;
|
|
1165
|
+
} else {
|
|
1166
|
+
for (const r of bucket.values()) if (r.path === args.path && !r.archived) affected.push(r);
|
|
1167
|
+
if (!affected.length) return {
|
|
1168
|
+
ok: false,
|
|
1169
|
+
error: "该文件没有可回滚的差异"
|
|
1170
|
+
};
|
|
1171
|
+
affected.sort((a, b2) => a.at < b2.at ? -1 : 1);
|
|
1172
|
+
beforeRec = affected[0];
|
|
1173
|
+
}
|
|
1174
|
+
const rerr = await restoreFile(ctx, sc.session, beforeRec);
|
|
1175
|
+
if (rerr) return {
|
|
1176
|
+
ok: false,
|
|
1177
|
+
error: rerr
|
|
1178
|
+
};
|
|
1179
|
+
if (batch === void 0) await archiveRecords(ctx, sc.session, sc.cwd, bucket, affected, "已回滚");
|
|
1180
|
+
else {
|
|
1181
|
+
const sumRecs = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd && b.path === args.path && b.batch === batch).reduce((s, b) => s + (b.records || []).length, 0);
|
|
1182
|
+
const logRec = Object.assign({}, affected[0], { note: (affected[0].note ? affected[0].note + ";" : "") + "回滚至本批次前(批次 " + batch + ")" });
|
|
1183
|
+
await appendArchiveEntries(ctx, sc.cwd, [archiveEntryFor([logRec], sc.cwd, "已回滚(批次 " + batch + "," + sumRecs + " 条)")], sc.session);
|
|
1184
|
+
const activeRecs = [];
|
|
1185
|
+
for (const r of bucket.values()) if (r.path === args.path && !r.archived) activeRecs.push(r);
|
|
1186
|
+
if (activeRecs.length) await archiveRecords(ctx, sc.session, sc.cwd, bucket, activeRecs, "已回滚(批次回滚覆盖)");
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
ok: true,
|
|
1190
|
+
path: args.path,
|
|
1191
|
+
batch: batch ?? null
|
|
1192
|
+
};
|
|
1193
|
+
},
|
|
1194
|
+
"edrv.debug": async (args) => {
|
|
1195
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1196
|
+
if ("err" in sc) return {
|
|
1197
|
+
ok: false,
|
|
1198
|
+
error: sc.err
|
|
1199
|
+
};
|
|
1200
|
+
const fs = ctx.get("fs");
|
|
1201
|
+
const text = String(args.text ?? "");
|
|
1202
|
+
const task = (debugWriteQueues.get(sc.cwd) || Promise.resolve()).then(async () => {
|
|
1203
|
+
try {
|
|
1204
|
+
const target = await fs.resolve(".dsh-edit-review-debug.log", { cwd: sc.cwd });
|
|
1205
|
+
const old = await fs.readText(target).catch(() => "");
|
|
1206
|
+
const line = (/* @__PURE__ */ new Date()).toISOString() + " " + text + "\n";
|
|
1207
|
+
await fs.writeText(target, (old || "") + line, void 0, void 0, policyOf(ctx, sc.session));
|
|
1208
|
+
} catch (e) {}
|
|
1209
|
+
});
|
|
1210
|
+
debugWriteQueues.set(sc.cwd, task);
|
|
1211
|
+
await task;
|
|
1212
|
+
console.error("[edrv-debug] " + text);
|
|
1213
|
+
return { ok: true };
|
|
1214
|
+
},
|
|
1215
|
+
"edrv.searchFiles": async (args) => {
|
|
1216
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
1217
|
+
if ("err" in sc) return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
error: sc.err
|
|
1220
|
+
};
|
|
1221
|
+
const query = String(args.query ?? "").trim().toLowerCase();
|
|
1222
|
+
const files = [];
|
|
1223
|
+
if (query.length >= 2) {
|
|
1224
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
1225
|
+
const pool = /* @__PURE__ */ new Set();
|
|
1226
|
+
for (const rec of bucket.values()) if (rec.path) pool.add(rec.path);
|
|
1227
|
+
const listed = await listWorkspaceFiles(ctx, sc.session, sc.cwd);
|
|
1228
|
+
if (listed) for (const f of listed) pool.add(f);
|
|
1229
|
+
const score = (p) => {
|
|
1230
|
+
const pl = p.toLowerCase();
|
|
1231
|
+
if ((pl.split(/[\\/]/).pop() || "").includes(query)) return 0;
|
|
1232
|
+
if (pl.includes(query)) return 1;
|
|
1233
|
+
return -1;
|
|
1234
|
+
};
|
|
1235
|
+
const hit = [...pool].filter((p) => score(p) >= 0);
|
|
1236
|
+
hit.sort((a, b) => score(a) - score(b) || (a < b ? -1 : 1));
|
|
1237
|
+
files.push(...hit.slice(0, 50));
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
ok: true,
|
|
1241
|
+
files,
|
|
1242
|
+
truncated: files.length >= 50
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* 统一入口:按方法分发到 handler 表。
|
|
1249
|
+
* @author ddj 2026年08月20号
|
|
1250
|
+
*/
|
|
1251
|
+
async function handleRpc(ctx, registry, method, args) {
|
|
1252
|
+
const handler = buildHandlers(ctx, registry)[method];
|
|
1253
|
+
if (!handler) return {
|
|
1254
|
+
ok: false,
|
|
1255
|
+
error: "未知方法: " + String(method)
|
|
1256
|
+
};
|
|
1257
|
+
return handler(args);
|
|
1258
|
+
}
|
|
1259
|
+
//#endregion
|
|
1260
|
+
//#region src/index.ts
|
|
1261
|
+
/**
|
|
1262
|
+
* @dsh-external 生态 → dsh-vscode-mode:DSH 上的类 VSCode 编码体验(Host 半入口)。
|
|
1263
|
+
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(11 个 edrv.* 方法)、
|
|
1264
|
+
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化。
|
|
1265
|
+
* 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes(host 模块)。
|
|
1266
|
+
* 作者 ddj 2026-08-20
|
|
1267
|
+
*/
|
|
1268
|
+
const name = "dsh-vscode-mode";
|
|
1269
|
+
const inject = [
|
|
1270
|
+
"sessions",
|
|
1271
|
+
"fs",
|
|
1272
|
+
"webServer"
|
|
1273
|
+
];
|
|
1274
|
+
/**
|
|
1275
|
+
* 装配插件:挂事件监听、注册路由。
|
|
1276
|
+
* @author ddj 2026年08月20号
|
|
1277
|
+
* @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
|
|
1278
|
+
* @param config 插件配置(可选 imageDir 覆盖图标目录)
|
|
1279
|
+
*/
|
|
1280
|
+
function apply(ctx, config) {
|
|
1281
|
+
const registry = /* @__PURE__ */ new Map();
|
|
1282
|
+
ctx.on("tools/result", (exec, result) => {
|
|
1283
|
+
captureToolResult(ctx, registry, exec, result);
|
|
1284
|
+
});
|
|
1285
|
+
ctx.on("session/disposed", (session) => {
|
|
1286
|
+
const cwd = cwdOf(session);
|
|
1287
|
+
if (cwd) {
|
|
1288
|
+
registry.delete(cwd);
|
|
1289
|
+
dropFileIndex(cwd);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args));
|
|
1293
|
+
ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪)");
|
|
1294
|
+
}
|
|
1295
|
+
//#endregion
|
|
1296
|
+
export { apply, inject, name };
|
|
1297
|
+
|
|
1298
|
+
//# sourceMappingURL=index.js.map
|