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.
- package/README.md +23 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +1167 -188
- package/lib/client.js.map +1 -1
- package/lib/index.js +845 -96
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +3 -1
- package/src/client/compat.ts +134 -4
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +65 -36
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/panels/FileExplorer.ts +1 -1
- package/src/client/sidebar/panels/SvnPanel.ts +1 -1
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/styles/editor.css +5 -3
- package/src/client/svnStore.ts +6 -1
- package/src/client/ui/EditorView.ts +117 -18
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/icons.ts +72 -0
- package/src/compat.ts +2 -2
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +163 -40
- package/src/index.ts +15 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/perf.ts +90 -4
- package/src/routes.ts +13 -1
- package/src/rpc.ts +96 -34
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
await provider.update(FILE_OPEN_SETTINGS_NS, { fileOpenTool: next }, expectedRevision);
|
|
673
|
-
const stored = (provider.describe?.({ redactSecrets: true })?.find((item) => item.ns === FILE_OPEN_SETTINGS_NS))?.value;
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
7108
|
-
function
|
|
7109
|
-
const raw = new Set(
|
|
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
|
-
|
|
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")
|
|
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
|
|
7850
|
-
if (
|
|
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
|
|
8637
|
-
const 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:
|
|
9006
|
+
revision: section?.revision
|
|
8643
9007
|
};
|
|
8644
9008
|
},
|
|
8645
9009
|
"vscode.fileOpenSettingsUpdate": async (args) => {
|
|
8646
9010
|
const settings = ctx.get("settings");
|
|
8647
|
-
|
|
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
|
-
|
|
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
|
|
10337
|
-
function
|
|
10784
|
+
/** hover/markdown 内容 → 纯文本(string | MarkupContent | MarkedString[] | MarkedString)。 */
|
|
10785
|
+
function plainText(value) {
|
|
10786
|
+
if (typeof value === "string") return value || void 0;
|
|
10787
|
+
if (Array.isArray(value)) {
|
|
10788
|
+
const parts = value.map(plainText).filter((part) => Boolean(part));
|
|
10789
|
+
return parts.length ? parts.join("\n\n") : void 0;
|
|
10790
|
+
}
|
|
10791
|
+
if (value && typeof value === "object") {
|
|
10792
|
+
const text = value.value;
|
|
10793
|
+
if (typeof text === "string") return text || void 0;
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
/**
|
|
10797
|
+
* 归一化补全文本编辑范围(单 range 与 insert/replace 双 range 两种形态)。
|
|
10798
|
+
*
|
|
10799
|
+
* 两种形态必须都认:只认一种会让采用另一种形态的服务器补全落点错位(替换范围算错,
|
|
10800
|
+
* 已输入的前缀被重复插入)。双 range 时 insert 用于「保留前缀插入」,replace 用于覆盖。
|
|
10801
|
+
* @author ddj 2026年09月22号
|
|
10802
|
+
* @param raw 原始 textEdit
|
|
10803
|
+
* @returns 归一化文本编辑;无有效范围或文本时 null
|
|
10804
|
+
*/
|
|
10805
|
+
function normalizeTextEdit(raw) {
|
|
10806
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10807
|
+
const obj = raw;
|
|
10808
|
+
const newText = typeof obj.newText === "string" ? obj.newText : "";
|
|
10809
|
+
if (!newText) return null;
|
|
10810
|
+
const single = normalizeRange(obj.range);
|
|
10811
|
+
if (single) return {
|
|
10812
|
+
range: single,
|
|
10813
|
+
newText
|
|
10814
|
+
};
|
|
10815
|
+
const insert = normalizeRange(obj.insert);
|
|
10816
|
+
const replace = normalizeRange(obj.replace);
|
|
10817
|
+
if (!insert && !replace) return null;
|
|
10818
|
+
return {
|
|
10819
|
+
insert: insert ?? replace,
|
|
10820
|
+
replace: replace ?? insert,
|
|
10821
|
+
newText
|
|
10822
|
+
};
|
|
10823
|
+
}
|
|
10824
|
+
/** 归一化 LSP Range(缺字段返回 null,避免产出畸形范围)。 */
|
|
10825
|
+
function normalizeRange(raw) {
|
|
10826
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10827
|
+
const r = raw;
|
|
10828
|
+
if (!r.start || !r.end) return null;
|
|
10829
|
+
return {
|
|
10830
|
+
start: {
|
|
10831
|
+
line: toInt(r.start.line),
|
|
10832
|
+
character: toInt(r.start.character)
|
|
10833
|
+
},
|
|
10834
|
+
end: {
|
|
10835
|
+
line: toInt(r.end.line),
|
|
10836
|
+
character: toInt(r.end.character)
|
|
10837
|
+
}
|
|
10838
|
+
};
|
|
10839
|
+
}
|
|
10840
|
+
/**
|
|
10841
|
+
* 归一化单个补全项。
|
|
10842
|
+
* @author ddj 2026年09月22号
|
|
10843
|
+
* @param raw 原始条目
|
|
10844
|
+
* @returns 归一化条目;缺 label 时 null
|
|
10845
|
+
*/
|
|
10846
|
+
function normCompItem(raw) {
|
|
10847
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10848
|
+
const obj = raw;
|
|
10849
|
+
const label = typeof obj.label === "string" ? obj.label : "";
|
|
10850
|
+
if (!label) return null;
|
|
10851
|
+
const item = { label };
|
|
10852
|
+
if (typeof obj.kind === "number") item.kind = toInt(obj.kind);
|
|
10853
|
+
if (typeof obj.detail === "string" && obj.detail) item.detail = obj.detail;
|
|
10854
|
+
const documentation = plainText(obj.documentation);
|
|
10855
|
+
if (documentation) item.documentation = documentation;
|
|
10856
|
+
if (typeof obj.insertText === "string") item.insertText = obj.insertText;
|
|
10857
|
+
if (typeof obj.insertTextFormat === "number") item.insertTextFormat = toInt(obj.insertTextFormat);
|
|
10858
|
+
const textEdit = normalizeTextEdit(obj.textEdit);
|
|
10859
|
+
if (textEdit) item.textEdit = textEdit;
|
|
10860
|
+
if (obj.data !== void 0) item.data = obj.data;
|
|
10861
|
+
if (typeof obj.sortText === "string") item.sortText = obj.sortText;
|
|
10862
|
+
if (typeof obj.filterText === "string") item.filterText = obj.filterText;
|
|
10863
|
+
if (obj.preselect === true) item.preselect = true;
|
|
10864
|
+
if (Array.isArray(obj.commitCharacters)) {
|
|
10865
|
+
const chars = obj.commitCharacters.filter((c) => typeof c === "string");
|
|
10866
|
+
if (chars.length) item.commitCharacters = chars;
|
|
10867
|
+
}
|
|
10868
|
+
const tags = Array.isArray(obj.tags) ? obj.tags : [];
|
|
10869
|
+
if (obj.deprecated === true || tags.some((tag) => toInt(tag) === 1)) item.deprecated = true;
|
|
10870
|
+
if (Array.isArray(obj.additionalTextEdits)) {
|
|
10871
|
+
const edits = obj.additionalTextEdits.map((edit) => {
|
|
10872
|
+
const range = normalizeRange(edit?.range);
|
|
10873
|
+
const text = edit?.newText;
|
|
10874
|
+
if (!range || typeof text !== "string") return null;
|
|
10875
|
+
return {
|
|
10876
|
+
range,
|
|
10877
|
+
newText: text
|
|
10878
|
+
};
|
|
10879
|
+
}).filter((edit) => edit !== null);
|
|
10880
|
+
if (edits.length) item.additionalTextEdits = edits;
|
|
10881
|
+
}
|
|
10882
|
+
return item;
|
|
10883
|
+
}
|
|
10884
|
+
/**
|
|
10885
|
+
* 归一化补全结果(CompletionList | CompletionItem[])。
|
|
10886
|
+
*
|
|
10887
|
+
* 两种顶层形态都要认:`{ isIncomplete, items }` 与裸数组。裸数组是最常见形态,
|
|
10888
|
+
* 只认 CompletionList 会让「补全列表恒为空」且无报错。
|
|
10889
|
+
* @author ddj 2026年09月22号
|
|
10890
|
+
* @param result 原始响应
|
|
10891
|
+
* @returns 归一化列表;无效时 null
|
|
10892
|
+
*/
|
|
10893
|
+
function normCompletion(result) {
|
|
10894
|
+
if (result == null) return null;
|
|
10895
|
+
const list = Array.isArray(result) ? {
|
|
10896
|
+
items: result,
|
|
10897
|
+
isIncomplete: false
|
|
10898
|
+
} : result;
|
|
10899
|
+
if (!Array.isArray(list.items)) return null;
|
|
10900
|
+
return {
|
|
10901
|
+
items: list.items.map(normCompItem).filter((item) => item !== null),
|
|
10902
|
+
incomplete: list.isIncomplete === true
|
|
10903
|
+
};
|
|
10904
|
+
}
|
|
10905
|
+
/**
|
|
10906
|
+
* 补全项 → LSP 载荷(resolve 回传用)。
|
|
10907
|
+
* 只回传协议字段,剔除 client 侧衍生字段;`data` 必须原样带上,服务器靠它认条目。
|
|
10908
|
+
* @author ddj 2026年09月22号
|
|
10909
|
+
* @param item 归一化条目
|
|
10910
|
+
* @returns LSP CompletionItem 载荷
|
|
10911
|
+
*/
|
|
10912
|
+
function toLspCompItem(item) {
|
|
10913
|
+
const out = { label: item.label };
|
|
10914
|
+
if (item.kind !== void 0) out.kind = item.kind;
|
|
10915
|
+
if (item.detail !== void 0) out.detail = item.detail;
|
|
10916
|
+
if (item.documentation !== void 0) out.documentation = item.documentation;
|
|
10917
|
+
if (item.insertText !== void 0) out.insertText = item.insertText;
|
|
10918
|
+
if (item.insertTextFormat !== void 0) out.insertTextFormat = item.insertTextFormat;
|
|
10919
|
+
if (item.data !== void 0) out.data = item.data;
|
|
10920
|
+
if (item.sortText !== void 0) out.sortText = item.sortText;
|
|
10921
|
+
if (item.filterText !== void 0) out.filterText = item.filterText;
|
|
10922
|
+
if (item.textEdit) out.textEdit = item.textEdit.range ? {
|
|
10923
|
+
range: item.textEdit.range,
|
|
10924
|
+
newText: item.textEdit.newText
|
|
10925
|
+
} : item.textEdit;
|
|
10926
|
+
if (item.additionalTextEdits) out.additionalTextEdits = item.additionalTextEdits;
|
|
10927
|
+
if (item.commitCharacters) out.commitCharacters = item.commitCharacters;
|
|
10928
|
+
return out;
|
|
10929
|
+
}
|
|
10930
|
+
/** 归一化单个签名参数(label 支持字符串与 [start, end] 元组,两者原样透传)。 */
|
|
10931
|
+
function toSignatureParam(raw) {
|
|
10932
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10933
|
+
const obj = raw;
|
|
10934
|
+
const tuple = Array.isArray(obj.label) && obj.label.length >= 2 ? [toInt(obj.label[0]), toInt(obj.label[1])] : null;
|
|
10935
|
+
if (typeof obj.label !== "string" && !tuple) return null;
|
|
10936
|
+
const param = { label: typeof obj.label === "string" ? obj.label : tuple };
|
|
10937
|
+
const documentation = plainText(obj.documentation);
|
|
10938
|
+
if (documentation) param.documentation = documentation;
|
|
10939
|
+
return param;
|
|
10940
|
+
}
|
|
10941
|
+
/** 归一化单个签名。 */
|
|
10942
|
+
function toSignature(raw) {
|
|
10943
|
+
if (!raw || typeof raw !== "object") return null;
|
|
10944
|
+
const obj = raw;
|
|
10945
|
+
if (typeof obj.label !== "string") return null;
|
|
10946
|
+
const signature = {
|
|
10947
|
+
label: obj.label,
|
|
10948
|
+
parameters: []
|
|
10949
|
+
};
|
|
10950
|
+
const documentation = plainText(obj.documentation);
|
|
10951
|
+
if (documentation) signature.documentation = documentation;
|
|
10952
|
+
if (Array.isArray(obj.parameters)) signature.parameters = obj.parameters.map(toSignatureParam).filter((param) => param !== null);
|
|
10953
|
+
if (typeof obj.activeParameter === "number") signature.activeParameter = toInt(obj.activeParameter);
|
|
10954
|
+
return signature;
|
|
10955
|
+
}
|
|
10956
|
+
/**
|
|
10957
|
+
* 归一化签名帮助,并裁剪越界的 activeSignature / activeParameter。
|
|
10958
|
+
*
|
|
10959
|
+
* 为什么必须裁:Monaco 用这两个索引直接取签名与参数,越界时取不到签名就整个浮窗不渲染;
|
|
10960
|
+
* 服务器在「实参刚敲下逗号」等边界时刻常给超前一位的索引。裁剪后至少显示首个签名/末个参数,
|
|
10961
|
+
* 观感是「高亮没跟上」,而不是「参数提示整个不见」。
|
|
10962
|
+
* @author ddj 2026年09月22号
|
|
10963
|
+
* @param result 原始响应
|
|
10964
|
+
* @returns 归一化结果;无有效签名时 null
|
|
10965
|
+
*/
|
|
10966
|
+
function toSignatureHelp(result) {
|
|
10967
|
+
if (!result || typeof result !== "object") return null;
|
|
10968
|
+
const obj = result;
|
|
10969
|
+
if (!Array.isArray(obj.signatures)) return null;
|
|
10970
|
+
const signatures = obj.signatures.map(toSignature).filter((signature) => signature !== null);
|
|
10971
|
+
if (!signatures.length) return null;
|
|
10972
|
+
const activeSignature = Math.min(Math.max(0, toInt(obj.activeSignature)), signatures.length - 1);
|
|
10973
|
+
const current = signatures[activeSignature];
|
|
10974
|
+
const rawActive = typeof obj.activeParameter === "number" ? toInt(obj.activeParameter) : current.activeParameter ?? 0;
|
|
10975
|
+
const count = current.parameters.length;
|
|
10976
|
+
return {
|
|
10977
|
+
signatures,
|
|
10978
|
+
activeSignature,
|
|
10979
|
+
activeParameter: count > 0 ? Math.min(Math.max(0, rawActive), count - 1) : 0
|
|
10980
|
+
};
|
|
10981
|
+
}
|
|
10982
|
+
/** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */ function stringifyHoverContents(contents) {
|
|
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
|