Files
Cyrene/docs/debug_log/2026-05-20-round8-docker-pwa-websocket.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

314 lines
17 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.
# 第8轮调试报告:Docker 容器化 + PWA/Service Worker + WebSocket 深度测试
> **日期**: 2026年5月20日 15:23 CST
> **范围**: Docker 配置审计、PWA 配置审计、WebSocket 代码审查与连接测试、前端构建配置审计
> **方法**: 静态代码审查 + HTTP/WebSocket 端点测试
---
## 目录
1. [Docker 配置审计](#1-docker-配置审计)
2. [PWA 配置审计](#2-pwa-配置审计)
3. [WebSocket 代码审查与测试](#3-websocket-代码审查与测试)
4. [前端构建配置审计](#4-前端构建配置审计)
5. [问题汇总与修复优先级](#5-问题汇总与修复优先级)
---
## 1. Docker 配置审计
### 1.1 Docker Compose 文件概览
| 文件 | 用途 | 服务数 |
|------|------|--------|
| [`docker-compose.yml`](docker-compose.yml:1) | 生产环境 | 10 (含基础设施) |
| [`docker-compose.dev.yml`](docker-compose.dev.yml:1) | 开发环境 (全栈) | 11 (含 NATS) |
| [`docker-compose.dev.db.yml`](docker-compose.dev.db.yml:1) | 开发环境 (仅基础设施) | 5 |
### 1.2 发现的问题
#### 🔴 严重 (P0)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 1 | **Caddyfile 缺失** | [`docker-compose.yml:12`](docker-compose.yml:12) 引用 `./Caddyfile`,但文件不存在 | 生产环境 `docker-compose up` 时 Caddy 容器将启动失败,整个集群不可访问 |
| 2 | **docker-compose.dev.yml 服务定义顺序错误** | [`docker-compose.dev.yml:108-133`](docker-compose.dev.yml:108) `ai-core``iot-debug-service` 之前定义,但 `depends_on` 引用了后者 | Docker Compose 会按依赖顺序启动,但配置中 ai-core 出现在 iot-debug-service 前面,可能造成启动顺序混乱 |
#### 🟡 中等 (P1)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 3 | **memory-service 和 tool-engine Dockerfile 缺少健康检查** | [`backend/memory-service/Dockerfile`](backend/memory-service/Dockerfile:1) 和 [`backend/tool-engine/Dockerfile`](backend/tool-engine/Dockerfile:1) 没有 `HEALTHCHECK` 指令 | Docker Compose `depends_on: condition: service_healthy` 将无法判断这两个服务是否就绪 |
| 4 | **memory-service 和 tool-engine Dockerfile 缺少非 root 用户** | 同上,缺少 `RUN adduser -D -H cyrene && USER cyrene` | 容器以 root 运行,违反最小权限原则 |
| 5 | **memory-service 和 tool-engine Dockerfile 缺少时区设置** | 同上,缺少 `tzdata` 安装和 Asia/Shanghai 时区配置 | 日志时间戳使用 UTC,与 gateway/ai-core 不一致 |
| 6 | **Redis 生产环境无密码** | [`docker-compose.yml:38`](docker-compose.yml:38) `REDIS_PASSWORD: ${REDIS_PASSWORD:-}` 默认为空 | 生产环境 Redis 无密码保护,存在安全隐患 |
| 7 | **Qdrant 和 MinIO 无健康检查** | [`docker-compose.yml:133-147`](docker-compose.yml:133) | 无法在 depends_on 中使用 condition 判断这些服务是否就绪 |
#### 🟢 轻微 (P2)
| # | 问题 | 位置 | 影响 |
|---|------|------|------|
| 8 | **Alpine 版本不一致** | gateway/ai-core/iot-debug 用 `alpine:3.20`memory-service/tool-engine/voice-service 用 `alpine:3.21` | 镜像版本碎片化,增加维护成本和安全扫描复杂度 |
| 9 | **memory-service/tool-engine Dockerfile 缺少 `-ldflags="-s -w"`** | 编译命令缺少 strip 参数 | 二进制文件体积更大(约多 30%) |
| 10 | **voice-service Dockerfile 只复制 `go.mod` 不复制 `go.sum`** | [`backend/voice-service/Dockerfile:9`](backend/voice-service/Dockerfile:9) `COPY go.mod ./` 缺少 `go.sum` | go mod download 时可能下载不一致的依赖版本 |
| 11 | **NATS 在开发环境定义但未被使用** | [`docker-compose.dev.yml:54-58`](docker-compose.dev.yml:54) | 占用资源,增加启动时间;如果计划使用 NATS 则无问题 |
| 12 | **生产环境不暴露后端端口** | [`docker-compose.yml`](docker-compose.yml:1) gateway 等服务没有 `ports:` 映射 | 这是合理的设计(通过 Caddy 反向代理),但 gateway 的 Dockerfile healthcheck 使用 `localhost:8080`,依赖 Caddy 链路可能不准确 |
### 1.3 Dockerfile 质量对比
| 特性 | Gateway | AI-Core | IoT-Debug | Voice-Svc | Memory-Svc | Tool-Engine |
|------|---------|---------|-----------|-----------|------------|-------------|
| 多阶段构建 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 静态编译 (CGO_ENABLED=0) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Strip 二进制 (-ldflags="-s -w") | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 非 root 用户 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| HEALTHCHECK | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 时区设置 (Asia/Shanghai) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| ca-certificates | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| git (构建阶段) | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| go.sum 复制 | ✅ | ✅ | N/A | ❌ | ✅ | ✅ |
| 运行时资源文件复制 | N/A | ✅ persona | N/A | N/A | N/A | N/A |
### 1.4 网络配置
生产环境使用 Docker 内部 DNS(服务名即主机名),服务间通信正确:
- `gateway``ai-core:8081`, `memory-service:8091`, `tool-engine:8092`, `voice-service:8093`, `iot-debug-service:8083`
- `tool-engine``iot-debug-service:8083`
- 所有服务 → `postgres:5432`
---
## 2. PWA 配置审计
### 2.1 文件清单
| 文件 | 行数 | 状态 |
|------|------|------|
| [`frontend/web/public/manifest.json`](frontend/web/public/manifest.json:1) | 45 | 基本完整 |
| [`frontend/web/public/sw.js`](frontend/web/public/sw.js:1) | 108 | 基本完整 |
| [`frontend/web/public/offline.html`](frontend/web/public/offline.html:1) | 131 | 良好 |
| [`frontend/web/src/hooks/usePWA.ts`](frontend/web/src/hooks/usePWA.ts:1) | 175 | 良好但有重复注册问题 |
| [`frontend/web/index.html`](frontend/web/index.html:1) | 18 | 缺少 Apple PWA meta 标签 |
### 2.2 🔴 严重问题 (P0)
#### 问题 13: Service Worker 重复注册
[`frontend/web/src/main.tsx:7-15`](frontend/web/src/main.tsx:7) 和 [`frontend/web/src/hooks/usePWA.ts:25-56`](frontend/web/src/hooks/usePWA.ts:25) 都注册了 Service Worker (`/sw.js`)。
- `main.tsx``window.load` 事件中注册
- `usePWA.ts``registerServiceWorker()` 函数也在 `window.load` 中注册
两个注册会产生竞争条件:第二个注册调用可能覆盖第一个,导致 SW 更新监听器丢失。如果 `usePWA` hook 未被挂载(例如未渲染 Header 组件),SW 仍会被 `main.tsx` 注册,但 `usePWA` 的更新检测将失效。
### 2.3 🟡 中等问题 (P1)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 14 | **缺少 Apple Web App meta 标签** | [`frontend/web/index.html`](frontend/web/index.html:1) | 缺少 `apple-mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style`, `apple-mobile-web-app-title` — iOS Safari 添加到主屏幕时不会以独立应用模式打开 |
| 15 | **SW 缓存资源列表不完整** | [`frontend/web/public/sw.js:2-5`](frontend/web/public/sw.js:2) | `ASSETS_TO_CACHE` 仅包含 `/``/index.html`,不包含 CSS/JS bundle — 首次离线访问可能缺少资源 |
| 16 | **CACHE_NAME 硬编码** | [`frontend/web/public/sw.js:1`](frontend/web/public/sw.js:1) | `'cyrene-v1'` 硬编码,版本更新需手动修改。建议基于构建时间戳或 hash |
| 17 | **Push 通知 badge 图标使用非单色图标** | [`frontend/web/public/sw.js:87`](frontend/web/public/sw.js:87) | badge 应为小尺寸单色图标(Android 通知栏专用),当前使用的 192x192 彩色 PNG 不适合 |
### 2.4 🟢 轻微问题 (P2)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 18 | **manifest.json start_url 建议用相对路径** | [`frontend/web/public/manifest.json:5`](frontend/web/public/manifest.json:5) | `"/"` 应改为 `"./"``"/index.html"`,避免部署在子路径时失效 |
| 19 | **缺少 512x512 专用图标** | [`frontend/web/public/manifest.json:18-22`](frontend/web/public/manifest.json:18) | 512x512 图标与 192x192 都指向同一张非正方形图,可能导致缩放失真 |
### 2.5 优点
- ✅ [`sw.js`](frontend/web/public/sw.js:1) 缓存策略合理:静态资源缓存优先,API 网络优先,WebSocket 不缓存
- ✅ [`offline.html`](frontend/web/public/offline.html:1) 设计良好,包含自动重连和优雅降级
- ✅ [`usePWA.ts`](frontend/web/src/hooks/usePWA.ts:1) 全面覆盖 PWA 生命周期:安装提示、更新检测、在线/离线状态
- ✅ Push 通知点击支持深度链接到会话
- ✅ manifest.json 包含 `share_target``shortcuts`
---
## 3. WebSocket 代码审查与测试
### 3.1 架构概览
```
前端 (useWebSocket.ts)
│ ws://localhost:8080/ws/chat?token=xxx&session_id=xxx
Gateway (router.go:213)
│ GET /ws/chat → chatHandler.HandleWebSocket
chat_handler.go
├── 1. Token 验证 (query param 或 Authorization header)
├── 2. Admin-only 检查 (admin_ 前缀)
├── 3. WebSocket Upgrade (gorilla/websocket)
├── 4. Client 创建 → Hub.Register
├── 5. ReadPump + WritePump goroutines
└── 消息路由:
├── "message" → AI-Core SSE 流式转发
├── "voice_input" → (占位,返回提示)
└── "history" → Hub 对话缓存查询
```
### 3.2 连接测试结果
```bash
# 1. 健康检查端点正常
$ curl -s http://localhost:8080/api/v1/health
HTTP 200
{"status":"ok","service":"cyrene-gateway","ws_connections":0}
# 2. WebSocket 端点正常响应(无 token 返回认证错误,符合预期)
$ curl -s http://localhost:8080/ws/chat
{"error":"需要认证令牌"}
```
WebSocket 端点可正常访问,认证机制生效。
### 3.3 代码质量评估
#### [`backend/gateway/internal/ws/hub.go`](backend/gateway/internal/ws/hub.go:1) (644行)
| 特性 | 状态 | 说明 |
|------|------|------|
| 客户端管理 (register/unregister) | ✅ | 使用 channel 模式的 Hub,线程安全 |
| 用户索引 (userClients) | ✅ | 支持按用户 ID 快速查找连接 |
| 会话状态追踪 | ✅ | 支持 idle/thinking/streaming/error 状态 |
| 广播机制 | ✅ | BroadcastToAll, SendToUser, SendToSession |
| 闲置清理 | ✅ | 每5分钟清理,标记超时会话为 idle |
| IoT 设备轮询 | ✅ | 每10秒从 IoT 服务获取设备状态并广播 |
| 对话缓存 | ✅ | sync.Map 缓存,最多50条消息 |
| 持久化存储集成 | ✅ | 可选 SessionStore 注入 |
#### [`backend/gateway/internal/ws/client.go`](backend/gateway/internal/ws/client.go:1) (141行)
| 特性 | 状态 | 说明 |
|------|------|------|
| Ping/Pong 心跳 | ✅ | 服务端每54秒发送 Ping,客户端60秒内需回复 Pong |
| 读写超时 | ✅ | 写超时10s,读超时60s(基于 Pong |
| 消息大小限制 | ✅ | 最大 65536 字节 |
| 优雅关闭 | ✅ | 通道关闭时发送 CloseMessage |
| 通道满处理 | ✅ | 记录日志而非静默丢弃 |
#### [`backend/gateway/internal/ws/protocol.go`](backend/gateway/internal/ws/protocol.go:1) (97行)
消息类型支持完整:
- **客户端消息**: `message`, `voice_input`, `ping`, `history`
- **服务端消息**: `response`, `stream_chunk`, `stream_end`, `history_response`, `device_update`, `notification`, `background_thinking`, `multi_message`, `stream_segments`, `pong`, `error`
### 3.4 前端 WebSocket Hook
[`frontend/web/src/hooks/useWebSocket.ts`](frontend/web/src/hooks/useWebSocket.ts:1) (275行)
| 特性 | 状态 | 说明 |
|------|------|------|
| 自动重连 | ✅ | 断开后3秒自动重连 |
| 会话感知 | ✅ | currentSessionId 变化时重建连接 |
| 消息类型处理 | ✅ | 完整覆盖所有服务端消息类型 |
| 历史竞态防护 | ✅ | HTTP 已加载时忽略 WS history_response |
| 桌面通知集成 | ✅ | Notification API 集成 |
| 连接状态反馈 | ✅ | 详细日志,包含 instance ID |
| 消息丢弃保护 | ✅ | 未就绪时发送提示消息 |
### 3.5 发现的问题
#### 🔴 严重 (P0)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 20 | **Hub.Run() 广播循环中 RLock 下执行写操作** | [`backend/gateway/internal/ws/hub.go:239-249`](backend/gateway/internal/ws/hub.go:239) | `case message := <-h.broadcast:` 块中使用了 `h.mu.RLock()` 但在 `default` 分支中执行 `delete(h.clients, client)``close(client.Send)` — 这是写操作,应该使用 `Lock()` 而非 `RLock()`。虽然 close(channel) 和 delete from map 在 Go 中可能不直接 panic,但这是不正确的锁使用,可能导致竞态条件 |
#### 🟡 中等 (P1)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 21 | **WebSocket CheckOrigin 允许所有来源** | [`backend/gateway/internal/handler/chat_handler.go:38-40`](backend/gateway/internal/handler/chat_handler.go:38) | `return true` 允许任意来源的 WebSocket 连接,生产环境应限制为已知域名 |
| 22 | **WebSocket 消息无速率限制** | [`backend/gateway/internal/handler/chat_handler.go:106`](backend/gateway/internal/handler/chat_handler.go:106) | 客户端可无限制发送消息,可能导致 AI-Core 过载 |
| 23 | **voice_input 消息类型未实现** | [`backend/gateway/internal/handler/chat_handler.go:378`](backend/gateway/internal/handler/chat_handler.go:378) | 仅返回占位错误,但 voice-service 已有 STT/TTS 实现 |
#### 🟢 轻微 (P2)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 24 | **SendMessage 通道满时静默返回 nil** | [`backend/gateway/internal/ws/client.go:138`](backend/gateway/internal/ws/client.go:138) | 函数签名返回 error 但通道满时返回 nil error,调用方无法区分丢弃和成功 |
| 25 | **WebSocket 连接无最大连接数限制** | Hub 无全局连接上限 | 恶意客户端可创建大量连接耗尽资源 |
| 26 | **useWebSocket.ts 中 connect 未被 useCallback 依赖追踪** | [`frontend/web/src/hooks/useWebSocket.ts:92`](frontend/web/src/hooks/useWebSocket.ts:92) | `connect` 的依赖数组为 `[]`,内部使用的 `getToken()` 等是函数调用而非状态依赖,这是有意的设计但需要文档说明 |
### 3.6 WebSocket 安全性总结
| 安全维度 | 状态 |
|----------|------|
| 认证 | ✅ JWT tokenquery 参数或 Authorization header |
| 授权 | ✅ 主对话仅限 admin_ 前缀用户 |
| 传输加密 | ⚠️ 当前为 ws://(明文),生产需 Caddy TLS 升级为 wss:// |
| 消息验证 | ✅ JSON 解析失败时仅记录日志继续 |
| 输入大小限制 | ✅ 最大 65536 字节 |
| 跨域控制 | ⚠️ CheckOrigin 全允许 |
| 速率限制 | ❌ 无 |
---
## 4. 前端构建配置审计
### 4.1 Vite 配置 ([`frontend/web/vite.config.ts`](frontend/web/vite.config.ts:1))
| 配置项 | 状态 | 说明 |
|--------|------|------|
| React 插件 | ✅ | `@vitejs/plugin-react` |
| 路径别名 | ✅ | `@/``./src/` |
| 开发服务器端口 | ✅ | 5173 |
| API 代理 | ✅ | `/api``http://localhost:8080` |
| WebSocket 代理 | ✅ | `/ws``ws://localhost:8080` (ws: true) |
### 4.2 发现的问题
#### 🟡 中等 (P1)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 27 | **缺少 vite-plugin-pwa** | [`frontend/web/package.json`](frontend/web/package.json:1) | 未集成 `vite-plugin-pwa`SW 和 manifest 需要手动维护,无法自动生成 hash-based 缓存策略和 Workbox 集成 |
| 28 | **缺少构建输出配置** | [`frontend/web/vite.config.ts`](frontend/web/vite.config.ts:1) | 未配置 `build.outDir`, `build.assetsDir`, `build.sourcemap` 等生产构建参数 |
#### 🟢 轻微 (P2)
| # | 问题 | 位置 | 说明 |
|---|------|------|------|
| 29 | **缺少环境变量类型声明** | 使用的 `import.meta.env.VITE_WS_URL` 未在 `vite-env.d.ts` 中声明 | TypeScript 可能报类型错误 |
---
## 5. 问题汇总与修复优先级
### 统计
| 严重级别 | 数量 |
|----------|------|
| 🔴 严重 (P0) | 4 |
| 🟡 中等 (P1) | 12 |
| 🟢 轻微 (P2) | 13 |
| **总计** | **29** |
### P0 修复列表(必须立即修复)
| # | 问题 | 修复建议 |
|---|------|----------|
| 1 | Caddyfile 缺失 | 创建 `Caddyfile` 或改用 Nginx/Traefik |
| 2 | `docker-compose.dev.yml` 服务定义顺序 | 将 `iot-debug-service` 移到 `ai-core` 之前 |
| 13 | SW 重复注册 | 移除 `main.tsx` 中的 SW 注册,仅保留 `usePWA.ts``registerServiceWorker()` |
| 20 | Hub.Run() 广播 RLock 下写操作 | 将 `h.mu.RLock()` 改为 `h.mu.Lock()`,或将 close/delete 操作移至单独加写锁的块 |
### P1 修复列表(建议本迭代修复)
- **Docker**: memory-service/tool-engine 添加 HEALTHCHECK、非 root 用户、时区设置、ldflagsRedis 生产密码;voice-service 补全 go.sum
- **PWA**: 添加 Apple meta 标签;SW 缓存资源列表扩展;CACHE_NAME 使用构建时变量
- **WebSocket**: CheckOrigin 限制生产域名;添加消息速率限制;实现 voice_input
- **构建**: 集成 vite-plugin-pwa;添加生产构建配置
### 总体评价
- **Docker 配置**: gateway/ai-core/iot-debug 三个核心服务的 Dockerfile 质量优秀(多阶段构建、非 root 用户、健康检查、二进制 strip、时区设置),但 memory-service 和 tool-engine 需要补齐。最严重的问题是 Caddyfile 缺失导致生产环境无法启动。
- **PWA**: 基础实现扎实,离线页面设计精良,但 SW 重复注册和缺少 Apple meta 标签是两个需要修复的问题。
- **WebSocket**: 架构设计成熟,Hub 模式 + 用户索引 + 会话追踪 + 对话缓存 + IoT 广播功能完整。主要问题是广播循环中的锁使用不正确和缺少速率限制。前端 useWebSocket hook 质量高,包含自动重连和竞态防护。
- **构建配置**: 基础配置合理,但缺少 PWA 插件自动化和生产构建优化。