docs: 添加 10 份技术文档,README 改为中文

- 01-architecture.md: 架构概览
- 02-backend-services.md: 后端服务层
- 03-frontend-interaction.md: 前端交互设计
- 04-database-design.md: 数据库设计
- 05-api-reference.md: API 接口文档
- 06-sse-streaming.md: SSE 流式传输
- 07-llm-integration.md: LLM 集成
- 08-deployment.md: 部署运维
- 09-development-guide.md: 开发指南
- 10-troubleshooting.md: 故障排查
This commit is contained in:
2026-06-23 22:38:43 +08:00
parent 6f0fabf934
commit 275e5cc886
11 changed files with 4877 additions and 87 deletions
+446
View File
@@ -0,0 +1,446 @@
# SSE 流式传输
## 概述
Server-Sent Events (SSE) 是 PR-Helper 的核心通信机制,用于实时推送克隆进度、PR 生成和代码审查结果。
## 为什么选择 SSE
### 对比 WebSocket
| 特性 | SSE | WebSocket |
|------|-----|-----------|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议 |
| 实现 | 简单 | 复杂 |
| 重连 | 自动 | 手动 |
| 兼容性 | 好 | 需要升级 |
### 适用场景
- 服务器推送数据到客户端
- 不需要客户端频繁发送数据
- 需要 HTTP 兼容性
- 需要自动重连
## SSE 协议
### 基本格式
```
event: message_type
data: {"key": "value"}
```
- `event:` 事件类型(可选)
- `data:` 事件数据(JSON 字符串)
- 空行分隔事件
### 多行数据
```
event: content
data: {"content": "第一行\n第二行"}
```
## 后端实现
### 设置响应头
```go
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
```
### 发送事件
```go
sendEvent := func(event string, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
jsonData = []byte(`{"error":"序列化事件数据失败"}`)
}
fmt.Fprintf(c.Writer, "event: %s\ndata: %s\n\n", event, jsonData)
flusher.Flush()
}
```
### 获取 Flusher
```go
flusher, ok := c.Writer.(http.Flusher)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器不支持流式传输"})
return
}
```
## 前端实现
### SSE 客户端
```javascript
const SSE = {
async post(url, body, handlers = {}) {
const controller = new AbortController();
const run = async () => {
try {
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body),
signal: controller.signal,
});
if (!resp.ok) {
const errText = await resp.text();
// 处理错误...
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentEvent = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('event: ')) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith('data: ')) {
const raw = line.slice(6);
let data;
try {
data = JSON.parse(raw);
} catch (_) {
data = raw;
}
if (currentEvent && handlers[currentEvent]) {
handlers[currentEvent](data);
}
}
}
}
if (handlers.done) handlers.done();
} catch (err) {
if (err.name === 'AbortError') return;
if (handlers.error) handlers.error({ message: err.message });
}
};
run();
return { abort: () => controller.abort() };
}
};
```
### 使用示例
```javascript
SSE.post('/api/repos/1/review', {
base: 'main',
head: 'feature',
top_n: 20,
concurrency: 5
}, {
// 开始事件
start: (data) => {
console.log(`开始审查 ${data.total_files} 个文件`);
updateProgress(0, data.reviewed_files);
},
// 文件开始
file_start: (data) => {
console.log(`正在审查 ${data.file} (${data.index}/${data.total})`);
highlightCurrentFile(data.file);
},
// 内容流
content: (data) => {
appendToOutput(data.content);
},
// 审查建议
suggestion: (data) => {
showSuggestion(data.file, data.severity, data.content);
},
// 文件结束
file_end: (data) => {
markFileComplete(data.file);
},
// 汇总
summary: (data) => {
showSummary(data.score, data.overall, data.findings, data.recommendations);
},
// 分析保存
analysis_saved: (data) => {
console.log(`分析已保存,ID: ${data.analysis_id}`);
},
// 进度
progress: (data) => {
if (data.step === 'generating_summary') {
showSpinner('生成汇总中...');
}
},
// 完成
done: () => {
console.log('审查完成');
hideSpinner();
},
// 错误
error: (data) => {
console.error('错误:', data.message);
showError(data.message);
}
});
```
## 事件类型
### PR 生成事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `content` | `{content: string}` | Markdown 片段 |
| `done` | `{content: ""}` | 完成 |
### 代码审查事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `start` | `{total_files, reviewed_files, top_n}` | 开始 |
| `file_start` | `{file, index, total}` | 文件开始 |
| `content` | `{content: string}` | LLM 输出 |
| `suggestion` | `{file, severity, content}` | 建议 |
| `file_end` | `{file}` | 文件结束 |
| `summary` | `{score, overall, findings, recommendations}` | 汇总 |
| `progress` | `{step: string}` | 进度 |
| `error` | `{message: string}` | 错误 |
| `analysis_saved` | `{analysis_id}` | 保存完成 |
| `done` | `{content: ""}` | 完成 |
### 克隆事件
| 事件 | 数据 | 说明 |
|------|------|------|
| `progress` | `{step, current, total}` | 进度 |
| `error` | `{message: string}` | 错误 |
| `done` | `{repo_id, branches, tags, commit_num}` | 完成 |
## 错误处理
### 后端错误
```go
if err != nil {
sendEvent("error", map[string]interface{}{
"message": err.Error(),
})
return
}
```
### 前端错误
```javascript
error: (data) => {
// 显示错误消息
showErrorToast(data.message);
// 重置 UI
resetProgressBar();
hideSpinner();
}
```
### 网络错误
```javascript
catch (err) {
if (err.name === 'AbortError') {
// 用户中断,忽略
return;
}
if (handlers.error) {
handlers.error({ message: err.message });
}
}
```
## 中断请求
### 前端中断
```javascript
const controller = SSE.post(url, body, handlers);
// 用户点击取消按钮
cancelButton.onclick = () => {
controller.abort();
};
```
### 后端处理
- 客户端断开连接时,Gin 会检测到
- 服务层应检查 context 取消
- 长时间运行的操作应支持取消
## 性能优化
### 缓冲控制
```
X-Accel-Buffering: no
```
- 禁用 Nginx 缓冲
- 确保事件立即发送
### 批量发送
- 避免频繁发送小事件
- 合并相关数据
### 压缩
- SSE 不支持 gzip 压缩
- 数据量大时考虑压缩正文
## 并发安全
### 线程安全回调
```go
safeCallback := callback
if callback != nil {
safeCallback = func(event string, data interface{}) {
mu.Lock()
defer mu.Unlock()
callback(event, data)
}
}
```
- 使用互斥锁保护回调
- 避免并发写入
### Goroutine 管理
```go
var wg sync.WaitGroup
for i, file := range files {
wg.Add(1)
go func(idx int, f FileDiff) {
defer wg.Done()
// ...
}(i, file)
}
wg.Wait()
```
- 等待所有 goroutine 完成
- 避免资源泄漏
## 测试
### 手动测试
```bash
curl -N -X POST http://localhost:8080/api/repos/1/review \
-H "Content-Type: application/json" \
-d '{"base":"main","head":"feature"}'
```
### 自动化测试
```go
func TestSSEStream(t *testing.T) {
// 创建测试服务器
// 发送请求
// 读取事件流
// 验证事件序列
}
```
## 监控
### 关键指标
- 连接数
- 事件发送速率
- 错误率
- 响应时间
### 日志
```go
log.Printf("SSE connected: %s", c.ClientIP())
log.Printf("SSE event: %s", event)
log.Printf("SSE disconnected: %s", c.ClientIP())
```
## 安全考虑
### 认证
- 所有 SSE 端点需要认证
- 使用会话 Cookie
### 速率限制
- 限制并发 SSE 连接数
- 限制事件发送频率
### 数据验证
- 验证输入参数
- 防止注入攻击
## 故障排查
### 常见问题
1. **事件不发送**
- 检查 Flusher 是否可用
- 确认响应头设置正确
- 检查 Nginx 缓冲配置
2. **连接断开**
- 检查超时设置
- 确认网络稳定
- 查看错误日志
3. **数据乱码**
- 确认 JSON 序列化正确
- 检查字符编码
- 验证事件格式
### 调试工具
- 浏览器开发者工具 Network 面板
- curl 命令行测试
- Wireshark 抓包