Files
ycode/docs/reference/ygent-rich-content.md

206 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`) | **暂不做**,先用回合粒度;出现「文字→制品→文字」再下沉 |