fix: regenerate expired Douyin login QR

This commit is contained in:
Rixuan Shao
2026-07-11 22:38:52 +08:00
parent 1cf15e8a6b
commit 712167b0fb
6 changed files with 58 additions and 4 deletions
+2
View File
@@ -2,6 +2,8 @@
## 2026-07-11
- Changed QR refresh from re-reading an expired image to forcing the Douyin login page to generate a new cache-busted QR code, and stopped serving expired QR screenshots.
- Fixed a blank Douyin login desktop by preventing build-time localhost proxy variables from leaking into runtime Chromium and explicitly wiring lowercase/uppercase runtime proxy variables.
- Added an authenticated same-origin noVNC HTTP/WebSocket proxy and synchronous mobile popup handling so login tasks work from phones without exposing port 8788 publicly.
+25 -2
View File
@@ -1,6 +1,7 @@
import asyncio
import os
import shutil
import time
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request, Response
@@ -176,13 +177,27 @@ class LoginDesktopManager:
}
async def open_login(self):
await self.refresh_login_qr()
async def refresh_login_qr(self):
refresh_url = f"{REMOTE_LOGIN_URL}?qr_refresh={int(time.time() * 1000)}"
try:
page = await self._get_active_page()
await page.goto(REMOTE_LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
await page.goto(refresh_url, wait_until="domcontentloaded", timeout=60000)
except Exception:
await self.reset()
page = await self._get_active_page()
await page.goto(REMOTE_LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
await page.goto(refresh_url, wait_until="domcontentloaded", timeout=60000)
await page.locator('img[class*="qrcode"]').first.wait_for(state="visible", timeout=30000)
try:
await page.wait_for_function(
"() => !/\u4e8c\u7ef4\u7801\u5931\u6548|\u4e8c\u7ef4\u7801\u8fc7\u671f/.test(document.body?.innerText || '')",
timeout=30000,
)
except Exception:
pass
return {"ok": True, "url": page.url}
async def export(self):
page = await self._get_active_page()
@@ -319,6 +334,11 @@ async def reset():
return {"ok": True}
@app.post("/refresh-qr")
async def refresh_qr():
return await manager.refresh_login_qr()
@app.post("/export")
async def export():
page = await manager._get_active_page()
@@ -335,6 +355,9 @@ async def export():
@app.get("/qr")
async def login_qr():
page = await manager._get_active_page()
expired = await page.locator('[class*="qrcode_expired"]').count()
if expired and await page.locator('[class*="qrcode_expired"]').first.is_visible():
raise HTTPException(status_code=409, detail="login QR code has expired")
selectors = (
'img[class*="qrcode"]',
'img[src^="data:image/png;base64"]',
@@ -114,7 +114,10 @@ class DeploymentContractTests(unittest.TestCase):
def test_login_desktop_exposes_cropped_qr_endpoint(self):
server = (SOURCE_ROOT / "login_desktop_server.py").read_text(encoding="utf-8")
self.assertIn('@app.get("/qr")', server)
self.assertIn('@app.post("/refresh-qr")', server)
self.assertIn("qr_refresh=", server)
self.assertIn('img[class*="qrcode"]', server)
self.assertIn('qrcode_expired', server)
self.assertIn('Cache-Control": "no-store, max-age=0', server)
def test_legacy_unused_entrypoints_are_removed(self):
@@ -195,6 +195,7 @@ class WebUiSafetyTests(unittest.TestCase):
self.assertLess(block.index("window.open(publicUrl"), block.index('await postForm("/login-desktop/open")'))
self.assertIn("refreshLoginQr(1800)", block)
self.assertIn("retries - 1", script)
self.assertIn('/login-desktop/qr/refresh', script)
def test_schedule_sync_writes_configured_window_to_shared_spool(self):
with tempfile.TemporaryDirectory() as temp_dir:
+15 -1
View File
@@ -1014,11 +1014,25 @@ def create_app():
headers={"Cache-Control": "no-store, max-age=0"},
)
except urllib.error.HTTPError as exc:
status = 404 if exc.code == 404 else 502
status = exc.code if exc.code in {404, 409} else 502
return PlainTextResponse("login QR code is not ready", status_code=status)
except (urllib.error.URLError, TimeoutError):
return PlainTextResponse("login QR service is unavailable", status_code=502)
@app.post("/login-desktop/qr/refresh")
async def login_desktop_qr_refresh(request: Request):
maybe_redirect = require_user(request)
if maybe_redirect:
return JSONResponse({"redirect": "/login"}, status_code=401)
form = await request.form()
if not validate_csrf(request, str(form.get("csrf_token", ""))):
return JSONResponse({"ok": False, "error": "Invalid CSRF token"}, status_code=403)
try:
payload = call_login_desktop("/refresh-qr", method="POST", payload={}, timeout=90)
return JSONResponse({"ok": True, "result": payload})
except RuntimeError as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
@app.get("/login-desktop/status")
async def login_desktop_status(request: Request):
maybe_redirect = require_user(request)
+12 -1
View File
@@ -462,7 +462,18 @@
});
document.querySelectorAll("[data-refresh-login-qr]").forEach((button) => {
button.addEventListener("click", () => refreshLoginQr());
button.addEventListener("click", async () => {
button.disabled = true;
if (qrStatus) qrStatus.textContent = "正在让抖音重新生成二维码...";
try {
await postForm("/login-desktop/qr/refresh");
refreshLoginQr(900);
} catch (error) {
if (qrStatus) qrStatus.textContent = `刷新二维码失败:${error.message}`;
} finally {
button.disabled = false;
}
});
});
document.querySelectorAll(".login-desktop-save").forEach((button) => {