dsh-m 0.9.49 → 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,14 @@ 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
+
11
19
  ### 0.9.49 变更:已装 README 基址贯通 + 链接归一三件(0.9.48 深入分析跟进)
12
20
 
13
21
  - **已装视图基址贯通(P0,主缺口)**:0.9.48 已装详情挂载点误传不存在的 `it.github`,而已装插件走本地读取路径(`repo` 字段只在 npm 兜底路径附带)→ 已装 README 相对链接全部归 `#`。现改传 `vm.githubRepo`——installed-view 视图模型现成的三级兜底(registry 匹配 `registryGithub` → 已装包 package.json `repository` 解析 → 安装 spec `github:o/r` 解析),图标与 LinksRow 早就在用。装机浏览场景的相对链接/图片自此全部可锚定。
@@ -371,6 +379,14 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
371
379
 
372
380
  ## English
373
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
+
374
390
  ### Added in 0.9.49 — installed-view repo base + three link normalizations (follow-up to the 0.9.48 deep-dive)
375
391
 
376
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.
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 前行为)。**链接归一(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 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
 
@@ -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 };
@@ -222,8 +222,8 @@ async function readTextLimited(path, limit) {
222
222
  await fh.close().catch(() => undefined);
223
223
  }
224
224
  }
225
- /** 读取单个已安装插件的 README(UTF-8,≤64KB,超限截断)。pkg 必须来自 profile 依赖。 */
226
- 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') {
227
227
  const key = String(pkg || '').trim();
228
228
  if (!isSafePkgName(key))
229
229
  throw new Error(`无效插件包名: ${pkg}`);
@@ -232,7 +232,7 @@ export async function readInstalledPluginReadme(pkg, profileDir = webProfileDir(
232
232
  // 授权边界必须用 own-property 判定:`in` 会沿原型链命中继承属性(如 'constructor'),
233
233
  // 绕过「pkg 必须来自 profile dependencies」的成员约束
234
234
  if (!Object.hasOwn(deps, key)) {
235
- throw new Error(`web profile 未安装该插件: ${key}`);
235
+ throw new Error(`${profileName} profile 未安装该插件: ${key}`);
236
236
  }
237
237
  const dir = resolvePluginDir(root, key, deps[key]);
238
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.49",
3
+ "version": "0.9.50",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",