@morehao/dsh-skills-console 0.2.3 → 0.2.4
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 +7 -4
- package/lib/core/cli.js +38 -16
- package/lib/core/paths.js +6 -2
- package/lib/core/service.js +50 -13
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ DSH(DeepSeek Harness)的**技能控制台**:装、更新、删除、接入
|
|
|
41
41
|
|
|
42
42
|
## 安装
|
|
43
43
|
|
|
44
|
+
插件依赖里自带 `skills@1.7.4`,**不需要用户预装 Node / npm / npx**:CLI 由宿主自带的 Node 启动,装插件时随依赖一起落盘。安装技能仍需要**本机有 git**(CLI 会 `git clone` 仓库);首次装插件需要联网拉取依赖。
|
|
45
|
+
|
|
44
46
|
在 Harness 里装本地 link:
|
|
45
47
|
|
|
46
48
|
```bash
|
|
@@ -55,7 +57,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
|
|
|
55
57
|
name: '@morehao/dsh-skills-console'
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
客户端 half 只注册 `platform: web`,面板出现在 DSH Web 的设置页 /
|
|
60
|
+
客户端 half 只注册 `platform: web`,面板出现在 DSH Web 的设置页 / 侧栏。本地 `link:` 开发时,先在仓库跑一次 `npm install`(装上 `skills`),否则 CLI 依赖层不会命中。
|
|
59
61
|
|
|
60
62
|
## 快速上手
|
|
61
63
|
|
|
@@ -66,7 +68,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
|
|
|
66
68
|
|
|
67
69
|
## 可靠性
|
|
68
70
|
|
|
69
|
-
- **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json`
|
|
71
|
+
- **canonical 与 CLI lock 只读**:`~/.agents/skills`、`~/.agents/.skill-lock.json`(设了 `XDG_STATE_HOME` 时为 `$XDG_STATE_HOME/skills/.skill-lock.json`)的增删改只经 `skills` CLI,插件不直接写;探测 / 更新检查会把仓库临时 clone 到 `os.tmpdir()`(只读、用完即删),结果绝不落进 canonical 或 lock。
|
|
70
72
|
- **只动自己的东西**:只处理本插件创建的 `managed` 链接,以及已记账的 `managed-external`;`real` 目录和 `foreign-link` 永不改写,删除前体检直接中止。
|
|
71
73
|
- **启停不改正文**:`mode` 只增删 DSH 侧链接,避免本地修改被下一次 `add` 覆盖。
|
|
72
74
|
- **可回滚**:删除进回收站(7 天);更新前快照到 `backups/`,失败自动还原(接口另提供手动回滚)。
|
|
@@ -78,7 +80,7 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
|
|
|
78
80
|
| 项 | 路径 | 归属 |
|
|
79
81
|
| --- | --- | --- |
|
|
80
82
|
| canonical 真身 | `~/.agents/skills` | `skills` CLI 独占 |
|
|
81
|
-
| CLI lock | `~/.agents/.skill-lock.json` | `skills` CLI 独占(本插件只读) |
|
|
83
|
+
| CLI lock | `~/.agents/.skill-lock.json`(设了 `XDG_STATE_HOME` 时为 `$XDG_STATE_HOME/skills/.skill-lock.json`) | `skills` CLI 独占(本插件只读) |
|
|
82
84
|
| DSH 全局根 | `~/.dsh/skills` | 本插件写软链 |
|
|
83
85
|
| 插件状态 | `$DSH_HOME/skills-console/state.json` | 本插件(`DSH_SKILLS_CONSOLE_HOME` 可覆盖) |
|
|
84
86
|
| 备份 / 回收站 | `$DSH_HOME/skills-console/{backups,trash}` | 本插件 |
|
|
@@ -90,11 +92,12 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
|
|
|
90
92
|
| --- | --- | --- |
|
|
91
93
|
| `DSH_SKILLS_CONSOLE_HOME` | `$DSH_HOME/skills-console` | 状态、备份、回收站与事件日志的根 |
|
|
92
94
|
| `DSH_SKILLS_CONSOLE_CLI` | 自动定位 | 覆盖 `skills` CLI(测试桩也走这里) |
|
|
93
|
-
| `DSH_SKILLS_CONSOLE_NPX_SPEC` | `skills` |
|
|
95
|
+
| `DSH_SKILLS_CONSOLE_NPX_SPEC` | `skills` | 仅 legacy `npx` 兜底用;桌面端没有 npx,正常应显示 `dependency` |
|
|
94
96
|
| `DSH_SKILLS_CONSOLE_TIMEOUT_MS` | `120000` | 单次 CLI 调用超时 |
|
|
95
97
|
| `DSH_SKILLS_CONSOLE_GIT` | `auto` | 设为 `off` 关闭 git 通道,强制只走 GitHub REST |
|
|
96
98
|
| `DSH_SKILLS_CONSOLE_GIT_TIMEOUT_MS` | `60000` | git 单次操作(`ls-remote` / `clone`)超时 |
|
|
97
99
|
| `GITHUB_TOKEN` / `GH_TOKEN` | 无 | 只读环境变量;带上后 REST 配额从匿名 60 次/小时提到 5000 次/小时 |
|
|
100
|
+
| `XDG_STATE_HOME` | 未设置 | 设置后 CLI lock 移到 `$XDG_STATE_HOME/skills/.skill-lock.json`,插件跟随读取 |
|
|
98
101
|
|
|
99
102
|
关于 GitHub 配额:**匿名 REST 是 60 次/小时,按出口 IP 共享**(公司、校园网出口常被同网段的人一起打满),打满后探测与更新会收到 403。本插件默认让探测、更新检查与 git 兜底走系统 `git`(`ls-remote` + `clone --filter=blob:none --no-checkout`),不占 REST 配额、也不需要 token;git 不可用时才回退 REST,所以原本能用的环境不会因为这条路径变坏。限流且 git 也不可用时,错误文案会给出恢复时间,并提示设置 `GITHUB_TOKEN`。
|
|
100
103
|
|
package/lib/core/cli.js
CHANGED
|
@@ -40,10 +40,15 @@ function findInPath(command, envPath) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* CLI
|
|
43
|
+
* CLI 定位顺序:显式覆盖 → 本插件依赖 → 宿主 profile → PATH → npx(legacy)。
|
|
44
|
+
* 插件依赖排在 profile 前面:版本随插件锁死,不被 PATH 或别的插件带的旧版抢占。
|
|
44
45
|
* 每一层都记来源,诊断页要展示"这次到底跑的哪一个"。
|
|
45
46
|
*/
|
|
46
|
-
export function locateCli({
|
|
47
|
+
export function locateCli({
|
|
48
|
+
env = process.env,
|
|
49
|
+
logger = () => {},
|
|
50
|
+
resolvePackage = () => require.resolve('skills/package.json'),
|
|
51
|
+
} = {}) {
|
|
47
52
|
const override = env.DSH_SKILLS_CONSOLE_CLI;
|
|
48
53
|
if (override && override.trim()) {
|
|
49
54
|
const parts = override.trim().split(/\s+/);
|
|
@@ -51,7 +56,20 @@ export function locateCli({ env = process.env, logger = () => {} } = {}) {
|
|
|
51
56
|
return { command: parts[0], prefixArgs: parts.slice(1), source: 'env-override', jsonCapable: true };
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
//
|
|
59
|
+
// 插件自己的 runtime 依赖:装插件时由宿主 pnpm 一起装好,用户不需要 Node/npm/npx。
|
|
60
|
+
try {
|
|
61
|
+
const pkg = resolvePackage();
|
|
62
|
+
const bin = path.join(path.dirname(pkg), 'bin', 'cli.mjs');
|
|
63
|
+
if (fs.existsSync(bin)) {
|
|
64
|
+
return { command: process.execPath, prefixArgs: [bin], source: 'dependency', jsonCapable: true };
|
|
65
|
+
}
|
|
66
|
+
// 包在但入口丢了,说明安装不完整;交给后面的层兜底,但要留痕
|
|
67
|
+
logger('cli.dependency.bin-missing', { pkg, bin });
|
|
68
|
+
} catch {
|
|
69
|
+
// 没装在本插件可见范围内(例如 link: 开发仓库还没 npm install),继续往下走
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 宿主 profile 里的 skills 依赖(兼容旧插件 / 其他插件已经带进来的版本)。
|
|
55
73
|
// 注意:GUI 启动的宿主进程常常拿不到 DSH_PROFILE_DIR,所以再按 DSH_HOME/profiles/* 兜一层探测。
|
|
56
74
|
const profileDir = env.DSH_PROFILE_DIR;
|
|
57
75
|
if (profileDir) {
|
|
@@ -74,16 +92,6 @@ export function locateCli({ env = process.env, logger = () => {} } = {}) {
|
|
|
74
92
|
// 没有 profiles 目录就继续往下走
|
|
75
93
|
}
|
|
76
94
|
|
|
77
|
-
try {
|
|
78
|
-
const pkg = require.resolve('skills/package.json');
|
|
79
|
-
const bin = path.join(path.dirname(pkg), 'bin', 'cli.mjs');
|
|
80
|
-
if (fs.existsSync(bin)) {
|
|
81
|
-
return { command: process.execPath, prefixArgs: [bin], source: 'dependency', jsonCapable: true };
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
// 没有作为依赖装在本插件可见范围内,继续往下走
|
|
85
|
-
}
|
|
86
|
-
|
|
87
95
|
const onPath = findInPath('skills', env.PATH);
|
|
88
96
|
if (onPath) return { command: onPath, prefixArgs: [], source: 'path', jsonCapable: true };
|
|
89
97
|
|
|
@@ -105,8 +113,13 @@ export function runCli(location, args, { timeoutMs = 120000, signal, env = proce
|
|
|
105
113
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
114
|
env: {
|
|
107
115
|
...env,
|
|
116
|
+
// 桌面宿主本身就是 Electron 的 Node 模式;显式补上,避免调用方传了裁剪过的 env
|
|
117
|
+
...(process.versions.electron && location.command === process.execPath ? { ELECTRON_RUN_AS_NODE: '1' } : {}),
|
|
108
118
|
CI: '1',
|
|
109
119
|
NO_COLOR: '1',
|
|
120
|
+
// CLI 的遥测只看这两个变量;CI=1 不会关闭,只会在上报里打 ci=1 标记
|
|
121
|
+
DO_NOT_TRACK: '1',
|
|
122
|
+
DISABLE_TELEMETRY: '1',
|
|
110
123
|
TERM: 'dumb',
|
|
111
124
|
GIT_TERMINAL_PROMPT: '0',
|
|
112
125
|
GIT_HTTP_LOW_SPEED_LIMIT: '1',
|
|
@@ -151,12 +164,12 @@ export function runCli(location, args, { timeoutMs = 120000, signal, env = proce
|
|
|
151
164
|
});
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
export function createCli({ env = process.env, logger = () => {} } = {}) {
|
|
167
|
+
export function createCli({ env = process.env, logger = () => {}, resolvePackage } = {}) {
|
|
155
168
|
let cached = null;
|
|
156
169
|
let capabilities = null;
|
|
157
170
|
|
|
158
171
|
function location() {
|
|
159
|
-
if (!cached) cached = locateCli({ env, logger });
|
|
172
|
+
if (!cached) cached = locateCli({ env, logger, resolvePackage });
|
|
160
173
|
return cached;
|
|
161
174
|
}
|
|
162
175
|
|
|
@@ -178,7 +191,16 @@ export function createCli({ env = process.env, logger = () => {} } = {}) {
|
|
|
178
191
|
/** 能力位:`--json` 与 `--full-depth` 任一不支持都要在 UI 上禁用写操作(漏装比不可用更危险)。 */
|
|
179
192
|
async capabilities({ force = false } = {}) {
|
|
180
193
|
if (capabilities && !force) return capabilities;
|
|
181
|
-
|
|
194
|
+
let loc;
|
|
195
|
+
try {
|
|
196
|
+
loc = location();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!(error instanceof CliUnavailableError)) throw error;
|
|
199
|
+
// 不缓存:用户补好依赖 / PATH 后,下一次 /state 就能恢复,不必重启宿主;
|
|
200
|
+
// 同时清掉旧的成功缓存,避免 force 探测到 missing 后又回落到上一轮的 usable。
|
|
201
|
+
capabilities = null;
|
|
202
|
+
return { version: null, json: false, fullDepth: false, source: 'missing', usable: false };
|
|
203
|
+
}
|
|
182
204
|
if (!loc.jsonCapable) {
|
|
183
205
|
capabilities = { version: null, json: false, fullDepth: false, source: loc.source, usable: false };
|
|
184
206
|
return capabilities;
|
package/lib/core/paths.js
CHANGED
|
@@ -12,6 +12,10 @@ export function createPaths(env = process.env, overrides = {}) {
|
|
|
12
12
|
const dshHome = overrides.dshHome ?? env.DSH_HOME ?? path.join(home, '.dsh');
|
|
13
13
|
const stateDir =
|
|
14
14
|
overrides.stateDir ?? env.DSH_SKILLS_CONSOLE_HOME ?? path.join(dshHome, 'skills-console');
|
|
15
|
+
// 与 skills CLI 的 getSkillLockPath 对齐:设了 XDG_STATE_HOME 时 lock 在 XDG 下
|
|
16
|
+
const lockFile = env.XDG_STATE_HOME
|
|
17
|
+
? path.join(env.XDG_STATE_HOME, 'skills', '.skill-lock.json')
|
|
18
|
+
: path.join(home, '.agents', '.skill-lock.json');
|
|
15
19
|
|
|
16
20
|
return {
|
|
17
21
|
home,
|
|
@@ -20,8 +24,8 @@ export function createPaths(env = process.env, overrides = {}) {
|
|
|
20
24
|
stateDir,
|
|
21
25
|
/** canonical 真身:CLI 唯一的落点(F1) */
|
|
22
26
|
canonical: path.join(home, '.agents', 'skills'),
|
|
23
|
-
/** CLI 全局 lock(F4
|
|
24
|
-
lockFile
|
|
27
|
+
/** CLI 全局 lock(F4);XDG_STATE_HOME 优先,与 CLI 一致 */
|
|
28
|
+
lockFile,
|
|
25
29
|
/** DSH 原生 user-dsh 根(rank 400) */
|
|
26
30
|
dshGlobalSkills: path.join(dshHome, 'skills'),
|
|
27
31
|
stateFile: path.join(stateDir, 'state.json'),
|
package/lib/core/service.js
CHANGED
|
@@ -33,6 +33,27 @@ function nowIso() {
|
|
|
33
33
|
return new Date().toISOString();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// skills CLI 对任意 GitHub 仓库走 git clone;没装 git 时它把错误打在 stdout(spinner)里
|
|
37
|
+
const GIT_MISSING_RE = /spawn git ENOENT|git: command not found|not recognized as an internal or external command/i;
|
|
38
|
+
|
|
39
|
+
/** 把 CLI 的 add 失败翻译成可执行错误;git 缺失时不要只给一句 spawn ENOENT。 */
|
|
40
|
+
function cliAddFailure(result, action) {
|
|
41
|
+
const detail = {
|
|
42
|
+
code: result.code,
|
|
43
|
+
stderr: (result.stderr || '').slice(-2000),
|
|
44
|
+
stdout: (result.stdout || '').slice(-2000),
|
|
45
|
+
};
|
|
46
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
47
|
+
if (GIT_MISSING_RE.test(output)) {
|
|
48
|
+
return new ServiceError(
|
|
49
|
+
'error.skills.git-missing',
|
|
50
|
+
'安装/更新技能需要本机有 git(skills CLI 会 clone 仓库);请安装 git 后重试',
|
|
51
|
+
detail,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return new ServiceError('error.skills.cli-failed', `skills ${action} 失败`, detail);
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
/** 本机时区的 `HH:mm`。手写而不是 `toLocaleTimeString`:不受 ICU / locale 差异影响。 */
|
|
37
58
|
function formatClock(ms) {
|
|
38
59
|
const at = new Date(ms);
|
|
@@ -192,6 +213,19 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
192
213
|
if (store.readOnly) throw new StateReadOnlyError(store.problem);
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
/** 所有会调 CLI 的写操作共用;缺失时给 400,不要把裸 CliUnavailableError 漏成 500。 */
|
|
217
|
+
async function requireUsableCli() {
|
|
218
|
+
const capabilities = await cli.capabilities();
|
|
219
|
+
if (!capabilities.usable) {
|
|
220
|
+
throw new ServiceError(
|
|
221
|
+
'error.skills.cli-missing',
|
|
222
|
+
'skills CLI 不可用或不支持 --json/--full-depth,写操作已禁用',
|
|
223
|
+
capabilities,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return capabilities;
|
|
227
|
+
}
|
|
228
|
+
|
|
195
229
|
function detectedTargets() {
|
|
196
230
|
return registry.detected();
|
|
197
231
|
}
|
|
@@ -831,10 +865,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
831
865
|
|
|
832
866
|
async install({ source, subscriptionId, skills = [], commit, targets, scopeProjects = [] }) {
|
|
833
867
|
assertWritable();
|
|
834
|
-
|
|
835
|
-
if (!capabilities.usable) {
|
|
836
|
-
throw new ServiceError('error.skills.cli-missing', 'skills CLI 不可用或不支持 --json/--full-depth,写操作已禁用', capabilities);
|
|
837
|
-
}
|
|
868
|
+
await requireUsableCli();
|
|
838
869
|
// 技能名可留空:有订阅快照就装快照里的条目,否则交给 CLI 的 `--skill '*'`(整仓全部)
|
|
839
870
|
const requested = (skills || []).filter(Boolean);
|
|
840
871
|
|
|
@@ -853,12 +884,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
853
884
|
|
|
854
885
|
const result = await cli.add({ source: sourceArg, skills: planned });
|
|
855
886
|
if (result.timedOut) throw new ServiceError('error.skills.cli-timeout', 'skills add 超时');
|
|
856
|
-
if (result.code !== 0)
|
|
857
|
-
throw new ServiceError('error.skills.cli-failed', 'skills add 失败', {
|
|
858
|
-
code: result.code,
|
|
859
|
-
stderr: result.stderr.slice(-2000),
|
|
860
|
-
});
|
|
861
|
-
}
|
|
887
|
+
if (result.code !== 0) throw cliAddFailure(result, 'add');
|
|
862
888
|
// 留空时实际装了哪些技能以 CLI 的 JSON 输出为准(可能不止一个)
|
|
863
889
|
const names = planned.length > 0 ? planned : installedNamesFrom(result.stdout);
|
|
864
890
|
if (names.length === 0) {
|
|
@@ -947,6 +973,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
947
973
|
|
|
948
974
|
async remove({ name }) {
|
|
949
975
|
assertWritable();
|
|
976
|
+
await requireUsableCli(); // 快照与体检之前就拒绝,避免留下孤儿 trash
|
|
950
977
|
return enqueue(`remove:${name}`, async () => {
|
|
951
978
|
const canonicalPath = path.join(paths.canonical, name);
|
|
952
979
|
if (!fs.existsSync(canonicalPath)) {
|
|
@@ -1341,6 +1368,7 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
1341
1368
|
|
|
1342
1369
|
async applyUpdate({ name, token, overwrite = false }) {
|
|
1343
1370
|
assertWritable();
|
|
1371
|
+
await requireUsableCli();
|
|
1344
1372
|
return enqueue(`apply:${name}`, async () => {
|
|
1345
1373
|
const plan = await this.planUpdate({ name });
|
|
1346
1374
|
if (token && token !== plan.token) {
|
|
@@ -1371,12 +1399,21 @@ export function createService({ paths, store, registry, cli, logger, env = proce
|
|
|
1371
1399
|
|
|
1372
1400
|
if (result.code !== 0 || !present || !verified) {
|
|
1373
1401
|
if (backup) restoreBackup({ backup, toDir: canonicalPath });
|
|
1402
|
+
const failure = result.code !== 0 ? cliAddFailure(result, 'add') : null;
|
|
1403
|
+
const gitMissing = failure?.code === 'error.skills.git-missing';
|
|
1374
1404
|
logger('updates.apply.failed', { name, code: result.code, verified, present });
|
|
1405
|
+
const suffix = backup ? ';已回滚到上一版' : ';没有可回滚的备份';
|
|
1375
1406
|
return {
|
|
1376
1407
|
ok: false,
|
|
1377
|
-
code: 'error.skills.update-failed',
|
|
1378
|
-
message:
|
|
1379
|
-
detail: {
|
|
1408
|
+
code: gitMissing ? failure.code : 'error.skills.update-failed',
|
|
1409
|
+
message: gitMissing ? `${failure.message}${suffix}` : `更新失败${suffix}`,
|
|
1410
|
+
detail: {
|
|
1411
|
+
cliCode: result.code,
|
|
1412
|
+
verified,
|
|
1413
|
+
present,
|
|
1414
|
+
stderr: (result.stderr || '').slice(-2000),
|
|
1415
|
+
...(failure?.detail || {}),
|
|
1416
|
+
},
|
|
1380
1417
|
backup: backup?.key || null,
|
|
1381
1418
|
};
|
|
1382
1419
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-skills-console",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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>",
|
|
@@ -68,6 +68,9 @@
|
|
|
68
68
|
"dsh-plugin",
|
|
69
69
|
"skills"
|
|
70
70
|
],
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"skills": "1.7.4"
|
|
73
|
+
},
|
|
71
74
|
"devDependencies": {
|
|
72
75
|
"esbuild": "0.28.2"
|
|
73
76
|
}
|