Files
Cyrene/docs/debug_log/2026-05-21-round2-fixes.md
T
AskaEth b123a36aae 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>
2026-05-23 13:09:18 +08:00

202 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 设备广播、记忆存储链路完整。