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.
Files changed (57) hide show
  1. package/README.md +40 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +9317 -7494
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1295 -173
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +3 -1
  11. package/src/client/compat.ts +134 -4
  12. package/src/client/copyText.ts +28 -0
  13. package/src/client/fileClipboard.ts +46 -0
  14. package/src/client/imagePreview.ts +76 -2
  15. package/src/client/index.ts +68 -39
  16. package/src/client/monaco/lsp/lspClient.ts +46 -0
  17. package/src/client/monaco/lsp/providers.ts +245 -1
  18. package/src/client/nativeOpenStore.ts +91 -0
  19. package/src/client/officialSidebar.ts +11 -45
  20. package/src/client/openWithDialog.ts +66 -0
  21. package/src/client/outline/index.ts +1 -1
  22. package/src/client/pdf/pdfPanel.ts +18 -0
  23. package/src/client/searchSeed.ts +36 -0
  24. package/src/client/settingsContext.ts +6 -4
  25. package/src/client/sidebar/contextMenu.ts +51 -10
  26. package/src/client/sidebar/menuItems.ts +473 -50
  27. package/src/client/sidebar/panels/FileExplorer.ts +1 -1
  28. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  29. package/src/client/sidebar/panels/SvnPanel.ts +1 -1
  30. package/src/client/sidebar/panels/index.ts +1 -1
  31. package/src/client/sidebar/types.ts +6 -0
  32. package/src/client/styles/editor.css +5 -3
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/tabActions.ts +22 -10
  35. package/src/client/ui/EditorView.ts +198 -25
  36. package/src/client/ui/McpSettings.ts +30 -0
  37. package/src/client/ui/OfficialSideTab.ts +1 -0
  38. package/src/client/ui/PerfSettings.ts +4 -1
  39. package/src/client/ui/PromptDialog.ts +63 -0
  40. package/src/client/ui/SideEditorTab.ts +1 -0
  41. package/src/client/ui/commandCatalog.ts +14 -0
  42. package/src/client/ui/icons.ts +72 -0
  43. package/src/compat.ts +2 -2
  44. package/src/dshVersion.ts +2 -1
  45. package/src/fileOpenSettings.ts +163 -40
  46. package/src/index.ts +15 -1
  47. package/src/lsp/client.ts +24 -0
  48. package/src/lsp/rpc.ts +57 -0
  49. package/src/lsp/server.ts +272 -2
  50. package/src/perf.ts +90 -4
  51. package/src/routes.ts +13 -1
  52. package/src/rpc.ts +294 -36
  53. package/src/shared/fsNames.ts +115 -0
  54. package/src/shared/keybindings.ts +2 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. 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 方法承载,见 runSettingsInstall 三策略)。
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 卸载回退)。回调内方法缺失再降级记录 none。
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.object({
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?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.revision;
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?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
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?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS)?.value;
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
- if (!provider?.update) {
668
- notify(next);
669
- return;
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.update(FILE_OPEN_SETTINGS_NS, body, expectedRevision);
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.6-alpha.2";
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.6-alpha.2):设置 API 按能力探测运行,异常时请回报适配矩阵");
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
- /** 标记活跃会话(live id 与目录段名双向匹配,兼容编码差异)。 */
7011
- function markActiveSessions(sessions, activeIds) {
7012
- const raw = new Set(activeIds);
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
- for (const s of sessions) if (raw.has(s.sessionId) || encoded.has(s.sessionId)) s.active = true;
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")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
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 fs = ctx.get("fs");
7656
- if (!fs) return {
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.searchFiles": async (args) => {
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 bucket = await bucketOf(registry, ctx, sc.cwd);
8040
- const activePaths = [];
8041
- for (const record of bucket.values()) if (record.path) activePaths.push(record.path);
8042
- return {
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
- "edrv.searchContent": async (args) => {
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: sc.err
8830
+ error: r.err
8057
8831
  };
8058
8832
  try {
8059
- return {
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: "搜索失败:" + String(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.listDir": async (args) => {
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 rel = normalizeRel(args.path);
8087
- if (rel === null) return {
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: res.error
8855
+ error: r.err
8095
8856
  };
8096
8857
  return {
8097
8858
  ok: true,
8098
- root: res.root,
8099
- path: rel,
8100
- entries: res.entries
8859
+ from: r.from,
8860
+ to: r.to
8101
8861
  };
8102
8862
  },
8103
- "edrv.revealInExplorer": async (args) => {
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 fs = ctx.get("fs");
8110
- if (!fs) return {
8869
+ const r = await fsTransfer(ctx, sc, args, "move");
8870
+ if ("err" in r) return {
8111
8871
  ok: false,
8112
- error: "缺少 fs"
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 descriptor = ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
8264
- const value = descriptor?.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: descriptor?.revision
9006
+ revision: section?.revision
8270
9007
  };
8271
9008
  },
8272
9009
  "vscode.fileOpenSettingsUpdate": async (args) => {
8273
9010
  const settings = ctx.get("settings");
8274
- if (!settings?.update) return {
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
- return result.ok ? {
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 contents文本行(MarkupContent | MarkedString | MarkedString[])。 */
9964
- function stringifyHoverContents(contents) {
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