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/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 first-level category: ${Object.keys(CATEGORY_LABELS).join(', ')}`,
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: timeoutMs + 5000,
60
- async execute(args) {
61
- const category = typeof args.category === 'string' && args.category ? args.category : null;
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: timeoutMs + 5000,
117
- async execute() {
118
- const result = await m.listInstalledWithMeta(cfg, { namespace: 'host' });
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
- card: 'generic',
151
- title: isError ? '安装失败' : `已安装 · ${meta?.pkg || ''}`,
152
- content: [],
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
- return cloneJson(await m.installFromRegistry(id, cfg, { version, forceIncompatible: force, namespace: 'host' }));
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
- return { card: 'generic', title: isError ? '检查失败' : n ? `${n} 个可升级` : '全部最新', content: [] };
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: timeoutMs + 10_000,
237
- async execute() {
238
- const result = await m.listInstalledWithMeta(cfg, { namespace: 'host' });
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
- card: 'generic',
269
- title: isError ? '升级失败' : `已升级 · ${meta?.pkg || ''}`,
270
- content: [],
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
- return cloneJson(await m.upgradePlugin(target, cfg, { namespace: 'host' }));
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
- if (!outdated.length)
394
- return `全部 ${out.items.length} 个插件均已是最新版本。对用户一句短话。`;
395
- const lines = outdated.map((it) => `${it.name} (${it.pkg}):v${it.version} → ${it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : '最新')}`);
396
- return [
397
- `${outdated.length}/${out.items.length} 个插件可升级:`,
398
- lines.join('\n'),
399
- '询问用户要升级哪个,确认后调 dshm_upgrade(pkg)。',
400
- ].join('\n');
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} → ` : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",