diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js
index 926d6c0..cd12593 100644
--- a/web/storyforge-web-v4/assets/app.js
+++ b/web/storyforge-web-v4/assets/app.js
@@ -618,6 +618,17 @@ function hasSessionBackendMismatch(expectedBackendUrl = DEFAULT_BACKEND_URL) {
return Boolean(expected && current && expected !== current);
}
+function formatBackendDisplayLabel(value = DEFAULT_BACKEND_URL) {
+ const normalized = normalizeBackendUrlValue(value || DEFAULT_BACKEND_URL);
+ if (!normalized) return "未配置后端";
+ try {
+ const parsed = new URL(normalized);
+ return parsed.host || normalized;
+ } catch {
+ return normalized;
+ }
+}
+
function compareDateDesc(leftValue, rightValue) {
return new Date(rightValue || 0).getTime() - new Date(leftValue || 0).getTime();
}
@@ -6930,11 +6941,45 @@ function renderCreditsScreen() {
const usage = appState.tenantUsage || quota?.usage || {};
const categories = usage?.categories || {};
const estimatedVideoUsage = (categories.ai_video?.quantity || 0) + (categories.real_cut?.quantity || 0);
+ const budgetAmount = (quota?.monthly_budget_cents || usage?.total_cost_cents || 0) / 100;
+ const usedAmount = (usage?.total_cost_cents || 0) / 100;
+ const quotaProtectionLabel = quota?.enabled === false ? "额度保护关闭" : "额度保护开启";
+ const riskLabel = quota?.storage_over_limit ? "存储超限" : "当前风险可控";
return screenShell(
"额度",
"在接真实计费前,先按任务量给出运营看板。",
`${button("刷新", "refresh-data")}`,
`
+
+
+ 当前额度任务
+ ${escapeHtml(quotaProtectionLabel)}
+
+
${escapeHtml(
+ quota
+ ? `先确认预算 ${formatNumber(budgetAmount)} 元和已用 ${formatNumber(usedAmount)} 元,再决定是继续放量还是先收紧高成本动作。`
+ : "先看预算和高成本动作的预估消耗,再决定是否要继续做视频或剪辑任务。"
+ )}
+
+ ${actionTag("刷新额度", "refresh-data")}
+ ${actionTag("去生产中心", "goto-production")}
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({
+ sourceScreen: "credits",
+ sourceActionKey: "credits-main-agent-handoff",
+ intentKey: "custom",
+ title: "评估当前额度和预算风险",
+ goal: "评估当前额度和预算风险",
+ summary: "让主 Agent 结合当前预算、已用额度和高成本动作,给出下一步建议。",
+ planSteps: ["读取当前额度看板", "判断预算与高成本动作风险", "给出下一步控制建议"]
+ }))}
+
+
+
+ 预算 ${escapeHtml(formatNumber(budgetAmount))} 元
+ ${escapeHtml(riskLabel)}
+ ${escapeHtml(formatNumber(categories.copy?.quantity || jobs.filter((item) => item.line_type === "analysis").length))} 条文案
+ ${escapeHtml(formatNumber(estimatedVideoUsage || jobs.filter((item) => item.line_type === "ai_video" || item.line_type === "real_cut").length))} 次视频
+
文案消耗预估${escapeHtml(formatNumber(categories.copy?.quantity || jobs.filter((item) => item.line_type === "analysis").length))}
本周期预算${escapeHtml(formatNumber((quota?.monthly_budget_cents || usage?.total_cost_cents || 0) / 100))}
@@ -6992,17 +7037,49 @@ function renderSettingsScreen() {
{ value: "display", label: "界面与帮助" }
];
const activeTab = getActiveDetailTab("settingsDetailTab", tabs);
+ const sessionConnected = Boolean(session);
+ const settingsHandoffAttrs = buildMainAgentHandoffAttrs({
+ sourceScreen: "settings",
+ sourceActionKey: "settings-main-agent-handoff",
+ intentKey: "custom",
+ title: "检查当前设置和工作区连接",
+ goal: "检查当前设置和工作区连接",
+ summary: "让主 Agent 读取当前连接状态、工作区和常用入口,再给出下一步建议。",
+ planSteps: ["读取当前连接与项目上下文", "确认当前工作区和入口状态", "生成下一步建议"]
+ });
return screenShell(
"设置",
"这里不放系统治理内容,只处理当前用户需要理解的连接、界面和帮助信息。",
`${button("连接状态", "open-auth")} ${isSuperAdmin() ? button("管理员配置台", "goto-admin-workbench", "primary") : button("刷新", "refresh-data", "primary")}`,
`
-
+
设置与帮助
把连接状态、当前工作区和使用说明放在一起,避免和管理员控制面混在同一页。
+
+
+ 当前设置任务
+ ${escapeHtml(sessionConnected ? "已自动连接" : "等待连接")}
+
+
${escapeHtml(
+ sessionConnected
+ ? `当前已经连到 ${session?.account?.display_name || session?.account?.username || "当前工作区"},先确认工作区和常用入口,再决定是否切项目或回到业务页。`
+ : "先确认当前站点是否已自动连接到工作区,再决定是重试连接还是回到业务页。"
+ )}
+
+ ${actionTag("打开连接状态", "open-auth")}
+ ${project ? actionTag("去我的项目", "goto-intake") : ""}
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", settingsHandoffAttrs)}
+
+
+
+ ${escapeHtml(sessionConnected ? "已自动连接" : "等待连接")}
+ ${escapeHtml(project?.name || "未选项目")}
+ ${escapeHtml(formatBackendDisplayLabel(session?.backendUrl || DEFAULT_BACKEND_URL))}
+ ${escapeHtml(activeTab === "workspace" ? "连接与工作区" : "界面与帮助")}
+
${renderDetailTabs("settingsDetailTab", tabs)}
${activeTab === "workspace" ? `
diff --git a/web/storyforge-web-v4/index.html b/web/storyforge-web-v4/index.html
index de07518..7f43aa3 100644
--- a/web/storyforge-web-v4/index.html
+++ b/web/storyforge-web-v4/index.html
@@ -4,7 +4,7 @@
-
StoryForge Web V4 Prototype
+ StoryForge Workbench
@@ -1687,7 +1687,7 @@
-
+
-
+
@@ -1941,7 +1941,7 @@
-
+
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
index 7c14183..be4e992 100644
--- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs
+++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs
@@ -287,6 +287,8 @@ test("remaining mobile workbench screens expose focus cards and compact summarie
const automation = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
const owned = extractBetween(APP, "function renderOwnedScreen()", "function renderPlaybookScreen()");
const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()");
+ const credits = extractBetween(APP, "function renderCreditsScreen()", "function renderSettingsScreen()");
+ const settings = extractBetween(APP, "function renderSettingsScreen()", "function renderTopbar()");
assert.match(projects, /mobile-only mobile-flow-focus-card/);
assert.match(projects, /当前项目任务/);
@@ -312,6 +314,16 @@ test("remaining mobile workbench screens expose focus cards and compact summarie
assert.match(review, /当前复盘任务/);
assert.match(review, /mobile-only compact-summary-row/);
assert.match(review, /已保存/);
+
+ assert.match(credits, /mobile-only mobile-flow-focus-card/);
+ assert.match(credits, /当前额度任务/);
+ assert.match(credits, /mobile-only compact-summary-row/);
+ assert.match(credits, /预算/);
+
+ assert.match(settings, /mobile-only mobile-flow-focus-card/);
+ assert.match(settings, /当前设置任务/);
+ assert.match(settings, /mobile-only compact-summary-row/);
+ assert.match(settings, /已自动连接/);
});
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {