# P0 功能实施计划

> 计划日期：2026-05-06
> 基于：产品化评估与规划.md 中的 P0 优先级

---

## 总览

| 功能 | 依赖链 | 工作量估计 |
|------|--------|-----------|
| 告警中心 | media-server → agent → 后端 | 最大 |
| 人脸库管理 | agent → 后端 | 中 |
| 视频监控 | agent → 后端 | 中 |
| Dashboard 改造 | 后端（依赖以上三项） | 小 |

---

## 一、告警中心（最大的缺口）

### 1.1 当前问题

media-server 的 alarm 节点触发告警后，直接发送给第三方（HTTP 回调、MinIO 等），**不在本地保存告警记录**。agent 无法查询历史告警，后端自然也无法展示。

### 1.2 架构设计

```
media-server alarm 节点触发
    ├── 截图/视频片段上传 MinIO（已有）
    ├── HTTP 回调通知第三方（已有）
    └── 写入本地告警日志（新增）──→ agent 查询 ──→ 后端收集展示
```

### 1.3 告警记录格式

```json
{
  "id": "alarm_20260506_143021_a1b2c3",
  "timestamp": "2026-05-06T14:30:21+08:00",
  "device_id": "d12a4719c91641df",
  "channel": "cam1",
  "rule_name": "intrusion_zone_a",
  "rule_type": "intrusion",
  "object_label": "person",
  "confidence": 0.87,
  "snapshot_url": "http://minio:9000/alarms/xxx.jpg",
  "clip_url": "http://minio:9000/clips/xxx.mp4",
  "duration_ms": 3500
}
```

### 1.4 任务分解

#### Task A1：media-server 增加本地告警存储（C++）

**文件**：`plugins/alarm/alarm_node.cpp`

在 alarm 节点触发告警动作时，追加写入本地 SQLite 数据库：

```
表结构：
CREATE TABLE alarm_records (
  id TEXT PRIMARY KEY,
  timestamp TEXT NOT NULL,
  channel TEXT NOT NULL,
  rule_name TEXT NOT NULL,
  rule_type TEXT NOT NULL,
  object_label TEXT,
  confidence REAL,
  snapshot_url TEXT,
  clip_url TEXT,
  duration_ms INTEGER,
  created_at TEXT NOT NULL
);
```

位置：`${modelsDir}/../resources/alarms/alarm_records.db`

#### Task A2：agent 增加告警查询端点（Go）

**文件**：`agent/internal/httpapi/alarms.go`（新建）

```
GET /v1/alarms/recent?limit=50&since=2026-05-06T00:00:00
```

返回最近 N 条告警记录。可选时间范围过滤。

#### Task A3：后端定时收集设备告警（Go）

**文件**：`internal/service/alarm_collector.go`（新建）

- RegistryService 增加 goroutine，每 10 秒遍历在线设备
- 对每个设备调用 `agent.Do("GET", dev.IP, dev.AgentPort, "/v1/alarms/recent?limit=100", nil)`
- 新告警写入后端 SQLite（`alarm_records` 表）
- 前端 SSE 推送新告警

**数据库**：`internal/storage/migrate.go` 新增表

```sql
CREATE TABLE IF NOT EXISTS alarm_records (
  id TEXT PRIMARY KEY,
  device_id TEXT NOT NULL,
  channel TEXT NOT NULL,
  timestamp TEXT NOT NULL,
  rule_name TEXT NOT NULL,
  rule_type TEXT NOT NULL,
  object_label TEXT,
  confidence REAL,
  snapshot_url TEXT,
  clip_url TEXT,
  duration_ms INTEGER,
  collected_at TEXT NOT NULL
);
```

#### Task A4：告警中心页面（Go + HTML）

**路由**：`GET /ui/alarms`

**布局**：
- 上半部分：告警筛选（设备/通道/规则类型/时间范围）
- 下半部分：告警列表（时间、设备、通道、规则、对象标签、截图缩略图、视频链接）
- 点击进入告警详情（大图、视频片段播放）

#### Task A5：Dashboard 告警信息流（HTML + JS）

在 Dashboard 顶部增加一个告警信息流卡片，SSE 实时推送最近 5 条告警。

---

## 二、人脸库管理

### 2.1 当前问题

只能整文件上传/替换 `.db`，无法增删改单个人物。

### 2.2 架构设计

```
后端人脸库页面 ──CRUD──▶ agent /v1/face-gallery/persons/* ──▶ media-server face_gallery.db
```

### 2.3 任务分解

#### Task B1：agent 增加人脸库 CRUD API（Go）

**文件**：`agent/internal/httpapi/face_gallery.go`（修改）

新增端点：
- `GET /v1/face-gallery/persons` — 返回人物列表 {id, name, photo_count, created_at}
- `POST /v1/face-gallery/persons` — 新增人物 {name, photo_bytes}，agent 调用 media-server 接口提取 embedding 并写入 DB
- `DELETE /v1/face-gallery/persons/{id}` — 删除人物及其 embeddings
- `PUT /v1/face-gallery/persons/{id}/name` — 修改人物姓名
- `POST /v1/face-gallery/persons/{id}/photo` — 追加人脸照片，提取 embedding

#### Task B2：后端人脸库管理页面（Go + HTML）

**路由**：`GET /ui/face-gallery`

**布局**：
- 人员列表（姓名、照片缩略图、注册时间、操作按钮）
- 搜索框（按姓名搜索）
- 新增按钮 → 弹出表单（姓名 + 上传照片）
- 编辑/删除确认

---

## 三、视频监控

### 3.1 当前问题

后台看不到摄像头画面。只能通过 `http://设备IP:9000/hls_player.html` 在设备上查看。

### 3.2 架构设计

```
后端视频监控页 ──iframe──▶ agent 返回 HLS 播放页面 URL
                └──img──▶ agent /v1/preview/snapshot?channel=X 返回 JPEG
```

### 3.3 任务分解

#### Task C1：agent 增加预览端点（Go）

**文件**：`agent/internal/httpapi/preview.go`（新建）

- `GET /v1/preview/channels` — 返回各通道信息 {name, hls_url, rtsp_url, resolution, fps}
- `GET /v1/preview/snapshot/:channel` — 从 media-server 请求当前帧，返回 JPEG

HLS URL 格式：代理转发到 media-server 的 `/hls/{channel}/index.m3u8`

#### Task C2：后端视频监控页面（Go + HTML）

**路由**：`GET /ui/monitor`

**布局**：
- 左侧：设备通道列表（可多选）
- 右侧：多路画面网格（4/9/16 宫格）
- 每个画面：HLS 播放器 iframe + 通道名称标签
- 单击画面可全屏

---

## 四、Dashboard 改造

### 4.1 当前问题

只有静态统计（设备数、任务数），缺乏实时信息。

### 4.2 目标布局

```
┌─────────────────────────────────────┐
│  实时告警（5条滚动）                │ ← 告警中心对接
├──────────┬──────────┬───────────────┤
│ 设备状态  │ NPU/CPU  │ 今日告警统计  │ ← agent metrics
│ 在线 3/3 │ ████░░░░ │ 告警: 12     │
├──────────┴──────────┴───────────────┤
│  关键通道预览（1-4路 snapshot）      │ ← 视频监控对接
├─────────────────────────────────────┤
│  最近任务                           │ ← 已有
└─────────────────────────────────────┘
```

### 4.3 任务分解

#### Task D1：Dashboard 实时数据 API（Go）

**文件**：`internal/web/ui.go`（修改 `pageDashboard`）

```go
func (u *UI) pageDashboard(w http.ResponseWriter, r *http.Request) {
    u.ensureDevicesLoaded()
    data := ...现有逻辑...
    
    // 新增：最近告警
    data.RecentAlarms = alarmService.GetRecent(5)
    
    // 新增：设备 metrics（聚合）
    data.DeviceMetrics = collectDeviceMetrics(u.agent, u.registry)
    
    u.render(w, r, "dashboard", data)
}
```

#### Task D2：Dashboard 模板改造（HTML）

**文件**：`internal/web/ui/templates/dashboard.html`

- 告警信息流（SSE 实时更新）
- 设备状态卡片（在线数、CPU、内存）
- 快照预览（定时刷新 img src）
- 最近任务（已有，保留）

---

## 五、实施顺序

```
第一阶段（打通告警链路）：
  Task A1（media-server）→ Task A2（agent）→ Task A3（后端收集）→ Task A4（告警页面）

第二阶段（人脸库 + 视频）：
  Task B1 + C1（agent）→ Task B2 + C2（后端页面）

第三阶段（Dashboard整合）：
  Task A5 + D1 + D2（Dashboard 改造）
```

---

## 六、涉及文件清单

| 项目 | 文件 | 操作 |
|------|------|------|
| media-server | `plugins/alarm/alarm_node.cpp` | 修改：追加本地告警存储 |
| agent | `internal/httpapi/alarms.go` | 新建：告警查询端点 |
| agent | `internal/httpapi/face_gallery.go` | 修改：人脸库 CRUD |
| agent | `internal/httpapi/preview.go` | 新建：视频预览端点 |
| agent | `internal/httpapi/server.go` | 修改：注册新路由 |
| 后端 | `internal/service/alarm_collector.go` | 新建：告警收集服务 |
| 后端 | `internal/storage/migrate.go` | 修改：alarm_records 表 |
| 后端 | `internal/web/ui/templates/alarms.html` | 新建：告警中心页面 |
| 后端 | `internal/web/ui/templates/face_gallery.html` | 新建：人脸库管理页面 |
| 后端 | `internal/web/ui/templates/monitor.html` | 新建：视频监控页面 |
| 后端 | `internal/web/ui/templates/dashboard.html` | 修改：实时化改造 |
| 后端 | `internal/web/ui.go` | 修改：新页面 handler + 路由 |
| 后端 | `cmd/safesightd/main.go` | 修改：注入新服务 |
