# 经验日志

记录已捕获的 learnings、纠正信息和新发现。开始重大任务前建议先回顾。

---

## [LRN-20260424-001] best_practice

**Logged**: 2026-04-24
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
OpenSSL 3.x 无法直接使用旧版 `.pfx` 证书，需要 `-legacy` flag 先转换为 PEM 再使用。

### Details
ADP API 的 mTLS 证书为 `.pfx`（PKCS#12）格式，使用旧版加密算法（RC2/3DES）。在 OpenSSL 3.x 环境下直接使用会报：`error:0308010C:digital envelope routines::unsupported`

解决方法：先用 `-legacy` flag 提取 PEM 证书，再配合 `.key` 私钥使用：
```bash
openssl pkcs12 -legacy -in cert.pfx -nokeys -clcerts -out /tmp/cert.pem -passin pass:
```
然后用 `/tmp/cert.pem` + `.key` 组合调用 API。Node.js 代码也需要同样处理，不能直接读 `.pfx`。

### Metadata
- Source: conversation
- Pattern-Key: infra.openssl3-pfx-legacy-flag
- Tags: openssl, mtls, certificate, adp-api

---

## [LRN-20260424-002] best_practice

**Logged**: 2026-04-24
**Priority**: medium
**Status**: pending
**Area**: backend

### Summary
ADP API 的总数参数是 `count=true`（不带 `$`），与其他 OData 参数（`$top`、`$skip`、`$filter`）规范不一致。

### Details
ADP `/hr/v2/workers` 的分页参数遵循 OData 规范（带 `$`），但总数参数是例外：正确写法是 `?count=true`，写成 `?$count=true` 不生效，meta 返回 None。另外 `$filter` 与 `count=true` 不能同时使用，无法做到 filter + count 组合查询。

### Metadata
- Source: conversation
- Pattern-Key: adp-api.count-param-no-dollar-sign
- Tags: adp-api, odata, pagination

---

## [LRN-20260330-001] best_practice

**Logged**: 2026-03-30T08:37:44Z
**Priority**: high
**Status**: promoted
**Area**: infra

### Summary
本仓库本地开发启动应优先使用 `scripts/dev/dev.sh`，不要把 `scripts/deploy/deploy.sh` 当作默认本地启动入口。

### Details
本次本地启动排查中，真实可用路径是先启动 Docker 基础服务，再执行 `backend` 的 `prisma generate`、`db:push`，最后分别启动后端和前端。`scripts/deploy/deploy.sh` 虽然存在，但职责偏部署与运维编排；直接拿它作为本地开发主入口，容易和当前 `.env` 的 Docker 变量缺口、开发服务行为混淆。另一个实际坑点是：当前 `.env` 可能只覆盖应用读取所需变量，未覆盖 `docker compose` 所需的 `POSTGRES_PORT`、`CONTAINER_PREFIX` 等变量。

### Suggested Action
进入仓库后，优先按以下顺序判断本地启动方式：`bash scripts/dev/dev.sh up` → `cd backend && npm run prisma:generate` → `cd backend && npm run db:push` → `cd backend && npm run start:dev` → `cd frontend && npm run dev`。如 Docker 端变量缺失，先补 `.env` 或在命令行临时注入。

### Metadata
- Source: conversation
- Pattern-Key: local-dev-startup-entrypoint-dev-sh
- Promoted: AGENTS.md, CLAUDE.md

---

## [LRN-20260330-002] best_practice

**Logged**: 2026-03-30T10:10:00Z
**Priority**: high
**Status**: pending
**Area**: tests

### Summary
会议出勤这类依赖外部同步的联调验证，在本地无样本数据时应先构造“最小受控样本”，再走真实 API 链路。

### Details
本次要验证“删除参会人后冻结 Outlook 同步”和“已有出勤记录禁止删除”，本地开发库没有任何会议或 Outlook 纳管记录。直接等待真实样本会阻塞验证，也无法区分代码问题还是数据缺失。更稳的方式是：先用真实 API 创建会议与参会人，再用 Prisma 插入最小 Outlook mailbox/binding 样本，最后调用真实删除接口和历史查询接口验证状态、返回码与日志。这种做法既覆盖了真实控制器/服务层，又避免依赖外部 Outlook 数据源。

### Suggested Action
后续凡是验证“本地业务对象 + 外部同步状态”联动场景，优先采用“真实 API 创建业务对象 + 最小测试 binding/fixture”策略，而不是手工猜测现有库里是否有可复用数据。

### Metadata
- Source: conversation
- Related Files: backend/src/modules/meeting-attendance/services/meetings.service.ts, backend/src/modules/meeting-attendance/services/outlook-sync.service.ts
- Tags: meeting-attendance, local-validation, outlook-sync
- Pattern-Key: local-validation.minimal-controlled-fixture

---

## [LRN-20260408-001] best_practice

**Logged**: 2026-04-08T02:30:00Z
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
多品牌部署：通过 `NEXT_PUBLIC_*` 环境变量 + `brand.ts` 配置中心抽离品牌标识，不同实例用不同 `.env` 即可切换品牌。

### Details
项目原为 FF 公司定制，需支持 AIxC 部署。品牌标识散落在 locales、layout、组件、邮件模板、初始化脚本等 13+ 个文件中。解决方案：
1. 新建 `frontend/src/lib/brand.ts` 作为品牌配置中心，集中读取 `NEXT_PUBLIC_*` 环境变量并提供默认值
2. 所有前端硬编码（Logo路径、品牌名、Slogan、示例邮箱域名）改为引用 `brand.*`
3. 后端在 `configuration.ts` 新增 `brand` 区块，邮件页脚、User-Agent 等改为从 ConfigService 读取
4. 所有新变量都有 FF 默认值，不设置时行为完全不变

关键注意点：`NEXT_PUBLIC_*` 是 Next.js **构建时**内联的，改了 `.env` 必须重新 `next build`，仅 restart PM2 不会生效。

### Suggested Action
后续新增用户可见文案时，先检查是否属于品牌标识（公司名、Logo、Slogan、邮件签名等），如果是则必须通过 `brand.ts` / `configuration.ts` 的环境变量控制，不能硬编码。

### Metadata
- Source: conversation
- Related Files: frontend/src/lib/brand.ts, backend/src/config/configuration.ts, .env.example
- Tags: multi-brand, white-label, deployment
- Pattern-Key: multi-brand.env-driven-branding

---

## [LRN-20260408-002] best_practice

**Logged**: 2026-04-08T02:30:00Z
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
Azure 全新服务器部署清单：安装依赖 → 克隆代码 → 上传 .env → 修复 lockfile 镜像 → db push + resolve migrations → 种子 → PM2 启动 → Nginx 反代 → 开 NSG 端口。

### Details
在两台全新 Azure VM (Ubuntu 24.04) 上从零部署项目，总结出的完整流程：
1. **基础依赖**：Node.js 20 (nodesource)、Docker CE、PM2、build-essential
2. **Docker 权限**：`usermod -aG docker $USER`，但 SSH session 不会立刻生效，必须用 `sg docker -c "命令"` 包裹
3. **代码克隆**：用 Gitea token URL 克隆，checkout 到 develop
4. **环境配置**：上传 `.env.uat` 或 `.env.pro`，含品牌变量、数据库密码、JWT Secret 等
5. **lockfile 修复**：`package-lock.json` 含腾讯云镜像 URL，Azure 服务器访问不了，每次 git pull 后都要 `sed` 替换
6. **数据库初始化**：全新数据库不用 `migrate deploy`（历史迁移有嵌套事务问题），改用 `prisma db push` + 逐个 `migrate resolve --applied`
7. **Nginx**：反向代理 80 端口到内部前端/后端端口，对外只暴露 80/443
8. **Azure NSG**：必须在 Azure Portal 手动开放 80/443 端口，VM 本地防火墙 (ufw) 默认 inactive 不阻挡

### Suggested Action
下次部署新环境时，按此清单执行。特别注意 `sg docker` 包裹命令、lockfile sed 替换时机（git pull 之后）、以及 db push 替代 migrate deploy 的策略。

### Metadata
- Source: conversation
- Related Files: scripts/deploy/deploy.sh, docker/docker-compose.yml
- Tags: azure, deployment, fresh-server, checklist
- Pattern-Key: deployment.azure-fresh-server-checklist

---

## [LRN-20260408-003] best_practice

**Logged**: 2026-04-08T02:30:00Z
**Priority**: medium
**Status**: pending
**Area**: infra

### Summary
Gitea 分支保护绕过：紧急合并时需同时禁用 `required_approvals` 和 `enable_status_check`，合并后立即恢复。

### Details
项目 develop 分支有三层保护：1 个 approval + 3 个 status check。当需要紧急合并且无法自我 approve 时：
1. 仅禁用 approvals 不够 — status check 可能还在 pending
2. 仅设置 status 为 success 不够 — 新 commit 会触发新 CI run 覆盖状态
3. 正确做法：通过 API 同时 `PATCH` 分支保护，设 `required_approvals: 0` + `enable_status_check: false`，合并后立即恢复原值

另外注意：Gitea merge API 成功时可能返回 204 No Content（空响应体），不是 JSON，不要用 json.load 解析。

### Suggested Action
紧急合并时使用此三步法：禁用保护 → 合并 → 恢复保护。合并前确认分支不落后 base（否则需要先 rebase）。

### Metadata
- Source: conversation
- Related Files: .gitea/workflows/
- Tags: gitea, branch-protection, emergency-merge
- Pattern-Key: gitea.emergency-merge-bypass

---

## [LRN-20260416-001] correction

**Logged**: 2026-04-16T06:30:00Z
**Priority**: critical
**Status**: resolved
**Area**: backend

### Summary
v2.2 user-effective API 没处理 effect=revoke，导致 sync 脚本无法将用户级工具取消同步到 OpenClaw

### Details
用户在 Workspace UI 给 Hongwei 取消了 image 工具（effect=revoke），但 OpenClaw agent config 仍然保留 image。根因：v2.2 `getUserEffectiveTools` 方法查 `AIToolGrantUser` 时没有区分 `effect` 字段，把 revoke 记录也当作 grant 加入了结果。sync 脚本调的是 v2.2 API，所以用户取消的工具永远不会从 OpenClaw 移除。同样的问题也存在于 `getToolSubjects` 反查接口。

### Suggested Action
**新增 effect 字段时，必须同时审查所有消费该表数据的方法。** DB schema 加字段容易遗漏下游读取方的过滤条件。应该在加字段的 PR review 阶段强制检查所有 `findMany({ where: { toolName } })` 类查询。

### Resolution
- **Resolved**: 2026-04-16T06:10:00Z
- **Notes**: 修复了 getUserEffectiveTools、getUserEffectiveToolsV2、getToolSubjects 三处

### Metadata
- Source: user_feedback
- Related Files: backend/src/modules/organization/ai-tools/ai-tools.service.ts
- Tags: data-integrity, schema-migration, sync-pipeline
- Pattern-Key: schema.field_add_downstream_audit

---

## [LRN-20260416-002] correction

**Logged**: 2026-04-16T06:30:00Z
**Priority**: high
**Status**: resolved
**Area**: frontend

### Summary
label 包裹 Radix Checkbox 导致双击 bug — 点击触发两次 toggle，checkbox 状态不变

### Details
在 Drawer 编辑器中用 `<label>` 包裹 `<Checkbox onCheckedChange={toggle}>` 时，点击 label 文本会：(1) label 的默认行为触发关联的 form control → onCheckedChange 执行一次 toggle (2) 事件冒泡到 label 本身再次触发。结果 toggle 被调用两次，状态回到原始值，看起来像"点了没反应"。这个 bug 在 v2.2 阶段也出现过（checkbox double-click bug in PR #75 会话摘要里提到过）。

### Suggested Action
Radix Checkbox 永远不要用 `<label>` 包裹。改用 `<div onClick={toggle}>` + `<Checkbox className="pointer-events-none" />`。这样只有 div 的 onClick 触发一次，Checkbox 只做视觉展示。

### Resolution
- **Resolved**: 2026-04-16T06:08:00Z
- **Notes**: RoleGrantsView + UserGrantsView 两处 label→div

### Metadata
- Source: user_feedback
- Related Files: frontend/src/app/(modules)/organization/ai-tools/components/RoleGrantsView.tsx, UserGrantsView.tsx
- Tags: radix-ui, checkbox, double-click, form-control
- Pattern-Key: frontend.radix_checkbox_label_double_click
- Recurrence-Count: 2
- First-Seen: 2026-04-15
- Last-Seen: 2026-04-16

---

## [LRN-20260416-003] correction

**Logged**: 2026-04-16T06:30:00Z
**Priority**: high
**Status**: pending
**Area**: frontend

### Summary
用原生 HTML 控件（select/checkbox/input）而不用项目设计系统组件，导致风格不一致被用户多次指出

### Details
项目有完整的 Radix UI 组件库（@/components/ui/checkbox, select, switch 等），但开发新功能时习惯性用原生 HTML 控件。用户多次反馈"勾选框不好看""下拉框和其他页面不一样"。正确做法是：开发前先查 `frontend/src/components/ui/` 下有哪些现成组件，参照同模块已有页面（如 Members 页）的实现方式。

### Suggested Action
**新页面开发前必须先查 components/ui/ 和参照页面的组件用法。** 建议在前端开发 skill 的 checklist 里加入"UI 组件对齐检查"步骤。

### Metadata
- Source: user_feedback
- Related Files: frontend/src/components/ui/
- Tags: design-system, radix-ui, ui-consistency
- Pattern-Key: frontend.use_design_system_components

---

## [LRN-20260416-004] best_practice

**Logged**: 2026-04-16T06:30:00Z
**Priority**: high
**Status**: pending
**Area**: frontend

### Summary
MCP 无头浏览器缺中文字体，截图看不到中文文字，不能代替真人在真实浏览器里过页面

### Details
Playwright MCP 在 Ubuntu 服务器上运行时缺少 CJK 字体，所有中文显示为方块。这导致无法通过 MCP 截图判断：文字是否溢出容器、i18n 是否正确、标签是否可读。功能测试（点击按钮、验证 API 返回）可以通过 MCP 完成，但**视觉审查必须由用户在真实浏览器完成或安装 CJK 字体**。

### Suggested Action
1. 在 MCP 测试环境安装中文字体：`apt-get install fonts-noto-cjk`
2. 功能验证用 MCP，视觉验证明确告知用户需要自行检查
3. 不要在 MCP 截图基础上声称"视觉正常"

### Metadata
- Source: conversation
- Tags: mcp, playwright, cjk-font, testing-limitation
- Pattern-Key: testing.mcp_cjk_font_limitation

---

## [LRN-20260416-005] correction

**Logged**: 2026-04-16T06:30:00Z
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
手动在服务器 git merge 会导致本地分支与 remote 分叉，CI 自动部署失败（divergent branches）

### Details
为了快速验证功能，我直接 SSH 到 FF test 执行 `git merge origin/feature-branch`，绕过了 Gitea PR + CI 自动部署流程。后续 CI 的 `git pull` 发现 local staging 和 origin/staging 分叉，报 `fatal: Need to specify how to reconcile divergent branches` 导致自动部署失败。修复需要 `git reset --hard origin/staging` 清理本地状态。

### Suggested Action
**绝不在生产/测试服务器上手动 git merge 或 git checkout。** 部署必须走 CI 自动化（push staging → deploy-uat.yml）。如果需要紧急验证，可以手动跑 deploy.sh 但不要改 git 状态。

### Metadata
- Source: error
- Tags: deployment, ci-cd, git-diverge
- Pattern-Key: infra.never_manual_merge_on_server

---

## [LRN-20260417-001] correction

**Logged**: 2026-04-17T03:00:00Z
**Priority**: critical
**Status**: resolved
**Area**: backend

### Summary
setUserGrants 全清重建（deleteMany userId）会丢失之前的用户级调整，必须增量操作

### Details
用户第一次编辑取消了邮件授权（effect=revoke），第二次编辑取消了日历和文件。保存时后端 `deleteMany({userId})` 清空了该用户所有 grants 再只写入本次 diff，导致第一次的邮件 revoke 丢失。用户反馈："第二次我编辑的时候取消授权了日历和文件。保存后发现邮件的取消授权没有了"。

### Suggested Action
**批量保存场景必须评估"全量替换 vs 增量更新"。** 如果前端 Drawer 只展示/修改了部分项目，后端只应操作这些项目。具体到 Prisma：`deleteMany({userId, toolName: {in: touchedTools}})` 而不是 `deleteMany({userId})`。这是一个通用模式——凡是"多项批量保存"的 CRUD，都要考虑这个问题。

### Resolution
- **Resolved**: 2026-04-17T02:30:00Z
- **Notes**: setUserGrants 改为只删改 touchedTools 范围内的记录

### Metadata
- Source: user_feedback
- Related Files: backend/src/modules/organization/ai-tools/ai-tools.service.ts
- Tags: data-integrity, batch-update, incremental-crud
- Pattern-Key: backend.incremental_batch_update

---

## [LRN-20260417-002] correction

**Logged**: 2026-04-17T03:00:00Z
**Priority**: high
**Status**: resolved
**Area**: infra

### Summary
Node 18 不支持 ES2023 数组方法（.toSorted/.toReversed 等），TypeScript 编译不报错但运行时崩溃

### Details
OpenClaw sync 脚本在容器内通过 `npx tsx` 执行，容器 Node 版本为 18。`.toSorted()` 是 ES2023 新增方法，Node 20+ 才支持。TypeScript 编译器的 target 配置允许该语法通过，但运行时抛 `out.toSorted is not a function`。这个问题在 v2.3 开发中反复出现 3 次——每次从分支合并后，`.toSorted()` 又被意外引入。

### Suggested Action
**凡是要在 Node 18 环境运行的代码（sync 脚本、Workspace 后端），不要使用 ES2023 数组方法。** 用 `.slice().sort()` 替代 `.toSorted()`，`.slice().reverse()` 替代 `.toReversed()`。可以考虑在 ESLint 中禁用这些方法。

### Resolution
- **Resolved**: 2026-04-16T08:00:00Z
- **Notes**: 所有 .toSorted() 替换为 .sort()，修复了 3 次才稳定

### Metadata
- Source: error
- Related Files: openclaw/scripts/sync-ai-tool-permissions.ts
- Tags: node-compat, es2023, runtime-error
- Pattern-Key: infra.node18_es2023_compat
- Recurrence-Count: 3

---

## [LRN-20260416-006] best_practice

**Logged**: 2026-04-16T06:30:00Z
**Priority**: medium
**Status**: pending
**Area**: backend

### Summary
OpenClaw pi-tools.policy.ts 的 allow/alsoAllow 解析语义：agent 级写入会 override 全局，空数组也算写入

### Details
OpenClaw 在 `resolveExplicitProfileAlsoAllow` 用 `Array.isArray(tools?.alsoAllow)` 判断，空数组 `[]` 也返回 true → `??` 不 fallback 到全局。这意味着：
- `agent.tools.alsoAllow = []` 会**收窄**权限到零（v2.2 的严重 bug）
- `agent.tools.allow = [...]` 是白名单过滤（v2.3 利用的语义）
- `agent.tools.allow` 不存在 = 不过滤（未被 sync 管理的 agent 不受影响）

sync 脚本必须写 allow（白名单）而不是 alsoAllow（加法），且 nextTools 始终 ≥ LOCKED_SET 4 项，避免退化为空白名单。

### Metadata
- Source: conversation
- Related Files: openclaw/src/agents/pi-tools.policy.ts, openclaw/scripts/sync-ai-tool-permissions.ts
- Tags: openclaw, tools-policy, allow-semantics
- Pattern-Key: openclaw.allow_alsoallow_semantics

---

## [LRN-20260417-003] correction

**Logged**: 2026-04-17T10:00:00Z
**Priority**: high
**Status**: resolved
**Area**: infra

### Summary
Gitea "head behind base" 时不要反向 merge production 回 staging，会触发测试环境 CI

### Details
staging→production PR 被 Gitea 拒绝合并，报 "The head branch is behind the base branch"（production 有一个旧 merge commit 不在 staging 历史中）。我把 production merge 回 staging 并 push 了 staging 分支来解决分叉，结果 push staging 触发了 `deploy-uat.yml` 和 `deploy-aixc-uat.yml`，两个测试环境被意外重新部署。

### Suggested Action
遇到 "head behind base" 时，用 Gitea PR 的 squash merge 方式合并（不要求 head 包含 base 全部 commit），或临时 API 关闭 "require up to date" 设置。绝不把 production 反向 merge 回 staging。

### Resolution
- **Resolved**: 2026-04-17T10:00:00Z
- **Notes**: 测试环境功能未受影响（staging 内容没变），但浪费了一轮 CI 执行

### Metadata
- Source: error
- Tags: gitea, branch-protection, ci-trigger, deployment
- Pattern-Key: infra.no_reverse_merge_staging

---

## [LRN-20260417-004] best_practice

**Logged**: 2026-04-17T10:00:00Z
**Priority**: medium
**Status**: pending
**Area**: infra

### Summary
跨服务器 Docker 镜像传输：管道直传不落本地磁盘

### Details
本地开发机只有 4GB 空闲无法构建 6GB 镜像。解决方案：从已有镜像的服务器直接管道传输到目标服务器：
`ssh source "docker save IMAGE | gzip" | ssh target "gunzip | docker load"`
实际 2.8GB 压缩后传输约 5-10 分钟，用此方式成功把镜像从 FF test 传到了 AIxClaw test、FF prod、AIxClaw prod 三个目标服务器。

### Suggested Action
标准流程仍然是 `scripts/enterprise/build-enterprise-image.sh` 本地构建 + `deploy-ffworkspace.sh` 上传。管道直传是本地磁盘不足时的应急方案，适合镜像在某个环境已存在需要推到其他环境的场景。

### Metadata
- Source: conversation
- Tags: docker, deployment, image-transfer, disk-space
- Pattern-Key: infra.docker_pipe_transfer

---

## [LRN-20260417-005] best_practice

**Logged**: 2026-04-17T10:00:00Z
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
多环境部署种子数据：npx tsx 在服务器上常失败，直接用 SQL 更可靠

### Details
在 FF prod 和 AIxClaw prod 上用 `npx tsx scripts/backend/init/init-ai-tool-grants.ts` 跑种子脚本，先后遇到：ts-node baseUrl deprecated 报错、tsx 找不到 @prisma/client（因为 npx 用的是全局临时目录不在 backend/node_modules 内）。最终直接用 SQL CROSS JOIN + VALUES 一次性批量插入 288/264 条 grants，秒级完成。

### Suggested Action
服务器上跑一次性种子/迁移脚本，如果 TypeScript 运行环境不稳定（npx 版本、模块解析路径等），直接用 SQL 更可靠更快。特别是 CROSS JOIN 模式适合"所有角色 × 所有工具"这类笛卡尔积式种子。

### Metadata
- Source: conversation
- Related Files: scripts/backend/init/init-ai-tool-grants.ts
- Tags: deployment, seed-script, sql, prisma
- Pattern-Key: infra.sql_seed_over_tsx

---

## [LRN-20260417-006] correction

**Logged**: 2026-04-17T12:00:00Z
**Priority**: high
**Status**: resolved
**Area**: infra

### Summary
监控栈合并时 OTel collector 端口从 0.0.0.0 改成 127.0.0.1 导致 metrics 断流

### Details
FF prod 监控栈从独立 compose 合入 gateway compose 时，为了"安全"把 OTel collector 的端口绑定从 `0.0.0.0:4318` 改成了 `127.0.0.1:4318`。结果 gateway 的 OTel metrics 完全断流。根因：FF prod 的 `openclaw.json` 里 `diagnostics.otel.endpoint` 配的是宿主机内网 IP `http://10.1.7.6:4318`，不是 compose 内部 DNS。当 collector 只绑 loopback 时，从宿主机 IP 来的请求被拒绝。

排查时还走了弯路：看到 counter 为 0 以为 diagnostics-otel 插件没加载，实际上 Prometheus 里 26 个 `openclaw_*` metric names 都已注册（说明 pipeline 之前是通的），counter 归零只是因为 gateway 容器 recreate 后进程内计数器重置。

### Suggested Action
1. 改 OTel 端口绑定前先检查 gateway config 的 endpoint 地址
2. 验证 OTel pipeline 是否正常：查 Prometheus `openclaw_*` metric names 注册数量，而不是看 counter 当前值

### Resolution
- **Resolved**: 2026-04-17T12:00:00Z
- **Notes**: OTel collector 端口恢复为 `0.0.0.0:4318`

### Metadata
- Source: error
- Tags: monitoring, otel, port-binding, prometheus
- Pattern-Key: infra.otel_port_binding_breaks_metrics

---

## [LRN-20260501-002] 测试 DB 重置脚本吞错误 → silent state corruption → CI 偶发大面积红

**Logged**: 2026-05-01
**Priority**: high
**Status**: applied
**Area**: CI / test-infra

### Summary
PR #207 CI `backend-integration` 反复 9 suite 红，本地 366/366 全过。真因：`reset_test_db_schema` 用 `>/dev/null 2>&1` 吞 DROP/CREATE 错误；共享 self-hosted runner 跨 PR 复用 docker 容器，上轮 PR 残留连接 → DROP 失败 silent → 跑在旧库上 → seed 混乱 → 大面积 403/FK。

### Pattern
- ❌ 基础设施 reset 脚本 `>/dev/null 2>&1` 吞错误
- ✅ `psql -v ON_ERROR_STOP=1` + 不 redirect stderr
- ✅ "本地 vs CI 容器独立性"是 CI flake 排查第一直觉

### Action
1. ✅ 修 `lib-test-db.sh` ON_ERROR_STOP（已随 PR #207 合并）
2. ⏳ self-hosted runner 加"每 PR 重建容器"hook
3. ⏳ backend-integration 加 DB freshness check

详见 `.learnings/2026-05-01-test-db-silent-state-corruption.md`

---

## [LRN-20260501-001] CI 检查 silent fail-open：浅 fetch + `2>/dev/null` 吞错误

**Logged**: 2026-05-01
**Priority**: high
**Status**: applied
**Area**: CI / infra

### Summary
PR #207 提交了 2 个 migration 目录，撞 CLAUDE.md "一个 PR 一个迁移文件"硬指标，但 CI `migration-file-count` 检查报 `New migration directories: 0 ✅`（假绿）。三个失误叠加：冗余 `--depth=1` 砍掉完整历史 + 三点语法依赖 merge-base 浅历史下失败 + `2>/dev/null || true` 吞错误。

### Pattern
- ❌ 已 `fetch-depth: 0` 的 checkout 再 `git fetch --depth=1` → 砍历史
- ❌ 关键检查上 `2>/dev/null || true` → fail-open
- ✅ 显式 `git rev-parse --verify origin/$BASE_REF` sanity check 后再 diff
- ✅ CI 检查写完必须**负向测试**（构造应 fail 的 PR）

### Action
1. ✅ 修 `quality-gates.yml` migration-file-count
2. ⏳ grep 其他 CI 步骤同模式：`grep -rn '2>/dev/null *|| *true\|--depth=1' .gitea/workflows/`
3. ⏳ `.gitea/workflows/README.md` 明示两条红线

详见 `.learnings/2026-05-01-ci-shallow-fetch-fail-open.md`

---

## [LRN-20260430-002] Tailwind v4 下 `border-*` 不带颜色变成黑线

**Logged**: 2026-04-30
**Priority**: high
**Status**: applied
**Area**: frontend / styling

### Summary
Tailwind v3 → v4 默认值破坏性变更：`border-t` / `border-b` / `border` 不带颜色时，v3 默认 `gray-200`，v4 默认 `currentColor`（继承文字色 = 黑色）。整个 ops-center 列表页 7 处 border 全黑。

### Pattern
- ❌ `<div className="border-t" />` → 黑线
- ✅ `<div className="border-t border-[#f2f3f5]" />` → 行间浅灰
- 项目 Lark token：边框 `#e5e6eb`、行间 `#f2f3f5`、主色 `#3370ff`
- 参考 `frontend/src/styles/theme.ts` + `system-roles/page.tsx` 表格写法

### Action
1. ✅ 修复 ops-center 7 处黑线
2. ⏳ 把规则写进 `frontend-main` skill
3. ⏳ 评估 ESLint 自定义规则

详见 `.learnings/2026-04-30-tailwind-v4-black-borders.md`

---

## [LRN-20260430-001] 文档审查应该用 4-lens 框架 + 前置项目侦察，否则会反复多轮发现新问题

**Logged**: 2026-04-30
**Priority**: high
**Status**: applied
**Area**: workflow / documentation

### Summary
开发新模块时连续 5 轮"再看一眼"才挤干文档 bug：第一稿凭记忆写、之后每轮 free-association 审。根因是没做项目惯例侦察 + 没用结构化 lens。

### Pattern
- **Phase 0 — 项目侦察（30min）**：grep 权限码格式 / 装饰器用法 / schema 命名 / env 复用 / 同类模块代码 / 状态库
- **Phase 1 — 4 lens 审**：实现者走查 / 外部 API 对抗 / 失败模式 / 跨文档字段 diff
- **关键**：换 lens > 加轮次。同一 lens 跑两次信号增量 ≈ 0

### Action
1. ✅ 详细教训沉淀至 `.learnings/2026-04-30-doc-review-multi-lens.md`
2. ✅ 扩 `docs-main` skill：加 Phase 0 项目侦察清单
3. ✅ 新建 `doc-review` skill：4-lens 框架专门做"实现就绪度"审查

详见 `.learnings/2026-04-30-doc-review-multi-lens.md`

---

## [LRN-20260425-001] 新增外部数据同步前先看 platform_automation 同步中心模式

### Context
ADP 同步设计阶段，最初打算自建 `AdpSyncRun` 表 + 独立 cron + 自建监控页。

### Discovery
项目已有完整的同步中心基础设施 `platform_automation` schema：
- `AutomationTask` 任务定义（unique code, type 枚举, schedule 配置, 统计字段）
- `AutomationExecution` 每次执行记录（status, duration, logs, error, result）
- 已接入：LDAP_SYNC（Entra）、DINGTALK_SYNC
- 配套 controller、管理后台、告警机制

新接入只需：
1. 给 `AutomationTaskType` 枚举加新值
2. 启动时 upsert `AutomationTask` 记录
3. cron 触发时创建 `AutomationExecution` + 更新 task 统计
4. 同步源数据表（如 DingtalkEmployee, DingtalkLeaveQuotaSnapshot）放在 `platform_automation` schema 内

### Suggested Action
新加任何"外部系统 → 本地"数据同步前，先做这两步：
1. `grep "^model.*Sync\|automation" backend/prisma/schema/*.prisma` 看是否已有同步基础设施
2. 看 `backend/src/modules/organization/dingtalk/sync/` 现有实现作为模板

跳过这一步直接自建 SyncRun 表是常见反模式，会造成监控/告警/手动触发能力分裂。

### Metadata
- Source: design
- Tags: architecture, sync-center, platform-automation, anti-pattern
- Pattern-Key: backend.use_existing_sync_infrastructure

---
