From a53a591c05fc26248cd942238435ed87ef6ddf64 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 30 Mar 2026 16:55:17 +0800 Subject: [PATCH] feat: sharpen live quota and review workspaces --- web/storyforge-web-v4/assets/app.js | 114 +++++++++++++++--- .../tests/workbench-pages.test.mjs | 14 +++ 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 98f1756..278c52f 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -3949,6 +3949,44 @@ function renderTenantQuotaPanel() { } const categories = usage?.categories || {}; const recentItems = safeArray(usage?.recent_items); + const categoryEntries = Object.values(categories || {}).sort((left, right) => (right?.cost_cents || 0) - (left?.cost_cents || 0)); + const topCategory = categoryEntries[0] || null; + const hasHardLimit = Boolean( + (quota?.monthly_budget_cents || 0) > 0 || + (quota?.storage_limit_bytes || 0) > 0 || + (quota?.analysis_quota || 0) > 0 || + (quota?.copy_quota || 0) > 0 || + (quota?.ai_video_quota || 0) > 0 || + (quota?.real_cut_quota || 0) > 0 || + (quota?.recorder_quota || 0) > 0 + ); + const usageCount = recentItems.length; + const quotaTaskTitle = quota?.storage_over_limit + ? "先处理存储超限" + : quota?.enabled === false + ? "先恢复额度保护" + : !hasHardLimit + ? "先补项目额度策略" + : usageCount + ? "先检查本周期消耗" + : "先跑出第一条计量"; + const quotaTaskSummary = quota?.storage_over_limit + ? "当前项目已经命中存储上限,先调整额度或清理产物,再继续高成本动作。" + : quota?.enabled === false + ? "额度保护已关闭,当前项目会按无限制模式运行,建议尽快恢复预算与动作保护。" + : !hasHardLimit + ? "当前项目虽然可继续运行,但还没有预算和动作配额,先把保护线补齐更稳。" + : usageCount + ? "本周期已经开始消耗额度,先看最主要的消耗来源,再决定是否收紧策略。" + : "额度已经建好,但当前周期还没有实际计量,先触发一次真实动作更容易校准策略。"; + const quotaTaskActionLabel = quota?.storage_over_limit || !hasHardLimit || quota?.enabled === false ? "调整额度" : usageCount ? "查看最近计量" : "去跑动作"; + const quotaTaskAction = quotaTaskActionLabel === "去跑动作" ? "goto-production" : quotaTaskActionLabel === "查看最近计量" ? "" : "open-tenant-quota"; + const usagePeriodLabel = usage?.cycle_start ? formatDateTime(usage.cycle_start).slice(0, 10) : "本周期"; + const quotaHealthTags = [ + quota?.enabled === false ? `额度保护关闭` : `额度保护开启`, + quota?.storage_over_limit ? `存储超限` : `存储正常`, + topCategory ? `${escapeHtml(`主要消耗 ${topCategory.category || "usage"}`)}` : `本周期未产生消耗` + ]; const cards = [ { label: "预算", value: `${formatNumber((quota?.monthly_budget_cents || 0) / 100)} 元`, sub: `已用 ${formatNumber((usage?.total_cost_cents || 0) / 100)} 元` }, { label: "分析配额", value: formatNumber(quota?.analysis_quota || 0), sub: `已用 ${formatNumber(categories.analysis?.quantity || 0)}` }, @@ -3959,22 +3997,36 @@ function renderTenantQuotaPanel() { ]; return `
-
-
-

租户额度与审计

-
预算、动作配额和最近计量都按租户 + 项目隔离。
+
+
+

租户额度与审计

+
预算、动作配额和最近计量都按租户 + 项目隔离,首屏先看风险和下一步。
+
+
+ ${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")} + ${quota?.storage_over_limit ? `存储超限` : ""} + 编辑额度 +
-
- ${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")} - ${quota?.storage_over_limit ? `存储超限` : ""} - 编辑额度 +
+

${escapeHtml(quotaTaskTitle)}

+

${escapeHtml(quotaTaskSummary)}

+
+ ${quotaTaskAction ? `${escapeHtml(quotaTaskActionLabel)}` : `${escapeHtml(quotaTaskActionLabel)}`} + ${quotaHealthTags.join("")} +
-
- ${quotaNotice} -
- ${cards.map((item) => ` -
- ${escapeHtml(item.label)} +
+ ${escapeHtml(`周期 ${usagePeriodLabel}`)} + ${escapeHtml(`计量 ${formatNumber(usageCount)} 条`)} + ${escapeHtml(`成本 ${(usage?.total_cost_cents || 0) / 100} 元`)} + ${escapeHtml(`主要消耗 ${topCategory?.category || "暂无"}`)} +
+ ${quotaNotice} +
+ ${cards.map((item) => ` +
+ ${escapeHtml(item.label)} ${escapeHtml(item.value)} ${escapeHtml(item.sub)}
@@ -6700,6 +6752,23 @@ function renderReviewScreen() { const project = getSelectedProject(); const completed = safeArray(appState.dashboard.recent_jobs).filter((item) => item.status === "completed").slice(0, 4); const reviews = getProjectReviews(project?.id || "").slice(0, 8); + const verdictCounts = reviews.reduce((acc, review) => { + const key = review?.verdict?.trim() || "待补结论"; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + const topVerdict = Object.entries(verdictCounts).sort((left, right) => right[1] - left[1])[0] || null; + const publishedReviewCount = reviews.filter((review) => review.publish_url || review.published_at).length; + const reviewTaskTitle = completed.length + ? "先把最近完成任务写成复盘" + : reviews.length + ? "先回看高频结论" + : "先跑出第一条可复盘任务"; + const reviewTaskSummary = completed.length + ? "最近已经有完成任务,先沉淀成结构化复盘,再决定是否回到生产继续放大。" + : reviews.length + ? "当前项目已经有复盘沉淀,先看出现次数最多的结论,再决定下一步继续做什么。" + : "当前还没有复盘和完成任务,先去生产中心跑出一条完整链路。"; const reviewHandoffAttrs = buildMainAgentHandoffAttrs({ sourceScreen: "review", sourceActionKey: "review-handoff", @@ -6728,7 +6797,18 @@ function renderReviewScreen() {
已保存${escapeHtml(formatNumber(reviews.length))}
最近完成${escapeHtml(formatNumber(completed.length))}
当前项目${escapeHtml(project?.name || "未选项目")}
-
下一步${escapeHtml(completed.length ? "写复盘" : "回生产")}
+
高频结论${escapeHtml(topVerdict ? topVerdict[0] : "待补结论")}
+
+
+
+

${escapeHtml(reviewTaskTitle)}

+

${escapeHtml(reviewTaskSummary)}

+
+ ${actionTag(completed.length ? "写复盘" : reviews.length ? "看复盘" : "去生产", completed.length ? "open-review-from-job" : reviews.length ? "goto-review" : "goto-production", completed[0]?.id ? `data-job-id="${escapeHtml(completed[0].id)}"` : "")} + ${actionTag("交给主 Agent", "handoff-to-main-agent", reviewHandoffAttrs)} + ${escapeHtml(`已保存 ${formatNumber(reviews.length)} 条`)} + ${escapeHtml(`已发布 ${formatNumber(publishedReviewCount)} 条`)} + ${escapeHtml(`高频结论 ${topVerdict ? topVerdict[0] : "待补"}`)}
@@ -6752,8 +6832,8 @@ function renderReviewScreen() {
已保存 ${escapeHtml(formatNumber(reviews.length))} 最近完成 ${escapeHtml(formatNumber(completed.length))} - ${escapeHtml(project?.name || "未选项目")} - ${escapeHtml(completed.length ? "可继续写复盘" : "先回生产")} + ${escapeHtml(`已发布 ${formatNumber(publishedReviewCount)}`)} + ${escapeHtml(topVerdict ? `高频 ${topVerdict[0]}` : (completed.length ? "可继续写复盘" : "先回生产"))}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 3540097..4f17407 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -271,6 +271,20 @@ test("governance and quota panels use real empty-state language instead of backe assert.match(platformAgents, /open-platform-agent-profile/); }); +test("quota and review screens foreground live next-step guidance", () => { + const tenantQuota = extractBetween(APP, "function renderTenantQuotaPanel()", "function policyScopeTagLabel("); + const review = extractBetween(APP, "function renderReviewScreen()", "function renderStrategyScreen()"); + + assert.match(tenantQuota, /先处理存储超限|先恢复额度保护|先补项目额度策略|先检查本周期消耗|先跑出第一条计量/); + assert.match(tenantQuota, /主要消耗/); + assert.match(tenantQuota, /周期 /); + assert.match(tenantQuota, /计量 /); + + assert.match(review, /先把最近完成任务写成复盘|先回看高频结论|先跑出第一条可复盘任务/); + assert.match(review, /高频结论/); + assert.match(review, /已发布/); +}); + test("discovery and production screens expose compact mobile flow summaries", () => { const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()"); const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");