facc04aa65
本提交为 .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-*/
1029 lines
79 KiB
Markdown
1029 lines
79 KiB
Markdown
# 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-01(H5 选项选中状态效果消失 — 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-05(QuickRuleResponse 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-02(H5用户端收不到AI回复 — WebSocket推送datetime序列化失败)|
|
||
| v2.2 | 2026-07-25 | 新增 CASE-20260725-01(H5始终显示坐席在线 — WS断连查询字段不匹配)+ 判定矩阵更新 |
|
||
| v2.0 | 2026-07-23 | 新增 CASE-20260723-01(H5 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-01(nginx rewrite 不全导致 502)+ CASE-20260713-02(@require_role 装饰器参数缺失) |
|
||
| v2.0 | 2026-07-23 | 新增 CASE-20260723-01(H5 AI回复显示 [object Object] — Dify原生API超时回退代理)|
|
||
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
|
||
| v3.0 | 2026-07-28 | 新增 CASE-20260728-05(QuickRuleResponse 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-01(Redis urlparse 挂起)|
|
||
| v2.5 | 2026-07-27 | 新增 CASE-20260727-01(H5 选项选中状态效果消失 — 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-01(CSP 拦截��联 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-01(H5始终显示坐席在线 — WS断连查询字段不匹配)+ 判定矩阵更新 |
|
||
| v3.0 | 2026-07-28 | 新增 CASE-20260728-05(QuickRuleResponse Pydantic 序列化失败 — 模型字段未声明 Optional + SQL 初始数据缺 priority 列)|
|
||
| v3.2 | 2026-07-29 | 新增 CASE-20260729-01(坐席接入提示消息重复推送 — 用户要求取消但未生效)|
|
||
|
||
---
|
||
|
||
## 1 快速诊断决策树
|
||
|
||
### 1.1 三步隔离法(通用)
|
||
任何"页面打不开 / 网络连接失败 / 接口无响应 / 422"都先用三步隔离,定位是 nginx、后端、还是依赖(DB / Redis)的问题。
|
||
|
||
#### Step 0:HTTP 响应头检查(页面 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 不执行——这类问题无法被三步隔离法捕获。根因通常是 CSP(Content-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 → 点击「业务分类」下拉菜单 → 弹出"服务器内部错误,请稍后重试或联系管理员"。其他 Tab(greeting、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-loop(Restarting (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:再次发生,返回 500(rewrite 循环 `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` 用 encoded,redis `--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)
|
||
- 根因1:nginx 未正确挂载 `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;改动需同步本文件版本号与日期。
|