发布到 Bluesky 只需要三个调用:com.atproto.server.createSession 用于获取令牌,com.atproto.repo.uploadBlob 用于上传图片(如有),以及 com.atproto.repo.createRecord 用于写入一条 app.bsky.feed.post 记录。这里没有应用审核,也没有需要获批的 OAuth 授权页面。这份简单背后的代价是:链接和提及不会自动识别,你需要自己计算 UTF-8 编码下的字节偏移量。
Bluesky API 能发布什么,不能发布什么?
一条帖子记录是一个普通的 JSON 文档。必填字段是 $type(值为 app.bsky.feed.post)、text 以及 ISO 8601 格式的时间戳 createdAt。其余都是可选结构。
| 功能 | 支持情况 |
|---|---|
| 每篇帖子图片数 | 最多4张 |
| 图片大小 | 文档规定每张最大1,000,000字节 |
| 每篇帖子blob总大小 | 最大2,000,000字节 |
| 替代文字(alt text) | 每张图片必填,需附宽高比 |
| 引用帖子 | app.bsky.embed.record |
| 链接卡片 | app.bsky.embed.external,需自行提供缩略图 |
| 回复 | reply,含 root 和 parent 的强引用 |
| 语言标签 | langs,如 ["en-US"] 的数组 |
需要提前说明的几个缺口:
- 没有任何内容会被自动识别。 链接、提及和话题标签如果不附加 facet,就只是纯文本。未附加 facet 的URL不会自动变为可点击链接。
- 链接卡片需要你自己构建。
app.bsky.embed.external需要标题、描述和缩略图,没有任何机制会替你抓取页面内容。 - 提及功能需要解析出 DID。 你不能直接在 facet 中放用户名,需要先把用户名解析为 DID。
- 图片上传前必须清除 EXIF 信息,这是文档的要求。
- 官方文档没有说明字符或字素数量上限。 我们无法从该页面核实出一个具体数字,所以这里不引用任何数字。具体客户端强制执行的限制,见Bluesky 字符数上限文章。
提示: 本文数据核实自 Bluesky 开发者文档(docs.bsky.app,目前会重定向到 bsky.network/docs),核实时间为2026年9月。平台可能随时变更这些数据且不另行通知。
认证机制是什么样的?
这可能是你在任何社交平台上会读到的最短的认证说明部分。
对于应用密码这条路径,没有应用注册、没有应用审核,也没有 OAuth 授权页面。用户在自己的 Bluesky 设置中创建一个应用密码,并交给你的软件使用。你用用户名加该应用密码,在 com.atproto.server.createSession 处换取一个访问 JWT 和一个刷新 JWT。访问令牌有效期短,你需要用刷新 JWT 来刷新它。
由此带来两个后果。应用密码是用户直接交给你的凭证:没有授权页面,意味着没有平台介入的授权范围管理,存储责任完全落在你身上。请对其加密,并为用户提供清晰可见的断开连接方式。
而且这个网络不属于单一公司。AT 协议账号存放在个人数据服务器(PDS)上,PDS 可以自行搭建。你的客户端要与用户的 PDS 主机通信,所以硬编码 bsky.social 眼下能跑通,却不符合协议本身的设计模式。应该从用户的 DID 文档中读取主机地址。
具体怎么发帖?调用顺序
POST /xrpc/com.atproto.server.createSession,传入identifier(用户名或 DID)和password(应用密码)。返回accessJwt、refreshJwt和did。- 如果有图片:对每张图片调用
POST /xrpc/com.atproto.repo.uploadBlob,传入原始字节和正确的Content-Type。每次调用返回一个 blob 引用。 - 为任何链接或提及计算 facet,作为
text的 UTF-8 编码中的字节偏移量。 POST /xrpc/com.atproto.repo.createRecord,将repo设为你的 DID,collection设为app.bsky.feed.post,并附上记录本身。
const base = 'https://bsky.social/xrpc';
const auth = await fetch(`${base}/com.atproto.server.createSession`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier: handle, password: appPassword }),
}).then((r) => r.json());
const text = 'Notes on the release: https://example.com/changelog';
const url = 'https://example.com/changelog';
const enc = new TextEncoder();
const byteStart = enc.encode(text.slice(0, text.indexOf(url))).length;
await fetch(`${base}/com.atproto.repo.createRecord`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${auth.accessJwt}`,
},
body: JSON.stringify({
repo: auth.did,
collection: 'app.bsky.feed.post',
record: {
$type: 'app.bsky.feed.post',
text,
createdAt: new Date().toISOString(),
facets: [
{
index: { byteStart, byteEnd: byteStart + enc.encode(url).length },
features: [{ $type: 'app.bsky.richtext.facet#link', uri: url }],
},
],
},
}),
});
facet 的字节计算,说清楚
byteStart 和 byteEnd 是文本 UTF-8 字节中的偏移量,不是 JavaScript 字符串的索引,也不是字符数。
"café".length 在 JavaScript 中是4,但其 UTF-8 编码却是5字节。链接前面出现的任何一个表情符号,都会让字节偏移量偏移4字节,而字符串索引却只移动2位。如果算错了,链接会显示为乱码,或高亮到错误的位置,而服务器不会报错:记录本身是合法的,只是指向了错误的字节。
只编码字符串一次,在字节数组中查找偏移量,切勿混用这两套坐标系统。链接 facet 的功能字段用的是 uri,不是 url。
Bluesky 的速率限制是多少?
写入操作按账号采用点数计量系统:CREATE 消耗3点,UPDATE 消耗2点,DELETE 消耗1点,配额为每小时5,000点,每天35,000点。换算下来大约是每小时1,666次创建,每天11,666次。
| 限制 | 数值 |
|---|---|
| 写入点数 | 5,000/小时,35,000/天 |
| 创建次数(推算) | 约1,666/小时,约11,666/天 |
| PDS 整体请求数 | 每5分钟每IP 3,000次 |
createSession | 每5分钟30次,每天300次(每账号) |
| Blob 上传上限 | 52,428,800字节(50 MB) |
createSession 的限制是最容易让定时发布工具踩坑的地方。每账号每5分钟30次,意味着你需要缓存会话并刷新它,而不是每次发帖都重新登录。如果一个工作进程在每个任务上都创建新会话,很快就会先触发速率限制,而不是先撞到发帖上限。
请注意这两个 blob 数字的区别:PDS 本身接受最大50 MB的 blob,而帖子文档规定每张图片最大1,000,000字节、总计最大2,000,000字节。按更小的那个限制来规划容量。
提示: 本文数据核实自 Bluesky 开发者速率限制文档,核实时间为2026年9月。平台可能随时变更这些数据且不另行通知。
真正会耗费你三周时间的是什么?
Bluesky 确实是各大平台中集成成本最低的一个,但”低成本”不等于”零成本”。
真实文本的 facet 计算。 检测URL、结尾标点、用户名和话题标签,把每一个匹配项转换为 UTF-8 字节偏移量,并在用户编辑文本时保持其正确性。这正是漏洞最容易出现的地方。
Blob 预算管理。 最多4张图片、总计2 MB的限制,意味着你需要在服务器端调整大小、重新编码、清除 EXIF 信息,并决定用户照片放不下时该怎么处理。
链接卡片。 为了让帖子看起来像真正的帖子,你需要抓取目标页面,提取标题、描述和图片,把图片作为 blob 上传,再构建 external embed。这相当于要自己实现一个带超时和失败处理的小型爬虫。
会话缓存(因为每5分钟30次的限制),以及 PDS 主机解析(因为假设都是 bsky.social,会以你无法从自身修复的方式破坏自托管账号的功能)。
如果你正在做内容镜像同步,从 X 跨发布到 Bluesky 和从 Threads 跨发布到 Bluesky 这两篇文章都涵盖了在记录生效之前需要协调的文本长度和媒体差异问题。
简版总结
- 三个调用:
createSession、图片用的uploadBlob,以及带app.bsky.feed.post记录的createRecord。 - 无需应用审核,无需 OAuth 授权页面。使用由用户交给你的应用密码。
- 链接和提及需要带 UTF-8 字节偏移量的 facet,没有任何内容会被自动识别。
- 每篇帖子最多4张图片,每张最大1,000,000字节,总计最大2,000,000字节,必须填写替代文字。
- 写入操作每次创建消耗3点,配额为每小时5,000点、每天35,000点。
createSession限制为每5分钟30次。 - 账号可以运行在自托管的 PDS 上,不要硬编码主机地址。
与另外14个网络一起发布到 Bluesky
Bluesky 是相对简单的一个。同一款产品往往还需要支持 X(OAuth 2.0 PKCE、分块媒体上传、按请求计费)、Threads(Meta 应用审核、先创建容器再发布、60天令牌刷新)和 TikTok(发布前需通过 Content Posting API 审核)。每一个都有自己的认证方式、媒体处理流程和异步失败模型。
BulkPublish 提供一个 REST API,覆盖15个平台,包括 Bluesky,facet 计算、blob 尺寸调整和会话刷新均由服务器端处理。开发者文档和 REST API 参考包含了具体接口,定时发布 Bluesky 帖子一文则不涉及代码,介绍了具体做法。