dsh-vscode-mode 0.3.1 → 0.3.3

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/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, 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, writeFileSync } from "node:fs";
4
+ import { 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";
@@ -132,7 +132,8 @@ const KEYBINDING_DEFAULTS = {
132
132
  "edrv.showCommands": "Ctrl+Shift+P|F1",
133
133
  "edrv.nextEditorRow": "Ctrl+Alt+ArrowDown",
134
134
  "edrv.prevEditorRow": "Ctrl+Alt+ArrowUp",
135
- "edrv.addSelectionRef": "Ctrl+U"
135
+ "edrv.addSelectionRef": "Ctrl+U",
136
+ "edrv.closeTab": "Ctrl+F4"
136
137
  };
137
138
  //#endregion
138
139
  //#region src/shared/integration.ts
@@ -948,6 +949,16 @@ function assetsDirOf(moduleUrl) {
948
949
  function vendorDirOf(moduleUrl) {
949
950
  return join(assetsDirOf(moduleUrl), "vendor");
950
951
  }
952
+ /**
953
+ * 插件自带技能组根目录(包根 skills/,与 assets/ 同级;随包发布)。
954
+ * 布局:<根>/<技能名>/SKILL.md(目录式)或 <根>/<技能名>.md(扁平式)。
955
+ * @author ddj 2026年09月11号
956
+ * @param moduleUrl 模块 URL(缺省 import.meta.url;测试注入)
957
+ * @returns 技能组根目录绝对路径
958
+ */
959
+ function skillsDirOf(moduleUrl) {
960
+ return join(dirname(fileURLToPath(moduleUrl)), "..", "skills");
961
+ }
951
962
  /** 图标目录:config.imageDir 覆盖优先,否则插件包 assets/。 */
952
963
  function imageDirOf(config, moduleUrl) {
953
964
  const cfg = config;
@@ -1065,6 +1076,450 @@ async function sweepTreeCache(home = dshHome(), cap = CACHE_TOTAL_CAP) {
1065
1076
  return removed;
1066
1077
  }
1067
1078
  //#endregion
1079
+ //#region src/skills.ts
1080
+ /**
1081
+ * dsh-vscode-mode host — 插件自带技能组(随包 skills/ 分发的 SKILL.md)。
1082
+ * - 存储:<包根>/skills/<技能名>/SKILL.md(目录式)或 <包根>/skills/<技能名>.md(扁平式),随包发布。
1083
+ * - 命名:技能名必须 kebab-case(DSH 硬约束,下划线会被 registry 拒绝)且以 dsh-vscodemode- 开头。
1084
+ * - 生效:注册自研 skill provider 到 ctx.skills(ctx.inject 惰性获取,服务缺失时插件仍完整可用);
1085
+ * 文件变更经 fs.watch → control.invalidate() 即时可见。
1086
+ * 为什么自研 provider 而不用 @deepseek-ai/dsh-skill-filesystem:见 README「插件技能组」小节。
1087
+ * --region 划分:常量 / 类型 / frontmatter 解析(纯)/ 目录扫描(只读)/ provider / 挂载与状态
1088
+ * 作者 ddj 2026年09月11号
1089
+ */
1090
+ /** provider 名(注册进 ctx.skills;不得与 'filesystem' / 'openviking' 及保留名 'runtime' 冲突)。 */
1091
+ const SKILL_PROVIDER_NAME = "dsh-vscodemode";
1092
+ /** 技能组前缀白名单:名字不在其中的技能文件被跳过并告警。 */
1093
+ const SKILL_PREFIXES = ["dsh-vscodemode-"];
1094
+ /** 技能发现来源标签(skill-explorer 归入 "System bundled" 组)。 */
1095
+ const SKILL_SOURCE = "bundled";
1096
+ /** 目录式技能的文件名。 */
1097
+ const SKILL_FILE = "SKILL.md";
1098
+ /** DSH 技能名语法(与 @deepseek-ai/dsh-skill 的 SKILL_NAME 一致:下划线非法)。 */
1099
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1100
+ /** 不受支持的遗留字段 → 规范字段(官方同样拒绝,避免写错键位却静默无效果)。 */
1101
+ const LEGACY_SKILL_KEYS = [
1102
+ ["disableModelInvocation", "disable-model-invocation"],
1103
+ ["modelInvocable", "disable-model-invocation"],
1104
+ ["userInvocable", "user-invocable"]
1105
+ ];
1106
+ /**
1107
+ * 去除标量值两侧成对引号。
1108
+ * 与 rules.ts 的同名私有工具语义一致;两处解析器面向不同格式(.mdc 规则 / SKILL.md),
1109
+ * 各自保持模块自治,避免为一处 4 行字符串处理引入跨模块耦合。
1110
+ * @author ddj 2026年09月11号
1111
+ * @param raw 原始标量文本
1112
+ * @returns 去引号后的文本
1113
+ */
1114
+ function stripQuotes$1(raw) {
1115
+ const value = raw.trim();
1116
+ return value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) ? value.slice(1, -1) : value;
1117
+ }
1118
+ /**
1119
+ * 解析 YAML 布尔标量(true/false/yes/no/on/off/1/0,大小写不敏感)。
1120
+ * @author ddj 2026年09月11号
1121
+ * @param raw 原始标量文本
1122
+ * @returns 布尔值;非布尔字面量返回 undefined
1123
+ */
1124
+ function parseBool(raw) {
1125
+ const value = raw.trim().toLowerCase();
1126
+ if (value === "true" || value === "yes" || value === "on" || value === "1") return true;
1127
+ if (value === "false" || value === "no" || value === "off" || value === "0") return false;
1128
+ }
1129
+ /**
1130
+ * 切出 frontmatter 与正文:首行必须是独立的 `---`,其后需有独立的闭合 `---`。
1131
+ * 容忍 BOM 与 CRLF;不满足即视为无 frontmatter(调用方按"忽略该文件"处理)。
1132
+ * @author ddj 2026年09月11号
1133
+ * @param text SKILL.md 全文
1134
+ * @returns frontmatter 行与正文;无合法 frontmatter 返回 null
1135
+ */
1136
+ function splitFrontmatter(text) {
1137
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
1138
+ if (lines[0] !== "---") return null;
1139
+ let close = -1;
1140
+ for (let i = 1; i < lines.length; i++) if (lines[i] === "---") {
1141
+ close = i;
1142
+ break;
1143
+ }
1144
+ if (close < 0) return null;
1145
+ return {
1146
+ fields: lines.slice(1, close),
1147
+ body: lines.slice(close + 1).join("\n").trim()
1148
+ };
1149
+ }
1150
+ /**
1151
+ * 读取块标量(`|` / `>` 及其 chomping/缩进指示符后接的缩进行),返回文本与下一个待扫描位置。
1152
+ * 折叠式(`>`)行间以空格连接,字面式(`|`)以换行连接;两者末尾均 trim。
1153
+ * @author ddj 2026年09月11号
1154
+ * @param fields frontmatter 行
1155
+ * @param start 起始下标
1156
+ * @param folded 是否为折叠式(`>`)
1157
+ * @returns 块文本与下一扫描位置
1158
+ */
1159
+ function readBlock(fields, start, folded) {
1160
+ const parts = [];
1161
+ let i = start;
1162
+ while (i < fields.length && (fields[i].trim() === "" || /^[ \t]/.test(fields[i]))) {
1163
+ const line = fields[i].trim();
1164
+ if (line !== "") parts.push(line);
1165
+ i += 1;
1166
+ }
1167
+ return {
1168
+ value: parts.join(folded ? " " : "\n").trim(),
1169
+ next: i
1170
+ };
1171
+ }
1172
+ /**
1173
+ * 把 frontmatter 行解析为键值映射(仅支持本插件用到的标量形式:内联标量与 `|`/`>` 块标量)。
1174
+ * @author ddj 2026年09月11号
1175
+ * @param fields frontmatter 行
1176
+ * @returns 键值映射(无法识别的行跳过)
1177
+ */
1178
+ function collectFields(fields) {
1179
+ const out = {};
1180
+ for (let i = 0; i < fields.length; i++) {
1181
+ const match = /^([A-Za-z0-9_-]+)[ \t]*:[ \t]?(.*)$/.exec(fields[i]);
1182
+ if (!match) continue;
1183
+ const key = match[1];
1184
+ const inline = match[2];
1185
+ const block = /^([|>])[+-]?\d*$/.exec(inline.trim());
1186
+ if (block === null) {
1187
+ out[key] = stripQuotes$1(inline);
1188
+ continue;
1189
+ }
1190
+ const read = readBlock(fields, i + 1, block[1] === ">");
1191
+ out[key] = read.value;
1192
+ i = read.next - 1;
1193
+ }
1194
+ return out;
1195
+ }
1196
+ /**
1197
+ * 由 frontmatter 推导可见性策略(缺省两者皆 true;值为非布尔字面量时报错)。
1198
+ * @author ddj 2026年09月11号
1199
+ * @param data frontmatter 键值映射
1200
+ * @returns 策略与可选错误文案
1201
+ */
1202
+ function toInvocation(data) {
1203
+ const defaults = {
1204
+ modelInvocable: true,
1205
+ userInvocable: true
1206
+ };
1207
+ const disabled = parseBool(data["disable-model-invocation"] ?? "");
1208
+ if (disabled === void 0 && data["disable-model-invocation"] !== void 0) return {
1209
+ policy: defaults,
1210
+ error: "frontmatter 字段 \"disable-model-invocation\" 必须是布尔值"
1211
+ };
1212
+ const user = parseBool(data["user-invocable"] ?? "");
1213
+ if (user === void 0 && data["user-invocable"] !== void 0) return {
1214
+ policy: defaults,
1215
+ error: "frontmatter 字段 \"user-invocable\" 必须是布尔值"
1216
+ };
1217
+ return { policy: {
1218
+ modelInvocable: disabled !== true,
1219
+ userInvocable: user !== false
1220
+ } };
1221
+ }
1222
+ /**
1223
+ * 判断解析结果是否为成功形态。
1224
+ * @author ddj 2026年09月11号
1225
+ * @param value 解析结果
1226
+ * @returns 是否为 ParsedSkill
1227
+ */
1228
+ function isParsedSkill(value) {
1229
+ return !("error" in value);
1230
+ }
1231
+ /**
1232
+ * 解析一条 SKILL.md(纯函数,永不抛错)。
1233
+ * @author ddj 2026年09月11号
1234
+ * @param text SKILL.md 全文
1235
+ * @returns 解析结果(成功含正文与元数据;失败含拒绝原因)
1236
+ */
1237
+ function parseSkillMd(text) {
1238
+ const parts = splitFrontmatter(text);
1239
+ if (parts === null) return { error: "缺少合法 frontmatter(首行须为 --- 且存在闭合 ---)" };
1240
+ const data = collectFields(parts.fields);
1241
+ const legacy = LEGACY_SKILL_KEYS.find(([key]) => data[key] !== void 0);
1242
+ if (legacy !== void 0) return { error: "frontmatter 字段 \"" + legacy[0] + "\" 不受支持,请改用 \"" + legacy[1] + "\"" };
1243
+ const name = (data.name ?? "").trim();
1244
+ if (!SKILL_NAME_RE.test(name)) return { error: "非法技能名 \"" + name + "\"(须 kebab-case:小写字母/数字,段间连字符;下划线非法)" };
1245
+ const description = (data.description ?? "").trim();
1246
+ if (!description) return { error: "技能 \"" + name + "\" 缺少 description" };
1247
+ const invocation = toInvocation(data);
1248
+ if (invocation.error !== void 0) return { error: invocation.error };
1249
+ const whenToUse = (data.whenToUse ?? "").trim();
1250
+ return {
1251
+ name,
1252
+ description,
1253
+ ...whenToUse ? { whenToUse } : {},
1254
+ invocation: invocation.policy,
1255
+ body: parts.body
1256
+ };
1257
+ }
1258
+ /**
1259
+ * 判断技能名是否落在技能组前缀白名单内。
1260
+ * @author ddj 2026年09月11号
1261
+ * @param name 技能名
1262
+ * @returns 是否命中前缀
1263
+ */
1264
+ function hasGroupPrefix(name) {
1265
+ return SKILL_PREFIXES.some((prefix) => name.startsWith(prefix));
1266
+ }
1267
+ /**
1268
+ * 目录条目 → 候选文件路径(目录式取 <名>/SKILL.md;扁平式取 <名>.md;其余跳过)。
1269
+ * @author ddj 2026年09月11号
1270
+ * @param dir 技能组根目录
1271
+ * @param entry 目录条目
1272
+ * @returns 候选文件绝对路径;不构成技能时返回 undefined
1273
+ */
1274
+ function skillFileOf(dir, entry) {
1275
+ if (entry.isDirectory()) return join(dir, entry.name, SKILL_FILE);
1276
+ if (entry.isFile() && entry.name.endsWith(".md")) return join(dir, entry.name);
1277
+ }
1278
+ /**
1279
+ * 由已解析的技能构造候选(provider 名/rank/source/resourceBase 按 registry 契约填充)。
1280
+ * @author ddj 2026年09月11号
1281
+ * @param parsed 解析成功的技能
1282
+ * @param file SKILL.md 绝对路径
1283
+ * @returns registry 候选
1284
+ */
1285
+ function candidateFrom(parsed, file) {
1286
+ return {
1287
+ name: parsed.name,
1288
+ description: parsed.description,
1289
+ ...parsed.whenToUse !== void 0 ? { whenToUse: parsed.whenToUse } : {},
1290
+ invocation: parsed.invocation,
1291
+ source: SKILL_SOURCE,
1292
+ provider: SKILL_PROVIDER_NAME,
1293
+ rank: 300,
1294
+ locator: { path: file },
1295
+ path: file,
1296
+ resourceBase: {
1297
+ kind: "directory",
1298
+ path: dirname(file)
1299
+ }
1300
+ };
1301
+ }
1302
+ /**
1303
+ * 读盘并校验单个技能文件(缺失/读失败/解析失败/非本组前缀 → undefined + 告警)。
1304
+ * @author ddj 2026年09月11号
1305
+ * @param file SKILL.md 绝对路径
1306
+ * @returns registry 候选;不可用时 undefined
1307
+ */
1308
+ async function candidateOf$1(file) {
1309
+ const info = await stat(file).catch(() => void 0);
1310
+ if (info === void 0 || !info.isFile()) return void 0;
1311
+ const text = await readFile(file, "utf8").catch(() => void 0);
1312
+ if (text === void 0) return void 0;
1313
+ const parsed = parseSkillMd(text);
1314
+ if (!isParsedSkill(parsed)) {
1315
+ log.warn("技能已忽略(" + file + "):" + parsed.error);
1316
+ return;
1317
+ }
1318
+ if (!hasGroupPrefix(parsed.name)) {
1319
+ log.warn("技能已忽略(" + file + "):名字 \"" + parsed.name + "\" 不在技能组前缀 " + SKILL_PREFIXES.join("/") + " 内");
1320
+ return;
1321
+ }
1322
+ return candidateFrom(parsed, file);
1323
+ }
1324
+ /**
1325
+ * 扫描技能组目录(目录缺失/读取失败 → 空数组,不抛)。
1326
+ * @author ddj 2026年09月11号
1327
+ * @param dir 技能组根目录
1328
+ * @returns 通过校验的候选(按名字排序,受 SKILL_DIR_CAP 约束)
1329
+ */
1330
+ async function listSkills(dir) {
1331
+ const ordered = [...await readdir(dir, { withFileTypes: true }).catch(() => [])].sort((a, b) => a.name.localeCompare(b.name)).slice(0, 200);
1332
+ const found = [];
1333
+ for (const entry of ordered) {
1334
+ const file = skillFileOf(dir, entry);
1335
+ if (file === void 0) continue;
1336
+ const candidate = await candidateOf$1(file);
1337
+ if (candidate !== void 0) found.push(candidate);
1338
+ }
1339
+ return found;
1340
+ }
1341
+ /**
1342
+ * 解析候选的落盘路径(locator 优先,回退 candidate.path)。
1343
+ * @author ddj 2026年09月11号
1344
+ * @param candidate registry 候选
1345
+ * @returns 绝对路径;不可解析时 undefined
1346
+ */
1347
+ function locatorPath(candidate) {
1348
+ const locator = candidate.locator;
1349
+ if (locator !== void 0 && typeof locator.path === "string") return locator.path;
1350
+ return typeof candidate.path === "string" ? candidate.path : void 0;
1351
+ }
1352
+ /**
1353
+ * 加载候选的完整技能定义(重读盘;文件消失或名字变化 → undefined,让 registry 自行失效缓存)。
1354
+ * @author ddj 2026年09月11号
1355
+ * @param candidate registry 候选
1356
+ * @returns 完整定义;不可用时 undefined
1357
+ */
1358
+ async function loadSkill(candidate) {
1359
+ const file = locatorPath(candidate);
1360
+ if (file === void 0) return void 0;
1361
+ const text = await readFile(file, "utf8").catch(() => void 0);
1362
+ if (text === void 0) return void 0;
1363
+ const parsed = parseSkillMd(text);
1364
+ if (!isParsedSkill(parsed) || parsed.name !== candidate.name) return void 0;
1365
+ return {
1366
+ ...candidateFrom(parsed, file),
1367
+ content: parsed.body
1368
+ };
1369
+ }
1370
+ /**
1371
+ * 关闭 watcher(注册被释放时的收尾;失败不影响装配)。
1372
+ * @author ddj 2026年09月11号
1373
+ * @param watcher 文件监听器
1374
+ */
1375
+ function closeWatcher(watcher) {
1376
+ try {
1377
+ watcher.close();
1378
+ } catch (error) {}
1379
+ }
1380
+ /**
1381
+ * 监听技能目录:变更 → control.invalidate()(registry 有收集缓存,必须失效才即时可见)。
1382
+ * 注册被释放时经 control.signal 关闭;监听不可用仅降级为"改动需重载插件",不影响正确性。
1383
+ * @author ddj 2026年09月11号
1384
+ * @param dir 技能组根目录
1385
+ * @param control registry 借出的控制面
1386
+ */
1387
+ function watchSkillDir(dir, control) {
1388
+ if (!existsSync(dir)) return;
1389
+ try {
1390
+ const watcher = watch(dir, {
1391
+ recursive: true,
1392
+ persistent: false
1393
+ }, () => control.invalidate());
1394
+ watcher.on("error", (error) => log.warn("技能目录监听中断(改动需重载插件生效):" + String(error)));
1395
+ control.signal.addEventListener("abort", () => closeWatcher(watcher), { once: true });
1396
+ } catch (error) {
1397
+ log.warn("技能目录监听不可用(改动需重载插件生效):" + String(error));
1398
+ }
1399
+ }
1400
+ /**
1401
+ * 创建技能组 provider(注册进 ctx.skills)。
1402
+ * @author ddj 2026年09月11号
1403
+ * @param dir 技能组根目录
1404
+ * @param control registry 借出的控制面
1405
+ * @returns provider 实例
1406
+ */
1407
+ function newSkillProvider(dir, control) {
1408
+ watchSkillDir(dir, control);
1409
+ return {
1410
+ name: SKILL_PROVIDER_NAME,
1411
+ list: () => listSkills(dir),
1412
+ get: (candidate) => loadSkill(candidate)
1413
+ };
1414
+ }
1415
+ /** 最近一次装配状态(兼容性页与启动日志读取;模块级单例,热重载后由新装配覆写)。 */
1416
+ let group = {
1417
+ dispatched: false,
1418
+ mounted: false,
1419
+ count: 0,
1420
+ dir: "",
1421
+ note: "未装配"
1422
+ };
1423
+ /**
1424
+ * 读取技能组装配状态(副本,调用方不可改写内部状态)。
1425
+ * @author ddj 2026年09月11号
1426
+ * @returns 状态快照
1427
+ */
1428
+ function skillGroupState() {
1429
+ return { ...group };
1430
+ }
1431
+ /**
1432
+ * 记录状态片段。
1433
+ * @author ddj 2026年09月11号
1434
+ * @param patch 待覆写字段
1435
+ */
1436
+ function recordGroup(patch) {
1437
+ group = {
1438
+ ...group,
1439
+ ...patch
1440
+ };
1441
+ }
1442
+ /**
1443
+ * 在 skills 就绪回调里注册 provider,并异步回填技能数。
1444
+ * @author ddj 2026年09月11号
1445
+ * @param sctx inject 回调给出的服务上下文
1446
+ * @param dir 技能组根目录
1447
+ */
1448
+ function mountGroup(sctx, dir) {
1449
+ const sc = sctx;
1450
+ const skills = (typeof sc?.get === "function" ? sc.get("skills") : void 0) ?? sc?.skills;
1451
+ const register = skills?.registerProvider;
1452
+ if (typeof register !== "function") {
1453
+ recordGroup({
1454
+ mounted: false,
1455
+ note: "skills 服务不可用或版本不含 registerProvider"
1456
+ });
1457
+ log.warn("技能组未挂载:skills 服务不可用,插件其余功能不受影响");
1458
+ return;
1459
+ }
1460
+ try {
1461
+ register.call(skills, (control) => newSkillProvider(dir, control));
1462
+ recordGroup({
1463
+ mounted: true,
1464
+ dir,
1465
+ note: "已挂载(等待技能扫描)"
1466
+ });
1467
+ } catch (error) {
1468
+ recordGroup({
1469
+ mounted: false,
1470
+ note: "provider 注册失败:" + String(error)
1471
+ });
1472
+ log.warn("技能组未挂载:provider 注册失败(" + String(error) + ")");
1473
+ return;
1474
+ }
1475
+ listSkills(dir).then((found) => {
1476
+ recordGroup({
1477
+ count: found.length,
1478
+ note: "已挂载 " + found.length + " 个技能"
1479
+ });
1480
+ log.info("插件技能组已挂载:" + SKILL_PREFIXES[0] + "* 共 " + found.length + " 个技能(" + dir + ")");
1481
+ }).catch((error) => log.warn("技能组扫描失败:" + String(error)));
1482
+ }
1483
+ /**
1484
+ * 装配插件技能组(惰性获取 skills 服务;服务缺失/版本过旧时降级记录,不抛错)。
1485
+ * 返回值只表示"是否已调度"——inject 回调异步执行,实际结果见 skillGroupState()。
1486
+ * @author ddj 2026年09月11号
1487
+ * @param ctx DSH host 上下文
1488
+ * @param dir 技能组根目录(缺省 import.meta.url 派生;测试注入)
1489
+ * @returns 是否已调度挂载
1490
+ */
1491
+ function installSkillGroup(ctx, dir = skillsDirOf(import.meta.url)) {
1492
+ const inject = ctx?.inject;
1493
+ if (typeof inject !== "function") {
1494
+ recordGroup({
1495
+ dispatched: false,
1496
+ mounted: false,
1497
+ dir,
1498
+ note: "ctx.inject 不可用(DSH 版本过旧),技能组未挂载"
1499
+ });
1500
+ log.warn("技能组未挂载:DSH 未提供 ctx.inject");
1501
+ return false;
1502
+ }
1503
+ recordGroup({
1504
+ dispatched: true,
1505
+ mounted: false,
1506
+ dir,
1507
+ note: "已调度 skills 服务装配(等待 skills 就绪)"
1508
+ });
1509
+ try {
1510
+ inject.call(ctx, ["skills"], (sctx) => mountGroup(sctx, dir));
1511
+ } catch (error) {
1512
+ recordGroup({
1513
+ dispatched: false,
1514
+ mounted: false,
1515
+ note: "挂载调度失败:" + String(error)
1516
+ });
1517
+ log.warn("技能组挂载调度失败:" + String(error));
1518
+ return false;
1519
+ }
1520
+ return true;
1521
+ }
1522
+ //#endregion
1068
1523
  //#region src/compat.ts
1069
1524
  /**
1070
1525
  * dsh-vscode-mode host — 兼容层:与其他插件 / DSH 版本的统一探测、适配、护栏与自诊断。
@@ -1103,6 +1558,7 @@ function detectExternal(ctx, depsAvailable) {
1103
1558
  const settings = ctx.get("settings");
1104
1559
  const hasSettings = Boolean(settings?.describe || settings?.update);
1105
1560
  const sub = ctx.get("subprocess");
1561
+ const skills = skillGroupState();
1106
1562
  return [
1107
1563
  {
1108
1564
  name: MCP_PACKAGE,
@@ -1123,6 +1579,11 @@ function detectExternal(ctx, depsAvailable) {
1123
1579
  name: "文件浏览器打开(subprocess 服务)",
1124
1580
  active: typeof sub?.spawn === "function",
1125
1581
  note: typeof sub?.spawn === "function" ? "可定位/打开 OS 文件浏览器" : "不可用(右键「在文件浏览器中打开」将提示失败)"
1582
+ },
1583
+ {
1584
+ name: "插件技能组(" + SKILL_PREFIXES[0] + "*)",
1585
+ active: skills.mounted,
1586
+ note: skills.note + (skills.mounted && skills.dir ? "(" + skills.dir + ")" : "")
1126
1587
  }
1127
1588
  ];
1128
1589
  }
@@ -11064,6 +11525,9 @@ function apply(ctx, config) {
11064
11525
  /** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
11065
11526
  const rulesInstalled = installRulesSection(ctx);
11066
11527
  if (!rulesInstalled) log.warn("未检测到 systemPrompt 服务,规则仅可管理不注入");
11528
+ /** 插件自带技能组(<包根>/skills,惰性获取 skills 服务;返回值仅表示"已调度",实际结果见 skillGroupState)。 */
11529
+ const skillsDispatched = installSkillGroup(ctx);
11530
+ if (!skillsDispatched) log.warn("技能组未调度,插件技能组不可用");
11067
11531
  /** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
11068
11532
  const lspRpc = createLspRpc({
11069
11533
  ctx,
@@ -11099,7 +11563,7 @@ function apply(ctx, config) {
11099
11563
  lspManager.disposeAll().catch(() => {});
11100
11564
  disposeAllServers();
11101
11565
  });
11102
- log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ")");
11566
+ log.info("编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入" + (rulesInstalled ? "已接入" : "未接入") + ",技能组" + (skillsDispatched ? "装配中" : "未装配") + ")");
11103
11567
  }
11104
11568
  /**
11105
11569
  * 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。