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, "");
}