# 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 CRUD；LLM 配置只在 `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 级 RBAC（member/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_documents、llm_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          → LlmConfigView（LLM 配置）
  /admin/skills       → SkillsView（Skill 管理）
  /admin/kb           → KbManagementView（KB 管理）
  /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_id（AuthMiddleware 已注入）
  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/cost，daily/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**: 实现前端管理界面，包括 AdminLayout、7 个管理页面、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？~~ → **已明确**：拒绝删除，强制管理员先移除用户。
