dsh-m 0.9.47 → 0.9.49

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,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.49 变更:已装 README 基址贯通 + 链接归一三件(0.9.48 深入分析跟进)
12
+
13
+ - **已装视图基址贯通(P0,主缺口)**:0.9.48 已装详情挂载点误传不存在的 `it.github`,而已装插件走本地读取路径(`repo` 字段只在 npm 兜底路径附带)→ 已装 README 相对链接全部归 `#`。现改传 `vm.githubRepo`——installed-view 视图模型现成的三级兜底(registry 匹配 `registryGithub` → 已装包 package.json `repository` 解析 → 安装 spec `github:o/r` 解析),图标与 LinksRow 早就在用。装机浏览场景的相对链接/图片自此全部可锚定。
14
+ - **链接归一(P1)**:①协议相对 `//host/x` 补 `https:`——此前有基址时被错剥成仓库内路径、无基址时跳 DSH 站内;②`git@github.com:o/r(.git)` 内联链接归一 https 形态(README 的 clone 指引段落);③有基址时 `#anchor` 锚 `github.com/<o>/<r>#anchor`(仓库首页 README 同名锚,裸 `#` 与图片 kind 不参与)。
15
+ - **host 侧解析口径统一(P2)**:`githubRepoFromRepository` 尾部从 `$` 放宽到 `[/?#]`——`github.com/o/r/`(尾斜杠)与 `…/tree/main` 等更深路径此前解析不出;与 versions.ts `extractGithubRepo` 同口径。
16
+ - **测试**:client-markdown 净增 4 例(协议相对两态、git@ 两形态、锚点四断言含 img kind 不锚)、installed 净增 1 例(repository 五形态含尾斜杠与子路径);锚点断言随新行为更新;全量 1185/1185、typecheck 零错误。纯客户端+host 只读字段透传变更,刷新页面即生效。
17
+
18
+ ### 0.9.48 变更:README 实体解码 + 相对路径仓库基址锚定(0.9.47 实装反馈两连修)
19
+
20
+ - **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;` 文本,与浏览器一致),解码结果只作为文本节点、绝不重新参与标签解析,无注入面。
21
+ - **行内优先判定**:包裹标签(`<p align="center">` 等)剥壳后,若内部无空行分段、无 markdown 块结构、且行首标签均为行内/void 标签,则整块按行内 token 流渲染——徽章 + `&nbsp;` 连排在同一居中行(GitHub 语义),不再每行拆成独立段落;含块级结构(`tr/td/ul/li` 开头等)仍走 markdown 递归保结构。
22
+ - **相对路径仓库基址锚定**:`[README.md](README.md)` 这类相对链接此前被 safeUrl 归 `#`(点击无反应——既有安全归化,非 0.9.47 回归)。现 README 预览可携带仓库基址:相对链接锚 `github.com/<o>/<r>/blob/HEAD/<path>`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/<path>`(`./`/`../` 前缀归一);绝对 URL 与页内锚点不受影响;无基址时保持归 `#` 旧行为。数据链路:npm packument 兜底时顺带提取 `repository` 归一为 `owner/repo`(仅 GitHub,git+https/git@/.git 形态归一,host-api 透传 `repo` 字段);客户端以卡片 `github` 字段兜底;渲染基址为 renderMarkdown 级同步设置、进出恢复,不跨渲染泄漏。
23
+ - **测试**:client-markdown 净增 6 例(实体四类、独立 `&nbsp;` 徽章块、code 不解码与属性解码、基址锚定七断言、无基址回归与跨渲染泄漏);versions 净增 1 例(repository 两形态归一 + 非 GitHub 归空);全量 1181/1181、typecheck 零错误;真实数据端到端——dsh-TUI 25926 字符零泄漏零回归、task-board 真实徽章块三徽章一行居中 + `&amp;` 解码、相对链接正确锚 blob/HEAD。纯客户端变更,刷新页面即生效。
24
+
11
25
  ### 0.9.47 变更:README 预览内嵌 HTML 子集渲染(GitHub 风 README 不再满屏标签)
12
26
 
13
27
  - **问题**:市场/收藏/已装详情 Modal 的 README 折叠页,遇到 GitHub 风重度内嵌 HTML 的 README(`<p align="center">` 徽章墙、`<img>` Logo、`<details>` 折叠等——npm packument 兜底与本地读取都常见)会把标签按字面整屏显示,观感如「乱码」。根因是渲染端自研 markdown 渲染器此前完全不解析 HTML 标签(原始内容本身没有问题,GitHub 上渲染正常)。
@@ -357,6 +371,20 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
357
371
 
358
372
  ## English
359
373
 
374
+ ### Added in 0.9.49 — installed-view repo base + three link normalizations (follow-up to the 0.9.48 deep-dive)
375
+
376
+ - **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.
377
+ - **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).
378
+ - **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`.
379
+ - **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.
380
+
381
+ ### Added in 0.9.48 — README entity decoding + repo-base anchoring for relative paths (two fixes from 0.9.47 field feedback)
382
+
383
+ - **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.
384
+ - **Inline-first heuristic**: after unwrapping a block wrapper (`<p align="center">` etc.), if the inner content has no blank-line paragraph breaks, no markdown block structures, and every line starts with an inline/void tag, the whole block renders through the inline token stream — badges + `&nbsp;` flow on one centered line (GitHub semantics) instead of one paragraph per line; content with block structures (lines starting with `tr/td/ul/li` etc.) still goes through the markdown recursion to preserve structure.
385
+ - **Repo-base anchoring for relative paths**: relative links like `[README.md](README.md)` previously collapsed to `#` via safeUrl (inert click — long-standing normalization, not a 0.9.47 regression). The README preview can now carry a repository base: relative links anchor to `github.com/<o>/<r>/blob/HEAD/<path>`, relative images anchor to `raw.githubusercontent.com/<o>/<r>/HEAD/<path>` (`./`/`../` prefixes normalized); absolute URLs and in-page anchors are untouched; without a base the old `#` behavior applies. Data flow: the npm packument fallback now also extracts `repository`, normalized to `owner/repo` (GitHub only; git+https/git@/.git forms normalized) and passed through host-api as `repo`; the client falls back to the card's `github` field; the render base is set per renderMarkdown call and restored on exit — no cross-render leakage.
386
+ - **Testing**: 6 new cases in client-markdown (four entity classes, standalone-`&nbsp;` badge block, code no-decode + attribute decoding, seven anchoring assertions, no-base regression + cross-render leakage); 1 new versions case (repository normalization in both shapes + non-GitHub → empty); full suite 1181/1181, typecheck clean; real-data end-to-end — dsh-TUI 25,926 chars zero leaks zero regression, the real task-board badge block renders three badges on one centered line with `&amp;` decoded, relative links anchor to blob/HEAD correctly. Client-only change — a page refresh takes effect.
387
+
360
388
  ### Added in 0.9.47 — embedded-HTML subset rendering for the README preview (GitHub-style READMEs no longer show raw tags)
361
389
 
362
390
  - **Problem**: the README fold in the detail Modal (market/curated/installed) showed GitHub-style HTML-heavy READMEs (`<p align="center">` badge walls, `<img>` logos, `<details>` blocks — common in both the npm packument fallback and local reads) with every tag rendered literally, looking like mojibake. Root cause: the in-house markdown renderer never parsed HTML tags at all. The source content itself was fine — GitHub renders it normally.
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 递归(保内部结构),行内标签(`a/img/strong/em/code/kbd/mark/br/sub/sup` 等)token 化渲染,`align` 属性映射 `textAlign`、`<img width/height>` 映射显式尺寸(放开徽章 20px 高度帽);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容丢弃,`javascript:` 等危险 URL 归 `#`。已知边界:npm packument 兜底的 README 不携带仓库基址,相对路径图片(如 `docs/assets/x.svg`)按 safeUrl 归 `#` 后由 onError 隐藏;行内文本含标签的解析粒度到段落级(`Press <kbd>Ctrl</kbd>` 类可渲染)。数据源:已装条目读 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 `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)。
186
186
 
187
187
  ## 5. Agent 工具(host)与 CLI
188
188
 
package/lib/client.js CHANGED
@@ -399,20 +399,55 @@ var init_backdrop = __esm({
399
399
  var markdown_exports = {};
400
400
  __export(markdown_exports, {
401
401
  createMarkdown: () => createMarkdown,
402
+ decodeEntities: () => decodeEntities,
402
403
  safeUrl: () => safeUrl
403
404
  });
404
- function safeUrl(u) {
405
- const t = String(u || "").trim();
405
+ function normalizeRepoInput(v) {
406
+ const s = String(v || "").trim();
407
+ if (!s) return "";
408
+ if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(s)) return s;
409
+ const m = /github\.com[/:]([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(s);
410
+ return m ? `${m[1]}/${m[2]}` : "";
411
+ }
412
+ function safeUrl(u, kind) {
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]}`;
406
418
  if (/^(https?:\/\/|mailto:)/i.test(t)) return t;
407
- if (/^[/#]/.test(t)) 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
+ }
423
+ if (RENDER_REPO && (kind === "img" || kind === "link")) {
424
+ const path = t.replace(/^(?:\.\/|\.\.\/|\/)+/, "");
425
+ return kind === "img" ? `https://raw.githubusercontent.com/${RENDER_REPO}/HEAD/${path}` : `https://github.com/${RENDER_REPO}/blob/HEAD/${path}`;
426
+ }
427
+ if (t.startsWith("/")) return t;
408
428
  return "#";
409
429
  }
430
+ function decodeEntities(s) {
431
+ const t = String(s || "");
432
+ if (!t.includes("&")) return t;
433
+ return t.replace(HTML_ENTITY_RE, (raw, body) => {
434
+ if (body.startsWith("#")) {
435
+ const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
436
+ return Number.isInteger(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : raw;
437
+ }
438
+ const named = HTML_ENTITIES[body] !== void 0 ? HTML_ENTITIES[body] : HTML_ENTITIES[body.toLowerCase()];
439
+ return named !== void 0 ? named : raw;
440
+ });
441
+ }
410
442
  function parseHtmlAttrs(s) {
411
443
  const attrs = {};
412
444
  if (!s) return attrs;
413
445
  const re = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
414
446
  let m;
415
- while (m = re.exec(s)) attrs[m[1].toLowerCase()] = m[2] !== void 0 ? m[2] : m[3] !== void 0 ? m[3] : m[4] !== void 0 ? m[4] : "";
447
+ while (m = re.exec(s)) {
448
+ const raw = m[2] !== void 0 ? m[2] : m[3] !== void 0 ? m[3] : m[4] !== void 0 ? m[4] : "";
449
+ attrs[m[1].toLowerCase()] = decodeEntities(raw);
450
+ }
416
451
  return attrs;
417
452
  }
418
453
  function htmlAlignStyle(attrs) {
@@ -451,13 +486,30 @@ function htmlBlockEnd(lines, i) {
451
486
  }
452
487
  return lastNonBlank;
453
488
  }
489
+ function decodeTextNodes(nodes) {
490
+ return nodes.map((n) => {
491
+ if (typeof n === "string") return decodeEntities(n);
492
+ if (Array.isArray(n)) return decodeTextNodes(n);
493
+ if (n && typeof n === "object" && n.type !== "code" && n.type !== "pre") {
494
+ const cur = n.props && n.props.children !== void 0 ? n.props.children : n.children;
495
+ if (cur !== void 0) {
496
+ const wasArr = Array.isArray(cur);
497
+ const decoded = decodeTextNodes(wasArr ? cur : [cur]);
498
+ const next = wasArr ? decoded : decoded[0];
499
+ if (n.props && n.props.children !== void 0) return { ...n, props: { ...n.props, children: next } };
500
+ return { ...n, children: next };
501
+ }
502
+ }
503
+ return n;
504
+ });
505
+ }
454
506
  function createMarkdown(h2) {
455
507
  function ExtLink2({ href, className, children }) {
456
508
  return h2(
457
509
  "a",
458
510
  {
459
511
  className: className || "dshm-md-a",
460
- href: safeUrl(href),
512
+ href: safeUrl(href, "link"),
461
513
  target: "_blank",
462
514
  rel: "noopener noreferrer",
463
515
  onClick: (e) => e.stopPropagation()
@@ -468,7 +520,7 @@ function createMarkdown(h2) {
468
520
  function MdImg2({ src, alt, style, title }) {
469
521
  return h2("img", {
470
522
  className: "dshm-md-img",
471
- src: safeUrl(src),
523
+ src: safeUrl(src, "img"),
472
524
  alt: alt || "",
473
525
  title: title || void 0,
474
526
  style: style || void 0,
@@ -494,7 +546,7 @@ function createMarkdown(h2) {
494
546
  const lm = /\]\(([^)]*)\)\s*$/.exec(tok);
495
547
  const img = h2(MdImg2, { src: im && im[2], alt: im && im[1] });
496
548
  const href = lm && lm[1];
497
- nodes.push(href && safeUrl(href) !== "#" ? h2(ExtLink2, { key: k, href }, img) : h2("span", { key: k }, img));
549
+ nodes.push(href && safeUrl(href, "link") !== "#" ? h2(ExtLink2, { key: k, href }, img) : h2("span", { key: k }, img));
498
550
  } else if (tok.startsWith("![") || tok.startsWith("<![")) {
499
551
  const im = /^!\[([^\]]*)\]\(([^)]*)\)$/.exec(tok);
500
552
  nodes.push(h2(MdImg2, { key: k, src: im && im[2], alt: im && im[1] }));
@@ -532,7 +584,7 @@ function createMarkdown(h2) {
532
584
  const isOl = (s) => /^\s*\d+[.)]\s+/.test(s);
533
585
  const isTableRow = (s) => s.includes("|") && /^\s*\|/.test(s);
534
586
  const isHtmlBlock = (s) => /^\s*<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s[^>]*?)?\/?>/.test(s);
535
- const inlineOf = (s, key) => /<[a-zA-Z]/.test(s) ? mdHtmlInline(s, key) : mdInline(s, key);
587
+ const inlineOf = (s, key) => decodeTextNodes(/<[a-zA-Z]/.test(s) ? mdHtmlInline(s, key) : mdInline(s, key));
536
588
  while (i < lines.length) {
537
589
  const line = lines[i];
538
590
  if (!line.trim()) {
@@ -620,13 +672,14 @@ function createMarkdown(h2) {
620
672
  const cur = () => stack.length ? stack[stack.length - 1].kids : out;
621
673
  const pushText = (t) => {
622
674
  if (!t) return;
623
- if (!String(t).trim()) {
624
- cur().push(" ");
675
+ const t2 = decodeEntities(t);
676
+ if (!t2.trim()) {
677
+ if (cur().length) cur().push(" ");
625
678
  return;
626
679
  }
627
- const parts = String(t).split(/\n[ \t]*\n+/);
680
+ const parts = t2.split(/\n[ \t]*\n+/);
628
681
  parts.forEach((part, idx) => {
629
- const nodes = mdInline(part.replace(/\s+/g, " "), `${kb}t${n++}`);
682
+ const nodes = decodeTextNodes(mdInline(part.replace(/\s+/g, " "), `${kb}t${n++}`));
630
683
  if (!nodes.length) return;
631
684
  if (idx === 0 || stack.length) cur().push(...nodes);
632
685
  else out.push(h2("p", { key: `${kb}tp${n++}` }, ...nodes));
@@ -686,6 +739,17 @@ function createMarkdown(h2) {
686
739
  while (stack.length) closeTop();
687
740
  return out;
688
741
  }
742
+ function isInlineOnlyHtml(inner) {
743
+ const lines = String(inner).split("\n");
744
+ if (lines.some((l) => l !== "" && !l.trim())) return false;
745
+ return !lines.some((l) => {
746
+ if (/^\s*(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|`|\|)/.test(l)) return true;
747
+ const tm = /^\s*<\/?([a-zA-Z][a-zA-Z0-9-]*)/.exec(l);
748
+ if (!tm) return false;
749
+ const name = tm[1].toLowerCase();
750
+ return !(name === "img" || name === "br" || name === "hr" || HTML_INLINE_MAP[name]);
751
+ });
752
+ }
689
753
  function mdHtmlBlock(raw, kb, depth) {
690
754
  const s = String(raw).trim();
691
755
  if (depth > 8) return [s];
@@ -698,6 +762,7 @@ function createMarkdown(h2) {
698
762
  let inner = s.slice(om[0].length, om[0].length + cm.index);
699
763
  const attrs = parseHtmlAttrs(om[2] || "");
700
764
  const style = htmlAlignStyle(attrs);
765
+ const inlineKids = () => isInlineOnlyHtml(inner) ? mdHtmlInline(inner, kb) : renderMarkdown2(inner, depth + 1);
701
766
  if (tag === "details") {
702
767
  const sm = /<summary[^>]*>([\s\S]*?)<\/summary>/i.exec(inner);
703
768
  let sumNode = null;
@@ -705,21 +770,30 @@ function createMarkdown(h2) {
705
770
  inner = inner.slice(0, sm.index) + inner.slice(sm.index + sm[0].length);
706
771
  sumNode = h2("summary", { key: `${kb}sum` }, ...mdHtmlInline(sm[1], `${kb}sm`));
707
772
  }
708
- const kids = sumNode ? [sumNode, ...renderMarkdown2(inner, depth + 1)] : renderMarkdown2(inner, depth + 1);
773
+ const kids = sumNode ? [sumNode, ...inlineKids()] : inlineKids();
709
774
  return [h2("details", attrs.open !== void 0 ? { key: kb, open: true } : { key: kb }, ...kids)];
710
775
  }
711
776
  if (/^h[1-6]$/.test(tag)) return [h2(tag, style ? { key: kb, style } : { key: kb }, ...mdHtmlInline(inner, kb))];
712
- return [h2(tag, style ? { key: kb, style } : { key: kb }, ...renderMarkdown2(inner, depth + 1))];
777
+ return [h2(tag, style ? { key: kb, style } : { key: kb }, ...inlineKids())];
713
778
  }
714
- function renderMarkdown2(src, depth = 0) {
715
- return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md", depth);
779
+ function renderMarkdown2(src, opts = {}, depth = 0) {
780
+ const prev = RENDER_REPO;
781
+ RENDER_REPO = normalizeRepoInput(opts && opts.repo);
782
+ try {
783
+ return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md", depth);
784
+ } finally {
785
+ RENDER_REPO = prev;
786
+ }
716
787
  }
717
788
  return { ExtLink: ExtLink2, MdImg: MdImg2, renderMarkdown: renderMarkdown2 };
718
789
  }
719
- var HTML_TAG_RE, HTML_VOID_TAGS, HTML_SKIP_TAGS, HTML_BLOCK_WRAP, HTML_SPAN_BLANK, HTML_INLINE_MAP, HTML_UNWRAP_SEP;
790
+ var RENDER_REPO, HTML_ENTITIES, HTML_ENTITY_RE, HTML_TAG_RE, HTML_VOID_TAGS, HTML_SKIP_TAGS, HTML_BLOCK_WRAP, HTML_SPAN_BLANK, HTML_INLINE_MAP, HTML_UNWRAP_SEP;
720
791
  var init_markdown = __esm({
721
792
  "src/client/markdown.js"() {
722
793
  "use strict";
794
+ RENDER_REPO = "";
795
+ HTML_ENTITIES = { nbsp: "\xA0", amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", copy: "\xA9", reg: "\xAE", trade: "\u2122", mdash: "\u2014", ndash: "\u2013", hellip: "\u2026", middot: "\xB7", laquo: "\xAB", raquo: "\xBB", times: "\xD7", divide: "\xF7", plusmn: "\xB1", deg: "\xB0", sup2: "\xB2", sup3: "\xB3", frac12: "\xBD", frac14: "\xBC", frac34: "\xBE", euro: "\u20AC", pound: "\xA3", yen: "\xA5", cent: "\xA2", sect: "\xA7", para: "\xB6", bull: "\u2022", dagger: "\u2020", Dagger: "\u2021", permil: "\u2030", prime: "\u2032", Prime: "\u2033", larr: "\u2190", uarr: "\u2191", rarr: "\u2192", darr: "\u2193", harr: "\u2194", minus: "\u2212", infin: "\u221E", ne: "\u2260", le: "\u2264", ge: "\u2265", asymp: "\u2248", equiv: "\u2261", check: "\u2713", cross: "\u2717", star: "\u2606", starf: "\u2605", hearts: "\u2665", alpha: "\u03B1", beta: "\u03B2", gamma: "\u03B3", delta: "\u03B4", pi: "\u03C0", Omega: "\u03A9", ldquo: "\u201C", rdquo: "\u201D", lsquo: "\u2018", rsquo: "\u2019" };
796
+ HTML_ENTITY_RE = /&(#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g;
723
797
  HTML_TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*(\/?)>/g;
724
798
  HTML_VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "source", "input", "wbr", "area", "col", "link", "meta"]);
725
799
  HTML_SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "noscript", "svg", "title", "textarea", "select", "video", "audio", "canvas", "template", "form", "button", "head"]);
@@ -2823,7 +2897,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeB
2823
2897
  if (e.target.open) setRmOpen(true);
2824
2898
  } },
2825
2899
  h("summary", null, lookup("readme.show")),
2826
- rmOpen ? h(ReadmeBlock, { pkg: it.npm }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
2900
+ rmOpen ? h(ReadmeBlock, { pkg: it.npm, repo: it.github }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
2827
2901
  ) : null,
2828
2902
  shots.length ? h(
2829
2903
  "div",
@@ -3337,11 +3411,11 @@ function ProgressLine() {
3337
3411
  st.currentPackage ? h("span", { className: "dshm-hint" }, String(st.currentPackage).slice(0, 44)) : null
3338
3412
  );
3339
3413
  }
3340
- function ReadmeBlock({ pkg }) {
3341
- const [state, setState] = useState({ loading: true, text: "", err: "", truncated: false });
3414
+ function ReadmeBlock({ pkg, repo }) {
3415
+ const [state, setState] = useState({ loading: true, text: "", err: "", truncated: false, repo: "" });
3342
3416
  useEffect(() => {
3343
3417
  let live = true;
3344
- api("readme", { pkg }).then((d) => live && setState({ loading: false, text: d.readme, err: "", truncated: d.truncated })).catch((e) => live && setState({ loading: false, text: "", err: String(e && e.message || e) }));
3418
+ api("readme", { pkg }).then((d) => live && setState({ loading: false, text: d.readme, err: "", truncated: d.truncated, repo: d.repo || "" })).catch((e) => live && setState({ loading: false, text: "", err: String(e && e.message || e) }));
3345
3419
  return () => {
3346
3420
  live = false;
3347
3421
  };
@@ -3352,7 +3426,7 @@ function ReadmeBlock({ pkg }) {
3352
3426
  return h(
3353
3427
  "div",
3354
3428
  { className: "dshm-readme md", onClick: (e) => e.stopPropagation() },
3355
- renderMarkdown(state.text),
3429
+ renderMarkdown(state.text, { repo: state.repo || repo }),
3356
3430
  state.truncated ? h("div", { className: "dshm-md-note" }, lookup("readme.truncated")) : null
3357
3431
  );
3358
3432
  }
@@ -3493,7 +3567,7 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
3493
3567
  }),
3494
3568
  open: openPkg === it.pkg,
3495
3569
  onToggle: () => setOpenPkg(openPkg === it.pkg ? null : it.pkg),
3496
- detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg }) : DetailRows([
3570
+ detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg, repo: vm.githubRepo }) : DetailRows([
3497
3571
  [lookup("detail.pkg"), it.pkg],
3498
3572
  [lookup("detail.spec"), it.spec],
3499
3573
  [lookup("detail.latest"), vm.latestLabelDetail],
@@ -449,7 +449,7 @@ export function createApiDispatcher(ctx) {
449
449
  catch (localErr) {
450
450
  try {
451
451
  const fallback = await d.npmPackumentReadme(target);
452
- result = { pkg: target, name: target, readme: fallback.readme, truncated: false };
452
+ result = { pkg: target, name: target, readme: fallback.readme, truncated: false, repo: fallback.repo || undefined };
453
453
  }
454
454
  catch {
455
455
  throw localErr;
@@ -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',数组必须显式排除)。 */
@@ -113,11 +113,28 @@ 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
+ * packument repository → "owner/repo"(仅 github.com;git+https / git@ / .git / #frag 归一;
118
+ * 非 GitHub 返回空串——README 相对路径锚定只对 GitHub raw/blob 有意义)。0.9.48。
119
+ */
120
+ export function extractGithubRepo(v) {
121
+ const url = typeof v === 'string'
122
+ ? v
123
+ : v && typeof v === 'object' && typeof v.url === 'string'
124
+ ? v.url
125
+ : '';
126
+ if (!url)
127
+ return '';
128
+ const m = /github\.com[/:]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(url);
129
+ return m ? `${m[1]}/${m[2]}` : '';
130
+ }
116
131
  /**
117
132
  * npm packument readme 兜底(0.9.45 U10b 修复):市场/收藏详情 Modal 的 README 折叠页对
118
133
  * 「未安装」条目无法走本地 profile 读取——回源 packument 顶层 readme 字段(publish 管线
119
134
  * 对多数包写入)。安全基线同 npmPackument:HTTPS、8MB 上限、超时;无 readme 字段返回空串
120
135
  * (不抛,客户端显示「没有 README」);L1(ADR-0012):显式 registry 参数 > 生效源。
136
+ * 0.9.48:随附 repo(packument repository 归一为 owner/repo,仅 GitHub),供 README
137
+ * 相对链接/图片锚定仓库基址;无 repository 或非 GitHub 时为空串。
121
138
  */
122
139
  export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, registry, deps) {
123
140
  if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
@@ -130,7 +147,7 @@ export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, regist
130
147
  maxBytes: 8 * 1024 * 1024,
131
148
  });
132
149
  const readme = typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
133
- return { readme };
150
+ return { readme, repo: extractGithubRepo(data?.repository) };
134
151
  }
135
152
  /**
136
153
  * 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.47",
3
+ "version": "0.9.49",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",