window.openai 组件桥接接口
ChatGPT 提供 window.openai,用于兼容性别名和可选的
ChatGPT 扩展。开发新 UI 时,只要共享规范提供了等效功能,
就应使用 MCP Apps 桥接接口,仅在需要 ChatGPT 专用能力时
使用 window.openai。
有关具体实现步骤,请参阅构建 ChatGPT UI。
如果您的工具需要确认,初始 toolInput 缺失属于正常情况。
ChatGPT 不会在审批通过前将需要审批的参数加载到小组件的值中;
而是在用户批准调用后,由主机通过
ui/notifications/tool-input 传递这些参数。
能力
| 能力 | 功能说明 | 典型用途 |
|---|---|---|
| 状态与数据 | window.openai.toolInput | 调用工具时提供的参数。对于需要审批的工具,此值可能一直为 null,直到审批通过后主机发送 ui/notifications/tool-input。 |
| 状态与数据 | window.openai.toolOutput | 您的 structuredContent。请保持字段内容简洁,模型会原样读取这些内容。 |
| 状态与数据 | window.openai.toolResponseMetadata | 仅供小组件使用的标准工具结果元数据。在 ChatGPT 中,这包括 status、call_tool_result 和 mcp_tool_result,保留完整的 MCP 结果封装,包括隐藏的 _meta。 |
| 状态与数据 | window.openai.widgetState | 在多次渲染之间持久保存的 UI 状态快照。 |
| 状态与数据 | window.openai.setWidgetState(state) | 同步存储新快照;请在每次有意义的 UI 交互后调用。 |
| 小组件运行时 API | window.openai.callTool(name, args) | 从小组件调用另一个 MCP 工具(与模型发起的调用行为一致)。 |
| 小组件运行时 API | window.openai.sendFollowUpMessage({ prompt, scrollToBottom }) | 请求 ChatGPT 发送由组件编写的消息。scrollToBottom 为可选参数,默认值为 true,可设为 false 以阻止自动滚动。 |
| 小组件运行时 API | window.openai.uploadFile(file, { library?: boolean }) | 上传用户选择的文件并获得 fileId。传入 { library: true } 可在用户的 ChatGPT 文件库可用时,将上传的文件同时保存到该文件库中。 |
| 小组件运行时 API | window.openai.selectFiles() | 打开 ChatGPT 文件库选择器,以 { fileId, fileName, mimeType }[] 的形式返回已授权供插件使用的文件。请检测此辅助函数是否可用,因为文件库可能并非对所有用户都可用。 |
| 小组件运行时 API | window.openai.getFileDownloadUrl({ fileId }) | 获取文件的临时下载 URL。文件可以由小组件上传、从文件库中选择、通过文件参数传入,或通过工具的文件引用返回。 |
| 小组件运行时 API | window.openai.requestDisplayMode(...) | 请求画中画或全屏模式。 |
| 小组件运行时 API | window.openai.requestModal({ params, template }) | 创建由 ChatGPT 管理的模态窗口。省略 template 可使用当前模板,也可传入已注册的模板 URI 来切换模态窗口内容。 |
| 小组件运行时 API | window.openai.requestClose() | 请求 ChatGPT 关闭当前小组件。 |
| 小组件运行时 API | window.openai.notifyIntrinsicHeight(...) | 报告小组件动态变化的高度,避免滚动内容被裁切。 |
| 小组件运行时 API | window.openai.openExternal({ href, redirectUrl }) | 在用户的浏览器中打开经过审核的外部链接。对于已批准的重定向目标,ChatGPT 默认会附加 ?redirectUrl=...;设置 redirectUrl: false 可跳过此操作。 |
| 小组件运行时 API | window.openai.setOpenInAppUrl({ href }) | 可选择覆盖全屏模式下显示的外部目标。如果未设置,ChatGPT 会保持默认行为,打开组件当前的 iframe 路径。 |
| 上下文 | window.openai.theme、window.openai.displayMode、window.openai.maxHeight、window.openai.safeArea、window.openai.view、window.openai.userAgent、window.openai.locale | 您可以通过 useOpenAiGlobal 读取或订阅这些环境信号,以调整视觉效果和文案。 |
useOpenAiGlobal 辅助函数
许多 ChatGPT UI 项目会将对 window.openai 的访问封装在小型辅助函数中,
以保持视图的可测试性。此示例辅助函数会监听主机的
openai:set_globals 事件,
让 React 组件能够订阅单个全局值:
export function useOpenAiGlobal<K extends keyof WebplusGlobals>(
key: K
): WebplusGlobals[K] {
return useSyncExternalStore(
(onChange) => {
const handleSetGlobal = (event: SetGlobalsEvent) => {
const value = event.detail.globals[key];
if (value === undefined) {
return;
}
onChange();
};
window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
passive: true,
});
return () => {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
};
},
() => window.openai[key]
);
}
关闭 UI
调用 window.openai.requestClose(),请求 ChatGPT 关闭当前 UI。
请求其他显示模式
使用 window.openai.requestDisplayMode 请求内嵌、画中画
或全屏显示:
await window.openai?.requestDisplayMode({ mode: "fullscreen" });
// On mobile, picture-in-picture may be presented as fullscreen.
打开模态窗口
使用 window.openai.requestModal 打开由主机控制的模态窗口。
提供同一 MCP 服务器注册的另一个 UI 模板的 URI,
或省略 template 以打开当前模板:
await window.openai.requestModal({
template: "ui://widget/checkout.html",
});
文件 API
ChatGPT 支持文件上传和下载辅助函数,
这些函数作为可选的 window.openai 扩展提供。
| API | 用途 | 说明 |
|---|---|---|
window.openai.uploadFile(file, { library?: boolean }) | 上传用户选择的文件并获得 fileId。 | 传入 { library: true } 可在 ChatGPT 文件库对当前用户可用时,将上传的文件同时保存到该用户的文件库中。 |
window.openai.selectFiles() | 打开文件库选择器以选择现有文件。 | 返回 [{ fileId, fileName, mimeType }]。请先检测此辅助函数是否可用,因为文件库可能并非对所有用户开放。 |
window.openai.getFileDownloadUrl({ fileId }) | 请求文件的临时下载 URL。 | 适用于小组件上传的文件、从文件库中选择的文件、通过文件参数传入的文件,以及通过工具文件引用返回的文件。 |
ChatGPT 文件库是一项可选功能,可能并非对所有用户开放。
当此辅助函数可用时,window.openai.selectFiles() 返回的文件
已获授权,可供当前插件使用。将返回的 fileId 用于
window.openai.getFileDownloadUrl({ fileId }),或将其用于采用
文件参数的工具输入。
上传用户选择的文件:
const { fileId } = await window.openai.uploadFile(file, {
library: true,
});
选择用户已上传到 ChatGPT 的文件:
if (window.openai?.selectFiles) {
const files = await window.openai.selectFiles();
// [{ fileId, fileName, mimeType }]
}
请检测 window.openai.selectFiles 是否可用,并在文件库不可用时回退到
window.openai.uploadFile。
请求临时下载 URL:
const { downloadUrl } = await window.openai.getFileDownloadUrl({ fileId });
定义文件输入
要让 ChatGPT 向工具传递文件,请在
_meta["openai/fileParams"] 中列出每个顶层文件输入。列出的每个字段都必须解析为文件对象或
文件对象数组。
每个文件对象模式都必须声明全部四个受支持的属性:
| 属性 | 类型 | 在 properties 中声明 | 包含在 required 中 |
|---|---|---|---|
download_url | string | 是 | 是 |
file_id | string | 是 | 是 |
mime_type | string | 是 | 否 |
file_name | string | 是 | 否 |
mime_type 和 file_name 的值是可选的,但您必须在模式中
声明这两个属性。如果文件模式存在以下任一情况, 扫描工具 步骤和插件提交都会拒绝该模式:
遗漏四个属性中的任意一个;未将
download_url 和 file_id 设为必填;将任一可选属性设为必填;或
将 download_url 和 file_id 以外的属性设为必填。您可以声明
额外的可选属性。
以下完整的工具描述符接受一个必填的文件输入:
{
"name": "analyze_file",
"title": "Analyze file",
"description": "Analyzes a user-provided file without modifying it.",
"inputSchema": {
"type": "object",
"$defs": {
"OpenAIFile": {
"type": "object",
"properties": {
"download_url": { "type": "string" },
"file_id": { "type": "string" },
"mime_type": { "type": "string" },
"file_name": { "type": "string" }
},
"required": ["download_url", "file_id"],
"additionalProperties": false
}
},
"properties": {
"file": { "$ref": "#/$defs/OpenAIFile" }
},
"required": ["file"]
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false,
"destructiveHint": false
},
"_meta": {
"openai/fileParams": ["file"]
}
}
要接受多个文件,请将顶层字段定义为数组,并在
items 中使用相同的文件对象模式。工具可以将顶层文件字段设为必填,
这一设置与每个文件对象内部的必填属性相互独立。
运行时,ChatGPT 传递的文件值使用蛇形命名法命名的字段:
{
"download_url": "https://...",
"file_id": "file_...",
"mime_type": "image/png",
"file_name": "input.png"
}
ChatGPT 始终包含 download_url 和 file_id,但可能省略 mime_type
和 file_name。当小组件需要新的临时下载 URL 时,
请将 file_id 作为 fileId 的值,
用于 window.openai.getFileDownloadUrl({ fileId })。
持久化小组件状态时,如果您希望模型在后续对话轮次中看到图像 ID,请使用结构化格式(modelContent、privateContent、imageIds)。
主机支持的导航
沙盒运行时会将 iframe 中的导航历史同步到 ChatGPT 的 UI。 使用 React Router 等标准路由 API, 主机就会让其导航控件与您的 UI 保持同步。
使用 React Router 的 BrowserRouter 设置路由:
export default function PizzaListRouter() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<PizzaListPlugin />}>
<Route path="place/:placeId" element={<PizzaListPlugin />} />
</Route>
</Routes>
</BrowserRouter>
);
}
编程式导航:
const navigate = useNavigate();
function openDetails(placeId: string) {
navigate(`place/${placeId}`, { replace: false });
}
function closeDetails() {
navigate("..", { replace: true });
}
工具描述符参数
默认情况下,工具描述应包含此处列出的字段。
请为所有返回 structuredContent 的工具声明 outputSchema。
该模式应准确描述工具返回的对象,以便客户端
验证结果,并让模型能够对后续工具调用进行推理。
工具描述符上的 _meta 字段
在工具描述符上使用以下 _meta 字段。将工具关联到 UI 模板时,优先使用 MCP Apps 标准
键 _meta.ui.resourceUri。ChatGPT 支持
OpenAI 专用元数据,用于兼容性支持和可选扩展。
| 键 | 位置 | 类型 | 限制 | 用途 |
|---|---|---|---|---|
_meta["securitySchemes"] | 工具描述符 | 数组 | 无 | 为仅读取 _meta 的客户端提供用于向后兼容的镜像字段。 |
_meta.ui.resourceUri | 工具描述符 | 字符串(URI) | 无 | UI 模板的标准资源 URI。 |
_meta.ui.visibility | 工具描述符 | string[] | 默认为 ["model", "app"] | 控制工具可供模型、UI 或两者使用。app 值是 MCP Apps 协议中表示 UI 的标识符。 |
_meta["openai/outputTemplate"] | 工具描述符 | 字符串(URI) | 无 | ChatGPT 中 _meta.ui.resourceUri 的 OpenAI 专用可选兼容别名。 |
_meta["openai/profile"] | 工具描述符 | boolean | 可选;只有值为 true 时才表示这是账户资料工具 | 标识用于返回当前账户资料且需要身份验证的只读工具。实现此工具可帮助用户识别和管理多个已连接的账户。即使未实现此工具,用户也可以连接多个账户。请参阅支持多个账户。 |
_meta["openai/widgetAccessible"] | 工具描述符 | 布尔值 | 默认为 false | 现有 UI 集成使用的 OpenAI 专用兼容字段;优先使用 _meta.ui.visibility + tools/call。 |
_meta["openai/visibility"] | 工具描述符 | string | public(默认)或 private | 现有 UI 集成使用的 OpenAI 专属兼容字段;建议优先使用 _meta.ui.visibility。 |
_meta["openai/toolInvocation/invoking"] | 工具描述符 | string | ≤ 64 个字符 | 工具运行期间显示的简短状态文本。 |
_meta["openai/toolInvocation/invoked"] | 工具描述符 | string | ≤ 64 个字符 | 工具完成后显示的简短状态文本。 |
_meta["openai/fileParams"] | 工具描述符 | string[] | 无 | 表示文件的顶层输入字段列表。每个字段接收 { download_url, file_id, mime_type?, file_name? }。 |
示例:
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
registerAppTool(
server,
"search",
{
title: "Public Search",
description: "Search public documents.",
inputSchema: { q: z.string() },
outputSchema: {
results: z.array(
z.object({
id: z.string(),
title: z.string(),
url: z.string(),
})
),
},
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["search.read"] },
],
_meta: {
securitySchemes: [
{ type: "noauth" },
{ type: "oauth2", scopes: ["search.read"] },
],
ui: { resourceUri: "ui://widget/story.html" },
// Optional compatibility alias (ChatGPT only):
// "openai/outputTemplate": "ui://widget/story.html",
"openai/toolInvocation/invoking": "Searching…",
"openai/toolInvocation/invoked": "Results ready",
},
},
async ({ q }) => {
const results = await performSearch(q);
return {
structuredContent: { results },
content: [{ type: "text", text: `Found ${results.length} results.` }],
};
}
);
注解
要将工具标记为“只读”,请使用以下
ToolAnnotations
字段
来配置工具描述符:
| 键 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
readOnlyHint | boolean | 必填 | 表明工具仅检索或计算信息,不会在对话之外创建、更新、删除或发送数据。 |
destructiveHint | boolean | 必填 | 声明工具可能删除或覆盖用户数据,以便主机先请求明确审批。 |
openWorldHint | boolean | 必填 | 声明工具会访问公共互联网或范围不受限的外部实体,包括通过网页搜索等只读操作进行访问。范围明确的私有账户或工作空间不会仅因托管在外部就被视为开放世界。 |
idempotentHint | boolean | 可选 | 声明使用相同参数调用工具不会对其环境产生额外影响。 |
这些提示仅影响 ChatGPT 或 Codex 向用户说明工具调用的方式;服务器仍须执行自身的授权逻辑。
示例:
import { z } from "zod";
server.registerTool(
"list_saved_recipes",
{
title: "List saved recipes",
description: "Returns the user’s saved recipes without modifying them.",
inputSchema: {},
outputSchema: {
recipes: z.array(
z.object({
id: z.string(),
title: z.string(),
})
),
},
annotations: { readOnlyHint: true },
},
async () => ({
structuredContent: { recipes: await fetchSavedRecipes() },
})
);
组件资源的 _meta 字段
在提供组件的资源模板(registerResource)上设置这些键。它们帮助 ChatGPT 描述渲染出的 iframe 并确定其展示方式,同时不会向其他客户端泄露元数据。
| 键 | 位置 | 类型 | 用途 |
|---|---|---|---|
_meta.ui.prefersBorder | 资源内容 | boolean | 提示在支持的情况下,应将组件渲染在带边框的卡片内。 |
_meta.ui.csp | 资源内容 | object | 标准小组件 CSP 字段的首选元数据配置位置,包括 connectDomains、resourceDomains 和可选的 frameDomains。 |
_meta.ui.domain | 资源内容 | string(源) | 托管组件的专用源(提交带 UI 的插件时必填,且每个插件必须使用唯一的源)。默认为 https://web-sandbox.oaiusercontent.com。 |
_meta["openai/widgetDescription"] | 资源内容 | string | 组件加载时提供给模型的易读摘要,可减少助手的重复说明。 |
_meta["openai/widgetPrefersBorder"] | 资源内容 | boolean | ChatGPT 中 _meta.ui.prefersBorder 的 OpenAI 专属兼容别名。 |
_meta["openai/widgetCSP"] | 资源内容 | object | 用于小组件 CSP 元数据的旧版 ChatGPT 兼容键。标准 CSP 字段已由 _meta.ui.csp 取代,但对于受信任的 openExternal 目标地址,仍须设置 redirect_domains。 |
_meta["openai/widgetDomain"] | 资源内容 | string(源) | ChatGPT 中 _meta.ui.domain 的 OpenAI 专属兼容别名。 |
ChatGPT 支持旧版兼容键 _meta["openai/widgetCSP"],其字段名称采用以下 snake_case 形式:
connect_domains:string[]resource_domains:string[]frame_domains?:string[]redirect_domains?:string[]。用于指定window.openai.openExternal重定向目标的 ChatGPT 扩展。
新 UI 通常应优先使用标准 _meta.ui.csp 对象,该对象支持以下字段:
connectDomains:string[]。小组件可通过 fetch/XHR 访问的域名。resourceDomains:string[]。静态资源(图像、字体、脚本、样式)使用的域名。frameDomains?:string[]。允许通过 iframe 嵌入的源列表(可选)。默认情况下,小组件无法渲染子框架。插件可以按照 iframe 政策嵌入自身域名下的内容,包括现有的编辑器和管理界面。提交时必须说明理由,使用 iframe 可能需要额外审查,或导致审批时间延长。
不过,_meta.ui.csp 不支持为 window.openai.openExternal(...) 链接设置 redirect_domains。要将重定向目标加入允许列表,您仍须设置 _meta["openai/widgetCSP"].redirect_domains。
工具结果
工具结果可以包含以下字段。重点如下:
| 键 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
structuredContent | object | 可选 | 提供给模型和组件。如果已声明 outputSchema,则必须与其匹配。 |
content | string 或 Content[] | 可选 | 提供给模型和组件。 |
_meta | object | 可选 | 仅传递给组件,对模型不可见。 |
只有 structuredContent 和 content 会出现在对话记录中。主机会将 _meta 转发给组件,让您可以向 UI 填充数据,而不向模型暴露这些数据。
主机提供的工具结果元数据:
| 键 | 位置 | 类型 | 用途 |
|---|---|---|---|
_meta["openai/widgetSessionId"] | 工具结果中的 _meta(由主机提供) | string | 当前已挂载小组件实例的稳定 ID;在小组件卸载前,可用它关联日志和工具调用。 |
示例:
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
registerAppTool(
server,
"get_zoo_animals",
{
title: "get_zoo_animals",
inputSchema: { count: z.number().int().min(1).max(20).optional() },
outputSchema: {
animals: z.array(
z.object({
id: z.string(),
name: z.string(),
species: z.string(),
})
),
},
_meta: { ui: { resourceUri: "ui://widget/widget.html" } },
},
async ({ count = 10 }) => {
const animals = generateZooAnimals(count);
return {
structuredContent: { animals },
content: [{ type: "text", text: `Here are ${animals.length} animals.` }],
_meta: {
allAnimalsById: Object.fromEntries(
animals.map((animal) => [animal.id, animal])
),
},
};
}
);
包含错误的工具结果
要在工具结果中返回错误,请使用以下 _meta 键:
| 键 | 用途 | 类型 | 说明 |
|---|---|---|---|
_meta["mcp/www_authenticate"] | 错误结果 | string 或 string[] | 用于触发 OAuth 的 RFC 7235 WWW-Authenticate 质询。 |
客户端提供的 _meta 字段
| 键 | 提供时机 | 类型 | 用途 |
|---|---|---|---|
_meta["openai/locale"] | 初始化和工具调用时 | string(BCP 47) | 请求使用的语言区域(旧版客户端可能会发送 _meta["webplus/i18n"])。 |
_meta["openai/userAgent"] | 工具调用时 | string | 尽力提供的可选用户代理提示信息,用于分析或格式设置。 |
_meta["openai/userLocation"] | 工具调用时 | object | 大致位置提示信息(city、region、country、timezone、longitude、latitude)。 |
_meta["openai/subject"] | 工具调用时 | string | 发送给 MCP 服务器的匿名化用户 ID,用于速率限制和用户识别。 |
_meta["openai/session"] | 工具调用时 | string | 匿名化对话 ID,用于关联同一 ChatGPT 会话中的工具调用。 |
_meta["openai/organization"] | 工具调用时 | string | 与当前 ChatGPT 组织关联的匿名化组织 ID(如有)。 |
操作阶段的 _meta["openai/userAgent"] 和 _meta["openai/userLocation"] 仅供参考;服务器绝不能依赖它们做出授权决策,并且必须能够处理它们缺失的情况。应将 _meta["openai/userAgent"] 视为可选且尽力提供的元数据,而不能将其作为可靠判断哪个宿主界面正在调用您的服务器的依据。
示例:
import { z } from "zod";
server.registerTool(
"recommend_cafe",
{
title: "Recommend a cafe",
inputSchema: {},
outputSchema: {
cafes: z.array(
z.object({
name: z.string(),
address: z.string(),
})
),
},
},
async (_args, { _meta }) => {
const locale = _meta?.["openai/locale"] ?? "en";
const location = _meta?.["openai/userLocation"]?.city;
const cafes = await findNearbyCafes(location);
return {
content: [{ type: "text", text: formatIntro(locale, location) }],
structuredContent: { cafes },
};
}
);