dsh-m 0.9.91 → 0.9.93
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 +24 -0
- package/lib/cli.js +17 -30
- package/lib/core/community-adapter.js +3 -3
- package/lib/core/host-api.js +18 -33
- package/lib/core/market-query.js +70 -0
- package/lib/core/market.js +15 -0
- package/lib/tools.js +14 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.93 重构:装后守卫线格式收敛至 InstallGuardError.toWire——加守卫字段编辑点 8 → 2(纯重构,线格式语义零变化)
|
|
12
|
+
|
|
13
|
+
- **需求(2026-10-16 架构巡查摩擦点 3,巡查首推)**:`InstallGuardError` → 线格式的投影在手写 5 处逐字重复(host-api 409 payload / tools install+upgrade 两臂 / cli install+upgrade 两臂)+ client 再拼一次——加一个守卫字段要碰 8 处(类 + 5 投影 + client 读取 + 测试),`repairBasis` 后加时各点手补已实证过一次复发。
|
|
14
|
+
- **方案**:产地方法 `toWire(): InstallGuardWire`(随类导出接口 + `GuardViolation` re-export)成为 6 字段共享线格式单一事实源;五处投影改一行展开(信封差异留在各端:host-api `ok/error`、tools `ok/guard/message`、cli 裸 JSON),`repairBasis` undefined 时键不存在的语义移入方法本体。加字段编辑点降为 2(类+toWire 一处、client 读取一处——浏览器边界不能 import core,防御性拼装留守,如实注记)。自觉差异注记:tools 信封键序 kind/message 交换(JSON 键序对消费方无语义,全 tests/ 无原始 JSON 键序钉)。
|
|
15
|
+
- **验证**:全量 1530 测试全绿(新增 toWire 三钉例:六字段齐/repairBasis 键缺席/键序;新增 host-api dispatcher 409 钉例补 host-api 投影零测试覆盖的缺口;tools 两臂 execute 钉例零迁移)、typecheck 零错误;收敛锚 `repairBasis ? { repairBasis: … }` 三文件 5 → 0(兼证无第 6 处投影);实施计划经两轮独立评审四项收敛 + 执行评审两轮(含 A1 钉例独立成 describe)记录在 `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`。
|
|
16
|
+
|
|
17
|
+
### 0.9.92 修复+重构:三端市场查询文法统一(market-query 单一入口)——CLI 非法 category 吞错修复
|
|
18
|
+
|
|
19
|
+
- **需求(2026-10-16 实测)**:`dshm search --category <非法>` 静默返回「没有匹配的收录条目」+ exit 0,而同输入 GUI 得 400、agent 工具显式报错——「合并市场」词条承诺的「三端同源同语义」被 CLI 打破;根因是分类 slug 正则、source/offset/limit 解析、精选标签表在三端各持一份拷贝(正则 ×3、解析 ×3、标签表 ×2、deadline 常量 ×2),CLI 的拷贝漏了 category 校验。
|
|
20
|
+
- **方案**:新模块 `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(单违例逐字节不变)。
|
|
21
|
+
- **验证**:全量 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`。
|
|
22
|
+
|
|
11
23
|
### 0.9.91 优化:设置页三卡行 4:7:3 → 4:7:4——「社区清单」与「关于」等宽
|
|
12
24
|
|
|
13
25
|
- **需求(2026-10-16,主人真机反馈)**:宽屏/全屏下设置页三卡行左右不对称——「社区清单」(4fr)与「关于」(3fr)宽度不一致,视觉失衡。
|
|
@@ -643,6 +655,18 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
643
655
|
|
|
644
656
|
## English
|
|
645
657
|
|
|
658
|
+
### Refactor in 0.9.93 — post-install guard wire format converged onto InstallGuardError.toWire; adding a guard field now touches 8 → 2 points (pure refactor, zero wire-format semantic change)
|
|
659
|
+
|
|
660
|
+
- **Need (2026-10-16 architecture walk, friction point 3, the walk's top pick)**: the `InstallGuardError` → wire-format projection was hand-written verbatim in 5 places (host-api 409 payload / tools install+upgrade arms / cli install+upgrade arms) plus one more assembly in the client — adding a guard field meant touching 8 points (class + 5 projections + client read + tests); the later addition of `repairBasis` had already demonstrated the recurrence once.
|
|
661
|
+
- **Approach**: a source method `toWire(): InstallGuardWire` (interface exported alongside the class, plus a `GuardViolation` re-export) becomes the single source of the shared 6-field wire format; the five projections become one-line spreads (envelope differences stay per-end: host-api `ok/error`, tools `ok/guard/message`, cli bare JSON), and the "repairBasis key absent when undefined" semantics move into the method body. Adding a field now touches 2 points (class+toWire, and the client read — the browser bundle cannot import core modules, so its defensive assembly stays, honestly noted). Deliberate delta: the tools envelope swaps key order between kind/message (JSON key order carries no consumer semantics; no raw-JSON key-order pins exist across tests/).
|
|
662
|
+
- **Verification**: full suite 1530 green (three new toWire pins: six fields / repairBasis key absence / key order; a new host-api dispatcher 409 pin closing the "host-api projection had zero test coverage" gap; tools-arm execute pins migrated zero), typecheck clean; the convergence anchor `repairBasis ? { repairBasis: … }` went 5 → 0 across the three files (also proving there is no 6th projection); plan review (two rounds, four findings) and execution review (two rounds, including A1 extracting the pin into its own describe) recorded in `docs/plans/2026-10-17-install-guard-towire-implementation-plan.md`.
|
|
663
|
+
|
|
664
|
+
### Fix + refactor in 0.9.92 — unified three-end market query intake (market-query single entry); CLI invalid-category silent swallow fixed
|
|
665
|
+
|
|
666
|
+
- **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.
|
|
667
|
+
- **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).
|
|
668
|
+
- **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`.
|
|
669
|
+
|
|
646
670
|
### Polish in 0.9.91 — settings three-card row 4:7:3 → 4:7:4: Community and About equal width
|
|
647
671
|
|
|
648
672
|
- **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} 条`);
|
|
@@ -345,7 +332,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
345
332
|
}
|
|
346
333
|
catch (e) {
|
|
347
334
|
if (e instanceof InstallGuardError) {
|
|
348
|
-
err('[⛔ 装后守卫拦截] ' + JSON.stringify(
|
|
335
|
+
err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
|
|
349
336
|
err(e.message);
|
|
350
337
|
err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
|
|
351
338
|
return 1;
|
|
@@ -370,7 +357,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
370
357
|
}
|
|
371
358
|
catch (e) {
|
|
372
359
|
if (e instanceof InstallGuardError) {
|
|
373
|
-
err('[⛔ 装后守卫拦截] ' + JSON.stringify(
|
|
360
|
+
err('[⛔ 装后守卫拦截] ' + JSON.stringify(e.toWire()));
|
|
374
361
|
err(e.message);
|
|
375
362
|
err(e.restartSafe ? '可以重启 DSH Web:dshm restart --yes' : '修复后再重启(不要现在重启)。');
|
|
376
363
|
return 1;
|
|
@@ -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 = [];
|
|
@@ -168,18 +166,14 @@ function errorStatus(err) {
|
|
|
168
166
|
return { status, payload: { ok: false, error: err.message, code: err.code } };
|
|
169
167
|
}
|
|
170
168
|
if (err instanceof InstallGuardError) {
|
|
171
|
-
// 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe
|
|
169
|
+
// 装后守卫拦截(M2 Task 3):结构化投影含 needsRestart/restartSafe/kind——GUI 一键重启只读 restartSafe;
|
|
170
|
+
// 0.9.93 起线格式单源于 toWire(信封 ok/error 本端自拼)
|
|
172
171
|
return {
|
|
173
172
|
status: 409,
|
|
174
173
|
payload: {
|
|
175
174
|
ok: false,
|
|
176
175
|
error: err.message,
|
|
177
|
-
|
|
178
|
-
violations: err.violations,
|
|
179
|
-
compensation: err.compensation,
|
|
180
|
-
needsRestart: err.needsRestart,
|
|
181
|
-
restartSafe: err.restartSafe,
|
|
182
|
-
...(err.repairBasis ? { repairBasis: err.repairBasis } : {}),
|
|
176
|
+
...err.toWire(),
|
|
183
177
|
},
|
|
184
178
|
};
|
|
185
179
|
}
|
|
@@ -363,30 +357,21 @@ export function createApiDispatcher(ctx) {
|
|
|
363
357
|
// GUI policy:忽略客户端 withLatest,固定 true;limit clamp 1..96、缺省 32
|
|
364
358
|
// (0.7.0 Task 7 探测预算决策:96 为 opt-in 页大小,0.9.28 起默认 32 仍低于 0.6.x 默认 50 的探测负载;
|
|
365
359
|
// 最坏情况被 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
|
-
}
|
|
360
|
+
// 0.9.92 三端查询文法:category/source/limit/offset 经 market-query 单一入口归一(与 tools/CLI
|
|
361
|
+
// 同源同语义);双违例顺序统一 category → source → 端本地参数(红线 #6 注记)。
|
|
362
|
+
let intake;
|
|
363
|
+
try {
|
|
364
|
+
intake = parseMarketQueryInput({ category: body.category, source: body.source, limit: body.limit, offset: body.offset }, { limitMax: 96, limitDefault: 32 });
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
if (e instanceof MarketQueryError)
|
|
368
|
+
throw new ApiProtocolError(400, e.message);
|
|
369
|
+
throw e;
|
|
380
370
|
}
|
|
381
371
|
// source 分区 + sort(0.7.0 Task 7):非法值 400(不静默吞);primaryOnly 已删除——显式拒绝而非静默忽略(审计 #8)
|
|
382
372
|
if (body.primaryOnly !== undefined) {
|
|
383
373
|
throw new ApiProtocolError(400, '参数 primaryOnly 已删除:改用 source(primary/community/all)');
|
|
384
374
|
}
|
|
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
375
|
let sort;
|
|
391
376
|
if (body.sort !== undefined && body.sort !== null) {
|
|
392
377
|
const s = body.sort;
|
|
@@ -405,14 +390,14 @@ export function createApiDispatcher(ctx) {
|
|
|
405
390
|
}
|
|
406
391
|
const result = await d.listMarket(cfg(), {
|
|
407
392
|
query: strArg(body, 'query'),
|
|
408
|
-
category,
|
|
409
|
-
source,
|
|
393
|
+
category: intake.category,
|
|
394
|
+
source: intake.source,
|
|
410
395
|
sort,
|
|
411
396
|
// 0.9.26:GUI 跨区搜索精选稳定前置(listMarket 内部仅在 source='all' 且 query 非空时生效)。
|
|
412
397
|
// 本参数仅 GUI 通道携带——tools/CLI 走各自调用面不传,搜索排序三端同序不变。
|
|
413
398
|
curatedFirst: true,
|
|
414
|
-
offset,
|
|
415
|
-
limit,
|
|
399
|
+
offset: intake.offset,
|
|
400
|
+
limit: intake.limit,
|
|
416
401
|
force: boolArg(body.force),
|
|
417
402
|
withLatest: true,
|
|
418
403
|
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/core/market.js
CHANGED
|
@@ -822,6 +822,21 @@ export class InstallGuardError extends Error {
|
|
|
822
822
|
this.compensation = fields.compensation;
|
|
823
823
|
this.repairBasis = fields.repairBasis;
|
|
824
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* 线格式单一事实源(0.9.93):三端共享的 6 字段守卫投影。各端信封自拼(host-api 409 payload 的
|
|
827
|
+
* ok/error、tools 的 ok/guard/message、cli 的裸 JSON),共享部分一律经此方法——加守卫字段只改
|
|
828
|
+
* 本方法 + client 读取一处。repairBasis undefined 时键不存在(今日 5 处投影同语义)。
|
|
829
|
+
*/
|
|
830
|
+
toWire() {
|
|
831
|
+
return {
|
|
832
|
+
kind: this.kind,
|
|
833
|
+
violations: this.violations,
|
|
834
|
+
compensation: this.compensation,
|
|
835
|
+
needsRestart: this.needsRestart,
|
|
836
|
+
restartSafe: this.restartSafe,
|
|
837
|
+
...(this.repairBasis ? { repairBasis: this.repairBasis } : {}),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
825
840
|
}
|
|
826
841
|
/** lockfile packages 段的目标包 resolution 提取(行扫描:键行 `pkg@ver:` → 收集至下一键行;引号/嵌套键兼容)。 */
|
|
827
842
|
function lockResolutionOf(lockText, pkg) {
|
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, {
|
|
@@ -265,7 +241,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
265
241
|
catch (err) {
|
|
266
242
|
if (err instanceof InstallGuardError) {
|
|
267
243
|
// 结构化 error result(M2 Task 3 ㉑-㉜):不渲染「已安装」,无 force 通道;restartSafe 随结果携带
|
|
268
|
-
return { ok: false, guard: true,
|
|
244
|
+
return { ok: false, guard: true, message: err.message, ...err.toWire() };
|
|
269
245
|
}
|
|
270
246
|
throw err;
|
|
271
247
|
}
|
|
@@ -427,7 +403,7 @@ export function registerTools(ctx, cfg, deps = {}, profile = resolveActiveProfil
|
|
|
427
403
|
}
|
|
428
404
|
catch (err) {
|
|
429
405
|
if (err instanceof InstallGuardError) {
|
|
430
|
-
return { ok: false, guard: true,
|
|
406
|
+
return { ok: false, guard: true, message: err.message, ...err.toWire() };
|
|
431
407
|
}
|
|
432
408
|
throw err;
|
|
433
409
|
}
|
|
@@ -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
|
-
}
|