@morehao/dsh-skills-console 0.3.7 → 0.4.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 +55 -65
- package/client.js +457 -457
- package/cordis.patch.yml +2 -1
- package/lib/agent-table.json +133 -64
- package/lib/core/agents.js +68 -9
- package/lib/core/exporter.js +137 -0
- package/lib/core/inventory.js +73 -39
- package/lib/core/migrations.js +178 -0
- package/lib/core/paths.js +13 -0
- package/lib/core/policy.js +373 -0
- package/lib/core/remote.js +3 -1
- package/lib/core/roots.js +182 -0
- package/lib/core/service.js +391 -137
- package/lib/core/shadow.js +195 -0
- package/lib/core/skill-md.js +23 -4
- package/lib/core/state.js +293 -42
- package/lib/core/visibility.js +145 -0
- package/lib/index.js +27 -0
- package/lib/routes.js +13 -3
- package/package.json +3 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { scanSkillRoot } from './roots.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 可见性判定:**扫描根里有什么 + 本地策略隐藏了哪一份**,而不是「链在不在」。
|
|
6
|
+
*
|
|
7
|
+
* 合并规则(与宿主 registry 对齐,见设计 §4.5):
|
|
8
|
+
* 1. 同一个根内同名:取一个(本插件不做同根多候选);
|
|
9
|
+
* 2. 跨根同名:rank 最小者胜(100 > 200 > 300 > 400 > 500 > 600 的优先级);
|
|
10
|
+
* 3. winner 是 canonical 且被本地策略停用 → 对模型不可见;
|
|
11
|
+
* 4. winner 自己写着 `disable-model-invocation: true`(旧版遗留停用桩 / 别人写的同名
|
|
12
|
+
* 停用技能)→ 也不可见,避免把「假技能」当成真身;
|
|
13
|
+
* 5. 宿主 `snapshot()` 里的 bundled/插件技能作为 rank 600 的额外候选,
|
|
14
|
+
* 任何本地根的同名技能都盖过它。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function samePath(left, right) {
|
|
18
|
+
if (!left || !right) return false;
|
|
19
|
+
return path.resolve(left) === path.resolve(right);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isPolicyHidden(entry, policyHiddenPaths) {
|
|
23
|
+
for (const hidden of policyHiddenPaths) {
|
|
24
|
+
if (samePath(hidden, entry.path)) return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @param {Array} options.roots `roots.listRoots()` 的结果
|
|
32
|
+
* @param {Array} options.hostSkills `ctx.skills.snapshot()` 的技能
|
|
33
|
+
* @param {Set} options.policyHiddenPaths `enabled === false` 的 canonical 目录
|
|
34
|
+
* @returns {{byName: Map, roots: Array, complete: boolean, errors: Array}}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveVisibility({ roots = [], hostSkills = [], policyHiddenPaths = new Set() }) {
|
|
37
|
+
const byName = new Map();
|
|
38
|
+
const errors = [];
|
|
39
|
+
|
|
40
|
+
const rootStats = roots.map((root) => {
|
|
41
|
+
const stat = { source: root.source, rank: root.rank, path: root.path, label: root.label, exists: root.exists, count: 0 };
|
|
42
|
+
if (!root.exists) return stat;
|
|
43
|
+
const scanned = scanSkillRoot(root.path, (error) => {
|
|
44
|
+
errors.push({ source: root.source, path: root.path, error: error.message || String(error) });
|
|
45
|
+
});
|
|
46
|
+
const { entries, ignored = [] } = scanned;
|
|
47
|
+
stat.count = entries.length;
|
|
48
|
+
if (ignored.length > 0) stat.ignored = ignored;
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const policyHidden = isPolicyHidden(entry, policyHiddenPaths);
|
|
51
|
+
const foreignHidden = entry.modelInvocable === false;
|
|
52
|
+
const candidate = {
|
|
53
|
+
source: root.source,
|
|
54
|
+
rank: root.rank,
|
|
55
|
+
path: entry.path,
|
|
56
|
+
dirName: entry.dirName || null,
|
|
57
|
+
kind: entry.kind,
|
|
58
|
+
hasSkillFile: entry.hasSkillFile,
|
|
59
|
+
linked: Boolean(entry.linked),
|
|
60
|
+
// `shadowed` 保留旧名字给 UI/诊断用:它现在表示「这一份对模型不可见」。
|
|
61
|
+
shadowed: policyHidden || foreignHidden,
|
|
62
|
+
policyHidden,
|
|
63
|
+
foreignHidden,
|
|
64
|
+
host: false,
|
|
65
|
+
};
|
|
66
|
+
push(byName, entry.name, candidate);
|
|
67
|
+
}
|
|
68
|
+
return stat;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
for (const skill of hostSkills) {
|
|
72
|
+
if (!skill || typeof skill.name !== 'string' || !skill.name) continue;
|
|
73
|
+
push(byName, skill.name, {
|
|
74
|
+
source: 'bundled',
|
|
75
|
+
rank: 600,
|
|
76
|
+
path: skill.path || null,
|
|
77
|
+
kind: 'dir',
|
|
78
|
+
hasSkillFile: true,
|
|
79
|
+
linked: false,
|
|
80
|
+
shadowed: false,
|
|
81
|
+
policyHidden: false,
|
|
82
|
+
foreignHidden: false,
|
|
83
|
+
host: true,
|
|
84
|
+
provider: skill.provider || null,
|
|
85
|
+
hostSource: skill.source || null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resolved = new Map();
|
|
90
|
+
for (const [name, candidates] of byName) {
|
|
91
|
+
candidates.sort((left, right) => left.rank - right.rank);
|
|
92
|
+
const winner = candidates[0];
|
|
93
|
+
// 每个根里的状态,供 UI 解释「为什么看不见」
|
|
94
|
+
const sources = {};
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
const state = candidate.shadowed ? 'shadowed' : 'present';
|
|
97
|
+
if (!sources[candidate.source] || state === 'shadowed') sources[candidate.source] = state;
|
|
98
|
+
}
|
|
99
|
+
// 同名但目录名不同 = 有一份「伪装成同名技能」的副本(如 `x.bak-2026…/`),
|
|
100
|
+
// 宿主按 frontmatter name 去重,所以它们真的会互相遮盖。这条必须让 UI 说出来。
|
|
101
|
+
const nameConflict = candidates.some(
|
|
102
|
+
(item) => item.dirName && winner.dirName && item.dirName !== winner.dirName,
|
|
103
|
+
);
|
|
104
|
+
resolved.set(name, {
|
|
105
|
+
name,
|
|
106
|
+
visible: !winner.shadowed,
|
|
107
|
+
shadowed: winner.shadowed,
|
|
108
|
+
policyHidden: Boolean(winner.policyHidden),
|
|
109
|
+
foreignHidden: Boolean(winner.foreignHidden),
|
|
110
|
+
hiddenBy: winner.shadowed
|
|
111
|
+
? [{ kind: winner.policyHidden ? 'policy' : 'foreign', path: winner.path }]
|
|
112
|
+
: [],
|
|
113
|
+
winnerSource: winner.source,
|
|
114
|
+
winnerPath: winner.path,
|
|
115
|
+
winnerDirName: winner.dirName || null,
|
|
116
|
+
path: winner.path,
|
|
117
|
+
kind: winner.kind,
|
|
118
|
+
hasSkillFile: winner.hasSkillFile,
|
|
119
|
+
hostOnly: winner.host,
|
|
120
|
+
nameConflict,
|
|
121
|
+
// 同名多份里被盖住的那几份,UI 要能列出「它们没生效」
|
|
122
|
+
shadowedBy: nameConflict
|
|
123
|
+
? candidates
|
|
124
|
+
.filter((item) => item !== winner)
|
|
125
|
+
.map((item) => ({
|
|
126
|
+
source: item.source,
|
|
127
|
+
rank: item.rank,
|
|
128
|
+
path: item.path,
|
|
129
|
+
dirName: item.dirName || null,
|
|
130
|
+
shadowed: item.shadowed,
|
|
131
|
+
}))
|
|
132
|
+
: [],
|
|
133
|
+
sources,
|
|
134
|
+
candidates,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { byName: resolved, roots: rootStats, complete: errors.length === 0, errors };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function push(map, name, candidate) {
|
|
142
|
+
const list = map.get(name);
|
|
143
|
+
if (list) list.push(candidate);
|
|
144
|
+
else map.set(name, [candidate]);
|
|
145
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createInstaller } from './core/installer.js';
|
|
|
2
2
|
import { createLogger } from './core/log.js';
|
|
3
3
|
import { createPaths } from './core/paths.js';
|
|
4
4
|
import { createRegistry } from './core/agents.js';
|
|
5
|
+
import { createPolicyProvider, installPolicyProviders } from './core/policy.js';
|
|
5
6
|
import { createService } from './core/service.js';
|
|
6
7
|
import { StateStore } from './core/state.js';
|
|
7
8
|
import { createRoutes, ROUTE_PREFIX } from './routes.js';
|
|
@@ -24,6 +25,20 @@ export function apply(ctx, config = {}) {
|
|
|
24
25
|
const registry = createRegistry({ paths, getState: () => store.state });
|
|
25
26
|
const installer = createInstaller({ paths, env, logger });
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 本地策略 provider:只报告 `enabled === false` 的 canonical 技能,注册到**每个 live
|
|
30
|
+
* agent 自己的 scope**(agent scope 是 preset scope 的子层,层序比 rank 更近)。
|
|
31
|
+
* 必须在 service 之前建好:setMode 写完后要通过闭包调 `invalidate()`。
|
|
32
|
+
*/
|
|
33
|
+
const policyProvider = createPolicyProvider({ paths, getState: () => store.state, logger });
|
|
34
|
+
const policies = installPolicyProviders(ctx, { provider: policyProvider, logger });
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 宿主的技能快照。**注意**:不传 `scope`,只能看到 global 层的 provider —— 文件系统
|
|
38
|
+
* provider 挂在 agent preset 的 scope 层,所以这里拿到的多半只有 bundled/插件技能。
|
|
39
|
+
* 可见性以 `roots.js` 的本地扫描为准,这个快照只作 rank 600 的垫底候选。
|
|
40
|
+
* 等宿主提供可用的 scope 入口,再考虑把可见性整体委托给它(见 README「边界(明确不做)」)。
|
|
41
|
+
*/
|
|
27
42
|
const visibleProvider = async (cwd) => {
|
|
28
43
|
const skills = ctx.get('skills');
|
|
29
44
|
if (!skills) return { skills: [], complete: false };
|
|
@@ -69,9 +84,21 @@ export function apply(ctx, config = {}) {
|
|
|
69
84
|
env,
|
|
70
85
|
visibleProvider,
|
|
71
86
|
workspacesProvider,
|
|
87
|
+
invalidatePolicies: () => policies.invalidate(),
|
|
88
|
+
policyStatus: () => policies.status(),
|
|
72
89
|
});
|
|
73
90
|
|
|
91
|
+
// 迁移必须尽早跑:GUI 一打开就该是「停用 = 真不可见」,而不是等用户点到某一行才发现。
|
|
92
|
+
// 失败只记日志:插件仍要能加载(读路径降级、写路径返回带原因的 409)。
|
|
93
|
+
try {
|
|
94
|
+
const migrated = service.ensureMigrated();
|
|
95
|
+
if (migrated?.migrated) logger('plugin.migrate', { report: migrated.report });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger('plugin.migrate.failed', { message: error?.message, code: error?.code });
|
|
98
|
+
}
|
|
99
|
+
|
|
74
100
|
const handler = createRoutes({ service, logger });
|
|
75
101
|
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }), 'dsh-skills-console:routes');
|
|
102
|
+
ctx.effect(() => () => policies.dispose(), 'dsh-skills-console:policy');
|
|
76
103
|
logger('plugin.apply', { stateDir: paths.stateDir, canonical: paths.canonical, readOnly: store.readOnly });
|
|
77
104
|
}
|
package/lib/routes.js
CHANGED
|
@@ -60,6 +60,8 @@ function matchRoute(method, pathname) {
|
|
|
60
60
|
if (segments.length === 1 && segments[0] === 'state' && method === 'GET') return { name: 'state' };
|
|
61
61
|
if (segments.length === 1 && segments[0] === 'agents' && method === 'GET') return { name: 'agents' };
|
|
62
62
|
if (segments.length === 1 && segments[0] === 'settings' && method === 'POST') return { name: 'settings' };
|
|
63
|
+
// `/links` 是 v1 名字,留一份 minor 的别名;新代码只认 `/exports`
|
|
64
|
+
if (segments.length === 1 && segments[0] === 'exports' && method === 'POST') return { name: 'exports' };
|
|
63
65
|
if (segments.length === 1 && segments[0] === 'links' && method === 'POST') return { name: 'links' };
|
|
64
66
|
if (segments.length === 1 && segments[0] === 'install' && method === 'POST') return { name: 'install' };
|
|
65
67
|
if (segments.length === 1 && segments[0] === 'probe' && method === 'POST') return { name: 'probe' };
|
|
@@ -138,7 +140,8 @@ export function createRoutes({ service, logger }) {
|
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
140
142
|
if (error instanceof ServiceError) {
|
|
141
|
-
|
|
143
|
+
// 默认 400;policy-unavailable / shadow-locked 等能力或状态冲突带 409。
|
|
144
|
+
sendJson(res, error.status || 400, { ok: false, code: error.code, message: error.message, detail: error.detail ?? null });
|
|
142
145
|
return;
|
|
143
146
|
}
|
|
144
147
|
logger('route.error', { message: error?.message, stack: String(error?.stack || '').slice(0, 800) });
|
|
@@ -157,8 +160,10 @@ async function dispatch(route, { body, query, service }) {
|
|
|
157
160
|
return { ok: true, all: service.registry.all(), detected: service.registry.detected() };
|
|
158
161
|
case 'settings':
|
|
159
162
|
return service.setSettings(body);
|
|
163
|
+
case 'exports':
|
|
164
|
+
return service.setExports(body);
|
|
160
165
|
case 'links':
|
|
161
|
-
return service.
|
|
166
|
+
return service.setExports(body);
|
|
162
167
|
case 'install':
|
|
163
168
|
return service.install(body);
|
|
164
169
|
case 'probe':
|
|
@@ -172,7 +177,12 @@ async function dispatch(route, { body, query, service }) {
|
|
|
172
177
|
case 'skill.adopt':
|
|
173
178
|
return service.adopt({ name: route.params.name });
|
|
174
179
|
case 'skill.mode':
|
|
175
|
-
return service.setMode({
|
|
180
|
+
return service.setMode({
|
|
181
|
+
name: route.params.name,
|
|
182
|
+
mode: body.mode,
|
|
183
|
+
scope: body.scope,
|
|
184
|
+
projectRoot: body.projectRoot,
|
|
185
|
+
});
|
|
176
186
|
case 'skill.remove':
|
|
177
187
|
return service.remove({ name: route.params.name });
|
|
178
188
|
case 'subscriptions.add':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-skills-console",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "morehao <morehao@qq.com>",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"build:client": "node scripts/build-client.mjs",
|
|
53
53
|
"dev:client": "node scripts/build-client.mjs --watch",
|
|
54
54
|
"check:client": "node scripts/build-client.mjs --check",
|
|
55
|
-
"check": "node scripts/check-
|
|
55
|
+
"check:cycles": "node scripts/check-cycles.mjs",
|
|
56
|
+
"check": "node scripts/check-syntax.mjs && node scripts/check-cycles.mjs && node scripts/build-client.mjs --check && node scripts/check-naming.mjs && node scripts/check-pack.mjs && node scripts/smoke-release-notes.mjs",
|
|
56
57
|
"check:syntax": "node scripts/check-syntax.mjs",
|
|
57
58
|
"check:naming": "node scripts/check-naming.mjs",
|
|
58
59
|
"check:pack": "node scripts/check-pack.mjs",
|