feat: add oneliner policy history controls

This commit is contained in:
kris
2026-03-29 16:42:12 +08:00
parent cb17fb0760
commit 26f86f8484
6 changed files with 719 additions and 11 deletions

View File

@@ -1610,6 +1610,52 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
raise HTTPException(status_code=400, detail="Target project does not belong to target user")
return row
def _governance_directory_payload() -> dict[str, Any]:
account_rows = legacy.db.fetch_all(
"""
SELECT id, username, display_name, role, approval_status, created_at, updated_at
FROM accounts
WHERE approval_status = 'approved'
ORDER BY CASE WHEN role = 'super_admin' THEN 0 ELSE 1 END ASC, updated_at DESC, created_at DESC
"""
)
project_rows = legacy.db.fetch_all(
"""
SELECT id, user_id, name, description, created_at, updated_at
FROM projects
ORDER BY updated_at DESC, created_at DESC
"""
)
projects_by_user: dict[str, list[dict[str, Any]]] = {}
for row in project_rows:
projects_by_user.setdefault(row.get("user_id", ""), []).append(
{
"id": row["id"],
"user_id": row.get("user_id", ""),
"name": row.get("name", ""),
"description": row.get("description", ""),
"created_at": row.get("created_at", ""),
"updated_at": row.get("updated_at", ""),
}
)
items = []
for row in account_rows:
projects = projects_by_user.get(row["id"], [])
items.append(
{
"id": row["id"],
"username": row.get("username", ""),
"display_name": row.get("display_name", ""),
"role": row.get("role", ""),
"approval_status": row.get("approval_status", ""),
"project_count": len(projects),
"projects": projects,
"created_at": row.get("created_at", ""),
"updated_at": row.get("updated_at", ""),
}
)
return {"items": items, "count": len(items)}
def _effective_policy_payload(
*,
subject_account: dict[str, Any],
@@ -4770,6 +4816,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
)
return payload
@app.get("/v2/admin/oneliner/governance/directory")
def get_admin_governance_directory(
admin: dict[str, Any] = Depends(legacy.require_super_admin),
) -> dict[str, Any]:
_ = admin
return _governance_directory_payload()
@app.get("/v2/admin/oneliner/governance/overrides")
def get_admin_override_policy(
target_user_id: str = Query(default=""),

View File

@@ -16,7 +16,8 @@ REMOTE_WEB_DIR="$REMOTE_WEB_PARENT/storyforge-web-v4"
REMOTE_ASSETS_DIR="$REMOTE_WEB_DIR/assets"
REMOTE_COMPOSE_DIR="${STORYFORGE_FNOS_COMPOSE_DIR:-/vol1/docker/hyzq-stack/current/deploy/fnos}"
REMOTE_COMPOSE_FILE="$REMOTE_COMPOSE_DIR/storyforge-fnos-web-v4.compose.yaml"
BACKEND_URL="${STORYFORGE_FNOS_BACKEND_URL:-https://storyforge.hyzq.net}"
COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}"
BACKEND_URL="${STORYFORGE_FNOS_BACKEND_URL:-${STORYFORGE_FNOS_COLLECTOR_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}}"
WEB_PORT="${STORYFORGE_WEB_V4_DEV_PORT:-19192}"
need_cmd() {

View File

@@ -311,6 +311,92 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(payload["effective_policy"]["tone"]["style"], "default")
self.assertEqual(payload["layers"][0]["current_version"]["title"], "Current system baseline")
def test_admin_governance_directory_lists_accounts_and_projects(self) -> None:
response = self.client.get(
"/v2/admin/oneliner/governance/directory",
headers=self.ctx["admin_headers"],
)
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertGreaterEqual(payload["count"], 2)
member = next((item for item in payload["items"] if item["id"] == self.ctx["member_id"]), None)
self.assertIsNotNone(member)
assert member is not None
self.assertEqual(member["project_count"], 1)
self.assertEqual(member["projects"][0]["id"], self.ctx["project_id"])
def test_admin_override_versions_support_rollback(self) -> None:
first_response = self.client.post(
"/v2/admin/oneliner/governance/overrides",
headers=self.ctx["admin_headers"],
json={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
"title": "Override v1",
"summary": "first override",
"policy": {"actions": {"max_cards": 2}},
"reason": "first override",
},
)
self.assertEqual(first_response.status_code, 200, first_response.text)
first_version_id = first_response.json()["current_version"]["id"]
second_response = self.client.post(
"/v2/admin/oneliner/governance/overrides",
headers=self.ctx["admin_headers"],
json={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
"title": "Override v2",
"summary": "second override",
"policy": {"actions": {"max_cards": 5}},
"reason": "second override",
},
)
self.assertEqual(second_response.status_code, 200, second_response.text)
versions_before = self.client.get(
"/v2/admin/oneliner/governance/overrides/versions",
headers=self.ctx["admin_headers"],
params={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
},
)
self.assertEqual(versions_before.status_code, 200, versions_before.text)
self.assertEqual(versions_before.json()["count"], 2)
rollback_response = self.client.post(
"/v2/admin/oneliner/governance/overrides/rollback",
headers=self.ctx["admin_headers"],
json={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
"version_id": first_version_id,
"reason": "rollback to v1",
},
)
self.assertEqual(rollback_response.status_code, 200, rollback_response.text)
rollback_payload = rollback_response.json()
self.assertEqual(rollback_payload["current_version"]["rollback_from_version_id"], first_version_id)
self.assertEqual(rollback_payload["effective_policy"]["actions"]["max_cards"], 2)
versions_after = self.client.get(
"/v2/admin/oneliner/governance/overrides/versions",
headers=self.ctx["admin_headers"],
params={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
},
)
self.assertEqual(versions_after.status_code, 200, versions_after.text)
self.assertEqual(versions_after.json()["count"], 3)
def test_user_global_versions_support_rollback_by_creating_new_version(self) -> None:
first_response = self.client.put(
"/v2/oneliner/governance/user/global",
@@ -367,6 +453,62 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(versions_after.status_code, 200, versions_after.text)
self.assertEqual(versions_after.json()["count"], 3)
def test_user_platform_versions_support_rollback_by_creating_new_version(self) -> None:
first_response = self.client.put(
"/v2/oneliner/governance/user/platforms/douyin",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"title": "Douyin strategy v1",
"policy": {"douyin": {"benchmark_mode": "strict"}},
"reason": "first platform pass",
},
)
self.assertEqual(first_response.status_code, 200, first_response.text)
first_version_id = first_response.json()["current_version"]["id"]
second_response = self.client.put(
"/v2/oneliner/governance/user/platforms/douyin",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"title": "Douyin strategy v2",
"policy": {"douyin": {"benchmark_mode": "aggressive"}},
"reason": "push harder",
},
)
self.assertEqual(second_response.status_code, 200, second_response.text)
versions_before = self.client.get(
"/v2/oneliner/governance/user/platforms/douyin/versions",
headers=self.ctx["member_headers"],
params={"project_id": self.ctx["project_id"]},
)
self.assertEqual(versions_before.status_code, 200, versions_before.text)
self.assertEqual(versions_before.json()["count"], 2)
rollback_response = self.client.post(
"/v2/oneliner/governance/user/platforms/douyin/rollback",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"version_id": first_version_id,
"reason": "restore previous platform strategy",
},
)
self.assertEqual(rollback_response.status_code, 200, rollback_response.text)
rollback_payload = rollback_response.json()
self.assertEqual(rollback_payload["current_version"]["rollback_from_version_id"], first_version_id)
self.assertEqual(rollback_payload["effective_policy"]["douyin"]["benchmark_mode"], "strict")
versions_after = self.client.get(
"/v2/oneliner/governance/user/platforms/douyin/versions",
headers=self.ctx["member_headers"],
params={"project_id": self.ctx["project_id"]},
)
self.assertEqual(versions_after.status_code, 200, versions_after.text)
self.assertEqual(versions_after.json()["count"], 3)
def test_non_admin_cannot_change_system_defaults(self) -> None:
response = self.client.put(
"/v2/admin/oneliner/governance/system/main-agent",

View File

@@ -249,6 +249,13 @@ class ProductionBaselineTests(unittest.TestCase):
]:
self.assertIn(expected, content)
def test_web_deploy_script_defaults_to_lan_collector(self) -> None:
script_path = ROOT / "scripts" / "deploy_fnos_storyforge_web.sh"
content = script_path.read_text(encoding="utf-8")
self.assertIn('COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}"', content)
self.assertIn('BACKEND_URL="${STORYFORGE_FNOS_BACKEND_URL:-${STORYFORGE_FNOS_COLLECTOR_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}}"', content)
self.assertNotIn('https://storyforge.hyzq.net', content)
def test_baseline_script_covers_homepage_dashboard_node_test(self) -> None:
script = (ROOT / "scripts" / "check_repo_baseline.sh").read_text(encoding="utf-8")
self.assertIn("dashboard-home.test.mjs", script)

View File

@@ -59,6 +59,9 @@ const appState = {
userCurrentPlatformPolicy: null,
adminSystemMainPolicy: null,
adminSystemPlatformPolicies: [],
adminGovernanceDirectory: null,
adminOverrideTarget: null,
adminOverridePolicy: null,
tenantQuota: null,
tenantUsage: null,
adminOpsOverview: null,
@@ -1261,6 +1264,9 @@ async function logoutSession() {
appState.userCurrentPlatformPolicy = null;
appState.adminSystemMainPolicy = null;
appState.adminSystemPlatformPolicies = [];
appState.adminGovernanceDirectory = null;
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
appState.tenantQuota = null;
appState.tenantUsage = null;
appState.adminOpsOverview = null;
@@ -1312,12 +1318,14 @@ async function loadAgentControlSurfaces(projectId = "") {
const supportsUserPlatformPolicy = backendSupports("/v2/oneliner/governance/user/platforms/{platform}");
const supportsAdminSystemMainPolicy = backendSupports("/v2/admin/oneliner/governance/system/main-agent");
const supportsAdminSystemPlatformPolicy = backendSupports("/v2/admin/oneliner/governance/system/platforms/{platform}");
const supportsAdminGovernanceDirectory = backendSupports("/v2/admin/oneliner/governance/directory");
const supportsAdminOverridePolicy = backendSupports("/v2/admin/oneliner/governance/overrides");
const supportsAdminOps = backendSupports("/v2/admin/ops/overview");
const supportsAdminFixRuns = backendSupports("/v2/admin/ops/fix-runs");
const supportsTenantQuota = backendSupports("/v2/tenant/quota");
const supportsTenantUsage = backendSupports("/v2/tenant/usage");
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, adminSystemMainPolicy, adminSystemPlatformPolicies, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, adminSystemMainPolicy, adminSystemPlatformPolicies, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
supportsOneLinerProfile
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
@@ -1347,6 +1355,9 @@ async function loadAgentControlSurfaces(projectId = "") {
storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(item.value)}`).catch(() => null)
))
: Promise.resolve([]),
supportsAdminGovernanceDirectory && isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/directory").catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsTenantQuota
? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
@@ -1373,6 +1384,24 @@ async function loadAgentControlSurfaces(projectId = "") {
appState.userCurrentPlatformPolicy = userCurrentPlatformPolicy;
appState.adminSystemMainPolicy = adminSystemMainPolicy;
appState.adminSystemPlatformPolicies = safeArray(adminSystemPlatformPolicies);
appState.adminGovernanceDirectory = safeArray(adminGovernanceDirectory?.items || adminGovernanceDirectory);
if (isSuperAdmin() && supportsAdminOverridePolicy && appState.adminGovernanceDirectory.length) {
const existingTarget = appState.adminOverrideTarget || {};
const targetUserId = String(existingTarget.targetUserId || existingTarget.target_user_id || appState.adminGovernanceDirectory[0]?.id || "");
const targetUser = appState.adminGovernanceDirectory.find((item) => item.id === targetUserId) || appState.adminGovernanceDirectory[0] || null;
const targetProjects = safeArray(targetUser?.projects);
const targetProjectId = String(existingTarget.targetProjectId || existingTarget.target_project_id || targetProjects[0]?.id || "");
const targetPlatform = normalizePlatformValue(existingTarget.platform || governancePlatform, "douyin");
appState.adminOverrideTarget = {
targetUserId,
targetProjectId,
platform: targetPlatform
};
appState.adminOverridePolicy = await storyforgeFetch(`/v2/admin/oneliner/governance/overrides?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}`).catch(() => null);
} else {
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
}
appState.tenantQuota = tenantQuota;
appState.tenantUsage = tenantUsage;
appState.adminOpsOverview = adminOpsOverview;
@@ -3203,9 +3232,86 @@ function summarizePolicyHighlights(policy = {}, platform = "") {
return items.slice(0, 4);
}
function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction = "", primaryLabel = "编辑策略", secondaryAction = "", secondaryLabel = "", secondaryPlatform = "" }) {
function getGovernanceDirectoryItems() {
return safeArray(appState.adminGovernanceDirectory?.items || appState.adminGovernanceDirectory);
}
function parseAdminOverrideScopeValue(value) {
const normalized = String(value || "").trim();
if (!normalized) return { targetUserId: "", targetProjectId: "" };
if (normalized.startsWith("project:")) {
const parts = normalized.slice("project:".length).split("|");
return { targetUserId: parts[0] || "", targetProjectId: parts[1] || "" };
}
if (normalized.startsWith("user:")) {
return { targetUserId: normalized.slice("user:".length), targetProjectId: "" };
}
return { targetUserId: normalized, targetProjectId: "" };
}
function getAdminOverrideTargetOptions(directoryItems = getGovernanceDirectoryItems()) {
return safeArray(directoryItems).flatMap((account) => {
const accountLabel = account.display_name || account.username || account.id;
const projects = safeArray(account.projects);
return [
{ value: `user:${account.id}`, label: `${accountLabel} · 全部项目` },
...projects.map((project) => ({
value: `project:${account.id}|${project.id}`,
label: `${accountLabel} / ${project.name || project.id}`
}))
];
});
}
function normalizeAdminOverrideTarget(target, directoryItems = getGovernanceDirectoryItems(), fallbackPlatform = "") {
const items = safeArray(directoryItems);
if (!items.length) {
return { targetUserId: "", targetProjectId: "", platform: normalizePlatformValue(fallbackPlatform, "douyin") };
}
const preferred = target && target.targetUserId
? items.find((item) => item.id === target.targetUserId)
: items.find((item) => item.role !== "super_admin") || items[0];
const preferredProjects = safeArray(preferred?.projects);
const project =
target?.targetProjectId
? preferredProjects.find((item) => item.id === target.targetProjectId)
: preferredProjects[0] || null;
return {
targetUserId: preferred?.id || "",
targetProjectId: project?.id || "",
platform: target?.platform === "" ? "" : normalizePlatformValue(target?.platform || fallbackPlatform, "douyin")
};
}
function findGovernanceDirectoryAccount(userId) {
return getGovernanceDirectoryItems().find((item) => item.id === userId) || null;
}
function findGovernanceDirectoryProject(userId, projectId) {
const account = findGovernanceDirectoryAccount(userId);
return safeArray(account?.projects).find((item) => item.id === projectId) || null;
}
function getAdminOverrideTargetSummary(target = appState.adminOverrideTarget) {
const normalized = target || {};
const account = findGovernanceDirectoryAccount(normalized.targetUserId || "");
const project = normalized.targetProjectId ? findGovernanceDirectoryProject(normalized.targetUserId || "", normalized.targetProjectId) : null;
return {
account,
project,
accountLabel: account?.display_name || account?.username || normalized.targetUserId || "未选择用户",
projectLabel: project?.name || (normalized.targetProjectId ? normalized.targetProjectId : "全部项目"),
platformLabel: normalized.platform ? platformLabel(normalized.platform) : "全部平台"
};
}
function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction = "", primaryLabel = "编辑策略", secondaryAction = "", secondaryLabel = "", secondaryPlatform = "", actions = null }) {
const layers = safeArray(effective?.layers);
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || secondaryPlatform || "");
const resolvedActions = safeArray(actions?.length ? actions : [
primaryAction ? { action: primaryAction, label: primaryLabel } : null,
secondaryAction ? { action: secondaryAction, label: secondaryLabel, platform: secondaryPlatform } : null
].filter(Boolean));
return `
<div class="task-item compact">
<h4>${escapeHtml(title)}</h4>
@@ -3214,10 +3320,13 @@ function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction
${layers.map((layer) => `<span class="tag ${layer.scope_kind === "admin_override" ? "orange" : "blue"}">${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}</span>`).join("") || `<span class="tag">尚未发布</span>`}
${highlights.map((item) => `<span class="tag green">${escapeHtml(item)}</span>`).join("")}
</div>
${(primaryAction || secondaryAction) ? `
${resolvedActions.length ? `
<div class="task-meta" style="margin-top:10px;">
${primaryAction ? `<span class="tag clickable-tag" data-action="${escapeHtml(primaryAction)}">${escapeHtml(primaryLabel)}</span>` : ""}
${secondaryAction ? `<span class="tag clickable-tag" data-action="${escapeHtml(secondaryAction)}" ${secondaryPlatform ? `data-platform="${escapeHtml(secondaryPlatform)}"` : ""}>${escapeHtml(secondaryLabel)}</span>` : ""}
${resolvedActions.map((item) => `
<span class="tag clickable-tag" data-action="${escapeHtml(item.action || "")}" ${item.platform ? `data-platform="${escapeHtml(item.platform)}"` : ""}>
${escapeHtml(item.label || "查看")}
</span>
`).join("")}
</div>
` : ""}
</div>
@@ -3228,6 +3337,8 @@ function renderAdminGovernanceSummaryPanel() {
const systemMain = appState.adminSystemMainPolicy;
const systemPlatforms = safeArray(appState.adminSystemPlatformPolicies);
const configuredPlatforms = systemPlatforms.filter((item) => item?.current_version);
const targetSummary = getAdminOverrideTargetSummary();
const overrideBundle = appState.adminOverridePolicy;
return `
<div class="panel pad" style="box-shadow:none; margin-bottom:18px;">
<div class="panel-head">
@@ -3248,6 +3359,20 @@ function renderAdminGovernanceSummaryPanel() {
<div class="entity-meta">
<span class="tag ${systemMain?.current_version ? "green" : "orange"}">${escapeHtml(systemMain?.current_version ? `版本 ${formatNumber(systemMain.current_version.version_no)}` : "未发布")}</span>
<span class="tag">历史 ${escapeHtml(formatNumber(systemMain?.versions?.count || 0))}</span>
<span class="tag clickable-tag" data-action="open-system-main-policy-history">历史与回滚</span>
</div>
</div>
<div class="entity-card pad">
<div class="cell-title">管理员覆盖</div>
<div class="cell-desc">${escapeHtml(overrideBundle?.current_version?.summary || `${targetSummary.accountLabel} / ${targetSummary.projectLabel} / ${targetSummary.platformLabel}`)}</div>
<div class="entity-meta">
<span class="tag blue">${escapeHtml(targetSummary.accountLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.projectLabel)}</span>
<span class="tag">${escapeHtml(targetSummary.platformLabel)}</span>
<span class="tag ${overrideBundle?.current_version ? "orange" : "blue"}">${escapeHtml(overrideBundle?.current_version ? `版本 ${formatNumber(overrideBundle.current_version.version_no)}` : "未发布")}</span>
<span class="tag clickable-tag" data-action="open-admin-override-target">切换目标</span>
<span class="tag clickable-tag" data-action="open-admin-override-policy">编辑覆盖</span>
<span class="tag clickable-tag" data-action="open-admin-override-history">历史与回滚</span>
</div>
</div>
${ACTIVE_PLATFORMS.map((platformItem) => {
@@ -3260,6 +3385,7 @@ function renderAdminGovernanceSummaryPanel() {
<span class="tag ${item?.current_version ? "green" : "blue"}">${escapeHtml(item?.current_version ? `版本 ${formatNumber(item.current_version.version_no)}` : "沿用系统默认")}</span>
<span class="tag">历史 ${escapeHtml(formatNumber(item?.versions?.count || 0))}</span>
<span class="tag clickable-tag" data-action="open-system-platform-policy" data-platform="${escapeHtml(platformItem.value)}">编辑</span>
<span class="tag clickable-tag" data-action="open-system-platform-policy-history" data-platform="${escapeHtml(platformItem.value)}">历史与回滚</span>
</div>
</div>
`;
@@ -4755,11 +4881,26 @@ function renderPlaybookScreen() {
title: "我的策略与历史",
subtitle: appState.userGlobalPolicy?.current_version?.summary || "你和主 Agent 的策略对话,会先沉淀成用户全局策略,再按需要下放到单平台。",
effective: appState.onelinerGovernanceEffective,
primaryAction: "open-user-global-policy",
primaryLabel: `编辑全局策略 · 历史 ${formatNumber(appState.userGlobalPolicy?.versions?.count || 0)}`,
secondaryAction: "open-user-platform-policy",
secondaryLabel: `编辑当前平台策略 · 历史 ${formatNumber(appState.userCurrentPlatformPolicy?.versions?.count || 0)}`,
secondaryPlatform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform()
actions: [
{
action: "open-user-global-policy",
label: `编辑全局策略 · 历史 ${formatNumber(appState.userGlobalPolicy?.versions?.count || 0)}`
},
{
action: "open-user-global-policy-history",
label: "查看全局历史"
},
{
action: "open-user-platform-policy",
label: `编辑当前平台策略 · 历史 ${formatNumber(appState.userCurrentPlatformPolicy?.versions?.count || 0)}`,
platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform()
},
{
action: "open-user-platform-policy-history",
label: "查看当前平台历史",
platform: appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform()
}
]
})}
</div>
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
@@ -6409,6 +6550,82 @@ function renderPolicyVersionSummary(bundle, emptyText) {
`;
}
function getAdminGovernanceDirectoryItems() {
return safeArray(appState.adminGovernanceDirectory);
}
function findAdminGovernanceDirectoryItem(targetUserId) {
return getAdminGovernanceDirectoryItems().find((item) => item.id === targetUserId) || null;
}
function getAdminOverrideTargetState() {
const directoryItems = getAdminGovernanceDirectoryItems();
const existing = appState.adminOverrideTarget || {};
const targetUserId = String(existing.targetUserId || existing.target_user_id || directoryItems[0]?.id || "");
const targetUser = findAdminGovernanceDirectoryItem(targetUserId) || directoryItems[0] || null;
const targetProjects = safeArray(targetUser?.projects);
const targetProjectId = String(existing.targetProjectId || existing.target_project_id || targetProjects[0]?.id || "");
return {
targetUserId,
targetProjectId,
platform: normalizePlatformValue(existing.platform || getPreferredPlatform(), "douyin")
};
}
function formatAdminGovernanceTargetLabel(target) {
const directoryItem = findAdminGovernanceDirectoryItem(target?.targetUserId || target?.target_user_id || "");
const project = safeArray(directoryItem?.projects).find((item) => item.id === (target?.targetProjectId || target?.target_project_id || ""));
const userLabel = directoryItem ? `${directoryItem.display_name || directoryItem.username || directoryItem.id}${directoryItem.role ? ` · ${directoryItem.role}` : ""}` : "未选择目标";
const projectLabel = project ? project.name || project.id : "默认用户全局";
return `${userLabel} / ${projectLabel}`;
}
function getAdminGovernanceDirectoryUserOptions() {
return getAdminGovernanceDirectoryItems().map((item) => ({
value: item.id,
label: `${item.display_name || item.username || item.id}${item.project_count ? ` · ${formatNumber(item.project_count)} 项目` : ""}`
}));
}
function getAdminGovernanceDirectoryProjectOptions(targetUserId) {
const directoryItem = findAdminGovernanceDirectoryItem(targetUserId);
return safeArray(directoryItem?.projects).map((item) => ({
value: item.id,
label: item.name || item.id
}));
}
function renderPolicyVersionsHtml(items, emptyText = "暂无历史版本。") {
const versions = safeArray(items);
if (!versions.length) {
return `<div class="task-item compact"><h4>还没有历史版本</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
return versions.slice(0, 8).map((version) => `
<div class="task-item compact">
<h4>${escapeHtml(version.title || `版本 ${formatNumber(version.version_no || 0)}`)}</h4>
<p>${escapeHtml(version.summary || "没有补充摘要。")}</p>
<div class="task-meta">
<span class="tag blue">版本 ${escapeHtml(formatNumber(version.version_no || 0))}</span>
${version.created_at ? `<span class="tag">${escapeHtml(formatDateTime(version.created_at))}</span>` : ""}
${version.rollback_from_version_id ? `<span class="tag orange">回滚生成</span>` : ""}
</div>
</div>
`).join("");
}
async function loadPolicyVersions(url) {
const payload = await storyforgeFetch(url).catch(() => ({ items: [] }));
const items = safeArray(payload?.items || payload);
return { items, count: Number(payload?.count || items.length) };
}
function buildPolicyVersionOptions(history) {
return safeArray(history?.items).map((item) => ({
value: item.id,
label: `v${formatNumber(item.version_no || 0)} · ${item.title || brief(item.summary || item.id, 24)}`
}));
}
function openUserGlobalPolicyAction() {
const project = requireSelectedProject();
const bundle = appState.userGlobalPolicy || {};
@@ -6478,6 +6695,77 @@ function openUserPlatformPolicyAction(platform) {
});
}
async function openUserGlobalPolicyHistoryAction() {
const project = requireSelectedProject();
const history = await loadPolicyVersions(`/v2/oneliner/governance/user/global/versions?project_id=${encodeURIComponent(project.id)}`);
const selectedVersionId = history.items[0]?.id || "";
const versionOptions = buildPolicyVersionOptions(history);
openActionModal({
title: "我的全局策略历史",
description: "查看你自己的全局策略版本,并从历史里选择一个版本回滚。回滚不会改旧记录,而是会生成一个新的生效版本。",
submitLabel: "回滚到所选版本",
hideSubmit: !selectedVersionId,
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.userGlobalPolicy || {}, "你还没有发布自己的全局策略。") },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, "你的全局策略还没有历史版本。") },
...(selectedVersionId ? [
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: versionOptions },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到更稳妥的首页动作和语气策略" }
] : [])
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/oneliner/governance/user/global/rollback", {
method: "POST",
body: {
project_id: project.id,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.userGlobalPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction("我的全局策略已回滚", `已生成回滚版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openUserPlatformPolicyHistoryAction(platform) {
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
const project = requireSelectedProject();
const history = await loadPolicyVersions(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}/versions?project_id=${encodeURIComponent(project.id)}`);
const selectedVersionId = history.items[0]?.id || "";
const versionOptions = buildPolicyVersionOptions(history);
openActionModal({
title: `${platformLabel(normalizedPlatform)} 平台策略历史`,
description: "查看该平台的个人策略版本,并从历史里选择一个版本回滚。回滚只影响当前平台,不会改动其他平台。",
submitLabel: "回滚到所选版本",
hideSubmit: !selectedVersionId,
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(appState.userCurrentPlatformPolicy || {}, `你还没有发布 ${platformLabel(normalizedPlatform)} 平台策略。`) },
{ type: "html", label: "历史版本", html: renderPolicyVersionsHtml(history.items, `${platformLabel(normalizedPlatform)} 还没有历史版本。`) },
...(selectedVersionId ? [
{ name: "versionId", label: "回滚版本", type: "select", value: selectedVersionId, options: versionOptions },
{ name: "reason", label: "回滚原因", type: "textarea", rows: 3, value: "", placeholder: "例如:恢复到更适合这个平台的拆解方式" }
] : [])
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}/rollback`, {
method: "POST",
body: {
project_id: project.id,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.userCurrentPlatformPolicy = saved;
await loadAgentControlSurfaces(project.id);
rememberAction(`${platformLabel(normalizedPlatform)} 平台策略已回滚`, `已生成回滚版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
function openSystemMainPolicyAction() {
const projectId = getOneLinerProjectId();
const bundle = appState.adminSystemMainPolicy || {};
@@ -6548,6 +6836,165 @@ function openSystemPlatformPolicyAction(platform) {
});
}
async function openAdminOverrideTargetAction() {
const current = getAdminOverrideTargetState();
const directoryItems = getAdminGovernanceDirectoryItems();
openActionModal({
title: "选择管理员覆盖目标",
description: "先选中要覆盖的用户、项目和平台,再去编辑覆盖策略或查看历史。",
submitLabel: "保存目标",
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() },
{ 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>` }
],
onSubmit: async (values) => {
appState.adminOverrideTarget = {
targetUserId: String(values.targetUserId || ""),
targetProjectId: String(values.targetProjectId || ""),
platform: normalizePlatformValue(values.platform || "douyin", "douyin")
};
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖目标已更新", `当前目标已切换到 ${formatAdminGovernanceTargetLabel(appState.adminOverrideTarget)}`, "green");
renderAll();
}
});
}
function openAdminOverridePolicyAction() {
const target = getAdminOverrideTargetState();
const bundle = appState.adminOverridePolicy || {};
const current = bundle.current_version || {};
openActionModal({
title: "编辑管理员覆盖策略",
description: "这层策略只作用于当前选中的目标,会叠加在用户策略和系统默认之上。",
submitLabel: "保存覆盖策略",
fields: [
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前还没有为 ${formatAdminGovernanceTargetLabel(target)} 发布覆盖策略。`) },
{ name: "title", label: "策略标题", value: current.title || `管理员覆盖:${formatAdminGovernanceTargetLabel(target)}`, placeholder: "例如:重点账号短期放量覆盖" },
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚这层覆盖是为了什么目标" },
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"guardrails\":{\"require_admin_review\":true}}" },
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:对该账号/项目临时放宽首页动作数量" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/overrides", {
method: "POST",
body: {
target_user_id: target.targetUserId,
target_project_id: target.targetProjectId,
platform: target.platform,
title: values.title || `管理员覆盖:${formatAdminGovernanceTargetLabel(target)}`,
summary: values.summary || "",
policy: parsePolicyJsonField(values.policyJson, "管理员覆盖策略 JSON"),
reason: values.reason || ""
}
});
appState.adminOverridePolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖策略已保存", `已为 ${formatAdminGovernanceTargetLabel(target)} 发布版本 ${saved.current_version?.version_no || 1}`, "green", saved);
renderAll();
}
});
}
async function openAdminOverrideHistoryAction() {
const target = getAdminOverrideTargetState();
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: "回滚到所选版本",
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: "例如:这版覆盖太激进,需要恢复到上一版" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/overrides/rollback", {
method: "POST",
body: {
target_user_id: target.targetUserId,
target_project_id: target.targetProjectId,
platform: target.platform,
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminOverridePolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("管理员覆盖已回滚", `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openSystemMainPolicyHistoryAction() {
const history = await loadPolicyVersions("/v2/admin/oneliner/governance/system/main-agent/versions");
const selectedVersionId = history.items[0]?.id || "";
openActionModal({
title: "系统主 Agent 历史",
description: "查看系统主 Agent 的历史版本,并选择某个版本回滚。",
submitLabel: "回滚到所选版本",
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 策略" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent/rollback", {
method: "POST",
body: {
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminSystemMainPolicy = saved;
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction("系统主 Agent 已回滚", `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
async function openSystemPlatformPolicyHistoryAction(platform) {
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 || "";
const bundle = safeArray(appState.adminSystemPlatformPolicies).find((item) => item?.scope?.platform === normalizedPlatform) || {};
openActionModal({
title: `${platformLabel(normalizedPlatform)} 系统平台历史`,
description: "查看该平台的系统默认策略历史,并选择某个版本回滚。",
submitLabel: "回滚到所选版本",
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: "例如:恢复到上一版平台默认方法论" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}/rollback`, {
method: "POST",
body: {
version_id: values.versionId || selectedVersionId,
reason: values.reason || ""
}
});
appState.adminSystemPlatformPolicies = safeArray(appState.adminSystemPlatformPolicies)
.filter((item) => item?.scope?.platform !== normalizedPlatform)
.concat(saved)
.sort((a, b) => String(a?.scope?.platform || "").localeCompare(String(b?.scope?.platform || "")));
await loadAgentControlSurfaces(getOneLinerProjectId());
rememberAction(`${platformLabel(normalizedPlatform)} 系统平台策略已回滚`, `已回滚到版本 ${saved.current_version?.version_no || "所选版本"}`, "green", saved);
renderAll();
}
});
}
function openPlatformAgentProfileAction(platform) {
const project = requireSelectedProject();
const agents = safeArray(appState.platformAgents);
@@ -8316,18 +8763,46 @@ document.addEventListener("click", async (event) => {
openUserGlobalPolicyAction();
return;
}
if (name === "open-user-global-policy-history") {
await openUserGlobalPolicyHistoryAction();
return;
}
if (name === "open-user-platform-policy") {
openUserPlatformPolicyAction(action.dataset.platform || "");
return;
}
if (name === "open-user-platform-policy-history") {
await openUserPlatformPolicyHistoryAction(action.dataset.platform || "");
return;
}
if (name === "open-system-main-policy") {
openSystemMainPolicyAction();
return;
}
if (name === "open-system-main-policy-history") {
await openSystemMainPolicyHistoryAction();
return;
}
if (name === "open-system-platform-policy") {
openSystemPlatformPolicyAction(action.dataset.platform || "");
return;
}
if (name === "open-system-platform-policy-history") {
await openSystemPlatformPolicyHistoryAction(action.dataset.platform || "");
return;
}
if (name === "open-admin-override-target") {
await openAdminOverrideTargetAction();
return;
}
if (name === "open-admin-override-policy") {
openAdminOverridePolicyAction();
return;
}
if (name === "open-admin-override-history") {
await openAdminOverrideHistoryAction();
return;
}
if (name === "select-oneliner-session") {
appState.selectedOnelinerSessionId = action.dataset.sessionId || "";
await loadOneLinerMessages(appState.selectedOnelinerSessionId);

View File

@@ -105,6 +105,8 @@ test("agent control surfaces load governance endpoints for user and admin summar
assert.match(source, /\/v2\/oneliner\/governance\/user\/platforms\/\$\{encodeURIComponent\(governancePlatform\)\}/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/system\/main-agent/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/system\/platforms\/\$\{encodeURIComponent\(item\.value\)\}/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/directory/);
assert.match(source, /\/v2\/admin\/oneliner\/governance\/overrides/);
});
test("oneliner meta and action handlers expose governance entry points", () => {
@@ -140,3 +142,31 @@ test("system governance saves refresh control surfaces after persisting", () =>
assert.match(platform, /appState\.adminSystemPlatformPolicies = safeArray\(appState\.adminSystemPlatformPolicies\)/);
assert.match(platform, /await loadAgentControlSurfaces\(projectId\);/);
});
test("governance UI exposes admin override target picker and history rollback entrypoints", () => {
const admin = extractBetween(APP, "function renderAdminGovernanceSummaryPanel()", "function renderPlatformAgentPanel()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(admin, /open-admin-override-target/);
assert.match(admin, /open-admin-override-policy/);
assert.match(admin, /open-admin-override-history/);
assert.match(admin, /open-system-main-policy-history/);
assert.match(admin, /open-system-platform-policy-history/);
assert.match(actions, /name === "open-admin-override-target"/);
assert.match(actions, /name === "open-admin-override-policy"/);
assert.match(actions, /name === "open-admin-override-history"/);
assert.match(actions, /name === "open-system-main-policy-history"/);
assert.match(actions, /name === "open-system-platform-policy-history"/);
});
test("user governance UI exposes personal history and rollback entrypoints", () => {
const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(playbook, /open-user-global-policy-history/);
assert.match(playbook, /open-user-platform-policy-history/);
assert.match(actions, /name === "open-user-global-policy-history"/);
assert.match(actions, /name === "open-user-platform-policy-history"/);
});