本文件面向 Bot 创建者和第三方程序集成者。通用 JSON envelope、UUID、RFC 3339 时间、Message/Conversation/Attachment DTO、分页和错误处理见 API.md。本文只使用 XChat 品牌和原创协议;接收模式借鉴了成熟 Bot 平台的“Webhook 或长轮询二选一”思想。
Bot 是独立身份类型,不是带特殊密码的真人账号。数据库中它拥有一个 users 外观行,使现有 conversation_members、messages.sender_id、回应和附件外键可以复用;独立 bots 行保存所有者、状态、接收模式和 Bot Token 哈希。Bot 不能登录普通 /auth/login、不能创建 session、不能获得管理员能力。
每个 Bot 只有一个真人 owner。owner 可修改资料、启停、重置 token、配置 Webhook、查看投递日志和删除。群组管理员可像管理普通成员一样把 Bot 加入、设为 MEMBER/READONLY 或移除;UI 用 BOT 角标区分。
Bot 的数据边界始终由成员关系和 Conversation capability 决定:
- 只可列出、读取和接收自己是 ACTIVE 成员的会话。
- 群组 MEMBER 可发,READONLY 不可发;频道普通 MEMBER 默认也不可发,除非被授予允许发送的角色。
- 不能指定别的 sender ID,消息 sender 永远来自 token 内 bot ID。
- 加入会话必须由真人管理员直接添加,或持有该会话有效邀请 token。
- 被移除、禁用或删除后,读取、发送、上传引用和更新接收立即停止。
- Bot API 有独立限流,不能借普通 Cookie 提高配额。
创建或重置响应只显示一次完整 token:
xb.<bot_uuid>.<32-byte-base64url-secret>
服务端只保存完整 token 的 SHA-256 哈希,不保存明文。第三方程序通过 Header 认证:
Authorization: Bot xb.7d99d244-d84c-4e7e-9717-547b20e524ce.qQ...不要使用 Bearer,不要把 token 放 query、URL、Webhook body、普通日志或代码库。生产程序把它放操作系统 secret store。重置会立即使旧 token 的全部 API 调用失败,并同时轮换 Webhook HMAC 派生键;删除则永久销毁身份能力。
Bot Token 的机器错误:
| 错误码 | HTTP | 处理 |
|---|---|---|
bot.invalid_token |
401 | token 缺失、格式错误、已重置或 Bot 已删除;停止任务并更换凭据。 |
bot.disabled |
403 | owner 已停用;不自动高频重试,等待 owner 启用。 |
bot.mode_conflict |
409 | 已配置 Webhook 时调用长轮询;选择一种接收模式。 |
bot.webhook_invalid |
400 | URL 非 HTTPS、含凭据/fragment、DNS 无结果或指向非公网地址。 |
本节接口使用真人 Cookie/Bearer session。浏览器非安全方法仍要求 CSRF。owner 之外的用户统一收到 404,避免枚举 Bot 配置。
查询 cursor 和 limit(1–100,默认 50)。按创建时间、ID 倒序返回当前真人创建且未删除的 Bot:
{
"ok": true,
"data": [
{
"id": "7d99d244-d84c-4e7e-9717-547b20e524ce",
"username": "release_bot",
"display_name": "Release Bot",
"bio": "发布通知",
"avatar_attachment_id": null,
"owner_user_id": "e38e45e0-b289-48ef-962a-003c6e8fb2d8",
"status": "ENABLED",
"receive_mode": "POLLING",
"webhook_url": null,
"created_at": "2026-08-02T09:00:00.000Z",
"updated_at": "2026-08-02T09:00:00.000Z"
}
],
"meta": { "next_cursor": null }
}列表永不返回 token、token hash 或签名键。
每个 owner 每小时最多 10 次。请求:
{
"username": "release_bot",
"display_name": "Release Bot",
"bio": "发布通知"
}username 全站唯一、3–32 位 ASCII 字母/数字/下划线。成功 HTTP 201,返回 {bot,token}。调用方必须在确认持久保存 token 后才关闭一次性展示界面;丢失只能重置,服务端无法找回。
返回 owner 自己的 Bot DTO,用于管理页刷新状态。
至少提交一个字段:
{
"display_name": "Release Assistant",
"bio": "只发布稳定版通知",
"avatar_attachment_id": null,
"status": "ENABLED"
}status 为 ENABLED/DISABLED。停用同步把外观用户设为 SUSPENDED并关闭实时连接;事件仍保留,Webhook 队列暂停,重新启用后可继续处理。头像必须是 owner 上传的图片附件。
软删除 Bot、销毁 token、取消投递队列并把它从所有 ACTIVE 会话移除。历史消息仍显示“Deleted bot”发送者占位,保证审计和回复结构稳定。返回 {"deleted":true}。此操作不能恢复。
每小时最多 10 次。立即轮换 token hash 和 Webhook HMAC 键,返回 {bot,token};明文仍只显示一次。应先部署能读取新 token 的第三方程序,再重置并尽快切换,期间旧 token 不再工作。
请求 {"url":"https://bot.example.com/xchat/webhook"}。URL 最长 2048,只允许 HTTPS、无 URL 用户名密码、无 fragment;配置时解析全部 DNS 地址,任何回环、私网、链路本地、文档或保留地址都会拒绝。成功切换为 WEBHOOK,并把尚未由 polling offset 确认的事件按顺序入队。
服务端每次真实投递前再次解析并固定公网 IP,不跟随 HTTP redirect,因此 DNS 后续变为内网仍会被拒绝。
删除 URL并切回 POLLING。从最后成功投递的 update 之后开始读取,未成功事件仍可取得;已有投递日志队列随切换清理。返回更新后的 Bot DTO。
查询 cursor、limit(默认 50)。仅 Webhook 模式会产生记录:
{
"id": "0b62461188282951b77ba771f36a1e16",
"update_id": "9822",
"status": "DELIVERED",
"attempts": 1,
"next_attempt_at": "2026-08-02T09:30:00.000Z",
"last_http_status": 204,
"last_duration_ms": 85,
"last_error": null,
"created_at": "2026-08-02T09:30:00.000Z",
"delivered_at": "2026-08-02T09:30:00.085Z"
}状态为 PENDING、DELIVERED、DEAD。日志不含响应 body,以免第三方回调把秘密反射进 XChat 数据库。
长轮询数组元素和 Webhook body 完全相同:
{
"update_id": "9822",
"event": {
"event_id": "9822",
"event_uuid": "c1d1c12e-7fe7-4e5d-9131-a05fbec13ab4",
"type": "message.created",
"occurred_at": "2026-08-02T09:30:00.123Z",
"conversation_id": "50bcc144-fd5e-4482-a109-5090eaa6d4d4",
"actor_id": "e38e45e0-b289-48ef-962a-003c6e8fb2d8",
"data": { "message": {} },
"schema_version": 1
},
"command": null
}update_id 等于持久 event_id,是该 Bot 的有序确认游标,但相邻值不保证连续。可收到的类型包括消息创建/编辑/删除、回应、会话更新/删除、成员加入/角色变化/离开/移除、已读、置顶、session 事件和自己的附件 ready;服务端只为 Bot 是事件 recipient 的记录创建 update。
message.created 如果第一个 text segment 完整匹配 /name、/name arguments、/name@this_bot arguments,会额外填充:
{
"name": "deploy",
"arguments": "production --dry-run",
"addressed_to_me": true
}命令名转小写,1–32 位字母/数字/下划线。显式 @other_bot 不会被当前 Bot 识别。普通消息 command:null;程序仍可自行处理结构化 segments。
Bot update 和 Bot 消息 REST 中的附件 URL统一指向 /api/v1/bot/attachments/...,调用时继续携带 Bot Header;不要使用普通用户附件 URL。附件 DTO 还包含 preview_status、preview_url 与 duration_ms。Bot 可以接收 VOICE 消息和服务端生成的 link_preview segment;VOICE 的 content.voice.duration_ms 已由服务端使用 ffprobe 校验,link_preview 只读且 Bot 不得在主动发消息时伪造。Bot v1 主动发送仍限于文本、图片和文件,录音采集能力属于客户端职责。
Bot 与真人 MEMBER 遵守同一发送边界:会话 members_muted=true、成员 send_muted=true 或角色为 READONLY 时,主动发送返回 403。只有被群主明确提升为 ADMIN 的 Bot 才获得 mention.everyone capability 并可提交 {type:"mention_all"};普通 Bot 只能在 update 中把它作为只读 segment 接收。
新 Bot 默认 POLLING。一个 Bot 同时只能有一个 active poller;部署方应使用进程锁或 leader election,不能由多个副本竞争同一个 token。
Bot Token。参数:
offset:正整数字符串。表示“确认所有 update_id 小于此值”,通常传上次响应的next_offset。limit:1–100,默认 50。timeout:0–50 秒,默认 0;没有 update 时挂起等待,期间不占用额外工作线程。
响应:
{
"ok": true,
"data": {
"updates": [],
"next_offset": null
}
}至少一次处理算法:
- 首次不传 offset,取得一批 updates。
- 按顺序处理,并把业务副作用与最后
update_id持久化在 Bot 自己的数据库。 - 下一次请求传
offset=last_update_id+1;服务端此时才确认前一批。 - 响应在网络中丢失时,不推进 offset,下一次会重复,业务处理必须按 update ID 幂等。
- 如果请求 abort,服务端返回空或连接结束,尚未确认事件不会丢失。
服务端会把异常大的 offset 收敛到该 Bot 当前真实 update 高水位,避免错误客户端确认尚未产生的未来事件;这不是跳过更新的机制,调用方仍应只提交上次成功处理得到的 next_offset。
配置 Webhook 后调用本接口返回 bot.mode_conflict,不会同时消费。
每次 HTTP POST 的 body 是单个 Update UTF-8 JSON,不是数组。Header:
Content-Type: application/json; charset=utf-8
User-Agent: XChat-Bot-Webhook/1.0
X-XChat-Delivery: 0b62461188282951b77ba771f36a1e16
X-XChat-Timestamp: 1785663000
X-XChat-Signature: sha256=<64 lowercase hex>2xx 表示成功;3xx 不跟随,4xx/5xx、DNS/TLS/连接错误和 5 秒超时均失败。按 Bot 内 update ID 串行,后续 update 不越过仍在退避的早期项。重试延迟依次约为 1 秒、5 秒、30 秒、2 分钟、10 分钟,之后每小时,最多 10 次;耗尽标记 DEAD并允许后续继续。接收端必须用 delivery/update ID 幂等,因为超时可能发生在它已提交业务之后。
对原始 Bot Token 的 UTF-8 字节计算:
signing_key = SHA256("XChat Bot Webhook v1\0" || bot_token)
结果是 32 字节,不是十六进制文本作为 HMAC 输入。服务端只保存这个派生键和 token hash,不能由它恢复 Bot Token。
签名输入是 ASCII 时间戳、点号和收到的原始 body 字节:
expected = HMAC-SHA256(signing_key, timestamp || "." || raw_body)
header = "sha256=" || lowercase_hex(expected)
接收程序应在解析 JSON 之前完成:
- 要求三个 X-XChat Header 都存在。
- 检查服务器当前 Unix 秒与 timestamp 差不超过组织允许窗口,建议 300 秒。
- 使用原始 body,不得先 parse 后重新 stringify。
- 用固定时间方法比较完整签名。
- 在数据库唯一约束中登记 delivery ID或 update ID,再提交业务和 2xx。
Node.js 22 验签示例:
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
function verifyXChatWebhook(botToken, timestamp, rawBody, signatureHeader) {
const key = createHash("sha256")
.update("XChat Bot Webhook v1\0", "utf8")
.update(botToken, "utf8")
.digest();
const expected = Buffer.from(
`sha256=${createHmac("sha256", key)
.update(timestamp, "utf8")
.update(".", "utf8")
.update(rawBody)
.digest("hex")}`,
);
const received = Buffer.from(signatureHeader || "");
return (
expected.length === received.length && timingSafeEqual(expected, received)
);
}Token reset后签名键立即改变;回调服务应和 API worker 一起原子切换 secret。
Bot Token。返回 PublicUser,is_bot:true、is_admin 不存在。用于启动时验证凭据和取得 bot ID/username。
Bot Token。cursor、limit 与普通会话列表相同,只返回 Bot 是 ACTIVE 成员的 Conversation。member.capabilities 是发送/读取决策的权威值。
Bot Token且是成员。返回单个 Conversation;未加入或被移除统一 404。
需要 member.list。返回 Member 数组;用户外观的 is_bot 区分人和机器人。Bot 不会因此得到成员的敏感认证资料。
Bot Token。请求 {"invite_token":"conversation invite"}。服务端先验证 token 指向 URL 中同一会话,再按邀请次数原子加入为 MEMBER。不能只凭公开会话 ID加入。
Bot Token且是成员。返回 {"left":true};离开后不能读取历史或下载原附件。owner/管理员可稍后重新邀请。
需要 message.read。before_seq、limit 与普通消息历史相同,返回升序 Message 和 next_cursor。附件 URL 已改写为 Bot 命名空间。
需要 message.send,每 Bot 每分钟 60 次。请求与普通发送相同:
{
"client_message_id": "5b160234-8a0c-414f-8830-2e0f25ef5da2",
"type": "TEXT",
"content": { "segments": [{ "type": "text", "text": "Build passed" }] },
"attachment_ids": [],
"reply_to_message_id": "8d300f49-96a2-4c7f-a26d-0c54b958f4c0"
}新建 201,幂等重复 200;始终复用原 client ID重试。Bot sender 由认证决定,body 没有 sender 字段。mention user ID必须是会话 ACTIVE 成员。
需要消息所在会话 message.read。返回 Message,附件 URL为 Bot 专用地址。
Bot Token,每小时 20 次。单文件 multipart,限制、MIME 检测、危险类型、SHA-256 和缩略图流程与普通附件完全相同。成功 201,响应 URL可用 Bot Token 下载。上传本身不要求会话;发送消息时检查附件 uploader 是当前 Bot、READY 且未被占用。
Bot Token。当前 Bot是未引用附件的上传者、仍是消息/群头像引用会话的 ACTIVE 成员,或附件是可检索用户/Bot 头像时返回元数据,否则 404。
同一鉴权,返回二进制并支持单段 Range。Header继续使用 Authorization: Bot ...,不要把 token 加入 content_url。
返回 Bot 当前可访问会话中图片附件的 WebP 缩略图。
使用 Bot Token 获取受控内联预览。图片返回 WebP,视频返回最高 480p MP4,音频支持
单段 Range;Bot 被移出会话后该 URL 立即失效。原文件下载仍使用 /content。
同一鉴权,图片有缩略图时返回 WebP,不支持 Range。
| 操作 | 默认限制 |
|---|---|
| Bot 读取/会话/成员 | 每 Token 每分钟 180 次 |
| 长轮询 | 每 Token 每分钟 120 次;timeout 最长 50 秒 |
| 发送消息 | 每 Token 每分钟 60 次 |
| 加入/退出 | 每 Token 每分钟 30 次 |
| 上传 | 每 Token 每小时 20 次 |
| owner 创建/重置 | 每 owner 每小时 10 次 |
限流键是 Authorization 的 SHA-256,不在日志保留原 token。收到 429 时按通用错误规范退避;发送重试仍复用 client ID。
const base = process.env.XCHAT_ORIGIN;
const token = process.env.XCHAT_BOT_TOKEN;
let offset;
async function call(path, options = {}) {
const response = await fetch(`${base}${path}`, {
...options,
headers: {
Authorization: `Bot ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
const body = await response.json();
if (!response.ok || !body.ok)
throw new Error(body.error?.code || response.status);
return body.data;
}
for (;;) {
const query = new URLSearchParams({ timeout: "50", limit: "50" });
if (offset) query.set("offset", offset);
const batch = await call(`/api/v1/bot/updates?${query}`);
for (const update of batch.updates) {
if (update.command?.name === "ping") {
await call(
`/api/v1/bot/conversations/${update.event.conversation_id}/messages`,
{
method: "POST",
body: JSON.stringify({
client_message_id: crypto.randomUUID(),
type: "TEXT",
content: { segments: [{ type: "text", text: "pong" }] },
attachment_ids: [],
reply_to_message_id: update.event.data.message.id,
}),
},
);
}
offset = (BigInt(update.update_id) + 1n).toString();
}
}生产代码还应持久化 offset、对 update 和命令副作用建立唯一键、处理 401/403/409/429、设置进程退出信号并对网络错误使用带抖动退避。
- Token 只存在第三方 secret store,创建/重置弹窗关闭后不可从 XChat 再读。
- Webhook 域名证书有效,回调在 5 秒内提交并返回 2xx,耗时任务进入自己的队列。
- 回调先验签、再解析,检查时间窗口和 delivery 唯一性。
- 长轮询只有一个消费者,offset 在业务事务成功后推进。
- Bot 在群内角色最小化;通知类 Bot 在频道通常只需 ADMIN 发送能力,不需要服务器管理员身份。
- owner 定期查看 DEAD/PENDING 日志并轮换泄漏 token;禁用用于临时止损,删除用于永久退役。