dsh-vscode-mode 0.5.0 → 0.5.2
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 +15 -2
- package/lib/client.js +518 -71
- package/lib/client.js.map +1 -1
- package/lib/index.js +193 -43
- package/lib/index.js.map +1 -1
- package/package.json +8 -7
- package/src/client/ai/inlineProvider.ts +42 -1
- package/src/client/index.ts +47 -45
- package/src/client/monaco/lsp/index.ts +33 -3
- package/src/client/monaco/lsp/lspClient.ts +3 -1
- package/src/client/monaco/lsp/providers.ts +32 -0
- package/src/client/officialSidebar.ts +67 -3
- package/src/client/sessionScope.ts +183 -0
- package/src/client/snippets/provider.ts +26 -4
- package/src/client/tabActions.ts +30 -5
- package/src/client/ui/AiSettings.ts +8 -0
- package/src/client/ui/EditorView.ts +51 -13
- package/src/compat.ts +3 -3
- package/src/dshVersion.ts +8 -1
- package/src/fileOpenSettings.ts +86 -10
- package/src/lsp/extmgr.ts +56 -3
- package/src/lsp/zip.ts +25 -9
- package/src/revert.ts +20 -8
- package/src/shared/ai.ts +25 -0
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { chmod, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, truncate, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, watch, writeFileSync } from "node:fs";
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { homedir, tmpdir } from "node:os";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -330,17 +330,83 @@ async function hostImport(specifier) {
|
|
|
330
330
|
return import(specifier);
|
|
331
331
|
}
|
|
332
332
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
333
|
+
* schema 库候选名(新名在前)。
|
|
334
|
+
* DSH 官方自 0.1.5 起把 vendored 包改名为 @deepseek-ai/schemastery 并全树改用新名,
|
|
335
|
+
* 安装树里**没有**裸 schemastery;裸名保留给 rc 线(其 dsh-settings 仍 peers 旧名),
|
|
336
|
+
* 也让开发形态(插件 node_modules 有 devDependency 副本)继续可用。
|
|
337
|
+
*/
|
|
338
|
+
const SCHEMA_SPECIFIERS = ["@deepseek-ai/schemastery", "schemastery"];
|
|
339
|
+
/** 设置持久化包名(installSettingsSection 仅 rc 线提供,alpha 线缺失属正常)。 */
|
|
340
|
+
const SETTINGS_SPECIFIER = "@deepseek-ai/dsh-settings";
|
|
341
|
+
/**
|
|
342
|
+
* 逐个尝试候选名,返回首个加载成功的模块及其包名(全失败返回 null,不抛错)。
|
|
343
|
+
* @author ddj 2026年09月18号
|
|
344
|
+
* @param specifiers 候选包名(按优先级)
|
|
345
|
+
* @param importFn 加载函数(测试注入)
|
|
346
|
+
* @returns 命中的模块与包名;全失败返回 null
|
|
347
|
+
*/
|
|
348
|
+
async function firstImport(specifiers, importFn = hostImport) {
|
|
349
|
+
for (const specifier of specifiers) try {
|
|
350
|
+
return {
|
|
351
|
+
specifier,
|
|
352
|
+
module: await importFn(specifier)
|
|
353
|
+
};
|
|
354
|
+
} catch {}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 抹平 schema 库的 ESM/CJS 互操作形态取默认导出。
|
|
359
|
+
* 三种实测形态:真 ESM(`default` 即 z)、CJS 经 import()(`default` 与
|
|
360
|
+
* `module.exports` 同为 z)、双层包装(`default.default` 才是 z)。
|
|
361
|
+
* @author ddj 2026年09月18号
|
|
362
|
+
* @param module 加载到的模块命名空间(可空)
|
|
363
|
+
* @returns 具备 object/string 等构造器的 z;取不到返回 null
|
|
364
|
+
*/
|
|
365
|
+
function pickSchema(module) {
|
|
366
|
+
const layers = [
|
|
367
|
+
module,
|
|
368
|
+
module?.default,
|
|
369
|
+
module?.["module.exports"]
|
|
370
|
+
];
|
|
371
|
+
for (const layer of layers) {
|
|
372
|
+
const z = layer?.default ?? layer;
|
|
373
|
+
if (z && typeof z.object === "function" && typeof z.string === "function") return z;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
/** 实际命中的 schema 库名(供兼容性报告展示;未命中为空串)。 */
|
|
378
|
+
let schemaLib = "";
|
|
379
|
+
/**
|
|
380
|
+
* 读取实际命中的 schema 库名(报告文案用)。
|
|
381
|
+
* @author ddj 2026年09月18号
|
|
382
|
+
* @returns 包名;未解析到为空串
|
|
383
|
+
*/
|
|
384
|
+
function schemaLibName() {
|
|
385
|
+
return schemaLib;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* 动态加载设置依赖(模块级缓存;schema 库缺失返回 null 而非抛错)。
|
|
389
|
+
* ⚠️ 两个依赖**独立解析**:@deepseek-ai/dsh-settings 仅 rc 线跑 legacy 策略时需要,
|
|
390
|
+
* alpha 线走 settings 服务 installSection 用不到它;原先 Promise.all 让该包缺失
|
|
391
|
+
* 拖垮整体 → 用户端(npm 安装)settings section 永不装配。
|
|
392
|
+
* 同理,schema 库按候选链解析(安装树只有新名 @deepseek-ai/schemastery)。
|
|
393
|
+
* @author ddj 2026年08月24号 / 2026年09月15号 / 2026年09月18号
|
|
394
|
+
* @param importFn 加载函数(测试注入;缺省宿主锚点动态导入)
|
|
337
395
|
* @returns 设置依赖或 null
|
|
338
396
|
*/
|
|
339
|
-
function loadSettingsDeps() {
|
|
340
|
-
if (!depsPromise) depsPromise = Promise.all([
|
|
341
|
-
|
|
342
|
-
z
|
|
343
|
-
|
|
397
|
+
function loadSettingsDeps(importFn = hostImport) {
|
|
398
|
+
if (!depsPromise) depsPromise = Promise.all([firstImport([SETTINGS_SPECIFIER], importFn), firstImport(SCHEMA_SPECIFIERS, importFn)]).then(([settingsHit, schemaHit]) => {
|
|
399
|
+
const z = pickSchema(schemaHit?.module);
|
|
400
|
+
if (!z) {
|
|
401
|
+
schemaLib = "";
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
schemaLib = schemaHit?.specifier ?? "";
|
|
405
|
+
return {
|
|
406
|
+
installSettingsSection: (settingsHit?.module)?.installSettingsSection,
|
|
407
|
+
z
|
|
408
|
+
};
|
|
409
|
+
}).catch(() => null);
|
|
344
410
|
return depsPromise;
|
|
345
411
|
}
|
|
346
412
|
const INSTALL_UNMOUNTED = "设置 section 尚未装配";
|
|
@@ -824,10 +890,17 @@ function inDshRange(version, range) {
|
|
|
824
890
|
}
|
|
825
891
|
return true;
|
|
826
892
|
}
|
|
827
|
-
/**
|
|
893
|
+
/**
|
|
894
|
+
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
895
|
+
* 逐线自新到旧匹配,先命中先返回(区间无上界,故顺序即优先级)。
|
|
896
|
+
* @author ddj 2026年09月02号 / 2026年09月18号
|
|
897
|
+
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
898
|
+
* @returns 版本线标签
|
|
899
|
+
*/
|
|
828
900
|
function familyLabel(input) {
|
|
829
901
|
const version = parseDshVersion(input);
|
|
830
902
|
if (!version) return "未知";
|
|
903
|
+
if (inDshRange(version, { from: "0.1.6-alpha.2" })) return "0.1.6-alpha.2 及更新(会话多实例共存 + 回合改动卡片 + Office 侧栏预览 + 侧栏浏览器)";
|
|
831
904
|
if (inDshRange(version, { from: "0.1.6-alpha.1" })) return "0.1.6-alpha 及更新(MCP SDK v2 + Web 侧边栏终端 + 文件链接默认侧栏预览)";
|
|
832
905
|
if (inDshRange(version, { from: "0.1.5-alpha.1" })) return "0.1.5-alpha 及更新(官方右侧 Sidebar 编辑区 + sidebar.panellist)";
|
|
833
906
|
if (inDshRange(version, { from: "0.1.3-alpha.1" })) return "0.1.3-alpha 及更新(对话文件链接=remote.session.openWorkspacePath)";
|
|
@@ -1691,7 +1764,7 @@ function detectExternal(ctx, depsAvailable) {
|
|
|
1691
1764
|
{
|
|
1692
1765
|
name: "设置持久化(@deepseek-ai/dsh-settings)",
|
|
1693
1766
|
active: depsAvailable,
|
|
1694
|
-
note: depsAvailable ? "设置 section
|
|
1767
|
+
note: depsAvailable ? "设置 section 已安装(schema: " + (schemaLibName() || "未知") + ")" : "未安装:fileOpenTool 持久化降级为配置值"
|
|
1695
1768
|
},
|
|
1696
1769
|
{
|
|
1697
1770
|
name: "settings 服务",
|
|
@@ -1765,7 +1838,7 @@ function detectGuards(ctx) {
|
|
|
1765
1838
|
}];
|
|
1766
1839
|
}
|
|
1767
1840
|
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。 */
|
|
1768
|
-
const TESTED_DSH_MAX = "0.1.6-alpha.
|
|
1841
|
+
const TESTED_DSH_MAX = "0.1.6-alpha.2";
|
|
1769
1842
|
/** 版本适配机制状态行:DSH 版本探测 + 设置 section 安装策略。 */
|
|
1770
1843
|
function versionAdapters(dshVersion) {
|
|
1771
1844
|
const adapters = [];
|
|
@@ -1811,7 +1884,7 @@ async function buildReport(ctx, options) {
|
|
|
1811
1884
|
if (settingsInstallStrategy() === "none") warnings.push("设置 section 安装不可用:" + settingsInstallNote());
|
|
1812
1885
|
const parsed = parseDshVersion(dshVersion);
|
|
1813
1886
|
const testedMax = parseDshVersion(TESTED_DSH_MAX);
|
|
1814
|
-
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.6-alpha.
|
|
1887
|
+
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) warnings.push("DSH " + dshVersion + " 高于已实测版本(0.1.6-alpha.2):设置 API 按能力探测运行,异常时请回报适配矩阵");
|
|
1815
1888
|
for (const guard of guards) if (!guard.active && guard.note) warnings.push(guard.note);
|
|
1816
1889
|
return {
|
|
1817
1890
|
pluginVersion: options?.version ?? pluginVersionOf(),
|
|
@@ -4249,8 +4322,33 @@ function newContentSearcher(ctx) {
|
|
|
4249
4322
|
//#endregion
|
|
4250
4323
|
//#region src/revert.ts
|
|
4251
4324
|
/**
|
|
4325
|
+
* 删除单文件的 argv(纯函数,platform 可注入便于单测)。
|
|
4326
|
+
* Windows 走 PowerShell Remove-Item;其余平台走 /bin/rm。
|
|
4327
|
+
* 原先无条件先发 powershell:macOS/Linux 上必然 spawn 失败后才回落,
|
|
4328
|
+
* 每删一个文件多一次无谓 spawn 与失败噪声。
|
|
4329
|
+
* @author ddj 2026年09月18号
|
|
4330
|
+
* @param absPath 绝对路径
|
|
4331
|
+
* @param platform 目标平台(缺省当前进程平台)
|
|
4332
|
+
* @returns 删除命令 argv
|
|
4333
|
+
*/
|
|
4334
|
+
function removeFileArgv(absPath, platform = process.platform) {
|
|
4335
|
+
if (platform === "win32") return [
|
|
4336
|
+
"powershell",
|
|
4337
|
+
"-NoProfile",
|
|
4338
|
+
"-NonInteractive",
|
|
4339
|
+
"-Command",
|
|
4340
|
+
"Remove-Item -LiteralPath \"" + absPath + "\" -Force"
|
|
4341
|
+
];
|
|
4342
|
+
return [
|
|
4343
|
+
"/bin/rm",
|
|
4344
|
+
"-f",
|
|
4345
|
+
"--",
|
|
4346
|
+
absPath
|
|
4347
|
+
];
|
|
4348
|
+
}
|
|
4349
|
+
/**
|
|
4252
4350
|
* 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
|
|
4253
|
-
* @author ddj 2026年08月20号
|
|
4351
|
+
* @author ddj 2026年08月20号 / 2026年09月18号
|
|
4254
4352
|
* @returns 成功或失败原因
|
|
4255
4353
|
*/
|
|
4256
4354
|
async function deleteCreated(ctx, session, record) {
|
|
@@ -4286,29 +4384,13 @@ async function deleteCreated(ctx, session, record) {
|
|
|
4286
4384
|
}
|
|
4287
4385
|
};
|
|
4288
4386
|
try {
|
|
4289
|
-
await attempt(
|
|
4290
|
-
"powershell",
|
|
4291
|
-
"-NoProfile",
|
|
4292
|
-
"-NonInteractive",
|
|
4293
|
-
"-Command",
|
|
4294
|
-
"Remove-Item -LiteralPath \"" + p + "\" -Force"
|
|
4295
|
-
]);
|
|
4387
|
+
await attempt(removeFileArgv(p));
|
|
4296
4388
|
return { ok: true };
|
|
4297
4389
|
} catch (error) {
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
"--",
|
|
4303
|
-
p
|
|
4304
|
-
]);
|
|
4305
|
-
return { ok: true };
|
|
4306
|
-
} catch (error2) {
|
|
4307
|
-
return {
|
|
4308
|
-
ok: false,
|
|
4309
|
-
error: "删除失败(文件仍存在):" + String(error) + " / " + String(error2)
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
4390
|
+
return {
|
|
4391
|
+
ok: false,
|
|
4392
|
+
error: "删除失败(文件仍存在):" + String(error)
|
|
4393
|
+
};
|
|
4312
4394
|
}
|
|
4313
4395
|
}
|
|
4314
4396
|
/** 整调用回滚:write/编辑用 before 整文件恢复;新建文件转 deleteCreated。 */
|
|
@@ -9857,9 +9939,9 @@ const CDIR_SIG = 33639248;
|
|
|
9857
9939
|
const LHDR_SIG = 67324752;
|
|
9858
9940
|
/**
|
|
9859
9941
|
* 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
|
|
9860
|
-
* @author ddj 2026年08月27号
|
|
9942
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
9861
9943
|
* @param buf zip 字节
|
|
9862
|
-
* @returns
|
|
9944
|
+
* @returns 条目元数据(含 Unix mode,见 modeOf)
|
|
9863
9945
|
*/
|
|
9864
9946
|
function zipEntries(buf) {
|
|
9865
9947
|
const eocd = findEocd(buf);
|
|
@@ -9877,22 +9959,36 @@ function zipEntries(buf) {
|
|
|
9877
9959
|
const commentLen = buf.readUInt16LE(offset + 32);
|
|
9878
9960
|
const localOffset = buf.readUInt32LE(offset + 42);
|
|
9879
9961
|
const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString("utf8");
|
|
9962
|
+
const mode = modeOf(buf.readUInt32LE(offset + 38));
|
|
9880
9963
|
out.push({
|
|
9881
9964
|
name,
|
|
9882
9965
|
method,
|
|
9883
9966
|
compressed,
|
|
9884
9967
|
uncompressed,
|
|
9885
|
-
localOffset
|
|
9968
|
+
localOffset,
|
|
9969
|
+
...mode === void 0 ? {} : { mode }
|
|
9886
9970
|
});
|
|
9887
9971
|
offset += 46 + nameLen + extraLen + commentLen;
|
|
9888
9972
|
}
|
|
9889
9973
|
return out;
|
|
9890
9974
|
}
|
|
9891
9975
|
/**
|
|
9976
|
+
* 中央目录 external attributes → Unix 权限位。
|
|
9977
|
+
* ZIP 把 Unix mode 放在 `st_mode << 16`(含 S_IFREG 等类型位),低 16 位仅 DOS 属性;
|
|
9978
|
+
* 故必须右移 16 再取低 9 位权限。非 Unix 归档(Windows 打包)该字段为 0 或纯 DOS 位。
|
|
9979
|
+
* @author ddj 2026年09月18号
|
|
9980
|
+
* @param attributes 中央目录 offset+38 的 4 字节
|
|
9981
|
+
* @returns 0o000~0o777 权限位;无有效 mode 返回 undefined
|
|
9982
|
+
*/
|
|
9983
|
+
function modeOf(attributes) {
|
|
9984
|
+
const mode = attributes >>> 16 & 511;
|
|
9985
|
+
return mode === 0 ? void 0 : mode;
|
|
9986
|
+
}
|
|
9987
|
+
/**
|
|
9892
9988
|
* 解压 ZIP → 条目列表(目录条目含尾部 /)。
|
|
9893
|
-
* @author ddj 2026年08月27号
|
|
9989
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
9894
9990
|
* @param buf zip 字节
|
|
9895
|
-
* @returns
|
|
9991
|
+
* @returns 条目(含内容与可选 mode)
|
|
9896
9992
|
*/
|
|
9897
9993
|
function unzip(buf) {
|
|
9898
9994
|
const metas = zipEntries(buf);
|
|
@@ -9912,7 +10008,8 @@ function unzip(buf) {
|
|
|
9912
10008
|
out.push({
|
|
9913
10009
|
path: meta.name,
|
|
9914
10010
|
data,
|
|
9915
|
-
isDirectory: isDir
|
|
10011
|
+
isDirectory: isDir,
|
|
10012
|
+
...meta.mode === void 0 ? {} : { mode: meta.mode }
|
|
9916
10013
|
});
|
|
9917
10014
|
}
|
|
9918
10015
|
return out;
|
|
@@ -9953,8 +10050,60 @@ function vsixManifest(vsix) {
|
|
|
9953
10050
|
throw new Error("VSIX 缺少 extension/package.json");
|
|
9954
10051
|
}
|
|
9955
10052
|
/**
|
|
10053
|
+
* 判定解包路径是否为可执行入口(无 mode 时的启发式兜底)。
|
|
10054
|
+
* 只认确定语义:`bin/` 段下的文件、语言服务器/运行时常见可执行名、`.sh` 脚本。
|
|
10055
|
+
* @author ddj 2026年09月18号
|
|
10056
|
+
* @param rel 相对路径(/ 分隔)
|
|
10057
|
+
* @returns 是否应补执行位
|
|
10058
|
+
*/
|
|
10059
|
+
function looksExec(rel) {
|
|
10060
|
+
const path = String(rel ?? "").replace(/\\/g, "/");
|
|
10061
|
+
if (!path) return false;
|
|
10062
|
+
if (path.split("/").includes("bin")) return true;
|
|
10063
|
+
if (/\.(sh|bash|zsh)$/i.test(path)) return true;
|
|
10064
|
+
const base = (path.split("/").pop() ?? "").replace(/\.exe$/i, "");
|
|
10065
|
+
return [
|
|
10066
|
+
"lua-language-server",
|
|
10067
|
+
"emmylua_ls",
|
|
10068
|
+
"OmniSharp",
|
|
10069
|
+
"dotnet",
|
|
10070
|
+
"node"
|
|
10071
|
+
].includes(base);
|
|
10072
|
+
}
|
|
10073
|
+
/**
|
|
10074
|
+
* 计算解包后要施加的权限位(纯函数,可单测)。
|
|
10075
|
+
* 优先信任归档自带的 Unix mode 中的执行位(VS Code 自身解 VSIX 即此口径);
|
|
10076
|
+
* 归档无 mode(Windows 打包的 VSIX)时按路径启发式补 0o755;
|
|
10077
|
+
* 其余保持 undefined(交给 umask 默认,不越权改动)。
|
|
10078
|
+
* @author ddj 2026年09月18号
|
|
10079
|
+
* @param entry 解压条目
|
|
10080
|
+
* @returns 目标权限位;无需处理返回 undefined
|
|
10081
|
+
*/
|
|
10082
|
+
function execModeOf(entry) {
|
|
10083
|
+
if (entry.isDirectory) return void 0;
|
|
10084
|
+
const mode = entry.mode;
|
|
10085
|
+
if (mode !== void 0 && (mode & 73) !== 0) return mode;
|
|
10086
|
+
return looksExec(entry.path) ? 493 : void 0;
|
|
10087
|
+
}
|
|
10088
|
+
/**
|
|
10089
|
+
* 落盘后补可执行位(POSIX 专属;Windows 无此概念,直接跳过)。
|
|
10090
|
+
* 必须 best-effort:只读文件系统/无权限时不能拖垮整个扩展安装。
|
|
10091
|
+
* @author ddj 2026年09月18号
|
|
10092
|
+
* @param target 落盘绝对路径
|
|
10093
|
+
* @param entry 解压条目
|
|
10094
|
+
* @param platform 目标平台(测试注入)
|
|
10095
|
+
*/
|
|
10096
|
+
function applyExecBit(target, entry, platform = process.platform) {
|
|
10097
|
+
if (platform === "win32") return;
|
|
10098
|
+
const mode = execModeOf(entry);
|
|
10099
|
+
if (mode === void 0) return;
|
|
10100
|
+
try {
|
|
10101
|
+
chmodSync(target, mode);
|
|
10102
|
+
} catch (error) {}
|
|
10103
|
+
}
|
|
10104
|
+
/**
|
|
9956
10105
|
* 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
|
|
9957
|
-
* @author ddj 2026年08月27号
|
|
10106
|
+
* @author ddj 2026年08月27号 / 2026年09月18号
|
|
9958
10107
|
* @param vsix vsix 字节
|
|
9959
10108
|
* @param dest 目标目录(已存在则先清空)
|
|
9960
10109
|
* @returns 清单对象
|
|
@@ -9978,6 +10127,7 @@ function unpackVsix(vsix, dest) {
|
|
|
9978
10127
|
}
|
|
9979
10128
|
mkdirSync(dirname(target), { recursive: true });
|
|
9980
10129
|
writeFileSync(target, entry.data);
|
|
10130
|
+
applyExecBit(target, entry);
|
|
9981
10131
|
if (rel === "package.json") manifest = JSON.parse(entry.data.toString("utf8"));
|
|
9982
10132
|
}
|
|
9983
10133
|
if (!manifest) throw new Error("VSIX 解包后缺少 package.json");
|