dsh-m 0.9.89 → 0.9.90
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 +14 -0
- package/README.md +1 -1
- package/lib/cli.js +9 -6
- package/lib/core/search-relevance.js +78 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,13 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.90 性能:搜索归一化冷路径单遍化——世代冷搜索 ~330ms → 99ms p50(宿主事件循环最后一块 >100ms 停摆清除)+ CLI search 位置参数修复
|
|
12
|
+
|
|
13
|
+
- **问题(2026-10-16 走查 + 评审实测)**:物化合并市场(ADR-0016)每个世代的**首次搜索**仍同步阻塞宿主事件循环 ~324–364ms——`normalizeSearchText` 逐字符双 regex 分类 + 收尾全量 regex replace 两遍扫描(全语料 256.8ms,NFKC 仅 13.6ms)+ `fieldText` 每字段三次数组分配;触发频率 = 每小时 registry SWR 换代(fetchedAt 无条件 bump)+ 每次宿主重启 + 每日目录换版后的第一次搜索,agent `dshm_search` 同路径。另:`dshm search 主题` 的位置参数被 parseArgs 静默丢弃(query=undefined → 无评分浏览整页)。
|
|
14
|
+
- **方案**:① `normalizeSearchText` 激进单遍化——ASCII 查表 + CJK 稳定核四区间直判(4E00–9FA5 / 3400–4DB5 / F900–FA6D / FA70–FAD9,Unicode 赋值稳定性 → 与运行时版本无关)+ 非词符运行就地折叠(收尾全量 replace 退役)+ astral 词对透明规则;FA 段 40 个未赋值洞(U+FA6E/FA6F/FADA–FAFF)与版本敏感缘一律回退 WORD_RE/CJK_RE(同谓词同运行时,等价由构造保证);② `fieldText` 单元素快路(join([n])===n 恒等,免三次数组分配);③ CLI 首个位置参数等价 `--query`(flag 优先、`--query` 缺值宽容回退、help 同步)。
|
|
15
|
+
- **等价红线**:golden 差分语料 3888 条(分层抽样 + mulberry32 种子 fuzz 含 astral/NFKC/FA 洞/lone surrogate 池 + 手写 quirk 钉 11 例)零 mismatch——𝕏 的 NFKC→ASCII 折叠、FA 段洞折叠、lone surrogate 折叠、astral 转角不触发等现行为**逐字节原样**(评审独立加固:839,295 例差分零 mismatch);mismatch 一律修实现、绝不改 fixture。生成器与计时探针入库(手动跑不进 CI;mulberry32 确定性 + 目录版本数值比较取最新,同 seed 再生同语料)。
|
|
16
|
+
- **验证**:全量 1506 测试全绿、typecheck 零错误;实测(本机空闲):纯函数全语料 256.8→**44.5ms**(5.8×);世代冷搜索 324–364 → **99.3ms p50**(3.3×;首跑 JIT 冷 120.2ms 单列);E2E 重启场景 ~500 → **348.6ms**、小时场景 ~380 → **207.2ms**(builds delta=1,换代确实发生);评审独立复现 91.5–96.2 / 45.1–45.8ms。实施计划经独立评审 Agent 四轮对抗审核收敛(计划 R1–R3 + 执行 R4),四项验收口径全部达成。
|
|
17
|
+
|
|
11
18
|
### 0.9.89 优化:两行态头部视觉恒定——完整标题保留 + winctl 尺寸/行高不随断点跳变
|
|
12
19
|
|
|
13
20
|
- **需求(2026-10-15,主人真机反馈)**:0.9.88 的 720 断点两行重排带来两处可感知跳变——标题在「插件市场 ↔ DeepSeek Harness 插件市场」间换字;右上角 winctl 按钮忽大忽小(28↔36px),且标题/按钮垂直位置跨断点漂移 1–2px,来回拖拽「一跳一跳」。
|
|
@@ -630,6 +637,13 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
630
637
|
|
|
631
638
|
## English
|
|
632
639
|
|
|
640
|
+
### Performance in 0.9.90 — single-pass search normalization cold path: per-generation cold search ~330ms → 99ms p50 (last >100ms stall on the host event loop cleared) + CLI search positional fix
|
|
641
|
+
|
|
642
|
+
- **Problem (2026-10-16 walk + review measurements)**: the first search of every materialized merged-market generation (ADR-0016) still blocked the DSH host event loop synchronously for ~324–364ms — `normalizeSearchText`'s per-char dual-regex classification plus a trailing full-scan regex replace (256.8ms over the full corpus; NFKC is only 13.6ms), plus three array allocations per field in `fieldText`. Triggered hourly by registry SWR generation churn (fetchedAt bumps unconditionally), after every host restart, and on the first search after each daily catalog bump — the agent `dshm_search` tool rides the same path. Also: `dshm search 主题` silently dropped the positional keyword (query=undefined → unranked browse page).
|
|
643
|
+
- **Approach**: ① aggressive single-pass `normalizeSearchText` — ASCII lookup table + direct classification for four stable CJK core ranges (4E00–9FA5 / 3400–4DB5 / F900–FA6D / FA70–FAD9; Unicode assignment stability makes them independent of the runtime's Unicode version) + in-pass collapsing of non-word runs (the trailing full-scan replace retires) + a transparent astral word-pair rule; the 40 unassigned FA holes (U+FA6E/FA6F/FADA–FAFF) and version-sensitive edges always fall back to WORD_RE/CJK_RE (same predicate, same runtime — equivalence by construction); ② single-element fast path in `fieldText` (join([n])===n identity, no triple allocations); ③ CLI positional keywords now equal `--query` (flag wins, a valueless `--query` falls back leniently, help updated).
|
|
644
|
+
- **Equivalence red line**: a 3,888-case golden differential corpus (stratified sampling + seeded fuzz with astral/NFKC/FA-hole/lone-surrogate pools + 11 handwritten quirk pins) shows **zero mismatch** — current behaviors such as 𝕏's NFKC→ASCII folding, FA-hole collapsing, lone-surrogate folding, and no-transition astral boundaries are preserved **byte-for-byte** (independently hardened by the reviewer: 839,295 differential cases, zero mismatch); mismatches are always fixed in the implementation, never in the fixture. The corpus generator and timing probe are committed (manual, not CI; mulberry32 determinism + numeric version comparison for catalog selection — same seed regenerates the same corpus).
|
|
645
|
+
- **Verification**: full suite 1506 green, typecheck clean; measured (idle machine): pure function 256.8→**44.5ms** (5.8×); per-generation cold search 324–364 → **99.3ms p50** (3.3×; first JIT-cold run 120.2ms reported separately); E2E restart scenario ~500 → **348.6ms**, hourly scenario ~380 → **207.2ms** (builds delta=1); reviewer reproduction 91.5–96.2 / 45.1–45.8ms. The implementation plan converged through four adversarial review rounds (plan R1–R3 + execution R4); all four acceptance criteria passed.
|
|
646
|
+
|
|
633
647
|
### Polish in 0.9.89 — constant two-row head: full title kept, winctl size/row height independent of the breakpoint
|
|
634
648
|
|
|
635
649
|
- **Need (2026-10-15, live feedback)**: the 720px two-row reflow shipped in 0.9.88 introduced two perceptible jumps — the title swapped between "插件市场" and "DeepSeek Harness 插件市场", and the winctl buttons changed size (28↔36px) while title/button vertical positions drifted 1–2px across the breakpoint.
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ GUI 与 Agent 工具始终跟随**当前宿主 profile**(Web / Desktop);
|
|
|
89
89
|
|
|
90
90
|
**自定义清单不可用怎么办?** 优先使用该源上次成功的缓存;没有缓存时会提示清单不可用,已装管理仍能工作。修正地址或在设置页恢复默认即可。
|
|
91
91
|
|
|
92
|
-
完整沿革见 [CHANGELOG](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md),设计与开发细节见 [DESIGN](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)、[ADR](https://github.com/iasiv5/dsh-m/tree/main/docs/adr)。截图取自 Web 实例;面板版本、条目数与安装状态会随时间和 profile 变化。在源码仓库运行 `npm install && npm run capture:readme` 可复现截图(需 Web 实例已装 dsh-m;此开发脚本不随 npm 插件包分发)。
|
|
92
|
+
完整沿革见 [CHANGELOG](https://github.com/iasiv5/dsh-m/blob/main/CHANGELOG.md),设计与开发细节见 [DESIGN](https://github.com/iasiv5/dsh-m/blob/main/docs/DESIGN.md)、[ADR](https://github.com/iasiv5/dsh-m/tree/main/docs/adr);编码与测试纪律(评审必读)见 [CODING-STANDARDS](https://github.com/iasiv5/dsh-m/blob/main/docs/CODING-STANDARDS.md)。截图取自 Web 实例;面板版本、条目数与安装状态会随时间和 profile 变化。在源码仓库运行 `npm install && npm run capture:readme` 可复现截图(需 Web 实例已装 dsh-m;此开发脚本不随 npm 插件包分发)。
|
|
93
93
|
|
|
94
94
|
## License
|
|
95
95
|
|
package/lib/cli.js
CHANGED
|
@@ -30,10 +30,13 @@ function categoryLabelOf(category) {
|
|
|
30
30
|
function parseArgs(argv) {
|
|
31
31
|
const cmd = argv[0] || 'help';
|
|
32
32
|
const flags = {};
|
|
33
|
+
const positionals = [];
|
|
33
34
|
for (let i = 1; i < argv.length; i++) {
|
|
34
35
|
const a = argv[i];
|
|
35
|
-
if (!a.startsWith('--'))
|
|
36
|
+
if (!a.startsWith('--')) {
|
|
37
|
+
positionals.push(a);
|
|
36
38
|
continue;
|
|
39
|
+
}
|
|
37
40
|
const key = a.slice(2);
|
|
38
41
|
const next = argv[i + 1];
|
|
39
42
|
if (next !== undefined && !next.startsWith('--')) {
|
|
@@ -44,7 +47,7 @@ function parseArgs(argv) {
|
|
|
44
47
|
flags[key] = true;
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
return { cmd, flags };
|
|
50
|
+
return { cmd, flags, positionals };
|
|
48
51
|
}
|
|
49
52
|
function cliConfig() {
|
|
50
53
|
return {
|
|
@@ -128,8 +131,8 @@ const HELP = `dshm — DSH Marketplace(DSH 插件市场:精选策展 + 社
|
|
|
128
131
|
用法:dshm <命令> [参数]
|
|
129
132
|
|
|
130
133
|
只读命令:
|
|
131
|
-
dshm search [--query 关键词] [--category market|tools|ui|search|other|<社区slug>] [--source primary|community|all] [--limit N] [--offset N]
|
|
132
|
-
|
|
134
|
+
dshm search [关键词] [--query 关键词] [--category market|tools|ui|search|other|<社区slug>] [--source primary|community|all] [--limit N] [--offset N]
|
|
135
|
+
(首个位置参数等价 --query,--query 缺值时也回退它,多余位置参数忽略;source 分区:community=社区目录 4000+ 条 / primary=精选策展 / all=默认;limit 默认 10;offset 翻页)
|
|
133
136
|
dshm list 列出 web profile 已装插件(含市场标注/可升级)
|
|
134
137
|
dshm outdated 检查已装插件的最新版本
|
|
135
138
|
dshm registry 查看收录清单来源与条目
|
|
@@ -174,7 +177,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
174
177
|
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
175
178
|
togglePlugin: deps.togglePlugin ?? coreTogglePlugin,
|
|
176
179
|
};
|
|
177
|
-
const { cmd, flags } = parseArgs(argv);
|
|
180
|
+
const { cmd, flags, positionals } = parseArgs(argv);
|
|
178
181
|
const cfg = cliConfig();
|
|
179
182
|
// 0.9.0 双 profile(ADR-0005):CLI 是 Web-only 入口(独立进程拿不到宿主 profileContext,
|
|
180
183
|
// 目标恒为 web profile);--profile desktop / 其他非 web 值显式拒绝并指引官方入口,
|
|
@@ -208,7 +211,7 @@ async function runCliDispatch(argv, deps, io) {
|
|
|
208
211
|
const offsetRaw = Number(flags.offset);
|
|
209
212
|
const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? Math.floor(offsetRaw) : 0;
|
|
210
213
|
const result = await d.listMarket(cfg, {
|
|
211
|
-
query: typeof flags.query === 'string' ? flags.query :
|
|
214
|
+
query: typeof flags.query === 'string' ? flags.query : positionals[0],
|
|
212
215
|
category: typeof flags.category === 'string' ? flags.category : null,
|
|
213
216
|
source,
|
|
214
217
|
offset,
|
|
@@ -25,20 +25,82 @@ const B_CONTAINS = 200;
|
|
|
25
25
|
const B_MULTI = 150;
|
|
26
26
|
const CJK_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
|
27
27
|
const WORD_RE = /[\p{L}\p{N}]/u;
|
|
28
|
-
/**
|
|
28
|
+
/** ASCII 词符查表(code<128):[0-9A-Za-z]=1(\p{L}\p{N} 在 ASCII 区间恰为字母数字),其余=0。 */
|
|
29
|
+
const ASCII_WORD = new Uint8Array(128);
|
|
30
|
+
for (let c = 48; c <= 57; c++)
|
|
31
|
+
ASCII_WORD[c] = 1;
|
|
32
|
+
for (let c = 65; c <= 90; c++)
|
|
33
|
+
ASCII_WORD[c] = 1;
|
|
34
|
+
for (let c = 97; c <= 122; c++)
|
|
35
|
+
ASCII_WORD[c] = 1;
|
|
36
|
+
/**
|
|
37
|
+
* 归一化:NFKC + 小写 + 中西文边界插空格(`MCP管理` ≈ `MCP 管理`)+ 标点/符号/空白归单空格。
|
|
38
|
+
*
|
|
39
|
+
* 单遍实现(2026-10-16 冷路径优化):一次扫描同时完成旧「转角插空 + 收尾 `[^\p{L}\p{N}]+` 归单空格 +
|
|
40
|
+
* trim」三件事,输出与旧两遍实现**逐字节等价**(golden 差分语料 3888 条 + quirk 钉钉死;mismatch 一律修实现不改 fixture)。
|
|
41
|
+
* - 分类:ASCII 走查表;CJK 稳定核四区间(4E00–9FA5 / 3400–4DB5 / F900–FA6D / FA70–FAD9)直判词符 CJK——
|
|
42
|
+
* 按段自 Unicode 1.1 / 3.0 / 3.2 / 4.1 起赋值且永不负赋值(≥4.1 运行时必全赋值,与运行时版本无关);
|
|
43
|
+
* 其余非 ASCII(FA 段未赋值洞 U+FA6E/FA6F/FADA–FAFF、版本敏感缘 U+4DB6+/U+9FA6+ 等)一律回退
|
|
44
|
+
* WORD_RE/CJK_RE——回退与旧实现同谓词同运行时,等价由构造保证。
|
|
45
|
+
* - astral 词对:代理对整体经 WORD_RE 按码点判词(命中者必 NFKC 稳定,如 U+20000;数学字母 𝕏 等已被
|
|
46
|
+
* NFKC 折成 ASCII 到不了这里)→ 原样保留、冲刷待定分隔、重置转角状态——精确复刻旧实现「代理项对半按
|
|
47
|
+
* 非词参与转角判定(不触发插空)、但收尾 replace 按码点保留 astral 词符」的合成行为;emoji 等非词对与
|
|
48
|
+
* 孤立代理半区走通用非词折叠。
|
|
49
|
+
*/
|
|
29
50
|
export function normalizeSearchText(input) {
|
|
30
51
|
const nfk = input.normalize('NFKC').toLowerCase();
|
|
31
52
|
let out = '';
|
|
53
|
+
let lastWordKind = 0; // 0=尚无词符,1=词非 CJK,2=词 CJK
|
|
54
|
+
let pendingSpace = false;
|
|
32
55
|
for (let i = 0; i < nfk.length; i++) {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
|
|
56
|
+
const code = nfk.charCodeAt(i);
|
|
57
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
58
|
+
const code2 = i + 1 < nfk.length ? nfk.charCodeAt(i + 1) : 0;
|
|
59
|
+
if (code2 >= 0xdc00 && code2 <= 0xdfff && WORD_RE.test(nfk.slice(i, i + 2))) {
|
|
60
|
+
if (pendingSpace) {
|
|
61
|
+
out += ' ';
|
|
62
|
+
pendingSpace = false;
|
|
63
|
+
}
|
|
64
|
+
out += nfk[i];
|
|
65
|
+
out += nfk[i + 1];
|
|
66
|
+
lastWordKind = 0;
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// 非词对(emoji 等)或孤立前导半区:落入通用路径按非词处理
|
|
71
|
+
}
|
|
72
|
+
let k;
|
|
73
|
+
if (code < 128) {
|
|
74
|
+
k = ASCII_WORD[code] ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
else if ((code >= 0x4e00 && code <= 0x9fa5) ||
|
|
77
|
+
(code >= 0x3400 && code <= 0x4db5) ||
|
|
78
|
+
(code >= 0xf900 && code <= 0xfa6d) ||
|
|
79
|
+
(code >= 0xfa70 && code <= 0xfad9)) {
|
|
80
|
+
k = 2;
|
|
38
81
|
}
|
|
39
|
-
|
|
82
|
+
else if (!WORD_RE.test(nfk[i])) {
|
|
83
|
+
k = 0;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
k = CJK_RE.test(nfk[i]) ? 2 : 1;
|
|
87
|
+
}
|
|
88
|
+
if (k === 0) {
|
|
89
|
+
if (out !== '')
|
|
90
|
+
pendingSpace = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (pendingSpace) {
|
|
94
|
+
out += ' ';
|
|
95
|
+
pendingSpace = false;
|
|
96
|
+
lastWordKind = 0;
|
|
97
|
+
}
|
|
98
|
+
if (lastWordKind !== 0 && (k === 2) !== (lastWordKind === 2))
|
|
99
|
+
out += ' ';
|
|
100
|
+
out += nfk[i];
|
|
101
|
+
lastWordKind = k;
|
|
40
102
|
}
|
|
41
|
-
return out
|
|
103
|
+
return out;
|
|
42
104
|
}
|
|
43
105
|
/** 分词(输入应为 normalizeSearchText 产物)。 */
|
|
44
106
|
export function tokenizeSearchText(normalized) {
|
|
@@ -46,6 +108,14 @@ export function tokenizeSearchText(normalized) {
|
|
|
46
108
|
}
|
|
47
109
|
/** 多段字段(如 tags)归一化为单空格连接的 token 串。 */
|
|
48
110
|
function fieldText(parts) {
|
|
111
|
+
// 单元素快路(2026-10-16 冷路径优化):单字符串字段(name/npm/owner/description/…占绝大多数)
|
|
112
|
+
// 的 join([n]) === n 恒等,免 filter/map/filter 三次数组分配;空/全符号串归 '' 与旧路径一致。
|
|
113
|
+
if (parts.length === 1) {
|
|
114
|
+
const p = parts[0];
|
|
115
|
+
if (typeof p === 'string' && p.trim() !== '')
|
|
116
|
+
return normalizeSearchText(p);
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
49
119
|
return parts
|
|
50
120
|
.filter((p) => typeof p === 'string' && p.trim() !== '')
|
|
51
121
|
.map((p) => normalizeSearchText(p))
|