业务会话:续聊、历史、事件与停止重开
用真实 Gateway 路由创建与延续会话,读取历史和 SSE,并以 generation、幂等键和控制记录管理停止与重开。
开发者接入会话、任务和控制操作#
先按外部接入总览发布服务并启用接入,再从业务后端调用这里的 Gateway API。
| 对象 | 用途 |
|---|---|
conversation_id | 一个长期业务会话;后续消息继续使用它 |
generation | 当前上下文代际,从 1 开始;reset 完成后加 1 |
run_id | 一次消息产生的任务,用于查结果、观察事件或停止 |
operation_id | 一次停止/reset 控制操作,必须另查是否完成 |
新建会话不会停止旧会话的任务。Reset 保留会话 ID 和业务关联,换一代上下文;它不会撤销已经发生的文件修改或外部操作。
准备示例环境#
以下命令用于 Bash、curl(支持 --fail-with-body)和 jq。示例响应用于说明返回格式,不是真实任务结果;请使用自己请求返回的 ID。
将 APP_KEY 从凭证存储注入测试终端或后端环境,其他变量按界面填写。不要打印密钥。用下面的固定操作标识完成一次练习;有意开始另一轮练习时再修改 FLOW_ID,网络重试时不要修改。
这里的 GATEWAY_URL 只填网关 origin,不包含末尾 /api/v1。 例如界面显示 http://127.0.0.1:48541/api/v1 时,变量填 http://127.0.0.1:48541;下方命令会自行追加 /api/v1,不要重复。
export GATEWAY_URL='http://127.0.0.1:REPLACE_WITH_API_PORT'
export ENDPOINT_ID='YOUR_PUBLISHED_ENDPOINT_ID'
export SUBJECT='docs-test-user'
: "${APP_KEY:?Load APP_KEY from your credential store first}"
FLOW_ID='docs-demo-001'
BUSINESS_KEY="case-$FLOW_ID"
CREATE_KEY="$FLOW_ID-create"
本页全部步骤需要 conversations:read、conversations:write、runs:read、runs:write。Scope 和业务用户主体的处理见鉴权与权限。本地地址使用 Onevium 显示的 API 地址;远端使用配置好的 HTTPS Gateway 地址。
1. 新建业务会话#
POST /api/v1/conversations,成功返回 201。
| 字段/请求头 | 要求 |
|---|---|
endpoint_id | 必填,当前应用下已发布的服务 ID |
conversation_key | 可选业务关联键,1–256 字符且无首尾空白,例如工单编号;不传则不绑定业务键 |
Idempotency-Key | 必填,1–128 个可见 ASCII 字符,不含空格;表示这一次创建操作 |
conversation_key 不是幂等键。在同一应用、subject、服务下,使用另一创建请求标识重复创建同一业务键,会返回 409 BUSY;应保存并继续原会话。
CREATE_BODY=$(jq -nc --arg endpoint "$ENDPOINT_ID" --arg key "$BUSINESS_KEY" \
'{endpoint_id:$endpoint,conversation_key:$key}')
CREATE_RESPONSE=$(curl --fail-with-body -sS "$GATEWAY_URL/api/v1/conversations" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $CREATE_KEY" \
-H 'Content-Type: application/json' --data "$CREATE_BODY")
printf '%s\n' "$CREATE_RESPONSE" | jq .
CONVERSATION_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.conversation_id')
GENERATION=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.generation')
只在请求成功并取得两个值后继续。示例响应:
{
"conversation_id": "conv_0123456789abcdef_0000000000000001",
"owner_id": "example-owner",
"app_id": "11111111111111111111111111111111",
"subject": "docs-test-user",
"endpoint_id": "22222222222222222222222222222222",
"binding_version": 1,
"generation": 1,
"business_key": "case-docs-demo-001",
"state": "active",
"created_at": "2026-09-09T08:00:00.000Z"
}
请求字段 conversation_key 在会话记录中名为 business_key。binding_version 标识会话采用的已发布服务版本;它与 generation 是两个不同字段。
2. 消息字段怎么填#
POST /api/v1/conversations/{conversation_id}/messages 接受以下字段,未知字段会被拒绝。
| 字段 | 要求与含义 |
|---|---|
expected_generation | 必填正整数,必须是当前代际;不要写字符串 "1" |
input | 必填对象,以下三种输入至少一种非空 |
input.text | 可选文字,最多 64 KiB UTF-8;缺省按空文字处理 |
input.data | 可选普通 JSON 对象,传递结构化业务材料 |
input.resource_ids | 可选,最多 5 个不重复的已就绪附件 ID;须属于同一应用和 subject,类型还需服务支持 |
start_before | 可选 RFC 3339 时间,必须带时区;消息接口要求晚于现在且不超过接纳时刻后 5 分钟,缺省为 5 分钟后 |
start_before 是最晚开始时间,不是执行超时。请求 JSON 总大小上限为 1 MiB。不要在这里传模型、工作目录、工具权限或 busy_policy;模型和能力来自已发布服务,忙碌策略只属于 reset。
3. 发送第一条消息#
MESSAGE_1_KEY="$FLOW_ID-message-1"
MESSAGE_1_BODY=$(jq -nc --argjson generation "$GENERATION" \
'{expected_generation:$generation,input:{text:"记住演示代号 Orion,只回复收到。",data:{case_id:"DEMO-001"}}}')
ADMISSION=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $MESSAGE_1_KEY" \
-H 'Content-Type: application/json' --data "$MESSAGE_1_BODY")
printf '%s\n' "$ADMISSION" | jq .
RUN_ID=$(printf '%s' "$ADMISSION" | jq -er '.run_id')
202 示例接纳响应:
{
"conversation_id": "conv_0123456789abcdef_0000000000000001",
"generation": 1,
"message_id": "msg_0123456789abcdef_0000000000000003",
"run_id": "run_0123456789abcdef_0000000000000002",
"state": "queued",
"event_cursor": 2
}
保存 run_id,不能把 202 当成模型已回复。event_cursor 是接纳时的会话事件序号。原操作的幂等重放可能仍返回最初这份 queued 响应,当前状态要用 GET 查询。
4. 查询任务状态与结果#
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/runs/$RUN_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" | jq .
成功终态的示例响应:
{
"run_id": "run_0123456789abcdef_0000000000000002",
"message_id": "msg_0123456789abcdef_0000000000000003",
"conversation_id": "conv_0123456789abcdef_0000000000000001",
"generation": 1,
"state": "succeeded",
"delivery_state": "device_received",
"created_at": "2026-09-09T08:00:01.000Z",
"updated_at": "2026-09-09T08:00:03.000Z",
"result": { "text": "收到。" }
}
| 状态 | 客户端应如何处理 |
|---|---|
queued | 已排队;结合投递状态和开始期限观察 |
running | 正在执行 |
waiting_approval | 等待审批,尚未完成 |
cancelling | 已请求停止,尚未确认停止 |
reconciling | 正在核对执行状态,不把未知结果当成成功 |
succeeded | 成功终态,读取 result |
failed | 失败终态,检查 result.error(若有) |
cancelled、interrupted、expired | 均为终态,但不是成功 |
waiting_approval 需要现有审批流程处理;这些公开业务路由没有“批准任意工具操作”的接口。
delivery_state 只有 waiting_device 和 device_received,与运行状态独立。result 可以为 null,有结果时可包含 text、resource_ids 或 error。失败也可能保留部分输出。
5. 继续同一会话#
先确认前一轮结果,再读当前快照取得真实代际:
SNAPSHOT=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT")
GENERATION=$(printf '%s' "$SNAPSHOT" | jq -er '.generation')
MESSAGE_2_KEY="$FLOW_ID-message-2"
MESSAGE_2_BODY=$(jq -nc --argjson generation "$GENERATION" \
'{expected_generation:$generation,input:{text:"之前的演示代号是什么?"}}')
ADMISSION_2=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $MESSAGE_2_KEY" \
-H 'Content-Type: application/json' --data "$MESSAGE_2_BODY")
printf '%s\n' "$ADMISSION_2" | jq .
RUN_ID=$(printf '%s' "$ADMISSION_2" | jq -er '.run_id')
这次会获得新的 message_id 和 run_id;保存第二个 Run ID 再查结果。演示中预期回复 Orion,实际仍应核对模型输出。
同会话允许排队。 一轮运行时,下一条消息可以被接纳为 queued,执行按会话串行。当前每会话最多 20 条 queued 任务,超过上限返回 429;会话在 resetting 等不可接纳状态时才会返回 BUSY。应用和设备另有总任务限额。
6. 读取快照和分页历史#
GET /conversations/{id} 返回快照,顶层为 conversation_id、当前 generation、watermark、messages 和 runs。它不是创建接口的完整会话记录。快照最多取近期 200 条消息和 100 条任务,并有响应体预算;更早消息用历史接口读取。
快照的每个 run 包含 run_id、generation、state、布尔值 device_received、result 和 result_available。若结果记录存在但快照受体积预算限制,可能出现 result_available: true 而 result: null,此时单独 GET run。watermark 用于下面的 SSE 续传,不用于历史翻页。
HISTORY=$(curl --fail-with-body -sS --get \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
--data-urlencode "generation=$GENERATION" --data-urlencode 'limit=50')
printf '%s\n' "$HISTORY" | jq .
| 查询参数 | 含义 |
|---|---|
generation | 可选正整数;不传则包含不同代际的保留消息 |
limit | 缺省 50;HTTP 接口接受 1–200,当前存储层每页实际最多 100 条,建议使用不超过 100 |
before | 可选正整数,使用上一页最小的 cursor 读取更早消息;不是 message_id,也不是 SSE seq |
当前页非空时,可以继续读取前一页:
BEFORE=$(printf '%s' "$HISTORY" | jq -er '.messages | map(.cursor) | min // empty')
curl --fail-with-body -sS --get \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
--data-urlencode "generation=$GENERATION" \
--data-urlencode "before=$BEFORE" --data-urlencode 'limit=50' | jq .
历史响应示例(这里仅有一条消息):
{
"messages": [
{
"cursor": 1,
"message_id": "msg_0123456789abcdef_0000000000000003",
"conversation_id": "conv_0123456789abcdef_0000000000000001",
"generation": 1,
"run_id": "run_0123456789abcdef_0000000000000002",
"role": "user",
"created_at": "2026-09-09T08:00:01.000Z",
"content": { "text": "记住演示代号 Orion,只回复收到。", "data": { "case_id": "DEMO-001" } }
}
]
}
每页按较早到较晚排列,下一页取本页最小 cursor;空数组时结束。快照可能还含 content.partial: true 的未完成助手消息;最终结果仍查对应 run。当前事件保留 7 天,终态消息/结果正文约 30 天,不将 Gateway 当作永久业务档案。
要列出会话,使用 GET /api/v1/conversations?endpoint_id=服务ID&limit=50,可用会话 ID 作为 before。这里的 endpoint_id 用于定位设备,当前返回该设备下同一应用/subject 的会话,不能假定只包含该服务。
curl --fail-with-body -sS --get "$GATEWAY_URL/api/v1/conversations" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
--data-urlencode "endpoint_id=$ENDPOINT_ID" --data-urlencode 'limit=50' | jq .
7. 用 SSE 观察和断线续传#
首次建立业务界面时,先 GET 快照,再从 watermark 订阅,避免把旧内容重复追加:
SNAPSHOT=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT")
LAST_SEQ=$(printf '%s' "$SNAPSHOT" | jq -er '.watermark')
curl --fail-with-body -sS -N \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/events" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Last-Event-ID: $LAST_SEQ" \
-H 'Accept: text/event-stream'
示例事件,不是实测输出:
id: 3
event: run.started
data: {"conversation_id":"conv_0123456789abcdef_0000000000000001","generation":1,"event_id":"evt_0123456789abcdef_0000000000000004","seq":3,"run_id":"run_0123456789abcdef_0000000000000002","message_id":null,"type":"run.started","payload":{"state":"running"}}
Last-Event-ID填数值 seq,不是event_id。也可以用?after=3;两者同时提供时请求头优先。- 保存最后已处理序号,再从该序号重连;服务只返回
seq更大的事件。先持久化处理结果,再推进客户端游标。 GET /api/v1/runs/{run_id}/events只观察一个 run,仍使用会话级 seq,因此允许跳号;会话 reset 等事件应观察会话流。assistant.delta是增量,assistant.message是完整消息;按消息 ID 更新,不把两者都重复追加成最终答案。410 CURSOR_EXPIRED时重新取快照,再从新 watermark 订阅。: ping是保活注释。
看到终态后可主动关闭观察流;SSE 不保证自动关闭。断开 SSE 不取消任务。当前每设备/应用/subject 的并发流上限分别为 32/16/4,应用应复用观察连接。
8. 停止一个 run#
停止接口是 POST /api/v1/runs/{run_id}/cancel,请求体必须是空对象。它不是 /stop,也不接受 expected_generation。
对仍需停止的实际 RUN_ID 执行:
CANCEL_KEY="$FLOW_ID-cancel-1"
CONTROL=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/runs/$RUN_ID/cancel" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $CANCEL_KEY" \
-H 'Content-Type: application/json' --data '{}')
printf '%s\n' "$CONTROL" | jq .
OPERATION_ID=$(printf '%s' "$CONTROL" | jq -er '.operation_id')
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" | jq .
202 控制响应示例:
{
"operation_id": "op_0123456789abcdef_0000000000000005",
"state": "stopping",
"ref": { "conversation_id": "conv_0123456789abcdef_0000000000000001", "generation": 1 }
}
pending、waiting_device、stopping 都未完成;succeeded 表示控制已完成;failed 表示控制未成功。停止/reset 操作通常返回 stopping 或 succeeded,客户端仍应识别完整协议枚举。
未开始的 queued run 可以直接取消;已执行的 run 需要确认实际终态。停止已经完成的 run,operation 可以成功,而 run 仍保留原来的 succeeded 等终态。控制完成后再 GET run 核对,不能仅凭 202 或 cancelling 宣称停止成功。
9. 重开上下文:reset#
POST /api/v1/conversations/{id}/reset 支持两个字段:
| 字段 | 含义 |
|---|---|
expected_generation | 必填,当前代际 |
busy_policy | 可选,缺省 reject;存在非终态任务则返回 409 BUSY。明确选择 cancel_and_reset 时,会取消本代未完成任务并等待停止,再换代 |
先读快照取得当前 generation。没有未完成任务时:
GENERATION=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" | jq -er '.generation')
RESET_KEY="$FLOW_ID-reset-idle-1"
RESET_BODY=$(jq -nc --argjson generation "$GENERATION" \
'{expected_generation:$generation,busy_policy:"reject"}')
RESET_RESPONSE=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/reset" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $RESET_KEY" \
-H 'Content-Type: application/json' --data "$RESET_BODY")
printf '%s\n' "$RESET_RESPONSE" | jq .
OPERATION_ID=$(printf '%s' "$RESET_RESPONSE" | jq -er '.operation_id')
RESET_STATUS=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT")
printf '%s\n' "$RESET_STATUS" | jq .
若你明确要停止未完成工作再重开,应作为另一控制操作,用新键发送:
RESET_KEY="$FLOW_ID-reset-busy-1"
RESET_BODY=$(jq -nc --argjson generation "$GENERATION" \
'{expected_generation:$generation,busy_policy:"cancel_and_reset"}')
RESET_RESPONSE=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/conversations/$CONVERSATION_ID/reset" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT" \
-H "Idempotency-Key: $RESET_KEY" \
-H 'Content-Type: application/json' --data "$RESET_BODY")
printf '%s\n' "$RESET_RESPONSE" | jq .
OPERATION_ID=$(printf '%s' "$RESET_RESPONSE" | jq -er '.operation_id')
两种方式择一,不要盲目依次执行。读取其返回的 operation ID 并查询到 succeeded;完成响应示例:
{
"operation_id": "op_0123456789abcdef_0000000000000006",
"state": "succeeded",
"ref": { "conversation_id": "conv_0123456789abcdef_0000000000000001", "generation": 2 }
}
完成后使用 ref.generation 或最新快照中的代际发送下一条消息,旧代际会得到 409。Reset 还会采用服务当前发布版本;服务重新发布后出现版本冲突时,需要检查是否应重开。历史消息并不因 reset 立即删除,可用 generation=1 查询旧代际的保留记录。
确认 operation 已成功后,再更新本地代际变量:
RESET_STATUS=$(curl --fail-with-body -sS \
"$GATEWAY_URL/api/v1/operations/$OPERATION_ID" \
-H "Authorization: Bearer $APP_KEY" \
-H "X-Onevium-Subject: $SUBJECT")
GENERATION=$(printf '%s' "$RESET_STATUS" | jq -er \
'select(.state == "succeeded") | .ref.generation')
幂等、重试与并发约定#
每次有意的新建、发消息、停止、reset 使用新键。同一次操作遇到断网或响应丢失时,保留原 应用、subject、服务、目标、请求键和正文,重放后查询资源当前状态。
- 同键不同内容:
409 IDEMPOTENCY_CONFLICT,不要用反复改键掩盖未知执行结果。 - 代际冲突:先读快照,核对待发送内容是否仍适用;若决定按新上下文重新执行,作为新操作使用新键。
- 创建时业务键重复:找回已保存的 conversation ID,不把它当成普通重试。
- 停止/reset 响应丢失:同键重试取回 operation,再继续查询。
- 429:读取
Retry-After并等待;队列、存储等条件也需实际恢复。
与直接创建 run 的区别#
总览中的 POST /api/v1/agent-endpoints/{endpoint_id}/runs 适合任务型入口,字段为 input、可选 conversation_key、可选 expected_generation、可选 start_before。
不带业务键时每次新操作建立新会话;带业务键时会查找或建立关联会话。传 expected_generation 时必须同时传 conversation_key,且关联会话已经存在。该入口的最晚开始窗口为 24 小时,与消息接口的 5 分钟不同。需要明确控制会话代际的交互流程,优先使用本页的会话接口。
验证清单和错误处理#
本地集成至少检查:新建、第二轮续聊、历史翻页、SSE 重连、取消 queued/运行中任务、两种 reset 策略、旧 generation 拒绝、同键重试和跨 subject 404。
| 错误 | 下一步 |
|---|---|
400 UNKNOWN_FIELD / INVALID_REQUEST | 对照对应接口字段,不混用 message、cancel、reset 请求体 |
| 401 / 403 | 核对密钥、scope 和应用/设备状态 |
| 404 | 核对真实 ID、应用和 subject;不要因猜到 ID 就认为有权限 |
409 GENERATION_CONFLICT | 读快照,检查代际及服务发布变化 |
409 BUSY | 区分业务键重复、会话 resetting、reset 存在未完成任务 |
410 CURSOR_EXPIRED | 重新加载快照并更新 SSE 游标 |
503 CAPABILITY_UNAVAILABLE | 检查接入是否暂停、Gateway 和设备是否可用 |
下一步#
配置应用密钥与 scope,学习Webhook 和回调,或部署远端网关。