BISMIH / 接口学习

BILIBILI / 每周图文

理解会话续期与图文投稿

从一次正常扫码授权,到图片上传、Opus 投稿、回执保存和平台状态确认。

接口边界

本项目由青龙调用 Python 脚本,未开放 Bilibili HTTP 代理。下列路径属于 B站上游平台;worker.py bilibili 是服务器脚本入口,不能直接当作 Item 的 HTTP API 调用。

1. 登录与认证

调用顺序:扫码授权 → nav 核验指定 UID → 按需续期 → 上传图片 → 持久保存投稿预留 → 提交一次 → 查询稿件状态。Cookie 和令牌只在自己的服务器私有配置中保存。这个文档站没有凭据输入框,也不会向平台发请求。

凭据用途保存方式
SESSDATA网页登录会话。受保护配置,不写日志或 Git。
bili_jctCookie 中的 CSRF 值;写接口另传到表单或查询参数。与对应会话一同更新。
DedeUserID登录返回的账户编号;必须与指定发布 UID 相符。由服务器验证,不接受静默改绑。
refresh_token与该次会话配套的续期令牌,不是 Cookie 字段。正常授权时保存,轮换后原子更新。
GEThttps://passport.bilibili.com/x/passport-login/web/qrcode/generate生成短时授权请求

返回 data.url 和 data.qrcode_key。用自己的 B站 App 扫码确认,再以该 key 查询下面的接口;不要将真实授权链接或 key 发布到公共页面。

GEThttps://passport.bilibili.com/x/passport-login/web/qrcode/poll查询参数:qrcode_key

观察到的 data.code:86101 等待扫码,86090 等待手机确认,86038 已过期,0 授权完成。成功时同时接收 Set-Cookie 与 data.refresh_token,再核验指定账号。二维码约三分钟有效;网页已登录不等于这次服务器授权已完成。

GEThttps://api.bilibili.com/x/web-interface/nav只读 · 带自己的会话 Cookie
{"code": 0, "data": {"isLogin": true, "mid": 42}}

这是脱敏字段示例。必须同时检查 code == 0、isLogin == true、mid == 指定发布 UID,以及登录 Cookie 中的 UID。失败立即停止,不上传到错误账号。

只读 Python 示例:检查自己的会话,不输出 Cookie
import json
import requests
from pathlib import Path

cfg = json.loads(Path('/etc/bismih-cron-task/config.json').read_text())
with requests.Session() as s:
    s.trust_env = False
    r = s.get('https://api.bilibili.com/x/web-interface/nav',
              headers={'User-Agent': 'Mozilla/5.0',
                       'Referer': 'https://www.bilibili.com/',
                       'Cookie': cfg['bili_cookie']},
              allow_redirects=False, timeout=(5, 15))
    r.raise_for_status()
    value = r.json()
    identity = value.get('data') or {}
    print({'logged_in': value.get('code') == 0 and identity.get('isLogin') is True,
           'expected_account': str(identity.get('mid')) == str(cfg['bili_expected_uid'])})

仅供服务器管理员在受保护环境中运行。网络异常请输出固定错误码,不打印包含认证材料的原始请求或响应。

2. Cookie 按需续期

Cookie 已失效且没有配套刷新令牌时,需要正常重新授权;不能从 Cookie 补造令牌。已有令牌也不代表会话永不过期。

步骤平台请求输入与结果
检查GET passport.bilibili.com/x/passport-login/web/cookie/info带会话 Cookie,可传 csrf。返回 data.refresh 和毫秒 timestamp;false 时不刷新。
取得短时口令GET www.bilibili.com/correspond/1/{correspondPath}对 refresh_{timestamp} 使用对应公钥及 RSA-OAEP / SHA-256 生成十六进制路径,从 HTML 的 div#1-name 读取 refresh_csrf。
轮换POST passport.bilibili.com/x/passport-login/web/cookie/refresh表单:旧 csrf、refresh_csrf、source=main_web、旧 refresh_token。接收新 Cookie 和新 refresh_token。
先验证并保存GET api.bilibili.com/x/web-interface/nav核对新会话属于指定 UID,原子持久化新 Cookie、新令牌及待确认的旧令牌。
最后确认POST passport.bilibili.com/x/passport-login/web/confirm/refresh表单:新 csrf 和 旧 refresh_token;这一步会使旧令牌对应的会话失效。

刷新 POST 前写私有备份与 inflight journal;结果不明时停止再次轮换,保留恢复材料。若只是最终确认失败,下一次只继续确认,不能重新开始整个刷新链。未通过新账号验证前,不确认旧令牌失效。

协议参考:Bilibili Cookie 刷新流程。这是社区整理的网页协议,非承诺长期稳定的官方开放 API。本项目在 bili_refresh.py 中实现并保留脱敏结果。

3. 逐张上传正文图片

POSThttps://api.bilibili.com/x/article/creative/article/upcover写入 · Cookie · multipart/form-data
字段位置含义
csrf表单当前会话的 bili_jct。
binary文件已校验图片,带文件名和对应图片 MIME。
{"code": 0, "data": {"url": "https://article.biliimg.com/example-image.jpg"}}

返回值必须为成功并带允许域名的图片 URL;对协议相对地址、HTTP 前缀做规范化后仍检查域名。本地记录图片宽、高和 KiB 大小。每张以 weekly-illust-{date}:bili-image:{illust_id} 保存回执,后续重试跳过已确认上传的图片。

4. 只提交一次 Opus 图文

POSThttps://api.bilibili.com/x/dynamic/feed/create/opus写入 · Cookie · application/json

查询参数为 csrf 和 gaia_source=main_web。正文由 worker.article_payload() 构造;下面展示关键结构,完整 JSON 示例可从学习规格下载。

{
  "raw_content": "",
  "opus_req": {
    "upload_id": "由服务端生成的本次上传标识",
    "scene": 12,
    "opus": {
      "opus_source": 2, "title": "Pixiv 每周插画榜 · 示例日期",
      "content": {"paragraphs": ["文本与图片段落对象,见完整示例"]},
      "article": {"category_id": 15, "originality": 0,
                  "reproduced": 1, "biz_tags": ["Pixiv", "插画", "周榜"]},
      "pub_info": {"editor_version": "eva3-4.0.0"}
    },
    "meta": {"app_meta": {"mobi_app": "web", "from": "create.creative.h5"}},
    "option": {"aigc": 2, "close_comment": 0,
               "up_choose_comment": 0, "private_pub": 2}
  }
}

这是结构讲解,paragraphs 中的说明字符串不能直接投稿。完整示例包含文本 nodes、图片 pics 和首图 cover;文本 font_level 使用字符串 regular,不能用整数 1。其他枚举沿用当前已验证编辑器载荷,不凭字段名猜测语义。

发请求前以 weekly-illust-{date}:bilibili-article 保存 pending。成功时保存 dyn_id_str/dyn_id 和 dyn_rid/aid;不使用实时生成的新 upload_id 替代本地防重复记录。

{"code": 0, "data": {"dyn_id_str": "123456789", "dyn_rid": 456789}}

收到唯一编号只代表平台接受了提交。POST 断线、非 JSON 或缺少稿件编号时,保留 pending,进入待核对;不能自动再发一篇。明确的结构化拒绝才释放该次投稿预留。

5. 提交与公开状态分别确认

GEThttps://api.bilibili.com/x/article/creative/article/view只读 · Cookie · 查询参数 aid

aid 使用刚保存的文章编号。当前实现仅在 code:0 且 data.state 为 0 或 9 时确认发布;其他值保持待核对。例如本次实测先返回 -2,后来才取得明确发布状态,不能把首次提交回执当成公开成功。

每日预检会读取同一条已提交回执;只有与最近那次运行编号匹配的发布确认,才能让 Item 转绿。原始投稿审计记录保留,即使其当时状态为 needs_review;新确认不会重发文章。

错误或阶段处理
-101登录失效,停止写入并恢复正常授权。
-111CSRF 校验失败,核对该次会话对应的 bili_jct。
86095(刷新)核对 refresh_csrf 及会话与 refresh_token 的配套关系。
HTTP 403 / 412 / 429、验证页面停止本轮,不用无限重试或轮换地址掩盖失败。
有投稿回执,公开状态未确认只读核对,保留回执;Item 显示红灯“待平台确认”。
POST 结果不明保留 pending 和恢复材料,管理员核对后处理。

6. 脚本入口与定时监控

此命令会真实上传和投稿。仅在已部署青龙容器中由管理员调用;本次文档发布不会执行它。

python3 /ql/data/scripts/bismih-job.py bilibili

生产每周一北京时间 09:40 运行,使用同一份已采集周榜。当前没有周榜则停止空白发布;已有投稿回执则读取确认,不再次提交。

脚本接口返回或职责
bilibili_auth.run('generate') / run('poll')管理员生成官方短时二维码、接收授权并验证原发布 UID。异常账号待确认,不自动改绑。
bili_refresh.maintain(config_path)在共享配置锁中按需续期;返回 ok、code 和刷新令牌是否存在,不返回凭据值。
worker.bilibili()上传、投稿及查询;成功返回 summary 和 metrics,失败交由工作引擎记录。
publisher_health.py --preflight每日预检与按需续期,同时对已提交稿件做只读状态确认。

正常会话维护的脱敏返回示意:

{"ok": true, "code": "SESSION_VALID", "refresh_token_present": true}

这不表示本次发生了令牌轮换。只有平台要求刷新时才执行刷新链;真正轮换并确认后返回 SESSION_REFRESHED。维护记录与私有恢复材料不公开。

每日 07:40 预检、每 5 分钟读取本机状态。绿灯要求青龙启用且周计划正确、每日预检未超过 36 小时、最近应执行的周任务成功或同一稿件已通过发布状态核对。失败、超期或待确认不能因心跳更新变绿。

实现对照:bilibili_auth.py、bili_refresh.py、worker.py 和 publisher_health.py。基线实测于 2026-10-26;上游为网页协议,平台字段可能变化。阅读本文不需要登录;源码仓库保持私有。