mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-24 16:44:53 +00:00
- ThinkingBlock: foldable last-paragraph teaser, hover-only indicator, auto-collapse when streaming ends, line-height aligned with content - ChatPane: static turn-summary at end of assistant turns - Composer: perm-pill background matches toggle-pill - SessionRow: kebab button moved before timestamp - ToolCall: replace text chevron with SVG
285 lines
22 KiB
HTML
285 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>kimi-web Dynamic HTML 最终设计</title>
|
||
<style>
|
||
:root {
|
||
--ink: #1a1a1a; --muted: #57606a; --faint: #8b949e;
|
||
--line: #d8dee4; --soft: #f6f8fa; --panel: #ffffff;
|
||
--blue: #0969da; --bluebg: #ddf4ff; --green: #1a7f37; --greenbg: #dafbe1;
|
||
--red: #cf222e; --redbg: #ffebe9; --amber: #9a6700; --amberbg: #fff8c5;
|
||
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0; padding: 0 16px 80px;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||
color: var(--ink); background: #fafbfc; line-height: 1.7; font-size: 15px;
|
||
}
|
||
main { max-width: 900px; margin: 0 auto; }
|
||
h1 { font-size: 24px; margin: 36px 0 6px; }
|
||
h2 { font-size: 19px; margin: 44px 0 12px; padding-bottom: 8px; border-bottom: 2px solid var(--line); }
|
||
h3 { font-size: 16px; margin: 24px 0 8px; }
|
||
p { margin: 8px 0; }
|
||
ul { margin: 8px 0; }
|
||
.sub { color: var(--muted); font-size: 13px; margin-bottom: 28px; }
|
||
code { font-family: var(--mono); font-size: 13px; background: var(--soft); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; }
|
||
pre { background: #0d1117; color: #e6edf3; border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
|
||
pre code { background: none; border: none; color: inherit; padding: 0; }
|
||
table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 13.5px; background: var(--panel); }
|
||
th, td { border: 1px solid var(--line); padding: 8px 10px; text-align: left; vertical-align: top; }
|
||
th { background: var(--soft); }
|
||
.tablewrap { overflow-x: auto; }
|
||
.verdict { background: var(--bluebg); border: 1px solid #54aeff66; border-radius: 10px; padding: 14px 18px; margin: 18px 0; }
|
||
.verdict b { color: var(--blue); }
|
||
.risk { background: var(--redbg); border-left: 4px solid var(--red); border-radius: 6px; padding: 10px 14px; margin: 12px 0; }
|
||
.note { background: var(--amberbg); border-left: 4px solid var(--amber); border-radius: 6px; padding: 10px 14px; margin: 12px 0; }
|
||
.ok { background: var(--greenbg); border-left: 4px solid var(--green); border-radius: 6px; padding: 10px 14px; margin: 12px 0; }
|
||
.tag { display: inline-block; font-size: 11.5px; font-weight: 600; border-radius: 999px; padding: 1px 9px; margin-right: 4px; white-space: nowrap; }
|
||
.t-green { background: var(--greenbg); color: var(--green); }
|
||
.t-red { background: var(--redbg); color: var(--red); }
|
||
.t-blue { background: var(--bluebg); color: var(--blue); }
|
||
.flow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 14px 0; font-size: 13px; }
|
||
.flow .step { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; }
|
||
.flow .arrow { color: var(--faint); font-weight: 700; }
|
||
.wire {
|
||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||
padding: 14px; margin: 12px 0; font-family: var(--mono); font-size: 12px;
|
||
line-height: 1.5; overflow-x: auto; white-space: pre; color: var(--muted);
|
||
}
|
||
#tocbtn {
|
||
position: fixed; right: 18px; bottom: 18px; z-index: 20;
|
||
background: var(--ink); color: #fff; border: none; border-radius: 999px;
|
||
padding: 10px 16px; font-size: 13px; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.2);
|
||
}
|
||
#toc {
|
||
position: fixed; right: 18px; bottom: 64px; z-index: 20; display: none;
|
||
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||
padding: 10px 16px; box-shadow: 0 8px 24px rgba(0,0,0,.14); max-width: 300px;
|
||
}
|
||
#toc a { display: block; color: var(--ink); text-decoration: none; padding: 4px 0; font-size: 13px; }
|
||
#toc a:hover { color: var(--blue); }
|
||
@media (max-width: 640px) { body { font-size: 14.5px; } th, td { padding: 6px 8px; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>kimi-web Dynamic HTML 最终设计</h1>
|
||
<p class="sub">在页面中嵌入可交互 HTML 组件 / iframe 页面 · 合并 docs/dynamic-html-research.html 与 reports/dynamic-html-design-research.html 两份调研的最终方案 · 2026-06-11</p>
|
||
|
||
<div class="verdict">
|
||
<b>设计一句话:</b>「artifact = 工作区里的 HTML 内容」。模型照常写 <code>.html</code> 文件或输出
|
||
<code>```html</code> 代码块,<b>不新增协议内容类型、不新增 artifact 工具</b>;前端把这两类内容识别为
|
||
artifact,在聊天卡片、右侧分屏和 files 面板里用<b>统一的沙箱 iframe</b>渲染——文件走 daemon
|
||
预览路由(服务端下发默认拒绝的 CSP),代码块走 srcdoc;两条投递路径共用同一个安全模型:
|
||
<code>sandbox</code> 不带 <code>allow-same-origin</code>。运行中的本地 dev server 作为第二类
|
||
artifact(URL 嵌入)。交互桥用对齐 MCP Apps 的 postMessage JSON-RPC,凭实例 token 校验。
|
||
</div>
|
||
|
||
<h2 id="s1">1. 范围</h2>
|
||
<div class="tablewrap"><table>
|
||
<tr><th>做</th><th>不做</th></tr>
|
||
<tr><td>
|
||
· 自包含 HTML(文件 / 代码块)的内联预览与分屏预览<br>
|
||
· 同目录静态资源(CSS/JS/图片)的相对路径加载<br>
|
||
· 本地 dev server 的 iframe 嵌入(URL artifact)<br>
|
||
· artifact → 对话的受控回传(postMessage 桥)<br>
|
||
· 新窗口打开 / 复制源码 / 下载
|
||
</td><td>
|
||
· 协议级 artifact 内容类型与独立 artifact store(文件即存储;将来需要版本管理时再评估)<br>
|
||
· WebContainers / Sandpack / 浏览器内打包 React 多文件项目(daemon 能跑真 dev server,由 URL artifact 覆盖)<br>
|
||
· artifact 发布/分享到外部(纯本地产品形态)
|
||
</td></tr>
|
||
</table></div>
|
||
|
||
<h2 id="s2">2. 产品形态</h2>
|
||
<h3>2.1 三个呈现位置</h3>
|
||
<div class="wire">┌────────────────────────────────────────────┬───────────────────────────┐
|
||
│ 聊天流 │ 右侧分屏(artifact 舞台) │
|
||
│ │ ┌───────────────────────┐ │
|
||
│ kimi > 我做了一个对比页…… │ │ ⛶ 方案对比 · 沙箱预览 │ │
|
||
│ ┌────────────────────────────────────┐ │ │ ┌───────────────────┐ │ │
|
||
│ │ ▤ docs/compare.html [预览] │ ──→ │ │ │ (iframe 渲染) │ │ │
|
||
│ │ HTML artifact · 24 KB │ │ │ │ │ │ │
|
||
│ └────────────────────────────────────┘ │ │ └───────────────────┘ │ │
|
||
│ (工具卡片 / 代码块卡片是同一种外壳) │ │ 预览 | 源码 ⟳ ↗ ⤓ │ │
|
||
│ │ └───────────────────────┘ │
|
||
└────────────────────────────────────────────┴───────────────────────────┘
|
||
files 标签页:FilePreview 对 .html 提供「预览 | 源码」切换(同一渲染组件)</div>
|
||
<ul>
|
||
<li><b>聊天卡片(ArtifactBlock)</b>:write/edit 了 <code>.html</code> 的工具卡片、超过 30 行的 <code>```html</code> 代码块,折叠为 artifact 卡片——标题(文件名或首个 <code><title></code>)+ 大小 + 「预览 / 分屏打开 / 复制」。默认<b>不自动渲染</b>,点击才创建 iframe(防止一轮多个 artifact 同时吃资源)。</li>
|
||
<li><b>右侧分屏(artifact 舞台)</b>:与 files/diff 共享分栏布局。顶部固定外壳:标题、来源路径、「沙箱预览」标识(防伪造 UI 冒充原生界面)、预览/源码切换、刷新、新窗口、下载。</li>
|
||
<li><b>files 面板</b>:FilePreview 对 <code>html</code> 类型增加预览 tab,复用同一个 <code>ArtifactFrame</code> 组件。</li>
|
||
<li><b>移动端</b>:卡片点开后全屏 sheet 呈现(复用现有 bottom-sheet 模式),不做分屏。</li>
|
||
</ul>
|
||
|
||
<h3>2.2 入口矩阵</h3>
|
||
<div class="tablewrap"><table>
|
||
<tr><th>artifact 来源</th><th>识别方式</th><th>投递方式</th></tr>
|
||
<tr><td>工作区 <code>.html</code> 文件(write/edit 工具、历史文件)</td><td>工具参数路径后缀(toolMeta 已能提取);files 树</td><td>daemon 预览路由(§4.1)</td></tr>
|
||
<tr><td>消息中的 <code>```html</code> 代码块</td><td>messagesToTurns 拆块时按语言 + 行数阈值识别</td><td>srcdoc(§4.2)</td></tr>
|
||
<tr><td>本地 dev server(vite 等后台任务)</td><td>daemon 后台任务元数据中的端口/URL(解析任务输出或显式约定)</td><td>URL iframe(§4.3)</td></tr>
|
||
</table></div>
|
||
|
||
<h2 id="s3">3. 架构与数据流</h2>
|
||
<div class="flow">
|
||
<span class="step">模型 write a.html / 输出 ```html</span><span class="arrow">→</span>
|
||
<span class="step">消息流(现有协议,零改动)</span><span class="arrow">→</span>
|
||
<span class="step">messagesToTurns 派生 artifact 块</span><span class="arrow">→</span>
|
||
<span class="step">ArtifactBlock 卡片</span><span class="arrow">→</span>
|
||
<span class="step">ArtifactFrame 沙箱渲染</span><span class="arrow">⇄</span>
|
||
<span class="step">postMessage 桥(token 校验)</span>
|
||
</div>
|
||
<p><b>为什么不上协议级 artifact 类型</b>(reports 版方案 C 的取舍):kimi-code 的 agent 工作流本来就以文件为产物(TUI 同理),
|
||
「artifact = 文件」让 web/TUI 行为一致、历史会话立即可用、文件系统天然承担存储与版本(git);
|
||
新增 content type + store + 专用工具会改变模型行为并造成双轨。派生层(messagesToTurns)已有先例——TurnBlock 的
|
||
text/thinking/tool 同样是派生的有序块,artifact 作为第四种 kind 加入即可。</p>
|
||
|
||
<h2 id="s4">4. 渲染机制(统一安全模型,两种投递)</h2>
|
||
|
||
<h3>4.1 文件 artifact:daemon 预览路由</h3>
|
||
<p>新增只读路由(薄包装现有 fs 读取,含路径穿越校验):</p>
|
||
<pre><code>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</code></pre>
|
||
<ul>
|
||
<li><code>'self'</code> 只为同目录相对资源服务:HTML 里 <code><img src="./chart.png"></code>、<code><script src="./app.js"></code> 会回到同一预览路由解析(非 HTML 文件按真实 MIME 返回,同样带 nosniff)。</li>
|
||
<li><code>connect-src 'none'</code> 与「iframe sandbox 不带 allow-same-origin」双保险:前者挡住一切网络 API,后者让文档处于 opaque origin(即使 CSP 被绕过,跨域写操作也会被预检拦下)。</li>
|
||
<li>外部 CDN 默认全禁,与仓库 HTML 生成规范的「默认自包含」一致;如确需放行(Chart.js 等),在 daemon 配置里加显式 allowlist 拼进 <code>script-src</code>,不做 UI 开关。</li>
|
||
<li>「新窗口打开」直接开这个 URL——头部策略在独立标签页同样生效。</li>
|
||
</ul>
|
||
|
||
<h3>4.2 代码块 artifact:srcdoc</h3>
|
||
<p>内容尚不在磁盘上,直接内联,并在文档头注入 CSP meta(meta CSP 与文档自带 CSP 取交集,只紧不松):</p>
|
||
<pre><code><iframe
|
||
:srcdoc="injectCspMeta(block.html)"
|
||
sandbox="allow-scripts allow-forms allow-modals allow-popups"
|
||
referrerpolicy="no-referrer"
|
||
loading="lazy"
|
||
:key="contentHash" ← 内容变化即整体重建,不复用实例
|
||
/></code></pre>
|
||
<ul>
|
||
<li>注入的 meta CSP 同 §4.1(去掉 'self':srcdoc 无相对资源概念),meta 不支持的指令(frame-ancestors 等)由 sandbox 兜底。</li>
|
||
<li>流式期间显示骨架占位 + 行数计数,<b>代码块闭合后才渲染</b>(避免每个 delta 重建 iframe)。</li>
|
||
<li>用户「保存为文件」后,卡片升级为文件 artifact(切到 §4.1 路径)。</li>
|
||
</ul>
|
||
|
||
<h3>4.3 URL artifact:本地 dev server</h3>
|
||
<pre><code><iframe src="http://localhost:5173/" loading="lazy"> ← 不加 sandbox</code></pre>
|
||
<ul>
|
||
<li>这是用户自己的项目进程,信任级别 = 用户代码;不同端口已是跨 origin,拿不到 kimi-web 与 daemon 的任何状态。</li>
|
||
<li>目标页面带 <code>X-Frame-Options</code> / <code>frame-ancestors</code> 拒绝被嵌时,外壳降级为「在新标签页打开」按钮 + 截图占位(iframe onload 探测失败即降级)。</li>
|
||
<li>入口:daemon 后台任务(background.task.started)携带端口元数据时,任务卡片出现「预览」。</li>
|
||
</ul>
|
||
|
||
<div class="risk">
|
||
<b>不可妥协的两条红线:</b>① 渲染模型生成的 HTML 时,sandbox 永远不带
|
||
<code>allow-same-origin</code>,且不提供任何「放开同源」的设置开关(Open WebUI 的反面教训);
|
||
② 不可信 HTML 永远不直接进宿主 DOM(不进 Markdown.vue、不用 v-html / dangerouslySetInnerHTML)。
|
||
</div>
|
||
|
||
<h2 id="s5">5. postMessage 交互桥</h2>
|
||
<p>协议形态对齐 MCP Apps(JSON-RPC over postMessage),方法集从最小白名单开始。宿主在 iframe
|
||
<code>load</code> 后用 <code>postMessage</code> 下发一次性 <b>实例 token</b>(opaque origin 下
|
||
<code>event.origin</code> 恒为 <code>null</code>,不能用 origin 鉴别,必须 token + <code>event.source === iframe.contentWindow</code> 双校验)。</p>
|
||
<div class="tablewrap"><table>
|
||
<tr><th>方法(artifact → 宿主)</th><th>语义</th><th>宿主行为</th></tr>
|
||
<tr><td><code>ui/ready</code></td><td>初始化完成</td><td>撤掉骨架占位</td></tr>
|
||
<tr><td><code>ui/height</code></td><td>内容高度变化</td><td>调整内联卡片 iframe 高度(上限 70vh,超出内部滚动)</td></tr>
|
||
<tr><td><code>ui/error</code></td><td>运行时错误(artifact 内 window.onerror 转发)</td><td>外壳显示错误条 +「让模型修复」按钮</td></tr>
|
||
<tr><td><code>ui/open-link</code></td><td>请求打开外部链接</td><td>宿主确认弹层后 window.open,绝不在 iframe 内导航</td></tr>
|
||
<tr><td><code>ui/follow-up</code></td><td>请求向对话追加一条消息(如「基于这个原型改 X」)</td><td>填入 Composer 草稿,<b>由用户按发送</b>——iframe 永远不能直接驱动 agent</td></tr>
|
||
</table></div>
|
||
<p>桥以一段宿主注入的 <script>(srcdoc 注入 / 预览路由响应改写 <head>)暴露
|
||
<code>window.kimi.{notifyHeight, reportError, openLink, followUp}</code>,模型的 HTML 生成规范同步增加这几个可选 API 的说明。
|
||
将来支持 MCP Apps(<code>ui://</code> 资源)时,这条桥直接换成完整 MCP JSON-RPC 方言,外壳与安全模型不变。</p>
|
||
|
||
<h2 id="s6">6. 安全模型</h2>
|
||
<div class="note"><b>前置依赖:daemon 启动 token。</b>daemon 生成随机 token,web URL 以
|
||
<code>#token</code> 携带,前端对 API 请求加 header,daemon 校验并对预览路由豁免(CSP 已默认拒绝、且其内容本身来自工作区)。
|
||
这同时关闭了与本功能无关的存量攻击面(任何本机网页可直接调用无鉴权的 127.0.0.1:7878)。</div>
|
||
<div class="tablewrap"><table>
|
||
<tr><th>威胁</th><th>后果</th><th>对策</th></tr>
|
||
<tr><td>提示注入控制的 HTML 调用 daemon API</td><td>读写任意文件 / 开会话执行命令(等价 RCE)</td><td>sandbox 无 allow-same-origin(opaque origin)+ <code>connect-src 'none'</code> + daemon token 三层</td></tr>
|
||
<tr><td>HTML 外联泄漏对话/工作区内容</td><td>数据经 img/fetch/表单发往外部域名</td><td>CSP default-src 'none'(img 仅 self/data/blob、form-action 'none'、connect 'none');CDN 走显式 allowlist</td></tr>
|
||
<tr><td>伪造宿主 UI(假登录框/假确认弹窗)</td><td>用户误信 artifact 内容为原生界面</td><td>外壳常显「沙箱预览」标识与来源路径;禁全屏 API;外链必经宿主确认</td></tr>
|
||
<tr><td>postMessage 伪造</td><td>其他 frame / 页面冒充 artifact 触发宿主动作</td><td>实例 token + event.source 校验 + zod 校验消息 schema + follow-up 永远停在草稿</td></tr>
|
||
<tr><td>资源滥用(巨型 HTML、死循环、一轮 N 个 artifact)</td><td>页面卡死、内存膨胀</td><td>大小上限(如 2 MB,超出仅源码模式)、点击才实例化、同屏最多 1 个活动 iframe(分屏内)、lazy + 离屏卸载</td></tr>
|
||
<tr><td>预览路由路径穿越</td><td>读出工作区之外的文件</td><td>复用 fs 路由既有的根目录约束与规范化(实现时验证用例:<code>../</code>、symlink、绝对路径)</td></tr>
|
||
</table></div>
|
||
|
||
<h2 id="s7">7. 模块改动清单</h2>
|
||
<div class="tablewrap"><table>
|
||
<tr><th>位置</th><th>改动</th></tr>
|
||
<tr><td><code>packages/daemon/src/routes/fs.ts</code></td><td>新增 <code>:preview</code> GET 路由(CSP/安全头 + 注入桥脚本);daemon 启动 token 中间件(独立小改动)</td></tr>
|
||
<tr><td><code>apps/kimi-web/src/types.ts</code></td><td><code>TurnBlock</code> 增加 <code>{ kind: 'artifact'; artifact: ArtifactView }</code>;<code>ArtifactView = { source: 'file' | 'inline' | 'url'; title; path?; html?; url?; size }</code></td></tr>
|
||
<tr><td><code>apps/kimi-web/src/composables/messagesToTurns.ts</code></td><td>派生 artifact 块:html 写盘工具 → file artifact;≥30 行 ```html 代码块 → inline artifact(保持原始顺序,不 hoist)</td></tr>
|
||
<tr><td><code>apps/kimi-web/src/components/ArtifactFrame.vue</code>(新)</td><td>统一沙箱 iframe:三种投递、骨架/错误态、postMessage 桥、高度管理</td></tr>
|
||
<tr><td><code>ArtifactBlock.vue</code>(新)/ <code>ChatPane.vue</code></td><td>聊天卡片外壳;ChatPane 渲染第四种 block</td></tr>
|
||
<tr><td><code>ConversationPane.vue</code></td><td>右侧分屏 artifact 舞台(与 files 分栏布局共享);移动端全屏 sheet</td></tr>
|
||
<tr><td><code>FilePreview.vue</code></td><td><code>html</code> 类型:预览 | 源码 tab,复用 ArtifactFrame</td></tr>
|
||
<tr><td><code>TasksPane.vue</code> / 任务卡片</td><td>带端口的后台任务显示「预览」入口(URL artifact)</td></tr>
|
||
<tr><td>HTML 生成规范(用户侧 CLAUDE.md / 系统提示)</td><td>补充沙箱约束说明(无 localStorage、无外联、可选 window.kimi 桥 API)</td></tr>
|
||
</table></div>
|
||
|
||
<h2 id="s8">8. 关键边界与降级</h2>
|
||
<ul>
|
||
<li><b>流式</b>:代码块未闭合 / 文件正在连续 edit 时显示骨架,settle 后渲染;刷新按钮手动重载。</li>
|
||
<li><b>大文件</b>:>2 MB 仅提供源码 + 新窗口打开(新窗口走预览路由不受限)。</li>
|
||
<li><b>opaque origin 的 API 限制</b>:localStorage/sessionStorage 抛异常、无 cookie——写进生成规范,让模型用内存状态。</li>
|
||
<li><b>历史会话</b>:派生逻辑对历史消息同样生效,旧会话里的 HTML 工具调用自动获得预览入口。</li>
|
||
<li><b>主题</b>:iframe 内容不继承应用暗色主题;外壳提示由内容自管(自包含 HTML 规范已要求移动端可读,同理可建议支持 prefers-color-scheme)。</li>
|
||
<li><b>嵌入被拒(URL artifact)</b>:onload 探测失败 → 新标签页按钮降级。</li>
|
||
</ul>
|
||
|
||
<h2 id="s9">9. 调研依据(两份输入报告的来源汇总)</h2>
|
||
<ul style="font-size:13.5px">
|
||
<li>Claude Artifacts 沙箱与 CSP 行为:simonwillison.net/tags/claude-artifacts;support.claude.com(artifacts 帮助页)</li>
|
||
<li>Open WebUI:iframe srcdoc + sandbox="allow-scripts" + IFRAME_CSP(commit 3bba1c2);其「同源放行」开关为反面教训</li>
|
||
<li>LibreChat:Sandpack 预览 + 自托管 bundler + static-browser-server(每预览独立 origin)——多文件浏览器内打包路线的成本参照</li>
|
||
<li>bolt.new / StackBlitz WebContainers:浏览器内 Node 运行时;本地 daemon 架构下由真实 dev server 替代</li>
|
||
<li>v0 Sandbox:每会话隔离 VM 的 full app preview 形态参照</li>
|
||
<li>MCP Apps(官方扩展)/ mcp-ui:sandboxed iframe + JSON-RPC over postMessage;本设计交互桥的协议对齐目标</li>
|
||
<li>OpenAI Apps SDK:model-visible 内容与组件私有数据(_meta)分层的设计参考</li>
|
||
<li>MDN:iframe sandbox、CSP、postMessage、srcdoc 语义</li>
|
||
</ul>
|
||
</main>
|
||
|
||
<button id="tocbtn">目录</button>
|
||
<nav id="toc">
|
||
<a href="#s1">1. 范围</a>
|
||
<a href="#s2">2. 产品形态</a>
|
||
<a href="#s3">3. 架构与数据流</a>
|
||
<a href="#s4">4. 渲染机制</a>
|
||
<a href="#s5">5. postMessage 交互桥</a>
|
||
<a href="#s6">6. 安全模型</a>
|
||
<a href="#s7">7. 模块改动清单</a>
|
||
<a href="#s8">8. 边界与降级</a>
|
||
<a href="#s9">9. 调研依据</a>
|
||
</nav>
|
||
<script>
|
||
const btn = document.getElementById('tocbtn');
|
||
const toc = document.getElementById('toc');
|
||
btn.addEventListener('click', () => {
|
||
toc.style.display = toc.style.display === 'block' ? 'none' : 'block';
|
||
});
|
||
toc.addEventListener('click', () => { toc.style.display = 'none'; });
|
||
</script>
|
||
</body>
|
||
</html>
|