dsh-m 0.9.48 → 0.9.50

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 CHANGED
@@ -8,6 +8,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.50 变更:README 兜底 /latest 两腿阶梯 + 未装文案 profile-aware + 兜底失败摘要
12
+
13
+ - **动机(上海 Windows 桌面机实机实证)**:desktop profile(生效源 npmmirror)打开 billion-context(705k 周下载)详情 Modal 的 README 折叠页恒报「web profile 未安装该插件」——npmmirror packument 实测 8,586,604 字节,超过 npmPackumentReadme 的 8MB(8,388,608)上限被 readCapped 拒(`502 响应超过上限`),兜底腿必失败后按 0.9.45 语义如实重抛原始本地错误;同期 npmjs packument 8,195,089 字节恰低于帽、readme 为空,web 侧显示「没有 README」只是帽下侥幸——该包高频发版,npmjs 超帽只是时间问题。
14
+ - **README 兜底两腿阶梯**:`npmPackumentReadme` 内部改两腿(对外签名与返回形状 `{readme, repo}` 不变)——腿1 `GET <生效源>/<pkg>/latest` 版本文档优先(KB 级小载荷、packument 超帽免疫;URL 拼法同 npmLatest 先例;**超时收紧 `Math.min(timeoutMs, 5_000)`**,同 npmLatest 首腿先例——生效源不可达时兜底最坏时延 40s→25s),readme 命中即短路;空/失败落腿2 packument(20s 满额,行为同 0.9.45)。任一腿成功即成功:readme 空渲染「没有 README」(含 latest 空白 readme + packument 失败的降级,不抛);两腿全败抛终末腿(packument)错误。两腿均在生效源(ADR-0012),**不跨源**;两腿 readme 判空同一 trim 规则;repo 随到达的腿取 repository(packument 到达时以 packument 为准)。
15
+ - **未装文案 profile-aware**:`readInstalledPluginReadme` 新增第三参 profileName(缺省 'web'),host-api readme 分支传 `profile.name`——desktop profile 报「desktop profile 未安装该插件: X」(形态对齐 profile-ops 先例);market/toggle/profile-transaction 等 web 事务专用路径的「web profile」文案不变(desktop 走官方 pluginManager 委派,到不了那些代码)。
16
+ - **兜底失败摘要(0.9.45「兜底失败如实抛原始错误」的语义修订)**:两腿全败时抛 `<本地未装错误>(npm README 兜底失败: <终末腿原因>)`——真实死因(如「响应超过上限 8388608 字节」)不再被吞,红字自解释;`cause` 保留原始本地错误。
17
+ - **测试**:versions.test.mjs describe 重写为阶梯用例组(显式 registry 贯穿两腿、缺省生效源形态断言、latest 命中短路、空落 packument、latest 失败兜住、空白 readme 降级、终末腿错误、两腿都空、超时收紧 deps spy、repo 归一语义注记),红→绿全程断言(红集合 {2,3,4,6,8,9} 与计划推演逐条吻合);installed.test.mjs 增 profileName 用例;host-api.test.mjs readme 用例改断言 + 增 profile.name 传递与合成错误形态两用例。全量测试零新增失败(Windows 本机 symlink 族既有基线豁免);typecheck 零错误。
18
+
19
+ ### 0.9.49 变更:已装 README 基址贯通 + 链接归一三件(0.9.48 深入分析跟进)
20
+
21
+ - **已装视图基址贯通(P0,主缺口)**:0.9.48 已装详情挂载点误传不存在的 `it.github`,而已装插件走本地读取路径(`repo` 字段只在 npm 兜底路径附带)→ 已装 README 相对链接全部归 `#`。现改传 `vm.githubRepo`——installed-view 视图模型现成的三级兜底(registry 匹配 `registryGithub` → 已装包 package.json `repository` 解析 → 安装 spec `github:o/r` 解析),图标与 LinksRow 早就在用。装机浏览场景的相对链接/图片自此全部可锚定。
22
+ - **链接归一(P1)**:①协议相对 `//host/x` 补 `https:`——此前有基址时被错剥成仓库内路径、无基址时跳 DSH 站内;②`git@github.com:o/r(.git)` 内联链接归一 https 形态(README 的 clone 指引段落);③有基址时 `#anchor` 锚 `github.com/<o>/<r>#anchor`(仓库首页 README 同名锚,裸 `#` 与图片 kind 不参与)。
23
+ - **host 侧解析口径统一(P2)**:`githubRepoFromRepository` 尾部从 `$` 放宽到 `[/?#]`——`github.com/o/r/`(尾斜杠)与 `…/tree/main` 等更深路径此前解析不出;与 versions.ts `extractGithubRepo` 同口径。
24
+ - **测试**:client-markdown 净增 4 例(协议相对两态、git@ 两形态、锚点四断言含 img kind 不锚)、installed 净增 1 例(repository 五形态含尾斜杠与子路径);锚点断言随新行为更新;全量 1185/1185、typecheck 零错误。纯客户端+host 只读字段透传变更,刷新页面即生效。
25
+
11
26
  ### 0.9.48 变更:README 实体解码 + 相对路径仓库基址锚定(0.9.47 实装反馈两连修)
12
27
 
13
28
  - **HTML 实体解码**:0.9.47 解析了标签但未解码实体——dsh-task-board 类 README 徽章间独立成行的 `&nbsp;` 按字面漏出。现命名实体(nbsp/amp/lt/gt/quot/copy/mdash/箭头/分数等 60+ 高频集)与数字实体(十进制/十六进制)均解码;文本段与 HTML 属性值都解(徽章 src 里 `&amp;labelColor` → `&labelColor`,shields 参数不再丢失);`code`/`pre` 内不解码;未知实体与裸 `&` 原样保留;单趟解码不回炉(`&amp;lt;` → `&lt;` 文本,与浏览器一致),解码结果只作为文本节点、绝不重新参与标签解析,无注入面。
@@ -364,6 +379,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
364
379
 
365
380
  ## English
366
381
 
382
+ ### Added in 0.9.50 — two-leg /latest README fallback ladder + profile-aware not-installed copy + fallback failure summary
383
+
384
+ - **Motivation (verified on the Shanghai Windows desktop)**: on the desktop profile (effective registry npmmirror), the README fold for billion-context (705k weekly downloads) always failed with "web profile 未安装该插件" — npmmirror's packument measures 8,586,604 bytes, above dsh-m's 8MB (8,388,608) maxBytes cap (readCapped throws a 502 "response over limit"), so the fallback leg always failed and the original local error was rethrown per the 0.9.45 contract; npmjs's packument (8,195,089 bytes) merely happens to sit under the cap with an empty readme field — the web profile's "(no README)" was luck, and this fast-releasing package will outgrow the cap on npmjs too.
385
+ - **Two-leg fallback ladder**: `npmPackumentReadme` now tries `/latest` internally (public signature and `{readme, repo}` shape unchanged) — leg 1 fetches the KB-sized version document (immune to oversized packuments; URL shape per the npmLatest precedent; **timeout tightened to `Math.min(timeoutMs, 5_000)`** like npmLatest's first leg — worst-case fallback latency drops from 40s to 25s) and short-circuits on a non-empty readme; empty/failed falls through to the packument leg (full 20s, unchanged 0.9.45 behavior). Any successful leg wins: an empty readme renders "no README" (including the degraded blank-latest + failed-packument case, no throw); both legs failing throws the terminal (packument) error. Both legs hit the effective registry (ADR-0012), never cross-source; both legs share the same trim-based emptiness rule; repo follows whichever leg was reached (packument wins when it responds).
386
+ - **Profile-aware not-installed copy**: `readInstalledPluginReadme` gains a third parameter profileName (default 'web'), and the host-api readme branch passes `profile.name` — the desktop profile now reports "desktop profile 未安装该插件: X" (shape aligned with the profile-ops precedent); the "web profile" copy in web-transaction-only paths (market/toggle/profile-transaction) is intentionally unchanged (desktop delegates to the official pluginManager and never reaches them).
387
+ - **Fallback failure summary (semantic revision of 0.9.45's "rethrow the original local error")**: when both legs fail, the error becomes `<local error>(npm README 兜底失败: <terminal leg reason>)` — the real cause (e.g. the over-cap 502) is no longer swallowed; the original local error is preserved as `cause`.
388
+ - **Testing**: versions.test.mjs describe rewritten as a ladder suite (explicit registry across both legs, default-registry shape assertions, latest short-circuit, empty-readme fallthrough, latest-failure recovery, blank-readme degradation, terminal-leg error, both-empty, tightened-timeout deps spy, repo-normalization note), asserted red→green throughout (red set {2,3,4,6,8,9} matched the plan case by case); installed.test.mjs adds a profileName case; host-api.test.mjs extends the readme case and adds profile.name pass-through plus composed-error shape cases. Full suite shows zero new failures (existing Windows symlink-family baseline exempt); typecheck clean.
389
+
390
+ ### Added in 0.9.49 — installed-view repo base + three link normalizations (follow-up to the 0.9.48 deep-dive)
391
+
392
+ - **Installed-view base wired through (P0, the main gap)**: the 0.9.48 installed-detail mount passed a non-existent `it.github`, and installed plugins take the local-read path (the `repo` field only rides on the npm fallback) → every relative link in installed READMEs collapsed to `#`. Now passes `vm.githubRepo` — the ready three-level fallback in the installed view model (registry match `registryGithub` → installed package.json `repository` parse → install spec `github:o/r` parse), which the icon and LinksRow already consumed. Relative links and images now anchor in the installed-browsing scenario too.
393
+ - **Link normalizations (P1)**: ① protocol-relative `//host/x` gains `https:` — previously mangled into a repo path with a base, or pointed at the DSH origin without; ② inline `git@github.com:o/r(.git)` links normalize to https form (clone-instruction paragraphs); ③ with a base, `#anchor` anchors to `github.com/<o>/<r>#anchor` (same-name anchor on the repo home README; bare `#` and the img kind do not participate).
394
+ - **Host-side parse alignment (P2)**: `githubRepoFromRepository` tail relaxed from `$` to `[/?#]` — `github.com/o/r/` (trailing slash) and deeper paths like `…/tree/main` previously failed to parse; now aligned with versions.ts `extractGithubRepo`.
395
+ - **Testing**: 4 new cases in client-markdown (protocol-relative both modes, git@ two shapes, four anchor assertions incl. img kind), 1 new installed case (five repository shapes incl. trailing slash and subpath); the anchor assertion updated to the new behavior; full suite 1185/1185, typecheck clean. Client-only plus a read-only host passthrough — a page refresh takes effect.
396
+
367
397
  ### Added in 0.9.48 — README entity decoding + repo-base anchoring for relative paths (two fixes from 0.9.47 field feedback)
368
398
 
369
399
  - **HTML entity decoding**: 0.9.47 parsed tags but left entities raw — standalone `&nbsp;` lines between badges (dsh-task-board-style READMEs) leaked literally. Named entities (nbsp/amp/lt/gt/quot/copy/mdash/arrows/fractions — 60+ common set) and numeric entities (decimal/hex) now decode; both text runs and HTML attribute values decode (badge src `&amp;labelColor` → `&labelColor`, shields params no longer dropped); `code`/`pre` content stays literal; unknown entities and bare `&` are preserved; single-pass decoding without re-processing (`&amp;lt;` → `&lt;` as text, matching browsers). Decoded output only ever becomes text nodes — never re-parsed as tags, no injection surface.
package/docs/DESIGN.md CHANGED
@@ -182,7 +182,7 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
182
182
  技术:`src/client.js` 经 **esbuild** 打包为 `lib/client.js`;`window.__ModuleLoader__.load({ id: "dsh-m", factory })` 注册;**React 从 module loader require**(零额外运行时依赖);manifest 注入 `@deepseek-ai/dsh-client-runtime`、`@deepseek-ai/dsh-client-ui-slots`、`@deepseek-ai/dsh-client-ui-settings`。
183
183
  图标:GitHub 来源自动用 `https://github.com/<owner>.png?size=64`;`icon` 字段可覆盖;npm-only 条目首字母色块回退。
184
184
 
185
- **README 预览渲染(`src/client/markdown.js`,0.9.47 起)**:自研轻量渲染器,安全模型 = 「白名单标签 + 白名单属性 + safeUrl 闸门 → 全部经 `h()` 构造 React 元素」,**绝不注入原始 HTML 字符串**(无 `dangerouslySetInnerHTML`,文本节点 React 转义兜底)。markdown 侧支持标题/列表/表格/围栏/引用/徽章链接等;HTML 侧支持 GitHub 风 README 常见内嵌子集——块级包裹标签(`p/div/center/details/table/tr/td/li/ul/ol/h1-6` 等)剥壳后渲染:内容纯行内(无空行分段/无 markdown 块结构/行首均为行内标签)时整块行走内 token 流(徽章 + `&nbsp;` 连排一行,GitHub 语义,0.9.48),否则按 markdown 递归(保内部结构);行内标签(`a/img/strong/em/code/kbd/mark/br/sub/sup` 等)token 化渲染,`align` 属性映射 `textAlign`、`<img width/height>` 映射显式尺寸(放开徽章 20px 高度帽);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容丢弃,`javascript:` 等危险 URL 归 `#`。**HTML 实体解码(0.9.48)**:命名(60+ 高频集)+ 数字(十/十六进制),文本段与属性值都解、`code/pre` 不解、单趟不回炉、解码结果仅作文本节点。**仓库基址锚定(0.9.48)**:`renderMarkdown(src, { repo: "owner/repo" })` 后相对链接锚 `github.com/<o>/<r>/blob/HEAD/`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/`;基址来源 = npm packument `repository`(host-api readme 结果 `repo` 字段,仅 GitHub 归一)> 卡片 `github` 字段;无基址时相对路径归 `#`(0.9.47 前行为)。已知边界:monorepo 子目录 README 的相对路径锚到仓库根(packument 不携带子目录信息);绝对 URL 与页内锚点不受锚定影响。数据源:已装条目读 profile 本地 `README.md`(≤64KB 截断);未装条目回源 npm packument 顶层 `readme` 字段(0.9.45 U10b)。
185
+ **README 预览渲染(`src/client/markdown.js`,0.9.47 起)**:自研轻量渲染器,安全模型 = 「白名单标签 + 白名单属性 + safeUrl 闸门 → 全部经 `h()` 构造 React 元素」,**绝不注入原始 HTML 字符串**(无 `dangerouslySetInnerHTML`,文本节点 React 转义兜底)。markdown 侧支持标题/列表/表格/围栏/引用/徽章链接等;HTML 侧支持 GitHub 风 README 常见内嵌子集——块级包裹标签(`p/div/center/details/table/tr/td/li/ul/ol/h1-6` 等)剥壳后渲染:内容纯行内(无空行分段/无 markdown 块结构/行首均为行内标签)时整块行走内 token 流(徽章 + `&nbsp;` 连排一行,GitHub 语义,0.9.48),否则按 markdown 递归(保内部结构);行内标签(`a/img/strong/em/code/kbd/mark/br/sub/sup` 等)token 化渲染,`align` 属性映射 `textAlign`、`<img width/height>` 映射显式尺寸(放开徽章 20px 高度帽);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容丢弃,`javascript:` 等危险 URL 归 `#`。**HTML 实体解码(0.9.48)**:命名(60+ 高频集)+ 数字(十/十六进制),文本段与属性值都解、`code/pre` 不解、单趟不回炉、解码结果仅作文本节点。**仓库基址锚定(0.9.48)**:`renderMarkdown(src, { repo: "owner/repo" })` 后相对链接锚 `github.com/<o>/<r>/blob/HEAD/`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/`;基址来源 = npm packument/latest 文档 `repository`(host-api readme 结果 `repo` 字段,仅 GitHub 归一)> 卡片 `github` 字段;无基址时相对路径归 `#`(0.9.47 前行为)。**链接归一(0.9.49)**:协议相对 `//host/x` 补 `https:`;`git@github.com:o/r(.git)` 内联归一 https 形态;有基址时 `#anchor` 锚 `github.com/<o>/<r>#anchor`(裸 `#` 与 img kind 不锚);已装视图挂载点改传 `vm.githubRepo`(installed-view 三级兜底:registryGithub → 已装包 repository 解析 → spec 解析;0.9.48 曾误传不存在的 `it.github` 致已装 README 相对链接全归 `#`);host 侧 `githubRepoFromRepository` 尾部放宽到 `[/?#]`(与 versions.ts `extractGithubRepo` 同口径)。已知边界:monorepo 子目录 README 的相对路径锚到仓库根(packument 不携带子目录信息);绝对 URL 不受锚定影响。数据源:已装条目读 profile 本地 `README.md`(≤64KB 截断);未装条目回源 npm,两腿阶梯(0.9.50)——`/latest` 版本文档 `readme` 优先(KB 级小载荷,packument 超帽免疫,超时收紧 ≤5s),空/失败落 packument 顶层 `readme`(0.9.45 U10b);任一腿成功即成功渲染(readme 空 → 「没有 README」),两腿全败抛本地未装错误并附兜底失败摘要(0.9.50);`repo` 随到达的腿取 `repository`(packument 到达时以 packument 为准)。两腿均在生效源(ADR-0012),不跨源。
186
186
 
187
187
  ## 5. Agent 工具(host)与 CLI
188
188
 
package/lib/client.js CHANGED
@@ -410,9 +410,16 @@ function normalizeRepoInput(v) {
410
410
  return m ? `${m[1]}/${m[2]}` : "";
411
411
  }
412
412
  function safeUrl(u, kind) {
413
- const t = String(u || "").trim();
413
+ let t = String(u || "").trim();
414
+ if (!t) return "#";
415
+ if (t.startsWith("//")) t = `https:${t}`;
416
+ const ghm = /^git@github\.com:([^/\s]+)\/([^\s]+?)(?:\.git)?$/i.exec(t);
417
+ if (ghm) t = `https://github.com/${ghm[1]}/${ghm[2]}`;
414
418
  if (/^(https?:\/\/|mailto:)/i.test(t)) return t;
415
- if (t.startsWith("#")) return t;
419
+ if (t.startsWith("#")) {
420
+ if (t.length > 1 && RENDER_REPO && kind === "link") return `https://github.com/${RENDER_REPO}#${t.slice(1)}`;
421
+ return t;
422
+ }
416
423
  if (RENDER_REPO && (kind === "img" || kind === "link")) {
417
424
  const path = t.replace(/^(?:\.\/|\.\.\/|\/)+/, "");
418
425
  return kind === "img" ? `https://raw.githubusercontent.com/${RENDER_REPO}/HEAD/${path}` : `https://github.com/${RENDER_REPO}/blob/HEAD/${path}`;
@@ -3560,7 +3567,7 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
3560
3567
  }),
3561
3568
  open: openPkg === it.pkg,
3562
3569
  onToggle: () => setOpenPkg(openPkg === it.pkg ? null : it.pkg),
3563
- detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg, repo: it.github }) : DetailRows([
3570
+ detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg, repo: vm.githubRepo }) : DetailRows([
3564
3571
  [lookup("detail.pkg"), it.pkg],
3565
3572
  [lookup("detail.spec"), it.spec],
3566
3573
  [lookup("detail.latest"), vm.latestLabelDetail],
@@ -440,19 +440,22 @@ export function createApiDispatcher(ctx) {
440
440
  throw new ApiProtocolError(400, '缺少 pkg');
441
441
  // 0.9.0 双 profile:README 优先只读当前 profile 的 node_modules;
442
442
  // 0.9.45 U10b 修复:市场/收藏详情 Modal 的 README 折叠页对「未安装」条目也会发起读取
443
- // (装前决策场景)——本地未装时回源 npm packument 顶层 readme 字段兜底;
444
- // 兜底也失败(离线等)则抛原始本地错误,不冒充成功。
443
+ // (装前决策场景)——本地未装时回源 npm 兜底(0.9.50 起为 /latest + packument 两腿阶梯);
444
+ // 0.9.50 语义修订:兜底两腿全败不再只抛原始本地错误——本地错误为主语、附「npm README
445
+ // 兜底失败: <终末腿原因>」摘要,真实死因(如响应超帽)不再被吞。
445
446
  let result;
446
447
  try {
447
- result = await d.readInstalledPluginReadme(target, profile.dir);
448
+ result = await d.readInstalledPluginReadme(target, profile.dir, profile.name);
448
449
  }
449
450
  catch (localErr) {
450
451
  try {
451
452
  const fallback = await d.npmPackumentReadme(target);
452
453
  result = { pkg: target, name: target, readme: fallback.readme, truncated: false, repo: fallback.repo || undefined };
453
454
  }
454
- catch {
455
- throw localErr;
455
+ catch (fallbackErr) {
456
+ const localMsg = localErr instanceof Error ? localErr.message : String(localErr);
457
+ const fallbackMsg = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
458
+ throw new Error(`${localMsg}(npm README 兜底失败: ${fallbackMsg})`, { cause: localErr });
456
459
  }
457
460
  }
458
461
  payload = { ...result };
@@ -52,7 +52,9 @@ export function resolvePluginDir(profileDir, pkg, spec) {
52
52
  }
53
53
  return join(resolve(profileDir), 'node_modules', pkg);
54
54
  }
55
- /** 从 repository 字段(字符串或 {url},git+/ssh/https 形态)提取 github owner/repo。 */
55
+ /** 从 repository 字段(字符串或 {url},git+/ssh/https 形态)提取 github owner/repo。
56
+ * 0.9.49:尾部放宽到 [/?#] 或结尾(与 versions.ts extractGithubRepo 同口径)——
57
+ * 此前 github.com/o/r/(尾斜杠)与更深路径解析不出。 */
56
58
  export function githubRepoFromRepository(raw) {
57
59
  let url = '';
58
60
  if (typeof raw === 'string')
@@ -62,7 +64,7 @@ export function githubRepoFromRepository(raw) {
62
64
  if (typeof u === 'string')
63
65
  url = u;
64
66
  }
65
- const m = /github\.com[/:]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?$/i.exec(url.trim());
67
+ const m = /github\.com[/:]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(url.trim());
66
68
  return m ? `${m[1]}/${m[2]}` : null;
67
69
  }
68
70
  /** JSON 合法根:非 null、非数组的对象(typeof [] === 'object',数组必须显式排除)。 */
@@ -220,8 +222,8 @@ async function readTextLimited(path, limit) {
220
222
  await fh.close().catch(() => undefined);
221
223
  }
222
224
  }
223
- /** 读取单个已安装插件的 README(UTF-8,≤64KB,超限截断)。pkg 必须来自 profile 依赖。 */
224
- export async function readInstalledPluginReadme(pkg, profileDir = webProfileDir()) {
225
+ /** 读取单个已安装插件的 README(UTF-8,≤64KB,超限截断)。pkg 必须来自 profile 依赖。profileName 仅用于错误文案(0.9.50:desktop 等 profile 不再误报「web profile」,形态对齐 profile-ops 先例)。 */
226
+ export async function readInstalledPluginReadme(pkg, profileDir = webProfileDir(), profileName = 'web') {
225
227
  const key = String(pkg || '').trim();
226
228
  if (!isSafePkgName(key))
227
229
  throw new Error(`无效插件包名: ${pkg}`);
@@ -230,7 +232,7 @@ export async function readInstalledPluginReadme(pkg, profileDir = webProfileDir(
230
232
  // 授权边界必须用 own-property 判定:`in` 会沿原型链命中继承属性(如 'constructor'),
231
233
  // 绕过「pkg 必须来自 profile dependencies」的成员约束
232
234
  if (!Object.hasOwn(deps, key)) {
233
- throw new Error(`web profile 未安装该插件: ${key}`);
235
+ throw new Error(`${profileName} profile 未安装该插件: ${key}`);
234
236
  }
235
237
  const dir = resolvePluginDir(root, key, deps[key]);
236
238
  if (!dir)
@@ -130,24 +130,49 @@ export function extractGithubRepo(v) {
130
130
  }
131
131
  /**
132
132
  * npm packument readme 兜底(0.9.45 U10b 修复):市场/收藏详情 Modal 的 README 折叠页对
133
- * 「未安装」条目无法走本地 profile 读取——回源 packument 顶层 readme 字段(publish 管线
134
- * 对多数包写入)。安全基线同 npmPackument:HTTPS、8MB 上限、超时;无 readme 字段返回空串
135
- * (不抛,客户端显示「没有 README」);L1(ADR-0012):显式 registry 参数 > 生效源。
136
- * 0.9.48:随附 repo(packument repository 归一为 owner/repo,仅 GitHub),供 README
137
- * 相对链接/图片锚定仓库基址;无 repository 或非 GitHub 时为空串。
133
+ * 「未安装」条目无法走本地 profile 读取——回源 npm 兜底。安全基线同 npmPackument:HTTPS、
134
+ * 8MB 上限;无 readme 字段返回空串(不抛,客户端显示「没有 README」);L1(ADR-0012):
135
+ * 显式 registry 参数 > 生效源。
136
+ * 0.9.48:随附 repo(repository 归一为 owner/repo,仅 GitHub),供 README 相对链接/图片
137
+ * 锚定仓库基址;无 repository 或非 GitHub 时为空串。
138
+ * 0.9.50:两腿阶梯——腿1 `/latest` 版本文档优先(KB 级小载荷,超大 packument 超帽免疫;
139
+ * 超时收紧 `Math.min(timeoutMs, 5_000)`,同 npmLatest 首腿先例),readme 命中即短路;空或
140
+ * 腿失败落腿2 packument(20s 满额,行为同 0.9.45)。任一腿成功即成功——readme 空渲染
141
+ * 「没有 README」(含 latest 空白 readme + packument 失败的降级,不抛);两腿全败抛终末腿
142
+ * (packument)错误。两腿均在生效源,不跨源;两腿 readme 判空同一 trim 规则。repo 随到达
143
+ * 的腿取 repository(packument 到达时以 packument 为准)。
138
144
  */
139
145
  export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, registry, deps) {
140
146
  if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
141
147
  throw new Error(`无效 npm 包名: ${pkg}`);
142
148
  const fetcher = deps?.fetchJsonLimited ?? fetchJsonLimited;
143
149
  const base = registry?.trim() ? registry.trim() : await activeNpmRegistry();
144
- const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}`, {
145
- timeoutMs,
146
- signal,
147
- maxBytes: 8 * 1024 * 1024,
148
- });
149
- const readme = typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
150
- return { readme, repo: extractGithubRepo(data?.repository) };
150
+ const extractReadme = (data) => typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
151
+ // 腿1:/latest 版本文档(小载荷;超时收紧同 npmLatest 首腿先例)
152
+ let latestOk = false;
153
+ let latestRepo = '';
154
+ try {
155
+ const latestDoc = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}/latest`, { timeoutMs: Math.min(timeoutMs, 5_000), signal, maxBytes: 8 * 1024 * 1024 });
156
+ latestOk = true;
157
+ latestRepo = extractGithubRepo(latestDoc?.repository);
158
+ const latestReadme = extractReadme(latestDoc);
159
+ if (latestReadme)
160
+ return { readme: latestReadme, repo: latestRepo };
161
+ }
162
+ catch {
163
+ // latest 腿失败 → 落 packument 腿(终末腿语义见下)
164
+ }
165
+ // 腿2:packument(0.9.45 U10b 原兜底腿,行为不变)
166
+ try {
167
+ const data = await fetcher(`${registryBase(base)}/${encodeURIComponent(pkg)}`, { timeoutMs, signal, maxBytes: 8 * 1024 * 1024 });
168
+ return { readme: extractReadme(data), repo: extractGithubRepo(data?.repository) };
169
+ }
170
+ catch (packumentErr) {
171
+ // latest 腿已成功证明包在而无有效 readme → 降级成功(空 readme),不冒充失败
172
+ if (latestOk)
173
+ return { readme: '', repo: latestRepo };
174
+ throw packumentErr;
175
+ }
151
176
  }
152
177
  /**
153
178
  * 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.48",
3
+ "version": "0.9.50",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",