Skybridge:基于React与TypeScript的AI嵌入式应用全栈框架
1. 从零到一为什么我们需要 Skybridge如果你最近在捣鼓 ChatGPT 的 Apps SDK 或者 Model Context ProtocolMCP想给大模型对话里塞点交互式 UI那你大概率已经体验过那种“原始”的开发手感了。官方 SDK 确实给了你一把锤子但用它来盖房子你会发现连颗像样的钉子都难找。没有热重载HMR改一行代码就得重启、重装类型安全基本靠猜状态管理自己从头造轮子。更别提那些复杂的、需要在 UI 和 AI 模型之间同步状态的需求了光是想想就头大。Skybridge 就是为了解决这些痛点而生的。它不是一个简单的封装而是一个全栈的 TypeScript 框架专为构建嵌入在 AI 对话中的富交互应用我们姑且称之为“AI 嵌入式应用”设计。它的核心主张是用现代前端开发者最熟悉、最高效的方式——React TypeScript 完善的开发工具链——来构建这类应用。简单说它让你能用写现代 Web 应用的心智模型和工具链去开发运行在 ChatGPT、Claude 或任何 MCP 兼容客户端里的“小程序”。我花了些时间深度体验和用它构建了几个原型最大的感受是它把开发体验从“刀耕火种”拉回到了“精装公寓”的水平。你不再需要关心底层协议如何序列化数据也不用自己手动维护 UI 状态和模型认知之间的同步。Skybridge 提供了一套完整的、类型安全的抽象让你可以专注于业务逻辑本身。无论是想做一个航班查询工具、一个交互式数据看板还是一个复杂的多步骤游戏你都能找到熟悉的模式和工具。2. 核心架构解析Skybridge 如何统一前后端要理解 Skybridge 的价值得先拆开看看它的架构。它采用了典型的“前后端分离但类型共享”的全栈模式只不过这个“前端”运行在 AI 客户端的沙箱里“后端”则是你的 MCP 服务器。2.1 服务端类型即契约的 MCP 增强Skybridge 的服务端模块skybridge/server是对官方modelcontextprotocol/sdk的增强和封装。它最大的魔法在于将 Zod 模式定义变成了贯穿整个应用的类型契约。传统的 MCP 工具或资源定义输入输出往往是松散的any类型或者需要你手动编写繁琐的类型声明。在 Skybridge 里你使用 Zod 来定义工具Tools或部件Widgets的输入模式inputSchema。这个模式定义不仅会在运行时进行数据验证更重要的是它会通过 TypeScript 的类型推断自动生成对应的前端 Hook 类型。举个例子你定义一个查询天气的工具// server/weather.ts import { McpServer } from skybridge/server; import { z } from zod; const server new McpServer({ name: weather-service, version: 1.0.0 }); server.registerTool( getWeather, { description: Get current weather for a city, }, { // 使用 Zod 定义输入模式 city: z.string().min(1, City name is required), unit: z.enum([celsius, fahrenheit]).default(celsius), }, async ({ city, unit }) { // 模拟获取天气数据 const temperature unit celsius ? 22 : 72; return { content: [ { type: text, text: The current weather in ${city} is ${temperature}°${unit celsius ? C : F}., }, ], // 结构化数据可供部件使用 structuredContent: { city, temperature, unit, condition: Sunny }, }; } );这里的关键是inputSchema。Zod 对象{ city: z.string()... }会被 TypeScript 推断为{ city: string; unit: celsius | fahrenheit }类型。这个类型信息不会停留在服务端它会通过 Skybridge 的构建工具链自动同步到前端代码中。实操心得Zod 模式设计在设计inputSchema时尽量保持原子性和明确性。避免使用过于复杂的嵌套对象这会让前端调用变得困难。善用.describe()方法为字段添加描述这些描述有时会在 AI 客户端中作为提示信息展示。对于可选参数务必使用.optional()或提供.default()值这能显著提升工具调用的成功率。2.2 客户端React Hooks 驱动的类型安全 UI服务端定义了契约客户端skybridge/web则提供了一系列 React Hook 来消费这些契约。这是 Skybridge 最让人愉悦的部分——你获得了与使用 TanStack Query原 React Query或 SWR 类似的开发体验。以前面的天气工具为例在前端部件中你可以这样使用// client/WeatherWidget.tsx import { useTool } from skybridge/web; // 注意这里导入的是由 Skybridge 自动生成的类型化 Hook // 实际开发中useTool 会根据服务端注册的工具名提供对应的类型 function WeatherWidget({ initialCity }: { initialCity: string }) { // 1. 使用工具 Hook const { execute, output, isPending, isError, error } useTool(getWeather, { // 初始参数类型安全 initialArgs: { city: initialCity } }); const [city, setCity] useState(initialCity); const handleSearch () { // 执行工具调用参数类型自动匹配 execute({ city, unit: celsius }); }; if (isPending) { return divLoading weather for {city}.../div; } if (isError) { return divError: {error?.message}/div; } // output 的类型来自服务端工具返回值的 structuredContent // TypeScript 知道它是 { city: string; temperature: number; condition: string } return ( div h3Weather in {output?.city}/h3 pTemperature: {output?.temperature}°C/p pCondition: {output?.condition}/p input value{city} onChange{(e) setCity(e.target.value)} placeholderEnter city name / button onClick{handleSearch} disabled{isPending} Search /button /div ); }useTool这个 Hook 提供了你熟悉的所有状态isPending请求中、isError请求失败、data成功返回的数据。最重要的是execute函数的参数类型、output的数据结构都完全由服务端的 Zod 模式推断而来实现了真正的端到端类型安全。你在编写execute({ city: ... })时如果拼错参数名或者传递错误类型TypeScript 会在编辑器中立即报错。2.3 开发环境Vite 驱动的极致体验Skybridge 的开发体验之所以流畅离不开其基于 Vite 的深度集成。它提供了一个 Vite 插件为 MCP 应用开发带来了现代前端开发的核心能力热模块替换HMR修改前端部件React 组件的代码后几乎瞬间就能在连接的 AI 客户端如 Claude Desktop 的 MCP 开发模式中看到更新无需重启服务器或重装应用。开发工具模拟器本地运行一个开发服务器模拟 AI 客户端的运行环境让你可以在浏览器中独立开发和调试部件而不必每次都依赖真实的 AI 客户端。类型生成与同步在开发服务器启动时会自动分析服务端的工具/部件定义并实时生成对应的 TypeScript 类型声明文件.d.ts确保前端引用的类型永远是最新的。优化的生产构建使用 Vite 进行打包对代码进行压缩、分割生成适合部署的高效产物。启动一个 Skybridge 项目通常只需要npm create skybridgelatest my-ai-app cd my-ai-app npm run dev运行dev命令后你会同时获得一个 MCP 服务器和一个前端开发服务器。控制台会输出 MCP 服务器的连接信息通常是ws://localhost:...你可以将其配置到 Claude Desktop 等客户端的 MCP 设置中实现实时联动开发。注意事项开发环境配置首次配置 MCP 客户端如 Claude Desktop连接本地开发服务器时确保防火墙允许相关端口。在claude_desktop_config.json中mcpServers部分的command应指向你的服务器启动命令或者直接使用args配置 WebSocket 连接地址。如果遇到连接问题首先检查开发服务器是否正常启动并输出了正确的 WebSocket URL。3. 核心特性深度剖析与实战应用Skybridge 宣称的特性很多但哪些是真正改变开发范式的我们来深入几个核心特性看看它们在实际项目中如何发挥作用。3.1 部件到工具调用让 UI 驱动 AI 行动在传统的 AI 对话中流程是线性的用户输入 - AI 思考 - 调用工具 - 返回结果 - AI 回复。但在复杂的交互场景中我们往往希望 UI 上的一个按钮点击能直接触发一个特定的工具调用并让 AI 基于结果进行后续对话。这就是“部件到工具调用”Widget-to-Tool Calls要解决的问题。Skybridge 通过useToolHook 让这变得异常简单。在部件里你不仅可以读取工具执行的结果output还可以直接调用execute函数来触发工具。更重要的是这个调用可以被配置为“通知 AI 模型”。// 一个航班预订部件中的“查看详情”按钮 function FlightCard({ flight }) { const { execute, isPending } useTool(getFlightDetails, { // 设置 notifyModel: true工具执行后结果会发送给 AI 模型 notifyModel: true, }); const handleViewDetails () { execute({ flightId: flight.id }); // 执行后AI 模型会收到一个包含航班详情的消息 // 模型可以据此生成后续对话例如“已为您找到航班 XYZ 的详细信息包括...” }; return ( div span{flight.number}/span button onClick{handleViewDetails} disabled{isPending} {isPending ? Loading... : View Details} /button /div ); }当notifyModel: true时工具返回的content通常是文本会被自动插入到对话上下文中AI 模型能“看到”这个结果并做出反应。这就实现了 UI 交互驱动对话流程的闭环。实操心得notifyModel的使用策略不要对所有工具调用都设置notifyModel: true。这会导致对话被大量工具输出打断体验嘈杂。只对那些需要 AI 知晓并参与后续决策的关键步骤使用。例如在电商场景中“加入购物车”可能不需要通知模型但“提交订单”或“获取配送估算”这类需要 AI 确认或提供信息的结果就应该通知模型。3.2 双表面同步用>function ProductFilterWidget({ products }) { const [selectedCategory, setSelectedCategory] useState(all); const [maxPrice, setMaxPrice] useState(100); const filteredProducts products.filter(p (selectedCategory all || p.category selectedCategory) p.price maxPrice ); // 计算当前状态的 LLM 可读摘要 const llmState JSON.stringify({ action: filtering_products, filters: { category: selectedCategory, maxPrice: maxPrice }, resultCount: filteredProducts.length }); return ( div>npm create skybridgelatest todo-ai-assistant cd todo-ai-assistant npm install生成的项目结构大致如下todo-ai-assistant/ ├── src/ │ ├── client/ │ │ ├── components/ # React 部件 │ │ ├── main.tsx # 客户端入口 │ │ └── vite-env.d.ts │ ├── server/ │ │ ├── index.ts # MCP 服务器主文件 │ │ ├── tools/ # 工具定义 │ │ └── resources/ # 资源定义可选 │ ├── shared/ │ │ └── schema.ts # 共享的 Zod 模式定义 │ └── types/ # 自动生成的类型 ├── index.html ├── vite.config.ts # Vite 配置已集成 Skybridge 插件 ├── package.json └── tsconfig.json4.2 定义数据模型与共享模式在src/shared/schema.ts中我们定义待办事项的核心模式。这保证了前后端对数据结构的理解一致。// src/shared/schema.ts import { z } from zod; export const todoSchema z.object({ id: z.string().uuid(), title: z.string().min(1, Title is required), description: z.string().optional(), completed: z.boolean().default(false), dueDate: z.string().datetime().optional(), // ISO 8601 字符串 priority: z.enum([low, medium, high]).default(medium), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }); export type Todo z.infertypeof todoSchema; // 用于创建待办事项的输入模式 export const createTodoInputSchema z.object({ title: z.string().min(1), description: z.string().optional(), dueDate: z.string().datetime().optional(), priority: z.enum([low, medium, high]).optional(), }); // 用于更新待办事项的输入模式 export const updateTodoInputSchema z.object({ id: z.string().uuid(), title: z.string().min(1).optional(), description: z.string().optional().nullable(), completed: z.boolean().optional(), dueDate: z.string().datetime().optional().nullable(), priority: z.enum([low, medium, high]).optional(), }).partial(); // .partial() 使所有字段可选4.3 实现服务端工具接下来在src/server/tools/todos.ts中实现核心的业务逻辑工具。这里我们使用内存存储来简化实际项目中应连接数据库。// src/server/tools/todos.ts import { McpServer } from skybridge/server; import { todoSchema, createTodoInputSchema, updateTodoInputSchema } from ../../shared/schema; // 模拟内存数据库 const todos: Mapstring, Todo new Map(); export function registerTodoTools(server: McpServer) { // 1. 获取所有待办事项 server.registerTool( getTodos, { description: Get all todo items, optionally filtered by completion status, }, { filterCompleted: z.boolean().optional(), }, async ({ filterCompleted }) { let todoList Array.from(todos.values()); if (filterCompleted ! undefined) { todoList todoList.filter(todo todo.completed filterCompleted); } // 按创建时间倒序排列 todoList.sort((a, b) new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); return { content: [{ type: text, text: Found ${todoList.length} todo item(s)., }], structuredContent: { todos: todoList, total: todoList.length, }, }; } ); // 2. 创建待办事项 server.registerTool( createTodo, { description: Create a new todo item, }, createTodoInputSchema, // 直接使用共享的 Zod 模式 async (input) { const id crypto.randomUUID(); const now new Date().toISOString(); const newTodo: Todo { id, title: input.title, description: input.description, completed: false, dueDate: input.dueDate, priority: input.priority || medium, createdAt: now, updatedAt: now, }; // 验证数据符合模式 const validatedTodo todoSchema.parse(newTodo); todos.set(id, validatedTodo); return { content: [{ type: text, text: Created new todo: ${input.title}, }], structuredContent: validatedTodo, }; } ); // 3. 更新待办事项如标记完成 server.registerTool( updateTodo, { description: Update an existing todo item, }, updateTodoInputSchema, async ({ id, ...updates }) { const existingTodo todos.get(id); if (!existingTodo) { throw new Error(Todo with ID ${id} not found); } const updatedTodo: Todo { ...existingTodo, ...updates, updatedAt: new Date().toISOString(), }; const validatedTodo todoSchema.parse(updatedTodo); todos.set(id, validatedTodo); const actionText updates.completed ? completed : updated; return { content: [{ type: text, text: Todo ${existingTodo.title} ${actionText}., }], structuredContent: validatedTodo, }; } ); // 4. 删除待办事项 server.registerTool( deleteTodo, { description: Delete a todo item, }, { id: z.string().uuid(), }, async ({ id }) { const todo todos.get(id); if (!todo) { throw new Error(Todo with ID ${id} not found); } todos.delete(id); return { content: [{ type: text, text: Deleted todo: ${todo.title}, }], structuredContent: { success: true, deletedId: id }, }; } ); }然后在src/server/index.ts中初始化服务器并注册这些工具// src/server/index.ts import { McpServer } from skybridge/server; import { registerTodoTools } from ./tools/todos; const server new McpServer({ name: todo-ai-assistant, version: 1.0.0, }); // 注册所有待办事项相关的工具 registerTodoTools(server); // 可以在这里注册更多工具或资源... // 启动服务器 server.start().catch(console.error);4.4 构建交互式前端部件服务端准备好了现在来构建用户看到的 UI 部件。在src/client/components/TodoListWidget.tsx中// src/client/components/TodoListWidget.tsx import React, { useState, useEffect } from react; import { useTool, useToolMutation } from skybridge/web; // 注意useToolMutation 是一个用于创建/更新/删除等“变更操作”的 Hook // 它提供了 mutate 函数和乐观更新等高级特性。 function TodoListWidget() { // 获取待办事项列表 const { output: listOutput, isPending: isListPending, error: listError, execute: refetchTodos } useTool(getTodos, { initialArgs: {}, // 初始为空获取所有 }); // 创建待办事项的 mutation const createTodoMutation useToolMutation(createTodo, { onSuccess: () { // 创建成功后重新获取列表 refetchTodos({}); setNewTodoTitle(); }, }); // 更新待办事项的 mutation const updateTodoMutation useToolMutation(updateTodo); // 删除待办事项的 mutation const deleteTodoMutation useToolMutation(deleteTodo, { onSuccess: () { refetchTodos({}); }, }); const [newTodoTitle, setNewTodoTitle] useState(); const [filter, setFilter] useStateall | active | completed(all); const handleCreateTodo (e: React.FormEvent) { e.preventDefault(); if (!newTodoTitle.trim()) return; createTodoMutation.mutate({ title: newTodoTitle.trim(), priority: medium, }); }; const handleToggleComplete (todo: any) { updateTodoMutation.mutate({ id: todo.id, completed: !todo.completed, }); }; const handleDeleteTodo (id: string) { if (window.confirm(Are you sure you want to delete this todo?)) { deleteTodoMutation.mutate({ id }); } }; // 计算过滤后的待办事项 const todos listOutput?.todos || []; const filteredTodos todos.filter(todo { if (filter active) return !todo.completed; if (filter completed) return todo.completed; return true; }); // 构建 LLM 状态摘要 const llmState JSON.stringify({ context: viewing_todo_list, filter: filter, totalCount: todos.length, activeCount: todos.filter(t !t.completed).length, completedCount: todos.filter(t t.completed).length, }); if (isListPending todos.length 0) { return divLoading your todos.../div; } if (listError) { return divError loading todos: {listError.message}/div; } return ( div classNametodo-widget>// src/client/main.tsx import React from react; import { createRoot } from react-dom/client; import { registerWidget } from skybridge/web; import TodoListWidget from ./components/TodoListWidget; // 注册部件使其可以被服务端工具引用 registerWidget(todoList, TodoListWidget); // 渲染根组件开发模式下用于本地预览 const container document.getElementById(root); if (container) { const root createRoot(container); root.render(TodoListWidget /); }4.5 本地开发与调试启动开发服务器运行npm run dev。这会同时启动 MCP 服务器和 Vite 开发服务器。配置 MCP 客户端以 Claude Desktop 为例打开其配置目录如~/Library/Application Support/Claude/claude_desktop_config.json添加你的 MCP 服务器{ mcpServers: { todo-assistant: { command: node, args: [ /absolute/path/to/your/todo-ai-assistant/dist/server/index.js ], env: { NODE_ENV: development } } } }更简单的方式是在开发时Claude Desktop 可以直接连接 Skybridge 开发服务器提供的 WebSocket 地址npm run dev命令输出中会显示。实时调试现在你可以在 Claude 中通过自然语言与待办事项助手交互例如“帮我添加一个明天下午 3 点团队会议的准备事项”。同时你在代码编辑器如 VS Code中修改前端部件代码保存后Claude 对话中的部件界面会几乎实时更新无需任何手动刷新。4.6 构建与部署开发完成后需要构建生产版本npm run build这个命令会使用 Vite 打包和优化客户端代码。编译 TypeScript 服务端代码。生成一个dist/目录包含所有生产就绪的文件。部署时你需要部署 MCP 服务器将dist/目录下的文件部署到一个可以运行 Node.js 的服务器或 Serverless 平台如 Vercel、AWS Lambda。确保服务器可以通过 WebSocket 访问。配置生产环境 MCP 客户端在用户的 Claude Desktop 等客户端配置中将command指向你部署的服务器入口文件例如一个启动脚本或 Serverless 函数 URL。针对 ChatGPT Apps打包为插件如果你要发布到 ChatGPT 插件商店需要使用 OpenAI 的 Apps SDK 打包流程。Skybridge 的构建输出与 Apps SDK 兼容你可以按照 OpenAI 的文档进行后续的提交和审核。部署注意事项跨域与安全性在生产环境中务必配置好 CORS跨源资源共享和身份验证。MCP 服务器通常通过 WebSocket 通信但初始握手可能涉及 HTTP。确保你的服务器只接受来自可信客户端如特定域的 ChatGPT的连接。对于涉及用户数据的工具一定要实现严格的权限检查和输入验证。5. 进阶技巧与避坑指南在实际使用 Skybridge 构建复杂应用的过程中我积累了一些宝贵的经验和踩过的坑这里分享给大家。5.1 状态管理超越useState对于复杂的部件仅靠 React 的useState可能不够。Skybridge 部件本质上是 React 组件因此你可以引入任何你熟悉的状态管理库如 Zustand、Jotai 或 Redux Toolkit。关键在于需要同步到 AI 模型的状态务必通过>import { create } from zustand; // 使用 Zustand 管理复杂的过滤器状态 const useTodoStore create((set) ({ filters: { category: all, priority: null, dueBefore: null, }, setFilter: (key, value) set((state) ({ filters: { ...state.filters, [key]: value } })), })); function AdvancedTodoWidget() { const { filters, setFilter } useTodoStore(); // 将 Zustand 状态同步到 LLM const llmState JSON.stringify({ context: advanced_todo_view, activeFilters: filters, }); return ( div>// 一个包装了错误处理的工具调用 Hook function useSafeTool(toolName, options) { const result useTool(toolName, options); useEffect(() { if (result.isError) { // 可以在这里集成错误报告服务如 Sentry console.error(Tool ${toolName} failed:, result.error); // 或者显示一个全局 toast 通知 } }, [result.isError, result.error, toolName]); return result; } // 在组件中使用 function RobustComponent() { const { output, isPending } useSafeTool(getTodos); if (isPending) { return SkeletonLoader /; // 自定义骨架屏 } // ... 正常渲染 }5.3 性能优化避免不必要的重渲染和工具调用记忆化工具参数如果工具调用依赖于某些经常变化但实际不影响结果的变量如表单的临时输入使用useMemo或useCallback来稳定参数引用避免不必要的重复调用。const searchParams useMemo(() ({ query: debouncedQuery, // 使用防抖后的值 page: currentPage, }), [debouncedQuery, currentPage]); // 只有这两个值变化时才重新计算 const { output } useTool(searchItems, { initialArgs: searchParams });谨慎使用notifyModel如前所述只有关键的状态变更才需要通知 AI。频繁的通知会污染对话上下文降低模型性能。优化>// 在 Skybridge 工具中调用现有后端 API server.registerTool(getUserProfile, {}, { userId: z.string() }, async ({ userId }) { // 调用你已有的用户服务 const response await fetch(https://api.yourcompany.com/users/${userId}, { headers: { Authorization: Bearer ${process.env.API_KEY} }, }); if (!response.ok) { throw new Error(Failed to fetch user: ${response.statusText}); } const userData await response.json(); return { content: [{ type: text, text: Fetched profile for ${userData.name} }], structuredContent: userData, }; });5.5 常见问题排查工具调用无响应或超时检查首先在开发工具模拟器里测试工具是否能正常调用。如果可以问题可能出在 MCP 客户端配置或网络连接上。排查查看 MCP 服务器的日志确认 WebSocket 连接是否建立工具调用请求是否收到。检查工具函数内部是否有未处理的异常或死循环。解决确保工具函数返回的 Promise 能够正确 resolve 或 reject。对于长时间运行的操作考虑实现进度通知或将其拆分为异步任务。部件在客户端不显示或显示异常检查运行npm run build看是否有 TypeScript 错误或编译警告。在浏览器中打开开发服务器的预览地址通常是http://localhost:5173查看控制台是否有 JS 错误。排查确认部件是否已在main.tsx中正确registerWidget。检查部件的 props 类型是否与服务端工具返回的structuredContent类型匹配。解决使用 Skybridge 的 DevTools 检查部件接收到的 props 数据。确保服务端返回的structuredContent结构与前端期望的一致。类型错误或 TypeScript 报错检查运行npm run dev后检查src/types/目录下是否生成了最新的类型定义文件如skybridge.d.ts。排查如果类型生成失败可能是服务端的 Zod 模式定义有循环引用或复杂类型导致推断失败。尝试简化模式或使用z.lazy()处理循环引用。解决重启开发服务器。有时 Vite 的类型生成插件需要热更新才能捕获最新的服务端变更。>
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2557077.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!