基于 OpenAI Apps SDK 的 NLWeb MCP 服务器集成指南:构建带交互式 Widget 的 ChatGPT Connector
基于 OpenAI Apps SDK 的 NLWeb MCP 服务器集成指南:构建带交互式 Widget 的 ChatGPT Connector
导读
本文以 openai-apps-sdk-integration 目录下的官方参考实现为主线,系统讲解如何将 NLWeb 检索后端通过 Model Context Protocol(MCP)接入 OpenAI Apps SDK,使 ChatGPT 在对话中既能调用工具完成搜索,又能以内联 Widget 的形式渲染 Schema.org 结构化结果与 Data Commons 可视化图表。读完本文,你将掌握 NLWeb MCP 服务器的整体架构、混合 Widget 设计原理、本地三终端联调方法、生产部署步骤与完整排障手段,并了解 Node 侧工具注册、Widget 元数据封装与前端 React 渲染的源码级实现细节。
NLWeb Apps SDK 集成概览
NLWeb 是使用 Python 实现的主参考实现,提供检索、排名与问答能力;而 openai-apps-sdk-integration 目录则承载了面向 ChatGPT 生态的接入层,包含两大部分:
- NLWeb MCP Server(Node):位于 nlweb_server_node,使用官方 TypeScript SDK 实现,对外暴露 MCP 工具(默认工具名为
nlweb-search)与两个 Widget 资源。 - Rich UI Widgets(React):位于 src 下,包含
nlweb-list混合 Widget 与nlweb-datacommons可视化 Widget,经 Vite 构建为带哈希版本的独立assets/产物,供 MCP 服务器引用。
MCP + Apps SDK 的协作模型
Model Context Protocol(MCP)是一套开放的规范,用于把大模型客户端与外部工具、数据和用户界面连接起来。MCP 服务器暴露模型在对话中可以调用的工具,并按工具契约返回结果;这些结果可以携带额外元数据(例如内联 HTML),Apps SDK 据此在助手消息旁渲染富 UI 组件(Widget)。
一个最小的 Apps SDK MCP 集成需要实现三项能力:
- 列出工具(List Tools):服务器通告其支持的每个工具,包括 JSON Schema 形式的输入/输出契约与可选注解(例如
readOnlyHint)。 - 调用工具(Call Tools):模型选定工具后发出
call_tool请求,参数与用户意图匹配;服务器执行动作并返回模型可解析的结构化内容。 - 返回 Widget(Return Widgets):在结构化内容之外,将内嵌资源放在响应元数据(
_meta)中,使 Apps SDK 客户端(ChatGPT)能够内联渲染界面。
由于协议与传输解耦,服务器既可以跑在 Server-Sent Events(SSE)上,也可以跑在流式 HTTP 上,Apps SDK 两种方式都支持。
仓库结构速览
| 目录/文件 | 职责 |
|---|---|
| src/nlweb-list | 混合 Widget 入口(同时处理 Schema.org 结果与可视化) |
| src/nlweb-datacommons | 可视化 Widget 组件(图表、地图、排名) |
| src/shared | 两个 Widget 共用的 UI 组件 |
assets/ |
构建后生成的带哈希版本 HTML/JS/CSS 产物 |
| nlweb_server_node | 基于官方 TypeScript SDK 的 MCP 服务器 |
| build-all.mts | Vite 构建编排器,产出带哈希的 Widget 产物 |
环境准备与依赖安装
前置条件
- Node.js 18+
- npm 或 pnpm(根工作区推荐 pnpm,见 package.json 中声明的
packageManager: pnpm@10.13.1) - 运行中的 NLWeb 后端(默认指向
localhost:8100的 AppSDK 适配器)
安装工作区依赖
npm install
# 或
pnpm install
根目录 package.json 声明了 React 19、Vite 7、Tailwind CSS 4、zod、framer-motion、mapbox-gl、mermaid 等依赖;nlweb_server_node/package.json 则声明了 @modelcontextprotocol/sdk、eventsource、node-fetch、zod 与 tsx、typescript 等运行时与构建依赖。
构建 Widget 产物
NLWeb 列表 Widget 被打包为独立静态资源,供 MCP 服务器引用:
npm run build
该命令通过 tsx ./build-all.mts 执行 build-all.mts:它用 fast-glob 扫描 src/**/index.{tsx,jsx},仅构建 nlweb-list 与 nlweb-datacommons 两个入口,随后以 package.json 中 version 字段的 SHA-256 前 4 位作为哈希重命名产物,并在 assets/ 下生成自包含的 nlweb-list-{hash}.html 单文件。产出为带版本的 .html、.js、.css 文件,例如 nlweb-list-2d2b.js。
本地迭代可启动 Vite 开发服务器:
npm run dev
vite.config.mts 固定了 4444 端口(strictPort: true,cors: true),并内置多入口开发插件:访问 http://localhost:4444/nlweb-list.html 即可热更新预览。
静态资源服务
构建后,为本地开发提供静态资源服务:
npm run serve
该命令执行 serve -s ./assets -p 4444 --cors,将 assets/ 暴露在 http://localhost:4444 并开启 CORS。
运行 NLWeb MCP 服务器
启动与测试
cd nlweb_server_node
# 启动 MCP Server(HTTP/SSE 模式,默认端口 8000)
npm start
# 运行自动化测试套件
npm run test
npm start 实际执行 tsx src/server.ts。服务器暴露两个端点:
- SSE 流:
GET http://localhost:8000/mcp - 消息提交:
POST http://localhost:8000/mcp/messages?sessionId=...
详见 nlweb_server_node/README.md。
自动化测试套件做了什么
npm run test 运行 test-server.mjs,它会自动:
- 连接 SSE 流并解析
endpoint事件中的sessionId; - 发送
tools/list,断言返回工具列表; - 发送
resources/list,断言返回 Widget 资源; - 发送
resources/read(URIui://widget/nlweb-list.html),检查资源内容; - 以
tools/call调用工具(测试查询为spicy snacks+ 站点seriouseats),检查content、structuredContent.results与_meta中的openai/outputTemplate等 Widget 元数据; - 输出测试汇总(Passed / Failed)。
测试通过 POST 消息端点提交 JSON-RPC 2.0 请求,SSE 传输协议预期返回 HTTP 202 Accepted,再等待通过 SSE 推送的响应。
在 ChatGPT 中测试集成
通用接入流程
将本地应用加入 ChatGPT,需先在 ChatGPT 中启用 developer mode(设置 → Developer Mode),然后在 Settings → Connectors 中添加应用。对于未部署的本地服务器,可用 ngrok 之类的隧道工具将其暴露到公网。
三终端快速联调
终端 1 —— 提供 Widget 静态资源:
npm run serve # Serves at http://localhost:4444
终端 2 —— 启动 MCP 服务器:
cd nlweb_server_node
npm start # Runs on port 8000
终端 3 —— 用 ngrok 暴露端口:
ngrok http 8000
ngrok 会给出一个公网 URL,将其填入 ChatGPT 的 Settings → Connectors 即可。例如:
https://<custom_endpoint>.ngrok-free.app/mcp
生产部署请参考 DEPLOYMENT.md。
混合 Widget 架构:一次解决 ChatGPT 的 Widget 缓存问题
设计动机
ChatGPT 客户端会依据工具定义缓存“加载哪个 Widget”。如果尝试按数据动态切换不同 Widget,缓存可能导致渲染错误。NLWeb 集成因此采用混合 Widget 架构:单个 nlweb-list Widget 自动识别并渲染两种数据类型:
- Schema.org 结构化数据 —— 传统搜索结果(餐厅、文章、地点等);
- 交互式可视化 —— Data Commons 图表、地图、排名及内嵌组件。
工作流程
┌──────────────────────────────────────────────────┐
│ ChatGPT loads nlweb-list widget │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ MCP Server returns structuredContent.results │
│ Each result has: @type, name, url, etc. │
│ OR: visualizationType, html, script, etc. │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ Widget detects result type: │
│ - Has html/script/visualizationType? │
│ → Use VisualizationBlock component │
│ - Regular Schema.org data? │
│ → Use ResultItem component │
└──────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────┐
│ VisualizationBlock: │
│ 1. Loads Data Commons script if needed │
│ 2. Injects result.html into DOM │
│ 3. Web components self-initialize │
│ 4. Renders interactive charts/maps │
└──────────────────────────────────────────────────┘
关键收益
- 无缓存问题:单个 Widget 处理两类数据,客户端缓存不会造成渲染错位;
- 向后兼容:既有 Schema.org 结果照常工作;
- 面向未来:同一响应中可混排可视化与普通结果;
- 架构简化:无需复杂的动态 Widget 选择逻辑。
组件结构(源码视角)
src/
├── nlweb-list/ # 主混合 Widget
│ ├── index.jsx # 检测结果类型并路由到对应渲染器
│ └── nlweb-list.css # Schema.org 结果样式
├── nlweb-datacommons/ # 可视化组件
│ ├── index.jsx # 独立可视化 Widget(备用)
│ ├── VisualizationBlock.jsx # 单个可视化渲染器
│ └── nlweb-datacommons.css # 可视化样式
└── shared/ # 共享组件
└── NLWebComponents.jsx # Header、Container、EmptyState
源码级的数据路由逻辑
在 src/nlweb-list/index.jsx 中,App 组件对每个 result 判段:若命中 visualizationType || html || script,则渲染 VisualizationBlock;否则渲染 ResultItem(Schema.org 列表项)。ResultItem 会从 result 或其嵌套 schema_object 中提取标题、描述、图片与评分:图片优先取 thumbnail / image / thumbnailUrl,其次取 schema_object.image(数组取首项);评分优先取 score,其次取 rating 或 aggregateRating.ratingValue。
可视化所需的脚本由 App 组件统一加载(index.jsx 中 useEffect):解析 result.script 中的 <script src>,去重后以 async 方式挂到 document.head。随后 VisualizationBlock(VisualizationBlock.jsx)把 result.html 直接注入容器 DOM,Data Commons Web Components 会自初始化渲染交互图表;它还支持展开/折叠,并在下方展示 places、variables 元数据。共享的 NLWebComponents.jsx 提供查询标题、结果计数、Save Results 按钮与空状态。
Widget 数据格式示例
Schema.org 结果:
{
"@type": "Restaurant",
"name": "Joe's Pizza",
"description": "Best pizza in town",
"image": "https://...",
"rating": 4.5,
"url": "https://..."
}
可视化结果:
{
"@type": "StatisticalResult",
"visualizationType": "map",
"html": "<datacommons-map header='...' ...></datacommons-map>",
"script": "<script src='https://datacommons.org/datacommons.js'></script>",
"places": ["geoId/06"],
"variables": ["Percent_Person_WithDiabetes"]
}
MCP 服务器的源码级实现
工具与 Widget 元数据封装
nlweb_server_node/src/server.ts 是 MCP 服务器的核心。它定义了 NLWebWidget 类型(id、title、templateUri、invoking、invoked、html)与 widgetMeta() 辅助函数,统一生成 Apps SDK 所需的元数据:
function widgetMeta(widget: NLWebWidget) {
return {
"openai/outputTemplate": widget.templateUri,
"openai/toolInvocation/invoking": widget.invoking,
"openai/toolInvocation/invoked": widget.invoked,
"openai/widgetAccessible": true,
"openai/resultCanProduceWidget": true
} as const;
}
服务器注册了两个 Widget 资源(nlweb-list 与 nlweb-visualization),资源 MIME 类型为 text/html+skybridge,并实现了 resources/list 与 resources/read 处理器(server.ts L220-L247)。两个 Widget 的 html 内容均引用 http://localhost:4444 上的构建产物,例如:
<div id="nlweb-list-root"></div>
<link rel="stylesheet" href="http://localhost:4444/nlweb-list-2d2b.css">
<script type="module" src="http://localhost:4444/nlweb-list-2d2b.js"></script>
工具定义:nlweb-search
服务器通过 ListToolsRequestSchema 注册工具 nlweb-search(注意:实际代码中的工具名为 nlweb-search,而 nlweb_server_node/README.md 描述为 nlweb-results,测试脚本则调用 nlweb-list,仓库内存在命名不一致,以 server.ts 中的实际注册名为准)。工具输入 Schema 使用 zod 定义:
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
query |
是 | string | 问题或搜索查询 |
site |
否 | string | 指定站点,使用裸站点名(如 datacommons,不要带 .org 后缀) |
mode |
否 | enum | list(默认,结构化结果)/ summarize / generate |
prev |
否 | string[] | 用于携带上下文的先前对话轮次 |
工具调用链路
调用 nlweb-search 时(server.ts L292-L342):
- 用
NLWebAskInputSchema.parse校验参数; callNLWebAsk()向${NLWEB_APPSDK_BASE_URL}/ask发起 GET 请求,拼接query、streaming=false,并可选带上site、mode;用AbortController+REQUEST_TIMEOUT实现超时控制,超时抛错Request timeout after Xms;selectWidget(response)检查structuredContent.results:若任一 result 含visualizationType/html/script字段则选中可视化 Widget,否则选中列表 Widget,并打印Widget Selection Debug日志;- 返回 Apps SDK 兼容响应:
content(文本摘要)、structuredContent(完整结果、messages、metadata、conversationId、generatedAnswers、legacyResponse)、_meta内嵌openai.com/widget资源与widgetMeta()。
return {
content: response.content,
structuredContent: response.structuredContent,
_meta: {
"openai.com/widget": widgetResource,
...widgetMeta(widget),
},
};
注释指出:structuredContent 会经 window.openai.toolOutput 暴露给前端,前端通过 useWidgetProps()(use-widget-props.ts)读取,后者基于 useSyncExternalStore 监听 window.openai 全局(use-openai-global.ts)。
HTTP/SSE 传输层
server.ts 用 node:http 实现传输层:GET /mcp 建立 SSE 会话(SSEServerTransport 生成 sessionId,会话存入 Map),POST /mcp/messages?sessionId=... 转发 JSON-RPC 消息;同时为两个路径处理 OPTIONS 预检并设置 CORS 头(Access-Control-Allow-Origin: *)。端口由 PORT 环境变量控制,默认 8000。
环境变量参考
| 变量 | 默认值 | 说明 |
|---|---|---|
NLWEB_APPSDK_BASE_URL |
http://localhost:8100 |
NLWeb AppSDK 后端 API 地址(对应 Python 侧 appsdk_adapter_server.py 之类适配器的监听地址) |
REQUEST_TIMEOUT |
30000 |
NLWeb 请求超时时间(毫秒) |
PORT |
8000 |
HTTP 模式下服务器端口 |
在 server.ts 中三者分别通过 process.env.NLWEB_APPSDK_BASE_URL || "http://localhost:8100" 与 parseInt(process.env.REQUEST_TIMEOUT || "30000", 10) 读取。
开发与定制
定制 Widget 显示
编辑 src/nlweb-list/index.jsx 可修改结果展示逻辑。修改后:
- 重新构建:
npm run build - 记录文件名中的新哈希(例如
nlweb-list-xxxx.js) - 同步更新 nlweb_server_node/src/server.ts 中的哈希引用
- 重启 MCP 服务器
哈希由
package.json的version字段派生(见 build-all.mts 中crypto.createHash("sha256").update(pkg.version)),因此每次改版本号或构建都会得到新哈希,务必保证服务器 HTML 片段中的文件名与assets/实际产物一致。
修改服务器
编辑 nlweb_server_node/src/server.ts 可以:
- 修改 NLWeb AppSDK 后端地址(
NLWEB_APPSDK_BASE_URL); - 定制工具参数与描述;
- 新增额外工具或 Widget。
调试日志
集成内置了较完整的调试日志:
服务器侧(MCP):
- NLWeb 适配器返回的响应结构(
structuredContentkeys、results 数量) - 结果计数与类型(
First result @type) - Widget 选择逻辑(
Widget Selection Debug,输出resultCount、hasVisualization、selectedWidget) - 字段存在性检查(
html、script、visualizationType)
客户端侧(Widget):
- Widget 初始化(
🎨 NLWeb List Widget,含resultsCount与hasVisualizations) - 数据接收
- 可视化检测与 HTML 注入过程(
✅ Injecting HTML for visualization:) - 脚本加载日志(
✅ Loaded script:)
排障时同时检查浏览器控制台与服务器日志。
生产部署指南
部署架构
生产环境由两部分组成:
- MCP Server —— Node.js 服务,处理工具调用与 Widget 元数据;
- Widget Assets —— 供 UI 加载的静态 HTML/CSS/JS。
┌─────────────────┐
│ ChatGPT │
│ (Client) │
└────────┬────────┘
│ HTTP/SSE
┌────────▼────────┐ ┌──────────────────┐
│ MCP Server │──────▶│ NLWeb Backend │
│ (Port 8000) │ HTTP │ (API Server) │
└────────┬────────┘ └──────────────────┘
│ References
┌────────▼────────┐
│ Widget Assets │
│ (CDN/Static) │
│ - nlweb-list.js │
│ - nlweb-list.css│
└─────────────────┘
完整步骤见 DEPLOYMENT.md。
第一步:构建并记录哈希
npm run build
ls -la assets/ | grep nlweb-list
预期输出类似:
nlweb-list-2d2b.css
nlweb-list-2d2b.html
nlweb-list-2d2b.js
第二步:托管 Widget 静态资源
方案 A —— CDN(推荐生产使用)
- CloudFlare R2 / Pages:
wrangler login后执行wrangler r2 object put nlweb-assets/nlweb-list-2d2b.js --file=assets/nlweb-list-2d2b.js,产物公开后可获得https://your-bucket.r2.dev/nlweb-list-2d2b.js形式的 URL; - AWS S3 + CloudFront:
aws s3 cp assets/nlweb-list-2d2b.js s3://your-bucket/widgets/上传并设置public-readACL,同时在桶上配置 CORS(AllowedOrigins: ["*"]、AllowedMethods: ["GET"]),使用 CloudFront 域名访问; - Azure Blob Storage + CDN:
az storage blob upload --account-name youraccount --container-name widgets --name nlweb-list-2d2b.js --file assets/nlweb-list-2d2b.js --content-type application/javascript,启用公共访问与 CORS。
方案 B —— 本地静态服务器(仅开发)
npm run serve
⚠️ 该方式仅适用于本地测试,ChatGPT 无法访问 localhost。
第三步:更新 MCP 服务器中的资源 URL
编辑 nlweb_server_node/src/server.ts,将 nlwebListWidget / nlwebVisualizationWidget 的 html 中 http://localhost:4444 替换为 CDN 地址:
const nlwebWidget: NLWebWidget = {
id: "nlweb-list",
title: "NLWeb Results",
templateUri: "ui://widget/nlweb-list.html",
invoking: "Searching NLWeb",
invoked: "Found results",
html: `
<div id="nlweb-list-root"></div>
<link rel="stylesheet" href="https://YOUR-CDN-URL/nlweb-list-2d2b.css">
<script type="module" src="https://YOUR-CDN-URL/nlweb-list-2d2b.js"></script>
`.trim(),
responseText: "Rendered a NLWeb result list!"
};
第四步:部署 MCP 服务器
方案 A —— Azure App Service:
cd nlweb_server_node
npm install
az webapp create \
--resource-group your-rg \
--plan your-plan \
--name nlweb-mcp-server \
--runtime "NODE:18-lts"
az webapp config appsettings set \
--resource-group your-rg \
--name nlweb-mcp-server \
--settings \
NLWEB_APPSDK_BASE_URL=<TODO> \
REQUEST_TIMEOUT="30000" \
PORT="8000"
az webapp deployment source config-zip \
--resource-group your-rg \
--name nlweb-mcp-server \
--src deploy.zip
部署后端点形如 https://nlweb-mcp-server.azurewebsites.net/mcp。
方案 B —— AWS(EC2 或 App Runner)+ PM2 + nginx:
安装 Node 18 与依赖后设置环境变量,用 PM2 守护:
sudo npm install -g pm2
pm2 start npm --name "nlweb-mcp" -- run start:http
pm2 startup
pm2 save
再用 nginx 反代(SSE 场景务必关闭缓冲):
server {
listen 80;
server_name your-domain.com;
location /mcp {
proxy_pass http://localhost:8000/mcp;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# SSE specific settings
proxy_buffering off;
proxy_cache off;
}
}
方案 C —— ngrok 快速验证: 本地 npm run start:http 后 ngrok http 8000,将 https://abc123.ngrok-free.app/mcp 填入 ChatGPT。
第五步:在 ChatGPT 中配置
- 启用 Developer Mode(Settings → Developer Mode);
- 在 Settings → Connectors 中点击 Add Connector,填入 MCP 端点 URL(生产
https://your-domain.com/mcp,ngrok 则https://abc123.ngrok-free.app/mcp); - 新开对话验证,例如询问 "Search for spicy snacks on seriouseats.com",
nlweb-listWidget 应渲染出结果列表。
部署验证
# 检查 MCP 服务器是否返回 SSE 流
curl -N https://your-domain.com/mcp
# 运行测试套件
cd nlweb_server_node && npm run test
# 验证 Widget 资源可访问
curl https://your-cdn-url/nlweb-list-2d2b.css
curl https://your-cdn-url/nlweb-list-2d2b.js
生产检查清单
- [ ] Widget 资源已上传 CDN 且开启 CORS
- [ ] 服务器以 HTTPS 部署(ChatGPT 强制要求 HTTPS)
- [ ] 环境变量已配置
- [ ] 服务器代码中的资源 URL 已更新为新哈希
- [ ] 测试套件通过
- [ ] ChatGPT Connector 已配置
- [ ] 已启用监控与日志
- [ ] 已制定备份/恢复方案
故障排查
可视化不渲染
如果 Data Commons 可视化显示为 HTML 代码而非交互组件:
1. 确认所有服务都在运行:
# NLWeb Core (port 8000)
curl http://localhost:8000/ask?query=test
# AppSDK Adapter (port 8100)
curl http://localhost:8100/ask?query=test
# Widget Server (port 4444)
curl http://localhost:4444/nlweb-list-2d2b.js
2. 检查浏览器控制台:
- 查找 "🎨 NLWeb List Widget" 日志,确认
hasVisualizations: true; - 查找 "✅ Injecting HTML for visualization" 消息;
- 检查脚本加载错误。
3. 检查 MCP 服务器日志:
- 应出现 "Widget Selection Debug" 且
hasVisualization: true; - 应显示含
html、script、visualizationType字段的结果结构。
4. 确认 Data Commons 脚本加载: 打开浏览器 DevTools → Network 标签,查看是否从 datacommons.org 加载了 datacommons.js。
5. 常见问题:
- 协议错误:URL 应为
http://localhost:8100而非https://; - AppSDK 适配器未启动:用
python -m webserver.appsdk_adapter_server启动(对应仓库 AskAgent/webserver/appsdk_adapter_server.py); - 结果不在
structuredContent.results中:检查适配器侧的响应转换; - Widget 缓存:在 ChatGPT 中强制刷新(macOS
Cmd+Shift+R/ WindowsCtrl+Shift+R)。
全栈验证脚本
仓库提供一个验证适配器响应结构的脚本:
cd openai-apps-sdk-integration
./test_adapter_response.sh
该脚本验证 8100 端口连通性、响应结构合法性、可视化字段存在性,并检查首条结果内容。
服务启动顺序
为了获得最佳效果,按以下顺序启动服务:
- NLWeb Core(端口 8000 或云端托管)
- AppSDK Adapter(端口 8100 或云端托管)—— 负责把 NLWeb 响应转换为 Apps SDK 兼容格式
- Widget Server(端口 4444)—— 提供静态资源
- MCP Server(端口 8000)—— 连接 AppSDK 适配器
其他部署期问题
- Widget 不渲染:检查 CORS 是否放行跨域、CSS/JS URL 是否可访问、浏览器控制台错误、以及文件名哈希与服务器引用是否一致;
- 连接问题:确认 8000 端口已开放、生产环境使用 HTTPS、检查服务器日志;
- ChatGPT 不调用工具:核对 Connector URL、查看服务器是否收到请求、用测试套件验证服务器工作正常。
安全注意事项
- HTTPS 必需:ChatGPT 只接受 HTTPS 端点;
- CORS 配置:仅放行必要来源;
- 限流:建议在服务器上实现速率限制;
- API 密钥:如需鉴权,可在对 NLWeb 后端的调用中加入认证;
- 内容安全策略:为 Widget 资源配置 CSP 头。
小结
NLWeb 的 Apps SDK 集成通过"一个 MCP 服务器 + 一个混合 Widget"的极简架构,把 NLWeb 的检索、排名与问答能力安全地接到 ChatGPT 中:服务器侧用官方 TypeScript SDK 注册 nlweb-search 工具、以 openai/outputTemplate 等元数据携带 Widget 契约;前端用单个 nlweb-list Widget 兼容 Schema.org 列表与 Data Commons 可视化两类数据,规避了客户端 Widget 缓存问题。无论是本地三终端快速联调(serve + npm start + ngrok),还是走 CDN + HTTPS 的生产部署,都可参照本指南逐步完成。下一步建议深入阅读 nlweb_server_node/README.md、DEPLOYMENT.md 以及 server.ts、build-all.mts 等源码,进一步定制自己的 Widget 与工具。