跳转至

Pre-commit 质量门禁手册

本仓库使用 pre-commit 框架,在 git commit / git commit-msg 两个阶段自动运行 15 项质量检查,覆盖后端(Python)与三前端(agent-system / admin-system / infinitecanvas)。本文档逐一说明:每条规则的作用、对应代码与配置位置、代码做了什么、带来的好处、失败时如何处理,并附技术债治理流程。

配置入口:.pre-commit-config.yaml 技术债追踪:docs/debt/quality-gates.md


一、总览:门禁流水线

提交一次代码会依次经过以下两层(顺序即执行顺序):

┌──────────────────────────── git commit 阶段(file 阶段)───────────────────────────┐
│ ① trailing-whitespace      ② end-of-file-fixer   ③ check-yaml   ④ check-toml     │
│ ⑤ check-merge-conflict     ⑥ detect-private-key  ⑦ ruff(--fix)  ⑧ ruff-format   │
│ ⑨ bandit                   ⑩ file-length         ⑪ mypy        ⑫ gitleaks        │
│ ⑬ import-linter            ⑭ alembic-migration-safety           ⑮ frontend-check │
└───────────────────────────────────────────────────────────────────────────────────┘
                                       ↓
┌──────────────────────────── git commit-msg 阶段 ──────────────────────────────────┐
│ ⑯ commitlint(conventional commits 校验)                                          │
└───────────────────────────────────────────────────────────────────────────────────┘

执行原则

原则 说明
零配置可复现 所有规则版本钉死在 .pre-commit-config.yaml(如 ruff v0.8.4),团队成员所见即所得
暂存区感知 frontend-check 只对本次 git add 的前端目录运行,未改不跑,秒级反馈
自动修复优先 trailing-whitespace / end-of-file-fixer / ruff --fix / ruff-format自动改写文件并重新暂存,无需手动处理
不阻塞即警告 frontend-checkmax-lines-per-functionno-console 为 warn 级,不阻断提交
CI 兜底 本地未安装的工具(如 gitleaks)会跳过,但 GitHub Actions 的 gitleaks-action 仍会扫描

安装与首次运行

make install                # 含 pre-commit install(钩子写入 .git/hooks/)
# 或手动
poetry install
poetry run pre-commit install
poetry run pre-commit install --hook-type commit-msg   # 启用 commitlint
poetry run pre-commit run --all-files                   # 全量自检

重要commitlint 挂在 commit-msg 阶段,需单独 pre-commit install --hook-type commit-msg 才生效。仅装 pre-commit install 不会校验提交信息。


二、基础卫生(pre-commit-hooks 官方包)

来源:pre-commit/pre-commit-hooks rev: v5.0.0。这是一组零依赖、跨语言的通用卫生检查。

① trailing-whitespace — 删除行尾空格

  • 作用:删除所有文本文件行尾的多余空格与 Tab
  • 行为:自动改写文件(autofix),改完会重新 git add
  • 好处:消除无意义的 diff 噪音;避免某些老式编辑器/终端因行尾空格产生奇怪渲染;防止 merge 冲突
  • 失败处理:无需处理,已自动修复。提交即可

② end-of-file-fixer — 文件末尾确保一个换行

  • 作用:每个文件以恰好一个换行符结尾
  • 行为:自动改写文件
  • 好处:POSIX 规范要求文本文件以换行结尾;部分工具(wc、cat、shell 循环)读到无换行的末行会拼接;git diff 末行更清晰
  • 失败处理:自动修复

③ check-yaml — YAML 语法校验

  • 配置args: [--allow-multiple-documents] 允许 --- 分隔的多文档 YAML(CI/Compose 场景常见)
  • 作用:解析所有 .yaml / .yml,语法错误即失败
  • 好处:CI workflow、K8s manifest、docker-compose、MkDocs 配置在提交时就拦截语法错误,避免部署到 K8s 才发现 apiVersion 拼错
  • 失败处理:按报错行号修正缩进/冒号/引号

④ check-toml — TOML 语法校验

  • 作用:解析所有 .toml(重点是 pyproject.toml
  • 好处pyproject.toml 是 Poetry/Ruff/mypy/pytest/import-linter/bandit 的单一配置源,一个语法错误会让整条工具链瘫痪。提前拦截
  • 失败处理:修正 TOML 语法

⑤ check-merge-conflict — 拦截残留冲突标记

  • 作用:检测文件中是否含 <<<<<<< / ======= / >>>>>>> 冲突标记
  • 好处:防止开发者解决冲突时漏删标记,把 >>>>>>> HEAD 提交进代码
  • 失败处理:打开文件删除冲突标记,保留正确内容

⑥ detect-private-key — 拦截私钥提交

  • 作用:检测 PEM 格式私钥文件头(RSA / OPENSSH / EC 等的 BEGIN 行,下含 base64 密钥块),正则为 BEGIN 后跟 PRIVATE KEY
  • 好处:第一道防线,防止 SSH/AWS/GPG 私钥误入仓库(与 gitleaks 形成纵深防御)
  • 失败处理:私钥移出仓库,加入 .gitignore;若已提交需 git filter-repo 清理历史并轮换密钥

三、Python:Linter + Formatter(Ruff)

来源:astral-sh/ruff-pre-commit rev: v0.8.4。Ruff 用 Rust 实现,比 flake8 + isort + Black 快 10-100 倍,本项目用它一站式替代 Black / isort / flake8 / pyupgrade。

⑦ ruff(--fix)— Linter + 自动修复

代码 规则集 检查内容
E W pycodestyle PEP 8 风格(缩进、空格、行长 100)
F Pyflakes 未使用变量、未使用 import、重复定义、f-string 缺占位符
I isort import 排序与分组(first-party: backend/messaging/storage)
B bugbear 常见陷阱(可变默认参数、except: 裸捕获、循环变量泄漏)
C4 comprehensions 推导式简化(list(x)[x]dict(){}
UP pyupgrade 升级到现代 Python 语法(dict(){}Optional[X]X \| None
SIM simplify 布尔简化、嵌套 if 合并
RUF ruff-specific RUF010(显式转换标志)、RUF013(隐式 Optional)等
  • 忽略B008(FastAPI Depends() 在默认参数合法)、RUF001/002/003(中文日志/注释允许全角标点)
  • --fix 行为:能自动修的(如 import 排序、删未用 import)直接改写文件
  • 好处:统一代码风格、捕捉真实 bug(B 系列)、自动升级语法(UP 系列)、单工具替代多工具
  • 失败处理
  • 看错误码(如 F401 = 未用 import),多数可 poetry run ruff check --fix <file> 自动修
  • 逻辑类(B/SIM)需手动改代码
  • 误报可用 # noqa: <code> 行内豁免并写明原因

⑧ ruff-format — 代码格式化

  • 配置[tool.ruff.format]quote-style = "double"indent-style = "space"
  • 作用:统一引号、缩进、换行(等价 Black,但与 Ruff lint 共享解析器,零冲突)
  • 行为:自动改写文件
  • 好处:消灭"格式之争",全团队代码长得一模一样,diff 只剩逻辑改动
  • 失败处理:自动修复,无需介入

四、Python:安全扫描(Bandit)

⑨ bandit — Python 安全漏洞扫描

  • 配置args: [-c, pyproject.toml, -r, src] + pyproject.toml [tool.bandit]
  • 扫描范围:递归扫 src/(backend / messaging / storage / commons)
  • 排除tests(测试允许 assert)、.venv
  • 跳过B101(assert)——测试中合法
  • 检测项(节选):
规则 含义
B102 exec() 使用
B301/B302 pickle / marshal 反序列化(远程代码执行风险)
B602/B603 subprocess shell 注入
B311 random 用于密码学(应改用 secrets
B324 hashlib.md5 用于加密
  • pass_filenames: false:对全量 src/ 扫描,而非仅暂存文件(避免漏扫依赖文件)
  • 好处:在提交时拦截低级安全错误,避免 secret 硬编码、命令注入、弱随机数进生产
  • 失败处理:按报告修改;误报用 # nosec 行内豁免并注释原因

五、Python:文件长度(本地 hook)

⑩ file-length — 单文件有效行数 ≤ 500

  • 入口poetry run python scripts/check_file_length.py
  • 源码scripts/check_file_length.py
  • 算法
  • 扫描 .py / .ts / .tsx跨语言统一,前后端同一把尺)
  • count_effective_lines不计空行、不计整行注释(与 ESLint skipBlankLines/skipComments 对齐)
  • 超过 MAX_LINES = 500 即 FAIL
  • 豁免SKIP_REL_PATHS(仅 seed_data 配置数据)+ SKIP_DIRS(node_modules 等)
  • 技术债追踪子命令--list-debt 输出豁免清单及各文件当前行数,供 docs/debt/quality-gates.md 核对
  • 为什么 500:见 AGENTS.md「文件体量约定」——单文件 ≤ 400(推荐)/ 500(硬上限),单文件单职责,避免上帝对象
  • always_run: true + pass_filenames: false:全仓库扫描(不依赖暂存区),保证已提交的大文件也被持续监控
  • 好处:客观、跨语言的复杂度红线,强制拆分大文件 → 可读性、可测试性、可 review 性同步提升
  • 失败处理:按职责拆分文件(barrel index / 子组件 / utils 抽取),参考 docs/debt/quality-gates.md 的拆分案例

六、Python:静态类型检查(mypy)

⑪ mypy — 强制类型标注

  • 配置pyproject.toml [tool.mypy]
  • 全局python_version = "3.12"strict = trueplugins = ["pydantic.mypy"]
  • 检查范围packages = ["backend", "messaging", "storage"](含 Pydantic 模型字段类型校验)
  • 渐进式严格
  • backend.* / storage.*:暂放宽(strict = false),关闭 no-untyped-def 等,待类型补齐再收紧
  • messaging.*:全量 strict
  • 第三方无类型库(redis/testcontainers/qrcode 等):ignore_missing_imports = true
  • always_run: true:每次提交全量类型检查
  • 好处:类型即文档;IDE 补全更准;重构有编译期保障;Pydantic 模型字段类型错误提前暴露
  • 失败处理:按报错补类型标注;第三方库缺类型用 # type: ignore[<code>] 并注释原因

七、Python:密钥泄露扫描(gitleaks)

⑫ gitleaks — 暂存区密钥/Token 扫描

  • 入口poetry run python scripts/gitleaks_precommit.py
  • 源码scripts/gitleaks_precommit.py
  • 行为:调用 gitleaks protect --verbose --redact --staged
  • --staged:只扫暂存区(快)
  • --redact:日志中密钥部分打码
  • 未安装降级:本地没装 gitleaks 时打印警告并跳过(返回 0),不阻塞开发;CI 的 gitleaks-action 兜底
  • 检测能力:AWS AK/SK、阿里云 AK/SK、GitHub Token、私钥、Stripe Key、Slack Token 等数百种(内置规则库 + 正则)
  • 好处:纵深防御的第二道防线(配合 detect-private-key),防止 .envconfig.json、硬编码 token 进仓库
  • 失败处理
  • 移除密钥,改用环境变量 / .env(已 gitignore)
  • 误报在 .gitleaksignore 登记规则 ID
  • 已泄露:git filter-repo 清历史 + 立即轮换密钥

八、Python:架构分层约束(import-linter)

⑬ import-linter — 模块依赖方向约束

契约 含义 违反示例
backend API 不得依赖 db API 层只能经 Service/Repository 访问数据 from backend.db import X 出现在 backend/api/*.py
backend core 不得依赖 api 业务层不应反向调用路由 backend/core/*.py import backend.api
backend db 不得依赖 api/core ORM/Repo 层是底层,不应知上层存在 backend/db/models/*.py import backend.core
messaging 不得依赖 backend 消息组件是独立可复用包 messaging import backend
storage 不得依赖 backend OSS 微服务独立部署,AK/SK 仅在 storage storage import backend
storage core 不得依赖 api storage 内部分层 storage.core import storage.api
  • root_packagesbackend / messaging / storage / commons 四个根包
  • allow_indirect_imports = true:仅校验直接 import(避免误伤 __init__ re-export 链)
  • always_run: true:每次提交全量校验依赖图
  • 好处:强制分层架构(API → core → db 单向),防止循环依赖、防止跨域走私、保证微服务独立性
  • 失败处理:按违反方向重构——把共享逻辑下沉到 commons 或正确层级,不要用 # noqa 绕过

九、Python:数据库迁移安全(Alembic)

⑭ alembic-migration-safety — 拦截高风险 DDL

  • 入口poetry run python scripts/check_alembic_migrations.py
  • 源码scripts/check_alembic_migrations.py
  • 触发条件files: ^alembic(_backend)?/versions/.*\.py$(只在该改迁移时跑)
  • 扫描目录alembic/versions + alembic_backend/versions
  • 拦截的高危操作(在 upgrade() 函数体内):
模式 风险
op.drop_column( 直接删列会丢数据,须先停写、迁移、删列三步走
op.drop_table( 同上
op.alter_column(..., type_=...) 改列类型无数据迁移会截断/报错
  • 豁免机制:行尾加 # alembic:allow-danger: 原因(必须写原因)
  • 配套规范docs/standards/database-migrations.md
  • 好处:PostgreSQL 大表在线 DDL 事故绝大多数源于直接 drop/alter,此门禁强制"安全三步走",保数据不丢
  • 失败处理
  • 改为零停机方案(加新列 → 双写 → 迁移 → 切换 → 删旧列)
  • 确需危险操作:行尾加 # alembic:allow-danger: <充分理由>,并在 PR 中说明

十、前端质量门禁(本地 hook)

⑮ frontend-check — 三前端统一门禁调度

  • 入口poetry run python scripts/frontend_precommit.py
  • 源码scripts/frontend_precommit.py
  • 触发条件files: ^src/(agent-system|infinitecanvas|admin-system)/(仅当前端文件被暂存)
  • 声明式架构:用 FrontendTarget dataclass 统一描述,避免三份 if/elif

暂存区感知(关键优化)

读取 git diff --cached --name-only只有暂存文件命中某前端前缀,该前端才执行。改后端只跑后端门禁,改 admin-system 只跑 admin-system,互不干扰,反馈秒级。

三前端步骤矩阵

前端 runner typecheck lint format-check arch
agent-system pnpm
admin-system pnpm
infinitecanvas npm

test 因较慢(秒级→分钟级)不进 pre-commit,由 make *-check 与 CI 负责。

各步骤详解

typecheckpnpm typechecktsc --noEmit): - TypeScript 编译期类型检查,无输出文件 - 拦截类型错误、缺失 import、隐式 any - 好处:前端也有"编译期"保障

lintpnpm lint → ESLint flat config): - 配置:src/admin-system/eslint.config.js(admin-system 示例) - 核心规则:

规则 级别 作用
max-lines: 500 error 单文件 ≤ 500 有效行(与后端 file-length 对齐,双保险)
max-lines-per-function: 80 warn 函数 ≤ 80 行(超长提示,不阻断)
no-console warn 生产代码不应留 console.log
@typescript-eslint/no-unused-vars error 未用变量(_ 前缀豁免)
react-hooks/rules-of-hooks error Hooks 使用规范

format-checkpnpm format-check → Prettier --check): - 配置:各前端 .prettierrc.json - 统一引号、缩进、尾逗号,与 Ruff format 对应 - 失败处理:pnpm exec prettier --write <file>

archpnpm arch → dependency-cruiser): - 配置:src/admin-system/dependency-cruiser.config.mjs - 三条契约:

规则 含义
features-no-cross-import features 之间禁止互相 import(同 feature 内子目录允许)
shared-not-features shared 层不得依赖 features(避免反向依赖)
no-direct-axios axios 仅允许在 shared/api/httpClient.ts / adminHttpClient.ts 中使用,强制 HTTP 请求走统一客户端
  • 正则技巧features-no-cross-importfrom: '^src/features/([^/]+)/' 捕获 feature 名,to.pathNot: '^src/features/$1/' 排除自身($1 反向引用),实现"禁止跨 feature、允许同 feature 内部"
  • 好处:前端也有架构红线,防止 features 耦合、防止散落的 fetch/axios 绕过统一拦截器(鉴权、错误处理、日志)

  • 失败处理:按步骤标签(如 admin-system lint)定位,进入对应前端目录运行 pnpm <step> 看详情


十一、提交信息规范(commitlint)

⑯ commitlint — Conventional Commits 强制

  • 阶段commit-msg(与上面 15 个 hook 不同阶段,需 pre-commit install --hook-type commit-msg
  • 入口npx --no-install -- @commitlint/cli --edit
  • 配置commitlint.config.cjs + package.json(根级 devDependencies)
  • 继承@commitlint/config-conventional

格式

<type>(<scope>): <subject>
↑          ↑         ↑
必需      可选      必需

type 白名单

type 含义
feat 新功能
fix 修复 bug
refactor 重构(非新功能、非修 bug)
perf 性能优化
style 格式调整(不改逻辑)
test 测试相关
docs 文档
build 构建系统/依赖
ci CI 配置
chore 杂项(不修改 src 或 test)
revert 回滚提交

本仓库自定义放宽

  • subject-case: [0]:关闭首字母大小写限制(中文 subject 无大小写概念)
  • header-max-length: 100:首行 ≤ 100 字符
  • 兼容 feat!: 破坏性变更标记、feat(admin): scope

示例

git commit -m "feat(admin): 拆分 AiModelsPage 为 3 个子组件"
git commit -m "fix: 修复 OSS 批量删除空指针"
git commit -m "refactor(api): adminApi 改为 barrel index"
git commit -m "docs: 补充 pre-commit 门禁手册"
  • 好处
  • 提交历史可机器解析 → 自动生成 changelog
  • 配合 GitHub Actions(auto-merge.yml)按 type 路由
  • 强制写清"这次改了什么类别的什么"
  • 历史迁移:仓库早期用 [FIX] / [FEAT] 方括号风格,自 commitlint 引入起统一为 conventional,历史不回填
  • 失败处理:按提示改首行前缀为合法 type

十二、门禁分层与 CI 关系

层级 触发 速度 覆盖 跳过项
pre-commit(file) 每次 git commit 秒-十秒 全部门禁(不含 test)
pre-commit(commit-msg) 每次提交信息 毫秒 commitlint
make check 手动/CI 分钟 后端全量(含 test)
make *-check 手动/CI 分钟 各前端全量(含 test + build)
GitHub Actions push/PR 分钟 全量 + gitleaks-action + pip-audit

设计意图:pre-commit 追求快与拦截低级错误,test/build/audit 等"重活"下沉到 make 与 CI,避免每次提交等数分钟。


十三、技术债治理流程

门禁不是"设了就完",豁免清单会随业务演进。本项目建立单一来源 + 文档追踪 + 定期核对三位一体机制。

13.1 单一来源原则

历史上文件长度豁免有两份清单(check_file_length.pySKIP_REL_PATHS + eslint.config.jsmax-lines overrides),易漂移。2026 年治理后业务豁免全部清空,仅留 seed_data 配置类。

场景 唯一来源
Python + TS/TSX 文件长度 scripts/check_file_length.pySKIP_REL_PATHS
前端 ESLint 额外规则 eslint.config.js(不再用 overrides 豁免行数)

13.2 技术债追踪文档

所有豁免必须登记在 docs/debt/quality-gates.md,含:文件、原因、负责人、预计消除时间。未登记的豁免视为违规。

13.3 自动核对

poetry run python scripts/check_file_length.py --list-debt

输出豁免清单 + 各文件当前有效行数 + 状态(OK / OVER)。建议每月核对一次,已达标项立即移除豁免。

13.4 新增豁免流程

  1. 评估必要性:能否拆分/重构消除,而非豁免?
  2. 登记:在 docs/debt/quality-gates.md 对应章节写明文件、原因、负责人、预计消除时间
  3. 配置:在 SKIP_REL_PATHSeslint.config.js overrides 添加条目
  4. PR 说明:在 PR 描述中说明豁免理由与消除计划
  5. 定期回顾:每月 --list-debt 核对,达标即移除

13.5 消除豁免流程(拆分大文件)

参考 2026 年完成的 6 个大文件拆分案例(详见 docs/debt/quality-gates.md 第一节):

拆分手法 适用场景 案例
barrel index 聚合层 API 文件 adminApi.tsadmin/ 下 15 域文件 + barrel re-export
子组件抽取 单大组件 AiModelsPage → 主页 + FormPanel + EditRow + KeyRow
Tab 拆分 多 Tab 页面 SystemConfigPage → 3 个 Tab 组件
utils 抽取 纯函数混在组件 flattenContentTree / buildProductTree / maskKey
shared 常量 多组件共用常量 panelShared.tsx(IconEdit + PANEL_TITLES)

每步拆分后立即跑 typecheck + lint + format-check + arch 验证,确认无回归再继续下一步。


十四、常见问题

Q1:pre-commit 很慢怎么办?

  • 后端文件改动只跑后端 hook(frontend-check 因暂存区不匹配会跳过)
  • 前端改动同理
  • 若首次 --all-files 慢属正常(全量扫描),日常 commit 是增量的

Q2:如何临时跳过某个 hook?

git commit --no-verify    # 跳过所有 hook(不推荐,CI 仍会检查)
SKIP=mypy git commit      # 跳过指定 hook(仅本地)

警告--no-verify 绕过的代码若 CI 失败仍会 block 合并。仅用于 WIP 快照提交。

Q3:gitleaks 报警告"未找到可执行文件"

本地未装 gitleaks,hook 自动跳过(返回 0)。CI 的 gitleaks-action 会兜底。安装见 gitleaks 官网

Q4:commitlint 没生效?

需单独安装 commit-msg 钩子:

poetry run pre-commit install --hook-type commit-msg

pre-commit install 不会启用 commitlint。

Q5:ruff 和 ruff-format 冲突?

不会冲突。两者共享 Ruff 的解析器,ruff-format 是 Black 兼容的格式化器,ruff --fix 只做 lint 修复(如删未用 import),分工明确。

Q6:如何添加新 hook?

  1. .pre-commit-config.yaml 添加 repo / hooks
  2. 本地 hook(repo: local)需把脚本放 scripts/ 并写 entry
  3. 运行 poetry run pre-commit run <hook-id> 验证
  4. 更新本文档对应章节

十五、速查:失败退出码对照

Hook 自动修复 常见失败原因 修复命令
trailing-whitespace 自动
end-of-file-fixer 自动
check-yaml YAML 语法 手动改
check-toml TOML 语法 手动改
check-merge-conflict 残留 <<<<<<< 手动删
detect-private-key 含私钥 移出仓库
ruff (--fix) ✅ 部分 lint 规则 ruff check --fix
ruff-format 格式 自动
bandit 安全规则 改代码 / # nosec
file-length 超 500 行 拆分文件
mypy 类型错误 补类型标注
gitleaks 含密钥 移除密钥
import-linter 跨层依赖 重构分层
alembic-migration-safety 高危 DDL 改三步走 / alembic:allow-danger
frontend-check typecheck/lint/format/arch 进前端目录 pnpm <step>
commitlint 非 conventional 改提交信息首行

十六、参考