feat: expand oneliner control surfaces and quotas

This commit is contained in:
kris
2026-03-23 16:37:33 +08:00
parent 5f7359c243
commit 71465b3d55
2 changed files with 1558 additions and 31 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,10 @@ const appState = {
onelinerSessions: [], onelinerSessions: [],
selectedOnelinerSessionId: "", selectedOnelinerSessionId: "",
onelinerMessages: [], onelinerMessages: [],
onelinerActionRegistry: [],
platformAgents: [], platformAgents: [],
tenantQuota: null,
tenantUsage: null,
adminOpsOverview: null, adminOpsOverview: null,
busy: false, busy: false,
message: "", message: "",
@@ -782,6 +785,8 @@ function renderOneLinerMessagesHtml() {
return `data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`; return `data-${escapeHtml(attrKey)}="${escapeHtml(serialized)}"`;
}) })
].filter(Boolean).join(" ") ].filter(Boolean).join(" ")
,
{ disabledReason: item.disabled_reason || "" }
)).join("")} )).join("")}
</div> </div>
` : ""} ` : ""}
@@ -1001,7 +1006,10 @@ async function logoutSession() {
appState.onelinerSessions = []; appState.onelinerSessions = [];
appState.selectedOnelinerSessionId = ""; appState.selectedOnelinerSessionId = "";
appState.onelinerMessages = []; appState.onelinerMessages = [];
appState.onelinerActionRegistry = [];
appState.platformAgents = []; appState.platformAgents = [];
appState.tenantQuota = null;
appState.tenantUsage = null;
appState.adminOpsOverview = null; appState.adminOpsOverview = null;
appState.integrationHealth = null; appState.integrationHealth = null;
appState.storageStatus = null; appState.storageStatus = null;
@@ -1040,19 +1048,31 @@ async function loadAgentControlSurfaces(projectId = "") {
const normalizedProjectId = projectId || getOneLinerProjectId(); const normalizedProjectId = projectId || getOneLinerProjectId();
const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile"); const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile");
const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions"); const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions");
const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry");
const supportsPlatformAgents = backendSupports("/v2/platform-agents"); const supportsPlatformAgents = backendSupports("/v2/platform-agents");
const supportsAdminOps = backendSupports("/v2/admin/ops/overview"); const supportsAdminOps = backendSupports("/v2/admin/ops/overview");
const supportsTenantQuota = backendSupports("/v2/tenant/quota");
const supportsTenantUsage = backendSupports("/v2/tenant/usage");
const [profile, sessionsPayload, platformAgentsPayload, adminOpsOverview] = await Promise.all([ const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, tenantQuota, tenantUsage, adminOpsOverview] = await Promise.all([
supportsOneLinerProfile supportsOneLinerProfile
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null) ? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null), : Promise.resolve(null),
supportsOneLinerSessions supportsOneLinerSessions
? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) ? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }), : Promise.resolve({ items: [] }),
supportsActionRegistry
? storyforgeFetch(`/v2/oneliner/action-registry?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsPlatformAgents supportsPlatformAgents
? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] })) ? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }), : Promise.resolve({ items: [] }),
supportsTenantQuota
? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsTenantUsage
? storyforgeFetch(`/v2/tenant/usage?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsAdminOps && isSuperAdmin() supportsAdminOps && isSuperAdmin()
? storyforgeFetch("/v2/admin/ops/overview").catch(() => null) ? storyforgeFetch("/v2/admin/ops/overview").catch(() => null)
: Promise.resolve(null) : Promise.resolve(null)
@@ -1060,10 +1080,13 @@ async function loadAgentControlSurfaces(projectId = "") {
appState.onelinerProfile = profile; appState.onelinerProfile = profile;
appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload); appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload);
appState.onelinerActionRegistry = safeArray(actionRegistryPayload?.items || actionRegistryPayload);
if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) { if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) {
appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || ""; appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || "";
} }
appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload); appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload);
appState.tenantQuota = tenantQuota;
appState.tenantUsage = tenantUsage;
appState.adminOpsOverview = adminOpsOverview; appState.adminOpsOverview = adminOpsOverview;
} }
@@ -2165,6 +2188,120 @@ function renderStorageStatusPanel() {
`; `;
} }
function renderOneLinerActionRegistryPanel() {
const items = safeArray(appState.onelinerActionRegistry);
if (!items.length) {
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>OneLiner 动作注册表</h3>
<div class="panel-subtitle">当前后端还没返回动作注册表,先沿用默认动作。</div>
</div>
</div>
<div class="task-item"><h4>暂未接入</h4><p>等 <code>/v2/oneliner/action-registry</code> 可用后,这里会显示动作开关、描述和租户级配置。</p></div>
</div>
`;
}
const grouped = items.reduce((acc, item) => {
const category = item.category || "custom";
acc[category] = acc[category] || [];
acc[category].push(item);
return acc;
}, {});
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>OneLiner 动作注册表</h3>
<div class="panel-subtitle">把 OneLiner 可执行动作做成租户级注册中心,便于商业化灰度和定制。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(items.length))} 条</span>
</div>
<div class="list">
${Object.entries(grouped).map(([category, list]) => `
<div class="task-item compact">
<h4>${escapeHtml(category)}</h4>
<p>${escapeHtml(`当前分类下 ${list.length} 条动作。`)}</p>
<div class="task-meta">
${list.map((item) => `
<span class="tag ${item.status === "enabled" ? "green" : "orange"} clickable-tag" data-action="open-action-registry-edit" data-action-key="${escapeHtml(item.action_key || "")}">
${escapeHtml(item.label || item.action_key || "action")}
</span>
`).join("")}
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function renderTenantQuotaPanel() {
const quota = appState.tenantQuota;
const usage = appState.tenantUsage || quota?.usage || {};
if (!quota && !usage) {
return `
<div class="panel pad">
<div class="panel-head"><div><h3>租户额度与审计</h3><div class="panel-subtitle">当前后端还没接入 quota / usage。</div></div></div>
<div class="task-item"><h4>暂未接入</h4><p>等 live collector 同步 `/v2/tenant/quota``/v2/tenant/usage` 后,这里会展示本周期预算、动作配额和最近计量记录。</p></div>
</div>
`;
}
const categories = usage?.categories || {};
const recentItems = safeArray(usage?.recent_items);
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)}` },
{ label: "文案配额", value: formatNumber(quota?.copy_quota || 0), sub: `已用 ${formatNumber(categories.copy?.quantity || 0)}` },
{ label: "AI 视频配额", value: formatNumber(quota?.ai_video_quota || 0), sub: `已用 ${formatNumber(categories.ai_video?.quantity || 0)}` },
{ label: "实拍剪辑配额", value: formatNumber(quota?.real_cut_quota || 0), sub: `已用 ${formatNumber(categories.real_cut?.quantity || 0)}` },
{ label: "存储上限", value: formatBytes(quota?.storage_limit_bytes || 0), sub: `当前 ${formatBytes(usage?.storage_bytes || 0)}` }
];
return `
<div class="panel pad">
<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="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>
`).join("")}
</div>
<div class="list" style="margin-top:14px;">
<div class="task-item compact">
<h4>最近计量记录</h4>
<p>动作执行后会写入租户级 ledger便于后面做商业化配额、成本和审计。</p>
</div>
${recentItems.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.category || "usage")}</h4>
<p>${escapeHtml(formatDateTime(item.created_at))}</p>
<div class="task-meta">
<span class="tag blue">次数 ${escapeHtml(formatNumber(item.quantity || 0))}</span>
<span class="tag">成本 ${(item.cost_cents || 0) / 100} 元</span>
${item.reference_type ? `<span class="tag">${escapeHtml(item.reference_type)}</span>` : ""}
${item.reference_id ? `<span class="tag">${escapeHtml(brief(item.reference_id, 14))}</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有计量记录</h4><p>等 OneLiner 或生产动作实际执行后,这里会累积本周期的 usage ledger。</p></div>`}
</div>
</div>
`;
}
function renderPlatformAgentPanel() { function renderPlatformAgentPanel() {
const items = safeArray(appState.platformAgents); const items = safeArray(appState.platformAgents);
if (!items.length) { if (!items.length) {
@@ -2242,6 +2379,7 @@ function renderAdminOpsPanel() {
} }
const incidents = safeArray(overview.incidents).slice(0, 6); const incidents = safeArray(overview.incidents).slice(0, 6);
const audits = safeArray(overview.recent_audits).slice(0, 5); const audits = safeArray(overview.recent_audits).slice(0, 5);
const fixRuns = safeArray(overview.recent_fix_runs).slice(0, 5);
return ` return `
<div class="panel pad" style="margin-top:18px;"> <div class="panel pad" style="margin-top:18px;">
<div class="panel-head"> <div class="panel-head">
@@ -2254,6 +2392,7 @@ function renderAdminOpsPanel() {
<span class="tag orange">待处理 ${escapeHtml(formatNumber(overview.open_incident_count || 0))}</span> <span class="tag orange">待处理 ${escapeHtml(formatNumber(overview.open_incident_count || 0))}</span>
<span class="tag red">错误 ${escapeHtml(formatNumber(overview.severity_counts?.error || 0))}</span> <span class="tag red">错误 ${escapeHtml(formatNumber(overview.severity_counts?.error || 0))}</span>
<span class="tag">${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务</span> <span class="tag">${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务</span>
<span class="tag">修复计划 ${escapeHtml(formatNumber(overview.fix_run_count || 0))}</span>
<span class="tag clickable-tag" data-action="scan-admin-ops">重新扫描</span> <span class="tag clickable-tag" data-action="scan-admin-ops">重新扫描</span>
</div> </div>
</div> </div>
@@ -2270,11 +2409,30 @@ function renderAdminOpsPanel() {
${item.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(item.source_id || "")}"`) : ""} ${item.source_type === "job" ? actionTag("看任务详情", "open-job-detail", `data-job-id="${escapeHtml(item.source_id || "")}"`) : ""}
${item.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""} ${item.source_type === "integration" ? actionTag("去自动流程", "goto-automation") : ""}
${item.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""} ${item.tenant_project_id ? actionTag("去生产中心", "goto-production") : ""}
<span class="tag clickable-tag" data-action="open-admin-repair-plan" data-incident-id="${escapeHtml(item.id)}">生成修复计划</span>
<span class="tag clickable-tag" data-action="review-admin-incident" data-incident-id="${escapeHtml(item.id)}">审计处理</span> <span class="tag clickable-tag" data-action="review-admin-incident" data-incident-id="${escapeHtml(item.id)}">审计处理</span>
</div> </div>
</div> </div>
`).join("") || `<div class="task-item"><h4>当前没有待处理事件</h4><p>最近主链比较稳定,继续观察即可。</p></div>`} `).join("") || `<div class="task-item"><h4>当前没有待处理事件</h4><p>最近主链比较稳定,继续观察即可。</p></div>`}
</div> </div>
<div class="list" style="margin-top:14px;">
<div class="task-item compact">
<h4>最近修复计划</h4>
<p>这里代表运维 Agent 输出的修复方案,必须经过审计 Agent 放行才算闭环。</p>
</div>
${fixRuns.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.plan?.summary || item.id || "修复计划")}</h4>
<p>${escapeHtml(item.plan?.steps?.[0] || "待补充修复步骤")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(item.plan_scope || "plan")}</span>
<span class="tag">${escapeHtml(item.audit_status || "pending")}</span>
${item.incident_id ? `<span class="tag">事件 ${escapeHtml(brief(item.incident_id, 10))}</span>` : ""}
<span class="tag clickable-tag" data-action="open-admin-fix-run-audit" data-run-id="${escapeHtml(item.id)}">审计放行</span>
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有修复计划</h4><p>当运维 Agent 针对故障事件生成 repair plan 后,这里会自动出现。</p></div>`}
</div>
<div class="list" style="margin-top:14px;"> <div class="list" style="margin-top:14px;">
<div class="task-item compact"> <div class="task-item compact">
<h4>最近审计记录</h4> <h4>最近审计记录</h4>
@@ -3116,6 +3274,12 @@ function renderAutomationScreen() {
</div> </div>
<div class="integration-note" style="margin-top:12px;">${escapeHtml(overview.subtitle)}</div> <div class="integration-note" style="margin-top:12px;">${escapeHtml(overview.subtitle)}</div>
</div> </div>
<div style="margin-top:18px;">
${renderTenantQuotaPanel()}
</div>
<div style="margin-top:18px;">
${renderOneLinerActionRegistryPanel()}
</div>
${renderAdminOpsPanel()} ${renderAdminOpsPanel()}
` `
); );
@@ -3195,9 +3359,15 @@ function renderPlaybookScreen() {
</div> </div>
</div> </div>
</div> </div>
<div style="margin-top:18px;">
${renderOneLinerActionRegistryPanel()}
</div>
<div style="margin-top:18px;"> <div style="margin-top:18px;">
${renderPlatformAgentPanel()} ${renderPlatformAgentPanel()}
</div> </div>
<div style="margin-top:18px;">
${renderTenantQuotaPanel()}
</div>
<div class="panel pad" style="margin-top:18px;"> <div class="panel pad" style="margin-top:18px;">
<div class="panel-head"> <div class="panel-head">
<div> <div>
@@ -4093,6 +4263,16 @@ async function openPlatformAgentDetailAction(platform) {
]); ]);
const memories = safeArray(memoriesPayload?.items || memoriesPayload).slice(0, 6); const memories = safeArray(memoriesPayload?.items || memoriesPayload).slice(0, 6);
const skills = safeArray(skillsPayload?.items || skillsPayload).slice(0, 6); const skills = safeArray(skillsPayload?.items || skillsPayload).slice(0, 6);
const skillVersionEntries = await Promise.all(
skills.map(async (item) => {
if (!backendSupports("/v2/platform-agents/{platform}/skills/{skill_id}/versions")) {
return [item.id, []];
}
const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(item.id)}/versions?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] }));
return [item.id, safeArray(payload?.items || payload).slice(0, 3)];
})
);
const skillVersions = Object.fromEntries(skillVersionEntries);
openActionModal({ openActionModal({
title: `${platformLabel(normalizedPlatform)} Agent 详情`, title: `${platformLabel(normalizedPlatform)} Agent 详情`,
description: "查看当前平台 Agent 最近沉淀的记忆、技能和就绪度。", description: "查看当前平台 Agent 最近沉淀的记忆、技能和就绪度。",
@@ -4138,6 +4318,16 @@ async function openPlatformAgentDetailAction(platform) {
<span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="true">验收通过</span> <span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="true">验收通过</span>
<span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="false">标记待优化</span> <span class="tag clickable-tag" data-action="review-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-accepted="false">标记待优化</span>
</div> </div>
${safeArray(skillVersions[item.id]).length ? `
<div class="task-meta" style="margin-top:8px;">
${safeArray(skillVersions[item.id]).map((version, index) => `
<span class="tag ${index === 0 ? "blue" : ""}${index !== 0 ? "" : ""} ${index === 0 ? "" : "clickable-tag"}"
${index === 0 ? "" : `data-action="rollback-platform-skill" data-platform="${escapeHtml(normalizedPlatform)}" data-skill-id="${escapeHtml(item.id || "")}" data-version-id="${escapeHtml(version.id || "")}"`}>
${escapeHtml(`v${formatNumber(version.version_no || 0)} · ${version.snapshot_reason || "snapshot"}`)}
</span>
`).join("")}
</div>
` : ""}
</div> </div>
`).join("") || `<div class="task-item compact"><h4>还没有平台技能</h4><p>等子 Agent 跑出稳定结果后,把方法固化成技能。</p></div>`} `).join("") || `<div class="task-item compact"><h4>还没有平台技能</h4><p>等子 Agent 跑出稳定结果后,把方法固化成技能。</p></div>`}
</div> </div>
@@ -4202,6 +4392,121 @@ function openPlatformSkillReviewAction(platform, skillId, accepted) {
}); });
} }
function openPlatformSkillRollbackAction(platform, skillId, versionId) {
const project = requireSelectedProject();
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
openActionModal({
title: "回滚平台技能",
description: "把当前技能回退到旧版本,并保留新的回滚快照,方便继续追踪。",
submitLabel: "确认回滚",
fields: [
{
name: "summary",
label: "回滚说明",
type: "html",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(platformLabel(normalizedPlatform))} 技能回滚</h4>
<p>${escapeHtml(`将 skill ${skillId} 回滚到版本 ${versionId}`)}</p>
</div>
</div>
`
}
],
onSubmit: async () => {
const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(skillId)}/rollback`, {
method: "POST",
body: {
project_id: project.id,
version_id: versionId
}
});
rememberAction("技能已回滚", `已回滚到版本 ${payload.rollback_from_version?.version_no || "指定版本"}`, "green", payload);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openActionRegistryEditAction(actionKey) {
const project = requireSelectedProject();
const actionDef = safeArray(appState.onelinerActionRegistry).find((item) => item.action_key === actionKey) || null;
if (!actionDef) {
alert("没有找到这条动作定义。");
return;
}
openActionModal({
title: "编辑 OneLiner 动作",
description: "在租户范围内控制动作名称、说明、开关和少量配置。",
submitLabel: "保存动作",
fields: [
{ name: "label", label: "动作名称", value: actionDef.label || "" },
{ name: "description", label: "动作说明", type: "textarea", rows: 4, value: actionDef.description || "" },
{ name: "status", label: "状态", type: "select", value: actionDef.status || "enabled", options: [{ value: "enabled", label: "启用" }, { value: "disabled", label: "禁用" }] },
{ name: "configJson", label: "配置 JSON", type: "textarea", rows: 5, value: JSON.stringify(actionDef.config || {}, null, 2) }
],
onSubmit: async (values) => {
let config = {};
if (String(values.configJson || "").trim()) {
config = JSON.parse(values.configJson);
}
const saved = await storyforgeFetch(`/v2/oneliner/action-registry/${encodeURIComponent(actionKey)}?project_id=${encodeURIComponent(project.id)}`, {
method: "PUT",
body: {
label: values.label || "",
description: values.description || "",
category: actionDef.category || "custom",
status: values.status || "enabled",
config
}
});
rememberAction("动作已更新", `OneLiner 动作「${saved.label || saved.action_key}」已保存。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openTenantQuotaAction() {
const project = requireSelectedProject();
const quota = appState.tenantQuota || {};
openActionModal({
title: "编辑租户额度",
description: "当前额度按租户 + 项目隔离,用于商业化预算、动作配额和存储保护。",
submitLabel: "保存额度",
fields: [
{ name: "enabled", label: "启用额度保护", type: "checkbox", value: quota.enabled !== false },
{ 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 },
{ name: "copyQuota", label: "文案配额", type: "number", value: quota.copy_quota || 0, min: 0 },
{ name: "aiVideoQuota", label: "AI 视频配额", type: "number", value: quota.ai_video_quota || 0, min: 0 },
{ name: "realCutQuota", label: "实拍剪辑配额", type: "number", value: quota.real_cut_quota || 0, min: 0 },
{ name: "recorderQuota", label: "录制配额", type: "number", value: quota.recorder_quota || 0, min: 0 }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(project.id)}`, {
method: "PUT",
body: {
enabled: Boolean(values.enabled),
monthly_budget_cents: Number(values.monthlyBudgetCents || 0),
storage_limit_bytes: Number(values.storageLimitBytes || 0),
analysis_quota: Number(values.analysisQuota || 0),
copy_quota: Number(values.copyQuota || 0),
ai_video_quota: Number(values.aiVideoQuota || 0),
real_cut_quota: Number(values.realCutQuota || 0),
recorder_quota: Number(values.recorderQuota || 0),
config: quota.config || {}
}
});
rememberAction("租户额度已更新", "当前项目的预算与配额已经保存。", "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openCreateAssistantAction() { function openCreateAssistantAction() {
const project = requireSelectedProject(); const project = requireSelectedProject();
const kbOptions = getKnowledgeBaseOptions(project.id); const kbOptions = getKnowledgeBaseOptions(project.id);
@@ -4526,6 +4831,86 @@ function openAdminIncidentReviewAction(incidentId) {
}); });
} }
function openAdminRepairPlanAction(incidentId) {
if (!isSuperAdmin()) {
alert("只有平台管理者才能生成修复计划。");
return;
}
const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId);
if (!incident) {
alert("没有找到这条故障事件。");
return;
}
openActionModal({
title: "生成修复计划",
description: "让运维 Agent 先生成一版 repair plan再由审计 Agent 决定是否放行。",
submitLabel: "生成计划",
fields: [
{ name: "scope", label: "计划范围", type: "select", value: "plan", options: [{ value: "plan", label: "标准计划" }, { value: "hotfix", label: "热修建议" }, { value: "watch", label: "仅观察" }] },
{ name: "notes", label: "附加说明", type: "textarea", rows: 4, placeholder: "例如:优先验证 cutvideo 上传链,不要动核心代码" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incidentId)}/repair-plan`, {
method: "POST",
body: {
incident_id: incidentId,
scope: values.scope || "plan",
notes: values.notes || ""
}
});
rememberAction("修复计划已生成", `已为事件「${incident.title}」生成 repair plan。`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
function openAdminFixRunAuditAction(runId) {
if (!isSuperAdmin()) {
alert("只有平台管理者才能审计修复计划。");
return;
}
const run = safeArray(appState.adminOpsOverview?.recent_fix_runs).find((item) => item.id === runId);
if (!run) {
alert("没有找到这条修复计划。");
return;
}
openActionModal({
title: "审计修复计划",
description: "审计 Agent 只做放行、驳回或继续观察,不会直接让用户一句话改核心代码。",
submitLabel: "保存审计",
fields: [
{
name: "summary",
label: "计划摘要",
type: "html",
html: `
<div class="sheet-html">
<div class="task-item compact">
<h4>${escapeHtml(run.plan?.summary || run.id)}</h4>
<p>${escapeHtml((run.plan?.steps || []).join("") || "暂无步骤")}</p>
</div>
</div>
`
},
{ name: "reviewStatus", label: "审计状态", type: "select", value: run.audit_status || "approved", options: [{ value: "approved", label: "通过" }, { value: "watching", label: "继续观察" }, { value: "rejected", label: "驳回" }] },
{ name: "reviewNotes", label: "审计备注", type: "textarea", rows: 4, value: run.review_notes || "", placeholder: "写清楚为什么通过、驳回或继续观察" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/fix-runs/${encodeURIComponent(runId)}/audit`, {
method: "POST",
body: {
review_status: values.reviewStatus || "approved",
review_notes: values.reviewNotes || ""
}
});
rememberAction("修复计划已审计", `修复计划 ${runId} 已更新为 ${saved.audit_status || values.reviewStatus}`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
function openJobDetailAction(jobId) { function openJobDetailAction(jobId) {
if (!jobId) return; if (!jobId) return;
setBusy(true, "正在加载任务详情..."); setBusy(true, "正在加载任务详情...");
@@ -5074,6 +5459,14 @@ document.addEventListener("click", async (event) => {
await openPlatformAgentDetailAction(action.dataset.platform || ""); await openPlatformAgentDetailAction(action.dataset.platform || "");
return; return;
} }
if (name === "open-action-registry-edit") {
openActionRegistryEditAction(action.dataset.actionKey || "");
return;
}
if (name === "open-tenant-quota") {
openTenantQuotaAction();
return;
}
if (name === "run-oneliner-action") { if (name === "run-oneliner-action") {
setBusy(true, "OneLiner 正在执行动作..."); setBusy(true, "OneLiner 正在执行动作...");
try { try {
@@ -5101,6 +5494,10 @@ document.addEventListener("click", async (event) => {
openPlatformSkillReviewAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.accepted !== "false"); openPlatformSkillReviewAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.accepted !== "false");
return; return;
} }
if (name === "rollback-platform-skill") {
openPlatformSkillRollbackAction(action.dataset.platform || "", action.dataset.skillId || "", action.dataset.versionId || "");
return;
}
if (name === "analyze-selected-account") { if (name === "analyze-selected-account") {
openAnalyzeSelectedAccountAction(); openAnalyzeSelectedAccountAction();
return; return;
@@ -5180,6 +5577,14 @@ document.addEventListener("click", async (event) => {
openAdminIncidentReviewAction(action.dataset.incidentId || ""); openAdminIncidentReviewAction(action.dataset.incidentId || "");
return; return;
} }
if (name === "open-admin-repair-plan") {
openAdminRepairPlanAction(action.dataset.incidentId || "");
return;
}
if (name === "open-admin-fix-run-audit") {
openAdminFixRunAuditAction(action.dataset.runId || "");
return;
}
if (name === "job-to-ai-video") { if (name === "job-to-ai-video") {
const jobId = action.dataset.jobId || ""; const jobId = action.dataset.jobId || "";
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null; const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;