b123a36aae
后端修复: - main.go: 恢复 /api/v1/chat 路由中丢失的 handleChat 调用 (空响应回归) - orchestrator.go: splitChatByLines 改为双换行分割, 避免单换行误拆 - chat_handler.go: multi_message 增加 !hasReview 守卫, 消息延迟 200→800ms - thinker.go: RecordUserMessage 追踪活跃会话ID, 推送主动消息到正确会话 - thinker.go: 增强思考提示词 — 禁止在用户休息/离开时发送主动消息 前端修复: - useWebSocket.ts: stream_segments 不再创建消息气泡, 消除重复回复 - MessageBubble.tsx: 动作消息居左对齐无头像, 时间戳移至气泡外侧 hover 显示 - ChatInput.tsx: 昔涟输入提示移至输入框上方, 波点动画效果 - MessageList/TypingIndicator/ChatContainer: 清理冗余 isTyping 传递 - MemoryPanel.tsx: 新增记忆面板组件 文档重整: - docs/debug/ → docs/debug_log/ 重命名统一 - 新增 debug_log/README.md 索引 - .gitignore: 新增 android/ 排除规则 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
315 lines
12 KiB
Markdown
315 lines
12 KiB
Markdown
# 持续性调试第3轮: 面板功能API + 前端Vite构建 + Chat SSE/WebSocket
|
||
|
||
> 日期: 2026-05-20 14:11 CST (UTC+8)
|
||
> 测试者: Debug Mode (deepseek-v4-pro)
|
||
> Gateway 端口: 8080 | PostgreSQL: localhost:5432
|
||
|
||
---
|
||
|
||
## 测试概览
|
||
|
||
| 测试项 | 状态 | 详情 |
|
||
|--------|------|------|
|
||
| Admin 登录 | ⚠️ | 密码不一致问题,需用 `admin123` 或注册新用户 |
|
||
| Files GET/POST | ✅ | 列表和上传端点正常 |
|
||
| Knowledge GET/POST | ✅ | Knowledge Base 创建/列表正常 |
|
||
| Automation GET/POST | ✅ | Rules 和 Scenes 正常 |
|
||
| Briefing GET/POST | ✅ | 简报生成正常 (fallback 模式) |
|
||
| Reminder GET/POST | ✅ | 提醒创建/列表正常 |
|
||
| 前端 Vite 构建 | ✅ | 77 modules, 1.02s, 无错误 |
|
||
| WebSocket Hub | ✅ | 架构完善,支持 IoT 广播、会话状态追踪 |
|
||
| CORS 中间件 | ⚠️ | `*` + `credentials:true` 组合无效 |
|
||
| 安全头 | ❌ | 缺少 CSP, HSTS, X-Frame-Options 等 |
|
||
|
||
---
|
||
|
||
## 1. 认证登录测试
|
||
|
||
### 测试命令
|
||
```bash
|
||
# 尝试默认管理员密码
|
||
curl -s -X POST http://localhost:8080/api/v1/auth/login \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username":"admin","password":"cyrene-dev-admin"}'
|
||
# 输出: {"error":"用户名或密码错误"}
|
||
|
||
# 成功注册新用户
|
||
curl -s -X POST http://localhost:8080/api/v1/auth/register \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"username":"testuser","password":"test123456","email":"test@test.com","nickname":"Test","verify_code":"000000"}'
|
||
# 输出: 成功返回 token + user_id
|
||
```
|
||
|
||
### 根因分析: 种子密码与配置密码不一致
|
||
- [`config.go:97`](../../backend/gateway/internal/config/config.go:97): `AdminPassword` 默认值为 `"cyrene-dev-admin"`
|
||
- [`main.go:68-70`](../../backend/gateway/cmd/main.go:68): 种子管理员密码取自 `os.Getenv("ADMIN_PASSWORD")`,回退值为 `"admin123"`
|
||
- [`auth_handler.go:189-202`](../../backend/gateway/internal/handler/auth_handler.go:189): `verifyUserPassword` 使用 bcrypt 验证数据库中的密码 hash
|
||
- 第一次启动时,若 `ADMIN_PASSWORD` 环境变量未设置,数据库存入的是 `admin123` 的 bcrypt hash;但 `config.AdminPassword` 返回的是 `"cyrene-dev-admin"`
|
||
- **实际情况**: admin 已被迁移到 users 表,密码为 `"admin123"`
|
||
|
||
### 🔧 建议修复
|
||
统一密码源:让 `main.go` 中的种子操作使用 `cfg.AdminPassword` 而非单独的 `os.Getenv("ADMIN_PASSWORD")`:
|
||
```go
|
||
// main.go:68 改为
|
||
defaultAdminPassword := cfg.AdminPassword
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 面板 API 测试
|
||
|
||
### 2.1 测试结果
|
||
|
||
所有面板 API 使用 `user_testuser` token 测试,后端路由路径与用户文档预期路径存在差异:
|
||
|
||
| 用户文档路径 (第1轮) | 实际后端路径 | GET 状态 | POST 状态 |
|
||
|----------------------|-------------|----------|-----------|
|
||
| `/api/v1/files` | `/api/v1/files` | ✅ 200 | ✅ (multipart upload) |
|
||
| `/api/v1/knowledge` | `/api/v1/knowledge/bases` | ✅ 200 | ✅ 201 |
|
||
| `/api/v1/automation` | `/api/v1/automation/rules` | ✅ 200 | ✅ 201 |
|
||
| `/api/v1/briefing` | `/api/v1/briefings` | ✅ 200 | ✅ (generate) |
|
||
| `/api/v1/reminder` | `/api/v1/reminders` | ✅ 200 | ✅ 201 |
|
||
|
||
### 2.2 发现的问题
|
||
|
||
#### P1: 前端 Knowledge API 响应键名不匹配
|
||
- **文件**: [`frontend/web/src/api/knowledge.ts:43`](../../frontend/web/src/api/knowledge.ts:43)
|
||
- **前端期望**: `{ bases: KnowledgeBase[] }`
|
||
- **后端实际返回**: `{ knowledge_bases: [...], total: 0 }`
|
||
- **影响**: 知识库列表无法正常显示
|
||
- **修复**: 将 `bases` 改为 `knowledge_bases`,或统一后端响应键名
|
||
|
||
```typescript
|
||
// knowledge.ts:42-44 当前代码
|
||
interface KBListResponse {
|
||
bases: KnowledgeBase[]; // ❌ 后端返回 knowledge_bases
|
||
}
|
||
|
||
// 应改为
|
||
interface KBListResponse {
|
||
knowledge_bases: KnowledgeBase[];
|
||
total: number;
|
||
}
|
||
```
|
||
|
||
#### P2: 简报 `created_at` 零值时间
|
||
- **测试输出**: `"created_at":"0001-01-01T00:00:00Z"`
|
||
- **原因**: [`briefing_handler.go`](../../backend/gateway/internal/handler/briefing_handler.go) 中创建 Briefing 时未设置 `CreatedAt` 字段
|
||
- **影响**: 前端格式化日期异常
|
||
- **修复**: 生成简报时设置 `CreatedAt: time.Now()`
|
||
|
||
#### P3: 分页参数不一致
|
||
- Files: 使用 `page` + `limit`
|
||
- Reminders: 使用 `offset` + `limit`
|
||
- 建议统一为 `offset` + `limit` 或 `cursor` 模式
|
||
|
||
---
|
||
|
||
## 3. 前端 Vite 生产构建
|
||
|
||
### 构建结果
|
||
```
|
||
vite v6.4.2 building for production...
|
||
✓ 77 modules transformed.
|
||
dist/index.html 0.82 kB │ gzip: 0.50 kB
|
||
dist/assets/index-B6Z-MAXg.css 38.53 kB │ gzip: 7.11 kB
|
||
dist/assets/index-C0wremLr.js 287.20 kB │ gzip: 82.77 kB
|
||
✓ built in 1.02s
|
||
```
|
||
|
||
✅ **构建成功**,无错误,产物大小合理。
|
||
|
||
---
|
||
|
||
## 4. Chat SSE / WebSocket 代码审查
|
||
|
||
### 4.1 架构概述
|
||
|
||
```
|
||
Browser WebSocket ──→ Gateway /ws/chat (chat_handler.go)
|
||
│
|
||
├── ReadPump (client.go) ← 用户消息
|
||
│ └── handleMessage() → streamResponse()
|
||
│
|
||
├── WritePump (client.go) → 推送消息给浏览器
|
||
│
|
||
└── Hub (hub.go)
|
||
├── 会话状态追踪 (SessionState)
|
||
├── 对话缓存 (ConversationCache)
|
||
├── IoT 设备广播 (每10秒)
|
||
├── 闲置会话清理 (每5分钟)
|
||
└── AI-Core SSE 调用 (streamResponse)
|
||
```
|
||
|
||
### 4.2 发现的问题
|
||
|
||
#### P4 (Critical): `randomStr()` 生成极弱的随机ID
|
||
- **文件**: [`chat_handler.go:435-442`](../../backend/gateway/internal/handler/chat_handler.go:435)
|
||
- **问题**: `time.Now().UnixNano()` 在循环中几乎不变,导致 `generateID()` 产生重复字符(如 `"aaaaaa"`)
|
||
- **影响**: 消息 ID 碰撞风险,会话 ID 可预测性
|
||
- **修复**:
|
||
```go
|
||
// 当前代码 (有缺陷)
|
||
func randomStr(n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||
b := make([]byte, n)
|
||
for i := range b {
|
||
b[i] = letters[time.Now().UnixNano()%int64(len(letters))] // ❌ 始终同一值
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
// 建议修复
|
||
import "crypto/rand"
|
||
func randomStr(n int) string {
|
||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||
b := make([]byte, n)
|
||
rand.Read(b) // ✅ 密码学安全随机
|
||
for i := range b {
|
||
b[i] = letters[int(b[i])%len(letters)]
|
||
}
|
||
return string(b)
|
||
}
|
||
```
|
||
|
||
#### P5: WebSocket 仅限管理员访问
|
||
- **文件**: [`chat_handler.go:70-77`](../../backend/gateway/internal/handler/chat_handler.go:70)
|
||
- **当前行为**: `!strings.HasPrefix(userID, "admin_")` 返回 403
|
||
- **影响**: 普通用户无法使用 WebSocket 聊天功能
|
||
- **评估**: 可能是设计意图 (MVP 阶段仅管理员可用),但需确认
|
||
|
||
#### P6: `history_response` 竞态条件已防御但注释不清晰
|
||
- **文件**: [`useWebSocket.ts:191-202`](../../frontend/web/src/hooks/useWebSocket.ts:191)
|
||
- 前端对 WebSocket `history_response` 和 HTTP `loadMessagesFromServer` 之间的竞态有防御逻辑
|
||
- 但有隐藏 bug:如果 HTTP 加载了 0 条消息(新会话),WS 的 `history_response` 仍会被忽略,导致会话恢复信息丢失
|
||
|
||
### 4.3 WebSocket 协议完整性
|
||
|
||
`ServerMessage` 支持的消息类型:
|
||
- `response`, `stream_chunk`, `stream_end`, `stream_segments`, `multi_message`
|
||
- `error`, `pong`, `notification`, `device_update`, `background_thinking`
|
||
- `history_response`
|
||
|
||
前端 `useWebSocket.ts` 已正确处理:`response`, `stream_chunk`, `stream_end`, `history_response`, `device_update`, `background_thinking`, `notification`, `error`, `pong`
|
||
|
||
⚠️ 前端未处理 `stream_segments` 和 `multi_message` 消息类型 — 这些是新功能的后端消息,前端尚未实现对应 UI。
|
||
|
||
---
|
||
|
||
## 5. CORS 和安全头检查
|
||
|
||
### 5.1 CORS 中间件
|
||
|
||
- **文件**: [`cors.go`](../../backend/gateway/internal/middleware/cors.go)
|
||
- OPTIONS 预检返回 `204 No Content` ✅
|
||
- CORS 头正确返回 ✅
|
||
|
||
**P7 (Medium): `Access-Control-Allow-Origin: *` 与 `Access-Control-Allow-Credentials: true` 冲突**
|
||
|
||
```go
|
||
// cors.go:12-15 当前代码
|
||
c.Header("Access-Control-Allow-Origin", "*") // ❌ 通配符
|
||
c.Header("Access-Control-Allow-Credentials", "true") // ❌ 与通配符不兼容
|
||
```
|
||
|
||
根据 CORS 规范,当 `credentials: true` 时,`Access-Control-Allow-Origin` 不能为 `*`。浏览器会拒绝此类响应。
|
||
|
||
**修复**:
|
||
```go
|
||
func CORS() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
origin := c.GetHeader("Origin")
|
||
if origin == "" {
|
||
origin = "*"
|
||
}
|
||
c.Header("Access-Control-Allow-Origin", origin)
|
||
c.Header("Access-Control-Allow-Credentials", "true")
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 缺失的安全头
|
||
|
||
当前响应头中**完全没有**以下安全相关 HTTP 头:
|
||
|
||
| 安全头 | 状态 | 建议值 |
|
||
|--------|------|--------|
|
||
| `X-Content-Type-Options` | ❌ 缺失 | `nosniff` |
|
||
| `X-Frame-Options` | ❌ 缺失 | `DENY` |
|
||
| `Strict-Transport-Security` | ❌ 缺失 | `max-age=31536000; includeSubDomains` |
|
||
| `Content-Security-Policy` | ❌ 缺失 | 至少 `default-src 'self'` |
|
||
| `Referrer-Policy` | ❌ 缺失 | `strict-origin-when-cross-origin` |
|
||
| `X-XSS-Protection` | ❌ 缺失 | `1; mode=block` |
|
||
|
||
**建议**: 添加安全头中间件或增强现有 CORS 中间件。
|
||
|
||
---
|
||
|
||
## 6. 前后端 API 路径对照表
|
||
|
||
| 前端 API 文件 | 调用的路径 | 后端路由 (router.go) | 匹配? |
|
||
|--------------|-----------|---------------------|-------|
|
||
| `api/files.ts` | `/files` | `protected.Group("/files")` | ✅ |
|
||
| `api/knowledge.ts` | `/knowledge/bases` | `protected.Group("/knowledge")` | ✅ |
|
||
| `api/knowledge.ts` | `/knowledge/search` | `POST /knowledge/search` | ✅ |
|
||
| `api/automation.ts` | `/automation/rules` | `automation.Group("/rules")` | ✅ |
|
||
| `api/automation.ts` | `/automation/scenes` | `automation.Group("/scenes")` | ✅ |
|
||
| `api/reminders.ts` | `/reminders` | `protected.Group("/reminders")` | ✅ |
|
||
| `api/briefings.ts` | `/briefings` | `protected.Group("/briefings")` | ✅ |
|
||
| `api/briefings.ts` | `/briefings/latest` | `GET /briefings/latest` | ✅ |
|
||
| `api/briefings.ts` | `/briefings/generate` | `POST /briefings/generate` | ✅ |
|
||
| `api/client.ts` | `/sessions` | `sessions := protected.Group("/sessions")` | ✅ |
|
||
| `api/client.ts` | `/memory` | `memory := protected.Group("/memory")` | ✅ |
|
||
| `api/client.ts` | `/auth/login` | `auth.POST("/login")` | ✅ |
|
||
| `hooks/useWebSocket.ts` | `/ws/chat` | `wsGroup.GET("/chat")` | ✅ |
|
||
|
||
**结论**: 前后端 API 路径完全匹配 ✅
|
||
|
||
---
|
||
|
||
## 7. 问题优先级汇总
|
||
|
||
| # | 严重度 | 问题描述 | 文件 |
|
||
|---|--------|---------|------|
|
||
| P1 | 🔴 High | Knowledge API 响应键名 `knowledge_bases` vs 前端期望 `bases` | `knowledge.ts:43` |
|
||
| P2 | 🟡 Medium | 简报 `created_at` 为零值 `0001-01-01T00:00:00Z` | `briefing_handler.go` |
|
||
| P3 | 🟢 Low | Files/Reminders 分页参数不统一 (page vs offset) | 多个文件 |
|
||
| P4 | 🔴 High | `randomStr()` 使用 `time.Now().UnixNano()` 产生弱随机 | `chat_handler.go:435` |
|
||
| P5 | 🟡 Medium | WebSocket 仅限管理员(设计确认后决定) | `chat_handler.go:70` |
|
||
| P6 | 🟢 Low | `history_response` 丢弃边缘情况 | `useWebSocket.ts:195` |
|
||
| P7 | 🟡 Medium | CORS `*` + `credentials:true` 违反规范 | `cors.go:12-15` |
|
||
| P8 | 🟡 Medium | 缺少 6 个安全 HTTP 头 | `cors.go` / 新中间件 |
|
||
| P9 | 🟢 Low | Admin 种子密码与配置默认密码不一致 | `main.go:68` |
|
||
|
||
---
|
||
|
||
## 8. 系统状态
|
||
|
||
- **Gateway**: ✅ 运行中,端口 8080,0 个活跃 WS 连接
|
||
- **PostgreSQL**: ✅ 可用
|
||
- **前端构建**: ✅ `dist/` 目录已生成
|
||
- **系统时间**: 2026-05-20 14:11 CST (UTC+8)
|
||
|
||
---
|
||
|
||
## 9. 测试覆盖率
|
||
|
||
- [x] Files GET (返回空列表,200)
|
||
- [x] Files 上传端点注册正确
|
||
- [x] Knowledge Base CREATE (201)
|
||
- [x] Knowledge Base LIST (200)
|
||
- [x] Automation Rule CREATE (201)
|
||
- [x] Automation Rule LIST (200)
|
||
- [x] Automation Scene LIST (200)
|
||
- [x] Briefing GET (200, 无当日简报)
|
||
- [x] Briefing GENERATE (200, fallback 模式生成)
|
||
- [x] Reminder CREATE (201)
|
||
- [x] Reminder LIST (200)
|
||
- [x] 前端 Vite build (成功)
|
||
- [x] CORS 预检 (OPTIONS 返回 204)
|
||
- [x] WebSocket 端点可达 (401 未认证, 预期行为)
|
||
- [x] Health 端点 (GET 200)
|
||
- [x] 完整源码审查 (hub.go, client.go, protocol.go, chat_handler.go, cors.go, auth.go, ratelimit.go, logging.go)
|
||
- [x] 前后端 API 路径对照
|