M5 执行层 · L12

发布引擎总览:七个平台,一套登录状态机

45 min · 精读 skills/shared/scripts/login_state.py + Web 端 LOGIN_RUNNERS

🎒 预备知识:完成上一课「L11」(零基础入口:第 0 课)

本课唯一结论:登录是分钟级长流程,所以 Web 后端不直接管浏览器—— 它只读写一个原子写的 JSON 状态文件。状态机是发布系统的心脏。

一、基础:为什么登录是"长流程",状态机是什么

先想登录到底要经过什么:打开登录页 → 人拿出手机扫码 → 平台校验 → (可能)人再收短信输验证码 → 拿到凭证。两个特征:① 中间有人的参与, 耗时不可预测(10 秒到 5 分钟都可能);② 参与者不止一个——跑浏览器的后台脚本、 展示二维码的 Web 页面、输验证码的用户。

多个不可预测的参与者协作,工程上的标准解法是有限状态机(FSM): 把流程拆成有限个明确的"状态",任何时刻系统处于且只处于一个状态,状态迁移由明确事件触发。 这样每个参与者不需要知道其他人在干什么,只需要:读当前状态 → 干自己那步 → 写新状态。

那"状态"存在哪、怎么共享?Easel 的答案是文件(login_state.py): 登录脚本和 Web 后端是两个独立进程,不共享内存——但共享同一个文件系统。 脚本推进状态就改写 JSON 文件,Web 轮询读它。为什么不用 RPC/消息队列? 登录一跑几分钟,常驻连接太重;状态文件让任意进程在任何时刻都能读到最后已知状态, 崩溃后遗留的文件还是排查证据。这叫"文件即协议"。

二、状态机逐态走读(源码注释原文)

# login_state.py:7-9(原文)
# 状态机:starting → qr_ready → [scanned] → [sms_required → verifying] → success | expired | error
# (sms_required:扫码后平台风控要求短信验证,runner 等前端回填验证码——见 read_sms_code;
#  verifying:已拿到验证码、正在提交校验,前端显示转圈;校验失败会退回 sms_required 让重输)

七个状态各自回答一个问题:starting(浏览器起了吗)→ qr_ready (二维码出来了,Web 端立刻展示给用户)→ scanned(用户扫了,等平台确认)→ sms_required(风控加验,等人输码)→ verifying(码已提交,转圈)→ 三个终态:success / expired(码过期重扫)/ error。

两个工程细节值得停一下。① 原子写:JSON 文件可能正被 Web 读取时被脚本更新—— 直接覆写会让读者拿到"写了一半"的损坏 JSON。解法是先写临时文件再 rename (原子操作,任何时刻读者要么看到旧完整版要么看到新完整版)。② 一次性验证码文件: 抖音短信码走 outputs/_login/douyin.code,脚本读走即删—— 用过即毁防重放;.resend 后缀文件作重发信号。文件名即协议,零基础设施。

三、七平台 × 三种登录形态

形态平台登录态存储为什么不同
Playwright 持久化目录小红书/抖音/快手/视频号/知乎/公众号~/.easel-browser-profiles/<X>Profile网页后台,浏览器是唯一接口(A16)
TV 端扫码 API(无浏览器)B 站项目根 cookies.json(biliup 兼容)B 站有公开 TV 端扫码协议——API 优先于浏览器,能用 API 不开浏览器
官方 API 凭证公众号(--official-api 回退)yaml 配置官方 API 需 app_secret + IP 白名单,所以网页会话是默认、API 是回退

小红书的特殊降级:无头浏览器被风控拦截(300012)时,脚本自动改开 CloakBrowser 有头窗口扫码(xhs_publish.py:135/:684)——无头是常规路径,不是保底(正课 L13 展开)。

登录态永不入库:profiles 在用户家目录 ~/.easel-browser-profiles/, AGENTS.md 明令禁止提交真实 Cookie——登录态=身份(A16),入库=把身份放进 git 历史。

四、Web 端怎么消费:LOGIN_RUNNERS 注册表

Web 后端对七个平台不做七份代码——一张注册表 LOGIN_RUNNERS: 每个平台登记自己的 runner 脚本路径和参数,POST /api/login/{platform} 按 注册表 Popen 后台脚本,然后轮询状态文件渲染二维码。加第八个平台 = 加一行注册, 不动引擎。这就是"引擎 + 注册表"模式:变化的(平台差异)与不变的(状态机/文件协议)分离。

五、常见踩坑

坑 1:轮询不设终局。Web 一直轮询状态文件——expired/error 是终态, 轮询必须能停。坑 2:验证码文件不删。用过的 code 留在磁盘 = 凭证泄露面。 坑 3:状态写进日志而非文件。脚本 stdout 的进度只有活着的进程能看到; 状态文件死了也在。

六、检索练习

七、出口检验

画出抖音完整登录时序(含短信墙分支),标注:每一步谁写状态文件、Web 在哪一步轮询、 验证码文件的生命周期(生成→消费→销毁)。讲给我听。

一手源:skills/shared/scripts/login_state.py 全文 + web/app.py 的 LOGIN_RUNNERS。