# PRD_计划执行_问题排查助手流式返回优化

## 基本信息

| 项目 | 内容 |
|------|------|
| 关联需求 | [PRD_需求文档_问题排查助手流式返回优化](./PRD_需求文档_问题排查助手流式返回优化.md) |
| 创建日期 | 2026-07-12 |
| 负责人 | 研发组 |
| 预计工期 | 1 天 |
| 状态 | 待执行 |

---

## 总览

### 核心交付物

| 交付物 | 说明 | 优先级 |
|--------|------|--------|
| 流式返回接口 | `/api/analyze/stream` SSE 接口 | P0 |
| 前端 EventSource 集成 | 前端接收流式数据并追加显示 | P0 |
| 安全过滤适配 | 流式模式下的安全过滤 | P0 |
| 错误处理 | 连接中断、超时等异常处理 | P1 |

### 执行顺序

```
阶段一：后端接口开发
    └── 新增 /api/analyze/stream 接口
         ↓
阶段二：前端集成
    └── EventSource 连接 + 内容追加显示
         ↓
阶段三：测试验证
    └── 端到端测试 + 兼容性测试
```

---

## 阶段一：后端接口开发

### 任务 1.1：新增流式返回接口

**文件**：`code/web/server.py`

**新增接口**：

```python
@app.route('/api/analyze/stream', methods=['GET'])
def analyze_stream():
    """
    流式返回 AI 分析结果（SSE）。
    
    参数通过 URL query string 传递：
      - project_name: 项目名称
      - system_type: 系统类型
      - apk_product: APK 产品
      - query: 问题描述
    
    返回格式（SSE）：
      data: {"type": "start", "message": "正在分析..."}
      data: {"type": "matched_cases", "cases": [...]}
      data: {"type": "chunk", "content": "..."}
      data: {"type": "done", "api_time": 45.2}
    """
```

**实现要点**：

```python
from flask import Response

def generate_stream(project_name, system_type, apk_product, query, matched_cases):
    """生成器：流式返回内容"""
    import json
    
    # 1. 发送开始标记
    yield f'data: {json.dumps({"type": "start", "message": "正在分析..."})}\n\n'
    
    # 2. 发送匹配案例
    yield f'data: {json.dumps({"type": "matched_cases", "cases": matched_cases})}\n\n'
    
    # 3. 调用 Claude API（流式）
    try:
        for chunk in call_claude_api_stream(prompt):
            # 4. 安全过滤（可选：在流结束后统一过滤）
            yield f'data: {json.dumps({"type": "chunk", "content": chunk})}\n\n'
        
        # 5. 发送完成标记
        yield f'data: {json.dumps({"type": "done", "api_time": elapsed_time})}\n\n'
    except Exception as e:
        yield f'data: {json.dumps({"type": "error", "message": str(e)})}\n\n'

@app.route('/api/analyze/stream', methods=['GET'])
def analyze_stream():
    project_name = request.args.get('project_name', '')
    system_type = request.args.get('system_type', '')
    apk_product = request.args.get('apk_product', '')
    query = request.args.get('query', '')
    
    # 搜索匹配案例
    matched_cases = get_search_engine().search(query, top_k=5)
    
    # 返回 SSE 流
    return Response(
        generate_stream(project_name, system_type, apk_product, query, matched_cases),
        mimetype='text/event-stream',
        headers={
            'Cache-Control': 'no-cache',
            'X-Accel-Buffering': 'no',  # 禁用 Nginx 缓冲
        }
    )
```

**状态**：[ ] 待开发

---

### 任务 1.2：Claude API 流式调用

**文件**：`code/web/server.py`

**新增函数**：

```python
def call_claude_api_stream(prompt):
    """
    流式调用 Claude API。
    
    返回：生成器，每次 yield 一个文本片段
    """
    import requests
    
    api_base = config.get('claude_api_base', '')
    api_key = config.get('claude_api_key', '')
    model = config.get('claude_model', 'glm-5.1')
    
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {api_key}',
    }
    
    data = {
        'model': model,
        'messages': [{'role': 'user', 'content': prompt}],
        'max_tokens': 4096,
        'stream': True,  # 启用流式返回
    }
    
    response = requests.post(
        f'{api_base}/v1/chat/completions',
        headers=headers,
        json=data,
        stream=True,  # requests 流式响应
        timeout=120,
    )
    
    for line in response.iter_lines():
        if line:
            line = line.decode('utf-8')
            if line.startswith('data: '):
                data_str = line[6:]  # 去掉 "data: " 前缀
                if data_str == '[DONE]':
                    break
                try:
                    chunk = json.loads(data_str)
                    content = chunk['choices'][0]['delta'].get('content', '')
                    if content:
                        yield content
                except:
                    pass
```

**状态**：[ ] 待开发

---

### 任务 1.3：安全过滤适配

**方案**：先缓存完整响应，过滤后再流式推送。

```python
def generate_stream_safe(project_name, system_type, apk_product, query, matched_cases):
    """带安全过滤的流式返回"""
    
    # 1. 发送开始标记
    yield f'data: {json.dumps({"type": "start", "message": "正在分析..."})}\n\n'
    
    # 2. 发送匹配案例
    yield f'data: {json.dumps({"type": "matched_cases", "cases": matched_cases})}\n\n'
    
    # 3. 收集完整响应
    full_response = ""
    for chunk in call_claude_api_stream(prompt):
        full_response += chunk
    
    # 4. 安全过滤
    filter_result = safety_filter.filter_output(full_response, query)
    
    # 5. 流式推送过滤后的内容
    for chunk in split_into_chunks(filter_result['filtered_text'], chunk_size=50):
        yield f'data: {json.dumps({"type": "chunk", "content": chunk})}\n\n'
    
    # 6. 发送完成标记
    yield f'data: {json.dumps({"type": "done"})}\n\n'
```

**状态**：[ ] 待开发

---

## 阶段二：前端集成

### 任务 2.1：EventSource 连接

**文件**：`code/web/templates/index.html`

**新增代码**：

```javascript
async function startTroubleshootStream(projectName, systemType, apkProduct, query) {
    // 构建 URL
    const params = new URLSearchParams({
        project_name: projectName,
        system_type: systemType,
        apk_product: apkProduct || '',
        query: query
    });
    
    // 创建 EventSource 连接
    const url = `/api/analyze/stream?${params.toString()}`;
    const source = new EventSource(url);
    
    // 结果容器
    let fullContent = '';
    
    // 监听消息
    source.onmessage = function(event) {
        const data = JSON.parse(event.data);
        
        switch (data.type) {
            case 'start':
                showStatus('正在分析...');
                break;
                
            case 'matched_cases':
                showMatchedCases(data.cases);
                break;
                
            case 'chunk':
                fullContent += data.content;
                updateResponseContent(fullContent);
                break;
                
            case 'done':
                source.close();
                hideLoading();
                showSubmitButton();
                break;
                
            case 'error':
                source.close();
                showError(data.message);
                break;
        }
    };
    
    // 监听错误
    source.onerror = function(event) {
        source.close();
        showError('连接中断，请重试');
    };
}
```

**状态**：[ ] 待开发

---

### 任务 2.2：内容追加显示

**文件**：`code/web/templates/index.html`

**新增函数**：

```javascript
function updateResponseContent(content) {
    const container = document.getElementById('responseContent');
    
    // 渲染 Markdown
    container.innerHTML = renderMarkdown(content);
    
    // 滚动到底部
    container.scrollTop = container.scrollHeight;
}
```

**状态**：[ ] 待开发

---

### 任务 2.3：降级处理

**文件**：`code/web/templates/index.html`

**降级逻辑**：SSE 不可用时回退到普通请求。

```javascript
async function startTroubleshoot(projectName, systemType, apkProduct, query) {
    // 检测 EventSource 是否可用
    if (typeof EventSource === 'undefined') {
        // 降级：使用普通请求
        return startTroubleshootNormal(projectName, systemType, apkProduct, query);
    }
    
    try {
        // 优先使用流式返回
        await startTroubleshootStream(projectName, systemType, apkProduct, query);
    } catch (error) {
        // 降级：流式失败时使用普通请求
        console.warn('SSE 失败，降级到普通请求:', error);
        await startTroubleshootNormal(projectName, systemType, apkProduct, query);
    }
}
```

**状态**：[ ] 待开发

---

## 阶段三：测试验证

### 任务 3.1：端到端测试

- [ ] 测试流式返回正常工作
- [ ] 测试首字显示时间 < 3 秒
- [ ] 测试匹配案例优先显示
- [ ] 测试安全过滤生效
- [ ] 测试错误处理（API 失败、连接中断）

### 任务 3.2：兼容性测试

- [ ] Chrome 浏览器测试
- [ ] Edge 浏览器测试
- [ ] 降级模式测试（禁用 EventSource）

### 任务 3.3：性能测试

- [ ] 并发测试：3 人同时请求
- [ ] 长时间连接测试（>60 秒）
- [ ] API 费用对比（流式 vs 非流式）

---

## 执行记录

| 日期 | 内容 | 结果 | 备注 |
|------|------|------|------|
| 2026-07-12 | 创建需求文档和计划执行文档 | ✅ 完成 | |
| 2026-07-12 | 实现流式 API 调用函数 call_claude_api_stream | ✅ 完成 | |
| 2026-07-12 | 新增 /api/analyze/stream SSE 接口 | ✅ 完成 | |
| 2026-07-12 | 前端 EventSource 集成 | ✅ 完成 | |
| 2026-07-12 | 修复 loadingText 元素 ID 问题 | ✅ 完成 | |
| 2026-07-12 | 本地测试验证 | ✅ 通过 | 3 秒内显示内容，降级机制正常 |
| 2026-07-12 | 更新计划执行文档 | ✅ 完成 | |

---

## 变更记录

| 日期 | 版本 | 变更内容 | 变更人 |
|------|------|----------|--------|
| 2026-07-12 | v1.0 | 初始版本 | Claude |
| 2026-07-12 | v1.1 | 完成实现并测试通过 | Claude |