CopilotKit 开放生成式 UI(高级):在沙箱 iframe 中调用宿主端沙箱函数
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
导读
本文围绕 CopilotKit 与 CrewAI Conversational Flows 集成项目中的open-gen-ui-advanced(开放生成式 UI 高级版)Demo 展开,讲解"完全开放"的生成式 UI 模式:Agent 直接生成一段 HTML/CSS/JavaScript 并渲染进沙箱 iframe,同时通过宿主端注册的沙箱函数(sandbox functions)与页面本体完成双向交互。读完本文,你将掌握openGenerativeUI运行时标志与前端 Provider 配置、宿主沙箱函数的注册与调用协议(Websandbox.connection.remote.*),以及如何用 E2E 测试验证"iframe 内点击 → 宿主端执行 → 结果回写 iframe"的完整链路。
一、场景定位:从"受约束渲染"到"完全开放生成"
在 CopilotKit 的生成式 UI 体系中,open-gen-ui-advanced属于"开放生成式 UI"(Open Generative UI)一档。与基于预定义组件库或 A2UI Schema 的受约束模式不同,这种模式下的 Agent 拥有极大自由度:它直接产出可运行的 HTML/CSS/JavaScript,前端将其安全地渲染在一个沙箱 iframe 中。而"高级"(Advanced)版本在open-gen-ui基础之上更进一步——Agent 生成的 iframe 内 UI 可以反向调用宿主页面注册的沙箱函数,形成"宿主 ↔ 沙箱"的双向桥接。
该能力在 manifest.yaml 中被登记为独立 featureopen-gen-ui-advanced,其描述为 "Agent-authored UI that invokes host sandbox functions from inside the iframe"(Agent 编写的 UI 从 iframe 内部调用宿主沙箱函数),highlight 文件为:
- page.tsx
- sandbox-functions.ts
- copilotkit-ogui 路由
二、QA 验收清单与整体数据流
仓库中的 QA 文档 open-gen-ui-advanced.md 给出了该功能的手工验收步骤:
- 访问
/demos/open-gen-ui-advanced页面; - 点击 "Calculator (calls evaluateExpression)" 建议词;
- 验证沙箱化计算器 UI 在 iframe 中渲染;
- 操作计算器(如计算
3 + 4.5),验证结果由宿主端evaluateExpression更新; - 点击 "Ping the host (calls notifyHost)",验证按钮触发远程调用并在沙箱中显示时间戳。
背后对应的完整数据流为:
- 用户点击建议词 → 聊天组件将消息发送给 Agent(CrewAI Conversational Flow);
- Agent 流式返回一个
generateSandboxedUi工具调用,其中携带 HTML/CSS 以及通过jsFunctions声明的 in-iframe 点击处理器; - 运行时的
OpenGenerativeUIMiddleware(由openGenerativeUI: { agents: [...] }开启)将该工具调用流转换为open-generative-ui活动事件; - 前端
CopilotKitProvider 传入openGenerativeUI配置后,内置的OpenGenerativeUIActivityRenderer将 Agent 编写的 HTML/CSS 挂载进带sandbox属性的 iframe; - iframe 内 UI 通过
Websandbox.connection.remote.<name>(args)调用宿主端沙箱函数,宿主执行完成后把返回值回传给 iframe 内调用方。
这一流程的代码级注释记录在 page.tsx 的头部注释中。
三、后端运行时:为开放生成式 UI 单独开一条路由
openGenerativeUI是运行时(Runtime)级别的标志,一旦开启会在 probe 响应中全局设置openGenerativeUIEnabled: true,这会影响默认运行时上的其他按 Demo 工具注册。因此仓库为开放生成式 UI Demo 专门隔离了一条运行时路由 copilotkit-ogui/route.ts。
该路由的关键配置如下:
import { CopilotRuntime, createCopilotRuntimeHandler } from "@copilotkit/runtime/v2"; import { HttpAgent } from "@ag-ui/client"; const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000"; function createAgent() { return new HttpAgent({ url: `${AGENT_URL}/conversational_flows/frontend-tools`, }); } const agents: Record<string, AbstractAgent> = { "open-gen-ui": createAgent(), "open-gen-ui-advanced": createAgent(), }; export const POST = async (req: NextRequest) => { const copilotHandler = createCopilotRuntimeHandler({ runtime: new CopilotRuntime({ agents, openGenerativeUI: { agents: ["open-gen-ui", "open-gen-ui-advanced"], }, }), basePath: "/api/copilotkit-ogui", mode: "single-route", }); return await copilotHandler(req); };要点说明:
openGenerativeUI.agents数组用于声明哪些 Agent 允许产出开放生成式 UI;这里open-gen-ui与open-gen-ui-advanced两个 Agent 都映射到同一个 CrewAI 端点/conversational_flows/frontend-tools;mode: "single-route"让该路由独立承载所有运行时请求,避免与默认路由互相干扰;- 从源码结构可以推断,运行时中
OpenGenerativeUIMiddleware负责把generateSandboxedUi工具调用流转换为open-generative-ui活动事件,供前端内置渲染器消费。
在 Python 侧,/frontend-tools端点由FrontendToolFlow提供,conversational_flows.py 中可以看到"frontend-tools": _conversational_type(FrontendToolFlow)的映射注册;Python 测试 test_specialized_flows.py 中也登记了"open-gen-ui-advanced": "/frontend-tools"的对应关系。
四、前端集成:Provider 上的openGenerativeUI与建议词
前端页面 page.tsx 通过CopilotKitProvider 的openGenerativeUI属性把沙箱函数数组交给内置渲染器:
"use client"; import { CopilotKit, CopilotChat, useConfigureSuggestions } from "@copilotkit/react-core/v2"; import { openGenUiSandboxFunctions } from "./sandbox-functions"; import { openGenUiSuggestions } from "./suggestions"; export default function OpenGenUiAdvancedDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit-ogui" agent="open-gen-ui-advanced" openGenerativeUI={{ sandboxFunctions: openGenUiSandboxFunctions }} > <div className="flex justify-center items-center h-screen w-full"> <div className="h-full w-full max-w-4xl"> <Chat /> </div> </div> </CopilotKit> ); } function Chat() { useConfigureSuggestions({ suggestions: openGenUiSuggestions, available: "always", }); return ( <div className="flex h-full w-full flex-col p-3"> <CopilotChat agentId="open-gen-ui-advanced" className="flex-1 rounded-2xl" /> </div> ); }三个关键点:
runtimeUrl="/api/copilotkit-ogui"指向第三节专门隔离的运行时路由;openGenerativeUI={{ sandboxFunctions: [...] }}将宿主端沙箱函数数组注入 Provider,内置的OpenGenerativeUIActivityRenderer会把这些函数作为可在 iframe 内调用的远端对象挂载进沙箱;useConfigureSuggestions用于在聊天输入区常驻建议词(available: "always")。
建议词定义在 suggestions.ts,共三条:
export const openGenUiSuggestions = [ { title: "Calculator", message: "Calculator (calls evaluateExpression)" }, { title: "Ping the host", message: "Ping the host (calls notifyHost)" }, { title: "Inline expression evaluator", message: "Inline expression evaluator" }, ];每条建议词的message字符串会原样作为 Agent 提示词,同时也作为 aimock 固定夹具(fixture)的键;在showcase/aimock/d5-all.json中存在与这些消息字符串逐字对齐的夹具条目,以保证每次点击建议词都会触发确定性的generateSandboxedUi工具调用,而不是被通用兜底夹具吸收。
五、宿主沙箱函数:定义、安全校验与调用协议
沙箱函数是"高级"模式的灵魂,定义在 sandbox-functions.ts。每个函数包含name、description、parameters(Zod Schema)和handler四个部分;名称、描述与由 Zod 推导出的 JSON Schema 会被注入 Agent 上下文,让 LLM 在生成 HTML/JS 时知道有哪些桥可用。每个 handler 运行在宿主页面上,其返回值会被 iframe 内的调用方 await。
5.1 evaluateExpression:宿主端安全求值
{ name: "evaluateExpression", description: "Safely evaluate a basic arithmetic expression on the host page and return the numeric result. " + "Supports +, -, *, /, parentheses, and decimal numbers. " + "Use this from inside a calculator or spreadsheet UI.", parameters: z.object({ expression: z.string().describe("An arithmetic expression, e.g. '12 * (3 + 4.5)'"), }), handler: async ({ expression }: { expression: string }) => { if (!/^[\d+\-*/().\s]+$/.test(expression)) { return { ok: false, error: "Unsupported characters in expression." }; } try { const value = Function(`"use strict"; return (${expression});`)(); if (typeof value !== "number" || !Number.isFinite(value)) { return { ok: false, error: "Not a finite number." }; } console.log("[open-gen-ui/advanced] evaluateExpression", expression, "=", value); return { ok: true, value }; } catch (err) { return { ok: false, error: err instanceof Error ? err.message : String(err) }; } }, }安全实现要点:
- 先用正则
/^[\d+\-*/().\s]+$/白名单过滤,仅允许数字、四则运算符、括号、小数点和空白,任何含标识符或可疑字符的表达式直接返回ok: false,绝不执行任意 JS; - 通过
Function("use strict"; return (...))求值,并对结果做typeof === "number"与Number.isFinite双重校验; - 返回统一结构
{ ok, value }或{ ok: false, error },便于 iframe 内 UI 消费。
5.2 notifyHost:从沙箱向宿主发送通知
{ name: "notifyHost", description: "Send a short notification message from the sandboxed UI to the host page. " + "The host logs the message and returns a confirmation object.", parameters: z.object({ message: z.string().describe("A short status message."), }), handler: async ({ message }: { message: string }) => { console.log("[open-gen-ui/advanced] notifyHost:", message); return { ok: true, receivedAt: new Date().toISOString(), message }; }, }该函数演示了"沙箱 → 宿主"的通知模式:宿主仅记录消息并返回receivedAt时间戳(ISO 字符串),iframe 内 UI 拿到这个确认对象后即可把时间戳渲染出来——这正好对应 QA 清单第 5 步"时间戳显示在沙箱中"。
5.3 调用协议
两个 handler 的函数头注释明确了调用方式:Agent 生成的 iframe 内脚本通过Websandbox.connection.remote.<name>(args)调用这些宿主函数(源码见 sandbox-functions.ts)。由于函数名、描述和 Schema 都被注入 Agent 上下文,LLM 生成 HTML 时会把按钮的点击事件正确绑定到对应远端调用上。从设计上看,这类函数的接口应保持"小而明确"——它们就是 Demo 的"应用侧工具"(app-side tools),供沙箱生成的 UI 调用。
六、E2E 验证:如何证明"iframe 内点击 → 宿主执行 → 结果回写"
仓库为高级开放生成式 UI 提供了完整的 Playwright 测试 open-gen-ui-advanced.spec.ts,这也是理解整条链路的最好"活文档"。测试分为两层断言:
6.1 冒烟层(SMOKE)
每个提示词(Calculator / Ping the host / Inline expression evaluator)都断言产生一个srcdoc非空的iframe[sandbox*="allow-scripts"],确认 open-gen-ui 管线成功挂载了内容。测试通过frameLocator与sandbox="allow-scripts"iframe 交互(依赖 CDP,不受 null origin 限制)。
6.2 往返层(ROUND-TRIP)
驱动 iframe 内控件,断言宿主端 handler 确实执行(通过宿主 console 中稳定的[open-gen-ui/advanced]前缀日志捕获)且 iframe 内输出元素被宿主返回值更新:
- Ping 测试:点击 iframe 内
#hi按钮 → 宿主notifyHost被触发 → 断言 iframe#out元素包含 "host replied at"; - 内联表达式求值测试:向
#in输入框pressSequentially("2+2")(注释说明:fill()在allow-scripts沙箱 iframe 内可能静默失效,按键序列总是可达)→ 点击#go→ 断言宿主日志出现= 4且#out显示 "= 4"; - 计算器测试:依次点击数字
2、运算符+、数字3按钮再点=(#eq)→ 断言宿主日志出现= 5且显示区#d文本为 "5"。
测试头部注释还透露了两个工程细节:
- 驱动采用"textarea 填充 + 回车"而非点击建议词 pill,因为页面存在 EmptyState 与 SuggestionBar 两套 chip 表面(依赖
messages.length挂载),pill 点击存在时序抖动;两条路径最终都会触发同一个runAgent; - d5-all.json 夹具在 feature-parity.json 之前加载,其 first-match-wins 顺序能命中精确的建议词消息,避免落入会返回通用问候语的
userMessage: "hi"兜底夹具。
6.3 测试要点速查
| 测试项 | 操作路径 | 断言 |
|---|---|---|
| 页面加载 | 访问/demos/open-gen-ui-advanced | 3 个建议词 pill 可见 |
| 冒烟:三条提示词 | 填充输入框并回车 | iframe[sandbox*="allow-scripts"]可见且srcdoc/src非空 |
| Ping 往返 | iframe 内点击#hi | 宿主日志含notifyHost,#out含 "host replied at" |
| 内联求值往返 | #in输入2+2,点击#go | 宿主日志= 4,#out含 "= 4" |
| 计算器往返 | 依次点2、+、3、#eq | 宿主日志= 5,#d文本为 "5" |
七、与基础版(open-gen-ui)的对照
基础版 open-gen-ui 页面 与高级版共用同一条/api/copilotkit-ogui运行时路由(两者都列在openGenerativeUI.agents中)。两者的本质区别在于:
- 基础版:Agent 只生成静态 HTML/CSS/可视化内容,渲染进沙箱 iframe 即可,不涉及宿主回调;其 design-skill.ts 通过向 Agent 注入视觉设计技能(如"像教科书插图或可探索解释一样")来约束产出质量;
- 高级版:在基础能力之上注册宿主沙箱函数,让 iframe 内 UI 拥有"对外部世界的副作用能力"(求值、通知等),把沙箱从"展示容器"升级为"可交互应用宿主"。
两者共同的约束是 iframe 安全模型:Agent 产出的 HTML 不包含<form>、不使用type='submit',事件一律通过addEventListener绑定(见 suggestions.ts 注释),这些约束会写进系统提示词中。
八、运行与验证方式
该 Demo 位于 CrewAI Conversational Flows 集成 showcase 中(manifest.yaml 登记的 slug 为crewai-conversational-flows,copilotkit_version: 2.0.0,后端语言 Python,Agent 端点为/conversational_flows/frontend-tools)。典型运行方式为:
- 启动 Python 后端(CrewAI Conversational Flows,默认监听
http://localhost:8000,可用AGENT_URL环境变量覆盖); - 启动 Next.js 前端,访问
/demos/open-gen-ui-advanced; - 手工按 QA 清单验证,或运行
tests/e2e/open-gen-ui-advanced.spec.ts进行自动化回归。
需要说明的适用前提:openGenerativeUI为运行时级全局标志,开启会影响 probe 响应,因此仓库将其隔离在独立路由/api/copilotkit-ogui上,避免干扰默认运行时中按 Demo 的工具注册——在自己项目中接入该能力时,建议沿用同样的路由隔离策略。
结语
open-gen-ui-advanced展示了生成式 UI 的一个关键进阶:Agent 自由生成界面,而宿主通过注册"沙箱函数"为其提供受控的能力边界。evaluateExpression的白名单正则与有限性校验、notifyHost的简单确认回执,加上 E2E 测试对"宿主日志 + iframe 输出"的双重断言,共同勾勒出一条清晰、可验证、可复用的"开放生成 + 沙箱桥接"实践路径。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考