1. 引言
ChatGPT App HelloWorld 项目并不是“CodeGym 的神秘黑盒,最好别碰”。它就是一个普通的 Next.js 项目,只是里面同时承载了:
- 在 ChatGPT 内部渲染的前端,
- 响应工具(tools)调用的 MCP 服务器,
- 将这些与 ChatGPT 粘合在一起的配置。
如果不了解各部分的放置位置,通常会发生三种经典情况:
- 开发者不小心在服务器文件里写了 window,导致崩溃,从而开始厌恶整个技术栈。
- 想在 UI 中加按钮,却改错了 page.tsx(比如改了应用根页而不是小部件页),在 ChatGPT 里看不到变化。
- 误把 OPENAI_API_KEY 放进了客户端代码,密钥泄露到浏览器。
因此今天的目标是画一张地图:哪里是 UI,哪里是 MCP,哪里是配置;当你想要:
- 调整小部件的外观;
- 添加新的 tool;
- 微调某些平台级设置(CORS、assetPrefix 等)。
2. 项目的高层解剖
ChatGPT App HelloWorld 的 Next.js 项目使用 App Router,围绕 app/ 目录组织。在同一页面树中并存:
- 会在 ChatGPT 内渲染的小部件 UI,
- 用于处理 tool 调用的 MCP endpoint。
典型的目录树(简化版,你的模板中文件夹名可能不同,但模式一致):
my-chatgpt-app/
├─ app/
│ ├─ api/ // REST API
│ │ └─ time/ // GET /api/time 返回服务器时间
│ │ └─ route.ts
│ ├─ hooks/ // 官方 Apps SDK 的 hooks 集合
│ │ ├─ use-call-tool.ts
│ │ ├─ use-display-mode.ts
│ │ └─ use-open-external.ts
│ ├─ mcp/ // MCP 服务器:ChatGPT 调用 tools 时会访问这里
│ │ └─ route.ts
│ ├─ globals.css // 应用的根级 globals.css
│ ├─ layout.tsx // 应用的根级 layout
│ └─ page.tsx // 在 ChatGPT 中渲染的小部件页面
├─ public/ // 静态资源:图标、清单等
├─ next.config.ts // Next.js 配置与 Apps 特定设置(assetPrefix 等)
├─ proxy.ts // 在 iframe 中工作的 CORS/头(原 middleware.ts)
├─ package.json // 项目依赖
├─ tsconfig.json // TypeScript 配置
└─ .env.local // 密钥:OPENAI_API_KEY 等
如果有多个小部件,通常不放在 app/page.tsx,而是放在 app/widget/page.tsx。但逻辑不变:仍然是一个小部件页面, 以及一个充当 MCP 服务器角色的 endpoint。
一个便于记忆的比喻:你的仓库就像“双面神雅努斯”。
- 一张“脸”是路径 /mcp,当 ChatGPT 想调用工具时会访问这里;
- 另一张“脸”是路径 /widget(或 /), 当模型决定展示你的 UI 时,会在 iframe 中加载它。
为避免混淆,我们在脑中固定三组文件:
- UI 层——与 React/Next 页面相关的一切 (app/widget、组件、样式)。
- MCP 层——app/mcp/route.ts 及其使用的文件。
- 粘合层与配置——next.config.ts、 proxy.ts、.env.local、 package.json、tsconfig.json。
下面我们逐一走查这些层。
3. 小部件位于哪里:app/widget 和/或 app/page.tsx
先从你最常改动的地方开始——小部件,也就是会在 ChatGPT 内部可见的 UI。
大多数现代项目中会有以下两种之一:
- 目录 app/widget/page.tsx——小部件位于单独的前缀 /widget 下,
- 或根页面 app/page.tsx——小部件与根页面相同。
小部件文件的主要特征:
- 文件最顶部有 'use client',因为组件在浏览器中运行, 会与 window 和 Apps SDK 交互;
- 它是一个普通的 React 组件,负责渲染标记,并且(稍后在课程中)与 window.openai 通信。
最简单的教学小部件示例(你的项目中可能已经有非常相似的代码):
// app/widget/page.tsx
'use client';
import React from 'react';
export default function WidgetPage() {
return (
<main className="p-4">
<h1 className="text-xl font-semibold">
HelloWorld — ChatGPT App
</h1>
<p className="text-sm text-gray-500">
我们将在这里构建小部件的 UI。
</p>
</main>
);
}
如果你的模板把小部件直接放在 app/page.tsx,代码基本一致, 只是没有中间的 widget 文件夹。
请注意以下几点。
首先,指令 'use client' 是必需的: 小部件要读/写 window.openai, 监听事件等——这些只能在客户端组件中完成。 如果去掉它,Next 会尝试将页面作为服务器端渲染,你会得到类似 “window is not defined” 的错误。
其次,它就是一个完全不神奇的 React 组件。你可以:
- 把它拆分到 components/ 子组件中,
- 使用 Tailwind 或任意其他 CSS 方案,
- 接入 context、hooks 等等。
第三,之后你会在这里:
- 读取 window.openai.toolInput 和 window.openai.toolOutput, 来渲染真实数据,
- 通过 window.openai.setWidgetState 保存 widgetState,
- 调用 openExternal、callTool 等运行时方法。
目前只需记住:如果你想修改视觉界面——十有八九要去 app/widget/page.tsx 或 app/page.tsx。
4. 根级 layout:作为整个应用“框架”的 app/layout.tsx
下一个重要文件是 app/layout.tsx。它:
- 定义 HTML 结构(<html>、<body>),
- 引入全局样式(globals.css),
- 经常会初始化 Apps SDK 的“bootstrap”(一个监听 window.openai 并把数据传给 React 的包装)。
一个简化示例:
// app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';
import { OpenAIAppProvider } from '@/lib/openai-app-provider';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<NextChatSDKBootstrap baseUrl={baseURL} />
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-hidden`}>
{children}
</body>
</html>
);
}
这里的 NextChatSDKBootstrap 只是示意命名,你的模板里可能是 OpenAIAppProvider 或其他组件。它的职责通常只有一个:在 React 树与 Apps SDK 运行时之间建立连接, 订阅全局数据(theme、 displayMode、toolInput 等)并下发给子组件。
一个重要的实践结论:如果你需要接入全局上下文、样式或 UI 库 (例如 shadcn/ui),那么位置几乎总是 app/layout.tsx(或者放在 app/widget 下的 layout,用于仅对小部件生效的设置与组件)。
解析 NextChatSDKBootstrap
NextChatSDKBootstrap 我是从 Vercel 的官方模板里借鉴的。 如果你不清楚,正是他们创建并发展了 Next。他们网站上有一篇不错的文章: 在 ChatGPT 中运行 Next 的深度解析。 还有一个 Starter Template。 虽然个别地方有点过时,但我认为他们很可能会持续维护其时效性。
我们提炼出 NextChatSDKBootstrap 带来的 5 件关键事情:
- 1. 修复水合问题
原因在于 ChatGPT 会先把你的小部件 HTML 加载到它的服务器上,进行清理与打补丁。 结果导致水合机制报错,在控制台抛出大量警告。这可能会影响你的审核通过。 - 2. 改写浏览器历史
你的小部件在 ChatGPT 的专用域名下以 iframe 方式加载。 如果使用你自己的域名,会破坏沙箱。因此浏览器历史里只保存不含域名的路径。 - 3. 重写 fetch() 函数
你在相对路径(无域名)上的 fetch() 在小部件中不会工作, 因为 iframe 的域不同。所以我们会替换 fetch(), 把无域名请求转发到正确的 URL;如果显式写了域名,则保持不变。 - 4. 点击链接可正常工作
如果链接在 iframe 内部打开,ChatGPT 不会认可。 因此我们添加代码监听链接点击,并通过 openExternal() 在外部窗口打开。 - 5. 设置 head base(已弃用)
此前这段代码会往 <head> 里添加 <base>, 但现在已不可用。沙箱会重置任何设置的 base, 因此建议为所有内容使用绝对链接:脚本、资源、字体、API 等。
5. MCP 服务器:app/mcp/route.ts
现在我们来到“双面神”的另一面——与 ChatGPT 通过 MCP 通信的服务器。
文件 app/mcp/route.ts 是一个普通的 App Router Route Handler,它:
- 接收来自 ChatGPT 的 HTTP 请求(通常是带 MCP 格式 JSON 负载的 POST),
- 把它们传给 MCP 服务器(基于 @modelcontextprotocol/sdk 或一个薄封装),
- 返回 MCP 格式的 JSON 响应。
有两种写法:要么直接用原生 MCP SDK,要么用 Next/Vercel 的一些类做封装让体验更友好。
这是使用纯 TS MCP SDK 的一个版本:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. 创建 MCP 服务器
const server = new McpServer({
name: "simple-mcp-server",
version: "1.0.0",
});
// 2. 注册 MCP Resources
// 3. 注册 MCP Tools
// 4. HTTP 传输
const transport = new HttpServerTransport({
port: 3001,
path: "/mcp",
});
// 5. 启动服务器
await server.connect(transport);
但更推荐借助一些现成的类,让开发更顺手:
// app/mcp/route.ts
import { NextRequest } from 'next/server';
import { createMcpHandler } from "mcp-handler";
const handler = createMcpHandler(async (server) => {
const gateway = new McpGateway(server);
await gateway.initialize();
gateway.registerResources();
gateway.registerTools();
});
export const GET = handler;
export const POST = handler;
这里的 McpGateway 是 McpServer 的包装类, 用 SDK 在某处(比如 lib/mcp/server.ts)创建。 在我们的例子中,它全部放在 app/mcp/route.ts。我们来完整拆解这个文件里的内容。
type ContentWidget
文件开头定义了类型 ContentWidget。它包含小部件的全部数据,并用于两个地方: 把小部件注册为 mcp-resource,以及当 mcp-tool 返回 metadata 时指明应使用哪个小部件展示它返回的数据。
type ContentWidget = {
id: string; // 唯一名称/key
title: string; // Title
description: string; // Description
templateUri: string; // 小部件的唯一 URI,可以是任意值。不会影响其他行为。
invoking: string; // 小部件加载时的标题文案
invoked: string; // 小部件加载完成后的标题文案
html: string; // 小部件的全部 html 代码
widgetDomain: string; // 小部件的“域名”。不会影响其他行为。
};
class McpGateway
这是对 McpServer 的包装类,用于简化若干事情。包含 6 个方法:
- initialize() —— 在这里加载我们的小部件 HTML
- registerResources() —— 将小部件注册为 mcp-resources
- registerTools() —— 将函数注册为 mcp-tools
- widgetMeta() —— 返回小部件的元数据
- getAppsSdkCompatibleHtml() —— 加载小部件 HTML 并做少量补丁
- makeImgUrlsAbsolute() —— 补丁 HTML:把图片链接改为绝对地址
逐个讲解如下:
public async initialize()
该方法从网络加载小部件的 HTML 代码,并填充 ContentWidget 类型的对象。
{
id: "hello_world", // 小部件的唯一 key
templateUri: "ui://widget/hello_world.html", // 小部件的唯一 URI。前缀 "ui:" 没有特殊含义。
title: "HelloWorld Widget", // 小部件名称
description: "Displays the HelloWorld widget", // 给 LLM 的说明:小部件做什么
invoking: "Loading widget...", // 小部件加载时的标题文案
invoked: "Widget loaded", // 小部件加载完成后的标题文案
html: htmlWidget, // 小部件的 HTML
widgetDomain: baseURL, // 小部件的“域名”。目前没有实质影响。
}
public registerResources()
将小部件注册为 mcp-resources。会调用 server.registerResource() 方法,传入 4 个参数:
- MCP 资源的 id/key
- 资源的 URI(这是 MCP 协议需要的;对小部件来说相当于唯一地址)
- MCP 资源的元数据
- 返回 MCP 资源的函数
小部件的元数据
{
title: widget.title, // 资源/小部件名称
description: widget.description, // 资源/小部件描述
mimeType: "text/html+skybridge", // 重要!只有这种 html 才会以小部件形式展示
_meta: {
"openai/widgetDescription": widget.description, // 小部件描述
"openai/widgetPrefersBorder": true, // 让 ChatGPT 绘制小部件边框
},
}
把小部件作为 MCP 资源返回
{
uri: uri.href, // 我们的 URI(来自参数 uri)
mimeType: "text/html+skybridge", // 重要!只有这种 html 才会以小部件形式展示
text: widget.html, // 小部件的 HTML
_meta: {
"openai/widgetDescription": widget.description, // 小部件描述
"openai/widgetPrefersBorder": true, // 让 ChatGPT 绘制小部件边框
"openai/widgetDomain": widget.widgetDomain, // 小部件的“域名”。目前没有实质影响。
"openai/widgetCSP": { // 重要!小部件可访问的域名:
connect_domains: [ // 用于连接的域名(fetch 等)
baseURL,
"https://codegym.cc",
],
resource_domains: [ // 资源域名(css/fonts/img)
baseURL,
"https://codegym.cc",
"https://cdn.tailwindcss.com",
"https://persistent.oaistatic.com",
"https://fonts.googleapis.com",
"https://fonts.gstatic.com"
]
}
},
}
将来我们还会多次提到 openai/widgetCSP,不过现在先强调两点:
- connect_domains —— 域名列表用于:
- fetch()
- 加载脚本
- openExternal()
- resource_domains —— 域名列表用于:
- 图片
- CSS
- 字体
理论上你可以写上 200 个域,但是否能带着这样的列表通过审核——就不好说了。
我也研究了部分已发布应用的这些参数,发现里面有 amplitude.com。 这也是个好消息。我认为良好的数据分析对谁都不坏。
public registerTools()
将函数注册为 mcp-tools。会调用 server.registerTool() 方法,传入 3 个参数:
- MCP tool 的 id/key
- MCP tool 的元数据
- 返回 MCP tool 的函数
工具的元数据
下面列表里的参数都很重要。细节我会在接下来的讲次中展开。
{
title: widget.title, // 工具名称
description: "Returns HelloWorld widget", // 重要!工具的功能说明
inputSchema: z.object({}).describe("No inputs"), // 工具参数的 Schema。可用 Zod
_meta: this.widgetMeta(widget), // 小部件的元数据:展示哪个小部件
annotations: {
destructiveHint: false, // 方法有破坏性/需确认
openWorldHint: false, // 方法会改动第三方服务
readOnlyHint: true // 方法不做任何修改
},
}
执行重要事情的函数
async (input, extra) => {
// 1. 参数校验
// 2. 执行重要操作
return {
content: [{ type: "text", text: "HelloWorld MCP-tool" }], // 给 AI 的结果描述
structuredContent: { // 重要!这就是结果的 JSON。
timestamp: new Date().toISOString() // 可以包含任意数据。
},
_meta: this.widgetMeta(widget), // 展示该 JSON 的小部件的元数据
}; // 可省略——省略则不显示小部件
}
private widgetMeta(widget: ContentWidget)
返回小部件的元数据——ChatGPT 会据此决定展示哪个小部件来显示 JSON 结果。
{
"openai/outputTemplate": widget.templateUri, // 小部件的 URI
"openai/toolInvocation/invoking": widget.invoking, // 小部件加载中时的标题文案
"openai/toolInvocation/invoked": widget.invoked, // 小部件加载完成时的标题文案
"openai/widgetAccessible": true, // 可从小部件内调用 MCP tool
"openai/resultCanProduceWidget": true, // MCP tool 会返回小部件
}
想单独说明一下 "openai/outputTemplate" 这个看似简单的东西。 在 MCP 协议中有 3 个实体(你会在第 6 模块详细了解):
- MCP Resources
- MCP Templates
- MCP Tools
这里的 "openai/outputTemplate"与 MCP Templates 没有任何关系。 MCP Templates 在 ChatGPT Apps 中完全没有用到。这里的 template 一词来源如下:
小部件被设计为展示 JSON 的模板。MCP tool 返回某个 JSON,模型展示小部件,通过 ToolOutput 参数把 JSON 传给小部件, 小部件再把这个 JSON 漂亮地呈现出来。outputTemplate 只是“小部件”的同义词。
这部分先到这里。我们会在第 4 模块更详细地讲:如何描述工具、JSON Schema 与处理器。 现在只需明白:凡是与工具(tools)和业务逻辑相关的内容——都去 app/mcp/route.ts 附近找。
6. 配置与“粘合层”:next.config.ts、middleware.ts、.env 及其伙伴
现在来看看让你的 Next.js 项目能在 ChatGPT 的 iframe 中正常工作、 并能通过 HTTPS 隧道(ngrok、Cloudflare Tunnel 等;我们之后会专门讲隧道)被 ChatGPT 访问所需的关键文件集。
next.config.ts
这个文件除了 Next.js 的常规配置外,常见还会配置:
- assetPrefix —— 让静态资源(/_next/ 下的 JS、CSS) 不从 ChatGPT 的域加载,而是从你的开发 URL(隧道或 Vercel)加载;
- 模板所需的其他特定设置(例如 Next 16 的实验性 flag)。
实际上,它就是导出一个包含相应字段的 nextConfig。 对本讲而言最重要的一点是:如果在 ChatGPT 中小部件加载不了 CSS/JS,罪魁祸首往往是 assetPrefix。
proxy.ts(原 middleware.ts)
该文件在来自 ChatGPT 的请求与路由之间插入 middleware 层。在模板中它通常会:
- 设置 CORS 头,让 ChatGPT 的 iframe 有权访问你的服务器;
- 有时也会为 React Server Components 添加额外的头。
现在无需掌握全部细节。只需记住:如果 ChatGPT 报 CORS,或你在 DevTools 看到奇怪的访问禁止错误,请查看 proxy.ts。
.env
.env(或 .env.local)是放置密钥和环境参数的地方:
- OPENAI_API_KEY(如果 MCP 服务器本身会访问 OpenAI API),
- 你的内部 API 地址,
- 第三方服务的 token 等等。
有个重要细节:在 Next.js 中,以 NEXT_PUBLIC_ 开头的环境变量会自动打包进 JS,并在浏览器中可用。 千万不要这样处理 OPENAI_API_KEY;密钥应该只存在于服务器端变量。
package.json 与 tsconfig.json
在 package.json 里你会看到:
- Next.js、React、Apps SDK、MCP SDK 等依赖的版本;
- dev、build、start 脚本,有时还有辅助命令(lint、format 等)。
在 tsconfig.json 中是你熟悉的 TypeScript 配置:
- 路径别名(@/lib、@/components),
- 严格模式,
- 编译目标。
就本课程而言,最重要的是理解:该模板使用的是常规的 TypeScript 技术栈,你可以以标准方式扩展它。
7. 开发者的“项目快速导航”
我们固定一下在做常见任务时该去哪里找。不用列大清单,就当作几个迷你场景。
如果你想修改小部件中的文字/按钮,打开小部件 UI 的文件:要么是 app/widget/page.tsx,要么是 app/page.tsx——取决于模板。 在那里你修改 JSX、添加新组件、接入设计系统。你也会在这里使用 Apps SDK 运行时 (window.openai 或便捷的 hooks)来展示数据。
如果要添加一个在服务器上执行操作的新按钮,你仍然从 UI 文件开始。 小部件中的按钮在点击时会调用 window.openai.callTool, 而该工具的实现会加到 MCP 服务器的配置中,也就是 app/mcp/route.ts 附近的代码。 UI ↔ tool 逻辑的串联我们会在第 4 模块及之后详细拆解。
当你想让 ChatGPT 学会新功能(比如“搜索旅行”或“选品”), 你要去 MCP 层(从 app/mcp/route.ts 引入的那些文件)。 在那里注册一个带 JSON Schema、描述与处理器的新 tool。小部件随后可以通过 window.openai.toolOutput 读取结果并进行友好展示。
如果静态资源失效或小部件只在 ChatGPT 中显示异常,而本地一切正常, 请回忆粘合层。首先检查 next.config.ts (尤其是 assetPrefix)以及 middleware.ts/proxy.ts(CORS)。 如果你刚换了隧道、URL,或部署到了 Vercel,这些设置的正确性至关重要。
最后,如果你怀疑是密钥或环境问题,请查看这三个文件—— .env.local、package.json (确认实际使用了哪些依赖与脚本)和开发服务器日志。 正是这一组合保证 MCP 能访问到必要的密钥和服务。
8. 小实践:动手熟悉文件系统
理论归理论,动手更扎实。你可以在编辑器/IDE 里立即按以下步骤操作。
在你的项目中打开 app 目录,找到负责小部件的文件。 如果模板使用 app/page.tsx,你会在其中看到类似 “HelloWorld — ChatGPT App” 的字样或欢迎文本。如果没有单独的小部件目录,请打开 app/page.tsx,确认里面有 'use client' 和一些 JSX 标记。
然后找到 app/mcp/route.ts。留意它引入了哪些模块: 通常要么直接使用 MCP SDK,要么调用 lib/mcp/* 下的辅助函数。 评估这层“薄封装”的程度——理想情况下几乎没有业务逻辑,只有“接收 JSON → 交给服务器 → 返回 JSON”。
之后看看 next.config.ts 和 proxy.ts/middleware.ts。 不必看懂全部内容,只需确认:
- next.config.ts 负责 Next 的配置,包括构建与静态资源的规则;
- proxy.ts 会介入 HTTP 请求(几乎可以看到它在处理各种头)。
最后打开 .env 或 .env.local,确保你的密钥确实放在这里,而不是写进代码。 如果你看到 NEXT_PUBLIC_OPENAI_API_KEY——那就是一个很好的改正时机,趁现在还只是本地开发。
9. 可视化图示:ChatGPT 如何与模板交互
为让全貌更清晰,看看这个简单的流程:
flowchart TD
U[ChatGPT 中的用户] -->|输入请求| M[ChatGPT 模型]
M -->|调用 tool| MCP["你的 MCP endpoint
app/mcp/route.ts"]
MCP -->|"MCP 的 JSON 响应(structuredContent、_meta、UI 链接)"| M
M -->|决定展示 UI| WIDGET_URL["小部件的 URL
(/widget 或 /)"]
WIDGET_URL -->|iframe| W[你的小部件
app/page.tsx]
W -->|读取 window.openai.toolOutput
+ widgetState| U
这里要注意,发起方几乎总是 ChatGPT 模型本身,而不是传统 Web 应用中的用户浏览器。 你的 app/mcp/route.ts 和 app/widget/page.tsx 就是同一个 Next.js 项目的两道“门”:一道给机器人(MCP),一道给 UI。
只要牢记这张项目地图(小部件 → MCP 层 → 配置)并有意识地避开上述坑, 后续课程中你就能把精力集中在 App 的逻辑与 UX 上,而不是在“那个把一切都搞坏的文件”里反复摸索。
10. 使用模板结构时的常见错误
错误 №1:把小部件当成网站的普通页面。
有时开发者在模板里同时看到 app/page.tsx 与 app/widget/page.tsx, 改错了文件,结果在 ChatGPT 中看不到变化。小部件是被用作 MCP 工具的 outputTemplate/iframe 的那个页面。 如果你改的是另一个路由,ChatGPT 根本不会察觉。务必查看模板的 README,确认哪个 URL 被指定为小部件。
错误 №2:在 MCP 的服务器端文件里写客户端代码(window、document)。
文件 app/mcp/route.ts 以及它引入的所有内容都在服务器执行。任何使用 window 或 DOM API 的尝试都会导致运行时崩溃。如果要在 UI 做点什么,几乎肯定应该放在 app/widget 或其他客户端组件中。MCP 层是纯后端:请求、数据库、外部 API, 以及生成结构化响应。
错误 №3:忽视 assetPrefix 和 CORS 设置。
在本地的 localhost:3000 一切完美,但通过隧道在 ChatGPT 打开 App 时——样式丢失、 JS 不加载、控制台出现大量 CORS 错误。很多时候原因是 next.config.ts 或 middleware.ts/proxy.ts 的配置没有考虑新的公共 URL, 或在重构时被无意破坏。修改这些文件时,请始终记住你的代码将运行在 ChatGPT 域的 iframe 里, 而不是直接在 localhost。
错误 №4:不把密钥放在 .env 中,而是直接写在代码里,或放在 NEXT_PUBLIC_* 变量中。
把 OPENAI_API_KEY 藏在 const apiKey = 'sk-...' 之类的 app/widget/page.tsx 中是最糟糕的做法:密钥会出现在 JS 包里,任何用户都能拿到。 几乎同样糟糕的是使用 NEXT_PUBLIC_OPENAI_API_KEY,因为 NEXT_PUBLIC_ 前缀保证它会进入浏览器。请始终把密钥放到 .env(不带此前缀)里,并且只在服务器端使用(MCP 服务器、后端函数)。
错误 №5:把模板想得“太聪明”,不敢修改。
有时开发者把官方 starter 当作神圣不可触碰的东西:“最好别改,免得破坏集成”。 结果把所有代码都写在旁边,架构更复杂,还是踩同样的坑。其实模板不过是一个为 Apps SDK 做了少量配置的 Next.js 项目。 理解 app/ 既包含 UI 又包含 MCP,而其他都是常规配置,这会让你轻松很多: 你会像操作一个熟悉的 React/Next 项目那样工作,而不是对着“神秘盒子”发愁。
错误 №6:试图把所有问题都“在小部件层面”解决。
有时会想在 UI 里做完所有事情:业务逻辑、访问数据库、外部 API 请求。在 ChatGPT Apps 的语境下这尤为糟糕: 小部件运行在非常严格的沙箱中,看不到你的密钥,并且强烈依赖 window.openai。 如果要做严肃的事——请放在 MCP 层与后端服务中;小部件应当是轻薄的展示层,负责渲染结构化数据,并在必要时触发工具。
GO TO FULL VERSION