当前位置: 首页 > news >正文

全网首发 OpenAI Apps SDK 使用教程

OpenAI Apps SDK 教程(附pizza点单案例)

Apps SDK 是 OpenAI 于 2025 年 10 月 10 日 在 DevDay 开发者大会 上正式发布的一套全新开发框架。

它为开发者提供了在 ChatGPT 平台内构建与运行 Apps 的标准化途径,使得第三方应用可以嵌入 ChatGPT 的对话界面,拥有独立的前端交互体验与可视化组件。

这或许意味着 ChatGPT 内 应用生态商业化 的开端,用户不仅能在对话中使用第三方 App,还能直接通过这些 App 完成消费与支付,实现资金与流量的闭环。

一、亮点: Apps inside ChatGPT

第三方应用可直接集成到ChatGPT对话界面中

用户可在ChatGPT内直接与应用可视化交互,无需跳转

提供基于MCP标准构建的Apps SDK供开发者使用

Apps SDK 简介

Apps SDK 基于 MCP 标准,扩展了 MCP 以使开发者能够设计应用逻辑和界面。APP 存在于 ChatGPT 内部,用于扩展用户的功能,同时又不会打断对话流程,并通过轻量级卡片、轮播、全屏视图和其他显示模式无缝集成到 ChatGPT 界面,同时保持其清晰度、可信度和语音功能。

APP 作为 MCP 扩展节点,每个第三方 App 的后端可以看作是一个 MCP 服务器,负责对外暴露能力。前端 UI 嵌入 ChatGPT 对话内, 并可以与 MCP 服务器双向通信。

想第一时间掌握 OpenAI Apps SDK 的实战用法,以及更多 MCP、Agent、RAG、多模态 应用落地案例
来 赋范大模型技术社区,这里不仅有 Apps SDK 全流程实操拆解,还有持续更新的 Agent / RAG / 多模态 应用落地实战案例,带你深入学习。

二、Apps SDK 教程

可使用node.js或python作为后端

下面将通过两部分来演示Apps SDK的使用:

  • 官方示例项目(python)
  • 自建两个简单的示例工具
官方示例项目演示

这里使用官方提供的示例项目 openai-apps-sdk-examples 中的 pizzaz_server_python 项目来演示。

克隆项目到本地,然后安装依赖并本地运行前端项目。

若没有安装pnpm,先安装pnpm

npm install -g pnpm@latest-10    
# 克隆项目到本地
git clone https://github.com/openai/openai-apps-sdk-examples.git# 安装依赖并本地运行前端项目
pnpm installpnpm run buildpnpm run dev
创建并激活 Python 虚拟环境,安装pizzaz_server的依赖并启动服务器。
# 创建虚拟环境
python -m venv .venv
# 激活虚拟环境
.venv\Scripts\Activate
# 安装pizzaz_server的依赖
pip install -r pizzaz_server_python/requirements.txt
# 启动服务器 这里的8000端口作为后面ngrok暴露的端口
uvicorn pizzaz_server_python.main:app --port 8000
安装ngrok(若有ngrok,跳到下一步)

在Microsoft Store中下载ngrok
下载ngrok
到 官网 注册ngrok账号,获取token,运行官网的配置token命令
注册ngrok账号并获取token

ngrok config add-authtoken <token>
通过ngrok将本地服务器暴露到公网,获取临时URL
ngrok http 8000

获取url

启用开发者模式

在ChatGPT中 设置->应用与连接器->高级设置->启用开发者模式
启用开发者模式

创建连接器

将ngrok提供的临时URL拼接上/mcp,在ChatGPT应用与连接器中创建一个新的连接器,名称填写pizza,MCP 服务器 URL填写拼接后的URL。
创建连接器

测试连接器

现在,我们可以在ChatGPT中提问,例如:pizza 奶酪披萨地图。
模型会返回可视化pizza地图,用户可以直接在ChatGPT中查看。
pizza地图

想第一时间掌握 OpenAI Apps SDK 的实战用法,以及更多 MCP、Agent、RAG、多模态 应用落地案例
来 赋范大模型技术社区,这里不仅有 Apps SDK 全流程实操拆解,还有持续更新的 Agent / RAG / 多模态 应用落地实战案例,带你深入学习。

新建自定义示例

接下来我们新建两个简单的示例来演示Apps SDK的使用。

新建 whoami 工具

/src下新建文件夹whoami,并新建文件index.jsx

// 示例:whoami 工具 最基础的工具示例
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
//这里的App组件将作为chatgpt内的whoami工具的前端界面
function App() {const [surprise, setSurprise] = useState(false);return (<div className="w-full h-full flex items-center justify-center p-6"><div className="rounded-2xl border border-black/10 dark:border-white/10 p-8 shadow bg-white text-black text-center"><h1 className="text-2xl font-semibold">我是 pizza 助手</h1><p className="mt-2 text-sm text-black/70">在这里为你提供披萨相关帮助 🍕</p><buttonclassName="mt-4 inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-black text-white hover:bg-black/80 active:scale-[0.99] transition"onClick={() => setSurprise(true)}>点击我有惊喜</button>{surprise && (<div className="mt-4 text-5xl select-none" aria-label="烟花"><span className="inline-block animate-bounce">🎆</span></div>)}</div></div>);
}
// 将组件绑定到 pizzaz-whoami-root 节点 
createRoot(document.getElementById("pizzaz-whoami-root")).render(<App />);
新建 order 工具

/src下新建文件夹pizzaz-order,并新建文件index.jsx
包含点单页面和模拟付款页面。通过监听 toolOutputtoolInput 获取 ChatGPT 给的点单信息并反映到购物车。

// 示例:order 工具 含MCP交互的工具示例
import { useMemo, useState, useEffect, useRef } from "react";
import { createRoot } from "react-dom/client";
import { HashRouter, Routes, Route, useNavigate, useLocation } from "react-router-dom";
import "../index.css";
import { useOpenAiGlobal } from "../use-openai-global";// 示例披萨数据(包含id、名称、图片、价格)
const PIZZAS = [{id: 1,name: "玛格丽塔披萨",price: 10,image:"https://tse1.mm.bing.net/th/id/OIP.g4QYOOmFvL-Kxpk4AuI3-gHaE7?cb=12&rs=1&pid=ImgDetMain&o=7&rm=3",},{id: 2,name: "夏威夷披萨",price: 15,image:"https://tse4.mm.bing.net/th/id/OIP.veSCe42vltnOTEhL8sPAsQHaLP?cb=12&rs=1&pid=ImgDetMain&o=7&rm=3",},{id: 3,name: "培根蘑菇披萨",price: 22,image:"https://tse3.mm.bing.net/th/id/OIP.8nCs6Gpm5ckETI-aRrePIwHaE8?cb=12&rs=1&pid=ImgDetMain&o=7&rm=3",},
];function formatCurrency(n) {return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY" }).format(n);
}function OrderPage() {const navigate = useNavigate();const [cart, setCart] = useState({});const seededRef = useRef(false);// 监听 toolOutput 与 toolInputconst toolOutput = useOpenAiGlobal("toolOutput");const toolInput = useOpenAiGlobal("toolInput");const mergedProps = useMemo(() => ({ ...(toolInput ?? {}), ...(toolOutput ?? {}) }), [toolInput, toolOutput]);const { orderItems = [] } = mergedProps;// 查找披萨const findPizzaByInput = (item) => {const name = item?.name;if (!name) return null;return PIZZAS.find((p) => p.name === name);};// 将初始条目注入购物车(chatgpt给的点单信息)useEffect(() => {if (seededRef.current) return;if (!Array.isArray(orderItems) || orderItems.length === 0) return;const next = {};for (const it of orderItems) {const pizza = findPizzaByInput(it);const qty = Number(it?.qty ?? it?.quantity ?? 0) || 0;if (pizza && qty > 0) {next[pizza.id] = (next[pizza.id] || 0) + qty;}}if (Object.keys(next).length > 0) {setCart((prev) => {const merged = { ...prev };for (const [id, q] of Object.entries(next)) {merged[id] = (merged[id] || 0) + q;}return merged;});seededRef.current = true;}}, [orderItems]);const items = useMemo(() => {return PIZZAS.filter((p) => cart[p.id]).map((p) => ({...p,qty: cart[p.id],lineTotal: p.price * cart[p.id],}));}, [cart]);const total = useMemo(() => items.reduce((sum, it) => sum + it.lineTotal, 0), [items]);const count = useMemo(() => items.reduce((sum, it) => sum + it.qty, 0), [items]);function addToCart(id) {setCart((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));}function goCheckout() {navigate("/checkout", { state: { items, total } });}return (<div className="min-h-screen bg-white text-gray-900"><header className="px-6 py-4 border-b"><h1 className="text-2xl font-bold">订购 Pizza</h1><p className="text-sm text-gray-500">选择你喜欢的披萨,加入购物车并结算</p></header><main className="px-6 py-6"><div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">{PIZZAS.map((p) => (<div key={p.id} className="rounded-lg border shadow-sm overflow-hidden"><div className="aspect-video bg-gray-100"><imgsrc={p.image}alt={p.name}className="w-full h-full object-cover"loading="lazy"/></div><div className="p-4 flex items-center justify-between"><div><div className="font-semibold">{p.name}</div><div className="text-sm text-gray-600">{formatCurrency(p.price)}</div></div><buttonclassName="inline-flex items-center rounded-md bg-orange-500 hover:bg-orange-600 text-white text-sm px-3 py-2"onClick={() => addToCart(p.id)}>加入购物车</button></div></div>))}</div></main>{/* 购物车汇总条 */}<div className="fixed left-0 right-0 bottom-0 border-t bg-white/95 backdrop-blur"><div className="mx-auto max-w-6xl px-6 py-3 flex items-center justify-between"><div className="text-sm text-gray-700">已选 {count} 件 · 合计 <span className="font-semibold">{formatCurrency(total)}</span></div><buttonclassName="inline-flex items-center rounded-md bg-green-600 hover:bg-green-700 text-white text-sm px-4 py-2 disabled:opacity-50"onClick={goCheckout}disabled={count === 0}>购物车结算</button></div></div></div>);
}function CheckoutPage() {const navigate = useNavigate();const location = useLocation();const items = location.state?.items || [];const total = location.state?.total || 0;function backToOrder() {navigate("/");}function payNow() {alert("已模拟付款,感谢你的订购!");}return (<div className="min-h-screen bg-white text-gray-900"><header className="px-6 py-4 border-b"><h1 className="text-2xl font-bold">付款页面</h1><p className="text-sm text-gray-500">确认订单并完成付款</p></header><main className="px-6 py-6 mx-auto max-w-3xl">{items.length === 0 ? (<div className="text-center text-gray-600">购物车为空<div className="mt-4"><buttonclassName="rounded-md bg-gray-800 hover:bg-gray-900 text-white px-4 py-2"onClick={backToOrder}>返回订购</button></div></div>) : (<div className="space-y-6"><div className="rounded-lg border overflow-hidden"><table className="w-full text-sm"><thead className="bg-gray-50"><tr><th className="py-2 px-3 text-left">商品</th><th className="py-2 px-3 text-right">数量</th><th className="py-2 px-3 text-right">小计</th></tr></thead><tbody>{items.map((it) => (<tr key={it.id} className="border-t"><td className="py-2 px-3">{it.name}</td><td className="py-2 px-3 text-right">{it.qty}</td><td className="py-2 px-3 text-right">{formatCurrency(it.lineTotal)}</td></tr>))}</tbody></table></div><div className="flex items-center justify-between"><div className="text-gray-700">总计:</div><div className="text-lg font-semibold">{formatCurrency(total)}</div></div><div className="flex items-center gap-3"><buttonclassName="rounded-md bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2"onClick={backToOrder}>返回订购</button><buttonclassName="rounded-md bg-green-600 hover:bg-green-700 text-white px-4 py-2"onClick={payNow}>前往付款</button></div></div>)}</main></div>);
}function RouterRoot() {return (<Routes><Route path="/" element={<OrderPage />} /><Route path="/checkout" element={<CheckoutPage />} /></Routes>);
}createRoot(document.getElementById("pizzaz-order-root")).render(<HashRouter><RouterRoot /></HashRouter>
);
新建 MCP服务端

新建openai-apps-sdk-examples\test_pizzaz_server\文件夹
添加 whoami和 order 工具,绑定 pizzaz-whoami-rootpizzaz-order-root 节点
定义 order 工具的输入参数 schema

# 概览:
# 该文件实现了一个基于 FastMCP 的 MCP 服务器,通过 HTTP+SSE 暴露 “带 UI 的工具”。
# ChatGPT Apps SDK 会根据返回的 _meta 信息加载对应的前端组件。
# 主要模块:
# 1) 定义 PizzazWidget 元信息(模板 URI、HTML、标题等)
# 2) 列出工具/资源/资源模板(供客户端发现)
# 3) 处理 ReadResource/CallTool 请求(返回 HTML 组件 + 文本 + 结构化数据)
# 4) 启动 Uvicorn 应用,提供 SSE 和消息端点from __future__ import annotationsfrom copy import deepcopy
from dataclasses import dataclass
from typing import Any, Dict, List
# - mcp.types/FastMCP 提供 MCP 与服务端封装
import mcp.types as types
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, ConfigDict, Field, ValidationError@dataclass(frozen=True)
class PizzazWidget:# Widget 元信息:每个组件的“工具描述”和其 HTML identifier: strtitle: strtemplate_uri: strinvoking: strinvoked: strhtml: strresponse_text: strwidgets: List[PizzazWidget] = [# Demo 小部件:用于生成工具与资源,引用持久化的 CSS/JS 资产,开发时可直接使用localhost:4444# whoami 小部件,显示“我是 pizza 助手”PizzazWidget(identifier="pizza-whoami",title="Who Am I",template_uri="ui://widget/pizza-whoami.html",invoking="Answering identity",invoked="Identity presented",html=("<div id=\"pizzaz-whoami-root\"></div>\n""<link rel=\"stylesheet\" href=\"http://localhost:4444/pizzaz-whoami.css\">\n""<script type=\"module\" src=\"http://localhost:4444/pizzaz-whoami.js\"></script>"),response_text="我是 pizza 助手",),# 披萨订购小部件PizzazWidget(identifier="pizza-order",title="Order Pizza",template_uri="ui://widget/pizza-order.html",invoking="Opening the order page",invoked="Order page ready",html=("<div id=\"pizzaz-order-root\"></div>\n""<link rel=\"stylesheet\" href=\"http://localhost:4444/pizzaz-order.css\">\n""<script type=\"module\" src=\"http://localhost:4444/pizzaz-order.js\"></script>"),response_text="打开订购页面,可选择披萨并结算",),
]# 资源的 MIME 类型:Skybridge HTML(供 Apps SDK 识别渲染)
MIME_TYPE = "text/html+skybridge"# 快速索引:按工具名或模板 URI 获取 Widget
WIDGETS_BY_ID: Dict[str, PizzazWidget] = {widget.identifier: widget for widget in widgets}
WIDGETS_BY_URI: Dict[str, PizzazWidget] = {widget.template_uri: widget for widget in widgets}class PizzaInput(BaseModel):"""Schema for pizza tools ( orderItems)."""# 订单条目列表,供 pizza-order 工具使用order_items: List[Dict[str, Any]] | None = Field(default=None,alias="orderItems",description=("Optional order items list for the order page. Each item can include ""id/name and qty or quantity."),)model_config = ConfigDict(populate_by_name=True, extra="forbid")# FastMCP 配置:与 Node 版路径一致,支持无状态 HTTP 检视
mcp = FastMCP(name="pizzaz-python",sse_path="/mcp",message_path="/mcp/messages",stateless_http=True,
)# 暴露给客户端的 JSON Schema
# 自定义结构化输入(在此项目中,定义orderItems来表示订单列表)
TOOL_INPUT_SCHEMA: Dict[str, Any] = {"type": "object","properties": {"orderItems": {"type": "array","description": "Optional list of items to seed the cart.","items": {"type": "object","properties": {"id": { "type": ["string", "null"] },"name": { "type": ["string", "null"] },"qty": { "type": ["integer", "null"] },"quantity": { "type": ["integer", "null"] },},"additionalProperties": True,},},},"required": [],"additionalProperties": False,
}def _resource_description(widget: PizzazWidget) -> str:# 资源描述文案return f"{widget.title} widget markup"def _tool_meta(widget: PizzazWidget) -> Dict[str, Any]:# Apps SDK 元数据:驱动 UI 渲染与状态文案return {"openai/outputTemplate": widget.template_uri,"openai/toolInvocation/invoking": widget.invoking,"openai/toolInvocation/invoked": widget.invoked,"openai/widgetAccessible": True,"openai/resultCanProduceWidget": True,"annotations": {"destructiveHint": False,"openWorldHint": False,"readOnlyHint": True,}}def _embedded_widget_resource(widget: PizzazWidget) -> types.EmbeddedResource:# 将 HTML 外壳封装为嵌入资源返回return types.EmbeddedResource(type="resource",resource=types.TextResourceContents(uri=widget.template_uri,mimeType=MIME_TYPE,text=widget.html,title=widget.title,),)@mcp._mcp_server.list_tools()
async def _list_tools() -> List[types.Tool]:# 列出可用工具:每个 widget 为一个工具项return [types.Tool(name=widget.identifier,title=widget.title,description=widget.title,inputSchema=deepcopy(TOOL_INPUT_SCHEMA),_meta=_tool_meta(widget),)for widget in widgets]@mcp._mcp_server.list_resources()
async def _list_resources() -> List[types.Resource]:# 列出可读资源:用于 ReadResource 返回 HTML 外壳return [types.Resource(name=widget.title,title=widget.title,uri=widget.template_uri,description=_resource_description(widget),mimeType=MIME_TYPE,_meta=_tool_meta(widget),)for widget in widgets]@mcp._mcp_server.list_resource_templates()
async def _list_resource_templates() -> List[types.ResourceTemplate]:# 列出资源模板:匹配 uriTemplatereturn [types.ResourceTemplate(name=widget.title,title=widget.title,uriTemplate=widget.template_uri,description=_resource_description(widget),mimeType=MIME_TYPE,_meta=_tool_meta(widget),)for widget in widgets]async def _handle_read_resource(req: types.ReadResourceRequest) -> types.ServerResult:# 处理读取资源:按 URI 返回 HTML widget = WIDGETS_BY_URI.get(str(req.params.uri))if widget is None:return types.ServerResult(types.ReadResourceResult(contents=[],_meta={"error": f"Unknown resource: {req.params.uri}"},))contents = [types.TextResourceContents(uri=widget.template_uri,mimeType=MIME_TYPE,text=widget.html,_meta=_tool_meta(widget),)]return types.ServerResult(types.ReadResourceResult(contents=contents))async def _call_tool_request(req: types.CallToolRequest) -> types.ServerResult:# 处理工具调用:校验输入、返回文本+结构化数据+嵌入的 HTML 资源与元信息widget = WIDGETS_BY_ID.get(req.params.name)if widget is None:return types.ServerResult(types.CallToolResult(content=[types.TextContent(type="text",text=f"Unknown tool: {req.params.name}",)],isError=True,))arguments = req.params.arguments or {}try:payload = PizzaInput.model_validate(arguments)except ValidationError as exc:return types.ServerResult(types.CallToolResult(content=[types.TextContent(type="text",text=f"Input validation error: {exc.errors()}",)],isError=True,))order_items = payload.order_itemswidget_resource = _embedded_widget_resource(widget)meta: Dict[str, Any] = {"openai.com/widget": widget_resource.model_dump(mode="json"),"openai/outputTemplate": widget.template_uri,"openai/toolInvocation/invoking": widget.invoking,"openai/toolInvocation/invoked": widget.invoked,"openai/widgetAccessible": True,"openai/resultCanProduceWidget": True,}structured: Dict[str, Any] = {}if widget.identifier == "pizza-order" and order_items is not None:structured["orderItems"] = order_itemsreturn types.ServerResult(types.CallToolResult(content=[types.TextContent(type="text",text=widget.response_text,)],structuredContent=structured,_meta=meta,))# 显式注册处理器:确保请求路由到对应逻辑
mcp._mcp_server.request_handlers[types.CallToolRequest] = _call_tool_request
mcp._mcp_server.request_handlers[types.ReadResourceRequest] = _handle_read_resource# 创建支持 SSE 的 HTTP 应用(Starlette)
app = mcp.streamable_http_app()try:from starlette.middleware.cors import CORSMiddlewareapp.add_middleware(CORSMiddleware,allow_origins=["*"],allow_methods=["*"],allow_headers=["*"],allow_credentials=False,)
except Exception:passif __name__ == "__main__":import uvicorn
# 入口:启动 Uvicorn,默认监听 8000 端口uvicorn.run("pizzaz_server_python.main:app", host="0.0.0.0", port=8000)
重新启动应用

接下来,我们重新启动应用。(要先把之前的应用 ctrl + c 停掉)

pnpm run buildpnpm run dev
uvicorn test_pizzaz_server.main:app --port 8000

想第一时间掌握 OpenAI Apps SDK 的实战用法,以及更多 MCP、Agent、RAG、多模态 应用落地案例
来 赋范大模型技术社区,这里不仅有 Apps SDK 全流程实操拆解,还有持续更新的 Agent / RAG / 多模态 应用落地实战案例,带你深入学习。

在ChatGPT刷新连接器
刷新连接器

测试使用

pizza 是谁

pizza 是谁

pizza 点单

测试使用

详情参考:

官方示例
官方文档

http://www.dtcms.com/a/477876.html

相关文章:

  • mysql_page pagesize 如何实现游标分页?
  • Hive 拉链表
  • 集宁网站建设SEO优化安卓开发基础教程
  • C++友元函数和友元类!
  • Java面向对象编程深度解析:从对象思维到系统架构的艺术
  • 多制式基站综合测试线的架构与验证实践(4)
  • 洛阳制作网站ihanshi汉口网站建设制作
  • 2025年 Varjo XR-4 升级新品发布!首款专为陆、海、空领域战备训练打造的XR头显
  • 【XR硬件系列】AR眼镜的终极形态会是“普通眼镜”吗?技术瓶颈还有哪些?
  • 发布自己的 jar 包到 Maven 中央仓库 ( mvnrepository.com )
  • 页表 vs. 组相联缓存:内存管理与性能优化的殊途同归
  • 泉州专业建站品牌校园门户网站开发需求分析
  • 版本控制器之Git理论与实战
  • 注册网站时应注意什么域名注册后 免费自建网站
  • wpf passwordbox控件 光标移到最后
  • Linux wlan网络协议栈-路由框架详解
  • 廊坊安次区网站建设公司上海高登联合建设网站
  • 凡科网站手机投票怎么做wordpress vr主题
  • 【ElasticSearch】text 和 keyword 类型区分
  • vue3的组件通信方式汇总
  • PortSwigger靶场之将 XSS 存储到onclick带有尖括号和双引号 HTML 编码以及单引号和反斜杠转义的事件中通关秘籍
  • 哪些方法可以建设网站后台网站模板下载
  • 根据PID获取K8S-Pod名称-反之POD名称获取PID
  • 做网站三年3万块钱论坛搭建一键
  • C#进阶技巧掌握外部进程的启动与控制(一):进程基础与基本操作
  • 昂瑞微:实现精准突破,攻坚射频“卡脖子”难题
  • 延安做网站的公司电话如何用云服务器搭建个人网站
  • shellSort
  • idea一直卡在build不动(Writing class)
  • LSTM自然语言处理情感分析项目(四)整合调用各类与方法形成主程序