本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
9.2 KiB
方案A 消息发送延时改造 — E2E 浏览器验证报告
验证日期:2026-07-08 验证方式:真实浏览器端到端实测(系统 Chrome 驱动 H5,Playwright Python) 验证目标:员工端发送消息瞬时返回不阻塞 UI,AI 回复经 WebSocket 打字机流式推送(方案A 核心机制) 结论:✅ 通过 — 发送即时、AI 经 WS 流式渲染、Duckula 头像/名称正常
1. 验真目标(方案A 是什么)
方案A 的核心改造:
- 员工发送消息 → 后端
POST /h5/conversations/current/messages立即返回(ai_reply: null,消息落库status: sent),不再同步等待 AI - AI 回复由
asyncio.create_task(process_h5_ai_reply)异步生成,逐 chunk 通过 WebSocket 广播ai_reply_chunk/ai_reply给 H5 - H5 监听 WS 帧,做打字机流式渲染;WS 断连时降级为 3s 轮询
本次 E2E 要证明的是:UI 不阻塞(发送瞬时) + AI 回复走 WS 流式(非轮询兜底)。
2. 测试环境
| 组件 | 版本/地址 | 说明 |
|---|---|---|
| 前端 H5 | localhost:5174/itdesk/(Vite dev) |
docker-compose.dev.yml frontend-h5 |
| 后端 | localhost:8000(FastAPI, --reload) |
dev 栈,单 worker |
| 浏览器 | 系统 Chrome 150(headless, Playwright 驱动) | 走系统 Chrome,非下载 Chromium |
| AI | Dify app-UaTWYdBSwN6VktKQlbh5YN5H(dev 临时切的可流式 app) |
用于验证 typewriter 主路径 |
| 登录 | Mock 登录 POST /h5/mock-login |
dev 模式免企微 OAuth |
3. 测试步骤
- 打开 H5 登录页
http://localhost:5174/itdesk/ - Mock 登录(employee_id=
E2E_BROWSER,employee_name=浏览器实测) - 在输入框发送「打印机无法连接网络怎么办」
- 计时发送点击返回(验证 UI 不阻塞)
- 监听页面 WebSocket 接收帧,轮询
.chat-panel__messages文本,捕获:- Duckula 头像/名称是否渲染
- AI 回复是否经
ai_reply_chunk流式到达(打字机证据) - 最终 AI 文本长度与真实内容
4. 验证结果(来自 e2e-screenshots/e2e_result.json)
| 指标 | 值 | 判定 |
|---|---|---|
发送点击耗时 send_click_s |
1.257s | ✅ 瞬时(< 3s 阈值) |
发送 UI 不阻塞 send_ui_instant |
true | ✅ |
AI 首屏渲染 ai_first_render_s |
2.27s | ✅ 发送后 2.3s 出现 |
WS 收到 ai_reply_chunk 帧数 |
343 | ✅ 流式打字机路径成立 |
打字机已证实 typewriter_proven |
true | ✅ 非轮询兜底 |
最终消息文本长度 final_text_len |
1469 字符 | ✅ 完整真实 AI 内容 |
含 Duckula 名称 final_has_duckula |
true | ✅ 头像/名称渲染 |
含真实内容 final_has_real_content |
true | ✅("问题描述/打印机/网络") |
文本增长 len_grew |
true | ✅ 流式累积 |
WS 打开总数 ws_open_total |
2(含 1 个 Vite HMR) | ✅ 无握手失败 |
截图证据(真实浏览器会话):
e2e-screenshots/01-login.png— 登录页e2e-screenshots/02-chat-after-login.png— 登录后进会话e2e-screenshots/03-after-send-instant.png— 发送后即时(用户气泡出现,AI 占位)e2e-screenshots/04-typewriter-mid.png— 打字机进行中e2e-screenshots/05-final.png— AI 完整回复(Duckula 头像 + 1469 字)
结论:方案A 在真实浏览器中验证通过 — 发送不阻塞 UI,AI 经 WebSocket 流式打字机渲染。
5. 验证过程中发现并修复的 4 个阻断问题
这些不是方案A 本身的缺陷,而是「dev 容器化栈跑真实浏览器」暴露的环境/代码阻断。其中第 4 条是真实后端 bug,会影响生产 WS。
① H5 应用无法挂载(Vite 资源解析失败)
- 现象:打开
/itdesk/只剩骨架屏,<vite-error-overlay>报错Failed to resolve import "/duckula.webp" - 根因:
MessageBubble.vue/MessageItem.vue引用<img src="/duckula.webp">,该资源在容器镜像烘焙时尚未加入public/,而 dev compose 只挂载了src没挂载public/ - 修复:
docker-compose.dev.ymlfrontend-h5 增加- ./frontend-h5/public:/app/public(与src一致的热更新挂载)
② Mock 登录返回 500(Vite 代理目标错误)
- 现象:浏览器点登录 → 后端 500;但纯 Python 直连
:8000却 200 - 根因:
vite.config.ts代理/api→target: 'http://localhost:8000',但容器内 localhost 不是后端(是容器自己)→ ECONNREFUSED → Vite 返 500 - 修复:代理目标改为可配置
process.env.VITE_PROXY_TARGET || 'http://localhost:8000';dev compose 注入VITE_PROXY_TARGET=http://backend:8000(compose 服务名)。本地非 Docker 开发仍走默认localhost:8000
③ CSP 阻断 dev WebSocket
- 现象:
Connecting to 'ws://localhost:8000/ws/h5/...' violates CSP connect-src(握手前被拦) - 根因:
index.htmlCSPconnect-src仅允许ws://localhost(默认端口 80),但 dev WS 用显式端口 8000,CSP 按端口精确匹配 → 视为不同源被拒 - 修复:
index.htmlCSPconnect-src增加ws://localhost:8000 ws://127.0.0.1:8000
④ ⚠️ WebSocket 握手失败(真实后端 bug,生产相关)
- 现象:CSP 放开后握手仍失败
Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received - 根因:浏览器用子协议
Sec-WebSocket-Protocol: bearer.{token}传递 token,后端ws_manager.connect()/connect_employee()读取该头做认证,但websocket.accept()未回显子协议 → 浏览器严格拒绝握手 - 影响:不仅 dev,生产环境坐席端/员工端 WS 同样会握手失败(之前被 3s 轮询兜底掩盖,导致 AI 回复实际走轮询而非流畅打字机)
- 修复:
backend/app/services/ws_manager.py的connect/connect_employee增加可选subprotocol参数,accept(subprotocol=subprotocol if subprotocol else None);backend/app/api/ws.py两处调用传入subprotocol - 验证:修复后 WS 握手成功,
ai_reply_chunk343 帧正常流式到达
6. 遗留(非阻断)事项
- CSP
font-src缺口:控制台仍有data:font/woff2与at.alicdn.com字体被 CSP 拦截(仅影响字体显示,不影响功能)。如需消除,可在 CSP 增加font-src 'self' data: https://at.alicdn.com。 - 404:一个资源 404(疑似 favicon 或字体文件),无害。
- dev Dify key 临时切换:
docker-compose.dev.yml中 Dify key 为验证 typewriter 主路径临时切到可流式 app,已在注释中标注。✅ 已还原 (2026-07-09) 回app-J3s8sHarZQ2SCaNF3xCppliL(dev 该 app 经 dify2openai 返回空 SSE,本地 typewriter 主路径需改用 backend/.env 工作 app 才能看到流式效果)。
7. 结论
✅ 方案A(员工端消息发送即时返回 + AI 经 WS 打字机流式推送)在真实浏览器端到端验证通过。 发送 1.26s 即时返回、AI 回复 2.3s 起经 343 个 WS chunk 流式渲染、Duckula 头像与完整 1469 字回复正常显示。 同时修复了 1 个真实 WebSocket 握手 bug(生产相关,此前被轮询兜底掩盖)及 3 个 dev 容器化环境阻断,使「Docker dev 栈 + 真实浏览器 E2E」链路从此可用。
8. 生产部署验证 (2026-07-09)
WS 子协议修复(第④条 bug)按既定建议合入 main 并部署到生产服务器
itsupport.servyou.com.cn(10.90.5.110)。
8.1 部署方式
生产后端容器 wecom_it_backend 代码烘焙进镜像(唯一挂载是 uploads),无源码卷挂载。因此采用:
- 将 2 个修复文件打包上传至生产服务器
/tmp/(v2_ops.py upload) docker cp进运行容器:/app/app/services/ws_manager.py、/app/app/api/ws.py- 同步更新宿主机源码
/opt/wecom-it-desk/backend/app/...(供后续镜像重建) docker restart wecom_it_backend- 原文件备份于
/tmp/ws_manager.py.bak、/tmp/ws.py.bak(回滚点)
8.2 部署后验证(真实证据)
| 检查项 | 结果 |
|---|---|
| 容器状态 | Up (healthy) — 重启后健康 |
| 修复代码就位 | ws_manager.py:76、:185 均为 accept(subprotocol=subprotocol if subprotocol else None) |
| 真实 WS 连接(重启后) | 日志显示 WebSocket /ws/h5/tangzhenzhen [accepted] + H5员工 WebSocket 连接建立: employee_id=tangzhenzhen,以及 WebSocket /ws/sxn [accepted] + 坐席连接建立 |
| 后端服务 | GET /conversations ... 200 OK(正常服务流量) |
关键证据:上述 H5 员工(tangzhenzhen)/坐席(sxn) 连接时间戳(01:15:03 / 01:15:06)均在容器重启(09:14:26)之后。用旧 bug 代码,浏览器会因「未回显 subprotocol」直接拒绝握手,连接根本到不了 [accepted]。现在成功建立 = 修复在生产真实生效(旧代码下这些连接本应失败)。
8.3 Git 合并
- 修复 commit
bacd34c(feature/message-reliability) - cherry-pick →
6db1c0e(main),已推送origin/main(6277db3..6db1c0e) - 生产部署与主干合并相互独立:部署用
docker cp(即时生效、重启保留),主干合并保证代码源一致