mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-07 00:17:20 +08:00
feat: support native Windows login browser
This commit is contained in:
@@ -229,6 +229,13 @@ def login_desktop_api_url():
|
||||
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:
|
||||
settings = get_app_settings(force_reload=True)
|
||||
configured_url = str(
|
||||
@@ -238,6 +245,8 @@ def login_desktop_public_url(request: Request) -> str:
|
||||
).strip()
|
||||
if configured_url:
|
||||
return configured_url
|
||||
if login_desktop_display_mode() == "native":
|
||||
return ""
|
||||
|
||||
return (
|
||||
"/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"),
|
||||
"app_settings": public_app_settings(),
|
||||
"login_desktop_public_url": login_desktop_public_url(request),
|
||||
"login_desktop_display_mode": login_desktop_display_mode(),
|
||||
}
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
@@ -1393,6 +1403,28 @@ def create_app():
|
||||
except RuntimeError as exc:
|
||||
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")
|
||||
async def login_desktop_status(request: Request):
|
||||
maybe_redirect = require_user(request)
|
||||
|
||||
@@ -1116,6 +1116,32 @@ textarea {
|
||||
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 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
if (!root) return;
|
||||
const section = document.getElementById("interactive-login-section");
|
||||
const csrfToken = root.dataset.csrfToken || "";
|
||||
const displayMode = root.dataset.displayMode || "novnc";
|
||||
const configuredPublicUrl = root.dataset.publicUrl || "";
|
||||
const publicUrl = (() => {
|
||||
if (!configuredPublicUrl) return "";
|
||||
@@ -350,6 +351,9 @@
|
||||
const runtimeState = document.getElementById("login-desktop-runtime-state");
|
||||
const statusText = document.getElementById("login-desktop-status-text");
|
||||
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 qrStatus = document.querySelector("[data-login-qr-status]");
|
||||
let timer = null;
|
||||
@@ -357,6 +361,8 @@
|
||||
let countdownTimer = null;
|
||||
let qrRefreshTimer = null;
|
||||
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 = "") => {
|
||||
if (statusText) statusText.textContent = text;
|
||||
@@ -377,6 +383,7 @@
|
||||
};
|
||||
|
||||
const loadFrame = (force = false) => {
|
||||
if (displayMode === "native") return;
|
||||
if (frame && (force || frame.dataset.loaded !== "1") && frame.dataset.src) {
|
||||
frame.src = frame.dataset.src;
|
||||
frame.dataset.loaded = "1";
|
||||
@@ -384,6 +391,18 @@
|
||||
};
|
||||
|
||||
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;
|
||||
frame.removeAttribute("src");
|
||||
frame.dataset.loaded = "0";
|
||||
@@ -403,7 +422,16 @@
|
||||
if (workspace.state === "active" && workspace.active) {
|
||||
const remaining = Math.max(0, Number(workspace.remaining_seconds || 0));
|
||||
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;
|
||||
}
|
||||
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) => {
|
||||
button.addEventListener("click", async () => {
|
||||
button.disabled = true;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>{% block title %}续火花{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/app.css?v=20260817">
|
||||
<link rel="stylesheet" href="/static/app.css?v=20260824">
|
||||
</head>
|
||||
<body data-page="{% block page_key %}dashboard{% endblock %}">
|
||||
<header class="mobile-topbar">
|
||||
@@ -139,7 +139,7 @@
|
||||
</dialog>
|
||||
|
||||
<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 %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
class="login-workspace-grid"
|
||||
id="login-desktop-controls"
|
||||
data-public-url="{{ login_desktop_public_url }}"
|
||||
data-display-mode="{{ login_desktop_display_mode }}"
|
||||
data-csrf-token="{{ csrf_token }}"
|
||||
>
|
||||
<div class="login-actions">
|
||||
@@ -259,6 +260,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
class="desktop-frame"
|
||||
data-login-frame
|
||||
|
||||
Reference in New Issue
Block a user