docs(repo): 移除过期的 SSE/调试文档

- 清理一次性调试记录与临时总结文档,保留正式文档\n- 降低仓库噪声,便于聚焦代码与正式说明
This commit is contained in:
jimmy committed 2025-10-07 14:32:49 +08:00
1 parent b10940d5d6
commit 806954fc94
8 files changed
-2233

No files matched your search

-339
View File
@@ -1,339 +0,0 @@
# SSE 订阅系统更新日志
## 版本 2.0 - 客户端级别订阅管理 (2025-10-04)
### 🎯 核心改进
实现了**客户端级别的 SSE 订阅管理系统**,解决了以下问题:
1. ✅ **精准推送** - 只向订阅了特定路径的客户端发送事件
2. ✅ **自动清理** - 客户端断开时自动清理订阅
3. ✅ **独立管理** - 每个客户端独立管理自己的订阅列表
4. ✅ **减少噪音** - 避免无关事件干扰客户端
---
## 📝 详细更改
### 后端更改 (Go)
#### 1. SSE Manager 增强 (`pkg/sse/manager.go`)
**Client 结构新增字段:**
```go
type Client struct {
// ... 原有字段
// 新增:订阅管理
subscriptions map[string]bool // key: 订阅的路径, value: true
subscriptionMutex sync.RWMutex // 保护 subscriptions
}
```
**新增方法:**
- `Subscribe(clientID, path)` - 客户端订阅指定路径
- `Unsubscribe(clientID, path)` - 客户端取消订阅
- `GetSubscriptions(clientID)` - 获取客户端所有订阅
- `IsSubscribed(clientID, path)` - 检查客户端是否订阅
- `BroadcastToSubscribers(path, eventType, message)` - 向订阅者广播事件
**修改:**
- `RegisterClient()` - 初始化 `subscriptions` 映射
---
#### 2. SSE Handler 增强 (`internal/handler/sse_handler.go`)
**新增 HTTP 接口:**
1. **订阅路径**
- 路由: `POST /api/v1/sse/subscribe`
- 请求体: `{ "client_id": "...", "path": "..." }`
2. **取消订阅**
- 路由: `POST /api/v1/sse/unsubscribe`
- 请求体: `{ "client_id": "...", "path": "..." }`
3. **获取订阅列表**
- 路由: `GET /api/v1/sse/subscriptions/:clientId`
---
#### 3. FileSystemEvent 增强 (`internal/utils/filesystem_watcher.go`)
**新增字段:**
```go
type FileSystemEvent struct {
// ... 原有字段
WatchedPath string `json:"watched_path"` // 被监听的根路径
}
```
**新增方法:**
```go
// findWatchedPath 查找事件路径对应的被监听根路径
func (fsw *FileSystemWatcher) findWatchedPath(eventPath string) string
```
**修改逻辑:**
- `handleEvent()` - 自动填充 `WatchedPath` 字段
---
#### 4. 文件系统 Handler 修改 (`internal/handler/filesystem_handler.go`)
**修改:**
- 使用 `BroadcastToSubscribers()` 替代 `BroadcastEvent()`
- 根据 `event.WatchedPath` 精准推送事件
**代码对比:**
```go
// 修改前
sseManager.BroadcastEvent("filesystem-change", eventStr)
// 修改后
sseManager.BroadcastToSubscribers(ev.WatchedPath, "filesystem-change", eventStr)
```
---
### 前端更改 (Vue3/TypeScript)
#### 1. SSEService 增强 (`services/sseService.ts`)
**新增字段:**
```typescript
class SSEService {
private clientID: string | null = null // 存储客户端 ID
// ...
}
```
**新增方法:**
```typescript
getClientID(): string | null
subscribe(path: string): Promise<boolean>
unsubscribe(path: string): Promise<boolean>
getSubscriptions(): Promise<string[]>
```
**新增事件监听:**
```typescript
// 监听 connected 事件获取客户端 ID
this.eventSource.addEventListener('connected', (event) => {
const data = JSON.parse(event.data)
this.clientID = data.client_id
})
```
---
#### 2. FileSystemChangeEvent 增强
**新增字段:**
```typescript
interface FileSystemChangeEvent {
// ... 原有字段
watched_path?: string // 被监听的根路径
}
```
---
## 🚀 使用示例
### 前端使用
```typescript
import { sseService } from '@/services/sseService'
// 1. 连接 SSE
await sseService.connect()
await new Promise(resolve => setTimeout(resolve, 1000))
// 2. 订阅路径
await sseService.subscribe('D:\\EdgeDownload')
// 3. 监听事件
const unsubscribe = sseService.onFileSystemChange((event) => {
console.log('文件变化:', event)
})
// 4. 清理
onUnmounted(async () => {
await sseService.unsubscribe('D:\\EdgeDownload')
unsubscribe()
})
```
### 后端 API 测试
```http
# 1. 订阅路径
POST http://127.0.0.1:9484/api/v1/sse/subscribe
Content-Type: application/json
{
"client_id": "your-client-id",
"path": "D:\\EdgeDownload"
}
# 2. 获取订阅列表
GET http://127.0.0.1:9484/api/v1/sse/subscriptions/your-client-id
# 3. 取消订阅
POST http://127.0.0.1:9484/api/v1/sse/unsubscribe
Content-Type: application/json
{
"client_id": "your-client-id",
"path": "D:\\EdgeDownload"
}
```
---
## 🔍 测试步骤
### 1. 启动后端
```bash
cd D:\go-argus\src\rear
go run main.go
```
### 2. 启动前端
```bash
cd D:\go-argus\src\front\argus-front
npm run dev
```
### 3. 测试订阅功能
1. 打开浏览器控制台
2. 等待 SSE 连接建立(看到客户端 ID)
3. 订阅路径:
```javascript
await sseService.subscribe('D:\\EdgeDownload')
```
4. 在 `D:\EdgeDownload` 中创建/修改/删除文件
5. 观察控制台输出的文件系统事件
### 4. 验证精准推送
1. 打开多个浏览器标签页
2. 每个标签页订阅不同的路径
3. 修改某个路径下的文件
4. 验证只有订阅该路径的标签页收到事件
---
## 📊 性能改进
### 1. 减少网络传输
**修改前:**
- 所有客户端收到所有文件系统事件
- 客户端自己过滤无关事件
**修改后:**
- 服务端根据订阅精准推送
- 网络传输量大幅减少
### 2. 减少客户端处理负担
**修改前:**
```
事件 100 个 × 客户端 10 个 = 1000 次处理
```
**修改后:**
```
事件 100 个 × 订阅客户端 2 个 = 200 次处理
```
---
## ⚠️ 破坏性更改
### 1. FileSystemEvent 结构变更
新增了 `watched_path` 字段,旧版本客户端可能无法正确解析。
**解决方案:**
- 字段为可选 (`json:"-omitempty"`)
- 旧客户端可忽略该字段
### 2. 订阅模式变更
**修改前:**
- 连接即收到所有文件系统事件
**修改后:**
- 需要主动订阅才能收到事件
- 未订阅的客户端不会收到 `filesystem-change` 事件
**迁移指南:**
```typescript
// 在连接成功后立即订阅
await sseService.connect()
await new Promise(resolve => setTimeout(resolve, 1000))
await sseService.subscribe(defaultPath) // 添加这行
```
---
## 🐛 已修复的问题
### 1. 编译错误: `undefined: path`
**问题:**
`filesystem_handler.go:91` 使用了未定义的变量 `path`
**解决方案:**
- 在 `FileSystemEvent` 中添加 `WatchedPath` 字段
- 在 `FileSystemWatcher.handleEvent()` 中自动填充该字段
- 使用 `event.WatchedPath` 作为订阅路径
---
## 📚 相关文档
- **使用指南**: `SSE_SUBSCRIPTION_GUIDE.md`
- **HTTP 测试**: `rear/tests/sse_subscription.http`
- **代码示例**: `front/argus-front/src/examples/sseSubscriptionExample.ts`
---
## 🔮 未来改进
### 计划中的功能
1. **路径模式匹配**
- 支持通配符订阅 (如 `D:\**\*.jpg`)
- 支持正则表达式匹配
2. **订阅限制**
- 单个客户端最大订阅数限制
- 路径权限验证
3. **订阅统计**
- 每个路径的订阅客户端数量
- 事件发送统计
4. **持久化订阅**
- 重连后自动恢复订阅
- 订阅信息存储到数据库
---
## 👥 贡献者
- Claude Code
---
**版本**: 2.0
**发布日期**: 2025-10-04
-159
View File
@@ -1,159 +0,0 @@
# 优雅关闭问题修复说明
## 问题描述
后端服务器在接收到关闭信号(Ctrl+C 或 SIGTERM)后,总是显示 "Shutting down server..." 但无法立即关闭,需要等待 30 秒超时才能退出。
## 根本原因
在调用 `srv.Shutdown()` 之前,没有清理以下长连接资源:
1. **SSE (Server-Sent Events) 连接** - SSE Manager 维护的长连接客户端
2. **文件系统监听器** - fsnotify 创建的文件监听 goroutine
这些未关闭的连接会阻止 HTTP 服务器优雅关闭,导致必须等待超时。
## 解决方案
### 1. 修改 `internal/router/router.go`
添加清理函数返回值:
```go
// SetupRoutes 设置路由
// 返回一个清理函数,用于在服务器关闭时清理资源
func SetupRoutes(r *gin.Engine, contain *container.DbContainer, imgContain *container.TaskContainer) CleanupFunc {
// ... 路由设置代码 ...
// 返回清理函数
return func() {
// 关闭 SSE Manager
if sseHandler != nil && sseHandler.Manager != nil {
sseHandler.Manager.Close()
}
// 关闭文件系统监听器
if fileSystemHandler != nil && fileSystemHandler.GetFileWatcher() != nil {
fileSystemHandler.GetFileWatcher().Close()
}
}
}
```
### 2. 修改 `internal/handler/filesystem_handler.go`
添加 `GetFileWatcher()` 方法暴露内部监听器:
```go
// GetFileWatcher 获取文件监听器(用于清理)
func (h *FileSystemHandler) GetFileWatcher() *utils.FileSystemWatcher {
return h.fileWatcher
}
```
### 3. 修改 `internal/utils/filesystem_watcher.go`
添加 `Close()` 方法作为 `Stop()` 的别名:
```go
// Close 关闭监听器(Stop 的别名,用于资源清理)
func (fsw *FileSystemWatcher) Close() {
fsw.Stop()
}
```
### 4. 修改 `main.go`
在服务器关闭前调用清理函数:
```go
// 设置路由,并获取清理函数
cleanup := router.SetupRoutes(r, con, imgContain)
// ... 启动服务器 ...
// 阻塞主goroutine,等待信号
<-quit
logger.Info("Shutting down server...")
// 先清理资源(关闭 SSE 连接和文件监听器)
logger.Info("Cleaning up resources...")
cleanup()
logger.Info("Resources cleaned up")
// 然后关闭 HTTP 服务器(超时时间从 30s 改为 5s)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Errorf("Server forced to shutdown: %v", err)
} else {
logger.Info("Server exited gracefully")
}
```
## 关闭流程
修复后的优雅关闭流程:
1. **接收信号** (SIGINT/SIGTERM)
2. **清理长连接资源**
- 关闭所有 SSE 客户端连接
- 停止文件系统监听器
3. **关闭 HTTP 服务器**
- 等待现有请求完成
- 最多等待 5 秒(之前是 30 秒)
4. **退出程序**
## 测试方法
1. 启动后端服务器
2. 打开前端,建立 SSE 连接
3. 在前端打开一个文件夹(触发文件监听)
4. 在后端控制台按 `Ctrl+C`
5. 观察日志输出
**预期输出**:
```
Shutting down server...
Cleaning up resources...
Resources cleaned up
Server exited gracefully
```
**预期时间**: < 1 秒(之前需要 30 秒)
## 修改的文件
- `rear/main.go` - 调用清理函数
- `rear/internal/router/router.go` - 返回清理函数
- `rear/internal/handler/filesystem_handler.go` - 暴露文件监听器
- `rear/internal/utils/filesystem_watcher.go` - 添加 Close 方法
## 注意事项
1. **清理顺序很重要**: 必须先清理长连接资源,再关闭 HTTP 服务器
2. **超时时间调整**: 从 30 秒改为 5 秒,因为长连接已提前关闭
3. **并发安全**: SSE Manager 和 FileSystemWatcher 的 Close 方法都是并发安全的
4. **幂等性**: Close 方法可以多次调用而不会出错
## 日志说明
关闭时的日志输出:
```
2025-10-05 15:30:00.000 INFO Shutting down server...
2025-10-05 15:30:00.100 INFO Cleaning up resources...
2025-10-05 15:30:00.150 INFO Resources cleaned up
2025-10-05 15:30:00.200 INFO Server exited gracefully
```
如果仍然超时,日志会显示:
```
2025-10-05 15:30:00.000 INFO Shutting down server...
2025-10-05 15:30:00.100 INFO Cleaning up resources...
2025-10-05 15:30:00.150 INFO Resources cleaned up
2025-10-05 15:30:05.200 ERROR Server forced to shutdown: context deadline exceeded
```
这种情况下需要检查是否有其他未关闭的连接或阻塞的 goroutine。
-207
View File
@@ -1,207 +0,0 @@
# 照片预览功能测试指南(v2 - macOS Quick Look 风格)
## ✅ 已实现的功能
### 后端
- [x] 预览 API: `GET /api/v1/photo/preview?path={path}&size={size}`
- [x] 智能格式转换(RAW → PNG → JPG/WebP)
- [x] 多级缩略图生成(720p / 2K / 原图)
- [x] 自动缓存机制
### 前端
- [x] **macOS Quick Look 风格预览窗口**(悬浮、圆角、半透明背景)
- [x] **文件列表中显示图片缩略图**(网格视图和列表视图)
- [x] 渐进式加载(缩略图 → 高清 → 原图)
- [x] 缩放和平移功能
- [x] 空格键快捷预览
## 🧪 测试步骤
### 1. 启动服务
#### 后端
```bash
cd D:\go-argus\src\rear
go run main.go
```
#### 前端
```bash
cd D:\go-argus\src\front\argus-front
npm run dev
```
### 2. 测试预览功能
1. **打开文件管理器**
- 访问:http://localhost:3000/field
2. **浏览到包含照片的文件夹**
- 双击进入文件夹
- 确保文件夹中有 JPG/PNG/RAW 等图片文件
3. **测试缩略图显示**(新功能!)
- 📸 **网格视图**: 照片文件应显示实际的缩略图,而不是图标
- 小尺寸:60x60 像素缩略图
- 中等尺寸:80x80 像素缩略图
- 大尺寸:120x120 像素缩略图
- 📋 **列表视图**: 照片文件图标处应显示 32x32 像素缩略图
- 文件夹和其他文件类型仍显示图标
4. **测试 Quick Look 预览**(新风格!)
- 点击选中一张照片(照片项会高亮)
- 按下 **空格键**
- 应该弹出 **macOS 风格的悬浮预览窗口**:
- ✨ 圆角边框(12px)
- 🌫️ 半透明背景(带模糊效果)
- 📏 适中大小(85%视口,可以看到后面的文件列表)
- 🎬 弹出动画(缩放 + 淡入)
- 窗口包含三部分:
- 顶部标题栏:文件名 + 文件大小 + 关闭按钮
- 中间图片区域:可缩放、平移
- 底部工具栏:缩放控制 + 质量切换
5. **测试预览交互**
- **初始加载**: 应显示 720p 缩略图(加载快)
- **双击放大**: 双击图片,自动放大并切换到高清模式
- **滚轮缩放**: 鼠标滚轮缩放图片
- **拖拽平移**: 放大后可拖拽图片查看细节
- **质量切换**:
- 点击底部"缩略图"按钮 → 720p
- 点击"高清"按钮 → 2K 高清
- 点击"原图"按钮 → 原图(可能很大)
- **关闭预览**:
- 按 ESC 键
- 点击右上角关闭按钮(悬停时变红色)
- 点击窗口外的背景区域
### 3. 测试不同文件格式
#### 小图片(< 5MB)
- 测试 JPG/PNG 小文件
- 预期:直接加载原图
#### 中等图片(5-20MB)
- 测试中等 JPG 文件
- 预期:初始加载 1080p 缩略图
#### 大图片(> 20MB)
- 测试专业相机拍摄的 RAW 或大 JPG
- 预期:
1. 初始加载 720p 缩略图(< 200KB,快速)
2. 切换到高清时加载 2K 版本(1-2MB)
3. 切换到原图时才加载完整文件
#### RAW 格式
- 测试 CR2/NEF/ARW 等 RAW 文件
- 预期:后端自动转换为 JPG 并生成缩略图
### 4. 测试边界情况
- **路径错误**: 选中文件后手动删除,再预览
- **网络错误**: 断开后端服务,再预览
- **非照片文件**: 选中文件夹或视频,按空格
- 预期:文件夹/其他文件使用旧的 QuickPreview
- 仅照片使用 PhotoPreviewModal
## 🎯 预期表现
### 性能指标
- **初始预览响应**: < 500ms(包括 Hash 计算)
- **缩略图加载**: < 200ms(720p < 200KB)
- **高清加载**: < 1s(2K 约 1-2MB)
- **原图加载**: 取决于文件大小,应显示加载进度
### 用户体验
- **渐进式加载**: 先模糊后清晰,不阻塞操作
- **平滑缩放**: 缩放和平移动画流畅
- **快捷键**: 空格预览,ESC 关闭,+/- 缩放
- **智能提示**: 底部控制栏显示当前质量和文件信息
## 🐛 已知问题与限制
1. **首次预览延迟**
- 问题:Hash 计算可能需要几百毫秒
- 解决:后续可优化为异步计算
2. **大文件内存占用**
- 问题:加载 40MB 原图可能占用大量内存
- 解决:用户需明确点击"原图"才加载
3. **视频不支持**
- 当前仅支持照片预览
- 视频预览将在未来版本实现
## 📊 测试检查清单
### 缩略图显示
- [ ] 网格视图中照片显示缩略图(不是图标)
- [ ] 列表视图中照片显示小缩略图
- [ ] 缩略图加载流畅(无明显延迟)
- [ ] 不同尺寸网格的缩略图大小适配
- [ ] 文件夹和其他文件仍显示图标
### Quick Look 预览窗口
- [ ] 窗口为悬浮样式(不是全屏)
- [ ] 窗口有圆角和阴影
- [ ] 背景半透明带模糊效果
- [ ] 弹出动画流畅(缩放+淡入)
- [ ] 可以看到后面的文件列表
- [ ] 窗口大小适中(85%视口)
### 预览功能
- [ ] 后端 API 正常响应
- [ ] 空格键触发预览
- [ ] 缩略图快速加载
- [ ] 高清模式切换正常
- [ ] 原图加载正常
- [ ] 缩放功能正常
- [ ] 平移功能正常
- [ ] 双击放大正常
- [ ] ESC 关闭正常
- [ ] 点击背景关闭正常
- [ ] 关闭按钮悬停变红
- [ ] 文件信息显示正确
- [ ] 不同格式都能预览(JPG/PNG/RAW)
- [ ] 错误处理正常(路径错误、网络错误)
## 🔧 调试技巧
### 查看网络请求
1. 打开浏览器开发者工具(F12)
2. 切换到 Network 标签
3. 预览照片,观察请求:
- `/api/v1/photo/preview?path=...&size=720` → 缩略图
- `/api/v1/photo/preview?path=...&size=2048` → 高清
- `/api/v1/photo/preview?path=...&size=0` → 原图
### 查看控制台日志
- 前端错误会显示在 Console 标签
- 后端日志在终端输出
### 检查文件生成
- 缩略图缓存位置:`D:\go-argus\src\rear\thumbnail\{hash前4位}\{hash}\{size}.jpg`
- 检查是否生成了对应尺寸的缩略图
## 📝 下一步优化
1. **性能优化**
- [ ] Hash 计算异步化
- [ ] 预加载相邻照片
- [ ] IndexedDB 缓存
2. **功能增强**
- [ ] 左右方向键切换照片
- [ ] 缩略图网格预览
- [ ] 幻灯片模式
3. **用户体验**
- [ ] 加载进度条
- [ ] 更多缩放选项
- [ ] 旋转功能
---
**测试日期**: 2025-10-06
**版本**: v1.0
**维护者**: Claude Code
-156
View File
@@ -1,156 +0,0 @@
# 照片预览功能 v2 - 更新总结
## 🎨 主要改进
### 1. macOS Quick Look 风格预览窗口
**之前**:全屏黑色背景,覆盖整个屏幕
**现在**:悬浮窗口,类似 macOS Finder 的 Quick Look
#### 视觉效果
- ✨ **悬浮窗口**:85% 视口大小,居中显示
- 🌫️ **半透明背景**:带 20px 模糊效果,可以看到后面的文件列表
- 🎨 **圆角设计**:12px 圆角 + 大阴影
- 🎬 **优雅动画**:弹出时缩放+淡入效果(0.25s)
#### 窗口结构
```
┌─────────────────────────────────────┐
│ 标题栏:文件名 + 文件大小 + [X] │ ← 深色半透明
├─────────────────────────────────────┤
│ │
│ 图片显示区域 │
│ (可缩放、可拖拽) │
│ │
├─────────────────────────────────────┤
│ 底部工具栏:缩放控制 + 质量切换 │ ← 深色半透明
└─────────────────────────────────────┘
```
#### 交互优化
- **多种关闭方式**:
- ESC 键
- 点击右上角 X(悬停变红)
- 点击窗口外背景区域
- **窗口尺寸**:
- 宽度:85vw(最大 1400px)
- 高度:85vh(最大 900px)
- 适中的大小,不会完全遮挡文件列表
### 2. 文件列表中显示真实缩略图
**之前**:所有照片都显示 🖼️ 图标
**现在**:照片文件显示实际的缩略图预览
#### 网格视图缩略图
- **小尺寸**:60x60 像素
- **中等尺寸**:80x80 像素
- **大尺寸**:120x120 像素
- 圆角边框(8px)+ 阴影效果
#### 列表视图缩略图
- **尺寸**:32x32 像素
- 小圆角(4px)+ 轻微阴影
- 与文件名对齐显示
#### 智能加载策略
- **网格视图**:加载 300px 缩略图(适中质量)
- **列表视图**:加载 200px 缩略图(更快速度)
- 使用背景图片方式加载,避免布局抖动
- 浏览器自动缓存,重复访问无需再次请求
## 📁 修改的文件
### 1. PhotoPreviewModal.vue
**位置**:`D:\go-argus\src\front\argus-front\src\components\FileManager\PhotoPreviewModal.vue`
**主要改动**:
- 重构 HTML 结构:`backdrop` + `window` + `header` + `footer`
- 完全重写 CSS 样式:macOS 风格
- 添加动画效果:`fadeIn` + `scaleIn`
- 优化按钮样式:关闭按钮悬停变红
### 2. FilePane.vue
**位置**:`D:\go-argus\src\front\argus-front\src\components\FileManager\FilePane.vue`
**主要改动**:
- 模板修改:
- 网格视图图标添加缩略图背景(第 129-139 行)
- 列表视图图标添加缩略图背景(第 194-203 行)
- JavaScript:
- 新增 `getThumbnailStyle()` 函数(第 1843-1856 行)
- CSS:
- 添加 `.file-icon.has-thumbnail` 样式(第 2318-2335 行)
- 添加 `.list-icon.has-thumbnail` 样式(第 2496-2503 行)
### 3. 测试文档更新
**位置**:`D:\go-argus\src\PHOTO_PREVIEW_TEST.md`
**更新内容**:
- 新增缩略图测试步骤
- 新增 Quick Look 风格测试要点
- 扩展测试检查清单
## 🎯 用户体验提升
### 快速浏览
- **之前**:看到图标,猜测内容,按空格查看
- **现在**:直接看到缩略图,一目了然
### 预览操作
- **之前**:全屏预览,看不到其他文件,需要关闭后才能切换
- **现在**:悬浮预览,可以看到文件列表,快速判断是否要切换
### 视觉美感
- **之前**:纯黑背景,过于简陋
- **现在**:半透明毛玻璃效果,优雅专业
## 🚀 性能优化
### 缩略图加载
- 使用小尺寸缩略图(200-300px),加载快速
- 浏览器自动缓存,重复访问无延迟
- CSS background-image 加载,不影响布局
### 预览窗口
- 初始加载 720p 缩略图(< 200KB)
- 按需加载高清/原图(用户主动切换时)
- 渐进式体验,不卡顿
## 📊 对比总结
| 特性 | v1 (全屏) | v2 (Quick Look) |
|------|----------|-----------------|
| 窗口样式 | 全屏黑色背景 | 悬浮窗口 |
| 背景效果 | 纯黑 | 半透明模糊 |
| 可见性 | 完全遮挡 | 可见文件列表 |
| 圆角 | 无 | 12px 圆角 |
| 阴影 | 无 | 大阴影 |
| 动画 | 无 | 缩放+淡入 |
| 关闭方式 | ESC / 按钮 | ESC / 按钮 / 点击背景 |
| 缩略图 | ❌ 仅图标 | ✅ 真实缩略图 |
## ✨ 下一步建议
1. **左右方向键切换照片**
- 预览状态下,按左右键切换同文件夹的照片
- 自动预加载前后照片
2. **缩略图加载优化**
- 虚拟滚动时按需加载可见区域缩略图
- IntersectionObserver 懒加载
3. **预览增强**
- 添加照片信息显示(分辨率、拍摄日期等)
- 支持旋转功能
- 支持全屏切换
4. **手势支持**(桌面应用)
- 双指捏合缩放
- 双指滑动切换照片
---
**版本**: v2.0
**更新日期**: 2025-10-06
**改进者**: Claude Code
-240
View File
@@ -1,240 +0,0 @@
# SSE 实时更新修复测试指南
## 📋 修复内容总结
### 前端修复 (Vue)
1. **修复响应式更新问题** - `FilePane.vue`
- ✅ `visibleItems` computed 直接使用 `fileSystemAPI.fileItems.value`
- ✅ 避免了通过 `currentFolder` 的中间转换导致响应式断裂
2. **移除双重刷新问题** - `useFileSystemAPI.ts`
- ✅ CRUD 操作 (create/delete/move/copy) 不再手动调用 `browse()`
- ✅ 完全依赖 SSE 推送自动刷新,避免双重请求
3. **修复路径匹配逻辑** - `useFileSystemAPI.ts`
- ✅ 统一路径分隔符处理 (支持 Windows/Linux/macOS)
- ✅ 使用 `normalize()` 函数将 `\` 转换为 `/`
4. **添加详细调试日志**
- 🔍 `useFileSystemAPI.browse()` - API 请求和响应
- 🔍 `useFileSystemAPI.fileItems` - computed 重新计算
- 🔍 `useFileSystemAPI.handleFileSystemChange()` - SSE 事件接收和路径匹配
- 🔍 `FilePane.visibleItems` - 最终渲染项目计算
- 🔍 `FilePane.currentFolder` - 文件夹数据计算
### 后端优化 (Go)
1. **优化日志输出** - `filesystem_handler.go`
- ✅ 浏览请求: `🔄 浏览文件系统请求`
- ✅ 订阅监听: `👀 订阅文件夹监听请求`
- ✅ 文件变化推送: `📤 文件系统变化事件已推送`
- ✅ 所有日志会写入 `logs/app.log`
---
## 🧪 测试步骤
### 准备工作
1. **启动后端服务**
```bash
cd D:\go-argus\src\rear
go run main.go
```
2. **启动前端服务**
```bash
cd D:\go-argus\src\front\argus-front
npm run dev
```
3. **打开浏览器控制台** (F12)
---
### 测试 1: 基础文件删除自动刷新
**步骤**:
1. 访问 `http://localhost:3000/#/field`
2. 进入某个文件夹 (例如 `D:\test`)
3. **观察控制台日志**,应该看到:
```
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
✅ [useFileSystemAPI.browse] API 返回: { items_count: 5, ... }
🔄 [useFileSystemAPI.fileItems] computed 重新计算: 5 个项目
🔄 [FilePane.visibleItems] computed 重新计算
✅ [FilePane.visibleItems] 最终返回: 5 个项目
```
4. **在文件资源管理器中删除该文件夹内的一个文件**
5. **观察控制台日志**,应该看到:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "test.txt", ... }
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: { match: true }
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (delete): test.txt
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
✅ [useFileSystemAPI.browse] API 返回: { items_count: 4, ... }
🔄 [useFileSystemAPI.fileItems] computed 重新计算: 4 个项目
🔄 [FilePane.visibleItems] computed 重新计算
✅ [FilePane.visibleItems] 最终返回: 4 个项目
```
6. **观察页面**, 文件列表应该自动更新,删除的文件消失
**预期结果**: ✅ 页面自动刷新,无需手动刷新
---
### 测试 2: 创建文件自动刷新
**步骤**:
1. 在当前文件夹中新建一个文件 (例如 `新建文本文档.txt`)
2. **观察控制台日志**:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "create", name: "新建文本文档.txt", ... }
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (create): 新建文本文档.txt
🔄 [useFileSystemAPI.browse] 开始刷新
✅ [useFileSystemAPI.browse] API 返回: { items_count: 5, ... }
```
3. **观察页面**, 新文件应该自动出现
**预期结果**: ✅ 页面自动显示新文件
---
### 测试 3: 重命名文件自动刷新
**步骤**:
1. 重命名文件夹中的某个文件
2. **观察控制台日志**:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "rename", ... }
```
3. **观察页面**, 文件名应该自动更新
**预期结果**: ✅ 页面自动显示新文件名
---
### 测试 4: 路径过滤 (不同文件夹的事件不应触发刷新)
**步骤**:
1. 当前在 `D:\test` 文件夹
2. 在 `D:\other` 文件夹中删除文件
3. **观察控制台日志**:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", path: "D:\other\file.txt", ... }
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: { match: false }
⏭️ [useFileSystemAPI.handleFileSystemChange] 事件不在当前路径,忽略
```
4. **观察页面**, 不应该发生任何变化
**预期结果**: ✅ 页面不刷新
---
### 测试 5: 防抖机制 (快速连续删除多个文件)
**步骤**:
1. 快速连续删除 3 个文件 (在 1 秒内)
2. **观察控制台日志**:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file1.txt" }
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file2.txt" }
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: { type: "delete", name: "file3.txt" }
// 300ms 防抖后只刷新一次
🔄 [useFileSystemAPI.browse] 开始刷新
```
3. **观察页面**, 应该只刷新一次,3 个文件同时消失
**预期结果**: ✅ 只发送 1 次 API 请求,页面正确更新
---
## 📊 后端日志检查
### 查看日志文件
```bash
# Windows
type D:\go-argus\src\rear\logs\app.log | findstr "文件系统"
# Linux/macOS
tail -f D:\go-argus\src\rear\logs\app.log | grep "文件系统"
```
### 预期看到的日志
**订阅监听时**:
```
👀 订阅文件夹监听请求 | path=D:\test
✅ 已开始监听文件夹 | path=D:\test | total_watched=1
```
**文件变化时**:
```
📤 文件系统变化事件已推送 | event_type=delete | file_path=D:\test\file.txt | file_name=file.txt | watched_path=D:\test | is_dir=false | timestamp=2025-10-05 14:30:45
```
**浏览请求时**:
```
🔄 浏览文件系统请求 | path=D:\test
✅ 浏览文件系统成功 | current_path=D:\test | items_count=4
```
---
## 🐛 问题排查
### 如果页面不自动刷新
**检查清单**:
1. **检查 SSE 连接是否建立**
- 控制台是否有 `✅ SSE 连接已建立`
- 网络面板是否有持续的 `GET /api/v1/sse/connect` 请求
2. **检查是否收到 SSE 事件**
- 控制台是否有 `📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件`
- 如果没有,检查后端日志是否有 `📤 文件系统变化事件已推送`
3. **检查路径匹配**
- 控制台日志中 `🔍 路径匹配检查` 的 `match` 字段是否为 `true`
- 如果为 `false`,检查 `eventDir` 和 `currentDir` 是否一致
4. **检查 computed 是否重新计算**
- 控制台是否有 `🔄 [FilePane.visibleItems] computed 重新计算`
- 如果没有,说明响应式链断裂
5. **检查后端监听是否正常**
- 查看 `logs/app.log` 是否有 `✅ 已开始监听文件夹`
- 检查 `total_watched` 数量是否正确
---
## 📝 已知限制
1. **防抖延迟**: 300ms 内的多次变化会合并为一次刷新
2. **只监听直接子项**: 不会递归监听子文件夹的变化
3. **Windows 路径**: 后端日志中路径使用 `\`,前端日志中统一为 `/`
---
## ✅ 成功标准
- ✅ 删除文件后,页面在 300-500ms 内自动更新
- ✅ 创建文件后,页面在 300-500ms 内自动显示新文件
- ✅ 重命名文件后,页面自动显示新名称
- ✅ 其他文件夹的变化不会触发当前页面刷新
- ✅ 控制台日志完整,无错误
- ✅ 后端日志文件包含所有关键事件
---
**测试日期**: 2025-10-05
**修复版本**: v1.0-sse-fix
-371
View File
@@ -1,371 +0,0 @@
# SSE 订阅和文件监听诊断指南
## 问题描述
前端打开某个文件夹后,通过文件管理器删除/改名文件,页面不自动更新。
## 诊断流程
### 步骤 1: 检查前端订阅请求是否发送
**操作**:
1. 打开浏览器控制台(F12)
2. 切换到 **Network** 标签页
3. 在前端进入某个文件夹(例如 `D:\test`)
**预期看到的请求**:
```
POST http://localhost:9484/api/v1/filesystem/watch
Request Payload: {"path":"D:\\test"}
Response:
{
"code": 200,
"message": "已开始监听文件夹",
"data": {
"path": "D:\\test",
"watched_paths": ["D:\\test"]
}
}
```
**同时检查 Console 日志**:
```
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
✅ [useFileSystemAPI.browse] API 返回: { ... }
✅ [useFileSystemAPI.browse] items.value 已更新: 5 个项目
✅ 已开始监听: D:\test
```
**如果没有看到 `✅ 已开始监听`**:
- 检查 `watchCurrentPath()` 是否被调用
- 检查是否有错误日志 `⚠️ [useFileSystemAPI.browse] 订阅监听失败`
- 检查 Network 是否有 `/filesystem/watch` 请求
---
### 步骤 2: 检查后端是否收到订阅请求
**操作**:
查看后端日志 `D:\go-argus\src\rear\logs\app.log`
**预期日志**:
```
👀 订阅文件夹监听请求 | path=D:\test | client_ip=127.0.0.1
开始监听文件夹: D:\test
✅ 已开始监听文件夹 | path=D:\test | total_watched=1
```
**如果没有这些日志**:
- 前端请求没有发送
- 检查前端代码 `watchCurrentPath()` 逻辑
- 检查路径是否为空: `if (!currentPath.value || currentPath.value === '')`
**如果有订阅日志,但 total_watched=0**:
- 后端 `fileWatcher.Watch()` 失败
- 检查路径是否有效
- 检查文件权限
---
### 步骤 3: 检查文件变化是否被监听到
**操作**:
1. 确认步骤 1 和 2 都通过
2. 在文件管理器中删除 `D:\test` 下的一个文件(例如 `test.txt`)
3. 立即查看后端日志
**预期日志**:
```
📤 文件系统变化事件已推送 | event_type=delete | file_path=D:\test\test.txt | file_name=test.txt | watched_path=D:\test | is_dir=false | timestamp=2025-10-05 14:30:45
```
**如果没有这条日志**:
- **问题**: fsnotify 没有监听到文件变化
- **可能原因**:
1. Windows 路径问题:`D:\test` vs `D:/test`
2. 文件监听器没有启动
3. 路径未正确添加到监听列表
**调试方法**: 在后端添加调试日志
```go
// 在 filesystem_handler.go 的 WatchPath 中添加
fmt.Printf("DEBUG: 监听路径 = %s\n", req.Path)
fmt.Printf("DEBUG: 当前监听列表 = %v\n", h.fileWatcher.GetWatchedPaths())
```
---
### 步骤 4: 检查 SSE 事件是否推送到前端
**操作**:
1. 删除文件后,检查浏览器控制台 Console 日志
**预期日志**:
```
📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件: {
type: "delete",
path: "D:\\test\\test.txt",
name: "test.txt",
is_dir: false,
timestamp: "2025-10-05T14:30:45Z"
}
```
**如果没有这条日志**:
- **问题**: SSE 事件没有推送到前端
- **可能原因**:
1. SSE 连接断开
2. 事件没有通过 SSE Manager 广播
3. 前端 `sseService.onFileSystemChange()` 没有注册
**检查 SSE 连接**:
在 Network 标签查看是否有持续的请求:
```
GET http://localhost:9484/api/v1/sse/connect
Status: 200 (持续连接)
Type: eventsource
```
---
### 步骤 5: 检查路径匹配逻辑
**操作**:
如果步骤 4 有日志,但页面不刷新,检查路径匹配日志
**预期日志**:
```
🔍 [useFileSystemAPI.handleFileSystemChange] 路径匹配检查: {
eventDir: "D:/test",
currentDir: "D:/test",
match: true
}
✅ [useFileSystemAPI.handleFileSystemChange] 当前文件夹内容发生变化 (delete): test.txt
🔄 [useFileSystemAPI.browse] 开始刷新: D:\test
```
**如果 match: false**:
- **问题**: 路径不匹配
- **可能原因**:
1. 路径分隔符不一致 (`\` vs `/`)
2. 路径大小写不一致
3. eventDir 和 currentDir 值不同
**调试方法**:
修改 `handleFileSystemChange` 添加详细日志:
```typescript
console.log('DEBUG: event.path =', event.path)
console.log('DEBUG: currentPath.value =', currentPath.value)
console.log('DEBUG: eventDir =', eventDir)
console.log('DEBUG: currentDir =', currentDir)
```
---
## 常见问题和解决方案
### 问题 1: 订阅请求没有发送
**症状**: Network 中没有 `/filesystem/watch` 请求
**原因**:
- `currentPath.value === ''` (根目录跳过监听)
- `currentPath.value === previousWatchedPath.value` (重复路径跳过)
**解决**:
清除 `previousWatchedPath` 缓存
```typescript
// 在 browse() 开始时强制重置
previousWatchedPath.value = ''
```
---
### 问题 2: 后端收到订阅但没有监听到文件变化
**症状**: 有 `👀 订阅文件夹监听请求` 日志,但删除文件后没有 `📤 文件系统变化事件已推送`
**原因**:
- `fileWatcher.Watch()` 内部失败
- Windows 路径规范化问题
**调试步骤**:
1. 检查 `filesystem_watcher.go` 的 `Watch()` 方法
2. 打印 `absPath` 和 `fsw.watchedPaths`
3. 确认 `fsw.watcher.Add(absPath)` 没有返回错误
**临时解决**:
在 `Watch()` 中添加详细日志:
```go
func (fsw *FileSystemWatcher) Watch(path string) error {
absPath, err := filepath.Abs(path)
fmt.Printf("DEBUG: 原始路径=%s, 绝对路径=%s\n", path, absPath)
if err := fsw.watcher.Add(absPath); err != nil {
fmt.Printf("ERROR: fsnotify.Add() 失败: %v\n", err)
return err
}
fmt.Printf("SUCCESS: 已添加监听: %s\n", absPath)
return nil
}
```
---
### 问题 3: SSE 事件推送了但前端没收到
**症状**: 后端有 `📤 文件系统变化事件已推送`,但前端 Console 没有对应日志
**原因**:
- SSE 连接已断开
- 事件类型不匹配
- `sseService.onFileSystemChange()` 回调没有注册
**检查 SSE 连接**:
```typescript
// 在 useFileSystemAPI onMounted 中添加
console.log('SSE 连接状态:', sseService.isConnected())
```
**检查事件订阅**:
```typescript
// 在 sseService.ts 中添加
onFileSystemChange(callback: (event: FileSystemChangeEvent) => void) {
console.log('注册 FileSystemChange 回调')
this.eventHandlers['filesystem_change'] = callback
}
```
---
### 问题 4: 路径匹配失败
**症状**: 有 SSE 事件,但 `match: false`
**原因**:
- Windows 路径 `D:\test` vs `D:/test`
- 事件路径是子文件路径 `D:\test\file.txt`
- currentPath 是文件夹路径 `D:\test`
**当前实现**:
```typescript
const eventDir = eventPath.substring(0, eventPath.lastIndexOf('/'))
if (eventDir === currentDir || eventPath.startsWith(currentDir + '/'))
```
**可能问题**:
- `eventDir` 提取错误
- `currentDir` 格式不一致
**改进建议**:
```typescript
// 更健壮的路径匹配
const normalize = (p: string) => {
return p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
}
const normalizedEvent = normalize(event.path)
const normalizedCurrent = normalize(currentPath.value)
// 检查事件是否发生在当前目录或其子目录
const isInCurrentDir = normalizedEvent.startsWith(normalizedCurrent + '/') ||
normalizedEvent === normalizedCurrent
// 对于删除/重命名,只关心直接子项(不包括子文件夹内的文件)
const eventDir = normalizedEvent.substring(0, normalizedEvent.lastIndexOf('/'))
const isDirectChild = (eventDir === normalizedCurrent)
if (isDirectChild) {
console.log('✅ 当前文件夹的直接子项发生变化')
debouncedRefresh(currentPath.value)
}
```
---
## 快速诊断命令
### 检查后端监听状态
```bash
# Windows
curl http://localhost:9484/api/v1/filesystem/watched
# 预期返回
{
"code": 200,
"message": "获取监听路径成功",
"data": {
"count": 1,
"paths": ["D:\\test"]
}
}
```
### 手动订阅测试
```bash
# 订阅
curl -X POST http://localhost:9484/api/v1/filesystem/watch ^
-H "Content-Type: application/json" ^
-d "{\"path\":\"D:\\test\"}"
# 取消订阅
curl -X POST http://localhost:9484/api/v1/filesystem/unwatch ^
-H "Content-Type: application/json" ^
-d "{\"path\":\"D:\\test\"}"
```
### 检查 SSE 连接
```bash
# 使用 curl 监听 SSE
curl -N http://localhost:9484/api/v1/sse/connect
# 预期看到心跳事件
event: heartbeat
data: {"timestamp":"2025-10-05T14:30:00Z"}
```
---
## 诊断检查清单
完成以下检查,标记每一步的结果:
- [ ] **步骤 1**: 前端发送订阅请求 `/filesystem/watch`
- [ ] Network 中看到请求
- [ ] Response 返回 200
- [ ] Console 显示 `✅ 已开始监听`
- [ ] **步骤 2**: 后端收到订阅请求
- [ ] 日志中有 `👀 订阅文件夹监听请求`
- [ ] 日志中有 `✅ 已开始监听文件夹`
- [ ] `total_watched >= 1`
- [ ] **步骤 3**: 文件变化被监听到
- [ ] 删除文件后日志中有 `📤 文件系统变化事件已推送`
- [ ] `event_type` 正确 (delete/create/modify/rename)
- [ ] `file_path` 正确
- [ ] **步骤 4**: SSE 事件推送到前端
- [ ] Console 中有 `📨 [useFileSystemAPI.handleFileSystemChange] 收到 SSE 事件`
- [ ] 事件内容正确
- [ ] Network 中 SSE 连接保持活跃
- [ ] **步骤 5**: 路径匹配和刷新
- [ ] Console 中有 `🔍 路径匹配检查`
- [ ] `match: true`
- [ ] Console 中有 `✅ 当前文件夹内容发生变化`
- [ ] Console 中有 `🔄 [useFileSystemAPI.browse] 开始刷新`
---
## 下一步操作
根据上面的检查结果,找到第一个失败的步骤:
1. **如果步骤 1 失败**: 修复前端订阅逻辑
2. **如果步骤 2 失败**: 检查后端路由和 Handler
3. **如果步骤 3 失败**: 检查 `filesystem_watcher.go` 实现
4. **如果步骤 4 失败**: 检查 SSE Manager 和事件广播
5. **如果步骤 5 失败**: 修复路径匹配逻辑
**请按照上面的诊断流程,逐步检查并告诉我哪一步失败了。**
-421
View File
@@ -1,421 +0,0 @@
# SSE 订阅管理系统使用指南
## 📋 概述
本系统实现了**客户端级别的 SSE 订阅管理**,解决了以下问题:
1. ✅ **精准推送** - 只向订阅了特定路径的客户端发送事件
2. ✅ **自动清理** - 客户端断开时自动清理订阅
3. ✅ **独立管理** - 每个客户端独立管理自己的订阅列表
4. ✅ **减少噪音** - 避免无关事件干扰客户端
---
## 🏗️ 架构设计
### 核心组件
```
┌─────────────────────────────────────────────┐
│ Frontend (Vue3) │
│ ┌──────────────────────────────────────┐ │
│ │ sseService.ts │ │
│ │ - connect() │ │
│ │ - subscribe(path) │ │
│ │ - unsubscribe(path) │ │
│ │ - getSubscriptions() │ │
│ └──────────────────────────────────────┘ │
└──────────────────┬──────────────────────────┘
│ HTTP + EventSource
┌──────────────────▼──────────────────────────┐
│ Backend (Go/Gin) │
│ ┌──────────────────────────────────────┐ │
│ │ SSE Manager │ │
│ │ - RegisterClient() │ │
│ │ - Subscribe(clientID, path) │ │
│ │ - Unsubscribe(clientID, path) │ │
│ │ - BroadcastToSubscribers(path, ...) │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ FileSystem Watcher │ │
│ │ - Watch(path) │ │
│ │ - Unwatch(path) │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
---
## 🚀 使用流程
### 1. 前端连接 SSE
```typescript
import { sseService } from '@/services/sseService'
// 连接 SSE 服务器
await sseService.connect()
// 等待客户端 ID 初始化
await new Promise(resolve => setTimeout(resolve, 1000))
// 获取客户端 ID
const clientID = sseService.getClientID()
console.log('客户端 ID:', clientID)
```
### 2. 订阅文件夹
```typescript
// 订阅指定路径
const path = 'D:\\EdgeDownload'
const success = await sseService.subscribe(path)
if (success) {
console.log('✅ 订阅成功')
}
```
### 3. 监听文件系统变化
```typescript
// 注册回调函数
const unsubscribe = sseService.onFileSystemChange((event) => {
console.log('文件系统变化:', event)
console.log('类型:', event.type) // create, modify, delete, rename
console.log('路径:', event.path)
console.log('名称:', event.name)
})
// 组件卸载时取消监听
onUnmounted(() => {
unsubscribe()
})
```
### 4. 取消订阅
```typescript
// 取消订阅指定路径
await sseService.unsubscribe(path)
```
### 5. 查看当前订阅
```typescript
// 获取当前所有订阅
const subscriptions = await sseService.getSubscriptions()
console.log('当前订阅:', subscriptions)
```
---
## 📦 完整 Vue 组件示例
```vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { sseService } from '@/services/sseService'
interface FileSystemEvent {
type: 'create' | 'modify' | 'delete' | 'rename'
path: string
name: string
timestamp: string
is_dir: boolean
}
const currentPath = ref('D:\\EdgeDownload')
const events = ref<FileSystemEvent[]>([])
const isSubscribed = ref(false)
// 订阅回调清理函数
let unsubscribeCallback: (() => void) | null = null
onMounted(async () => {
// 连接 SSE
await sseService.connect()
// 等待客户端 ID 初始化
await new Promise(resolve => setTimeout(resolve, 1000))
// 订阅当前路径
const success = await sseService.subscribe(currentPath.value)
if (success) {
isSubscribed.value = true
// 监听文件系统变化
unsubscribeCallback = sseService.onFileSystemChange((event) => {
events.value.unshift(event)
// 保留最近 100 个事件
if (events.value.length > 100) {
events.value = events.value.slice(0, 100)
}
})
}
})
onUnmounted(async () => {
// 取消订阅
if (isSubscribed.value) {
await sseService.unsubscribe(currentPath.value)
}
// 取消回调监听
if (unsubscribeCallback) {
unsubscribeCallback()
}
})
// 切换监听的文件夹
async function watchFolder(newPath: string) {
// 取消旧订阅
await sseService.unsubscribe(currentPath.value)
// 添加新订阅
const success = await sseService.subscribe(newPath)
if (success) {
currentPath.value = newPath
events.value = []
}
}
</script>
<template>
<div>
<h3>当前监听: {{ currentPath }}</h3>
<p>订阅状态: {{ isSubscribed ? '✅ 已订阅' : '❌ 未订阅' }}</p>
<div>
<h4>文件系统事件 ({{ events.length }})</h4>
<ul>
<li v-for="(event, index) in events" :key="index">
<strong>{{ event.type }}</strong>: {{ event.name }}
<span v-if="event.is_dir"> (文件夹)</span>
</li>
</ul>
</div>
</div>
</template>
```
---
## 🔧 后端 API
### 1. 订阅路径
```http
POST /api/v1/sse/subscribe
Content-Type: application/json
{
"client_id": "your-client-id",
"path": "D:\\EdgeDownload"
}
```
**响应:**
```json
{
"success": true,
"message": "Subscribed successfully",
"data": {
"client_id": "your-client-id",
"path": "D:\\EdgeDownload"
}
}
```
### 2. 取消订阅
```http
POST /api/v1/sse/unsubscribe
Content-Type: application/json
{
"client_id": "your-client-id",
"path": "D:\\EdgeDownload"
}
```
### 3. 获取订阅列表
```http
GET /api/v1/sse/subscriptions/:clientId
```
**响应:**
```json
{
"success": true,
"message": "Subscriptions retrieved successfully",
"data": {
"client_id": "your-client-id",
"subscriptions": ["D:\\EdgeDownload", "D:\\Projects"],
"count": 2
}
}
```
---
## 🔍 调试技巧
### 前端调试
```typescript
// 1. 检查客户端 ID
console.log('客户端 ID:', sseService.getClientID())
// 2. 检查连接状态
console.log('连接状态:', sseService.isConnected())
// 3. 查看当前订阅
const subs = await sseService.getSubscriptions()
console.log('当前订阅:', subs)
```
### 后端日志
后端会输出以下日志:
```bash
# 客户端连接
Client connected: abc-123 (Total: 1)
# 订阅路径
客户端 abc-123 订阅了路径: D:\EdgeDownload
# 广播事件
向 1 个订阅了路径 D:\EdgeDownload 的客户端广播事件
# 取消订阅
客户端 abc-123 取消订阅路径: D:\EdgeDownload
# 客户端断开
Client disconnected: abc-123 (Total: 0)
```
---
## ⚠️ 注意事项
### 1. 客户端 ID 初始化
客户端 ID 是在 SSE 连接建立后通过 `connected` 事件传递的,需要等待:
```typescript
await sseService.connect()
await new Promise(resolve => setTimeout(resolve, 1000)) // 等待初始化
```
### 2. 组件卸载时清理
**必须在组件卸载时取消订阅和回调**:
```typescript
onUnmounted(async () => {
await sseService.unsubscribe(path)
unsubscribeCallback()
})
```
### 3. 路径格式
- ✅ Windows: `D:\\EdgeDownload`(双反斜杠)
- ✅ Linux/Mac: `/home/user/Downloads`
### 4. SSE 重连处理
SSE 连接会自动重连,但订阅信息保留在服务端,**无需重新订阅**。
---
## 🎯 常见问题
### Q1: 订阅后收不到事件?
检查清单:
- [ ] 客户端 ID 是否正确获取
- [ ] 路径是否已正确订阅(调用 `getSubscriptions()` 确认)
- [ ] 后端是否正在监听该路径(检查文件系统监听器)
- [ ] 监听路径是否与实际文件变化路径一致
### Q2: SSE 连接频繁断开?
- 当前后端配置:WriteTimeout = 5 分钟
- 前端心跳超时:90 秒
- Keepalive 间隔:5 秒
- Ping 间隔:10 秒
如果仍然断开,检查:
- 网络代理或防火墙设置
- 浏览器是否限制长连接
- 服务器资源是否充足
### Q3: 客户端断开后订阅会自动清理吗?
✅ **是的**!客户端断开时:
1. SSE Manager 自动注销客户端
2. 客户端的订阅映射自动清理
3. 不会再收到任何事件
---
## 📊 性能优化
### 1. 事件节流
后端使用事件节流通道(容量 10):
- 高频事件会被自动丢弃
- 避免 SSE 通道阻塞
### 2. 精准推送
只向订阅了特定路径的客户端发送事件:
- 减少无效网络传输
- 降低客户端处理负担
### 3. 并发发送
后端使用 goroutine 并发向多个客户端发送事件:
- 单个客户端阻塞不影响其他客户端
- 100ms 超时自动断开慢速客户端
---
## 🔒 安全建议
1. **客户端 ID 验证** - 服务端验证客户端 ID 是否存在
2. **路径验证** - 限制订阅路径范围(防止订阅敏感目录)
3. **订阅数量限制** - 限制单个客户端最大订阅数
4. **连接数限制** - 当前限制 500 个并发连接
---
## 🎉 总结
### 优势
- ✅ **精准推送** - 只收到感兴趣的事件
- ✅ **自动清理** - 断开连接自动清理订阅
- ✅ **易于使用** - 简洁的 API 设计
- ✅ **高性能** - 节流 + 并发优化
- ✅ **可扩展** - 支持多客户端、多路径订阅
### 下一步
1. 实现路径权限验证
2. 添加订阅数量限制
3. 支持路径模糊匹配(如 `D:\\**\\*.jpg`)
4. 添加订阅统计和监控
---
**文档版本**: 1.0
**最后更新**: 2025-10-04
**维护者**: Claude Code
-340
View File
@@ -1,340 +0,0 @@
# SSE 连接断开问题解决方案
## 🔍 问题分析
### 错误现象
```
GET http://127.0.0.1:3001/api/v1/sse/connect net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)
```
### 根本原因
**Go 的 `http.Server.WriteTimeout` 与 SSE 长连接冲突**
1. **WriteTimeout 机制**:
- 从第一次写入开始计时
- 整个响应必须在超时前完成
- **不会因为持续写入而重置**
2. **SSE 的 c.Stream() 机制**:
```go
c.Stream(func(w io.Writer) bool {
select {
case event := <-client.EventChan:
writeEvent(w, event) // 写入
return true
}
})
```
- 每次循环等待新事件
- 等待期间**没有写入操作**
- 如果等待时间 > WriteTimeout → 连接关闭
3. **时间线示例** (WriteTimeout = 5分钟):
```
0:00 - 连接建立,发送 connected 事件
0:05 - 发送 keepalive (写入)
0:10 - 发送 ping (写入)
...
5:00 - 从第一次写入起已过 5 分钟
5:00 - http.Server 强制关闭连接
5:00 - 前端收到 ERR_INCOMPLETE_CHUNKED_ENCODING
```
---
## ✅ 解决方案
### 当前实现: WriteTimeout = 0
**配置文件**: `rear/internal/config/loader.go:125`
```go
App: AppConfig{
Port: getEnv("PORT", "8080"),
Mode: getEnv("GIN_MODE", "debug"),
ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // 🔑 SSE 需要长连接,设置为 0(无限期)
IdleTimeout: 10 * time.Minute,
}
```
**HTTP 服务器**: `rear/main.go:237-243`
```go
srv := &http.Server{
Addr: ":" + config.CONFIG.Port,
Handler: r,
ReadTimeout: config.CONFIG.ReadTimeout, // 30秒
WriteTimeout: config.CONFIG.WriteTimeout, // 0(无限期)
IdleTimeout: config.CONFIG.IdleTimeout, // 10分钟
}
```
---
## ⚠️ WriteTimeout = 0 的影响
### 对所有路由的影响
| 路由类型 | 影响 | 风险 |
|----------|------|------|
| 普通 API | ✅ 无影响 | ⚠️ 慢速客户端可能占用连接 |
| 文件上传 | ✅ 支持大文件 | ⚠️ 恶意客户端可慢速上传 |
| 文件下载 | ✅ 支持大文件 | ⚠️ 慢速客户端占用资源 |
| SSE | ✅✅ 正常工作 | ✅ 无风险 |
### 风险缓解措施
#### 1. 应用层超时控制
普通 API 使用 Context 超时:
```go
func (h *Handler) SomeAPI(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// 使用 ctx 进行数据库查询等操作
result, err := db.QueryContext(ctx, ...)
}
```
#### 2. 限流中间件
```go
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(100, 200) // 100 req/s, burst 200
func RateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(429, gin.H{"error": "Too many requests"})
c.Abort()
return
}
c.Next()
}
}
```
#### 3. 连接数限制
当前配置:
```go
// SSE Manager
opts.MaxClients = 500 // 最多 500 个 SSE 连接
```
#### 4. IdleTimeout 保护
```go
IdleTimeout: 10 * time.Minute // 10分钟无活动则关闭
```
---
## 🚀 生产环境部署建议
### 使用 Nginx 反向代理
#### Nginx 配置示例
```nginx
upstream argus_backend {
server 127.0.0.1:9484;
keepalive 64;
}
server {
listen 80;
server_name your-domain.com;
# 普通 API 路由(有超时限制)
location /api/ {
proxy_pass http://argus_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 普通请求超时设置
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# SSE 路由(特殊配置)
location /api/v1/sse/ {
proxy_pass http://argus_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# SSE 专用配置
proxy_buffering off; # 🔑 禁用缓冲
proxy_cache off; # 禁用缓存
proxy_read_timeout 3600s; # 1小时超时
proxy_connect_timeout 3600s;
chunked_transfer_encoding on; # 启用分块传输
proxy_set_header X-Accel-Buffering no; # 禁用 Nginx 缓冲
}
}
```
### 架构图
```
客户端
↓
Nginx (80端口)
├─→ /api/* (30秒超时)
└─→ /api/v1/sse/* (1小时超时)
↓
Go 服务器 (9484端口, WriteTimeout=0)
```
---
## 🔬 其他尝试过的方案
### ❌ 方案 1: 中间件修改 Deadline
**代码**:
```go
func NoWriteDeadlineMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if conn, ok := getUnderlyingConn(c.Writer); ok {
conn.SetWriteDeadline(time.Time{}) // 移除 deadline
}
c.Next()
}
}
```
**问题**:
- 无法在不 Hijack 的情况下访问 `net.Conn`
- Hijack 会接管连接,无法继续使用 Gin 的 Stream
**结论**: ❌ 不可行
---
### ❌ 方案 2: 定期写入 comment
**代码**:
```go
// 每隔一段时间写入 SSE comment 保持连接
go func() {
ticker := time.NewTicker(2 * time.Minute)
for range ticker.C {
fmt.Fprintf(w, ": keepalive\n\n")
w.Flush()
}
}()
```
**问题**:
- WriteTimeout 是从**第一次写入**开始计时
- 持续写入并不会重置超时
- 5分钟后依然会断开
**结论**: ❌ 无效
---
## 📊 测试结果
### 修改前 (WriteTimeout = 5分钟)
```
0:00 - 连接建立
0:05 - keepalive
0:10 - ping
...
5:00 - ❌ ERR_INCOMPLETE_CHUNKED_ENCODING
```
### 修改后 (WriteTimeout = 0)
```
0:00 - 连接建立
0:05 - keepalive
0:10 - ping
0:15 - keepalive
...
60:00 - ✅ 仍然连接
```
**测试命令**:
```bash
# 1. 启动后端
cd D:\go-argus\src\rear
go run main.go
# 2. 浏览器控制台测试
const es = new EventSource('http://127.0.0.1:9484/api/v1/sse/connect')
es.addEventListener('keepalive', e => console.log('keepalive:', e.data))
es.addEventListener('ping', e => console.log('ping:', e.data))
# 观察是否在5分钟后仍然收到事件
```
---
## 📚 参考资料
### Go 官方文档
- [http.Server](https://pkg.go.dev/net/http#Server)
> WriteTimeout: 写入响应的最大持续时间(从请求头读取完成到响应写入完成)。**零值表示无超时**。
- [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
### 相关问题
- [Gin + SSE timeout issue](https://github.com/gin-gonic/gin/issues/1287)
- [Go http.Server WriteTimeout for SSE](https://stackoverflow.com/questions/37294519)
---
## 🎯 总结
### 最终方案
✅ **WriteTimeout = 0** (当前实现)
### 优点
1. ✅ 简单可靠,一行配置解决
2. ✅ SSE 连接稳定,不会断开
3. ✅ 适用于开发和小型部署
### 注意事项
1. ⚠️ 生产环境建议使用 Nginx 反向代理
2. ⚠️ 应用层需要自己控制超时(Context)
3. ⚠️ 配置限流和连接数限制
### 是否还会断开?
**仍可能断开的情况**:
1. **IdleTimeout 触发** (10分钟无活动)
- **解决**: 后端每 5 秒发送 keepalive ✅
2. **浏览器/代理超时**
- **解决**: 前端自动重连 ✅
3. **网络波动**
- **解决**: 前端自动重连 ✅
**正常情况**: 连接应该保持稳定,偶尔断开会自动重连
---
**文档版本**: 1.0
**最后更新**: 2025-10-04
**作者**: Claude Code