dsh-m 0.9.67 → 0.9.69
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 +32 -0
- package/lib/client.js +332 -2
- package/lib/core/community.js +35 -31
- package/lib/core/market.js +97 -198
- package/lib/core/merged-market.js +356 -0
- package/lib/core/profile-ops.js +17 -6
- package/package.json +1 -1
- package/registry.json +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,22 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.69 功能:还原态面板头部拖拽 + 位置记忆(ADR-0017)
|
|
12
|
+
|
|
13
|
+
- **方案**:市场面板(`.dshm-panel`)还原态下按住头部(标题/tab 行)即可自由拖拽——头部全域为手柄,tab/全屏/关闭钮点击语义原样(`closest("button")` 门卫 + 多指重入守卫);双击头部非按钮区切换全屏;释放位置烘焙为视口绝对坐标存 `dshm-panel-pos`,下次打开还原,无记忆首开与旧版 flex 居中渲染逐字节一致。运动范围四周 8px;desktop profile 额外让出壳层标题拖拽带(quota-watch 契约:`data-windows-titlebar` + `--dsh-windows-titlebar-height`,web 无此变量恒 0)——胶囊时代「停进系统按钮区就再也抓不回」的坑由 clamp 机制性杜绝。挂载/全屏往返/resize 三触发源重夹紧,还原 commit 的重夹紧时序由 render 期 latest-ref 镜像保证(执行评审 E1-1 实证修复并以结构锚钉死)。
|
|
14
|
+
- **皮肤免疫(拖拽期 = 最快渲染路径)**:拖拽背景取手势开始时 `getComputedStyle` 的最终色强制 α=1 写入 `--dshm-drag-bg` 并 `!important` 自持——openbmc/uefi 玻璃皮肤(bg-base α=0.55 病类,quota-watch 胶囊拖拽变半透明的同款病例)下按住拖动面板完全实心不透字;同时拖拽期关闭 backdrop-filter,每帧只剩 compositor 平移一层:pointer capture + 手势期才挂 document 监听 + pointermove 只存点、样式帧内写 + 拖拽帧绝不走 React state。静止态玻璃外观、0.9.27 防误关守卫、灯箱/详情 modal 语义全部不动。
|
|
15
|
+
- **验证**:1326 项测试全绿、typecheck 零错误;构建产物含 `dshm-panel--dragging`/`dshm-panel-pos` 逐字锚;`!important` 清单定长 4→7(client-lightbox 断言登记)。实施计划与执行结果经独立评审 Agent 五轮对抗评审全闭环(16 条:13 条修于计划、3 条修于代码/如实申报)。
|
|
16
|
+
|
|
17
|
+
### 0.9.68 性能:合并市场物化——同身份免重付 5MB 解析 + 适配 + 合并(ADR-0016)
|
|
18
|
+
|
|
19
|
+
- **问题(性能架构审查旗舰项,三轮评审闭环)**:领域概念「合并市场(Merged Market)」在 GLOSSARY 有名无主——装配逻辑内联在 `communityOutcome`,每个市场请求(缓存全命中的快路径也在内)都重付磁盘读 5.36MB 社区目录 + JSON.parse(实测 30–55ms)→ 目录适配层重建 4,400 条(30–90ms)→ 合并排序(5–35ms);且全部是**同步 JS、阻塞宿主事件循环**——市场每翻一页/每提交一次搜索,宿主的 SSE 对话流与工具调用停摆 65–400ms。搜索评分因归一化 WeakMap 跨请求永不命中再加 ~330ms(冷)。
|
|
20
|
+
- **两级 memo,各持天然安全键(ADR-0016 决策 1)**:L1——community.ts `readCache` 按不可变 version-pin 文件名缓存已解析目录(cap=2,meta 每次实读保 checkedAt 新鲜,失败路径不入 memo);L2——新模块 `src/core/merged-market.ts` 物化适配→合并→匹配索引(cap=4),身份键 = (namespace, profile, registry 身份, 目录 version+pin+开关)。
|
|
21
|
+
- **探测驱动换代 + 同步临界区(决策 3)**:每请求照常走廉价快路径(registry ~10KB + meta 141B ≈1–2ms)以返回身份字段查代;全部 await 位于键派生前、临界区全同步——并发同键天然只建一次,`builds` 计数断言哨兵。**summary 永不入代**每调用现算(status/checkedAt/errors 永远新鲜,SWR 与⑫同版本短路零漂移);deadline/unavailable/disabled/task=null 透传不落代;communityTask reject 透传(安装侧窄 catch 吞为 miss)。
|
|
22
|
+
- **双索引保安装语义(R2-3)**:merged 域 `lookupInstalled` 服务读路径;全量域 `findById`/`lookupInstalledAll`([primary…, adapt 全量] 含 displaced 让位条目)服务安装/升级三处(resolveRegistryEntry / upgradePlugin / profile-ops desktop 升级)——id-让位条目今日可装可升,语义零变化;主清单优先短路保留(社区异常不阻断主清单命中)。
|
|
23
|
+
- **解环 + score-once**:合并层六符号自 market.ts 迁入 merged-market.ts、23 条分类标签自 community.ts 随迁(原位 re-export 保 cli/tools 零改动)——market↔community import 环三方向断开;listMarket 评分段单遍化(类别短路先行、rank 查表),tie-break 与 0.9.67 逐点一致。
|
|
24
|
+
- **行为零变化承诺**:ADR-0003/0006/0008/0012/0013 全部不动;golden 等价基线(七维度 capture,含分页/搜索/排序/探测徽标)全程逐字节绿。
|
|
25
|
+
- **验证**:1305 项测试全绿、typecheck 零错误;实测本机 4,460 条真实目录首次 319ms → 同身份第二次 65ms,搜索评分冷 330ms → 同代热 8ms(40×)。实施经三轮独立评审闭环(计划评审 16 项 + 执行评审 7 项,全部「已修复并复核通过」;含一次修复脚本静默 no-op 的虚报,被评审 `git diff` 核对抓出并以真实 diff 补正)。实施计划与 ADR-0016 v3 在案(`docs/plans/`、`docs/adr/`)。
|
|
26
|
+
|
|
11
27
|
### 0.9.67 修复:操作记录卡移动端字号失控——显式入 12px 家族 + 折行自洽
|
|
12
28
|
|
|
13
29
|
- **根因(用户手机截图反馈)**:操作记录行 `.dsvm-oprow` 从不声明 `font-size`——桌面宿主基础字号下无感,移动端浏览器基础字号(≈15-16px)下整行被放大近一倍,与同屏设置卡的 12px 家族(`dsvm-kv` 配置地址行、`dshm-hint`、`dsvm-fold`)明显脱节,观感突兀。
|
|
@@ -495,6 +511,22 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
495
511
|
|
|
496
512
|
## English
|
|
497
513
|
|
|
514
|
+
### Feature in 0.9.69 — drag the marketplace panel by its header in restore state, with position memory (ADR-0017)
|
|
515
|
+
|
|
516
|
+
- **Design**: hold the panel header (title/tab row) in restore state to drag the marketplace panel freely - the whole header is the handle while tab / fullscreen / close buttons keep their click semantics (`closest("button")` gate + multi-pointer re-entry guard); double-click on a non-button header area toggles fullscreen; on release the position is baked into absolute viewport coordinates and stored under `dshm-panel-pos`, restored on next open - with no stored position the first render stays byte-identical to the old flex-centered layout. The motion range is clamped 8px on all sides; the desktop profile additionally yields the shell titlebar drag band (quota-watch contract: `data-windows-titlebar` + `--dsh-windows-titlebar-height`; web without the variable is always 0) - the capsule-era trap of parking inside the system button zone and never being grabbable again is eliminated by construction. Mount, fullscreen round-trips and window resize all re-clamp; the restore-commit re-clamp is ordered by render-phase latest-ref mirrors (execution review E1-1, reproduced, fixed, and pinned by structural anchors).
|
|
517
|
+
- **Skin immunity (drag = fastest render path)**: the drag background is captured at gesture start from `getComputedStyle` with alpha forced to 1 into `--dshm-drag-bg`, self-held by `!important` - on glass skins (openbmc/uefi, bg-base alpha 0.55 disease class - the same case that made the quota-watch capsule translucent while dragged) the panel stays fully opaque with zero see-through. backdrop-filter is disabled for the gesture, so each frame is a single compositor translation: pointer capture + document-level listeners only during the gesture + pointermove stores the point only (styles written inside rAF) + drag frames never touch React state. Resting glass looks, the 0.9.27 mis-close guard, lightbox and detail-modal semantics are all untouched.
|
|
518
|
+
- **Verification**: 1326 tests green, typecheck clean; bundle carries literal anchors `dshm-panel--dragging` / `dshm-panel-pos`; the `!important` inventory is length-pinned 4→7 (registered in client-lightbox assertions). Plan and execution were adversarially reviewed by an independent reviewer agent across five rounds - 16 findings, all closed (13 fixed in the plan, 3 fixed in code / honestly declared).
|
|
519
|
+
|
|
520
|
+
### Performance in 0.9.68 — materialized merged market: skip the 5MB parse + adapt + merge for repeat identities (ADR-0016)
|
|
521
|
+
|
|
522
|
+
- **Problem (flagship item of the performance architecture review, closed over three review rounds)**: the domain concept "Merged Market" existed in the GLOSSARY without an owning module - assembly logic lived inline in `communityOutcome`, so every market request (including fully cache-hit fast paths) re-paid a 5.36MB community catalog disk read + JSON.parse (measured 30-55ms) → adapter rebuild of 4,400 entries (30-90ms) → merge + sort (5-35ms). All of it is **synchronous JS blocking the host event loop** - every page turn / committed search froze the host's SSE streams and tool calls for 65-400ms. Search scoring added ~330ms cold because the normalization WeakMap never survived across requests.
|
|
523
|
+
- **Two-level memo, each with a naturally safe key (ADR-0016 decision 1)**: L1 - community.ts `readCache` memoizes the parsed catalog keyed by the immutable version-pinned body filename (cap=2; meta re-read every time so `checkedAt` stays fresh; failure paths never enter the memo); L2 - new module `src/core/merged-market.ts` materializes adapt→merge→match-index (cap=4), keyed by identity = (namespace, profile, registry identity, catalog version+pin+flag).
|
|
524
|
+
- **Probe-driven generations + synchronous critical section (decision 3)**: every request still walks the cheap fast path (registry ~10KB + meta 141B ≈1-2ms) and probes the store with the returned identity fields; all awaits happen before key derivation and the critical section (derive→check→adapt→merge→index→set) is fully synchronous - concurrent same-key callers naturally build once, asserted by a `builds` counter sentinel. **Summaries never enter the generation**: they are computed per call from the current state (status/checkedAt/errors always fresh; zero drift across SWR and the ⑫ same-version short-circuit). deadline/unavailable/disabled/task=null pass through without landing; communityTask rejections propagate (install-side narrow catches swallow them into a miss).
|
|
525
|
+
- **Dual indexes preserve install semantics (R2-3)**: the merged-domain `lookupInstalled` serves read paths; the full-domain `findById`/`lookupInstalledAll` ([primary…, full adapt set] including displaced entries) serves the three install/upgrade sites (resolveRegistryEntry / upgradePlugin / profile-ops desktop upgrade) - id-displaced entries remain installable and upgradable, byte-for-byte semantics. Primary-first short-circuits preserved (community failures never block primary hits).
|
|
526
|
+
- **Cycle dissolved + score-once**: six merge-layer symbols moved from market.ts into merged-market.ts, the 23 category labels moved from community.ts (re-exported in place so cli/tools stay untouched) - the market↔community import cycle is broken in all three directions; listMarket's scoring is single-pass (category short-circuit first, rank reads the table) with tie-breaks byte-identical to 0.9.67.
|
|
527
|
+
- **Zero behavior change pledge**: ADR-0003/0006/0008/0012/0013 untouched; a golden equivalence baseline (seven capture dimensions incl. paging/search/sort/probe badges) stayed byte-identical throughout.
|
|
528
|
+
- **Verification**: 1305 tests green, typecheck clean; measured on this machine against the real 4,460-entry catalog: first call 319ms → second same-identity call 65ms; search scoring 330ms cold → 8ms warm within a generation (40×). Implementation closed through three independent review rounds (16 plan-review + 7 execution-review findings, all "fixed and re-verified"; including one silent no-op fix caught by the reviewer's `git diff` discipline and corrected with a real diff). Implementation plan and ADR-0016 v3 on record (`docs/plans/`, `docs/adr/`).
|
|
529
|
+
|
|
498
530
|
### Fixed in 0.9.67 — operations card font blows up on mobile: join the 12px family + wrap self-consistency
|
|
499
531
|
|
|
500
532
|
- **Root cause (user phone screenshot)**: the operations rows `.dsvm-oprow` never declared a `font-size` — invisible on the desktop host base size, but on mobile browsers (base ≈15-16px) the whole row rendered nearly twice the size of the settings cards around it, clashing with the 12px family (`dsvm-kv` config rows, `dshm-hint`, `dsvm-fold`) on the same screen.
|
package/lib/client.js
CHANGED
|
@@ -494,6 +494,120 @@ var init_lightbox = __esm({
|
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
496
|
|
|
497
|
+
// src/client/window-drag.js
|
|
498
|
+
var window_drag_exports = {};
|
|
499
|
+
__export(window_drag_exports, {
|
|
500
|
+
DRAG_SLOP_MOUSE: () => DRAG_SLOP_MOUSE,
|
|
501
|
+
DRAG_SLOP_TOUCH: () => DRAG_SLOP_TOUCH,
|
|
502
|
+
PANEL_MARGIN: () => PANEL_MARGIN,
|
|
503
|
+
POS_KEY: () => POS_KEY,
|
|
504
|
+
clampPoint: () => clampPoint,
|
|
505
|
+
dragSlop: () => dragSlop,
|
|
506
|
+
grabOffset: () => grabOffset,
|
|
507
|
+
isDragTarget: () => isDragTarget,
|
|
508
|
+
loadPanelPos: () => loadPanelPos,
|
|
509
|
+
savePanelPos: () => savePanelPos,
|
|
510
|
+
solidifyColor: () => solidifyColor,
|
|
511
|
+
titlebarTopInset: () => titlebarTopInset
|
|
512
|
+
});
|
|
513
|
+
function dragSlop(pointerType) {
|
|
514
|
+
return pointerType === "touch" || pointerType === "pen" ? DRAG_SLOP_TOUCH : DRAG_SLOP_MOUSE;
|
|
515
|
+
}
|
|
516
|
+
function grabOffset(point, rect) {
|
|
517
|
+
return { dx: point.x - rect.left, dy: point.y - rect.top };
|
|
518
|
+
}
|
|
519
|
+
function titlebarTopInset({ titlebar = false, fullscreen = false, height } = {}) {
|
|
520
|
+
if (!titlebar || fullscreen) return 0;
|
|
521
|
+
const value = Number.parseFloat(height);
|
|
522
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
523
|
+
}
|
|
524
|
+
function normalizeSize(size) {
|
|
525
|
+
if (typeof size === "number") return { width: size, height: size };
|
|
526
|
+
const width = typeof size?.width === "number" ? size.width : 0;
|
|
527
|
+
const height = typeof size?.height === "number" ? size.height : 0;
|
|
528
|
+
return { width, height };
|
|
529
|
+
}
|
|
530
|
+
function finiteInset(v) {
|
|
531
|
+
return Number.isFinite(v) ? v : 0;
|
|
532
|
+
}
|
|
533
|
+
function clampPoint(point, viewport, size = 0, insets = ZERO_INSETS) {
|
|
534
|
+
const { width, height } = normalizeSize(size);
|
|
535
|
+
const insetLeft = finiteInset(insets?.left);
|
|
536
|
+
const insetRight = finiteInset(insets?.right);
|
|
537
|
+
const insetTop = finiteInset(insets?.top);
|
|
538
|
+
const insetBottom = finiteInset(insets?.bottom);
|
|
539
|
+
const minX = PANEL_MARGIN + insetLeft;
|
|
540
|
+
const minY = PANEL_MARGIN + insetTop;
|
|
541
|
+
const maxX = Math.max(minX, viewport.width - insetRight - PANEL_MARGIN - width);
|
|
542
|
+
const maxY = Math.max(minY, viewport.height - insetBottom - PANEL_MARGIN - height);
|
|
543
|
+
return {
|
|
544
|
+
x: Math.min(Math.max(point.x, minX), maxX),
|
|
545
|
+
y: Math.min(Math.max(point.y, minY), maxY)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function solidifyColor(computedColor) {
|
|
549
|
+
if (typeof computedColor !== "string") return null;
|
|
550
|
+
const srgb = COLOR_SRGB_RE.exec(computedColor.trim());
|
|
551
|
+
if (srgb) {
|
|
552
|
+
const alpha = srgb[4] !== void 0 ? Number.parseFloat(srgb[4]) : 1;
|
|
553
|
+
if (!Number.isFinite(alpha) || alpha < 0.05) return null;
|
|
554
|
+
const to255 = (v) => Math.round(Number.parseFloat(v) * 255);
|
|
555
|
+
return `rgb(${to255(srgb[1])}, ${to255(srgb[2])}, ${to255(srgb[3])})`;
|
|
556
|
+
}
|
|
557
|
+
const rgb = RGB_RE.exec(computedColor.trim());
|
|
558
|
+
if (rgb) {
|
|
559
|
+
const parts = rgb[1].split(",").map((p) => p.trim());
|
|
560
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
561
|
+
const r = Number.parseFloat(parts[0]);
|
|
562
|
+
const g = Number.parseFloat(parts[1]);
|
|
563
|
+
const b = Number.parseFloat(parts[2]);
|
|
564
|
+
const a = parts.length === 4 ? Number.parseFloat(parts[3]) : 1;
|
|
565
|
+
if (![r, g, b, a].every(Number.isFinite)) return null;
|
|
566
|
+
if (a < 0.05) return null;
|
|
567
|
+
return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
function loadPanelPos(storage) {
|
|
572
|
+
if (!storage || typeof storage.getItem !== "function") return null;
|
|
573
|
+
try {
|
|
574
|
+
const raw = storage.getItem(POS_KEY);
|
|
575
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
576
|
+
const parsed = JSON.parse(raw);
|
|
577
|
+
if (typeof parsed !== "object" || parsed === null || !Number.isFinite(parsed.x) || !Number.isFinite(parsed.y)) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return { x: parsed.x, y: parsed.y };
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function savePanelPos(storage, point) {
|
|
586
|
+
if (typeof point !== "object" || point === null || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
|
|
587
|
+
throw new TypeError(`invalid panel position: ${JSON.stringify(point)}`);
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
storage.setItem(POS_KEY, JSON.stringify({ x: point.x, y: point.y }));
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function isDragTarget(el) {
|
|
595
|
+
return Boolean(el?.closest?.("button"));
|
|
596
|
+
}
|
|
597
|
+
var POS_KEY, PANEL_MARGIN, DRAG_SLOP_MOUSE, DRAG_SLOP_TOUCH, ZERO_INSETS, RGB_RE, COLOR_SRGB_RE;
|
|
598
|
+
var init_window_drag = __esm({
|
|
599
|
+
"src/client/window-drag.js"() {
|
|
600
|
+
"use strict";
|
|
601
|
+
POS_KEY = "dshm-panel-pos";
|
|
602
|
+
PANEL_MARGIN = 8;
|
|
603
|
+
DRAG_SLOP_MOUSE = 6;
|
|
604
|
+
DRAG_SLOP_TOUCH = 10;
|
|
605
|
+
ZERO_INSETS = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
606
|
+
RGB_RE = /^rgba?\(([^)]+)\)$/i;
|
|
607
|
+
COLOR_SRGB_RE = /^color\(srgb\s+([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)(?:\s*\/\s*([0-9.]+))?\)$/i;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
497
611
|
// src/client/img-chain.js
|
|
498
612
|
var img_chain_exports = {};
|
|
499
613
|
__export(img_chain_exports, {
|
|
@@ -1668,6 +1782,7 @@ var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZE
|
|
|
1668
1782
|
var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
|
|
1669
1783
|
var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
|
|
1670
1784
|
var { lbStep: lbStep2, lbNeighbors: lbNeighbors2, swipeDir: swipeDir2 } = (init_lightbox(), __toCommonJS(lightbox_exports));
|
|
1785
|
+
var { dragSlop: dragSlop2, grabOffset: grabOffset2, titlebarTopInset: titlebarTopInset2, clampPoint: clampPoint2, solidifyColor: solidifyColor2, loadPanelPos: loadPanelPos2, savePanelPos: savePanelPos2, isDragTarget: isDragTarget2 } = (init_window_drag(), __toCommonJS(window_drag_exports));
|
|
1671
1786
|
var { WESERV_TIMEOUT_MS: WESERV_TIMEOUT_MS2, weservUrl: weservUrl2, serviceBucketOf: serviceBucketOf2, tierOrder: tierOrder2, nextTier: nextTier2, needsTimeout: needsTimeout2, preferredTier: preferredTier2, rememberSuccess: rememberSuccess2 } = (init_img_chain(), __toCommonJS(img_chain_exports));
|
|
1672
1787
|
var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
|
|
1673
1788
|
var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
|
|
@@ -1845,6 +1960,7 @@ var ZH = {
|
|
|
1845
1960
|
"pager.jump.ph": "\u9875\u53F7",
|
|
1846
1961
|
"panel.fullscreen": "\u5168\u5C4F",
|
|
1847
1962
|
"panel.restore": "\u8FD8\u539F",
|
|
1963
|
+
"panel.dragHint": "\u62D6\u62FD\u79FB\u52A8 \xB7 \u53CC\u51FB\u5207\u6362\u5168\u5C4F",
|
|
1848
1964
|
"lb.title": "\u63D2\u4EF6\u622A\u56FE\u9884\u89C8",
|
|
1849
1965
|
"lb.prev": "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09",
|
|
1850
1966
|
"lb.next": "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09",
|
|
@@ -2131,6 +2247,7 @@ var EN = {
|
|
|
2131
2247
|
"pager.jump.ph": "Page",
|
|
2132
2248
|
"panel.fullscreen": "Fullscreen",
|
|
2133
2249
|
"panel.restore": "Restore",
|
|
2250
|
+
"panel.dragHint": "Drag to move \xB7 Double-click to toggle fullscreen",
|
|
2134
2251
|
"lb.title": "Plugin screenshot preview",
|
|
2135
2252
|
"lb.prev": "Previous (\u2190)",
|
|
2136
2253
|
"lb.next": "Next (\u2192)",
|
|
@@ -2275,6 +2392,23 @@ function communityLabels(data) {
|
|
|
2275
2392
|
var CSS = `
|
|
2276
2393
|
.dshm-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-mask-3,rgba(15,23,42,.48));display:flex;align-items:center;justify-content:center;padding:max(24px,var(--dsh-windows-titlebar-height,0px)) 16px 24px;box-sizing:border-box}
|
|
2277
2394
|
.dshm-panel{width:min(920px,100%);height:min(680px,86vh);display:flex;flex-direction:column;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 86%,transparent);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);overflow:hidden;font-family:inherit;color:var(--dsw-alias-label-primary,inherit)}
|
|
2395
|
+
/* 0.9.69 \u8FD8\u539F\u6001\u62D6\u62FD\uFF1A.abs = \u6709\u4F4D\u7F6E\u8BB0\u5FC6\u65F6\u7684\u7EDD\u5BF9\u5B9A\u4F4D\u5F62\u6001\uFF08overlay \u4E3A inset:0 fixed\uFF0C
|
|
2396
|
+
\u7EDD\u5BF9\u5B9A\u4F4D\u5750\u6807\u5373\u89C6\u53E3\u5750\u6807\uFF09\uFF1B\u65E0\u8BB0\u5F55\u65F6\u9762\u677F\u4ECD\u8D70 overlay flex \u5C45\u4E2D\uFF0C\u6E32\u67D3\u8DEF\u5F84\u4E0E\u65E7\u7248\u9010\u5B57\u8282\u4E00\u81F4\u3002
|
|
2397
|
+
\u62D6\u62FD\u6001\u81EA\u6301\uFF080.9.62 \u5F71\u9662\u540C\u6B3E\u7EAA\u5F8B\uFF09\uFF1Abackground \u53D6\u8C03\u7528\u65B9\u624B\u52BF\u5F00\u59CB\u65F6\u5199\u5165\u7684 --dshm-drag-bg
|
|
2398
|
+
\uFF08getComputedStyle \u89E3\u6790\u540E\u7684\u6700\u7EC8\u8272\u5F3A\u5236 \u03B1=1\uFF0C\u89C1 window-drag.solidifyColor\uFF09\uFF0C\u76AE\u80A4\u5BF9
|
|
2399
|
+
--dsw-alias-* \u7684\u534A\u900F\u660E\u5B9A\u4E49\uFF08openbmc/uefi bg-base \u03B1=0.55 \u75C5\u7C7B\uFF09\u65E0\u6CD5\u518D\u8BA9\u62D6\u62FD\u4E2D\u9762\u677F\u900F\u5149\uFF1B
|
|
2400
|
+
blur \u5173\u95ED\u662F\u62D6\u62FD\u671F\u6700\u5927\u6027\u80FD\u6536\u76CA\uFF08\u6BCF\u5E27\u514D\u5927\u9762\u79EF\u91CD\u6EE4\u955C\uFF09\uFF0Ctransform \u2260 none \u7EF4\u6301\u5305\u542B\u5757\uFF0C
|
|
2401
|
+
fixed \u540E\u4EE3\u4E0D\u9003\u9038\u3002\u81EA\u6301\u4E09\u5904\u5DF2\u767B\u8BB0\u4E8E client-lightbox.test.mjs \u5B9A\u957F\u65AD\u8A00\uFF084\u21927\uFF09\uFF1B
|
|
2402
|
+
\u672C\u6CE8\u91CA\u523B\u610F\u4E0D\u7528\u6E05\u5355\u5173\u952E\u5B57\u5B57\u9762\u91CF\u2014\u2014\u8BA1\u6570\u5BF9\u6CE8\u91CA\u63D0\u53CA\u540C\u6837\u751F\u6548\uFF08\u8BC4\u5BA1 R1-2\uFF09\u3002 */
|
|
2403
|
+
.dshm-panel.abs{position:absolute;margin:0}
|
|
2404
|
+
.dshm-panel--dragging{cursor:grabbing;background:var(--dshm-drag-bg,Canvas)!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important;box-shadow:0 24px 64px rgba(2,6,23,.38)}
|
|
2405
|
+
/* 0.9.69 \u62D6\u62FD\u624B\u67C4\uFF1Auser-select/touch-action/tap-highlight \u65E0\u6761\u4EF6\u751F\u6548\uFF08\u5168\u5C4F\u53CC\u51FB\u5207\u6362\u65F6
|
|
2406
|
+
\u6807\u9898\u4E0D\u51FA\u73B0\u9009\u4E2D\u95EA\u73B0\uFF09\uFF1Bgrab \u5149\u6807\u4EC5\u5728\u300C\u8FD8\u539F\u6001\u4E14\u975E\u62D6\u62FD\u4E2D\u300D\u76F4\u63A5\u547D\u4E2D head\u2014\u2014\u62D6\u62FD\u4E2D head
|
|
2407
|
+
\u65E0\u81EA\u6709 cursor \u58F0\u660E\uFF0C\u81EA\u7136\u7EE7\u627F\u9762\u677F\u6839 .dshm-panel--dragging \u7684 grabbing\uFF08cursor \u6309
|
|
2408
|
+
\u300C\u5143\u7D20\u81EA\u6709\u58F0\u660E > \u7EE7\u627F\u300D\u89E3\u6790\uFF1Agrab \u82E5\u5728\u62D6\u62FD\u4E2D\u4ECD\u76F4\u63A5\u547D\u4E2D head\uFF0Cgrabbing \u6C38\u8FDC\u7EE7\u627F\u4E0D\u8FDB
|
|
2409
|
+
\u6210\u4E3A\u6B7B\u4EE3\u7801\u2014\u2014\u8BC4\u5BA1 R2-2\uFF09\u3002\u5168\u5C4F\u7981\u62D6\uFF08\u786C\u6027\u8981\u6C42\u2462\uFF09\uFF0C\u5149\u6807\u4E0D\u8BEF\u5BFC\uFF08\u8BC4\u5BA1 R1-6\uFF09\u3002 */
|
|
2410
|
+
.dshm-head{user-select:none;touch-action:none;-webkit-tap-highlight-color:transparent}
|
|
2411
|
+
.dshm-panel:not(.full):not(.dshm-panel--dragging) .dshm-head{cursor:grab}
|
|
2278
2412
|
/* 0.9.4 Windows Desktop \u5168\u5C4F\u4FEE\u590D\uFF1A\u58F3\u5728 html \u4E0A\u8BBE --dsh-windows-titlebar-height\uFF0840px\uFF0C
|
|
2279
2413
|
titleBarStyle:hidden + titleBarOverlay:42 \u7CFB\u7EDF\u7ED8\u5236 \u2014 \u25A1 \u2715\uFF09\uFF0C\u5176\u4E0A\u8FD8\u6709\u5168\u5BBD
|
|
2280
2414
|
-webkit-app-region:drag \u62D6\u62FD\u5E26\u2014\u2014\u8BE5\u5E26\u6309\u5E03\u5C40\u53C2\u4E0E\u62D6\u62FD\u5224\u5B9A\u3001\u65E0\u89C6 z-index/\u7ED8\u5236\u987A\u5E8F\uFF0C
|
|
@@ -4666,6 +4800,27 @@ function FsIcon({ full }) {
|
|
|
4666
4800
|
h("path", { d: "M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
4667
4801
|
);
|
|
4668
4802
|
}
|
|
4803
|
+
function readTitlebarInsets() {
|
|
4804
|
+
if (typeof document === "undefined" || !document.documentElement) {
|
|
4805
|
+
return { left: 0, right: 0, top: 0, bottom: 0 };
|
|
4806
|
+
}
|
|
4807
|
+
const htmlEl = document.documentElement;
|
|
4808
|
+
let height = "";
|
|
4809
|
+
try {
|
|
4810
|
+
height = window.getComputedStyle(htmlEl).getPropertyValue("--dsh-windows-titlebar-height");
|
|
4811
|
+
} catch {
|
|
4812
|
+
}
|
|
4813
|
+
return {
|
|
4814
|
+
left: 0,
|
|
4815
|
+
right: 0,
|
|
4816
|
+
top: titlebarTopInset2({
|
|
4817
|
+
titlebar: htmlEl.hasAttribute("data-windows-titlebar"),
|
|
4818
|
+
fullscreen: htmlEl.hasAttribute("data-fullscreen"),
|
|
4819
|
+
height
|
|
4820
|
+
}),
|
|
4821
|
+
bottom: 0
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4669
4824
|
function MarketPanel({ onClose }) {
|
|
4670
4825
|
const [tab, setTab] = useState("market");
|
|
4671
4826
|
const FS_KEY = "dshm-panel-fullscreen";
|
|
@@ -4686,6 +4841,162 @@ function MarketPanel({ onClose }) {
|
|
|
4686
4841
|
return next;
|
|
4687
4842
|
});
|
|
4688
4843
|
}, []);
|
|
4844
|
+
const panelRef = useRef(null);
|
|
4845
|
+
const headRef = useRef(null);
|
|
4846
|
+
const draggingRef = useRef(false);
|
|
4847
|
+
const suppressClickRef = useRef(false);
|
|
4848
|
+
const [pos, setPos] = useState(() => {
|
|
4849
|
+
try {
|
|
4850
|
+
return loadPanelPos2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
|
|
4851
|
+
} catch {
|
|
4852
|
+
return null;
|
|
4853
|
+
}
|
|
4854
|
+
});
|
|
4855
|
+
const fullRef = useRef(full);
|
|
4856
|
+
fullRef.current = full;
|
|
4857
|
+
const posRef = useRef(pos);
|
|
4858
|
+
posRef.current = pos;
|
|
4859
|
+
const reclamp = useCallback(() => {
|
|
4860
|
+
if (fullRef.current || draggingRef.current || !panelRef.current || typeof window === "undefined") return;
|
|
4861
|
+
const prev = posRef.current;
|
|
4862
|
+
if (!prev) return;
|
|
4863
|
+
const rect = panelRef.current.getBoundingClientRect();
|
|
4864
|
+
const next = clampPoint2(
|
|
4865
|
+
{ x: prev.x, y: prev.y },
|
|
4866
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
4867
|
+
{ width: rect.width, height: rect.height },
|
|
4868
|
+
readTitlebarInsets()
|
|
4869
|
+
);
|
|
4870
|
+
if (next.x !== prev.x || next.y !== prev.y) setPos(next);
|
|
4871
|
+
}, []);
|
|
4872
|
+
useLayoutEffect(() => {
|
|
4873
|
+
reclamp();
|
|
4874
|
+
}, [full, reclamp]);
|
|
4875
|
+
useEffect(() => {
|
|
4876
|
+
window.addEventListener("resize", reclamp);
|
|
4877
|
+
return () => window.removeEventListener("resize", reclamp);
|
|
4878
|
+
}, [reclamp]);
|
|
4879
|
+
useEffect(() => {
|
|
4880
|
+
const headEl = headRef.current;
|
|
4881
|
+
if (!headEl) return void 0;
|
|
4882
|
+
let dragging = false;
|
|
4883
|
+
let moved = false;
|
|
4884
|
+
let slop = 6;
|
|
4885
|
+
let originX = 0;
|
|
4886
|
+
let originY = 0;
|
|
4887
|
+
let grab = { dx: 0, dy: 0 };
|
|
4888
|
+
let rest = { x: 0, y: 0 };
|
|
4889
|
+
let size = { width: 0, height: 0 };
|
|
4890
|
+
let lastPoint = null;
|
|
4891
|
+
const bindGesture = () => {
|
|
4892
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
4893
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
4894
|
+
document.addEventListener("pointercancel", onPointerCancel);
|
|
4895
|
+
};
|
|
4896
|
+
const unbindGesture = () => {
|
|
4897
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
4898
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
4899
|
+
document.removeEventListener("pointercancel", onPointerCancel);
|
|
4900
|
+
};
|
|
4901
|
+
const frameStep = () => {
|
|
4902
|
+
if (!dragging || !lastPoint || !panelRef.current) return;
|
|
4903
|
+
const topLeft = clampPoint2(
|
|
4904
|
+
{ x: lastPoint.x - grab.dx, y: lastPoint.y - grab.dy },
|
|
4905
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
4906
|
+
size,
|
|
4907
|
+
readTitlebarInsets()
|
|
4908
|
+
);
|
|
4909
|
+
panelRef.current.style.transform = `translate3d(${Math.round(topLeft.x - rest.x)}px, ${Math.round(topLeft.y - rest.y)}px, 0)`;
|
|
4910
|
+
};
|
|
4911
|
+
const onPointerMove = (event) => {
|
|
4912
|
+
if (!dragging) return;
|
|
4913
|
+
if (!moved) {
|
|
4914
|
+
if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return;
|
|
4915
|
+
moved = true;
|
|
4916
|
+
document.body.style.userSelect = "none";
|
|
4917
|
+
panelRef.current.classList.add("dshm-panel--dragging");
|
|
4918
|
+
panelRef.current.style.willChange = "transform";
|
|
4919
|
+
}
|
|
4920
|
+
lastPoint = { x: event.clientX, y: event.clientY };
|
|
4921
|
+
if (typeof window.requestAnimationFrame === "function") window.requestAnimationFrame(frameStep);
|
|
4922
|
+
else frameStep();
|
|
4923
|
+
};
|
|
4924
|
+
const bakeFinal = (event) => {
|
|
4925
|
+
const panel = panelRef.current;
|
|
4926
|
+
const final = clampPoint2(
|
|
4927
|
+
{ x: event.clientX - grab.dx, y: event.clientY - grab.dy },
|
|
4928
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
4929
|
+
size,
|
|
4930
|
+
readTitlebarInsets()
|
|
4931
|
+
);
|
|
4932
|
+
panel.style.left = `${final.x}px`;
|
|
4933
|
+
panel.style.top = `${final.y}px`;
|
|
4934
|
+
panel.classList.remove("dshm-panel--dragging");
|
|
4935
|
+
panel.style.willChange = "";
|
|
4936
|
+
panel.style.transform = "";
|
|
4937
|
+
panel.style.removeProperty("--dshm-drag-bg");
|
|
4938
|
+
try {
|
|
4939
|
+
savePanelPos2(window.localStorage, final);
|
|
4940
|
+
} catch {
|
|
4941
|
+
}
|
|
4942
|
+
suppressClickRef.current = true;
|
|
4943
|
+
setPos(final);
|
|
4944
|
+
};
|
|
4945
|
+
const onPointerUp = (event) => {
|
|
4946
|
+
if (!dragging) return;
|
|
4947
|
+
dragging = false;
|
|
4948
|
+
draggingRef.current = false;
|
|
4949
|
+
unbindGesture();
|
|
4950
|
+
document.body.style.userSelect = "";
|
|
4951
|
+
if (!moved) return;
|
|
4952
|
+
bakeFinal(event);
|
|
4953
|
+
};
|
|
4954
|
+
const onPointerCancel = () => {
|
|
4955
|
+
if (!dragging) return;
|
|
4956
|
+
dragging = false;
|
|
4957
|
+
draggingRef.current = false;
|
|
4958
|
+
unbindGesture();
|
|
4959
|
+
document.body.style.userSelect = "";
|
|
4960
|
+
const panel = panelRef.current;
|
|
4961
|
+
panel.classList.remove("dshm-panel--dragging");
|
|
4962
|
+
panel.style.willChange = "";
|
|
4963
|
+
panel.style.transform = "";
|
|
4964
|
+
panel.style.removeProperty("--dshm-drag-bg");
|
|
4965
|
+
};
|
|
4966
|
+
const onPointerDown = (event) => {
|
|
4967
|
+
if (dragging) return;
|
|
4968
|
+
if (fullRef.current || event.button !== 0 || isDragTarget2(event.target)) return;
|
|
4969
|
+
const panel = panelRef.current;
|
|
4970
|
+
if (!panel) return;
|
|
4971
|
+
dragging = true;
|
|
4972
|
+
draggingRef.current = true;
|
|
4973
|
+
moved = false;
|
|
4974
|
+
lastPoint = null;
|
|
4975
|
+
originX = event.clientX;
|
|
4976
|
+
originY = event.clientY;
|
|
4977
|
+
slop = dragSlop2(event.pointerType);
|
|
4978
|
+
const rect = panel.getBoundingClientRect();
|
|
4979
|
+
grab = grabOffset2({ x: event.clientX, y: event.clientY }, rect);
|
|
4980
|
+
rest = { x: rect.left, y: rect.top };
|
|
4981
|
+
size = { width: rect.width, height: rect.height };
|
|
4982
|
+
const solid = solidifyColor2(window.getComputedStyle(panel).backgroundColor);
|
|
4983
|
+
if (solid) panel.style.setProperty("--dshm-drag-bg", solid);
|
|
4984
|
+
else panel.style.removeProperty("--dshm-drag-bg");
|
|
4985
|
+
if (headEl.setPointerCapture) {
|
|
4986
|
+
try {
|
|
4987
|
+
headEl.setPointerCapture(event.pointerId);
|
|
4988
|
+
} catch {
|
|
4989
|
+
}
|
|
4990
|
+
}
|
|
4991
|
+
bindGesture();
|
|
4992
|
+
};
|
|
4993
|
+
headEl.addEventListener("pointerdown", onPointerDown);
|
|
4994
|
+
return () => {
|
|
4995
|
+
headEl.removeEventListener("pointerdown", onPointerDown);
|
|
4996
|
+
unbindGesture();
|
|
4997
|
+
document.body.style.userSelect = "";
|
|
4998
|
+
};
|
|
4999
|
+
}, []);
|
|
4689
5000
|
const marketCommunity = useMarketData("community");
|
|
4690
5001
|
const marketPrimary = useMarketData("primary");
|
|
4691
5002
|
const markets = { community: marketCommunity, primary: marketPrimary };
|
|
@@ -4841,10 +5152,29 @@ function MarketPanel({ onClose }) {
|
|
|
4841
5152
|
{ className: full ? "dshm-overlay full" : "dshm-overlay", ...backdropCloseHandlers2(onClose) },
|
|
4842
5153
|
h(
|
|
4843
5154
|
"div",
|
|
4844
|
-
{
|
|
5155
|
+
{
|
|
5156
|
+
className: ["dshm-panel", full && "full", !full && pos && "abs"].filter(Boolean).join(" "),
|
|
5157
|
+
style: !full && pos ? { left: `${pos.x}px`, top: `${pos.y}px` } : void 0,
|
|
5158
|
+
ref: panelRef,
|
|
5159
|
+
onClick: (e) => e.stopPropagation()
|
|
5160
|
+
},
|
|
4845
5161
|
h(
|
|
4846
5162
|
"div",
|
|
4847
|
-
{
|
|
5163
|
+
{
|
|
5164
|
+
className: "dshm-head",
|
|
5165
|
+
title: full ? void 0 : lookup("panel.dragHint"),
|
|
5166
|
+
ref: headRef,
|
|
5167
|
+
onDoubleClick: (e) => {
|
|
5168
|
+
if (isDragTarget2(e.target)) return;
|
|
5169
|
+
toggleFull();
|
|
5170
|
+
},
|
|
5171
|
+
onClickCapture: (e) => {
|
|
5172
|
+
if (!suppressClickRef.current) return;
|
|
5173
|
+
suppressClickRef.current = false;
|
|
5174
|
+
e.stopPropagation();
|
|
5175
|
+
e.preventDefault();
|
|
5176
|
+
}
|
|
5177
|
+
},
|
|
4848
5178
|
h("span", { className: "dshm-title" }, lookup("title.full")),
|
|
4849
5179
|
h("span", { className: "dshm-head-divider", "aria-hidden": "true" }),
|
|
4850
5180
|
h(
|
package/lib/core/community.js
CHANGED
|
@@ -11,42 +11,15 @@ import { isAbsolute, join, relative } from 'node:path';
|
|
|
11
11
|
import { cacheRoot } from './env.js';
|
|
12
12
|
import { decodeUtf8Fatal, fetchBytesLimited, describeFetchFailure } from './httpx.js';
|
|
13
13
|
import { isExactVersion, npmLatest } from './versions.js';
|
|
14
|
-
import { communityOutcome } from './market.js';
|
|
14
|
+
import { communityOutcome } from './merged-market.js';
|
|
15
15
|
export const COMMUNITY_NPM_PACKAGE = 'dsh-plugin-catalog';
|
|
16
16
|
export const MAX_COMMUNITY_BYTES = 32 * 1024 * 1024;
|
|
17
17
|
export const MAX_COMMUNITY_ENTRIES = 30_000;
|
|
18
18
|
export const CATALOG_BODY_TIMEOUT_MS = 15_000;
|
|
19
19
|
export const COMMUNITY_CHAIN_BUDGET_MS = 30_000;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* GUI zoneChips 消费服务端数据,客户端内嵌副本(market-state.js)随 Task 8 删除。
|
|
24
|
-
*/
|
|
25
|
-
export const COMMUNITY_CATEGORY_LABELS = {
|
|
26
|
-
agi: 'AGI 架构探索',
|
|
27
|
-
ui: 'UI 增强',
|
|
28
|
-
usage: '用量与计费',
|
|
29
|
-
theme: '主题与外观',
|
|
30
|
-
model: '模型与账号接入',
|
|
31
|
-
identity: '身份与通信',
|
|
32
|
-
session: '会话与消息',
|
|
33
|
-
memory: '记忆',
|
|
34
|
-
tools: '工具与能力',
|
|
35
|
-
wsl: 'WSL 与 Windows 互操作',
|
|
36
|
-
browser: '浏览器与网页',
|
|
37
|
-
vision: '视觉与多模态',
|
|
38
|
-
voice: '语音与音频',
|
|
39
|
-
docs: '文档与渲染',
|
|
40
|
-
skill: '技能包',
|
|
41
|
-
workflow: '工作流与自动化',
|
|
42
|
-
git: 'Git 与代码评审',
|
|
43
|
-
notify: '通知与集成',
|
|
44
|
-
dev: '开发与运行时',
|
|
45
|
-
security: '安全与权限',
|
|
46
|
-
remote: '远程与移动端',
|
|
47
|
-
market: '插件市场与管理',
|
|
48
|
-
fun: '娱乐',
|
|
49
|
-
};
|
|
20
|
+
// 0.9.68(ADR-0016 R1-2):COMMUNITY_CATEGORY_LABELS 随 communitySummary 迁入 merged-market.ts
|
|
21
|
+
// (标签单一事实源不变,原位 re-export 保 cli.ts/tools.ts 既有 import 零改动)。
|
|
22
|
+
export { COMMUNITY_CATEGORY_LABELS } from './merged-market.js';
|
|
50
23
|
const TOP_LEVEL_KEYS = new Set(['name', 'url', 'source', 'updated', 'count', 'categories', 'plugins']);
|
|
51
24
|
/**
|
|
52
25
|
* 容器层严格校验(条目层由适配层负责):
|
|
@@ -120,6 +93,31 @@ async function atomicWrite(path, text) {
|
|
|
120
93
|
await writeFile(tmp, text);
|
|
121
94
|
await rename(tmp, path);
|
|
122
95
|
}
|
|
96
|
+
// ---------- ADR-0016 决策 1 L1:已解析目录 body memo ----------
|
|
97
|
+
// body 文件名 version-pin 不可变(catalog-{version}.json,atomicWrite 新版本写新文件),
|
|
98
|
+
// 同 (dir, version) 即同字节——memo 键即身份,零失效逻辑。cap=2(身份组合常态 ≤2;
|
|
99
|
+
// 每条 ≈15-20MB 解析对象,最坏 ≈40MB)。失败路径(parse/validate 失败)不写 memo。
|
|
100
|
+
const bodyMemo = new Map();
|
|
101
|
+
const BODY_MEMO_CAP = 2;
|
|
102
|
+
let communityBodyReads = 0;
|
|
103
|
+
function rememberBody(dir, version, catalog) {
|
|
104
|
+
const key = `${dir}§${version}`;
|
|
105
|
+
if (!bodyMemo.has(key) && bodyMemo.size >= BODY_MEMO_CAP) {
|
|
106
|
+
const oldest = bodyMemo.keys().next().value;
|
|
107
|
+
if (oldest !== undefined)
|
|
108
|
+
bodyMemo.delete(oldest);
|
|
109
|
+
}
|
|
110
|
+
bodyMemo.set(key, catalog);
|
|
111
|
+
}
|
|
112
|
+
/** 测试钩子:readCache 对 body 文件的实读次数(memo 命中不计;命名先例 _waitForCommunityBackgroundForTests)。 */
|
|
113
|
+
export function _communityBodyReadsForTests() {
|
|
114
|
+
return communityBodyReads;
|
|
115
|
+
}
|
|
116
|
+
/** 测试钩子:清 body memo 与计数(测试隔离用,latest-cache resetLatestCacheForTest 同款)。 */
|
|
117
|
+
export function _resetCommunityBodyMemoForTests() {
|
|
118
|
+
bodyMemo.clear();
|
|
119
|
+
communityBodyReads = 0;
|
|
120
|
+
}
|
|
123
121
|
/** 读取顺序(v8 定稿):meta 结构校验 → isExactVersion(meta.version) → body containment → 才读正文;损坏文件清理。 */
|
|
124
122
|
async function readCache(dir, pin) {
|
|
125
123
|
const metaPath = join(dir, pin ? metaPinName(pin) : META_LATEST);
|
|
@@ -149,6 +147,11 @@ async function readCache(dir, pin) {
|
|
|
149
147
|
const bodyPath = join(dir, bodyName(meta.version));
|
|
150
148
|
if (!contained(dir, bodyPath))
|
|
151
149
|
return null;
|
|
150
|
+
// L1 快路径:meta 每次实读(checkedAt 会更新),body 命中 memo 即免 5MB 级 parse+校验
|
|
151
|
+
const memoHit = bodyMemo.get(`${dir}§${meta.version}`);
|
|
152
|
+
if (memoHit)
|
|
153
|
+
return { meta, catalog: memoHit };
|
|
154
|
+
communityBodyReads += 1;
|
|
152
155
|
let data;
|
|
153
156
|
try {
|
|
154
157
|
data = JSON.parse(await readFile(bodyPath, 'utf8'));
|
|
@@ -162,6 +165,7 @@ async function readCache(dir, pin) {
|
|
|
162
165
|
await rm(bodyPath, { force: true }).catch(() => undefined);
|
|
163
166
|
return null;
|
|
164
167
|
}
|
|
168
|
+
rememberBody(dir, meta.version, v.catalog);
|
|
165
169
|
return { meta, catalog: v.catalog };
|
|
166
170
|
}
|
|
167
171
|
async function writeMeta(dir, pin, meta) {
|