feat: add agent governance audit surfaces

This commit is contained in:
kris
2026-03-29 17:12:44 +08:00
parent 26f86f8484
commit 8bb58be5ff
5 changed files with 469 additions and 2 deletions

View File

@@ -1480,6 +1480,49 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
assert row is not None
return _policy_audit_payload(row)
def _policy_audit_record_payload(row: dict[str, Any] | None) -> dict[str, Any] | None:
if not row:
return None
payload = _policy_audit_payload(row) or {}
scope_row = legacy.db.fetch_one("SELECT * FROM agent_policy_scopes WHERE id = ?", (row.get("scope_id", ""),)) if row.get("scope_id") else None
version_row = legacy.db.fetch_one("SELECT * FROM agent_policy_versions WHERE id = ?", (row.get("version_id", ""),)) if row.get("version_id") else None
scope_kind = (scope_row or {}).get("scope_kind", "") or (version_row or {}).get("scope_kind", "")
platform = (scope_row or {}).get("platform", "") or (version_row or {}).get("platform", "")
subject_user_id = (scope_row or {}).get("subject_user_id", "") or (version_row or {}).get("subject_user_id", "")
subject_project_id = (scope_row or {}).get("subject_project_id", "") or (version_row or {}).get("subject_project_id", "")
payload.update(
{
"scope_kind": scope_kind,
"subject_user_id": subject_user_id,
"subject_project_id": subject_project_id,
"platform": platform,
"platform_label": legacy.platform_label(platform) if platform else "",
"scope": _policy_scope_payload(
scope_row,
fallback_kind=scope_kind,
fallback_platform=platform,
fallback_user_id=subject_user_id,
fallback_project_id=subject_project_id,
),
"version": _policy_version_payload(version_row),
}
)
return payload
def _fetch_policy_audit_records(where_sql: str, params: tuple[Any, ...], *, limit: int = 20) -> list[dict[str, Any]]:
rows = legacy.db.fetch_all(
f"""
SELECT audit.*
FROM agent_policy_audit_logs audit
JOIN agent_policy_scopes scope ON scope.id = audit.scope_id
{where_sql}
ORDER BY audit.created_at DESC
LIMIT ?
""",
params + (limit,),
)
return [_policy_audit_record_payload(row) for row in rows if row]
def _create_policy_version(
scope_row: dict[str, Any],
*,
@@ -4496,6 +4539,26 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
items = _list_policy_versions(scope_row)
return {"items": items, "count": len(items)}
@app.get("/v2/oneliner/governance/user/audits")
def list_user_policy_audits(
project_id: str | None = Query(default=None),
platform: str = Query(default=""),
limit: int = Query(default=20, ge=1, le=100),
account: dict[str, Any] = Depends(legacy.require_approved),
) -> dict[str, Any]:
project = _resolve_project(account, project_id or None)
normalized_platform = _normalize_policy_platform(platform)
where_clauses = [
"WHERE scope.subject_user_id = ?",
"AND (scope.subject_project_id = ? OR scope.subject_project_id = '')",
]
params: list[Any] = [account["id"], project["id"]]
if normalized_platform:
where_clauses.append("AND (scope.platform = '' OR scope.platform = ?)")
params.append(normalized_platform)
items = _fetch_policy_audit_records(" ".join(where_clauses), tuple(params), limit=limit)
return {"items": items, "count": len(items)}
@app.post("/v2/oneliner/governance/user/global/rollback")
def rollback_user_global_policy(
request: AgentPolicyRollbackRequest,
@@ -4823,6 +4886,43 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None:
_ = admin
return _governance_directory_payload()
@app.get("/v2/admin/oneliner/governance/audits")
def list_admin_governance_audits(
target_user_id: str = Query(default=""),
target_project_id: str = Query(default=""),
platform: str = Query(default=""),
include_system: bool = Query(default=False),
limit: int = Query(default=30, ge=1, le=100),
admin: dict[str, Any] = Depends(legacy.require_super_admin),
) -> dict[str, Any]:
_ = admin
normalized_platform = _normalize_policy_platform(platform)
clauses: list[str] = []
params: list[Any] = []
if target_user_id.strip():
target_parts = ["(scope.subject_user_id = ?"]
params.append(target_user_id.strip())
if target_project_id.strip():
target_parts.append("AND (scope.subject_project_id = ? OR scope.subject_project_id = '')")
params.append(target_project_id.strip())
if normalized_platform:
target_parts.append("AND (scope.platform = '' OR scope.platform = ?)")
params.append(normalized_platform)
target_parts.append(")")
clauses.append(" ".join(target_parts))
if include_system:
if normalized_platform:
clauses.append("(scope.scope_kind = 'system_main' OR (scope.scope_kind = 'system_platform' AND scope.platform = ?))")
params.append(normalized_platform)
else:
clauses.append("(scope.scope_kind IN ('system_main', 'system_platform'))")
where_sql = "WHERE 1 = 1" if not clauses else "WHERE " + " OR ".join(f"({clause})" for clause in clauses)
items = _fetch_policy_audit_records(where_sql, tuple(params), limit=limit)
return {"items": items, "count": len(items)}
@app.get("/v2/admin/oneliner/governance/overrides")
def get_admin_override_policy(
target_user_id: str = Query(default=""),

View File

@@ -509,6 +509,85 @@ class MainAgentGovernanceTests(unittest.TestCase):
self.assertEqual(versions_after.status_code, 200, versions_after.text)
self.assertEqual(versions_after.json()["count"], 3)
def test_user_policy_audits_include_personal_and_admin_layers_for_project(self) -> None:
self.client.put(
"/v2/oneliner/governance/user/global",
headers=self.ctx["member_headers"],
json={
"project_id": self.ctx["project_id"],
"title": "Global strategy",
"policy": {"tone": {"style": "analytical"}},
"reason": "personalize defaults",
},
)
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": "Admin override",
"policy": {"actions": {"max_cards": 4}},
"reason": "contain drift",
},
)
response = self.client.get(
"/v2/oneliner/governance/user/audits",
headers=self.ctx["member_headers"],
params={"project_id": self.ctx["project_id"], "platform": "douyin"},
)
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertGreaterEqual(payload["count"], 2)
scope_kinds = [item["scope_kind"] for item in payload["items"]]
self.assertIn("user_global", scope_kinds)
self.assertIn("admin_override", scope_kinds)
first_item = payload["items"][0]
self.assertIn("version", first_item)
self.assertIn("scope", first_item)
def test_admin_policy_audits_include_target_and_system_layers(self) -> None:
self.client.put(
"/v2/admin/oneliner/governance/system/main-agent",
headers=self.ctx["admin_headers"],
json={
"title": "System main",
"policy": {"homepage": {"focus": "ops"}},
"reason": "seed system baseline",
},
)
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": "Admin override",
"policy": {"actions": {"max_cards": 5}},
"reason": "focus target account",
},
)
response = self.client.get(
"/v2/admin/oneliner/governance/audits",
headers=self.ctx["admin_headers"],
params={
"target_user_id": self.ctx["member_id"],
"target_project_id": self.ctx["project_id"],
"platform": "douyin",
"include_system": "1",
},
)
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertGreaterEqual(payload["count"], 2)
scope_kinds = [item["scope_kind"] for item in payload["items"]]
self.assertIn("system_main", scope_kinds)
self.assertIn("admin_override", scope_kinds)
def test_non_admin_cannot_change_system_defaults(self) -> None:
response = self.client.put(
"/v2/admin/oneliner/governance/system/main-agent",

View File

@@ -30,6 +30,7 @@ const appState = {
dashboardOverviewTab: "project_progress",
discoveryDetailTab: "overview",
playbookDetailTab: "workspace",
strategyDetailTab: "effective",
productionDetailTab: "queue",
automationDetailTab: "health",
adminWorkbenchTab: "integrations",
@@ -57,11 +58,13 @@ const appState = {
onelinerGovernanceEffective: null,
userGlobalPolicy: null,
userCurrentPlatformPolicy: null,
userPolicyAudits: [],
adminSystemMainPolicy: null,
adminSystemPlatformPolicies: [],
adminGovernanceDirectory: null,
adminOverrideTarget: null,
adminOverridePolicy: null,
adminPolicyAudits: [],
tenantQuota: null,
tenantUsage: null,
adminOpsOverview: null,
@@ -877,6 +880,9 @@ function openActionModal(config) {
submit.disabled = false;
submit.hidden = Boolean(config.hideSubmit);
modal.classList.remove("hidden");
if (typeof config.onOpen === "function") {
config.onOpen({ modal, title, description, fields, message, submit });
}
}
function closeActionModal() {
@@ -1262,11 +1268,13 @@ async function logoutSession() {
appState.onelinerGovernanceEffective = null;
appState.userGlobalPolicy = null;
appState.userCurrentPlatformPolicy = null;
appState.userPolicyAudits = [];
appState.adminSystemMainPolicy = null;
appState.adminSystemPlatformPolicies = [];
appState.adminGovernanceDirectory = null;
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
appState.adminPolicyAudits = [];
appState.tenantQuota = null;
appState.tenantUsage = null;
appState.adminOpsOverview = null;
@@ -1316,16 +1324,18 @@ async function loadAgentControlSurfaces(projectId = "") {
const supportsGovernanceEffective = backendSupports("/v2/oneliner/governance/effective");
const supportsUserGlobalPolicy = backendSupports("/v2/oneliner/governance/user/global");
const supportsUserPlatformPolicy = backendSupports("/v2/oneliner/governance/user/platforms/{platform}");
const supportsUserPolicyAudits = backendSupports("/v2/oneliner/governance/user/audits");
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 supportsAdminGovernanceAudits = backendSupports("/v2/admin/oneliner/governance/audits");
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, adminGovernanceDirectory, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, userPolicyAuditsPayload, 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 +1357,9 @@ async function loadAgentControlSurfaces(projectId = "") {
supportsUserPlatformPolicy
? storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsUserPolicyAudits
? storyforgeFetch(`/v2/oneliner/governance/user/audits?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsAdminSystemMainPolicy && isSuperAdmin()
? storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent").catch(() => null)
: Promise.resolve(null),
@@ -1382,15 +1395,23 @@ async function loadAgentControlSurfaces(projectId = "") {
appState.onelinerGovernanceEffective = governanceEffective;
appState.userGlobalPolicy = userGlobalPolicy;
appState.userCurrentPlatformPolicy = userCurrentPlatformPolicy;
appState.userPolicyAudits = safeArray(userPolicyAuditsPayload?.items || userPolicyAuditsPayload);
appState.adminSystemMainPolicy = adminSystemMainPolicy;
appState.adminSystemPlatformPolicies = safeArray(adminSystemPlatformPolicies);
appState.adminGovernanceDirectory = safeArray(adminGovernanceDirectory?.items || adminGovernanceDirectory);
if (isSuperAdmin() && supportsAdminOverridePolicy && appState.adminGovernanceDirectory.length) {
const existingTarget = appState.adminOverrideTarget || {};
const hasExistingProjectTarget = Object.prototype.hasOwnProperty.call(existingTarget, "targetProjectId")
|| Object.prototype.hasOwnProperty.call(existingTarget, "target_project_id");
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 requestedProjectId = hasExistingProjectTarget
? String(existingTarget.targetProjectId ?? existingTarget.target_project_id ?? "")
: String(targetProjects[0]?.id || "");
const targetProjectId = requestedProjectId === ""
? ""
: (targetProjects.some((item) => String(item?.id || "") === requestedProjectId) ? requestedProjectId : String(targetProjects[0]?.id || ""));
const targetPlatform = normalizePlatformValue(existingTarget.platform || governancePlatform, "douyin");
appState.adminOverrideTarget = {
targetUserId,
@@ -1398,9 +1419,13 @@ async function loadAgentControlSurfaces(projectId = "") {
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);
appState.adminPolicyAudits = supportsAdminGovernanceAudits
? safeArray((await storyforgeFetch(`/v2/admin/oneliner/governance/audits?target_user_id=${encodeURIComponent(targetUserId)}&target_project_id=${encodeURIComponent(targetProjectId)}&platform=${encodeURIComponent(targetPlatform)}&include_system=1`).catch(() => ({ items: [] })))?.items || [])
: [];
} else {
appState.adminOverrideTarget = null;
appState.adminOverridePolicy = null;
appState.adminPolicyAudits = [];
}
appState.tenantQuota = tenantQuota;
appState.tenantUsage = tenantUsage;
@@ -3395,6 +3420,52 @@ function renderAdminGovernanceSummaryPanel() {
`;
}
function renderAdminGovernanceAuditPanel() {
const targetSummary = getAdminOverrideTargetSummary();
const audits = safeArray(appState.adminPolicyAudits);
return `
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
<div class="panel-head">
<div>
<h3>覆盖与审计</h3>
<div class="panel-subtitle">按当前目标查看管理员覆盖、系统默认和相关策略变更,避免治理动作只留在弹窗里。</div>
</div>
<div class="task-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 clickable-tag" data-action="open-admin-override-target">切换目标</span>
</div>
</div>
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="task-item compact">
<h4>当前管理员覆盖</h4>
<p>${escapeHtml(appState.adminOverridePolicy?.current_version?.summary || "当前目标还没有管理员覆盖版本。")}</p>
<div class="task-meta">
<span class="tag ${appState.adminOverridePolicy?.current_version ? "orange" : "blue"}">${escapeHtml(appState.adminOverridePolicy?.current_version ? `版本 ${formatNumber(appState.adminOverridePolicy.current_version.version_no || 0)}` : "未发布")}</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>
<div class="task-item compact" style="margin-top:14px;">
<h4>治理说明</h4>
<p>管理员对用户策略的代改不会抹掉用户自己的历史,只会形成更高优先级的覆盖层,并在这里留下审计记录。</p>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近策略审计</h3><div class="panel-subtitle">系统默认、用户策略和管理员覆盖都会在这里形成时间线。</div></div></div>
<div class="list">
${renderPolicyAuditFeed(audits, "当前目标还没有策略审计记录。")}
</div>
</div>
</div>
</div>
</div>
`;
}
function renderPlatformAgentPanel() {
const items = safeArray(appState.platformAgents);
if (!items.length) {
@@ -4142,6 +4213,7 @@ function renderAdminWorkbenchScreen() {
{ value: "integrations", label: "依赖健康" },
{ value: "storage", label: "存储状态" },
{ value: "agents", label: "Agent 治理" },
{ value: "governance_audit", label: "覆盖与审计" },
{ value: "ops", label: "运维审计" }
];
const activeTab = getActiveDetailTab("adminWorkbenchTab", tabs);
@@ -4164,6 +4236,8 @@ function renderAdminWorkbenchScreen() {
? renderStorageStatusPanel()
: activeTab === "agents"
? `${renderAdminGovernanceSummaryPanel()}${renderPlatformAgentPanel()}<div style="margin-top:18px;">${renderOneLinerActionRegistryPanel()}</div>`
: activeTab === "governance_audit"
? renderAdminGovernanceAuditPanel()
: renderAdminOpsPanel()}
</div>
`
@@ -5230,6 +5304,139 @@ function renderReviewScreen() {
);
}
function renderStrategyScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
return renderAutoConnectingScreen("我的策略", "工作区就绪后,这里会自动展示你自己的策略层和治理记录。");
}
return screenShell("我的策略", "先自动连接工作区。", `${button("自动连接", "open-auth", "primary")}`, renderEmptyState("策略未加载", "自动连接成功后,这里会展示你的全局策略、平台策略和治理记录。"));
}
const tabs = [
{ value: "effective", label: "当前生效" },
{ value: "global", label: "全局策略" },
{ value: "platform", label: "当前平台策略" },
{ value: "activity", label: "变更记录" }
];
const activeTab = getActiveDetailTab("strategyDetailTab", tabs);
const project = getSelectedProject();
const platform = appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform();
return screenShell(
"我的策略",
"把你和主 Agent 的对话沉淀成可查看、可回滚、可追溯的个人策略层。",
`${button("编辑全局策略", "open-user-global-policy")} ${button("编辑当前平台策略", "open-user-platform-policy", "primary")} ${button("打开 OneLiner", "open-oneliner")}`,
`
<div class="hero-card">
<h3>当前策略工作区</h3>
<p>${escapeHtml(project?.name || "当前项目")} · ${escapeHtml(platformLabel(platform))}。这里展示系统默认、你的个性化策略和管理员覆盖是如何叠加生效的。</p>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>策略治理</h3>
<div class="panel-subtitle">先看当前生效,再回看你自己的历史和管理员覆盖,不必再通过多个弹窗来回切。</div>
</div>
</div>
${renderDetailTabs("strategyDetailTab", tabs)}
${activeTab === "effective" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
${renderGovernanceSummaryCard({
title: "当前生效策略",
subtitle: appState.onelinerGovernanceEffective?.summary || "当前主 Agent 会按下面这些层级叠加执行。",
effective: appState.onelinerGovernanceEffective,
actions: [
{ action: "open-user-global-policy", label: "编辑我的全局策略" },
{ action: "open-user-platform-policy", label: "编辑当前平台策略", platform },
{ action: "open-oneliner", label: "交给 OneLiner 调整" }
]
})}
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>当前叠加层</h3><div class="panel-subtitle">系统默认、用户层和管理员覆盖会按优先级叠加。</div></div></div>
<div class="list">
${safeArray(appState.onelinerGovernanceEffective?.layers).map((layer) => `
<div class="task-item compact">
<h4>${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || platform))}</h4>
<p>${escapeHtml(layer.current_version?.summary || layer.scope?.summary || "当前层还没有补充摘要。")}</p>
<div class="task-meta">
${layer.current_version?.version_no ? `<span class="tag blue">版本 ${escapeHtml(formatNumber(layer.current_version.version_no || 0))}</span>` : ""}
${layer.scope?.platform ? `<span class="tag">${escapeHtml(platformLabel(layer.scope.platform))}</span>` : ""}
</div>
</div>
`).join("") || `<div class="task-item compact"><h4>还没有策略层</h4><p>当前会话还没有拉到治理层信息。</p></div>`}
</div>
</div>
</div>
</div>
` : activeTab === "global" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>我的全局策略</h3><div class="panel-subtitle">这层只影响你自己,会先于平台策略被主 Agent 读取。</div></div></div>
${renderPolicyVersionSummary(appState.userGlobalPolicy || {}, "你还没有发布自己的全局策略。")}
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="open-user-global-policy">编辑</span>
<span class="tag clickable-tag" data-action="open-user-global-policy-history">历史与回滚</span>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近全局版本</h3><div class="panel-subtitle">你的全局策略回滚不会覆盖旧记录,而是生成一个新的版本。</div></div></div>
<div class="list">${renderPolicyVersionsHtml(appState.userGlobalPolicy?.versions?.items || appState.userGlobalPolicy?.versions || [], "你的全局策略还没有历史版本。")}</div>
</div>
</div>
</div>
` : activeTab === "platform" ? `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>${escapeHtml(platformLabel(platform))} 当前平台策略</h3><div class="panel-subtitle">只影响当前平台,不会连带改动其他平台。</div></div></div>
${renderPolicyVersionSummary(appState.userCurrentPlatformPolicy || {}, `你还没有发布 ${platformLabel(platform)} 平台策略。`)}
<div class="task-meta" style="margin-top:12px;">
<span class="tag clickable-tag" data-action="open-user-platform-policy" data-platform="${escapeHtml(platform)}">编辑</span>
<span class="tag clickable-tag" data-action="open-user-platform-policy-history" data-platform="${escapeHtml(platform)}">历史与回滚</span>
</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近平台版本</h3><div class="panel-subtitle"></div></div></div>
<div class="list">${renderPolicyVersionsHtml(appState.userCurrentPlatformPolicy?.versions?.items || appState.userCurrentPlatformPolicy?.versions || [], `${platformLabel(platform)} 还没有历史版本。`)}</div>
</div>
</div>
</div>
` : `
<div class="layout-grid grid-main">
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>最近策略变更</h3><div class="panel-subtitle"></div></div></div>
<div class="list">${renderPolicyAuditFeed(appState.userPolicyAudits, "当前项目还没有治理记录。")}</div>
</div>
</div>
<div class="side-stack">
<div class="panel pad" style="box-shadow:none;">
<div class="panel-head"><div><h3>治理提醒</h3><div class="panel-subtitle"></div></div></div>
<div class="list">
<div class="task-item compact">
<h4>用户层不会影响其他人</h4>
<p>你发布或回滚自己的策略只会影响你当前账户下的工作方式</p>
</div>
<div class="task-item compact">
<h4>管理员覆盖会明确可见</h4>
<p>如果管理员对你当前项目施加了覆盖层这里会出现对应的记录与摘要</p>
</div>
</div>
</div>
</div>
</div>
`}
</div>
`
);
}
function renderCreditsScreen() {
if (!appState.dashboard) {
if (isAutoConnectionPending()) {
@@ -5464,6 +5671,9 @@ function renderAll() {
screenMap.automation.innerHTML = renderAutomationScreen();
screenMap.owned.innerHTML = renderOwnedScreen();
screenMap.playbook.innerHTML = renderPlaybookScreen();
if (screenMap.strategy) {
screenMap.strategy.innerHTML = renderStrategyScreen();
}
screenMap.production.innerHTML = renderProductionScreen();
screenMap.review.innerHTML = renderReviewScreen();
screenMap.credits.innerHTML = renderCreditsScreen();
@@ -6619,6 +6829,34 @@ async function loadPolicyVersions(url) {
return { items, count: Number(payload?.count || items.length) };
}
function renderPolicyAuditFeed(items, emptyText = "还没有策略变更记录。") {
const records = safeArray(items);
if (!records.length) {
return `<div class="task-item compact"><h4>还没有治理动作</h4><p>${escapeHtml(emptyText)}</p></div>`;
}
return records.slice(0, 10).map((item) => {
const scopeKind = item.scope_kind || item.scope?.scope_kind || "";
const platform = item.platform || item.scope?.platform || "";
const version = item.version || {};
const details = item.details || {};
const rollbackId = version.rollback_from_version_id || details.rollback_to_version_id || "";
return `
<div class="task-item compact">
<h4>${escapeHtml(item.summary || version.title || item.action_key || "策略变更")}</h4>
<p>${escapeHtml(version.summary || version.reason || "当前记录没有补充摘要。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(policyScopeTagLabel(scopeKind, platform))}</span>
${version.version_no ? `<span class="tag">版本 ${escapeHtml(formatNumber(version.version_no || 0))}</span>` : ""}
${item.action_key ? `<span class="tag">${escapeHtml(item.action_key)}</span>` : ""}
${platform ? `<span class="tag">${escapeHtml(platformLabel(platform))}</span>` : ""}
${rollbackId ? `<span class="tag orange">回滚动作</span>` : ""}
${item.created_at ? `<span class="tag">${escapeHtml(formatDateTime(item.created_at))}</span>` : ""}
</div>
</div>
`;
}).join("");
}
function buildPolicyVersionOptions(history) {
return safeArray(history?.items).map((item) => ({
value: item.id,
@@ -6850,6 +7088,14 @@ async function openAdminOverrideTargetAction() {
{ 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: () => {
const userSelect = document.querySelector('[data-action-field="targetUserId"]');
if (!(userSelect instanceof HTMLSelectElement)) return;
syncAdminOverrideProjectOptions(userSelect.value, current.targetProjectId);
userSelect.addEventListener("change", () => {
syncAdminOverrideProjectOptions(userSelect.value, "");
});
},
onSubmit: async (values) => {
appState.adminOverrideTarget = {
targetUserId: String(values.targetUserId || ""),
@@ -6863,6 +7109,20 @@ async function openAdminOverrideTargetAction() {
});
}
function syncAdminOverrideProjectOptions(targetUserId, preferredProjectId = "") {
const projectSelect = document.querySelector('[data-action-field="targetProjectId"]');
if (!(projectSelect instanceof HTMLSelectElement)) return;
const options = [{ value: "", label: "用户全局" }, ...getAdminGovernanceDirectoryProjectOptions(targetUserId)];
projectSelect.innerHTML = options.map((option) => `
<option value="${escapeHtml(option.value)}">${escapeHtml(option.label)}</option>
`).join("");
const normalizedPreferred = String(preferredProjectId ?? "");
const nextValue = normalizedPreferred === "" || options.some((option) => String(option.value) === normalizedPreferred)
? normalizedPreferred
: String(options[0]?.value || "");
projectSelect.value = nextValue;
}
function openAdminOverridePolicyAction() {
const target = getAdminOverrideTargetState();
const bundle = appState.adminOverridePolicy || {};
@@ -8689,6 +8949,10 @@ document.addEventListener("click", async (event) => {
setScreen("production");
return;
}
if (name === "goto-strategy") {
setScreen("strategy");
return;
}
if (name === "goto-review") {
setScreen("review");
return;

View File

@@ -44,6 +44,10 @@
<span class="icon"></span>
<span>Agent</span>
</button>
<button class="nav-item" data-screen-target="strategy">
<span class="icon"></span>
<span>我的策略</span>
</button>
<button class="nav-item" data-screen-target="production">
<span class="icon"></span>
<span>生产中心</span>
@@ -1914,6 +1918,7 @@
<div class="footer-note">StoryForge Web V4 prototype · credit system expressed as user-facing quota pools</div>
</section>
<section class="screen" data-screen="strategy"></section>
<section class="screen" data-screen="admin-workbench"></section>
<section class="screen" data-screen="settings"></section>
</main>

View File

@@ -24,6 +24,13 @@ test("settings navigation and screen are real routes", () => {
assert.match(APP, /window\.addEventListener\("hashchange"/);
});
test("strategy navigation and screen are real routes", () => {
assert.match(HTML, /data-screen-target="strategy"/);
assert.match(HTML, /data-screen="strategy"/);
assert.match(APP, /function renderStrategyScreen\(/);
assert.match(APP, /screenMap\.strategy\.innerHTML = renderStrategyScreen\(\);/);
});
test("automation screen stays user-facing and excludes admin-only panels", () => {
const source = extractBetween(APP, "function renderAutomationScreen()", "function renderOwnedScreen()");
assert.doesNotMatch(source, /renderTenantQuotaPanel\(/);
@@ -46,11 +53,15 @@ test("discovery, production, and admin screens use page tabs for heavy content",
const discovery = extractBetween(APP, "function renderDiscoveryScreen()", "function renderTrackingScreen()");
const production = extractBetween(APP, "function renderProductionScreen()", "function renderReviewScreen()");
const admin = extractBetween(APP, "function renderAdminWorkbenchScreen()", "function renderDashboardScreen()");
const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()");
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
assert.match(admin, /覆盖与审计/);
assert.match(strategy, /renderDetailTabs\("strategyDetailTab"/);
assert.match(strategy, /renderPolicyAuditFeed\(/);
});
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {
@@ -107,6 +118,9 @@ test("agent control surfaces load governance endpoints for user and admin summar
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/);
assert.match(source, /Object\.prototype\.hasOwnProperty\.call\(existingTarget, "targetProjectId"\)/);
assert.match(source, /const requestedProjectId = hasExistingProjectTarget/);
assert.match(source, /const targetProjectId = requestedProjectId === ""/);
});
test("oneliner meta and action handlers expose governance entry points", () => {
@@ -145,6 +159,7 @@ test("system governance saves refresh control surfaces after persisting", () =>
test("governance UI exposes admin override target picker and history rollback entrypoints", () => {
const admin = extractBetween(APP, "function renderAdminGovernanceSummaryPanel()", "function renderPlatformAgentPanel()");
const targetPicker = extractBetween(APP, "async function openAdminOverrideTargetAction()", "function openAdminOverridePolicyAction()");
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
assert.match(admin, /open-admin-override-target/);
@@ -152,6 +167,10 @@ test("governance UI exposes admin override target picker and history rollback en
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(APP, /function syncAdminOverrideProjectOptions\(/);
assert.match(targetPicker, /onOpen: \(\) => \{/);
assert.match(targetPicker, /userSelect\.addEventListener\("change"/);
assert.match(targetPicker, /syncAdminOverrideProjectOptions\(userSelect\.value, ""\)/);
assert.match(actions, /name === "open-admin-override-target"/);
assert.match(actions, /name === "open-admin-override-policy"/);