dsh-m 0.9.33 → 0.9.45
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 +34 -0
- package/README.md +1 -1
- package/docs/DESIGN.md +1 -0
- package/lib/client.js +155 -25
- package/lib/core/host-api.js +26 -3
- package/lib/core/latest-cache.js +8 -0
- package/lib/core/market.js +54 -38
- package/lib/core/versions.js +19 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,23 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.45 变更:市场页两段加载 + 强制刷新穿透探测 + 精选页动线批
|
|
12
|
+
|
|
13
|
+
- **市场页两段加载([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**:浏览态 market 查询拆两段——第一段 `probeMode:'cache-only'` 只回 TTL 内缓存命中、零网络瞬时回页,有缺口(`latestComplete=false`,豁免社区 github 条目——Q46 永久缺口不构成第二段理由)才自动发起第二段既有探测语义就地补徽标;重启后首开不再被全页重探阻塞(弱网 8–16s → 恒秒开 + 徽标异步补全)。`mergeLatestFields` 纯函数保证会话内切换筛选态时徽标不回退;快照只在终态写入。服务端 latest 缓存保持纯内存不动(ADR-0006 不翻案);tools/CLI 的 `withLatest:false` 通路零变化。
|
|
14
|
+
- **force 探测穿透(重开 ADR-0006 暂缓裁决)**:设置页「强制刷新」新链路 onForceMarket → marketReloadAll(true) → core「peek 旧值兜底 + 全页重探」——严格落实 ADR-0006 在案约束「peek 不删除语义而非 ttlMin=0 先删后探」,重探失败保留旧值 + latestError,不产生空徽标窗口;强刷后精选清单与「有更新」徽标同时刷新。市场页 probeMode 只有 full/cache-only 两态(不设 'only')。
|
|
15
|
+
- **详情 Modal 升级动线(U10)**:已装且可升级条目的 Modal 动作区新增「升级」主按钮(市场区与收藏区两挂载点;record.target=安装包名,走 ops 泵与 0.9.22 生效判定分流文案,guard/opSuperseded/普通失败三分支对齐已装页);新增 README 折叠页(展开才拉取,收起态零请求);「收录日期」走 fmtDate 本地化。**修复(实机验收发现)**:README 折叠页原直接复用已装页本地读取接口,未安装条目(市场浏览的主体)恒报「web profile 未安装该插件」——现本地未装时回源 npm packument 顶层 readme 字段兜底(HTTPS/8MB 上限/超时;无 readme 字段显示「没有 README」;兜底失败如实抛原始错误),已装条目仍走本地读取。
|
|
16
|
+
- **错误恢复与空态(U9)**:错误行内联「重试」按钮(此前只能关开面板或绕道设置页);空态三分支——registry unavailable 主文案 / 分类空桶专用文案 / 通用文案。
|
|
17
|
+
- **收藏身份(U12)**:收藏快照增补 verified/audience/decoupled 三字段(只存不标——§2.7 裁决⑤「收藏页不打标」不动);收藏区为精选条目补「精选」身份徽标与「已实测」质量徽标(此前精选条目进收藏区连「精选」标识都丢失)。
|
|
18
|
+
- **可达性与杂项(U8/U3/U7)**:详情 Modal 补 role=dialog/aria-modal/aria-label 与打开时聚焦关闭钮,截图缩放支持键盘 Enter/Space(Tab 圈闭明确不做);精选区 0 计数桶显式渲染「0」+ 降透明;跨桶条目使分类计数之和大于总数时 chips 行容器给出 title 说明。
|
|
19
|
+
- **测试**:market/host-api/client-market-state/client-render-smoke 净增用例与断言(probeMode 两态与 force peek 语义、缺口豁免、cache-only outdated、mergeLatestFields、两段与强刷接线、Modal 升级/README/可达性、收藏徽标、0 计数桶);全量测试 Windows 本机除既有 symlink 族基线 15 项外零新增失败;typecheck 零错误。
|
|
20
|
+
|
|
21
|
+
### 0.9.44 变更:插件卡片网格按容器宽度自适应列数(全屏显出更多卡片)
|
|
22
|
+
|
|
23
|
+
- **自适应网格**:市场/已装/收藏三视图共用的卡片网格从固定两列改为按容器实际可用宽度自适应——`grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr))`。列数 ≈ `max(1, floor((W+8)/368))`(最小卡宽 360px、列距 8px,剩余宽度各列均分):浮动态(~920px 面板)保持 2 列零回归;全屏 1920 宽约 5 列、1280 宽 3 列,显著显出更多插件卡片。
|
|
24
|
+
- **删视口断点**:移除 `@media (max-width:680px)` 的单列覆盖——视口宽度不代表面板实际可用宽度;极窄容器由 `min(100%,360px)` 兜底自动单列,不横向溢出。
|
|
25
|
+
- **范围与边界**:搜索分组段头(`.dsvm-grouphead`)仍以 `grid-column:1/-1` 跨满整行;纯客户端变更,刷新页面即生效。
|
|
26
|
+
- **测试**:新增 SSR 冒烟回归 1 例(锁定自适应网格规则与断点删除);typecheck 零错误。
|
|
27
|
+
|
|
11
28
|
### 0.9.33 变更:自研条目元数据 v1.1——`audience`/`decoupled` 两字段与三端标注
|
|
12
29
|
|
|
13
30
|
- **schema**:registry 条目增补可选 `decoupled?: true`(解耦条目,实操口径判定;与 `verified` 互斥——同条目共存报 error)与 `audience?: 'public' | 'internal'`(缺省 public 不产生键;`'team'` 值集预留未开放)。决策共识与五项裁决记录见 DESIGN §2.7,术语见 GLOSSARY「解耦条目 / 自用条目」。
|
|
@@ -325,6 +342,23 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
325
342
|
|
|
326
343
|
## English
|
|
327
344
|
|
|
345
|
+
### Added in 0.9.45 — market two-phase loading + force probe passthrough + curated-zone motion batch
|
|
346
|
+
|
|
347
|
+
- **Market two-phase loading ([ADR-0013](docs/adr/0013-market-two-phase-probe.md))**: browsing market queries split into two phases — phase 1 sends `probeMode:'cache-only'` (TTL-fresh cache hits only, zero network, instant page), and only when gaps exist (`latestComplete=false`, community github entries exempt — a Q46 permanent gap is never a phase-2 reason) does phase 2 run the existing probe semantics to patch badges in place. First open after a DSH restart is no longer blocked by a full-page re-probe (8–16s on weak networks → always instant page + async badge completion). `mergeLatestFields` keeps badges from regressing during in-session filter switches; snapshots are written at terminal states only. The server-side latest cache stays memory-only (ADR-0006 untouched); the tools/CLI `withLatest:false` path is unchanged.
|
|
348
|
+
- **Force probe passthrough (reopens ADR-0006's deferred ruling)**: the settings-page "Force refresh" gains the onForceMarket → marketReloadAll(true) → core "peek old values as fallback + full-page re-probe" chain — strictly honoring ADR-0006's on-record constraint ("peek without delete, not ttlMin=0 delete-then-probe"); a failed re-probe keeps old values + latestError with no empty-badge window, and one click now refreshes both the curated list and the update badges. Market probeMode has exactly two values (full/cache-only) — no 'only'.
|
|
349
|
+
- **Detail Modal upgrade motion (U10)**: installed-and-outdated entries get a primary "Upgrade" button in the Modal actions (both the market and favorites mount points; record.target = installed package name, routed through the ops pump with 0.9.22 activation-based messaging; guard/opSuperseded/generic failure branches aligned with the Installed tab); a README fold (fetched on expand, zero requests while collapsed); "Added" date localized via fmtDate. **Fix (found in real-machine acceptance)**: the fold originally reused the Installed tab's local-read endpoint, so every not-installed entry (the majority of market browsing) errored with "web profile 未安装该插件" — not-installed entries now fall back to the npm packument top-level readme field (HTTPS/8MB cap/timeout; a missing readme field renders "no README"; a failed fallback surfaces the original error honestly), while installed entries keep the local read.
|
|
350
|
+
- **Error recovery & empty states (U9)**: inline "Retry" button on the error row (previously only panel reopen or a detour to Settings); three-way empty copy — registry-unavailable primary message / curated-bucket-specific message / generic.
|
|
351
|
+
- **Favorites identity (U12)**: favorite snapshots store verified/audience/decoupled (stored but not labeled — DESIGN §2.7 ruling ⑤ "favorites stay unmarked" untouched); curated entries in Favorites regain a "Curated" identity badge plus the "Verified" quality badge (previously they lost even the Curated marker).
|
|
352
|
+
- **Accessibility & misc (U8/U3/U7)**: Detail Modal gains role=dialog/aria-modal/aria-label and initial focus on the close button; screenshot lightboxes open via keyboard Enter/Space (focus trap explicitly out of scope); zero-count curated buckets render an explicit "0" at reduced opacity; the chips row gets a title explanation whenever cross-bucket double counting pushes Σchip counts above the total.
|
|
353
|
+
- **Tests**: new cases and assertions across market / host-api / client-market-state / client-render-smoke (two-state probeMode and force peek semantics, gap exemption, cache-only outdated, mergeLatestFields, two-phase and force-refresh wiring, Modal upgrade/README/a11y, favorites badges, zero-count chips); on this Windows machine the full suite has zero new failures beyond the 15 pre-existing symlink-family baseline cases; typecheck clean.
|
|
354
|
+
|
|
355
|
+
### Added in 0.9.44 — plugin card grid adapts column count to available width (more cards in fullscreen)
|
|
356
|
+
|
|
357
|
+
- **Adaptive grid**: the card grid shared by the Market / Installed / Favorites views switches from a fixed two-column layout to container-width adaptation — `grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr))`. Column count ≈ `max(1, floor((W+8)/368))` (360px min card width, 8px gap, remaining width split evenly): the floating panel (~920px) keeps its 2 columns with zero regression; fullscreen at 1920px shows ~5 columns and 1280px shows 3 — noticeably more plugin cards on screen.
|
|
358
|
+
- **Viewport breakpoint removed**: the `@media (max-width:680px)` single-column override is gone — viewport width does not describe the width actually available to the panel; very narrow containers fall back to a single column via `min(100%,360px)` without horizontal overflow.
|
|
359
|
+
- **Scope and edges**: search group headers (`.dsvm-grouphead`) still span the full row via `grid-column:1/-1`; client-only change — refresh the page to take effect.
|
|
360
|
+
- **Tests**: 1 new SSR smoke regression case (locks the adaptive grid rule and the breakpoint removal); typecheck clean.
|
|
361
|
+
|
|
328
362
|
### Added in 0.9.33 — self-dev entry metadata v1.1: `audience`/`decoupled` fields with three-surface marking
|
|
329
363
|
|
|
330
364
|
- **Schema**: registry entries gain optional `decoupled?: true` (decoupled entries, operational criterion; mutually exclusive with `verified` — co-occurrence is an error) and `audience?: 'public' | 'internal'` (defaults to public, no key emitted; `'team'` reserved, not accepted this release). The decision record and five rulings live in DESIGN §2.7; terms in GLOSSARY (Decoupled Entry / Internal Entry).
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ dsh-m 是 DeepSeek Harness(DSH)的插件市场:**收录 · 安装 · 卸
|
|
|
60
60
|
|
|
61
61
|
| 视图 | 能力 |
|
|
62
62
|
|---|---|
|
|
63
|
-
| **市场** | 社区(4,000+ 条)/ 精选 / 收藏三分区;浏览态各分区独立保留分类、排序与分页状态,搜索为跨区全局(社区 + 精选一并命中);卡片点开详情 Modal
|
|
63
|
+
| **市场** | 社区(4,000+ 条)/ 精选 / 收藏三分区;浏览态各分区独立保留分类、排序与分页状态,搜索为跨区全局(社区 + 精选一并命中);卡片点开详情 Modal,安装进度与结果就地显示;已装且可升级的条目可直接在 Modal 内一键升级并展开 README 预览。 |
|
|
64
64
|
| **操作记录** | 安装、升级、卸载、开关统一进入全局操作日志,状态不挂卡片;宿主重载后自动恢复未完成操作。 |
|
|
65
65
|
| **已装** | 当前 profile 实装列表:来源标注、运行相位徽标、一键开关、升级与两段式确认卸载。 |
|
|
66
66
|
| **设置** | 精选清单配置与状态、社区目录开关、dsh-m 自更新。 |
|
package/docs/DESIGN.md
CHANGED
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
- **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 条。
|
|
98
98
|
- **技术默认件**:图片三层懒加载(IntersectionObserver + `loading=lazy` + `fetchPriority=low`;缩略图本机直连原图,不引第三方代理服务);移除客户端重复排序(服务端单一排序源);`total>200` 性能提示随分区退役;host-api limit 上限对齐新页大小;空状态逐分区定制;错误态带具体原因 + 重试。
|
|
99
99
|
- **Backlog(明确不做)**:UI 完整双语字典、时间窗过滤、浏览层宿主兼容徽章/过滤、组管理、个人备注、giscus 评论、静态官网。
|
|
100
|
+
- **修订(2026-10-06,0.9.45,[ADR-0013](./adr/0013-market-two-phase-probe.md))**:市场页两段加载——浏览态第一段 `probeMode:'cache-only'` 零网络回页(缺口 → latestComplete=false,豁免社区 github,Q46),第二段既有探测语义就地补徽标;force 语义 = peek 旧值兜底 + 全页重探(ADR-0006「peek 不删除」约束落实,重开其暂缓裁决),设置页「强制刷新」经 onForceMarket → marketReloadAll(true) 全链路接通。同批动线:详情 Modal 对已装且可升级条目提供「升级」主按钮(record.target=安装包名;市场区与收藏区两挂载点)+ README 折叠页(展开才拉取)+ 收录日期 fmtDate 本地化;错误行内联「重试」+ 空态三分支(unavailable 主文案 / 分类空桶 / 通用);收藏快照补 verified/audience/decoupled 三字段(只存不标——§2.7 裁决⑤不动),收藏区补「精选」身份徽标与「已实测」;0 计数桶显式渲染「0」+ 降透明 + 跨桶 title 说明;Modal 可达性子集(role=dialog/aria-modal/初始聚焦关闭钮/截图键盘开灯箱,Tab 圈闭不做)。
|
|
100
101
|
|
|
101
102
|
### 2.7 自研条目元数据:受众与解耦(grilling 定稿 2026-10-05)
|
|
102
103
|
|
package/lib/client.js
CHANGED
|
@@ -30,6 +30,7 @@ __export(market_state_exports, {
|
|
|
30
30
|
MARKET_PAGE_SIZES: () => MARKET_PAGE_SIZES,
|
|
31
31
|
createZoneState: () => createZoneState,
|
|
32
32
|
marketNotice: () => marketNotice,
|
|
33
|
+
mergeLatestFields: () => mergeLatestFields,
|
|
33
34
|
normalizeMarketQuery: () => normalizeMarketQuery,
|
|
34
35
|
normalizeMarketResponse: () => normalizeMarketResponse,
|
|
35
36
|
pageItems: () => pageItems,
|
|
@@ -209,7 +210,27 @@ function marketNotice(registryState, community) {
|
|
|
209
210
|
if (notice) out.notice = notice;
|
|
210
211
|
return out;
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
+
function mergeLatestFields(nextItems, prevItems) {
|
|
214
|
+
if (!Array.isArray(nextItems) || !Array.isArray(prevItems) || prevItems.length === 0) return nextItems;
|
|
215
|
+
const prevById = new Map(prevItems.map((it) => [it && it.id, it]));
|
|
216
|
+
let touched = false;
|
|
217
|
+
const out = nextItems.map((it) => {
|
|
218
|
+
if (!it || typeof it !== "object") return it;
|
|
219
|
+
const hasLatest = it.latestVersion !== void 0 || it.latestTag !== void 0 || it.latestSha !== void 0 || it.latestError !== void 0;
|
|
220
|
+
if (hasLatest) return it;
|
|
221
|
+
const prev = prevById.get(it.id);
|
|
222
|
+
if (!prev) return it;
|
|
223
|
+
const patch = {};
|
|
224
|
+
for (const k of LATEST_MERGE_FIELDS) {
|
|
225
|
+
if (it[k] === void 0 && prev[k] !== void 0) patch[k] = prev[k];
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(patch).length === 0) return it;
|
|
228
|
+
touched = true;
|
|
229
|
+
return { ...it, ...patch };
|
|
230
|
+
});
|
|
231
|
+
return touched ? out : nextItems;
|
|
232
|
+
}
|
|
233
|
+
var MARKET_PAGE_SIZES, DEFAULT_PAGE_SIZE, CURATED_ORDER, CURATED_LABELS, CURATED_IDS, SLUG_RE, SORT_FIELDS, sameSort, FALLBACK_REGISTRY_STATE, FALLBACK_COMMUNITY, LATEST_MERGE_FIELDS;
|
|
213
234
|
var init_market_state = __esm({
|
|
214
235
|
"src/client/market-state.js"() {
|
|
215
236
|
"use strict";
|
|
@@ -253,6 +274,7 @@ var init_market_state = __esm({
|
|
|
253
274
|
errors: [],
|
|
254
275
|
warnings: []
|
|
255
276
|
};
|
|
277
|
+
LATEST_MERGE_FIELDS = ["latestVersion", "latestTag", "latestSha", "latestError", "latestErrorCode", "outdated"];
|
|
256
278
|
}
|
|
257
279
|
});
|
|
258
280
|
|
|
@@ -1247,7 +1269,7 @@ var h = React.createElement;
|
|
|
1247
1269
|
var { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;
|
|
1248
1270
|
var PLUGIN_ID = "dsh-m";
|
|
1249
1271
|
var API = "/dshm";
|
|
1250
|
-
var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2 } = (init_market_state(), __toCommonJS(market_state_exports));
|
|
1272
|
+
var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2, searchSourceOf: searchSourceOf2, mergeLatestFields: mergeLatestFields2 } = (init_market_state(), __toCommonJS(market_state_exports));
|
|
1251
1273
|
var { readMarketSnapshot: readMarketSnapshot2, writeMarketSnapshot: writeMarketSnapshot2, isDefaultFirstPageQuery: isDefaultFirstPageQuery2 } = (init_market_snapshot(), __toCommonJS(market_snapshot_exports));
|
|
1252
1274
|
var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCommonJS(backdrop_exports));
|
|
1253
1275
|
var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
|
|
@@ -1330,6 +1352,10 @@ var ZH = {
|
|
|
1330
1352
|
"common.none": "\u2014",
|
|
1331
1353
|
"market.loading": "\u52A0\u8F7D\u6536\u5F55\u6E05\u5355\u4E2D\u2026 ",
|
|
1332
1354
|
"market.empty": "\u65E0\u5339\u914D\u63D2\u4EF6\uFF0C\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\u6216\u5206\u7C7B",
|
|
1355
|
+
"market.retry": "\u91CD\u8BD5",
|
|
1356
|
+
"market.empty.category": "\u8BE5\u5206\u7C7B\u6682\u65E0\u6536\u5F55\uFF0C\u6362\u4E2A\u6876\u6216\u6E05\u7A7A\u7B5B\u9009\u770B\u770B",
|
|
1357
|
+
"market.empty.unavailable": "\u6536\u5F55\u6E05\u5355\u4E0D\u53EF\u7528\uFF0C\u6682\u65F6\u65E0\u6CD5\u5217\u51FA\u63D2\u4EF6",
|
|
1358
|
+
"chips.crossbucket.tip": "\u8DE8\u6876\u6761\u76EE\u4F1A\u5728\u591A\u4E2A\u5206\u7C7B\u91CD\u590D\u8BA1\u6570\uFF0C\u6545\u5206\u7C7B\u8BA1\u6570\u4E4B\u548C\u5927\u4E8E\u603B\u6570",
|
|
1333
1359
|
"installed.loading": "\u8BFB\u53D6\u5DF2\u88C5\u5217\u8868\u4E2D\u2026 ",
|
|
1334
1360
|
"installed.empty": "\u5F53\u524D profile \u5C1A\u672A\u5B89\u88C5\u4EFB\u4F55 dsh \u63D2\u4EF6",
|
|
1335
1361
|
"installed.none": "\u672A\u5B89\u88C5",
|
|
@@ -1600,6 +1626,10 @@ var EN = {
|
|
|
1600
1626
|
"common.none": "\u2014",
|
|
1601
1627
|
"market.loading": "Loading listings\u2026 ",
|
|
1602
1628
|
"market.empty": "No matching plugins \u2014 try another keyword or category",
|
|
1629
|
+
"market.retry": "Retry",
|
|
1630
|
+
"market.empty.category": "Nothing curated in this category yet \u2014 try another bucket or clear the filter",
|
|
1631
|
+
"market.empty.unavailable": "Registry unavailable \u2014 listings are temporarily down",
|
|
1632
|
+
"chips.crossbucket.tip": "Cross-bucket entries count in every bucket, so chip counts add up above the total",
|
|
1603
1633
|
"installed.loading": "Reading installed list\u2026 ",
|
|
1604
1634
|
"installed.empty": "No DSH plugins installed in this profile",
|
|
1605
1635
|
"installed.none": "Not installed",
|
|
@@ -1885,6 +1915,7 @@ var CSS = `
|
|
|
1885
1915
|
.dsvm-filteropt.on{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:600}
|
|
1886
1916
|
.dsvm-filtercheck{font-size:11px}
|
|
1887
1917
|
.dshm-chip{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:2px 10px;font:inherit;font-size:11px;cursor:pointer}
|
|
1918
|
+
.dshm-chip.zero{opacity:.55}
|
|
1888
1919
|
.dshm-chip:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
|
|
1889
1920
|
.dshm-chip.on{background:var(--dsw-specific-sidebar-nav-item-active,rgba(38,49,72,.08));border-color:transparent;color:var(--dsw-alias-label-primary,inherit);font-weight:500}
|
|
1890
1921
|
.dsvm-chipswrap{position:sticky;top:-14px;z-index:5;background:var(--dsw-alias-bg-base,#fff);padding:8px 0;margin:-8px 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.14))}
|
|
@@ -1941,10 +1972,12 @@ var CSS = `
|
|
|
1941
1972
|
.dsvm-favbtn:hover{color:var(--dsw-alias-state-business-primary,#4d6bfe)}
|
|
1942
1973
|
.dsvm-favbtn.on{color:#e0a33c}
|
|
1943
1974
|
.dsvm-reddot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-error-primary,#ef4444);margin-left:5px;vertical-align:middle}
|
|
1944
|
-
|
|
1975
|
+
/* \u4F9D\u5361\u7247\u5BB9\u5668\u5BBD\u5EA6\u81EA\u52A8\u51B3\u5B9A\u5217\u6570\uFF1AN=max(1,floor((W+8)/(360+8)))\uFF0C\u5355\u5361\u5BBD\u5EA6\u5747\u5206\u5269\u4F59\u7A7A\u95F4\u3002
|
|
1976
|
+
\u666E\u901A\u7A97\u53E3\u3001\u5168\u5C4F\u548C\u7A84\u5C4F\u90FD\u8D70\u540C\u4E00\u89C4\u5219\uFF1Bmin(100%,360px) \u4FDD\u8BC1\u6781\u7A84\u5BB9\u5668\u4E0D\u6A2A\u5411\u6EA2\u51FA\u3002 */
|
|
1977
|
+
.dshm-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr));gap:8px}
|
|
1945
1978
|
.dsvm-searchmeta{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-caption,#6b7280);font-size:12px;line-height:18px;margin:2px 0}
|
|
1946
1979
|
.dsvm-grouphead{grid-column:1/-1;color:var(--dsw-alias-label-caption,#6b7280);font-size:11px;line-height:16px;margin:2px 0 0;font-weight:600;letter-spacing:.02em}
|
|
1947
|
-
|
|
1980
|
+
|
|
1948
1981
|
.dshm-card{display:flex;gap:12px;align-items:flex-start;background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.04));border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:12px;cursor:pointer;text-align:left;width:100%;box-sizing:border-box;min-width:0;font:inherit;color:var(--dsw-alias-label-primary,inherit);transition:border-color .16s,background .16s}
|
|
1949
1982
|
.dshm-card:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));border-color:var(--dsw-alias-label-dimmed,#c7d2fe)}
|
|
1950
1983
|
.dshm-icon{width:40px;height:40px;border-radius:10px;object-fit:cover;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);flex-shrink:0;background:linear-gradient(135deg,#c7d2fe,#fbcfe8);display:grid;place-items:center;font-weight:700;font-size:16px;color:#374151}
|
|
@@ -2116,7 +2149,7 @@ function useMarketData(zone = "community") {
|
|
|
2116
2149
|
const queryRef = useRef(query);
|
|
2117
2150
|
const dataRef = useRef(boot.data);
|
|
2118
2151
|
const storage = () => typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
|
|
2119
|
-
const fetchPage = useCallback((nextQuery, force, background = false) => {
|
|
2152
|
+
const fetchPage = useCallback((nextQuery, force, background = false, probeMode = "cache-only") => {
|
|
2120
2153
|
const gen = ++genRef.current;
|
|
2121
2154
|
abortRef.current?.abort();
|
|
2122
2155
|
const ac = new AbortController();
|
|
@@ -2133,15 +2166,23 @@ function useMarketData(zone = "community") {
|
|
|
2133
2166
|
...nextQuery.sort ? { sort: nextQuery.sort } : {},
|
|
2134
2167
|
offset: nextQuery.offset,
|
|
2135
2168
|
limit: nextQuery.limit,
|
|
2169
|
+
// 0.9.45 两段加载(ADR-0013):第一段 cache-only 零网络回页;'full' 为终态语义不下传(core 缺省等价)
|
|
2170
|
+
...probeMode !== "full" ? { probeMode } : {},
|
|
2136
2171
|
...force ? { force: true } : {}
|
|
2137
2172
|
};
|
|
2138
2173
|
return api("market", params, ac.signal).then((raw) => {
|
|
2139
2174
|
if (genRef.current !== gen || ac.signal.aborted) return;
|
|
2140
2175
|
const next = normalizeMarketResponse2(raw);
|
|
2141
|
-
dataRef.current
|
|
2142
|
-
|
|
2176
|
+
const prevItems = dataRef.current && Array.isArray(dataRef.current.items) ? dataRef.current.items : null;
|
|
2177
|
+
const mergedItems = mergeLatestFields2(next.items, prevItems);
|
|
2178
|
+
const merged = mergedItems === next.items ? next : { ...next, items: mergedItems };
|
|
2179
|
+
dataRef.current = merged;
|
|
2180
|
+
setData(merged);
|
|
2143
2181
|
setLoading(false);
|
|
2144
|
-
if (
|
|
2182
|
+
if (probeMode === "cache-only" && next.latestComplete === false) {
|
|
2183
|
+
return fetchPage(nextQuery, force, true, "full");
|
|
2184
|
+
}
|
|
2185
|
+
if (isDefaultFirstPageQuery2(nextQuery, zone)) writeMarketSnapshot2(storage(), { zone, response: merged });
|
|
2145
2186
|
}).catch((e) => {
|
|
2146
2187
|
if (genRef.current !== gen || ac.signal.aborted) return;
|
|
2147
2188
|
if (background && dataRef.current) return;
|
|
@@ -2155,7 +2196,7 @@ function useMarketData(zone = "community") {
|
|
|
2155
2196
|
setQuery(next);
|
|
2156
2197
|
if (opts.fetch !== false) fetchPage(next, opts.force);
|
|
2157
2198
|
}, [fetchPage]);
|
|
2158
|
-
const reload = useCallback((force) => fetchPage(queryRef.current, force), [fetchPage]);
|
|
2199
|
+
const reload = useCallback((force) => fetchPage(queryRef.current, force, false, "full"), [fetchPage]);
|
|
2159
2200
|
useEffect(() => {
|
|
2160
2201
|
fetchPage(queryRef.current, false, boot.has);
|
|
2161
2202
|
return () => abortRef.current?.abort();
|
|
@@ -2369,7 +2410,7 @@ function SearchBox({ placeholder, initial, onCommit }) {
|
|
|
2369
2410
|
}, h(XIcon)) : null
|
|
2370
2411
|
);
|
|
2371
2412
|
}
|
|
2372
|
-
function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
2413
|
+
function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }) {
|
|
2373
2414
|
const chips = useMemo(() => zoneChips2(counts, labels, zone), [counts, labels, zone]);
|
|
2374
2415
|
const [expanded, setExpanded] = useState(false);
|
|
2375
2416
|
const [stuck, setStuck] = useState(false);
|
|
@@ -2421,8 +2462,9 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
|
2421
2462
|
const hidden = chips.length - shown.length;
|
|
2422
2463
|
const btn = (c) => h(
|
|
2423
2464
|
"button",
|
|
2424
|
-
{ key: c.id, "data-chip": "1", className: `dshm-chip${active === c.id ? " on" : ""}`, onClick: () => onPick(active === c.id ? null : c.id) },
|
|
2425
|
-
|
|
2465
|
+
{ key: c.id, "data-chip": "1", className: `dshm-chip${active === c.id ? " on" : ""}${c.count === 0 ? " zero" : ""}`, onClick: () => onPick(active === c.id ? null : c.id) },
|
|
2466
|
+
// 0.9.45 U3/U7:0 计数桶显式渲染「0」+ 降透明(社区区 chip 计数恒 >0,行为不变)
|
|
2467
|
+
`${c.labelKey ? lookup(c.labelKey) : c.label} ${c.count || 0}`
|
|
2426
2468
|
);
|
|
2427
2469
|
return h(
|
|
2428
2470
|
React.Fragment,
|
|
@@ -2433,7 +2475,7 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
|
2433
2475
|
{ className: "dsvm-chipswrap" },
|
|
2434
2476
|
h(
|
|
2435
2477
|
"div",
|
|
2436
|
-
{ ref: wrapRef, className: "dshm-chips" },
|
|
2478
|
+
{ ref: wrapRef, className: "dshm-chips", title: wrapTitle || void 0 },
|
|
2437
2479
|
h("button", { "data-chip": "1", className: `dshm-chip${active == null ? " on" : ""}`, onClick: () => onPick(null) }, lookup("cat.all")),
|
|
2438
2480
|
...shown.map(btn),
|
|
2439
2481
|
hidden > 0 ? h(
|
|
@@ -2477,7 +2519,20 @@ function Shot({ src, onClick }) {
|
|
|
2477
2519
|
}, []);
|
|
2478
2520
|
return h(
|
|
2479
2521
|
"div",
|
|
2480
|
-
{
|
|
2522
|
+
{
|
|
2523
|
+
ref,
|
|
2524
|
+
className: "dsvm-shotbox",
|
|
2525
|
+
onClick,
|
|
2526
|
+
role: "button",
|
|
2527
|
+
tabIndex: 0,
|
|
2528
|
+
// 0.9.45 U8:键盘可达(此前 role=button 但键盘无法开灯箱)
|
|
2529
|
+
onKeyDown: (e) => {
|
|
2530
|
+
if ((e.key === "Enter" || e.key === " ") && !e.isComposing) {
|
|
2531
|
+
e.preventDefault();
|
|
2532
|
+
onClick();
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
},
|
|
2481
2536
|
show ? h("img", { className: "dsvm-shot", src, alt: "", loading: "lazy", referrerPolicy: "no-referrer", fetchPriority: "low" }) : null
|
|
2482
2537
|
);
|
|
2483
2538
|
}
|
|
@@ -2510,11 +2565,16 @@ function useModalDepth(active) {
|
|
|
2510
2565
|
};
|
|
2511
2566
|
}, [active]);
|
|
2512
2567
|
}
|
|
2513
|
-
function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, installRec, installNote }) {
|
|
2568
|
+
function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeBusy, upgradeRec, profileKind, installRec, installNote }) {
|
|
2514
2569
|
useModalDepth(true);
|
|
2515
2570
|
const shots = it.community === true ? safeScreenshots(it) : [];
|
|
2516
2571
|
const [lb, setLb] = useState(null);
|
|
2517
2572
|
const [copied, setCopied] = useState(false);
|
|
2573
|
+
const [rmOpen, setRmOpen] = useState(false);
|
|
2574
|
+
const closeRef = useRef(null);
|
|
2575
|
+
useEffect(() => {
|
|
2576
|
+
if (closeRef.current) closeRef.current.focus();
|
|
2577
|
+
}, []);
|
|
2518
2578
|
useEffect(() => {
|
|
2519
2579
|
const onKey = (e) => {
|
|
2520
2580
|
if (e.key === "Escape") {
|
|
@@ -2547,7 +2607,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2547
2607
|
{ className: "dsvm-modal", ...backdropCloseHandlers2(onClose) },
|
|
2548
2608
|
h(
|
|
2549
2609
|
"div",
|
|
2550
|
-
{ className: "dsvm-modalbox", onClick: (e) => e.stopPropagation() },
|
|
2610
|
+
{ className: "dsvm-modalbox", role: "dialog", "aria-modal": "true", "aria-label": it.name, onClick: (e) => e.stopPropagation() },
|
|
2551
2611
|
h(
|
|
2552
2612
|
"div",
|
|
2553
2613
|
{ className: "dsvm-modalhead" },
|
|
@@ -2561,7 +2621,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2561
2621
|
it.community === true ? h("span", { className: "dshm-badge info" }, lookup("badge.community")) : null,
|
|
2562
2622
|
h("span", { className: "dshm-badge info" }, it.source === "npm" ? "npm" : "github"),
|
|
2563
2623
|
h("span", { className: "dshm-spacer" }),
|
|
2564
|
-
h("button", { className: "dshm-xbtn", "aria-label": lookup("common.close"), title: lookup("common.close"), onClick: onClose }, h(XIcon))
|
|
2624
|
+
h("button", { ref: closeRef, className: "dshm-xbtn", "aria-label": lookup("common.close"), title: lookup("common.close"), onClick: onClose }, h(XIcon))
|
|
2565
2625
|
),
|
|
2566
2626
|
it.community === true ? h(
|
|
2567
2627
|
"div",
|
|
@@ -2575,7 +2635,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2575
2635
|
"dl",
|
|
2576
2636
|
{ className: "dsvm-kv" },
|
|
2577
2637
|
kv(lookup("modal.category"), catLabel),
|
|
2578
|
-
it.added ? kv(lookup("modal.added"), it.added) : null,
|
|
2638
|
+
it.added ? kv(lookup("modal.added"), fmtDate(it.added)) : null,
|
|
2579
2639
|
it.community === true ? kv(
|
|
2580
2640
|
lookup("modal.dlwindow"),
|
|
2581
2641
|
typeof it.downloads === "number" ? h("span", { title: String(it.downloads) }, `${compactCount(it.downloads)}\uFF08${it.downloadsStart || "?"} ~ ${it.downloadsEnd || "?"}${it.downloadsCheckedAt ? `\uFF0C${lookup("modal.checkedat")} ${it.downloadsCheckedAt}` : ""}\uFF09`) : lookup("modal.dlnone")
|
|
@@ -2589,6 +2649,14 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2589
2649
|
it.deprecated === true && it.replacement ? kv(lookup("modal.replacement"), it.replacement) : null
|
|
2590
2650
|
),
|
|
2591
2651
|
h("div", { className: "dshm-desc", style: { WebkitLineClamp: "unset" } }, descFull),
|
|
2652
|
+
it.npm ? h(
|
|
2653
|
+
"details",
|
|
2654
|
+
{ className: "dsvm-fold", onToggle: (e) => {
|
|
2655
|
+
if (e.target.open) setRmOpen(true);
|
|
2656
|
+
} },
|
|
2657
|
+
h("summary", null, lookup("readme.show")),
|
|
2658
|
+
rmOpen ? h(ReadmeBlock, { pkg: it.npm }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
|
|
2659
|
+
) : null,
|
|
2592
2660
|
shots.length ? h(
|
|
2593
2661
|
"div",
|
|
2594
2662
|
{ className: "dsvm-shotrow" },
|
|
@@ -2621,12 +2689,18 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2621
2689
|
// installRec 派生自全局操作记录(DESIGN §2.6「状态不挂卡片」的所有权模型不变,此处只是展示):
|
|
2622
2690
|
// 仅 queued/running 挂行,input(兼容待决)走上层 CompatDialog;installNote 为本次终态摘要。
|
|
2623
2691
|
installRec && (installRec.status === "running" || installRec.status === "queued") ? h(ProgressLine, { key: "opprog" }) : null,
|
|
2692
|
+
upgradeRec && (upgradeRec.status === "running" || upgradeRec.status === "queued") ? h(ProgressLine, { key: "upprog" }) : null,
|
|
2624
2693
|
installNote ? h("div", { key: "opnote", className: installNote.kind === "err" ? "dshm-err" : installNote.kind === "hint" ? "dshm-hint" : "dshm-ok" }, installNote.text) : null,
|
|
2625
2694
|
installNote && installNote.kind === "ok" ? h("div", { key: "opnote-rh", className: "dshm-hint" }, lookup(profileKind === "desktop" ? "profile.restartHint" : "banner.done")) : null,
|
|
2626
2695
|
h(
|
|
2627
2696
|
"div",
|
|
2628
2697
|
{ className: "dsvm-modalactions" },
|
|
2629
|
-
it.installed ? h(
|
|
2698
|
+
it.installed ? h(
|
|
2699
|
+
React.Fragment,
|
|
2700
|
+
null,
|
|
2701
|
+
it.outdated ? h("button", { className: "dshm-btn primary", disabled: upgradeBusy, onClick: () => onUpgrade(it) }, upgradeBusy ? h(Spin) : lookup("action.upgrade")) : null,
|
|
2702
|
+
h("span", { className: "dshm-hint" }, lookup("manage.hint"))
|
|
2703
|
+
) : h("button", { className: "dshm-btn primary", disabled: busy, onClick: () => onInstall(it) }, busy ? h(Spin) : lookup("action.install"))
|
|
2630
2704
|
)
|
|
2631
2705
|
),
|
|
2632
2706
|
lb !== null && shots.length ? h(Lightbox, { shots, index: lb, onNav: setLb, onClose: () => setLb(null) }) : null
|
|
@@ -2659,6 +2733,7 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2659
2733
|
const [pageJump, setPageJump] = useState("");
|
|
2660
2734
|
const activeInstallRec = ops.records.find((r) => r.kind === "install" && (r.status === "running" || r.status === "queued" || r.status === "input")) || null;
|
|
2661
2735
|
const activeInstallTarget = activeInstallRec ? activeInstallRec.target : null;
|
|
2736
|
+
const activeUpgradeRec = ops.records.find((r) => r.kind === "upgrade" && (r.status === "running" || r.status === "queued" || r.status === "input")) || null;
|
|
2662
2737
|
const [installNote, setInstallNote] = useState(null);
|
|
2663
2738
|
const items = data && data.items || [];
|
|
2664
2739
|
const total = data && data.total || 0;
|
|
@@ -2751,6 +2826,36 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2751
2826
|
}
|
|
2752
2827
|
}
|
|
2753
2828
|
};
|
|
2829
|
+
const doUpgrade = async (it) => {
|
|
2830
|
+
try {
|
|
2831
|
+
const res = await ops.runOp("upgrade", it.installedPkg, () => api("upgrade", { pkg: it.installedPkg }));
|
|
2832
|
+
const note = upgradeNotify2(res.activation);
|
|
2833
|
+
const text = lookup("notify.upgraded", { pkg: res.pkg, from: res.fromVersion ? `v${res.fromVersion}` : "\u2014", to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (note.suffixKey ? lookup(note.suffixKey) : "");
|
|
2834
|
+
notify({ kind: "ok", needsRestart: note.needsRestart, text });
|
|
2835
|
+
setInstallNote({ id: it.id, kind: "ok", text });
|
|
2836
|
+
await (onMutation ? onMutation() : void 0);
|
|
2837
|
+
} catch (e) {
|
|
2838
|
+
if (e && e.opSuperseded) {
|
|
2839
|
+
const sup = lookup("op.superseded.note", { target: it.installedPkg });
|
|
2840
|
+
notify({ kind: "ok", text: sup });
|
|
2841
|
+
setInstallNote({ id: it.id, kind: "hint", text: sup });
|
|
2842
|
+
} else if (e && e.guard) {
|
|
2843
|
+
const guardText = [
|
|
2844
|
+
lookup("guard.blocked"),
|
|
2845
|
+
`${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
|
|
2846
|
+
e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
|
|
2847
|
+
lookup("guard.noforce"),
|
|
2848
|
+
e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
|
|
2849
|
+
].filter(Boolean).join(" | ");
|
|
2850
|
+
notify({ kind: "err", text: guardText });
|
|
2851
|
+
setInstallNote({ id: it.id, kind: "err", text: guardText });
|
|
2852
|
+
} else {
|
|
2853
|
+
const failText = lookup("failed.upgrade", { err: e && e.message || e });
|
|
2854
|
+
notify({ kind: "err", text: failText });
|
|
2855
|
+
setInstallNote({ id: it.id, kind: "err", text: failText });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
};
|
|
2754
2859
|
const CompatDialog = compatConfirm ? h(
|
|
2755
2860
|
"div",
|
|
2756
2861
|
{ className: "dshm-compat-overlay", ...backdropCloseHandlers2(() => setCompatConfirm(null)) },
|
|
@@ -2822,6 +2927,9 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2822
2927
|
busy: activeInstallTarget === favDetailItem.id,
|
|
2823
2928
|
onClose: () => setFavDetailItem(null),
|
|
2824
2929
|
onInstall: (it2) => doInstall(it2),
|
|
2930
|
+
onUpgrade: (it2) => doUpgrade(it2),
|
|
2931
|
+
upgradeBusy: activeUpgradeRec != null,
|
|
2932
|
+
upgradeRec: activeUpgradeRec && activeUpgradeRec.target === favDetailItem.installedPkg ? activeUpgradeRec : null,
|
|
2825
2933
|
profileKind,
|
|
2826
2934
|
installRec: activeInstallRec && activeInstallRec.target === favDetailItem.id ? activeInstallRec : null,
|
|
2827
2935
|
installNote: installNote && installNote.id === favDetailItem.id ? installNote : null
|
|
@@ -2962,12 +3070,27 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2962
3070
|
labels: communityLabels(data),
|
|
2963
3071
|
active: query.category,
|
|
2964
3072
|
onPick: (id) => updateQuery({ category: id, offset: 0 }),
|
|
2965
|
-
trailing: filterTrigger
|
|
3073
|
+
trailing: filterTrigger,
|
|
3074
|
+
// 0.9.45 U3:跨桶条目在每桶双计(alsoCategories)→ Σchips 可大于总数;有跨桶时给容器 title 说明
|
|
3075
|
+
wrapTitle: zone === "primary" && data ? Object.values(counts).reduce((a, b) => a + (Number(b) || 0), 0) > (data.registryState && data.registryState.count || 0) ? lookup("chips.crossbucket.tip") : void 0 : void 0
|
|
2966
3076
|
}),
|
|
2967
3077
|
// 0.9.15:安装目标条目的详情 Modal 打开时,进度行入 Modal、底层行让位(避免隔着遮罩双重透出);
|
|
2968
3078
|
// Modal 关闭后底层行照常回归(关闭弹窗的安装仍可见)
|
|
2969
3079
|
activeInstallTarget && !(detailItem && detailItem.id === activeInstallTarget) ? h(ProgressLine, { key: "prog" }) : null,
|
|
2970
|
-
loading && !data ? h("div", { className: "dshm-empty" }, lookup("market.loading"), Spin()) : error ? h(
|
|
3080
|
+
loading && !data ? h("div", { className: "dshm-empty" }, lookup("market.loading"), Spin()) : error ? h(
|
|
3081
|
+
"div",
|
|
3082
|
+
{ className: "dshm-err" },
|
|
3083
|
+
lookup("failed.load", { err: error }),
|
|
3084
|
+
" ",
|
|
3085
|
+
// 0.9.45 U9:错误就地恢复出口(此前只能关开面板或绕道设置页)
|
|
3086
|
+
h("button", { className: "dshm-btn sm", onClick: () => reload(false) }, lookup("market.retry"))
|
|
3087
|
+
) : items.length === 0 ? h(
|
|
3088
|
+
"div",
|
|
3089
|
+
{ className: "dshm-empty" },
|
|
3090
|
+
lookup(
|
|
3091
|
+
data && data.registryState.status === "unavailable" && !query.query && !query.category ? "market.empty.unavailable" : query.category ? "market.empty.category" : "market.empty"
|
|
3092
|
+
)
|
|
3093
|
+
) : h(
|
|
2971
3094
|
React.Fragment,
|
|
2972
3095
|
null,
|
|
2973
3096
|
h(
|
|
@@ -3006,6 +3129,9 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
3006
3129
|
busy: activeInstallTarget === detailItem.id,
|
|
3007
3130
|
onClose: () => setDetailId(null),
|
|
3008
3131
|
onInstall: (it2) => doInstall(it2),
|
|
3132
|
+
onUpgrade: (it2) => doUpgrade(it2),
|
|
3133
|
+
upgradeBusy: activeUpgradeRec != null,
|
|
3134
|
+
upgradeRec: activeUpgradeRec && activeUpgradeRec.target === detailItem.installedPkg ? activeUpgradeRec : null,
|
|
3009
3135
|
profileKind,
|
|
3010
3136
|
installRec: activeInstallRec && activeInstallRec.target === detailItem.id ? activeInstallRec : null,
|
|
3011
3137
|
installNote: installNote && installNote.id === detailItem.id ? installNote : null
|
|
@@ -3251,7 +3377,7 @@ function regSourceLabel(data) {
|
|
|
3251
3377
|
function configStatusLabel(status) {
|
|
3252
3378
|
return lookup(`settings.status.${status || "loading"}` || "settings.status.loading");
|
|
3253
3379
|
}
|
|
3254
|
-
function SettingsTab({ notify, onRegistryChanged }) {
|
|
3380
|
+
function SettingsTab({ notify, onRegistryChanged, onForceMarket }) {
|
|
3255
3381
|
const reg = useAsync((force) => api("registry", force ? { force: true } : {}), []);
|
|
3256
3382
|
const cfgState = useAsync(() => api("registry-config"), []);
|
|
3257
3383
|
const [busy, setBusy] = useState(false);
|
|
@@ -3273,6 +3399,7 @@ function SettingsTab({ notify, onRegistryChanged }) {
|
|
|
3273
3399
|
try {
|
|
3274
3400
|
await reg.reload(true);
|
|
3275
3401
|
await reloadRegistryState().catch(() => void 0);
|
|
3402
|
+
onForceMarket?.();
|
|
3276
3403
|
notify({ kind: "ok", text: lookup("registry.refreshed"), needsRestart: false });
|
|
3277
3404
|
} finally {
|
|
3278
3405
|
setBusy(false);
|
|
@@ -3522,7 +3649,7 @@ function snapshotOf(it) {
|
|
|
3522
3649
|
categoryLabel: it.categoryLabel,
|
|
3523
3650
|
source: it.source
|
|
3524
3651
|
};
|
|
3525
|
-
for (const k of ["descriptionEn", "npm", "github", "homepage", "owner", "downloads", "stars", "added", "deprecated"]) {
|
|
3652
|
+
for (const k of ["descriptionEn", "npm", "github", "homepage", "owner", "downloads", "stars", "added", "deprecated", "verified", "audience", "decoupled"]) {
|
|
3526
3653
|
if (it[k] !== void 0 && it[k] !== null) s[k] = it[k];
|
|
3527
3654
|
}
|
|
3528
3655
|
return s;
|
|
@@ -3581,7 +3708,10 @@ function FavoriteZone({ favorites, onOpen }) {
|
|
|
3581
3708
|
name: s.name,
|
|
3582
3709
|
badges: [
|
|
3583
3710
|
s.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
|
|
3584
|
-
|
|
3711
|
+
// 0.9.45 U12(R7 收窄):无 owner = 精选条目——补「精选」身份徽标;verified 质量徽标恢复;
|
|
3712
|
+
// 受众/解耦徽标不进收藏区(DESIGN §2.7 裁决⑤「已装视图与收藏页不打标」),快照仅存字段
|
|
3713
|
+
s.owner ? h("span", { className: "dshm-badge info", key: "c" }, lookup("badge.community")) : h("span", { className: "dshm-badge", key: "cz" }, lookup("zone.primary")),
|
|
3714
|
+
Array.isArray(s.verified) && s.verified.length && s.community !== true ? h("span", { className: "dshm-badge", key: "v", title: s.verified.join("\u3001") }, lookup("badge.verified")) : null,
|
|
3585
3715
|
h("span", { className: "dshm-badge info", key: "s" }, s.source === "npm" ? "npm" : "github"),
|
|
3586
3716
|
isStale ? h("span", { className: "dshm-badge warn", key: "st" }, lookup("favorites.stalebadge")) : null
|
|
3587
3717
|
],
|
|
@@ -3960,7 +4090,7 @@ function MarketPanel({ onClose }) {
|
|
|
3960
4090
|
{ className: "dshm-body" },
|
|
3961
4091
|
tab === "market" ? h(MarketTab, { notify, markets, onMutation: refreshViews, ops, favorites, profileKind: profile?.kind ?? null }) : null,
|
|
3962
4092
|
tab === "installed" ? h(InstalledTab, { notify, installed, updates, onMutation: refreshViews, ops }) : null,
|
|
3963
|
-
tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged }) : null,
|
|
4093
|
+
tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged, onForceMarket: () => marketReloadAll(true) }) : null,
|
|
3964
4094
|
h(OperationsPanel, {
|
|
3965
4095
|
records: opRecords,
|
|
3966
4096
|
onClearFinished: () => {
|
package/lib/core/host-api.js
CHANGED
|
@@ -13,7 +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 { isNewerVersion, npmLatest } from './versions.js';
|
|
16
|
+
import { isNewerVersion, npmLatest, npmPackumentReadme } from './versions.js';
|
|
17
17
|
import { getCommunitySummary } from './community.js';
|
|
18
18
|
import { RegistryConfigError } from './registry-controller.js';
|
|
19
19
|
import { checkRegistryEntries } from './registry-check.js';
|
|
@@ -216,6 +216,8 @@ export function createApiDispatcher(ctx) {
|
|
|
216
216
|
selfUpgrade: ctx.deps?.selfUpgrade ?? selfUpgrade,
|
|
217
217
|
checkRegistryEntries: ctx.deps?.checkRegistryEntries ?? checkRegistryEntries,
|
|
218
218
|
npmLatest: ctx.deps?.npmLatest ?? npmLatest,
|
|
219
|
+
readInstalledPluginReadme: ctx.deps?.readInstalledPluginReadme ?? readInstalledPluginReadme,
|
|
220
|
+
npmPackumentReadme: ctx.deps?.npmPackumentReadme ?? npmPackumentReadme,
|
|
219
221
|
getCommunitySummary: ctx.deps?.getCommunitySummary ?? getCommunitySummary,
|
|
220
222
|
runTransaction: ctx.deps?.runTransaction ?? runProfileTransaction,
|
|
221
223
|
scheduleRestart: ctx.deps?.scheduleRestart ?? scheduleRestart,
|
|
@@ -362,6 +364,11 @@ export function createApiDispatcher(ctx) {
|
|
|
362
364
|
throw new ApiProtocolError(400, '非法 sort: 需 { field: downloads|stars|added, dir: asc|desc }');
|
|
363
365
|
}
|
|
364
366
|
}
|
|
367
|
+
// probeMode 两态(0.9.45 市场页两段加载,ADR-0013):GUI 两段式自带;非法值 400(与 source/sort 同款不静默吞)
|
|
368
|
+
const probeModeRaw = typeof body.probeMode === 'string' ? body.probeMode : undefined;
|
|
369
|
+
if (probeModeRaw !== undefined && probeModeRaw !== 'full' && probeModeRaw !== 'cache-only') {
|
|
370
|
+
throw new ApiProtocolError(400, `非法 probeMode: ${probeModeRaw}(需 full/cache-only)`);
|
|
371
|
+
}
|
|
365
372
|
const result = await d.listMarket(cfg(), {
|
|
366
373
|
query: strArg(body, 'query'),
|
|
367
374
|
category,
|
|
@@ -374,6 +381,7 @@ export function createApiDispatcher(ctx) {
|
|
|
374
381
|
limit,
|
|
375
382
|
force: boolArg(body.force),
|
|
376
383
|
withLatest: true,
|
|
384
|
+
probeMode: probeModeRaw,
|
|
377
385
|
namespace: 'host',
|
|
378
386
|
// 0.9.5 双 profile 读路径接线(实机回归 2026-10-01):desktop 下市场「已安装」
|
|
379
387
|
// 徽标此前漏传 profile → listMarket 内部落回 webProfileDir,已装徽标恒空。
|
|
@@ -430,8 +438,23 @@ export function createApiDispatcher(ctx) {
|
|
|
430
438
|
const target = strArg(body, 'pkg');
|
|
431
439
|
if (!target)
|
|
432
440
|
throw new ApiProtocolError(400, '缺少 pkg');
|
|
433
|
-
// 0.9.0 双 profile:README
|
|
434
|
-
|
|
441
|
+
// 0.9.0 双 profile:README 优先只读当前 profile 的 node_modules;
|
|
442
|
+
// 0.9.45 U10b 修复:市场/收藏详情 Modal 的 README 折叠页对「未安装」条目也会发起读取
|
|
443
|
+
// (装前决策场景)——本地未装时回源 npm packument 顶层 readme 字段兜底;
|
|
444
|
+
// 兜底也失败(离线等)则抛原始本地错误,不冒充成功。
|
|
445
|
+
let result;
|
|
446
|
+
try {
|
|
447
|
+
result = await d.readInstalledPluginReadme(target, profile.dir);
|
|
448
|
+
}
|
|
449
|
+
catch (localErr) {
|
|
450
|
+
try {
|
|
451
|
+
const fallback = await d.npmPackumentReadme(target);
|
|
452
|
+
result = { pkg: target, name: target, readme: fallback.readme, truncated: false };
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
throw localErr;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
435
458
|
payload = { ...result };
|
|
436
459
|
break;
|
|
437
460
|
}
|
package/lib/core/latest-cache.js
CHANGED
|
@@ -35,6 +35,14 @@ export function readLatestCache(key, ttlMin) {
|
|
|
35
35
|
}
|
|
36
36
|
return entry.value;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* peek 读取(0.9.45,ADR-0013):不判 TTL、不删除——供 force 全页重探时旧值兜底展示。
|
|
40
|
+
* 落实 ADR-0006 在案约束:「force 穿透须用 peek 不删除语义,而非 ttlMin=0 先删后探(失败时旧值丢失)」;
|
|
41
|
+
* force 重探成功覆盖缓存,失败保留旧值 + latestError,不产生空徽标窗口。
|
|
42
|
+
*/
|
|
43
|
+
export function peekLatestCache(key) {
|
|
44
|
+
return latestCache.get(key)?.value ?? null;
|
|
45
|
+
}
|
|
38
46
|
/** 内存 set(超上限淘汰最旧);纯内存,无任何落盘。 */
|
|
39
47
|
export function writeLatestCache(key, value) {
|
|
40
48
|
if (latestCache.size >= LATEST_CACHE_MAX) {
|
package/lib/core/market.js
CHANGED
|
@@ -24,7 +24,7 @@ import { normalizeSearchText, relevanceScore, tokenizeSearchText } from './searc
|
|
|
24
24
|
import { GithubBudgetExhaustedError, createGithubRequestBudget, isExactVersion } from './versions.js';
|
|
25
25
|
import { classifyUpgradeActivation } from './activation.js';
|
|
26
26
|
import { HttpError } from './httpx.js';
|
|
27
|
-
import { ensureLatestCacheSwept, invalidateLatestCache, latestCacheKey, latestItemId, readLatestCache, writeLatestCache } from './latest-cache.js';
|
|
27
|
+
import { ensureLatestCacheSwept, invalidateLatestCache, latestCacheKey, latestItemId, peekLatestCache, readLatestCache, writeLatestCache } from './latest-cache.js';
|
|
28
28
|
import { verifyInstalledAdditions } from './install-guard.js';
|
|
29
29
|
import { fetchTextLimited } from './httpx.js';
|
|
30
30
|
import { githubLatestTag as defaultGithubLatestTag, isNewerVersion, npmLatest as defaultNpmLatest, npmVersion, } from './versions.js';
|
|
@@ -527,53 +527,69 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
527
527
|
if (withLatest && items.length > 0) {
|
|
528
528
|
// 0.9.20:latest 缓存纯内存(ADR-0006)——探测段前仅一次性清扫 0.9.14 遗留磁盘信封
|
|
529
529
|
await ensureLatestCacheSwept({ namespace, profile: opts.profile });
|
|
530
|
-
//
|
|
530
|
+
// 0.9.45 探测两态(ADR-0013):'full'(缺省,行为不变)| 'cache-only'(两段加载第一段:零网络)
|
|
531
|
+
const mode = opts.probeMode ?? 'full';
|
|
532
|
+
// force(P2,ADR-0013):peek 旧值兜底 + 全页重探——不走 ttlMin=0(ADR-0006 在案约束:
|
|
533
|
+
// 先删后探失败会丢旧值);重探成功覆盖缓存,失败保留旧值 + latestError,无空徽标窗口
|
|
534
|
+
const forceProbe = mode !== 'cache-only' && opts.force === true;
|
|
531
535
|
const ttlMin = Math.max(0, cfg.cacheTtlMin ?? 60);
|
|
536
|
+
// 先吃 cache 命中(force 用 peek:不判 TTL、不删除)
|
|
532
537
|
for (const item of items) {
|
|
533
|
-
const
|
|
538
|
+
const key = latestCacheKey(namespace, loaded.configuredAddress, item);
|
|
539
|
+
const cached = forceProbe ? peekLatestCache(key) : readLatestCache(key, ttlMin);
|
|
534
540
|
if (cached)
|
|
535
541
|
applyProbe(item, cached);
|
|
536
542
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
it.
|
|
543
|
+
if (mode === 'cache-only') {
|
|
544
|
+
// 两段加载第一段:零网络;缺口判定镜像 todo 豁免(社区 github 永不探测 → 不构成第二段理由,Q46)
|
|
545
|
+
const gap = items.some((it) => it.latestVersion === undefined &&
|
|
546
|
+
it.latestTag === undefined &&
|
|
547
|
+
it.latestSha === undefined &&
|
|
548
|
+
!(it.community === true && it.source === 'github'));
|
|
549
|
+
latestComplete = !gap;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
const todo = items.filter((it) =>
|
|
540
553
|
// Q46 探测边界:社区 github 条目不做浏览页 REST 探测(配额不可控);社区 npm 条目照常
|
|
541
|
-
!(it.community === true && it.source === 'github')
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
item
|
|
548
|
-
|
|
549
|
-
latestComplete = false;
|
|
550
|
-
latestTimedOut = true;
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
await mapWithConcurrency(todo, LATEST_WORKERS, async (item) => {
|
|
554
|
-
const perBudget = Math.max(1, remaining());
|
|
555
|
-
const task = probeTask(item, d, Math.min(cfg.timeoutMs ?? 20_000, perBudget), signal);
|
|
556
|
-
const outcome = await probeWithBudget(task, perBudget, signal);
|
|
557
|
-
if (outcome.ok && outcome.value) {
|
|
558
|
-
applyProbe(item, outcome.value);
|
|
559
|
-
writeLatestCache(latestCacheKey(namespace, loaded.configuredAddress, item), outcome.value);
|
|
560
|
-
}
|
|
561
|
-
else if (outcome.timeout) {
|
|
562
|
-
item.latestError = '更新检查未完成:超时';
|
|
554
|
+
!(it.community === true && it.source === 'github') &&
|
|
555
|
+
// force:全页重探(缓存内旧值仅作兜底展示);常态:仅未命中条目
|
|
556
|
+
(forceProbe || (it.latestVersion === undefined && it.latestTag === undefined && it.latestSha === undefined)));
|
|
557
|
+
if (todo.length > 0) {
|
|
558
|
+
const budget = remaining();
|
|
559
|
+
if (budget <= 0) {
|
|
560
|
+
for (const item of todo) {
|
|
561
|
+
item.latestError = '更新检查未完成:时间预算已用尽';
|
|
563
562
|
item.latestErrorCode = 'timeout';
|
|
564
|
-
latestComplete = false;
|
|
565
|
-
latestTimedOut = true;
|
|
566
|
-
}
|
|
567
|
-
else if (outcome.error !== undefined) {
|
|
568
|
-
if (signal?.aborted)
|
|
569
|
-
throw abortError();
|
|
570
|
-
item.latestError = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
571
|
-
item.latestErrorCode = classifyLatestError(outcome.error);
|
|
572
563
|
}
|
|
573
|
-
|
|
564
|
+
latestComplete = false;
|
|
565
|
+
latestTimedOut = true;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
await mapWithConcurrency(todo, LATEST_WORKERS, async (item) => {
|
|
569
|
+
const perBudget = Math.max(1, remaining());
|
|
570
|
+
const task = probeTask(item, d, Math.min(cfg.timeoutMs ?? 20_000, perBudget), signal);
|
|
571
|
+
const outcome = await probeWithBudget(task, perBudget, signal);
|
|
572
|
+
if (outcome.ok && outcome.value) {
|
|
573
|
+
applyProbe(item, outcome.value);
|
|
574
|
+
writeLatestCache(latestCacheKey(namespace, loaded.configuredAddress, item), outcome.value);
|
|
575
|
+
}
|
|
576
|
+
else if (outcome.timeout) {
|
|
577
|
+
item.latestError = '更新检查未完成:超时';
|
|
578
|
+
item.latestErrorCode = 'timeout';
|
|
579
|
+
latestComplete = false;
|
|
580
|
+
latestTimedOut = true;
|
|
581
|
+
}
|
|
582
|
+
else if (outcome.error !== undefined) {
|
|
583
|
+
if (signal?.aborted)
|
|
584
|
+
throw abortError();
|
|
585
|
+
item.latestError = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
586
|
+
item.latestErrorCode = classifyLatestError(outcome.error);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
574
590
|
}
|
|
575
591
|
}
|
|
576
|
-
// outdated 判定统一在 probe
|
|
592
|
+
// outdated 判定统一在 probe 后进行(0.9.45:cache-only 也执行——暖缓存不丢「可升级」徽标)
|
|
577
593
|
for (const item of items) {
|
|
578
594
|
const inst = item.installedPkg !== undefined ? installedItems.find((i) => i.pkg === item.installedPkg) : undefined;
|
|
579
595
|
if (!inst)
|
package/lib/core/versions.js
CHANGED
|
@@ -113,6 +113,25 @@ export async function npmPackument(pkg, timeoutMs = 20_000, signal, registry, de
|
|
|
113
113
|
const versions = data?.versions !== null && typeof data?.versions === 'object' ? Object.keys(data.versions) : [];
|
|
114
114
|
return { versions };
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* npm packument readme 兜底(0.9.45 U10b 修复):市场/收藏详情 Modal 的 README 折叠页对
|
|
118
|
+
* 「未安装」条目无法走本地 profile 读取——回源 packument 顶层 readme 字段(publish 管线
|
|
119
|
+
* 对多数包写入)。安全基线同 npmPackument:HTTPS、8MB 上限、超时;无 readme 字段返回空串
|
|
120
|
+
* (不抛,客户端显示「没有 README」);L1(ADR-0012):显式 registry 参数 > 生效源。
|
|
121
|
+
*/
|
|
122
|
+
export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, registry, deps) {
|
|
123
|
+
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
124
|
+
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
125
|
+
const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
|
|
126
|
+
const base = registry?.trim() ? registry.trim() : await activeNpmRegistry();
|
|
127
|
+
const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}`, {
|
|
128
|
+
timeoutMs,
|
|
129
|
+
signal,
|
|
130
|
+
maxBytes: 8 * 1024 * 1024,
|
|
131
|
+
});
|
|
132
|
+
const readme = typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
|
|
133
|
+
return { readme };
|
|
134
|
+
}
|
|
116
135
|
/**
|
|
117
136
|
* 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
|
|
118
137
|
* (首腿超时收紧 `Math.min(timeoutMs, 5_000)`),失败降级生效源(≠npmjs 时);
|