feat: sharpen live quota and review workspaces
This commit is contained in:
@@ -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 ? `<span class="tag orange">额度保护关闭</span>` : `<span class="tag green">额度保护开启</span>`,
|
||||
quota?.storage_over_limit ? `<span class="tag red">存储超限</span>` : `<span class="tag blue">存储正常</span>`,
|
||||
topCategory ? `<span class="tag">${escapeHtml(`主要消耗 ${topCategory.category || "usage"}`)}</span>` : `<span class="tag">本周期未产生消耗</span>`
|
||||
];
|
||||
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 `
|
||||
<div class="panel pad">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>租户额度与审计</h3>
|
||||
<div class="panel-subtitle">预算、动作配额和最近计量都按租户 + 项目隔离。</div>
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>租户额度与审计</h3>
|
||||
<div class="panel-subtitle">预算、动作配额和最近计量都按租户 + 项目隔离,首屏先看风险和下一步。</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${quota?.enabled === false ? "orange" : "green"}">${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")}</span>
|
||||
${quota?.storage_over_limit ? `<span class="tag red">存储超限</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-tenant-quota">编辑额度</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${quota?.enabled === false ? "orange" : "green"}">${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")}</span>
|
||||
${quota?.storage_over_limit ? `<span class="tag red">存储超限</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-tenant-quota">编辑额度</span>
|
||||
<div class="task-item" style="margin-top:14px;">
|
||||
<h4>${escapeHtml(quotaTaskTitle)}</h4>
|
||||
<p>${escapeHtml(quotaTaskSummary)}</p>
|
||||
<div class="task-meta">
|
||||
${quotaTaskAction ? `<span class="tag clickable-tag" data-action="${escapeHtml(quotaTaskAction)}">${escapeHtml(quotaTaskActionLabel)}</span>` : `<span class="tag blue">${escapeHtml(quotaTaskActionLabel)}</span>`}
|
||||
${quotaHealthTags.join("")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${quotaNotice}
|
||||
<div class="mini-grid" style="margin-top:14px;">
|
||||
${cards.map((item) => `
|
||||
<div class="mini-card">
|
||||
<small>${escapeHtml(item.label)}</small>
|
||||
<div class="compact-summary-row" style="margin-top:14px;">
|
||||
<span class="tag blue">${escapeHtml(`周期 ${usagePeriodLabel}`)}</span>
|
||||
<span class="tag green">${escapeHtml(`计量 ${formatNumber(usageCount)} 条`)}</span>
|
||||
<span class="tag">${escapeHtml(`成本 ${(usage?.total_cost_cents || 0) / 100} 元`)}</span>
|
||||
<span class="tag">${escapeHtml(`主要消耗 ${topCategory?.category || "暂无"}`)}</span>
|
||||
</div>
|
||||
${quotaNotice}
|
||||
<div class="mini-grid" style="margin-top:14px;">
|
||||
${cards.map((item) => `
|
||||
<div class="mini-card">
|
||||
<small>${escapeHtml(item.label)}</small>
|
||||
<strong>${escapeHtml(item.value)}</strong>
|
||||
<span>${escapeHtml(item.sub)}</span>
|
||||
</div>
|
||||
@@ -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() {
|
||||
<div class="mini-card"><small>已保存</small><strong>${escapeHtml(formatNumber(reviews.length))}</strong></div>
|
||||
<div class="mini-card"><small>最近完成</small><strong>${escapeHtml(formatNumber(completed.length))}</strong></div>
|
||||
<div class="mini-card"><small>当前项目</small><strong>${escapeHtml(project?.name || "未选项目")}</strong></div>
|
||||
<div class="mini-card"><small>下一步</small><strong>${escapeHtml(completed.length ? "写复盘" : "回生产")}</strong></div>
|
||||
<div class="mini-card"><small>高频结论</small><strong>${escapeHtml(topVerdict ? topVerdict[0] : "待补结论")}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-item" style="margin-top:16px;">
|
||||
<h4>${escapeHtml(reviewTaskTitle)}</h4>
|
||||
<p>${escapeHtml(reviewTaskSummary)}</p>
|
||||
<div class="task-meta">
|
||||
${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)}
|
||||
<span class="tag blue">${escapeHtml(`已保存 ${formatNumber(reviews.length)} 条`)}</span>
|
||||
<span class="tag">${escapeHtml(`已发布 ${formatNumber(publishedReviewCount)} 条`)}</span>
|
||||
<span class="tag">${escapeHtml(`高频结论 ${topVerdict ? topVerdict[0] : "待补"}`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-only mobile-flow-focus-card">
|
||||
@@ -6752,8 +6832,8 @@ function renderReviewScreen() {
|
||||
<div class="mobile-only compact-summary-row" style="margin-top:14px; margin-bottom:14px;">
|
||||
<span class="tag blue">已保存 ${escapeHtml(formatNumber(reviews.length))}</span>
|
||||
<span class="tag green">最近完成 ${escapeHtml(formatNumber(completed.length))}</span>
|
||||
<span class="tag">${escapeHtml(project?.name || "未选项目")}</span>
|
||||
<span class="tag">${escapeHtml(completed.length ? "可继续写复盘" : "先回生产")}</span>
|
||||
<span class="tag">${escapeHtml(`已发布 ${formatNumber(publishedReviewCount)}`)}</span>
|
||||
<span class="tag">${escapeHtml(topVerdict ? `高频 ${topVerdict[0]}` : (completed.length ? "可继续写复盘" : "先回生产"))}</span>
|
||||
</div>
|
||||
<div class="layout-grid grid-main">
|
||||
<div class="side-stack">
|
||||
|
||||
@@ -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()");
|
||||
|
||||
Reference in New Issue
Block a user