# PRD 计划执行 — SSH 连接测试失败修复

## 概述

| 项目 | 内容 |
|------|------|
| 需求来源 | PRD_问题处理_SSH连接测试失败.md |
| 计划日期 | 2026-07-20 |
| 预计工时 | 2 小时 |
| 优先级 | P1（影响功能可用性） |

## 目标

1. 增强 SSH 连接测试的错误信息反馈，区分不同错误类型
2. 前端显示详细错误信息，帮助用户排查问题
3. 部署脚本自动检查并安装服务器端依赖

## 修改清单

### 阶段一：后端错误分类

#### 1.1 修改 `executor.py`

**文件**: `skill/code/web/service_monitor/utils/executor.py`

**改动点**:

1. 模块顶层检查 paramiko 是否可用：
   ```python
   # 模块顶层
   _HAS_PARAMIKO = True
   try:
       import paramiko
   except ImportError:
       _HAS_PARAMIKO = False
   ```

2. `SSHExecutor.__init__()` 检查 paramiko：
   ```python
   def __init__(self, ...):
       if not _HAS_PARAMIKO:
           raise DependencyError("paramiko 未安装，请在服务器执行 pip install paramiko")
   ```

3. 新增 `DependencyError` 异常类

4. `test_connection()` 返回更详细的错误分类

**错误分类映射**:

| 异常类型 | 错误消息 | 错误码 |
|----------|----------|--------|
| `DependencyError` | paramiko 未安装，请在服务器执行 pip install paramiko | DEPENDENCY_MISSING |
| `paramiko.AuthenticationException` | 认证失败：用户名或密码错误 | AUTH_FAILED |
| `paramiko.SSHException` | SSH 协议错误：{detail} | SSH_ERROR |
| `socket.timeout` / `TimeoutError` | 连接超时：目标不可达或端口未开放 | TIMEOUT |
| `ConnectionRefusedError` | 连接被拒绝：目标端口未开放或 SSH 服务未运行 | CONNECTION_REFUSED |
| `socket.gaierror` | 主机名解析失败：{detail} | DNS_ERROR |
| 其他 | SSH 连接失败：{detail} | UNKNOWN |

#### 1.2 修改 `target_service.py`

**文件**: `skill/code/web/service_monitor/services/target_service.py`

**改动点**:

`test_connection()` 返回值增加 `detail` 和 `error_code` 字段：

```python
def test_connection(data: dict) -> dict:
    """测试远程目标 SSH 连通性（不落库）。
    
    Returns: {
        "success": bool,
        "message": str,
        "detail": str | None,    # 详细错误信息
        "error_code": str | None # 错误码
    }
    """
```

#### 1.3 修改 `routes.py`

**文件**: `skill/code/web/service_monitor/routes.py`

**改动点**:

`api_test_connection()` 返回完整结果：

```python
@bp.route('/api/service-monitor/targets/test', methods=['POST'])
def api_test_connection():
    guard = _require_admin_json()
    if guard:
        return guard
    result = target_service.test_connection(request.get_json(force=True) or {})
    return jsonify(result)  # 直接返回完整结果
```

### 阶段二：前端显示详细错误

#### 2.1 修改 `targets.html`

**文件**: `skill/code/web/templates/service_monitor/targets.html`

**改动点**:

测试连接结果显示 `detail` 字段：

```javascript
async function testConn() {
    const msg = document.getElementById('test-msg');
    // ...
    const d = await r.json();
    msg.style.color = d.success ? 'var(--green)' : 'var(--red)';
    if (d.success) {
        msg.textContent = '✓ ' + (d.message || '连接成功');
    } else {
        // 显示详细错误
        let errMsg = '✗ ' + (d.message || '连接失败');
        if (d.detail) {
            errMsg += ' (' + d.detail + ')';
        }
        msg.textContent = errMsg;
        msg.title = d.error_code || '';  // 鼠标悬停显示错误码
    }
}
```

### 阶段三：部署脚本增加依赖检查

#### 3.1 修改 `upload_to_server.py`

**文件**: `deploy/upload_to_server.py`

**改动点**:

在 `upload_files()` 开头增加依赖检查步骤：

```python
# 需要检查的 Python 包
REQUIRED_PACKAGES = ['paramiko', 'cryptography']

def _check_and_install_deps(ssh):
    """检查并安装服务器端 Python 依赖。"""
    for pkg in REQUIRED_PACKAGES:
        stdin, stdout, stderr = ssh.exec_command(
            f"python3 -c 'import {pkg}' 2>&1"
        )
        result = stdout.read().decode()
        if 'ModuleNotFoundError' in result or 'No module named' in result:
            print(f"  [INSTALL] {pkg} 未安装，正在安装...")
            stdin, stdout, stderr = ssh.exec_command(
                f"pip3 install --break-system-packages {pkg} 2>&1"
            )
            install_result = stdout.read().decode()
            if 'Successfully installed' in install_result:
                print(f"  [OK] {pkg} 安装成功")
            else:
                print(f"  [WARN] {pkg} 安装失败，请手动安装")
        else:
            print(f"  [OK] {pkg} 已安装")
```

在 `upload_files()` 中调用：

```python
print("\n[2/5] Checking dependencies...")
_check_and_install_deps(ssh)

print("\n[3/5] Backing up...")
# ...

print("\n[4/5] Uploading files...")
# ...

print("\n[5/5] Restarting service...")
# ...
```

## 测试计划

### 单元测试

```bash
cd skill/code && python -m pytest -v
# 预期：218 用例全绿
```

### API 测试

1. **测试 paramiko 未安装**（模拟）：
   - 返回 `error_code: DEPENDENCY_MISSING`

2. **测试认证失败**：
   ```bash
   curl -X POST http://192.168.5.60:8088/api/service-monitor/targets/test \
     -H "Content-Type: application/json" \
     -d '{"host":"192.168.5.44","port":22,"username":"wrong","password":"wrong"}'
   # 预期：{"success":false,"message":"认证失败：用户名或密码错误","error_code":"AUTH_FAILED"}
   ```

3. **测试连接超时**：
   ```bash
   curl -X POST http://192.168.5.60:8088/api/service-monitor/targets/test \
     -H "Content-Type: application/json" \
     -d '{"host":"192.168.5.99","port":22,"username":"root","password":"test"}'
   # 预期：{"success":false,"message":"连接超时：目标不可达或端口未开放","error_code":"TIMEOUT"}
   ```

4. **测试连接成功**：
   ```bash
   curl -X POST http://192.168.5.60:8088/api/service-monitor/targets/test \
     -H "Content-Type: application/json" \
     -d '{"host":"192.168.5.44","port":22,"username":"root","password":"Ubains@123"}'
   # 预期：{"success":true,"message":"连接成功"}
   ```

### 前端验证

1. 浏览器访问 http://192.168.5.60:8088/service-monitor/targets
2. 新建远程目标，填写错误凭据，点击测试连接
3. 确认显示详细错误信息（如"认证失败：用户名或密码错误"）

### 部署验证

1. 清理服务器上的 paramiko：
   ```bash
   pip3 uninstall -y paramiko
   ```

2. 重新运行部署脚本：
   ```bash
   cd deploy && SSH_PASSWORD='***' python upload_to_server.py
   ```

3. 确认输出包含 `[INSTALL] paramiko 未安装，正在安装...` 和 `[OK] paramiko 安装成功`

## 回滚方案

如果修复引入新问题：

1. 回退 `executor.py`、`target_service.py`、`routes.py`、`targets.html` 的改动
2. 重新部署旧版本代码
3. 服务器端手动安装 paramiko 作为临时方案

## 验收标准

1. ✅ 测试连接返回详细错误信息，区分不同错误类型
2. ✅ 前端显示具体错误原因
3. ✅ 部署脚本自动检查并安装 paramiko
4. ✅ 单元测试全部通过
5. ✅ API 测试和前端验证通过

## 元信息

- 编写人：Claude Code
- 审核人：待定
- 状态：待实施