45 min · 精读 skills/shared/scripts/login_state.py + Web 端 LOGIN_RUNNERS
🎒 预备知识:完成上一课「L11」(零基础入口:第 0 课)
先想登录到底要经过什么:打开登录页 → 人拿出手机扫码 → 平台校验 → (可能)人再收短信输验证码 → 拿到凭证。两个特征:① 中间有人的参与, 耗时不可预测(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:
每个平台登记自己的 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。