`sau douyin login` fails end-to-end after the 2026 Q2 creator-center refactor
Summary
python sau_cli.py douyin login --account <name> (or the equivalent WebUI
"login" button) fails to log any account in. Three independent breakages,
all on the same refactored creator center UI, all needed to land before
the flow is usable again.
This is observed in our environment on
patchright 1.x / playwright 1.5x / Chromium 145+ against
https://creator.douyin.com/creator-micro/content/upload as of 2026-06.
Steps to reproduce
sau douyin login --account <your_account_name>Expected: QR code is printed to the terminal, the user scans with the
Douyin app, optional SMS code is entered, the cookie is written to
cookies/douyin_<account_name>.json, and a follow-up
sau douyin check --account <name> returns valid.
Actual: one of three things goes wrong depending on the moment of the race:
_extract_douyin_qrcode_srctimes out at 30s with no QR extracted. Log line:Locator.wait_for: Timeout 30000ms exceeded. waiting for get_by_text("扫码登录").first to be visible- After the QR is forced to extract manually,
_wait_for_douyin_logintimes out at 120s even after a successful scan, with log line登录失败: 等待抖音扫码登录超时. The cookie is never written. sau douyin checkreturnsinvalidfor cookies that are actually still valid, becausecookie_authis racing the SPA.
Root causes
(1) QR selector no longer matches the page
Old selector chain (commit 90e01c0-ish):
scan_login_tab = page.get_by_text("扫码登录", exact=True).first
qrcode_img = (
scan_login_tab
.locator("..")
.locator("xpath=following-sibling::div[1]")
.locator('img[aria-label="二维码"]')
.first
)This worked when the login page rendered the QR inside a sibling <div>
with aria-label="二维码". The current page is a single-tab layout
(class single_tab-...); the QR lives inside
div#animate_qrcode_container as a base64 PNG with no aria-label.
(2) _wait_for_douyin_login only waits for sessionid cookie
sessionid is set after the 2FA / SMS code step completes, not when the
user scans. For accounts that trigger 2FA, the URL changes immediately
on the phone's "confirm" tap, but the cookie does not appear for
several seconds while the user is typing the SMS code. A 120s timeout
is not enough; the loop also needs to recognise that the page is in a
"verification in progress" state and not bail.
(3) cookie_auth returns a non-deterministic answer
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
try:
await page.wait_for_url(".../upload", timeout=5000)
except Exception:
return False
if await page.get_by_text("手机号登录").count() or await page.get_by_text("扫码登录").count():
return False
return Truepage.goto returns at the load event but the SPA has not finished
hydrating. wait_for_url is satisfied immediately (the URL is
unchanged after a soft-redirect). The next get_by_text(...).count()
fires inside the race window and sometimes returns 0 even when the
login card is on the page, yielding a false-positive "valid" answer.
Calling the function twice in a row gives different results; we
verified this empirically (5 calls, mixed True/False) on the
maintainer-built cookie from a known-valid account.
Fix (proposed)
A branch fix/douyin-cli-login-overhaul is being prepared with four
atomic commits:
fix(douyin): overhaul CLI auto-login to handle new creator center UI- New selector chain with a 4-tier fallback (id -> class partial ->
container partial -> legacy aria-label) preceded by
wait_for_load_state("networkidle"). - New
cookie_authpolling loop (10s) that returns invalid if neither the login-card text nor the upload-page text is observed. - New
_wait_for_douyin_loginloop that detects 2FA input fields and logs a warning so the user knows to type the SMS code, with timeout raised to 200s.
- New selector chain with a 4-tier fallback (id -> class partial ->
container partial -> legacy aria-label) preceded by
fix(export_douyin_cookie): read USERNAME from os.environ in embedded python- The shell passed USERNAME as an env var to a python heredoc, but
the python block referenced it as a local. NameError on every
run. One-line fix:
USERNAME = os.environ.get('USERNAME', '').
- The shell passed USERNAME as an env var to a python heredoc, but
the python block referenced it as a local. NameError on every
run. One-line fix:
feat(ks): switch default channel to chromium and add cdp-url support- patchright's bundled launcher does not always resolve the system
"chrome" channel cleanly. Fall back to bundled "chromium" by
default;
conf.py:LOCAL_CHROME_PATHstill works for real Chrome. - New
cdp_urlparameter onks_setup/get_ks_cookieto drive the user's already-running real Chrome viaconnect_over_cdp. This is the only way to get past strict anti-bot fingerprint checks on some accounts.
- patchright's bundled launcher does not always resolve the system
"chrome" channel cleanly. Fall back to bundled "chromium" by
default;
chore(xiaohongshu): switch default channel to chromium- Same as the ks uploader.
Out of scope / notes
sau_cli.pyhas an unrelatedregister_cli_cookie_to_webuiblock that we are intentionally not proposing upstream; it is a private-deployment feature and is left in the local working tree.- The
channel="chrome" -> channel="chromium"change is a compatibility fix for patchright, not a functional regression for users on plainplaywright;LOCAL_CHROME_PATHstill points at real Chrome for those who want it. - We are not in a position to test the proposed fix against every
Douyin account type, but the selector chain is verified live on
https://creator.douyin.com/.
Happy to adjust scope, split into smaller PRs, or move any of the above into a separate issue if the maintainer prefers.
Source: dreammis/social-auto-upload