fix: harden main agent governance flows
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-03-31 03:19:28 +08:00
parent 2f98a1735d
commit 1f3631a648
3 changed files with 107 additions and 9 deletions

View File

@@ -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后续可以直接基于当前分支继续收剩余真实能力细节。

View File

@@ -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`, {

View File

@@ -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)");