dsh-vscode-mode 0.7.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 (42) hide show
  1. package/README.md +23 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +1167 -188
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +845 -96
  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/imagePreview.ts +76 -2
  13. package/src/client/index.ts +65 -36
  14. package/src/client/monaco/lsp/lspClient.ts +46 -0
  15. package/src/client/monaco/lsp/providers.ts +245 -1
  16. package/src/client/nativeOpenStore.ts +91 -0
  17. package/src/client/officialSidebar.ts +11 -45
  18. package/src/client/outline/index.ts +1 -1
  19. package/src/client/pdf/pdfPanel.ts +18 -0
  20. package/src/client/settingsContext.ts +6 -4
  21. package/src/client/sidebar/panels/FileExplorer.ts +1 -1
  22. package/src/client/sidebar/panels/SvnPanel.ts +1 -1
  23. package/src/client/sidebar/panels/index.ts +1 -1
  24. package/src/client/styles/editor.css +5 -3
  25. package/src/client/svnStore.ts +6 -1
  26. package/src/client/ui/EditorView.ts +117 -18
  27. package/src/client/ui/McpSettings.ts +30 -0
  28. package/src/client/ui/PerfSettings.ts +4 -1
  29. package/src/client/ui/icons.ts +72 -0
  30. package/src/compat.ts +2 -2
  31. package/src/dshVersion.ts +2 -1
  32. package/src/fileOpenSettings.ts +163 -40
  33. package/src/index.ts +15 -1
  34. package/src/lsp/client.ts +24 -0
  35. package/src/lsp/rpc.ts +57 -0
  36. package/src/lsp/server.ts +272 -2
  37. package/src/perf.ts +90 -4
  38. package/src/routes.ts +13 -1
  39. package/src/rpc.ts +96 -34
  40. package/src/shared/lsp.ts +79 -0
  41. package/src/shared/nativeOpen.ts +91 -0
  42. package/src/shared/rpc.ts +146 -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
  /**
@@ -208,6 +209,103 @@ function contentGuardOf(text) {
208
209
  return {};
209
210
  }
210
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
211
309
  //#region src/shared/logger.ts
212
310
  /**
213
311
  * dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
@@ -322,10 +420,11 @@ function bindHostLog(ctx) {
322
420
  * dsh-vscode-mode host — 文件链接打开工具 + 快捷键的持久化设置。
323
421
  * 依赖守卫:schemastery 动态加载;@deepseek-ai/dsh-settings 仅作 legacy 探测——
324
422
  * rc 线导出 installSettingsSection free function,0.1.2-alpha 起移除(改由 settings
325
- * 服务的 installSection 方法承载,见 runSettingsInstall 三策略)。
423
+ * 服务的 installSection 方法承载),0.1.7 再移除 installSection(设置并入 profile
424
+ * 插件 Config schema,见 runSettingsInstall 四策略:legacy/service/forms/none)。
326
425
  * 缺失/任一策略失败时插件仍可加载(fileOpenTool 降级为配置值,compat 报告可见),
327
426
  * 全程 try/catch,不产生未捕获 rejection。
328
- * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号
427
+ * 作者 ddj 2026年08月24号 / 2026年08月26号 / 2026年09月02号 / 2026年09月22号
329
428
  */
330
429
  const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
331
430
  const FILE_OPEN_DEFAULT = "auto";
@@ -437,6 +536,7 @@ function loadSettingsDeps(importFn = hostImport) {
437
536
  const INSTALL_UNMOUNTED = "设置 section 尚未装配";
438
537
  const INSTALL_LEGACY = "rc 线:dsh-settings.installSettingsSection";
439
538
  const INSTALL_SERVICE = "0.1.2-alpha 线:settings 服务 installSection";
539
+ const INSTALL_FORMS = "0.1.7 线:设置并入插件 Config schema(SettingsForms)";
440
540
  const INSTALL_NONE = "两路均不可用:设置持久化降级为配置值";
441
541
  let observedInstall = {
442
542
  strategy: "unknown",
@@ -459,14 +559,51 @@ function settingsInstallNote() {
459
559
  return observedInstall.note;
460
560
  }
461
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
+ /**
462
597
  * 设置 section 版本自适应安装(核心策略分派)。
463
598
  * legacy:dsh-settings 仍导出 installSettingsSection(rc 线)→ 原样调用,行为与旧版一致。
464
599
  * service:该导出已移除(0.1.2-alpha 起)→ 经 ctx.inject(['settings']) 走服务方法
465
600
  * provider.installSection(owner, ns, schema, entry, hooks)(等义封装,含
466
- * base 层与 fiber 卸载回退)。回调内方法缺失再降级记录 none。
601
+ * base 层与 fiber 卸载回退)。
602
+ * forms:0.1.7 起 installSection 移除,设置并入 profile 插件 Config(SettingsForms
603
+ * 的 describe+update 在场即成立)→ 不装 section,改订阅 document-updated。
467
604
  * none:两条路由都不存在 → 仅记录并告警,调用方按配置值运行。
468
605
  * 全程不抛错、不产生未捕获 rejection。
469
- * @author ddj 2026年09月02号
606
+ * @author ddj 2026年09月02号(2026年09月22号 增补 forms 策略)
470
607
  * @param ctx DSH host 上下文(inject 可选探测)
471
608
  * @param ns 设置命名空间
472
609
  * @param schema schemastery schema
@@ -499,6 +636,12 @@ async function runSettingsInstall(ctx, ns, schema, entry, hooks, loader = loadSe
499
636
  const provider = typeof sc.get === "function" ? sc.get("settings") : sc.settings;
500
637
  const install = provider?.installSection;
501
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
+ }
502
645
  log.warn("settings 服务无 installSection(DSH 版本 API 变化),section " + ns + " 降级为配置值");
503
646
  recordInstall("none", INSTALL_NONE);
504
647
  return;
@@ -529,6 +672,99 @@ function keybindingsShape(z) {
529
672
  for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
530
673
  return shape;
531
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
+ }
532
768
  function normalizeValue(value) {
533
769
  if (typeof value !== "string" || value.trim() === "") return FILE_OPEN_DEFAULT;
534
770
  return value.trim();
@@ -567,23 +803,11 @@ async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSe
567
803
  deps = await loader();
568
804
  } catch {}
569
805
  if (!deps) return false;
570
- const strategy = await runSettingsInstall(ctx, ns, deps.z.object({
571
- fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
572
- keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS }),
573
- sidebarMinWidth: deps.z.number().default(300),
574
- maxOpenEditors: deps.z.number().default(10),
575
- integrationBaseUrl: deps.z.string().default(INTEGRATION_BASE_DEFAULT),
576
- aiInline: deps.z.boolean().default(AI_CONFIG_DEFAULT.enabled),
577
- aiProvider: deps.z.string().default(AI_CONFIG_DEFAULT.provider),
578
- aiModel: deps.z.string().default(AI_CONFIG_DEFAULT.model),
579
- aiEffort: deps.z.string().default(AI_CONFIG_DEFAULT.effort),
580
- svnPath: deps.z.string().default(""),
581
- tortoisePath: deps.z.string().default(TORTOISE_DIR_DEFAULT)
582
- }), entry, {
806
+ const strategy = await runSettingsInstall(ctx, ns, buildSettingsSchema(deps.z), entry, {
583
807
  setSource: (source) => hooks.setSource(source),
584
808
  onChange: hooks.onChange
585
809
  }, loader);
586
- return strategy === "legacy" || strategy === "service";
810
+ return strategy === "legacy" || strategy === "service" || strategy === "forms";
587
811
  }
588
812
  /** AI 配置脏值读取(settings 未就绪时回退默认)。 */
589
813
  function aiValueOf(stored) {
@@ -612,13 +836,13 @@ function setupOpenSettings(ctx, config, onChange) {
612
836
  onChange(current);
613
837
  };
614
838
  const syncRevision = () => {
615
- revision = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.revision;
839
+ revision = sectionOf(provider, FILE_OPEN_SETTINGS_NS)?.revision;
616
840
  };
617
841
  const settingsChange = () => syncRevision();
618
842
  /** AI 配置当前值(settings 未就绪回退默认)。 */
619
843
  let aiCurrent = { ...AI_CONFIG_DEFAULT };
620
844
  const aiSync = () => {
621
- const stored = (provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
845
+ const stored = readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
622
846
  aiCurrent = stored !== void 0 ? aiValueOf(stored) : aiCurrent;
623
847
  };
624
848
  /** SVN 路径当前值(settings 未就绪回退配置值)。 */
@@ -634,7 +858,7 @@ function setupOpenSettings(ctx, config, onChange) {
634
858
  };
635
859
  /** 读取 settings 存储值(describe 未就绪返回 undefined)。 */
636
860
  const storedValue = () => {
637
- return provider?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS)?.value;
861
+ return readSectionValue(provider, FILE_OPEN_SETTINGS_NS);
638
862
  };
639
863
  installOpenSettingsSection(ctx, FILE_OPEN_SETTINGS_NS, {
640
864
  fileOpenTool: current,
@@ -665,25 +889,15 @@ function setupOpenSettings(ctx, config, onChange) {
665
889
  },
666
890
  update: async (value, expectedRevision) => {
667
891
  const next = normalizeValue(value);
668
- if (!provider?.update) {
669
- notify(next);
670
- return;
671
- }
672
- await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
673
- const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
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);
674
895
  notify(stored?.fileOpenTool ?? next);
675
896
  syncRevision();
676
897
  },
677
898
  ai: () => aiCurrent,
678
899
  svn: () => svnCurrent,
679
900
  aiUpdate: async (patch, expectedRevision) => {
680
- if (!provider?.update) {
681
- if (patch.enabled !== void 0) aiCurrent.enabled = patch.enabled;
682
- if (patch.provider !== void 0) aiCurrent.provider = patch.provider;
683
- if (patch.model !== void 0) aiCurrent.model = patch.model;
684
- if (patch.effort !== void 0) aiCurrent.effort = patch.effort;
685
- return aiCurrent;
686
- }
687
901
  const stored = { ...aiCurrent };
688
902
  const body = {};
689
903
  if (patch.enabled !== void 0) {
@@ -702,7 +916,10 @@ function setupOpenSettings(ctx, config, onChange) {
702
916
  stored.effort = patch.effort;
703
917
  body.aiEffort = patch.effort;
704
918
  }
705
- 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
+ }
706
923
  aiSync();
707
924
  return aiCurrent;
708
925
  }
@@ -919,13 +1136,14 @@ function inDshRange(version, range) {
919
1136
  /**
920
1137
  * 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
921
1138
  * 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
922
- * @author ddj 2026年09月02号 / 2026年09月18号
1139
+ * @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号
923
1140
  * @param input 版本串(如 '0.1.6-alpha.2')
924
1141
  * @returns 版本线标签
925
1142
  */
926
1143
  function familyLabel(input) {
927
1144
  const version = parseDshVersion(input);
928
1145
  if (!version) return "未知";
1146
+ if (inDshRange(version, { from: "0.1.7-alpha.1" })) return "0.1.7-alpha.1 及更新(设置=profile Config + configForms 客户端面 + 会话 V4)";
929
1147
  if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
930
1148
  if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
931
1149
  if (inDshRange(version, { from: "0.1.5-alpha.1" })) return "0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)";
@@ -1864,7 +2082,7 @@ function detectGuards(ctx) {
1864
2082
  }];
1865
2083
  }
1866
2084
  /** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
1867
- const TESTED_DSH_MAX = "0.1.6-alpha.2";
2085
+ const TESTED_DSH_MAX = "0.1.7-alpha.1";
1868
2086
  /** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
1869
2087
  function versionAdapters(dshVersion) {
1870
2088
  const adapters = [];
@@ -1887,7 +2105,7 @@ function versionAdapters(dshVersion) {
1887
2105
  const strategy = settingsInstallStrategy();
1888
2106
  adapters.push({
1889
2107
  name: "设置 section 安装(版本适配)",
1890
- active: strategy === "legacy" || strategy === "service",
2108
+ active: strategy === "legacy" || strategy === "service" || strategy === "forms",
1891
2109
  note: settingsInstallNote()
1892
2110
  });
1893
2111
  return adapters;
@@ -1910,7 +2128,7 @@ async function buildReport(ctx, options) {
1910
2128
  if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
1911
2129
  const parsed = parseDshVersion(dshVersion);
1912
2130
  const testedMax = parseDshVersion(TESTED_DSH_MAX);
1913
- 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 按能力探测运行,异常时请回报适配矩阵");
1914
2132
  for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
1915
2133
  return {
1916
2134
  pluginVersion: options?.version ?? pluginVersionOf(),
@@ -2000,6 +2218,16 @@ function registerRoutes(ctx, config, handleRpc, onWarning) {
2000
2218
  input = {};
2001
2219
  }
2002
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
+ }
2003
2231
  res.statusCode = 200;
2004
2232
  res.setHeader("content-type", "application/json");
2005
2233
  res.end(JSON.stringify(result));
@@ -7104,12 +7332,70 @@ async function scanSessionInventory(home = dshHome(), archive = sessionsArchiveR
7104
7332
  }
7105
7333
  };
7106
7334
  }
7107
- /** 标记活跃会话(live id 与目录段名双向匹配,兼容编码差异)。 */
7108
- function markActiveSessions(sessions, activeIds) {
7109
- const raw = new Set(activeIds);
7335
+ /** id 双向匹配器:原始 id 与编码段名任一命中(兼容编码差异)。 */
7336
+ function idMatcher(ids) {
7337
+ const raw = new Set(ids);
7110
7338
  const encoded = /* @__PURE__ */ new Set();
7111
7339
  for (const id of raw) encoded.add(sessionIdSegment(id));
7112
- 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
+ }
7113
7399
  }
7114
7400
  /**
7115
7401
  * 移出规划(纯函数):从盘点中按「显式集合」或「规则(minBytes / olderThanDays)」
@@ -7273,6 +7559,27 @@ async function restoreSession(home, archive, workspaceKey, sessionId) {
7273
7559
  };
7274
7560
  }
7275
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
+ }
7276
7583
  /** 清除归档区早于 N 天的会话(破坏性,仅限归档区)。 */
7277
7584
  async function purgeArchive(archive, olderThanDays) {
7278
7585
  const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1e3;
@@ -7508,6 +7815,52 @@ function snippetTargetOf(path) {
7508
7815
  return isSnippetFilePath(path) ? path.replace(/\\/g, "/") : null;
7509
7816
  }
7510
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
+ /**
7511
7864
  * 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
7512
7865
  * 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
7513
7866
  * @author ddj 2026年09月15号
@@ -7633,7 +7986,7 @@ async function latestPatchBackup(patchPath) {
7633
7986
  }
7634
7987
  /** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
7635
7988
  async function integrationBaseUrlOf(ctx) {
7636
- 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;
7637
7990
  return (typeof value?.integrationBaseUrl === "string" ? value.integrationBaseUrl.trim() : "") || "http://127.0.0.1:3080";
7638
7991
  }
7639
7992
  /**
@@ -7846,45 +8199,56 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
7846
8199
  ok: false,
7847
8200
  error: sc.err
7848
8201
  };
7849
- const fs = ctx.get("fs");
7850
- if (!fs) return {
7851
- ok: false,
7852
- error: "缺少 fs"
7853
- };
8202
+ const prep = await readTargetOf(ctx, sc, args.path);
8203
+ if ("err" in prep) return targetErrOf(prep);
7854
8204
  try {
7855
- const target = await resolveTarget(ctx, sc.session, args.path);
7856
- debugRecord(ctx, sc.cwd, "[DEBUG path.resolve] input=" + String(args.path ?? "") + " resolved=" + fs.processPath(target), "debug");
7857
- const info = await fs.stat(target);
7858
- if (!info || info.type !== "file") return {
7859
- ok: false,
7860
- error: "文件不存在",
7861
- resolvedPath: fs.processPath(target)
7862
- };
7863
- if ((info.size ?? 0) > 33554432) return {
7864
- ok: false,
7865
- error: "文件过大(>32MB),不支持整文件预览"
7866
- };
7867
8205
  if (args.encoding === "base64") {
7868
- 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);
7869
8207
  return {
7870
8208
  ok: true,
7871
8209
  content: Buffer.from(bytes).toString("base64"),
7872
8210
  size: bytes.byteLength,
7873
8211
  encoding: "base64",
7874
8212
  mime: binaryMimeOf(args.path),
7875
- version: String(info.version ?? "")
8213
+ version: String(prep.info.version ?? "")
7876
8214
  };
7877
8215
  }
7878
- if ((info.size ?? 0) > 8388608) return {
8216
+ if ((prep.info.size ?? 0) > 8388608) return {
7879
8217
  ok: false,
7880
8218
  error: "文件过大(>8MB),不支持整文件预览"
7881
8219
  };
7882
- const content = await fs.readText(target);
8220
+ const content = await prep.fs.readText(prep.target);
7883
8221
  return {
7884
8222
  ok: true,
7885
8223
  content,
7886
8224
  size: content.length,
7887
- 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
+ }
7888
8252
  };
7889
8253
  } catch (error) {
7890
8254
  return {
@@ -8633,39 +8997,32 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8633
8997
  }
8634
8998
  },
8635
8999
  "vscode.fileOpenSettingsGet": async () => {
8636
- const descriptor = ctx.get("settings")?.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
8637
- const value = descriptor?.value;
9000
+ const section = sectionOf(ctx.get("settings"), FILE_OPEN_SETTINGS_NS);
9001
+ const value = section?.value;
8638
9002
  return {
8639
9003
  ok: true,
8640
9004
  fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? "auto"),
8641
9005
  integrationBaseUrl: await integrationBaseUrlOf(ctx),
8642
- revision: descriptor?.revision
9006
+ revision: section?.revision
8643
9007
  };
8644
9008
  },
8645
9009
  "vscode.fileOpenSettingsUpdate": async (args) => {
8646
9010
  const settings = ctx.get("settings");
8647
- 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 {
8648
9015
  ok: false,
8649
- 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
8650
9025
  };
8651
- try {
8652
- const patch = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) };
8653
- if (typeof args.integrationBaseUrl === "string" && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim();
8654
- await settings.update(FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision);
8655
- const descriptor = settings.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS);
8656
- const value = descriptor?.value;
8657
- return {
8658
- ok: true,
8659
- fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool),
8660
- integrationBaseUrl: await integrationBaseUrlOf(ctx),
8661
- revision: descriptor?.revision
8662
- };
8663
- } catch (error) {
8664
- return {
8665
- ok: false,
8666
- error: String(error)
8667
- };
8668
- }
8669
9026
  },
8670
9027
  "compat": async () => ({
8671
9028
  ok: true,
@@ -8829,6 +9186,7 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8829
9186
  const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home));
8830
9187
  const active = activeSessionIds(ctx);
8831
9188
  markActiveSessions(inventory.sessions, active);
9189
+ markOfficialFlags(inventory.sessions, ctx);
8832
9190
  return {
8833
9191
  ok: true,
8834
9192
  ...inventory,
@@ -8897,13 +9255,15 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
8897
9255
  "edrv.perf.restore": async (args) => {
8898
9256
  try {
8899
9257
  const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId);
8900
- return result.ok ? {
8901
- ok: true,
8902
- restored: true
8903
- } : {
9258
+ if (!result.ok) return {
8904
9259
  ok: false,
8905
9260
  error: result.error ?? "恢复失败"
8906
9261
  };
9262
+ await unarchiveOfficial(ctx, args.sessionId);
9263
+ return {
9264
+ ok: true,
9265
+ restored: true
9266
+ };
8907
9267
  } catch (error) {
8908
9268
  return {
8909
9269
  ok: false,
@@ -9616,6 +9976,9 @@ function createLspClient(transport, events = {}) {
9616
9976
  workspaceSymbol: false,
9617
9977
  hover: false,
9618
9978
  semanticTokens: false,
9979
+ completion: false,
9980
+ completionResolve: false,
9981
+ signatureHelp: false,
9619
9982
  semanticTokenTypes: [],
9620
9983
  semanticTokenModifiers: []
9621
9984
  };
@@ -9749,6 +10112,23 @@ function createLspClient(transport, events = {}) {
9749
10112
  references: { dynamicRegistration: false },
9750
10113
  documentSymbol: { dynamicRegistration: false },
9751
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
+ },
9752
10132
  semanticTokens: {
9753
10133
  dynamicRegistration: false,
9754
10134
  requests: {
@@ -9771,6 +10151,9 @@ function createLspClient(transport, events = {}) {
9771
10151
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
9772
10152
  hover: Boolean(caps.hoverProvider),
9773
10153
  semanticTokens: Boolean(caps.semanticTokensProvider),
10154
+ completion: Boolean(caps.completionProvider),
10155
+ completionResolve: Boolean(caps.completionProvider?.resolveProvider),
10156
+ signatureHelp: Boolean(caps.signatureHelpProvider),
9774
10157
  semanticTokenTypes: Array.isArray(legend?.tokenTypes) ? legend.tokenTypes.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_TYPES],
9775
10158
  semanticTokenModifiers: Array.isArray(legend?.tokenModifiers) ? legend.tokenModifiers.filter((item) => typeof item === "string") : [...LSP_SEMANTIC_TOKEN_MODIFIERS]
9776
10159
  };
@@ -9972,6 +10355,9 @@ function createLspServer(spec, root, languageId, logger) {
9972
10355
  workspaceSymbol: false,
9973
10356
  hover: false,
9974
10357
  semanticTokens: false,
10358
+ completion: false,
10359
+ completionResolve: false,
10360
+ signatureHelp: false,
9975
10361
  semanticTokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
9976
10362
  semanticTokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS]
9977
10363
  };
@@ -10228,6 +10614,68 @@ function createLspServer(spec, root, languageId, logger) {
10228
10614
  if (!result || !result.contents) return null;
10229
10615
  return { contents: stringifyHoverContents(result.contents) };
10230
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
+ },
10231
10679
  /** 查询全文 semantic tokens,并将服务器 legend 归一到插件固定 legend。 */
10232
10680
  async semanticTokens(path) {
10233
10681
  const doc = docs.get(path);
@@ -10333,8 +10781,205 @@ function normalizeSymbol(item) {
10333
10781
  }
10334
10782
  return symbol;
10335
10783
  }
10336
- /** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */
10337
- 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) {
10338
10983
  if (typeof contents === "string") return [contents];
10339
10984
  if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean);
10340
10985
  if (contents && typeof contents === "object") {
@@ -12243,6 +12888,99 @@ function createLspRpc(deps) {
12243
12888
  };
12244
12889
  }
12245
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
+ },
12246
12984
  "edrv.lsp.semanticTokens": async (args) => {
12247
12985
  const lang = langOfPath(args.path);
12248
12986
  if (!lang) return {
@@ -16428,6 +17166,17 @@ const inject = [
16428
17166
  "agents"
16429
17167
  ];
16430
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
+ /**
16431
17180
  * 装配插件:挂事件监听、注册路由、安装兼容层。
16432
17181
  * @author ddj 2026年08月20号
16433
17182
  * @param ctx DSH 上下文(sessions/fs/webServer 由 inject 提供;sandboxPolicy/subprocess 惰性获取)
@@ -16517,6 +17266,6 @@ async function logCompatSummary(ctx, warnings) {
16517
17266
  }
16518
17267
  }
16519
17268
  //#endregion
16520
- export { apply, inject, name };
17269
+ export { Config, apply, inject, name };
16521
17270
 
16522
17271
  //# sourceMappingURL=index.js.map