kimi-web Dynamic HTML 最终设计

在页面中嵌入可交互 HTML 组件 / iframe 页面 · 合并 docs/dynamic-html-research.html 与 reports/dynamic-html-design-research.html 两份调研的最终方案 · 2026-06-11

设计一句话:「artifact = 工作区里的 HTML 内容」。模型照常写 .html 文件或输出 ```html 代码块,不新增协议内容类型、不新增 artifact 工具;前端把这两类内容识别为 artifact,在聊天卡片、右侧分屏和 files 面板里用统一的沙箱 iframe渲染——文件走 daemon 预览路由(服务端下发默认拒绝的 CSP),代码块走 srcdoc;两条投递路径共用同一个安全模型: sandbox 不带 allow-same-origin。运行中的本地 dev server 作为第二类 artifact(URL 嵌入)。交互桥用对齐 MCP Apps 的 postMessage JSON-RPC,凭实例 token 校验。

1. 范围

不做
· 自包含 HTML(文件 / 代码块)的内联预览与分屏预览
· 同目录静态资源(CSS/JS/图片)的相对路径加载
· 本地 dev server 的 iframe 嵌入(URL artifact)
· artifact → 对话的受控回传(postMessage 桥)
· 新窗口打开 / 复制源码 / 下载
· 协议级 artifact 内容类型与独立 artifact store(文件即存储;将来需要版本管理时再评估)
· WebContainers / Sandpack / 浏览器内打包 React 多文件项目(daemon 能跑真 dev server,由 URL artifact 覆盖)
· artifact 发布/分享到外部(纯本地产品形态)

2. 产品形态

2.1 三个呈现位置

┌────────────────────────────────────────────┬───────────────────────────┐ │ 聊天流 │ 右侧分屏(artifact 舞台) │ │ │ ┌───────────────────────┐ │ │ kimi > 我做了一个对比页…… │ │ ⛶ 方案对比 · 沙箱预览 │ │ │ ┌────────────────────────────────────┐ │ │ ┌───────────────────┐ │ │ │ │ ▤ docs/compare.html [预览] │ ──→ │ │ │ (iframe 渲染) │ │ │ │ │ HTML artifact · 24 KB │ │ │ │ │ │ │ │ └────────────────────────────────────┘ │ │ └───────────────────┘ │ │ │ (工具卡片 / 代码块卡片是同一种外壳) │ │ 预览 | 源码 ⟳ ↗ ⤓ │ │ │ │ └───────────────────────┘ │ └────────────────────────────────────────────┴───────────────────────────┘ files 标签页:FilePreview 对 .html 提供「预览 | 源码」切换(同一渲染组件)

2.2 入口矩阵

artifact 来源识别方式投递方式
工作区 .html 文件(write/edit 工具、历史文件)工具参数路径后缀(toolMeta 已能提取);files 树daemon 预览路由(§4.1)
消息中的 ```html 代码块messagesToTurns 拆块时按语言 + 行数阈值识别srcdoc(§4.2)
本地 dev server(vite 等后台任务)daemon 后台任务元数据中的端口/URL(解析任务输出或显式约定)URL iframe(§4.3)

3. 架构与数据流

模型 write a.html / 输出 ```html 消息流(现有协议,零改动) messagesToTurns 派生 artifact 块 ArtifactBlock 卡片 ArtifactFrame 沙箱渲染 postMessage 桥(token 校验)

为什么不上协议级 artifact 类型(reports 版方案 C 的取舍):kimi-code 的 agent 工作流本来就以文件为产物(TUI 同理), 「artifact = 文件」让 web/TUI 行为一致、历史会话立即可用、文件系统天然承担存储与版本(git); 新增 content type + store + 专用工具会改变模型行为并造成双轨。派生层(messagesToTurns)已有先例——TurnBlock 的 text/thinking/tool 同样是派生的有序块,artifact 作为第四种 kind 加入即可。

4. 渲染机制(统一安全模型,两种投递)

4.1 文件 artifact:daemon 预览路由

新增只读路由(薄包装现有 fs 读取,含路径穿越校验):

GET /api/v1/sessions/{session_id}/fs/{path}:preview

响应头(对 text/html):
Content-Type: text/html; charset=utf-8
Content-Security-Policy:
  default-src 'none';
  script-src 'self' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob:;
  font-src 'self' data:;
  media-src 'self' data: blob:;
  connect-src 'none';            ← 关键:禁止 fetch/XHR/WebSocket,daemon API 打不通
  frame-src 'none';
  base-uri 'none';
  form-action 'none'
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff

4.2 代码块 artifact:srcdoc

内容尚不在磁盘上,直接内联,并在文档头注入 CSP meta(meta CSP 与文档自带 CSP 取交集,只紧不松):

<iframe
  :srcdoc="injectCspMeta(block.html)"
  sandbox="allow-scripts allow-forms allow-modals allow-popups"
  referrerpolicy="no-referrer"
  loading="lazy"
  :key="contentHash"        ← 内容变化即整体重建,不复用实例
/>

4.3 URL artifact:本地 dev server

<iframe src="http://localhost:5173/" loading="lazy">   ← 不加 sandbox
不可妥协的两条红线:① 渲染模型生成的 HTML 时,sandbox 永远不带 allow-same-origin,且不提供任何「放开同源」的设置开关(Open WebUI 的反面教训); ② 不可信 HTML 永远不直接进宿主 DOM(不进 Markdown.vue、不用 v-html / dangerouslySetInnerHTML)。

5. postMessage 交互桥

协议形态对齐 MCP Apps(JSON-RPC over postMessage),方法集从最小白名单开始。宿主在 iframe load 后用 postMessage 下发一次性 实例 token(opaque origin 下 event.origin 恒为 null,不能用 origin 鉴别,必须 token + event.source === iframe.contentWindow 双校验)。

方法(artifact → 宿主)语义宿主行为
ui/ready初始化完成撤掉骨架占位
ui/height内容高度变化调整内联卡片 iframe 高度(上限 70vh,超出内部滚动)
ui/error运行时错误(artifact 内 window.onerror 转发)外壳显示错误条 +「让模型修复」按钮
ui/open-link请求打开外部链接宿主确认弹层后 window.open,绝不在 iframe 内导航
ui/follow-up请求向对话追加一条消息(如「基于这个原型改 X」)填入 Composer 草稿,由用户按发送——iframe 永远不能直接驱动 agent

桥以一段宿主注入的 <script>(srcdoc 注入 / 预览路由响应改写 <head>)暴露 window.kimi.{notifyHeight, reportError, openLink, followUp},模型的 HTML 生成规范同步增加这几个可选 API 的说明。 将来支持 MCP Apps(ui:// 资源)时,这条桥直接换成完整 MCP JSON-RPC 方言,外壳与安全模型不变。

6. 安全模型

前置依赖:daemon 启动 token。daemon 生成随机 token,web URL 以 #token 携带,前端对 API 请求加 header,daemon 校验并对预览路由豁免(CSP 已默认拒绝、且其内容本身来自工作区)。 这同时关闭了与本功能无关的存量攻击面(任何本机网页可直接调用无鉴权的 127.0.0.1:7878)。
威胁后果对策
提示注入控制的 HTML 调用 daemon API读写任意文件 / 开会话执行命令(等价 RCE)sandbox 无 allow-same-origin(opaque origin)+ connect-src 'none' + daemon token 三层
HTML 外联泄漏对话/工作区内容数据经 img/fetch/表单发往外部域名CSP default-src 'none'(img 仅 self/data/blob、form-action 'none'、connect 'none');CDN 走显式 allowlist
伪造宿主 UI(假登录框/假确认弹窗)用户误信 artifact 内容为原生界面外壳常显「沙箱预览」标识与来源路径;禁全屏 API;外链必经宿主确认
postMessage 伪造其他 frame / 页面冒充 artifact 触发宿主动作实例 token + event.source 校验 + zod 校验消息 schema + follow-up 永远停在草稿
资源滥用(巨型 HTML、死循环、一轮 N 个 artifact)页面卡死、内存膨胀大小上限(如 2 MB,超出仅源码模式)、点击才实例化、同屏最多 1 个活动 iframe(分屏内)、lazy + 离屏卸载
预览路由路径穿越读出工作区之外的文件复用 fs 路由既有的根目录约束与规范化(实现时验证用例:../、symlink、绝对路径)

7. 模块改动清单

位置改动
packages/daemon/src/routes/fs.ts新增 :preview GET 路由(CSP/安全头 + 注入桥脚本);daemon 启动 token 中间件(独立小改动)
apps/kimi-web/src/types.tsTurnBlock 增加 { kind: 'artifact'; artifact: ArtifactView }ArtifactView = { source: 'file' | 'inline' | 'url'; title; path?; html?; url?; size }
apps/kimi-web/src/composables/messagesToTurns.ts派生 artifact 块:html 写盘工具 → file artifact;≥30 行 ```html 代码块 → inline artifact(保持原始顺序,不 hoist)
apps/kimi-web/src/components/ArtifactFrame.vue(新)统一沙箱 iframe:三种投递、骨架/错误态、postMessage 桥、高度管理
ArtifactBlock.vue(新)/ ChatPane.vue聊天卡片外壳;ChatPane 渲染第四种 block
ConversationPane.vue右侧分屏 artifact 舞台(与 files 分栏布局共享);移动端全屏 sheet
FilePreview.vuehtml 类型:预览 | 源码 tab,复用 ArtifactFrame
TasksPane.vue / 任务卡片带端口的后台任务显示「预览」入口(URL artifact)
HTML 生成规范(用户侧 CLAUDE.md / 系统提示)补充沙箱约束说明(无 localStorage、无外联、可选 window.kimi 桥 API)

8. 关键边界与降级

9. 调研依据(两份输入报告的来源汇总)