fischer-agentkit/docs/plans/2026-06-21-001-feat-admin-c...

734 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan: Admin Console — Enterprise Department-Scoped Management
**Date**: 2026-06-21
**Status**: completed
**Type**: feat
**Origin**: `docs/brainstorms/2026-06-21-admin-console-requirements.md`
**Branch**: `feat/admin-console`
---
## Summary
为 Fischer AgentKit 构建统一企业管理端,嵌入主应用 `/admin` 路由组。核心模型:单企业部署 + 部门级权限隔离 + 能力按部门绑定 + 用户多部门归属权限并集。MVP 覆盖 4 个领域部门与用户管理、LLM 配置管理、Skill 与 KB 管理、用量仪表盘与配额。Web UI + CLI 双通道共享 service 层。
## Problem Frame
当前 AgentKit 没有统一管理端admin 端点只有 4 个session 管理),无 user CRUDLLM 配置只在 `agentkit.yaml`KB/Skill 无部门隔离;用量不分用户。管理工作流是"零散脚本不成体系"。
实际场景:单企业部署,按部门(人事/研发/财务)划分权限,部门绑定专属 skill/KB/工具,用户可多部门(权限并集)。
## Requirements (from origin doc)
- **R1**: 部门 CRUD + 部门能力绑定skill/KB/工具权限/LLM 配额)
- **R2**: 用户 CRUD + 多部门归属(`user_departments` 多对多)+ 密码重置 + 禁用/启用
- **R3**: LLM 配置运行时管理Provider/Model/API Key CRUD + fallback 链 + 按部门配额)
- **R4**: Skill 启停/编辑/导入 + 按部门绑定
- **R5**: KB 文档 CRUD + 按部门隔离
- **R6**: 用量仪表盘(按部门/用户/时间)+ 配额硬拒绝429
- **R7**: CLI `agentkit admin <domain> <action>` 镜像 Web UI
- **R8**: 部门隔离安全测试(人事部用户无法访问研发部资源,除非同时属于两个部门)
- **SC1**: 超管可在 Web UI 或 CLI 完成全部管理操作,无需手动编辑 YAML 或操作数据库
- **SC2**: 部门隔离通过安全测试
- **SC3**: LLM 配置运行时修改立即生效
- **SC4**: 用量仪表盘数据延迟 < 1 分钟
- **SC5**: 配额超限返回 429
- **SC6**: CLI Web UI 操作结果一致
## Key Technical Decisions
### KTD1: 角色模型简化为两级MVP
**决策**MVP 只有超级管理员admin和普通用户member两种角色不实现部门管理员
**理由**用户明确选择"MVP 只有超管和普通用户"。部门管理员涉及跨部门权限委托本部门资源管理边界等复杂逻辑放到第二期
**影响**现有 `permissions.py` 3 RBACmember/operator/admin保持不变`operator` 角色保留但 MVP 不主动使用所有 admin 端点用现有 `_require_admin``USER_MANAGE` 权限守卫
### KTD2: 部门隔离用 `department_id` 列 + `user_departments` 多对多表
**决策**
- 新增 `departments` id, name, description, is_active, created_at
- 新增 `user_departments` 多对多表user_id, department_id, created_at
- 资源表skills 绑定kb_documentsllm_usage `department_id` NULL 表示全局共享
- `DepartmentContextMiddleware` JWT 读取 user_id查询 `user_departments`注入 `request.state.department_ids`列表
- Repository 层强制 `WHERE department_id IN (?, ?, NULL)` 过滤NULL 表示全局资源所有部门可见
**理由**用户明确选择"共享数据库 + department_id "。多对多表支持用户多部门归属NULL `department_id` 表示全局共享资源如默认 LLM 配置避免每个部门都重复配置
**风险**middleware 完整性是关键——任何遗漏 `department_id` 过滤的查询都是数据泄露通过 repository 层强制参数缓解但需要安全测试覆盖R8)。
### KTD3: LLM 配置保持 YAML + 写回文件MVP
**决策**LLM 配置继续以 `agentkit.yaml` 为权威源运行时修改通过 `PUT /settings/llm` 写回 YAML 文件依赖现有 `ServerConfig.watch_config()` 文件监听触发热重载
**理由**用户明确选择"保持 YAML + 写回文件"。完全迁移到数据库工作量大且现有文件监听热重载机制已可用
**影响**
- 不新增 `llm_providers` / `llm_models` / `llm_api_keys` 数据库表
- 按部门配额存储在数据库`department_quotas` 不存 YAML
- API Key YAML 中存储现有方式按部门配额在 DB
**风险**并发修改 YAML 文件可能冲突MVP 用文件锁`fcntl.flock`缓解第二期考虑迁移到 DB
### KTD4: 用量跟踪加 `user_id` + `department_id` 字段
**决策**
- 修改 `UsageRecord` dataclass新增 `user_id` `department_id` 字段
- 修改 `InMemoryUsageStore` `RedisUsageStore`存储和查询时包含新字段
- LLM Gateway 调用时从请求上下文获取 `user_id` `department_id`传入 `UsageTracker`
- 新增 `GET /admin/usage` 端点按部门/用户/时间维度聚合查询
**理由**当前 `UsageRecord` 只有 `agent_name`无法按用户/部门统计必须加字段才能满足 R6
**影响**`UsageRecord` dataclass加字段向后兼容旧记录的 `user_id`/`department_id` None)。Redis Hash 结构需要扩展 key 包含 `user_id` `department_id`
### KTD5: CLI/Web 共享 service 层
**决策**所有管理操作封装在 `src/agentkit/server/admin/` 下的 service 模块 `user_service.py`、`department_service.py`、`llm_config_service.py`)。Web UI 路由和 CLI 命令都调用这些 service
**理由**用户明确要求"CLI/Web 一致性"。共享 service 层避免双份业务逻辑
**影响**CLI 需要通过 HTTP 调用 server`agentkit admin` 命令实际是调用 `/api/v1/admin/*` 端点而不是直接操作数据库这保证了一致性但要求 CLI server URL 配置
### KTD6: 前端 `AdminLayout` 独立路由树
**决策**前端新增 `AdminLayout.vue`左侧导航 + 内容区所有 admin 页面作为子路由路由结构
```
/admin → AdminLayout
/admin/dashboard → AdminDashboard概览
/admin/departments → DepartmentsView部门管理
/admin/users → UsersView用户管理扩展现有
/admin/llm → LlmConfigViewLLM 配置)
/admin/skills → SkillsViewSkill 管理)
/admin/kb → KbManagementViewKB 管理)
/admin/usage → UsageDashboardView用量仪表盘
```
**理由**独立路由树便于未来拆分到独立前端应用左侧导航统一入口解决"散落各处"问题
## High-Level Technical Design
### 数据模型 ERD
```mermaid
erDiagram
departments ||--o{ user_departments : has
users ||--o{ user_departments : belongs_to
departments ||--o{ department_skill_bindings : binds
skills ||--o{ department_skill_bindings : bound_to
departments ||--o{ department_kb_bindings : binds
kb_sources ||--o{ department_kb_bindings : bound_to
departments ||--o{ department_quotas : has
users ||--o{ llm_usage : generates
departments ||--o{ llm_usage : belongs_to
departments {
string id PK
string name
string description
bool is_active
datetime created_at
}
user_departments {
string user_id FK
string department_id FK
datetime created_at
}
department_skill_bindings {
string id PK
string department_id FK
string skill_name
datetime created_at
}
department_kb_bindings {
string id PK
string department_id FK
string kb_source_id
datetime created_at
}
department_quotas {
string id PK
string department_id FK
string quota_type
string limit_value
string period
datetime updated_at
}
llm_usage {
string id PK
string user_id FK
string department_id FK
string model
int prompt_tokens
int completion_tokens
int total_tokens
float cost
datetime timestamp
}
```
### 请求隔离流程
```mermaid
sequenceDiagram
participant Client
participant AuthMiddleware
participant DepartmentMiddleware
participant Route
participant Service
participant Repository
Client->>AuthMiddleware: Request + JWT
AuthMiddleware->>AuthMiddleware: Verify JWT, extract user_id
AuthMiddleware->>DepartmentMiddleware: request.user_id
DepartmentMiddleware->>DepartmentMiddleware: Query user_departments
DepartmentMiddleware->>DepartmentMiddleware: Set request.state.department_ids
DepartmentMiddleware->>Route: Forward with department context
Route->>Service: Call service method
Service->>Repository: Pass department_ids
Repository->>Repository: WHERE department_id IN (?, ?, NULL)
Repository-->>Service: Filtered results
Service-->>Route: Response
Route-->>Client: Response
```
## Implementation Units
### U1. 数据库 schema 扩展——部门表 + 多对多 + 资源表 department_id
**Goal**: 新增 `departments`、`user_departments`、`department_skill_bindings`、`department_kb_bindings`、`department_quotas` 并为 `llm_usage` `user_id`/`department_id` 字段
**Requirements**: R1, R2, R4, R5, R6
**Dependencies**: 基础单元
**Files**:
- `src/agentkit/server/auth/models.py` 新增表 DDL + ORM 模型bump `_SCHEMA_VERSION` 3
- `src/agentkit/server/admin/__init__.py` 新建 admin 模块
- `src/agentkit/server/admin/models.py` admin 相关 Pydantic 模型Department, UserDepartment, etc.
- `tests/unit/admin/test_models.py` 表创建 + CRUD 测试
**Approach**:
- `models.py` `_SCHEMA_SQL` 中追加 5 个新表的 CREATE TABLE IF NOT EXISTS
- 新增 `_migrate_v2_to_v3()` 迁移函数gated on `auth_meta` marker `schema_v3_departments`
- `departments` id (UUID), name (UNIQUE), description, is_active, created_at
- `user_departments` user_id, department_id, created_at, PRIMARY KEY (user_id, department_id)
- `department_skill_bindings` id, department_id, skill_name, created_at, UNIQUE (department_id, skill_name)
- `department_kb_bindings` id, department_id, kb_source_id, created_at, UNIQUE (department_id, kb_source_id)
- `department_quotas` id, department_id, quota_type (token_limit/cost_limit/model_whitelist), limit_value (JSON), period (daily/monthly), updated_at
- `llm_usage` 不是 SQL Redis所以 `user_id`/`department_id` 加在 `UsageRecord` dataclass U8
**Patterns to follow**: 现有 `models.py` `_SCHEMA_SQL` + `_migrate_v2_to_v3` 模式参考 `_backfill_user_sessions`
**Test scenarios**:
- Happy path: `init_auth_db()` 创建新表`auth_meta` 记录 schema_v3_departments
- Edge case: 重复调用 `init_auth_db()` 不报错幂等
- Edge case: 已有 v2 数据库升级到 v3新表创建成功现有数据不丢失
- Integration: `departments` 表插入 + 查询`user_departments` 多对多关系正确
**Verification**: `pytest tests/unit/admin/test_models.py -v` 通过手动检查 `data/auth.db` 新表存在
---
### U2. 部门 CRUD service + API 端点
**Goal**: 实现部门的创建列表编辑禁用/启用删除以及部门能力绑定skill/KB管理
**Requirements**: R1, R4, R5
**Dependencies**: U1
**Files**:
- `src/agentkit/server/admin/department_service.py` DepartmentService CRUD + 能力绑定
- `src/agentkit/server/routes/admin.py` 新建独立 admin_router 模块 auth.py 迁出 session 端点 + 新增 department 端点
- `src/agentkit/server/app.py` 挂载新 admin_router
- `tests/unit/admin/test_department_service.py` service 单元测试
- `tests/integration/admin/test_department_routes.py` API 集成测试
**Approach**:
- `DepartmentService` 方法`create_department`, `list_departments`, `get_department`, `update_department`, `disable_department`, `delete_department`, `bind_skill`, `unbind_skill`, `list_department_skills`, `bind_kb`, `unbind_kb`, `list_department_kbs`
- 删除部门时检查是否有用户归属有则拒绝或强制 cascade 删除 user_departments
- 禁用部门时`is_active=0`),该部门用户仍可登录但无法访问部门资源
- API 端点
- `POST /api/v1/admin/departments` 创建
- `GET /api/v1/admin/departments` 列表
- `GET /api/v1/admin/departments/{id}` 详情
- `PATCH /api/v1/admin/departments/{id}` 编辑
- `DELETE /api/v1/admin/departments/{id}` 删除
- `POST /api/v1/admin/departments/{id}/skills/{name}` 绑定 skill
- `DELETE /api/v1/admin/departments/{id}/skills/{name}` 解绑 skill
- `POST /api/v1/admin/departments/{id}/kb/{source_id}` 绑定 KB
- `DELETE /api/v1/admin/departments/{id}/kb/{source_id}` 解绑 KB
- 所有端点用 `_require_admin` 守卫
**Patterns to follow**: `kb_management.py` APIRouter + Pydantic 模型 + Depends 模式
**Test scenarios**:
- Happy path: 创建部门 列表返回 编辑名称 禁用 启用 删除
- Happy path: 绑定 skill 列表返回绑定的 skill 解绑
- Edge case: 创建重名部门 409 Conflict
- Edge case: 删除有用户归属的部门 400 Bad Request
- Error path: 非管理员访问 403
- Error path: 不存在的部门 ID 404
- Integration: 部门禁用后该部门用户访问部门资源 403
**Verification**: `pytest tests/unit/admin/test_department_service.py tests/integration/admin/test_department_routes.py -v` 通过
---
### U3. 用户 CRUD service + API 端点 + 密码重置
**Goal**: 实现用户创建列表编辑禁用/启用删除密码重置多部门归属管理
**Requirements**: R2
**Dependencies**: U1, U2
**Files**:
- `src/agentkit/server/admin/user_service.py` UserService
- `src/agentkit/server/routes/admin.py` 新增 user 端点扩展 U2 admin_router
- `src/agentkit/server/auth/providers/local.py` 新增 `create_user` 方法
- `tests/unit/admin/test_user_service.py`
- `tests/integration/admin/test_user_routes.py`
**Approach**:
- `LocalAuthProvider.create_user(username, email, password, role)` bcrypt hash + INSERT INTO users
- `UserService` 方法`create_user`, `list_users`, `get_user`, `update_user`, `disable_user`, `enable_user`, `delete_user`, `reset_password`, `assign_department`, `remove_department`, `list_user_departments`
- API 端点
- `POST /api/v1/admin/users` 创建用户
- `GET /api/v1/admin/users` 列表支持 department_id 过滤
- `GET /api/v1/admin/users/{id}` 详情含部门归属
- `PATCH /api/v1/admin/users/{id}` 编辑role, is_active, is_terminal_authorized
- `DELETE /api/v1/admin/users/{id}` 删除软删除is_active=0
- `POST /api/v1/admin/users/{id}/reset-password` 重置密码
- `POST /api/v1/admin/users/{id}/departments/{dept_id}` 分配部门
- `DELETE /api/v1/admin/users/{id}/departments/{dept_id}` 移除部门归属
- 创建用户时可选指定部门列表
- 密码重置用 bcrypt hash 新密码更新 `password_hash` revoke 该用户所有会话`revoke_all_for_user`
**Patterns to follow**: 现有 `auth.py` `_resolve_db_path` + `aiosqlite` 模式
**Test scenarios**:
- Happy path: 创建用户 列表返回 分配部门 用户详情含部门 重置密码 旧会话失效
- Happy path: 用户多部门归属 两个部门都返回该用户
- Edge case: 创建重名用户 409
- Edge case: 删除自己 400
- Edge case: 移除用户最后一个部门 允许用户变为无部门全局用户
- Error path: 非管理员访问 403
- Error path: 重置密码后旧 token 仍可用 失败会话已 revoke
- Integration: 创建用户后用新用户登录 成功
**Verification**: `pytest tests/unit/admin/test_user_service.py tests/integration/admin/test_user_routes.py -v` 通过
---
### U4. DepartmentContextMiddleware + repository 层隔离
**Goal**: 实现请求级别的部门上下文注入确保所有资源查询按部门过滤
**Requirements**: R8, SC2
**Dependencies**: U1, U2, U3
**Files**:
- `src/agentkit/server/admin/middleware.py` DepartmentContextMiddleware
- `src/agentkit/server/admin/context.py` DepartmentContext dataclass + get_department_context() 依赖
- `src/agentkit/server/app.py` 注册 middleware AuthMiddleware 之后
- `src/agentkit/server/routes/skills.py` 修改 skill 查询 department_ids 过滤
- `src/agentkit/server/routes/kb_management.py` 修改 KB 查询 department_ids 过滤
- `tests/integration/admin/test_department_isolation.py` 隔离安全测试
**Approach**:
- `DepartmentContextMiddleware`
1. `request.state.user` 获取 user_idAuthMiddleware 已注入
2. 查询 `user_departments` 获取 department_ids
3. 注入 `request.state.department_ids`列表可能为空表示全局用户
4. 白名单路径/auth/*, /docs, /health跳过
- `get_department_context()` 依赖 request.state 读取 department_ids返回 DepartmentContext
- Skill 查询修改`GET /skills` 返回全局 skill + 用户部门绑定的 skill
- KB 查询修改`GET /kb-management/sources` 返回全局 KB + 用户部门绑定的 KB
- Admin 端点`/admin/*`跳过部门过滤超管可看所有
**Patterns to follow**: 现有 `AuthMiddleware` BaseHTTPMiddleware 模式
**Test scenarios**:
- Happy path: 用户属于部门 A GET /skills 返回全局 skill + 部门 A 绑定的 skill
- Happy path: 用户属于部门 A B GET /skills 返回全局 + A + B skill
- Happy path: 用户无部门 GET /skills 只返回全局 skill
- Security: 用户 A仅部门 A访问部门 B 绑定的 skill 404 或不在列表中
- Security: 用户 A 尝试通过 API 直接访问部门 B KB 文档 403/404
- Security: Admin 用户访问任意部门资源 成功跳过过滤
- Integration: 用户从部门 A 移除后 立即无法访问部门 A skill
**Verification**: `pytest tests/integration/admin/test_department_isolation.py -v` 通过安全测试覆盖所有资源类型
---
### U5. LLM 配置管理端点YAML 写回 + 按部门配额)
**Goal**: 实现 LLM Provider/Model/API Key 的运行时 CRUD写回 `agentkit.yaml`支持按部门配额
**Requirements**: R3, SC3
**Dependencies**: U1, U2
**Files**:
- `src/agentkit/server/admin/llm_config_service.py` LlmConfigService
- `src/agentkit/server/routes/admin.py` 新增 LLM 配置端点
- `src/agentkit/server/routes/settings.py` 复用/扩展现有 `GET/PUT /settings/llm`
- `tests/unit/admin/test_llm_config_service.py`
- `tests/integration/admin/test_llm_config_routes.py`
**Approach**:
- `LlmConfigService` 方法
- `list_providers()`, `get_provider(name)`, `create_provider(name, config)`, `update_provider(name, config)`, `delete_provider(name)`
- `list_models(provider)`, `add_model(provider, model, config)`, `update_model()`, `delete_model()`
- `list_api_keys()` 返回 provider + key 前缀不返回完整 key
- `set_api_key(provider, key)` 写入 YAML`${ENV_VAR}` 替换保持
- `get_fallbacks()`, `set_fallbacks(model, chain)`
- `set_department_quota(dept_id, quota_type, limit, period)`, `get_department_quota(dept_id)`
- 写回 YAML `yaml.dump` + `fcntl.flock` 文件锁
- 现有 `watch_config()` 监听文件变化触发热重载无需额外通知
- API 端点
- `GET /api/v1/admin/llm/providers` 列表
- `POST /api/v1/admin/llm/providers` 创建
- `PATCH /api/v1/admin/llm/providers/{name}` 编辑
- `DELETE /api/v1/admin/llm/providers/{name}` 删除
- `POST /api/v1/admin/llm/providers/{name}/api-key` 设置 API Key
- `GET /api/v1/admin/llm/fallbacks` fallback
- `PUT /api/v1/admin/llm/fallbacks/{model}` 设置 fallback
- `GET /api/v1/admin/departments/{id}/quotas` 部门配额
- `PUT /api/v1/admin/departments/{id}/quotas` 设置配额
**Patterns to follow**: 现有 `settings.py` `GET/PUT /settings/llm` 模式
**Test scenarios**:
- Happy path: 添加 provider YAML 文件更新 热重载触发 provider 可用
- Happy path: 修改 API Key YAML 更新 key 失效
- Happy path: 设置 fallback model A 失败时自动切换到 model B
- Happy path: 设置部门配额 部门用户超限 429
- Edge case: YAML 文件被外部修改 watch_config 触发重载 配置同步
- Edge case: 并发修改 文件锁保护 后写者覆盖
- Error path: 无效 provider 配置 400
- Error path: 删除正在使用的 provider 400
**Verification**: `pytest tests/unit/admin/test_llm_config_service.py tests/integration/admin/test_llm_config_routes.py -v` 通过手动验证 YAML 修改后热重载
---
### U6. Skill 与 KB 管理端点(启停 + 部门绑定)
**Goal**: 实现 Skill 启停/编辑/导入的 admin 端点KB 文档管理 admin 端点按部门绑定
**Requirements**: R4, R5
**Dependencies**: U1, U2, U4
**Files**:
- `src/agentkit/server/admin/skill_service.py` SkillService 启停/编辑/导入
- `src/agentkit/server/admin/kb_service.py` KbService 文档管理
- `src/agentkit/server/routes/admin.py` 新增 skill/kb 端点
- `src/agentkit/server/routes/skill_management.py` 实现 reload 端点当前是 stub
- `tests/unit/admin/test_skill_service.py`
- `tests/integration/admin/test_skill_kb_routes.py`
**Approach**:
- `SkillService` 方法`enable_skill(name)`, `disable_skill(name)`, `update_skill_config(name, config)`, `import_skill(yaml_content)`, `reload_skill(name)`
- 启停通过在 skill registry 中标记 `enabled=False`查询时过滤
- `KbService` 方法`list_documents(department_id)`, `upload_document(file, department_id)`, `delete_document(id)`, `sync_source(id)`, `rebuild_index(id)`
- KB 文档加 `department_id` KB store 当前是内存存储需要持久化或加 metadata
- API 端点
- `POST /api/v1/admin/skills/{name}/enable` 启用
- `POST /api/v1/admin/skills/{name}/disable` 禁用
- `PATCH /api/v1/admin/skills/{name}` 编辑配置
- `POST /api/v1/admin/skills/import` YAML 导入
- `POST /api/v1/admin/skills/{name}/reload` 重载
- `GET /api/v1/admin/kb/documents` 列表支持 department_id 过滤
- `POST /api/v1/admin/kb/documents` 上传指定 department_id
- `DELETE /api/v1/admin/kb/documents/{id}` 删除
- `POST /api/v1/admin/kb/sources/{id}/sync` 同步
- `POST /api/v1/admin/kb/sources/{id}/rebuild` 重建索引
**Patterns to follow**: 现有 `skills.py` install/unregister 模式
**Test scenarios**:
- Happy path: 禁用 skill GET /skills 不返回该 skill 用户无法使用
- Happy path: 启用 skill 恢复可用
- Happy path: 导入 skill YAML 注册成功 可用
- Happy path: 上传 KB 文档到部门 A 部门 A 用户可搜索 部门 B 用户不可见
- Edge case: 禁用不存在的 skill 404
- Edge case: 导入无效 YAML 400
- Security: 部门 A 用户尝试访问部门 B KB 文档 404
- Integration: 禁用 skill 正在使用该 skill 的会话 优雅降级或错误提示
**Verification**: `pytest tests/unit/admin/test_skill_service.py tests/integration/admin/test_skill_kb_routes.py -v` 通过
---
### U7. 用量仪表盘 + 配额执行
**Goal**: 实现按部门/用户/时间的用量查询配额检查在 LLM 调用时执行超限返回 429)。
**Requirements**: R6, SC4, SC5
**Dependencies**: U1, U2, U5
**Files**:
- `src/agentkit/llm/providers/usage_store.py` 修改 `UsageRecord` + store 实现
- `src/agentkit/llm/gateway.py` 调用时传入 user_id/department_id调用前检查配额
- `src/agentkit/server/admin/usage_service.py` UsageService 聚合查询
- `src/agentkit/server/admin/quota_service.py` QuotaService 配额检查
- `src/agentkit/server/routes/admin.py` 新增 usage 端点
- `tests/unit/admin/test_usage_service.py`
- `tests/unit/admin/test_quota_service.py`
- `tests/integration/admin/test_usage_routes.py`
**Approach**:
- `UsageRecord` `user_id: str | None`, `department_id: str | None`
- `RedisUsageStore` Hash key 扩展`agentkit:usage:{date}:{user_id}:{department_id}`
- `LLMGateway.complete()` 调用前
1. 获取当前 user_id + department_ids
2. 对每个 department_id 检查配额token/costdaily/monthly
3. 超限 raise `QuotaExceededError` 路由层返回 429
4. 调用后记录 usage user_id + department_id
- `UsageService` 方法`get_usage_summary(department_id, user_id, start, end)`, `get_usage_timeseries(department_id, user_id, start, end, interval)`, `get_usage_by_model(department_id, user_id, start, end)`, `get_top_users(department_id, limit)`, `export_usage(department_id, format)`
- API 端点
- `GET /api/v1/admin/usage/summary?department_id=&user_id=&start=&end=` 汇总
- `GET /api/v1/admin/usage/timeseries?...&interval=hour/day` 时间序列
- `GET /api/v1/admin/usage/by-model?...` model 分桶
- `GET /api/v1/admin/usage/top-users?department_id=&limit=` 用户排行
- `GET /api/v1/admin/usage/export?format=csv/json` 导出
**Patterns to follow**: 现有 `llm.py` `GET /llm/usage` 模式
**Test scenarios**:
- Happy path: 用户调用 LLM usage 记录含 user_id + department_id
- Happy path: 查询部门 A 用量 返回部门 A 的聚合数据
- Happy path: 查询用户 X 用量 返回用户 X 的数据
- Happy path: 导出 CSV 格式正确
- Quota: 部门 A token 配额 1000 1001 token 429
- Quota: 用户月成本上限 $10 $11 429
- Quota: 多部门用户部门 A 超限但部门 B 未超 拒绝取最严约束
- Edge case: usage 数据 返回空结果
- Edge case: 跨天查询 按天聚合
- Error path: 非管理员访问 403
**Verification**: `pytest tests/unit/admin/test_usage_service.py tests/unit/admin/test_quota_service.py tests/integration/admin/test_usage_routes.py -v` 通过
---
### U8. CLI admin 命令组
**Goal**: 实现 `agentkit admin <domain> <action>` 命令组通过 HTTP 调用 server API
**Requirements**: R7, SC6
**Dependencies**: U2, U3, U5, U6, U7
**Files**:
- `src/agentkit/cli/admin.py` admin Typer sub-app
- `src/agentkit/cli/main.py` 注册 admin sub-app
- `src/agentkit/cli/admin_client.py` AdminHttpClient封装 HTTP 调用
- `tests/unit/cli/test_admin_commands.py`
**Approach**:
- `AdminHttpClient` `agentkit.yaml` 或环境变量读取 server URL + admin API key
- 命令结构
```
agentkit admin department list/create/update/delete/bind-skill/unbind-skill/bind-kb/unbind-kb
agentkit admin user list/create/update/delete/reset-password/assign-department/remove-department
agentkit admin llm list-providers/add-provider/update-provider/delete-provider/set-api-key/set-fallback/set-quota
agentkit admin skill list/enable/disable/import/reload
agentkit admin kb list-documents/upload/delete/sync/rebuild
agentkit admin usage summary/timeseries/by-model/top-users/export
```
- 所有命令输出用 Rich 表格/JSON 格式
- `--json` 标志输出原始 JSON便于脚本处理
- 认证 `agentkit pair` 生成的 API key已有机制 admin 用户名密码登录获取 JWT
**Patterns to follow**: 现有 `cli/task.py` Typer sub-app + Rich 输出模式
**Test scenarios**:
- Happy path: `agentkit admin department list` 返回部门列表Rich 表格
- Happy path: `agentkit admin user create --username alice --email alice@corp.com` 创建用户
- Happy path: `agentkit admin llm add-provider --name openai --api-key $KEY` 添加 provider
- Happy path: `agentkit admin usage summary --department hr --start 2026-06-01 --end 2026-06-30` 用量汇总
- Happy path: `--json` 标志 输出有效 JSON
- Edge case: server 不可达 友好错误提示
- Edge case: API key 无效 401 提示
- Edge case: 创建重名用户 显示 409 错误
**Verification**: `pytest tests/unit/cli/test_admin_commands.py -v` 通过手动验证 CLI Web UI 操作结果一致
---
### U9. 前端 AdminLayout + 管理页面
**Goal**: 实现前端管理界面包括 AdminLayout7 个管理页面API 客户端扩展
**Requirements**: SC1, SC6
**Dependencies**: U2, U3, U5, U6, U7
**Files**:
- `src/agentkit/server/frontend/src/layouts/AdminLayout.vue` 左侧导航 + 内容区
- `src/agentkit/server/frontend/src/views/admin/DashboardView.vue` 概览
- `src/agentkit/server/frontend/src/views/admin/DepartmentsView.vue` 部门管理
- `src/agentkit/server/frontend/src/views/admin/UsersView.vue` 扩展现有 CRUD
- `src/agentkit/server/frontend/src/views/admin/LlmConfigView.vue` LLM 配置
- `src/agentkit/server/frontend/src/views/admin/SkillsView.vue` Skill 管理
- `src/agentkit/server/frontend/src/views/admin/KbManagementView.vue` KB 管理
- `src/agentkit/server/frontend/src/views/admin/UsageDashboardView.vue` 用量仪表盘
- `src/agentkit/server/frontend/src/api/admin.ts` 扩展 AdminApiClient
- `src/agentkit/server/frontend/src/router/index.ts` 更新路由结构
- `src/agentkit/server/frontend/src/components/layout/TopNav.vue` 更新 admin 入口
**Approach**:
- `AdminLayout`左侧导航7 个菜单项+ 顶部用户信息 + 内容区 `<router-view />`
- 路由结构改为嵌套
```ts
{
path: '/admin',
component: AdminLayout,
meta: { requiresAdmin: true },
children: [
{ path: '', redirect: '/admin/dashboard' },
{ path: 'dashboard', component: DashboardView },
{ path: 'departments', component: DepartmentsView },
{ path: 'users', component: UsersView },
{ path: 'llm', component: LlmConfigView },
{ path: 'skills', component: SkillsView },
{ path: 'kb', component: KbManagementView },
{ path: 'usage', component: UsageDashboardView },
]
}
```
- `AdminApiClient` 扩展department/user/llm/skill/kb/usage 方法
- 各页面用 Ant Design Vue 组件Table, Form, Modal, Card, Chart
- 用量仪表盘用 ECharts Ant Design Charts
**Patterns to follow**: 现有 `UsersView.vue` 的双标签页 + `UserSessionsPanel` 模式
**Test scenarios**:
- Happy path: admin 用户访问 /admin 重定向到 /admin/dashboard
- Happy path: 部门管理页 创建/编辑/删除部门 列表更新
- Happy path: 用户管理页 创建用户 分配部门 列表更新
- Happy path: LLM 配置页 添加 provider 列表更新 热重载提示
- Happy path: 用量仪表盘 选择部门和时间范围 图表显示
- Edge case: 非管理员访问 /admin 重定向到 /agent/chat
- Edge case: 网络错误 友好错误提示
- Edge case: 表单验证错误 字段级错误提示
**Verification**: `npm run typecheck` 通过手动验证各页面功能
---
### U10. 集成测试 + 安全测试
**Goal**: 端到端集成测试 + 部门隔离安全测试验证 SC1-SC6
**Requirements**: R8, SC1-SC6
**Dependencies**: U1-U9
**Files**:
- `tests/integration/admin/test_e2e_admin_flow.py` 端到端流程测试
- `tests/integration/admin/test_security_isolation.py` 部门隔离安全测试
- `tests/integration/admin/test_quota_enforcement.py` 配额执行测试
**Approach**:
- E2E 流程测试
1. 创建部门人事研发
2. 创建用户并分配部门
3. 绑定 skill/KB 到部门
4. 配置 LLM provider + 部门配额
5. 用普通用户登录 验证只能访问所属部门资源
6. 查看用量仪表盘 数据正确
7. CLI 执行相同操作 结果一致
- 安全测试
1. 人事部用户尝试访问研发部 skill 404
2. 人事部用户尝试访问研发部 KB 文档 404
3. 人事部用户尝试访问研发部用量 403
4. 用户从部门移除后 立即无法访问该部门资源
5. 部门禁用后 该部门用户无法访问部门资源
6. JWT 篡改 department_ids middleware 重新查询不信任 JWT
- 配额测试
1. 设置部门 token 配额 1000
2. 用户调用 LLM 直到 1000 token
3. 1001 token 429
4. 重置配额 可继续调用
**Test scenarios**: 见上述 Approach
**Verification**: `pytest tests/integration/admin/ -v` 通过安全测试 100% 覆盖
---
## Scope Boundaries
### In Scope
- 部门 CRUD + 能力绑定
- 用户 CRUD + 多部门归属 + 密码重置
- LLM 配置 YAML 写回 + 按部门配额
- Skill 启停/编辑/导入 + 部门绑定
- KB 文档管理 + 部门隔离
- 用量仪表盘 + 配额硬拒绝
- CLI admin 命令组
- 前端 AdminLayout + 7 个管理页面
- 部门隔离安全测试
### Deferred to Follow-Up Work
- 部门管理员角色第二期
- LLM 配置迁移到数据库第二期
- 审计日志第二期
- SLA 监控第二期
- SSO/SAML 集成第二期
- 独立管理应用部署架构已预留第二期实现
- 配额软降级第二期
- 用量告警 Webhook/邮件第二期MVP 只做日志
### Outside This Product's Identity
- SaaS 多租户外部多客户租户计费
- 物理隔离的独立数据库
- 计费系统集成实际扣款
## Risks & Dependencies
### Risks
1. **部门隔离 middleware 完整性**)—— 任何遗漏 `department_id` 过滤的查询都是数据泄露缓解repository 层强制参数 + 安全测试覆盖U10)。
2. **LLM 配置 YAML 并发修改**)—— 多人同时修改 YAML 可能冲突缓解`fcntl.flock` 文件锁第二期迁移到 DB
3. **用量跟踪 Redis Hash key 扩展**)—— 修改 key 结构可能影响现有数据缓解版本化 key`agentkit:usage:v2:{date}:{user_id}:{dept_id}` key 保留
4. **KB 内存存储持久化**)—— 当前 KB 是内存存储 `department_id` 需要持久化或 metadata 扩展缓解MVP metadata dict 存储 department_id第二期迁移到 DB
5. **CLI 认证机制**)—— CLI 需要 server URL + API key 配置缓解复用 `agentkit pair` 机制
### Dependencies
- 现有 JWT 认证 + RBAC 权限模型
- 现有 `agentkit.yaml` 文件监听热重载机制
- 现有 `agentkit` CLI 框架Typer
- 现有 Ant Design Vue 组件库
## Phased Delivery
| 批次 | 实施单元 | 交付物 |
|------|----------|--------|
| 批次 1 | U1, U2, U3, U4 | 部门与用户管理 + 隔离 middleware |
| 批次 2 | U5 | LLM 配置管理 |
| 批次 3 | U6 | Skill KB 管理 |
| 批次 4 | U7 | 用量仪表盘 + 配额 |
| 批次 5 | U8, U9, U10 | CLI + 前端 + 集成测试 |
每批次独立可交付可测试可合并
## Open Questions (Resolved)
- ~~KB 内存存储如何加 `department_id`~~ **已明确**MVP metadata dict 存储 department_id不持久化重启丢失绑定关系可接受)。第二期迁移到 SQLite
- ~~CLI 认证用 API key 还是用户名密码~~ **已明确**API key复用 `agentkit pair` 机制CLI 配置文件存储 server_url + api_key
- ~~用量仪表盘图表库选 ECharts 还是 Ant Design Charts~~ **已明确**Ant Design Charts与现有 UI 一致
- ~~部门删除策略拒绝删除有用户的部门还是 cascade 删除 user_departments~~ **已明确**拒绝删除强制管理员先移除用户