dsh-m 0.9.86 → 0.9.88
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 +28 -0
- package/lib/client.js +322 -94
- package/lib/core/community.js +20 -7
- package/lib/core/host-api.js +23 -2
- package/lib/core/installed.js +68 -37
- package/lib/core/market.js +108 -5
- package/lib/core/merged-market.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.88 特性:面板三区 resize——尺寸记忆 + 就地生长(ADR-0021)+ 验收三修
|
|
12
|
+
|
|
13
|
+
- **需求(2026-10-15)**:市场面板「920px 浏览卡片太挤、全屏又太大」的真实档位空洞 + 桌面窗口本能手势(下意识移向边缘想 resize)。`/grill-with-docs` 两轮锁定设计:三区手柄(右/下/右下角)、就地生长锚定、尺寸记忆全套;实施计划经独立评审 Agent 三轮对抗审核(计划 11 条 + 执行 2 条全闭环,含两条方法论沉淀:`treeShaking:false` 下「bundle 标识符缺席」类预言必假、esbuild 默认 charset=ascii 下中文 bundle 锚必假)。
|
|
14
|
+
- **方案(ADR-0021)**:① 纯逻辑 `window-resize.js`——`clampSize`(下限 640×460、视口−8px 边距、`.abs` 形态 origin 第二钳保右/下缘不破边距、视口小于下限退化)+ 尺寸记忆存取(与位置记忆逐字对称);② 手势状态机逐条镜像 ADR-0017(rAF 帧内写/手势帧零 setState/setPointerCapture/主键门卫/多指守卫/卸载清理);③ 热区 6/8px 边 + 14×14 角,cursor 即全部供性,全屏/窄视口/触摸四重豁免;④ reclamp 升维:先夹尺寸、后夹位置。
|
|
15
|
+
- **主人实测三修(同版)**:头部两行重排断点 600→720(resize min 640 落入两行区,标题/seg 零折行);详情弹窗中段包进 `modalbody`(README 任意展开后 ✕ 与「安装」双端常驻);设置网格三列门控由 `.full` 改容器宽 ≥1100(还原态拉宽同样升三列,顺带修正小视口全屏强塞三列旧账)。
|
|
16
|
+
- **实验记录**:tab 切换淡入当日实现当日被主人否决,整体回滚(字节级干净,评审复核在案)。
|
|
17
|
+
- **验证**:全量 1496 测试(+24:纯逻辑 18 + 冒烟 3 + 三修源锚 3)、typecheck 零错误;CDP 量化矩阵 14 格全过(列数阈值 640→1/920→2/1140→3/1500→4、钳位下限 640×460 与上限、视口缩小双夹紧、720 断点、modalbody、coarse 豁免);Performance CDP 实测与 Windows 壳冒烟按计划延后(前者有代码级保证:手势帧零 setState)。验收记录(诚实口径):dsh-workspace `01_docs/research/2026-10-15-dshm-panel-resize-sweep-record.md`。
|
|
18
|
+
|
|
19
|
+
### 0.9.87 性能:热路径四连——已装枚举并行化 + patchRows 按需采集 + 收藏 stale 批量化 + 设置页社区 summary 接入 L2
|
|
20
|
+
|
|
21
|
+
- **问题(2026-10-15 架构走查 + 独立复核)**:市场查询热路径上仍有三处重复支付——① `listInstalledPlugins` 逐依赖**串行**双文件读(package.json + cordis.patch.yml),每次查询/翻页/每次防抖搜索提交都全量重跑(14 依赖实测 ~11ms,占暖缓存查询近半预算);② 收藏分区 stale 检测对**每个收藏发一整条 market 查询**(≤8 并发扇出):回答一个布尔值却付全管线(枚举+社区+4445 条评分+排序+分页),20 收藏 ≈ 3 批 × 全管线;且旧路径存在 curatedFirst 挤出边界误判——primary 域同词命中 ≥8 条时 id-exact 社区条目被前置挤出前 8,**误判 stale**;③ 设置页 registry 方法背后的 `getCommunitySummary` 走无 memo 的全量 adapt+merge(~4400 条,每次打开 ~10-50ms CPU),而同身份的 L2 物化代(ADR-0016)近在咫尺。
|
|
22
|
+
- **方案(热路径四连)**:① 枚举并行化——每依赖分类独立无跨依赖状态,`Promise.all` 保序折叠(输出仍按依赖键字典序),新增可选 `{ patchRows: false }`:市场热路径不再付每插件 ~0.55ms 的 YAML CST 解析(patchRows 仅 enablement/toggle 消费,已装页路径保持缺省采集;退出时**省略字段**而非伪造 `readable:false`);② 批量 id 存在性——core 新增 `existingMarketIds`(三不原则:不枚举已装/不探测 latest/不分页;merged 域原始 id Set 判定,与旧搜索扇出除 curatedFirst 边界外逐点一致,该边界上属**修复**)、host 新 method `market-ids-exist`(ids ≤500 校验 400)、客户端单发批量 + 失败保守全 stale;③ `getCommunitySummary` 改道 `mergedOutcome`(同身份免 adapt+merge,「summary 永不入代」保持;registry 传 `snap.loaded` 整体,namespace/profile 显式缺省与改道前缓存路径逐字节同路——保 parity 优先于共享代的自觉取舍);④ `toMarketItem` 逐页线性扫描改每查询一次已装索引(byPkg/byName/byGithub 三域 first-wins + min-ord,与 `matchInstalledByEntry` 逐点等价,随机 fuzz 640 条零不一致),`GITHUB_SPEC_RE` 导出为单一事实源。
|
|
23
|
+
- **验证**:1472 项测试全绿(净增 7 例:枚举排序钉住/patchRows 退出对照/FP5 匹配等价四路径/existingMarketIds 四例含 L2 命中钉与 curatedFirst 边界钉/host method 正常+400/C3 源串锚迁移)、typecheck 零错误;实测(保守口径):枚举市场路径 10.55ms → 2.08ms(5.1×)、`listMarket` cache-only 端到端中位 ~28ms → 5-7ms、`existingMarketIds`(20 id) 2.52ms/次;两份实施计划均经独立评审 Agent 多轮对齐(FP1/FP5 两轮零保留通过;FP2/FP3+发版两轮 7+1 条意见全闭环)。
|
|
24
|
+
|
|
11
25
|
### 0.9.86 性能:packument 访问器——registry 元数据属主模块,登记网络腿出锁(钢人 C2 / ADR-0019)
|
|
12
26
|
|
|
13
27
|
- **问题(钢人报告 C2 节)**:「registry 元数据」没有 owning module——登记(Register)只为读一个发布时戳,却在官方文件锁内发起 8MiB 级全量 packument 网络取(npmjs 首腿 5s 超时 + 生效源备腿至 12s),镜像机器装机的白等直接计入关键路径;B3 预热再经无缓存通道把同一全量取付第二次;三 adapter 各自取、各自缓存(或没有)。
|
|
@@ -609,6 +623,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
609
623
|
|
|
610
624
|
## English
|
|
611
625
|
|
|
626
|
+
### Feature in 0.9.88 — three-zone panel resize: size memory + grow-in-place (ADR-0021) + acceptance fixes
|
|
627
|
+
|
|
628
|
+
- **Need (2026-10-15)**: a real gap between "cramped at 920px" and "overkill fullscreen", plus the instinctive desktop gesture of grabbing window edges. Design locked via two grill rounds: three handles (right/bottom/bottom-right), grow-in-place anchoring, full size memory; the implementation plan survived three independent adversarial review rounds (11 plan + 2 execution issues all closed, including two methodology findings: bundle-absence predictions are structurally false under `treeShaking:false`, and CJK bundle anchors are false under esbuild's default ascii charset).
|
|
629
|
+
- **Approach (ADR-0021)**: ① DOM-free `window-resize.js` — `clampSize` (min 640×460, viewport−8px margins, an origin second clamp keeping right/bottom edges inside margins for the freely-positioned form, degraded bounds on tiny viewports) + size-memory storage symmetric to position memory; ② gesture state machine mirrors ADR-0017 (rAF-batched writes, zero setState in gesture frames, setPointerCapture, primary-button guard, multi-pointer guard, unmount cleanup); ③ hot zones 6/8px edges + 14×14 corner, cursor-only affordance, four exemptions (fullscreen / narrow viewport / touch / scrollbar trade-off); ④ reclamp upgraded to size-first-then-position.
|
|
630
|
+
- **Acceptance fixes from live testing (same release)**: head two-row breakpoint 600→720 (min-width 640 no longer crushes the title); detail modal middle wrapped in `modalbody` (✕ and Install stay pinned however far the README expands); settings 3-column gate switched from `.full` to container ≥1100px (a widened restored panel now gets three columns too).
|
|
631
|
+
- **Experiment record**: a tab-switch fade was implemented and rejected by the owner the same day, fully reverted (byte-identical, reviewer-verified).
|
|
632
|
+
- **Verification**: 1496 tests green (+24), typecheck clean; CDP quantified matrix 14/14 cells passed (column thresholds 640→1/920→2/1140→3/1500→4, lower/upper clamping, dual reclamp on viewport shrink, 720 breakpoint, modalbody, coarse-pointer exemption); Performance CDP measurement and Windows shell smoke deferred per plan (the former backed by a code-level guarantee: zero setState in gesture frames). Honest acceptance record: dsh-workspace `01_docs/research/2026-10-15-dshm-panel-resize-sweep-record.md`.
|
|
633
|
+
|
|
634
|
+
### Performance in 0.9.87 — hot-path quartet: parallel installed enumeration, on-demand patchRows, batched favorites stale check, settings community summary via L2
|
|
635
|
+
|
|
636
|
+
- **Problem (2026-10-15 architecture walk + independent audit)**: three recurring costs remained on the market-query hot path - (1) `listInstalledPlugins` read each dependency's two files (package.json + cordis.patch.yml) **strictly serially**, re-running in full on every query / page turn / debounced search commit (~11ms for 14 deps, nearly half the warm-cache query budget); (2) the favorites stale check fired **one full market query per favorite** (fan-out of 8): a single boolean answer paid the entire pipeline (enumeration + community + scoring 4,445 entries + sort + paging), 20 favorites ≈ 3 batches × full pipeline; the old path also had a curatedFirst squeeze-out false-stale edge - with ≥8 primary same-token hits the id-exact community entry got pushed past the page-8 window; (3) the settings-page registry method's `getCommunitySummary` ran an unmemoized full adapt+merge (~4,400 entries, ~10-50ms CPU per open) while the same-identity L2 materialized generation (ADR-0016) sat one function away.
|
|
637
|
+
- **Fix (hot-path quartet)**: (1) parallel enumeration - per-dependency classification is independent, `Promise.all` folds in key order (output stays sorted by dependency key); new opt-out `{ patchRows: false }` spares the market path the ~0.55ms/plugin YAML CST parse (patchRows is only consumed by enablement/toggle, which keep the default; opting out **omits the field** instead of faking `readable:false`); (2) batch id-existence - new core `existingMarketIds` (three no's: no installed enumeration / no latest probing / no paging; raw-id Set membership over the merged domain, pointwise identical to the old search fan-out except the curatedFirst edge, where it is a **fix**), new host method `market-ids-exist` (ids ≤500, 400 on malformed), client sends one batch call and falls back to all-stale on error; (3) `getCommunitySummary` now routes through `mergedOutcome` (same identity skips adapt+merge; "summary never enters the generation" preserved; receives the whole `snap.loaded`, explicit namespace/profile defaults byte-identical to the pre-change cache paths - a deliberate parity-over-generation-sharing trade-off); (4) `toMarketItem`'s per-page linear scan became a once-per-query installed index (byPkg/byName/byGithub first-wins + min-ord, pointwise equal to `matchInstalledByEntry`, zero mismatches over a 640-entry random fuzz), with `GITHUB_SPEC_RE` exported as the single source of truth.
|
|
638
|
+
- **Verification**: 1,472 tests green (+7: enumeration ordering pin / patchRows opt-out contrast / FP5 four-path match equivalence / four existingMarketIds cases incl. L2-hit and curatedFirst-edge pins / host method ok+400 / migrated C3 source anchor), typecheck clean; measured (conservative baseline): market-path enumeration 10.55ms → 2.08ms (5.1×), `listMarket` cache-only end-to-end median ~28ms → 5-7ms, `existingMarketIds` (20 ids) 2.52ms/call; both implementation plans aligned over multiple independent review rounds (FP1/FP5 two rounds, zero reservations; FP2/FP3+release two rounds, 7+1 findings all closed).
|
|
639
|
+
|
|
612
640
|
### Performance in 0.9.86 — packument accessor: registry metadata gets an owning module, register's network leg leaves the lock (steelman C2 / ADR-0019)
|
|
613
641
|
|
|
614
642
|
- **Problem (steelman report, card C2)**: "registry metadata" had no owning module - Register fetched an 8MiB-scale full packument INSIDE the official file lock just to read one publish timestamp (npmjs first leg 5s timeout + effective-source leg up to 12s), putting mirror machines' white-wait directly on the install critical path; the B3 prewarm paid the same full fetch a second time through an uncached channel; three adapters each fetched, interpreted and cached (or not) independently.
|
package/lib/client.js
CHANGED
|
@@ -624,6 +624,66 @@ var init_window_drag = __esm({
|
|
|
624
624
|
}
|
|
625
625
|
});
|
|
626
626
|
|
|
627
|
+
// src/client/window-resize.js
|
|
628
|
+
var window_resize_exports = {};
|
|
629
|
+
__export(window_resize_exports, {
|
|
630
|
+
MIN_H: () => MIN_H,
|
|
631
|
+
MIN_W: () => MIN_W,
|
|
632
|
+
SIZE_KEY: () => SIZE_KEY,
|
|
633
|
+
clampSize: () => clampSize,
|
|
634
|
+
loadPanelSize: () => loadPanelSize,
|
|
635
|
+
savePanelSize: () => savePanelSize
|
|
636
|
+
});
|
|
637
|
+
function finiteInset2(v) {
|
|
638
|
+
return Number.isFinite(v) ? v : 0;
|
|
639
|
+
}
|
|
640
|
+
function clampSize(size, viewport, insets = ZERO_INSETS2, origin = null) {
|
|
641
|
+
const maxW = Math.max(0, viewport.width - finiteInset2(insets?.left) - finiteInset2(insets?.right) - PANEL_MARGIN * 2);
|
|
642
|
+
const maxH = Math.max(0, viewport.height - finiteInset2(insets?.top) - finiteInset2(insets?.bottom) - PANEL_MARGIN * 2);
|
|
643
|
+
const effMaxW = origin ? Math.min(maxW, Math.max(0, viewport.width - finiteInset2(insets?.right) - PANEL_MARGIN - finiteInset2(origin.left))) : maxW;
|
|
644
|
+
const effMaxH = origin ? Math.min(maxH, Math.max(0, viewport.height - finiteInset2(insets?.bottom) - PANEL_MARGIN - finiteInset2(origin.top))) : maxH;
|
|
645
|
+
const minW = Math.min(MIN_W, effMaxW);
|
|
646
|
+
const minH = Math.min(MIN_H, effMaxH);
|
|
647
|
+
return {
|
|
648
|
+
w: Math.min(Math.max(size.w, minW), effMaxW),
|
|
649
|
+
h: Math.min(Math.max(size.h, minH), effMaxH)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function loadPanelSize(storage) {
|
|
653
|
+
if (!storage || typeof storage.getItem !== "function") return null;
|
|
654
|
+
try {
|
|
655
|
+
const raw = storage.getItem(SIZE_KEY);
|
|
656
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
657
|
+
const parsed = JSON.parse(raw);
|
|
658
|
+
if (typeof parsed !== "object" || parsed === null || !Number.isFinite(parsed.w) || !Number.isFinite(parsed.h)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
return { w: parsed.w, h: parsed.h };
|
|
662
|
+
} catch {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function savePanelSize(storage, size) {
|
|
667
|
+
if (typeof size !== "object" || size === null || !Number.isFinite(size.w) || !Number.isFinite(size.h)) {
|
|
668
|
+
throw new TypeError(`invalid panel size: ${JSON.stringify(size)}`);
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
storage.setItem(SIZE_KEY, JSON.stringify({ w: size.w, h: size.h }));
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
var SIZE_KEY, MIN_W, MIN_H, ZERO_INSETS2;
|
|
676
|
+
var init_window_resize = __esm({
|
|
677
|
+
"src/client/window-resize.js"() {
|
|
678
|
+
"use strict";
|
|
679
|
+
init_window_drag();
|
|
680
|
+
SIZE_KEY = "dshm-panel-size";
|
|
681
|
+
MIN_W = 640;
|
|
682
|
+
MIN_H = 460;
|
|
683
|
+
ZERO_INSETS2 = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
|
|
627
687
|
// src/client/img-chain.js
|
|
628
688
|
var img_chain_exports = {};
|
|
629
689
|
__export(img_chain_exports, {
|
|
@@ -1958,6 +2018,7 @@ var { backdropCloseHandlers: backdropCloseHandlers2 } = (init_backdrop(), __toCo
|
|
|
1958
2018
|
var { lbStep: lbStep2, lbNeighbors: lbNeighbors2, swipeDir: swipeDir2 } = (init_lightbox(), __toCommonJS(lightbox_exports));
|
|
1959
2019
|
var { shotWheelAction: shotWheelAction2 } = (init_wheel_strip(), __toCommonJS(wheel_strip_exports));
|
|
1960
2020
|
var { dragSlop: dragSlop2, grabOffset: grabOffset2, titlebarTopInset: titlebarTopInset2, clampPoint: clampPoint2, loadPanelPos: loadPanelPos2, savePanelPos: savePanelPos2, isDragTarget: isDragTarget2 } = (init_window_drag(), __toCommonJS(window_drag_exports));
|
|
2021
|
+
var { clampSize: clampSize2, loadPanelSize: loadPanelSize2, savePanelSize: savePanelSize2 } = (init_window_resize(), __toCommonJS(window_resize_exports));
|
|
1961
2022
|
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));
|
|
1962
2023
|
var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
|
|
1963
2024
|
var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
|
|
@@ -2150,7 +2211,7 @@ var ZH = {
|
|
|
2150
2211
|
"pager.jump.ph": "\u9875\u53F7",
|
|
2151
2212
|
"panel.fullscreen": "\u5168\u5C4F",
|
|
2152
2213
|
"panel.restore": "\u8FD8\u539F",
|
|
2153
|
-
"panel.dragHint": "\u62D6\u62FD\u79FB\u52A8 \xB7 \u53CC\u51FB\
|
|
2214
|
+
"panel.dragHint": "\u62D6\u62FD\u79FB\u52A8 \xB7 \u53CC\u51FB\u5168\u5C4F \xB7 \u62D6\u8FB9\u89D2\u8C03\u5927\u5C0F",
|
|
2154
2215
|
"lb.title": "\u63D2\u4EF6\u622A\u56FE\u9884\u89C8",
|
|
2155
2216
|
"lb.prev": "\u4E0A\u4E00\u5F20\uFF08\u2190\uFF09",
|
|
2156
2217
|
"lb.next": "\u4E0B\u4E00\u5F20\uFF08\u2192\uFF09",
|
|
@@ -2450,7 +2511,7 @@ var EN = {
|
|
|
2450
2511
|
"pager.jump.ph": "Page",
|
|
2451
2512
|
"panel.fullscreen": "Fullscreen",
|
|
2452
2513
|
"panel.restore": "Restore",
|
|
2453
|
-
"panel.dragHint": "Drag to move \xB7 Double-click to
|
|
2514
|
+
"panel.dragHint": "Drag to move \xB7 Double-click to fullscreen \xB7 Drag edges to resize",
|
|
2454
2515
|
"lb.title": "Plugin screenshot preview",
|
|
2455
2516
|
"lb.prev": "Previous (\u2190)",
|
|
2456
2517
|
"lb.next": "Next (\u2192)",
|
|
@@ -2619,6 +2680,17 @@ var CSS = `
|
|
|
2619
2680
|
DSH Web / \u6D4F\u89C8\u5668\u65E0\u6B64\u53D8\u91CF \u2192 \u56DE\u843D 0px\uFF0C\u884C\u4E3A\u4E0E\u65E7\u7248\u9010\u5B57\u8282\u4E00\u81F4\u3002 */
|
|
2620
2681
|
.dshm-overlay.full{padding:0;top:var(--dsh-windows-titlebar-height,0px)}
|
|
2621
2682
|
.dshm-panel.full{width:100%;height:100%;border-radius:0}
|
|
2683
|
+
/* resize \u70ED\u533A\uFF08panel resize\uFF09\uFF1A\u900F\u660E\u8D34\u5185\u7F18\uFF08\u9762\u677F overflow:hidden \u4E0D\u53EF\u5916\u7F6E\uFF09\u3002
|
|
2684
|
+
\u8FB9 6/8px\u3001\u89D2 14\xD714\uFF08Q8 \u5B9A\u7A3F 12\u201316px \u5E26\u5185\uFF1B\u626B\u63CF\u671F\u53EF\u6821\u51C6\uFF09\u3002\u53F3\u70ED\u533A\u76D6\u4F4F .dshm-body
|
|
2685
|
+
\u7EB5\u5411\u6EDA\u52A8\u6761\u5916\u7F18 ~6px\uFF08\u4F59 ~9px \u53EF\u62D6\uFF09\u3001\u89D2\u70ED\u533A\u4E0E\u6EDA\u52A8\u6761 corner \u6B7B\u533A\u57FA\u672C\u91CD\u5408\uFF08Q4 \u63A5\u53D7\uFF09\u3002
|
|
2686
|
+
cursor \u5373\u5168\u90E8\u4F9B\u6027\u2014\u2014\u65E0\u89C6\u89C9\u63A7\u4EF6\uFF08Q8\uFF09\u3002z-index \u538B\u8FC7 body \u5185\u5BB9\u4F46\u4E0D\u8FDB\u5934\u90E8\u4EA4\u4E92\u533A
|
|
2687
|
+
\uFF08head padding 14px > 6px \u70ED\u533A\uFF0Cwinctl \u6309\u94AE\u65E0\u78B0\u649E\uFF09\u3002 */
|
|
2688
|
+
.dshm-rz{position:absolute;z-index:5;touch-action:none}
|
|
2689
|
+
.rz-e{top:0;right:0;width:6px;height:100%;cursor:ew-resize}
|
|
2690
|
+
.rz-s{bottom:0;left:0;width:100%;height:8px;cursor:ns-resize}
|
|
2691
|
+
.rz-se{right:0;bottom:0;width:14px;height:14px;cursor:nwse-resize}
|
|
2692
|
+
.dshm-panel.full .dshm-rz{display:none} /* belt \u2460\uFF1A\u5168\u5C4F\u6001\u624B\u67C4\u4E0D\u6E32\u67D3\uFF0CCSS \u515C\u5E95 */
|
|
2693
|
+
@media (pointer:coarse){.dshm-rz{display:none}} /* belt \u2461\uFF1Acoarse \u6307\u9488\u6574\u7EC4\u9690\u85CF\uFF08Q8\uFF09 */
|
|
2622
2694
|
/* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u7B49\u5BBD\u4E24\u683C + \u53D1\u4E1D\u5206\u9694\u7EBF\uFF0C\u7EDF\u4E00\u7EBF\u6761\u56FE\u6807 */
|
|
2623
2695
|
/* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7/0.7.8\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u900F\u660E\u5E95 + \u53D1\u4E1D\u5916\u6846\uFF0C\u4E0E\u641C\u7D22\u6E05\u9664\u94AE\u540C\u4E00\u914D\u8272\u8BED\u8A00\uFF1B\u5BBD\u683C\u9632\u8BEF\u89E6 */
|
|
2624
2696
|
.dshm-winctl{display:inline-flex;align-items:stretch;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:9px;background:transparent;overflow:hidden;flex:none}
|
|
@@ -2628,10 +2700,10 @@ var CSS = `
|
|
|
2628
2700
|
.dshm-winctl button:active{background:var(--dsw-alias-fill-secondary,rgba(127,127,127,.2))}
|
|
2629
2701
|
.dshm-winctl button.danger:hover{background:#e5484d;color:#fff}
|
|
2630
2702
|
.dshm-head{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
|
|
2631
|
-
.dshm-title{font-weight:600;font-size:15px;margin-right:2px}
|
|
2703
|
+
.dshm-title{font-weight:600;font-size:15px;margin-right:2px;white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
2632
2704
|
.dshm-head-divider{width:1px;height:18px;background:var(--dsw-alias-border-l2,#e5e7eb);flex:none}
|
|
2633
2705
|
.dshm-seg{display:inline-flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--dsw-alias-border-l2,#e2e4e8);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f5f6f8)}
|
|
2634
|
-
.dshm-seg button{appearance:none;border:0;background:transparent;height:28px;padding:0 14px;border-radius:7px;font:inherit;font-size:12px;color:var(--dsw-alias-label-tertiary,#7b8088);cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background .15s,color .15s,box-shadow .15s}
|
|
2706
|
+
.dshm-seg button{appearance:none;border:0;background:transparent;height:28px;padding:0 14px;border-radius:7px;font:inherit;font-size:12px;color:var(--dsw-alias-label-tertiary,#7b8088);cursor:pointer;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;transition:background .15s,color .15s,box-shadow .15s}
|
|
2635
2707
|
.dshm-seg button:hover{color:var(--dsw-alias-label-secondary,#4b5058)}
|
|
2636
2708
|
.dshm-seg button.on{background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-primary,#17191c);font-weight:600;box-shadow:var(--dsw-shadow-lv1,0 2px 8px rgb(20 24 32 / 8%))}
|
|
2637
2709
|
.dsvm-filterbtn{appearance:none;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-elevated,transparent);color:var(--dsw-alias-label-secondary,#4b5563);border-radius:8px;padding:4px 12px;font:inherit;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;transition:color .15s,border-color .15s,background .15s}
|
|
@@ -2707,7 +2779,8 @@ var CSS = `
|
|
|
2707
2779
|
.dsvm-pagebtn.on{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5);color:var(--dsw-alias-interactive-bg-selected,#4f46e5);font-weight:600}
|
|
2708
2780
|
.dsvm-pagebtn:disabled{opacity:.4;cursor:default}
|
|
2709
2781
|
.dsvm-modal{position:fixed;inset:0;z-index:2147483100;background:var(--dsw-alias-bg-mask-3,rgba(15,23,42,.55));display:flex;align-items:center;justify-content:center;padding:24px 16px}
|
|
2710
|
-
.dsvm-modalbox{width:min(720px,100%);max-height:86vh;overflow:
|
|
2782
|
+
.dsvm-modalbox{width:min(720px,100%);max-height:86vh;overflow:hidden;background:var(--dsw-alias-bg-elevated,#fff);background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 94%,transparent);backdrop-filter:blur(14px);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);padding:16px;display:flex;flex-direction:column;gap:10px}
|
|
2783
|
+
.dsvm-modalbody{overflow:auto;min-height:0;overscroll-behavior:contain;display:flex;flex-direction:column;gap:10px}
|
|
2711
2784
|
.dsvm-modalhead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
2712
2785
|
.dsvm-kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:12px;margin:0}
|
|
2713
2786
|
.dsvm-kv dt{color:var(--dsw-alias-label-caption,#6b7280);white-space:nowrap}
|
|
@@ -2860,8 +2933,13 @@ button.dshm-badge:hover{filter:brightness(.95)}
|
|
|
2860
2933
|
.dshm-settings-grid>.dshm-section{margin:0;min-width:0}
|
|
2861
2934
|
.dshm-settings-grid>.dshm-section:nth-child(3){grid-column:1/-1}
|
|
2862
2935
|
.dshm-settings-grid>.dshm-ops-card{grid-column:1/-1}
|
|
2863
|
-
.
|
|
2864
|
-
|
|
2936
|
+
/* \u4E09\u5217\u5236\u95E8\u63A7 = \u5BB9\u5668\u5BBD\u5EA6\u800C\u975E\u5168\u5C4F\u6001\uFF080.9.88\uFF0Cpanel resize \u4F34\u751F\u4FEE\u590D\uFF09\uFF1A\u8FD8\u539F\u6001\u53EF\u62C9\u5230
|
|
2937
|
+
\u4EFB\u610F\u5BBD\uFF0C\u22651100px \u65F6\u300C\u5173\u4E8E\u300D\u5347\u7B2C\u4E09\u5217\uFF08\u7EA6 230px \u5BBD\uFF0Ckv \u884C\u653E\u5F97\u4E0B\uFF09\uFF1B\u987A\u5E26\u4FEE\u6B63\u65E7\u8D26\u2014\u2014
|
|
2938
|
+
\u5C0F\u89C6\u53E3\u5168\u5C4F\uFF08<1100\uFF09\u4E0D\u518D\u88AB\u5F3A\u585E\u4E09\u5217\uFF0C\u56DE\u843D\u4E24\u5217\u3002\u2264760px \u89C6\u53E3\u5355\u5217\u5A92\u4F53\u67E5\u8BE2\u539F\u6837\u4FDD\u7559\u3002 */
|
|
2939
|
+
@container (min-width:1100px){
|
|
2940
|
+
.dshm-settings-grid{grid-template-columns:4fr 7fr 3fr}
|
|
2941
|
+
.dshm-settings-grid>.dshm-section:nth-child(3){grid-column:auto}
|
|
2942
|
+
}
|
|
2865
2943
|
@media (max-width:760px){.dshm-settings-grid,.dshm-panel.full .dshm-settings-grid{grid-template-columns:minmax(0,1fr)}.dshm-settings-grid>.dshm-section:nth-child(3),.dshm-settings-grid>.dshm-ops-card{grid-column:1/-1}}
|
|
2866
2944
|
.dshm-kvlines{display:flex;flex-direction:column}
|
|
2867
2945
|
.dshm-kvline-row{display:flex;justify-content:space-between;align-items:baseline;gap:10px;padding:5px 0;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.14));min-width:0}
|
|
@@ -2913,7 +2991,7 @@ button.dshm-badge:hover{filter:brightness(.95)}
|
|
|
2913
2991
|
.dshm-links{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:7px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);font-size:11px;color:var(--dsw-alias-label-caption,#6b7280)}
|
|
2914
2992
|
.dshm-links-k,.dshm-links-sep{color:var(--dsw-alias-label-caption,#9ca3af)}
|
|
2915
2993
|
.dshm-links a{color:var(--dsw-alias-state-business-primary,#4d6bfe);text-decoration:none;font-weight:500}
|
|
2916
|
-
.dshm-links-ver{font-weight:500;color:var(--dsw-alias-label-
|
|
2994
|
+
.dshm-links-ver{font-weight:500;color:var(--dsw-alias-label-caption,#6b7280);font-variant-numeric:tabular-nums}
|
|
2917
2995
|
.dshm-links a:hover{text-decoration:underline}
|
|
2918
2996
|
.dshm-prog{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;font-size:12px;color:var(--dsw-alias-label-secondary,#4b5563)}
|
|
2919
2997
|
.dshm-prog .bar{flex:1;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.08));overflow:hidden;min-width:80px}
|
|
@@ -2973,8 +3051,10 @@ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
|
|
|
2973
3051
|
\u89C6\u53E3\u5BBD \u2260 \u9762\u677F\u5BBD\uFF08600px \u89C6\u53E3\u2192568px \u9762\u677F\u4ECD\u6EA2\u51FA\uFF09\uFF1B\u5BB9\u5668\u67E5\u8BE2\u8DDF\u968F\u9762\u677F\u5B9E\u9645\u5BBD\u5EA6\u3002\u9762\u677F\u5BBD\u9AD8
|
|
2974
3052
|
\u5747\u4E3A min(920px,100%)/min(680px,86vh) \u516C\u5F0F\u6D3E\u751F\u3001\u4E0D\u4F9D\u8D56\u5185\u5BB9\uFF0Cinline-size \u5305\u542B\u5B89\u5168\uFF1B
|
|
2975
3053
|
\u706F\u7BB1 portal \u6302 body\uFF080.9.73\uFF09\u3001\u5206\u7C7B\u884C sticky \u5728 .dshm-body \u6EDA\u52A8\u5BB9\u5668\u5185\uFF0C\u5747\u4E0D\u53D7\u7956\u5148
|
|
2976
|
-
inline-size \u5305\u542B\u5F71\u54CD\u3002\u65AD\u70B9\uFF1A\u5BB9\u5668
|
|
2977
|
-
overlay 8px\uFF09\
|
|
3054
|
+
inline-size \u5305\u542B\u5F71\u54CD\u3002\u65AD\u70B9\uFF1A\u5BB9\u5668 720px\uFF08\u4E24\u884C\u91CD\u6392\uFF09\u4E0E\u89C6\u53E3 600px\uFF08\u9ED8\u8BA4\u5168\u5C4F\u5224\u5B9A +
|
|
3055
|
+
overlay 8px\uFF09\u2014\u2014\u4E24\u65AD\u70B9 0.9.88 \u8D77\u89E3\u8026\uFF1Apanel resize \u7684 min 640 \u843D\u5728 601\u2013720 \u5E26\u65F6\u65E7
|
|
3056
|
+
600px \u65AD\u70B9\u4E0D\u89E6\u53D1\uFF0C\u957F\u6807\u9898\u6298\u884C/seg \u6324\u538B\uFF08\u4E3B\u4EBA\u5B9E\u6D4B 2026-10-15\uFF09\uFF0C720 \u65AD\u70B9\u8BA9 min \u6052\u5165
|
|
3057
|
+
\u4E24\u884C\u533A\uFF1B\u526F\u4F5C\u7528 = 600\u2013720px \u89C6\u53E3\u7684\u624B\u673A\u5168\u5C4F\u4E5F\u4E24\u884C\uFF0C\u5C5E\u4FEE\u590D\u800C\u975E\u56DE\u5F52\u3002
|
|
2978
3058
|
\u515C\u5E95\u8BED\u4E49\u8BF4\u660E\uFF08\u5171\u8BC6 Q3 + \u8BC4\u5BA1 R7\uFF09\uFF1A\u300C\u4E0D\u652F\u6301\u5BB9\u5668\u67E5\u8BE2\u7684\u6D4F\u89C8\u5668\u65B0\u89C4\u5219\u96F6\u751F\u6548\u300D\u6309 CSS
|
|
2979
3059
|
\u6761\u4EF6\u89C4\u5219\u7EC4\u673A\u5236\u7B49\u6548\u843D\u5730\u2014\u2014.dshm-title-short \u57FA\u7840\u9690\u85CF\u4E0D\u4F9D\u8D56\u5BB9\u5668\u67E5\u8BE2\uFF08\u8001\u6D4F\u89C8\u5668\u4E5F\u8981
|
|
2980
3060
|
\u751F\u6548\uFF09\uFF1B\u5176\u4F59\u65B0\u89C4\u5219\u90FD\u662F\u6574\u5757 at-rule\uFF08@supports/@container/@media \u9876\u5C42\u5144\u5F1F\uFF09\uFF0C\u4E0D\u652F\u6301
|
|
@@ -2988,7 +3068,7 @@ button.dshm-dshchip:disabled{cursor:default;opacity:.75}
|
|
|
2988
3068
|
/* \u4E24\u884C\u91CD\u6392\uFF1A\u884C1 [\u77ED\u6807\u9898 \u22EFspacer\u22EF chip winctl]\uFF0C\u884C2 [seg \u62C9\u901A]\u3002DOM \u987A\u5E8F\u4E0D\u52A8
|
|
2989
3069
|
\uFF08title\u2192divider\u2192seg\u2192spacer\u2192chip\u2192winctl\uFF09\uFF0C\u5168\u9760 order+wrap\u2014\u2014\u6E90\u951A/tab \u5E8F/\u8BFB\u5C4F\u5E8F
|
|
2990
3070
|
\u96F6\u6270\u52A8\uFF1B\u89C6\u89C9\u5E8F\u4E0E DOM \u5E8F\u4E0D\u4E00\u81F4\uFF08seg \u5728 DOM \u524D\u3001\u89C6\u89C9\u5728\u540E\uFF09\uFF0C\u9762\u677F\u5185\u5143\u7D20\u5C11\u53EF\u63A5\u53D7 */
|
|
2991
|
-
@container (max-width:
|
|
3071
|
+
@container (max-width:720px){
|
|
2992
3072
|
.dshm-head{flex-wrap:wrap;row-gap:6px}
|
|
2993
3073
|
.dshm-head-divider{display:none}
|
|
2994
3074
|
.dshm-title-full{display:none}
|
|
@@ -3822,75 +3902,82 @@ function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeB
|
|
|
3822
3902
|
h("span", { className: "dshm-spacer" }),
|
|
3823
3903
|
h("button", { ref: closeRef, className: "dshm-xbtn", "aria-label": lookup("common.close"), title: lookup("common.close"), onClick: onClose }, h(XIcon))
|
|
3824
3904
|
),
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
it.owner ? h("span", null, `by ${it.owner}`) : null,
|
|
3829
|
-
typeof it.stars === "number" ? h("span", { title: String(it.stars) }, `${compactCount(it.stars)} \u2605`) : null,
|
|
3830
|
-
typeof it.downloads === "number" ? h("span", { title: String(it.downloads) }, `${compactCount(it.downloads)} \u2193`) : null
|
|
3831
|
-
) : null,
|
|
3832
|
-
h(LinksRow, { npm: it.npm, github: it.github, homepage: it.homepage }),
|
|
3905
|
+
// 0.9.88(T7 验收·主人反馈二):中段内容包进 modalbody 滚动层——✕(head)与
|
|
3906
|
+
// 「安装」(actions)双端常驻,README/能力披露展开多长都关得上、装得了;
|
|
3907
|
+
// box 攂 overflow:hidden,滚动收进 body(overscroll-behavior 防链式透传底页)。
|
|
3833
3908
|
h(
|
|
3834
|
-
"dl",
|
|
3835
|
-
{ className: "dsvm-kv" },
|
|
3836
|
-
kv(lookup("modal.category"), catLabel),
|
|
3837
|
-
it.added ? kv(lookup("modal.added"), fmtDate(it.added)) : null,
|
|
3838
|
-
it.community === true ? kv(
|
|
3839
|
-
lookup("modal.dlwindow"),
|
|
3840
|
-
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")
|
|
3841
|
-
) : null,
|
|
3842
|
-
kv(lookup("detail.latest"), it.latestVersion ? `v${it.latestVersion}` : it.latestTag ? it.latestTag : it.latestSha ? it.latestSha : it.latestError ? it.version ? lookup("sub.snapshot", { v: it.version }) : it.latestError : "\u2014"),
|
|
3843
|
-
kv(lookup("detail.installed"), it.installedPkg ? `${it.installedPkg} v${it.installedVersion || "?"}` : lookup("installed.none")),
|
|
3844
|
-
it.community !== true && Array.isArray(it.verified) && it.verified.length ? kv(lookup("modal.verified"), it.verified.join("\u3001")) : null,
|
|
3845
|
-
it.audience === "internal" ? kv(lookup("modal.audience.label"), lookup("modal.audience.value")) : null,
|
|
3846
|
-
it.decoupled === true ? kv(lookup("modal.decoupled.label"), lookup("modal.decoupled.value")) : null,
|
|
3847
|
-
(it.tags || []).length ? kv(lookup("modal.tags"), it.tags.join(", ")) : null,
|
|
3848
|
-
it.deprecated === true && it.replacement ? kv(lookup("modal.replacement"), it.replacement) : null
|
|
3849
|
-
),
|
|
3850
|
-
h("div", { className: "dshm-desc", style: { WebkitLineClamp: "unset" } }, descFull),
|
|
3851
|
-
it.npm ? h(
|
|
3852
|
-
"details",
|
|
3853
|
-
{ className: "dsvm-fold", onToggle: (e) => {
|
|
3854
|
-
if (e.target.open) setRmOpen(true);
|
|
3855
|
-
} },
|
|
3856
|
-
h("summary", null, lookup("readme.show")),
|
|
3857
|
-
rmOpen ? h(ReadmeBlock, { pkg: it.npm, repo: it.github }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
|
|
3858
|
-
) : null,
|
|
3859
|
-
visible.length ? h(
|
|
3860
3909
|
"div",
|
|
3861
|
-
{
|
|
3862
|
-
|
|
3863
|
-
) : null,
|
|
3864
|
-
it.community === true ? h(
|
|
3865
|
-
"details",
|
|
3866
|
-
{ className: "dsvm-fold" },
|
|
3867
|
-
h("summary", null, lookup("detail.capabilities")),
|
|
3868
|
-
Array.isArray(it.capabilities) && it.capabilities.length ? h(
|
|
3910
|
+
{ className: "dsvm-modalbody" },
|
|
3911
|
+
it.community === true ? h(
|
|
3869
3912
|
"div",
|
|
3870
|
-
|
|
3871
|
-
h("
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
"details",
|
|
3877
|
-
{ className: "dsvm-fold" },
|
|
3878
|
-
h("summary", null, lookup("modal.installcmd")),
|
|
3913
|
+
{ className: "dsvm-byline" },
|
|
3914
|
+
it.owner ? h("span", null, `by ${it.owner}`) : null,
|
|
3915
|
+
typeof it.stars === "number" ? h("span", { title: String(it.stars) }, `${compactCount(it.stars)} \u2605`) : null,
|
|
3916
|
+
typeof it.downloads === "number" ? h("span", { title: String(it.downloads) }, `${compactCount(it.downloads)} \u2193`) : null
|
|
3917
|
+
) : null,
|
|
3918
|
+
h(LinksRow, { npm: it.npm, github: it.github, homepage: it.homepage }),
|
|
3879
3919
|
h(
|
|
3920
|
+
"dl",
|
|
3921
|
+
{ className: "dsvm-kv" },
|
|
3922
|
+
kv(lookup("modal.category"), catLabel),
|
|
3923
|
+
it.added ? kv(lookup("modal.added"), fmtDate(it.added)) : null,
|
|
3924
|
+
it.community === true ? kv(
|
|
3925
|
+
lookup("modal.dlwindow"),
|
|
3926
|
+
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")
|
|
3927
|
+
) : null,
|
|
3928
|
+
kv(lookup("detail.latest"), it.latestVersion ? `v${it.latestVersion}` : it.latestTag ? it.latestTag : it.latestSha ? it.latestSha : it.latestError ? it.version ? lookup("sub.snapshot", { v: it.version }) : it.latestError : "\u2014"),
|
|
3929
|
+
kv(lookup("detail.installed"), it.installedPkg ? `${it.installedPkg} v${it.installedVersion || "?"}` : lookup("installed.none")),
|
|
3930
|
+
it.community !== true && Array.isArray(it.verified) && it.verified.length ? kv(lookup("modal.verified"), it.verified.join("\u3001")) : null,
|
|
3931
|
+
it.audience === "internal" ? kv(lookup("modal.audience.label"), lookup("modal.audience.value")) : null,
|
|
3932
|
+
it.decoupled === true ? kv(lookup("modal.decoupled.label"), lookup("modal.decoupled.value")) : null,
|
|
3933
|
+
(it.tags || []).length ? kv(lookup("modal.tags"), it.tags.join(", ")) : null,
|
|
3934
|
+
it.deprecated === true && it.replacement ? kv(lookup("modal.replacement"), it.replacement) : null
|
|
3935
|
+
),
|
|
3936
|
+
h("div", { className: "dshm-desc", style: { WebkitLineClamp: "unset" } }, descFull),
|
|
3937
|
+
it.npm ? h(
|
|
3938
|
+
"details",
|
|
3939
|
+
{ className: "dsvm-fold", onToggle: (e) => {
|
|
3940
|
+
if (e.target.open) setRmOpen(true);
|
|
3941
|
+
} },
|
|
3942
|
+
h("summary", null, lookup("readme.show")),
|
|
3943
|
+
rmOpen ? h(ReadmeBlock, { pkg: it.npm, repo: it.github }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
|
|
3944
|
+
) : null,
|
|
3945
|
+
visible.length ? h(
|
|
3880
3946
|
"div",
|
|
3881
|
-
{ className: "dsvm-
|
|
3882
|
-
h(
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3947
|
+
{ ref: shotRowRef, className: "dsvm-shotrow" },
|
|
3948
|
+
...shots.map((src, i) => h(Shot, { key: `${i}:${src}`, src, onClick: () => setLb(visible.indexOf(src)), onBroken: breakShot }))
|
|
3949
|
+
) : null,
|
|
3950
|
+
it.community === true ? h(
|
|
3951
|
+
"details",
|
|
3952
|
+
{ className: "dsvm-fold" },
|
|
3953
|
+
h("summary", null, lookup("detail.capabilities")),
|
|
3954
|
+
Array.isArray(it.capabilities) && it.capabilities.length ? h(
|
|
3955
|
+
"div",
|
|
3956
|
+
null,
|
|
3957
|
+
h("div", null, it.capabilities.join(", ")),
|
|
3958
|
+
Array.isArray(it.capabilityRedLines) && it.capabilityRedLines.length ? h("div", { className: "dshm-err" }, `${lookup("detail.redlines")}: ${it.capabilityRedLines.join("; ")}`) : null
|
|
3959
|
+
) : h("div", { className: "dshm-hint" }, lookup("detail.capabilities.unscanned"))
|
|
3960
|
+
) : null,
|
|
3961
|
+
showInstallCmd ? h(
|
|
3962
|
+
"details",
|
|
3963
|
+
{ className: "dsvm-fold" },
|
|
3964
|
+
h("summary", null, lookup("modal.installcmd")),
|
|
3965
|
+
h(
|
|
3966
|
+
"div",
|
|
3967
|
+
{ className: "dsvm-cmdrow" },
|
|
3968
|
+
h("code", { className: "dsvm-code" }, installCmd),
|
|
3969
|
+
h("button", { className: "dshm-btn sm", onClick: copyCmd }, copied ? lookup("modal.copied") : lookup("modal.copy"))
|
|
3970
|
+
)
|
|
3971
|
+
) : null,
|
|
3972
|
+
// 0.9.15:安装信息就地进 Modal(主人反馈 2026-10-02:进度/终态原先只隔遮罩在底层透出)。
|
|
3973
|
+
// 进度行复用 ProgressLine(自轮询 host status);底层同源行由 MarketTab 在本 Modal 打开时让位。
|
|
3974
|
+
// installRec 派生自全局操作记录(DESIGN §2.6「状态不挂卡片」的所有权模型不变,此处只是展示):
|
|
3975
|
+
// 仅 queued/running 挂行,input(兼容待决)走上层 CompatDialog;installNote 为本次终态摘要。
|
|
3976
|
+
installRec && (installRec.status === "running" || installRec.status === "queued") ? h(ProgressLine, { key: "opprog" }) : null,
|
|
3977
|
+
upgradeRec && (upgradeRec.status === "running" || upgradeRec.status === "queued") ? h(ProgressLine, { key: "upprog" }) : null,
|
|
3978
|
+
installNote ? h("div", { key: "opnote", className: installNote.kind === "err" ? "dshm-err" : installNote.kind === "hint" ? "dshm-hint" : "dshm-ok" }, installNote.text) : null,
|
|
3979
|
+
installNote && installNote.kind === "ok" ? h("div", { key: "opnote-rh", className: "dshm-hint" }, lookup(profileKind === "desktop" ? "profile.restartHint" : "banner.done")) : null
|
|
3980
|
+
),
|
|
3894
3981
|
h(
|
|
3895
3982
|
"div",
|
|
3896
3983
|
{ className: "dsvm-modalactions" },
|
|
@@ -4901,15 +4988,14 @@ function FavoriteZone({ favorites, onOpen }) {
|
|
|
4901
4988
|
if (!list.length) return;
|
|
4902
4989
|
const timer = setTimeout(() => {
|
|
4903
4990
|
void (async () => {
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
};
|
|
4991
|
+
let present = /* @__PURE__ */ new Set();
|
|
4992
|
+
try {
|
|
4993
|
+
const res = await api("market-ids-exist", { ids: list.map((f) => f.id) });
|
|
4994
|
+
if (res && Array.isArray(res.present)) present = new Set(res.present);
|
|
4995
|
+
} catch {
|
|
4996
|
+
present = /* @__PURE__ */ new Set();
|
|
4997
|
+
}
|
|
4998
|
+
const lookupLive = async (fav) => present.has(fav.id);
|
|
4913
4999
|
const { stale } = await partitionStale2(list, lookupLive);
|
|
4914
5000
|
if (live) setCheck({ staleIds: stale.map((f) => f.id) });
|
|
4915
5001
|
})();
|
|
@@ -5226,20 +5312,35 @@ function MarketPanel({ onClose }) {
|
|
|
5226
5312
|
return null;
|
|
5227
5313
|
}
|
|
5228
5314
|
});
|
|
5315
|
+
const [size, setSize] = useState(() => {
|
|
5316
|
+
try {
|
|
5317
|
+
return loadPanelSize2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
|
|
5318
|
+
} catch {
|
|
5319
|
+
return null;
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5229
5322
|
const fullRef = useRef(full);
|
|
5230
5323
|
fullRef.current = full;
|
|
5231
5324
|
const posRef = useRef(pos);
|
|
5232
5325
|
posRef.current = pos;
|
|
5326
|
+
const sizeRef = useRef(size);
|
|
5327
|
+
sizeRef.current = size;
|
|
5328
|
+
const resizingRef = useRef(false);
|
|
5233
5329
|
const reclamp = useCallback(() => {
|
|
5234
|
-
if (fullRef.current || draggingRef.current || !panelRef.current || typeof window === "undefined") return;
|
|
5330
|
+
if (fullRef.current || draggingRef.current || resizingRef.current || !panelRef.current || typeof window === "undefined") return;
|
|
5331
|
+
const vp = { width: window.innerWidth, height: window.innerHeight };
|
|
5332
|
+
const insets = readTitlebarInsets();
|
|
5333
|
+
const prevSize = sizeRef.current;
|
|
5334
|
+
const eff = prevSize ? clampSize2(prevSize, vp, insets) : null;
|
|
5335
|
+
if (eff && prevSize && (eff.w !== prevSize.w || eff.h !== prevSize.h)) setSize(eff);
|
|
5235
5336
|
const prev = posRef.current;
|
|
5236
5337
|
if (!prev) return;
|
|
5237
5338
|
const rect = panelRef.current.getBoundingClientRect();
|
|
5238
5339
|
const next = clampPoint2(
|
|
5239
5340
|
{ x: prev.x, y: prev.y },
|
|
5240
|
-
|
|
5241
|
-
{ width: rect.width, height: rect.height },
|
|
5242
|
-
|
|
5341
|
+
vp,
|
|
5342
|
+
eff ? { width: eff.w, height: eff.h } : { width: rect.width, height: rect.height },
|
|
5343
|
+
insets
|
|
5243
5344
|
);
|
|
5244
5345
|
if (next.x !== prev.x || next.y !== prev.y) setPos(next);
|
|
5245
5346
|
}, []);
|
|
@@ -5260,7 +5361,7 @@ function MarketPanel({ onClose }) {
|
|
|
5260
5361
|
let originY = 0;
|
|
5261
5362
|
let grab = { dx: 0, dy: 0 };
|
|
5262
5363
|
let rest = { x: 0, y: 0 };
|
|
5263
|
-
let
|
|
5364
|
+
let size2 = { width: 0, height: 0 };
|
|
5264
5365
|
let lastPoint = null;
|
|
5265
5366
|
const bindGesture = () => {
|
|
5266
5367
|
document.addEventListener("pointermove", onPointerMove);
|
|
@@ -5277,7 +5378,7 @@ function MarketPanel({ onClose }) {
|
|
|
5277
5378
|
const topLeft = clampPoint2(
|
|
5278
5379
|
{ x: lastPoint.x - grab.dx, y: lastPoint.y - grab.dy },
|
|
5279
5380
|
{ width: window.innerWidth, height: window.innerHeight },
|
|
5280
|
-
|
|
5381
|
+
size2,
|
|
5281
5382
|
readTitlebarInsets()
|
|
5282
5383
|
);
|
|
5283
5384
|
panelRef.current.style.transform = `translate3d(${Math.round(topLeft.x - rest.x)}px, ${Math.round(topLeft.y - rest.y)}px, 0)`;
|
|
@@ -5300,7 +5401,7 @@ function MarketPanel({ onClose }) {
|
|
|
5300
5401
|
const final = clampPoint2(
|
|
5301
5402
|
{ x: event.clientX - grab.dx, y: event.clientY - grab.dy },
|
|
5302
5403
|
{ width: window.innerWidth, height: window.innerHeight },
|
|
5303
|
-
|
|
5404
|
+
size2,
|
|
5304
5405
|
readTitlebarInsets()
|
|
5305
5406
|
);
|
|
5306
5407
|
panel.style.left = `${final.x}px`;
|
|
@@ -5350,7 +5451,7 @@ function MarketPanel({ onClose }) {
|
|
|
5350
5451
|
const rect = panel.getBoundingClientRect();
|
|
5351
5452
|
grab = grabOffset2({ x: event.clientX, y: event.clientY }, rect);
|
|
5352
5453
|
rest = { x: rect.left, y: rect.top };
|
|
5353
|
-
|
|
5454
|
+
size2 = { width: rect.width, height: rect.height };
|
|
5354
5455
|
if (headEl.setPointerCapture) {
|
|
5355
5456
|
try {
|
|
5356
5457
|
headEl.setPointerCapture(event.pointerId);
|
|
@@ -5366,6 +5467,122 @@ function MarketPanel({ onClose }) {
|
|
|
5366
5467
|
document.body.style.userSelect = "";
|
|
5367
5468
|
};
|
|
5368
5469
|
}, []);
|
|
5470
|
+
useEffect(() => {
|
|
5471
|
+
const panelEl = panelRef.current;
|
|
5472
|
+
if (!panelEl) return void 0;
|
|
5473
|
+
let zone = null;
|
|
5474
|
+
let resizing = false;
|
|
5475
|
+
let moved = false;
|
|
5476
|
+
let slop = 6;
|
|
5477
|
+
let originX = 0, originY = 0;
|
|
5478
|
+
let startW = 0, startH = 0;
|
|
5479
|
+
let originRect = null;
|
|
5480
|
+
let lastPoint = null;
|
|
5481
|
+
let raf = 0;
|
|
5482
|
+
const ZONE_CURSOR = { e: "ew-resize", s: "ns-resize", se: "nwse-resize" };
|
|
5483
|
+
const bindGesture = () => {
|
|
5484
|
+
document.addEventListener("pointermove", onPointerMove);
|
|
5485
|
+
document.addEventListener("pointerup", onPointerUp);
|
|
5486
|
+
document.addEventListener("pointercancel", onPointerCancel);
|
|
5487
|
+
};
|
|
5488
|
+
const unbindGesture = () => {
|
|
5489
|
+
document.removeEventListener("pointermove", onPointerMove);
|
|
5490
|
+
document.removeEventListener("pointerup", onPointerUp);
|
|
5491
|
+
document.removeEventListener("pointercancel", onPointerCancel);
|
|
5492
|
+
};
|
|
5493
|
+
const frameStep = () => {
|
|
5494
|
+
raf = 0;
|
|
5495
|
+
if (!resizing || !moved || !lastPoint || !panelRef.current) return;
|
|
5496
|
+
const raw = {
|
|
5497
|
+
w: zone === "s" ? startW : startW + (lastPoint.x - originX),
|
|
5498
|
+
h: zone === "e" ? startH : startH + (lastPoint.y - originY)
|
|
5499
|
+
};
|
|
5500
|
+
const next = clampSize2(raw, { width: window.innerWidth, height: window.innerHeight }, readTitlebarInsets(), originRect);
|
|
5501
|
+
panelRef.current.style.width = `${Math.round(next.w)}px`;
|
|
5502
|
+
panelRef.current.style.height = `${Math.round(next.h)}px`;
|
|
5503
|
+
};
|
|
5504
|
+
const onPointerMove = (event) => {
|
|
5505
|
+
if (!resizing) return;
|
|
5506
|
+
if (!moved) {
|
|
5507
|
+
if (Math.hypot(event.clientX - originX, event.clientY - originY) < slop) return;
|
|
5508
|
+
moved = true;
|
|
5509
|
+
document.body.style.userSelect = "none";
|
|
5510
|
+
document.body.style.cursor = ZONE_CURSOR[zone];
|
|
5511
|
+
}
|
|
5512
|
+
lastPoint = { x: event.clientX, y: event.clientY };
|
|
5513
|
+
if (!raf) raf = requestAnimationFrame(frameStep);
|
|
5514
|
+
};
|
|
5515
|
+
const onPointerUp = () => {
|
|
5516
|
+
if (!resizing) return;
|
|
5517
|
+
const baked = panelRef.current ? clampSize2(
|
|
5518
|
+
moved && lastPoint ? { w: zone === "s" ? startW : startW + (lastPoint.x - originX), h: zone === "e" ? startH : startH + (lastPoint.y - originY) } : { w: startW, h: startH },
|
|
5519
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
5520
|
+
readTitlebarInsets(),
|
|
5521
|
+
originRect
|
|
5522
|
+
) : null;
|
|
5523
|
+
unbindGesture();
|
|
5524
|
+
if (raf) cancelAnimationFrame(raf);
|
|
5525
|
+
raf = 0;
|
|
5526
|
+
resizing = false;
|
|
5527
|
+
resizingRef.current = false;
|
|
5528
|
+
document.body.style.userSelect = "";
|
|
5529
|
+
document.body.style.cursor = "";
|
|
5530
|
+
if (!moved) return;
|
|
5531
|
+
try {
|
|
5532
|
+
savePanelSize2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null, baked);
|
|
5533
|
+
} catch {
|
|
5534
|
+
}
|
|
5535
|
+
setSize(baked);
|
|
5536
|
+
};
|
|
5537
|
+
const onPointerCancel = () => {
|
|
5538
|
+
if (!resizing) return;
|
|
5539
|
+
unbindGesture();
|
|
5540
|
+
if (raf) cancelAnimationFrame(raf);
|
|
5541
|
+
raf = 0;
|
|
5542
|
+
resizing = false;
|
|
5543
|
+
resizingRef.current = false;
|
|
5544
|
+
document.body.style.userSelect = "";
|
|
5545
|
+
document.body.style.cursor = "";
|
|
5546
|
+
if (panelRef.current) {
|
|
5547
|
+
panelRef.current.style.width = sizeRef.current ? `${sizeRef.current.w}px` : "";
|
|
5548
|
+
panelRef.current.style.height = sizeRef.current ? `${sizeRef.current.h}px` : "";
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
const onPointerDown = (event) => {
|
|
5552
|
+
if (resizing) return;
|
|
5553
|
+
if (event.button !== 0) return;
|
|
5554
|
+
if (event.pointerType !== "mouse") return;
|
|
5555
|
+
const handle = event.target?.closest?.("[data-dshm-resize]");
|
|
5556
|
+
if (!handle) return;
|
|
5557
|
+
zone = handle.getAttribute("data-dshm-resize");
|
|
5558
|
+
const rect = panelRef.current.getBoundingClientRect();
|
|
5559
|
+
startW = rect.width;
|
|
5560
|
+
startH = rect.height;
|
|
5561
|
+
originRect = posRef.current ? { left: rect.left, top: rect.top } : null;
|
|
5562
|
+
originX = event.clientX;
|
|
5563
|
+
originY = event.clientY;
|
|
5564
|
+
slop = dragSlop2(event.pointerType);
|
|
5565
|
+
resizing = true;
|
|
5566
|
+
resizingRef.current = true;
|
|
5567
|
+
moved = false;
|
|
5568
|
+
lastPoint = null;
|
|
5569
|
+
if (handle.setPointerCapture) {
|
|
5570
|
+
try {
|
|
5571
|
+
handle.setPointerCapture(event.pointerId);
|
|
5572
|
+
} catch {
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
bindGesture();
|
|
5576
|
+
};
|
|
5577
|
+
panelEl.addEventListener("pointerdown", onPointerDown);
|
|
5578
|
+
return () => {
|
|
5579
|
+
panelEl.removeEventListener("pointerdown", onPointerDown);
|
|
5580
|
+
unbindGesture();
|
|
5581
|
+
if (raf) cancelAnimationFrame(raf);
|
|
5582
|
+
document.body.style.userSelect = "";
|
|
5583
|
+
document.body.style.cursor = "";
|
|
5584
|
+
};
|
|
5585
|
+
}, []);
|
|
5369
5586
|
const marketCommunity = useMarketData("community");
|
|
5370
5587
|
const marketPrimary = useMarketData("primary");
|
|
5371
5588
|
const markets = { community: marketCommunity, primary: marketPrimary };
|
|
@@ -5544,7 +5761,12 @@ function MarketPanel({ onClose }) {
|
|
|
5544
5761
|
"div",
|
|
5545
5762
|
{
|
|
5546
5763
|
className: ["dshm-panel", full && "full", !full && pos && "abs"].filter(Boolean).join(" "),
|
|
5547
|
-
|
|
5764
|
+
// panel resize:尺寸记忆以 inline 宽高覆盖类默认 min(920px,100%)·min(680px,86vh);
|
|
5765
|
+
// 与位置记忆互不诱发(就地生长:居中形态只带 width/height 仍由 flex 居中)
|
|
5766
|
+
style: !full ? {
|
|
5767
|
+
...pos ? { left: `${pos.x}px`, top: `${pos.y}px` } : {},
|
|
5768
|
+
...size ? { width: `${size.w}px`, height: `${size.h}px` } : {}
|
|
5769
|
+
} : void 0,
|
|
5548
5770
|
ref: panelRef,
|
|
5549
5771
|
onClick: (e) => e.stopPropagation()
|
|
5550
5772
|
},
|
|
@@ -5662,7 +5884,13 @@ function MarketPanel({ onClose }) {
|
|
|
5662
5884
|
h("span", { className: `dshm-banner-text${toastScrollCls}`, ref: toastTextRef }, toast.text)
|
|
5663
5885
|
) : null,
|
|
5664
5886
|
banner ? h(RestartBanner, { note: banner.text, onDone: () => setBanner(null), desktop: banner.desktop === true, onRestarted: reloadPing }) : null
|
|
5665
|
-
) : null
|
|
5887
|
+
) : null,
|
|
5888
|
+
// resize 热区(panel resize):透明无视觉;pointer-only 供性(aria-hidden)。
|
|
5889
|
+
// 全屏态不渲染(Task 5 CSS @media pointer:coarse 与 .full belt 另有双保险);
|
|
5890
|
+
// data-dshm-resize 供 T3 手势机 closest 门卫。
|
|
5891
|
+
!full && h("div", { className: "dshm-rz rz-e", "data-dshm-resize": "e", "aria-hidden": "true" }),
|
|
5892
|
+
!full && h("div", { className: "dshm-rz rz-s", "data-dshm-resize": "s", "aria-hidden": "true" }),
|
|
5893
|
+
!full && h("div", { className: "dshm-rz rz-se", "data-dshm-resize": "se", "aria-hidden": "true" })
|
|
5666
5894
|
)
|
|
5667
5895
|
);
|
|
5668
5896
|
}
|
package/lib/core/community.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, readFile, rm, rename, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { isAbsolute, join, relative } from 'node:path';
|
|
11
|
-
import { cacheRoot } from './env.js';
|
|
11
|
+
import { cacheRoot, WEB_PROFILE } from './env.js';
|
|
12
12
|
import { decodeUtf8Fatal, fetchBytesLimited, describeFetchFailure } from './httpx.js';
|
|
13
13
|
import { isExactVersion, npmLatest } from './versions.js';
|
|
14
|
-
import {
|
|
14
|
+
import { mergedOutcome } 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;
|
|
@@ -448,16 +448,29 @@ export async function fetchCommunityCatalog(cfg = {}, opts = {}) {
|
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
/**
|
|
451
|
-
* 单一深接口(计划 Task 6):fetch →
|
|
452
|
-
* -
|
|
451
|
+
* 单一深接口(计划 Task 6):fetch → mergedOutcome(L2 代存储)→ CommunityRegistrySummary。
|
|
452
|
+
* - 0.9.87 FP3:改道 mergedOutcome——同身份(registry 代 + 目录 version)命中即免全量
|
|
453
|
+
* adapt+merge(每次设置页打开 ~10-50ms CPU),summary 仍由当次 state 现算(「summary
|
|
454
|
+
* 永不入代」不变量保持);失败/deadline/unavailable/disabled 形态透传不落代;
|
|
455
|
+
* - registry 直接取本次 registry snapshot 的 LoadedRegistry(displaced 与本次响应同代,
|
|
453
456
|
* 不重复加载主清单;内部禁止调用 loadRegistry——单一来源约束);
|
|
457
|
+
* - namespace/profile 显式透传:缺省 'host'/WEB_PROFILE,与改道前 fetchCommunityCatalog/
|
|
458
|
+
* awesomeDir 的隐式默认逐字节同路(缓存目录恒 web 段——**自觉取舍**:保 parity 优先于
|
|
459
|
+
* 与 market 臂共享 L2 代,desktop host 下各建各代是接受的成本,勿当「漏传」修复);
|
|
454
460
|
* - 3s 是本 waiter 的等待上限(waiter-scoped race):到点未 materialize → unavailable +
|
|
455
461
|
* 「社区目录状态获取超时」errors,调用方主响应照常;共享 flight 按 30s hard cap 继续;
|
|
456
462
|
* - 数据源 cache-first:fetchCommunityCatalog 的版本缓存 + TTL 保证 registry 刷新无重复网络压力。
|
|
457
463
|
*/
|
|
458
|
-
export async function getCommunitySummary(
|
|
459
|
-
const task = fetchCommunityCatalog(cfg, { signal: opts.signal, force: opts.force });
|
|
460
|
-
const outcome = await
|
|
464
|
+
export async function getCommunitySummary(registry, cfg = {}, opts) {
|
|
465
|
+
const task = fetchCommunityCatalog(cfg, { namespace: opts.namespace, signal: opts.signal, force: opts.force, profile: opts.profile });
|
|
466
|
+
const outcome = await mergedOutcome({
|
|
467
|
+
namespace: opts.namespace ?? 'host',
|
|
468
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
469
|
+
cfg,
|
|
470
|
+
registry,
|
|
471
|
+
communityTask: task,
|
|
472
|
+
deadlineAt: opts.deadlineAt,
|
|
473
|
+
});
|
|
461
474
|
return outcome.summary;
|
|
462
475
|
}
|
|
463
476
|
// 0.9.14 SWR:过期触发即登记的后台自愈 flight(waiter 隔离;flight 本体走 fetchCommunityCatalog
|
package/lib/core/host-api.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { BOOT_ID, publicInstallStatus } from './dsh-cli.js';
|
|
11
11
|
import { resolveDshVersion, readLauncherPackageVersion } from './dsh-version.js';
|
|
12
12
|
import { runDoctor } from './doctor.js';
|
|
13
|
-
import { listInstalledWithMeta, listMarket, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
13
|
+
import { listInstalledWithMeta, listMarket, existingMarketIds, installFromRegistry, selfUpgrade, uninstallPlugin, upgradePlugin, InstallGuardError, } from './market.js';
|
|
14
14
|
import { runProfileTransaction, TransactionError } from './profile-transaction.js';
|
|
15
15
|
import { listExternalOperations, clearExternalOperations, removeExternalOperation } from './external-journal.js';
|
|
16
16
|
import { readInstalledPluginReadme } from './installed.js';
|
|
@@ -212,6 +212,7 @@ export function createApiDispatcher(ctx) {
|
|
|
212
212
|
const d = {
|
|
213
213
|
listMarket: ctx.deps?.listMarket ?? listMarket,
|
|
214
214
|
listInstalledWithMeta: ctx.deps?.listInstalledWithMeta ?? listInstalledWithMeta,
|
|
215
|
+
existingMarketIds: ctx.deps?.existingMarketIds ?? existingMarketIds,
|
|
215
216
|
installFromRegistry: ctx.deps?.installFromRegistry ?? installFromRegistry,
|
|
216
217
|
uninstallPlugin: ctx.deps?.uninstallPlugin ?? uninstallPlugin,
|
|
217
218
|
upgradePlugin: ctx.deps?.upgradePlugin ?? upgradePlugin,
|
|
@@ -323,10 +324,14 @@ export function createApiDispatcher(ctx) {
|
|
|
323
324
|
// 0.9.11:不再透传 force——「强制刷新」语义是精选清单(registry 链),社区目录走自己的
|
|
324
325
|
// TTL/共享 flight;此前 force 连坐会在弱网下把社区卡打成 3s 超时的「不可用」占位
|
|
325
326
|
// (Windows 实机 2026-10-01:点强制刷新 → 社区卡「获取超时」,flight 后台自愈前一直报错)。
|
|
326
|
-
|
|
327
|
+
// 0.9.87 FP3:getCommunitySummary 改道 mergedOutcome(L2)——registry 传 snap.loaded
|
|
328
|
+
// 整体(身份键需要 configuredAddress/fetchedAt);namespace 'host' + profile 缺省
|
|
329
|
+
// (WEB_PROFILE)与改道前缓存路径逐字节同路。
|
|
330
|
+
const community = await d.getCommunitySummary(snap.loaded, cfg(), {
|
|
327
331
|
force: false,
|
|
328
332
|
signal,
|
|
329
333
|
deadlineAt: Date.now() + 3_000,
|
|
334
|
+
namespace: 'host',
|
|
330
335
|
});
|
|
331
336
|
payload = { plugins: snap.loaded.registry.plugins, registryState: snap.loaded, community };
|
|
332
337
|
break;
|
|
@@ -421,6 +426,22 @@ export function createApiDispatcher(ctx) {
|
|
|
421
426
|
payload = { ...result };
|
|
422
427
|
break;
|
|
423
428
|
}
|
|
429
|
+
case 'market-ids-exist': {
|
|
430
|
+
// 0.9.87 FP2:收藏 stale 检测批量化——一次批量 id 存在性查询取代逐收藏完整 market 扇出。
|
|
431
|
+
// 校验:非空字符串数组 ≤500(trim 后透传);违例 400(与 source/sort/probeMode 同款不静默吞)。
|
|
432
|
+
const rawIds = body.ids;
|
|
433
|
+
if (!Array.isArray(rawIds) || rawIds.length > 500 || rawIds.some((v) => typeof v !== 'string' || v.trim() === '')) {
|
|
434
|
+
throw new ApiProtocolError(400, '非法 ids: 需非空字符串数组(≤500)');
|
|
435
|
+
}
|
|
436
|
+
const ids = rawIds.map((v) => v.trim());
|
|
437
|
+
const result = await d.existingMarketIds(ids, cfg(), {
|
|
438
|
+
namespace: 'host',
|
|
439
|
+
profile: profile.name,
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
payload = { present: result.present };
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
424
445
|
case 'installed': {
|
|
425
446
|
await ctx.controller.ensureReady();
|
|
426
447
|
// 0.9.5 双 profile 读路径接线(实机回归 2026-10-01):desktop 下已装列表此前
|
package/lib/core/installed.js
CHANGED
|
@@ -146,41 +146,42 @@ function sanitizePkgJson(raw, fallbackName) {
|
|
|
146
146
|
githubRepo: githubRepoFromRepository(raw.repository),
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
async function classifyDep(root, pkg, spec, wantPatch) {
|
|
150
|
+
const dir = resolvePluginDir(root, pkg, spec);
|
|
151
|
+
if (!dir)
|
|
152
|
+
return 'unknown';
|
|
153
|
+
const rawResult = await readPkgJsonResult(dir);
|
|
154
|
+
if (!rawResult.ok)
|
|
155
|
+
return 'unknown';
|
|
156
|
+
const raw = rawResult.value;
|
|
157
|
+
if (!('dsh' in raw))
|
|
158
|
+
return 'other';
|
|
159
|
+
const info = sanitizePkgJson(raw, pkg);
|
|
160
|
+
// 包内补丁层行枚举(开关粒度判定原料):缺失 → readable:false(按多行 → Bundle 级处理)。
|
|
161
|
+
// wantPatch=false(市场查询热路径)省略 patchRows 字段——不伪造 readable:false(那是
|
|
162
|
+
// 「多行插件」的语义谎报);消费方(enablement/toggle,listInstalledWithMeta 路径)全程 ?. 链。
|
|
163
|
+
if (!wantPatch) {
|
|
164
|
+
return {
|
|
165
|
+
item: {
|
|
166
|
+
pkg,
|
|
167
|
+
name: info.name,
|
|
168
|
+
version: info.version,
|
|
169
|
+
description: info.description,
|
|
170
|
+
homepage: info.homepage,
|
|
171
|
+
spec,
|
|
172
|
+
source: parseSpecSource(spec),
|
|
173
|
+
dsh: true,
|
|
174
|
+
path: dir,
|
|
175
|
+
githubRepo: info.githubRepo,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const patchText = await readTextLimited(join(dir, 'cordis.patch.yml'), PATCH_MAX_BYTES);
|
|
180
|
+
const patchRows = patchText
|
|
181
|
+
? readBundlePatchRows(patchText.text)
|
|
182
|
+
: { inserts: [], configRows: 0, readable: false };
|
|
183
|
+
return {
|
|
184
|
+
item: {
|
|
184
185
|
pkg,
|
|
185
186
|
name: info.name,
|
|
186
187
|
version: info.version,
|
|
@@ -190,9 +191,39 @@ export async function listInstalledPlugins(profileDir = webProfileDir()) {
|
|
|
190
191
|
source: parseSpecSource(spec),
|
|
191
192
|
dsh: true,
|
|
192
193
|
path: dir,
|
|
193
|
-
githubRepo:
|
|
194
|
+
githubRepo: info.githubRepo,
|
|
194
195
|
patchRows,
|
|
195
|
-
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 枚举 web profile 已安装插件(只读)。complete:false 时 items/others 为 partial 结果(已确认部分保留)。
|
|
201
|
+
* opts.patchRows=false:跳过每插件的 cordis.patch.yml 读取与 YAML 解析(热路径省 ~0.55ms/插件),
|
|
202
|
+
* 产出的 item 不带 patchRows 字段——仅供不需要开关粒度原料的调用方(市场查询)使用。
|
|
203
|
+
*/
|
|
204
|
+
export async function listInstalledPlugins(profileDir = webProfileDir(), opts) {
|
|
205
|
+
const root = resolve(profileDir);
|
|
206
|
+
const result = await readProfileDepsResult(root);
|
|
207
|
+
let complete = result.complete;
|
|
208
|
+
const deps = result.deps;
|
|
209
|
+
const wantPatch = opts?.patchRows !== false;
|
|
210
|
+
// 每依赖文件读并发执行(原串行 for-await 是热路径上每次市场查询都重复支付的一半预算,
|
|
211
|
+
// 14 依赖实测 IO 串行 ~11ms → 并行 ~2.5ms);Promise.all 保序,输出仍按依赖键字典序。
|
|
212
|
+
const keys = Object.keys(deps).sort();
|
|
213
|
+
const classified = await Promise.all(keys.map((pkg) => classifyDep(root, pkg, deps[pkg], wantPatch)));
|
|
214
|
+
const items = [];
|
|
215
|
+
let others = 0;
|
|
216
|
+
for (const c of classified) {
|
|
217
|
+
if (c === 'unknown') {
|
|
218
|
+
// 依赖键不安全、目录无法解析、package.json 缺失/不可读/坏 JSON → 无法判断(不冒充非 DSH)
|
|
219
|
+
complete = false;
|
|
220
|
+
}
|
|
221
|
+
else if (c === 'other') {
|
|
222
|
+
others += 1;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
items.push(c.item);
|
|
226
|
+
}
|
|
196
227
|
}
|
|
197
228
|
return { items, others, complete, profileDir: root };
|
|
198
229
|
}
|
package/lib/core/market.js
CHANGED
|
@@ -22,7 +22,7 @@ import { fetchCommunityCatalog as defaultFetchCommunityCatalog, } from './commun
|
|
|
22
22
|
// 0.9.68(ADR-0016):合并层迁入 merged-market.ts(communityOutcome/mergeRegistries/communitySummary/
|
|
23
23
|
// deadlineRace/matchInstalledByEntry/isCommunityEntry/COMMUNITY_CATEGORY_LABELS)——market↔community
|
|
24
24
|
// import 环自此断开;re-export 保既有消费方(tools.ts 的类型、profile-ops 的 matchInstalledByEntry)零改动。
|
|
25
|
-
import { communityOutcome, communityTimeoutSummary, withFallbackLookups, deadlineRace, isCommunityEntry, matchInstalledByEntry, mergedOutcome, } from './merged-market.js';
|
|
25
|
+
import { communityOutcome, communityTimeoutSummary, withFallbackLookups, deadlineRace, isCommunityEntry, matchInstalledByEntry, mergedOutcome, GITHUB_SPEC_RE, } from './merged-market.js';
|
|
26
26
|
export { communityOutcome, matchInstalledByEntry } from './merged-market.js';
|
|
27
27
|
import { normalizeSearchText, relevanceScore, tokenizeSearchText } from './search-relevance.js';
|
|
28
28
|
import { GithubBudgetExhaustedError, createGithubRequestBudget, isExactVersion } from './versions.js';
|
|
@@ -229,9 +229,52 @@ function sortEntries(entries, sort) {
|
|
|
229
229
|
return (aa < ab ? -1 : aa > ab ? 1 : 0) * dirMul || nameCmp(a, b);
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
|
+
function buildInstalledIndex(installedItems) {
|
|
233
|
+
const byPkg = new Map();
|
|
234
|
+
const byName = new Map();
|
|
235
|
+
const byGithub = new Map();
|
|
236
|
+
installedItems.forEach((inst, ord) => {
|
|
237
|
+
const slot = { inst, ord };
|
|
238
|
+
if (!byPkg.has(inst.pkg))
|
|
239
|
+
byPkg.set(inst.pkg, slot);
|
|
240
|
+
if (!byName.has(inst.name))
|
|
241
|
+
byName.set(inst.name, slot);
|
|
242
|
+
if (inst.source === 'github') {
|
|
243
|
+
const m = GITHUB_SPEC_RE.exec(inst.spec);
|
|
244
|
+
if (m && !byGithub.has(m[1]))
|
|
245
|
+
byGithub.set(m[1], slot);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return { byPkg, byName, byGithub };
|
|
249
|
+
}
|
|
250
|
+
/** 三候选(byPkg[npm] / byName[npm] / byGithub[github],truthiness 守卫与旧谓词一致)取 ord 最小者胜。 */
|
|
251
|
+
function lookupInstalledByIndex(index, entry) {
|
|
252
|
+
const candidates = [];
|
|
253
|
+
if (entry.npm) {
|
|
254
|
+
const byPkg = index.byPkg.get(entry.npm);
|
|
255
|
+
if (byPkg)
|
|
256
|
+
candidates.push(byPkg);
|
|
257
|
+
const byName = index.byName.get(entry.npm);
|
|
258
|
+
if (byName)
|
|
259
|
+
candidates.push(byName);
|
|
260
|
+
}
|
|
261
|
+
if (entry.github) {
|
|
262
|
+
const byRepo = index.byGithub.get(entry.github);
|
|
263
|
+
if (byRepo)
|
|
264
|
+
candidates.push(byRepo);
|
|
265
|
+
}
|
|
266
|
+
if (candidates.length === 0)
|
|
267
|
+
return undefined;
|
|
268
|
+
let best = candidates[0];
|
|
269
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
270
|
+
if (candidates[i].ord < best.ord)
|
|
271
|
+
best = candidates[i];
|
|
272
|
+
}
|
|
273
|
+
return best.inst;
|
|
274
|
+
}
|
|
232
275
|
/** 合并条目 → MarketItem:社区条目带 community 标记与旁路字段。 */
|
|
233
|
-
function toMarketItem(entry,
|
|
234
|
-
const inst =
|
|
276
|
+
function toMarketItem(entry, installedIndex) {
|
|
277
|
+
const inst = lookupInstalledByIndex(installedIndex, entry);
|
|
235
278
|
const item = { ...entry, installed: Boolean(inst), outdated: false };
|
|
236
279
|
if (inst) {
|
|
237
280
|
item.installedPkg = inst.pkg;
|
|
@@ -289,7 +332,7 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
289
332
|
const signal = opts.signal;
|
|
290
333
|
const remaining = () => deadlineAt - Date.now();
|
|
291
334
|
const registryTask = d.loadRegistry(cfg, { namespace, signal, force: opts.force, deadlineMs, profile: opts.profile });
|
|
292
|
-
const installedTask = d.listInstalledPlugins(opts.profileDir).catch(() => null);
|
|
335
|
+
const installedTask = d.listInstalledPlugins(opts.profileDir, { patchRows: false }).catch(() => null);
|
|
293
336
|
// 社区 flight 并发启动(source=primary 零调用;0.7.0 Task 7:primaryOnly 字段已删除);共享 loader 不接收调用者 deadline——
|
|
294
337
|
// listMarket 作为 waiter 在 communityOutcome 内 race 自己的剩余 deadline/signal(v10 契约)
|
|
295
338
|
const communityTask = source === 'primary' ? null : d.fetchCommunityCatalog(cfg, { namespace, signal, force: opts.force, profile: opts.profile });
|
|
@@ -393,7 +436,9 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
393
436
|
let offset = normalizeOffset(opts.offset);
|
|
394
437
|
if (total > 0 && offset >= total)
|
|
395
438
|
offset = Math.floor((total - 1) / limit) * limit;
|
|
396
|
-
|
|
439
|
+
// 0.9.68(ADR-0016 决策 5 同款思路,FP5 2026-10-15):已装索引每查询建一次,页内条目 O(1) 查表
|
|
440
|
+
const installedIndex = buildInstalledIndex(installedItems);
|
|
441
|
+
const items = ordered.slice(offset, offset + limit).map((entry) => toMarketItem(entry, installedIndex));
|
|
397
442
|
let latestComplete = true;
|
|
398
443
|
let latestTimedOut = false;
|
|
399
444
|
if (withLatest && items.length > 0) {
|
|
@@ -494,6 +539,64 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
494
539
|
community: community.summary,
|
|
495
540
|
};
|
|
496
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* 批量 id 存在性(收藏 stale 检测专用,FP2):一次装载 registry+社区、经物化合并市场(L2,
|
|
544
|
+
* 同身份与 listMarket 共享代)对 **merged 域**(主清单 + 存活社区条目,不含 displaced——与
|
|
545
|
+
* 今日 source:'all' 搜索域一致)建原始 id Set,按输入序过滤返回。
|
|
546
|
+
*
|
|
547
|
+
* 三不原则(与 listMarket 的接口差异,也是性能来源):不枚举已装、不探测 latest、不分页。
|
|
548
|
+
*
|
|
549
|
+
* 语义对照(旧路径 = 逐收藏 `market {query:id, source:'all', limit:8, probeMode:'cache-only'}`
|
|
550
|
+
* + items.some(id)):除一处自觉差异外逐点一致——**curatedFirst 挤出边界**:旧搜索页路径在
|
|
551
|
+
* primary 域同字段全词命中 ≥limit 条时会把 id-exact 的社区条目挤出前 8(误判 stale),本接口
|
|
552
|
+
* 的 Set 判定在该边界判 live(=「已从目录消失」的正确语义),属修复而非回归,勿改回搜索页行为。
|
|
553
|
+
*
|
|
554
|
+
* 失败语义:registry 装载 reject → 原样抛出(host-api 统一 500 → 客户端 catch 空 Set 全 stale,
|
|
555
|
+
* 与今日 market 同路径等价);deadline 逃逸 → `{ present: [] }`(同今日超时空页 → 全 stale);
|
|
556
|
+
* 社区 unavailable/disabled → merged=primary-only → 社区收藏判 stale(同今日)。
|
|
557
|
+
*/
|
|
558
|
+
export async function existingMarketIds(ids, cfg = {}, opts = {}, deps) {
|
|
559
|
+
const list = Array.isArray(ids) ? ids : [];
|
|
560
|
+
if (list.length === 0)
|
|
561
|
+
return { present: [] };
|
|
562
|
+
const d = {
|
|
563
|
+
loadRegistry: deps?.loadRegistry ?? defaultLoadRegistry,
|
|
564
|
+
fetchCommunityCatalog: deps?.fetchCommunityCatalog ?? defaultFetchCommunityCatalog,
|
|
565
|
+
};
|
|
566
|
+
const deadlineMs = typeof opts.deadlineMs === 'number' && Number.isFinite(opts.deadlineMs) && opts.deadlineMs > 0
|
|
567
|
+
? opts.deadlineMs
|
|
568
|
+
: DEFAULT_DEADLINE_MS;
|
|
569
|
+
const deadlineAt = Date.now() + deadlineMs;
|
|
570
|
+
const namespace = opts.namespace ?? 'host';
|
|
571
|
+
const signal = opts.signal;
|
|
572
|
+
const registryTask = d.loadRegistry(cfg, { namespace, signal, force: opts.force, deadlineMs, profile: opts.profile });
|
|
573
|
+
const communityTask = d.fetchCommunityCatalog(cfg, { namespace, signal, force: opts.force, profile: opts.profile });
|
|
574
|
+
let loaded;
|
|
575
|
+
try {
|
|
576
|
+
loaded = await deadlineRace(registryTask, deadlineAt - Date.now());
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
void communityTask.catch(() => undefined);
|
|
580
|
+
throw err;
|
|
581
|
+
}
|
|
582
|
+
if (loaded === 'deadline') {
|
|
583
|
+
void communityTask.catch(() => undefined);
|
|
584
|
+
return { present: [] };
|
|
585
|
+
}
|
|
586
|
+
const community = await mergedOutcome({
|
|
587
|
+
namespace,
|
|
588
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
589
|
+
cfg,
|
|
590
|
+
registry: loaded,
|
|
591
|
+
communityTask,
|
|
592
|
+
deadlineAt,
|
|
593
|
+
});
|
|
594
|
+
const knownIds = new Set(community.merged.map((entry) => entry.id));
|
|
595
|
+
const present = list
|
|
596
|
+
.map((id) => (typeof id === 'string' ? id.trim() : ''))
|
|
597
|
+
.filter((id) => id !== '' && knownIds.has(id));
|
|
598
|
+
return { present };
|
|
599
|
+
}
|
|
497
600
|
// ---------- 已装列表 ----------
|
|
498
601
|
export async function listInstalledWithMeta(cfg = {}, opts = {}, deps = {}) {
|
|
499
602
|
const d = { ...marketDeps(), ...deps };
|
|
@@ -54,7 +54,7 @@ export function matchInstalledByEntry(entry, installed) {
|
|
|
54
54
|
if (entry.npm && it.name === entry.npm)
|
|
55
55
|
return true;
|
|
56
56
|
if (entry.github && it.source === 'github') {
|
|
57
|
-
const m =
|
|
57
|
+
const m = GITHUB_SPEC_RE.exec(it.spec);
|
|
58
58
|
if (m && m[1] === entry.github)
|
|
59
59
|
return true;
|
|
60
60
|
}
|
|
@@ -158,7 +158,8 @@ function buildMatchIndex(domain) {
|
|
|
158
158
|
});
|
|
159
159
|
return { byNpm, byGithub, byId };
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
/** github spec 的 owner/repo 提取(matchInstalledByEntry 与 market.ts 已装索引共用单一事实源;2026-10-15 导出)。 */
|
|
162
|
+
export const GITHUB_SPEC_RE = /^github:([^#]+)/;
|
|
162
163
|
/** 三候选(npm[pkg] / npm[name] / github[spec](仅 source==='github'))取域序最前者胜——与 matchInstalledByEntry 逐点等价。 */
|
|
163
164
|
function lookupInIndex(index, installed) {
|
|
164
165
|
const candidates = [];
|