fix: harden main agent governance flows
This commit is contained in:
27
CHANGELOG.md
27
CHANGELOG.md
@@ -54,3 +54,30 @@
|
||||
|
||||
- 新增仓库级 `CHANGELOG.md`,让 Gitea 仓库能直接看到阶段性更新记录。
|
||||
- 最小回归 workflow 同时落在 `.github/workflows/ci.yml` 和 `.gitea/workflows/ci.yml`,GitHub Actions 与 Gitea Actions 都能跑相同的基线、后端单测和 Web 测试。
|
||||
|
||||
## 2026-03-31
|
||||
|
||||
### 主 Agent 配置业务流收口
|
||||
|
||||
- 管理员配置台里的系统主 Agent、系统平台策略、管理员覆盖这条配置流补上了前端本地权限兜底,非超级管理员不会再直接撞到后端 403。
|
||||
- 管理员覆盖目标为空时,前端会明确提示“当前治理目录里还没有可选用户”,不再放出无效保存和回滚动作。
|
||||
- 管理员侧三类历史回滚弹层都改成了只读空态:没有历史版本时会隐藏提交按钮,也不会再让空 `version_id` 发起无效回滚请求。
|
||||
|
||||
### 配置流回归护栏
|
||||
|
||||
- Web 工作台测试新增了主 Agent 配置流空态保护和权限保护覆盖,重点锁住:
|
||||
- 管理员历史回滚空态
|
||||
- 管理员治理动作本地权限 guard
|
||||
- 管理员覆盖目标为空时的编辑/历史保护
|
||||
- 当前基线重新验证通过:
|
||||
- 前端测试 `66/66`
|
||||
- 后端单测 `35/35`
|
||||
- `bash scripts/check_repo_baseline.sh`
|
||||
- `bash scripts/smoke_fnos_storyforge_lan.sh`
|
||||
|
||||
### NAS 联调发布
|
||||
|
||||
- 最新 Web 已重新发布到 fnOS NAS:
|
||||
- Web: `http://192.168.31.188:19192/`
|
||||
- Collector: `http://192.168.31.188:19193/healthz`
|
||||
- 主 Agent 配置业务流的这轮修复已经同步到 Gitea,后续可以直接基于当前分支继续收剩余真实能力细节。
|
||||
|
||||
@@ -8645,6 +8645,21 @@ function buildPolicyVersionOptions(history) {
|
||||
}));
|
||||
}
|
||||
|
||||
function ensureAdminGovernanceAccess() {
|
||||
if (isSuperAdmin()) return true;
|
||||
rememberAction("需要管理员权限", "当前动作仅超级管理员可用,请切换到管理员账号后再继续。", "orange");
|
||||
renderAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
function ensureAdminOverrideTargetReady(target) {
|
||||
if (!ensureAdminGovernanceAccess()) return false;
|
||||
if (target?.targetUserId) return true;
|
||||
rememberAction("还没有可治理目标", "当前治理目录里还没有可选用户,暂时无法执行管理员覆盖动作。", "orange");
|
||||
renderAll();
|
||||
return false;
|
||||
}
|
||||
|
||||
function openUserGlobalPolicyAction() {
|
||||
const project = requireSelectedProject();
|
||||
const bundle = appState.userGlobalPolicy || {};
|
||||
@@ -8786,6 +8801,7 @@ async function openUserPlatformPolicyHistoryAction(platform) {
|
||||
}
|
||||
|
||||
function openSystemMainPolicyAction() {
|
||||
if (!ensureAdminGovernanceAccess()) return;
|
||||
const projectId = getOneLinerProjectId();
|
||||
const bundle = appState.adminSystemMainPolicy || {};
|
||||
const current = bundle.current_version || {};
|
||||
@@ -8819,6 +8835,7 @@ function openSystemMainPolicyAction() {
|
||||
}
|
||||
|
||||
function openSystemPlatformPolicyAction(platform) {
|
||||
if (!ensureAdminGovernanceAccess()) return;
|
||||
const normalizedPlatform = normalizePlatformValue(platform, "douyin");
|
||||
const projectId = getOneLinerProjectId();
|
||||
const bundle = safeArray(appState.adminSystemPlatformPolicies).find((item) => item?.scope?.platform === normalizedPlatform) || {};
|
||||
@@ -8856,17 +8873,21 @@ function openSystemPlatformPolicyAction(platform) {
|
||||
}
|
||||
|
||||
async function openAdminOverrideTargetAction() {
|
||||
if (!ensureAdminGovernanceAccess()) return;
|
||||
const current = getAdminOverrideTargetState();
|
||||
const directoryItems = getAdminGovernanceDirectoryItems();
|
||||
openActionModal({
|
||||
title: "选择管理员覆盖目标",
|
||||
description: "先选中要覆盖的用户、项目和平台,再去编辑覆盖策略或查看历史。",
|
||||
submitLabel: "保存目标",
|
||||
hideSubmit: !directoryItems.length,
|
||||
fields: [
|
||||
{ type: "html", label: "当前目标", html: renderPolicyVersionSummary(appState.adminOverridePolicy || {}, `当前目标是 ${formatAdminGovernanceTargetLabel(current)}。`) },
|
||||
{ name: "targetUserId", label: "目标用户", type: "select", value: current.targetUserId, options: getAdminGovernanceDirectoryUserOptions() },
|
||||
{ name: "targetProjectId", label: "目标项目", type: "select", value: current.targetProjectId, options: [{ value: "", label: "用户全局" }, ...getAdminGovernanceDirectoryProjectOptions(current.targetUserId)] },
|
||||
{ name: "platform", label: "平台", type: "select", value: current.platform, options: getPlatformOptions() },
|
||||
...(directoryItems.length ? [
|
||||
{ name: "targetUserId", label: "目标用户", type: "select", value: current.targetUserId, options: getAdminGovernanceDirectoryUserOptions() },
|
||||
{ name: "targetProjectId", label: "目标项目", type: "select", value: current.targetProjectId, options: [{ value: "", label: "用户全局" }, ...getAdminGovernanceDirectoryProjectOptions(current.targetUserId)] },
|
||||
{ name: "platform", label: "平台", type: "select", value: current.platform, options: getPlatformOptions() }
|
||||
] : []),
|
||||
{ type: "html", label: "目录提示", html: directoryItems.length ? `<div class="task-item compact"><h4>可选目标</h4><p>当前目录里有 ${escapeHtml(formatNumber(directoryItems.length))} 位已审核账号。</p></div>` : `<div class="task-item compact"><h4>目录为空</h4><p>后端还没有返回可选账号。</p></div>` }
|
||||
],
|
||||
onOpen: () => {
|
||||
@@ -8906,6 +8927,7 @@ function syncAdminOverrideProjectOptions(targetUserId, preferredProjectId = "")
|
||||
|
||||
function openAdminOverridePolicyAction() {
|
||||
const target = getAdminOverrideTargetState();
|
||||
if (!ensureAdminOverrideTargetReady(target)) return;
|
||||
const bundle = appState.adminOverridePolicy || {};
|
||||
const current = bundle.current_version || {};
|
||||
openActionModal({
|
||||
@@ -8942,17 +8964,21 @@ function openAdminOverridePolicyAction() {
|
||||
|
||||
async function openAdminOverrideHistoryAction() {
|
||||
const target = getAdminOverrideTargetState();
|
||||
if (!ensureAdminOverrideTargetReady(target)) return;
|
||||
const history = await loadPolicyVersions(`/v2/admin/oneliner/governance/overrides/versions?target_user_id=${encodeURIComponent(target.targetUserId)}&target_project_id=${encodeURIComponent(target.targetProjectId)}&platform=${encodeURIComponent(target.platform)}`);
|
||||
const selectedVersionId = history.items[0]?.id || "";
|
||||
openActionModal({
|
||||
title: "管理员覆盖历史",
|
||||
description: "查看当前目标的管理员覆盖版本,并从历史里选择一个版本回滚。",
|
||||
submitLabel: "回滚到所选版本",
|
||||
hideSubmit: !selectedVersionId,
|
||||
fields: [
|
||||
{ type: "html", label: "当前目标", html: renderPolicyVersionSummary(appState.adminOverridePolicy || {}, `当前查看的是 ${formatAdminGovernanceTargetLabel(target)} 的覆盖历史。`) },
|
||||
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "当前目标还没有历史版本。") },
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:这版覆盖太激进,需要恢复到上一版" }
|
||||
...(selectedVersionId ? [
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:这版覆盖太激进,需要恢复到上一版" }
|
||||
] : [])
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/overrides/rollback", {
|
||||
@@ -8974,17 +9000,21 @@ async function openAdminOverrideHistoryAction() {
|
||||
}
|
||||
|
||||
async function openSystemMainPolicyHistoryAction() {
|
||||
if (!ensureAdminGovernanceAccess()) return;
|
||||
const history = await loadPolicyVersions("/v2/admin/oneliner/governance/system/main-agent/versions");
|
||||
const selectedVersionId = history.items[0]?.id || "";
|
||||
openActionModal({
|
||||
title: "系统主 Agent 历史",
|
||||
description: "查看系统主 Agent 的历史版本,并选择某个版本回滚。",
|
||||
submitLabel: "回滚到所选版本",
|
||||
hideSubmit: !selectedVersionId,
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.adminSystemMainPolicy || {}, "系统主 Agent 还没有历史版本。") },
|
||||
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "系统主 Agent 还没有历史版本。") },
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版系统主 Agent 策略" }
|
||||
...(selectedVersionId ? [
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版系统主 Agent 策略" }
|
||||
] : [])
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent/rollback", {
|
||||
@@ -9003,6 +9033,7 @@ async function openSystemMainPolicyHistoryAction() {
|
||||
}
|
||||
|
||||
async function openSystemPlatformPolicyHistoryAction(platform) {
|
||||
if (!ensureAdminGovernanceAccess()) return;
|
||||
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
|
||||
const history = await loadPolicyVersions(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}/versions`);
|
||||
const selectedVersionId = history.items[0]?.id || "";
|
||||
@@ -9011,11 +9042,14 @@ async function openSystemPlatformPolicyHistoryAction(platform) {
|
||||
title: `${platformLabel(normalizedPlatform)} 系统平台历史`,
|
||||
description: "查看该平台的系统默认策略历史,并选择某个版本回滚。",
|
||||
submitLabel: "回滚到所选版本",
|
||||
hideSubmit: !selectedVersionId,
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前 ${platformLabel(normalizedPlatform)} 还没有系统平台历史版本。`) },
|
||||
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, `${platformLabel(normalizedPlatform)} 还没有历史版本。`) },
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版平台默认方法论" }
|
||||
...(selectedVersionId ? [
|
||||
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: safeArray(history.items).map((item) => ({ value: item.id, label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}` })) },
|
||||
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到上一版平台默认方法论" }
|
||||
] : [])
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}/rollback`, {
|
||||
|
||||
@@ -639,6 +639,32 @@ test("governance UI exposes admin override target picker and history rollback en
|
||||
assert.match(actions, /name === "open-system-platform-policy-history"/);
|
||||
});
|
||||
|
||||
test("admin governance history actions stay read-only when there are no versions yet", () => {
|
||||
const overrideHistory = extractBetween(APP, "async function openAdminOverrideHistoryAction()", "async function openSystemMainPolicyHistoryAction()");
|
||||
const systemMainHistory = extractBetween(APP, "async function openSystemMainPolicyHistoryAction()", "async function openSystemPlatformPolicyHistoryAction(platform)");
|
||||
const systemPlatformHistory = extractBetween(APP, "async function openSystemPlatformPolicyHistoryAction(platform)", "function openPlatformAgentProfileAction(platform)");
|
||||
|
||||
assert.match(overrideHistory, /hideSubmit:\s*!selectedVersionId/);
|
||||
assert.match(systemMainHistory, /hideSubmit:\s*!selectedVersionId/);
|
||||
assert.match(systemPlatformHistory, /hideSubmit:\s*!selectedVersionId/);
|
||||
assert.match(overrideHistory, /\.\.\.\(selectedVersionId \? \[/);
|
||||
assert.match(systemMainHistory, /\.\.\.\(selectedVersionId \? \[/);
|
||||
assert.match(systemPlatformHistory, /\.\.\.\(selectedVersionId \? \[/);
|
||||
});
|
||||
|
||||
test("admin governance actions apply local permission and empty-directory guards before mutating", () => {
|
||||
const helpers = extractBetween(APP, "function ensureAdminGovernanceAccess()", "function openUserGlobalPolicyAction()");
|
||||
const targetPicker = extractBetween(APP, "async function openAdminOverrideTargetAction()", "function openAdminOverridePolicyAction()");
|
||||
const systemMain = extractBetween(APP, "function openSystemMainPolicyAction()", "function openSystemPlatformPolicyAction(platform)");
|
||||
const systemPlatform = extractBetween(APP, "function openSystemPlatformPolicyAction(platform)", "async function openAdminOverrideTargetAction()");
|
||||
|
||||
assert.match(helpers, /function ensureAdminGovernanceAccess\(\)/);
|
||||
assert.match(helpers, /function ensureAdminOverrideTargetReady\(target\)/);
|
||||
assert.match(targetPicker, /hideSubmit:\s*!directoryItems\.length/);
|
||||
assert.match(systemMain, /if \(!ensureAdminGovernanceAccess\(\)\) return;/);
|
||||
assert.match(systemPlatform, /if \(!ensureAdminGovernanceAccess\(\)\) return;/);
|
||||
});
|
||||
|
||||
test("user governance UI exposes personal history and rollback entrypoints", () => {
|
||||
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
|
||||
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
|
||||
@@ -660,6 +686,17 @@ test("user governance UI exposes personal history and rollback entrypoints", ()
|
||||
assert.match(actions, /name === "open-user-platform-policy-history"/);
|
||||
});
|
||||
|
||||
test("admin override actions guard against missing governance targets", () => {
|
||||
const override = extractBetween(APP, "function openAdminOverridePolicyAction()", "async function openAdminOverrideHistoryAction()");
|
||||
const overrideHistory = extractBetween(APP, "async function openAdminOverrideHistoryAction()", "async function openSystemMainPolicyHistoryAction()");
|
||||
const helpers = extractBetween(APP, "function ensureAdminGovernanceAccess()", "function openUserGlobalPolicyAction()");
|
||||
|
||||
assert.match(helpers, /function ensureAdminOverrideTargetReady\(target\)/);
|
||||
assert.match(helpers, /当前治理目录里还没有可选用户/);
|
||||
assert.match(override, /if \(!ensureAdminOverrideTargetReady\(target\)\) return;/);
|
||||
assert.match(overrideHistory, /if \(!ensureAdminOverrideTargetReady\(target\)\) return;/);
|
||||
});
|
||||
|
||||
test("main agent result rendering offers a direct route back into the recommended screen", () => {
|
||||
const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)");
|
||||
const lastAction = extractBetween(APP, "function renderLastActionCard()", "function getJobRecoveryCategory(job)");
|
||||
|
||||
Reference in New Issue
Block a user