提交 248d6328 authored 作者: 陈泽健's avatar 陈泽健

fix(service-monitor): 修复定时任务并发执行失败 + Docker 容器化方案

定时任务修复:
- 新增 _schedules_lock 防止 schedules.json 并发写入状态丢失
- _add_job() 添加 misfire_grace_time=3600/coalesce/max_instances
- _execute_scheduled_job 改为 threading.Thread 后台执行,不阻塞 APScheduler 线程

Docker 容器化:
- 重写 Dockerfile(python:3.11-slim + 单进程 + CRLF 修复)
- 重写 docker-compose.yml(单容器 + volume + 资源限制 + 健康检查)
- 新增 deploy/docker_deploy.sh 部署脚本
- 更新 .dockerignore 排除规则

搜索索引路径适配:
- utils/paths.py 新增 SEARCH_INDEX_DIR 常量
- search_engine.py 改用统一路径

文档:
- 新增定时任务问题处理 + 计划执行文档
- 新增 Vue 前端迁移任务清单
- 更新 HANDOFF 交接文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 22368d9f
...@@ -22,9 +22,28 @@ HANDOFF.md ...@@ -22,9 +22,28 @@ HANDOFF.md
# 测试与覆盖率 # 测试与覆盖率
skill/code/tests skill/code/tests
skill/code/.pytest_cache skill/code/.pytest_cache
skill/code/web/service_monitor/tests
.coverage .coverage
htmlcov htmlcov
# 本地数据与日志(挂载方式注入,不入镜像) # 运行时数据(volume 挂载,不打入镜像)
logs skill/code/web/service_monitor/data
cache skill/code/web/cache
skill/code/web/logs
skill/code/web/audit.log
skill/code/web/users.json
# 部署脚本(构建镜像不需要)
deploy/
# IDE
.vscode
.idea
# 环境变量
.env
.env.local
# Docker 自身
Dockerfile
docker-compose.yml
FROM python:3.10-slim # ============================================================
# Troubleshoot AI Assistant — Docker 镜像
# ============================================================
# 基础镜像:python:3.11-slim(Debian bookworm,含 bash)
# 运行时:Flask 内置 server(单进程,APScheduler 兼容)
# 数据持久化:通过 volume 挂载,不写入镜像层
# ============================================================
# 工作目录设为 /app/web,与 utils/paths.py 的 SCRIPT_DIR 推导一致 FROM python:3.11-slim AS base
# gunicorn 直接加载 server 模块(server.py:107 已有模块级 app = create_app())
WORKDIR /app/web # 系统依赖:bash(LocalExecutor 执行检测脚本)
# 不装 docker CLI —— 不挂载 docker.sock,避免影响宿主机其他容器
RUN apt-get update && apt-get install -y --no-install-recommends \
bash \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# 安装依赖(版本锁定,离线环境复现性保障) # ---------- 依赖层(利用 Docker 缓存) ----------
COPY requirements.txt /app/requirements.txt COPY skill/code/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt
# 拷贝代码 # ---------- 代码层 ----------
COPY skill/code/web/ /app/web/ COPY skill/code/web/ /app/web/
# 复制知识库 SKILL.md(问题排查助手 prompt 模板)
COPY skill/code/SKILL.md /app/SKILL.md COPY skill/code/SKILL.md /app/SKILL.md
# 端口 # 运行时数据目录(volume 挂载点,容器内不写数据到镜像层)
EXPOSE 8088 RUN mkdir -p /app/data \
&& mkdir -p /app/web/service_monitor/data/reports \
&& mkdir -p /app/web/cache \
&& mkdir -p /app/web/logs
# 修复 Windows 开发环境产生的 CRLF 行尾(bash 脚本在 Linux 必须是 LF)
RUN find /app/web/service_monitor/assets -name '*.sh' -o -name '*.template' \
| xargs -r sed -i 's/\r$//'
# 环境变量 # 环境变量
ENV PYTHONIOENCODING=utf-8 ENV FLASK_DEBUG=0 \
PYTHONIOENCODING=utf-8 \
TROUBLESHOOT_ROOT=/app \
LANG=C.UTF-8
EXPOSE 8088
WORKDIR /app/web
# 启动:gunicorn 多 worker # 单进程启动(不用 gunicorn 多 worker,避免 APScheduler 多实例冲突)
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8088", "server:app"] CMD ["python3", "server.py"]
# Flask 模板迁移到 Vue 前端 — 工作任务清单
> 版本:1.0 | 日期:2026-07-27 | 作者:Claude
---
## 1. 背景与目标
### 现状
- 前端全部是 Flask 模板渲染的 HTML(`templates/*.html`
- CSS/JS 内联在模板中,无组件化开发体验
- ECharts 从 CDN 加载(离线环境不可用)
- 前后端耦合,更新前端需重建镜像或 volume 挂载模板
### 目标
- 新建 Vue 3 前端项目,实现前后端分离
- Flask 只提供 REST API,不再渲染页面
- 前端编译后的静态文件(`dist/`)由 nginx 服务
- 前端更新只需替换静态文件,无需重启容器
- 保持现有功能和 UI 一致,不新增功能
- 覆盖所有三个模块:问题排查助手、服务监测、服务管理
---
## 2. 技术选型
| 组件 | 选型 | 理由 |
|------|------|------|
| 框架 | Vue 3 + Composition API | 最新稳定版,组合式 API 更灵活 |
| 构建工具 | Vite | 比 Vue CLI 更快,官方推荐 |
| UI 组件库 | Element Plus | 适合后台管理系统,组件丰富,文档完善 |
| 图表 | ECharts(npm 安装) | 当前用 ECharts,保持一致;打包进 bundle 避免 CDN 依赖 |
| HTTP 客户端 | Axios | 主流选择,拦截器支持好 |
| 路由 | Vue Router | 官方路由 |
| 状态管理 | Pinia | Vue 3 官方推荐,比 Vuex 更简洁 |
| 样式 | SCSS + Element Plus 主题 | 支持变量、嵌套,便于定制主题 |
| 语言 | TypeScript | 类型安全,IDE 支持好 |
---
## 3. 现有页面清单(三大模块共 18 个页面)
### 模块一:问题排查助手(3 个页面)
| 模板 | 路由 | 功能 | 复杂度 |
|------|------|------|--------|
| `login.html` | `/login` | 登录页(用户名/密码 + 记住我) | 低 |
| `platform.html` | `/` | 平台首页(三个模块卡片入口) | 低 |
| `index.html` | `/troubleshoot` | 问题排查主页(搜索框 + 知识库匹配 + AI 分析流式输出) | **高** |
**关键功能**
- SSE 流式输出 AI 分析结果
- 知识库匹配结果高亮
- 历史记录侧边栏
---
### 模块二:服务监测(10 个页面)
| 模板 | 路由 | 功能 | 复杂度 |
|------|------|------|--------|
| `base.html` | - | 基础布局(侧边栏 + 顶部栏 + 内容区) | 中 |
| `index.html` | `/service-monitor` | 监测主页(目标卡片 + 最近报告列表) | 中 |
| `targets.html` | `/service-monitor/targets` | 目标管理(CRUD 弹窗 + 连通性测试) | 中 |
| `run.html` | `/service-monitor/run` | 巡检执行(SSE 进度条 + 模块清单实时更新) | **高** |
| `report.html` | `/service-monitor/report/:id` | 报告详情(汇总卡片 + 异常置顶 + 分模块折叠) | 高 |
| `reports.html` | `/service-monitor/reports` | 报告列表(目标筛选 + 批量删除) | 中 |
| `statistics.html` | `/service-monitor/statistics` | 监测统计(ECharts 多图表 + 筛选) | **高** |
| `schedule.html` | `/service-monitor/schedule` | 定时任务管理(CRUD + 启用/禁用 + 立即执行) | 中 |
| `compare.html` | `/service-monitor/compare` | 报告对比(两份报告并列对比差异) | 中 |
| `notification.html` | `/service-monitor/notification` | 通知配置(钉钉/企业微信/邮件 + 测试发送) | 中 |
**关键功能**
- SSE 实时进度(巡检执行页)
- ECharts 多图表联动(统计页)
- 报告免登访问(token 参数)
- 定时任务执行状态轮询
---
### 模块三:服务管理(4 个页面)
| 模板 | 路由 | 功能 | 复杂度 |
|------|------|------|--------|
| `base.html` | - | 服务管理基础布局(Tab 切换) | 低 |
| `authorization.html` | `/service-manage/authorization` | 授权管理(授权码输入 + 状态展示) | 低 |
| `upgrade.html` | `/service-manage/upgrade` | 升级管理(版本信息 + 升级操作) | 低 |
| `info.html` | `/service-manage/info` | 服务信息(系统版本、运行状态) | 低 |
---
## 4. 后端 API 清单
前端迁移需确保以下 API 稳定可用:
### 问题排查助手 API
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/troubleshoot` | POST | AI 分析(SSE 流式) |
| `/api/search` | GET | 知识库搜索 |
| `/api/projects` | GET | 项目列表 |
| `/api/categories` | GET | 分类列表 |
| `/api/health` | GET | 健康检查 |
| `/api/user/info` | GET | 当前用户信息 |
| `/api/cache/stats` | GET | 缓存统计 |
| `/api/cache/clear` | POST | 清空缓存 |
| `/api/export` | POST | 导出报告 |
| `/api/submit` | POST | 提交问题记录 |
### 服务监测 API
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/service-monitor/targets` | GET/POST/PUT/DELETE | 目标 CRUD |
| `/api/service-monitor/targets/:id/test` | POST | 连通性测试 |
| `/api/service-monitor/reports` | GET | 报告列表 |
| `/api/service-monitor/reports/:id` | GET/DELETE | 报告详情/删除 |
| `/api/service-monitor/reports/:id/export` | GET | 报告导出(md/json/xlsx/pdf) |
| `/api/service-monitor/run` | POST | 执行巡检(SSE) |
| `/api/service-monitor/schedules` | GET/POST/PUT/DELETE | 定时任务 CRUD |
| `/api/service-monitor/schedules/:id/run` | POST | 立即执行 |
| `/api/service-monitor/statistics/*` | GET | 统计数据(5 个接口) |
| `/api/service-monitor/notification` | GET/POST | 通知配置 |
| `/api/service-monitor/notification/test` | POST | 测试发送 |
### 服务管理 API
| 接口 | 方法 | 功能 |
|------|------|------|
| `/api/service-manage/authorization` | GET/POST | 授权管理 |
| `/api/service-manage/upgrade` | GET/POST | 升级管理 |
| `/api/service-manage/info` | GET | 服务信息 |
### 认证 API
| 接口 | 方法 | 功能 |
|------|------|------|
| `/login` | GET/POST | 登录页面/提交登录 |
| `/logout` | POST | 登出 |
| `/api/users` | GET/POST | 用户管理(管理员) |
---
## 5. 工作阶段划分
### 阶段一:前端项目骨架搭建(预估 2-3 天)
| # | 任务 | 输出 |
|---|------|------|
| 1.1 | 创建 Vue 3 + Vite + TypeScript 项目 | `frontend/` 目录 |
| 1.2 | 配置 Element Plus + SCSS + Vue Router + Pinia | `main.ts`, `router/`, `stores/` |
| 1.3 | 安装并配置 ECharts(npm 方式) | `package.json` |
| 1.4 | 实现基础布局组件(侧边栏 + 顶部栏 + 内容区) | `components/Layout/` |
| 1.5 | 实现登录页 + 路由守卫 + 登录状态管理 | `views/Login.vue`, `stores/user.ts` |
| 1.6 | 配置 Axios + 请求/响应拦截器 | `utils/http.ts` |
| 1.7 | 配置环境变量(开发/生产 API 地址) | `.env.*` |
| 1.8 | 实现全局组件(Loading、Message 确认框等) | `components/common/` |
### 阶段二:API 层封装 + TypeScript 类型定义(预估 2 天)
| # | 任务 | 输出 |
|---|------|------|
| 2.1 | 定义 TypeScript 类型(API 请求/响应结构) | `types/*.ts` |
| 2.2 | 封装认证 API | `api/auth.ts` |
| 2.3 | 封装问题排查 API(含 SSE 流式处理) | `api/troubleshoot.ts` |
| 2.4 | 封装服务监测 API(10+ 接口) | `api/service-monitor/*.ts` |
| 2.5 | 封装服务管理 API | `api/service-manage.ts` |
### 阶段三:页面迁移 — 问题排查助手模块(预估 2 天)
| # | 页面 | 预估工时 |
|---|------|----------|
| 3.1 | 登录页 | 0.5 天 |
| 3.2 | 平台首页 | 0.5 天 |
| 3.3 | 问题排查主页(SSE 流式输出) | 1 天 |
### 阶段四:页面迁移 — 服务监测模块(预估 6-8 天)
| # | 页面 | 预估工时 |
|---|------|----------|
| 4.1 | 监测主页(目标卡片 + 报告列表) | 1 天 |
| 4.2 | 目标管理 | 1 天 |
| 4.3 | 定时任务管理 | 1 天 |
| 4.4 | 通知配置 | 1 天 |
| 4.5 | 报告列表 | 1 天 |
| 4.6 | 报告详情 | 1 天 |
| 4.7 | 巡检执行(SSE 进度) | 1 天 |
| 4.8 | 监测统计(ECharts 多图表) | 1.5 天 |
| 4.9 | 报告对比 | 1 天 |
### 阶段五:页面迁移 — 服务管理模块(预估 1 天)
| # | 页面 | 预估工时 |
|---|------|----------|
| 5.1 | 服务管理布局(Tab 切换) | 0.3 天 |
| 5.2 | 授权管理 | 0.2 天 |
| 5.3 | 升级管理 | 0.2 天 |
| 5.4 | 服务信息 | 0.3 天 |
### 阶段六:后端调整(预估 1 天)
| # | 任务 | 输出 |
|---|------|------|
| 6.1 | 移除模板渲染路由,只保留 API 路由 | 修改 `routes/*.py` |
| 6.2 | 新增 SPA 回退路由(所有非 API 请求返回 index.html) | `server.py` |
| 6.3 | CORS 配置(开发环境允许前端跨域) | `flask-cors` |
| 6.4 | 移除 templates/ 目录(可选,保留备份) | - |
### 阶段七:部署配置(预估 1 天)
| # | 任务 | 输出 |
|---|------|------|
| 7.1 | Dockerfile 多阶段构建(Node 构建 Vue → Python Flask) | `Dockerfile` |
| 7.2 | nginx 配置(静态文件服务 + API 反向代理) | `nginx.conf` |
| 7.3 | docker-compose.yml 更新 | `docker-compose.yml` |
| 7.4 | 部署脚本更新 | `deploy/docker_deploy.sh` |
| 7.5 | 前端构建脚本 | `frontend/build.sh` |
### 阶段八:测试与验收(预估 2-3 天)
| # | 任务 | 输出 |
|---|------|------|
| 8.1 | 功能回归测试(18 个页面) | 测试报告 |
| 8.2 | API 兼容性测试 | - |
| 8.3 | SSE 流式输出测试 | - |
| 8.4 | ECharts 图表渲染测试 | - |
| 8.5 | 部署验证(本地 Docker + 5.60) | - |
| 8.6 | 性能对比(页面加载时间) | - |
| 8.7 | 浏览器兼容性测试(Chrome/Edge/Firefox) | - |
---
## 6. 目录结构设计
```
/data/third_party/monitor-platform/
├── frontend/ # Vue 前端项目
│ ├── src/
│ │ ├── api/ # API 封装
│ │ │ ├── auth.ts
│ │ │ ├── troubleshoot.ts
│ │ │ └── service-monitor/
│ │ │ ├── target.ts
│ │ │ ├── report.ts
│ │ │ ├── schedule.ts
│ │ │ ├── statistics.ts
│ │ │ └── notification.ts
│ │ ├── components/ # 公共组件
│ │ │ ├── Layout/
│ │ │ │ ├── AppLayout.vue # 主布局
│ │ │ │ ├── Sidebar.vue # 侧边栏
│ │ │ │ └── Header.vue # 顶部栏
│ │ │ └── common/
│ │ │ ├── Loading.vue
│ │ │ └── ConfirmDialog.vue
│ │ ├── views/ # 页面组件
│ │ │ ├── login/
│ │ │ │ └── Index.vue
│ │ │ ├── platform/
│ │ │ │ └── Index.vue
│ │ │ ├── troubleshoot/
│ │ │ │ └── Index.vue # 问题排查主页
│ │ │ ├── service-monitor/
│ │ │ │ ├── Index.vue # 监测主页
│ │ │ │ ├── Targets.vue
│ │ │ │ ├── Run.vue
│ │ │ │ ├── Report.vue
│ │ │ │ ├── Reports.vue
│ │ │ │ ├── Statistics.vue
│ │ │ │ ├── Schedule.vue
│ │ │ │ ├── Compare.vue
│ │ │ │ └── Notification.vue
│ │ │ └── service-manage/
│ │ │ ├── Index.vue
│ │ │ ├── Authorization.vue
│ │ │ ├── Upgrade.vue
│ │ │ └── Info.vue
│ │ ├── stores/ # Pinia 状态管理
│ │ │ ├── user.ts # 用户登录状态
│ │ │ ├── troubleshoot.ts # 问题排查状态
│ │ │ └── service-monitor/ # 服务监测状态
│ │ ├── router/ # 路由配置
│ │ │ └── index.ts
│ │ ├── styles/ # 全局样式
│ │ │ ├── variables.scss
│ │ │ └── global.scss
│ │ ├── types/ # TypeScript 类型定义
│ │ │ ├── api.ts
│ │ │ ├── troubleshoot.ts
│ │ │ └── service-monitor.ts
│ │ ├── utils/ # 工具函数
│ │ │ ├── http.ts # Axios 封装
│ │ │ ├── sse.ts # SSE 工具
│ │ │ └── storage.ts # 本地存储
│ │ ├── App.vue
│ │ └── main.ts
│ ├── public/
│ │ └── favicon.ico
│ ├── .env.development # 开发环境变量
│ ├── .env.production # 生产环境变量
│ ├── index.html
│ ├── package.json
│ ├── vite.config.ts
│ └── tsconfig.json
├── backend/ # Flask 后端(当前 skill/code/web/)
│ ├── server.py
│ ├── routes/
│ │ ├── auth.py
│ │ ├── troubleshoot.py
│ │ ├── service_monitor.py
│ │ └── service_manage.py
│ ├── services/
│ ├── utils/
│ └── service_monitor/
├── dist/ # 前端编译产物(部署时)
│ ├── index.html
│ └── assets/
├── data/ # 运行时数据(volume 挂载)
│ ├── troubleshoot.db # SQLite 数据库
│ ├── 搜索索引.json
│ ├── 搜索向量.json
│ └── logs/
├── nginx.conf # nginx 配置
├── docker-compose.yml # 编排配置
└── Dockerfile
```
---
## 7. Docker 部署架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ nginx:80 │
│ ├── / → /data/dist/index.html (前端 SPA) │
│ ├── /assets/* → /data/dist/assets/* │
│ └── /api/* → 反向代理到 Flask:8088 │
│ │
│ Flask:8088 (后端 API) │
│ └── /api/* → 业务逻辑 + SQLite │
│ │
│ SQLite: /data/data/troubleshoot.db │
└─────────────────────────────────────────────────────────────────┘
port 80:80 (对外)
宿主机 5.60
```
**更新流程**
- 更新前端:替换 `/data/dist/` 目录,**无需重启容器**
- 更新后端:重建镜像并重启容器
---
## 8. 关键技术点
### 8.1 SSE 流式输出(问题排查 AI 分析)
```typescript
// Vue 前端接收 SSE
async function fetchAIAnalysis(query: string) {
const response = await fetch(`/api/troubleshoot?query=${encodeURIComponent(query)}`)
const reader = response.body?.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader!.read()
if (done) break
const chunk = decoder.decode(value)
// 解析 SSE 格式: data: {...}\n\n
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
// 更新响应内容
}
}
}
}
```
### 8.2 巡检执行 SSE 进度
```typescript
// EventSource 方式(巡检执行页)
const eventSource = new EventSource('/api/service-monitor/run?target_id=xxx')
eventSource.addEventListener('progress', (event) => {
const data = JSON.parse(event.data)
// 更新进度条
})
eventSource.addEventListener('complete', (event) => {
// 跳转报告页
router.push(`/service-monitor/report/${data.report_id}`)
})
```
### 8.3 ECharts 使用
```typescript
import * as echarts from 'echarts'
const chartDom = ref<HTMLElement>()
const chart = echarts.init(chartDom.value!)
chart.setOption({
title: { text: '模块健康概览' },
tooltip: {},
xAxis: { type: 'category' },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [...] }]
})
```
### 8.4 路由配置
```typescript
// router/index.ts
const routes = [
{ path: '/login', component: () => import('@/views/login/Index.vue') },
{ path: '/', component: () => import('@/views/platform/Index.vue') },
{
path: '/troubleshoot',
component: () => import('@/views/troubleshoot/Index.vue'),
meta: { requiresAuth: true }
},
{
path: '/service-monitor',
component: () => import('@/views/service-monitor/Index.vue'),
meta: { requiresAuth: true },
children: [
{ path: 'targets', component: () => import('@/views/service-monitor/Targets.vue') },
{ path: 'run', component: () => import('@/views/service-monitor/Run.vue') },
// ... 其他子路由
]
},
// ...
]
```
### 8.5 Axios 拦截器
```typescript
// utils/http.ts
const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 30000,
withCredentials: true, // 自动带 cookie
})
// 响应拦截器:处理错误
http.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// 未登录,跳转登录页
router.push('/login')
}
return Promise.reject(error)
}
)
```
---
## 9. 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 前端重构引入 UI 差异 | 用户不适应 | 严格按现有模板还原,不改变交互逻辑 |
| SSE 流式输出兼容性问题 | AI 分析/巡检进度失效 | 优先实现并单独测试 SSE |
| ECharts 图表渲染差异 | 统计页面异常 | 保持相同配置项 |
| 后端 API 变更 | 前端调用失败 | 先冻结 API,前端完全适配后再考虑优化 |
| 开发周期超出预期 | 项目延期 | 按阶段交付,优先核心页面 |
| TypeScript 类型定义不全 | 开发效率下降 | 先定义核心类型,逐步补充 |
| 移动端适配问题 | 体验下降 | 复用当前已有的响应式样式 |
---
## 10. 估算工时
| 阶段 | 预估工时 | 说明 |
|------|----------|------|
| 阶段一:前端骨架 | 2-3 天 | 含布局、登录、Axios 配置 |
| 阶段二:API 封装 + TS 类型 | 2 天 | TypeScript 类型定义 |
| 阶段三:问题排查模块 | 2 天 | 3 个页面 |
| 阶段四:服务监测模块 | 6-8 天 | 10 个页面,核心模块 |
| 阶段五:服务管理模块 | 1 天 | 4 个页面,逻辑简单 |
| 阶段六:后端调整 | 1 天 | 移除模板路由、CORS |
| 阶段七:部署配置 | 1 天 | Dockerfile、nginx |
| 阶段八:测试验收 | 2-3 天 | 功能回归、部署验证 |
| **总计** | **17-21 天** | 约 3-4 周 |
---
## 11. 建议执行顺序
### 方案 A:先容器化,再 Vue 重构(推荐)
1. **先完成容器化部署**(用当前 Flask 模板 + volume 挂载模板目录)
- 部署架构先定下来
- SQLite 迁移也先完成
- 验证容器化流程
2. **并行推进 Vue 前端重构**
- 独立任务,不影响后端开发
- 可分配给前端开发者
3. **Vue 完成后切换**
- 修改 nginx 配置指向新的前端 dist/
- 删除 volume 挂载的旧模板
### 方案 B:直接一步到位
1. 新建 Vue 项目
2. 完成所有页面迁移
3. 后端调整 + Dockerfile + nginx
4. 一次性部署
**方案 A 的优势**:不阻塞容器化和 SQLite 迁移,Vue 重构可以稳步推进,风险更低。
---
## 12. 后续扩展方向
Vue 前端建成后,后续可方便地:
- **移动端适配**:响应式布局已具备基础
- **国际化**:Vue I18n 插件
- **暗黑模式**:Element Plus 内置支持
- **PWA 支持**:离线访问能力
- **组件库升级**:Element Plus 持续更新
- **前端单元测试**:Vitest + Vue Test Utils
\ No newline at end of file
# HANDOFF — 服务监测模块实施进度 # HANDOFF — 服务监测模块实施进度
> 最后更新:2026-07-16 | 分支:troubleshoot-ai-assistant | 模块:service-monitor > 最后更新:2026-07-27 | 分支:troubleshoot-ai-assistant | 模块:service-monitor
> 状态:**代码全部完成,218 测试全绿,待部署到 5.60** > 状态:**定时任务修复完成 + Docker 容器化方案设计完成 + Vue 前端迁移规划完成,待部署**
--- ---
...@@ -156,3 +156,99 @@ templates/service_monitor/{index,targets,run,report}.html ...@@ -156,3 +156,99 @@ templates/service_monitor/{index,targets,run,report}.html
### 已删除 ### 已删除
- `skill/code/web/routes/service_monitor.py`(旧占位路由) - `skill/code/web/routes/service_monitor.py`(旧占位路由)
- `skill/code/web/templates/service_monitor.html`(旧占位模板) - `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 已过期 |
| 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),需通过 API 更新
2. **APScheduler day_of_week 约定 bug**`CronTrigger.from_crontab('1-5')` 按 APScheduler 约定是周二到周六,非周一到周五,需改为 `CronTrigger(day_of_week='0-4')`
3. **容器化尚未部署**:需在 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 迁移任务清单 |
# 计划执行 — 定时任务并发执行失败修复
> 版本:1.0 | 日期:2026-07-27 | 作者:czj
> 关联问题文档:`PRD_问题处理_定时任务并发执行失败.md`
---
## 修复目标
1. 修复 5.44 定时任务不执行问题(P0)
2. 修复 schedules.json 并发写入状态丢失问题(P1)
3. 修复 APScheduler 线程阻塞问题(P1)
---
## 改动文件
| 文件 | 改动内容 |
|------|---------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 三处修复(见下文) |
---
## 修复步骤
### 步骤 1:添加 schedules.json 读写锁
**位置**`schedule_service.py` 模块级变量区(第 28 行附近)
**改动**:新增 `_schedules_lock = threading.Lock()`
**涉及函数加锁**
- `_save_schedules()` — 写操作加锁
- `_update_current_status()` — 读-改-写加锁
- `update_run_status()` — 读-改-写加锁
**注意**`_load_schedules()` 是纯读操作,不需要加锁(JSON 文件读取是原子的)。但读-改-写模式(先 load 再 save)必须整体加锁。
```python
# 新增模块级锁
_schedules_lock = threading.Lock()
# _save_schedules 加锁
def _save_schedules(schedules: list) -> None:
with _schedules_lock:
ensure_dirs()
SCHEDULES_FILE.write_text(
json.dumps(schedules, ensure_ascii=False, indent=2),
encoding="utf-8",
)
# _update_current_status 加锁
def _update_current_status(schedule_id: str, status: str) -> None:
with _schedules_lock:
schedules = _load_schedules()
for i, s in enumerate(schedules):
if s.get("id") == schedule_id:
schedules[i]["current_status"] = status
_save_schedules(schedules)
return
raise ValueError("定时任务不存在")
# update_run_status 加锁
def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str) -> dict:
with _schedules_lock:
schedules = _load_schedules()
for i, s in enumerate(schedules):
if s.get("id") == schedule_id:
schedules[i]["last_run_at"] = run_at
schedules[i]["last_run_status"] = status
schedules[i]["last_report_id"] = report_id
schedules[i]["next_run_at"] = _calc_next_run(s["cron"])
_save_schedules(schedules)
return schedules[i]
raise ValueError("定时任务不存在")
```
---
### 步骤 2:`_add_job()` 添加关键参数
**位置**`schedule_service.py` 第 451-457 行
**改动**:添加 `misfire_grace_time``coalesce``max_instances` 参数
```python
_scheduler.add_job(
func=_execute_scheduled_job,
trigger=trigger,
id=job_id,
args=[sched["id"]],
replace_existing=True,
misfire_grace_time=3600, # 允许 1 小时内的错过执行(默认 1 秒太短)
coalesce=True, # 错过多次只执行一次
max_instances=1, # 同一 job 不并发(显式声明)
)
```
**参数说明**
- `misfire_grace_time=3600`:如果任务错过触发时间,1 小时内仍会执行。解决因 GIL 竞争、线程繁忙等导致的短暂延迟跳过问题
- `coalesce=True`:如果任务连续错过多次触发(如服务重启期间),只执行一次,不堆积
- `max_instances=1`:同一 job 同一时间只允许一个实例运行,防止并发重复执行
---
### 步骤 3:`_execute_scheduled_job` 改为后台线程执行
**位置**`schedule_service.py` 第 512-521 行
**改动**:将同步调用改为 `threading.Thread` 后台执行,与 `run_now()` 保持一致
```python
def _execute_scheduled_job(schedule_id: str) -> None:
"""定时任务执行入口(APScheduler 线程中调用),含 enabled 检查。"""
logger.info("定时任务触发: %s", schedule_id)
sched = get_schedule(schedule_id)
if not sched or not sched.get("enabled"):
logger.warning("定时任务不存在或已禁用: %s", schedule_id)
return
# 检查是否已在执行中(防止定时触发与手动触发并发)
if sched.get("current_status") == "running":
logger.warning("定时任务已在执行中,跳过: %s", schedule_id)
return
# 使用后台线程执行,不阻塞 APScheduler 调度线程
t = threading.Thread(target=_run_job_body, args=(schedule_id,), daemon=True)
t.start()
```
**改动说明**
- 新增 `current_status == "running"` 检查,防止定时触发与手动触发并发执行同一任务
- 使用 `threading.Thread` 后台执行,APScheduler 线程立即释放,可以继续调度其他任务
- `daemon=True` 确保主进程退出时线程自动终止
---
## 验证步骤
### 本地验证
1. 启动服务,创建两个定时任务(间隔 1 分钟)
2. 确认两个任务都能正常触发执行
3. 手动触发一个正在执行的任务,确认被拒绝("已在执行中")
4. 检查 `schedules.json` 状态更新正确
### 部署后验证
1. 部署到 5.60
2. 检查健康状态:`curl -s http://192.168.5.60:8088/api/health`
3. 检查 scheduler 状态:`curl -s http://192.168.5.60:8088/api/service-monitor/scheduler/status`
4. 确认 5.44 和 5.202 两个定时任务都已注册
5. 等待下一个工作日 8:40,确认 5.44 任务正常执行
6. 等待 9:00,确认 5.202 任务正常执行
7. 检查 `schedules.json` 中两个任务的状态更新均正确
### 单元测试
```bash
cd skill/code && python -m pytest -v
```
确认 218 个测试全绿,无回归。
---
## 回滚方案
如果修复后出现问题,回滚步骤:
1. 恢复 `schedule_service.py` 到修复前版本
2. 重新部署
3. 手动检查 `schedules.json` 中任务状态是否正确
---
## 文档信息
- 创建时间:2026-07-27
- 创建人:Claude
- 关联问题文档:`PRD_问题处理_定时任务并发执行失败.md`
# 问题处理 — 定时任务并发执行失败
> 版本:1.0 | 日期:2026-07-27 | 作者:czj
---
## 问题描述
| # | 问题 | 严重度 |
|---|------|--------|
| 1 | 5.44 服务器定时任务(工作日 8:40)不执行,5.202 服务器定时任务(工作日 9:00)正常执行 | P0 功能 |
| 2 | `schedules.json` 并发读写无锁保护,多任务同时执行时可能丢失状态更新 | P1 数据安全 |
| 3 | `_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行,长时间巡检占用调度线程 | P1 性能 |
---
## 问题 1 根因分析(P0)
### 现象
- 5.44 定时任务配置"工作日 08:40 执行",到了时间不执行,无报告生成
- 5.202 定时任务配置"工作日 09:00 执行",正常执行,报告正常生成
- 服务器健康检查 status: ok, scheduler: running
### 根因
**根因 1(主因):`_add_job()` 未设置 `misfire_grace_time`,APScheduler 默认 1 秒**
`schedule_service.py` 第 451-457 行:
```python
_scheduler.add_job(
func=_execute_scheduled_job,
trigger=trigger,
id=job_id,
args=[sched["id"]],
replace_existing=True,
# ❌ 缺少 misfire_grace_time、coalesce、max_instances
)
```
APScheduler 的 `BackgroundScheduler` 默认 `misfire_grace_time = 1` 秒。含义:如果调度器在预定时间后超过 1 秒才检查到该任务需要执行,该任务就被标记为 misfire 并**跳过不执行**
**触发场景**
- 5.44 任务设定 8:40 触发
- 8:40 时 APScheduler 的执行线程正忙于其他操作(GIL 竞争、垃圾回收、或其他任务正在同步执行巡检)
- 调度器延迟 1 秒以上才检查到 8:40 的任务
- 该任务被判定为 misfire,跳过不执行
- 5.202 的 9:00 任务正常,因为 9:00 时调度器线程空闲
**根因 2(次因):`_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行**
`schedule_service.py` 第 512-521 行:
```python
def _execute_scheduled_job(schedule_id: str) -> None:
sched = get_schedule(schedule_id)
if not sched or not sched.get("enabled"):
return
_run_job_body(schedule_id) # ❌ 同步阻塞!
```
对比 `run_now()`(手动触发)使用了 `threading.Thread` 后台执行(第 539 行),而定时触发直接在 APScheduler 线程中同步调用 `_run_job_body`
`run_inspection_sync()` 执行全量巡检可能耗时数分钟(每个模块 90 秒超时 × 多个模块),在此期间 APScheduler 的执行线程被阻塞,无法触发其他定时任务,加剧 misfire 问题。
### 影响范围
- **所有定时任务**都可能受影响,不仅限于 5.44
- 任务执行时间越长,后续任务越容易 misfire
- 多个定时任务配置在相近时间段时问题更明显
---
## 问题 2 根因分析(P1)
### 现象
两个定时任务同时执行时,`schedules.json` 中的状态可能不正确(如一个任务完成后的状态更新被另一个任务的写入覆盖)。
### 根因
`_update_current_status()`(第 567-575 行)和 `update_run_status()`(第 357-368 行)都采用"读-改-写"模式操作 `schedules.json`,但**没有任何锁保护**
```python
def _update_current_status(schedule_id: str, status: str) -> None:
schedules = _load_schedules() # 1. 读
for i, s in enumerate(schedules):
if s.get("id") == schedule_id:
schedules[i]["current_status"] = status # 2. 改
_save_schedules(schedules) # 3. 写
return
```
**竞态场景**
1. 5.44 任务完成,线程 A 调用 `update_run_status("sched_5.44", "success", ...)`
2. 同时 5.202 任务执行中,线程 B 调用 `_update_current_status("sched_5.202", "running")`
3. 线程 A 先 `_load_schedules()` 读到旧数据
4. 线程 B 也 `_load_schedules()` 读到同一份旧数据
5. 线程 A 写入,更新了 5.44 的状态
6. 线程 B 写入,基于旧数据更新 5.202 的状态,**覆盖了线程 A 对 5.44 状态的更新**
7. 结果:5.44 的 `last_run_status` 被回退,看起来像从未执行
### 影响范围
- 多个定时任务同时执行时,状态更新可能丢失
- 手动触发 + 定时触发同时进行时同样受影响
- `current_status` 可能永远停留在 "running",导致手动触发被拒绝
---
## 问题 3 根因分析(P1)
### 现象
定时任务执行期间,APScheduler 调度线程被阻塞,影响其他任务触发。
### 根因
见问题 1 根因 2。`_execute_scheduled_job` 在 APScheduler 的工作线程中同步执行 `run_inspection_sync()`,该函数执行全量巡检可能耗时数分钟。
APScheduler 默认线程池大小为 10,如果多个定时任务同时占用线程,新的触发请求可能因线程池耗尽而延迟或失败。
---
## 修复方案
| 问题 | 修复方案 | 改动文件 |
|------|---------|---------|
| P0:misfire_grace_time 过短 | `_add_job()` 添加 `misfire_grace_time=3600`(1 小时容错)、`coalesce=True``max_instances=1` | `schedule_service.py` |
| P1:APScheduler 线程阻塞 | `_execute_scheduled_job` 改为 `threading.Thread` 后台执行,与 `run_now()` 保持一致 | `schedule_service.py` |
| P1:schedules.json 并发写入 | 添加模块级 `threading.Lock()``_update_current_status` / `update_run_status` / `_save_schedules` 等函数加锁 | `schedule_service.py` |
---
## 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| misfire_grace_time 过大导致任务堆积 | 低频任务不会堆积;高频任务应设 max_instances=1 | 已设 max_instances=1 |
| 后台线程异常无法捕获 | 线程内已有 try/except 兜底 | 日志记录完整 |
| 锁粒度过大影响性能 | schedules.json 操作是毫秒级,锁持有时间极短 | 可接受 |
---
## 文档信息
- 创建时间:2026-07-27
- 创建人:Claude
- 关联问题:定时任务时区问题、定时任务与报告优化
- 关联提交:待修复
# HANDOFF — 服务监测模块:报告免登访问 + 监测统计 + 多项修复 # HANDOFF — 服务监测模块:定时任务修复 + Docker 容器化
> 最后更新:2026-07-22 02:00 | 分支:troubleshoot-ai-assistant | 负责人:czj > 最后更新:2026-07-27 21:00 | 分支:troubleshoot-ai-assistant | 负责人:czj
--- ---
...@@ -8,113 +8,80 @@ ...@@ -8,113 +8,80 @@
服务监测模块(service_monitor)是运行维护平台的子模块,提供目标服务器巡检、报告管理、定时任务、通知推送等功能。 服务监测模块(service_monitor)是运行维护平台的子模块,提供目标服务器巡检、报告管理、定时任务、通知推送等功能。
本轮会话完成了三个主要任务 本轮会话完成了两个主要任务,启动了容器化部署
1. **报告免登访问**:钉钉/企业微信通知中的报告链接,原来需要登录才能查看。用户希望点击链接直接看到报告,如果点击其他模块再跳转登录。核心价值是提升移动端体验,减少操作割裂感 1. **定时任务并发执行失败修复**:5.44 服务器定时任务(工作日 8:40)不执行,5.202 的 9:00 正常。根因是 APScheduler 的 `misfire_grace_time` 默认仅 1 秒,调度线程稍有延迟就跳过任务;加上 `_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行,加剧了 misfire。同时发现 `schedules.json` 并发写入无锁保护,多任务同时执行时可能丢失状态更新
2. **监测统计模块**:新增左侧菜单"监测统计",提供可视化统计分析页面。支持按目标服务器、日期范围筛选,展示模块健康概览(堆叠条形图)、检测项趋势分析(折线图+饼图)、高频异常检测项排行。 2. **Docker 容器化(阶段一)**:将服务从裸机部署(systemd/nohup + python3 直跑)迁移到 Docker 单容器部署。阶段一只做容器化,JSON 文件存储不变,通过 volume 持久化。SQLite 迁移(阶段二)留到下一轮。
3. **多项问题修复**:检测项趋势图表部分检测项无法正常显示(状态类/文本类无数值数据)、JavaScript 代码残留导致 `fetchAllData` 未定义。
--- ---
## 2. 已经完成了什么 ## 2. 已经完成了什么
### 2.1 报告免登访问与操作鉴权 ✅ ### 2.1 定时任务并发执行失败修复 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_报告免登访问与操作鉴权.md` **问题文档**`Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md`
**计划文档**`Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md`
**核心实现**
- `report_service.py`:报告生成时自动生成 `access_token`(格式 `rpt_<32位随机hex>`)和 `access_token_expires_at`(7天有效期) **根因**
- `runner_service.py`:通知链接拼接 `?token=rpt_xxx` 参数 - `_add_job()` 未设置 `misfire_grace_time`,APScheduler 默认 1 秒容错,调度线程稍有延迟就跳过任务
- `routes.py``page_report()` 支持 token 验证,验证通过后渲染报告页并标记 `token_auth=True`(访客模式);`api_export_report()` 支持 token 导出 - `_execute_scheduled_job` 在 APScheduler 线程中同步阻塞执行巡检(可能数分钟),阻塞调度线程
- `auth.py`:GET/POST `/login` 支持 `next` 参数,登录成功后跳转回原始页面 - `schedules.json` 并发写入无锁保护,多任务同时执行时可能丢失状态更新
- `decorators.py``page_login_required` 重定向时携带 `next` 参数
- `login.html`:表单提交携带 `next` 参数,登录成功后跳转 **修复内容**(5 处,均在 `schedule_service.py`):
- `report.html`:访客模式显示提示条、隐藏管理操作、导出链接携带 token
- `base.html`:访客模式下导航菜单点击跳转登录页,顶部显示"访客 + 登录按钮" | # | 修复 | 位置 |
|---|------|------|
**验证结果** | 1 | 新增 `_schedules_lock` 模块级锁 | 第 30-31 行 |
- 未登录点击通知链接 → 直接查看报告 ✅ | 2 | `_save_schedules()` 加锁 | 第 140 行 |
- 访客模式点击其他模块 → 弹窗提示并跳转登录 ✅ | 3 | `update_run_status()` 读-改-写整体加锁 | 第 363 行 |
- 登录后回跳到原始页面 ✅ | 4 | `_add_job()` 添加 `misfire_grace_time=3600``coalesce=True``max_instances=1` | 第 462-464 行 |
- 218 个单元测试全绿 ✅ | 5 | `_execute_scheduled_job` 改为 `threading.Thread` 后台执行 + `current_status` 并发检查 | 第 520-536 行 |
### 2.2 监测统计模块 ✅ **验证**:218 个单元测试全绿 ✅
**需求文档**`Docs/需求文档/服务监测/PRD_需求文档_监测统计模块.md` ### 2.2 Docker 容器化(阶段一) ✅
**核心实现** **核心文件**
- `statistics_service.py`(新增):统计聚合服务,包含 5 个公开函数:
- `get_overview()`:概览摘要(总报告数、异常率、最常异常项等) | 文件 | 说明 |
- `get_module_stats()`:模块健康统计 |------|------|
- `get_item_trend()`:检测项趋势数据(时间序列 + 状态分布) | `Dockerfile` | 基于 python:3.11-slim,单进程启动(不用 gunicorn,避免 APScheduler 多 worker 冲突),自动修复 .sh CRLF |
- `get_abnormal_items()`:高频异常检测项 TOP 10 | `.dockerignore` | 排除测试/运行时数据/文档 |
- `get_item_keys_for_target()`:检测项列表(按模块分组,含 `has_numeric` 标记) | `docker-compose.yml` | 单容器 + volume 挂载 + 资源限制(1G/1CPU)+ 健康检查 |
- `routes.py`:新增页面路由 `/service-monitor/statistics` + 5 个 API 端点 | `deploy/docker_deploy.sh` | 5.60 部署脚本(前置检查→准备数据目录→构建镜像→启动容器→健康检查) |
- `base.html`:侧边栏添加"📈 监测统计"菜单项
- `statistics.html`(新增):完整统计页面,使用 ECharts 渲染图表 **关键设计决策**
- 筛选栏:目标选择器 + 日期范围(自定义) - **不用 gunicorn 多 worker**:APScheduler 的 BackgroundScheduler 在多 worker 模式下会创建多个调度器实例,定时任务重复执行
- 概览卡片:4 项摘要指标 - **不挂载 docker.sock**:5.60 上有其他服务容器,挂载 sock 会暴露宿主机所有容器控制权
- 模块健康概览:堆叠条形图 - **资源限制**`--memory=1g --cpus=1.0`,防止抢占其他容器资源
- 检测项趋势分析:模块/检测项两级联动选择器 + 折线图 + 饼图 - **容器名 `troubleshoot`**:明确前缀,不与其他容器冲突
- 高频异常检测项:水平堆叠条形图
**Volume 挂载**
**验证结果**
- 统计页面正常加载 ✅ | 容器路径 | 宿主机路径 | 说明 |
- 各 API 返回正确数据结构 ✅ |---------|-----------|------|
- 图表正常渲染 ✅ | `/app/web/service_monitor/data` | `/opt/troubleshoot/monitor-data` | targets/schedules/reports |
- 218 个单元测试全绿 ✅ | `/app/web/users.json` | `/opt/troubleshoot/data/users.json` | 用户数据 |
| `/app/data/搜索索引.json` | `/opt/troubleshoot/data/搜索索引.json` | 知识库索引(只读) |
### 2.3 检测项趋势图表显示问题修复 ✅ | `/app/data/搜索向量.json` | `/opt/troubleshoot/data/搜索向量.json` | 向量索引(只读) |
| `/app/web/logs` | `/opt/troubleshoot/logs` | 日志 |
**问题文档**`Docs/需求文档/服务监测/PRD_问题处理_检测项趋势图表显示问题.md`
### 2.3 搜索引擎路径适配 ✅
**问题根因**
- 部分检测项的值是状态文本("正常"/"异常")、yes/no、路径等,`_extract_numeric()` 无法提取数值 **改动**
- 前端收到 `value_num=null` 后折线图无数据点,但未显示任何提示 - `utils/paths.py` 新增 `SEARCH_INDEX_DIR` 常量,支持 `SEARCH_INDEX_DIR` 环境变量覆盖,自动检测容器/裸机路径
- `search_engine.py` 改用 `utils/paths.py``SEARCH_INDEX_DIR` 替代硬编码路径列表
**修复内容**
- `statistics_service.py``get_item_keys_for_target()` 增加 `has_numeric` 字段,标记检测项是否有数值数据
- `statistics.html``fetchItemTrend()` 增加三种情况处理:
- 无数据点:显示"暂无数据"
- 有数据点但无数值:显示友好提示"该检测项为状态/文本类型,无法绘制数值趋势图,请查看右侧状态分布",饼图正常渲染
- 有数值数据:正常渲染折线图 + 饼图
- 检测项选择器标注:无数值能力的检测项标注 "(状态)" 后缀
**验证结果**
- 选择 `CPU_USAGE` 等数值项,折线图正常 ✅
- 选择 `CPU_STATUS` 等状态项,显示友好提示 + 饼图正常 ✅
- 选择器中状态项标注 "(状态)" ✅
### 2.4 JavaScript 代码残留问题修复 ✅
**问题文档**`Docs/需求文档/服务监测/PRD_问题处理_统计页面JS错误.md`
**问题根因**
- 编辑 `fetchItemKeys` 函数时,旧代码尾部 4 行未被删除,残留在新函数之后
- 导致 `onModuleChange``fetchItemTrend``fetchAllData` 等函数被错误嵌套,全局作用域无法访问 `fetchAllData`
**修复内容**
- 删除第 330-333 行、第 492-494 行的残留代码
- 补回被误删的 `onModuleChange` 函数体
**验证结果**
- 花括号平衡(124 对)✅
- 9 个关键函数全部存在 ✅
- 查询按钮点击正常工作 ✅
--- ---
## 3. 当前卡在哪 ## 3. 当前卡在哪
**无卡点**。所有功能已实现、部署到 5.60、验证通过。 **无代码卡点**。所有修复和容器化文件已完成,测试通过。
**当前状态** **待执行**
- 服务健康检查:status: ok, scheduler: running ✅ - 5.60 上尚未部署容器化版本(需先确认 Docker 已安装)
- 报告免登访问正常工作 ✅ - 5.44 定时任务 `end_date` 已过期(2026-07-22),即使修复部署后也不会触发,需通过 API 更新
- 统计模块正常工作 ✅
- 检测项趋势图表友好提示正常 ✅
--- ---
...@@ -122,48 +89,52 @@ ...@@ -122,48 +89,52 @@
| 优先级 | 任务 | 说明 | | 优先级 | 任务 | 说明 |
|--------|------|------| |--------|------|------|
| P1 | 创建 Merge Request | 访问 http://git.ubainsyun.com/bing/ubains-module-test/merge_requests/new?merge_request%5Bsource_branch%5D=troubleshoot-ai-assistant 合并到 master | | **P0** | 部署容器化版本到 5.60 | 1. SSH 确认 Docker 已安装;2. 准备 `/opt/troubleshoot/.env`;3. 执行 `docker_deploy.sh` |
| P2 | 提交 HANDOFF.md | 本次交接文档加入 git | | **P0** | 更新 5.44 定时任务 end_date | 通过 API 或前端更新,当前 end_date=2026-07-22 已过期 |
| P2 | 清理测试数据 | 删除测试用的定时任务,配置 `MONITOR_ENC_KEY` 环境变量 | | P1 | 修复 APScheduler day_of_week 约定 bug | `CronTrigger.from_crontab('1-5')` 实际是周二到周六而非周一到周五(APScheduler 3.x 约定与标准 cron 不同),需改为直接构造 `CronTrigger(day_of_week='0-4')` |
| P3 | 企业微信通知链接同步 | 邮件/企业微信通知也携带 token | | P2 | SQLite 迁移(阶段二) | 替换 JSON 文件存储,需:1. 新增 db.py 连接管理;2. 逐模块替换 target/schedule/report/notification service;3. 更新 55 个测试 fixture;4. 迁移脚本 |
| P2 | 迁移 auth.py 用户数据到 SQLite | UserManager 的 JSON 读写替换为 SQLite |
| P2 | 数据迁移脚本 | JSON → SQLite,保留 .bak 备份 |
--- ---
## 5. 踩过的坑(绝对不要再踩) ## 5. 踩过的坑(绝对不要再踩)
### 坑 1:JavaScript 编辑时代码残留 ### 坑 1:APScheduler misfire_grace_time 默认 1 秒
**坑**`_add_job()` 不设置 `misfire_grace_time`,APScheduler 默认仅 1 秒容错。调度线程稍有延迟(GIL 竞争、垃圾回收等),任务就被标记为 misfire 跳过不执行。
**避免方法**`_add_job()` 必须显式设置 `misfire_grace_time=3600`(1 小时容错)。
**坑**:使用 Edit 工具替换函数时,`old_string` 未精确匹配完整范围,只替换了函数开头,尾部代码残留。导致后续函数定义在错误的闭包内,全局作用域无法访问。 ### 坑 2:`_execute_scheduled_job` 同步阻塞 APScheduler 线程
**避免方法** **坑**:定时触发直接在 APScheduler 线程中同步调用 `_run_job_body`,巡检可能耗时数分钟,阻塞调度线程导致后续任务 misfire。手动触发 `run_now()` 用了 `threading.Thread`,但定时触发没有。
- 编辑前完整阅读原代码,确保 `old_string` 匹配完整范围
- 编辑后检查花括号平衡
- 本地测试后再部署
### 坑 2:状态类检测项折线图空白无提示 **避免方法**:定时触发也用 `threading.Thread` 后台执行,与 `run_now()` 保持一致。
**坑**`_extract_numeric()` 对非数值("正常"/"异常"/"yes"/"no")返回 `(None, value)`,前端收到 `value_num=null` 后折线图无数据点,但未显示任何提示。用户以为"图表坏了"。 ### 坑 3:JSON 文件并发写入无锁保护
**避免方法** **坑**`_update_current_status()``update_run_status()` 采用"读-改-写"模式操作 `schedules.json`,但无锁保护。两个定时任务同时执行时,一个线程的写入可能被另一个线程覆盖。
- 前端处理数据前检查数值情况,无数值时显示友好提示
- 饼图使用 `status` 字段,不依赖数值
### 坑 3:检测项选择器选项过多难找 **避免方法**:用 `threading.Lock()` 保护读-改-写操作。SQLite 迁移后 WAL 模式自带并发安全,可删除此锁。
**坑**:419 个检测项平铺在一个下拉框,用户难以找到目标项。 ### 坑 4:Docker 多 worker 与 APScheduler 不兼容
**避免方法** **坑**:旧 Dockerfile 用 `gunicorn -w 4`,APScheduler 的 BackgroundScheduler 在每个 worker 中各创建一个实例,导致定时任务重复执行 4 次。
- 用两级联动:第一级选模块,第二级选检测项
- 检测项选择器按模块分组(optgroup)
- 标注无趋势能力的检测项
### 坑 4:部署后健康检查误报 **避免方法**:容器化用单进程 `python3 server.py` 启动,不用 gunicorn。
**坑**`upload_to_server.py` 结尾的 `[FAIL] Service may not be running` 是误报。服务启动需要加载索引(357 条),sleep 3 秒不够。 ### 坑 5:5.60 上有其他服务容器
**避免方法** **坑**:5.60 上运行着其他服务的 Docker 容器,必须严格隔离。挂载 docker.sock 会暴露宿主机所有容器控制权,资源不限制会抢占其他容器。
-`curl http://192.168.5.60:8088/api/health` 权威验证
- 不要信赖部署脚本的自动检查 **避免方法**:不挂载 docker.sock、设置 `--memory=1g --cpus=1.0`、volume 路径用 `/opt/troubleshoot/` 独占前缀、容器名 `troubleshoot` 明确区分。
### 坑 6:APScheduler 3.x day_of_week 约定与标准 cron 不同
**坑**`CronTrigger.from_crontab('40 8 * * 1-5')``1-5` 按 APScheduler 约定是周二到周六,而非标准 cron 的周一到周五。导致"工作日"定时任务实际在周二到周六触发,周一不执行。
**避免方法**:不用 `from_crontab()`,改为直接构造 `CronTrigger(day_of_week='0-4')`(APScheduler 约定 0=周一,4=周五)。**待修复**
--- ---
...@@ -173,52 +144,47 @@ ...@@ -173,52 +144,47 @@
| 文件 | 改动 | | 文件 | 改动 |
|------|------| |------|------|
| `skill/code/web/service_monitor/services/report_service.py` | 新增 `access_token`/`access_token_expires_at` 生成、`validate_access_token()` | | `skill/code/web/service_monitor/services/schedule_service.py` | 5 处并发修复:锁 + misfire_grace_time + 后台线程 |
| `skill/code/web/service_monitor/services/runner_service.py` | 通知链接拼接 token | | `skill/code/web/search_engine.py` | 搜索索引路径改用 `utils/paths.py``SEARCH_INDEX_DIR` |
| `skill/code/web/service_monitor/services/statistics_service.py` | 新增 - 统计聚合服务 | | `skill/code/web/utils/paths.py` | 新增 `SEARCH_INDEX_DIR` 常量 |
| `skill/code/web/service_monitor/services/__init__.py` | 导入 `statistics_service` | | `Dockerfile` | 重写:python:3.11-slim + 单进程 + CRLF 修复 |
| `skill/code/web/service_monitor/routes.py` | 页面路由 + 5 个统计 API + token 验证 | | `docker-compose.yml` | 重写:单容器 + volume + 资源限制 + 健康检查 |
| `skill/code/web/routes/auth.py` | next 参数回跳 | | `.dockerignore` | 更新:补充 service_monitor/tests 等排除 |
| `skill/code/web/decorators.py` | `page_login_required` 携带 next |
| `skill/code/web/templates/login.html` | next 回跳支持 | ### 本次新增文件
| `skill/code/web/templates/service_monitor/base.html` | 侧边栏菜单 + 访客模式处理 |
| `skill/code/web/templates/service_monitor/report.html` | 访客模式适配 |
| `skill/code/web/templates/service_monitor/statistics.html` | 新增 - 统计页面 |
### 新增文档
| 文件 | 说明 | | 文件 | 说明 |
|------|------| |------|------|
| `Docs/需求文档/服务监测/PRD_需求文档_报告免登访问与操作鉴权.md` | 需求文档 | | `deploy/docker_deploy.sh` | 5.60 Docker 部署脚本 |
| `Docs/需求文档/服务监测/PRD_计划执行_报告免登访问与操作鉴权.md` | 计划执行文档 | | `Docs/需求文档/服务监测/PRD_问题处理_定时任务并发执行失败.md` | 问题处理文档 |
| `Docs/需求文档/服务监测/PRD_需求文档_监测统计模块.md` | 需求文档 | | `Docs/需求文档/服务监测/PRD_计划执行_定时任务并发执行失败修复.md` | 计划执行文档 |
| `Docs/需求文档/服务监测/PRD_计划执行_监测统计模块.md` | 计划执行文档 |
| `Docs/需求文档/服务监测/PRD_问题处理_统计页面JS错误.md` | 问题处理文档 | ### 未提交文件
| `Docs/需求文档/服务监测/PRD_计划执行_统计页面JS错误修复.md` | 计划执行文档 |
| `Docs/需求文档/服务监测/PRD_问题处理_检测项趋势图表显示问题.md` | 问题处理文档 | - `skill/code/web/users.json`(仅登录统计更新)
| `Docs/需求文档/服务监测/PRD_计划执行_检测项趋势图表显示问题修复.md` | 计划执行文档 | - `deploy/` 下多个测试/检查脚本(非正式部署文件)
### 常用命令 ### 常用命令
```bash ```bash
# 本地启动 # 单元测试(218 用例,应全绿)
python skill/code/web/server.py
# 单元测试
cd skill/code && python -m pytest -v cd skill/code && python -m pytest -v
# 部署到 5.60(用 ! 前缀执行) # 本地 Docker 构建
! cd deploy && SSH_PASSWORD='***' python upload_to_server.py docker build -t troubleshoot:latest .
# 本地 Docker 启动
docker run -d --name troubleshoot -p 8088:8088 troubleshoot:latest
# 5.60 部署(在服务器上执行)
cd /path/to/project && bash deploy/docker_deploy.sh
# 健康检查 # 健康检查
curl -s http://192.168.5.60:8088/api/health curl -s http://192.168.5.60:8088/api/health
# 测试报告免登访问 # 查看容器日志
curl -s "http://192.168.5.60:8088/service-monitor/report/<id>?token=rpt_xxx" docker logs -f troubleshoot
```
### 未提交文件
- 多个 PRD 文档(需求/计划执行/问题处理) # 查看容器状态
- `skill/code/web/users.json`(仅登录统计更新) docker ps -f name=troubleshoot
- `HANDOFF.md`(本轮待提交) ```
\ No newline at end of file
#!/bin/bash
# ============================================================
# Troubleshoot AI Assistant — Docker 部署脚本
# 在 5.60 服务器上执行
# ============================================================
# 前置条件:
# 1. 服务器已安装 Docker
# 2. 项目代码已上传到服务器(或从构建机 scp 镜像)
# 3. /opt/troubleshoot/.env 已配置(SECRET_KEY 等)
#
# 使用方式:
# ./docker_deploy.sh # 构建并启动
# ./docker_deploy.sh --build-only # 仅构建镜像
# ./docker_deploy.sh --stop # 停止容器
# ./docker_deploy.sh --status # 查看状态
# ============================================================
set -euo pipefail
# 配置
CONTAINER_NAME="troubleshoot"
IMAGE_NAME="troubleshoot:latest"
DATA_BASE="/opt/troubleshoot"
ENV_FILE="${DATA_BASE}/.env"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
# ---------- 前置检查 ----------
check_prerequisites() {
info "检查前置条件..."
# Docker 是否安装
if ! command -v docker &> /dev/null; then
error "Docker 未安装,请先安装: sudo apt install docker.io"
fi
info "Docker: $(docker --version)"
# 当前用户是否有 Docker 权限
if ! docker ps &> /dev/null; then
error "当前用户无 Docker 权限,请将用户加入 docker 组: sudo usermod -aG docker $USER"
fi
# .env 文件是否存在
if [ ! -f "${ENV_FILE}" ]; then
warn ".env 文件不存在: ${ENV_FILE}"
warn "请创建并配置 SECRET_KEY 等环境变量"
mkdir -p "${DATA_BASE}"
cat > "${ENV_FILE}" << 'EOF'
# 必填
SECRET_KEY=your_secret_key_here
# Claude API(离线模式可留空)
CLAUDE_API_BASE=
CLAUDE_API_KEY=
# 离线模式(true=不调 Claude API)
OFFLINE_MODE=true
# 服务监测加密密钥(可选,不设则从 SECRET_KEY 派生)
MONITOR_ENC_KEY=
EOF
warn "已创建模板 ${ENV_FILE},请编辑后重新运行"
exit 1
fi
info ".env 文件: ${ENV_FILE}"
}
# ---------- 准备数据目录 ----------
prepare_dirs() {
info "准备数据目录..."
# 监测数据目录
mkdir -p "${DATA_BASE}/monitor-data/reports"
# 用户数据(首次部署时初始化空文件)
if [ ! -f "${DATA_BASE}/data/users.json" ]; then
mkdir -p "${DATA_BASE}/data"
echo '{"users":[]}' > "${DATA_BASE}/data/users.json"
info "已初始化空 users.json"
fi
# 日志目录
mkdir -p "${DATA_BASE}/logs"
# 搜索索引(如果存在旧部署的索引,复制过来)
if [ -f "${DATA_BASE}/搜索索引.json" ] && [ ! -f "${DATA_BASE}/data/搜索索引.json" ]; then
cp "${DATA_BASE}/搜索索引.json" "${DATA_BASE}/data/搜索索引.json"
info "已复制搜索索引到 data/"
fi
# 搜索向量(可选)
if [ -f "${DATA_BASE}/搜索向量.json" ] && [ ! -f "${DATA_BASE}/data/搜索向量.json" ]; then
cp "${DATA_BASE}/搜索向量.json" "${DATA_BASE}/data/搜索向量.json"
info "已复制搜索向量到 data/"
fi
info "数据目录准备完成"
}
# ---------- 构建镜像 ----------
build_image() {
info "构建 Docker 镜像..."
docker build -t "${IMAGE_NAME}" .
info "镜像构建完成: ${IMAGE_NAME}"
}
# ---------- 启动容器 ----------
start_container() {
# 检查是否有同名容器在运行
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
info "停止并移除旧容器..."
docker stop "${CONTAINER_NAME}" 2>/dev/null || true
docker rm "${CONTAINER_NAME}" 2>/dev/null || true
fi
info "启动容器..."
docker run -d \
--name "${CONTAINER_NAME}" \
--restart unless-stopped \
-p 8088:8088 \
-v "${DATA_BASE}/monitor-data:/app/web/service_monitor/data" \
-v "${DATA_BASE}/data/users.json:/app/web/users.json" \
-v "${DATA_BASE}/data/搜索索引.json:/app/data/搜索索引.json:ro" \
-v "${DATA_BASE}/data/搜索向量.json:/app/data/搜索向量.json:ro" \
-v "${DATA_BASE}/logs:/app/web/logs" \
--env-file "${ENV_FILE}" \
-e FLASK_DEBUG=0 \
-e TROUBLESHOOT_ROOT=/app \
-e PYTHONIOENCODING=utf-8 \
--memory=1g \
--cpus=1.0 \
"${IMAGE_NAME}"
info "容器已启动: ${CONTAINER_NAME}"
}
# ---------- 健康检查 ----------
health_check() {
info "等待服务启动..."
local max_retries=15
local count=0
while [ $count -lt $max_retries ]; do
if curl -sf http://localhost:8088/api/health > /dev/null 2>&1; then
info "服务健康检查通过!"
echo ""
curl -s http://localhost:8088/api/health | python3 -m json.tool 2>/dev/null || \
curl -s http://localhost:8088/api/health
echo ""
info "访问地址: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo '192.168.5.60'):8088"
return 0
fi
count=$((count + 1))
echo " 等待中... ($count/$max_retries)"
sleep 2
done
error "健康检查失败,请查看日志: docker logs ${CONTAINER_NAME}"
}
# ---------- 查看状态 ----------
show_status() {
info "容器状态:"
docker ps -f "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || \
warn "容器 ${CONTAINER_NAME} 不存在"
echo ""
info "服务健康:"
curl -sf http://localhost:8088/api/health | python3 -m json.tool 2>/dev/null || \
warn "服务未响应"
}
# ---------- 停止容器 ----------
stop_container() {
info "停止容器 ${CONTAINER_NAME}..."
docker stop "${CONTAINER_NAME}" 2>/dev/null || warn "容器未在运行"
docker rm "${CONTAINER_NAME}" 2>/dev/null || true
info "容器已停止并移除"
}
# ---------- 主流程 ----------
main() {
local action="${1:-deploy}"
case "${action}" in
--build-only)
check_prerequisites
build_image
;;
--stop)
stop_container
;;
--status)
show_status
;;
deploy|"")
check_prerequisites
prepare_dirs
build_image
start_container
health_check
;;
*)
echo "用法: $0 [--build-only|--stop|--status]"
exit 1
;;
esac
}
main "$@"
version: '3.8' # Troubleshoot AI Assistant — Docker Compose
#
# 使用方式:
# docker compose up -d # 启动
# docker compose logs -f # 查看日志
# docker compose down # 停止
# docker compose up -d --build # 重新构建并启动
#
# 注意:
# - 单容器部署,不用 gunicorn 多 worker(APScheduler 不兼容多进程)
# - 不挂载 docker.sock(避免影响宿主机其他容器)
# - 资源限制防止单容器抢占宿主机资源
# ============================================================
services: services:
troubleshoot: troubleshoot:
build: . build:
context: .
dockerfile: Dockerfile
image: troubleshoot:latest
container_name: troubleshoot
restart: unless-stopped
ports: ports:
- "8088:8088" - "8088:8088"
restart: always
volumes: volumes:
# 配置与用户数据只读挂载(便于现场修改无需重建镜像) # 服务监测数据(targets/schedules/reports/notifications)
- ./config.json:/app/web/config.json:ro - monitor-data:/app/web/service_monitor/data
- ./users.json:/app/web/users.json:ro # 用户数据
- ./records:/app/web/records:ro - /opt/troubleshoot/data/users.json:/app/web/users.json
# 日志持久化到宿主机 # 知识库索引(只读)
- ./logs:/app/logs - /opt/troubleshoot/data/搜索索引.json:/app/data/搜索索引.json:ro
- /opt/troubleshoot/data/搜索向量.json:/app/data/搜索向量.json:ro
# 缓存目录
- cache-data:/app/web/cache
# 日志
- /opt/troubleshoot/logs:/app/web/logs
environment: environment:
- FLASK_DEBUG=0
- PYTHONIOENCODING=utf-8 - PYTHONIOENCODING=utf-8
- SECRET_KEY=${SECRET_KEY} - TROUBLESHOOT_ROOT=/app
# 离线模式开关:true=跳过 Claude API(离线 Q&A),false=在线调 API - SECRET_KEY=${SECRET_KEY:?SECRET_KEY is required}
- CLAUDE_API_BASE=${CLAUDE_API_BASE:-}
- CLAUDE_API_KEY=${CLAUDE_API_KEY:-}
- OFFLINE_MODE=${OFFLINE_MODE:-true} - OFFLINE_MODE=${OFFLINE_MODE:-true}
- MONITOR_ENC_KEY=${MONITOR_ENC_KEY:-}
# 资源限制(5.60 有其他服务容器,防止抢占资源)
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
reservations:
memory: 256M
cpus: '0.25'
# 健康检查
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8088/api/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
monitor-data:
driver: local
driver_opts:
type: none
o: bind
device: /opt/troubleshoot/monitor-data
cache-data:
...@@ -27,6 +27,7 @@ except ImportError: ...@@ -27,6 +27,7 @@ except ImportError:
HAS_NUMPY = False HAS_NUMPY = False
from utils.logger import get_logger from utils.logger import get_logger
from utils.paths import DATA_DIR, SEARCH_INDEX_DIR, VECTOR_INDEX_FILENAME
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -35,19 +36,20 @@ logger = get_logger(__name__) ...@@ -35,19 +36,20 @@ logger = get_logger(__name__)
# ============================================================ # ============================================================
SCRIPT_DIR = Path(__file__).resolve().parent # .../web SCRIPT_DIR = Path(__file__).resolve().parent # .../web
DATA_DIR = SCRIPT_DIR.parent # 部署时即 /opt/troubleshoot
# 搜索索引路径(按优先级查找:部署环境 → 开发环境) # 搜索索引路径(按优先级查找:统一路径 → 部署环境 → 开发环境)
SEARCH_INDEX_PATHS = [ SEARCH_INDEX_PATHS = [
DATA_DIR / "搜索索引.json", # 部署环境(/opt/troubleshoot/) SEARCH_INDEX_DIR / "搜索索引.json", # 统一路径(容器/裸机)
DATA_DIR / "搜索索引.json", # 裸机部署(/opt/troubleshoot/)
Path("E:/github/ubains-module-test/develop/Docs/PRD/问题知识库/搜索索引.json"), # 开发环境绝对路径 Path("E:/github/ubains-module-test/develop/Docs/PRD/问题知识库/搜索索引.json"), # 开发环境绝对路径
SCRIPT_DIR.parent.parent.parent.parent / "Docs" / "PRD" / "问题知识库" / "搜索索引.json", # 相对路径 SCRIPT_DIR.parent.parent.parent.parent / "Docs" / "PRD" / "问题知识库" / "搜索索引.json", # 相对路径
] ]
# 向量索引路径(与搜索索引同目录查找) # 向量索引路径(与搜索索引同目录查找)
VECTOR_INDEX_PATHS = [ VECTOR_INDEX_PATHS = [
DATA_DIR / "搜索向量.json", # 部署环境 SEARCH_INDEX_DIR / VECTOR_INDEX_FILENAME, # 统一路径(容器/裸机)
SCRIPT_DIR.parent.parent.parent.parent / "Docs" / "PRD" / "问题知识库" / "搜索向量.json", # 相对路径 DATA_DIR / VECTOR_INDEX_FILENAME, # 裸机部署
SCRIPT_DIR.parent.parent.parent.parent / "Docs" / "PRD" / "问题知识库" / VECTOR_INDEX_FILENAME, # 相对路径
] ]
# query 向量缓存上限 # query 向量缓存上限
......
...@@ -27,6 +27,9 @@ SHANGHAI_TZ = _pytz_timezone("Asia/Shanghai") ...@@ -27,6 +27,9 @@ SHANGHAI_TZ = _pytz_timezone("Asia/Shanghai")
_scheduler = None _scheduler = None
_scheduler_lock = threading.Lock() _scheduler_lock = threading.Lock()
# schedules.json 读写锁(防止多线程并发读-改-写导致状态丢失)
_schedules_lock = threading.Lock()
# 星期映射:数字 → 中文名 # 星期映射:数字 → 中文名
WEEKDAY_NAMES = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"} WEEKDAY_NAMES = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"}
...@@ -134,6 +137,7 @@ def _load_schedules() -> list: ...@@ -134,6 +137,7 @@ def _load_schedules() -> list:
def _save_schedules(schedules: list) -> None: def _save_schedules(schedules: list) -> None:
"""保存定时任务列表。""" """保存定时任务列表。"""
with _schedules_lock:
ensure_dirs() ensure_dirs()
SCHEDULES_FILE.write_text( SCHEDULES_FILE.write_text(
json.dumps(schedules, ensure_ascii=False, indent=2), json.dumps(schedules, ensure_ascii=False, indent=2),
...@@ -356,6 +360,7 @@ def toggle_enabled(schedule_id: str, enabled: Optional[bool] = None) -> dict: ...@@ -356,6 +360,7 @@ def toggle_enabled(schedule_id: str, enabled: Optional[bool] = None) -> dict:
def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str) -> dict: def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str) -> dict:
"""更新定时任务执行状态。""" """更新定时任务执行状态。"""
with _schedules_lock:
schedules = _load_schedules() schedules = _load_schedules()
for i, s in enumerate(schedules): for i, s in enumerate(schedules):
if s.get("id") == schedule_id: if s.get("id") == schedule_id:
...@@ -454,6 +459,9 @@ def _add_job(sched: dict) -> None: ...@@ -454,6 +459,9 @@ def _add_job(sched: dict) -> None:
id=job_id, id=job_id,
args=[sched["id"]], args=[sched["id"]],
replace_existing=True, replace_existing=True,
misfire_grace_time=3600, # 允许 1 小时内的错过执行(默认 1 秒太短)
coalesce=True, # 错过多次只执行一次
max_instances=1, # 同一 job 不并发(显式声明)
) )
logger.info("定时任务已注册: %s (%s) [Asia/Shanghai]", sched["name"], sched["cron"]) logger.info("定时任务已注册: %s (%s) [Asia/Shanghai]", sched["name"], sched["cron"])
except Exception as e: except Exception as e:
...@@ -518,7 +526,14 @@ def _execute_scheduled_job(schedule_id: str) -> None: ...@@ -518,7 +526,14 @@ def _execute_scheduled_job(schedule_id: str) -> None:
logger.warning("定时任务不存在或已禁用: %s", schedule_id) logger.warning("定时任务不存在或已禁用: %s", schedule_id)
return return
_run_job_body(schedule_id) # 检查是否已在执行中(防止定时触发与手动触发并发)
if sched.get("current_status") == "running":
logger.warning("定时任务已在执行中,跳过: %s", schedule_id)
return
# 使用后台线程执行,不阻塞 APScheduler 调度线程
t = threading.Thread(target=_run_job_body, args=(schedule_id,), daemon=True)
t.start()
def run_now(schedule_id: str) -> dict: def run_now(schedule_id: str) -> dict:
...@@ -566,6 +581,7 @@ def get_scheduler_status() -> dict: ...@@ -566,6 +581,7 @@ def get_scheduler_status() -> dict:
def _update_current_status(schedule_id: str, status: str) -> None: def _update_current_status(schedule_id: str, status: str) -> None:
"""更新定时任务当前执行状态(running/success/failed)。""" """更新定时任务当前执行状态(running/success/failed)。"""
with _schedules_lock:
schedules = _load_schedules() schedules = _load_schedules()
for i, s in enumerate(schedules): for i, s in enumerate(schedules):
if s.get("id") == schedule_id: if s.get("id") == schedule_id:
......
...@@ -58,3 +58,18 @@ AUDIT_LOG_FILE = SCRIPT_DIR / "audit.log" ...@@ -58,3 +58,18 @@ AUDIT_LOG_FILE = SCRIPT_DIR / "audit.log"
# 向量索引文件名(与搜索索引同目录部署) # 向量索引文件名(与搜索索引同目录部署)
VECTOR_INDEX_FILENAME = "搜索向量.json" VECTOR_INDEX_FILENAME = "搜索向量.json"
# 搜索索引目录(优先使用环境变量,否则自动检测)
# 容器化部署时 TROUBLESHOOT_ROOT=/app,索引文件在 /app/data/ 下
SEARCH_INDEX_DIR = os.environ.get('SEARCH_INDEX_DIR')
if SEARCH_INDEX_DIR:
SEARCH_INDEX_DIR = Path(SEARCH_INDEX_DIR)
elif (DATA_DIR / "data" / "搜索索引.json").exists():
# 容器化部署:/app/data/搜索索引.json
SEARCH_INDEX_DIR = DATA_DIR / "data"
elif (DATA_DIR / "搜索索引.json").exists():
# 裸机部署:/opt/troubleshoot/搜索索引.json
SEARCH_INDEX_DIR = DATA_DIR
else:
# 默认:与 DATA_DIR 同级
SEARCH_INDEX_DIR = DATA_DIR
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论