# HANDOFF — 服务监测模块（Service Monitor）会话交接

> 最后更新：2026-07-16 14:20 ｜ 分支：troubleshoot-ai-assistant ｜ 提交：`c552a6b3`
> 状态：**代码全部完成，218 测试全绿，已提交 git，** ⏳ **待部署到 5.60**

---

## 1. 模块概览

| 项目 | 描述 |
|------|------|
| **模块 ID** | `service-monitor` |
| **首页 URL** | `/service-monitor` |
| **API 前缀** | `/api/service-monitor/*` |
| **目录** | `skill/code/web/service_monitor/`（自包含子包） |
| **设计思想** | 与「问题排查助手」**完全物理隔离**，不共用 `routes/`/`services/`/`utils/`/`config.json`/`container.py`，仅通过 `server.py` 一行注册 Blueprint 接入平台 |
| **本期范围** | 核心子集：system(01/02/03/04) + Docker-basic + MySQL-basic + Redis-basic |
| **执行模式** | 本机（subprocess 调 bash）+ 远程（paramiko SSH） |
| **报告套件** | 快速巡检（5 模块） / 全量巡检（7 模块） |

---

## 2. 完成的功能

### 2.1 监测目标管理
- ✅ 内置本地目标（不可删）
- ✅ 远程目标 CRUD（SSH 地址/端口/用户名/密码，密码 Fernet 加密存储）
- ✅ 连通性测试（保存前测试 SSH 是否可达）
- ✅ 目标级配置覆盖（容器名模式、凭据、阈值）

### 2.2 巡检执行
- ✅ 快速巡检（`quick`）：01/02/03/04 + Docker-basic
- ✅ 全量巡检（`full`）：再加 MySQL-basic + Redis-basic
- ✅ 本机执行：直接 subprocess bash
- ✅ 远程执行：paramiko SFTP 上传模块 → SSH 执行
- ✅ SSE 实时推送进度（模块逐项亮起，进度条，汇总）
- ✅ 可取消（Event 标志位）
- ✅ 配置按目标渲染（容器名覆盖 + 凭据注入 + 阈值覆盖）

### 2.3 巡检报告
- ✅ 报告存储：`service_monitor/data/reports/{id}.json`
- ✅ 汇总卡片（总项数 / 正常 / 警告 / 严重）
- ✅ 异常项置顶显示（红/黄色标识 + 模块来源）
- ✅ 分模块折叠明细
- ✅ 导出 Markdown
- ✅ 导出 JSON
- ✅ 报告列表（按时间倒序，支持按目标过滤）
- ✅ 报告历史默认保留 14 天（可配置，`cleanup_expired()`）

### 2.4 角色权限
- ✅ 管理员：目标 CRUD / 触发巡检 / 取消 / 删除报告 / 导出 / 修复预留
- ✅ 普通用户：仅查看（目标列表只读、报告详情、导出），操作按钮隐藏

### 2.5 修复预留
- ✅ 报告中异常项旁的「修复」按钮调用 `/api/service-monitor/fix`，返回「修复能力开发中」

---

## 3. 架构与目录结构

```
skill/code/web/service_monitor/       # ★ 自包含子包
├── __init__.py                       #   from .routes import bp
├── routes.py                         #   4 页面 + 12 API 路由
├── services/                         #   业务层（均用相对导入）
│   ├── __init__.py
│   ├── target_service.py            #   目标 CRUD + 解密凭据 + 执行器工厂
│   ├── runner_service.py            #   巡检编排（SSE 事件生成器）+ 取消
│   └── report_service.py            #   报告 CURD + 过期清理 + MD/JSON 导出
├── utils/                            #   工具层（均用相对导入）
│   ├── __init__.py
│   ├── paths.py                     #   私有路径常量（MODULE_DIR/ASSETS_DIR/DATA_DIR/…）
│   ├── crypto.py                    #   Fernet 加密/解密（MONITOR_ENC_KEY → SECRET_KEY → 兜底）
│   ├── check_modules.py             #   模块清单 + get_suite() + render_config()(shlex.quote)
│   ├── parser.py                    #   KEY:VALUE 解析（过滤脏行 + 状态判定优先 *_LEVEL）
│   ├── thresholds.py                #   阈值表 + judge_status()（数值比较，分级严重/警告）
│   ├── display_names.py             #   KEY → 中文显示名映射
│   └── executor.py                  #   BaseExecutor / LocalExecutor / SSHExecutor(paramiko)
├── assets/                           #   bash 检测模块（静态资源）
│   ├── config.sh.template           #   模板（容器名模式 + 凭据占位符 + 阈值默认值）
│   ├── common.sh                    #   通用函数库（log_info/output_result/resolve_container 等）
│   ├── system/
│   │   ├── 01_system_basic.sh      #   系统基础信息
│   │   ├── 02_cpu_check.sh         #   CPU 检测
│   │   ├── 03_memory_check.sh      #   内存检测
│   │   └── 04_disk_check.sh        #   磁盘检测
│   └── service/
│       ├── 20_docker_basic.sh      #   Docker 基础
│       ├── 22_mysql_basic.sh       #   MySQL 基础
│       └── 24_redis_basic.sh       #   Redis 基础
├── data/                             #   运行时数据（.gitignore 忽略，不提交）
│   ├── .gitkeep
│   ├── targets.json                 #   监测目标列表（密码加密存储）
│   └── reports/                     #   巡检报告 JSON 归档
└── tests/                            #   模块私有测试（conftest 独立，tmp 隔离数据）
    ├── __init__.py & conftest.py
    ├── test_parser.py               #   7 用例
    ├── test_crypto.py               #   5 用例
    ├── test_check_modules.py        #   7 用例
    ├── test_target_service.py       #   8 用例
    ├── test_routes_sm.py            #   18 用例（含 routing permission）
    └── test_report_service.py       #   6 用例

skill/code/web/templates/service_monitor/   # 模板（独立目录，不与 index.html 冲突）
├── index.html                       #   主页：目标卡片网格 + 报告列表
├── targets.html                     #   目标管理：表格 + 弹窗 CRUD + 连通性测试
├── run.html                         #   巡检执行：进度条 + SSE 模块清单
└── report.html                      #   报告详情：汇总 + 异常置顶 + 折叠明细 + 导出 + 修复
```

---

## 4. 路由与 API 清单

### 页面路由

| 方法 | 路径 | 权限 | 功能 |
|------|------|------|------|
| GET | `/service-monitor` | 登录 | 监测主页 |
| GET | `/service-monitor/targets` | 管理员 | 目标管理页 |
| GET | `/service-monitor/run/<target_id>` | 管理员 | 巡检执行页 |
| GET | `/service-monitor/report/<report_id>` | 登录 | 报告详情页 |

### API

| 方法 | 路径 | 权限 | 功能 |
|------|------|------|------|
| GET | `/api/service-monitor/targets` | 登录 | 目标列表 |
| POST | `/api/service-monitor/targets` | 管理员 | 新增目标 |
| PUT | `/api/service-monitor/targets/<tid>` | 管理员 | 更新目标 |
| DELETE | `/api/service-monitor/targets/<tid>` | 管理员 | 删除目标 |
| POST | `/api/service-monitor/targets/test` | 管理员 | 连通性测试 |
| GET | `/api/service-monitor/run/stream` | 管理员 | SSE 巡检流 |
| POST | `/api/service-monitor/run/<run_id>/cancel` | 管理员 | 取消巡检 |
| GET | `/api/service-monitor/reports` | 登录 | 报告列表 |
| GET | `/api/service-monitor/reports/<rid>` | 登录 | 报告详情 |
| DELETE | `/api/service-monitor/reports/<rid>` | 管理员 | 删除报告 |
| GET | `/api/service-monitor/reports/<rid>/export` | 登录 | 导出 MD/JSON |
| POST | `/api/service-monitor/fix` | 管理员 | 修复预留 |

---

## 5. 关键设计决策

| 决策 | 选择 | 原因 |
|------|------|------|
| 代码隔离 | 自包含子包 `service_monitor/` | 不与问题排查助手代码纠缠，删除模块只需删一个目录 |
| 执行器 | LocalExecutor(subprocess) / SSHExecutor(paramiko) | paramiko 纯 Python，无外部二进制依赖 |
| 凭据存储 | Fernet 加密 | SSH 密码禁止明文落盘 |
| 密钥来源 | MONITOR_ENC_KEY > 派生自 SECRET_KEY > 兜底 | 灵活，生产推荐独立设置 |
| 容器名匹配 | 模糊匹配（`grep -iE`） | 不同服务器容器名不一致 |
| config 渲染 | shlex.quote 防注入 | 密码含特殊字符时不破坏 shell 语法 |
| 状态判定 | 优先模块自带 *_LEVEL | 模块内判定最准确，阈值表为兜底 |
| 进度反馈 | SSE（text/event-stream） | 已有 SSE 经验，无需 WebSocket |
| 数据存储 | JSON 文件 | 与项目现有风格一致（users.json），避免 SQLite 依赖 |
| 报告保留 | 14 天可配置 | 通过 cleanup_expired() 定时或手动清理 |

---

## 6. 关键执行链路（本地巡检）

```
用户点「快速巡检」
  → GET /service-monitor/run/local?suite=quick
    → 页面加载空白进度模板 + JS 开 EventSource
  → GET /api/service-monitor/run/stream?target_id=local&suite=quick
    → runner_service.run_inspection("local", "quick"):
      1. target_service.get_target("local")     → 取本地目标
      2. check_modules.get_suite("quick")        → 5 个模块列表
      3. render_config(target)                   → 渲染 config.sh 文本
      4. LocalExecutor.setup()                   → mktemp + 复制 assets
      5. for 每个模块:
           executor.run_module(module, config)   → bash -c 'cd <wd> && …/module.sh'
           parser.parse_module_output(raw)       → KEY:VALUE → CheckItem[]
           yield SSE event("module_done", ...)
      6. report_service.save(...)                → 报告落盘
      7. yield SSE event("finished", report_id)
  → 前端 progress 模块亮起 → 1.5s 自动跳 `/service-monitor/report/<id>`
```

---

## 7. 踩过的坑 & 注意事项

### 🚨 坑 1：模块头部 LIB_DIR 写死覆盖
- **现象**：原脚本 `LIB_DIR="/tmp/check_modules"` 位于模块头部，`source common.sh` 之前，改为 `${LIB_DIR:-/tmp/check_modules}` 才能让 executor 传入的工作目录生效
- **涉及**：7 个 .sh 文件均已改

### 🚨 坑 2：common.sh 加载路径
- `common.sh` 会 `source $LIB_DIR/lib/config.sh`，所以 executor 必须把 assets 布置为 `<workdir>/lib/{config.sh, common.sh, system/, service/}` 结构，不能平铺

### 🚨 坑 3：config 模板凭据占位符不加引号
- 模板写 `MYSQL_PASSWORD=__MYSQL_PASSWORD__`（无引号），渲染时 `shlex.quote` 自动按需加单引号。若模板自带 `"__var__"` 会导致 `shlex.quote("'value'")` 产生错误嵌套

### 🚨 坑 4：测试 fixture 不要共用一个 client 实例
- Flask test_client 的 session 是绑在 client 对象上的，`user` 和 `admin` 如果用同一个 `client` 注入不同 session，会互相覆盖。必须各自 `app.test_client()` 独立实例

### 🚨 坑 5：Docker basic 的逐容器精确匹配
- `20_docker_basic.sh` 的 `check_container_status` 用 `grep -q "^${name}$"` 精确匹配。如果是 config 里是模式（如 `mysql`），跑在容器名 `umysql` 的服务器上匹配不到。**本期可接受**（Docker basic 还有 service 状态/资源/日志等通用项），后续若需逐容器检测，应改精确匹配为 `resolve_container` 函数

### 🚨 坑 6：旧占位文件冲突（已解决）
- 平台化创建的 `routes/service_monitor.py` 和 `templates/service_monitor.html` 与新子包 bp 端点名冲突。已删除

### ⚠️ 注意 7：部署前服务器需装依赖
- `cryptography` 可能需 libffi-dev 等 C 扩展；`paramiko` 服务器已有（deploy 脚本用了），但需确认版本兼容

### ⚠️ 注意 8：运行时 data 自动创建
- `service_monitor/utils/paths.ensure_dirs()` 在首次访问时自动 `mkdir -p data/` 和 `data/reports/`，无需手动创建

---

## 8. 当前待办：部署到 5.60

```
# 1. 服务器安装依赖（cryptography 确保装好）
ssh ubains@192.168.5.60 "pip install paramiko cryptography"

# 2. 上传代码（用 ! 前缀在会话内执行）
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py

# 3. 验证部署
cd deploy && python verify_deployment.py

# 4. 手动验证
#   浏览器访问 http://192.168.5.60:8088/service-monitor
#   管理员账号登录 → 看到本机目标卡片 → 点快速巡检
#   → SSE 进度条走完 → 自动跳报告 → 看异常项 + 导出 MD
```

### 部署涉及文件清单

上传脚本已增量修改，**新增 `RECURSIVE_DIRS_TO_UPLOAD`** 递归上传 service_monitor 子包（排除 tests/data/__pycache__）：

```
# 自动上传（含递归）：
service_monitor/               → /opt/troubleshoot/web/service_monitor/
templates/service_monitor/     → /opt/troubleshoot/web/templates/service_monitor/

# 修改的全局文件自动覆盖：
server.py                     → 已从 service_monitor import（占位时改好）
.gitignore                    → 本地不影响
CLAUDE.md                     → 本地不影响
```

---

## 9. 备忘：模块健康检查

部署后通过以下几点确认模块正常工作：

1. **Blueprint 注册成功**：访问 `/service-monitor` 返回 200（非 404）
2. **目标自动创建**：能列出本机目标（`/api/service-monitor/targets` 返回含 `local` 项）
3. **本地巡检全链路**：SSE 流走完，报告落盘
4. **权限隔离**：普通用户看不到操作按钮，API 返回 403
5. **修复预留**：管理员点修复按钮弹出「开发中」
6. **导出**：MD/JSON 下载正常

---

## 10. 相关文档

| 文件 | 说明 |
|------|------|
| `Docs/需求文档/服务监测/PRD_需求文档_服务监测模块.md` | 需求定义（已确认 Q1-Q10） |
| `Docs/需求文档/服务监测/PRD_计划执行_服务监测模块.md` | 实现计划（7 阶段，已全部完成） |
| `Docs/需求文档/服务监测/HANDOFF.md` | 本文件，服务监测会话交接 |
| `临时目录/服务器监测/` | 参考脚本（bash 模块 + 架构 v4.0） |
