Files
MediaCrawler/mac-agent-os-main/03_knowledge/99_system/matrix/douyin-login-pitfalls.md
T
butubb 2112c1a870
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 抖音 Web 接口客户端 —— 绕开爬虫子进程,直接发 HTTP
移植自 mac-agent-os 的 mediacrawler_adapter:不起子进程、不开页面,用浏览器里那份
登录态直接调抖音 Web 接口。产物键名照抄 store/douyin,所以 ingest 那条链路一个字不用改。

**目前能用的(真环境实测,非推断)**:

    profile/other : 200, 7075 字节  —— 博主主页指标(粉丝/获赞/作品数/昵称)
    aweme/detail  : 200, 45425 字节 —— 单条作品详情(含点赞/评论/收藏/分享)

**目前不能用的:作品列表 `aweme/post`。** 两个互相独立的原因:
  1. 这个接口被抖音单独升级成了真校验:不带 x-tt-argus 回 403「Uifid Not Found」,
     带上 dummy 值回 200 + **空 body**。也就是说「头在不在」骗得过,「真校验」过不了。
     同一套头打 profile/other 和 aweme/detail 都是通的 —— 抖音是挑着接口加保护的,
     挑中的恰好是「批量拉作品列表」这个最敏感的动作。
  2. 改走页面截获也不行:CDP 浏览器打开博主主页会落到「验证码中间页」(当天大量探测的
     代价,过几小时要重测)。

  所以现在的边界是:**已知作品的指标刷新能做,自动发现新作品做不了**。

**排查中控住变量后得到的两条事实**(都写进注释了):
  · `Accept` / `Accept-Language` / `Referer` 才是主页接口能返回真数据的原因 —— 只有
    UA+client hints+Cookie 时是 200 但仅 121 字节的空壳,补上这三个头变 7074 字节。
    (我先前猜的 sec-ch-ua 不是关键。)
  · 因此 UA 与 client hints 必须**成套地取自同一个浏览器**,所以 BrowserIdentity 一次
    从 CDP 取齐 cookie + UA + hints,而不是各自写死。

「200 + 空 body 必须当场报错」也是刻意写死的:放过去它会在下游变成「这个博主没作品」,
把一次失败伪装成一条正常结果 —— 爬虫那条路正是这么栽的,还被翻译成「账号被封」。

测试 +11:cookie 解析、请求头成套性(含 uifid 缺失/回退)、产物键名与 store 对齐、
以及 _get 的三条失败路径(空 body / 403 带网关原话 / 正常返回)。
2026-10-10 17:12:25 +08:00

13 KiB
Raw Blame History

title, tags, created, updated, nature
title tags created updated nature
抖音登录流程 — 全链路知识与踩坑记录
douyin
login
sms
verification
iframe
selector
timing
2026-06-28 2026-06-28 reference

抖音登录流程 — 全链路知识与踩坑记录

本文包含 2026-06-28 大规模调试中积累的所有登录经验。 适用于 douyin_ops.py 的 _ensure_logged_in() 和 sms_login() 方法。


一、登录状态检测

1.1 判断已登录的方法(按可靠性排序)

方法 选择器 可靠性 说明
data-e2e 用户信息区 [data-e2e="user-info"] ⭐⭐⭐ 最高 抖音官方 e2e 标记,有就必已登录
头像元素 [class*="avatar"] 或 [data-e2e*="avatar"] ⭐⭐ 中 部分页面没有这个选择器
登录按钮不可见 button:has-text("登录") 不可见 ⭐ 低 仅作兜底,不精确

1.2 判断未登录的方法

  • 右上角可见的**「登录」按钮**
  • 选择器:button:has-text("登录"), span:has-text("登录"), div:has-text("登录")
  • 注意:首页推荐流中这个按钮始终存在,要用 is_visible() 确认

1.3 关键注意事项

  • 首次打开抖音首页加载慢,至少要等 6 秒才能做登录检测(我们试过 2s→3s→5s→6s,6s 最稳定)
  • 登录弹窗可能在上次会话的 cookie 有效时不弹出,直接显示已登录状态
  • 登录成功后必须重新导航到 /user/self,因为弹窗处理改变了页面 URL

二、登录弹窗检测

2.1 弹窗位置:iframe

抖音的登录弹窗(passport 护照页)是在 iframe 中加载的,iframe URL 模式:

  • passport.douyin.com
  • sso.douyin.com
  • 域名含 login

2.2 弹窗选择器

用途 选择器 说明
弹窗容器 .second-verify-panel, .uc-ui-verify_sms-verify 覆盖新旧两版 class
二维码区域 div[class*=qrcode], img[class*=qrcode], canvas[class*=qrcode] 用于二维码检测

2.3 ⚠️ iframe 查找顺序(重要!)

必须先查 iframe,再查主页面!

❌ 错误做法(我们踩过的坑):
   1. 主页面查 verify_panel → 匹配到遮罩层(`.second-verify-panel`)
   2. login_frame = page(主页面)
   3. 在主页面搜手机输入框 → 找不到 ❌(因为输入框在 iframe 内)

✅ 正确做法(修复后):
   1. 遍历所有 iframe,找 passport/login 域的 verify_panel
   2. 找到 → login_frame = iframe
   3. 在 iframe 内搜手机输入框 → 能找到 ✅
   4. iframe 没找到 → fallback 到主页面

2.4 弹窗类型判断

弹窗可能有三种状态:

弹窗出现
  ├─ 一键登录 → 有 "一键登录" 按钮,无手机号输入框
  ├─ 验证码登录 → 有手机号输入框 + 获取验证码按钮
  └─ 二维码登录 → 有二维码图片/画布,需切 tab

一键登录特征

  • 选择器:button:has-text("一键登录"), span:has-text("一键登录")
  • 点击后等 5 秒
  • 可能直接登录成功,也可能跳到验证码页(继续走 sms)

验证码登录特征

  • 手机号输入框:input[placeholder*="手机"](匹配 placeholder="请输入手机号")
  • 验证码输入框:input[placeholder*="验证码"](匹配 placeholder="请输入验证码")
  • 获取验证码按钮:button:has-text("获取验证码")

二维码登录特征

  • 检测:div[class*=qrcode], img[class*=qrcode], canvas[class*=qrcode]
  • 处理:找到「验证码登录」tab 并点击切换

三、SMS 验证码流程

3.1 完整流程

1. 导航到登录页(如果不在)
2. 查找 passport iframe(最多 5 次尝试,每次间隔 2 秒)
3. 等 3 秒渲染
4. 检测二维码 → 切「验证码登录」tab
5. 找手机号输入框 → 填入手机号(3 次重试,每次间隔 1 秒)
6. 点击「获取验证码」
7. 轮询 SMS API 获取验证码(最多 6 次,共 60 秒超时)
8. 填入验证码
9. 点击「登录」
10. 判断是否登录成功

3.2 手机号输入框查找(带重试)

phone_sel = "input[placeholder*='手机'], input[type='tel'], input[name='mobile'], input[id*='phone'], input[id*='mobile']"
for retry in range(3):
    for sel in [phone_sel, "input:first-of-type"]:
        inp = await login_frame.query_selector(sel)
        if inp and await inp.is_visible():
            # 填入手机号
            break
    if phone_filled:
        break
    await asyncio.sleep(1)

注意:input:first-of-type 会匹配到首页搜索框,所以优先用 placeholder 选择器。

3.3 验证码输入框查找

code_sel = "input[placeholder*='验证码'], input[maxlength='6'], input[maxlength='4'], input[autocomplete='one-time-code']"

3.4 登录成功判断

判断方法 说明
URL 变化 "passport" in current_url → 仍在登录页 / not → 登录成功
右上角头像 [data-e2e="user-info"] 存在且可见 → 登录成功(兜底方案)

两者都用! 如果 URL 变了但右上角还没渲染出头像,等 3 秒再查一次。


四、时序控制(重要)

4.1 各步骤等待时间

步骤 等待时间 原因
首次打开后检测 6 秒 抖音首页加载慢,特别是第一打开,推荐流+弹窗需要完整渲染
切换 tab 后 3 秒 tab 切换动画 + 新内容渲染
填手机号后 1 秒 防抖 + 输入框状态同步
点击获取验证码后 2 秒 等待 SMS 发送
填入验证码后 1 秒 防抖
点击登录后 3-5 秒 等待页面跳转
登录成功后重导航 3 秒 等待 /user/self 渲染

4.2 超时控制

  • SMS 验证码轮询:最长 60 秒(6 次 × 10 秒)
  • 单次 iframe 查找:最长 10 秒(5 次 × 2 秒)
  • 页面 goto 超时:20 秒

五、选择器备忘(抖音 Web 版)

5.1 官方 data-e2e 选择器

这些是抖音官方的自动化测试标记,最稳定:

用途 选择器
用户信息区(已登录标志) [data-e2e="user-info"]
用户头像 [data-e2e="user-avatar"](不一定存在)
搜索输入框 [data-e2e="searchbar-input"]
点赞按钮 [data-e2e="video-player-digg"]
收藏按钮 [data-e2e="video-player-collect"]
分享按钮 [data-e2e="video-player-share"]
评论列表 [data-e2e="comment-list"]
评论点赞 [data-e2e="comment-digg"]
粉丝数 [data-e2e="user-info-fans"](值可能混合中文)
关注数 [data-e2e="user-info-follow"]
获赞数 [data-e2e="user-info-like"]

5.2 登录弹窗选择器(不定期变化)

这些 class 名由抖音前端动态生成,每次发布可能变化:

用途 6月28日已确认的 class
手机号输入框 yKcGN1NT X5L8kib_
验证码输入框 tnpNAdqe _Yqor1vk
获取验证码按钮 gKXDWaPG KX2Mhb6E
验证码 tab zad3EGHJ

因此,不要依赖 class 名,用 placeholder/text 代替。

5.3 推荐做法:文本/属性匹配

# ✅ 稳定
input[placeholder*="手机"]     # 匹配 "请输入手机号"
input[placeholder*="验证码"]    # 匹配 "请输入验证码"
button:has-text("获取验证码")   # 匹配按钮文字
div:has-text("验证码登录")      # 匹配 tab 文字

# ❌ 不稳定(class 会变)
.tnpNAdqe                      # 下个版本可能就改了
.yKcGN1NT                      # 同上

六、已踩过的坑(必读)

坑 1:logger 未定义导致 executor 闪退

现象:任务提交后状态一直 "running",实际已挂,slots 空但 active_tasks 有残留。

根因:executor.py 中使用了 logger.info() 但 logger = logging.getLogger("guardd.executor") 未定义,NameError 抛出,executor 闪退但任务状态来不及更新。

修复:添加 logger = logging.getLogger("guardd.executor")。

坑 2:主页面遮罩层匹配导致 iframe 检测失败

现象:弹窗出现了但手机号输入框找不到。

根因:verify_panel 选择器 .second-verify-panel 在主页面遮罩层上匹配成功,login_frame 设为主页面(不是 iframe),在主页面搜手机输入框当然搜不到。

修复:先查 iframe,再查主页面。

坑 3:登录后页面 URL 变化导致采集到首页数据

现象:采集任务显示完成,但 profiles.json 中昵称/粉丝数据为空或为首页标题。

根因:登录弹窗处理后,页面 URL 从 /user/self 变成了首页或其他地址,但 goto_profile() 继续刮取当前页面内容(首页推荐流),拿不到用户数据。

修复:登录成功后重新 goto("/user/self")。

坑 4:2 秒等待不够

现象:浏览器打开后鼠标立即动了,此时页面还没渲染完,弹窗还没出现。

根因:抖音首页首次加载~4-5 秒,2 秒时 _has_avatar() 判断为未登录(因为没渲染完),然后触发弹窗检测也找不到弹窗,最后点了"登录"按钮导致页面刷新。

修复:初始等待时间从 2 秒改为 6 秒。

坑 5:_build_cmd 按字段拼装导致 command_line 出错

现象:采集任务一直失败,executor 日志显示 --blueprints= 为空。

根因:executor._build_cmd() 从 task.get("blueprint") 读蓝图名,但 CommandBus 传的 blueprint 字段是空字符串(没传对),拼出了 mc run --blueprints= --rounds=1。

修复:优先使用 task.command_line(CommandBus 已渲染好的完整命令),_build_cmd 按字段拼装仅作 fallback。

坑 6:5kecheng 双 guardd 进程

现象:远程机器任务不执行,所有请求都返回空。

根因:SSH 部署时 pkill -9 -f guardd + nohup python3 guardd.py 启动了一个新实例,而 launchd 也管理了一个旧实例。两个 guardd 争抢端口 9090,其中一个没有 HTTP 服务。

修复:统一用 launchd 管理,不用 nohup。

坑 7:登录后 URL 不变(前端 overlay 形式)

现象:登录成功了但 sms_login 返回 False。

根因:有的登录弹窗是前端 overlay(不在 passport 域名下),URL 不变化。sms_login 只检查 URL,误判为失败。

修复:加 _has_avatar() 兜底判断。

坑 8:profiles.json 同步走本地文件违反联邦对等

现象:7kecheng 采集了数据,但 Dashboard 看不到。

根因:AccountService._get_profile_for_account() 直接读本机 profiles.json,不走 guardd API。

修复:改为统一通过 guardd HTTP API (/accounts/profiles) 读取,本机远程无差别。


七、录制文件分析指南

录制的 JSON 文件位于 agent-local/tools/matrix/recordings/。

7.1 关键字段含义

字段 含义
t 事件类型(click/hover/key/scroll)
tag HTML 标签
text 元素文本内容
cls 元素的 CSS class(关键!)
placeholder input 元素的占位符
type input 类型
e2e data-e2e 属性值
vis 元素是否可见
before_state.selectors 录制前页面状态快照
before_state.text_snippet 页面文本片段

7.2 如何从录制中提取特征码

# 找手机号输入框
grep -B5 -A3 "请输入手机号" recording_xxx.json

# 找验证码输入框
grep -B5 -A3 "请输入验证码" recording_xxx.json

# 找登录按钮
grep -B5 -A3 "获取验证码" recording_xxx.json

# 查看页面文本快照(了解页面状态)
grep "text_snippet" recording_xxx.json | head -1 | python3 -c "import json,sys;print(json.load(sys.stdin)['text_snippet'][:500])"

八、蓝图登录检测覆盖范围

当前 _ensure_logged_in() 被以下操作调用:

蓝图操作 在流程中调用位置
goto_home execute_op() 中 goto 首页后
dy_goto_profile execute_op() 中 goto 个人主页后
goto_profile 独立方法中被调用
_ensure_logged_in 自身 无弹窗时递归调用

设计原则

所有涉及浏览器导航的操作(goto_home、goto_url、goto_profile)都应该调用 _ensure_logged_in()。目前 goto_url 还没有加,待补。


九、相关文件索引

文件 内容
douyin_ops.py 抖音原子操作 + _ensure_logged_in + sms_login
matrix_modules/account/sms_login.py SMS 登录模块
matrix_modules/account/douyin_login.py 抖音登录模块
matrix_modules/account/login_state_machine.py 登录状态机
03_knowledge/99_system/matrix/matrix-known-pitfalls.md 已知坑(浏览器/系统)
03_knowledge/99_system/matrix/matrix-sms-verification.md SMS 验证码自动接收配置