提交 06369d48 authored 作者: 陈泽健's avatar 陈泽健

docs(monitor): HANDOFF 精简为运维速查并归档完整历史

- HANDOFF.md 由 677 行精简为 146 行:保留运维拓扑/部署方式/踩坑清单/进度速览/遗留清单/下一步
- 新增 HANDOFF_归档_2026-09-01.md 存档完整历史会话进度(§8-§14 可按会话编号检索)
- 记录 5.202 定时任务 end_date 过期修复与部署(7249d355 + 运维延长 2027-08-31)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 7249d355
# HANDOFF — 服务监测模块实施进度 # HANDOFF — 服务监测模块实施进度
> 最后更新:2026-08-31 | 分支:troubleshoot-ai-assistant | 模块:service-monitor > 最后更新:2026-09-01 | 分支:troubleshoot-ai-assistant | 模块:service-monitor
> 状态:**报告链接免登访问已修复并部署复测通过(07d17632)+ token 限制评估结论:维持 7 天有效期不加次数 + 临时探针脚本已清理(未提交)** > 状态:**5.202 定时任务 end_date 过期问题已修复并部署 5.60(7249d355);5.202 end_date 已延长至 2027-08-31 恢复巡检**
> 📦 完整历史会话进度见归档:`Docs/需求文档/服务监测/HANDOFF_归档_2026-09-01.md`
> (本文仅保留运维必需信息 + 当前状态;历史根因/部署细节已在归档中按会话编号可查)
--- ---
## 1. 我们在做什么 ## 1. 我们在做什么
为运维平台落地「服务监测」模块:监控本机/远程服务器系统资源与服务状态,输出巡检报告。 为运维平台落地「服务监测」模块:监控本机/远程服务器系统资源与服务状态,输出巡检报告。**只做监测不做修复**(修复接口预留)。
**只做监测不做修复**(修复接口预留,点击返回"开发中")。
需求与计划文档: 需求与计划文档:
- `Docs/需求文档/服务监测/PRD_需求文档_服务监测模块.md` - `Docs/需求文档/服务监测/PRD_需求文档_服务监测模块.md`
- `Docs/需求文档/服务监测/PRD_计划执行_服务监测模块.md` - `Docs/需求文档/服务监测/PRD_计划执行_服务监测模块.md`
### 关键决策(已与用户对齐) ### 关键决策(已与用户对齐)
- 代码**完全隔离**:所有代码进 `skill/code/web/service_monitor/` 子包,不动 `routes/`/`services/`/`utils/`/`config.json`/`container.py`,对外仅 `server.py` 一行注册 - 代码**完全隔离**:进 `skill/code/web/service_monitor/` 子包,不动 `routes/`/`services/`/`utils/`/`config.json`/`container.py`,对外仅 `server.py` 一行注册
- 本期**核心子集**:system(01/02/03/04) + Docker-basic + MySQL-basic + Redis-basic - 核心子集:system(01/02/03/04) + Docker-basic + MySQL-basic + Redis-basic
- bash 模块**搬进项目**当静态资源 + config 外置模板 - bash 模块搬进项目当静态资源 + config 外置模板;容器名**模糊匹配**;SSH 用 paramiko,密码 **Fernet 加密存储**;进度反馈 **SSE**;报告保留 **14 天**
- 容器名**模糊匹配**(不同服务器容器名不一致,用 grep -iE 模式发现) - **快速巡检 / 全量巡检** 两套件;**角色控制**:普通用户仅查看,管理员可操作
- 远程 SSH 用 **paramiko**;SSH 密码 **Fernet 加密存储**
- 进度反馈 **SSE**;报告默认保留 **14 天**可配置
- **快速巡检 / 全量巡检** 两套件
- **角色控制**:普通用户仅查看(隐藏操作入口),管理员可操作
---
## 2. 已完成(全部 7 阶段)
### ✅ 阶段一:子包骨架 + bash 资产搬迁
- 建子包目录 `service_monitor/{__init__,routes,services/,utils/,assets/,data/,tests/}`
- 搬迁核心 bash 模块到 `assets/`:common.sh + config.sh.template + system/{01,02,03,04} + service/{20,22,24}
- 改造模块头部 `LIB_DIR="${LIB_DIR:-/tmp/check_modules}"`(7个模块,支持注入)
- config.sh.template:容器名改匹配模式、密码改占位符、阈值可覆盖、新增 resolve_container
- 端到端本地执行验证通过(KEY:VALUE 输出正常)
### ✅ 阶段二:utils 层
- `paths.py` — 模块私有路径常量 + ensure_dirs()
- `crypto.py` — Fernet 加密/解密(MONITOR_ENC_KEY > 派生自 SECRET_KEY > 兜底)
- `check_modules.py` — CheckModule 清单 + get_suite() + render_config()(shlex.quote 防注入)
- `parser.py` — 移植 Parse-ModuleResult(过滤脏行 + KEY:VALUE 解析 + 状态判定,优先 *_LEVEL)
- `thresholds.py` — 阈值表 + judge_status()(数值/百分比比较,严重/警告分级)
- `display_names.py` — KEY→中文显示名(核心子集)
- `executor.py` — BaseExecutor/LocalExecutor(subprocess)/SSHExecutor(paramiko)
### ✅ 阶段三:services 层
- `target_service.py` — 目标 CRUD + 连通性测试 + make_executor + resolve_credentials(解密)
- `runner_service.py` — 巡检编排(生成器 yield SSE 事件)+ cancel_run + get_run_status
- `report_service.py` — 报告 save/get/list/delete/cleanup_expired/export_md/export_json
### ✅ 阶段四:routes 层 + 接入 + 删旧占位
- `routes.py` — 16 条路由(4页面 + 12API),角色控制完整
- 删除旧占位 `routes/service_monitor.py` + `templates/service_monitor.html`
- `server.py` 已有 `from service_monitor import bp`(之前占位就改好了)
### ✅ 阶段五:前端模板(UI 已与用户确认)
- `index.html` — 目标卡片 + 报告列表(普通用户隐藏操作按钮)
- `targets.html` — 目标 CRUD 弹窗 + 连通性测试
- `run.html` — 进度条 + 模块清单 + SSE 实时反馈 + 完成跳报告
- `report.html` — 汇总卡片 + 异常置顶 + 分模块折叠 + 导出MD/JSON + 修复预留按钮
### ✅ 阶段六:测试
- 模块私有测试 55 用例(parser/crypto/check_modules/target_service/report_service/routes_sm)
- **全套 218 测试全绿**(原 163 + 新增 55,未破坏现有测试)
- `pytest.ini` 新增 testpath `web/service_monitor/tests`
- 子包 conftest 注入 tmp 数据目录隔离
### ✅ 阶段七:部署与依赖
- `requirements.txt` 加 paramiko + cryptography
- `.env.example` 加 MONITOR_ENC_KEY 说明(含生成命令)
- `deploy/upload_to_server.py``RECURSIVE_DIRS_TO_UPLOAD` + `_upload_dir_recursive()`(递归上传 service_monitor,排除 tests/data/__pycache__)
- `.gitignore` 加服务监测运行时数据忽略(targets.json/reports/*.json,保留 .gitkeep)
- `CLAUDE.md` 更新项目结构说明
- 本机 test_client 冒烟全通过(页面200/权限403/API正常)
--- ---
## 3. 待办(部署 ## 2. 已完成(阶段 1-7 代码 + 部署全部落地
代码完成,**尚未部署到 5.60**。部署步骤: | 阶段 | 内容 | 状态 |
|------|------|:----:|
| 1 | 子包骨架 + bash 资产搬迁(common.sh/config 模板/system+service 模块,头部 `${LIB_DIR:-...}` 可注入) | ✅ |
| 2 | utils 层(paths/crypto Fernet/check_modules/parser/thresholds/display_names/executor Local+SSH) | ✅ |
| 3 | services 层(target CRUD+连通性 / runner SSE 编排+cancel / report save/get/list/cleanup/export) | ✅ |
| 4 | routes 层 16 条路由 + 删旧占位 + 角色控制 | ✅ |
| 5 | Flask 模板前端(index/targets/run/report,UI 已确认)→ 已迁 Vue SPA | ✅ |
| 6 | 测试 55 用例 + 全套回归全绿 | ✅ |
| 7 | 部署(paramiko/cryptography 依赖、upload 递归上传、gitignore 运行时数据) | ✅ 已上线 5.60 |
```bash **后续已上线功能**(详见归档 §8-§14 或对应 PRD):
# 1. 服务器需装依赖(paramiko 已在 deploy 用,cryptography 需确认) - 定时任务(APScheduler,北京时区,misfire_grace_time=3600 并发修复)
# 服务器执行:pip install paramiko cryptography - 报告缺失告警(每日 09:30 检测,仅查启用定时任务的目标)、统计/对比
# (或确认 paramiko 已装则 cryptography 作为其依赖已存在) - 钉钉通知(含"完整报告"链接访客免登只读访问,token 7 天有效)
- end_date 过期感知(`is_expired` 派生字段 + 前端"已过期"展示)
# 2. 上传代码(用 ! 前缀在会话内执行,SSH_PASSWORD 环境变量) ---
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py
# 3. 权威验证 ## 3. 生产拓扑与部署(接手者必读)
cd deploy && python verify_deployment.py
# 4. 手动验证服务监测 **生产环境:5.60**
# - 浏览器访问 http://192.168.5.60:8088/service-monitor - 容器 `troubleshoot``troubleshoot:latest`,supervisord 管 nginx:80 + Flask:8088,健康检查 `curl http://localhost/api/health`
# - 本地目标快速巡检 → 看报告 - 端口映射 `8088→80`(nginx);**镜像 build 源:`/data/third_party/monitor-platform/`**(含 `skill/code/web/` + `requirements.txt` + `frontend/dist/`),`docker compose up -d --build` 重建
# - (可选)新增远程目标测连通 + 远程巡检 - volume:monitor-data→`/app/web/service_monitor/data`(schedules.json/targets.json/reports/)、logs→`/app/web/logs`、users.json、前端 dist→`/data/dist`、搜索索引(只读)
```
--- ### 部署方式
| 方式 | 场景 | 步骤 |
|------|------|------|
| **热更新**(改单文件) | 日常小修 | LF 转码 → 同步 build 源(⚠️ 必须,否则下次 `--build` 回滚)→ `docker cp` 注入容器 `/app/web/` → 清 `__pycache__``docker restart troubleshoot` |
| **镜像重建**(改前端/依赖) | 前端 dist / requirements | 本机构建新 dist → 同步 build 源 → `docker compose up -d --build`(不是 restart) |
| 前端 dist | 热更新无需重启 | 新 `dist/``/opt/troubleshoot/dist`(bind volume,nginx root `/data/dist`) |
## 4. 任务进度 **登录与 API**:登录走 `POST /login`**不是** `/api/login`)→ 设置 cookie;服务监测 API 前缀 `/api/service-monitor/`(schedules/targets/reports)。
| # | 阶段 | 状态 | ### 复验要点
|---|------|------| - 部署成功看 `docker ps` 状态 **healthy** + `curl -sf http://localhost/api/health` 返回 **200**——health 探针只认 200,**不要解析响应体**(nginx 对未知路径按 SPA 回退 index.html,返回 HTML 属正常)
| 1 | 子包骨架 + bash 资产搬迁 | ✅ | - 后端核对容器内文件 md5 与本地一致;前端核对 dist bundle 文件名
| 2 | utils 层 | ✅ |
| 3 | services 层 | ✅ |
| 4 | routes 层 + 接入 + 删旧占位 | ✅ |
| 5 | 前端模板 | ✅ |
| 6 | 测试(55 用例,全套 218 绿) | ✅ |
| 7 | 部署与依赖 | ✅ 代码完成,待部署 |
--- ---
## 5. 踩过的坑 / 注意事项 ## 4. 踩坑 / 注意事项(合并自各会话,最值钱的部分)
1. **模块头部 LIB_DIR 写死**:原脚本 `LIB_DIR="/tmp/check_modules"` 覆盖环境变量,改 `${LIB_DIR:-...}` 才能注入 1. **孤儿 logger 最隐蔽**:模块若用裸 `logging.getLogger()`(不在 `troubleshoot` 命名空间),事件被全局 root logger(WARNING 无 handler)**静默丢弃**——功能正常但日志全无。排查"日志不可见"先验 `logger.name` 前缀 + `parent`
2. **common.sh 加载路径**:source `$LIB_DIR/lib/config.sh`,executor 必须布置成 `<workdir>/lib/` 结构 2. **热更新必须同步 build 源**:容器 `/app/web/` 是镜像层,`docker cp` 只改运行中容器;不同步 `/data/third_party/monitor-platform/` 会在下次 `--build` 回滚。
3. **MySQL/Redis 模块已内置模糊匹配**`grep -i "${CONTAINERS[mysql]}"`,config 容器名值直接当模式用 3. **前端 dist 热更新权限陷阱**:用 `mv` 备份旧 dist 后目录会变 root,容器内 nginx(非 root worker)读不了 → 404 白屏。备份用 `cp -a`(保留目录权限)+ `chown -R ubains:ubains`,再传文件。
4. **config 凭据占位符**:模板里 `MYSQL_PASSWORD=__MYSQL_PASSWORD__`(不加引号),渲染时用 shlex.quote 输出安全引用;若模板自带引号会双引号套单引号出错 4. **CRLF 坑**:Windows 上传的 `assets/*.sh` 若 CRLF,bash 把 `\r` 当命令内容导致输出全空。executor 上传/布置强制 LF;本地 `.sh`/`.template``.gitattributes` 强制 LF。
5. **shlex.quote 空值**:返回 `''`(空字符串带引号),bash 合法 5. **token 是能力边界**:报告 token 只解锁"读详情/导出",写操作(删除/对比/触发巡检)绝不因 token 放行。Axios 401 双语义(token 访客 vs 会话失效)处理完全不同,**token 分支不清用户 session**
6. **测试 fixture session 覆盖**:user/admin 不能共用同一 client 实例(session 互相覆盖),改为各自独立 test_client() 6. **APScheduler day_of_week 约定**`from_crontab('1-5')` 是周二~周六,标准 cron 1=周一 → APScheduler 0=周一,需 `_convert_dow_to_apscheduler` 统一 -1 偏移。
7. **测试断言 `'password' not in json`**:太严,`has_password` 字段含子串;改为检查不返回 `password_enc`/`password` 明文字段 7. **end_date 过期语义**:APScheduler 对过期任务注册 `next_run_time=None`**永不触发**(正确行为,勿改);页面展示靠 `_calc_next_run(end_date)` 返回 None + `is_expired` 派生字段(不落盘)兜底。end_date 当天 23:59:59 前仍执行。
8. **upload 递归上传**:原 `DIRS_TO_UPLOAD` 只上传一层文件,service_monitor 多层子目录需 `_upload_dir_recursive`;排除 tests/data/__pycache__ 8. **探针脚本勿裸建在仓库根/deploy/**:多数含硬编码明文密码(违反 CLAUDE.md);临时探针放 `.tmp/` 并加 `.gitignore`
9. **运行时 data 目录**:服务器首次部署无 data/,靠 `ensure_dirs()` 自动创建(mkdir parents=True) 9. **部署后 404 先看 nginx access log 的 method**:urllib 无 body POST 可能降级为 GET。
10. **Blueprint 端点名**`service-monitor.xxx`(横线),url_for 用全名 10. **Windows 控制台打印容器输出**:GBK 控制台遇 emoji/中文会 UnicodeEncodeError,脚本内 `sys.stdout = TextIOWrapper(...errors='replace')` 包裹。
11. **Docker basic 的 check_container_status**:用精确名 `grep -q "^${name}$"`,模式下可能匹配不到逐容器项;本期可接受(还有 service 状态/资源等通用项)
--- ---
## 6. 关键文件速查 ## 5. 关键文件速查
### 新增子包
``` ```
skill/code/web/service_monitor/ skill/code/web/service_monitor/
├── __init__.py # from .routes import bp ├── routes.py # 16+ 路由(/service-monitor/* + /api/service-monitor/*)
├── routes.py # 16 条路由 ├── services/{target,runner,report,schedule,notification,statistics,compare}_service.py
├── services/{__init__,target_service,runner_service,report_service}.py ├── utils/{paths,crypto,check_modules,parser,thresholds,display_names,executor}.py
├── utils/{__init__,paths,crypto,check_modules,parser,thresholds,display_names,executor}.py ├── assets/{config.sh.template, common.sh, system/01-04, service/20,22,24}.sh
├── assets/{config.sh.template, common.sh, system/{01,02,03,04}*.sh, service/{20,22,24}*.sh} ├── data/ # 运行时数据(targets.json 加密凭据 + reports/,gitignore)
├── data/{.gitkeep, reports/.gitkeep} # 运行时数据,gitignore └── tests/ # 模块私有测试(90 用例)
└── tests/{__init__,conftest,test_parser,test_crypto,test_check_modules,test_target_service,test_report_service,test_routes_sm}.py 前端:frontend/src/views/service-monitor/*.vue + frontend/dist/(生产 build,部署一部分)
templates/service_monitor/{index,targets,run,report}.html 部署:deploy/upload_to_server.py、verify_deployment.py、deploy_560_enddate_fix.py(热更新脚本)
``` ```
### 修改的全局文件(仅这些)
- `server.py``from service_monitor import bp as service_monitor_bp`(占位时就改好)
- `skill/code/requirements.txt` — +paramiko +cryptography
- `skill/code/pytest.ini` — testpaths 加 web/service_monitor/tests
- `deploy/upload_to_server.py` — +RECURSIVE_DIRS_TO_UPLOAD + _upload_dir_recursive
- `.env.example` — +MONITOR_ENC_KEY
- `.gitignore` — +服务监测运行时数据忽略
- `CLAUDE.md` — 项目结构说明
### 已删除
- `skill/code/web/routes/service_monitor.py`(旧占位路由)
- `skill/code/web/templates/service_monitor.html`(旧占位模板)
---
## 8. 2026-07-27 会话进度追加
### 8.1 完成的工作
#### 定时任务并发执行失败修复 ✅
**问题**:5.44 服务器定时任务(工作日 8:40)不执行,5.202 的 9:00 正常。
**根因**
- APScheduler `misfire_grace_time` 默认 1 秒,调度线程稍有延迟就跳过任务
- `_execute_scheduled_job` 同步阻塞 APScheduler 线程
- `schedules.json` 并发写入无锁保护
**修复**`schedule_service.py` 5 处):
1. 新增 `_schedules_lock`
2. `_save_schedules()` 加锁
3. `update_run_status()` 读-改-写整体加锁
4. `_add_job()` 添加 `misfire_grace_time=3600``coalesce=True``max_instances=1`
5. `_execute_scheduled_job` 改为 `threading.Thread` 后台执行
**验证**:218 测试全绿 ✅
**文档**
- `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md`
- `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md`
#### Docker 容器化方案设计 ✅
**用户需求**
- 部署目录:`/data/third_party/monitor-platform/`
- 单容器部署(nginx + Flask + SQLite)
- 前端更新不需要容器重启,后端更新需要容器重启
- 数据库采用轻量级 SQLite
- 5.60 上有其他服务容器,必须隔离不互相影响
**已完成文件**
- `Dockerfile` — python:3.11-slim,单进程,CRLF 自动修复
- `.dockerignore` — 排除测试/运行时数据/文档
- `docker-compose.yml` — 单容器 + volume + 资源限制(1G/1CPU)
- `deploy/docker_deploy.sh` — 5.60 部署脚本
**隔离保障**
- 不挂载 docker.sock
- 容器名 `troubleshoot`,端口 8088
- volume 路径 `/opt/troubleshoot/` 独占前缀
#### 搜索引擎路径适配 ✅
- `utils/paths.py` 新增 `SEARCH_INDEX_DIR` 常量
- `search_engine.py` 改用统一路径
#### Vue 前端迁移规划 ✅
**文档**`Docs/需求文档/Flask模板迁移Vue前端任务清单.md`
**范围**:三大模块共 18 个页面
- 问题排查助手:3 个页面
- 服务监测:10 个页面
- 服务管理:4 个页面
**技术选型**:Vue 3 + Vite + Element Plus + Pinia + TypeScript + ECharts
**预估工时**:17-21 天(约 3-4 周)
**建议执行顺序**:先容器化部署落地,再并行推进 Vue 重构
### 8.2 待执行任务(历史,已部分完成)
| 优先级 | 任务 | 说明 | 状态 |
|--------|------|------|:----:|
| **P0** | 部署容器化版本到 5.60 | 需先确认 Docker 已安装,执行 `docker_deploy.sh` | ✅ 已上线 |
| **P0** | 更新 5.44 定时任务 end_date | 当前 end_date=2026-07-22 已过期 | ✅ 已更新为 2027-08-31 |
| P1 | 修复 APScheduler day_of_week 约定 bug | `from_crontab('1-5')` 实际是周二到周六 | ✅ 已修复 |
| P2 | SQLite 迁移 | 替换 JSON 文件存储 | ⏳ |
| P2 | Vue 前端开发 | 按任务清单执行 | ⏳ |
### 8.3 遗留问题(历史,已解决)
1. ~~**5.44 定时任务 end_date 已过期**(2026-07-22)~~ ✅ 已更新为 2027-08-31
2. ~~**APScheduler day_of_week 约定 bug**`CronTrigger.from_crontab('1-5')` 按 APScheduler 约定是周二到周六~~ ✅ 已修复(见 9.1)
3. ~~**容器化尚未部署**~~ ✅ 2026-08-13 已上线 5.60
### 8.4 本次新增文件
| 文件 | 说明 |
|------|------|
| `Dockerfile` | 重写:python:3.11-slim + 单进程 |
| `docker-compose.yml` | 重写:单容器 + volume + 资源限制 |
| `.dockerignore` | 更新:排除 tests/data 等 |
| `deploy/docker_deploy.sh` | 新增:5.60 部署脚本 |
| `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md` | 问题处理文档 |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md` | 计划执行文档 |
| `Docs/需求文档/Flask模板迁移Vue前端任务清单.md` | Vue 迁移任务清单 |
---
## 9. 2026-08-17 会话进度追加
### 状态更新
**容器化部署已上线** ✅:
- 5.60 容器 `troubleshoot` 运行中,启动于 2026-08-13 12:25(北京时间)
- supervisor 管理 nginx + Flask 双进程
- 三个定时任务均正常工作,5.44 end_date 已更新为 `2027-08-31`
### 9.1 修复 APScheduler day_of_week 约定 bug ✅
**问题**`CronTrigger.from_crontab('1-5')` 按 APScheduler 约定 day_of_week 0=周一,1=周二…6=周日,导致 `1-5` 被解释为**周二到周六**,而非预期的周一到周五。
**实测验证**
- 2026-08-15(周六)三个任务均执行 → 确认 `1-5` 确实包含周六
- 修复前 `from_crontab('30 8 * * 1-5')` 首次触发:Tue 08-18
- 修复后 `from_crontab('30 8 * * 0-4')` 首次触发:Mon 08-17
**修复方案**`schedule_service.py`):
1. 新增 `_convert_dow_to_apscheduler(cron_expr)` 函数:将 cron 表达式的 day_of_week 字段统一 -1 偏移(标准 cron 1=周一 → APScheduler 0=周一)
2. 支持单值、范围、列表、混合格式
3.`_add_job()` 中调用转换函数,将转换后的表达式传给 `CronTrigger.from_crontab()`
**修改文件**
- `skill/code/web/service_monitor/services/schedule_service.py` — 新增 `_convert_dow_to_apscheduler` 函数 + `_add_job` 中调用
**新增文件**
- `skill/code/web/service_monitor/tests/test_schedule_service.py` — 15 个测试用例(11 转换 + 3 APScheduler 真实触发验证)
**验证**:全套 253 测试全绿 ✅
### 9.2 待办更新
| 优先级 | 任务 | 说明 |
|--------|------|------|
| P2 | SQLite 迁移 | 替换 JSON 文件存储 |
| P2 | Vue 前端开发 | 按任务清单执行 |
### 9.3 遗留问题
1. **4 个 routes 页面测试失败**`test_routes_sm.py::TestPages`):返回 200 而非 302,因前端改为 Vue SPA 后页面路由由 `index.html` 处理,无重定向。需后续更新测试断言。
### 9.4 本次新增/修改文件
| 文件 | 说明 |
|------|------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 修改:新增 `_convert_dow_to_apscheduler` 转换函数 |
| `skill/code/web/service_monitor/tests/test_schedule_service.py` | 新增:15 个 day_of_week 转换测试用例 |
--- ---
## 10. 2026-08-29 会话进度追加(三问题修复 + 部署复测) ## 6. 会话进度速览(详细见归档 / 对应 PRD)
### 10.1 背景(为什么做)
用户连续反馈 3 个问题:
1. **触发巡检(如 5.44 全量巡检)完成后,日志里看不到钉钉通知发送的痕迹**("触发过会发通知吧?")
2. **手动重跑 5.44 定时任务,发送的内容特别少**(约 40 行,正常应几百项)
3. **每天 09:30 报告缺失告警误报**:把从未配定时任务的内置目标「本机(当前服务器)/local」报成"2 天无新报告(上次:无报告)"
### 10.2 根因与修复(全部已部署复测)
#### ✅ 根因 1:service_monitor 孤儿 logger(通知日志不可见) | 归档§ | 日期 | 内容 | 提交 |
|-------|------|------|------|
| 8 | 07-27 | 定时任务并发执行失败修复;Docker 容器化方案设计(build 源 `/data/third_party/monitor-platform/`) | - |
| 9 | 08-17 | 容器化已上线;APScheduler day_of_week 约定 bug 修复(15 用例) | - |
| 10 | 08-29 | 三问题修复:孤儿 logger 通知日志不可见 / CRLF 输出缺失 / 09:30 报告缺失误报;热更新部署复测(5.44 full 509 项) | 未提交 |
| 11 | 08-31 | 报告链接免登访问(token 只读鉴权);镜像重建部署;269 用例 | **07d17632** |
| 12 | 08-31 | token 维持仅有效期不加次数;清理 60+ 临时探针脚本 | **eebe75d4** |
| 13 | 09-01 | 提交 HANDOFF 更新 | **eebe75d4** |
| 14 | 09-01 | **5.202 end_date 过期修复 + 部署 5.60 + 运维延长恢复**(本次,详见下) | **7249d355** |
**现象**:钉钉机器人实际**一直在发**(errcode 0 成功),只是 `钉钉通知已发送` 这行日志永远看不到。 ### §14 本次:5.202 定时任务 end_date 过期(修复 + 部署 + 运维恢复)
**根因**`service_monitor/` 9 个模块用裸 `logging.getLogger("service_monitor.X")`,脱离了 `utils/logger.py``troubleshoot` 命名空间(根 logger 有 StreamHandler+RotatingFileHandler)。孤儿 logger 的事件 propagate 到全局 `root` logger(默认 WARNING、无 handler),**INFO/WARNING 全部静默丢弃** **根因**5.202 任务(`sched_7cd80b06-09f`,工作日 08:50 全量巡检)`end_date=2026-08-31` 已过期 → APScheduler 注册 `next_run_time=None` 永不触发;但页面"下次执行"仍显示虚假时间(`_calc_next_run` 不看 end_date)+ 无"已过期"状态 → 静默失效
**修复**:8 个文件头部 `import logging` + `logging.getLogger(...)``from utils.logger import get_logger` + `logger = get_logger("service_monitor.X")`(自动归入 `troubleshoot` 命名空间): **代码修复(7249d355,已推送)**
- `routes.py``utils/check_modules.py``services/{target_service, notification_service, report_service, statistics_service, compare_service, schedule_service}.py`(schedule 此前已正确) - `_calc_next_run(cron, end_date)`:过期/超期返回 None;新增 `_parse_end_date`(当天 23:59:59)/`_is_schedule_expired``list_schedules``is_expired` 派生字段
- `utils/crypto.py` **故意不改**:其 docstring 声明"模块自包含,不依赖全局 utils/logger",自带 StreamHandler,保持隔离 - 前端 `Schedule.vue`:过期显示"已过期"徽标、"下次执行:已失效"、禁用"启用";类型加 `is_expired?`
- 测试 +8 用例;`test_schedule_service.py` 未跟踪,随提交入库
**验证** **部署 5.60 + 复验**:后端 docker cp 热更新(md5 一致)+ 前端 dist 热更新(`Schedule-C4EnHSuH.js`);容器 healthy / health 200;生产 API 实测 `is_expired: true`
- 容器内 import 探针:9/9 模块 logger.name 均为 `troubleshoot.service_monitor.*`,parent=troubleshoot ✅
- 本地 PROBE 日志写入 app.log ✅
#### ✅ 根因 2:bash 资产 CRLF 导致模块输出几乎全空("发送内容少") **运维恢复**:PUT `{"end_date":"2027-08-31"}``is_expired: false``next_run_at: 2026-09-02T08:50:00`,5.202 巡检恢复。
**现象**:5.44 full 巡检 01_system_basic 输出被截断到 ~40 行,主机名/IP/内存等项缺失。 **文档**`PRD_问题处理_5.202定时任务end_date过期.md` + `PRD_计划执行_5.202定时任务end_date过期修复.md`
**根因**:Windows 上传的 `assets/*.sh` 是 CRLF,Linux bash 执行时把 `\r` 当命令内容的一部分,大量命令失效。
**修复**(双层):
- `utils/executor.py` 上传/布置脚本时强制 LF:`dst.write_bytes(data.replace(b"\r\n", b"\n"))`(write_bytes 3 处 + config write_text,见 325/445 行附近)
- 本地 assets `.sh`/`.template` 统一转 LF(git 已跟踪)
**验证**:5.44 full 复测 = **42 模块 / 509 项**(正常 486 / 警告 10 / 严重 13),`01_system_basic` 18 项完整(主机名、内核、负载、CPU 核心等全有值)✅
#### ✅ 根因 3:check_missing_reports 每日误报内置目标
**现象**:09:30 报告缺失告警误报 `本机(当前服务器)/local` 无报告。
**根因**`check_missing_reports`**所有**目标做缺失判定,内置目标从未配定时任务 → 永远"无报告"。
**修复**`report_service.py:838` check_missing_reports):仅检查**启用了定时任务**的目标(`schedule_service.list_schedules()` 中 enabled 且 target_id 非空);schedules.json 读取失败回退为检查全部目标(保持原行为兜底)。
**验证**
- 新增 4 个单测(`test_report_service.py`):仅定时目标参与判定 / 无定时任务返回空 / 最近有报告不缺失 / 过期报告 missing_days=真实天数
- 容器内实测 `check_missing_reports(2)` 返回 **0 个缺失**(之前会报 local)✅
- 全套 257 测试全绿 ✅
#### ✅ 附带:通知发送结果日志(runner_service)
`run_inspection_sync` 通知块增加结果日志(`runner_service.py:376-403`):
```
巡检完成通知:已发送(report_id=…)
未找到报告 …,跳过
发送通知失败: …
连续异常告警检查失败: …
```
配合根因 1 的 logger 修复,通知结果现在**可在 app.log 留痕验证**
### 10.3 部署与复测(关键流程,接手者复用)
**生产拓扑**(5.60,2026-08-29 已确认):
- 容器 `troubleshoot``troubleshoot:latest`,supervisord 管 nginx:80 + Flask:8088,健康检查 `curl http://localhost/api/health`
- 端口映射 `8088→80`(nginx)
- 镜像真实 build 源:**`/data/third_party/monitor-platform/`**`skill/code/web/` + `skill/code/requirements.txt` + `frontend/dist/`
- `docker-compose.yml` 在该目录,`docker compose up -d --build` 重建
- volume:monitor-data→`/app/web/service_monitor/data`、logs→`/app/web/logs`、users.json、dist、搜索索引.json(只读)
**本次部署方式**(热更新,未重建镜像):
1. 18 个改动文件(代码+assets)LF 同步到 build 源 `/data/third_party/monitor-platform/skill/code/web/service_monitor/`,md5 18/18 校验
2. `docker cp` 逐文件注入容器 `/app/web/service_monitor/`,清 `__pycache__``docker restart troubleshoot`
3. 容器内 md5 复验 MATCH + logger 探针 9/9 归位 + health healthy
**复测**(5.44 full,report `20260829_034555_f7ee98`):
- `POST /api/service-monitor/schedules/sched_042074c6-5e2/run` 触发(管理员登录 + Cookie)
- 巡检 4 分钟(03:41:53→03:45:55)→ 报告 509 项
- app.log 关键行:
```
保存报告: 20260829_034555_f7ee98 (目标=新统一平台5.44, 套件=full, 项=509)
报告保存完成: 20260829_034555_f7ee98
钉钉通知已发送 ← 之前永远不可见
巡检完成通知:已发送(report_id=20260829_034555_f7ee98)
巡检执行完成: success=True
```
### 10.4 当前状态
- **代码已改完 + 本地 257 测试全绿 + 生产容器已热更新并复测通过**
- **改动尚未 git 提交**(分支 `troubleshoot-ai-assistant`,勿 merge master)
- 待办表更新:SQLite 迁移 / Vue 前端(P2,历史遗留,与本批无关)
### 10.5 本次改动文件清单
| 文件 | 改动 |
|------|------|
| `service_monitor/services/notification_service.py` | logger 迁移(通知发送本就正常,只是日志不可见) |
| `service_monitor/services/runner_service.py` | logger 迁移 + 通知结果日志块 |
| `service_monitor/services/report_service.py` | logger 迁移 + check_missing_reports 仅查定时目标 |
| `service_monitor/services/{schedule_service,target_service,statistics_service,compare_service}.py` | logger 迁移 |
| `service_monitor/routes.py`、`utils/check_modules.py` | logger 迁移 |
| `service_monitor/utils/executor.py` | 上传/布置脚本强制 LF(CRLF 根因) |
| `service_monitor/assets/*.sh` + `config.sh.template`(8 个) | 统一转 LF |
| `service_monitor/tests/conftest.py` | tmp_data 补 `schedule_service.SCHEDULES_FILE` patch |
| `service_monitor/tests/test_report_service.py` | +4 check_missing 单测 |
### 10.6 踩坑(本次新增,务必注意)
1. **孤儿 logger 是最隐蔽的坑**:模块里用 `logging.getLogger()` 拿到的事件若不在 `troubleshoot` 命名空间,会被全局 root logger(WARNING 无 handler)**静默丢弃**——功能正常但日志全无。排查"日志不可见"先验 `logger.name` 前缀 + `parent`。
2. **部署热更新必须同步 build 源**:容器 `/app/web/` 是**镜像层**,`docker cp` 只改运行中容器;不同步 `/data/third_party/monitor-platform/` 会在下次 `--build` 时**回滚**。同理 `/opt/troubleshoot` 是 nohup 旧运行源,也要防 systemd 回退路径用旧代码。
3. **urllib POST 被降级为 GET**:urllib 复用连接时对无 body 的 POST 可能自动发 GET → 拿到 404 而非 405/200。排查 404 先看 nginx access log 的 method;用 raw socket 显式 POST 即可(本次 `RAW POST 200 triggered=true`)。
4. **报告文件路径**:`/app/web/service_monitor/data/reports/<rid>.json`(注意实际在 reports/ 根,不是 reports/index/reports/)。
5. **Windows 控制台打印容器输出**:GBK 控制台遇 emoji/中文会 UnicodeEncodeError,脚本内 `sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')` 包裹;复杂引号命令用 `base64` payload 传参。
### 10.7 下一步(优先级排序)
1. **git 提交本批改动**:`troubleshoot-ai-assistant` 分支(勿 merge master),Conventional Commits,例如 `fix(service-monitor): 修复通知日志不可见/CRLF 输出缺失/报告缺失误报`,并同步 `Docs/需求文档/服务监测/HANDOFF.md` 的提交记录
2. **考虑重建镜像**:本次是 docker cp 热更新,长期稳妥做法是 `docker compose up -d --build`(build 源已同步,重建后代码一致)
3. **回归确认定时任务**:周一到周五 08:30(5.44)自动巡检时,验证 app.log 有 `巡检完成通知:已发送` + `钉钉通知已发送`,且**不再**出现 09:30 报告缺失误报
4. **清理临时探针脚本**:仓库根/`deploy/` 下 `check_*.py`、`_diag_*.py`、`debug_notification.py` 等临时文件按需归档或删除
5. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
### 10.8 本次验证用的关键命令
```bash
# 触发 5.44 full 巡检(管理员 Cookie + 显式 POST)
POST /api/service-monitor/schedules/sched_042074c6-5e2/run
# 看通知日志(容器内)
docker exec troubleshoot tail -50 /app/web/logs/app.log | grep -E '通知|保存报告|巡检执行完成'
# 验证 check_missing(容器内)
docker exec troubleshoot python3 -c "$(echo <b64> | base64 -d)" # from service_monitor.services import report_service; check_missing_reports(2)
```
--- ---
## 11. 2026-08-31 会话进度追加(报告链接免登访问 + 生产 docker compose 重建) ## 7. 工作区遗留未提交(下次提交范围参考)
### 11.1 背景(为什么做)
用户反馈:**钉钉通知里的"完整报告"链接**(含 `?token=...`)在未登录浏览器点击后,
被重定向到登录页并提示"登录已过期"。
复现链接示例: **✅ 应提交(服务监测正式代码,已测试):**
``` - logger 迁移:`services/{compare,notification,report,runner,statistics,target}_service.py``utils/check_modules.py`(→ `utils.logger.get_logger`
http://192.168.5.60:8088/login?next=/service-monitor/report/20260831_010950_bce779?token=rpt_f129... - CRLF:`utils/executor.py`(强制 LF)+ `assets/*.sh`/`config.sh.template`
``` - 功能:`report_service.py`(check_missing 仅查定时目标)、`runner_service.py`(通知结果日志 + 僵尸清理)、`schedule_service.py`(已随 7249d355 提交)
- 测试:`tests/conftest.py`
- 服务管理:`services/five44_client.py``services/service_manage.py`(另立 commit)
### 11.2 根因(三层鉴权链路都挡在访客前) **⚠️ 其他模块/勿提交:** `Dockerfile``.gitattributes``Docs/服务管理/``HANDOFF_容器部署_5.69.md``frontend/tsconfig.app.tsbuildinfo`(build 产物)、`users.json`(运行时变更)、`deploy/*.py` 运维脚本(已确认保留,是否入库视需要)、`Docs/维护手册/`(111MB 勿入库)、`skill/code/搜索索引.json`/`搜索向量.json`(运行时生成)
1. **前端路由守卫**:Vue Router 把 `/service-monitor/report/:id` 视为必须登录页面,未登录一律跳登录(`next` 还带了 `?token=`
2. **后端详情 API**`api_get_report` 只认 session,访客请求直接 401
3. **Axios 全局 401 拦截器**:把 token 访客的 401 也当"登录已过期",再跳一次登录页 → 用户看到误导性提示
### 11.3 修复方案(只放读,不扩权)
| 层 | 改动 | 要点 |
|----|------|------|
| 后端 `routes.py` | `api_get_report` 优先校验 `request.args['token']` | 有效 token 允许**只读**详情访问;token 缺失/无效回退到原登录校验 |
| 后端 | 删除/批量删除/对比等**写操作不动** | token 不获得任何写权限,权限范围不扩大 |
| 前端 `router/index.ts` | 路由守卫仅放行 `MonitorReportDetail` **且带 token** 的请求 | 其他带 token 页面仍走正常认证 |
| 前端 `api/report.ts` | `getReport(id, token?)` 把 token 作为 **query params** 发送 | 用参数对象避免手工拼接编码问题 |
| 前端 `ReportDetail.vue` | 详情加载时把当前路由 token 传给 `getReport`;导出链接统一 URL 编码 | token 特殊字符不破坏查询串 |
| 前端 `utils/http.ts` | 401 分支区分"报告 token 访客" vs "会话失效" | token 访客显示「报告链接无效或已过期」,**不**跳登录不清 session |
### 11.4 测试(263 → 269 全绿)
- `test_routes_sm.py` 新增 `TestReportTokenAccess` 类 4 用例:
- 无 session + 有效 token → 详情 200
- 缺 token / 错误 token → 401 需登录
- 过期 token → 401
- token **不能**删除报告(写权限未扩大)
- `test_report_service.py` 新增 token 校验单测(有效/错误/过期/缺字段)
- 全套 pytest **269 用例全绿**(本机本地环境)
- 前端:`vue-tsc --noEmit` 类型检查通过 + 生产 build 成功(dist 含修复 bundle)
### 11.5 部署(本次为**镜像重建**,非热更新)
生产拓扑延续 10.3:build 源 `/data/third_party/monitor-platform/` + `docker compose up -d --build`
步骤:
1. 本机构建新前端 dist(含修复 bundle)
2. 同步 build 源:后端改动文件 + 新 `frontend/dist/` 上传到 `/data/third_party/monitor-platform/`
3. 服务器 `docker compose up -d --build` 重建镜像重启容器(**不是** `docker restart`
4. 容器内部 md5/代码版本校验 + 健康检查
### 11.6 生产复测结果(已验证 ✅)
- **访客免登**:无登录 Cookie 直接打开 `report/20260831_010950_bce779?token=...` → 进入报告详情(不再重定向登录)
- **详情 API**:带 token 访客请求返回 200
- **导出**:带 token 访客导出 MD/JSON 仍可用
- **错误/过期 token**:显示「报告链接无效或已过期」,不误报"登录已过期"、不跳登录
- **权限未扩大**:无 token / token 错误时未登录访问详情仍 401 或跳登录;删除/写操作仍需要管理员登录
- **普通页面回归**:未登录访问报告列表等其他服务监测页面仍跳转登录
### 11.7 git 提交
- 已提交并推送:**`07d17632`**
`fix(service-monitor): 报告链接免登访问 + 钉钉链接直达报告详情`
- 后端 token 只读鉴权、前端路由守卫/API/401 拦截器区分访客 token、导出 URL 编码
- 测试 269 全绿、dist 重建、生产 docker compose 重建并复测通过
- 分支 `troubleshoot-ai-assistant`**勿 merge master**
### 11.8 本次改动文件清单
| 文件 | 改动 |
|------|------|
| `skill/code/web/service_monitor/routes.py` | `api_get_report` 支持 token 只读访问 |
| `frontend/src/router/index.ts` | 守卫放行带 token 的报告详情 |
| `frontend/src/api/service-monitor/report.ts` | `getReport(id, token?)` query 传参 |
| `frontend/src/views/service-monitor/ReportDetail.vue` | token 传入详情/导出 |
| `frontend/src/utils/http.ts` | 401 拦截器区分访客 token |
| `skill/code/web/service_monitor/tests/test_routes_sm.py` | +TestReportTokenAccess 4 用例 |
| `skill/code/web/service_monitor/tests/test_report_service.py` | +token 校验单测 |
| `frontend/dist/` | 生产 build(含修复 bundle) |
| `Dockerfile` 等 | 重建相关微调 |
### 11.9 踩坑 / 注意(本次新增)
1. **token 是能力边界**:token 只应该解锁"读报告详情/导出",任何写操作(删除、批量删除、对比、触发巡检)绝不能因 token 放行——改权限要当安全变更对待,回归测试必须覆盖"token 无权写"。
2. **Axios 401 双语义**:同一个 401 状态码可能是"访客 token 失效"或"登录过期",处理方法完全不同(前者显示链接失效、后者清 session 跳登录)。用 `error.config.params.token` / URL 含 `token=` 区分,且**不要在 token 分支清除用户会话**
3. **前端 build 产物是部署的一部分**:改前端源码后必须重新 build 并替换 `frontend/dist/`,再同步 build 源重建镜像;只改源码不重建,线上仍是旧 bundle。
4. **路由守卫判断抽纯函数更可测**:守卫里"是否 token 访客报告页"的判断逻辑建议独立成纯函数,便于后续前端单测(当前项目无前端测试框架,本次用 vue-tsc + build 验证)。
### 11.10 下一步(优先级排序)
1. **回归确认定时任务**:下次工作日定时巡检(5.44 08:30)后,确认钉钉"完整报告"链接访客可直达(本轮已修)
2. **考虑给 token 加查看限制**(可选):报告 token 长期有效,若担心泄露,可加有效期/访问次数/按报告自定义失效时间(现状:14 天报告清理时一并过期)→ **已评估,维持现状,见 12.1**
3. **清理临时探针脚本**:仓库根/`deploy/` 下临时 `check_*.py``_diag_*.py` 等按需归档或删除(10.7 遗留)→ **已清理,见 12.2**
4. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
--- ---
## 12. 2026-08-31 会话进度追加(token 限制评估 + 清理临时探针脚本) ## 8. 下一步(优先级排序)
### 12.1 报告 token:评估结论 — 维持「仅有效期限制」,不加访问次数
**需求**:11.10 提到的可选「给 token 加查看限制」。
**评估结论**(与用户确认):**不加访问次数限制,维持现有有效期机制**
- token 有效期 `ACCESS_TOKEN_DAYS = 7``report_service.py:33`
- `validate_access_token()``report_service.py:604`)校验格式/报告绑定/有效期
- 报告保留 14 天,`cleanup_expired()` 到期删除报告,token 随之失效
- **本轮不改任何代码**`report_service.py` / `routes.py` / 前端均不动)
后续如需收紧(如加访问次数/按报告自定义失效),方案已备好,见 12.4 备忘,接入时注意并发原子消费与历史报告兼容。
### 12.2 清理临时探针脚本(已完成 ✅)
**范围**:只删纯探针(一次性诊断/排查脚本,全部未跟踪、无正式引用),保留可能复用脚本。
**已删除**(未 git 跟踪,删除不影响历史):
- **仓库根目录**:22 个 `check_*.py`(app_log/code_version/container_status/data_dir/docker_logs/flask_*/logs_direct/monitor_data/reports/running_process/running_scheduler/schedule_issue/scheduler_*/supervisor*/task_result/threads)、`debug_notification.py``final_check.py``final_diagnosis.py``test_docker_on_989.sh``test_scheduler2/4/_init.py``test_sudo_config.py`
- **deploy/**`_diag*.py`(14 个)、`_debug_500.py``_deploy_and_test.py``debug_inspect*.py`(4 个)、`_probe_544_candidates.py``_run_diag*.py`(3 个)、`_run_verify_560.py``_verify_sign_fix.py``_final_check.py``_final_verify.py``cat_log.py`、未跟踪 `check_*`(inspect_error/local_ssh/logs/report/run_logs/schedules/ssh_config/sudo,8 个)、一次性 `test_*`(full_inspect/pdf/pdf2/quick_inspect/sse/ssh)、`verify_69_deploy.py`
- **deploy/tmp_test/** 整个目录(19 文件 ~5.8MB,含 5.44/5.60 抓包 bundle `app_544_latest.js`/`app_backstage.js`
**明确保留**(勿再删):
- 已跟踪正式文件:`deploy/check_service.py``deploy/build_index.py``deploy/upload_to_server.py``deploy/verify_deployment.py``deploy/deploy_docker.py``deploy/deploy_service_manage.py``deploy/verify_new_features.py``deploy/deploy_to_69.py``skill/code/web/service_monitor/utils/check_modules.py`
- 可能复用脚本(用户确认保留):根目录 `api_trigger.py``trigger_*``deploy_fix*.py``download_*.py``fix_*.py``force_rebuild.py``rebuild_and_deploy.py``restart_and_verify.py``deploy/``deploy_frontend*.py``deploy_to_560.py``upload_backend.py``upload_dist.py``upload_docs*.py`(含 v2~v10/final/final2/win)、`upload_skill.py``encode_docs.ps1``upload_docs.ps1`
**验证**
- 删除前逐文件 `git grep` 复核引用:仅 `HANDOFF.md` 自身提及 `debug_notification`/`check_container_status`;bash 资产中的 `check_app_log_errors()`/`check_container_status()` 是函数名,与根目录探针脚本无关,不误删
- `git status` 无已跟踪文件被删(无 `D` 状态);保留文件全部在位
- 全套 pytest **269 用例全绿**(纯删未跟踪脚本,无影响)
### 12.3 踩坑 / 注意(本次新增)
1. **探针脚本多为硬编码明文密码**`Ubains@123`,违反 CLAUDE.md「密码禁止硬编码」):本次删除的 `_diag*`/`debug_*`/`check_*` 系列绝大多数含明文凭据,删掉顺带降低泄漏面。**今后排查勿再在仓库根/`deploy/` 裸建诊断脚本**,如需临时探针放 `.tmp/` 并加入 `.gitignore`,且禁止硬编码密码。
2. **bash 资产里的同名函数不是探针**`assets/service/*.sh``check_app_log_errors()`/`check_container_status()` 是监测模块函数,与根目录探针脚本无引用关系——清理时按「文件名 + git grep」判断,勿按关键词批量删。
### 12.4 备忘:token 加访问次数的备用方案(当前未做)
若后续要加访问次数限制(用户当前已确认不加):
- `report_service.py` 增加 `ACCESS_TOKEN_MAX_USES` 常量 + `save()``access_token_used_count`/`access_token_max_uses`/`access_token_issued_at`/`access_token_invalidated_at`
- `validate_access_token()` 改为「校验 + 原子递增」(进程内锁 `_token_lock` 包住读-判-增-写,单进程部署适用),成功后消费一次,失败请求不计
- `cleanup_expired()` 对「报告仍保留但 token 已过期/耗尽」仅清 token 不删报告
- 兼容:旧报告无新字段按原有效期校验,不重生成 token;详情/导出共用额度,登录访问不计入
### 12.5 下一步(优先级排序)
1. **回归确认定时任务**下次工作日定时巡检(5.44 08:30)后确认钉钉链接访客可直达、`钉钉通知已发送` 留痕、09:30 无误报 1. **回归确认定时任务**2026-09-02 08:50 5.202 自动巡检后,确认新报告 + 钉钉通知 + app.log `巡检完成通知:已发送` 留痕、09:30 无误报(5.44 08:30 同机制)
2. **git 提交本次清理 + HANDOFF 更新**(分支 `troubleshoot-ai-assistant`,勿 merge master),Conventional Commits 如 `chore(monitor): 清理临时探针脚本 + 记录 token 限制评估结论` 2. **提交遗留服务监测源码改动**(§7 应提交清单)
3. (历史遗留 P2)SQLite 迁移 / Vue 前端开发 3. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
# HANDOFF — 服务监测模块实施进度
> 最后更新:2026-09-01 | 分支:troubleshoot-ai-assistant | 模块:service-monitor
> 状态:**5.202 定时任务 end_date 过期问题已修复并部署 5.60 复测通过(7249d355)+ 已提交推送;5.202 end_date 已延长至 2027-08-31 恢复巡检**
---
## 1. 我们在做什么
为运维平台落地「服务监测」模块:监控本机/远程服务器系统资源与服务状态,输出巡检报告。
**只做监测不做修复**(修复接口预留,点击返回"开发中")。
需求与计划文档:
- `Docs/需求文档/服务监测/PRD_需求文档_服务监测模块.md`
- `Docs/需求文档/服务监测/PRD_计划执行_服务监测模块.md`
### 关键决策(已与用户对齐)
- 代码**完全隔离**:所有代码进 `skill/code/web/service_monitor/` 子包,不动 `routes/`/`services/`/`utils/`/`config.json`/`container.py`,对外仅 `server.py` 一行注册
- 本期**核心子集**:system(01/02/03/04) + Docker-basic + MySQL-basic + Redis-basic
- bash 模块**搬进项目**当静态资源 + config 外置模板
- 容器名**模糊匹配**(不同服务器容器名不一致,用 grep -iE 模式发现)
- 远程 SSH 用 **paramiko**;SSH 密码 **Fernet 加密存储**
- 进度反馈 **SSE**;报告默认保留 **14 天**可配置
- **快速巡检 / 全量巡检** 两套件
- **角色控制**:普通用户仅查看(隐藏操作入口),管理员可操作
---
## 2. 已完成(全部 7 阶段)
### ✅ 阶段一:子包骨架 + bash 资产搬迁
- 建子包目录 `service_monitor/{__init__,routes,services/,utils/,assets/,data/,tests/}`
- 搬迁核心 bash 模块到 `assets/`:common.sh + config.sh.template + system/{01,02,03,04} + service/{20,22,24}
- 改造模块头部 `LIB_DIR="${LIB_DIR:-/tmp/check_modules}"`(7个模块,支持注入)
- config.sh.template:容器名改匹配模式、密码改占位符、阈值可覆盖、新增 resolve_container
- 端到端本地执行验证通过(KEY:VALUE 输出正常)
### ✅ 阶段二:utils 层
- `paths.py` — 模块私有路径常量 + ensure_dirs()
- `crypto.py` — Fernet 加密/解密(MONITOR_ENC_KEY > 派生自 SECRET_KEY > 兜底)
- `check_modules.py` — CheckModule 清单 + get_suite() + render_config()(shlex.quote 防注入)
- `parser.py` — 移植 Parse-ModuleResult(过滤脏行 + KEY:VALUE 解析 + 状态判定,优先 *_LEVEL)
- `thresholds.py` — 阈值表 + judge_status()(数值/百分比比较,严重/警告分级)
- `display_names.py` — KEY→中文显示名(核心子集)
- `executor.py` — BaseExecutor/LocalExecutor(subprocess)/SSHExecutor(paramiko)
### ✅ 阶段三:services 层
- `target_service.py` — 目标 CRUD + 连通性测试 + make_executor + resolve_credentials(解密)
- `runner_service.py` — 巡检编排(生成器 yield SSE 事件)+ cancel_run + get_run_status
- `report_service.py` — 报告 save/get/list/delete/cleanup_expired/export_md/export_json
### ✅ 阶段四:routes 层 + 接入 + 删旧占位
- `routes.py` — 16 条路由(4页面 + 12API),角色控制完整
- 删除旧占位 `routes/service_monitor.py` + `templates/service_monitor.html`
- `server.py` 已有 `from service_monitor import bp`(之前占位就改好了)
### ✅ 阶段五:前端模板(UI 已与用户确认)
- `index.html` — 目标卡片 + 报告列表(普通用户隐藏操作按钮)
- `targets.html` — 目标 CRUD 弹窗 + 连通性测试
- `run.html` — 进度条 + 模块清单 + SSE 实时反馈 + 完成跳报告
- `report.html` — 汇总卡片 + 异常置顶 + 分模块折叠 + 导出MD/JSON + 修复预留按钮
### ✅ 阶段六:测试
- 模块私有测试 55 用例(parser/crypto/check_modules/target_service/report_service/routes_sm)
- **全套 218 测试全绿**(原 163 + 新增 55,未破坏现有测试)
- `pytest.ini` 新增 testpath `web/service_monitor/tests`
- 子包 conftest 注入 tmp 数据目录隔离
### ✅ 阶段七:部署与依赖
- `requirements.txt` 加 paramiko + cryptography
- `.env.example` 加 MONITOR_ENC_KEY 说明(含生成命令)
- `deploy/upload_to_server.py``RECURSIVE_DIRS_TO_UPLOAD` + `_upload_dir_recursive()`(递归上传 service_monitor,排除 tests/data/__pycache__)
- `.gitignore` 加服务监测运行时数据忽略(targets.json/reports/*.json,保留 .gitkeep)
- `CLAUDE.md` 更新项目结构说明
- 本机 test_client 冒烟全通过(页面200/权限403/API正常)
---
## 3. 待办(部署)
代码完成,**尚未部署到 5.60**。部署步骤:
```bash
# 1. 服务器需装依赖(paramiko 已在 deploy 用,cryptography 需确认)
# 服务器执行:pip install paramiko cryptography
# (或确认 paramiko 已装则 cryptography 作为其依赖已存在)
# 2. 上传代码(用 ! 前缀在会话内执行,SSH_PASSWORD 环境变量)
! 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
# - 本地目标快速巡检 → 看报告
# - (可选)新增远程目标测连通 + 远程巡检
```
---
## 4. 任务进度
| # | 阶段 | 状态 |
|---|------|------|
| 1 | 子包骨架 + bash 资产搬迁 | ✅ |
| 2 | utils 层 | ✅ |
| 3 | services 层 | ✅ |
| 4 | routes 层 + 接入 + 删旧占位 | ✅ |
| 5 | 前端模板 | ✅ |
| 6 | 测试(55 用例,全套 218 绿) | ✅ |
| 7 | 部署与依赖 | ✅ 代码完成,待部署 |
---
## 5. 踩过的坑 / 注意事项
1. **模块头部 LIB_DIR 写死**:原脚本 `LIB_DIR="/tmp/check_modules"` 覆盖环境变量,改 `${LIB_DIR:-...}` 才能注入
2. **common.sh 加载路径**:source `$LIB_DIR/lib/config.sh`,executor 必须布置成 `<workdir>/lib/` 结构
3. **MySQL/Redis 模块已内置模糊匹配**`grep -i "${CONTAINERS[mysql]}"`,config 容器名值直接当模式用
4. **config 凭据占位符**:模板里 `MYSQL_PASSWORD=__MYSQL_PASSWORD__`(不加引号),渲染时用 shlex.quote 输出安全引用;若模板自带引号会双引号套单引号出错
5. **shlex.quote 空值**:返回 `''`(空字符串带引号),bash 合法
6. **测试 fixture session 覆盖**:user/admin 不能共用同一 client 实例(session 互相覆盖),改为各自独立 test_client()
7. **测试断言 `'password' not in json`**:太严,`has_password` 字段含子串;改为检查不返回 `password_enc`/`password` 明文字段
8. **upload 递归上传**:原 `DIRS_TO_UPLOAD` 只上传一层文件,service_monitor 多层子目录需 `_upload_dir_recursive`;排除 tests/data/__pycache__
9. **运行时 data 目录**:服务器首次部署无 data/,靠 `ensure_dirs()` 自动创建(mkdir parents=True)
10. **Blueprint 端点名**`service-monitor.xxx`(横线),url_for 用全名
11. **Docker basic 的 check_container_status**:用精确名 `grep -q "^${name}$"`,模式下可能匹配不到逐容器项;本期可接受(还有 service 状态/资源等通用项)
---
## 6. 关键文件速查
### 新增子包
```
skill/code/web/service_monitor/
├── __init__.py # from .routes import bp
├── routes.py # 16 条路由
├── services/{__init__,target_service,runner_service,report_service}.py
├── utils/{__init__,paths,crypto,check_modules,parser,thresholds,display_names,executor}.py
├── assets/{config.sh.template, common.sh, system/{01,02,03,04}*.sh, service/{20,22,24}*.sh}
├── data/{.gitkeep, reports/.gitkeep} # 运行时数据,gitignore
└── tests/{__init__,conftest,test_parser,test_crypto,test_check_modules,test_target_service,test_report_service,test_routes_sm}.py
templates/service_monitor/{index,targets,run,report}.html
```
### 修改的全局文件(仅这些)
- `server.py``from service_monitor import bp as service_monitor_bp`(占位时就改好)
- `skill/code/requirements.txt` — +paramiko +cryptography
- `skill/code/pytest.ini` — testpaths 加 web/service_monitor/tests
- `deploy/upload_to_server.py` — +RECURSIVE_DIRS_TO_UPLOAD + _upload_dir_recursive
- `.env.example` — +MONITOR_ENC_KEY
- `.gitignore` — +服务监测运行时数据忽略
- `CLAUDE.md` — 项目结构说明
### 已删除
- `skill/code/web/routes/service_monitor.py`(旧占位路由)
- `skill/code/web/templates/service_monitor.html`(旧占位模板)
---
## 8. 2026-07-27 会话进度追加
### 8.1 完成的工作
#### 定时任务并发执行失败修复 ✅
**问题**:5.44 服务器定时任务(工作日 8:40)不执行,5.202 的 9:00 正常。
**根因**
- APScheduler `misfire_grace_time` 默认 1 秒,调度线程稍有延迟就跳过任务
- `_execute_scheduled_job` 同步阻塞 APScheduler 线程
- `schedules.json` 并发写入无锁保护
**修复**`schedule_service.py` 5 处):
1. 新增 `_schedules_lock`
2. `_save_schedules()` 加锁
3. `update_run_status()` 读-改-写整体加锁
4. `_add_job()` 添加 `misfire_grace_time=3600``coalesce=True``max_instances=1`
5. `_execute_scheduled_job` 改为 `threading.Thread` 后台执行
**验证**:218 测试全绿 ✅
**文档**
- `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md`
- `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md`
#### Docker 容器化方案设计 ✅
**用户需求**
- 部署目录:`/data/third_party/monitor-platform/`
- 单容器部署(nginx + Flask + SQLite)
- 前端更新不需要容器重启,后端更新需要容器重启
- 数据库采用轻量级 SQLite
- 5.60 上有其他服务容器,必须隔离不互相影响
**已完成文件**
- `Dockerfile` — python:3.11-slim,单进程,CRLF 自动修复
- `.dockerignore` — 排除测试/运行时数据/文档
- `docker-compose.yml` — 单容器 + volume + 资源限制(1G/1CPU)
- `deploy/docker_deploy.sh` — 5.60 部署脚本
**隔离保障**
- 不挂载 docker.sock
- 容器名 `troubleshoot`,端口 8088
- volume 路径 `/opt/troubleshoot/` 独占前缀
#### 搜索引擎路径适配 ✅
- `utils/paths.py` 新增 `SEARCH_INDEX_DIR` 常量
- `search_engine.py` 改用统一路径
#### Vue 前端迁移规划 ✅
**文档**`Docs/需求文档/Flask模板迁移Vue前端任务清单.md`
**范围**:三大模块共 18 个页面
- 问题排查助手:3 个页面
- 服务监测:10 个页面
- 服务管理:4 个页面
**技术选型**:Vue 3 + Vite + Element Plus + Pinia + TypeScript + ECharts
**预估工时**:17-21 天(约 3-4 周)
**建议执行顺序**:先容器化部署落地,再并行推进 Vue 重构
### 8.2 待执行任务(历史,已部分完成)
| 优先级 | 任务 | 说明 | 状态 |
|--------|------|------|:----:|
| **P0** | 部署容器化版本到 5.60 | 需先确认 Docker 已安装,执行 `docker_deploy.sh` | ✅ 已上线 |
| **P0** | 更新 5.44 定时任务 end_date | 当前 end_date=2026-07-22 已过期 | ✅ 已更新为 2027-08-31 |
| P1 | 修复 APScheduler day_of_week 约定 bug | `from_crontab('1-5')` 实际是周二到周六 | ✅ 已修复 |
| P2 | SQLite 迁移 | 替换 JSON 文件存储 | ⏳ |
| P2 | Vue 前端开发 | 按任务清单执行 | ⏳ |
### 8.3 遗留问题(历史,已解决)
1. ~~**5.44 定时任务 end_date 已过期**(2026-07-22)~~ ✅ 已更新为 2027-08-31
2. ~~**APScheduler day_of_week 约定 bug**`CronTrigger.from_crontab('1-5')` 按 APScheduler 约定是周二到周六~~ ✅ 已修复(见 9.1)
3. ~~**容器化尚未部署**~~ ✅ 2026-08-13 已上线 5.60
### 8.4 本次新增文件
| 文件 | 说明 |
|------|------|
| `Dockerfile` | 重写:python:3.11-slim + 单进程 |
| `docker-compose.yml` | 重写:单容器 + volume + 资源限制 |
| `.dockerignore` | 更新:排除 tests/data 等 |
| `deploy/docker_deploy.sh` | 新增:5.60 部署脚本 |
| `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md` | 问题处理文档 |
| `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md` | 计划执行文档 |
| `Docs/需求文档/Flask模板迁移Vue前端任务清单.md` | Vue 迁移任务清单 |
---
## 9. 2026-08-17 会话进度追加
### 状态更新
**容器化部署已上线** ✅:
- 5.60 容器 `troubleshoot` 运行中,启动于 2026-08-13 12:25(北京时间)
- supervisor 管理 nginx + Flask 双进程
- 三个定时任务均正常工作,5.44 end_date 已更新为 `2027-08-31`
### 9.1 修复 APScheduler day_of_week 约定 bug ✅
**问题**`CronTrigger.from_crontab('1-5')` 按 APScheduler 约定 day_of_week 0=周一,1=周二…6=周日,导致 `1-5` 被解释为**周二到周六**,而非预期的周一到周五。
**实测验证**
- 2026-08-15(周六)三个任务均执行 → 确认 `1-5` 确实包含周六
- 修复前 `from_crontab('30 8 * * 1-5')` 首次触发:Tue 08-18
- 修复后 `from_crontab('30 8 * * 0-4')` 首次触发:Mon 08-17
**修复方案**`schedule_service.py`):
1. 新增 `_convert_dow_to_apscheduler(cron_expr)` 函数:将 cron 表达式的 day_of_week 字段统一 -1 偏移(标准 cron 1=周一 → APScheduler 0=周一)
2. 支持单值、范围、列表、混合格式
3.`_add_job()` 中调用转换函数,将转换后的表达式传给 `CronTrigger.from_crontab()`
**修改文件**
- `skill/code/web/service_monitor/services/schedule_service.py` — 新增 `_convert_dow_to_apscheduler` 函数 + `_add_job` 中调用
**新增文件**
- `skill/code/web/service_monitor/tests/test_schedule_service.py` — 15 个测试用例(11 转换 + 3 APScheduler 真实触发验证)
**验证**:全套 253 测试全绿 ✅
### 9.2 待办更新
| 优先级 | 任务 | 说明 |
|--------|------|------|
| P2 | SQLite 迁移 | 替换 JSON 文件存储 |
| P2 | Vue 前端开发 | 按任务清单执行 |
### 9.3 遗留问题
1. **4 个 routes 页面测试失败**`test_routes_sm.py::TestPages`):返回 200 而非 302,因前端改为 Vue SPA 后页面路由由 `index.html` 处理,无重定向。需后续更新测试断言。
### 9.4 本次新增/修改文件
| 文件 | 说明 |
|------|------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 修改:新增 `_convert_dow_to_apscheduler` 转换函数 |
| `skill/code/web/service_monitor/tests/test_schedule_service.py` | 新增:15 个 day_of_week 转换测试用例 |
---
## 10. 2026-08-29 会话进度追加(三问题修复 + 部署复测)
### 10.1 背景(为什么做)
用户连续反馈 3 个问题:
1. **触发巡检(如 5.44 全量巡检)完成后,日志里看不到钉钉通知发送的痕迹**("触发过会发通知吧?")
2. **手动重跑 5.44 定时任务,发送的内容特别少**(约 40 行,正常应几百项)
3. **每天 09:30 报告缺失告警误报**:把从未配定时任务的内置目标「本机(当前服务器)/local」报成"2 天无新报告(上次:无报告)"
### 10.2 根因与修复(全部已部署复测)
#### ✅ 根因 1:service_monitor 孤儿 logger(通知日志不可见)
**现象**:钉钉机器人实际**一直在发**(errcode 0 成功),只是 `钉钉通知已发送` 这行日志永远看不到。
**根因**`service_monitor/` 9 个模块用裸 `logging.getLogger("service_monitor.X")`,脱离了 `utils/logger.py``troubleshoot` 命名空间(根 logger 有 StreamHandler+RotatingFileHandler)。孤儿 logger 的事件 propagate 到全局 `root` logger(默认 WARNING、无 handler),**INFO/WARNING 全部静默丢弃**
**修复**:8 个文件头部 `import logging` + `logging.getLogger(...)``from utils.logger import get_logger` + `logger = get_logger("service_monitor.X")`(自动归入 `troubleshoot` 命名空间):
- `routes.py``utils/check_modules.py``services/{target_service, notification_service, report_service, statistics_service, compare_service, schedule_service}.py`(schedule 此前已正确)
- `utils/crypto.py` **故意不改**:其 docstring 声明"模块自包含,不依赖全局 utils/logger",自带 StreamHandler,保持隔离
**验证**
- 容器内 import 探针:9/9 模块 logger.name 均为 `troubleshoot.service_monitor.*`,parent=troubleshoot ✅
- 本地 PROBE 日志写入 app.log ✅
#### ✅ 根因 2:bash 资产 CRLF 导致模块输出几乎全空("发送内容少")
**现象**:5.44 full 巡检 01_system_basic 输出被截断到 ~40 行,主机名/IP/内存等项缺失。
**根因**:Windows 上传的 `assets/*.sh` 是 CRLF,Linux bash 执行时把 `\r` 当命令内容的一部分,大量命令失效。
**修复**(双层):
- `utils/executor.py` 上传/布置脚本时强制 LF:`dst.write_bytes(data.replace(b"\r\n", b"\n"))`(write_bytes 3 处 + config write_text,见 325/445 行附近)
- 本地 assets `.sh`/`.template` 统一转 LF(git 已跟踪)
**验证**:5.44 full 复测 = **42 模块 / 509 项**(正常 486 / 警告 10 / 严重 13),`01_system_basic` 18 项完整(主机名、内核、负载、CPU 核心等全有值)✅
#### ✅ 根因 3:check_missing_reports 每日误报内置目标
**现象**:09:30 报告缺失告警误报 `本机(当前服务器)/local` 无报告。
**根因**`check_missing_reports`**所有**目标做缺失判定,内置目标从未配定时任务 → 永远"无报告"。
**修复**`report_service.py:838` check_missing_reports):仅检查**启用了定时任务**的目标(`schedule_service.list_schedules()` 中 enabled 且 target_id 非空);schedules.json 读取失败回退为检查全部目标(保持原行为兜底)。
**验证**
- 新增 4 个单测(`test_report_service.py`):仅定时目标参与判定 / 无定时任务返回空 / 最近有报告不缺失 / 过期报告 missing_days=真实天数
- 容器内实测 `check_missing_reports(2)` 返回 **0 个缺失**(之前会报 local)✅
- 全套 257 测试全绿 ✅
#### ✅ 附带:通知发送结果日志(runner_service)
`run_inspection_sync` 通知块增加结果日志(`runner_service.py:376-403`):
```
巡检完成通知:已发送(report_id=…)
未找到报告 …,跳过
发送通知失败: …
连续异常告警检查失败: …
```
配合根因 1 的 logger 修复,通知结果现在**可在 app.log 留痕验证**
### 10.3 部署与复测(关键流程,接手者复用)
**生产拓扑**(5.60,2026-08-29 已确认):
- 容器 `troubleshoot``troubleshoot:latest`,supervisord 管 nginx:80 + Flask:8088,健康检查 `curl http://localhost/api/health`
- 端口映射 `8088→80`(nginx)
- 镜像真实 build 源:**`/data/third_party/monitor-platform/`**`skill/code/web/` + `skill/code/requirements.txt` + `frontend/dist/`
- `docker-compose.yml` 在该目录,`docker compose up -d --build` 重建
- volume:monitor-data→`/app/web/service_monitor/data`、logs→`/app/web/logs`、users.json、dist、搜索索引.json(只读)
**本次部署方式**(热更新,未重建镜像):
1. 18 个改动文件(代码+assets)LF 同步到 build 源 `/data/third_party/monitor-platform/skill/code/web/service_monitor/`,md5 18/18 校验
2. `docker cp` 逐文件注入容器 `/app/web/service_monitor/`,清 `__pycache__``docker restart troubleshoot`
3. 容器内 md5 复验 MATCH + logger 探针 9/9 归位 + health healthy
**复测**(5.44 full,report `20260829_034555_f7ee98`):
- `POST /api/service-monitor/schedules/sched_042074c6-5e2/run` 触发(管理员登录 + Cookie)
- 巡检 4 分钟(03:41:53→03:45:55)→ 报告 509 项
- app.log 关键行:
```
保存报告: 20260829_034555_f7ee98 (目标=新统一平台5.44, 套件=full, 项=509)
报告保存完成: 20260829_034555_f7ee98
钉钉通知已发送 ← 之前永远不可见
巡检完成通知:已发送(report_id=20260829_034555_f7ee98)
巡检执行完成: success=True
```
### 10.4 当前状态
- **代码已改完 + 本地 257 测试全绿 + 生产容器已热更新并复测通过**
- **改动尚未 git 提交**(分支 `troubleshoot-ai-assistant`,勿 merge master)
- 待办表更新:SQLite 迁移 / Vue 前端(P2,历史遗留,与本批无关)
### 10.5 本次改动文件清单
| 文件 | 改动 |
|------|------|
| `service_monitor/services/notification_service.py` | logger 迁移(通知发送本就正常,只是日志不可见) |
| `service_monitor/services/runner_service.py` | logger 迁移 + 通知结果日志块 |
| `service_monitor/services/report_service.py` | logger 迁移 + check_missing_reports 仅查定时目标 |
| `service_monitor/services/{schedule_service,target_service,statistics_service,compare_service}.py` | logger 迁移 |
| `service_monitor/routes.py`、`utils/check_modules.py` | logger 迁移 |
| `service_monitor/utils/executor.py` | 上传/布置脚本强制 LF(CRLF 根因) |
| `service_monitor/assets/*.sh` + `config.sh.template`(8 个) | 统一转 LF |
| `service_monitor/tests/conftest.py` | tmp_data 补 `schedule_service.SCHEDULES_FILE` patch |
| `service_monitor/tests/test_report_service.py` | +4 check_missing 单测 |
### 10.6 踩坑(本次新增,务必注意)
1. **孤儿 logger 是最隐蔽的坑**:模块里用 `logging.getLogger()` 拿到的事件若不在 `troubleshoot` 命名空间,会被全局 root logger(WARNING 无 handler)**静默丢弃**——功能正常但日志全无。排查"日志不可见"先验 `logger.name` 前缀 + `parent`。
2. **部署热更新必须同步 build 源**:容器 `/app/web/` 是**镜像层**,`docker cp` 只改运行中容器;不同步 `/data/third_party/monitor-platform/` 会在下次 `--build` 时**回滚**。同理 `/opt/troubleshoot` 是 nohup 旧运行源,也要防 systemd 回退路径用旧代码。
3. **urllib POST 被降级为 GET**:urllib 复用连接时对无 body 的 POST 可能自动发 GET → 拿到 404 而非 405/200。排查 404 先看 nginx access log 的 method;用 raw socket 显式 POST 即可(本次 `RAW POST 200 triggered=true`)。
4. **报告文件路径**:`/app/web/service_monitor/data/reports/<rid>.json`(注意实际在 reports/ 根,不是 reports/index/reports/)。
5. **Windows 控制台打印容器输出**:GBK 控制台遇 emoji/中文会 UnicodeEncodeError,脚本内 `sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')` 包裹;复杂引号命令用 `base64` payload 传参。
### 10.7 下一步(优先级排序)
1. **git 提交本批改动**:`troubleshoot-ai-assistant` 分支(勿 merge master),Conventional Commits,例如 `fix(service-monitor): 修复通知日志不可见/CRLF 输出缺失/报告缺失误报`,并同步 `Docs/需求文档/服务监测/HANDOFF.md` 的提交记录
2. **考虑重建镜像**:本次是 docker cp 热更新,长期稳妥做法是 `docker compose up -d --build`(build 源已同步,重建后代码一致)
3. **回归确认定时任务**:周一到周五 08:30(5.44)自动巡检时,验证 app.log 有 `巡检完成通知:已发送` + `钉钉通知已发送`,且**不再**出现 09:30 报告缺失误报
4. **清理临时探针脚本**:仓库根/`deploy/` 下 `check_*.py`、`_diag_*.py`、`debug_notification.py` 等临时文件按需归档或删除
5. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
### 10.8 本次验证用的关键命令
```bash
# 触发 5.44 full 巡检(管理员 Cookie + 显式 POST)
POST /api/service-monitor/schedules/sched_042074c6-5e2/run
# 看通知日志(容器内)
docker exec troubleshoot tail -50 /app/web/logs/app.log | grep -E '通知|保存报告|巡检执行完成'
# 验证 check_missing(容器内)
docker exec troubleshoot python3 -c "$(echo <b64> | base64 -d)" # from service_monitor.services import report_service; check_missing_reports(2)
```
---
## 11. 2026-08-31 会话进度追加(报告链接免登访问 + 生产 docker compose 重建)
### 11.1 背景(为什么做)
用户反馈:**钉钉通知里的"完整报告"链接**(含 `?token=...`)在未登录浏览器点击后,
被重定向到登录页并提示"登录已过期"。
复现链接示例:
```
http://192.168.5.60:8088/login?next=/service-monitor/report/20260831_010950_bce779?token=rpt_f129...
```
### 11.2 根因(三层鉴权链路都挡在访客前)
1. **前端路由守卫**:Vue Router 把 `/service-monitor/report/:id` 视为必须登录页面,未登录一律跳登录(`next` 还带了 `?token=`
2. **后端详情 API**`api_get_report` 只认 session,访客请求直接 401
3. **Axios 全局 401 拦截器**:把 token 访客的 401 也当"登录已过期",再跳一次登录页 → 用户看到误导性提示
### 11.3 修复方案(只放读,不扩权)
| 层 | 改动 | 要点 |
|----|------|------|
| 后端 `routes.py` | `api_get_report` 优先校验 `request.args['token']` | 有效 token 允许**只读**详情访问;token 缺失/无效回退到原登录校验 |
| 后端 | 删除/批量删除/对比等**写操作不动** | token 不获得任何写权限,权限范围不扩大 |
| 前端 `router/index.ts` | 路由守卫仅放行 `MonitorReportDetail` **且带 token** 的请求 | 其他带 token 页面仍走正常认证 |
| 前端 `api/report.ts` | `getReport(id, token?)` 把 token 作为 **query params** 发送 | 用参数对象避免手工拼接编码问题 |
| 前端 `ReportDetail.vue` | 详情加载时把当前路由 token 传给 `getReport`;导出链接统一 URL 编码 | token 特殊字符不破坏查询串 |
| 前端 `utils/http.ts` | 401 分支区分"报告 token 访客" vs "会话失效" | token 访客显示「报告链接无效或已过期」,**不**跳登录不清 session |
### 11.4 测试(263 → 269 全绿)
- `test_routes_sm.py` 新增 `TestReportTokenAccess` 类 4 用例:
- 无 session + 有效 token → 详情 200
- 缺 token / 错误 token → 401 需登录
- 过期 token → 401
- token **不能**删除报告(写权限未扩大)
- `test_report_service.py` 新增 token 校验单测(有效/错误/过期/缺字段)
- 全套 pytest **269 用例全绿**(本机本地环境)
- 前端:`vue-tsc --noEmit` 类型检查通过 + 生产 build 成功(dist 含修复 bundle)
### 11.5 部署(本次为**镜像重建**,非热更新)
生产拓扑延续 10.3:build 源 `/data/third_party/monitor-platform/` + `docker compose up -d --build`
步骤:
1. 本机构建新前端 dist(含修复 bundle)
2. 同步 build 源:后端改动文件 + 新 `frontend/dist/` 上传到 `/data/third_party/monitor-platform/`
3. 服务器 `docker compose up -d --build` 重建镜像重启容器(**不是** `docker restart`
4. 容器内部 md5/代码版本校验 + 健康检查
### 11.6 生产复测结果(已验证 ✅)
- **访客免登**:无登录 Cookie 直接打开 `report/20260831_010950_bce779?token=...` → 进入报告详情(不再重定向登录)
- **详情 API**:带 token 访客请求返回 200
- **导出**:带 token 访客导出 MD/JSON 仍可用
- **错误/过期 token**:显示「报告链接无效或已过期」,不误报"登录已过期"、不跳登录
- **权限未扩大**:无 token / token 错误时未登录访问详情仍 401 或跳登录;删除/写操作仍需要管理员登录
- **普通页面回归**:未登录访问报告列表等其他服务监测页面仍跳转登录
### 11.7 git 提交
- 已提交并推送:**`07d17632`**
`fix(service-monitor): 报告链接免登访问 + 钉钉链接直达报告详情`
- 后端 token 只读鉴权、前端路由守卫/API/401 拦截器区分访客 token、导出 URL 编码
- 测试 269 全绿、dist 重建、生产 docker compose 重建并复测通过
- 分支 `troubleshoot-ai-assistant`**勿 merge master**
### 11.8 本次改动文件清单
| 文件 | 改动 |
|------|------|
| `skill/code/web/service_monitor/routes.py` | `api_get_report` 支持 token 只读访问 |
| `frontend/src/router/index.ts` | 守卫放行带 token 的报告详情 |
| `frontend/src/api/service-monitor/report.ts` | `getReport(id, token?)` query 传参 |
| `frontend/src/views/service-monitor/ReportDetail.vue` | token 传入详情/导出 |
| `frontend/src/utils/http.ts` | 401 拦截器区分访客 token |
| `skill/code/web/service_monitor/tests/test_routes_sm.py` | +TestReportTokenAccess 4 用例 |
| `skill/code/web/service_monitor/tests/test_report_service.py` | +token 校验单测 |
| `frontend/dist/` | 生产 build(含修复 bundle) |
| `Dockerfile` 等 | 重建相关微调 |
### 11.9 踩坑 / 注意(本次新增)
1. **token 是能力边界**:token 只应该解锁"读报告详情/导出",任何写操作(删除、批量删除、对比、触发巡检)绝不能因 token 放行——改权限要当安全变更对待,回归测试必须覆盖"token 无权写"。
2. **Axios 401 双语义**:同一个 401 状态码可能是"访客 token 失效"或"登录过期",处理方法完全不同(前者显示链接失效、后者清 session 跳登录)。用 `error.config.params.token` / URL 含 `token=` 区分,且**不要在 token 分支清除用户会话**
3. **前端 build 产物是部署的一部分**:改前端源码后必须重新 build 并替换 `frontend/dist/`,再同步 build 源重建镜像;只改源码不重建,线上仍是旧 bundle。
4. **路由守卫判断抽纯函数更可测**:守卫里"是否 token 访客报告页"的判断逻辑建议独立成纯函数,便于后续前端单测(当前项目无前端测试框架,本次用 vue-tsc + build 验证)。
### 11.10 下一步(优先级排序)
1. **回归确认定时任务**:下次工作日定时巡检(5.44 08:30)后,确认钉钉"完整报告"链接访客可直达(本轮已修)
2. **考虑给 token 加查看限制**(可选):报告 token 长期有效,若担心泄露,可加有效期/访问次数/按报告自定义失效时间(现状:14 天报告清理时一并过期)→ **已评估,维持现状,见 12.1**
3. **清理临时探针脚本**:仓库根/`deploy/` 下临时 `check_*.py``_diag_*.py` 等按需归档或删除(10.7 遗留)→ **已清理,见 12.2**
4. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
---
## 12. 2026-08-31 会话进度追加(token 限制评估 + 清理临时探针脚本)
### 12.1 报告 token:评估结论 — 维持「仅有效期限制」,不加访问次数
**需求**:11.10 提到的可选「给 token 加查看限制」。
**评估结论**(与用户确认):**不加访问次数限制,维持现有有效期机制**
- token 有效期 `ACCESS_TOKEN_DAYS = 7``report_service.py:33`
- `validate_access_token()``report_service.py:604`)校验格式/报告绑定/有效期
- 报告保留 14 天,`cleanup_expired()` 到期删除报告,token 随之失效
- **本轮不改任何代码**`report_service.py` / `routes.py` / 前端均不动)
后续如需收紧(如加访问次数/按报告自定义失效),方案已备好,见 12.4 备忘,接入时注意并发原子消费与历史报告兼容。
### 12.2 清理临时探针脚本(已完成 ✅)
**范围**:只删纯探针(一次性诊断/排查脚本,全部未跟踪、无正式引用),保留可能复用脚本。
**已删除**(未 git 跟踪,删除不影响历史):
- **仓库根目录**:22 个 `check_*.py`(app_log/code_version/container_status/data_dir/docker_logs/flask_*/logs_direct/monitor_data/reports/running_process/running_scheduler/schedule_issue/scheduler_*/supervisor*/task_result/threads)、`debug_notification.py``final_check.py``final_diagnosis.py``test_docker_on_989.sh``test_scheduler2/4/_init.py``test_sudo_config.py`
- **deploy/**`_diag*.py`(14 个)、`_debug_500.py``_deploy_and_test.py``debug_inspect*.py`(4 个)、`_probe_544_candidates.py``_run_diag*.py`(3 个)、`_run_verify_560.py``_verify_sign_fix.py``_final_check.py``_final_verify.py``cat_log.py`、未跟踪 `check_*`(inspect_error/local_ssh/logs/report/run_logs/schedules/ssh_config/sudo,8 个)、一次性 `test_*`(full_inspect/pdf/pdf2/quick_inspect/sse/ssh)、`verify_69_deploy.py`
- **deploy/tmp_test/** 整个目录(19 文件 ~5.8MB,含 5.44/5.60 抓包 bundle `app_544_latest.js`/`app_backstage.js`
**明确保留**(勿再删):
- 已跟踪正式文件:`deploy/check_service.py``deploy/build_index.py``deploy/upload_to_server.py``deploy/verify_deployment.py``deploy/deploy_docker.py``deploy/deploy_service_manage.py``deploy/verify_new_features.py``deploy/deploy_to_69.py``skill/code/web/service_monitor/utils/check_modules.py`
- 可能复用脚本(用户确认保留):根目录 `api_trigger.py``trigger_*``deploy_fix*.py``download_*.py``fix_*.py``force_rebuild.py``rebuild_and_deploy.py``restart_and_verify.py``deploy/``deploy_frontend*.py``deploy_to_560.py``upload_backend.py``upload_dist.py``upload_docs*.py`(含 v2~v10/final/final2/win)、`upload_skill.py``encode_docs.ps1``upload_docs.ps1`
**验证**
- 删除前逐文件 `git grep` 复核引用:仅 `HANDOFF.md` 自身提及 `debug_notification`/`check_container_status`;bash 资产中的 `check_app_log_errors()`/`check_container_status()` 是函数名,与根目录探针脚本无关,不误删
- `git status` 无已跟踪文件被删(无 `D` 状态);保留文件全部在位
- 全套 pytest **269 用例全绿**(纯删未跟踪脚本,无影响)
### 12.3 踩坑 / 注意(本次新增)
1. **探针脚本多为硬编码明文密码**`Ubains@123`,违反 CLAUDE.md「密码禁止硬编码」):本次删除的 `_diag*`/`debug_*`/`check_*` 系列绝大多数含明文凭据,删掉顺带降低泄漏面。**今后排查勿再在仓库根/`deploy/` 裸建诊断脚本**,如需临时探针放 `.tmp/` 并加入 `.gitignore`,且禁止硬编码密码。
2. **bash 资产里的同名函数不是探针**`assets/service/*.sh``check_app_log_errors()`/`check_container_status()` 是监测模块函数,与根目录探针脚本无引用关系——清理时按「文件名 + git grep」判断,勿按关键词批量删。
### 12.4 备忘:token 加访问次数的备用方案(当前未做)
若后续要加访问次数限制(用户当前已确认不加):
- `report_service.py` 增加 `ACCESS_TOKEN_MAX_USES` 常量 + `save()``access_token_used_count`/`access_token_max_uses`/`access_token_issued_at`/`access_token_invalidated_at`
- `validate_access_token()` 改为「校验 + 原子递增」(进程内锁 `_token_lock` 包住读-判-增-写,单进程部署适用),成功后消费一次,失败请求不计
- `cleanup_expired()` 对「报告仍保留但 token 已过期/耗尽」仅清 token 不删报告
- 兼容:旧报告无新字段按原有效期校验,不重生成 token;详情/导出共用额度,登录访问不计入
### 12.5 下一步(优先级排序)
1. **回归确认定时任务**:下次工作日定时巡检(5.44 08:30)后确认钉钉链接访客可直达、`钉钉通知已发送` 留痕、09:30 无误报
2. **git 提交本次清理 + HANDOFF 更新**(分支 `troubleshoot-ai-assistant`,勿 merge master)→ **✅ 已完成:eebe75d4** `docs(monitor): 记录 token 有效期限制评估结论并清理临时探针脚本`(仅提交 HANDOFF.md)
3. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
---
## 13. 2026-09-01 会话进度追加(提交 HANDOFF 更新)
### 13.1 本次完成
- **已提交推送到 `troubleshoot-ai-assistant`**`eebe75d4` `docs(monitor): 记录 token 有效期限制评估结论并清理临时探针脚本`
- 仅提交 `Docs/需求文档/服务监测/HANDOFF.md`(12.1/12.2 token 评估结论 + 探针清理清单)
- 探针脚本本身未跟踪,删除无需入库
- 全套 pytest **269 用例全绿**
- 未 merge master
### 13.2 工作区遗留未提交(下次提交范围参考,已审查分类)
**✅ 应提交(服务监测正式代码,已测试,改动实质存在):**
- logger 迁移:`service_monitor/services/{compare_service,notification_service,report_service,runner_service,schedule_service,statistics_service,target_service}.py``utils/check_modules.py`(裸 `logging.getLogger``utils.logger.get_logger`,纳入 `troubleshoot` 命名空间,修复通知日志不可见)
- CRLF 修复:`utils/executor.py`(上传/布置脚本强制 LF)+ `assets/*.sh`/`config.sh.template`(统一转 LF,修复 5.44 巡检输出缺失)
- 功能:`report_service.py``check_missing_reports` 仅查启用定时任务的目标,修复 09:30 内置目标误报)、`runner_service.py`(通知结果日志 + 僵尸巡检清理 `_cleanup_stale_runs`)、`schedule_service.py``_convert_dow_to_apscheduler` 星期偏移)
- 测试:`tests/conftest.py``tests/test_schedule_service.py`(新增 15 用例,未跟踪建议提交)
- 服务管理:`services/five44_client.py`(签名 body 手动序列化 separators 紧凑 + Authorization 恒带)、`services/service_manage.py`(save_project 5.44 同步失败不阻塞本地保存)
**⚠️ 提示:**
- `Dockerfile`(apt 链 `;` 兜底)、`.gitattributes`(.sh/template 强制 LF)、`Docs/服务管理/HANDOFF_服务管理.md``Docs/部署手册/deploy_to_69_operation_manual.md``HANDOFF_容器部署_5.69.md` 等——属其他模块/部署文档,按需另行提交
- `frontend/tsconfig.app.tsbuildinfo`(build 产物,建议忽略或随前端 build 提交)
- `skill/code/web/users.json`(last_login 运行时变更,建议不提交)
- 未跟踪:`api_trigger.py``trigger_*``fix_*``download_*``deploy_fix*``upload_docs*`(v2~v10/final/final2/win)、`deploy_frontend*``deploy_to_560.py``deploy_to_69.py` 等运维脚本(用户已确认保留,勿删,是否入库视需要)
- `Docs/维护手册/`(111MB docx 文档库)、`skill/code/搜索索引.json`/`搜索向量.json`(运行时生成 1KB 占位)、`skill/code/web/service_monitor/data/report_index.json``deploy/tmp_upload/``frontend/deploy/``skill/code/service_manage_data/``.verify_cookies.txt``deploy/nohup_output.txt``deploy/docs_list.txt`——均未跟踪;如需入库先加 `.gitignore`/确认用途(维护手册 111MB 不宜入库,建议外部管理)
### 13.3 下一步(优先级排序)
1. **回归确认定时任务**:下次工作日定时巡检(5.44 08:30)后确认钉钉链接访客可直达、`钉钉通知已发送` 留痕、09:30 无误报(历史遗留,持续有效)
2. **提交遗留服务监测源码改动**:按 13.2 分类,`fix(service-monitor): 修复通知日志不可见/CRLF 输出缺失/报告缺失误报/僵尸巡检`(建议包 logger 迁移 + CRLF + check_missing + runner + schedule + 测试);`fix(service-manage): 5.44 签名序列化修复 + save_project 失败不阻塞`(服务管理另立)
3. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
---
## 14. 2026-09-01 会话进度追加(5.202 定时任务 end_date 过期:修复 + 部署 + 运维恢复)
### 14.1 问题背景与根因(已与用户确认)
- 5.202 定时任务 `sched_7cd80b06-09f`(工作日 08:50 全量巡检)`end_date=2026-08-31` 已过期
- 记录显示"执行了"且"成功"(陈旧 `last_run_at: 2026-08-31T08:54:49` / `current_status: success`),但 09-01 起**不再触发**:APScheduler 对 end_date 已过的 job 注册为 `next_run_time=None`(日志 `schedule_sched_7cd80b06-09f: next_run=None`
- 代码健壮性缺口:`_calc_next_run` 只看 cron 下一跳、不看 end_date → 页面显示"下次执行 09-01 08:50"误导;前端无"已过期"状态
### 14.2 代码修复(已提交推送 7249d355)
- `_calc_next_run(cron_expr, end_date)`:end_date 已过或下一跳超期返回 `None`
- 新增 `_parse_end_date` / `_is_schedule_expired``list_schedules` 附加只读派生字段 `is_expired`
- 前端 `Schedule.vue`:过期显示"已过期"徽标、"下次执行:已失效"、禁用"启用"按钮;`types/service-monitor.ts``is_expired?`
- 测试:`test_schedule_service.py` 新增 8 用例(end_date 过期 None / 有效期内正常 / 无 end_date 不变 / 同天仍执行 / is_expired 判断)——该文件未跟踪,随本提交一并入库
- 文档:`Docs/需求文档/服务监测/PRD_问题处理_5.202定时任务end_date过期.md` + `PRD_计划执行_5.202定时任务end_date过期修复.md`
- 注:本提交的 `schedule_service.py` 为最新版(含此前未提交的 logger 迁移 / `_convert_dow_to_apscheduler`),13.2 遗留清单中该文件改动随之清零
### 14.3 部署 5.60(热更新,已完成)
- **后端**:LF 转码 → 同步 build 源 `/data/third_party/monitor-platform/``docker cp` 注入 `/app/web/` → 清 `__pycache__``docker restart troubleshoot`
- **前端**:新 `frontend/dist/`(80 文件,含 `Schedule-C4EnHSuH.js``is_expired`)→ `/opt/troubleshoot/dist`(bind volume,nginx root `/data/dist`
- **复验**:容器 healthy、`/api/health` 200、容器内 `schedule_service.py` md5 与本地一致
- 部署脚本:`deploy/deploy_560_enddate_fix.py`(未跟踪,可复用)
### 14.4 生产复验(登录 API 实测)
| 字段 | 部署后(end_date 未改) | 延长 end_date 后 |
|------|----------------------|------------------|
| `is_expired` | `true`(新字段生效) | `false` |
| `end_date` | `2026-08-31` | `2027-08-31` |
| `next_run_at` | `2026-09-01T08:50:00`(列表不主动清空,靠 is_expired 展示"已失效") | `2026-09-02T08:50:00` |
**运维恢复**:已通过 API 把 5.202 `end_date` 延长至 `2027-08-31`,下次自动执行 2026-09-02(周三)08:50。与 5.44/9.89 对齐。
### 14.5 踩坑 / 注意(本次新增)
1. **`/api/health` 返回 HTML 而非 JSON 是正常现象**:nginx 将 `/api/health` 之外的未知路径按 SPA 回退到 `index.html`,且 health 探针只认 HTTP 200。判断部署成功应看 `docker ps` 状态 healthy + `curl -sf` 返回 200,**不要**解析 health 响应体。
2. **前端 dist 热更新权限陷阱**:首次用脚本 `mv` 旧 dist 备份时,`mv` 目标目录由**根目录继承权限变 root**,容器内 nginx(非 root worker)无法读 → 前端 404 白屏。修复:`chown -R ubains:ubains` + `chmod -R u+rwX` 后再传文件;备份改用 `cp -a` 而非 `mv`(保留目录本身权限)。
3. **`list_schedules` 的 `is_expired` 是派生字段不落盘**`next_run_at` 在过期后仍是上次计算值,前端靠 `is_expired` 显示"已失效"兜底,勿依赖 `next_run_at=None` 判断过期。
### 14.6 下一步(优先级排序)
1. **回归确认定时任务**:2026-09-02 08:50 5.202 自动巡检后,确认新报告 + 钉钉通知 + app.log `巡检完成通知:已发送` 留痕(与 5.44 08:30 同一机制)
2. **提交本次 HANDOFF 更新**(本会话将提交,分支 `troubleshoot-ai-assistant`,勿 merge master)
3. **提交遗留服务监测源码改动**(13.2 剩余:logger 迁移其余文件、CRLF、check_missing、runner 等)
4. (历史遗留 P2)SQLite 迁移 / Vue 前端开发
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论