客户端消费 (useChat)

为什么需要这一页

前四页讲的全是发送端——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 版本4.0.9
锚定 ai 核心包版本7.0.8
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 的能力矩阵

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):

配置类型用途
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_ 前缀, 签名可能变

选型要点:

  • 多轮对话 / 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协议用途典型后端
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 / ServiceWorker postMessage)

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

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

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

UIMessageModelMessage
来自@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-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,
  });

  // v7: `result.toUIMessageStreamResponse()` 方法已废弃 —— 改用独立辅助函数
  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  }); // 返回 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)。

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.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
});

服务端侧 (核心):

  • 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(); // WRONG —— id 随机生成,组件重挂后变了
useChat({ id: chatId }); // OK —— id 稳定,历史关联得上

多个组件 (sidebar preview + main chat) 要共享同一 chat state 也必须传同 id。

2. onError 后 messages 不自动回滚

见上文“回调契约 / onError”小节。默认行为是保留——配合“点击重试”场景。要回滚就自己 setMessages。

3. convertToModelMessages 是 async,服务端要 await

// WRONG —— messages 变成 Promise<ModelMessage[]>,编译过但运行时错
streamText({ messages: convertToModelMessages(uiMessages) });

// OK
streamText({ messages: await convertToModelMessages(uiMessages) });

4. isAbort vs isDisconnect vs isError 都当成“失败”

三者语义完全不同 (见“回调契约 / onFinish”小节)。错误恢复策略:

  • isAbort → 不做任何事, 用户主动的
  • isDisconnect → 暴露 resumeStream() 重连
  • isError → 查 error, 显示真因

5. 直接 mutate messages 数组

messages.push({ ... });           // WRONG —— React 看不到变化,不 re-render
messages[0].parts.push({ ... });  // WRONG —— 同上 + 可能污染 SDK 内部 state

setMessages([...messages, newMsg]);  // OK

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

aibuddy 的选择: 用 TanStack React Query + 自定义 streaming hook, 不用 useChat。

为什么不用

角度useChat 假设aibuddy 现实
聊天 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)、task abort controller 管理、WebSocket 资源广播、多个并发 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 机制 (aibuddy agent 都是 server 侧工具,不需要客户端 tool)
  • convertToModelMessages 的 client 侧桥接 (aibuddy backend 自己持久化 UIMessage 派生类型 AibuddyMessage、在调 streamText 之前自己调 convertToModelMessages,不经过 useChat 的内置桥接路径)

什么项目应该选 useChat

  • 纯聊天应用, 没有 task / run 概念
  • 没有复杂 server state 管理需求 (React Query / SWR 没用上)
  • 后端已经是标准 Next.js API Route 或 Hono, 一问一答
  • 对手/追赶时间重要,少写代码比掌控粒度更值

这不是 “useChat 不好”,是“定位是否匹配”——useChat 是“对话中心”的抽象,超出这个范畴时它开始卡手。

延伸阅读

相关 SDK 章节

SDK 源码锚点

  • @ai-sdk/react@4.0.9 —— src/use-chat.ts:53 (useChat 签名;UseChatHelpers / UseChatOptions 同文件)
  • ai@7.0.8 —— src/ui/chat-transport.ts:15-83 (ChatTransport 接口) / src/ui/chat.ts:131 (ChatStatus) / src/ui/chat.ts (ChatInit、AbstractChat.sendMessage 等方法)。v7 里这些已移入 ai core,@ai-sdk/react 再导出。
  • ai@7.0.8 —— src/ui/default-chat-transport.ts:12-36 (DefaultChatTransport) / src/ui/direct-chat-transport.ts:55-143 (DirectChatTransport) / src/ui/text-stream-chat-transport.ts (TextStreamChatTransport)

aibuddy 落地参考

  • packages/app/src/hooks/ — 自定义 streaming hook 的组织方式
  • packages/app/src/api/modules/task.ts — 不走 useChat 的前端 API 客户端
这页有帮助吗?