Files
eaiadminandClaude Code bc30bfce28 docs(rules): 记录「dev 早期一包提交」——G14.5 例外 + E09 复盘
上一条提交(c1af86c)刻意混合了多条并行线,本提交把这次的教训沉淀下来:

- G14.5 增补例外条款:dev 早期直接 `git add -A` 一包提交,不为粒度打断节奏、
  不反问用户提交范围;同时保留「如实披露混合内容」的硬要求,并给出可操作判据
  「为了让提交能编译而不得不先测依赖锥 ⇒ 本来就是一个单元」。
  原文一字未删,例外写在原文之下;头部版本 V1.3 → V1.5。
- bugs_and_errors.md 新增 E09:症状(一句「提交一下」被做成依赖锥测量 +
  hunk 挑拣工具 + 5 轮索引导出编译 + 最后反问用户范围)、根因(把 G14
  「git 是后台事务」读成了「提交粒度值得花成本」)、修法、以及实测出的依赖锥
  (ASR / LLM 调用层 / 编排 Agent / 联网搜索 是一个编译单元)。

E09 记的是我自己的工作方式错误,不是环境坑。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 22:22:34 +08:00

783 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# eai_agentplatform 博昇 AI 数字员工平台(EAI Agent Platform)— 编码与调试最高准则
> **版本:V1.5**
> **日期:2026-09-26**
> **状态:必须强制执行 (Highest Priority)**
> **适用范围:eai_agentplatform(EAI 数字员工平台)后端(Go)、前端(Vue3)、数据库(SQLite)、AI 检索/对话、考试引擎、素材上传与审批**
> **AI 助手启动任何任务前必须先读取并确认本文件。**
---
> **整理说明**:参考 pj034-oeamgt TOP_CODING_RULES.md 结构体系重组。
> **分组方式**:第一部分是 EAIHub 博昇 AI 中心通用规则;第二部分是本项目专用规则。
> **编号方式**:第一部分 G01-G18;第二部分 P01-P06。
>
> **V1.1 补充说明**:本次只做加法,不删减、不压缩原有规则正文。
> 通用部分新增 G09-G18(承接 pj0034 同名文件的通用规则,按本项目 Go / Vue3 / SQLite / Ubuntu 技术栈改写,
> 不适用的部分——如 Python 虚拟环境、Playwright E2E、OSS 多租户——明确不抄);
> G04 补充第 6-10 条(来自两个项目共同踩过的坑);本项目新增 P06 常见技术陷阱清单。
>
> **V1.2 补充说明**:把命名前缀从「一条要求」扩成「一套规则」,全部落在 **G03**(当时不新开编号,避免命名规则被拆到两处)。
> 仍只做加法:G03 原第 1-4 条正文一字未改,只在其后新增第 5-10 条 + 关联节;标题由「变量命名锚定」放宽为「命名锚定」
> (前缀要管表名、API 路径、文件名),索引行同步更新。
> 展开与可执行化版本在 `docs/02_Architecture/AR09_对象命名规范.md` §5.7 + §6.2 守卫 G–J + §7.6。
>
> **V1.3 补充说明**:新增 **G19(外部下载物统一存放 `external_download/raw/`)**,仍只做加法。
> 起因是本地 ASR 落地时下了 4.1GB 模型与一整套 CUDA 依赖,散在 `~` 与 `/tmp` 里 ——
> 换个会话、换个人就没人知道那是什么、能不能删、要不要重下。规则要求
> 「**原始件进只读的 `raw/`(一份)+ 一份清单 + 一份 `SHA256SUMS`;
> 解压/转换/配置后的加工件放同级目录,可删可重建,没改过的大文件用软链指回 `raw/`**」。
>
> **V1.4 补充说明**:P06 增补 7 条(P06.12–P06.18),并在 P06.1 补两条延伸;仍只做加法。
> 收录标准是「**以后还会再踩**」,不是「修过一次就好了」——
> 已经修完、且不会以同样形状复发的过程性修复不收录(那些留在提交记录与代码注释里)。
> 本次来源:把仓库里散落在代码注释、架构文档、历史会话中的事故做了一次普查,
> 按「是否属于工具/语言/框架的固有性质、是否会换个场景再犯」筛过后才进来。
> 另:本地 ASR 落地中遇到的版本与依赖坑(`torchaudio>=2.9` 删 API 等)记在仓库根
> `bugs_and_errors.md`,不重复进 P06 —— 那份文件管「一次性的错」,P06 管「会复发的坑」。
>
> **V1.5 补充说明**:G14.5 增补**「dev 早期一包提交」例外**,并给出可操作判据
> (「为了让提交能编译而不得不先测依赖锥 ⇒ 本来就是一个单元,别拆」)。
> 起因:一句「提交一下」被我做成了一次依赖锥测量 + hunk 级挑选工具 + 反复导出索引编译,
> 最后停下来问用户「这次提交装哪些文件」——用户驳回,要的是直接一包提交。
> 原文「一次提交只装一件事」一字未删,例外写在它下面;**粒度可以放宽,如实披露不能放宽**。
> 事故复盘见 `bugs_and_errors.md` E09。
---
# 第一部分:通用开发规则
> **适用范围**:适用于 eai_agentplatform 及 EAIHub 下其他涉及 Go/Vue3/AI 的项目。
> **使用方式**:新任务开始前应先通读本部分;项目专用规则(P)在遵守本部分基础上叠加。
## 索引
- G01:深度调试日志 — 全链路埋点 + 特殊日志文件
- G02:Fail Fast 与零静默兜底
- G03:命名锚定 — 防命名漂移(含**前缀规范**:必要性判据 / 三类前缀 / 硬约束 / 退出条件 / 改名禁令)
- G04:测试与验收 — 完成判定必须靠事实
- G05:安全迁移与重构流程
- G06:AI 助手行为规范
- G07:交互控件可用态颜色统一
- G08:分层清晰,禁止前后端职责串线
- G09:登录态与接口必须无状态化
- G10:配置化优先,禁止写死环境细节
- G11:素材必须可追溯,不可静默修改
- G12:长耗时任务必须异步化
- G13:先跑通主线,再细化和优化
- G14:Git 不抢戏 — 收工时统一提示一次
- G15:禁止通用名启动入口文件,启动逻辑必须在 start_dev_10231_10232.sh
- G16:Windows 侧 .ps1 脚本统一 UTF-8 with BOM
- G17:脚本内禁止兼容式依赖回退,必须固定单一工具链
- G18:仓库应尽量支持拷贝后直接运行
- G19:外部下载物统一存放 `external_download/raw/`(原始件只读 + `SHA256SUMS`,加工件另置同级目录),一次下载永久复用
---
## G01 最高原则:深度调试日志 (Special Log & Console Print)
**任何**涉及功能异常、逻辑排错、API 失败或模板渲染问题的任务,必须遵循以下调试流程:
1. **强制全链路埋点**:禁止盲目猜测,必须在后端路由、中间件、服务层以及前端 JS 关键回调中,大量写入过程性输出。
2. **统一特殊日志文件**:
- 路径:`debuglog/backend_YYYY-MM-DD.log`(按天生成,由 `start_dev_*.sh` 落盘)
- 内容:必须包含 `[时间戳] [模块名] [详细描述]`
- 必须包含:请求参数、Session 状态、关键业务变量
- **脱敏红线**:落盘前必须脱敏/删除敏感信息(Authorization/Cookie/JWT/密码/API Key 等)
3. **同步控制台输出**:所有写入调试日志的内容必须同步 `print` 到 Console,以便开发者实时观察。
4. **排查先读日志**:在提出任何修复方案前,必须先调用读取工具检查该日志。
### 关键埋点清单
| 埋点位置 | 必须记录内容 |
|---|---|
| API 调用 | 请求参数、响应状态、异常栈 |
| 数据库交互 | 查询关键参数、结果集摘要 |
| 文件操作 | 上传/转换/提取的文件路径、大小、状态 |
| AI LLM 调用 | 发送给 AI 的完整 Payload + 返回原始正文;落盘前脱敏密钥/令牌 |
---
## G02 最高原则:Fail Fast 与零静默兜底 (Fail Fast & Zero-Fallback)
1. **禁止隐式回退**:所有涉及配置、运行时资产的读取,**严禁使用硬编码的默认值进行静默兜底**。
2. **配置/字段缺失即报错**:如果代码依赖某项配置或 JSON 字段且其缺失,必须立即抛出异常并终止流程。
3. **禁止入口层吞错**:路由入口不得 `try/except` 后静默放行;凡关键身份、权限、业务校验失败,必须返回明确错误(4xx/5xx)并阻断流程。
4. **禁止"先跑通再修正"策略**:不得为"先可用"加入 hardcode 默认值、临时跳过校验等行为。这类行为视为质量事故。
5. **错误可见性强制**:任何违反业务规则或数据约束的问题,必须对开发者显式可见(日志 + 返回错误 + 可复现路径),禁止隐藏真实错误来源。
6. **图片不做 OCR**:严格按照 PRD V1.1 规定,图片(png/jpg/jpeg)不做 OCR、不进 AI 文本库。
### 反例与正例
| | 反例 ❌ | 正例 ✅ |
|---|---|---|
| A | 文件审批状态未知时默认视为"已通过" | 状态非 `approved` 则拦截并返回明确错误 |
| B | LLM 配置缺失时使用硬编码的默认地址 | 配置缺失立即报错,引导管理员在系统参数补充 |
| C | 考试 session 不存在时返回空结果 | Session 不存在返回 404 + 明确错误信息 |
---
## G03 原则:命名锚定 — 防命名漂移 (Identity Anchoring)
> V1.2 起本条从「变量命名」放宽为「命名」:前缀规范要管到表名、API 路径、文件名,不只是变量。
1. **变量名前缀强制化**:所有业务相关变量必须带明确前缀(如 `media_file_id`, `exam_session_key`, `product_code`)。禁止使用 `id`, `data`, `res` 等模糊命名。(**哪些命名空间需要前缀,见第 5 条**)
2. **变量名全链路同步**:同一业务参数在 API、Service、Model 层必须保持变量名完全一致。
3. **最小长度约束**:变量名原则上不短于 5 个字符(循环索引除外)。
4. **AI 引用已定义标识符必须按字符复制**:AI 在生成或修改代码时,引用任何**已在项目中定义过**的标识符,必须先 Read/Grep 找到定义处,**按字符原样复制**,禁止自行改写大小写或分隔符。例如 `user_id` 不应被写成 `userId` 或 `uid`。
5. **前缀的必要性由「命名空间的形状」决定,不由对象的重要性决定**:
- **必须加**:SQLite 表名、表内列名、URL query、JSON key、目录内文件名、shell 变量 —— 这些命名空间**平铺且无类型**,名字是唯一的消歧手段。
- **不必加**:Go 包内标识符、结构体字段 —— 有作用域,编译器/运行时替你消歧,前缀只是噪音。
- **判据一句话**:*去掉它,同一个命名空间里会不会出现两个可能同名的东西?* 会 → 加;不会 → 别加。
- **本项目正例(两种写法都对,别去"统一")**:`deploy/eai_agentplatform.env` 用裸名 `PORT`(一个 systemd unit 独占进程环境);`start_dev_10231_10232.sh` 用 `BACKEND_PORT` / `FRONTEND_PORT`(同一 shell 跑两个服务)。
6. **三类前缀,各有各的生命周期**:
- **对象前缀**(`skill_definition` / `worker_task`):标记归属,**永久**。
- **来源前缀**(`staticSkillCatalog` / `normalizeCustomApp` / `legacy_*`):标记来路,**必须写退出条件**(见第 9 条)。
- **作用域前缀**(query 的 `app_`、API 的 `my_`):标记入口与归属,**禁止进入模型、表、字段名**。
- 对象前缀的白名单 = 正式对象术语表 + 已登记的子系统前缀。**白名单外的前缀不许发明**(同 G10:能用的集合必须封闭,否则每个人都会造自己的)。
7. **两条已收敛的规律,守住不回退**:
```text
数据库表名 API 路径
一级对象 specialist /api/specialists
归属或复合 worker_task /api/worker/*
```
DB 层与 API 路径**各自独立**收敛到同一条分法 —— 这是自然规律,不是硬塞的。**写进规范是为了守住,不是为了改造。**
8. **前缀硬约束**:
- 一个标识符最多带**一个**类型前缀:`worker_task` ✅ / `app_specialist_skill_key` ❌
- 次序固定「前缀 + 核心词 + 后缀」:`skill_definition` ✅ / `definition_skill` ❌
- 前缀**写全,禁止缩写**:`specialist_` ✅ / `sp_`、`sk_`、`wr_` ❌
- **禁止拼音前缀**:中文是对外展示层的事,不进标识符
- **过渡前缀禁止嵌套**:`legacy_` 之上不许再叠一层(理由见第 9 条)
- **来源前缀不得跨模块引用**:调用方不该知道数据是从哪来的。本项目现状是反例 —— `staticSkillCatalog` 的兜底写法 `skillCatalog.getByKey(k) || staticSkillCatalog.find(...)` 被抄到了 5 个调用点;正解是在 `skillCatalog` 里加 `resolve(key)` 把兜底收进模块
9. **来源前缀必须带退出条件**(`static*` / `legacy*` / `tmp*` / `old*` / `deprecated*` 描述的是过程状态,而过程会结束):
- **反例(本项目真实)**:`skill_definition` 一个字段先后有 **5 代列名** —— `entry_route → route → legacy_entry_route / legacy_route → legacy_object_entry_route`。过渡前缀叠到第二层,**就是上一次迁移没有退出条件的证据**。
- **正例**:该批 legacy 列的删除与迁移逻辑写在**同一笔提交**里,而不是"先留着以后再说"。照这个做。
- **要求**:写下来源前缀时,同处注释或相邻 TODO 必须写清「什么时候可以去掉」。
10. **前缀改名 = 协议改名,必须按字符串精确锚定**:
- 前缀几乎总活在**字符串**里(表名、列名、JSON tag、URL 参数、env 变量名),**编译器一个都管不着** —— 所以要按字符串 grep,不是按符号 grep。
- **禁止子串替换、禁止正则通配。** 本项目现成的雷:`role_kind`(正确新名 `object_kind`)与 `role_card_json`(正确新名 `interaction_card_json`)**都以 `role_` 开头但去向完全不同**,一句 `sed 's/role_/object_/g'` 会把第二个误伤成 `object_card_json`。
- 宁可一个标识符一个标识符地改,也不要图快。
### 关联
- 关联 G04:命名是否真的统一,靠 grep / build 验证,不靠感觉
- 关联 G10:白名单机制与"配置化优先"同源 —— 集合封闭才能防漂移
- 完整展开见 `docs/02_Architecture/AR09_对象命名规范.md`:判据 / 对象术语表 / 分层规范 / 机器守卫 G–J / 修复流程 / 现状问题登记 / 反例库
---
## G04 最高原则:测试与验收 — 「我说完成」必须靠观察的事实
1. **完成判定必须看 exit code,禁止仅看屏幕末尾文字**:命令后立刻 `echo $?` 或 `if [ $? -ne 0 ]`;链式命令必须确认每一段都退出 0。
2. **修改既存文件前必须 Read 整文件**,禁止凭印象 Edit。
3. **新依赖必须同步进 `go.mod` / `package.json`**:任何新 import 出现必检查此包是否在依赖清单中。
4. **测试/build 失败时禁止"再试一次"侥幸**:失败原因必须先找出来。
5. **声明完成前的最小验证清单**:
- [ ] 后端:`go build ./...` 无编译错误 且 `go test ./...` 通过
- [ ] 前端:`npm run build` exit 0
- [ ] API smoke:`/api/health` 返回 200,管理员 login 返回 200
- [ ] 任何新 import 在依赖清单中
6. **「测试套件全绿」不等于「功能跑通」**:单测覆盖的是代码路径,端到端要单独验证(curl / 浏览器 / 真实演示)。特别针对 UI:type-check + build 通过**不代表**用户能用,必须真起 dev server 在浏览器里点一遍。
7. **报告事实而非意图**:写"x 通过 y 测试"之前必须真跑过;不能写"应该可以工作""理论上没问题"。
8. **读清楚用户的 bug 描述,逐字理解,不凭经验脑补**:遇到歧义表述,先用自己的话 paraphrase 确认("你是说这个数字不该出现,还是说数字放错位置了?"),而不是直接动手。
- 规则:**用户描述 bug 时,默认用户是对的。先确信自己没看懂,不确信用户没说清。**
- 同理适用于"我没看懂你的意思"——这通常意味着回答里实现细节太多,需要换成平实语言重讲,而不是重复一遍原话。
9. **修 UI 显示 bug 必须追完整渲染链**:数据定义 → 数据构建 → 判断函数 → 模板条件,四个环节逐一确认,不能只改一环就宣布完成。遇到动态拼接的值(模板字面量 key、派生字段),先 Read 构建代码确认**实际格式**再写检查条件。
10. **跨区移动数据后,另一端的完整性必须显式验证**:从集合 A 移到集合 B 的条目,若后来又从 B 移除,必须检查要不要放回 A;改完 grep 一遍预期的 key 是否都在预期位置,`git diff` 扫一眼删除行是否多于预期。
- 这不是技术判断失误,是**注意力切换导致的数据完整性断裂**——修 bug 时只想着"删掉有问题的条目",没意识到这次删除附带"恢复到原处"的义务。
---
## G05 原则:安全迁移与重构流程 (Secure Migration)
1. **非简化原则**:重构不得以简化逻辑为目的,必须保留所有原始业务深度。
2. **实质性内容保护**:除非内容明确放错位置、存在重复副本或已经完成等价迁移验证,否则不得删除原有实质性内容与技术细节。
3. **全量备份**:大规模操作前,将原始文件完整备份。
4. **文件安全强制**:
- 文件扩展名白名单校验(仅允许 ppt/pptx/pdf/doc/docx/mp4/png/jpg/jpeg)
- 文件名重命名为存储 UUID,杜绝路径穿越
- 上传目录对静态预览只读,禁止直接执行
5. **开发阶段路由调整直接收口到新结构**:在开发阶段,信息架构或导航结构调整时,默认**不需要保留老路由**。除非用户明确要求兼容、灰度、外链保持可用,否则应直接删除旧路由与旧入口,避免同时维护新旧两套路由造成漂移与误判。
---
## G06 原则:AI 助手行为规范
1. **任务启动预读**:AI 助手在接收到新任务后的第一步操作中,必须读取本项目根目录下的 `TOP_CODING_RULES.md` 与 `eai_agentplatform/CLAUDE.md`。
2. **持续追踪**:直到问题完全解决并经由日志或接口验证通过前,不得结束任务。
3. **新对话启动仪式**:新对话必须先读 `eai_agentplatform/PROJECT_STATE.md` → `TOP_CODING_RULES.md`,再开始干活。
4. **发现规则与实现冲突时**,优先提醒并修正,不得静默忽略。
5. **Git 同步状态必须用 `-sb` 显式确认**:首次执行 git 相关操作时,必须使用 `git status -sb` 或 `git status -v` 而非简短版本,确保能清晰看到本地与远端的 ahead/behind 关系。禁止依赖 `git diff HEAD origin/HEAD` 单条链式命令判断,因其 `&&` 链在中间出错时会导致后续输出丢失。
6. **Git 检查必须指定分支名**:使用 `origin/<branch>`(如 `origin/main`)而非 `origin/HEAD`,避免 `origin/HEAD` 歧义。
7. **验证输出后再总结**:在给出 git 状态结论前,必须确认所有关键信息(ahead/behind、clean/dirty、branch)均已成功读取,不得在未确认完整性的情况下返回结论。
---
## G07 原则:交互控件可用态颜色统一规范
1. **按钮可用态统一蓝底**:所有可点击的关键交互按钮必须使用蓝色底(推荐 `#1677ff`)。
2. **按钮不可用态统一灰底**:所有不可点击按钮必须使用灰色底(推荐 `#cbd5e1`)与灰色文字(推荐 `#64748b`),并保持 `cursor: not-allowed`。
3. **状态变化必须实时联动视觉**:按钮的 `disabled` 状态变化后,底色必须立即同步变化。
4. **Element Plus 严格类型约束**:`el-tag` / `el-button` 的 type prop 禁止传入 `""` 或 `null`;无条件匹配时应传 `undefined`。
---
## G08 原则:分层清晰,禁止前后端职责串线
1. **前端只负责**:页面渲染、用户交互、表单收集、数据展示
2. **后端必须负责**:登录认证、权限校验、文件操作、AI 接口调用、业务判分逻辑、数据库操作
3. **前端不可以直接持有 JWT Secret 或 AI API Key** — 敏感凭据只在后端
4. **前端可以做格式和必填校验,但后端必须再次做强校验**
5. **所有权限以后端鉴权为准**:前端路由守卫仅作 UX 隐藏,不作为安全边界
---
## G09 原则:登录态与接口必须无状态化
1. **认证统一**:JWT(golang-jwt)+ bcrypt,角色只有 `employee` / `admin` 两级,不引入第三级与复杂 RBAC。
2. **每个受保护接口在入口层完成鉴权**:token 解析、用户识别、角色校验都在路由与中间件(`middleware.CurrentUser`)完成,不得下推到 service 层靠调用方自觉。
3. **API 默认无状态**:不在进程内保存登录态、会话、当前任务等跨请求状态;一切跨请求状态落库(`system_config` / 任务表 / 会话表),进程重启后行为一致。
4. **前端路由守卫只做 UX 隐藏**,不是安全边界;权限以后端判定为准(关联 G08)。
5. **无状态不等于无审计**:状态落库时必须带时间戳与归属(谁、什么时候改的),否则线上出问题只能靠猜。
### 关联
- 关联 G08:权限判定在后端,前端只负责隐藏入口
- 关联 G02:token 无效/缺失必须明确返回 401,不静默放行为匿名用户
---
## G10 原则:配置化优先,禁止写死环境细节
1. **禁止写死 `localhost`、固定端口、服务器绝对路径**:端口只在启动脚本里定义一次(`BACKEND_PORT=10232` / `FRONTEND_PORT=10231`),其它脚本引用它,禁止各处复制造成漂移。
2. **AI 相关的一切走配置**:LLM `base_url` / `api_key` / `model`、embedding 地址、JWT 密钥一律走 `system_config` 表 → `.env` 两层,缺失时按 G02 立即报错(501),不得硬编码默认地址兜底。
3. **可调参数不写进代码常量**:检索 top-k、切片长度、相似度阈值、积分规则这类"迟早要调"的值,集中放 `system_config` 或集中常量文件,调参不改代码、不重新编译。
4. **新增环境变量必须同步三处**:`.env` 样例(`backend-go/deploy/`)、启动脚本、`deploy/DELIVERY.md`。只改代码不改这三处,等于把坑留给下一次整盘克隆。
5. **禁止把"我这台机器能跑"的路径写进代码**:数据目录、日志目录、知识源目录都从配置派生——交付形态是裸进程 + systemd,换台机器路径就变。
### 关联
- 关联 G02:配置缺失 Fail Fast,不用默认值静默兜底
- 关联 G18:能拷到新机器跑,前提就是没有写死的环境细节
---
## G11 原则:素材必须可追溯,不可静默修改
1. **状态只前进、不跳变**:素材 `pending → approved / rejected`,不允许绕过审批直接改 `approved`。
2. **每一步留痕**:上传、转换、审批、入库、删除都要能回答"谁、什么时候、把它变成了什么状态";驳回必须写 `reject_reason`。
3. **已 `approved` 的素材原文件不可覆盖**:要改就新增素材/新版本,历史切片保留,检索结果能回溯到那份原文。
4. **AI 产出必须能对上来源**:一次 AI 调用记录 `ai_route_id` / `model` / `specialist_key`(`ai_call_log`),能回答"这句话是哪个专员、用哪个模型说的"。
5. **不做"看起来还在、其实已失效"的中间态**:删除就是删除(硬删或明确逻辑删),状态字段与前端展示必须一致,禁止靠前端过滤掩盖脏数据。
### 关联
- 关联 G02:审批状态未知时拦截,不默认放行
- 关联 P02 / P04:审批是入库转换的前置条件
- 关联 P06.7:删除与批量修改前先备份、先列清单
---
## G12 原则:长耗时任务必须异步化
1. **重活不占请求**:文档转换(LibreOffice → pdftotext)、批量提取、批量 embedding、批处理入库一律异步执行,API 层不得阻塞等待。
2. **提交即返回**:接口立刻返回任务标识与当前状态,进度靠状态查询;本项目固定用轮询(`GET /api/media/{id}/status`),不上 WebSocket。
3. **状态必须四态齐全**:`pending / running / success / failed`;`failed` 必须带失败原因,不允许只留一个没有下文的失败状态。
4. **异步失败不得静默**:日志 + 状态字段 + 前端可见,至少占两处;只在后台 print 一句不算。
5. **判断进度不要看 `worker_run.status`**:该字段恒为 `done`(见 P06.3),执行进度看任务自身的状态字段。
### 关联
- 关联 G01:异步任务每一步都要埋点,否则失败后无迹可查
- 关联 G11:状态变化要留痕,异步任务尤其
---
## G13 最高原则:先跑通主线,再细化和优化
1. **顺序硬规则**:主线端到端跑通(能演示)→ 修真实发现的 bug → 量级出现后才做性能优化。三段不可跳序。
2. **禁止"未来优化"占位代码**:不写 `// TODO: 上缓存` `// 后续换向量库` `// 假如有 10 万 QPS` 这类注释。理由:
- 优化的前提是先有真实流量画像,没量级先做 = 凭空猜
- 占位 TODO 会被反复重读但永远不做,变成纯认知负担
- 真要优化时,前面的代码已被改过 N 次,原 TODO 的假设大概率已失效
3. **决定优化前必须先算三笔账**:
- **当前量级**:用户数 / 单日调用次数 / 单次数据量
- **优化收益**:省了多少延迟 / 多少钱 / 用户体验差多少
- **运维成本**:新增依赖 / 新增分支路径 / 测试矩阵变大
- 收益 < 成本 → 不做,哪怕技术上很优雅
4. **本项目已拍板的「不做」清单**(不要重新提议):
- 不上 Milvus / FAISS / Elasticsearch(P02:brute-force 余弦够用)
- 不上 Celery / Redis 类外部队列(Go 侧 goroutine + 状态表够用)
- 不上 WebSocket(轮询够用)
- 不做图片 OCR(G02.6)、不做视频转码与 ASR(D06)
- 不做社交社区 / 讨论区(D19)
5. **正确做法**:当前阶段把主线打磨到能跑、能演示、能收集真实反馈;量级出现后(单日调用 >1 万 / 单文件 >5MB / 用户开始抱怨慢)再针对**真实瓶颈**优化,且优化必须有前后对比数据。
6. **AI 助手特别提示**:被"行业最佳实践""业内通用做法""理论上更优"诱导往优化方向走时,先回到本条算量级、收益、成本,再决定做不做。
### 关联
- 关联 G04:优化效果必须靠前后对比数据说话,不靠直觉
- 关联 P02:检索方案的边界已定,不在本条重复讨论
---
## G14 原则:Git 不抢戏 — 收工时统一提示一次
> dev 阶段 git 是后台事务,不是讨论焦点。
1. **不重点讨论**:不在回答里大段解释 commit / branch / rename 细节,一句话带过即可。
2. **不列为 TODO**:commit / push / 分支操作不进任务清单,不算未完成项。
3. **收工时提示一次**:一天工作结束时用一行说明当天有没有未提交改动、要不要提交;其余时间不主动提。
4. **用户显式要求时照做**:用户说提交就提交、说推送才推送 —— 本条只约束 AI 的主动行为。
5. **要提交就要提交得干净**:
- 一次提交只装一件事,多件事分开提交(例:功能改动与在制品清理分两次)
- **例外:dev 早期一包提交**(2026-09-26 用户指示)。项目还在早期时,粒度不是收益,
是打扰——**直接 `git add -A` 一包提交,不要停下来问用户「这次提交装哪些文件」**。
用户原话:「不做任何区分了,直接一包提交,这种事情以后不要干扰工作节奏」。
- 但「一包」不等于「闭嘴」:混合了哪些并行线,**必须在提交正文里逐条写明**
(下面第 3 条照旧生效)。粒度可以放宽,诚实不能放宽。
- 什么时候收回来:进入交付/需要回滚定位/多人协作时,再恢复按事拆分。
- 判据(比「阶段」更好操作):**如果为了让这次提交能编译,你得先测量一遍依赖锥,
那这些改动本来就是一个单元,不要拆。** 详见 `bugs_and_errors.md` E09。
- 提交信息写「为什么」,不只写「改了什么」
- 共享文件里混有并行改动、他人改动时,在提交正文里**如实写明**,不假装全是自己这次的改动
- 提交前扫一遍待提交内容有无密钥、大文件、运行期数据(参见 P06.10)
6. **状态核查按 G06.5-G06.7 执行**:`git status -sb`、显式指定 `origin/<branch>`,不用 `origin/HEAD`。
### 关联
- 关联 G13:git 卫生属于优化项,不阻塞主线
- 关联 P06.10:remote 里嵌明文凭据是提交前必查项
---
## G15 最高原则:禁止通用名启动入口文件,启动逻辑必须在 start_dev_10231_10232.sh
1. **禁止**在仓库里出现 `run.sh` / `start.sh` / `boot.sh` / `run.py` 这类通用名入口文件。
2. **唯一启动入口是 `start_dev_10231_10232.sh`**(Windows 侧为同名 `.ps1`):端口检查、占用进程处理、依赖 preflight、日志落盘(`debuglog/`)全部在这一层做。
3. **server 只做业务**:Go 后端不内置"帮你起前端 / 建库 / 改端口"的启动魔法;`go run` 绕过脚本时缺环境准备,属于个人排障手段,不得写进文档当作标准启动方式。
4. **确需封装时文件名必须带项目前缀**(如 `launch_eai.sh`),不可用通用名 —— AI 助手与新人搜索 "start/run" 时最先撞上的就是通用名文件,会绕过全部环境准备。
5. **为什么危险**:绕过端口检查与进程清理后,症状表现为"改了代码不生效""端口被占"这类查不出根因的怪事。
### 关联
- 关联 G18:启动脚本承担环境准备与自检
- 关联 P06.5:开发服务常驻 10231/10232,禁止按进程名模糊匹配杀进程
---
## G16 最高原则:Windows 侧 .ps1 脚本统一 UTF-8 with BOM
> **适用前提**:本仓库以 Ubuntu 为主(裸进程 + systemd),`start_dev_10231_10232.ps1` 是唯一的 Windows 侧脚本;本节只约束 `.ps1`,不影响 `.sh`。
1. **`.ps1` 必须保存为 UTF-8 with BOM**:Windows PowerShell 5.x 下,含中文注释/中文路径/中文输出的无 BOM 脚本会乱码、参数误读、解析失败。
2. **现状标注(2026-09-17)**:当前 `start_dev_10231_10232.ps1` 是**纯 ASCII、无 BOM**。一旦要往里加中文,必须**先补 BOM 再写中文**,不能直接存成无 BOM。
3. **注释优先用英文**:降低中文编码风险,也便于 AI 助手稳定编辑。
4. **执行策略被阻止时必须给出可运行命令**:使用说明同时提供 `.\start_dev_10231_10232.ps1` 与 `powershell -ExecutionPolicy Bypass -File .\start_dev_10231_10232.ps1`;禁止把"自己去改执行策略"当唯一方案。
5. **端口常量必须与 `.sh` 一致**:10231(前端)/ 10232(后端)在两侧脚本中保持同步,禁止某一侧临时改端口造成漂移(见 G10.1)。
### 关联
- 关联 G10:端口是配置,不是各脚本各写一份的常量
- 关联 G17:工具链与编码统一,同属可移植性
---
## G17 最高原则:脚本内禁止兼容式依赖回退,必须固定单一工具链
1. **一类工具只固定一种**:禁止写"有 pnpm 用 pnpm,没有就退回 npm/yarn"这类兼容分支。
2. **本项目前端固定 `npm`**(仓库内是 `package-lock.json`):安装 `npm install`、开发 `npm run dev`、构建 `npm run build`。凡文档、脚本、排障说明统一用 `npm`,不出现 pnpm / yarn 的第二套命令。
3. **后端固定 Go 工具链**:`CGO_ENABLED=0` 静态构建,不引入第二套构建方式。
4. **机器缺依赖就 fail fast**:缺 node / npm / go 直接报错并给出安装提示(`start_dev_10231_10232.sh` 的 preflight 就是这么做的),不许偷偷改走另一套工具链把流程"救活"。
### 关联
- 关联 G02:依赖缺失是错误状态,不是可回退状态
- 关联 G18:环境准备脚本幂等的前提是工具链唯一
---
## G18 最高原则:仓库应尽量支持拷贝后直接运行
1. **交付目标不是"作者机器能跑"**,而是"拷到另一台机器(或整盘克隆后)按文档和脚本能跑起来"。本项目交付形态是单二进制 + systemd + Clonezilla 整盘克隆(D14)。
2. **启动脚本必须做 preflight**:`start_dev_10231_10232.sh` 启动前检查 `go` / `node` / `npm` / `curl` / `lsof|ss`,缺哪个就报出安装命令并**整体失败**,不允许"检查失败也继续起"。
3. **补准备必须幂等**:已装则跳过、未装才装;启动脚本不得假设作者本机残留环境一定存在。
4. **交付物与文档对齐**:`backend-go/deploy/`(systemd unit、env 样例、清理脚本、`DELIVERY.md`)是交付事实源;改了启动方式 / 环境变量 / 数据目录,必须同步这里。
5. **禁止同一套依赖在多个脚本里重复安装**,只因为"这样比较保险"——重复安装迟早版本不一致。
### 关联
- 关联 G10:配置化是"拷了能跑"的前提
- 关联 G15 / G16 / G17:入口唯一、编码统一、工具链固定,同属可移植性
---
## G19 最高原则:外部下载物统一存放 `external_download/raw/`,一次下载永久复用
> 起因:本地 ASR 落地时下了 4.1GB 模型 + 一整套 CUDA 依赖(合计约 6GB)。
> 这些文件散在 `~` 和 `/tmp` 里时,下一个会话 / 下一个人只知道「磁盘少了 6G」,
> 不知道那是什么、能不能删、要不要重下 —— 于是又下一遍。
>
> 目录形状(`raw/` 是原始件,只读;`asr-local/` 是从它生成的加工件,可删可重建):
>
> ```
> external_download/
> ├── raw/
> │ └── asr-local/ 原始件 ★ 磁盘上只有这一份
> │ ├── README.md 来源 / 许可 / 清单
> │ ├── SHA256SUMS 逐文件校验和
> │ ├── models/ 模型权重(下下来那一刻的字节)
> │ └── wheels/ 依赖的 .whl(同属原始件)
> └── asr-local/ 加工件:解压 / 转换 / 配置后的可用件
> └── models/ 权重按原样引用;被改写过的 config.yaml 放这里
> ```
1. **一切从外部下载的原始文件必须放仓库根的 `external_download/raw/`**:模型权重、wheel、
数据集、字体、预编译二进制、第三方发行包都算。禁止散落在 `~`、`/tmp`、`/opt`
或各人自己的家目录里 —— 那些位置要么会被清理(`/tmp`),要么别人找不到(`~`)。
2. **原始件与加工件分开:`raw/` 只放下载下来的那套,解压 / 转换 / 配置的结果放 `raw/` 之外的
同级目录**(`external_download/raw/<批次>/` ↔ `external_download/<批次>/`)。
一个批次一个子目录,命名带用途与来源。
`raw/` 是**只读区** —— 里面存的必须是「下载下来那一刻」的字节,
任何程序都不得就地改写、解包、改名或生成中间产物。
加工件必须**可丢弃、可重建**(一条脚本能从 `raw/` 再生成一遍),否则它就成了第二份真本,
两边一旦不一致就没人说得清哪份是对的。
**大文件不复制**:加工件里没被改过的权重用软链指回 `raw/`,别为了「看起来完整」把 4GB 存两遍 ——
存两遍的结果是两份都会漂移。只有真正被改写过的文件(如指向 HF repo id 的 `config.yaml`
要改成本地路径)才在加工件里落成真实文件。
3. **每个批次必须有一份清单**(`raw/<批次>/README.md`,加工件目录里也放一份说明它是什么、
怎么重建),写明:
**下载时间、来源站点 / repo ID、版本或 commit、文件清单与大小、许可**。
没有清单的下载物按不可用处理 —— 没人知道它是什么,就没人敢删也没人敢用。
4. **每个批次必须有一份校验和清单**(`raw/<批次>/SHA256SUMS`),由下载/导出脚本在收尾时自动重算,
并可用 `sha256sum -c SHA256SUMS` 一条命令自证。**"文件还在"不等于"文件还是当初那份"** ——
被覆盖、拷坏、下到一半续传错位,只有校验和能发现(关联 G04:完成判定靠事实,不靠印象)。
校验和只算 `raw/`;加工件可重建,不需要单独记账。
5. **禁止重复下载**:动手下之前先 `ls external_download/raw/`。已有就用已有的。
真需要新版本时,**新开一个批次子目录**(`raw/` 与加工目录各一个),不要覆盖旧的
(关联 G11:不可静默修改)。
6. **下载脚本必须幂等且必须校验**:已存在且大小正确的跳过;每个文件校验
**HTTP 状态码 + 落盘字节数**,不完整即失败并报出是哪个文件。
禁止「下不到就跳过」式的静默兜底(关联 G02)。
7. **依赖要能离线重建**:Python 依赖除装进虚拟环境外,还须用 `pip download` 导出
wheelhouse 到该批次的 `raw/<批次>/wheels/`(`.whl` 是依赖的原始件,和模型同级)。
虚拟环境本身是加工件,留在工作区即可 —— 有 wheelhouse 就能离线重建它。
只留一个虚拟环境,换机器就得重下。
8. **`external_download/` 不进 git**:在仓库根 `.gitignore` 排除。它是本机资产库,不是源码;
体积以 GB 计,提交一次就永久留在历史里(关联 G14.5 提交前扫大文件)。
9. **不得放密钥**:清单里写来源 URL,不写 token;需要鉴权的下载把凭据放环境变量
(关联 G01 脱敏红线、P06.10)。
10. **许可必须记录并核对**:引入任何外部素材前先读 LICENSE,把许可写进清单。
特别注意:**HF 上的 gated(需登录/接受条款)不等于许可变更**,但通过镜像绕过 gated
拿到的文件,其许可要与官方来源核对后再写进清单(关联 P06.11)。
11. **与交付物划清边界**:`external_download/` 是本机开发资产,**不是交付物**。
交付走 `eai_agentplatform/backend-go/deploy/`(D14 单二进制 + systemd + 整盘克隆);
整盘克隆时本目录随镜像一起过去,这正是它要放仓库根、让脚本能定位到的原因。
### 反例与正例
| | 反例 ❌ | 正例 ✅ |
|---|---|---|
| A | `~/models/whisper/` 下 3GB 权重,无清单,作者离职后无人敢动 | `external_download/raw/asr-local/models/` + 同目录 `README.md` 写明来源与许可 |
| B | 换个会话发现「好像下过」,不确定,于是又下 3GB | 先 `ls external_download/raw/`,命中即复用 |
| C | 下载脚本 `curl -o f ... \|\| true`,静默跳过半个文件 | 校验 HTTP 码 + 字节数,不对就报出文件名并整体失败 |
| D | 只装了 venv,换机器重装时又拉一遍 2GB CUDA 库 | 同批次目录里带 wheelhouse,`--no-index --find-links` 离线重装 |
| E | 在 `raw/` 里就地解包、改名、跑脚本生成中间文件 | `raw/` 只读;加工结果写到同级目录,且一条脚本能重建 |
| F | 文件都在,就认为「还是当初那份」 | `sha256sum -c SHA256SUMS` 全 OK 才算数 |
| G | 为了「保险」把 4GB 权重在 raw 和加工目录各存一份,之后两边不一致 | 加工件里的权重用软链指回 `raw/`,只有改过的文件才落真实副本 |
### 关联
- 关联 G02:下载不完整是错误状态,不是可跳过状态
- 关联 G04:完成判定靠事实 —— 「文件还在」不等于「还是当初那份」,用 `SHA256SUMS` 自证
- 关联 G10:路径不写死在代码里,**由脚本从仓库根派生**
- 关联 G11:新版本新目录,不覆盖旧的
- 关联 G14:提交前扫大文件与密钥,本目录整目录排除
- 关联 G18:拷到新机器能跑,前提就是依赖有本地来源
- 关联 P06.11:引入第三方素材前先读 LICENSE
---
# 第二部分:eai_agentplatform 项目专用规则
> **适用范围**:仅适用于博昇 AI 数字员工平台项目。
> **使用方式**:本部分在通用规则之上叠加。若两者看似冲突,应先检查是否为项目专用规则对通用规则的场景化收敛。
## 索引
- P01:平台定位 — 通用数字员工平台(对齐 D20/D25/D26)
- P02:知识库安全与检索边界
- P03:考试判分与记录规则
- P04:素材上传与审批流程
- P05:AI PathCoach 对话安全边界
- P06:常见技术陷阱清单(本项目真实踩坑)
---
## P01 最高原则:平台定位 — 通用数字员工平台(对齐 D20/D25/D26)
1. 本项目是 **EAI 通用数字员工平台(Agent Platform)**:以「数字员工/专家、技能、长程 App」为核心对象,Chat 优先界面承载新建任务/对话,知识库作为组织级内建 App 有独立入口(对齐 D20「先通用、后定制」、D25 六层架构第 5 层对象层、D26 路由三类命名)。不再被描述为「博昇内部培训平台 / AI 对话机器人平台」。
2. **首批四类通用数字员工**(对齐 D20):知识运营、培训考试、内容生成、任务推进;后续再沉淀行业包与企业半定制。(原有产品知识/销售培训/考试等业务能力收敛为内部 App 注入其中。)
3. **业务对象统一为一级对象**(对齐 D21/D25/D26):Expert(专家/专员)、Skill(技能)、App(长程任务壳)。任务定义为「工作实例容器」,可挂载主 app、右栏 expert、后台 skill;消息流降级为对话轨迹,长程状态由 app 侧状态承载。
4. **知识库是组织级内建 App**(对齐 D25):素材审批 → 文本提取 → 知识切片 → 混合检索,有独立一级入口,不再只是后台能力。
5. 开始写某个模块前,先明确对应文档:
- `docs/04_Backend/BE*.md`(后端详细设计)
- `docs/06_Product_Lines/PL*.md`(产品原型)
- `docs/08_Design_Rules/DR*.md`(设计规则)
### 关联
- 关联 G08:前端不持有业务逻辑,AI PathCoach 调用必须走后端 API
- 关联 G02:配置/素材缺失必须报错,不静默兜底
---
## P02 最高原则:知识库安全与检索边界
1. **只存元数据,不存文件二进制**:数据库仅存储文件路径、大小、类型等元数据。
2. **素材文本存 knowledge_chunk 表**:审批通过的文档类素材经异步转换提取文本,切片写入。
3. **混合检索**:AI 知识检索 = 向量(Go 内 brute-force 余弦,Ollama bge-m3 embedding)+ 关键词兜底;数据量小无需 ANN/向量库(不上 Milvus/FAISS/Elasticsearch)。
4. **图片不进知识库**:png/jpg/jpeg 仅做存储预览,不做 OCR 提取,不进 AI 文本检索。
5. **审批前置**:素材只有 `approved` 状态才会触发异步转换;`pending` / `rejected` 的素材不进知识库。
6. **视频仅存储**:mp4 文件上传后仅做存储和预览,不做视频分析、不做帧提取。
### 关联
- 关联 G02:状态非 `approved` 的前端预览请求必须拦截
- 关联 G05:文件安全校验(扩展名 + UUID存储 + 只读预览)
- 关联 P04:审批是转换的前置条件
---
## P03 原则:考试判分与记录规则
1. **确定性判分**:单选/多选/判断题型采用确定性规则判分;**简答题(essay)除外**——按 D17 走后端 LLM 评分(复用 title_gen 路由、essay_grade 能力,不扣点数)。所有判分都在后端。
2. **判分逻辑必须在后端**:前端仅做选项展示和提交,不得在前端判分。
3. **自测 vs 正式考**:
- **自测(self_test)**:提交后立即显示正确答案 + 解析,不持久化成绩
- **正式考(formal)**:提交后判分落 `exam_record` 表,不显示正确答案
4. **考试试卷落库**:试卷为 DB 持久化配置(`exam_paper` 表),按配置从题库抽题,成绩与记录保存到 `exam_record`,不使用进程内字典兜底。
5. **正式考不可重做**:同一用户对同一正式考卷仅可提交一次(后端校验)。
---
## P04 原则:素材上传与审批流程
1. **管理员上传自动通过**:管理员(`role=admin`)上传的素材直接 `approved` + 立即触发异步转换。
2. **员工上传需审批**:员工上传后状态为 `pending`,管理员审批通过后才转 `approved`。
3. **驳回必须填写理由**:管理员驳回素材时,`reject_reason` 字段必填。
4. **异步转换管线**:审批通过 → 后台线程执行 `LibreOffice(ppt/docx→pdf) → pdftotext(提取文本) → 切片 → 写入 knowledge_chunk`。
5. **前端轮询状态**:前端通过 `GET /api/media/{id}/status` 轮询素材/转换状态,不使用 WebSocket。
6. **分片上传**:> 100MB 的视频文件走分片上传(init → chunk → complete)。
### 关联
- 关联 G02:状态/配置缺失必须报错
- 关联 G05:文件安全校验
- 关联 P02:审批是知识库转换的前置条件
---
## P05 原则:AI PathCoach 对话安全边界
1. **SSE 流式响应**:AI PathCoach 对话采用 SSE (Server-Sent Events),末包采集 token usage。
2. **上下文注入规则**:AI 回答基于知识库检索结果 + 当前页面上下文,禁止注入管理员凭据/内部配置。
3. **快捷动作限范围**:
- `scenario`(情景演练)— 使用预设 prompt 模拟客户对话
- `commission`(查佣金)— 检索产品佣金数据
- `compare`(产品对比)— 对比两个产品参数
4. **LLM 配置链**:system_config 表 → .env 文件两层优先级,缺失任何一项(base_url/api_key/model)抛 `501 LLMNotConfiguredError`。
5. **禁止功能**:AI PathCoach 不做图片生成、不做代码生成、不做外部 API 调用。
### 关联
- 关联 G02:配置缺失 Fail Fast(501),不静默兜底
- 关联 G01:AI LLM 调用完整 Payload + 返回正文必须埋点日志
- 关联 G08:AI 调用全程在后端,前端仅展示流式输出
---
## P06 最高原则:常见技术陷阱清单(本项目真实踩坑)
> 每条都来自本项目的真实事故或真实验证。遇到对应场景**先查本条,再动手**。
> 与 G13(先跑通主线)呼应:这里记的是"已经付过学费"的具体坑,不是设想。
### P06.1 GORM AutoMigrate 只加不删 —— 删列 / 改类型必须手写迁移
- **事实**:`store/db.go` 的 `AutoMigrate` 能自动加列(`specialist.rule_file_markdown`、`specialist.allowed_skills`、`ai_call_log.specialist_key` 三列已在开发库**副本**上实测加成功),但**不会**删列、不会改类型。
- **后果**:以为"改了 model 就完事",实际老列一直留在库里,代码与库结构悄悄分叉。
- **正确做法**:加字段 → 确认 model 在 `AutoMigrate` 列表里;删字段 / 改类型 → 写一次性迁移,或明确保留旧列并在文档里写明原因。
- **验证方式**:**改库结构后必须在数据库副本上跑一遍升级探测**,确认列真的加上、seed 真的填对,再动真库(动用户数据前先备份,见 P06.7)。
- **延伸一 · 删列会先撞上索引**:SQLite **拒绝删除仍被索引引用的列**,而 GORM AutoMigrate
**只建新索引、不清理旧模型遗留的索引**(如 `idx_skill_definition_role_kind`)。
于是手写的 `DROP COLUMN` 会以 `error in index ... after drop column` 失败,
**直接让 `store.Init` 崩在启动** —— 不是迁移没生效那么温和,是服务起不来。
正确做法:删列前先 `dropIndexesOnColumn` 把该列上的索引摘掉(`store/db.go` 的 `dropColumn` 已内置)。
- **延伸二 · 全局 `sed` 改名会改坏迁移本身**:改名时禁止 `sed -i 's/worker_/task_/g'` 式的全局替换 ——
`migrateLegacyTaskRuntimeSchema` 和它的迁移测试里出现的旧名**是必须保留的**
(它们的工作就是「认旧名、迁到新名」),改掉之后迁移永远不生效,而且没有任何报错。
判据一句话:**看这行是在「改」名字还是在「用」名字** —— 改名字的留旧名,用名字的改新名。
### P06.2 一次性探测脚本用完即删,别留成测试
- **事实**:为验证升级路径写过 `zz_upgrade_probe_test.go`,它第一步断言"这三列还不存在"。探测当时是对的,**但用户一重启服务,列就存在了,这个测试下次必红**。
- **规则**:断言"升级前状态"的探测代码是一次性工具,验完就删;留一个注定失败的测试比不留更糟——它会把"环境已经升级"误报成"代码坏了"。
- **正确做法**:要长期守护就写"升级**后**状态"的断言(列存在、seed 填对),而不是"升级前不存在"。
### P06.3 `worker_run.status` 恒为 `done`,别拿它当执行进度
- **事实**:`model/worker_run.go` 里 `Status` 的默认值是 `done`,后端没有任何一处写入别的值(全仓库只有 model 定义这一处出现该字段的赋值)。
- **后果**:以为能靠它判断"这一步跑完没有",实际永远是 done。
- **正确做法**:进度看任务自身的状态字段;run 与前后端的配对靠 `action_key` / `action_title`——前端 `specialistFlow.js` 的 `findLatestRun` 按两者之一匹配,`action_key` 由标题派生(`normalizeWorkerActionKey` / `normalizeActionKey`,两边规则必须一致)。
- **引申**:**配对字段来自中文标题,改一次文案就可能静默断链**——改标题必须同时确认配对没断。
### P06.4 种子数据只补空字段,不覆盖管理员改动
- **事实**:`seed.go` 的约定是"存在则只补空字段":`if existing.RuleFileMarkdown == "" { updates["rule_file_markdown"] = ... }`。
- **后果**:写成无条件 UPDATE,每次重启都会把管理员在后台手改的岗位说明书、技能绑定冲回种子值——用户改了半天,重启一次全白干。
- **守卫**:`seed_specialist_upsert_test.go` 用测试锁住这条约定,不要绕过它写无条件 UPDATE。
- **同理**:种子里的 key 拼错或重复必须在**启动时大声失败**(`applySpecialistRuleFiles` 就是这么做的),不能静默跳过。
### P06.5 开发服务常驻 10231/10232 —— 禁止按进程名模糊匹配杀进程
- **事实**:开发服务是常驻的,`pkill -f vite` / `pkill -f node` 这类模糊匹配会连用户正在用的那个一起杀掉。
- **正确做法**:按端口精确定位 PID(`lsof -ti:10231` / `ss -ltnp`)再杀单个 PID;`start_dev_10231_10232.sh` 已内置端口占用处理,优先用它。
- **引申**:任何"清理进程"的自动化脚本,默认按端口 / 精确 PID 定位,禁止按进程名模糊匹配。
### P06.6 Element Plus `persistent` 默认值会留下隐藏 DOM —— 曾因此误删真实任务
- **事实**:`el-dropdown` / `el-tooltip` 这类浮层组件默认 `persistent=true`,关闭后仍留一份节点在 DOM 里;任务行菜单曾因这个隐藏节点误删真实任务数据。
- **正确做法**:
- 这类组件一律显式 `:persistent="false"`(本项目 `MainLayout.vue` / `ProjectsPage.vue` / `ProjectDetailPage.vue` 已有先例与注释)
- 涉及删除的操作,脚本/代码自带数量校验:删几条、影响哪几条,先打印出来再执行
- **为什么**:隐藏 DOM 承载着真实数据的副本,"用户看不见"不等于"它不在"。
### P06.7 动用户数据前:先备份 → 再列清单 → 才动手
- **事实**:开发库里有用户的真实数据,不是可以随手重建的测试数据。
- **正确做法**:
- 动手前先备份(`data/backups/` 内置 `VACUUM INTO` 备份,D24;`cp` 主库在边服务边写时可能拷到写了一半的页)
- 把将要改动/删除的清单列出来(几条、哪些)再执行
- 需要探测结构变化时,**在副本上探测,不碰真库**
- **边界**:内置备份只防误删/误改/写坏,不防整盘损坏,跨机保存仍需人工。
### P06.8 前端路由是 hash 模式,改 hash 不会整页重载
- **事实**:路由用 `createWebHashHistory`。用 CDP / 自动化工具改 hash 时页面**不会**重新加载,于是出现"改了没生效"的假象,截图也可能截到旧状态。
- **正确做法**:改完 hash 显式 `Page.reload`,再截图 / 断言;用截图做验收前,先确认页面真的重载过。
### P06.9 `gofmt` 不要对整个目录 `-w`
- **事实**:仓库里有一部分文件在 HEAD 上本来就未格式化。直接 `gofmt -w internal/` 会把它们一起改,diff 里混进大量与本次任务无关的空行改动,评审时看不出哪行是真正改的。
- **正确做法**:先 `gofmt -l` 列清单,逐个确认该文件在 HEAD 上是否本来就干净;**只格式化本次真正改过的文件**,其余另开提交处理。
### P06.10 git remote 里禁止嵌明文凭据
- **事实**:本仓库的 remote URL 曾以 `https://用户名:令牌@域名/...` 形式保存明文令牌——任何能读这个仓库目录的人都能拿到它,`git remote -v` 也会把它打印出来。
- **正确做法**:凭据走 SSH key 或凭据管理器;发现已嵌入时,改 URL **并轮换该令牌**(只改 URL 不等于安全,令牌已经出现在历史输出与日志里)。
### P06.11 禁止引入第三方受限素材(Anthropic builtin-skills)
- **事实**:`builtin-skills/pdf/` 的 LICENSE 为 © 2025 Anthropic, PBC,含额外限制:禁止提取材料、禁止在服务之外保留副本、禁止复制、**禁止创作衍生作品**、禁止分发与再许可。
- **规则**:该目录目前已不在仓库中;**不得重新引入,也不得改写成"我们的 pdf 技能"**。需要 PDF 能力时自己实现或选许可允许的方案。
- **引申**:引入任何第三方素材/代码前,先读它的 LICENSE——"能用"和"能合法地用并交付"是两件事。
### P06.12 同一步的产物必须取「最后一次」的 run —— 否则用户对着作废产物点确认
- **事实**:取某一步的产出时如果用 `find()` / `[0]` 取**第一条** run,重跑之后「产出」区
仍然指向**上一版**。用户对着一份已经作废的名单点确认,而**界面上完全看不出这是旧的**。
- **后果**:不是显示错误,是**用户基于过期内容做了决定**。公众号面板与专员面板各自踩过一次。
- **正确做法**:取**同一步最后一次**跑出来的那条 run(`latestRunOfActionKey` / `latestArtifactOfStep`)。
凡是「同一步可重跑」的流程都适用,不限于这两个面板。
- **配套**:后端的闸门也不能只看「历史上有没有确认过」——那会拿**上一版**的身份去写新稿子;
闸门必须锚定到当前生效的那一版(`audio_speakers_gate_test.go` 有对应断言)。
- **为什么**:「重跑」在本平台是常态操作,而不是异常路径。
### P06.13 备份会自己毁掉备份 —— 三条自毁路径
- **事实**:内置备份(D24)有三处会让「保护数据的机制」反过来损害数据:
1. **主库不存在或是空的,仍然去备** —— 会拿一份**空快照顶掉保留窗口里的好备份**
(被人误删、或还没初始化时最危险,那时恰恰最需要旧备份)。正确做法:主库为空直接跳过。
2. **每次启动都备** —— 进程反复重启(crashloop)时每次产一份新备份,**把保留窗口撑爆**,
反而把有价值的旧备份挤出去。正确做法:走 `BackupIfDue`,按间隔补齐。
3. **解析备份文件名时去找「-2」** —— 时间戳 `20260914-200000` **本身就含 `-2`**。
正确做法:时间戳长度固定,按**长度**切前一段(`raw[:len(backupLayout)]`)。
- **为什么**:备份是「出事之后才用」的东西,它的失败**在正常路径上完全看不见**,
等真要用的时候才发现窗口里全是空快照 —— 这是最典型的「看起来完成了,其实没有」(G04)。
### P06.14 上游返回 HTTP 200 不等于这次调用成功
- **事实**:两种「200 但没用」的报文都真实出现过:
- **网关回 200,报文里却没有 `choices`**。实测背景:某技能第 3 步要连打 11 次模型
(逐字稿按 1200 字分块),**第 4 次**撞上这个,整步就此失败。
- **`finish_reason=length` + 半截正文** —— 改之前是**原样返回、落库成产物**,
用户拿到一份被截断的稿子,且系统认为它成功了。
- **正确做法**:
- 判成功要判**报文结构**,不能只判状态码;
- **重试与否必须由类型决定,不能由文案猜**:模型确实写不出正文(预算被思考吃光、拒答)
重发一百次都是同一结果,重试只是再花一次钱 —— 这类用 `EmptyCompletionError`;
协议/链路抖动(无 choices、限流、上游 5xx、连接被掐断)重发有机会成 —— 用 `TransientUpstreamError`。
- 超长导致的截断必须**报错**,报错要带上路由名与该路由的 `max_tokens`(否则不知道往哪调)。
- **引申 · 计费的真实性**:转写技能曾有「无论成败都写 `Success: true`」,**用量与计费都是假的**。
凡是要计点/计量的调用,成功标志必须来自真实结果,不能来自「函数返回了」。
### P06.15 同步长任务的超时上限,就是整套流程的真实长度上限
- **事实**:`audioSkill.js` 里 structure/minutes 的超时值**不是随手填的护栏,是这套流程的长度天花板**。
实测一条 26:41 的录音(11936 字 → 10 块):**全程 589.54s**,
原来的 5 分钟上限**已经被吃到 ≥89%**,稍慢一点就会被前端自己掐断。
- **后果**:前端超时了,**但后端还在跑、产物也已经落库** ——
用户看到的却是「失败」,然后重跑一次,产出一份重复的。
- **正确做法**:把超时当**容量参数**对待,按实测最长耗时的余量来定,不要凭感觉写整数;
更长的输入应当走异步(G12),而不是继续加超时。
- **为什么**:这是「前后端对同一件事的成功判定不一致」,与 G04 同源。
### P06.16 授权边界必须「失败即最小权限」
- **事实**:两条真实存在、且**方向相反**的边界约定:
- `GetByIDForOwners`:**owners 为空时查不到任何东西(而不是查到全部)** ——
归属标识缺失时必须退化成「什么都看不到」,**绝不能反过来退化成「看所有人的」**。
- 前端 `deleteTaskCascade`(按 id 级联删、**不校验归属**)与 `deleteMyTask`
(限定 owner、不级联)**两条不能合并**:侧边栏对所有人可见,
改调前者等于**给任何登录用户一个按 id 删任意任务的口子**;工作台只对管理员开放,
用不校验归属的那条才是它原本的语义。
- **正确做法**:权限参数缺失/为空时,默认落到**权限最小**的那一侧;
两条语义不同的删除路径**不要为了「统一」而合并**。
- **为什么**:这类代码在正常路径上永远是对的,只有边界输入才透光 —— 而边界输入正是攻击者的入口。
### P06.17 整盘克隆会把原型机的历史数据带到客户机上
- **事实**:原型机的 `data/backups/` 里会有**原型机自己的历史数据**(测试账号、演示素材)。
交付走整盘克隆(D14)时,这些会被**原样带到客户机器上**。
- **正确做法**:`clonezilla-cleanup.sh` 清理时一并**删空 `data/backups/`**,让客户机从干净状态开始。
- **为什么**:「删了主库」不等于「删了数据」—— 备份目录是数据的第二份副本,
清理脚本漏掉它,等于数据没清干净就交付了。
### P06.18 过滤条件与判定逻辑必须共用一份实现
- **事实**:`resolveChunkMeta` 只认已审批的来源(素材 `status=approved`、知识源 `audit_status=approved`)。
上游批量加载元信息时,**如果传了空 map,所有带 `media_file_id` / `knowledge_source_id` 的分片
会被整段丢弃** —— 检索候选集只剩「无指针」的那些,**不报错,只是结果悄悄变少**。
- **正确做法**:过滤条件与内部判定**共用同一个方法**(`ApprovedMetaMaps()`),
两处各自手写过滤条件必然漂移;且「过滤结果为空」与「本来就没有」必须能区分开。
- **为什么**:这是「两处实现同一个判据」的必然结局 —— 与 P06.12 / P06.14 同源:
**静默的错误比报错难查一个量级**。
### 关联
- 关联 G04:这些坑的共同点是"看起来完成了,其实没有"——完成判定必须靠事实
- 关联 G11 / P04:数据可追溯、只补空字段,同属"不可静默修改"
- 关联 G12:异步任务的失败与进度不能靠 `worker_run.status`
- 关联 G14:提交前扫密钥与大文件(P06.10)
- 关联 D14:交付走整盘克隆,清理必须覆盖 `data/backups/`(P06.17)
- **P06.12–P06.18 的共同形状**:失败不报错,只是**结果悄悄变少或变旧** ——
过期产物、空快照、无 `choices` 的 200、被丢弃的分片。
遇到「结果看着对但就是不对」时,先按这几条查,别从头推。
- **收录标准**:本清单只收「**以后还会再踩**」的坑 —— 工具/语言/框架的固有性质、
或换个场景就会复发的失误模式。**已经修完且不会以同样形状复发的过程性修复不收录**
(那些属于提交记录与代码注释)。一次性的报错、版本事故记在 `bugs_and_errors.md`。
---
*注:本准则放置于项目根目录,作为全局 Rule 永久锁定。*