Skip to content

Latest commit

 

History

History
441 lines (317 loc) · 18.9 KB

File metadata and controls

441 lines (317 loc) · 18.9 KB

XChat Bot API v1

本文件面向 Bot 创建者和第三方程序集成者。通用 JSON envelope、UUID、RFC 3339 时间、Message/Conversation/Attachment DTO、分页和错误处理见 API.md。本文只使用 XChat 品牌和原创协议;接收模式借鉴了成熟 Bot 平台的“Webhook 或长轮询二选一”思想。

安全模型

Bot 是独立身份类型,不是带特殊密码的真人账号。数据库中它拥有一个 users 外观行,使现有 conversation_membersmessages.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 提高配额。

Bot Token

创建或重置响应只显示一次完整 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 无结果或指向非公网地址。

Owner 管理 API

本节接口使用真人 Cookie/Bearer session。浏览器非安全方法仍要求 CSRF。owner 之外的用户统一收到 404,避免枚举 Bot 配置。

GET /api/v1/bots

查询 cursorlimit(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 或签名键。

POST /api/v1/bots

每个 owner 每小时最多 10 次。请求:

{
  "username": "release_bot",
  "display_name": "Release Bot",
  "bio": "发布通知"
}

username 全站唯一、3–32 位 ASCII 字母/数字/下划线。成功 HTTP 201,返回 {bot,token}。调用方必须在确认持久保存 token 后才关闭一次性展示界面;丢失只能重置,服务端无法找回。

GET /api/v1/bots/{bot_id}

返回 owner 自己的 Bot DTO,用于管理页刷新状态。

PATCH /api/v1/bots/{bot_id}

至少提交一个字段:

{
  "display_name": "Release Assistant",
  "bio": "只发布稳定版通知",
  "avatar_attachment_id": null,
  "status": "ENABLED"
}

status 为 ENABLED/DISABLED。停用同步把外观用户设为 SUSPENDED并关闭实时连接;事件仍保留,Webhook 队列暂停,重新启用后可继续处理。头像必须是 owner 上传的图片附件。

DELETE /api/v1/bots/{bot_id}

软删除 Bot、销毁 token、取消投递队列并把它从所有 ACTIVE 会话移除。历史消息仍显示“Deleted bot”发送者占位,保证审计和回复结构稳定。返回 {"deleted":true}。此操作不能恢复。

POST /api/v1/bots/{bot_id}/token/reset

每小时最多 10 次。立即轮换 token hash 和 Webhook HMAC 键,返回 {bot,token};明文仍只显示一次。应先部署能读取新 token 的第三方程序,再重置并尽快切换,期间旧 token 不再工作。

PUT /api/v1/bots/{bot_id}/webhook

请求 {"url":"https://bot.example.com/xchat/webhook"}。URL 最长 2048,只允许 HTTPS、无 URL 用户名密码、无 fragment;配置时解析全部 DNS 地址,任何回环、私网、链路本地、文档或保留地址都会拒绝。成功切换为 WEBHOOK,并把尚未由 polling offset 确认的事件按顺序入队。

服务端每次真实投递前再次解析并固定公网 IP,不跟随 HTTP redirect,因此 DNS 后续变为内网仍会被拒绝。

DELETE /api/v1/bots/{bot_id}/webhook

删除 URL并切回 POLLING。从最后成功投递的 update 之后开始读取,未成功事件仍可取得;已有投递日志队列随切换清理。返回更新后的 Bot DTO。

GET /api/v1/bots/{bot_id}/deliveries

查询 cursorlimit(默认 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 数据库。

Update 模型

长轮询数组元素和 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_statuspreview_urlduration_ms。Bot 可以接收 VOICE 消息和服务端生成的 link_preview segment;VOICEcontent.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。

GET /api/v1/bot/updates

Bot Token。参数:

  • offset:正整数字符串。表示“确认所有 update_id 小于此值”,通常传上次响应的 next_offset
  • limit:1–100,默认 50。
  • timeout:0–50 秒,默认 0;没有 update 时挂起等待,期间不占用额外工作线程。

响应:

{
  "ok": true,
  "data": {
    "updates": [],
    "next_offset": null
  }
}

至少一次处理算法:

  1. 首次不传 offset,取得一批 updates。
  2. 按顺序处理,并把业务副作用与最后 update_id 持久化在 Bot 自己的数据库。
  3. 下一次请求传 offset=last_update_id+1;服务端此时才确认前一批。
  4. 响应在网络中丢失时,不推进 offset,下一次会重复,业务处理必须按 update ID 幂等。
  5. 如果请求 abort,服务端返回空或连接结束,尚未确认事件不会丢失。

服务端会把异常大的 offset 收敛到该 Bot 当前真实 update 高水位,避免错误客户端确认尚未产生的未来事件;这不是跳过更新的机制,调用方仍应只提交上次成功处理得到的 next_offset

配置 Webhook 后调用本接口返回 bot.mode_conflict,不会同时消费。

Webhook 模式

请求和成功判定

每次 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 之前完成:

  1. 要求三个 X-XChat Header 都存在。
  2. 检查服务器当前 Unix 秒与 timestamp 差不超过组织允许窗口,建议 300 秒。
  3. 使用原始 body,不得先 parse 后重新 stringify。
  4. 用固定时间方法比较完整签名。
  5. 在数据库唯一约束中登记 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 业务 API

GET /api/v1/bot/me

Bot Token。返回 PublicUser,is_bot:trueis_admin 不存在。用于启动时验证凭据和取得 bot ID/username。

GET /api/v1/bot/conversations

Bot Token。cursorlimit 与普通会话列表相同,只返回 Bot 是 ACTIVE 成员的 Conversation。member.capabilities 是发送/读取决策的权威值。

GET /api/v1/bot/conversations/{conversation_id}

Bot Token且是成员。返回单个 Conversation;未加入或被移除统一 404。

GET /api/v1/bot/conversations/{conversation_id}/members

需要 member.list。返回 Member 数组;用户外观的 is_bot 区分人和机器人。Bot 不会因此得到成员的敏感认证资料。

POST /api/v1/bot/conversations/{conversation_id}/join

Bot Token。请求 {"invite_token":"conversation invite"}。服务端先验证 token 指向 URL 中同一会话,再按邀请次数原子加入为 MEMBER。不能只凭公开会话 ID加入。

POST /api/v1/bot/conversations/{conversation_id}/leave

Bot Token且是成员。返回 {"left":true};离开后不能读取历史或下载原附件。owner/管理员可稍后重新邀请。

GET /api/v1/bot/conversations/{conversation_id}/messages

需要 message.readbefore_seqlimit 与普通消息历史相同,返回升序 Message 和 next_cursor。附件 URL 已改写为 Bot 命名空间。

POST /api/v1/bot/conversations/{conversation_id}/messages

需要 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 成员。

GET /api/v1/bot/messages/{message_id}

需要消息所在会话 message.read。返回 Message,附件 URL为 Bot 专用地址。

POST /api/v1/bot/attachments

Bot Token,每小时 20 次。单文件 multipart,限制、MIME 检测、危险类型、SHA-256 和缩略图流程与普通附件完全相同。成功 201,响应 URL可用 Bot Token 下载。上传本身不要求会话;发送消息时检查附件 uploader 是当前 Bot、READY 且未被占用。

GET /api/v1/bot/attachments/{attachment_id}

Bot Token。当前 Bot是未引用附件的上传者、仍是消息/群头像引用会话的 ACTIVE 成员,或附件是可检索用户/Bot 头像时返回元数据,否则 404。

GET /api/v1/bot/attachments/{attachment_id}/content

同一鉴权,返回二进制并支持单段 Range。Header继续使用 Authorization: Bot ...,不要把 token 加入 content_url

GET /api/v1/bot/attachments/{attachment_id}/thumbnail

返回 Bot 当前可访问会话中图片附件的 WebP 缩略图。

GET /api/v1/bot/attachments/{attachment_id}/preview

使用 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。

最小可运行 Long Polling 示例

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;禁用用于临时止损,删除用于永久退役。