mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-09 09:27:09 +08:00
feat: support native Windows login browser
This commit is contained in:
@@ -313,7 +313,7 @@ tail -f logs/app.log
|
|||||||
A: 检查 `login_desktop_server.py` 是否正常运行,端口 18090 是否被占用。
|
A: 检查 `login_desktop_server.py` 是否正常运行,端口 18090 是否被占用。
|
||||||
|
|
||||||
**Q: 浏览器启动失败?**
|
**Q: 浏览器启动失败?**
|
||||||
A: 确保已安装 Playwright:`playwright install chromium`
|
A: Windows 本地模式请先运行 `.\scripts\start_login_desktop.ps1`;同时确保已安装 Playwright:`playwright install chromium`
|
||||||
|
|
||||||
**Q: 消息发送失败?**
|
**Q: 消息发送失败?**
|
||||||
A: 检查网络连接,查看 `logs/app.log` 或 Web 运行日志中的错误信息。
|
A: 检查网络连接,查看 `logs/app.log` 或 Web 运行日志中的错误信息。
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ from core.login import collect_login_result
|
|||||||
|
|
||||||
REMOTE_LOGIN_URL = "https://creator.douyin.com/"
|
REMOTE_LOGIN_URL = "https://creator.douyin.com/"
|
||||||
WWW_SELF_URL = "https://www.douyin.com/user/self"
|
WWW_SELF_URL = "https://www.douyin.com/user/self"
|
||||||
PROFILE_DIR = Path("/data/login-profile")
|
DEFAULT_PROFILE_DIR = (
|
||||||
|
Path(__file__).resolve().parents[1] / "state" / "login-profile"
|
||||||
|
if os.name == "nt"
|
||||||
|
else Path("/data/login-profile")
|
||||||
|
)
|
||||||
|
PROFILE_DIR = Path(os.getenv("LOGIN_PROFILE_DIR", str(DEFAULT_PROFILE_DIR))).expanduser()
|
||||||
|
LOGIN_DESKTOP_MODE = str(
|
||||||
|
os.getenv("LOGIN_DESKTOP_MODE", "native" if os.name == "nt" else "novnc")
|
||||||
|
).strip().lower()
|
||||||
|
if LOGIN_DESKTOP_MODE not in {"native", "novnc"}:
|
||||||
|
LOGIN_DESKTOP_MODE = "native" if os.name == "nt" else "novnc"
|
||||||
IDLE_TIMEOUT_SECONDS = max(300, int(os.getenv("LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS", "1800")))
|
IDLE_TIMEOUT_SECONDS = max(300, int(os.getenv("LOGIN_DESKTOP_IDLE_TIMEOUT_SECONDS", "1800")))
|
||||||
STOP_AFTER_EXPORT_SECONDS = max(0, int(os.getenv("LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS", "60")))
|
STOP_AFTER_EXPORT_SECONDS = max(0, int(os.getenv("LOGIN_DESKTOP_STOP_AFTER_EXPORT_SECONDS", "60")))
|
||||||
STATUS_CACHE_SECONDS = max(1, int(os.getenv("LOGIN_DESKTOP_STATUS_CACHE_SECONDS", "15")))
|
STATUS_CACHE_SECONDS = max(1, int(os.getenv("LOGIN_DESKTOP_STATUS_CACHE_SECONDS", "15")))
|
||||||
@@ -127,26 +137,32 @@ class LoginDesktopManager:
|
|||||||
pass
|
pass
|
||||||
self.playwright = None
|
self.playwright = None
|
||||||
self.playwright = await async_playwright().start()
|
self.playwright = await async_playwright().start()
|
||||||
|
launch_args = [
|
||||||
|
"--start-maximized",
|
||||||
|
"--window-position=0,0",
|
||||||
|
"--window-size=1600,1000",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--disable-sync",
|
||||||
|
"--disable-features=Translate,MediaRouter,OptimizationHints,AutofillServerCommunication",
|
||||||
|
]
|
||||||
|
if os.name != "nt":
|
||||||
|
launch_args.extend(
|
||||||
|
[
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--disable-gpu-compositing",
|
||||||
|
"--disable-software-rasterizer",
|
||||||
|
"--disable-accelerated-2d-canvas",
|
||||||
|
"--disable-accelerated-video-decode",
|
||||||
|
"--renderer-process-limit=2",
|
||||||
|
]
|
||||||
|
)
|
||||||
self.context = await self.playwright.chromium.launch_persistent_context(
|
self.context = await self.playwright.chromium.launch_persistent_context(
|
||||||
str(PROFILE_DIR),
|
str(PROFILE_DIR),
|
||||||
headless=False,
|
headless=False,
|
||||||
viewport={"width": 1600, "height": 1000},
|
viewport={"width": 1600, "height": 1000},
|
||||||
args=[
|
args=launch_args,
|
||||||
"--disable-dev-shm-usage",
|
|
||||||
"--no-sandbox",
|
|
||||||
"--start-maximized",
|
|
||||||
"--window-position=0,0",
|
|
||||||
"--window-size=1600,1000",
|
|
||||||
"--disable-gpu",
|
|
||||||
"--disable-gpu-compositing",
|
|
||||||
"--disable-software-rasterizer",
|
|
||||||
"--disable-accelerated-2d-canvas",
|
|
||||||
"--disable-accelerated-video-decode",
|
|
||||||
"--renderer-process-limit=2",
|
|
||||||
"--disable-background-networking",
|
|
||||||
"--disable-sync",
|
|
||||||
"--disable-features=Translate,MediaRouter,OptimizationHints,AutofillServerCommunication",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
self.page = self.context.pages[0] if self.context.pages else await self.context.new_page()
|
self.page = self.context.pages[0] if self.context.pages else await self.context.new_page()
|
||||||
|
|
||||||
@@ -173,13 +189,22 @@ class LoginDesktopManager:
|
|||||||
async def stop(self, clear_profile=False):
|
async def stop(self, clear_profile=False):
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if self.page:
|
if self.page:
|
||||||
await self.page.close()
|
try:
|
||||||
|
await self.page.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.page = None
|
self.page = None
|
||||||
if self.context:
|
if self.context:
|
||||||
await self.context.close()
|
try:
|
||||||
|
await self.context.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.context = None
|
self.context = None
|
||||||
if self.playwright:
|
if self.playwright:
|
||||||
await self.playwright.stop()
|
try:
|
||||||
|
await self.playwright.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.playwright = None
|
self.playwright = None
|
||||||
if clear_profile and PROFILE_DIR.exists():
|
if clear_profile and PROFILE_DIR.exists():
|
||||||
shutil.rmtree(PROFILE_DIR, ignore_errors=True)
|
shutil.rmtree(PROFILE_DIR, ignore_errors=True)
|
||||||
@@ -195,6 +220,15 @@ class LoginDesktopManager:
|
|||||||
if not self.context or self._context_is_closed():
|
if not self.context or self._context_is_closed():
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
|
async def focus_browser(self):
|
||||||
|
self.mark_activity()
|
||||||
|
page = await self._get_active_page()
|
||||||
|
try:
|
||||||
|
await page.bring_to_front()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True, "url": page.url, "mode": LOGIN_DESKTOP_MODE}
|
||||||
|
|
||||||
async def status(self):
|
async def status(self):
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if self._status_cache is not None and now - self._status_checked_at < STATUS_CACHE_SECONDS:
|
if self._status_cache is not None and now - self._status_checked_at < STATUS_CACHE_SECONDS:
|
||||||
@@ -505,6 +539,11 @@ async def close():
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/focus")
|
||||||
|
async def focus():
|
||||||
|
return await manager.focus_browser()
|
||||||
|
|
||||||
|
|
||||||
@app.post("/refresh-qr")
|
@app.post("/refresh-qr")
|
||||||
async def refresh_qr():
|
async def refresh_qr():
|
||||||
return await manager.refresh_login_qr()
|
return await manager.refresh_login_qr()
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$appRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$repoRoot = Split-Path -Parent $appRoot
|
||||||
|
Set-Location $appRoot
|
||||||
|
|
||||||
|
if (-not $env:LOGIN_DESKTOP_MODE) { $env:LOGIN_DESKTOP_MODE = "native" }
|
||||||
|
if (-not $env:LOGIN_DESKTOP_API_PORT) { $env:LOGIN_DESKTOP_API_PORT = "18090" }
|
||||||
|
if (-not $env:LOGIN_PROFILE_DIR) {
|
||||||
|
$env:LOGIN_PROFILE_DIR = Join-Path $repoRoot "state\login-profile"
|
||||||
|
}
|
||||||
|
|
||||||
|
$python = Join-Path $repoRoot ".venv\Scripts\python.exe"
|
||||||
|
if (-not (Test-Path $python)) {
|
||||||
|
$python = (Get-Command python -ErrorAction Stop).Source
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $env:LOGIN_PROFILE_DIR | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Starting Windows native login browser..."
|
||||||
|
Write-Host "Profile: $env:LOGIN_PROFILE_DIR"
|
||||||
|
Write-Host "API: http://127.0.0.1:$env:LOGIN_DESKTOP_API_PORT"
|
||||||
|
|
||||||
|
& $python .\login_desktop_server.py
|
||||||
@@ -188,6 +188,7 @@ class WebUiSafetyTests(unittest.TestCase):
|
|||||||
os.environ,
|
os.environ,
|
||||||
{
|
{
|
||||||
"SPARKFLOW_LOGIN_DESKTOP_PUBLIC_URL": "",
|
"SPARKFLOW_LOGIN_DESKTOP_PUBLIC_URL": "",
|
||||||
|
"SPARKFLOW_LOGIN_DESKTOP_MODE": "novnc",
|
||||||
},
|
},
|
||||||
clear=False,
|
clear=False,
|
||||||
),
|
),
|
||||||
@@ -198,6 +199,18 @@ class WebUiSafetyTests(unittest.TestCase):
|
|||||||
self.assertTrue(url.startswith("/login-desktop/proxy/vnc.html?"))
|
self.assertTrue(url.startswith("/login-desktop/proxy/vnc.html?"))
|
||||||
self.assertIn("path=login-desktop/proxy/websockify", url)
|
self.assertIn("path=login-desktop/proxy/websockify", url)
|
||||||
|
|
||||||
|
def test_login_desktop_defaults_to_native_mode_on_windows(self):
|
||||||
|
request = type("Request", (), {"url": type("Url", (), {"hostname": "example", "scheme": "http"})()})()
|
||||||
|
with (
|
||||||
|
patch.dict(os.environ, {"SPARKFLOW_LOGIN_DESKTOP_PUBLIC_URL": "", "SPARKFLOW_LOGIN_DESKTOP_MODE": "native"}, clear=False),
|
||||||
|
patch.object(app_module, "get_app_settings", return_value={}),
|
||||||
|
):
|
||||||
|
url = app_module.login_desktop_public_url(request)
|
||||||
|
if os.name == "nt":
|
||||||
|
self.assertEqual("", url)
|
||||||
|
else:
|
||||||
|
self.assertTrue(url.startswith("/login-desktop/proxy/vnc.html?"))
|
||||||
|
|
||||||
def test_login_desktop_http_proxy_requires_auth_and_forwards_assets(self):
|
def test_login_desktop_http_proxy_requires_auth_and_forwards_assets(self):
|
||||||
client = TestClient(app_module.app)
|
client = TestClient(app_module.app)
|
||||||
unauthenticated = client.get(
|
unauthenticated = client.get(
|
||||||
|
|||||||
@@ -229,6 +229,13 @@ def login_desktop_api_url():
|
|||||||
return str(configured or "http://127.0.0.1:18090").rstrip("/")
|
return str(configured or "http://127.0.0.1:18090").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def login_desktop_display_mode() -> str:
|
||||||
|
settings = get_app_settings(force_reload=True)
|
||||||
|
configured = os.getenv("SPARKFLOW_LOGIN_DESKTOP_MODE") or settings.get("login_desktop_mode")
|
||||||
|
mode = str(configured or ("native" if os.name == "nt" else "novnc")).strip().lower()
|
||||||
|
return mode if mode in {"native", "novnc"} else "novnc"
|
||||||
|
|
||||||
|
|
||||||
def login_desktop_public_url(request: Request) -> str:
|
def login_desktop_public_url(request: Request) -> str:
|
||||||
settings = get_app_settings(force_reload=True)
|
settings = get_app_settings(force_reload=True)
|
||||||
configured_url = str(
|
configured_url = str(
|
||||||
@@ -238,6 +245,8 @@ def login_desktop_public_url(request: Request) -> str:
|
|||||||
).strip()
|
).strip()
|
||||||
if configured_url:
|
if configured_url:
|
||||||
return configured_url
|
return configured_url
|
||||||
|
if login_desktop_display_mode() == "native":
|
||||||
|
return ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"/login-desktop/proxy/vnc.html"
|
"/login-desktop/proxy/vnc.html"
|
||||||
@@ -436,6 +445,7 @@ def create_app():
|
|||||||
"is_admin": bool(current_principal(request) and current_principal(request).get("role") == "admin"),
|
"is_admin": bool(current_principal(request) and current_principal(request).get("role") == "admin"),
|
||||||
"app_settings": public_app_settings(),
|
"app_settings": public_app_settings(),
|
||||||
"login_desktop_public_url": login_desktop_public_url(request),
|
"login_desktop_public_url": login_desktop_public_url(request),
|
||||||
|
"login_desktop_display_mode": login_desktop_display_mode(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -1393,6 +1403,28 @@ def create_app():
|
|||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
|
||||||
|
|
||||||
|
@app.post("/login-desktop/focus")
|
||||||
|
async def login_desktop_focus(request: Request):
|
||||||
|
maybe_redirect = require_user(request)
|
||||||
|
if maybe_redirect:
|
||||||
|
return JSONResponse({"redirect": "/login"}, status_code=401)
|
||||||
|
lock_error = login_lock_required(request, api=True)
|
||||||
|
if lock_error:
|
||||||
|
return lock_error
|
||||||
|
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)
|
||||||
|
heartbeat_login(
|
||||||
|
username=principal(request)["username"],
|
||||||
|
session_id=principal(request).get("session_id", ""),
|
||||||
|
ticket=str(form.get("ticket", "")),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = call_login_desktop("/focus", method="POST", payload={}, timeout=20)
|
||||||
|
return JSONResponse({"ok": True, "result": payload, "workspace": _workspace_payload(request)})
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return JSONResponse({"ok": False, "error": str(exc)}, status_code=503)
|
||||||
|
|
||||||
@app.get("/login-desktop/status")
|
@app.get("/login-desktop/status")
|
||||||
async def login_desktop_status(request: Request):
|
async def login_desktop_status(request: Request):
|
||||||
maybe_redirect = require_user(request)
|
maybe_redirect = require_user(request)
|
||||||
|
|||||||
@@ -1116,6 +1116,32 @@ textarea {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.native-login-panel {
|
||||||
|
min-height: 380px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--surface-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-panel > svg {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-panel p {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-mode .desktop-frame {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.desktop-frame-wrap {
|
.desktop-frame-wrap {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -338,6 +338,7 @@
|
|||||||
if (!root) return;
|
if (!root) return;
|
||||||
const section = document.getElementById("interactive-login-section");
|
const section = document.getElementById("interactive-login-section");
|
||||||
const csrfToken = root.dataset.csrfToken || "";
|
const csrfToken = root.dataset.csrfToken || "";
|
||||||
|
const displayMode = root.dataset.displayMode || "novnc";
|
||||||
const configuredPublicUrl = root.dataset.publicUrl || "";
|
const configuredPublicUrl = root.dataset.publicUrl || "";
|
||||||
const publicUrl = (() => {
|
const publicUrl = (() => {
|
||||||
if (!configuredPublicUrl) return "";
|
if (!configuredPublicUrl) return "";
|
||||||
@@ -350,6 +351,9 @@
|
|||||||
const runtimeState = document.getElementById("login-desktop-runtime-state");
|
const runtimeState = document.getElementById("login-desktop-runtime-state");
|
||||||
const statusText = document.getElementById("login-desktop-status-text");
|
const statusText = document.getElementById("login-desktop-status-text");
|
||||||
const frame = document.querySelector("[data-login-frame]");
|
const frame = document.querySelector("[data-login-frame]");
|
||||||
|
const frameWrap = document.querySelector(".desktop-frame-wrap");
|
||||||
|
const nativePanel = document.querySelector("[data-native-login]");
|
||||||
|
const copyLoginUrlButton = document.querySelector("[data-copy-login-url]");
|
||||||
const qrImage = document.querySelector("[data-login-qr]");
|
const qrImage = document.querySelector("[data-login-qr]");
|
||||||
const qrStatus = document.querySelector("[data-login-qr-status]");
|
const qrStatus = document.querySelector("[data-login-qr-status]");
|
||||||
let timer = null;
|
let timer = null;
|
||||||
@@ -357,6 +361,8 @@
|
|||||||
let countdownTimer = null;
|
let countdownTimer = null;
|
||||||
let qrRefreshTimer = null;
|
let qrRefreshTimer = null;
|
||||||
let workspace = { state: "closed", active: false, position: 0, ticket: "" };
|
let workspace = { state: "closed", active: false, position: 0, ticket: "" };
|
||||||
|
if (displayMode === "native" && copyLoginUrlButton) copyLoginUrlButton.hidden = true;
|
||||||
|
if (displayMode === "native" && frameWrap) frameWrap.hidden = true;
|
||||||
|
|
||||||
const setStatus = (text, tone = "") => {
|
const setStatus = (text, tone = "") => {
|
||||||
if (statusText) statusText.textContent = text;
|
if (statusText) statusText.textContent = text;
|
||||||
@@ -377,6 +383,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadFrame = (force = false) => {
|
const loadFrame = (force = false) => {
|
||||||
|
if (displayMode === "native") return;
|
||||||
if (frame && (force || frame.dataset.loaded !== "1") && frame.dataset.src) {
|
if (frame && (force || frame.dataset.loaded !== "1") && frame.dataset.src) {
|
||||||
frame.src = frame.dataset.src;
|
frame.src = frame.dataset.src;
|
||||||
frame.dataset.loaded = "1";
|
frame.dataset.loaded = "1";
|
||||||
@@ -384,6 +391,18 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeFrame = () => {
|
const closeFrame = () => {
|
||||||
|
if (nativePanel) nativePanel.hidden = true;
|
||||||
|
if (frameWrap) {
|
||||||
|
frameWrap.classList.remove("native-login-mode");
|
||||||
|
if (displayMode === "native") frameWrap.hidden = true;
|
||||||
|
}
|
||||||
|
if (qrImage) {
|
||||||
|
qrImage.hidden = true;
|
||||||
|
const previous = qrImage.dataset.objectUrl || "";
|
||||||
|
if (previous) URL.revokeObjectURL(previous);
|
||||||
|
delete qrImage.dataset.objectUrl;
|
||||||
|
qrImage.removeAttribute("src");
|
||||||
|
}
|
||||||
if (!frame) return;
|
if (!frame) return;
|
||||||
frame.removeAttribute("src");
|
frame.removeAttribute("src");
|
||||||
frame.dataset.loaded = "0";
|
frame.dataset.loaded = "0";
|
||||||
@@ -403,7 +422,16 @@
|
|||||||
if (workspace.state === "active" && workspace.active) {
|
if (workspace.state === "active" && workspace.active) {
|
||||||
const remaining = Math.max(0, Number(workspace.remaining_seconds || 0));
|
const remaining = Math.max(0, Number(workspace.remaining_seconds || 0));
|
||||||
const tone = remaining > 0 && remaining <= 60 ? "warning" : "success";
|
const tone = remaining > 0 && remaining <= 60 ? "warning" : "success";
|
||||||
setStatus(`登录工作区已分配给当前会话,剩余 ${remaining} 秒。完成扫码后请保存登录态。`, tone);
|
if (displayMode === "native") {
|
||||||
|
if (nativePanel) nativePanel.hidden = false;
|
||||||
|
if (frameWrap) {
|
||||||
|
frameWrap.hidden = false;
|
||||||
|
frameWrap.classList.add("native-login-mode");
|
||||||
|
}
|
||||||
|
setStatus(`Windows 本地登录浏览器已打开,剩余 ${remaining} 秒。完成扫码后请保存登录态。`, tone);
|
||||||
|
} else {
|
||||||
|
setStatus(`登录工作区已分配给当前会话,剩余 ${remaining} 秒。完成扫码后请保存登录态。`, tone);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus("登录工作区当前关闭。请从账号卡片点击“重新登录”。");
|
setStatus("登录工作区当前关闭。请从账号卡片点击“重新登录”。");
|
||||||
@@ -498,6 +526,20 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-focus-native-browser]").forEach((button) => {
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
await postForm("/login-desktop/focus", { ticket: workspace.ticket });
|
||||||
|
setStatus("已请求显示本机登录浏览器,请在桌面窗口中继续操作。", "success");
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(`显示登录浏览器失败:${error.message}`, "danger");
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll("[data-refresh-login-qr]").forEach((button) => {
|
document.querySelectorAll("[data-refresh-login-qr]").forEach((button) => {
|
||||||
button.addEventListener("click", async () => {
|
button.addEventListener("click", async () => {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>{% block title %}续火花{% endblock %}</title>
|
<title>{% block title %}续火花{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="/static/app.css?v=20260817">
|
<link rel="stylesheet" href="/static/app.css?v=20260824">
|
||||||
</head>
|
</head>
|
||||||
<body data-page="{% block page_key %}dashboard{% endblock %}">
|
<body data-page="{% block page_key %}dashboard{% endblock %}">
|
||||||
<header class="mobile-topbar">
|
<header class="mobile-topbar">
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script defer src="/static/lucide.min.js?v=20260710"></script>
|
<script defer src="/static/lucide.min.js?v=20260710"></script>
|
||||||
<script defer src="/static/app.js?v=20260823"></script>
|
<script defer src="/static/app.js?v=20260824"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
class="login-workspace-grid"
|
class="login-workspace-grid"
|
||||||
id="login-desktop-controls"
|
id="login-desktop-controls"
|
||||||
data-public-url="{{ login_desktop_public_url }}"
|
data-public-url="{{ login_desktop_public_url }}"
|
||||||
|
data-display-mode="{{ login_desktop_display_mode }}"
|
||||||
data-csrf-token="{{ csrf_token }}"
|
data-csrf-token="{{ csrf_token }}"
|
||||||
>
|
>
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
@@ -259,6 +260,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop-frame-wrap">
|
<div class="desktop-frame-wrap">
|
||||||
|
<div class="native-login-panel" data-native-login hidden>
|
||||||
|
<i data-lucide="monitor-up"></i>
|
||||||
|
<strong>Windows 本地登录浏览器已启动</strong>
|
||||||
|
<p class="muted compact">请在本机弹出的 Chromium 窗口中完成抖音扫码。扫码完成后回到这里点击“保存新账号登录”。</p>
|
||||||
|
<button class="button button-primary" type="button" data-focus-native-browser>
|
||||||
|
<i data-lucide="focus"></i><span>显示登录浏览器</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<iframe
|
<iframe
|
||||||
class="desktop-frame"
|
class="desktop-frame"
|
||||||
data-login-frame
|
data-login-frame
|
||||||
|
|||||||
@@ -128,10 +128,13 @@ pip install -r requirements-web.txt
|
|||||||
# 3. 安装 Playwright 浏览器
|
# 3. 安装 Playwright 浏览器
|
||||||
playwright install chromium
|
playwright install chromium
|
||||||
|
|
||||||
# 4. 启动 Web 服务
|
# 4. 启动 Windows 本地登录浏览器(另开一个 PowerShell)
|
||||||
|
.\scripts\start_login_desktop.ps1
|
||||||
|
|
||||||
|
# 5. 启动 Web 服务(再开一个终端)
|
||||||
python main.py --web
|
python main.py --web
|
||||||
|
|
||||||
# 5. 访问 http://localhost:8787
|
# 6. 访问 http://localhost:8787
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in New Issue
Block a user