# HANDOFF — SSH sudo 自动检测 + Docker 权限适配

> 最后更新：2026-08-05 ｜ 分支：troubleshoot-ai-assistant ｜ 负责人：czj

---

## 1. 我们在做什么

**背景**：用户在 9.89 服务器上执行巡检时，发现 Docker 容器显示为"未运行"，但实际容器在运行。根本原因是 SSH 用户 `openkylin` 不在 docker 组，需要 `sudo docker` 才能执行 docker 命令，而巡检脚本直接执行 `docker ps` 导致权限拒绝。

**目标**：实现 SSH sudo 自动检测和提权，让巡检脚本在非 root 用户下也能正确检测 Docker 容器。

**来源**：用户反馈巡检报告显示容器未运行，9.89 上实际有 12 个容器在运行。

---

## 2. 已经完成了什么

### 2.1 SSHExecutor sudo 自动检测 ✅

修改 `skill/code/web/service_monitor/utils/executor.py`：
- `_detect_sudo_need()` 方法连接后自动检测：
  1. 先测试 `docker ps` 是否可直接执行
  2. 失败则测试 `sudo -n docker ps`
  3. 尝试用 SSH 密码自动配置免密 sudoers
  4. 配置失败时给出明确提示
- `_exec()` 方法根据 `use_sudo` 自动用 `sudo bash -c` 包裹命令
- `_try_configure_passwordless_sudo()` 尝试自动写入 `/etc/sudoers.d/<username>`

### 2.2 Docker 检测脚本 sudo 适配 ✅

修改 `skill/code/web/service_monitor/assets/service/20_docker_basic.sh`：
- 新增 `_detect_docker_sudo()` 函数，自动检测 `docker` 或 `sudo docker`
- 所有 docker 命令使用 `$DOCKER_CMD` 变量
- 每个检测函数开头增加 `if [ -z "$DOCKER_CMD" ]` 保护

### 2.3 前端目标管理增强 ✅

修改 `skill/code/web/templates/service_monitor/targets.html`：
- 添加"需要 sudo 提权"复选框
- 目标列表显示 sudo 状态列
- 编辑功能完整实现：`openEdit()` 通过 API 回填数据

### 2.4 后端数据支持 ✅

修改 `skill/code/web/service_monitor/services/target_service.py`：
- `create_target()` / `update_target()` 支持 `use_sudo` 字段
- `list_targets()` / `_build_view()` 返回 `use_sudo` 状态

修改 `skill/code/web/service_monitor/routes.py`：
- 新增 `GET /api/service-monitor/targets/<target_id>` API（编辑回填用）

### 2.5 部署到 5.60 ✅

- 重建 Docker 镜像（包含最新 sudo 自动检测代码）
- 修复端口冲突（systemd 与 Docker 容器抢占 8088，已禁用 systemd）
- 修复前端文件缺失（`/opt/troubleshoot/dist/` 为空，已恢复）
- 修复搜索索引文件错误（目录→文件，volume 挂载覆盖）
- 修复通知配置解密失败（`notifications.json` 中旧密文无法解密，已清除）

### 2.6 代码提交 ✅

提交 `65b25c4d` 到 `troubleshoot-ai-assistant` 分支，已推送到远程。

---

## 3. 当前卡在哪

**无卡点**。所有功能已完成并部署验证。

5.60 服务状态：
- 健康检查：✅ ok
- 前端页面：✅ `http://192.168.5.60:8088/`
- 通知 API：✅ 已修复（需重新配置钉钉/邮箱凭据）
- 搜索引擎：✅ TF-IDF 模式
- 定时任务：✅ 2 个任务运行中

---

## 4. 下一步计划

| 优先级 | 任务 | 说明 |
|--------|------|------|
| **P1** | 验证 9.89 巡检 | 在目标管理添加 9.89（主机：192.168.9.89，用户：openkylin），运行全量巡检，验证 Docker 容器检测是否正常显示 12 个容器 |
| **P2** | 验证定时任务 | 明天 08:30/09:00 检查定时任务是否自动执行，巡检报告是否生成 |
| **P3** | 重新配置通知凭据 | 进入通知配置页面重新填写钉钉 secret（容器重建后加密密钥变更，旧密文无法解密） |
| **P4** | 为 `MONITOR_ENC_KEY` 设置固定值 | 在 `docker-compose.yml` 的 environment 中设置固定的加密密钥，避免每次重建容器后密钥变更导致凭据解密失败 |

---

## 5. 踩过的坑（绝对不要再踩）

### 坑 1：Docker 容器与 systemd 服务端口冲突

**坑**：Docker 容器和 systemd 服务同时运行抢占 8088 端口，systemd 每 6 秒重启一次，累计重启 9300+ 次。定时任务因服务不稳定无法执行。

**原因**：部署脚本只更新宿主机文件并重启 systemd 服务，没检查 Docker 容器是否已在运行。容器先启动占了端口，systemd 后启动失败。

**避免方法**：
```bash
# 部署前先检查
sudo docker ps | grep troubleshoot  # 如果有容器在运行
sudo systemctl disable troubleshoot  # 禁用 systemd，避免冲突
# 代码更新必须重建镜像
cd /data/third_party/monitor-platform && sudo docker compose up -d --build
```

### 坑 2：Docker 镜像内代码未随部署更新

**坑**：部署脚本只更新宿主机 `/opt/troubleshoot/web/` 下的文件，但 Docker 镜像构建时 `COPY skill/code/web/ /app/web/` 复制代码，容器内代码仍是旧版本。sudo 自动检测功能部署后不生效。

**原因**：代码文件不在 volume 挂载范围内（只有 data/dist/logs 通过 volume 挂载），必须重建镜像才能更新容器内代码。

**避免方法**：
```bash
# 代码更新后必须重建镜像
sudo rsync -av --delete /opt/troubleshoot/web/ /data/third_party/monitor-platform/skill/code/web/
cd /data/third_party/monitor-platform && sudo docker compose up -d --build
```

### 坑 3：volume 挂载文件类型不匹配导致容器启动失败

**坑**：`搜索索引.json` 被误创建为目录，docker-compose 挂载时报错 `not a directory: Are you trying to mount a directory onto a file`，容器无法启动。

**原因**：`docker compose down -v` 删除 volumes 后重建时，宿主机上 `/opt/troubleshoot/data/搜索索引.json` 被错误创建为目录。docker-compose 试图将目录挂载到镜像内的文件位置，类型不匹配。

**避免方法**：
```bash
# 重建前检查挂载文件类型
sudo file /opt/troubleshoot/data/搜索索引.json  # 应输出：JSON text data
# 如果是目录，先删除再复制正确的文件
sudo rm -rf /opt/troubleshoot/data/搜索索引.json
sudo cp /data/third_party/monitor-platform/data/搜索索引.json /opt/troubleshoot/data/
# 必须先停止容器再修改挂载文件
sudo docker stop troubleshoot
```

### 坑 4：通知配置解密失败导致 API 500

**坑**：`notifications.json` 中钉钉 secret 用旧密钥加密（`enc:gAAAAA...`），重建容器后 `MONITOR_ENC_KEY` 变更，解密失败导致通知 API 返回 500。

**原因**：Fernet 加密密钥每次容器启动可能不同（如果 `MONITOR_ENC_KEY` 环境变量未固定），旧密文无法解密。

**避免方法**：在 `docker-compose.yml` 中设置固定的 `MONITOR_ENC_KEY` 环境变量。已清除无法解密的加密字段作为临时修复。

### 坑 5：sudo 检测逻辑不完整——只测用户权限没测命令权限

**坑**：最初只检测 `sudo -n whoami`（用户是否有免密 sudo），没检测 Docker 命令权限。9.89 上 openkylin 有免密 sudo 但不在 docker 组，`docker ps` 仍失败。

**原因**：想当然认为"有 sudo 权限就能执行所有命令"，忽略了 docker 组权限。

**避免方法**：检测要具体到命令本身。正确逻辑：先测 `docker ps`，失败再测 `sudo -n docker ps`，而不是只测 `sudo -n whoami`。

### 坑 6：前端 dist 目录被空 volume 覆盖

**坑**：docker-compose 将 `/opt/troubleshoot/dist` 挂载到容器的 `/data/dist`，但 `/opt/troubleshoot/dist` 是空目录，覆盖了镜像内构建时复制的前端文件，导致首页 403。

**原因**：`docker compose down -v` 删除旧 volume 后，新建的 volume 目录为空。

**避免方法**：重建后必须将前端文件复制到挂载目录：
```bash
sudo cp -r /data/third_party/monitor-platform/frontend/dist/* /opt/troubleshoot/dist/
```

---

## 6. 关键文件与命令速查

### 本次修改的核心文件

| 文件 | 改动 |
|------|------|
| `skill/code/web/service_monitor/utils/executor.py` | SSHExecutor sudo 自动检测（+150 行） |
| `skill/code/web/service_monitor/services/target_service.py` | use_sudo 字段支持（+10 行） |
| `skill/code/web/service_monitor/routes.py` | 新增 GET 单个目标 API（+12 行） |
| `skill/code/web/service_monitor/assets/service/20_docker_basic.sh` | Docker 检测 sudo 适配（+69 行） |
| `skill/code/web/templates/service_monitor/targets.html` | 前端 sudo 选项 + 编辑回填（+37 行） |

### 部署命令

```bash
# 同步代码到构建目录
sudo rsync -av --delete /opt/troubleshoot/web/ /data/third_party/monitor-platform/skill/code/web/

# 重建并启动容器
cd /data/third_party/monitor-platform && sudo docker compose up -d --build

# 恢复前端文件
sudo cp -r /data/third_party/monitor-platform/frontend/dist/* /opt/troubleshoot/dist/

# 健康检查
curl http://localhost:8088/api/health
```

### 调试命令

```bash
# 测试 SSH sudo（9.89）
ssh openkylin@192.168.9.89 "whoami && sudo -n whoami && docker ps 2>&1 | head -3 && sudo docker ps 2>&1 | head -3"

# 容器内测试通知服务
sudo docker exec troubleshoot python3 << 'EOF'
import sys; sys.path.insert(0, '/app/web')
from service_monitor.services import notification_service
import json; print(json.dumps(notification_service.get_config_masked(), ensure_ascii=False, indent=2)[:300])
EOF

# 查看容器日志
sudo docker logs troubleshoot --tail 50

# 检查容器内文件
sudo docker exec troubleshoot ls -la /app/data/
sudo docker exec troubleshoot ls -la /app/web/service_monitor/data/
```

### 环境配置

```bash
# docker-compose.yml 关键环境变量
LOCAL_SSH_USER=ubains
LOCAL_SSH_PASSWORD=Ubains@123
MONITOR_ENC_KEY=  # 建议设为固定值，避免重建后凭据解密失败
```

### 登录凭据

| 服务 | 地址 | 用户名 | 密码 |
|------|------|--------|------|
| Web 管理员 | http://192.168.5.60:8088 | admin | Ubains@1357 |
| 5.60 SSH | 192.168.5.60 | ubains | Ubains@123 |
| 9.89 SSH | 192.168.9.89 | openkylin | Ubains@123 |
