客户端消费 (useChat)
useChat 的完整能力矩阵 / 四种 Transport 的选型 / UIMessage vs ModelMessage 桥接 / status 四态的实际含义 / 回调契约 / 客户端工具执行 / resume 断线重连的双端协议 / 10 个常见陷阱
为什么需要这一页
前四页讲的全是发送端——streamText 怎么跑步循环、createUIMessageStream
怎么编排 UI 流、消息链怎么组装。但流发出去之后,接收端怎么把它变成 React
state、渲染在用户面前,是另一个完全独立的话题。
AI SDK 官方为此提供了 @ai-sdk/react 包,里面的 useChat hook 是最常见的接收端实现——一行代码接上后端,就能拿到:
messages(累加的对话状态)status(流的四态机)sendMessage/regenerate/stop等操作方法error+ 错误恢复钩子
这一页把 useChat 的内部模型讲透——它不是黑盒,理解了 AbstractChat + ChatTransport + ChatStatus
这三件事,定位 “为什么 status 卡在 submitted”、“为什么 onError 后消息还在”、“为什么 tool call 没渲染” 都会变得直接。
不是非用不可——如果你已有成熟的 server state 管理(React Query、Redux、Zustand…),自己实现一个接收端完全可行。本页末尾的 Case study 就展示了一个不用 useChat 的生产选择,以及它的权衡点。
一眼看懂
| 关键数字 | 值 |
|---|---|
锚定 @ai-sdk/react 版本 | 3.0.136 |
锚定 ai 核心包版本 | 6.0.134 |
ChatStatus 状态数 | 4 (submitted / streaming / ready / error) |
ChatTransport 内置实现 | 3 (DefaultChatTransport / TextStreamChatTransport / DirectChatTransport) |
| Tool UI 状态数 | 6 (input-streaming / input-available / approval-requested / approval-responded / output-available / output-error) |
onFinish payload 中的”未完成”标志位 | 3 (isAbort / isDisconnect / isError) |
| 默认 API endpoint | /api/chat (可覆盖) |
和发送端的对照关系:
useChat 的能力矩阵
useChat 是 AbstractChat 的 React 绑定。返回值结构:
type UseChatHelpers<UI_MESSAGE> = {
readonly id: string; // chat 会话 id (不显式传 options.id 则随机生成)
messages: UI_MESSAGE[]; // 累加的对话状态
status: ChatStatus; // submitted / streaming / ready / error
error: Error | undefined; // 最近一次错误
sendMessage: (...) => Promise<void>; // 发一条 user message, 触发 LLM 调用
regenerate: (...) => Promise<void>; // 重新生成指定 (或最后一条) assistant message
stop: () => Promise<void>; // 中止当前 stream
resumeStream: (...) => Promise<void>; // 断线后重连当前 stream
addToolOutput: (...) => void; // 异步推送客户端工具执行结果
addToolResult: (...) => void; // @deprecated, 用 addToolOutput
addToolApprovalResponse: (...) => void; // 推送工具审批结果
clearError: () => void; // 清 error state, 回到 ready
setMessages: (msgs) => void; // 本地修改 messages (不触发请求)
};
配置项 (UseChatOptions → ChatInit):
| 配置 | 类型 | 用途 |
|---|---|---|
id | string | chat 会话 id, 多 hook 实例共享 state 的钥匙 |
transport | ChatTransport | 传输实现, 默认 DefaultChatTransport (HTTP POST /api/chat) |
messages | UI_MESSAGE[] | 初始消息 (用于 SSR 水合、从数据库加载历史) |
onFinish | (opts) => void | stream 成功完成时调 |
onError | (error) => void | 任何错误触发时调 |
onToolCall | (opts) => void | 收到 tool call 时调, 可同步 return 结果 |
onData | (dataPart) => void | 每个 data-* 事件触发时调 |
sendAutomaticallyWhen | (opts) => boolean | tool output 写入后, 是否自动 regenerate |
generateId | () => string | 自定义 id 生成器 (测试时稳定 id 用) |
experimental_throttle | number | re-render 节流 (ms), 默认关闭 |
resume | boolean | 挂载时自动 reconnectToStream |
useChat vs useCompletion vs useObject
@ai-sdk/react 里有三个场景型 hook,不是新旧版替换,各管一个独立场景:
useChat | useCompletion | experimental_useObject | |
|---|---|---|---|
| 场景 | 多轮对话 (messages 数组) | 单轮文本补全 (prompt → text) | 单轮结构化输出 (prompt → 符合 schema 的 JSON 对象) |
| 后端 API | streamText().toUIMessageStreamResponse() | streamText().toTextStreamResponse() | streamObject({ schema }).toTextStreamResponse() |
| 输入 | sendMessage({ text }) | complete(prompt) | submit(input) (input 作为 JSON body) |
| 状态 | messages[] + status 四态机 | completion 字符串 + isLoading 布尔 | object: DeepPartial<T> (流式部分填充) + isLoading |
| Schema | 无 (纯对话) | 无 (纯文本) | 必需 (Zod / JSON Schema, 客户端校验 + 类型推导) |
| 工具 | 支持 tool call state 机 | 不支持 | 不支持 |
| 流式渲染粒度 | part 级 (text-delta / tool-input-delta / …) | token 级字符追加 | 字段级 (DeepPartial——字段从 undefined → 部分字符串 → 完整) |
| 自带 input 状态 | 否 (你自己管) | 是 (input / handleInputChange / handleSubmit) | 否 (submit 由你触发) |
| 稳定性 | 稳定 | 稳定 | experimental_ 前缀, 签名可能变 |
选型要点:
- 多轮对话 / agent →
useChat - 一问一答、没有历史、没有工具 (写作助手、代码翻译、摘要) →
useCompletion - 生成一个有 shape 的对象并边生成边渲染(表单字段填充、卡片模板、行程规划、菜谱)→
experimental_useObject。关键价值是DeepPartial让”菜名已出、食材列表前 3 条已出、步骤仍 undefined”这种中间态也能渲染出来,而不是等整个 JSON 生成完再一次性显示——对”看得见模型在思考”的体验提升非常明显。
Transport 四选一
ChatTransport 接口只有两个方法:
interface ChatTransport<UI_MESSAGE> {
sendMessages(options): Promise<ReadableStream<UIMessageChunk>>;
reconnectToStream(options): Promise<ReadableStream<UIMessageChunk> | null>;
}
内置三种实现 + 自定义兜底:
| Transport | 协议 | 用途 | 典型后端 |
|---|---|---|---|
DefaultChatTransport | HTTP POST + SSE | 最常见, 浏览器 ↔ server 全栈 | Next.js Route Handler / Hono / Express |
TextStreamChatTransport | HTTP POST + 纯文本流 | 只发 text, 不走 UI message 协议 | 老代码 / 非 AI SDK 后端 |
DirectChatTransport | 进程内 | 无 HTTP, 直接调 Agent | SSR / 测试 / 单进程 Electron |
| 自定义 | 任意 | WebSocket / IPC / 跨进程 | 需要双向消息、长连接、或非 HTTP 协议 |
DefaultChatTransport 配置
useChat({
transport: new DefaultChatTransport({
api: "/api/chat", // 默认就是 /api/chat, 需要换就改这里
credentials: "include", // 带 cookie (跨域认证)
headers: { Authorization: ... },
body: { sessionId: "xxx" }, // 额外透传给后端的字段 (会 merge 进 request body)
prepareSendMessagesRequest: ({ messages, ... }) => ({ // 完全重写 body
body: convertToMyFormat(messages),
}),
fetch: customFetch, // 拦截 / mock 用
}),
});
body vs prepareSendMessagesRequest:
body是追加——原有{ messages, ... }加上你的字段prepareSendMessagesRequest是完全替换——只用你返回的 body (测试或后端 schema 特殊时用)
DirectChatTransport —— 绕开 HTTP
useChat({
transport: new DirectChatTransport({
agent: myAgent, // 直接传 Agent 实例
options: { model, ... },
}),
});
agent.stream() 的输出不走 HTTP,在同一 JS 进程里直接管道给 useChat。适合:
- SSR / RSC 预渲染时需要一次补全
- Jest / Vitest 里测试 UI 但不想起真实 server
- Electron 主进程里跑 agent, renderer 里消费
限制: reconnectToStream 返回 null (进程内没断连概念)。
什么时候自定义
内置三种都基于 HTTP (DirectChatTransport 虽然绕 HTTP 但结构固定)。三种场景需要自定义:
- WebSocket —— 长连接, 双向消息, 资源广播 (多 tab 同步聊天状态)
- IPC (Electron) —— renderer 和 main 之间, 跑在 Node 侧的 agent
- 跨进程定制协议 (Electron IPC / Chrome extension messaging / ServiceWorker postMessage)
自定义时两个方法都必须实现——reconnectToStream 用不到的话返回 Promise.resolve(null) 即可。内部流必须是
ReadableStream<UIMessageChunk>,chunk 类型要和服务端发出的一致 (参见
UI 流编排)。
UIMessage vs ModelMessage —— 最容易混淆的两个类型
两者在 SDK 里是完全不同的类型,混用会静默出错:
UIMessage | ModelMessage | |
|---|---|---|
| 来自 | @ai-sdk/react / ai 的 UI 消息协议 | ai 的模型调用协议 |
| 客户端 | useChat 的 messages 就是这个 | 客户端几乎不接触 |
| 服务端 | HTTP body 收到的是这个 | 塞给 streamText({ messages }) 的是这个 |
| 结构 | { id, role, metadata, parts: UIMessagePart[] } | { role, content: string | ContentPart[] } |
| 包含 | text / reasoning / tool (带 state) / data-* / file / source / step-start | text / image / file / tool-call / tool-result |
| 设计目的 | UI 消费:累加、重试、工具状态机、自定义事件 | 模型调用:给 LLM 的 prompt 格式 |
桥接函数: convertToModelMessages(uiMessages) (在 ai 包, 返回 Promise<ModelMessage[]>)。
典型的 Next.js Route Handler:
// POST /api/chat
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-6"),
messages: await convertToModelMessages(messages), // ← 桥接
tools,
});
return result.toUIMessageStreamResponse(); // 返回 UIMessage 流给 client
}
常见错误:
- 客户端直接把
ModelMessage塞给setMessages→ 类型不对, parts 缺失 - 服务端直接
streamText({ messages: uiMessages })→ ModelMessage 才是 streamText 参数 - 忘 await
convertToModelMessages→ ModelMessage[] 变成 Promise (编译能过,运行时炸)
UIMessagePart 的九种类型
客户端渲染消息就是遍历 message.parts,处理每种 type:
type 前缀 | 用途 | 渲染要点 |
|---|---|---|
text | 纯文本 delta 累加 | 流式显示 .text |
reasoning | 推理过程 (Anthropic thinking, OpenAI reasoning) | 可选折叠显示 |
tool-${name} | 静态工具调用 (L1 注册了 schema) | 按 state 分支渲染, 见下一节 |
dynamic-tool | 动态工具 (MCP / 运行时发现) | 同上, 但 toolName 需从 part 读 |
source-url / source-document | 引用来源 | 渲染引用卡片 / 脚注 |
file | 文件 part (图片等) | <img> / 下载链接 |
data-${string} | 自定义事件 payload (非 transient 的) | 按 type 分发渲染 |
step-start | step 边界标记 | 可视分隔符 / 不显示 |
data-* 的消费有两条路径:
- 持久化的 (transient: false) → 进
message.parts, 遍历时渲染 - transient 的 → 不进 parts, 要在
onData回调里拦截 (见下文)
status 状态机
| 状态 | 含义 | UI 常见做法 |
|---|---|---|
submitted | 请求已发出,等待第一个 chunk | 显示”正在连接…”; 禁用输入 |
streaming | 第一个 chunk 到了,流进行中 | 显示 cursor 光标; 启用 stop 按钮 |
ready | 完整收完,可以发下一条 | 启用输入 |
error | 出错 | 显示错误 + 重试按钮; 调 clearError() 恢复 |
最常见的”卡在 submitted”问题: 服务端已经收到请求,但第一个 chunk 还没 flush (LLM 还在思考、或者 server 在做耗时准备工作)。不是 hook 坏了——优化方法是:
- 服务端在
createUIMessageStream({ execute })的 execute 开头立刻writer.write({ type: "data-progress", ... })推一个 transient 事件,强制第一个 chunk 尽快发出 - 或者直接推
data-run-init持久化事件 (见 UI 流编排)
这样 status 会在几十 ms 内切到 streaming,用户感知不到延迟。
回调契约
onFinish({ message, messages, isAbort, isDisconnect, isError, finishReason })
最容易踩的三态区分:
| 标志 | 含义 | 应对 |
|---|---|---|
isAbort | 用户主动调了 stop() | 保留已有 partial 内容, UI 可标记”已中止” |
isDisconnect | 网络断连 (fetch reject / 连接被切) | 显示”网络问题,重连?” 并暴露 resumeStream() |
isError | 服务端返回了 error chunk 或 SDK 内部错误 | 查 error 对象,显示真错误 |
三者互斥——某次 finish 触发,只有一个为 true。但如果都是 false,finishReason 才是主要信号 (stop / length /
tool-calls / content-filter / other)。
message 和 messages 的区分:
message: 刚结束的那条 assistant message (单条)messages: 包含这条 message 的完整历史数组
持久化对话通常只需要 messages——message 是方便你立刻对新产出做副作用用的 (比如 toast、analytics)。
onError(error)
关键点: onError 被调用后,messages 不会被 SDK 自动回滚。你刚 push 的 user
message 还在 state 里,用户看到”发出去但没回应”。
正确的错误恢复:
const { messages, setMessages, sendMessage, clearError } = useChat({
onError: (error) => {
toast.error(error.message);
// 可选: 回滚最后一条 user message
setMessages((prev) => {
if (prev[prev.length - 1]?.role === "user") {
return prev.slice(0, -1);
}
return prev;
});
// 或者不回滚,保留用户输入,让用户点"重试"触发 regenerate
},
});
两种策略:
- 回滚: 适合”一次性操作,失败就当没发生过”
- 保留 + 重试: 适合”对话式操作,用户可以继续编辑 input”
onToolCall({ toolCall }) + 客户端工具
场景: 某些工具在浏览器侧执行 (读地理位置、读 localStorage、弹原生文件选择器、调用浏览器 API),不走后端。
useChat({
onToolCall: async ({ toolCall }) => {
if (toolCall.toolName === "getLocation") {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject);
});
// 同步 return 会被 SDK 自动塞回为 output
return { lat: position.coords.latitude, lng: position.coords.longitude };
}
},
});
同步 return = 简单场景,SDK 自动把返回值塞进 tool output state,自动触发下一轮。
异步长操作 = 用 addToolOutput 明确推送:
const { addToolOutput } = useChat({
onToolCall: ({ toolCall }) => {
if (toolCall.toolName === "selectFile") {
// 不 return, 只是启动一个 UI 流程
openFilePickerModal((file) => {
addToolOutput({
tool: "selectFile",
toolCallId: toolCall.toolCallId,
output: { fileName: file.name, content: await file.text() },
});
});
}
},
});
onData(dataPart)
用途: 捕获后端 writer.write({ type: "data-xxx" }) 推送的每个 data-* 事件——包括 transient 的。
和直接遍历 message.parts 的区别:
遍历 message.parts | onData 回调 | |
|---|---|---|
| 能看到 transient 事件吗 | ❌ | ✅ |
| 每次 part 新增都触发 | ❌ (re-render 才看到) | ✅ (每个事件一次) |
| 适合 | 渲染持久化的业务数据 | 一次性副作用 (toast / 进度 / 打点) |
典型用法:
useChat({
onData: (part) => {
if (part.type === "data-progress") {
// transient, 不进 message.parts
setProgress(part.data.percent);
}
if (part.type === "data-toast") {
toast(part.data.message);
}
},
});
sendAutomaticallyWhen({ messages })
场景: 客户端工具执行完,要不要立刻 regenerate 让 LLM 接着跑下一步?
useChat({
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
// ↑ 内置: 如果最后一条 assistant message 的所有 tool call 都有 output 了, 自动 regenerate
});
不设这个回调 → tool output 写进 state 就停住,要手动 regenerate() 才会继续。设了 →
tool 执行完自动进下一步,形成完整的 agent 循环在客户端也能闭环。
客户端工具 UI 状态机
tool call 在消息 parts 里的生命周期:
input-streaming
↓ (参数 delta 推完)
input-available
↓
├→ (工具需要人工审批, 极少)
│ ↓
│ approval-requested → approval-responded
│ ↓
▼
output-available 或 output-error
必须在每个状态下都有 UI 反馈,否则会出现:
input-streaming没 UI → 用户看不到参数正在流式生成input-available没 UI → 工具正在执行,用户以为卡死output-error没 UI → 工具报错了,用户只看到对话戛然而止
简单渲染模板:
message.parts.map((part, i) => {
if (isToolUIPart(part)) {
switch (part.state) {
case "input-streaming":
return <span key={i}>正在生成参数...</span>;
case "input-available":
return (
<span key={i}>
调用 {part.type} ({JSON.stringify(part.input)})...
</span>
);
case "output-available":
return <ToolResult key={i} tool={part.type} output={part.output} />;
case "output-error":
return (
<span key={i} className="text-red-500">
失败: {part.errorText}
</span>
);
}
}
// ... 其他 part 类型
});
工具类型有两种: ToolUIPart<TOOLS> (L1 注册 schema 的静态工具) 和 DynamicToolUIPart (动态工具 / MCP)。前者
part.type === "tool-${name}",后者 part.type === "dynamic-tool"。用 isToolUIPart / isDynamicToolUIPart 区分。
resume / 断线重连
这是双端协议,只设 resume: true 不够:
客户端侧 (最简):
useChat({
id: "task-123",
resume: true, // 挂载时自动调 reconnectToStream
});
服务端侧 (核心):
DefaultChatTransport的reconnectToStream会 POST 到${api}?chatId=${id}(默认)- 后端必须在请求开始时把流缓存下来 (Redis / 内存), 收到重连请求时再把缓存 + 后续 chunks 一并返回
- 如果流已经完成,返回
null让客户端走正常渲染
实现 pattern: 在 createUIMessageStream 外面再包一层缓冲区 (如 createUIMessageStreamResponse 的 consumeSseStream +
Redis)。具体实现参见官方 resumableStream 教程或自建。
常见误区:
| 错误 | 真相 |
|---|---|
只设 resume: true 就能自动恢复 | ❌ 后端不实现缓冲,重连请求拿到 null, 什么也恢复不了 |
resume 能恢复 HTTP 以外的 transport | ❌ DirectChatTransport.reconnectToStream 永远返回 null (进程内没断连概念) |
| 用户刷页就自动重连 | ⚠️ 要设稳定的 chat id (不传或随机生成 → 刷页后 id 变了, 对不上缓存) |
experimental_throttle —— 别忘了设
默认关闭,每个 chunk 都触发 React re-render。一次长回答 1000+ text-delta 会:
- React 重渲整条对话
- 浏览器掉帧,滚动卡顿
- 主线程忙,onToolCall 之类的异步回调延迟
useChat({
experimental_throttle: 50, // 50ms 合并 chunk, 一次 re-render
});
取值经验:
50(20fps): 默认推荐, 肉眼无感100(10fps): 低端设备 / 移动端16(60fps): 高端设备要丝滑0或不设: 除非你真的每个 chunk 都要即时反应 (极罕见)
throttle 的代价: text-delta 不是一字一字出,是一批一批出。阅读体验上基本无差,性能上差好几倍。
常见陷阱
1. 不设 id → 每次 hook 重挂生成新 chat
useChat(); // ❌ id 随机生成, 组件重挂后变了
useChat({ id: chatId }); // ✅ id 稳定, 历史关联得上
多个组件 (sidebar preview + main chat) 要共享同一 chat state 也必须传同 id。
2. onError 后 messages 不自动回滚
见上文”回调契约 / onError”小节。默认行为是保留——配合”点击重试”场景。要回滚就自己 setMessages。
3. convertToModelMessages 是 async,服务端要 await
// ❌ messages 变成 Promise<ModelMessage[]>, 编译过但运行时错
streamText({ messages: convertToModelMessages(uiMessages) });
// ✅
streamText({ messages: await convertToModelMessages(uiMessages) });
4. isAbort vs isDisconnect vs isError 都当成”失败”
三者语义完全不同 (见”回调契约 / onFinish”小节)。错误恢复策略:
isAbort→ 不做任何事, 用户主动的isDisconnect→ 暴露resumeStream()重连isError→ 查error, 显示真因
5. 直接 mutate messages 数组
messages.push({ ... }); // ❌ React 看不到变化, 不 re-render
messages[0].parts.push({ ... }); // ❌ 同上 + 可能污染 SDK 内部 state
setMessages([...messages, newMsg]); // ✅
6. 忘渲染 input-available / output-error 状态
tool call UI 只渲染 output-available 会造成”工具正在执行 → 用户看不到任何东西 → 以为卡死”。必须覆盖所有 6 个 state。
7. 不设 experimental_throttle 导致掉帧
长回答里 text-delta 动辄 1000+, 每个都 re-render 性能炸。至少 50ms。
8. sendMessage 不 await → 按钮 loading state 不对
const handleClick = () => {
sendMessage({ text: input }); // 立即返回, button loading 状态错
};
const handleClick = async () => {
setLocalLoading(true);
await sendMessage({ text: input });
setLocalLoading(false);
};
或者直接用 status (更推荐): status === "submitted" || status === "streaming" 就是”忙着”。
9. 用 useCompletion 跑多轮对话
useCompletion 只保留最新一次补全,历史会丢。多轮必须 useChat。
10. SSR 水合时 messages 不一致
服务端渲染时 useChat 不知道历史,客户端 hydrate 后也是空 → 页面先白一下再填充。
useChat({
id: chatId,
messages: initialMessagesFromServer, // SSR 传进来
});
messages 这个 init 参数就是给水合场景用的。
Case study: 什么时候不用 useChat
Zapvol 的选择: 用 TanStack React Query + 自定义 streaming hook, 不用 useChat。
为什么不用
| 角度 | useChat 假设 | Zapvol 现实 |
|---|---|---|
| 聊天 state 生命周期 | ”一次会话 = 一个 hook 实例” | Task 有独立生命周期 (创建 / 运行 / 暂停 / 恢复 / 归档), 和 hook 挂载解耦 |
| 历史加载 | init messages 传进来就行 | 历史、运行状态、metadata、user info 都是 server state, React Query 一以贯之 |
| 跨组件共享 | 同一 id 的多个 useChat 共享 | task list 页、task 详情页、preview modal, 都要看同一 task state——React Query 的 query client 更顺 |
| 错误恢复 | 挂在 hook 里 | 错误 state 要跨页面持久化 (从 task list 跳详情还是 error) |
| 协议定制 | DefaultChatTransport 已足够 | 要加 SSE resume (Redis buffered), browser bridge 双向通信, task abort controller 管理, BUA 控制, 多个并发 task 独立 abort |
核心决策: agent 运行产生的不只是”一段对话”,是”一个有状态的任务”。用 React Query 把 task 当成 server
resource,一次拿到 { task, messages, status, metadata, artifacts } 整包,用 SSE
hook 订阅流式更新,比 useChat 的模型更贴合。
放弃的东西
用自己的 hook 替代 useChat 要自己实现:
status四态机 (其实 React Query 的isPending/isError等状态已覆盖大半, 只是语义要自己映射)experimental_throttle(自己实现 buffer + flush)- tool call UI 状态机 (自己解析 UIMessageChunk 流里的 tool-input-start / tool-input-available / …)
onToolCall机制 (Zapvol agent 都是 server 侧工具,不需要客户端 tool)convertToModelMessages的 client 侧桥接 (Zapvol backend 自己持久化UIMessage派生类型ZapvolMessage、在调streamText之前自己调convertToModelMessages,不经过useChat的内置桥接路径)
什么项目应该选 useChat
- 纯聊天应用, 没有 task / run 概念
- 没有复杂 server state 管理需求 (React Query / SWR 没用上)
- 后端已经是标准 Next.js API Route 或 Hono, 一问一答
- 对手/追赶时间重要,少写代码比掌控粒度更值
这不是 “useChat 不好”,是”定位是否匹配”——useChat 是”对话中心”的抽象,超出这个范畴时它开始卡手。
延伸阅读
相关 SDK 章节
- UI 流编排 — 发送端对应章节, 讲
createUIMessageStream/writer/ data-* 事件协议 - 运行生命周期 — 整体时间线 (三层 12 回调)
- 消息引用模型 — UIMessage / ModelMessage 的 SDK 内部模型
SDK 源码锚点
@ai-sdk/[email protected]——dist/index.d.ts第 39 行 (useChat签名) / 第 13-37 行 (UseChatHelpers/UseChatOptions)[email protected]——dist/index.d.ts第 3589-3656 行 (ChatTransport接口) / 3719 行 (ChatStatus) / 3753-3790 行 (ChatInit) / 3815-3880 行 (AbstractChat.sendMessage等方法)[email protected]——dist/index.d.ts第 4004-4007 行 (DefaultChatTransport) / 4042-4057 行 (DirectChatTransport) / 4078-4083 行 (TextStreamChatTransport)
Zapvol 落地参考
packages/app/src/hooks/— 自定义 streaming hook 的组织方式packages/app/src/api/modules/task.ts— 不走 useChat 的前端 API 客户端