# 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 设备广播、记忆存储链路完整。