dsh-m 0.4.2 → 0.5.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.en.md +12 -0
- package/README.md +13 -1
- package/docs/DESIGN.md +30 -0
- package/lib/cli.js +94 -20
- package/lib/client.js +282 -17
- package/lib/core/community-adapter.js +181 -0
- package/lib/core/community.js +433 -0
- package/lib/core/host-api.js +53 -19
- package/lib/core/httpx.js +38 -0
- package/lib/core/install-guard.js +308 -0
- package/lib/core/market.js +678 -55
- package/lib/core/profile-transaction.js +238 -1
- package/lib/core/registry-controller.js +8 -0
- package/lib/core/registry.js +3 -3
- package/lib/core/settings-compat.js +4 -0
- package/lib/core/toggle.js +6 -1
- package/lib/core/versions.js +149 -25
- package/lib/host.js +2 -0
- package/lib/tools.js +137 -33
- package/package.json +1 -1
package/lib/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
2
|
import { installTimeoutMs } from './core/env.js';
|
|
3
|
-
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, } from './core/market.js';
|
|
3
|
+
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, InstallGuardError, } from './core/market.js';
|
|
4
4
|
import { appExitFromContext, scheduleRestart } from './core/restart.js';
|
|
5
5
|
import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
|
|
6
6
|
export const CATEGORY_LABELS = {
|
|
@@ -10,6 +10,38 @@ export const CATEGORY_LABELS = {
|
|
|
10
10
|
search: '搜索',
|
|
11
11
|
other: '其他',
|
|
12
12
|
};
|
|
13
|
+
/** 工具 deadline 对齐(M1 Task 7):search 45s(core 44s)、list/outdated 65s(core 60s + 5s 回包余量)。 */
|
|
14
|
+
const SEARCH_TOOL_TIMEOUT_MS = 45_000;
|
|
15
|
+
const SEARCH_CORE_DEADLINE_MS = 44_000;
|
|
16
|
+
const INSTALLED_TOOL_TIMEOUT_MS = 65_000;
|
|
17
|
+
const INSTALLED_CORE_DEADLINE_MS = 60_000;
|
|
18
|
+
/** 社区开放分类安全 slug(与 host-api 侧同语义) */
|
|
19
|
+
const COMMUNITY_SLUG_RE = /^[a-z0-9-]{1,32}$/;
|
|
20
|
+
/** 社区 summary 四键投影(Task 7 ②):agent 工具输出只带这四个字段。 */
|
|
21
|
+
export function communityToolSummary(c) {
|
|
22
|
+
if (!c)
|
|
23
|
+
return { acceptedCount: null, route: null, status: 'unavailable', version: null };
|
|
24
|
+
const unavailableLike = c.status === 'disabled' || c.status === 'unavailable';
|
|
25
|
+
return {
|
|
26
|
+
acceptedCount: unavailableLike ? null : c.acceptedCount,
|
|
27
|
+
route: unavailableLike ? null : c.route,
|
|
28
|
+
status: c.status,
|
|
29
|
+
version: c.version,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** latestErrorCode → 安全化原因(Task 7 ⑨):不泄露内部细节,按 code 归类。 */
|
|
33
|
+
export function latestErrorCodeReason(code) {
|
|
34
|
+
switch (code) {
|
|
35
|
+
case 'budget-exhausted':
|
|
36
|
+
return '因 GitHub 预算未完成';
|
|
37
|
+
case 'rate-limited':
|
|
38
|
+
return 'GitHub 限流';
|
|
39
|
+
case 'timeout':
|
|
40
|
+
return '检查超时';
|
|
41
|
+
default:
|
|
42
|
+
return '网络错误';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
13
45
|
function cloneJson(value) {
|
|
14
46
|
return JSON.parse(JSON.stringify(value));
|
|
15
47
|
}
|
|
@@ -36,9 +68,10 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
36
68
|
query: { type: 'string', description: 'Main keyword, e.g. 主题 or 搜索. Optional.' },
|
|
37
69
|
category: {
|
|
38
70
|
type: 'string',
|
|
39
|
-
description: `Optional
|
|
71
|
+
description: `Optional category: curated ${Object.keys(CATEGORY_LABELS).join('/')} or any community slug ([a-z0-9-]{1,32}, e.g. theme/memory/git).`,
|
|
40
72
|
},
|
|
41
73
|
limit: { type: 'number', description: 'Cards in this batch. Default all (registry is curated & small).' },
|
|
74
|
+
primary_only: { type: 'boolean', description: '只看主清单(排除社区条目). Optional.' },
|
|
42
75
|
},
|
|
43
76
|
output: {
|
|
44
77
|
schema: { type: 'object', additionalProperties: true },
|
|
@@ -56,19 +89,30 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
56
89
|
title: isError ? '市场搜索失败' : `DSH 市场 · ${meta?.items?.length ?? 0} 条`,
|
|
57
90
|
content: [],
|
|
58
91
|
}),
|
|
59
|
-
timeoutMs:
|
|
60
|
-
async execute(args) {
|
|
61
|
-
const
|
|
92
|
+
timeoutMs: SEARCH_TOOL_TIMEOUT_MS,
|
|
93
|
+
async execute(args, exec) {
|
|
94
|
+
const categoryRaw = typeof args.category === 'string' ? args.category.trim() : '';
|
|
95
|
+
let category = null;
|
|
96
|
+
if (categoryRaw !== '') {
|
|
97
|
+
if (categoryRaw in CATEGORY_LABELS || COMMUNITY_SLUG_RE.test(categoryRaw))
|
|
98
|
+
category = categoryRaw;
|
|
99
|
+
else
|
|
100
|
+
throw new Error(`非法分类: ${categoryRaw}(需精选分类或 [a-z0-9-]{1,32} slug)`);
|
|
101
|
+
}
|
|
62
102
|
const rawLimit = Number(args.limit);
|
|
63
103
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? clamp(rawLimit, 1, 80) : undefined;
|
|
64
|
-
// metadata-only:agent 卡片不需要 latest;与 Host GUI 共用 host namespace
|
|
104
|
+
// metadata-only:agent 卡片不需要 latest;与 Host GUI 共用 host namespace;
|
|
105
|
+
// deadline 44s 是本 waiter 的绝对上限(工具 timeout 45s 先到兜底)
|
|
65
106
|
const result = await m.listMarket(cfg, {
|
|
66
107
|
query: String(args.query || ''),
|
|
67
108
|
category,
|
|
68
109
|
offset: 0,
|
|
69
110
|
limit,
|
|
111
|
+
primaryOnly: args.primary_only === true,
|
|
70
112
|
withLatest: false,
|
|
71
113
|
namespace: 'host',
|
|
114
|
+
deadlineMs: SEARCH_CORE_DEADLINE_MS,
|
|
115
|
+
signal: exec?.signal,
|
|
72
116
|
});
|
|
73
117
|
// 安装标注唯一来源:listMarket 的单次 profile 快照。
|
|
74
118
|
// 状态不完整且存在可被误标的条目时 fail-closed——不把未知安装状态呈现成未安装;
|
|
@@ -81,6 +125,7 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
81
125
|
category,
|
|
82
126
|
total: result.total,
|
|
83
127
|
registry: summaryOf(result.registryState),
|
|
128
|
+
community: communityToolSummary(result.community),
|
|
84
129
|
items: result.items.map((e) => ({
|
|
85
130
|
id: e.id,
|
|
86
131
|
name: e.name,
|
|
@@ -113,11 +158,16 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
113
158
|
title: isError ? '列出失败' : `已装 · ${meta?.items?.length ?? 0} 个`,
|
|
114
159
|
content: [],
|
|
115
160
|
}),
|
|
116
|
-
timeoutMs:
|
|
117
|
-
async execute() {
|
|
118
|
-
const result = await m.listInstalledWithMeta(cfg, {
|
|
161
|
+
timeoutMs: INSTALLED_TOOL_TIMEOUT_MS,
|
|
162
|
+
async execute(_args, exec) {
|
|
163
|
+
const result = await m.listInstalledWithMeta(cfg, {
|
|
164
|
+
namespace: 'host',
|
|
165
|
+
deadlineMs: INSTALLED_CORE_DEADLINE_MS,
|
|
166
|
+
signal: exec?.signal,
|
|
167
|
+
});
|
|
119
168
|
return cloneJson({
|
|
120
169
|
registry: summaryOf(result.registryState),
|
|
170
|
+
community: communityToolSummary(result.community),
|
|
121
171
|
profileDir: result.profileDir,
|
|
122
172
|
others: result.others,
|
|
123
173
|
items: result.items.map((it) => ({
|
|
@@ -128,6 +178,8 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
128
178
|
registryId: it.registryId ?? null,
|
|
129
179
|
latestVersion: it.latestVersion ?? null,
|
|
130
180
|
outdated: it.outdated,
|
|
181
|
+
latestError: it.latestError ?? null,
|
|
182
|
+
latestErrorCode: it.latestErrorCode ?? null,
|
|
131
183
|
})),
|
|
132
184
|
});
|
|
133
185
|
},
|
|
@@ -146,11 +198,12 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
146
198
|
presentationMeta: (_args, value) => ({ kind: 'dshm-install', ...value }),
|
|
147
199
|
},
|
|
148
200
|
presentCall: (args) => ({ card: 'generic', title: `安装 · ${String(args.id || '')}`, kind: 'search', content: [] }),
|
|
149
|
-
presentResult: (_args, { isError, meta }) =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
201
|
+
presentResult: (_args, { isError, meta }) => {
|
|
202
|
+
const out = meta;
|
|
203
|
+
if (out?.guard === true)
|
|
204
|
+
return { card: 'generic', title: `守卫拦截 · ${out.compensation?.status ?? ''}`, content: [] };
|
|
205
|
+
return { card: 'generic', title: isError ? '安装失败' : `已安装 · ${out?.pkg || ''}`, content: [] };
|
|
206
|
+
},
|
|
154
207
|
timeoutMs: installTimeoutMs() + 60_000,
|
|
155
208
|
async execute(args) {
|
|
156
209
|
const id = String(args.id || '').trim();
|
|
@@ -158,7 +211,16 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
158
211
|
throw new Error('缺少收录 id');
|
|
159
212
|
const version = typeof args.version === 'string' && args.version.trim() ? args.version.trim() : undefined;
|
|
160
213
|
const force = args.force === true;
|
|
161
|
-
|
|
214
|
+
try {
|
|
215
|
+
return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host' }));
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
if (err instanceof InstallGuardError) {
|
|
219
|
+
// 结构化 error result(M2 Task 3 ㉑-㉜):不渲染「已安装」,无 force 通道;restartSafe 随结果携带
|
|
220
|
+
return { ok: false, guard: true, kind: err.kind, message: err.message, violations: err.violations, compensation: err.compensation, needsRestart: err.needsRestart, restartSafe: err.restartSafe, ...(err.repairBasis ? { repairBasis: err.repairBasis } : {}) };
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
162
224
|
},
|
|
163
225
|
}));
|
|
164
226
|
ctx.tools.register(defineTool({
|
|
@@ -231,11 +293,24 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
231
293
|
presentResult: (_args, { isError, meta }) => {
|
|
232
294
|
const out = meta;
|
|
233
295
|
const n = out?.items?.filter((it) => it.outdated).length ?? 0;
|
|
234
|
-
|
|
296
|
+
const incomplete = out?.incompleteCount ?? 0;
|
|
297
|
+
// 禁止「未完成检查」冒充「全部最新」(Task 7 ⑨)
|
|
298
|
+
const title = isError
|
|
299
|
+
? '检查失败'
|
|
300
|
+
: incomplete > 0
|
|
301
|
+
? `${n} 个可升级(${incomplete} 项检查未完成)`
|
|
302
|
+
: n
|
|
303
|
+
? `${n} 个可升级`
|
|
304
|
+
: '全部最新';
|
|
305
|
+
return { card: 'generic', title, content: [] };
|
|
235
306
|
},
|
|
236
|
-
timeoutMs:
|
|
237
|
-
async execute() {
|
|
238
|
-
const result = await m.listInstalledWithMeta(cfg, {
|
|
307
|
+
timeoutMs: INSTALLED_TOOL_TIMEOUT_MS,
|
|
308
|
+
async execute(_args, exec) {
|
|
309
|
+
const result = await m.listInstalledWithMeta(cfg, {
|
|
310
|
+
namespace: 'host',
|
|
311
|
+
deadlineMs: INSTALLED_CORE_DEADLINE_MS,
|
|
312
|
+
signal: exec?.signal,
|
|
313
|
+
});
|
|
239
314
|
const items = result.items.map((it) => ({
|
|
240
315
|
pkg: it.pkg,
|
|
241
316
|
name: it.name,
|
|
@@ -244,11 +319,16 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
244
319
|
latestVersion: it.latestVersion ?? null,
|
|
245
320
|
latestTag: it.latestTag ?? null,
|
|
246
321
|
outdated: it.outdated,
|
|
322
|
+
latestError: it.latestError ?? null,
|
|
323
|
+
latestErrorCode: it.latestErrorCode ?? null,
|
|
247
324
|
}));
|
|
325
|
+
const incompleteCount = items.filter((it) => it.latestErrorCode !== null).length;
|
|
248
326
|
return cloneJson({
|
|
249
327
|
registry: summaryOf(result.registryState),
|
|
328
|
+
community: communityToolSummary(result.community),
|
|
250
329
|
items,
|
|
251
330
|
outdatedCount: items.filter((it) => it.outdated).length,
|
|
331
|
+
incompleteCount,
|
|
252
332
|
});
|
|
253
333
|
},
|
|
254
334
|
}));
|
|
@@ -264,17 +344,26 @@ export function registerTools(ctx, cfg, deps = {}) {
|
|
|
264
344
|
presentationMeta: (_args, value) => ({ kind: 'dshm-upgrade', ...value }),
|
|
265
345
|
},
|
|
266
346
|
presentCall: (args) => ({ card: 'generic', title: `升级 · ${String(args.pkg || '')}`, content: [] }),
|
|
267
|
-
presentResult: (_args, { isError, meta }) =>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
347
|
+
presentResult: (_args, { isError, meta }) => {
|
|
348
|
+
const out = meta;
|
|
349
|
+
if (out?.guard === true)
|
|
350
|
+
return { card: 'generic', title: `守卫拦截 · ${out.compensation?.status ?? ''}`, content: [] };
|
|
351
|
+
return { card: 'generic', title: isError ? '升级失败' : `已升级 · ${out?.pkg || ''}`, content: [] };
|
|
352
|
+
},
|
|
272
353
|
timeoutMs: installTimeoutMs() + 60_000,
|
|
273
354
|
async execute(args) {
|
|
274
355
|
const target = String(args.pkg || '').trim();
|
|
275
356
|
if (!target)
|
|
276
357
|
throw new Error('缺少 pkg');
|
|
277
|
-
|
|
358
|
+
try {
|
|
359
|
+
return cloneJson(await m.upgradePlugin(target, cfg, { namespace: 'host' }));
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
if (err instanceof InstallGuardError) {
|
|
363
|
+
return { ok: false, guard: true, kind: err.kind, message: err.message, violations: err.violations, compensation: err.compensation, needsRestart: err.needsRestart, restartSafe: err.restartSafe, ...(err.repairBasis ? { repairBasis: err.repairBasis } : {}) };
|
|
364
|
+
}
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
278
367
|
},
|
|
279
368
|
}));
|
|
280
369
|
ctx.tools.register(defineTool({
|
|
@@ -363,6 +452,15 @@ function renderList(out) {
|
|
|
363
452
|
].join('\n');
|
|
364
453
|
}
|
|
365
454
|
function renderInstall(out) {
|
|
455
|
+
const guardBlock = out.guard !== true ? null : [
|
|
456
|
+
`⛔ 安装被装后守卫拦截:${out.message ?? ''}`,
|
|
457
|
+
`终态:${out.compensation?.status ?? '—'}(${out.compensation?.note ?? ''})`,
|
|
458
|
+
out.repairBasis ? `修复依据:${out.repairBasis}` : null,
|
|
459
|
+
out.restartSafe === true ? '可以重启 DSH Web。' : '修复后再重启(不要现在一键重启)。',
|
|
460
|
+
'对用户如实说明守卫拦截与终态,不得说「已安装成功」或「已恢复原版本」。',
|
|
461
|
+
].filter(Boolean).join('\n');
|
|
462
|
+
if (out.guard === true)
|
|
463
|
+
return guardBlock;
|
|
366
464
|
const extra = buildsNote(out);
|
|
367
465
|
const compatNote = out.compat
|
|
368
466
|
? '注意:该版本与当前 DSH 运行时 peer 不兼容,已按用户确认强制安装。'
|
|
@@ -388,16 +486,22 @@ function renderUninstall(out) {
|
|
|
388
486
|
}
|
|
389
487
|
function renderOutdated(out) {
|
|
390
488
|
const outdated = (out.items || []).filter((it) => it.outdated);
|
|
489
|
+
const incomplete = (out.items || []).filter((it) => it.latestErrorCode);
|
|
391
490
|
if (!out.items?.length)
|
|
392
491
|
return 'web profile 没有已装插件。';
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
492
|
+
const head = [];
|
|
493
|
+
if (outdated.length) {
|
|
494
|
+
head.push(`${outdated.length}/${out.items.length} 个插件可升级:`, outdated.map((it) => `${it.name} (${it.pkg}):v${it.version} → ${it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : '最新')}`).join('\n'));
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
head.push(`全部 ${out.items.length} 个插件均已是最新版本。`);
|
|
498
|
+
}
|
|
499
|
+
// 禁止「未完成检查」冒充「全部最新」(Task 7 ⑨):按 code 安全化原因列出
|
|
500
|
+
if (incomplete.length) {
|
|
501
|
+
head.push(`${incomplete.length} 项检查未完成:`, incomplete.map((it) => `· ${it.name} (${it.pkg})——${latestErrorCodeReason(it.latestErrorCode)}`).join('\n'));
|
|
502
|
+
}
|
|
503
|
+
head.push('询问用户要升级哪个,确认后调 dshm_upgrade(pkg)。');
|
|
504
|
+
return head.join('\n');
|
|
401
505
|
}
|
|
402
506
|
function renderUpgrade(out) {
|
|
403
507
|
const from = out.fromVersion ? `v${out.fromVersion} → ` : '';
|