206 lines
9.0 KiB
Markdown
206 lines
9.0 KiB
Markdown
# Ygent 富内容渲染设计(HTML 制品 / 流程图 / 公式)
|
||
|
||
> 状态:设计提案,待确认
|
||
> 背景:`docs/reference/workbuddy/chat-tool-fold.md` 已摸清 WorkBuddy 的实现与安全边界
|
||
> 本文只讨论「对话区要支持哪些内容类型、怎么安全地渲染、分几步做」
|
||
|
||
---
|
||
|
||
## 1. 现状盘点
|
||
|
||
| 内容类型 | WorkBuddy | Ygent 现状 | 差距 |
|
||
|---|---|---|---|
|
||
| 段落 / 列表 / 引用 | ✓ | ✓(remark-gfm) | — |
|
||
| 表格 | ✓ | ✓ | — |
|
||
| 代码块高亮 | ✓ Prism | ✓ highlight.js | 可接受 |
|
||
| 代码 diff(改文件时) | ✓ `tool-diff` | ✗ | 见 §7 |
|
||
| 图片 | ✓ | ✓(react-markdown 默认) | — |
|
||
| **流程图 / 图表** | ✓ mermaid | **✗** | 需新增 |
|
||
| **数学公式** | ✓ KaTeX | **✗** | 需新增 |
|
||
| **HTML 制品** | ✓ sandbox iframe | **✗** | 需新增 |
|
||
| 内联 SVG(消毒) | ✓ DOMPurify | ✗ | 低优先 |
|
||
|
||
Ygent 当前渲染管线:`remarkGfm` + `rehypeHighlight`,**无 `rehype-raw`**(即不渲染 Markdown 里的裸 HTML)——这一点与 WorkBuddy 一致,是安全基线,**不要为了省事打开它**。
|
||
|
||
---
|
||
|
||
## 2. 两条进入路径(关键决策)
|
||
|
||
富内容有两种进入对话的方式,**建议两条都做,但优先级不同**:
|
||
|
||
### 路径 A:Markdown 内联(模型天然会用)
|
||
|
||
模型输出 ```mermaid 代码块或 `$$…$$` 公式,由渲染管线识别。
|
||
|
||
- 优点:**零工具改动**。模型不需要被教会调用新工具,它在别处已经习惯了这套写法。
|
||
- 证据:WorkBuddy 的 mermaid 就是这条路径——存在 i18n 键 `markdown.mermaid.rendering`(渲染中占位文案),说明是代码块触发而非工具产出。
|
||
- 适用:mermaid、KaTeX
|
||
|
||
### 路径 B:工具产出制品
|
||
|
||
专用工具(如 `show_html`)返回一段 HTML,以「制品块」形式呈现,带独立查看器 UI。
|
||
|
||
- 优点:有明确的类型边界,可以配查看器(切代码 / 复制 / 下载 / 导出图片)与独立的安全边界。
|
||
- 证据:WorkBuddy 的 `show_widget` 工具 + `SYn = {show_widget, visualize:show_widget, visualizer:show_widget}`,走 `<iframe sandbox="allow-scripts">`。
|
||
- 适用:HTML 制品(交互式页面、可视化)
|
||
|
||
---
|
||
|
||
## 3. 各类型的渲染方案与安全边界
|
||
|
||
### 3.1 mermaid 流程图(路径 A)
|
||
|
||
```
|
||
```mermaid
|
||
flowchart LR
|
||
A[用户] --> B[内核] --> C[结果]
|
||
```
|
||
```
|
||
|
||
渲染:` ```mermaid ` 代码块 → 动态加载 mermaid → `mermaid.render()` → 得到 SVG → **消毒** → 注入。
|
||
|
||
**消毒是必须的**,直接照抄 WorkBuddy 已验证的配置:
|
||
|
||
```js
|
||
DOMPurify.sanitize(svg, {
|
||
USE_PROFILES: { svg: true, svgFilters: true },
|
||
FORBID_TAGS: ["script", "foreignObject"],
|
||
FORBID_ATTR: ["srcdoc", "formaction"],
|
||
});
|
||
```
|
||
|
||
要点:
|
||
- **只开 SVG profile**(不含 HTML profile)——消毒结果只能表达图形,带不了 `<a>`/`<form>`
|
||
- **禁 `foreignObject`**:它能把 HTML 塞进 SVG 里绕过 SVG profile
|
||
- 渲染中显示占位(对应 `markdown.mermaid.rendering` 那种文案),失败时**回退显示原始代码**而不是空白
|
||
|
||
### 3.2 数学公式(路径 A)
|
||
|
||
`$inline$` 与 `$$block$$` → remark-math + rehype-katex → KaTeX 产出 HTML。
|
||
|
||
KaTeX 的输入是公式文本、输出是受控 HTML,**不需要额外消毒**(与 WorkBuddy 一致)。
|
||
|
||
### 3.3 HTML 制品(路径 B)
|
||
|
||
```jsx
|
||
<iframe sandbox="allow-scripts" srcDoc={html} title="交互制品" />
|
||
```
|
||
|
||
**不带 `allow-same-origin` 是整个方案的核心**:制品里的 JS 能跑(图表要动),但因为源是 opaque origin ——
|
||
读不到 `localStorage`、拿不到父窗口 DOM、发不出带 cookie 的请求。
|
||
|
||
已确认的前提:Ygent 的 `tauri.conf.json` 里 `"csp": null`,**不会拦 iframe srcdoc**。
|
||
若将来收紧 CSP,需为 iframe 放行(或在制品 HTML 内自带 `<meta http-equiv="Content-Security-Policy">`)。
|
||
|
||
查看器能力(参考 WorkBuddy 的 `widget.*` 文案):
|
||
显示 UI ⇄ 查看代码、复制代码、下载到本地、导出为图片、加载中态、错误面板。
|
||
|
||
---
|
||
|
||
## 4. 数据模型
|
||
|
||
**不需要新增 `MsgKind`。** 制品就是工具产出,复用现有 `tool` kind:
|
||
|
||
```ts
|
||
export type MsgKind = "user" | "agent" | "tool" | "perm"; // 不变
|
||
|
||
export interface ChatMessage {
|
||
// …现有字段
|
||
/** 制品:HTML 内容或落盘路径(见 §4.1) */
|
||
artifact?: { kind: "html"; path: string; title?: string };
|
||
/** 制品必须 keepVisible —— 折叠时不能被收走(否则制品整个消失) */
|
||
keepVisible?: boolean;
|
||
}
|
||
```
|
||
|
||
`toolState` 已在本轮补上,制品卡直接复用「运行中 / 完成」两态。
|
||
|
||
### 4.1 制品内容存哪(待决策)
|
||
|
||
| 方案 | 优点 | 缺点 |
|
||
|---|---|---|
|
||
| 存 `messages.result`(TEXT) | 零改动 | 大 HTML 撑大 SQLite;`list_messages` 变重 |
|
||
| **落项目工作目录 + DB 只存路径(建议)** | 符合「目录即项目」哲学;可被工具读回;DB 轻 | 需要文件生命周期管理 |
|
||
|
||
建议第二种:`.agent/artifacts/<message-id>.html`,DB 的 `result` 只存相对路径。
|
||
删除会话时一并清理(或按「永不删除项目目录」的铁律只解除登记)。
|
||
|
||
---
|
||
|
||
## 5. 与「回合分组 / 折叠」的关系(容易踩坑)
|
||
|
||
这是 WorkBuddy 用 `isShowWidget` + `keepVisible` + `presentationGroupId` 解决的问题,Ygent 必须一起考虑:
|
||
|
||
1. **制品不能被折叠掉。** 回合结束时会把连续工具块收成一行摘要——制品必须 `keepVisible`,
|
||
否则用户点了折叠,图表就没了。
|
||
2. **制品要和它的解释文字绑在一起。** WorkBuddy 的做法是制品块向前找最近的正文块,
|
||
共享 `presentationGroupId`。Ygent 目前用「回合」粒度分组(见 `ChatView.tsx` 的 `groupTurns`),
|
||
如果制品总是出现在回合末尾,回合粒度暂时够用;一旦出现「文字→制品→文字」的结构,
|
||
就需要下沉到块粒度。
|
||
3. **制品高度不定**,不能像工具卡那样单行省略。需要 iframe 通过 `postMessage` 上报高度自适应,
|
||
或给一个可拖拽/固定高度 + 内部滚动。
|
||
|
||
---
|
||
|
||
## 6. 体积代价(必须先决策)
|
||
|
||
| 依赖 | 体积 | 现状 |
|
||
|---|---|---|
|
||
| mermaid | **~2.8 MB**(WorkBuddy 的 `vendor-mermaid` chunk) | Ygent 前端 bundle 现在共 563 KB |
|
||
| katex | ~300 KB(含字体) | — |
|
||
| dompurify | ~45 KB | — |
|
||
|
||
**结论:一律动态 `import()`,按需加载,首次出现该内容类型时才拉。**
|
||
|
||
否则只为了偶尔一张流程图,就把首屏 bundle 撑到 3 MB+,冷启动明显变慢——这跟前面查到的「OMP 内核启动 1.31s」是同一类问题。
|
||
|
||
---
|
||
|
||
## 7. 顺带:代码 diff 视图(与富内容同级)
|
||
|
||
工具改文件时显示行级 diff,WorkBuddy 的 `tool-diff` 是现成参考:
|
||
|
||
- 输入 `oldCode` / `newCode` / `fileName`
|
||
- 按扩展名推断语言(`ts/tsx/js/py/rs/go/java/kt/swift/c/cpp/cs/php/css/html/vue/sh/yml/sql/scala`)
|
||
- Prism 懒加载该语言 + LRU 缓存,**语言不支持时回退 HTML 转义纯文本**(不要渲染空白)
|
||
- 行渲染 `+/−` 号
|
||
|
||
Ygent 的 `edit` / `write` 卡目前只显示「项目/新增文档.md +15」徽标。
|
||
要做 diff 的话,**必须用转义兜底**——文件内容绝不能当 HTML 注入。
|
||
|
||
---
|
||
|
||
## 8. 建议的分阶段落地
|
||
|
||
| 阶段 | 内容 | 成本 | 收益 |
|
||
|---|---|---|---|
|
||
| **P1** | mermaid 代码块(动态 import + DOMPurify SVG 消毒 + 失败回退原始代码) | 低 | 直观,模型零适配 |
|
||
| **P2** | KaTeX 公式(remark-math + rehype-katex,动态 import) | 低 | 技术对话可读性明显提升 |
|
||
| **P3** | 代码 diff 视图(`edit`/`write` 卡) | 中 | 直接改善现有工具卡 |
|
||
| **P4** | HTML 制品(工具 + iframe 查看器 + 存储策略) | 高 | 能力上限最高,但需要先定 §4.1 和 §5 |
|
||
|
||
P1 + P2 可以在一个改动里做完(都是 Markdown 管线扩展 + 动态加载),建议作为第一步。
|
||
|
||
---
|
||
|
||
## 9. 待决策清单
|
||
|
||
1. **制品 HTML 存 DB 还是落项目目录?**(建议落目录,见 §4.1)
|
||
2. **是否允许制品访问外网(CDN 脚本/字体)?** 现在 `csp: null` 允许;要不要收紧到白名单?
|
||
3. **制品高度**:`postMessage` 自适应,还是固定高度 + 内部滚动?
|
||
4. **mermaid 之外是否还要别的图表库**(如 ECharts)?WorkBuddy 的 `show_widget` 走的是「生成 HTML」而不是绑定图表库,这条路更通用但要制品能力先就位。
|
||
5. **是否给制品加「导出为图片」**?WorkBuddy 有(`widget.downloadImage`),但需要额外依赖做 HTML→图片。
|
||
|
||
---
|
||
|
||
## 附:与 WorkBuddy 的差异(有意为之)
|
||
|
||
| 项 | WorkBuddy | Ygent 建议 |
|
||
|---|---|---|
|
||
| 高亮库 | Prism | 保持 highlight.js(已用,换库无收益) |
|
||
| Markdown 裸 HTML | 未开启 | **同样不开启** |
|
||
| 制品渲染 | iframe sandbox | 同 |
|
||
| 内联 SVG 消毒 | DOMPurify SVG profile | 同(做 mermaid 时一并引入) |
|
||
| 虚拟列表 | 有 | **不做**(Ygent 无此瓶颈) |
|
||
| widget 分组 | 有(`presentationGroupId`) | **暂不做**,先用回合粒度;出现「文字→制品→文字」再下沉 |
|