后端修复: - 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>
8.0 KiB
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
问题: 参考 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:
// 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
根因:
iot_provider.go:116:persona.NewLoader("")传入空字符串persona/loader.go:24-27:os.ReadDir("")失败,返回nil, erroriot_provider.go:118: 仅log.Printf错误,未检查loader是否为 niliot_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 守卫,优雅降级使用默认值:
// 加载人格配置
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
问题: 测试脚本查找 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 中 AIMessageActions 的 checkEnd interval 没有在组件卸载时清理,可能导致内存泄漏。
4.3 低优先级 — IoT 子会话未匹配到"列出所有设备"
iot_provider.go 的 matchIotOperation() 函数未匹配 "列出所有IoT设备" 关键词。该消息被降级到 General 子会话处理,虽不影响功能但无法触发 IoT 专用响应。
4.4 信息 — 登录响应无 refresh_token 字段
后端 auth_handler.go:208 登录响应仅返回 token, user_id, expires,前端 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 设备广播、记忆存储链路完整。