跳到主要内容
Logo2SOMEone

接口参考

机器人开放接口:鉴权方式、能力端点与调用规则

机器人程序通过 HTTPS 调用平台开放接口。所有请求都发往 https://2some.ren,鉴权统一用请求头携带运行密钥:

x-api-key: sk_xxxxxxxx

验证身份

启动时建议先做一次身份校验,确认密钥有效且确实是机器人密钥:

curl https://2some.ren/api/v1/bots/me \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

返回的 user.accountType 应为 "bot"user.id 即机器人的 botUserId

运行密钥只用于 /api/v1 下的公开 Bot 能力,不能调用 /api/auth 的账号、API Key、Passkey 或设备授权控制面;这些管理操作必须使用 human account key。

能力与安装授权

能力先在 Bot 资料中声明,再由每次群聊安装显式授予;运行密钥 scopes 与当前 Bot capability、安装 grant 的交集才是最终权限。当前群聊能力如下:

Capability作用是否附带其他能力
chat.send发送、编辑或撤回机器人自己的消息;Thread 回复还需 chat.read不附带 chat.readchat.react
chat.read按安装隐私模式读取群聊与 Thread不附带发送或 reaction
chat.react为机器人自己发送且当前安装仍可见的消息添加/取消自己的 reaction不附带发送或读取

权限不足、安装失效或消息超出隐私范围时,服务端仍是最终裁决者;不要把 Bot roster 的 ConversationMember 角色当作安装授权,也不要把 chat.react 当成可操作任意可见消息的权限。

发布泡泡

需要 bubble.create 能力。

curl -X POST https://2some.ren/api/v1/bubbles \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "你好,泡泡!详情 → https://example.com/article"}'
字段必填说明
content是*纯文本内容,不超过 500 个可见字符;不能与 richContent 同时提交
richContent是*富文本字符串,可用 <sticker:stickerId[:packId]> 内联已审核且可用的贴纸;原始字符串最长 2048 字符、可见内容最长 500 个字符、最多 9 张贴纸;不能与 content 同时提交
visibilitypublic(默认)/ followers / unlisted
attachmentIds图片附件 id 数组,最多 9 张(附件必须由当前身份上传并已完成上传)
aiImageIds当前身份拥有、审核通过、图片已就绪且当前身份有权分享的 AI 作品 id;服务端会复制为 Bubble 图片附件,和 attachmentIds 合计最多 9 张
replyToBubbleId回复某条泡泡(可见性会继承被回复的泡泡)
quoteBubbleId引用某条泡泡
quoteArticleId引用某篇文章

*contentrichContent 二选一(也可以都省略);文本/富文本、attachmentIdsaiImageIdsquoteBubbleIdquoteArticleId 至少要有一个。richContent 可以只含合法贴纸;replyToBubbleId 只指定回复关系,不能单独作为内容。quoteBubbleIdquoteArticleId 不能同时提交。公开接口不接受 quoteLetterId;转发树洞稿件只能在稿件详情中操作。

机器人发图片需注意

attachmentIds 是接口支持的字段,但它要求先把图片上传拿到附件 id,而上传接口 /api/v1/media/upload-url 目前只对真人会话开放,不接受机器人运行密钥aiImageIds 不需要再次上传,但只能使用当前身份拥有、审核通过且已就绪、并且仍有分享权限的 AI 作品;richContent 中的贴纸也必须是当前身份可用且已审核的贴纸。没有预先准备这些资源时,机器人实际能稳定发送的是文本、富文本(不含不可用贴纸)、链接、回复和引用;要发普通图片需官方单独开通上传入口。回复和引用对机器人是可用的,适合做「自动回复某话题」「转发引用外部内容并加点评」。

成功返回 { "success": true, "id": "<bubbleId>" },泡泡地址为 https://2some.ren/bubble/<bubbleId>

内容规则

  • 内容会经过平台审核,违规内容发布失败
  • #词语 会成为话题标签;@用户名真实通知对应用户——转发外部内容时建议把 @ 替换为全角 ,避免误打扰
  • 机器人发布的 public 原创泡泡会进入关注它的人的关注流(并可能触发粉丝推送)。回复不会创建主 Feed 条目;引用和文章评论会保留可发现的 Feed 条目,但纯原创关注流会过滤它们,三者都不会触发粉丝 fan-out 推送。机器人高频发 public 原创泡泡会打扰粉丝,请控制节奏
  • 写操作有频率限制,收到 429 时请退避重试

读取自己的泡泡

需要 bubble.read 能力。机器人只能读自己发布的内容,常用于「判断哪些内容已经发过」的去重场景。

curl "https://2some.ren/api/v1/bubbles?authorId=<botUserId>&limit=50" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"
参数说明
authorId必填,机器人自己的 botUserId
limit每页条数,最大 50
cursor翻页游标,取上一页返回的 nextCursor

返回 { "success": true, "items": [...], "nextCursor": "<游标>" }nextCursornull 时表示没有更多数据。

发现已安装的群聊

机器人运行密钥无需 chat.read 能力也可以查询自身当前可用的安装,用返回的 conversationId 调用群聊接口:

curl "https://2some.ren/api/v1/bots/me/installations" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

返回 { "success": true, "items": [...] },每项只包含以下低敏字段:

字段说明
installationId该次安装的 id
conversationId已安装群聊的 id
privacyModemention_only / commands_only / read_all
updatedAt安装最后更新时间(UTC ISO 8601)

接口只返回机器人自己的 active 安装,并排除已归档群聊和成员关系已不存在的记录;不会返回群名、群成员、安装者或 disabled 安装。该端点仅接受机器人运行密钥,账号登录态和账号 API Key 均不可调用。

向群聊发消息

需要 chat.send 能力,且机器人已被安装到该群聊

curl -X POST "https://2some.ren/api/v1/chat/conversations/<conversationId>/messages" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "今晚 8 点开播,记得来!"}'

text 始终按纯文本处理,也是默认和最兼容的写法。要显式发送受限 Markdown,请改传公开 input block;REST 没有顶层 parseMode 字段(Bot SDK 的 parseMode 只是转换该 block 的 convenience):

curl -X POST "https://2some.ren/api/v1/chat/conversations/<conversationId>/messages" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"blocks":[{"type":"rich_text_v1","source":"**重点**:见 [说明](https://example.com)"}]}'

群聊消息不止能发纯文本,下面这些字段都可以带:

字段必填说明
text否*默认的纯文本内容,最长 8000 字,不解析 Markdown 或 HTML
attachmentIds图片、视频或文件附件 id 数组,最多 9 个(需先上传,见下方提醒)
aiImageIds当前身份拥有、审核通过、图片已就绪且当前身份有权分享的 AI 作品 id;服务端会复制为聊天图片附件,和 attachmentIds 合计最多 9 个
voiceNoteId语音消息 id(不能和 attachmentIdsaiImageIds 同时发)
parentMessageId引用回复某条消息;Bot 同时需要 chat.read,避免用发送权限读取引用上下文
threadRootId回复某条 Thread 根消息的 id;Bot 同时需要 chat.read,且 root 必须满足安装隐私模式
clientMessageId客户端生成的 UUID;同一会话内重试同一条消息时必须复用,省略后不同请求之间不会去重
blocks结构化消息块:textmentionmention_allrich_text_v1sticker;贴纸必须单独成条
linkCard仅用于正文、text block 中第一个可识别链接,或 rich_text_v1 中第一个安全 link node:url 必须与服务端规范化后的链接完全一致。客户端 card 只作为临时预览;服务端能解析并获取目标时会校正 Bilibili / 2SOMEone 站内实体卡片,失败时可能保留通用卡片或标记获取失败。不能单独作为消息内容
asLiveTalk请求将消息标记为当前活跃直播会话的消息;没有活跃会话时不生效,Bot runtime 不允许传 true

*text、非空 rich_text_v1attachmentIdsaiImageIdsvoiceNoteId 或贴纸 block 至少要有一个;目前仅有 text block 不能单独通过 API 发送。parentMessageIdthreadRootIdlinkCard 不能单独作为内容。voiceNoteId 不能和任何 attachmentIds / aiImageIds 同发;两类图片/文件 id 合计最多 9 个。@提及、@所有人、贴纸通过 blocks 字段发送,其中 @所有人只有群主或管理员能用,贴纸必须单独成条。

如果同时传顶层 textblocks,消息正文以 blocks 为准,顶层 text 不会再作为 fallback 写入;顶层 text 仍会经过 8000 字符的请求校验。要发送带格式的正文,只传 rich_text_v1 block 最清楚。

受限 Markdown 与兼容读取

公开写入形态严格限定为 { "type": "rich_text_v1", "source": string }。支持粗体、斜体、删除线、行内代码、代码块、引用、有序/无序列表、换行和 HTTP/HTTPS 链接;不支持的 Markdown 语法会按输入中的字面文本保留,不会按该语法生成格式化节点。javascript:data: 和自定义 scheme 不会成为链接,也会按字面文本保留。@用户名 不会从 Markdown 推断为通知对象,需要通知时仍应使用顶层 mention / mention_all block。

任意 HTML、CSS、JavaScript、DOM、iframe 或 WebView 正文都不受支持,也不会执行。服务端会从 source 生成 { plainText, nodes } 和消息的纯文本 textplainTextnodestext 都是派生结果,客户端不得提交,也不要把顶层 text 当作 fallback(若同传,按上面的 blocks 优先规则处理)。带 plainTextnodes 或其他额外字段的 rich_text_v1 input block 会被拒绝。

读取时,x-chat-message-blocks 是读取方声明的完整 capability 集合。当前已知类型为 textmentionmention_allstickercustom_emojirich_text_v1unsupported;只声明 rich_text_v1 不等于声明其它类型,未声明的 block 会按 legacy/unsupported 规则处理。只有显式声明 rich_text_v1 的消费者会获得完整 { type, source, plainText, nodes } block,例如:

curl "https://2some.ren/api/v1/chat/conversations/<conversationId>/messages" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" \
  -H "x-chat-message-blocks: text,mention,mention_all,sticker,custom_emoji,rich_text_v1,unsupported"

未带 capability(包括 Bot SDK 默认读取)的 legacy 消费者会按顶层顺序把每个富文本 block 降级为真实 { type: "text", content: plainText },相邻 Mention 仍保留;链接 fallback 会保留可访问 URL。服务端不会用“请更新客户端”占位替换真实正文;legacy 消费者也不能用 fallback 纯文本静默覆盖原富文本,编辑会返回 409 升级提示。

单条消息最多 64 个公开 blocks;消息内所有 block 的 authoring 内容总长和派生 plainText 总长分别不得超过 8000 字符(两项独立计算,不是相加)。服务端会把一个消息内全部 rich_text_v1 block 的 AST 节点累计限制在 1024 个以内,嵌套深度最多 8 层;Markdown 链接的原始值与规范化结果都不能超过 2048 字符,且只有有效的 HTTP/HTTPS URL 才会生成链接节点。超过字符数、block 数、AST 节点数或嵌套深度上限的请求会被拒绝;超长、危险协议或无效 scheme 的链接不会成为链接,而是按字面文本保留。不会执行 HTML、CSS、JavaScript、iframe 或 WebView。

机器人发图片 / 语音需注意

attachmentIds / voiceNoteId 需要先上传媒体拿到 id,而上传接口暂不对机器人运行密钥开放;aiImageIds 不需要再次上传,但只能使用当前 Bot 自己拥有、审核通过且已就绪、并且仍有分享权限的 AI 作品。没有预先开通媒体、可分享 AI 作品或可用贴纸时,机器人在群里实际能稳定发的是文本、结构化文本、回复(parentMessageId)、Thread 回复和链接卡片;要发普通图片 / 视频 / 文件或语音需官方单独开通上传入口。接口仍支持 aiImageIds、贴纸和其他结构化 block,能否使用取决于资源状态与权限。

读取群聊与 Thread

需要 chat.read 能力,且机器人已安装到目标会话。服务端会严格执行安装时的隐私模式: mention_only 返回提及机器人的顶层消息、机器人自己在可见会话中的消息,以及可见提及 root 下的 Thread 上下文;commands_only 返回顶层命令消息、机器人自己在可见会话中的消息,以及可见命令 root 下的 Thread 上下文,read_all 返回常规可见性过滤范围内的群聊历史(仍受拉黑、举报、内部消息、管理员受众、LiveTalk 和归档状态等规则约束)。机器人自己发在已隐藏 Thread root 下的回复也不会绕过 root 隐私边界。普通隐藏 Thread 中单独 @Bot 的回复不会反向开放此前隐藏的 root;只有 root 先满足隐私模式时,Thread 回复才会进入上下文。Bot runtime 目前只支持已安装且未归档的 Group,不适用于私信、Channel 或客服会话。

curl "https://2some.ren/api/v1/chat/conversations/<conversationId>/messages?after=100&limit=50&order=asc" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

curl "https://2some.ren/api/v1/chat/conversations/<conversationId>/messages/<threadRootId>/thread?after=100&order=asc" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

两条接口都支持正整数 before / after 游标;limit 最终限制为 1~200。非法游标会返回 400,不会静默回到默认页。传 after 且省略 order 时按升序取游标之后最早的一页;传 before 且省略 order 时,按降序取游标之前最近的一页(SDK 对 before 单独调用会显式传 order=desc)。显式 order 只决定从哪一端取这一页窗口,不改变响应排列:服务端返回的 items 始终按消息 id/时间升序排列。未传游标时,顶层消息接口返回最新窗口,Thread 接口返回按时间正序的回复。两条接口当前都返回 { "success": true, "items": [...] },不会返回 nextCursor;客户端应把本页最大消息 id 保存为下一次 after,直到返回空页。beforeafter 不建议同时传;需要组合条件时请显式指定 order。Thread 回复可用 threadRootId 继续发送,但同时要求 chat.sendchat.read;root 必须是同一会话中可见、未撤回的顶层消息,不能是 LiveTalk 消息。机器人只能编辑、撤回和 reaction 自己发送的消息:编辑 / 撤回使用 chat.send,reaction 使用独立的 chat.react;写接口只返回自身消息的最小 receipt,完整消息及引用上下文仍需 chat.read 后调用读取接口。

Bot 的消息响应会把 threadReplyCount 固定为 0threadLastReplyAt 固定为 null。这两个持久聚合无法按每个 Bot 的拉黑、举报、内部消息、管理员受众和安装隐私重新计算,因此不能用来判断 Thread 是否有隐藏活动;需要处理已知 Thread 时请分页读取其可见回复。

对 Bot 而言,不存在或因安装隐私模式不可见的 Thread root 会与“可见但暂无回复”统一返回 200 和空 items,不会通过 404 暴露 root 是否存在;请把它视为不可用上下文,不要据此创建跨会话映射。

列表的 items 可能包含已撤回消息的 tombstone(例如历史页或 Bot 自己的消息)。这类项仍保留 id、发送者和时间等元数据,并以 deletedAt(以及可选的 deletedReason)标记撤回;textnullblocksattachmentslinkCardsreactions 为空。不要把 text === null 当成网络缺失,请以 deletedAt 判断消息状态。

单条消息与自己的消息操作

读取单条消息(需要 chat.read):

curl "https://2some.ren/api/v1/chat/messages/<messageId>" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

消息必须位于机器人已安装并获隐私模式允许的会话中;不存在或被隐私模式过滤的消息按 404 处理。已撤回的消息仍可能返回一个不含正文的 tombstone(deletedAt 有值);已撤回的 Thread root 不能再读取其回复。编辑和撤回需要 chat.send;reaction 使用独立的 chat.react。这些写操作都只能操作机器人自己发送的消息:

# 编辑(发送后 15 分钟内)
curl -X PATCH "https://2some.ren/api/v1/chat/messages/<messageId>" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" -H "Content-Type: application/json" \
  -d '{"text":"更新后的内容"}'

# 编辑为受限 Markdown(同样只提交 source)
curl -X PATCH "https://2some.ren/api/v1/chat/messages/<messageId>" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" -H "Content-Type: application/json" \
  -d '{"blocks":[{"type":"rich_text_v1","source":"**更新后的内容**"}]}'

# 撤回(发送后 2 分钟内)
curl -X DELETE "https://2some.ren/api/v1/chat/messages/<messageId>" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

# 添加 / 取消自己的 reaction(需要独立的 chat.react;不会授予 chat.send 或 chat.read)
curl -X POST "https://2some.ren/api/v1/chat/messages/<messageId>/reactions" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" -H "Content-Type: application/json" \
  -d '{"emojiCode":"👍"}'
curl -X DELETE "https://2some.ren/api/v1/chat/messages/<messageId>/reactions?emojiCode=👍" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY"

编辑 / 撤回成功返回 { "success": true, "message": { "id", "conversationId", "clientMessageId", "createdAt", "editedAt", "deletedAt" } } 的最小 receipt;reaction 成功只返回 { "success": true }chat.react 是独立 capability,不隐式授予 chat.sendchat.read;Bot 只能对自己发送的消息添加或取消自己的 reaction,不能操作其他发送者的消息。after 游标只覆盖新建消息,不能补回离线期间发生的编辑、撤回或 reaction 变更。反应型 Bot 默认用 webhook + GET /api/v1/bots/me/events 作为 inbox(当前会在斜杠命令命中时叫醒)。需要监控会话里更多可见消息时,继续用 GET .../messages / SDK iterateChatMessagePageschat.read);拉消息限额按主人当前 VIP,见下方 429。

事件 inbox 与 Webhook 叫醒

操作步骤、验签与限额见 Webhook 与轮询。下面是接口契约。

反应型机器人被斜杠命令点到时,平台会把一条 interaction.created 写入事件表,并在主人已登记 active HTTPS 回调时签名 POST 过去。Webhook 丢失后,用 runtime key 拉同一条事件:

curl "https://2some.ren/api/v1/bots/me/events?limit=100" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" \
  -H "x-2someone-bot-runtime: 1"

新轮换的 runtime key 自动带 events.read;这不是群安装授权。wait 非 0 会返回 400(当前 Route 不做 long poll)。登记回调是主人操作(Web 设置或 2s1 bot endpoint create),群管理员安装时看不到回调 URL,也没有名为 Webhook 的勾选。验证请求是签名 POST { type: "endpoint.verify", challenge },对端须 2xx 且回显同一个 challenge。投递请求头包含 x-webhook-timestampx-webhook-endpoint-idx-webhook-signaturev1: + Ed25519),签名内容为 timestamp.endpointId.body

# 主人登记 HTTPS 回调(human account key)
curl -X POST "https://2some.ren/api/v1/bots/<botUserId>/endpoint" \
  -H "x-api-key: $TWOSOMEONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/bot/events","idempotencyKey":"<uuid>"}'

# 查看当前回调与验签公钥
curl "https://2some.ren/api/v1/bots/<botUserId>/endpoint" \
  -H "x-api-key: $TWOSOMEONE_API_KEY"

# 暂停出站(需带当前 configRevision;事件仍可 pull)
curl -X PATCH "https://2some.ren/api/v1/bots/<botUserId>/endpoint" \
  -H "x-api-key: $TWOSOMEONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"paused","expectedConfigRevision":"<revision>","idempotencyKey":"<uuid>"}'

Thread 已读与关注状态

Bot 也可以用 chat.read 更新自己在可见 Thread上的已读水位或关注状态;这两个端点不需要 chat.send。它们只接受机器人已安装、未归档 Group 中满足安装隐私模式的顶层 Thread root。

# 把 Thread 已读位置推进到某条回复
curl -X POST "https://2some.ren/api/v1/chat/conversations/<conversationId>/threads/<threadRootId>/read" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" -H "Content-Type: application/json" \
  -d '{"upToMessageId":12345}'

# 关注 / 取消关注一个 Thread
curl -X POST "https://2some.ren/api/v1/chat/conversations/<conversationId>/threads/<threadRootId>/follow" \
  -H "x-api-key: $TWOSOMEONE_BOT_API_KEY" -H "Content-Type: application/json" \
  -d '{"followed":true}'

upToMessageId 必须是正安全整数;服务端只会把水位向前推进到该 Thread 中实际可见的回复,不会因传入更大的 id 越过可见内容。已读成功返回 { "success": true, "updated": boolean, "lastReadMessageId": number | null };关注成功返回 { "success": true, "followed": boolean }。对不存在或因 privacy mode 不可见的 root,Bot 两个端点都保持成功形态(已读返回 updated: false,关注回显请求值)且不写入状态,不要用响应判断 root 是否存在;真人会话对这类 root 返回 404。这些状态目前没有对应的 SDK 方法,需直接调用 REST。

当前关注字段还不是 tri-state:首次创建已读状态会按 followed: true 保存;即使随后写入 false,root 作者、被提及者或参与者仍可能因 Activity 的自动资格继续看到该 Thread。完整的“显式取消关注覆盖自动资格”尚未实现。

clientMessageId 幂等边界

同一会话内使用相同 clientMessageId、相同发送者和相同 parentMessageId / threadRootId 重试,会返回第一次写入的消息,不会重复创建。幂等校验不会比较正文、blocks、附件、语音或链接卡片等 payload;同一 UUID 和同一上下文即使改了内容,仍返回第一次结果。要发送新内容请换 UUID,或在允许的编辑窗口内修改原消息。重放仍会重新检查当前 Bot 安装状态和 Thread root 可见性;权限已撤销或 root 已失效时不会返回旧正文。若该 id 已被其他发送者使用,或同一发送者改用了不同的 parent / Thread 上下文,服务端会返回 409;此时应生成新的 UUID。省略 clientMessageId 的请求不会跨请求去重。

常见错误

下表针对机器人 runtime 发布接口(发泡泡 / 发消息):

状态码含义处理建议
401未提供、无效或已被轮换的密钥;部分鉴权入口也会把当前 capability / key scope 不足视为未授权检查运行密钥和 capability;新增能力后按创建页说明轮换运行密钥
403身份有效,但会话成员关系、安装隐私模式或其他业务策略不允许该操作检查安装状态、会话权限和消息规则;最终以响应的 error 字段为准
400参数错误(Bubble 文本超 500 字、Chat 文本超 8000 字、内容违规、附件超 9 张等)按返回的 error 字段修正
409同一会话内的 clientMessageId 已被其他发送者占用,或同一发送者改变了 parent / Thread 上下文生成新的 UUID;不要重试当前 id
429code: rate_limited。可能是整把运行密钥限额(自助默认 100 次/分钟),也可能是 GET .../messages / Thread 的单独拉消息限额(默认 5 次/分钟,VIP 1 为 30,VIP 2 为 60,按主人当前 VIP 即时生效)。响应带 Retry-AfterX-RateLimit-*Retry-After 后重试;反应型默认走 webhook / 事件 feed。拉消息分页每一页都计数,跨会话共用同一只桶

创建机器人(用账号密钥调管理接口)时,若用户名已被占用会返回 409

完整示例

一个最小可运行的 Node.js 机器人(Node 20+,零依赖)。保存为 bot.mjs,然后 TWOSOMEONE_BOT_API_KEY=sk_xxx node bot.mjs 运行:

// bot.mjs — 顶层 await 需要 ESM,文件后缀用 .mjs(或在 package.json 设 "type": "module")
const BASE = "https://2some.ren";
const KEY = process.env.TWOSOMEONE_BOT_API_KEY;

async function api(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { "x-api-key": KEY, "Content-Type": "application/json", ...init.headers },
  });
  const body = await res.json();
  if (!res.ok || body.success === false) throw new Error(body.error ?? `HTTP ${res.status}`);
  return body;
}

// 验证身份
const { user } = await api("/api/v1/bots/me");
if (user?.accountType !== "bot") throw new Error("不是机器人密钥");

// 发一条泡泡
const { id } = await api("/api/v1/bubbles", {
  method: "POST",
  body: JSON.stringify({ content: `大家好,我是 ${user.username} 🤖` }),
});
console.log(`已发布:${BASE}/bubble/${id}`);

更完整的实战范例(RSS 拉取、无状态去重、GitHub Actions 免服务器部署)见官方模板仓库 leaperone/2someone-news-bot。模板仓库的 README/SKILL 当前仍有旧的 /api/auth/get-session 身份调用,Fork 后请先改为 GET /api/v1/bots/me 并校验 accountType === "bot",不能原样运行。