fix: 第四轮调试 — 回复去重/消息时序/UI布局/自主思考深度优化 + 文档重整

后端修复:
- 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>
This commit is contained in:
2026-05-23 13:09:18 +08:00
parent 0c1bbff7b4
commit b123a36aae
37 changed files with 580 additions and 174 deletions
+201
View File
@@ -0,0 +1,201 @@
# Round 2 深度调试报告
**日期**: 2026-05-21
**类型**: 综合性深度调试 + 即时修复
**状态**: ✅ 完成
---
## 1. 执行摘要
本次 Round 2 深度调试通过 Chromium CDP 进行前端 E2E 测试 + 后端全面 API 验证,发现并修复了 **3 个问题**(1 个 P0 崩溃、1 个 P0 后端 PANIC、1 个测试工具缺陷),最终 **14/14 全部通过**
---
## 2. 发现的问题与修复
### 2.1 P0 — `useSpeechSynthesis.ts` `cancel()` 未守卫调用
**文件**: [`frontend/web/src/hooks/useSpeechSynthesis.ts`](../../frontend/web/src/hooks/useSpeechSynthesis.ts)
**问题**: 参考 `docs/debug/2026-05-21-crash-cancel.md` 的详细分析。
- `stop()` 回调 (原 L223):仅当 `utteranceRef.current` 存在时才调用 `cancel()`,但浏览器 Web Speech API 可能在任意时刻有活跃 utterance,导致 `cancel()` 被跳过
- Cleanup effect (原 L261):同样的问题,组件卸载时可能跳过 `cancel()`
**修复**: 将守卫条件从 `utteranceRef.current` 改为 `isSupported`
```typescript
// stop() — 当 isSupported 为 true 时始终调用 cancel()
const stop = useCallback(() => {
if (!isSupported) {
console.warn('[useSpeechSynthesis] stop: speechSynthesis not supported');
return;
}
window.speechSynthesis.cancel();
// ... 重置状态
}, [isSupported]);
// cleanup effect — 同样始终调用 cancel(),并清理 resumeIntervalRef
useEffect(() => {
return () => {
if (isSupported) {
window.speechSynthesis.cancel();
}
if (resumeIntervalRef.current) {
clearInterval(resumeIntervalRef.current);
resumeIntervalRef.current = null;
}
};
}, [isSupported]);
```
**状态**: ✅ 已修复
---
### 2.2 P0 — IoT 子会话 `nil pointer dereference` PANIC
**文件**: [`backend/ai-core/internal/subsession/iot_provider.go`](../../backend/ai-core/internal/subsession/iot_provider.go)
**根因**:
1. [`iot_provider.go:116`](../../backend/ai-core/internal/subsession/iot_provider.go:116): `persona.NewLoader("")` 传入空字符串
2. [`persona/loader.go:24-27`](../../backend/ai-core/internal/persona/loader.go:24): `os.ReadDir("")` 失败,返回 `nil, error`
3. [`iot_provider.go:118`](../../backend/ai-core/internal/subsession/iot_provider.go:118): 仅 `log.Printf` 错误,未检查 `loader` 是否为 nil
4. [`iot_provider.go:120`](../../backend/ai-core/internal/subsession/iot_provider.go:120): `loader.Get("cyrene")` 对 nil 解引用 → **PANIC**
**崩溃日志**:
```
[iot-provider] 加载人格配置失败: 读取人格目录失败: open : no such file or directory
[subsession] dispatch goroutine panic 恢复 (type=iot): runtime error: invalid memory address or nil pointer dereference
```
**修复**: 添加 `loader != nil` 守卫,优雅降级使用默认值:
```go
// 加载人格配置
trueName := "昔涟"
loader, err := persona.NewLoader("")
if err != nil {
log.Printf("[iot-provider] 加载人格配置失败: %v", err)
}
if loader != nil {
if personaConfig, err := loader.Get("cyrene"); err == nil && personaConfig != nil {
trueName = personaConfig.Identity.TrueName
}
}
```
**编译与部署**: 重新编译 `ai-core` (`go build`)kill 旧进程 (PID 12870),启动新进程 (PID 22942)。
**状态**: ✅ 已修复并验证
---
### 2.3 测试工具 — CDP 脚本按钮匹配错误
**文件**: [`debug/cache/test_cdp_e2e_v4.py`](../../debug/cache/test_cdp_e2e_v4.py)
**问题**: 测试脚本查找 `btn.textContent.includes('登录')` 的按钮,但页面上有两个包含"登录"的按钮:
- 模式切换按钮 `"登录"` → 调用 `switchMode('login')`(不触发表单提交)
- 真正的提交按钮 `"进入昔涟的世界 ♪"` → 调用 `handleLogin()`
**修复**: 改为匹配提交按钮的特征文字 `"进入"``"昔涟"`
**状态**: ✅ 已修复并验证
---
## 3. 验证结果汇总
### 3.1 CDP E2E v4 测试 (14/14 全部通过)
| # | 检查项 | 状态 |
|---|--------|------|
| 1 | 页面无 JS 异常 | ✅ |
| 2 | 无硬编码 `test-token-cyrene` | ✅ |
| 3 | 找到用户名输入框 | ✅ |
| 4 | 找到密码输入框 | ✅ |
| 5 | 找到登录按钮 (`"进入昔涟的世界 ♪"`) | ✅ |
| 6 | 无认证错误 (API 调用 8 次) | ✅ |
| 7 | localStorage 有 token | ✅ |
| 8 | 侧边栏或聊天区域存在 | ✅ |
| 9 | 会话列表 | ✅ |
| 10 | 创建新会话 | ✅ |
| 11 | 获取消息历史 | ✅ |
| 12 | 记忆列表 | ✅ |
| 13 | WebSocket 连接 | ✅ |
| 14 | 收到 IoT 响应 | ✅ |
### 3.2 后端 API 深度验证
| API | 方法 | 状态 | 备注 |
|-----|------|------|------|
| `/api/v1/health` | GET | ✅ 200 | 5 服务全部健康 |
| `/api/v1/auth/login` | POST | ✅ 200 | 返回 `token`, `user_id`, `expires` |
| `/api/v1/auth/refresh` | POST | ✅ 200 | 返回新 token |
| `/api/v1/sessions?user_id=admin` | GET | ✅ 200 | `{"sessions": [...]}` |
| `/api/v1/sessions` | POST | ✅ 201 | 创建会话成功 |
| `/api/v1/sessions/{id}/messages` | GET | ✅ 200 | 历史消息正常 |
| `/api/v1/memory` | GET | ✅ 200 | 记忆列表正常 |
| `/api/v1/memory/search?query=IoT` | GET | ⚠️ 400 | 参数格式问题(非阻塞) |
| `/ws/chat` | WS | ✅ 101 | 消息流正常 |
### 3.3 WebSocket + IoT 流验证
- **WebSocket 连接**: ✅ 正常建立
- **设备状态广播**: ✅ 每 10 秒推送 8 个设备状态
- **IoT 查询消息流**: ✅ 完整流程:`device_update``stream_chunk``response`
- **IoT 子会话**: ✅ 不再 PANIC,优雅降级
- **后台思考**: ✅ `post_chat` 触发正常
### 3.4 服务运行时状态
| 服务 | 端口 | PID | 状态 |
|------|------|-----|------|
| gateway | 8080 | 12874 | ✅ |
| ai-core | 8081 | 22942 | ✅ (已更新) |
| memory-service | 8091 | 12864 | ✅ |
| tool-engine | 8092 | 12868 | ✅ |
| iot-debug-service | 8083 | 12866 | ✅ |
| vite preview | 5199 | 1266 | ✅ |
| chromium CDP | 9225 | 4741 | ✅ |
---
## 4. 已知遗留问题
### 4.1 低优先级 — Memory Search 400
`GET /api/v1/memory/search?query=IoT` 返回 400。需要在 `memory_handler.go` 中确认查询参数名是否正确(可能是 `q` 而非 `query`)。
### 4.2 低优先级 — `MessageBubble.tsx` `setInterval` 无清理
[`MessageBubble.tsx:105-110`](../../frontend/web/src/components/chat/MessageBubble.tsx) 中 `AIMessageActions``checkEnd` interval 没有在组件卸载时清理,可能导致内存泄漏。
### 4.3 低优先级 — IoT 子会话未匹配到"列出所有设备"
`iot_provider.go``matchIotOperation()` 函数未匹配 `"列出所有IoT设备"` 关键词。该消息被降级到 General 子会话处理,虽不影响功能但无法触发 IoT 专用响应。
### 4.4 信息 — 登录响应无 `refresh_token` 字段
后端 [`auth_handler.go:208`](../../backend/gateway/internal/handler/auth_handler.go:208) 登录响应仅返回 `token`, `user_id`, `expires`,前端 [`client.ts:80`](../../frontend/web/src/api/client.ts:80) `getRefreshToken()` 读取 `localStorage.getItem('refresh_token')` 始终为 null。
---
## 5. 修改文件清单
| 文件 | 变更类型 | 描述 |
|------|----------|------|
| `frontend/web/src/hooks/useSpeechSynthesis.ts` | 🐛 修复 | P0: cancel() 守卫条件改为 isSupported |
| `backend/ai-core/internal/subsession/iot_provider.go` | 🐛 修复 | P0: 添加 loader nil 检查,防止 PANIC |
| `backend/ai-core/cmd/ai-core` | 🔄 重新编译 | 包含上述修复 |
| `debug/cache/test_cdp_e2e_v4.py` | 🐛 修复 | 按钮匹配逻辑改为匹配提交按钮 |
| `debug/cache/test_cdp_token_investigation.py` | 新增 | test-token-cyrene 来源诊断脚本 |
| `debug/cache/test_cdp_e2e_v3.py` | ➕ 新增 | E2E v3 测试(发现 API 格式差异) |
---
## 6. 结论
Round 2 深度调试成功完成。发现了 **2 个 P0 级别缺陷**useSpeechSynthesis cancel 守卫缺失、IoT 子会话 nil pointer PANIC)和 **1 个测试工具缺陷**(按钮匹配错误),全部已修复并验证通过。系统各组件(gateway、ai-core、memory-service、tool-engine、iot-debug-service)协同工作正常,WebSocket 消息流、IoT 设备广播、记忆存储链路完整。