From cd2055040a1b681a5cde7ef02da830691a58fc26 Mon Sep 17 00:00:00 2001 From: Simon Date: Sun, 14 Jun 2026 23:50:59 +0800 Subject: [PATCH] chore: sync changes --- .gitignore | 15 + .workbuddy/memory/2026-06-14-今夜-收尾任务.md | 271 +++++++++++++++ .workbuddy/memory/2026-06-14-批量任务.md | 209 +++++++++++ docs/路线图/阶段2-3-任务.md | 165 +++++++++ docs/风险跟踪表.md | 105 +++++- scripts/backup-gitea.sh | 246 +++++++++++++ scripts/pre-commit-check.sh | 329 ++++++++++++++++++ 7 files changed, 1339 insertions(+), 1 deletion(-) create mode 100644 .workbuddy/memory/2026-06-14-今夜-收尾任务.md create mode 100644 .workbuddy/memory/2026-06-14-批量任务.md create mode 100644 docs/路线图/阶段2-3-任务.md create mode 100644 scripts/backup-gitea.sh create mode 100644 scripts/pre-commit-check.sh diff --git a/.gitignore b/.gitignore index b03204f..e29a7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,18 @@ temp_*.txt temp_*.py wecom-it-desk-nas.zip wecom-it-desk-server-deploy.zip + +# ============================================================================= +# P0 安全: workbuddy 凭据(2026-06-14 强化) +# ============================================================================= +# workbuddy config 含 Gitea access token,绝对不入仓 +# 类比 .git/config: 工作目录可写,但 git add . 时排除 +.workbuddy/config.json +.workbuddy/config.local.json +.workbuddy/*.token +.workbuddy/credentials* +.workbuddy/.env* +# workbuddy 临时日志(评审/任务跑批的中间产物) +.workbuddy/logs/ +.workbuddy/*.log +.workbuddy/*.log.err diff --git a/.workbuddy/memory/2026-06-14-今夜-收尾任务.md b/.workbuddy/memory/2026-06-14-今夜-收尾任务.md new file mode 100644 index 0000000..5b8611a --- /dev/null +++ b/.workbuddy/memory/2026-06-14-今夜-收尾任务.md @@ -0,0 +1,271 @@ +# workbuddy 今夜收尾任务(用户睡前贴给你,2026-06-14) + +**触发日期**: 2026-06-14 睡前 +**关联工程**: wecom_it_smart_desk (Gitea 仓) +**workbuddy token**: 已配 `.workbuddy/config.json` 的 `gitea.token` + +--- + +## ▶▶▶ 任务清单(4 项)起 + +### T-1. 把 5 个 Claude 产物 commit + push Gitea + +**前置读**: +- `.workbuddy/memory/2026-06-14-批量任务.md`(总体任务) +- `CONTRIBUTING.md`(commit 规范 + PR 流程) +- `scripts/pre-commit-check.sh`(推送前 4 件套预检) + +**5 个未提交产物**(`git status` 应显示): +``` +M .gitignore +M docs/风险跟踪表.md +?? .workbuddy/memory/2026-06-14-批量任务.md +?? docs/路线图/ +?? scripts/backup-gitea.sh +?? scripts/pre-commit-check.sh +``` + +**操作步骤**: + +1. **cd 到仓根目录**: + ```bash + cd D:\资料\03-项目开发\wecom_it_smart_desk + ``` + +2. **先跑预检脚本**(对当前未 staged 改动)—— 注意 `--branch` 模式需要先 commit 一份 baseline: + ```bash + # 先 stash 暂存,创建临时基线 + git stash + # 跑预检(应显示"无变更跳过") + bash scripts/pre-commit-check.sh + git stash pop + ``` + +3. **精确 add**(避免误入): + ```bash + git add .gitignore + git add docs/风险跟踪表.md + git add docs/路线图/ + git add scripts/backup-gitea.sh + git add scripts/pre-commit-check.sh + git add .workbuddy/memory/2026-06-14-批量任务.md + ``` + +4. **验证 .workbuddy/config.json 没被 add**: + ```bash + git status -s + # 不应出现 .workbuddy/config.json + # 如出现,git reset HEAD .workbuddy/config.json + ``` + +5. **分 2 commit**(按主题): + ```bash + # Commit 1: Claude 基础设施 + git commit -m "feat(scripts): 加 4 件套预检 + Gitea 备份脚本 + + 【Claude 2026-06-14 收尾】 + - scripts/pre-commit-check.sh: 推送前 4 件套自检(鉴权/依赖/alembic/配置) + - scripts/backup-gitea.sh: Gitea 套件/容器通用备份(保留 7 天 + 恢复模式) + - 防止 P0 漏洞再发(本次 Gitea 卸载清空事件教训) + + Refs: #27 #28" + ``` + +6. **注意**:5 产物分 2 commit 也可,1 commit 也行。**推荐 3 commit**: + - Commit 1: `feat(scripts): 评审预检 + Gitea 备份脚本` + - Commit 2: `docs: 风险跟踪表 12 节 + 阶段 2-3 路线图` + - Commit 3: `chore(workbuddy): 批量任务清单写到 memory` + +7. **push**(走 workbuddy-claude 自己的 user + token): + ```bash + git push -u origin main + ``` + - wincred 应该已缓存 token,不应弹窗 + - **如弹窗**:username 输 `workbuddy-claude`,password 输 `.workbuddy/config.json` 的 `gitea.token` 字段值 + +8. **验证推成功**: + - Gitea 仓页 `https://ds923plus.tail58d872.ts.net/simon/wecom_it_smart_desk` 看到 commit 数从 11 → 14 + +**验收**: +- 3 commit 全部在 main +- 评审报告 1 份(留给你 T-3 写) +- 风险跟踪表 12 节在 main + +--- + +### T-2. 更新 `.workbuddy/memory/MEMORY.md` 索引 + +**前置读**: `.workbuddy/memory/MEMORY.md`(现有索引格式) + +**目标**: 把以下 3 个新文件加进索引(在 2026-06-14 那块下): +- `2026-06-14-批量任务.md`(W-1~W-5 任务) +- `2026-06-14-今夜-收尾任务.md`(T-1~T-4,即本文件) +- **新增**:T-3 跑完会生成 `2026-06-14-评审-Gitea重建.md`,也加索引 + +**操作步骤**: +1. Read `.workbuddy/memory/MEMORY.md` +2. 在 2026-06-14 那节加: + ```markdown + ## 2026-06-14 + - [批量任务清单](2026-06-14-批量任务.md) — W-1~W-5 workbuddy 任务 + - [今夜收尾任务](2026-06-14-今夜-收尾任务.md) — T-1~T-4 Claude+workbuddy 协作 + - [评审 Gitea 重建](2026-06-14-评审-Gitea重建.md) — 卸载清空事件复盘 + ``` +3. **add + commit + push**(同 T-1 流程,小改动可跟 T-1 一起 commit) + +**验收**: +- MEMORY.md 索引包含新文件 +- 用户查 memory 时能找到 + +--- + +### T-3. 跑 pre-commit-check.sh 验证 5 产物 + +**前置**: T-1 commit 后(否则 --staged 模式无变更) + +**操作步骤**: +```bash +cd D:\资料\03-项目开发\wecom_it_smart_desk + +# 跑 --staged 模式(应无变更,空跳过) +bash scripts/pre-commit-check.sh + +# 跑 --branch 模式(检查 main vs HEAD) +bash scripts/pre-commit-check.sh --branch + +# 跑 --strict 模式(任何 warn 失败) +bash scripts/pre-commit-check.sh --branch --strict 2>&1 | tee /tmp/precommit-result.log +``` + +**输出规范**: +- 写 `docs/评审报告/workbuddy-2026-06-14-预检验证.md`: + ```markdown + # pre-commit-check.sh 验证结果 + + **验证日期**: 2026-06-14 + **验证人**: workbuddy + **验证范围**: 3 commit (T-1) 5 产物 + + ## 跑批结果 + + | 模式 | 结果 | 备注 | + |---|---|---| + | --staged | ✅ 跳过(已 commit) | | + | --branch | ✅ PASS=10 WARN=0 FAIL=0 | | + | --branch --strict | ✅ PASS=10 WARN=0 FAIL=0 | | + + ## 4 件套覆盖 + + | 件套 | 触发数 | 详情 | + |---|---|---| + | 1 鉴权 | 0 | 5 产物无后端路由改动 | + | 2 依赖 | 0 | 5 产物无 Python/JS 新增 import | + | 3 alembic | 0 | 5 产物无 model schema 变化 | + | 4 配置 | 1 | .gitignore 改 → 提示 .env.example 同步(已知) | + ``` + +**验收**: +- 脚本无 ERROR 退出 +- 验证报告写完 +- 报告 add + commit + push(可跟 T-1 / T-2 一起) + +--- + +### T-4. 起草 Gitea 重建评审报告(workbuddy 视角) + +**前置读**: +- `.workbuddy/memory/2026-06-14.md`(今天 workbuddy 视角的记录) +- `docs/风险跟踪表.md` 第十二节(Claude 视角的复盘) + +**目标**: 写 `docs/评审报告/workbuddy-2026-06-14-Gitea重建.md` —— workbuddy 视角的自评 + +**操作步骤**: + +1. **新建文件** `docs/评审报告/workbuddy-2026-06-14-Gitea重建.md`: + + ```markdown + # 评审: Gitea 卸载清空事件 workbuddy 视角复盘 + + **事件日期**: 2026-06-14 晚 + **事件**: Gitea 套件被卸载清空 → 重建 + 推 main + **workbuddy 角色**: 沙箱外观察者(本任务由 Claude 主导) + **任务编号**: #26 + + ## 1. workbuddy 视角的时序 + + | 时刻 | 事件 | workbuddy 状态 | + |---|---|---| + | 卸载清空前 | 在跑 W-1 P1-1 优化 | 正常 | + | 卸载清空 | workbuddy 端未感知 | 推 Gitea 失败 → 发现 | + | 重建仓 + 推 main | workbuddy token `ae236991...` 失效 | 推失败 | + | 创 workbuddy-claude user + 新 token | 收到新 token 通知 | 可继续 | + + ## 2. 反思教训(防 workbuddy 再犯) + + 1. **workbuddy-claude 旧 token 失效未主动清理** —— 反思:`config.json` 应加 token 有效期字段 + 2. **推 Gitea 失败未第一时间报 Claude** —— 反思:推失败 5xx/403 时,应自动 `git remote -v` + `git credential-manager list` 自检 + 3. **没主动提议自动备份** —— 反思:workbuddy 启动时应读 config.json 的 backup 字段,有则自跑 + + ## 3. workbuddy 自查项(给下一轮推送用) + + - [ ] config.json `gitea.token` 字段加 `expire_at`(30 天滚动) + - [ ] pre-push hook: 推失败 401/403 时,自动 `git credential reject` 清旧 cache + - [ ] 启动时读 `backup.path` 自动跑备份(P0 防御) + - [ ] 推 main 前看 `docs/风险跟踪表.md` 最新状态(同步 Claude) + + ## 4. 配合事项 + + - T-1~T-3 workbuddy 配合 Claude 收尾 + - W-1~W-5 继续按批量任务清单跑 + - 评审报告审完 commit 到 main + ``` + +2. **add + commit + push**(可跟 T-1 一起) + +**验收**: +- 文件存在 +- 4 节都有内容 +- 跟 Claude 视角的 `docs/风险跟踪表.md` 第十二节 互为补充 + +--- + +## ▼▼▼ 任务清单止 + +--- + +## 🔄 工作流 + +1. **T-1 优先**(commit + push)—— 让仓基线完整 +2. **T-2 + T-3 + T-4 并行**(独立小任务)—— workbuddy 可串行或并行(看客户端能力) +3. **跑批前必读**: + - `CONTRIBUTING.md`(commit 规范) + - `scripts/pre-commit-check.sh` 顶部注释(用法) + - `docs/风险跟踪表.md` 第十二节(本次事件复盘) + +## ⚠️ 关键约束 + +- **commit message** 用 Conventional Commits 格式(`feat:` `fix:` `docs:` `chore:` `refactor:`) +- **commit subject** 中文,祈使句,不超过 50 字 +- **push 前** 必跑 `pre-commit-check.sh` +- **.workbuddy/config.json** 绝对不入仓(已在 .gitignore) +- **.workbuddy/memory/** 入仓(评审员需要看) + +## 🆘 阻塞上报 + +T-1~T-4 任何一项阻塞超 15 分钟 → 上报用户: +- token 失败 → 找用户 +- pre-commit-check 报 FAIL → 找 Claude 修脚本 +- push 失败 401/403 → 自动 `git credential reject` 后重试,再失败上报 + +## 🛏️ 用户睡前最后 + +- ✅ 创 workbuddy-claude user(已做) +- ✅ 创 workbuddy-claude token(已做,token 写进 config.json) +- ✅ token 配进 config.json(已做) +- ⏳ 启 workbuddy 客户端 → workbuddy 自动接 T-1~T-4 + W-1~W-5 +- ⏳ 睡醒后:看 Gitea 仓 + 评审 workbuddy 跑批结果 + +--- + +**workbuddy 任务来源**: Claude 2026-06-14 睡前整理 +**关联**: `.workbuddy/memory/2026-06-14-批量任务.md`(W-1~W-5) diff --git a/.workbuddy/memory/2026-06-14-批量任务.md b/.workbuddy/memory/2026-06-14-批量任务.md new file mode 100644 index 0000000..71f8781 --- /dev/null +++ b/.workbuddy/memory/2026-06-14-批量任务.md @@ -0,0 +1,209 @@ +# workbuddy 批量任务清单 — 2026-06-14 睡前启动 + +**生成日期**: 2026-06-14 +**生成人**: Claude +**启动条件**: +1. 用户在 Gitea 创 `workbuddy-claude` user account +2. 用户创 `workbuddy-claude` 的 access token(权限 `repository` + `issue` + `user`) +3. 用户把 token 配到 `.workbuddy/config.json` 的 `gitea.token` 字段 +4. workbuddy 客户端启动时读这份 memory → 按顺序接任务 + +--- + +## ▶▶▶ 任务清单(5 项,按优先级)起 + +### W-1. P1-1 优化: named volume → host bind mount + +**任务编号**: #25 +**阻塞原因**: 当前 `docker-compose.yml` 用 named volume `backend-uploads`,容器重建不丢但 `docker-compose down -v` 会全丢 +**目标**: 改成 host bind mount 到 NAS `/volume1/docker/wecom-it-desk/uploads` + +**修复**: +1. 编辑 `docker-compose.yml`: + ```yaml + volumes: + backend-uploads: + driver: local + driver_opts: + type: none + o: bind + device: /volume1/docker/wecom-it-desk/uploads + ``` +2. `scripts/deploy.sh` 部署时建 host 目录: + ```bash + sudo mkdir -p /volume1/docker/wecom-it-desk/uploads + sudo chown -R 1000:1000 /volume1/docker/wecom-it-desk/uploads + ``` +3. 加 deploy 文档警示"别用 `docker-compose down -v`" + +**验收**: +- 容器重建后上传文件不丢 +- `df -h /volume1/docker/wecom-it-desk/uploads` 体积能涨 + +**评审员**: Claude + +--- + +### W-2. P0 二次评审 5 遗留修完 + +**任务编号**: #18 遗留 +**关联**: `docs/评审报告/workbuddy-2026-06-14-P0安全.md` 11.x 节(5 项遗留) + +**5 项遗留**: +1. **浏览器 WS API 不支持 header** —— 用 `Sec-WebSocket-Protocol: bearer.` 方案 +2. **nginx access_log 没关** —— `location /ws/ { access_log off; }` 已修,验证部署版也有 +3. **类型 bug** —— `ws.py` 某处类型断言错误 +4. **降级放行** —— `agents.py` 缺 password 时,`existing_agent.password_hash` 已存在 → 必须 verify password,不能放行 +5. **缺依赖** —— `requirements.txt` 缺 `bcrypt` / `pyotp`(已加,验证) + +**修复**: 逐项对照评审报告修复,**每项单独 commit** + +**验收**: +- 全部 5 项 commit 推 Gitea +- 评审员 Claude 二次评审通过 +- 风险跟踪表 第九节 / 第十节 状态从 🟡 改 ✅ + +**评审员**: Claude + +--- + +### W-3. pytest 基础配置 + 跑 pre-commit-check.sh + +**任务编号**: README 已知问题 #2 +**关联**: `scripts/pre-commit-check.sh`(本次新增,C-1 任务) + +**修复**: +1. `backend/pytest.ini`(或 `pyproject.toml` [tool.pytest.ini_options]): + ```ini + [pytest] + testpaths = tests + python_files = test_*.py + addopts = -v --tb=short + ``` +2. `backend/tests/conftest.py`: + - 异步 client fixture + - 测试 DB(用 sqlite:///:memory:) + - mock WECOM 凭据 +3. `backend/tests/test_agents.py`: + - 鉴权测试(mock_login 关闭 / 开启) + - password_hash 验证 +4. `backend/tests/test_messages.py`: + - 5 个端点鉴权测试(P0-2~6) +5. `backend/tests/test_ws.py`: + - WS token 鉴权(Authorization header / subprotocol / query 三种) +6. `scripts/pre-commit-check.sh` 加进 `scripts/deploy.sh` 流程(可选) + +**验收**: +- `cd backend && pytest` 跑过 +- CI 跑预检脚本 +- 评审员 Claude 看测试覆盖度 + +**评审员**: Claude + +--- + +### W-4. Dify API 集成预研(POC) + +**任务编号**: 阶段 3 启动前置(关联 `docs/路线图/阶段2-3-任务.md` §3.3) +**关联**: `docs/现有系统交接文档内容.txt` + `docs/ExternalSystemAdapter设计文档.md` + +**预研目标**: +1. 查 Dify 工作流 API 文档(看是否需要新 app,还是共用) +2. POC 三个端点: + - `POST /v1/chat-messages` 流式对话 + - `POST /v1/workflows/run` 工作流触发 + - `POST /v1/datasets/{id}/retrieve` 知识库检索 +3. 在 `backend/app/services/dify_client.py` 写 Dify 客户端 +4. `backend/app/api/ai_wingman.py` 三个端点接 Dify 客户端 +5. 写 `docs/集成验证/Dify_POC_报告.md` + +**验收**: +- 三个端点跑通(返回 Dify 响应) +- 文档含 API 限流 / 错误降级 / 配额申请 +- 评审员 Claude 看方案可行性 + +**评审员**: Claude + +--- + +### W-5. nginx 配置审计(全局 access_log 检查) + +**任务编号**: 新增(M-2 风险项 衍生) +**关联**: `docs/风险跟踪表.md` 第十二节 M-2 + +**审计目标**: +1. 扫描所有 `nginx.conf` / `deploy-server/nginx.conf` / `*/nginx.conf` +2. 找敏感路径(WS / token / OAuth callback)是否都 `access_log off` +3. 找未配 access_log off 但应配的路径 +4. 写 `docs/审计报告/nginx_access_log_审计.md` + +**修复**: 缺的补 `access_log off;` + +**验收**: +- 审计报告列出所有敏感路径的 access_log 状态 +- 缺的已补 commit +- 评审员 Claude 抽查 3 处 + +**评审员**: Claude + +--- + +## ▼▼▼ 任务清单止 + +--- + +## 🔄 工作流(workbuddy 启动后) + +1. **读这份 memory** → 看 5 任务 +2. **按 W-1 → W-2 → W-3 → W-4 → W-5 顺序**(W-3 W-4 W-5 可并行) +3. **每完成一项**: + - 提交 commit(走 `scripts/pre-commit-check.sh`) + - 推 Gitea 远端 `feature/xxx` 分支 + - 通知 Claude 评审 + - Claude 评审通过 → 用户合并 PR +4. **状态同步**: + - `docs/风险跟踪表.md` 更新状态 + - `.workbuddy/memory/{日期}-{主题}.md` 留评审记录 + +## ⚠️ 关键约束(读 README + CONTRIBUTING.md) + +- **鉴权**: 新增/修改端点必须有 `Depends(get_current_agent)` 或 `_get_current_employee` +- **依赖**: 新增第三方 import 必须同步 `requirements.txt` / `package.json` +- **alembic**: model schema 变化必须生成迁移脚本 +- **配置**: nginx / docker / conf 改动 plan 写完必须做完 +- **评审报告**: 每次推送生成 `docs/评审报告/workbuddy-{日期}-{主题}.md` +- **5 项遗留**: 上一轮评审遗留未修完,不许推新功能 + +## 🔗 关联文档 + +- 评审主报告: `docs/评审报告/` +- 风险跟踪表: `docs/风险跟踪表.md` 第九/十/十一/十二节 +- 路线图 2-3 阶段: `docs/路线图/阶段2-3-任务.md` +- 推送预检脚本: `scripts/pre-commit-check.sh` +- 推送流程: `CONTRIBUTING.md` §PR 流程 + +## 🆘 阻塞上报 + +workbuddy 启动后,**任何一项阻塞超过 30 分钟未推进** → 上报用户: +- token 问题 → 找用户 +- 凭据不全 → 找用户给 WECOM_SECRET / Dify API key +- 测试失败定位 → 找 Claude +- 评审反复打回 3 次 → 升级用户 + +## 🛏️ 用户睡前最后做的事 + +1. **Gitea Web** → 站点管理 → 用户 → **创建新用户**: + - 用户名: `workbuddy-claude` + - 邮箱: (用户填) + - 密码: (临时,首次登录改) + - 权限: 普通用户(非管理员) +2. **用 simon token 创 workbuddy-claude 的 access token**: + - 登录 workbuddy-claude 账号 → 头像 → 设置 → 应用 → 创建 + - 令牌名: `claude-push` + - 权限: `repository` (读/写) + `issue` (读/写) + `user` (读) +3. **把 workbuddy-claude token 粘给 Claude**: + - Claude 写进 `.workbuddy/config.json` 的 `gitea.token` 字段 + - 同时配 Gitea Web 的 deploy key(ssh,可选) +4. (可选)改 `docs/风险跟踪表.md` 第十二节 §12.4 待办 #5 → `block_admin_merge` 改 `true` + +完成上述 3 步 → workbuddy 客户端启动 → 自动接 5 任务 diff --git a/docs/路线图/阶段2-3-任务.md b/docs/路线图/阶段2-3-任务.md new file mode 100644 index 0000000..411860e --- /dev/null +++ b/docs/路线图/阶段2-3-任务.md @@ -0,0 +1,165 @@ +# 5 阶段路线图 — 阶段 2-3 任务拆解 + +**生成日期**: 2026-06-14 +**生成人**: Claude +**状态**: 待 workbuddy 接入执行(workbuddy-claude user 创好后启动) +**关联**: PRD.md §5 五阶段演进路径 + +--- + +## 阶段 1 完成度盘点(对照 PRD.md §5.1) + +| 阶段 1 项 | 状态 | 备注 | +|---|---|---| +| 员工端 H5 WebView | ✅ 完成 | `frontend-h5/` | +| OAuth2 静默授权 + 身份识别 | ✅ 完成 | `backend/app/api/h5.py` | +| 坐席工作台 MVP | ✅ 完成 | `frontend-agent/` | +| 三栏工作台 | ✅ 完成 | 会话列表 / 对话区 / AI 助手 | +| 6 分区会话列表 | ✅ 完成 | 待接单/我的/协作/其他坐席/AI处理/已结单 | +| 快速回复 7 大类 | ✅ 完成 | 28 子类 180 模板 | +| 转人工触发 | ✅ 完成 | 关键字检测 → 推 H5 链接 | +| WebSocket 实时推送 | ✅ 完成 | P0 鉴权修复后 | +| 应急模式 | ✅ 完成 | 系统故障时手动开 | +| Alembic 迁移 | 🟡 部分 | 008 (agent password) + 009 (message status) 已加,初始 001 缺 | + +**剩余扫尾**: +- [ ] 初始 alembic 迁移(把当前 schema 导出成 001 基准,后续 migrations 增量) +- [ ] pytest 基础配置(README 已知问题 #2) +- [ ] P1-1 优化(named volume → host bind mount,任务 #25) +- [ ] P0 二次评审 5 遗留(浏览器 WS API / nginx access_log / 类型 bug / 降级放行 / 缺依赖) + +--- + +## 阶段 2: H5 员工端完整体验 + 双通道消息推送 + +**目标**: 员工 H5 端从 MVP 升级到完整体验,加 摇人 / 评分 / 排队,推送到企微应用消息 + +**对应 PRD.md**: §5.2 阶段二,§痛点1(分散渠道) + +### 2.1 任务清单(8 项) + +| # | 任务 | 优先级 | 文件位置 | 阻塞 | +|---|---|---|---|---| +| 2-1.1 | 摇人按钮(H5 输入框左侧,7 种 SVG 动画) | 🟡 重要 | `frontend-h5/src/components/KnockButton.vue` | 无 | +| 2-1.2 | 满意度评价(会话结束 → 弹 5 星 + 文字反馈) | 🟡 重要 | `frontend-h5/src/components/ConversationRating.vue` | 无 | +| 2-1.3 | 排队系统(多员工同时咨询,显示"前面 N 位") | 🟡 重要 | `backend/app/api/conversations.py` + `frontend-h5/src/views/QueueStatus.vue` | 无 | +| 2-1.4 | 快速回复 7 大类 28 子类 180 模板(已有,需补完) | 🟢 常规 | `backend/app/models/quick_reply.py` | 无 | +| 2-1.5 | 知识库基础(FAQ 手动维护,坐席/员工可查) | 🟡 重要 | `backend/app/api/knowledge.py` (新) | 无 | +| 2-1.6 | 企微应用消息推送(双通道:企微应用消息 + WebSocket) | 🟡 重要 | `backend/app/services/wecom_push.py` | WECOM 应用 secret | +| 2-1.7 | 消息已读回执(员工读完 → 推 message_status) | 🟡 重要 | `backend/app/api/messages.py:mark_read` | 已有 ws_manager.broadcast | +| 2-1.8 | 会话转接(坐席 A → 坐席 B,带交接说明) | 🟢 常规 | `backend/app/api/conversations.py:transfer` | 无 | + +### 2.2 验收标准 + +- [ ] 员工在 H5 看到会话输入框左侧"摇人"按钮,点击 → 7 种动画之一 + 企微应用消息推送 +- [ ] 会话结束 → 员工看到 5 星评价 + 文字反馈框 → 提交 → 落库 +- [ ] 多员工并发咨询 → 排队显示"您前面有 N 位" + 预计等待时间 +- [ ] 坐席/员工可查 FAQ,输入关键字 → 命中模板 +- [ ] 员工 1 分钟内未读 → 推企微应用消息"您有一条新消息" +- [ ] 员工点开消息 → mark_read 调 → ws_manager 广播 → 坐席端"已读"标识 + +### 2.3 与 P0/P1 修复的关联 + +- 2-1.6 推送用到 P0 已修的 WS 鉴权 +- 2-1.7 已读回执用到 P1-4 已实现的 `broadcast_message_status` +- 2-1.5 知识库要等阶段 4 闭环,先做基础 CRUD + +### 2.4 预估工时 + +| 任务 | 预估人天 | 难度 | +|---|---|---| +| 2-1.1 摇人 | 2 | 低(Vue 组件 + 已有 SVG 库) | +| 2-1.2 满意度 | 1.5 | 低(弹窗 + 后端落库) | +| 2-1.3 排队 | 3 | 中(后端排队算法 + 前端轮询) | +| 2-1.4 快速回复补完 | 2 | 低(数据导入 + CRUD) | +| 2-1.5 知识库基础 | 5 | 中(模型 + 检索 + UI) | +| 2-1.6 企微应用消息 | 3 | 中(企微 API + 降级) | +| 2-1.7 已读回执 | 1 | 低(接 P1-4) | +| 2-1.8 会话转接 | 2 | 低(已有 transfer 端点) | +| **合计** | **19.5** | | + +--- + +## 阶段 3: 坐席工作台 AI Wingman + +**目标**: 给坐席端加 AI 辅助(草稿回复 / 自动摘要 / 知识推荐 / 排查步骤) +**对应 PRD.md**: §5.2 阶段三,§痛点2(坐席重复劳动) + +### 3.1 任务清单(6 项) + +| # | 任务 | 优先级 | 文件位置 | 阻塞 | +|---|---|---|---|---| +| 3-1.1 | AI 草稿回复(坐席输入 → AI 给回复草稿 → 坐席改/发) | 🟡 重要 | `backend/app/api/ai_wingman.py` (新) | Dify 集成 | +| 3-1.2 | 自动摘要(会话结束 → AI 生成 200 字摘要) | 🟡 重要 | `backend/app/services/summarizer.py` (新) | Dify 集成 | +| 3-1.3 | 知识推荐(对话中识别关键字 → 推 FAQ / 排查步骤) | 🟡 重要 | `backend/app/api/knowledge.py:recommend` | 2-1.5 知识库 | +| 3-1.4 | 排查步骤生成(员工描述问题 → AI 给 step-by-step) | 🟡 重要 | `backend/app/api/ai_wingman.py:troubleshoot` | Dify 集成 | +| 3-1.5 | 会话标注(坐席标"AI 推荐有用/无用") | 🟢 常规 | `backend/app/models/annotation.py` (新) | 无 | +| 3-1.6 | 坐席端 AI 助手面板(右侧栏,实时显示 AI 草稿) | 🟡 重要 | `frontend-agent/src/components/AIWingmanPanel.vue` | 3-1.1~4 后端 | + +### 3.2 验收标准 + +- [ ] 坐席输入框打字 → 右侧 AI 面板显示 3 条草稿回复(实时) +- [ ] 坐席点"采用" → 草稿填入输入框 → 可改后发送 +- [ ] 会话结束 → 自动生成 200 字摘要存库 +- [ ] 对话中员工说"VPN 连不上" → AI 推 5 条排查步骤 +- [ ] 坐席标注"有用/无用" → 落库 → 用于阶段 4 知识库迭代 + +### 3.3 Dify 集成前置 + +- 阶段 3 强依赖 Dify 工作流(已有 `docs/现有系统交接文档内容.txt` 描述) +- workbuddy W-4 任务 = Dify API 集成预研(POC),阶段 3 启动前完成 +- 风险: 企微 AI 机器人已在用 Dify,要确认是否新开 app / 共用 + +### 3.4 预估工时 + +| 任务 | 预估人天 | 难度 | +|---|---|---| +| 3-1.1 草稿回复 | 5 | 高(Dify 流式 + 实时推送) | +| 3-1.2 自动摘要 | 3 | 中(异步任务 + 触发时机) | +| 3-1.3 知识推荐 | 3 | 中(向量检索 + 评分) | +| 3-1.4 排查步骤 | 4 | 中(Dify prompt 工程) | +| 3-1.5 会话标注 | 2 | 低(模型 + UI) | +| 3-1.6 右侧栏 | 3 | 中(实时更新 + 草稿交互) | +| **合计** | **20** | | + +--- + +## 阶段 2-3 总工时 + 关键路径 + +``` +阶段 2 累计: 19.5 人天 +阶段 3 累计: 20 人天 +合计: 39.5 人天 +``` + +**关键路径**: +1. Dify 集成预研(W-4)→ 阶段 3 启动前置 +2. 知识库基础(2-1.5)→ 阶段 3 知识推荐(3-1.3)前置 +3. 摇人 / 评分 / 排队(2-1.1~3)可并行 + +--- + +## 启动条件(给 workbuddy) + +workbuddy-claude user account 创好后,把这份文档读进 `.workbuddy/memory/`,按以下顺序接任务: + +1. **先收尾 P1-1 优化 + 5 P0 遗留 + 初始 alembic**(#25 + 5 遗留 + 001 基准) +2. **阶段 2.1** → 2-1.1 摇人(Vue 组件简单,热身) +3. **阶段 2.2~3** → 满意度 / 排队 +4. **阶段 2.5~6** → 知识库基础 + 企微应用消息(企微 secret 需用户给) +5. **Dify 集成预研**(W-4)→ 阶段 3 启动 +6. **阶段 3 全部** + +每完成一项 → 提交 commit → 推 Gitea(走 pre-commit-check.sh 4 件套)→ Claude 评审 → 合 main + +--- + +## 风险与依赖 + +| 风险 | 等级 | 缓解 | +|---|---|---| +| Dify API 限流 | 🟡 | 加 Redis 缓存 + 异步队列 | +| 企微应用消息配额 | 🟡 | 双通道降级(WS 优先) | +| 知识库检索召回率 | 🟡 | 阶段 4 闭环后优化 | +| workbuddy token 不稳定 | 🟠 | 用户创 workbuddy-claude user 解决 | +| 阶段 2 推 main 冲突 | 🟡 | 强制走 PR + 评审 | diff --git a/docs/风险跟踪表.md b/docs/风险跟踪表.md index 0c8e9e4..36e592d 100644 --- a/docs/风险跟踪表.md +++ b/docs/风险跟踪表.md @@ -798,7 +798,110 @@ location /api/ { | 编号 | 状态 | |---|---| -| H-12 (P1-1) upload 路径 | 🔄 评审闭环中 | +| H-12 (P1-1) upload 路径 | 🔄 评审闭环中(留 P2 优化,任务 #25) | | H-13 (P1-2) Alembic 迁移 | 🔄 评审闭环中 | | H-14 (P1-3) healthcheck | 🔄 评审闭环中 | | H-15 (P1-4) ws 状态广播 | 🔄 评审闭环中 | + +--- + +## 第十二节: 2026-06-14 Gitea 卸载清空事故 + 重建复盘 ⚠️ 教训重灾区 + +**触发时间**: 2026-06-14 晚 +**触发原因**: 用户在 DSM 套件中心用 "卸载清空" 选项卸载 Gitea +**影响范围**: Gitea 服务停 + Web 不可达 + 仓裸仓库可能残留 +**恢复时长**: ~30 分钟 +**任务编号**: #26 + +### 12.1 事故时序 + +| 时刻 | 事件 | +|---|---| +| T+0 | 用户在 DSM 套件中心 → Gitea → 卸载 → 勾选"清空" | +| T+1m | Gitea 服务停止,8418 端口无响应 | +| T+1m | 外部 Funnel 域名 `ds923plus.tail58d872.ts.net` 无法访问 | +| T+5m | 本地仓 `D:\资料\03-项目开发\wecom_it_smart_desk` 检查 11 commit 完整 | +| T+10m | 用户发现"创仓报已存在文件" → 数据没清干净 | +| T+15m | 用户用 Gitea Web "删除仓库" → "创建新仓库" | +| T+20m | 用户创新 token `9754e1d8c8a0...` (权限含 admin) | +| T+22m | 我改 `.git/config` URL 清旧 token(走 wincred 缓存) | +| T+25m | PowerShell 推 main 成功(639 对象 / 3.67 MiB) | +| T+28m | 配 main 分支保护 (PR + 1 reviewer) | +| T+30m | 全部恢复,功能等价 | + +### 12.2 教训 + 防御 + +#### 🛑 教训 1: 卸载"清空" 不等于 数据清除 +- **现象**: 套件"卸载清空"清了 app + 数据库,**但仓裸仓库目录残留**(`/volume1/@appdata/gitea/gitea/repos/`) +- **后果**: 重装 Gitea 后创仓冲突("已存在文件") +- **修复**: 用户手动"删除仓库 → 创建新仓库"解决 +- **防御**: + - ✅ 部署 `scripts/backup-gitea.sh`(本次新增,C-2 任务) + - ✅ 卸载前**强制备份** + - ✅ 评估"卸载清空" vs "卸载保留数据" + +#### 🛑 教训 2: token 嵌入 `.git/config` URL 是反模式 +- **现象**: 之前为 workbuddy 推 Gitea,把 token `ae236991c3d5...` 直接嵌入 `origin.url` +- **后果**: workbuddy-claude token 失效后,URL 里有死凭据 + auto-classifier 拒绝重写 URL +- **修复**: URL 改回 `https://simon@...`,用 `git credential approve` 存 wincred +- **防御**: + - ✅ **永远不**在 URL 里嵌 token(写进 [[locked-decisions]] 候选) + - ✅ 推 Gitea 走 `git credential approve` + wincred + - ✅ workbuddy-claude 创独立 user account(避免 token 跟 simon 账号混) + +#### 🛑 教训 3: PowerShell 弹窗在后台易丢 +- **现象**: 用户推 main 时第一次"fatal: User cancelled dialog"(可能弹窗在后台没看到) +- **修复**: 用 `git credential approve` 预先存 wincred,推时不弹窗 +- **防御**: + - ✅ **CI / workbuddy / 脚本** 永远走 wincred(不弹) + - ✅ 交互推送前先 `git credential approve` + +#### 🛑 教训 4: main 分支保护配置需考虑"评审员有谁" +- **现象**: 配 `block_admin_merge: true` + `required_approvals: 1` + 只有 simon 一个 user → **simon 永远合不进自己 PR** +- **修复**: 临时改 `block_admin_merge: false`,等 workbuddy 接入再开 +- **防御**: + - ✅ 配保护前**确认有 ≥2 个 user**(评审员 + 推送者) + - ✅ 创 workbuddy-claude user account(本次未做,等用户睡前安排) + +### 12.3 数据保全审计 + +| 资源 | 卸载清空前 | 卸载清空后 | 重建后 | 完整性 | +|---|---|---|---|---| +| Gitea 服务 | ✅ 运行 | ❌ 停止 | ✅ 启动 | ✅ 100% | +| Gitea 数据库 (SQLite) | ✅ 完整 | ⚠️ 残留可能 | ✅ 全新 | ✅ 100%(旧数据丢) | +| 仓裸仓库 (repos/) | ✅ 11 commit | ⚠️ 残留 | ✅ 0 commit | ⚠️ 0%(待重推) | +| 本地仓 (windows) | ✅ 11 commit | ✅ 11 commit | ✅ 11 commit | ✅ 100% | +| Token 表 | ✅ 3 token | ⚠️ 残留 | ✅ 1 token(simon's) | ⚠️ 旧 token 全失效 | +| wincred 缓存 | ✅ workbuddy-claude | ⚠️ 残留 | ✅ simon 新 | ✅ 重置 | + +### 12.4 待办 + +| # | 项 | 阻塞 | +|---|---|---| +| 1 | **Gitea 备份脚本部署**(`scripts/backup-gitea.sh` 推到 NAS) | 用户需 SCP | +| 2 | **备份 cron 配置**(每天 3 点) | SSH 进 NAS | +| 3 | **创 workbuddy-claude user** | 用户睡前做 | +| 4 | **workbuddy-claude token 替换** | 等 #3 | +| 5 | **`block_admin_merge` 改回 `true`**(workbuddy 接入后) | 等 #3 | +| 6 | **删旧 workbuddy-claude token 残留** | 等 #3 | +| 7 | **Gitea 部署文档**(`docs/Gitea部署指南.md` 含备份恢复) | 我写 | +| 8 | **风险跟踪表加 "数据丢失" 风险项** | 我写(下面) | + +### 12.5 新增风险项 + +| 编号 | 严重度 | 内容 | 状态 | +|---|---|---|---| +| **M-1 (新)** | 🟠 中高 | **Gitea 数据无异地备份** —— 一旦 NAS 硬盘故障,Gitea 全失 | 🆕 本节新增 | +| **M-2 (新)** | 🟡 中 | **套件卸载误操作风险** —— 误勾"清空"导致数据全失 | 🆕 本节新增 | +| **L-2 (新)** | 🟢 低 | **PowerShell 弹窗后台丢失** —— 关键推送可能因弹窗丢失而失败 | 🆕 本节新增 | + +### 12.6 推送约定升级 (写进 [[locked-decisions]] 候选) + +> **所有 Gitea 推送凭据走 wincred,禁止明文嵌入 `.git/config` URL** + +具体: +1. `.git/config` 的 `origin.url` **只写用户名**(`https://simon@...`),不写 token +2. 首次推 / 换 token → `git credential approve` 一次性存 wincred +3. workbuddy 推送 → 创独立 user account + 自己的 token(不跟 simon 共用) +4. CI / 自动化推送 → 用环境变量 + `git -c credential.helper=!gh auth git-credential`(gh CLI) 或 secret store +5. **违反 → auto-classifier 拒绝**(已成事实) diff --git a/scripts/backup-gitea.sh b/scripts/backup-gitea.sh new file mode 100644 index 0000000..3483df1 --- /dev/null +++ b/scripts/backup-gitea.sh @@ -0,0 +1,246 @@ +#!/bin/bash +# ============================================================================= +# Gitea 备份脚本 (套件版 / Docker 版 通用) +# ============================================================================= +# 用途: 定期备份 Gitea 数据,防再次出现"卸载清空"惨案 +# +# 备份内容: +# - Gitea 配置文件 (app.ini) +# - SQLite 数据库 +# - 所有仓库 (git bare repos) +# - LFS 数据 +# - 附件 / avatars +# +# 用法: +# sudo bash scripts/backup-gitea.sh # 默认备份到 /volume1/backups/gitea/ +# sudo bash scripts/backup-gitea.sh /path/to/backup # 自定义备份目录 +# sudo bash scripts/backup-gitea.sh --keep 30 # 保留 30 天 +# sudo bash scripts/backup-gitea.sh --restore latest # 恢复最新备份(谨慎) +# +# Cron 建议 (每天凌晨 3 点): +# 0 3 * * * /volume1/docker/wecom-it-desk/scripts/backup-gitea.sh >> /var/log/gitea-backup.log 2>&1 +# ============================================================================= + +set -e + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +ok() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +# 默认参数 +BACKUP_DIR="${1:-/volume1/backups/gitea}" +KEEP_DAYS=7 +RESTORE="" + +for arg in "$@"; do + case $arg in + --keep) KEEP_DAYS="$2"; shift 2 ;; + --restore) RESTORE="$2"; shift 2 ;; + esac +done + +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +BACKUP_NAME="gitea-backup-${TIMESTAMP}" +BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}" + +# ============================================================================= +# 路径探测 +# ============================================================================= +detect_gitea_paths() { + info "探测 Gitea 数据目录..." + + # 套件版默认路径 + if [ -d "/volume1/@appdata/gitea" ]; then + GITEA_HOME="/volume1/@appdata/gitea" + info " 套件版: $GITEA_HOME" + # Docker 版常见路径 + elif [ -d "/volume1/docker/gitea" ]; then + GITEA_HOME="/volume1/docker/gitea" + info " Docker 版: $GITEA_HOME" + else + error "未找到 Gitea 数据目录,请手动指定 GITEA_HOME 环境变量" + fi + + # 找配置和数据子目录 + for sub in gitea data config repos lfs avatars; do + if [ -d "$GITEA_HOME/$sub" ]; then + declare -g "GITEA_${sub^^}=$GITEA_HOME/$sub" + fi + done + + # 找 SQLite 数据库 + for db in "$GITEA_HOME/gitea/data/gitea.db" "$GITEA_HOME/data/gitea.db"; do + [ -f "$db" ] && GITEA_DB="$db" && break + done + + if [ -z "$GITEA_DB" ]; then + warn "未找到 SQLite 数据库,可能用 MySQL/Postgres(此脚本不备份 DB)" + else + info " 数据库: $GITEA_DB" + fi +} + +# ============================================================================= +# 备份模式 +# ============================================================================= +do_backup() { + info "=== Gitea 备份开始 ===" + info "时间: $TIMESTAMP" + info "目标: $BACKUP_PATH" + + mkdir -p "$BACKUP_PATH" + detect_gitea_paths + + # 1. 备份 app.ini 配置 + if [ -f "$GITEA_HOME/gitea/conf/app.ini" ]; then + mkdir -p "$BACKUP_PATH/conf" + cp "$GITEA_HOME/gitea/conf/app.ini" "$BACKUP_PATH/conf/" + ok "备份配置 app.ini" + fi + + # 2. 备份 SQLite(必须先停 Gitea,或用 .backup 命令) + if [ -n "$GITEA_DB" ] && [ -f "$GITEA_DB" ]; then + # 用 sqlite3 .backup(支持热备) + if command -v sqlite3 &> /dev/null; then + sqlite3 "$GITEA_DB" ".backup '$BACKUP_PATH/gitea.db'" + ok "SQLite 热备完成 (sqlite3 .backup)" + else + warn "无 sqlite3 命令,改用文件复制(可能不一致)" + cp "$GITEA_DB" "$BACKUP_PATH/gitea.db" + fi + fi + + # 3. 备份仓库 (bare git) + if [ -d "$GITEA_HOME/gitea/repos" ]; then + info "备份仓库目录(可能大)..." + tar -czf "$BACKUP_PATH/repos.tar.gz" \ + -C "$GITEA_HOME/gitea" repos 2>/dev/null || \ + warn "仓库备份失败(可能权限不足,需 sudo)" + ok "仓库 tar 完成: $(du -h "$BACKUP_PATH/repos.tar.gz" 2>/dev/null | cut -f1)" + fi + + # 4. 备份 LFS + if [ -d "$GITEA_HOME/gitea/lfs" ]; then + tar -czf "$BACKUP_PATH/lfs.tar.gz" \ + -C "$GITEA_HOME/gitea" lfs 2>/dev/null || warn "LFS 备份失败" + fi + + # 5. 备份附件 / avatars + if [ -d "$GITEA_HOME/gitea/avatars" ]; then + cp -r "$GITEA_HOME/gitea/avatars" "$BACKUP_PATH/" 2>/dev/null || true + fi + + # 6. 备份元信息 + cat > "$BACKUP_PATH/backup.meta" </dev/null && git rev-parse HEAD 2>/dev/null || echo "N/A") +EOF + + # 7. 打包成单文件(方便转存) + info "打包最终备份..." + tar -czf "${BACKUP_PATH}.tar.gz" -C "$BACKUP_DIR" "$BACKUP_NAME" + rm -rf "$BACKUP_PATH" + ok "最终备份: ${BACKUP_PATH}.tar.gz" + ok " 大小: $(du -h "${BACKUP_PATH}.tar.gz" | cut -f1)" + + # 8. 清理旧备份 + info "清理 $KEEP_DAYS 天前的旧备份..." + local cleaned=0 + while IFS= read -r old; do + rm -f "$old" + cleaned=$((cleaned+1)) + done < <(find "$BACKUP_DIR" -maxdepth 1 -name "gitea-backup-*.tar.gz" -mtime +$KEEP_DAYS) + ok "清理旧备份: $cleaned 个" + + info "=== 备份完成 ===" + info "建议: 把 ${BACKUP_PATH}.tar.gz scp 到异地(例 NAS2 / 阿里云 OSS / 本地电脑)" +} + +# ============================================================================= +# 恢复模式 +# ============================================================================= +do_restore() { + if [ -z "$RESTORE" ]; then + error "未指定要恢复的备份名(--restore latest|YYYYMMDD-HHMMSS)" + fi + + if [ "$RESTORE" = "latest" ]; then + RESTORE_FILE=$(ls -t "$BACKUP_DIR"/gitea-backup-*.tar.gz 2>/dev/null | head -1) + [ -z "$RESTORE_FILE" ] && error "找不到备份文件" + else + RESTORE_FILE="$BACKUP_DIR/gitea-backup-${RESTORE}.tar.gz" + [ -f "$RESTORE_FILE" ] || error "备份文件不存在: $RESTORE_FILE" + fi + + warn "!!! 恢复操作会覆盖当前 Gitea 数据 !!!" + warn " 备份文件: $RESTORE_FILE" + warn " 按 Ctrl+C 取消,或 5 秒后继续" + sleep 5 + + info "解压备份..." + TMP_DIR=$(mktemp -d) + tar -xzf "$RESTORE_FILE" -C "$TMP_DIR" + BACKUP_CONTENT=$(ls "$TMP_DIR" | head -1) + [ -z "$BACKUP_CONTENT" ] && error "备份文件内容为空" + + detect_gitea_paths + EXTRACT_DIR="$TMP_DIR/$BACKUP_CONTENT" + + # 停 Gitea(套件 / Docker) + info "停 Gitea..." + if command -v synopkg &> /dev/null; then + sudo synopkg stop Gitea 2>/dev/null || true + fi + docker stop gitea 2>/dev/null || true + + # 恢复 app.ini + if [ -f "$EXTRACT_DIR/conf/app.ini" ]; then + cp "$EXTRACT_DIR/conf/app.ini" "$GITEA_HOME/gitea/conf/app.ini" + ok "恢复 app.ini" + fi + + # 恢复 DB + if [ -f "$EXTRACT_DIR/gitea.db" ]; then + cp "$EXTRACT_DIR/gitea.db" "$GITEA_DB" + ok "恢复 SQLite" + fi + + # 恢复 repos + if [ -f "$EXTRACT_DIR/repos.tar.gz" ]; then + rm -rf "$GITEA_HOME/gitea/repos" + tar -xzf "$EXTRACT_DIR/repos.tar.gz" -C "$GITEA_HOME/gitea/" + ok "恢复 repos" + fi + + # 启动 Gitea + info "启动 Gitea..." + if command -v synopkg &> /dev/null; then + sudo synopkg start Gitea + fi + docker start gitea 2>/dev/null || true + + rm -rf "$TMP_DIR" + ok "恢复完成" +} + +# ============================================================================= +# 入口 +# ============================================================================= +if [ -n "$RESTORE" ]; then + do_restore +else + do_backup +fi diff --git a/scripts/pre-commit-check.sh b/scripts/pre-commit-check.sh new file mode 100644 index 0000000..4874fc6 --- /dev/null +++ b/scripts/pre-commit-check.sh @@ -0,0 +1,329 @@ +#!/bin/bash +# ============================================================================= +# workbuddy / Claude 推送前 4 件套预检脚本 +# ============================================================================= +# 用途: 推送前自检 4 件套,发现 P0 漏洞立即拦截 +# 1. 鉴权: 新增/修改端点是否带 Depends(get_current_*) 鉴权 +# 2. 依赖: 新增 import 是否同步 requirements.txt / package.json +# 3. alembic: model schema 变化是否生成迁移脚本 +# 4. 配置: nginx / docker / conf 改动是否完整 +# +# 用法: +# bash scripts/pre-commit-check.sh # 检查 staged 变更 +# bash scripts/pre-commit-check.sh --staged # 同上(默认) +# bash scripts/pre-commit-check.sh --branch # 检查当前分支相对 main 的全部变更 +# bash scripts/pre-commit-check.sh --strict # 严格模式(任何 warn 也失败) +# bash scripts/pre-commit-check.sh --json # 输出 JSON 格式(给 workbuddy 解析) +# +# 退出码: +# 0 = 全过 / 仅 INFO +# 1 = 有 WARN(--strict 下) +# 2 = 有 ERROR(必须修) +# ============================================================================= + +set -e + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +PASS_COUNT=0 +WARN_COUNT=0 +FAIL_COUNT=0 +WARN_LIST=() +FAIL_LIST=() + +pass() { PASS_COUNT=$((PASS_COUNT+1)); echo -e "${GREEN}[PASS]${NC} $1"; } +warn() { WARN_COUNT=$((WARN_COUNT+1)); WARN_LIST+=("$1"); echo -e "${YELLOW}[WARN]${NC} $1"; } +fail() { FAIL_COUNT=$((FAIL_COUNT+1)); FAIL_LIST+=("$1"); echo -e "${RED}[FAIL]${NC} $1"; } +info() { echo -e "${BLUE}[INFO]${NC} $1"; } + +# 参数 +MODE="staged" +STRICT=false +JSON_OUT=false +for arg in "$@"; do + case $arg in + --staged) MODE="staged" ;; + --branch) MODE="branch" ;; + --strict) STRICT=true ;; + --json) JSON_OUT=true ;; + *) ;; + esac +done + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_ROOT" + +# 收集变更文件 +if [ "$MODE" = "staged" ]; then + info "检查 staged 变更..." + CHANGED=$(git diff --cached --name-only) + BASE="staged" +elif [ "$MODE" = "branch" ]; then + info "检查当前分支 vs main 的变更..." + BASE_BRANCH="main" + git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 || BASE_BRANCH="origin/main" + git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1 || { + fail "找不到 main 分支,请先 git fetch" + exit 2 + } + CHANGED=$(git diff --name-only "$BASE_BRANCH"...HEAD) +fi + +if [ -z "$CHANGED" ]; then + info "无变更文件,跳过" + exit 0 +fi + +info "变更文件 ($([ "$MODE" = "staged" ] && echo "staged" || echo "branch")):" +echo "$CHANGED" | sed 's/^/ /' + +# ============================================================================= +# 检查 1: 鉴权 +# ============================================================================= +info "" +info "── 检查 1/4: 鉴权 (Depends(get_current_*))" + +check_auth() { + local file="$1" + # 只检查后端 api 路由文件 + case "$file" in + backend/app/api/*.py|backend/app/api/**/*.py) ;; + *) return 0 ;; + esac + # 跳过非路由文件 + case "$file" in + *schemas*) return 0 ;; + *_test*) return 0 ;; + esac + + # 看 diff 是否新增/修改了路由(@router.*) + local diff + if [ "$MODE" = "staged" ]; then + diff=$(git diff --cached "$file") + else + diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") + fi + # 有 router 装饰器改动? + if ! echo "$diff" | grep -qE '^\+.*@router\.(get|post|put|delete|patch)'; then + return 0 # 无路由变化,跳过 + fi + + # 看新增/修改的函数是否带 Depends + local func_diff + func_diff=$(echo "$diff" | grep -E '^\+.*(async )?def [a-z_]+\(') + if echo "$func_diff" | grep -qE 'Depends\(.*get_current'; then + pass " $file: 新路由有 Depends 鉴权" + elif echo "$func_diff" | grep -qE '@router\.(get|post|put|delete|patch)'; then + # 新增路由但没 Depends → 极可能是 P0 漏洞 + local new_routes + new_routes=$(echo "$func_diff" | grep -B 5 'def [a-z_]' | grep -E '^\+.*@router\.' | head -5) + if [ -n "$new_routes" ]; then + fail " $file: 新增路由可能无鉴权: $(echo "$new_routes" | wc -l) 处" + fi + fi +} + +for f in $CHANGED; do + [ -f "$f" ] && check_auth "$f" +done + +# ============================================================================= +# 检查 2: 依赖 +# ============================================================================= +info "" +info "── 检查 2/4: 依赖 (requirements.txt / package.json)" + +check_deps() { + local file="$1" + # 只检查 Python / JS 文件 + case "$file" in + *.py) + # 看 diff 是否新增 import + local diff + if [ "$MODE" = "staged" ]; then + diff=$(git diff --cached "$file") + else + diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") + fi + local new_imports + new_imports=$(echo "$diff" | grep -E '^\+.*^(from|import) ' | grep -vE '^\+\s*#' | head -10) + if [ -z "$new_imports" ]; then return 0; fi + + # 提取第三方包(标准库除外) + local third_party + third_party=$(echo "$new_imports" | grep -E '^\+.*^(from|import) [a-z_]+' | \ + sed -E 's/^\+ *(from|import) ([a-z_][a-z0-9_]*).*/\2/' | \ + grep -vE '^(os|sys|re|json|time|datetime|typing|asyncio|pathlib|hashlib|hmac|secrets|base64|urllib|http|logging|functools|collections|itertools|contextlib|io|copy|enum|dataclasses|abc|math|random|string|subprocess|threading|multiprocessing|signal|socket|ssl|tempfile|shutil|glob|fnmatch|stat|argparse|getopt|unittest|traceback|warnings|pickle|csv|xml|html|email|zoneinfo|decimal|fractions|gcd)' | \ + sort -u) + if [ -n "$third_party" ]; then + # 检查 requirements.txt 是否已有 + local missing="" + for pkg in $third_party; do + if ! grep -qiE "^${pkg}([=<>!~]|$)" backend/requirements.txt 2>/dev/null; then + missing+="$pkg " + fi + done + if [ -n "$missing" ]; then + fail " $file: 新增第三方 import 但 requirements.txt 缺: $missing" + else + pass " $file: 新增 import 已在 requirements.txt" + fi + fi + ;; + *.ts|*.tsx|*.vue|*.js|*.jsx) + # 看 diff 是否新增 import / require + local diff + if [ "$MODE" = "staged" ]; then + diff=$(git diff --cached "$file") + else + diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") + fi + local new_imports + new_imports=$(echo "$diff" | grep -E '^\+.*(import .* from |require\()' | head -10) + if [ -z "$new_imports" ]; then return 0; fi + + # 找对应 package.json + local pkg_json="package.json" + case "$file" in + frontend-admin/*) pkg_json="frontend-admin/package.json" ;; + frontend-agent/*) pkg_json="frontend-agent/package.json" ;; + frontend-h5/*) pkg_json="frontend-h5/package.json" ;; + frontend-portal/*) pkg_json="frontend-portal/package.json" ;; + esac + if [ -f "$pkg_json" ]; then + # 提取包名(简单粗暴,workbuddy 改的常规 npm 包) + local new_pkgs + new_pkgs=$(echo "$new_imports" | grep -oE "from ['\"](@?[a-z][a-z0-9_/.-]+)" | sed -E "s/from ['\"]//" | sort -u) + if [ -n "$new_pkgs" ]; then + local missing="" + for pkg in $new_pkgs; do + if ! grep -qE "\"${pkg#@*/?}\"" "$pkg_json" 2>/dev/null; then + missing+="$pkg " + fi + done + if [ -n "$missing" ]; then + warn " $file: 新增 import,需确认 $pkg_json 有: $missing" + fi + fi + fi + ;; + esac +} + +for f in $CHANGED; do + [ -f "$f" ] && check_deps "$f" +done + +# ============================================================================= +# 检查 3: alembic +# ============================================================================= +info "" +info "── 检查 3/4: alembic 迁移" + +check_alembic() { + local file="$1" + # model 改了 → 必须有 alembic 迁移 + case "$file" in + backend/app/models/*.py) + # 看 diff 是否改 schema(Column / type / nullable / default) + local diff + if [ "$MODE" = "staged" ]; then + diff=$(git diff --cached "$file") + else + diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") + fi + if echo "$diff" | grep -qE '^\+.*Column\(|^\+.*Mapped\['; then + # 找本次 commit/branch 是否新增 alembic 迁移 + local migrations + if [ "$MODE" = "staged" ]; then + migrations=$(git diff --cached --name-only | grep "alembic/versions/.*\.py" || true) + else + migrations=$(git diff --name-only "$BASE_BRANCH"...HEAD | grep "alembic/versions/.*\.py" || true) + fi + if [ -z "$migrations" ]; then + fail " $file: model schema 变化但无 alembic 迁移" + else + pass " $file: model 改了,有 alembic 迁移: $migrations" + fi + fi + ;; + backend/alembic/versions/*.py) + info " $file: alembic 迁移新增" + ;; + esac +} + +for f in $CHANGED; do + [ -f "$f" ] && check_alembic "$f" +done + +# ============================================================================= +# 检查 4: 配置 +# ============================================================================= +info "" +info "── 检查 4/4: 配置 (nginx / docker / conf)" + +check_config() { + local file="$1" + case "$file" in + nginx.conf|deploy-server/nginx.conf|docker-compose.yml|docker-compose*.yml|.env.example) + warn " $file: 配置文件改动,确认 deploy.sh / docs/DEPLOY_NAS.md 同步更新" + ;; + backend/app/config.py) + # 配置改了 → 看 .env.example 是否同步 + local diff + if [ "$MODE" = "staged" ]; then + diff=$(git diff --cached "$file") + else + diff=$(git diff "$BASE_BRANCH"...HEAD -- "$file") + fi + local new_settings + new_settings=$(echo "$diff" | grep -E '^\+.*(os\.getenv|Field\(.*env=)' | head -5) + if [ -n "$new_settings" ]; then + warn " $file: 新增配置项,确认 .env.example 同步" + fi + ;; + esac +} + +for f in $CHANGED; do + [ -f "$f" ] && check_config "$f" +done + +# ============================================================================= +# 总结 +# ============================================================================= +info "" +info "── 总结" +echo " PASS: $PASS_COUNT / WARN: $WARN_COUNT / FAIL: $FAIL_COUNT" + +if [ $FAIL_COUNT -gt 0 ]; then + echo "" + echo "🛑 [FAIL] 列表:" + for msg in "${FAIL_LIST[@]}"; do echo " - $msg"; done + echo "" + echo "🛑 预检失败,请修 FAIL 项后再推送" + exit 2 +fi + +if [ $WARN_COUNT -gt 0 ]; then + echo "" + echo "⚠️ [WARN] 列表:" + for msg in "${WARN_LIST[@]}"; do echo " - $msg"; done + if [ "$STRICT" = true ]; then + echo "" + echo "🛑 严格模式下 WARN 也算失败" + exit 1 + fi + echo "" + echo "⚠️ 有 WARN 项,但允许推送(评审员关注)" +fi + +echo "" +echo "✅ 预检通过,可以推送" +exit 0