Wave Terminal 中的 AI SDK UIMessage 类型体系:从泛型定义到消息渲染的完整实践
【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm
UIMessage是 Vercel AI SDK 中用于描述应用状态的消息模型:它承载完整的消息历史、元数据、数据部件(data parts)与工具调用上下文,是前端渲染useChat会话消息的"唯一事实来源"(source of truth)。本文以 aisdk-uimessage-type.md 为骨架,结合 Wave Terminal(AI 集成跨平台终端)的 Wave AI 面板源码,系统讲解UIMessage的三个泛型参数、全部UIMessagePart类型定义、如何构造自定义类型的UIMessage,以及这些类型如何在前端渲染、工具审批、流式更新中被真实消费。读完本文,你将能够为任意 AI 聊天应用设计出类型安全的UIMessage模型,并理解 AI SDK 流式协议与UIMessage部件之间的映射关系。
一、UIMessage 与 ModelMessage:两种消息模型的职责划分
在 AI SDK 中,UIMessage与ModelMessage是两套面向不同消费者的消息模型:
ModelMessage:表示传给模型的状态或上下文,是模型推理的输入(例如转换为 OpenAI Chat Completions、Anthropic Messages 格式的历史消息)。UIMessage:表示应用的完整状态,包含渲染 UI 所需的全部信息——消息 ID、角色、元数据,以及结构化的parts数组。它同时被客户端功能(重试、编辑、分支、工具审批等)所消费。
Wave Terminal 的实现恰好体现了这一分工:Go 后端在 uctypes.go 中定义了与 AI SDK 对齐的UIMessage/UIMessagePartJSON 结构(UseChatRequest.Messages、UIChat.Messages均以[]UIMessage传输),并将这些消息转换为各提供商(Anthropic / OpenAI / Gemini)的GenAIMessage原生格式;而前端 aitypes.ts 直接从"ai"包导入UIMessage作为 Wave AI 面板的消息基类。前后端通过同一套 JSON 字段契约(如toolCallId、input、output、state、providerMetadata)保持类型一致。
二、类型安全:UIMessage 的三个泛型参数
UIMessage被设计为完全类型安全,接受三个泛型参数:
METADATA:消息级自定义元数据类型,用于附加额外信息(默认unknown)。DATA_PARTS:自定义数据部件(data part)类型,用于结构化数据组件(默认UIDataTypes)。TOOLS:工具定义集合,用于类型安全的工具交互(默认UITools)。
泛型参数的约束关系体现在接口签名上:
interface UIMessage<METADATA = unknown, DATA_PARTS extends UIDataTypes = UIDataTypes, TOOLS extends UITools = UITools> { id: string; role: "system" | "user" | "assistant"; metadata?: METADATA; parts: Array<UIMessagePart<DATA_PARTS, TOOLS>>; }从声明可以看出:DATA_PARTS与TOOLS会沿着parts向下传播到每一个UIMessagePart,因此一旦你在应用层定义了自定义数据类型和工具集合,parts数组中的data-*与tool-*部件就能获得精确到字段级别的类型推导,而不是退化为any。
实战:构造你自己的 UIMessage 类型
文档给出如下示例,用于创建带自定义元数据、数据部件与工具集合的UIMessage:
import { InferUITools, ToolSet, UIMessage, tool } from "ai"; import z from "zod"; const metadataSchema = z.object({ someMetadata: z.string().datetime(), }); type MyMetadata = z.infer<typeof metadataSchema>; const dataPartSchema = z.object({ someDataPart: z.object({}), anotherDataPart: z.object({}), }); type MyDataPart = z.infer<typeof dataPartSchema>; const tools = { someTool: tool({}), } satisfies ToolSet; type MyTools = InferUITools<typeof tools>; export type MyUIMessage = UIMessage<MyMetadata, MyDataPart, MyTools>;要点拆解:
- 元数据与数据部件用
zodschema 定义后,通过z.infer提取为 TypeScript 类型,保证运行时校验与静态类型同源; tool({})定义工具,satisfies ToolSet让对象字面量保持精确类型而非被拓宽;InferUITools<typeof tools>从ToolSet推导出 UI 侧工具类型(含input/output结构);- 最终
UIMessage<MyMetadata, MyDataPart, MyTools>即为整个应用共享的消息类型。
Wave Terminal 的真实泛型实例
Wave Terminal 在 aitypes.ts 中定义了自己的数据类型并实例化:
import { ChatRequestOptions, FileUIPart, UIMessage, UIMessagePart } from "ai"; type WaveUIDataTypes = { userfile: { filename: string; size: number; mimetype: string; previewurl?: string; }; tooluse: { toolcallid: string; toolname: string; tooldesc: string; status: "pending" | "error" | "completed"; runts?: number; errormessage?: string; approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout"; blockid?: string; writebackupfilename?: string; inputfilename?: string; }; toolprogress: { toolcallid: string; toolname: string; statuslines: string[]; }; }; export type WaveUIMessage = UIMessage<unknown, WaveUIDataTypes, any>; export type WaveUIMessagePart = UIMessagePart<WaveUIDataTypes, any>;这里WaveUIDataTypes就是第二个泛型参数DATA_PARTS的实例——它声明了三种data-*部件:data-userfile(用户上传文件)、data-tooluse(工具调用状态与审批信息)、data-toolprogress(工具执行进度)。这些类型与 Go 后端的UIMessageDataUserFile、UIMessageDataToolUse、UIMessageDataToolProgress一一对应,代码注释明确标注"when updating this struct, also modify frontend/app/aipanel/aitypes.ts",实现了跨语言的双向契约同步。
三、UIMessage 接口核心字段
UIMessage接口本身十分精简,四个字段各司其职:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 消息的唯一标识,用于 React key、重试、编辑与分支定位 |
role | "system" \| "user" \| "assistant" | 消息角色,决定渲染方向与样式 |
metadata? | METADATA | 消息级自定义元数据,可选 |
parts | Array<UIMessagePart<DATA_PARTS, TOOLS>> | 消息的全部结构化内容,是 UI 渲染的主要依据 |
在 aipanelmessages.tsx 中可以看到role与id的典型用法:messages.map((message) => <AIMessage key={message.id} ... />),并用message.role === "assistant"判断是否为最后一条流式消息;system角色则用于注入系统提示(参见 usechat-backend-design.md 中initialMessages注入系统上下文的示例)。
四、UIMessagePart:八种部件类型全解析
parts数组是UIMessage的灵魂。AI SDK 用**可判别联合类型(discriminated union)**将不同内容形态统一在UIMessagePart之下,每种部件通过type字段区分。下面逐一展开文档给出的全部类型。
1. TextUIPart — 文本部件
type TextUIPart = { type: "text"; text: string; state?: "streaming" | "done"; };state字段标注文本块是否仍在流式传输:流式过程中为"streaming",完成(收到text-end)后为"done"。前端可据此决定是否启用"不完整 Markdown 增量解析"。
2. ReasoningUIPart — 推理部件
type ReasoningUIPart = { type: "reasoning"; text: string; state?: "streaming" | "done"; providerMetadata?: Record<string, any>; };推理部件承载思考模型(thinking model)的推理过程文本,providerMetadata可透传提供商额外信息(如 token 用量、推理模式等)。Wave Terminal 在 aimessage.tsx 的getThinkingMessage中判断lastPart?.type === "reasoning",在流式期间展示 "AI is thinking..." 并滚动显示推理文本,一旦text部件出现即切换为正文展示——这正是state与部件顺序共同驱动 UI 状态机的实例。
3. ToolUIPart — 工具调用部件
工具部件是状态机最复杂的部件,其type为模板字面量类型`tool-${NAME}`(即工具名someTool对应tool-someTool),并且按state划分四个互斥子状态:
type ToolUIPart<TOOLS extends UITools = UITools> = ValueOf<{ [NAME in keyof TOOLS & string]: { type: `tool-${NAME}`; toolCallId: string; } & ( | { state: "input-streaming"; input: DeepPartial<TOOLS[NAME]["input"]> | undefined; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: "input-available"; input: TOOLS[NAME]["input"]; providerExecuted?: boolean; output?: never; errorText?: never; } | { state: "output-available"; input: TOOLS[NAME]["input"]; output: TOOLS[NAME]["output"]; errorText?: never; providerExecuted?: boolean; } | { state: "output-error"; input: TOOLS[NAME]["input"]; output?: never; errorText: string; providerExecuted?: boolean; } ); }>;四个状态对应工具调用的生命周期:
input-streaming:模型正在生成工具入参,input为DeepPartial(允许部分字段);input-available:入参生成完毕,可执行工具;output-available:工具执行完成,携带output;output-error:工具执行失败,携带errorText。
providerExecuted?: boolean用于标识该工具是否已由模型提供商侧执行。Wave Terminal 的 aimessage.tsx 中isDisplayPart只把state === "input-available"的tool-*部件视为可显示内容,流式生成入参期间(input-streaming)不打断正文展示。
4. SourceUrlUIPart — 来源 URL 部件
type SourceUrlUIPart = { type: "source-url"; sourceId: string; url: string; title?: string; providerMetadata?: Record<string, any>; };用于标注回答引用的外部 URL(联网搜索或引用来源),sourceId用于去重与关联。
5. SourceDocumentUIPart — 来源文档部件
type SourceDocumentUIPart = { type: "source-document"; sourceId: string; mediaType: string; title: string; filename?: string; providerMetadata?: Record<string, any>; };用于引用文档类来源,mediaType标识文档 MIME 类型。
6. FileUIPart — 文件部件
type FileUIPart = { type: "file"; mediaType: string; // IANA media type filename?: string; url: string; // 托管文件 URL 或 Data URL };url既可以是远端托管地址,也可以是data:Data URL(前端本地读取文件时常用)。Wave Terminal 前端在 ai-utils.ts 中通过createDataUrl(file)将File转为 Data URL,并定义了文件类型白名单(图片、PDF、文本/代码文件)与体积上限(文本 200KB、PDF 5MB、图片 10MB),超出时给出错误提示。
7. DataUIPart — 自定义数据部件
type DataUIPart<DATA_TYPES extends UIDataTypes> = ValueOf<{ [NAME in keyof DATA_TYPES & string]: { type: `data-${NAME}`; id?: string; data: DATA_TYPES[NAME]; }; }>;data-*是 AI SDK 提供的扩展点:任何自定义结构化数据都可以通过data-${NAME}成为消息的一部分,前端按名处理。本文第二节中WaveUIDataTypes的userfile、tooluse、toolprogress即为三个实例。以data-tooluse为例,Go 侧 uctypes.go 定义了其数据结构:
type UIMessageDataToolUse struct { ToolCallId string `json:"toolcallid"` ToolName string `json:"toolname"` ToolDesc string `json:"tooldesc"` Status string `json:"status"` // pending | error | completed RunTs int64 `json:"runts,omitempty"` ErrorMessage string `json:"errormessage,omitempty"` Approval string `json:"approval,omitempty"` // needs-approval | user-approved | ... BlockId string `json:"blockid,omitempty"` WriteBackupFileName string `json:"writebackupfilename,omitempty"` InputFileName string `json:"inputfilename,omitempty"` }IsApproved()方法定义:Approval == "" || user-approved || auto-approved视为已批准。前端 aitooluse.tsx 则消费这些字段:渲染✓/✗/•状态图标、展示 "Approve / Deny" 审批按钮、为写文件工具提供 "Revert File" 备份还原与 "Show Diff" 差异查看入口,甚至通过blockid在终端中高亮相关 Block——充分体现data-*部件承载"客户端专属功能状态"的设计意图。
8. StepStartUIPart — 步骤边界部件
type StepStartUIPart = { type: "step-start"; };标记一个 step(后端一次 LLM API 调用)的开始,与流式协议中的start-step/finish-step事件配套,用于区分多步拼接的 assistant 消息。
五、流式协议如何落地为 UIMessage
UIMessage是静态形态,而其内容来自流式协议。配套文档 aisdk-streaming.md 描述了 AI SDK 基于 SSE 的数据流协议(自定义后端需设置x-vercel-ai-ui-message-stream: v1头)。两类文档的映射关系如下:
流式事件(SSEdata:) | 落地的 UIMessage 部件 |
|---|---|
text-start/text-delta/text-end | TextUIPart(state先streaming后done) |
reasoning-start/reasoning-delta/reasoning-end | ReasoningUIPart |
source-url | SourceUrlUIPart |
source-document | SourceDocumentUIPart |
file | FileUIPart |
data-* | DataUIPart |
tool-input-start/tool-input-delta/tool-input-available/tool-output-available | ToolUIPart(状态机流转) |
start-step/finish-step | StepStartUIPart与 step 边界 |
error | 以errorText形式附加到消息 |
finish/[DONE] | 消息完成标记 |
Wave Terminal 的 Go 后端在 uctypes.go 中定义了与之一一对应的UseChatStreamPart结构(text-start、text-delta、tool-input-available、tool-output-available、finish-step、finish等),注释中完整列举了协议支持的 type 常量。流式部件在 aimessage.tsx 中被消费:text部件流式期间交给WaveStreamdown做增量 Markdown 解析(parseIncompleteMarkdown={isStreaming});消息渲染完成且非流式时显示反馈按钮(AIFeedbackButtons)。这一"流式协议 → UIMessage 部件 → React 渲染"的链路正是 AI SDK 聊天架构的标准范式。
六、在应用中完整落地:从类型定义到渲染管线
综合以上内容,在 Wave Terminal(或其他 AI 应用)中落地UIMessage类型体系的完整链路为:
- 定义类型:用
zod定义METADATA与DATA_PARTS,用tool()定义TOOLS,组合成应用级MyUIMessage(见第二节示例); - 传输契约:前端
UIMessage的 JSON 形态与后端结构体对齐(如uctypes.go的UIMessage/UIMessagePart),保证消息持久化(UIChat.Messages)与跨请求传输一致; - 流式接收:
useChat通过 SSE 协议(aisdk-streaming.md)增量构建消息的parts,由DATA_PARTS与TOOLS泛型保证每个部件的类型安全; - 按类型渲染:渲染器对
parts做判别式分发——text走 Markdown 渲染、reasoning走思考展示、tool-*与data-tooluse/data-toolprogress走工具卡片(含审批、进度、diff 等交互)、data-userfile走文件缩略图列表(见 aimessage.tsx 的AIMessagePart、UserMessageFiles、AIToolUseGroup); - 客户端功能:基于
id实现重试/编辑,基于state实现流式状态展示,基于data-*的approval字段实现工具审批交互。
usechat-backend-design.md 还展示了更宏观的视角:Wave Terminal 的目标架构是"前端useChat→ HTTP/SSE → Go 后端 → AI 提供商",前端由单个useChathook 管理全部消息状态,后端解析配置后流式返回标准 AI SDK 格式——而UIMessage正是这条链路上前后端共享的"消息唯一事实来源",也是上述全部能力得以类型安全地串联起来的基础。
总结
UIMessage通过三个泛型参数(METADATA、DATA_PARTS、TOOLS)将元数据、自定义数据部件与工具类型注入到统一的消息结构中;八种UIMessagePart(文本、推理、工具、来源 URL、来源文档、文件、自定义数据、步骤边界)以可判别联合覆盖了现代 AI 聊天的全部内容形态。Wave Terminal 的实践表明:前端UIMessage类型(aitypes.ts)与 Go 后端结构体(uctypes.go)双端对齐、渲染组件(aimessage.tsx、aitooluse.tsx)按部件判别分发、流式协议(aisdk-streaming.md)增量驱动状态机,三者共同构成了可复用、可扩展、类型安全的 AI 聊天消息体系。
【免费下载链接】wavetermAn open-source, AI-integrated, cross-platform terminal for seamless workflows项目地址: https://gitcode.com/GitHub_Trending/wa/waveterm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考