客户端消费 (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 (可覆盖)

和发送端的对照关系:

发送端 / 接收端 (Send / Receive) 对照 服务端 (Server) — Node / Hono / Next.js streamText / ToolLoopAgent └─ result.toUIMessageStream() / createUIMessageStream({ execute }) └─ ReadableStream<UIMessageChunk> └─ HTTP SSE response body SSE / WebSocket / IPC 客户端 (Client) — React / @ai-sdk/react ChatTransport.sendMessages() └─ 读取 ReadableStream<UIMessageChunk> └─ AbstractChat — 状态机 + parts 累加 └─ useChat() React 绑定 └─ { messages, status, sendMessage, ... }

useChat 的能力矩阵

useChatAbstractChat 的 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 (不触发请求)
};

配置项 (UseChatOptionsChatInit):

配置类型用途
idstringchat 会话 id, 多 hook 实例共享 state 的钥匙
transportChatTransport传输实现, 默认 DefaultChatTransport (HTTP POST /api/chat)
messagesUI_MESSAGE[]初始消息 (用于 SSR 水合、从数据库加载历史)
onFinish(opts) => voidstream 成功完成时调
onError(error) => void任何错误触发时调
onToolCall(opts) => void收到 tool call 时调, 可同步 return 结果
onData(dataPart) => void每个 data-* 事件触发时调
sendAutomaticallyWhen(opts) => booleantool output 写入后, 是否自动 regenerate
generateId() => string自定义 id 生成器 (测试时稳定 id 用)
experimental_throttlenumberre-render 节流 (ms), 默认关闭
resumeboolean挂载时自动 reconnectToStream

useChat vs useCompletion vs useObject

@ai-sdk/react 里有三个场景型 hook,不是新旧版替换,各管一个独立场景:

useChatuseCompletionexperimental_useObject
场景多轮对话 (messages 数组)单轮文本补全 (prompt → text)单轮结构化输出 (prompt → 符合 schema 的 JSON 对象)
后端 APIstreamText().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_ 前缀, 签名可能变

选型要点:

  • 多轮对话 / agentuseChat
  • 一问一答、没有历史、没有工具 (写作助手、代码翻译、摘要) → 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协议用途典型后端
DefaultChatTransportHTTP POST + SSE最常见, 浏览器 ↔ server 全栈Next.js Route Handler / Hono / Express
TextStreamChatTransportHTTP POST + 纯文本流只发 text, 不走 UI message 协议老代码 / 非 AI SDK 后端
DirectChatTransport进程内无 HTTP, 直接调 AgentSSR / 测试 / 单进程 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 但结构固定)。三种场景需要自定义:

  1. WebSocket —— 长连接, 双向消息, 资源广播 (多 tab 同步聊天状态)
  2. IPC (Electron) —— renderer 和 main 之间, 跑在 Node 侧的 agent
  3. 跨进程定制协议 (Electron IPC / Chrome extension messaging / ServiceWorker postMessage)

自定义时两个方法都必须实现——reconnectToStream 用不到的话返回 Promise.resolve(null) 即可。内部流必须是 ReadableStream<UIMessageChunk>,chunk 类型要和服务端发出的一致 (参见 UI 流编排)。

UIMessage vs ModelMessage —— 最容易混淆的两个类型

两者在 SDK 里是完全不同的类型,混用会静默出错:

UIMessageModelMessage
来自@ai-sdk/react / ai 的 UI 消息协议ai 的模型调用协议
客户端useChatmessages 就是这个客户端几乎不接触
服务端HTTP body 收到的是这个塞给 streamText({ messages }) 的是这个
结构{ id, role, metadata, parts: UIMessagePart[] }{ role, content: string | ContentPart[] }
包含text / reasoning / tool (带 state) / data-* / file / source / step-starttext / 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-startstep 边界标记可视分隔符 / 不显示

data-* 的消费有两条路径:

  • 持久化的 (transient: false) → 进 message.parts, 遍历时渲染
  • transient 的 → 不进 parts, 要在 onData 回调里拦截 (见下文)

status 状态机

ChatStatus 状态机 (State Machine) 4 态 — submitted · streaming · ready · error submitted streaming ready error sendMessage() 首个 chunk 到达 finish chunk 再次 sendMessage() error chunk / fetch reject clearError() 源: @ai-sdk/react — ChatStatus = 'submitted' | 'streaming' | 'ready' | 'error'

状态含义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)。

messagemessages 的区分:

  • 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.partsonData 回调
能看到 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
});

服务端侧 (核心):

  • DefaultChatTransportreconnectToStream 会 POST 到 ${api}?chatId=${id} (默认)
  • 后端必须在请求开始时把流缓存下来 (Redis / 内存), 收到重连请求时再把缓存 + 后续 chunks 一并返回
  • 如果流已经完成,返回 null 让客户端走正常渲染

实现 pattern: 在 createUIMessageStream 外面再包一层缓冲区 (如 createUIMessageStreamResponseconsumeSseStream + Redis)。具体实现参见官方 resumableStream 教程或自建。

常见误区:

错误真相
只设 resume: true 就能自动恢复❌ 后端不实现缓冲,重连请求拿到 null, 什么也恢复不了
resume 能恢复 HTTP 以外的 transportDirectChatTransport.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. sendMessageawait → 按钮 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 章节

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 客户端
这页有帮助吗?