@khorsheed/dsh-inline-html-render 0.1.13 → 0.1.15

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
@@ -1,5 +1,11 @@
1
1
  # 变更记录
2
2
 
3
- ## 未发布
3
+ ## 0.1.15(2026-09-30)
4
4
 
5
- - 从 `@khorsheed/dsh-file-preview` 接管 `3d-artifact` 作者 skill;两个 bundled skill 现在都通过既有的延迟 skills 注入注册,provider 统一为 `inline-html-render`。
5
+ 无功能变更。加宽 `@deepseek-ai/dsh-*` peer 区间以覆盖宿主 0.2.0(0.2.0 的兼容闸会禁用 peer 区间不覆盖宿主版本的已装插件)。
6
+
7
+ ## 0.1.14(2026-09-27)
8
+
9
+ - **修复 rc.1 宿主上卡片不再渲染**:rc.1 的 `CodeToolbar` 对 shiki 不认得的 fence 语言一律显示宿主本地化的通用标签(「代码块」),`dsh-card` 信息串完全不进 DOM(无 `.infostring`、无前导文本、无 `language-*` 类)。检测新增内容签名兜底:完整 HTML 文档(doctype/`<html` 开头 + `</html>` 收尾)或含逐字严格 CSP meta 的块即判定为卡片;`.infostring` 与前导文本两臂原样保留(旧宿主与 0.1.5 线不变)。裸 HTML 片段在信息串不可见的宿主上不再识别为卡片(已知损失,上游 `data-lang` 提案见 docs/upstream-proposals)。
10
+ - 从 `@khorsheed/dsh-file-preview` 接管 `3d-artifact` 作者 skill;两个 bundled skill 现在都通过既有的延迟 skills 注入注册,provider 统一为 `inline-html-render`。
11
+ - README 兼容段的 DOM 锚点复核钉到 master 0.1.7-rc.2(锚点无变化,无行为变更)。
package/README.en.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # @khorsheed/dsh-inline-html-render
2
2
 
3
- [English](README.en.md) | 中文
3
+ English | [中文](README.md)
4
4
 
5
- A plugin that lets the agent render **interactive HTML cards** directly in the conversation. When the agent writes a ` ```dsh-card ```` fenced block, the plugin swaps that region in the middle of the message flow for a **sandboxed iframe** running the authored HTML — genuinely inline and interactive, not ASCII art or a prose description.
5
+ "Show me" stops being a code block — a `` ```dsh-card `` fence the agent writes turns into a live, clickable HTML card right in the conversation.
6
+
7
+ Without it, an agent that wants you to *see* a result can only paste code for you to imagine, or write an HTML file you have to open in a preview. This plugin swaps the fenced block whose info string is `dsh-card` for a sandboxed iframe — mid-message — that runs the authored HTML: inline between paragraphs and genuinely interactive, not ASCII art or a prose description.
8
+
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/inline-html-card-1.png" width="640" alt="a dsh-card block in the agent's reply rendered as a tabbed data-observation card, flowing with the surrounding text">
6
10
 
7
11
  ```dsh-card
8
12
  <div style="font:14px system-ui;padding:16px;background:#1e1e1e;border-radius:10px;color:#eee">
@@ -19,61 +23,76 @@ In the session, that is not a code block — it is a rendered card.
19
23
 
20
24
  ## Features
21
25
 
22
- - **Inline in the middle of the paragraph** — the card sits exactly where the agent wrote it, flowing naturally with the surrounding text.
23
- - **Truly interactive** — the card runs in a `sandbox="allow-scripts"` iframe and supports arbitrary DOM/CSS/JS interaction (hover, click, tab switching, inline SVG/canvas animation).
24
- - **Zero-invasive to official code** — it only mutates the DOM the official client has **already rendered** (inserts an iframe beside the rendered `.md-code-block` and hides that block); no official source is touched. Without this plugin the ` ```dsh-card ```` block simply degrades to a normal code block, never crashing.
25
- - **Strict sandbox** — a CSP is injected (`connect-src 'none'`), so the card's scripts cannot reach the network or the host. Only three controlled `window.dshBridge.openLink / copy / download` capabilities are exposed.
26
+ - **Inline between paragraphs** — the card sits exactly where the agent wrote it, flowing with the surrounding text; not an attachment, not a sidebar.
27
+ - **Truly interactive** — the card runs in a `sandbox="allow-scripts"` iframe with full DOM/CSS/JS: hover, click, tab switching, inline SVG/canvas animation all work.
28
+ - **Strict sandbox** — an opaque origin (never `allow-same-origin`) plus an injected strict CSP (`connect-src 'none'`, scripts/styles inline only): card scripts cannot touch host DOM or cookies and cannot reach the network. The only way out is `window.dshBridge` with three capabilities — `openLink` (https only), `copy`, `download` (data: URLs only) — each validated host-side for source window and argument shape.
29
+ - **Auto-height** — the opaque origin keeps the parent from reading content height, so the card document posts its real content height on load/resize and the parent sizes to it (capped at 20000px against runaway cards).
30
+ - **Links follow the host's route** — `openLink` opens into the right-Sidebar Browser tab on hosts shipping ui-sidebar-browser, and falls back to a new window (`noopener`) otherwise; probed on every open, so hot-added or hot-removed tab types never wedge.
31
+ - **Zero-invasive, degrades by design** — it only rewrites DOM the official client has **already rendered** (inserting an iframe beside the code block and hiding the block); no official source is touched. Without this plugin, `` ```dsh-card `` is just a normal code block — nothing breaks.
32
+ - **Two bundled authoring skills** — the host half registers `inline-html-card` and `3d-artifact` (both with provider `inline-html-render`), discovered pull-style: the agent picks up the protocol when it needs to *draw*, instead of every session carrying it.
26
33
 
27
34
  ## Install
28
35
 
29
- Not in the default web bundle; install and mount with one command (self-mounting via `dsh.bundle`):
36
+ ```sh
37
+ dsh plugin --profile web add @khorsheed/dsh-inline-html-render
38
+ ```
39
+
40
+ Restart the web instance to activate; uninstall restores the previous composition exactly.
30
41
 
31
42
  ```sh
32
- dsh plugin --profile <p> add @khorsheed/dsh-inline-html-render # install
33
- dsh plugin --profile <p> remove @khorsheed/dsh-inline-html-render # uninstall
43
+ dsh plugin --profile web remove @khorsheed/dsh-inline-html-render
34
44
  ```
35
45
 
36
46
  ## Authoring for the agent
37
47
 
38
- The host half registers the two HTML-authoring protocols, `inline-html-card` and `3d-artifact` (both with provider `inline-html-render`). Whenever the user wants a **visible result** ("show me", "preview", "make me a card", "compare these visual options", a clickable widget) rather than a paragraph of text, the agent picks up this protocol. Core constraints (see the skills):
48
+ The two skills the host half registers, `inline-html-card` and `3d-artifact`, share one HTML-authoring protocol. Whenever the user wants a **visible result** ("show me", "preview", "make me a card", "compare these visual options", a clickable widget), the agent picks up this protocol. Core constraints (the full protocol lives in the bundled skills):
39
49
 
40
- - Exactly one fenced block whose info string is exactly `dsh-card`; use **three** backticks ( ``` ), never four — four backticks or a nested fence make the parser read it as an outer block and the card never renders.
41
- - Self-contained content: inline CSS/JS, `data:` URIs for images, zero external network.
42
- - Never reach the host — use `window.dshBridge` for opening links / copying / downloading.
50
+ - Exactly one fenced block whose info string is exactly `dsh-card`; **three backticks, never four, never nested** — a nested fence is parsed as an outer code block and the card never renders.
51
+ - Self-contained content: all CSS/JS inline, images as `data:` URIs, zero runtime network.
52
+ - The content IS the card: render the thing itself, not a fake window/title-bar container around it.
53
+ - Never reach the host — open links, copy, and download through `window.dshBridge`.
54
+
55
+ `3d-artifact` targets single-file 3D / digital-twin HTML (three.js via whitelisted-CDN importmaps, GLB models inlined as base64, a self-embedded Tier1 CSP); the renderer respects a card's own CSP and never injects a duplicate.
43
56
 
44
57
  ## Compatibility
45
58
 
46
- - npm release line (`@deepseek-ai/dsh@0.1.2-rc.1`): ✅ full — this plugin consumes only standard DOM + the client runtime `ClientContext`; the `.md-code-block` wrapper, its banner info-string display, and the `data-streaming` attribute that `ui-conversation` renders are unchanged on 0.1.2-rc.1; minHost moves up to 0.1.2-rc.1 — older hosts stay on the previous release line.
47
- - source line (deepseek-harness master): ✅ (verifiedHost: 0.1.2-rc.1)
59
+ - npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): ✅ full — this plugin consumes only standard DOM plus the client-runtime `ClientContext`, with no host-service dependency; minHost is 0.1.2-rc.1 — older hosts stay on the previous release line.
60
+ - source line (deepseek-harness master): ✅ (verifiedHost: 0.1.2-rc.1) — the DOM anchors re-checked on master (0.1.7-rc.2) are unchanged: `CodeBlock` still renders the `.md-code-block` wrapper with the info string in its banner (a CSS-module hashed class, caught by the leading-text fallback), `AssistantMarkdown` still sets `data-streaming`, and `ui-sidebar-browser` still registers the `browser` right-Sidebar tab kind.
48
61
 
49
- ## Known limitations
62
+ ## Known Limitations
50
63
 
51
- - **Depends on official rendered DOM details** — the renderer locates blocks via the `.md-code-block` wrapper and the leading `dsh-card` text in its banner (this release's official CodeBlock renders a hashed banner wrap with no stable `.infostring` class; also compatible with an explicit `.infostring` element), and aligns streaming completion via `AssistantMarkdown`'s `data-streaming`. This is the repo-accepted last-resort DOM-anchor pattern (same shape as message-tools' dom-hider). If the official client changes the wrapper class, the banner text shape, or the info-string placement, the MutationObserver fallback path must be re-verified.
52
- - **No network in the sandbox** — card scripts cannot `fetch`/load external resources (even whitelisted CDNs are not allowed; stricter than file-preview's Tier1). For network-bearing visuals use file-preview's HTML render path instead.
53
- - **Auto-height iframe** — the iframe is sized to its content height; rare dynamically-growing layouts may need a manual `data-dsh-card` re-mount.
64
+ - **Depends on official rendered DOM details** — blocks are located via the `.md-code-block` wrapper, and recognition runs in three arms: the explicit `.infostring` element (older shape) → the banner's leading text (0.1.6 shape) → the **content signature** (0.1.7-rc.1+: `CodeToolbar` shows only the host's localized generic label for fence languages shiki cannot highlight, so `dsh-card` never reaches the DOM; there a complete HTML document — doctype/`<html` head, `</html>` tail — or a block carrying the verbatim strict-CSP meta counts as a card). Streaming completion is aligned via `AssistantMarkdown`'s `data-streaming`. This is the repo-accepted last-resort DOM-anchor pattern (same shape as message-tools' dom-hider). Known gap: on hosts where the info string is invisible, **bare HTML fragments** (not a complete document, no CSP meta) are no longer recognized as cards — author cards as complete documents; once the host ships a `data-lang` hook (proposal in docs/upstream-proposals) recognition falls back to the pure info string.
65
+ - **No network under the default CSP** — the injected default CSP allows no network at all, not even whitelisted CDNs (stricter than file-preview's Tier1); a card document's own embedded CSP wins (`3d-artifact` embeds the Tier1 CSP that whitelists two CDNs for `script-src`). For other network-bearing visuals use file-preview's HTML render path instead.
66
+ - **Card interaction state does not survive re-renders** — React rebuilds the code-block node on content change; the old iframe is collected and re-mounted, so transient state inside a card (inputs, scroll position) is lost.
67
+ - **Auto-height has edges** — height comes from the card's own report, capped at 20000px; rare continuously-growing layouts may need a manual re-scan to settle.
54
68
 
55
- ## Implementation
69
+ ## How it works
56
70
 
57
71
  <details>
58
- <summary>Internal structure (expand)</summary>
72
+ <summary>Internals (click to expand)</summary>
59
73
 
60
- - `src/index.ts` (node half) — registers the `inline-html-card` and `3d-artifact` skills (`skills` service optional; stays pending when absent).
61
- - `src/client/` (browser half, `/plugins/inline-html-render/client.js`):
74
+ **Architecture.** The browser half is a pure DOM-layer effect: it owns no slot and registers no Remote, mutating only the DOM the official client already rendered — the same repo-accepted last-resort pattern as message-tools' dom-hider — so it composes as an independent package with no core edits. The host half (`src/index.ts`) does exactly one thing: once the skill registry is ready (awaited via `ctx.inject(['skills'], …)` rather than probed synchronously at apply time), it registers the two bundled content-only skills (a missing/malformed SKILL.md warns, never throws; a composition without the `skills` service stays pending, never fails boot). Identity triangle: the cordis.patch.yml row id `inline-html-render` = the package.json name = `PACKAGE_NAME` in `src/invariant.ts`; the invariant companion holds no runtime invariant and only marks the package alive for the loader.
62
75
 
63
76
  | Module | Responsibility |
64
77
  | --- | --- |
65
- | `renderer.ts` | A `MutationObserver` watches the DOM; for each `.md-code-block` whose info string is `dsh-card` (an explicit `.infostring` or the banner's leading text), reads the `<pre>` text (the raw HTML), inserts an iframe beside the block, hides the block; never mounts while streaming (`data-streaming` present), re-scans once it settles. |
66
- | `srcdoc.ts` | Wraps the card HTML into a full document carrying the strict CSP + `dshBridge` (`srcdoc`). |
67
- | `bridge.ts` | Host-side validation of iframe messages (source window + whitelist + arg shape), answering `openLink/copy/download`. |
78
+ | `src/client/renderer.ts` | A `MutationObserver` watches the DOM; for each `.md-code-block` whose info string is `dsh-card` (an explicit `.infostring` or the banner's leading text) it reads the `<pre>` text (the raw HTML), inserts an iframe beside the block, and hides the block; never mounts while streaming (a `data-streaming` ancestor exists), re-scans once settled; collects orphaned iframes and re-mounts when React rebuilds a block. |
79
+ | `src/client/srcdoc.ts` | Wraps the card HTML into a complete srcdoc document carrying the strict CSP plus `dshBridge`: fragments get `<html>/<head>` synthesized, full documents get the injection right after their `<head>`; a card that already declares a CSP is not given a duplicate. The sandbox never carries `allow-same-origin`. |
80
+ | `src/client/bridge.ts` | Host-side validation of every iframe message (`event.source` must be the controlled frame's window, an `fn` whitelist, per-function argument shapes), answering `openLink/copy/download` and height reports; unknown or malformed calls get an error reply, never a host exception. |
81
+
82
+ **Why a sibling instead of replacing the node**: React owns the markdown tree, so a bare `removeChild` would make React's reconciliation fight the DOM (potentially throwing or resurrecting the block). The plugin never removes a React node — the iframe is inserted as a sibling right after the block, the block is hidden with `style.display='none'` (a style React does not manage), and a MutationObserver re-scans on every change; when React replaces a node, the new block is caught and hidden again.
68
83
 
69
- **Why a sibling instead of replacing the node**: React owns the markdown tree, so a bare `removeChild` would make React's reconciliation fight the DOM (potentially throwing or resurrecting the block). So this plugin never removes a React node — it inserts the iframe as a sibling **after** the block, hides the block with `style.display='none'` (a style React does not manage), and re-scans with a MutationObserver on every change. When React replaces a node (rebuilding it on content change), the new block is caught and hidden again.
84
+ **Streaming alignment**: while a turn runs, `AssistantMarkdown` sets `data-streaming` on its root and the block content is partial and re-rendered; the plugin only mounts when no `data-streaming` ancestor exists, so a card never renders on a half-streamed body. When the turn ends, React re-renders, drops the attribute, and rebuilds the block — the observer swaps then.
70
85
 
71
- **Streaming alignment**: while a turn runs, `AssistantMarkdown` sets `data-streaming` on its root and the block content is partial and re-rendered. The plugin only mounts when there is no `[data-streaming]` ancestor, so a card never renders on a half-streamed body. When the turn ends, React re-renders, removes `data-streaming`, and rebuilds the block; the observer swaps then.
86
+ **Link routing**: `openLink` probes `sidebarRightTabs`/`sidebarRight` on every open — when the host registers the `browser` tab kind (ui-sidebar-browser) the link opens as a right-Sidebar Browser tab; a failed probe or a throwing open falls back to `window.open` (https, `noopener`). The bridge handler enforces https-only targets.
72
87
 
73
- **For plugin authors** — the card protocol is pull-based (a skill), not pushed to every session. Just write self-contained HTML inside ` ```dsh-card ````.
88
+ **For plugin authors**: the card protocol is pull-based (a skill), not pushed to every session — just write self-contained HTML inside a `` ```dsh-card `` fence.
74
89
 
75
90
  </details>
76
91
 
77
- ## License
92
+ ## Development
93
+
94
+ Part of the [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo (`packages/inline-html-render`). Issues and contributions welcome there.
95
+
96
+ ## Changelog
78
97
 
79
- MIT
98
+ See [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record: the git blob hash of each side as of the
2
+ # last confirmed-consistent state. Both languages carry equal authority; after
3
+ # editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/inline-html-render/README.en.md
5
+ packages/inline-html-render/README.en.md: 17e993d9748623d0e654efaf7943d35c7e344c3c
6
+ packages/inline-html-render/README.md: 7dc3c908363ccbd6b9eeea16b2abeb2379ef89d2
package/README.md CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  [English](README.en.md) | 中文
4
4
 
5
- 让 agent 直接在对话正文里渲染**可交互的 HTML 卡片**的插件:agent 写一个 ` ```dsh-card ```` fenced block,插件把这一段在消息流中间替换成**沙箱 iframe** 运行其 HTML——真正的内联、可交互,而不是 ASCII 图或一句描述。
5
+ 把「做出来给你看」变成字面意思——agent 写的 `` ```dsh-card `` 代码块,在对话正文里原地变成一张能点、能动的 HTML 卡片。
6
+
7
+ 没有它的时候,agent 想让你**看到**一个效果,只能贴段代码让你脑补,或者写个 HTML 文件等你去预览里打开。这个插件把 info string 为 `dsh-card` 的 fenced block 在消息流中间替换成一个沙箱 iframe,直接运行其中的 HTML——内联在段落之间、真正可交互,而不是 ASCII 图或一句描述。
8
+
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/inline-html-card-1.png" width="640" alt="agent 回复中间的 dsh-card 代码块被渲染成一张带 tab 的数据观测卡片,与上下文文字自然衔接">
6
10
 
7
11
  ```dsh-card
8
12
  <div style="font:14px system-ui;padding:16px;background:#1e1e1e;border-radius:10px;color:#eee">
@@ -19,61 +23,76 @@
19
23
 
20
24
  ## 特性
21
25
 
22
- - **内联在段落中间**——卡片就长在 agent 写它的位置,与文字自然衔接。
23
- - **真正可交互**——卡片在 `sandbox="allow-scripts"` 的 iframe 里,支持任意 DOM/CSS/JS 交互(hover、点击、tab 切换、内联 SVG/canvas 动图)。
24
- - **零侵入官方**——只改写官方**已经渲染出来的 DOM**(在渲染后的 `.md-code-block` 旁插入 iframe 并隐藏原块),不改一行官方源码。没有本插件时,` ```dsh-card ```` 退化成普通代码块,零崩溃。
25
- - **严格沙箱**——注入 CSP(`connect-src 'none'`),卡片脚本无法联网、无法访问宿主。仅开放 `window.dshBridge.openLink / copy / download` 三个受控能力。
26
+ - **内联在段落中间**——卡片就长在 agent 写它的位置,与上下文文字自然衔接;不是附件,也不占侧栏。
27
+ - **真正可交互**——卡片跑在 `sandbox="allow-scripts"` 的 iframe 里,完整 DOM/CSS/JS 可用:hover、点击、tab 切换、内联 SVG/canvas 动图都行。
28
+ - **严格沙箱**——不透明源(永不带 `allow-same-origin`)加注入式严格 CSP(`connect-src 'none'`,脚本/样式仅内联):卡片脚本碰不到宿主 DOM 与 Cookie,也联不了网。对外只开放 `window.dshBridge` 的 `openLink`(仅 https)/`copy`/`download`(仅 data: URL)三个能力,宿主侧逐条校验来源窗口与参数形状。
29
+ - **高度自适应**——不透明源让外层读不到内容高度,卡片文档在 load/resize 时把真实内容高度 postMessage 出来,外层据此撑开(封顶 20000px,防失控卡片)。
30
+ - **链接走宿主路由**——`openLink` 在带 ui-sidebar-browser 的宿主上开进右侧栏 Browser 标签页,否则退化为新窗口(`noopener`);每次打开时现探测,标签类型热增热删都不卡死。
31
+ - **零侵入、天然降级**——只改写官方**已经渲染出来的** DOM(在代码块旁插入 iframe 并隐藏原块),不改一行官方源码;没装本插件时,`` ```dsh-card `` 就是一个普通代码块,什么也不坏。
32
+ - **随包两个作者 skill**——host 半边注册 `inline-html-card` 与 `3d-artifact`(provider 均为 `inline-html-render`),拉取式发现:agent 需要「画出来」时按协议写作,不往每个会话里塞协议。
26
33
 
27
34
  ## 安装
28
35
 
29
- 不在默认 web bundle 中;一条命令完成安装并挂载(通过 `dsh.bundle` 自挂载):
36
+ ```sh
37
+ dsh plugin --profile web add @khorsheed/dsh-inline-html-render
38
+ ```
39
+
40
+ 重启 web 实例后生效;卸载即精确还原之前的组合。
30
41
 
31
42
  ```sh
32
- dsh plugin --profile <p> add @khorsheed/dsh-inline-html-render # 安装
33
- dsh plugin --profile <p> remove @khorsheed/dsh-inline-html-render # 卸载
43
+ dsh plugin --profile web remove @khorsheed/dsh-inline-html-render
34
44
  ```
35
45
 
36
46
  ## 给 agent 的写法
37
47
 
38
- host 半边注册了同属 HTML 作者协议的 `inline-html-card` 与 `3d-artifact` 两个 skill(provider 均为 `inline-html-render`)。只要用户想要**看得见的结果**(比如 "show me / 预览 / 做个卡片看看 / 对比几个视觉效果 / 想要个能点击的小组件",而不是读一段文字),agent 就会套用这个协议。核心约束(详见 skill):
48
+ host 半边注册的 `inline-html-card` 与 `3d-artifact` 两个 skill 同属一套 HTML 作者协议。只要用户想要**看得见的结果**(“show me / 预览 / 做个卡片看看 / 对比几个视觉效果 / 想要个能点的小组件”),agent 就会套用这个协议。核心约束(完整协议见随包 skill):
39
49
 
40
- - 恰好一个 fenced block,info string 严格为 `dsh-card`;**用三个反引号 ```,不要用四个反引号嵌套**(嵌套会被解析成外层代码块,卡片不渲染)。
41
- - 内容自包含:CSS/JS 内联,图片用 `data:` URI,零外部网络。
42
- - 不要访问宿主——用 `window.dshBridge` 做外链/复制/下载。
50
+ - 恰好一个 fenced block,info string 严格为 `dsh-card`;**用三个反引号,不要四个、不要嵌套**——嵌套会被解析成外层代码块,卡片不渲染。
51
+ - 内容自包含:CSS/JS 全部内联,图片用 `data:` URI,零运行时网络。
52
+ - 内容即卡片:直接渲染内容本身,别再包一层假窗口/标题栏容器。
53
+ - 不访问宿主——外链、复制、下载走 `window.dshBridge`。
54
+
55
+ `3d-artifact` 面向单文件 3D / 数字孪生 HTML(three.js importmap 走白名单 CDN、GLB 模型 base64 内联、自带 Tier1 CSP);渲染器尊重卡片自带的 CSP,不重复注入。
43
56
 
44
57
  ## Compatibility
45
58
 
46
- - npm 发布线(`@deepseek-ai/dsh@0.1.2-rc.1`):✅ 完整——本插件只消费标准 DOM + 客户端运行时 `ClientContext`,`ui-conversation` 渲染出的 `.md-code-block` / banner infostring 显示位 / `data-streaming` 结构在 0.1.2-rc.1 上无变化;minHost 前移至 0.1.2-rc.1,旧宿主请停留在旧发布线。
47
- - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.2-rc.1)
59
+ - npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 完整——本插件只消费标准 DOM 与客户端运行时 `ClientContext`,不依赖任何 host 服务;minHost 0.1.2-rc.1,0.1.2 之前的宿主请停留在旧发布线。
60
+ - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.2-rc.1)——所依赖的 DOM 锚点在 master(0.1.7-rc.2)复核无变化:`CodeBlock` 仍渲染 `.md-code-block` 容器且 banner 带 info string(CSS-module 哈希类名,由前导文本回退路径识别),`AssistantMarkdown` 仍置 `data-streaming`,`ui-sidebar-browser` 仍注册 `browser` 右侧栏标签类型。
48
61
 
49
62
  ## 已知限制
50
63
 
51
- - **依赖官方渲染 DOM 细节**——渲染器靠 `.md-code-block` 包裹及其 banner 里 info string 的**前导文本** `dsh-card`(本版本官方 CodeBlock 用 hash banner 包裹,没有稳定的 `.infostring` 类;also 兼容显式 `.infostring` 的元素)和 `AssistantMarkdown` 的 `data-streaming` 定位并对齐流式结束。这是仓库接受的 last-resort DOM-anchor 模式(与 message-tools 的 dom-hider 同型)。官方若改动 wrapper 类名、banner 文本形状或 info string 位置,需要用 MutationObserver 的降级路径复核。
52
- - **沙箱无网络**——卡片脚本不能 fetch/外链资源(白名单 CDN 也不放行,比 file-preview 的 Tier1 更严)。需要联网的画面请用 file-preview 的 HTML 渲染链路,而非本插件。
53
- - **iframe 高度自适应**——卡片渲染后按其内容高度自动撑开;极少数动态撑高的场景可能需手动触发 `data-dsh-card` 重新挂载。
64
+ - **依赖官方渲染 DOM 细节**——定位靠 `.md-code-block` 容器,识别走三层:显式 `.infostring` 元素(旧形状)→ banner 前导文本(0.1.6 形状)→ **内容签名**(0.1.7-rc.1 起:`CodeToolbar` 对 shiki 不认得的 fence 语言只显示本地化通用标签,`dsh-card` 信息串不进 DOM;此时完整 HTML 文档——doctype/`<html` 开头、`</html>` 收尾——或含逐字严格 CSP meta 的块即判为卡片)。并用 `AssistantMarkdown` 的 `data-streaming` 对齐流式结束。这是仓库接受的 last-resort DOM-anchor 模式(与 message-tools 的 dom-hider 同型)。已知缺口:信息串不可见的宿主上,**裸 HTML 片段**(非完整文档、无 CSP meta)不再识别为卡片——请把卡片写成完整文档;官方若提供 `data-lang` 钩子(提案见 docs/upstream-proposals)即可退回纯信息串识别。
65
+ - **默认 CSP 无网络**——渲染器注入的默认 CSP 连白名单 CDN 也不放行(比 file-preview 的 Tier1 更严);卡片文档自带 CSP 时以自带为准(`3d-artifact` 协议即内嵌放行两个 CDN `script-src` 的 Tier1 CSP)。需要联网画面的其它场景请走 file-preview 的 HTML 渲染链路,而非本插件。
66
+ - **卡片交互状态不跨重渲染**——React 内容变化会重建代码块节点,旧 iframe 随之回收重挂,卡片里的临时状态(输入、滚动位置)不保留。
67
+ - **高度自适应有边界**——高度来自卡片内部上报,封顶 20000px;极少数持续动态撑高的布局可能需要手动触发重扫。
54
68
 
55
69
  ## 实现原理
56
70
 
57
71
  <details>
58
72
  <summary>内部结构(点击展开)</summary>
59
73
 
60
- - `src/index.ts`(node 半边)——注册 `inline-html-card` 与 `3d-artifact` skill(`skills` 服务可选,缺失时保持 pending)。
61
- - `src/client/`(浏览器半边,`/plugins/inline-html-render/client.js`):
74
+ **架构。** 浏览器半部分是纯 DOM 层效果:不拥有 slot、不注册 Remote,只改写官方已渲染的 DOM——与 message-tools 的 dom-hider 同属仓库接受的 last-resort 模式,因此可以作为独立包组合,不改任何核心包。host 半部分(`src/index.ts`)只做一件事:经 `ctx.inject(['skills'], …)` 等 skill 注册表就绪后,注册随包的两个内容型 skill(SKILL.md 缺失/畸形只 warn 不 throw;组合里没有 `skills` 服务时保持 pending,绝不炸 boot)。身份三角:cordis.patch.yml 行 id `inline-html-render` = package.json name = `src/invariant.ts` 的 PACKAGE_NAME;invariant 伴生无运行时不变量,仅向 loader 登记存活。
62
75
 
63
76
  | 模块 | 职责 |
64
77
  | --- | --- |
65
- | `renderer.ts` | `MutationObserver` 监控 DOM;对每个 info string 为 `dsh-card` 的 `.md-code-block`(显式 `.infostring` 或 banner 前导文本),读取 `<pre>` 文本(原始 HTML),在块旁插入 iframe、隐藏原块;流式未结束(`data-streaming` 存在)时不挂载,结束后重扫。 |
66
- | `srcdoc.ts` | 把卡片 HTML 包成带严格 CSP + `dshBridge` 的完整文档(`srcdoc`)。 |
67
- | `bridge.ts` | 宿主侧校验 iframe 消息(来源窗口 + 白名单 + 参数形状),响应 `openLink/copy/download`。 |
78
+ | `src/client/renderer.ts` | `MutationObserver` 监控 DOM;对每个 info string 为 `dsh-card` 的 `.md-code-block`(显式 `.infostring` 或 banner 前导文本)读取 `<pre>` 文本(原始 HTML),在块旁插入 iframe、隐藏原块;流式未结束(`data-streaming` 祖先存在)时不挂载,落定后重扫;React 重建块后回收孤儿 iframe 并重挂。 |
79
+ | `src/client/srcdoc.ts` | 把卡片 HTML 包成带严格 CSP + `dshBridge` 的完整 srcdoc 文档:片段自动补 `<html>/<head>`,完整文档在其 `<head>` 后注入;卡片已自带 CSP 时不重复注入。sandbox 永不带 `allow-same-origin`。 |
80
+ | `src/client/bridge.ts` | 宿主侧逐条校验 iframe 消息(`event.source` 必须是受控 iframe 的 window、fn 白名单、按函数校验参数形状),响应 `openLink/copy/download` 与高度上报;未知或畸形调用回错误,绝不在宿主抛异常。 |
81
+
82
+ **为什么插兄弟节点而不是替换**:React 拥有 markdown 树,直接 `removeChild` 会让 React 的 reconciliation 与 DOM 打架(可能抛错或让原块复活)。所以本插件**从不删 React 的节点**——iframe 作为兄弟节点插在块之后,再把块 `style.display='none'`(React 不管的样式),并用 MutationObserver 在每次变更后重扫;React 替换节点时新块同样被捕获、再次隐藏。
68
83
 
69
- **为什么插 sibling 而不是替换节点**:React 拥有 markdown 树,直接 `removeChild` 会让 React 的 reconciliation 与 DOM 打架(可能抛错或让原块复活)。所以本插件**从不删 React 的节点**——把 iframe 作为兄弟节点插入该块之后,再把块 `style.display='none'`(一个 React 不管的样式),并用 MutationObserver 在每次变更后重扫。React 替换节点时(内容变化重建节点),新的块同样被捕获、再次隐藏。
84
+ **流式对齐**:回合运行时 `AssistantMarkdown` 在根上置 `data-streaming`,块内容是部分且会被重渲染;插件只在无 `data-streaming` 祖先时挂载,杜绝在半截内容上渲染。回合结束 React 重渲染、移除该属性并重建块,observer 此时才真正替换。
70
85
 
71
- **流式对齐**:回合运行时,`AssistantMarkdown` 在根上设 `data-streaming`,块内容是部分且会被重渲染。插件只在无 `data-streaming` 祖先时挂载,杜绝在半截内容上渲染。回合结束 React 重渲染时会移除 `data-streaming` 并重建块,observer 此时才真正替换。
86
+ **链接路由**:`openLink` 每次打开时探测 `sidebarRightTabs`/`sidebarRight`——宿主注册了 `browser` 标签类型(ui-sidebar-browser)就开进右侧栏 Browser 标签,探测失败或 open 抛错都退化为 `window.open`(https、noopener);桥处理器强制仅 https 目标。
72
87
 
73
- **给插件作者**——卡片协议是拉取式(skill),不推给每个会话。你只需照 ` ```dsh-card ```` 写自包含 HTML。
88
+ **给插件作者**:卡片协议是拉取式(skill),不推给每个会话;照 `` ```dsh-card `` 写自包含 HTML 即可。
74
89
 
75
90
  </details>
76
91
 
77
- ## License
92
+ ## 开发
93
+
94
+ 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/inline-html-render`)。问题与贡献请移步该仓库。
95
+
96
+ ## 变更记录
78
97
 
79
- MIT
98
+ 见 [CHANGELOG.md](CHANGELOG.md)。
package/lib/client.js CHANGED
@@ -222,22 +222,38 @@ window.__ModuleLoader__.load({
222
222
  const FRAME_CLASS = "dsh-inline-card-frame";
223
223
  /** Attribute used to find the bridge-ready frame and its disposer. */
224
224
  const BRIDGE_ATTR = "data-dsh-bridge";
225
+ /**
226
+ * The card document signature for hosts that hide the fence info string.
227
+ * The card protocol (skills/inline-html-card) authors a card as a COMPLETE
228
+ * HTML document, so a settled block whose decoded body is one — doctype or
229
+ * `<html` at the head, `</html>` at the tail — is a card even when the banner
230
+ * shows only the host's generic label. The verbatim strict-CSP meta (the
231
+ * skill's recommended self-describing header) is accepted on its own: no
232
+ * ordinary code listing carries that exact string.
233
+ */
234
+ function isCardDocument(body) {
235
+ if (body.includes("default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; worker-src blob:; object-src 'none'; base-uri 'none'; form-action 'none'")) return true;
236
+ const complete = /<\/html>\s*$/i.test(body);
237
+ return (/^\s*<!doctype html[\s>]/i.test(body) || /^\s*<html[\s>]/i.test(body)) && complete;
238
+ }
225
239
  /** True when one `.md-code-block` was authored with the `dsh-card` info string.
226
240
  *
227
241
  * The official `CodeBlock` renders the fence info string in the banner, but the
228
242
  * DOM shape is not a stable contract: some builds render a dedicated
229
- * `.infostring` element, while this release flattens the banner wrap
243
+ * `.infostring` element, one release flattened the banner wrap
230
244
  * (`_bannerWrap_…`) whose leading text is `<info>复制` (info string first, then
231
- * the localized copy label), with no `.infostring`. So we match on the block's
232
- * leading text: the info string is always rendered before the code body, and a
233
- * normal code block cannot begin with `dsh-card` unless it was authored that
234
- * way. Prefer the explicit `.infostring` element when present, then fall back
235
- * to the text prefix.
245
+ * the localized copy label), and the 0.1.7-rc.1 `CodeToolbar` shows only the
246
+ * host's localized generic label for any language shiki cannot highlight —
247
+ * `dsh-card` never reaches the DOM there at all. So: prefer the explicit
248
+ * `.infostring` element, then the leading-text prefix, and finally fall back
249
+ * to the content signature ({@link isCardDocument}) — the protocol's complete
250
+ * HTML document with its strict CSP is unambiguous regardless of the banner.
236
251
  */
237
252
  function isCardInfoBlock(block) {
238
253
  const explicit = block.querySelector(".infostring");
239
254
  if (explicit !== null) return explicit.textContent?.trim() === CARD_INFO_STRING;
240
- return block.textContent.trimStart().startsWith(CARD_INFO_STRING);
255
+ if (block.textContent.trimStart().startsWith("dsh-card")) return true;
256
+ return isCardDocument(block.querySelector("pre")?.textContent ?? "");
241
257
  }
242
258
  /** Find every rendered `dsh-card` block under `root`. */
243
259
  function findCardBlocks(root) {
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * @module @khorsheed/dsh-inline-html-render
23
23
  */
24
- import { buildCardSrcDoc } from './srcdoc.js';
24
+ import { buildCardSrcDoc, CARD_CSP } from './srcdoc.js';
25
25
  import { attachBridge } from './bridge.js';
26
26
  /** Info string that marks a fence as an inline card. */
27
27
  export const CARD_INFO_STRING = 'dsh-card';
@@ -31,24 +31,42 @@ const PROCESSED_ATTR = 'data-dsh-card-processed';
31
31
  const FRAME_CLASS = 'dsh-inline-card-frame';
32
32
  /** Attribute used to find the bridge-ready frame and its disposer. */
33
33
  const BRIDGE_ATTR = 'data-dsh-bridge';
34
+ /**
35
+ * The card document signature for hosts that hide the fence info string.
36
+ * The card protocol (skills/inline-html-card) authors a card as a COMPLETE
37
+ * HTML document, so a settled block whose decoded body is one — doctype or
38
+ * `<html` at the head, `</html>` at the tail — is a card even when the banner
39
+ * shows only the host's generic label. The verbatim strict-CSP meta (the
40
+ * skill's recommended self-describing header) is accepted on its own: no
41
+ * ordinary code listing carries that exact string.
42
+ */
43
+ function isCardDocument(body) {
44
+ if (body.includes(CARD_CSP))
45
+ return true;
46
+ const complete = /<\/html>\s*$/i.test(body);
47
+ return (/^\s*<!doctype html[\s>]/i.test(body) || /^\s*<html[\s>]/i.test(body)) && complete;
48
+ }
34
49
  /** True when one `.md-code-block` was authored with the `dsh-card` info string.
35
50
  *
36
51
  * The official `CodeBlock` renders the fence info string in the banner, but the
37
52
  * DOM shape is not a stable contract: some builds render a dedicated
38
- * `.infostring` element, while this release flattens the banner wrap
53
+ * `.infostring` element, one release flattened the banner wrap
39
54
  * (`_bannerWrap_…`) whose leading text is `<info>复制` (info string first, then
40
- * the localized copy label), with no `.infostring`. So we match on the block's
41
- * leading text: the info string is always rendered before the code body, and a
42
- * normal code block cannot begin with `dsh-card` unless it was authored that
43
- * way. Prefer the explicit `.infostring` element when present, then fall back
44
- * to the text prefix.
55
+ * the localized copy label), and the 0.1.7-rc.1 `CodeToolbar` shows only the
56
+ * host's localized generic label for any language shiki cannot highlight —
57
+ * `dsh-card` never reaches the DOM there at all. So: prefer the explicit
58
+ * `.infostring` element, then the leading-text prefix, and finally fall back
59
+ * to the content signature ({@link isCardDocument}) — the protocol's complete
60
+ * HTML document with its strict CSP is unambiguous regardless of the banner.
45
61
  */
46
62
  function isCardInfoBlock(block) {
47
63
  const explicit = block.querySelector('.infostring');
48
64
  if (explicit !== null)
49
65
  return explicit.textContent?.trim() === CARD_INFO_STRING;
50
66
  // `textContent` begins with the info string baked before the copy label.
51
- return block.textContent.trimStart().startsWith(CARD_INFO_STRING);
67
+ if (block.textContent.trimStart().startsWith(CARD_INFO_STRING))
68
+ return true;
69
+ return isCardDocument(block.querySelector('pre')?.textContent ?? '');
52
70
  }
53
71
  /** Find every rendered `dsh-card` block under `root`. */
54
72
  export function findCardBlocks(root) {
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "card",
11
11
  "sandbox"
12
12
  ],
13
- "version": "0.1.13",
13
+ "version": "0.1.15",
14
14
  "type": "module",
15
15
  "main": "lib/index.js",
16
16
  "types": "lib/types/index.d.ts",
@@ -47,7 +47,7 @@
47
47
  "license": "MIT",
48
48
  "peerDependencies": {
49
49
  "@deepseek-ai/cordis": "^4.0.1",
50
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6"
50
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6 || ^0.2.0-rc.1"
51
51
  },
52
52
  "files": [
53
53
  "lib",