@max-null/dsh-skill-mcp-center 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/client.js +11 -8
- package/dist/rpc.js +7 -0
- package/dist/service.d.ts +26 -0
- package/dist/service.js +94 -1
- package/docs/shots/skill-hotplug-toggle.png +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,12 @@ DSH 的 `Loader.write()` 是空实现,面板上的启停只改内存树——
|
|
|
29
29
|
|
|
30
30
|

|
|
31
31
|
|
|
32
|
+
### 设置页:插件技能的启停
|
|
33
|
+
|
|
34
|
+
插件包内自带的技能没有可写的 `SKILL.md`——改名、移出目录、断链对安装内容都不生效(社区同类项目因此都只能显示「内置只读」)。这里改用 registry 的 rank 遮蔽:停用时注册一个同名占位技能(runtime rank 250 赢过包内 provider ≈550、bundled 600),两个 invocation 都设为 false,于是模型与人都调不到它,但它仍在 catalog 里、仍可被面板列出与管理;启用时调 disposer。停用清单落在 `$DSH_HOME/skill-mcp-center/disabled-skills.json`,重启后自动重放。
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
32
38
|
### 侧栏:Skill 一览
|
|
33
39
|
|
|
34
40
|
搜索 + 命名空间筛选(全部 / 全局 / 工作区),每条带开关与 `MD`(查看 SKILL.md)。**插件包内自带的 skill 同样列出**,并标出来源插件——下图里 8 个 `ssid-*` 来自 `dsh-skills`,而用户级的 `ssid-release` 没有来源标签,这正是两者的区别:
|
package/client.js
CHANGED
|
@@ -217,6 +217,7 @@ var zhDict = {
|
|
|
217
217
|
sectionSub: "\u7BA1\u7406\uFF08\u9759\u6001\u914D\u7F6E\uFF09\xB7 \u8FD0\u884C\u65F6\u72B6\u6001\u5728\u53F3\u4FA7\u8FB9\u680F",
|
|
218
218
|
modelDisabled: "\u6A21\u578B\u5DF2\u505C\u7528",
|
|
219
219
|
toggleModelVisible: "\u5207\u6362\u6A21\u578B\u53EF\u89C1",
|
|
220
|
+
toggleSkillEnabled: "\u542F\u7528 / \u505C\u7528\uFF08\u8FD0\u884C\u65F6\u906E\u853D\uFF0C\u5373\u65F6\u751F\u6548\uFF09",
|
|
220
221
|
skillReadonly: "system/runtime \u6280\u80FD\u53EA\u8BFB",
|
|
221
222
|
enable: "\u542F\u7528",
|
|
222
223
|
disable: "\u505C\u7528",
|
|
@@ -284,6 +285,7 @@ var enDict = {
|
|
|
284
285
|
sectionSub: "Manage (static config) \xB7 runtime status lives in the sidebar",
|
|
285
286
|
modelDisabled: "Model disabled",
|
|
286
287
|
toggleModelVisible: "Toggle model visibility",
|
|
288
|
+
toggleSkillEnabled: "Enable / disable (runtime shadow, applies immediately)",
|
|
287
289
|
skillReadonly: "system/runtime skills are read-only",
|
|
288
290
|
enable: "Enable",
|
|
289
291
|
disable: "Disable",
|
|
@@ -458,8 +460,9 @@ function SkillView() {
|
|
|
458
460
|
if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loadFailed", { e: error }) });
|
|
459
461
|
if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "smc-sub", children: t("loading") });
|
|
460
462
|
const toggle = (s) => {
|
|
461
|
-
setBusy(s
|
|
462
|
-
|
|
463
|
+
setBusy(skillKeyOf(s));
|
|
464
|
+
const call = s.writable ? rpc("toggleSkill", { path: s.path }) : rpc("setSkillEnabled", { name: s.name, enabled: !s.enabled });
|
|
465
|
+
void call.then(
|
|
463
466
|
() => {
|
|
464
467
|
setBusy(null);
|
|
465
468
|
load();
|
|
@@ -535,19 +538,19 @@ function SkillView() {
|
|
|
535
538
|
]
|
|
536
539
|
}
|
|
537
540
|
),
|
|
538
|
-
s.writable
|
|
541
|
+
(s.writable || hasSkillFile(s)) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
539
542
|
"button",
|
|
540
543
|
{
|
|
541
544
|
type: "button",
|
|
542
|
-
className: `smc-toggle${s.modelInvocable ? " on" : ""}`,
|
|
543
|
-
disabled: busy === s
|
|
544
|
-
title: t("toggleModelVisible"),
|
|
545
|
+
className: `smc-toggle${(s.writable ? s.modelInvocable : s.enabled) ? " on" : ""}`,
|
|
546
|
+
disabled: busy === skillKeyOf(s),
|
|
547
|
+
title: s.writable ? t("toggleModelVisible") : t("toggleSkillEnabled"),
|
|
545
548
|
onClick: () => {
|
|
546
549
|
toggle(s);
|
|
547
550
|
},
|
|
548
|
-
"aria-label": s.modelInvocable ? t("disable") : t("enable")
|
|
551
|
+
"aria-label": (s.writable ? s.modelInvocable : s.enabled) ? t("disable") : t("enable")
|
|
549
552
|
}
|
|
550
|
-
)
|
|
553
|
+
),
|
|
551
554
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `smc-chevron${open ? " open" : ""}` })
|
|
552
555
|
] }),
|
|
553
556
|
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "smc-detail", children: [
|
package/dist/rpc.js
CHANGED
|
@@ -93,6 +93,13 @@ async function dispatch(ctx, body) {
|
|
|
93
93
|
return internal('toggleSkill: path is required');
|
|
94
94
|
return { ok: true, value: await ctx.skillMcp.toggleSkill(path) };
|
|
95
95
|
}
|
|
96
|
+
case 'setSkillEnabled': {
|
|
97
|
+
const name = p.name;
|
|
98
|
+
if (typeof name !== 'string' || name === '')
|
|
99
|
+
return internal('setSkillEnabled: name is required');
|
|
100
|
+
await ctx.skillMcp.setSkillEnabled(name, p.enabled === true);
|
|
101
|
+
return { ok: true, value: null };
|
|
102
|
+
}
|
|
96
103
|
case 'readSkill': {
|
|
97
104
|
const path = p.path;
|
|
98
105
|
if (typeof path !== 'string' || path === '')
|
package/dist/service.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface SkillView {
|
|
|
25
25
|
writable: boolean;
|
|
26
26
|
/** Absolute SKILL.md path (the toggle target; opaque to the client display). */
|
|
27
27
|
path: string;
|
|
28
|
+
/** False when the panel disabled this skill; a runtime placeholder shadows the real definition. */
|
|
29
|
+
enabled: boolean;
|
|
28
30
|
}
|
|
29
31
|
/** Plugin configuration for the skill/MCP engine. */
|
|
30
32
|
export interface SkillConfig {
|
|
@@ -155,6 +157,12 @@ export declare class SkillMcpService extends Service {
|
|
|
155
157
|
private readonly officialSkillDirs;
|
|
156
158
|
/** Ids toggled by the panel during this run (in-memory only, by design: the host does not persist it). */
|
|
157
159
|
private readonly sessionToggled;
|
|
160
|
+
/** Skill names the panel disabled; runtime placeholders shadow the real definitions. */
|
|
161
|
+
private readonly skillsDisabled;
|
|
162
|
+
/** Live disposers for those shadow registrations, keyed by skill name. */
|
|
163
|
+
private readonly shadowDisposers;
|
|
164
|
+
/** Set once the disabled list has been read from disk and replayed. */
|
|
165
|
+
private shadowsLoaded;
|
|
158
166
|
constructor(ctx: Context, config?: SkillConfig);
|
|
159
167
|
/**
|
|
160
168
|
* Skill roots that live **inside loaded plugin packages**.
|
|
@@ -218,6 +226,24 @@ export declare class SkillMcpService extends Service {
|
|
|
218
226
|
removeMcpServer(id: string): Promise<void>;
|
|
219
227
|
/** Enable/disable one server without deleting its config. The host does not persist this. */
|
|
220
228
|
setMcpServerEnabled(id: string, enabled: boolean): Promise<void>;
|
|
229
|
+
/** 技能禁用清单的落盘位置:跟在 DSH_HOME 下,隔离实例各自一份。 */
|
|
230
|
+
private disabledFile;
|
|
231
|
+
/**
|
|
232
|
+
* 读回落盘的禁用清单并重放遮蔽。为什么要自己存:registry 不提供「查看被遮蔽定义」的
|
|
233
|
+
* API(`The registry still exposes no API to inspect shadowed definitions`),
|
|
234
|
+
* 不落盘的话重启即失忆,面板开关就成了一次性的。
|
|
235
|
+
*/
|
|
236
|
+
private loadDisabledSkills;
|
|
237
|
+
private persistDisabledSkills;
|
|
238
|
+
/**
|
|
239
|
+
* 停用一条技能:注册同名占位——runtime rank(250)赢过包内 provider(≈550)与 bundled(600),
|
|
240
|
+
* 两个 invocation 都为 false,于是模型和人都调不到它,但它仍在 catalog 里、可被面板列出。
|
|
241
|
+
* 这是包内技能**唯一**可逆的停用方式:改名 / 移目录 / 断链对安装内容都不生效。
|
|
242
|
+
*/
|
|
243
|
+
private applyShadow;
|
|
244
|
+
private removeShadow;
|
|
245
|
+
/** 启用/停用一条技能。停用走占位遮蔽、启用调 disposer——不写 SKILL.md、不重启。 */
|
|
246
|
+
setSkillEnabled(name: string, enabled: boolean): Promise<void>;
|
|
221
247
|
/** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
|
|
222
248
|
mcpStatus(): Promise<McpServerStatus[]>;
|
|
223
249
|
}
|
package/dist/service.js
CHANGED
|
@@ -135,6 +135,7 @@ export async function scanSkillRoot(root, source, writable = true, provider = 'f
|
|
|
135
135
|
userInvocable: true,
|
|
136
136
|
writable,
|
|
137
137
|
path: skillPath,
|
|
138
|
+
enabled: true,
|
|
138
139
|
});
|
|
139
140
|
}
|
|
140
141
|
return skills;
|
|
@@ -234,6 +235,12 @@ export class SkillMcpService extends Service {
|
|
|
234
235
|
officialSkillDirs;
|
|
235
236
|
/** Ids toggled by the panel during this run (in-memory only, by design: the host does not persist it). */
|
|
236
237
|
sessionToggled = new Set();
|
|
238
|
+
/** Skill names the panel disabled; runtime placeholders shadow the real definitions. */
|
|
239
|
+
skillsDisabled = new Set();
|
|
240
|
+
/** Live disposers for those shadow registrations, keyed by skill name. */
|
|
241
|
+
shadowDisposers = new Map();
|
|
242
|
+
/** Set once the disabled list has been read from disk and replayed. */
|
|
243
|
+
shadowsLoaded = false;
|
|
237
244
|
constructor(ctx, config = {}) {
|
|
238
245
|
super(ctx, 'skillMcp');
|
|
239
246
|
this.officialSkillDirs = config.officialSkillDirs ?? [];
|
|
@@ -256,6 +263,7 @@ export class SkillMcpService extends Service {
|
|
|
256
263
|
* bundled inside loaded plugin packages, and any configured official roots.
|
|
257
264
|
*/
|
|
258
265
|
async listSkills(cwd) {
|
|
266
|
+
await this.loadDisabledSkills();
|
|
259
267
|
const skills = [];
|
|
260
268
|
for (const root of SKILL_ROOTS) {
|
|
261
269
|
skills.push(...await scanSkillRoot(root.path, root.source));
|
|
@@ -271,7 +279,11 @@ export class SkillMcpService extends Service {
|
|
|
271
279
|
skills.push(...await scanSkillRoot(dir, 'bundled', false, 'dsh-official'));
|
|
272
280
|
}
|
|
273
281
|
skills.push(...await this.runtimeOnlySkills(skills));
|
|
274
|
-
return skills
|
|
282
|
+
return skills.map(skill => ({
|
|
283
|
+
...skill,
|
|
284
|
+
// 只有「清单里有 + 占位注册成功」才算真禁用:同名冲突会让 register 抛错,那时不能谎报禁用。
|
|
285
|
+
enabled: !(this.skillsDisabled.has(skill.name) && this.shadowDisposers.has(skill.name)),
|
|
286
|
+
}));
|
|
275
287
|
}
|
|
276
288
|
/**
|
|
277
289
|
* 补上「运行时注册」的 skill:`ctx.skills.register()` 注册的条目没有 SKILL.md,
|
|
@@ -308,6 +320,7 @@ export class SkillMcpService extends Service {
|
|
|
308
320
|
userInvocable: entry.invocation.userInvocable,
|
|
309
321
|
writable: false,
|
|
310
322
|
path: '',
|
|
323
|
+
enabled: true,
|
|
311
324
|
});
|
|
312
325
|
}
|
|
313
326
|
return extra;
|
|
@@ -335,6 +348,7 @@ export class SkillMcpService extends Service {
|
|
|
335
348
|
userInvocable: true,
|
|
336
349
|
writable: true,
|
|
337
350
|
path,
|
|
351
|
+
enabled: true,
|
|
338
352
|
};
|
|
339
353
|
}
|
|
340
354
|
/**
|
|
@@ -451,6 +465,85 @@ export class SkillMcpService extends Service {
|
|
|
451
465
|
// 所以这里改的只是内存态;面板据此显示“重启后恢复”,而不是假装它是持久设置。
|
|
452
466
|
this.sessionToggled.add(id);
|
|
453
467
|
}
|
|
468
|
+
/** 技能禁用清单的落盘位置:跟在 DSH_HOME 下,隔离实例各自一份。 */
|
|
469
|
+
disabledFile() {
|
|
470
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
471
|
+
return join(home, 'skill-mcp-center', 'disabled-skills.json');
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* 读回落盘的禁用清单并重放遮蔽。为什么要自己存:registry 不提供「查看被遮蔽定义」的
|
|
475
|
+
* API(`The registry still exposes no API to inspect shadowed definitions`),
|
|
476
|
+
* 不落盘的话重启即失忆,面板开关就成了一次性的。
|
|
477
|
+
*/
|
|
478
|
+
async loadDisabledSkills() {
|
|
479
|
+
if (!this.shadowsLoaded) {
|
|
480
|
+
this.shadowsLoaded = true;
|
|
481
|
+
try {
|
|
482
|
+
const raw = JSON.parse(await readFile(this.disabledFile(), 'utf8'));
|
|
483
|
+
if (Array.isArray(raw.names)) {
|
|
484
|
+
for (const name of raw.names)
|
|
485
|
+
if (typeof name === 'string')
|
|
486
|
+
this.skillsDisabled.add(name);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// 文件不存在 / 损坏 = 没有禁用项。读失败不该让整个面板打不开。
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// 每次调用都补齐缺失的占位:skills 服务可能晚于本插件就绪,只重放一次会永久失效。
|
|
494
|
+
for (const name of this.skillsDisabled)
|
|
495
|
+
this.applyShadow(name);
|
|
496
|
+
}
|
|
497
|
+
async persistDisabledSkills() {
|
|
498
|
+
const file = this.disabledFile();
|
|
499
|
+
await mkdir(dirname(file), { recursive: true });
|
|
500
|
+
await writeFile(file, `${JSON.stringify({ names: [...this.skillsDisabled].sort() }, null, 2)}\n`, 'utf8');
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* 停用一条技能:注册同名占位——runtime rank(250)赢过包内 provider(≈550)与 bundled(600),
|
|
504
|
+
* 两个 invocation 都为 false,于是模型和人都调不到它,但它仍在 catalog 里、可被面板列出。
|
|
505
|
+
* 这是包内技能**唯一**可逆的停用方式:改名 / 移目录 / 断链对安装内容都不生效。
|
|
506
|
+
*/
|
|
507
|
+
applyShadow(name) {
|
|
508
|
+
if (this.shadowDisposers.has(name))
|
|
509
|
+
return;
|
|
510
|
+
const skills = this.ctx.get('skills');
|
|
511
|
+
if (skills === undefined)
|
|
512
|
+
return;
|
|
513
|
+
try {
|
|
514
|
+
this.shadowDisposers.set(name, skills.register({
|
|
515
|
+
name,
|
|
516
|
+
description: 'Disabled in the Skill & MCP panel.',
|
|
517
|
+
source: 'runtime',
|
|
518
|
+
content: '',
|
|
519
|
+
invocation: { modelInvocable: false, userInvocable: false },
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// 名字不合法、或该名字的占位已存在:跳过。面板仍按清单显示禁用态,
|
|
524
|
+
// 调用方可从 listSkills 的实际 invocation 看出遮蔽没成功。
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
removeShadow(name) {
|
|
528
|
+
const dispose = this.shadowDisposers.get(name);
|
|
529
|
+
if (dispose === undefined)
|
|
530
|
+
return;
|
|
531
|
+
this.shadowDisposers.delete(name);
|
|
532
|
+
dispose();
|
|
533
|
+
}
|
|
534
|
+
/** 启用/停用一条技能。停用走占位遮蔽、启用调 disposer——不写 SKILL.md、不重启。 */
|
|
535
|
+
async setSkillEnabled(name, enabled) {
|
|
536
|
+
await this.loadDisabledSkills();
|
|
537
|
+
if (enabled) {
|
|
538
|
+
this.skillsDisabled.delete(name);
|
|
539
|
+
this.removeShadow(name);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
this.skillsDisabled.add(name);
|
|
543
|
+
this.applyShadow(name);
|
|
544
|
+
}
|
|
545
|
+
await this.persistDisabledSkills();
|
|
546
|
+
}
|
|
454
547
|
/** Runtime status per server: upstream `mcpStatus` seam when present, else derived. */
|
|
455
548
|
async mcpStatus() {
|
|
456
549
|
const servers = await this.listMcpServers();
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-skill-mcp-center",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Skill & MCP management center for DeepSeek Harness — manage skills and MCP servers in Settings, live MCP status in the sidebar",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|