dsh-m 0.9.44 → 0.9.46
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.en.md +52 -150
- package/README.md +52 -151
- package/docs/DESIGN.md +1 -0
- package/lib/client.js +151 -23
- 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 +4 -2
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))}
|
|
@@ -2118,7 +2149,7 @@ function useMarketData(zone = "community") {
|
|
|
2118
2149
|
const queryRef = useRef(query);
|
|
2119
2150
|
const dataRef = useRef(boot.data);
|
|
2120
2151
|
const storage = () => typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
|
|
2121
|
-
const fetchPage = useCallback((nextQuery, force, background = false) => {
|
|
2152
|
+
const fetchPage = useCallback((nextQuery, force, background = false, probeMode = "cache-only") => {
|
|
2122
2153
|
const gen = ++genRef.current;
|
|
2123
2154
|
abortRef.current?.abort();
|
|
2124
2155
|
const ac = new AbortController();
|
|
@@ -2135,15 +2166,23 @@ function useMarketData(zone = "community") {
|
|
|
2135
2166
|
...nextQuery.sort ? { sort: nextQuery.sort } : {},
|
|
2136
2167
|
offset: nextQuery.offset,
|
|
2137
2168
|
limit: nextQuery.limit,
|
|
2169
|
+
// 0.9.45 两段加载(ADR-0013):第一段 cache-only 零网络回页;'full' 为终态语义不下传(core 缺省等价)
|
|
2170
|
+
...probeMode !== "full" ? { probeMode } : {},
|
|
2138
2171
|
...force ? { force: true } : {}
|
|
2139
2172
|
};
|
|
2140
2173
|
return api("market", params, ac.signal).then((raw) => {
|
|
2141
2174
|
if (genRef.current !== gen || ac.signal.aborted) return;
|
|
2142
2175
|
const next = normalizeMarketResponse2(raw);
|
|
2143
|
-
dataRef.current
|
|
2144
|
-
|
|
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);
|
|
2145
2181
|
setLoading(false);
|
|
2146
|
-
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 });
|
|
2147
2186
|
}).catch((e) => {
|
|
2148
2187
|
if (genRef.current !== gen || ac.signal.aborted) return;
|
|
2149
2188
|
if (background && dataRef.current) return;
|
|
@@ -2157,7 +2196,7 @@ function useMarketData(zone = "community") {
|
|
|
2157
2196
|
setQuery(next);
|
|
2158
2197
|
if (opts.fetch !== false) fetchPage(next, opts.force);
|
|
2159
2198
|
}, [fetchPage]);
|
|
2160
|
-
const reload = useCallback((force) => fetchPage(queryRef.current, force), [fetchPage]);
|
|
2199
|
+
const reload = useCallback((force) => fetchPage(queryRef.current, force, false, "full"), [fetchPage]);
|
|
2161
2200
|
useEffect(() => {
|
|
2162
2201
|
fetchPage(queryRef.current, false, boot.has);
|
|
2163
2202
|
return () => abortRef.current?.abort();
|
|
@@ -2371,7 +2410,7 @@ function SearchBox({ placeholder, initial, onCommit }) {
|
|
|
2371
2410
|
}, h(XIcon)) : null
|
|
2372
2411
|
);
|
|
2373
2412
|
}
|
|
2374
|
-
function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
2413
|
+
function ZoneChips({ zone, counts, labels, active, onPick, trailing, wrapTitle }) {
|
|
2375
2414
|
const chips = useMemo(() => zoneChips2(counts, labels, zone), [counts, labels, zone]);
|
|
2376
2415
|
const [expanded, setExpanded] = useState(false);
|
|
2377
2416
|
const [stuck, setStuck] = useState(false);
|
|
@@ -2423,8 +2462,9 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
|
2423
2462
|
const hidden = chips.length - shown.length;
|
|
2424
2463
|
const btn = (c) => h(
|
|
2425
2464
|
"button",
|
|
2426
|
-
{ key: c.id, "data-chip": "1", className: `dshm-chip${active === c.id ? " on" : ""}`, onClick: () => onPick(active === c.id ? null : c.id) },
|
|
2427
|
-
|
|
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}`
|
|
2428
2468
|
);
|
|
2429
2469
|
return h(
|
|
2430
2470
|
React.Fragment,
|
|
@@ -2435,7 +2475,7 @@ function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
|
|
|
2435
2475
|
{ className: "dsvm-chipswrap" },
|
|
2436
2476
|
h(
|
|
2437
2477
|
"div",
|
|
2438
|
-
{ ref: wrapRef, className: "dshm-chips" },
|
|
2478
|
+
{ ref: wrapRef, className: "dshm-chips", title: wrapTitle || void 0 },
|
|
2439
2479
|
h("button", { "data-chip": "1", className: `dshm-chip${active == null ? " on" : ""}`, onClick: () => onPick(null) }, lookup("cat.all")),
|
|
2440
2480
|
...shown.map(btn),
|
|
2441
2481
|
hidden > 0 ? h(
|
|
@@ -2479,7 +2519,20 @@ function Shot({ src, onClick }) {
|
|
|
2479
2519
|
}, []);
|
|
2480
2520
|
return h(
|
|
2481
2521
|
"div",
|
|
2482
|
-
{
|
|
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
|
+
},
|
|
2483
2536
|
show ? h("img", { className: "dsvm-shot", src, alt: "", loading: "lazy", referrerPolicy: "no-referrer", fetchPriority: "low" }) : null
|
|
2484
2537
|
);
|
|
2485
2538
|
}
|
|
@@ -2512,11 +2565,16 @@ function useModalDepth(active) {
|
|
|
2512
2565
|
};
|
|
2513
2566
|
}, [active]);
|
|
2514
2567
|
}
|
|
2515
|
-
function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, installRec, installNote }) {
|
|
2568
|
+
function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeBusy, upgradeRec, profileKind, installRec, installNote }) {
|
|
2516
2569
|
useModalDepth(true);
|
|
2517
2570
|
const shots = it.community === true ? safeScreenshots(it) : [];
|
|
2518
2571
|
const [lb, setLb] = useState(null);
|
|
2519
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
|
+
}, []);
|
|
2520
2578
|
useEffect(() => {
|
|
2521
2579
|
const onKey = (e) => {
|
|
2522
2580
|
if (e.key === "Escape") {
|
|
@@ -2549,7 +2607,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2549
2607
|
{ className: "dsvm-modal", ...backdropCloseHandlers2(onClose) },
|
|
2550
2608
|
h(
|
|
2551
2609
|
"div",
|
|
2552
|
-
{ className: "dsvm-modalbox", onClick: (e) => e.stopPropagation() },
|
|
2610
|
+
{ className: "dsvm-modalbox", role: "dialog", "aria-modal": "true", "aria-label": it.name, onClick: (e) => e.stopPropagation() },
|
|
2553
2611
|
h(
|
|
2554
2612
|
"div",
|
|
2555
2613
|
{ className: "dsvm-modalhead" },
|
|
@@ -2563,7 +2621,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2563
2621
|
it.community === true ? h("span", { className: "dshm-badge info" }, lookup("badge.community")) : null,
|
|
2564
2622
|
h("span", { className: "dshm-badge info" }, it.source === "npm" ? "npm" : "github"),
|
|
2565
2623
|
h("span", { className: "dshm-spacer" }),
|
|
2566
|
-
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))
|
|
2567
2625
|
),
|
|
2568
2626
|
it.community === true ? h(
|
|
2569
2627
|
"div",
|
|
@@ -2577,7 +2635,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2577
2635
|
"dl",
|
|
2578
2636
|
{ className: "dsvm-kv" },
|
|
2579
2637
|
kv(lookup("modal.category"), catLabel),
|
|
2580
|
-
it.added ? kv(lookup("modal.added"), it.added) : null,
|
|
2638
|
+
it.added ? kv(lookup("modal.added"), fmtDate(it.added)) : null,
|
|
2581
2639
|
it.community === true ? kv(
|
|
2582
2640
|
lookup("modal.dlwindow"),
|
|
2583
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")
|
|
@@ -2591,6 +2649,14 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2591
2649
|
it.deprecated === true && it.replacement ? kv(lookup("modal.replacement"), it.replacement) : null
|
|
2592
2650
|
),
|
|
2593
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,
|
|
2594
2660
|
shots.length ? h(
|
|
2595
2661
|
"div",
|
|
2596
2662
|
{ className: "dsvm-shotrow" },
|
|
@@ -2623,12 +2689,18 @@ function DetailModal({ it, labels, busy, onClose, onInstall, profileKind, instal
|
|
|
2623
2689
|
// installRec 派生自全局操作记录(DESIGN §2.6「状态不挂卡片」的所有权模型不变,此处只是展示):
|
|
2624
2690
|
// 仅 queued/running 挂行,input(兼容待决)走上层 CompatDialog;installNote 为本次终态摘要。
|
|
2625
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,
|
|
2626
2693
|
installNote ? h("div", { key: "opnote", className: installNote.kind === "err" ? "dshm-err" : installNote.kind === "hint" ? "dshm-hint" : "dshm-ok" }, installNote.text) : null,
|
|
2627
2694
|
installNote && installNote.kind === "ok" ? h("div", { key: "opnote-rh", className: "dshm-hint" }, lookup(profileKind === "desktop" ? "profile.restartHint" : "banner.done")) : null,
|
|
2628
2695
|
h(
|
|
2629
2696
|
"div",
|
|
2630
2697
|
{ className: "dsvm-modalactions" },
|
|
2631
|
-
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"))
|
|
2632
2704
|
)
|
|
2633
2705
|
),
|
|
2634
2706
|
lb !== null && shots.length ? h(Lightbox, { shots, index: lb, onNav: setLb, onClose: () => setLb(null) }) : null
|
|
@@ -2661,6 +2733,7 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2661
2733
|
const [pageJump, setPageJump] = useState("");
|
|
2662
2734
|
const activeInstallRec = ops.records.find((r) => r.kind === "install" && (r.status === "running" || r.status === "queued" || r.status === "input")) || null;
|
|
2663
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;
|
|
2664
2737
|
const [installNote, setInstallNote] = useState(null);
|
|
2665
2738
|
const items = data && data.items || [];
|
|
2666
2739
|
const total = data && data.total || 0;
|
|
@@ -2753,6 +2826,36 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2753
2826
|
}
|
|
2754
2827
|
}
|
|
2755
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
|
+
};
|
|
2756
2859
|
const CompatDialog = compatConfirm ? h(
|
|
2757
2860
|
"div",
|
|
2758
2861
|
{ className: "dshm-compat-overlay", ...backdropCloseHandlers2(() => setCompatConfirm(null)) },
|
|
@@ -2824,6 +2927,9 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2824
2927
|
busy: activeInstallTarget === favDetailItem.id,
|
|
2825
2928
|
onClose: () => setFavDetailItem(null),
|
|
2826
2929
|
onInstall: (it2) => doInstall(it2),
|
|
2930
|
+
onUpgrade: (it2) => doUpgrade(it2),
|
|
2931
|
+
upgradeBusy: activeUpgradeRec != null,
|
|
2932
|
+
upgradeRec: activeUpgradeRec && activeUpgradeRec.target === favDetailItem.installedPkg ? activeUpgradeRec : null,
|
|
2827
2933
|
profileKind,
|
|
2828
2934
|
installRec: activeInstallRec && activeInstallRec.target === favDetailItem.id ? activeInstallRec : null,
|
|
2829
2935
|
installNote: installNote && installNote.id === favDetailItem.id ? installNote : null
|
|
@@ -2964,12 +3070,27 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
2964
3070
|
labels: communityLabels(data),
|
|
2965
3071
|
active: query.category,
|
|
2966
3072
|
onPick: (id) => updateQuery({ category: id, offset: 0 }),
|
|
2967
|
-
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
|
|
2968
3076
|
}),
|
|
2969
3077
|
// 0.9.15:安装目标条目的详情 Modal 打开时,进度行入 Modal、底层行让位(避免隔着遮罩双重透出);
|
|
2970
3078
|
// Modal 关闭后底层行照常回归(关闭弹窗的安装仍可见)
|
|
2971
3079
|
activeInstallTarget && !(detailItem && detailItem.id === activeInstallTarget) ? h(ProgressLine, { key: "prog" }) : null,
|
|
2972
|
-
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(
|
|
2973
3094
|
React.Fragment,
|
|
2974
3095
|
null,
|
|
2975
3096
|
h(
|
|
@@ -3008,6 +3129,9 @@ function MarketTab({ notify, markets, onMutation, ops, favorites, profileKind })
|
|
|
3008
3129
|
busy: activeInstallTarget === detailItem.id,
|
|
3009
3130
|
onClose: () => setDetailId(null),
|
|
3010
3131
|
onInstall: (it2) => doInstall(it2),
|
|
3132
|
+
onUpgrade: (it2) => doUpgrade(it2),
|
|
3133
|
+
upgradeBusy: activeUpgradeRec != null,
|
|
3134
|
+
upgradeRec: activeUpgradeRec && activeUpgradeRec.target === detailItem.installedPkg ? activeUpgradeRec : null,
|
|
3011
3135
|
profileKind,
|
|
3012
3136
|
installRec: activeInstallRec && activeInstallRec.target === detailItem.id ? activeInstallRec : null,
|
|
3013
3137
|
installNote: installNote && installNote.id === detailItem.id ? installNote : null
|
|
@@ -3253,7 +3377,7 @@ function regSourceLabel(data) {
|
|
|
3253
3377
|
function configStatusLabel(status) {
|
|
3254
3378
|
return lookup(`settings.status.${status || "loading"}` || "settings.status.loading");
|
|
3255
3379
|
}
|
|
3256
|
-
function SettingsTab({ notify, onRegistryChanged }) {
|
|
3380
|
+
function SettingsTab({ notify, onRegistryChanged, onForceMarket }) {
|
|
3257
3381
|
const reg = useAsync((force) => api("registry", force ? { force: true } : {}), []);
|
|
3258
3382
|
const cfgState = useAsync(() => api("registry-config"), []);
|
|
3259
3383
|
const [busy, setBusy] = useState(false);
|
|
@@ -3275,6 +3399,7 @@ function SettingsTab({ notify, onRegistryChanged }) {
|
|
|
3275
3399
|
try {
|
|
3276
3400
|
await reg.reload(true);
|
|
3277
3401
|
await reloadRegistryState().catch(() => void 0);
|
|
3402
|
+
onForceMarket?.();
|
|
3278
3403
|
notify({ kind: "ok", text: lookup("registry.refreshed"), needsRestart: false });
|
|
3279
3404
|
} finally {
|
|
3280
3405
|
setBusy(false);
|
|
@@ -3524,7 +3649,7 @@ function snapshotOf(it) {
|
|
|
3524
3649
|
categoryLabel: it.categoryLabel,
|
|
3525
3650
|
source: it.source
|
|
3526
3651
|
};
|
|
3527
|
-
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"]) {
|
|
3528
3653
|
if (it[k] !== void 0 && it[k] !== null) s[k] = it[k];
|
|
3529
3654
|
}
|
|
3530
3655
|
return s;
|
|
@@ -3583,7 +3708,10 @@ function FavoriteZone({ favorites, onOpen }) {
|
|
|
3583
3708
|
name: s.name,
|
|
3584
3709
|
badges: [
|
|
3585
3710
|
s.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
|
|
3586
|
-
|
|
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,
|
|
3587
3715
|
h("span", { className: "dshm-badge info", key: "s" }, s.source === "npm" ? "npm" : "github"),
|
|
3588
3716
|
isStale ? h("span", { className: "dshm-badge warn", key: "st" }, lookup("favorites.stalebadge")) : null
|
|
3589
3717
|
],
|
|
@@ -3962,7 +4090,7 @@ function MarketPanel({ onClose }) {
|
|
|
3962
4090
|
{ className: "dshm-body" },
|
|
3963
4091
|
tab === "market" ? h(MarketTab, { notify, markets, onMutation: refreshViews, ops, favorites, profileKind: profile?.kind ?? null }) : null,
|
|
3964
4092
|
tab === "installed" ? h(InstalledTab, { notify, installed, updates, onMutation: refreshViews, ops }) : null,
|
|
3965
|
-
tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged }) : null,
|
|
4093
|
+
tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged, onForceMarket: () => marketReloadAll(true) }) : null,
|
|
3966
4094
|
h(OperationsPanel, {
|
|
3967
4095
|
records: opRecords,
|
|
3968
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 时);
|