Files
wecom_it_smart_desk/docs/04-运维文档/部署运维/00-标准故障排查手册.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .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-*/
2026-08-07 22:31:32 +08:00

1029 lines
79 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.
# 00 · 标准故障排查手册
> **版本**: v3.2 | **日期**: 2026-07-29 | **维护人**: 宋献 / Duckula
> **定位**: 所有故障排查前**首先查看本手册**。
> **最新**: v3.2 新增 CASE-20260729-01(坐席接入提示消息重复推送 — 用户要求取消但未生效)。
| v2.9 | 2026-07-28 | 文档规范化整改条目 — REQ-通用-002 部署文档 v1.0 → v1.1;新建 [`00-文档规范化整改记录.md`](./00-文档规范化整改记录.md) 索引;新灰度开关 `QUICK_RULE_ENABLED` 上线;新测试用例 [`TC-通用-002-快速回复规则后台管理.md`](../../03-测试文档/03-功能测试用例/TC-通用-002-快速回复规则后台管理.md) 发布 |
| v2.5 | 2026-07-27 | 新增 CASE-20260727-01H5 选项选中状态效果消失 — Store 状态未绑定 UI)|
| v2.8 | 2026-07-28 | 新增 CASE-20260728-04###CRASHFIX 清理误删路由注册 → 页面 404)|
| v2.7 | 2026-07-28 | 新增 CASE-20260728-01~03(前端".data 层数不匹配"导致加载失败 / API 404 / 本地-远程代码不同步)|
| v2.6 | 2026-07-27 | 新增 CASE-20260727-02(管理后台 el-table 白底白字 — Element Plus 3 层覆盖问题 + global.css 全局修复方案)|
| v3.0 | 2026-07-28 | 新增 CASE-20260728-05QuickRuleResponse Pydantic 序列化失败 — 模型字段未声明 Optional + SQL 初始数据缺 priority 列)|
| v3.1 | 2026-07-28 | 新增 CASE-20260728-06(坐席端缺少"已选:xxx ✓"汇总标签 — 员工最近一次选项选择不可见)|
| v3.2 | 2026-07-29 | 新增 CASE-20260729-01(坐席接入提示消息重复推送 — 用户要求取消但未生效)|
| v2.4 | 2026-07-26 | 新增 CASE-20260726-01~08(选项交互全链路修复)|
| v2.3 | 2026-07-25 | 新增 CASE-20260725-02H5用户端收不到AI回复 — WebSocket推送datetime序列化失败)|
| v2.2 | 2026-07-25 | 新增 CASE-20260725-01H5始终显示坐席在线 — WS断连查询字段不匹配)+ 判定矩阵更新 |
| v2.0 | 2026-07-23 | 新增 CASE-20260723-01H5 AI回复显示 [object Object] — Dify原生API超时回退代理)|
| v1.6 | 2026-07-14 | 新增 CASE-20260714-01(前端 bind mount 未生效导致 403+ 错误码速查更新 |
| v1.7 | 2026-07-14 | 新增 CASE-20260714-02(员工端 /h5/ 404 — nginx 配置错误)|
| v1.9 | 2026-07-16 | 新增 CASE-20260716-01(员工端 /h5/ 404 — nginx 反向代理配置错误)|
| v1.8 | 2026-07-15 | 新增 CASE-20260715-01(员工端审批模板ID不正确 — 企微审批模板失效)|
| v1.7 | 2026-07-14 | 新增 CASE-20260714-01(前端 bind mount 未生效导致 403+ 错误码速查更新 |
| v1.6 | 2026-07-14 | 新增 CASE-20260714-02(员工端 /h5/ 404 — nginx 配置错误)|
| v1.5 | 2026-07-13 | 新增 CASE-20260713-01nginx rewrite 不全导致 502+ CASE-20260713-02@require_role 装饰器参数缺失) |
| v2.0 | 2026-07-23 | 新增 CASE-20260723-01H5 AI回复显示 [object Object] — Dify原生API超时回退代理)|
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
| v3.0 | 2026-07-28 | 新增 CASE-20260728-05QuickRuleResponse Pydantic 序列化失败 — 模型字段未声明 Optional + SQL 初始数据缺 priority 列)|
> **前置阅读**: [运维手册(部署/回滚/备份/应急)](../01-项目总览/01-智能IT服务系统运维手册-20260704.md) · [SOP-04 应急响应](../07-项目管理/SOPs-标准流程/SOP-04-应急响应.md)
---
## 0 文档说明与版本
### 0.1 为什么要有这本手册
原先"故障排查"主题散落在 9 份文档中(500 诊断、服务器端诊断、故障排查指南、4 份修复记录、通讯链路、deploy/02 手册、调试验证指南),内容重复且存在断链。任何故障都应**先翻这一本**,按决策树定位,再查案例库。
### 0.2 ⛔ 验证完成硬规则(最重要)
**宣布"已修复 / 已完成"之前,必须提供真实可验证证据**,不得仅凭 curl / 日志 / "我认为"
- **前端 / 登录类问题**:真实浏览器登录或操作截图(用真实 Chromium / Playwright 打开页面、输入凭据、完成动作、进入目标页的截图)。
- **API / 后端类问题**:端到端调用证据(curl 真实返回 + 必要时代码层拦截响应体)。
- **禁止**:只因 `docker logs` 无报错就断言修复;只因"我认为应该好了"就宣布完成。
### 0.3 版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| v1.0 | 2026-07-07 | 整合 9 份散落文档 + 新增 CASE-20260707-01Redis urlparse 挂起)|
| v2.5 | 2026-07-27 | 新增 CASE-20260727-01H5 选项选中状态效果消失 — Store 状态未绑定 UI)|
| v2.4 | 2026-07-26 | 新增 CASE-20260726-01~08(选项交互全链路修复:Pinia .value / UUID排序 / 编码损坏 / 轮询去重 / 代理坐席选中)|
| v1.1 | 2026-07-08 | 新增 CASE-20260708-01~06 + nginx 急救流程 + 端到端验证更新 |
| v1.2 | 2026-07-10 | 新增 CASE-20260710-01CSP 拦截联 JS+ Step 0 响应头检查 + sed -i inode 教训 |
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
| v1.3 | 2026-07-10 | 新增 CASE-20260710-02(镜像缺文件导致 API 404)+ 判定矩阵扩展 + 部署前同步检查清单 |
| v2.2 | 2026-07-25 | 新增 CASE-20260725-01H5始终显示坐席在线 — WS断连查询字段不匹配)+ 判定矩阵更新 |
| v3.0 | 2026-07-28 | 新增 CASE-20260728-05QuickRuleResponse Pydantic 序列化失败 — 模型字段未声明 Optional + SQL 初始数据缺 priority 列)|
| v3.2 | 2026-07-29 | 新增 CASE-20260729-01(坐席接入提示消息重复推送 — 用户要求取消但未生效)|
---
## 1 快速诊断决策树
### 1.1 三步隔离法(通用)
任何"页面打不开 / 网络连接失败 / 接口无响应 / 422"都先用三步隔离,定位是 nginx、后端、还是依赖(DB / Redis)的问题。
#### Step 0HTTP 响应头检查(页面 200 但功能异常时首先执行)
```bash
# 检查响应头,特别关注 Content-Security-Policy / X-Frame-Options / X-Content-Type-Options
curl -ksI https://itsupport.servyou.com.cn/h5/ | grep -iE 'content-security|x-frame|content-type'
```
**为什么要有 Step 0**:页面返回 HTTP 200、HTML 正常加载,但 JavaScript 不执行——这类问题无法被三步隔离法捕获。根因通常是 CSPContent-Security-Policy)头限制了 `script-src`,导致内联 `<script>` 被浏览器静默拦截。详见 CASE-20260710-01。
```bash
# 第1步:nginx 层可达性(在服务器执行;浏览器走 HTTPS,故用 https 而非 localhost
curl -ksI https://itsupport.servyou.com.cn/itadmin/ | head -5
curl -ksI https://itsupport.servyou.com.cn/api/health | head -5
# 第2步:直连后端(绕过 nginx,确认后端本身)
docker compose exec backend curl -s http://localhost:8000/health
# 或容器外:
docker exec wecom_it_backend curl localhost:8000/health
# 第3步:依赖可达性
docker compose exec redis redis-cli ping # 期望 PONG
docker compose exec postgres pg_isready -U wecom # 期望 accepting
```
**判定矩阵**
| 现象 | 第1步 | 第2步 | 第3步 | 定位 |
|------|------|------|------|------|
| 浏览器"网络连接失败"、curl 永远不返回 | ✅200 | ✅200 | ❌挂起 | **依赖挂起**(如 Redis 连到错误 host|
| 全站 500 | ❌500 | ✅/❌ | — | 后端异常,看 backend 日志 |
| 某端点 502 | ❌502 | ❌后端 down | — | 后端未起 / 缺 `PYTHONPATH=/app` |
| /itdesk/ 200 但 /api/... 404 | ✅ | — | — | nginx 代理路径不匹配 |
| 422 | ✅ | API 校验失败 | — | 请求体缺字段(见 §2)|
| 页面 200 但 JS 不执行 | ✅200 | — | — | **CSP 拦截内联 JS**(检查 `Content-Security-Policy` 头的 `script-src` 是否含 `'unsafe-inline'`,见 Step 0 / CASE-20260710-01|
| 页面 200 但点击菜单/切换 Tab 提示"加载失败"/"请求失败" | ✅200 | ✅200 | — | **前端 `.data` 层数不匹配**(拦截器已返回 inner data,视图层重复取 `.data.items` / `.data.data` → undefined → TypeError,见 CASE-20260728-01|
| 浏览器控制台 404 + "请求的资源不存在" | ✅200 | ✅200 | — | **API 模块导入了不存在的 client**(如 `import { request } from '@/utils/request'` 而项目实际用的是 `apiClient` from `@/api/index.ts`,见 CASE-20260728-02|
| API 404 但本地代码存在 | ✅200 | ❌404 | — | ~~镜像缺文件~~(方案 C 前根因)。卷挂载模式下检查 volume 挂载 + `./app/` 代码完整性(见 CASE-20260710-02 / §1.4|
| DB 状态与 WS 状态不一致(离线仍显示在线) | ✅200 | ✅ | ✅ | WS 断连逻辑中 DB 查询字段不匹配——URL 参数名与 DB 列名不同(如 URL `{agent_id}` 实际是 `user_id`,但代码里用 `Agent.id` 去查)|
### 1.2 关键陷阱:URL 特殊字符导致依赖"静默挂起"
详见案例 **CASE-20260707-01**。密码含 `@` `#` 时,`urlparse` 把它们当 URL 分隔符,连到不存在的 host,连接**无限挂起**(浏览器表现为"网络连接失败",curl 永远等不到返回)。这是最隐蔽的一类故障——容器全 Up、nginx 全 200、唯独业务接口卡死。
### 1.3 在服务器跑诊断的 3 种方式(经堡垒机)
公司服务器只能经堡垒机(`sxn@10.212.189.210:2222``ssh sxn@10.90.5.110`)操作,无法本地 scp。推荐用 jumpserver-V2 工具自动执行:
```powershell
# 本地(Windows)用 jumpserver-V2 跑(自动复用会话,~2-3s/条):
cd C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts
python v2_ops.py exec "docker compose ps"
python v2_ops.py exec "curl -ksI https://itsupport.servyou.com.cn/api/health"
```
> 原"服务器端跑诊断"的 3 种手工方式(PuTTY 跳堡垒机 / scp 上传 / 服务器下载)已不推荐,统一用上述 jumpserver-V2 自动化。
### 1.4 部署前检查清单(卷挂载模式 — 方案 C)
**触发条件**:任何涉及后端代码变更的部署(新增/修改 `.py` 文件、新增 Python 依赖)。
#### ⛔ 硬规则:后端代码部署方式(方案 C 卷挂载,2026-07-10 上线)
| 变更类型 | 正确命令 | ❌ 禁止操作 | 耗时 |
|---------|---------|---------|------|
| `.py` 文件变更 | `cd /opt/wecom-it-desk && docker compose restart backend` | ❌ `docker compose build` | ~15-30 秒 |
| `requirements.txt` 变更 | `docker compose build backend && docker compose up -d backend` | — | ~60-90 秒 |
| `.env` / `docker-compose.yml` 变更 | `docker compose up -d backend` | — | ~10 秒 |
> 代码通过 `./app:/app/app` volume 挂载到容器,不烘焙进镜像。改代码只需 restart 让 uvicorn 重新加载,无需重建镜像。`docker compose build` 只在 Python 依赖变化时才需要。
**背景**:方案 C(卷挂载)已于 2026-07-10 上线。代码不再烘焙进 Docker 镜像,而是通过 `./app:/app/app` volume 挂载到容器。`backend/app/` 旧代码目录已删除。部署前只需验证 `./app/` 代码目录完整性和 volume 挂载状态。
> ⚠️ **历史背景**(已消除):方案 C 前,服务器存在两份代码目录 `app/` 和 `backend/app/`,不同步导致镜像缺文件(见 CASE-20260710-02)。方案 C 消除了此根因。
**检查清单(经堡垒机执行)**
```bash
# 1. 验证 ./app/ 关键文件存在
for f in app/__init__.py app/main.py app/api/auth.py; do
[ -f "/opt/wecom-it-desk/$f" ] && echo "PASS: $f" || echo "FAIL: $f missing"
done
# 2. 验证 volume 挂载正常(容器内可访问代码)
docker exec wecom_it_backend ls /app/app/main.py
# 3. 验证代码一致性(宿主机与容器内 MD5 一致)
HOST=$(md5sum /opt/wecom-it-desk/app/main.py | awk '{print $1}')
CONTAINER=$(docker exec wecom_it_backend md5sum /app/app/main.py | awk '{print $1}')
[ "$HOST" = "$CONTAINER" ] && echo "PASS: code match" || echo "FAIL: code mismatch"
# 4. 如有新依赖,检查 requirements.txt
# diff 本地 requirements.txt 与服务器上的(如需要)
# 5. 代码更新后只需重启(无需重建镜像)
cd /opt/wecom-it-desk && docker compose restart backend
```
**简化版(用 jumpserver-V2 一键检查)**
```bash
python v2_ops.py exec "for f in app/__init__.py app/main.py app/api/auth.py; do [ -f /opt/wecom-it-desk/\$f ] && echo 'PASS: \$f' || echo 'FAIL: \$f'; done"
python v2_ops.py exec "docker exec wecom_it_backend ls /app/app/main.py && echo PASS_mount"
```
> ⚠️ 代码更新后 `docker compose restart` 即可(15-30 秒)。仅当 `requirements.txt` 有变化时才需要 `docker compose build`。
#### 前端部署检查清单(bind mount 模式)
**触发条件**:任何前端文件更新(`frontend-*/dist/` 目录变更)。
```bash
# 1. 验证宿主机源目录有文件
ls -la /opt/wecom-it-desk/frontend-agent/dist/ # 坐席端
ls -la /opt/wecom-it-desk/frontend-h5/dist/ # H5 端
# 2. 验证容器内挂载成功(关键!)
docker exec wecom_it_nginx ls /usr/share/nginx/html/itagent/ # 坐席端
docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/ # H5 端
# 3. dist 更新后**必须重启 nginx 容器**让 bind mount 感知新文件
# 3 种方式(任选其一):
# 轻量(推荐):docker restart wecom_it_nginx # ~3 秒
# 中量:docker compose restart nginx # 同上但更标准
# 重量(极端情况):docker compose stop nginx && docker compose rm -f nginx && docker compose up -d nginx # ~10 秒
# 4. 验证外部可访问
curl -sI https://itsupport.servyou.com.cn/itagent/ | head -3
```
> ⚠️ **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。前端部署后务必验证容器内文件存在(步骤2),否则会 403。
---
## 2 常见错误码速查(E5xx
| 错误码 | 含义 | 首选排查 |
|--------|------|---------|
| **E500** | 后端未捕获异常 / 缺列 / 缺依赖 / 装饰器参数不匹配 | `docker compose logs backend --tail=200 \| grep -i error`;查数据库缺列 / 缺 Python 依赖;检查 `@require_role` 等装饰器是否与函数签名不匹配(见 CASE-20260713-02 |
| **E502** | nginx 连不到后端 / 路由不匹配 | 后端容器 `unhealthy``docker logs wecom_it_backend`;检查 nginx rewrite 是否正确剥离 `/api/` 前缀(见 CASE-20260713-01 |
| **E503** | 服务过载 / 维护 | `docker stats``docker inspect ... Health` |
| **E403** | IP 白名单 / 无权限 / 前端挂载未生效 | `grep allow /opt/wecom-it-desk/nginx/nginx.conf`;admin 角色不足;前端部署后检查 `docker exec wecom_it_nginx ls <挂载路径>` 确认容器内文件存在(见 CASE-20260714-01 |
| **E422** | 请求体校验失败(Pydantic)| 确认必填字段齐全(如登录需 `user_id`+`name`|
| **网络失败 / 连接挂起** | 依赖不可达(最常见 Redis 配置错)| 见 §1.2 / CASE-20260707-01 |
| **页面 200 但 JS 不执行** | CSP 头 `script-src``'unsafe-inline'`,内联 `<script>` 被浏览器静默拦截 | `curl -ksI <URL> \| grep content-security`;检查 `script-src` 是否含 `'unsafe-inline'`(见 CASE-20260710-01|
| **API 404 但本地代码存在** | ~~镜像缺文件~~(方案 C 前根因已消除)。卷挂载模式下检查:volume 是否正常挂载 + `./app/` 是否有该文件。**同时检查前端 API 封装是否导入了正确的 client 模块**(见 CASE-20260728-02| `docker exec <容器> ls /app/app/api/auth.py``docker exec <容器> md5sum /app/app/main.py` 对比宿主机(见 §1.4 / CASE-20260710-02|
| **页面操作时提示"加载失败"/"请求失败"** | 前端 `.data` 层数不匹配——拦截器已返回 inner data,调用方重复取 `.data` → undefined → TypeError → catch 显示"加载失败" | 搜索 `response.data.data` / `res.data.items` / `.data.total` 等双重 `.data` 模式(见 CASE-20260728-01|
| **部署后容器 crash-loop** | 本地-远程代码不同步——覆盖的文件 import 了远程不存在的模块 | 对比远程文件列表 `ls /opt/wecom-it-desk/app/models/*.py`,注释掉缺失模块的 import(见 CASE-20260728-03|
| **页面正常但下拉筛选/筛选查询提示"服务器内部错误"** | **Pydantic 序列化失败**——响应模型字段声明为非 Optional 但 DB 字段为 NULL(见 CASE-20260728-05| 查看后端日志 `docker logs wecom_it_backend \| grep "ValidationError"`SQL 排查 `SELECT * FROM <table> WHERE <nullable_field> IS NULL`;修复响应模型字段类型 + SQL 初始数据 + DB 回填 |
### 2.1 E500 常见根因速查
- 数据库缺列 → `ALTER TABLE ... ADD COLUMN IF NOT EXISTS ...`
- 缺 Python 依赖 → `requirements.txt` 补依赖后重构建(如 `wordfilter`
- 代码签名不匹配(如缺 `current_agent` 参数)→ 修函数签名
- `import aioredis` 与 Python 3.12 冲突 → 改 `redis.asyncio`,设 `PYTHONPATH=/app`
- **Pydantic 序列化失败**(响应模型字段未声明 Optional,但 DB 字段为 NULL)→ 修响应模型 + SQL 初始数据 + DB 回填(见 CASE-20260728-05
### 2.2 E422 登录场景
登录端点 `/api/agents/login` 要求 `user_id`(必填) + `name`(必填);缺字段直接 422。前端 `admin.ts``name: inputUserId` 发送。
---
## 3 诊断脚本与命令
### 3.1 一键系统状态
```bash
#!/bin/bash
echo "==== 容器状态 ===="; docker compose ps
echo "==== 端口 ===="; netstat -tlnp | grep -E "80|443|5432|6379|8000"
echo "==== 前端文件 ===="; ls -la /opt/wecom-it-desk/html/itdesk/ 2>/dev/null | head
echo "==== backend 错误 ===="; docker compose logs --tail=20 backend 2>&1 | grep -i error
echo "==== Redis ===="; docker compose exec redis redis-cli ping
echo "==== PG ===="; docker compose exec postgres pg_isready -U wecom
```
### 3.2 500 错误快速对照
| 现象 | 诊断 |
|------|------|
| `ls .../frontend-h5/dist/` No such file | 部署包未含 dist |
| nginx 容器内 `ls /usr/share/nginx/html/itdesk/` 失败 | 挂载路径错 |
| curl /itdesk/ 返回 500 | 后端代理或 SPA 内部错 |
| /itportal/ 200 但 /itdesk/ 500 | H5 端特定问题 |
| nginx 日志有 `proxy_pass` 错 | 后端未起 / 端口不通 |
| nginx 日志 `rewrite ... cycle` | try_files 死循环,修 nginx 配置 |
### 3.3 通讯链路检查点(用户 ↔ 坐席 ↔ 企微)
- 用户→系统:企微回调 `/wecom/callback``message_router` → 消息入库 → 坐席 WS / 轮询
- 系统→用户:坐席 POST `/conversations/{id}/messages``wecom_service.send_text_message()`errcode=0)→ 用户收到
- 已知风险:非文本消息(图片/文件)不推送;`dev_mode` 跳过企微推送;企微 API 失败静默(仅日志)
- 检查点文件:`wecom_callback.py` / `message_router.py` / `messages.py` / `wecom_service.py` / `ws_manager.py`
### 3.4 WebSocket 失败
```bash
grep -r 'websocket' /opt/wecom-it-desk/nginx/nginx.conf # 需 proxy_http_version 1.1 + Upgrade/Connection
docker logs wecom_it_backend | grep -i websocket
```
---
## 4 案例库(倒序,编号 CASE-YYYYMMDD-序号)
### CASE-20260728-06 · 坐席端缺少"已选:xxx ✓"汇总标签 — 员工最近一次选项选择不可见 ⭐⭐
- **现象**:坐席端查看对话时,员工已经在 H5 端依次选了"卡纸/其他"(第一条 AI 消息)→"提示错误"(第二条)→"错误代码"(第三条)。每条 AI 消息下方的"推荐选项"标签都会在已选条目后面带 ✓ 标记,但**坐席无法一眼看出员工最终选了哪个**。需要逐个气泡阅读"已选 ✓"标记才能拼出完整决策路径。
- **期望**:在**最近一次被选的那条 AI 消息**(即选项区里包含最近一次被选 label 的那条)的"推荐选项"区域**顶部**,加一个明显的"已选:错误代码 ✓"绿色徽章,让坐席一眼看到员工最终选了哪个。前几条历史 AI 消息的选项区不显示这个徽章(保持现状的"✓ 在被选条目后"设计)。
- **根因(v2.1 之前)**
1. `frontend-agent/src/components/chat/MessageBubble.vue` 第 55-65 行(v2.1 版本)只渲染了"推荐选项:卡纸 缺墨 其他"这种列表,对已选项只加 `ai-structured-options__check ✓` 标记
2. 没有"汇总标签"的设计 → 坐席要扫读所有 AI 消息的 ✓ 标记才能拼出员工最终选了哪个
- **诊断**
1. 加载一个员工点过选项的会话(用真实浏览器或 curl 调用 `/api/conversations/{id}/messages``extra_data.options`
2. 坐席端 store `conversationStore.selectedOptionLabels` 应有"最近一次被选"的 label(如"错误代码"
3. 检查 `MessageBubble.vue` 是否渲染了"已选:xxx ✓"汇总徽章
- **修复**v2.2 增量,2026-07-28):
1. **新计算属性 `latestSelectedLabel`**:从 `conversationStore.selectedOptionLabels` 数组取最后一个值(最近一次被选)
2. **新计算属性 `messageHasLatestSelected`**:判断**当前 AI 消息**的 `extra_data.options` 是否包含 `latestSelectedLabel`(避免前几条历史 AI 消息的选项区也显示"已选"徽章)
3. **模板新增"已选:xxx ✓"绿色徽章**`.ai-structured-options__summary`):用 `v-if="messageHasLatestSelected"` 控制,只在最近一次被选的那条 AI 消息显示
4. **CSS 样式**:绿色背景 `#07C160` + 白色文字 + 圆角 12px + `flex-basis: 100%` 占据整行
- **关键代码**`MessageBubble.vue`):
```typescript
// 1. 最近一次被选 label
const latestSelectedLabel = computed<string>(() => {
const labels = conversationStore.selectedOptionLabels
if (!labels || labels.length === 0) return ''
return labels[labels.length - 1]
})
// 2. 当前消息是否包含最近一次被选
const messageHasLatestSelected = computed<boolean>(() => {
if (!latestSelectedLabel.value) return false
const options = props.message.extra_data?.options || []
return options.some((opt: any) => (opt?.label || opt?.value) === latestSelectedLabel.value)
})
```
```vue
<div
v-if="messageHasLatestSelected"
class="ai-structured-options__summary"
>
<span class="ai-structured-options__summary-label">已选:</span>
<span class="ai-structured-options__summary-value">{{ latestSelectedLabel }}</span>
<span class="ai-structured-options__summary-check">✓</span>
</div>
```
- **部署**
1. 本地 `npm run build`(约 5s)→ `Workspace-B-lO49Vu.js`hash 变化)
2. `Compress-Archive` 压缩 dist → `v2_ops.py upload` 到 `/tmp`
3. 服务器 `cd /opt/wecom-it-desk/frontend-agent && mv dist dist.bak.v2.2b && mkdir dist && cd dist && unzip /tmp/dist-agent-option-summary-v2.2b.zip`
4. `docker restart wecom_it_nginx`bind mount 必须 restart
5. 验证:`curl -I https://itsupport.servyou.com.cn/itagent/assets/Workspace-B-lO49Vu.js` → 200 OK
6. 验证新代码:`grep -c "messageHasLatestSelected" /opt/wecom-it-desk/frontend-agent/dist/assets/Workspace-B-lO49Vu.js` → 1
- **真实浏览器验证**
- 坐席端是企微扫码登录,agent-browser 无法脚本模拟扫码
- **请用户人工测试**
1. 企微扫码登录坐席端
2. 找一个员工点过选项的会话(如"打印机"会话)
3. 看最近一次被选的那条 AI 消息(如"提示错误?具体是什么错误代码...")的选项区顶部,是否显示"已选:错误代码 ✓"绿色徽章
4. 前几条历史 AI 消息(如"已刷卡但打不出?"和"其他问题?具体是啥情况呢?")的选项区**不应**显示"已选"徽章
- **预防**
1. **坐席端只读 ≠ 不需要优化**:即使坐席端是只读视图,也需要让坐席一眼看清员工操作路径
2. **设计"汇总徽章"考虑语义准确性**:用 `messageHasLatestSelected` 限定只在"包含最近一次被选 label 的那条 AI 消息"显示,避免前几条历史 AI 消息也显示"已选"造成误导
3. **`flex-basis: 100%` 让徽章独占一行**:避免和"推荐选项:"label 挤在同一行造成视觉混乱
- **⚠️ 教训**
1. **不要在 store 状态变更时改动消息数据**:`selectedOptionLabels` 是**全局**的,不能用它去 mark 整条 AI 消息
2. **条件渲染用 message 级判断**:用 `messageHasLatestSelected` 把"是否显示徽章"的判断**下沉到每条消息**,避免无差别地所有 AI 消息都显示
3. **真实浏览器测试的硬性限制**:企微扫码登录**无法用 agent-browser 模拟**,部署后必须由用户人工扫码验证
### CASE-20260728-05 · Pydantic 序列化失败 — 响应模型字段未声明 Optional + SQL 初始数据缺列 ⭐⭐
- **现象**:管理后台 → 快速回复规则 → 切换到「路由目标」Tab → 点击「业务分类」下拉菜单 → 弹出"服务器内部错误,请稍后重试或联系管理员"。其他 Tabgreeting、routing_prefilter)正常。
- **错误日志**(后端 `docker logs wecom_it_backend`):
```
pydantic_core._pydantic_core.ValidationError: 1 validation error for QuickRuleResponse
priority
Input should be a valid integer [type=int_type, input_value=None, input_type=NoneType]
```
- **根因(三层任一即可造成,必备至少两层)**:
1. **数据层**(SQL 初始数据缺列):`scripts/init_quick_rules.sql` 第 80 行 `routing_target` INSERT **未指定 `priority` 列**DB 中 6 条记录 `priority=NULL`
2. **模型层**Pydantic 字段未声明 Optional):`app/api/admin/quick_rules.py:77` `QuickRuleResponse.priority: int`(非 Optional
3. **中间件层**`catch_errors_and_log` 捕获 Pydantic ValidationError 后统一返回 1005 通用错误,前端无法区分具体类型
- **诊断**
```bash
# 1. 查后端日志确认是 Pydantic 错误
docker logs wecom_it_backend --tail=200 | grep -A5 "ValidationError"
# 2. 查 DB 该字段是否为 NULL
docker exec wecom_it_postgres psql -U wecom -d wecom_it_desk \
-c "SELECT id, rule_type, priority FROM quick_rules WHERE rule_type='routing_target' ORDER BY id;"
# 3. 查 SQL 初始数据是否漏列
grep -n "routing_target" scripts/init_quick_rules.sql | head -10
```
- **修复**3 处):
1. **响应模型**`QuickRuleResponse.priority: int` → `priority: Optional[int] = None`(同时建议为 `response_template` / `extra_data` 等潜在 NULL 字段也补 Optional
2. **SQL 初始数据**:补 `priority` 列 + 6 条记录添加 `0` 值
3. **DB 实时回填**`UPDATE quick_rules SET priority = 0 WHERE priority IS NULL;`
- **隐藏陷阱(排查时发现)**
- 本地 `quick_rules.py` 上线前存在 **3 处遗留语法错误**(2 处 `))` + 1 处 `)` 缺失),AST 校验才能发现
- 容器 `/opt/wecom-it-desk/app/` 是 bind mount 自宿主机,**本地 Windows 路径不直接同步到容器**
- 正确流程:本地 Edit → `python -c "import ast; ast.parse(...)"` 校验 → `v2_ops.py upload` → `cp /tmp/xxx /opt/wecom-it-desk/app/xxx` → `docker restart wecom_it_backend`
- **验证**8/8 通过):
- `rule_type=routing_target` → 6 条
- 6 个分类筛选各 1 条(行政/人力资源/财务/法务/行政-物业/IT服务)
- 不存在的分类 → 0 条(正常空集)
- 后端日志全部 200 OK,无 ValidationError
- **预防**
1. **SQL 初始数据必须显式列出所有列**:不要依赖表结构默认值(业务上 priority=0 是兜底语义,但 DB DEFAULT 0 与 INSERT 时显式 0 是不同语义)
2. **Pydantic 响应模型字段默认加 Optional**:DB 字段允许 NULL 的,模型字段必须 Optional
3. **catch_errors_and_log 应记录异常类型**:建议在 1005 错误响应体中区分 `validation_error` / `database_error` / `unexpected_error`,便于前端分级提示
4. **后端源码上线前必须 AST 静态校验**:防止本地有遗留语法错误导致容器启动失败
- **关联文档**
- BUG 单:`docs/03-测试文档/05-缺陷单/BUG-通用-快速回复规则-路由目标筛选500-002.md`
- 任务说明书:`docs/07-项目管理/任务说明书/任务说明书-131-快速回复规则后台管理.md`v1.3
- **⚠️ 教训**
1. **「数据-模型-中间件」三层任一不一致即可触发 bug**:DB 字段 nullable ≠ 模型字段 Optional ≠ 中间件返回结构
2. **全局 catch 是一把双刃剑**:屏蔽 Pydantic 校验细节 → 错误难以定位;建议至少在日志中保留原始异常类型
3. **上线前必须 AST 校验**:语法错误不会在 IDE 显示,但会导致容器启动失败,本地排查 30 分钟才发现
### CASE-20260728-04 · ###CRASHFIX 清理误删路由注册 → 页面 API 404 ⭐⭐
- **现象**:管理后台某个已有功能页面(如"欢迎与引导")打开/刷新提示"请求的资源不存在"和"加载配置失败"。页面 HTML 正常,但 API 返回 404。nginx 和 backend 均 healthy。
- **根因**:先前为了修复本地-远程代码不同步问题(CASE-20260728-03),用 `###CRASHFIX` 注释了远程缺失模块的 import 和 `include_router`。后续的"清理 ###CRASHFIX"步骤(`sed -i '/^###CRASHFIX/d'`)**连注释带有效代码一并删除了**,导致该路由永远无法注册。`/admin/welcome-config` 等端点返回 404。
- **诊断**
1. `docker exec wecom_it_backend python -c "from app.api.router import api_router; [print(r.path) for r in api_router.routes if 'welcome' in r.path]"` → 空输出
2. `ls /opt/wecom-it-desk/app/api/admin/welcome.py` → 文件不存在(从未上传)
3. `grep -n 'welcome' /opt/wecom-it-desk/app/api/router.py` → 仅有注释,无 import 行
- **修复**
1. 上传缺失的模块文件到服务器:`cp welcome.py → /opt/wecom-it-desk/app/api/admin/welcome.py`
2. 在 `router.py` 中恢复两行:
```
from app.api.admin.welcome import router as welcome_config_router
api_router.include_router(welcome_config_router, tags=["欢迎与引导配置"])
```
3. 重启 backend`docker restart wecom_it_backend`
- **预防**
1. **清理 ###CRASHFIX 前先备份**`cp router.py router.py.bak`
2. **用 `grep -c` 验证清理措施**:确认只删除了注释标记行,`import` + `include_router` 仍存在
3. **CRASHFIX 是技术债**:应尽快上传缺失模块,不应该长期靠注释维持
- **⚠️ 教训**
1. **`sed -i '/^###CRASHFIX/d'` 的杀伤力**:它删除的是"以 ###CRASHFIX 开头的行",但当 import 行被改写为 `###CRASHFIX from app...` 后,整行都被删除,路由永远丢失
2. **清理后的验证步骤不够**:删除后应检查受影响路由是否仍可访问
### CASE-20260728-01 · 前端"加载失败"/"请求失败" — `.data` 层数不匹配 ⭐⭐⭐
- **现象**:管理后台页面(快速回复规则 / 欢迎与引导 / 审计日志等)点击菜单或切换 Tab 时弹出"加载失败"或"请求失败"。页面 HTML 正常加载(HTTP 200),但所有 API 调用在 catch 块中被捕获。
- **根因**`api/index.ts` 响应拦截器已返回 inner data(剥离了 `{code, data, message}` 外层),部分视图层代码仍按旧习惯重复取 `.data` 层:
```typescript
// 错误:拦截器已返回 {greeting: 12, ...},再取 .data → undefined
stats.value = sr.data
rules.value = lr.data.items
// 正确:拦截器返回的就是业务数据本身
stats.value = sr
rules.value = lr.items
```
对于无 `{code}` 包装的 API(如 quick-rules 返回 Pydantic model 直接序列化),拦截器返回整包,`.data` 同样为 undefined。
- **诊断**
1. 浏览器 F12 → Console → 看是否有 `TypeError: Cannot read properties of undefined`
2. F12 → Network → 查看 API 响应内容 → 确认数据结构(有无 `{code, data}` 包装)
3. 搜索代码中 `response.data.data` 或 `res.data.items` 等双重 `.data` 模式
- **修复**:去掉所有多余的 `.data`。7 处修改:
```
quick-rules/index.vue:199 sr.data → sr, lr.data.items → lr.items
quick-rules/audit.vue res.data.items → res.items
quick-rules/GenericRuleList.vue res.data.items → res.items
WelcomeConfig.vue (2处) response.data.data → response
welcome.ts Promise<{data:{code,data}}> → Promise<WelcomeConfig>
```
- **预防**
1. **拦截器已做归一化**`return (res.code !== undefined ? res.data : res)`),调用方不应再假设有 `.data` 层
2. API 模块的 TypeScript 返回类型标注应与拦截器行为一致,不要标 `Promise<{data:{data:{...}}}>`
3. 新增 API 调用时,先 F12 看 Network 响应结构,再决定取数方式
- **⚠️ 教训**
1. **拦截器改了但调用方没同步改** → 这是一个"静默不报错"的 bug`undefined.items` 抛 TypeError 但被 catch 吞了,只显示"加载失败")
2. **后端是否用 `success_response()` 包装不重要**——拦截器已兼容两种格式,关键是不重复取 `.data`
3. **TypeScript 类型标注如果是虚假的**(标明 `{data}` 但实际没有),会误导后续开发者
### CASE-20260728-02 · 前端 API 404 — 导入了不存在的模块 ⭐⭐
- **现象**:管理后台某新功能页面的所有 API 调用返回 404 "请求的资源不存在"。nginx 和 backend 均 healthy,其他页面工作正常。
- **根因**:API 封装文件导入了**不存在的 client 模块**。项目实际 API client 是 `api/index.ts`(导出 `apiClient`),但新写的 `quickRules.ts` 写成了 `import { request } from '@/utils/request'`,而 `@/utils/request` 不存在。(项目构建时用一个空 stub 通过了编译,但运行时返回 undefined)
- **诊断**
1. F12 → Network → 看请求 URL —— 如果路径全是 `/undefined` 或完全不发请求,说明 client 初始化失败
2. 搜索 `import.*from.*request` 看是否引用了不存在的模块
3. 对比其他正常工作的 API 文件(如 `admin.ts`、`welcome.ts`)看正确的 import 是什么
- **修复**
```typescript
// 错误:
import { request } from '@/utils/request'
request.get('/api/admin/quick-rules/...')
// 正确:
import apiClient from './index'
apiClient.get('/admin/quick-rules/...') // apiClient 已有 baseURL='/api'
```
- **预防**:新增 API 模块时,复制现有模块的 import 语句;不要凭空写 `@/utils/request`
- **⚠️ 教训**
1. **构建通过 ≠ 运行时正确**:vite 可能解析到一个空 stub 文件
2. **路径前缀注意**:用项目统一 apiClient 时路径不要再加 `/api/` 前缀
### CASE-20260728-03 · 部署后容器 crash-loop — 本地-远程代码不同步 ⭐⭐⭐
- **现象**:通过 tar.gz 批量上传新代码到服务器后,backend 容器进入 crash-loopRestarting (1))。日志显示 `ModuleNotFoundError: No module named 'app.models.xxx'`。
- **根因**:本地代码仓库包含了远程服务器**不存在的模块**(如 `meetingroom_guide` / `meetingroom_repair` / `device_inventory` / `welcome` / `terminal_binding`)。全量覆盖 `__init__.py` 和 `router.py` 后,这些 import 在远程触发 `ModuleNotFoundError`。
- **诊断**
1. 检查崩溃日志:`docker logs --tail 20 wecom_it_backend`
2. 对比远程和本地文件:`ls /opt/wecom-it-desk/app/models/*.py` vs 本地
3. 找出远程不存在的模块:`grep "^from app\." <file> | while read l; do ... if [ ! -f "...py" ]; then echo "MISSING: $l"; fi; done`
- **修复**
1. 停止容器:`docker stop wecom_it_backend`
2. 注释掉不存在的 import`sed -i 's/^from app.models.xxx/###from app.models.xxx/' <file>`
3. 同时从 `__all__` 列表中移除对应项
4. 启动并验证:`docker start wecom_it_backend`
- **黄金法则(增量部署)**
- ✅ **仅上传新增文件**,不覆盖已在服务器运行的旧文件
- ✅ 如果必须覆盖,**先对比差异**,只合并增量部分
- ❌ **不要用本地代码全量覆盖远程**,除非确认代码完全一致
- **⚠️ 教训**
1. **本地-远程代码不同步是生产事故的首要原因**。应建立定期 `diff` 同步机制
2. **全量覆盖的代价远大于增量合并**
3. **部署前应先在服务器上做文件存在性检查**
### CASE-20260726-01 · H5 白屏 — store 文件注释编码损坏 ⭐⭐⭐
- **现象**:H5 员工端打开即白屏,页面完全无法加载,无任何交互。
- **根因**`conversation.ts` 中一行注释因 Edit 工具写入异常出现编码损坏(`记录用选择`),导致 TypeScript 编译虽通过但运行时语法异常,Vue 应用无法挂载。
- **修复**:删除损坏行,用 Edit 工具重新写入正确注释
- **教训**:(1) Edit 工具大批量修改可能写入异常字符,改后务必 Read/Grep 验证 (2) 构建成功不能保证运行时正常,白屏优先怀疑 JS 运行时错误
### CASE-20260726-02 · H5 AI结构化回复不显示(坐席能看到)⭐⭐⭐
- **现象**:员工端发送消息后,AI 返回 `msg_type: ai_structured` 的带选项回复,坐席能看到,员工端看不到。普通文本正常。
- **根因**`MessageBubble.vue` 中 `ai_structured` 模板用 `displayContent`(来自 `useTypewriter`)。`useTypewriter` 的 `watch(() => fullText)` 捕获的是函数参数闭包值,无法响应 `props.msg.content` 变化。当 Vue 因 `:key` 变化(`ai_thinking_xxx`→UUID)重建组件时,与 typewriter 生命周期不兼容。
- **修复**`ai_structured` 消息改直接用 `msg.content`(绕过 typewriter),`displayContent` 仅用于纯文本打- **教训**`useTypewriter` 设计缺陷——接受 `string` 而非 `Ref<string>` 导致 watch 无法响应变化
### CASE-20260726-03 · H5 白屏(v2)— 未 import ref 就使用 ⭐⭐
- **现象**:新增 `const isConnected = ref(false)` 后 H5 白屏
- **根因**`useH5WebSocket.ts` 中用了 `ref(false)` 但未 `import { ref } from 'vue'`。构建通过但运行时抛 `ReferenceError`
- **修复**:删除未使用的 ref(或补 import)
- **教训**:新增代码前检查 import,不要依赖 Vite 自动导入;`ref` 不是全局变量
### CASE-20260726-04 · ✓选中效果导致 AI 消息消失 — Pinia ref 多写 .value ⭐⭐
- **现象**:添加 `isOptionSelected` 和 ✓ 后,AI 回复消息不渲染(组件崩溃但不白屏)
- **根因**`conversationStore.selectedOptionLabels.value.includes(label)` — Pinia setup store 自动解包返回的 ref`store.selectedOptionLabels` 已是 `string[]`。加 `.value` → `undefined` → `undefined.includes()` 抛 TypeError → 组件静默崩溃
- **修复**:去掉 `.value`,加 try-catch 防护
- **教训**Pinia setup store 外部访问不用 `.value`,内部需要
### CASE-20260726-05 · 员工选项消息延迟出现 — after_message_id UUID 排序 ⭐⭐
- **现象**:员工点击选项后,选项消息"打不了"只在刷新后才出现
- **根因**:轮询 API `after_message_id` 按 UUID 字典序过滤。`handleAiReply` 更新 `lastMessageId` 为 AI 回复 UUID(如 `d415fb07`),员工选项消息 UUID`9daa7c2e`)字典序更小被跳过
- **修复**`handleAiReply` 不再更新 `lastMessageId`——WS 实时推送不走轮询
- **教训**:UUID 排序 ≠ 时间排序;WS 推送消息不应更新轮询游标
### CASE-20260726-06 · 坐席端消息重复 — 轮询缺去重 ⭐
- **现象**:员工点选项后坐席端显示两条相同消息
- **根因**:坐席端轮询直接 `push(...data.items)` 无去重,WS 和轮询同时到达导致重复
- **修复**:轮询中加去重——过滤 `existingIds` + `processedMessageIds`
- **教训**:双通道(WS+轮询)必须有去重
### CASE-20260726-07 · 坐席端 dist 清空 + 构建堆积 ⭐
- **现象**:部署失败后坐席端 dist 为空;原 dist 135 文件 16MB
- **根因**`rm -rf dist/*` 后解压失败(psftp 5MB 上传超限)+ 每次构建不清理旧文件
- **修复**:清理后重建 → 561KB;用 `tar.gz` 替代 `zip`
- **教训**:rm -rf 后必须确认写入成功;构建前清理 dist;psftp 有 ~4MB 限制
### CASE-20260726-08 · 坐席端看不到用户选项选择 ⭐
- **现象**:员工点选项后坐席端看不到选中状态
- **根因**:为去消 H5 端员工选项消息、`_handle_option_select` 去掉了创建+广播,坐席失去可见性
- **修复**:后端新增 `option_selected` WS 广播;坐席端 store `selectedOptionLabels` 追踪
- **教训**:前后端改动必须双向核对影响面
### CASE-20260727-01 · H5 员工端选项选中状态效果消失 ⭐⭐
- **现象**:员工端与 AI 会话中,选中答案状态效果消失(无绿色边框+✓标记)
- **根因**`conversation.ts` store 已定义 `selectedOptionLabels` 状态并实现了更新逻辑(注释写明"用于显示选中样式"),但 `MessageBubble.vue` 组件**从未使用这个状态来渲染选中效果**。这是功能实现不完整,而非之前有后来丢失的 bug。
- **修复**
1. 在 `MessageBubble.vue` 中从 `conversationStore` 解构 `selectedOptionLabels`
2. 为选项按钮添加动态 class 绑定:`:class="{ 'ai-options__btn--selected': selectedOptionLabels.includes(option.label || option.value) }"`
3. 添加 CSS 样式:绿色背景 `rgba(7, 193, 96, 0.2)` + 绿色边框 `#07C160` + ✓ 前缀
- **教训**
1. **Store 定义状态 + UI 渲染必须同步实现**:新增状态时,同时完成 Store 定义、UI 渲染、CSS 样式
2. **部署前后需对比**:构建/部署前记录 hash,部署后验证关键文件是否正确更新
3. **代码审查清单**:提交前检查 Store 状态是否有对应 UI 渲染、CSS 是否覆盖所有交互状态(normal/hover/active/selected/disabled
### CASE-20260729-01 · 坐席接入提示消息重复推送(用户要求取消但未生效)⭐⭐
- **现象**:智能IT服务推送了"坐席正在查看您的信息,请等待处理回复!"消息,用户提到之前多次要求取消该功能但消息仍在发送。
- **根因**:代码中从未实现关闭该功能的逻辑。之前的要求可能被遗忘或未正确部署。
- **诊断**
1. 检查后端 `session_service.py` 第 229-276 行是否有发送通知的代码
2. 检查前端 `conversation.ts` 是否有 `handleAgentConnected` 函数处理 `agent_connected` 消息
3. 检查 `useH5WebSocket.ts` 是否有 `agent_connected` 类型的处理 case
- **修复**2026-07-27 彻底移除):
1. **后端** `session_service.py`:移除第 229-276 行(发送接入通知给员工的 WebSocket 推送逻辑),保留会话状态更新和坐席广播
2. **前端** `conversation.ts`:移除 `handleAgentConnected` 函数定义和导出引用
3. **前端** `useH5WebSocket.ts`:注释掉 `agent_connected` 消息类型的处理 case 块
- **验证**:坐席手动接单后,员工端不再收到"坐席正在查看您的信息"提示消息
- **⚠️ 教训**
1. **功能关闭请求必须验证**:用户要求取消功能后,需验证代码确实已修改且部署生效
2. **前后端需同步修改**:移除功能时后端推送和前端处理逻辑都需要删除
3. **代码删除要彻底**:只注释后端代码不够,前端 WS 处理也要同步移除
### CASE-20260727-02 · 管理后台 el-table 白底白字(用户角色分配/会话审计/坐席绩效等)⭐⭐⭐
- **现象**:管理后台多个表格出现文字看不清/白底白字/偶数行模糊——具体影响:用户角色分配(BUG-通用-001 原始发现)、会话审计、坐席绩效、配置变更历史、运行期日志、满意度评价。
- **根因(3 层覆盖问题)**
1. `.el-table__cell`td 内 div)默认 `background: var(--el-fill-color-light)`(浅色)—— td 上的深色背景会被 cell 覆盖
2. `.el-table-fixed-column--left/--right` 有独立 `background-color: var(--el-table-header-bg-color)`——固定列特殊处理
3. 任何只在 `<td>` 层覆盖的样式都会被 cell 层覆盖回来
- **诊断**
1. F12 查看实际计算样式:cell 的 background 是 `rgb(255,255,255)` 而不是 td 上的深色
2. 看 Element Plus 源码:`el-table__cell` 选择器默认有 `background:var(--el-fill-color-light)`
3. 看是否使用 `fixed="left"` —— 有的话还要覆盖 `el-table-fixed-column--left`
- **修复**(在 `src/frontend-admin/src/styles/global.css` 全局覆盖):
```css
/* tbody 所有单元格(普通 + 固定列)—— 必须在 cell 层覆盖 */
.el-table .el-table__body td,
.el-table .el-table__body td.el-table__cell,
.el-table .el-table__body td.el-table-fixed-column--left,
.el-table .el-table__body td.el-table-fixed-column--right {
background-color: var(--bg-secondary) !important;
color: var(--text-primary);
}
/* 斑马纹行(偶数行) */
.el-table .el-table__row--striped td, ... { background-color: var(--bg-tertiary) !important; }
/* hover 状态 */
.el-table .el-table__body tr:hover > td, ... { background-color: rgba(59, 130, 246, 0.15) !important; }
```
- **3 次踩坑迭代**
1. 第 1 次:只覆盖 `<td>` → 用户反馈"半清半不清"(cell 层仍浅色)
2. 第 2 次:加 `td.el-table__cell` → 用户反馈"偶数行还是白底"(斑马纹 + 固定列未覆盖)
3. 第 3 次:加 `!important` + 4 个 selector(普通 td / cell / fixed-left / fixed-right)→ ✅ 完美修复
- **验证清单**
- [ ] 普通行(无 striped)背景深、文字浅 → 清晰
- [ ] 偶数行(striped)背景更深一档、文字浅 → 清晰
- [ ] 固定列(`fixed="left"` 或 `fixed="right"`)与同行普通列颜色一致
- [ ] hover 时整行变蓝透 → 文字仍可读
- **⚠️ 教训**
1. **不要在视图里加 scoped `:deep()` 覆盖**:治标,且 scoped 选择器优先级不够;3 层覆盖必须全局放 `global.css`
2. **Element Plus 的 cell 层有自己的 background**:必须用 `td.el-table__cell` 选择器显式覆盖
3. **必须用 `!important`**Element Plus 的 `.el-table-fixed-column--left/--right` 优先级很高,不加 `!important` 无法胜出
4. **完整 Skill**`~/.workbuddy/skills/element-plus-dark-table/SKILL.md`
- **关联文档**
- PRD`PRD-REQ-通用-003-管理后台表格可读性-v1.0.md`
- BUG 单:`03-测试文档/05-缺陷单/BUG-通用-用户角色分配表格看不清-001.md`
---
### CASE-20260716-01 · 员工端 /h5/ 404 — nginx 反向代理配置错误 ⭐⭐
- **现象**:企微工作台 → IT智能服务 → `https://itsupport.servyou.com.cn/h5/` 返回 `{"detail":"Not Found"}`FastAPI 404
- **根因**nginx 配置中 `/h5/` 被错误配置为**反向代理**到后端 API(`proxy_pass http://backend_api/`),而不是静态文件服务。后端没有 `/h5/` 路由,返回 404
- **诊断**
1. `docker logs wecom_it_nginx | grep /h5/` → 显示后端返回 404
2. `docker exec wecom_it_nginx cat /etc/nginx/nginx.conf | grep -A5 'location /h5/'` → 发现 `proxy_pass http://backend_api/`
3. 确认 H5 静态文件已正确挂载:`docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/`
- **修复**
1. **docker-compose.yml**:新增 H5 挂载到 `/h5/`
```
- ./frontend-h5/dist:/usr/share/nginx/html/h5:ro
```
2. **nginx/nginx.conf**:将 `/h5/` 从反向代理改为静态文件服务
```
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/index.html;
}
```
3. **部署**:上传配置后重建 nginx 容器
```
cd /opt/wecom-it-desk
docker compose stop nginx && docker compose rm -f nginx && docker compose up -d nginx
```
- **验证**
- 容器内 `curl -sI http://localhost/h5/` → HTTP 200
- 浏览器访问 `https://itsupport.servyou.com.cn/h5/` → 正常显示 H5 页面
- **⚠️ 教训**
1. **H5 前端是静态文件应用**nginx 应配置 `alias` 或 `root` 提供静态文件,而不是 `proxy_pass` 到后端
2. **这是第二次出现同类问题**:上次 CASE-20260714-02 修复后,今天再次出现,可能是上次修复未同步到服务器或配置被覆盖
3. **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。前端部署后务必验证容器内文件存在
4. **建议**:在 nginx 配置中添加注释说明 `/h5/` 是静态文件服务,避免未来误改
### CASE-20260715-01 · 员工端审批"获取审批流程失败,审批模板ID不正确" ⭐⭐
- **现象**:员工端H5点击"IT资产升级"、"设备申请"、"商业软件申请"等审批卡片时,提示"获取审批流程失败,审批模板ID不正确"。
- **根因**(两层代码都需要修复):
1. **前端**`RecommendCard.vue` 中 `APPROVAL_URL_MAP` 的 `asset_upgrade` 映射到企微审批模板 `Bs7ucTGs...`(已失效),`商业软件申请` 映射到企微审批模板 `3TmACf8D...`(也已失效),第298-299行硬编码 fallback 跳转到失效的企微审批URL
2. **后端**`backend/app/api/approval.py` 中 `APPROVAL_TEMPLATES` 字典的 `asset_upgrade` 和 `software_service` 同样映射到失效的企微审批模板(第81行和第115行)
- **诊断**
1. 浏览器F12查看网络请求,确认调用的是企微审批模板URL
2. 检查前端 `RecommendCard.vue` 和后端 `approval.py` 中的URL映射
3. 对比 `ApprovalCardModal.vue` 中的正确映射(已改用ITSM
- **修复**
1. **前端**`frontend-h5/src/components/assistant/RecommendCard.vue`
- 将 `asset_upgrade` 改为ITSM工单系统URL
- 将 `商业软件申请` 改为ITSM工单系统URL
- 修改 `handleActionClick()` 的fallback逻辑,移除硬编码的失效URL
- 构建:`npm run build` → 部署到 `/opt/wecom-it-desk/frontend-h5/dist/`
2. **后端**`backend/app/api/approval.py`
- 将 `asset_upgrade` 的 `url` 改为ITSM工单系统URL`location` 改为"运维平台"
- 将 `software_service` 的 `url` 改为ITSM工单系统URL`location` 改为"运维平台"
- 上传到服务器:`/opt/wecom-it-desk/app/api/approval.py`
- 重启后端:`docker compose restart backend`
3. 部署:`docker exec wecom_it_nginx nginx -s reload`
- **验证**:浏览器访问H5,点击审批卡片,跳转到ITSM工单系统而非企微审批(已失效模板)
- **⚠️ 教训**
1. **审批入口有两层**:左侧"企微-审批"入口数据来自后端API `/approval/links`,AI推荐卡片来自前端代码,两者都需要修复
2. **企微审批模板会失效**:审批模板在企微后台可能被删除或变更,前端不应硬编码模板ID
3. **ITSM是更稳定的方案**:ITSM工单系统URL更稳定,不受企微审批模板变更影响
4. **推荐做法**:所有审批类型统一使用ITSM工单系统,前后端都要同步修改
### CASE-20260725-02 · H5用户端收不到AI回复(坐席能看到)⭐⭐
- **现象**:用户(H5端)发送消息后,AI回复了,坐席端能看到,但用户端看不到。数据库中消息已落库。
- **根因**WebSocket推送时,数据中包含 `datetime` 对象,导致 JSON 序列化失败:`Object of type datetime is not JSON serializable`。推送失败后连接被清理,用户端收不到实时推送。
- **诊断**
1. 查看后端日志:`docker logs wecom_it_backend | grep "datetime is not JSON serializable"`
2. 确认数据库有消息:`SELECT * FROM messages WHERE content LIKE '%打印机%'`
3. 确认坐席端能收到(坐席用轮询,不依赖WebSocket)
- **修复**:修改 `app/services/ws_manager.py` 的 `send_to_employee` 方法,添加 datetime 预处理函数:
```python
def convert_datetime(obj):
if isinstance(obj, dt.datetime):
return obj.isoformat()
elif isinstance(obj, dict):
return {k: convert_datetime(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [convert_datetime(i) for i in obj]
return obj
data = convert_datetime(data)
```
- **验证**H5发送"打印机",确认AI回复能实时显示在用户端
- **⚠️ 教训**
1. WebSocket推送使用 `send_json()`,数据中的 datetime 对象无法自动序列化
2. 历史错误 `datetime is not JSON serializable` 说明此问题之前也发生过,需要全局检查所有推送数据
3. 坐席端使用轮询(HTTP),不依赖WebSocket,所以不受影响;用户端依赖WebSocket,会受影响
### CASE-20260725-01 · H5 端始终显示"坐席在线"(坐席已离线)⭐⭐
- **现象**:坐席关闭浏览器窗口后,H5 员工端始终显示"🟢 坐席在线",不因坐席离线而更新。API `/h5/agents/online-status` 返回 `{"online":true}`。
- **误判历程**(3 轮,供反思):
1. 最初坐席在线状态是硬编码 `true`,以为只需新增 API 查询 DB 即可
2. 新增 API + 30 秒轮询后仍然显示在线,发现坐席关闭浏览器后 DB 状态从未更新
3. 在 `ws.py` 的 `finally` 块中添加 DB 更新逻辑 → 部署 → 仍无效。后端日志显示 `[坐席离线处理] 未找到坐席`,但 `finally` 块确实执行了
4. **真正根因**WS URL 参数 `{agent_id}` 在代码中叫 `agent_id`,但其值际是企微 `user_id`(如 `sxn`),而 DB 主键 `Agent.id` 是 `agent-sxn-001`。代码错误使用 `Agent.id == agent_id` → 每次查 `agent-sxn-001 == sxn` → 永远不相等 → 从未 COMMIT
- **根因**WebSocket URL 路径参数 `{agent_id}` 命名有误导性——其值为企微 `user_id`,不是 DB `Agent.id`UUID 格式)
| 字段 | 值 | 说明 |
|------|-----|------|
| `Agent.id` | `agent-sxn-001` | DB 主键(UUID 格式) |
| `Agent.user_id` | `sxn` | 企微 userid |
| URL 参数 `{agent_id}` | `sxn` | **实际值是 user_id** |
- **诊断**
1. 检查 DB`SELECT user_id, status, updated_at FROM agents;` → status 长时间为 `online`updated_at 不动
2. 检查后端日志:`docker logs wecom_it_backend | grep "离线处理"` → 看到"未找到坐席"
3. 确认代码:`grep -n "Agent.id == agent_id\|Agent.user_id == agent_id" ws.py` → 发现用错字段
- **修复**`ws.py` 第 225 行:`Agent.id == agent_id` → `Agent.user_id == agent_id`
- **验证**
- 坐席登录 → 手动切在线 → 关闭浏览器
- 等 10 秒 → `curl https://itsupport.servyou.com.cn/h5/agents/online-status` → `{"online":false}`
- **⚠️ 教训**
1. **URL 参数名可能与 DB 列名不一致**:不要假设 `/ws/{agent_id}` 就对应 `Agent.id`,需核对实际传递的值
2. **"查不到"比"报错"更隐蔽**`select().where()` 查不到只是返回 None,不抛异常,日志显示"未找到"但不易引起警觉
3. **部署后必须验证 DB 状态变更**:不仅要看日志有没有执行,还要确认 COMMIT 是否真的发生——用 `SELECT` 验证最直接
### CASE-20260724-01 · H5选项选择消息重复 ⭐⭐
- **现象**:员工端H5点击AI提供的选项后,出现两条相同的员工消息("检查同步设置"等)
- **时间特征**:一条立即出现,另一条约3秒后出现
- **根因**
1. 前端点击选项时:本地立即添加消息(message_id = `option_select_${timestamp}`
2. 后端收到后:存储到数据库(message_id = UUID,与前端不同)+ 广播给前端
3. 前端收到后端广播的消息时,因为 message_id 不同,去重检查失效,导致重复添加
- **诊断**
1. 观察消息出现时间:一条立即显示,另一条约3秒后显示(轮询间隔)
2. 坐席端只看到一条(说明后端只存储了一条,问题在前端显示)
3. 检查前端代码:`sendOptionSelect` 函数中有本地添加消息的逻辑
- **修复**
修改 `frontend-h5/src/stores/conversation.ts` 中的 `sendOptionSelect` 函数:
- 移除本地立即添加消息的代码
- 只发 WS 给后端,等后端存储后通过轮询/广播回来再添加
- 确保消息来源唯一,避免重复
- **验证**:H5点击选项,确认只显示一条消息
- **关联文档**
- 任务说明书:`07-项目管理/任务说明书/任务说明书-125-H5选项交互消息重复处理.md`
- **⚠️ 教训**
1. **前端本地添加的消息与后端存储的消息 message_id 不同**,去重机制基于 message_id 会失效
2. **WS 广播和轮询都可能导致重复**:需要统一消息来源
3. **解决方案**:前端不立即添加消息,依赖后端回传后添加
---
### CASE-20260723-01 · H5 AI回复显示 [object Object] ⭐⭐
- **现象**:员工端H5发送「打印机坏了」或「无法刷卡打印」,AI回复显示 `[object Object]` 而非正常文本
- **根因**Dify 原生 API 超时(默认12秒太短),回退到有 bug 的 dify2openai 代理,代理将 JSON 响应序列化为字符串 `[object Object]`
- **诊断**
1. 查看后端日志:`docker logs wecom_it_backend | grep -E '原生|超时|object'`
2. 查找日志特征:`Dify 原生 API 超时,将回退到代理路径` → `Dify 返回非 JSON 格式,降级为纯文本。content=[object Object]`
3. 检查容器环境变量:`docker exec wecom_it_backend env | grep DIFY_NATIVE_TIMEOUT`
- **修复**
1. 在服务器 `/opt/wecom-it-desk/.env` 中添加/更新:
```
DIFY_NATIVE_TIMEOUT=25
DIFY_PROXY_TIMEOUT=20
```
2. 重新创建后端容器使配置生效:`docker compose up -d backend`
3. 验证容器内环境变量已更新:`docker exec wecom_it_backend env | grep DIFY_NATIVE_TIMEOUT`
- **验证**:H5 发送「打印机坏了」,确认返回正常文本(而非 [object Object]
- **⚠️ 教训**
1. **修改 config.py 默认值 ≠ 配置生效**:必须同步更新服务器 `.env` 文件
2. **docker-compose environment 必须显式声明每个变量**(.env 不自动注入)
3. **Dify 原生直连可绕过 [object Object] bug**:优先使用原生 API,避免经过有 bug 的 dify2openai 代理
4. **超时时间要足够**:Dify 在高峰期响应慢,25秒可避免频繁回退
### CASE-20260714-01 · 坐席端 403 错误 — 前端 bind mount 未生效 ⭐⭐
- **现象**:坐席端 `/itagent/` 返回 403 Forbidden,浏览器控制台显示 `Failed to load resource: the server responded with a status of 403`。
- **根因**:前端文件虽然上传到服务器 `/opt/wecom-it-desk/frontend-agent/dist/`,但 nginx 容器的 bind mount 没有正确工作,容器内 `/usr/share/nginx/html/itagent/` 目录为空。
- **诊断**
1. `docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itagent/` → 容器内目录为空
2. `docker inspect wecom_it_nginx | grep itagent` → 挂载配置存在且正确(`Source: /opt/wecom-it-desk/frontend-agent/dist`
3. `ls -la /opt/wecom-it-desk/frontend-agent/dist/` → 宿主机源目录有文件
4. nginx 日志显示 `directory index of "/usr/share/nginx/html/itagent/" is forbidden`
- **修复**
```bash
# 重建 nginx 容器让 bind mount 生效
cd /opt/wecom-it-desk
docker compose stop nginx
docker compose rm -f nginx
docker compose up -d nginx
```
- **验证**`docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itagent/` → 容器内文件存在;外部访问 `/itagent/` → HTTP 200
- **⚠️ 教训**
1. **bind mount 有时不生效**:即使 docker-compose.yml 配置正确,挂载也可能失效。重建容器是常用解决方法。
2. **容器内目录为空但宿主机有文件 ≠ 挂载成功**:必须进入容器内验证,不能只看宿主机。
3. **前端部署后务必验证容器内文件**:用 `docker exec <容器> ls <挂载路径>` 确认。
4. **此问题可能复发**:任何前端部署(dist 目录更新)后,建议重建 nginx 容器确保挂载生效。
- **复发记录**
- 2026-07-16:再次发生,返回 500rewrite 循环 `internal redirection cycle while internally redirecting to "/itage"`),重建 nginx 容器修复
### CASE-20260714-02 · 员工端 /h5/ 404 — nginx 配置错误 ⭐⭐
- **现象**:员工端打开报错 `Failed to load resource: the server responded with a status of 404 (Not Found)`。
- **根因**nginx 配置中 `/h5/` 被错误配置为**反向代理**到后端(`proxy_pass http://backend_api/`),而不是静态文件服务。后端没有 `/h5/` 路由,导致返回 404。
- **诊断**
1. `docker logs wecom_it_nginx | grep /h5/` → 显示 `GET /h5/ HTTP/1.1" 404`
2. `docker exec wecom_it_nginx cat /etc/nginx/nginx.conf | grep -A10 'location /h5/'` → 发现 `proxy_pass http://backend_api/`
3. 确认静态文件已正确挂载:`docker exec wecom_it_nginx ls /usr/share/nginx/html/h5/`
- **修复**:修改 `nginx/nginx.conf`,将 `/h5/` 从反向代理改为静态文件服务:
```
location /h5/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri /h5/index.html;
}
```
部署:`docker compose restart nginx`
- **验证**:浏览器访问 `https://itsupport.servyou.com.cn/h5/` → HTTP 200,显示"仅限企业微信访问"页面
- **⚠️ 教训**
1. **H5 前端是静态文件应用**nginx 应配置 `alias` 或 `root` 提供静态文件,而不是 `proxy_pass` 到后端。
2. **本地 nginx.conf 与服务器不同步**:发现问题后,检查本地配置是否已修复(本次修复已同步到本地 `nginx/nginx.conf`)。
3. **常见混淆**`/itdesk/` 和 `/h5/` 都指向 H5 应用,但只有 `/itdesk/` 是在企微工作台中配置的入口。两者都应配置为静态文件服务。
### CASE-20260713-02 · 坐席端 502 错误 — @require_role 装饰器参数缺失 ⭐⭐
- **现象**:坐席端 `/itagent/` 全部 API 返回 502,后端日志显示 `TypeError: list_agents() got an unexpected keyword argument 'current_user'`。
- **根因**`@require_role` 装饰器在调用被装饰函数时强制传递 `current_user` 参数,但部分使用该装饰器的函数未声明此参数。
- **诊断**`docker logs wecom_it_backend | grep -i error` 查看具体错误;定位缺少参数的函数。
- **修复**
1. `backend/app/api/agents.py` 第 425 行:添加 `current_user: UserInfo = Depends(get_current_user)`
2. `backend/app/api/otp.py` 第 348 行和第 389 行:同样添加 `current_user` 参数
3. 部署:`docker compose restart backend`
- **验证**:后端 `/health` → 200 OK`/agents` → 200 OK
- **⚠️ 教训**
1. **所有使用 `@require_role` 装饰器的函数都必须声明 `current_user` 参数**,装饰器会自动注入此参数。
2. 排查 502 错误时,先检查后端日志中的 `TypeError` 错误,可能是装饰器参数不匹配。
3. 建议在代码中添加类型注解和 lint 规则,提前发现此类问题。
### CASE-20260713-01 · 坐席端 502 错误 — nginx rewrite 规则不完整 ⭐⭐
- **现象**:部分 API(如 `/api/auth/qrcode`)返回 502,但 `/api/agents` 返回 200。
- **根因**nginx 配置只对 `/api/admin/` 路径做 rewrite 剥离前缀,其他路径(如 `/api/auth/`)未处理,导致后端收到 `/api/auth/qrcode` 而非 `/auth/qrcode`,路由不匹配返回 404。
- **诊断**
1. `docker logs wecom_it_nginx | grep 502` 查看哪些路径返回 502
2. `docker exec wecom_it_backend curl http://localhost:8000/auth/qrcode` 直接测试后端(绕过 nginx
3. 检查 nginx 配置 `location /api/` 是否正确 rewrite
- **修复**:修改 `nginx/nginx.conf`,对所有 `/api/` 路径统一做 rewrite
```
location /api/ {
# 剥离 /api/ 前缀,使后端收到 /auth/... 而非 /api/auth/...
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend_api;
...
}
```
部署:`docker restart wecom_it_nginx`
- **验证**:外部访问 `/api/auth/qrcode` → 200 OK
- **⚠️ 教训**
1. **nginx rewrite 规则必须覆盖所有需要剥离前缀的路径**,不能只处理特定路径(如 `/api/admin/`)。
2. 判定矩阵更新:`/xxx/ 200 但 /api/... 404` → 检查 nginx 是否正确 rewrite。
3. 部署新功能后,务必用真实浏览器测试所有关键路径,不能只测"看起来正常"的端点。
### CASE-20260710-02 · 坐席端/管理端登录"获取二维码失败"(Docker 镜像缺文件)⭐⭐
- **现象**:坐席端和管理端登录均报"获取二维码失败",浏览器控制台显示 `WebSocket connection failed` + `/api/auth/qrcode` 返回 404。
- **误判历程**
1. 误判为 Nginx 未 reload → `nginx -s reload` 后 `/auth_qrcode/create` 返回 200,但前端实际调用的是 `/auth/qrcode`(不同路由)
2. 尝试 `docker cp` 临时复制 `auth.py` 到容器 → 重启后文件丢失(临时文件系统)
3. 发现连锁依赖:`auth.py` 依赖 `schemas/auth.py` → `services/auth_service.py` → `models/login_log.py`,全部缺失
4. **真正根因**:服务器存在两份代码——`/opt/wecom-it-desk/app/`(较新,开发用)和 `/opt/wecom-it-desk/backend/app/`(较旧,Docker 构建用)。新增的 `auth.py` 只放到了 `app/`,未同步到 `backend/app/`,导致镜像构建时打包的是旧代码。
- **根因**:服务器上 `app/`(开发目录)与 `backend/app/`Docker 构建目录)不同步,`docker compose build` 打包了旧代码。同时 `requirements.txt` 也未同步(新增 `neo4j` 依赖),首次重建后容器因缺包 crash。
- **修复**
1. 用 Python 脚本同步 `/opt/wecom-it-desk/app/` → `/opt/wecom-it-desk/backend/app/`
2. 上传最新 `requirements.txt`(含 `neo4j>=5.26.0`)到 `/opt/wecom-it-desk/backend/`
3. `docker compose build backend`(约 65s
4. `docker compose up -d backend`
5. 验证:后端日志显示 `GET /auth/qrcode → 200 OK`,容器状态 healthy
- **⚠️ 教训**
1. **部署新功能时必须检查两份代码一致性**:`app/` 和 `backend/app/` 不同步 = 镜像缺文件。
2. **`docker cp` 是临时方案**:容器重启后丢失。永久修复必须重建镜像。
3. **全链路检查**:代码同步 → requirements 更新 → 镜像重建 → 容器重启,四步缺一不可。
- **防护措施**:见 §1.4 部署前检查清单。
- **✅ 根因已消除**2026-07-10):方案 C(卷挂载)已上线。代码不再烘焙进 Docker 镜像,通过 `./app:/app/app` volume 挂载到容器。`backend/app/` 旧代码目录已删除,不再存在"两份代码不同步"问题。此案例保留作为历史参考。
### CASE-20260710-01 · 扫码登录成功页 JS 不执行(CSP 拦截内联脚本)⭐⭐
- **现象**:企微扫码登录成功后,"登录成功"页面显示但不会自动关闭。页面停在"JS加载中…",所有内联 `<script>` 从未执行。后端返回 HTTP 200,HTML 内容正确。
- **误判历程**(5 轮,供反思):
1. 误判为未引入企微 JS-SDK → 添加后仍不工作
2. 误判为 JSAPI 签名 URL 协议不一致(HTTP vs HTTPS)→ 修复 `X-Forwarded-Proto` 后仍不工作
3. 误判为外部 JS-SDK 加载失败 → 移除外部依赖改用 `WeixinJSBridge` 仍不工作
4. 误判为 `WeixinJSBridgeReady` 事件未触发 → 改为轮询检测仍不工作
5. **真正根因**Nginx CSP 头 `script-src 'self' 'unsafe-eval' https://res.wx.qq.com` 缺少 `'unsafe-inline'`,所有内联 `<script>` 被浏览器静默拦截。**JS 从未执行过**——前面 4 轮的代码修改全部白费。
- **根因**`Content-Security-Policy` 的 `script-src` 指令未包含 `'unsafe-inline'`,浏览器根据 CSP 规范静默拦截所有内联脚本(`<script>...</script>` 和 `onclick=` 等内联事件处理器)。页面 HTML 正常返回,但 JS 引擎从未解析执行任何脚本。
- **修复**Nginx 配置 CSP 头 `script-src` 加入 `'unsafe-inline'`
```
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://res.wx.qq.com; ..." always;
```
修改后 `docker restart wecom_it_nginx`(不能用 `reload`,见下方教训)。
- **验证**:用户手机企微扫码 → 显示"登录成功"→ 1 秒后自动关闭返回企微 ✅
- **⚠️ 教训**
1. **页面 200 + HTML 正确 ≠ JS 会执行**。排查前端功能异常时,Step 0 应检查 HTTP 响应头(CSP / X-Frame-Options),而非直接深入代码逻辑。
2. **CSP 拦截是静默的**——浏览器控制台报错但页面无任何错误提示,后端日志也完全正常。如果不主动检查响应头,会一直在代码层面打转。
3. **5 轮误判的共同盲点**:每次都在"JS 代码为什么不对"上做文章,从未怀疑"JS 根本没执行"。正确的排查路径应该是先在浏览器 F12 控制台输入 `1+1` 确认 JS 引擎可用,再看 Console 是否有 CSP 报错。
### CASE-20260707-01 · 管理后台登录"网络连接失败"(Redis 密码 URL 解析挂起)⭐
- **现象**:浏览器登录 `/itadmin/` 一直转圈 / "网络连接失败"API 永远不返回;curl 超时。
- **根因**`REDIS_URL=redis://:R3d!s@2026#Secure@redis:6379/0`,密码含 `@` 和 `#`。`urlparse()` 把 `#` 当 fragment、`@` 当 host 分隔符 → 解析出 host=`2026`、password=`R3d!s` → 连到不存在的 host → **无限挂起**。后端 `token_service.create_token()` 调 `redis.setex` 时卡死。
- **修复**
1. `docker-compose.yml` 后端 `REDIS_URL` 改为 URL-encoded`redis://:R3d%21s%402026%23Secure@redis:6379/0`
2. `backend/app/config.py` 的 `create_redis_client` 增加 `unquote()` 解码 + `socket_connect_timeout=5` / `socket_timeout=5`
3. redis 服务 `--requirepass` 与 healthcheck **保持明文** `R3d!s@2026#Secure`(与后端解码后的明文一致)
4. 重建 backend + redis 容器
- **验证**Redis `PING→PONG``curl` 登录 `/api/agents/login` 返回 `HTTP 200, 0.64s, role:admin`;**真实浏览器登录截图进入 dashboard 成功**(见 §5)。
- **⚠️ 同类复发防护**:本项目 Redis 密码含特殊字符,**改 docker-compose 密码时两处必须一致**(后端 `REDIS_URL` 用 encodedredis `--requirepass` 用明文);且 `config.py` 必须 `unquote`。
### CASE-20260705-01 · 502 Bad Gateway(后端启动失败 / aioredis + PYTHONPATH
- **现象**:坐席端登录失败 `502`,后端容器 `unhealthy`。
- **根因**:旧镜像 `import aioredis` 与 Python 3.12 冲突(`TypeError: duplicate base class TimeoutError`);且未设 `PYTHONPATH=/app` 致 `ModuleNotFoundError: No module named 'app.core'`。
- **修复**Dockerfile 改 `import redis.asyncio as aioredis``docker-compose.yml` 设 `PYTHONPATH=/app`;重建后端。
### CASE-20260705-02 · 坐席端 4 个问题(消息列表 500 / 页面抖动 / 发送失败 / 文档缺失)
- #1 消息列表 500`list_messages()` 缺 `current_agent: Agent = Depends(get_current_agent)` 参数。修 `messages.py` + 重启。
- #2 页面短暂不可用:容器重启波动,自愈。
- #3 发送失败 `ModuleNotFoundError: wordfilter``requirements.txt` 缺 `wordfilter==0.2.7`,容器内 `pip install` 临时修 + 同步 requirements。
- #4 文档补"Python 依赖管理"章节(服务器部署手册)。
### CASE-20260613-01 · H5 消息 500(缺列 + AIHandler 签名)
- **现象**`POST /api/h5/.../messages` 500`column conversations.impact_scope does not exist` + `AIHandler.__init__() missing 'ai_service'`。
- **根因**DB 缺 4 列(`impact_scope`/`is_blocking`/`emotion_state`/`dify_conversation_id`);`dependencies.py` 两处 `AIHandler()` 未传 `ai_service`。
- **修复**`ALTER TABLE` 补列;`dependencies.py` 改 `AIHandler(ai_service=AIService())`。
### 附:企微工作台"加载失败 / 无限加载"2026-07-04
- 根因1nginx 未正确挂载 `nginx.conf` → API 404,重建 nginx 容器。
- 根因2:后端 `h5.py` 存在 `NameError: _require_wework_ua` → 代码未同步最新,复制最新 `h5.py` + 重启。
---
### CASE-20260708-01 · 后端路由 404 — OTP 统一路由未注册 ⭐
- **现象**`POST /api/auth/otp-bind` 返回 404;前端 OTP 绑定面板密钥和二维码不显示。
- **根因**:部署 `otp.py` 时漏部署 `router.py``api_router.include_router(otp_router)` 未执行。本地 `router.py` 包含服务器不存在的模块(`knowledge_iteration` / `approval_queue` / `vision` / `ragflow_ingestion` / `automation`),导入失败导致整个 `router.py` 加载失败。
- **修复**:上传 `router.py`,注释掉服务器上不存在的模块导入;同时修复 `otp.py` 中 `@require_role("admin")` 装饰器与显式 `current_user` 参数的冲突(服务器旧版 `require_role` 会自动追加 `current_user`)。
- **教训**:修改路由时务必同步部署 `router.py`,否则新端点虽然代码存在但永远不会注册。
### CASE-20260708-02 · 管理端 API 全部 404 — nginx 正则 location proxy_pass 缺 rewrite
- **现象**`/api/admin/roles`、`/api/admin/dashboard/overview` 等全部返回 404。
- **根因**nginx 配置中 `location ~ ^/api/admin/`(正则匹配)内 `proxy_pass http://backend_api;` 不带尾部斜杠,导致 `/api/` 前缀未剥离,后端收到 `/api/admin/roles` 而非 `/admin/roles`。带尾部斜杠又会报错 `"proxy_pass" cannot have URI part in location given by regular expression`。
- **修复**:去掉嵌套正则 location,改为统一 `location /api/` → `proxy_pass http://backend_api/;`(尾部斜杠剥离 /api/ 前缀)。
- **教训**nginx 中正则 location 不能直接用 `proxy_pass` 带 URI;需要时用 `rewrite` 剥离前缀。
### CASE-20260708-03 · sxn 扫码登录无 admin 权限 — QR 扫描写死 `roles=["agent"]`
- **现象**:管理后台扫码登录后,OTP 管理/角色管理返回 403/无权限提示。`sxn` 账密登录正常。
- **根因**`auth_qrcode.py` 扫码自动确认逻辑中,`create_token` 写死了 `roles=["agent"]`,未调用 `get_user_roles()`。
- **修复**:扫码确认改为调用 `RoleMappingService.get_user_roles()` 获取真实角色。
- **教训**:所有登录路径(账密/扫码/OAuth)的角色获取必须统一走 `get_user_roles()`。
### CASE-20260708-04 · 用户角色列表为空 — `get_user_roles()` 只查 `user_roles` 表
- **现象**:管理后台角色管理页"用户角色分配"表格为空;OTP 管理 API 403。
- **根因**`get_user_roles()` 仅查询 `user_roles` 表,但旧数据(包括 sxn 的 admin)只存在于 `agents.role` 字段。`user_roles` 表为空时返回 `["user"]`。
- **修复**`get_user_roles()` 增加 `agents.role` 回退查询;新增 `GET /admin/roles/user-roles` 端点;前端 `Roles.vue` 加载用户角色分配数据。
- **教训**:新旧数据迁移时需确保角色数据完整同步到 `user_roles` 表。
### CASE-20260708-05 · 企微工作台点"IT支持服务"进坐席登录页 — 员工端口缺失
- **现象**:企微工作台 → IT支持服务 → 显示坐席扫码登录页,而非员工 H5 页面。
- **根因**(1) nginx `/itdesk/` 被错误配置为 301 重定向到 `/itagent/`;(2) H5 构建文件未挂载到容器(`docker-compose.yml` 缺少 `./html/h5` 挂载);(3) 改重定向到 `/h5/` 后仍失败,因为 H5 `vite base` 是 `/itdesk/`OAuth 回调依赖此路径。
- **修复**`/itdesk/` 直接 alias 到 H5 构建目录(`/usr/share/nginx/html/h5/`),不再 301 跳转;`docker-compose.yml` 添加 h5 volume 挂载;根路径 `/` 改为 302 → `/h5/`。
- **nginx 关键配置**alias + try_files SPA 模式):
```
location /itdesk/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /index.html;
}
```
注意:fallback 用 `/index.html` 而非 `/itdesk/index.html`——alias 会自动映射。
- **教训**:前端 `vite base` 路径必须与 nginx 服务路径一致;OAuth 回调路径不能用 301 重定向。
### CASE-20260708-06 · 管理端 OTP 用户列表返回错误结构
- **现象**:OTP 管理页面提示加载失败,`/auth/otp-admin-users` 返回 403。
- **根因**(1) `admin_list_otp_users` 返回普通数组,前端期望 `{total, items}` 结构;(2) `@require_role("admin")` 因 token 不含 admin 角色返回 403(见 CASE-03/04)。
- **修复**:后端改为分页查询返回 `{total, items}`;增加 keyword/bound/page/page_size 参数支持。
### 附:nginx 配错急救流程(2026-07-08 实战总结)
| 步骤 | 操作 |
|------|------|
| 1 | 备份:`sudo cp /opt/wecom-it-desk/nginx/nginx.conf /tmp/nginx.bak` |
| 2 | 修改宿主文件后必须 `docker stop nginx && docker start nginx``reload` 有时不生效) |
| 3 | 验证容器内配置已同步:`docker exec wecom_it_nginx grep 关键字 /etc/nginx/nginx.conf` |
| 4 | 确认语法:`docker exec wecom_it_nginx nginx -t` |
| 5 | 检查容器状态:`docker ps --filter name=wecom_it_nginx` |
| 6 | 用 `agent-browser` 打开 URL 做端到端验证 |
| 7 | 避免用 sed 修改 nginx 配置——`$uri`/`$host` 等变量会被 shell 解释;用 Python 脚本或直接上传文件 |
| 8 | ⚠️ **`sed -i` 会创建新 inode**Docker bind mount 不跟踪新 inode,导致容器内看到的仍是旧文件。用 `sed -i` 修改后必须 `docker restart`(而非 `nginx -s reload`),或改用 `sed` 不带 `-i` + 重定向写入原文件(保持 inode 不变) |
---
## 5 端到端验证完成标准(原《调试验证指南》整合)
> 宣布完成前,按 §0.2 提供真实证据。
### 5.1 管理后台验证(最常见)
| 步骤 | 操作 | 预期 |
|------|------|------|
| 1 | 浏览器开 `https://itsupport.servyou.com.cn/itadmin/` | 登录页 |
| 2 | 输入 `sxn` / `test123` 登录 | 进入 dashboard,右上角显示"宋" |
| 3 | 仪表盘数据渲染 | 在线坐席 / 今日会话 / 平均响应 / AI 命中率 有值 |
| 4 | API 拦截 `POST /api/agents/login` | HTTP 200 + `role:admin` + token |
### 5.2 通用验证清单(P0 必须通过)
- [ ] H5 登录流程正常(企微 OAuth 跳转 → 回跳 → 欢迎)
- [ ] 坐席登录正常,可接单
- [ ] 消息收发双向正常(文本 / 图片 / 文件)
- [ ] 邀请功能闭环
- [ ] 管理后台可访问且数据正常
### 5.3 真实浏览器证据获取(推荐 Playwright
本机 Windows 可直接访问服务器(TCP 443 通)。用 `playwright-core` 驱动已安装的 Chromium(路径 `~/.agent-browser/browsers/chrome-*/chrome.exe`),拦截 API 响应作为证据,避免 CLI 工具 IPC 不稳。
---
## 6 升级与应急(交叉引用,不重复)
- **回滚方案** → 见 [运维手册·第六章](../01-项目总览/01-智能IT服务系统运维手册-20260704.md#六回滚方案)
- **备份恢复** → 见 [运维手册·第七章](../01-项目总览/01-智能IT服务系统运维手册-20260704.md#七备份恢复)
- **应急响应(P0/P1 分级、止血、通知)** → 见 [SOP-04 应急响应](../07-项目管理/SOPs-标准流程/SOP-04-应急响应.md)
- 本手册只负责"定位 + 修复",变更管理与事故流程以上述文档为准。
---
## 7 参考文档索引
| 文档 | 说明 |
|------|------|
| `01-项目总览/01-智能IT服务系统运维手册-20260704.md` | 部署 / 回滚 / 备份 / 应急(故障排查章已并入本手册)|
| `07-项目管理/SOPs-标准流程/SOP-04-应急响应.md` | 应急响应 SOP |
| `04-运维文档/部署运维/deploy/01-部署指南.md` | 部署操作 |
| `04-运维文档/部署运维/deploy/03-版本记录.md` | 版本与修复记录索引 |
| `04-运维文档/部署运维/deploy/服务器部署手册.md` | 服务器部署细节 |
| `03-测试文档/testing-测试/E2E-CHECKLIST-v0.7.0.md` | E2E 验收清单 |
---
> **维护说明**: 本手册为故障排查唯一入口。新增案例请按 `CASE-YYYYMMDD-序号` 倒序追加到 §4;改动需同步本文件版本号与日期。