feat: add main agent governance foundation
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,284 @@
|
||||
# Main Agent Governance Foundation Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the first production-ready governance foundation for StoryForge main-agent policy layers, versioning, admin overrides, rollback, and minimal governance UI.
|
||||
|
||||
**Architecture:** Add dedicated governance tables and endpoints inside `oneliner_features.py`, compute effective policy layers at runtime for OneLiner context, then expose a minimal read/write UI in the existing Agent page and Admin Workbench without redesigning the shell.
|
||||
|
||||
**Tech Stack:** FastAPI, SQLite, existing StoryForge Web V4 vanilla JS, Node test runner, Python unittest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Spec + plan docs
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/specs/2026-03-29-main-agent-governance-foundation-design.md`
|
||||
- Create: `docs/superpowers/plans/2026-03-29-main-agent-governance-foundation.md`
|
||||
|
||||
- [ ] **Step 1: Save the approved design**
|
||||
|
||||
Write the governance design into the spec file above.
|
||||
|
||||
- [ ] **Step 2: Save this implementation plan**
|
||||
|
||||
Write this plan file and keep it committed with the implementation.
|
||||
|
||||
- [ ] **Step 3: Commit docs checkpoint**
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-03-29-main-agent-governance-foundation-design.md docs/superpowers/plans/2026-03-29-main-agent-governance-foundation.md
|
||||
git commit -m "docs: add main agent governance foundation spec"
|
||||
```
|
||||
|
||||
### Task 2: Add failing backend governance tests
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_main_agent_governance.py`
|
||||
- Modify: `tests/test_production_baseline.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for scope creation and runtime layering**
|
||||
|
||||
Add tests that verify:
|
||||
- system default policy can be written and read
|
||||
- user global policy overrides system default
|
||||
- user platform policy overrides user global for one platform
|
||||
- admin override wins over user layers
|
||||
- rollback creates a new version instead of mutating history
|
||||
|
||||
- [ ] **Step 2: Run the failing test file**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_main_agent_governance -v
|
||||
```
|
||||
|
||||
Expected: failures because governance tables and endpoints do not exist yet.
|
||||
|
||||
### Task 3: Add backend schema and payload helpers
|
||||
|
||||
**Files:**
|
||||
- Modify: `collector-service/app/oneliner_features.py`
|
||||
|
||||
- [ ] **Step 1: Add schema tables**
|
||||
|
||||
Add table creation SQL for:
|
||||
- `agent_policy_scopes`
|
||||
- `agent_policy_versions`
|
||||
- `agent_policy_effectivity`
|
||||
- `agent_policy_audit_logs`
|
||||
|
||||
- [ ] **Step 2: Add policy helper functions**
|
||||
|
||||
Implement helpers for:
|
||||
- scope payload
|
||||
- version payload
|
||||
- audit payload
|
||||
- system scope ensure
|
||||
- current active version lookup
|
||||
- effective layer merge
|
||||
|
||||
- [ ] **Step 3: Re-run failing governance tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_main_agent_governance -v
|
||||
```
|
||||
|
||||
Expected: some tests still fail because endpoints are missing, but schema-related failures should move forward.
|
||||
|
||||
### Task 4: Add governance write/read endpoints
|
||||
|
||||
**Files:**
|
||||
- Modify: `collector-service/app/oneliner_features.py`
|
||||
|
||||
- [ ] **Step 1: Add user-side endpoints**
|
||||
|
||||
Implement:
|
||||
- `GET /v2/oneliner/governance/effective`
|
||||
- `GET /v2/oneliner/governance/user/global`
|
||||
- `PUT /v2/oneliner/governance/user/global`
|
||||
- `GET /v2/oneliner/governance/user/global/versions`
|
||||
- `POST /v2/oneliner/governance/user/global/rollback`
|
||||
- `GET /v2/oneliner/governance/user/platforms/{platform}`
|
||||
- `PUT /v2/oneliner/governance/user/platforms/{platform}`
|
||||
- `GET /v2/oneliner/governance/user/platforms/{platform}/versions`
|
||||
- `POST /v2/oneliner/governance/user/platforms/{platform}/rollback`
|
||||
|
||||
- [ ] **Step 2: Add admin-side endpoints**
|
||||
|
||||
Implement:
|
||||
- `GET /v2/admin/oneliner/governance/system/main-agent`
|
||||
- `PUT /v2/admin/oneliner/governance/system/main-agent`
|
||||
- `GET /v2/admin/oneliner/governance/system/main-agent/versions`
|
||||
- `POST /v2/admin/oneliner/governance/system/main-agent/rollback`
|
||||
- `GET /v2/admin/oneliner/governance/system/platforms/{platform}`
|
||||
- `PUT /v2/admin/oneliner/governance/system/platforms/{platform}`
|
||||
- `GET /v2/admin/oneliner/governance/system/platforms/{platform}/versions`
|
||||
- `POST /v2/admin/oneliner/governance/system/platforms/{platform}/rollback`
|
||||
- `GET /v2/admin/oneliner/governance/overrides`
|
||||
- `POST /v2/admin/oneliner/governance/overrides`
|
||||
- `GET /v2/admin/oneliner/governance/overrides/versions`
|
||||
- `POST /v2/admin/oneliner/governance/overrides/rollback`
|
||||
|
||||
- [ ] **Step 3: Add audit logging inside every governance mutation**
|
||||
|
||||
Record actor, target, scope, version, reason, and rollback source where relevant.
|
||||
|
||||
- [ ] **Step 4: Run governance backend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_main_agent_governance -v
|
||||
```
|
||||
|
||||
Expected: backend governance tests pass.
|
||||
|
||||
### Task 5: Connect runtime layering into OneLiner context
|
||||
|
||||
**Files:**
|
||||
- Modify: `collector-service/app/oneliner_features.py`
|
||||
- Test: `tests/test_main_agent_governance.py`
|
||||
|
||||
- [ ] **Step 1: Inject runtime policy into session context**
|
||||
|
||||
Extend the OneLiner context builder so the runtime payload includes:
|
||||
- effective merged policy
|
||||
- ordered policy layers
|
||||
- active admin override notice
|
||||
|
||||
- [ ] **Step 2: Make OneLiner reply builder surface active governance context**
|
||||
|
||||
Use the runtime policy payload to explain active strategy layers in the result payload, without rewriting all prompt logic.
|
||||
|
||||
- [ ] **Step 3: Add tests for runtime payload**
|
||||
|
||||
Verify the runtime endpoint and OneLiner context expose the merged policy stack.
|
||||
|
||||
- [ ] **Step 4: Run backend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_main_agent_governance tests.test_production_baseline -v
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
### Task 6: Add minimal governance UI loading and rendering
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/storyforge-web-v4/assets/app.js`
|
||||
- Modify: `web/storyforge-web-v4/tests/workbench-pages.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write failing frontend tests**
|
||||
|
||||
Add assertions that:
|
||||
- Agent workspace references effective policy summary
|
||||
- Admin Workbench Agent governance tab references system policy, user overrides, and audit history
|
||||
|
||||
- [ ] **Step 2: Run frontend tests and verify failure**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test web/storyforge-web-v4/tests/workbench-pages.test.mjs
|
||||
```
|
||||
|
||||
Expected: fail on missing governance UI text and loaders.
|
||||
|
||||
- [ ] **Step 3: Load governance payloads in app state**
|
||||
|
||||
Add app state fields and data loading for:
|
||||
- current runtime policy
|
||||
- current user version history
|
||||
- admin governance overview
|
||||
|
||||
- [ ] **Step 4: Render minimal governance panels**
|
||||
|
||||
Render:
|
||||
- user-side policy summary + version list in `Agent -> 当前 Agent 工作台`
|
||||
- admin-side system default, user override, audit summary in `管理员配置台 -> Agent 治理`
|
||||
|
||||
- [ ] **Step 5: Re-run frontend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test web/storyforge-web-v4/tests/workbench-pages.test.mjs
|
||||
node --check web/storyforge-web-v4/assets/app.js
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
### Task 7: Add minimal edit flows for first batch
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/storyforge-web-v4/assets/app.js`
|
||||
- Modify: `web/storyforge-web-v4/tests/workbench-pages.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Add user edit entrypoints**
|
||||
|
||||
Provide modal actions for:
|
||||
- update user global strategy
|
||||
- update current platform strategy
|
||||
|
||||
- [ ] **Step 2: Add admin edit entrypoints**
|
||||
|
||||
Provide modal actions for:
|
||||
- update system default main-agent strategy
|
||||
- update system default platform strategy
|
||||
- update admin override strategy for selected user/platform
|
||||
- rollback selected scope version
|
||||
|
||||
- [ ] **Step 3: Keep first batch UI intentionally small**
|
||||
|
||||
Do not build a full-blown designer. Use the existing modal patterns with JSON textarea + summary/reason fields if needed.
|
||||
|
||||
- [ ] **Step 4: Re-run frontend tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test web/storyforge-web-v4/tests/workbench-pages.test.mjs
|
||||
```
|
||||
|
||||
Expected: pass.
|
||||
|
||||
### Task 8: Full verification, deploy, and publish
|
||||
|
||||
**Files:**
|
||||
- Modify as needed from previous tasks only
|
||||
|
||||
- [ ] **Step 1: Run full repo checks**
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_platform_contracts tests.test_production_baseline tests.test_main_agent_governance -v
|
||||
node --test web/storyforge-web-v4/tests/dashboard-home.test.mjs web/storyforge-web-v4/tests/workbench-pages.test.mjs
|
||||
node --check web/storyforge-web-v4/assets/app.js
|
||||
python3 -m compileall collector-service/app tests
|
||||
git diff --check
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Deploy to fnOS**
|
||||
|
||||
```bash
|
||||
bash scripts/deploy_fnos_storyforge_lan_stack.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run fnOS smoke**
|
||||
|
||||
```bash
|
||||
bash scripts/smoke_fnos_storyforge_lan.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit and push**
|
||||
|
||||
```bash
|
||||
git add collector-service/app/oneliner_features.py web/storyforge-web-v4/assets/app.js tests/test_main_agent_governance.py tests/test_production_baseline.py web/storyforge-web-v4/tests/workbench-pages.test.mjs docs/superpowers/specs/2026-03-29-main-agent-governance-foundation-design.md docs/superpowers/plans/2026-03-29-main-agent-governance-foundation.md
|
||||
git commit -m "feat: add main agent governance foundation"
|
||||
git push gitea codex/storyforge-live-orchestrator-sync-20260323
|
||||
```
|
||||
@@ -0,0 +1,283 @@
|
||||
# StoryForge 主 Agent 治理底座设计
|
||||
|
||||
- 日期:2026-03-29
|
||||
- 状态:已确认,直接进入实现
|
||||
- 范围:`collector-service`、`web/storyforge-web-v4`
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前仓库已经有 `OneLiner` 主 Agent、平台 Agent、平台记忆、平台技能、额度和管理员运维面板,但真正决定长期生命力的治理层还没有闭环。
|
||||
|
||||
现状问题:
|
||||
|
||||
- `OneLiner` 能承接对话,但“系统默认策略 / 用户策略 / 管理员覆盖”还没有正式的数据模型。
|
||||
- 平台 Agent 已经有 profile、memory、skill,但用户策略变更还不能以“全局 / 单平台 / 覆盖层 / 历史版本”的方式正式管理。
|
||||
- 管理员配置台已经有入口,但还没有系统级主 Agent 策略、用户策略审计和覆盖管理的正式控制面。
|
||||
- 首页和工作台已经开始围绕“交给主 Agent”组织,但推荐逻辑和执行上下文还缺少一个可审计的策略底座。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
- 建立主 Agent 治理底座的数据模型,不再把策略历史继续塞进旧 JSON。
|
||||
- 支持三层用户相关策略:
|
||||
- 用户全局策略
|
||||
- 用户平台策略
|
||||
- 管理员覆盖策略
|
||||
- 保留系统默认策略层:
|
||||
- 系统默认主 Agent 策略
|
||||
- 系统默认平台 Agent 策略
|
||||
- 所有策略都具备:
|
||||
- 版本
|
||||
- 生效范围
|
||||
- 状态
|
||||
- 原因
|
||||
- 操作者
|
||||
- 回滚来源
|
||||
- 主 Agent 运行时可以读取当前用户的有效策略叠加结果。
|
||||
- Web 先补最小可用治理 UI:
|
||||
- 用户侧可查看当前有效策略与版本历史
|
||||
- 管理员侧可查看系统默认、用户覆盖、管理员覆盖与回滚记录
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
- 本轮不重做主 Agent 全部 UI 体验。
|
||||
- 本轮不做完整自然语言“自动解析策略修改”闭环。
|
||||
- 本轮不把所有平台专属策略细节全部产品化。
|
||||
- 本轮不引入新的数据库或事件总线。
|
||||
|
||||
## 4. 数据模型
|
||||
|
||||
### 4.1 新表
|
||||
|
||||
新增四张表:
|
||||
|
||||
1. `agent_policy_scopes`
|
||||
- 定义策略作用域
|
||||
- 核心字段:
|
||||
- `id`
|
||||
- `scope_kind`:`system_main` / `system_platform` / `user_global` / `user_platform` / `admin_override`
|
||||
- `subject_user_id`
|
||||
- `subject_project_id`
|
||||
- `platform`
|
||||
- `status`
|
||||
- `title`
|
||||
- `summary`
|
||||
- `current_version_id`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
2. `agent_policy_versions`
|
||||
- 每次修改都生成一个新版本
|
||||
- 核心字段:
|
||||
- `id`
|
||||
- `scope_id`
|
||||
- `scope_kind`
|
||||
- `subject_user_id`
|
||||
- `subject_project_id`
|
||||
- `platform`
|
||||
- `version_no`
|
||||
- `title`
|
||||
- `policy_json`
|
||||
- `summary`
|
||||
- `reason`
|
||||
- `source_type`
|
||||
- `rollback_from_version_id`
|
||||
- `actor_user_id`
|
||||
- `created_at`
|
||||
|
||||
3. `agent_policy_effectivity`
|
||||
- 描述某版本当前是否生效,以及生效范围
|
||||
- 核心字段:
|
||||
- `id`
|
||||
- `scope_id`
|
||||
- `version_id`
|
||||
- `effect_mode`:`ongoing` / `scheduled`
|
||||
- `starts_at`
|
||||
- `ends_at`
|
||||
- `status`
|
||||
- `config_json`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
4. `agent_policy_audit_logs`
|
||||
- 记录查看、创建、发布、代改、覆盖、回滚等动作
|
||||
- 核心字段:
|
||||
- `id`
|
||||
- `scope_id`
|
||||
- `version_id`
|
||||
- `action_key`
|
||||
- `actor_user_id`
|
||||
- `summary`
|
||||
- `details_json`
|
||||
- `created_at`
|
||||
|
||||
### 4.2 运行时叠加顺序
|
||||
|
||||
运行时有效策略按以下顺序叠加:
|
||||
|
||||
1. 系统默认主 Agent / 平台 Agent 策略
|
||||
2. 用户全局策略
|
||||
3. 用户平台策略
|
||||
4. 管理员覆盖策略
|
||||
|
||||
后者覆盖前者同名字段。
|
||||
|
||||
## 5. 权限边界
|
||||
|
||||
### 5.1 普通用户
|
||||
|
||||
- 只能读取和更新自己的:
|
||||
- 用户全局策略
|
||||
- 用户平台策略
|
||||
- 只能查看自己的版本历史
|
||||
- 不能修改系统默认策略
|
||||
- 不能查看其他用户策略
|
||||
|
||||
### 5.2 超级管理员
|
||||
|
||||
- 可查看和修改系统默认主 Agent 策略
|
||||
- 可查看和修改系统默认平台 Agent 策略
|
||||
- 可查看任意用户策略
|
||||
- 可创建管理员覆盖策略
|
||||
- 可对用户策略或管理员覆盖进行回滚
|
||||
- 管理员操作不会抹掉用户自己的历史版本,只会形成更高优先级覆盖层
|
||||
|
||||
### 5.3 Agent 读取边界
|
||||
|
||||
- 主 Agent 可读取当前用户:
|
||||
- 用户全局策略
|
||||
- 用户平台策略
|
||||
- 当前平台 Agent 记忆与技能
|
||||
- 平台 Agent 只能读取当前用户且当前平台相关的策略、记忆和技能
|
||||
|
||||
## 6. 回滚规则
|
||||
|
||||
- 回滚不是修改旧版本。
|
||||
- 回滚操作会生成一个新版本。
|
||||
- 新版本记录 `rollback_from_version_id`。
|
||||
- 同时写入审计日志。
|
||||
|
||||
## 7. 后端接口
|
||||
|
||||
### 7.1 用户侧
|
||||
|
||||
- `GET /v2/oneliner/governance/effective`
|
||||
- 返回当前用户在当前项目、当前平台下的有效策略叠加结果
|
||||
- `GET /v2/oneliner/governance/user/global`
|
||||
- 返回当前用户全局策略 bundle 和历史统计
|
||||
- `PUT /v2/oneliner/governance/user/global`
|
||||
- 更新当前用户全局策略
|
||||
- `GET /v2/oneliner/governance/user/global/versions`
|
||||
- 返回当前用户全局策略历史
|
||||
- `POST /v2/oneliner/governance/user/global/rollback`
|
||||
- 回滚当前用户全局策略到历史版本
|
||||
- `GET /v2/oneliner/governance/user/platforms/{platform}`
|
||||
- 返回当前用户单平台策略 bundle 和历史统计
|
||||
- `PUT /v2/oneliner/governance/user/platforms/{platform}`
|
||||
- 更新当前用户单平台策略
|
||||
- `GET /v2/oneliner/governance/user/platforms/{platform}/versions`
|
||||
- 返回当前用户单平台策略历史
|
||||
- `POST /v2/oneliner/governance/user/platforms/{platform}/rollback`
|
||||
- 回滚当前用户单平台策略到历史版本
|
||||
|
||||
### 7.2 管理员侧
|
||||
|
||||
- `GET /v2/admin/oneliner/governance/system/main-agent`
|
||||
- 读取系统主 Agent 策略 bundle
|
||||
- `PUT /v2/admin/oneliner/governance/system/main-agent`
|
||||
- 更新系统主 Agent 策略
|
||||
- `GET /v2/admin/oneliner/governance/system/main-agent/versions`
|
||||
- 读取系统主 Agent 历史
|
||||
- `POST /v2/admin/oneliner/governance/system/main-agent/rollback`
|
||||
- 回滚系统主 Agent 策略
|
||||
- `GET /v2/admin/oneliner/governance/system/platforms/{platform}`
|
||||
- 读取系统平台策略 bundle
|
||||
- `PUT /v2/admin/oneliner/governance/system/platforms/{platform}`
|
||||
- 更新系统平台策略
|
||||
- `GET /v2/admin/oneliner/governance/system/platforms/{platform}/versions`
|
||||
- 读取系统平台策略历史
|
||||
- `POST /v2/admin/oneliner/governance/system/platforms/{platform}/rollback`
|
||||
- 回滚系统平台策略
|
||||
- `GET /v2/admin/oneliner/governance/overrides`
|
||||
- 读取某个用户/项目/平台的管理员覆盖策略 bundle
|
||||
- `POST /v2/admin/oneliner/governance/overrides`
|
||||
- 发布管理员覆盖策略
|
||||
- `GET /v2/admin/oneliner/governance/overrides/versions`
|
||||
- 读取管理员覆盖策略历史
|
||||
- `POST /v2/admin/oneliner/governance/overrides/rollback`
|
||||
- 回滚管理员覆盖策略
|
||||
|
||||
## 8. 运行时接入
|
||||
|
||||
### 8.1 主 Agent
|
||||
|
||||
`OneLiner` 生成上下文时,补充:
|
||||
|
||||
- 当前用户有效策略栈
|
||||
- 当前命中的管理员覆盖层
|
||||
- 当前平台有效策略
|
||||
|
||||
这些信息进入:
|
||||
|
||||
- `runtime_policy`
|
||||
- `policy_layers`
|
||||
- `admin_override_notice`
|
||||
|
||||
用于:
|
||||
|
||||
- 主 Agent 回复中解释“为什么按这个策略执行”
|
||||
- 后续首页动作推荐引用同一份有效策略
|
||||
|
||||
### 8.2 前端
|
||||
|
||||
#### 用户侧 Agent 页面
|
||||
|
||||
在 `Agent -> 当前 Agent 工作台` 增加:
|
||||
|
||||
- 当前有效主 Agent 策略摘要
|
||||
- 当前用户全局策略
|
||||
- 当前平台策略入口
|
||||
- 最近版本历史
|
||||
|
||||
#### 管理员配置台
|
||||
|
||||
在 `管理员配置台 -> Agent 治理` 增加:
|
||||
|
||||
- 系统默认主 Agent 策略
|
||||
- 系统默认平台 Agent 策略
|
||||
- 当前项目用户策略总览
|
||||
- 管理员覆盖层
|
||||
- 最近策略审计
|
||||
|
||||
## 9. 测试
|
||||
|
||||
### 9.1 后端
|
||||
|
||||
- schema 初始化测试
|
||||
- 用户全局策略写入与读取测试
|
||||
- 用户平台策略覆盖测试
|
||||
- 管理员覆盖优先级测试
|
||||
- 回滚生成新版本测试
|
||||
- 审计日志记录测试
|
||||
- `runtime` 叠加顺序测试
|
||||
|
||||
### 9.2 前端
|
||||
|
||||
- Agent 页包含治理摘要入口
|
||||
- 管理员台包含治理区块
|
||||
- 版本历史和覆盖标记文案存在
|
||||
|
||||
## 10. 第一批实现范围
|
||||
|
||||
本轮只做:
|
||||
|
||||
- 治理底座表结构
|
||||
- 核心策略接口
|
||||
- 主 Agent 运行时接入
|
||||
- 最小治理 UI
|
||||
- 基本测试
|
||||
|
||||
不做:
|
||||
|
||||
- 复杂自然语言自动改策略
|
||||
- 多项目多阶段高级调度面板
|
||||
- 完整的对比 diff 可视化
|
||||
@@ -16,13 +16,19 @@ need_cmd node
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
echo "[1/5] compile collector-service"
|
||||
echo "[1/6] compile collector-service"
|
||||
python3 -m compileall collector-service/app >/dev/null
|
||||
|
||||
echo "[2/5] validate docker compose"
|
||||
echo "[2/6] run backend contract tests"
|
||||
python3 -m unittest \
|
||||
tests.test_main_agent_governance \
|
||||
tests.test_platform_contracts \
|
||||
tests.test_production_baseline >/dev/null
|
||||
|
||||
echo "[3/6] validate docker compose"
|
||||
docker compose config >/dev/null
|
||||
|
||||
echo "[3/5] validate n8n workflows"
|
||||
echo "[4/6] validate n8n workflows"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
@@ -33,13 +39,13 @@ for path in sorted(pathlib.Path("n8n/workflows").glob("*.json")):
|
||||
print(f"workflow ok: {path.name}")
|
||||
PY
|
||||
|
||||
echo "[4/5] validate web scripts"
|
||||
echo "[5/6] validate web scripts"
|
||||
for file in web/storyforge-web-v4/assets/app.js web/storyforge-web-v4/assets/storyforge-*.js; do
|
||||
node --check "$file"
|
||||
done
|
||||
node --check scripts/douyin-browser-capture/control_panel.mjs
|
||||
|
||||
echo "[5/5] validate homepage and workbench tests"
|
||||
echo "[6/6] validate homepage and workbench tests"
|
||||
node --test \
|
||||
web/storyforge-web-v4/tests/dashboard-home.test.mjs \
|
||||
web/storyforge-web-v4/tests/workbench-pages.test.mjs
|
||||
|
||||
380
tests/test_main_agent_governance.py
Normal file
380
tests/test_main_agent_governance.py
Normal file
@@ -0,0 +1,380 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_ROOT = ROOT / "collector-service"
|
||||
if str(APP_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(APP_ROOT))
|
||||
|
||||
|
||||
class MainAgentGovernanceTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.tempdir = tempfile.TemporaryDirectory()
|
||||
temp_root = Path(cls.tempdir.name)
|
||||
os.environ["DATA_DIR"] = str(temp_root / "data")
|
||||
os.environ["DATABASE_PATH"] = str(temp_root / "data" / "storyforge.db")
|
||||
os.environ["DOWNLOADS_DIR"] = str(temp_root / "downloads")
|
||||
os.environ["JOBS_DIR"] = str(temp_root / "jobs")
|
||||
os.environ["MODELS_DIR"] = str(temp_root / "models")
|
||||
os.environ["ORCHESTRATOR_SHARED_SECRET"] = "test-secret"
|
||||
os.environ["WEB_AUTOLOGIN_ENABLED"] = "0"
|
||||
os.environ.setdefault("BOOTSTRAP_SUPERADMIN_USERNAME", "")
|
||||
os.environ.setdefault("BOOTSTRAP_SUPERADMIN_PASSWORD", "")
|
||||
|
||||
cls.db_module = importlib.reload(importlib.import_module("app.database"))
|
||||
cls.core = importlib.reload(importlib.import_module("app.core_main"))
|
||||
cls.app_main = importlib.reload(importlib.import_module("app.main"))
|
||||
cls.core.db.init_schema()
|
||||
cls.client = TestClient(cls.app_main.app)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.client.close()
|
||||
cls.tempdir.cleanup()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._clear_tables()
|
||||
self.ctx = self._seed_accounts()
|
||||
|
||||
def _clear_tables(self) -> None:
|
||||
tables = [
|
||||
"agent_policy_audit_logs",
|
||||
"agent_policy_effectivity",
|
||||
"agent_policy_versions",
|
||||
"agent_policy_scopes",
|
||||
"agent_skill_versions",
|
||||
"agent_skills",
|
||||
"agent_memories",
|
||||
"platform_agent_profiles",
|
||||
"oneliner_messages",
|
||||
"oneliner_sessions",
|
||||
"oneliner_profiles",
|
||||
"auth_tokens",
|
||||
"projects",
|
||||
"accounts",
|
||||
"model_profiles",
|
||||
]
|
||||
for table in tables:
|
||||
try:
|
||||
self.core.db.execute(f"DELETE FROM {table}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _seed_accounts(self) -> dict[str, Any]:
|
||||
now = self.db_module.utc_now()
|
||||
admin_id = "acct_admin"
|
||||
member_id = "acct_member"
|
||||
project_id = "proj_member"
|
||||
model_id = "model_default"
|
||||
admin_token = "token_admin"
|
||||
member_token = "token_member"
|
||||
|
||||
self.core.db.execute(
|
||||
"""
|
||||
INSERT INTO accounts (
|
||||
id, username, password_hash, password_salt, display_name, role, approval_status,
|
||||
approved_by, approved_at, preferred_analysis_model_id, created_at, updated_at
|
||||
) VALUES (?, ?, 'hash', 'salt', ?, ?, 'approved', ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(admin_id, "admin", "Admin", "super_admin", admin_id, now, model_id, now, now),
|
||||
)
|
||||
self.core.db.execute(
|
||||
"""
|
||||
INSERT INTO accounts (
|
||||
id, username, password_hash, password_salt, display_name, role, approval_status,
|
||||
approved_by, approved_at, preferred_analysis_model_id, created_at, updated_at
|
||||
) VALUES (?, ?, 'hash', 'salt', ?, ?, 'approved', ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(member_id, "member", "Member", "operator", admin_id, now, model_id, now, now),
|
||||
)
|
||||
self.core.db.execute(
|
||||
"""
|
||||
INSERT INTO projects (id, user_id, name, description, created_at, updated_at)
|
||||
VALUES (?, ?, ?, '', ?, ?)
|
||||
""",
|
||||
(project_id, member_id, "Member Project", now, now),
|
||||
)
|
||||
self.core.db.execute(
|
||||
"""
|
||||
INSERT INTO model_profiles (
|
||||
id, owner_account_id, name, provider, base_url, api_key, model_name,
|
||||
is_system, is_default, created_at, updated_at
|
||||
) VALUES (?, NULL, 'Default Model', 'openai_compat', 'http://127.0.0.1:8317/v1', '', 'GLM-5', 1, 1, ?, ?)
|
||||
""",
|
||||
(model_id, now, now),
|
||||
)
|
||||
self.core.db.execute(
|
||||
"INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)",
|
||||
(admin_token, admin_id, now),
|
||||
)
|
||||
self.core.db.execute(
|
||||
"INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)",
|
||||
(member_token, member_id, now),
|
||||
)
|
||||
return {
|
||||
"admin_id": admin_id,
|
||||
"member_id": member_id,
|
||||
"project_id": project_id,
|
||||
"admin_headers": {"Authorization": f"Bearer {admin_token}"},
|
||||
"member_headers": {"Authorization": f"Bearer {member_token}"},
|
||||
}
|
||||
|
||||
def test_effective_policy_merges_system_user_global_and_platform_layers(self) -> None:
|
||||
system_response = self.client.put(
|
||||
"/v2/admin/oneliner/governance/system/main-agent",
|
||||
headers=self.ctx["admin_headers"],
|
||||
json={
|
||||
"title": "System main agent",
|
||||
"summary": "Default baseline",
|
||||
"policy": {
|
||||
"tone": {"style": "default"},
|
||||
"homepage": {"focus": "ops"},
|
||||
"actions": {"max_cards": 3},
|
||||
},
|
||||
"reason": "seed system baseline",
|
||||
},
|
||||
)
|
||||
self.assertEqual(system_response.status_code, 200, system_response.text)
|
||||
|
||||
global_response = self.client.put(
|
||||
"/v2/oneliner/governance/user/global",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"title": "Member global strategy",
|
||||
"summary": "Personal operating style",
|
||||
"policy": {
|
||||
"tone": {"style": "analytical"},
|
||||
"memory": {"default_window": "30d"},
|
||||
"actions": {"max_cards": 2},
|
||||
},
|
||||
"reason": "personalize global defaults",
|
||||
},
|
||||
)
|
||||
self.assertEqual(global_response.status_code, 200, global_response.text)
|
||||
|
||||
platform_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",
|
||||
"summary": "Tighter benchmark workflow",
|
||||
"policy": {
|
||||
"actions": {"max_cards": 1},
|
||||
"douyin": {"benchmark_mode": "strict"},
|
||||
},
|
||||
"reason": "tighten douyin execution",
|
||||
},
|
||||
)
|
||||
self.assertEqual(platform_response.status_code, 200, platform_response.text)
|
||||
|
||||
effective_response = self.client.get(
|
||||
"/v2/oneliner/governance/effective",
|
||||
headers=self.ctx["member_headers"],
|
||||
params={"project_id": self.ctx["project_id"], "platform": "douyin"},
|
||||
)
|
||||
self.assertEqual(effective_response.status_code, 200, effective_response.text)
|
||||
payload = effective_response.json()
|
||||
self.assertEqual(
|
||||
[item["scope_kind"] for item in payload["layers"]],
|
||||
["system_main", "user_global", "user_platform"],
|
||||
)
|
||||
self.assertEqual(payload["effective_policy"]["tone"]["style"], "analytical")
|
||||
self.assertEqual(payload["effective_policy"]["homepage"]["focus"], "ops")
|
||||
self.assertEqual(payload["effective_policy"]["actions"]["max_cards"], 1)
|
||||
self.assertEqual(payload["effective_policy"]["douyin"]["benchmark_mode"], "strict")
|
||||
|
||||
def test_admin_override_takes_precedence_in_effective_policy(self) -> None:
|
||||
self.client.put(
|
||||
"/v2/admin/oneliner/governance/system/main-agent",
|
||||
headers=self.ctx["admin_headers"],
|
||||
json={
|
||||
"title": "System main agent",
|
||||
"policy": {"actions": {"max_cards": 3}},
|
||||
"reason": "seed baseline",
|
||||
},
|
||||
)
|
||||
self.client.put(
|
||||
"/v2/oneliner/governance/user/platforms/douyin",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"title": "Douyin strategy",
|
||||
"policy": {"actions": {"max_cards": 1}},
|
||||
"reason": "tighten douyin execution",
|
||||
},
|
||||
)
|
||||
|
||||
override_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": "Safety override",
|
||||
"summary": "Require review after recent drift",
|
||||
"policy": {
|
||||
"actions": {"max_cards": 5},
|
||||
"guardrails": {"require_admin_review": True},
|
||||
},
|
||||
"reason": "contain unexpected drift",
|
||||
},
|
||||
)
|
||||
self.assertEqual(override_response.status_code, 200, override_response.text)
|
||||
|
||||
effective_response = self.client.get(
|
||||
"/v2/oneliner/governance/effective",
|
||||
headers=self.ctx["member_headers"],
|
||||
params={"project_id": self.ctx["project_id"], "platform": "douyin"},
|
||||
)
|
||||
self.assertEqual(effective_response.status_code, 200, effective_response.text)
|
||||
payload = effective_response.json()
|
||||
self.assertEqual(payload["layers"][-1]["scope_kind"], "admin_override")
|
||||
self.assertEqual(payload["effective_policy"]["actions"]["max_cards"], 5)
|
||||
self.assertTrue(payload["effective_policy"]["guardrails"]["require_admin_review"])
|
||||
|
||||
def test_admin_override_without_target_project_applies_to_member_projects(self) -> None:
|
||||
override_response = self.client.post(
|
||||
"/v2/admin/oneliner/governance/overrides",
|
||||
headers=self.ctx["admin_headers"],
|
||||
json={
|
||||
"target_user_id": self.ctx["member_id"],
|
||||
"title": "Global safety override",
|
||||
"summary": "Apply guardrails across every project",
|
||||
"policy": {
|
||||
"guardrails": {"require_admin_review": True},
|
||||
"actions": {"max_cards": 4},
|
||||
},
|
||||
"reason": "global containment",
|
||||
},
|
||||
)
|
||||
self.assertEqual(override_response.status_code, 200, override_response.text)
|
||||
|
||||
effective_response = self.client.get(
|
||||
"/v2/oneliner/governance/effective",
|
||||
headers=self.ctx["member_headers"],
|
||||
params={"project_id": self.ctx["project_id"], "platform": "douyin"},
|
||||
)
|
||||
self.assertEqual(effective_response.status_code, 200, effective_response.text)
|
||||
payload = effective_response.json()
|
||||
self.assertEqual(payload["layers"][-1]["scope_kind"], "admin_override")
|
||||
self.assertEqual(payload["effective_policy"]["actions"]["max_cards"], 4)
|
||||
self.assertTrue(payload["effective_policy"]["guardrails"]["require_admin_review"])
|
||||
|
||||
def test_effective_policy_skips_future_scheduled_versions_until_window_opens(self) -> None:
|
||||
first_response = self.client.put(
|
||||
"/v2/admin/oneliner/governance/system/main-agent",
|
||||
headers=self.ctx["admin_headers"],
|
||||
json={
|
||||
"title": "Current system baseline",
|
||||
"summary": "Active now",
|
||||
"policy": {"tone": {"style": "default"}},
|
||||
"reason": "baseline",
|
||||
},
|
||||
)
|
||||
self.assertEqual(first_response.status_code, 200, first_response.text)
|
||||
|
||||
second_response = self.client.put(
|
||||
"/v2/admin/oneliner/governance/system/main-agent",
|
||||
headers=self.ctx["admin_headers"],
|
||||
json={
|
||||
"title": "Future strategy",
|
||||
"summary": "Should not be active yet",
|
||||
"policy": {"tone": {"style": "future"}},
|
||||
"effect_mode": "scheduled",
|
||||
"starts_at": "2099-01-01T00:00:00Z",
|
||||
"reason": "future rollout",
|
||||
},
|
||||
)
|
||||
self.assertEqual(second_response.status_code, 200, second_response.text)
|
||||
|
||||
effective_response = self.client.get(
|
||||
"/v2/oneliner/governance/effective",
|
||||
headers=self.ctx["member_headers"],
|
||||
params={"project_id": self.ctx["project_id"], "platform": "douyin"},
|
||||
)
|
||||
self.assertEqual(effective_response.status_code, 200, effective_response.text)
|
||||
payload = effective_response.json()
|
||||
self.assertEqual(payload["effective_policy"]["tone"]["style"], "default")
|
||||
self.assertEqual(payload["layers"][0]["current_version"]["title"], "Current system baseline")
|
||||
|
||||
def test_user_global_versions_support_rollback_by_creating_new_version(self) -> None:
|
||||
first_response = self.client.put(
|
||||
"/v2/oneliner/governance/user/global",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"title": "Global strategy v1",
|
||||
"policy": {"tone": {"style": "analytical"}},
|
||||
"reason": "first 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/global",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"title": "Global strategy v2",
|
||||
"policy": {"tone": {"style": "decisive"}},
|
||||
"reason": "refine tone",
|
||||
},
|
||||
)
|
||||
self.assertEqual(second_response.status_code, 200, second_response.text)
|
||||
|
||||
versions_before = self.client.get(
|
||||
"/v2/oneliner/governance/user/global/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/global/rollback",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"project_id": self.ctx["project_id"],
|
||||
"version_id": first_version_id,
|
||||
"reason": "restore best baseline",
|
||||
},
|
||||
)
|
||||
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"]["tone"]["style"], "analytical")
|
||||
|
||||
versions_after = self.client.get(
|
||||
"/v2/oneliner/governance/user/global/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",
|
||||
headers=self.ctx["member_headers"],
|
||||
json={
|
||||
"title": "Not allowed",
|
||||
"policy": {"tone": {"style": "rogue"}},
|
||||
"reason": "should be blocked",
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 403, response.text)
|
||||
@@ -54,6 +54,11 @@ const appState = {
|
||||
onelinerMessages: [],
|
||||
onelinerActionRegistry: [],
|
||||
platformAgents: [],
|
||||
onelinerGovernanceEffective: null,
|
||||
userGlobalPolicy: null,
|
||||
userCurrentPlatformPolicy: null,
|
||||
adminSystemMainPolicy: null,
|
||||
adminSystemPlatformPolicies: [],
|
||||
tenantQuota: null,
|
||||
tenantUsage: null,
|
||||
adminOpsOverview: null,
|
||||
@@ -1038,6 +1043,9 @@ function renderOneLinerUi() {
|
||||
const status = document.querySelector('[data-role="oneliner-status"]');
|
||||
const input = document.querySelector('[data-role="oneliner-input"]');
|
||||
const profile = appState.onelinerProfile;
|
||||
const effective = appState.onelinerGovernanceEffective;
|
||||
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || "");
|
||||
const layers = safeArray(effective?.layers);
|
||||
if (fab) {
|
||||
fab.hidden = !appState.session;
|
||||
}
|
||||
@@ -1050,6 +1058,11 @@ function renderOneLinerUi() {
|
||||
<span class="tag green">${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent</span>
|
||||
</div>
|
||||
<div class="helper-text">${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}</div>
|
||||
<div class="task-meta" style="margin-top:10px;">
|
||||
${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("")}
|
||||
<span class="tag clickable-tag" data-action="open-user-global-policy">我的策略</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (sessions) sessions.innerHTML = renderOneLinerSessionTabs();
|
||||
@@ -1243,6 +1256,11 @@ async function logoutSession() {
|
||||
appState.onelinerMessages = [];
|
||||
appState.onelinerActionRegistry = [];
|
||||
appState.platformAgents = [];
|
||||
appState.onelinerGovernanceEffective = null;
|
||||
appState.userGlobalPolicy = null;
|
||||
appState.userCurrentPlatformPolicy = null;
|
||||
appState.adminSystemMainPolicy = null;
|
||||
appState.adminSystemPlatformPolicies = [];
|
||||
appState.tenantQuota = null;
|
||||
appState.tenantUsage = null;
|
||||
appState.adminOpsOverview = null;
|
||||
@@ -1284,16 +1302,22 @@ async function loadStorageStatus(projectId = "") {
|
||||
|
||||
async function loadAgentControlSurfaces(projectId = "") {
|
||||
const normalizedProjectId = projectId || getOneLinerProjectId();
|
||||
const governancePlatform = normalizePlatformValue(getPreferredPlatform(), "douyin");
|
||||
const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile");
|
||||
const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions");
|
||||
const supportsActionRegistry = backendSupports("/v2/oneliner/action-registry");
|
||||
const supportsPlatformAgents = backendSupports("/v2/platform-agents");
|
||||
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 supportsAdminSystemMainPolicy = backendSupports("/v2/admin/oneliner/governance/system/main-agent");
|
||||
const supportsAdminSystemPlatformPolicy = backendSupports("/v2/admin/oneliner/governance/system/platforms/{platform}");
|
||||
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, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
|
||||
const [profile, sessionsPayload, actionRegistryPayload, platformAgentsPayload, governanceEffective, userGlobalPolicy, userCurrentPlatformPolicy, adminSystemMainPolicy, adminSystemPlatformPolicies, tenantQuota, tenantUsage, adminOpsOverview, adminFixRunsPayload] = await Promise.all([
|
||||
supportsOneLinerProfile
|
||||
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
@@ -1306,6 +1330,23 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
supportsPlatformAgents
|
||||
? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
|
||||
: Promise.resolve({ items: [] }),
|
||||
supportsGovernanceEffective
|
||||
? storyforgeFetch(`/v2/oneliner/governance/effective?project_id=${encodeURIComponent(normalizedProjectId)}&platform=${encodeURIComponent(governancePlatform)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsUserGlobalPolicy
|
||||
? storyforgeFetch(`/v2/oneliner/governance/user/global?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsUserPlatformPolicy
|
||||
? storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(governancePlatform)}?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsAdminSystemMainPolicy && isSuperAdmin()
|
||||
? storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent").catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
supportsAdminSystemPlatformPolicy && isSuperAdmin()
|
||||
? Promise.all(ACTIVE_PLATFORMS.map((item) =>
|
||||
storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(item.value)}`).catch(() => null)
|
||||
))
|
||||
: Promise.resolve([]),
|
||||
supportsTenantQuota
|
||||
? storyforgeFetch(`/v2/tenant/quota?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
|
||||
: Promise.resolve(null),
|
||||
@@ -1327,6 +1368,11 @@ async function loadAgentControlSurfaces(projectId = "") {
|
||||
appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || "";
|
||||
}
|
||||
appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload);
|
||||
appState.onelinerGovernanceEffective = governanceEffective;
|
||||
appState.userGlobalPolicy = userGlobalPolicy;
|
||||
appState.userCurrentPlatformPolicy = userCurrentPlatformPolicy;
|
||||
appState.adminSystemMainPolicy = adminSystemMainPolicy;
|
||||
appState.adminSystemPlatformPolicies = safeArray(adminSystemPlatformPolicies);
|
||||
appState.tenantQuota = tenantQuota;
|
||||
appState.tenantUsage = tenantUsage;
|
||||
appState.adminOpsOverview = adminOpsOverview;
|
||||
@@ -3138,6 +3184,91 @@ function renderTenantQuotaPanel() {
|
||||
`;
|
||||
}
|
||||
|
||||
function policyScopeTagLabel(scopeKind, platform = "") {
|
||||
if (scopeKind === "system_main") return "系统默认";
|
||||
if (scopeKind === "system_platform") return `${platformLabel(platform || "douyin")} 默认`;
|
||||
if (scopeKind === "user_global") return "我的全局";
|
||||
if (scopeKind === "user_platform") return `${platformLabel(platform || "douyin")} 我的策略`;
|
||||
if (scopeKind === "admin_override") return "管理员覆盖";
|
||||
return "策略层";
|
||||
}
|
||||
|
||||
function summarizePolicyHighlights(policy = {}, platform = "") {
|
||||
const items = [];
|
||||
if (policy?.tone?.style) items.push(`语气 ${policy.tone.style}`);
|
||||
if (policy?.actions?.max_cards != null) items.push(`首页动作 ${formatNumber(policy.actions.max_cards)} 条`);
|
||||
if (policy?.memory?.default_window) items.push(`记忆窗口 ${policy.memory.default_window}`);
|
||||
if (platform && policy?.[platform]?.benchmark_mode) items.push(`${platformLabel(platform)} 对标 ${policy[platform].benchmark_mode}`);
|
||||
if (policy?.guardrails?.require_admin_review) items.push("需管理员复核");
|
||||
return items.slice(0, 4);
|
||||
}
|
||||
|
||||
function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction = "", primaryLabel = "编辑策略", secondaryAction = "", secondaryLabel = "", secondaryPlatform = "" }) {
|
||||
const layers = safeArray(effective?.layers);
|
||||
const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || secondaryPlatform || "");
|
||||
return `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(title)}</h4>
|
||||
<p>${escapeHtml(subtitle || "当前还没有策略摘要。")}</p>
|
||||
<div class="task-meta">
|
||||
${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) ? `
|
||||
<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>` : ""}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAdminGovernanceSummaryPanel() {
|
||||
const systemMain = appState.adminSystemMainPolicy;
|
||||
const systemPlatforms = safeArray(appState.adminSystemPlatformPolicies);
|
||||
const configuredPlatforms = systemPlatforms.filter((item) => item?.current_version);
|
||||
return `
|
||||
<div class="panel pad" style="box-shadow:none; margin-bottom:18px;">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h3>系统级主 Agent 治理</h3>
|
||||
<div class="panel-subtitle">先管系统默认主 Agent,再按平台补默认策略,普通用户的个性化覆盖会叠加在这些底座之上。</div>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">系统主 Agent ${escapeHtml(systemMain?.current_version ? "已发布" : "未发布")}</span>
|
||||
<span class="tag">${escapeHtml(formatNumber(configuredPlatforms.length))} 个平台默认策略</span>
|
||||
<span class="tag clickable-tag" data-action="open-system-main-policy">编辑系统主 Agent</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three-col">
|
||||
<div class="entity-card pad">
|
||||
<div class="cell-title">系统主 Agent</div>
|
||||
<div class="cell-desc">${escapeHtml(systemMain?.current_version?.summary || "还没有发布系统默认主 Agent 策略。")}</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
${ACTIVE_PLATFORMS.map((platformItem) => {
|
||||
const item = systemPlatforms.find((entry) => entry?.scope?.platform === platformItem.value) || null;
|
||||
return `
|
||||
<div class="entity-card pad">
|
||||
<div class="cell-title">${escapeHtml(platformItem.label)} 默认策略</div>
|
||||
<div class="cell-desc">${escapeHtml(item?.current_version?.summary || "还没有平台默认策略,当前会沿用系统主 Agent 默认。")}</div>
|
||||
<div class="entity-meta">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPlatformAgentPanel() {
|
||||
const items = safeArray(appState.platformAgents);
|
||||
if (!items.length) {
|
||||
@@ -3906,7 +4037,7 @@ function renderAdminWorkbenchScreen() {
|
||||
: activeTab === "storage"
|
||||
? renderStorageStatusPanel()
|
||||
: activeTab === "agents"
|
||||
? `${renderPlatformAgentPanel()}<div style="margin-top:18px;">${renderOneLinerActionRegistryPanel()}</div>`
|
||||
? `${renderAdminGovernanceSummaryPanel()}${renderPlatformAgentPanel()}<div style="margin-top:18px;">${renderOneLinerActionRegistryPanel()}</div>`
|
||||
: renderAdminOpsPanel()}
|
||||
</div>
|
||||
`
|
||||
@@ -4620,6 +4751,16 @@ function renderPlaybookScreen() {
|
||||
<span class="tag clickable-tag" data-action="open-oneliner-profile">编辑配置</span>
|
||||
</div>
|
||||
</div>
|
||||
${renderGovernanceSummaryCard({
|
||||
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()
|
||||
})}
|
||||
</div>
|
||||
<div class="panel pad" style="box-shadow:none; margin-top:18px;">
|
||||
<div class="panel-head">
|
||||
@@ -6221,12 +6362,192 @@ function openOneLinerProfileAction() {
|
||||
}
|
||||
});
|
||||
appState.onelinerProfile = saved;
|
||||
await loadAgentControlSurfaces(project.id);
|
||||
rememberAction("OneLiner 已保存", `已更新 OneLiner「${saved.display_name || "OneLiner"}」配置。`, "green", saved);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parsePolicyJsonField(rawValue, label = "策略 JSON") {
|
||||
const text = String(rawValue || "").trim();
|
||||
if (!text) return {};
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error(`${label} 必须是 JSON 对象`);
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`${label} 格式不正确:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPolicyVersionSummary(bundle, emptyText) {
|
||||
if (!bundle?.current_version) {
|
||||
return `
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>还没有已发布版本</h4>
|
||||
<p>${escapeHtml(emptyText)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<div class="sheet-html">
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(bundle.current_version.title || bundle.scope?.title || "当前版本")}</h4>
|
||||
<p>${escapeHtml(bundle.current_version.summary || "当前版本还没有补摘要。")}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">版本 ${escapeHtml(formatNumber(bundle.current_version.version_no || 0))}</span>
|
||||
<span class="tag">历史 ${escapeHtml(formatNumber(bundle.versions?.count || 0))}</span>
|
||||
${bundle.effectivity?.effect_mode ? `<span class="tag">${escapeHtml(bundle.effectivity.effect_mode)}</span>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function openUserGlobalPolicyAction() {
|
||||
const project = requireSelectedProject();
|
||||
const bundle = appState.userGlobalPolicy || {};
|
||||
const current = bundle.current_version || {};
|
||||
openActionModal({
|
||||
title: "编辑我的全局策略",
|
||||
description: "这层策略只影响你自己,会优先被主 Agent 读取,再决定是否下发到各个平台 Agent。",
|
||||
submitLabel: "保存全局策略",
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "你还没有发布自己的全局策略,当前会沿用系统默认和平台默认。") },
|
||||
{ name: "title", label: "策略标题", value: current.title || bundle.scope?.title || "用户全局策略", 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: "{\"tone\":{\"style\":\"analytical\"}}" },
|
||||
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:用户要求首页动作更聚焦,默认走分析型语气" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch("/v2/oneliner/governance/user/global", {
|
||||
method: "PUT",
|
||||
body: {
|
||||
project_id: project.id,
|
||||
title: values.title || "用户全局策略",
|
||||
summary: values.summary || "",
|
||||
policy: parsePolicyJsonField(values.policyJson, "全局策略 JSON"),
|
||||
reason: values.reason || ""
|
||||
}
|
||||
});
|
||||
appState.userGlobalPolicy = saved;
|
||||
await loadAgentControlSurfaces(project.id);
|
||||
rememberAction("我的全局策略已保存", `已发布版本 ${saved.current_version?.version_no || 1}。`, "green", saved);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openUserPlatformPolicyAction(platform) {
|
||||
const normalizedPlatform = normalizePlatformValue(platform || getPreferredPlatform(), "douyin");
|
||||
const project = requireSelectedProject();
|
||||
const bundle = appState.userCurrentPlatformPolicy || {};
|
||||
const current = bundle.current_version || {};
|
||||
openActionModal({
|
||||
title: `编辑 ${platformLabel(normalizedPlatform)} 平台策略`,
|
||||
description: "这层策略只作用于你当前项目下的单个平台,会覆盖你的全局策略,但不会影响其他平台。",
|
||||
submitLabel: "保存平台策略",
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "你还没有发布单平台策略,当前会沿用全局策略和系统默认。") },
|
||||
{ name: "title", label: "策略标题", value: current.title || `${platformLabel(normalizedPlatform)} 用户平台策略`, 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: "{\"actions\":{\"max_cards\":1}}" },
|
||||
{ name: "reason", label: "变更原因", type: "textarea", rows: 3, value: "", placeholder: "例如:抖音只保留 1 条首页动作,优先高分作品拆解" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch(`/v2/oneliner/governance/user/platforms/${encodeURIComponent(normalizedPlatform)}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
project_id: project.id,
|
||||
title: values.title || `${platformLabel(normalizedPlatform)} 用户平台策略`,
|
||||
summary: values.summary || "",
|
||||
policy: parsePolicyJsonField(values.policyJson, "平台策略 JSON"),
|
||||
reason: values.reason || ""
|
||||
}
|
||||
});
|
||||
appState.userCurrentPlatformPolicy = saved;
|
||||
await loadAgentControlSurfaces(project.id);
|
||||
rememberAction(`${platformLabel(normalizedPlatform)} 平台策略已保存`, `已发布版本 ${saved.current_version?.version_no || 1}。`, "green", saved);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openSystemMainPolicyAction() {
|
||||
const projectId = getOneLinerProjectId();
|
||||
const bundle = appState.adminSystemMainPolicy || {};
|
||||
const current = bundle.current_version || {};
|
||||
openActionModal({
|
||||
title: "编辑系统主 Agent 策略",
|
||||
description: "这是所有用户共享的系统级主 Agent 基座能力,后续用户层和管理员覆盖都会叠加在它上面。",
|
||||
submitLabel: "保存系统策略",
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, "系统主 Agent 还没有系统默认策略。") },
|
||||
{ name: "title", label: "策略标题", value: current.title || bundle.scope?.title || "系统主 Agent 策略", placeholder: "例如:StoryForge 主 Agent 默认策略" },
|
||||
{ name: "summary", label: "摘要", type: "textarea", rows: 3, value: current.summary || "", placeholder: "写清楚当前系统主 Agent 主要服务的方向和约束" },
|
||||
{ name: "policyJson", label: "策略 JSON", type: "textarea", rows: 8, value: JSON.stringify(current.policy || {}, null, 2), placeholder: "{\"homepage\":{\"focus\":\"ops\"}}" },
|
||||
{ name: "reason", label: "发布原因", type: "textarea", rows: 3, value: "", placeholder: "例如:更新市场节奏后,需要调整首页推荐和调度逻辑" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch("/v2/admin/oneliner/governance/system/main-agent", {
|
||||
method: "PUT",
|
||||
body: {
|
||||
title: values.title || "系统主 Agent 策略",
|
||||
summary: values.summary || "",
|
||||
policy: parsePolicyJsonField(values.policyJson, "系统策略 JSON"),
|
||||
reason: values.reason || ""
|
||||
}
|
||||
});
|
||||
appState.adminSystemMainPolicy = saved;
|
||||
await loadAgentControlSurfaces(projectId);
|
||||
rememberAction("系统主 Agent 策略已保存", `已发布版本 ${saved.current_version?.version_no || 1}。`, "green", saved);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openSystemPlatformPolicyAction(platform) {
|
||||
const normalizedPlatform = normalizePlatformValue(platform, "douyin");
|
||||
const projectId = getOneLinerProjectId();
|
||||
const bundle = safeArray(appState.adminSystemPlatformPolicies).find((item) => item?.scope?.platform === normalizedPlatform) || {};
|
||||
const current = bundle.current_version || {};
|
||||
openActionModal({
|
||||
title: `编辑 ${platformLabel(normalizedPlatform)} 系统平台策略`,
|
||||
description: "这是所有用户共享的系统级平台默认策略,用户自己的平台偏好会在这层之上覆盖。",
|
||||
submitLabel: "保存平台默认策略",
|
||||
fields: [
|
||||
{ type: "html", label: "当前版本", html: renderPolicyVersionSummary(bundle, `当前 ${platformLabel(normalizedPlatform)} 还没有系统平台默认策略。`) },
|
||||
{ name: "title", label: "策略标题", value: current.title || `${platformLabel(normalizedPlatform)} 系统平台策略`, 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: "{\"douyin\":{\"benchmark_mode\":\"strict\"}}" },
|
||||
{ name: "reason", label: "发布原因", type: "textarea", rows: 3, value: "", placeholder: "例如:平台节奏变化,需要调整系统默认方法论" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const saved = await storyforgeFetch(`/v2/admin/oneliner/governance/system/platforms/${encodeURIComponent(normalizedPlatform)}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
title: values.title || `${platformLabel(normalizedPlatform)} 系统平台策略`,
|
||||
summary: values.summary || "",
|
||||
policy: parsePolicyJsonField(values.policyJson, "平台默认策略 JSON"),
|
||||
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(projectId);
|
||||
rememberAction(`${platformLabel(normalizedPlatform)} 系统平台策略已保存`, `已发布版本 ${saved.current_version?.version_no || 1}。`, "green", saved);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openPlatformAgentProfileAction(platform) {
|
||||
const project = requireSelectedProject();
|
||||
const agents = safeArray(appState.platformAgents);
|
||||
@@ -7991,6 +8312,22 @@ document.addEventListener("click", async (event) => {
|
||||
openOneLinerProfileAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-user-global-policy") {
|
||||
openUserGlobalPolicyAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-user-platform-policy") {
|
||||
openUserPlatformPolicyAction(action.dataset.platform || "");
|
||||
return;
|
||||
}
|
||||
if (name === "open-system-main-policy") {
|
||||
openSystemMainPolicyAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-system-platform-policy") {
|
||||
openSystemPlatformPolicyAction(action.dataset.platform || "");
|
||||
return;
|
||||
}
|
||||
if (name === "select-oneliner-session") {
|
||||
appState.selectedOnelinerSessionId = action.dataset.sessionId || "";
|
||||
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
|
||||
|
||||
@@ -37,6 +37,9 @@ test("agent screen excludes quota and registry panels and uses page tabs", () =>
|
||||
assert.doesNotMatch(source, /renderTenantQuotaPanel\(/);
|
||||
assert.doesNotMatch(source, /renderOneLinerActionRegistryPanel\(/);
|
||||
assert.match(source, /renderDetailTabs\("playbookDetailTab"/);
|
||||
assert.match(source, /renderGovernanceSummaryCard\(/);
|
||||
assert.match(source, /open-user-global-policy/);
|
||||
assert.match(source, /open-user-platform-policy/);
|
||||
});
|
||||
|
||||
test("discovery, production, and admin screens use page tabs for heavy content", () => {
|
||||
@@ -47,6 +50,7 @@ test("discovery, production, and admin screens use page tabs for heavy content",
|
||||
assert.match(discovery, /renderDetailTabs\("discoveryDetailTab"/);
|
||||
assert.match(production, /renderDetailTabs\("productionDetailTab"/);
|
||||
assert.match(admin, /renderDetailTabs\("adminWorkbenchTab"/);
|
||||
assert.match(admin, /renderAdminGovernanceSummaryPanel\(/);
|
||||
});
|
||||
|
||||
test("projects screen uses an adaptive project grid instead of a fixed three-column squeeze", () => {
|
||||
@@ -93,3 +97,46 @@ test("oneliner submit failures stay inside the app instead of using a browser al
|
||||
assert.doesNotMatch(APP, /alert\("OneLiner 调度失败:/);
|
||||
assert.match(APP, /presentActionFailure\(error,\s*"OneLiner 调度失败"\)/);
|
||||
});
|
||||
|
||||
test("agent control surfaces load governance endpoints for user and admin summaries", () => {
|
||||
const source = extractBetween(APP, "async function loadAgentControlSurfaces(projectId = \"\")", "async function loadOneLinerMessages(sessionId)");
|
||||
assert.match(source, /\/v2\/oneliner\/governance\/effective/);
|
||||
assert.match(source, /\/v2\/oneliner\/governance\/user\/global/);
|
||||
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\)\}/);
|
||||
});
|
||||
|
||||
test("oneliner meta and action handlers expose governance entry points", () => {
|
||||
const meta = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()");
|
||||
const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {");
|
||||
assert.match(meta, /open-user-global-policy/);
|
||||
assert.match(meta, /policyScopeTagLabel/);
|
||||
assert.match(actions, /name === "open-user-global-policy"/);
|
||||
assert.match(actions, /name === "open-system-main-policy"/);
|
||||
});
|
||||
|
||||
test("system governance saves refresh control surfaces after persisting", () => {
|
||||
const profile = extractBetween(APP, "function openOneLinerProfileAction()", "function parsePolicyJsonField(rawValue, label = \"策略 JSON\")");
|
||||
const userGlobal = extractBetween(APP, "function openUserGlobalPolicyAction()", "function openUserPlatformPolicyAction(platform)");
|
||||
const userPlatform = extractBetween(APP, "function openUserPlatformPolicyAction(platform)", "function openSystemMainPolicyAction()");
|
||||
const main = extractBetween(APP, "function openSystemMainPolicyAction()", "function openSystemPlatformPolicyAction(platform)");
|
||||
const platform = extractBetween(APP, "function openSystemPlatformPolicyAction(platform)", "function openPlatformAgentProfileAction(platform)");
|
||||
|
||||
assert.match(profile, /appState\.onelinerProfile = saved;/);
|
||||
assert.match(profile, /await loadAgentControlSurfaces\(project\.id\);/);
|
||||
|
||||
assert.match(userGlobal, /appState\.userGlobalPolicy = saved;/);
|
||||
assert.match(userGlobal, /await loadAgentControlSurfaces\(project\.id\);/);
|
||||
|
||||
assert.match(userPlatform, /appState\.userCurrentPlatformPolicy = saved;/);
|
||||
assert.match(userPlatform, /await loadAgentControlSurfaces\(project\.id\);/);
|
||||
|
||||
assert.match(main, /const projectId = getOneLinerProjectId\(\);/);
|
||||
assert.match(main, /appState\.adminSystemMainPolicy = saved;/);
|
||||
assert.match(main, /await loadAgentControlSurfaces\(projectId\);/);
|
||||
|
||||
assert.match(platform, /const projectId = getOneLinerProjectId\(\);/);
|
||||
assert.match(platform, /appState\.adminSystemPlatformPolicies = safeArray\(appState\.adminSystemPlatformPolicies\)/);
|
||||
assert.match(platform, /await loadAgentControlSurfaces\(projectId\);/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user