dsh-vscode-mode 0.6.0 → 0.8.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 +40 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +9317 -7494
- package/lib/client.js.map +1 -1
- package/lib/index.js +1295 -173
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +3 -1
- package/src/client/compat.ts +134 -4
- package/src/client/copyText.ts +28 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +68 -39
- 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/openWithDialog.ts +66 -0
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/FileExplorer.ts +1 -1
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/panels/SvnPanel.ts +1 -1
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/types.ts +6 -0
- package/src/client/styles/editor.css +5 -3
- package/src/client/svnStore.ts +6 -1
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +198 -25
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/OfficialSideTab.ts +1 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/PromptDialog.ts +63 -0
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/commandCatalog.ts +14 -0
- package/src/client/ui/icons.ts +72 -0
- package/src/compat.ts +2 -2
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +163 -40
- package/src/index.ts +15 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/perf.ts +90 -4
- package/src/routes.ts +13 -1
- package/src/rpc.ts +294 -36
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +2 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +158 -1
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
|
/**
|
|
@@ -130,6 +131,7 @@ const KEYBINDING_DEFAULTS = {
|
|
|
130
131
|
"edrv.navigateForward": "Alt+ArrowRight|Ctrl+Shift+-",
|
|
131
132
|
"edrv.nextTab": "Ctrl+Alt+ArrowRight|Ctrl+PageDown",
|
|
132
133
|
"edrv.prevTab": "Ctrl+Alt+ArrowLeft|Ctrl+PageUp",
|
|
134
|
+
"edrv.goToLine": "Ctrl+G",
|
|
133
135
|
"edrv.showCommands": "Ctrl+Shift+P|F1",
|
|
134
136
|
"edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
|
|
135
137
|
"edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
|
|
@@ -207,6 +209,103 @@ function contentGuardOf(text) {
|
|
|
207
209
|
return {};
|
|
208
210
|
}
|
|
209
211
|
//#endregion
|
|
212
|
+
//#region src/shared/nativeOpen.ts
|
|
213
|
+
/**
|
|
214
|
+
* dsh-vscode-mode shared — 原生打开范围判定(host/client 双面契约)。
|
|
215
|
+
* 背景:DSH 文件预览双形态——本插件认领 `dsh-resource://file/**` 进 Monaco 编辑,
|
|
216
|
+
* 官方渲染器(Office 预览/不可预览提示/表格预览)看认领让位。本模块承载「原生打开」
|
|
217
|
+
* 范围的单一事实源:默认集 = 让位清单(插件本来就不认领的后缀),用户可经设置
|
|
218
|
+
* `nativeOpenExts`(逗号分隔)增删;host 的 Config schema 默认值与 client 的
|
|
219
|
+
* 文件树点击判定 / claim 认领判定共用,保证三处永不同源漂移。
|
|
220
|
+
* 决策沿袭(不因本模块迁移而变):CSV/TSV **不在**默认集——官方 0.1.7 有表格预览,
|
|
221
|
+
* 但编辑器可编辑性优先,仅用户显式加入 nativeOpenExts 才让位(同 avif 例外先例)。
|
|
222
|
+
* 作者 ddj 2026年09月22号
|
|
223
|
+
*/
|
|
224
|
+
/**
|
|
225
|
+
* Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
|
|
226
|
+
* 声明 `doc/docx/xls/xlsx/ppt/pptx`(0.1.6 起侧栏 Office 预览;0.1.7 扩展表格 CSV/TSV
|
|
227
|
+
* 只读预览——本清单刻意不含 csv/tsv,见文件头决策)。本插件让位官方(不认领),
|
|
228
|
+
* 否则会把这些文件路由进 Monaco 而丢掉官方预览。
|
|
229
|
+
*/
|
|
230
|
+
const OFFICE_EXT = [
|
|
231
|
+
"doc",
|
|
232
|
+
"docx",
|
|
233
|
+
"xls",
|
|
234
|
+
"xlsx",
|
|
235
|
+
"ppt",
|
|
236
|
+
"pptx",
|
|
237
|
+
"odt",
|
|
238
|
+
"ods",
|
|
239
|
+
"odp",
|
|
240
|
+
"pages",
|
|
241
|
+
"numbers"
|
|
242
|
+
];
|
|
243
|
+
/**
|
|
244
|
+
* 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
|
|
245
|
+
* `dsh-client-ui-sidebar-documentpreview/lib/client.js`)。这些后缀官方会给出
|
|
246
|
+
* 「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
|
|
247
|
+
*
|
|
248
|
+
* ⚠️ 刻意例外:官方清单含 `avif`,但本插件图片预览支持 avif(浏览器原生解码),
|
|
249
|
+
* 故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
|
|
250
|
+
* @author ddj 2026年09月18号
|
|
251
|
+
*/
|
|
252
|
+
const BLIND_EXT = [
|
|
253
|
+
"mp4",
|
|
254
|
+
"mov",
|
|
255
|
+
"avi",
|
|
256
|
+
"mkv",
|
|
257
|
+
"webm",
|
|
258
|
+
"flv",
|
|
259
|
+
"wmv",
|
|
260
|
+
"m4v",
|
|
261
|
+
"mp3",
|
|
262
|
+
"wav",
|
|
263
|
+
"flac",
|
|
264
|
+
"ogg",
|
|
265
|
+
"m4a",
|
|
266
|
+
"aac",
|
|
267
|
+
"wma",
|
|
268
|
+
"opus",
|
|
269
|
+
"zip",
|
|
270
|
+
"gz",
|
|
271
|
+
"tgz",
|
|
272
|
+
"bz2",
|
|
273
|
+
"xz",
|
|
274
|
+
"zst",
|
|
275
|
+
"7z",
|
|
276
|
+
"rar",
|
|
277
|
+
"tar",
|
|
278
|
+
"jar",
|
|
279
|
+
"exe",
|
|
280
|
+
"dll",
|
|
281
|
+
"so",
|
|
282
|
+
"dylib",
|
|
283
|
+
"bin",
|
|
284
|
+
"o",
|
|
285
|
+
"class",
|
|
286
|
+
"pyc",
|
|
287
|
+
"wasm",
|
|
288
|
+
"ttf",
|
|
289
|
+
"otf",
|
|
290
|
+
"woff",
|
|
291
|
+
"woff2",
|
|
292
|
+
"eot",
|
|
293
|
+
"dmg",
|
|
294
|
+
"iso",
|
|
295
|
+
"img",
|
|
296
|
+
"sqlite",
|
|
297
|
+
"db",
|
|
298
|
+
"psd",
|
|
299
|
+
"ai",
|
|
300
|
+
"sketch",
|
|
301
|
+
"tiff",
|
|
302
|
+
"tif",
|
|
303
|
+
"heic",
|
|
304
|
+
"heif"
|
|
305
|
+
];
|
|
306
|
+
/** 默认范围的设置序列(逗号分隔;Config schema 默认值与设置页回显同源)。 */
|
|
307
|
+
const DEFAULT_NATIVE_CSV = [...OFFICE_EXT, ...BLIND_EXT].join(",");
|
|
308
|
+
//#endregion
|
|
210
309
|
//#region src/shared/logger.ts
|
|
211
310
|
/**
|
|
212
311
|
* dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
|
|
@@ -321,10 +420,11 @@ function bindHostLog(ctx) {
|
|
|
321
420
|
* dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
|
|
322
421
|
* 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
|
|
323
422
|
* rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
|
|
324
|
-
* 服务的 installSection
|
|
423
|
+
* 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
|
|
424
|
+
* 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
|
|
325
425
|
* 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
|
|
326
426
|
* 全程 try/catch,不产生未捕获 rejection。
|
|
327
|
-
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
|
|
427
|
+
* 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号
|
|
328
428
|
*/
|
|
329
429
|
const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
|
|
330
430
|
const FILE_OPEN_DEFAULT = "auto";
|
|
@@ -436,6 +536,7 @@ function loadSettingsDeps(importFn = hostImport) {
|
|
|
436
536
|
const INSTALL_UNMOUNTED = "设置 section 尚未装配";
|
|
437
537
|
const INSTALL_LEGACY = "rc 线:dsh-settings.installSettingsSection";
|
|
438
538
|
const INSTALL_SERVICE = "0.1.2-alpha 线:settings 服务 installSection";
|
|
539
|
+
const INSTALL_FORMS = "0.1.7 线:设置并入插件 Config schema(SettingsForms)";
|
|
439
540
|
const INSTALL_NONE = "两路均不可用:设置持久化降级为配置值";
|
|
440
541
|
let observedInstall = {
|
|
441
542
|
strategy: "unknown",
|
|
@@ -458,14 +559,51 @@ function settingsInstallNote() {
|
|
|
458
559
|
return observedInstall.note;
|
|
459
560
|
}
|
|
460
561
|
/**
|
|
562
|
+
* 判定设置写冲突错误(0.1.7 SettingsConflictError:code 常量 / 构造器名双通道)。
|
|
563
|
+
* @author ddj 2026年09月22号
|
|
564
|
+
* @param error 捕获到的错误
|
|
565
|
+
* @returns 是否为 revision 冲突
|
|
566
|
+
*/
|
|
567
|
+
function isConflictError(error) {
|
|
568
|
+
if (!error || typeof error !== "object") return false;
|
|
569
|
+
const e = error;
|
|
570
|
+
return e.code === "SETTINGS_CONFLICT" || e.name === "SettingsConflictError";
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 0.1.7 forms 策略:订阅 settings/document-updated 事件,目标 namespace 变化时
|
|
574
|
+
* 把 describe 最新值推给 hooks(setSource + onChange),替代旧 installSection 的
|
|
575
|
+
* 变更回调;disposer 经 ctx.effect 随插件 fiber 卸载(热重载不泄漏)。
|
|
576
|
+
* @author ddj 2026年09月22号
|
|
577
|
+
* @param ctx DSH host 上下文
|
|
578
|
+
* @param provider settings 服务(SettingsForms)
|
|
579
|
+
* @param ns 目标设置命名空间
|
|
580
|
+
* @param hooks 设置源绑定与变更回调
|
|
581
|
+
*/
|
|
582
|
+
function watchDocUpdates(ctx, provider, ns, hooks) {
|
|
583
|
+
try {
|
|
584
|
+
const bus = ctx;
|
|
585
|
+
if (typeof bus.on !== "function") return;
|
|
586
|
+
const off = bus.on("settings/document-updated", (updatedNs) => {
|
|
587
|
+
if (updatedNs !== ns) return;
|
|
588
|
+
hooks.setSource(() => readSectionValue(provider, ns));
|
|
589
|
+
hooks.onChange();
|
|
590
|
+
});
|
|
591
|
+
if (typeof bus.effect === "function" && typeof off === "function") bus.effect(() => off);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
log.warn("settings/document-updated 订阅失败:" + String(error));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
461
597
|
* 设置 section 版本自适应安装(核心策略分派)。
|
|
462
598
|
* legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
|
|
463
599
|
* service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
|
|
464
600
|
* provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
|
|
465
|
-
* base 层与 fiber
|
|
601
|
+
* base 层与 fiber 卸载回退)。
|
|
602
|
+
* forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
|
|
603
|
+
* 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
|
|
466
604
|
* none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
|
|
467
605
|
* 全程不抛错、不产生未捕获 rejection。
|
|
468
|
-
* @author ddj 2026年09月02号
|
|
606
|
+
* @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
|
|
469
607
|
* @param ctx DSH host 上下文(inject 可选探测)
|
|
470
608
|
* @param ns 设置命名空间
|
|
471
609
|
* @param schema schemastery schema
|
|
@@ -498,6 +636,12 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
|
|
|
498
636
|
const provider = typeof sc.get === "function" ? sc.get("settings") : sc.settings;
|
|
499
637
|
const install = provider?.installSection;
|
|
500
638
|
if (typeof install !== "function") {
|
|
639
|
+
const forms = provider;
|
|
640
|
+
if (typeof forms?.describe === "function" && typeof forms?.update === "function") {
|
|
641
|
+
recordInstall("forms", INSTALL_FORMS);
|
|
642
|
+
watchDocUpdates(ctx, forms, ns, hooks);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
501
645
|
log.warn("settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
|
|
502
646
|
recordInstall("none", INSTALL_NONE);
|
|
503
647
|
return;
|
|
@@ -528,6 +672,99 @@ function keybindingsShape(z) {
|
|
|
528
672
|
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
|
|
529
673
|
return shape;
|
|
530
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* 设置节 describe 项的形状校验:value 须为普通对象。
|
|
677
|
+
* ns 在两代语义不同(旧=自装 section 名,0.1.7=profile entry id),同名撞车时
|
|
678
|
+
* 以形状兜底防误命中非设置数据;0.1.7 下本插件 entry 的 Config 值与旧 section
|
|
679
|
+
* 值同为设置键对象,两形状天然兼容。
|
|
680
|
+
* @author ddj 2026年09月22号
|
|
681
|
+
* @param value describe 项的 value 字段
|
|
682
|
+
* @returns 是否具备设置节形状
|
|
683
|
+
*/
|
|
684
|
+
function isSectionShaped(value) {
|
|
685
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* 按 ns + 形状校验查找设置节(host 读路径统一入口)。
|
|
689
|
+
* @author ddj 2026年09月22号
|
|
690
|
+
* @param provider settings 服务(可空)
|
|
691
|
+
* @param ns 设置命名空间
|
|
692
|
+
* @returns 命中的 describe 项;未命中返回 undefined
|
|
693
|
+
*/
|
|
694
|
+
function sectionOf(provider, ns) {
|
|
695
|
+
const items = provider?.describe?.({ redactSecrets: true });
|
|
696
|
+
if (!items) return void 0;
|
|
697
|
+
return items.find((item) => item.ns === ns && isSectionShaped(item.value));
|
|
698
|
+
}
|
|
699
|
+
/** 读设置节存储值(未就绪/形状不符返回 undefined)。 */
|
|
700
|
+
function readSectionValue(provider, ns) {
|
|
701
|
+
return sectionOf(provider, ns)?.value;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* 带冲突自愈的设置写入:0.1.7 SettingsConflictError(revision 拒写)时重读
|
|
705
|
+
* revision 重试一次;其余错误与服务缺失一律结构化返回,绝不抛未捕获异常。
|
|
706
|
+
* @author ddj 2026年09月22号
|
|
707
|
+
* @param provider settings 服务(可空)
|
|
708
|
+
* @param ns 设置命名空间
|
|
709
|
+
* @param patch 写入字段
|
|
710
|
+
* @param expectedRevision 读侧携带的 revision(缺省用 describe 最新值)
|
|
711
|
+
* @returns 写入结果
|
|
712
|
+
*/
|
|
713
|
+
async function updateSection(provider, ns, patch, expectedRevision) {
|
|
714
|
+
if (!provider?.update) return {
|
|
715
|
+
ok: false,
|
|
716
|
+
error: "settings 服务无 update"
|
|
717
|
+
};
|
|
718
|
+
const first = expectedRevision ?? sectionOf(provider, ns)?.revision;
|
|
719
|
+
try {
|
|
720
|
+
await provider.update(ns, patch, first);
|
|
721
|
+
return { ok: true };
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (!isConflictError(error)) return {
|
|
724
|
+
ok: false,
|
|
725
|
+
error: String(error)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
const fresh = sectionOf(provider, ns)?.revision;
|
|
729
|
+
try {
|
|
730
|
+
await provider.update(ns, patch, fresh);
|
|
731
|
+
return {
|
|
732
|
+
ok: true,
|
|
733
|
+
conflict: true
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return {
|
|
737
|
+
ok: false,
|
|
738
|
+
conflict: true,
|
|
739
|
+
error: String(error)
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
|
|
745
|
+
* 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
|
|
746
|
+
* aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
|
|
747
|
+
* (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
|
|
748
|
+
* @author ddj 2026年09月22号
|
|
749
|
+
* @param z schemastery 命名空间(静态 import 或动态加载均可)
|
|
750
|
+
* @returns schemastery object schema
|
|
751
|
+
*/
|
|
752
|
+
function buildSettingsSchema(z) {
|
|
753
|
+
return z.object({
|
|
754
|
+
fileOpenTool: z.string().default(FILE_OPEN_DEFAULT),
|
|
755
|
+
keybindings: z.object(keybindingsShape(z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
756
|
+
sidebarMinWidth: z.number().default(300),
|
|
757
|
+
maxOpenEditors: z.number().default(10),
|
|
758
|
+
integrationBaseUrl: z.string().default(INTEGRATION_BASE_DEFAULT),
|
|
759
|
+
aiInline: z.boolean().default(AI_CONFIG_DEFAULT.enabled),
|
|
760
|
+
aiProvider: z.string().default(AI_CONFIG_DEFAULT.provider),
|
|
761
|
+
aiModel: z.string().default(AI_CONFIG_DEFAULT.model),
|
|
762
|
+
aiEffort: z.string().default(AI_CONFIG_DEFAULT.effort),
|
|
763
|
+
svnPath: z.string().default(""),
|
|
764
|
+
tortoisePath: z.string().default(TORTOISE_DIR_DEFAULT),
|
|
765
|
+
nativeOpenExts: z.string().default(DEFAULT_NATIVE_CSV)
|
|
766
|
+
});
|
|
767
|
+
}
|
|
531
768
|
function normalizeValue(value) {
|
|
532
769
|
if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
|
|
533
770
|
return value.trim();
|
|
@@ -566,23 +803,11 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
|
|
|
566
803
|
deps = await loader();
|
|
567
804
|
} catch {}
|
|
568
805
|
if (!deps) return false;
|
|
569
|
-
const strategy = await runSettingsInstall(ctx, ns, deps.z
|
|
570
|
-
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
571
|
-
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
|
|
572
|
-
sidebarMinWidth: deps.z.number().default(300),
|
|
573
|
-
maxOpenEditors: deps.z.number().default(10),
|
|
574
|
-
integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
|
|
575
|
-
aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
|
|
576
|
-
aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
|
|
577
|
-
aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
|
|
578
|
-
aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
|
|
579
|
-
svnPath: deps.z.string().default(""),
|
|
580
|
-
tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT)
|
|
581
|
-
}), entry, {
|
|
806
|
+
const strategy = await runSettingsInstall(ctx, ns, buildSettingsSchema(deps.z), entry, {
|
|
582
807
|
setSource: (source) => hooks.setSource(source),
|
|
583
808
|
onChange: hooks.onChange
|
|
584
809
|
}, loader);
|
|
585
|
-
return strategy === "legacy" || strategy === "service";
|
|
810
|
+
return strategy === "legacy" || strategy === "service" || strategy === "forms";
|
|
586
811
|
}
|
|
587
812
|
/** AI 配置脏值读取(settings 未就绪时回退默认)。 */
|
|
588
813
|
function aiValueOf(stored) {
|
|
@@ -611,13 +836,13 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
611
836
|
onChange(current);
|
|
612
837
|
};
|
|
613
838
|
const syncRevision = () => {
|
|
614
|
-
revision = (provider
|
|
839
|
+
revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision;
|
|
615
840
|
};
|
|
616
841
|
const settingsChange = () => syncRevision();
|
|
617
842
|
/** AI 配置当前值(settings 未就绪回退默认)。 */
|
|
618
843
|
let aiCurrent = { ...AI_CONFIG_DEFAULT };
|
|
619
844
|
const aiSync = () => {
|
|
620
|
-
const stored = (provider
|
|
845
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
621
846
|
aiCurrent = stored !== void 0 ? aiValueOf(stored) : aiCurrent;
|
|
622
847
|
};
|
|
623
848
|
/** SVN 路径当前值(settings 未就绪回退配置值)。 */
|
|
@@ -633,7 +858,7 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
633
858
|
};
|
|
634
859
|
/** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
|
|
635
860
|
const storedValue = () => {
|
|
636
|
-
return provider
|
|
861
|
+
return readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
637
862
|
};
|
|
638
863
|
installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
|
|
639
864
|
fileOpenTool: current,
|
|
@@ -664,25 +889,15 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
664
889
|
},
|
|
665
890
|
update: async (value, expectedRevision) => {
|
|
666
891
|
const next = normalizeValue(value);
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
|
|
672
|
-
const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
|
|
892
|
+
const result = await updateSection(provider, FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
|
|
893
|
+
if (!result.ok) log.warn("fileOpenTool 设置写入失败:" + (result.error ?? "未知原因"));
|
|
894
|
+
const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
|
|
673
895
|
notify(stored?.fileOpenTool ?? next);
|
|
674
896
|
syncRevision();
|
|
675
897
|
},
|
|
676
898
|
ai: () => aiCurrent,
|
|
677
899
|
svn: () => svnCurrent,
|
|
678
900
|
aiUpdate: async (patch, expectedRevision) => {
|
|
679
|
-
if (!provider?.update) {
|
|
680
|
-
if (patch.enabled !== void 0) aiCurrent.enabled = patch.enabled;
|
|
681
|
-
if (patch.provider !== void 0) aiCurrent.provider = patch.provider;
|
|
682
|
-
if (patch.model !== void 0) aiCurrent.model = patch.model;
|
|
683
|
-
if (patch.effort !== void 0) aiCurrent.effort = patch.effort;
|
|
684
|
-
return aiCurrent;
|
|
685
|
-
}
|
|
686
901
|
const stored = { ...aiCurrent };
|
|
687
902
|
const body = {};
|
|
688
903
|
if (patch.enabled !== void 0) {
|
|
@@ -701,7 +916,10 @@ function setupOpenSettings(ctx, config, onChange) {
|
|
|
701
916
|
stored.effort = patch.effort;
|
|
702
917
|
body.aiEffort = patch.effort;
|
|
703
918
|
}
|
|
704
|
-
await provider
|
|
919
|
+
if (!(await updateSection(provider, "dsh-vscode-mode", body, expectedRevision)).ok) {
|
|
920
|
+
aiCurrent = stored;
|
|
921
|
+
return aiCurrent;
|
|
922
|
+
}
|
|
705
923
|
aiSync();
|
|
706
924
|
return aiCurrent;
|
|
707
925
|
}
|
|
@@ -918,13 +1136,14 @@ function inDshRange(version, range) {
|
|
|
918
1136
|
/**
|
|
919
1137
|
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
920
1138
|
* 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
|
|
921
|
-
* @author ddj 2026年09月02号 / 2026年09月18号
|
|
1139
|
+
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号
|
|
922
1140
|
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
923
1141
|
* @returns 版本线标签
|
|
924
1142
|
*/
|
|
925
1143
|
function familyLabel(input) {
|
|
926
1144
|
const version = parseDshVersion(input);
|
|
927
1145
|
if (!version) return "未知";
|
|
1146
|
+
if (inDshRange(version, { from: "0.1.7-alpha.1" })) return "0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)";
|
|
928
1147
|
if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
|
|
929
1148
|
if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
|
|
930
1149
|
if (inDshRange(version, { from: "0.1.5-alpha.1" })) return "0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)";
|
|
@@ -1863,7 +2082,7 @@ function detectGuards(ctx) {
|
|
|
1863
2082
|
}];
|
|
1864
2083
|
}
|
|
1865
2084
|
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
1866
|
-
const TESTED_DSH_MAX = "0.1.
|
|
2085
|
+
const TESTED_DSH_MAX = "0.1.7-alpha.1";
|
|
1867
2086
|
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
|
|
1868
2087
|
function versionAdapters(dshVersion) {
|
|
1869
2088
|
const adapters = [];
|
|
@@ -1886,7 +2105,7 @@ function versionAdapters(dshVersion) {
|
|
|
1886
2105
|
const strategy = settingsInstallStrategy();
|
|
1887
2106
|
adapters.push({
|
|
1888
2107
|
name: "设置 section 安装(版本适配)",
|
|
1889
|
-
active: strategy === "legacy" || strategy === "service",
|
|
2108
|
+
active: strategy === "legacy" || strategy === "service" || strategy === "forms",
|
|
1890
2109
|
note: settingsInstallNote()
|
|
1891
2110
|
});
|
|
1892
2111
|
return adapters;
|
|
@@ -1909,7 +2128,7 @@ async function buildReport(ctx, options) {
|
|
|
1909
2128
|
if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
|
|
1910
2129
|
const parsed = parseDshVersion(dshVersion);
|
|
1911
2130
|
const testedMax = parseDshVersion(TESTED_DSH_MAX);
|
|
1912
|
-
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.
|
|
2131
|
+
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.7-alpha.1):设置 API 按能力探测运行,异常时请回报适配矩阵");
|
|
1913
2132
|
for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
|
|
1914
2133
|
return {
|
|
1915
2134
|
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
@@ -1999,6 +2218,16 @@ function registerRoutes(ctx, config, handleRpc, onWarning) {
|
|
|
1999
2218
|
input = {};
|
|
2000
2219
|
}
|
|
2001
2220
|
const result = await handleRpc(typeof input.method === "string" ? input.method : "", input.args ?? {});
|
|
2221
|
+
const bin = result?.binary;
|
|
2222
|
+
if (result && result.ok === true && bin?.bytes instanceof Uint8Array) {
|
|
2223
|
+
res.statusCode = 200;
|
|
2224
|
+
res.setHeader("content-type", "application/octet-stream");
|
|
2225
|
+
res.setHeader("x-edrv-mime", String(bin.mime ?? ""));
|
|
2226
|
+
res.setHeader("x-edrv-version", String(bin.version ?? ""));
|
|
2227
|
+
res.setHeader("x-edrv-size", String(bin.size ?? bin.bytes.byteLength));
|
|
2228
|
+
res.end(Buffer.from(bin.bytes));
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2002
2231
|
res.statusCode = 200;
|
|
2003
2232
|
res.setHeader("content-type", "application/json");
|
|
2004
2233
|
res.end(JSON.stringify(result));
|
|
@@ -6670,6 +6899,102 @@ function pendingState(token, take) {
|
|
|
6670
6899
|
function clientsAlive() {
|
|
6671
6900
|
return Date.now() - lastPollAt <= PRESENCE_WINDOW_MS;
|
|
6672
6901
|
}
|
|
6902
|
+
/**
|
|
6903
|
+
* 路径归一:去首尾空白 + 反斜杠转 '/'(Windows 输入等价处理)。
|
|
6904
|
+
* @author ddj 2026年09月22号
|
|
6905
|
+
* @param raw 原始输入
|
|
6906
|
+
* @returns 归一后的字符串
|
|
6907
|
+
*/
|
|
6908
|
+
function normPath(raw) {
|
|
6909
|
+
return String(raw ?? "").trim().replace(/\\/g, "/");
|
|
6910
|
+
}
|
|
6911
|
+
/**
|
|
6912
|
+
* 单个名称段的通用违规检查(两套校验共用)。
|
|
6913
|
+
* @author ddj 2026年09月22号
|
|
6914
|
+
* @param seg 名称段
|
|
6915
|
+
* @returns 错误文案;null = 通过
|
|
6916
|
+
*/
|
|
6917
|
+
function checkSegment(seg) {
|
|
6918
|
+
if (!seg) return "名称包含空路径段";
|
|
6919
|
+
if (seg === "." || seg === "..") return "名称不能包含 . 或 .. 路径段";
|
|
6920
|
+
if (seg.length > 255) return "名称过长(单段超过 255 字符)";
|
|
6921
|
+
if (/[\0-\x1f]/.test(seg)) return "名称包含非法控制字符";
|
|
6922
|
+
return null;
|
|
6923
|
+
}
|
|
6924
|
+
/**
|
|
6925
|
+
* 校验新建文件/文件夹的相对路径(允许 a/b.c 嵌套段)。
|
|
6926
|
+
* @author ddj 2026年09月22号
|
|
6927
|
+
* @param raw 用户输入的名称或相对路径
|
|
6928
|
+
* @returns 错误文案;null = 校验通过
|
|
6929
|
+
*/
|
|
6930
|
+
function checkNewName(raw) {
|
|
6931
|
+
const text = normPath(raw);
|
|
6932
|
+
if (!text) return "名称不能为空";
|
|
6933
|
+
if (text.startsWith("/")) return "名称不能是绝对路径";
|
|
6934
|
+
if (/^[a-zA-Z]:/.test(text)) return "名称不能带盘符";
|
|
6935
|
+
for (const seg of text.split("/")) {
|
|
6936
|
+
const bad = checkSegment(seg);
|
|
6937
|
+
if (bad) return bad;
|
|
6938
|
+
}
|
|
6939
|
+
return null;
|
|
6940
|
+
}
|
|
6941
|
+
/**
|
|
6942
|
+
* 校验重命名的新名称(仅单个名称段,不接受路径)。
|
|
6943
|
+
* @author ddj 2026年09月22号
|
|
6944
|
+
* @param raw 用户输入的新名称
|
|
6945
|
+
* @returns 错误文案;null = 校验通过
|
|
6946
|
+
*/
|
|
6947
|
+
function checkRenameName(raw) {
|
|
6948
|
+
const text = normPath(raw);
|
|
6949
|
+
if (!text) return "名称不能为空";
|
|
6950
|
+
if (text.includes("/")) return "新名称不能包含路径分隔符";
|
|
6951
|
+
return checkSegment(text);
|
|
6952
|
+
}
|
|
6953
|
+
/**
|
|
6954
|
+
* 拼接工作区相对路径(dir 为空 = 根;name 新建时可含多段)。
|
|
6955
|
+
* @author ddj 2026年09月22号
|
|
6956
|
+
* @param dir 目标目录(工作区相对)
|
|
6957
|
+
* @param name 名称
|
|
6958
|
+
* @returns 拼接后的相对路径
|
|
6959
|
+
*/
|
|
6960
|
+
function joinRelPath(dir, name) {
|
|
6961
|
+
const base = normPath(dir).replace(/\/+$/, "");
|
|
6962
|
+
const add = normPath(name).replace(/^\/+/, "");
|
|
6963
|
+
return base ? base + "/" + add : add;
|
|
6964
|
+
}
|
|
6965
|
+
/**
|
|
6966
|
+
* 取相对路径的父目录(顶层条目返回空串 = 根)。
|
|
6967
|
+
* @author ddj 2026年09月22号
|
|
6968
|
+
* @param path 工作区相对路径
|
|
6969
|
+
* @returns 父目录相对路径
|
|
6970
|
+
*/
|
|
6971
|
+
function parentRelOf(path) {
|
|
6972
|
+
const parts = normPath(path).split("/").filter(Boolean);
|
|
6973
|
+
parts.pop();
|
|
6974
|
+
return parts.join("/");
|
|
6975
|
+
}
|
|
6976
|
+
/**
|
|
6977
|
+
* 取相对路径的名称段(末段)。
|
|
6978
|
+
* @author ddj 2026年09月22号
|
|
6979
|
+
* @param path 工作区相对路径
|
|
6980
|
+
* @returns 末段名称
|
|
6981
|
+
*/
|
|
6982
|
+
function baseNameOf(path) {
|
|
6983
|
+
return normPath(path).split("/").filter(Boolean).pop() ?? "";
|
|
6984
|
+
}
|
|
6985
|
+
/**
|
|
6986
|
+
* other 是否为 base 本身或其内部路径(复制/移动「不能以自身或其子目录为目标」护栏)。
|
|
6987
|
+
* @author ddj 2026年09月22号
|
|
6988
|
+
* @param base 基准相对路径(源)
|
|
6989
|
+
* @param other 待判定相对路径(目标目录)
|
|
6990
|
+
* @returns 是否为自身或子路径
|
|
6991
|
+
*/
|
|
6992
|
+
function isSubPath(base, other) {
|
|
6993
|
+
const b = normPath(base).replace(/\/+$/, "");
|
|
6994
|
+
const o = normPath(other).replace(/\/+$/, "");
|
|
6995
|
+
if (!b) return false;
|
|
6996
|
+
return o === b || o.startsWith(b + "/");
|
|
6997
|
+
}
|
|
6673
6998
|
//#endregion
|
|
6674
6999
|
//#region src/reveal.ts
|
|
6675
7000
|
/**
|
|
@@ -7007,12 +7332,70 @@ async function scanSessionInventory(home = dshHome(), archive = sessionsArchiveR
|
|
|
7007
7332
|
}
|
|
7008
7333
|
};
|
|
7009
7334
|
}
|
|
7010
|
-
/**
|
|
7011
|
-
function
|
|
7012
|
-
const raw = new Set(
|
|
7335
|
+
/** id 双向匹配器:原始 id 与编码段名任一命中(兼容编码差异)。 */
|
|
7336
|
+
function idMatcher(ids) {
|
|
7337
|
+
const raw = new Set(ids);
|
|
7013
7338
|
const encoded = /* @__PURE__ */ new Set();
|
|
7014
7339
|
for (const id of raw) encoded.add(sessionIdSegment(id));
|
|
7015
|
-
|
|
7340
|
+
return (id) => raw.has(id) || encoded.has(id);
|
|
7341
|
+
}
|
|
7342
|
+
/** 标记活跃会话(live id 与目录段名双向匹配,兼容编码差异)。 */
|
|
7343
|
+
function markActiveSessions(sessions, activeIds) {
|
|
7344
|
+
const match = idMatcher(activeIds);
|
|
7345
|
+
for (const s of sessions) if (match(s.sessionId)) s.active = true;
|
|
7346
|
+
}
|
|
7347
|
+
/** 空标志集合(服务/字段缺失的统一降级返回值)。 */
|
|
7348
|
+
function emptyFlagIds() {
|
|
7349
|
+
return {
|
|
7350
|
+
archived: /* @__PURE__ */ new Set(),
|
|
7351
|
+
pinned: /* @__PURE__ */ new Set()
|
|
7352
|
+
};
|
|
7353
|
+
}
|
|
7354
|
+
/** 任意值 → id 字符串集合(非数组或非字符串元素一律丢弃)。 */
|
|
7355
|
+
function toIdSet(ids) {
|
|
7356
|
+
if (!Array.isArray(ids)) return /* @__PURE__ */ new Set();
|
|
7357
|
+
return new Set(ids.filter((id) => typeof id === "string"));
|
|
7358
|
+
}
|
|
7359
|
+
/**
|
|
7360
|
+
* 一次读取 workspaceRegistry 的官方归档/置顶 id 集合(摊到盘点各行使用)。
|
|
7361
|
+
* 服务缺失、字段缺失或非数组(DSH 0.1.6 及更旧没有)一律视为空集,绝不抛错。
|
|
7362
|
+
* @author ddj 2026年09月22号
|
|
7363
|
+
* @param ctx DSH 上下文
|
|
7364
|
+
* @returns archived/pinned 两组 id 集合(缺失即空集)
|
|
7365
|
+
*/
|
|
7366
|
+
function registryFlags(ctx) {
|
|
7367
|
+
try {
|
|
7368
|
+
const registry = ctx.get("workspaceRegistry");
|
|
7369
|
+
if (!registry) return emptyFlagIds();
|
|
7370
|
+
return {
|
|
7371
|
+
archived: toIdSet(registry.archivedSessionIds),
|
|
7372
|
+
pinned: toIdSet(registry.pinnedSessionIds)
|
|
7373
|
+
};
|
|
7374
|
+
} catch (error) {
|
|
7375
|
+
log.debug("registryFlags 跳过(服务缺失或读取失败):" + String(error));
|
|
7376
|
+
return emptyFlagIds();
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
/**
|
|
7380
|
+
* 给盘点行附官方归档/置顶标志(best-effort:仅命中附 true,缺失/未命中不附字段),
|
|
7381
|
+
* 让 PerfSettings 会话行能看到官方侧栏的归档/置顶状态。id 匹配复用 markActiveSessions
|
|
7382
|
+
* 的双向(原始 id / 编码段名)口径;服务/字段缺失不附字段、绝不抛错(旧 host 兼容降级)。
|
|
7383
|
+
* @author ddj 2026年09月22号
|
|
7384
|
+
* @param sessions 盘点会话行(就地附加 archived/pinned)
|
|
7385
|
+
* @param ctx DSH 上下文
|
|
7386
|
+
*/
|
|
7387
|
+
function markOfficialFlags(sessions, ctx) {
|
|
7388
|
+
try {
|
|
7389
|
+
const flags = registryFlags(ctx);
|
|
7390
|
+
const archived = idMatcher(flags.archived);
|
|
7391
|
+
const pinned = idMatcher(flags.pinned);
|
|
7392
|
+
for (const s of sessions) {
|
|
7393
|
+
if (archived(s.sessionId)) s.archived = true;
|
|
7394
|
+
if (pinned(s.sessionId)) s.pinned = true;
|
|
7395
|
+
}
|
|
7396
|
+
} catch (error) {
|
|
7397
|
+
log.debug("markOfficialFlags 跳过(读取失败,保持无标志):" + String(error));
|
|
7398
|
+
}
|
|
7016
7399
|
}
|
|
7017
7400
|
/**
|
|
7018
7401
|
* 移出规划(纯函数):从盘点中按「显式集合」或「规则(minBytes / olderThanDays)」
|
|
@@ -7176,6 +7559,27 @@ async function restoreSession(home, archive, workspaceKey, sessionId) {
|
|
|
7176
7559
|
};
|
|
7177
7560
|
}
|
|
7178
7561
|
}
|
|
7562
|
+
/**
|
|
7563
|
+
* 恢复后对齐官方归档标志(best-effort):官方 archive 是 workspaceRegistry 的
|
|
7564
|
+
* archivedSessionIds 持久标志(只藏 UI/拦模型步骤,不搬目录;dsh-workspace 源码注明
|
|
7565
|
+
* unarchiveSession 对未归档与未知 id 均为无写入 no-op、对失踪会话也容忍)。
|
|
7566
|
+
* 插件把目录搬回后若该会话曾被官方归档,官方侧栏会继续隐藏它、0.1.7 的
|
|
7567
|
+
* archived-session-gate 会继续拦它的模型步骤——这里探测 host 的 workspaceRegistry
|
|
7568
|
+
* 服务做一次幂等清除;服务缺失、id 编码差异未命中或调用抛错一律静默跳过
|
|
7569
|
+
* (保持现状,不加猜的行为)。有效期:仅恢复成功后触发一次,不轮询不重试。
|
|
7570
|
+
* @author ddj 2026年09月22号
|
|
7571
|
+
* @param ctx DSH 上下文
|
|
7572
|
+
* @param sessionId 恢复的会话 id(目录段名与原始 id 同形时可命中官方标志)
|
|
7573
|
+
*/
|
|
7574
|
+
async function unarchiveOfficial(ctx, sessionId) {
|
|
7575
|
+
try {
|
|
7576
|
+
const registry = ctx.get("workspaceRegistry");
|
|
7577
|
+
if (!registry || typeof registry.unarchiveSession !== "function") return;
|
|
7578
|
+
await registry.unarchiveSession(sessionId);
|
|
7579
|
+
} catch (error) {
|
|
7580
|
+
log.debug("unarchiveOfficial 跳过(服务缺失或调用失败,保持现状):" + String(error));
|
|
7581
|
+
}
|
|
7582
|
+
}
|
|
7179
7583
|
/** 清除归档区早于 N 天的会话(破坏性,仅限归档区)。 */
|
|
7180
7584
|
async function purgeArchive(archive, olderThanDays) {
|
|
7181
7585
|
const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3;
|
|
@@ -7411,6 +7815,52 @@ function snippetTargetOf(path) {
|
|
|
7411
7815
|
return isSnippetFilePath(path) ? path.replace(/\\/g, "/") : null;
|
|
7412
7816
|
}
|
|
7413
7817
|
/**
|
|
7818
|
+
* 二进制/文本读取共用前置(edrv.read 与 edrv.readBinary):解析路径 + debugRecord +
|
|
7819
|
+
* stat + 文件类型校验 + 32MB 二进制上限,保证两条读通道的错误口径完全一致。
|
|
7820
|
+
* @author ddj 2026年09月22号
|
|
7821
|
+
* @param ctx DSH 上下文
|
|
7822
|
+
* @param sc requireSession 成功结果(session + cwd)
|
|
7823
|
+
* @param path 客户端请求路径
|
|
7824
|
+
* @returns 成功返回解析目标与 stat;失败返回错误文案(文件不存在时附 resolvedPath)
|
|
7825
|
+
*/
|
|
7826
|
+
async function readTargetOf(ctx, sc, path) {
|
|
7827
|
+
const fs = ctx.get("fs");
|
|
7828
|
+
if (!fs) return { err: "缺少 fs" };
|
|
7829
|
+
try {
|
|
7830
|
+
const target = await resolveTarget(ctx, sc.session, path);
|
|
7831
|
+
debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(path ?? "") + " resolved=" + fs.processPath(target), "debug");
|
|
7832
|
+
const info = await fs.stat(target);
|
|
7833
|
+
if (!info || info.type !== "file") return {
|
|
7834
|
+
err: "文件不存在",
|
|
7835
|
+
resolvedPath: fs.processPath(target)
|
|
7836
|
+
};
|
|
7837
|
+
if ((info.size ?? 0) > 33554432) return { err: "文件过大(>32MB),不支持整文件预览" };
|
|
7838
|
+
return {
|
|
7839
|
+
target,
|
|
7840
|
+
fs,
|
|
7841
|
+
info
|
|
7842
|
+
};
|
|
7843
|
+
} catch (error) {
|
|
7844
|
+
return { err: "读取失败:" + String(error) };
|
|
7845
|
+
}
|
|
7846
|
+
}
|
|
7847
|
+
/**
|
|
7848
|
+
* readTargetOf 失败结果 → RPC 错误载荷(文件不存在时保留 resolvedPath 供诊断)。
|
|
7849
|
+
* @author ddj 2026年09月22号
|
|
7850
|
+
* @param prep readTargetOf 的失败分支
|
|
7851
|
+
* @returns { ok:false } 形态载荷
|
|
7852
|
+
*/
|
|
7853
|
+
function targetErrOf(prep) {
|
|
7854
|
+
return prep.resolvedPath !== void 0 ? {
|
|
7855
|
+
ok: false,
|
|
7856
|
+
error: prep.err,
|
|
7857
|
+
resolvedPath: prep.resolvedPath
|
|
7858
|
+
} : {
|
|
7859
|
+
ok: false,
|
|
7860
|
+
error: prep.err
|
|
7861
|
+
};
|
|
7862
|
+
}
|
|
7863
|
+
/**
|
|
7414
7864
|
* 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
|
|
7415
7865
|
* 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
|
|
7416
7866
|
* @author ddj 2026年09月15号
|
|
@@ -7536,9 +7986,106 @@ async function latestPatchBackup(patchPath) {
|
|
|
7536
7986
|
}
|
|
7537
7987
|
/** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
|
|
7538
7988
|
async function integrationBaseUrlOf(ctx) {
|
|
7539
|
-
const value = (ctx.get("settings")
|
|
7989
|
+
const value = sectionOf(ctx.get("settings"), FILE_OPEN_SETTINGS_NS)?.value;
|
|
7540
7990
|
return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
|
|
7541
7991
|
}
|
|
7992
|
+
/**
|
|
7993
|
+
* 文件操作目标解析:工作区相对路径 → 绝对路径,并做工作区边界检查。
|
|
7994
|
+
* edrv.fs.* 各方法共用(对齐 edrv.saveBinary 的 contains 护栏口径)。
|
|
7995
|
+
* @author ddj 2026年09月22号
|
|
7996
|
+
* @param ctx DSH 上下文
|
|
7997
|
+
* @param session 当前会话
|
|
7998
|
+
* @param rel 工作区相对路径
|
|
7999
|
+
* @returns 绝对路径;缺 fs/越界/解析失败返回错误文案
|
|
8000
|
+
*/
|
|
8001
|
+
async function fsOpsTarget(ctx, session, rel) {
|
|
8002
|
+
const fs = ctx.get("fs");
|
|
8003
|
+
if (!fs) return { err: "缺少 fs" };
|
|
8004
|
+
try {
|
|
8005
|
+
const target = await resolveTarget(ctx, session, rel);
|
|
8006
|
+
const rootTarget = await fs.resolve(policyOf(ctx, session)?.workspaceRoot ?? ".", {});
|
|
8007
|
+
if (!fs.contains(rootTarget, target)) return { err: "拒绝操作:目标不在会话工作区内" };
|
|
8008
|
+
return { abs: fs.processPath(target) };
|
|
8009
|
+
} catch (error) {
|
|
8010
|
+
return { err: "路径解析失败:" + String(error) };
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
/**
|
|
8014
|
+
* 取异常的系统错误码(EEXIST/EXDEV 等分支判定用)。
|
|
8015
|
+
* @author ddj 2026年09月22号
|
|
8016
|
+
* @param error 捕获到的异常
|
|
8017
|
+
* @returns 错误码;取不到返回空串
|
|
8018
|
+
*/
|
|
8019
|
+
function fsErrCode(error) {
|
|
8020
|
+
const code = error?.code;
|
|
8021
|
+
return typeof code === "string" ? code : "";
|
|
8022
|
+
}
|
|
8023
|
+
/**
|
|
8024
|
+
* 复制/移动共用核心(edrv.fsCopy / edrv.fsMove):目标目录存在 + 同名拒绝覆盖 +
|
|
8025
|
+
* 「不能以自身或其子目录为目标」护栏;move 跨设备(EXDEV)回退「递归复制 + 删除源」。
|
|
8026
|
+
* @author ddj 2026年09月22号
|
|
8027
|
+
* @param ctx DSH 上下文
|
|
8028
|
+
* @param sc requireSession 成功结果(session + cwd)
|
|
8029
|
+
* @param args 请求载荷(from / toDir 均为工作区相对路径)
|
|
8030
|
+
* @param mode copy = 复制;move = 移动
|
|
8031
|
+
* @returns 成功返回 from/to 相对路径;失败返回错误文案
|
|
8032
|
+
*/
|
|
8033
|
+
async function fsTransfer(ctx, sc, args, mode) {
|
|
8034
|
+
const fromRel = normalizeRel(args.from);
|
|
8035
|
+
const toDirRel = normalizeRel(args.toDir);
|
|
8036
|
+
if (fromRel === null || fromRel === "" || toDirRel === null) return { err: "路径不合法" };
|
|
8037
|
+
if (isSubPath(fromRel, toDirRel)) return { err: "不能以自身或其子目录为目标" };
|
|
8038
|
+
const src = await fsOpsTarget(ctx, sc.session, fromRel);
|
|
8039
|
+
if ("err" in src) return src;
|
|
8040
|
+
const dir = await fsOpsTarget(ctx, sc.session, toDirRel);
|
|
8041
|
+
if ("err" in dir) return dir;
|
|
8042
|
+
const dirInfo = await stat(dir.abs).catch(() => null);
|
|
8043
|
+
if (!dirInfo || !dirInfo.isDirectory()) return { err: "目标目录不存在" };
|
|
8044
|
+
const toRel = joinRelPath(toDirRel, baseNameOf(fromRel));
|
|
8045
|
+
const dst = await fsOpsTarget(ctx, sc.session, toRel);
|
|
8046
|
+
if ("err" in dst) return dst;
|
|
8047
|
+
if (!await stat(src.abs).catch(() => null)) return { err: "源不存在" };
|
|
8048
|
+
if (await stat(dst.abs).catch(() => null)) return { err: "目标目录已存在同名文件或目录" };
|
|
8049
|
+
try {
|
|
8050
|
+
await transferEntry(src.abs, dst.abs, mode);
|
|
8051
|
+
} catch (error) {
|
|
8052
|
+
return { err: (mode === "copy" ? "复制失败:" : "移动失败:") + String(error) };
|
|
8053
|
+
}
|
|
8054
|
+
invalidateIndex(ctx, sc.cwd, fromRel);
|
|
8055
|
+
invalidateIndex(ctx, sc.cwd, toRel);
|
|
8056
|
+
return {
|
|
8057
|
+
from: fromRel,
|
|
8058
|
+
to: toRel
|
|
8059
|
+
};
|
|
8060
|
+
}
|
|
8061
|
+
/**
|
|
8062
|
+
* 单条目复制/移动落盘(文件/目录通用;move 跨设备回退复制+删除源)。
|
|
8063
|
+
* @author ddj 2026年09月22号
|
|
8064
|
+
* @param absFrom 源绝对路径
|
|
8065
|
+
* @param absTo 目标绝对路径
|
|
8066
|
+
* @param mode copy = 复制;move = 移动
|
|
8067
|
+
*/
|
|
8068
|
+
async function transferEntry(absFrom, absTo, mode) {
|
|
8069
|
+
if (mode === "copy") {
|
|
8070
|
+
await cp(absFrom, absTo, {
|
|
8071
|
+
recursive: true,
|
|
8072
|
+
force: false,
|
|
8073
|
+
errorOnExist: true
|
|
8074
|
+
});
|
|
8075
|
+
return;
|
|
8076
|
+
}
|
|
8077
|
+
try {
|
|
8078
|
+
await rename(absFrom, absTo);
|
|
8079
|
+
} catch (error) {
|
|
8080
|
+
if (fsErrCode(error) !== "EXDEV") throw error;
|
|
8081
|
+
await cp(absFrom, absTo, {
|
|
8082
|
+
recursive: true,
|
|
8083
|
+
force: false,
|
|
8084
|
+
errorOnExist: true
|
|
8085
|
+
});
|
|
8086
|
+
await rm(absFrom, { recursive: true });
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
7542
8089
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
7543
8090
|
function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx), lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers) {
|
|
7544
8091
|
return {
|
|
@@ -7652,45 +8199,56 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
7652
8199
|
ok: false,
|
|
7653
8200
|
error: sc.err
|
|
7654
8201
|
};
|
|
7655
|
-
const
|
|
7656
|
-
if (
|
|
7657
|
-
ok: false,
|
|
7658
|
-
error: "缺少 fs"
|
|
7659
|
-
};
|
|
8202
|
+
const prep = await readTargetOf(ctx, sc, args.path);
|
|
8203
|
+
if ("err" in prep) return targetErrOf(prep);
|
|
7660
8204
|
try {
|
|
7661
|
-
const target = await resolveTarget(ctx, sc.session, args.path);
|
|
7662
|
-
debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
|
|
7663
|
-
const info = await fs.stat(target);
|
|
7664
|
-
if (!info || info.type !== "file") return {
|
|
7665
|
-
ok: false,
|
|
7666
|
-
error: "文件不存在",
|
|
7667
|
-
resolvedPath: fs.processPath(target)
|
|
7668
|
-
};
|
|
7669
|
-
if ((info.size ?? 0) > 33554432) return {
|
|
7670
|
-
ok: false,
|
|
7671
|
-
error: "文件过大(>32MB),不支持整文件预览"
|
|
7672
|
-
};
|
|
7673
8205
|
if (args.encoding === "base64") {
|
|
7674
|
-
const bytes = await fs.readBytes(target, void 0, BINARY_READ_CAP);
|
|
8206
|
+
const bytes = await prep.fs.readBytes(prep.target, void 0, BINARY_READ_CAP);
|
|
7675
8207
|
return {
|
|
7676
8208
|
ok: true,
|
|
7677
8209
|
content: Buffer.from(bytes).toString("base64"),
|
|
7678
8210
|
size: bytes.byteLength,
|
|
7679
8211
|
encoding: "base64",
|
|
7680
8212
|
mime: binaryMimeOf(args.path),
|
|
7681
|
-
version: String(info.version ?? "")
|
|
8213
|
+
version: String(prep.info.version ?? "")
|
|
7682
8214
|
};
|
|
7683
8215
|
}
|
|
7684
|
-
if ((info.size ?? 0) > 8388608) return {
|
|
8216
|
+
if ((prep.info.size ?? 0) > 8388608) return {
|
|
7685
8217
|
ok: false,
|
|
7686
8218
|
error: "文件过大(>8MB),不支持整文件预览"
|
|
7687
8219
|
};
|
|
7688
|
-
const content = await fs.readText(target);
|
|
8220
|
+
const content = await prep.fs.readText(prep.target);
|
|
7689
8221
|
return {
|
|
7690
8222
|
ok: true,
|
|
7691
8223
|
content,
|
|
7692
8224
|
size: content.length,
|
|
7693
|
-
version: String(info.version ?? "")
|
|
8225
|
+
version: String(prep.info.version ?? "")
|
|
8226
|
+
};
|
|
8227
|
+
} catch (error) {
|
|
8228
|
+
return {
|
|
8229
|
+
ok: false,
|
|
8230
|
+
error: "读取失败:" + String(error)
|
|
8231
|
+
};
|
|
8232
|
+
}
|
|
8233
|
+
},
|
|
8234
|
+
"edrv.readBinary": async (args) => {
|
|
8235
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8236
|
+
if ("err" in sc) return {
|
|
8237
|
+
ok: false,
|
|
8238
|
+
error: sc.err
|
|
8239
|
+
};
|
|
8240
|
+
const prep = await readTargetOf(ctx, sc, args.path);
|
|
8241
|
+
if ("err" in prep) return targetErrOf(prep);
|
|
8242
|
+
try {
|
|
8243
|
+
const bytes = await prep.fs.readBytes(prep.target, void 0, BINARY_READ_CAP);
|
|
8244
|
+
return {
|
|
8245
|
+
ok: true,
|
|
8246
|
+
binary: {
|
|
8247
|
+
bytes,
|
|
8248
|
+
mime: binaryMimeOf(args.path),
|
|
8249
|
+
size: bytes.byteLength,
|
|
8250
|
+
version: String(prep.info.version ?? "")
|
|
8251
|
+
}
|
|
7694
8252
|
};
|
|
7695
8253
|
} catch (error) {
|
|
7696
8254
|
return {
|
|
@@ -8027,118 +8585,297 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8027
8585
|
};
|
|
8028
8586
|
return {
|
|
8029
8587
|
ok: true,
|
|
8030
|
-
opened: true
|
|
8588
|
+
opened: true
|
|
8589
|
+
};
|
|
8590
|
+
},
|
|
8591
|
+
"edrv.searchFiles": async (args) => {
|
|
8592
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8593
|
+
if ("err" in sc) return {
|
|
8594
|
+
ok: false,
|
|
8595
|
+
error: sc.err
|
|
8596
|
+
};
|
|
8597
|
+
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
8598
|
+
const activePaths = [];
|
|
8599
|
+
for (const record of bucket.values()) if (record.path) activePaths.push(record.path);
|
|
8600
|
+
return {
|
|
8601
|
+
ok: true,
|
|
8602
|
+
...await searcher.search({
|
|
8603
|
+
session: sc.session,
|
|
8604
|
+
cwd: sc.cwd,
|
|
8605
|
+
query: args.query,
|
|
8606
|
+
activePaths
|
|
8607
|
+
})
|
|
8608
|
+
};
|
|
8609
|
+
},
|
|
8610
|
+
"edrv.searchContent": async (args) => {
|
|
8611
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8612
|
+
if ("err" in sc) return {
|
|
8613
|
+
ok: false,
|
|
8614
|
+
error: sc.err
|
|
8615
|
+
};
|
|
8616
|
+
try {
|
|
8617
|
+
return {
|
|
8618
|
+
ok: true,
|
|
8619
|
+
...await contentSearcher.search({
|
|
8620
|
+
session: sc.session,
|
|
8621
|
+
cwd: sc.cwd,
|
|
8622
|
+
query: args.query,
|
|
8623
|
+
matchCase: args.matchCase,
|
|
8624
|
+
wholeWord: args.wholeWord,
|
|
8625
|
+
regex: args.regex,
|
|
8626
|
+
maxResults: args.maxResults,
|
|
8627
|
+
include: args.include,
|
|
8628
|
+
exclude: args.exclude
|
|
8629
|
+
})
|
|
8630
|
+
};
|
|
8631
|
+
} catch (error) {
|
|
8632
|
+
return {
|
|
8633
|
+
ok: false,
|
|
8634
|
+
error: "搜索失败:" + String(error)
|
|
8635
|
+
};
|
|
8636
|
+
}
|
|
8637
|
+
},
|
|
8638
|
+
"edrv.listDir": async (args) => {
|
|
8639
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8640
|
+
if ("err" in sc) return {
|
|
8641
|
+
ok: false,
|
|
8642
|
+
error: sc.err
|
|
8643
|
+
};
|
|
8644
|
+
const rel = normalizeRel(args.path);
|
|
8645
|
+
if (rel === null) return {
|
|
8646
|
+
ok: false,
|
|
8647
|
+
error: "路径不合法"
|
|
8648
|
+
};
|
|
8649
|
+
const res = await listDirCached(ctx, sc.cwd, rel, args.force === true);
|
|
8650
|
+
if ("error" in res) return {
|
|
8651
|
+
ok: false,
|
|
8652
|
+
error: res.error
|
|
8653
|
+
};
|
|
8654
|
+
return {
|
|
8655
|
+
ok: true,
|
|
8656
|
+
root: res.root,
|
|
8657
|
+
path: rel,
|
|
8658
|
+
entries: res.entries
|
|
8659
|
+
};
|
|
8660
|
+
},
|
|
8661
|
+
"edrv.revealInExplorer": async (args) => {
|
|
8662
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8663
|
+
if ("err" in sc) return {
|
|
8664
|
+
ok: false,
|
|
8665
|
+
error: sc.err
|
|
8666
|
+
};
|
|
8667
|
+
const fs = ctx.get("fs");
|
|
8668
|
+
if (!fs) return {
|
|
8669
|
+
ok: false,
|
|
8670
|
+
error: "缺少 fs"
|
|
8671
|
+
};
|
|
8672
|
+
try {
|
|
8673
|
+
const rel = normalizeRel(args.path);
|
|
8674
|
+
if (rel === null) return {
|
|
8675
|
+
ok: false,
|
|
8676
|
+
error: "路径不合法"
|
|
8677
|
+
};
|
|
8678
|
+
const target = await fs.resolve(rel || ".", { cwd: sc.cwd });
|
|
8679
|
+
const info = await fs.stat(target);
|
|
8680
|
+
if (!info) return {
|
|
8681
|
+
ok: false,
|
|
8682
|
+
error: "路径不存在"
|
|
8683
|
+
};
|
|
8684
|
+
const abs = fs.processPath(target);
|
|
8685
|
+
const outcome = await revealInExplorer(abs, info.type === "directory");
|
|
8686
|
+
if (!outcome.ok) return {
|
|
8687
|
+
ok: false,
|
|
8688
|
+
error: outcome.error
|
|
8689
|
+
};
|
|
8690
|
+
return {
|
|
8691
|
+
ok: true,
|
|
8692
|
+
revealed: abs
|
|
8693
|
+
};
|
|
8694
|
+
} catch (error) {
|
|
8695
|
+
return {
|
|
8696
|
+
ok: false,
|
|
8697
|
+
error: "打开失败:" + String(error)
|
|
8698
|
+
};
|
|
8699
|
+
}
|
|
8700
|
+
},
|
|
8701
|
+
"edrv.fsCreateFile": async (args) => {
|
|
8702
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8703
|
+
if ("err" in sc) return {
|
|
8704
|
+
ok: false,
|
|
8705
|
+
error: sc.err
|
|
8706
|
+
};
|
|
8707
|
+
const bad = checkNewName(args.path);
|
|
8708
|
+
if (bad) return {
|
|
8709
|
+
ok: false,
|
|
8710
|
+
error: bad
|
|
8711
|
+
};
|
|
8712
|
+
const r = await fsOpsTarget(ctx, sc.session, args.path);
|
|
8713
|
+
if ("err" in r) return {
|
|
8714
|
+
ok: false,
|
|
8715
|
+
error: r.err
|
|
8716
|
+
};
|
|
8717
|
+
try {
|
|
8718
|
+
await mkdir(dirname(r.abs), { recursive: true });
|
|
8719
|
+
await writeFile(r.abs, "", { flag: "wx" });
|
|
8720
|
+
} catch (error) {
|
|
8721
|
+
return {
|
|
8722
|
+
ok: false,
|
|
8723
|
+
error: fsErrCode(error) === "EEXIST" ? "已存在同名文件或目录" : "新建文件失败:" + String(error)
|
|
8724
|
+
};
|
|
8725
|
+
}
|
|
8726
|
+
invalidateIndex(ctx, sc.cwd, args.path);
|
|
8727
|
+
return {
|
|
8728
|
+
ok: true,
|
|
8729
|
+
path: args.path
|
|
8730
|
+
};
|
|
8731
|
+
},
|
|
8732
|
+
"edrv.fsCreateDir": async (args) => {
|
|
8733
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8734
|
+
if ("err" in sc) return {
|
|
8735
|
+
ok: false,
|
|
8736
|
+
error: sc.err
|
|
8737
|
+
};
|
|
8738
|
+
const bad = checkNewName(args.path);
|
|
8739
|
+
if (bad) return {
|
|
8740
|
+
ok: false,
|
|
8741
|
+
error: bad
|
|
8742
|
+
};
|
|
8743
|
+
const r = await fsOpsTarget(ctx, sc.session, args.path);
|
|
8744
|
+
if ("err" in r) return {
|
|
8745
|
+
ok: false,
|
|
8746
|
+
error: r.err
|
|
8747
|
+
};
|
|
8748
|
+
try {
|
|
8749
|
+
await mkdir(dirname(r.abs), { recursive: true });
|
|
8750
|
+
await mkdir(r.abs);
|
|
8751
|
+
} catch (error) {
|
|
8752
|
+
return {
|
|
8753
|
+
ok: false,
|
|
8754
|
+
error: fsErrCode(error) === "EEXIST" ? "已存在同名文件或目录" : "新建文件夹失败:" + String(error)
|
|
8755
|
+
};
|
|
8756
|
+
}
|
|
8757
|
+
invalidateIndex(ctx, sc.cwd, args.path);
|
|
8758
|
+
return {
|
|
8759
|
+
ok: true,
|
|
8760
|
+
path: args.path
|
|
8761
|
+
};
|
|
8762
|
+
},
|
|
8763
|
+
"edrv.fsRename": async (args) => {
|
|
8764
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
8765
|
+
if ("err" in sc) return {
|
|
8766
|
+
ok: false,
|
|
8767
|
+
error: sc.err
|
|
8768
|
+
};
|
|
8769
|
+
const bad = checkRenameName(args.newName);
|
|
8770
|
+
if (bad) return {
|
|
8771
|
+
ok: false,
|
|
8772
|
+
error: bad
|
|
8773
|
+
};
|
|
8774
|
+
const fromRel = normalizeRel(args.path);
|
|
8775
|
+
if (fromRel === null || fromRel === "") return {
|
|
8776
|
+
ok: false,
|
|
8777
|
+
error: "路径不合法"
|
|
8778
|
+
};
|
|
8779
|
+
const toRel = joinRelPath(parentRelOf(fromRel), args.newName);
|
|
8780
|
+
if (toRel === fromRel) return {
|
|
8781
|
+
ok: true,
|
|
8782
|
+
from: fromRel,
|
|
8783
|
+
to: fromRel
|
|
8784
|
+
};
|
|
8785
|
+
const src = await fsOpsTarget(ctx, sc.session, fromRel);
|
|
8786
|
+
if ("err" in src) return {
|
|
8787
|
+
ok: false,
|
|
8788
|
+
error: src.err
|
|
8789
|
+
};
|
|
8790
|
+
const dst = await fsOpsTarget(ctx, sc.session, toRel);
|
|
8791
|
+
if ("err" in dst) return {
|
|
8792
|
+
ok: false,
|
|
8793
|
+
error: dst.err
|
|
8794
|
+
};
|
|
8795
|
+
if (!await stat(src.abs).catch(() => null)) return {
|
|
8796
|
+
ok: false,
|
|
8797
|
+
error: "源不存在"
|
|
8798
|
+
};
|
|
8799
|
+
try {
|
|
8800
|
+
await rename(src.abs, dst.abs);
|
|
8801
|
+
} catch (error) {
|
|
8802
|
+
const code = fsErrCode(error);
|
|
8803
|
+
return {
|
|
8804
|
+
ok: false,
|
|
8805
|
+
error: code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM" ? "已存在同名文件或目录" : "重命名失败:" + String(error)
|
|
8806
|
+
};
|
|
8807
|
+
}
|
|
8808
|
+
invalidateIndex(ctx, sc.cwd, fromRel);
|
|
8809
|
+
invalidateIndex(ctx, sc.cwd, toRel);
|
|
8810
|
+
return {
|
|
8811
|
+
ok: true,
|
|
8812
|
+
from: fromRel,
|
|
8813
|
+
to: toRel
|
|
8031
8814
|
};
|
|
8032
8815
|
},
|
|
8033
|
-
"edrv.
|
|
8816
|
+
"edrv.fsDelete": async (args) => {
|
|
8034
8817
|
const sc = await requireSession(ctx, args.sessionId);
|
|
8035
8818
|
if ("err" in sc) return {
|
|
8036
8819
|
ok: false,
|
|
8037
8820
|
error: sc.err
|
|
8038
8821
|
};
|
|
8039
|
-
const
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
ok: true,
|
|
8044
|
-
...await searcher.search({
|
|
8045
|
-
session: sc.session,
|
|
8046
|
-
cwd: sc.cwd,
|
|
8047
|
-
query: args.query,
|
|
8048
|
-
activePaths
|
|
8049
|
-
})
|
|
8822
|
+
const rel = normalizeRel(args.path);
|
|
8823
|
+
if (rel === null || rel === "") return {
|
|
8824
|
+
ok: false,
|
|
8825
|
+
error: "路径不合法"
|
|
8050
8826
|
};
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
const sc = await requireSession(ctx, args.sessionId);
|
|
8054
|
-
if ("err" in sc) return {
|
|
8827
|
+
const r = await fsOpsTarget(ctx, sc.session, rel);
|
|
8828
|
+
if ("err" in r) return {
|
|
8055
8829
|
ok: false,
|
|
8056
|
-
error:
|
|
8830
|
+
error: r.err
|
|
8057
8831
|
};
|
|
8058
8832
|
try {
|
|
8059
|
-
|
|
8060
|
-
ok: true,
|
|
8061
|
-
...await contentSearcher.search({
|
|
8062
|
-
session: sc.session,
|
|
8063
|
-
cwd: sc.cwd,
|
|
8064
|
-
query: args.query,
|
|
8065
|
-
matchCase: args.matchCase,
|
|
8066
|
-
wholeWord: args.wholeWord,
|
|
8067
|
-
regex: args.regex,
|
|
8068
|
-
maxResults: args.maxResults,
|
|
8069
|
-
include: args.include,
|
|
8070
|
-
exclude: args.exclude
|
|
8071
|
-
})
|
|
8072
|
-
};
|
|
8833
|
+
await rm(r.abs, { recursive: true });
|
|
8073
8834
|
} catch (error) {
|
|
8074
8835
|
return {
|
|
8075
8836
|
ok: false,
|
|
8076
|
-
error: "
|
|
8837
|
+
error: "删除失败:" + String(error)
|
|
8077
8838
|
};
|
|
8078
8839
|
}
|
|
8840
|
+
invalidateIndex(ctx, sc.cwd, rel);
|
|
8841
|
+
return {
|
|
8842
|
+
ok: true,
|
|
8843
|
+
path: rel
|
|
8844
|
+
};
|
|
8079
8845
|
},
|
|
8080
|
-
"edrv.
|
|
8846
|
+
"edrv.fsCopy": async (args) => {
|
|
8081
8847
|
const sc = await requireSession(ctx, args.sessionId);
|
|
8082
8848
|
if ("err" in sc) return {
|
|
8083
8849
|
ok: false,
|
|
8084
8850
|
error: sc.err
|
|
8085
8851
|
};
|
|
8086
|
-
const
|
|
8087
|
-
if (
|
|
8088
|
-
ok: false,
|
|
8089
|
-
error: "路径不合法"
|
|
8090
|
-
};
|
|
8091
|
-
const res = await listDirCached(ctx, sc.cwd, rel, args.force === true);
|
|
8092
|
-
if ("error" in res) return {
|
|
8852
|
+
const r = await fsTransfer(ctx, sc, args, "copy");
|
|
8853
|
+
if ("err" in r) return {
|
|
8093
8854
|
ok: false,
|
|
8094
|
-
error:
|
|
8855
|
+
error: r.err
|
|
8095
8856
|
};
|
|
8096
8857
|
return {
|
|
8097
8858
|
ok: true,
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
entries: res.entries
|
|
8859
|
+
from: r.from,
|
|
8860
|
+
to: r.to
|
|
8101
8861
|
};
|
|
8102
8862
|
},
|
|
8103
|
-
"edrv.
|
|
8863
|
+
"edrv.fsMove": async (args) => {
|
|
8104
8864
|
const sc = await requireSession(ctx, args.sessionId);
|
|
8105
8865
|
if ("err" in sc) return {
|
|
8106
8866
|
ok: false,
|
|
8107
8867
|
error: sc.err
|
|
8108
8868
|
};
|
|
8109
|
-
const
|
|
8110
|
-
if (
|
|
8869
|
+
const r = await fsTransfer(ctx, sc, args, "move");
|
|
8870
|
+
if ("err" in r) return {
|
|
8111
8871
|
ok: false,
|
|
8112
|
-
error:
|
|
8872
|
+
error: r.err
|
|
8873
|
+
};
|
|
8874
|
+
return {
|
|
8875
|
+
ok: true,
|
|
8876
|
+
from: r.from,
|
|
8877
|
+
to: r.to
|
|
8113
8878
|
};
|
|
8114
|
-
try {
|
|
8115
|
-
const rel = normalizeRel(args.path);
|
|
8116
|
-
if (rel === null) return {
|
|
8117
|
-
ok: false,
|
|
8118
|
-
error: "路径不合法"
|
|
8119
|
-
};
|
|
8120
|
-
const target = await fs.resolve(rel || ".", { cwd: sc.cwd });
|
|
8121
|
-
const info = await fs.stat(target);
|
|
8122
|
-
if (!info) return {
|
|
8123
|
-
ok: false,
|
|
8124
|
-
error: "路径不存在"
|
|
8125
|
-
};
|
|
8126
|
-
const abs = fs.processPath(target);
|
|
8127
|
-
const outcome = await revealInExplorer(abs, info.type === "directory");
|
|
8128
|
-
if (!outcome.ok) return {
|
|
8129
|
-
ok: false,
|
|
8130
|
-
error: outcome.error
|
|
8131
|
-
};
|
|
8132
|
-
return {
|
|
8133
|
-
ok: true,
|
|
8134
|
-
revealed: abs
|
|
8135
|
-
};
|
|
8136
|
-
} catch (error) {
|
|
8137
|
-
return {
|
|
8138
|
-
ok: false,
|
|
8139
|
-
error: "打开失败:" + String(error)
|
|
8140
|
-
};
|
|
8141
|
-
}
|
|
8142
8879
|
},
|
|
8143
8880
|
"mcp.list": async () => ({
|
|
8144
8881
|
ok: true,
|
|
@@ -8260,39 +8997,32 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8260
8997
|
}
|
|
8261
8998
|
},
|
|
8262
8999
|
"vscode.fileOpenSettingsGet": async () => {
|
|
8263
|
-
const
|
|
8264
|
-
const value =
|
|
9000
|
+
const section = sectionOf(ctx.get("settings"), FILE_OPEN_SETTINGS_NS);
|
|
9001
|
+
const value = section?.value;
|
|
8265
9002
|
return {
|
|
8266
9003
|
ok: true,
|
|
8267
9004
|
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? "auto"),
|
|
8268
9005
|
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
8269
|
-
revision:
|
|
9006
|
+
revision: section?.revision
|
|
8270
9007
|
};
|
|
8271
9008
|
},
|
|
8272
9009
|
"vscode.fileOpenSettingsUpdate": async (args) => {
|
|
8273
9010
|
const settings = ctx.get("settings");
|
|
8274
|
-
|
|
9011
|
+
const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
|
|
9012
|
+
if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
|
|
9013
|
+
const result = await updateSection(settings, FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
|
|
9014
|
+
if (!result.ok) return {
|
|
8275
9015
|
ok: false,
|
|
8276
|
-
error: "设置服务不可用"
|
|
9016
|
+
error: result.error ?? "设置服务不可用"
|
|
9017
|
+
};
|
|
9018
|
+
const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS);
|
|
9019
|
+
const value = section?.value;
|
|
9020
|
+
return {
|
|
9021
|
+
ok: true,
|
|
9022
|
+
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
|
|
9023
|
+
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
9024
|
+
revision: section?.revision
|
|
8277
9025
|
};
|
|
8278
|
-
try {
|
|
8279
|
-
const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
|
|
8280
|
-
if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
|
|
8281
|
-
await settings.update(FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
|
|
8282
|
-
const descriptor = settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
|
|
8283
|
-
const value = descriptor?.value;
|
|
8284
|
-
return {
|
|
8285
|
-
ok: true,
|
|
8286
|
-
fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
|
|
8287
|
-
integrationBaseUrl: await integrationBaseUrlOf(ctx),
|
|
8288
|
-
revision: descriptor?.revision
|
|
8289
|
-
};
|
|
8290
|
-
} catch (error) {
|
|
8291
|
-
return {
|
|
8292
|
-
ok: false,
|
|
8293
|
-
error: String(error)
|
|
8294
|
-
};
|
|
8295
|
-
}
|
|
8296
9026
|
},
|
|
8297
9027
|
"compat": async () => ({
|
|
8298
9028
|
ok: true,
|
|
@@ -8456,6 +9186,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8456
9186
|
const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home));
|
|
8457
9187
|
const active = activeSessionIds(ctx);
|
|
8458
9188
|
markActiveSessions(inventory.sessions, active);
|
|
9189
|
+
markOfficialFlags(inventory.sessions, ctx);
|
|
8459
9190
|
return {
|
|
8460
9191
|
ok: true,
|
|
8461
9192
|
...inventory,
|
|
@@ -8524,13 +9255,15 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
8524
9255
|
"edrv.perf.restore": async (args) => {
|
|
8525
9256
|
try {
|
|
8526
9257
|
const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId);
|
|
8527
|
-
|
|
8528
|
-
ok: true,
|
|
8529
|
-
restored: true
|
|
8530
|
-
} : {
|
|
9258
|
+
if (!result.ok) return {
|
|
8531
9259
|
ok: false,
|
|
8532
9260
|
error: result.error ?? "恢复失败"
|
|
8533
9261
|
};
|
|
9262
|
+
await unarchiveOfficial(ctx, args.sessionId);
|
|
9263
|
+
return {
|
|
9264
|
+
ok: true,
|
|
9265
|
+
restored: true
|
|
9266
|
+
};
|
|
8534
9267
|
} catch (error) {
|
|
8535
9268
|
return {
|
|
8536
9269
|
ok: false,
|
|
@@ -9243,6 +9976,9 @@ function createLspClient(transport, events = {}) {
|
|
|
9243
9976
|
workspaceSymbol: false,
|
|
9244
9977
|
hover: false,
|
|
9245
9978
|
semanticTokens: false,
|
|
9979
|
+
completion: false,
|
|
9980
|
+
completionResolve: false,
|
|
9981
|
+
signatureHelp: false,
|
|
9246
9982
|
semanticTokenTypes: [],
|
|
9247
9983
|
semanticTokenModifiers: []
|
|
9248
9984
|
};
|
|
@@ -9376,6 +10112,23 @@ function createLspClient(transport, events = {}) {
|
|
|
9376
10112
|
references: { dynamicRegistration: false },
|
|
9377
10113
|
documentSymbol: { dynamicRegistration: false },
|
|
9378
10114
|
hover: { dynamicRegistration: false },
|
|
10115
|
+
completion: {
|
|
10116
|
+
dynamicRegistration: false,
|
|
10117
|
+
contextSupport: true,
|
|
10118
|
+
completionItem: {
|
|
10119
|
+
snippetSupport: true,
|
|
10120
|
+
documentationFormat: ["markdown", "plaintext"],
|
|
10121
|
+
resolveSupport: { properties: [
|
|
10122
|
+
"documentation",
|
|
10123
|
+
"detail",
|
|
10124
|
+
"additionalTextEdits"
|
|
10125
|
+
] }
|
|
10126
|
+
}
|
|
10127
|
+
},
|
|
10128
|
+
signatureHelp: {
|
|
10129
|
+
dynamicRegistration: false,
|
|
10130
|
+
signatureInformation: { documentationFormat: ["markdown", "plaintext"] }
|
|
10131
|
+
},
|
|
9379
10132
|
semanticTokens: {
|
|
9380
10133
|
dynamicRegistration: false,
|
|
9381
10134
|
requests: {
|
|
@@ -9398,6 +10151,9 @@ function createLspClient(transport, events = {}) {
|
|
|
9398
10151
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
9399
10152
|
hover: Boolean(caps.hoverProvider),
|
|
9400
10153
|
semanticTokens: Boolean(caps.semanticTokensProvider),
|
|
10154
|
+
completion: Boolean(caps.completionProvider),
|
|
10155
|
+
completionResolve: Boolean(caps.completionProvider?.resolveProvider),
|
|
10156
|
+
signatureHelp: Boolean(caps.signatureHelpProvider),
|
|
9401
10157
|
semanticTokenTypes: Array.isArray(legend?.tokenTypes) ? legend.tokenTypes.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
9402
10158
|
semanticTokenModifiers: Array.isArray(legend?.tokenModifiers) ? legend.tokenModifiers.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
9403
10159
|
};
|
|
@@ -9599,6 +10355,9 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
9599
10355
|
workspaceSymbol: false,
|
|
9600
10356
|
hover: false,
|
|
9601
10357
|
semanticTokens: false,
|
|
10358
|
+
completion: false,
|
|
10359
|
+
completionResolve: false,
|
|
10360
|
+
signatureHelp: false,
|
|
9602
10361
|
semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
9603
10362
|
semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
|
|
9604
10363
|
};
|
|
@@ -9855,6 +10614,68 @@ function createLspServer(spec, root, languageId, logger) {
|
|
|
9855
10614
|
if (!result || !result.contents) return null;
|
|
9856
10615
|
return { contents: stringifyHoverContents(result.contents) };
|
|
9857
10616
|
},
|
|
10617
|
+
/**
|
|
10618
|
+
* 查询补全列表(textDocument/completion)。
|
|
10619
|
+
*
|
|
10620
|
+
* 列表阶段**剥离 documentation**:EmmyLua 把文档挂在 resolve 阶段(我方 initialize 已声明
|
|
10621
|
+
* resolveSupport),但并非所有服务器都遵守;每条都带长文档会让单次 RPC 载荷暴涨。
|
|
10622
|
+
* `data` 原样透传,resolve 时由服务器回认该条目。
|
|
10623
|
+
* @author ddj 2026年09月22号
|
|
10624
|
+
* @param path 工作区相对路径
|
|
10625
|
+
* @param line 0-based 行
|
|
10626
|
+
* @param character 0-based 列
|
|
10627
|
+
* @param context LSP CompletionContext(触发字符/触发类型)
|
|
10628
|
+
* @returns 归一化补全列表;不支持或文档未打开时 null
|
|
10629
|
+
*/
|
|
10630
|
+
async completion(path, line, character, context) {
|
|
10631
|
+
const doc = docs.get(path);
|
|
10632
|
+
if (!doc || !await readyWait || !capabilities.completion) return null;
|
|
10633
|
+
const params = {
|
|
10634
|
+
textDocument: { uri: doc.uri },
|
|
10635
|
+
position: {
|
|
10636
|
+
line,
|
|
10637
|
+
character
|
|
10638
|
+
}
|
|
10639
|
+
};
|
|
10640
|
+
if (context) params.context = context;
|
|
10641
|
+
return normCompletion(await ensureClient().request("textDocument/completion", params));
|
|
10642
|
+
},
|
|
10643
|
+
/**
|
|
10644
|
+
* 补全项惰性补全(completionItem/resolve):按 `data` 取回 documentation/detail/additionalTextEdits。
|
|
10645
|
+
*
|
|
10646
|
+
* 必须回传**完整条目**(服务器按 data + label 精确匹配),只发 label/data 可能被服务器忽略。
|
|
10647
|
+
* @author ddj 2026年09月22号
|
|
10648
|
+
* @param path 工作区相对路径
|
|
10649
|
+
* @param item 列表阶段返回的补全项
|
|
10650
|
+
* @returns 补全后的条目;服务器不支持时原样返回
|
|
10651
|
+
*/
|
|
10652
|
+
async resolveCompletion(path, item) {
|
|
10653
|
+
if (!item || typeof item.label !== "string") return null;
|
|
10654
|
+
if (!await readyWait || !capabilities.completionResolve) return item;
|
|
10655
|
+
return normCompItem(await ensureClient().request("completionItem/resolve", toLspCompItem(item))) ?? item;
|
|
10656
|
+
},
|
|
10657
|
+
/**
|
|
10658
|
+
* 查询签名帮助(textDocument/signatureHelp)。
|
|
10659
|
+
*
|
|
10660
|
+
* activeSignature/activeParameter 做边界裁剪:服务器可能给越界值(多签名时按实参推进出错),
|
|
10661
|
+
* Monaco 拿到越界索引会取不到签名而不渲染 —— 裁剪后至少显示首个签名。
|
|
10662
|
+
* @author ddj 2026年09月22号
|
|
10663
|
+
* @param path 工作区相对路径
|
|
10664
|
+
* @param line 0-based 行
|
|
10665
|
+
* @param character 0-based 列
|
|
10666
|
+
* @returns 归一化签名帮助;无签名/不支持时 null
|
|
10667
|
+
*/
|
|
10668
|
+
async signatureHelp(path, line, character) {
|
|
10669
|
+
const doc = docs.get(path);
|
|
10670
|
+
if (!doc || !await readyWait || !capabilities.signatureHelp) return null;
|
|
10671
|
+
return toSignatureHelp(await ensureClient().request("textDocument/signatureHelp", {
|
|
10672
|
+
textDocument: { uri: doc.uri },
|
|
10673
|
+
position: {
|
|
10674
|
+
line,
|
|
10675
|
+
character
|
|
10676
|
+
}
|
|
10677
|
+
}));
|
|
10678
|
+
},
|
|
9858
10679
|
/** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
|
|
9859
10680
|
async semanticTokens(path) {
|
|
9860
10681
|
const doc = docs.get(path);
|
|
@@ -9960,8 +10781,205 @@ function normalizeSymbol(item) {
|
|
|
9960
10781
|
}
|
|
9961
10782
|
return symbol;
|
|
9962
10783
|
}
|
|
9963
|
-
/** hover
|
|
9964
|
-
function
|
|
10784
|
+
/** hover/markdown 内容 → 纯文本(string | MarkupContent | MarkedString[] | MarkedString)。 */
|
|
10785
|
+
function plainText(value) {
|
|
10786
|
+
if (typeof value === "string") return value || void 0;
|
|
10787
|
+
if (Array.isArray(value)) {
|
|
10788
|
+
const parts = value.map(plainText).filter((part) => Boolean(part));
|
|
10789
|
+
return parts.length ? parts.join("\n\n") : void 0;
|
|
10790
|
+
}
|
|
10791
|
+
if (value && typeof value === "object") {
|
|
10792
|
+
const text = value.value;
|
|
10793
|
+
if (typeof text === "string") return text || void 0;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
/**
|
|
10797
|
+
* 归一化补全文本编辑范围(单 range 与 insert/replace 双 range 两种形态)。
|
|
10798
|
+
*
|
|
10799
|
+
* 两种形态必须都认:只认一种会让采用另一种形态的服务器补全落点错位(替换范围算错,
|
|
10800
|
+
* 已输入的前缀被重复插入)。双 range 时 insert 用于「保留前缀插入」,replace 用于覆盖。
|
|
10801
|
+
* @author ddj 2026年09月22号
|
|
10802
|
+
* @param raw 原始 textEdit
|
|
10803
|
+
* @returns 归一化文本编辑;无有效范围或文本时 null
|
|
10804
|
+
*/
|
|
10805
|
+
function normalizeTextEdit(raw) {
|
|
10806
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10807
|
+
const obj = raw;
|
|
10808
|
+
const newText = typeof obj.newText === "string" ? obj.newText : "";
|
|
10809
|
+
if (!newText) return null;
|
|
10810
|
+
const single = normalizeRange(obj.range);
|
|
10811
|
+
if (single) return {
|
|
10812
|
+
range: single,
|
|
10813
|
+
newText
|
|
10814
|
+
};
|
|
10815
|
+
const insert = normalizeRange(obj.insert);
|
|
10816
|
+
const replace = normalizeRange(obj.replace);
|
|
10817
|
+
if (!insert && !replace) return null;
|
|
10818
|
+
return {
|
|
10819
|
+
insert: insert ?? replace,
|
|
10820
|
+
replace: replace ?? insert,
|
|
10821
|
+
newText
|
|
10822
|
+
};
|
|
10823
|
+
}
|
|
10824
|
+
/** 归一化 LSP Range(缺字段返回 null,避免产出畸形范围)。 */
|
|
10825
|
+
function normalizeRange(raw) {
|
|
10826
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10827
|
+
const r = raw;
|
|
10828
|
+
if (!r.start || !r.end) return null;
|
|
10829
|
+
return {
|
|
10830
|
+
start: {
|
|
10831
|
+
line: toInt(r.start.line),
|
|
10832
|
+
character: toInt(r.start.character)
|
|
10833
|
+
},
|
|
10834
|
+
end: {
|
|
10835
|
+
line: toInt(r.end.line),
|
|
10836
|
+
character: toInt(r.end.character)
|
|
10837
|
+
}
|
|
10838
|
+
};
|
|
10839
|
+
}
|
|
10840
|
+
/**
|
|
10841
|
+
* 归一化单个补全项。
|
|
10842
|
+
* @author ddj 2026年09月22号
|
|
10843
|
+
* @param raw 原始条目
|
|
10844
|
+
* @returns 归一化条目;缺 label 时 null
|
|
10845
|
+
*/
|
|
10846
|
+
function normCompItem(raw) {
|
|
10847
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10848
|
+
const obj = raw;
|
|
10849
|
+
const label = typeof obj.label === "string" ? obj.label : "";
|
|
10850
|
+
if (!label) return null;
|
|
10851
|
+
const item = { label };
|
|
10852
|
+
if (typeof obj.kind === "number") item.kind = toInt(obj.kind);
|
|
10853
|
+
if (typeof obj.detail === "string" && obj.detail) item.detail = obj.detail;
|
|
10854
|
+
const documentation = plainText(obj.documentation);
|
|
10855
|
+
if (documentation) item.documentation = documentation;
|
|
10856
|
+
if (typeof obj.insertText === "string") item.insertText = obj.insertText;
|
|
10857
|
+
if (typeof obj.insertTextFormat === "number") item.insertTextFormat = toInt(obj.insertTextFormat);
|
|
10858
|
+
const textEdit = normalizeTextEdit(obj.textEdit);
|
|
10859
|
+
if (textEdit) item.textEdit = textEdit;
|
|
10860
|
+
if (obj.data !== void 0) item.data = obj.data;
|
|
10861
|
+
if (typeof obj.sortText === "string") item.sortText = obj.sortText;
|
|
10862
|
+
if (typeof obj.filterText === "string") item.filterText = obj.filterText;
|
|
10863
|
+
if (obj.preselect === true) item.preselect = true;
|
|
10864
|
+
if (Array.isArray(obj.commitCharacters)) {
|
|
10865
|
+
const chars = obj.commitCharacters.filter((c) => typeof c === "string");
|
|
10866
|
+
if (chars.length) item.commitCharacters = chars;
|
|
10867
|
+
}
|
|
10868
|
+
const tags = Array.isArray(obj.tags) ? obj.tags : [];
|
|
10869
|
+
if (obj.deprecated === true || tags.some((tag) => toInt(tag) === 1)) item.deprecated = true;
|
|
10870
|
+
if (Array.isArray(obj.additionalTextEdits)) {
|
|
10871
|
+
const edits = obj.additionalTextEdits.map((edit) => {
|
|
10872
|
+
const range = normalizeRange(edit?.range);
|
|
10873
|
+
const text = edit?.newText;
|
|
10874
|
+
if (!range || typeof text !== "string") return null;
|
|
10875
|
+
return {
|
|
10876
|
+
range,
|
|
10877
|
+
newText: text
|
|
10878
|
+
};
|
|
10879
|
+
}).filter((edit) => edit !== null);
|
|
10880
|
+
if (edits.length) item.additionalTextEdits = edits;
|
|
10881
|
+
}
|
|
10882
|
+
return item;
|
|
10883
|
+
}
|
|
10884
|
+
/**
|
|
10885
|
+
* 归一化补全结果(CompletionList | CompletionItem[])。
|
|
10886
|
+
*
|
|
10887
|
+
* 两种顶层形态都要认:`{ isIncomplete, items }` 与裸数组。裸数组是最常见形态,
|
|
10888
|
+
* 只认 CompletionList 会让「补全列表恒为空」且无报错。
|
|
10889
|
+
* @author ddj 2026年09月22号
|
|
10890
|
+
* @param result 原始响应
|
|
10891
|
+
* @returns 归一化列表;无效时 null
|
|
10892
|
+
*/
|
|
10893
|
+
function normCompletion(result) {
|
|
10894
|
+
if (result == null) return null;
|
|
10895
|
+
const list = Array.isArray(result) ? {
|
|
10896
|
+
items: result,
|
|
10897
|
+
isIncomplete: false
|
|
10898
|
+
} : result;
|
|
10899
|
+
if (!Array.isArray(list.items)) return null;
|
|
10900
|
+
return {
|
|
10901
|
+
items: list.items.map(normCompItem).filter((item) => item !== null),
|
|
10902
|
+
incomplete: list.isIncomplete === true
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10905
|
+
/**
|
|
10906
|
+
* 补全项 → LSP 载荷(resolve 回传用)。
|
|
10907
|
+
* 只回传协议字段,剔除 client 侧衍生字段;`data` 必须原样带上,服务器靠它认条目。
|
|
10908
|
+
* @author ddj 2026年09月22号
|
|
10909
|
+
* @param item 归一化条目
|
|
10910
|
+
* @returns LSP CompletionItem 载荷
|
|
10911
|
+
*/
|
|
10912
|
+
function toLspCompItem(item) {
|
|
10913
|
+
const out = { label: item.label };
|
|
10914
|
+
if (item.kind !== void 0) out.kind = item.kind;
|
|
10915
|
+
if (item.detail !== void 0) out.detail = item.detail;
|
|
10916
|
+
if (item.documentation !== void 0) out.documentation = item.documentation;
|
|
10917
|
+
if (item.insertText !== void 0) out.insertText = item.insertText;
|
|
10918
|
+
if (item.insertTextFormat !== void 0) out.insertTextFormat = item.insertTextFormat;
|
|
10919
|
+
if (item.data !== void 0) out.data = item.data;
|
|
10920
|
+
if (item.sortText !== void 0) out.sortText = item.sortText;
|
|
10921
|
+
if (item.filterText !== void 0) out.filterText = item.filterText;
|
|
10922
|
+
if (item.textEdit) out.textEdit = item.textEdit.range ? {
|
|
10923
|
+
range: item.textEdit.range,
|
|
10924
|
+
newText: item.textEdit.newText
|
|
10925
|
+
} : item.textEdit;
|
|
10926
|
+
if (item.additionalTextEdits) out.additionalTextEdits = item.additionalTextEdits;
|
|
10927
|
+
if (item.commitCharacters) out.commitCharacters = item.commitCharacters;
|
|
10928
|
+
return out;
|
|
10929
|
+
}
|
|
10930
|
+
/** 归一化单个签名参数(label 支持字符串与 [start, end] 元组,两者原样透传)。 */
|
|
10931
|
+
function toSignatureParam(raw) {
|
|
10932
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10933
|
+
const obj = raw;
|
|
10934
|
+
const tuple = Array.isArray(obj.label) && obj.label.length >= 2 ? [toInt(obj.label[0]), toInt(obj.label[1])] : null;
|
|
10935
|
+
if (typeof obj.label !== "string" && !tuple) return null;
|
|
10936
|
+
const param = { label: typeof obj.label === "string" ? obj.label : tuple };
|
|
10937
|
+
const documentation = plainText(obj.documentation);
|
|
10938
|
+
if (documentation) param.documentation = documentation;
|
|
10939
|
+
return param;
|
|
10940
|
+
}
|
|
10941
|
+
/** 归一化单个签名。 */
|
|
10942
|
+
function toSignature(raw) {
|
|
10943
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10944
|
+
const obj = raw;
|
|
10945
|
+
if (typeof obj.label !== "string") return null;
|
|
10946
|
+
const signature = {
|
|
10947
|
+
label: obj.label,
|
|
10948
|
+
parameters: []
|
|
10949
|
+
};
|
|
10950
|
+
const documentation = plainText(obj.documentation);
|
|
10951
|
+
if (documentation) signature.documentation = documentation;
|
|
10952
|
+
if (Array.isArray(obj.parameters)) signature.parameters = obj.parameters.map(toSignatureParam).filter((param) => param !== null);
|
|
10953
|
+
if (typeof obj.activeParameter === "number") signature.activeParameter = toInt(obj.activeParameter);
|
|
10954
|
+
return signature;
|
|
10955
|
+
}
|
|
10956
|
+
/**
|
|
10957
|
+
* 归一化签名帮助,并裁剪越界的 activeSignature / activeParameter。
|
|
10958
|
+
*
|
|
10959
|
+
* 为什么必须裁:Monaco 用这两个索引直接取签名与参数,越界时取不到签名就整个浮窗不渲染;
|
|
10960
|
+
* 服务器在「实参刚敲下逗号」等边界时刻常给超前一位的索引。裁剪后至少显示首个签名/末个参数,
|
|
10961
|
+
* 观感是「高亮没跟上」,而不是「参数提示整个不见」。
|
|
10962
|
+
* @author ddj 2026年09月22号
|
|
10963
|
+
* @param result 原始响应
|
|
10964
|
+
* @returns 归一化结果;无有效签名时 null
|
|
10965
|
+
*/
|
|
10966
|
+
function toSignatureHelp(result) {
|
|
10967
|
+
if (!result || typeof result !== "object") return null;
|
|
10968
|
+
const obj = result;
|
|
10969
|
+
if (!Array.isArray(obj.signatures)) return null;
|
|
10970
|
+
const signatures = obj.signatures.map(toSignature).filter((signature) => signature !== null);
|
|
10971
|
+
if (!signatures.length) return null;
|
|
10972
|
+
const activeSignature = Math.min(Math.max(0, toInt(obj.activeSignature)), signatures.length - 1);
|
|
10973
|
+
const current = signatures[activeSignature];
|
|
10974
|
+
const rawActive = typeof obj.activeParameter === "number" ? toInt(obj.activeParameter) : current.activeParameter ?? 0;
|
|
10975
|
+
const count = current.parameters.length;
|
|
10976
|
+
return {
|
|
10977
|
+
signatures,
|
|
10978
|
+
activeSignature,
|
|
10979
|
+
activeParameter: count > 0 ? Math.min(Math.max(0, rawActive), count - 1) : 0
|
|
10980
|
+
};
|
|
10981
|
+
}
|
|
10982
|
+
/** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */ function stringifyHoverContents(contents) {
|
|
9965
10983
|
if (typeof contents === "string") return [contents];
|
|
9966
10984
|
if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean);
|
|
9967
10985
|
if (contents && typeof contents === "object") {
|
|
@@ -11870,6 +12888,99 @@ function createLspRpc(deps) {
|
|
|
11870
12888
|
};
|
|
11871
12889
|
}
|
|
11872
12890
|
},
|
|
12891
|
+
"edrv.lsp.completion": async (args) => {
|
|
12892
|
+
const lang = langOfPath(args.path);
|
|
12893
|
+
if (!lang) return {
|
|
12894
|
+
ok: true,
|
|
12895
|
+
completions: void 0
|
|
12896
|
+
};
|
|
12897
|
+
const sc = await rootOf(args.sessionId);
|
|
12898
|
+
if ("err" in sc) return {
|
|
12899
|
+
ok: false,
|
|
12900
|
+
error: sc.err
|
|
12901
|
+
};
|
|
12902
|
+
try {
|
|
12903
|
+
const server = serverOf(sc.root, lang);
|
|
12904
|
+
if (!server) return {
|
|
12905
|
+
ok: true,
|
|
12906
|
+
completions: void 0
|
|
12907
|
+
};
|
|
12908
|
+
const completions = await server.completion(wsPath(sc.root, args.path), args.position.line, args.position.character, args.context);
|
|
12909
|
+
if (!completions) return {
|
|
12910
|
+
ok: true,
|
|
12911
|
+
completions: void 0
|
|
12912
|
+
};
|
|
12913
|
+
return {
|
|
12914
|
+
ok: true,
|
|
12915
|
+
completions: completions.items.length > 500 ? {
|
|
12916
|
+
...completions,
|
|
12917
|
+
items: completions.items.slice(0, 500),
|
|
12918
|
+
truncated: true
|
|
12919
|
+
} : completions
|
|
12920
|
+
};
|
|
12921
|
+
} catch (error) {
|
|
12922
|
+
return {
|
|
12923
|
+
ok: false,
|
|
12924
|
+
error: "LSP 补全查询失败:" + String(error)
|
|
12925
|
+
};
|
|
12926
|
+
}
|
|
12927
|
+
},
|
|
12928
|
+
"edrv.lsp.resolveCompletion": async (args) => {
|
|
12929
|
+
const lang = langOfPath(args.path);
|
|
12930
|
+
if (!lang) return {
|
|
12931
|
+
ok: true,
|
|
12932
|
+
item: void 0
|
|
12933
|
+
};
|
|
12934
|
+
const sc = await rootOf(args.sessionId);
|
|
12935
|
+
if ("err" in sc) return {
|
|
12936
|
+
ok: false,
|
|
12937
|
+
error: sc.err
|
|
12938
|
+
};
|
|
12939
|
+
try {
|
|
12940
|
+
const server = serverOf(sc.root, lang);
|
|
12941
|
+
if (!server) return {
|
|
12942
|
+
ok: true,
|
|
12943
|
+
item: args.item
|
|
12944
|
+
};
|
|
12945
|
+
return {
|
|
12946
|
+
ok: true,
|
|
12947
|
+
item: await server.resolveCompletion(wsPath(sc.root, args.path), args.item) ?? void 0
|
|
12948
|
+
};
|
|
12949
|
+
} catch (error) {
|
|
12950
|
+
return {
|
|
12951
|
+
ok: false,
|
|
12952
|
+
error: "LSP 补全解析失败:" + String(error)
|
|
12953
|
+
};
|
|
12954
|
+
}
|
|
12955
|
+
},
|
|
12956
|
+
"edrv.lsp.signatureHelp": async (args) => {
|
|
12957
|
+
const lang = langOfPath(args.path);
|
|
12958
|
+
if (!lang) return {
|
|
12959
|
+
ok: true,
|
|
12960
|
+
signatureHelp: void 0
|
|
12961
|
+
};
|
|
12962
|
+
const sc = await rootOf(args.sessionId);
|
|
12963
|
+
if ("err" in sc) return {
|
|
12964
|
+
ok: false,
|
|
12965
|
+
error: sc.err
|
|
12966
|
+
};
|
|
12967
|
+
try {
|
|
12968
|
+
const server = serverOf(sc.root, lang);
|
|
12969
|
+
if (!server) return {
|
|
12970
|
+
ok: true,
|
|
12971
|
+
signatureHelp: void 0
|
|
12972
|
+
};
|
|
12973
|
+
return {
|
|
12974
|
+
ok: true,
|
|
12975
|
+
signatureHelp: await server.signatureHelp(wsPath(sc.root, args.path), args.position.line, args.position.character) ?? void 0
|
|
12976
|
+
};
|
|
12977
|
+
} catch (error) {
|
|
12978
|
+
return {
|
|
12979
|
+
ok: false,
|
|
12980
|
+
error: "LSP 签名帮助查询失败:" + String(error)
|
|
12981
|
+
};
|
|
12982
|
+
}
|
|
12983
|
+
},
|
|
11873
12984
|
"edrv.lsp.semanticTokens": async (args) => {
|
|
11874
12985
|
const lang = langOfPath(args.path);
|
|
11875
12986
|
if (!lang) return {
|
|
@@ -16055,6 +17166,17 @@ const inject = [
|
|
|
16055
17166
|
"agents"
|
|
16056
17167
|
];
|
|
16057
17168
|
/**
|
|
17169
|
+
* 插件 Config schema(cordis 从 module 导出读 `plugin.Config` 做启动校验;
|
|
17170
|
+
* DSH 0.1.7 起设置页按此 schema 自动生成表单,设置值 = profile 插件配置)。
|
|
17171
|
+
* 字段集与 installOpenSettingsSection 的 section schema 同源(buildSettingsSchema),
|
|
17172
|
+
* 全字段带默认值——undefined/空配置经 schemastery 校验自动填充(rc/alpha 两代
|
|
17173
|
+
* cordis 均读该导出,实测 4.0.0-rc.8 与 0.1.7 行为一致,校验必过)。
|
|
17174
|
+
* 运行时解析:@deepseek-ai/schemastery 经插件自身 node_modules(link/dev/npm 安装
|
|
17175
|
+
* 三形态均在 peer/dev 依赖面内,先于宿主树命中)。
|
|
17176
|
+
* @author ddj 2026年09月22号
|
|
17177
|
+
*/
|
|
17178
|
+
const Config = buildSettingsSchema(z);
|
|
17179
|
+
/**
|
|
16058
17180
|
* 装配插件:挂事件监听、注册路由、安装兼容层。
|
|
16059
17181
|
* @author ddj 2026年08月20号
|
|
16060
17182
|
* @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
|
|
@@ -16144,6 +17266,6 @@ async function logCompatSummary(ctx, warnings) {
|
|
|
16144
17266
|
}
|
|
16145
17267
|
}
|
|
16146
17268
|
//#endregion
|
|
16147
|
-
export { apply, inject, name };
|
|
17269
|
+
export { Config, apply, inject, name };
|
|
16148
17270
|
|
|
16149
17271
|
//# sourceMappingURL=index.js.map
|