feat: add quota package tiers and recovery guidance
This commit is contained in:
@@ -4113,6 +4113,7 @@ function renderOneLinerActionRegistryPanel() {
|
||||
function renderTenantQuotaPanel() {
|
||||
const quota = appState.tenantQuota;
|
||||
const usage = appState.tenantUsage || quota?.usage || {};
|
||||
const quotaConfig = quota?.config || {};
|
||||
const quotaNotice = renderQuotaBlockingNotice();
|
||||
if (!quota && !usage) {
|
||||
return `
|
||||
@@ -4148,6 +4149,8 @@ function renderTenantQuotaPanel() {
|
||||
(quota?.recorder_quota || 0) > 0
|
||||
);
|
||||
const usageCount = recentItems.length;
|
||||
const packageLabel = String(quotaConfig.package_label || "").trim() || (hasHardLimit ? "自定义套餐" : "未设套餐");
|
||||
const warnThreshold = Number(quotaConfig.warn_threshold ?? 0.8);
|
||||
const quotaTaskTitle = quota?.storage_over_limit
|
||||
? "先处理存储超限"
|
||||
: quota?.enabled === false
|
||||
@@ -4175,6 +4178,7 @@ function renderTenantQuotaPanel() {
|
||||
topCategory ? `<span class="tag">${escapeHtml(`主要消耗 ${topCategory.category || "usage"}`)}</span>` : `<span class="tag">本周期未产生消耗</span>`
|
||||
];
|
||||
const cards = [
|
||||
{ label: "套餐档位", value: packageLabel, sub: `预警阈值 ${formatNumber((warnThreshold || 0.8) * 100)}%` },
|
||||
{ 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)}` },
|
||||
{ label: "文案配额", value: formatNumber(quota?.copy_quota || 0), sub: `已用 ${formatNumber(categories.copy?.quantity || 0)}` },
|
||||
@@ -4191,6 +4195,7 @@ function renderTenantQuotaPanel() {
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${quota?.enabled === false ? "orange" : "green"}">${escapeHtml(quota?.enabled === false ? "已停用额度保护" : "额度保护开启")}</span>
|
||||
<span class="tag blue">${escapeHtml(packageLabel)}</span>
|
||||
${quota?.storage_over_limit ? `<span class="tag red">存储超限</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-tenant-quota">编辑额度</span>
|
||||
</div>
|
||||
@@ -4204,10 +4209,11 @@ function renderTenantQuotaPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="compact-summary-row" style="margin-top:14px;">
|
||||
<span class="tag blue">${escapeHtml(`套餐 ${packageLabel}`)}</span>
|
||||
<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>
|
||||
<span class="tag">${escapeHtml(`预警 ${(warnThreshold || 0.8) * 100}%`)}</span>
|
||||
</div>
|
||||
${quotaNotice}
|
||||
<div class="mini-grid" style="margin-top:14px;">
|
||||
@@ -10031,12 +10037,20 @@ function openActionRegistryEditAction(actionKey) {
|
||||
function openTenantQuotaAction() {
|
||||
const project = requireSelectedProject();
|
||||
const quota = appState.tenantQuota || {};
|
||||
const quotaConfig = quota.config || {};
|
||||
openActionModal({
|
||||
title: "编辑租户额度",
|
||||
description: "当前额度按租户 + 项目隔离,用于商业化预算、动作配额和存储保护。",
|
||||
submitLabel: "保存额度",
|
||||
fields: [
|
||||
{ name: "enabled", label: "启用额度保护", type: "checkbox", value: quota.enabled !== false },
|
||||
{ name: "packageLabel", label: "套餐档位", type: "select", value: quotaConfig.package_label || "custom", options: [
|
||||
{ value: "trial", label: "试用套餐" },
|
||||
{ value: "growth", label: "增长套餐" },
|
||||
{ value: "scale", label: "规模套餐" },
|
||||
{ value: "custom", label: "自定义套餐" }
|
||||
] },
|
||||
{ name: "warnThreshold", label: "预算预警阈值", type: "number", value: quotaConfig.warn_threshold ?? 0.8, min: 0, max: 1, step: 0.05 },
|
||||
{ name: "monthlyBudgetCents", label: "月预算(分)", type: "number", value: quota.monthly_budget_cents || 0, min: 0 },
|
||||
{ name: "storageLimitBytes", label: "存储上限(字节)", type: "number", value: quota.storage_limit_bytes || 0, min: 0 },
|
||||
{ name: "analysisQuota", label: "分析配额", type: "number", value: quota.analysis_quota || 0, min: 0 },
|
||||
@@ -10057,7 +10071,11 @@ function openTenantQuotaAction() {
|
||||
ai_video_quota: Number(values.aiVideoQuota || 0),
|
||||
real_cut_quota: Number(values.realCutQuota || 0),
|
||||
recorder_quota: Number(values.recorderQuota || 0),
|
||||
config: quota.config || {}
|
||||
config: {
|
||||
...(quota.config || {}),
|
||||
package_label: String(values.packageLabel || "custom"),
|
||||
warn_threshold: Number(values.warnThreshold ?? 0.8) || 0.8
|
||||
}
|
||||
}
|
||||
});
|
||||
rememberAction("租户额度已更新", "当前项目的预算与配额已经保存。", "green", saved);
|
||||
@@ -10662,7 +10680,48 @@ function openRecoverJobAction(jobId) {
|
||||
}
|
||||
const recovery = getJobRecoverability(job);
|
||||
if (!recovery.recoverable) {
|
||||
presentActionFailure(new Error(recovery.reason || "当前任务需要人工处理后再继续"), "当前任务需要先补信息或转人工处理");
|
||||
const nextActionTag = (() => {
|
||||
if (recovery.actionKey === "open-job-detail") {
|
||||
const targetJobId = recovery.sourceJobId || job.id;
|
||||
return actionTag(recovery.actionLabel || "查看详情", "open-job-detail", `data-job-id="${escapeHtml(targetJobId)}"`);
|
||||
}
|
||||
if (recovery.actionKey) {
|
||||
return actionTag(recovery.actionLabel || "继续处理", recovery.actionKey);
|
||||
}
|
||||
return "";
|
||||
})();
|
||||
openActionModal({
|
||||
title: "当前任务需要先处理依赖",
|
||||
description: recovery.reason || "这条任务需要先补信息或转人工处理。",
|
||||
hideSubmit: true,
|
||||
fields: [
|
||||
{
|
||||
type: "html",
|
||||
label: "处理建议",
|
||||
html: `
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(job.title || job.id)}</h4>
|
||||
<p>${escapeHtml(recovery.reason || "先补回缺失信息,再继续推进这条任务。")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${recovery.state === "blocked" ? "red" : "orange"}">${escapeHtml(recovery.label || "需人工处理")}</span>
|
||||
${nextActionTag}
|
||||
${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({
|
||||
sourceScreen: "production",
|
||||
sourceActionKey: "recover-job-handoff",
|
||||
intentKey: "custom",
|
||||
title: `处理失败任务 ${job.title || job.id}`,
|
||||
goal: "结合失败原因和当前任务上下文,给出下一步恢复建议",
|
||||
summary: "主 Agent 会先判断这条任务缺的是素材、额度还是源任务,再给出具体处理路径。",
|
||||
planSteps: ["读取失败任务详情", "判断缺失输入或依赖", "生成下一步恢复建议"]
|
||||
}))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
return;
|
||||
}
|
||||
openActionModal({
|
||||
|
||||
Reference in New Issue
Block a user