dsh-m 0.9.91 → 0.9.92
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/CHANGELOG.md +12 -0
- package/lib/cli.js +15 -28
- package/lib/core/community-adapter.js +3 -3
- package/lib/core/host-api.js +15 -26
- package/lib/core/market-query.js +70 -0
- package/lib/tools.js +12 -39
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.92 修复+重构:三端市场查询文法统一(market-query 单一入口)——CLI 非法 category 吞错修复
|
|
12
|
+
|
|
13
|
+
- **需求(2026-10-16 实测)**:`dshm search --category <非法>` 静默返回「没有匹配的收录条目」+ exit 0,而同输入 GUI 得 400、agent 工具显式报错——「合并市场」词条承诺的「三端同源同语义」被 CLI 打破;根因是分类 slug 正则、source/offset/limit 解析、精选标签表在三端各持一份拷贝(正则 ×3、解析 ×3、标签表 ×2、deadline 常量 ×2),CLI 的拷贝漏了 category 校验。
|
|
14
|
+
- **方案**:新模块 `src/core/market-query.ts`(GLOSSARY「市场查询文法」)作为三端共享文法单一事实源——单一入口 `parseMarketQueryInput(raw, policy)` 吃下 category(精选五桶 ∪ 社区 slug,trim 后校验与透传)/source(三值枚举)/limit(floor 先行 + 各端 policy:GUI 96/32、tools/CLI 80/10)/offset,文法违规 throw `MarketQueryError`(消息逐字节沿用既有文案),各端一行信封映射(host-api 400 / tools throw 直飞 / CLI 经 dispatch 统一 catch `错误:` + exit 1);顺手单源化 slug 正则(含目录适配层第 3 份)、精选标签表(以 registry `CATEGORIES` 为键集基准 + 漂移守卫测试)、deadline 常量;CLI help 的 category 列表同步为现行策展五桶(旧值为 0.9.16 前残留)。自觉差异注记:① tools limit 的 (1,80) 区间小数今日由 core `clampLimit` 隐式 floor,改后在入口显式 floor(floor 位置前移,值域逐点相同,输出对象不含 limit 字段);② CLI category/source 改为 trim 后校验(对齐 host-api/tools,`' self-dev '` 今日匹配不到任何条目、`--source ' all '` 今日报非法,改后均按 trim 值正常判定);③ 双违例请求报错归属顺序统一 category → source(单违例逐字节不变)。
|
|
15
|
+
- **验证**:全量 1526 测试全绿(新增 market-query 文法矩阵 18 例 + 标签键集 ≡ CATEGORIES 漂移守卫 + CLI 非法 category/非法 source 回归钉例;既有「非法分类/非法 source」锚零迁移)、typecheck 零错误;真机 `node lib/cli.js search --category '非法分类!!'` → stderr `错误:非法分类: 非法分类!!(需精选分类或 [a-z0-9-]{1,32} slug)` + exit 1(校验先于任何网络调用);实施计划与 grill 十决议、独立评审三轮 15 项收敛记录在 `docs/plans/2026-10-16-market-query-intake-implementation-plan.md`。
|
|
16
|
+
|
|
11
17
|
### 0.9.91 优化:设置页三卡行 4:7:3 → 4:7:4——「社区清单」与「关于」等宽
|
|
12
18
|
|
|
13
19
|
- **需求(2026-10-16,主人真机反馈)**:宽屏/全屏下设置页三卡行左右不对称——「社区清单」(4fr)与「关于」(3fr)宽度不一致,视觉失衡。
|
|
@@ -643,6 +649,12 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
643
649
|
|
|
644
650
|
## English
|
|
645
651
|
|
|
652
|
+
### Fix + refactor in 0.9.92 — unified three-end market query intake (market-query single entry); CLI invalid-category silent swallow fixed
|
|
653
|
+
|
|
654
|
+
- **Need (2026-10-16, reproduced live)**: `dshm search --category <invalid>` silently printed "no matching entries" and exited 0, while the same input gets a 400 from the panel API and an explicit error from the agent tool — breaking the "same source, same semantics across GUI / tools / CLI" promise. Root cause: the category slug regex, source/offset/limit parsing, and the curated label map each had per-end copies (regex ×3, parsing ×3, labels ×2, deadline constants ×2), and the CLI copy lacked category validation.
|
|
655
|
+
- **Approach**: new module `src/core/market-query.ts` (GLOSSARY "Market Query Intake") is the single source of the shared grammar — one entry point `parseMarketQueryInput(raw, policy)` covers category (curated five ∪ community slug, validate-and-pass trimmed), source (three-value enum), limit (floor-first + per-end policy: GUI 96/32, tools/CLI 80/10), and offset; grammar violations throw `MarketQueryError` with byte-identical legacy messages, and each end maps to its own envelope (HTTP 400 / thrown tool error / CLI dispatch catch `错误:` + exit 1). Also single-sourced: the slug regex (including the catalog adapter's third copy), the curated label map (keyed off registry `CATEGORIES` with a drift-guard test), and the deadline constants; the CLI help category list now shows the current curated five (the old list was a pre-0.9.16 leftover). Deliberate deltas: (1) tools' in-range fractional limits were implicitly floored by core `clampLimit`; the floor now happens explicitly at intake (same values pointwise; output objects carry no limit field); (2) CLI category/source are now validated after trimming, matching host-api/tools (`' self-dev '` used to match nothing and `--source ' all '` used to error); (3) dual-violation requests now report category before source (single-violation messages byte-identical).
|
|
656
|
+
- **Verification**: full suite 1526 green (18 new grammar-matrix cases + label-keys ≡ CATEGORIES drift guard + CLI invalid-category/invalid-source regression pins; existing "非法分类/非法 source" anchors migrated zero), typecheck clean; live `node lib/cli.js search --category '非法分类!!'` → stderr `错误:非法分类: 非法分类!!(需精选分类或 [a-z0-9]{1,32} slug)` + exit 1 (validation precedes any network call); plan, grill decisions, and the three-round 15-item independent review live in `docs/plans/2026-10-16-market-query-intake-implementation-plan.md`.
|
|
657
|
+
|
|
646
658
|
### Polish in 0.9.91 — settings three-card row 4:7:3 → 4:7:4: Community and About equal width
|
|
647
659
|
|
|
648
660
|
- **Need (2026-10-16, live feedback)**: on wide/fullscreen the settings three-card row was asymmetric — Community (4fr) and About (3fr) had different widths.
|
package/lib/cli.js
CHANGED
|
@@ -11,21 +11,15 @@ import { runDoctor } from './core/doctor.js';
|
|
|
11
11
|
import { readLauncherPackageVersion } from './core/dsh-version.js';
|
|
12
12
|
import { webProfileDir, desktopProfileDir } from './core/env.js';
|
|
13
13
|
import { loadRegistry } from './core/registry.js';
|
|
14
|
+
import { parseMarketQueryInput, PRIMARY_CATEGORY_LABELS, SEARCH_CORE_DEADLINE_MS, INSTALLED_CORE_DEADLINE_MS } from './core/market-query.js';
|
|
14
15
|
import { COMMUNITY_CATEGORY_LABELS } from './core/community.js';
|
|
15
16
|
import { scheduleRestart } from './core/restart.js';
|
|
16
17
|
import { upgradeEffectLine } from './core/activation.js';
|
|
17
18
|
import { pathToFileURL } from 'node:url';
|
|
18
19
|
import { realpathSync, existsSync } from 'node:fs';
|
|
19
|
-
/** 分类中文标签(0.
|
|
20
|
-
const CLI_CATEGORY_LABELS = {
|
|
21
|
-
essentials: '装机必备',
|
|
22
|
-
'cui-picks': '崔添翼精选',
|
|
23
|
-
'self-dev': 'iasi自研',
|
|
24
|
-
'tencent-lighthouse': '腾讯轻量云专区',
|
|
25
|
-
watchlist: '观察区',
|
|
26
|
-
};
|
|
20
|
+
/** 分类中文标签(0.9.16 策展五桶):精选桶单一事实源 market-query PRIMARY_CATEGORY_LABELS + 社区已知标签单一事实源;未知 slug 原样。 */
|
|
27
21
|
function categoryLabelOf(category) {
|
|
28
|
-
return
|
|
22
|
+
return PRIMARY_CATEGORY_LABELS[category] ?? COMMUNITY_CATEGORY_LABELS[category] ?? category;
|
|
29
23
|
}
|
|
30
24
|
function parseArgs(argv) {
|
|
31
25
|
const cmd = argv[0] || 'help';
|
|
@@ -59,9 +53,7 @@ function cliConfig() {
|
|
|
59
53
|
communityCatalogPin: process.env.DSHM_COMMUNITY_CATALOG_PIN || undefined,
|
|
60
54
|
};
|
|
61
55
|
}
|
|
62
|
-
/** 搜索工具 deadline 对齐(Task 7):CLI 无请求 signal
|
|
63
|
-
const SEARCH_CORE_DEADLINE_MS = 44_000;
|
|
64
|
-
const INSTALLED_CORE_DEADLINE_MS = 60_000;
|
|
56
|
+
/** 搜索工具 deadline 对齐(Task 7):CLI 无请求 signal,两个 core deadline 常量单源于 market-query(0.9.92,与工具侧共用)。 */
|
|
65
57
|
/** latestErrorCode → 安全化原因(与 tools.ts 同表;CLI 终端呈现)。 */
|
|
66
58
|
function latestErrorCodeReason(code) {
|
|
67
59
|
switch (code) {
|
|
@@ -131,7 +123,7 @@ const HELP = `dshm — DSH Marketplace(DSH 插件市场:精选策展 + 社
|
|
|
131
123
|
用法:dshm <命令> [参数]
|
|
132
124
|
|
|
133
125
|
只读命令:
|
|
134
|
-
dshm search [关键词] [--query 关键词] [--category
|
|
126
|
+
dshm search [关键词] [--query 关键词] [--category essentials|cui-picks|self-dev|tencent-lighthouse|watchlist|<社区slug>] [--source primary|community|all] [--limit N] [--offset N]
|
|
135
127
|
(首个位置参数等价 --query,--query 缺值时也回退它,多余位置参数忽略;source 分区:community=社区目录 4000+ 条 / primary=精选策展 / all=默认;limit 默认 10;offset 翻页)
|
|
136
128
|
dshm list 列出 web profile 已装插件(含市场标注/可升级)
|
|
137
129
|
dshm outdated 检查已装插件的最新版本
|
|
@@ -198,24 +190,18 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
198
190
|
out(HELP);
|
|
199
191
|
return 0;
|
|
200
192
|
case 'search': {
|
|
201
|
-
// metadata-only:不构造全量 latest;
|
|
193
|
+
// metadata-only:不构造全量 latest;0.9.92 三端查询文法:category/source/limit/offset 经
|
|
194
|
+
// market-query 单一入口归一(与 host-api/工具同源同语义);文法违规 throw 直飞 dispatch 统一
|
|
195
|
+
// catch(`错误:${msg}` + exit 1,与此前 source 内联报错输出逐字节同款)——非法 category
|
|
196
|
+
// 自此显式报错(0.9.91 前静默空结果吞错,回归钉例在 cli-search.test.mjs)。
|
|
202
197
|
// 双源判定:主清单 unavailable 但社区有条目 → 照常出页(Q42);两层皆不可用 → exit 1
|
|
203
|
-
const
|
|
204
|
-
if (sourceRaw !== '' && sourceRaw !== 'primary' && sourceRaw !== 'community' && sourceRaw !== 'all') {
|
|
205
|
-
err(`错误:非法 source: ${sourceRaw}(需 primary/community/all)`);
|
|
206
|
-
return 1;
|
|
207
|
-
}
|
|
208
|
-
const source = sourceRaw === '' ? 'all' : sourceRaw;
|
|
209
|
-
const limitRaw = Number(flags.limit);
|
|
210
|
-
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(80, Math.max(1, Math.floor(limitRaw))) : 10;
|
|
211
|
-
const offsetRaw = Number(flags.offset);
|
|
212
|
-
const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? Math.floor(offsetRaw) : 0;
|
|
198
|
+
const intake = parseMarketQueryInput({ category: flags.category, source: flags.source, limit: flags.limit, offset: flags.offset }, { limitMax: 80, limitDefault: 10 });
|
|
213
199
|
const result = await d.listMarket(cfg, {
|
|
214
200
|
query: typeof flags.query === 'string' ? flags.query : positionals[0],
|
|
215
|
-
category:
|
|
216
|
-
source,
|
|
217
|
-
offset,
|
|
218
|
-
limit,
|
|
201
|
+
category: intake.category,
|
|
202
|
+
source: intake.source,
|
|
203
|
+
offset: intake.offset,
|
|
204
|
+
limit: intake.limit,
|
|
219
205
|
withLatest: false,
|
|
220
206
|
namespace: 'cli',
|
|
221
207
|
deadlineMs: SEARCH_CORE_DEADLINE_MS,
|
|
@@ -241,6 +227,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
241
227
|
out(` ${it.description}`);
|
|
242
228
|
}
|
|
243
229
|
const shown = result.items.length;
|
|
230
|
+
const offset = intake.offset;
|
|
244
231
|
const nextOffset = offset + shown < result.total ? offset + shown : null;
|
|
245
232
|
const s = result.registryState;
|
|
246
233
|
out(`\n来源:${registrySourceLabel(s.source)}${s.stale ? '(缓存)' : ''} · 更新:${s.fetchedAt ?? '—'} · 共 ${result.total} 条`);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 本体,仅随 CommunityEntry 透传、由市场层展示(Q44:卡片不打标,详情折叠区才展示)。
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
+
import { COMMUNITY_SLUG_RE } from './market-query.js';
|
|
10
11
|
/** 与主清单 v1 同款约束(registry.ts MAX_ID / ID_RE / NPM_RE / GITHUB_RE 同语义,锚定不漂移)。 */
|
|
11
12
|
const MAX_ID = 64;
|
|
12
13
|
const MAX_DESCRIPTION = 500;
|
|
@@ -14,8 +15,6 @@ const ID_ILLEGAL_RE = /[^a-z0-9._-]+/g;
|
|
|
14
15
|
const ID_LEADING_ILLEGAL_RE = /^[._-]+/;
|
|
15
16
|
const NPM_SHAPE_RE = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
16
17
|
const GITHUB_SHAPE_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
|
|
17
|
-
/** 社区开放分类的安全 slug(计划 Task 4 契约;原生值保留不转译,Q40)。 */
|
|
18
|
-
const COMMUNITY_CATEGORY_RE = /^[a-z0-9-]{1,32}$/;
|
|
19
18
|
const HASH_SUFFIX_LEN = 8;
|
|
20
19
|
const TRUNCATED_BODY_LEN = MAX_ID - (HASH_SUFFIX_LEN + 1);
|
|
21
20
|
function isBlank(v) {
|
|
@@ -109,7 +108,8 @@ export function adaptCommunityCatalog(catalog) {
|
|
|
109
108
|
}
|
|
110
109
|
const name = raw.name.trim();
|
|
111
110
|
const owner = raw.owner.trim();
|
|
112
|
-
|
|
111
|
+
// 社区分类安全 slug:正则单源于 market-query(0.9.92),与三端查询文法同一文法规则
|
|
112
|
+
if (typeof raw.category !== 'string' || !COMMUNITY_SLUG_RE.test(raw.category)) {
|
|
113
113
|
skippedDirty += 1;
|
|
114
114
|
continue;
|
|
115
115
|
}
|
package/lib/core/host-api.js
CHANGED
|
@@ -19,7 +19,7 @@ import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
|
19
19
|
import { getCommunitySummary } from './community.js';
|
|
20
20
|
import { RegistryConfigError } from './registry-controller.js';
|
|
21
21
|
import { checkRegistryEntries } from './registry-check.js';
|
|
22
|
-
import {
|
|
22
|
+
import { parseMarketQueryInput, MarketQueryError } from './market-query.js';
|
|
23
23
|
import { servingPort, scheduleRestart } from './restart.js';
|
|
24
24
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
25
25
|
import { IncompatibleError } from './compat-check.js';
|
|
@@ -39,8 +39,6 @@ class ApiProtocolError extends Error {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
const BODY_MAX_BYTES = 1 << 20;
|
|
42
|
-
/** 社区开放分类安全 slug(计划 Task 6;与适配层 COMMUNITY_CATEGORY_RE 同语义) */
|
|
43
|
-
const COMMUNITY_SLUG_RE = /^[a-z0-9-]{1,32}$/;
|
|
44
42
|
function readBody(req, maxBytes = BODY_MAX_BYTES) {
|
|
45
43
|
return new Promise((resolve, reject) => {
|
|
46
44
|
const chunks = [];
|
|
@@ -363,30 +361,21 @@ export function createApiDispatcher(ctx) {
|
|
|
363
361
|
// GUI policy:忽略客户端 withLatest,固定 true;limit clamp 1..96、缺省 32
|
|
364
362
|
// (0.7.0 Task 7 探测预算决策:96 为 opt-in 页大小,0.9.28 起默认 32 仍低于 0.6.x 默认 50 的探测负载;
|
|
365
363
|
// 最坏情况被 core 60s deadline 框死为 latestError,不阻塞列表,Q46 不动)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
throw new ApiProtocolError(400, `非法分类: ${categoryRaw}(需精选分类或 [a-z0-9-]{1,32} slug)`);
|
|
379
|
-
}
|
|
364
|
+
// 0.9.92 三端查询文法:category/source/limit/offset 经 market-query 单一入口归一(与 tools/CLI
|
|
365
|
+
// 同源同语义);双违例顺序统一 category → source → 端本地参数(红线 #6 注记)。
|
|
366
|
+
let intake;
|
|
367
|
+
try {
|
|
368
|
+
intake = parseMarketQueryInput({ category: body.category, source: body.source, limit: body.limit, offset: body.offset }, { limitMax: 96, limitDefault: 32 });
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
if (e instanceof MarketQueryError)
|
|
372
|
+
throw new ApiProtocolError(400, e.message);
|
|
373
|
+
throw e;
|
|
380
374
|
}
|
|
381
375
|
// source 分区 + sort(0.7.0 Task 7):非法值 400(不静默吞);primaryOnly 已删除——显式拒绝而非静默忽略(审计 #8)
|
|
382
376
|
if (body.primaryOnly !== undefined) {
|
|
383
377
|
throw new ApiProtocolError(400, '参数 primaryOnly 已删除:改用 source(primary/community/all)');
|
|
384
378
|
}
|
|
385
|
-
const sourceRaw = typeof body.source === 'string' ? body.source.trim() : '';
|
|
386
|
-
if (sourceRaw !== '' && sourceRaw !== 'primary' && sourceRaw !== 'community' && sourceRaw !== 'all') {
|
|
387
|
-
throw new ApiProtocolError(400, `非法 source: ${sourceRaw}(需 primary/community/all)`);
|
|
388
|
-
}
|
|
389
|
-
const source = sourceRaw === '' ? 'all' : sourceRaw;
|
|
390
379
|
let sort;
|
|
391
380
|
if (body.sort !== undefined && body.sort !== null) {
|
|
392
381
|
const s = body.sort;
|
|
@@ -405,14 +394,14 @@ export function createApiDispatcher(ctx) {
|
|
|
405
394
|
}
|
|
406
395
|
const result = await d.listMarket(cfg(), {
|
|
407
396
|
query: strArg(body, 'query'),
|
|
408
|
-
category,
|
|
409
|
-
source,
|
|
397
|
+
category: intake.category,
|
|
398
|
+
source: intake.source,
|
|
410
399
|
sort,
|
|
411
400
|
// 0.9.26:GUI 跨区搜索精选稳定前置(listMarket 内部仅在 source='all' 且 query 非空时生效)。
|
|
412
401
|
// 本参数仅 GUI 通道携带——tools/CLI 走各自调用面不传,搜索排序三端同序不变。
|
|
413
402
|
curatedFirst: true,
|
|
414
|
-
offset,
|
|
415
|
-
limit,
|
|
403
|
+
offset: intake.offset,
|
|
404
|
+
limit: intake.limit,
|
|
416
405
|
force: boolArg(body.force),
|
|
417
406
|
withLatest: true,
|
|
418
407
|
probeMode: probeModeRaw,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 市场查询文法(GLOSSARY「市场查询文法」):三端(面板 API / agent 工具 / CLI)市场查询
|
|
3
|
+
* 参数的共享校验与归一模块——「合并市场」条目「三端同源同语义」承诺的执行机制。
|
|
4
|
+
*
|
|
5
|
+
* 单一入口 parseMarketQueryInput 一次吃下 {category, source, limit, offset},文法违规
|
|
6
|
+
* throw MarketQueryError(消息即契约,逐字节沿用 host-api/tools 既有文案,测试锚零迁移);
|
|
7
|
+
* 各端 policy(limitMax / limitDefault)表达 channel 差异(GUI 96/32,tools/CLI 80/10)。
|
|
8
|
+
*
|
|
9
|
+
* 环纪律:本模块 import 白名单仅 ./registry.js——community → merged-market →
|
|
10
|
+
* community-adapter 已是值链,adapter 接入本模块后再值引用 community 即成环。
|
|
11
|
+
* categoryLabelOf 的社区段合并(?? COMMUNITY_CATEGORY_LABELS[cat])因此留在 tools/cli 各一行。
|
|
12
|
+
*
|
|
13
|
+
* 不入本模块(grill D1):sort/probeMode/force/primaryOnly(仅 GUI 暴露)、query 字符串
|
|
14
|
+
* (三端语义不同:host-api strArg 会 trim、tools 不 trim、cli 位置参数合并在前)。
|
|
15
|
+
*/
|
|
16
|
+
import { CATEGORIES } from './registry.js';
|
|
17
|
+
/** 社区分类 slug 文法(单一事实源;host-api/tools/cli/community-adapter 共用)。 */
|
|
18
|
+
export const COMMUNITY_SLUG_RE = /^[a-z0-9-]{1,32}$/;
|
|
19
|
+
/** 精选五桶中文标签(键集 ≡ registry CATEGORIES,market-query.test.mjs 漂移守卫钉住)。 */
|
|
20
|
+
export const PRIMARY_CATEGORY_LABELS = {
|
|
21
|
+
essentials: '装机必备',
|
|
22
|
+
'cui-picks': '崔添翼精选',
|
|
23
|
+
'self-dev': 'iasi自研',
|
|
24
|
+
'tencent-lighthouse': '腾讯轻量云专区',
|
|
25
|
+
watchlist: '观察区',
|
|
26
|
+
};
|
|
27
|
+
/** 三端查询接线策略常量(原 tools.ts / cli.ts 双份字面量收敛;0.9.92 起单源)。 */
|
|
28
|
+
export const SEARCH_CORE_DEADLINE_MS = 44_000;
|
|
29
|
+
export const INSTALLED_CORE_DEADLINE_MS = 60_000;
|
|
30
|
+
/** 文法违规错误:消息即契约(各端映射自身信封:400 / throw 直飞 / stderr+exit 1),无 code 字段(YAGNI)。 */
|
|
31
|
+
export class MarketQueryError extends Error {
|
|
32
|
+
constructor(message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'MarketQueryError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 归一文法(错误检查顺序 category → source,红线 #6 双违例注记):
|
|
39
|
+
* - category:非字符串或 trim 后为空 → null;精选五桶(registry CATEGORIES)∪ 社区 slug → trim 后值;
|
|
40
|
+
* 否则 throw `非法分类: …`
|
|
41
|
+
* - source:非字符串或 trim 后为空 → 'all';primary/community/all → trim 后值(勿返回未 trim 原值);
|
|
42
|
+
* 否则 throw `非法 source: …`
|
|
43
|
+
* - limit:Number() 后 Number.isFinite && > 0 ? floor 先行钳 [1, limitMax] : limitDefault
|
|
44
|
+
* (floor 发生位置从 core clampLimit 前移到入口,值域结果逐点相同——红线 #3)
|
|
45
|
+
* - offset:Number() 后 Number.isFinite && > 0 ? Math.floor : 0
|
|
46
|
+
*/
|
|
47
|
+
export function parseMarketQueryInput(raw, policy) {
|
|
48
|
+
const categoryRaw = typeof raw.category === 'string' ? raw.category.trim() : '';
|
|
49
|
+
let category = null;
|
|
50
|
+
if (categoryRaw !== '') {
|
|
51
|
+
if (CATEGORIES.includes(categoryRaw) || COMMUNITY_SLUG_RE.test(categoryRaw)) {
|
|
52
|
+
category = categoryRaw;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
throw new MarketQueryError(`非法分类: ${categoryRaw}(需精选分类或 [a-z0-9-]{1,32} slug)`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const sourceRaw = typeof raw.source === 'string' ? raw.source.trim() : '';
|
|
59
|
+
if (sourceRaw !== '' && sourceRaw !== 'primary' && sourceRaw !== 'community' && sourceRaw !== 'all') {
|
|
60
|
+
throw new MarketQueryError(`非法 source: ${sourceRaw}(需 primary/community/all)`);
|
|
61
|
+
}
|
|
62
|
+
const source = sourceRaw === '' ? 'all' : sourceRaw;
|
|
63
|
+
const rawLimit = Number(raw.limit);
|
|
64
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
65
|
+
? Math.min(policy.limitMax, Math.max(1, Math.floor(rawLimit)))
|
|
66
|
+
: policy.limitDefault;
|
|
67
|
+
const rawOffset = Number(raw.offset);
|
|
68
|
+
const offset = Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;
|
|
69
|
+
return { category, source, limit, offset };
|
|
70
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -2,30 +2,19 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
2
2
|
import { installTimeoutMs } from './core/env.js';
|
|
3
3
|
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, InstallGuardError, } from './core/market.js';
|
|
4
4
|
import { COMMUNITY_CATEGORY_LABELS } from './core/community.js';
|
|
5
|
+
import { parseMarketQueryInput, PRIMARY_CATEGORY_LABELS, SEARCH_CORE_DEADLINE_MS, INSTALLED_CORE_DEADLINE_MS } from './core/market-query.js';
|
|
5
6
|
import { appExitFromContext, scheduleRestart } from './core/restart.js';
|
|
6
7
|
import { togglePlugin as coreTogglePlugin } from './core/toggle.js';
|
|
7
8
|
import { resolveActiveProfile, assertWriteAllowed } from './core/active-profile.js';
|
|
8
9
|
import { desktopInstallFromRegistry, desktopToggle, desktopUninstall, desktopUpgradeFromRegistry } from './core/profile-ops.js';
|
|
9
|
-
|
|
10
|
-
essentials: '装机必备',
|
|
11
|
-
'cui-picks': '崔添翼精选',
|
|
12
|
-
'self-dev': 'iasi自研',
|
|
13
|
-
'tencent-lighthouse': '腾讯轻量云专区',
|
|
14
|
-
watchlist: '观察区',
|
|
15
|
-
};
|
|
16
|
-
/** 分类中文标签(0.7.0 Task 5):精选 5 类 + 社区已知标签(单一事实源 COMMUNITY_CATEGORY_LABELS);未知 slug 原样。 */
|
|
10
|
+
/** 分类中文标签(0.7.0 Task 5):精选 5 类(单一事实源 market-query PRIMARY_CATEGORY_LABELS)+ 社区已知标签(单一事实源 COMMUNITY_CATEGORY_LABELS);未知 slug 原样。 */
|
|
17
11
|
function categoryLabelOf(category) {
|
|
18
|
-
return
|
|
19
|
-
COMMUNITY_CATEGORY_LABELS[category] ??
|
|
20
|
-
category);
|
|
12
|
+
return PRIMARY_CATEGORY_LABELS[category] ?? COMMUNITY_CATEGORY_LABELS[category] ?? category;
|
|
21
13
|
}
|
|
22
|
-
/** 工具 deadline 对齐(M1 Task 7):search 45s(core 44s)、list/outdated 65s(core 60s + 5s 回包余量)。
|
|
14
|
+
/** 工具 deadline 对齐(M1 Task 7):search 45s(core 44s)、list/outdated 65s(core 60s + 5s 回包余量)。
|
|
15
|
+
* core 侧两个 deadline 常量单源于 market-query(0.9.92,与 CLI 共用);TOOL_TIMEOUT 为本端信封专属。 */
|
|
23
16
|
const SEARCH_TOOL_TIMEOUT_MS = 45_000;
|
|
24
|
-
const SEARCH_CORE_DEADLINE_MS = 44_000;
|
|
25
17
|
const INSTALLED_TOOL_TIMEOUT_MS = 65_000;
|
|
26
|
-
const INSTALLED_CORE_DEADLINE_MS = 60_000;
|
|
27
|
-
/** 社区开放分类安全 slug(与 host-api 侧同语义) */
|
|
28
|
-
const COMMUNITY_SLUG_RE = /^[a-z0-9-]{1,32}$/;
|
|
29
18
|
/** 社区 summary 四键投影(Task 7 ②):agent 工具输出只带这四个字段。 */
|
|
30
19
|
export function communityToolSummary(c) {
|
|
31
20
|
if (!c)
|
|
@@ -83,7 +72,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
83
72
|
query: { type: 'string', description: 'Main keyword, e.g. 主题 or 搜索. Optional; omit to browse.' },
|
|
84
73
|
category: {
|
|
85
74
|
type: 'string',
|
|
86
|
-
description: `Optional category: curated ${Object.keys(
|
|
75
|
+
description: `Optional category: curated ${Object.keys(PRIMARY_CATEGORY_LABELS).join('/')} or any community slug ([a-z0-9-]{1,32}, e.g. theme/memory/git).`,
|
|
87
76
|
},
|
|
88
77
|
source: {
|
|
89
78
|
type: 'string',
|
|
@@ -110,24 +99,11 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
110
99
|
}),
|
|
111
100
|
timeoutMs: SEARCH_TOOL_TIMEOUT_MS,
|
|
112
101
|
async execute(args, exec) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
else
|
|
119
|
-
throw new Error(`非法分类: ${categoryRaw}(需精选分类或 [a-z0-9-]{1,32} slug)`);
|
|
120
|
-
}
|
|
121
|
-
// 分区参数(0.7.0 Task 5):与 GUI 分区对齐;primary_only 已删除(D6a 允许破坏性变更)
|
|
122
|
-
const sourceRaw = typeof args.source === 'string' ? args.source.trim() : '';
|
|
123
|
-
if (sourceRaw !== '' && sourceRaw !== 'primary' && sourceRaw !== 'community' && sourceRaw !== 'all') {
|
|
124
|
-
throw new Error(`非法 source: ${sourceRaw}(需 primary/community/all)`);
|
|
125
|
-
}
|
|
126
|
-
const source = sourceRaw === '' ? 'all' : sourceRaw;
|
|
127
|
-
const rawLimit = Number(args.limit);
|
|
128
|
-
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? clamp(rawLimit, 1, 80) : 10;
|
|
129
|
-
const rawOffset = Number(args.offset);
|
|
130
|
-
const offset = Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;
|
|
102
|
+
// 0.9.92 三端查询文法:category/source/limit/offset 经 market-query 单一入口归一
|
|
103
|
+
//(与 host-api/CLI 同源同语义;文法违规 throw 直飞,与此前 throw new Error 同通道)。
|
|
104
|
+
// 分区参数语义 0.7.0 Task 5 不变;primary_only 已删除(D6a 允许破坏性变更)。
|
|
105
|
+
const intake = parseMarketQueryInput({ category: args.category, source: args.source, limit: args.limit, offset: args.offset }, { limitMax: 80, limitDefault: 10 });
|
|
106
|
+
const { category, source, offset, limit } = intake;
|
|
131
107
|
// metadata-only:agent 卡片不需要 latest;与 Host GUI 共用 host namespace;
|
|
132
108
|
// deadline 44s 是本 waiter 的绝对上限(工具 timeout 45s 先到兜底)
|
|
133
109
|
const result = await m.listMarket(cfg, {
|
|
@@ -464,7 +440,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
464
440
|
order: 211,
|
|
465
441
|
text: [
|
|
466
442
|
'Finding / recommending / browsing DSH plugins (插件) in the personal marketplace: you MUST call dshm_search, never web_search or bash. One call per user message; extract a real keyword. After cards appear, reply with AT MOST one short sentence. Do not print install commands.',
|
|
467
|
-
`Plugin categories: ${Object.entries(
|
|
443
|
+
`Plugin categories: ${Object.entries(PRIMARY_CATEGORY_LABELS).map(([k, v]) => `${k}=${v}`).join(', ')}.`,
|
|
468
444
|
'Install only after the user names a card: dshm_install with its id. Then one short sentence mentioning the restart requirement; offer dshm_restart.',
|
|
469
445
|
'For installed plugins, call dshm_list / dshm_outdated. Upgrade only after the user confirms which one: dshm_upgrade. Uninstall only after confirmation: dshm_uninstall.',
|
|
470
446
|
'dshm_toggle switches a plugin on/off: confirm first unless the user already said it in the same message (把 skins 关掉 → execute directly); report live vs restart-required accordingly. ',
|
|
@@ -603,6 +579,3 @@ export function renderUpgrade(out) {
|
|
|
603
579
|
: '需要重启生效——询问是否 dshm_restart。';
|
|
604
580
|
return `✅ ${out.pkg} 已升级(${from}${to})。${extra}${tail}`;
|
|
605
581
|
}
|
|
606
|
-
function clamp(n, min, max) {
|
|
607
|
-
return Math.min(max, Math.max(min, n));
|
|
608
|
-
}
|