# AI 值班 Web 平台 - 详细需求规格说明书 (PRD)

**版本**: v8.0 (Execution Ready)
**日期**: 2025-XX-XX
**项目代号**: AI-Watch-Pro
**核心架构**: SPA Served by Backend (Vue3 build -> FastAPI static)
**包管理**: `uv` (Python)
**关键技术**: OpenCV Grab-Only, WebSocket, APScheduler, MySQL

---

## 1. 系统总体架构

### 1.1 技术栈清单
*   **后端 (Server)**:
    *   语言: Python 3.10+ (由 `uv` 管理)
    *   Web 框架: FastAPI
    *   服务器: Uvicorn
    *   视觉处理: OpenCV (`opencv-python-headless`) - 多线程
    *   数据库: MySQL 8.0 (ORM: SQLAlchemy, Driver: pymysql)
    *   任务调度: APScheduler (AsyncIOScheduler)
    *   AI 客户端: OpenAI SDK (兼容通义千问/GPT-4o/Gemini)
*   **前端 (Client)**:
    *   框架: Vue 3 (Composition API)
    *   构建: Vite
    *   UI 库: Element Plus
    *   状态管理: Pinia
    *   图表: ECharts
*   **部署模式**: 前端 `npm run build` 生成静态文件，由 FastAPI 的 `StaticFiles` 统一托管，实现单端口服务。

### 1.2 目录结构规范
```text
ai-watch-platform/
├── .venv/                      # uv 虚拟环境
├── backend/
│   ├── app/
│   │   ├── api/                # REST API 路由 (v1)
│   │   ├── core/               # 配置 (config.py, security.py)
│   │   ├── models/             # SQLAlchemy 数据库模型
│   │   ├── schemas/            # Pydantic 数据校验模型
│   │   ├── services/           # 核心业务逻辑
│   │   │   ├── video_core.py   # [重点] 视频流线程池管理
│   │   │   ├── llm_agent.py    # AI 调用封装
│   │   │   └── scheduler.py    # 定时任务
│   │   ├── static/             # 存放运行时生成的报警截图
│   │   └── main.py             # 启动入口 (含前端托管逻辑)
│   ├── dist/                   # [Vue打包产物] css/js/index.html
│   └── requirements.txt        # 依赖列表
├── frontend/                   # Vue 源代码
├── run.py                      # 根目录启动脚本
└── pyproject.toml              # uv 配置 (可选)
```

---

## 2. 数据库设计 (Schema)

所有表均使用 `utf8mb4` 字符集。

### 2.1 用户表 (`users`)
*虽然目前只有 Admin，但预留扩展性。*
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | INT (PK) | 自增主键 |
| `username` | VARCHAR(50) | 唯一，默认 `admin` |
| `hashed_password` | VARCHAR(255)| 加密后的密码 |
| `is_active` | BOOLEAN | 默认 True |

### 2.2 摄像头表 (`cameras`)
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | INT (PK) | 自增 |
| `name` | VARCHAR(100)| 摄像头备注名 (如: 1号车间) |
| `stream_url` | VARCHAR(500)| RTSP 或 HTTP-FLV/MP4 地址 |
| `status` | INT | 0:离线, 1:在线 (由后台心跳更新) |
| `created_at` | DATETIME | 创建时间 |

### 2.3 模型配置表 (`model_configs`)
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | INT (PK) | |
| `name` | VARCHAR(50) | 配置别名 (如: GPT-4o-Mini) |
| `base_url` | VARCHAR(255)| API 基础地址 |
| `api_key` | VARCHAR(255)| 密钥 (前端展示时需脱敏) |
| `model_name` | VARCHAR(100)| 实际模型ID (e.g., gpt-4o) |

### 2.4 值班任务表 (`tasks`)
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | INT (PK) | |
| `name` | VARCHAR(100)| 任务名 |
| `model_config_id`| INT (FK) | 关联使用的模型 |
| `camera_ids` | JSON | 摄像头ID数组 `[1, 5, 12]` |
| `prompt` | TEXT | 检测提示词 |
| `cron_expression`| VARCHAR(50) | 轮询频率 (如 `*/5 * * * *`) |
| `is_running` | BOOLEAN | 开/关状态 |

### 2.5 报警日志表 (`task_logs`)
| 字段 | 类型 | 说明 |
| :--- | :--- | :--- |
| `id` | BIGINT (PK)| |
| `task_id` | INT (FK) | 关联任务 |
| `camera_id` | INT (FK) | 关联摄像头 |
| `check_time` | DATETIME | 巡检时间 |
| `snapshot_path` | VARCHAR(255)| **关键**: 图片相对路径 `/static/snapshots/...` |
| `ai_result` | TEXT | AI 返回的完整分析 |
| `is_alarm` | BOOLEAN | 是否异常 |

---

## 3. 功能模块详细规格

### 3.1 认证模块 (Auth Module)
*   **前端逻辑**:
    *   登录页输入账号 `admin` / `HNYZ0821`。
    *   调用 `POST /api/v1/login`。
    *   获取 JWT Token 存入 localStorage。
    *   Axios 拦截器：所有请求头自动携带 `Authorization: Bearer <token>`。
    *   如果 Token 失效 (401)，自动跳转回登录页。
*   **后端逻辑**:
    *   验证用户名密码。
    *   签发 JWT (过期时间建议设为 7 天)。

### 3.2 仪表盘模块 (Dashboard & Real-time Monitor)
此模块需解决 20 路并发显示的性能问题。

*   **功能描述**: 
    *   展示 20 个摄像头的实时画面（低延迟）。
    *   展示今日异常统计、任务运行状态。
*   **后端实现 (WebSocket + Grab-Only)**:
    *   **WebSocket 端点**: `ws://host/ws/stream`。
    *   **推流循环**: 
        *   创建一个后台 `asyncio` 任务。
        *   **每 1000ms (1秒)** 循环一次。
        *   调用 `VideoManager.get_all_snapshots()`。
        *   该方法遍历所有 20 个 `LowCostStream` 对象，触发一次 `retrieve()` 解码，获取最新帧。
        *   将 20 张图片压缩为 JPEG (质量 60)，转 Base64。
        *   打包成 JSON: `{"cam_1": "base64...", "cam_2": "base64..."}`。
        *   通过 WebSocket 群发给所有已连接的客户端。
*   **前端实现**:
    *   建立 WebSocket 连接。
    *   接收到 JSON 后，解析并直接修改对应 `<img>` 标签的 `src` 属性。
    *   **优化**: 如果某摄像头 ID 在 JSON 中为 `null`，显示“信号中断”占位图。

### 3.3 视频资源管理模块 (Video Manager)
*   **功能**: 增删改查摄像头。
*   **后端核心逻辑 (`VideoManager` 单例)**:
    *   **初始化**: FastAPI 启动时 (`@app.on_event("startup")`)，从数据库读取所有摄像头，并为每一个创建一个 `LowCostStream` 线程。
    *   **Grab-Only 线程逻辑**:
        *   死循环执行 `cap.grab()` (只清空缓冲区，不解码，CPU < 1%)。
        *   当 `get_snapshot()` 被调用时，执行一次 `cap.retrieve()` (解码，消耗 CPU)。
        *   **断线重连**: 如果 `grab()` 失败，线程休眠 5 秒后尝试 `cap.open()`。
    *   **新增 API**: 写入数据库 -> 调用 `VideoManager.add_stream(url)` 启动新线程。
    *   **删除 API**: 删除数据库记录 -> 调用 `VideoManager.stop_stream(id)` 销毁线程。

### 3.4 任务调度模块 (AI Task Scheduler)
这是系统的“大脑”。

*   **功能**: 定义何时、看哪里、查什么。
*   **输入**:
    *   选择 10 个摄像头。
    *   Prompt: "检查工人是否佩戴安全帽，若未佩戴返回异常"。
    *   频率: 每 5 分钟。
*   **后端逻辑 (APScheduler)**:
    1.  任务创建时，向调度器添加一个 Job。
    2.  **Job 执行流程**:
        *   **步骤 A (极速截帧)**: 遍历这 10 个摄像头，调用 `video_manager.get_snapshot(id)`。由于是内存操作，10 路截帧耗时 < 0.5秒。
        *   **步骤 B (落盘)**: 将获取到的图片写入 `/backend/app/static/snapshots/{date}/{uuid}.jpg`。
        *   **步骤 C (AI 分析)**: 
            *   使用 `asyncio.gather` 并发调用 LLM 接口 (防止一个请求卡住后面 9 个)。
            *   构造请求: Image (Base64/URL) + Prompt。
            *   系统提示词 (System Prompt): "你是一个安防助手。请简短回答。如果发现异常，请在回复开头加上 [ALARM] 标记。"
        *   **步骤 D (入库)**:
            *   解析 AI 回复，检测是否包含 `[ALARM]`。
            *   将 `image_path` (相对路径), `ai_result`, `is_alarm` 写入 `task_logs` 表。

### 3.5 报警与报表模块 (Reporting)
*   **功能**: 历史回溯与导出。
*   **前端**:
    *   表格展示日志，支持按“仅看异常”筛选。
    *   **图片预览**: 点击缩略图，弹出 `<el-dialog>` 显示大图。图片源地址为 `/static/snapshots/...`。
*   **后端**:
    *   **静态文件挂载**: 必须在 `main.py` 中配置 `app.mount("/static", ...)`，否则前端无法访问磁盘上的图片。
    *   **导出 API**: 
        *   查询数据库 -> 生成 Excel (使用 `pandas` 或 `openpyxl`)。
        *   Excel 中需包含完整的 HTTP 链接 (如 `http://192.168.1.10:8000/static/...`)，以便用户下载后点击查看。

---

## 4. API 接口定义 (RESTful v1)

所有接口前缀: `/api/v1`

| 方法 | 路径 | 描述 | 请求体/参数 |
| :--- | :--- | :--- | :--- |
| **Auth** | | | |
| POST | `/login` | 获取 Token | `{"username": "admin", "password": "..."}` |
| **Cameras** | | | |
| GET | `/cameras` | 获取列表 | |
| POST | `/cameras` | 新增摄像头 | `{"name": "...", "url": "..."}` |
| DELETE | `/cameras/{id}` | 删除摄像头 | |
| **Models** | | | |
| GET | `/models` | 获取模型配置 | |
| POST | `/models` | 新增配置 | `{"base_url": "...", "api_key": "..."}` |
| **Tasks** | | | |
| GET | `/tasks` | 获取任务列表 | |
| POST | `/tasks` | 创建任务 | `{"camera_ids": [1,2], "prompt": "...", "cron": "*/5..."}` |
| POST | `/tasks/{id}/toggle`| 启停任务 | `{"running": true}` |
| **Logs** | | | |
| GET | `/logs` | 获取日志 | `?page=1&only_alarm=true` |
| GET | `/logs/export` | 导出Excel | |

---

## 5. 开发环境准备 (uv 指南)

为了确保依赖环境一致，请严格执行以下命令。

### 5.1 初始化
```bash
# 1. 创建虚拟环境 (速度极快)
uv venv --python python@3.13 .venv 

# 2. 激活环境
# Windows
.venv\Scripts\activate
# Linux/Mac
source .venv/bin/activate
```

### 5.2 安装依赖
创建 `backend/requirements.txt`:
```text
fastapi==0.109.0
uvicorn[standard]==0.27.0
sqlalchemy==2.0.25
pymysql==1.1.0
opencv-python-headless==4.9.0.80
apscheduler==3.10.4
openai==1.10.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
openpyxl==3.1.2
websockets==12.0
```

执行安装:
```bash
uv pip install -r backend/requirements.txt
```

---

## 6. 部署与交付逻辑

这是实现 **"后端启动前端"** 的最终步骤。

1.  **前端打包**:
    在 `frontend/` 目录下运行 `npm run build`。
    配置 `vite.config.ts` 将 `outDir` 设置为 `../backend/dist`。
    
2.  **后端静态托管**:
    在 `backend/app/main.py` 底部添加：
    ```python
    # 必须放在所有 API 路由之后
    
    # 1. 挂载 Vue 的静态资源 (js/css/img)
    app.mount("/assets", StaticFiles(directory="../backend/dist/assets"), name="assets")
    
    # 2. 挂载报警截图 (数据)
    app.mount("/static", StaticFiles(directory="backend/app/static"), name="static")

    # 3. Catch-All 路由 (SPA 核心)
    @app.get("/{full_path:path}")
    async def catch_all(full_path: str):
        # 允许直接访问 dist 根目录文件 (如 favicon.ico)
        dist_path = "../backend/dist"
        file_path = os.path.join(dist_path, full_path)
        if os.path.exists(file_path) and os.path.isfile(file_path):
            return FileResponse(file_path)
        # 否则一律返回 index.html
        return FileResponse(os.path.join(dist_path, "index.html"))
    ```

3.  **启动命令**:
    ```bash
    uvicorn backend.app.main:app --host 0.0.0.0 --port 8000
    ```
    此时，访问 `http://localhost:8000` 即可看到完整的 Web 平台。
