@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 +8 -2
- package/README.en.md +50 -31
- package/README.i18n.yaml +6 -0
- package/README.md +46 -27
- package/lib/client.js +23 -7
- package/lib/types/client/renderer.js +26 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.1.15(2026-09-30)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
[
|
|
3
|
+
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
23
|
-
- **Truly interactive** — the card runs in a `sandbox="allow-scripts"` iframe
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
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
|
-
|
|
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
|
|
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
|
|
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`;
|
|
41
|
-
- Self-contained content:
|
|
42
|
-
-
|
|
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.
|
|
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
|
|
62
|
+
## Known Limitations
|
|
50
63
|
|
|
51
|
-
- **Depends on official rendered DOM details** —
|
|
52
|
-
- **No network
|
|
53
|
-
- **
|
|
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
|
-
##
|
|
69
|
+
## How it works
|
|
56
70
|
|
|
57
71
|
<details>
|
|
58
|
-
<summary>
|
|
72
|
+
<summary>Internals (click to expand)</summary>
|
|
59
73
|
|
|
60
|
-
- `src/index.ts` (
|
|
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)
|
|
66
|
-
| `srcdoc.ts` | Wraps the card HTML into a
|
|
67
|
-
| `bridge.ts` | Host-side validation of iframe
|
|
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
|
-
**
|
|
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
|
-
**
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
98
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
package/README.i18n.yaml
ADDED
|
@@ -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
|
-
|
|
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
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
42
|
-
-
|
|
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
|
|
47
|
-
-
|
|
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
|
|
52
|
-
-
|
|
53
|
-
-
|
|
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`(
|
|
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 前导文本)
|
|
66
|
-
| `srcdoc.ts` | 把卡片 HTML 包成带严格 CSP + `dshBridge`
|
|
67
|
-
| `bridge.ts` |
|
|
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
|
-
|
|
84
|
+
**流式对齐**:回合运行时 `AssistantMarkdown` 在根上置 `data-streaming`,块内容是部分且会被重渲染;插件只在无 `data-streaming` 祖先时挂载,杜绝在半截内容上渲染。回合结束 React 重渲染、移除该属性并重建块,observer 此时才真正替换。
|
|
70
85
|
|
|
71
|
-
|
|
86
|
+
**链接路由**:`openLink` 每次打开时探测 `sidebarRightTabs`/`sidebarRight`——宿主注册了 `browser` 标签类型(ui-sidebar-browser)就开进右侧栏 Browser 标签,探测失败或 open 抛错都退化为 `window.open`(https、noopener);桥处理器强制仅 https 目标。
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
**给插件作者**:卡片协议是拉取式(skill),不推给每个会话;照 `` ```dsh-card `` 写自包含 HTML 即可。
|
|
74
89
|
|
|
75
90
|
</details>
|
|
76
91
|
|
|
77
|
-
##
|
|
92
|
+
## 开发
|
|
93
|
+
|
|
94
|
+
隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/inline-html-render`)。问题与贡献请移步该仓库。
|
|
95
|
+
|
|
96
|
+
## 变更记录
|
|
78
97
|
|
|
79
|
-
|
|
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,
|
|
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),
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* to the
|
|
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
|
-
|
|
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,
|
|
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),
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* to the
|
|
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
|
-
|
|
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
|
+
"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",
|