mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-08-29 03:57:07 +08:00
fix: regenerate expired Douyin login QR
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user