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/README.md +78 -3
- package/lib/client.js +1152 -209
- package/lib/client.js.map +1 -1
- package/lib/index.js +467 -3
- package/lib/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +181 -0
- package/src/client/addToConversation.ts +192 -20
- package/src/client/editorModelState.ts +18 -0
- package/src/client/openFlow.ts +2 -1
- package/src/client/paths.ts +4 -2
- package/src/client/saveDebounce.ts +69 -0
- package/src/client/sidebar/panels/FileExplorer.ts +83 -2
- package/src/client/state/scopeStore.ts +2 -1
- package/src/client/styles/editor.css +17 -2
- package/src/client/tabActions.ts +326 -0
- package/src/client/tabMenu.ts +107 -0
- package/src/client/ui/ContextMenu.ts +52 -8
- package/src/client/ui/EditorView.ts +221 -57
- package/src/client/ui/QuickOpen.ts +61 -13
- package/src/client/ui/commandCatalog.ts +17 -1
- package/src/client/ui/searchNav.ts +24 -0
- package/src/compat.ts +4 -0
- package/src/index.ts +5 -1
- package/src/paths.ts +11 -0
- package/src/shared/keybindings.ts +3 -0
- package/src/skills.ts +534 -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, 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
|
* 异步输出兼容性报告摘要(含重复装配/路由冲突自诊断)。
|