> 内容来源：Onevium 官方文档
> 文章: 业务会话：续聊、历史、事件与停止重开
> 原文: https://onevium.com/zh/docs/developers/conversations
> 语言：简体中文
> 更新于: 2026-09-09
> 适用版本: 1.1.23+

---

# 业务会话：续聊、历史、事件与停止重开

用真实 Gateway 路由创建与延续会话，读取历史和 SSE，并以 generation、幂等键和控制记录管理停止与重开。

## 会话、任务和控制操作

先按[外部接入总览](https://onevium.com/zh/docs/developers/external-access)发布服务并启用接入，再从业务后端调用这里的 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`，不要重复。

```bash
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 和业务用户主体的处理见[鉴权与权限](https://onevium.com/zh/docs/developers/authentication)。本地地址使用 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`；应保存并继续原会话。

```bash
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')
```

只在请求成功并取得两个值后继续。**示例响应：**

```json
{
  "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. 发送第一条消息

```bash
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 示例接纳响应：**

```json
{
  "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. 查询任务状态与结果

```bash
curl --fail-with-body -sS "$GATEWAY_URL/api/v1/runs/$RUN_ID" \
  -H "Authorization: Bearer $APP_KEY" \
  -H "X-Onevium-Subject: $SUBJECT" | jq .
```

**成功终态的示例响应：**

```json
{
  "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. 继续同一会话

先确认前一轮结果，再读当前快照取得真实代际：

```bash
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 续传，不用于历史翻页。

```bash
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 |

当前页非空时，可以继续读取前一页：

```bash
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 .
```

**历史响应示例（这里仅有一条消息）：**

```json
{
  "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 的会话，不能假定只包含该服务。

```bash
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` 订阅，避免把旧内容重复追加：

```bash
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'
```

**示例事件，不是实测输出：**

```text
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` 执行：

```bash
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 控制响应示例：**

```json
{
  "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。没有未完成任务时：

```bash
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 .
```

若你明确要停止未完成工作再重开，应作为另一控制操作，用新键发送：

```bash
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`；**完成响应示例：**

```json
{
  "operation_id": "op_0123456789abcdef_0000000000000006",
  "state": "succeeded",
  "ref": { "conversation_id": "conv_0123456789abcdef_0000000000000001", "generation": 2 }
}
```

完成后使用 `ref.generation` 或最新快照中的代际发送下一条消息，旧代际会得到 409。Reset 还会采用服务当前发布版本；服务重新发布后出现版本冲突时，需要检查是否应重开。历史消息并不因 reset 立即删除，可用 `generation=1` 查询旧代际的保留记录。

确认 operation 已成功后，再更新本地代际变量：

```bash
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](https://onevium.com/zh/docs/developers/authentication)，学习[Webhook 和回调](https://onevium.com/zh/docs/developers/webhooks)，或部署[远端网关](https://onevium.com/zh/docs/developers/remote-gateway)。
