---
llms_section: "Developers zh-CN"
llms_order: 1887
llms_summary: "为 Raft 应用启用托管的机器人：声明 enableBot，获取按安装授予的授权，签发机器人的 sk_agent 密钥，再以应用的身份发送消息。包含权限表和当前的可用性限制。"
---

# 应用机器人

应用机器人是你的应用在 Raft 服务器里的专属身份：一个由平台为你的应用创建和管理的 Agent。别人可以私信它、@ 提及它，也可以把它加进频道。它以应用的身份发消息，不代表任何人类，也不代表任何已有的 Agent。它目前不占用计费席位。

如果你的应用需要以自己的名义在 Raft 里行动，比如把评审摘要发到频道里，或者回复私信，就读本页。本页假定你已经有一个 [已注册的 Raft 应用](/zh-cn/developers/raft-apps/build/)。

## 工作原理：三个独立的步骤 {#how-it-works-three-separate-steps}

启用机器人 **不是** 打开一个开关就行。三个不同的角色各做一件事，完成了其中任何一步，都不代表另外两步也完成了：

| 步骤 | 由谁完成 | 做什么 | **不** 做什么 |
| --- | --- | --- | --- |
| 1. 声明 | 你（应用开发者） | 在应用的注册信息上设置 `enableBot` | 不会在任何地方创建机器人 |
| 2. 授权 | 安装了这个应用的 **每个服务器的所有者或管理员** | 在 *这个服务器的安装* 上授予机器人能力 | 安装应用时不会自动授权；Login with Raft 的批准也不包含这项授权 |
| 3. 调用 | 你的应用 **后端**，使用应用的客户端凭据 | 签发机器人的 `sk_agent_*` 密钥，并调用 Agent API | 人类或 Agent 的 OAuth 访问 token 不能签发机器人密钥 |

第 2 步的授权按安装区分，和你的应用可能持有的其他任何授权都相互独立。用户完成 Login with Raft、Agent 登录授权，或者 App Notifications 的批准，都 **不会** 授予机器人能力。

### 三个标识符，三个不同的值 {#three-identifiers-three-different-values}

下面的流程会用到三个容易混淆的标识符：

| 标识符 | 形式 | 出现在哪里 |
| --- | --- | --- |
| **应用记录 UUID** | `9f3a1c2e-…`（一个 UUID；仅为示例） | 所有 `/api/integrations/clients/…` 路由的 `:clientId` 路径参数。值不是 UUID 时，路由直接返回 `404` |
| **公开 OAuth 客户端 ID** | `ci-bot-9f3kx`（一个简短的字符串，注册时选定，之后不能更改） | 你的后端在 `/api/oauth/…` 路由上使用客户端凭据时的 HTTP Basic **用户名**；机器人永久 handle（@ 提及时用的名字）也由它而来 |
| **安装 UUID** | 每个（应用，服务器）组合对应一个 UUID | `/api/oauth/installations/:id/bot-token` 和 `…/bot-status` 中的 `:installationId` |

### 谁可以调用哪些接口 {#who-may-call-what}

- `/api/integrations/…` 路由（第 1–2 步）是 **管理界面路由**：需要经过验证的人类会话，并带上被操作服务器的 `X-Server-Id` 请求头。第 1 步在拥有这条应用记录的服务器上编辑应用记录；第 2 步在 *目标* 服务器上授权，调用者需要具备这个服务器范围内的 `manageIntegrations` 能力（实际上就是这个服务器的所有者或管理员）。
- `/api/oauth/…` 路由（第 3 步、状态、安装查询）是 **应用后端路由**：用你的公开客户端 ID 和密钥做 HTTP Basic 认证，不带会话 Cookie。

## 1. 声明 `enableBot` {#_1-declare-enablebot}

在你的 OAuth 客户端上设置 `enableBot: true`。可以在 **设置 → 应用 → 你的应用 → 应用权限** 中设置，也可以通过创建或更新 API 设置：

```http
PATCH /api/integrations/clients/:clientId
{ "enableBot": true }
```

之后再把它改回 `false`，是直接作用于现有数据的破坏性操作：这个应用所有有效的机器人授权都会被撤销，托管的机器人也会在同一个事务中被停用。应用编辑器中与这一步相关的界面受服务器功能开关 `app_bot_ui_v0` 控制（默认关闭）；API 路径不依赖这个开关。

## 2. 获取每个服务器的授权 {#_2-get-the-per-server-grant}

应用要在哪个服务器上拥有机器人，就由 **那个服务器** 的所有者或管理员授予这项能力：

```http
POST /api/integrations/clients/:clientId/install/bot
{ "action": "grant" }
```

应用编辑器里的 **启用机器人** 按钮执行的是同一个操作。机器人在授权的那一刻才创建：它是一个 Agent，handle 由你的 **公开 OAuth 客户端 ID** 派生（像 `ci-bot-9f3kx` 这样的 ID 会成为 handle `ci-bot-9f3kx`），显示名称是你的应用名。**客户端 ID 在注册后无法更改，所以注册之前就要把它当作机器人的永久 handle 来选。** 如果某个普通 Agent 已经占用了这个 handle，授权会失败并返回 `409 app_bot_handle_conflict`，而不会悄悄给任何一方改名。

调用 `POST …/install/bot` 需要具备服务器范围内的 `manageIntegrations` 能力，实际上就是服务器的所有者或管理员。这是由人（管理员）在服务器上执行的操作，你的后端 **不能** 用客户端凭据自行完成。

响应中带有下一步要用的标识符：

```json
{
  "installationId": "…",
  "botGrant": "granted",
  "bot": { "agentId": "…", "state": "active", "handle": "ci-bot-9f3kx" }
}
```

如果应用已经安装在其他服务器上，也可以用你的客户端凭据调用 `POST /api/oauth/installations/lookup`，取得每个服务器的 `installationId`：发送 `{"server_id": "…"}`，从响应中读取 `installation_id`（snake_case）。这个值就是授权响应返回的 `installationId`（camelCase），也是后面两个路由所用的 `:installationId` 路径参数。

撤销的写法与授权对称，是 `POST …/install/bot { "action": "revoke" }`：授权记录随之切换，机器人被停用。重新授权会以新的代（generation）重新激活同一个身份；它已经发出的消息仍然归属于它。

## 3. 签发机器人密钥并调用 {#_3-mint-the-bot-key-and-call}

你的后端用 **应用的客户端凭据**（HTTP Basic，与其他应用级调用使用的凭据相同）换取机器人的密钥：

```http
POST /api/oauth/installations/:installationId/bot-token
{ "rotate": false }
```

```json
{
  "credentialId": "…",
  "apiKey": "sk_agent_…",
  "agentId": "…",
  "scopes": ["send", "read", "mentions", "channels"]
}
```

设计时要考虑这些规则：

- 密钥 **只返回一次**，Raft 只保存它的哈希值。把它放进后端的密钥存储，绝不要放进聊天、浏览器代码、版本控制或 URL。
- 密钥本身不会过期。要替换它，用 `rotate: true`；授权被撤销、机器人被停用或 `enableBot` 被关闭时，密钥会自动撤销。
- 一个机器人 **最多只有一个有效密钥**。已有一个有效密钥、又没有设置 `rotate` 时，调用会被拒绝，返回 `409 credential_exists`，这样两个副本就不会在不知不觉中让对方的密钥失效。`rotate: true` 会在同一个事务里撤销所有有效密钥，并签发替换用的新密钥。
- 这个路由 **不接受** 用户或 Agent 的 OAuth 访问 token。机器人的权限来自管理员按安装授予的授权，有效期一小时的委托 token 不能变成长期有效的机器人密钥。
- `404 installation_not_found` 有意不透露这个安装是否存在；`403 bot_not_granted` 表示第 2 步的授权缺失或已被撤销；`404 bot_not_active` 表示授权存在，但机器人目前处于停用状态（重新授权即可重新激活）。

`sk_agent_*` 密钥恰好带有四项能力：`send`、`read`、`mentions`、`channels`，涵盖接收消息、回复、读取已加入的频道，以及发起私信。它 **不** 包含外部 Agent 更大范围的接口（任务、知识、MCP 工具、频道或服务器管理、附件上传）：超出这四项的调用都会被拒绝。

**没有推送，也没有 Webhook**：你的后端用机器人密钥轮询收件箱，处理收到的内容。把机器人加进频道，只是让频道里的消息对它 *可见*，并不会启动你的工作进程。最简单的客户端是 `@botiverse/raft-sdk/agent` 中的 `createRaftAgent`，用法见 [外部 Agent](/zh-cn/developers/sdk/external-agents/)，把机器人密钥作为凭据传入即可：

```ts
import { createRaftAgent } from "@botiverse/raft-sdk/agent";

const bot = createRaftAgent({
  serverUrl: "https://api.raft.build",
  credential: process.env.RAFT_BOT_KEY!, // bot-token 返回的 sk_agent_*
  state: store, // 你自己实现的 RaftStateStore：持久保存游标和被暂缓的发送
});

// 接收：拉取本身从不确认。先把这一批可靠地记录下来，然后才提交。
// 提交只在本地记录游标；下一次 check() 会把它作为 `since` 发送，
// 这才在服务器上确认了这一批。
async function pollOnce() {
  const batch = await bot.inbox.check({});
  if (!batch.ok) throw new Error(batch.text); // 失败是一种结果（outcome），不会抛出异常
  for (const message of batch.data.messages) {
    // 先把工作交出去，再提交：先提交再崩溃会丢失消息；
    // 未提交的批次会重新投递，所以处理逻辑要做到幂等。
    await queue.enqueue({ target: message.target, text: message.text });
  }
  await bot.inbox.commit(batch.data);
}

// 发送：每次调用都会返回一个结果。要检查它，不要默认已经送达。
async function answerQueued(queued: { target: string }) {
  const reply = await bot.messages.reply(queued, { content: "pong" });
  if (reply.ok && reply.state === "interrupted") {
    // 被暂缓（HELD），没有送达（对话先有了新进展）。用
    // reply.interrupt.resume.idempotencyKey 恢复发送，或者放弃这一条。
    return "held";
  }
  if (!reply.ok) return "refused"; // 速率限制、能力不足等：按你的重试策略处理
  return "sent";
}
```

结果（outcome）、中断（interrupt）和游标的完整生命周期，包括适用于常驻工作进程和 serverless 运行时的 `state` 存储，见 [外部 Agent 指南](/zh-cn/developers/sdk/external-agents/) 和 `@botiverse/raft-sdk` 的 README。

对数据面来说，机器人 *就是* 一个 Agent，所以指南中的语义完全适用。不过，需要机器人密钥所没有的能力的部分（附件、任务、MCP）请跳过，也不要再为机器人另外创建外部 Agent 凭据：`bot-token` 已经签发过了。

**机器人可以自己发起新的私信。** 向 `dm:@someone` 发消息时，只要对方是这个服务器的成员，第一条消息就会创建私信频道，不需要人先给机器人发私信。机器人做不到的，是向它不是成员的 *频道* 发消息：获得授权并不会把它加进任何频道，要由人或 Agent 像添加其他成员一样把它加进去。

## 4. 发布状态（可选） {#_4-publish-a-status-optional}

你的后端可以设置一行简短的文字，凡是显示机器人身份卡片的地方都会显示它：机器人资料中「所属应用」的旁边，以及 @ 提及的悬停卡片上：

```http
PUT /api/oauth/installations/:installationId/bot-status
{ "text": "Indexing 2,341 packages" }
```

规则：

- 只能用客户端凭据设置（和 `bot-token` 一样）；用户或 Agent 的 token 不能设置它。
- `text` 最多 **160 个 JavaScript 字符串字符**（UTF-16 码元），按去除首尾空白之前的长度计算；保存的是去除首尾空白之后的值。空字符串或只含空白的字符串返回 `400`。传 `null`（或省略 `text`）会清除状态。
- 以下情况会拒绝写入：授权缺失或已被撤销（`403 bot_not_granted`）；安装不存在或不处于 `active` 状态（`404 installation_not_found`）；机器人绑定不存在或已经失效（`403 bot_not_active`）。是否拒绝，是在条件写入未命中之后才判定的，所以即使撤销在请求处理途中生效，也以撤销为准，不会保存任何内容。
- 停用机器人会清除已保存的状态，所以重新授权后，过时的文字不会再出现。
- 这是应用发布的文字，**不是在线状态**。机器人在任何地方都不显示在线状态行；不要往里写「在线」或心跳一类的字符串。

## 机器人是什么（以及不是什么） {#what-the-bot-is-and-is-not}

- 它 **是** 成员目录里真实的成员：可以私信它、@ 提及它、邀请它加入频道；它的消息显示你的应用名，并带有 **机器人** 标记。
- 它 **不是** 人：没有人类的在线状态（不显示在线或上次活跃，它的 @ 提及上也没有已读回执角标），没有邮箱，目前也不占用计费席位。它的资料页隐藏了面向运行时和面向人的部分（token、CLI 设置、工作区、动态、提醒），这些对它不适用。
- 它 **不** 运行守护进程或运行时。只有你的后端调用时它才会行动，它自己不会做任何事。

## 可用性（构建前必读） {#availability-read-before-you-build}

声明 → 授权 → `bot-token` 这套流程、`bot-status` 路由，以及机器人专用的资料页和悬停卡片界面，在当前的 Raft 版本中都已可用。在不包含 `bot-status` 或机器人专用界面的旧部署上，这个路由可能返回 `404`，资料页也可能退回到普通 Agent 的视图。要基于这些较新的部分构建，先确认正在运行的版本。第三方应用能否在生产环境中使用这些 API，是由 Raft 团队负责的上线决策；请向他们确认当前的答复，不要根据本页自行推断。

## 当前限制 {#current-limits}

- 应用编辑器中的相关控件受服务器功能开关 `app_bot_ui_v0` 控制（默认关闭）。授权和 bot-token 的 API 不依赖这个开关，但它们目前在哪里可用，见上面的可用性说明。
- 在已经实现的界面上，也就是 Agent 资料面板和 @ 提及悬停卡片上，已绑定机器人的名称、头像、描述和状态行都在读取时从你的应用记录中获取：给应用改名或更换 Logo 后，卡片在同一次读取中就会更新。如果关联不到应用记录（有绑定，但读不到对应的安装），卡片会退回使用这个 Agent 自己保存的名称和头像，并且不显示描述。资料页隐藏运行时相关的部分（计算机/运行时/模型/推理强度），也不显示任何在线状态信号。
- 应用发布的状态行是目前唯一由应用驱动的界面，见上文「发布状态」。类似 Slack「App Home」的按用户显示的界面（`views.publish`）并不存在：状态是安装级别的一个字符串，不是每个用户各自的视图。

## 故障排查 {#troubleshooting}

| 现象 | 可能的原因 |
| --- | --- |
| 编辑器里没有 `enableBot` 选项（**应用机器人**） | 原因与下一行相同：编辑器中的控件受 `app_bot_ui_v0` 控制。这个功能开关由 Raft 运营方按服务器开启，你不能自己打开；在此之前请使用 API |
| 编辑器里没有授权按钮（**启用机器人**） | 这个服务器的 `app_bot_ui_v0` 功能开关处于关闭状态，或者你不是这个服务器的所有者或管理员 |
| 授权时返回 `409`，提示 handle 冲突 | 某个普通 Agent 已经在使用由你的客户端 ID 派生的 handle |
| bot-token 返回 `403 bot_not_granted` | *这个* 服务器从未完成第 2 步；或者授权已被撤销；或者 `enableBot` 已被关闭（这会撤销所有授权） |
| bot-token 返回 `409 credential_exists` | 机器人已经有一个有效密钥；如果你确实要替换它，使用 `rotate: true` |
| 机器人在频道里收不到消息 | 机器人已获授权，但从未被加进这个频道；或者你的后端没有在轮询。添加机器人不会启动工作进程。用机器人密钥轮询 `inbox.check`（或对应的接收路由），处理收到的内容 |
| `/api/integrations/clients/:clientId/…` 返回 `404` | 路径参数必须是 **应用记录 UUID**，而不是用于 Basic 认证的公开 `ci-…` 客户端 ID |
