dsh-vscode-mode 0.7.0 → 0.9.0
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 +37 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +2804 -300
- package/lib/client.js.map +1 -1
- package/lib/index.js +1975 -191
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +4 -2
- package/src/ai/inline.ts +19 -2
- package/src/ai/svnTriage.ts +150 -0
- package/src/capture.ts +225 -51
- package/src/client/addToConversation.ts +203 -1
- package/src/client/compat.ts +134 -4
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +65 -36
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/pendingNav.ts +141 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +47 -5
- package/src/client/svnStatus.ts +206 -1
- package/src/client/svnStore.ts +6 -1
- package/src/client/ui/AiSettings.ts +47 -14
- package/src/client/ui/DiffBox.ts +1 -1
- package/src/client/ui/DiffLauncher.ts +6 -4
- package/src/client/ui/EditorView.ts +149 -22
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +91 -0
- package/src/compat.ts +26 -5
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +274 -49
- package/src/index.ts +18 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/model.ts +2 -1
- package/src/perf.ts +90 -4
- package/src/remoteWorkspace.ts +177 -0
- package/src/routes.ts +13 -1
- package/src/rpc.ts +115 -39
- package/src/shared/ai.ts +12 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +162 -2
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import z from "@deepseek-ai/schemastery";
|
|
10
11
|
import { inflateRawSync } from "node:zlib";
|
|
11
12
|
//#region src/mcp.ts
|
|
12
13
|
/**
|
|
@@ -190,6 +191,17 @@ function tortoiseLaunchArgv(exe, action, absPath) {
|
|
|
190
191
|
"/path:" + absPath
|
|
191
192
|
];
|
|
192
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* 是否属于「工作副本变更」(`normal`/`external` 不算;`ignored` 由开关决定是否收录)。
|
|
196
|
+
* @author ddj 2026年09月16号
|
|
197
|
+
* @param entry 变更条目
|
|
198
|
+
* @returns 是否计入变更清单
|
|
199
|
+
*/
|
|
200
|
+
function isSvnChange(entry) {
|
|
201
|
+
if (entry.status === "normal" || entry.status === "external") return false;
|
|
202
|
+
if (entry.status === "ignored") return false;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
193
205
|
/** W2-6 内容护栏嗅探窗(字符数;NUL/U+FFFD 只看前 8K,全量扫描大文本得不偿失)。 */
|
|
194
206
|
const GUARD_SNIFF_CHARS = 8192;
|
|
195
207
|
/**
|
|
@@ -207,6 +219,245 @@ function contentGuardOf(text) {
|
|
|
207
219
|
if (window.includes("�")) return { encodingHint: true };
|
|
208
220
|
return {};
|
|
209
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* 分区名(changelist 名)合法性校验:返回错误文案,合法返回 null。
|
|
224
|
+
* 规则:去首尾空白后非空、不以 `-` 开头(防被 svn 当成选项解析)、长度 ≤ 上限。
|
|
225
|
+
* client 预检 + host 权威校验共用,避免两处漂移。
|
|
226
|
+
* @author ddj 2026年09月23号
|
|
227
|
+
* @param name 用户输入的分区名
|
|
228
|
+
* @returns 错误文案;合法为 null
|
|
229
|
+
*/
|
|
230
|
+
function svnChangelistNameErrorOf(name) {
|
|
231
|
+
const trimmed = String(name ?? "").trim();
|
|
232
|
+
if (!trimmed) return "分区名不能为空";
|
|
233
|
+
if (trimmed.startsWith("-")) return "分区名不能以 - 开头";
|
|
234
|
+
if (trimmed.length > 64) return "分区名最长 64 字符";
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/** AI 方案分析条目数上限(超出仅按路径分析并标记截断)。 */
|
|
238
|
+
const AI_PLAN_PATHS_CAP = 1500;
|
|
239
|
+
/** AI 方案 diff 上下文字节上限(超出降级 paths-only)。 */
|
|
240
|
+
const AI_PLAN_DIFF_CAP = 2097152;
|
|
241
|
+
/** AI 方案 LLM 调用超时(毫秒;推理模型 + 高思考档位 + diff 上下文实测 120s 不够(huoshan/glm-5.3-flash),放宽到 5 分钟)。 */
|
|
242
|
+
const AI_PLAN_TIMEOUT_MS = 3e5;
|
|
243
|
+
/** 松散数组取值(非数组按空处理;AI 输出形状容错)。 */
|
|
244
|
+
function listOf(raw) {
|
|
245
|
+
return Array.isArray(raw) ? raw : [];
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* AI 方案条目解析(宽松:接受 `{path, reason}` 或裸路径字符串;反斜杠归一为 `/`)。
|
|
249
|
+
* @author ddj 2026年09月23号
|
|
250
|
+
* @param raw 原始元素
|
|
251
|
+
* @returns 解析结果(不可解析返回 null——非路径垃圾不作数)
|
|
252
|
+
*/
|
|
253
|
+
function planItemOf(raw) {
|
|
254
|
+
const obj = typeof raw === "string" ? { path: raw } : raw && typeof raw === "object" ? raw : null;
|
|
255
|
+
if (!obj) return null;
|
|
256
|
+
const path = typeof obj.path === "string" ? obj.path.trim().replace(/\\/g, "/") : "";
|
|
257
|
+
if (!path) return null;
|
|
258
|
+
const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 120) : "";
|
|
259
|
+
return reason ? {
|
|
260
|
+
path,
|
|
261
|
+
reason
|
|
262
|
+
} : { path };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* AI 方案分组解析(宽松:name + paths 数组;路径逐个规范化)。
|
|
266
|
+
* @author ddj 2026年09月23号
|
|
267
|
+
* @param raw 原始元素
|
|
268
|
+
* @returns 解析结果(无合法名返回 null)
|
|
269
|
+
*/
|
|
270
|
+
function planGroupOf(raw) {
|
|
271
|
+
if (!raw || typeof raw !== "object") return null;
|
|
272
|
+
const obj = raw;
|
|
273
|
+
const name = typeof obj.name === "string" ? obj.name.trim() : "";
|
|
274
|
+
if (!name) return null;
|
|
275
|
+
const paths = [];
|
|
276
|
+
for (const p of listOf(obj.paths)) {
|
|
277
|
+
const path = typeof p === "string" ? p.trim().replace(/\\/g, "/") : "";
|
|
278
|
+
if (path) paths.push(path);
|
|
279
|
+
}
|
|
280
|
+
const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 120) : "";
|
|
281
|
+
return reason ? {
|
|
282
|
+
name,
|
|
283
|
+
paths,
|
|
284
|
+
reason
|
|
285
|
+
} : {
|
|
286
|
+
name,
|
|
287
|
+
paths
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* AI 方案归一化(纯函数):以真实变更清单为白名单裁决三段建议。
|
|
292
|
+
* - 幻觉路径(不在清单中)/状态不符(revert 须 versioned、ignore 须 unversioned)→ 丢弃并计数;
|
|
293
|
+
* - 互斥优先级 revert > ignore > group(同一路径至多归一段,还原后条目消失故优先);
|
|
294
|
+
* - 组名过 svnChangelistNameErrorOf、组数上限 AI_PLAN_GROUPS_CAP,空组剔除。
|
|
295
|
+
* @author ddj 2026年09月23号
|
|
296
|
+
* @param raw AI 输出解析结果(形状不定,内部容错)
|
|
297
|
+
* @param entries 真实变更清单(白名单来源)
|
|
298
|
+
* @returns 归一化方案与被丢弃的候选路径数
|
|
299
|
+
*/
|
|
300
|
+
function normalizeAiPlan(raw, entries) {
|
|
301
|
+
const plan = {
|
|
302
|
+
groups: [],
|
|
303
|
+
reverts: [],
|
|
304
|
+
ignores: []
|
|
305
|
+
};
|
|
306
|
+
if (!raw || typeof raw !== "object") return {
|
|
307
|
+
plan,
|
|
308
|
+
dropped: 0
|
|
309
|
+
};
|
|
310
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const entry of entries) if (entry?.path) byPath.set(entry.path, entry);
|
|
312
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
313
|
+
let dropped = 0;
|
|
314
|
+
/** 单条裁决:真实存在 + 状态符合 + 未被更高优先级段认领。 */
|
|
315
|
+
const accept = (path, wantVersioned) => {
|
|
316
|
+
const entry = byPath.get(path);
|
|
317
|
+
if (!entry || !isSvnChange(entry) || entry.versioned !== wantVersioned || claimed.has(path)) {
|
|
318
|
+
dropped++;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
claimed.add(path);
|
|
322
|
+
return true;
|
|
323
|
+
};
|
|
324
|
+
const src = raw;
|
|
325
|
+
for (const el of listOf(src.reverts)) {
|
|
326
|
+
const item = planItemOf(el);
|
|
327
|
+
if (item && accept(item.path, true)) plan.reverts.push(item);
|
|
328
|
+
}
|
|
329
|
+
for (const el of listOf(src.ignores)) {
|
|
330
|
+
const item = planItemOf(el);
|
|
331
|
+
if (item && accept(item.path, false)) plan.ignores.push(item);
|
|
332
|
+
}
|
|
333
|
+
for (const el of listOf(src.groups)) {
|
|
334
|
+
const group = planGroupOf(el);
|
|
335
|
+
if (!group) {
|
|
336
|
+
const obj = el && typeof el === "object" ? el : null;
|
|
337
|
+
for (const p of listOf(obj?.paths)) if (typeof p === "string" && p.trim()) dropped++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const nameError = svnChangelistNameErrorOf(group.name) !== null;
|
|
341
|
+
const over = plan.groups.length >= 20;
|
|
342
|
+
const valid = [];
|
|
343
|
+
for (const path of group.paths) {
|
|
344
|
+
if (nameError || over) {
|
|
345
|
+
dropped++;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (accept(path, true)) valid.push(path);
|
|
349
|
+
}
|
|
350
|
+
if (valid.length && !nameError && !over) plan.groups.push(group.reason ? {
|
|
351
|
+
name: group.name,
|
|
352
|
+
paths: valid,
|
|
353
|
+
reason: group.reason
|
|
354
|
+
} : {
|
|
355
|
+
name: group.name,
|
|
356
|
+
paths: valid
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
plan,
|
|
361
|
+
dropped
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/shared/nativeOpen.ts
|
|
366
|
+
/**
|
|
367
|
+
* dsh-vscode-mode shared — 原生打开范围判定(host/client 双面契约)。
|
|
368
|
+
* 背景:DSH 文件预览双形态——本插件认领 `dsh-resource://file/**` 进 Monaco 编辑,
|
|
369
|
+
* 官方渲染器(Office 预览/不可预览提示/表格预览)看认领让位。本模块承载「原生打开」
|
|
370
|
+
* 范围的单一事实源:默认集 = 让位清单(插件本来就不认领的后缀),用户可经设置
|
|
371
|
+
* `nativeOpenExts`(逗号分隔)增删;host 的 Config schema 默认值与 client 的
|
|
372
|
+
* 文件树点击判定 / claim 认领判定共用,保证三处永不同源漂移。
|
|
373
|
+
* 决策沿袭(不因本模块迁移而变):CSV/TSV **不在**默认集——官方 0.1.7 有表格预览,
|
|
374
|
+
* 但编辑器可编辑性优先,仅用户显式加入 nativeOpenExts 才让位(同 avif 例外先例)。
|
|
375
|
+
* 作者 ddj 2026年09月22号
|
|
376
|
+
*/
|
|
377
|
+
/**
|
|
378
|
+
* Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
|
|
379
|
+
* 声明 `doc/docx/xls/xlsx/ppt/pptx`(0.1.6 起侧栏 Office 预览;0.1.7 扩展表格 CSV/TSV
|
|
380
|
+
* 只读预览——本清单刻意不含 csv/tsv,见文件头决策)。本插件让位官方(不认领),
|
|
381
|
+
* 否则会把这些文件路由进 Monaco 而丢掉官方预览。
|
|
382
|
+
*/
|
|
383
|
+
const OFFICE_EXT = [
|
|
384
|
+
"doc",
|
|
385
|
+
"docx",
|
|
386
|
+
"xls",
|
|
387
|
+
"xlsx",
|
|
388
|
+
"ppt",
|
|
389
|
+
"pptx",
|
|
390
|
+
"odt",
|
|
391
|
+
"ods",
|
|
392
|
+
"odp",
|
|
393
|
+
"pages",
|
|
394
|
+
"numbers"
|
|
395
|
+
];
|
|
396
|
+
/**
|
|
397
|
+
* 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
|
|
398
|
+
* `dsh-client-ui-sidebar-documentpreview/lib/client.js`)。这些后缀官方会给出
|
|
399
|
+
* 「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
|
|
400
|
+
*
|
|
401
|
+
* ⚠️ 刻意例外:官方清单含 `avif`,但本插件图片预览支持 avif(浏览器原生解码),
|
|
402
|
+
* 故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
|
|
403
|
+
* @author ddj 2026年09月18号
|
|
404
|
+
*/
|
|
405
|
+
const BLIND_EXT = [
|
|
406
|
+
"mp4",
|
|
407
|
+
"mov",
|
|
408
|
+
"avi",
|
|
409
|
+
"mkv",
|
|
410
|
+
"webm",
|
|
411
|
+
"flv",
|
|
412
|
+
"wmv",
|
|
413
|
+
"m4v",
|
|
414
|
+
"mp3",
|
|
415
|
+
"wav",
|
|
416
|
+
"flac",
|
|
417
|
+
"ogg",
|
|
418
|
+
"m4a",
|
|
419
|
+
"aac",
|
|
420
|
+
"wma",
|
|
421
|
+
"opus",
|
|
422
|
+
"zip",
|
|
423
|
+
"gz",
|
|
424
|
+
"tgz",
|
|
425
|
+
"bz2",
|
|
426
|
+
"xz",
|
|
427
|
+
"zst",
|
|
428
|
+
"7z",
|
|
429
|
+
"rar",
|
|
430
|
+
"tar",
|
|
431
|
+
"jar",
|
|
432
|
+
"exe",
|
|
433
|
+
"dll",
|
|
434
|
+
"so",
|
|
435
|
+
"dylib",
|
|
436
|
+
"bin",
|
|
437
|
+
"o",
|
|
438
|
+
"class",
|
|
439
|
+
"pyc",
|
|
440
|
+
"wasm",
|
|
441
|
+
"ttf",
|
|
442
|
+
"otf",
|
|
443
|
+
"woff",
|
|
444
|
+
"woff2",
|
|
445
|
+
"eot",
|
|
446
|
+
"dmg",
|
|
447
|
+
"iso",
|
|
448
|
+
"img",
|
|
449
|
+
"sqlite",
|
|
450
|
+
"db",
|
|
451
|
+
"psd",
|
|
452
|
+
"ai",
|
|
453
|
+
"sketch",
|
|
454
|
+
"tiff",
|
|
455
|
+
"tif",
|
|
456
|
+
"heic",
|
|
457
|
+
"heif"
|
|
458
|
+
];
|
|
459
|
+
/** 默认范围的设置序列(逗号分隔;Config schema 默认值与设置页回显同源)。 */
|
|
460
|
+
const DEFAULT_NATIVE_CSV = [...OFFICE_EXT, ...BLIND_EXT].join(",");
|
|
210
461
|
//#endregion
|
|
211
462
|
//#region src/shared/logger.ts
|
|
212
463
|
/**
|
|
@@ -322,19 +573,23 @@ function bindHostLog(ctx) {
|
|
|
322
573
|
* dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
|
|
323
574
|
* 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
|
|
324
575
|
* rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
|
|
325
|
-
* 服务的 installSection
|
|
576
|
+
* 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
|
|
577
|
+
* 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
|
|
326
578
|
* 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
|
|
327
579
|
* 全程 try/catch,不产生未捕获 rejection。
|
|
328
|
-
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
|
|
580
|
+
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号 / 2026年09月23号
|
|
329
581
|
*/
|
|
330
582
|
const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
|
|
331
583
|
const FILE_OPEN_DEFAULT = "auto";
|
|
332
|
-
/** AI
|
|
584
|
+
/** AI 配置默认值(补全默认关;路由空 = 自动;档位空 = 跟随模型默认;任务模型空 = 跟随补全配置)。 */
|
|
333
585
|
const AI_CONFIG_DEFAULT = {
|
|
334
586
|
enabled: false,
|
|
335
587
|
provider: "",
|
|
336
588
|
model: "",
|
|
337
|
-
effort: ""
|
|
589
|
+
effort: "",
|
|
590
|
+
taskProvider: "",
|
|
591
|
+
taskModel: "",
|
|
592
|
+
taskEffort: ""
|
|
338
593
|
};
|
|
339
594
|
let depsPromise;
|
|
340
595
|
/**
|
|
@@ -437,6 +692,7 @@ function loadSettingsDeps(importFn = hostImport) {
|
|
|
437
692
|
const INSTALL_UNMOUNTED = "设置 section 尚未装配";
|
|
438
693
|
const INSTALL_LEGACY = "rc 线:dsh-settings.installSettingsSection";
|
|
439
694
|
const INSTALL_SERVICE = "0.1.2-alpha 线:settings 服务 installSection";
|
|
695
|
+
const INSTALL_FORMS = "0.1.7 线:设置并入插件 Config schema(SettingsForms)";
|
|
440
696
|
const INSTALL_NONE = "两路均不可用:设置持久化降级为配置值";
|
|
441
697
|
let observedInstall = {
|
|
442
698
|
strategy: "unknown",
|
|
@@ -459,14 +715,51 @@ function settingsInstallNote() {
|
|
|
459
715
|
return observedInstall.note;
|
|
460
716
|
}
|
|
461
717
|
/**
|
|
718
|
+
* 判定设置写冲突错误(0.1.7 SettingsConflictError:code 常量 / 构造器名双通道)。
|
|
719
|
+
* @author ddj 2026年09月22号
|
|
720
|
+
* @param error 捕获到的错误
|
|
721
|
+
* @returns 是否为 revision 冲突
|
|
722
|
+
*/
|
|
723
|
+
function isConflictError(error) {
|
|
724
|
+
if (!error || typeof error !== "object") return false;
|
|
725
|
+
const e = error;
|
|
726
|
+
return e.code === "SETTINGS_CONFLICT" || e.name === "SettingsConflictError";
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* 0.1.7 forms 策略:订阅 settings/document-updated 事件,目标 namespace 变化时
|
|
730
|
+
* 把 describe 最新值推给 hooks(setSource + onChange),替代旧 installSection 的
|
|
731
|
+
* 变更回调;disposer 经 ctx.effect 随插件 fiber 卸载(热重载不泄漏)。
|
|
732
|
+
* @author ddj 2026年09月22号
|
|
733
|
+
* @param ctx DSH host 上下文
|
|
734
|
+
* @param provider settings 服务(SettingsForms)
|
|
735
|
+
* @param ns 目标设置命名空间
|
|
736
|
+
* @param hooks 设置源绑定与变更回调
|
|
737
|
+
*/
|
|
738
|
+
function watchDocUpdates(ctx, provider, ns, hooks) {
|
|
739
|
+
try {
|
|
740
|
+
const bus = ctx;
|
|
741
|
+
if (typeof bus.on !== "function") return;
|
|
742
|
+
const off = bus.on("settings/document-updated", (updatedNs) => {
|
|
743
|
+
if (updatedNs !== ns) return;
|
|
744
|
+
hooks.setSource(() => readSectionValue(provider, ns));
|
|
745
|
+
hooks.onChange();
|
|
746
|
+
});
|
|
747
|
+
if (typeof bus.effect === "function" && typeof off === "function") bus.effect(() => off);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
log.warn("settings/document-updated 订阅失败:" + String(error));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
462
753
|
* 设置 section 版本自适应安装(核心策略分派)。
|
|
463
754
|
* legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
|
|
464
755
|
* service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
|
|
465
756
|
* provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
|
|
466
|
-
* base 层与 fiber
|
|
757
|
+
* base 层与 fiber 卸载回退)。
|
|
758
|
+
* forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
|
|
759
|
+
* 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
|
|
467
760
|
* none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
|
|
468
761
|
* 全程不抛错、不产生未捕获 rejection。
|
|
469
|
-
* @author ddj 2026年09月02号
|
|
762
|
+
* @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
|
|
470
763
|
* @param ctx DSH host 上下文(inject 可选探测)
|
|
471
764
|
* @param ns 设置命名空间
|
|
472
765
|
* @param schema schemastery schema
|
|
@@ -499,6 +792,12 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
|
|
|
499
792
|
const provider = typeof sc.get === "function" ? sc.get("settings") : sc.settings;
|
|
500
793
|
const install = provider?.installSection;
|
|
501
794
|
if (typeof install !== "function") {
|
|
795
|
+
const forms = provider;
|
|
796
|
+
if (typeof forms?.describe === "function" && typeof forms?.update === "function") {
|
|
797
|
+
recordInstall("forms", INSTALL_FORMS);
|
|
798
|
+
watchDocUpdates(ctx, forms, ns, hooks);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
502
801
|
log.warn("settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
|
|
503
802
|
recordInstall("none", INSTALL_NONE);
|
|
504
803
|
return;
|
|
@@ -529,26 +828,205 @@ function keybindingsShape(z) {
|
|
|
529
828
|
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
|
|
530
829
|
return shape;
|
|
531
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* 设置节 describe 项的形状校验:value 须为普通对象。
|
|
833
|
+
* ns 在两代语义不同(旧=自装 section 名,0.1.7=profile entry id),同名撞车时
|
|
834
|
+
* 以形状兜底防误命中非设置数据;0.1.7 下本插件 entry 的 Config 值与旧 section
|
|
835
|
+
* 值同为设置键对象,两形状天然兼容。
|
|
836
|
+
* @author ddj 2026年09月22号
|
|
837
|
+
* @param value describe 项的 value 字段
|
|
838
|
+
* @returns 是否具备设置节形状
|
|
839
|
+
*/
|
|
840
|
+
function isSectionShaped(value) {
|
|
841
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* 按 ns + 形状校验查找设置节(host 读路径统一入口)。
|
|
845
|
+
* @author ddj 2026年09月22号
|
|
846
|
+
* @param provider settings 服务(可空)
|
|
847
|
+
* @param ns 设置命名空间
|
|
848
|
+
* @returns 命中的 describe 项;未命中返回 undefined
|
|
849
|
+
*/
|
|
850
|
+
function sectionOf(provider, ns) {
|
|
851
|
+
const items = provider?.describe?.({ redactSecrets: true });
|
|
852
|
+
if (!items) return void 0;
|
|
853
|
+
return items.find((item) => item.ns === ns && isSectionShaped(item.value));
|
|
854
|
+
}
|
|
855
|
+
/** 读设置节存储值(未就绪/形状不符返回 undefined)。 */
|
|
856
|
+
function readSectionValue(provider, ns) {
|
|
857
|
+
return sectionOf(provider, ns)?.value;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 带冲突自愈的设置写入:0.1.7 SettingsConflictError(revision 拒写)时重读
|
|
861
|
+
* revision 重试一次;其余错误与服务缺失一律结构化返回,绝不抛未捕获异常。
|
|
862
|
+
* @author ddj 2026年09月22号
|
|
863
|
+
* @param provider settings 服务(可空)
|
|
864
|
+
* @param ns 设置命名空间
|
|
865
|
+
* @param patch 写入字段
|
|
866
|
+
* @param expectedRevision 读侧携带的 revision(缺省用 describe 最新值)
|
|
867
|
+
* @returns 写入结果
|
|
868
|
+
*/
|
|
869
|
+
async function updateSection(provider, ns, patch, expectedRevision) {
|
|
870
|
+
if (!provider?.update) return {
|
|
871
|
+
ok: false,
|
|
872
|
+
error: "settings 服务无 update"
|
|
873
|
+
};
|
|
874
|
+
const first = expectedRevision ?? sectionOf(provider, ns)?.revision;
|
|
875
|
+
try {
|
|
876
|
+
await provider.update(ns, patch, first);
|
|
877
|
+
return { ok: true };
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (!isConflictError(error)) return {
|
|
880
|
+
ok: false,
|
|
881
|
+
error: String(error)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
const fresh = sectionOf(provider, ns)?.revision;
|
|
885
|
+
try {
|
|
886
|
+
await provider.update(ns, patch, fresh);
|
|
887
|
+
return {
|
|
888
|
+
ok: true,
|
|
889
|
+
conflict: true
|
|
890
|
+
};
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return {
|
|
893
|
+
ok: false,
|
|
894
|
+
conflict: true,
|
|
895
|
+
error: String(error)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
|
|
901
|
+
* 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
|
|
902
|
+
* aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
|
|
903
|
+
* (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
|
|
904
|
+
* options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
|
|
905
|
+
* 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
|
|
906
|
+
* client configForms 恒 unavailable;且字段须位于固定 object 路径(keybindings 整个
|
|
907
|
+
* object 标记、子键不标)。section 安装路径(legacy/service)**不标**:rc/0.1.6 的
|
|
908
|
+
* register→resolve 无该门槛,标了反而让 scope.get() 返回引用污染读取。
|
|
909
|
+
* 标记经 markVolatile 能力守卫:z 无 .volatile()(旧 schemastery)或调用抛错时保持
|
|
910
|
+
* 原字段并记录观测态,模块加载绝不抛错(降级=维持现状,兼容性报告可见)。
|
|
911
|
+
* @author ddj 2026年09月22号(2026年09月23号 增补 options.volatile 与观测)
|
|
912
|
+
* @param z schemastery 命名空间(静态 import 或动态加载均可)
|
|
913
|
+
* @param options volatile:是否按 0.1.7 Config 语义标记字段
|
|
914
|
+
* @returns schemastery object schema
|
|
915
|
+
*/
|
|
916
|
+
function buildSettingsSchema(z, options) {
|
|
917
|
+
const wantVolatile = options?.volatile === true;
|
|
918
|
+
let marked = 0;
|
|
919
|
+
const withVol = (field) => {
|
|
920
|
+
if (!wantVolatile) return field;
|
|
921
|
+
const result = markVolatile(field);
|
|
922
|
+
if (result.marked) marked += 1;
|
|
923
|
+
return result.field;
|
|
924
|
+
};
|
|
925
|
+
const shape = {
|
|
926
|
+
fileOpenTool: withVol(z.string().default(FILE_OPEN_DEFAULT)),
|
|
927
|
+
keybindings: withVol(z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS })),
|
|
928
|
+
sidebarMinWidth: withVol(z.number().default(300)),
|
|
929
|
+
maxOpenEditors: withVol(z.number().default(10)),
|
|
930
|
+
integrationBaseUrl: withVol(z.string().default(INTEGRATION_BASE_DEFAULT)),
|
|
931
|
+
aiInline: withVol(z.boolean().default(AI_CONFIG_DEFAULT.enabled)),
|
|
932
|
+
aiProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.provider)),
|
|
933
|
+
aiModel: withVol(z.string().default(AI_CONFIG_DEFAULT.model)),
|
|
934
|
+
aiEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.effort)),
|
|
935
|
+
aiTaskProvider: withVol(z.string().default(AI_CONFIG_DEFAULT.taskProvider)),
|
|
936
|
+
aiTaskModel: withVol(z.string().default(AI_CONFIG_DEFAULT.taskModel)),
|
|
937
|
+
aiTaskEffort: withVol(z.string().default(AI_CONFIG_DEFAULT.taskEffort)),
|
|
938
|
+
svnPath: withVol(z.string().default("")),
|
|
939
|
+
tortoisePath: withVol(z.string().default(TORTOISE_DIR_DEFAULT)),
|
|
940
|
+
nativeOpenExts: withVol(z.string().default(DEFAULT_NATIVE_CSV))
|
|
941
|
+
};
|
|
942
|
+
if (wantVolatile) configVolatile = {
|
|
943
|
+
requested: true,
|
|
944
|
+
marked,
|
|
945
|
+
total: Object.keys(shape).length
|
|
946
|
+
};
|
|
947
|
+
return z.object(shape);
|
|
948
|
+
}
|
|
949
|
+
let configVolatile = {
|
|
950
|
+
requested: false,
|
|
951
|
+
marked: 0,
|
|
952
|
+
total: 0
|
|
953
|
+
};
|
|
954
|
+
/**
|
|
955
|
+
* 读取 Config volatile 标记观测(0.1.7 设置页可用性的构建期判据)。
|
|
956
|
+
* @author ddj 2026年09月23号
|
|
957
|
+
* @returns 观测态(requested=是否按 Config 语义构建;marked/total=成功标记字段数)
|
|
958
|
+
*/
|
|
959
|
+
function configVolatileState() {
|
|
960
|
+
return configVolatile;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* 为已完成 default 链的 schema 字段追加 volatile 标记(能力守卫)。
|
|
964
|
+
* @author ddj 2026年09月23号
|
|
965
|
+
* @param field schema 字段(两代 schemastery 兼容面,可能没有 volatile 方法)
|
|
966
|
+
* @returns 处理后字段与是否成功标记
|
|
967
|
+
*/
|
|
968
|
+
function markVolatile(field) {
|
|
969
|
+
const target = field;
|
|
970
|
+
if (!target || typeof target.volatile !== "function") return {
|
|
971
|
+
field,
|
|
972
|
+
marked: false
|
|
973
|
+
};
|
|
974
|
+
try {
|
|
975
|
+
return {
|
|
976
|
+
field: target.volatile(),
|
|
977
|
+
marked: true
|
|
978
|
+
};
|
|
979
|
+
} catch {
|
|
980
|
+
return {
|
|
981
|
+
field,
|
|
982
|
+
marked: false
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/** cosmokit volatile 引用写协议符号(Symbol.for 跨拷贝一致,同 cosmokit.isVolatile 判据)。 */
|
|
987
|
+
const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
|
|
988
|
+
/**
|
|
989
|
+
* 解引用 0.1.7 volatile 配置引用(普通值直传)。
|
|
990
|
+
* cordis resolveConfig 会把 .volatile() 字段解析成稳定引用,直接读会得到引用对象;
|
|
991
|
+
* 不解引用则配置回退值全部落入默认值分支。
|
|
992
|
+
* @author ddj 2026年09月23号
|
|
993
|
+
* @param value 配置字段原始值
|
|
994
|
+
* @returns 引用的当前快照,或原值
|
|
995
|
+
*/
|
|
996
|
+
function unref(value) {
|
|
997
|
+
if (value !== null && typeof value === "object" && VOLATILE_WRITE in value) return value.get();
|
|
998
|
+
return value;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* 读插件组合配置字段并解引用(配置读取唯一入口)。
|
|
1002
|
+
* @author ddj 2026年09月23号
|
|
1003
|
+
* @param config 插件组合配置(apply 收到的 Config 值)
|
|
1004
|
+
* @param key 字段名
|
|
1005
|
+
* @returns 字段值(引用已解包;缺失为 undefined)
|
|
1006
|
+
*/
|
|
1007
|
+
function configField(config, key) {
|
|
1008
|
+
return unref(config?.[key]);
|
|
1009
|
+
}
|
|
532
1010
|
function normalizeValue(value) {
|
|
533
1011
|
if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
|
|
534
1012
|
return value.trim();
|
|
535
1013
|
}
|
|
536
1014
|
function configValue(config) {
|
|
537
|
-
return normalizeValue(config
|
|
1015
|
+
return normalizeValue(configField(config, "fileOpenTool"));
|
|
538
1016
|
}
|
|
539
1017
|
/** 配置/设置里的深链基址(缺省/非法回退默认值)。 */
|
|
540
1018
|
function baseValueOf(config) {
|
|
541
|
-
const raw = config
|
|
1019
|
+
const raw = configField(config, "integrationBaseUrl");
|
|
542
1020
|
return typeof raw === "string" && raw.trim() ? raw.trim() : INTEGRATION_BASE_DEFAULT;
|
|
543
1021
|
}
|
|
544
1022
|
/** 配置/设置里的 svn CLI 覆盖(空 = 从 PATH 解析 'svn')。 */
|
|
545
1023
|
function svnPathValueOf(config) {
|
|
546
|
-
const raw = config
|
|
1024
|
+
const raw = configField(config, "svnPath");
|
|
547
1025
|
return typeof raw === "string" ? raw.trim() : "";
|
|
548
1026
|
}
|
|
549
1027
|
/** 配置/设置里的 TortoiseSVN 目录(空 = 默认安装目录)。 */
|
|
550
1028
|
function tortoiseDirValueOf(config) {
|
|
551
|
-
const raw = config
|
|
1029
|
+
const raw = configField(config, "tortoisePath");
|
|
552
1030
|
return typeof raw === "string" && raw.trim() ? raw.trim() : TORTOISE_DIR_DEFAULT;
|
|
553
1031
|
}
|
|
554
1032
|
/**
|
|
@@ -567,23 +1045,11 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
|
|
|
567
1045
|
deps = await loader();
|
|
568
1046
|
} catch {}
|
|
569
1047
|
if (!deps) return false;
|
|
570
|
-
const strategy = await runSettingsInstall(ctx, ns, deps.z
|
|
571
|
-
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
572
|
-
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
573
|
-
sidebarMinWidth: deps.z.number().default(300),
|
|
574
|
-
maxOpenEditors: deps.z.number().default(10),
|
|
575
|
-
integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
|
|
576
|
-
aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
|
|
577
|
-
aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
|
|
578
|
-
aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
|
|
579
|
-
aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
|
|
580
|
-
svnPath: deps.z.string().default(""),
|
|
581
|
-
tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT)
|
|
582
|
-
}), entry, {
|
|
1048
|
+
const strategy = await runSettingsInstall(ctx, ns, buildSettingsSchema(deps.z), entry, {
|
|
583
1049
|
setSource: (source) => hooks.setSource(source),
|
|
584
1050
|
onChange: hooks.onChange
|
|
585
1051
|
}, loader);
|
|
586
|
-
return strategy === "legacy" || strategy === "service";
|
|
1052
|
+
return strategy === "legacy" || strategy === "service" || strategy === "forms";
|
|
587
1053
|
}
|
|
588
1054
|
/** AI 配置脏值读取(settings 未就绪时回退默认)。 */
|
|
589
1055
|
function aiValueOf(stored) {
|
|
@@ -592,7 +1058,10 @@ function aiValueOf(stored) {
|
|
|
592
1058
|
enabled: raw.aiInline === true,
|
|
593
1059
|
provider: typeof raw.aiProvider === "string" ? raw.aiProvider : AI_CONFIG_DEFAULT.provider,
|
|
594
1060
|
model: typeof raw.aiModel === "string" ? raw.aiModel : AI_CONFIG_DEFAULT.model,
|
|
595
|
-
effort: typeof raw.aiEffort === "string" ? raw.aiEffort : AI_CONFIG_DEFAULT.effort
|
|
1061
|
+
effort: typeof raw.aiEffort === "string" ? raw.aiEffort : AI_CONFIG_DEFAULT.effort,
|
|
1062
|
+
taskProvider: typeof raw.aiTaskProvider === "string" ? raw.aiTaskProvider : AI_CONFIG_DEFAULT.taskProvider,
|
|
1063
|
+
taskModel: typeof raw.aiTaskModel === "string" ? raw.aiTaskModel : AI_CONFIG_DEFAULT.taskModel,
|
|
1064
|
+
taskEffort: typeof raw.aiTaskEffort === "string" ? raw.aiTaskEffort : AI_CONFIG_DEFAULT.taskEffort
|
|
596
1065
|
};
|
|
597
1066
|
}
|
|
598
1067
|
/**
|
|
@@ -612,13 +1081,13 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
612
1081
|
onChange(current);
|
|
613
1082
|
};
|
|
614
1083
|
const syncRevision = () => {
|
|
615
|
-
revision = (provider
|
|
1084
|
+
revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision;
|
|
616
1085
|
};
|
|
617
1086
|
const settingsChange = () => syncRevision();
|
|
618
1087
|
/** AI 配置当前值(settings 未就绪回退默认)。 */
|
|
619
1088
|
let aiCurrent = { ...AI_CONFIG_DEFAULT };
|
|
620
1089
|
const aiSync = () => {
|
|
621
|
-
const stored = (provider
|
|
1090
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
622
1091
|
aiCurrent = stored !== void 0 ? aiValueOf(stored) : aiCurrent;
|
|
623
1092
|
};
|
|
624
1093
|
/** SVN 路径当前值(settings 未就绪回退配置值)。 */
|
|
@@ -634,7 +1103,7 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
634
1103
|
};
|
|
635
1104
|
/** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
|
|
636
1105
|
const storedValue = () => {
|
|
637
|
-
return provider
|
|
1106
|
+
return readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
638
1107
|
};
|
|
639
1108
|
installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
|
|
640
1109
|
fileOpenTool: current,
|
|
@@ -652,8 +1121,11 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
652
1121
|
});
|
|
653
1122
|
ctx.inject?.(["settings"], (settingsCtx) => {
|
|
654
1123
|
provider = settingsCtx.get("settings");
|
|
655
|
-
|
|
656
|
-
|
|
1124
|
+
const stored = storedValue();
|
|
1125
|
+
const section = stored;
|
|
1126
|
+
if (section?.fileOpenTool !== void 0) notify(section.fileOpenTool);
|
|
1127
|
+
aiCurrent = stored !== void 0 ? aiValueOf(stored) : aiCurrent;
|
|
1128
|
+
svnSync(stored);
|
|
657
1129
|
syncRevision();
|
|
658
1130
|
});
|
|
659
1131
|
return {
|
|
@@ -665,25 +1137,15 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
665
1137
|
},
|
|
666
1138
|
update: async (value, expectedRevision) => {
|
|
667
1139
|
const next = normalizeValue(value);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
|
|
673
|
-
const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
|
|
1140
|
+
const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
|
|
1141
|
+
if (!result.ok) log.warn("fileOpenTool 设置写入失败:" + (result.error ?? "未知原因"));
|
|
1142
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
674
1143
|
notify(stored?.fileOpenTool ?? next);
|
|
675
1144
|
syncRevision();
|
|
676
1145
|
},
|
|
677
1146
|
ai: () => aiCurrent,
|
|
678
1147
|
svn: () => svnCurrent,
|
|
679
1148
|
aiUpdate: async (patch, expectedRevision) => {
|
|
680
|
-
if (!provider?.update) {
|
|
681
|
-
if (patch.enabled !== void 0) aiCurrent.enabled = patch.enabled;
|
|
682
|
-
if (patch.provider !== void 0) aiCurrent.provider = patch.provider;
|
|
683
|
-
if (patch.model !== void 0) aiCurrent.model = patch.model;
|
|
684
|
-
if (patch.effort !== void 0) aiCurrent.effort = patch.effort;
|
|
685
|
-
return aiCurrent;
|
|
686
|
-
}
|
|
687
1149
|
const stored = { ...aiCurrent };
|
|
688
1150
|
const body = {};
|
|
689
1151
|
if (patch.enabled !== void 0) {
|
|
@@ -702,7 +1164,22 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
702
1164
|
stored.effort = patch.effort;
|
|
703
1165
|
body.aiEffort = patch.effort;
|
|
704
1166
|
}
|
|
705
|
-
|
|
1167
|
+
if (patch.taskProvider !== void 0) {
|
|
1168
|
+
stored.taskProvider = patch.taskProvider;
|
|
1169
|
+
body.aiTaskProvider = patch.taskProvider;
|
|
1170
|
+
}
|
|
1171
|
+
if (patch.taskModel !== void 0) {
|
|
1172
|
+
stored.taskModel = patch.taskModel;
|
|
1173
|
+
body.aiTaskModel = patch.taskModel;
|
|
1174
|
+
}
|
|
1175
|
+
if (patch.taskEffort !== void 0) {
|
|
1176
|
+
stored.taskEffort = patch.taskEffort;
|
|
1177
|
+
body.aiTaskEffort = patch.taskEffort;
|
|
1178
|
+
}
|
|
1179
|
+
if (!(await updateSection(provider, "dsh-vscode-mode", body, expectedRevision)).ok) {
|
|
1180
|
+
aiCurrent = stored;
|
|
1181
|
+
return aiCurrent;
|
|
1182
|
+
}
|
|
706
1183
|
aiSync();
|
|
707
1184
|
return aiCurrent;
|
|
708
1185
|
}
|
|
@@ -919,13 +1396,14 @@ function inDshRange(version, range) {
|
|
|
919
1396
|
/**
|
|
920
1397
|
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
921
1398
|
* 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
|
|
922
|
-
* @author ddj 2026年09月02号 / 2026年09月18号
|
|
1399
|
+
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号
|
|
923
1400
|
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
924
1401
|
* @returns 版本线标签
|
|
925
1402
|
*/
|
|
926
1403
|
function familyLabel(input) {
|
|
927
1404
|
const version = parseDshVersion(input);
|
|
928
1405
|
if (!version) return "未知";
|
|
1406
|
+
if (inDshRange(version, { from: "0.1.7-alpha.1" })) return "0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)";
|
|
929
1407
|
if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
|
|
930
1408
|
if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
|
|
931
1409
|
if (inDshRange(version, { from: "0.1.5-alpha.1" })) return "0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)";
|
|
@@ -1864,8 +2342,10 @@ function detectGuards(ctx) {
|
|
|
1864
2342
|
}];
|
|
1865
2343
|
}
|
|
1866
2344
|
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
1867
|
-
const TESTED_DSH_MAX = "0.1.
|
|
1868
|
-
/**
|
|
2345
|
+
const TESTED_DSH_MAX = "0.1.7-alpha.2";
|
|
2346
|
+
/** Config volatile 标记起效的版本线(此前设置走 section 安装,与 volatile 无关)。 */
|
|
2347
|
+
const CONFIG_VOLATILE_MIN = "0.1.7-alpha.1";
|
|
2348
|
+
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略 + Config volatile 标记。 */
|
|
1869
2349
|
function versionAdapters(dshVersion) {
|
|
1870
2350
|
const adapters = [];
|
|
1871
2351
|
const parsed = parseDshVersion(dshVersion);
|
|
@@ -1887,14 +2367,23 @@ function versionAdapters(dshVersion) {
|
|
|
1887
2367
|
const strategy = settingsInstallStrategy();
|
|
1888
2368
|
adapters.push({
|
|
1889
2369
|
name: "设置 section 安装(版本适配)",
|
|
1890
|
-
active: strategy === "legacy" || strategy === "service",
|
|
2370
|
+
active: strategy === "legacy" || strategy === "service" || strategy === "forms",
|
|
1891
2371
|
note: settingsInstallNote()
|
|
1892
2372
|
});
|
|
2373
|
+
const volatileState = configVolatileState();
|
|
2374
|
+
if (volatileState.requested) {
|
|
2375
|
+
const volatileOk = volatileState.total > 0 && volatileState.marked === volatileState.total;
|
|
2376
|
+
adapters.push({
|
|
2377
|
+
name: "Config volatile 字段(0.1.7 设置页)",
|
|
2378
|
+
active: volatileOk,
|
|
2379
|
+
note: volatileOk ? volatileState.marked + "/" + volatileState.total + " 已标记(SettingsForms 可下发本插件 ns)" : volatileState.marked + "/" + volatileState.total + " 已标记:schemastery 依赖过旧无 .volatile(),0.1.7 设置页不可用"
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
1893
2382
|
return adapters;
|
|
1894
2383
|
}
|
|
1895
2384
|
/**
|
|
1896
2385
|
* 构建完整兼容性报告(RPC 与启动日志共用)。
|
|
1897
|
-
* @author ddj 2026年08月24号 / 2026年09月02号
|
|
2386
|
+
* @author ddj 2026年08月24号 / 2026年09月02号 / 2026年09月23号
|
|
1898
2387
|
* @param ctx DSH host 上下文
|
|
1899
2388
|
* @param options 测试注入:depsAvailable 跳过动态导入、version 固定插件版本、dshVersion 固定 DSH 版本
|
|
1900
2389
|
* @returns 兼容性报告
|
|
@@ -1910,7 +2399,10 @@ async function buildReport(ctx, options) {
|
|
|
1910
2399
|
if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
|
|
1911
2400
|
const parsed = parseDshVersion(dshVersion);
|
|
1912
2401
|
const testedMax = parseDshVersion(TESTED_DSH_MAX);
|
|
1913
|
-
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.
|
|
2402
|
+
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.7-alpha.2):设置 API 按能力探测运行,异常时请回报适配矩阵");
|
|
2403
|
+
const volatileState = configVolatileState();
|
|
2404
|
+
const volatileMin = parseDshVersion(CONFIG_VOLATILE_MIN);
|
|
2405
|
+
if (volatileState.requested && volatileState.marked < volatileState.total && parsed && volatileMin && compareDshVersions(parsed, volatileMin) >= 0) warnings.push("插件 Config volatile 标记不完整(" + volatileState.marked + "/" + volatileState.total + "):@deepseek-ai/schemastery 依赖过旧,0.1.7 设置页不可用(重装插件依赖后重启 DSH)");
|
|
1914
2406
|
for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
|
|
1915
2407
|
return {
|
|
1916
2408
|
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
@@ -2000,6 +2492,16 @@ function registerRoutes(ctx, config, handleRpc, onWarning) {
|
|
|
2000
2492
|
input = {};
|
|
2001
2493
|
}
|
|
2002
2494
|
const result = await handleRpc(typeof input.method === "string" ? input.method : "", input.args ?? {});
|
|
2495
|
+
const bin = result?.binary;
|
|
2496
|
+
if (result && result.ok === true && bin?.bytes instanceof Uint8Array) {
|
|
2497
|
+
res.statusCode = 200;
|
|
2498
|
+
res.setHeader("content-type", "application/octet-stream");
|
|
2499
|
+
res.setHeader("x-edrv-mime", String(bin.mime ?? ""));
|
|
2500
|
+
res.setHeader("x-edrv-version", String(bin.version ?? ""));
|
|
2501
|
+
res.setHeader("x-edrv-size", String(bin.size ?? bin.bytes.byteLength));
|
|
2502
|
+
res.end(Buffer.from(bin.bytes));
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2003
2505
|
res.statusCode = 200;
|
|
2004
2506
|
res.setHeader("content-type", "application/json");
|
|
2005
2507
|
res.end(JSON.stringify(result));
|
|
@@ -2302,7 +2804,7 @@ function normalizeRecord(raw) {
|
|
|
2302
2804
|
...Number.isInteger(item.beforeEnd) ? { beforeEnd: item.beforeEnd } : {}
|
|
2303
2805
|
};
|
|
2304
2806
|
}) : [];
|
|
2305
|
-
const toolName = r.toolName === "write"
|
|
2807
|
+
const toolName = r.toolName === "write" || r.toolName === "remote_ssh_write" ? r.toolName : "edit";
|
|
2306
2808
|
const rawDecisions = r.decisions && typeof r.decisions === "object" ? r.decisions : {};
|
|
2307
2809
|
const call = rawDecisions.call === "accepted" || rawDecisions.call === "rejected" ? rawDecisions.call : "pending";
|
|
2308
2810
|
const rawPerHunk = Array.isArray(rawDecisions.perHunk) ? rawDecisions.perHunk : [];
|
|
@@ -2761,6 +3263,149 @@ function bucketOf(registry, ctx, cwd) {
|
|
|
2761
3263
|
});
|
|
2762
3264
|
}
|
|
2763
3265
|
//#endregion
|
|
3266
|
+
//#region src/remoteWorkspace.ts
|
|
3267
|
+
/**
|
|
3268
|
+
* dsh-vscode-mode host — dsh-remote-ssh 远程工作区探测与镜像↔远端路径映射。
|
|
3269
|
+
* 背景(issue #7):dsh-remote-ssh 的「原生远程工作区」cwd 指向本机镜像目录
|
|
3270
|
+
* (如 ~/.dsh/remote-workspaces/<id>,根下含 .remote-ssh.json {profileId, host,
|
|
3271
|
+
* user, remotePath}),agent 的 remote_ssh_write 直写远端;差异审查需要把
|
|
3272
|
+
* 远端参数路径映射回镜像(捕获/读/决策复用本地链路),并把展示路径翻译成远端形态。
|
|
3273
|
+
* 判据与上游 dsh-remote-ssh 的 findMirrorRoot 对齐(host/user/remotePath 必填、
|
|
3274
|
+
* 向上 ≤8 层有界);纯函数为主,node:fs 仅用于标记读取(带 60s TTL 缓存)。
|
|
3275
|
+
* 作者 ddj 2026年09月23号
|
|
3276
|
+
*/
|
|
3277
|
+
/** dsh-remote-ssh 工作区标记文件名。 */
|
|
3278
|
+
const MARKER = ".remote-ssh.json";
|
|
3279
|
+
/** 向上探测的最大层数(含 cwd 自身;与上游 findMirrorRoot 的 8 层有界一致)。 */
|
|
3280
|
+
const MAX_DEPTH = 8;
|
|
3281
|
+
/** 探测结果 TTL(edrv.list 为 5s 轮询,避免每轮都做文件 IO)。 */
|
|
3282
|
+
const TTL_MS = 6e4;
|
|
3283
|
+
/** cwd → { at, ws } 探测缓存(含阴性结果)。 */
|
|
3284
|
+
const cache$2 = /* @__PURE__ */ new Map();
|
|
3285
|
+
/**
|
|
3286
|
+
* 标记内容 → 远程工作区(host/user/remotePath 缺任一字段判非法,与上游一致)。
|
|
3287
|
+
* @author ddj 2026年09月23号
|
|
3288
|
+
* @param root 标记所在目录
|
|
3289
|
+
* @param raw 标记文件文本
|
|
3290
|
+
* @returns 合法返回 RemoteWs;非法返回 null
|
|
3291
|
+
*/
|
|
3292
|
+
function markerWsOf(root, raw) {
|
|
3293
|
+
try {
|
|
3294
|
+
const info = JSON.parse(raw);
|
|
3295
|
+
if (typeof info.host !== "string" || !info.host) return null;
|
|
3296
|
+
if (typeof info.user !== "string" || !info.user) return null;
|
|
3297
|
+
if (typeof info.remotePath !== "string" || !info.remotePath) return null;
|
|
3298
|
+
const ws = {
|
|
3299
|
+
root,
|
|
3300
|
+
host: info.host,
|
|
3301
|
+
user: info.user,
|
|
3302
|
+
remotePath: info.remotePath
|
|
3303
|
+
};
|
|
3304
|
+
if (typeof info.profileId === "string" && info.profileId) ws.profileId = info.profileId;
|
|
3305
|
+
return ws;
|
|
3306
|
+
} catch {
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* 从 cwd 起向上有界探测标记(不走缓存)。
|
|
3312
|
+
* @author ddj 2026年09月23号
|
|
3313
|
+
* @param cwd 会话工作区根
|
|
3314
|
+
* @returns 命中合法标记返回 RemoteWs;否则 null
|
|
3315
|
+
*/
|
|
3316
|
+
function walkRemoteWs(cwd) {
|
|
3317
|
+
let dir = cwd;
|
|
3318
|
+
for (let i = 0; i < MAX_DEPTH; i++) {
|
|
3319
|
+
try {
|
|
3320
|
+
const ws = markerWsOf(dir, readFileSync(join(dir, MARKER), "utf8"));
|
|
3321
|
+
if (ws) return ws;
|
|
3322
|
+
} catch {}
|
|
3323
|
+
const parent = dirname(dir);
|
|
3324
|
+
if (parent === dir) break;
|
|
3325
|
+
dir = parent;
|
|
3326
|
+
}
|
|
3327
|
+
return null;
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* 探测工作区是否为 dsh-remote-ssh 远程工作区(cwd 或向上 ≤8 层找 .remote-ssh.json)。
|
|
3331
|
+
* 结果按 cwd 缓存 60s(含阴性);标记变更后可用 clearWsCache 立即重置。
|
|
3332
|
+
* @author ddj 2026年09月23号
|
|
3333
|
+
* @param cwd 会话工作区根(可空)
|
|
3334
|
+
* @returns 远程工作区信息;非远程或标记非法返回 null
|
|
3335
|
+
*/
|
|
3336
|
+
function findRemoteWs(cwd) {
|
|
3337
|
+
if (!cwd) return null;
|
|
3338
|
+
const now = Date.now();
|
|
3339
|
+
const hit = cache$2.get(cwd);
|
|
3340
|
+
if (hit && now - hit.at < TTL_MS) return hit.ws;
|
|
3341
|
+
const ws = walkRemoteWs(cwd);
|
|
3342
|
+
cache$2.set(cwd, {
|
|
3343
|
+
at: now,
|
|
3344
|
+
ws
|
|
3345
|
+
});
|
|
3346
|
+
return ws;
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* 前缀剥离:path 必须严格位于 root 之下;等于 root 或不匹配返回 null。
|
|
3350
|
+
* @author ddj 2026年09月23号
|
|
3351
|
+
* @param path 待剥离路径(posix 形态)
|
|
3352
|
+
* @param root 前缀(posix 形态、无尾斜杠)
|
|
3353
|
+
* @returns 相对段;不匹配返回 null
|
|
3354
|
+
*/
|
|
3355
|
+
function prefixRel(path, root) {
|
|
3356
|
+
if (path === root || !path.startsWith(root + "/")) return null;
|
|
3357
|
+
return path.slice(root.length + 1);
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* 远端参数路径 → 相对远端工作区目录的相对段。
|
|
3361
|
+
* 支持:相对路径(工具语义即基于远端工作区目录)、绝对路径且 remotePath 为绝对
|
|
3362
|
+
* 形态并为其前缀、`~` 形态且 remotePath 同为 `~` 前缀;Windows 盘符形态按
|
|
3363
|
+
* 非远端语义拒绝。含 `..` 逃逸或空相对段返回 null。
|
|
3364
|
+
* @author ddj 2026年09月23号
|
|
3365
|
+
* @param argPath remote_ssh_write 的 path 参数
|
|
3366
|
+
* @param ws 远程工作区
|
|
3367
|
+
* @returns posix 相对段;不可映射返回 null
|
|
3368
|
+
*/
|
|
3369
|
+
function remoteRelOf(argPath, ws) {
|
|
3370
|
+
const path = argPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3371
|
+
if (!path) return null;
|
|
3372
|
+
const root = ws.remotePath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3373
|
+
if (!root) return null;
|
|
3374
|
+
let rel;
|
|
3375
|
+
if (path.startsWith("/")) rel = root.startsWith("/") ? prefixRel(path, root) : null;
|
|
3376
|
+
else if (path === "~" || path.startsWith("~/")) rel = root.startsWith("~/") ? prefixRel(path, root) : null;
|
|
3377
|
+
else if (/^[A-Za-z]:\//.test(path)) rel = null;
|
|
3378
|
+
else rel = path;
|
|
3379
|
+
if (rel === null || rel.split("/").some((seg) => seg === "..")) return null;
|
|
3380
|
+
return rel;
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* 远端工具参数路径 → 本机镜像绝对路径。
|
|
3384
|
+
* @author ddj 2026年09月23号
|
|
3385
|
+
* @param argPath remote_ssh_write 的 path 参数
|
|
3386
|
+
* @param ws 远程工作区
|
|
3387
|
+
* @returns 镜像内绝对路径;镜像外/前缀形态不一致/逃逸返回 null
|
|
3388
|
+
*/
|
|
3389
|
+
function mirrorTargetOf(argPath, ws) {
|
|
3390
|
+
const rel = remoteRelOf(argPath, ws);
|
|
3391
|
+
return rel === null ? null : join(ws.root, rel);
|
|
3392
|
+
}
|
|
3393
|
+
/**
|
|
3394
|
+
* 镜像内绝对路径 → 远端展示路径(remotePath + 相对段,posix 拼接)。
|
|
3395
|
+
* @author ddj 2026年09月23号
|
|
3396
|
+
* @param mirrorAbs 镜像内绝对路径(通常是记录 path)
|
|
3397
|
+
* @param ws 远程工作区
|
|
3398
|
+
* @returns 远端路径;不在镜像根下返回 null(调用方回落原 path)
|
|
3399
|
+
*/
|
|
3400
|
+
function remoteDisplayOf(mirrorAbs, ws) {
|
|
3401
|
+
const path = mirrorAbs.replace(/\\/g, "/");
|
|
3402
|
+
const root = ws.root.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3403
|
+
if (!root) return null;
|
|
3404
|
+
const rel = prefixRel(path, root);
|
|
3405
|
+
if (rel === null || rel.split("/").some((seg) => seg === "..")) return null;
|
|
3406
|
+
return ws.remotePath.replace(/\/+$/, "") + "/" + rel;
|
|
3407
|
+
}
|
|
3408
|
+
//#endregion
|
|
2764
3409
|
//#region src/tree.ts
|
|
2765
3410
|
/** 目录在文件前、名称按小写字母序的比较器(对齐 VSCode 资源管理器习惯)。 */
|
|
2766
3411
|
function entryOrder(a, b) {
|
|
@@ -3215,6 +3860,16 @@ async function listDirCached(ctx, cwd, rel, force) {
|
|
|
3215
3860
|
}
|
|
3216
3861
|
//#endregion
|
|
3217
3862
|
//#region src/capture.ts
|
|
3863
|
+
/**
|
|
3864
|
+
* dsh-vscode-mode host — 捕获层:监听 tools/result,把 edit/write/remote_ssh_write
|
|
3865
|
+
* 差异落成审查记录。
|
|
3866
|
+
* 迁移自原 src/index.ts 的 tools/result 处理;2026-09-23(issue #7)增补
|
|
3867
|
+
* remote_ssh_write:远端工作区会话里 agent 直写远端的全量覆写——before 取本地
|
|
3868
|
+
* 镜像快照、after 取参数 content,捕获后把同一内容**写穿**回镜像(node:fs 直写,
|
|
3869
|
+
* 刻意不走 ctx.fs,避开 dsh-remote-ssh 写桥的重复推送与补丁次序问题),使镜像 =
|
|
3870
|
+
* 远端最新内容,读/stale/决策口径与远端一致。
|
|
3871
|
+
* 作者 ddj 2026-08-20(2026-09-23 扩展 remote_ssh_write 捕获)
|
|
3872
|
+
*/
|
|
3218
3873
|
/** 校验并提取工具 result meta 中的文件 hunk。 */
|
|
3219
3874
|
function validHunks(raw) {
|
|
3220
3875
|
if (!Array.isArray(raw)) return [];
|
|
@@ -3252,8 +3907,190 @@ function fallbackHunks(toolName, args, before, after, create) {
|
|
|
3252
3907
|
return [];
|
|
3253
3908
|
}
|
|
3254
3909
|
/**
|
|
3255
|
-
*
|
|
3256
|
-
*
|
|
3910
|
+
* 是否为纳入审查的编辑类工具(本地 edit/write + 远端 remote_ssh_write)。
|
|
3911
|
+
* @author ddj 2026年09月23号
|
|
3912
|
+
* @param name 工具名
|
|
3913
|
+
* @returns 纳入审查返回 true
|
|
3914
|
+
*/
|
|
3915
|
+
function isCapturable(name) {
|
|
3916
|
+
return name === "edit" || name === "write" || name === "remote_ssh_write";
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* 本地 edit/write 执行结果 → 审查记录(原 captureToolResult 主体语义不变)。
|
|
3920
|
+
* @author ddj 2026年09月23号
|
|
3921
|
+
* @param exec 工具执行描述
|
|
3922
|
+
* @param args 工具参数
|
|
3923
|
+
* @param result 工具执行结果
|
|
3924
|
+
* @returns 记录;hunk 为空或缺 callId 返回 null
|
|
3925
|
+
*/
|
|
3926
|
+
function buildLocalRecord(exec, args, result) {
|
|
3927
|
+
const value = result.value;
|
|
3928
|
+
const path = value && typeof value.path === "string" ? value.path : args.file_path;
|
|
3929
|
+
if (typeof path !== "string" || !path) return null;
|
|
3930
|
+
const before = value && typeof value.before === "string" ? value.before : null;
|
|
3931
|
+
const after = value && typeof value.after === "string" ? value.after : typeof args.content === "string" ? args.content : null;
|
|
3932
|
+
const create = exec.name === "write" && !!value && value.before === null;
|
|
3933
|
+
const callHunk = exec.name === "edit" && typeof args.old_string === "string" && typeof args.new_string === "string" ? {
|
|
3934
|
+
oldText: args.old_string,
|
|
3935
|
+
newText: args.new_string
|
|
3936
|
+
} : null;
|
|
3937
|
+
const metaHunks = validHunks(result.meta?.diffs);
|
|
3938
|
+
const hunks = annotateHunks(metaHunks.length ? metaHunks : fallbackHunks(String(exec.name), args, before, after, create), before, after);
|
|
3939
|
+
if (!hunks.length || typeof exec.callId !== "string") return null;
|
|
3940
|
+
return {
|
|
3941
|
+
callId: exec.callId,
|
|
3942
|
+
toolName: exec.name === "write" ? "write" : "edit",
|
|
3943
|
+
path,
|
|
3944
|
+
before,
|
|
3945
|
+
after,
|
|
3946
|
+
baseFingerprint: fingerprint(before),
|
|
3947
|
+
afterFingerprint: fingerprint(after),
|
|
3948
|
+
legacy: false,
|
|
3949
|
+
conflict: false,
|
|
3950
|
+
create,
|
|
3951
|
+
callHunk,
|
|
3952
|
+
hunks,
|
|
3953
|
+
decisions: {
|
|
3954
|
+
call: "pending",
|
|
3955
|
+
perHunk: hunks.map(() => "pending")
|
|
3956
|
+
},
|
|
3957
|
+
note: create ? "新建文件:全部拒绝将删除该文件" : before === null ? "未捕获修改前内容(大文件)" : null,
|
|
3958
|
+
superseded: false,
|
|
3959
|
+
archived: false,
|
|
3960
|
+
batch: 0,
|
|
3961
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* 读取镜像文件当前内容(node:fs 直读;缺失 → missing,非文件/过大/读失败 → unreadable)。
|
|
3966
|
+
* @author ddj 2026年09月23号
|
|
3967
|
+
* @param absPath 镜像内绝对路径
|
|
3968
|
+
* @returns 读取状态
|
|
3969
|
+
*/
|
|
3970
|
+
async function readMirrorState(absPath) {
|
|
3971
|
+
let info;
|
|
3972
|
+
try {
|
|
3973
|
+
info = await stat(absPath);
|
|
3974
|
+
} catch {
|
|
3975
|
+
return { kind: "missing" };
|
|
3976
|
+
}
|
|
3977
|
+
if (!info.isFile()) return { kind: "unreadable" };
|
|
3978
|
+
if ((info.size ?? 0) > 8388608) return { kind: "unreadable" };
|
|
3979
|
+
try {
|
|
3980
|
+
return {
|
|
3981
|
+
kind: "content",
|
|
3982
|
+
content: await readFile(absPath, "utf8")
|
|
3983
|
+
};
|
|
3984
|
+
} catch {
|
|
3985
|
+
return { kind: "unreadable" };
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* 把远端写入内容写穿回本地镜像(node:fs 直写 + mkdir -p 父目录)。
|
|
3990
|
+
* 刻意不用 ctx.fs:避免触发 dsh-remote-ssh 写桥把相同内容再推回远端。
|
|
3991
|
+
* @author ddj 2026年09月23号
|
|
3992
|
+
* @param absPath 镜像内绝对路径
|
|
3993
|
+
* @param content 远端写入后的完整内容
|
|
3994
|
+
* @returns 成功返回 null;失败返回错误文案(不抛)
|
|
3995
|
+
*/
|
|
3996
|
+
async function syncMirrorFile(absPath, content) {
|
|
3997
|
+
try {
|
|
3998
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
3999
|
+
await writeFile(absPath, content, "utf8");
|
|
4000
|
+
return null;
|
|
4001
|
+
} catch (error) {
|
|
4002
|
+
return String(error);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
/**
|
|
4006
|
+
* 远端记录的 note 组装(新建/未捕获 before/镜像外路径/写穿失败可叠加)。
|
|
4007
|
+
* @author ddj 2026年09月23号
|
|
4008
|
+
* @param create 镜像中目标不存在(远端新建)
|
|
4009
|
+
* @param before 镜像快照内容(null = 未捕获)
|
|
4010
|
+
* @param mirrorAbs 可映射的镜像绝对路径(null = 路径不在镜像内)
|
|
4011
|
+
* @param syncErr 写穿失败原因(null = 成功)
|
|
4012
|
+
* @returns note 文本;无需提示返回 null
|
|
4013
|
+
*/
|
|
4014
|
+
function remoteNoteOf(create, before, mirrorAbs, syncErr) {
|
|
4015
|
+
const notes = [];
|
|
4016
|
+
if (!mirrorAbs) notes.push("远端路径不在工作区镜像内(仅记录,无法本地审查)");
|
|
4017
|
+
else if (create) notes.push("新建文件:全部拒绝将删除该文件");
|
|
4018
|
+
else if (before === null) notes.push("未捕获修改前内容(大文件)");
|
|
4019
|
+
if (syncErr) notes.push("远端内容写回镜像失败(记录仍已保存):" + syncErr);
|
|
4020
|
+
return notes.length ? notes.join(";") : null;
|
|
4021
|
+
}
|
|
4022
|
+
/**
|
|
4023
|
+
* remote_ssh_write 执行 → 审查记录(含镜像写穿,issue #7)。
|
|
4024
|
+
* 非远程工作区、参数不合法或内容无变化返回 null(不产生记录)。
|
|
4025
|
+
* @author ddj 2026年09月23号
|
|
4026
|
+
* @param cwd 会话工作区(远程会话即镜像根)
|
|
4027
|
+
* @param exec 工具执行描述
|
|
4028
|
+
* @param args 工具参数(path 相对远端工作区目录,或绝对/`~` 形态)
|
|
4029
|
+
* @returns 记录;不满足捕获条件返回 null
|
|
4030
|
+
*/
|
|
4031
|
+
async function buildRemoteRecord(cwd, exec, args) {
|
|
4032
|
+
const ws = findRemoteWs(cwd);
|
|
4033
|
+
if (!ws) return null;
|
|
4034
|
+
const argPath = typeof args.path === "string" ? args.path : "";
|
|
4035
|
+
const after = typeof args.content === "string" ? args.content : null;
|
|
4036
|
+
if (!argPath || after === null || typeof exec.callId !== "string") return null;
|
|
4037
|
+
const mirrorAbs = mirrorTargetOf(argPath, ws);
|
|
4038
|
+
const state = mirrorAbs ? await readMirrorState(mirrorAbs) : { kind: "unreadable" };
|
|
4039
|
+
const before = state.kind === "content" ? state.content : null;
|
|
4040
|
+
const create = state.kind === "missing";
|
|
4041
|
+
if (before !== null && before === after) return null;
|
|
4042
|
+
const syncErr = mirrorAbs ? await syncMirrorFile(mirrorAbs, after) : null;
|
|
4043
|
+
const hunks = annotateHunks([{
|
|
4044
|
+
oldText: before,
|
|
4045
|
+
newText: after
|
|
4046
|
+
}], before, after);
|
|
4047
|
+
return {
|
|
4048
|
+
callId: exec.callId,
|
|
4049
|
+
toolName: "remote_ssh_write",
|
|
4050
|
+
path: mirrorAbs ?? argPath,
|
|
4051
|
+
before,
|
|
4052
|
+
after,
|
|
4053
|
+
baseFingerprint: fingerprint(before),
|
|
4054
|
+
afterFingerprint: fingerprint(after),
|
|
4055
|
+
legacy: false,
|
|
4056
|
+
conflict: false,
|
|
4057
|
+
create,
|
|
4058
|
+
callHunk: null,
|
|
4059
|
+
hunks,
|
|
4060
|
+
decisions: {
|
|
4061
|
+
call: "pending",
|
|
4062
|
+
perHunk: hunks.map(() => "pending")
|
|
4063
|
+
},
|
|
4064
|
+
note: remoteNoteOf(create, before, mirrorAbs, syncErr),
|
|
4065
|
+
superseded: false,
|
|
4066
|
+
archived: false,
|
|
4067
|
+
batch: 0,
|
|
4068
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* 记录入桶:按工作区分桶(缺失先加载)、按文件递增批次、裁剪、落盘。
|
|
4073
|
+
* @author ddj 2026年09月23号
|
|
4074
|
+
* @param ctx DSH 上下文
|
|
4075
|
+
* @param registry 工作区记录桶注册表
|
|
4076
|
+
* @param session 会话
|
|
4077
|
+
* @param cwd 会话工作区
|
|
4078
|
+
* @param record 待入桶记录
|
|
4079
|
+
*/
|
|
4080
|
+
async function persistRecord(ctx, registry, session, cwd, record) {
|
|
4081
|
+
let bucket = registry.get(cwd);
|
|
4082
|
+
if (!bucket) {
|
|
4083
|
+
bucket = await loadBucket(ctx, cwd);
|
|
4084
|
+
registry.set(cwd, bucket);
|
|
4085
|
+
}
|
|
4086
|
+
record.batch = fileMaxBatch(bucket, record.path) + 1;
|
|
4087
|
+
bucket.set(record.callId, record);
|
|
4088
|
+
prune(bucket);
|
|
4089
|
+
await saveBucket(ctx, cwd, bucket, session);
|
|
4090
|
+
}
|
|
4091
|
+
/**
|
|
4092
|
+
* 捕获一次工具执行结果(tools/result):edit/write/remote_ssh_write 成功时
|
|
4093
|
+
* 构造记录、递增文件批次、落盘;远端写入额外做镜像写穿(issue #7)。
|
|
3257
4094
|
* @author ddj 2026年08月20号
|
|
3258
4095
|
* @param ctx DSH 上下文
|
|
3259
4096
|
* @param registry 工作区记录桶注册表
|
|
@@ -3262,58 +4099,17 @@ function fallbackHunks(toolName, args, before, after, create) {
|
|
|
3262
4099
|
*/
|
|
3263
4100
|
async function captureToolResult(ctx, registry, exec, result) {
|
|
3264
4101
|
const session = exec?.agent?.session;
|
|
3265
|
-
if (!session || exec?.name
|
|
3266
|
-
if (result?.isError
|
|
4102
|
+
if (!session || !isCapturable(exec?.name)) return;
|
|
4103
|
+
if (result?.isError) return;
|
|
4104
|
+
if (exec.name !== "remote_ssh_write" && !result?.value) return;
|
|
3267
4105
|
try {
|
|
3268
4106
|
const cwd = cwdOf(session);
|
|
3269
4107
|
if (!cwd) return;
|
|
3270
|
-
const value = result.value;
|
|
3271
4108
|
const args = exec.arguments || {};
|
|
3272
|
-
const
|
|
3273
|
-
if (
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
const create = exec.name === "write" && value.before === null;
|
|
3277
|
-
const callHunk = exec.name === "edit" && typeof args.old_string === "string" && typeof args.new_string === "string" ? {
|
|
3278
|
-
oldText: args.old_string,
|
|
3279
|
-
newText: args.new_string
|
|
3280
|
-
} : null;
|
|
3281
|
-
const metaHunks = validHunks(result.meta?.diffs);
|
|
3282
|
-
const hunks = annotateHunks(metaHunks.length ? metaHunks : fallbackHunks(exec.name, args, before, after, create), before, after);
|
|
3283
|
-
if (!hunks.length || typeof exec.callId !== "string") return;
|
|
3284
|
-
const record = {
|
|
3285
|
-
callId: exec.callId,
|
|
3286
|
-
toolName: exec.name,
|
|
3287
|
-
path,
|
|
3288
|
-
before,
|
|
3289
|
-
after,
|
|
3290
|
-
baseFingerprint: fingerprint(before),
|
|
3291
|
-
afterFingerprint: fingerprint(after),
|
|
3292
|
-
legacy: false,
|
|
3293
|
-
conflict: false,
|
|
3294
|
-
create,
|
|
3295
|
-
callHunk,
|
|
3296
|
-
hunks,
|
|
3297
|
-
decisions: {
|
|
3298
|
-
call: "pending",
|
|
3299
|
-
perHunk: hunks.map(() => "pending")
|
|
3300
|
-
},
|
|
3301
|
-
note: create ? "新建文件:全部拒绝将删除该文件" : before === null ? "未捕获修改前内容(大文件)" : null,
|
|
3302
|
-
superseded: false,
|
|
3303
|
-
archived: false,
|
|
3304
|
-
batch: 0,
|
|
3305
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3306
|
-
};
|
|
3307
|
-
let bucket = registry.get(cwd);
|
|
3308
|
-
if (!bucket) {
|
|
3309
|
-
bucket = await loadBucket(ctx, cwd);
|
|
3310
|
-
registry.set(cwd, bucket);
|
|
3311
|
-
}
|
|
3312
|
-
record.batch = fileMaxBatch(bucket, path) + 1;
|
|
3313
|
-
bucket.set(exec.callId, record);
|
|
3314
|
-
prune(bucket);
|
|
3315
|
-
await saveBucket(ctx, cwd, bucket, session);
|
|
3316
|
-
invalidateIndex(ctx, cwd, path);
|
|
4109
|
+
const record = exec.name === "remote_ssh_write" ? await buildRemoteRecord(cwd, exec, args) : buildLocalRecord(exec, args, result);
|
|
4110
|
+
if (!record) return;
|
|
4111
|
+
await persistRecord(ctx, registry, session, cwd, record);
|
|
4112
|
+
invalidateIndex(ctx, cwd, record.path);
|
|
3317
4113
|
} catch (error) {
|
|
3318
4114
|
log.error("capture failed: " + String(error));
|
|
3319
4115
|
}
|
|
@@ -7104,28 +7900,86 @@ async function scanSessionInventory(home = dshHome(), archive = sessionsArchiveR
|
|
|
7104
7900
|
}
|
|
7105
7901
|
};
|
|
7106
7902
|
}
|
|
7107
|
-
/**
|
|
7108
|
-
function
|
|
7109
|
-
const raw = new Set(
|
|
7903
|
+
/** id 双向匹配器:原始 id 与编码段名任一命中(兼容编码差异)。 */
|
|
7904
|
+
function idMatcher(ids) {
|
|
7905
|
+
const raw = new Set(ids);
|
|
7110
7906
|
const encoded = /* @__PURE__ */ new Set();
|
|
7111
7907
|
for (const id of raw) encoded.add(sessionIdSegment(id));
|
|
7112
|
-
|
|
7908
|
+
return (id) => raw.has(id) || encoded.has(id);
|
|
7909
|
+
}
|
|
7910
|
+
/** 标记活跃会话(live id 与目录段名双向匹配,兼容编码差异)。 */
|
|
7911
|
+
function markActiveSessions(sessions, activeIds) {
|
|
7912
|
+
const match = idMatcher(activeIds);
|
|
7913
|
+
for (const s of sessions) if (match(s.sessionId)) s.active = true;
|
|
7914
|
+
}
|
|
7915
|
+
/** 空标志集合(服务/字段缺失的统一降级返回值)。 */
|
|
7916
|
+
function emptyFlagIds() {
|
|
7917
|
+
return {
|
|
7918
|
+
archived: /* @__PURE__ */ new Set(),
|
|
7919
|
+
pinned: /* @__PURE__ */ new Set()
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
/** 任意值 → id 字符串集合(非数组或非字符串元素一律丢弃)。 */
|
|
7923
|
+
function toIdSet(ids) {
|
|
7924
|
+
if (!Array.isArray(ids)) return /* @__PURE__ */ new Set();
|
|
7925
|
+
return new Set(ids.filter((id) => typeof id === "string"));
|
|
7113
7926
|
}
|
|
7114
7927
|
/**
|
|
7115
|
-
*
|
|
7116
|
-
*
|
|
7117
|
-
* @author ddj 2026年09月
|
|
7118
|
-
* @param
|
|
7119
|
-
* @
|
|
7120
|
-
* @returns 待移出清单与释放字节
|
|
7928
|
+
* 一次读取 workspaceRegistry 的官方归档/置顶 id 集合(摊到盘点各行使用)。
|
|
7929
|
+
* 服务缺失、字段缺失或非数组(DSH 0.1.6 及更旧没有)一律视为空集,绝不抛错。
|
|
7930
|
+
* @author ddj 2026年09月22号
|
|
7931
|
+
* @param ctx DSH 上下文
|
|
7932
|
+
* @returns archived/pinned 两组 id 集合(缺失即空集)
|
|
7121
7933
|
*/
|
|
7122
|
-
function
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7934
|
+
function registryFlags(ctx) {
|
|
7935
|
+
try {
|
|
7936
|
+
const registry = ctx.get("workspaceRegistry");
|
|
7937
|
+
if (!registry) return emptyFlagIds();
|
|
7938
|
+
return {
|
|
7939
|
+
archived: toIdSet(registry.archivedSessionIds),
|
|
7940
|
+
pinned: toIdSet(registry.pinnedSessionIds)
|
|
7941
|
+
};
|
|
7942
|
+
} catch (error) {
|
|
7943
|
+
log.debug("registryFlags 跳过(服务缺失或读取失败):" + String(error));
|
|
7944
|
+
return emptyFlagIds();
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7947
|
+
/**
|
|
7948
|
+
* 给盘点行附官方归档/置顶标志(best-effort:仅命中附 true,缺失/未命中不附字段),
|
|
7949
|
+
* 让 PerfSettings 会话行能看到官方侧栏的归档/置顶状态。id 匹配复用 markActiveSessions
|
|
7950
|
+
* 的双向(原始 id / 编码段名)口径;服务/字段缺失不附字段、绝不抛错(旧 host 兼容降级)。
|
|
7951
|
+
* @author ddj 2026年09月22号
|
|
7952
|
+
* @param sessions 盘点会话行(就地附加 archived/pinned)
|
|
7953
|
+
* @param ctx DSH 上下文
|
|
7954
|
+
*/
|
|
7955
|
+
function markOfficialFlags(sessions, ctx) {
|
|
7956
|
+
try {
|
|
7957
|
+
const flags = registryFlags(ctx);
|
|
7958
|
+
const archived = idMatcher(flags.archived);
|
|
7959
|
+
const pinned = idMatcher(flags.pinned);
|
|
7960
|
+
for (const s of sessions) {
|
|
7961
|
+
if (archived(s.sessionId)) s.archived = true;
|
|
7962
|
+
if (pinned(s.sessionId)) s.pinned = true;
|
|
7963
|
+
}
|
|
7964
|
+
} catch (error) {
|
|
7965
|
+
log.debug("markOfficialFlags 跳过(读取失败,保持无标志):" + String(error));
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
/**
|
|
7969
|
+
* 移出规划(纯函数):从盘点中按「显式集合」或「规则(minBytes / olderThanDays)」
|
|
7970
|
+
* 圈选可移出会话。活跃会话一律排除;无显式集合且无规则时不选(防误移)。
|
|
7971
|
+
* @author ddj 2026年09月02号
|
|
7972
|
+
* @param inventory 盘点结果
|
|
7973
|
+
* @param criteria 圈选条件
|
|
7974
|
+
* @returns 待移出清单与释放字节
|
|
7975
|
+
*/
|
|
7976
|
+
function planMoveOut(inventory, criteria) {
|
|
7977
|
+
const items = [];
|
|
7978
|
+
const explicit = Array.isArray(criteria.sessionIds) && criteria.sessionIds.length > 0;
|
|
7979
|
+
const cutoff = criteria.olderThanDays ? Date.now() - criteria.olderThanDays * 24 * 60 * 60 * 1e3 : 0;
|
|
7980
|
+
for (const s of inventory.sessions) {
|
|
7981
|
+
if (s.active) continue;
|
|
7982
|
+
if (criteria.workspaceKey && s.workspaceKey !== criteria.workspaceKey) continue;
|
|
7129
7983
|
if (explicit) {
|
|
7130
7984
|
if (!criteria.sessionIds.includes(s.sessionId)) continue;
|
|
7131
7985
|
} else {
|
|
@@ -7273,6 +8127,27 @@ async function restoreSession(home, archive, workspaceKey, sessionId) {
|
|
|
7273
8127
|
};
|
|
7274
8128
|
}
|
|
7275
8129
|
}
|
|
8130
|
+
/**
|
|
8131
|
+
* 恢复后对齐官方归档标志(best-effort):官方 archive 是 workspaceRegistry 的
|
|
8132
|
+
* archivedSessionIds 持久标志(只藏 UI/拦模型步骤,不搬目录;dsh-workspace 源码注明
|
|
8133
|
+
* unarchiveSession 对未归档与未知 id 均为无写入 no-op、对失踪会话也容忍)。
|
|
8134
|
+
* 插件把目录搬回后若该会话曾被官方归档,官方侧栏会继续隐藏它、0.1.7 的
|
|
8135
|
+
* archived-session-gate 会继续拦它的模型步骤——这里探测 host 的 workspaceRegistry
|
|
8136
|
+
* 服务做一次幂等清除;服务缺失、id 编码差异未命中或调用抛错一律静默跳过
|
|
8137
|
+
* (保持现状,不加猜的行为)。有效期:仅恢复成功后触发一次,不轮询不重试。
|
|
8138
|
+
* @author ddj 2026年09月22号
|
|
8139
|
+
* @param ctx DSH 上下文
|
|
8140
|
+
* @param sessionId 恢复的会话 id(目录段名与原始 id 同形时可命中官方标志)
|
|
8141
|
+
*/
|
|
8142
|
+
async function unarchiveOfficial(ctx, sessionId) {
|
|
8143
|
+
try {
|
|
8144
|
+
const registry = ctx.get("workspaceRegistry");
|
|
8145
|
+
if (!registry || typeof registry.unarchiveSession !== "function") return;
|
|
8146
|
+
await registry.unarchiveSession(sessionId);
|
|
8147
|
+
} catch (error) {
|
|
8148
|
+
log.debug("unarchiveOfficial 跳过(服务缺失或调用失败,保持现状):" + String(error));
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
7276
8151
|
/** 清除归档区早于 N 天的会话(破坏性,仅限归档区)。 */
|
|
7277
8152
|
async function purgeArchive(archive, olderThanDays) {
|
|
7278
8153
|
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3;
|
|
@@ -7465,8 +8340,8 @@ function patchInsertPerfConfig(text) {
|
|
|
7465
8340
|
const staleCheckedAt = /* @__PURE__ */ new Map();
|
|
7466
8341
|
/** stale 自动清理最小间隔。 */
|
|
7467
8342
|
const STALE_CHECK_MIN_MS = 1e4;
|
|
7468
|
-
/** 记录 → 客户端视图(不含 before
|
|
7469
|
-
function recView(record) {
|
|
8343
|
+
/** 记录 → 客户端视图(不含 before 全文,仅长度);远程工作区附远端展示路径(issue #7)。 */
|
|
8344
|
+
function recView(record, ws = null) {
|
|
7470
8345
|
return {
|
|
7471
8346
|
callId: record.callId,
|
|
7472
8347
|
toolName: record.toolName,
|
|
@@ -7483,7 +8358,8 @@ function recView(record) {
|
|
|
7483
8358
|
afterFingerprint: record.afterFingerprint ?? null,
|
|
7484
8359
|
conflict: record.conflict === true,
|
|
7485
8360
|
legacy: record.legacy === true,
|
|
7486
|
-
at: record.at
|
|
8361
|
+
at: record.at,
|
|
8362
|
+
...ws ? { displayPath: remoteDisplayOf(record.path, ws) ?? void 0 } : {}
|
|
7487
8363
|
};
|
|
7488
8364
|
}
|
|
7489
8365
|
/** 会话/工作区公共前置:返回 {session,cwd} 或错误文案。 */
|
|
@@ -7508,6 +8384,52 @@ function snippetTargetOf(path) {
|
|
|
7508
8384
|
return isSnippetFilePath(path) ? path.replace(/\\/g, "/") : null;
|
|
7509
8385
|
}
|
|
7510
8386
|
/**
|
|
8387
|
+
* 二进制/文本读取共用前置(edrv.read 与 edrv.readBinary):解析路径 + debugRecord +
|
|
8388
|
+
* stat + 文件类型校验 + 32MB 二进制上限,保证两条读通道的错误口径完全一致。
|
|
8389
|
+
* @author ddj 2026年09月22号
|
|
8390
|
+
* @param ctx DSH 上下文
|
|
8391
|
+
* @param sc requireSession 成功结果(session + cwd)
|
|
8392
|
+
* @param path 客户端请求路径
|
|
8393
|
+
* @returns 成功返回解析目标与 stat;失败返回错误文案(文件不存在时附 resolvedPath)
|
|
8394
|
+
*/
|
|
8395
|
+
async function readTargetOf(ctx, sc, path) {
|
|
8396
|
+
const fs = ctx.get("fs");
|
|
8397
|
+
if (!fs) return { err: "缺少 fs" };
|
|
8398
|
+
try {
|
|
8399
|
+
const target = await resolveTarget(ctx, sc.session, path);
|
|
8400
|
+
debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(path ?? "") + " resolved=" + fs.processPath(target), "debug");
|
|
8401
|
+
const info = await fs.stat(target);
|
|
8402
|
+
if (!info || info.type !== "file") return {
|
|
8403
|
+
err: "文件不存在",
|
|
8404
|
+
resolvedPath: fs.processPath(target)
|
|
8405
|
+
};
|
|
8406
|
+
if ((info.size ?? 0) > 33554432) return { err: "文件过大(>32MB),不支持整文件预览" };
|
|
8407
|
+
return {
|
|
8408
|
+
target,
|
|
8409
|
+
fs,
|
|
8410
|
+
info
|
|
8411
|
+
};
|
|
8412
|
+
} catch (error) {
|
|
8413
|
+
return { err: "读取失败:" + String(error) };
|
|
8414
|
+
}
|
|
8415
|
+
}
|
|
8416
|
+
/**
|
|
8417
|
+
* readTargetOf 失败结果 → RPC 错误载荷(文件不存在时保留 resolvedPath 供诊断)。
|
|
8418
|
+
* @author ddj 2026年09月22号
|
|
8419
|
+
* @param prep readTargetOf 的失败分支
|
|
8420
|
+
* @returns { ok:false } 形态载荷
|
|
8421
|
+
*/
|
|
8422
|
+
function targetErrOf(prep) {
|
|
8423
|
+
return prep.resolvedPath !== void 0 ? {
|
|
8424
|
+
ok: false,
|
|
8425
|
+
error: prep.err,
|
|
8426
|
+
resolvedPath: prep.resolvedPath
|
|
8427
|
+
} : {
|
|
8428
|
+
ok: false,
|
|
8429
|
+
error: prep.err
|
|
8430
|
+
};
|
|
8431
|
+
}
|
|
8432
|
+
/**
|
|
7511
8433
|
* 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
|
|
7512
8434
|
* 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
|
|
7513
8435
|
* @author ddj 2026年09月15号
|
|
@@ -7567,6 +8489,7 @@ async function afterManualSave(ctx, registry, sc, path) {
|
|
|
7567
8489
|
async function applyDecisions(ctx, session, cwd, bucket, items) {
|
|
7568
8490
|
const results = [];
|
|
7569
8491
|
const resolved = [];
|
|
8492
|
+
const ws = findRemoteWs(cwd);
|
|
7570
8493
|
let changed = false;
|
|
7571
8494
|
for (const item of items) {
|
|
7572
8495
|
const record = bucket.get(item.callId);
|
|
@@ -7601,7 +8524,7 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
|
|
|
7601
8524
|
const result = {
|
|
7602
8525
|
callId: item.callId,
|
|
7603
8526
|
ok: true,
|
|
7604
|
-
record: recView(record)
|
|
8527
|
+
record: recView(record, ws)
|
|
7605
8528
|
};
|
|
7606
8529
|
if (revertedStale) result.stale = true;
|
|
7607
8530
|
results.push(result);
|
|
@@ -7633,7 +8556,7 @@ async function latestPatchBackup(patchPath) {
|
|
|
7633
8556
|
}
|
|
7634
8557
|
/** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
|
|
7635
8558
|
async function integrationBaseUrlOf(ctx) {
|
|
7636
|
-
const value = (ctx.get("settings")
|
|
8559
|
+
const value = sectionOf(ctx.get("settings"), FILE_OPEN_SETTINGS_NS)?.value;
|
|
7637
8560
|
return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
|
|
7638
8561
|
}
|
|
7639
8562
|
/**
|
|
@@ -7756,10 +8679,11 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
7756
8679
|
}
|
|
7757
8680
|
}
|
|
7758
8681
|
const out = [];
|
|
8682
|
+
const ws = findRemoteWs(sc.cwd);
|
|
7759
8683
|
for (const rec of bucket.values()) {
|
|
7760
8684
|
if (!want && rec.archived) continue;
|
|
7761
8685
|
if (want && !want.has(rec.callId)) continue;
|
|
7762
|
-
out.push(recView(rec));
|
|
8686
|
+
out.push(recView(rec, ws));
|
|
7763
8687
|
}
|
|
7764
8688
|
out.sort((a, b) => a.at < b.at ? -1 : 1);
|
|
7765
8689
|
return {
|
|
@@ -7846,45 +8770,56 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
7846
8770
|
ok: false,
|
|
7847
8771
|
error: sc.err
|
|
7848
8772
|
};
|
|
7849
|
-
const
|
|
7850
|
-
if (
|
|
7851
|
-
ok: false,
|
|
7852
|
-
error: "缺少 fs"
|
|
7853
|
-
};
|
|
8773
|
+
const prep = await readTargetOf(ctx, sc, args.path);
|
|
8774
|
+
if ("err" in prep) return targetErrOf(prep);
|
|
7854
8775
|
try {
|
|
7855
|
-
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
7856
|
-
debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
|
|
7857
|
-
const info = await fs.stat(target);
|
|
7858
|
-
if (!info || info.type !== "file") return {
|
|
7859
|
-
ok: false,
|
|
7860
|
-
error: "文件不存在",
|
|
7861
|
-
resolvedPath: fs.processPath(target)
|
|
7862
|
-
};
|
|
7863
|
-
if ((info.size ?? 0) > 33554432) return {
|
|
7864
|
-
ok: false,
|
|
7865
|
-
error: "文件过大(>32MB),不支持整文件预览"
|
|
7866
|
-
};
|
|
7867
8776
|
if (args.encoding === "base64") {
|
|
7868
|
-
const bytes = await fs.readBytes(target, void 0, BINARY_READ_CAP);
|
|
8777
|
+
const bytes = await prep.fs.readBytes(prep.target, void 0, BINARY_READ_CAP);
|
|
7869
8778
|
return {
|
|
7870
8779
|
ok: true,
|
|
7871
8780
|
content: Buffer.from(bytes).toString("base64"),
|
|
7872
8781
|
size: bytes.byteLength,
|
|
7873
8782
|
encoding: "base64",
|
|
7874
8783
|
mime: binaryMimeOf(args.path),
|
|
7875
|
-
version: String(info.version ?? "")
|
|
8784
|
+
version: String(prep.info.version ?? "")
|
|
7876
8785
|
};
|
|
7877
8786
|
}
|
|
7878
|
-
if ((info.size ?? 0) > 8388608) return {
|
|
8787
|
+
if ((prep.info.size ?? 0) > 8388608) return {
|
|
7879
8788
|
ok: false,
|
|
7880
8789
|
error: "文件过大(>8MB),不支持整文件预览"
|
|
7881
8790
|
};
|
|
7882
|
-
const content = await fs.readText(target);
|
|
8791
|
+
const content = await prep.fs.readText(prep.target);
|
|
7883
8792
|
return {
|
|
7884
8793
|
ok: true,
|
|
7885
8794
|
content,
|
|
7886
8795
|
size: content.length,
|
|
7887
|
-
version: String(info.version ?? "")
|
|
8796
|
+
version: String(prep.info.version ?? "")
|
|
8797
|
+
};
|
|
8798
|
+
} catch (error) {
|
|
8799
|
+
return {
|
|
8800
|
+
ok: false,
|
|
8801
|
+
error: "读取失败:" + String(error)
|
|
8802
|
+
};
|
|
8803
|
+
}
|
|
8804
|
+
},
|
|
8805
|
+
"edrv.readBinary": async (args) => {
|
|
8806
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8807
|
+
if ("err" in sc) return {
|
|
8808
|
+
ok: false,
|
|
8809
|
+
error: sc.err
|
|
8810
|
+
};
|
|
8811
|
+
const prep = await readTargetOf(ctx, sc, args.path);
|
|
8812
|
+
if ("err" in prep) return targetErrOf(prep);
|
|
8813
|
+
try {
|
|
8814
|
+
const bytes = await prep.fs.readBytes(prep.target, void 0, BINARY_READ_CAP);
|
|
8815
|
+
return {
|
|
8816
|
+
ok: true,
|
|
8817
|
+
binary: {
|
|
8818
|
+
bytes,
|
|
8819
|
+
mime: binaryMimeOf(args.path),
|
|
8820
|
+
size: bytes.byteLength,
|
|
8821
|
+
version: String(prep.info.version ?? "")
|
|
8822
|
+
}
|
|
7888
8823
|
};
|
|
7889
8824
|
} catch (error) {
|
|
7890
8825
|
return {
|
|
@@ -8054,7 +8989,9 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8054
8989
|
ok: false,
|
|
8055
8990
|
error: sc.err
|
|
8056
8991
|
};
|
|
8057
|
-
const
|
|
8992
|
+
const batches = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd);
|
|
8993
|
+
const ws = findRemoteWs(sc.cwd);
|
|
8994
|
+
const entries = batches.map((b) => {
|
|
8058
8995
|
const recs = Array.isArray(b.records) ? b.records : [];
|
|
8059
8996
|
const sum = recs.reduce((s, r) => {
|
|
8060
8997
|
const sm = r.summary || {
|
|
@@ -8078,6 +9015,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8078
9015
|
at: b.at,
|
|
8079
9016
|
lastAt: b.lastAt || b.at,
|
|
8080
9017
|
path: b.path,
|
|
9018
|
+
...ws ? { displayPath: remoteDisplayOf(b.path, ws) ?? void 0 } : {},
|
|
8081
9019
|
batch: b.batch ?? null,
|
|
8082
9020
|
reason: b.reason ?? null,
|
|
8083
9021
|
nRecords: recs.length,
|
|
@@ -8633,39 +9571,32 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8633
9571
|
}
|
|
8634
9572
|
},
|
|
8635
9573
|
"vscode.fileOpenSettingsGet": async () => {
|
|
8636
|
-
const
|
|
8637
|
-
const value =
|
|
9574
|
+
const section = sectionOf(ctx.get("settings"), FILE_OPEN_SETTINGS_NS);
|
|
9575
|
+
const value = section?.value;
|
|
8638
9576
|
return {
|
|
8639
9577
|
ok: true,
|
|
8640
9578
|
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? "auto"),
|
|
8641
9579
|
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
8642
|
-
revision:
|
|
9580
|
+
revision: section?.revision
|
|
8643
9581
|
};
|
|
8644
9582
|
},
|
|
8645
9583
|
"vscode.fileOpenSettingsUpdate": async (args) => {
|
|
8646
9584
|
const settings = ctx.get("settings");
|
|
8647
|
-
|
|
9585
|
+
const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
|
|
9586
|
+
if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
|
|
9587
|
+
const result = await updateSection(settings, FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
|
|
9588
|
+
if (!result.ok) return {
|
|
8648
9589
|
ok: false,
|
|
8649
|
-
error: "设置服务不可用"
|
|
9590
|
+
error: result.error ?? "设置服务不可用"
|
|
9591
|
+
};
|
|
9592
|
+
const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS);
|
|
9593
|
+
const value = section?.value;
|
|
9594
|
+
return {
|
|
9595
|
+
ok: true,
|
|
9596
|
+
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
|
|
9597
|
+
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
9598
|
+
revision: section?.revision
|
|
8650
9599
|
};
|
|
8651
|
-
try {
|
|
8652
|
-
const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
|
|
8653
|
-
if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
|
|
8654
|
-
await settings.update(FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
|
|
8655
|
-
const descriptor = settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
|
|
8656
|
-
const value = descriptor?.value;
|
|
8657
|
-
return {
|
|
8658
|
-
ok: true,
|
|
8659
|
-
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
|
|
8660
|
-
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
8661
|
-
revision: descriptor?.revision
|
|
8662
|
-
};
|
|
8663
|
-
} catch (error) {
|
|
8664
|
-
return {
|
|
8665
|
-
ok: false,
|
|
8666
|
-
error: String(error)
|
|
8667
|
-
};
|
|
8668
|
-
}
|
|
8669
9600
|
},
|
|
8670
9601
|
"compat": async () => ({
|
|
8671
9602
|
ok: true,
|
|
@@ -8829,6 +9760,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8829
9760
|
const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home));
|
|
8830
9761
|
const active = activeSessionIds(ctx);
|
|
8831
9762
|
markActiveSessions(inventory.sessions, active);
|
|
9763
|
+
markOfficialFlags(inventory.sessions, ctx);
|
|
8832
9764
|
return {
|
|
8833
9765
|
ok: true,
|
|
8834
9766
|
...inventory,
|
|
@@ -8897,13 +9829,15 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8897
9829
|
"edrv.perf.restore": async (args) => {
|
|
8898
9830
|
try {
|
|
8899
9831
|
const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId);
|
|
8900
|
-
|
|
8901
|
-
ok: true,
|
|
8902
|
-
restored: true
|
|
8903
|
-
} : {
|
|
9832
|
+
if (!result.ok) return {
|
|
8904
9833
|
ok: false,
|
|
8905
9834
|
error: result.error ?? "恢复失败"
|
|
8906
9835
|
};
|
|
9836
|
+
await unarchiveOfficial(ctx, args.sessionId);
|
|
9837
|
+
return {
|
|
9838
|
+
ok: true,
|
|
9839
|
+
restored: true
|
|
9840
|
+
};
|
|
8907
9841
|
} catch (error) {
|
|
8908
9842
|
return {
|
|
8909
9843
|
ok: false,
|
|
@@ -9616,6 +10550,9 @@ function createLspClient(transport, events = {}) {
|
|
|
9616
10550
|
workspaceSymbol: false,
|
|
9617
10551
|
hover: false,
|
|
9618
10552
|
semanticTokens: false,
|
|
10553
|
+
completion: false,
|
|
10554
|
+
completionResolve: false,
|
|
10555
|
+
signatureHelp: false,
|
|
9619
10556
|
semanticTokenTypes: [],
|
|
9620
10557
|
semanticTokenModifiers: []
|
|
9621
10558
|
};
|
|
@@ -9749,6 +10686,23 @@ function createLspClient(transport, events = {}) {
|
|
|
9749
10686
|
references: { dynamicRegistration: false },
|
|
9750
10687
|
documentSymbol: { dynamicRegistration: false },
|
|
9751
10688
|
hover: { dynamicRegistration: false },
|
|
10689
|
+
completion: {
|
|
10690
|
+
dynamicRegistration: false,
|
|
10691
|
+
contextSupport: true,
|
|
10692
|
+
completionItem: {
|
|
10693
|
+
snippetSupport: true,
|
|
10694
|
+
documentationFormat: ["markdown", "plaintext"],
|
|
10695
|
+
resolveSupport: { properties: [
|
|
10696
|
+
"documentation",
|
|
10697
|
+
"detail",
|
|
10698
|
+
"additionalTextEdits"
|
|
10699
|
+
] }
|
|
10700
|
+
}
|
|
10701
|
+
},
|
|
10702
|
+
signatureHelp: {
|
|
10703
|
+
dynamicRegistration: false,
|
|
10704
|
+
signatureInformation: { documentationFormat: ["markdown", "plaintext"] }
|
|
10705
|
+
},
|
|
9752
10706
|
semanticTokens: {
|
|
9753
10707
|
dynamicRegistration: false,
|
|
9754
10708
|
requests: {
|
|
@@ -9771,6 +10725,9 @@ function createLspClient(transport, events = {}) {
|
|
|
9771
10725
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
9772
10726
|
hover: Boolean(caps.hoverProvider),
|
|
9773
10727
|
semanticTokens: Boolean(caps.semanticTokensProvider),
|
|
10728
|
+
completion: Boolean(caps.completionProvider),
|
|
10729
|
+
completionResolve: Boolean(caps.completionProvider?.resolveProvider),
|
|
10730
|
+
signatureHelp: Boolean(caps.signatureHelpProvider),
|
|
9774
10731
|
semanticTokenTypes: Array.isArray(legend?.tokenTypes) ? legend.tokenTypes.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
9775
10732
|
semanticTokenModifiers: Array.isArray(legend?.tokenModifiers) ? legend.tokenModifiers.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
9776
10733
|
};
|
|
@@ -9972,6 +10929,9 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
9972
10929
|
workspaceSymbol: false,
|
|
9973
10930
|
hover: false,
|
|
9974
10931
|
semanticTokens: false,
|
|
10932
|
+
completion: false,
|
|
10933
|
+
completionResolve: false,
|
|
10934
|
+
signatureHelp: false,
|
|
9975
10935
|
semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
9976
10936
|
semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
9977
10937
|
};
|
|
@@ -10228,6 +11188,68 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
10228
11188
|
if (!result || !result.contents) return null;
|
|
10229
11189
|
return { contents: stringifyHoverContents(result.contents) };
|
|
10230
11190
|
},
|
|
11191
|
+
/**
|
|
11192
|
+
* 查询补全列表(textDocument/completion)。
|
|
11193
|
+
*
|
|
11194
|
+
* 列表阶段**剥离 documentation**:EmmyLua 把文档挂在 resolve 阶段(我方 initialize 已声明
|
|
11195
|
+
* resolveSupport),但并非所有服务器都遵守;每条都带长文档会让单次 RPC 载荷暴涨。
|
|
11196
|
+
* `data` 原样透传,resolve 时由服务器回认该条目。
|
|
11197
|
+
* @author ddj 2026年09月22号
|
|
11198
|
+
* @param path 工作区相对路径
|
|
11199
|
+
* @param line 0-based 行
|
|
11200
|
+
* @param character 0-based 列
|
|
11201
|
+
* @param context LSP CompletionContext(触发字符/触发类型)
|
|
11202
|
+
* @returns 归一化补全列表;不支持或文档未打开时 null
|
|
11203
|
+
*/
|
|
11204
|
+
async completion(path, line, character, context) {
|
|
11205
|
+
const doc = docs.get(path);
|
|
11206
|
+
if (!doc || !await readyWait || !capabilities.completion) return null;
|
|
11207
|
+
const params = {
|
|
11208
|
+
textDocument: { uri: doc.uri },
|
|
11209
|
+
position: {
|
|
11210
|
+
line,
|
|
11211
|
+
character
|
|
11212
|
+
}
|
|
11213
|
+
};
|
|
11214
|
+
if (context) params.context = context;
|
|
11215
|
+
return normCompletion(await ensureClient().request("textDocument/completion", params));
|
|
11216
|
+
},
|
|
11217
|
+
/**
|
|
11218
|
+
* 补全项惰性补全(completionItem/resolve):按 `data` 取回 documentation/detail/additionalTextEdits。
|
|
11219
|
+
*
|
|
11220
|
+
* 必须回传**完整条目**(服务器按 data + label 精确匹配),只发 label/data 可能被服务器忽略。
|
|
11221
|
+
* @author ddj 2026年09月22号
|
|
11222
|
+
* @param path 工作区相对路径
|
|
11223
|
+
* @param item 列表阶段返回的补全项
|
|
11224
|
+
* @returns 补全后的条目;服务器不支持时原样返回
|
|
11225
|
+
*/
|
|
11226
|
+
async resolveCompletion(path, item) {
|
|
11227
|
+
if (!item || typeof item.label !== "string") return null;
|
|
11228
|
+
if (!await readyWait || !capabilities.completionResolve) return item;
|
|
11229
|
+
return normCompItem(await ensureClient().request("completionItem/resolve", toLspCompItem(item))) ?? item;
|
|
11230
|
+
},
|
|
11231
|
+
/**
|
|
11232
|
+
* 查询签名帮助(textDocument/signatureHelp)。
|
|
11233
|
+
*
|
|
11234
|
+
* activeSignature/activeParameter 做边界裁剪:服务器可能给越界值(多签名时按实参推进出错),
|
|
11235
|
+
* Monaco 拿到越界索引会取不到签名而不渲染 —— 裁剪后至少显示首个签名。
|
|
11236
|
+
* @author ddj 2026年09月22号
|
|
11237
|
+
* @param path 工作区相对路径
|
|
11238
|
+
* @param line 0-based 行
|
|
11239
|
+
* @param character 0-based 列
|
|
11240
|
+
* @returns 归一化签名帮助;无签名/不支持时 null
|
|
11241
|
+
*/
|
|
11242
|
+
async signatureHelp(path, line, character) {
|
|
11243
|
+
const doc = docs.get(path);
|
|
11244
|
+
if (!doc || !await readyWait || !capabilities.signatureHelp) return null;
|
|
11245
|
+
return toSignatureHelp(await ensureClient().request("textDocument/signatureHelp", {
|
|
11246
|
+
textDocument: { uri: doc.uri },
|
|
11247
|
+
position: {
|
|
11248
|
+
line,
|
|
11249
|
+
character
|
|
11250
|
+
}
|
|
11251
|
+
}));
|
|
11252
|
+
},
|
|
10231
11253
|
/** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
|
|
10232
11254
|
async semanticTokens(path) {
|
|
10233
11255
|
const doc = docs.get(path);
|
|
@@ -10333,8 +11355,205 @@ function normalizeSymbol(item) {
|
|
|
10333
11355
|
}
|
|
10334
11356
|
return symbol;
|
|
10335
11357
|
}
|
|
10336
|
-
/** hover
|
|
10337
|
-
function
|
|
11358
|
+
/** hover/markdown 内容 → 纯文本(string | MarkupContent | MarkedString[] | MarkedString)。 */
|
|
11359
|
+
function plainText(value) {
|
|
11360
|
+
if (typeof value === "string") return value || void 0;
|
|
11361
|
+
if (Array.isArray(value)) {
|
|
11362
|
+
const parts = value.map(plainText).filter((part) => Boolean(part));
|
|
11363
|
+
return parts.length ? parts.join("\n\n") : void 0;
|
|
11364
|
+
}
|
|
11365
|
+
if (value && typeof value === "object") {
|
|
11366
|
+
const text = value.value;
|
|
11367
|
+
if (typeof text === "string") return text || void 0;
|
|
11368
|
+
}
|
|
11369
|
+
}
|
|
11370
|
+
/**
|
|
11371
|
+
* 归一化补全文本编辑范围(单 range 与 insert/replace 双 range 两种形态)。
|
|
11372
|
+
*
|
|
11373
|
+
* 两种形态必须都认:只认一种会让采用另一种形态的服务器补全落点错位(替换范围算错,
|
|
11374
|
+
* 已输入的前缀被重复插入)。双 range 时 insert 用于「保留前缀插入」,replace 用于覆盖。
|
|
11375
|
+
* @author ddj 2026年09月22号
|
|
11376
|
+
* @param raw 原始 textEdit
|
|
11377
|
+
* @returns 归一化文本编辑;无有效范围或文本时 null
|
|
11378
|
+
*/
|
|
11379
|
+
function normalizeTextEdit(raw) {
|
|
11380
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11381
|
+
const obj = raw;
|
|
11382
|
+
const newText = typeof obj.newText === "string" ? obj.newText : "";
|
|
11383
|
+
if (!newText) return null;
|
|
11384
|
+
const single = normalizeRange(obj.range);
|
|
11385
|
+
if (single) return {
|
|
11386
|
+
range: single,
|
|
11387
|
+
newText
|
|
11388
|
+
};
|
|
11389
|
+
const insert = normalizeRange(obj.insert);
|
|
11390
|
+
const replace = normalizeRange(obj.replace);
|
|
11391
|
+
if (!insert && !replace) return null;
|
|
11392
|
+
return {
|
|
11393
|
+
insert: insert ?? replace,
|
|
11394
|
+
replace: replace ?? insert,
|
|
11395
|
+
newText
|
|
11396
|
+
};
|
|
11397
|
+
}
|
|
11398
|
+
/** 归一化 LSP Range(缺字段返回 null,避免产出畸形范围)。 */
|
|
11399
|
+
function normalizeRange(raw) {
|
|
11400
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11401
|
+
const r = raw;
|
|
11402
|
+
if (!r.start || !r.end) return null;
|
|
11403
|
+
return {
|
|
11404
|
+
start: {
|
|
11405
|
+
line: toInt(r.start.line),
|
|
11406
|
+
character: toInt(r.start.character)
|
|
11407
|
+
},
|
|
11408
|
+
end: {
|
|
11409
|
+
line: toInt(r.end.line),
|
|
11410
|
+
character: toInt(r.end.character)
|
|
11411
|
+
}
|
|
11412
|
+
};
|
|
11413
|
+
}
|
|
11414
|
+
/**
|
|
11415
|
+
* 归一化单个补全项。
|
|
11416
|
+
* @author ddj 2026年09月22号
|
|
11417
|
+
* @param raw 原始条目
|
|
11418
|
+
* @returns 归一化条目;缺 label 时 null
|
|
11419
|
+
*/
|
|
11420
|
+
function normCompItem(raw) {
|
|
11421
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11422
|
+
const obj = raw;
|
|
11423
|
+
const label = typeof obj.label === "string" ? obj.label : "";
|
|
11424
|
+
if (!label) return null;
|
|
11425
|
+
const item = { label };
|
|
11426
|
+
if (typeof obj.kind === "number") item.kind = toInt(obj.kind);
|
|
11427
|
+
if (typeof obj.detail === "string" && obj.detail) item.detail = obj.detail;
|
|
11428
|
+
const documentation = plainText(obj.documentation);
|
|
11429
|
+
if (documentation) item.documentation = documentation;
|
|
11430
|
+
if (typeof obj.insertText === "string") item.insertText = obj.insertText;
|
|
11431
|
+
if (typeof obj.insertTextFormat === "number") item.insertTextFormat = toInt(obj.insertTextFormat);
|
|
11432
|
+
const textEdit = normalizeTextEdit(obj.textEdit);
|
|
11433
|
+
if (textEdit) item.textEdit = textEdit;
|
|
11434
|
+
if (obj.data !== void 0) item.data = obj.data;
|
|
11435
|
+
if (typeof obj.sortText === "string") item.sortText = obj.sortText;
|
|
11436
|
+
if (typeof obj.filterText === "string") item.filterText = obj.filterText;
|
|
11437
|
+
if (obj.preselect === true) item.preselect = true;
|
|
11438
|
+
if (Array.isArray(obj.commitCharacters)) {
|
|
11439
|
+
const chars = obj.commitCharacters.filter((c) => typeof c === "string");
|
|
11440
|
+
if (chars.length) item.commitCharacters = chars;
|
|
11441
|
+
}
|
|
11442
|
+
const tags = Array.isArray(obj.tags) ? obj.tags : [];
|
|
11443
|
+
if (obj.deprecated === true || tags.some((tag) => toInt(tag) === 1)) item.deprecated = true;
|
|
11444
|
+
if (Array.isArray(obj.additionalTextEdits)) {
|
|
11445
|
+
const edits = obj.additionalTextEdits.map((edit) => {
|
|
11446
|
+
const range = normalizeRange(edit?.range);
|
|
11447
|
+
const text = edit?.newText;
|
|
11448
|
+
if (!range || typeof text !== "string") return null;
|
|
11449
|
+
return {
|
|
11450
|
+
range,
|
|
11451
|
+
newText: text
|
|
11452
|
+
};
|
|
11453
|
+
}).filter((edit) => edit !== null);
|
|
11454
|
+
if (edits.length) item.additionalTextEdits = edits;
|
|
11455
|
+
}
|
|
11456
|
+
return item;
|
|
11457
|
+
}
|
|
11458
|
+
/**
|
|
11459
|
+
* 归一化补全结果(CompletionList | CompletionItem[])。
|
|
11460
|
+
*
|
|
11461
|
+
* 两种顶层形态都要认:`{ isIncomplete, items }` 与裸数组。裸数组是最常见形态,
|
|
11462
|
+
* 只认 CompletionList 会让「补全列表恒为空」且无报错。
|
|
11463
|
+
* @author ddj 2026年09月22号
|
|
11464
|
+
* @param result 原始响应
|
|
11465
|
+
* @returns 归一化列表;无效时 null
|
|
11466
|
+
*/
|
|
11467
|
+
function normCompletion(result) {
|
|
11468
|
+
if (result == null) return null;
|
|
11469
|
+
const list = Array.isArray(result) ? {
|
|
11470
|
+
items: result,
|
|
11471
|
+
isIncomplete: false
|
|
11472
|
+
} : result;
|
|
11473
|
+
if (!Array.isArray(list.items)) return null;
|
|
11474
|
+
return {
|
|
11475
|
+
items: list.items.map(normCompItem).filter((item) => item !== null),
|
|
11476
|
+
incomplete: list.isIncomplete === true
|
|
11477
|
+
};
|
|
11478
|
+
}
|
|
11479
|
+
/**
|
|
11480
|
+
* 补全项 → LSP 载荷(resolve 回传用)。
|
|
11481
|
+
* 只回传协议字段,剔除 client 侧衍生字段;`data` 必须原样带上,服务器靠它认条目。
|
|
11482
|
+
* @author ddj 2026年09月22号
|
|
11483
|
+
* @param item 归一化条目
|
|
11484
|
+
* @returns LSP CompletionItem 载荷
|
|
11485
|
+
*/
|
|
11486
|
+
function toLspCompItem(item) {
|
|
11487
|
+
const out = { label: item.label };
|
|
11488
|
+
if (item.kind !== void 0) out.kind = item.kind;
|
|
11489
|
+
if (item.detail !== void 0) out.detail = item.detail;
|
|
11490
|
+
if (item.documentation !== void 0) out.documentation = item.documentation;
|
|
11491
|
+
if (item.insertText !== void 0) out.insertText = item.insertText;
|
|
11492
|
+
if (item.insertTextFormat !== void 0) out.insertTextFormat = item.insertTextFormat;
|
|
11493
|
+
if (item.data !== void 0) out.data = item.data;
|
|
11494
|
+
if (item.sortText !== void 0) out.sortText = item.sortText;
|
|
11495
|
+
if (item.filterText !== void 0) out.filterText = item.filterText;
|
|
11496
|
+
if (item.textEdit) out.textEdit = item.textEdit.range ? {
|
|
11497
|
+
range: item.textEdit.range,
|
|
11498
|
+
newText: item.textEdit.newText
|
|
11499
|
+
} : item.textEdit;
|
|
11500
|
+
if (item.additionalTextEdits) out.additionalTextEdits = item.additionalTextEdits;
|
|
11501
|
+
if (item.commitCharacters) out.commitCharacters = item.commitCharacters;
|
|
11502
|
+
return out;
|
|
11503
|
+
}
|
|
11504
|
+
/** 归一化单个签名参数(label 支持字符串与 [start, end] 元组,两者原样透传)。 */
|
|
11505
|
+
function toSignatureParam(raw) {
|
|
11506
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11507
|
+
const obj = raw;
|
|
11508
|
+
const tuple = Array.isArray(obj.label) && obj.label.length >= 2 ? [toInt(obj.label[0]), toInt(obj.label[1])] : null;
|
|
11509
|
+
if (typeof obj.label !== "string" && !tuple) return null;
|
|
11510
|
+
const param = { label: typeof obj.label === "string" ? obj.label : tuple };
|
|
11511
|
+
const documentation = plainText(obj.documentation);
|
|
11512
|
+
if (documentation) param.documentation = documentation;
|
|
11513
|
+
return param;
|
|
11514
|
+
}
|
|
11515
|
+
/** 归一化单个签名。 */
|
|
11516
|
+
function toSignature(raw) {
|
|
11517
|
+
if (!raw || typeof raw !== "object") return null;
|
|
11518
|
+
const obj = raw;
|
|
11519
|
+
if (typeof obj.label !== "string") return null;
|
|
11520
|
+
const signature = {
|
|
11521
|
+
label: obj.label,
|
|
11522
|
+
parameters: []
|
|
11523
|
+
};
|
|
11524
|
+
const documentation = plainText(obj.documentation);
|
|
11525
|
+
if (documentation) signature.documentation = documentation;
|
|
11526
|
+
if (Array.isArray(obj.parameters)) signature.parameters = obj.parameters.map(toSignatureParam).filter((param) => param !== null);
|
|
11527
|
+
if (typeof obj.activeParameter === "number") signature.activeParameter = toInt(obj.activeParameter);
|
|
11528
|
+
return signature;
|
|
11529
|
+
}
|
|
11530
|
+
/**
|
|
11531
|
+
* 归一化签名帮助,并裁剪越界的 activeSignature / activeParameter。
|
|
11532
|
+
*
|
|
11533
|
+
* 为什么必须裁:Monaco 用这两个索引直接取签名与参数,越界时取不到签名就整个浮窗不渲染;
|
|
11534
|
+
* 服务器在「实参刚敲下逗号」等边界时刻常给超前一位的索引。裁剪后至少显示首个签名/末个参数,
|
|
11535
|
+
* 观感是「高亮没跟上」,而不是「参数提示整个不见」。
|
|
11536
|
+
* @author ddj 2026年09月22号
|
|
11537
|
+
* @param result 原始响应
|
|
11538
|
+
* @returns 归一化结果;无有效签名时 null
|
|
11539
|
+
*/
|
|
11540
|
+
function toSignatureHelp(result) {
|
|
11541
|
+
if (!result || typeof result !== "object") return null;
|
|
11542
|
+
const obj = result;
|
|
11543
|
+
if (!Array.isArray(obj.signatures)) return null;
|
|
11544
|
+
const signatures = obj.signatures.map(toSignature).filter((signature) => signature !== null);
|
|
11545
|
+
if (!signatures.length) return null;
|
|
11546
|
+
const activeSignature = Math.min(Math.max(0, toInt(obj.activeSignature)), signatures.length - 1);
|
|
11547
|
+
const current = signatures[activeSignature];
|
|
11548
|
+
const rawActive = typeof obj.activeParameter === "number" ? toInt(obj.activeParameter) : current.activeParameter ?? 0;
|
|
11549
|
+
const count = current.parameters.length;
|
|
11550
|
+
return {
|
|
11551
|
+
signatures,
|
|
11552
|
+
activeSignature,
|
|
11553
|
+
activeParameter: count > 0 ? Math.min(Math.max(0, rawActive), count - 1) : 0
|
|
11554
|
+
};
|
|
11555
|
+
}
|
|
11556
|
+
/** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */ function stringifyHoverContents(contents) {
|
|
10338
11557
|
if (typeof contents === "string") return [contents];
|
|
10339
11558
|
if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean);
|
|
10340
11559
|
if (contents && typeof contents === "object") {
|
|
@@ -12243,6 +13462,99 @@ function createLspRpc(deps) {
|
|
|
12243
13462
|
};
|
|
12244
13463
|
}
|
|
12245
13464
|
},
|
|
13465
|
+
"edrv.lsp.completion": async (args) => {
|
|
13466
|
+
const lang = langOfPath(args.path);
|
|
13467
|
+
if (!lang) return {
|
|
13468
|
+
ok: true,
|
|
13469
|
+
completions: void 0
|
|
13470
|
+
};
|
|
13471
|
+
const sc = await rootOf(args.sessionId);
|
|
13472
|
+
if ("err" in sc) return {
|
|
13473
|
+
ok: false,
|
|
13474
|
+
error: sc.err
|
|
13475
|
+
};
|
|
13476
|
+
try {
|
|
13477
|
+
const server = serverOf(sc.root, lang);
|
|
13478
|
+
if (!server) return {
|
|
13479
|
+
ok: true,
|
|
13480
|
+
completions: void 0
|
|
13481
|
+
};
|
|
13482
|
+
const completions = await server.completion(wsPath(sc.root, args.path), args.position.line, args.position.character, args.context);
|
|
13483
|
+
if (!completions) return {
|
|
13484
|
+
ok: true,
|
|
13485
|
+
completions: void 0
|
|
13486
|
+
};
|
|
13487
|
+
return {
|
|
13488
|
+
ok: true,
|
|
13489
|
+
completions: completions.items.length > 500 ? {
|
|
13490
|
+
...completions,
|
|
13491
|
+
items: completions.items.slice(0, 500),
|
|
13492
|
+
truncated: true
|
|
13493
|
+
} : completions
|
|
13494
|
+
};
|
|
13495
|
+
} catch (error) {
|
|
13496
|
+
return {
|
|
13497
|
+
ok: false,
|
|
13498
|
+
error: "LSP 补全查询失败:" + String(error)
|
|
13499
|
+
};
|
|
13500
|
+
}
|
|
13501
|
+
},
|
|
13502
|
+
"edrv.lsp.resolveCompletion": async (args) => {
|
|
13503
|
+
const lang = langOfPath(args.path);
|
|
13504
|
+
if (!lang) return {
|
|
13505
|
+
ok: true,
|
|
13506
|
+
item: void 0
|
|
13507
|
+
};
|
|
13508
|
+
const sc = await rootOf(args.sessionId);
|
|
13509
|
+
if ("err" in sc) return {
|
|
13510
|
+
ok: false,
|
|
13511
|
+
error: sc.err
|
|
13512
|
+
};
|
|
13513
|
+
try {
|
|
13514
|
+
const server = serverOf(sc.root, lang);
|
|
13515
|
+
if (!server) return {
|
|
13516
|
+
ok: true,
|
|
13517
|
+
item: args.item
|
|
13518
|
+
};
|
|
13519
|
+
return {
|
|
13520
|
+
ok: true,
|
|
13521
|
+
item: await server.resolveCompletion(wsPath(sc.root, args.path), args.item) ?? void 0
|
|
13522
|
+
};
|
|
13523
|
+
} catch (error) {
|
|
13524
|
+
return {
|
|
13525
|
+
ok: false,
|
|
13526
|
+
error: "LSP 补全解析失败:" + String(error)
|
|
13527
|
+
};
|
|
13528
|
+
}
|
|
13529
|
+
},
|
|
13530
|
+
"edrv.lsp.signatureHelp": async (args) => {
|
|
13531
|
+
const lang = langOfPath(args.path);
|
|
13532
|
+
if (!lang) return {
|
|
13533
|
+
ok: true,
|
|
13534
|
+
signatureHelp: void 0
|
|
13535
|
+
};
|
|
13536
|
+
const sc = await rootOf(args.sessionId);
|
|
13537
|
+
if ("err" in sc) return {
|
|
13538
|
+
ok: false,
|
|
13539
|
+
error: sc.err
|
|
13540
|
+
};
|
|
13541
|
+
try {
|
|
13542
|
+
const server = serverOf(sc.root, lang);
|
|
13543
|
+
if (!server) return {
|
|
13544
|
+
ok: true,
|
|
13545
|
+
signatureHelp: void 0
|
|
13546
|
+
};
|
|
13547
|
+
return {
|
|
13548
|
+
ok: true,
|
|
13549
|
+
signatureHelp: await server.signatureHelp(wsPath(sc.root, args.path), args.position.line, args.position.character) ?? void 0
|
|
13550
|
+
};
|
|
13551
|
+
} catch (error) {
|
|
13552
|
+
return {
|
|
13553
|
+
ok: false,
|
|
13554
|
+
error: "LSP 签名帮助查询失败:" + String(error)
|
|
13555
|
+
};
|
|
13556
|
+
}
|
|
13557
|
+
},
|
|
12246
13558
|
"edrv.lsp.semanticTokens": async (args) => {
|
|
12247
13559
|
const lang = langOfPath(args.path);
|
|
12248
13560
|
if (!lang) return {
|
|
@@ -12489,6 +13801,29 @@ async function routeOf(llm, cfg) {
|
|
|
12489
13801
|
};
|
|
12490
13802
|
}
|
|
12491
13803
|
/**
|
|
13804
|
+
* AI 任务模型路由(非补全场景如 AI 智能整理):任务配置优先,空则回落补全配置链。
|
|
13805
|
+
* 回落序:taskProvider/taskModel 双非空直用 → routeOf(补全配置 → 自动首个);
|
|
13806
|
+
* effort 同序:taskEffort 非空用之,否则 cfg.effort(空串 = 不携带)。
|
|
13807
|
+
* @author ddj 2026年09月23号
|
|
13808
|
+
* @param llm llm 运行时
|
|
13809
|
+
* @param cfg AI 配置(含可选任务模型字段)
|
|
13810
|
+
* @returns 路由(provider/model/effort);不可路由返回 null
|
|
13811
|
+
*/
|
|
13812
|
+
async function taskRouteOf(llm, cfg) {
|
|
13813
|
+
const effort = cfg.taskEffort || cfg.effort || "";
|
|
13814
|
+
if (cfg.taskProvider && cfg.taskModel) return {
|
|
13815
|
+
provider: cfg.taskProvider,
|
|
13816
|
+
model: cfg.taskModel,
|
|
13817
|
+
effort
|
|
13818
|
+
};
|
|
13819
|
+
const base = await routeOf(llm, cfg);
|
|
13820
|
+
if (!base) return null;
|
|
13821
|
+
return {
|
|
13822
|
+
...base,
|
|
13823
|
+
effort
|
|
13824
|
+
};
|
|
13825
|
+
}
|
|
13826
|
+
/**
|
|
12492
13827
|
* 单次 AI 内联补全。
|
|
12493
13828
|
* @author ddj
|
|
12494
13829
|
* @param deps 服务依赖
|
|
@@ -12774,6 +14109,126 @@ function createFileVersions(ctx) {
|
|
|
12774
14109
|
};
|
|
12775
14110
|
}
|
|
12776
14111
|
//#endregion
|
|
14112
|
+
//#region src/ai/svnTriage.ts
|
|
14113
|
+
/** 方案生成最大输出 token(三段 JSON 上限;再大是幻觉温床)。 */
|
|
14114
|
+
const AI_PLAN_MAX_TOKENS = 8192;
|
|
14115
|
+
/** 系统指令:输出唯一 JSON + 三段判据 + 硬约束(不虚构路径/一路径一类)。 */
|
|
14116
|
+
const AI_PLAN_SYSTEM = [
|
|
14117
|
+
"你是 SVN 变更整理助手。根据给出的工作副本变更清单(可能附 diff),产出整理方案,",
|
|
14118
|
+
"输出唯一 JSON 对象,除 JSON 外不得输出任何文本(不要围栏、不要解释):",
|
|
14119
|
+
"{",
|
|
14120
|
+
" \"groups\": [{ \"name\": \"英文短名\", \"paths\": [\"路径1\"], \"reason\": \"分组理由\" }],",
|
|
14121
|
+
" \"reverts\": [{ \"path\": \"路径\", \"reason\": \"还原理由\" }],",
|
|
14122
|
+
" \"ignores\": [{ \"path\": \"路径\", \"reason\": \"忽略理由\" }]",
|
|
14123
|
+
"}",
|
|
14124
|
+
"规则:",
|
|
14125
|
+
"1. 只可使用清单中出现的路径,绝不虚构或改写路径。",
|
|
14126
|
+
"2. 每个路径至多归入一段(groups/reverts/ignores 互斥),拿不准的路径三段都不列。",
|
|
14127
|
+
"3. groups:按功能主题把待提交改动分入 changelist 组;name 用简短英文(不以 - 开头),",
|
|
14128
|
+
" 同一主题合并为一组,组数尽量少。",
|
|
14129
|
+
"4. reverts:仅针对受版本控制的改动建议还原——行尾/空白噪音、误改、试验性改动等。",
|
|
14130
|
+
"5. ignores:仅针对未版本化的条目建议忽略——构建产物、缓存、日志、临时文件等生成物。",
|
|
14131
|
+
"6. reason 用简短中文一句话说明判定依据。"
|
|
14132
|
+
].join("\n");
|
|
14133
|
+
/**
|
|
14134
|
+
* 构造分析用户段:变更清单行(status | path | changelist)+ 可选 diff 上下文。
|
|
14135
|
+
* @author ddj 2026年09月23号
|
|
14136
|
+
* @param entries 变更清单(调用方已按 AI_PLAN_PATHS_CAP 截断)
|
|
14137
|
+
* @param diffText diff 文本(null = paths-only 降级)
|
|
14138
|
+
* @param truncated 清单是否已截断(prompt 注明,防 AI 误以为是全集)
|
|
14139
|
+
* @returns 用户消息文本
|
|
14140
|
+
*/
|
|
14141
|
+
function buildAiPrompt(entries, diffText, truncated = false) {
|
|
14142
|
+
const lines = entries.map((entry) => entry.status + " | " + entry.path + (entry.changelist ? " | " + entry.changelist : ""));
|
|
14143
|
+
let text = (truncated ? "变更清单(已截断,仅分析所列条目;status | path | changelist):" : "变更清单(status | path | changelist):") + "\n" + lines.join("\n");
|
|
14144
|
+
if (diffText) text += "\n\n变更 diff(unified,0 上下文行):\n" + diffText;
|
|
14145
|
+
else text += "\n\n(无 diff 上下文,仅按路径与状态判断)";
|
|
14146
|
+
return text;
|
|
14147
|
+
}
|
|
14148
|
+
/**
|
|
14149
|
+
* 解析 AI 输出 JSON:剥 ``` 围栏 → 取首个 `{` 至末个 `}` → JSON.parse。
|
|
14150
|
+
* 失败抛带原文摘要的错误(调用方整体报错零执行,不静默降级)。
|
|
14151
|
+
* @author ddj 2026年09月23号
|
|
14152
|
+
* @param text 模型输出原文
|
|
14153
|
+
* @returns 解析出的对象(形状由 normalizeAiPlan 容错)
|
|
14154
|
+
*/
|
|
14155
|
+
function parseAiPlanJson(text) {
|
|
14156
|
+
const raw = String(text ?? "");
|
|
14157
|
+
const start = raw.indexOf("{");
|
|
14158
|
+
const end = raw.lastIndexOf("}");
|
|
14159
|
+
if (start < 0 || end <= start) throw new Error("AI 输出不含 JSON 对象:" + raw.slice(0, 200));
|
|
14160
|
+
const body = raw.slice(start, end + 1);
|
|
14161
|
+
try {
|
|
14162
|
+
return JSON.parse(body);
|
|
14163
|
+
} catch (error) {
|
|
14164
|
+
throw new Error("AI 输出无法解析为 JSON:" + String(error) + "(原文摘要:" + body.slice(0, 200) + ")");
|
|
14165
|
+
}
|
|
14166
|
+
}
|
|
14167
|
+
/**
|
|
14168
|
+
* 一次 AI 智能整理分析(一次性非会话流式调用)。
|
|
14169
|
+
* llm 缺失/不可路由/流失败/解析失败一律抛错,由 handler 转 `{ok:false, error}`(零执行)。
|
|
14170
|
+
* @author ddj 2026年09月23号
|
|
14171
|
+
* @param ctx DSH 上下文(取 llm)
|
|
14172
|
+
* @param cfg AI 配置(任务模型路由优先,回落补全配置)
|
|
14173
|
+
* @param prompt 用户段(buildAiPrompt 产物)
|
|
14174
|
+
* @returns 原始方案对象与模型标识
|
|
14175
|
+
*/
|
|
14176
|
+
async function svnAiPlanOf(ctx, cfg, prompt) {
|
|
14177
|
+
const llm = llmOf(ctx);
|
|
14178
|
+
if (!llm) throw new Error("llm 服务不可用(请检查 DSH 模型配置)");
|
|
14179
|
+
const route = await taskRouteOf(llm, cfg);
|
|
14180
|
+
if (!route) throw new Error("未找到可用模型(请检查 DSH 模型配置)");
|
|
14181
|
+
const controller = new AbortController();
|
|
14182
|
+
let timedOut = false;
|
|
14183
|
+
const timer = setTimeout(() => {
|
|
14184
|
+
timedOut = true;
|
|
14185
|
+
controller.abort(/* @__PURE__ */ new Error("timeout"));
|
|
14186
|
+
}, AI_PLAN_TIMEOUT_MS);
|
|
14187
|
+
try {
|
|
14188
|
+
const stream = llm.stream({
|
|
14189
|
+
provider: route.provider,
|
|
14190
|
+
model: route.model,
|
|
14191
|
+
...route.effort ? { reasoningEffort: route.effort } : {},
|
|
14192
|
+
messages: [{
|
|
14193
|
+
role: "user",
|
|
14194
|
+
content: [{
|
|
14195
|
+
type: "text",
|
|
14196
|
+
text: prompt
|
|
14197
|
+
}],
|
|
14198
|
+
source: { kind: "user" }
|
|
14199
|
+
}],
|
|
14200
|
+
system: AI_PLAN_SYSTEM,
|
|
14201
|
+
temperature: .2,
|
|
14202
|
+
maxTokens: AI_PLAN_MAX_TOKENS,
|
|
14203
|
+
signal: controller.signal
|
|
14204
|
+
});
|
|
14205
|
+
let text = "";
|
|
14206
|
+
let failNote = "";
|
|
14207
|
+
for await (const chunk of stream) {
|
|
14208
|
+
const c = chunk;
|
|
14209
|
+
if (c.type === "text-delta") text += c.text;
|
|
14210
|
+
else if (c.type === "finish") {
|
|
14211
|
+
const kind = c.reason?.kind;
|
|
14212
|
+
if (kind === "error" || kind === "aborted") {
|
|
14213
|
+
const failure = c.reason.failure;
|
|
14214
|
+
failNote = "模型流失败 " + (failure?.code ?? kind) + ": " + (failure?.message ?? "模型调用失败");
|
|
14215
|
+
}
|
|
14216
|
+
break;
|
|
14217
|
+
}
|
|
14218
|
+
}
|
|
14219
|
+
if (failNote) throw new Error(failNote);
|
|
14220
|
+
return {
|
|
14221
|
+
raw: parseAiPlanJson(text),
|
|
14222
|
+
model: route.provider + "/" + route.model
|
|
14223
|
+
};
|
|
14224
|
+
} catch (error) {
|
|
14225
|
+
if (timedOut) throw new Error("AI 分析超时(" + Math.round(AI_PLAN_TIMEOUT_MS / 1e3) + "s):可在设置页调低任务模型思考强度或换更快模型");
|
|
14226
|
+
throw error;
|
|
14227
|
+
} finally {
|
|
14228
|
+
clearTimeout(timer);
|
|
14229
|
+
}
|
|
14230
|
+
}
|
|
14231
|
+
//#endregion
|
|
12777
14232
|
//#region src/svnXml.ts
|
|
12778
14233
|
/**
|
|
12779
14234
|
* dsh-vscode-mode host — SVN XML 解析基元(零第三方依赖,可单测)。
|
|
@@ -13338,6 +14793,22 @@ async function findTortoiseProc(dirSetting, exists = defaultExists, platform = p
|
|
|
13338
14793
|
return null;
|
|
13339
14794
|
}
|
|
13340
14795
|
/**
|
|
14796
|
+
* svn:ignore 名称合法性校验(host 权威;防换行注入 propset 值与路径穿越)。
|
|
14797
|
+
* 规则:去空白后非空、无换行、无路径分隔符、不以 `-` 开头(防被 svn 当选项解析)。
|
|
14798
|
+
* @author ddj 2026年09月23号
|
|
14799
|
+
* @param name 待写入的忽略名(basename / 通配模式)
|
|
14800
|
+
* @returns 错误文案;合法为 null
|
|
14801
|
+
*/
|
|
14802
|
+
function ignoreNameErrorOf(name) {
|
|
14803
|
+
if (typeof name !== "string") return "忽略名必须是字符串";
|
|
14804
|
+
const trimmed = name.trim();
|
|
14805
|
+
if (!trimmed) return "忽略名不能为空";
|
|
14806
|
+
if (/[\r\n]/.test(trimmed)) return "忽略名不能包含换行";
|
|
14807
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) return "忽略名不能包含路径分隔符";
|
|
14808
|
+
if (trimmed.startsWith("-")) return "忽略名不能以 - 开头";
|
|
14809
|
+
return null;
|
|
14810
|
+
}
|
|
14811
|
+
/**
|
|
13341
14812
|
* 本插件已自研的 SVN 能力(「自研替换时间线」的推进点)。
|
|
13342
14813
|
*
|
|
13343
14814
|
* 每完成一个阶段就把对应能力加进来:client 会据此隐藏被覆盖的 TortoiseProc 项,
|
|
@@ -13642,6 +15113,8 @@ function createSvnRpc(deps) {
|
|
|
13642
15113
|
const statusCache = /* @__PURE__ */ new Map();
|
|
13643
15114
|
/** 变更清单缓存(键 = 工作副本根;TTL 短,仅合并同一轮 UI 的多处拉取)。 */
|
|
13644
15115
|
const changesCache = /* @__PURE__ */ new Map();
|
|
15116
|
+
/** 混合通道方案收件箱(key = 工作副本根;覆盖式入箱 + TTL,见 svn.aiPlanSubmit/Pending)。 */
|
|
15117
|
+
const planInbox = /* @__PURE__ */ new Map();
|
|
13645
15118
|
/** 工作副本根 relative-url 缓存(日志路径映射用;探测失败也缓存空串避免反复 spawn)。 */
|
|
13646
15119
|
const urlCache = /* @__PURE__ */ new Map();
|
|
13647
15120
|
let cliProbe = null;
|
|
@@ -14071,6 +15544,63 @@ function createSvnRpc(deps) {
|
|
|
14071
15544
|
};
|
|
14072
15545
|
return merged.binary || merged.encodingHint ? merged : {};
|
|
14073
15546
|
};
|
|
15547
|
+
/**
|
|
15548
|
+
* svn:ignore 写入执行(AI 整理执行段③):逐目录 `propget` 读既有值 → 合并去重 → `propset`。
|
|
15549
|
+
* 不整体覆盖(保留团队既有 ignore);逐目录失败收集不中断,全部失败才 ok:false。
|
|
15550
|
+
* @author ddj 2026年09月23号
|
|
15551
|
+
* @param wcRoot 工作副本根
|
|
15552
|
+
* @param items 目录聚合写入项(dir 已归一,根为 '.')
|
|
15553
|
+
* @returns 新增计数/摘要/原文
|
|
15554
|
+
*/
|
|
15555
|
+
const ignoreApplyOf = async (wcRoot, items) => {
|
|
15556
|
+
let count = 0;
|
|
15557
|
+
const notes = [];
|
|
15558
|
+
const failures = [];
|
|
15559
|
+
for (const item of items) {
|
|
15560
|
+
const getOutcome = await runSvn(wcRoot, [
|
|
15561
|
+
"propget",
|
|
15562
|
+
"svn:ignore",
|
|
15563
|
+
"--",
|
|
15564
|
+
item.dir
|
|
15565
|
+
], READ_GRACE_MS);
|
|
15566
|
+
if (getOutcome.code !== 0) {
|
|
15567
|
+
failures.push(item.dir + " 读取失败:" + tailOfText(getOutcome.stderr || getOutcome.stdout));
|
|
15568
|
+
continue;
|
|
15569
|
+
}
|
|
15570
|
+
const merged = [...getOutcome.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)];
|
|
15571
|
+
let added = 0;
|
|
15572
|
+
for (const name of item.names) {
|
|
15573
|
+
if (merged.includes(name)) continue;
|
|
15574
|
+
merged.push(name);
|
|
15575
|
+
added++;
|
|
15576
|
+
}
|
|
15577
|
+
if (!added) {
|
|
15578
|
+
notes.push(item.dir + ":无新增(已在忽略列表)");
|
|
15579
|
+
continue;
|
|
15580
|
+
}
|
|
15581
|
+
const setOutcome = await runSvn(wcRoot, [
|
|
15582
|
+
"propset",
|
|
15583
|
+
"svn:ignore",
|
|
15584
|
+
merged.join("\n"),
|
|
15585
|
+
"--",
|
|
15586
|
+
item.dir
|
|
15587
|
+
], MUTATE_GRACE_MS);
|
|
15588
|
+
if (setOutcome.code !== 0) {
|
|
15589
|
+
failures.push(item.dir + " 写入失败:" + tailOfText(setOutcome.stderr || setOutcome.stdout));
|
|
15590
|
+
continue;
|
|
15591
|
+
}
|
|
15592
|
+
count += added;
|
|
15593
|
+
notes.push(item.dir + ":新增 " + added + " 项");
|
|
15594
|
+
}
|
|
15595
|
+
const ok = failures.length < items.length;
|
|
15596
|
+
const summary = failures.length ? "svn:ignore 写入 " + count + " 项;失败 " + failures.length + " 个目录" : count > 0 ? "svn:ignore 写入 " + count + " 项" : "无新增忽略项";
|
|
15597
|
+
return {
|
|
15598
|
+
ok,
|
|
15599
|
+
count,
|
|
15600
|
+
summary,
|
|
15601
|
+
output: [...notes, ...failures].join("\n")
|
|
15602
|
+
};
|
|
15603
|
+
};
|
|
14074
15604
|
return { handlers: {
|
|
14075
15605
|
"svn.status": async (args) => {
|
|
14076
15606
|
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
@@ -14186,6 +15716,26 @@ function createSvnRpc(deps) {
|
|
|
14186
15716
|
"svn.add": async (args) => {
|
|
14187
15717
|
return mutate("加入版本控制", ["add"], args.sessionId, args.paths);
|
|
14188
15718
|
},
|
|
15719
|
+
/**
|
|
15720
|
+
* 分区(changelist)批量操作:关联或解除工作副本条目的 changelist 登记。
|
|
15721
|
+
* 复用 mutate 全套护栏(路径白名单 / BATCH_PATHS_CAP / 超时 / 变更缓存作废);
|
|
15722
|
+
* 分区名 host 权威校验(`-` 开头会被 svn 当成选项解析),remove 分支不需要名字。
|
|
15723
|
+
* @author ddj 2026年09月23号
|
|
15724
|
+
* @param args.paths 工作区相对路径列表
|
|
15725
|
+
* @param args.name 目标分区名(remove 时忽略)
|
|
15726
|
+
* @param args.remove true = 移出分区(`changelist --remove`)
|
|
15727
|
+
* @returns 批量动作结果
|
|
15728
|
+
*/
|
|
15729
|
+
"svn.changelist": async (args) => {
|
|
15730
|
+
if (args.remove !== true) {
|
|
15731
|
+
const nameError = svnChangelistNameErrorOf(String(args.name ?? ""));
|
|
15732
|
+
if (nameError) return {
|
|
15733
|
+
ok: false,
|
|
15734
|
+
error: nameError
|
|
15735
|
+
};
|
|
15736
|
+
}
|
|
15737
|
+
return mutate(args.remove === true ? "移出分区" : "分区", args.remove === true ? ["changelist", "--remove"] : ["changelist", String(args.name ?? "").trim()], args.sessionId, args.paths);
|
|
15738
|
+
},
|
|
14189
15739
|
"svn.log": async (args) => {
|
|
14190
15740
|
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
14191
15741
|
if ("err" in sc) return {
|
|
@@ -14615,35 +16165,255 @@ function createSvnRpc(deps) {
|
|
|
14615
16165
|
ok: false,
|
|
14616
16166
|
error: "缺少 fs"
|
|
14617
16167
|
};
|
|
14618
|
-
const
|
|
14619
|
-
for (const raw of list) {
|
|
16168
|
+
const sizeOne = async (raw) => {
|
|
14620
16169
|
const rel = safeRel(raw);
|
|
14621
|
-
if (rel === null || rel === "") {
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
});
|
|
14626
|
-
continue;
|
|
14627
|
-
}
|
|
16170
|
+
if (rel === null || rel === "") return {
|
|
16171
|
+
path: String(raw ?? ""),
|
|
16172
|
+
size: null
|
|
16173
|
+
};
|
|
14628
16174
|
try {
|
|
14629
16175
|
const resolved = await fs.resolve(rel, { cwd: sc.cwd });
|
|
14630
16176
|
const info = await fs.stat(resolved);
|
|
14631
|
-
|
|
16177
|
+
return {
|
|
14632
16178
|
path: rel,
|
|
14633
16179
|
size: info && info.type === "file" ? info.size ?? null : null
|
|
14634
|
-
}
|
|
16180
|
+
};
|
|
14635
16181
|
} catch {
|
|
14636
|
-
|
|
16182
|
+
return {
|
|
14637
16183
|
path: rel,
|
|
14638
16184
|
size: null
|
|
14639
|
-
}
|
|
16185
|
+
};
|
|
14640
16186
|
}
|
|
14641
|
-
}
|
|
16187
|
+
};
|
|
16188
|
+
const sizes = [];
|
|
16189
|
+
const BATCH = 16;
|
|
16190
|
+
for (let start = 0; start < list.length; start += BATCH) sizes.push(...await Promise.all(list.slice(start, start + BATCH).map(sizeOne)));
|
|
14642
16191
|
return {
|
|
14643
16192
|
ok: true,
|
|
14644
16193
|
sizes
|
|
14645
16194
|
};
|
|
14646
16195
|
},
|
|
16196
|
+
/**
|
|
16197
|
+
* AI 智能整理分析(只读):拉最新变更 → 附 diff 上下文(超限降级 paths-only)→
|
|
16198
|
+
* LLM 流式产出 JSON → normalizeAiPlan 以真实清单白名单归一。任何失败零执行只报错。
|
|
16199
|
+
* @author ddj 2026年09月23号
|
|
16200
|
+
* @param args.sessionId 会话 id
|
|
16201
|
+
* @returns 三段方案 + 分析元信息(条目数/是否含 diff/丢弃数/模型)
|
|
16202
|
+
*/
|
|
16203
|
+
"svn.aiPlan": async (args) => {
|
|
16204
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
16205
|
+
if ("err" in sc) return {
|
|
16206
|
+
ok: false,
|
|
16207
|
+
error: sc.err
|
|
16208
|
+
};
|
|
16209
|
+
const status = await statusOf(sc.cwd);
|
|
16210
|
+
if (!status.managed || !status.wcRoot) return {
|
|
16211
|
+
ok: false,
|
|
16212
|
+
error: "当前工作区不受 SVN 管理"
|
|
16213
|
+
};
|
|
16214
|
+
if (!status.svnCli) return {
|
|
16215
|
+
ok: false,
|
|
16216
|
+
error: "svn 命令不可用(可在设置页配置 svn 路径)"
|
|
16217
|
+
};
|
|
16218
|
+
try {
|
|
16219
|
+
const { entries } = await changesOf(status.wcRoot, true);
|
|
16220
|
+
let diffText = null;
|
|
16221
|
+
try {
|
|
16222
|
+
const outcome = await runSvn(status.wcRoot, [
|
|
16223
|
+
"diff",
|
|
16224
|
+
"-x",
|
|
16225
|
+
"-U0"
|
|
16226
|
+
], READ_GRACE_MS, AI_PLAN_DIFF_CAP);
|
|
16227
|
+
if (outcome.code === 0 && outcome.stdout.trim() && outcome.stdout.length < 2097152) diffText = outcome.stdout;
|
|
16228
|
+
} catch {}
|
|
16229
|
+
const capped = entries.length > AI_PLAN_PATHS_CAP;
|
|
16230
|
+
const used = capped ? entries.slice(0, AI_PLAN_PATHS_CAP) : entries;
|
|
16231
|
+
const diffIncluded = diffText !== null && !capped;
|
|
16232
|
+
const prompt = buildAiPrompt(used, diffIncluded ? diffText : null, capped);
|
|
16233
|
+
const cfg = settings.ai ? settings.ai() : {
|
|
16234
|
+
enabled: true,
|
|
16235
|
+
provider: "",
|
|
16236
|
+
model: "",
|
|
16237
|
+
effort: ""
|
|
16238
|
+
};
|
|
16239
|
+
const result = await svnAiPlanOf(ctx, cfg, prompt);
|
|
16240
|
+
const { plan, dropped } = normalizeAiPlan(result.raw, entries);
|
|
16241
|
+
return {
|
|
16242
|
+
ok: true,
|
|
16243
|
+
plan,
|
|
16244
|
+
entriesCount: entries.length,
|
|
16245
|
+
diffIncluded,
|
|
16246
|
+
dropped,
|
|
16247
|
+
model: result.model
|
|
16248
|
+
};
|
|
16249
|
+
} catch (error) {
|
|
16250
|
+
return {
|
|
16251
|
+
ok: false,
|
|
16252
|
+
error: String(error instanceof Error ? error.message : error)
|
|
16253
|
+
};
|
|
16254
|
+
}
|
|
16255
|
+
},
|
|
16256
|
+
/**
|
|
16257
|
+
* AI 智能整理执行段③:按目录合并写入 svn:ignore 属性(propget 合并,不整体覆盖)。
|
|
16258
|
+
* 复用受管理/svnCli 校验与变更缓存作废语义(对齐 mutate)。
|
|
16259
|
+
* @author ddj 2026年09月23号
|
|
16260
|
+
* @param args.items 目录聚合写入项(dir + 该目录下新增忽略名)
|
|
16261
|
+
* @returns 新增计数/摘要/原文
|
|
16262
|
+
*/
|
|
16263
|
+
"svn.ignore": async (args) => {
|
|
16264
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
16265
|
+
if ("err" in sc) return {
|
|
16266
|
+
ok: false,
|
|
16267
|
+
error: sc.err
|
|
16268
|
+
};
|
|
16269
|
+
const items = Array.isArray(args.items) ? args.items : [];
|
|
16270
|
+
if (!items.length) return {
|
|
16271
|
+
ok: false,
|
|
16272
|
+
error: "未选择任何路径"
|
|
16273
|
+
};
|
|
16274
|
+
const clean = [];
|
|
16275
|
+
let total = 0;
|
|
16276
|
+
for (const item of items) {
|
|
16277
|
+
const rel = safeRel(item?.dir);
|
|
16278
|
+
if (rel === null) return {
|
|
16279
|
+
ok: false,
|
|
16280
|
+
error: "路径不合法"
|
|
16281
|
+
};
|
|
16282
|
+
const names = Array.isArray(item?.names) ? item.names : [];
|
|
16283
|
+
if (!names.length) return {
|
|
16284
|
+
ok: false,
|
|
16285
|
+
error: "忽略名列表为空"
|
|
16286
|
+
};
|
|
16287
|
+
const checked = [];
|
|
16288
|
+
for (const name of names) {
|
|
16289
|
+
const nameError = ignoreNameErrorOf(name);
|
|
16290
|
+
if (nameError) return {
|
|
16291
|
+
ok: false,
|
|
16292
|
+
error: nameError
|
|
16293
|
+
};
|
|
16294
|
+
checked.push(String(name).trim());
|
|
16295
|
+
}
|
|
16296
|
+
total += checked.length;
|
|
16297
|
+
if (total > 64) return {
|
|
16298
|
+
ok: false,
|
|
16299
|
+
error: "一次最多处理 64 个路径"
|
|
16300
|
+
};
|
|
16301
|
+
clean.push({
|
|
16302
|
+
dir: rel === "" ? "." : rel,
|
|
16303
|
+
names: checked
|
|
16304
|
+
});
|
|
16305
|
+
}
|
|
16306
|
+
const status = await statusOf(sc.cwd);
|
|
16307
|
+
if (!status.managed || !status.wcRoot) return {
|
|
16308
|
+
ok: false,
|
|
16309
|
+
error: "当前工作区不受 SVN 管理"
|
|
16310
|
+
};
|
|
16311
|
+
if (!status.svnCli) return {
|
|
16312
|
+
ok: false,
|
|
16313
|
+
error: "svn 命令不可用(可在设置页配置 svn 路径)"
|
|
16314
|
+
};
|
|
16315
|
+
try {
|
|
16316
|
+
const result = await ignoreApplyOf(status.wcRoot, clean);
|
|
16317
|
+
changesCache.delete(status.wcRoot);
|
|
16318
|
+
if (!result.ok) return {
|
|
16319
|
+
ok: false,
|
|
16320
|
+
error: result.summary + (result.output ? "\n" + tailOfText(result.output) : "")
|
|
16321
|
+
};
|
|
16322
|
+
return {
|
|
16323
|
+
ok: true,
|
|
16324
|
+
count: result.count,
|
|
16325
|
+
summary: result.summary,
|
|
16326
|
+
output: result.output
|
|
16327
|
+
};
|
|
16328
|
+
} catch (error) {
|
|
16329
|
+
return {
|
|
16330
|
+
ok: false,
|
|
16331
|
+
error: "svn:ignore 写入失败:" + String(error)
|
|
16332
|
+
};
|
|
16333
|
+
}
|
|
16334
|
+
},
|
|
16335
|
+
/**
|
|
16336
|
+
* 混合通道投递口(01-hybrid-deep-analysis):会话 agent 深度分析后 POST 方案入收件箱。
|
|
16337
|
+
* 只读 + 内存态:经 normalizeAiPlan 以最新变更清单白名单归一(幻觉路径丢弃计数),
|
|
16338
|
+
* 零接受拒收(防 agent 空转误报成功);覆盖式入箱(新投递顶掉旧件)。
|
|
16339
|
+
* @author ddj 2026年09月23号
|
|
16340
|
+
* @param args.plan agent 产出的方案 JSON(形状宽松,归一兜底)
|
|
16341
|
+
* @returns 接受路径数与丢弃数
|
|
16342
|
+
*/
|
|
16343
|
+
"svn.aiPlanSubmit": async (args) => {
|
|
16344
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
16345
|
+
if ("err" in sc) return {
|
|
16346
|
+
ok: false,
|
|
16347
|
+
error: sc.err
|
|
16348
|
+
};
|
|
16349
|
+
const status = await statusOf(sc.cwd);
|
|
16350
|
+
if (!status.managed || !status.wcRoot) return {
|
|
16351
|
+
ok: false,
|
|
16352
|
+
error: "当前工作区不受 SVN 管理"
|
|
16353
|
+
};
|
|
16354
|
+
if (!status.svnCli) return {
|
|
16355
|
+
ok: false,
|
|
16356
|
+
error: "svn 命令不可用(可在设置页配置 svn 路径)"
|
|
16357
|
+
};
|
|
16358
|
+
try {
|
|
16359
|
+
const { entries } = await changesOf(status.wcRoot, true);
|
|
16360
|
+
const { plan, dropped } = normalizeAiPlan(args.plan, entries);
|
|
16361
|
+
const accepted = plan.reverts.length + plan.ignores.length + plan.groups.reduce((n, group) => n + group.paths.length, 0);
|
|
16362
|
+
if (!accepted) return {
|
|
16363
|
+
ok: false,
|
|
16364
|
+
error: "方案为空:无任何路径通过白名单校验(丢弃 " + dropped + " 项)"
|
|
16365
|
+
};
|
|
16366
|
+
planInbox.set(status.wcRoot, {
|
|
16367
|
+
plan,
|
|
16368
|
+
dropped,
|
|
16369
|
+
at: now()
|
|
16370
|
+
});
|
|
16371
|
+
return {
|
|
16372
|
+
ok: true,
|
|
16373
|
+
accepted,
|
|
16374
|
+
dropped
|
|
16375
|
+
};
|
|
16376
|
+
} catch (error) {
|
|
16377
|
+
return {
|
|
16378
|
+
ok: false,
|
|
16379
|
+
error: "方案投递失败:" + String(error)
|
|
16380
|
+
};
|
|
16381
|
+
}
|
|
16382
|
+
},
|
|
16383
|
+
/**
|
|
16384
|
+
* 混合通道取件口:面板轮询取收件箱方案。
|
|
16385
|
+
* since = 注入时刻——早于它的旧投递不算新件(防上一轮残留被误认为本轮结果);
|
|
16386
|
+
* TTL 过期返回 plan:null。非破坏性读取(重复取到同一份由 client 侧取件即停消化)。
|
|
16387
|
+
* @author ddj 2026年09月23号
|
|
16388
|
+
* @param args.since 注入时刻时间戳(缺省 0 = 取任意件)
|
|
16389
|
+
* @returns 方案与元信息(无新件 plan 为 null)
|
|
16390
|
+
*/
|
|
16391
|
+
"svn.aiPlanPending": async (args) => {
|
|
16392
|
+
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
16393
|
+
if ("err" in sc) return {
|
|
16394
|
+
ok: false,
|
|
16395
|
+
error: sc.err
|
|
16396
|
+
};
|
|
16397
|
+
const status = await statusOf(sc.cwd);
|
|
16398
|
+
if (!status.managed || !status.wcRoot) return {
|
|
16399
|
+
ok: false,
|
|
16400
|
+
error: "当前工作区不受 SVN 管理"
|
|
16401
|
+
};
|
|
16402
|
+
const hit = planInbox.get(status.wcRoot);
|
|
16403
|
+
const since = Number(args.since) || 0;
|
|
16404
|
+
if (!hit || hit.at < since || now() - hit.at > 18e5) return {
|
|
16405
|
+
ok: true,
|
|
16406
|
+
plan: null,
|
|
16407
|
+
dropped: 0,
|
|
16408
|
+
at: 0
|
|
16409
|
+
};
|
|
16410
|
+
return {
|
|
16411
|
+
ok: true,
|
|
16412
|
+
plan: hit.plan,
|
|
16413
|
+
dropped: hit.dropped,
|
|
16414
|
+
at: hit.at
|
|
16415
|
+
};
|
|
16416
|
+
},
|
|
14647
16417
|
"svn.wcRev": async (args) => {
|
|
14648
16418
|
const sc = await requireSvnSession(ctx, args.sessionId);
|
|
14649
16419
|
if ("err" in sc) return {
|
|
@@ -16428,6 +18198,20 @@ const inject = [
|
|
|
16428
18198
|
"agents"
|
|
16429
18199
|
];
|
|
16430
18200
|
/**
|
|
18201
|
+
* 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
|
|
18202
|
+
* DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
|
|
18203
|
+
* 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
|
|
18204
|
+
* 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
|
|
18205
|
+
* cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
|
|
18206
|
+
* volatile: true —— 0.1.7 的 SettingsForms.describe 只下发含 volatile 字段的 entry
|
|
18207
|
+
* (volatileForm 门槛),漏标则 ns 不出现 → client configForms 恒 unavailable →
|
|
18208
|
+
* 设置页「设置服务暂不可用」;同时 volatile 字段由 Loader 就地提交免重载。
|
|
18209
|
+
* 运行时解析:@deepseek-ai/schemastery 经插件自身 dependencies(link/dev/npm 安装
|
|
18210
|
+
* 三形态一致,钉 ~3.18.4 —— 3.18.1 起才有 .volatile(),低版本经 markVolatile 守卫降级)。
|
|
18211
|
+
* @author ddj 2026年09月22号(2026年09月23号 增补 volatile 标记)
|
|
18212
|
+
*/
|
|
18213
|
+
const Config = buildSettingsSchema(z, { volatile: true });
|
|
18214
|
+
/**
|
|
16431
18215
|
* 装配插件:挂事件监听、注册路由、安装兼容层。
|
|
16432
18216
|
* @author ddj 2026年08月20号
|
|
16433
18217
|
* @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
|
|
@@ -16517,6 +18301,6 @@ async function logCompatSummary(ctx, warnings) {
|
|
|
16517
18301
|
}
|
|
16518
18302
|
}
|
|
16519
18303
|
//#endregion
|
|
16520
|
-
export { apply, inject, name };
|
|
18304
|
+
export { Config, apply, inject, name };
|
|
16521
18305
|
|
|
16522
18306
|
//# sourceMappingURL=index.js.map
|