Chat 订阅与提交
说明 Chat 提交消息、订阅实时输出的事件参数,以及 AskUserQuestion 问题卡片的渲染与回答方式
Chat 订阅与提交
本文是常用 API中 Chat 能力的展开说明,重点讲三件事:
chat/submit的完整请求参数、返回字段和路由结果。chat/conversations/{conversationId}/stream的订阅参数、SSE 帧格式和事件结构。- AI 通过
AskUserQuestion提问时,如何把ask_question事件渲染成问题卡片,并把用户答案提交回去。
如果你只需要“发一条消息并接收文字输出”,看完「一次完整对话的调用顺序」即可;需要接入问题卡片时,再看「AskUserQuestion 问题卡片」。
小提示:Chat 能力还在持续打磨,文中的参数和事件结构后续可能还会有些调整。新增字段或事件类型一般不会影响已有对接,按
eventIndex消费事件、忽略不认识的内容就能平滑跟进;如果将来确实需要做不兼容的调整,也会提前通过变更说明告知。
一次完整对话的调用顺序
1. POST /chat/submit
提交用户消息,拿到 conversationId 和 activeResponseId
2. GET /chat/conversations/{conversationId}/stream?expectedResponseId={activeResponseId}
订阅本轮输出
3. 处理 SSE 事件
content / tool_call / tool_result / ask_question / stream_end ...
4. 如果收到 ask_question
渲染问题卡片,等待用户选择或输入
5. POST /chat/submit(再次提交)
message 填写答案摘要,pendingResponse 携带 requestId、toolUseId 和 answers
6. 使用新的 activeResponseId 重新订阅 stream
继续接收 AI 的后续输出一轮对话中,responseId 标识一轮 AI 回复。每次提交或回答问题后,按返回的 observationDisposition 决定是否使用最新 activeResponseId 重新订阅,避免把上一轮的事件当成新一轮。
提交消息
POST /api/v1/workspaces/{workspaceId}/chat/submit
Authorization: Bearer <token>
Content-Type: application/json请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
message | string | 是 | 用户消息正文。回答问题时填写答案摘要,作为用户可见消息保存 |
clientMessageId | string | 是 | 客户端生成的消息 ID,用于幂等和界面去重。同一条逻辑消息重试时保持不变;内容变化时必须换新 ID |
conversationId | string | 否 | 会话 ID。不传则创建新会话;续聊或回答问题时必须传入 |
pendingResponse | object | 否 | 回答待处理的 AskUserQuestion 或权限确认,详见「AskUserQuestion 问题卡片」 |
title | string | 否 | 新会话初始标题,trim 后 1–200 个 Unicode 码点;不能与 conversationId 同时传入 |
sdkType | string | 否 | 新会话运行时类型,如 claude-code、codex |
model | string | 否 | 指定本轮使用的模型;创建新会话和续聊时生效,插话(并入运行中的执行)不会改变当前执行的模型;不传则沿用会话当前模型 |
effortLevel | string | 否 | 推理强度:low、medium、high、xhigh、max、auto、disabled |
permissionMode | string | 否 | 权限模式,如 default、acceptEdits、plan、bypassPermissions |
systemPrompt | string | 否 | 本轮使用的系统提示词 |
botId | string | 否 | 使用指定 Bot 发起对话 |
env | object | 否 | 新会话自定义环境变量;key 必须以 JAVIS_CUSTOM_ 开头,且不能与 conversationId 同时传入。最多 10 个,key 最长 128、value 最长 4096 个字符 |
tags | string[] | 否 | 新会话标签,最多 5 个,每个最多 20 字符;仅创建新会话时生效 |
contextCarryover | object | 否 | 新会话上下文来源,mode 为 fork 或 recover-history,并传 sourceConversationId |
errorAction | object | 否 | 错误卡片动作,action 为 continue 或 retry_original_request,并传 sourceResponseId |
attachmentRefs | string[] | 否 | 已上传附件的相对路径;当前为保留字段,服务端暂不据此校验或重发 |
任务绑定、执行环境选择、问题诊断等平台内部字段不在本文展开,门户对接通常不需要使用。
返回字段
接口遵循常用 API的统一响应格式,下面展示的是其中 data 字段的内容;错误时外层 code 为对应错误码。
{
"conversationId": "conv_xxx",
"messageId": "msg-1713081600000-abc123",
"route": "started",
"accepted": true,
"acceptedAt": "2026-11-05T08:00:00.000Z",
"activeResponseId": "assistant-20261105-abc123",
"observationDisposition": "subscribe_new",
"idempotentReplay": false
}| 字段 | 类型 | 说明 |
|---|---|---|
conversationId | string | 会话 ID。新会话会返回新 ID |
messageId | string | 本次提交的消息 ID,即 clientMessageId 的原样回显 |
route | string | 提交路由结果,见下表 |
accepted | boolean | 消息是否已被接受处理 |
acceptedAt | string | 接受时间,部分路由场景返回 |
activeResponseId | string | 本轮 AI 回复 ID,订阅流时作为 expectedResponseId |
observationDisposition | string | 本轮应如何订阅:subscribe_new、reuse_existing、no_stream |
idempotentReplay | boolean | 是否为幂等重放。true 表示同一个 clientMessageId 和相同内容已处理过,本次不会产生新一轮 |
route 取值:
| 值 | 含义 | 客户端处理 |
|---|---|---|
started | 已开始新一轮处理 | 使用返回的 activeResponseId 订阅 stream |
interject | 消息被并入正在运行的执行 | 继续使用已有订阅,通常不需要再开新流 |
pending_response_required | 会话正暂停等待问题或权限回答 | 先提交 pendingResponse,不能直接发普通消息 |
observationDisposition 取值:
| 值 | 含义 |
|---|---|
subscribe_new | 产生了新的执行,需要新开订阅 |
reuse_existing | 已有另一个订阅在接收本轮输出,复用现有流,不要重复订阅 |
no_stream | 幂等重放或没有新输出,不需要订阅 |
常见错误
| 状态码 | 错误码 | 含义 | 客户端处理 |
|---|---|---|---|
| 400 | - | 参数校验失败,如标题不合法、env 与 conversationId 同时传入 | 修正请求 |
| 401 / 403 | - | 凭证失效或无权操作该会话 | 刷新凭证或检查授权 |
| 404 | - | 会话不存在 | 回到会话列表 |
| 409 | CHAT_IDEMPOTENCY_KEY_PAYLOAD_MISMATCH | 同一个 clientMessageId 被用于不同内容 | 换新的 clientMessageId |
| 409 | PENDING_RESPONSE_REQUEST_MISMATCH | 回答的 requestId/toolUseId 已不是当前待处理交互 | 刷新会话历史,不要自动重放旧答案 |
| 409 | PENDING_RESPONSE_CANCELLED | 原问题已自动取消 | 清理本地问题卡片并刷新历史,不要再发起中断 |
| 429 | CHAT_WORKSPACE_CONCURRENCY_LIMITED | 工作空间活跃会话数达到上限 | 稍后重试,建议做退避并至少间隔数秒 |
| 503 | PENDING_RESPONSE_OWNER_NOT_READY、PENDING_RESPONSE_WRITER_BUSY、PENDING_RESPONSE_STOP_UNCONFIRMED | 执行正在恢复或切换 | 保留用户答案,稍后用相同请求体重试 |
订阅实时输出
GET /api/v1/workspaces/{workspaceId}/chat/conversations/{conversationId}/stream?expectedResponseId={activeResponseId}
Authorization: Bearer <token>
Accept: text/event-stream订阅参数
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
workspaceId | string | 是 | 工作空间 ID |
conversationId | string | 是 | 会话 ID,来自提交接口返回的 conversationId |
查询参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
expectedResponseId | string | 否 | 期望订阅的回复 ID,建议始终传入提交接口返回的 activeResponseId。不传时订阅当前活跃回复;传入且与当前活跃回复不一致时不会报错,服务端会下发一条 stream_superseded 事件后结束连接 |
fromIndex | number | 否 | 从指定事件序号开始接收,默认 0。断线重连时传“最后已处理事件序号 + 1” |
botId | string | 否 | Bot 场景使用的 Bot ID,用于可见性和权限判断 |
请求头:
| 请求头 | 说明 |
|---|---|
Authorization | 登录态或 PAT。浏览器原生 EventSource 不能自定义该头时,可依赖登录 Cookie |
Last-Event-ID | 标准 SSE 续传头。未传 fromIndex 时,服务端从该序号的下一条开始续传 |
SSE 帧格式
普通事件使用默认的 message 事件,事件类型在 JSON 的 type 字段中:
id: 42
data: {"type":"content","responseId":"assistant-xxx","eventIndex":42,"blockIndex":0,"timestamp":"2026-11-05T08:00:01.000Z","content":"正在整理文档"}
id: 43
data: {"type":"ask_question","responseId":"assistant-xxx","eventIndex":43,"requestId":"req_xxx","toolUseId":"toolu_xxx","questions":[...]}心跳事件:
event: heartbeat
data: {}id 就是 eventIndex。断线重连时可用它作为 Last-Event-ID,或换算成 fromIndex。心跳约每 15 秒一次,不带业务含义,只用于保持连接和判断连接是否存活。
通用事件字段
| 字段 | 类型 | 说明 |
|---|---|---|
type | string | 事件类型 |
responseId | string | 所属 AI 回复 ID,用于判断是否为本轮事件 |
eventIndex | number | 单轮回复内的事件序号,从 0 开始单调递增,新一轮回复重新计数;配合 responseId 用于断线恢复和去重 |
blockIndex | number | 内容块序号,文本和工具块事件可能携带 |
timestamp | string | 事件产生时间,RFC 3339 |
status | string | 结束状态,主要用于 stream_end |
content | string | 文本增量,主要用于 content |
toolName | string | 工具名称 |
toolUseId | string | 工具调用 ID,用于关联工具调用与工具结果 |
args | object | 工具入参,主要用于 tool_call |
result | any | 工具执行结果,主要用于 tool_result |
errorType | string | 错误类型,主要用于 error |
message | string | 错误或提示信息 |
chatError | object | 版本化错误详情,包含 rawError 和 presentation |
事件类型
| 事件类型 | 说明 | 关键字段 |
|---|---|---|
response_started | 一轮回复开始 | conversationId、responseId |
system_init / session | 运行时初始化信息,部分运行时在一轮开始时下发 | — |
response_snapshot | 恢复订阅时下发的当前回复快照,用快照替换本地未完成状态 | status、blocks、snapshotEventIndex、timing |
user_message_received | 服务端已接收用户消息 | messageId、content |
content | AI 文本增量 | content、blockIndex |
content_end | 某个文本块结束,不表示整轮成功 | blockIndex |
thinking | 思考过程增量 | content |
tool_call | 工具调用开始或参数更新 | toolName、toolUseId、args、toolStatus |
tool_result_delta | 工具执行过程的增量输出 | toolUseId、result |
tool_result | 工具执行完成后的完整结果 | toolUseId、result、error |
permission_request | 需要用户授权某个工具操作 | requestId、toolUseId、toolName、title、description、options |
ask_question | AI 向用户提问,需要渲染问题卡片 | requestId、toolUseId、questions、allowFreeform |
title_updated | 会话标题更新 | conversationId、title |
context_usage | 上下文窗口使用率快照 | usedTokens、contextWindow、usedPercentage、autoCompactThreshold |
token_usage | 本轮 Token 用量 | inputTokens、outputTokens、cacheCreationInputTokens、cacheReadInputTokens |
retry | 运行时下发的重试通知 | 字段由运行时决定 |
compact_notice | 上下文压缩通知 | phase |
handoff_notice | 会话上下文交接通知 | 事件内附带的交接信息 |
interrupt_status | 中断请求已被确认 | status |
pause | 执行即将暂停等待用户输入,随后通常紧跟 stream_end(status=paused) | — |
stream_superseded | 本轮已被新的执行接管 | previousResponseId、nextResponseId |
session_degraded | 会话降级运行 | 事件内附带的降级信息 |
error | 本轮出错 | errorType、message、retriable、errorCode、chatError |
stream_end | 本轮输出结束 | status,取值见下表 |
heartbeat | 连接心跳 | 无业务字段 |
stream_end.status 取值:
| 值 | 含义 | 客户端处理 |
|---|---|---|
completed | 正常完成 | 关闭订阅,展示最终内容 |
paused | 暂停,等待用户回答问题或授权 | 关闭订阅,保持问题/权限卡片可操作 |
cancelled | 用户取消 | 关闭订阅,停止 loading |
interrupted | 被新的执行中断或接管 | 关闭订阅,按会话状态刷新 |
idle | 订阅时会话没有正在进行的执行(如该回复不存在或已结束) | 关闭订阅,查询会话详情确认最终结果 |
error | 本轮失败 | 关闭订阅,展示错误信息;是否可重试看 error 事件 |
收到 stream_end 或 error 后,本轮订阅即结束,不要继续等待更多事件。
快照事件 response_snapshot
当客户端订阅时本轮输出已经在进行,或断线期间的事件缓存已经无法补齐,服务端可能先下发一条 response_snapshot:
{
"type": "response_snapshot",
"conversationId": "conv_xxx",
"responseId": "assistant-xxx",
"snapshotEventIndex": 41,
"status": "paused",
"blocks": [
{
"blockIndex": 0,
"blockType": "text",
"content": "我需要确认一下你的选择。",
"completed": true
},
{
"blockIndex": 1,
"blockType": "tool_use",
"toolName": "AskUserQuestion",
"toolUseId": "toolu_xxx",
"toolStatus": "pending",
"toolSubtype": "start",
"args": {
"questions": [
{
"question": "选择部署环境",
"header": "部署环境",
"options": [
{ "label": "生产环境" },
{ "label": "预发环境" }
],
"multiSelect": false
}
],
"__knodoPendingRequest": {
"kind": "question",
"requestId": "req_xxx",
"toolUseId": "toolu_xxx"
}
}
}
],
"timing": {
"responseStartedAt": "2026-11-05T08:00:00.000Z"
}
}处理规则:
- 用快照替换本地
responseId相同的未完成回复,不要和已有文本重复拼接。 - 下一次重连使用
fromIndex = snapshotEventIndex + 1。 status取值为active、paused、completed、failed、interrupted;不是active就说明本轮已结束,关闭订阅,不必等待后续stream_end。注意快照的失败终态是failed而不是error。- 快照里的
tool_use块同样可以恢复未回答的问题卡片。
AskUserQuestion 问题卡片
当 AI 需要用户做选择、确认方案或补充信息时,会调用 AskUserQuestion。对门户来说,这就是“问题卡片”的数据来源。
事件顺序
一轮提问通常会依次收到三条事件:
tool_call:工具调用开始,toolName为AskUserQuestion。ask_question:问题内容,用于渲染卡片。stream_end:status为paused,表示本轮暂停等待用户回答。
示例:
data: {"type":"tool_call","responseId":"assistant-xxx","eventIndex":6,"toolName":"AskUserQuestion","toolUseId":"toolu_xxx","toolStatus":"pending","toolSubtype":"start","args":{"questions":[...],"__knodoPendingRequest":{"kind":"question","requestId":"req_xxx","toolUseId":"toolu_xxx"}}}
data: {"type":"ask_question","responseId":"assistant-xxx","eventIndex":7,"requestId":"req_xxx","toolUseId":"toolu_xxx","questions":[...],"allowFreeform":true}
data: {"type":"stream_end","responseId":"assistant-xxx","eventIndex":8,"status":"paused"}渲染卡片时应同时保存:
requestId:回答时必填,用于定位当前待处理交互。toolUseId:回答时建议带上,用于关联工具调用。questions:完整问题列表。responseId:本轮回复 ID,用于判断卡片是否仍然有效。
ask_question 事件结构
{
"type": "ask_question",
"responseId": "assistant-20261105-abc123",
"eventIndex": 43,
"timestamp": "2026-11-05T08:12:33.120Z",
"requestId": "req_01JABCDEF",
"toolUseId": "toolu_01ABCDEF",
"question": "选择部署环境",
"description": "部署环境",
"allowFreeform": true,
"questions": [
{
"id": "env",
"question": "选择部署环境",
"header": "部署环境",
"options": [
{
"id": "prod",
"label": "生产环境",
"description": "对外提供服务,变更需谨慎"
},
{
"id": "staging",
"label": "预发环境",
"description": "上线前验证"
}
],
"multiSelect": false,
"isSecret": false
},
{
"id": "regions",
"question": "需要发布到哪些区域?",
"header": "发布区域",
"options": [
{ "id": "cn", "label": "中国大陆" },
{ "id": "sg", "label": "新加坡" },
{ "id": "us", "label": "美国" }
],
"multiSelect": true
}
],
"options": [
{ "id": "prod", "label": "生产环境", "description": "对外提供服务,变更需谨慎" },
{ "id": "staging", "label": "预发环境", "description": "上线前验证" }
]
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
requestId | string | 本次交互的请求 ID,回答时必须原样带回 |
toolUseId | string | 工具调用 ID,回答时建议带回 |
question | string | 第一个问题的文本,方便只处理单题的客户端 |
description | string | 第一个问题的标题,通常等于第一个问题的 header |
options | array | 第一个问题的选项,方便只处理单题的客户端 |
questions | array | 完整问题列表,多题场景应使用该字段 |
allowFreeform | boolean | 是否允许自定义输入。当前 Knodo 默认提供“其他”输入框,可把它当作提示字段 |
questions[] 元素:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 问题 ID,部分运行时会提供;回答时建议同时以它为 key |
question | string | 问题正文,支持 Markdown |
header | string | 问题短标题,用于卡片标题或 tab 文案;可能为空 |
options | array | 选项列表,可能为空数组 |
multiSelect | boolean | true 表示多选,false 表示单选 |
isSecret | boolean | 提示答案可能是敏感信息,渲染时可使用密码输入或避免回显;部分运行时不提供 |
不同运行时对问题字段的补全程度不同,id、isSecret 等字段可能缺失,渲染时按可选字段处理。
options[] 元素:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 选项 ID,部分运行时会提供;没有时用 label 作为唯一值 |
label | string | 选项文案,也是回答时提交的值 |
description | string | 选项补充说明,可能为空 |
渲染成问题卡片
参考 Knodo 的交互,建议按以下规则渲染:
- 一题一卡:
questions只有一个元素时直接展示;多个元素时用 tab、步骤条或分页,不要把所有问题堆在一起。 - 标题:优先使用
header,为空时显示“问题 N”。 - 问题正文:使用 Markdown 渲染,保留换行和列表。
- 单选 / 多选:
multiSelect=false用单选控件,true用复选控件。 - 选项:展示
label;有description时作为副标题展示。 - 自定义输入:在选项末尾追加“其他 / Other”,选中后展示输入框。用户输入的自定义文本直接作为答案。
- 提交条件:每个问题都必须有答案。单选至少选一项,多选至少选一项,选择“其他”时必须填写输入框。
- 敏感问题:
isSecret=true时使用密码输入或提交后不回显答案。 - 可操作性:只有收到
ask_question且本轮状态仍为暂停时才允许提交;提交后立即把卡片置为“已提交”或“已取消”,防止重复点击。 - 不要自动回答:即使用户之前回答过类似问题,也不要复用旧答案代替用户确认。
多题卡片可以在顶部显示“已完成 1/2”的进度,并在用户切换问题时校验当前问题,这些只是展示层增强,不影响提交协议。
提交回答
用户完成选择后,再次调用 chat/submit,并在 pendingResponse 中带回交互信息。
POST /api/v1/workspaces/{workspaceId}/chat/submit
Authorization: Bearer <token>
Content-Type: application/json{
"conversationId": "conv_xxx",
"message": "部署环境:生产环境;发布区域:中国大陆、新加坡",
"clientMessageId": "msg-answer-20261105-001",
"pendingResponse": {
"requestId": "req_01JABCDEF",
"toolUseId": "toolu_01ABCDEF",
"response": {
"behavior": "allow",
"answers": {
"env": "生产环境",
"选择部署环境": "生产环境",
"regions": "中国大陆\n新加坡",
"需要发布到哪些区域?": "中国大陆\n新加坡"
}
}
}
}pendingResponse 字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
requestId | string | 是 | 来自 ask_question 或 __knodoPendingRequest.requestId,必须与当前待处理交互一致 |
toolUseId | string | 否 | 来自 ask_question 或 __knodoPendingRequest.toolUseId;提供时必须一致 |
response.behavior | string | 是 | 正常情况下传 allow;拒绝回答时传 deny |
response.answers | object | 问题回答时是 | 答案对象,key 为问题,value 为用户答案。回答问题卡片时必须提供;权限确认场景可不带 |
response.message | string | 否 | deny 时的说明,例如“用户取消回答” |
response.updatedInput | object | 否 | 高级用法,直接覆盖工具入参;普通问题卡片不需要 |
answers 的 key 和 value 规则:
- key 优先使用问题的
question文本;如果问题带有id,建议同时写入一份idkey,兼容不同运行时。 - 单选:value 为选中的
label,或用户填写的自定义文本。 - 多选:多个
label用换行符\n连接。 - 自定义输入:value 直接使用用户填写的文本。
- 同一个问题使用
id和question两个 key 时,value 必须相同。
示例:单选、多选、自定义输入混用。
{
"requestId": "req_01JABCDEF",
"toolUseId": "toolu_01ABCDEF",
"response": {
"behavior": "allow",
"answers": {
"env": "staging",
"选择部署环境": "预发环境",
"regions": "中国大陆\n新加坡",
"需要发布到哪些区域?": "中国大陆\n新加坡",
"备注": "请先通知值班同学"
}
}
}message 用于在会话中保存一条用户可见的回答摘要。Knodo 会把各题答案用换行拼接后作为 message,你也可以自定义更易读的摘要。clientMessageId 必须使用新的值;如果因为网络问题重试同一个回答,则保持同一个 clientMessageId 和相同的请求体。
成功响应:
{
"conversationId": "conv_xxx",
"messageId": "msg-answer-20261105-001",
"route": "started",
"accepted": true,
"activeResponseId": "assistant-20261105-def456",
"observationDisposition": "subscribe_new",
"idempotentReplay": false
}随后使用新的 activeResponseId 重新订阅 stream,继续接收 AI 的后续输出:
GET /api/v1/workspaces/ws_xxx/chat/conversations/conv_xxx/stream?expectedResponseId=assistant-20261105-def456用户取消或拒绝回答
有两种处理方式:
- 取消本轮执行:调用
POST /api/v1/workspaces/{workspaceId}/chat/conversations/{conversationId}/interrupt。服务端会结束暂停中的本轮执行,问题卡片可以关闭。 - 通过回答通道拒绝:提交
pendingResponse,把response.behavior设为deny,并在response.message中说明原因。注意deny不会终止本轮执行,AI 会收到拒绝说明后继续运行;如果要直接停止执行,应使用方式 1 的 interrupt。例如:
{
"conversationId": "conv_xxx",
"message": "用户取消回答",
"clientMessageId": "msg-answer-20261105-002",
"pendingResponse": {
"requestId": "req_01JABCDEF",
"toolUseId": "toolu_01ABCDEF",
"response": {
"behavior": "deny",
"message": "用户取消回答"
}
}
}回答失败与重试
| 状态码 | 错误码 | 含义 | 客户端处理 |
|---|---|---|---|
| 409 | PENDING_RESPONSE_REQUEST_MISMATCH | requestId/toolUseId 已不是当前待处理交互,或交互已结束 | 关闭卡片,刷新会话历史;不要自动重放旧答案 |
| 409 | PENDING_RESPONSE_CANCELLED | 原执行已确认丢失,平台已自动取消 | 清理本地卡片并刷新历史;不要再调用 interrupt |
| 503 | PENDING_RESPONSE_OWNER_NOT_READY | 执行正在恢复,暂时不能接收回答 | 可重试,保留用户答案和同一个 clientMessageId |
| 503 | PENDING_RESPONSE_WRITER_BUSY | 本轮输出仍在收尾 | 可重试,保留用户答案和同一个 clientMessageId |
| 503 | PENDING_RESPONSE_STOP_UNCONFIRMED | 旧执行退出状态未确认 | 可重试;平台确认旧执行丢失后会返回 PENDING_RESPONSE_CANCELLED |
重试时请使用完全相同的 requestId、toolUseId、response 和 clientMessageId。如果 requestId 已经过期,不要用旧答案强行提交,应重新查询会话历史。
刷新页面后恢复未回答问题
用户刷新页面或重新进入会话时,问题卡片可能还没有回答。可以通过两种方式恢复:
方式一:订阅流时收到 response_snapshot
快照 status 为 paused,blocks 中存在:
{
"blockType": "tool_use",
"toolName": "AskUserQuestion",
"toolUseId": "toolu_xxx",
"toolStatus": "pending",
"args": {
"questions": [
{
"question": "选择部署环境",
"header": "部署环境",
"options": [{ "label": "生产环境" }, { "label": "预发环境" }],
"multiSelect": false
}
],
"__knodoPendingRequest": {
"kind": "question",
"requestId": "req_xxx",
"toolUseId": "toolu_xxx"
}
}
}方式二:查询会话历史
调用 GET /api/v1/workspaces/{workspaceId}/chat/conversations/{conversationId}/messages,在返回的消息中查找:
blockType为tool_usetoolName为AskUserQuestiontoolResult为空toolInput.questions存在toolInput.__knodoPendingRequest存在
对应字段:
| 历史消息字段 | 含义 |
|---|---|
toolInput.questions | 完整问题列表,结构同 ask_question.questions |
toolInput.__knodoPendingRequest.requestId | 回答时使用的 requestId |
toolInput.__knodoPendingRequest.toolUseId | 回答时使用的 toolUseId |
toolResult | 已有值表示该问题已回答或已结束,不要再渲染可提交的卡片 |
同时可通过会话详情或状态接口确认 conversationStatus 为 paused。如果会话已经结束或继续运行,未回答的旧问题不应再提供提交入口。
权限确认与问题的区别
非问答类工具(例如执行命令、写文件)需要授权时,事件类型是 permission_request,不是 ask_question:
{
"type": "permission_request",
"responseId": "assistant-xxx",
"eventIndex": 20,
"requestId": "req_allow_xxx",
"toolUseId": "toolu_allow_xxx",
"toolName": "Bash",
"title": "请求执行命令",
"description": "命令: npm run build",
"options": [
{ "id": "allow", "label": "允许" },
{ "id": "deny", "label": "拒绝" }
]
}权限确认同样通过 chat/submit 的 pendingResponse 回答,response.behavior 传 allow 或 deny;需要调整工具入参时使用 response.updatedInput,allow 时还可在 response.permissionMode 中指定后续权限模式。问题卡片渲染只需要关注 ask_question。
断线重连
订阅连接可能因平台升级、网络波动或服务端恢复而中断,但会话本身会继续执行。客户端重新订阅即可接上,不需要重新提交消息。
关键规则:
- 记录最后一个已处理事件的
eventIndex,重连时传fromIndex = eventIndex + 1。 - 未传
fromIndex时,服务端兼容标准 SSE 客户端的Last-Event-ID请求头,从该序号的下一条开始续传。 - 按
responseId+eventIndex去重,SSE 重放可能重复投递相同事件。 - 收到
stream_end或error后本轮结束,不要继续重连。 - 超过约 45 秒既没有事件也没有心跳时,可视为连接失效,主动断开后重连。
- 503、429 会返回
Retry-After响应头,客户端等待时间不应短于该值。 - 409 表示续传位置已无法补齐,应先查询会话详情获取已持久化内容,再从
fromIndex=0重新订阅。 - 浏览器原生
EventSource无法读取状态码和Retry-After,需要精细重试时建议使用基于fetch的 SSE 客户端。
重试节奏、状态码明细和更多断线处理规则见常用 API的「断线重连」章节。