Files
pj0235-eai_agentplatform/TOP_CODING_RULES.md
T
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

60 KiB
Raw Blame History

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. 两条已收敛的规律,守住不回退:
               数据库表名            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 永久锁定。