feat: expand oneliner control surfaces and quotas
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user