前端 Harness 工程化 - Claude Code 工作流增强方案
前端 Harness 工程化 - Claude Code 工作流增强方案
基于 Claude Code Hook 机制构建的六层质量防御体系,让 AI 编码更加规范、可控、可靠。
📖 前端react Harness
一套为 Claude Code + React + TypeScript 项目量身定制的工程化增强方案,通过 Claude Code 的 Hook 机制 在关键节点插入质量检查,规范 AI 编码行为,从源头减少低级错误。
核心设计理念(六层防御)
| 层级 | 机制 | 作用 |
|---|---|---|
| 第一层 | 危险操作拦截 | PreToolUse 拦截 rm -rf /、强制推送到 main、DROP DATABASE 等危险操作 |
| 第二层 | 书写规范校验 | PostToolUse 每次写入文件后自动校验是否符合项目规范 |
| 第三层 | 技能强制评估 | PostToolUse 写完代码后强制匹配激活对应Skill,确保规范真的被执行 |
| 第四层 | 上下文智能注入 | 根据操作文件路径自动注入对应规范到对话上下文 |
| 第五层 | 结束前验证 | Stop Hook 在每次回答结束强制进行五项质量门检查 |
| 第六层 | 专业化分Agent | 针对不同场景使用专门的 Subagent 处理(代码审查、性能审计等) |
🚀 从零开始配置
前置要求
- 已安装 Claude Code CLI
- React + TypeScript 项目
- 项目已遵从本文所述规范
第一步:创建目录结构
在项目根目录创建 .claude/ 目录,结构如下:
.claude/
├── settings.json # Claude 设置(Hook 配置)
├── CLAUDE.md # 项目级规范说明
├── hooks/ # Hook 脚本
│ ├── inject-context.js # 上下文注入
│ ├── validate-frontend.js # 前端规范验证
│ ├── block-dangerous-ops.js # 危险操作拦截
│ └── skill-forced-eval.js
├── rules/ # 路径规则(按路径注入不同规范)
│ ├── component-rules.md
│ ├── api-rules.md
│ ├── style-rules.md
│ └── typescript-rules.md
├── context/
│ └── fe-conventions.md # 完整前端规范(SessionStart 注入)
├── agents/ # 自定义 Subagent
│ ├── component-analyzer.md
│ ├── api-checker.md
│ ├── code-reviewer.md
│ └── performance-auditor.md
├── commands/ # 自定义 Slash 命令
│ ├── dev.md
│ ├── check.md
│ ├── crud.md
│ └── code-format.md
└── skills/ # 自定义 Skills
├── ui-pc/SKILL.md
├── store-pc/SKILL.md
└── ...
你可以直接从本项目复制所有文件到你的项目:
# 如果你面前已经有一个配置好的项目,可以直接复制
cp -r .claude /path/to/your-project/
第二步:配置 settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-frontend.js\"",
"timeout": 30,
"statusMessage": "验证前端规范..."
},
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/skill-forced-eval.js\"",
"timeout": 10,
"statusMessage": "技能匹配检查..."
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous-ops.js\"",
"timeout": 10
}
]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-context.js\"",
"statusMessage": "重新注入前端规范..."
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "结束前请验证:(1) 所有请求的功能已实现并测试。(2) 新代码中无 TODO/FIXME 注释遗留。(3) CLAUDE.md 的当前迭代状态已更新。(4) 生产代码中无 console.log 或 any 类型。(5) pnpm run lint 通过。如有未完成项,告知用户但不要重新开始工作。",
"model": "claude-haiku-4-5-20251001"
}
]
}
]
}
}
使用小模型(Haiku)运行 Stop Hook 验证,节省大模型 token。
第三步:复制 Hook 脚本
hooks/block-dangerous-ops.js - 危险操作拦截
async function main() {
if (process.stdin.isTTY) {
process.exit(0);
}
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
let input;
try {
input = JSON.parse(chunks.join(''));
} catch {
process.exit(0);
}
const cmd = (input?.tool_input?.command || '').trim();
const blocked = [];
// Rule 1: Block rm -rf on non-node_modules/dist paths
if (/rm\s+(-\w*r\w*f\w*|--recursive\s+--force)\s+/.test(cmd)) {
if (!/node_modules|dist|\.tmp/.test(cmd)) {
blocked.push('Dangerous rm -rf detected (only allowed for node_modules/dist)');
}
}
// Rule 2: Block git force push to main/master
if (/git\s+push.*--force/.test(cmd)) {
if (/main|master/.test(cmd)) {
blocked.push('Force push to main/master is blocked');
} else if (!/feature|fix|bugfix|develop|release/.test(cmd)) {
blocked.push('Force push detected -- verify target branch is not main/master');
}
}
// Rule 3: Block dropping databases
if (/\bdrop\s+(database|schema)\b/i.test(cmd)) {
blocked.push('DROP DATABASE/SCHEMA is blocked');
}
if (blocked.length > 0) {
blocked.forEach(msg => console.error(`BLOCKED: ${msg}`));
process.exit(2);
}
process.exit(0);
}
main().catch(() => process.exit(0));
拦截策略:只允许对 node_modules、dist、.tmp 执行 rm -rf,禁止直接删除项目源代码。
hooks/validate-frontend.js - 前端规范自动验证
import { readFileSync } from 'node:fs';
import { extname, basename, sep } from 'node:path';
const SEP = sep === '\\' ? '\\\\' : '/';
function isComponentFile(filePath) {
return new RegExp(`src${SEP}components${SEP}|src${SEP}pages${SEP}`).test(filePath)
|| /src[\\/]components[\\/]/.test(filePath)
|| /src[\\/]pages[\\/]/.test(filePath);
}
function isApiOrStoreFile(filePath) {
return /src[\\/]api[\\/]/.test(filePath) || /src[\\/]stores[\\/]/.test(filePath);
}
function isLegacyCss(filePath) {
return /(?:App|index)\.css$/.test(filePath);
}
async function main() {
if (process.stdin.isTTY) {
process.exit(0);
}
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
let input;
try {
input = JSON.parse(chunks.join(''));
} catch {
process.exit(0);
}
const filePath = input?.tool_input?.file_path || '';
const ext = extname(filePath);
if (!['.ts', '.tsx', '.less', '.css'].includes(ext)) {
process.exit(0);
}
let content;
try {
content = readFileSync(filePath, 'utf8');
} catch {
process.exit(0);
}
const errors = [];
const warnings = [];
// TypeScript: always run base checks
if (['.ts', '.tsx'].includes(ext)) {
validateTypeScript(content, filePath, errors, warnings);
if (isComponentFile(filePath)) {
validateComponent(content, filePath, errors, warnings);
}
if (isApiOrStoreFile(filePath)) {
validateApiLayer(content, filePath, errors, warnings);
}
}
// Styles: run expanded checks
if (['.less', '.css'].includes(ext)) {
validateStyles(content, filePath, errors, warnings);
}
if (errors.length > 0) {
console.error(`=== Frontend validation FAILED: ${basename(filePath)} ===`);
errors.forEach(e => console.error(` CRITICAL: ${e}`));
warnings.forEach(w => console.error(` WARNING: ${w}`));
process.exit(2);
}
if (warnings.length > 0) {
console.error(`Warnings in ${basename(filePath)}:`);
warnings.forEach(w => console.error(` ${w}`));
}
console.log(`Frontend validation passed: ${basename(filePath)}`);
process.exit(0);
}
// --- Spec 1: ESLint-level TypeScript checks ---
function validateTypeScript(content, filePath, errors, warnings) {
const lines = content.split('\n');
lines.forEach((line, i) => {
const lineNum = i + 1;
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
return;
}
if (/: any\b/.test(trimmed) || /as any/.test(trimmed) || /<any>/.test(trimmed)) {
errors.push(`Line ${lineNum}: Forbidden 'any' type usage`);
}
if (/console\.log/.test(trimmed) && !/\.(test|spec)\./.test(filePath)) {
warnings.push(`Line ${lineNum}: console.log found (remove before production)`);
}
if (/\bvar\s+\w/.test(trimmed)) {
errors.push(`Line ${lineNum}: Use const/let instead of var`);
}
if (/(@ts-ignore|@ts-nocheck)/.test(trimmed)) {
errors.push(`Line ${lineNum}: Forbidden @ts-ignore or @ts-nocheck`);
}
});
}
// --- Spec 2: Component Development checks ---
function validateComponent(content, filePath, errors, warnings) {
const fileName = basename(filePath);
// PascalCase file name check
if (!/^[A-Z][A-Za-z0-9]*\.tsx$/.test(fileName)) {
errors.push(`Component file must be PascalCase.tsx (got: ${fileName})`);
}
const lines = content.split('\n');
lines.forEach((line, i) => {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('*')) return;
if (/style=\{\{/.test(line)) {
errors.push(`Line ${i + 1}: Forbidden inline style (style={{}})`);
}
// No key={index}
if (/key=\{(?:i|idx|index)\}/.test(line)) {
errors.push(`Line ${i + 1}: Forbidden key={index} in list rendering`);
}
});
// Props interface must end with "Props"
const propsMatch = content.match(/interface\s+(\w+)\s*\{/g);
if (propsMatch) {
propsMatch.forEach(m => {
const name = m.match(/interface\s+(\w+)/)[1];
if (name && !name.endsWith('Props')) {
errors.push(`Props interface must end with "Props" (got: ${name})`);
}
});
}
}
// --- Spec 4: API/Data Layer checks ---
function validateApiLayer(content, filePath, errors, warnings) {
const lines = content.split('\n');
lines.forEach((line, i) => {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) return;
// API endpoint must use /api prefix
if ((/(?:fetch|axios|request)\s*\(\s*['"`]/.test(trimmed) || /['"`]\s*\+\s*['"`]/.test(trimmed))) {
const urlMatch = trimmed.match(/['"`](\/[^'"`]*?)['"`]/);
if (urlMatch && !urlMatch[1].startsWith('/api')) {
errors.push(`Line ${i + 1}: API endpoint must start with /api (got: ${urlMatch[1]})`);
}
}
});
// Store file naming: must be useXxxStore.ts
if (/stores[\\/]/.test(filePath)) {
const fileName = basename(filePath);
if (!/^use[A-Z]\w*Store\.ts$/.test(fileName)) {
errors.push(`Store file must match useXxxStore.ts pattern (got: ${fileName})`);
}
// Must use zustand create()
if (!/from\s+['"]zustand['"]/.test(content)) {
errors.push('Store file must import from "zustand"');
}
}
}
// --- Spec 3: Design System/Style checks ---
function validateStyles(content, filePath, errors, warnings) {
if (isLegacyCss(filePath)) {
const lines = content.split('\n');
lines.forEach((line, i) => {
if (/!important/.test(line) && !line.trim().startsWith('/*') && !line.trim().startsWith('*')) {
warnings.push(`Line ${i + 1}: !important found (should use specificity or justify with comment)`);
}
});
return;
}
const lines = content.split('\n');
lines.forEach((line, i) => {
const trimmed = line.trim();
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return;
if (/!important/.test(line)) {
warnings.push(`Line ${i + 1}: !important found (should use specificity or justify with comment)`);
}
// No hardcoded hex colors (outside variable definitions)
const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\b/);
if (hexMatch && !/^\s*@/.test(trimmed) && !/^\s*--/.test(trimmed)) {
errors.push(`Line ${i + 1}: Hardcoded hex color "${hexMatch[0]}" — use a Less variable instead`);
}
// No z-index literals (should use variable)
const zIndexMatch = line.match(/z-index\s*:\s*(\d+)/);
if (zIndexMatch && !/@/.test(trimmed) && !/var\s*\(/.test(trimmed)) {
warnings.push(`Line ${i + 1}: z-index literal "${zIndexMatch[1]}" — use a Less variable instead`);
}
// Magic number pixel values (skip 0px, 1px, 100%)
const pxMatches = line.match(/\b(\d+)px\b/g);
if (pxMatches && !/^\s*@/.test(trimmed) && !/^\s*--/.test(trimmed)) {
const problematic = pxMatches.filter(m => parseInt(m) > 1);
if (problematic.length > 0 && !/calc\(/.test(trimmed)) {
warnings.push(`Line ${i + 1}: Magic pixel values ${problematic.join(', ')} — use Less variables from variables.less`);
}
}
});
}
main().catch(() => process.exit(0));
验证维度:
| 类别 | 检查项 | 级别 |
|---|---|---|
| TypeScript | any 类型 | Error |
| TypeScript | var 声明 | Error |
| TypeScript | @ts-ignore | Error |
| TypeScript | console.log(非测试) | Warning |
| 组件 | 文件名 PascalCase | Error |
| 组件 | 内联样式 style={{}} | Error |
| 组件 | key={index} | Error |
| 组件 | Props 接口以 Props 结尾 | Error |
| API | 接口以 /api 开头 | Error |
| Store | 文件名 useXxxStore.ts | Error |
| Store | 从 zustand 导入 | Error |
| 样式 | 硬编码十六进制颜色 | Error |
| 样式 | 魔法像素值 | Warning |
| 样式 | 字面量 z-index | Warning |
| 样式 | !important 无注释 | Warning |
hooks/inject-context.js - 上下文智能注入
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
async function main() {
const projectDir = process.env.CLAUDE_PROJECT_DIR;
if (!projectDir) {
process.exit(0);
}
const conventionFile = join(projectDir, '.claude', 'context', 'fe-conventions.md');
if (existsSync(conventionFile)) {
const content = readFileSync(conventionFile, 'utf8');
console.log(content);
}
process.exit(0);
}
main().catch(() => process.exit(0));
在 Session 启动(compact 后)自动注入完整前端规范到上下文。
第四步:配置路径规则注入
在 rules/ 目录下,每个规则文件以 --- frontmatter 开头,声明匹配路径:
rules/component-rules.md
---
paths:
- "src/components/**"
- "src/pages/**"
---
# 组件开发规则(操作组件/页面文件时自动加载)
## 文件结构
- 每个 .tsx 文件一个组件
- 同目录同名 .less 文件(如 Button.tsx + Button.less)
- Props 接口定义在同文件中(XxxProps 后缀)
- 类型导入使用 `import type` 语法
## 命名规范
- 组件文件:PascalCase.tsx
- 组件函数:PascalCase,与文件名一致
- Props 接口:组件名Props(如 ButtonProps)
- 事件处理:onXxx(如 onClick、onSubmit)
- 组件内自定义 hook:use组件名Action
## 导出模式
```typescript
export function Button({ children, variant }: ButtonProps) {
// 实现
}
export default Button;
禁止模式
- style={{}}(使用 className + .less)
- key={index} 在 .map() 中(使用稳定 ID)
- 直接 fetch() 调用(使用 api/ 层)
- useEffect 缺少依赖项
- 超过 2 层的 props 透传(使用 zustand 或 Context)
#### `rules/api-rules.md`
```markdown
---
paths:
- "src/api/**"
- "src/stores/**"
---
# 接口与状态管理规则(操作 api/store 文件时自动加载)
## 接口层
- 基础请求封装:src/api/request.ts
- 接口模块:src/api/modules/{资源名}.ts
- 所有端点:/api/{资源名} 模式
- 错误处理:const [err, data] = await apiCall()
- 响应类型:定义在 src/types/ 或同目录
## 请求封装模式
```typescript
export async function request<T>(url: string, options?: RequestInit): Promise<[Error | null, T | null]> {
// 实现
}
zustand Store 模式
import { create } from 'zustand';
import type { UserState, UserActions } from './types';
export const useUserStore = create<UserState & UserActions>((set, get) => ({
user: null,
setUser: (user) => set({ user }),
}));
禁止事项
- 组件中直接 fetch()
- 用 React state 存储 API 响应(使用 zustand)
- API 类型定义中使用
any - 接口调用缺少错误处理
- zustand 中存储不可序列化数据(函数、DOM 节点)
#### `rules/style-rules.md`
```markdown
---
paths:
- "src/**/*.less"
- "src/styles/**"
---
# 样式开发规则(操作 Less/样式文件时自动加载)
## 变量体系
- 所有值来自 variables.less:@spacing-xs/sm/md/lg/xl,@font-size-xs/sm/md/lg/xl
- 颜色:@color-primary、@color-text、@color-bg、@color-border 等
- z-index:@z-dropdown、@z-sticky、@z-modal、@z-popover、@z-tooltip
- 断点:@screen-sm/md/lg/xl
## 命名规范(BEM)
- .block__element--modifier
- .card__title--large
- .nav__item--active
- 类名禁止 camelCase
- 类名禁止单字母
## 禁止事项
- 魔法数字:裸像素值(5px、16px)未使用变量
- 硬编码颜色:#ff0000、rgba(0,0,0,0.1) 未使用变量
- 无注释说明的 !important
- 嵌套选择器超过 3 层
- 组件样式中使用 ID 选择器(#id)
rules/typescript-rules.md
---
paths:
- "src/**/*.ts"
- "src/**/*.tsx"
---
# TypeScript 规则(操作 TypeScript 文件时自动加载)
## 严格合规
- 类型导入使用 import type(verbatimModuleSyntax)
- 禁止 TypeScript enum(erasableSyntaxOnly)—— 使用 `as const` 对象
- 禁止 namespace
- 禁止未使用的局部变量或参数(noUnusedLocals、noUnusedParameters)
- 禁止 var 声明(使用 const/let)
## 类型模式
```typescript
// 替代 enum:
export const Status = { ACTIVE: 'active', INACTIVE: 'inactive' } as const;
export type Status = (typeof Status)[keyof typeof Status];
// 替代 any:
// 使用 unknown + 类型守卫,或具体的联合类型
禁止事项
any类型(确实未知时用 unknown)@ts-ignore/@ts-nocheck(修复类型错误而非忽略)- 无守卫的非空断言
! - 无理由的类型断言
as X(使用类型守卫) - 非测试文件中的 console.log
Claude Code 会**根据你当前编辑的文件路径**自动匹配并加载对应的规则注入到对话上下文,提示 Claude 遵守规范。
---
### 第五步:配置 Subagents(专业分Agent)
在 `.claude/agents/` 目录下创建:
#### `agents/component-analyzer.md`
```markdown
# React Component Dependency Analysis Agent
Analyzes component imports, detects circular dependencies, validates component structure and naming conventions. Use when creating new components, refactoring imports, or debugging component architecture issues. Runs in isolated context to prevent dependency graph from polluting main conversation.
## Tools Read, Grep, Glob
agents/api-checker.md
# API & State Validation Agent
Checks API endpoint conventions, error handling patterns, zustand store structure, and type definitions. Use when creating API modules, zustand stores, or auditing the data layer. Runs in isolated context.
## Tools: Read, Grep, Glob
agents/code-reviewer.md
# Frontend Code Review Agent
Validates React/TypeScript code against project conventions including component naming, Props interface, API patterns, zustand store structure, and style rules.
## Tools: Read, Grep, Glob
agents/performance-auditor.md
# React Performance Audit Agent
Analyzes component re-render risks, bundle size concerns, lazy loading opportunities, and rendering optimization. Use when optimizing performance, reviewing component patterns, or before production deployment. Runs in isolated context.
## Tools: Read, Grep, Glob, Bash
使用方式:
/Agent api-checker "Check the new API module I just wrote"
✅ 验证安装完成
重启 Claude Code,执行 /check 命令,如果看到:
Frontend validation passed: xxx.tsx
说明配置生效。
测试拦截功能:尝试执行 rm -rf src,应该被拦截:
BLOCKED: Dangerous rm -rf detected
📋 完整项目规范(内置)
文件结构约定
src/
components/ # 通用复用组件(PascalCase 目录)
pages/ # 路由级页面组件(PascalCase 目录)
stores/ # zustand 状态(useXxxStore.ts)
api/ # 接口层(request.ts + modules/)
styles/ # 全局 Less(variables.less、global.less)
hooks/ # 自定义 React hooks(useXxx.ts)
utils/ # 纯工具函数
types/ # 共享 TypeScript 类型(*.d.ts)
assets/ # 静态资源
TypeScript 严格规则
verbatimModuleSyntax:类型导入必须用import typeerasableSyntaxOnly:禁止 enum、禁止 namespace → 使用as constnoUnusedLocals/noUnusedParameters:零容忍未使用变量- 禁止:
var、any、@ts-ignore
组件规范
- 每个文件一个组件
- 文件名:
PascalCase.tsx - 导出:命名导出 + 默认导出
- Props:
interface XxxProps(必须以Props后缀) - 样式:同目录同名
.less文件 - 禁止:内联样式
style={{}}、.map()中使用index作为key
样式规范(Less)
- 变量定义在
src/styles/variables.less - 禁止魔法数字(使用
@spacing-sm、@font-size-md等) - BEM 命名:
.block__element--modifier - 禁止无注释说明的
!important - 颜色值必须通过变量引用
接口规范
- 所有接口统一
/api前缀 - 错误处理:
const [err, data] = await apiCall()(Go 风格) - 响应类型定义在
types/目录 - 组件中禁止直接
fetch(),必须通过api/层调用
🎯 六层防御体系工作流
1. 操作前 - PreToolUse
执行 Bash 命令前先检查是否是危险操作,如果是直接拦截,避免事故。
2. 写入后 - PostToolUse:规范校验
每次 Write / Edit 后自动运行规范校验,不通过则中止,要求修复。
3. 写入后 - PostToolUse:技能强制评估
规范校验通过后,强制 Claude 评估当前任务匹配哪个内置 Skill,如果匹配必须先激活 Skill 再继续。保证规范真的被执行,而不只是嘴上说遵守。
4. 编辑时 - 路径规则注入
Claude 根据你编辑的文件路径自动注入对应规则到上下文,提示 Claude 遵守。
5. 回答结束 - Stop Hook
强制 Claude 在结束前验证五项质量门:
- 所有请求的功能已实现并测试
- 新代码中无 TODO/FIXME 注释遗留
- CLAUDE.md 的当前迭代状态已更新
- 生产代码中无 console.log 或 any 类型
- pnpm run lint 通过
有未完成项必须告知用户,不能偷偷带过。
6. 专业任务 - 专用 Subagent
复杂任务交给专门的 Subagent 在隔离上下文处理,不污染主对话。
💡 最佳实践
对 AI 友好的规范
- 规则清晰,可检查,多数能自动化验证
- 避免模糊的"代码整洁"类要求
- 每条规则都明确写在 markdown 中,AI 能直接阅读
错误即拦截
- PostToolUse 验证不通过直接进程非零退出,Claude 能看到错误并修复
- 不需要人工介入,AI 能自己发现问题并修正
渐进式落地
- 不需要一次性把存量代码全部改造
- 从新建文件开始遵守,存量代码改到哪规范到哪
🔧 常见问题
Q: Hook 脚本需要执行权限吗?
A: 不需要,因为用 node 执行,只要文件可读即可。
Q: 会减慢 Claude 响应速度吗?
A: 验证脚本是纯 Node.js 运行,非常快,一般几十毫秒,对响应影响可忽略。
Q: 如何禁用某个检查?
A: 在 .claude/hooks/validate-frontend.js 中注释掉对应检查行即可。
📊 效果对比
| 问题 | 无 Harness | 有 Harness |
|---|---|---|
| 违规命名 | 需要人工提醒 | 自动拦截 |
| any/console.log 残留 | 合并代码才发现 | 写入时就发现 |
| 内联样式 | 人工code review发现 | 自动报错 |
| 忘记激活Skill规范 | AI经常忘记 | 强制评估,必须激活 |
| 危险操作 | 手滑就出事 | 直接拦截 |
| 忘记更新文档 | AI容易忘 | Stop Hook强制检查 |