docs: add round 8 debug report - Docker, PWA, and WebSocket deep audit

This commit is contained in:
2026-05-20 15:31:13 +08:00
parent d71e7b4c83
commit 9c9f54ab9a
@@ -0,0 +1,313 @@
# 第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 插件自动化和生产构建优化。