用 @tanstack/ai-vue 接流式对话:SSE 端点、useChat 与工具审批
TanStack AI 是 provider-agnostic 的类型安全 TypeScript SDK,用于流式聊天、工具调用 agent、结构化输出、多模态与语音。框架绑定覆盖 React / Solid / Vue / Svelte / Preact,另有 headless 的 @tanstack/ai-client。2026 JavaScript 开源奖 AI Project of the Year。
- 仓库:https://github.com/TanStack/ai
- 文档:https://tanstack.com/ai
- Vue 端 API:https://tanstack.com/ai/latest/docs/api/ai-vue
- 与 Vercel AI SDK 的对比:https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk
包结构上,核心是 @tanstack/ai,适配器有 @tanstack/ai-openai / ai-anthropic / ai-gemini / ai-openrouter / ai-ollama / ai-groq / ai-cloudflare 等;@tanstack/ai-vue 提供 composable,@tanstack/ai-vue/ui 提供 headless UI 组件。适配器按能力 tree-shake:openaiText(聊天)、openaiImage、falVideo、geminiSpeech 各自独立。
安装
pnpm add @tanstack/ai @tanstack/ai-openai
Vue 聊天 UI:
pnpm add @tanstack/ai @tanstack/ai-client @tanstack/ai-vue @tanstack/ai-openai
或者用 OpenRouter 一个 key 打多家:
pnpm add @tanstack/ai @tanstack/ai-openrouter
服务端 SSE 端点
任何能返回 Response 的运行时都行:
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const body = await request.json()
const stream = chat({ adapter: openaiText('gpt-5.2'), messages: body.messages, threadId: body.threadId })
return toServerSentEventsResponse(stream)
}
Vue 端 useChat
import { ref } from 'vue'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-vue'
const input = ref('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})
模板里遍历 message.parts 逐段渲染,part.type === 'text' 时取 part.content。
必填与互斥项:
threadId必填,用会话 id 或路由 id。connection与fetcher二选一,同时传会冲突。connection是fetchServerSentEvents这类连接适配器;fetcher是返回Response的函数,配合服务端函数使用。
其它选项:tools(客户端工具,可传 ref 或 getter,便于后续替换)、initialMessages、forwardedProps(作为 AG-UI forwardedProps 发给服务端,如 { model: 'gpt-6.1-sol' })、persistence(true 从服务端加载线程,或传 localStoragePersistence / sessionStoragePersistence / indexedDBPersistence)、outputSchema(结构化输出,返回带类型的 partial / final)、context(只给客户端工具用的运行时上下文,不发服务端)、live、queue(默认忙时排队)、interrupts、byok / byokProvider、devtools。
返回值都是只读 ref:script setup 里读 .value,模板里 Vue 自动解包,所以直接写裸名,不要再点 .value。
字段包括:
messages、sendMessage(content, options)、isLoading、errorstatus('ready' | 'submitted' | 'streaming' | 'error')、stop()、reload()(重跑最后一条用户消息)append(message)、addToolResult(result)、setMessages/clearqueue/cancelQueued、runIdinterrupts及resolveInterrupts/cancelInterrupts/retryInterruptsisHydrating、hasOlderMessages/loadOlderMessages(持久化分页)isSubscribed/connectionStatuspartial/final(仅传outputSchema时)
清理走 onScopeDispose:把 useChat 放在组件 setup 或 effectScope 内,作用域销毁时连接与监听一起回收。中断当前生成用 stop(),它和 scope 清理是两件事。
createChatHook 收敛配置
把选项在模块作用域绑定一次,返回带配置的 useChat:
export const { useChat: useSupportChat } = createChatHook({
connection: fetchServerSentEvents('/api/chat'),
})
组件里:
const chat = useSupportChat({ threadId: 'support-1' })
单次调用可覆盖 threadId / initialMessages / live / forwardedProps;tools、interrupts、outputSchema 必须留在工厂选项上,不能按调用点改。
toolDefinition:一份定义,服务端客户端共用
const getProducts = toolDefinition({
name: 'getProducts',
description: 'Search the product catalog',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.array(z.object({ id: z.string(), name: z.string() })),
}).server(async ({ query }) => db.products.search(query))
客户端工具用 .client(),会在浏览器里跑,通过 useChat 的 tools 传过去。同一个定义两边复用,schema 只有一份。
审批流:needsApproval 走 interrupts
toolDefinition 里加 needsApproval: true,这次 run 会暂停,待批调用出现在 interrupts 里,每条带 resolveInterrupt(true|false):
Approve: {{ interrupt.toolName }}
Approve
Deny
生成类 composable
一次性请求并保留结果:useGenerateImage / useGenerateAudio / useGenerateSpeech / useTranscription / useSummarize / useGenerateVideo / useGeneration(自定义输入输出)。每个返回 generate(input)、result、isLoading、error、status、stop()、reset()、runId。useGenerateVideo 额外有 jobId 与 videoStatus,并接受 onJobCreated / onStatusUpdate。
其它导出:useAudioRecorder(麦克风录音,返回 recording / isRecording / isSupported / start / stop / cancel,onComplete 可做转码)、useByok(BYOK keyring 快照,含 status / locked / prompt / storageError,存 key 用 byok.update(provider, value))、useRegisterWebMCPTools / usePageWebMCPTools(WebMCP 工具注册,Vue 作用域销毁时自动移除)。
没有导出的:realtime 聊天没有 composable,用包内 re-export 的 RealtimeClient;MCP App 用 createMcpAppBridge。
从 @tanstack/ai-client 再导出的东西
可以直接从 @tanstack/ai-vue 引入:
- 连接适配器:
fetchServerSentEvents/fetchHttpStream/fetchJson/xhrServerSentEvents/xhrHttpStream/webSocket/stream/rpcStream createChatClientOptions/clientTools- 持久化:
localStoragePersistence/sessionStoragePersistence/indexedDBPersistence - 客户端类:
ChatClient/GenerationClient/VideoGenerationClient/RealtimeClient/AudioRecorder/InterruptManager - 错误类型:
UnsupportedResponseStreamError/StreamTruncatedError/DurableStreamIncompleteError/StreamReconnectLimitError/StorageUnavailableError StreamProcessor与 chunk 策略、parsePartialJSON、消息转换器
结构化输出
chat() 传 outputSchema(Zod / ArkType / Valibot / JSON Schema),返回 typed 对象而不是自由文本;客户端 useChat 传 outputSchema 后拿到 partial(流式期间)与 final。
和 Vercel AI SDK 的差别
差异细节看官方对比页 https://tanstack.com/ai/latest/docs/comparison/vercel-ai-sdk。TanStack 这边的卖点是:provider 适配器可运行时切换、按能力 tree-shake、OpenTelemetry / middleware / devtools 属于一等包。
什么情况下值得换
自己写一套 fetch + ref,本质是把 SSE 解析、断线重连、消息分片、tool 调用往返、审批中断、线程持久化分页都手搓一遍。如果只是单向文本流、单一 provider、不需要工具调用,几十行 ReadableStream 循环加一个 ref 数组更直接,引入 SDK 反而多一层抽象。
需要 tool 调用往返、needsApproval 审批、跨轮次线程持久化、多 provider 运行时切换,或者要在 devtools 里看 stream chunk 时,这套东西的收益才明显——这些能力在自写版本里要么缺,要么散在几个文件里难维护。Vue 端只需要 Vue 3 + 一个能返回 Response 的服务端(Node / Bun / Cloudflare Workers 等),SDK 本身与框架无关。
Agent Skills 可以装到 Claude Code / Cursor:
npx skills add TanStack/ai -g --skill tanstack-ai tanstack-ai-migration