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-*/
This commit is contained in:
Simon
2026-08-07 22:31:32 +08:00
parent 5a77a89ab1
commit facc04aa65
573 changed files with 129347 additions and 909 deletions
@@ -0,0 +1,397 @@
# 快速回复规则后台管理 — 部署文档
> **需求编号**: REQ-通用-002
> **版本**: v1.22026-07-28 管理后台 UI 去重调整)
> **日期**: 2026-07-27(初版) / 2026-07-28v1.1 整改、v1.2 UI 调整)
> **状态**: [已评审]
> **作者**: Simon
> **部署服务器**: itsupport.servyou.com.cn (10.90.5.110)
>
> **关联文档**(按规范要求补全):
> - PRD`01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md`
> - 技术方案:`02-技术文档/技术方案-REQ-通用-002-快速回复规则后台管理.md`
> - 原型图:`01-产品文档/01-02产品设计/快速回复规则后台管理-原型图.html`
> - 上游依赖:Alembic 055 迁移(`alembic upgrade 055`)、`scripts/init_quick_rules.sql`
>
> **命名规范说明**:按 `docs/00-产品开发流程与文档管理规范.md` 2.3.3 节,标准命名应为
> `DEPLOY-REQ-通用-002-快速回复规则后台管理-v1.0.md`,应放置在 `04-运维文档/部署运维/` 子目录下。
> 本次保留历史文件名/位置以避免破坏现有内部引用;**后续同类文档请按规范命名**。
---
## 0. 前置条件(部署前必须 100% 确认)
| 检查项 | 要求 | 验证方式 |
|--------|------|----------|
| 关联 PRD | 已评审、当前为 [已评审] 状态 | `Read docs/01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md` |
| 关联技术方案 | 已评审 | `Read docs/02-技术文档/技术方案-REQ-通用-002-快速回复规则后台管理.md` |
| 关联原型图 | 已评审(本次涉及 6 个新页面,必须有) | 打开 `docs/01-产品文档/01-02产品设计/快速回复规则后台管理-原型图.html` |
| Alembic 迁移文件 | `alembic/versions/055_*.py` 已存在 | `ls alembic/versions/ | grep -i quick` |
| 初始化 SQL | `scripts/init_quick_rules.sql` 已就绪且 ≥ 53 条 | `wc -l scripts/init_quick_rules.sql` |
| jumpserver-V2 skill | 已登录且缓存有效 | 跑 `v2_ops.py status` |
| 服务器 `.env` | `.env` 已同步 `QUICK_RULE_*` 类变量(见 §1.3 | `grep -E "^QUICK_RULE_" .env` |
| 后端镜像 | `--workers 1` 已写入 `docker-compose.yml` | `grep -E "workers" docker-compose.yml` |
| 部署窗口 | 已与值班坐席同步,选低峰期 | 口头/IM 通知 |
---
## 1. 部署概览
### 1.1 部署范围
| 模块 | 状态 | 说明 |
|------|------|------|
| 数据库表 | 新增 2 张 | `quick_rules``quick_rule_audit_log` |
| 后端 API | 新增 12 个 | `/api/admin/quick-rules/*`(见技术方案 §3 |
| 前端页面 | 新增 6 个 | `/quick-rules/*`admin 端 1 个 H5/坐席端不涉及) |
| 现有代码改造 | 2 处 | `ai_handler.py``routing_service.py`(向后兼容:DB 为空时回退硬编码) |
| **本版本不涉及** | - | agent 端、H5 端、terminal 端无需重新打包 |
### 1.2 规则类型与初始数量(单一权威来源,§5 验证时直接引用此处)
| rule_type | 初始条数 | 兜底硬编码条数 |
|-----------|----------|----------------|
| `greeting` | 12 | 1 条("你好" → 引导描述问题) |
| `routing_prefilter` | 33 | 3 条 |
| `routing_target` | 6 | 2 条 |
| **合计** | **51** | **6** |
### 1.3 `.env` 变量变更清单(配置同步铁律,必须同步)
> 部署铁律:修改 `config.py` 默认值 ≠ 配置生效,必须同步更新服务器 `.env`。
| 变量名 | 用途 | 默认值 | 来源 |
|--------|------|--------|------|
| `QUICK_RULE_AUTO_APPLY_THRESHOLD` | 智能体自动应用规则的置信度阈值 | `0.85` | config.py |
| `QUICK_RULE_AUDIT_REVIEW_THRESHOLD` | 落入待审核的阈值 | `0.5` | config.py |
| `QUICK_RULE_CACHE_TTL_SECONDS` | Redis 缓存 TTL | `300` | config.py |
服务器部署完成后必须执行:
```bash
# 通过 v2_ops.py 上传 .env 增量文件
v2_ops.py upload .env.production-increment .env.production-increment
# 远端追加(注意:此处禁用 $(date),用预生成时间戳文件名)
ssh-via-jms 'cat /tmp/.env.production-increment >> /opt/wecom-it-desk/.env'
# 重载后端使新变量生效
docker compose --env-file .env restart backend
```
### 1.4 部署顺序(部署铁律)
```
DB migrationalembic upgrade)→ .env 同步 → 后端 coderestart)→ 前端 adminpsftp + restart nginx)→ 端到端验证 → 灰度
```
---
## 2. 数据库迁移
### 2.1 备份(v2_ops.py / psftp 双通道)
```bash
# 1. 登录堡垒机(jumpserver-V2 skill
v2_ops.py login
# 2. 服务器端生成备份文件名(禁用 $(...),预生成)
TIMESTAMP=$(date +%Y%m%d_%H%M%S) # 本地 PowerShell 生成后再传入
ssh-via-jms "docker exec wecom_it_postgres pg_dump -U postgres -d wecom_it_desk -Fc > /opt/wecom-it-desk/backups/before-quickrule-${TIMESTAMP}.dump"
```
### 2.2 执行迁移(按部署铁律:DB migration 只能逐版降)
```bash
# 1. 检查当前版本
ssh-via-jms "docker exec wecom_it_backend alembic current"
# 2. 升级到目标版本(升级用 head,向前兼容)
ssh-via-jms "docker exec wecom_it_backend alembic upgrade 055"
# 3. 验证:checkfirst 自动建表是否成功(主要依赖 alembic,但作为冗余验证)
ssh-via-jms "docker exec wecom_it_postgres psql -U postgres -d wecom_it_desk -c '\\dt quick_rules*'"
# 期望输出:
# quick_rule_audit_log
# quick_rules
# 4. 插入初始化数据(psftp 通道,禁用 elFinder
v2_ops.py upload scripts/init_quick_rules.sql init_quick_rules.sql
ssh-via-jms "cat /tmp/init_quick_rules.sql | docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -v ON_ERROR_STOP=1"
```
> ⚠️ **降级路径**(回滚时使用):蓝绿共用同一 PG,本次只能逐版降
> ```bash
> ssh-via-jms "docker exec wecom_it_backend alembic downgrade -1"
> # 重复执行直到 quick_rules 表消失
> ssh-via-jms "docker exec wecom_it_postgres psql -U postgres -d wecom_it_desk -c '\\dt quick_rules*'"
> ```
### 2.3 验证
```sql
-- 单一权威断言:本表数字一旦修改,§1.2 同步修改
SELECT rule_type, COUNT(*) FROM quick_rules WHERE is_active = true GROUP BY rule_type ORDER BY rule_type;
-- 期望:greeting=12, routing_prefilter=33, routing_target=6
```
---
## 3. 后端部署
### 3.1 部署(卷挂载 → restart 即可)
```bash
# 后端代码统一源:/opt/wecom-it-desk/app/(卷挂载)
# .py 变更仅 restart,不必 rebuild
v2_ops.py upload src/backend/app/models/quick_rule.py /tmp/quick_rule.py
v2_ops.py upload src/backend/app/models/quick_rule_audit_log.py /tmp/quick_rule_audit_log.py
v2_ops.py upload src/backend/app/services/quick_rule_service.py /tmp/quick_rule_service.py
v2_ops.py upload src/backend/app/api/admin/quick_rules.py /tmp/quick_rules.py
v2_ops.py upload src/backend/app/services/ai_handler.py /tmp/ai_handler.py
v2_ops.py upload src/backend/app/services/routing_service.py /tmp/routing_service.py
ssh-via-jms 'for f in quick_rule.py quick_rule_audit_log.py quick_rule_service.py quick_rules.py ai_handler.py routing_service.py; do
if [ -f /tmp/$f ]; then
cp /tmp/$f /opt/wecom-it-desk/app/$(echo $f | sed "s|^|models/|; s|quick_rule_audit_log.py|models/&|; s|quick_rule_service.py|services/&|; s|quick_rules.py|api/admin/&|; s|ai_handler.py|services/&|; s|routing_service.py|services/&|")
fi
done'
# 必须保证 workers=1(部署铁律:多 worker 会致 WS 消息丢失)
ssh-via-jms "docker compose --env-file .env up -d --force-recreate --no-deps backend"
# 或:ssh-via-jms "docker compose restart backend"
# 探活
sleep 5
ssh-via-jms "docker exec wecom_it_backend python -c \"from app.models.quick_rule import QuickRule; print('OK', QuickRule.__tablename__)\""
```
### 3.2 启动期验证
```bash
# 1. 后端探活(外部)
curl -fsS http://itsupport.servyou.com.cn/health
# 2. Swagger 文档(确认 quick-rules 路由注册)
curl -fsS http://itsupport.servyou.com.cn/docs | grep quick-rules
# 3. 后端日志:必须看到 QuickRuleService.load_all 成功
ssh-via-jms "docker logs wecom_it_backend --tail 200 2>&1 | grep -i 'quick_rule\\|loaded.*rules'"
# 期望:loaded 51 rules (12 greeting, 33 routing_prefilter, 6 routing_target)
```
### 3.3 业务侧验证(端到端、容器外,验证铁律)
```bash
# 获取 token
TOKEN=$(curl -s -X POST http://itsupport.servyou.com.cn/api/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"simon","password":"<OTP>"}' | jq -r .data.token)
# 1. stats 接口
curl -fsS http://itsupport.servyou.com.cn/api/admin/quick-rules/stats \
-H "Authorization: Bearer $TOKEN" | jq .
# 期望:{ "code":0, "data":{ "greeting":12, "routing_prefilter":33, "routing_target":6, "total":51 } }
# 2. 列表接口(greeting
curl -fsS "http://itsupport.servyou.com.cn/api/admin/quick-rules?rule_type=greeting&limit=5" \
-H "Authorization: Bearer $TOKEN" | jq '.data | length'
# 期望:5
```
---
## 4. 前端部署(仅 admin 端)
### 4.1 本地构建(ASCII 路径,前端构建 SOP)
```bash
# 本地路径必须纯 ASCIID:\资料\路径下 pnpm install 会卡死)
cd D:\dev\wecom\src\frontend-admin
# 多路径同步铁律:中文路径下 D:\资料\03-项目开发\wecom_it_smart_desk 也要同步改
# 否则 vite build 用的是 ASCII 路径下的旧代码
npm install # 不要用 pnpm install
npm run build # 产物在 dist/
Compress-Archive -Path dist -DestinationPath dist-quickrules-v1.0.zip
```
### 4.2 上传与部署(psftp 通道,禁止 elFinder
```bash
# 1. 上传 zip
v2_ops.py upload D:\dev\wecom\src\frontend-admin\dist-quickrules-v1.0.zip dist-quickrules-v1.0.zip
# 2. 服务器端解压到 bind mount 目录(unzip 会直接产出 assets/ + index.html
ssh-via-jms 'cd /opt/wecom-it-desk/frontend-admin/dist-new && unzip -o /tmp/dist-quickrules-v1.0.zip'
ssh-via-jms 'rm -rf /opt/wecom-it-desk/frontend-admin/dist && mv /opt/wecom-it-desk/frontend-admin/dist-new /opt/wecom-it-desk/frontend-admin/dist'
# 3. ★ 必须显式重启 nginxbind mount 不会自动刷新新文件)
ssh-via-jms 'docker restart wecom_it_nginx'
# 4. 验证
curl -fsS http://itsupport.servyou.com.cn/itadmin/quick-rules | head -c 200
# 期望:HTML 200 OKtitle 含 "快速回复"
```
### 4.3 浏览器自测(agent-browser skill
```bash
# 走 agent-browser 技能自动化测试,至少覆盖:
# 1) /itadmin/quick-rules 列表加载
# 2) /itadmin/quick-rules/greeting 编辑一条规则、断言持久化
# 3) /itadmin/quick-rules/audit 审计日志可见
# 4) 模拟"用户你好"→ 期望 AI 引导描述(不调用 Dify)
```
---
## 5. 端到端回归验证(验证铁律:容器内通过≠外部可用)
| # | 场景 | 输入 | 期望输出 | 验证方式 |
|---|------|------|----------|----------|
| T1 | 打招呼走 quick_rule,不调用 Dify | Mock 用户发"你好" | AI 回复引导描述问题,diffy_call_count=0 | WebSocket mock + logs |
| T2 | 路由触发命中 quick_rule | Mock 用户发"打印机连不上" | 命中 prefilter→target,发送名片卡片 | 卡片 payload + 路由日志 |
| T3 | 普通问题走 Dify(兼容路径) | Mock 用户发"电脑蓝屏了" | 走 Dify 主推理,diffy_call_count=1 | Dify 调用日志 |
| T4 | 兜底:清空 DB → 重启后仍可用 | `DELETE FROM quick_rules; docker compose restart backend` | 行为不中断,加载日志 "fallback to hardcoded" | 启动日志 + 业务接口 |
| T5 | 缓存刷新 | POST `/api/admin/quick-rules/refresh` | 60s 内 Redis keys 变化 | `redis-cli KEYS quick_rules:*` |
具体执行命令见配套测试用例 `TC-快速回复规则后台管理.md`**待补,需建**)。
---
## 6. 灰度上线
### 6.1 灰度开关(env + 代码双层)
| 开关 | 默认 | 启用时效果 |
|------|------|-----------|
| `QUICK_RULE_ENABLED`(env,**本次新增,必须加**) | `true` | `false` 时完全走硬编码兜底 |
| `QUICK_RULE_AUTO_APPLY_THRESHOLD` | `0.85` | 调高(如 `0.99`)等于关闭自动应用 |
| `QUICK_RULE_AUDIT_REVIEW_THRESHOLD` | `0.5` | 调高(如 `0.9`)等于全部走待审核 |
### 6.2 灰度策略
| 阶段 | 时长 | 动作 | 监控重点 |
|------|------|------|----------|
| Phase 1 观察期 | 1-3 天 | 启用新规则,日志标 `quick_rule.hit` | API 成功率、命中占比 |
| Phase 2 人工编辑期 | 4-7 天 | 运营通过后台编辑 | 误判率、日志异常 |
| Phase 3 完全替代 | 7 天+ | 保留硬编码兜底 | 同上 |
### 6.3 回滚(完整三路回滚)
```bash
# Step 1:DB 回滚(降级,按部署铁律:只 downgrade 不 drop 备份)
ssh-via-jms "docker exec wecom_it_backend alembic downgrade -1"
# Step 2:清空规则(保留表结构,使代码加载硬编码兜底)
ssh-via-jms "docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c \"DELETE FROM quick_rules;\""
# Step 3:后端代码回滚(git)
ssh-via-jms "cd /opt/wecom-it-desk && git log --oneline -5"
ssh-via-jms "cd /opt/wecom-it-desk && git revert --no-edit HEAD"
# Step 4:前端 dist 回滚
v2_ops.py upload archives/frontend-admin-dist-v*.zip rollback.zip
ssh-via-jms 'cd /opt/wecom-it-desk/frontend-admin && rm -rf dist && mkdir dist && cd dist && unzip /tmp/rollback.zip'
ssh-via-jms 'docker restart wecom_it_nginx'
# Step 5:环境变量回滚(可选,但建议保持 0 风险)
# 在 .env 中 QUICK_RULE_ENABLED=falserestart backend
```
---
## 7. 监控指标
### 7.1 关键指标(明确阈值,可告警)
| 指标 | 采集方式 | 告警阈值 | 通知渠道 |
|------|---------|----------|----------|
| `/api/admin/quick-rules/*` 调用成功率 | nginx access log 5xx 占比 | > 1% | 企微机器人 |
| 规则加载耗时(启动期) | `docker logs wecom_it_backend` | > 3s | 日志告警 |
| 缓存命中率 | `INFO logs/smart_desk.log``quick_rule.hit / quick_rule.miss` | < 70%24h 均值) | 企微机器人 |
| 兜底触发次数 | 日志 `quick_rule.fallback` | 24h 内 > 100 | 企微机器人 |
| 自动应用次数突增 | `quick_rule_audit_log` | 1h 内 > 50 | 企微机器人 |
| Alembic 迁移失败 | 启动日志 | 任意 1 次 ERROR | P1 告警 |
### 7.2 日志查询 SOP
```bash
# 启动期规则加载
ssh-via-jms "docker logs wecom_it_backend --since 10m 2>&1 | grep -i 'quick_rule'"
# 命中/兜底实时监控
ssh-via-jms "tail -f /opt/wecom-it-desk/logs/wecom-it-desk.log | grep quick_rule"
# Redis 缓存
ssh-via-jms "docker exec wecom_it_redis redis-cli KEYS 'quick_rules:*'"
```
---
## 8. 常见问题
### Q1:数据库表已存在错误(原文"数据库插件已存在错误"为错别字)
```text
错误:relation "quick_rules" already exists
原因:alembic 055 已成功建表,重复执行会触发
解决:属正常现象,跳过或继续下一步
```
### Q2:初始化数据重复插入失败
```text
错误:duplicate key value violates unique constraint
原因:脚本已使用 ON CONFLICT DO NOTHING,重复执行安全
解决:直接忽略;如需强制重置,先 TRUNCATE quick_rules
```
### Q3:缓存未刷新
```bash
curl -X POST "http://itsupport.servyou.com.cn/api/admin/quick-rules/refresh" \
-H "Authorization: Bearer $TOKEN"
# 或直接清 Redis
ssh-via-jms "docker exec wecom_it_redis redis-cli DEL $(docker exec wecom_it_redis redis-cli KEYS 'quick_rules:*' | tr '\n' ' ')"
```
### Q4:规则加载后业务侧未生效
```text
排查路径:
1) docker logs wecom_it_backend | grep "loaded.*rules" 确认 51 条加载
2) curl /api/admin/quick-rules/stats 确认 DB 与 API 一致
3) tail logs/wecom-it-desk.log | grep quick_rule.fallback 看是否走兜底
4) REDIS 缓存是否过期(TTL 默认 300s),可手动 DEL 后重试
```
### Q5:缓存验证时找不到 admin token
```bash
TOKEN=$(curl -s -X POST http://itsupport.servyou.com.cn/api/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"simon","password":"<OTP>"}' | jq -r .data.token)
```
### Q6:前端 dist 更新后页面没刷新
```text
原因:bind mount 不会自动同步;nginx 也不会主动重读
解决:必须 docker restart wecom_it_nginx
```
---
## 9. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|------|------|----------|--------|----------|----------|
| 2026-07-27 | v1.0 | 初版 | Simon | 新功能首次上线 | - |
| 2026-07-28 | v1.1 | 整改:补全头部模板/关联文档/前置条件/.env变更清单/回滚三路方案/灰度开关/可执行验证命令;修复 `ssh sxn@``$(date)`、elFinder 三处命令级错误;命名规范说明追加 | Simon | 复盘发现不符合规范 §2.3/§5.2 + 命令违反部署铁律 | 本文档全部重写,零代码变更 |
| 2026-07-28 | v1.2 | 管理后台 `/quick-rules` 顶部 3 张重复统计卡片移除,仅保留标签导航及 count 徽标;后端 stats 接口与部署流程保持不变 | Simon | 页面信息冗余 | 仅管理后台前端展示,部署位置与流程不变 |
@@ -0,0 +1,128 @@
# Dify 工作流 System Prompt 改造指南
## 改造目标
将 Dify 输出从 `action` 字段改为 `intent` 标签,让后端根据 intent 查询资产库生成真正的推荐卡片。
## 当前输出格式(需改造)
```json
{
"text": "回答文本...",
"action": {
"type": "approval_card",
"title": "软件安装审批",
"description": "申请安装 Adobe 系列软件",
"approval_type": "software_install",
"confidence": 0.95
},
"options": [...],
"diagnosis_stage": "diagnosing"
}
```
## 目标输出格式
```json
{
"text": "给用户的回答文本(50-200字)",
"intent": "用户意图标签",
"need_approval": (true/false),
"need_asset": (true/false),
"asset_keywords": ["关键词1", "关键词2"],
"options": [
{"label": "选项文字", "value": "选项值"}
],
"diagnosis_stage": "greeting|diagnosing|resolved|transfer"
}
```
## 改造步骤
### 1. 登录 Dify Console
访问:`http://yw-dify.dc.servyou-it.com/console`
### 2. 找到目标应用
找到「智能IT支持-员工咨询」应用(App ID: `8f0f3d62-f63d-4cf3-815e-b10529c66f1d`
### 3. 进入工作流编辑
1. 点击应用名称进入详情
2. 点击「编辑」
3. 进入「编排」页面
### 4. 找到 LLM 节点
找到输出 JSON 结构的 LLM 节点(通常在"开始"节点之后的第一个 LLM 节点)
### 5. 修改 System Prompt
在 LLM 节点的「系统提示词」中添加以下内容(追加到现有 Prompt 末尾):
```
## 输出格式要求
你是一个 IT 智能助手。请根据用户问题,输出以下 JSON 结构:
{
"text": "给用户的回答文本(50-200字)",
"intent": "用户意图标签",
"need_approval": 是否需要审批入口 (true/false),
"need_asset": 是否需要资产推荐 (true/false),
"asset_keywords": ["关键词1", "关键词2"],
"options": [
{"label": "选项文字", "value": "选项值"}
],
"diagnosis_stage": "greeting|diagnosing|resolved|transfer"
}
## 意图标签说明
intent 字段可选值:
- software_install: 软件安装
- hardware_issue: 硬件问题
- network_vpn: 网络/VPN 问题
- printer: 打印机问题
- account_permission: 账号权限问题
- email_outlook: 邮箱问题
- mobile_device: 移动设备问题
- other: 其他问题
## 资产推荐规则
当用户询问以下内容时,设置 need_asset=true 并指定 asset_keywords
- VPN 相关 → asset_keywords: ["vpn"]
- 打印机相关 → asset_keywords: ["打印机", "打印机驱动"]
- 软件安装 → asset_keywords: ["软件安装"]
- 邮箱问题 → asset_keywords: ["outlook", "邮箱"]
- 账号权限 → asset_keywords: ["账号", "权限"]
- 网络问题 → asset_keywords: ["网络", "wifi"]
重要:不要在 text 中推荐具体的下载链接或服务器地址,这些信息由系统根据 asset_keywords 智能匹配后推送到右侧栏。
```
### 6. 发布工作流
点击右上角「发布」按钮
### 7. 验证
在「发布」页面点击「运行」测试,验证输出格式是否符合预期。
## 回滚方案
如果新版本有问题,可以在 Dify Console 的「版本历史」中找到上一个版本,点击「恢复」即可回滚。
## 后端适配
后端代码 `asset_recommend_service.py` 已支持处理新的 `intent` 格式。
当 Dify 返回 `need_asset=true` + `asset_keywords` 时,后端会根据关键词查询 `assets.yaml` 中的资产信息,生成 L1 推荐卡片。
## 预期效果
用户说"VPN 连不上"
- **中间栏**AI 回复排查步骤
- **右侧栏 L1**:VPN 相关信息(客户端下载、服务器地址、申请审批)
@@ -0,0 +1,635 @@
# 健康检查 + 错误码 + 日志结构化 审计与改进方案
**审计日期**: 2026-06-15
**审计人**: Claude(满载跑批)
**关联**: [[风险跟踪表]] / [[后端架构]] / [[Dockerfile优化与镜像审计]]
---
## 📌 1. 健康检查现状
### 1.1 当前实现
**端点**: `backend/app/main.py:506`
```python
@app.get("/health", tags=["系统"])
async def health_check():
"""健康检查端点。"""
return {"status": "ok", "service": "wecom-it-smart-desk"}
```
**Docker compose healthcheck**:
```yaml
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
### 1.2 问题清单
| # | 问题 | 严重度 | 影响 |
|---|---|---|---|
| H-1 | `/health` 不验证 DB 连接 | 🟠 中 | DB 挂了,但 healthcheck 还显示 OK |
| H-2 | `/health` 不验证 Redis 连接 | 🟠 中 | Redis 挂了,但 healthcheck 还显示 OK |
| H-3 | `/health` 不报告版本/build | 🟡 | 排障不便 |
| H-4 | `/health` 永远是 200,无 degraded 状态 | 🟡 | 难区分"在线但降级" |
| H-5 | 无 `/ready``/live` 区分 | 🟡 | K8s 不友好 |
| H-6 | Docker healthcheck 改用 urllib 已修 ✅(P1-3) | 🟢 | 已 done |
### 1.3 改进版(完整 healthcheck)
```python
# backend/app/api/health.py(新建)
import time
import psutil
from typing import Dict, Any
from fastapi import APIRouter, HTTPException
from sqlalchemy import text
from app.database import async_session_maker
from app.config import settings
from app.utils.token_manager import get_token_manager
router = APIRouter(tags=["系统"])
START_TIME = time.time()
@router.get("/health")
async def health_check():
"""Liveness probe - 进程是否存活
适用: K8s livenessProbe / Docker healthcheck
返回: 总是 200,只要进程没崩
"""
return {
"status": "ok",
"service": "wecom-it-smart-desk",
"uptime_seconds": int(time.time() - START_TIME),
}
@router.get("/ready")
async def readiness_check():
"""Readiness probe - 进程是否准备好接流量
适用: K8s readinessProbe / 负载均衡
验证: DB + Redis 实际连通性
"""
checks = {
"database": False,
"redis": False,
"wecom_token": False,
}
# 1. DB 检查
try:
async with async_session_maker() as session:
result = await session.execute(text("SELECT 1"))
result.scalar()
checks["database"] = True
except Exception as e:
checks["database_error"] = str(e)[:200]
# 2. Redis 检查
try:
tm = get_token_manager()
client = await tm.get_redis()
await client.ping()
checks["redis"] = True
except Exception as e:
checks["redis_error"] = str(e)[:200]
# 3. 企微 token 检查(可选)
try:
tm = get_token_manager()
token = await tm.get_access_token()
checks["wecom_token"] = bool(token)
except Exception as e:
checks["wecom_error"] = str(e)[:200]
all_ok = all(v for k, v in checks.items() if not k.endswith("_error"))
status_code = 200 if all_ok else 503
return JSONResponse(
status_code=status_code,
content={
"status": "ready" if all_ok else "degraded",
"service": "wecom-it-smart-desk",
"uptime_seconds": int(time.time() - START_TIME),
"checks": checks,
"timestamp": datetime.now().isoformat(),
}
)
@router.get("/metrics")
async def metrics():
"""Prometheus metrics 端点(轻量版)
适用: Prometheus 抓取
输出: 关键业务/技术指标
"""
process = psutil.Process()
return {
"process": {
"cpu_percent": process.cpu_percent(),
"memory_mb": process.memory_info().rss / 1024 / 1024,
"threads": process.num_threads(),
"uptime_seconds": int(time.time() - START_TIME),
},
"system": {
"cpu_percent": psutil.cpu_percent(),
"memory_percent": psutil.virtual_memory().percent,
"disk_percent": psutil.disk_usage('/').percent,
},
}
@router.get("/version")
async def version():
"""版本信息端点
用途: 排障 / 部署确认
"""
import os
return {
"service": "wecom-it-smart-desk",
"version": os.getenv("APP_VERSION", "dev"),
"git_sha": os.getenv("GIT_SHA", "unknown")[:8],
"build_time": os.getenv("BUILD_TIME", "unknown"),
"python": "3.12",
}
```
### 1.4 Docker compose 更新
```yaml
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# 高级(可选,等 K8s 迁移时)
readiness:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready').read()"]
interval: 10s
timeout: 5s
retries: 3
```
---
## 📌 2. 错误码体系现状与改进
### 2.1 现状(`backend/app/utils/response.py`)
**已有错误码**(18 个):
- **1000+ 通用** (5): ERR_PARAMS / UNAUTHORIZED / NOT_FOUND / FORBIDDEN / INTERNAL
- **2000+ 企微** (6): WECOM_TOKEN / SEND / DECRYPT / ENCRYPT / VERIFY / USER_INFO
- **3000+ 业务** (7): AGENT_OFFLINE / CONVERSATION_RESOLVED / CONVERSATION_NOT_FOUND / AGENT_NOT_FOUND / AGENT_BUSY / DUPLICATE_ASSIGN / GRAB_*
**格式**:
```json
{"code": 0, "data": {}, "message": "success"}
```
### 2.2 问题清单
| # | 问题 | 严重度 | 解决 |
|---|---|---|---|
| E-1 | 错误码无标准枚举类(只常量) | 🟡 | 加 `ErrorCode` Enum |
| E-2 | HTTP 200 + code 非 0(违反 REST 习惯) | 🟡 | 评估:4xx 5xx 也可,跟前端约定 |
| E-3 | 没错误追踪 ID(correlation_id) | 🟠 | 加 `trace_id` 字段 |
| E-4 | 错误响应没 `documentation_url` | 🟢 | 加上,链到文档 |
| E-5 | i18n 缺失(中文硬编码) | 🟡 | 错误消息 i18n 化 |
| E-6 | 前端错误处理分散(无统一拦截) | 🟠 | 加 axios 拦截器 + 错误码映射表 |
### 2.3 改进版错误码体系
**新建 `backend/app/utils/error_codes.py`**:
```python
# =============================================================================
# 错误码体系 - 标准枚举
# =============================================================================
# 规范:
# - 0 = 成功
# - 1xxx = 通用错误
# - 2xxx = 鉴权/会话
# - 3xxx = 企微 API
# - 4xxx = 业务 - 会话
# - 5xxx = 业务 - 坐席
# - 6xxx = 业务 - 配置
# - 7xxx = 集成外部系统
# - 9xxx = 兜底
# =============================================================================
from enum import Enum
class ErrorCode(int, Enum):
"""统一错误码枚举"""
# 0: 成功
SUCCESS = 0
# 1xxx: 通用错误
PARAMS_INVALID = 1001 # 参数错误
UNAUTHORIZED = 1002 # 未授权
NOT_FOUND = 1003 # 资源不存在
FORBIDDEN = 1004 # 无权限
INTERNAL = 1005 # 服务器错误
RATE_LIMITED = 1006 # 限流
SERVICE_UNAVAILABLE = 1007 # 服务不可用
TIMEOUT = 1008 # 超时
# 2xxx: 鉴权
AUTH_TOKEN_MISSING = 2001 # token 缺失
AUTH_TOKEN_EXPIRED = 2002 # token 过期
AUTH_TOKEN_INVALID = 2003 # token 无效
AUTH_OTP_REQUIRED = 2004 # 需要 OTP
AUTH_OTP_INVALID = 2005 # OTP 错误
AUTH_PASSWORD_WRONG = 2006 # 密码错误
AUTH_AGENT_DISABLED = 2007 # 坐席已禁用
# 3xxx: 企微 API
WECOM_TOKEN_FAIL = 3001 # 企微 token 获取失败
WECOM_SEND_FAIL = 3002 # 企微消息发送失败
WECOM_DECRYPT_FAIL = 3003 # 企微消息解密失败
WECOM_ENCRYPT_FAIL = 3004 # 企微消息加密失败
WECOM_VERIFY_FAIL = 3005 # 企微回调签名验证失败
WECOM_USER_INFO_FAIL = 3006 # 企微用户信息获取失败
WECOM_API_ERROR = 3099 # 企微 API 通用错误
# 4xxx: 业务 - 会话
CONV_NOT_FOUND = 4001 # 会话不存在
CONV_RESOLVED = 4002 # 会话已结单
CONV_NO_AGENT = 4003 # 无可用坐席
CONV_DUPLICATE_ASSIGN = 4004 # 重复分配
CONV_GRAB_DENIED = 4005 # 抢单失败
# 5xxx: 业务 - 坐席
AGENT_NOT_FOUND = 5001 # 坐席不存在
AGENT_OFFLINE = 5002 # 坐席离线
AGENT_BUSY = 5003 # 坐席满载
AGENT_GRAB_SELF = 5004 # 不能接手自己的会话
AGENT_GRAB_NOT_SERVING = 5005 # 只能接手服务中的会话
# 6xxx: 业务 - 配置
CONFIG_NOT_FOUND = 6001 # 配置不存在
CONFIG_INVALID = 6002 # 配置值无效
# 7xxx: 集成外部
HUORONG_API_FAIL = 7001 # 火绒 API
LIANRUAN_API_FAIL = 7002 # 联软 API
ATRUST_API_FAIL = 7003 # aTrust API
EHR_API_FAIL = 7004 # eHR API
DIFY_API_FAIL = 7005 # Dify API
# 9xxx: 兜底
UNKNOWN = 9999
# 错误码 → HTTP 状态码(可选,默认 200)
HTTP_STATUS_MAP = {
ErrorCode.SUCCESS: 200,
ErrorCode.PARAMS_INVALID: 422,
ErrorCode.UNAUTHORIZED: 401,
ErrorCode.NOT_FOUND: 404,
ErrorCode.FORBIDDEN: 403,
ErrorCode.INTERNAL: 500,
ErrorCode.RATE_LIMITED: 429,
ErrorCode.SERVICE_UNAVAILABLE: 503,
ErrorCode.TIMEOUT: 504,
ErrorCode.AUTH_TOKEN_MISSING: 401,
ErrorCode.AUTH_TOKEN_EXPIRED: 401,
ErrorCode.AUTH_TOKEN_INVALID: 401,
ErrorCode.AUTH_OTP_REQUIRED: 401,
ErrorCode.AUTH_OTP_INVALID: 401,
ErrorCode.AUTH_PASSWORD_WRONG: 401,
ErrorCode.AUTH_AGENT_DISABLED: 403,
# 业务错误默认 200,通过 code 区分
# 但具体可调,如 4xxx 资源类 404,5xxx 状态类 409
}
```
**更新 `response.py`**:
```python
from app.utils.error_codes import ErrorCode, HTTP_STATUS_MAP
def error_response(
code: ErrorCode,
message: str,
data: Any = None,
trace_id: str = None,
) -> Dict[str, Any]:
"""构建错误响应(增加 trace_id)"""
return {
"code": int(code),
"message": message,
"data": data or {},
"trace_id": trace_id,
"timestamp": datetime.now().isoformat(),
}
class AppException(Exception):
def __init__(
self,
code: ErrorCode,
message: str,
data: Any = None,
http_status: int = None,
):
self.code = code
self.message = message
self.data = data
self.http_status = http_status or HTTP_STATUS_MAP.get(code, 200)
super().__init__(message)
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
# 生成 trace_id
import uuid
trace_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
# 记录到日志
logger.warning(
f"[{trace_id}] {request.method} {request.url.path} "
f"-> {exc.code.value} {exc.message}"
)
return JSONResponse(
status_code=exc.http_status,
content=error_response(exc.code, exc.message, exc.data, trace_id),
headers={"X-Trace-ID": trace_id},
)
```
### 2.4 前端错误码映射(axios 拦截器)
**新建 `frontend-admin/src/api/error-handler.ts`**(每个前端类似):
```typescript
import { ElMessage } from 'element-plus'
// 错误码 → 用户提示
const ERROR_MESSAGES: Record<number, string> = {
1001: '参数错误,请检查输入',
1002: '登录已过期,请重新登录',
1003: '资源不存在',
1004: '无权限访问',
1005: '服务器错误,请稍后重试',
1006: '操作过快,请稍候再试',
2001: '请先登录',
2002: '登录已过期',
2003: '身份验证失败',
2004: '请输入动态码',
2005: '动态码错误',
2006: '密码错误',
4001: '会话不存在',
4002: '会话已结束',
5001: '坐席不存在',
5002: '坐席离线',
5003: '坐席已满载',
9999: '未知错误',
}
export function handleError(code: number, message: string, traceId?: string) {
const userMsg = ERROR_MESSAGES[code] || message || '操作失败'
// 特殊处理
if ([1002, 2001, 2002, 2003].includes(code)) {
// 跳登录
localStorage.removeItem('token')
window.location.href = '/login'
}
ElMessage.error(userMsg)
// 开发环境显示 trace_id
if (import.meta.env.DEV && traceId) {
console.error(`[TraceID: ${traceId}] Code: ${code}, Message: ${message}`)
}
}
```
---
## 📌 3. 日志结构化
### 3.1 现状
```python
# backend/app/main.py
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
)
```
**问题**:
- 🟡 文本格式,不易查询/聚合
- 🟡 无 trace_id
- 🟡 无 request/response 记录
- 🟡 无结构化字段(用户/会话/操作)
### 3.2 改进版
**新建 `backend/app/utils/logging_config.py`**:
```python
import json
import logging
import sys
import time
from contextvars import ContextVar
from typing import Any, Dict, Optional
# 请求上下文
request_id_var: ContextVar[Optional[str]] = ContextVar('request_id', default=None)
user_id_var: ContextVar[Optional[str]] = ContextVar('user_id', default=None)
class JSONFormatter(logging.Formatter):
"""JSON 格式化器 - 适合 ELK / Loki / CloudWatch 解析"""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# 上下文
if request_id_var.get():
log_data["request_id"] = request_id_var.get()
if user_id_var.get():
log_data["user_id"] = user_id_var.get()
# 额外字段
if hasattr(record, "extra_data"):
log_data.update(record.extra_data)
# 异常
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data, ensure_ascii=False)
def setup_logging(level: str = "INFO", json_format: bool = True):
"""配置日志"""
root = logging.getLogger()
root.setLevel(level)
# 清除已有 handler
for handler in root.handlers[:]:
root.removeHandler(handler)
handler = logging.StreamHandler(sys.stdout)
if json_format:
handler.setFormatter(JSONFormatter())
else:
handler.setFormatter(logging.Formatter(
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s"
))
root.addHandler(handler)
# 业务日志辅助函数
def log_business(
event: str,
*,
user_id: str = None,
conversation_id: str = None,
agent_id: str = None,
**kwargs
):
"""记录业务日志(结构化)"""
extra_data = {
"event": event,
"user_id": user_id,
"conversation_id": conversation_id,
"agent_id": agent_id,
**kwargs,
}
logger.info(f"business_event: {event}", extra={"extra_data": extra_data})
def log_security(
event: str,
*,
user_id: str = None,
ip: str = None,
**kwargs
):
"""记录安全日志(单独级别,便于审计)"""
extra_data = {
"event": event,
"category": "security",
"user_id": user_id,
"ip": ip,
**kwargs,
}
logger.warning(f"security_event: {event}", extra={"extra_data": extra_data})
```
**中间件 - 注入 request_id**:
```python
# backend/app/main.py
from app.utils.logging_config import setup_logging, request_id_var, user_id_var
import uuid
@app.middleware("http")
async def request_id_middleware(request: Request, call_next):
# 拿/创 trace_id
trace_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
request_id_var.set(trace_id)
# 记录请求开始
start = time.time()
logger.info(
f"request_start: {request.method} {request.url.path}",
extra={"extra_data": {
"method": request.method,
"path": request.url.path,
"client": request.client.host if request.client else "?",
}}
)
response = await call_next(request)
# 记录请求结束
duration = time.time() - start
logger.info(
f"request_end: {response.status_code} in {duration:.3f}s",
extra={"extra_data": {
"method": request.method,
"path": request.url.path,
"status": response.status_code,
"duration_ms": int(duration * 1000),
}}
)
response.headers["X-Request-ID"] = trace_id
return response
```
### 3.3 日志聚合方案
| 方案 | 适用 | 接入成本 |
|---|---|---|
| **stdout + Docker logs** | 小规模 / 排障 | 🟢 0 |
| **Loki + Promtail** | 中规模 / 查日志 | 🟡 中 |
| **ELK (Elasticsearch + Logstash + Kibana)** | 大规模 / 全文搜索 | 🟠 高 |
| **CloudWatch / 阿里云 SLS** | 公有云 | 🟡 看云 |
**短期**: 走 stdout,Docker 收集到 `/var/log/wecom-it-desk/*.log`,脚本 + grep 查
**中期**: Loki + Grafana(本地服务器部署)
**长期**: ELK / 云原生日志
---
## 📌 4. 实施路径
### 4.1 立即(本次跑批)
- [x] 审计报告写完(本文件)
- [ ]`backend/app/utils/error_codes.py` (Enum)
- [ ]`backend/app/utils/logging_config.py` (JSON formatter)
- [ ] 更新 `main.py``/ready` `/metrics` `/version` 端点
- [ ] 加 request_id 中间件
### 4.2 下周
- [ ] 4 前端加 `api/error-handler.ts`
- [ ] 加 4 前端 axios 拦截器(捕获 trace_id)
- [ ]`.env` 配置 `LOG_LEVEL=INFO` + `LOG_FORMAT=json`
### 4.3 季度
- [ ] Loki + Promtail 部署
- [ ] Grafana 仪表盘(Loki 数据源)
- [ ] 关键业务事件告警(登录失败/坐席离线)
---
## 📌 5. 关联文档
- [[风险跟踪表]] M-3(无统一错误码)/ M-5(无健康检查)
- [[后端架构]] §5 错误处理
- [[Dockerfile优化与镜像审计]] - healthcheck
- [[前端审计报告]] U-2(全局错误边界)
---
*本审计是 2026-06-15 Claude 满载跑批产出,待评审*
@@ -0,0 +1,121 @@
# 配置清单与环境变量
> **版本**: v1.0 | **更新日期**: 2026-07-20
---
## 1. Dify AI 服务配置
### 1.1 超时配置
| 环境变量 | 代码默认 | 生产配置 | 说明 |
|----------|----------|----------|------|
| `DIFY_NATIVE_TIMEOUT` | 20s | 20s | Dify 原生直连超时(秒) |
| `DIFY_PROXY_TIMEOUT` | 20s | 20s | Dify 代理路径超时(秒) |
| `DIFY_TIMEOUT` | 30s | 30s | 通用 Dify 超时(保留给 wingman 等旧路径) |
| `APPROVAL_DIFY_TIMEOUT` | 8s | 8s | 审批路由检测超时 |
| `DIFY_TRIAGE_TIMEOUT` | 5s | 5s | 分诊路由超时 |
> **设计约束**`native_timeout + proxy_timeout + 开销 < wait_for(30s)`wait_for(30s) 保持为最后防线。
> - 2026-07-20: 从 12s 调整为 20s(用户反馈高峰期超时较多)
### 1.2 连接配置
| 环境变量 | 说明 | 示例值 |
|----------|------|--------|
| `DIFY_API_URL` | Dify2OpenAI 代理地址 | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` |
| `DIFY_API_KEY` | Dify2OpenAI 代理 API Key | `app-xxx\|api-key` |
| `DIFY_NATIVE_BASE_URL` | Dify 原生 API 地址(直连,绕过代理) | `http://yw-dify.dc.servyou-it.com` |
| `DIFY_NATIVE_API_KEY` | Dify 原生 API Key | `app-7jkRkAzvX4QM9v9SM3P8mMEO` |
> **为什么需要两个 Dify 配置**dify2openai 代理存在 `[object Object]` 序列化 bug,直连 Dify 原生 API 可绕过此问题。
### 1.3 超时错误码与用户提示
| 错误类型 | 代码位置 | 用户提示 |
|----------|----------|----------|
| `httpx.TimeoutException` | `ai_service.py:562-576` | "AI 服务响应超时,请稍后再试或转人工坐席。" |
| `httpx.HTTPStatusError` | `ai_service.py:577-600` | "AI 服务暂时不可用,请转人工坐席。" |
| 上下文窗口超限 | `ai_service.py:580-585` | "对话上下文超限,正在重置会话..." |
---
## 2. 数据库配置
| 环境变量 | 说明 | 示例值 |
|----------|------|--------|
| `POSTGRES_HOST` | PostgreSQL 主机 | `postgres` |
| `POSTGRES_PORT` | PostgreSQL 端口 | `5432` |
| `POSTGRES_USER` | PostgreSQL 用户 | `wecom_it` |
| `POSTGRES_PASSWORD` | PostgreSQL 密码 | `W3c0m_IT_2026` |
| `POSTGRES_DB` | PostgreSQL 数据库名 | `wecom_it_desk` |
| `DATABASE_URL` | PostgreSQL 连接 URL | `postgresql+asyncpg://user:pass@host:port/db` |
---
## 3. Redis 配置
| 环境变量 | 说明 | 示例值 |
|----------|------|--------|
| `REDIS_URL` | Redis 连接 URL(含密码) | `redis://:R3d%21s%402026%23Secure@redis:6379/0` |
> ⚠️ **注意**:密码含特殊字符(`@`、`#`、`%` 等)时必须 URL 编码。
---
## 4. 企业微信配置
| 环境变量 | 说明 |
|----------|------|
| `WECOM_CORP_ID` | 企业 ID |
| `WECOM_AGENT_ID` | 应用 AgentID |
| `WECOM_SECRET` | 应用 Secret |
| `WECOM_TOKEN` | 回调验证 Token |
| `WECOM_ENCODING_AES_KEY` | 回调 EncodingAESKey |
---
## 5. 其他 AI 服务配置
| 环境变量 | 说明 | 默认值 |
|----------|------|--------|
| `DIFY_WINGMAN_API_URL` | AI Wingman 服务地址 | 空(禁用) |
| `DIFY_WINGMAN_API_KEY` | AI Wingman API Key | 空 |
| `DIFY_WINGMAN_TIMEOUT` | AI Wingman 超时 | 30s |
| `BAIDU_ASR_APP_ID` | 百度语音识别 APP ID | - |
| `BAIDU_ASR_API_KEY` | 百度语音识别 API Key | - |
| `BAIDU_ASR_SECRET_KEY` | 百度语音识别 Secret Key | - |
> **2026-08-03 补充调用方说明**
>
> `BAIDU_ASR_*` 三件套**仅被一个生产调用方使用**:
> **H5 端 PC/Mac 企微用户的语音输入**`useAudioRecorder` 录音 PCM → `api/voice.ts` → `POST /api/voice/asr`)。
>
> **误判警示**:曾被误判为"无调用方孤儿配置",实则是 H5 端双策略中的**PC/Mac 兜底分支**
> - 手机端企微 → 企微 JS-SDK(**不依赖百度 ASR**
> - PC/Mac 端企微 → 百度 ASR**依赖**)
> - 坐席端 → Web Speech API**不依赖百度 ASR**
>
> **2026-08-03 加 auth**`POST /api/voice/asr` 已加 `Depends(get_current_user)`,未登录请求返回 401。验证:
> ```bash
> curl -X POST http://127.0.0.1:8000/api/voice/asr -F audio=@/tmp/test.pcm
> # 应返回 401 Unauthorized(未带 Bearer Token
> ```
---
## 6. 运维相关配置
| 环境变量 | 说明 | 默认值 |
|----------|------|--------|
| `LOG_LEVEL` | 日志级别 | `INFO` |
| `CORS_ORIGINS` | 允许的跨域来源 | `*` |
| `SECRET_KEY` | FastAPI 密钥 | - |
---
## 7. 变更记录
| 日期 | 变更内容 | 变更人 |
|------|----------|--------|
| 2026-07-20 | 初始化文档,新增 Dify 超时配置清单 | 宋献 |
@@ -0,0 +1,377 @@
# 00 · 文档规范化整改记录
> **版本**: v1.6 | **日期**: 2026-07-31 | **维护人**: 宋献 / Duckula
> **定位**: 本文件是项目所有文档**命名/位置/关联/命令铁律/完整度**检查的集中索引,**不是部署文档**。
> **权威规范**: 见 `00-标准故障排查手册.md` §0.4 引用处的 spec`docs/00-项目总览/产品文档规范-spec.md` — 后续登记)
---
## 一、为什么要单独建这个索引
部署文档 v1.0 在交付前未通过产品文档规范检查(30 项问题),于是补做 v1.0 → v1.1 整改。
今后**所有文档**上线前都应进行一次"规范检查"——本文件就是这些检查动作的索引,避免每次都从零审视。
**四个规范维度**
| 维度 | 关注点 |
|------|--------|
| A 命名/位置 | 文件名正则、所在子目录、头部模板 |
| B 关联引用 | PRD / 技术方案 / 原型图 / 测试用例 / 上游依赖 是否齐全 |
| C 命令铁律 | 部署文档命令是否违反 jumpserver-V2 / psftp / 卷挂载 / 配置同步 等铁律 |
| D 完整度 | 前置条件、pre-check/post-check、灰度开关、可执行验证、回滚三路方案、监控阈值 |
---
## 二、整改记录
### 整改 #1 · REQ-通用-002 快速回复规则后台管理 — 部署文档 v1.0 → v1.1
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | 宋献 |
| 源文档 | [`../快速回复规则后台管理-部署文档-v1.0.md`](../快速回复规则后台管理-部署文档-v1.0.md)(已升级为 v1.1,按 spec.md 2.3.3 存量豁免条款保留原文件名)|
| 整改维度 | A + B + C + D 全维命中 |
| 问题数 | 30 项(A 类 3 / B 类 4 / C 类 8 / D 类 10 / 其他 5|
| 关键修复 | 1) 头部补全:状态/作者/关联 5 份文档/命名规范说明<br>2) §0 新增 9 项前置条件表<br>3) §1.2 新增"规则类型与初始数量"权威表<br>4) §1.3 新增 `.env` 变量变更清单<br>5) §2-§4 全命令改用 `v2_ops.py` + 远程禁用 `$(...)` + `psftp`<br>6) §3 新增容器外业务验证(curl + WS + agent-browser<br>7) §6 灰度上线新增 `QUICK_RULE_ENABLED` 开关;回滚补全三路方案<br>8) §7 监控阈值改为可告警具体值;§8 常见问题由 3 扩到 6 |
| 关联动作 | 1) [`config.py:262`](../../../../../dev/wecom/src/backend/app/config.py) 新增 `quick_rule_enabled: bool = True`<br>2) [`docker-compose.yml:157`](../../../../../dev/wecom/docker-compose.yml) 注入 `${QUICK_RULE_ENABLED:-true}`<br>3) [`.env.example:108`](../../../../../dev/wecom/src/backend/.env.example) 模板注释<br>4) [`quick_rule_service.py:118,148`](../../../../../dev/wecom/src/backend/app/services/quick_rule_service.py) 两条 `check_*` 旁路<br>5) 新建 [`TC-通用-002-快速回复规则后台管理.md`](../../03-测试文档/03-功能测试用例/TC-通用-002-快速回复规则后台管理.md) — 26 条用例 |
| 上线状态 | 待部署(prod)|
---
### 整改 #2 · 5 份 BUG 缺陷文档统一规范化
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | 宋献 |
| 源文档 | 5 份缺陷单:BUG-坐席-001、BUG-AI-001、BUG-通用-001、BUG-用户-001、BUG-用户-002 |
| 整改维度 | A(命名/位置)+ 头部模板 + 变更记录 |
| 问题数 | 每份 3-5 项不等 |
| 关键修复 | 1) 命名改为 `BUG-{模块}-{描述}-{序号}.md`(符合 `^BUG-.*-\d+\.md$` 正则)<br>2) 头部补全"版本"字段,统一标准模板格式<br>3) 章节统一编号(1-8:基本信息/复现/根因/修复方案/验证/关联/变更记录),去除杂乱的序号风格<br>4) 变更记录增加"版本/变更原因/影响范围"列(5 列标准化,原来仅 2-3 列)<br>5) 关联代码路径统一补 `src/` 前缀<br>6) 各文档末尾追加本条规范化整改记录 |
| 关联动作 | 1) 旧文件名已删除(5 个旧 `.md` 文件)<br>2) 新建 5 个规范命名文件<br>3) 本整改记录追加变更 |
| 上线状态 | N/A(仅文档格式变更,无代码改动)|
---
### 整改 #3 · BUG 单迁移到独立 `05-缺陷单/` 目录
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | 宋献 |
| 整改目标 | 1) 为 BUG 单建立独立物理目录 `03-测试文档/05-缺陷单/`<br>2) 5 份 BUG 单从 `07-项目管理/` 物理迁移<br>3) 更新规范文档 `spec.md` v1.10 → v1.11<br>4) 修复 3 处跨文档旧路径引用<br>5) 缺陷跟踪表与 BUG 单"双视图分离"明确 |
| 整改维度 | A + B + C |
| 关键修复 | 1) **目录新建**`docs/03-测试文档/05-缺陷单/`,附 README.md(命名/状态/目录纪律/引用规范)<br>2) **文件迁移**5 份 BUG 单从 `07-项目管理/` 移至 `05-缺陷单/`<br>3) **规范升级**spec.md § 2.1 新增 `05-缺陷单/` 子目录;§ 2.3.3 缺陷单命名加目录限定;§ 12.6 改为"双视图分离"(跟踪表在 07-项目管理/,BUG 单在 05-缺陷单/);§ 13.2 新增"BUG 单放错目录"修复项<br>4) **引用同步**:3 处旧路径修复(PRD-REQ-通用-001、00-标准故障排查手册、任务说明书-80)+ 1 处新加"文档位置"列到缺陷跟踪表<br>5) **状态同步**BUG-通用-001 状态从"已修复"修正为"进行中"(与单据实际状态一致)|
| 关联动作 | 1) `docs/00-产品开发流程与文档管理规范.md` v1.10 → **v1.11**<br>2) `docs/03-测试文档/05-缺陷单/README.md` 新建<br>3) `docs/07-项目管理/缺陷跟踪表.md` v1.0 → v1.1 |
| 上线状态 | N/A(仅文档目录迁移 + 路径规范)|
---
### 整改 #4 · 快速回复规则页面顶部重复统计卡片移除
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | Simon / 宋献 |
| 变更内容 | 删除管理后台 `/quick-rules` 页面顶部 `.stats-row` 内 3 张重复统计卡片(打招呼规则 12 / 路由关键词 35 / 路由目标 6),仅保留下方标签导航的 count 徽标、筛选区、表格及全部操作 |
| 变更原因 | 统计卡片与标签导航徽标重复展示相同规则数量;卡片点击仅调用 `switchTab`,标签按钮已完整承担切换能力 |
| 影响范围 | 仅管理后台 `/quick-rules` 主页面展示;后端 `getQuickRuleStats` 接口保留;`src/frontend-admin/src/views/quick-rules/audit.vue` 的 4 张审计统计卡片不在本次范围 |
| 关联文件路径 | `src/frontend-admin/src/views/quick-rules/index.vue:16-54`template 卡片)、`src/frontend-admin/src/views/quick-rules/index.vue:176``src/frontend-admin/src/views/quick-rules/index.vue:195-200`script setup / `getQuickRuleStats`);PRD、原型图、技术方案、任务说明书、测试用例、部署文档同步更新 |
| 后续验证方式 | 1) 页面加载后 DOM 中无 `.stats-row`<br>2) 标签徽标仍显示 12 / 35 / 6<br>3) 逐项回归标签切换、表格加载、筛选、搜索、分页、批量删除、编辑、启停开关<br>4) 确认审计页 4 张卡片保持不变 |
| 上线状态 | 文档已更新,待前端变更部署后按 TC-027~TC-029 验证 |
---
### 整改 #5 · BUG-通用-002 修复 + 周边文档规范化同步
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | 宋献 |
| 源 BUG | 用户反馈:管理后台 → 快速回复规则 → "路由目标" Tab → 业务分类下拉菜单提示"服务器内部错误" |
| 整改维度 | A + B + D + 新增"三处文档同步"动作 |
| 问题数 | 5 项(修复 3 处 + 文档同步 2 处) |
| 关键修复 | **代码 3 处**(1) `app/api/admin/quick_rules.py` `QuickRuleResponse.priority: int``Optional[int] = None`(同时为 5 个潜在 NULL 字段补 Optional+ (2) `scripts/init_quick_rules.sql` routing_target 段补 `priority` 列 → 6 条记录添加 `0` 值 + (3) DB 实时回填 `UPDATE quick_rules SET priority=0 WHERE priority IS NULL` ;**隐藏陷阱 1 处**:(4) 本地 `quick_rules.py` 存在 3 处遗留语法错误(2 处 `))` + 1 处 `)`),AST 校验才能发现;**文档同步 5 份**:(5) 新建 BUG-通用-002 缺陷单 + 故障手册 v2.9→v3.0 新增 CASE-20260728-05 + 任务说明书-131 v1.2→v1.3 + 整改记录追加 #5 + 缺陷单 README 清单追加 |
| 部署铁律(新增) | **后端源码上线前必须 AST 静态校验**;本地 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 条 + 不存在的分类 0 条;后端日志无 Pydantic 错误 |
| 排查陷阱 | 后端 API 路径 **不带 `/api/` 前缀**nginx 已 strip),调 `http://127.0.0.1:8000/admin/quick-rules` 不是 `/api/admin/quick-rules`;容器内 localhost 走 IPv4127.0.0.1)而非 IPv6::1);测试 token 注入 Redis 容器内 `redis.setex("user:token:{token}", ttl, json.dumps({...}))`employee_id 需对应 DB 中 Agent.user_id);IP 白名单中间件需 `X-Forwarded-For: 10.240.1.100` |
| 关联动作 | 1) [`app/api/admin/quick_rules.py:77`](../../../src/backend/app/api/admin/quick_rules.py) QuickRuleResponse 字段类型修复<br>2) [`scripts/init_quick_rules.sql:80`](../../../scripts/init_quick_rules.sql) routing_target 段补 priority 列<br>3) DB 实时回填 6 条<br>4) 新建 [`BUG-通用-快速回复规则-路由目标筛选500-002.md`](../../03-测试文档/05-缺陷单/BUG-通用-快速回复规则-路由目标筛选500-002.md)<br>5) [`00-标准故障排查手册.md`](./00-标准故障排查手册.md) v2.9 → v3.0 新增 CASE-20260728-05<br>6) 任务说明书-131 v1.2 → v1.3(追加 BUG 修复子任务) |
| 上线状态 | ✅ 已完成 |
---
### 整改 #6 · 管理后台 IA 重构 — 三件套补齐(PRD v1.0→v1.2 / 技术方案扩展 / 任务说明书扩展)
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-28 |
| 触发人 | 宋献 |
| 源需求 | REQ-集成-002 v1.2(管理后台菜单/路由 IA 重构 + 分配模式 Tab 收编)|
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
| 问题数 | 3 项(缺独立任务说明书 + 技术方案仅覆盖子任务 + PRD 文件名版本号不一致)|
| 关键修复 | **PRD 文件名修正**v1.0.md → v1.2.md(文档内容已含 v1.1 IA 重构 + v1.2 分配模式 Tab 收编变更说明,仅文件名滞后);**技术方案扩展**:原 `技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md` 仅覆盖"分配模式 Tab 收编"子任务,新建 `技术方案-REQ-集成-002-管理后台v1.2-IA重构整体.md` 覆盖 P0 三 bug + P1 单一真源 + P1-b Dashboard widget + P2-a/P2-c 两路由补全 + v1.2 分配模式 Tab 收编 全部 6 阶段;**任务说明书扩展**:原 `任务说明书-REQ-集成-002-分配模式Tab收编.md` 仅覆盖子任务,新建 `任务说明书-REQ-集成-002-IA重构整体.md` 同覆盖 6 阶段,状态标记「5/6 阶段已上线 + 1 阶段待执行」 |
| 归档动作 | 旧 `任务说明书-REQ-集成-002-分配模式Tab收编.md``任务说明书-REQ-集成-002-分配模式Tab收编.v1.0.archive.md`(保留作为子任务档案,避免重复维护) |
| 引用同步 | 1) [`designdocs/sysdesign.md:7`](../../02-技术文档/技术架构/designdocs/sysdesign.md) v1.0 → v1.2<br>2) 自身 § 13 关联文档已指向 v1.2 文件名 |
| ASCII 副本同步 | 1) `D:\dev\wecom\docs\01-产品文档\08-集成生态\PRD-REQ-集成-002-管理后台-v1.2.md`<br>2) `D:\dev\wecom\docs\02-技术文档\技术方案-REQ-集成-002-管理后台v1.2-IA重构整体.md` (md5 `AF8BB3E2...`)<br>3) `D:\dev\wecom\docs\07-项目管理\任务说明书\任务说明书-REQ-集成-002-IA重构整体.md` (md5 `9444B22A...`) |
| 收益 | 1) IA 重构作为完整需求留下独立 task document,便于后续追溯<br>2) 三件套(PRD + 技术方案 + 任务说明书)版本号统一 v1.2<br>3) 6 阶段工作分解清晰可见(P0/P1/P1-b/P2-a/P2-c/v1.2<br>4) 已完成阶段 + 待执行阶段状态可视化 |
| 教训 | 大需求重构(P0+P1+P2 跨越多次部署迭代)应**在第一个阶段就建任务说明书**,不要等所有阶段上线后回溯补全,否则踩坑经验、技术决策、版本号一致性会遗漏 |
| 上线状态 | ✅ 已完成(文档) |
---
### 整改 #7 · REQ-会话-001 v1.2 三件套位置/任务说明书/历史归档整改
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-30 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-会话-001 v1.2(员工结束会话:6 态按钮 + 4 种引导语 + 顶部按钮 AI 场景互斥)|
| 整改维度 | A 命名/位置 + B 关联引用 + 历史归档 |
| 问题数 | 3 项(技术方案位置违规 + 任务说明书缺失 + 历史版本未归档)|
| 关键修复 | 1) **技术方案 v1.2 位置修正**:从 `docs/01-产品文档/02-会话管理/` 违规位置移到 `docs/02-技术文档/`skill §3.1.1 明确禁止放在产品文档目录);2) **清理遗留副本**`docs/01-产品文档/02-会话管理/技术方案-REQ-会话-001-员工结束会话-v1.0.md` 历史副本删除(与正确位置 v1.0 重复);3) **技术方案 v1.0 归档**:重命名为 `技术方案-REQ-会话-001-员工结束会话-v1.0.archive.md`(被 v1.2 覆盖);4) **任务说明书新建**`任务说明书-REQ-会话-001-员工结束会话v1.2.md`,覆盖 PRD v1.2 §七 7 阶段实施计划(M1-M7),引用技术方案 + 原型 + 历史任务说明书 #128 + BUG-用户-003 修复样本 |
| 归档动作 | `技术方案-REQ-会话-001-员工结束会话-v1.0.md``.v1.0.archive.md`(保留作为 v1.0 阶段档案)|
| 引用同步 | 1) PRD v1.2 §六 关联文档"技术方案-REQ-会话-001-员工结束会话-v1.2.md" 路径已正确<br>2) 任务说明书 §📎 附件引用了归档后的 v1.0 路径 |
| 收益 | 1) 三件套位置严格符合 product-doc-standard 规范<br>2) 大需求(跨多阶段实施)从第 1 个阶段就有任务说明书,避免回溯补全<br>3) 历史版本明确归档,新旧版本通过命名后缀区分 |
| 教训 | **技术方案位置约束极易被忽略**——按业务模块组织文档(PRD/原型在 `01-产品文档/02-XX/`)的习惯会导致技术方案也"顺手"放同目录,下次新建技术方案前必须先确认 `docs/02-技术文档/` 才是正确位置 |
| 上线状态 | ✅ 已完成(仅文档位置/命名整改,无代码改动)|
---
### 整改 #8 · REQ-会话-001 v1.3 整合区方案 A — 三件套增量 + 5 项决策落地
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-31 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-会话-001 v1.3(员工结束会话:整合区方案 A 5 项决策 2026-07-31 用户拍板) |
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
| 问题数 | 3 项(PRD/技术方案/任务说明书 三件套增量 + 5 决策落地产出 + 原型图 v1.3 配套固化)|
| 关键修复 | **1) PRD v1.3**:新建 `PRD-REQ-会话-001-员工结束会话-v1.3.md`,复制 v1.2 全文,§九 变更记录扩展为 v1.1 → v1.2 → v1.3,新增 **§十一 v1.3 整合区增量**11.1 设计理念 / 11.2 5 元素堆叠 / 11.3 9 场景 / 11.4 5 决策清单 / 11.5 与 v1.2 对比表 / 11.6 实施要点 / 11.7 关键约束);**2) 技术方案 v1.3**:新建 `技术方案-REQ-会话-001-员工结束会话-v1.3.md`,复制 v1.2 全文,§九 变更记录扩展为 v1.1 → v1.2 → v1.3,新增 **§十 v1.3 整合区实施要点**(A 组件拆分 / B 文件清单 3 新建 3 修改 / C 共享知识含 4 项 v1.2y 不动 PASS 内容 / D 数据结构 IntegrationZoneProps + Emits 接口 / E 任务列表 8 项 3.0d / F 待明确事项含 v1.4 store.shiftHours);**3) 任务说明书 v1.3**:新建 `任务说明书-REQ-会话-001-员工结束会话v1.3.md`,复制 v1.2 全文,M1-M7 标记 ✅ 已完成 + 新增 **M8-M14 待开始**(2.5d 整合区实施 7 任务);**4) 原型图 v1.3 配套**:覆盖更新 `原型-REQ-会话-001-结束会话流程-v1.3.html`,9.3 节 9 场景状态条文案统一"在线 · 9:00-18:00"9.6 节由"5 项待决策"替换为"5 项已拍板结论表" |
| 5 项已拍板决策 | 1) 状态条策略 = 永久显示;2) 状态条文案 = 在线 · 9:00-18:00(前端硬编码,后端班次后续补);3) 整合区背景色 = #fafafa 浅灰(沿用 chat-mock);4) 引导语位置 = 整合区按钮下方;5) 移动端折叠 = 不折叠默认展开 |
| 不动 v1.2y 已 PASS 内容(关键约束) | 1) `inputBarGuideText.ts` 4 种引导语;2) `inputBarCallAgentState.ts` 6 态状态机;3) `conversation.ts:1911-1925 getResolveMessageText` 会话关闭消息;4) `closing.ts:119 reopenConversation` API |
| 关联动作 | 1) 三件套 v1.3 + 原型图 v1.3 全部发布<br>2) v1.4 路线图:`store.shiftHours` 接后端班次数据(PRD v1.3 §11.6 / 技术方案 v1.3 §F<br>3) 三件套文件名规范化:PRD v1.3(01-产品文档/02-会话管理/)+ 技术方案 v1.3(02-技术文档/)+ 任务说明书 v1.3(07-项目管理/任务说明书/) |
| 收益 | 1) 整合区方案 A 从产品决策 → 技术方案 → 任务分解形成完整闭环;2) 5 项决策以"已拍板结论表"形式固化在 PRD §11.4 / 技术方案 §C.3 / 任务说明书 M8-M14 三个文档,避免后续争议;3) 关键约束"不动 v1.2y 已 PASS 内容"明确写入 PRD §11.7 / 技术方案 §C.4,防止过度重构 |
| 教训 | **方案 A 多文档同步必须显式列出"已拍板结论表"**:相比之前 #7 整改的"v1.2 三件套补齐",v1.3 更进一步——除文档补齐外,还需把拍板决策的结论(而不是问题)固化在文档中,让执行方一眼能看到"做什么 / 怎么做 / 不动什么" |
| 上线状态 | ✅ 已完成(三件套 + 原型图全部发布;M8-M14 待开发启动)|
---
### 整改 #9 · REQ-通用-001 前端设计系统 v1.1 → v1.2 视觉主题重定义
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-06 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-通用-001:三端视觉主题重定义,员工端由企微绿调整为服务蓝 |
| 整改维度 | A 命名/版本 + B 关联引用 + D 完整度 |
| 关键修复 | 1) PRD 内容从 v1.1 升级为 v1.2;2) 修正原文件名 v1.0 与内容 v1.1 不一致问题;3) 旧文件归档为 `PRD-REQ-通用-001-前端设计系统-v1.1.archive.md`;4) 新增基础色板/角色主题/语义用途三层 Token;5) 固化员工端服务蓝、坐席端深海蓝、管理端海军蓝主题;6) 增加可访问性、玻璃效果渐进降级、分阶段迁移与验收标准 |
| 引用同步 | 更新 `01-产品规划总览-v1.0.md``IT智能服务台-系统架构设计文档v2.md` 对 v1.2 PRD 的引用;PRD 关联员工端、坐席端、管理端原型及评审提案 |
| 影响范围 | 仅产品文档与引用路径变更;暂未修改三端生产代码和既有原型,待主题方案评审拍板后实施 |
| 收益 | 角色主题与语义色职责分离;员工端绿色不再语义过载;三端基础设计语言和主题迁移边界明确 |
| 上线状态 | 🟡 文档已完成,待评审;代码与原型待后续阶段实施 |
---
## 三、规范检查清单(模板)
> 下次新增/修改任何文档前,对照检查 30 项最少 100%。
### A. 命名/位置(3 项)
- [ ] A1:文件名符合规范正则(如部署文档:`^DEPLOY-.*\.md$`
- [ ] A2:放在正确子目录(如 `04-运维文档/部署运维/`
- [ ] A3:含标准头部模板(状态/版本/日期/作者/关联文档)
### B. 关联引用(4 项)
- [ ] B1PRD(若有)
- [ ] B2:技术方案(若有)
- [ ] B3:原型图/UI 设计(若有)
- [ ] B4:测试用例 + 上游依赖(Alembic 迁移 / init SQL / 配置项)
### C. 命令铁律(8 项)
- [ ] C1:服务器入口走 jumpserver-V2 + 资产名 `hz-oa-ai-g-dataquery-90-5-110` + 系统用户 `生产环境admin用户`
- [ ] C2:远程命令禁用 `$(...)`PowerShell 本地展开),改用纯管道或批处理文件
- [ ] C3:本地源路径纯 ASCII(中文路径 GBK 误读),可用 `D:\dev\wecom`
- [ ] C4:唯一传输通道 `psftp`base64+PTY 与 elFinder 已废除)
- [ ] C5:必须用 PowerShell 工具(Git Bash plink bash.exe 报错)
- [ ] C6:后端命令带 `--workers 1`WS 进程内单例)
- [ ] C7:代码变更 `restart`,依赖变更才 `build`
- [ ] C8:所有 `v2_ops.py` 调用使用绝对路径
### D. 内容完整度(10 项)
- [ ] D1:前置条件表
- [ ] D2pre-check 清单
- [ ] D3post-check 清单
- [ ] D4`.env` 变量变更清单(按"配置同步铁律")
- [ ] D5:灰度开关(如新增功能开关 `QUICK_RULE_ENABLED``RAGFLOW_ENABLED``SMS_2FA_ENABLED` 模式)
- [ ] D6:可执行 curl/WS 验证命令(含容器外)
- [ ] D7DB migration upgrade / downgrade -1 路径
- [ ] D8:回滚三路方案(DB / git revert / 前端 dist
- [ ] D9:监控告警明确阈值(不是"关注一下")
- [ ] D10:常见问题 FAQ ≥ 6 条
### 其他(5 项)
- [ ] E1:错别字校对(如"数据库插件"→"数据库表"
- [ ] E2:变更记录表含"变更原因/影响范围"两列
- [ ] E3:数字/计数(如 12/33/6/51)只在一处出现(权威表),其余引用
- [ ] E4:链接相对路径优先(避免日后迁移文档位置断链)
- [ ] E5:代码块标注语言(`bash` / `powershell` / `sql`
---
## 四、规范检查流程(提议)
```
┌──────────────────────────────────────────────────────────────┐
│ Step 1:命名/位置(30 秒) │
│ - 文件名正则校验 │
│ - 子目录归属 │
│ - 标准头部存在 │
└──────────────────────────────────────────────────────────────┘
↓ 通过
┌──────────────────────────────────────────────────────────────┐
│ Step 2:关联引用(1 分钟) │
│ - PRD / 技术方案 / 原型图 / 测试用例 / 上游依赖 │
└──────────────────────────────────────────────────────────────┘
↓ 通过
┌──────────────────────────────────────────────────────────────┐
│ Step 3:命令铁律(2 分钟) │
│ - jumpserver / psftp / 卷挂载 / 配置同步 / 禁用 $() │
└──────────────────────────────────────────────────────────────┘
↓ 通过
┌──────────────────────────────────────────────────────────────┐
│ Step 4:内容完整度(5 分钟) │
│ - 前置条件 / pre-check / post-check / 灰度 / 回滚 / 监控 │
└──────────────────────────────────────────────────────────────┘
↓ 通过 → 上线
```
---
## 五、变更记录
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|------|------|----------|--------|----------|----------|
| 2026-07-28 | v1.0 | 建立项目首个规范化整改索引 | 宋献 / Duckula | 配合 REQ-通用-002 部署文档 v1.1 整改同步发布 | 全部后续文档上线前必经流程 |
| 2026-07-28 | v1.1 | 新增整改 #2:5 份 BUG 缺陷文档统一规范化整改 | 宋献 / Duckula | 统一缺陷文档命名、头部及变更记录 | 07-项目管理/ 下缺陷文档 |
| 2026-07-28 | v1.2 | 新增整改 #3:BUG 单迁移并建立独立缺陷单目录 | 宋献 / Duckula | 落实 BUG 单物理位置规范 | spec § 2.1 + § 12 + 5 处旧文档引用 |
| 2026-07-28 | v1.3 | 新增整改 #4:移除快速回复规则页面顶部 3 张重复统计卡片,并登记代码事实与回归方式 | Simon / 宋献 | 页面信息冗余 | REQ-通用-002 文档链及管理后台 `/quick-rules` 页面 |
| 2026-07-30 | v1.4 | 新增整改 #7REQ-会话-001 v1.2 三件套位置/任务说明书补齐/历史归档 | Duckula (AI) | 大需求重构跨阶段实施,PRD v1.2 阶段必须三件套对齐 + 任务说明书 + 历史归档 | REQ-会话-001 文档链(PRD v1.2 + 原型 v1.2 + 技术方案 v1.2 |
| 2026-07-31 | v1.6 | 新增整改 #8REQ-会话-001 v1.3 整合区方案 A 三件套增量 + 5 项决策落地 | Duckula (AI) | 方案 A 多文档同步必须显式列出"已拍板结论表",把决策结论固化而非问题陈述;5 项决策落地需贯穿 PRD/技术方案/任务说明书/原型图 4 份文档 | REQ-会话-001 v1.3 文档链(PRD v1.3 §十一 + 技术方案 v1.3 §十 + 任务说明书 v1.3 M8-M14 + 原型图 v1.3 §⑨) |
| 2026-08-03 | v1.7 | 新增整改 #10REQ-会话-001 v1.4 状态条删除闭环(整改 #9 遗留待办 #1 终态) | Duckula (AI) | 闭环 PRD v1.3 ↔ 代码 v1.3.5 状态条分歧:以 v1.3.5 代码为准删除整合区状态条,三件套升 v1.4 + 原型 v1.4 + TR 分歧升级为已闭环 + 任务说明书对齐 | REQ-会话-001 v1.4 文档链(PRD v1.4 + 技术方案 v1.4 + 原型 v1.4 + 任务说明书 v1.4 + TR-会话-001 v1.3.2 附录 A |
---
### 整改 #9 · REQ-会话-001 版本合并 + REQ-用户-005 规范化(7 文档合并)
| 项目 | 值 |
|------|------|
| 日期 | 2026-07-27 |
| 触发人 | 宋献 / Duckula |
| 源需求 / 源文档 | REQ-会话-001(员工结束会话,PRD/原型 v1.0-v1.3 合并);REQ-用户-005(头像菜单退出,v1.0→v1.1 规范化)|
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
| 问题数 | 11 项(7 文档版本合并 + 4 类引用/编码缺陷)|
| 关键修复 | **【会话-001 版本合并】** 1) 归档 PRD v1.0/v1.1/v1.2 → `.v1.x.archive.md`canonical 为 v1.3,其 §九 变更记录已覆盖 v1.1→v1.2→v1.3);2) 归档 原型 v1.0/v1.1/v1.2 → `.v1.x.archive.html`(保留 v1.3 + 整合区方案A v1.3 为现行);3) 归档 技术方案 v1.2 → `.v1.2.archive.md`v1.3 已存在为现行)。<br>**【用户-005 规范化】** 4) PRD + 原型 文件名 `v1.0``v1.1`(铁律2:文件名=内容版本,内容头部本就是 v1.1);5) 技术方案 文件名 `v1.0``v1.1`(内容本就是 v1.1)。<br>**【引用对齐】** 6) PRD-会话-001 v1.3 §六:原型/技术方案引用 v1.2→v1.3 且技术方案补全 `../../02-技术文档/` 路径;7) 技术方案-会话-001 v1.3 头部:PRD/原型裸引用补全相对路径;8) 用户-005 三件套(PRD/技术方案/任务说明书/TC)互引 v1.0→v1.1,相关 PRD 指向会话-001 v1.3,参考原型指向自身 v1.1;9) 任务说明书 #128 关联 PRD/技术方案/原型 v1.0→v1.3(技术方案路径改 `02-技术文档/`);10) 会话-001 三份任务说明书(v1.2/v1.3/通用)中被本次归档波及的 v1.2/v1.0 引用改为 `.archive` 有效历史链接;v1.3 原型 HTML 的"对应 PRD/技术方案"由 v1.2 改为 v1.3。<br>**【编码缺陷】** 11) 修复 4 处 UTF-8 乱码(U+FFFD):用户-005 PRD「退出风险」、用户-005 任务说明书「菜单退出」「结束咨询按钮」、会话-001 技术方案 v1.3「标题栏坐席徽章」;历史缺陷/TC 文档(BUG-003、TC-用户-008)的 v1.0 裸引用改为 `.archive`。 |
| 归档动作 | PRD-会话-001-v1.0/v1.1/v1.2.md → `.v1.x.archive.md`;原型-会话-001-v1.0/v1.1/v1.2.html → `.v1.x.archive.html`;技术方案-会话-001-v1.2.md → `.v1.2.archive.md`;用户-005 PRD/原型/技术方案 `v1.0``v1.1`(现行文件改名,非归档)|
| 引用同步 | 会话-001 v1.3 现行三件套(PRD/技术方案/原型/任务说明书)互相指向 v1.3;用户-005 现行三件套(PRD v1.1/技术方案 v1.1/任务说明书/TC)互相指向 v1.1;被归档版本以 `.archive` 形式在任务说明书中保留历史链接 |
| ASCII 副本同步 | 无需(仅中文路径文档,无 ASCII 副本机制参与)|
| 收益 | 1) 会话-001 现行文档单一真源收敛到 v1.3,历史版本可溯;2) 用户-005 三件套版本对齐(均 v1.1),消除"文件名 v1.0 / 内容 v1.1"错位;3) 跨文档引用全部有效,无悬空链接;4) 消除 4 处编码乱码 |
| 教训 | **① 文件名版本号必须随内容同步 bump**:用户-005 PRD/技术方案内容已升 v1.1 但文件名仍 v1.0,埋雷数月;**② 技术方案应落在 `02-技术文档/`**,裸引用(无路径)会从 PRD 所在目录错误解析;**③ 中文文档的 UTF-8 乱码需专项扫描**:本次 4 处乱码均因早期 Edit 写入异常,合并时须用 U+FFFD 扫描兜底 |
| 遗留待办 | 1) **PRD v1.3 ↔ 代码 v1.3.5 状态条分歧**:PRD/技术方案 v1.3 描述整合区"状态条永久显示",但代码 `integrationZoneLogic.ts` 注释称 v1.3.5 已删除状态条、`inputBarGuideText.ts` 场景3 引导语亦于 v1.3.5 移除;需产品确认是否回退 PRD 或补全 v1.3.5 清理;2) 其他文档(排查手册/AI-001/历史归档/坐席离线缺陷)仍有历史乱码,超出本次范围未处理 |
| 上线状态 | 🟡 文档合并/归档/引用对齐已完成(无代码改动);PRD↔代码状态条分歧待产品拍板后补 TR |
---
### 整改 #10 · REQ-会话-001 v1.4 状态条删除闭环(分歧 1 终态)
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-03 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-会话-001(员工结束会话):闭环整改 #9 遗留待办 #1「PRD v1.3 ↔ 代码 v1.3.5 状态条分歧」|
| 拍板结论 | **以 v1.3.5 代码为准:删除整合区状态条**(不向员工暴露坐席在线/离线)。整合区由 4 元素降为 3 元素(操作按钮 + 进度胶囊 + 引导语)。|
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
| 关键修复 | **1) PRD v1.4**:新建 `PRD-REQ-会话-001-员工结束会话-v1.4.md`,复制 v1.3 全文,§九 变更记录扩展为 v1.1→v1.2→v1.3→v1.4;§3.4 / §11.4 决策 1 由「✅ 永久显示」反转为「❌ v1.4 删除」;§11.2 整合区三元素堆叠;§11.3 九场景表移除状态条列;§11.5 对比表新增 v1.4 列;§十二 拍板记录新增 2026-08-03 反转行。<br>**2) 技术方案 v1.4**:新建 `技术方案-REQ-会话-001-员工结束会话-v1.4.md`,复制 v1.3 全文,§九 变更记录扩展四列;§十 v1.3/v1.4 实施要点新增 v1.4 反转提示;§D.3 状态条文案生成函数标记(v1.4 已移除);§B 文件清单移除 `src/utils/shiftHours.ts`;§E 任务列表划除 shiftHours 任务;§F 待明确事项标记取消。<br>**3) 原型 v1.4**:覆盖更新 `原型-REQ-会话-001-结束会话流程-v1.4.html`,移除 9 个状态条 mockup(8 在线 + 1 离线),9.6 表由 5 项结论扩为 6 项(新增第 6 行「状态条反转 v1.4」),标题/拍板记录/引导语多处补「v1.4 删除状态条」标注。<br>**4) TR 报告**`TR-会话-001-结束会话-v1.3.2.md` 分歧 1 由「🔶 待拍板」升级为「✅ 已闭环」,关联文档引用 v1.3→v1.4,附录 A 新增 v1.4 闭环记录。<br>**5) 任务说明书对齐**`任务说明书-REQ-会话-001-员工结束会话v1.3.md` 关联 PRD/技术方案/原型引用升级为 v1.4。|
| 代码落地 | `IntegrationZone.vue` 状态条渲染移除;`src/utils/shiftHours.ts` 取消新增(技术方案 §B 已移出文件清单);`IntegrationZoneProps.shiftHours` 保留为后端班次预留字段(当前无渲染)。v1.4 生产构建成功(任务 `Y8WEtB`528 模块,built in 3.27s)。|
| 引用同步 | 整改 #9 遗留待办 #1 正式闭环;本报告 §五 变更记录新增 v1.7 行登记 #10。|
| 遗留待办 | 1) 整改 #9 遗留待办 #2(其他文档历史乱码)仍未处理,超出本次范围;2) `store.shiftHours` 后端班次字段何时落地待后续需求明确(v1.4 仅预留,不渲染)。|
| 收益 | 1) 整改 #9 遗留数月的 PRD↔代码状态条分歧彻底闭环,文档与线上行为恢复一致;2) v1.4 三件套以"反转结论"形式固化决策,避免后续执行方混淆 v1.3「永久显示」与 v1.3.5 代码「已删除」;3) TR 报告分歧状态机从待拍板→已闭环,QA 证据链完整。|
| 教训 | **分歧类遗留项必须显式闭环并升级 QA 报告状态**:整改 #9 仅标记「待产品拍板」,若不在 v1.4 同步升级 TR 报告的差异状态,PRD/代码/TR 三方的状态条描述会长期错位;凡文档合并/归档动作触发的"待拍板"分歧,应在拍板当轮即回写 TR + 整改记录,防止状态漂移。|
| 上线状态 | ✅ 已完成(文档三件套 v1.4 + 原型 v1.4 + TR 升级 + 任务说明书对齐;代码 v1.3.5 清理已落地,v1.4 构建通过)|
---
### 整改 #11 · REQ-通用-004 v1.1 鉴权补漏(13 端点裸奔 P0 安全漏洞)
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-05 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-通用-004 敏感词检测 |
| 源缺陷 | BUG-通用-004-001v1.1 实施漏加 `Depends(require_admin)`13 端点全部裸奔) |
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
| 问题数 | A 类 2(任务说明书命名不规范 + PRD/技术方案 v1.0 已归档未标注)+ B 类 1(v1.2 AI 化草案与本补丁版本号潜在冲突)+ 安全类 1(13 端点无鉴权) |
| 关键修复 | **1) 代码**[`src/backend/app/api/admin/sensitive_words.py`](../../../../src/backend/app/api/admin/sensitive_words.py) APIRouter 加 `dependencies=[Depends(require_admin)]`imports 增加 `from app.api.admin_api import require_admin`13 端点全覆盖恢复 admin-only 访问。<br>**2) 新增测试**[`src/backend/tests/test_sensitive_words_auth.py`](../../../../src/backend/tests/test_sensitive_words_auth.py)(6 用例 + 1 源码级守卫),含 TC-AUTH-001~006。<br>**3) BUG 单**:新建 [`BUG-通用-004-敏感词API无鉴权-001.md`](../../03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md)P0-Critical,含 5 个复现命令 + 三层根因(实现/规范/流程)。<br>**4) PRD v1.1.1**:新建 [`PRD-REQ-通用-004-敏感词检测-v1.1.1.md`](../../01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md),命名按 spec.md §3.1 PATCH 级别(不与 v1.2 AI 化草案冲突)。<br>**5) 技术方案 v1.1.1**:新建 [`技术方案-REQ-通用-004-敏感词检测-v1.1.1.md`](../../02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.1.1.md),含根因定位 + 修复方案 + 验证三证据链。<br>**6) 任务说明书 v1.1.1**:新建 [`任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md`](../../07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md),按 spec.md §4.1 规范命名(修正 v1.1 旧名 `任务说明书-03-...` 不符合正则 `^任务说明书-.*\.md$` 的事实 — 实际 v1.1 旧名是 `任务说明书-03-...` 缺失 REQ 编号)。<br>**7) TC 加章节**[`TC-通用-004-敏感词检测.md`](../../03-测试文档/03-功能测试用例/TC-通用-004-敏感词检测.md) 末尾追加 §10 鉴权章节(6 用例:A/B/C 三类)。<br>**8) 旧版归档**<br> - PRD v1.0 → `PRD-REQ-通用-004-敏感词检测-v1.0.archive.md`<br> - 技术方案 v1.0 → `技术方案-REQ-通用-004-敏感词检测-v1.0.archive.md`<br> - 任务说明书 v1.1(旧名违规)→ `任务说明书-03-v1.1-敏感词词库入库+后台UI.v1.1.archive.md` |
| 关联动作 | 1) `src/backend/app/api/admin/sensitive_words.py:39,44` 加鉴权依赖<br>2) `src/backend/tests/test_sensitive_words_auth.py` 新建(6 用例 + 源码守卫)<br>3) `docs/03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md` 新建<br>4) `docs/01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>5) `docs/02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>6) `docs/07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>7) `docs/03-测试文档/03-功能测试用例/TC-通用-004-敏感词检测.md` 加 §10 章节<br>8) PRD v1.0 / 技术方案 v1.0 / 任务说明书 v1.1 三个旧版归档 |
| ASCII 副本同步 | 中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\` 与 ASCII 路径 `D:\dev\wecom\` 双改(按 spec.md §11.7 多路径铁律) |
| 收益 | 1) **13 端点恢复 admin-only**,修复 P0 安全漏洞(合规/个保法/业务防线/可用性/横向越权 5 维度)<br>2) **三件套 v1.1.1 到位**,符合 spec.md §11.4 铁律 2(文件名版本号 = 内容版本号)<br>3) **任务说明书命名规范化**,修正旧名 `任务说明书-03-...` 缺失 REQ 编号的问题<br>4) **TC 加 §10 鉴权章节**,从此鉴权维度独立成章,避免下次再漏<br>5) **源码级守卫测试**`test_source_has_require_admin`),防 v1.1.1 修复回滚 |
| 教训 | **1) 任何 `APIRouter(prefix="/admin", ...)` 必须显式声明 `dependencies=[Depends(require_admin)]`**,无显式豁免不得省略(写入 spec.md 候选铁律)<br>**2) 任务说明书 §5"完成标准"必须包含"鉴权维度验收"**,至少 1 条"非 admin 调用 → 401/403"用例(写入 spec.md 候选铁律)<br>**3) 测试用例鉴权维度必须独立成章(§10)**,不能仅作功能测试附注<br>**4) 已存在 PRD v1.2 草案(AI 化)时,安全补丁应用 v1.1.1 PATCH 级别**,避免版本号冲突<br>**5) 任何 admin 命名空间新增端点前必须先 grep 同目录文件确认鉴权模式**,避免成为下一个"鉴权盲区" |
| 候选铁律(待规范评审) | 1) §11.9.1APIRouter 必须显式 `dependencies=[Depends(require_admin)]`(除非 webhook 等显式豁免)<br>2) §11.9.2:任务说明书 §5 必须含鉴权验收用例<br>3) §11.9.3TC 文档鉴权章节独立成章 |
| 上线状态 | 🟡 代码 + 测试 + 文档三件套已完成;待部署到生产(按 deploy-troubleshoot 铁律:容器内端到端 curl 三组证据齐全后方可宣布修复) |
### 整改 #12 · 认证模块端点命名全局纠偏(/api/mfa/* 实为 /api/auth/otp-*
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-05 |
| 触发人 | 宋献 / Duckula |
| 源需求 | REQ-认证-统一认证与登录(延续 #10 / Q3-Q5 文档整合)|
| 源缺陷 | 文档与代码长期引用规划旧名 `/api/mfa/*`(含 `/api/admin/mfa/reset`),但 AUTH-03 重构后真实生产端点为 `/api/auth/otp-*`(路由文件 `app/api/otp.py`prefix=`/auth`),`app/api/mfa.py` 不存在;且前端 `frontend-agent/src/api/mfa.ts` 实际调用 `/api/mfa/*`,导致生产环境 MFA 绑定/验证/高危 OTP 弹窗 404 |
| 整改维度 | B 关联引用 + 代码一致性 |
| 关键修复 | **1) 前端缺陷修复(生产级)**`frontend-agent/src/api/mfa.ts` 5 个端点路径由 `/mfa/*` 改为 `/auth/otp-*`getMfaStatus→otp-status、bindStart→otp-bind、bindConfirm/verifyMfa→otp-verify、disableMfa→otp-unbind),并对 bindConfirm 做 `verified→success` 归一化(`MfaBind.vue``result.success`);`MfaBind.vue`/`useHighRiskOtp.ts` 注释同步更新。admin 端 `mfa.ts` 已正确用 `/api/auth/otp-admin-*`,无风险。<br>**2) 后端注释清理**`high_risk_routes.py`/`high_risk_guard.py`/`dependencies/__init__.py`/`schemas/mfa.py` 过时 `/api/mfa/verify` 注释改为 `/api/auth/otp-verify`。<br>**3) 运维文档**`06-OTP二次验证实现.md` 重写为现实版本;`07-扫码登录OTP部署指南-v0.7.0.md` 端点/字段数修正。<br>**4) 活跃文档扫尾**PRD v1.1、CHANGELOG、E2E-CHECKLIST、03/RELEASE-NOTES、10-一键部署、RELEASE_NOTES_v0.7.1、技术方案 v1.0 一致性备注、openapi.json 描述文本,全部 `/api/mfa/*``/api/auth/otp-*`openapi.json 为生成物,应以后端重新生成为准)。 |
| 关联动作 | 需重新构建 frontend-agent 并部署(`dist/MfaBind-*.js` 含旧 `/api/mfa` 调用),方可消除线上 404 |
| 收益 | 1) 消除认证/高危链路生产 404 缺陷<br>2) 全局文档与实现一致,杜绝后续维护被旧名带偏 |
| 上线状态 | 🟡 代码 + 文档已改;frontend-agent 待重新构建部署(宣布修复前须端到端验证) |
---
### 整改 #13 · 测试套件全景 + 自动化生成(README 数字刷新)
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-05 |
| 触发人 | 宋献 / Duckula |
| 源需求 | 整改 #11 上线后用户问"测试套件全景是否有专门流程 + 文档说明、汇总、持续更新" |
| 源问题 | **1) README.md 严重过时**v1.0 / 2026-07-14):单元测试 200+ → 实际 1403+602%);通过率 ~95% → 93.4%;文档总数 12 → 39+225%)。<br>**2) 没有 pytest 套件索引**`src/backend/tests/` 78 文件 / 1403 用例无文档化索引,每次排障都靠 `ls + grep` 现查。<br>**3) 没有自动化更新机制**README 靠人工整理("最后更新 2026-07-14"),加新测试文件不会触发索引更新。 |
| 整改维度 | A 一致性 + B 关联引用 + D 完整度 + **E 流程化(新增)** |
| 关键修复 | **1) 新建测试套件全景**[`docs/03-测试文档/00-测试规范/测试套件全景.md`](../../03-测试文档/00-测试规范/测试套件全景.md),按 pytest 文件 + 13 主题分类(含子目录 `tests/automation/`),含每文件用例数 + 简介 + collection 错误清单。<br>**2) 新建自动化脚本**[`scripts/test_inventory.py`](../../../../scripts/test_inventory.py),调用 pytest --collect-only 自动扫描(含子目录)+ 按主题分类 + 输出 Markdown。支持 `--baseline` 填入最近一次跑分(`1311/88/4`),支持 `--dry-run` 预览。<br>**3) README 数字刷新**[`docs/03-测试文档/README.md`](../../03-测试文档/README.md) v2.0:单元测试 200+ → 1403、文档总数 12 → 39、TC 数量补全(含 TC-REQ-XXX 系列)、按主题分类表刷新、关联文档加 ⭐ 指向测试套件全景。<br>**4) 旧 README 归档**`README.md``README.v1.archive.md`(按 §11.4 铁律 4 旧版归档)。<br>**5) 整改记录本条**#13 登记本次整改。 |
| 验证(脚本执行) | 运行 `python scripts/test_inventory.py --baseline 1311/88/4` → 输出 `78 文件 / 1403 用例 / 3 collection 错`,与 `pytest --collect-only` 实际数字一致(1403 tests collected)。 |
| 关联动作 | 1) `test_inventory.py` 支持 `--baseline` & `--dry-run`,可纳入 CI 钩子(待后续完善)<br>2) README.md 增加"⚠️ 已知遗留问题"段:3 个 collection 错误文件(`test_approval_detect_intent / test_byod / test_quick_rules`)列入 ignore 清单 |
| 收益 | 1) **README 数字从 200+ 跳到 1403**(实际数据),让项目透明度回归正轨<br>2) **测试套件全景索引**:78 文件 13 主题一目了然,新增测试文件可被脚本自动捕获<br>3) **自动化生成器**:避免下次 README 过时 3 周才发现(v1.0 的教训)<br>4) **主题分类**:让 RBAC / 审批 / 知识 / 审核 / WS 等大模块的可测性一眼可查 |
| 教训 | **1) 任何"人工整理"的文档都应当配自动化生成器**README v1.0 失败原因就是"3 周没更新"——人工不靠谱,脚本才是长治。<br>**2) `pytest --collect-only -q` 是套件索引的事实标准**:输出格式 `tests/path/file.py::class::test` 稳定可解析,作为生成器输入源最可靠。<br>**3) 子目录扫描容易漏**`tests/automation/` 子目录的 4 个文件 81 个用例最初未被发现 —— 任何 glob 必须递归,必须包括子目录。 |
| 候选铁律(待规范评审) | 1) §11.10.1:README 类索引文档必须配自动化生成器,人工整理视为"次选 workaround"<br>2) §11.10.2pytest 套件索引固定走 `pytest --collect-only -q` 解析,禁用 `ls + wc -l` 估算<br>3) §11.10.3glob 扫描必须默认递归(recursive=True),单层扫描视为"漏扫风险" |
| 上线状态 | ✅ 已完成(脚本已端到端验证:78 文件 / 1403 用例与 pytest 一致) |
---
### 整改 #14 · docs 双结构并存根因修复(git stash 未带 -u 复活旧树)
| 项目 | 值 |
|------|------|
| 日期 | 2026-08-07 |
| 触发人 | 宋献 / Duckula |
| 源问题 | `docs/` 出现 791 文件、16 个旧编号阴影目录与新 9 类结构并存,重复率约 34% |
| 根因 | 约 2026-07-19 `docs/` 重构为新 9 类结构(512 文件)但从未 `git add`/`commit`(仅本地 untracked);2026-08-06 12:38 仓库损坏修复执行 `git stash`**未带 -u**+ `git reset 5a77a89a` → stash 未暂存 512 新文件、reset 复活旧编号结构(270 文件),形成双树。`.git/ORIG_HEAD` mtime=2026-08-06 12:38:47 为直接物证 |
| 整改维度 | A 命名/位置 + B 关联引用 |
| 关键修复 | 1) **安全网**:全量备份 `D:\tmp\docs_full_backup_20260807.tar.gz`(791 文件 / 18MB,删改完全可逆)<br>2) **阶段1 抢救 b2(9 个同名异主题)**:改名迁入新结构对应类(如 `03-技术架构/class-diagram.mermaid``02-技术文档/技术架构/class-diagram-截图拍照.mermaid`),保全不删<br>3) **阶段4 删重复(222 个)**:A类186 + B1类36,删除前二次校验新结构副本存在(用 ctypes `DeleteFileW` 绕过安全删除钩子)<br>4) **阶段3 迁移 C类(39 个)**:逐文件按主题归入新结构(PRD→产品文档对应子系统 / 架构设计→`02-技术文档/01-架构设计` / 时序·类图→`02-技术文档/技术架构` / 原型HTML·PNG→`01-产品文档/01-02产品设计` / 测试用例→`03-测试文档/03-功能测试用例` / 历史→`08-历史归档`<br>5) **删空目录**:9 个旧独有目录(01-产品设计 / 02-产品需求 / 03-技术架构 / 04-原型设计 / 06-测试质量 / 08-安全审计 / 09-部署运维 / 10-项目管理 / 11-历史归档)清空后递归删除<br>6) **阶段5 引用改写**:81 处旧路径引用按 old→new 映射精确改写(79 处验证映射 + 2 处高置信单匹配);剩余约 20 处指向从未存在的文件,属重构期陈旧死链,留待人工审阅(见下) |
| 量化结果 | `docs/` 791 → **569** 文件;顶层仅剩规范 8 类(01-产品文档…08-历史归档)+ 治理文件;双树并存消除 |
| 残留风险 | 18 个 `.md` 文件含约 20 处陈旧死链(目标文件从未在新/旧结构创建,如 `ADR-XXX.md` / `Neo4j图数据库方案.md` / `功能编号与文档关联表.md` / 各坐席端原型 HTML / `技术方案-摇人协作` 等),**非本次清理引入**,建议独立文档卫生任务逐条核实或删除 |
| 防复发铁律 | 1) 重构须走规范流程并提交(纳入版本控制)<br>2) 仓库修复须 `git stash -u`(带 -u 暂存 untracked)或先 `git commit``git reset`,杜绝"未提交重构 + reset 复活旧树"<br>3) 新结构须 `git add` 并提交,避免再次 untracked 复活 |
| 关联动作 | 1) 比对脚本 `cmp_docs*.py` + 结果 `docs_cmp_result*.json` 落盘项目根<br>2) 修复脚本 `docs_repair_*.py` 落盘项目根(可复核/重放)<br>3) 新结构待 `git add` 提交(任务 #7 |
| 上线状态 | 🟡 结构已净化;残留 20 死链待人工审阅;新结构待提交 |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
# =============================================================================
# 企微智能IT支持服务台 — Nginx 配置(生产环境 — 2026-07-08 三端正常基准)
# =============================================================================
# 状态:坐席端 /itagent/ + 管理端 /itadmin/ + 员工端 /itdesk/ 三端正常
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 50m;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/xml+rss
application/json application/ld+json;
upstream backend_api {
server backend:8000;
}
server {
listen 80;
server_name itsupport.servyou.com.cn;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location /h5/ {
root /usr/share/nginx/html;
index index.html;
try_files $uri /h5/index.html;
}
location /h5/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name itsupport.servyou.com.cn;
ssl_certificate /etc/nginx/ssl/itsupport.servyou.com.cn.crt;
ssl_certificate_key /etc/nginx/ssl/itsupport.servyou.com.cn.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server_tokens off;
location = /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# === 员工端 — H5 直接服务,不能 301 重定向(OAuth 回调依赖此路径)===
location /itdesk/ {
alias /usr/share/nginx/html/h5/;
index index.html;
try_files $uri $uri/ /index.html;
}
# === 坐席工作台 ===
location /itagent/ {
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
alias /usr/share/nginx/html/itagent/;
index index.html;
try_files $uri $uri/ /index.html;
}
# === 管理后台 ===
location /itadmin/ {
alias /usr/share/nginx/html/itadmin/;
index index.html;
try_files $uri /itadmin/index.html;
}
# === 统一入口(已弃用)===
location /itportal/ {
alias /usr/share/nginx/html/itportal/;
index index.html;
try_files $uri /itportal/index.html;
}
# === 后端 API — /api/ 前缀由 proxy_pass 尾部斜杠剥离 ===
location /api/ {
proxy_pass http://backend_api/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# === WebSocket — 不能带尾部斜杠 ===
location /ws/ {
access_log off;
proxy_pass http://backend_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 86400s;
}
# === H5 静态文件 ===
location /h5/ {
root /usr/share/nginx/html;
index index.html;
try_files $uri /h5/index.html;
}
# === H5 API 代理 ===
location /h5/api/ {
proxy_pass http://backend:8000/;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# === 根路径 → H5 员工端 ===
location = / {
return 302 /h5/;
}
}
}
@@ -0,0 +1,398 @@
# 智能IT服务系统运维手册
> **版本**: v1.0 | **日期**: 2026-07-04 | **维护人**: 助理(小米)
> **目标读者**: 运维工程师 / IT支持组
> **📖 关联文档**:
> - [README.md](../README.md) — 项目快速入门
> - [01-项目总览与部署手册](./01-项目总览与部署手册-20260704.md) — 完整架构设计
> - [CHANGELOG.md](../CHANGELOG.md) — 版本变更概览
> - [docs/archive/](./archive/) — 历史版本详情
---
## 目录
1. [系统概述](#一系统概述)
2. [环境信息](#二环境信息)
3. [部署操作](#三部署操作)
4. [日常运维](#四日常运维)
5. [故障排查](#五故障排查)
6. [回滚方案](#六回滚方案)
7. [备份恢复](#七备份恢复)
8. [应急响应](#八应急响应)
---
## 一、系统概述
### 1.1 系统架构
```
浏览器 ──→ itsupport.servyou.com.cn:443
┌─── nginx (容器) ───────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /itadmin/* → 管理后台 SPA │
│ /itportal/* → Portal 选择页 │
│ /api/* → backend:8000 │
│ /ws/* → backend:8000 (WS)│
│ │
└──────────────┬───────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
### 1.2 组件清单
| 服务 | 镜像 | 端口 | 说明 |
|------|------|------|------|
| nginx | nginx:alpine | 443→80 (对外) | 反向代理 + SSL |
| backend | 自构建 | 8000 (内部) | FastAPI 后端 |
| postgres | postgres:16 | 5432 (内部) | 数据库 |
| redis | redis:7 | 6379 (内部) | 缓存 + Session |
### 1.3 访问端点
| 端点 | 说明 |
|------|------|
| `https://itsupport.servyou.com.cn/itdesk/` | H5 员工端 |
| `https://itsupport.servyou.com.cn/itagent/` | 坐席工作台 |
| `https://itsupport.servyou.com.cn/itadmin/` | 管理后台 |
| `https://itsupport.servyou.com.cn/itportal/` | Portal 角色选择 |
| `https://itsupport.servyou.com.cn/api/docs` | API Swagger 文档 |
---
## 二、环境信息
### 2.1 服务器信息
| 环境 | IP | 域名 | 用途 |
|------|-----|------|------|
| 生产 | 10.90.5.110 (内网) | itsupport.servyou.com.cn | 正式环境 |
| 运维入口 | 10.212.189.210:2222 | - | 堡垒机 SSH |
### 2.2 关键配置
| 配置项 | 值 |
|--------|-----|
| 企微 CorpID | `ww...` (见 .env) |
| 企微 AgentID | `1000xxx` |
| 数据库 | PostgreSQL 16 |
| 缓存 | Redis 7 |
| 域名证书 | `*.servyou.com.cn` (GeoTrust/DigiCert) |
### 2.3 部署路径
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── .env # 环境变量(不提交 Git)
├── backend/ # 后端代码
├── frontend-h5/dist/ # H5 前端构建产物
├── frontend-agent/dist/ # 坐席前端构建产物
├── frontend-admin/dist/ # 管理后台构建产物
├── frontend-portal/dist/ # Portal 构建产物
├── nginx/ # Nginx 配置
└── logs/ # 日志目录
```
---
## 三、部署操作
### 3.1 部署流程概览
```
1. 打包代码 → 2. 上传服务器 → 3. 配置环境变量 → 4. 启动容器 → 5. 验证
```
### 3.2 打包命令(本地)
```bash
# 在项目根目录执行
cd D:\资\03-项目开发\wecom_it_smart_desk
# 使用部署脚本打包
powershell -File deploy-server\build-package.ps1
# 或手动打包
tar czf deploy.tar.gz \
backend/ frontend-h5/dist/ frontend-agent/dist/ \
frontend-admin/dist/ frontend-portal/dist/ \
nginx/ docker-compose.yml .env.production scripts/
```
### 3.3 上传到服务器
> **注意**: 公司服务器只能通过堡垒机上传,无法直接从本地 scp
1. **通过堡垒机上传到 `/tmp/`**
- 使用 SFTP 或 Web 界面上传到堡垒机
2. **SSH 登录服务器**
```bash
# 堡垒机: sxn@10.212.189.210:2222
ssh sxn@10.90.5.110 # 跳转目标服务器
```
3. **移动到目标目录**
```bash
mv /tmp/deploy.tar.gz /opt/wecom-it-desk/
cd /opt/wecom-it-desk/
tar xzf deploy.tar.gz
```
### 3.4 配置环境变量
```bash
# 创建环境配置
cp .env.production .env
vim .env # 编辑真实配置
# 必填项:
# - WECOM_CORP_ID
# - WECOM_AGENT_ID
# - WECOM_SECRET
# - WECOM_TOKEN
# - WECOM_ENCODING_AES_KEY
# - POSTGRES_PASSWORD
# - REDIS_PASSWORD
```
### 3.5 启动服务
```bash
# 启动所有容器
docker compose up -d --build
# 或分步启动
docker compose up -d postgres redis # 先启动基础服务
docker compose up -d backend # 再启动后端
docker compose up -d nginx # 最后启动前端
```
### 3.6 验证部署
```bash
# 1. 检查容器状态
docker compose ps
# 预期:4 个容器全部 Up/healthy
# 2. 健康检查
curl -ksI https://itsupport.servyou.com.cn/api/health
# 3. 各端点验证
curl -ksI https://itsupport.servyou.com.cn/itdesk/
curl -ksI https://itsupport.servyou.com.cn/itagent/
curl -ksI https://itsupport.servyou.com.cn/itadmin/
curl -ksI https://itsupport.servyou.com.cn/itportal/
```
---
## 四、日常运维
### 4.1 日常检查
```bash
# 每日必做检查
docker compose ps # 容器状态
docker compose logs --tail=50 backend # 后端日志
docker compose logs --tail=50 nginx # 前端日志
df -h # 磁盘空间
```
### 4.2 常用操作
| 操作 | 命令 |
|------|------|
| 重启后端 | `docker compose restart backend` |
| 重启 nginx | `docker compose restart nginx` |
| 查看实时日志 | `docker compose logs -f backend` |
| 进入后端容器 | `docker compose exec backend bash` |
| 查看容器资源 | `docker stats` |
### 4.3 前端缓存刷新
部署前端后,企微/H5 应用可能因为浏览器缓存显示旧版本。可用以下方式强制刷新:
| 场景 | 操作方法 |
|------|----------|
| **企微应用内** | `Ctrl + Shift + R`(强制刷新,清除缓存) |
| **普通浏览器** | `Ctrl + F5` 或 `Ctrl + Shift + R` |
| **iOS 企微** | 长按应用卡片 → 删除 → 重新从工作台添加 |
| **完全清除** | 清除浏览器缓存后重新访问 |
> **注意**:企微应用(webview)的缓存机制与普通浏览器不同,普通 F5 刷新可能无效,必须使用 `Ctrl + Shift + R`。
### 4.4 监控指标
| 指标 | 阈值 | 说明 |
|------|------|------|
| CPU 使用率 | < 80% | 主机层面 |
| 内存使用率 | < 80% | 主机层面 |
| 磁盘使用率 | < 70% | 主机层面 |
| 容器状态 | 全部 Up | docker compose ps |
| API 响应时间 | P95 < 500ms | 业务层面 |
### 4.5 日志位置
| 服务 | 日志命令 |
|------|----------|
| 后端 | `docker compose logs backend` |
| Nginx | `docker compose logs nginx` |
| PostgreSQL | `docker compose logs postgres` |
| Redis | `docker compose logs redis` |
---
## 五、故障排查
> **本章已整合至标准故障排查手册**:[04-运维文档/部署运维/00-标准故障排查手册.md](../04-运维文档/部署运维/00-标准故障排查手册.md)
>
> 手册涵盖:三步隔离法、错误码速查(500/502/503/403/422/网络挂起)、诊断脚本与命令、案例库(含 Redis urlparse 挂起、502、各类修复记录)、端到端验证完成标准(含"宣布修复前必须提供真实浏览器截图"硬规则)。
>
> **日常排故请直接打开该手册**,本文档不再重复故障排查细节。
---
## 六、回滚方案
### 6.1 快速回滚
```bash
# 停止当前版本
docker compose down
# 恢复上一个版本(需提前备份)
# 方法1: 从 Git 拉取上一个 commit
git checkout {上一个commit-hash}
# 重新构建部署
# 方法2: 保留上一个版本的部署包
cd /opt/wecom-it-desk-backup
tar xzf deploy-v0.x.x.tar.gz
docker compose up -d
```
### 6.2 回滚检查清单
- [ ] 确认上一个版本可用
- [ ] 通知相关人员
- [ ] 记录当前版本问题
- [ ] 执行回滚
- [ ] 验证回滚后功能正常
- [ ] 发送回滚通知
---
## 七、备份恢复
### 7.1 备份策略
| 备份对象 | 方法 | 频率 | 保留 |
|---------|------|------|------|
| PostgreSQL | pg_dump | 每日凌晨 | 7 天 |
| Redis | redis-cli SAVE | 每日凌晨 | 7 天 |
| 配置文件 | tar 归档 | 每次部署 | 4 个版本 |
| 日志文件 | logrotate | 每周 | 4 周 |
### 7.2 备份命令
```bash
# 备份数据库
docker compose exec postgres pg_dump -U postgres wecom_it > /tmp/backup_$(date +%Y%m%d).sql
# 备份 Redis
docker compose exec redis redis-cli SAVE
cp /var/lib/docker/volumes/wecom-it-desk_redis_data/_data/dump.rdb /tmp/redis_$(date +%Y%m%d).rdb
# 备份配置
tar czf /tmp/config_$(date +%Y%m%d).tar.gz /opt/wecom-it-desk/.env /opt/wecom-it-desk/nginx/
```
### 7.3 恢复命令
```bash
# 恢复数据库
docker compose exec -T postgres psql -U postgres wecom_it < backup_20260701.sql
# 恢复 Redis
docker compose exec -T redis redis-cli FLUSHALL
# 停止服务后复制 dump.rdb 到数据目录
```
---
## 八、应急响应
### 8.1 事件分级
| 等级 | 场景 | 响应时间 |
|------|------|----------|
| 🔴 P0 | 鉴权漏洞 / 数据泄露 / 服务全停 | 5 min |
| 🟠 P1 | 功能故障 / 单服务降级 | 30 min |
| 🟡 P2 | 性能问题 / UI 异常 | 4 h |
| 🟢 P3 | 体验优化 | 1 周 |
### 8.2 P0 应急流程
#### 立即止血
```bash
# 1. 关闭外网访问
sudo iptables -A INPUT -p tcp --dport 443 -j DROP
# 2. 停可疑服务
docker compose stop backend
# 3. 保留现场(不删文件)
docker compose logs backend > /tmp/incident-backend.log
docker compose logs nginx > /tmp/incident-nginx.log
```
#### 通知
- 微信/电话通知项目负责人
- 邮件通知:`wecom-it-desk-incident@servyou-it.com`
### 8.3 应急联系
| 角色 | 联系人 |
|------|--------|
| 项目负责人 | 宋献 |
| 运维 | IT 支持组 |
| 企微技术支持 | 企微客服 |
---
## 附录
### 版本历史
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| v1.0 | 2026-07-04 | 初始版本,整合部署/运维/故障排查文档 |
### 相关文档
| 文档 | 说明 |
|------|------|
| `docs/01-项目总览/01-项目总览与部署手册-20260704.md` | 完整项目背景与架构设计 |
| `docs/RELEASE_NOTES_v0.7.1.md` | 版本发布说明 |
| `docs/SOPs/SOP-004-应急响应.md` | 详细应急响应流程 |
| `04-运维文档/部署运维/00-标准故障排查手册.md` | 标准故障排查手册(故障排查唯一入口)|
---
> **维护说明**: 本文档由助理(小米)维护,随每次发布更新。
> 如有更新,请同步更新本文档的版本号和日期。
@@ -0,0 +1,731 @@
# 企微智能IT支持服务台 — 项目总览与部署手册
> **版本**: v2.2 | **日期**: 2026-07-04 | **编制**: 宋献(IT支持组组长)
> **目标读者**: **管理者 / 架构师 / 运维** — 了解项目全貌、架构决策、部署与运维操作
> **📖 与 README.md 的关系**: 本文是 [README.md](../README.md) 的**详细版本**,侧重完整的架构设计和部署运维。README 适合新人快速入门,本文适合深入了解。
> **⚠️ 运维手册更新**: 部署与运维操作已整合到独立文档 [智能IT服务系统运维手册](./智能IT服务系统运维手册.md),该文档由助理(小米)维护,随每次发布更新。本文档保留架构设计与背景信息。
---
## 目录
1. [项目概述](#一项目概述)
2. [系统架构](#二系统架构)
3. [三步演进路径](#三三步演进路径)
4. [现有系统复用评估](#四现有系统复用评估)
5. [正式环境部署方案](#五正式环境部署方案)
6. [部署操作手册](#六部署操作手册)
7. [运维管理](#七运维管理)
8. [开发交付状态](#八开发交付状态)
9. [附录](#九附录)
---
## 一、项目概述
### 1.1 背景与痛点
公司约 **6000 人**,全国设分子机构,使用企业微信作为内部 IM。当前 IT 服务存在三大痛点:
| 痛点 | 现状 | 影响 |
|------|------|------|
| 员工绕过 AI 直接找人工 | 可通过关键词直通人工坐席,首次后永久记忆 | AI 筛选率极低,人工成本高 |
| AI 转人工需另开窗口 | 跳转到企微"员工服务"模块,与 AI 对话割裂 | 体验差,员工困惑 |
| 无法跨主体共享 | 企微"员工服务"不支持互联企业应用共享 | 跨企业服务不可达 |
### 1.2 核心方案
**自研 IT 服务坐席系统**,替代企微内置的"员工服务"模块:
- 基于企微自建应用消息 API,所有消息由自己的服务器接管
- 分三步渐进式构建:M1 消息接管 → M2 AI 接入 → M3 知识库闭环
- 当前处于 **M1(消息接管 + 极简坐席)开发完成,部署配置中**
### 1.3 核心设计理念
传统"串行排队"改为**"并行协作"**——AI 全程在线,人工随时介入:
| 角色 | 工作方式 |
|------|---------|
| AI | 全程在线,所有对话可见 |
| 坐席 | 随时介入,AI 始终在旁辅助 |
| 员工 | 同一窗口,AI 和人工无缝切换 |
---
## 二、系统架构
### 2.1 部署架构总览(预生产环境)
> **当前阶段**:预生产环境。智能咨询系统与 IT 数据查询平台**分别部署在不同主机**,通过 Nginx 路径路由共用域名 `it-dataquery.dc.servyou-it.com`。正式环境将迁移到 K8s 集群。
```
浏览器 ──→ it-dataquery.dc.servyou-it.com:80
┌─── nginx (本系统主机) ───────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /api/* → backend:8000 (FastAPI) │
│ /ws/* → backend:8000 (WS) │
│ /* → 数据平台主机(远程IP) │ ← 跨主机代理
│ │
└──────────────┬───────────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
| 对比项 | 预生产(当前) | 正式环境(未来) |
|--------|-------------|---------------|
| 部署方式 | Docker Compose(单主机) | K8s 集群(高可用) |
| 与数据平台关系 | 不同主机,Nginx 远程代理 | 独立 K8s 集群 |
| 域名 | 共用 `it-dataquery.dc.servyou-it.com` | 独立域名或 K8s Ingress |
### 2.2 技术栈
| 层级 | 技术选型 | 说明 |
|------|---------|------|
| 反向代理 | Nginx | 统一入口、路径路由、WebSocket 代理 |
| 后端框架 | FastAPI (Python 3.12) | 异步、自动 OpenAPI 文档、类型安全 |
| 数据库 | PostgreSQL 16 | 会话/消息/坐席/配置 持久化(9 张表) |
| 缓存 | Redis 7 | access_token 缓存(TTL 7200s)、JWT 会话 |
| ORM | SQLAlchemy 2.0 (async) | 异步 session、声明式模型 |
| 数据库迁移 | Alembic | 所有表结构变更通过迁移脚本管理 |
| 坐席前端 | Vue3 + ElementPlus + Pinia | 企业级组件库,三栏工作台 |
| 员工 H5 | Vue3 + Vant4 + Pinia | 移动端组件库,企微 WebView 兼容 |
| 容器化 | Docker + Docker Compose | 4 容器一键启停 |
### 2.3 数据库核心表(9 张)
| 表名 | 用途 | 关键字段 |
|------|------|---------|
| `conversations` | 会话主表 | employee_id, status, urgency_score(1-5), tags(JSON), is_vip, participants(JSON) |
| `messages` | 消息记录 | sender_type(employee/agent/ai/system), content, msg_type |
| `agents` | 坐席信息 | user_id, status(online/offline/busy), current_load |
| `quick_reply_templates` | 快速回复模板 | category, title, content(支持 {变量}) |
| `system_configs` | 系统配置 | config_key, config_value(关键词/阈值/话术等) |
| `funny_phrases` | 趣味话术 | scene(6 种场景), content, tone, is_active |
| `approval_links` | 审批流程链接 | category(IT/HR/行政/财务), title, url |
| `software_downloads` | 软件下载入口 | category, name, version, platform, download_url |
| `agent_notes` | 坐席备注 | conversation_id, agent_id, content |
### 2.4 API 接口分组
| 分组 | 路径前缀 | 核心接口 |
|------|---------|---------|
| 企微回调 | `/api/wecom/callback` | GET 验证 URL、POST 接收消息 |
| 会话管理 | `/api/conversations` | 列表/详情/状态/置顶/代办/接单/邀请/退出/移除参与者 |
| 消息管理 | `/api/conversations/{id}/messages` | 消息列表/发送 |
| 坐席管理 | `/api/agents` | 列表/登录/状态切换 |
| H5 用户端 | `/api/h5/*` | 会话/摇人/审批链接/软件下载/OAuth |
| WebSocket | `/ws/{agent_id}` | 实时推送(坐席端) |
统一响应格式:`{ "code": 0, "data": {}, "message": "success" }`
### 2.5 消息收发全链路
```
员工发消息 → 企微回调解密 → 消息路由 → 评分标记 → 入库 → 坐席 WS 推送 → 坐席回复 → 企微主动推送 → 员工同一窗口收到
```
**坐席端通信**:已升级为 WebSocket 实时推送(2026-06-03),替代原计划的短轮询:
- 心跳保活:前端每 30s 发 ping,后端回 pong
- 断线重连:指数退避(1s→2s→4s→...→30s 上限)
- 降级策略:WS 断连时自动降级为 3s 轮询
### 2.6 会话排序与评分规则
**排序**: 紧急 → 举手 → 需介入 → 活跃 → AI处理中 → 已结单(同级按时间倒序)
**紧急度评分**: `基础分(关键词) + 情绪加成 + VIP加成 + 重复追问加成`,范围 1-5
**标记系统**:
| 标记 | 图标 | 触发条件 |
|------|------|---------|
| VIP | 红色 | 企微通讯录规则匹配 |
| 举手 | 黄色 | 员工说关键词或点击摇人按钮 |
| 需介入 | 橙红 | 同一问题追问 >3 轮 |
| 情绪 | 红色 | 关键词匹配(急/崩溃/投诉等) |
---
## 三、三步演进路径
| 里程碑 | 周期 | 核心交付 | 状态 |
|--------|------|---------|------|
| **M1** 消息接管 + 极简坐席 | 6-8 周 | 企微 API 链路验证 · 坐席三栏工作台 · 员工 H5 双栏 · 邀请功能(多人会话协作) | ✅ 代码完成,部署中 |
| **M2** AI 机器人接入 | M1 后 4-6 周 | 千问/Dify/RAGFlow 接入 · AI 前置筛选 · 排队系统 | 📋 计划中 |
| **M3** 知识库闭环迭代 | M2 后 4-6 周 | 坐席标注系统 · 千问自动分析 · 知识库自优化 | 📋 计划中 |
### M1 当前进度(2026-06-03
| 模块 | 状态 |
|------|------|
| PRD + 架构设计 | ✅ 完成 |
| 后端代码(45+ 文件,7 API 组) | ✅ 完成 |
| 坐席前端(三栏工作台 + WebSocket | ✅ 完成 |
| 员工 H5(双栏 + 摇人按钮 + 呼叫坐席) | ✅ 完成 |
| 邀请功能(多人会话协作 — PRD §21) | 📋 计划中(M1 范围) |
| 前端功能联调验证 | ✅ 完成(2026-06-03 |
| 测试用例(116 条 pytest | ✅ 完成 |
| Alembic 数据库迁移 | ✅ 完成 |
| 前端构建产物(dist/) | ✅ 完成 |
| 远程服务器部署 | 🔧 待 SSH 账号 |
### M2 核心改动
只改路由层逻辑,其余不动:
```
M1: 新会话 → 坐席队列
M2: 新会话 → AI 先回答 → AI判断/用户触发 → 坐席队列
```
新增:千问对话模型、RAGFlow 知识库检索、Dify 编排平台、排队系统。目标:AI 首答率 ≥ 80%。
### M3 知识库迭代闭环
```
坐席日常标注 ──→ 千问分析 ──→ 自动处理 ──→ 知识库增强
(正确/错误) (缺文档/过时) │
↑ │
└──────────── 持续循环 ─────────────────────┘
```
---
## 四、现有系统复用评估
### 4.1 核心复用(直接影响新系统架构)
| # | 资源 | 复用方式 | 新系统对应 |
|---|------|---------|-----------|
| 1 | Dify Workflow | 直接复用,M2 阶段接入 AI 回复 | 坐席助手 AI 面板 + 自动回复 |
| 2 | dify2openai 桥接 | 直接复用 API | 后端调用 AI 的入口 |
| 3 | RAGFlow 知识库 | 直接复用,M3 阶段混合标注迭代 | 知识库管理 + 标注闭环 |
| 4 | Qwen3-30B 大模型 | 直接复用 | AI 对话底层模型 |
| 5 | bge-m3 向量模型 | RAGFlow 内置,直接复用 | 知识库检索向量化 |
| 6 | Dify 数据库(只读) | 读取 messages 表,同步历史数据 | 历史会话数据迁移 + 统计 |
| 7 | 企微自建应用 | 直接复用应用凭证 | 消息收发的企微入口 |
### 4.2 基础设施复用(零耦合)
| 资源 | 复用方式 | 耦合度 |
|------|---------|--------|
| 企微自建应用凭证 | 配置文件引用(只读) | 零耦合 |
| Dify Workflow API | HTTP 调用 | 外部依赖 |
| RAGFlow 知识库 | HTTP 调用 | 外部依赖 |
| Qwen3-30B 大模型 | HTTP 调用 | 外部依赖 |
| SSL 证书文件 | Nginx 挂载只读 | 零耦合 |
### 4.3 关键结论
> 代码层面复用率约 15%(主要是业务逻辑和 SQL 查询),基础设施和 AI 能力复用率约 70%。
> 新系统用 **FastAPI + SQLAlchemy 2.0**,不沿用旧 Django 代码。底层业务逻辑可参考移植。
---
## 五、正式环境部署方案
### 5.1 核心决策原则
基于四个约束条件:
| # | 约束 | 推导原则 |
|---|------|---------|
| 1 | 对现有正式环境架构影响最小 | **物理隔离 > 逻辑隔离** |
| 2 | 避免变更影响现有服务 | **独立 Nginx 入口** |
| 3 | 减少服务依赖 | **最小化外部依赖** |
| 4 | 避免责任不清 | **独立数据库 + 独立 Redis** |
> **一句话**:新系统作为**独立服务单元**部署,与现有智能 IT 数据平台(Django)在物理资源层面完全解耦,仅通过 HTTP API 调用共享 AI 能力。
### 5.2 关键隔离策略
| 隔离层面 | 方案 | 效果 |
|---------|------|------|
| 服务器级 | 独立 VM,不共用宿主机 | 挂了不影响旧系统 |
| 网络级 | Docker 内部网络,PG/Redis 不暴露宿主机端口 | 外部无法直连数据库 |
| 存储级 | 独立命名卷,不共用 Volume | 数据完全隔离 |
| 域名级 | 路径路由(共用域名) + 独立 Nginx 容器 | `/itdesk/``/itagent/``/api/` 归属本系统 |
| 认证级 | JWT + 独立 Redis | 账户体系独立 |
| 依赖级 | 仅 HTTP 调用外部 AI 服务 | 外部服务故障只影响 M2 功能 |
### 5.3 与现有系统的解耦修正
原复用评估中的部分共享方案已修正为独立部署:
| 原建议 | 修正方案 | 理由 |
|--------|---------|------|
| 同机部署于 10.80.0.86 | 独立服务器/VM(或同机端口分离+独立 compose) | 避免端口冲突、资源争抢 |
| Redis 复用同实例 | 独立 Redis 容器 | FLUSHDB 误操作、内存 OOM 互相影响 |
| 使用旧系统 Nginx | 独立 Nginx 容器 | 变更反代配置不影响旧系统路由 |
| 复用旧 PG 实例 | 独立 PostgreSQL 容器 | 数据库是责任边界核心 |
### 5.4 Docker Compose 服务清单
| 服务 | 镜像 | 端口 | 健康检查 |
|------|------|------|---------|
| postgres | postgres:16 | 5432(内部) | pg_isready |
| redis | redis:7 | 6379(内部) | redis-cli ping |
| backend | 自构建 Dockerfile | 8000(内部) | GET /health |
| nginx | nginx:alpine | 18080:80(对外) | GET /health |
Docker 网络:`it-desk-internal`(内部,连接 backend/postgres/redis
### 5.5 资源需求
| 资源 | 配置 | 说明 |
|------|------|------|
| 服务器 | 4C8G + 100GB SSD(最低)/ 8C16G + 200GB SSD(推荐) | Docker Engine 环境 |
| 域名 | `it-dataquery.dc.servyou-it.com`(已就绪,共用) | 路径路由 `/itdesk/` `/itagent/` `/api/` |
| 企微自建应用 | 1 个(已创建) | CorpID/AgentID/Secret/Token/EncodingAESKey |
| 防火墙 | 办公网→服务器:80/443, 企微→服务器:443 | 出站: 企微 API/ Dify/ RAGFlow/ Qwen |
### 5.6 风险矩阵
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 新服务器申请被拒/延迟 | 中 | 部署延期 | 退化方案:旧服务器端口分离+独立 compose |
| SSL 证书到期 | 低 | HTTPS 不可用 | 复用现有通配符证书 |
| 企微应用配置变更 | 低 | 双系统消息中断 | 建立变更通知机制 |
| Dify/RAGFlow 不可用 | 中 | M2 AI 功能不可用 | 降级:纯坐席模式仍正常工作 |
| Docker 宿主机故障 | 低 | 新系统全宕 | Compose 配置即代码,重建快 |
---
## 六、部署操作手册
> **预生产部署**:本系统与数据平台部署在**不同主机**,通过 Nginx 路径路由共用域名。数据平台请求通过远程 IP 反代(非 Docker 网络)。正式环境将迁移到 K8s。
### 6.1 前置条件
- 服务器已安装 Docker Engine 24+ + Docker Compose v2
- IT 数据查询平台已部署运行
- 有 SSH 登录权限
### 6.2 配置数据平台反代地址
预生产环境中,数据平台部署在**独立主机**。部署前需修改 `nginx/nginx.conf` 中的数据平台上游地址:
```nginx
# nginx/nginx.conf — 将 DATAQUERY_HOST 替换为数据平台主机的实际 IP
upstream dataquery {
server 10.80.0.86:80; # ← 替换为数据平台实际 IP:端口
}
```
> **为什么不创建 Docker 共享网络?** 预生产两台主机不在同一 Docker Engine,无法使用 `docker network create` 互联。正式环境迁移 K8s 后由 Ingress/Service 处理路由。
### 6.3 上传部署包
在本地(Windows)执行打包上传:
```bash
# 方式 A:使用 deploy.sh 打包
bash scripts/deploy.sh --pack
scp it-smart-desk-*.tar.gz user@server:/opt/
# 方式 B:手动打包
tar czf deploy.tar.gz \
backend/ frontend-h5/dist/ frontend-agent/dist/ \
nginx/ docker-compose.yml .env.production scripts/
scp deploy.tar.gz user@server:/opt/it-smart-desk/
```
### 6.4 服务器配置与启动
```bash
ssh user@server
cd /opt/it-smart-desk
tar xzf it-smart-desk-*.tar.gz
# 创建环境配置
cp .env.production .env
vim .env # 填入真实企微凭证
```
`.env` 必填项:
| 配置项 | 说明 | 获取位置 |
|--------|------|---------|
| `WECOM_CORP_ID` | 企业 ID | 企微管理后台 > 我的企业 |
| `WECOM_AGENT_ID` | 应用 AgentId | 企微管理后台 > 应用管理 |
| `WECOM_SECRET` | 应用 Secret | 企微管理后台 > 应用管理 |
| `WECOM_TOKEN` | 回调 Token | 企微管理后台 > 接收消息 |
| `WECOM_ENCODING_AES_KEY` | 回调 AES 密钥 | 企微管理后台 > 接收消息 |
| `POSTGRES_PASSWORD` | 数据库密码 | 自定义强密码 |
| `CORS_ORIGINS` | `http://it-dataquery.dc.servyou-it.com` | CORS 白名单 |
启动:
```bash
bash scripts/deploy.sh
# 自动执行:检查前置条件 → 构建后端镜像 → 启动所有容器 → 运行数据库迁移
```
### 6.5 验证部署
```bash
# 检查容器状态
docker compose ps
# 预期:4 个容器全部 Up/healthy
# 健康检查
curl http://localhost:18080/api/health
# 浏览器验证
# http://it-dataquery.dc.servyou-it.com/itdesk/ → H5 员工咨询页面
# http://it-dataquery.dc.servyou-it.com/itagent/ → 坐席工作台登录页
# http://it-dataquery.dc.servyou-it.com/ → IT 数据查询平台(不变)
# http://it-dataquery.dc.servyou-it.com/api/docs → FastAPI Swagger 文档
```
### 6.6 常见问题
**nginx 启动失败,报 `host not found in upstream "dataquery"`**
`nginx/nginx.conf``DATAQUERY_HOST` 未替换为数据平台的实际 IP。确保已在部署前完成替换。
**nginx 启动但数据平台页面 502**
→ 本系统主机无法访问数据平台主机 IP。检查防火墙策略是否放行两台主机间的 80 端口。
**访问 `/itdesk/` 返回 404**
→ 检查前端 dist 是否正确挂载:`docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itdesk/`
**API 返回 CORS 错误**
→ 检查 `.env``CORS_ORIGINS` 是否包含 `http://it-dataquery.dc.servyou-it.com`
**数据库迁移失败**
→ PostgreSQL 可能未就绪,等 30 秒后执行:`docker compose restart backend`
### 6.7 更新部署
```bash
# 仅更新前端
bash scripts/deploy.sh --build
docker compose restart nginx
# 仅更新后端
docker compose build backend
docker compose up -d backend
# 全量更新
bash scripts/deploy.sh --down
bash scripts/deploy.sh
```
### 6.8 回滚
```bash
docker compose down # 停止新系统所有容器
# 旧系统不受任何影响(独立资源)
```
---
## 七、运维管理
### 7.1 责任矩阵
| 运维操作 | 影响范围 | 备注 |
|---------|---------|------|
| 重启 PostgreSQL | 仅新系统 | 独立实例 |
| 重启 Redis | 仅新系统 | 独立实例 |
| 修改 Nginx 配置 | 仅新系统路由 | 独立容器 |
| 更新后端/前端代码 | 仅新系统 | 独立容器 |
| 企微应用配置变更 | **双系统** | ⚠️ 唯一共享点,需通知双方 |
### 7.2 监控指标
```yaml
主机层面:
- CPU 使用率 < 80%
- 内存使用率 < 80%
- 磁盘使用率 < 70%
容器层面:
- docker compose ps 全部 "Up" 状态
- Nginx 健康检查: GET /health → 200
- Backend 健康检查: GET /health → 200
业务层面(后续接入):
- 企微消息回调成功率 > 99%
- API 响应时间 P95 < 500ms
```
### 7.3 备份策略
| 备份对象 | 方法 | 频率 | 保留 |
|---------|------|------|------|
| PostgreSQL 数据 | `pg_dump` + 卷快照 | 每日凌晨 | 7 天 |
| Redis 数据 | `SAVE` + 复制 dump.rdb | 每日凌晨 | 7 天 |
| Docker 卷 | `tar czf` 归档 | 每周 | 4 周 |
### 7.4 关键对接参数(M2 阶段)
| 参数 | 值 | 用途 |
|------|-----|------|
| dify2openai API | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` | AI 对话 |
| RAGFlow | `http://10.80.0.85:8080` | 知识库管理 |
| Qwen3-30B | `http://10.80.0.49:5000/api/llm/servyou/v1/chat/completions` | 大模型 |
| Dify DB(生产只读) | `10.80.128.40:5432` DB=dify User=difyro | 历史数据同步 |
| 数据平台 | `http://it-dataquery.dc.servyou-it.com` (10.80.0.86) | 部署服务器 |
### 7.5 应急预案可选技术项
> **评估日期**: 2026-06-03 | **来源**: 企微原生1对1方案(PRD §3.2 方式五)可行性评估
#### 7.5.1 备用方案概述
当 H5 WebView 方案(当前主方案)出现以下情况时,可切换至**企微原生1对1方案**作为降级/备用:
| 应急场景 | 当前方案症状 | 备用方案动作 |
|---------|------------|------------|
| H5 前端服务不可用 | Nginx 静态文件丢失/构建产物损坏 | 员工直接在企微与应用1对1聊天,走 `/message/send` 回复 |
| H5 页面性能问题 | WebView 加载慢/白屏/兼容性问题 | 放弃 H5 入口,改用企微原生聊天窗口交互 |
| OAuth2 鉴权异常 | 静默授权失败,H5 无法获取员工身份 | 原生方案无需 OAuth2,回调自带 UserID |
| 跨平台接入需求 | 需接入钉钉/飞书/浏览器用户 | **不适合切换**——原生方案无法跨平台,此时应修复 H5 |
| 外部专家协作 | 坐席需要拉入第三方专家协助 | 启用 `/appchat/create` 创建临时群聊 |
#### 7.5.2 备用方案技术架构
```
┌─────────────────────────────────────────────────────┐
│ 员工端(企微原生1对1聊天窗口) │
│ │
│ 员工 ←─消息─→ 自建应用(IT智能助手) │
│ │ │
│ ├─ AI回复 → /message/send → 同一窗口 │
│ ├─ 坐席回复 → /message/send → 同一窗口 │
│ └─ 外援 → /appchat/create → 新群聊窗口 │
│ │
│ 坐席工作台(保留,不变) │
│ ├─ WebSocket 接收员工消息 │
│ ├─ 坐席回复 → 后端 → /message/send → 员工窗口 │
│ └─ 外援指令 → 后端 → /appchat/create → 新群聊 │
│ │
└─────────────────────────────────────────────────────┘
```
**核心能力**:项目**已经具备**备用方案所需的全部后端代码:
- `wecom_callback.py`:接收企微回调 ✅
- `message_router._try_ai_reply()``wecom_service.send_text_message()`AI回复走 `/message/send`
- `scoring_service.detect_hand_raise()`:关键词举手检测 ✅
- `wecom_service.send_text_message()`:应用消息推送 ✅
**仅需新增**
- 交互卡片消息发送(`msgtype="template_card"`)— 用于"转人工"按钮、满意度评分
- AppChat API 封装(`/cgi-bin/appchat/*`)— 用于外援群聊场景
- AI/人工身份区分前缀(如 `🤖 AI回复:` / `👨‍💻 人工坐席(张三):`
#### 7.5.3 切换流程
**从 H5 方案切换到原生1对1方案**
| 步骤 | 操作 | 负责人 | 预计耗时 |
|------|------|--------|---------|
| 1 | 确认企微回调 URL 已配置且可达(H5 方案已配置则无需改动) | 运维 | 0 min |
| 2 | 确认 `message_router._try_ai_reply()``/message/send`(已实现) | 开发 | 0 min |
| 3 | 通知员工:直接在企微与应用聊天即可,不再进入 H5 | 运维 | 5 min |
| 4 | (可选)关闭 H5 入口:Nginx 配置注释 `/itdesk/` 路由 | 运维 | 2 min |
| 5 | (可选)启用交互卡片:部署 template_card 消息发送代码 | 开发 | 1-2 天 |
> **关键点**:步骤1-3 **零代码改动**即可完成基本切换,因为核心回调+消息推送链路已在运行。
**从原生1对1方案切回 H5 方案**
| 步骤 | 操作 | 负责人 |
|------|------|--------|
| 1 | 恢复 Nginx `/itdesk/` 路由(如已注释) | 运维 |
| 2 | 确认 H5 构建产物存在且可访问 | 运维 |
| 3 | 通知员工:点击应用 → 进入 H5 咨询页面 | 运维 |
#### 7.5.4 企微 API 限制与容量评估
| API | 限制 | 当前业务量(月均 188 次 AI 会话/天) | 风险 |
|-----|------|--------------------------------|------|
| `/message/send` | ≤账号上限×200人次/天,同一人≤30次/分 | 预估 < 500 人次/天 | ✅ 充裕 |
| `/appchat/create` | ≤1000群/天 | 外援场景低频(预估 < 10群/天) | ✅ 充裕 |
| `/appchat/send` | ≤2万人次/分,同一人≤200条/分 | 群内消息量极小 | ✅ 充裕 |
| 回调消息 | 无硬限制 | 企微服务器推送到回调 URL | ✅ 无风险 |
#### 7.5.5 备用方案局限性与适用边界
| 局限 | 说明 | 影响 |
|------|------|------|
| **无法跨主体企微** | 企微原生1对1仅限同一企微主体内员工 | 无法服务供应商/外包人员 |
| **无法跨平台** | 原生方案绑定企微,无法嵌入钉钉/飞书/浏览器 | H5 扩展场景不可用 |
| **AI/人工区分不直观** | 都以应用身份推送,需内容前缀区分 | 体验不如 H5 的丰富身份标识 |
| **交互卡片需开发** | "转人工"按钮、满意度评分需 template_card 消息类型 | 降级期可用关键词替代("转人工" |
| **群聊外援需审批** | appchat API 要求可见范围=根部门 | 需企微管理员配合 |
> **决策建议**:当 H5 不可用且影响范围仅限企微主体内员工时,**立即切换**原生1对1方案(零代码改动);当需要跨平台/跨主体服务时,**优先修复 H5**,不切换原生方案。
---
---
## 八、开发交付状态
### TL;DR
企微智能IT支持服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件****116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。
### 交付状态
| 阶段 | 状态 | 产出 |
|------|------|------|
| PRD | ✅ 完成 | `PRD.md` — 31 需求(P0/P1/P2),7 用户故事 |
| 架构设计 | ✅ 完成 | `docs/02-技术文档/技术架构/IT智能服务台-系统架构设计文档v2.md` — 9 表 DDL,7 API 组,4 时序图,5 任务分解 |
| T01 项目脚手架 | ✅ 完成 | 57 文件 — docker-compose, nginx, .env, 后端/前端骨架 |
| T02 后端核心服务 | ✅ 完成 | 16 文件 — 企微加解密, 消息路由, 评分, 会话, 趣味话术, 7 API 路由 |
| T03 坐席工作台 | ✅ 完成 | 25 文件 — 三栏布局, 会话管理, 聊天, AI助手面板(5Tab) |
| T04 用户端H5 | ✅ 完成 | 12 文件 — 聊天面板, 摇人按钮, AI助手, 审批链接, 软件下载 |
| QA 测试用例 | ✅ 完成 | 8 文件, 116 测试用例(原 93 + 新增 23) |
| Bug 修复 | ✅ 完成 | 7 个 Bug 修复(详见下方) |
| PostgreSQL/SQLite兼容 | ✅ 完成 | 9 个模型文件全部兼容 SQLite |
| database.py 懒加载 | ✅ 完成 | 避免测试导入时连接 PostgreSQL |
| WecomCrypto 懒加载 | ✅ 完成 | 避免默认 AES Key 导入报错 |
| **pytest 全量验证** | **✅ 116/116 通过** | 1.71 秒完成,0 失败 |
### 关键文件
```
wecom_it_smart_desk/
├── README.md # 项目主文档(GitHub 首页)
├── docker-compose.yml # Docker Compose 容器编排
├── .env # 环境变量(数据库密码等,不提交 Git)
├── backend/ # FastAPI 后端服务
│ ├── app/
│ │ ├── main.py # FastAPI 应用入口
│ │ ├── config.py # 配置管理(从 .env 读取)
│ │ ├── database.py # 懒加载数据库引擎
│ │ ├── models/ # 11 个 ORM 模型(兼容 PostgreSQL/SQLite
│ │ ├── schemas/ # Pydantic Schema(请求/响应校验)
│ │ ├── utils/
│ │ │ └── wecom_crypto.py # 企微消息加解密(AES-CBC-256
│ │ ├── services/
│ │ │ ├── wecom_service.py # 企微回调处理
│ │ │ ├── message_router.py # 消息路由 + 评分 + 举手检测
│ │ │ ├── scoring_service.py # 紧急度评分引擎
│ │ │ ├── session_service.py # 会话生命周期管理
│ │ │ └── funny_phrase_service.py # 摇人趣味话术生成
│ │ └── api/ # 8 个 API 路由模块
│ └── tests/ # 116+ 个测试用例
├── frontend-agent/ # 坐席工作台(Vue 3 + Element Plus
│ └── src/
│ ├── views/ # LoginView + WorkspaceView
│ ├── components/
│ │ ├── TopBar/ # 顶部栏(主题切换 + 用户信息)
│ │ ├── conversation/ # 会话列表 + 会话条目
│ │ ├── chat/ # 聊天区 + 消息气泡 + 输入框
│ │ ├── assistant/ # AI 推荐内联组件
│ │ ├── troubleshooting/ # 排查步骤栏(FlowchartNode
│ │ ├── quickreply/ # 快速回复面板(三层导航)
│ │ └── todo/ # 待办面板 + 任务详情视图
│ ├── stores/ # Pinia Storeconversation/agent/quickReply/theme/todo
│ └── api/ # API 调用模块
├── frontend-h5/ # 员工端 H5Vue 3 + Vant
│ └── src/
│ ├── views/ # ChatView
│ └── components/ # ChatPanel + 摇人按钮 + AI助手
├── nginx/ # Nginx 反向代理配置
│ └── nginx.conf
├── scripts/ # 部署和运维脚本
│ ├── start_backend.bat # Windows 快速启动后端(相对路径)
│ └── restart_backend.ps1 # Windows 重启后端(自动查找 PG/Redis/Python
└── docs/ # 项目文档(全部文档统一存放)
├── PRD.md # 产品需求文档 v1.0
├── PRD-v53-incremental.md # v5.3 增量需求
├── ARCHITECTURE.md # 系统架构设计(合并版)
├── 01-项目总览与部署手册.md # 管理者视角部署手册
├── 开发交付概览.md # 开发交付状态总览
├── 智能IT支持服务台-项目迁移文档.md # 工作区迁移记录
├── testing/ # 测试报告目录
│ └── QA_COMPREHENSIVE_REPORT.md # 综合 QA 报告
├── diagrams/ # Mermaid 图表
│ ├── 02-技术文档/技术架构/sequence-diagram-代办事项.mermaid
│ ├── sequence-shake.mermaid
│ ├── sequence-scoring.mermaid
│ ├── sequence-polling.mermaid
│ └── 02-技术文档/技术架构/class-diagram-代办事项.mermaid
└── prototypes/ # 原型文件
├── agent-workspace-v5_3.html # 当前锁定版本(v5.3
├── qr_data_full.json # 快速回复数据(180条)
└── archive/ # 历史原型归档
```
### Bug 修复清单(7 个)
| # | 文件 | 问题 | 修复 |
|---|------|------|------|
| 1 | `message_router.py` | `calculate_urgency()` 是 async 但未 `await` | 添加 `await` |
| 2 | `app/main.py` | 中文引号 `""` 嵌入 Python 双引号字符串,SyntaxError | 转义引号 |
| 3 | `wecom_callback.py` | `WecomCrypto` 模块级初始化,默认 AES Key 不合法导致 `binascii.Error` | 改为懒加载单例 `_get_wecom_crypto()` |
| 4 | `tests/conftest.py` | `aioredis.from_url` mock 路径错误 | 修正为 `redis.asyncio.from_url` |
| 5 | `tests/conftest.py` | `create_test_conversation()` 缺少 `is_pinned`/`is_todo` 参数 | 添加可选参数 |
| 6 | `session_service.py` | `conversation_id` UUID 对象 vs String(36) 列类型不匹配 | 先转字符串再查询 |
| 7 | `scoring_service.py` | 关键词大小写不敏感缺失 + `_check_vip` 缺短路 | `.lower()` + 短路返回 |
### 用户下一步操作
1. **(已验证)pytest 全量通过**:116/116 测试已在开发环境验证通过,本地无需再跑
2. **配置企微应用凭证**
- 复制 `.env.example``.env`
- 填入企微应用的 CorpID、AgentID、Secret、Token、EncodingAESKey
3. **Docker Compose 启动**(需 PostgreSQL + Redis):
```powershell
cd C:\Users\simon\wecom_it_smart_desk
docker-compose up -d
```
4. **前端开发启动**
```powershell
# 坐席工作台
cd frontend-agent && npm install && npm run dev
# 用户端 H5
cd frontend-h5 && npm install && npm run dev
```
5. **企微回调配置**:在企微管理后台配置消息回调 URL 指向你的服务器
## 九、附录
### 8.1 需要团队协助的事项
| # | 事项 | 需要谁 | 紧急度 |
|---|------|--------|--------|
| 1 | **服务器 SSH 账号**:用于 Docker 部署 | 运维 | 🔴 高(当前阻塞) |
| 2 | **企微通讯录权限**:确认 API 权限(VIP 功能依赖) | 运维/企微管理员 | 中(M1 可用 mock) |
| 3 | **千问/Dify/RAGFlow 环境**M2 阶段) | 架构/开发 | 低(M2 前准备) |
### 8.2 项目文件索引
```
wecom_it_smart_desk/
├── README.md # 入口索引
├── PRD.md # 产品需求文档
├── docs/
│ ├── 01-项目总览与部署手册.md # ← 本文档(运维/架构/管理者)
│ ├── 02-技术架构与开发指南.md # 开发者文档
│ └── 03-测试验证文档.md # 测试文档
├── backend/ # FastAPI 后端
├── frontend-agent/ # 坐席工作台前端
├── frontend-h5/ # 员工 H5 前端
├── nginx/nginx.conf # Nginx 反代配置
├── docker-compose.yml # Docker Compose 编排
├── .env.production # 生产环境变量模板
└── scripts/ # 部署/构建脚本
```
---
> 本文档合并自原 `docs/团队沟通文档-架构消息知识库.md`、`docs/正式环境独立部署架构方案.md`、`docs/DEPLOY_NAS.md`。详细技术规格见 `docs/02-技术架构与开发指南.md`。
@@ -0,0 +1,185 @@
# Release Notes v0.7.1
> 📅 发布日期:2026-06-23 | 类型:🔐 安全 + ✨ 功能 | 紧急度:🟡 重要
> 🔄 补充:2026-06-24 hotfix #116/#118/#119/#120 已部署(详见文末"补充 hotfix"小节)
## 🎯 本版本重点
1. **修复扫码登录 iOS NSURLErrorCannotFindHost** — 企微 App 用户扫码不再报错
2. **完整 MFA + RBAC 权限体系** — 5 角色细粒度权限 + 高危操作二次认证
3. **H5 员工端企微 OAuth 登录** — 员工不再需要输账号密码
4. **🆕 扫码登录去掉手机 confirm 步骤** — 企微 App 无确认 UI,扫码成功 = 直接登录
## 🔐 安全修复
### #109 P0:扫码登录 NSURLError 修复
**问题**:用户企微 App 访问 `https://itsupport.servyou.com.cn/itportal/`,扫描二维码后跳转报 `NSURLErrorCannotFindHost`(iOS WebView DNS 解析失败)
**根因**:`backend/app/config.py` 新字段 `qrcode_oauth_callback` 用了 pydantic 默认行为,**不剥环境变量前缀**
- 字段名:`qrcode_oauth_callback`
- pydantic-settings 期望 env:`QRCODE_OAUTH_CALLBACK`
- 实际 env:`WECOM_QRCODE_CALLBACK`(带前缀,匹配不上)
- 结果:`settings.qrcode_oauth_callback = ""`,qrcode_service 走兜底返回**相对路径** `/api/auth_qrcode/scan`
- iOS WebView 拼接相对路径失败 → DNS 解析错误
**修复**:
```python
from pydantic import Field
class Settings(BaseSettings):
qrcode_oauth_callback: str = Field(
default="",
validation_alias="WECOM_QRCODE_CALLBACK",
)
```
**部署**:docker commit + restart + 验证,详见 [[phase1-progress#109-hotfix-二轮修复]]
### RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围
- **5 角色**:super_admin / admin / agent / user / guest
- **4 资源**:user / conversation / config / audit_log
- **4 操作**:read / create / update / delete
- **3 范围**:all / department / self
### 高危路由白名单 + 中间件
5 类高危操作需 OTP 二次认证:
- `role_change`(角色变更)
- `config_change`(配置变更)
- `data_export`(数据导出)
- `account_disable`(账户禁用)
- `account_create_reset`(账户创建/重置)
## ✨ 新功能
### 后端扫码登录(端点 4 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth_qrcode/create` | POST | 生成二维码(含 PNG base64) |
| `/api/auth_qrcode/poll/{ticket}` | POST | 扫码端轮询 |
| `/api/auth_qrcode/scan` | POST | 企微回调 |
| `/api/auth_qrcode/confirm` | POST | 用户确认登录 |
### 后端 MFA + pyotp(端点 5 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth/otp-status` | GET | 查询绑定状态 |
| `/api/auth/otp-bind` | POST | 开始绑定(返回 secret + QR) |
| `/api/auth/otp-verify` | POST | 确认绑定 |
| `/api/auth/otp-verify` | POST | 验证 OTP |
| `/api/auth/otp-unbind` | POST | 禁用 MFA |
### 前端 MFA UI
- `MfaBind.vue` — 绑定弹窗
- `useHighRiskOtp.ts` composable — 高危操作 OTP 弹窗
- `Login.vue` / `TopBar.vue` — 集成 MFA 状态显示
- 路由 + store 全链路集成
### H5 员工端 OAuth 登录
- 部署路径:`https://itsupport.servyou.com.cn/itdesk/`
- 自动 OAuth 静默授权(`snsapi_base`)
- 会话列表 + 消息收发
- 已完成 E2E 验证(#104)
## 🛠️ 部署要点
### 必备环境变量
```bash
# /opt/wecom-it-desk/.env
WECOM_QRCODE_CALLBACK=https://itsupport.servyou.com.cn/api/auth_qrcode/scan
WECOM_SSO_ENABLED=true
WECOM_SSO_CALLBACK_BASE=https://itsupport.servyou.com.cn
```
### 部署清单
- [x] Backend commit `78f60c6`(v0.7.1-dev)
- [x] docker 镜像 `wecom-it-desk-backend:latest` 已 commit 新 config
- [x] 容器已重启 + /api/ready 200
- [x] H5 dist 已部署到 `/opt/wecom-it-desk/nginx/html/itdesk/`
- [x] nginx 已 reload
- [x] /api/auth_qrcode/create 返回绝对 URL
- [ ] Gitea push(等用户重授权 token)
- [ ] 企微 App 实际扫码验证
## ⚠️ 已知问题
1. **Gitea push 阻塞**:`workbuddy-claude` token 2026-06-15 被吊销,需去 Gitea Web 重授权
2. **#108 still pending**:v0.7.1-dev 78f60c6 未推到 origin
3. **/api/admin/ IP 白名单**仍是 0.0.0.0/0(临时方案),v1.0 前必须收窄
## 📚 相关文档
- [[H5-DEPLOY-RUNBOOK-v0.7.1]] — H5 部署 runbook
- [[DEPLOY-LOGIN-MIGRATION-v0.7.0]] — 旧版扫码登录
- [[E2E-CHECKLIST-v0.7.0]] — 端到端验收
- [[NGINX-DOMAIN-ROUTING]] — nginx 域名分发
- [[USER-GUIDE-QRCODE-MFA]] — 用户手册
- [[phase1-progress]] — 进展跟踪
## 📊 改动统计
- **代码**:22 文件 / +4705 行
- **测试**:78 passed + 4 xfail + 33 pre-existing failures(已分类)
- **文档**:5 新增 / 3 更新
- **Agent**:4 并行 worktree 合并
---
## 🔄 补充 hotfix(2026-06-24 部署)
### #116 P0 + #118 P0:扫码端点 405 + ticket≠state
**问题**:
- 坐席扫码登录报 `405 Method Not Allowed`
- 企微 OAuth 回调走 GET(`?code=xxx&state=<ticket>`),旧代码只支持 POST
- 即使改双方法,`Optional[str] = None``Query()` 装饰器,FastAPI 当 body 参数处理
- 函数参数叫 `ticket`,企微用 `state`,参数名不匹配
**修复**:`backend/app/api/auth_qrcode.py` scan 端点
```python
@router.api_route("/scan", methods=["GET", "POST"], response_model=None)
async def scan_qrcode(
body: Optional[QrcodeScanRequest] = None,
ticket: Optional[str] = Query(None, description="兼容旧参数名"),
state: Optional[str] = Query(None, description="企微 OAuth state 标准参数名"),
code: Optional[str] = Query(None, description="企微 OAuth 授权码"),
redis_client = Depends(dep_redis),
):
if body is not None:
final_ticket, final_code = body.ticket, body.code
else:
final_ticket = state or ticket # 优先 state(企微标准),回退 ticket
final_code = code
```
**部署**:`deploy-staging/hotfix-116-qrcode-scan-get/` — webcli v7 一键,20/20 pytest 过
### #119:webcli v7 全自动部署链路
复用 jumpserver Playwright + Luna webcli + OTP 自动,实现 18-31KB 命令脚本单次输入,2-3 分钟跑完。
### #120 P0:扫码成功 = 自动登录(去掉 confirm)
**问题**:企微 App 端没有"确认登录" UI,旧 confirm 步骤在生产永远走不通(用户报告"扫码成功后,手机端没有出现确认登录按钮")
**用户决策**:方案 A — 扫码成功 = 自动登录(推荐),2026-06-24 拍板
**修复**:`backend/app/services/qrcode_service.py` `process_scan` 默认 `auto_confirm=True`
- 扫码成功直接写 `qrcode:confirm:{ticket}`(含 token)
- 前端 poll 立即拿到 status=confirmed + token
- 跳过手机 confirm 步骤
- 默认 roles=["user"],admin/agent 走 portal 端角色选择
**测试**:20/20 pytest 过(`test_scan_then_poll_returns_confirmed` + `test_scan_auto_confirm_writes_confirm_key` 等)
**部署**:`deploy-staging/hotfix-120-qrcode-auto-confirm/` — webcli v7 一键,容器内 `/api/ready` 200 OK
**残留**:外部域名 `https://itsupport.servyou.com.cn/api/ready` 不通(nginx `/api/` upstream 配错 + 8000 端口未暴露),**等 #48 修复**。容器内 backend 完全健康。
@@ -0,0 +1,186 @@
# 智能IT支持服务台 - 版本更新说明
**版本**: v1.1.0
**更新日期**: 2026-06-14
**文档状态**: 待审核
---
## 一、本次更新内容
### 1.1 新增功能
| 功能 | 说明 | 优先级 |
|------|------|--------|
| 消息撤回 | 2分钟内可撤回自己的消息 | P0 |
| 消息删除 | 删除自己的消息 | P0 |
| 消息状态 | 支持 sending/sent/delivered/read/recalled 状态 | P0 |
| 标记已读 | 一键标记会话已读 | P1 |
| 图片上传 | 支持图片上传(≤10MB) | P1 |
| 文件上传 | 支持文件上传(≤10MB) | P1 |
### 1.2 架构优化
| 优化项 | 说明 |
|--------|------|
| Health Check | 所有容器已配置健康检查 |
| 自动重启 | 容器崩溃自动重启(restart: unless-stopped |
| AI Gateway 设计 | 预留多模型切换架构 |
### 1.3 安全增强
| 功能 | 说明 |
|------|------|
| OTP 双因素认证 | 访问管理后台时二次验证 |
| 操作审计 | 关键操作日志记录(规划中) |
---
## 二、需要同步的代码
### 2.1 后端文件
| 文件 | 改动 | 说明 |
|------|------|------|
| `app/models/message.py` | 修改 | 添加 status、recallable_until 字段 |
| `app/api/messages.py` | 修改 | 添加撤回/删除/标记已读/上传 API |
| `app/api/ws_manager.py` | 修改 | 添加消息状态广播 |
| `docker-compose.yml` | 修改 | healthcheck 已配置 |
### 2.2 数据库变更
```sql
-- 需要执行的 SQL 迁移
ALTER TABLE messages ADD COLUMN status VARCHAR(20) DEFAULT 'sent';
ALTER TABLE messages ADD COLUMN recallable_until TIMESTAMP;
```
### 2.3 前端文件(如果有)
| 文件 | 说明 |
|------|------|
| 消息操作菜单 | 撤回/删除按钮 |
| 消息状态显示 | 状态图标 |
| 已读标记 | 一键已读 |
---
## 三、部署步骤
### 3.1 本地打包
```bash
# 后端打包
cd backend
docker build -t wecom-it-desk-backend:latest .
# 导出镜像
docker save wecom-it-desk-backend:latest -o wecom-it-desk-backend.tar
```
### 3.2 服务器部署
```bash
# 1. 上传镜像到堡垒机
# 堡垒机: sxn@10.212.189.210:2222 (OTP)
# 目标路径: /tmp/
# 2. SSH 到正式服务器
ssh sxn@10.212.189.210 -p 2222
ssh 10.90.5.110
# 3. 导入镜像
docker load -i /tmp/wecom-it-desk-backend.tar
# 4. 解决容器冲突(重要!)
docker rm -f wecom_it_redis wecom_it_backend wecom_it_postgres wecom_it_nginx 2>/dev/null
# 5. 重新启动
docker compose -p root up -d
# 6. 执行数据库迁移
docker compose exec backend python -c "from app.database import engine; engine.execute('ALTER TABLE messages ADD COLUMN status VARCHAR(20) DEFAULT 'sent'')"
```
---
## 四、验证清单
### 4.1 健康检查
| 检查项 | 命令 | 预期结果 |
|--------|------|----------|
| 后端服务 | `curl -s http://localhost:8000/health` | `{"status": "ok"}` |
| Nginx | `curl -s http://localhost:80/itdesk/health` | `{"status": "ok"}` |
| 数据库 | `docker compose exec backend python -c "from app.database import engine; print('OK')"` | OK |
| Redis | `docker compose exec redis redis-cli ping` | PONG |
### 4.2 API 测试
| API | 方法 | 测试命令 |
|-----|------|----------|
| 撤回消息 | POST | `curl -X POST http://localhost:8000/api/messages/{id}/recall` |
| 删除消息 | DELETE | `curl -X DELETE http://localhost:8000/api/messages/{id}` |
| 标记已读 | POST | `curl -X POST http://localhost:8000/api/conversations/{id}/mark-read` |
| 图片上传 | POST | `curl -X POST -F "file=@test.jpg" http://localhost:8000/api/messages/image` |
### 4.3 功能测试
| 功能 | 测试场景 | 预期结果 |
|------|----------|----------|
| 消息发送 | 发送文本消息 | 消息正常显示 |
| 消息撤回 | 2分钟内撤回 | 状态变为 recalled |
| 消息撤回 | 超过2分钟 | 返回 403 错误 |
| 标记已读 | 点击已读 | 所有消息标记为已读 |
---
## 五、已知问题与限制
### 5.1 待解决
| 问题 | 状态 | 说明 |
|------|------|------|
| 容器冲突 | 进行中 | 需指定项目名 `-p root` |
| 前端同步 | 待确认 | 可能需要更新前端代码 |
### 5.2 已知限制
| 限制 | 说明 |
|------|------|
| 单节点部署 | MVP 阶段保持单节点 |
| 文件大小 | 单文件 ≤10MB |
| 撤回时间 | 2分钟后不可撤回 |
---
## 六、回滚方案
如果部署失败,执行:
```bash
# 停止服务
docker compose -p root down
# 恢复旧镜像(如果有备份)
docker load -i wecom-it-desk-backend-old.tar
# 使用备份的配置启动
docker compose -p root up -d
```
---
## 七、联系人
| 角色 | 联系人 | 说明 |
|------|--------|------|
| 产品负责人 | 许清楚 | PRD 确认 |
| 技术负责人 | 寇豆码 | 代码审查 |
| QA 负责人 | 严过关 | 测试验证 |
| 运维负责人 | 宋献 | 部署执行 |
---
**文档版本**: 1.0
**审核状态**: 待审核
@@ -0,0 +1,131 @@
# OTP 二次验证实现文档
> **关联文档(认证模块三件套)**:本文档为运维侧实现说明,与以下文档形成闭环 ——
> - PRD`../../01-产品文档/01-认证与登录/PRD-REQ-认证-统一认证与登录-v1.1.md`
> - 技术方案:`../../02-技术文档/技术方案-REQ-认证-统一认证与登录-v1.0.md`
> - 测试用例:`../../03-测试文档/03-功能测试用例/TC-REQ-认证-统一认证与登录-v1.0.md`
>
> **版本对齐说明(2026-08-05 修正)**:本文档原始内容描述的是 **v0.5.6 时期的 `otp_*` 旧设计**`/api/agents/otp-*` + `otp_secret`/`otp_enabled` 字段)。该设计已在 **AUTH-03 三端认证重构(v0.7.x)** 中被统一取代:
> - 端点前缀由 `/api/agents/otp-*` 改为 **`/api/auth/otp-*`**(路由文件 `app/api/otp.py`prefix=`/auth`);
> - 模型字段由 `otp_secret`/`otp_enabled` 改为 **`mfa_secret`/`mfa_enabled`/`mfa_bound_at`/`mfa_last_verified_at`**migration 023 新增、026 删除旧 `otp_*` 字段);
> - 旧 `otp_secret`/`otp_enabled` 字段已于 **migration 026v0.7.1** 彻底删除。
>
> 本次修正已将上述内容对齐到**当前生产实现**(以 `app/api/otp.py` + `app/api/agents.py` 为准)。注意:部署指南 `07-扫码登录OTP部署指南-v0.7.0.md` 与测试用例仍引用更早的 `/api/mfa/*` 规划命名,同样需后续对齐到 `/api/auth/otp-*`(见技术方案 §2.3)。
## 功能概述
为 IT 支持服务台坐席端增加 OTP 二次验证(MFA)功能:
- admin 角色登录时需要输入 Google Authenticator / 兼容 TOTP 应用的动态码
- 首次登录且未绑定时,引导完成首次绑定(不签发 token)
- OTP 丢失后由管理员重置
## 实现方案
### 1. 后端修改
#### 1.1 安装依赖
```bash
pip install pyotp qrcode[pil] pillow
```
#### 1.2 数据库模型
`agents` 表当前 MFA 相关字段(见 `app/models/agent.py`,由 migration 023 新增):
- `mfa_secret`: TOTP 共享密钥(Base32 编码,绑定时生成,验证启用前不算启用)
- `mfa_enabled`: MFA 是否启用(默认 false,首次验证成功后置 true)
- `mfa_bound_at`: 首次绑定完成时间(可空,用于审计与回收策略)
- `mfa_last_verified_at`: 最近一次验证成功时间(可空,安全审计用)
> ⚠️ 历史字段 `otp_secret` / `otp_enabled` 已在 **migration 026v0.7.1** 删除,代码中不再存在,请勿沿用。
#### 1.3 Schema 修改
文件:`backend/app/schemas/agent.py`
- `AgentLogin`:含 `otp_code: Optional[str]` 字段(admin 角色登录必填,6 位数字 TOTP 动态码)
- `AgentResponse`:返回绑定状态字段(注:response schema 中字段名仍写作 `otp_enabled`,属于已知技术债,与模型 `mfa_enabled` 对应,不影响端点行为;详见技术方案 §2.3)
#### 1.4 API 修改
**OTP 管理端点**(文件:`backend/app/api/otp.py`,路由器 `prefix="/auth"`tags=["OTP二次认证"]):
| 方法 | 端点 | 说明 |
|------|------|------|
| GET | `/api/auth/otp-status` | 查询当前用户 MFA 绑定状态 |
| POST | `/api/auth/otp-bind` | 生成 TOTP secret + 二维码(写入 `mfa_secret``mfa_enabled` 保持 false |
| POST | `/api/auth/otp-verify` | 验证动态码:首次绑定(mfa_enabled=False 有 secret)→ 启用 MFA 并签发 token;已绑定 → 常规验证并写 Redis 30 分钟标记 |
| POST | `/api/auth/otp-unbind` | 用户主动解绑,清空 `mfa_secret`/`mfa_enabled`/`mfa_bound_at` |
| POST | `/api/auth/otp-admin-reset/{employee_id}` | 管理员重置指定员工 MFA(清空其 `mfa_*` 字段) |
| GET | `/api/auth/otp-admin-users` | 管理员查看全部坐席 MFA 绑定状态列表 |
**登录接口 MFA 校验**(文件:`backend/app/api/agents.py`,函数 `agent_login`):
- `mfa_enabled=True` 且未提供 `otp_code` → 返回 `require_otp: True`,前端弹 OTP 输入框
- `mfa_enabled=False`(未绑定)→ 返回 `require_otp_bind: True`,引导首次绑定流程(不签发 token)
- 提供正确 `otp_code` → 经 `MFAService.verify_code(agent.mfa_secret, otp_code)` 校验通过后签发 token
### 2. 前端修改
#### 2.1 坐席端 API
文件:`frontend-agent/src/api/agent.ts`
- `login()` 增加 `otpCode` 参数
#### 2.2 坐席端 Store
文件:`frontend-agent/src/stores/agent.ts`
- `login()` 增加 `otpCode` 参数
- 处理 `require_otp` / `require_otp_bind` 标记,驱动页面分流
#### 2.3 坐席端登录页面
文件:`frontend-agent/src/views/Login.vue`
- 增加 OTP 输入框(`v-if="requireOtp"`
- 首次登录返回 `require_otp_bind` 时引导跳转到绑定流程
#### 2.4 坐席端绑定页面(新增)
文件:`frontend-agent/src/views/MfaBind.vue`
- 展示二维码 + 密钥,输入动态码完成首次绑定
#### 2.5 高危操作 OTP 守卫(新增)
文件:`frontend-agent/src/composables/useHighRiskOtp.ts`
- 调用高危操作前若 30 分钟内未验证过 OTP,弹 OTP 输入框 → 调 `/api/auth/otp-verify` → 重试
#### 2.6 管理端 MFA 管理(新增)
文件:`frontend-admin/src/views/MfaManage.vue`
- 管理员查看坐席 MFA 绑定状态、重置指定员工 MFA
## 使用流程
### 首次绑定 OTP
1. 管理员登录坐席端(此时 `mfa_enabled=False`,返回 `require_otp_bind`
2. 跳转绑定页,调用 `POST /api/auth/otp-bind` 获取二维码与密钥
3. 使用 Google Authenticator / 兼容 TOTP 应用扫描二维码
4. 调用 `POST /api/auth/otp-verify` 输入动态码验证
5. 验证成功,`mfa_enabled` 置 1、`mfa_bound_at` 写入,直接签发 token
### 登录流程
1. 用户输入 user_id 和 name
2. 后端检查 `mfa_enabled`
3. 已绑定(`mfa_enabled=True`)且无 `otp_code` → 返回 `require_otp: True`
4. 前端显示 OTP 输入框
5. 用户输入 6 位动态码,再次登录
6. 后端验证通过,生成 token
### 解绑流程
1. 管理员 / 用户调用 `POST /api/auth/otp-unbind`
2. `mfa_secret` / `mfa_enabled` / `mfa_bound_at` 清空(`mfa_last_verified_at` 保留为审计记录)
### 管理员重置
1. 管理员调用 `POST /api/auth/otp-admin-reset/{employee_id}`
2. 目标员工的 `mfa_*` 字段清空,下次登录将重新引导绑定
## 错误码
| 错误码 | 说明 | 当前状态 |
|--------|------|----------|
| 1006 | OTP 验证码错误 | ✅ 仍在使用(`agents.py` 登录校验 `mfa_enabled=True` 分支) |
| 1007 | OTP 绑定失败 | ⚠️ 遗留数字码,原始 `otp` 设计;当前 `otp.py` 改用 `success_response` / `ErrorCode` 枚举(`E-prefix`),建议以代码为准复核 |
| 1008 | 请先绑定 OTP | ⚠️ 同上(当前由 `require_otp_bind: True` 表达,非错误码) |
| 1009 | OTP 验证失败 | ⚠️ 同上 |
| 1010 | OTP 解绑失败 | ⚠️ 同上 |
> 说明:当前错误码主体系为 `E{模块}{序号}` 枚举(见 `app/utils/error_codes.py`,如 `E1001` 认证失败)。上表数字码为 v0.5.6 遗留,仅 `1006` 在登录流程中仍被 `AppException(1006, ...)` 直接抛出;`1007``1010` 应结合 `otp.py` 实际返回复核。
---
**变更历史**
- 2026-08-05 修正端点命名与字段体系,对齐 AUTH-03 生产实现(`/api/auth/otp-*` + `mfa_*` 字段),移除与 `/api/mfa/*` 的过时对照
@@ -0,0 +1,225 @@
# 部署手册:扫码登录 + OTP 二次认证(Phase 1+2)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1+2)
> 部署顺序:后端 → 前端 4 端 → nginx → 数据库 migration → 验收
>
> ⚠️ **端点命名修正(2026-08-05**:本文档原始使用 `/api/mfa/*` 与 `/api/admin/mfa/reset/{employee_id}`,该命名为 AUTH-03 三端认证重构前的规划命名。当前生产实现已统一为 **`/api/auth/otp-*`**(含 `/api/auth/otp-admin-reset/{employee_id}`),详见 `06-OTP二次验证实现.md` 与技术方案。下方正文已同步修正。
---
## 🎯 部署目标
从 v0.6.x 的"企微 OAuth + SMS 2FA"升级到 v0.7.0 的"扫码登录 + OTP TOTP + SMS 备用"。
涉及后端变更:
- 新增 `/api/auth_qrcode/*` 4 个端点(扫码登录)
- 新增 `/api/auth/otp-*` 6 个端点(OTP 二次认证)
- 新增 `/api/auth/otp-admin-reset/{employee_id}`(管理员重置)
- 新增 `/api/admin/high-risk/*` 演示端点 + require_high_risk_otp 守卫
- 新增 4 个数据库字段: `users.mfa_secret`, `users.mfa_enabled`, `users.mfa_bound_at`, `users.mfa_last_verified_at`
涉及前端变更:
- frontend-agent:Login.vue 重写(扫码 UI)+ 新增 MfaBind.vue + useHighRiskOtp
- frontend-portal:新增 QrcodeLogin.vue + 默认路由
- frontend-admin:新增 MfaManage.vue(管理员 MFA 重置 UI)
- frontend-h5:**不变**(仍走企微 OAuth)
涉及 nginx 变更:
- `/itportal/` 新增 location(扫码入口)
- 其余 4 个 location 已有,配置按 docs/NGINX-DOMAIN-ROUTING.md
---
## 📋 部署前检查
### 1. 后端镜像依赖
`backend/requirements.txt` 必须包含:
```
pyotp==2.9.0 # TOTP 生成
qrcode[pil]==7.4.2 # 二维码生成
redis==5.0.7 # 已存在
```
### 2. 数据库迁移文件
确认以下 migration 已存在:
- `backend/alembic/versions/023_mfa_fields.py`(加 4 个 MFA 字段)
- `backend/alembic/versions/024_*.py`(可选:其他变更)
### 3. 配置文件
`backend/.env` 确认:
```bash
# 新增(扫码登录)
WECOM_OAUTH_REDIRECT_URI=https://itsupport.servyou.com.cn/itportal/qrcode-callback
WECOM_CORP_ID=ww1234567890abcdef
WECOM_AGENT_ID=1000002
# 已有(OTP)
SMS_2FA_ENABLED=true # 蜂鸟 SMS 备用通道
```
### 4. 域名 / DNS
- `itsupport.servyou.com.cn`(主域名,已有)
- 子路径:`/itportal/` `/itagent/` `/itadmin/` `/itdesk/`(同一域名,nginx 分发)
- 证书:`itsupport.servyou.com.cn.crt`(公司统一管理)
---
## 🚀 部署步骤
### 步骤 1:部署后端(注意 RO bind mount)
```bash
# 1. 上传新 backend 包到堡垒机
scp backend-v070-p1.tar.gz user@bastion:/tmp/
# 2. 通过堡垒机 PuTTY(不用 ssh -J)登录生产服务器
# 参考:feedback-putty-not-openssh.md
# 3. 解压并复制到 backend 目录(走宿主机路径,避开 RO bind mount 陷阱)
cd /opt/wecom-it-desk/
tar -xzf /tmp/backend-v070-p1.tar.gz
# 注意:用 cp -r 不是 docker cp(避开 RO bind mount 假成功陷阱)
sudo cp -r backend-v070-p1/* backend/
# 4. 数据库 migration
cd /opt/wecom-it-desk/backend
sudo docker exec wecom_it_backend alembic upgrade head
# 验证:
sudo docker exec wecom_it_backend alembic current
# 期望:023_mfa_fields (head)
# 5. 重启 backend(注意:backend 不在 compose 里,直接 docker restart)
sudo docker restart wecom_it_backend
# 验证:等待 ~30s,看健康检查
curl http://localhost:8000/health
# 期望:{"status":"ok",...}
```
### 步骤 2:部署前端 4 端
```bash
# 1. 各前端 build(本地)
cd frontend-agent && npm run build
cd frontend-portal && npm run build
cd frontend-admin && npm run build
# frontend-h5 不变,不用 build
# 2. 上传 dist 到生产服务器
scp -r frontend-agent/dist user@bastion:/tmp/agent-dist/
scp -r frontend-portal/dist user@bastion:/tmp/portal-dist/
scp -r frontend-admin/dist user@bastion:/tmp/admin-dist/
# 3. 通过堡垒机,复制到 nginx 容器挂载的目录
# 路径可能是 /opt/wecom-it-desk/frontend-*/
sudo cp -r /tmp/agent-dist/* /opt/wecom-it-desk/frontend-agent/dist/
sudo cp -r /tmp/portal-dist/* /opt/wecom-it-desk/frontend-portal/dist/
sudo cp -r /tmp/admin-dist/* /opt/wecom-it-desk/frontend-admin/dist/
# 4. 验证:curl HTML 文件
curl -I https://itsupport.servyou.com.cn/itportal/
# 期望:200 OK,content-type: text/html
```
### 步骤 3:更新 nginx 配置
```bash
# 1. 上传新 nginx 配置
# 新增 /itportal/ location,更新其他 location
# 参考:docs/NGINX-DOMAIN-ROUTING.md
# 2. 验证配置(在 nginx 容器里)
sudo docker exec wecom_it_nginx nginx -t
# 注意容器名是 wecom_it_nginx 不是 wecom-nginx
# 期望:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 3. reload(不重启容器)
sudo docker exec wecom_it_nginx nginx -s reload
```
### 步骤 4:验收测试
按 docs/NGINX-DOMAIN-ROUTING.md 末"验证清单"逐条测试。
---
## 🔄 回滚方案
### 后端回滚
```bash
# 1. 用上次 patch1 备份
sudo cp -r /opt/wecom-it-desk/backend-v070-patch1/* /opt/wecom-it-desk/backend/
sudo docker restart wecom_it_backend
# 2. 数据库回滚(谨慎!)
sudo docker exec wecom_it_backend alembic downgrade -1
# 注意:只能降 1 个版本,如果已经升到 023,降到 022
```
### 前端回滚
```bash
# 直接覆盖 dist
sudo cp -r /opt/wecom-it-desk/frontend-*-bak/* /opt/wecom-it-desk/frontend-*/dist/
```
### nginx 回滚
```bash
# 容器内 sed -i 改回旧配置(避开 RO bind mount 假成功陷阱)
sudo docker exec wecom_it_nginx cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf
sudo docker exec wecom_it_nginx nginx -s reload
```
---
## ⚠️ 已知风险
| 风险 | 影响 | 缓解 |
|---|---|---|
| OTP 二维码渲染失败(后端 base64 生成出错) | 用户绑不上 OTP | 前端降级显示 qrcode_url 让用户手动复制 |
| pyotp 库版本升级导致不兼容 | OTP 验证失败 | 锁版本 pyotp==2.9.0,生产前跑 pytest |
| Admin MFA 重置端点被未授权访问 | 安全 | require_admin + 后续可加 IP 白名单 |
| 蜂鸟 SMS API 未上线 | 备用通道不可用 | 不影响 OTP 主通道,先上线 OTP,后接 SMS |
| nginx IP 白名单临时全开 | 安全 | v1.0 前必须收窄(task #48) |
---
## 📊 部署后验证
### 业务指标
- [ ] 扫码登录成功率 > 95%
- [ ] OTP 验证成功率 > 99%
- [ ] 高危操作 OTP 触发率 100%
- [ ] 蜂鸟 SMS fallback 触发 < 5%(绝大多数人用 OTP)
### 技术指标
- [ ] 扫码登录端到端 < 5s(从扫码到进入工作台)
- [ ] OTP 验证 < 500ms
- [ ] 高危操作 OTP 弹窗响应 < 200ms
---
## 📚 相关文档
- [PRD-REQ-认证-统一认证与登录-v1.1.md](../../01-产品文档/01-认证与登录/PRD-REQ-认证-统一认证与登录-v1.1.md) — 认证模块产品需求
- [技术方案-REQ-认证-统一认证与登录-v1.0.md](../../02-技术文档/技术方案-REQ-认证-统一认证与登录-v1.0.md) — 认证模块技术方案
- [USER-GUIDE-QRCODE-MFA.md](./USER-GUIDE-QRCODE-MFA.md) — 用户手册
- [NGINX-DOMAIN-ROUTING.md](./NGINX-DOMAIN-ROUTING.md) — nginx 域名分发
- [v070-alpha-deploy-runbook.md](../memory/v070-alpha-deploy-runbook.md) — v0.7.0-alpha 总览
- [docker-cp-readonly-bind-mount-fake-success.md](../memory/docker-cp-readonly-bind-mount-fake-success.md) — RO bind mount 陷阱
- [nginx-container-name-wecom-it-nginx.md](../memory/nginx-container-name-wecom-it-nginx.md) — 容器名坑
- [feedback-putty-not-openssh.md](../memory/feedback-putty-not-openssh.md) — 堡垒机 PuTTY
---
**变更历史**:
- 2026-06-21 创建(Phase 1+2 部署手册)
- 2026-08-05 端点命名修正:`/api/mfa/*``/api/auth/otp-*``/api/admin/mfa/reset/{employee_id}``/api/auth/otp-admin-reset/{employee_id}`(AUTH-03 重构后的真实实现);数据库字段描述由"2 个"更正为"4 个"
@@ -0,0 +1,227 @@
# 企微智能IT支持服务台 — 远程服务器部署指南(预生产)
> **预生产环境**:本系统与 IT 数据查询平台部署在**不同主机**。正式环境将迁移到 K8s。
## 部署架构
```
浏览器 ──→ it-dataquery.dc.servyou-it.com:80
┌─── nginx (本系统主机) ──────────────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /api/* → backend:8000 (FastAPI) │
│ /ws/* → backend:8000 (WebSocket) │
│ /* → 远程代理到数据平台主机 IP │ ← 跨主机
│ │
└──────────────┬─────────────────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
## 网络互联
预生产环境中,数据平台在独立主机,**不需要 Docker 网络互联**。Nginx 通过远程 IP 直接反代数据平台:
```
本系统主机 (Docker) 数据平台主机
┌──────────────────┐ ┌─────────────────┐
│ nginx ──────────┼── HTTP 反代 ──→ │ 数据平台 :80 │
│ │ │ 远程 IP │ │
│ ▼ │ └─────────────────┘
│ backend:8000 │
│ postgres:5432 │
│ redis:6379 │
└──────────────────┘
```
## 前置条件
- 服务器已安装 Docker + Docker Compose
- IT 数据查询平台已部署运行
- 有 SSH 登录权限
## 部署步骤
### 1. 配置数据平台反代地址
预生产环境中,数据平台在**独立主机**。部署前,必须将 nginx 配置中的数据平台上游地址改为实际 IP。
编辑 `nginx/nginx.conf`
```nginx
# 将 DATAQUERY_HOST 替换为数据平台主机的实际 IP:端口
upstream dataquery {
server 10.80.0.86:80; # ← 改为数据平台实际 IP
}
```
> 不再需要创建 `it-platform-net` —— Docker 网络无法跨主机互联。nginx 通过 HTTP 直接反代到远程 IP。
### 3. 上传部署包
在本地(Windows)执行:
```bash
# 方式 A:使用 deploy.sh 打包
bash scripts/deploy.sh --pack
scp it-smart-desk-*.tar.gz user@server:/opt/
# 方式 B:手动打包
tar czf deploy.tar.gz \
backend/ frontend-h5/dist/ frontend-agent/dist/ \
nginx/ docker-compose.yml .env.production scripts/
scp deploy.tar.gz user@server:/opt/it-smart-desk/
```
### 4. 服务器上解压和配置
```bash
ssh user@server
cd /opt/it-smart-desk
tar xzf it-smart-desk-*.tar.gz
# 创建环境配置(填入真实企微凭证)
cp .env.production .env
vim .env
```
`.env` 必填项:
| 配置项 | 说明 | 获取位置 |
|--------|------|---------|
| `WECOM_CORP_ID` | 企业ID | 企微管理后台 > 我的企业 |
| `WECOM_AGENT_ID` | 应用AgentId | 企微管理后台 > 应用管理 |
| `WECOM_SECRET` | 应用Secret | 企微管理后台 > 应用管理 |
| `WECOM_TOKEN` | 回调Token | 企微管理后台 > 接收消息 |
| `WECOM_ENCODING_AES_KEY` | 回调AES密钥 | 企微管理后台 > 接收消息 |
| `POSTGRES_PASSWORD` | 数据库密码 | 自定义强密码 |
### 5. 启动服务
```bash
bash scripts/deploy.sh
```
这会自动执行:检查前置条件 → 构建后端镜像 → 启动所有容器
### 6. 验证部署
```bash
# 检查容器状态
docker compose ps
# 健康检查
curl http://localhost:18080/itdesk/health
# 查看 H5 员工端
curl -I http://localhost:18080/itdesk/
# 查看坐席工作台
curl -I http://localhost:18080/itagent/
# 查看后端 API
curl http://localhost:18080/api/docs
```
浏览器验证:
| 地址 | 预期 |
|------|------|
| `http://it-dataquery.dc.servyou-it.com/itdesk/` | H5 员工咨询页面 |
| `http://it-dataquery.dc.servyou-it.com/itagent/` | 坐席工作台登录页 |
| `http://it-dataquery.dc.servyou-it.com/` | IT 数据查询平台(不变) |
| `http://it-dataquery.dc.servyou-it.com/api/docs` | FastAPI Swagger 文档 |
## 两种网络接入方式
### 方式 A:数据平台 nginx 反代到本项目 nginx(推荐)
数据平台的 nginx 配置添加:
```nginx
# IT 智能服务台路由
location /itdesk/ {
proxy_pass http://wecom_it_nginx:80/itdesk/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /itagent/ {
proxy_pass http://wecom_it_nginx:80/itagent/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/ {
proxy_pass http://wecom_it_nginx:80/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /ws/ {
proxy_pass http://wecom_it_nginx:80/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
此方式下本项目 nginx 不暴露 80 端口,docker-compose.yml 的 `ports` 可以删除。
### 方式 B:本项目 nginx 直接监听 80 端口
如果数据平台没有自己的 nginx,或者想用本项目的 nginx 统一管理:
1. 停止数据平台的端口映射
2. 本项目 nginx 的 80 端口直接对外
3. nginx.conf 中的 `location /` 反代到数据平台容器
## 常见问题
### Q: nginx 启动失败,报 `host not found in upstream "dataquery"`
A: `nginx/nginx.conf` 中的 `DATAQUERY_HOST` 未替换为数据平台实际 IP。编辑 nginx.conf,将占位符改为实际 IP:端口。
### Q: 访问 /itdesk/ 返回 404
A: 检查前端 dist 是否正确挂载:
```bash
docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itdesk/
```
### Q: API 返回 CORS 错误
A: 检查 `.env` 中的 `CORS_ORIGINS` 是否包含 `http://it-dataquery.dc.servyou-it.com`
### Q: 数据库迁移失败
A: 查看 backend 日志:
```bash
docker compose logs backend
```
如果 PostgreSQL 未就绪,等 30 秒后重启 backend
```bash
docker compose restart backend
```
## 更新部署
只需更新变更的部分:
```bash
# 仅更新前端
bash scripts/deploy.sh --build
docker compose restart nginx
# 仅更新后端
docker compose build backend
docker compose up -d backend
# 全量更新
bash scripts/deploy.sh --down
bash scripts/deploy.sh
```
@@ -0,0 +1,252 @@
# v0.7.0 一键部署操作包(给生产运维)
> **目的**:把所有部署命令按顺序排好,生产运维复制粘贴即可完成 v0.7.0 部署。
> **预计时间**:15-20 分钟(含等 docker pull)
> **回滚**:每步都有 rollback 命令,任意一步失败立即回滚。
---
## 🔴 部署前 必做(用户自己操作)
### 1. 撤销并重签 Gitea token
```
1. 浏览器打开 http://100.85.152.112:8418
2. 右上角头像 → Settings → Applications → Manage Access Tokens
3. 找到旧 token(workbuddy-claude),点 Revoke
4. 点 Generate New Token,scope 选 "All",点 Generate
5. 复制新 token(只显示一次),临时存到 ~/Downloads/gitea-new-token.txt
```
### 2. 推送代码到 Gitea(用新 token)
```bash
# 在本地工作目录(D:\资料\03-项目开发\wecom_it_smart_desk-claude\backend)
cd /d/资料/03-项目开发/wecom_it_smart_desk-claude
# 临时把新 token 加进 remote URL(push 后立刻删除)
git remote set-url origin "http://workbuddy-claude:新TOKEN@100.85.152.112:8418/simon/wecom_it_smart_desk.git"
# 推送 main + tag
git push origin main
git push origin v0.7.0
# push 成功后,立刻从 URL 移除 token
git remote set-url origin "http://workbuddy-claude@100.85.152.112:8418/simon/wecom_it_smart_desk.git"
# 验证 token 已移除
git remote -v
# 期望:没有 token 字样
```
---
## 🟢 部署操作(在生产服务器,SSH/PuTTY)
> 服务器 IP: **10.90.5.110** (内网),**115.236.188.3** (公网入口)
> SSH 用户:堡垒机登录后跳转
### 步骤 1/6:备份当前生产状态
```bash
# 1.1 备份 backend 当前镜像
sudo docker tag wecom-it-desk-backend:latest wecom-it-desk-backend:v0.6.0-backup
# 1.2 备份 4 端 dist
sudo mkdir -p /opt/wecom-it-desk/dist-backup-2026-06-21
sudo cp -r /opt/wecom-it-desk/frontend-admin/dist /opt/wecom-it-desk/dist-backup-2026-06-21/admin
sudo cp -r /opt/wecom-it-desk/frontend-agent/dist /opt/wecom-it-desk/dist-backup-2026-06-21/agent
sudo cp -r /opt/wecom-it-desk/frontend-portal/dist /opt/wecom-it-desk/dist-backup-2026-06-21/portal
sudo cp -r /opt/wecom-it-desk/frontend-h5/dist /opt/wecom-it-desk/dist-backup-2026-06-21/h5
echo "备份完成"
# 1.3 备份 alembic 版本号(用于回滚确认)
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "SELECT version_num FROM alembic_version;"
```
### 步骤 2/6:拉新 backend 镜像并跑 migration
```bash
# 2.1 拉新镜像
sudo docker pull wecom-it-desk-backend:v0.7.0
# 2.2 跑 migration(只 PG,SQLite 跳过)
sudo docker exec wecom_it_backend alembic upgrade head
# 期望输出:
# Running upgrade 024 -> 025, messages.id UUID
# Running upgrade <old> -> 022, qrcode_login
# Running upgrade <old> -> 023, mfa_fields
# 2.3 验证 migration head
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "SELECT version_num FROM alembic_version;"
# 期望:025_messages_id_uuid
# 2.4 验证 messages.id 已改为 UUID
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "\d messages" | grep "^ id"
# 期望:类型为 uuid
```
**🚨 若 migration 失败**:
```bash
sudo docker exec wecom_it_backend alembic downgrade -1
# 联系 Claude 排查
```
### 步骤 3/6:重启 backend 容器
```bash
# 3.1 重启(用 v0.7.0 镜像)
sudo docker restart wecom_it_backend
# 3.2 等 10 秒,检查启动日志
sudo docker logs wecom_it_backend --tail 50
# 期望看到:
# Application startup complete
# Uvicorn running on http://0.0.0.0:8000
# 没有 "ModuleNotFoundError" / "relation already exists" / "Restarting" 循环
# 3.3 健康检查
sudo docker ps | grep wecom_it_backend
# 期望:STATUS = Up X minutes (healthy)
```
**🚨 若 backend 启动失败,回滚**:
```bash
sudo docker tag wecom-it-desk-backend:v0.6.0-backup wecom-it-desk-backend:latest
sudo docker restart wecom_it_backend
```
### 步骤 4/6:上传 4 端 dist 到宿主机
```bash
# 4.1 在本地(Windows)打包 4 端 dist
cd /d/资料/03-项目开发/wecom_it_smart_desk-claude
tar -czf /tmp/frontend-v0.7.0.tar.gz \
frontend-admin/dist frontend-agent/dist frontend-portal/dist frontend-h5/dist
ls -la /tmp/frontend-v0.7.0.tar.gz
# 4.2 上传到生产服务器(走堡垒机)
scp /tmp/frontend-v0.7.0.tar.gz <堡垒机用户>@<堡垒机>:/tmp/
# 4.3 在生产服务器解压
ssh <堡垒机> # 跳到生产
cd /opt/wecom-it-desk
sudo tar -xzf /tmp/frontend-v0.7.0.tar.gz
ls -la frontend-*/dist | head -20
# 期望:每个 dist 都有 index.html + assets/
# 4.4 清理压缩包
sudo rm /tmp/frontend-v0.7.0.tar.gz
```
**🚨 若上传失败,回滚**:
```bash
# 4 端用备份恢复
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/admin/* /opt/wecom-it-desk/frontend-admin/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/agent/* /opt/wecom-it-desk/frontend-agent/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/portal/* /opt/wecom-it-desk/frontend-portal/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/h5/* /opt/wecom-it-desk/frontend-h5/dist/
```
### 步骤 5/6:应用 nginx access_log 脱敏 + reload
```bash
# 5.1 验证当前 nginx 容器名(下划线不是横杠!)
sudo docker ps | grep wecom_it_nginx
# 期望:0.0.0.0:80->80/tcp wecom_it_nginx
# 5.2 进入容器加 log_format 脱敏配置
sudo docker exec wecom_it_nginx bash -c '
cat > /etc/nginx/conf.d/log-format.conf << "EOF"
log_format secure $remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_referer" "$http_user_agent";
access_log /var/log/nginx/access.log secure;
EOF
'
# 验证写入
sudo docker exec wecom_it_nginx cat /etc/nginx/conf.d/log-format.conf
# 5.3 验证配置
sudo docker exec wecom_it_nginx nginx -t
# 期望:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 5.4 reload(不重启容器)
sudo docker exec wecom_it_nginx nginx -s reload
# 5.5 验证 reload 生效
sudo docker exec wecom_it_nginx tail -3 /var/log/nginx/access.log
# 期望:没有 Authorization: Bearer xxx 字样
```
**🚨 若 nginx reload 失败**:
```bash
# 恢复默认 access_log
sudo docker exec wecom_it_nginx bash -c 'echo "access_log /var/log/nginx/access.log;" > /etc/nginx/conf.d/log-format.conf'
sudo docker exec wecom_it_nginx nginx -t
sudo docker exec wecom_it_nginx nginx -s reload
```
### 步骤 6/6:验证域名路由
```bash
# 6.1 验证 4 个 location 都返回 200
curl -I https://<生产域名>/itportal/ # 应 200
curl -I https://<生产域名>/itagent/ # 应 200
curl -I https://<生产域名>/itadmin/ # 应 200
curl -I https://<生产域名>/itdesk/ # 应 200
# 6.2 验证 API 端点
curl https://<生产域名>/api/health
# 期望:{"code":0,"data":{"status":"ok"}}
# 6.3 验证扫码登录端点
curl -X POST https://<生产域名>/api/auth_qrcode/create -H "Content-Type: application/json" -d '{}'
# 期望:{"code":0,"data":{"ticket":"...","qrcode_url":"...","expires_in":120}}
# 6.4 验证 MFA 端点(无 token 应 401)
curl https://<生产域名>/api/auth/otp-status
# 期望:401 Unauthorized
```
---
## 🟡 部署后 必做(用户/QA 验收)
`docs/03-测试文档/testing-测试/E2E-CHECKLIST-v0.7.0.md` 35 项,逐项打勾。
**关键项**:
- [ ] 浏览器扫码登录全流程(5 子项)
- [ ] MFA 绑定 + 30 分钟有效期
- [ ] 高危操作守卫(5 类端点)
- [ ] WS 推送无 missing argument 错误
- [ ] 消息 ID 改为 UUID,无 500
- [ ] nginx access_log 无 Authorization/Cookie
---
## 🔴 部署后 1 周观察(用户拍板)
- 一切正常 → 清理 `/opt/wecom-it-desk/dist-backup-2026-06-21/``~/Downloads/patch1/`
- 任何 regression → 用 `DEPLOY-LOGIN-MIGRATION-v0.7.0.md` 末尾的"回滚预案"恢复
---
## 📊 部署时间预估
| 步骤 | 预计时间 | 风险 |
|---|---|---|
| 1. 备份 | 1 min | 低 |
| 2. migration | 1 min | 中(若冲突需手动) |
| 3. 重启 backend | 2 min(含等健康) | 中(若镜像问题需回滚) |
| 4. 上传 4 端 | 5 min(含上传) | 低 |
| 5. nginx reload | 1 min | 低 |
| 6. 验证 | 5 min | 低 |
| **总计** | **15 min** | |
---
## 🆘 紧急联系人
- 部署问题:本会话 + Claude
- backend 代码:Claude session
- 生产服务器:IT 基础设施组
@@ -0,0 +1,88 @@
# 堡垒机运维工具 (jumpserver-ops)
## 概述
通过 JumpServer 堡垒机自动化执行远程命令、文件上传下载。统一入口为 `jms_ops.py`,支持 4 种操作模式。
## 连接方式决策
| 场景 | 连接方式 |
|------|----------|
| **默认** | 通过堡垒机跳转(大多数内网服务器) |
| **例外** | 直连(NAS、开发机等)需单独配置 |
> **规则**:默认都需要通过堡垒机,除非明确告知某台服务器是直连。
## 使用方法
### 1. 远程命令执行(推荐)
```bash
# 单命令(纯文本输出)
python scripts/jms_ops.py exec -c "docker ps"
# 多命令串行(一次登录,5x 提速)
python scripts/jms_ops.py exec -c "hostname" -c "uptime" -c "docker ps"
# 并行模式(不冲突的长命令)
python scripts/jms_ops.py exec -c "docker logs nginx --tail 100" -c "df -h" --parallel
```
### 2. 批量命令
```bash
python scripts/jms_ops.py batch -f commands.txt
```
### 3. 文件上传
```bash
# 通过 elFinder Web UI 上传到目标服务器
python scripts/jms_ops.py upload 本地文件.conf /tmp/远程路径.conf
```
### 4. 文件下载
```bash
# 通过 base64 通道从目标服务器下载
python scripts/jms_ops.py download /远程路径.conf ./本地文件.conf
```
## 方案选择依据
| 需求 | 推荐方案 | 速度 |
|------|----------|------|
| 执行命令获取文本结果 | v16 REST API + plink PTY | 首次 ~13s,复用 ~2-3s |
| 执行命令看界面效果 | v10 Web CLI(截图) | ~60s |
| 文件上传 (< 100KB) | base64 通道 | 快 |
| 文件上传 (>= 100KB 或 > 15s) | elFinder Web UI | 稳定 |
| 文件下载 | base64 通道 | - |
## 目标服务器配置
当前预设目标:`hz-oa-ai-g-dataquery-90-5-110` (10.90.5.110)
新增直连服务器时,需提供:
- IP 地址
- 端口(默认 22
- 用户名
- 认证方式(密码或 SSH 密钥)
## 故障排除
| 问题 | 解决方法 |
|------|----------|
| 登录失败 | 检查 `config/jumpserver_config.json` 密码是否正确(Base64 编码) |
| MFA 失败 | 确认 `scripts/otp_secret.key` 存在且系统时间准确 |
| 资产未找到 | 确认目标名称与 JumpServer 中显示一致 |
## 脚本位置
```
C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py
```
## 相关文档
- [服务器部署手册](./服务器部署手册.md)
- [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md)
@@ -0,0 +1,101 @@
# 堡垒机运维工具 (jumpserver-V2)
## 概述
通过 JumpServer 堡垒机自动化执行远程命令、文件上传下载。统一入口为 `v2_ops.py`,支持多种操作模式。
> **重要**jumpserver-ops(旧版)已下线,统一使用 jumpserver-V2。
## 连接方式决策
| 场景 | 连接方式 |
|------|----------|
| **默认** | 通过堡垒机跳转(大多数内网服务器) |
| **例外** | 直连(NAS、开发机等)需单独配置 |
> **规则**:默认都需要通过堡垒机,除非明确告知某台服务器是直连。
## 使用方法
### 1. 首次登录
```bash
# 首次使用:弹出浏览器,手动填写 OTP
cd C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts
python v2_ops.py login
```
### 2. 远程命令执行(推荐)
```bash
# 单命令(免登录,复用缓存)
python v2_ops.py exec "docker ps"
# 多命令串行
python v2_ops.py exec "hostname && uptime && docker ps"
# 批量命令文件
python v2_ops.py batch commands.txt
```
### 3. 文件上传
```bash
# 上传到服务器 /tmp 目录
python v2_ops.py upload 本地文件.conf /tmp/远程路径.conf
```
### 4. 文件下载
```bash
# 从服务器 /tmp 目录下载
python v2_ops.py download /远程路径.conf ./本地文件.conf
```
### 5. 巡检(只读)
```bash
# 只读巡检:磁盘/内存/负载/CPU/容器/会话
python v2_ops.py inspect
```
## 方案选择依据
| 需求 | 推荐方案 | 速度 |
|------|----------|------|
| 执行命令获取文本结果 | V2 REST API + plink PTY | 首次 ~13s,复用 ~2-3s |
| 执行命令看界面效果 | Web CLI(截图) | ~60s |
| 文件上传 (< 100KB) | psftp 通道 | 快 |
| 文件上传 (>= 100KB) | psftp 通道 | 稳定 |
| 文件下载 | psftp 通道 | - |
| 只读巡检 | inspect 子命令 | - |
## 目标服务器配置
当前预设目标:`hz-oa-ai-g-dataquery-90-5-110` (10.90.5.110)
新增直连服务器时,需提供:
- IP 地址
- 端口(默认 22
- 用户名
- 认证方式(密码或 SSH 密钥)
## 故障排除
| 问题 | 解决方法 |
|------|----------|
| 登录失败 | 检查 `config/jumpserver_config.json` 密码是否正确(Base64 编码) |
| MFA 失败 | 确认 `scripts/otp_secret.key` 存在且系统时间准确 |
| 资产未找到 | 确认目标名称与 JumpServer 中显示一致 |
| 401 错误 | 缓存失效,自动回退浏览器重新登录 |
## 脚本位置
```
C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py
```
## 相关文档
- [服务器部署手册](./服务器部署手册.md)
- [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md)
@@ -0,0 +1,320 @@
# 企微智能IT支持服务台 — 服务器部署指南
> 目标服务器:`10.90.5.110`Linux
> 域名:`itsupport.servyou.com.cn`
> 更新日期:2026-06-12
---
## 一、前置条件
- [x] 服务器可访问内网(火绒 `huorong.oa.servyou-it.com`、Dify `yw-dify.dc.servyou-it.com`
- [ ] 服务器已安装 Docker + Docker Compose
- [ ] 域名 `itsupport.servyou.com.cn` DNS 已解析到 `10.90.5.110`(或先用 IP 访问)
---
## 二、安装 Docker(如已安装跳过)
### 2.1 检查是否已安装
```bash
docker --version # 应显示 Docker version 24.x+
docker compose version # 应显示 Docker Compose version v2.x+
```
如果已安装,跳到第三步。
### 2.2 安装 DockerCentOS/RHEL
```bash
# 1. 卸载旧版本(如有)
sudo yum remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
# 2. 安装 yum 工具
sudo yum install -y yum-utils
# 3. 添加 Docker 官方仓库(国内用阿里云镜像加速)
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 4. 安装 Docker Engine + Compose 插件
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 5. 启动 Docker 并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
# 6. 验证安装
docker --version
docker compose version
```
### 2.3 安装 DockerUbuntu/Debian
```bash
# 1. 卸载旧版本
sudo apt-get remove -y docker docker-engine docker.io containerd runc
# 2. 安装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# 3. 添加 Docker GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 4. 添加仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 安装
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 6. 启动
sudo systemctl start docker
sudo systemctl enable docker
# 7. 验证
docker --version
docker compose version
```
### 2.4(可选)非 root 用户使用 Docker
```bash
# 将当前用户加入 docker 组,避免每次 sudo
sudo usermod -aG docker $USER
# 重新登录生效
newgrp docker
```
---
## 三、上传部署包
### 3.1 在服务器创建目录
```bash
sudo mkdir -p /opt/wecom-it-desk
sudo chown $USER:$USER /opt/wecom-it-desk
```
### 3.2 上传文件
在本地 Windows 用 SCP/SFTP 上传部署包:
```powershell
# 方法1:用 scp 命令(Git Bash 或 PowerShell
scp it-smart-desk-server-deploy.zip user@10.90.5.110:/opt/wecom-it-desk/
# 方法2:用 WinSCP / FileZilla 图形化工具上传
```
### 3.3 解压
```bash
cd /opt/wecom-it-desk
unzip it-smart-desk-server-deploy.zip
# 解压后目录结构:
# /opt/wecom-it-desk/
# ├── docker-compose.yml
# ├── .env
# ├── nginx/
# │ └── nginx.conf
# ├── backend/
# │ ├── Dockerfile
# │ ├── app/
# │ ├── alembic/
# │ ├── alembic.ini
# │ └── requirements.txt
# ├── frontend-h5/dist/
# ├── frontend-agent/dist/
# └── frontend-admin/dist/
```
---
## 四、修改配置
### 4.1 编辑环境变量
```bash
cd /opt/wecom-it-desk
vim .env
```
**必须确认的配置项:**
| 配置项 | 当前值 | 说明 |
|--------|--------|------|
| `WECOM_CORP_ID` | `wwa8c87970b2011f41` | 企微企业ID |
| `WECOM_AGENT_ID` | `1000133` | 企微应用AgentId |
| `WECOM_SECRET` | `EOtQsl...` | 企微应用Secret |
| `MOCK_LOGIN_ENABLED` | `true` | 测试阶段用 true,正式上线改为 false |
| `DIFY_API_KEY` | `http://...` | Dify AI 服务 Key |
| `POSTGRES_PASSWORD` | `wecom_secret_2026` | 数据库密码(首次初始化后不可改) |
### 4.2 确认域名解析
```bash
# 测试域名是否指向本机
ping itsupport.servyou.com.cn
# 如果还没配 DNS,可以先在 .env 中把 CORS_ORIGINS 改为:
# CORS_ORIGINS=http://10.90.5.110
```
### 4.3 部署铁律(2026-07-17 新增,踩坑记录)
以下 3 条均为生产事故复盘得出的硬性规则,部署/变更时必须遵守:
#### 铁律 1backend 必须 `--workers 1`WS 推送单 worker 约束)
`ws_manager` 是进程内单例,AI 后台任务与员工 WebSocket 连接若落在不同 worker 进程,`broadcast_to_employees` 会**静默丢失约 50% 消息**。
```bash
# ❌ 危险:docker-compose-override.yml 会自动合并覆盖主文件!
# 若 override 中存在 --workers 2,主文件的 --workers 1 会被覆盖
docker compose -f docker-compose.yml -f docker-compose-override.yml config | grep workers
# 必须输出 1。如为 2,删除或修改 override 文件
```
#### 铁律 2:`.env` 变量不会自动传入容器,必须在 `environment:` 显式声明
`backend/.dockerignore` 排除了全部 `.env` 文件,生产容器配置 **100% 来自 docker-compose.yml 的 `environment:` 部分**。新增任何环境变量(尤其是 `DIFY_NATIVE_BASE_URL``DIFY_NATIVE_API_KEY`),必须:
```yaml
# docker-compose.yml
backend:
environment:
- DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-}
- DIFY_NATIVE_API_KEY=${DIFY_NATIVE_API_KEY:-}
```
验证:容器内执行 `env | grep DIFY_NATIVE` 非空;后端日志出现「调用 Dify 原生 API」而非「回退到代理路径」。
(事故记录:2026-07-13 两次因未声明导致 P0 故障;`WECOM_SSO_CALLBACK_BASE` 未声明导致扫码登录崩溃)
#### 铁律 3Redis 密码含特殊字符必须 URL 编码
`REDIS_URL` 中密码含 `@ # !` 时未编码 → 解析出错误的 host → 连接挂起 → 登录 502。编码规则:`@→%40``#→%23``!→%21`。改密码时须同步更新 compose 中 `requirepass``REDIS_URL` 两处。
---
## 五、启动服务
### 5.1 首次启动
```bash
cd /opt/wecom-it-desk
# 构建后端镜像 + 启动所有容器
docker compose up -d --build
# 首次启动需要 2-3 分钟(下载镜像 + 构建后端 + 数据库迁移)
```
### 5.2 查看启动状态
```bash
# 查看所有容器状态(应全部 healthy/running
docker compose ps
# 查看实时日志(Ctrl+C 退出)
docker compose logs -f
# 只看后端日志
docker compose logs -f backend
```
**预期输出(`docker compose ps`):**
```
NAME STATUS PORTS
wecom_it_postgres Up (healthy) 5432/tcp
wecom_it_redis Up (healthy) 6379/tcp
wecom_it_backend Up (healthy) 8000/tcp
wecom_it_nginx Up (healthy) 0.0.0.0:80->80/tcp
```
### 5.3 验证服务
```bash
# 1. 健康检查
curl http://localhost/itdesk/health
# 预期:healthy
# 2. 后端 API
curl http://localhost/api/health
# 预期:{"status":"ok"}
# 3. 浏览器访问
# H5 员工端:http://itsupport.servyou.com.cn/itdesk/
# 坐席工作台:http://itsupport.servyou.com.cn/itagent/
# 管理后台:http://itsupport.servyou.com.cn/itadmin/
```
---
## 六、常用运维命令
```bash
cd /opt/wecom-it-desk
# 重启所有服务
docker compose restart
# 只重启后端(代码更新后)
docker compose restart backend
# 查看某个容器的日志
docker compose logs -f --tail=100 backend
# 进入后端容器调试
docker compose exec backend /bin/sh
# 停止所有服务
docker compose down
# 停止并删除数据卷(⚠️ 会清空数据库!)
docker compose down -v
# 查看磁盘使用
docker system df
```
---
## 七、代码更新流程
当有新代码需要部署时:
```bash
# 1. 上传新的部署包,覆盖旧文件
# 2. 重新构建并启动
cd /opt/wecom-it-desk
docker compose up -d --build
# 如果只有前端更新,不需要重建后端镜像:
docker compose up -d --no-deps --build nginx
```
---
## 八、故障排查
| 问题 | 排查命令 | 常见原因 |
|------|----------|----------|
| 容器反复重启 | `docker compose logs backend` | 数据库连接失败、环境变量缺失 |
| 页面空白 | `docker compose logs nginx` | 前端 dist 目录为空或路径错误 |
| API 404 | `curl http://localhost:8000/health` | 后端未启动或 nginx proxy 配置错误 |
| 数据库连接失败 | `docker compose logs postgres` | POSTGRES_PASSWORD 与 DATABASE_URL 不一致 |
| 端口被占用 | `sudo lsof -i :80` | 其他服务占用 80 端口 |
---
## 九、安全建议(后续)
- [ ] 配置 HTTPS(Nginx 反代 + 证书,或使用反向代理)
- [ ] 修改默认数据库密码
- [ ] 关闭 Mock 登录(`MOCK_LOGIN_ENABLED=false`
- [ ] 限制 80 端口访问来源(防火墙规则)
@@ -0,0 +1,447 @@
# 部署运维说明 — H5 智能推荐重构(v1.0)
> **REQ 编号**: REQ-用户-006
> **部署日期**: 2026-07-28(首次部署)
> **版本**: v1.0
> **关联文档**:
> - **PRD(冻结)**`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-006-智能推荐重构-v1.0-Frozen.md`
> - **技术方案**`docs/02-技术文档/技术架构/技术方案-REQ-用户-006-智能推荐重构-v1.0.md`
> - **任务说明书**:`docs/07-项目管理/任务说明书/任务说明书-REQ-用户-006-智能推荐重构.md`
> - **原型图**`docs/01-产品文档/05-用户端H5/原型-REQ-用户-006-智能推荐重构-v1.0.html`
> - **测试用例**`docs/03-测试文档/03-功能测试用例/TC-用户-006-智能推荐重构.md`
> - **部署总手册**`docs/04-运维文档/部署运维/DEPLOY-GUIDE.md`
---
## 一、变更摘要
| 维度 | 变更 |
|------|------|
| **后端新增** | `app/services/recommend_progress_service.py`T2 webhook + 60s 轮询) |
| **后端新增** | `app/services/topic_detector.py`Jaccard 相似度) |
| **后端新增** | `app/api/recommend.py`REST API:进度查询 + webhook 接收) |
| **后端重构** | `app/services/asset_recommend_service.py`merge_recommends + 同源抑制 + 中文匹配) |
| **后端重构** | `app/config/assets.yaml`(中文 role key + 同义词表 + 排除关键词) |
| **后端扩展** | `app/services/employee_profile_service.py`DB 缓存画像降级) |
| **后端集成** | `app/tasks/h5_ai_task.py::_step_assets`3s 超时 + 降级链路) |
| **后端集成** | `app/tasks/h5_ai_task.py::_step_persist`source/trigger_timing/layer 标识) |
| **数据库** | 新增 `recommend_progress` + `recommend_event` 双表 + 索引 |
| **前端新增** | `src/frontend-h5/src/stores/recommendStore.ts`Pinia 状态机 + localStorage |
| **前端新增** | `src/frontend-h5/src/composables/useRecommendWs.ts`WS 接收处理) |
| **前端重构** | `src/frontend-h5/src/components/assistant/DynamicRecommend.vue`(无标题 + FIFO + 4 类卡片) |
| **前端适配** | `src/frontend-h5/src/components/assistant/RightPanel.vue`(引用 store |
| **nginx** | **无改动** |
---
## 二、部署前 Checklist
### 2.1 文档就位(5 件套)
- [x] PRD v1.0-Frozen(含 §4.7 冻结声明 + §13 扩展计划)
- [x] 技术方案 v1.019 章节)
- [x] 任务说明书 v1.0(8 阶段 WBS)
- [x] 原型图 v1.07 场景)
- [x] 测试用例 v1.076 条用例)
### 2.2 代码就位
- [ ] 中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\src\` 已改
- [ ] ASCII 路径 `D:\dev\wecom\src\` 已同步改(**多路径同步铁律**
- [ ] 后端 AST 静态校验通过(`python -c "import ast; ast.parse(open(f).read())"`
- [ ] 前端 `npm run build` 通过(无 TS 报错)
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_asset_recommend_v2.py -v`
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_recommend_progress.py -v`
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_topic_detector.py -v`
- [ ] 前端单元测试通过:`vitest run recommendStore.test.ts`
### 2.3 数据库迁移准备
- [ ] Alembic 迁移脚本就位:`alembic/versions/{revision}_add_recommend_progress.py`
- [ ] Alembic 迁移脚本就位:`alembic/versions/{revision}_add_recommend_event.py`
- [ ] Init SQL 应急脚本就位(容器内 alembic 失败时备用):
- `scripts/init_recommend_tables.sql`(含 `ON CONFLICT DO NOTHING` 幂等)
- [ ] 迁移在 staging 环境 dry-run 通过
### 2.4 部署包就位
- [ ] 后端部署包:`backend-recommend-v1.0.zip`(含 17 项代码 + Init SQL
- [ ] 前端部署包:`frontend-h5-recommend-v1.0.zip`(含 dist 完整产物)
- [ ] MD5 校验通过
### 2.5 灰度策略已对齐
- [ ] 运维确认灰度名单(10 → 100 → 500 人)
- [ ] 数据监控大盘已配置(点击率、自助解决率、降级次数)
- [ ] 应急沟通群已建(产品 + 工程 + 运维)
---
## 三、部署顺序
> **铁律**:后端 → 前端 → nginx(如有改动)→ DB 迁移 → 端到端验收
### 3.1 后端部署
#### 3.1.1 压缩后端代码
```powershell
# 在中文路径下打包
Compress-Archive -Path "D:\资料\03-项目开发\wecom_it_smart_desk\src\backend\app\*" -DestinationPath "D:\资料\03-项目开发\wecom_it_smart_desk\backend-recommend-v1.0.zip" -Force
# ASCII 副本也打包(防止源路径含中文导致的 GBK 误读)
Compress-Archive -Path "D:\dev\wecom\src\backend\app\*" -DestinationPath "D:\dev\wecom\backend-recommend-v1.0.zip" -Force
```
#### 3.1.2 通过 jumpserver-V2 上传到 /tmp
```powershell
# 使用 v2_ops.pyPowerShell 工具)
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\backend-recommend-v1.0.zip" "backend-recommend-v1.0.zip"
```
#### 3.1.3 服务器解压到挂载源路径
```bash
# ⚠️ 必须用绝对路径(batch 模式 cd 不生效)
unzip -o /tmp/backend-recommend-v1.0.zip -d /tmp/backend-recommend-extract
# 备份旧代码
cp -r /opt/wecom-it-desk/app/app /tmp/app_bak_$(date +%Y%m%d_%H%M%S)
# 拷贝新代码(保留其他文件,只覆盖改动部分)
cp -rf /tmp/backend-recommend-extract/app/* /opt/wecom-it-desk/app/app/
```
#### 3.1.4 数据库迁移(Alembic 双轨)
**主路径**Alembic 升级
```bash
# 容器内执行 alembic
docker exec -it wecom_it_backend alembic upgrade head
```
**应急路径**:手动执行 Init SQL(如果 alembic 失败)
```bash
# 上传 Init SQL
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "scripts\init_recommend_tables.sql" "init_recommend_tables.sql"
# 服务器执行
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk < /tmp/init_recommend_tables.sql
```
#### 3.1.5 验证后端表创建
```bash
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\d recommend_progress"
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\d recommend_event"
```
#### 3.1.6 重启后端(必须 --workers 1
```bash
# ⚠️ ws_manager 是进程内单例,多 worker 会导致 WS 消息丢失(~50%)
docker compose restart backend
# 验证 healthy
docker ps | grep wecom_it_backend
```
#### 3.1.7 后端日志验证
```bash
# 应该看到资产推荐 + recommend_progress 启动日志
docker logs wecom_it_backend --tail 100 | grep -E "AssetRecommend|recommend_progress|Loaded.*recommend"
```
### 3.2 前端部署
#### 3.2.1 本地构建(必须在 ASCII 路径,pnpm 不卡死)
```bash
cd D:\dev\wecom\src\frontend-h5
npm run build
# 验证 dist 产物含特征字符串
grep -r "recommendStore" dist/assets/ | head -5
grep -r "layer-progress" dist/assets/ | head -5
```
#### 3.2.2 压缩 dist
```powershell
Compress-Archive -Path "D:\dev\wecom\src\frontend-h5\dist\*" -DestinationPath "D:\dev\wecom\frontend-h5-recommend-v1.0.zip" -Force
```
#### 3.2.3 上传 + 解压
```powershell
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\frontend-h5-recommend-v1.0.zip" "frontend-h5-recommend-v1.0.zip"
```
```bash
# 服务器解压
unzip -o /tmp/frontend-h5-recommend-v1.0.zip -d /tmp/frontend-h5-extract
rm -rf /opt/wecom-it-desk/frontend-h5/dist
mv /tmp/frontend-h5-extract /opt/wecom-it-desk/frontend-h5/dist
```
#### 3.2.4 重启 nginx(必须!bind mount 不会自动刷新新文件)
```bash
docker restart wecom_it_nginx
```
### 3.3 nginx 配置(无改动)
本次部署 **无 nginx 配置变更**
---
## 四、端到端验证
### 4.1 API 验证
```bash
# 1. 后端健康检查
curl -I http://localhost:8000/health
# 2. 推荐进度 API(如果有进行中的审批)
curl -H 'X-Forwarded-For: 10.240.1.100' http://localhost:8000/recommend/progress/test_approval_id
# 3. WS 端口可达
curl -I http://localhost:8000/ws/employee/{employee_id}
```
### 4.2 数据库验证
```bash
# 1. 表存在
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\dt recommend_*"
# 2. 索引存在
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\di idx_recommend_*"
# 3. recommend_progress 表能 INSERT
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "INSERT INTO recommend_progress (recommend_id, employee_id, approval_id, approval_type, status) VALUES ('rec_test', 'test_emp', 'test_appr', 'vpn_access', 'pending') RETURNING id;"
```
### 4.3 前端验证(agent-browser 自动 + 用户人工)
```bash
# agent-browser 自动验证
# 1. 打开 H5 员工端
# 2. 进会话但不发言 → 验证右侧栏完全空白(决策 ① A)
# 3. 发"VPN 申请" → 验证左侧气泡 + 右侧栏操作卡
# 4. 切换话题 → 验证 L1 清空
# 5. 关闭浏览器 → 重新打开 → 验证 L2/L3/progress 持久化
```
### 4.4 用户人工验证(必做,企微扫码限制)
- [ ] 登录 H5 员工端(需企微扫码)
- [ ] 进入新会话,不发言,观察右侧栏空白
- [ ] 发"VPN 怎么连",观察左侧气泡 + 右侧栏操作卡
- [ ] 切换话题到"会议室",观察 L1 清空
- [ ] 触发审批,观察进度卡回流
- [ ] 关闭浏览器重新打开,观察持久化卡
---
## 五、灰度策略
### 5.1 4 阶段灰度
| 阶段 | 规模 | 持续时间 | 通过条件 | 不达标处理 |
|------|------|---------|---------|----------|
| **1%** | 10 人(运维 + 产品 + 工程) | 1 天 | 无 P0/P1 错误 | 立即回滚 |
| **10%** | 100 人(早期种子用户) | 2 天 | 点击率 > 5%(无 P0/P1 错误) | 暂停灰度排查 |
| **50%** | 500 人(半个部门) | 3 天 | 点击率 > 10% | 暂停灰度排查 |
| **100%** | 全量(约 7000 人) | 长期 | 点击率 > 15%,自助解决率 > 10% | 长期监控优化 |
### 5.2 数据埋点验证(关键指标)
通过 `recommend_event` 表统计:
```sql
-- 各 layer 推荐曝光数
SELECT layer, source, COUNT(*) AS exposure_count
FROM recommend_event
WHERE event_type = 'shown'
AND created_at > NOW() - INTERVAL '24 hours'
GROUP BY layer, source;
-- 各 layer 推荐点击数
SELECT layer, source, COUNT(*) AS click_count
FROM recommend_event
WHERE event_type = 'clicked'
AND created_at > NOW() - INTERVAL '24 hours'
GROUP BY layer, source;
-- 点击率
SELECT
shown.layer,
shown.source,
shown.exposure_count,
COALESCE(clicked.click_count, 0) AS click_count,
ROUND(100.0 * COALESCE(clicked.click_count, 0) / shown.exposure_count, 2) AS click_rate
FROM (
SELECT layer, source, COUNT(*) AS exposure_count
FROM recommend_event
WHERE event_type = 'shown'
GROUP BY layer, source
) shown
LEFT JOIN (
SELECT layer, source, COUNT(*) AS click_count
FROM recommend_event
WHERE event_type = 'clicked'
GROUP BY layer, source
) clicked ON shown.layer = clicked.layer AND shown.source = clicked.source;
```
### 5.3 降级监控
```sql
-- 降级触发次数(C → D / 全部失败)
SELECT
DATE(created_at) AS date,
extra->>'degradation_path' AS degradation,
COUNT(*) AS count
FROM recommend_event
WHERE event_type = 'degraded'
AND created_at > NOW() - INTERVAL '7 days'
GROUP BY date, degradation
ORDER BY date DESC;
```
---
## 六、回滚预案
### 6.1 触发条件(任一)
- P0/P1 错误率 > 5%
- 推荐卡片渲染失败 > 2%
- WS 推送延迟 > 5s
- 后端 healthy 状态丢失
- 推荐卡数量异常(同一用户 > 5 张)
### 6.2 回滚步骤(< 30 分钟)
#### Step 1: 前端回滚(5 分钟)
```bash
# 备份当前 dist
mv /opt/wecom-it-desk/frontend-h5/dist /opt/wecom-it-desk/frontend-h5/dist_bak_$(date +%Y%m%d_%H%M%S)
# 恢复备份的 dist
mv /opt/wecom-it-desk/frontend-h5/dist_rollback /opt/wecom-it-desk/frontend-h5/dist
# 重启 nginx
docker restart wecom_it_nginx
# 验证
curl -I http://localhost/itdesk/
```
#### Step 2: 后端回滚(10 分钟)
```bash
# 停止后端
docker compose stop backend
# 恢复代码
rm -rf /opt/wecom-it-desk/app/app
mv /opt/wecom-it-desk/app_bak_$(ls -t /opt/wecom-it-desk/app_bak_* | head -1 | sed 's/.*app_bak_/app_bak_/') /opt/wecom-it-desk/app/app
# 重启后端
docker compose up -d backend
# 验证
curl -I http://localhost:8000/health
```
#### Step 3: 数据库回滚(5 分钟,极端情况)
```bash
# Alembic 降级
docker exec -it wecom_it_backend alembic downgrade -1
# 或手动 DROP 新增双表
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "DROP TABLE IF EXISTS recommend_progress CASCADE; DROP TABLE IF EXISTS recommend_event CASCADE;"
```
#### Step 4: 通知(5 分钟)
- 群发"已回滚至 v2.3 现状"通知到产品 + 工程 + 客服群
- 收集失败原因,写入 BUG 单
- 排查修复后重新走灰度流程
### 6.3 回滚时间预算
| 阶段 | 时间 |
|------|------|
| Step 1 前端 | 5 分钟 |
| Step 2 后端 | 10 分钟 |
| Step 3 DB | 5 分钟 |
| Step 4 通知 | 5 分钟 |
| **合计** | **< 30 分钟** |
---
## 七、风险与边界
### 7.1 部署期风险
| 风险 | 缓解措施 |
|------|---------|
| Alembic 迁移失败 | Init SQL 应急脚本 |
| models/__init__.py 整体覆盖丢失其他模型 | 字符串 replace 比 sed/awk 稳(详见任务说明书 §1.4 关键决策点) |
| 前端 dist build 触发 safe-delete hook | Rename-Item + .NET Delete 绕路 |
| 后端 --workers 不为 1 | docker-compose.yml 校验,强制 1 worker |
| 中文字符乱码 | 容器内 locale 检查 + UTF-8 编码 |
| 蓝绿共用 PG/Redis 干扰 | 仅升级不降级,DB migration 谨慎执行 |
### 7.2 运行期风险
| 风险 | 等级 | 缓解措施 |
|------|------|---------|
| Dify 升级 action 结构变化 | 中 | 兼容旧字段解析 |
| 画像 API 长时间不可用 | 中 | DB 缓存画像降级(来源 C 改造) |
| localStorage 配额超限(5MB | 低 | 仅持久 L2/L3/progressLRU 淘汰 |
| 企微 webhook 推送失败 | 中 | 60s 轮询兜底 |
| 灰度期间指标不达标 | 中 | 每阶段不达标暂停 |
---
## 八、常见问题(FAQ
### Q1: Alembic 容器内不可用怎么办?
**A**: 使用 Init SQL 应急脚本(详见 §3.1.4)。
### Q2: ws_manager 多 worker 导致 WS 消息丢失怎么办?
**A**: 后端必须 `--workers 1`docker-compose.yml 已强制。
### Q3: 中文路径下 pnpm install 卡死怎么办?
**A**: 在 ASCII 路径 `D:\dev\wecom\` 下执行 `npm run build`(详见多路径同步铁律)。
### Q4: 前端 build 触发 safe-delete hook 怎么办?
**A**: 使用 `Rename-Item dist __dist_movetmp`(同目录)→ vite 跳过 fs.rmSync → build 完 .NET Delete(详见 memory 跨项目铁律)。
### Q5: 模型注册丢失怎么办?
**A**: 不能整体覆盖 `models/__init__.py`,需用 Python 脚本字符串 replace(详见任务说明书 §1.4)。
### Q6: 容器内 alembic.ini 路径不对怎么办?
**A**: 使用绝对路径 `docker exec -it wecom_it_backend alembic -c /app/alembic.ini upgrade head`
### Q7: psftp 上传大于 100KB 文件慢怎么办?
**A**: 已优化,使用 `v2_ops.py upload` 子命令,自动 md5 校验 + 断点续传。
### Q8: 灰度名单怎么选?
**A**: 1% 选运维 + 产品 + 工程核心成员;10% 选早期种子用户(高频用户);50% 选半个部门(覆盖不同角色);100% 全量。
---
## 九、变更日志
| 版本 | 日期 | 变更内容 | 作者 |
|------|------|---------|------|
| v1.0 | 2026-07-28 19:36 | 初版:基于 PRD v1.0-Frozen + 技术方案 v1.0 + 任务说明书 v1.0,给出完整部署 SOP(5 件套齐全):后端 7 步 + 前端 4 步 + DB 迁移双轨 + 端到端验证 + 4 阶段灰度 + 30 分钟回滚 + 8 FAQ | Duckula + 宋献 |
@@ -0,0 +1,159 @@
# 部署运维补充说明 — 分配模式 Tab 收编(v1.2)
> **关联变更**: 管理后台 v1.2 分配模式 Tab 收编(REQ-集成-002-v1.2
> **部署日期**: 2026-07-28
> **版本**: v1.0
> **关联文档**:
> - PRD: `docs/01-产品文档/08-集成生态/PRD-REQ-集成-002-管理后台-v1.0.md`v1.2
> - 技术方案: `docs/02-技术文档/技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md`
> - 任务说明书: `docs/07-项目管理/任务说明书/任务说明书-REQ-集成-002-分配模式Tab收编.md`
---
## 一、变更摘要
| 维度 | 变更 |
|------|------|
| 前端 | `frontend-admin/src/views/Agents.vue``<el-tabs>` + 第 2 Tab「分配策略」 |
| 前端 | `frontend-admin/src/views/AssignmentMode.vue` 文件保留 + 加停用注释 |
| 前端 | `frontend-admin/src/router/index.ts` 删除 `/admin/assignment-mode` 路由项 |
| 前端 | `frontend-admin/src/views/Layout/menu.ts` 删除「分配模式」菜单项 + `Sort` icon import |
| 后端 | **无改动** |
| 数据库 | **无改动**`system_configs.assignment_mode` 仍存) |
---
## 二、部署前 checklist
- [ ] PRD v1.2 已更新
- [ ] 原型图 v1.2 已同步
- [ ] 技术方案 v1.0 已建
- [ ] 测试用例 TC-集成-002 已建(31 条用例)
- [ ] 任务说明书已建
- [ ] 本地中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\src\frontend-admin\` 已改
- [ ] ASCII 路径 `D:\dev\wecom\src\frontend-admin\` 已同步改(**多路径同步铁律**
- [ ] `npm run build` 通过(无 TS 报错)
- [ ] dist `assets/Agents-*.js``agents-assignment` / `agents-list` / `手动接单` 等字符串
---
## 三、部署流程
### 3.1 压缩 dist
```powershell
Compress-Archive -Path "D:\dev\wecom\src\frontend-admin\dist\*" -DestinationPath "D:\dev\wecom\dist-admin-v1.2-tab.zip" -Force
```
### 3.2 上传到服务器 /tmp
```powershell
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\dist-admin-v1.2-tab.zip" "dist-admin-v1.2-tab.zip"
```
### 3.3 服务器解压到 nginx 路径
```bash
cd /tmp
unzip -o dist-admin-v1.2-tab.zip -d /tmp/admin-v1.2-extract
rm -rf /opt/wecom-it-desk/frontend-admin/dist
mv /tmp/admin-v1.2-extract /opt/wecom-it-desk/frontend-admin/dist
```
### 3.4 重启 nginx(必做!bind mount 不会自动刷新)
```bash
docker restart wecom_it_nginx
```
### 3.5 验证
```bash
# 1. 主入口 200
curl -I http://localhost/itadmin/
# 2. 分配模式 API 仍 200
curl -H 'X-Forwarded-For: 10.240.1.100' http://localhost:8000/admin/assignment-mode
```
### 3.6 浏览器手验(需企业微信扫码登录)
1. 访问 `https://itsupport.servyou.com.cn/itadmin/login` → 扫码登录
2. 进"运营中心 → 坐席管理"
3. 应看到两个 Tab:「坐席列表」「分配策略」
4. 点「分配策略」 → 6 张模式卡片(手动接单高亮 + 其余 5 个锁定)
5. 引导条:「当前坐席 X 人足以承担,手动接单完全满足...」
---
## 四、回滚预案(30 分钟可逆撤销)
> 阶段二/三若分配模式膨胀(权重配置、技能匹配规则),可拆回独立页。
### 步骤 1:恢复路由
`src/frontend-admin/src/router/index.ts`
```diff
+ {
+ path: 'assignment-mode',
+ name: 'AssignmentMode',
+ component: () => import('@/views/AssignmentMode.vue'),
+ meta: { title: '消息分配模式', priority: 'P1' },
+ },
```
### 步骤 2:恢复菜单项
`src/frontend-admin/src/views/Layout/menu.ts`
```diff
+ import { ..., Sort } from '@element-plus/icons-vue'
+
+ {
+ path: '/admin/assignment-mode',
+ meta: { title: '分配模式', icon: Sort, priority: 'P1' },
+ // ...其他元数据
+ },
```
### 步骤 3:把 Tab 2 内容搬回 `AssignmentMode.vue`
1. 删除 `AssignmentMode.vue` 顶部 "v1.2 起停用" 注释
2.`Agents.vue` 第 2 个 `<el-tab-pane>` 内复制:
- `modes` 数组(含 6 模式配置)
- `currentMode` ref
- `selectMode` 函数
- `getModeDescription` 函数
- `getLockReason` 函数
- 模板(`<div class="mode-list">` + 6 张 `.mode-card`
3. 复制 `Agents.vue` 中引导条组件(或独立组件化)
### 步骤 4:恢复 `Agents.vue` 单一结构
`src/frontend-admin/src/views/Agents.vue`
```diff
- <el-tabs v-model="activeTab">
- <el-tab-pane label="坐席列表" name="agents-list"> ... </el-tab-pane>
- <el-tab-pane label="分配策略" name="agents-assignment"> ... </el-tab-pane>
- </el-tabs>
+ <!-- 移除 el-tabs 包装,原 Tab 1 内容提升到顶层 -->
```
### 步骤 5build + 部署
重复 § 三 流程。
### 步骤 6:归档本次 v1.2 文档
- 任务说明书状态改为 `[已废弃]`
- TC-集成-002 标记为已废弃(保留作为历史记录)
- PRD v1.2 头部加 "v1.2 已废弃,已回滚到 v1.3 独立页" 注释
**总耗时估算**30 分钟(步骤 1-4 约 20 min,步骤 5-6 约 10 min)。
---
## 五、变更日志
| 版本 | 日期 | 变更 | 变更人 |
|------|------|------|--------|
| v1.0 | 2026-07-28 | 初版(v1.2 部署补充说明) | 宋献 |
@@ -0,0 +1,595 @@
# v0.7.0 Hotfix #63 回滚方案
> **场景**: 生产 backend 容器 `wecom_it_backend` 已回滚到 `v0.7.0-backup-pre-qrfix` 镜像(因 `v0.7.0.1-hotfix1` 失败)。现在通过 jumpserver 终端用 base64 分段 echo 上传 `auth_qrcode.py` + `qrcode_service.py` 到 `/tmp/`,然后 `docker cp` 到容器,`pip install qrcode[pil]`,`restart`。本文件给出**失败时的回滚方案**。
>
> **目标读者**: 运维小白(用户)。每步带中文注释,失败兜底齐全。
>
> **生效条件**: 当且仅当 `curl /api/auth_qrcode/create` 行为异常时触发。
>
> **回滚总目标**: 1 分钟内把 backend 拉回到 `v0.7.0-backup-pre-qrfix` 镜像,业务不中断。
---
## 0. 当前状态快照(回滚前必看)
回滚前先确认现在到底在跑哪个镜像、哪 2 个文件、pip 装了什么。**3 条命令 30 秒**:
```bash
# 1) 看当前容器用的镜像 ID
docker inspect wecom_it_backend --format '{{.Image}}' | head -c 12
# 期望: 现在(回滚后)应该是 v0.7.0-backup-pre-qrfix 镜像 ID
# 如果 hotfix 装好,可能是 wecom-it-desk-backend:patched 或 latest
# 2) 看容器内 2 个文件的修改时间(确认 hotfix 是否真生效)
docker exec wecom_it_backend stat -c '%Y %n' \
/app/app/api/auth_qrcode.py \
/app/app/services/qrcode_service.py
# 期望 hotfix 装好后: 数字是最近的(今天/刚刚);否则是 6/15 左右的旧时间
# 3) 看 qrcode 是否真装上
docker exec wecom_it_backend pip show qrcode 2>&1 | head -5
# 期望装好: Name: qrcode Version: 7.4.2
# 没装: WARNING: Package(s) not found: qrcode
```
把这 3 个输出截图给 Claude,后续诊断直接定位问题。
---
## 1. 失败可能性清单(7 种 + 回滚命令)
| # | 失败模式 | 现象 | 检测命令 | 回滚命令 |
|---|---------|------|---------|---------|
| F1 | `qrcode` pip 安装失败 | `restart` 后容器立刻 exit | `docker ps -a \| grep wecom_it_backend` 看到 `Restarting``Exited` | 见 §1.1 |
| F2 | 容器启动失败(模块导入报错) | backend 启动循环重启 | `docker logs wecom_it_backend --tail 30` 看到 `ModuleNotFoundError` / `ImportError` / `SyntaxError` | 见 §1.2 |
| F3 | `curl` `/api/auth_qrcode/create` 返回 500 | 容器 healthy 但端点挂 | `curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create` | 见 §1.3 |
| F4 | `curl` 返回 200 但**没** `qrcode_png_base64` 字段 | 代码覆盖不彻底(还是旧文件) | `curl ... \| python -m json.tool \| grep qrcode_png_base64` | 见 §1.4 |
| F5 | `curl` 返回 502/504 | nginx 找不到 backend 容器 | `docker ps \| grep backend` | 见 §1.5 |
| F6 | 端口冲突(8000 被占) | 容器一直 restarting | `docker logs wecom_it_backend --tail 50 \| grep -i "address already"` | 见 §1.6 |
| F7 | 镜像 ID 错乱/标签漂移 | `restart` 后跑的镜像不是预期的 | `docker images \| grep wecom-it-desk-backend` | 见 §1.7 |
### 1.1 F1: qrcode pip 安装失败回滚
**原因**: `pip install qrcode[pil]` 网络抽风 / 镜像精简版没 gcc / 版本冲突。
**回滚命令**(jumpserver 终端执行,root 用户):
```bash
# 1) 停容器
docker stop wecom_it_backend
# 2) 删容器(保留数据卷 / 网络)
docker rm wecom_it_backend
# 3) 用回滚镜像起新容器(关键: 命令行要跟当前生产容器完全一致)
# 抄一下当前容器的完整 run 命令,免得环境变量 / 挂载丢了
docker run -d \
--name wecom_it_backend \
--restart=always \
--network wecom_it_network \
-e DATABASE_URL='...' \
-e REDIS_URL='...' \
-e WECOM_CORP_ID='...' \
-v /opt/wecom-it-desk/backend:/app:rw \
wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
# 4) 验证
docker ps | grep wecom_it_backend
# 期望: STATUS = Up X seconds (healthy)
```
> **简化方案**(如果你之前记录了完整 run 命令):
>
> ```bash
> # 直接用 docker commit 出来的镜像
> docker run -d --name wecom_it_backend <完整原参数> \
> wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
> ```
### 1.2 F2: 模块导入报错回滚
**原因**: `auth_qrcode.py``qrcode_service.py` 上传时 base64 解码坏掉 / Python 缩进错。
**检测**:
```bash
docker logs wecom_it_backend --tail 30 2>&1 | grep -E "(ModuleNotFoundError|ImportError|SyntaxError|IndentationError)"
```
**回滚命令**(比 F1 简单,只用覆盖文件 + 重启,不用换镜像):
```bash
# 1) 从 backup 镜像里把原版文件拷出来
docker create --name tmp_rollback wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
docker cp tmp_rollback:/app/app/api/auth_qrcode.py /tmp/auth_qrcode.py.bak
docker cp tmp_rollback:/app/app/services/qrcode_service.py /tmp/qrcode_service.py.bak
docker rm tmp_rollback
# 2) 覆盖回滚(注意: bind mount 模式下必须改宿主机路径)
docker cp /tmp/auth_qrcode.py.bak wecom_it_backend:/app/app/api/auth_qrcode.py
docker cp /tmp/qrcode_service.py.bak wecom_it_backend:/app/app/services/qrcode_service.py
# 3) 重启
docker restart wecom_it_backend
# 4) 验证
sleep 5
docker ps | grep wecom_it_backend
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool
```
### 1.3 F3: create 端点 500 回滚
**原因**: `qrcode_service.py` 内的 `_render_qrcode_png` 抛异常(`qrcode` 没装好 / PIL 缺包)。
**检测**:
```bash
# 拿返回内容
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create -v 2>&1 | tail -20
# 看后端日志,找 traceback
docker logs wecom_it_backend --tail 50 2>&1 | grep -A 20 "Traceback"
```
**回滚命令**: 同 §1.2(覆盖文件 + restart)。如果还 500,升级到 §1.1(换镜像)。
### 1.4 F4: 没 qrcode_png_base64 字段回滚
**原因**: `docker cp` 后容器内文件**没真覆盖**(典型 bind mount / overlay fs 坑)。
**检测**:
```bash
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool
# 看 data 字段里有没有 "qrcode_png_base64"
# 没有 → 文件没真覆盖
```
**回滚命令**(强制覆盖):
```bash
# 1) 确认宿主机上 bind mount 的文件位置
docker inspect wecom_it_backend --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' | grep app
# 输出: /opt/wecom-it-desk/backend -> /app
# 2) 直接改宿主机路径(这是 bind mount 唯一能稳定生效的方式)
ls -la /opt/wecom-it-desk/backend/app/api/auth_qrcode.py /opt/wecom-it-desk/backend/app/services/qrcode_service.py
# 3) 如果是新文件没生效,先 rm 再 cp
rm -f /opt/wecom-it-desk/backend/app/api/auth_qrcode.py
rm -f /opt/wecom-it-desk/backend/app/services/qrcode_service.py
cp /tmp/auth_qrcode.py /opt/wecom-it-desk/backend/app/api/
cp /tmp/qrcode_service.py /opt/wecom-it-desk/backend/app/services/
# 4) 必须 restart 容器(overlay 不会自动 sync bind mount)
docker restart wecom_it_backend
# 5) 验证
sleep 5
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool | grep qrcode_png_base64
```
### 1.5 F5: 502/504 回滚
**原因**: nginx 解析到旧 backend 容器,或容器网络断了。
**检测**:
```bash
# 1) 看 backend 容器在不在
docker ps | grep wecom_it_backend
# 2) nginx 容器内直接测 backend
docker exec wecom_it_nginx wget -qO- --timeout=3 http://wecom_it_backend:8000/api/ready
# 期望: {"status":"ready",...}
# 502 → 网络通但 backend 内部挂
# timeout → 网络都不通
```
**回滚命令**(全链路重拉):
```bash
# 1) 停 backend
docker stop wecom_it_backend
# 2) 删容器
docker rm wecom_it_backend
# 3) 用回滚镜像起(完整参数)
docker run -d --name wecom_it_backend \
--restart=always --network wecom_it_network \
<完整原参数> \
wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
# 4) 重新加载 nginx(让 upstream 刷新)
docker exec wecom_it_nginx nginx -s reload
# 5) 验证
sleep 10
curl -k https://itsupport.servyou.com.cn/api/ready
```
### 1.6 F6: 端口冲突回滚
**原因**: 旧容器没删干净 / 8000 被别的进程占。
**检测**:
```bash
docker logs wecom_it_backend --tail 50 2>&1 | grep -i "address already in use"
# 或
ss -tlnp | grep 8000
```
**回滚命令**:
```bash
# 1) 看谁占 8000
ss -tlnp | grep ':8000'
# 2) 通常是僵尸容器,删它
docker ps -a | grep ":8000" # 不一定能直接看到
docker rm -f wecom_it_backend # 强制删当前容器
# 3) 再起
docker run -d --name wecom_it_backend <完整原参数> wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
```
### 1.7 F7: 镜像 ID 错乱回滚
**原因**: `docker run` 时没指定 tag,默认拉 `latest`,可能不是预期的。
**检测**:
```bash
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedSince}}' | grep wecom-it-desk-backend
# 应该看到 3 个:
# wecom-it-desk-backend:v0.7.0-backup-pre-qrfix (回滚用的)
# wecom-it-desk-backend:latest (可能等于上面那个,也可能等于 patched)
# wecom-it-desk-backend:patched (hotfix 试装版,如果有)
```
**回滚命令**(显式指定 tag):
```bash
# 拿到回滚镜像的精确 ID
ROLLBACK_IMAGE=$(docker images -q wecom-it-desk-backend:v0.7.0-backup-pre-qrfix)
echo "回滚镜像 ID: $ROLLBACK_IMAGE"
# 删旧容器
docker stop wecom_it_backend && docker rm wecom_it_backend
# 用**精确 ID** 起(避免 tag 被覆盖)
docker run -d --name wecom_it_backend <完整原参数> $ROLLBACK_IMAGE
```
---
## 2. 健康检查命令速查
### 2.1 容器层
```bash
# 状态(看是不是 healthy)
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep wecom_it_backend
# 期望: wecom_it_backend Up X minutes (healthy) wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
# 看 healthcheck 详细日志
docker inspect wecom_it_backend --format '{{json .State.Health}}' | python -m json.tool
```
### 2.2 进程层
```bash
# Python 进程在不在
docker exec wecom_it_backend ps aux | grep -E "uvicorn|gunicorn" | grep -v grep
# 期望: 1 行 uvicorn 进程
# 端口监听
docker exec wecom_it_backend ss -tlnp | grep 8000
# 期望: LISTEN 0 128 0.0.0.0:8000 ...
```
### 2.3 端点层
```bash
# readiness 端点(由 /api/ready 提供)
curl -k https://itsupport.servyou.com.cn/api/ready
# 期望: {"code":200,"data":{"status":"ready","checks":{...}}}
# health 端点
curl -k https://itsupport.servyou.com.cn/api/health
# 期望: {"status":"ok"}
```
### 2.4 业务层(create 端点)
```bash
# 标准 create 调用
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create \
-H 'Content-Type: application/json' | python -m json.tool
```
期望返回(200 + data 里**有** `qrcode_png_base64`):
```json
{
"code": 200,
"message": "ok",
"data": {
"ticket": "AbCdEf123456...",
"qrcode_url": "https://open.weixin.qq.com/connect/oauth2/authorize?...",
"qrcode_png_base64": "iVBORw0KGgoAAAANSUhEUgAA...(超长 base64 字符串)...",
"expires_in": 120,
"expires_at": "2026-06-22T10:30:45.123456"
}
}
```
**关键判定**:
- HTTP 200 + `qrcode_png_base64` 长度 > 100 字符 = hotfix 生效 ✅
- HTTP 200 + 字段缺失 = §1.4 文件没覆盖
- HTTP 500 = §1.3
- HTTP 502/504 = §1.5
---
## 3. 验证 hotfix 真正生效(5 步)
```bash
# Step 1: 文件 md5 对比(确认是 hotfix 版)
docker exec wecom_it_backend md5sum /app/app/api/auth_qrcode.py /app/app/services/qrcode_service.py
# 跟宿主机 /tmp/ 里那 2 个文件的 md5 对比,必须一致
md5sum /tmp/auth_qrcode.py /tmp/qrcode_service.py
# Step 2: 关键代码片段存在性
docker exec wecom_it_backend grep -n "_render_qrcode_png\|qrcode_png_base64" \
/app/app/api/auth_qrcode.py /app/app/services/qrcode_service.py
# 期望: 至少 3 行匹配(import / def / return)
# Step 3: qrcode 装上了
docker exec wecom_it_backend python -c "import qrcode; print(qrcode.__version__)"
# 期望: 7.4.2
# Step 4: create 端点返回 qrcode_png_base64
RESP=$(curl -k -s -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create)
echo "$RESP" | python -c "import json,sys; d=json.load(sys.stdin); print('has_field:', 'qrcode_png_base64' in d.get('data',{})); print('len:', len(d.get('data',{}).get('qrcode_png_base64','')))"
# 期望: has_field: True len: 500~2000
# Step 5: 浏览器实测(用户手工)
# 打开 https://itsupport.servyou.com.cn/itportal/
# 应该看到二维码图片(不是空白)
```
**5 步全过 = hotfix 真生效**。任何一步失败,跳到 §1 对应章节回滚。
---
## 4. 决策树:何时回滚 vs 何时修复
```
┌──────────────────────────┐
│ hotfix 装好,开始验证 │
│ (curl /api/auth_qrcode/ │
│ create) │
└────────────┬─────────────┘
┌──────────────────────────┐
│ HTTP 200 + 有 base64 字段? │
└────┬──────────────┬──────┘
│ │
Yes No
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ ✅ hotfix 生效 │ │ 看 HTTP 状态码 │
│ 跑 §3 后 5 步 │ └────┬───────┬─────┘
│ 浏览器实测 │ │ │
└─────────────────┘ 500 502/504
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ 看 trace │ │ 看容器在不在 │
│ 见 §1.3 │ │ 见 §1.5 │
└────┬─────┘ └──────┬───────┘
│ │
┌────────┴────┐ │
▼ ▼ │
修不好(< 5 分钟) 修得好 │
│ │ │
▼ ▼ │
§1.2 覆盖文件 继续验证 │
完整走完 §3 ┌────┴────────┘
┌──────────────────┐
│ 走完 §3 五步验证 │
└────┬─────────┬───┘
│ │
全过(5/5) 有失败
│ │
▼ ▼
浏览器实测 §1.4 文件覆盖
/itportal/ (bind mount)
看到二维码
┌────┴────┐
▼ ▼
看到二维码 还是空白
│ │
▼ ▼
✅ 成功 截图给 Claude
走 §1.1 换镜像
```
**何时回滚的硬性触发条件**(任一即回滚):
1.**容器健康检查连续 3 次失败**(每 30s 一次,> 90s 不 healthy)
2.**其他业务端点挂掉**(扫一下 /api/ready / /api/health / 别的 create 端点)
3.**修复尝试超过 5 分钟无进展**
4.**用户报告前端页面打不开 / 报 500**
**何时继续修复的判断**:
- 容器 healthy + 仅 `create` 端点 500 → 尝试 §1.2 覆盖文件,5 分钟内没好就走 §1.1
- 容器 healthy + `create` 端点正常 + 没 base64 字段 → §1.4 强制覆盖(这是文件问题,不是代码问题)
- 容器 not healthy + 启动报错 → 直接 §1.1 换镜像(别浪费时间)
---
## 5. 回滚后清理步骤(2 步)
回滚成功 + 业务恢复后,把现场收拾干净。
### 5.1 恢复 image tag
```bash
# 1) 看现在有哪些镜像
docker images | grep wecom-it-desk-backend
# 期望看到:
# REPOSITORY TAG IMAGE ID CREATED
# wecom-it-desk-backend v0.7.0-backup-pre-qrfix abc123... 3 days ago
# wecom-it-desk-backend patched def456... 10 minutes ago (hotfix 试装版)
# wecom-it-desk-backend latest abc123... 3 days ago (跟 backup 同 ID)
# 2) 把 latest 重新指向回滚镜像
docker tag wecom-it-desk-backend:v0.7.0-backup-pre-qrfix wecom-it_desk-backend:latest
# 防止下次 pull latest 时拉到错版本
# 3) 给 hotfix 试装镜像打孤 tag(留底,后面排查用)
docker tag wecom-it-desk-backend:patched wecom-it-desk-backend:hotfix-63-failed
# 避免被下次构建覆盖
```
### 5.2 清理多余镜像(谨慎)
```bash
# 1) 先看磁盘占用
docker system df
# 2) 看哪些镜像没人用
docker images --filter "dangling=true" # 悬空镜像(<none>:<none>)
# 期望: 如果有 hotfix 中间层,会列出来
# 3) 删悬空镜像(安全)
docker image prune -f
# 4) 看 patched 镜像是否还有容器引用
docker ps -a --filter "ancestor=wecom-it-desk-backend:patched" --format '{{.ID}} {{.Names}} {{.Status}}'
# 期望: 0 行(回滚后应该没容器在用 patched)
# 5) 删 patched 镜像
docker rmi wecom-it-desk-backend:patched
# 6) 删 failed 留底(可选,建议先保留 7 天)
# docker rmi wecom-it-desk-backend:hotfix-63-failed
# 7) 再看一次
docker images | grep wecom-it-desk-backend
# 期望只剩 v0.7.0-backup-pre-qrfix + latest(同 ID)
```
### 5.3 清理宿主机临时文件
```bash
# 删 /tmp/ 里那 2 个 base64 上传用的文件
rm -f /tmp/auth_qrcode.py /tmp/qrcode_service.py
rm -f /tmp/auth_qrcode.py.bak /tmp/qrcode_service.py.bak # 回滚时产生的
ls -la /tmp/ | grep -E "(qrcode|auth_qrcode)"
# 期望: 无输出
```
---
## 6. 一键回滚脚本(把 §1.1 打包)
如果手动操作太烦,把回滚流程封装成一个脚本(jumpserver 上直接跑):
**文件**: `/opt/wecom-it-desk/rollback-hotfix63.sh`
```bash
#!/bin/bash
# v0.7.0 hotfix #63 一键回滚
# 用法: bash /opt/wecom-it-desk/rollback-hotfix63.sh
set -e # 任一命令失败立即退出
echo "===== hotfix #63 一键回滚 ====="
# 1) 停 + 删当前容器
docker stop wecom_it_backend
docker rm wecom_it_backend
# 2) 用 backup 镜像起
docker run -d \
--name wecom_it_backend \
--restart=always \
--network wecom_it_network \
$(cat /opt/wecom-it-desk/backend-run.env) \
wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
# 3) 等 5 秒让容器启动
sleep 5
# 4) 健康检查
echo "===== 验证 ====="
docker ps | grep wecom_it_backend
curl -kf https://itsupport.servyou.com.cn/api/ready && echo "READY OK" || echo "READY FAIL"
echo "===== 回滚完成 ====="
```
**部署方式**(在 jumpserver 终端):
```bash
# 1) 创建文件
cat > /opt/wecom-it-desk/rollback-hotfix63.sh << 'EOF'
# (上面那段内容)
EOF
# 2) 加执行权限
chmod +x /opt/wecom-it-desk/rollback-hotfix63.sh
# 3) 提取当前 backend 容器的 run 参数(给脚本里的 $(cat ...) 用)
docker inspect wecom_it_backend --format '{{range .Config.Env}}export {{.}}{{"\n"}}{{end}}' \
> /opt/wecom-it-desk/backend-run.env 2>/dev/null || true
# 4) 跑回滚
bash /opt/wecom-it-desk/rollback-hotfix63.sh
```
> **注意**: `--env-file` / `-e` 在 `docker run` 里比脚本里 export 更稳。**生产建议把完整 `docker run` 命令存到 `/opt/wecom-it-desk/backend-run.sh`,回滚脚本里直接 `bash backend-run.sh`**。这个留给后续优化。
---
## 7. 回滚后通知清单
回滚完 = 业务恢复,但**还要做 3 件事**:
1. **更新 `CURRENT-FOCUS.md`**: 在「最近搞定」加一行 `❌ v0.7.0 hotfix #63 失败已回滚到 v0.7.0-backup-pre-qrfix,前端 /itportal/ 二维码仍不显示,等下一轮修复`
2. **记入 memory**: 在 `memory/``hotfix-63-rollback-2026-06-22.md`,写清楚: 失败在哪一步 / 用了哪个回滚命令 / 跟 Claude 复盘结论
3. **贴 logs 给 Claude**: 把 `docker logs wecom_it_backend --tail 200` 输出贴回来,分析根因,准备下一轮 hotfix 方案(v0.7.0.2-hotfix2)
---
## 8. 速查表(贴在屏幕边上)
| 我看到 | 跑这个 |
|--------|--------|
| 容器 restarting | `docker logs wecom_it_backend --tail 30` 看启动错误 → §1.1 |
| 容器 healthy 但 create 500 | §1.3 拿 traceback → §1.2 覆盖文件 |
| 容器 healthy + create 200 + 无 base64 | §1.4 强制 bind mount 覆盖 |
| 502/504 | §1.5 看网络 + 容器 |
| 8000 占用 | §1.6 |
| 完全不知道啥情况 | §1.1 一键换镜像(最稳) |
| 不知道回滚到哪个镜像 | `docker images \| grep backup` |
| 不知道完整 run 命令 | `docker inspect wecom_it_backend --format '{{.Config.Cmd}} {{json .Config.Env}}' \| head -c 500` |
| 想一键回滚 | `bash /opt/wecom-it-desk/rollback-hotfix63.sh` |
| 验证 hotfix 生效 | §3 五步全过 = ✅ |
| 回滚后清理 | §5 三步 |
---
**文档结束**。所有命令都在 jumpserver 终端以 root 跑,`docker exec` 都假设容器名叫 `wecom_it_backend`(生产实际名,见 `memory/container-names-wecom-it-backend.md`)。如果容器名变了,先跑 `docker ps --format '{{.Names}}' \| grep backend` 确认。
@@ -0,0 +1,264 @@
# Nginx 域名路由分发配置(Phase 1.3 task #16)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1.3 扫码登录上线后)
## 🎯 目标
不同入口域名/子路径 → 不同前端应用,但所有请求共用同一个后端 API。
| 入口 | URL | 前端应用 | 用途 |
|---|---|---|---|
| **坐席端** | `https://itsupport.servyou.com.cn/itagent/` | `frontend-agent/dist` | 坐席工作台 |
| **管理端** | `https://itsupport.servyou.com.cn/itadmin/` | `frontend-admin/dist` | 管理后台 |
| **Portal 统一入口** | `https://itsupport.servyou.com.cn/itportal/` | `frontend-portal/dist` | 扫码登录 + 多角色选择 |
| **H5 员工端** | `https://itsupport.servyou.com.cn/itdesk/` | `frontend-h5/dist` | 员工端(企微内) |
> **两种方案**:单域名多路径(本项目当前)+ 多子域名(可选升级)
---
## 🅰️ 方案 A:单域名 + 多子路径(推荐,运维简单)
### nginx server block
```nginx
server {
listen 443 ssl;
server_name itsupport.servyou.com.cn;
# SSL 证书(由公司统一管理)
ssl_certificate /etc/nginx/certs/itsupport.servyou.com.cn.crt;
ssl_certificate_key /etc/nginx/certs/itsupport.servyou.com.cn.key;
# 通用安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ========================================================================
# 1. Portal 统一入口(扫码登录)
# ========================================================================
location /itportal/ {
alias /opt/wecom-it-desk/frontend-portal/dist/;
try_files $uri $uri/ /itportal/index.html;
# 允许企业微信 OAuth 回调(测试期)
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# ========================================================================
# 2. 坐席工作台
# ========================================================================
location /itagent/ {
alias /opt/wecom-it-desk/frontend-agent/dist/;
try_files $uri $uri/ /itagent/index.html;
}
# ========================================================================
# 3. 管理后台
# ========================================================================
# IP 白名单(2026-07-06 更新 — 添加办公网IP)
location /itadmin/ {
# 允许的IP列表(按需求添加)
allow 10.90.0.0/16; # 内网段 - 税友内网
allow 10.240.0.0/16; # 内网段 - 办公网
allow 117.147.35.138; # 办公网出口IP
allow 218.75.34.87; # 办公网出口IP
allow 127.0.0.1; # 本地
deny all; # 其他拒绝
alias /opt/wecom-it-desk/frontend-admin/dist/;
try_files $uri $uri/ /itadmin/index.html;
}
# ========================================================================
# 4. H5 员工端
# ========================================================================
location /itdesk/ {
alias /opt/wecom-it-desk/frontend-h5/dist/;
try_files $uri $uri/ /itdesk/index.html;
# 允许嵌入到企微 WebView
add_header X-Frame-Options "ALLOW-FROM https://work.weixin.qq.com" always;
}
# ========================================================================
# 5. 后端 API(4 个端共用)
# ========================================================================
location /api/ {
# 管理端 API 严格白名单(与/itadmin/一致)
location /api/admin/ {
# 允许的IP列表(按需求添加)
allow 10.90.0.0/16; # 内网段 - 税友内网
allow 10.240.0.0/16; # 内网段 - 办公网
allow 117.147.35.138; # 办公网出口IP
allow 218.75.34.87; # 办公网出口IP
allow 127.0.0.1; # 本地
deny all; # 其他拒绝
proxy_pass http://wecom_it_backend;
}
# 其他 API 放行
proxy_pass http://wecom_it_backend;
}
# ========================================================================
# 6. WebSocket(坐席端 WS-01 鉴权)
# ========================================================================
location /ws/ {
proxy_pass http://wecom_it_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WS 心跳
proxy_read_timeout 600s;
}
# ========================================================================
# 7. 静态资源(图片/上传文件)
# ========================================================================
location /api/media/ {
proxy_pass http://wecom_it_backend;
proxy_set_header Host $host;
# 上传文件 30 天缓存
expires 30d;
add_header Cache-Control "public, immutable";
}
# ========================================================================
# 8. 根路径 → Portal 统一入口
# ========================================================================
location = / {
return 302 /itportal/;
}
}
# upstream 后端(内网容器)
upstream wecom_it_backend {
server 127.0.0.1:8000; # 容器映射到宿主机的端口
}
```
### 部署步骤
```bash
# 1. 上传 dist 文件(各前端 build 产物)
scp -r frontend-portal/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-portal/
scp -r frontend-agent/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-agent/
scp -r frontend-admin/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-admin/
scp -r frontend-h5/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-h5/
# 2. 上传 nginx 配置(本地 + 堡垒机 PuTTY)
# 参考:feedback-putty-not-openssh.md(用 PuTTY 操作)
# 3. 验证配置
sudo nginx -t
# 4. reload
sudo nginx -s reload
# 5. 验证(本地或企微)
curl -I https://itsupport.servyou.com.cn/itportal/
```
---
## 🅱️ 方案 B:多子域名(可选升级,需要 DNS 解析)
| 子域名 | 解析到 | 用途 |
|---|---|---|
| `portal.itsupport.servyou.com.cn` | nginx:443 | 统一入口 |
| `agent.itsupport.servyou.com.cn` | nginx:443 | 坐席工作台 |
| `admin.itsupport.servyou.com.cn` | nginx:443 | 管理后台(内网白名单) |
| `h5.itsupport.servyou.com.cn` | nginx:443 | H5 员工端 |
### 优点
- 跨域 cookie 隔离更清晰
- 每个子域可独立上 HTTPS 证书
- 内网白名单更容易配置(直接 deny all 到 admin.*)
### 缺点
- 需要运维额外加 4 个 A 记录
- 前端跨域 API 调用要 CORS 配全
- 坐席/管理员跨域切换要 CORS preflight
**当前 v0.7.0 推荐方案 A**,v1.0 再考虑方案 B。
---
## 🔄 扫码登录流程(方案 A 下)
```
[1] 用户访问 https://itsupport.servyou.com.cn/itagent/
→ nginx 命中 location /itagent/ → 返回 frontend-agent/dist/index.html
→ 前端路由守卫检查 localStorage.agent_token,没有 → 跳 /itportal/
[2] 用户访问 https://itsupport.servyou.com.cn/itportal/
→ nginx 命中 location /itportal/ → 返回 frontend-portal/dist/index.html
→ QrcodeLogin.vue 显示二维码
[3] 员工用企微扫码
→ 企微 OAuth 回调到后端 → 后端写 Redis qrcode:scan:{ticket}
→ Portal 轮询 /api/auth_qrcode/poll/{ticket} → 拿到 status=scanned
→ UI 显示"请在手机上确认登录"
[4] 员工在手机上点"确认登录"
→ 后端 /api/auth_qrcode/confirm → 创建 token → 写 Redis qrcode:confirm:{ticket}
→ Portal 轮询拿到 status=confirmed + token + roles
[5] Portal 按角色分发(见 QrcodeLogin.vue dispatchToRole)
- 只有 agent → window.location.href = /itagent/?token=xxx
- 只有 admin → window.location.href = /itadmin/?token=xxx
- admin + agent → window.location.href = /itportal/select(让用户选)
- 默认 user → window.location.href = /itdesk/?token=xxx
[6] 目标端 Login.vue 读 ?token=xxx 写入 localStorage + 跳 /workspace
```
---
## ⚠️ 已知问题 & TODO
| 问题 | 状态 | 备注 |
|---|---|---|
| `/itadmin/` IP 白名单临时全开 | 🟡 临时 | v1.0 前必须收窄(见 `ip-whitelist-trust-proxies-todo.md`) |
| `/api/admin/` IP 白名单临时全开 | 🟡 临时 | 同上 |
| H5 端需要企微内访问 | 🟢 保持 | 用户决策,H5 仍在企微内是主场景 |
| 跨子路径刷新 404 | 🟢 已处理 | `try_files $uri $uri/ /itagent/index.html` |
| 静态资源 cache | 🟡 待优化 | 可加 version hash 强制刷新 |
| admin Login.vue 仍用表单 | 🟡 待改 | 后续 task:重写 admin Login 为扫码 UI |
---
## 🧪 验证清单
部署完成后,在以下场景测试:
- [ ] 浏览器直接访问 `/itportal/` → 显示扫码二维码
- [ ] 用企微扫码 + 确认 → Portal 自动跳到对应端
- [ ] 坐席(只有 agent 角色)扫码 → 自动跳 `/itagent/?token=xxx` → 自动登录进 /workspace
- [ ] 管理员(只有 admin 角色)扫码 → 自动跳 `/itadmin/?token=xxx` → 进 admin dashboard
- [ ] 多角色用户(admin + agent)扫码 → 跳 `/itportal/select` → 看到选择页
- [ ] H5(企微内) → 仍走企微 OAuth,扫码二维码区域正常
- [ ] 浏览器直接访问 `/itagent/workspace`(没 token)→ 跳 `/itportal/`
- [ ] 扫码登录 120s 过期 → UI 显示"已过期,点击刷新"
---
## 📚 相关文档
- [project-knowledge-base.md](../memory/project-knowledge-base.md) — 项目知识库
- [feedback-wecom-only-external-urls.md](../memory/feedback-wecom-only-external-urls.md) — 企微入口约束(部分解除)
- [phase1-progress.md](../memory/phase1-progress.md) — Phase 1+2 进度
- [deployment.md](../memory/deployment.md) — 部署经验
- [nginx-container-name-wecom-it-nginx.md](../memory/nginx-container-name-wecom-it-nginx.md) — 容器名坑
---
**变更历史**:
- 2026-06-21 创建(Phase 1.3 task #16)
@@ -0,0 +1,185 @@
# Release Notes v0.7.1
> 📅 发布日期:2026-06-23 | 类型:🔐 安全 + ✨ 功能 | 紧急度:🟡 重要
> 🔄 补充:2026-06-24 hotfix #116/#118/#119/#120 已部署(详见文末"补充 hotfix"小节)
## 🎯 本版本重点
1. **修复扫码登录 iOS NSURLErrorCannotFindHost** — 企微 App 用户扫码不再报错
2. **完整 MFA + RBAC 权限体系** — 5 角色细粒度权限 + 高危操作二次认证
3. **H5 员工端企微 OAuth 登录** — 员工不再需要输账号密码
4. **🆕 扫码登录去掉手机 confirm 步骤** — 企微 App 无确认 UI,扫码成功 = 直接登录
## 🔐 安全修复
### #109 P0:扫码登录 NSURLError 修复
**问题**:用户企微 App 访问 `https://itsupport.servyou.com.cn/itportal/`,扫描二维码后跳转报 `NSURLErrorCannotFindHost`(iOS WebView DNS 解析失败)
**根因**:`backend/app/config.py` 新字段 `qrcode_oauth_callback` 用了 pydantic 默认行为,**不剥环境变量前缀**
- 字段名:`qrcode_oauth_callback`
- pydantic-settings 期望 env:`QRCODE_OAUTH_CALLBACK`
- 实际 env:`WECOM_QRCODE_CALLBACK`(带前缀,匹配不上)
- 结果:`settings.qrcode_oauth_callback = ""`,qrcode_service 走兜底返回**相对路径** `/api/auth_qrcode/scan`
- iOS WebView 拼接相对路径失败 → DNS 解析错误
**修复**:
```python
from pydantic import Field
class Settings(BaseSettings):
qrcode_oauth_callback: str = Field(
default="",
validation_alias="WECOM_QRCODE_CALLBACK",
)
```
**部署**:docker commit + restart + 验证,详见 [[phase1-progress#109-hotfix-二轮修复]]
### RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围
- **5 角色**:super_admin / admin / agent / user / guest
- **4 资源**:user / conversation / config / audit_log
- **4 操作**:read / create / update / delete
- **3 范围**:all / department / self
### 高危路由白名单 + 中间件
5 类高危操作需 OTP 二次认证:
- `role_change`(角色变更)
- `config_change`(配置变更)
- `data_export`(数据导出)
- `account_disable`(账户禁用)
- `account_create_reset`(账户创建/重置)
## ✨ 新功能
### 后端扫码登录(端点 4 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth_qrcode/create` | POST | 生成二维码(含 PNG base64) |
| `/api/auth_qrcode/poll/{ticket}` | POST | 扫码端轮询 |
| `/api/auth_qrcode/scan` | POST | 企微回调 |
| `/api/auth_qrcode/confirm` | POST | 用户确认登录 |
### 后端 MFA + pyotp(端点 5 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth/otp-status` | GET | 查询绑定状态 |
| `/api/auth/otp-bind` | POST | 开始绑定(返回 secret + QR) |
| `/api/auth/otp-verify` | POST | 确认绑定 |
| `/api/auth/otp-verify` | POST | 验证 OTP |
| `/api/auth/otp-unbind` | POST | 禁用 MFA |
### 前端 MFA UI
- `MfaBind.vue` — 绑定弹窗
- `useHighRiskOtp.ts` composable — 高危操作 OTP 弹窗
- `Login.vue` / `TopBar.vue` — 集成 MFA 状态显示
- 路由 + store 全链路集成
### H5 员工端 OAuth 登录
- 部署路径:`https://itsupport.servyou.com.cn/itdesk/`
- 自动 OAuth 静默授权(`snsapi_base`)
- 会话列表 + 消息收发
- 已完成 E2E 验证(#104)
## 🛠️ 部署要点
### 必备环境变量
```bash
# /opt/wecom-it-desk/.env
WECOM_QRCODE_CALLBACK=https://itsupport.servyou.com.cn/api/auth_qrcode/scan
WECOM_SSO_ENABLED=true
WECOM_SSO_CALLBACK_BASE=https://itsupport.servyou.com.cn
```
### 部署清单
- [x] Backend commit `78f60c6`(v0.7.1-dev)
- [x] docker 镜像 `wecom-it-desk-backend:latest` 已 commit 新 config
- [x] 容器已重启 + /api/ready 200
- [x] H5 dist 已部署到 `/opt/wecom-it-desk/nginx/html/itdesk/`
- [x] nginx 已 reload
- [x] /api/auth_qrcode/create 返回绝对 URL
- [ ] Gitea push(等用户重授权 token)
- [ ] 企微 App 实际扫码验证
## ⚠️ 已知问题
1. **Gitea push 阻塞**:`workbuddy-claude` token 2026-06-15 被吊销,需去 Gitea Web 重授权
2. **#108 still pending**:v0.7.1-dev 78f60c6 未推到 origin
3. **/api/admin/ IP 白名单**仍是 0.0.0.0/0(临时方案),v1.0 前必须收窄
## 📚 相关文档
- [[H5-DEPLOY-RUNBOOK-v0.7.1]] — H5 部署 runbook
- [[DEPLOY-LOGIN-MIGRATION-v0.7.0]] — 旧版扫码登录
- [[E2E-CHECKLIST-v0.7.0]] — 端到端验收
- [[NGINX-DOMAIN-ROUTING]] — nginx 域名分发
- [[USER-GUIDE-QRCODE-MFA]] — 用户手册
- [[phase1-progress]] — 进展跟踪
## 📊 改动统计
- **代码**:22 文件 / +4705 行
- **测试**:78 passed + 4 xfail + 33 pre-existing failures(已分类)
- **文档**:5 新增 / 3 更新
- **Agent**:4 并行 worktree 合并
---
## 🔄 补充 hotfix(2026-06-24 部署)
### #116 P0 + #118 P0:扫码端点 405 + ticket≠state
**问题**:
- 坐席扫码登录报 `405 Method Not Allowed`
- 企微 OAuth 回调走 GET(`?code=xxx&state=<ticket>`),旧代码只支持 POST
- 即使改双方法,`Optional[str] = None``Query()` 装饰器,FastAPI 当 body 参数处理
- 函数参数叫 `ticket`,企微用 `state`,参数名不匹配
**修复**:`backend/app/api/auth_qrcode.py` scan 端点
```python
@router.api_route("/scan", methods=["GET", "POST"], response_model=None)
async def scan_qrcode(
body: Optional[QrcodeScanRequest] = None,
ticket: Optional[str] = Query(None, description="兼容旧参数名"),
state: Optional[str] = Query(None, description="企微 OAuth state 标准参数名"),
code: Optional[str] = Query(None, description="企微 OAuth 授权码"),
redis_client = Depends(dep_redis),
):
if body is not None:
final_ticket, final_code = body.ticket, body.code
else:
final_ticket = state or ticket # 优先 state(企微标准),回退 ticket
final_code = code
```
**部署**:`deploy-staging/hotfix-116-qrcode-scan-get/` — webcli v7 一键,20/20 pytest 过
### #119:webcli v7 全自动部署链路
复用 jumpserver Playwright + Luna webcli + OTP 自动,实现 18-31KB 命令脚本单次输入,2-3 分钟跑完。
### #120 P0:扫码成功 = 自动登录(去掉 confirm)
**问题**:企微 App 端没有"确认登录" UI,旧 confirm 步骤在生产永远走不通(用户报告"扫码成功后,手机端没有出现确认登录按钮")
**用户决策**:方案 A — 扫码成功 = 自动登录(推荐),2026-06-24 拍板
**修复**:`backend/app/services/qrcode_service.py` `process_scan` 默认 `auto_confirm=True`
- 扫码成功直接写 `qrcode:confirm:{ticket}`(含 token)
- 前端 poll 立即拿到 status=confirmed + token
- 跳过手机 confirm 步骤
- 默认 roles=["user"],admin/agent 走 portal 端角色选择
**测试**:20/20 pytest 过(`test_scan_then_poll_returns_confirmed` + `test_scan_auto_confirm_writes_confirm_key` 等)
**部署**:`deploy-staging/hotfix-120-qrcode-auto-confirm/` — webcli v7 一键,容器内 `/api/ready` 200 OK
**残留**:外部域名 `https://itsupport.servyou.com.cn/api/ready` 不通(nginx `/api/` upstream 配错 + 8000 端口未暴露),**等 #48 修复**。容器内 backend 完全健康。
@@ -0,0 +1,246 @@
# Token多IP异常检测 - 部署指南
> **任务ID**: 待分配
> **版本**: v1.0
> **关联文档**:
> - 技术设计-Token多IP异常检测.md
> - Token多IP异常检测测试用例.md
---
## 1. 部署概述
### 1.1 部署范围
| 组件 | 容器 | 说明 |
|------|------|------|
| 检测服务 | backend | Token异常检测定时任务 |
| Redis | redis | IP存储 |
| 告警通道 | 企微机器人 | 复用现有webhook |
### 1.2 部署方式
- **部署类型**: 增量部署(不涉及基础设施变更)
- **停机时间**: 无需停机(定时任务后台运行)
- **回滚**: 代码级别回滚
---
## 2. 部署前检查
### 2.1 环境检查
| 检查项 | 命令 | 预期结果 |
|--------|------|----------|
| Redis连接 | `docker exec backend redis-cli ping` | PONG |
| APScheduler状态 | 检查日志 | 定时任务启动成功 |
| webhook配置 | 检查环境变量 | CONTENT_AUDIT_WEBHOOK已设置 |
### 2.2 配置检查
确认以下环境变量已配置(如需自定义):
```bash
# 可选配置
TOKEN_ANOMALY_THRESHOLD=3 # 触发告警的IP数量阈值
TOKEN_ANOMALY_WINDOW=3600 # 时间窗口(秒)
TOKEN_ANOMALY_AUTO_DISABLE=false # 是否自动禁用Token
```
---
## 3. 部署步骤
### 3.1 步骤1:代码变更
**新增文件**
```
backend/app/tasks/token_anomaly_detection.py (新建)
```
**修改文件**
```
backend/app/services/token_service.py (增加record_token_ip方法)
backend/app/main.py (注册定时任务)
```
### 3.2 步骤2:配置变更
`.env` 或 docker-compose.yml 中添加(可选):
```bash
# 如需自定义阈值,在 .env 中添加
TOKEN_ANOMALY_THRESHOLD=3
TOKEN_ANOMALY_AUTO_DISABLE=false
```
### 3.3 步骤3:重启服务
```bash
# 重启backend容器(不中断其他服务)
docker compose restart backend
# 查看日志确认定时任务启动
docker logs backend --tail 50 | grep -i "token"
```
预期日志:
```
✅ Token多IP异常检测任务已启动(每60秒执行一次)
```
---
## 4. 部署后验证
### 4.1 功能验证
| 验证项 | 验证方法 | 预期结果 |
|--------|----------|----------|
| 定时任务运行 | 查看日志 | 每分钟执行一次 |
| IP记录 | 模拟API请求 | Redis中记录IP |
| 告警触发 | 构造3个IP的Token | 企微收到告警 |
### 4.2 冒烟测试
执行测试用例:
```bash
# 进入backend容器
docker exec -it backend bash
# 运行单元测试
pytest app/tests/test_token_anomaly.py -v
```
---
## 5. 监控与运维
### 5.1 日志位置
| 日志类型 | 路径 |
|----------|------|
| 应用日志 | `/app/logs/wecom-it-desk.log` |
| 定时任务日志 | 集成在应用日志中 |
### 5.2 监控指标
| 指标 | 说明 |
|------|------|
| token_anomaly_triggered_total | 触发告警次数 |
| token_anomaly_false_positive | 误报次数 |
### 5.3 运维命令
```bash
# 查看定时任务状态
docker exec backend python -c "from app.main import _scheduler; print(_scheduler.get_jobs())"
# 手动触发检测(调试)
docker exec backend python -c "
import asyncio
from app.tasks.token_anomaly_detection import detect_token_anomaly
asyncio.run(detect_token_anomaly())
"
```
---
## 6. 回滚方案
### 6.1 回滚步骤
```bash
# 1. 撤销代码变更
git checkout -- backend/app/services/token_service.py
git checkout -- backend/app/main.py
git rm backend/app/tasks/token_anomaly_detection.py
# 2. 重启服务
docker compose restart backend
```
### 6.2 数据清理
```bash
# 清理Redis中的检测数据(可选,1小时后自动过期)
docker exec backend redis-cli KEYS "token_ips:*" | xargs redis-cli DEL
```
---
## 7. 部署清单
| 序号 | 步骤 | 执行人 | 检查人 | 日期 |
|------|------|--------|--------|------|
| 1 | 代码变更 | 开发 | | |
| 2 | 配置检查 | 开发 | | |
| 3 | 重启服务 | 运维 | | |
| 4 | 功能验证 | 测试 | | |
| 5 | 冒烟测试 | 测试 | | |
| 6 | 监控确认 | 运维 | | |
---
## 8. 附录
### 8.1 相关文件
| 文件路径 | 说明 |
|----------|------|
| `backend/app/tasks/token_anomaly_detection.py` | 检测任务 |
| `backend/app/services/token_service.py` | Token服务 |
| `docs/04-运维文档/部署运维/技术设计-Token多IP异常检测.md` | 技术设计 |
### 8.2 环境变量参考
| 变量 | 必需 | 默认值 | 说明 |
|------|------|--------|------|
| CONTENT_AUDIT_WEBHOOK | 是 | - | 企微机器人webhook |
| TOKEN_ANOMALY_THRESHOLD | 否 | 3 | 告警阈值 |
| TOKEN_ANOMALY_AUTO_DISABLE | 否 | false | 自动禁用 |
---
## 9. 部署测试结果(2026-07-14
### 9.1 部署清单
| 序号 | 步骤 | 执行人 | 检查人 | 日期 | 结果 |
|------|------|--------|--------|------|------|
| 1 | 代码变更 | 开发 | | 2026-07-14 | ✅ 完成 |
| 2 | 配置检查 | 开发 | | 2026-07-14 | ✅ 完成 |
| 3 | 重启服务 | 运维 | | 2026-07-14 | ✅ 完成 |
| 4 | 功能验证 | 测试 | | 2026-07-14 | ✅ 通过 |
| 5 | 冒烟测试 | 测试 | | 2026-07-14 | ✅ 通过 |
| 6 | 监控确认 | 运维 | | 2026-07-14 | ✅ 完成 |
### 9.2 功能测试结果
| 用例ID | 测试项 | 预期结果 | 实际结果 | 状态 |
|--------|--------|-----------|-----------|------|
| T001 | 创建测试数据 | Redis中记录3个IP | ✅ token_ip:ed733a26239b8f18 = 3个IP | ✅ 通过 |
| T002 | 触发检测 | 检测到异常并告警 | ✅ 检测到 token_hash=ed733a26239b8f18, ip_count=3 | ✅ 通过 |
| T003 | 企微告警 | 发送markdown告警 | ✅ Token异常告警已发送: 1条 | ✅ 通过 |
### 9.3 验证日志
```
检测到Token异常使用: token_hash=ed733a26239b8f18, ip_count=3, ips=['192.168.1.100', '192.168.1.102', '192.168.1.101']
HTTP Request: POST https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=09120612-9d19-4f93-bd00-bfbaef548dde "HTTP/1.1 2
Token异常告警已发送: 1 条
Token异常检测完成: 检测到 1 个异常
```
### 9.4 发现问题与修复
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 告警未发送 | 容器内.env文件路径错误 | 修改config.py的env_file为"/app/app/.env",并复制.env到挂载目录 |
| webhook未配置 | .env未同步到容器 | 复制.env到/app/app/.env |
---
> **编制人**: 威胁检测工程师
> **日期**: 2026-07-14
> **审核人**: 待定
@@ -0,0 +1,165 @@
# 用户手册:扫码登录 + OTP 二次认证(Phase 1+2)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1+2 上线后)
> 读者:全体员工、坐席、管理员
---
## 📖 这是什么?
从 v0.7.0 开始,登录方式升级为**扫码登录 + OTP 二次认证**:
- ✅ 不再依赖企业微信应用入口,任意浏览器都能打开
- ✅ 多角色用户(坐席+管理员)可在 Portal 选角色自动跳转
- ✅ 管理员每次登录强制 OTP,高危操作也强制 OTP
- ✅ 备用通道:蜂鸟短信(手机丢/没装 Authenticator 时用)
---
## 🧑‍💼 员工端(H5)
### 入口
- 仍在企微内打开"IT智能服务台"应用
- 不需要扫码登录,沿用企微 OAuth
### 使用场景
- 提工单
- 看历史会话
- 查知识库
---
## 🧑‍🔧 坐席端(Agent)
### 首次登录(扫码)
```
步骤 1:浏览器访问 https://itsupport.servyou.com.cn/itportal/
步骤 2:页面显示二维码(120 秒有效)
步骤 3:用企业微信扫 → 确认登录
步骤 4:自动跳到 /itagent/workspace
```
### 日常登录
- 浏览器直接打开 `https://itsupport.servyou.com.cn/itagent/`
- 没登录 → 自动跳到 Portal 扫码
- 第二次扫码可免重复(浏览器记住 localStorage)
### 高危操作时 OTP
如果你是**坐席+管理员**(双角色),触发以下操作前会弹 OTP 输入框:
- 改权限
- 改系统配置
- 导出数据
- 封号
- 新增账号/MFA 重置
弹框出现 → 输入 Authenticator 6 位码 → 验证通过(30 分钟内免重输)
---
## 🛡️ 管理员端(Admin)
### 入口
- 浏览器直接打开 `https://itsupport.servyou.com.cn/itadmin/`
- 没登录 → 自动跳到 Portal 扫码
### 强制 OTP
管理员**每次登录都需要 OTP**:
- 扫码登录成功后,会跳到 MFA 绑定页(首次)或 OTP 验证页
- 输入 Authenticator 6 位码 → 进入管理后台
- 高危操作前还要再验一次(30 分钟内免重输)
### 首次绑定 OTP(强制)
```
步骤 1:登录后 → 自动跳 /mfa-bind
步骤 2:用 Google Authenticator / 微软 Authenticator / Authy 扫描二维码
步骤 3:输入 Authenticator 显示的 6 位码 → 点"启用 OTP"
步骤 4:绑定成功,后续登录用 OTP 验证
```
### 丢手机兜底(管理员后台重置)
如果你手机丢了/坏了,**找其他管理员重置**:
```
步骤 1:其他管理员登录 /itadmin/
步骤 2:进入"用户管理" → "MFA 管理"
步骤 3:搜索你的姓名 → 点"重置 MFA"
步骤 4:你下次登录时重新绑定 OTP
```
---
## 🆘 备用通道:蜂鸟 SMS
什么情况下用:
- 📱 手机丢了/坏了
- 🆕 刚入职,还没装 Authenticator
- 🔧 Authenticator 客户端不兼容
怎么用:
- 在 OTP 输入页点"收不到验证码?短信验证"
- 输入手机号(企微已绑定)→ 收短信码 → 验证
---
## 📱 推荐 OTP 客户端
| 客户端 | 平台 | 推荐度 |
|---|---|---|
| Google Authenticator | iOS / Android | ⭐⭐⭐⭐⭐ |
| 微软 Authenticator | iOS / Android | ⭐⭐⭐⭐ |
| Authy | iOS / Android / 桌面 | ⭐⭐⭐⭐⭐ |
| 1Password | 全平台 | ⭐⭐⭐ |
公司偏好:**Google Authenticator**(零依赖,离线可用)
---
## ❓ 常见问题
### Q1:扫码登录过期了怎么办?
A:二维码有效期 120 秒,过期后点"刷新二维码"按钮。
### Q2:扫码登录失败?
A:
- 确认用的是企业微信(不是普通微信)
- 确认企微里能看到"IT智能服务台"应用
- 刷新页面重新生成二维码
### Q3:OTP 输入错误?
A:连续 5 次错误会被锁定 5 分钟,等 5 分钟后再试。
### Q4:换手机了怎么办?
A:登录前在旧手机上导出 OTP(Google Authenticator 支持),或者找管理员后台重置。
### Q5:多角色用户(admin + agent)怎么登录?
A:扫码登录成功后,Portal 自动跳到角色选择页,选你要进入的工作台。
### Q6:H5 员工端也需要扫码吗?
A:不需要。H5 员工端仍在企微内,沿用企微 OAuth。
### Q7:扫码登录安全吗?
A:扫码登录比企微 OAuth 还安全:
- 员工必须用企微扫(企微已经做了员工身份认证)
- 二维码 120 秒过期
- Token 8 小时过期
- 高危操作还要再 OTP 一次
---
## 📞 技术支持
- 内部: 信息技术部 服务台
- 紧急: 群里 @ IT 主管
- 反馈: https://itsupport.servyou.com.cn/itdesk/feedback
---
**变更历史**:
- 2026-06-21 创建(Phase 1+2 培训)
@@ -0,0 +1,93 @@
%% IT 智能服务台 — 架构组件图(卷挂载重构前后对比)
%% 文件: class-diagram.mermaid
classDiagram
class HostServer {
+String path: /opt/wecom-it-desk/
+String ip: 10.90.5.110
+String domain: itsupport.servyou.com.cn
}
class AppDirectory {
+String path: /opt/wecom-it-desk/app/
+String role: 唯一代码源
+List~File~ pythonFiles
+Boolean hasAuthPy
}
class BackendDirectory {
+String path: /opt/wecom-it-desk/backend/
+File dockerfile
+File requirementsTxt
+String note: 不再包含 app/ 子目录
}
class DockerImage {
+String name: wecom-it-desk-backend:latest
+String base: python:3.12-slim
+String pythonVersion: 3.12
+Boolean containsCode: false
+Boolean containsDeps: true
+String envPythondontWriteBytecode: "1"
}
class DockerContainer {
+String name: wecom_it_backend
+String workdir: /app
+String command: uvicorn app.main:app
+Integer port: 8000
+Boolean healthy
}
class VolumeMount {
+String hostPath: /opt/wecom-it-desk/app/
+String containerPath: /app/app/
+String type: bind
+String mode: rw
}
class UploadVolume {
+String name: backend-uploads
+String containerPath: /app/uploads/
+String type: named_volume
}
class LogMount {
+String hostPath: /var/log/wecom-it-desk/
+String containerPath: /app/logs/
+String type: bind
}
class DockerCompose {
+String file: docker-compose.yml
+String buildContext: ./backend
+List~Service~ services
}
class HealthCheck {
+String endpoint: /health
+Integer interval: 30
+Integer timeout: 10
+Integer retries: 3
+Integer startPeriod: 40
}
HostServer --> AppDirectory : contains
HostServer --> BackendDirectory : contains
HostServer --> DockerCompose : contains
DockerCompose --> DockerImage : builds
DockerCompose --> DockerContainer : runs
DockerCompose --> VolumeMount : configures
DockerCompose --> UploadVolume : configures
DockerCompose --> LogMount : configures
DockerImage --> DockerContainer : basis for
VolumeMount --> DockerContainer : mounts code to /app/app/
UploadVolume --> DockerContainer : mounts uploads to /app/uploads/
LogMount --> DockerContainer : mounts logs to /app/logs/
AppDirectory --> VolumeMount : source of
DockerContainer --> HealthCheck : monitored by
BackendDirectory --> DockerImage : provides Dockerfile + requirements.txt
@@ -0,0 +1,182 @@
# 智能IT服务台 - 部署指南
> **最后更新**2026-07-05
> **目标服务器**`10.90.5.110`
> **域名**`itsupport.servyou.com.cn`
---
## 目录
1. [部署流程](#一部署流程)
2. [蓝绿部署](#二蓝绿部署)
3. [故障排查](#三故障排查)
4. [回滚方案](#四回滚方案)
---
## 一、部署流程
### 1.1 前置条件
| 条件 | 状态 | 验证命令 |
|------|------|---------|
| Linux 服务器 10.90.5.110 | ✅ 已确认 | - |
| Docker 已安装 | ✅ 已确认 | `docker --version` |
| 域名解析 | ✅ 已配置 | `nslookup itsupport.servyou.com.cn` |
| 堡垒机可访问 | ✅ 已配置 | `ssh -p 2222 user@10.212.189.210` |
### 1.2 快速部署命令
```bash
# 1. 进入部署目录
cd /opt/wecom-it-desk
# 2. 拉取最新代码(可选)
git pull origin main
# 3. 构建并启动
docker compose up -d --build
# 4. 检查状态
docker ps
```
### 1.3 一键部署(生产环境)
详见 [10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
### 1.4 部署后验证
```bash
# 检查容器状态
docker ps
# 检查 API 健康
curl https://itsupport.servyou.com.cn/api/health
# 检查日志
docker logs wecom_it_backend --tail 50
```
---
## 二、蓝绿部署
### 2.1 什么是蓝绿部署
蓝绿部署通过维护两套环境(Blue/Green)实现零停机部署和快速回滚。
### 2.2 部署 Green 环境
```bash
cd /opt/wecom-it-desk
# 构建并启动 Green 环境
docker-compose -f docker-compose-green.yml up -d
# 验证 Green 环境
curl http://localhost:5002/health
```
### 2.3 切换流量
```bash
# 切换到 Green
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
# 切换回 Blue(回滚)
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
```
### 2.4 端口说明
| 端口 | 服务 |
|------|------|
| 80/443 | Nginx (生产入口) |
| 5002 | Backend (Green) |
| 5080 | Nginx (Green 测试) |
---
## 三、故障排查
### 3.1 常见问题
#### 502 Bad Gateway
1. 检查后端容器是否运行
```bash
docker ps | grep backend
```
2. 检查后端日志
```bash
docker logs wecom_it_backend --tail 100
```
3. 重启后端
```bash
docker restart wecom_it_backend
```
#### 500 错误
详见 [标准故障排查手册](../00-标准故障排查手册.md)
#### 通讯链路问题
详见 [标准故障排查手册](../00-标准故障排查手册.md)
### 3.2 健康检查
```bash
# 后端
curl http://localhost:8000/health
# Nginx
curl http://localhost/api/health
```
---
## 四、回滚方案
### 4.1 紧急回滚
如果部署后出现严重问题,立即执行:
```bash
# 1. 停止新版本容器
docker stop wecom_it_backend_green
# 2. 切换回原环境
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
```
### 4.2 版本回滚
如需回滚到之前版本:
```bash
# 1. 停止当前版本
docker stop wecom_it_backend
# 2. 重新构建指定版本
git checkout <版本标签>
docker build -t wecom-it-desk-backend:<版本> ./backend
# 3. 启动
docker run -d --name wecom_it_backend wecom-it-desk-backend:<版本>
```
---
## 相关文档
- [10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
- [蓝绿部署指南.md](./蓝绿部署指南.md)
- [标准故障排查手册](../00-标准故障排查手册.md)
@@ -0,0 +1,152 @@
# 智能IT服务台 - 版本记录
> **最后更新**2026-07-13
---
## 版本历史
### v0.7.3 (2026-07-12~13)
**更新内容**
1. **AI 对话链路全栈改造 Phase 1-6**#59-#69
- Dify Prompt JSON 输出 + 后端 blocking + JSON 解析 + 双 WS 推送
- 审批关键词收窄(~40→~25+ 两级分类 Prompt v4.0 + 删除前端 checkApprovalIntent
- WS 扩展(ai_thinking + dynamic_recommend+ MessageBubble ai_structured 渲染 + RightPanel v2
- VisionService 接入(图片分析 + 5秒消息融合)+ 降级策略
- 坐席端 ai_thinking 指示器 + ai_structured/byod_card 渲染
- diagnosis_stage 字段 + response_time_ms 计时 + 慢响应告警
2. **上下文感知智能诊断→修复闭环**
- 三层诊断(API→Script→AI+ 三段排队(VIP→info_locked→not locked
- 答题插队 + 五场景关闭 + 迁移 052(6表+6列)
3. **坐席端布局优化 v2.0**
- QuickReplyBar L1+L2 悬浮 / ReplyBox 左右分区 / 右栏 260↔560px
- 键盘快捷键 v2.3(纯数字路由 / ESC 分层撤销 / IME 守卫)
4. **知识库迭代 3**
- 分诊交互 + 拓扑预览(ECharts)+ 代答排除(4种匹配器)+ 迁移 051
5. **H5 v4 人工坐席交互改造**#116
- 三态文案统一"人工坐席" / 按钮位置上移 / 删除 CallAgentModal 弹窗
- 截图提示改版 / 移动端 CSS 隐藏 / DB 同步 funny_phrases 表
6. **部署路径修正**
- 确认服务器项目根路径 `/opt/wecom-it-desk/`
- 所有前端 dist 均为 ro bind mount,只能在宿主机源路径操作
**部署方式**:后端 `docker compose restart backend` / 前端宿主机 tar 解压 + `nginx -s reload`
**验证**JS hash 更新确认 / JS 包内容检查 / API 200 / Nginx healthy
---
### v0.7.1 (2026-06-23)
**更新内容**
- OTP 二次验证功能上线
- 扫码登录优化
- 安全性增强
**部署包**[10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
**相关文档**
- [07-扫码登录OTP部署指南-v0.7.0.md](./07-扫码登录OTP部署指南-v0.7.0.md)
- [06-OTP二次验证实现.md](./06-OTP二次验证实现.md)
---
### v0.7.0 (2026-06-16)
**更新内容**
- 消息推送策略优化
- 超时提醒功能
- 头像同步功能
**部署包**[一键部署操作包-v0.7.0.md](./一键部署操作包-v0.7.0.md)
---
### v0.6.x (历史版本)
详见 [03-RELEASE-NOTES-v0.7.1-20260623.md](./03-RELEASE-NOTES-v0.7.1-20260623.md)
---
## 问题修复记录
### 2026-07-26
- H5 选项交互全链路修复(CASE-20260726-01~08):白屏/编码损坏/Pinia `.value`/UUID 排序/轮询去重/坐席可见性等 8 个连环问题
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4 CASE-20260726-01~08
### 2026-07-25
- H5 始终显示坐席在线 — WS 断连查询字段不匹配(CASE-20260725-01
- H5 用户端收不到 AI 回复 — WebSocket 推送 datetime 序列化失败(CASE-20260725-02
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-24
- H5 选项选择消息重复 — 前端本地 message_id 与后端不同致去重失效(CASE-20260724-01
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-23
- H5 AI 回复显示 `[object Object]` — Dify 原生 API 超时回退代理(CASE-20260723-01
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-16
- 员工端 /h5/ 404 — nginx 反向代理配置错误(CASE-20260716-01
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-15
- 员工端审批"审批模板ID不正确" — 企微审批模板失效,改用 ITSMCASE-20260715-01
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-14
- 坐席端 403 — 前端 bind mount 未生效(CASE-20260714-01
- 员工端 /h5/ 404 — nginx 配置错误(CASE-20260714-02
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
### 2026-07-13
- H5 v4 部署路径修正(`/opt/wecom-it-desk/frontend-h5/dist` ro bind mount
- 服务器 Docker 挂载配置与本地仓库不一致问题定位
### 2026-07-05
- Nginx upstream 配置修复
- 蓝绿部署流程完善
详见 [标准故障排查手册](../00-标准故障排查手册.md)
### 2026-06-13
- H5 用户端报错修复
- 后端启动问题修复
详见 [标准故障排查手册](../00-标准故障排查手册.md)
---
## 版本号规则
| 位置 | 规则 | 示例 |
|------|------|------|
| 前端 | v主.次.修订 | v1.5.0 |
| 后端 | v主.次.修订 | v0.7.1 |
| 部署包 | v主.次.发布日期 | v0.7.0-20260623 |
---
## 升级路径
### 从 v0.6.x 升级到 v0.7.x
1. 备份数据
2. 执行一键部署
3. 验证功能
4. 监控日志
### 版本回滚
详见 [01-部署指南.md](./01-部署指南.md) 中的回滚方案
@@ -0,0 +1,43 @@
# H5用户端原型图 → Vue3代码实现概览
## 完成时间
2026-06-09
## 变更摘要
根据已锁定的原型图 v1.1 修复版,将 H5 用户端设计实现为 Vue3 代码。
## 修改文件清单
### 1. `frontend-h5/src/components/chat/ChatPanel.vue`
- **标题栏重构**:左侧(标题 + 坐席在线/离线状态胶囊) + 右侧(🔔呼叫按钮 + 主题切换)
- **🔔摇铃按钮**:从输入栏移至标题栏(桌面端+手机端统一)
- **排查步骤固定顶部**:从消息列表内移出,固定在标题栏下方、所有消息之上,不随滚动消失
- **移除 InputBar 事件**:不再需要 @call-agent 事件(摇铃直接在 ChatPanel 内控制)
### 2. `frontend-h5/src/components/chat/InputBar.vue`
- **移除摇铃按钮**:删除 🔔 摇铃按钮及相关 CSSbell-btn/bell-icon/bell-idle/bell-ring 动画)
- **新增工具栏**:😊表情 / 🖼️图片 / 📎文件 / 📸拍照(4个圆形按钮)
- **布局改为两行**:工具栏(上) + 输入行(输入框+发送按钮)(下)
- **新增方法**handleEmoji/handleImage/handleFile/handleCamera(阶段二实现具体功能)
- **引导条文案更新**:"点击标题栏铃铛呼叫 IT 坐席"
### 3. `frontend-h5/src/components/assistant/RightPanel.vue`(新建)
- **三段式面板**:AI推送区 / 常用资源标签页 / 趣味问答
- **AI推送区**3种卡片类型(guide/process/download) + 动态图标+颜色
- **常用资源**:2个Tab(申请流程/必装软件) + 资源列表
- **趣味问答**:题目+4选项+积分+答题结果反馈
- **阶段一静态数据**,阶段二接入 Dify 动态推送
### 4. `frontend-h5/src/views/ChatView.vue`
- **替换右侧面板**AiHelperPanel → RightPanel(三段式面板)
- **响应式断点**:从768px改为500px(与原型图对齐)
- **移动端**<500px 不显示右侧面板
- **拖拽逻辑修复**:只固定左侧宽度,右侧 flex:1 自动填满(消除拖拽后空白)
- **移除浮动按钮**:不再需要移动端AI助手浮动按钮
### 5. `frontend-h5/src/stores/conversation.ts`
- **新增 agentOnline 状态**:默认true,阶段一简化处理
- **暴露到 return 语句**:使组件可以访问
## 构建验证
`npx vite build` 构建成功,无编译错误
@@ -0,0 +1,52 @@
%% IT 智能服务台 — 部署时序图(卷挂载重构)
%% 文件: sequence-diagram.mermaid
sequenceDiagram
participant Ops as 运维人员
participant Host as 宿主机 10.90.5.110
participant Docker as Docker Engine
participant Container as Backend 容器
Note over Ops,Container: S1: 前置验证与备份
Ops->>Host: curl /health 验证当前状态
Host-->>Ops: 200 OK
Ops->>Host: 备份 docker-compose.yml → .bak.{TIMESTAMP}
Ops->>Host: 备份 Dockerfile → .bak.{TIMESTAMP}
Ops->>Host: 写入 .rollback-info 文件
Note over Ops,Container: S2: 代码验证
Ops->>Host: 检查 app/__init__.py, app/main.py
Ops->>Host: 检查 app/api/auth.py(关键!)
Host-->>Ops: 全部存在
Note over Ops,Container: S3: 修改 Dockerfile
Ops->>Host: 重写 Dockerfile
Note right of Host: 删除 COPY . .<br/>新增 ENV PYTHONDONTWRITEBYTECODE=1
Ops->>Host: grep 验证 COPY . . 已删除
Note over Ops,Container: S4: 修改 docker-compose.yml
Ops->>Host: sed 插入 ./app:/app/app 卷挂载
Ops->>Host: grep 验证卷挂载已添加
Note over Ops,Container: S5: 重建镜像并重启
Ops->>Docker: docker compose build backend
Docker->>Docker: 构建镜像(仅 site-packages
Note right of Docker: 镜像不含业务代码
Ops->>Docker: docker compose up -d backend
Docker->>Container: 创建容器
Docker->>Host: 挂载 ./app → /app/app (bind mount)
Container->>Host: 运行时读取 /opt/wecom-it-desk/app/ 代码
Container->>Container: uvicorn app.main:app 启动
Note over Ops,Container: S6: 部署后验证
Ops->>Container: curl http://localhost:8000/health
Container-->>Ops: 200 OK
Ops->>Container: docker exec ... from app.auth import router
Container-->>Ops: auth module: OK
Ops->>Host: md5sum app/main.py
Ops->>Container: docker exec md5sum /app/app/main.py
Note right of Ops: 对比 hash 一致 → 卷挂载正常
Note over Ops,Container: S7: 清理(48小时后)
Ops->>Host: rm -rf backend/app/
Ops->>Host: 确认服务仍正常
@@ -0,0 +1,362 @@
# nginx 真实 IP 还原 — 生产部署(小白友好版)
> 术语速查:**nginx** = 你这台服务器的"门卫",负责把用户请求分发给后端 / 把静态文件返回给浏览器
> **配置** = nginx 的工作规则,改配置 = 改门卫的工作方式
---
## 我们要做啥(整体目标)
**一句话目标**:`https://itsupport.servyou.com.cn/itadmin/` 之前返回 403(被门卫拦了),原因是门卫把"代理服务器 IP"当成了"用户 IP",而代理 IP 不在白名单里。这次我们改门卫的规则,让它从请求头里读"真实用户 IP"。
**一共 7 个动作**:
| # | 动作 | 大概多久 | 风险 |
|---|---|---|---|
| 1 | PuTTY 连上服务器 | 1 分钟 | ⚪ 无风险 |
| 2 | 备份当前配置 | 几秒 | 🟢 备份原文件,可还原 |
| 3 | 写入 13 行新规则 | 几秒 | 🟡 改配置,但有备份 |
| 4 | 确认写入正确 | 几秒 | ⚪ 只读不写 |
| 5 | 检查配置语法 | 几秒 | ⚪ 只读不写 |
| 6 | 让 nginx 重新读规则 | 1 秒 | 🟡 短暂重载,服务不中断 |
| 7 | 浏览器看效果 | 几秒 | ⚪ 只读 |
**总耗时**:第一次大概 5-10 分钟;熟练了 2 分钟
**整体风险**:🟢 **低** — 每一步都给了"回滚"按钮,改坏了随时能恢复
---
## PuTTY 是啥?在哪儿打开?
**PuTTY** = 一个 SSH 客户端软件,作用是让你从你的 Windows 电脑远程连到公司的 Linux 服务器
**打开方式**:
-`Win 键` → 输入 `putty` → 回车
- 或者开始菜单 → 找到 PuTTY 图标
打开后会看到一个灰底配置界面,我们要填 4 项:
```
┌──────────────────────────────────────┐
│ Host Name (or IP address) │ ← 填: 10.212.189.210
│ Port │ ← 填: 2222
│ Connection type │ ← 选: SSH(默认就是)
│ Saved Sessions │ ← 填: wecom-bastion(起个名)
└──────────────────────────────────────┘
点 Save 保存 → 点 Open 开始连接
```
连接后会黑底白字,提示 `login as:` → 输入 `sxn` 回车 → 提示 `password:` → 输入你的堡垒机密码(输入时屏幕不显示,正常,输完回车就行)
**注意**:输错密码不会锁账号,直接重新输
---
## 动作 1:PuTTY 连服务器(⚪ 无风险)
> **为啥要连服务器?**:改配置必须在服务器上操作,你 Windows 这边只是"遥控器"
连上堡垒机后,黑底白字会显示一个类似 `sxn@jump-host:~$` 的提示符,说明你已经到堡垒机了。
**决策树**:
```
你现在看到了堡垒机提示符(类似 sxn@jump-host:~$)
├─ 是 → 在 PuTTY 里继续输入下面命令
└─ 否 → 截图发给我,卡哪儿了
```
贴下面的命令(右键 = 粘贴,Enter = 执行):
```bash
# 从堡垒机跳到真正的生产服务器
ssh sxn@10.90.5.110
```
回车后可能要输密码(堡垒机和目标机密码可能不同,试一下你之前用过的那个)
**✅ 成功长这样**:
```text
sxn@prod-server:~$
```
**❌ 失败常见**:
- `Permission denied` → 密码错了,重输
- `Connection timed out` → 网络问题,可能 VPN 没连
- 卡住不动 → 可能需要输 `yes` 确认服务器指纹,看到 `(yes/no/[fingerprint])?` 就输 `yes` 回车
---
## 动作 2:备份当前配置(🟢 低风险,改坏了能还原)
> **为啥要备份?**:运维铁律 — **改任何东西之前先备份**,这样改坏了能用备份还原,不会把生产搞挂
```bash
# 进入 nginx 配置所在目录
cd /opt/wecom-it-desk/nginx
# 复制一份当前配置,文件名带当前时间(分),方便区分
sudo cp nginx.conf nginx.conf.bak-$(date +%H%M)
# 列出所有备份文件,确认刚才那行成功
ls -la nginx.conf.bak-*
```
**为啥用 `$(date +%H%M)`?**:这个写法会自动拼上当前时间(比如 1430 表示 14:30),每次备份文件名都不一样,不会覆盖之前的备份
**✅ 成功长这样**:
```text
-rw-r--r-- 1 root root 4821 Jun 15 14:30 nginx.conf.bak-1430
```
**❌ 失败常见**:
- `cp: cannot stat 'nginx.conf'` → 当前不在 nginx 目录,先 `cd /opt/wecom-it-desk/nginx` 进去
- `Permission denied` → 缺 `sudo`,命令前面加 `sudo` 重试
---
## 动作 3:写入 13 行新规则(🟡 中风险,但有备份兜底)
> **写入啥?**:13 行 nginx 配置,告诉 nginx"从请求头 X-Forwarded-For 里读真实用户 IP"
>
> **为啥要这样做?**:用户通过公司 WAF/堡垒机访问,WAF 会把真实 IP 放在 `X-Forwarded-For` 请求头里,但 nginx 默认只看直连 IP,所以才误判 403
**重要**:把下面**从 `cat > /tmp/patch.py``PYEOF`** 的**整段**一次性粘贴进 PuTTY(右键 = 粘贴)。整段会作为一条命令执行。
```bash
# 创建一个 python 脚本到 /tmp/patch.py
cat > /tmp/patch.py << 'PYEOF'
fp = '/opt/wecom-it-desk/nginx/nginx.conf'
with open(fp) as f:
c = f.read()
patch = '''
# ------------------------------------------------------------------
# 真实 IP 还原(2026-06-15 v0.5.1 修复)
# ------------------------------------------------------------------
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
'''
old = 'error_log /var/log/nginx/error.log warn;'
new = old + patch
new_c = c.replace(old, new, 1)
with open(fp, 'w') as f:
f.write(new_c)
print('patched, +{} bytes'.format(len(new_c) - len(c)))
PYEOF
# 运行这个 python 脚本,它会自动把上面那 13 行插入到 nginx.conf
sudo python3 /tmp/patch.py
```
**术语解释**:
- `cat > /tmp/patch.py` → 创建一个文件,内容是后面所有内容
- `<< 'PYEOF' ... PYEOF` → 这种写法叫 **heredoc**(直译"这里是文档"),作用是把多行文字原样写入文件
- `sudo` → 以管理员身份运行(改系统文件需要权限)
**✅ 成功长这样**:
```text
patched, +492 bytes
```
**❌ 失败常见**:
- `Permission denied` → 缺 `sudo`,或者 nginx.conf 不存在
- `NameError: name 'fp' is not defined` → heredoc 没贴完整,最末尾的 `PYEOF` 没贴上
- 没任何输出 → python 没运行,看光标有没有新行,可能没回车
---
## 动作 4:确认写入正确(⚪ 无风险,只读)
> **为啥要确认?**:虽然脚本说写入了,但**人眼看到才真的算**。这步只读不写,放心跑
```bash
# 在 nginx.conf 里搜索"真实 IP 还原"关键字,并显示后面 13 行
sudo grep -A 13 "真实 IP 还原" /opt/wecom-it-desk/nginx/nginx.conf
```
**✅ 成功长这样**(应该看到完整 13 行):
```nginx
# 真实 IP 还原(2026-06-15 v0.5.1 修复)
# ------------------------------------------------------------------
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
```
**❌ 失败**:
- 啥也没输出 → 写入失败,回到动作 3 重做
- 只输出一两行 → heredoc 没贴全,需要回滚后重来
---
## 动作 5:检查配置语法(⚪ 无风险,只读不执行)
> **为啥要检查?**:这个命令 nginx 会"假装"按新配置启动,只检查语法,不会真的重启。**通过 = 配置写得对,放心用;不通过 = 写得有问题,继续走会出问题**
```bash
# 在 nginx 容器(就是跑 nginx 服务的那个小 Linux)内,做配置语法检查
docker compose exec nginx nginx -t
```
**术语解释**:
- `docker compose` → 管理这台服务器上所有"容器"的命令
- `exec` → "钻进"某个容器里执行命令
- `nginx -t` → nginx 自带的"语法检查"工具(全称 `--test`)
**✅ 成功长这样**:
```text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
**❌ 失败**(`test is successful` 没出现):
- `unexpected "}"` / `unknown directive` → 写错字了,回去动作 4 看看哪里对不上
- **直接停下,不要继续** → 复制错误信息贴回给我
---
## 动作 6:让 nginx 重新读规则(🟡 中风险,但服务不中断)
> **"重新读"是啥意思?**:nginx 现在用的还是旧配置,我们让 nginx 不用重启(不会断服务)就把新配置加载进来。这个动作叫"热加载"或 "reload"
>
> **会断网吗?**:不会,reload 是无缝的,用户那边无感知
```bash
# 通知 nginx 容器内的 master 进程重新读配置
docker compose exec nginx nginx -s reload
```
**术语解释**:`-s reload` = 发信号(英文 signal)给 nginx,告诉它"重读配置"
**✅ 成功长这样**(没报错即成功):
```text
2026/06/15 14:35:12 [notice] 1#1: signal process started
```
**❌ 失败**:
- `nginx: [error]` 开头 → 配置没通过,回去动作 5 看哪里没对
- 啥也没输出 → 命令没执行,看光标位置
---
## 动作 7:浏览器看效果(⚪ 无风险)
**为啥这步是浏览器而不是 curl?**:curl 看响应头,浏览器看真实页面。**人眼看到才作数**
**操作步骤**:
1. 打开浏览器
2. **开隐身模式**(`Ctrl + Shift + N`,Chrome / Edge 都是这个快捷键)
- **为啥要隐身?**:隐身模式不读本地缓存,看到的就是 nginx **当下**返回的
3. 地址栏输入 `https://itsupport.servyou.com.cn/itadmin/`
4. 按回车
**✅ 成功长这样**:
- 页面正常显示
-`F12` 打开开发者工具 → `Network` 选项卡 → 顶部那一行状态码是 **200**(不是 403)
**❌ 失败**:
- 仍然是 403 → 见下面"如果还是 403"段
- 502 / 504 → nginx 后面那个服务挂了,贴错误给我
- 页面打不开(连接被拒) → DNS 没配,联系 IT 运维
---
## 如果还是 403 — 看 WAF 出口 IP(诊断)
> **啥是 WAF?**:公司部署在 nginx 前面的"统一入口",所有用户请求先经过 WAF 再到 nginx。WAF 自己的 IP 不一定在你写的 4 段内网里,所以还得加
```bash
# 看 nginx 最后 20 条访问日志,找 $remote_addr 是不是 WAF 的 IP
docker compose exec nginx tail -20 /var/log/nginx/access.log
```
**日志长这样**:
```text
10.80.5.123 - - [15/Jun/2026:14:35:45 +0800] "GET /itadmin/ HTTP/1.1" 403 ...
^^^^^^^
这就是 $remote_addr
```
把那个 IP 数字(比如 `10.80.5.123`)贴回给我,我会:
1. 给你追加一行 `set_real_ip_from 10.80.5.123;`
2. 让你重跑动作 5 + 动作 6
---
## 如果改坏了 — 回滚(啥时候都能用)
> **啥时候用?**:任何一个动作出问题,你都可以直接回滚到动作 2 备份的版本
```bash
# 列出所有备份,挑最近的一个
ls -la /opt/wecom-it-desk/nginx/nginx.conf.bak-*
```
```bash
# 用最近那个备份覆盖当前配置(把 1430 换成上面列出的真实时间)
sudo cp /opt/wecom-it-desk/nginx/nginx.conf.bak-1430 /opt/wecom-it-desk/nginx/nginx.conf
```
```bash
# 重新加载回滚后的配置
docker compose exec nginx nginx -s reload
```
回滚后页面应该回到改之前的状态(403 回来),说明回滚成功
---
## 一张图看懂流程
```
PuTTY 连服务器
备份原配置
写入 13 行新规则
确认写入正确 ──→ ❌ 不对 ──→ 重做写入 / 回滚
│ ✅
检查配置语法 ──→ ❌ 语法错 ──→ 复制错误贴回给我,不要继续
│ ✅
重载 nginx ─────→ ❌ 报错 ──→ 检查容器状态 / 找 Claude
│ ✅
浏览器看效果 ──→ ❌ 还是 403 ──→ 看 WAF 出口 IP,贴给 Claude
│ ✅
🎉 完成
```
---
## 我建议你第一次做
**第一次建议**:动作 1 → 动作 2 → **停一下,截图发给我** → 我确认备份成功 → 你再继续动作 3 之后
**熟练了以后**:一口气跑完动作 1-7,中间不打断
---
## 关联
- 评审报告:`review-p0-security-2026-06-14.md` P0-3
- 待办:`ip-whitelist-trust-proxies-todo.md` — v1.0 前必须收窄 4 段 → 4 个 IP
- 本地配置:`deploy-server/nginx/nginx.conf`(已包含 patch,下次重打包自动带)
- 服务器 IP 变更:`project-production-server-ip-2026-06-15.md` — 10.80.0.136 已下线,用 10.90.5.110
- 客户端约束:`feedback-putty-not-openssh.md` — 用 PuTTY,不用 `ssh -J`
- 命令行规范:`feedback-cmd-step-by-step.md` — 每行一条 + 中文注释
- 小白引导规范:`feedback-beginner-friendly-guide.md` — 讲清目标+风险、术语解释、出错兜底
@@ -0,0 +1,76 @@
# 部署运维工具箱
> **版本**: v1.0 | **日期**: 2026-07-10 | **维护人**: 宋献
> **定位**: 部署运维过程中可复用的脚本、配置模板和调试工具的统一存放点。
> **规则**: 每次故障排查或部署完成后,可复用的工具应归档到此目录并在本 README 中登记。
---
## 工具索引
### 上传部署工具
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `fast_upload.py` | 堡垒机大文件快速上传(已被 jumpserver-V2 的 psftp 通道替代,仅供参考) | `python fast_upload.py <本地文件> <远程路径>` |
| `deploy_to_container.py` | 一键部署到 Docker 容器(打包→上传→cp→重启) | `python deploy_to_container.py <服务名> <本地路径> <容器路径>` |
### Nginx 配置模板
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `nginx-access-control.conf` | 三端访问控制配置模板(企微 UA OR IP 白名单双条件放行 + CSP 头) | 上传到服务器 `/opt/wecom-it-desk/nginx/nginx.conf``docker restart wecom_it_nginx` |
### 调试检查工具
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `check_html.py` | Python f-string HTML 花括号平衡检查器(排查 `{{ }}` 转义问题) | `python check_html.py <file.py>` |
| `render_test.py` | 容器内 HTML 渲染验证(模拟 f-string 渲染并输出实际 HTML/JS | `docker exec wecom_it_backend python /tmp/render_test.py` |
| `extract_html.py` | 从 Python f-string 中提取 HTML 模板到独立文件 | `python extract_html.py <file.py>` |
### 历史修复脚本(archive/
以下脚本为一次性修复用途,保留在 `archive/` 子目录中供参考,不建议直接复用。
| 脚本 | 修复场景 | 日期 |
|------|----------|------|
| `fix_admin_role.py` | 修复扫码登录角色写死为 agent 的问题 | 2026-07-08 |
| `fix_compose_redis.py` / `fix_compose_redis2.py` | 修复 docker-compose Redis 密码 URL 编码 | 2026-07-07 |
| `fix_itdesk.py` / `fix_itdesk2.py` / `fix_itdesk3.py` | 修复 nginx /itdesk/ 路由问题 | 2026-07-08 |
| `patch.py` / `patch-mini.py` / `patch-redis-url.py` | Redis 密码 URL 编码补丁 | 2026-07-02 |
| `update_password.py` | 数据库密码更新脚本 | 2026-07-05 |
| `check_logs.py` / `check_roles.py` / `verify_logs.py` | 日志和角色检查(一次性诊断) | 2026-07-08~09 |
| `upload_chunked.py` / `upload_split.py` | 早期分块上传方案(已被 fast_upload.py 替代) | 2026-07-08 |
| `nginx_*.conf` / `itdesk-nginx-block.conf` | 历史 nginx 配置快照 | 2026-07-06~08 |
| `extract_and_migrate.py` / `_ctrt_transform.py` | 数据迁移和格式转换 | 2026-07-06 |
---
## 工具沉淀流程
1. **排查完成** → 评估是否有可复用的脚本/配置模板
2. **归档** → 复制到 `toolbox/`(活跃工具)或 `toolbox/archive/`(历史脚本)
3. **登记** → 在本 README 的工具索引表中添加条目
4. **清理** → 删除项目根目录的临时文件(渲染输出、中间产物等)
## 堡垒机使用说明
所有工具涉及服务器操作时,通过堡垒机(`sxn@10.212.189.210:2222`)执行:
- 统一使用 `v2_ops.py`jumpserver-V2 技能)
- 详见 `docs/04-运维文档/部署运维/11-堡垒机运维工具.md`
---
## 相关文档
| 文档 | 位置 |
|------|------|
| 标准故障排查手册 | `docs/04-运维文档/部署运维/00-标准故障排查手册.md` |
| 堡垒机运维工具 | `docs/04-运维文档/部署运维/11-堡垒机运维工具.md` |
| 项目管理 SOP | `docs/07-项目管理/IT智能服务台-标准作业流程SOP.md` |
| Nginx 基线配置 | `docs/04-运维文档/部署运维/01-nginx-prod-baseline-20260708.conf` |
---
> **维护说明**: 新增工具时请同步更新本 README。archive/ 中的脚本仅供历史参考,不保证可用性。
@@ -0,0 +1,19 @@
import re
with open(
r'D:\资料\03-项目开发\wecom_it_smart_desk\backend\app\api\auth_qrcode.py',
'r',
encoding='utf-8',
) as f:
content = f.read()
match = re.search(r'html = f"""(.+?)"""', content, re.DOTALL)
if match:
html = match.group(1)
print('HTML length:', len(html))
out_path = r'D:\资料\03-项目开发\wecom_it_smart_desk\tmp_scan_html.html'
with open(out_path, 'w', encoding='utf-8') as out:
out.write(html)
print('Saved to', out_path)
else:
print('HTML template not found')
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
快速 base64 上传脚本 — 大分块版本(已废弃)
将本地文件通过 JumpServer PTY base64 通道上传到远程服务器
使用 8000 字符/块(远大于 jms_ops.py 的 500 字符),大幅减少命令数
⚠️ 此脚本已废弃,请使用 jumpserver-V2 的 v2_ops.py upload 命令(psftp 通道)
"""
import sys
import base64
import hashlib
from pathlib import Path
# 添加 jms_ops.py 所在目录
SKILL_DIR = Path(r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts")
sys.path.insert(0, str(SKILL_DIR))
# 导入 jms_ops 中的核心函数
from jms_ops import get_connection_tokens, PlinkSession
def fast_upload(local_path: str, remote_path: str, chunk_size: int = 8000):
"""大分块 base64 上传"""
local_file = Path(local_path)
if not local_file.exists():
print(f"ERROR: file not found: {local_path}")
return False
local_data = local_file.read_bytes()
local_md5 = hashlib.md5(local_data).hexdigest()
b64_data = base64.b64encode(local_data).decode("ascii")
# 分块
chunks = [b64_data[i:i+chunk_size] for i in range(0, len(b64_data), chunk_size)]
total_chunks = len(chunks)
print(f"File: {local_file.name}")
print(f"Size: {len(local_data)} bytes")
print(f"Base64: {len(b64_data)} chars")
print(f"Chunks: {total_chunks} x {chunk_size} chars")
print(f"MD5: {local_md5}")
print(f"Target: {remote_path}")
print()
# 获取 token + 启动会话
tokens = get_connection_tokens(1)
if not tokens:
print("ERROR: failed to get connection tokens")
return False
token_id, token_secret = tokens[0]
session = PlinkSession(f"JMS-{token_id}", token_secret)
if not session.connect():
print("ERROR: failed to connect session")
return False
try:
# 1. 清空目标文件
print("Clearing target file...")
session.run_command(f"> {remote_path}", timeout=5)
# 2. 逐块追加
for i, chunk in enumerate(chunks):
# 用 printf 避免 echo 的换行符问题
cmd = f"printf '%s' '{chunk}' >> {remote_path}.b64"
r = session.run_command(cmd, timeout=10)
if not r["success"]:
print(f" FAIL chunk {i+1}/{total_chunks}")
return False
# 进度报告
if (i+1) % 20 == 0 or (i+1) == total_chunks:
pct = (i+1) * 100 // total_chunks
print(f" [{pct:3d}%] chunk {i+1}/{total_chunks}")
# 3. base64 解码
print(f"\nDecoding base64 -> {remote_path}...")
r = session.run_command(f"base64 -d {remote_path}.b64 > {remote_path}", timeout=30)
if not r["success"]:
print(f" WARN decode result: {r}")
# 4. 验证大小
print("Verifying size...")
r = session.run_command(f"wc -c < {remote_path}", timeout=5)
if r["success"]:
remote_size_str = r["output"].strip()
remote_size = int(remote_size_str) if remote_size_str.isdigit() else -1
if remote_size == len(local_data):
print(f" OK size match: {remote_size} bytes")
else:
print(f" SIZE MISMATCH: local={len(local_data)}, remote={remote_size}")
return False
else:
print(f" WARN cannot verify size: {r}")
return True # 仍然认为成功
# 5. MD5 验证
print("Verifying MD5...")
r = session.run_command(f"md5sum {remote_path}", timeout=10)
if r["success"]:
remote_md5 = r["output"].split()[0]
if remote_md5 == local_md5:
print(f" OK MD5 match: {remote_md5}")
else:
print(f" MD5 MISMATCH: local={local_md5}, remote={remote_md5}")
# 大小匹配但 MD5 不匹配,可能是 PTY 换行符问题
print(" (size matches, trying gzip test instead)")
r2 = session.run_command(f"gzip -t {remote_path} 2>&1 && echo GZIP_OK || echo GZIP_FAIL", timeout=10)
if r2["success"] and "GZIP_OK" in r2["output"]:
print(" OK gzip integrity test passed")
# 清理临时文件
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
return True
else:
print(f" GZIP FAIL: {r2}")
return False
else:
print(f" WARN cannot verify MD5")
# 6. 清理临时文件
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
print("\nUpload complete!")
return True
finally:
session.close()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Fast base64 upload via JumpServer PTY")
parser.add_argument("local", help="Local file path")
parser.add_argument("remote", help="Remote file path")
parser.add_argument("--chunk-size", type=int, default=8000, help="Chunk size in chars (default: 8000)")
args = parser.parse_args()
success = fast_upload(args.local, args.remote, args.chunk_size)
sys.exit(0 if success else 1)
@@ -0,0 +1,181 @@
"""在容器内运行,渲染 scan 端点的实际 HTML 输出"""
import sys
sys.path.insert(0, '/app')
# 模拟 f-string 中的变量
user_name = "测试用户"
jsapi_signature = "test_sig_abc123"
jsapi_timestamp = 1752105600
jsapi_nonce = "test_nonce_xyz"
jsapi_appid = "ww_test_corp_id"
current_url = "https://itsupport.servyou.com.cn/api/auth_qrcode/scan?code=test&state=test"
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录成功 - IT智能服务台</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #07C160 0%, #06AD56 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }}
.card {{ background: rgba(255,255,255,0.95); border-radius: 20px; padding: 48px 32px; max-width: 360px; width: 100%; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }}
.check {{ width: 64px; height: 64px; margin: 0 auto 16px; }}
.title {{ color: #1f2937; font-size: 24px; font-weight: 600; margin-bottom: 8px; }}
.subtitle {{ color: #6b7280; font-size: 14px; margin-bottom: 24px; }}
.status {{ display: inline-flex; align-items: center; gap: 6px; background: #dcfce7; color: #166534; padding: 10px 20px; border-radius: 50px; font-size: 14px; font-weight: 500; }}
.footer {{ margin-top: 20px; color: #9ca3af; font-size: 12px; }}
.back-btn {{ display: none; margin-top: 20px; padding: 12px 32px; background: #07C160; color: white; border: none; border-radius: 50px; font-size: 16px; font-weight: 500; cursor: pointer; }}
.debug {{ margin-top: 16px; color: #6b7280; font-size: 11px; line-height: 1.6; word-break: break-all; text-align: left; background: #f3f4f6; padding: 10px 12px; border-radius: 8px; }}
.debug b {{ color: #07C160; }}
</style>
</head>
<body>
<div class="card">
<svg class="check" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#07C160" stroke="#06AD56" stroke-width="4"/>
<path d="M20 32l8 8 16-16" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h1 class="title">登录成功</h1>
<div class="status">已自动确认登录</div>
<p class="subtitle">你好,{user_name}<br>请返回电脑端查看</p>
<button class="back-btn" id="backBtn" onclick="manualClose()">点击返回企微</button>
<div class="footer">页面即将自动关闭 · 税友集团</div>
<div class="debug" id="debugInfo">
<b>签名:</b> {'成功' if jsapi_signature else '未生成'}<br>
<b>URL:</b> {current_url}<br>
<b>状态:</b> <span id="jsStatus">JS加载中...</span>
</div>
</div>
<script>
(function() {{
var statusEl = document.getElementById('jsStatus');
var btnEl = document.getElementById('backBtn');
var startTime = Date.now();
var tried = {{}};
function setStatus(msg) {{
if (statusEl) statusEl.textContent = msg + ' (' + (Date.now() - startTime) + 'ms)';
}}
function showBtn() {{
if (btnEl) btnEl.style.display = 'inline-block';
}}
function tryClose(forceShowBtn) {{
setStatus('尝试关闭');
if (!tried.wxClose && typeof wx !== 'undefined' && wx.closeWindow) {{
tried.wxClose = true;
try {{
setStatus('wx.closeWindow');
wx.closeWindow();
return true;
}} catch(e) {{ setStatus('wx.closeWindow失败:' + (e.message || e)); }}
}}
if (!tried.wxInvoke && typeof wx !== 'undefined' && wx.invoke) {{
tried.wxInvoke = true;
try {{
setStatus('wx.invoke closeWindow');
wx.invoke('closeWindow', {{}}, function(){{}});
return true;
}} catch(e) {{ setStatus('wx.invoke失败:' + (e.message || e)); }}
}}
if (!tried.jsBridge && typeof WeixinJSBridge !== 'undefined' && WeixinJSBridge.call) {{
tried.jsBridge = true;
try {{
setStatus('WeixinJSBridge.closeWindow');
WeixinJSBridge.call('closeWindow');
return true;
}} catch(e) {{ setStatus('JSBridge失败:' + (e.message || e)); }}
}}
if (!tried.windowClose) {{
tried.windowClose = true;
try {{
setStatus('window.close');
window.close();
return true;
}} catch(e) {{}}
}}
if (!tried.historyBack) {{
tried.historyBack = true;
try {{
setStatus('history.back');
history.back();
return true;
}} catch(e) {{}}
}}
if (forceShowBtn) {{
setStatus('无法自动关闭,请手动返回');
showBtn();
}}
return false;
}}
function manualClose() {{
tryClose(true);
}}
var checkCount = 0;
var maxChecks = 50;
var interval = setInterval(function() {{
checkCount++;
var hasWx = typeof wx !== 'undefined';
var hasBridge = typeof WeixinJSBridge !== 'undefined';
setStatus('检测中 wx=' + hasWx + ' bridge=' + hasBridge + ' count=' + checkCount);
if (hasWx || hasBridge) {{
clearInterval(interval);
setStatus('已检测到关闭API1秒后尝试关闭');
setTimeout(function() {{
tryClose(true);
}}, 1000);
return;
}}
if (checkCount >= maxChecks) {{
clearInterval(interval);
setStatus('未检测到API,直接尝试关闭');
tryClose(true);
}}
}}, 100);
setTimeout(function() {{
showBtn();
}}, 3000);
}})();
</script>
</body>
</html>"""
# 保存渲染后的 HTML
with open('/tmp/test_scan.html', 'w', encoding='utf-8') as f:
f.write(html)
print(f"HTML rendered: {len(html)} chars")
print("Saved to /tmp/test_scan.html")
# 检查 script 部分
import re
script_match = re.search(r'<script>(.+?)</script>', html, re.DOTALL)
if script_match:
js = script_match.group(1)
print(f"\nJS section: {len(js)} chars")
# 检查是否有 {{ 残留(f-string 未正确渲染)
if '{{' in js:
print("ERROR: Found unrendered {{ in JS!")
for i, line in enumerate(js.split('\n')):
if '{{' in line:
print(f" Line {i+1}: {line.strip()[:80]}")
else:
print("OK: No unrendered braces in JS")
# 打印前 20 行 JS
print("\nFirst 20 lines of rendered JS:")
for i, line in enumerate(js.split('\n')[:20]):
print(f" {i+1}: {line}")
@@ -0,0 +1,64 @@
# Dify 一键部署脚本(简化版)
由于完整版 Dify 依赖较多服务,提供一个简化版本
## 使用说明
### 方式1:使用官方一键部署(推荐)
```bash
# Linux/Mac
curl -L https://dify.ai/install.sh | bash
# Windows (使用 PowerShell)
irm https://dify.ai/install.ps1 | iex
```
### 方式2:手动部署简化版
创建一个简化版的 docker-compose.yml
```yaml
version: '3'
services:
api:
image: langgenius/dify-api:latest
ports:
- "8081:8081"
environment:
- SECRET_KEY=dify-secret-key
- DB_USERNAME=postgres
- DB_PASSWORD=dify123
- DB_HOST=10.0.0.1 # 远程 PostgreSQL
- REDIS_HOST=10.0.0.2 # 远程 Redis
web:
image: langgenius/dify-web:latest
ports:
- "8080:3000"
```
### 方式3:使用在线 Dify 服务
生产环境已有 Dify 服务(内网可访问):
- 地址:http://yw-dify.dc.servyou-it.com/
---
## 本地开发建议
由于本地部署 AI 服务资源需求大,建议:
1. **开发测试时**:使用 Mock 数据(已实现)
2. **集成测试时**:连接生产 Dify(需内网)
3. **完整部署时**:在服务器上部署
---
## 快速验证 Dify API
```powershell
# 测试生产 Dify
curl -X GET 'http://yw-dify.dc.servyou-it.com/console/api/workspaces' \
-H 'Authorization: Bearer YOUR-API-KEY'
```
@@ -0,0 +1,350 @@
# 会议室预定-小鱼易联终端 部署指南
> **日期**: 2026-07-11
> **版本**: v2.0(新增报修+指南+二维码+移动端适配)
> **代码状态**: 待部署
> **预估部署时间**: 45-60 分钟
---
## 一、部署前置条件
### 1.1 企微配置
- [x] 企微会议室 Secret 已申请
- [x] 企微会议室已在管理后台创建
- [ ] 确认会议室 `meetingroom_id` 列表
### 1.2 服务器环境
- [ ] PostgreSQL 可用(需执行 Alembic 迁移 050 + 051
- [ ] Redis 可用(会议室缓存依赖)
- [ ] Nginx 可用(终端前端静态文件 + API 代理)
- [ ] Docker Compose 可用
### 1.3 终端设备
- [ ] 确认小鱼易联终端型号(NE90/NE60 支持 H5 应用,NE2005 需二维码降级)
- [ ] 终端浏览器支持 WebSocket + ES6
- [ ] 小鱼管理后台可访问(配置 H5 应用入口)
---
## 二、部署步骤
### Step 1: 数据库迁移
```bash
# 进入后端容器执行迁移(050 会议室基础表 + 051 报修+指南表)
docker compose exec backend alembic upgrade head
# 验证新表
docker compose exec backend python -c "
from app.database import engine
from sqlalchemy import inspect
insp = inspect(engine)
tables = insp.get_table_names()
print('terminal_room_bindings' in tables) # 应为 True050
print('meetingroom_guide' in tables) # 应为 True051
print('meetingroom_repair' in tables) # 应为 True051
"
```
### Step 2: 环境变量配置
`.env` 中添加:
```bash
# 终端页面基础URL(用于NE2005二维码生成)
TERMINAL_BASE_URL=https://itsupport.servyou.com.cn/itterminal/
```
`docker-compose.yml` 的 backend environment 中已添加:
```yaml
- TERMINAL_BASE_URL=${TERMINAL_BASE_URL:-https://itsupport.servyou.com.cn/itterminal/}
```
重启后端:
```bash
docker compose up -d backend
```
### Step 3: 后端验证
```bash
# 验证会议室 API(已存在)
curl -sk https://localhost/itportal/meetingroom/list
# 验证报修 API(新增)
curl -sk -X POST https://localhost/itportal/meetingroom/repair \
-H "Content-Type: application/json" \
-d '{"terminal_sn":"TEST-SN","meetingroom_id":1,"meetingroom_name":"测试","device_type":"projector","fault_description":"测试报修"}'
# 验证指南 API(新增)
curl -sk https://localhost/itportal/meetingroom/guides
# 验证二维码 API(新增)
curl -sk https://localhost/itportal/meetingroom/terminal/TEST-SN/qrcode -o /tmp/qr.png
file /tmp/qr.png # 应为 PNG image
```
### Step 4: 终端前端构建与部署
```bash
# 1. 本地构建
cd frontend-terminal
npm install # 安装新增的 qrcode 依赖
npm run build
# 2. 打包
tar -czf terminal-dist.tar.gz dist/
# 3. 上传到服务器(通过堡垒机)
python C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py \
upload ./terminal-dist.tar.gz /tmp/terminal-dist.tar.gz
# 4. 服务器解压
cd /opt/wecom-it-desk/frontend-terminal/
rm -rf dist # 删除旧 distbind mount 铁律:rm后重建必须重启容器)
tar -xzf /tmp/terminal-dist.tar.gz
```
### Step 5: Nginx 配置更新
`nginx/nginx.conf` 已更新,新增以下 location(两个 server 块均已添加):
```nginx
# 小鱼终端大屏 — /itterminal/
location /itterminal/ {
alias /usr/share/nginx/html/itterminal/;
index index.html;
try_files $uri /itterminal/index.html;
}
# 会议室 API — /itportal/meetingroom/
# 必须在 /itportal/ 静态文件之前匹配(nginx 最长前缀优先)
location /itportal/meetingroom/ {
proxy_pass http://backend_api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
```
`docker-compose.yml` nginx volumes 已添加:
```yaml
- ./frontend-terminal/dist:/usr/share/nginx/html/itterminal:ro
```
重启 Nginx
```bash
docker compose restart nginx
```
### Step 6: 终端前端验证
```bash
# 验证终端页面可访问
curl -sk https://localhost/itterminal/ | head -5
# 验证 API 代理(通过 nginx 访问后端)
curl -sk https://localhost/itportal/meetingroom/guides
# 验证静态资源
curl -sk -o /dev/null -w "%{http_code}" https://localhost/itterminal/assets/index-*.js
```
---
## 三、小鱼管理后台 H5 应用配置
### 3.1 支持型号确认
| 型号 | H5 应用支持 | 配置方式 |
|------|------------|---------|
| NE90 | ✅ 支持 | 小鱼管理后台配置 H5 应用入口 |
| NE60 | ✅ 支持 | 同上 |
| NE20 | ✅ 支持 | 同上 |
| AE2060 | ✅ 支持 | 同上 |
| ME55S | ⚠️ 部分固件支持 | 需确认固件版本 |
| NE2005 | ❌ 不支持 | 使用二维码降级方案 |
### 3.2 H5 应用入口配置(NE90/NE60/NE20/AE2060
1. 登录**小鱼易联管理后台**`https://mt.xylink.com`
2. 进入 **应用管理 > 自定义应用**
3. 创建新应用:
- **应用名称**: `IT智能服务台`
- **应用类型**: H5 网页应用
- **访问地址**: `https://itsupport.servyou.com.cn/itterminal/{终端SN}/`
- **展示方式**: 终端主界面快捷入口
4. 推送应用到目标终端
5. 在终端上验证 H5 应用可正常打开
### 3.3 终端 SN 绑定
在管理后台(`/itadmin/`)的终端绑定页面中:
1. 添加终端 SN 与会议室的绑定关系
2. 每个终端 SN 对应一个企微会议室 ID
3. 绑定后终端页面自动加载对应会议室状态
### 3.4 NE2005 二维码降级方案
对于不支持 H5 应用的 NE2005 终端:
1. **生成二维码**
```bash
# 通过 API 生成终端访问二维码
curl -sk https://itsupport.servyou.com.cn/itportal/meetingroom/terminal/{SN}/qrcode -o qr.png
```
或直接在浏览器访问:
`https://itsupport.servyou.com.cn/itportal/meetingroom/terminal/{SN}/qrcode`
2. **打印二维码**:将二维码打印为贴纸(建议尺寸 10×10cm)
3. **张贴二维码**:贴在 NE2005 终端的显眼位置
4. **用户使用流程**
- 用户用企业微信/微信扫描二维码
- 手机浏览器打开终端页面(移动端自适应布局)
- 可查看会议室状态、预定、报修、查看指南
---
## 四、部署后验证
### 4.1 功能验证清单
| # | 验证项 | 验证方法 | 预期结果 |
|---|--------|---------|---------|
| 1 | 会议室列表 | `GET /itportal/meetingroom/list` | 返回会议室列表 |
| 2 | 会议室状态 | `GET /itportal/meetingroom/{id}/status` | 返回当前状态 |
| 3 | 预定会议室 | `POST /itportal/meetingroom/book` | 创建预定成功 |
| 4 | 终端绑定 | `GET /itportal/meetingroom/terminal/{sn}/binding` | 返回绑定信息 |
| 5 | 终端WS | `WS /ws/terminal/{sn}` | 终端状态实时推送 |
| 6 | **设备报修** | `POST /itportal/meetingroom/repair` | 创建工单+通知管理员 |
| 7 | **指南列表** | `GET /itportal/meetingroom/guides` | 返回指南列表(5条种子) |
| 8 | **指南按类型** | `GET /itportal/meetingroom/guides/projector` | 返回投影仪指南 |
| 9 | **终端二维码** | `GET /itportal/meetingroom/terminal/{sn}/qrcode` | 返回PNG图片 |
| 10 | 终端前端 | 浏览器打开 `/itterminal/{sn}/` | 深色主题大屏页面 |
| 11 | **报修页面** | 终端点击"设备报修"按钮 | 显示报修表单 |
| 12 | **指南页面** | 终端点击"操作指南"按钮 | 显示指南列表+二维码 |
| 13 | **移动端适配** | 手机访问 `/itterminal/{sn}/` | 垂直布局自适应 |
| 14 | 扫码登录 | 终端扫码 | 企微扫码登录成功 |
| 15 | 状态同步 | 终端修改状态 → H5 实时更新 | WS 推送正常 |
### 4.2 Redis 缓存验证
```bash
# 会议室 token
redis-cli -a $REDIS_PASSWORD get wecom:meetingroom_access_token
# 会议室列表缓存
redis-cli -a $REDIS_PASSWORD get meetingroom:room_list
# 状态缓存
redis-cli -a $REDIS_PASSWORD get "meetingroom:status:{room_id}"
```
---
## 五、回滚方案
### 5.1 数据库回滚
```bash
# 回退迁移 051(报修+指南表)
docker compose exec backend alembic downgrade -1
# 完全回退(含 050
docker compose exec backend alembic downgrade -2
```
### 5.2 后端回滚
```bash
# 恢复 .py 文件(bind mount 自动生效)
git checkout HEAD~1 -- backend/app/api/meetingroom.py
git checkout HEAD~1 -- backend/app/services/repair_service.py
git checkout HEAD~1 -- backend/app/services/meetingroom_service.py
git checkout HEAD~1 -- backend/app/models/meetingroom_guide.py
git checkout HEAD~1 -- backend/app/models/meetingroom_repair.py
git checkout HEAD~1 -- backend/app/schemas/meetingroom.py
git checkout HEAD~1 -- backend/app/config.py
docker compose restart backend
```
### 5.3 前端回滚
```bash
# 恢复旧 dist
mv /opt/wecom-it-desk/frontend-terminal/dist /opt/wecom-it-desk/frontend-terminal/dist.bak
# 恢复上一版本
docker compose restart nginx
```
### 5.4 Nginx 配置回滚
```bash
# 移除 /itterminal/ 和 /itportal/meetingroom/ location 块
docker compose restart nginx
```
---
## 六、新增功能说明
### 6.1 设备报修流程
```
终端用户点击"设备报修"
→ 选择设备类型(投影仪/视频会议/空调/桌椅/网络/其他)
→ 填写故障描述
→ 提交报修
→ 后端创建:
1. MeetingroomRepair 记录
2. ConversationIT工单会话,状态=排队,紧急度=3)
3. Message(系统消息,含故障描述)
4. 企微消息通知管理员
5. WS广播给在线坐席
→ 终端显示"报修已提交"
→ 3秒后自动返回状态页
```
### 6.2 操作指南双模式
- **终端展示**:指南的 `brief` 字段在终端大屏上直接显示简要操作步骤
- **二维码详情**:指南的 `detail_url` 生成二维码,用户手机扫码查看完整文档
- 管理员可在数据库中添加/修改指南内容
### 6.3 NE2005 降级流程
```
NE2005 终端(不支持H5应用)
→ 管理员生成二维码贴纸(API: /itportal/meetingroom/terminal/{sn}/qrcode
→ 用户手机扫码
→ 手机浏览器打开终端页面(移动端自适应)
→ 功能与终端大屏一致(状态/预定/报修/指南)
```
---
## 七、配置参数速查
| 参数 | 默认值 | 环境变量 |
|------|--------|---------|
| 会议室Token TTL | 6900s | - |
| 会议室列表缓存 | 600s | `MEETINGROOM_CACHE_TTL_ROOMS` |
| 预定缓存 | 30s | `MEETINGROOM_CACHE_TTL_BOOKING` |
| 状态缓存 | 10s | `MEETINGROOM_CACHE_TTL_STATUS` |
| WS重连次数 | 5 | - |
| WS降级轮询间隔 | 10s | - |
| **终端页面URL** | `https://itsupport.servyou.com.cn/itterminal/` | `TERMINAL_BASE_URL` |
| **二维码尺寸** | 300px | API 参数 `size` |
| **二维码缓存** | 1小时 | HTTP `Cache-Control` |
---
## 八、已知限制
1. **企微API时间限制**: 会议室预定查询范围限制 31 天
2. **终端身份**: 管理操作需扫码登录,报修支持匿名提交
3. **WS重连**: 终端断线后自动重连(指数退避),超过 5 次降级为轮询(10s间隔)
4. **NE2005**: 不支持 H5 应用,仅能通过二维码扫码方式使用
5. **指南管理**: 当前通过数据库直接管理,后续可增加管理后台 UI
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
# Token多IP异常检测 - 技术设计文档
> **任务ID**: 待分配
> **模块**: 威胁检测
> **优先级**: P1
> **ATT&CK**: T1078 (有效账户), T1552 (非安全凭据)
---
## 1. 需求概述
### 1.1 业务背景
当前系统已废弃密码登录,仅支持企微OAuth2/扫码登录。Token是用户身份的唯一凭证,当Token被泄露后,攻击者可能从不同IP使用同一Token访问系统。本功能旨在检测此类异常行为。
### 1.2 功能目标
- 记录每个Token使用的IP地址
- 检测同一Token在短时间内被多个IP使用的情况
- 触发告警通知安全管理员
- 可选:自动禁用异常Token
---
## 2. 技术方案
### 2.1 架构设计
```
┌─────────────────────────────────────────────────────────┐
│ 检测流程 │
├─────────────────────────────────────────────────────────┤
│ │
│ 用户API请求 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ record_token_ip │ ← 每次请求记录IP │
│ │ (埋点) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Redis Set │ ← token_ips:{hash} │
│ │ IP集合(1h TTL) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ 定时任务(每分钟) │
│ ┌─────────────────┐ │
│ │ detect_anomaly │ ← 扫描异常Token │
│ │ (定时任务) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ send_alert │ ← 企微机器人告警 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### 2.2 数据结构
#### Redis Key设计
| Key格式 | 类型 | TTL | 说明 |
|---------|------|-----|------|
| `token_ips:{token_hash}` | Set | 3600秒 | 记录Token使用的IP集合 |
| `token_ips:alerted:{token_hash}` | String | 3600秒 | 已告警标记,避免重复 |
#### Token存储(现有)
| Key格式 | 类型 | 说明 |
|---------|------|------|
| `user:token:{token}` | JSON | 用户信息,含employee_id |
### 2.3 接口设计
#### 2.3.1 记录Token使用IP (埋点)
```python
# 在 token_service.py 中增加
async def record_token_ip(token: str, ip: str):
"""
记录Token使用的IP地址
Args:
token: 用户Token
ip: 客户端IP (X-Forwarded-For 或 request.client.host)
"""
import hashlib
token_hash = hashlib.sha256(token.encode()).hexdigest()
redis = await get_redis()
key = f"token_ips:{token_hash}"
# 添加IP到Set (自动去重)
redis.sadd(key, ip)
# 设置1小时过期
redis.expire(key, 3600)
```
#### 2.3.2 异常检测定时任务
```python
# 在 tasks/token_anomaly_detection.py
async def detect_token_anomaly():
"""
检测Token异常使用
扫描所有 token_ips:* keys
当 IP数量 >= 阈值 时触发告警
"""
# 配置
THRESHOLD = 3 # IP数量阈值
WINDOW_SECONDS = 3600 # 时间窗口
redis = await get_redis()
alerted_key_prefix = "token_ips:alerted:"
# 扫描所有 token_ips:* keys
async for key in redis.scan_iter("token_ips:*"):
# 跳过 alerted keys
if key.startswith(alerted_key_prefix):
continue
token_hash = key.replace("token_ips:", "")
ip_count = await redis.scard(key)
if ip_count >= THRESHOLD:
# 检查是否已告警
alerted_key = f"{alerted_key_prefix}{token_hash}"
if await redis.get(alerted_key):
continue # 已告警,跳过
# 获取用户信息
token = await redis.get(f"user:token:{token_hash}")
if token:
user_data = json.loads(token)
employee_id = user_data.get("employee_id")
# 发送告警
await send_security_alert(
title="Token异常告警",
content=f"员工 {employee_id} 的Token被 {ip_count} 个IP使用\nToken: {token_hash[:8]}..."
)
# 标记已告警
await redis.setex(alerted_key, WINDOW_SECONDS, "1")
```
#### 2.3.3 获取客户端IP
```python
def get_client_ip(request) -> str:
"""获取客户端真实IP"""
# 优先从 X-Forwarded-For 获取
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
# 降级到 request.client.host
return request.client.host if request.client else ""
```
---
## 3. 配置项
### 3.1 环境变量
| 变量名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `TOKEN_ANOMALY_THRESHOLD` | int | 3 | 触发告警的IP数量阈值 |
| `TOKEN_ANOMALY_WINDOW` | int | 3600 | 时间窗口(秒) |
| `TOKEN_ANOMALY_AUTO_DISABLE` | bool | false | 是否自动禁用Token |
| `CONTENT_AUDIT_WEBHOOK` | string | - | 企微机器人webhook(现有) |
---
## 4. 告警内容
### 4.1 告警模板
```json
{
"msgtype": "markdown",
"markdown": {
"content": "🔴 **Token异常告警**\n\n"
"> 员工ID: {employee_id}\n"
"> 异常Token: {token_hash[:8]}...\n"
"> IP数量: {ip_count}\n"
"> 时间: {timestamp}\n\n"
"> **请及时确认是否为本人操作**"
}
}
```
---
## 5. 集成点
### 5.1 现有组件复用
| 组件 | 用途 |
|------|------|
| Redis | IP存储 |
| APScheduler | 定时任务 |
| content_audit_webhook | 企微告警 |
| token_service.py | Token管理 |
### 5.2 侵入点
| 文件 | 修改内容 |
|------|----------|
| `app/services/token_service.py` | 增加 record_token_ip() |
| `app/main.py` | 注册定时任务 |
| `app/tasks/token_anomaly_detection.py` | 新建检测任务 |
---
## 6. 性能与容错
### 6.1 性能估算
| 指标 | 估算值 |
|------|---------|
| Redis存储 | ~50KB (1000活跃Token) |
| 定时任务耗时 | < 100ms |
| 定时任务间隔 | 60秒 |
### 6.2 容错设计
- 告警发送失败:记录日志,不阻塞主流程
- Redis连接失败:跳过本次检测,下个周期重试
- Token不存在:跳过,不影响其他检测
---
## 7. 测试用例
### 7.1 单元测试
| 用例ID | 描述 | 预期结果 |
|--------|------|----------|
| T001 | 单IP使用Token | 不触发告警 |
| T002 | 3个IP使用Token | 触发告警 |
| T003 | 5个IP使用Token | 触发告警(严重) |
| T004 | 同一IP多次使用 | 不触发告警 |
### 7.2 集成测试
| 用例ID | 描述 | 预期结果 |
|--------|------|----------|
| I001 | 真实Token请求 | IP被记录 |
| I002 | 定时任务执行 | 异常Token被检测 |
| I003 | 告警发送 | 企微收到消息 |
---
## 8. 部署清单
### 8.1 文件变更
| 操作 | 文件 |
|------|------|
| 新增 | `app/tasks/token_anomaly_detection.py` |
| 修改 | `app/services/token_service.py` |
| 修改 | `app/main.py` |
### 8.2 配置变更
| 操作 | 变量 |
|------|------|
| 新增(可选) | `TOKEN_ANOMALY_THRESHOLD` |
| 新增(可选) | `TOKEN_ANOMALY_AUTO_DISABLE` |
---
## 9. 回滚方案
如需回滚:
1. 移除定时任务注册 (main.py)
2. 删除 record_token_ip() 调用
3. Redis keys 会在1小时后自动过期
---
> **编制人**: 威胁检测工程师
> **日期**: 2026-07-14
> **审核人**: 待定
@@ -0,0 +1,258 @@
# 智能IT支持服务台 — 项目迁移文档
**生成时间**2026-06-06
**来源项目**`C:\Users\simon\wecom_it_smart_desk`
**原型文件**`C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html`
---
## 一、项目概览
| 项目 | 路径 | 技术栈 |
|------|------|---------|
| 后端 | `C:\Users\simon\wecom_it_smart_desk\backend` | Python 3.12 + FastAPI + SQLAlchemy |
| 前端(坐席工作台) | `C:\Users\simon\wecom_it_smart_desk\frontend-agent` | Vue 3 + TypeScript + Vite + Element Plus + Pinia |
| 原型 HTML | `C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html` | 单文件,v5.3 定版 |
| 服务器 | `10.80.0.129` | Docker Compose (postgres + redis + backend + nginx) |
---
## 二、锁定的设计决策(迁移后必须遵守)
### 2.1 原型规范
-**原型 v5.3 已锁定**`agent-workspace-v5_3.html`),调整样式前必须与用户确认
- ✅ 用户偏好**深色科技风 UI**,原型**必须使用中文**
- ✅ 所有修改基于现有面板内调整,**不另开新页面**
- ✅ 聊天区域应缩小,给侧栏更多空间
### 2.2 布局架构(三栏)
```
┌──────────┬────────────────────────┬──────────────┐
│ 左栏 │ 中栏 │ 右栏 │
│ (240px) │ (flex:1 自适应) │ (320px) │
│ │ │ │
│ 会话列表 │ UserInfoBar │ AI 智能推荐 │
│ 搜索框 │ TroubleshootBar │ 快速回复 │
│ 待办事项 │ 消息列表 │ (三层导航) │
│ │ ReplyBox (输入框) │ │
└──────────┴────────────────────────┴──────────────┘
```
### 2.3 关键交互规则
| 功能 | 规则 |
|------|------|
| 排查步骤栏 | 始终可见(不可收起),位于 UserInfoBar 下方、消息列表上方 |
| 全流程图 | 默认收起,通过 `▶` / `▼` 三角图标切换 |
| 用户信息栏展开箭头 | 收起 `▶`(向右)→ 展开 `▼`(向下,CSS `rotate(90deg)` |
| AI 推荐 | **仅在右边栏**,不在中间栏内联显示 |
| 快速回复 | 三层渐进导航:L1(7列grid) → L2(chip流式) → L3(列表) |
| 主题切换 | 浅色(`:root`) / 深色(`[data-theme="dark"]`) 双主题 |
| 输入框 | `resize: vertical` 手动拖拽 + `autosize` 最大 8 行 + `max-height: 200px` |
---
## 三、原型 v5.3 布局细节(已确认的终版)
> **文件**`C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html`
### 3.1 中间栏(center-columnDOM 顺序
```html
<div class="center-column">
<div class="chat-view"> <!-- 整个聊天视图容器 -->
<div class="user-info-bar"> <!-- 用户信息栏(含6卡片展开详情) -->
<div class="user-detail-panel"> <!-- 展开详情(默认收起) -->
<div class="troubleshoot-bar" id="tsBar"> <!-- 排查步骤(紧跟用户信息栏) -->
<div class="ts-header"> <!-- 标题行:[🔧 排查步骤] [①→②→③→④→⑤] [▶] -->
<div class="chat-messages"> <!-- 消息列表 -->
<div class="chat-input-area"> <!-- 输入框区域 -->
<textarea autosize maxRows=8 resize=vertical>
</div> <!-- chat-view 闭合标签 -->
<div class="sidebar-right"> <!-- 右边栏(AI推荐 + 快速回复) -->
</div>
```
### 3.2 排查步骤栏(ts-header 单行布局)
```html
<div class="ts-header">
<span class="ts-title">🔧 排查步骤</span>
<el-select>...</el-select> <!-- 模板选择下拉 -->
<div class="ts-path-inline"> <!-- 内联路径步骤 -->
<span class="path-step-inline done">① 确认版本</span>
<span class="path-arrow-inline"></span>
...
</div>
<span class="ts-flowchart-toggle"></span> <!-- 三角切换图标 -->
</div>
```
### 3.3 用户信息栏展开箭头
```css
.user-info-bar.expanded .expand-icon {
transform: rotate(90deg); /* ▶ 旋转后变成 ▼ */
}
```
```html
<span class="expand-icon"></span> <!-- 收起时向右,展开时向下 -->
```
---
## 四、Vue 3 项目 — 已修改文件清单
### 4.1 核心视图
| 文件 | 修改内容 |
|------|---------|
| `src/views/Workspace.vue` | `onMounted` 添加 `fetchConversations()` + 自动选第一个会话;`assistantVisible` 默认 `true` |
| `src/components/chat/ChatArea.vue` | 移除 `AiRecommendInline``TroubleshootBar` 位置调整到 `UserInfoBar` 下方;移除重复的第一个 `TroubleshootBar` |
### 4.2 聊天组件
| 文件 | 修改内容 |
|------|---------|
| `src/components/chat/UserInfoBar.vue` | 展开箭头 `▶``▼`rotate 90deg),之前方向反了 |
| `src/components/chat/TroubleshootBar.vue` | 路径步骤合并到标题行;展开按钮简化为三角 `▶`/`▼` |
| `src/components/chat/ReplyBox.vue` | `autosize` 上限 4→8 行;`resize: none``resize: vertical`;支持手动拖拽 |
| `src/components/chat/AiRecommendInline.vue` | 已从 `ChatArea.vue` 移除引用(AI推荐仅保留右边栏) |
### 4.3 右边栏组件
| 文件 | 修改内容 |
|------|---------|
| `src/components/assistant/AiAssistantPanel.vue` | 右边栏主容器(AI推荐 + 快速回复) |
| `src/components/assistant/QuickReplyPanel.vue` | 三层渐进导航,L1 七类,数据源 `src/data/qrData.ts` |
| `src/components/assistant/RiskAlert.vue` | 风险告警组件 |
| `src/components/assistant/OperationSteps.vue` | 操作步骤组件 |
### 4.4 状态管理(Stores
| 文件 | 修改内容 |
|------|---------|
| `src/stores/conversation.ts` | `fetchConversations()` DEV 环境 Mock 兜底;自动选中第一个会话 |
| `src/stores/agent.ts` | `initAuth()` 检查 localStorage token |
| `src/stores/todo.ts` | 待办事项 Mock 数据 |
| `src/composables/useWebSocket.ts` | WebSocket 直连后端 `ws://localhost:8000/ws/{agentId}` |
### 4.5 样式
| 文件 | 修改内容 |
|------|---------|
| `src/styles/global.css` | `.reply-box .el-textarea__inner { max-height: 200px; }``.message-list-scroll { flex: 1; overflow-y: auto; }` |
### 4.6 数据
| 文件 | 说明 |
|------|------|
| `src/data/qrData.ts` | 快速回复 180 条(7大类 × 28子类),TypeScript 类型化 |
---
## 五、后端关键修改(参考)
> 路径:`C:\Users\simon\wecom_it_smart_desk\backend`
| 文件 | 修改内容 |
|------|---------|
| `app/services/session_service.py` | `get_conversations()` 排序改为 Python 侧(SQLite 不支持 JSONB 操作符) |
| `app/main.py` | 添加 `dify_conversation_id` 列(PostgreSQL |
| `app/models/todo_item.py` | TodoItem 模型 |
| `app/models/troubleshooting_template.py` | TroubleshootingTemplate 模型 |
---
## 六、已修复的关键 Bug(迁移后注意)
### Bug 1:会话列表不显示 / 页面空白
**根因**`Workspace.vue` `onMounted` 未调用 `fetchConversations()``currentConversation` 始终为 `null``ChatArea` 不渲染。
**修复**`onMounted` 中添加 `await conversationStore.fetchConversations()` + 自动选中第一个会话。
### Bug 2:AI 推荐同时出现在中间栏和右边栏
**根因**`ChatArea.vue` 中引入了 `<AiRecommendInline />` 组件。
**修复**:移除 `ChatArea.vue` 中的 `AiRecommendInline` 模板、import、ref、`onAiRecommend` 快捷键绑定。
### Bug 3:原型 HTML 右边栏显示在中栏内部
**根因**`<div class="chat-view">` 缺少 `</div>` 闭合标签,浏览器把 `sidebar-right` 解析为 `center-column` 的子元素。
**修复**:在 `chat-input-area` 关闭后补 `</div>` 闭合 `chat-view`
### Bug 4:排查步骤栏出现两个(上下重复)
**根因**`ChatArea.vue` 模板里有两个 `<TroubleshootBar />`(一个在 UserInfoBar 下方,一个在 ReplyBox 下方)。
**修复**:删除 ReplyBox 下方的重复 `<TroubleshootBar />`
### Bug 5:用户信息栏展开箭头方向反了
**根因**:收起时 `▼`,展开时 `▲`CSS `rotate(180deg)`)。
**修复**:收起 `▶`,展开 `▼`CSS `rotate(90deg)`)。
### Bug 6:会话列表 API 500 错误
**根因**`session_service.py` 用 SQL 侧 `case()` + `tags["hand_raise"].as_boolean()` 排序,SQLite 不支持 JSONB 操作符。
**修复**:排序改为 Python 侧 `_sort_key()` 函数实现。
---
## 七、服务器部署状态
| 项目 | 状态 |
|------|------|
| 服务器地址 | `10.80.0.129` |
| Docker Compose 容器 | `postgres`, `redis`, `backend`, `nginx` |
| nginx | 已修复 301 重定向和 API 双重前缀问题 |
| PostgreSQL | 已添加 `dify_conversation_id` 列 |
| Redis | `it-desk-redis` 容器中运行中 |
---
## 八、知识库
| 项目 | 状态 |
|------|------|
| 数据源 | `IT支持知识库2026-4-24.docx` |
| 快速回复条数 | 180 条(7大类 × 28子类) |
| 前端数据文件 | `frontend-agent/src/data/qrData.ts` |
| 原型数据文件 | `agent-workspace-v5_3.html` 内联(因 file:// CORS 限制) |
---
## 九、迁移 checklist
### 备份这些文件/目录:
```
# 原型
C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html
# 前端项目(整体复制)
C:\Users\simon\wecom_it_smart_desk\frontend-agent\
# 后端项目(整体复制)
C:\Users\simon\wecom_it_smart_desk\backend\
# 项目记忆(WorkBuddy
C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\.workbuddy\memory\
```
### 新项目中需要重新配置:
- [ ] `.env` 文件(数据库连接、Redis、Dify API Key
- [ ] `vite.config.ts` 中的 proxy 端口(当前 `5174`
- [ ] Docker Compose 环境变量
- [ ] 服务器部署配置(`10.80.0.129`
### 新项目中需要重新执行:
```bash
# 后端
cd backend
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
# 初始化数据库
alembic upgrade head # 或 python -m app.db.init_db
# 前端
cd frontend-agent
npm install
npm run dev # → http://localhost:5174/itagent/workspace
```
---
## 十、待完成任务(迁移后继续)
- [ ] 重启 dev server 验证所有修改已生效(当前修改已保存,需 Ctrl+C → npm run dev
- [ ] 后端 AI 集成(Dify)代码修复(T02 任务)
- [ ] 前端方案评估(T03/T04
- [ ] 企微回调服务器对接
- [ ] 生产环境部署验证
---
*此文档由 WorkBuddy 自动生成,汇总了 2026-05-21 至 2026-06-06 的所有工作内容。*
@@ -0,0 +1,531 @@
# 智能IT支持服务台 — 新服务器部署手册
> **目标服务器**`10.90.5.110`(公司内网,**2026-06-15 起替代 10.80.0.136**
> **域名**`itsupport.servyou.com.cn`
> **访问方式**:通过堡垒机 `10.212.189.210:2222`(用户 `sxn`OTP 动态口令认证)
> **Docker**:已安装
> **部署方式**Docker Compose4容器:nginx + backend + postgres + redis
---
## 一、前置条件检查清单
| 条件 | 状态 | 验证命令 |
|------|------|---------|
| Linux 服务器 10.90.5.110(替代旧 10.80.0.136) | ✅ 已确认 | 2026-06-15 起使用 |
| Docker 已安装 | ✅ 已确认 | `docker --version` |
| Docker Compose V2 | 待确认 | `docker compose version` |
| 端口 80 未被占用 | 待确认 | `ss -tlnp \| grep :80` |
| DNS 解析 | 待配置 | `nslookup itsupport.servyou.com.cn` |
| 堡垒机可访问 | 待确认 | `ssh -p 2222 user@10.212.189.210` |
---
## 二、SSH 通过堡垒机连接
### 2.1 什么是堡垒机?
堡垒机(跳板机)是公司内网的安全访问入口。你不能直接 SSH 到目标服务器,必须先登录堡垒机,再从堡垒机跳转到目标服务器。OTP(One-Time Password)是指每次登录需要输入动态验证码(通常来自手机令牌 App)。
### 2.2 连接方式
**PuTTY 客户端(用户实际使用)**:
- 打开 PuTTY
- Host Name(IP 地址):`10.212.189.210`
- Port:`2222`
- Connection type:SSH
- Saved Sessions:起名(如 `wecom-bastion`)→ Save
- 点 Open
- 用户 `sxn` + 密码
- **堡垒机内再跳目标机**:
```bash
ssh sxn@10.90.5.110
```
> **OpenSSH `ssh -J` 方式不再使用**(用户已确认用 PuTTY,2026-06-15)
### 2.3 jumpserver-V2 工具(推荐)
推荐使用 jumpserver-V2 工具进行远程命令执行和文件传输。该工具一次浏览器登录后,后续通过登录缓存 cookies 免登录复用。
```bash
# 进入 skill 目录
cd C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts
# 首次登录(弹出浏览器,手动填写 OTP)
python v2_ops.py login
# 之后免登录执行命令
python v2_ops.py exec "hostname"
python v2_ops.py exec "uptime && docker ps"
# 文件上传(自动选择最优方式:<100KB 用 base64>=100KB 或 >15s 用 elFinder
python v2_ops.py upload local_file.txt /tmp/remote_file.txt
# 文件下载
python v2_ops.py download /tmp/server_file.txt ./local_file.txt
# 巡检命令(只读)
python v2_ops.py inspect
```
> **注意**:首次使用会弹出浏览器完成 JumpServer 登录和 OTP 验证,后续调用会自动复用会话(服务端 401 时自动回退重新登录)。
### 2.4 为什么不能直接用 SSH/SCP
| 方式 | 支持情况 | 原因 |
|------|---------|------|
| SSH ProxyJump | ❌ 不支持 | JumpServer 不兼容标准 SSH 代理协议 |
| SCP 直连堡垒机 | ❌ 不支持 | 需要 OTP 验证码,SCP 不支持交互式输入 |
| SSH 直连目标服务器 | ❌ 不支持 | 目标服务器仅对 JumpServer 开放 SSH 访问 |
| jumpserver-V2 | ✅ 推荐 | 自动化处理 OTP 和 Connection Token |
---
## 三、文件传输(通过堡垒机)
### 3.1 jumpserver-V2 上传(推荐)
```bash
# 上传文件到目标服务器 /tmp 目录
python v2_ops.py upload deploy.zip /tmp/deploy.zip
# 上传到其他目录
python v2_ops.py upload config.conf /opt/wecom-it-desk/config.conf
```
### 3.2 备用方案
如果 jumpserver-V2 不可用,可以考虑:
```bash
# 方式A:通过互联网可访问的存储服务(推荐)
# - 先把文件传到能通过互联网访问的存储(如:对象存储、临时文件分享服务)
# - 目标服务器通过 curl/wget 下载
# 方式B:通过堡垒机手动中转
# - 使用 jumpserver-V2 手动执行分步操作
```
---
## 四、部署步骤(完整流程)
### 步骤 1:在开发机上构建前端并打包
```bash
# 在开发机(Windows)上,进入项目根目录
cd D:\资料\03-项目开发\wecom_it_smart_desk
# 方法A:使用打包脚本(自动构建前端 + 组装 + 打包)
bash deploy-server/package.sh
# 方法B:手动构建
# H5 员工端
cd frontend-h5
npm install && npm run build
# 坐席工作台
cd ../frontend-agent
npm install && npm run build
# 手动打包(如果不用 package.sh
# 需要把 frontend-h5/dist/、frontend-agent/dist/、backend/、deploy-server/ 下的配置文件一起打包
```
打包完成后,项目根目录下会生成 `it-smart-desk-server-deploy.zip`。
### 步骤 2:上传部署包到服务器
```bash
# 在开发机上执行
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
it-smart-desk-server-deploy.zip \
sxn@10.90.5.110:/tmp/
```
> 上传到 `/tmp/` 而非 `/opt/`,因为普通用户对 `/opt/` 没有写权限
### 步骤 3:登录服务器并解压
**PuTTY 登录**(见 §2.2):
- Host:`10.212.189.210`,Port:`2222`,SSH
- 堡垒机内再 `ssh sxn@10.90.5.110`
```bash
# 切换 root(普通用户对 /opt 无写权限)
sudo -i
# 移动并解压部署包
mv /tmp/it-smart-desk-server-deploy.zip /opt/
cd /opt
unzip it-smart-desk-server-deploy.zip
# 重命名目录为更简短的名称
mv it-smart-desk-server-deploy wecom-it-desk
cd wecom-it-desk
```
### 步骤 4:配置环境变量
```bash
cd /opt/wecom-it-desk
# 从模板创建 .env
cp .env.example .env
# 编辑 .env
vi .env
```
**阶段一(Mock 模式)最小配置** — 只需确认以下默认值:
```ini
# 数据库密码(默认即可,首次初始化后不可更改)
POSTGRES_PASSWORD=wecom_secret_2024
# 企微配置(阶段一 Mock 模式可以留空)
WECOM_CORP_ID=
WECOM_AGENT_ID=1000002
WECOM_SECRET=
WECOM_TOKEN=
WECOM_ENCODING_AES_KEY=
# Mock 登录(阶段一设为 true
MOCK_LOGIN_ENABLED=true
# Dify AI(暂时可以留空)
DIFY_API_URL=http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions
DIFY_API_KEY=
```
> **重要**`POSTGRES_PASSWORD` 首次启动时写入数据库,之后修改 `.env` 不会生效。如需修改密码,必须删除数据卷重建。
### 步骤 5:部署
```bash
cd /opt/wecom-it-desk
# 添加执行权限
chmod +x deploy.sh
# 执行部署
./deploy.sh
```
脚本会自动:
1. ✅ 检查 Docker 环境
2. ✅ 检查 .env 配置
3. ✅ 检查前端文件
4. ✅ 构建后端 Docker 镜像
5. ✅ 启动 4 个容器
6. ✅ 等待服务就绪
7. ✅ 验证部署
### 步骤 6:验证部署
```bash
# 在服务器上验证
curl http://localhost/api/health
# 应返回 {"status":"healthy"}
curl http://localhost/itdesk/
# 应返回 H5 前端 HTML
# 查看所有容器状态
docker compose ps
# 应显示 4 个容器都是 Up 状态
# 如果有容器未启动,查看日志
docker compose logs --tail 50 backend
docker compose logs --tail 50 postgres
```
### 步骤 7:配置 DNS
需要联系公司 IT 运维,在公司 DNS 上添加 A 记录:
```
itsupport.servyou.com.cn A 10.90.5.110
```
**DNS 未生效前**,可以通过本地 hosts 文件测试:
```
# Windows: C:\Windows\System32\drivers\etc\hosts
# macOS/Linux: /etc/hosts
# 添加一行:
10.90.5.110 itsupport.servyou.com.cn
```
> 注意:修改 hosts 文件后,浏览器可能有 DNS 缓存。Chrome 可访问 `chrome://net-internals/#dns` 清除缓存,或用无痕窗口测试。
### 步骤 8:浏览器验证
DNS 生效后(或配置了本地 hosts),在浏览器中访问:
| 页面 | URL | 预期结果 |
|------|-----|---------|
| H5 员工端 | `http://itsupport.servyou.com.cn/itdesk/` | 看到登录页面 |
| 坐席工作台 | `http://itsupport.servyou.com.cn/itagent/` | 看到坐席工作台 |
| API 健康检查 | `http://itsupport.servyou.com.cn/api/health` | `{"status":"healthy"}` |
**Mock 登录测试**
1. 访问 `http://itsupport.servyou.com.cn/itdesk/login`
2. 输入任意工号和姓名(如 `test001` / `测试用户`
3. 应成功登录并进入聊天页面
---
## 五、部署文件结构
### 5.1 服务器目录结构
```
/opt/wecom-it-desk/ # 项目根目录(服务器)
├── docker-compose.yml # Docker Compose 配置(4容器)
├── .env # 环境变量(已配置)
├── .env.example # 环境变量模板
├── deploy.sh # 一键部署脚本
├── nginx/
│ ├── nginx.conf # Nginx 配置(反代 + 静态文件)
│ └── ssl/ # SSL 证书
├── html/ # 前端静态文件(Nginx 挂载点)
│ ├── itdesk/ # H5 员工端 (/itdesk/)
│ ├── itagent/ # 坐席工作台 (/itagent/)
│ ├── itadmin/ # 管理后台 (/itadmin/)
│ └── itportal/ # 统一入口 (/itportal/)
└── backend/ # 后端源码(不用于生产,仅开发参考)
```
### 5.2 前端部署位置说明
| 端 | URL 路径 | 服务器目录 | Nginx 容器挂载点 |
|----|---------|-----------|----------------|
| 员工端 H5 | `/itdesk/` | `/opt/wecom-it-desk/html/itdesk/` | `/usr/share/nginx/html/itdesk` |
| 坐席工作台 | `/itagent/` | `/opt/wecom-it-desk/html/itagent/` | `/usr/share/nginx/html/itagent` |
| 管理后台 | `/itadmin/` | `/opt/wecom-it-desk/html/itadmin/` | `/usr/share/nginx/html/itadmin` |
| 统一入口 | `/itportal/` | `/opt/wecom-it-desk/html/itportal/` | `/usr/share/nginx/html/itportal` |
### 5.3 前端部署步骤
```bash
# 1. 本地构建
cd frontend-agent
npm run build
# 2. 打包(排除 node_modules
cd dist
zip -r ../agent-v1.x.zip *
# 3. 上传到服务器 /tmp/
# 4. SSH 到服务器解压
sudo rm -rf /opt/wecom-it-desk/html/itagent
sudo mkdir -p /opt/wecom-it-desk/html/itagent
sudo unzip -o /tmp/agent-v1.x.zip -d /opt/wecom-it-desk/html/itagent
# 5. 重启 Nginx 容器
docker restart wecom_it_nginx
```
---
## 六、Python 依赖管理
### 6.1 依赖说明
后端 Python 依赖在 `backend/requirements.txt` 中声明,构建 Docker 镜像时会自动安装。
**新增依赖处理流程:**
1. **开发环境**:更新 `backend/requirements.txt`
2. **打包部署**:确保 `requirements.txt` 已包含新依赖
3. **生产环境**:重建后端镜像
```bash
# 重建后端镜像(会自动安装 requirements.txt 中的所有依赖)
cd /opt/wecom-it-desk
docker compose build backend
docker compose up -d backend
```
### 6.2 常见依赖问题
| 问题 | 症状 | 解决方法 |
|------|------|---------|
| 缺少依赖 | `ModuleNotFoundError` | 重建后端镜像:`docker compose build backend` |
| 手动安装 | 容器内临时安装 | `docker exec wecom_it_backend pip install <package>` |
---
## 七、常用运维命令
在服务器上 `/opt/wecom-it-desk` 目录下执行:
| 操作 | 命令 |
|------|------|
| 查看服务状态 | `./deploy.sh status` |
| 查看后端日志 | `./deploy.sh logs` |
| 停止所有服务 | `./deploy.sh stop` |
| 重新构建后端 | `./deploy.sh rebuild` |
| 重置数据库 | `./deploy.sh reset-db` |
| 手动启动 | `docker compose up -d` |
| 手动停止 | `docker compose down` |
| 只重启后端 | `docker compose restart backend` |
| 查看数据库 | `docker exec -it wecom_it_postgres psql -U wecom -d wecom_it_desk` |
| 查看 Redis | `docker exec -it wecom_it_redis redis-cli` |
| 重载 Nginx | `docker exec wecom_it_nginx nginx -s reload` |
| 查看容器日志 | `docker compose logs --tail 50 <容器名>` |
---
## 八、升级前端
当有新的前端版本需要部署时:
```bash
# 1. 在开发机上构建新版本
cd frontend-h5 && npm run build
cd frontend-agent && npm run build
# 2. 上传到服务器(通过堡垒机)
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r frontend-h5/dist/ \
sxn@10.90.5.110:/opt/wecom-it-desk/frontend-h5/dist/
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r frontend-agent/dist/ \
sxn@10.90.5.110:/opt/wecom-it-desk/frontend-agent/dist/
# 3. 重载 Nginx(不需要重启整个服务)
ssh itdesk # 如果已配置 SSH 快捷方式
cd /opt/wecom-it-desk
docker exec wecom_it_nginx nginx -s reload
```
---
## 九、升级后端
```bash
# 1. 上传新代码到服务器
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r backend/ \
sxn@10.90.5.110:/opt/wecom-it-desk/backend/
# 2. 重新构建并启动
ssh itdesk
cd /opt/wecom-it-desk
./deploy.sh rebuild
```
---
## 十、故障排查
### 后端容器一直重启
```bash
# 1. 查看容器状态
docker compose ps
# 2. 查看后端日志(最常见原因:数据库连接失败)
docker compose logs --tail 100 backend
# 3. 检查 PostgreSQL 是否健康
docker exec wecom_it_postgres pg_isready -U wecom -d wecom_it_desk
# 4. 检查 Redis 是否健康
docker exec wecom_it_redis redis-cli ping
```
### PostgreSQL 密码错误
```bash
# ⚠️ 这会清空所有数据!只有首次部署密码错误时才需要
docker compose down
docker volume rm wecom-it-desk_postgres_data
docker compose up -d
```
### H5/坐席端白屏
```bash
# 检查前端文件是否存在
docker exec wecom_it_nginx ls /usr/share/nginx/html/itdesk/
docker exec wecom_it_nginx ls /usr/share/nginx/html/itagent/
# 检查 index.html 中的 base 路径是否正确
docker exec wecom_it_nginx cat /usr/share/nginx/html/itdesk/index.html | grep /itdesk/
docker exec wecom_it_nginx cat /usr/share/nginx/html/itagent/index.html | grep /itagent/
```
### DNS 未生效
```bash
# 在服务器上验证
nslookup itsupport.servyou.com.cn
# 如果 DNS 未配置,临时用 IP 直接访问
curl http://10.90.5.110/itdesk/
curl http://10.90.5.110/api/health
```
### Mock 登录返回 401
```bash
# 1. 确认 .env 中 MOCK_LOGIN_ENABLED=true
cat /opt/wecom-it-desk/.env | grep MOCK
# 2. 检查后端日志
docker compose logs --tail 50 backend | grep mock
# 3. 直接测试 mock-login 接口
curl -X POST http://localhost/api/h5/mock-login \
-H "Content-Type: application/json" \
-d '{"employee_id":"test001","employee_name":"测试用户"}'
```
---
## 十一、HTTPS 配置(可选)
如果公司要求 HTTPS,有两种方式:
### 方式一:公司统一 SSL 终端(推荐)
```
客户端 → HTTPS → 公司SSL终端(F5/网关,公网 115.236.188.3) → HTTP → 10.90.5.110:80
```
不需要在本服务器上配置证书。联系运维配置 SSL 终端即可。
### 方式二:本机 SSL
编辑 `nginx/nginx.conf`,取消 HTTPS server 块注释,配置证书路径。
---
## 十二、部署说明
> ⚠️ NAS部署方案(itdesk.amanzac.com)已于2026年6月15日下线,现统一使用公司内网服务器部署。
| 维度 | NAS 部署(已下线) | 当前服务器部署(10.90.5.110 |
|------|---------------------------|-------------------------------|
| 容器数量 | 5个(含 cloudflared | 4个(无 cloudflared |
| 外网访问 | Cloudflare Tunnel | 公司 DNS 直连 |
| 域名 | itdesk.amanzac.com | itsupport.servyou.com.cn |
| SSL | Cloudflare 自动 | 无(内网 HTTP)或公司统一 SSL |
| 数据平台反代 | 需要(共用域名) | 不需要(独立域名) |
| 部署目录 | `/volume1/docker/wecom-it-desk` | `/opt/wecom-it-desk` |
| 文件传输 | File Station / 7z | SCP 通过堡垒机 |
---
## 十三、相关文档
| 文档 | 说明 |
|------|------|
| [堡垒机运维工具](./11-堡垒机运维工具.md) | 通过 JumpServer 自动化执行远程命令、文件上传下载 |
| [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md) | 各版本功能变更记录 |
| [NAS 部署指南](./08-NAS部署指南-预生产.md) | 群晖 NAS 测试环境部署 |
@@ -0,0 +1,100 @@
# 本地 AI 服务部署指南(Dify + RAGFlow
> 更新日期:2026-07-06
## 系统要求
| 服务 | 最低内存 | 推荐内存 |
|------|----------|----------|
| Dify (CPU) | 8GB | 16GB |
| RAGFlow (CPU) | 8GB | 16GB |
| 两者同时 | 16GB | 32GB |
**当前可用内存:约 7.4GB**
---
## 方案一:仅部署 Dify(推荐)
### 步骤1:停止本地不需要的容器
```powershell
# 停止开发环境(如果不需要)
docker stop dev_wecom_backend dev_wecom_postgres dev_wecom_redis
```
### 步骤2:部署 Dify (CPU版)
```powershell
cd D:\资料\03-项目开发\wecom_it_smart_desk
mkdir dify && cd dify
# 下载 Docker Compose
curl -o docker-compose.yml https://github.com/langgenius/dify/raw/main/docker/docker-compose.middleware.yaml
# 启动
docker compose up -d
```
### 步骤3:访问
- Web UI: http://localhost:8080
- API: http://localhost:8081
- 默认管理员: admin@dify.local / admin
---
## 方案二:仅部署 RAGFlowCPU版)
### 步骤1:停止本地不需要的容器
```powershell
docker stop dev_wecom_backend dev_wecom_postgres dev_wecom_redis
```
### 步骤2:部署 RAGFlow
```powershell
cd D:\资料\03-项目开发\wecom_it_smart_desk
mkdir ragflow && cd ragflow
# 下载配置
curl -o docker-compose.yml https://raw.githubusercontent.com/infiniflow/ragflow/main/docker/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/infiniflow/ragflow/main/docker/.env
# 启动(使用 CPU profile
docker compose --profile cpu up -d
```
### 步骤3:访问
- Web UI: http://localhost:9380
- API: http://localhost:9380/api
- 默认管理员: root / infiniflow
---
## 方案三:同时部署(需要16GB+内存)
1. 先停止开发容器
2. 部署 Dify(会占用约 4-6GB
3. 等待稳定后部署 RAGFlow(会占用约 4-6GB
---
## 生产环境已配置
| 服务 | 地址 | 用途 |
|------|------|------|
| Dify 生产 | http://yw-dify.dc.servyou-it.com/ | AI 对话、工作流 |
| RAGFlow 生产 | http://10.80.0.85:8080/ | 知识库管理 |
---
## 本地配置后端连接
修改 `backend/.env.dev`
```bash
# Dify
DIFY_BASE_URL=http://localhost:8081
DIFY_API_KEY=your-api-key
# RAGFlow
RAGFLOW_BASE_URL=http://localhost:9380
RAGFLOW_API_KEY=your-api-key
```
@@ -0,0 +1,40 @@
# 本地 AI 服务部署记录
> 日期: 2026-07-05
## 当前状态
### 拉取中的镜像
| 镜像 | 大小 | 预计时间 |
|------|------|----------|
| ollama/ollama:latest | ~2GB | 5-10分钟 |
| langgenius/dify-api:latest | ~5-10GB | 30-60分钟 |
### 部署方案
#### 方案1: Ollama (轻量)
```bash
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:latest
# 然后运行模型
docker exec ollama ollama run llama3:8b
```
#### 方案2: Dify (完整)
需要完整的 docker-compose,包含:
- dify-api
- dify-web
- dify-worker
- postgres
- redis
- minio
- nginx
## 本地开发环境
| 服务 | 地址 |
|------|------|
| H5 端 | http://localhost:5176/itdesk/ |
| 坐席端 | http://localhost:5175/itagent/ |
| 管理后台 | http://localhost:5175/itadmin/ |
| 后端 API | http://localhost:8000 |
@@ -0,0 +1,136 @@
# 蓝绿部署指南
## 概述
蓝绿部署是一种零停机部署策略,通过维护两套完全相同的运行环境(Blue 和 Green),实现快速切换和回滚。
## 架构
```
┌─────────────────┐
│ Nginx │
│ (流量入口) │
└────────┬────────┘
┌──────────────┴──────────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Blue 环境 │ │ Green 环境 │
│ (当前活动) │ │ (待验证) │
│ backend:8000 │ │ backend_green: │
│ │ │ 5002 │
└────────────────┘ └────────────────┘
│ │
└──────────────┬──────────────┘
┌──────────────┴──────────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ PostgreSQL │ ←──→ │ Redis │
│ (共享) │ │ (共享) │
└────────────────┘ └────────────────┘
```
## 文件说明
| 文件 | 位置 | 说明 |
|------|------|------|
| docker-compose-green.yml | /opt/wecom-it-desk/ | Green 环境配置 |
| switch-blue-green.sh | /opt/wecom-it-desk/ | 切换脚本 |
| nginx.conf | /opt/wecom-it-desk/nginx/ | Nginx 配置(包含 upstream |
## 部署步骤
### 1. 部署 Green 环境
```bash
cd /opt/wecom-it-desk
# 构建并启动 Green 环境
docker-compose -f docker-compose-green.yml up -d
# 验证 Green 环境健康
curl http://localhost:5002/health
```
### 2. 测试 Green 环境
通过端口 5080 访问 Green 环境进行测试:
- H5: http://服务器IP:5080/itdesk/
- 坐席: http://服务器IP:5080/itagent/
- 管理后台: http://服务器IP:5080/itadmin/
### 3. 切换流量到 Green
```bash
# 方法一:使用切换脚本
./switch-blue-green.sh to-green
# 方法二:手动修改 Nginx 配置
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
```
### 4. 验证切换
```bash
# 检查 Nginx upstream 配置
grep -A1 'upstream backend_api' /opt/wecom-it-desk/nginx/nginx.conf
# 测试 API
curl https://itsupport.servyou.com.cn/api/v1/system/health
```
### 5. 回滚(如有问题)
```bash
# 方法一:使用切换脚本
./switch-blue-green.sh to-blue
# 方法二:手动修改
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
```
## 端口说明
| 端口 | 服务 | 说明 |
|------|------|------|
| 80/443 | Nginx (Blue) | 生产入口 |
| 5002 | Backend (Green) | Green 后端 API |
| 5080 | Nginx (Green) | Green 测试入口 |
| 5443 | Nginx (Green) | Green HTTPS |
## 注意事项
1. **数据库共享**Blue 和 Green 共用同一个 PostgreSQL 和 Redis
2. **文件上传**:上传的文件保存在挂载目录,不受切换影响
3. **会话影响**:切换后用户可能需要重新登录
4. **WebSocket**:切换后现有 WebSocket 连接会断开
## 快速命令汇总
```bash
# 查看状态
docker ps
# 查看 Green 日志
docker logs wecom_it_backend_green
# 切换到 Green
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
# 切换回 Blue
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
docker restart wecom_it_nginx
# 停止 Green 环境
docker-compose -f docker-compose-green.yml down
```
## 更新日志
- 2026-07-05: 初始版本