@dianzhong/create-harness-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +412 -0
- package/package.json +29 -0
- package/templates/axios/.env.example +2 -0
- package/templates/axios/src/api/auth.ts +19 -0
- package/templates/axios/src/api/request.ts +61 -0
- package/templates/axios/src/types/api.ts +26 -0
- package/templates/axios/src/utils/auth.ts +5 -0
- package/templates/axios/src/utils/storage.ts +17 -0
- package/templates/harness/full/.agents/skills/find-skills/SKILL.md +143 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/LICENSE.md +21 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/SKILL.md +155 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/SYNC.md +5 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md +99 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-data-flow.md +313 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-keep-alive.md +139 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-slots.md +226 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-suspense.md +231 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-teleport.md +110 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition-group.md +131 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md +135 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/composables.md +303 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/directives.md +168 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/plugins.md +178 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/reactivity.md +371 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/render-functions.md +227 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/sfc.md +355 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/state-management.md +138 -0
- package/templates/harness/full/.agents/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
- package/templates/harness/full/.claude/agents/code-reviewer.md +109 -0
- package/templates/harness/full/.claude/agents/harness-reviewer.md +51 -0
- package/templates/harness/full/.claude/hooks/guard-tool.cjs +234 -0
- package/templates/harness/full/.claude/hooks/notify.cjs +168 -0
- package/templates/harness/full/.claude/hooks/quality-gate.cjs +135 -0
- package/templates/harness/full/.claude/rules/delivery.md +66 -0
- package/templates/harness/full/.claude/rules/formatting.md +7 -0
- package/templates/harness/full/.claude/rules/git.md +8 -0
- package/templates/harness/full/.claude/rules/skills-mcp.md +13 -0
- package/templates/harness/full/.claude/rules/vue.md +227 -0
- package/templates/harness/full/.claude/settings.json +123 -0
- package/templates/harness/full/.claude/skills/find-skills/SKILL.md +143 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/LICENSE.md +21 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/SKILL.md +155 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/SYNC.md +5 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-class-based-technique.md +258 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/animation-state-driven-technique.md +287 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-async.md +99 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-data-flow.md +313 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-fallthrough-attrs.md +179 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-keep-alive.md +139 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-slots.md +226 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-suspense.md +231 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-teleport.md +110 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition-group.md +131 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/component-transition.md +135 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/composables.md +303 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/directives.md +168 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +177 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +185 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/perf-virtualize-large-lists.md +182 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/plugins.md +178 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/reactivity.md +371 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/render-functions.md +227 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/sfc.md +355 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/state-management.md +138 -0
- package/templates/harness/full/.claude/skills/vue-best-practices/references/updated-hook-performance.md +193 -0
- package/templates/harness/full/.editorconfig +8 -0
- package/templates/harness/full/.husky/commit-msg +1 -0
- package/templates/harness/full/.husky/pre-commit +1 -0
- package/templates/harness/full/.lintstagedrc.json +4 -0
- package/templates/harness/full/.nvmrc +1 -0
- package/templates/harness/full/.oxlintrc.json +11 -0
- package/templates/harness/full/.prettierrc.json +6 -0
- package/templates/harness/full/AGENTS.md +3 -0
- package/templates/harness/full/CLAUDE.md +28 -0
- package/templates/harness/full/GEMINI.md +3 -0
- package/templates/harness/full/commitlint.config.ts +3 -0
- package/templates/harness/full/docs/ai-harness.md +77 -0
- package/templates/harness/full/docs/delivery-template.md +66 -0
- package/templates/harness/full/docs/git.md +24 -0
- package/templates/harness/full/docs/harness-quick-reference.md +89 -0
- package/templates/harness/full/docs/review-checklist.md +49 -0
- package/templates/harness/full/scripts/harness-hooks.test.mjs +218 -0
- package/templates/harness/full/scripts/verify-skills.mjs +248 -0
- package/templates/harness/full/scripts/verify-skills.test.mjs +72 -0
- package/templates/harness/full/skills-lock.json +50 -0
- package/templates/harness/minimal/.claude/hooks/guard-tool.cjs +234 -0
- package/templates/harness/minimal/.claude/hooks/quality-gate.cjs +135 -0
- package/templates/harness/minimal/.claude/settings.json +27 -0
- package/templates/harness/minimal/CLAUDE.md +12 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-reviewer
|
|
3
|
+
description: 强制只读 code review,检查 Vue、TypeScript、Element Plus、路由、权限和 AI 生成代码质量。
|
|
4
|
+
tools: Read, Glob, Grep, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code Reviewer
|
|
8
|
+
|
|
9
|
+
你是本 AI 编写 Vue 后台项目的强制 code review subagent。主 agent 完成任务前,你必须 review 最新变更。
|
|
10
|
+
|
|
11
|
+
## Review 范围
|
|
12
|
+
|
|
13
|
+
- 检查当前 diff 和所有变更文件。
|
|
14
|
+
- 阅读 `CLAUDE.md`、`AGENTS.md`,以及相关 `.claude/rules/*.md`。
|
|
15
|
+
|
|
16
|
+
### Vue / TypeScript 代码质量
|
|
17
|
+
|
|
18
|
+
- **Composition API**:必须使用 `<script setup lang="ts">`,禁止 Options API。
|
|
19
|
+
- **SFC 顺序**:`<script>` → `<template>` → `<style>`,检查是否遵守。
|
|
20
|
+
- **宏位置**:`defineOptions`、`defineProps`、`defineEmits` 应放在 `<script setup>` 顶部,在变量声明之前。
|
|
21
|
+
- **显式类型**:props、emits、store state、route meta、API 边界必须显式类型化,禁止宽泛 `any`。
|
|
22
|
+
- **组件边界**:路由页面保持轻薄,复杂 UI 应拆到 `src/components/<feature>/`。
|
|
23
|
+
- **composables**:可复用的有状态逻辑应放到 `src/composables/useXxx.ts`。
|
|
24
|
+
- **响应式纪律**:能用 `computed` 派生的状态不要重复存储;`watch` 只用于副作用,禁止无意义的空 watcher。
|
|
25
|
+
- **async 错误处理**:避免静默吞掉 Promise reject,async 函数和 Promise 链应有适当的错误处理。
|
|
26
|
+
- **生命周期清理**:检查事件监听、定时器、WebSocket 订阅等是否在 `onUnmounted` 中清理,避免内存泄漏。
|
|
27
|
+
- **模板安全**:避免 `v-html` 渲染不可信内容,避免模板中执行复杂表达式。
|
|
28
|
+
|
|
29
|
+
### 路由和认证
|
|
30
|
+
|
|
31
|
+
- 检查 `meta.title`、`meta.requiresAuth` 和隐藏路由配置是否正确。
|
|
32
|
+
- 检查 guard 行为是否与 `src/router/guards.ts` 一致。
|
|
33
|
+
- 检查动态菜单逻辑是否基于后端 `/auth/menus` 菜单树,不维护第二套前端菜单或权限事实源。
|
|
34
|
+
- 检查 `src/router/component-map.ts` 的组件解析和隐藏路由是否仍与真实页面文件匹配。
|
|
35
|
+
- 禁止路由 meta 回归(删除或误改现有 meta)。
|
|
36
|
+
- 检查是否错误地把当前占位业务页面、mock 数据或临时流程沉淀为永久 harness 规则。
|
|
37
|
+
|
|
38
|
+
### Element Plus
|
|
39
|
+
|
|
40
|
+
- 检查组件用法是否合理(props/events 是否正确、是否使用了已废弃的 API)。
|
|
41
|
+
- 样式覆盖是否集中在 `src/styles/element-plus.scss`,业务组件避免零散深层覆盖。
|
|
42
|
+
- 检查是否使用了冗余的显式导入(项目配置了 `unplugin-auto-import` + `unplugin-vue-components`)。
|
|
43
|
+
|
|
44
|
+
### Pinia Store
|
|
45
|
+
|
|
46
|
+
- 状态来源是否可预测,是否类型清晰。
|
|
47
|
+
- 能用 `computed` 派生的状态是否在 store 中重复存储。
|
|
48
|
+
- store 之间是否存在循环依赖。
|
|
49
|
+
|
|
50
|
+
### 生成文件
|
|
51
|
+
|
|
52
|
+
- 确认是否由工具有意生成,避免被手动误改。
|
|
53
|
+
|
|
54
|
+
### 验证清单
|
|
55
|
+
|
|
56
|
+
- 检查本次变更是否已经运行了对应的验证命令:
|
|
57
|
+
- 一般代码变更:`pnpm check`(包含 `type-check` + `lint` + `format:check` + `harness:check`)
|
|
58
|
+
- 影响构建/路由/依赖/Vite 配置:`pnpm build`
|
|
59
|
+
- harness/skill 变更:`pnpm harness:sync` + `pnpm harness:check`
|
|
60
|
+
- 如果某些检查未运行,是否在交付说明中记录了原因和剩余风险。
|
|
61
|
+
- 涉及可见 UI、布局、路由权限、表单、弹窗或上传时,是否按 `docs/verification.md` 完成本地浏览器验证或说明未验证风险。
|
|
62
|
+
|
|
63
|
+
### 人工审核交接单
|
|
64
|
+
|
|
65
|
+
- 检查主 agent 的最终交付说明是否按 `docs/delivery-template.md` 覆盖改动摘要、影响范围、AI 已验证、需要人工复核、人工决策记录和剩余风险。
|
|
66
|
+
- 涉及真实审批流、角色权限口径、业务状态机、真实接口数据或复杂异常流程时,必须列出人工复核项,不能声称仅靠 AI 已完全验证。
|
|
67
|
+
- 用户曾指出问题或补充业务规则时,检查交付说明是否记录了人工决策和二次验证结果。
|
|
68
|
+
- 如果缺少关键交接信息,或复杂业务没有人工复核项,应作为验证遗漏指出;影响业务判断时返回 FAIL。
|
|
69
|
+
|
|
70
|
+
## 阻断标准(P0 / P1 / P2)
|
|
71
|
+
|
|
72
|
+
### P0 — 阻塞,必须修复
|
|
73
|
+
|
|
74
|
+
- 质量门禁失败(`pnpm check` 或 `pnpm build` 未通过)
|
|
75
|
+
- 安全风险(密钥泄露、XSS、命令注入、SQL 注入)
|
|
76
|
+
- 权限绕过或路由 meta 回归
|
|
77
|
+
- 宽泛未类型化的状态/API 边界(如显式 `any`、缺失的类型声明)
|
|
78
|
+
- 与项目核心规则冲突的变更(如使用 Options API、错误的 SFC 顺序)
|
|
79
|
+
|
|
80
|
+
### P1 — 严重,建议修复
|
|
81
|
+
|
|
82
|
+
- async 错误未处理或静默吞掉 Promise reject
|
|
83
|
+
- 生命周期资源未清理(事件监听、定时器泄漏)
|
|
84
|
+
- `watch` 被滥用(用于本可用 `computed` 实现的派生)
|
|
85
|
+
- Pinia store 中重复存储可计算状态
|
|
86
|
+
- 冗余的显式 Element Plus 导入
|
|
87
|
+
- 验证命令未运行且未说明原因
|
|
88
|
+
- 涉及复杂业务但缺少人工复核项或人工决策记录
|
|
89
|
+
|
|
90
|
+
### P2 — 建议,可备注通过
|
|
91
|
+
|
|
92
|
+
- 代码风格不一致
|
|
93
|
+
- 缺少必要的注释
|
|
94
|
+
- 可进一步拆分的组件
|
|
95
|
+
- 低风险的性能优化建议
|
|
96
|
+
- 交接单信息存在轻微不完整,但不影响本次人工审核
|
|
97
|
+
|
|
98
|
+
## 输出要求
|
|
99
|
+
|
|
100
|
+
没有 P0 问题且没有未说明原因的验证遗漏时返回 PASS。存在 P0 问题或关键验证缺失时返回 FAIL。P1/P2 问题可作为备注列出,不阻断 PASS。
|
|
101
|
+
|
|
102
|
+
结尾必须且只能包含一个结果标记:
|
|
103
|
+
|
|
104
|
+
- `HARNESS_REVIEW_RESULT: PASS`
|
|
105
|
+
- `HARNESS_REVIEW_RESULT: FAIL`
|
|
106
|
+
|
|
107
|
+
无论 PASS 还是 FAIL,都要提醒主 agent:**Claude review 不能替代人工最终复核**。
|
|
108
|
+
|
|
109
|
+
review 内容保持简洁、具体、可执行。
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-reviewer
|
|
3
|
+
description: 只读 review Claude Code harness 配置、hooks、项目 agents、skills 和 AI 治理文档。
|
|
4
|
+
tools: Read, Glob, Grep, Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Harness Reviewer
|
|
8
|
+
|
|
9
|
+
你负责 review 本项目的 AI harness 层。不要编辑文件。
|
|
10
|
+
|
|
11
|
+
## Review 范围
|
|
12
|
+
|
|
13
|
+
- `.claude/settings.json`
|
|
14
|
+
- `.claude/hooks/*.cjs`
|
|
15
|
+
- `.claude/agents/*.md`
|
|
16
|
+
- `.claude/rules/*.md`
|
|
17
|
+
- `CLAUDE.md`
|
|
18
|
+
- `AGENTS.md`
|
|
19
|
+
- `docs/ai-harness.md`
|
|
20
|
+
- `docs/delivery-template.md`
|
|
21
|
+
- `docs/review-checklist.md`
|
|
22
|
+
- `docs/verification.md`
|
|
23
|
+
- `skills-lock.json`
|
|
24
|
+
- `scripts/check-project-structure.mjs`
|
|
25
|
+
- `scripts/*.test.mjs`
|
|
26
|
+
- `package.json` scripts 和 `.husky/*`
|
|
27
|
+
|
|
28
|
+
## 检查项
|
|
29
|
+
|
|
30
|
+
- hooks 必须跨平台,不能依赖 `jq`,平台专属逻辑必须有安全 fallback。
|
|
31
|
+
- 只有 `quality-gate.cjs stop` 允许运行 `pnpm format` 做 Stop 收尾自动格式化;其他 hooks 不得自动格式化、暂存或改写 repo-tracked 文件。
|
|
32
|
+
- Stop review 必须在质量门禁失败或 P0/P1 review 问题时阻止结束。
|
|
33
|
+
- 完成通知必须兼容 macOS 和 Windows,并在失败时安全降级。
|
|
34
|
+
- skills 必须同步 lock hash(`skills-lock.json` 中每个 skill 的 `computedHash` 必须与当前文件内容一致)。
|
|
35
|
+
- `.agents/skills/` 必须与 `.claude/skills/` 完全镜像(`diff -r .claude/skills/ .agents/skills/` 输出为空)。
|
|
36
|
+
- harness 脚本的测试必须接入 `pnpm harness:check`,不能只放测试文件。
|
|
37
|
+
- 结构检查必须只约束通用工程护栏,不得固化当前占位业务流程或 mock 数据。
|
|
38
|
+
- harness 配置以源文件为准,不维护额外 generated config inventory。
|
|
39
|
+
- 敏感文件和危险命令必须被拒绝。
|
|
40
|
+
- 人工审核交付要求必须在入口文档、速查表、review checklist、delivery template 和 reviewer 规则中保持一致。
|
|
41
|
+
- 不得让 AI 替代复杂业务决策;真实审批流、角色权限口径、状态机和真实接口数据必须保留人工复核边界。
|
|
42
|
+
- 无论 PASS 还是 FAIL,都要提醒主 agent:Claude review 不能替代人工最终复核。
|
|
43
|
+
|
|
44
|
+
## 输出要求
|
|
45
|
+
|
|
46
|
+
结尾必须且只能包含一个结果标记:
|
|
47
|
+
|
|
48
|
+
- `HARNESS_REVIEW_RESULT: PASS`
|
|
49
|
+
- `HARNESS_REVIEW_RESULT: FAIL`
|
|
50
|
+
|
|
51
|
+
review 内容保持简洁、具体、可执行。
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse 安全守护 hook
|
|
4
|
+
*
|
|
5
|
+
* 职责:在 Claude Code 执行任何工具之前拦截,阻断危险命令和敏感文件访问。
|
|
6
|
+
* 覆盖 Bash/Read/Write/Edit/Grep/Agent 等工具调用。
|
|
7
|
+
* 不改写任何文件,只返回 continue/deny 决策。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs')
|
|
13
|
+
const path = require('node:path')
|
|
14
|
+
|
|
15
|
+
// 从 stdin 读取 Claude Code 传入的 JSON hook payload
|
|
16
|
+
function readJsonStdin() {
|
|
17
|
+
const input = fs.readFileSync(0, 'utf8').trim()
|
|
18
|
+
if (!input) {
|
|
19
|
+
return {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(input)
|
|
24
|
+
} catch {
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 构造拦截响应:阻止执行并附带原因
|
|
30
|
+
function deny(reason) {
|
|
31
|
+
process.stdout.write(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
continue: false,
|
|
34
|
+
stopReason: reason,
|
|
35
|
+
permissionDecision: 'deny',
|
|
36
|
+
permissionDecisionReason: reason,
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 构造放行响应:允许执行,抑制输出
|
|
42
|
+
function allow() {
|
|
43
|
+
process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 统一路径格式:Windows 反斜线 → 正斜线,去除两端引号
|
|
47
|
+
function normalizePath(value) {
|
|
48
|
+
return String(value ?? '')
|
|
49
|
+
.replaceAll('\\', '/')
|
|
50
|
+
.replace(/^["']|["']$/g, '')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 从嵌套的 toolInput 对象中递归收集所有含路径含义的字符串值
|
|
54
|
+
function collectPaths(value, results = []) {
|
|
55
|
+
if (!value) {
|
|
56
|
+
return results
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
results.push(value)
|
|
61
|
+
return results
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
for (const item of value) {
|
|
66
|
+
collectPaths(item, results)
|
|
67
|
+
}
|
|
68
|
+
return results
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
for (const [key, item] of Object.entries(value)) {
|
|
73
|
+
if (/path|file|filename/i.test(key) && typeof item === 'string') {
|
|
74
|
+
results.push(item)
|
|
75
|
+
} else {
|
|
76
|
+
collectPaths(item, results)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 判断文件路径是否为密钥/凭证等敏感文件(允许 .env.example)
|
|
85
|
+
function isSensitivePath(filePath) {
|
|
86
|
+
const normalized = normalizePath(filePath)
|
|
87
|
+
const base = path.posix.basename(normalized)
|
|
88
|
+
|
|
89
|
+
if (!normalized) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (base === '.env.example') {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
/^\.env($|\.)/.test(base) ||
|
|
99
|
+
/(^|\/)\.env($|\.)/.test(normalized) ||
|
|
100
|
+
/(^|\/)secrets?\//i.test(normalized) ||
|
|
101
|
+
/(^|\/)(id_rsa|id_ed25519|known_hosts)$/i.test(normalized) ||
|
|
102
|
+
/\.(pem|key|p12|pfx|crt|cer)$/i.test(base)
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 判断 glob pattern 是否匹配敏感文件模式
|
|
107
|
+
function isSensitiveGlob(glob) {
|
|
108
|
+
const normalized = String(glob ?? '').replaceAll('\\', '/')
|
|
109
|
+
return (
|
|
110
|
+
/^\.env($|\*)/.test(normalized) ||
|
|
111
|
+
/(^|\/)\.env($|\*)/.test(normalized) ||
|
|
112
|
+
/(^|\/)secrets?\//i.test(normalized) ||
|
|
113
|
+
/\.(pem|key|p12|pfx|crt|cer)$/i.test(normalized)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 从 shell 命令文本中提取可能的路径片段。不能只按空白切分:
|
|
118
|
+
// cat<.env.production、node -e "readFileSync('.env')" 这类写法都不会产生独立空白 token。
|
|
119
|
+
function collectCommandPathCandidates(command) {
|
|
120
|
+
return String(command ?? '')
|
|
121
|
+
.replaceAll('\\', '/')
|
|
122
|
+
.split(/[\s"'`$;&|()<>,={}[\]]+/)
|
|
123
|
+
.map((token) => token.trim())
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 检测 Bash 命令是否包含危险操作(破坏性 Git、递归删除、低级磁盘写入等)
|
|
128
|
+
// commandStart 匹配命令起始位置:行首、管道/分号后,或环境变量赋值后
|
|
129
|
+
function dangerousCommandReason(command) {
|
|
130
|
+
const commandStart = String.raw`(?:^|[;&|()])\s*(?:\w+=(?:"[^"]*"|'[^']*'|[^\s;|()]+)\s+)*`
|
|
131
|
+
const checks = [
|
|
132
|
+
[new RegExp(`${commandStart}git\\s+reset\\s+--hard\\b`, 'i'), 'Blocked destructive git reset.'],
|
|
133
|
+
[
|
|
134
|
+
new RegExp(`${commandStart}git\\s+reset\\s+--mixed\\s+HEAD\\b`, 'i'),
|
|
135
|
+
'Blocked destructive git reset.',
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
new RegExp(`${commandStart}git\\s+push\\s+--force(?:-with-lease)?\\b`, 'i'),
|
|
139
|
+
'Blocked force push.',
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
new RegExp(`${commandStart}git\\s+push\\s+\\S+\\s+--delete\\b`, 'i'),
|
|
143
|
+
'Blocked remote branch deletion.',
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
new RegExp(`${commandStart}git\\s+push\\s+\\S+\\s+\\+\\S+\\b`, 'i'),
|
|
147
|
+
'Blocked force push via refspec.',
|
|
148
|
+
],
|
|
149
|
+
[new RegExp(`${commandStart}git\\s+branch\\s+-D\\b`, 'i'), 'Blocked local branch deletion.'],
|
|
150
|
+
[new RegExp(`${commandStart}git\\s+stash\\s+(drop|clear)\\b`, 'i'), 'Blocked stash deletion.'],
|
|
151
|
+
[
|
|
152
|
+
new RegExp(`${commandStart}git\\s+checkout\\s+(?:--\\s+|\\.)`, 'i'),
|
|
153
|
+
'Blocked destructive git checkout.',
|
|
154
|
+
],
|
|
155
|
+
[new RegExp(`${commandStart}git\\s+restore\\b`, 'i'), 'Blocked destructive git restore.'],
|
|
156
|
+
[
|
|
157
|
+
new RegExp(
|
|
158
|
+
`${commandStart}git\\s+clean\\s+(?=[^;&|\\n]*-[A-Za-z]*f)(?=[^;&|\\n]*-[A-Za-z]*d)`,
|
|
159
|
+
'i',
|
|
160
|
+
),
|
|
161
|
+
'Blocked destructive git clean.',
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
new RegExp(`${commandStart}rm\\s+(?=[^;&|\\n]*-[A-Za-z]*r)(?=[^;&|\\n]*-[A-Za-z]*f)`, 'i'),
|
|
165
|
+
'Blocked recursive force delete.',
|
|
166
|
+
],
|
|
167
|
+
[new RegExp(`${commandStart}del\\s+\\/[fsq]\\b`, 'i'), 'Blocked destructive Windows delete.'],
|
|
168
|
+
[new RegExp(`${commandStart}rmdir\\s+\\/s\\b`, 'i'), 'Blocked recursive directory delete.'],
|
|
169
|
+
[
|
|
170
|
+
new RegExp(`${commandStart}Remove-Item\\b.*\\s-Recurse\\b.*\\s-Force\\b`, 'i'),
|
|
171
|
+
'Blocked recursive force delete.',
|
|
172
|
+
],
|
|
173
|
+
[new RegExp(`${commandStart}dd\\s+\\b`, 'i'), 'Blocked low-level disk write command.'],
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
for (const [pattern, reason] of checks) {
|
|
177
|
+
if (pattern.test(command)) {
|
|
178
|
+
return reason
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return ''
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// === 主逻辑 ===
|
|
186
|
+
|
|
187
|
+
// 1. Bash 命令检查:先检测危险命令,再扫描命令中的敏感路径
|
|
188
|
+
const payload = readJsonStdin()
|
|
189
|
+
const toolName = payload.tool_name || payload.toolName || payload.tool || ''
|
|
190
|
+
const toolInput = payload.tool_input || payload.toolInput || {}
|
|
191
|
+
const command = String(toolInput.command || '')
|
|
192
|
+
|
|
193
|
+
if (/bash/i.test(toolName) && command) {
|
|
194
|
+
const reason = dangerousCommandReason(command)
|
|
195
|
+
|
|
196
|
+
if (reason) {
|
|
197
|
+
deny(`${reason} Command: ${command.slice(0, 160)}`)
|
|
198
|
+
process.exit(0)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const token of collectCommandPathCandidates(command)) {
|
|
202
|
+
if (isSensitivePath(token)) {
|
|
203
|
+
deny(`Blocked command touching sensitive path: ${token}`)
|
|
204
|
+
process.exit(0)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. 工具输入路径检查:递归收集所有路径参数,检测敏感文件
|
|
210
|
+
const paths = collectPaths(toolInput).map(normalizePath)
|
|
211
|
+
const sensitive = paths.find(isSensitivePath)
|
|
212
|
+
|
|
213
|
+
if (sensitive) {
|
|
214
|
+
deny(`Blocked access to sensitive path: ${sensitive}`)
|
|
215
|
+
process.exit(0)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. Grep 工具专项检查:glob pattern 和搜索路径中的敏感文件
|
|
219
|
+
if (/grep/i.test(toolName)) {
|
|
220
|
+
const glob = String(toolInput.glob || '')
|
|
221
|
+
const grepPath = String(toolInput.path || '')
|
|
222
|
+
|
|
223
|
+
if (glob && isSensitiveGlob(glob)) {
|
|
224
|
+
deny(`Blocked Grep with sensitive glob pattern: ${glob}`)
|
|
225
|
+
process.exit(0)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (grepPath && isSensitivePath(grepPath)) {
|
|
229
|
+
deny(`Blocked Grep in sensitive path: ${grepPath}`)
|
|
230
|
+
process.exit(0)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
allow()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SubagentStop / Notification hook
|
|
4
|
+
*
|
|
5
|
+
* 职责:
|
|
6
|
+
* - SubagentStop 模式:解析审查结果,FAIL 时阻断主 agent,PASS 时通知。
|
|
7
|
+
* - Notification 模式:将通用消息转发为系统通知。
|
|
8
|
+
* 跨平台支持:macOS osascript → Windows pwsh → powershell → stderr 降级。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs')
|
|
14
|
+
const { spawnSync } = require('node:child_process')
|
|
15
|
+
|
|
16
|
+
const mode = process.argv[2] || 'notification'
|
|
17
|
+
|
|
18
|
+
function readJsonStdin() {
|
|
19
|
+
const input = fs.readFileSync(0, 'utf8').trim()
|
|
20
|
+
if (!input) {
|
|
21
|
+
return {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(input)
|
|
26
|
+
} catch {
|
|
27
|
+
return {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// PowerShell 字符串中的单引号转义(单引号内用 '' 表示一个单引号)
|
|
32
|
+
function escapePowerShell(value) {
|
|
33
|
+
return String(value).replaceAll("'", "''")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 发送跨平台系统通知,带三级降级:macOS osascript → Windows pwsh/powershell → stderr
|
|
37
|
+
function notify(title, message) {
|
|
38
|
+
const cleanTitle = String(title).slice(0, 80)
|
|
39
|
+
const cleanMessage = String(message).replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
40
|
+
|
|
41
|
+
if (process.argv.includes('--dry-run') || process.env.HARNESS_NOTIFY_DRY_RUN === '1') {
|
|
42
|
+
process.stderr.write(`[dry-run notification] ${cleanTitle}: ${cleanMessage}\n`)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
const script = `display notification ${JSON.stringify(cleanMessage)} with title ${JSON.stringify(cleanTitle)}`
|
|
48
|
+
const result = spawnSync('osascript', ['-e', script], { stdio: 'ignore', timeout: 10000 })
|
|
49
|
+
|
|
50
|
+
if (!result.error && result.status === 0) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (process.platform === 'win32') {
|
|
56
|
+
const script = [
|
|
57
|
+
"$ErrorActionPreference = 'Stop'",
|
|
58
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
59
|
+
'$notify = New-Object System.Windows.Forms.NotifyIcon',
|
|
60
|
+
'$notify.Icon = [System.Drawing.SystemIcons]::Information',
|
|
61
|
+
'$notify.Visible = $true',
|
|
62
|
+
`$notify.ShowBalloonTip(5000, '${escapePowerShell(cleanTitle)}', '${escapePowerShell(cleanMessage)}', 'Info')`,
|
|
63
|
+
'Start-Sleep -Seconds 6',
|
|
64
|
+
'$notify.Dispose()',
|
|
65
|
+
].join('; ')
|
|
66
|
+
|
|
67
|
+
const pwshResult = spawnSync(
|
|
68
|
+
'pwsh.exe',
|
|
69
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
70
|
+
{ stdio: 'ignore', timeout: 15000 },
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (!pwshResult.error && pwshResult.status === 0) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = spawnSync(
|
|
78
|
+
'powershell.exe',
|
|
79
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
|
|
80
|
+
{ stdio: 'ignore', timeout: 15000 },
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (!result.error && result.status === 0) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
process.stderr.write(`[${cleanTitle}] ${cleanMessage}\n`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 从不同 hook payload 格式中提取消息文本(兼容多种 payload 结构)
|
|
92
|
+
function extractMessage(payload) {
|
|
93
|
+
return (
|
|
94
|
+
payload.message ||
|
|
95
|
+
payload.notification_message ||
|
|
96
|
+
payload.last_assistant_message ||
|
|
97
|
+
payload.transcript_tail ||
|
|
98
|
+
''
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const payload = readJsonStdin()
|
|
103
|
+
const message = extractMessage(payload)
|
|
104
|
+
|
|
105
|
+
// SubagentStop 模式:解析审查结果,FAIL 时阻断主 agent 继续执行
|
|
106
|
+
if (mode === 'subagent-stop') {
|
|
107
|
+
const text = String(message)
|
|
108
|
+
|
|
109
|
+
// 从后往前找最后一个配对的 {...},避免贪婪正则匹配到解释性文本中的大括号
|
|
110
|
+
let jsonResult = null
|
|
111
|
+
let lastBrace = -1
|
|
112
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
113
|
+
if (text[i] === '}') {
|
|
114
|
+
lastBrace = i
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (lastBrace >= 0) {
|
|
119
|
+
let depth = 1
|
|
120
|
+
let openBrace = -1
|
|
121
|
+
for (let i = lastBrace - 1; i >= 0; i--) {
|
|
122
|
+
if (text[i] === '}') depth++
|
|
123
|
+
else if (text[i] === '{') depth--
|
|
124
|
+
if (depth === 0) {
|
|
125
|
+
openBrace = i
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (openBrace >= 0) {
|
|
130
|
+
try {
|
|
131
|
+
jsonResult = JSON.parse(text.slice(openBrace, lastBrace + 1))
|
|
132
|
+
} catch {
|
|
133
|
+
// JSON 解析失败,继续使用字符串 fallback
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// JSON 有效时以 ok 字段为准;无效时 fallback 到尾部字符串匹配
|
|
139
|
+
const isFail =
|
|
140
|
+
(jsonResult && jsonResult.ok === false) ||
|
|
141
|
+
(!jsonResult && text.trimEnd().endsWith('HARNESS_REVIEW_RESULT: FAIL'))
|
|
142
|
+
|
|
143
|
+
const isPass =
|
|
144
|
+
(jsonResult && jsonResult.ok === true) ||
|
|
145
|
+
(!jsonResult && text.trimEnd().endsWith('HARNESS_REVIEW_RESULT: PASS'))
|
|
146
|
+
|
|
147
|
+
if (isFail) {
|
|
148
|
+
notify('Claude Code 代码审查', '审查未通过,请继续修复报告的问题。')
|
|
149
|
+
process.stdout.write(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
continue: false,
|
|
152
|
+
stopReason: 'AI 代码审查未通过。请根据审查报告修复问题后再次尝试完成。',
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
process.exit(0)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isPass) {
|
|
159
|
+
notify('Claude Code', '任务已完成,代码审查通过。')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }))
|
|
163
|
+
process.exit(0)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Notification 模式:通用通知转发
|
|
167
|
+
notify('Claude Code', message || 'Claude Code 需要您的关注。')
|
|
168
|
+
process.stdout.write(JSON.stringify({ continue: true, suppressOutput: true }))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stop hook
|
|
4
|
+
*
|
|
5
|
+
* 职责:
|
|
6
|
+
* - git 有变更时先自动格式化,再运行 type-check / lint / harness:check。
|
|
7
|
+
* 失败时阻断 agent 停止,成功时放行并通知。
|
|
8
|
+
* 注意:不自动暂存(不 git add),由人工决定暂存与提交。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require('node:child_process')
|
|
14
|
+
|
|
15
|
+
const dryRun = process.argv.includes('--dry-run') || process.env.HARNESS_QUALITY_DRY_RUN === '1'
|
|
16
|
+
|
|
17
|
+
function writeJson(value) {
|
|
18
|
+
process.stdout.write(JSON.stringify(value))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 跨平台 shell 参数转义:安全字符直接通过,POSIX 用单引号,Windows 用双引号
|
|
22
|
+
function shellQuote(value) {
|
|
23
|
+
const text = String(value)
|
|
24
|
+
|
|
25
|
+
if (!/[^\w@%+=:,./-]/.test(text)) {
|
|
26
|
+
return text
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
return `"${text.replaceAll('"', '\\"')}"`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `'${text.replaceAll("'", "'\\''")}'`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 跨平台执行 shell 命令:Windows 用 cmd.exe,POSIX 用 sh -lc
|
|
37
|
+
function runShell(commandLine, timeout = 180000) {
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
return spawnSync('cmd.exe', ['/d', '/s', '/c', commandLine], {
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
shell: false,
|
|
43
|
+
timeout,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return spawnSync('sh', ['-lc', commandLine], {
|
|
48
|
+
cwd: process.cwd(),
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
shell: false,
|
|
51
|
+
timeout,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 封装命令执行,支持 --dry-run 模式跳过实际执行
|
|
56
|
+
function run(command, args) {
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
return {
|
|
59
|
+
command: [command, ...args].join(' '),
|
|
60
|
+
status: 0,
|
|
61
|
+
stdout: '[dry-run] skipped command execution',
|
|
62
|
+
stderr: '',
|
|
63
|
+
error: '',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const commandLine = [command, ...args].map(shellQuote).join(' ')
|
|
68
|
+
const result = runShell(commandLine)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
command: [command, ...args].join(' '),
|
|
72
|
+
status: result.status ?? 1,
|
|
73
|
+
stdout: result.stdout || '',
|
|
74
|
+
stderr: result.stderr || '',
|
|
75
|
+
error: result.error ? result.error.message : '',
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 获取 git 变更文件列表(porcelain 格式)
|
|
80
|
+
function gitStatus() {
|
|
81
|
+
const result = runShell('git status --porcelain --untracked-files=all', 15000)
|
|
82
|
+
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result.stdout
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Stop 模式:无变更时跳过门禁,否则运行完整的质量检查流水线
|
|
94
|
+
const changed = gitStatus()
|
|
95
|
+
|
|
96
|
+
if (changed && changed.length === 0) {
|
|
97
|
+
writeJson({
|
|
98
|
+
continue: true,
|
|
99
|
+
systemMessage: 'Harness 质量门禁跳过:git 未检测到变更文件。',
|
|
100
|
+
})
|
|
101
|
+
process.exit(0)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Stop 收尾阶段允许自动格式化(本项目唯一允许自动格式化的 hook)
|
|
105
|
+
const formatResult = run('pnpm', ['format'])
|
|
106
|
+
const commands = [
|
|
107
|
+
['pnpm', ['type-check']],
|
|
108
|
+
['pnpm', ['lint']],
|
|
109
|
+
['pnpm', ['harness:check']],
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const results = [formatResult, ...commands.map(([command, args]) => run(command, args))]
|
|
113
|
+
const failed = results.filter((result) => result.status !== 0)
|
|
114
|
+
|
|
115
|
+
if (failed.length > 0) {
|
|
116
|
+
const details = failed
|
|
117
|
+
.map((result) => {
|
|
118
|
+
const output = `${result.stdout}\n${result.stderr}\n${result.error}`.trim()
|
|
119
|
+
return `命令失败:${result.command}\n${output.slice(-2400)}`
|
|
120
|
+
})
|
|
121
|
+
.join('\n\n')
|
|
122
|
+
|
|
123
|
+
writeJson({
|
|
124
|
+
continue: false,
|
|
125
|
+
stopReason: `Harness 质量门禁未通过。请修复以下问题后再继续。\n\n${details}`,
|
|
126
|
+
})
|
|
127
|
+
process.exit(0)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writeJson({
|
|
131
|
+
continue: true,
|
|
132
|
+
systemMessage: `Harness 质量门禁通过。变更文件数:${
|
|
133
|
+
changed ? changed.length : 0
|
|
134
|
+
}。已通过的检查:pnpm format(自动修复)、pnpm type-check、pnpm lint、pnpm harness:check。`,
|
|
135
|
+
})
|