> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ycloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 外部 Agent

> 将您自有的 AI Agent 接入 YCloud 收件箱，以接收客户消息、发送回复并将对话转接给您的团队。

<Note>
  外部 Agent 适用于 **YCloud Pro 及以上套餐**。
</Note>

将您自有的 AI Agent 作为 **External Agent** 接入 YCloud。您的 Agent 运行在您自己的基础设施中，并在收件箱中与人工客服协同工作：它可以接收客户消息、发送回复、获取对话历史记录并将对话转接给您的团队。

YCloud 会向您的 Webhook URL 发送事件。您的服务处理这些事件并调用收件箱 REST API 进行回复或管理对话。YCloud 不会调用您的 AI 推理 API，且您的 Webhook 返回的 HTTP 响应不会发送给客户。

## 准备工作

请准备：

* 一个公网可访问、支持接收 JSON `POST` 请求的 HTTPS 端点。
* 一个能够验证 Webhook 签名、存储事件、防止重复处理并运行 AI Agent 的服务。
* 存储在您服务器上的普通 YCloud 账户 API 密钥。这些收件箱端点不支持开发者应用 API 密钥。
* 已接入 YCloud 的 WhatsApp 商业号码，以及用于接收转接的人工团队或客服人员。

使用 `https://api.ycloud.com` 作为 API 基础 URL。

## 连接您的 Agent

### 1. 创建外部 Agent

1. 在 YCloud 控制台中，打开 **AI Agent** > **AI Agent**。
2. 选择 **Create Agent** > **External Agent**。
3. 输入 **Name** 以及您的 **Webhook URL**，例如 `https://agent.example.com/webhooks/ycloud`。
4. 选择 **Save**。
5. 从 Agent 详情中复制生成的 **Bot ID** 。在 API 请求中将该值用作 `botId`；您无需提供来自您自有 AI 系统的 ID。

<Frame caption="Create an External Agent with a name and a webhook URL. This example shows the form before saving.">
  <img src="https://mintcdn.com/lchnan/gMy2vRUEvLE3uccm/product-assets/external-agent-2026-10-20/create-external-agent.png?fit=max&auto=format&n=gMy2vRUEvLE3uccm&q=85&s=b9098d53ba0ff38823991500a9fb40b2" alt="包含 Name、Webhook URL 和 Save 控件的 YCloud 创建外部 Agent 表单。" width="3024" height="1656" data-path="product-assets/external-agent-2026-10-20/create-external-agent.png" />
</Frame>

详情页面显示了 Webhook 端点、其 **Signing secret** 以及四种事件类型。使用该签名密钥验证接收到的事件。选择 **View delivery logs** 可查看 Webhook 推送详情。同一个端点也会在 **Webhooks** 页面中提供。

请将端点签名密钥与您的 API 密钥分开保存。签名密钥用于验证传入的 Webhook；`X-API-Key` 用于验证您发出的 REST 请求。Bot ID 是标识符，而非凭据。

### 2. 分配 WhatsApp 号码

打开 **WhatsApp Manager** > **WhatsApp accounts**，然后打开该商业号码的设置并选择 **Inbox** > **Assignment**。在 **Priority 3 Assign to** 下，选择 **AI Agent**，然后在 Agent 选择器的 **External Agent** 区域选择您的 Agent。

检查规则优先级：分配给客户负责人或最后接待的客服人员可能会优先执行。参见[对话分配规则](/zh/documentation/channels/whatsapp-accounts-management/phone-number-management/conversation-assignment-rules)。

一个外部 Agent 可以处理多个号码。每个号码一次只能关联一个机器人。您可以在号码的分配设置中管理这些关联。

### 3. 处理对话

1. 客户向您的商业号码发送 WhatsApp 消息。
2. YCloud 会打开或继续一个收件箱对话，并应用该号码的分配规则。
3. 当您的外部 Agent 接管对话时，YCloud 会将相应的 Webhook 事件推送到其端点。
4. 您的服务验证并存储该事件，返回 `2xx`，然后异步处理客户输入。
5. 您的服务调用收件箱 API 来回复、转接或关闭对话。
6. 您的服务通过 `whatsapp.message.updated` 跟踪送达情况。

## 了解各类标识符

| 标识符 | 用途 |
| - | - |
| `botId` | YCloud 生成的外部 Agent ID。 |
| `phoneNumberId` | YCloud WhatsApp 商业号码 ID。 |
| `conversationId` | 客户与商业号码之间持续的关系标识。 |
| `dialogId` | 单次服务会话，从发起到关闭。转接会保留此 ID；关闭后有效的新入站消息将发起新对话。 |
| Webhook `id` | 用于去重的不透明事件 ID。它不代表事件发生顺序。 |
| `inboxMessage.id` / `inboxMessageId` | 收件箱消息 ID，用于回复引用和关联送达状态。 |
| `channelMessageId` / `wamid` | 渠道及 WhatsApp 消息标识符。请将其与收件箱 ID 分开存储。 |

ID 均为字符串。时间戳采用 UTC RFC 3339 格式。在对话 API 路径中使用 `dialogId`，而非 `conversationId`。

## 接收 Webhook 事件

所有四种事件类型都会发送到 **同一个 Webhook URL**。每次专用的 External Agent 推送都包含 `id`、`type`、`apiVersion`、`createTime` 和 `externalBotContext`，以及相应的业务对象。

| 事件 | 业务对象 | 您的服务应执行的操作 |
| - | - | - |
| `inbox.message.created` | `inboxMessage` | 仅当 `direction=inbound` 且 `actionable=true` 时才处理客户输入。同步出站消息，而不触发另一次 AI 回复。 |
| `inbox.conversation.assigned` | `inboxConversation` | 跟踪分配变更。当您的 Agent 转接时停止处理；当接管现有对话时检索历史记录。 |
| `inbox.conversation.status_updated` | `inboxConversation` | 跟踪对话状态并在其关闭时停止挂起的工作。 |
| `whatsapp.message.updated` | `whatsappMessage` | 跟踪您的 Agent 所发送消息的送达状态或失败情况。 |

### 验证、存储和确认

在解析 JSON 之前，使用端点的签名密钥和 **原始 HTTP 请求体** 验证 `YCloud-Signature` 标头。签名格式为：

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
YCloud-Signature: t=UNIX_TIMESTAMP,s=SIGNATURE_HEX
signed_payload = timestamp + "." + raw_request_body
signature = hex(HMAC-SHA256(endpoint_secret, signed_payload))
```

使用恒定时间比较并在合理的时间容差（例如五分钟）内验证时间戳。有关签名验证示例，请参阅[实现 Webhook 接收器](/zh/api-reference/guides/api-fundamentals/implement-a-webhook-receiver)。

持久化事件及其去重状态，然后迅速返回任意成功的 `2xx` 响应。异步运行 AI 推理和其他耗时任务。非 `2xx` 响应和超时可能会触发有限次数的重试。

在您的 YCloud 账户内按 **`(botId, event.id)`** 去重。分配变更可能会同时通知迁出和迁入的 Agent；仅使用全局事件 ID 可能会丢弃其中一个 Agent 的通知。保留处理状态，避免重复事件触发第二次回复。

### 示例：客户消息

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "evt_1001",
  "type": "inbox.message.created",
  "apiVersion": "v2",
  "createTime": "2026-10-20T08:00:00Z",
  "externalBotContext": {
    "botId": "bot_1001",
    "phoneNumberId": "phone_1001",
    "conversationId": "9001",
    "dialogId": "9002",
    "status": "open",
    "assignee": { "type": "external_bot", "id": "bot_1001" },
    "actionable": true
  },
  "inboxMessage": {
    "id": "9003",
    "direction": "inbound",
    "inbound": {
      "id": "wa_in_1001",
      "wamid": "wamid.DEMO_IN_1001",
      "type": "text",
      "sendTime": "2026-10-20T08:00:00Z",
      "text": { "body": "How can I change my booking?" }
    },
    "conversation": { "id": "9001", "dialog": { "id": "9002" } }
  }
}
```

从 `inboxMessage.inbound` 中读取客户内容。出站创建事件则使用 `inboxMessage.outbound`。保留未知的消息类型和字段以保持兼容性，而不是将其视为文本。

### 决定是否回复

`externalBotContext` 是生成通知时创建的快照。其 `botId` 标识接收 Bot；`assignee` 标识该快照中的实际处理者。转接后这两者可能会有所不同。

`actionable=true` 表示接收 Bot 当时正在处理一个进行中的对话。在出站创建事件或送达回执中它也可以为 `true`。

<Warning>
  不要仅凭 `actionable` 触发 AI 回复。检查事件类型和消息方向。延迟到达的事件不能证明您的 Agent 仍在处理该对话。需要时请查询当前对话，并处理来自每个写入请求的归属权错误。
</Warning>

事件可能会多次到达或无序到达。没有事件序号字段。按 `dialogId` 对任务分组，避免对同一客户输入处理两次。初始分配事件不应独立触发对客户起始消息的第二次回复。

### 转接或关闭后会发生什么？

一旦对话转由人工处理、处于排队中、未分配或已关闭，新的客户消息和人工回复将不再投递给先前的 External Agent。

先前的 Agent 仍会收到适用于它的迁出分配或关闭通知。它已发送消息的创建通知和送达回执即使在转接或号码重新分配后，也仍与该发送 Agent 保持关联。

先前生成的通知可能会延迟到达。当归属权变更或对话关闭时，请取消挂起的工作，不要仅因为旧事件带有 `actionable=true` 就恢复该工作。

普通 Webhook 端点保留其现有的订阅行为和 Payload 格式。专用的 External Agent 推送会添加 `externalBotContext`；不要在普通 `whatsapp.message.updated` 推送中要求该字段。

## 使用 Inbox API

在每个请求中发送 `X-API-Key`，并在 `POST` 请求中发送 `Content-Type: application/json`。以下所有示例均使用占位符 ID。请将其替换为来自您的 Agent 和所接收事件的值。

| 操作 | 方法和路径 |
| - | - |
| 获取当前对话 | `GET /v2/inbox/dialogs/{dialogId}` |
| 获取会话历史记录 | `GET /v2/inbox/dialogs/{dialogId}/messages` |
| 列出转接目标 | `GET /v2/inbox/handoffTargets` |
| 发送回复 | `POST /v2/inbox/dialogs/{dialogId}/messages` |
| 转接 | `POST /v2/inbox/dialogs/{dialogId}/handoff` |
| 关闭对话 | `POST /v2/inbox/dialogs/{dialogId}/close` |

成功的响应会直接返回业务对象，而不包含 `data` 包装。写入操作会检查对话是否处于打开状态并且当前正由指定的 Bot 处理。YCloud 会从对话中确定发送方和客户；请勿提交 `tenantId`、`from`、`to`、`recipient`、`operatorId` 或 `source` 来覆盖它们。

### 获取当前对话

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl 'https://api.ycloud.com/v2/inbox/dialogs/9002?botId=bot_1001' \
  --header "X-API-Key: $YCLOUD_API_KEY"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": "9002",
  "conversationId": "9001",
  "phoneNumberId": "phone_1001",
  "status": "open",
  "bot": { "id": "bot_1001", "name": "Booking assistant" },
  "assignee": { "type": "external_bot", "id": "bot_1001" },
  "assignmentOutcome": "assigned",
  "canAct": true
}
```

请检查 `status`、`assignee` 和 `canAct`。`bot` 字段可能指向上次关联的外部 Bot，不能作为当前归属权的证明。`canAct` 反映了请求时被查询 Bot 的权限；后续的写入操作仍会执行自身的检查。

### 检索会话历史记录

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl 'https://api.ycloud.com/v2/inbox/dialogs/9002/messages?botId=bot_1001&limit=50' \
  --header "X-API-Key: $YCLOUD_API_KEY"
```

响应包含 `items` 和 `nextCursor`。每个条目都包含 Inbox 消息 `id`、`direction`、`type`、`content` 和 `createTime`。对于文本消息，请读取 `content.text.body`。

历史记录包含来自 **同一会话**的客户可见消息，可能跨越先前的对话。消息本身的 `dialogId` 可能与所请求的对话不同，或者在较早的历史记录中缺失。内部备注、私密转接摘要和内部系统消息均不包含在内。

返回的消息按消息 ID 升序排列，从较早到较新。首次请求时请省略 `after`；在后续请求中原样传递 `nextCursor` 作为 `after`，直到其为 `null`。默认 `limit` 为 50，范围为 1–100。转接目标列表使用相同的分页模式。

### 发送回复

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --request POST 'https://api.ycloud.com/v2/inbox/dialogs/9002/messages' \
  --header "X-API-Key: $YCLOUD_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "botId": "bot_1001",
    "sourceEventId": "evt_1001",
    "replyToMessageId": "9003",
    "message": {
      "type": "text",
      "text": { "body": "Open your booking details and select Change booking." }
    }
  }'
```

`botId` 和 `message` 为必填项。`sourceEventId` 和 `replyToMessageId` 为选填项。回复引用是 **Inbox 消息 ID**，例如 `inboxMessage.id`；它不是 `wamid`。

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "inboxMessageId": "9004",
  "channelMessageId": "wa_out_1001",
  "status": "accepted"
}
```

即使 HTTP 返回 `200`，也请检查 `status`。`accepted` 仅表示消息已进入发送管道，并不代表已送达客户。`200` 响应也可能报告 `failed`；`channelMessageId` 可能尚不可用。

使用 `whatsapp.message.updated` 跟踪后续状态，包括 `sent`、`delivered`、`read`、`failed`、`expired` 或 `deleted`。将回执的 `externalBotContext.inboxMessageId` 与发送响应的 `inboxMessageId` 进行关联。对于失败情况，请检查 `whatsappMessage.errorCode` 和 `errorMessage`。

#### 消息类型

`message` 对象使用标准 WhatsApp 消息模型：`type` 加上匹配的内容字段。示例包括 `text`、`image`、`video`、`audio`、`document`、`sticker`、`location`、`contacts`、`interactive`、`template` 和 `reaction`。现有的 WhatsApp 发送和模板要求仍然适用。

例如，将 `message` 对象替换为：

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "type": "image",
  "image": {
    "link": "https://cdn.example.com/booking-help.png",
    "caption": "Where to find your booking settings."
  }
}
```

对于媒体，请提供 `id` 或 `link`。有关消息体结构，请参阅[发送 WhatsApp 消息](/zh/api-reference/guides/whatsapp-platform/send-whatsapp-message)。请通过上述 Inbox 端点发送回复，以便使用对话的处理程序检查和 Inbox 上下文。请勿在此 Inbox 消息请求中提交或依赖 `filterUnsubscribed` 或 `filterBlocked`。

客户回复可以包含 `interactive.button_reply`、`interactive.list_reply`、`interactive.nfm_reply` 或 `button`。Flow 回复的 `response_json` 是一个 JSON 字符串。入站 `button` 类型不是此端点的出站消息类型。

### 转接给人工

首先列出对话所属商业号码的可用团队或客服人员：

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl 'https://api.ycloud.com/v2/inbox/handoffTargets?phoneNumberId=phone_1001&type=team&limit=50' \
  --header "X-API-Key: $YCLOUD_API_KEY"
```

使用 `type=agent` 改为列出成员。每个返回的目标都会提供转接请求所需的 `id` 和 `type`。客服人员可选的 `online` 字段描述在线状态；它不保证会立即分配。

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --request POST 'https://api.ycloud.com/v2/inbox/dialogs/9002/handoff' \
  --header "X-API-Key: $YCLOUD_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "botId": "bot_1001",
    "sourceEventId": "evt_1001",
    "target": { "type": "team", "id": "team_1001" },
    "reason": "The customer requested human help.",
    "summary": "The customer needs to change a booking and requires assistance."
  }'
```

您必须指定带有 `type`（`team` 或 `agent`）和 `id` 的 `target`。没有默认的人工后备选项。可选的 `reason` 最多支持 256 个字符；可选的 `summary` 最多支持 4,000 个字符，并且不属于客户可见的消息历史记录。

响应包含 `outcome` 以及更新后的 `dialog`。结果为 `assigned`、`queued`、`team_unassigned` 或 `unassigned`。请检查实际结果，而不要假设人工已经接受了会话。

转接会保留相同的 `dialogId`。一旦您的 Agent 失去归属权，请停止回复或尝试关闭该对话。

### 关闭已解决的对话

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --request POST 'https://api.ycloud.com/v2/inbox/dialogs/9002/close' \
  --header "X-API-Key: $YCLOUD_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{ "botId": "bot_1001", "sourceEventId": "evt_1001" }'
```

响应包含带有 `status=closed` 和 `canAct=false` 的 `dialog`。您的 Agent 无法继续发送或转接该已关闭的对话。后续有效的客户消息将启动新对话，并再次运行该号码的分配规则。

## 处理错误与重试

错误响应使用包含 `status`、`code`、`message` 和 `requestId` 的 `error` 对象。联系 YCloud 支持时请保留请求 ID。

| HTTP 状态 / 示例代码 | 操作 |
| - | - |
| `400 INVALID_ARGUMENT` | 更正请求字段，包括缺失的转接目标。 |
| `401`，或 `403 CREDENTIAL_TYPE_NOT_SUPPORTED` / `BOT_MISMATCH` | 请检查账户 API 密钥、凭据类型、Bot ID 以及租户归属权。 |
| `404 NOT_FOUND` | 请检查 ID 与租户范围。如果转接目标已不可用，请重新列出目标。 |
| `409 NOT_CURRENT_HANDLER` / `DIALOG_CLOSED` | 停止过期的处理工作。重新检查当前对话，而不是重试相同的操作。 |
| `429`、`503` 或网络超时 | 读取操作请进行退避重试。对于写入操作，在重试前请先检查状态、历史记录和回执，因为原始请求可能已经成功。 |

<Warning>
  `sourceEventId` 和 `X-Request-Id` 支持链路追踪；两者均不是幂等性密钥。API 不提供跨请求去重功能。在结果不确定的情况下盲目重试可能会发送重复消息。
</Warning>

监控 **查看送达日志** 以排查 Webhook 失败。保留事件 ID、对话 ID、消息 ID、请求 ID 以及处理结果。Webhook 重试次数是有限的，因此必要时请使用对话和历史记录查询来恢复上下文。

## 常见问题

### 我可以接入使用自己的 AI 框架构建的 Agent 吗？

可以。您的服务必须能够接收并验证 YCloud Webhook，并调用 Inbox REST API。您可以完全控制服务如何生成回复、访问知识库以及调用其自有工具。

### 我的 Agent 会收到所有 Inbox 会话吗？

不会。其专属端点仅接收其处理范围内的事件，以及相关的分配和关闭通知、其自身发送消息的回执。转接人工后的新消息不会转发给之前的 Agent。先前生成的事件可能仍会延迟送达。

### 我的 Agent 可以使用来自先前对话的消息吗？

可以。历史记录端点会返回该会话中客户可见的历史记录，包括先前的对话。如果可用，请使用每条消息各自的 `dialogId`；切勿假定返回的每条消息都属于当前的服务会话。

### 如果我更改分配给号码的 Agent 会发生什么？

当前由机器人处理的打开状态对话会根据该号码的路由行为异步重新分配。迁出的 Agent 会收到 `assignmentRole=outgoing` 并应停止其工作。接入的 Agent 会收到带有 `assignmentRole=incoming` 的 `reason=phone_binding_changed`；在继续之前，它应先查询当前对话和历史记录。

对话 ID 和历史记录保持不变。当前由人工处理的对话不会通过此号码重新分配转移给新的机器人。在被其他机器人替换后，旧机器人无法再通过这些端点查询该对话。

### 我的 Webhook 响应会直接成为客户回复吗？

不会。请返回 `2xx` 以确认收到事件，然后通过 `POST /v2/inbox/dialogs/{dialogId}/messages` 发送客户回复。

### 为什么我的 Agent 会为其自身的回复收到 `actionable=true`？

该字段描述生成事件时由谁处理该打开状态的对话。因此出站创建事件和送达回执也可能带有 `actionable=true`。仅针对入站 `inbox.message.created` 事件触发常规 AI 回复，并注意处理实时对话归属权的变化。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.