> **Base URL:** `https://router.fis.aliyuncs.com/finx`

---

# 介绍

本指南帮助你快速接入点金智能体 Open API — 从开通服务、获取 API Key 到完成第一次 API 调用。

点金智能体 Open API 提供了完整的智能体生命周期管理能力，包括智能体创建与配置、会话管理、流式运行、工具接入、权限管控等。

## 核心能力

| 能力 | 说明 |
| --- | --- |
| 智能体管理 | 创建、配置、调用智能体实例 |
| 会话与对话 | 多轮对话、上下文管理、异步运行 |
| SSE 流式输出 | 实时获取智能体思考过程与结果 |
| 工具接入 | MCP / HTTP 外部服务挂载，扩展智能体能力 |
| 附件处理 | 上传文件并在对话中引用并分析 |
| 权限与身份 | 多租户、多主体隔离，细粒度权限控制 |

## 接入流程总览

> **典型接入流程：** 开通服务 → 创建应用 → 获取 API Key → 创建身份主体 → 发起对话 → 消费 SSE 结果

## 开通服务

使用主账号登录阿里云控制台，依次完成以下两个服务的开通：

| 服务 | 说明 |
| --- | --- |
| 阿里云百炼 | 开通阿里云百炼。[前往开通 →](https://bailian.console.aliyun.com/cn-beijing) |
| 通义点金 | 开通点金。[前往开通 →](https://common-buy.aliyun.com/?commodityCode=sfm_dianjin_public_cn) |

开通完成后，前往[「获取 API Key」](/docs/quickstart/principal.md#apikey)继续创建应用并获取调用凭证。

### API 概览

点金智能体 Open API 采用 **RESTful** 风格设计。接收 JSON 编码的请求体，返回标准 JSON 响应，并使用标准 HTTP 状态码与 Bearer Token 鉴权。

API 以「异步任务 + SSE 事件流」模式驱动智能体运行，原生支持工具调用、多轮对话、权限管控等能力。

**Base URL**

`https://router.fis.aliyuncs.com/finx`

---

## 获取 API Key

3 步在点金租户控制台创建应用并获取 API Key。

> **前置条件：** 已在[「介绍」](/docs/quickstart/intro.md#%E5%BC%80%E9%80%9A%E6%9C%8D%E5%8A%A1)中完成阿里云百炼与通义点金的开通。

### Step 1: 进入租户控制台

进入点金租户控制台，管理你的应用和用户。

[打开租户控制台 →](https://dianjin.biz.aliyun.com/tenant-admin)

### Step 2: 创建应用

在控制台中创建一个应用（App），点击页面右上角的「创建应用」按钮，填写应用名称后点击「创建」。

### Step 3: 创建 API Key

点击应用卡片进入详情，在「API Keys」区域点击「新建」按钮。创建成功后系统将展示 API Key 明文内容。

> **重要**
> 
> API Key 仅在创建时明文展示一次，请务必复制并妥善保存。关闭窗口后将无法再次查看完整密钥。

---

## 创建用户

User 模式下，租户管理员需要为每位真实用户创建账号，用户使用账号密码登录点金工作台。整个流程在租户控制台「用户管理」页面完成。

> **接入流程：** 进入租户控制台 → 用户管理 → 创建用户 → 安全发送登录凭据 → 用户登录工作台

### Step 1: 进入用户管理

登录租户控制台后，在左侧导航选择「用户管理」，即可看到当前租户下的全部用户账号。

[打开租户控制台 →](https://dianjin.biz.aliyun.com/tenant-admin/users)

### Step 2: 点击「创建用户」

在用户管理页面右上角点击「创建用户」按钮，弹出创建表单。

### Step 3: 填写账号信息

表单包含三项内容：

| 字段 | 必填 | 说明 |
| --- | --- | --- |
| 用户名 | 是 | 用户登录工作台使用的账号；系统会自动追加 `@<当前租户名>` 后缀，租户内不可重复 |
| 昵称 | 否 | 显示在工作台顶部的友好名称，留空时回退为用户名 |
| 密码 | 是 | 必须同时满足：长度 ≥ 8、含大小写字母、含数字、含特殊字符、不包含用户名 |

> 不想自己想密码？点击密码字段右上角的「自动生成」按钮，系统会随机生成一个满足全部规则的强密码并自动填入「确认密码」。

### Step 4: 复制并发送登录凭据

点击「创建」后系统会弹出登录信息卡片，一次性展示完整的用户名、密码与登录地址。

> **重要**
> 
> 密码仅在创建成功的弹窗中明文展示一次，关闭后无法再次查看完整密钥。请通过 IM 等安全渠道把凭据发送给本人，切勿截图发到公开群组。 若密码遗失，可在用户卡片菜单选择「重置密码」重新设置。

### Step 5: 登录点金工作台

收到凭据的用户访问以下地址，输入用户名（含 @租户名 后缀）与密码即可登录。

| 项 | 值 |
| --- | --- |
| 登录地址 | [https://dianjin.fis.aliyuncs.com/finx/chat](https://dianjin.fis.aliyuncs.com/finx/chat) |
| 账号格式 | `<用户名>@<租户名>`，如 `alice@demo` |
| 首次登录 | 建议提示用户尽快通过工作台的「修改密码」入口更换为自己熟悉的密码 |

> 💡 **提示：** 如需管理用户账号，请前往[用户管理页面](https://dianjin.biz.aliyun.com/tenant-admin/users)（注意：这里的用户并非阿里云账号，由您按实际需求创建）。用户收到凭据后，访问[登录页面](https://dianjin.fis.aliyuncs.com/finx/login)即可进入智能工作台。

---

# 响应码

点金 Open API 使用标准 HTTP 状态码指示请求成功或失败。

`2xx` 表示请求成功；`4xx` 表示客户端错误；`5xx` 表示服务端异常（极少发生）。

## 响应结构

所有 API 响应遵循统一的 JSON 结构：

```json
{
  "httpStatusCode": 200,
  "message": "success",
  "data": { },
  "success": true
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `httpStatusCode` | Integer | HTTP 状态码，与响应头一致 |
| `message` | String | 便于阅读的响应描述 |
| `data` | Object | null | 业务数据，结构因接口而异 |
| `success` | Boolean | `true` 表示请求成功，`false` 表示失败 |

## 错误响应属性

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `code` | String | 业务错误码，用于程序化处理 |
| `message` | String | 错误描述信息 |
| `suggestion` | String | 推荐的处理方式 |

### HTTP 状态码参考

- **200** 成功 — 请求正常处理并返回结果

- **302** 临时重定向 — 附件预览接口，重定向到 OSS 预签名下载地址

- **400** 请求参数错误 — 参数校验失败、类型不匹配、请求体缺失或 JSON 格式错误

- **401** 未授权 — 缺少应用标识、API Key 无效或缺失

- **403** 禁止访问 — 应用不存在或已被禁用、无渠道访问权限

- **404** 资源不存在 — 请求路径无匹配 Handler、资源不存在

- **409** 资源冲突 — 资源已存在，重复创建

- **429** 请求过于频繁 — 触发限流，附带 Retry-After 响应头

- **500** 服务器内部错误 — 未捕获异常、业务逻辑内部错误

- **502** 上游服务错误 — 上游服务端返回 5xx 错误

- **503** 服务不可用 — DNS 解析失败、网络超时等

---

# 数据模型

点金智能体 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\"}"
}
```

---

# 身份主体与用户

点金提供两种接入模式：App 模式（API Key）与 User 模式（智能工作台），适用于不同场景。

## 两种接入模式 

用一句话理解：**User 是"用我们产品的人"，App 是"调我们接口的系统"。**

## App 模式（API Key 接入）

开发者 / ISV 希望将 AI Agent 的能力集成到**自己的系统**中。

| 维度 | 说明 |
| --- | --- |
| 谁在用 | 开发者编写的程序 |
| 接入方式 | 通过 API Key 调用 OpenAPI |
| 身份管理 | 开发者自行创建和维护「身份主体」（Principal），每次 API 调用时指定 |
| 数据隔离 | 按 Principal 隔离，开发者灵活控制 |
| 前端界面 | 客户自己开发 |
| 典型场景 | 客户的 CRM 系统通过 API 接入 Agent 能力，CRM 中每个用户对应一个 Principal |

> App 模式（API Key） 架构示意图（请访问网页版查看完整插图）

### 身份主体（Principal）

在 App 模式下，开发者需要理解一个核心概念：**Principal（身份主体）**。

| 维度 | 说明 |
| --- | --- |
| 职责划分 | **Agent** 决定"用哪个 AI"，**Principal** 决定"数据归谁" |
| 数据隔离 | 每个 Principal 拥有独立的对话历史、附件、权限 |
| 映射关系 | 开发者需自行维护「自己系统用户 ↔ Principal ID」的映射 |
| 创建方式 | `POST /api/principal/create` → 得到 `principalId` |
| 调用方式 | 每次 API 调用时通过 `X-Principal-Id` Header 传入 |

## User 模式（智能工作台访问）

租户希望让自己的员工 / 客户直接使用我们提供的 AI Agent 产品。

| 维度 | 说明 |
| --- | --- |
| 谁在用 | 真实的人（员工、客户等） |
| 接入方式 | 登录智能工作台页面，或通过 IM 渠道（钉钉、企微、飞书）直接使用 |
| 身份管理 | 平台自动管理，用户无感 |
| 数据隔离 | 天然按用户隔离，每人只能看到自己的对话 |
| 前端界面 | 我们提供 |
| 典型场景 | 企业为内部员工开通 AI 助手账号，员工登录即用 |

> User 模式（智能工作台） 架构示意图（请访问网页版查看完整插图）

## 模式对比

| 对比维度 | App 模式 | User 模式 |
| --- | --- | --- |
| 面向角色 | 开发者 / ISV | 终端用户 |
| 接入方式 | API Key | 账号登录（OIDC） |
| 身份管理 | 开发者自主创建维护 Principal | 平台自动管理 |
| 数据隔离 | 按 Principal 隔离，灵活可控 | 天然按用户隔离 |
| 前端界面 | 客户自己开发 | 平台提供 |

## 选型建议

| 客户需求 | 推荐模式 |
| --- | --- |
| "我想让员工直接用起来，不想开发" | User 模式，开通账号即用 |
| "我想把 AI 能力嵌到自己的产品中" | App 模式，通过 API 集成 |
| "两个都要" | 可以同时使用，互不冲突 |

---

# 通过私网地址访问点金

通过私网地址访问点金OpenAPI，无需公网出口，保留全部 API 路径与鉴权方式，安全可靠。

## 私网端点接入

3 步基于阿里云 PrivateLink 让 VPC 内服务以内网链路调用点金 OpenAPI，无需公网出口、保留全部 API 路径与鉴权方式。

> **前置条件：** 阿里云账号已开通 VPC 与 PrivateLink，目标 VPC 至少包含一个交换机；当前已能通过公网 Base URL `https://router.fis.aliyuncs.com/finx` 成功调用点金 OpenAPI。

### Step 1: 创建接口终端节点

在专有网络控制台创建接口终端节点并关联目标 VPC。

进入[阿里云专有网络控制台 → 终端节点](https://vpc.console.aliyun.com/endpoint/cn-beijing/endpoints)，点击「[创建终端节点](https://vpc.console.aliyun.com/endpoint/cn-beijing/endpoints/new)」。在「终端节点服务名称」字段中，填入以下**全账号通用、固定不变**的服务名：

```
com.aliyuncs.privatelink.cn-beijing.epsrv-2ze7lrpsqbaewls93fty
```

按 VPC 所在地域选择以下两种配置之一，其余字段保持默认。

**同可用区配置（推荐）：** 终端节点与点金服务部署在同一地域，无需承担跨地域流量费用。

| 字段 | 取值 |
| --- | --- |
| 所属地域 | 华北 2（北京） |
| 终端节点类型 | 接口终端节点 |
| 节点名称 | `dianjin-agent-endpoint`（推荐） |
| 类型 | 其他终端节点服务 |
| 服务地域 | **不勾选**「启用跨地域端点」 |
| 网络配置 → 专有网络 | 北京地域下需要打通的 VPC |
| IP 版本 | IPv4 |

**跨地域配置：** 当 VPC 不在北京地域时使用；跨地域流量由[云数据传输 CDT](https://help.aliyun.com/zh/cdt/inter-region-data-transfers) 结算，按量计费。

| 字段 | 取值（以华东 1 杭州为例） |
| --- | --- |
| 所属地域 | 华东 1（杭州） |
| 终端节点类型 | 接口终端节点 |
| 节点名称 | `dianjin-agent-endpoint`（推荐） |
| 类型 | 其他终端节点服务 |
| 服务地域 | **勾选**「启用跨地域端点」并选择「华北 2（北京）」 |
| 跨地域带宽 | 按需配置（最大 1000 Mbps），按 CDT 计费 |
| 网络配置 → 专有网络 | 杭州地域下需要打通的 VPC |
| IP 版本 | IPv4 |

填写完成后点击「确定创建」，等待终端节点状态变为「已连接」。

### Step 2: 获取终端节点域名

复制控制台自动生成的私网域名。

终端节点状态变为「已连接」后，在列表页可以看到自动生成的「终端节点域名」，示例如下：

```
ep-2zeiae8a0597c3a06829.epsrv-2ze7lrpsqbaewls93fty.cn-beijing.privatelink.aliyuncs.com
```

**域名规则：** `<终端节点 ID>.<服务 ID>.<服务地域>.privatelink.aliyuncs.com`。每个账号的终端节点 ID 不同，请使用实际值，**不要照搬示例**。

### Step 3: 替换 Base URL 调用

将公网 Base URL 替换为私网域名，API 路径与鉴权保持不变。

| 类型 | Base URL |
| --- | --- |
| 公网 | `https://router.fis.aliyuncs.com/finx` |
| 私网 | `https://<你的终端节点域名>/finx` |

> 私网域名整体平替 `router.fis.aliyuncs.com`，**`/finx` 前缀仍需保留**，API 路径与鉴权方式与公网完全一致。

```bash
# 私网调用示例
curl -X POST 'https://ep-2zeiae8a0597c3a06829.epsrv-2ze7lrpsqbaewls93fty.cn-beijing.privatelink.aliyuncs.com/finx/api/principal/create' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"principalName":"客服机器人主体"}'
```

> **注意事项**
> 
> - **DNS 解析范围**：终端节点域名仅在已关联的 VPC 内可解析。跨 VPC 访问需先通过 VPC 对等连接或云企业网将网络连通。
> - **HTTPS 证书**：私网域名沿用阿里云 PrivateLink 通配证书，调用方无需特殊处理。
> - **服务名固定**：终端节点服务名称 `com.aliyuncs.privatelink.cn-beijing.epsrv-2ze7lrpsqbaewls93fty` 全账号通用，请勿替换；需要替换的只有自己创建出的「终端节点域名」。
> - **跨地域计费**：勾选「启用跨地域端点」会产生 CDT 流量费用，详见 [CDT 计费详情](https://help.aliyun.com/zh/cdt/inter-region-data-transfers)；同地域无此费用。
> - **公网仍可用**：私网端点上线后公网 Base URL 不会被停用，可按需混用。

### 关键参数速查

**点金 OpenAPI 服务名（固定）**

```
com.aliyuncs.privatelink.cn-beijing.epsrv-2ze7lrpsqbaewls93fty
```

**终端节点域名模板**

```
<endpoint-id>.<service-id>.<region>.privatelink.aliyuncs.com
```

**Base URL 替换前后**

| 类型 | URL |
| --- | --- |
| 公网 | `https://router.fis.aliyuncs.com/finx` |
| 私网 | `https://<endpoint-domain>/finx` |

---

# 最佳实践

典型场景的完整调用链与代码示例，覆盖从身份认证到获取结果的完整链路。每个场景均提供 curl 完整代码，可直接复制运行。

## 场景 01 · 发起智能体对话

从创建身份主体到实时消费智能体输出的端到端流程。

> **调用流程：** 创建身份主体（Principal） → 创建智能体（Agent） → 异步提交对话（获取 requestId） → 订阅 SSE 事件流。

## 前置条件

已开通点金租户并创建应用、已获取 API Key。

```text
Base URL: https://router.fis.aliyuncs.com/finx
```

## 鉴权说明（二要素）

| Header | 说明 | 必须 |
| --- | --- | --- |
| `Authorization: Bearer <TOKEN>` | 访问令牌（API Key） | 是 |
| `X-Principal-Id: <id>` | 身份主体 ID | 运行类接口必须 |
| `Content-Type: application/json` | JSON 请求体 | 视接口 |

### Step 1: 创建身份主体（Principal）

POST /api/principal/create — 为每个最终用户/场景创建独立身份主体

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| principalName | string | 主体名称（必填） |
| principalDescription | string | 主体描述 |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/principal/create' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"principalName": "客服机器人主体"}'
```

### Step 2: 创建智能体（Agent）

POST /api/agent/create — 创建智能体实例，返回 agentId

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentName | string | 智能体名称（必填） |
| agentSoul | string | System Prompt |
| defaultModelName | string | 默认模型 |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/create' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"agentName": "我的智能体"}'
```

### Step 3: 提交对话（异步运行）

POST /api/agent/run/async — 立即返回 requestId，不阻塞

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentId | int64 | 智能体 ID（必填） |
| userInput | string | 发送给智能体的消息（必填） |
| sessionId | int64 | 会话 ID（不传则新建） |
| triggerSource | object | 触发来源，建议 api_call |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/run/async' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"userInput":"你好","triggerSource":{"sourceType":"api_call"}}'
```

### Step 4: 消费 SSE 事件流

GET /api/agent/run/stream?requestId=xxx — 实时读取智能体输出

| 参数 | 说明 |
| --- | --- |
| requestId | 步骤 3 返回的异步请求 ID（必填） |
| lastEventId | 断线重连时传入上次最大事件 ID |

```bash
curl -N -X GET 'https://router.fis.aliyuncs.com/finx/api/agent/run/stream?requestId=<requestId>' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'Accept: text/event-stream'
```

SSE 数据格式：每条事件的 `event` 为 `EventType` 枚举名（如 `TEXT_DELTA`），`data` 为完整的 `AgentEvent` JSON 信封（包含 `eventType`、`data`、`timestamp`、`step`、`totalTokens`）。

```text
id: 0
event: TEXT_DELTA
data: {"eventType":"TEXT_DELTA","data":{"type":"text","text":"你好...","synthetic":false},"timestamp":"2026-05-07T10:30:01","step":1,"totalTokens":256}
```

更多事件类型和数据结构详见[数据模型 -> SSE 事件结构](/docs/quickstart/data-model.md#sse-event-types)。

> **注意事项**
> 
> API 接入必须携带 X-Principal-Id；提交对话后应尽快订阅 SSE；断线用 lastEventId 重连；所有非流式接口先判断 success 再取 data。

> 文件：`agent_conversation.py`

```python
# 公共：Base URL + Token + 鉴权头
import requests

BASE_URL = "https://router.fis.aliyuncs.com/finx"
TOKEN    = "<YOUR_API_KEY>"

def auth_headers(principal_id=None, json_body=True):
    h = {"Authorization": f"Bearer {TOKEN}"}
    if json_body:
        h["Content-Type"] = "application/json"
    if principal_id is not None:
        h["X-Principal-Id"] = str(principal_id)
    return h

def unwrap(resp):
    body = resp.json()
    if not body.get("success"):
        raise RuntimeError(body["message"])
    return body["data"]
```

```python
# 步骤 1：创建身份主体 → principalId
def create_principal(name="客服机器人主体"):
    data = unwrap(requests.post(
        f"{BASE_URL}/api/principal/create",
        headers=auth_headers(),
        json={"principalName": name},
    ))
    return data["principalId"]
```

```python
# 步骤 2：创建智能体 → agentId
def create_agent(principal_id, name="我的智能体"):
    data = unwrap(requests.post(
        f"{BASE_URL}/api/agent/create",
        headers=auth_headers(principal_id),
        json={"agentName": name},
    ))
    return data["agentId"]
```

```python
# 步骤 3：异步提交对话 → requestId
def run_async(principal_id, agent_id, user_input):
    data = unwrap(requests.post(
        f"{BASE_URL}/api/agent/run/async",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "userInput": user_input,
            "triggerSource": {"sourceType": "api_call"},
        },
    ))
    return data["requestId"], data["sessionId"]
```

```python
# 步骤 4：SSE 消费事件流
def stream_events(principal_id, request_id):
    headers = auth_headers(principal_id, json_body=False)
    headers["Accept"] = "text/event-stream"
    with requests.get(
        f"{BASE_URL}/api/agent/run/stream",
        headers=headers,
        params={"requestId": request_id},
        stream=True, timeout=300,
    ) as resp:
        for raw in resp.iter_lines(decode_unicode=True):
            if raw and raw.startswith("data:"):
                print(raw[5:].strip())

if __name__ == "__main__":
    pid = create_principal()
    aid = create_agent(pid)
    req_id, sid = run_async(pid, aid, "你好，请做个自我介绍")
    stream_events(pid, req_id)
```

## 场景 02 · 附件上传与引用

上传文件并在智能体对话中引用，获取处理结果。附件必须归属于会话。

> **配置流程：** 先建会话 → 上传附件 → 带 attachmentIds 提交对话。

### Step 1: 预创建空会话

POST /api/session/create

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentId | int64 | 智能体 ID（必填） |
| title | string | 会话标题（必填） |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/session/create' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"title":"Q3财务分析"}'
```

### Step 2: 上传附件

POST /api/agent/attachment/upload（multipart/form-data）

| 响应字段 | 说明 |
| --- | --- |
| attachmentId | 附件 ID（提交对话时使用） |
| fileName | 原始文件名 |
| fileSize | 文件大小（字节） |
| previewUrl | 预览 URL |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/attachment/upload?sessionId=<sid>' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -F 'file=@/path/to/report.pdf'
```

### Step 3: 带附件提交对话

POST /api/agent/run/async — 与普通对话相比多了 sessionId + attachmentIds

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/run/async' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"userInput":"帮我分析这份财报","sessionId":<sid>,"attachmentIds":[<attId>]}'
```

### Step 4: 消费结果

同「发起对话」的 SSE 消费流程，用 requestId 订阅事件流。

> **注意事项**
> 
> attachmentIds 必须属于本会话且为当前用户上传；Content-Type 使用 multipart/form-data；附件上传前必须先有 sessionId。

> 文件：`attachment_flow.py`

```python
# 步骤 1：预创建会话 → sessionId
def create_session(principal_id, agent_id, title="附件分析"):
    data = unwrap(requests.post(
        f"{BASE_URL}/api/session/create",
        headers=auth_headers(principal_id),
        json={"agentId": agent_id, "title": title},
    ))
    return data["sessionId"]
```

```python
# 步骤 2：上传附件（multipart）→ attachmentId
def upload_attachment(principal_id, session_id, file_path):
    with open(file_path, "rb") as f:
        resp = requests.post(
            f"{BASE_URL}/api/agent/attachment/upload",
            headers=auth_headers(principal_id, json_body=False),
            params={"sessionId": session_id},
            files={"file": f},
        )
    return unwrap(resp)["attachmentId"]
```

```python
# 步骤 3：带附件提交对话
def run_async_with_attachment(
    principal_id, agent_id, sid, att_id, user_input,
):
    data = unwrap(requests.post(
        f"{BASE_URL}/api/agent/run/async",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "userInput": user_input,
            "sessionId": sid,
            "attachmentIds": [att_id],
            "triggerSource": {"sourceType": "api_call"},
        },
    ))
    return data["requestId"]
```

```python
# 步骤 4：复用 stream_events 消费结果
if __name__ == "__main__":
    pid = create_principal()
    aid = create_agent(pid)
    sid = create_session(pid, aid, "财报分析")
    att = upload_attachment(pid, sid, "/path/to/report.pdf")
    req_id = run_async_with_attachment(
        pid, aid, sid, att, "帮我分析这份财报",
    )
    stream_events(pid, req_id)
```

## 场景 03 · 接入外部数据源

为智能体挂载外部服务，扩展调用能力。

> **配置流程：** 创建服务 → （如需鉴权：查 Schema → 存凭证） → 启用服务 → 校验工具列表 → 对话中自动调用

## 路径 A：导入 MCP 服务

### Step 1: 导入 MCP 服务

POST /api/agent/finx/importMcpService — 支持标准 MCP JSON 格式

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentId | int64 | 智能体 ID（必填） |
| mcpServers | object | key=服务名, value=配置（仅 1 个条目） |
| autoGenerateDescription | bool | 自动生成描述 |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/finx/importMcpService' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{
    "agentId": 1001,
    "autoGenerateDescription": true,
    "mcpServers": {
      "my-weather": {
        "type": "sse",
        "url": "https://example.com/mcp/sse"
      }
    }
  }'
```

## 路径 B：创建 HTTP 服务

### Step 1: 创建 HTTP 服务

POST /api/agent/finx/createHttpService

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/finx/createHttpService' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"serviceName":"crm-api","httpConfig":{"baseUrl":"https://crm.example.com/api"}}'
```

### Step 2: 为服务添加工具

POST /api/agent/finx/createTool — 每个工具对应一个 HTTP 端点

| 字段 | 说明 |
| --- | --- |
| toolName | 工具名称（必填） |
| description | 工具描述（模型据此决定何时调用） |
| parameterSchema | 入参 JSON Schema |
| httpEndpoint | HTTP 端点配置（method、path） |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/finx/createTool' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"serviceId":5001,"toolName":"queryCustomer","description":"查询客户详情"}'
```

## 配置鉴权（按需）

### Step 1: 查询授权 Schema

POST /api/agent/finx/getAuthSchema — 获取需要填写的鉴权字段

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentId | int64 | 智能体 ID（必填） |
| serviceName | string | 服务名称（必填） |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/finx/getAuthSchema' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"serviceName":"my-weather"}'
```

### Step 2: 保存授权凭证

POST /api/agent/finx/saveAuthCredential — 提交凭证键值对

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| agentId | int64 | 智能体 ID（必填） |
| serviceName | string | 服务名称（必填） |
| credentials | object | 凭证键值对（必填），例如 `{"apiKey": "xxx"}` |

```bash
curl -X POST 'https://router.fis.aliyuncs.com/finx/api/agent/finx/saveAuthCredential' \
  -H 'Authorization: Bearer <TOKEN>' \
  -H 'X-Principal-Id: <principalId>' \
  -H 'Content-Type: application/json' \
  -d '{"agentId":1001,"serviceName":"my-weather","credentials":{"apiKey":"sk-xxxx"}}'
```

## 启用与校验

| 操作 | 方法 | 路径 |
| --- | --- | --- |
| 启用/禁用服务 | POST | /api/agent/finx/toggleServiceEnabled |
| 查询服务列表 | GET | /api/agent/finx/listServices |
| 查询工具列表 | GET | /api/agent/finx/listTools |
| 调试工具 | POST | /api/agent/finx/execute |

> **调试技巧**
> 
> 使用 POST /api/agent/finx/execute 直接调试工具，命令格式：`finx <服务名> <工具名> [--参数名 值]`

> 文件：`finx_tools.py`

```python
# 配置流程：创建服务 → 鉴权 → 启用 → 校验（MCP 推荐 / HTTP 可选）
# 两种接入方式：MCP（推荐）或 HTTP，均按 agentId 维度配置。
```

```python
# A. 导入 MCP 服务（最简，推荐）
def import_mcp_service(principal_id, agent_id, name, mcp_cfg):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/importMcpService",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "autoGenerateDescription": True,
            "mcpServers": {name: mcp_cfg},
        },
    ))
```

```python
# B1. 创建 HTTP 服务 → serviceId
def create_http_service(principal_id, agent_id, name, http_cfg):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/createHttpService",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "serviceName": name,
            "httpConfig": http_cfg,
        },
    ))
```

```python
# B2. 为 HTTP 服务添加工具 → toolId
def create_tool(
    principal_id, agent_id, sid, name, desc, schema, ep,
):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/createTool",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "serviceId": sid,
            "toolName": name,
            "description": desc,
            "parameterSchema": schema,
            "httpEndpoint": ep,
        },
    ))
```

```python
# 鉴权 1：查询授权 Schema
def get_auth_schema(principal_id, agent_id, service_name):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/getAuthSchema",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "serviceName": service_name,
        },
    ))
```

```python
# 鉴权 2：保存授权凭证
def save_auth_credential(
    principal_id, agent_id, service_name, creds,
):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/saveAuthCredential",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "serviceName": service_name,
            "credentials": creds,
        },
    ))
```

```python
# 启用与校验
def toggle_service(principal_id, agent_id, name, enabled=True):
    return unwrap(requests.post(
        f"{BASE_URL}/api/agent/finx/toggleServiceEnabled",
        headers=auth_headers(principal_id),
        json={
            "agentId": agent_id,
            "serviceName": name,
            "enabled": enabled,
        },
    ))

def list_tools(principal_id, agent_id, name):
    return unwrap(requests.get(
        f"{BASE_URL}/api/agent/finx/listTools",
        headers=auth_headers(principal_id, json_body=False),
        params={"agentId": agent_id, "serviceName": name},
    ))

if __name__ == "__main__":
    pid = create_principal()
    aid = create_agent(pid)
    import_mcp_service(pid, aid, "my-weather", {
        "type": "sse",
        "url": "https://example.com/mcp/sse",
    })
    toggle_service(pid, aid, "my-weather", True)
    print("已加载工具:", list_tools(pid, aid, "my-weather"))
```

---

# 常见问题

关于点金智能体 Open API 的常见问题与解答。

## 环境与部署

**Q: 生产环境的 Base URL 是什么？**

`https://router.fis.aliyuncs.com/finx`

**Q: 接口超时建议设多少？**

普通接口建议 connectTimeout=10s, readTimeout=30s；SSE 流式接口建议 timeout=300s（5 分钟），因为智能体可能长时间思考。

## 认证与鉴权

**Q: 调用运行类接口报「身份相关错误」怎么办？**

API 接入**必须携带 `X-Principal-Id` 请求头**，这是区别于页面访问的关键点。所有运行类接口（创建会话/上传附件/提交对话/订阅事件）都需要此 Header。

**Q: API Key 的格式是什么？如何安全存储？**

格式为 `dk_` 前缀，仅在创建时明文展示一次。建议通过环境变量存储，不要硬编码在代码中。

**Q: Authorization 和 X-Principal-Id 的关系是什么？**

二者缺一不可。`Authorization` 标识应用身份（API Key），`X-Principal-Id` 标识操作主体（为每个最终用户/场景分配独立主体）。

## 身份与访问模式

**Q: App 模式和 User 模式有什么区别？**

User 模式面向终端用户，通过智能工作台或 IM 渠道直接使用，身份由平台自动管理。App 模式面向开发者，通过 API Key 接入，开发者自行创建和维护 Principal（身份主体）来隔离数据。详见 [身份主体与用户](/docs/quickstart/principal.md)。

**Q: App 模式下，我有 1000 个用户，需要创建 1000 个 Principal 吗？**

取决于是否需要对话数据互相隔离。需要隔离就为每个用户创建独立 Principal；不需要隔离则共享一个即可。

**Q: 一个 App 创建了 3 个 Agent，可以服务 1000 个用户吗？**

可以。Agent 决定"用哪个 AI"，Principal 决定"数据归谁"。为用户创建对应的 Principal，每次调用 API 时传入对应 Principal ID 即可。

**Q: 两种模式的 AI Agent 能力有区别吗？**

没有区别。底层调用同一套 Agent 引擎，只是接入方式和身份管理方式不同。

**Q: 通过API调用接入时，开发者需要维护用户 ID 与 Principal ID 的映射关系吗？**

是的。平台不感知开发者系统内部的用户体系，只提供 Principal 作为身份隔离标识。开发者创建 Principal 后，需在自己系统中记录「用户 A ↔ Principal X」的映射关系。

## SSE 事件流

**Q: SSE 事件流的格式是什么？**

标准 SSE 协议，每条事件含 `id`（递增序号）、`event`（事件类型）、`data`（JSON 负载）三部分。完整事件类型与负载结构详见 [SSE 事件结构](/docs/quickstart/data-model.md#data-sse)。

**Q: 如何判断任务结束？**

当本次运行结束（回答完成/失败/终止）时事件流自动关闭。也可调用 `GET /api/agent/run/getTask` 查询最终状态。

**Q: 连接中断如何重连？**

把已收到的最大事件 id 作为 `lastEventId` 参数重新发起请求，服务端从该序号之后续推，保证不丢失、不重复。

## 附件相关

**Q: 上传附件报错「不属于本会话」怎么办？**

`attachmentIds` 必须是当前用户上传、且属于同一 `sessionId` 的附件。推荐流程：先建会话 → 上传附件 → 带 sessionId 提交对话。

**Q: 附件接口的 Content-Type 应该用什么？**

使用 `multipart/form-data`（-F 方式），不要设 application/json。

### 快速导航

- [环境与部署 — Base URL、超时](#faq-deploy)
- [认证与鉴权 — 身份、Key、Header](#faq-auth)
- [身份与访问模式 — App vs User](#faq-principal)
- [SSE 事件流 — 格式、结束、重连](#faq-sse)
- [附件相关 — 归属、Content-Type](#faq-attachment)
