跳到主要内容

OpenAI Apps SDK Examples — 架构与原理

30 秒导读: 这是 OpenAI 官方的「Apps SDK 示例画廊」。它演示一件事:让 ChatGPT 在回答里嵌入一个真正的 React 小程序(叫 widget),比如一张披萨地图、一个购物车。底层靠 MCP(Model Context Protocol)——你的服务器把 widget 当成「工具」暴露,ChatGPT 调用工具时拿到一段 HTML 并在沙箱 iframe 里渲染,widget 再通过注入的 window.openai 对象和宿主双向通信。

1. 这是什么(零基础也能懂)

一句话定义: 一组可运行的样例,教你把「会渲染 UI 的工具」接进 ChatGPT。

解决什么问题 / 给谁用。 假设你做了个找披萨店的 API。普通做法是接成一个 LLM 工具,模型调用后把结果念成一段文字。但「附近 12 家店」用文字念很烂——用户想要的是一张能点、能滑、能加购物车的地图。Apps SDK 让你把这张地图直接塞进 ChatGPT 的对话气泡里。给谁用:想给 ChatGPT 加「带界面的连接器(connector)」的开发者。

它能做什么:

  • 把一个或多个 React 组件打包成可被 ChatGPT 渲染的 widget。
  • 用 MCP server(Node 或 Python 任选)把这些 widget 声明成工具。
  • widget 在沙箱里能读宿主状态(主题、入参、工具输出)、能回写自己的状态、能再调用别的工具、能让对话发后续消息。
  • 支持 OAuth:某些工具要登录才能用。

用起来什么样。 开发者跑三件事,然后在 ChatGPT 里问一句话:

pnpm run build # 把 src/ 里每个组件打成 assets/<name>.html
pnpm run serve # 静态服务器,端口 4444,带 CORS
cd pizzaz_server_node && pnpm start # MCP server,端口 8000

接着在 ChatGPT 里加这个连接器,问「What are the best pizzas in town?」——模型选中 pizza-map 工具,对话里就长出一张披萨地图。(README:78-200 行附近的运行说明)

一句话直觉 / 类比。 把它想成给聊天框装了浏览器插件位:模型决定「该放哪个小程序」,你的服务器提供「小程序的 HTML」,ChatGPT 是那个安全地把小程序裱在对话里、还递给它一个 window.openai 遥控器的宿主。

2. 顶层全景(它大概怎么转)

这套系统有三个角色,跨两台「机器」运行:你的 MCP server、ChatGPT 这个 host、以及跑在沙箱 iframe 里的 widget

怎么读下面这张图: 从左到右是一次「用户问问题 → 渲染出 widget」的数据流;虚线是 widget 渲染之后才会发生的回向调用。

用户在 ChatGPT 里问问题


┌─────────────────┐ ① list_tools / list_resources
│ ChatGPT host │ ───────────────────────────────►┌──────────────────┐
│ (模型 + 沙箱) │ ② call_tool(name, args) │ 你的 MCP server │
│ │ ───────────────────────────────►│ (Node 或 Python) │
│ │ ③ 返回 structuredContent │ │
│ │ ◄───────────────────────────────│ _meta 指向 │
│ │ ④ read_resource(模板 URI) │ ui://widget/... │
│ │ ◄──── HTML(text/html+skybridge)─│ │
└────────┬────────┘ └──────────────────┘
│ ⑤ 把 HTML 裱进沙箱 iframe,注入 window.openai

┌─────────────────┐
│ widget (iframe)│ ⑥ 读 toolOutput/theme/...,渲染 React
│ React + window │ · · · · · · · · · · · · · · · · ► callTool / setWidgetState
│ .openai │ (回向:再调工具、回写状态、发后续消息)
└─────────────────┘

部件一句话职责:

部件干什么在哪
MCP server声明工具、把 widget HTML 当 resource 提供、处理 call_toolpizzaz_server_node/src/server.ts*_python/main.py
widget 源码一个 React 应用,从 window.openai 取数据并渲染src/<name>/index.tsx
build 脚本把每个组件打成自包含的 <name>.htmlbuild-all.mts
window.openai 类型 + hookwidget 侧读宿主状态 / 调宿主 API 的胶水src/types.tssrc/use-openai-global.ts
静态服务器把打好的 JS/CSS/HTML 发给 ChatGPT 加载pnpm run serve(serve assets)

主线走一遍(高层):

  1. ChatGPT 连上你的 server,拉 list_tools —— 每个工具的 _meta 里写着「我渲染哪个 widget 模板」。
  2. 用户提问,模型选中某工具,发 call_tool
  3. server 返回 structuredContent(给 widget 的数据)+ 文本(给模型读的)。
  4. ChatGPT 按工具声明的模板 URI 去 read_resource 拿 widget 的 HTML。
  5. ChatGPT 在沙箱 iframe 里渲染 HTML,并注入 window.openai,把第 3 步的数据塞进 window.openai.toolOutput
  6. widget 跑起来,可随时回头调 callTool / setWidgetState 等。

3. 阅读地图(建议顺序)

这五章由浅入深,建议顺序读:

  1. 01-server-contract.md — 先懂「服务器侧怎么把 widget 声明成工具」。这是整套协议的核心契约:_meta 四件套、text/html+skybridge、call_tool 的返回结构。最重要,先读。
  2. 02-widget-runtime.md — 再懂「widget 侧怎么拿数据、怎么反向调宿主」。window.openai 全局面 + useOpenAiGlobal 订阅机制 + 一整排 host helper。
  3. 03-state-sync.md — 进阶:状态怎么跨对话轮次不丢。widgetState + widgetSessionId 的合流模型(购物车例子)。
  4. 04-build-pipeline.md — 工程细节:build-all.mts 怎么把每个组件打成一个自包含、可被任意 host 加载的 HTML 壳。
  5. 05-auth.md — 认证:securitySchemes + RFC 9728 元数据 + WWW-Authenticate 挑战,演示「免登录 + 需登录」混合工具。

如果你只想知道「协议长什么样」,读 §1 + 01 章足矣。

4. 巧妙之处(可带走的)

  • Node 与 Python 两份 server 是逐字镜像。 同样的 widget 列表、同样的 _meta、同样的 call_tool 返回——只是一个用 TS SDK、一个用 FastMCP。读懂一个就懂两个。见 01 章对照表。
  • widget 是「纯消费者」。 它从不主动连服务器;一切数据由 host 注入 window.openai,一切回调由 host 转发。这让 widget 可以在任何实现了这套接口的 host 里跑。
  • 状态跨轮次靠一个字符串 ID。 不需要数据库,host 用 widgetSessionId 把上一轮的 widgetState 喂回来(见 03 章)。

5. 边界与局限(诚实)

  • 这是示例,不是生产框架。 购物车的状态存在内存 dict 里、还被故意写成不持久化(shopping_cart_python/main.py:200 把真正的 carts[cart_id] 注释掉了)。
  • window.openai 的宿主实现不在本仓库。 仓库只有消费侧的类型(src/types.ts)和订阅 hook;真正注入 window.openai、跑 iframe 沙箱的代码在 ChatGPT 的 web-sandbox(src/types.ts:27 注释写明「currently copied from types.ts in chatgpt/web-sandbox」)。所以本文涉及 host 行为处会标 (inferred)。
  • 传输用 SSE 或 streamable HTTP。 Node 例子用旧的 SSE 双端点(/mcp + /mcp/messages),Python 例子用较新的 streamable HTTP。

6. 代码地图(导航索引)

主题文件路径符号名
Node server 主体pizzaz_server_node/src/server.tscreatePizzazServerwidgetDescriptorMeta
Python server 主体pizzaz_server_python/main.py_list_tools_call_tool_request_tool_meta
widget→宿主类型契约src/types.tsOpenAiGlobalsAPISET_GLOBALS_EVENT_TYPE
订阅宿主状态src/use-openai-global.tsuseOpenAiGlobalreadOpenAiGlobal
回写 widget 状态src/use-widget-state.tsuseWidgetState
全 API 演示 widgetsrc/kitchen-sink-lite/kitchen-sink-lite.tsxKitchenSinkLite
跨轮次状态shopping_cart_python/main.py_handle_call_tool_get_or_create_cart
构建管线build-all.mtswrapEntryPlugintargets
认证authenticated_server_python/main.py_get_bearer_token_from_request_oauth_error_result