代码 用 @tanstack/ai-vue 接流式对话:SSE 端点、useChat 与工具审批

2026-11-12 21:32:08

用 @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、error
  • status('ready' | 'submitted' | 'streaming' | 'error')、stop()、reload()(重跑最后一条用户消息)
  • append(message)、addToolResult(result)、setMessages / clear
  • queue / cancelQueued、runId
  • interrupts 及 resolveInterrupts / cancelInterrupts / retryInterrupts
  • isHydrating、hasOlderMessages / loadOlderMessages(持久化分页)
  • isSubscribed / connectionStatus
  • partial / 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
复制全文 生成海报 Vue TypeScript 大模型 SSE 开源项目

推荐文章

程序员茄子在线接单