# PRD 需求文档：异常项趋势告警

> 模块：服务监测（service_monitor）
> 优先级：P1
> 创建日期：2026-07-23
> 状态：待开发

---

## 1. 背景与目标

### 1.1 背景

服务监测模块已实现巡检报告生成和通知推送功能。当前通知仅在巡检完成时触发一次，无法识别"同一检测项持续异常"的趋势。运维人员需要及时发现持续恶化的检测项，避免问题长期未处理。

### 1.2 目标

实现异常项趋势告警功能：
- 同一检测项连续 N 次异常时自动发送告警通知
- 支持配置连续次数阈值、告警级别、通知渠道
- 告警消息包含异常趋势信息

---

## 2. 功能需求

### 2.1 告警配置

**配置位置**：通知配置页（`notification.html`）

**新增配置项**：

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| 启用连续异常告警 | 开关 | 关闭 | 总开关 |
| 连续次数阈值 | 数字 | 3 | 连续 N 次异常触发告警 |
| 告警级别 | 选择 | 严重 | 仅严重 / 警告+严重 |
| 通知渠道 | 多选 | 复用现有 | 邮件/钉钉/企微（复用现有通知配置） |

**存储方式**：
- 扩展现有 `notification_config` 字段，新增 `alert_on_consecutive` 子对象
- 存储在 `targets.json` 的目标配置中

### 2.2 告警检测逻辑

**触发时机**：每次巡检报告完成后

**检测流程**：
1. 获取当前报告的所有异常项（按告警级别过滤）
2. 查询该目标最近 N-1 份报告（N = 配置的连续次数阈值）
3. 对每个异常项，检查是否在最近 N 份报告中连续出现
4. 连续 N 次异常的检测项触发告警

**检测函数**：

```python
def check_consecutive_abnormal(target_id: str, current_report: dict) -> list:
    """
    检查连续异常检测项
    
    Args:
        target_id: 目标ID
        current_report: 当前报告数据
    
    Returns:
        连续异常检测项列表 [
            {
                "key": "CPU_USAGE",
                "name": "CPU使用率",
                "consecutive_count": 3,
                "first_seen": "2026-07-21 10:00:00",
                "last_value": "78%",
                "last_status": "warning"
            }
        ]
    """
    pass
```

### 2.3 告警消息格式

**消息模板**：

```
⚠️ 【连续异常告警】{目标名称}

检测项"{检测项名称}"连续 {N} 次异常，请关注！

- 检测项：{KEY} ({显示名称})
- 首次异常：{首次时间}
- 最近值：{当前值}
- 当前状态：{状态}
- 连续异常次数：{N}

查看详情：{报告链接}
```

**钉钉/企微消息格式**（Markdown）：

```markdown
### ⚠️ 连续异常告警

**目标**：{目标名称}
**检测项**：{KEY} ({显示名称})
**连续异常**：{N} 次
**当前值**：{当前值}
**首次异常**：{首次时间}

[查看详情]({报告链接})
```

### 2.4 告警去重

**问题**：同一检测项连续 4、5、6...次异常时，不应每次都发告警

**去重策略**：
- 同一目标 + 同一检测项，在告警触发后设置冷却期
- 冷却期 = 连续次数阈值 × 巡检间隔（默认）
- 冷却期内不重复告警
- 冷却期过后如果仍然连续异常，再次告警

**实现方式**：
- 内存字典 `_alert_cooldown`：`{(target_id, item_key): last_alert_time}`
- 每次告警前检查冷却期

---

## 3. API 设计

### 3.1 更新通知配置

复用现有通知配置 API，扩展 `alert_on_consecutive` 字段：

```
PUT /api/service-monitor/targets/{target_id}/notification
```

**请求体新增字段**：

```json
{
  "alert_on_consecutive": {
    "enabled": true,
    "consecutive_threshold": 3,
    "alert_level": "critical",
    "channels": ["dingtalk", "email"]
  }
}
```

### 3.2 查询连续异常状态（可选）

```
GET /api/service-monitor/targets/{target_id}/consecutive-abnormal
```

**响应**：

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "key": "CPU_USAGE",
        "name": "CPU使用率",
        "consecutive_count": 3,
        "first_seen": "2026-07-21 10:00:00",
        "last_value": "78%",
        "last_status": "warning"
      }
    ]
  }
}
```

---

## 4. 前端设计

### 4.1 通知配置页修改

**位置**：`templates/service_monitor/notification.html`

**新增区域**：在现有通知配置下方新增"连续异常告警"配置区

```
┌─────────────────────────────────────────────────────────────┐
│ 连续异常告警                                                 │
├─────────────────────────────────────────────────────────────┤
│ 启用连续异常告警  [Toggle 开关]                              │
│                                                             │
│ 连续次数阈值  [3] 次                                        │
│ 告警级别      [仅严重 ▼]                                    │
│ 通知渠道      [☑ 钉钉  ☑ 邮件  ☐ 企微]                     │
└─────────────────────────────────────────────────────────────┘
```

**交互**：
- 开关关闭时，下方配置项灰显灰不可编辑
- 开关打开时，配置项可编辑
- 保存时与现有通知配置一起提交

---

## 5. 技术方案

### 5.1 后端实现

**修改文件**：
- `skill/code/web/service_monitor/services/report_service.py`：新增 `_check_consecutive_abnormal()` 函数，在报告完成回调中调用
- `skill/code/web/service_monitor/services/runner_service.py`：报告完成时触发告警检查
- `skill/code/web/service_monitor/routes.py`：新增查询连续异常状态 API（可选）

**新增逻辑**：

```python
# report_service.py 新增

_ALERT_COOLDOWN = {}  # {(target_id, item_key): last_alert_time}

def check_consecutive_abnormal(target_id: str, current_report: dict) -> list:
    """检查连续异常检测项，返回需告警列表"""
    config = _get_alert_config(target_id)
    if not config or not config.get('enabled'):
        return []
    
    threshold = config.get('consecutive_threshold', 3)
    alert_level = config.get('alert_level', 'critical')
    
    # 获取当前报告异常项
    abnormal_items = _get_abnormal_items(current_report, alert_level)
    
    # 查询最近 N-1 份报告
    recent_reports = _get_recent_reports(target_id, threshold - 1)
    
    # 检查连续性
    alert_items = []
    for item in abnormal_items:
        if _is_consecutive(item['key'], recent_reports, threshold):
            # 检查冷却期
            if not _in_cooldown(target_id, item['key']):
                alert_items.append(item)
                _set_cooldown(target_id, item['key'])
    
    return alert_items
```

### 5.2 通知发送

复用现有通知机制：
- `runner_service.py` 已有 `_send_notification()` 函数
- 新增告警消息模板，调用现有发送函数

---

## 6. 验收标准

### 6.1 功能验收

- [ ] 通知配置页新增"连续异常告警"配置区
- [ ] 开关控制启用/禁用
- [ ] 连续次数阈值可配置
- [ ] 告警级别可配置（仅严重 / 警告+严重）
- [ ] 通知渠道复用现有配置
- [ ] 巡检完成后自动检测连续异常
- [ ] 连续 N 次异常触发告警通知
- [ ] 告警消息格式正确
- [ ] 冷却期内不重复告警

### 6.2 边界情况

- [ ] 目标无历史报告时不触发告警
- [ ] 历史报告不足 N 份时不触发告警
- [ ] 告警配置未启用时不检测
- [ ] 服务重启后冷却期重置（内存字典，可接受）

### 6.3 测试覆盖

- [ ] 单元测试：`test_consecutive_alert.py`（新增）
- [ ] 覆盖率：告警逻辑 > 80%

---

## 7. 时间规划

| 阶段 | 任务 | 预估时间 |
|------|------|---------|
| 阶段一 | 后端告警检测逻辑 | 2h |
| 阶段二 | 通知发送集成 | 1h |
| 阶段三 | 前端配置页 | 1h |
| 阶段四 | 单元测试 | 1h |
| 阶段五 | 集成测试 + 部署 | 1h |
| **合计** | | **6h** |

---

## 8. 相关文档

- `HANDOFF_服务监测.md`：模块交接文档
- `PRD_需求文档_通知配置.md`：通知配置需求
