WPS灵犀Claw如何实现Markdown文件的实时动态预览?
灵犀 Claw 作为 AI 对话客户端,每天需要将海量的 AI 流式输出实时渲染为格式化的 Markdown 文档。为了实现这一目标,灵犀引入了一套完整的渲染管线,这套渲染引擎实现了从 AI 流式输出到产品级预览,解决了 AI 场景特有的渲染难题。
一、整体架构
灵犀的 Markdown 渲染在前端进程(app.asar)内完成,基于 streamdown 库构建。streamdown 由 Vercel 开源,定位是"面向 AI 流式输出的 react-markdown 替代品"。灵犀在此基础上集成了代码高亮、图表渲染、数学公式和安全过滤等完整能力。
核心管线:
Markdown 文本流
│
▼
streamdown (parseMarkdownIntoBlocks) ← 逐块切分
│
▼
remend (v1.2.2) ← 流式容错:补全未关闭的语法
│
▼
unified 管线
├─ remark-parse (micromark) ← Markdown → mdast
├─ remark-gfm ← GitHub Flavored Markdown 扩展
├─ remark-rehype ← mdast → hast
├─ rehype-raw ← 透传原始 HTML
├─ rehype-sanitize ← HTML 安全过滤
└─ rehype-harden ← 二次安全加固
│
▼
React 组件树渲染
├─ 文本块(带动画)
├─ 代码块(shiki 高亮,明/暗主题)
├─ 表格(交互:复制、下载)
├─ Mermaid 图表(可选插件)
└─ 数学公式(@streamdown/math,可选插件)所有代码均打包在 renderer bundle(out/renderer/assets/index-z1lmW2JW.js)中,不依赖外部 CDN。
二、streamdown:为 AI 场景而生的渲染引擎
2.1 核心设计理念
传统 Markdown 渲染库(如 react-markdown)假设输入是完整的文档。而灵犀的场景完全不同:AI 模型逐 token 输出,文本在每毫秒都可能增长或变化。这意味着渲染器必须面对:
不完整的语法结构:代码围栏只开了头还没关、表格只写了一半、加粗标记只有开标签没有闭标签
高频重渲染:每次收到新 token 都需要重新解析和渲染
视觉连续性:渲染结果不能因中间状态而剧烈跳动
streamdown 通过两个机制解决这些问题:
逐块独立渲染。parseMarkdownIntoBlocks 将 Markdown 文本按顶层块级元素(段落、代码块、表格、标题等)拆分为独立单元。每个块有自己完整的语法边界,可以独立解析、独立挂载到 DOM 上。当新 token 到达时,只需更新当前正在生长的块,已完成的块保持不变。
remend 流式容错。remend(v1.2.2)是 streamdown 的姊妹库,专门处理不完整的 Markdown 语法。从 app.asar 中提取的源码可以看到,remend 对以下场景做了容错处理:
未关闭的代码围栏:\``python\nprint("hello` 不会导致整个后续文本被吞入代码块,remend 会检测到围栏未关闭,自动补全闭合标记后再交给解析器
未配对的加粗/斜体:**important 在输出过程中不会产生格式泄漏,remend 会将其视为普通文本
转义反引号:正确处理 \` 在代码块内外的语义差异,避免将转义的反引号误判为行内代码的边界
2.2 渲染模式配置
从 renderer bundle 中提取的默认配置:
mode: "streaming"
isAnimating: false
shikiTheme: ["github-light", "github-dark"]
controls: true
linkSafety: { enabled: true }
mermaid: void 0 // 默认关闭mode: "streaming":启用流式渲染,每次 token 更新只处理变化的块
isAnimating: false:块入场动画默认关闭,可通过 StreamdownContext 按需开启
controls: true:代码块显示"复制"和"下载"按钮
2.3 入场动画
streamdown/styles.css定义了三种动画:
@keyframes sd-fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes sd-blurIn { from { opacity: 0; filter: blur(4px) } to { opacity: 1; filter: blur(0) } }
@keyframes sd-slideUp { from { opacity: 0; transform: translateY(4px) } to { opacity: 1; transform: translateY(0) } }
[data-sd-animate] {
animation: var(--sd-animation, sd-fadeIn) var(--sd-duration, 150ms) var(--sd-easing, ease) both;
}当 isAnimating: true 时,每个新渲染的块会带动画出现,动画持续 150ms,默认使用 fadeIn 效果。用户可通过 CSS 变量 --sd-animation、--sd-duration、--sd-easing 自定义。
三、代码高亮:shiki
3.1 双主题支持
灵犀使用 shiki 进行代码高亮,默认配置了明暗双主题:
shikiTheme: ["github-light", "github-dark"]渲染后的代码块通过 CSS 变量实现主题切换:
--sdm-bg:代码块背景色
--shiki-dark-bg:暗色主题下的代码块背景
--shiki-dark:暗色主题的 class 标记
--sdm-c:文字颜色
这意味着可以在不重新解析代码的情况下,仅通过切换 CSS 变量即可在明暗主题间切换代码高亮。
3.2 语言支持
renderer bundle 中内置了一个包含 295 条映射规则的语言表。部分映射:
输入标识 | shiki 语言 | 输入标识 | shiki 语言 |
python / py | py | typescript / ts | ts |
javascript / js | js | java | java |
c | c | cpp | cpp |
rust / rs | rs | go | go |
sql | sql | bash / sh | sh |
docker / dockerfile | dockerfile | yaml / yml | yaml |
json | json | latex / tex | tex |
wasm | wasm | mermaid / mmd | mmd |
文言 | wy | wolfram / wl | wl |
3.3 动态加载策略
HighlightedCodeBlockBody 组件通过 React lazy() + Vite __vitePreload 动态加载:
HighlightedCodeBlockBody = react.lazy(() => __vitePreload(() => import("./highlighted-body-TPN3WLV5-L3cR62FZ.js")))这意味着 shiki 引擎(含 Oniguruma WASM 和语法 grammar)只有在页面首次出现代码块时才会加载,避免了首屏性能开销。
四、表格交互能力
streamdown 内置了表格交互组件,支持以下操作:
复制表格:TableCopyDropdown 将 HTML 表格转为多种格式(CSV / TSV / Markdown)复制到剪贴板
下载表格:TableDownloadButton 将表格导出为 CSV、TSV 或 Markdown 文件
这些组件通过 extractTableDataFromElement 从 DOM 中提取结构化数据,实现了纯前端的表格导出能力,无需后端参与。
代码块同样支持复制(CodeBlockCopyButton)和下载源文件(CodeBlockDownloadButton)。
五、可选插件
streamdown 采用插件化架构,灵犀注册了以下插件:
插件 | 来源 | 默认状态 | 说明 |
remarkGfm | 内置 remark-gfm v4 | 启用 | GitHub Flavored Markdown(表格、删除线、任务列表等) |
codeMeta | streamdown 内置 | 启用 | 代码块元信息(语言标识、标题、行高亮等) |
@streamdown/cjk | 可选包 v1.0.2 | 未在 deps 中 | CJK 字符优化(中日韩文本的排版处理) |
@streamdown/math | 可选包 v1.0.2 | 未在 deps 中 | 数学公式渲染(KaTeX/LaTeX) |
@streamdown/mermaid | 可选包 v1.0.2 | 未在 deps 中 | Mermaid 图表渲染 |
其中 @streamdown/cjk 和 @streamdown/math 虽在 devDependencies 中列出,但未出现在 dependencies 中,说明灵犀当前版本未启用数学公式和 CJK 优化插件。Mermaid 通过 StreamdownContext 的 mermaid prop 按需注入。
六、安全机制
Markdown 渲染的输入来自 AI 模型输出,安全性至关重要。灵犀部署了三重过滤:
层级 | 库 | 职责 |
第一层 | rehype-raw | 允许在 Markdown 中嵌入原始 HTML 标签(如 <details>、<kbd> 等) |
第二层 | rehype-sanitize | 基于 HTML Sanitizer 规范,白名单放行安全标签和属性 |
第三层 | rehype-harden | 二次加固,额外过滤潜在的 XSS 向量 |
此外,linkSafety: { enabled: true } 启用了链接安全机制,对用户可点击的 URL 进行安全检查。
七、建议将 Markdown 预览能力引入 WPS 客户端
WPS 目前不支持直接打开和预览 .md 文件。用户查看 Markdown 文件通常需要:
使用第三方编辑器(VS Code、Typora 等)
或在浏览器中使用 Markdown 预览插件
或将 Markdown 转换为其他格式后再打开
这造成了工作流中断——用户在 Markdown 文件和 WPS 办公套件之间频繁切换。
具体建议:
在 WPS 客户端中增加 .md 文件的关联打开能力。双击 .md 文件时,在 WPS 内嵌的 Chromium 视图中渲染 Markdown 预览。渲染引擎可复用灵犀的 streamdown + shiki 方案,实现零依赖的纯前端渲染。
在预览基础上增加所见即所得的 Markdown 编辑。streamdown 的逐块渲染架构天然适合编辑场景——每个块可独立编辑、独立保存,不会影响其他块的状态。可引入 CodeMirror 或 Monaco Editor 作为代码块的编辑器。
将 Markdown 文件纳入 WPS 文档生态:
支持 Markdown 与 WPS 文档格式(docx)的双向转换
支持 Markdown 中嵌入 WPS 公式(与现有公式编辑器互通)
支持 Mermaid 图表的渲染和导出(与 WPS 的 SmartArt 生态打通)
.md 文件可直接保存到金山文档云空间,在 Web 端同步预览