# PRD 需求文档 — 问题排查助手离线部署方案

> **适用范围**：本文档仅针对「问题排查助手」模块的离线部署。服务管理、服务监测模块的容器化方案另立文档。

> 版本：2.2 ｜ 日期：2026-07-16 ｜ 分支：troubleshoot-ai-assistant

---

## 1. 需求背景

运行维护平台部署到客户现场服务器时，通常处于**内网/离线环境**，无法访问外部 AI API（Claude）。当前问题排查助手依赖 Claude API 进行 AI 分析，在离线环境下不可用。

同时，现场部署采用**容器化（Docker）**方式，不直接部署在宿主机上。

> **范围声明**：本方案**仅覆盖问题排查助手模块**的离线化与容器化。服务管理、服务监测两个模块的容器化部署方案见各自独立文档。三者共用同一 Docker 镜像底座还是分镜像，由后续总部署方案统筹决策。

---

## 2. 现状分析

| 模块 | 外部依赖 | 离线可用性 | 备注 |
|------|----------|-----------|------|
| 问题排查助手 | Claude API + TF-IDF 搜索 | ❌ AI 不可用 | 搜索可用（TF-IDF，纯本地运行），AI 分析不可用 |
| 服务管理 | 无（占位） | ✅ | 后续走 SSH 连接内部系统 |
| 服务监测 | SSH（局域网） | ✅ | SSH 连接目标服务器 |
| 平台首页 / 认证 | 无（Flask 本地） | ✅ | — |

> **上表包含服务管理/服务监测仅用于对比**，说明为何只有问题排查助手需要离线改造。二者的隔离设计见 §3.7。

**核心卡点**：问题排查助手的 AI 分析功能依赖外网 API。

**搜索选型说明**：本方案搜索引擎采用 **TF-IDF**（词频-逆文档频率），而非 Embedding 向量搜索。原因有二：
1. 向量搜索需在线调用 `/v1/embeddings` API 预计算向量文件（`搜索向量.json`），会产生额外 API 费用；
2. 知识库更新后需重新跑预计算脚本，离线环境无法完成。
TF-IDF 为纯本地计算，零外部依赖、零额外费用，满足离线部署需求。

---

## 3. 离线方案设计

### 3.1 问题排查助手 — 离线 Q&A 模式

**原理**：跳过 Claude API 调用，直接将 TF-IDF 搜索匹配结果作为排查建议返回。

> **为什么是 TF-IDF 而不是向量搜索？** 项目已完成 P2-1 向量搜索升级，但向量搜索需要在联网环境下调用 `/v1/embeddings` API 预计算 `搜索向量.json`，会产生额外费用。离线模式下 TF-IDF 纯本地运行、零外部依赖，是最安全的选择。

**流程对比**：

```
【在线模式】
用户提问 → TF-IDF 搜索 → 匹配结果 + 提问 → Claude API → AI 分析报告

【离线模式】
用户提问 → TF-IDF 搜索 → 匹配结果格式化 → 直接返回离线排查建议
```

**离线模式返回内容**：

| 区域 | 在线模式 | 离线模式 |
|------|---------|---------|
| 标题 | 🤖 AI 分析结果 | 📋 离线排查参考（基于知识库匹配） |
| 匹配案例列表 | 简要列表 | 完整记录（标题 + 现象 + 解决方案 + 相似度） |
| AI 分析 | Claude 生成 | 无（标注"离线模式，无 AI 分析"） |
| 响应时间 | 20-60 秒 | <1 秒 |
| 横幅提示 | 无 | "当前为离线模式，结果基于历史知识库匹配" |

**匹配案例展示格式（详细版）**：
```
案例1（相似度：92%）
  项目：厦门银行
  标题：门口屏 MQTT 绑定失败
  现象：门口屏绑定时报 MQTT 连接错误，无法完成绑定
  解决方案：升级门口屏到 5.0+ 版本，使用默认 MQTT 凭据
  参考文件：record_001.md

案例2（相似度：75%）
  项目：展厅
  标题：门口屏不显示会议信息
  现象：会议开始后门口屏不显示会议主题和参会人
  解决方案：检查 EMQX 容器状态，确认 docker ps -a | grep uemqx 有输出
  参考文件：record_002.md
```

### 3.2 配置开关 — 环境变量

`offline_mode` 通过**环境变量**控制，与 `SECRET_KEY` 保持一致（不从 `config.json` 读取），避免配置随代码暴露。

```bash
# .env 文件或 docker-compose environment 块
OFFLINE_MODE=true     # true = 离线模式（跳过 API），false = 在线模式（调 Claude API）
```

- `OFFLINE_MODE=false`（默认未设置）— 在线模式，调 Claude API
- `OFFLINE_MODE=true` — 离线模式，跳过 API，直接返回搜索结果

**生效方式**：进程启动时读取一次，修改后需重启容器。配置热更新本期不做。

### 3.3 部署方式 — 容器化

现场服务器采用 Docker 容器化部署，不直接部署在宿主机：

```
宿主机
├── Docker
│   ├── troubleshooot 容器（运行维护平台）
│   │   ├── Flask/Gunicorn + Python
│   │   ├── 知识库（357 条记录）
│   │   └── 配置文件
│   └── 其他服务容器
```

**Dockerfile 要点**：
- 基础镜像：`python:3.10-slim`
- 依赖：Flask, scikit-learn, scipy, numpy, jieba（TF-IDF 搜索引擎所需）
- **不需要** requests 的 API 调用（离线模式），但保留以便在线模式切换
- 端口：8088
- 启动命令：`gunicorn -w 4 -b 0.0.0.0:8088 server:app`（生产）或 `python server.py`（调试）

**容器化优势**：
- 环境一致性：镜像打包后现场直接 `docker run`，无需手动安装 Python 依赖
- 进程守护：`docker restart policy = always`，容器挂了自动重启
- 资源隔离：不污染宿主机环境

### 3.4 API 行为变更

**`POST /api/troubleshoot`**：

| 场景 | 在线模式 | 离线模式 |
|------|---------|---------|
| 搜索 | TF-IDF | TF-IDF |
| AI 分析 | 调 Claude API | 跳过，格式化搜索结果 |
| 返回 | `{analysis, cases}` | `{analysis: null, cases, offline: true}` |
| 耗时 | 20-60 秒 | <1 秒 |

**`POST /api/analyze`**：同上。

**`POST /api/analyze_stream`**：

**保留原有 SSE 逻辑**，离线模式不调用流式 API，但响应仍走 SSE 协议（前端逻辑无需修改）。区别在于：在线模式多帧推送 AI 生成文本，离线模式**单帧推送**完整匹配结果后即 `close`。

```text
data: {"success": true, "offline": true, "analysis": null, "matched_cases": [...]}

[event: close]
```

**前端无需改动**：前端按现有 SSE 解析逻辑读取 `data` 帧，渲染 `matched_cases`；`analysis: null` 时前端显示"当前为离线模式，无 AI 分析"提示。

### 3.5 /api/health 响应

新增 `offline_mode` 字段（来源：环境变量 `OFFLINE_MODE`），与现有 `search.mode` 字段并存：

```json
{
  "status": "ok",
  "version": "1.4.0",
  "search": {"mode": "tfidf"},
  "offline_mode": true
}
```

**字段关系说明**：
- `search.mode`：搜索引擎当前模式（离线镜像下应为 `tfidf`，见 §4.4 向量文件处理）
- `offline_mode`：是否跳过 Claude API 走离线 Q&A 模式

两个开关相互独立。合法组合：
| offline_mode | search.mode | 场景 |
|--------------|-------------|------|
| true | tfidf | 纯离线现场（本方案默认） |
| false | vector | 在线环境 + 向量搜索 |
| false | tfidf | 在线环境 + TF-IDF（省资源） |

> `offline_mode: true` + `search.mode: vector` 理论上可行（向量查询不需联网），但本方案离线镜像不打包向量文件，故该组合不会出现。

### 3.6 代码实现分层

遵循项目 P1-3 三层架构约定（routes/services/utils 分层）：

| 层 | 文件 | 职责 |
|----|------|------|
| utils | `utils/config.py`（或 container.py 集中） | 读取环境变量 `OFFLINE_MODE`，返回 bool |
| services | `services/ai_service.py` | 新增 `build_offline_response(cases)` 函数：格式化 TF-IDF 匹配结果为离线响应（标题/横幅/案例详情） |
| routes | `routes/troubleshoot.py` | `/api/troubleshoot`、`/api/analyze`、`/api/analyze_stream` 三个路由入口判断 `OFFLINE_MODE`，为 true 时调用 `build_offline_response` 并跳过 Claude API |

**约束**：
- 离线格式化逻辑统一走 `ai_service.build_offline_response()`，两个路由不各自重写
- 不修改 `search_engine.py` / `safety_filter.py` / `cache_manager.py` 公开接口
- `cache_manager.py` 在离线模式下照常运行（空跑缓存，不动接口）
- 异常处理遵循 P1-1 规范：禁止空 `except:`，必须记录日志

### 3.7 模块隔离边界（部署层隔离）

三个模块（问题排查助手、服务管理、服务监测）共用同一个 Flask 进程，通过 Blueprint 平行注册（`server.py` 第 86-101 行）。**本方案采用部署层隔离**：代码保持现有物理分层不变，离线改造严格限定在问题排查助手的边界内，不触碰另外两个模块。

**现状（已天然隔离）**：
| 归属 | 文件 | 说明 |
|------|------|------|
| 问题排查助手 | `routes/troubleshoot.py`、`services/ai_service.py`、`services/record_service.py` | 独立 Blueprint，无跨模块 import |
| 服务管理 | `routes/service_manage.py` | 平行 Blueprint |
| 服务监测 | `routes/service_monitor.py` | 平行 Blueprint |
| 公共 | `routes/auth.py`、`routes/platform.py`、`utils/*` | 三模块共享 |

**隔离红线（离线改造必须遵守）**：
1. **改动范围仅限问题排查助手文件**：`routes/troubleshoot.py`、`services/ai_service.py`，以及 `container.py`/`utils` 中新增的 `OFFLINE_MODE` 读取。**禁止修改** `service_manage.py`、`service_monitor.py`。
2. **`OFFLINE_MODE` 只作用于问题排查助手路由**：该开关仅在 `troubleshoot.py` 的三个路由内判断，不影响服务管理/服务监测的任何行为。服务监测的 SSH 连接、服务管理的后续功能不受离线开关约束。
3. **共享工具层保持中立**：`utils/*`、`auth.py`、`platform.py` 的改动若发生，必须对三模块无差别兼容，不得引入问题排查助手专属逻辑。
4. **container.py 单例边界**：`container.py` 现仅持有问题排查助手的单例（search_engine/safety_filter/cache_manager）。新增 `OFFLINE_MODE` 读取属于问题排查助手配置，不得在此混入另两个模块的状态。
5. **验证要求**：离线改造后需回归测试确认服务管理/服务监测的现有路由行为无变化（响应结构、状态码不变）。

> 本方案为**部署层隔离（理解 A）**，非源码拆包。若后续需要三模块各自独立镜像/独立进程，属另一次重构任务，不在本 PRD 范围。

---

## 4. 容器化部署方案

### 4.1 Dockerfile

```dockerfile
FROM python:3.10-slim

WORKDIR /app

# 安装依赖（含在线模式所需的 requests，镜像兼容在线/离线切换）
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

# 拷贝代码
COPY skill/code/web/ /app/web/
COPY skill/code/SKILL.md /app/SKILL.md

# 端口
EXPOSE 8088

# 启动（gunicorn 需 server.py 暴露模块级 app 变量）
ENV PYTHONIOENCODING=utf-8
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8088", "server:app"]
```

**`requirements.txt`（版本锁定，离线环境复现性保障）**：

```text
flask==3.0.3
gunicorn==22.0.0
scikit-learn==1.4.2
scipy==1.13.0
numpy==1.26.4
jieba==0.42.1
werkzeug==3.0.3
requests==2.32.3
cryptography==46.0.7
paramiko==4.0.0
```

> - `requests` 保留——离线镜像需兼容在线模式切换（容器改 `OFFLINE_MODE=false` 即可调 Claude API）。
> - `cryptography` / `paramiko` 为**同进程邻居 service_monitor 子包的依赖**（见 §4.6），非问题排查助手自身所需，但同一镜像同一 Flask 进程必须具备，否则启动即崩。
> - 版本以本地实测通过为准（service_monitor 51 + 问题排查助手 167 用例全绿）；在线构建环境若解析失败再调整。

### 4.1.1 gunicorn 启动路径校验

项目 `server.py` 第 106-107 行已有模块级 `app = create_app()`，gunicorn `server:app` 可正常加载。无需额外修改。

### 4.2 docker-compose.yml（推荐）

```yaml
version: '3.8'
services:
  troubleshoot:
    build: .
    ports:
      - "8088:8088"
    restart: always
    volumes:
      - ./config.json:/app/web/config.json:ro
      - ./users.json:/app/web/users.json:ro
      - ./records:/app/web/records:ro
      - ./logs:/app/logs
    environment:
      - PYTHONIOENCODING=utf-8
      - SECRET_KEY=${SECRET_KEY}
      - OFFLINE_MODE=${OFFLINE_MODE:-true}
```

### 4.3 现场部署流程

```
# 1. 在线环境构建镜像
docker build -t troubleshoot:latest .

# 2. 导出镜像
docker save troubleshoot:latest | gzip > troubleshoot.tar.gz

# 3. 拷贝到现场服务器（U盘/内网传输）
scp troubleshoot.tar.gz user@field-server:/opt/

# 4. 现场服务器加载并启动
docker load < troubleshoot.tar.gz
docker compose up -d
```

### 4.4 向量文件处理（关键）

离线镜像**不打包 `搜索向量.json`**。原因：SearchEngine 初始化时若检测到向量文件存在，会**静默走 vector 模式**（见 CLAUDE.md 避坑第 7 条），与"离线用 TF-IDF"的设计矛盾。

**处理规则**：
- 构建镜像时确保 `搜索向量.json` 不被 `COPY` 进容器（`.dockerignore` 排除或 records 目录不含该文件）
- 部署后验证 `/api/health` 返回 `search.mode: tfidf`，确认未误走向量模式
- `搜索索引.json`（TF-IDF 索引）**必须打包或挂载**，SearchEngine 初始化依赖它

### 4.5 路径一致性校验

`utils/paths.py` 用 `Path(__file__).parent` 推导路径。容器内代码位于 `/app/web/`，故：
- `SCRIPT_DIR` = `/app/web/`
- `RECORDS_DIR` = `/app/web/records/` ← 与 docker-compose 挂载点 `/app/web/records` 一致 ✅

部署后需验证知识库记录数（357 条）正确加载，确认路径未偏移。

### 4.6 同进程模块依赖耦合（运行时事实）

本方案采用 §3.7 的**部署层隔离**——问题排查助手与 service_monitor 在代码层无相互 import、数据分离。但**运行时同处一个 Flask 进程**（`server.py` 的 `create_app()` 一次性注册全部 Blueprint）。这意味着：

> **邻居模块的导入失败，会连累整个进程启动失败，问题排查助手也随之不可用。**

**影响来源**（service_monitor 子包）：
| 依赖 | 导入方式 | 失败时机 | 影响 |
|------|----------|----------|------|
| `cryptography` | `utils/crypto.py` 模块级 `from cryptography.fernet import Fernet` | `create_app()` 阶段 | **启动即崩**，问题排查助手无法启动 |
| `paramiko` | `executor.py` 函数内延迟 `import paramiko`（第 249 行） | 调用 SSH 监测时 | 启动不崩，点 SSH 功能才崩 |

**处理决策（本期采用）**：
- **补依赖**：将 `cryptography` / `paramiko` 纳入 `requirements.txt`（见 §4.1）。承认运行时同进程这一既定事实，而非制造耦合。
- **不拆镜像 / 不拆进程**：拆分属另一次架构重构，超出本 PRD 范围；且会破坏 service_monitor 已做好的独立子包设计。
- **不延迟加载 cryptography**：改 service_monitor 源码会违背 §3.7 隔离红线（不改 service_monitor）。

**可选增强（本期不做）**：若需让问题排查助手不受 service_monitor 导入失败连累，可在 `server.py` 注册 Blueprint 时包一层容错 try/except，使单模块加载失败时其余模块仍能起。该项涉及 `server.py`（service_monitor 重构进行中），待该重构稳定后另行评估，避免文件冲突。

> **本节是设计妥协的如实记录**：部署层隔离 ≠ 进程隔离。同进程邻居的硬依赖必须随镜像一起提供。

---

## 5. 非功能需求

| 项目 | 要求 |
|------|------|
| 配置切换 | 改环境变量 `OFFLINE_MODE` 即可，无需改代码（修改后重启容器生效） |
| 搜索质量 | 离线模式下 TF-IDF 搜索逻辑不变，纯本地运行 |
| 响应速度 | 离线模式排查 <1 秒（无 API 调用） |
| 前端适配 | 离线模式结果展示清晰标注"离线模式"；`analysis: null` 时显示"当前为离线模式，无 AI 分析" |
| 流式兼容 | 离线模式保留 SSE 协议，前端解析逻辑无需改动 |
| 容器化 | 提供 Dockerfile + docker-compose.yml + requirements.txt（版本锁定） |
| 进程守护 | Docker restart policy always + gunicorn 多 worker |
| 日志持久化 | 日志挂载到宿主机 volume |
| 镜像兼容性 | 离线镜像同时支持在线模式（保留 requests + Claude 依赖）；同进程 service_monitor 依赖（cryptography/paramiko）一并打入镜像（见 §4.6） |

---

## 6. 约束

- 离线模式下不提供 AI 分析和步骤建议，仅返回知识库匹配结果
- 离线模式不影响现有在线模式行为（环境变量开关隔离）
- 不修改 `search_engine.py` / `safety_filter.py` / `cache_manager.py` 公开接口
- 离线镜像**不打包 `搜索向量.json`**，强制走 TF-IDF（见 §4.4）
- `OFFLINE_MODE` 启动时读取一次，配置热更新本期不做
- **模块隔离红线**：离线改造改动范围仅限问题排查助手（`routes/troubleshoot.py`、`services/ai_service.py` 及新增的 `OFFLINE_MODE` 读取），禁止修改 `service_manage.py` / `service_monitor.py`（见 §3.7）
- Docker 镜像构建在在线环境完成，现场只需 `docker load` + `docker run`

---

## 7. 后续扩展（本期不做）

- **本地小模型推理**：Docker 内嵌 Ollama + 小参数模型
- **知识库离线导入**：支持通过平台界面上传离线 Q&A 文档
- **配置热更新**：修改 `OFFLINE_MODE` 后无需重启容器