# PRD_计划执行_P1级代码质量优化

## 1. 项目概述

### 1.1 项目背景

修复代码质量分析发现的三个 P1 级别问题，提升代码可维护性和稳定性。

### 1.2 项目目标

| 目标编号 | 描述 | 优先级 |
|----------|------|--------|
| P1-1 | 规范异常处理，消除静默失败 | 高 |
| P1-2 | 建立 pytest 单元测试框架 | 高 |
| P1-3 | 架构分层，拆分 server.py | 高 |

### 1.3 开发周期

**总计：4 天**

---

## 2. 技术方案

### 2.1 P1-1：异常处理规范化

#### 2.1.1 定义统一错误码

```python
# utils/error_codes.py
class ErrorCodes:
    # 通用错误
    UNKNOWN_ERROR = (1000, "未知错误")
    INVALID_PARAM = (1001, "参数错误")
    
    # 搜索相关
    SEARCH_INDEX_ERROR = (2000, "搜索索引加载失败")
    SEARCH_QUERY_ERROR = (2001, "搜索查询失败")
    
    # 缓存相关
    CACHE_READ_ERROR = (3000, "缓存读取失败")
    CACHE_WRITE_ERROR = (3001, "缓存写入失败")
    
    # AI 相关
    AI_API_ERROR = (4000, "AI API 调用失败")
    AI_RESPONSE_ERROR = (4001, "AI 响应解析失败")
```

#### 2.1.2 规范异常处理

```python
# 修改前
except:
    pass

# 修改后
import logging
logger = logging.getLogger(__name__)

except FileNotFoundError as e:
    logger.error(f"缓存文件不存在: {e}")
    return None
except json.JSONDecodeError as e:
    logger.error(f"缓存文件 JSON 解析失败: {e}")
    return None
except Exception as e:
    logger.exception(f"缓存读取未知异常: {e}")
    return None
```

#### 2.1.3 统一 API 错误响应

```python
# utils/response.py
def error_response(error_code, message=None):
    """统一错误响应格式"""
    code, default_msg = error_code
    return {
        "success": False,
        "error": {
            "code": code,
            "message": message or default_msg
        }
    }
```

---

### 2.2 P1-2：添加单元测试

#### 2.2.1 配置 pytest

```toml
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --tb=short"

[tool.coverage.run]
source = ["skill/code/web"]
omit = ["*/tests/*"]

[tool.coverage.report]
fail_under = 80
```

#### 2.2.2 测试目录结构

```
skill/code/tests/
├── conftest.py              # 公共配置
├── test_search_engine.py    # 搜索引擎测试
├── test_safety_filter.py    # 安全过滤器测试
├── test_cache_manager.py    # 缓存管理测试
└── fixtures/                # 测试数据
    └── sample_records.json
```

#### 2.2.3 测试用例示例

> ⚠️ 注：`safety_filter` 是**函数式模块，无 `SafetyFilter` 类**。测试直接 import 顶层函数。
> `filter_output` 对危险行是**替换为安全提示行**（非整行删除），断言要查原命令不在结果文本 + `removed_lines` 非空。

```python
# tests/test_safety_filter.py
import safety_filter
from safety_filter import filter_output, filter_sensitive_info, filter_output_with_sensitive

class TestSafetyFilter:

    def test_dangerous_command_rm_filtered(self):
        """测试 rm -rf 危险命令被过滤（行被替换为安全提示）"""
        text = "执行 rm -rf / 删除所有文件"
        result = filter_output(text, "删除文件")
        assert result['safe'] is False
        assert len(result['removed_lines']) == 1
        assert "rm -rf" not in result['filtered_text']
        assert "已自动过滤" in result['filtered_text']

    def test_dangerous_command_restart_filtered(self):
        """测试 systemctl restart 被过滤"""
        text = "systemctl restart docker"
        result = filter_output(text, "重启服务")
        assert result['safe'] is False
        assert "restart" not in result['filtered_text']

    def test_safe_command_allowed(self):
        """测试安全命令不被过滤"""
        text = "docker ps -a 查看容器状态"
        result = filter_output(text, "查看容器")
        assert result['safe'] is True
        assert "docker ps" in result['filtered_text']

    def test_sensitive_password_masked(self):
        """测试敏感信息脱敏"""
        text = "密码是 password=secret123"
        out = filter_sensitive_info(text)
        assert "secret123" not in out
        assert "***" in out
```

#### 2.2.4 requirements.txt 更新

```
# 测试依赖
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-mock>=3.0.0
```

---

### 2.3 P1-3：架构分层重构

#### 2.3.1 目标目录结构

```
skill/code/web/
├── server.py              # 入口文件（< 100 行）
├── app.py                 # Flask app 工厂
├── config.py              # 配置管理
├── routes/                # 路由层
│   ├── __init__.py
│   ├── troubleshoot.py    # 排查相关路由
│   ├── auth.py            # 认证路由
│   ├── cache.py           # 缓存路由
│   └── export.py          # 导出路由
├── services/              # 业务层
│   ├── __init__.py
│   ├── troubleshoot_service.py
│   ├── auth_service.py
│   └── cache_service.py
├── repositories/          # 数据层
│   ├── __init__.py
│   └── search_repository.py
├── models/                # 数据模型
│   └── schemas.py
├── utils/                 # 工具函数
│   ├── error_codes.py
│   ├── response.py
│   └── logger.py
└── templates/             # HTML 模板
```

#### 2.3.2 重构步骤

**步骤 1：创建目录结构**
```bash
mkdir -p routes services repositories models utils
touch routes/__init__.py services/__init__.py repositories/__init__.py
```

**步骤 2：提取路由**

```python
# routes/troubleshoot.py
from flask import Blueprint, request, jsonify
from services.troubleshoot_service import TroubleshootService

bp = Blueprint('troubleshoot', __name__)

@bp.route('/api/troubleshoot', methods=['POST'])
def troubleshoot():
    service = TroubleshootService()
    return service.analyze(request.get_json())

@bp.route('/api/search', methods=['POST'])
def search():
    service = TroubleshootService()
    return service.search(request.get_json())
```

**步骤 3：提取业务逻辑**

```python
# services/troubleshoot_service.py
from repositories.search_repository import SearchRepository
from utils.response import success_response, error_response

class TroubleshootService:
    
    def __init__(self):
        self.search_repo = SearchRepository()
    
    def analyze(self, data):
        # 业务逻辑
        query = data.get('query', '').strip()
        if not query:
            return error_response(ErrorCodes.INVALID_PARAM, "请输入问题描述")
        
        # 调用数据层
        cases = self.search_repo.search(query)
        
        # 返回结果
        return success_response(cases=cases)
```

**步骤 4：提取数据访问**

```python
# repositories/search_repository.py
from search_engine import SearchEngine

class SearchRepository:
    
    def __init__(self):
        self.engine = SearchEngine()
    
    def search(self, query, top_k=5, project_filter=None):
        return self.engine.search(query, top_k, project_filter)
    
    def get_projects(self):
        return self.engine.get_projects()
```

**步骤 5：精简 server.py**

```python
# server.py
from flask import Flask
from routes import troubleshoot, auth, cache, export

app = Flask(__name__)

# 注册路由
app.register_blueprint(troubleshoot.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(cache.bp)
app.register_blueprint(export.bp)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8088, debug=True)
```

---

## 3. 实施计划

### 3.1 任务分解

| 序号 | 任务 | 预计时间 | 状态 | 依赖 |
|------|------|----------|------|------|
| 1 | P1-1：异常处理规范化 | 0.5天 | ✅ 完成 | - |
| 1.1 | 定义错误码规范 | 0.1天 | ✅ 完成 | - |
| 1.2 | 修改 cache_manager.py | 0.1天 | ✅ 完成 | 1.1 |
| 1.3 | 修改 server.py 异常处理 | 0.2天 | ✅ 完成 | 1.1 |
| 1.4 | 添加统一错误响应 | 0.1天 | ✅ 完成 | 1.1 |
| 2 | P1-2：添加单元测试 | 1天 | ✅ 完成 | - |
| 2.1 | 配置 pytest 环境 | 0.2天 | ✅ 完成 | - |
| 2.2 | 编写 search_engine 测试 | 0.2天 | ✅ 完成 | 2.1 |
| 2.3 | 编写 safety_filter 测试 | 0.3天 | ✅ 完成 | 2.1 |
| 2.4 | 编写 cache_manager 测试 | 0.2天 | ✅ 完成 | 2.1 |
| 2.5 | 验证覆盖率 > 80% | 0.1天 | ✅ 完成 | 2.2-2.4 |
| 3 | P1-3：架构分层重构 | 2天 | 待开始 | 1,2 |
| 3.1 | 创建目录结构 | 0.1天 | 待开始 | - |
| 3.2 | 提取工具函数到 utils/ | 0.2天 | 待开始 | 3.1 |
| 3.3 | 提取数据层到 repositories/ | 0.3天 | 待开始 | 3.2 |
| 3.4 | 提取业务层到 services/ | 0.5天 | 待开始 | 3.3 |
| 3.5 | 提取路由层到 routes/ | 0.5天 | 待开始 | 3.4 |
| 3.6 | 精简 server.py | 0.2天 | 待开始 | 3.5 |
| 3.7 | 功能验证 | 0.2天 | 待开始 | 3.6 |

---

## 4. 测试验证

### 4.1 验证项

| 验收项 | 标准 |
|--------|------|
| 异常处理 | 无空异常捕获，有日志记录 |
| 单元测试 | pytest 通过，覆盖率 > 80% |
| 架构分层 | server.py < 100 行，三层分离 |
| API 功能 | 所有接口正常响应 |

### 4.2 回归测试

- [ ] 流式 API 正常
- [ ] 登录认证正常
- [ ] 缓存管理正常
- [ ] Word 导出正常

---

## 5. 执行记录

| 日期 | 任务 | 执行人 | 结果 | 备注 |
|------|------|--------|------|------|
| 2026-07-13 | P1-1.1 新建 utils/logger.py | Claude | 完成 | RotatingFileHandler，1MB×3，控制台+文件双输出 |
| 2026-07-13 | P1-1.1 新建 utils/error_codes.py | Claude | 完成 | 四组错误码（通用/搜索/缓存/AI） |
| 2026-07-13 | P1-1.1 新建 utils/response.py | Claude | 完成 | success_response/error_response 统一封装 |
| 2026-07-13 | P1-1.2 规范化 cache_manager.py | Claude | 完成 | 5 处异常：2 处 except Exception 细化、3 处 except: 消除 |
| 2026-07-13 | P1-1.3 规范化 server.py 异常处理 | Claude | 完成 | 12 处路由/工具异常改 logger，保留 API 响应结构 |
| 2026-07-13 | P1-1.4 验证 | Claude | 完成 | 6 文件编译通过、cache 冒烟通过、server 导入通过、无空捕获残留 |
| 2026-07-13 | P1-2.1 配置 pytest 环境 | Claude | 完成 | 新建 pytest.ini + tests/conftest.py + __init__.py；requirements.txt 追加 pytest/pytest-cov/pytest-mock |
| 2026-07-13 | P1-2.2 编写 search_engine 测试 | Claude | 完成 | 34 个用例：tokenize/compute_tf/idf/tfidf/cosine + SearchEngine 实例化/search/过滤/get_projects/get_categories |
| 2026-07-13 | P1-2.3 编写 safety_filter 测试 | Claude | 完成 | 41 个用例：check_dangerous/is_allowed_operation/check_relevance/extract_query_keywords/filter_output/filter_sensitive_info/filter_output_with_sensitive |
| 2026-07-13 | P1-2.4 编写 cache_manager 测试 | Claude | 完成 | 19 个用例：_get_cache_key/get/set/clear_expired/clear_all/get_stats/check_and_clean_if_needed，含损坏文件/不可序列化/超限清理边界 |
| 2026-07-13 | P1-2.5 覆盖率验证 | Claude | 完成 | safety_filter 99% / cache_manager 87% / search_engine 85%，三模块均 > 80% |

### 5.1 验证结果

| 验证项 | 标准 | 实测 |
|--------|------|------|
| 文件编译 | py_compile 无错 | ✅ 全部通过 |
| 日志模块 | 控制台+文件双输出 | ✅ logs/app.log 生成 |
| 错误码/响应 | error_response/success_response 可用 | ✅ 返回结构正确 |
| cache 模块 | get/set/clear 正常 | ✅ hit=True |
| 空捕获消除 | `grep -nE "except\s*:"` 无输出 | ✅ 无残留 |
| server 导入 | `import server` 无错 | ✅ ok |
| API 兼容 | jsonify 响应结构未变 | ✅ 仅日志通道变更 |
| pytest 框架 | 配置生效、用例可收集 | ✅ pytest.ini 识别、94 用例收集 |
| 全量测试 | `pytest` 全绿 | ✅ 94 passed in 0.90s |
| 核心覆盖率 | safety_filter/cache_manager/search_engine > 80% | ✅ 99% / 87% / 85% |
| 索引依赖隔离 | conftest 注入 deploy 副本，SearchEngine 可实例化 | ✅ 357 条记录加载、无 FileNotFoundError |

### 5.2 问题记录

| 日期 | 问题 | 解决方案 | 状态 |
|------|------|----------|------|
| 2026-07-13 | 需求文档列出 cache_manager 行号 79-81/126-128/161-162 与实际略有偏差 | 以实际代码为准，覆盖 79-81、113-114、126-127、161-162、185-186 共 5 处（含文档未列出的 check_and_clean_if_needed 第 185-186 行） | 已解决 |
| 2026-07-13 | server.py 路由层 except Exception 为兜底捕获，强行收紧有漏捕风险 | 保留 except Exception 兜底，仅替换日志输出方式（print/traceback→logger），并细化工具函数层（AI 调用）的具体异常 | 已解决 |
| 2026-07-13 | 计划文档 §2.2.3 测试示例 `from safety_filter import SafetyFilter` 错误——该模块无此类，是顶层函数 | 修正示例为 `import safety_filter` + 顶层函数调用；同步修正断言（filter_output 危险行是替换为提示而非删除，断言查原命令不在 + removed_lines 非空） | 已解决 |
| 2026-07-13 | `SearchEngine()` 本地初始化抛 FileNotFoundError——SEARCH_INDEX_PATHS 三条路径在本地开发环境均不存在 | conftest.py autouse fixture 用 monkeypatch 把 SEARCH_INDEX_PATHS 指向仓库内 deploy/搜索索引.json 副本（357 条），不复制文件、不污染源码 | 已解决 |
| 2026-07-13 | `cosine_similarity(v, v)` 浮点精度返回 0.9999999999999998 而非 1.0 | 断言改用 `math.isclose(..., 1.0, rel_tol=1e-9)`，容忍浮点误差（属真实代码行为，非 bug） | 已解决 |


---

## 6. 注意事项

1. **分步重构**：每次只改一处，验证后再继续
2. **保持兼容**：API 接口签名不变
3. **测试先行**：重构前先写测试
4. **日志完善**：异常必须记录日志

---

## 7. 相关文档

- [PRD_需求文档_P1级代码质量优化](./PRD_需求文档_P1级代码质量优化.md)
- [PRD_需求文档_项目优化方向](./PRD_需求文档_项目优化方向.md)
