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

请求参数

参数类型必填说明
messagestring是用户消息正文。回答问题时填写答案摘要,作为用户可见消息保存
clientMessageIdstring是客户端生成的消息 ID,用于幂等和界面去重。同一条逻辑消息重试时保持不变;内容变化时必须换新 ID
conversationIdstring否会话 ID。不传则创建新会话;续聊或回答问题时必须传入
pendingResponseobject否回答待处理的 AskUserQuestion 或权限确认,详见「AskUserQuestion 问题卡片」
titlestring否新会话初始标题,trim 后 1–200 个 Unicode 码点;不能与 conversationId 同时传入
sdkTypestring否新会话运行时类型,如 claude-code、codex
modelstring否指定本轮使用的模型;创建新会话和续聊时生效,插话(并入运行中的执行)不会改变当前执行的模型;不传则沿用会话当前模型
effortLevelstring否推理强度:low、medium、high、xhigh、max、auto、disabled
permissionModestring否权限模式,如 default、acceptEdits、plan、bypassPermissions
systemPromptstring否本轮使用的系统提示词
botIdstring否使用指定 Bot 发起对话
envobject否新会话自定义环境变量;key 必须以 JAVIS_CUSTOM_ 开头,且不能与 conversationId 同时传入。最多 10 个,key 最长 128、value 最长 4096 个字符
tagsstring[]否新会话标签,最多 5 个,每个最多 20 字符;仅创建新会话时生效
contextCarryoverobject否新会话上下文来源,mode 为 fork 或 recover-history,并传 sourceConversationId
errorActionobject否错误卡片动作,action 为 continue 或 retry_original_request,并传 sourceResponseId
attachmentRefsstring[]否已上传附件的相对路径;当前为保留字段,服务端暂不据此校验或重发

任务绑定、执行环境选择、问题诊断等平台内部字段不在本文展开,门户对接通常不需要使用。

返回字段

接口遵循常用 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 }
字段类型说明
conversationIdstring会话 ID。新会话会返回新 ID
messageIdstring本次提交的消息 ID,即 clientMessageId 的原样回显
routestring提交路由结果,见下表
acceptedboolean消息是否已被接受处理
acceptedAtstring接受时间,部分路由场景返回
activeResponseIdstring本轮 AI 回复 ID,订阅流时作为 expectedResponseId
observationDispositionstring本轮应如何订阅:subscribe_new、reuse_existing、no_stream
idempotentReplayboolean是否为幂等重放。true 表示同一个 clientMessageId 和相同内容已处理过,本次不会产生新一轮

route 取值:

值含义客户端处理
started已开始新一轮处理使用返回的 activeResponseId 订阅 stream
interject消息被并入正在运行的执行继续使用已有订阅,通常不需要再开新流
pending_response_required会话正暂停等待问题或权限回答先提交 pendingResponse,不能直接发普通消息

observationDisposition 取值:

值含义
subscribe_new产生了新的执行,需要新开订阅
reuse_existing已有另一个订阅在接收本轮输出,复用现有流,不要重复订阅
no_stream幂等重放或没有新输出,不需要订阅

常见错误

状态码错误码含义客户端处理
400-参数校验失败,如标题不合法、env 与 conversationId 同时传入修正请求
401 / 403-凭证失效或无权操作该会话刷新凭证或检查授权
404-会话不存在回到会话列表
409CHAT_IDEMPOTENCY_KEY_PAYLOAD_MISMATCH同一个 clientMessageId 被用于不同内容换新的 clientMessageId
409PENDING_RESPONSE_REQUEST_MISMATCH回答的 requestId/toolUseId 已不是当前待处理交互刷新会话历史,不要自动重放旧答案
409PENDING_RESPONSE_CANCELLED原问题已自动取消清理本地问题卡片并刷新历史,不要再发起中断
429CHAT_WORKSPACE_CONCURRENCY_LIMITED工作空间活跃会话数达到上限稍后重试,建议做退避并至少间隔数秒
503PENDING_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

订阅参数

路径参数:

参数类型必填说明
workspaceIdstring是工作空间 ID
conversationIdstring是会话 ID,来自提交接口返回的 conversationId

查询参数:

参数类型必填说明
expectedResponseIdstring否期望订阅的回复 ID,建议始终传入提交接口返回的 activeResponseId。不传时订阅当前活跃回复;传入且与当前活跃回复不一致时不会报错,服务端会下发一条 stream_superseded 事件后结束连接
fromIndexnumber否从指定事件序号开始接收,默认 0。断线重连时传“最后已处理事件序号 + 1”
botIdstring否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 秒一次,不带业务含义,只用于保持连接和判断连接是否存活。

通用事件字段

字段类型说明
typestring事件类型
responseIdstring所属 AI 回复 ID,用于判断是否为本轮事件
eventIndexnumber单轮回复内的事件序号,从 0 开始单调递增,新一轮回复重新计数;配合 responseId 用于断线恢复和去重
blockIndexnumber内容块序号,文本和工具块事件可能携带
timestampstring事件产生时间,RFC 3339
statusstring结束状态,主要用于 stream_end
contentstring文本增量,主要用于 content
toolNamestring工具名称
toolUseIdstring工具调用 ID,用于关联工具调用与工具结果
argsobject工具入参,主要用于 tool_call
resultany工具执行结果,主要用于 tool_result
errorTypestring错误类型,主要用于 error
messagestring错误或提示信息
chatErrorobject版本化错误详情,包含 rawError 和 presentation

事件类型

事件类型说明关键字段
response_started一轮回复开始conversationId、responseId
system_init / session运行时初始化信息,部分运行时在一轮开始时下发—
response_snapshot恢复订阅时下发的当前回复快照,用快照替换本地未完成状态status、blocks、snapshotEventIndex、timing
user_message_received服务端已接收用户消息messageId、content
contentAI 文本增量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_questionAI 向用户提问,需要渲染问题卡片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。对门户来说,这就是“问题卡片”的数据来源。

事件顺序

一轮提问通常会依次收到三条事件:

  1. tool_call:工具调用开始,toolName 为 AskUserQuestion。
  2. ask_question:问题内容,用于渲染卡片。
  3. 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": "上线前验证" } ] }

字段说明:

字段类型说明
requestIdstring本次交互的请求 ID,回答时必须原样带回
toolUseIdstring工具调用 ID,回答时建议带回
questionstring第一个问题的文本,方便只处理单题的客户端
descriptionstring第一个问题的标题,通常等于第一个问题的 header
optionsarray第一个问题的选项,方便只处理单题的客户端
questionsarray完整问题列表,多题场景应使用该字段
allowFreeformboolean是否允许自定义输入。当前 Knodo 默认提供“其他”输入框,可把它当作提示字段

questions[] 元素:

字段类型说明
idstring问题 ID,部分运行时会提供;回答时建议同时以它为 key
questionstring问题正文,支持 Markdown
headerstring问题短标题,用于卡片标题或 tab 文案;可能为空
optionsarray选项列表,可能为空数组
multiSelectbooleantrue 表示多选,false 表示单选
isSecretboolean提示答案可能是敏感信息,渲染时可使用密码输入或避免回显;部分运行时不提供

不同运行时对问题字段的补全程度不同,id、isSecret 等字段可能缺失,渲染时按可选字段处理。

options[] 元素:

字段类型说明
idstring选项 ID,部分运行时会提供;没有时用 label 作为唯一值
labelstring选项文案,也是回答时提交的值
descriptionstring选项补充说明,可能为空

渲染成问题卡片

参考 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 字段:

字段类型必填说明
requestIdstring是来自 ask_question 或 __knodoPendingRequest.requestId,必须与当前待处理交互一致
toolUseIdstring否来自 ask_question 或 __knodoPendingRequest.toolUseId;提供时必须一致
response.behaviorstring是正常情况下传 allow;拒绝回答时传 deny
response.answersobject问题回答时是答案对象,key 为问题,value 为用户答案。回答问题卡片时必须提供;权限确认场景可不带
response.messagestring否deny 时的说明,例如“用户取消回答”
response.updatedInputobject否高级用法,直接覆盖工具入参;普通问题卡片不需要

answers 的 key 和 value 规则:

  • key 优先使用问题的 question 文本;如果问题带有 id,建议同时写入一份 id key,兼容不同运行时。
  • 单选: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

用户取消或拒绝回答

有两种处理方式:

  1. 取消本轮执行:调用 POST /api/v1/workspaces/{workspaceId}/chat/conversations/{conversationId}/interrupt。服务端会结束暂停中的本轮执行,问题卡片可以关闭。
  2. 通过回答通道拒绝:提交 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": "用户取消回答" } } }

回答失败与重试

状态码错误码含义客户端处理
409PENDING_RESPONSE_REQUEST_MISMATCHrequestId/toolUseId 已不是当前待处理交互,或交互已结束关闭卡片,刷新会话历史;不要自动重放旧答案
409PENDING_RESPONSE_CANCELLED原执行已确认丢失,平台已自动取消清理本地卡片并刷新历史;不要再调用 interrupt
503PENDING_RESPONSE_OWNER_NOT_READY执行正在恢复,暂时不能接收回答可重试,保留用户答案和同一个 clientMessageId
503PENDING_RESPONSE_WRITER_BUSY本轮输出仍在收尾可重试,保留用户答案和同一个 clientMessageId
503PENDING_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_use
  • toolName 为 AskUserQuestion
  • toolResult 为空
  • 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的「断线重连」章节。