dsh-m 0.9.61 → 0.9.62
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 +16 -0
- package/docs/DESIGN.md +3 -2
- package/lib/client.js +61 -5
- package/lib/core/host-api.js +24 -0
- package/lib/core/primary-screenshots.js +142 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,14 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.62 修复:灯箱控件浅色模式下不可见——影院遮罩自持 + 控件深色玻璃芯片 + 加载转圈/淡入
|
|
12
|
+
|
|
13
|
+
- **根因(2026-10-09 用户浅/深双模式实拍)**:dsh-skins ADR-0007 浮层毛玻璃并集按 `[role="dialog"][aria-modal="true"]` 命中灯箱根(灯箱根带 dialog 语义),浅色模式下 86% 暖白 tint + blur14 把影院式黑遮罩改成了浅色毛玻璃;而 ‹›/✕ 沿用基础 `.dsvm-btn` 白色半透配方(`rgba(255,255,255,.14)` 底 + 白字白边),浅底上整体隐形。深色模式走皮肤 `rgb(18,18,26)` 分支尚可读,故「浅色看不清、深色还行」。无皮肤的原生形态(黑遮罩恒在)无症状——这正是它漏网到 0.9.61 的原因。
|
|
14
|
+
- **双侧修复**:① dsh-m 侧(本版):灯箱 `background` 加 `!important` 自持——「深色遮罩 + 浅色控件」是影院面的组件自有可读性不变量,不随颜色模式/皮肤翻转;marketplace 社区皮肤不可枚举,不变量必须组件自己兜底(`!important` 清单定长 4,回归门钉住,新增须显式登记)。② dsh-skins 侧:三皮肤 `[role="dialog"][aria-modal="true"]` 追加 `:not(.dsvm-lightbox)` 豁免(1.5.1,ADR-0007 amendment)——语义上「弹窗毛玻璃」本就不该染指图片影院。
|
|
15
|
+
- **控件深色玻璃芯片**:‹›/✕ 从裸透明白改为与底部 pill 同族的深色芯片(slate-900 55% + 白描边 + blur8 + 投影)——深遮罩上靠描边/blur/投影出层次,浅遮罩上芯片本身就是对比面,任何颜色模式与皮肤下都可读;显式 `:hover` 提亮(特异性 0,3,0 压基础 `.dsvm-btn:hover` 0,2,0,免疫源顺序——评审 R1 同族教训)。
|
|
16
|
+
- **加载指示与淡入(UX 补强)**:灯箱走链期间(weserv 慢速/8s 守卫换层/直连大图)此前是空黑黑洞,现转圈占位(`useImgChain` 暴露 `settled`;点转圈视为点遮罩可关——急躁关闭是合理逃生门);图片 onLoad 后 160ms 淡入换场;`prefers-reduced-motion` 下停用淡入、转圈降速。
|
|
17
|
+
- **验证**:1283 项测试全绿、typecheck 零错误;`client-lightbox.test.mjs` 新增 6 项结构锚(遮罩 `!important` 定长清单/芯片规则与显式 hover/转圈门控/淡入类名/settled 暴露/reduced-motion),0.9.60 批次既有锚(portal/复合选择器/拖拽带/焦点还原/序号 key)全数保持。
|
|
18
|
+
|
|
11
19
|
### 0.9.61 修复:大陆浏览器拉不到插件截图/图标——图片加载链 weserv 优先双兜底
|
|
12
20
|
|
|
13
21
|
- **根因(2026-10-09 实测)**:截图(raw.githubusercontent.com)与图标(github.com 头像)由用户浏览器直连拉取——服务器侧 200/0.2s,大陆浏览器路径时断时通,当日故障样本 dsh-wallpaper-engine 五图(22.5MB,含 10.4MB GIF)缩略图条与灯箱全空;0.9.60 已排除灯箱 v2 回归,图标「能显示」实为本地字母兜底。对标 dsh-market 1.66.14:缩略图无条件 weserv(其源码实测大陆 1.39s/23KB vs 原图 41KB)但灯箱直连零兜底——大陆点开大图同款黑洞。
|
|
@@ -453,6 +461,14 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
453
461
|
|
|
454
462
|
## English
|
|
455
463
|
|
|
464
|
+
### Fixed in 0.9.62 — lightbox controls invisible in light color schemes: self-owned cinema backdrop + dark glass control chips + loading spinner/fade
|
|
465
|
+
|
|
466
|
+
- **Root cause (user photos, both color schemes, 2026-10-09)**: the dsh-skins ADR-0007 overlay-frost union targets `[role="dialog"][aria-modal="true"]` - which matches the lightbox root (it carries dialog semantics). In light schemes the 86% warm-white tint + blur14 turned the cinema-black backdrop into light frosted glass, while ‹›/✕ reuse the base `.dsvm-btn` translucent-white recipe (`rgba(255,255,255,.14)` fill, white glyph/border) - invisible on a light surface. Dark schemes hit the skin's `rgb(18,18,26)` branch and stayed readable, hence "light mode unreadable, dark mode okay". Vanilla installs (backdrop always near-black) never showed it - which is how it slipped past 0.9.61.
|
|
467
|
+
- **Two-sided fix**: ① dsh-m side (this release): the lightbox `background` is now `!important`-owned - "dark backdrop + light controls" is the component's own readability invariant for a cinema surface and must not flip with color schemes or skins; marketplace community skins are un-enumerable, so the component must defend itself (the `!important` inventory is pinned at a fixed length of 4 by the regression gate; additions must register explicitly). ② dsh-skins side: all three skins exempt `.dsvm-lightbox` via `:not(...)` (1.5.1, ADR-0007 amendment) - semantically a "dialog frost" never should have tinted an image theater.
|
|
468
|
+
- **Dark glass control chips**: ‹›/✕ move from bare translucent white to a chip in the same family as the bottom pill (slate-900 55% + white border + blur8 + shadow) - on dark backdrops the border/blur/shadow provide edge definition, on light backdrops the chip itself is the contrast surface; readable under any scheme or skin. Explicit `:hover` brightening (specificity 0,3,0 beats base `.dsvm-btn:hover` 0,2,0 regardless of source order - the R1 family lesson).
|
|
469
|
+
- **Loading indicator & fade (UX)**: while the image chain runs (slow weserv / 8s guard tier-swap / large direct original) the lightbox used to be a silent black void; it now shows a spinner (`useImgChain` exposes `settled`; clicking the spinner counts as a backdrop click - an impatient close is a legitimate escape hatch). The image fades in over 160ms on onLoad; under `prefers-reduced-motion` the fade is disabled and the spinner slows down.
|
|
470
|
+
- **Verification**: 1283 tests green, typecheck clean; `client-lightbox.test.mjs` gains 6 structural anchors (backdrop `!important` fixed-length inventory / chip rules with explicit hover / spinner gating / fade class / settled exposure / reduced-motion); all 0.9.60-era anchors (portal / compound selectors / titlebar offset / focus restore / index keys) retained.
|
|
471
|
+
|
|
456
472
|
### Fixed in 0.9.61 — screenshots/icons unreachable from mainland browsers: weserv-first dual-fallback image chain
|
|
457
473
|
|
|
458
474
|
- **Root cause (measured 2026-10-09)**: screenshots (raw.githubusercontent.com) and icons (github.com avatars) load browser-direct - fine from the server (200/0.2s), intermittently dead from mainland browsers. That day's failure sample dsh-wallpaper-engine (5 images, 22.5MB incl. a 10.4MB GIF) rendered an empty strip and a black lightbox; 0.9.60 ruled out a lightbox-v2 regression, and "working" icons were the local letter fallback. Benchmark dsh-market 1.66.14: thumbnails unconditionally via weserv (their in-source mainland measurement: 1.39s/23KB vs 41KB original) but the lightbox loads direct with zero fallback - the same mainland black hole.
|
package/docs/DESIGN.md
CHANGED
|
@@ -71,12 +71,12 @@
|
|
|
71
71
|
|
|
72
72
|
- **双层模型**:主清单(§2.1–2.4 全部语义不变)之上叠加只读的**社区清单**——awesome-dsh-plugin 维护的全量社区目录(4,377+ 条,日增约 62)。**合并市场 = 主清单 ∪ 社区清单去重(主清单恒优先)**,GUI / agent 工具 / CLI 三端同源同语义。本节修订 §9.1 Q25「不做运行时多源合并」,架构决策见 [ADR-0003](./adr/0003-community-catalog-merge.md)。
|
|
73
73
|
- **数据锚定 npm 包(Q39)**:`dsh-plugin-catalog`(CC0-1.0,版本 `YYYY.MDD.RUN`)为唯一数据锚。获取链:`registry.npmjs.org` dist-tags 探测最新版本(几 KB)→ **版本未变不重拉正文(版本号即 revalidate 验证器)**→ jsDelivr 按精确版本直取 `plugins.json`(版本 pin 后内容不可变,CDN 缓存无害)→ npmmirror files API → unpkg 按精确版本直取(末位兜底;均为纯 JSON 文件线路,不引入 tar 解包依赖)。npm 精确版本不可变 = 完整性锚;**不做包内快照兜底**(对日增 62 条的目录,过期数据不得冒充最新——陈旧必须显式标注),失败如实报错(发生了什么 / 为什么 / 现在怎么办)。
|
|
74
|
-
- **目录适配层**:原生条目 → 收录条目。`description.zh`(缺省回退 `en`;>500 字符截断并计数)→ description;`npm` 非空 → `source: npm`(`url` 为 github.com 形态时同时派生 `github` owner/repo,子包取 repo 根,0.9.51 起),否则 `source: github`(owner/repo 取自 url);`url` → homepage;tags 置空;**分类保留原生值不转译**(开放集,Q40
|
|
74
|
+
- **目录适配层**:原生条目 → 收录条目。`description.zh`(缺省回退 `en`;>500 字符截断并计数)→ description;`npm` 非空 → `source: npm`(`url` 为 github.com 形态时同时派生 `github` owner/repo,子包取 repo 根,0.9.51 起),否则 `source: github`(owner/repo 取自 url);`url` → homepage;tags 置空;**分类保留原生值不转译**(开放集,Q40);Community Catalog 的 `stars/downloads/capabilities/screenshots` 等不进收录条目本体,仅在 CommunityEntry/市场层作为旁路数据透传;Primary 截图另按 §2.6 从作者仓库读取,不进入 MarketItem。id 合成:原生 `name` 是裸名(实测 4,377 条中 190 个重名),合成小写 `owner--name` 形态并满足 v1 id 规则(非法字符折叠为 `-`,超 64 字符截断加哈希尾缀),适配层内冲突追加序号。
|
|
75
75
|
- **校验语义(Q43)**:社区清单独立校验——容器层严格(顶层键白名单、`plugins` 数组、32 MiB / 30,000 条上限,超限**整份拒收**)+ 条目层宽松(单条不合格**跳过 + 计数**,汇入 warnings,不整份拒绝)。主清单严格校验(§2.2)不变。**无 npm 的 monorepo 子包条目跳过并计数**(repo 根不是正确安装目标);有 npm 的子包条目按 npm 安装、正常收录;tarball-only 条目按 github 源收录(dsh-m 不安装 tarball)。
|
|
76
76
|
- **分类(Q40)**:精选分类(5 个,主清单 schema 不变)∪ 社区分类(开放集)。已知 20 个社区分类带中文标签进筛选栏「社区」组;`ui`/`tools`/`market` 三 id 与精选同名、共享过滤桶;上游新增的未知分类原样渲染进「社区·新分类」临时组,发版收录标签。
|
|
77
77
|
- **降级语义(Q42)**:主清单 unavailable + 社区可用 → 显示社区条目 + 顶部错误横幅 + 安装不禁用;社区 unavailable → 主清单照常 + 静默 notice;探测失败时回落 `<ns>/awesome/` **运行时缓存**并显式标注 stale(绝不冒充 ready)——运行时缓存语义与主清单自定义源一致,被禁止的只是**包内快照**;社区**绝不**回退主清单伪装。(**0.9.15 修订**:SWR 常态化后市场页不再显示「缓存快照」临时横幅——stale 标注移至设置页社区卡,「绝不冒充 ready / 过期可被观察到」的诚实约束不变。)
|
|
78
78
|
- **开关与缓存**:`communityCatalog`(默认 true,volatile live 生效)+ `communityCatalogPin`(可选锁 npm 版本);CLI 用 `DSHM_COMMUNITY_CATALOG=0` 退出。社区缓存放 `<namespace>/awesome/` 子目录(不参与 `pruneCaches` 的顶层 `*.json` 清理),正文按版本文件缓存,`fetchedAt`/版本号/线路进设置页展示;TTL 复用 `cacheTtlMin`(只约束 dist-tags 探测频率)。
|
|
79
|
-
- **市场行为(Q45/Q46)**:默认排序主清单置顶(组内维持原顺序)+ 社区按 30 天下载量降序(无数据按名称);搜索同时匹配中英文描述;「只看主清单」chip 常驻筛选栏首位。**探测边界**:市场浏览页对社区 npm 条目做 latest 探测(registry 无配额限制);社区 github 条目**不做**浏览页 REST 探测(GitHub 匿名 60 次/小时在 50 条/页 × 2 调用下不可控),更新检查收敛到详情/安装时的按需解析——配额耗尽时已有可读提示(versions.ts `githubRateLimitMessage`)。**已装页豁免**:探测对象受已装数量天然约束,继续探测;计量在**真实 GitHub HTTP 请求层**(一次 `githubLatestTag` ≈ 1–3 个请求:release 路径 1–2 个、fallback 路径 releases→tags→commits 最多 3 个):单请求 ≤25 次、宿主进程滚动 1 小时 ≤50 次(**仅作用于被动探测**——用户主动 install/upgrade/诊断不经此预算)、同仓库 in-flight single-flight、超限标 `latestError` 不阻塞列表——否则用户装的社区 github 插件永远没有更新徽标。独立 CLI 进程不共享宿主内预算,文档如实标注 best-effort(并发场景不承诺 60/h 绝不耗尽)。能力披露(capabilities/红线)只在详情折叠区展示(缺省 = 未扫描 ≠ 未检出),卡片不打标(Q44
|
|
79
|
+
- **市场行为(Q45/Q46)**:默认排序主清单置顶(组内维持原顺序)+ 社区按 30 天下载量降序(无数据按名称);搜索同时匹配中英文描述;「只看主清单」chip 常驻筛选栏首位。**探测边界**:市场浏览页对社区 npm 条目做 latest 探测(registry 无配额限制);社区 github 条目**不做**浏览页 REST 探测(GitHub 匿名 60 次/小时在 50 条/页 × 2 调用下不可控),更新检查收敛到详情/安装时的按需解析——配额耗尽时已有可读提示(versions.ts `githubRateLimitMessage`)。**已装页豁免**:探测对象受已装数量天然约束,继续探测;计量在**真实 GitHub HTTP 请求层**(一次 `githubLatestTag` ≈ 1–3 个请求:release 路径 1–2 个、fallback 路径 releases→tags→commits 最多 3 个):单请求 ≤25 次、宿主进程滚动 1 小时 ≤50 次(**仅作用于被动探测**——用户主动 install/upgrade/诊断不经此预算)、同仓库 in-flight single-flight、超限标 `latestError` 不阻塞列表——否则用户装的社区 github 插件永远没有更新徽标。独立 CLI 进程不共享宿主内预算,文档如实标注 best-effort(并发场景不承诺 60/h 绝不耗尽)。能力披露(capabilities/红线)只在详情折叠区展示(缺省 = 未扫描 ≠ 未检出),卡片不打标(Q44,防警告疲劳);社区截图仅详情层加载且其 GitHub 图床白名单由上游保证;Primary 作者清单按 §2.6 由 Host 与客户端消费端各自校验。
|
|
80
80
|
|
|
81
81
|
### 2.6 双清单分区市场与交互升级(0.7.0 grilling 定稿 2026-09-29)
|
|
82
82
|
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
- 搜索:相关性加权管线——NFKC 归一化 + 中西文边界插空格 + 字段权重(name/npm > owner > 描述 > 分类)+ 命中类型加分 + 按条目缓存归一化结果;输入 250ms debounce + IME composition 全程处理 + draft/已提交 query 分离。**0.9.25 起搜索跨区全局**(两分区搜索框同语义):摘要行报两分区精确命中数(`sourceCounts`),结果页内按精选段置顶分组,分类 chips 搜索态隐藏、「筛选」按钮随摘要行保留(页大小组)。**0.9.26 补充**:GUI 跨区搜索通道精选命中稳定前置(`curatedFirst`,分区内相关序不变;tools/CLI 不传,三端同序不变)——摘要行全局计数与首页所见一致,弱命中精选不被 downloads tie-break 埋进后页。
|
|
93
93
|
- 描述 5 行钳制(真实溢出才显示展开钮);**数据层双语**——描述按 UI 语言取 zh/en(数据现成),UI 完整双语字典列 backlog。
|
|
94
94
|
- **详情 = Modal 且为卡片超集**(「detail 显示少于摘要就是倒退」):byline / 分类 / 收录日期 / 下载量窗口三要素(计数 + 窗口 + 核对时间)/ 描述全文 / 截图灯箱(←→/Esc、**禁自动轮播**)/ 能力披露 + 红线(**默认收起**)/ 安装命令折叠行(0.9.14 起按上下文显隐:desktop 上下文与已安装条目整行隐藏——命令两来源恒为 `--profile web` 语义,desktop 装机走弹窗内安装按钮)/ deprecated 替代链接;精选条目超集另加 verified 与 tags。**安装确认走 Modal**。安装信息就地显示(0.9.15):进行中进度行与终态结果行挂 Modal 内、底层同源进度行让位——展示派生自全局操作记录,§2.6「状态不挂卡片」所有权模型不变。卡片瘦身:能力披露、截图、安装命令全部迁出卡片(Q44 精神「只在详情层、默认收起」不变,载体升级)。
|
|
95
|
+
- **详情截图来源(2026-10-09,[ADR-0015](./adr/0015-primary-screenshot-manifest.md))**:Community live 条目沿用 `CommunityEntry.screenshots`(来自版本化 `dsh-plugin-catalog`);Primary DetailModal 调 GUI Host method `primary-screenshots({ id })`,Host 按 active Primary Registry 精确查 id 与 `github`,仅当 `github` 存在时懒读仓库根 `screenshots.json`(HEAD)。清单为顶层最多 8 个仓库相对路径;缺 github、缺文件、无效内容或网络失败均返回空列表,不回退 Community Catalog、README 或 npm repository。清单只在 Modal state/Host 内存缓存(成功结果 10 分钟、空/失败结果负缓存 1 分钟),不进入 `MarketItem`、市场 localStorage 快照、收藏快照、Agent 工具或 CLI;最终图片仍经现有 `safeScreenshots` 与 [ADR-0014](./adr/0014-image-chain-weserv-first.md) 的 `useImgChain`。`source='primary'` 是 market 分区参数,与 `primary-screenshots` Host method 分离;现有 Community Zone 可独立加载社区数据。
|
|
95
96
|
- **全局操作记录**:每个变更操作一条 record(`queued / running / input=冲突待决 / done / warned / failed`),**状态不挂卡片**——翻页、搜索、切 tab 不丢;**localStorage 持久化队列**,宿主重载恢复时逐条校验「此刻仍成立才执行,否则报告」;覆盖安装、升级、卸载、开关。已装侧跟随一致化:「全部更新 (N)」批量入口 + tab 更新红点 + 卡片视觉与发现侧同体系;组管理、个人备注列 backlog。
|
|
96
97
|
- **收藏**:浏览器 localStorage 本地收藏(不进 profile、不进服务端);收藏区 stale 条目(目录中已下架)单独提示 + 一键清理。
|
|
97
98
|
- **agent/CLI 契约(允许破坏性变更)**:`dshm_search` 参数改为 `query / category / source('primary'|'community'|'all',默认 all,与 GUI 分区对齐) / limit(默认 10,clamp 1–80) / offset(真翻页)`;`primary_only` 删除。输出补 `community` 标记与 `downloads/stars`;社区分类直出中文标签(不再回退英文 slug);工具描述整体重写(真实规模 + 翻页语义,删「registry is curated & small」)。CLI 对齐 `--source / --offset / --limit`,默认 10 条。
|
package/lib/client.js
CHANGED
|
@@ -2357,20 +2357,43 @@ var CSS = `
|
|
|
2357
2357
|
\u8BC4\u5BA1 R1\uFF1A\u2715/\u2039\u203A \u7528 .dsvm-btn.dsvm-lb* \u590D\u5408\u9009\u62E9\u5668\u2014\u2014\u57FA\u7840 .dsvm-btn \u89C4\u5219\u6E90\u987A\u5E8F\u9760\u540E
|
|
2358
2358
|
\uFF08padding:5px 14px/font-size:13px/radius:8px\uFF09\uFF0C\u540C\u7279\u5F02\u6027\u4E0B\u4F1A\u53CD\u6740\u5355\u7C7B\u89C4\u5219\uFF08\u5B9E\u6D4B\u5B57\u5F62 13px\u226026px\uFF09\u3002
|
|
2359
2359
|
\u8BC4\u5BA1 R2\uFF1Atop \u8BA9\u51FA Windows Desktop \u62D6\u62FD\u5E26 var(--dsh-windows-titlebar-height)\uFF080.9.4 \u5148\u4F8B\uFF0C
|
|
2360
|
-
\u58F3\u9876 -webkit-app-region:drag \u541E\u70B9\u51FB\u3001\u65E0\u89C6 z-index\uFF09\uFF1BWeb \u65E0\u6B64\u53D8\u91CF\u56DE\u843D 0px \u884C\u4E3A\u4E0D\u53D8\u3002
|
|
2361
|
-
.
|
|
2360
|
+
\u58F3\u9876 -webkit-app-region:drag \u541E\u70B9\u51FB\u3001\u65E0\u89C6 z-index\uFF09\uFF1BWeb \u65E0\u6B64\u53D8\u91CF\u56DE\u843D 0px \u884C\u4E3A\u4E0D\u53D8\u3002
|
|
2361
|
+
0.9.62 \u5F71\u9662\u906E\u7F69\u81EA\u6301\uFF1A\u706F\u7BB1\u662F\u56FE\u7247\u5F71\u9662\u9762\uFF0C\u300C\u6DF1\u8272\u906E\u7F69 + \u6D45\u8272\u63A7\u4EF6\u300D\u662F\u7EC4\u4EF6\u81EA\u6709\u53EF\u8BFB\u6027
|
|
2362
|
+
\u4E0D\u53D8\u91CF\uFF0C\u4E0D\u968F\u989C\u8272\u6A21\u5F0F/\u76AE\u80A4\u7FFB\u8F6C\u3002\u5B9E\u8BC1\u78B0\u649E\uFF1Adsh-skins ADR-0007 \u6D6E\u5C42\u6BDB\u73BB\u7483\u5E76\u96C6\u6309
|
|
2363
|
+
[role=dialog][aria-modal=true] \u547D\u4E2D\u672C\u6839\uFF08\u6D45\u8272\u6A21\u5F0F 86% \u6696\u767D tint+blur14\uFF09\uFF0C\u767D\u8272\u63A7\u4EF6
|
|
2364
|
+
\u5728\u6D45\u5E95\u4E0A\u4E0D\u53EF\u89C1\uFF08\u7528\u6237\u53CC\u6A21\u5F0F\u5B9E\u62CD\uFF09\u3002\u53CC\u4FA7\u4FEE\u590D\u2014\u2014\u76AE\u80A4\u4FA7 :not(.dsvm-lightbox) \u8C41\u514D
|
|
2365
|
+
\uFF08dsh-skins 1.5.1 \u8D77\uFF09\uFF1B\u672C\u4FA7 background !important \u81EA\u6301\uFF1Amarketplace \u793E\u533A\u76AE\u80A4\u4E0D\u53EF
|
|
2366
|
+
\u679A\u4E3E\uFF0C\u5F71\u9662\u4E0D\u53D8\u91CF\u5FC5\u987B\u7531\u7EC4\u4EF6\u81EA\u5DF1\u515C\u5E95\u3002!important \u6E05\u5355\u5B9A\u957F\uFF08\u6CE8\u91CA\u63D0\u53CA\xD72 + \u672C\u89C4\u5219
|
|
2367
|
+
+ sidebar footer \u517C\u5BB9\u89C4\u5219\uFF09\uFF0C\u65B0\u589E\u987B\u6539 client-lightbox \u65AD\u8A00\u663E\u5F0F\u767B\u8BB0\uFF0C\u6CE8\u91CA\u5373\u5B88\u536B\u3002 */
|
|
2368
|
+
.dsvm-lightbox{position:fixed;top:var(--dsh-windows-titlebar-height,0px);left:0;right:0;bottom:0;z-index:2147483200;background:rgba(0,0,0,.88)!important;display:flex;align-items:center;justify-content:center}
|
|
2362
2369
|
.dsvm-lightbox img{max-width:min(92vw,1500px);max-height:74vh;border-radius:8px;cursor:zoom-out;box-shadow:0 10px 44px rgba(0,0,0,.55);display:block}
|
|
2363
2370
|
.dsvm-btn:focus-visible{outline:2px solid rgba(255,255,255,.75);outline-offset:2px}
|
|
2364
2371
|
.dsvm-btn.dsvm-lbclose{position:absolute;top:12px;right:12px;width:38px;height:38px;padding:0;border-radius:10px;display:flex;align-items:center;justify-content:center}
|
|
2365
2372
|
.dsvm-btn.dsvm-lbarrow{position:absolute;top:50%;transform:translateY(-50%);width:46px;height:60px;padding:0;font-size:26px;line-height:1;border-radius:10px}
|
|
2366
2373
|
.dsvm-lbarrow.prev{left:12px}
|
|
2367
2374
|
.dsvm-lbarrow.next{right:12px}
|
|
2375
|
+
/* 0.9.62 \u63A7\u4EF6\u6DF1\u8272\u73BB\u7483\u82AF\u7247\uFF1A\u2039\u203A/\u2715 \u539F\u968F\u57FA\u7840 .dsvm-btn \u88F8\u900F\u660E\u767D\uFF08rgba(255,255,255,.14) \u5E95
|
|
2376
|
+
+ \u767D\u5B57\u767D\u8FB9\uFF09\uFF0C\u906E\u7F69\u4E00\u65E6\u88AB\u4EFB\u4F55\u6765\u6E90\u6539\u6D45\u5373\u6574\u4F53\u9690\u5F62\uFF08\u672C\u6B21\u4E8B\u6545\u7684\u53E6\u4E00\u534A\uFF09\u3002\u6539\u7528\u4E0E\u5E95\u90E8
|
|
2377
|
+
pill \u540C\u65CF\u7684\u6DF1\u8272\u82AF\u7247\uFF08slate-900 55% + \u767D\u63CF\u8FB9 + blur + \u6295\u5F71\uFF09\u2014\u2014\u6DF1\u906E\u7F69\u4E0A\u9760\u63CF\u8FB9/blur/\u6295\u5F71
|
|
2378
|
+
\u51FA\u5C42\u6B21\uFF0C\u6D45\u906E\u7F69\u4E0A\u82AF\u7247\u672C\u8EAB\u5C31\u662F\u5BF9\u6BD4\u9762\uFF0C\u4EFB\u4F55\u989C\u8272\u6A21\u5F0F\u4E0E\u76AE\u80A4\u4E0B\u90FD\u53EF\u8BFB\uFF1Bhover \u63D0\u4EAE\u3002
|
|
2379
|
+
\u663E\u5F0F :hover\uFF08\u7279\u5F02\u6027 0,3,0\uFF09\u538B\u8FC7\u57FA\u7840 .dsvm-btn:hover\uFF080,2,0\uFF09\u2014\u2014\u5373\u4FBF\u672C\u5757\u6392\u5728\u57FA\u7840\u89C4\u5219
|
|
2380
|
+
\u4E4B\u524D\u4E5F\u4E0D\u88AB\u6E90\u987A\u5E8F\u53CD\u6740\uFF08\u8BC4\u5BA1 R1 \u540C\u65CF\u6559\u8BAD\uFF09\u3002 */
|
|
2381
|
+
.dsvm-btn.dsvm-lbclose,.dsvm-btn.dsvm-lbarrow{background:rgba(15,23,42,.55);border-color:rgba(255,255,255,.28);box-shadow:0 4px 16px rgba(0,0,0,.35);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
|
|
2382
|
+
.dsvm-btn.dsvm-lbclose:hover,.dsvm-btn.dsvm-lbarrow:hover{background:rgba(15,23,42,.8);border-color:rgba(255,255,255,.5)}
|
|
2368
2383
|
.dsvm-lbbar{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;align-items:center;gap:12px;max-width:94vw;background:rgba(15,23,42,.62);border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:7px 16px;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px)}
|
|
2369
2384
|
.dsvm-lbcount{color:rgba(255,255,255,.82);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2370
2385
|
.dsvm-lbhint{color:rgba(255,255,255,.55);font-size:11px;white-space:nowrap}
|
|
2371
2386
|
.dsvm-lbdots{display:flex;gap:2px}
|
|
2372
2387
|
.dsvm-lbdot{box-sizing:content-box;width:8px;height:8px;padding:5px;border-radius:50%;background:rgba(255,255,255,.32);background-clip:content-box;cursor:pointer}
|
|
2373
2388
|
.dsvm-lbdot.on{background:#fff;background-clip:content-box}
|
|
2389
|
+
/* 0.9.62 \u52A0\u8F7D\u6307\u793A\u4E0E\u6DE1\u5165\uFF1A\u706F\u7BB1\u8D70\u94FE\u671F\u95F4\uFF08weserv \u6162\u901F/8s \u5B88\u536B\u6362\u5C42/\u76F4\u8FDE\u5927\u56FE\uFF09\u6B64\u524D\u662F
|
|
2390
|
+
\u7A7A\u9ED1\u9ED1\u6D1E\uFF0C\u73B0\u8F6C\u5708\u5360\u4F4D\uFF1B\u56FE\u7247 onLoad\uFF08useImgChain settled\uFF09\u540E 160ms \u6DE1\u5165\u6362\u573A\u3002
|
|
2391
|
+
prefers-reduced-motion \u4E0B\u8F6C\u573A\u505C\u7528\uFF08\u8F6C\u5708\u964D\u901F\u4FDD\u7559\u6D3B\u52A8\u6307\u793A\uFF09\u3002 */
|
|
2392
|
+
@keyframes dsvm-lbspin{to{transform:rotate(360deg)}}
|
|
2393
|
+
.dsvm-lbspin{position:absolute;left:50%;top:50%;width:34px;height:34px;margin:-17px 0 0 -17px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:rgba(255,255,255,.92);animation:dsvm-lbspin .8s linear infinite}
|
|
2394
|
+
.dsvm-lightbox .dsvm-lbimg{opacity:0;transition:opacity .16s ease-out}
|
|
2395
|
+
.dsvm-lightbox .dsvm-lbimg.dsvm-lbimg-on{opacity:1}
|
|
2396
|
+
@media (prefers-reduced-motion:reduce){.dsvm-lbspin{animation-duration:1.6s}.dsvm-lightbox .dsvm-lbimg{transition:none}}
|
|
2374
2397
|
@media (max-width:640px){.dsvm-lbhint{display:none}.dsvm-btn.dsvm-lbarrow{width:40px;height:54px}}
|
|
2375
2398
|
/* 0.9.61 \u53CC\u8D25\u7EC8\u6001\u5360\u4F4D\uFF1A\u5C45\u4E2D\u5217\u2014\u2014\u26A0 \u6587\u6848 + \u91CD\u8BD5\uFF08\u590D\u7528 dsvm-btn\uFF09+ \u6253\u5F00\u539F\u56FE\uFF08ExtLink \u65B0\u6807\u7B7E\uFF09 */
|
|
2376
2399
|
.dsvm-lbfail{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 32px;background:rgba(15,23,42,.55);border:1px solid rgba(255,255,255,.14);border-radius:12px}
|
|
@@ -3115,7 +3138,7 @@ function useImgChain(url, opts) {
|
|
|
3115
3138
|
const timer = setTimeout(advance, WESERV_TIMEOUT_MS2);
|
|
3116
3139
|
return () => clearTimeout(timer);
|
|
3117
3140
|
}, [url, active, settled, tier]);
|
|
3118
|
-
return { src, failed, retry, onError, onLoad };
|
|
3141
|
+
return { src, failed, retry, onError, onLoad, settled };
|
|
3119
3142
|
}
|
|
3120
3143
|
function UseImgChainProbe(props) {
|
|
3121
3144
|
const chain = useImgChain(props.url, { w: props.w, h: props.h });
|
|
@@ -3146,7 +3169,11 @@ function Lightbox({ shots, index, onNav, onClose }) {
|
|
|
3146
3169
|
h("div", { className: "dsvm-lbfailmsg" }, `\u26A0 ${lookup("lb.fail")}`),
|
|
3147
3170
|
h("button", { className: "dsvm-btn", onClick: chain.retry }, lookup("lb.retry")),
|
|
3148
3171
|
h(ExtLink, { href: shots[index] }, lookup("lb.open"))
|
|
3149
|
-
) :
|
|
3172
|
+
) : [
|
|
3173
|
+
// 0.9.62 走链期间转圈占位(settled 即卸载;点它视为点遮罩可关——急躁关闭是合理逃生门)
|
|
3174
|
+
!chain.settled && chain.src ? h("div", { className: "dsvm-lbspin", "aria-hidden": "true" }) : null,
|
|
3175
|
+
h("img", { className: `dsvm-lbimg${chain.settled ? " dsvm-lbimg-on" : ""}`, src: chain.src, alt: "", onClick: onClose, referrerPolicy: "no-referrer", decoding: "async", onError: chain.onError, onLoad: chain.onLoad })
|
|
3176
|
+
],
|
|
3150
3177
|
h("button", { ref: closeRef, className: "dsvm-btn dsvm-lbclose", "aria-label": lookup("common.close"), title: lookup("common.close"), onClick: onClose }, h(XIcon)),
|
|
3151
3178
|
single ? null : h("button", { className: "dsvm-btn dsvm-lbarrow prev", "aria-label": lookup("lb.prev"), title: lookup("lb.prev"), onClick: () => onNav(lbStep2(index, -1, shots.length)) }, "\u2039"),
|
|
3152
3179
|
single ? null : h("button", { className: "dsvm-btn dsvm-lbarrow next", "aria-label": lookup("lb.next"), title: lookup("lb.next"), onClick: () => onNav(lbStep2(index, 1, shots.length)) }, "\u203A"),
|
|
@@ -3173,8 +3200,37 @@ function useModalDepth(active) {
|
|
|
3173
3200
|
}
|
|
3174
3201
|
function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeBusy, upgradeRec, profileKind, installRec, installNote }) {
|
|
3175
3202
|
useModalDepth(true);
|
|
3176
|
-
const
|
|
3203
|
+
const communityScreenshotSource = it.community === true || typeof it.owner === "string";
|
|
3204
|
+
const [primaryShotState, setPrimaryShotState] = useState({ id: null, screenshots: [] });
|
|
3205
|
+
useEffect(() => {
|
|
3206
|
+
let live = true;
|
|
3207
|
+
const controller = new AbortController();
|
|
3208
|
+
setPrimaryShotState({ id: it.id, screenshots: [] });
|
|
3209
|
+
if (communityScreenshotSource || typeof it.id !== "string" || it.id.trim() === "") {
|
|
3210
|
+
return () => {
|
|
3211
|
+
live = false;
|
|
3212
|
+
controller.abort();
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
api("primary-screenshots", { id: it.id }, controller.signal).then((data) => {
|
|
3216
|
+
if (!live) return;
|
|
3217
|
+
setPrimaryShotState({ id: it.id, screenshots: safeScreenshots({ screenshots: data && data.screenshots }) });
|
|
3218
|
+
}).catch(() => {
|
|
3219
|
+
if (live) setPrimaryShotState({ id: it.id, screenshots: [] });
|
|
3220
|
+
});
|
|
3221
|
+
return () => {
|
|
3222
|
+
live = false;
|
|
3223
|
+
controller.abort();
|
|
3224
|
+
};
|
|
3225
|
+
}, [it.id, it.github, it.community, it.owner]);
|
|
3226
|
+
const primaryShots = primaryShotState.id === it.id ? primaryShotState.screenshots : [];
|
|
3227
|
+
const shots = communityScreenshotSource ? safeScreenshots(it) : primaryShots;
|
|
3177
3228
|
const [brokenShots, setBrokenShots] = useState([]);
|
|
3229
|
+
const [brokenShotsKey, setBrokenShotsKey] = useState(it.id);
|
|
3230
|
+
if (brokenShotsKey !== it.id) {
|
|
3231
|
+
setBrokenShotsKey(it.id);
|
|
3232
|
+
setBrokenShots([]);
|
|
3233
|
+
}
|
|
3178
3234
|
const breakShot = useCallback((src) => setBrokenShots((prev) => prev.includes(src) ? prev : prev.concat(src)), []);
|
|
3179
3235
|
const visible = shots.filter((src) => !brokenShots.includes(src));
|
|
3180
3236
|
const [lb, setLb] = useState(null);
|
package/lib/core/host-api.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runDoctor } from './doctor.js';
|
|
|
13
13
|
import { listInstalledWithMeta, listMarket, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
14
14
|
import { runProfileTransaction, TransactionError } from './profile-transaction.js';
|
|
15
15
|
import { readInstalledPluginReadme } from './installed.js';
|
|
16
|
+
import { loadPrimaryScreenshots } from './primary-screenshots.js';
|
|
16
17
|
import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
17
18
|
import { getCommunitySummary } from './community.js';
|
|
18
19
|
import { RegistryConfigError } from './registry-controller.js';
|
|
@@ -218,6 +219,7 @@ export function createApiDispatcher(ctx) {
|
|
|
218
219
|
npmLatest: ctx.deps?.npmLatest ?? npmLatest,
|
|
219
220
|
readInstalledPluginReadme: ctx.deps?.readInstalledPluginReadme ?? readInstalledPluginReadme,
|
|
220
221
|
npmPackumentReadme: ctx.deps?.npmPackumentReadme ?? npmPackumentReadme,
|
|
222
|
+
loadPrimaryScreenshots: ctx.deps?.loadPrimaryScreenshots ?? loadPrimaryScreenshots,
|
|
221
223
|
getCommunitySummary: ctx.deps?.getCommunitySummary ?? getCommunitySummary,
|
|
222
224
|
runTransaction: ctx.deps?.runTransaction ?? runProfileTransaction,
|
|
223
225
|
scheduleRestart: ctx.deps?.scheduleRestart ?? scheduleRestart,
|
|
@@ -324,6 +326,28 @@ export function createApiDispatcher(ctx) {
|
|
|
324
326
|
payload = { plugins: snap.loaded.registry.plugins, registryState: snap.loaded, community };
|
|
325
327
|
break;
|
|
326
328
|
}
|
|
329
|
+
case 'primary-screenshots': {
|
|
330
|
+
const id = strArg(body, 'id');
|
|
331
|
+
if (!id)
|
|
332
|
+
throw new ApiProtocolError(400, '缺少 id');
|
|
333
|
+
await ctx.controller.ensureReady({ signal });
|
|
334
|
+
const snap = await ctx.controller.snapshot({ signal });
|
|
335
|
+
// 只信 active Primary Registry 的精确 id → github 映射;请求体中的 github/url 一律忽略。
|
|
336
|
+
// 本 method 不调用 listMarket/getCommunitySummary,也不加载 Community Catalog。
|
|
337
|
+
const entry = snap.loaded.registry.plugins.find((candidate) => candidate.id === id);
|
|
338
|
+
if (!entry || typeof entry.github !== 'string' || entry.github.trim() === '') {
|
|
339
|
+
payload = { screenshots: [] };
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
payload = { screenshots: await d.loadPrimaryScreenshots(entry.github) };
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// 截图是可选详情元数据;manifest 不可用不应阻塞详情或安装决策。
|
|
347
|
+
payload = { screenshots: [] };
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
327
351
|
case 'market': {
|
|
328
352
|
await ctx.controller.ensureReady();
|
|
329
353
|
// GUI policy:忽略客户端 withLatest,固定 true;limit clamp 1..96、缺省 32
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { fetchTextLimited } from './httpx.js';
|
|
2
|
+
const MANIFEST_HOST = 'raw.githubusercontent.com';
|
|
3
|
+
const MANIFEST_FILE = 'screenshots.json';
|
|
4
|
+
const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
5
|
+
const MAX_SCREENSHOTS = 8;
|
|
6
|
+
const SUCCESS_TTL_MS = 10 * 60_000;
|
|
7
|
+
const EMPTY_TTL_MS = 60_000;
|
|
8
|
+
const OWNER_MAX = 39;
|
|
9
|
+
const REPO_MAX = 100;
|
|
10
|
+
const GITHUB_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
|
|
11
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/;
|
|
12
|
+
function manifestUrl(github) {
|
|
13
|
+
const value = typeof github === 'string' ? github.trim() : '';
|
|
14
|
+
if (!GITHUB_REPO_RE.test(value))
|
|
15
|
+
return null;
|
|
16
|
+
const slash = value.indexOf('/');
|
|
17
|
+
const owner = value.slice(0, slash);
|
|
18
|
+
const repo = value.slice(slash + 1);
|
|
19
|
+
if (owner.length > OWNER_MAX || repo.length > REPO_MAX || repo === '.' || repo === '..')
|
|
20
|
+
return null;
|
|
21
|
+
return `https://${MANIFEST_HOST}/${owner}/${repo}/HEAD/${MANIFEST_FILE}`;
|
|
22
|
+
}
|
|
23
|
+
function screenshotUrl(manifest, rawPath) {
|
|
24
|
+
if (typeof rawPath !== 'string')
|
|
25
|
+
return null;
|
|
26
|
+
const path = rawPath.trim();
|
|
27
|
+
if (!path || CONTROL_RE.test(path))
|
|
28
|
+
return null;
|
|
29
|
+
if (path.startsWith('/') || path.startsWith('\\') || path.includes('\\'))
|
|
30
|
+
return null;
|
|
31
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith('//'))
|
|
32
|
+
return null;
|
|
33
|
+
if (/[?#]/.test(path))
|
|
34
|
+
return null;
|
|
35
|
+
const segments = path.split('/');
|
|
36
|
+
if (segments.some((part) => part === '' || part === '.' || part === '..'))
|
|
37
|
+
return null;
|
|
38
|
+
const base = new URL('.', manifest);
|
|
39
|
+
return new URL(segments.map((part) => encodeURIComponent(part)).join('/'), base).toString();
|
|
40
|
+
}
|
|
41
|
+
function parseManifest(text, manifestUrl) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > MAX_SCREENSHOTS)
|
|
50
|
+
return [];
|
|
51
|
+
const result = [];
|
|
52
|
+
for (const path of parsed) {
|
|
53
|
+
const url = screenshotUrl(manifestUrl, path);
|
|
54
|
+
if (!url)
|
|
55
|
+
return [];
|
|
56
|
+
result.push(url);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function assertManifestHost(raw, isRedirectHop) {
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(raw);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error(isRedirectHop ? 'screenshot manifest redirect URL is invalid' : 'invalid screenshot manifest request URL');
|
|
67
|
+
}
|
|
68
|
+
if (url.protocol !== 'https:' || url.hostname !== MANIFEST_HOST || url.username || url.password || url.port) {
|
|
69
|
+
throw new Error(isRedirectHop
|
|
70
|
+
? 'screenshot manifest redirects must remain on raw.githubusercontent.com'
|
|
71
|
+
: 'screenshot manifest request URL must be https://raw.githubusercontent.com');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read an author-owned root screenshots.json without consulting Community Catalog.
|
|
76
|
+
* All network access is bounded, and a rejected/empty manifest degrades to no gallery.
|
|
77
|
+
*/
|
|
78
|
+
export function createPrimaryScreenshotReader(options = {}) {
|
|
79
|
+
const fetchText = options.fetchText ?? fetchTextLimited;
|
|
80
|
+
const now = options.now ?? Date.now;
|
|
81
|
+
const cache = new Map();
|
|
82
|
+
const flights = new Map();
|
|
83
|
+
return async (github) => {
|
|
84
|
+
const url = manifestUrl(github);
|
|
85
|
+
if (!url)
|
|
86
|
+
return [];
|
|
87
|
+
const key = url.toLowerCase();
|
|
88
|
+
const timestamp = now();
|
|
89
|
+
for (const [cachedKey, value] of cache) {
|
|
90
|
+
if (value.expiresAt <= timestamp)
|
|
91
|
+
cache.delete(cachedKey);
|
|
92
|
+
}
|
|
93
|
+
const cached = cache.get(key);
|
|
94
|
+
if (cached && cached.expiresAt > timestamp)
|
|
95
|
+
return [...cached.screenshots];
|
|
96
|
+
const pending = flights.get(key);
|
|
97
|
+
if (pending)
|
|
98
|
+
return [...(await pending)];
|
|
99
|
+
const flight = (async () => {
|
|
100
|
+
let screenshots = [];
|
|
101
|
+
let hop = 0;
|
|
102
|
+
try {
|
|
103
|
+
const text = await fetchText(url, {
|
|
104
|
+
timeoutMs: 5_000,
|
|
105
|
+
maxBytes: MAX_MANIFEST_BYTES,
|
|
106
|
+
headers: { accept: 'application/json' },
|
|
107
|
+
onRequest: (hopUrl) => {
|
|
108
|
+
// 逐跳 host guard;错误文案区分初始跳与重定向跳,避免调试误导(评审建议落盘)。
|
|
109
|
+
const isRedirectHop = hop > 0;
|
|
110
|
+
hop += 1;
|
|
111
|
+
assertManifestHost(hopUrl, isRedirectHop);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
screenshots = parseManifest(text, url);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Images are optional metadata: a missing/unavailable manifest must not block detail/install.
|
|
118
|
+
screenshots = [];
|
|
119
|
+
}
|
|
120
|
+
cache.set(key, {
|
|
121
|
+
screenshots,
|
|
122
|
+
expiresAt: now() + (screenshots.length > 0 ? SUCCESS_TTL_MS : EMPTY_TTL_MS),
|
|
123
|
+
});
|
|
124
|
+
return screenshots;
|
|
125
|
+
})();
|
|
126
|
+
flights.set(key, flight);
|
|
127
|
+
try {
|
|
128
|
+
return [...(await flight)];
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (flights.get(key) === flight)
|
|
132
|
+
flights.delete(key);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Production reader: one process-memory cache shared by GUI Host calls. */
|
|
137
|
+
export const loadPrimaryScreenshots = createPrimaryScreenshotReader();
|
|
138
|
+
/** Exported parser contract for focused regression tests; production calls the reader above. */
|
|
139
|
+
export const PRIMARY_SCREENSHOT_LIMIT = MAX_SCREENSHOTS;
|
|
140
|
+
export const PRIMARY_SCREENSHOT_MANIFEST_MAX_BYTES = MAX_MANIFEST_BYTES;
|
|
141
|
+
export const PRIMARY_SCREENSHOT_SUCCESS_TTL_MS = SUCCESS_TTL_MS;
|
|
142
|
+
export const PRIMARY_SCREENSHOT_EMPTY_TTL_MS = EMPTY_TTL_MS;
|