后端与接口
需要为 assistant-ui 编写自定义流式端点…
当你不使用或不完全依赖 Vercel AI SDK,需要在 assistant-ui 中构建自定义流式响应、手动解码流、选择 Data Stream/Assistant Transport 编码,或接入可恢复流时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
需要为 assistant-ui 编写自特定
解决思路
- 始终查阅 `https://www.assistant-ui.com/llms.txt` 获取最新 API。
- 运行 `npm install assistant-stream` 安装依赖。
- 新代码从 `@assistant-ui/ai-sdk` 导入 AI SDK 集成;`@assistant-ui/react-ai-sdk` 仍为旧安装重新导出相同 API。
- 需要协议细节时查阅 `./references/data-stream.md`(Data Stream 协议、`useDataStreamRuntime` 及其 wire format)、`./references/assistant-transport.md`(Assistant Transport SSE 格式和 `useAssistantTransportRuntime` state-snapshot runtime)、`./references/encoders.md`(encoder/decoder 目录、`PlainTextEncoder`、`UIMessageStreamDecoder`、accumulators 和调试)、`./references/resumable.md`(`assistant-stream/resumable`:context、stores 和 client wiring)。
- 实现自定义流式响应时,用 `createAssistantStreamResponse` 接收带 `AssistantStreamController` 的回调,并返回编码为 Data Stream 的 `Response`;其他编码器见 `data-stream.md`。
- 在 `POST` 路由中写成 `export async function POST(req: Request) { return createAssistantStreamResponse(async (controller) => { ... }); }`。
- 追加文本用 `controller.appendText("Hello ")` 和 `controller.appendText("world!")`;首次调用打开 text part,后续追加。
- 追加推理用 `controller.appendReasoning("Checking the forecast first.", { unstable_summary: "Looking up the weather" })`;传入 `options` 总会打开新 part,因此 summary 会落在自己的 part 上。
- 追加来源用 `controller.appendSource({ type: "source", sourceType: "url", id: "s1", url: "https://example.com/forecast", title: "Forecast" })`。
- 添加工具调用用 `const tool = controller.addToolCallPart({ toolName: "get_weather" }); tool.argsText.append('{"city":"NYC"}'); tool.argsText.close(); tool.setResponse({ result: { temperature: 22 } });`。
- 调用 `controller.close()` 结束流;它会关闭任何仍打开的 part,然后结束流;回调内未捕获的 throw 会自动转换为 `error` chunk。
- 所有 server-side stream 都通过 `AssistantStreamController` 写入;`createAssistantStream`、`createAssistantStreamController` 和 `createAssistantStreamResponse` 都会给你一个 controller。
- `appendText(textDelta: string) => void`:首次调用打开 text part,后续调用追加到该 part。
- `appendReasoning(reasoningDelta: string, options?: { unstable_summary?: string }) => void`:传入 `options` 总是打开新 part,使 summary 单独成 part。
- `appendSource(part: SourcePart) => void`:`SourcePart` 为 `{ type: "source", sourceType: "url", id, url, title?, parentId? }`。
- `appendFile(part: FilePart) => void`:`FilePart` 为 `{ type: "file", data, mimeType, parentId? }`。
- `appendData(part: DataPart) => void`:`DataPart` 为 `{ type: "data", name, data, parentId? }`,是命名应用自定义 part。
- `addTextPart() => TextStreamController`:返回显式 `{ append(text), close() }` writer,用于与其他 part 交错。
- `addReasoningPart(options?) => TextStreamController`:与 `addTextPart` 相同 writer 形状。
- `addToolCallPart(toolName: string) => ToolCallStreamController`:生成 `toolCallId`;需要稳定 id 时用对象重载。
- `addToolCallPart(init: ToolCallPartInit) => ToolCallStreamController`:`{ toolCallId?, toolName, argsText?, args?, response? }`。
- `enqueue(chunk: AssistantStreamChunk) => void`:原始逃生舱;优先使用上面的 helper。
- `merge(stream: AssistantStream) => void`:把另一个 `AssistantStream` 的 parts 拼接到当前流。
- `withParentId(parentId: string) => AssistantStreamController`:返回的 controller 写入时附加 `parentId`,用于嵌套或关联 part。
- `close() => void`:关闭打开的 part,然后关闭流。
- `addToolCallPart` 返回 `ToolCallStreamController`:`{ argsText: TextStreamController, setResponse(response), close() }`;`setResponse` 接收 `{ result, artifact?, isError?, modelContent?, messages? }`(`ToolResponse` 返回的形状),自动关闭 part,并忽略第二次调用。
- 实现解码时,所有 decoder 无论 wire format 都产出归一化的 `AssistantStreamChunk` 联合类型:`{ path: number[] } & { type, ... }`。
- 按 `type` 处理事件:`part-start` 带 `part: PartInit`;`part-finish` 无额外字段;`tool-call-args-text-finish` 无额外字段;`text-delta` 带 `textDelta: string`;`annotations` 带 `annotations: ReadonlyJSONValue[]`;`data` 带 `data: ReadonlyJSONValue[]`;`step-start` 带 `messageId: string`;`step-finish` 带 `finishReason, usage: { inputTokens, outputTokens }, isContinued: boolean`;`message-finish` 带 `finishReason, usage`;`result` 带 `result, isError: boolean, artifact?, modelContent?, messages?`;`error` 带 `error: string, code?, severity?: "critical" | "warning" | "info"`;`update-state` 带 `operations: AssistantTransportStateOperation[]`(见 `assistant-transport.md`)。
- 构造 `PartInit`(`part-start` 的 `part` 字段)时按六种 part 类型之一处理,每个变体可带可选 `parentId`:`text` 无额外字段;`reasoning` 带 `unstable_summary?: string`;`tool-call` 带 `toolCallId: string, toolName: string`;`source` 带 `sourceType: "url", id, url, title?`;`file` 带 `data: string, mimeType: string`;`data` 带 `name: string, data: ReadonlyJSONValue`。
- 避免 `appendSource`、`appendFile`、`appendData` 静默丢弃 part:传完整 part 对象并包含其 `type` 字段(`"source"`、`"file"` 或 `"data"`);方法名不会替你补。
- 避免工具调用在 UI 中永不结束:`addToolCallPart` 需要 `toolName`;除非传 id,否则自动生成;关闭 `argsText`(或调用 `setResponse`,它会自动关闭)否则 part 永不完成;用 `"use generative"` toolkit 注册渲染,不要用已弃用的 `makeAssistantToolUI`;见 tools。
- 避免两个独立 reasoning part 在客户端合并:在 Data Stream wire 上,只有设置 `unstable_summary` 时才发送 reasoning part-start 帧;普通 `appendReasoning(text)` 只作为 text delta 传输,decoder 没有其他信息判断新 part 开始;连续打开两个无 summary 的 reasoning part(例如围绕工具调用)会在客户端重建成一个连续 reasoning part;给每个 part 一个 `unstable_summary`(即使感觉为空)或把工具调用路由到单独 message step 来保持区分。
- 流未更新 UI 时检查 Content-Type 与实际所用 encoder:`DataStreamEncoder`(`createAssistantStreamResponse` 默认)发送 `text/plain; charset=utf-8` 和 `x-vercel-ai-data-stream: v1`,不是 `text/event-stream`;`AssistantTransportEncoder` 和 AI SDK 的 UI message stream 确实发送 `text/event-stream`。
- 避免 decoder 抛 `"Stream ended abruptly without receiving [DONE] marker"`:`AssistantTransportDecoder` 和 `UIMessageStreamDecoder` 需要终止 `[DONE]` sentinel;代理、CDN 或中间件缓冲/截断 body 会破坏它;`DataStreamDecoder` 没有这个 marker。
- 注意 `createAssistantStreamResponse` 总是编码为 Data Stream:它硬编码 `DataStreamEncoder`;不同 wire format 要手动编码:`AssistantStream.toResponse(createAssistantStream(callback), new AssistantTransportEncoder())`,或使用 `createAssistantStreamController` 并自行编码返回的 stream。
- 需要 runtime hooks 用 runtime;无自定义协议的 AI SDK route handlers 用 setup;云端持久化用 cloud。
适用边界 · 注意事项
- 后端已经使用 Vercel AI SDK 时,不要直接使用 assistant-stream;优先用 `streamText` 加 `toUIMessageStream` / `createUIMessageStreamResponse`(或 `result.toUIMessageStreamResponse()`),只有需要自行解码响应或添加可恢复流时才用 assistant-stream。
- 需要 React runtime hooks 时不要在本技能处理,转到 runtime;无自定义协议的 AI SDK route handler 转到 setup;云端持久化转到 cloud。
- 非 assistant-ui / assistant-stream 技术栈,或不需要 wire 级控制、手动解码、可恢复流时不适用。