dsh-m 0.9.88 → 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 +29 -1
- package/README.md +1 -1
- package/lib/cli.js +9 -6
- package/lib/client.js +10 -4
- package/lib/core/search-relevance.js +78 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ 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
|
+
|
|
18
|
+
### 0.9.89 优化:两行态头部视觉恒定——完整标题保留 + winctl 尺寸/行高不随断点跳变
|
|
19
|
+
|
|
20
|
+
- **需求(2026-10-15,主人真机反馈)**:0.9.88 的 720 断点两行重排带来两处可感知跳变——标题在「插件市场 ↔ DeepSeek Harness 插件市场」间换字;右上角 winctl 按钮忽大忽小(28↔36px),且标题/按钮垂直位置跨断点漂移 1–2px,来回拖拽「一跳一跳」。
|
|
21
|
+
- **方案**:① 两行态保留完整标题——seg 让行后首行空间充裕(640 宽实测 612px 内容区只需 ~300px),短标题切换退役(DOM 双 span 与基础隐藏规则保留,极端窄由 nowrap+ellipsis 兜底);② winctl 触控 36px 从容器断点迁 `pointer:coarse`(按钮大小跟随输入方式而非宽度:鼠标恒 28px、触屏恒 36px);③ winctl 盒钉 34px = seg 行高(按钮 28 + 边框 2 同源公式),单行/两行第一行高度归一。
|
|
22
|
+
- **验证**:全量 1496 测试全绿、typecheck 零错误;CDP 实测跨断点标题/按钮垂直中心 delta 0.0px(修复前 -1~-2px);主人真机验收通过。
|
|
23
|
+
- **杂项**:package-lock 根版本字段自 0.9.68 起历届发版漏改,本次补齐至当前版本并纳入发版流程。
|
|
24
|
+
|
|
11
25
|
### 0.9.88 特性:面板三区 resize——尺寸记忆 + 就地生长(ADR-0021)+ 验收三修
|
|
12
26
|
|
|
13
27
|
- **需求(2026-10-15)**:市场面板「920px 浏览卡片太挤、全屏又太大」的真实档位空洞 + 桌面窗口本能手势(下意识移向边缘想 resize)。`/grill-with-docs` 两轮锁定设计:三区手柄(右/下/右下角)、就地生长锚定、尺寸记忆全套;实施计划经独立评审 Agent 三轮对抗审核(计划 11 条 + 执行 2 条全闭环,含两条方法论沉淀:`treeShaking:false` 下「bundle 标识符缺席」类预言必假、esbuild 默认 charset=ascii 下中文 bundle 锚必假)。
|
|
@@ -623,7 +637,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
623
637
|
|
|
624
638
|
## English
|
|
625
639
|
|
|
626
|
-
###
|
|
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
|
+
|
|
647
|
+
### Polish in 0.9.89 — constant two-row head: full title kept, winctl size/row height independent of the breakpoint
|
|
648
|
+
|
|
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.
|
|
650
|
+
- **Approach**: ① the two-row state keeps the full title — with seg moved to its own row the first row has ample room (~300px used of 612px at 640 width), so the short-title swap retires (DOM spans and base hiding kept; nowrap+ellipsis covers extreme narrow); ② the winctl touch bump (36px) moved from the container breakpoint to `pointer:coarse` (button size follows input modality, not width: mouse always 28px, touch always 36px); ③ the winctl box pinned to 34px = seg row height (same 28+2-border formula), normalizing first-row height across both modes.
|
|
651
|
+
- **Verification**: full suite 1496 green, typecheck clean; CDP measurement shows 0.0px vertical-center delta for title and buttons across the breakpoint (was -1~-2px); accepted on-device by the owner.
|
|
652
|
+
- **Misc**: package-lock root version fields, unsynced since 0.9.68, now track the release version and join the release checklist.
|
|
653
|
+
|
|
654
|
+
### 0.9.88 — three-zone panel resize: size memory + grow-in-place (ADR-0021) + acceptance fixes
|
|
627
655
|
|
|
628
656
|
- **Need (2026-10-15)**: a real gap between "cramped at 920px" and "overkill fullscreen", plus the instinctive desktop gesture of grabbing window edges. Design locked via two grill rounds: three handles (right/bottom/bottom-right), grow-in-place anchoring, full size memory; the implementation plan survived three independent adversarial review rounds (11 plan + 2 execution issues all closed, including two methodology findings: bundle-absence predictions are structurally false under `treeShaking:false`, and CJK bundle anchors are false under esbuild's default ascii charset).
|
|
629
657
|
- **Approach (ADR-0021)**: ① DOM-free `window-resize.js` — `clampSize` (min 640×460, viewport−8px margins, an origin second clamp keeping right/bottom edges inside margins for the freely-positioned form, degraded bounds on tiny viewports) + size-memory storage symmetric to position memory; ② gesture state machine mirrors ADR-0017 (rAF-batched writes, zero setState in gesture frames, setPointerCapture, primary-button guard, multi-pointer guard, unmount cleanup); ③ hot zones 6/8px edges + 14×14 corner, cursor-only affordance, four exemptions (fullscreen / narrow viewport / touch / scrollbar trade-off); ④ reclamp upgraded to size-first-then-position.
|
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,
|
package/lib/client.js
CHANGED
|
@@ -2693,7 +2693,7 @@ var CSS = `
|
|
|
2693
2693
|
@media (pointer:coarse){.dshm-rz{display:none}} /* belt \u2461\uFF1Acoarse \u6307\u9488\u6574\u7EC4\u9690\u85CF\uFF08Q8\uFF09 */
|
|
2694
2694
|
/* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u7B49\u5BBD\u4E24\u683C + \u53D1\u4E1D\u5206\u9694\u7EBF\uFF0C\u7EDF\u4E00\u7EBF\u6761\u56FE\u6807 */
|
|
2695
2695
|
/* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7/0.7.8\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u900F\u660E\u5E95 + \u53D1\u4E1D\u5916\u6846\uFF0C\u4E0E\u641C\u7D22\u6E05\u9664\u94AE\u540C\u4E00\u914D\u8272\u8BED\u8A00\uFF1B\u5BBD\u683C\u9632\u8BEF\u89E6 */
|
|
2696
|
-
.dshm-winctl{display:inline-flex;align-items:
|
|
2696
|
+
.dshm-winctl{display:inline-flex;align-items:center;height:34px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:9px;background:transparent;overflow:hidden;flex:none}
|
|
2697
2697
|
.dshm-winctl button{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);width:44px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,color .15s}
|
|
2698
2698
|
.dshm-winctl button + button{border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}
|
|
2699
2699
|
.dshm-winctl button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,inherit)}
|
|
@@ -3071,12 +3071,16 @@ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
|
|
|
3071
3071
|
@container (max-width:720px){
|
|
3072
3072
|
.dshm-head{flex-wrap:wrap;row-gap:6px}
|
|
3073
3073
|
.dshm-head-divider{display:none}
|
|
3074
|
-
.
|
|
3075
|
-
|
|
3074
|
+
/* 0.9.89\uFF1A\u4E24\u884C\u6001\u4FDD\u7559\u5B8C\u6574\u6807\u9898\uFF08\u4E3B\u4EBA\u63D0\u8BAE\u5B9E\u6D4B\u91C7\u7EB3\uFF09\u2014\u2014seg \u8BA9\u884C\u540E\u9996\u884C\u7A7A\u95F4\u5145\u88D5
|
|
3075
|
+
\uFF08640 \u5BBD\u5B9E\u6D4B 612px \u5185\u5BB9\u533A\u53EA\u9700 ~300px\uFF09\uFF0C\u77ED\u6807\u9898\u5207\u6362\u53CD\u800C\u5236\u9020\u65AD\u70B9\u6362\u5B57\u95EA\u70C1\uFF1B
|
|
3076
|
+
\u6781\u7AEF\u7A84\u7531 .dshm-title \u7684 nowrap+ellipsis \u515C\u5E95\u3002DOM \u53CC span \u4E0E\u57FA\u7840\u9690\u85CF\u89C4\u5219
|
|
3077
|
+
\u4FDD\u7559\uFF08\u77ED\u6807\u9898\u6C38\u4E0D\u663E\u793A\uFF0C\u56DE\u9000\u96F6\u6210\u672C\uFF09\u3002 */
|
|
3076
3078
|
.dshm-spacer{order:1}
|
|
3077
3079
|
.dshm-head>.dshm-dshchip{order:2}
|
|
3078
3080
|
.dshm-winctl{order:3}
|
|
3079
|
-
.
|
|
3081
|
+
/* 0.9.89\uFF1Awinctl \u89E6\u63A7\u52A0\u5927\uFF0836px\uFF09\u4ECE\u5BB9\u5668\u65AD\u70B9\u8FC1\u81F3 pointer:coarse\u2014\u2014\u6309\u94AE\u5927\u5C0F\u4E0D\u8BE5\u968F
|
|
3082
|
+
\u4E24\u884C/\u5355\u884C\u6A21\u5F0F\u8DF3\u53D8\uFF08\u4E3B\u4EBA\u5B9E\u6D4B 720 \u8FB9\u754C\u5FFD\u5927\u5FFD\u5C0F\uFF09\uFF0C\u771F\u6B63\u53D8\u91CF\u662F\u8F93\u5165\u65B9\u5F0F\uFF1B\u89E6\u5C4F\u8BBE\u5907
|
|
3083
|
+
\u5168\u6A21\u5F0F\u6052 36px\uFF0C\u9F20\u6807\u8BBE\u5907\u5168\u6A21\u5F0F\u6052 28px */
|
|
3080
3084
|
.dshm-seg{order:4;flex:1 1 100%}
|
|
3081
3085
|
.dshm-seg button{flex:1;justify-content:center;white-space:nowrap;padding:0 10px;height:36px}
|
|
3082
3086
|
/* \u7248\u672C chip\uFF08\u5171\u8BC6 Q7 + \u8BC4\u5BA1 R5 \u6536\u7A84\uFF09\uFF1A\u5E38\u6001\u9690\u85CF\uFF08\u8BBE\u7F6E\u9875\u968F\u65F6\u53EF\u89C1\uFF09\uFF1Bwarn \u6001\u7F29\u4E3A \u2B06
|
|
@@ -3089,6 +3093,8 @@ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
|
|
|
3089
3093
|
/* overlay \u7A84\u89C6\u53E3\u515C\u5E95\uFF1A\u8FD8\u539F\u6001 8px\uFF08.full \u7684 padding:0 \u7279\u5F02\u6027 (0,2,0)>(0,1,0) \u81EA\u7136\u4FDD\u6301\uFF09\uFF1B
|
|
3090
3094
|
\u7A84\u89C6\u53E3\u9ED8\u8BA4\u5168\u5C4F\u7684 JS \u5224\u5B9A\u5728 MarketPanel full \u521D\u59CB\u5316 */
|
|
3091
3095
|
@media (max-width:600px){.dshm-overlay{padding:8px}}
|
|
3096
|
+
/* \u89E6\u63A7\u76EE\u6807\u52A0\u5927\u968F\u8F93\u5165\u65B9\u5F0F\u800C\u975E\u5BBD\u5EA6\uFF080.9.89 \u8FC1\u79FB\uFF0C\u89C1 720 \u5757\u5185\u6CE8\u91CA\uFF09 */
|
|
3097
|
+
@media (pointer:coarse){.dshm-winctl{height:38px}.dshm-winctl button{height:36px}}
|
|
3092
3098
|
`;
|
|
3093
3099
|
function cssVersionOf(s) {
|
|
3094
3100
|
let h2 = 5381;
|
|
@@ -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))
|