feat: add agent governance audit surfaces
This commit is contained in:
@@ -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=""),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/);
|
||||
|
||||
Reference in New Issue
Block a user