From ea2d305a3c0c9dd30dda230d84639db92237722d Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 26 Mar 2026 20:51:05 +0800 Subject: [PATCH] feat: enable automatic web backend sessions --- .env.example | 4 + README.md | 18 +++ collector-service/app/core_main.py | 75 +++++++-- deploy/storyforge-collector.service.example | 4 + docs/CURRENT_PROJECT_STATE_2026-03-26.md | 2 + docs/PRODUCTION_BASELINE_2026-03-26.md | 5 + tests/test_production_baseline.py | 24 ++- web/storyforge-web-v4/assets/app.js | 170 +++++++++++--------- 8 files changed, 210 insertions(+), 92 deletions(-) diff --git a/.env.example b/.env.example index a7012e5..d374731 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,10 @@ COLLECTOR_N8N_BASE_URL=http://n8n:5678 BOOTSTRAP_SUPERADMIN_USERNAME=storyforge-admin BOOTSTRAP_SUPERADMIN_PASSWORD=__set_a_strong_password__ BOOTSTRAP_SUPERADMIN_DISPLAY_NAME=StoryForge Admin +WEB_AUTOLOGIN_ENABLED=0 +WEB_AUTOLOGIN_ACCOUNT_USERNAME= +WEB_AUTOLOGIN_USERNAME= +WEB_AUTOLOGIN_PASSWORD= N8N_ANALYSIS_WEBHOOK_PATH=/webhook/storyforge-analysis N8N_REAL_CUT_WEBHOOK_PATH=/webhook/storyforge-real-cut N8N_AI_VIDEO_WEBHOOK_PATH=/webhook/storyforge-ai-video diff --git a/README.md b/README.md index 3c99925..4201c28 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,22 @@ BOOTSTRAP_SUPERADMIN_USERNAME=storyforge-admin BOOTSTRAP_SUPERADMIN_PASSWORD=your_strong_admin_password ``` +如果希望 Web 端打开后直接自动建会话,不让用户手动输入账号密码,再额外打开: + +```bash +WEB_AUTOLOGIN_ENABLED=1 +WEB_AUTOLOGIN_ACCOUNT_USERNAME=your_existing_approved_username +``` + +推荐直接指定一个已经存在且已审批通过的账号用户名,服务端会直接为该账号签发自动会话,不需要额外保存该账号密码。 + +如果你更希望复用 bootstrap 超级管理员口令,或者切到专门账号,也可以继续走密码模式: + +```bash +WEB_AUTOLOGIN_USERNAME=your_autologin_username +WEB_AUTOLOGIN_PASSWORD=your_autologin_password +``` + 如果要让本机模型网关 `cli-proxy-api` 自动提供 `GLM-5`,建议在启动前确保本机环境里存在: ```bash @@ -151,6 +167,8 @@ N8N_BASE_URL=http://127.0.0.1:5670 `BOOTSTRAP_SUPERADMIN_USERNAME / BOOTSTRAP_SUPERADMIN_PASSWORD / BOOTSTRAP_SUPERADMIN_DISPLAY_NAME` 创建最高权限账号。未配置时不会再自动写入默认口令账号。 +如果开启了 `WEB_AUTOLOGIN_ENABLED=1`,前端会在启动时直接请求 `/v2/auth/auto-session` 自动建会话,不再显示用户名 / 密码 / token 输入流程。推荐优先使用 `WEB_AUTOLOGIN_ACCOUNT_USERNAME`,只在必须时才使用 `WEB_AUTOLOGIN_USERNAME / WEB_AUTOLOGIN_PASSWORD`。 + ## 当前架构 - `collector-service` 负责: diff --git a/collector-service/app/core_main.py b/collector-service/app/core_main.py index db01de9..f6f1325 100644 --- a/collector-service/app/core_main.py +++ b/collector-service/app/core_main.py @@ -53,6 +53,10 @@ ORCHESTRATOR_SHARED_SECRET = os.getenv("ORCHESTRATOR_SHARED_SECRET", "") BOOTSTRAP_SUPERADMIN_USERNAME = os.getenv("BOOTSTRAP_SUPERADMIN_USERNAME", "") BOOTSTRAP_SUPERADMIN_PASSWORD = os.getenv("BOOTSTRAP_SUPERADMIN_PASSWORD", "") BOOTSTRAP_SUPERADMIN_DISPLAY_NAME = os.getenv("BOOTSTRAP_SUPERADMIN_DISPLAY_NAME", "StoryForge Admin") +WEB_AUTOLOGIN_ENABLED = os.getenv("WEB_AUTOLOGIN_ENABLED", "0") +WEB_AUTOLOGIN_ACCOUNT_USERNAME = os.getenv("WEB_AUTOLOGIN_ACCOUNT_USERNAME", "") +WEB_AUTOLOGIN_USERNAME = os.getenv("WEB_AUTOLOGIN_USERNAME", "") +WEB_AUTOLOGIN_PASSWORD = os.getenv("WEB_AUTOLOGIN_PASSWORD", "") CUTVIDEO_BASE_URL = os.getenv("CUTVIDEO_BASE_URL", "http://192.168.31.18:7860") CUTVIDEO_API_KEY = os.getenv("CUTVIDEO_API_KEY", "") HUOBAO_BASE_URL = os.getenv("HUOBAO_BASE_URL", "http://127.0.0.1:5678") @@ -408,6 +412,32 @@ def bootstrap_superadmin_configured() -> bool: return bool(username) and not is_placeholder_config(password) +def env_flag(value: str | None) -> bool: + return normalize_config_value(value).lower() in {"1", "true", "yes", "on"} + + +def web_autologin_credentials() -> tuple[str, str]: + username = normalize_config_value(WEB_AUTOLOGIN_USERNAME) + password = normalize_config_value(WEB_AUTOLOGIN_PASSWORD) + if username or password: + return username, password + bootstrap_username, bootstrap_password, _ = bootstrap_superadmin_credentials() + return bootstrap_username, bootstrap_password + + +def web_autologin_account_username() -> str: + return normalize_config_value(WEB_AUTOLOGIN_ACCOUNT_USERNAME) + + +def web_autologin_configured() -> bool: + if not env_flag(WEB_AUTOLOGIN_ENABLED): + return False + if web_autologin_account_username(): + return True + username, password = web_autologin_credentials() + return bool(username) and not is_placeholder_config(password) + + def normalize_model_profile(row: dict[str, Any]) -> dict[str, Any]: return { "id": row["id"], @@ -439,6 +469,20 @@ def normalize_account(row: dict[str, Any]) -> dict[str, Any]: } +def issue_auth_token(account: dict[str, Any], *, mode: str = "password") -> dict[str, Any]: + token = secrets.token_urlsafe(32) + db.execute( + "INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)", + (token, account["id"], utc_now()), + ) + return { + "token": token, + "account": normalize_account(account), + "default_external_base_url": DEFAULT_EXTERNAL_BASE_URL, + "mode": mode, + } + + def model_profile_for_account(account_id: str, requested_id: str | None) -> dict[str, Any]: if requested_id: row = db.fetch_one( @@ -3168,6 +3212,7 @@ def healthz() -> dict[str, Any]: "liveRecorderBaseUrl": LIVE_RECORDER_BASE_URL, "orchestratorSecretConfigured": orchestrator_secret_configured(), "bootstrapSuperadminConfigured": bootstrap_superadmin_configured(), + "webAutoLoginConfigured": web_autologin_configured(), } @@ -3630,16 +3675,26 @@ def login(request: LoginRequest) -> dict[str, Any]: account = db.fetch_one("SELECT * FROM accounts WHERE username = ?", (request.username.strip(),)) if not account or not verify_password(request.password, account["password_hash"], account["password_salt"]): raise HTTPException(status_code=401, detail="Invalid credentials") - token = secrets.token_urlsafe(32) - db.execute( - "INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)", - (token, account["id"], utc_now()), - ) - return { - "token": token, - "account": normalize_account(account), - "default_external_base_url": DEFAULT_EXTERNAL_BASE_URL, - } + return issue_auth_token(account, mode="password") + + +@app.post("/v2/auth/auto-session") +def create_auto_session() -> dict[str, Any]: + if not web_autologin_configured(): + raise HTTPException(status_code=503, detail="Auto session is not configured on this deployment") + account_username = web_autologin_account_username() + if account_username: + account = db.fetch_one("SELECT * FROM accounts WHERE username = ?", (account_username,)) + if not account: + raise HTTPException(status_code=503, detail="Auto session account is missing on this deployment") + else: + username, password = web_autologin_credentials() + account = db.fetch_one("SELECT * FROM accounts WHERE username = ?", (username,)) + if not account or not verify_password(password, account["password_hash"], account["password_salt"]): + raise HTTPException(status_code=503, detail="Auto session credentials are invalid on this deployment") + if account["approval_status"] != "approved" and account["role"] != "super_admin": + raise HTTPException(status_code=403, detail="Auto session account is not approved") + return issue_auth_token(account, mode="auto") @app.post("/v2/auth/logout") diff --git a/deploy/storyforge-collector.service.example b/deploy/storyforge-collector.service.example index e9012f5..a590006 100644 --- a/deploy/storyforge-collector.service.example +++ b/deploy/storyforge-collector.service.example @@ -19,6 +19,10 @@ Environment=ORCHESTRATOR_SHARED_SECRET=__set_a_strong_shared_secret__ Environment=BOOTSTRAP_SUPERADMIN_USERNAME=storyforge-admin Environment=BOOTSTRAP_SUPERADMIN_PASSWORD=__set_a_strong_password__ Environment=BOOTSTRAP_SUPERADMIN_DISPLAY_NAME=StoryForge Admin +Environment=WEB_AUTOLOGIN_ENABLED=1 +Environment=WEB_AUTOLOGIN_ACCOUNT_USERNAME= +Environment=WEB_AUTOLOGIN_USERNAME= +Environment=WEB_AUTOLOGIN_PASSWORD= Environment=HUOBAO_BASE_URL=http://127.0.0.1:15678 Environment=CUTVIDEO_BASE_URL=http://127.0.0.1:17860 Environment=LIVE_RECORDER_BASE_URL=http://127.0.0.1:19106 diff --git a/docs/CURRENT_PROJECT_STATE_2026-03-26.md b/docs/CURRENT_PROJECT_STATE_2026-03-26.md index c43af86..0d71676 100644 --- a/docs/CURRENT_PROJECT_STATE_2026-03-26.md +++ b/docs/CURRENT_PROJECT_STATE_2026-03-26.md @@ -39,6 +39,7 @@ - 生产中心 - 复盘 - 额度与运维面板 + - 自动建会话连接 ## 当前量产基线 @@ -46,6 +47,7 @@ - `tenant_quota_profiles` 与 `tenant_usage_ledger` 已接入核心生产链,`explore/*`、`content-source-sync`、`reviews`、`real-cut`、`ai-video`、`assistants/{id}/generate`、`live-recorder create` 都会先做额度硬拦截,再记账。 - `jobs` 已补 `retry / requeue` 单任务入口,以及管理员批量重试失败任务入口,便于失败链路恢复。 - 仓库内已新增 SQLite 备份脚本,可在发布或故障前快速生成一致性快照。 +- Web 前端已改成固定后端自动建会话模式,不再要求用户手动输入账号密码;是否启用由服务端 `WEB_AUTOLOGIN_*` 环境变量控制,推荐直接用 `WEB_AUTOLOGIN_ACCOUNT_USERNAME` 绑定现有已审批账号。 ## 当前支持的平台 diff --git a/docs/PRODUCTION_BASELINE_2026-03-26.md b/docs/PRODUCTION_BASELINE_2026-03-26.md index 114469f..180b213 100644 --- a/docs/PRODUCTION_BASELINE_2026-03-26.md +++ b/docs/PRODUCTION_BASELINE_2026-03-26.md @@ -26,6 +26,11 @@ - `POST /v2/explore/jobs/{job_id}/retry` - `POST /v2/explore/jobs/{job_id}/requeue` - `POST /v2/admin/jobs/retry-failed` +- Web 已支持固定后端自动建会话: + - `POST /v2/auth/auto-session` + - 开关由 `WEB_AUTOLOGIN_ENABLED` 控制 + - 推荐使用 `WEB_AUTOLOGIN_ACCOUNT_USERNAME` 直接绑定现有已审批账号 + - 兼容 `WEB_AUTOLOGIN_USERNAME / WEB_AUTOLOGIN_PASSWORD` 或 bootstrap 超级管理员口令回退 - 仓库内已新增 SQLite 备份脚本: - `scripts/backup_storyforge_sqlite.sh` diff --git a/tests/test_production_baseline.py b/tests/test_production_baseline.py index 9bef8cc..429e373 100644 --- a/tests/test_production_baseline.py +++ b/tests/test_production_baseline.py @@ -31,6 +31,10 @@ class ProductionBaselineTests(unittest.TestCase): os.environ["JOBS_DIR"] = str(temp_root / "jobs") os.environ["MODELS_DIR"] = str(temp_root / "models") os.environ["ORCHESTRATOR_SHARED_SECRET"] = "test-secret" + os.environ["WEB_AUTOLOGIN_ENABLED"] = "1" + os.environ["WEB_AUTOLOGIN_ACCOUNT_USERNAME"] = "" + os.environ["WEB_AUTOLOGIN_USERNAME"] = "" + os.environ["WEB_AUTOLOGIN_PASSWORD"] = "" os.environ.setdefault("BOOTSTRAP_SUPERADMIN_USERNAME", "") os.environ.setdefault("BOOTSTRAP_SUPERADMIN_PASSWORD", "") @@ -97,6 +101,8 @@ class ProductionBaselineTests(unittest.TestCase): assistant_id = f"assistant_{tag}" token = f"token_{tag}" username = f"user_{tag}" + login_password = f"pass_{tag}" + password_hash, password_salt = self.core.create_password_hash(login_password) self.core.db.execute( """ @@ -108,8 +114,8 @@ class ProductionBaselineTests(unittest.TestCase): ( account_id, username, - "hash", - "salt", + password_hash, + password_salt, f"User {tag}", "super_admin", "approved", @@ -206,8 +212,22 @@ class ProductionBaselineTests(unittest.TestCase): "kb_id": kb_id, "assistant_id": assistant_id, "token": token, + "username": username, + "password": login_password, } + def test_auto_session_issues_token_without_manual_credentials(self) -> None: + ctx = self._seed_context("auto", exhausted=False) + self.core.WEB_AUTOLOGIN_ENABLED = "1" + self.core.WEB_AUTOLOGIN_ACCOUNT_USERNAME = ctx["username"] + self.core.WEB_AUTOLOGIN_USERNAME = "" + self.core.WEB_AUTOLOGIN_PASSWORD = "" + response = self.client.post("/v2/auth/auto-session") + self.assertEqual(response.status_code, 200, response.text) + payload = response.json() + self.assertEqual(payload["account"]["username"], ctx["username"]) + self.assertEqual(payload["mode"], "auto") + def test_database_uses_wal_and_busy_timeout(self) -> None: conn = self.core.db.connect() try: diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index d843013..97a2d26 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -52,6 +52,9 @@ const appState = { adminOpsOverview: null, adminFixRuns: [], recoveryRecords: [], + autoConnectAttempted: false, + autoConnectSuppressed: false, + autoConnectError: "", busy: false, message: "", lastAction: null, @@ -646,7 +649,7 @@ function ensureAuthUi() { const inline = document.createElement("div"); inline.className = "auth-inline"; inline.innerHTML = ` - + `; @@ -662,30 +665,22 @@ function ensureAuthUi() {

连接 StoryForge

-

先登录后端,再加载项目、对标、Agent 和生产数据。

+

当前站点会直接向后端请求自动会话,不再要求用户输入账号密码。

- +
-
- - -
-
- - -
-
- - +
+

自动连接说明

+

前端只会请求固定后端的自动会话接口;如果当前部署没有开启自动建会话,这里会直接显示服务端返回的原因。

- - + +
@@ -701,17 +696,17 @@ function renderAuthUi() { const logoutButton = document.querySelector('[data-action="logout-session"]'); const status = document.querySelector(".auth-status"); const message = document.querySelector('[data-role="auth-message"]'); - if (openButton) openButton.textContent = session ? "切换连接" : "连接后端"; + if (openButton) openButton.textContent = session ? "连接状态" : "自动连接"; if (logoutButton) logoutButton.hidden = !session; if (status) { status.textContent = appState.busy ? appState.message || "正在加载..." : session ? `${session.account?.display_name || session.account?.username || "已连接"} · ${session.backendUrl}` - : "未连接"; + : appState.autoConnectError || "等待自动连接"; } if (message) { - message.textContent = appState.busy ? appState.message : ""; + message.textContent = appState.busy ? appState.message : (appState.autoConnectError || ""); } } @@ -722,9 +717,6 @@ function openAuthModal() { const session = appState.session; modal.classList.remove("hidden"); setAuthField("backendUrl", session?.backendUrl || DEFAULT_BACKEND_URL); - setAuthField("username", session?.account?.username || ""); - setAuthField("password", ""); - setAuthField("token", ""); } function closeAuthModal() { @@ -737,12 +729,8 @@ function setAuthField(name, value) { } function readAuthForm() { - const pick = (name) => document.querySelector(`[data-auth-field="${name}"]`)?.value?.trim() || ""; return { - backendUrl: pick("backendUrl") || DEFAULT_BACKEND_URL, - username: pick("username"), - password: document.querySelector('[data-auth-field="password"]')?.value || "", - token: pick("token") + backendUrl: document.querySelector('[data-auth-field="backendUrl"]')?.value?.trim() || DEFAULT_BACKEND_URL }; } @@ -1114,36 +1102,44 @@ function backendSupports(path) { return API_CLIENT.backendSupports(path); } -async function loginWithForm() { - const auth = readAuthForm(); - if (!auth.backendUrl) { - throw new Error("请先填写后端地址"); - } - if (auth.token) { - const account = await storyforgeFetch("/v2/me", { - backendUrl: auth.backendUrl, - token: auth.token - }); - persistSession({ backendUrl: auth.backendUrl, token: auth.token, account }); - return; - } - if (!auth.username || !auth.password) { - throw new Error("请填写账号密码,或者直接填 Token"); - } - const payload = await storyforgeFetch("/v2/auth/login", { - backendUrl: auth.backendUrl, +async function loginWithAutoSession(backendUrl = DEFAULT_BACKEND_URL) { + const payload = await storyforgeFetch("/v2/auth/auto-session", { + backendUrl, auth: false, method: "POST", - body: { - username: auth.username, - password: auth.password - } + body: {} }); persistSession({ - backendUrl: auth.backendUrl, + backendUrl, token: payload.token, account: payload.account }); + appState.autoConnectError = ""; + appState.autoConnectAttempted = true; + appState.autoConnectSuppressed = false; +} + +async function ensureAutoSession(options = {}) { + const backendUrl = options.backendUrl || readAuthForm().backendUrl || DEFAULT_BACKEND_URL; + const force = Boolean(options.force); + if (appState.session && !force) { + return true; + } + if (appState.autoConnectSuppressed && !force) { + return false; + } + if (appState.autoConnectAttempted && !force) { + return Boolean(appState.session); + } + appState.autoConnectAttempted = true; + try { + await loginWithAutoSession(backendUrl); + return true; + } catch (error) { + appState.autoConnectError = formatActionErrorMessage(error, "自动连接失败"); + persistSession(null); + return false; + } } async function refreshFromAuthModal() { @@ -1153,16 +1149,10 @@ async function refreshFromAuthModal() { await bootstrap(); return; } - const auth = readAuthForm(); - const hasAnyInlineAuth = Boolean(auth.token || auth.username || auth.password); - const hasInlineAuth = Boolean(auth.token || (auth.username && auth.password)); - if (hasAnyInlineAuth && !hasInlineAuth) { - throw new Error("请填写账号密码,或者直接填 Token"); - } - if (hasInlineAuth) { - await loginWithForm(); - closeAuthModal(); - } + appState.autoConnectSuppressed = false; + appState.autoConnectAttempted = false; + await ensureAutoSession({ force: true }); + if (appState.session) closeAuthModal(); await bootstrap(); } @@ -1173,6 +1163,9 @@ async function logoutSession() { } } catch {} persistSession(null); + appState.autoConnectAttempted = true; + appState.autoConnectSuppressed = true; + appState.autoConnectError = "当前会话已退出。需要时可以点右上角重新自动连接。"; appState.me = null; appState.dashboard = null; appState.contentSources = []; @@ -1659,6 +1652,14 @@ async function loadPlatformAccount(platform, accountId, requestToken = 0) { async function bootstrap() { renderAll(); + if (!appState.session) { + setBusy(true, "正在自动连接后端..."); + try { + await ensureAutoSession(); + } finally { + setBusy(false, ""); + } + } if (!appState.session) { renderAuthUi(); return; @@ -1810,8 +1811,13 @@ async function bootstrap() { } } catch (error) { appState.message = error.message; - if (String(error.message || "").includes("401") || String(error.message || "").includes("Not authenticated")) { + if ( + String(error.message || "").includes("401") + || String(error.message || "").includes("Not authenticated") + || String(error.message || "").includes("Invalid token") + ) { persistSession(null); + appState.autoConnectAttempted = false; } } finally { appState.recoveryRecords = getRecoveryRecords(); @@ -3460,9 +3466,9 @@ function renderDashboardScreen() { if (!appState.session) { return screenShell( "项目总台", - "先连接后端,再加载项目、对标、Agent 和生产状态。", - `${button("连接后端", "open-auth", "primary")}`, - renderEmptyState("还没有连接 StoryForge", "登录后就能把项目总台替换成真实数据。") + "先自动连接工作区,再加载项目、对标、Agent 和生产状态。", + `${button("自动连接", "open-auth", "primary")}`, + renderEmptyState("还没有连接 StoryForge", "自动连接成功后,这里会替换成真实项目总台。") ); } if (!appState.dashboard) { @@ -3600,7 +3606,7 @@ function renderDashboardScreen() { function renderProjectsScreen() { if (!appState.dashboard) { - return screenShell("我的项目", "先连接工作区,再加载项目。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("项目未加载", "登录成功后,这里会显示真实项目和导入队列。")); + return screenShell("我的项目", "先完成工作区自动连接,再加载项目。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("项目未加载", "自动连接成功后,这里会显示真实项目和导入队列。")); } const projects = safeArray(appState.dashboard.projects); const selectedProject = getSelectedProject(); @@ -3662,7 +3668,7 @@ function renderProjectsScreen() { function renderDiscoveryScreen() { if (!appState.dashboard) { - return screenShell("找对标", "连接后端后才能加载真实对标账号。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "登录后这里会显示当前平台的账号列表和详情。")); + return screenShell("找对标", "完成工作区自动连接后才能加载真实对标账号。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("对标库未加载", "自动连接成功后,这里会显示当前平台的账号列表和详情。")); } const query = appState.discoveryQuery.toLowerCase(); const currentPlatform = getCurrentPlatformValue(); @@ -3904,7 +3910,7 @@ function renderDiscoveryScreen() { function renderTrackingScreen() { if (!appState.dashboard) { - return screenShell("跟踪账号", "登录后才能生成真实日报。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); + return screenShell("跟踪账号", "完成工作区自动连接后才能生成真实日报。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("日报未加载", "当前还没有可用的对标账号数据。")); } const currentPlatform = getCurrentPlatformValue(); const trackingAccountsPath = getWorkbenchRoute(currentPlatform, "trackingAccounts"); @@ -4043,7 +4049,7 @@ function renderAutomationScreen() { function renderOwnedScreen() { if (!appState.dashboard) { - return screenShell("我的账号", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("我的账号未加载", "登录后这里会展示当前账号和建议动作。")); + return screenShell("我的账号", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("我的账号未加载", "自动连接成功后,这里会展示当前账号和建议动作。")); } const me = appState.me || appState.session?.account || {}; const firstAssistant = safeArray(appState.dashboard.assistants)[0]; @@ -4073,7 +4079,7 @@ function renderOwnedScreen() { function renderPlaybookScreen() { if (!appState.dashboard) { - return screenShell("Agent", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("Agent 未加载", "登录后这里会展示真实 Agent 列表和模型。")); + return screenShell("Agent", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("Agent 未加载", "自动连接成功后,这里会展示真实 Agent 列表和模型。")); } const assistants = safeArray(appState.dashboard.assistants); const models = safeArray(appState.dashboard.model_profiles); @@ -4226,7 +4232,7 @@ function renderPlaybookScreen() { function renderProductionScreen() { if (!appState.dashboard) { - return screenShell("生产中心", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("生产中心未加载", "登录后这里会展示真实任务和作品。")); + return screenShell("生产中心", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("生产中心未加载", "自动连接成功后,这里会展示真实任务和作品。")); } const jobs = safeArray(appState.dashboard.recent_jobs); const activeJobs = jobs.filter((item) => item.status !== "completed").slice(0, 4); @@ -4340,7 +4346,7 @@ function renderProductionScreen() { function renderReviewScreen() { if (!appState.dashboard) { - return screenShell("发布与复盘", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("复盘未加载", "登录后这里会先用最近任务生成一版复盘入口。")); + return screenShell("发布与复盘", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("复盘未加载", "自动连接成功后,这里会先用最近任务生成一版复盘入口。")); } if (!backendSupports("/v2/reviews")) { return screenShell( @@ -4407,7 +4413,7 @@ function renderReviewScreen() { function renderCreditsScreen() { if (!appState.dashboard) { - return screenShell("额度", "先连接后端。", `${button("连接后端", "open-auth", "primary")}`, renderEmptyState("额度未加载", "后续接真实计费前,先用任务量做运营看板。")); + return screenShell("额度", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("额度未加载", "自动连接成功后,这里会展示真实额度和运营看板。")); } const jobs = safeArray(appState.dashboard.recent_jobs); return screenShell( @@ -7088,16 +7094,18 @@ document.addEventListener("click", async (event) => { return; } if (name === "submit-auth") { - setBusy(true, "正在登录并加载..."); + setBusy(true, "正在自动连接并加载..."); try { const message = document.querySelector('[data-role="auth-message"]'); if (message) message.textContent = ""; - await loginWithForm(); + appState.autoConnectSuppressed = false; + appState.autoConnectAttempted = false; + await ensureAutoSession({ force: true }); closeAuthModal(); await bootstrap(); } catch (error) { const message = document.querySelector('[data-role="auth-message"]'); - if (message) message.textContent = error.message; + if (message) message.textContent = formatActionErrorMessage(error, "自动连接失败"); } finally { setBusy(false, ""); } @@ -7111,7 +7119,7 @@ document.addEventListener("click", async (event) => { return; } if (name === "auth-refresh" || name === "refresh-data") { - setBusy(true, name === "auth-refresh" ? "正在连接并刷新..." : "正在刷新数据..."); + setBusy(true, name === "auth-refresh" ? "正在重新自动连接..." : "正在刷新数据..."); try { if (name === "auth-refresh") { const message = document.querySelector('[data-role="auth-message"]'); @@ -7123,7 +7131,7 @@ document.addEventListener("click", async (event) => { } catch (error) { const message = document.querySelector('[data-role="auth-message"]'); if (name === "auth-refresh" && message) { - message.textContent = error.message; + message.textContent = formatActionErrorMessage(error, "自动连接失败"); } else { alert("刷新数据失败: " + error.message); } @@ -7515,14 +7523,16 @@ document.addEventListener("submit", async (event) => { if (!(form instanceof HTMLFormElement)) return; if (form.dataset.role === "auth-form") { event.preventDefault(); - setBusy(true, "正在登录并加载..."); + setBusy(true, "正在自动连接并加载..."); try { - await loginWithForm(); + appState.autoConnectSuppressed = false; + appState.autoConnectAttempted = false; + await ensureAutoSession({ force: true }); closeAuthModal(); await bootstrap(); } catch (error) { const message = document.querySelector('[data-role="auth-message"]'); - if (message) message.textContent = error.message; + if (message) message.textContent = formatActionErrorMessage(error, "自动连接失败"); } finally { setBusy(false, ""); }