# 数据模型

点金智能体 Open API 的统一响应包装与 SSE 事件协议。

## 响应结构

所有 REST 接口统一使用以下响应包装结构。判断请求是否成功以 `success` 为准，错误重试策略可参考 `httpStatusCode`。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `success` | Boolean | `true` 表示请求成功，`false` 表示失败，是判断请求是否成功的唯一标准。 |
| `httpStatusCode` | Integer | 标准 HTTP 响应码，用于错误类型区分，可作为重试策略判定依据。 |
| `data` | Object | 响应数据对象，类型随接口变化。 |
| `message` | String | 接口返回的提示信息（如有）。 |

## SSE 事件结构

智能体运行接口（`POST /api/agent/run/stream` 与恢复接口 `GET /api/agent/run/stream`）采用标准 SSE 协议推送。每条事件的 `data` 字段为一个 JSON 对象，统一信封结构如下。

### 事件信封结构 

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `eventType` | String | 事件类型枚举值，见下方「事件类型一览」。 |
| `data` | Object | 事件数据载体，不同事件类型携带不同结构（通过 `type` 字段区分）。 |
| `timestamp` | String | 事件产生时间，ISO 格式。 |
| `step` | Integer | 当前执行步骤编号（对应一次 LLM 调用），可为 null。 |
| `totalTokens` | Long | 累积消耗的 Token 总数（实时递增），可为 null。 |

### 事件类型一览 

| 事件类型 | 分类 | 说明 | 是否终止事件 |
| --- | --- | --- | --- |
| `SESSION_CREATED` | 会话 | Agent 开始执行，返回会话标识 | — |
| `TEXT_START` | 文本 | 文本输出片段开始 | — |
| `TEXT_DELTA` | 文本 | 文本增量内容（流式逐字输出） | — |
| `TEXT_END` | 文本 | 文本输出片段结束 | — |
| `REASONING_START` | 推理 | 模型推理过程开始 | — |
| `REASONING_DELTA` | 推理 | 推理增量内容 | — |
| `REASONING_END` | 推理 | 推理过程结束 | — |
| `TOOL_START` | 工具 | 工具调用开始 | — |
| `TOOL_RUNNING` | 工具 | 工具正在执行中 | — |
| `TOOL_COMPLETED` | 工具 | 工具执行成功 | — |
| `TOOL_FAILED` | 工具 | 工具执行失败 | — |
| `TOOL_EVENT` | 工具 | 工具侧信道事件（执行过程中的附加信息） | — |
| `PERMISSION_ASK` | 权限 | 需要用户确认权限（阻塞等待决策） | — |
| `PERMISSION_ASK_RESOLVED` | 权限 | 权限确认结果已返回 | — |
| `MESSAGE_COMPLETED` | 消息 | Agent 执行完成 | 是 |
| `MESSAGE_SUSPENDED` | 消息 | Agent 执行挂起（等待用户操作） | — |
| `MESSAGE_FAILED` | 消息 | Agent 执行失败 | 是 |
| `SUPPLEMENTARY_INPUT_CONSUMED` | 补充输入 | Agent 消费了用户的运行时追加输入 | — |
| `ATTACHMENT_CHANGED` | 附件 | 附件发生变更（新建或更新），前端可实时刷新附件列表 | — |
| `ATTACHMENT_SENT` | 附件 | 附件被主动发送给用户 | — |
| `HEARTBEAT` | 心跳 | 连接保活信号，客户端应忽略 | — |
| `AGENT_STATUS` | 运行期状态 | Agent 运行期瞬时状态信号（如 LLM 限流、网络重试、上下文压缩等） | — |

**终止事件说明：** 收到 `MESSAGE_COMPLETED` 或 `MESSAGE_FAILED` 后 SSE 流将关闭；`MESSAGE_SUSPENDED` 不会关闭流，Agent 恢复后会继续推送事件。

### 各事件数据结构详解 

#### 1. SESSION_CREATED — 会话创建

Agent 开始执行时首先推送此事件，客户端可获取会话和消息标识用于后续操作。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `sessionId` | Long | 会话 ID |
| `userMessageId` | Long | 触发本次执行的用户消息 ID |
| `assistantMessageId` | Long | Agent 响应的助手消息 ID |

#### 2. TEXT_START / TEXT_DELTA / TEXT_END — 文本流式输出

Agent 生成的文本回复通过这三个事件流式推送。典型流程：`TEXT_START` → 多次 `TEXT_DELTA` → `TEXT_END`。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `text` | String | 文本内容。`TEXT_START` 为初始文本，`TEXT_DELTA` 为增量文本，`TEXT_END` 不携带文本字段。 |
| `synthetic` | Boolean | 是否为合成文本（如系统生成的提示信息）。仅 `TEXT_START` 事件携带此字段。 |

**前端处理建议：** 将 `TEXT_START` 和所有 `TEXT_DELTA` 的 `text` 拼接即为完整回复内容。

#### 3. REASONING_START / REASONING_DELTA / REASONING_END — 推理过程

仅在使用支持推理的模型（如 QwQ 等深度思考模型）时产生，展示模型的思考过程。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `text` | String | 推理文本。`REASONING_START` 为初始内容，`REASONING_DELTA` 为增量内容，`REASONING_END` 不携带文本内容。 |

**前端处理建议：** 可折叠展示推理过程，帮助用户理解 Agent 的决策逻辑。

#### 4. TOOL_START — 工具调用开始

Agent 决定调用某个工具时推送，携带工具名称、调用 ID 与参数。

#### 5. TOOL_RUNNING — 工具执行中

工具正在执行的进度通知，可能多次推送。

#### 6. TOOL_COMPLETED — 工具执行完成

工具成功返回结果，`output` 为工具输出的字符串（通常为 JSON）。

#### 7. TOOL_FAILED — 工具执行失败

工具执行异常时推送，`errorMessage` 为错误信息。

**工具事件字段汇总：**

| 字段 | 类型 | 说明 | 出现时机 |
| --- | --- | --- | --- |
| `toolCallId` | String | 工具调用唯一标识 | 所有 TOOL_* 事件 |
| `title` | String | 工具调用的展示标题 | TOOL_RUNNING |
| `toolName` | String | 工具名称 | 所有 TOOL_* 事件 |
| `arguments` | String | 工具调用参数（JSON 格式） | TOOL_START |
| `output` | String | 工具执行输出结果 | TOOL_COMPLETED |
| `errorMessage` | String | 错误信息 | TOOL_FAILED |

#### 8. TOOL_EVENT — 工具侧信道事件

工具执行过程中产生的附加结构化信息，用于前端做差异化展示（如需要用户登录、资源已创建等）。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `toolCallId` | String | 工具调用唯一标识 |
| `toolName` | String | 工具名称 |
| `category` | String | 事件分类标识（如 `auth_required`、`resource_created`） |
| `payload` | Object | 结构化数据，内容随 category 变化 |
| `userMessage` | String | 用户友好的提示信息（可选） |

**前端处理建议：** 根据 `category` 决定展示方式，`payload` 提供所需数据，`userMessage` 可直接展示给用户。

#### 9. PERMISSION_ASK — 权限确认请求

当 Agent 执行的工具触发了权限规则时推送，需要用户确认后才能继续执行。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `askId` | String | 确认请求唯一标识（回复时需携带） |
| `principalId` | Long | 发起请求的用户 ID |
| `toolName` | String | 触发确认的工具名称 |
| `toolCallId` | String | 工具调用 ID |
| `arguments` | String | 工具参数（JSON），供用户了解将要执行的操作 |
| `ruleDescription` | String | 规则描述，告知用户为什么需要确认 |
| `rulePattern` | String | 触发的规则模式 |

**前端处理建议：** 展示确认对话框，显示 `ruleDescription` 和 `arguments`，用户决策后调用权限回复接口。

#### 10. PERMISSION_ASK_RESOLVED — 权限确认结果

用户对权限请求做出决策后（或超时）推送。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `askId` | String | 确认请求唯一标识 |
| `approved` | Boolean | 是否批准（`true` = 批准，`false` = 拒绝） |
| `reason` | String | 结果原因（如「用户已批准」、「用户拒绝」、「等待超时」） |

#### 11. MESSAGE_COMPLETED — 执行完成

Agent 完成所有处理后推送，SSE 流随后关闭。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `messageId` | Long | 消息 ID |

#### 12. MESSAGE_FAILED — 执行失败

Agent 执行过程中发生不可恢复的错误，SSE 流随后关闭。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `messageId` | Long | 消息 ID |
| `errorMessage` | String | 错误信息 |
| `errorCode` | String | 运行错误码（如 `LLM_ERROR`），用于错误分类统计 |
| `recoverable` | Boolean | 是否可从失败点恢复（前端可据此渲染"重试"按钮） |
| `networkError` | Boolean | 是否为网络类错误（DNS / 连接 / 读取超时等瞬态故障） |

#### 13. MESSAGE_SUSPENDED — 执行挂起

Agent 执行被挂起等待外部操作（如权限确认、节点关停等），SSE 流保持打开。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `messageId` | Long | 消息 ID |
| `suspendReason` | Object | 挂起原因，包含 `type` 字段标识挂起类型以及对应上下文数据 |

**各挂起类型的字段详解：**

**PERMISSION_ASK**（等待权限确认）：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | String | 固定为 `PERMISSION_ASK` |
| `askId` | String | 确认请求唯一标识 |
| `toolCallId` | String | 触发确认的工具调用 ID |
| `toolName` | String | 工具名称 |
| `arguments` | String | 工具参数（JSON） |
| `pendingToolCalls` | Array | 同批次尚未执行的后续工具调用 |
| `step` | Integer | 挂起时的步骤编号 |
| `messageId` | Long | 关联的 assistant 消息 ID |

**MANUAL_SUSPEND**（手动挂起）：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | String | 固定为 `MANUAL_SUSPEND` |
| `pendingToolCalls` | Array | 挂起时尚未执行的工具调用 |
| `step` | Integer | 挂起时的步骤编号 |
| `messageId` | Long | 关联的 assistant 消息 ID |

**SHUTDOWN_INTERRUPT**（节点关闭中断）：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | String | 固定为 `SHUTDOWN_INTERRUPT` |
| `pendingToolCalls` | Array | 挂起时尚未执行的工具调用（崩溃恢复时为空） |
| `step` | Integer | 挂起时的步骤编号（`-1` 表示未知） |
| `messageId` | Long | 关联的 assistant 消息 ID |

**ASYNC_TOOL_PENDING**（异步工具待完成）：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `type` | String | 固定为 `ASYNC_TOOL_PENDING` |
| `asyncTaskId` | String | 外部异步任务 ID |
| `toolCallId` | String | 触发异步工具的工具调用 ID |
| `toolName` | String | 工具名称 |
| `arguments` | String | 工具参数（JSON） |
| `pendingToolCalls` | Array | 异步工具之后尚未执行的工具调用 |
| `step` | Integer | 挂起时的步骤编号 |
| `messageId` | Long | 关联的 assistant 消息 ID |
| `submittedAtMs` | Long | 提交时间戳（毫秒） |
| `timeoutMs` | Long | 超时时长（毫秒） |
| `awakeStrategy` | String | 唤醒策略（`POLL` / `NOTIFY`） |
| `toolPartId` | Long | 恢复时定位原 ToolPart 的 partId |

#### 14. SUPPLEMENTARY_INPUT_CONSUMED — 补充输入已消费

Agent 在执行步骤间隙消费了用户追加的输入，开启新的对话轮次。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `inputId` | String | 被消费的补充输入 ID |
| `content` | String | 输入内容（前端展示用） |
| `userMessageId` | Long | 新创建的用户消息 ID |
| `assistantMessageId` | Long | 新创建的助手消息 ID（后续回复归属此消息） |

#### 15. ATTACHMENT_CHANGED / ATTACHMENT_SENT — 附件变更与发送

智能体在执行过程中产生或发送附件时推送，前端可据此实时刷新附件列表。

两者携带相同的数据结构，通过 `eventType` 区分语义：`ATTACHMENT_CHANGED` 表示「文件被转存为会话附件」，`ATTACHMENT_SENT` 表示「附件被主动发送给用户」。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `attachmentId` | Long | 附件 ID |
| `sessionId` | Long | 所属会话 ID |
| `messageId` | Long | 关联消息 ID（可为 null） |
| `fileName` | String | 文件名 |
| `fileExtension` | String | 文件扩展名（如 pdf、xlsx、png），前端可据此选择图标 |
| `contentType` | String | MIME 类型（如 application/pdf），前端可据此决定预览方式 |
| `fileSize` | Long | 文件大小（字节） |
| `virtualPath` | String | 虚拟路径，用于构造下载 URL（`/api/agent/attachment/download?path=...`） |
| `source` | String | 附件来源：`USER_UPLOAD`（用户上传）、`AGENT_GENERATED`（智能体生成） |
| `changeType` | String | 变更类型：`CREATED`（新建）、`UPDATED`（更新） |

**前端处理建议：** 收到此事件后可实时刷新附件列表，无需在消息结束后再调用 `GET /api/agent/attachment/getByMessage` 轮询。

#### 16. HEARTBEAT — 心跳保活

每 15 秒推送一次，用于保持 SSE 连接不被中间代理或防火墙关闭。

**注意：** 心跳事件不携带 SSE ID，不影响断线重连的 `lastEventId` 计算。客户端应直接忽略此事件。

#### 17. AGENT_STATUS — Agent 运行期状态

Agent 执行过程中的瞬时状态信号。该事件不写入消息记录、不回灌 LLM 上下文，任务终态后不可见。前端收到后可展示临时提示（如"模型限流中，正在重试..."），下一条内容事件抵达即隐含恢复。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | String | 状态码枚举，见下方「状态码一览」 |
| `level` | String | 严重等级：`INFO`（信息提示）/ `WARN`（警告，可恢复）/ `ERROR`（错误，未到终态） |
| `message` | String | 用户可读文案，前端可直接展示 |
| `attributes` | Object | 结构化扩展属性，不同 code 携带不同字段（如 `attempt`、`maxRetries`、`backoffMs`） |

**状态码一览：**

| 状态码 | 等级 | 说明 |
| --- | --- | --- |
| `LLM_RATE_LIMITED` | WARN | 模型限流，正在重试中 |
| `LLM_NETWORK_RETRYING` | WARN | 模型网络抖动，正在重连中 |
| `LLM_AUTO_RECOVERING` | WARN | 网络类失败已被拦截，正在从失败点自动恢复 |
| `LLM_EMPTY_RESPONSE_RETRYING` | WARN | 模型返回空回复，正在自动重试 |
| `CONTEXT_COMPACTING` | INFO | 正在对历史上下文做语义压缩 |

**前端处理建议：** 根据 `level` 统一渲染样式（颜色/图标），按 `code` 做差异化处理。状态码采用只追加策略，前端遇到未知 code 时按 `level` 降级展示即可。

### 典型事件流示例 

一次完整的 Agent 执行，事件推送顺序示例：

```text
SESSION_CREATED          → 会话建立
REASONING_START          → 模型开始思考
REASONING_DELTA …        → 思考过程流式输出
REASONING_END            → 思考完成
TOOL_START               → 决定调用工具
TOOL_RUNNING             → 工具执行中
TOOL_COMPLETED           → 工具返回结果
ATTACHMENT_CHANGED       → 文件产出被转存为附件
AGENT_STATUS             → 模型限流，正在重试中（瞬时状态）
TEXT_START               → 开始生成回复（状态隐含恢复）
TEXT_DELTA …             → 回复文本流式输出
ATTACHMENT_SENT          → 附件被主动发送给用户
TEXT_END                 → 文本输出完成
MESSAGE_COMPLETED        → 执行结束，SSE 流关闭
```

### 断线重连机制 

1. 每个非心跳事件携带递增的 SSE `id`（在 SSE 协议层，非 JSON 内部）。
2. 客户端断线后，使用 `lastEventId` 参数重连：`GET /api/agent/run/stream?requestId={requestId}&lastEventId={lastId}`。
3. 服务端从断点之后的事件开始推送，确保不丢失任何事件。
4. 心跳事件不分配 ID，不干扰重连定位。

## Webhook 事件结构

配置 Hook（拦截型 `pre_tool_use` / `user_message` / `pre_scheduled_task`，观察型 `post_tool_use` / `session_start` / `agent_completion` / `post_llm_call` / `post_scheduled_task`）的 `dispatch_type=webhook` 后，系统会以 HTTP POST 调用客户回调地址。本节定义信封字段、HTTP 头、签名算法、各 `hookType` 的 context 子结构，以及拦截型响应格式。

> **调用流程：** 触发点命中 → 构造事件信封（含 `eventId` / `traceId` / 业务 context）→ 计算 HMAC 签名 → POST 客户 URL → 拦截型同步等待响应解析为 Allow/Deny/Modify；观察型异步投递，失败按秒级快档 + 分钟级阶梯退避重试，争取最终送达。

### HTTP 请求规范 

| 项 | 值 |
| --- | --- |
| Method | `POST` |
| Content-Type | `application/json` |
| 超时 | 拦截型默认 3000 ms，观察型默认 5000 ms（可被 `dispatch_config.timeoutMs` 覆盖） |
| 重试 | 拦截型不重试；观察型按退避阶梯重试（详见下方[投递可靠性](#webhook-reliability)），重试期间 `eventId` / `occurredAt` 不变，`dispatchedAt` / `deliveryAttempt` 更新并重新签名 |
| 幂等 | 客户应基于 `eventId` 去重（重试时同一事件 `eventId` 一致） |

### 请求头 

| 头部 | 出现时机 | 说明 |
| --- | --- | --- |
| `X-Request-Id` | 当前调用链有 traceId 时 | 链路追踪 ID，与信封 `traceId` 字段同值 |
| `X-Webhook-Timestamp` | 配置了 `signingSecret` 时 | 签名时间戳（epoch 毫秒），与本次请求体一起参与签名 |
| `X-Webhook-Signature` | 配置了 `signingSecret` 时 | 签名串，格式 `sha256=<hex>` |
| `Authorization` 等 | 配置了 `auth` 时 | 由统一鉴权模型 `FinxHttpAuth` 注入，支持 `NONE` / `API_KEY` / `BEARER` / `BASIC` / `CUSTOM` / `CUSTOM_HTTP_TOKEN` |

### 签名算法 

服务端使用 HMAC-SHA256 对 `timestamp + "." + rawBody` 签名（`rawBody` 是实际发送的字节序列），结果以 `sha256=<lowercase-hex>` 形式置入 `X-Webhook-Signature`。

**客户端验签步骤：**

1. 校验 `X-Webhook-Timestamp` 与本地时钟偏差在合理窗口内（建议 ±5 分钟），防止重放。
2. 用约定的 `signingSecret` 重新计算 HMAC-SHA256，与请求头里的签名做**常时间比较**（如 Java `MessageDigest.isEqual`、Python `hmac.compare_digest`），防止时序攻击。
3. 校验失败一律拒绝处理。

> **签名算法可能演进。** 头部前缀 `sha256=` 用于将来切换算法（如 `sha512=`），客户端应解析前缀而非硬编码算法名。

### dispatch_config 字段详解 

Hook 的投递行为由 `dispatch_config`（JSON）控制，字段如下：

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `url` | String | —（必填） | 回调地址，须以 `http://` 或 `https://` 开头（生产建议 HTTPS） |
| `timeoutMs` | Integer | 拦截型 3000 / 观察型 5000 | 单次请求超时（毫秒） |
| `maxRetries` | Integer | 3 | 仅观察型生效；只截断快档重试次数（0~3 档），分钟级阶梯恒定附加 |
| `signingSecret` | String | 不签名 | HMAC 签名密钥；服务端仅存储不回显，出参中 mask 为 `***` |
| `failureMode` | String | `ALLOW` | 仅拦截型生效；失败时 `ALLOW`（fail-open 放行）或 `DENY`（fail-closed 拒绝） |
| `auth` | Object | 不鉴权 | 统一鉴权模型 `FinxHttpAuth`，类型见下表 |

**auth 鉴权类型一览：**

| 类型 | 注入方式 |
| --- | --- |
| `NONE` | 不附加鉴权头 |
| `API_KEY` | 自定义 Header（默认 `X-API-Key`）+ 凭证值 |
| `BEARER` | `Authorization: Bearer <credential>`（前缀可自定义） |
| `BASIC` | `Authorization: Basic <credential>` |
| `CUSTOM` | 自定义静态请求头集合 |
| `CUSTOM_HTTP_TOKEN` | 按 `tokenFetchConfig` 先调用客户端点换取动态 token 再注入 |

**toolNameMatch 匹配语法**（仅对 `pre_tool_use` / `post_tool_use` 生效，默认 `*`）：

| 写法 | 语义 | 示例 |
| --- | --- | --- |
| `*` 或留空 | 匹配全部工具 | `*` |
| `/正则/` | 斜杠包裹的正则匹配 | `/^transfer.*/` |
| `a|b|c` | 管道分隔枚举，命中其一即匹配 | `transferFunds|payOrder` |
| 其余 | 与工具名精确匹配 | `transferFunds` |

不支持 glob 通配（如 `transfer*` 会被当作精确匹配处理）。

**signingSecret 更新语义**（编辑 Hook 时）：

| 传值 | 行为 |
| --- | --- |
| 不传或传 `***` | 保留原密钥 |
| 传新值 | 替换为新密钥 |
| 传空字符串 `""` | 清除密钥，后续投递不再签名 |

### 投递可靠性 

**拦截型：** 不重试。超时 / 非 2xx / 解析失败直接按 `failureMode` 降级。

**观察型重试阶梯：** 首投失败后按以下阶梯退避重投，每档附加 ±25% 随机抖动：

| 阶段 | 间隔 | 说明 |
| --- | --- | --- |
| 快档 | 1.5s → 3s → 6s | 兜网络抖动 / 瞬时 5xx；档数受 `maxRetries` 截断（0~3 档） |
| 分钟级阶梯 | 1m → 3m → 5m → 10m → 20m → 30m | 兜持续故障，恒定附加在快档之后，不受 `maxRetries` 影响 |

默认配置（`maxRetries=3`）下最多 1 次首投 + 9 次重试，总投递窗口约 70 分钟。

**重试条件：** 仅 `429`、`5xx` 与网络类错误（超时 / 连接失败）触发重试；其余 `4xx` 视为客户端明确拒绝，不重试直接丢弃。重试期间 `eventId` / `occurredAt` 不变，`dispatchedAt` / `deliveryAttempt` 每次更新并重新签名。

**熔断与降级：** 平台按回调 URL 维度做健康统计，连续多次「无响应」类故障（超时 / 网络 / 连接错误）会触发短暂熔断，随后以单探针试探恢复；熔断期间拦截型直接按 `failureMode` 短路降级。收到 HTTP 响应（含 4xx / 5xx）视为端点存活，不计入熔断失败。另有拦截型并发上限与观察型在途重试上限等舱壁保护。

> 熔断阈值、窗口时长与并发上限均为**平台内部保护参数，可能随版本调整**，不构成接口契约；客户端只需实现幂等与验签即可。

### 事件信封 

所有 `hookType` 共用同一个顶层信封，业务字段在 `context` 中。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `schemaVersion` | String | 协议版本号，当前固定为 `"1.0"` |
| `eventId` | String | 事件唯一 ID（TSID），重试时不变，客户端据此幂等 |
| `hookType` | String | 触发类型，枚举见下方「hookType 一览」 |
| `hookName` | String | Hook 配置名称（创建时定义） |
| `occurredAt` | Long | 事件发生时间（epoch 毫秒），首次发出时记录，重试不变 |
| `dispatchedAt` | Long | 本次投递时间（epoch 毫秒），每次重试更新 |
| `deliveryAttempt` | Integer | 投递尝试次数，首次为 `1`，重试递增 |
| `traceId` | String | 链路追踪 ID（可空），与请求头 `X-Request-Id` 同值 |
| `sessionId` | Long | 会话 ID（部分 hookType 适用，可空） |
| `principalId` | Long | 主体 ID / 用户标识（部分 hookType 适用，可空） |
| `agentId` | Long | 智能体 ID（部分 hookType 适用，可空） |
| `context` | Object | 业务上下文，结构因 `hookType` 而异 |

### hookType 一览 

| `hookType` | 分类 | 触发时机 | 是否影响执行流 |
| --- | --- | --- | --- |
| `pre_tool_use` | 拦截型 | 工具参数修复后、实际执行前 | Allow / Deny / Modify |
| `user_message` | 拦截型 | Agent 入口处接收到用户输入时 | Allow / Deny / Modify |
| `pre_scheduled_task` | 拦截型 | 定时任务实际执行前 | Allow / Deny / Modify |
| `post_tool_use` | 观察型 | 工具执行完成或失败后 | 仅通知 |
| `session_start` | 观察型 | 会话创建后 | 仅通知 |
| `agent_completion` | 观察型 | Agent 执行完成或失败后 | 仅通知 |
| `post_llm_call` | 观察型 | 每次 LLM 流式调用完成后 | 仅通知 |
| `post_scheduled_task` | 观察型 | 定时任务执行完成或失败后 | 仅通知 |

**拦截型 vs 观察型差异：**

| 维度 | 拦截型 | 观察型 |
| --- | --- | --- |
| 调用方式 | 同步阻塞，等待响应 | 异步 fire-and-forget |
| 响应要求 | 必须 2xx + JSON body（缺字段按 Allow） | 仅要求 2xx，body 忽略 |
| 失败处理 | 按 `dispatch_config.failureMode` 决定 `ALLOW`（默认）或 `DENY`（fail-closed） | 按退避阶梯重试，阶梯耗尽仍失败仅记日志 |
| 重试 | 不重试 | 秒级快档（受 `maxRetries` 截断）+ 分钟级阶梯持续重投，详见[投递可靠性](#webhook-reliability) |

### 各 hookType 的 context 结构 

#### 1. `pre_tool_use` — 工具调用前（拦截型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `toolName` | String | 工具名称 |
| `toolCallId` | String | 工具调用 ID |
| `arguments` | Object | 工具调用参数；LLM 输出为合法 JSON 时是对象 / 数组，否则为原始字符串 |

**Modify 决策时 `modifiedValue` 应为新的 `arguments` JSON 字符串。**

#### 2. `post_tool_use` — 工具调用后（观察型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `toolName` | String | 工具名称 |
| `toolCallId` | String | 工具调用 ID |
| `arguments` | Object | 工具调用参数；合法 JSON 时是对象 / 数组，否则为原始字符串 |
| `output` | Object | 工具执行输出；合法 JSON 时是对象，否则为原始字符串 |
| `succeeded` | Boolean | 是否执行成功 |
| `errorMessage` | String | 错误信息（失败时非空） |
| `durationMs` | Long | 执行耗时（毫秒） |

#### 3. `user_message` — 用户消息（拦截型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `userInput` | String | 用户输入文本 |
| `chatMode` | String | 聊天模式：`single_chat` / `group_chat`（小写） |
| `triggerSource` | Object | 触发来源，含 `sourceType`（小写：`human_message` / `scheduled_task` / `api_call` / `internal`）、`channelType`（如 `im`）、`channelProvider`（`dingtalk` / `wecom` / `wechat` / `feishu` / `qq`）；非渠道消息时 channel 字段为 null |
| `bizKey` | String | 业务键（可选） |
| `userNickname` | String | 用户昵称（可选） |

**Modify 决策时 `modifiedValue` 应为新的 `userInput` 字符串。**

#### 4. `session_start` — 会话开始（观察型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `newSession` | Boolean | 是否为新创建的会话；`false` 表示恢复已有会话 |

会话 / 主体 / 智能体标识请读信封级 `sessionId` / `principalId` / `agentId`。

#### 5. `agent_completion` — Agent 执行完成（观察型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `succeeded` | Boolean | 是否执行成功 |
| `errorMessage` | String | 错误信息（失败时非空） |
| `errorCode` | String | 错误码（失败时非空，用于错误分类统计） |
| `model` | String | 模型名称 |
| `durationMs` | Long | 运行总耗时（毫秒） |
| `totalTokens` | Long | 总 Token 消耗 |
| `stepCount` | Integer | 总步数 |
| `triggerSourceType` | String | 触发来源类型（小写）：`human_message` / `scheduled_task` / `api_call` / `internal` |
| `requestId` | String | 本次 Agent Run 的唯一标识（TSID） |
| `assistantMessageId` | Long | 本轮回答消息 ID |
| `userMessageId` | Long | 触发本轮的用户消息 ID |
| `newSession` | Boolean | 是否为新创建的会话（首轮对话） |
| `agentName` | String | 智能体名称 |
| `channelProvider` | String | 渠道提供商（`dingtalk` / `wecom` / `wechat` / `feishu` / `qq`），仅 `human_message` 触发时有值 |

**判断成功 / 失败：** 以 `succeeded` 字段为准，不要依赖 `errorMessage` 是否为空。

#### 6. `post_llm_call` — LLM 调用后（观察型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `model` | String | 模型名称 |
| `step` | Integer | 当前步数 |
| `durationMs` | Long | LLM 调用耗时（毫秒） |
| `promptTokens` | Long | 输入令牌数 |
| `completionTokens` | Long | 输出令牌数 |
| `totalTokens` | Long | 总令牌数 |
| `cachedTokens` | Long | 缓存命中令牌数 |
| `reasoningTokens` | Long | 推理 / 思考过程令牌数 |

#### 7. `pre_scheduled_task` — 定时任务前（拦截型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | Long | 定时任务 ID |
| `taskName` | String | 任务名称 |

**信封级 `sessionId` 语义：** Pre 阶段运行时会话尚未创建，仅当任务配置了会话复用（`reuseSession=true`）时为任务绑定的会话 ID，否则为 `null`；实际执行会话以 `post_scheduled_task` 为准。

#### 8. `post_scheduled_task` — 定时任务后（观察型）

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | Long | 定时任务 ID |
| `taskName` | String | 任务名称 |
| `succeeded` | Boolean | 是否执行成功 |
| `errorMessage` | String | 错误信息（失败时非空） |
| `errorCode` | String | 错误码（失败时非空） |
| `durationMs` | Long | 任务执行总耗时（毫秒） |
| `requestId` | String | 对应 `agent_run.request_id`；在创建运行记录前提前失败时为 `null`（无对应运行记录） |

**信封级 `sessionId` 语义：** 正常为本次执行实际使用的会话 ID；执行未产生会话（如提前失败）时回退为任务绑定的会话，仍无则为 `null`。

### 拦截型响应格式 

拦截型 Hook 必须返回 HTTP 2xx 与 JSON 响应体；观察型仅需 HTTP 2xx，body 被忽略。

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `decision` | String | 决策枚举：`allow` / `deny` / `modify`（不区分大小写） |
| `reason` | String | 拒绝原因（`decision=deny` 时使用，反馈给 LLM 或用户） |
| `modifiedValue` | String | 修改后的值（`decision=modify` 时使用）；`pre_tool_use` 为新 `arguments` JSON 字符串，`user_message` 为新 `userInput` |

**容错策略：**

- 响应体为空、缺 `decision` 字段、或 `decision` 为未知值 → 按 `allow` 处理。
- `decision=modify` 但缺 `modifiedValue` → 降级为 `allow` 并记录告警日志。
- HTTP 非 2xx、超时、解析失败 → 按 `dispatch_config.failureMode` 处理：`ALLOW`（默认，fail-open）或 `DENY`（fail-closed，适用于审批 / 合规场景）。

### 客户端实现要点 

1. **幂等去重：** 用 `eventId` 做 24 小时窗口去重，重试事件携带相同 `eventId`。
2. **快速 ACK：** 拦截型对延迟敏感，建议在 1 秒内返回响应；耗时业务（如二次 RPC）应改用观察型并异步处理。
3. **签名验证：** 生产环境必须校验 `X-Webhook-Signature`，避免接口被未授权调用伪造事件。
4. **失败模式选型：** 审批 / 合规 / 风控类拦截 Hook 应配置 `failureMode=DENY`；业务降级容忍场景保持默认 `ALLOW`。
5. **观察型不要做关键决策：** 观察型可能因网络问题最终失败，关键状态机不可依赖其投递成功。

> **注意事项**
> 
> - 信封字段未来可能新增（向前兼容），客户端解析时**忽略未知字段**，不要严格 schema 校验。
> - 同一 `eventId` 可能因网络抖动被多次接收，业务侧必须做幂等。
> - `signingSecret` 仅服务端单向存储，出参中会被 mask 为 `"***"`，丢失后须重新生成并下发。
> - `pre_tool_use` 的 `modifiedValue` 必须是合法 JSON 字符串，否则会被工具参数解析阶段拒绝。
> - 拦截型超时时间过短可能导致客户处理逻辑被中断，建议根据实际响应耗时设置 `timeoutMs`。

> 文件：`response.json`

```json
# 统一响应包装
{
  "httpStatusCode": 200,
  "message": "success",
  "data": {},
  "success": true
}
```

> 文件：`sse-events.json`

```json
# 事件信封结构
{
  "eventType": "TEXT_DELTA",
  "data": { "type": "text", "...": "..." },
  "timestamp": "2026-05-07T10:30:00",
  "step": 2,
  "totalTokens": 1520
}
```

```json
# 1. SESSION_CREATED — 会话创建
{
  "eventType": "SESSION_CREATED",
  "data": {
    "type": "session",
    "sessionId": 10001,
    "userMessageId": 20001,
    "assistantMessageId": 20002
  },
  "timestamp": "2026-05-07T10:30:00",
  "step": null,
  "totalTokens": null
}
```

```json
# 2. TEXT_DELTA — 文本流式输出
{
  "eventType": "TEXT_DELTA",
  "data": {
    "type": "text",
    "text": "根据您的需求，我建议",
    "synthetic": false
  },
  "timestamp": "2026-05-07T10:30:01",
  "step": 1,
  "totalTokens": 256
}
```

```json
# 3. REASONING_DELTA — 推理过程
{
  "eventType": "REASONING_DELTA",
  "data": {
    "type": "reasoning",
    "text": "让我先分析一下用户的问题…"
  },
  "timestamp": "2026-05-07T10:30:00",
  "step": 1,
  "totalTokens": 128
}
```

```json
# 4. TOOL_START — 工具调用开始
{
  "eventType": "TOOL_START",
  "data": {
    "type": "tool",
    "toolCallId": "call_abc123",
    "title": "查询订单信息",
    "toolName": "queryOrder",
    "arguments": "{\"orderId\":\"ORD-2026001\"}"
  },
  "timestamp": "2026-05-07T10:30:02",
  "step": 1,
  "totalTokens": 512
}
```

```json
# 5. TOOL_RUNNING — 工具执行中
{
  "eventType": "TOOL_RUNNING",
  "data": {
    "type": "tool",
    "toolCallId": "call_abc123",
    "title": "查询订单信息",
    "toolName": "queryOrder"
  }
}
```

```json
# 6. TOOL_COMPLETED — 工具执行完成
{
  "eventType": "TOOL_COMPLETED",
  "data": {
    "type": "tool",
    "toolCallId": "call_abc123",
    "title": "查询订单信息",
    "toolName": "queryOrder",
    "output": "{\"status\":\"已发货\"}"
  },
  "timestamp": "2026-05-07T10:30:03",
  "step": 1,
  "totalTokens": 512
}
```

```json
# 7. TOOL_FAILED — 工具执行失败
{
  "eventType": "TOOL_FAILED",
  "data": {
    "type": "tool",
    "toolCallId": "call_abc123",
    "toolName": "queryOrder",
    "errorMessage": "订单不存在"
  },
  "timestamp": "2026-05-07T10:30:03",
  "step": 1,
  "totalTokens": 512
}
```

```json
# 8. TOOL_EVENT — 工具侧信道事件
{
  "eventType": "TOOL_EVENT",
  "data": {
    "type": "tool_side",
    "toolCallId": "call_abc123",
    "toolName": "connectDatabase",
    "category": "auth_required",
    "payload": {
      "authUrl": "https://example.com/oauth",
      "provider": "mysql"
    },
    "userMessage": "需要您授权数据库访问权限"
  }
}
```

```json
# 9. PERMISSION_ASK — 权限确认请求
{
  "eventType": "PERMISSION_ASK",
  "data": {
    "type": "permission_ask",
    "askId": "ask_xyz789",
    "principalId": 1001,
    "toolName": "deleteRecord",
    "toolCallId": "call_def456",
    "arguments": "{\"recordId\":\"REC-001\"}",
    "ruleDescription": "删除操作需要您的确认",
    "rulePattern": "delete*"
  }
}
```

```json
# 10. PERMISSION_ASK_RESOLVED — 权限确认结果
{
  "eventType": "PERMISSION_ASK_RESOLVED",
  "data": {
    "type": "permission_ask_resolved",
    "askId": "ask_xyz789",
    "approved": true,
    "reason": "用户已批准"
  }
}
```

```json
# 11. MESSAGE_COMPLETED — 执行完成（终止）
{
  "eventType": "MESSAGE_COMPLETED",
  "data": {
    "type": "message",
    "messageId": 20002
  },
  "timestamp": "2026-05-07T10:30:10",
  "step": 3,
  "totalTokens": 2048
}
```

```json
# 12. MESSAGE_FAILED — 执行失败（终止）
{
  "eventType": "MESSAGE_FAILED",
  "data": {
    "type": "message",
    "messageId": 20002,
    "errorMessage": "模型调用超时，请稍后重试",
    "errorCode": "LLM_ERROR",
    "recoverable": true,
    "networkError": true
  },
  "timestamp": "2026-05-07T10:30:10",
  "step": 2,
  "totalTokens": 1024
}
```

```json
# 13. MESSAGE_SUSPENDED — 执行挂起（ASYNC_TOOL_PENDING 示例）
{
  "eventType": "MESSAGE_SUSPENDED",
  "data": {
    "type": "message",
    "messageId": 20002,
    "suspendReason": {
      "type": "ASYNC_TOOL_PENDING",
      "asyncTaskId": "task_456",
      "toolCallId": "call_def456",
      "toolName": "generateReport",
      "arguments": "{\"type\":\"财务分析\"}",
      "pendingToolCalls": [],
      "step": 2,
      "messageId": 20002,
      "submittedAtMs": 1749300000000,
      "timeoutMs": 300000,
      "awakeStrategy": "POLL",
      "toolPartId": 105
    }
  },
  "timestamp": "2026-05-07T10:30:20",
  "step": 2,
  "totalTokens": 1024
}
```

```json
# 14. SUPPLEMENTARY_INPUT_CONSUMED — 补充输入已消费
{
  "eventType": "SUPPLEMENTARY_INPUT_CONSUMED",
  "data": {
    "type": "supplementary_input",
    "inputId": "input_001",
    "content": "请额外关注退款状态",
    "userMessageId": 20003,
    "assistantMessageId": 20004
  }
}
```

```json
# 15. ATTACHMENT_CHANGED — 附件变更
{
  "eventType": "ATTACHMENT_CHANGED",
  "data": {
    "type": "attachment",
    "attachmentId": 30001,
    "sessionId": 10001,
    "messageId": 20002,
    "fileName": "Q3财务分析报告.pdf",
    "fileExtension": "pdf",
    "contentType": "application/pdf",
    "fileSize": 245760,
    "virtualPath": "/attachments/2026/06/30001_Q3_report.pdf",
    "source": "AGENT_GENERATED",
    "changeType": "CREATED"
  },
  "timestamp": "2026-05-07T10:30:04",
  "step": 2,
  "totalTokens": 768
}
```

```json
# 16. HEARTBEAT — 心跳保活（每 15 秒）
{
  "eventType": "HEARTBEAT",
  "timestamp": "2026-05-07T10:30:15"
}
```

```json
# 17. AGENT_STATUS — 运行期瞬时状态
{
  "eventType": "AGENT_STATUS",
  "data": {
    "type": "agent_status",
    "code": "LLM_RATE_LIMITED",
    "level": "WARN",
    "message": "模型限流中，正在第 2 次重试...",
    "attributes": {
      "attempt": 2,
      "maxRetries": 5,
      "backoffMs": 3000
    }
  },
  "timestamp": "2026-05-07T10:30:05",
  "step": 2,
  "totalTokens": 768
}
```

> 文件：`webhook-events.json`

```json
# Webhook 事件信封通用结构 + 请求头
// POST /your/webhook/endpoint
// Content-Type: application/json
// X-Request-Id: req-7a8b9c0d1e2f3g4h
// X-Webhook-Timestamp: 1749300000000
// X-Webhook-Signature: sha256=2c1f47e9b6b5a3c8d4f0e9a1b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX1234567",
  "hookType": "pre_tool_use",
  "hookName": "财务审批 Hook",
  "occurredAt": 1749300000000,
  "dispatchedAt": 1749300000000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": { "...": "各 hookType 不同，详见下方" }
}
```

```json
# 1. pre_tool_use — 工具调用前（拦截型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX1234567",
  "hookType": "pre_tool_use",
  "hookName": "财务审批 Hook",
  "occurredAt": 1749300000000,
  "dispatchedAt": 1749300000000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "toolName": "transferFunds",
    "toolCallId": "call_abc123",
    "arguments": { "amount": 50000, "target": "ACC-002" }
  }
}
```

```json
# 2. post_tool_use — 工具调用后（观察型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX1234568",
  "hookType": "post_tool_use",
  "hookName": "工具审计日志",
  "occurredAt": 1749300003000,
  "dispatchedAt": 1749300003000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "toolName": "queryOrder",
    "toolCallId": "call_abc123",
    "arguments": { "orderId": "ORD-2026001" },
    "output": { "status": "已发货" },
    "succeeded": true,
    "errorMessage": null,
    "durationMs": 320
  }
}
```

```json
# 3. user_message — 用户消息（拦截型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX1234569",
  "hookType": "user_message",
  "hookName": "输入内容安全审核",
  "occurredAt": 1749300010000,
  "dispatchedAt": 1749300010000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "userInput": "帮我查一下 Q3 财报",
    "chatMode": "single_chat",
    "triggerSource": {
      "sourceType": "human_message",
      "channelType": "im",
      "channelProvider": "dingtalk"
    },
    "bizKey": "order-2026-001",
    "userNickname": "张三"
  }
}
```

```json
# 4. session_start — 会话开始（观察型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX123456A",
  "hookType": "session_start",
  "hookName": "会话追踪",
  "occurredAt": 1749300011000,
  "dispatchedAt": 1749300011000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "newSession": true
  }
}
```

```json
# 5. agent_completion — Agent 执行完成（观察型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX123456B",
  "hookType": "agent_completion",
  "hookName": "执行结果统计",
  "occurredAt": 1749300020000,
  "dispatchedAt": 1749300020000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "succeeded": true,
    "errorMessage": null,
    "errorCode": null,
    "model": "qwen-max",
    "durationMs": 8420,
    "totalTokens": 2048,
    "stepCount": 3,
    "triggerSourceType": "human_message",
    "requestId": "0E7G6R01AB234567",
    "assistantMessageId": 20003,
    "userMessageId": 20002,
    "newSession": true,
    "agentName": "财报助手",
    "channelProvider": "dingtalk"
  }
}
```

```json
# 6. post_llm_call — LLM 调用后（观察型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX123456C",
  "hookType": "post_llm_call",
  "hookName": "LLM 调用度量",
  "occurredAt": 1749300015000,
  "dispatchedAt": 1749300015000,
  "deliveryAttempt": 1,
  "traceId": "req-7a8b9c0d1e2f3g4h",
  "sessionId": 10001,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "model": "qwen-max",
    "step": 2,
    "durationMs": 2150,
    "promptTokens": 850,
    "completionTokens": 320,
    "totalTokens": 1170,
    "cachedTokens": 0,
    "reasoningTokens": 128
  }
}
```

```json
# 7. pre_scheduled_task — 定时任务前（拦截型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX123456D",
  "hookType": "pre_scheduled_task",
  "hookName": "定时任务准入",
  "occurredAt": 1749301800000,
  "dispatchedAt": 1749301800000,
  "deliveryAttempt": 1,
  "traceId": "req-9z8y7x6w5v4u3t2s",
  "sessionId": null,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "taskId": 9001,
    "taskName": "每日财报生成"
  }
}
```

```json
# 8. post_scheduled_task — 定时任务后（观察型）
{
  "schemaVersion": "1.0",
  "eventId": "0E7G6QYZX123456E",
  "hookType": "post_scheduled_task",
  "hookName": "定时任务结果统计",
  "occurredAt": 1749301860000,
  "dispatchedAt": 1749301860000,
  "deliveryAttempt": 1,
  "traceId": "req-9z8y7x6w5v4u3t2s",
  "sessionId": 10002,
  "principalId": 1001,
  "agentId": 5001,
  "context": {
    "taskId": 9001,
    "taskName": "每日财报生成",
    "succeeded": true,
    "errorMessage": null,
    "errorCode": null,
    "durationMs": 60000,
    "requestId": "0E7G6R01AB234568"
  }
}
```

```json
# 拦截型响应体（allow / deny / modify 三种）
// allow：放行
{ "decision": "allow" }

// deny：拒绝，reason 将反馈给 LLM 或用户
{
  "decision": "deny",
  "reason": "此转账金额超出授权限额，需人工审批"
}

// modify：修改后放行，modifiedValue 为新参数/输入 JSON 字符串
{
  "decision": "modify",
  "modifiedValue": "{\"amount\":10000,\"target\":\"ACC-002\"}"
}
```
