文档
构建流媒体助手客户端
打开回合,呈现其真实事件,回答待处理的请求,并在连接断开后恢复对话。
从英文自动翻译。 查看英文原文
连接到正在运行的边车
流媒体助理客户端连接到与桌面相同的本地 sidecar。从您正在集成的连接获取其分配的环回 URL 和当前应用程序承载令牌。从昨天的启动中保存的端口不是持久地址:sidecar 每次启动时都会请求一个临时端口。帐户会话令牌、同伴配对令牌和作用域操作桥令牌具有不同的用途,并且不能替换此聊天路径上的应用程序承载。
在提交回合之前,请阅读 GET /api/ai/status, /api/ai/providers,并且可选地 /api/ai/models?configuredOnly=true。配置好的凭证意味着存在密钥;它不能证明提供者将接受请求或端点支持所选模型。默认主机是嵌入式 nikcli。直接模式可以传输模型答案,但不提供嵌入式主机的持久工具循环或专业调度程序。
本指南使用 CADCAMFUN_URL 和 CADCAMFUN_TOKEN 作为客户端 shell 变量。通过现有的连接处理来提供它们。请参阅 人工智能API参考 对于确切的模式和 边车 用于启动行为。这些示例不会代表您安装凭据或选择模型。
选择项目、绘图和会话标识
客户可以提供一个 sessionId 在第一个回合中并在以后的回合中重复使用它。该服务将该客户端标识符映射到实际的主机会话,并在每次提示之前检查绑定。将事件/会话标识保留为对话状态,而不是每次 UI 重新连接时创建不同的标识符。
一个真实的 projectId 允许轮流访问该项目允许的资源。可选的 drawingId 必须属于该项目;它将转弯的工具绑定到该绘图。没有项目就无法命名绘图。已由另一个帐户或项目拥有的会话将被拒绝。这些是模型执行项目工具之前的准入检查,而不是系统提示中的提示。
对于一般性解释性对话,请省略项目和绘图。应用程序使用其默认上下文。对于几何工作,通过聊天、状态恢复、队列、中止和控制请求携带实际选定的项目/图形 ID。帐户切换不应默默地采用先前帐户的活动会话。
提交流转
POST /api/ai/chat/stream 返回 SSE 主体。即使传输模式使多个字段可选,以便处理程序可以返回可读的验证消息,它也需要非空消息。以下一般问题禁用存储的上下文注入,使其适合独立于项目几何形状检查传输。
curl --no-buffer --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" \
-H "Content-Type: application/json" \
--data '{"message":"Explain the difference between G0 and G1.","sessionId":"inspection-demo","includeContext":false,"mode":"gcode"}' \
"$CADCAMFUN_URL/api/ai/chat/stream" 添加 projectId 和 drawingId 处理特定绘图。 mode 接受常规、CAD、CAM、Gcode 或刀具路径。一个 model 选择器可以包含提供商、模型、层和变体;等级是快速、平衡或最佳。从运行目录中发现提供商/模型 ID,而不是硬编码关于哪个供应商可用的假设。
permissionMode 接受批准、询问或自动。破坏性工具组在每种模式下都保留了人类的认可。这 agent 字段选择配置的助理角色;读 GET /api/ai/agents 而不是猜测角色 ID。
读取完整的SSE数据帧
该路由使用 Effect 的 SSE 数据模式:每个 SSE 数据负载都是一个 JSON 事件记录,其中包含 type 鉴别器。网络块不是事件边界。 JSON、行结尾和 UTF-8 字符可以在读取之间拆分。累积解码文本并仅解析完整的帧,在存在时连接多个数据行。
async function* events(response: Response): AsyncGenerator<Record<string, unknown>> {
if (!response.ok) throw new Error(await response.text())
if (!response.body) throw new Error("The response has no stream")
const reader = response.body.getReader()
const decoder = new TextDecoder()
let pending = ""
try {
while (true) {
const next = await reader.read()
pending += decoder.decode(next.value, { stream: !next.done })
let boundary: RegExpExecArray | null
while ((boundary = /\r?\n\r?\n/.exec(pending)) !== null) {
const frame = pending.slice(0, boundary.index)
pending = pending.slice(boundary.index + boundary[0].length)
const data = frame.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, "")).join("\n")
if (!data) continue
const event: unknown = JSON.parse(data)
if (!event || typeof event !== "object" || !("type" in event)) {
throw new Error("Invalid assistant event")
}
yield event as Record<string, unknown>
}
if (next.done) break
}
} finally {
await reader.cancel().catch(() => {})
reader.releaseLock()
}
} 该读者演示了框架;生产客户端还应该根据共享合约验证每个事件或使用类型化生成的流客户端。它故意忽略 SSE 注释/控制字段,因为此端点的应用程序状态位于 JSON 数据记录中。释放客户端流是传输清理,而不是中止服务器端工作的显式请求。
渲染进度而不重复内容
使用上面所示的相同承载和 JSON 正文开始获取,然后迭代读取器。附加 delta.text 可见的答案文本和路线 reasoning.text 以自己的观点。一个 done 事件携带最终的响应文本;用最终值替换或协调累积的答案,而不是再次附加整个答案。
| 活动 | 状态维持 |
|---|---|
turn, session_status, step | 忙/重试状态和当前步骤;在同一回合中,完成一个步骤后可以接着执行另一个步骤。 |
tool_call, tool_progress, tool_result | callId 记录,包含名称、输入、参数进度、输出和成功;代理人在场时识别专家。 |
tool_diagnostic | 参数/执行/部分/验证结果、严重性、恢复文本和可选的已应用突变。 |
todo, goal | 替换当前计划/目标快照而不是附加重复副本。 |
retry, compaction, compacted | 解释继续工作,同时保留对话的当前文本和会话标识。 |
heartbeat | 无可见内容;当源流保持活动状态时,服务器大约每五秒发送一次。 |
通过以下方式关联工具调用和结果 callId,而不是它们的数组位置。失败的工具可以在助手继续运行时返回有用的诊断信息。成功编辑工具后出现的提供程序错误不会回滚几何更改。
回答权限和问题
A permission_request 包括sessionId、requestId、权限和模式。向此人提出请求并将所选决定发送至 POST /api/ai/permission/reply。等效的会话控制操作是 reply_permission。服务器验证该请求在会话或已接纳的子会话中是否仍处于待处理状态。
{
"sessionId": "inspection-demo",
"requestId": "request-id-from-the-event",
"decision": "once"
} 决定是一次、永远、或者拒绝。它是服务器权限决策,与确认等操作参数不同。待定 question_request 包含一个有序的问题数组。回复通过 /api/ai/sessions/control 用行动 answer_question、请求 ID 以及每个问题的一个答案数组;拒绝它 reject_question.
{
"sessionId": "inspection-demo",
"action": "answer_question",
"requestId": "question-id-from-the-event",
"answers": [["The selected option label"]]
} 每个答案都必须非空。单选题拒绝多个答案,custom=false 仅允许列出的标签。保留准确的标签和问题顺序。如果服务器表示请求不再待处理,请刷新控件,而不是重复发布旧回复。
排队、引导和明确停止工作
使用 POST /api/ai/chat/queue 对于轮流进行时提交的消息。它需要 sessionId、消息、可选的projectId 和交付。 queue 是默认值并等待下一个安全步骤; steer 请楼主及时采纳。一个 user_message 事件表明消息已进入对话。
使用 POST /api/ai/chat/abort 当该人按停止时的对话身份。这会停止转弯并加入其相关的工作人员。不要仅通过取消获取来实现停止:丢失 HTTP 流不是同一操作。然后,会话可以接受另一回合,而无需创建新的项目标识。
POST /api/ai/sessions/remove 取消并加入其子会话后删除主机会话。它返回一个已删除的标志。删除对话并不是回滚其工具所做的项目编辑;而是回滚。需要时通过项目/历史 API 查看或恢复这些内容。
丢失流后恢复
当读者在没有最终事件的情况下结束时,将完成视为未知。称呼 POST /api/ai/sessions/state 具有相同的 sessionId 和 projectId。响应包含忙碌和文本,以及可选的恢复工具/待办事项/诊断事件、使用情况和任务。从此快照重建 UI;重放恢复的工具事件不会再次执行它们。
然后请求 /api/ai/sessions/control 具有操作状态来检查待处理的权限/问题、工作人员任务、目标和上下文策略。等待未回答问题的忙碌轮流需要答复,而不是原始提示的第二份副本。即使原始 TCP 连接消失,报告空闲且具有累积文本的会话也可以显示为恢复的输出。
区分网络故障、类型化提供程序/配置错误事件、显式中止和会话删除。仅在检查第一轮实际执行的操作后重试模型请求。特别是在部分突变诊断或中断的编辑工作之后读取当前绘图。
在服务器内部,断开的模型连接最多恢复五次,延迟时间为 2 秒,延迟时间加倍,最多可达 30 秒。重新连接到流的客户端仍应读取会话状态,因为这些恢复可以完成客户端未看到流的工作。
上下文、附件和压缩
includeContext: false 禁用获取存储的上下文;仍然可以为该回合提供显式上下文文本。正常装配顺序要求、当前观察结果、UI 快照、内存和支持参考,对它们进行重复数据删除,并应用 24,000 字符的总计预算。报告遗漏/截断的块;该预算不是提供商的代币窗口。
附件包括名称、可选的 MIME、数据和可选的 text/base64 编码。每个请求最多有五个。每个图像限制为 5 MiB,PDF 限制为 10 MiB,文本限制为 2、000、000 个字符;最多 6,000 个字符的短文本也会内嵌显示。为工具保留的文本在内存中是项目范围的,因此重新连接与重新启动服务不同。不支持的文件会被拒绝并给出原因。
控制操作上下文请求详细的令牌细分;普通状态轮询避免了标记器的工作。行动契约要求主持人总结以前的谈话材料。内存整合是一项单独的主机操作。通过自己的 API 保留源绘图和制造请求,而不是依赖压缩的对话来重现精确的几何形状。
将传输错误与域结果分开
HTTP 401 表示请求未通过承载授权。流式传输之前的 400 通常意味着空消息、无法访问的会话标识、无效的项目/绘图组合或格式错误的正文。一旦建立了流,处理程序就可以发出带有配置、提供者、中止或内部分类的错误事件;即使响应状态为 200,也会处理该事件。
ok=false 的工具结果是工具结果,不一定是失败的流。显示其输出和诊断结果,以便助手或人员可以恢复。工作队列也可能已满:调度程序允许四个活动任务和十六个等待任务。中断的阅读器可以在有限的尝试内恢复;编辑工作被视为不确定而不是自动重播。
阅读可执行合约 packages/api/src/groups/ai.ts 和 ai-events.ts;的入场和接线行为 apps/sidecar/src/http/ai.ts;会话映射在 packages/ai/src/service.ts;问题验证 nikcli/session-controls.ts;心跳计时 heartbeat.ts。这些来源定义了客户必须呈现的结果,独立于助理的散文。
事件参考
每个上证所 data: 框架是一个 JSON 对象,带有 type 和一个 sessionId。仅解析完整的帧;帧可能会跨网络块到达。
| 类型 | 关键领域 | 如何处理 |
|---|---|---|
turn | phase: start, recovering, continuing, finish;选修的 reason | 显示进度状态。 recovering 表示正在恢复与模型的连接。 |
session_status | status: busy, idle, retry | 启用或禁用发送按钮和队列。 |
user_message | 存储的用户消息 | 协调乐观的用户界面。 |
delta | text | 附加到可见答案。 |
reasoning | 推理文本 | 选修的;显示在可折叠区域中。 |
step, todo, goal | 计划进度 | 呈现任务列表或计划。 |
tool_call | callId、工具、参数、可选 agent | 显示哪个工具运行以及为哪个专家运行。 |
tool_progress | callId, 进步 | 更新长时间运行的工具。 |
tool_result | callId, 结果 | 显示结果;匹配它 callId. |
tool_diagnostic | 详细信息包括 mutationApplied | 确定模型是否发生变化。在重试编辑之前请阅读它。 |
permission_request / permission_resolved / permission_blocked | requestId;回复 once, always, reject | 询问用户,然后 POST /api/ai/permission/reply. |
question_request / question_resolved | 问题数据 | 提出问题并发送答案。 |
context_snapshot, compaction, compacted | 上下文状态 | 通知用户旧的历史记录已被汇总。 |
retry | attempt, message | 显示临时通知;不要重新发送提示。 |
done | text, 选修的 usage, 选修的 recovered | 本回合的最终文本。用它替换累积的增量。 |
error | code: provider, config, aborted, internal; message | 在 UI 中显示消息结束回合; config 通常意味着缺少凭据。 |
heartbeat | 没有任何 | 大约每 5 秒保持一次活动;忽略。 |
一个完整的流媒体客户端
const base = process.env.CADCAMFUN_URL! // e.g. http://127.0.0.1:53211
const token = process.env.CADCAMFUN_TOKEN!
const response = await fetch(`${base}/api/ai/chat/stream`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
body: JSON.stringify({ projectId: "p1", message: "Summarise this project", mode: "general" }),
})
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`)
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = "", answer = ""
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += value
let end
while ((end = buffer.indexOf("\n\n")) >= 0) { // one SSE frame
const frame = buffer.slice(0, end); buffer = buffer.slice(end + 2)
const data = frame.split("\n").filter((l) => l.startsWith("data:"))
.map((l) => l.slice(5).trimStart()).join("\n")
if (!data) continue
const event = JSON.parse(data)
switch (event.type) {
case "delta": answer += event.text; break
case "permission_request": /* ask, then POST /api/ai/permission/reply */ break
case "done": answer = event.text; console.log(answer); break
case "error": throw new Error(`${event.code}: ${event.message}`)
}
}
} 如果连接断开,请勿再次发送消息。阅读会话 POST /api/ai/sessions/state,因为服务器可能已恢复模型连接并在没有您的情况下完成转弯。使用 /api/ai/chat/abort 故意停止转弯,并且 /api/ai/chat/queue 在会话繁忙时添加消息。
没有提供者的流是什么样子
在未配置 AI 提供程序的情况下,流仍会打开并回答键入的错误,然后是心跳。从新鲜的边车捕获:
data: {"type":"error","sessionId":"","code":"config","message":"No AI provider is configured. Add an API key in the AI settings."}
data: {"type":"heartbeat"} - HTTP 状态仍然是 200 且内容类型
text/event-stream:配置问题是领域结果,作为事件交付。 sessionId为空,因为没有创建会话。heartbeat帧保持连接处于活动状态;在渲染时忽略它们并且不将它们计为进度。- 每帧以空行结束。通过分割空行并加入
data:帧的行数,就像 SDK 迭代器所做的那样。
无需模型即可测试您的客户
将客户端指向没有凭据的 sidecar:它可以免费执行完整的 SSE 路径和错误分支。