dsh-plugin-tool-management 0.2.0 → 0.5.1
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 +238 -226
- package/README_EN.md +304 -231
- package/cordis.patch.yml +11 -0
- package/docs/Changelog.md +517 -0
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
- package/lib/approval-policy.js +46 -0
- package/lib/client.js +3560 -2173
- package/lib/hub.js +94 -0
- package/lib/imports/upload.js +286 -0
- package/lib/index.js +512 -155
- package/lib/rules/archive-engine.js +191 -0
- package/lib/rules/archive.js +140 -0
- package/lib/rules/service.js +609 -100
- package/lib/skills/core.js +356 -22
- package/lib/skills/service.js +31 -5
- package/lib/subagents/service.js +333 -0
- package/lib/subagents/tools.js +62 -0
- package/package.json +12 -3
- package/screenshots.json +7 -5
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
package/lib/skills/core.js
CHANGED
|
@@ -71,6 +71,22 @@ export function userRoots() {
|
|
|
71
71
|
native: true,
|
|
72
72
|
rank: 400,
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
// v0.4:插件**新建/导入**的技能落到 hub 内(用户要求:插件产生的文件收在
|
|
76
|
+
// $DSH_HOME/tool-management/)。rank 高于 DSH 技能,同名时 hub 版本遮蔽官方目录里的;
|
|
77
|
+
// 官方 ~/.dsh/skills/ 仍作为可切换来源列出(不搬走、不删)。
|
|
78
|
+
//
|
|
79
|
+
// 界面名「导入技能」:它是插件导入/新建技能的落点,不是"管理器自己的一类技能"。
|
|
80
|
+
// **用户级来源不可删**(dsh / hub 都是):技能只能停用,删除留给项目级来源。
|
|
81
|
+
key: "hub",
|
|
82
|
+
path: join(resolveDshHome(), "tool-management", "skills"),
|
|
83
|
+
label: "导入技能",
|
|
84
|
+
localeKey: "hub",
|
|
85
|
+
mutable: true,
|
|
86
|
+
toggleable: true,
|
|
87
|
+
native: false,
|
|
88
|
+
rank: 350,
|
|
89
|
+
},
|
|
74
90
|
{
|
|
75
91
|
key: "agents",
|
|
76
92
|
path: join(resolveAgentsHome(), "skills"),
|
|
@@ -317,6 +333,9 @@ export async function projectRoots(projectCwds = [], diagnostics) {
|
|
|
317
333
|
const id = projectIdentity(project.root);
|
|
318
334
|
const common = {
|
|
319
335
|
mutable: false,
|
|
336
|
+
// 项目级 DSH 根是**唯一**允许删除技能文件的来源;用户级来源(dsh / hub)一律不可删。
|
|
337
|
+
// 用「作用域」推导而不是再加一个平行开关:这是项目根与用户根的本质差别。
|
|
338
|
+
deletable: false,
|
|
320
339
|
toggleable: false,
|
|
321
340
|
native: true,
|
|
322
341
|
scope: "project",
|
|
@@ -334,6 +353,7 @@ export async function projectRoots(projectCwds = [], diagnostics) {
|
|
|
334
353
|
label: "Project DSH",
|
|
335
354
|
rank: 100,
|
|
336
355
|
mutable: true,
|
|
356
|
+
deletable: true,
|
|
337
357
|
toggleable: true,
|
|
338
358
|
},
|
|
339
359
|
{
|
|
@@ -464,6 +484,14 @@ export async function browseDirectories(inputPath) {
|
|
|
464
484
|
function dshRootPath() {
|
|
465
485
|
return userRoots().find((root) => root.key === "dsh").path;
|
|
466
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* v0.4 新建/导入技能的落点:hub 内 `tool-management/skills/`(用户要求插件产物集中)。
|
|
489
|
+
* hub 根缺失(旧版本状态文件/异常)时退回官方 DSH 技能目录,保证创建功能永不因布局变化而失效。
|
|
490
|
+
*/
|
|
491
|
+
function skillCreateRootPath() {
|
|
492
|
+
const hub = userRoots().find((root) => root.key === "hub");
|
|
493
|
+
return hub && hub.path ? hub.path : dshRootPath();
|
|
494
|
+
}
|
|
467
495
|
/** 只读来源的拒绝结果;action 为可翻译语义值(toggle/delete)。 */
|
|
468
496
|
function readonlyError(action) {
|
|
469
497
|
return {
|
|
@@ -475,6 +503,19 @@ function readonlyError(action) {
|
|
|
475
503
|
: "该技能来源不允许启用或停用",
|
|
476
504
|
};
|
|
477
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* 来源可写但**不允许删除**时的拒绝结果(用户级 DSH 技能 / 导入技能)。
|
|
508
|
+
* 与「只读来源」区分开:这两种来源可以创建、可以停用,只是不提供删除 —— 提示要能说清
|
|
509
|
+
* 「你还能做什么」,否则用户会以为是权限坏了。
|
|
510
|
+
*/
|
|
511
|
+
function notDeletableError(definition) {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
code: "error.skill.notDeletable",
|
|
515
|
+
params: { root: definition && definition.key ? definition.key : "" },
|
|
516
|
+
error: "该来源的技能不能删除(技能只能停用):只在项目级来源(<项目>/.dsh/skills)提供删除",
|
|
517
|
+
};
|
|
518
|
+
}
|
|
478
519
|
function rootDefinition(root) {
|
|
479
520
|
if (root && typeof root === "object" && typeof root.key === "string")
|
|
480
521
|
return root;
|
|
@@ -486,7 +527,7 @@ function rootDefinition(root) {
|
|
|
486
527
|
function rootByKey(key) {
|
|
487
528
|
return userRoots().find((item) => item.key === key) || null;
|
|
488
529
|
}
|
|
489
|
-
/** 只允许用户 DSH 根,或由活动 Session 推导出的项目 DSH 根参与文件写入。 */
|
|
530
|
+
/** 只允许用户 DSH 根 / hub 根,或由活动 Session 推导出的项目 DSH 根参与文件写入。 */
|
|
490
531
|
function writableRootDefinition(root) {
|
|
491
532
|
const definition = rootDefinition(root);
|
|
492
533
|
if (!definition || definition.mutable !== true)
|
|
@@ -495,6 +536,11 @@ function writableRootDefinition(root) {
|
|
|
495
536
|
return resolve(definition.path) === resolve(dshRootPath())
|
|
496
537
|
? rootByKey("dsh")
|
|
497
538
|
: null;
|
|
539
|
+
// v0.4:hub 内技能目录同为用户级可写根(插件新建/导入的落点)。
|
|
540
|
+
if (definition.key === "hub")
|
|
541
|
+
return resolve(definition.path) === resolve(skillCreateRootPath())
|
|
542
|
+
? rootByKey("hub")
|
|
543
|
+
: null;
|
|
498
544
|
if (definition.scope !== "project" ||
|
|
499
545
|
definition.kind !== "project-dsh" ||
|
|
500
546
|
typeof definition.projectRoot !== "string" ||
|
|
@@ -1048,11 +1094,36 @@ function defaultManagerState() {
|
|
|
1048
1094
|
disabledSkills[root.key] = [];
|
|
1049
1095
|
enabledSkills[root.key] = [];
|
|
1050
1096
|
}
|
|
1051
|
-
return {
|
|
1097
|
+
return {
|
|
1098
|
+
version: 1,
|
|
1099
|
+
sources,
|
|
1100
|
+
disabledSkills,
|
|
1101
|
+
enabledSkills,
|
|
1102
|
+
customRoots: [],
|
|
1103
|
+
// 同名技能「首选来源」:技能名 → 来源 key。缺键 = 按来源 rank 取最高优先级者。
|
|
1104
|
+
preferredSkills: Object.create(null),
|
|
1105
|
+
// 用户从技能页「移除」的来源:插件**不再读取**这些目录(技能不出现在列表里,
|
|
1106
|
+
// 也不参与 provider 候选)。与「停用来源」不同——停用仍列出技能、只是不可调用。
|
|
1107
|
+
// 源文件一个字节都不动;清空这个数组即可恢复。
|
|
1108
|
+
removedSources: [],
|
|
1109
|
+
};
|
|
1052
1110
|
}
|
|
1053
1111
|
function validStateSkillName(name) {
|
|
1054
1112
|
return validDiscoveryName(name);
|
|
1055
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* 首选表的键是技能**声明名**(不是文件名)——声明名允许大小写与空格(此时技能本身带
|
|
1116
|
+
* name.invalid 诊断,但仍可被选择)。这里只做「能当 JSON 键、不像路径」的宽松校验,
|
|
1117
|
+
* 避免一个脏键把整份状态文件判为非法(那会让全部技能 fail-closed 停用)。
|
|
1118
|
+
*/
|
|
1119
|
+
function validPreferredSkillName(name) {
|
|
1120
|
+
return (typeof name === "string" &&
|
|
1121
|
+
name.length > 0 &&
|
|
1122
|
+
name.length <= MAX_ENTRY_NAME_LENGTH &&
|
|
1123
|
+
!name.includes("\0") &&
|
|
1124
|
+
!name.includes("/") &&
|
|
1125
|
+
!name.includes("\\"));
|
|
1126
|
+
}
|
|
1056
1127
|
/** 状态文件已存在但不可用时一律关闭外部来源,避免损坏配置重新暴露技能。 */
|
|
1057
1128
|
function failClosedManagerState() {
|
|
1058
1129
|
const state = defaultManagerState();
|
|
@@ -1090,6 +1161,22 @@ function validManagerStateDocument(value) {
|
|
|
1090
1161
|
// 自定义来源列表:可选字段;容器必须是数组(条目级合法性由 normalize 逐条裁剪)。
|
|
1091
1162
|
if (value.customRoots !== undefined && !Array.isArray(value.customRoots))
|
|
1092
1163
|
return false;
|
|
1164
|
+
// 已移除的来源:可选字段;数组里的每一项必须能当来源 key 用。
|
|
1165
|
+
if (value.removedSources !== undefined) {
|
|
1166
|
+
if (!Array.isArray(value.removedSources) ||
|
|
1167
|
+
value.removedSources.some((key) => typeof key !== "string" || key.length === 0 || key.length > 128))
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
// 同名首选表:可选字段;只校验容器与值类型,键值逐条在 normalize 里裁剪(见 validPreferredSkillName)。
|
|
1171
|
+
if (value.preferredSkills !== undefined) {
|
|
1172
|
+
if (!value.preferredSkills ||
|
|
1173
|
+
typeof value.preferredSkills !== "object" ||
|
|
1174
|
+
Array.isArray(value.preferredSkills))
|
|
1175
|
+
return false;
|
|
1176
|
+
for (const key of Object.values(value.preferredSkills))
|
|
1177
|
+
if (typeof key !== "string")
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1093
1180
|
for (const root of userRoots()) {
|
|
1094
1181
|
if (root.key === "dsh")
|
|
1095
1182
|
continue;
|
|
@@ -1153,8 +1240,50 @@ function normalizeManagerState(value) {
|
|
|
1153
1240
|
}
|
|
1154
1241
|
}
|
|
1155
1242
|
}
|
|
1243
|
+
// 同名首选表:键必须是技能声明名、值必须指向一个已知来源(用户级 / 自定义 / 项目级),
|
|
1244
|
+
// 否则丢弃该条(源目录被移除后残留的首选会自然失效)。
|
|
1245
|
+
const preferred = value.preferredSkills;
|
|
1246
|
+
if (preferred && typeof preferred === "object" && !Array.isArray(preferred)) {
|
|
1247
|
+
for (const [name, rootKey] of Object.entries(preferred)) {
|
|
1248
|
+
if (!validPreferredSkillName(name) || typeof rootKey !== "string")
|
|
1249
|
+
continue;
|
|
1250
|
+
if (!userRoots().some((root) => root.key === rootKey) &&
|
|
1251
|
+
!customKeys.has(rootKey) &&
|
|
1252
|
+
!PROJECT_ROOT_KEY_RE.test(rootKey))
|
|
1253
|
+
continue;
|
|
1254
|
+
normalized.preferredSkills[name] = rootKey;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
// 已移除的来源:只保留「确实是已知来源 key」的条目,其余丢弃(避免脏键让整份状态非法)。
|
|
1258
|
+
{
|
|
1259
|
+
const raw = Array.isArray(value.removedSources) ? value.removedSources : [];
|
|
1260
|
+
const known = new Set([
|
|
1261
|
+
...userRoots().map((root) => root.key),
|
|
1262
|
+
...customKeys,
|
|
1263
|
+
]);
|
|
1264
|
+
normalized.removedSources = [
|
|
1265
|
+
...new Set(raw.filter((key) => typeof key === "string" &&
|
|
1266
|
+
known.has(key) &&
|
|
1267
|
+
key !== "dsh" &&
|
|
1268
|
+
key !== "hub")),
|
|
1269
|
+
].sort();
|
|
1270
|
+
}
|
|
1156
1271
|
return normalized;
|
|
1157
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* 参与扫描的来源 = 全部来源 − 用户已移除的。
|
|
1275
|
+
*
|
|
1276
|
+
* 「移除来源」与「停用来源」是两件事:
|
|
1277
|
+
* - 停用(`sources[key] = false`):仍然读取并列出技能,只是不可调用;
|
|
1278
|
+
* - 移除(出现在 `removedSources`):**连目录都不读**,技能不出现在列表里,
|
|
1279
|
+
* 也不参与 provider 候选。源文件不动,清掉这个标记即恢复。
|
|
1280
|
+
*/
|
|
1281
|
+
export function activeUserRoots(stateValue) {
|
|
1282
|
+
const removed = new Set(stateValue && Array.isArray(stateValue.removedSources)
|
|
1283
|
+
? stateValue.removedSources
|
|
1284
|
+
: []);
|
|
1285
|
+
return userRoots().filter((root) => !removed.has(root.key));
|
|
1286
|
+
}
|
|
1158
1287
|
/** 裁剪自定义来源列表:非法条目丢弃,key/path 绑定校验,label 规整。 */
|
|
1159
1288
|
function normalizeCustomRoots(value) {
|
|
1160
1289
|
if (!Array.isArray(value))
|
|
@@ -1177,14 +1306,51 @@ function normalizeCustomRoots(value) {
|
|
|
1177
1306
|
}
|
|
1178
1307
|
return out;
|
|
1179
1308
|
}
|
|
1309
|
+
/**
|
|
1310
|
+
* 读状态文件。
|
|
1311
|
+
*
|
|
1312
|
+
* **校验的是「归一化之后」的文档,不是磁盘上的原始 JSON** —— 这一条是踩出来的:
|
|
1313
|
+
* 校验器要求每个 `userRoots()` 来源都在 `sources` 里有布尔值、在 `disabledSkills` 里有数组,
|
|
1314
|
+
* 而来源列表会随版本增加(v0.4 加了 `hub` 来源)。于是**版本升级本身**就会把一份完好的旧
|
|
1315
|
+
* 状态文件判成「非法」,紧接着 fail-closed:所有来源停用、写入锁定,Skills 页弹
|
|
1316
|
+
* 「状态文件不可读,已拒绝覆盖」。文件其实一个字节都没坏。
|
|
1317
|
+
*
|
|
1318
|
+
* 归一化会把缺失的键补成默认值(新来源默认启用),所以「我们后来加过键」与「文件真的坏了」
|
|
1319
|
+
* 由此区分开:前者修复后通过,后者(不是对象、version 不是 1、字段类型不对)仍然拒绝。
|
|
1320
|
+
*/
|
|
1180
1321
|
export async function readManagerState() {
|
|
1181
1322
|
try {
|
|
1182
1323
|
const raw = await fs.readFile(managerStatePath(), "utf8");
|
|
1183
1324
|
const parsed = JSON.parse(raw);
|
|
1184
|
-
|
|
1325
|
+
// 版本号在归一化时不会被保留(恒为 1),所以要在解析结果上直接判:
|
|
1326
|
+
// 缺失按 1 处理(更早的文档没有这个字段),存在但不是 1 才判非法。
|
|
1327
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1328
|
+
&& parsed.version !== undefined && parsed.version !== 1)
|
|
1329
|
+
throw codedError("invalid manager state schema", "error.state.invalid");
|
|
1330
|
+
// 结构守卫:字段**可以缺**(旧文档),但**一旦存在就必须形状正确**。
|
|
1331
|
+
// 少了这道守卫,归一化会把「类型写错」当成「键缺失」一起补默认值——于是 `sources: []`
|
|
1332
|
+
// 或 `disabledSkills: { dsh: "x" }` 这种真损坏反而被自愈放行,静默丢掉文件里的策略。
|
|
1333
|
+
// 契约:**缺键自愈(我们后来加过的东西),类型错必须报错。**
|
|
1334
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1335
|
+
for (const field of ["sources", "disabledSkills", "enabledSkills"]) {
|
|
1336
|
+
const value = parsed[field];
|
|
1337
|
+
if (value === undefined)
|
|
1338
|
+
continue;
|
|
1339
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1340
|
+
throw codedError("invalid manager state schema", "error.state.invalid");
|
|
1341
|
+
// 策略表的每个桶必须是数组(条目级合法性仍由 normalize 逐条裁剪)。
|
|
1342
|
+
if (field === "disabledSkills" || field === "enabledSkills") {
|
|
1343
|
+
for (const bucket of Object.values(value))
|
|
1344
|
+
if (!Array.isArray(bucket))
|
|
1345
|
+
throw codedError("invalid manager state schema", "error.state.invalid");
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const normalized = normalizeManagerState(parsed);
|
|
1350
|
+
if (!validManagerStateDocument(normalized))
|
|
1185
1351
|
throw codedError("invalid manager state schema", "error.state.invalid");
|
|
1186
1352
|
return {
|
|
1187
|
-
state:
|
|
1353
|
+
state: normalized,
|
|
1188
1354
|
warning: null,
|
|
1189
1355
|
writable: true,
|
|
1190
1356
|
};
|
|
@@ -1282,6 +1448,48 @@ export async function setSourceEnabled(rootOrKey, enabled, log) {
|
|
|
1282
1448
|
log(enabled ? "source-enable" : "source-disable", `${enabled ? "启用" : "停用"}来源 ${root.key}: ${root.path}`);
|
|
1283
1449
|
return { root: root.key, enabled: enabled === true };
|
|
1284
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* 移除 / 恢复一个来源(用户要求:「是不读取这个文件夹了,不是把文件夹删除」)。
|
|
1453
|
+
*
|
|
1454
|
+
* - `removed = true`:来源进入 `removedSources`,插件**不再读取该目录** —— 技能不出现在
|
|
1455
|
+
* 技能页,也不参与 provider 候选。**源文件与目录一个字节都不动。**
|
|
1456
|
+
* - `removed = false`:清除标记,下一步扫描即恢复。
|
|
1457
|
+
*
|
|
1458
|
+
* 与 `setSourceEnabled(false)` 的区别:停用仍然读目录、仍然列出技能(只是不可调用);
|
|
1459
|
+
* 移除是"当它不存在"。`dsh`(官方技能目录)与 `hub`(导入技能落点)不允许移除 ——
|
|
1460
|
+
* 它们是插件自身的读写根,移除会让创建/导入无处落脚。
|
|
1461
|
+
*/
|
|
1462
|
+
export async function setSourceRemoved(rootOrKey, removed, log) {
|
|
1463
|
+
const root = rootOrKey && typeof rootOrKey === "object" && typeof rootOrKey.key === "string"
|
|
1464
|
+
? rootOrKey
|
|
1465
|
+
: rootByKey(rootOrKey);
|
|
1466
|
+
if (!root)
|
|
1467
|
+
return readonlyError(removed === true ? "remove" : "restore");
|
|
1468
|
+
if (root.key === "dsh" || root.key === "hub")
|
|
1469
|
+
return {
|
|
1470
|
+
ok: false,
|
|
1471
|
+
code: "error.source.reserved",
|
|
1472
|
+
params: { root: root.key },
|
|
1473
|
+
error: root.key === "dsh"
|
|
1474
|
+
? "DSH 技能目录是官方来源,不能从管理器移除"
|
|
1475
|
+
: "导入技能目录是插件自身的读写落点,不能移除",
|
|
1476
|
+
};
|
|
1477
|
+
if (root.scope === "project")
|
|
1478
|
+
return readonlyError("remove");
|
|
1479
|
+
const current = await readManagerState();
|
|
1480
|
+
if (current.writable === false)
|
|
1481
|
+
return invalidManagerStateWrite();
|
|
1482
|
+
const set = new Set(Array.isArray(current.state.removedSources) ? current.state.removedSources : []);
|
|
1483
|
+
if (removed === true)
|
|
1484
|
+
set.add(root.key);
|
|
1485
|
+
else
|
|
1486
|
+
set.delete(root.key);
|
|
1487
|
+
current.state.removedSources = [...set].sort();
|
|
1488
|
+
await writeManagerState(current.state);
|
|
1489
|
+
if (log)
|
|
1490
|
+
log(removed === true ? "source-remove" : "source-restore", `${removed === true ? "移除(不再读取)" : "恢复读取"}来源 ${root.key}: ${root.path}`);
|
|
1491
|
+
return { root: root.key, removed: removed === true };
|
|
1492
|
+
}
|
|
1285
1493
|
async function checkedPolicyRootDefinition(root) {
|
|
1286
1494
|
const definition = rootDefinition(root);
|
|
1287
1495
|
if (!definition || !definition.toggleable)
|
|
@@ -1383,6 +1591,62 @@ async function setPolicySkillEnabled(root, name, enabled, log) {
|
|
|
1383
1591
|
export async function setSkillEnabled(root, name, enabled, log) {
|
|
1384
1592
|
return setPolicySkillEnabled(root, name, enabled, log);
|
|
1385
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* 同名技能「首选来源」:默认同名技能按来源 rank 取优先级最高者生效、其余显示为被覆盖;
|
|
1596
|
+
* 这里让用户显式指定哪个同名技能生效(preferred=false 取消,回到 rank 顺序)。
|
|
1597
|
+
* 只写本地策略,不改任何源文件。
|
|
1598
|
+
*/
|
|
1599
|
+
export async function setPreferredSkill(root, name, preferred, log) {
|
|
1600
|
+
const definition = await checkedPolicyRootDefinition(root);
|
|
1601
|
+
if (definition && definition.ok === false)
|
|
1602
|
+
return definition;
|
|
1603
|
+
if (!definition)
|
|
1604
|
+
return readonlyError("toggle");
|
|
1605
|
+
const resolved = await resolveEntry(definition, name);
|
|
1606
|
+
if (resolved === null)
|
|
1607
|
+
return {
|
|
1608
|
+
ok: false,
|
|
1609
|
+
error: `技能不存在: ${name}`,
|
|
1610
|
+
code: "error.skill.notFound",
|
|
1611
|
+
params: { name },
|
|
1612
|
+
};
|
|
1613
|
+
let summary;
|
|
1614
|
+
try {
|
|
1615
|
+
summary = entryOf(name, resolved.kind, resolved.docPath, parseSkillDoc(await fs.readFile(resolved.realDocPath || resolved.docPath, "utf8")));
|
|
1616
|
+
}
|
|
1617
|
+
catch {
|
|
1618
|
+
return {
|
|
1619
|
+
ok: false,
|
|
1620
|
+
error: `技能不存在: ${name}`,
|
|
1621
|
+
code: "error.skill.notFound",
|
|
1622
|
+
params: { name },
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
if (!summary.loadable)
|
|
1626
|
+
return {
|
|
1627
|
+
ok: false,
|
|
1628
|
+
error: `技能结构不完整,无法设为同名首选: ${name}`,
|
|
1629
|
+
code: "error.skill.notLoadable",
|
|
1630
|
+
params: { name, action: "prefer" },
|
|
1631
|
+
};
|
|
1632
|
+
// 首选表按**声明名**索引(与 groupLoadableSkillsByName 的分组键同源),不是文件名。
|
|
1633
|
+
const canonicalName = summary.declaredName || name;
|
|
1634
|
+
const current = await readManagerState();
|
|
1635
|
+
if (current.writable === false)
|
|
1636
|
+
return invalidManagerStateWrite();
|
|
1637
|
+
const map = { ...(current.state.preferredSkills || {}) };
|
|
1638
|
+
if (preferred === false)
|
|
1639
|
+
delete map[canonicalName];
|
|
1640
|
+
else
|
|
1641
|
+
map[canonicalName] = definition.key;
|
|
1642
|
+
current.state.preferredSkills = map;
|
|
1643
|
+
await writeManagerState(current.state);
|
|
1644
|
+
if (log)
|
|
1645
|
+
log(preferred === false ? "skill-unprefer" : "skill-prefer", preferred === false
|
|
1646
|
+
? `取消同名首选 ${canonicalName}`
|
|
1647
|
+
: `同名首选 ${canonicalName} → ${definition.key}`);
|
|
1648
|
+
return { name: canonicalName, root: preferred === false ? null : definition.key };
|
|
1649
|
+
}
|
|
1386
1650
|
async function safeExistingEntryPaths(root, name) {
|
|
1387
1651
|
const paths = [];
|
|
1388
1652
|
const bundle = entryPath(root, name);
|
|
@@ -1498,13 +1762,17 @@ async function restoreRootDefinition(metadata, options = {}) {
|
|
|
1498
1762
|
}
|
|
1499
1763
|
return checkedWritableRootDefinition(root);
|
|
1500
1764
|
}
|
|
1501
|
-
/**
|
|
1765
|
+
/** 把项目级 DSH 根中的单个技能移入 manager-owned 回收站。 */
|
|
1502
1766
|
export async function deleteSkill(root, name, log, options = {}) {
|
|
1503
1767
|
const definition = await checkedWritableRootDefinition(root);
|
|
1504
1768
|
if (definition && definition.ok === false)
|
|
1505
1769
|
return definition;
|
|
1506
1770
|
if (!definition)
|
|
1507
1771
|
return readonlyError("delete");
|
|
1772
|
+
// 用户级来源(dsh / hub)**不可删**:技能只能停用。删除会把用户自己放进去的技能
|
|
1773
|
+
// 从磁盘上搬走,代价远大于收益;项目级来源(本仓库自己的 .dsh/skills)才允许删。
|
|
1774
|
+
if (definition.deletable !== true)
|
|
1775
|
+
return notDeletableError(definition);
|
|
1508
1776
|
const resolved = await resolveEntry(definition, name);
|
|
1509
1777
|
if (resolved === null)
|
|
1510
1778
|
return {
|
|
@@ -1866,7 +2134,7 @@ async function replaceWithCopy(source, dest, isDir, existing = []) {
|
|
|
1866
2134
|
* 成功返回 { kind, imported, skipped, failed };失败返回 { ok:false, error }。
|
|
1867
2135
|
*/
|
|
1868
2136
|
export async function importSkill(source, log, options = {}) {
|
|
1869
|
-
const targetRoot =
|
|
2137
|
+
const targetRoot = skillCreateRootPath();
|
|
1870
2138
|
const conflict = options.conflict === "overwrite" ? "overwrite" : "skip";
|
|
1871
2139
|
const dryRun = options.dryRun === true;
|
|
1872
2140
|
const analysis = await analyzeSource(source);
|
|
@@ -2210,9 +2478,11 @@ function yamlString(value) {
|
|
|
2210
2478
|
return JSON.stringify(String(value));
|
|
2211
2479
|
}
|
|
2212
2480
|
export async function createSkill(input, log, options = {}) {
|
|
2481
|
+
// v0.4:默认落点由「DSH 技能目录」改为 hub 内的 `tool-management/skills/`;
|
|
2482
|
+
// 调用方显式传 options.root(如项目根)时仍以调用方为准。
|
|
2213
2483
|
const requestedRoot = Object.prototype.hasOwnProperty.call(options, "root")
|
|
2214
2484
|
? options.root
|
|
2215
|
-
: rootByKey("dsh");
|
|
2485
|
+
: rootByKey("hub") || rootByKey("dsh");
|
|
2216
2486
|
const definition = await checkedWritableRootDefinition(requestedRoot);
|
|
2217
2487
|
if (definition && definition.ok === false)
|
|
2218
2488
|
return definition;
|
|
@@ -2283,13 +2553,15 @@ export async function skillDetail(keyOrRoot, name, options = {}) {
|
|
|
2283
2553
|
const root = (keyOrRoot && typeof keyOrRoot === "object" && typeof keyOrRoot.key === "string" ? keyOrRoot : null) ||
|
|
2284
2554
|
rootByKey(keyOrRoot) ||
|
|
2285
2555
|
scopedRoots.find((item) => item.key === keyOrRoot);
|
|
2286
|
-
if (!root)
|
|
2556
|
+
if (!root) {
|
|
2557
|
+
const unknownRoot = (keyOrRoot && typeof keyOrRoot === "object" && keyOrRoot.key) || keyOrRoot;
|
|
2287
2558
|
return {
|
|
2288
2559
|
ok: false,
|
|
2289
|
-
error: `未知技能来源: ${
|
|
2560
|
+
error: `未知技能来源: ${unknownRoot}`,
|
|
2290
2561
|
code: "error.root.unknown",
|
|
2291
|
-
params: { root:
|
|
2562
|
+
params: { root: unknownRoot },
|
|
2292
2563
|
};
|
|
2564
|
+
}
|
|
2293
2565
|
const entry = await visibleEntryForRoot(root, name);
|
|
2294
2566
|
if (!entry)
|
|
2295
2567
|
return {
|
|
@@ -2332,7 +2604,7 @@ export async function skillDetail(keyOrRoot, name, options = {}) {
|
|
|
2332
2604
|
export async function listProviderCandidates(options = {}) {
|
|
2333
2605
|
const policyResult = await readManagerState();
|
|
2334
2606
|
const candidates = [];
|
|
2335
|
-
const user =
|
|
2607
|
+
const user = activeUserRoots(policyResult.state).concat(customRootsFromState(policyResult.state).filter((root) => !policyResult.state.removedSources.includes(root.key)));
|
|
2336
2608
|
const userScans = await scanDeduplicatedRoots(user);
|
|
2337
2609
|
const items = user.flatMap((root) => userScans.get(root.key).entries.map((entry) => ({ root, entry })));
|
|
2338
2610
|
const cwd = options && typeof options.cwd === "string" ? options.cwd : undefined;
|
|
@@ -2450,10 +2722,28 @@ export async function getProviderSkill(candidate, options = {}) {
|
|
|
2450
2722
|
function canonicalSkillName(item) {
|
|
2451
2723
|
return item.entry.declaredName || item.entry.name;
|
|
2452
2724
|
}
|
|
2453
|
-
/**
|
|
2454
|
-
|
|
2725
|
+
/**
|
|
2726
|
+
* 真实路径去重后按声明名分组;管理页与当前工作区 provider 共用来源优先级。
|
|
2727
|
+
*
|
|
2728
|
+
* `preferred`(技能名 → 来源 key)是用户对同名技能的显式选择:命中的来源排到同名前,
|
|
2729
|
+
* 其余仍按来源 rank 排序;未命中(或键已失效)时退化为纯 rank 顺序。
|
|
2730
|
+
*/
|
|
2731
|
+
export function groupLoadableSkillsByName(items, preferred) {
|
|
2732
|
+
const preferredFor = (item) => {
|
|
2733
|
+
if (!preferred)
|
|
2734
|
+
return null;
|
|
2735
|
+
const key = preferred[canonicalSkillName(item)];
|
|
2736
|
+
return typeof key === "string" ? key : null;
|
|
2737
|
+
};
|
|
2738
|
+
const ordered = [...items].sort((a, b) => {
|
|
2739
|
+
const rankA = preferredFor(a) === a.root.key ? 0 : 1;
|
|
2740
|
+
const rankB = preferredFor(b) === b.root.key ? 0 : 1;
|
|
2741
|
+
if (rankA !== rankB)
|
|
2742
|
+
return rankA - rankB;
|
|
2743
|
+
return a.root.rank - b.root.rank;
|
|
2744
|
+
});
|
|
2455
2745
|
const groups = new Map();
|
|
2456
|
-
for (const item of
|
|
2746
|
+
for (const item of ordered) {
|
|
2457
2747
|
if (!item.entry.loadable)
|
|
2458
2748
|
continue;
|
|
2459
2749
|
const name = canonicalSkillName(item);
|
|
@@ -2465,8 +2755,12 @@ function groupLoadableSkillsByName(items) {
|
|
|
2465
2755
|
}
|
|
2466
2756
|
function markWinners(items, options = {}) {
|
|
2467
2757
|
const winners = new Map();
|
|
2468
|
-
|
|
2758
|
+
const preferred = options.preferred;
|
|
2759
|
+
for (const [canonicalName, [winner, ...shadowed],] of groupLoadableSkillsByName(items, preferred)) {
|
|
2469
2760
|
winners.set(canonicalName, winner);
|
|
2761
|
+
// 只有「首选命中且确实赢了」才算首选;来源被停用/移除时不谎报。
|
|
2762
|
+
if (preferred && preferred[canonicalName] === winner.root.key)
|
|
2763
|
+
winner.view.preferred = true;
|
|
2470
2764
|
if (options.markShadowed !== false) {
|
|
2471
2765
|
for (const item of shadowed)
|
|
2472
2766
|
item.view.shadowedBy = {
|
|
@@ -2484,8 +2778,16 @@ function markWinners(items, options = {}) {
|
|
|
2484
2778
|
/** DSH、常见 Agent、自定义目录与活动 Session 项目根的技能快照。 */
|
|
2485
2779
|
export async function state(options = {}) {
|
|
2486
2780
|
const policyResult = await readManagerState();
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2781
|
+
const removedKeys = new Set(Array.isArray(policyResult.state.removedSources)
|
|
2782
|
+
? policyResult.state.removedSources
|
|
2783
|
+
: []);
|
|
2784
|
+
// 已移除的来源仍然出现在 roots 里(界面要能显示「已移除」并允许恢复),但**不扫描**:
|
|
2785
|
+
// 技能不入列表、不参与重名决胜 —— 这正是用户要的「不读取这个文件夹」。
|
|
2786
|
+
// 注意这里必须用**全部**来源 + 自定义来源,不能只用 activeUserRoots(),
|
|
2787
|
+
// 否则被移除的来源会连行都不剩,界面就无从提供「恢复读取」。
|
|
2788
|
+
const user = [...userRoots(), ...customRootsFromState(policyResult.state)];
|
|
2789
|
+
const scannable = user.filter((root) => !removedKeys.has(root.key));
|
|
2790
|
+
const userScans = await scanDeduplicatedRoots(scannable);
|
|
2489
2791
|
const projectWarnings = [];
|
|
2490
2792
|
const scoped = await projectRoots(options.projectCwds, projectWarnings);
|
|
2491
2793
|
const trash = await listTrash();
|
|
@@ -2513,9 +2815,35 @@ export async function state(options = {}) {
|
|
|
2513
2815
|
});
|
|
2514
2816
|
const all = [];
|
|
2515
2817
|
for (const root of [...scoped, ...user]) {
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2518
|
-
:
|
|
2818
|
+
const isRemoved = removedKeys.has(root.key);
|
|
2819
|
+
const { exists, entries, truncated } = isRemoved
|
|
2820
|
+
? { exists: false, entries: [], truncated: false }
|
|
2821
|
+
: root.scope === "project"
|
|
2822
|
+
? (await scanDeduplicatedRoots([root])).get(root.key)
|
|
2823
|
+
: userScans.get(root.key);
|
|
2824
|
+
if (isRemoved) {
|
|
2825
|
+
// 已移除:登记一行(供界面显示与恢复),但没有任何技能。
|
|
2826
|
+
result.roots.push({
|
|
2827
|
+
key: root.key,
|
|
2828
|
+
...(root.kind ? { kind: root.kind } : {}),
|
|
2829
|
+
...(root.localeKey ? { localeKey: root.localeKey } : {}),
|
|
2830
|
+
path: root.path,
|
|
2831
|
+
label: root.label,
|
|
2832
|
+
mutable: root.mutable,
|
|
2833
|
+
deletable: root.deletable === true,
|
|
2834
|
+
removable: root.key !== "dsh" && root.key !== "hub" && root.scope !== "project",
|
|
2835
|
+
toggleable: root.toggleable,
|
|
2836
|
+
native: root.native,
|
|
2837
|
+
rank: root.rank,
|
|
2838
|
+
scope: root.scope || "user",
|
|
2839
|
+
exists: false,
|
|
2840
|
+
truncated: false,
|
|
2841
|
+
removed: true,
|
|
2842
|
+
enabled: false,
|
|
2843
|
+
skills: [],
|
|
2844
|
+
});
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2519
2847
|
// 即使项目 .dsh/skills 尚不存在,也要把可写根返回给创建表单;只读项目根仍按实际存在性展示。
|
|
2520
2848
|
if (root.scope === "project" && !exists && root.kind !== "project-dsh")
|
|
2521
2849
|
continue;
|
|
@@ -2563,6 +2891,8 @@ export async function state(options = {}) {
|
|
|
2563
2891
|
path: root.path,
|
|
2564
2892
|
label: root.label,
|
|
2565
2893
|
mutable: root.mutable,
|
|
2894
|
+
// 是否提供「删除」:只有项目级 DSH 根为 true。用户级 dsh / hub 可写但不可删。
|
|
2895
|
+
deletable: root.deletable === true,
|
|
2566
2896
|
toggleable: root.toggleable,
|
|
2567
2897
|
native: root.native,
|
|
2568
2898
|
rank: root.rank,
|
|
@@ -2576,6 +2906,9 @@ export async function state(options = {}) {
|
|
|
2576
2906
|
: {}),
|
|
2577
2907
|
exists,
|
|
2578
2908
|
truncated: truncated === true,
|
|
2909
|
+
// 界面用:能否从管理器「移除」(不再读取)。dsh / hub / 项目级不可移除。
|
|
2910
|
+
removable: root.key !== "dsh" && root.key !== "hub" && root.scope !== "project",
|
|
2911
|
+
removed: false,
|
|
2579
2912
|
enabled: policyResult.writable !== false &&
|
|
2580
2913
|
(root.scope === "project" ||
|
|
2581
2914
|
root.key === "dsh" ||
|
|
@@ -2584,7 +2917,8 @@ export async function state(options = {}) {
|
|
|
2584
2917
|
});
|
|
2585
2918
|
}
|
|
2586
2919
|
const userItems = all.filter((item) => item.root.scope !== "project");
|
|
2587
|
-
|
|
2920
|
+
const preferredSkills = policyResult.state.preferredSkills || null;
|
|
2921
|
+
markWinners(userItems, { preferred: preferredSkills });
|
|
2588
2922
|
const projectGroups = new Map();
|
|
2589
2923
|
for (const item of all.filter((candidate) => candidate.root.scope === "project")) {
|
|
2590
2924
|
const group = projectGroups.get(item.root.projectRoot) || [];
|
|
@@ -2597,7 +2931,7 @@ export async function state(options = {}) {
|
|
|
2597
2931
|
...item,
|
|
2598
2932
|
view: { ...item.view },
|
|
2599
2933
|
}));
|
|
2600
|
-
markWinners([...projectItems, ...userCopies]);
|
|
2934
|
+
markWinners([...projectItems, ...userCopies], { preferred: preferredSkills });
|
|
2601
2935
|
}
|
|
2602
2936
|
const seenProjects = new Set();
|
|
2603
2937
|
for (const root of result.roots.filter((item) => item.scope === "project")) {
|
package/lib/skills/service.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { basename, join } from 'node:path';
|
|
12
|
-
import { state, setSkillEnabled, setSourceEnabled, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
|
|
12
|
+
import { state, setSkillEnabled, setPreferredSkill, setSourceEnabled, setSourceRemoved, deleteSkill, restoreTrash, permanentlyDeleteTrash, importSkill, importUploadedSkill, browseDirectories, createSkill, skillDetail, listProviderCandidates, getProviderSkill, userRoots, projectRoots, readManagerState, customRootsFromState, addCustomRoot, removeCustomRoot, logPath, trashRootPath, } from './core.js';
|
|
13
13
|
const MAX_LOG_BYTES = 1 << 20;
|
|
14
14
|
const PROVIDER_NAME = 'dsh-plugin-tool-management-external';
|
|
15
15
|
const message = (e) => String((e && e.message) || e);
|
|
@@ -282,8 +282,15 @@ function notifyChatCatalog(ctx, invalidateSkills) {
|
|
|
282
282
|
}
|
|
283
283
|
catch { /* 斜杠菜单刷新失败不影响已完成的操作 */ }
|
|
284
284
|
}
|
|
285
|
+
/** 全插件共用的审计日志:单一串行写队列,避免多处写同一文件交叉。 */
|
|
286
|
+
let sharedLog = null;
|
|
287
|
+
export function pluginLog() {
|
|
288
|
+
if (!sharedLog)
|
|
289
|
+
sharedLog = makeLog();
|
|
290
|
+
return sharedLog;
|
|
291
|
+
}
|
|
285
292
|
export function createSkillsService(ctx) {
|
|
286
|
-
const log =
|
|
293
|
+
const log = pluginLog();
|
|
287
294
|
const roots = userRoots();
|
|
288
295
|
const rootByKey = Object.fromEntries(roots.map((root) => [root.key, root]));
|
|
289
296
|
const projectOptions = () => ({ projectCwds: activeSessionCwds(ctx) });
|
|
@@ -387,18 +394,37 @@ export function createSkillsService(ctx) {
|
|
|
387
394
|
return { ok: true, data: result };
|
|
388
395
|
};
|
|
389
396
|
const write = (task) => enqueueMutation(task);
|
|
397
|
+
// 写操作清单:与下方 ops 表同文件同源维护;HTTP 端门禁由 index.ts 从本集合派生,勿在宿主端另抄一份。
|
|
398
|
+
const writeOps = new Set([
|
|
399
|
+
'skill-enable', 'skill-disable', 'skill-source-enable', 'skill-source-disable',
|
|
400
|
+
'skill-prefer', 'skill-unprefer',
|
|
401
|
+
'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
|
|
402
|
+
'skill-trash-restore', 'skill-trash-delete', 'skill-custom-add', 'skill-custom-remove',
|
|
403
|
+
]);
|
|
390
404
|
const ops = {
|
|
391
405
|
// 读操作(state 走短 TTL 缓存,见上方 readState)
|
|
392
406
|
'skill-state': wrap(() => readState()),
|
|
393
|
-
'skill-detail': wrap(async (args) =>
|
|
407
|
+
'skill-detail': wrap(async (args) => {
|
|
408
|
+
const key = String(args.root || 'dsh');
|
|
409
|
+
// requestRoot 未命中时回传原始 key,让 core 的 error.root.unknown 带上来源名(而非 undefined)。
|
|
410
|
+
return skillDetail((await requestRoot(key)) || key, String(args.name || ''), projectOptions());
|
|
411
|
+
}),
|
|
394
412
|
'skill-browse': wrap((args) => browseDirectories(args && args.path)),
|
|
395
413
|
// 启停(写 manager 状态,不改源文件)
|
|
396
414
|
'skill-enable': wrap((args) => write(async () => setSkillEnabled(await requestRoot(String(args.root || 'dsh')), String(args.name || ''), true, log)), afterWrite),
|
|
397
415
|
'skill-disable': wrap((args) => write(async () => setSkillEnabled(await requestRoot(String(args.root || 'dsh')), String(args.name || ''), false, log)), afterWrite),
|
|
398
416
|
'skill-source-enable': wrap((args) => write(async () => setSourceEnabled(await requestRoot(String(args.root || '')), true, log)), afterWrite),
|
|
399
417
|
'skill-source-disable': wrap((args) => write(async () => setSourceEnabled(await requestRoot(String(args.root || '')), false, log)), afterWrite),
|
|
418
|
+
// 移除 / 恢复来源(「不再读取这个文件夹」):只改本地状态,源目录一个字节都不动。
|
|
419
|
+
'skill-source-remove': wrap((args) => write(async () => setSourceRemoved(await requestRoot(String(args.root || '')), true, log)), afterWrite),
|
|
420
|
+
'skill-source-restore': wrap((args) => write(async () => setSourceRemoved(await requestRoot(String(args.root || '')), false, log)), afterWrite),
|
|
421
|
+
// 同名技能首选来源(手工指定哪个同名版本生效;只写本地策略)。
|
|
422
|
+
'skill-prefer': wrap((args) => write(async () => setPreferredSkill(await requestRoot(String(args.root || 'dsh')), String(args.name || ''), true, log)), afterWrite),
|
|
423
|
+
'skill-unprefer': wrap((args) => write(async () => setPreferredSkill(await requestRoot(String(args.root || 'dsh')), String(args.name || ''), false, log)), afterWrite),
|
|
400
424
|
// 创建 / 导入 / 删除 / 回收站
|
|
401
|
-
|
|
425
|
+
// 创建默认落 hub(`tool-management/skills/`):UI 不传 root 时走这里;
|
|
426
|
+
// core.js 的 createSkill 默认值是第二道保险(同口径)。官方 `~/.dsh/skills/` 仍作为来源列出。
|
|
427
|
+
'skill-create': wrap((args) => write(async () => createSkill({ name: args.name, description: args.description, body: args.body }, log, { root: await requestRoot(String(args.root || 'hub')) })), afterWrite),
|
|
402
428
|
'skill-import': wrap((args) => write(() => importSkill(String(args.source || ''), log, {
|
|
403
429
|
conflict: args.conflict === 'overwrite' ? 'overwrite' : 'skip',
|
|
404
430
|
dryRun: args.dryRun === true,
|
|
@@ -413,5 +439,5 @@ export function createSkillsService(ctx) {
|
|
|
413
439
|
'skill-custom-add': wrap((args) => write(() => addCustomRoot(args && args.path, args && args.label, log)), afterWrite),
|
|
414
440
|
'skill-custom-remove': wrap((args) => write(() => removeCustomRoot(args && args.key, log)), afterWrite),
|
|
415
441
|
};
|
|
416
|
-
return { ops, registerProviders };
|
|
442
|
+
return { ops, writeOps, registerProviders };
|
|
417
443
|
}
|