@routerhub/agent-rules 1.4.15 → 1.5.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/AGENTS.base.md +5 -0
- package/CHANGELOG.md +173 -0
- package/merge.js +282 -33
- package/package.json +3 -2
- package/postinstall.js +1 -1
- package/rules/devops.md +21 -0
- package/rules/frontend.md +67 -0
- package/rules/global.md +26 -0
- package/rules/go-backend.md +15 -0
package/AGENTS.base.md
CHANGED
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
- 优先值传递与局部变量生命周期控制,避免内存逃逸;无必要不返回局部变量指针。
|
|
49
49
|
- 所有删除操作必须使用软删除机制;`DeletedAt` 字段类型必须用 `gorm.DeletedAt`,禁止用 `*time.Time`(后者不实现 `DeleteClausesInterface`,会导致硬删除)。
|
|
50
50
|
|
|
51
|
+
## GORM Model 规范
|
|
52
|
+
|
|
53
|
+
- `DeletedAt` 字段必须使用 `gorm.DeletedAt` 类型,严禁使用 `*time.Time`。`*time.Time` 不会触发 GORM v2 的软删除机制,会导致 `Delete()` 执行物理删除(DELETE FROM)而非软删除(UPDATE SET deleted_at)。
|
|
54
|
+
- 新建 Model 时,优先嵌入已有的公共基础结构体(如包含 ID、CreatedAt、UpdatedAt、DeletedAt 的 BaseModel),避免各 Model 独立定义这些字段导致类型不一致。
|
|
55
|
+
|
|
51
56
|
## Git 规范
|
|
52
57
|
|
|
53
58
|
- 分支用 Git Flow(`feature/`、`bugfix/`、`hotfix/`、`refactor/`、`chore/`、`docs/`、`test/`),英文小写中划线分隔。
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
所有对 @routerhub/agent-rules 的重大更改都会记录在这个文件中。
|
|
4
|
+
|
|
5
|
+
## [1.3.12] - 2026-05-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 新增规则:每次开始实现新需求前,必须先在 `docs/` 目录编写需求说明 MD 文档,至少包含需求目标、需求范围、功能要求、验收标准,必要时补充备注。
|
|
10
|
+
|
|
11
|
+
## [1.0.29] - 2026-04-20
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- 新增规则:相关测试用例执行通过后,才允许运行 npm run test:e2e:ui 打开 UI 自动化测试页面。
|
|
16
|
+
- 新增规则:编写测试用例时,测试描述、断言说明和相关说明文字统一使用中文。
|
|
17
|
+
|
|
18
|
+
## [1.0.28] - 2026-04-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- 新增规则:当用户要求“加测试用例”时,必须主动执行 npm run test:e2e:ui,并说明本次主要测试用例名称或编号。
|
|
23
|
+
|
|
24
|
+
## [1.0.27] - 2026-04-20
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- 移除自动化测试规则中“关键步骤之间需间隔 2 秒再执行下一步”的要求。
|
|
29
|
+
|
|
30
|
+
## [1.0.25] - 2026-04-20
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- 新增规则:错误处理与日志规范,统一使用 console.error 并要求包含上下文信息。
|
|
35
|
+
|
|
36
|
+
## [1.0.24] - 2026-04-20
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- 在示例私有规则文件中补充测试注释示例。
|
|
41
|
+
|
|
42
|
+
## [1.0.23] - 2026-04-20
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- 新增规则:打开 UI 自动化测试后,应明确告知当前业务对应的具体测试用例名称或编号,让开发者清楚当前验证范围。
|
|
47
|
+
|
|
48
|
+
## [1.0.22] - 2026-04-20
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- 新增规则:写完业务后,必须自动打开 UI 自动化测试页面,执行 `pnpm run test:e2e:ui`,让开发者手动点击验证功能效果。
|
|
53
|
+
|
|
54
|
+
## [1.0.20] - 2026-04-19
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- UI 自动化测试端口被占用时,需自动切换到未被占用的新端口。
|
|
59
|
+
|
|
60
|
+
## [1.0.18] - 2026-04-19
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- 分支管理规范:所有新建分支名称必须使用中文(汉字)或纯中文拼音,禁止英文缩写、数字、拼音与英文混用。
|
|
65
|
+
|
|
66
|
+
## [1.0.17] - 2026-04-19
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- 新增分支管理规范:所有新建分支名称必须使用中文拼音或汉字,禁止使用无意义英文缩写。
|
|
71
|
+
|
|
72
|
+
## [1.0.16] - 2026-04-19
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- 新增测试用例规范:所有测试用例必须包含中文断言或校验内容,确保覆盖中文场景。
|
|
77
|
+
|
|
78
|
+
## [1.0.15] - 2026-04-19
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- 触发自动更新链路验证发布
|
|
83
|
+
|
|
84
|
+
## [1.0.14] - 2026-04-19
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- 自动更新流程改为直接提交到主分支,无需手动合并 PR
|
|
89
|
+
|
|
90
|
+
## [1.0.13] - 2026-04-19
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
|
|
94
|
+
- 修复自动更新 workflow 权限问题,使用 PAT token 创建 PR
|
|
95
|
+
|
|
96
|
+
## [1.0.12] - 2026-04-19
|
|
97
|
+
|
|
98
|
+
### Changed
|
|
99
|
+
|
|
100
|
+
- 验证自动更新链路
|
|
101
|
+
|
|
102
|
+
## [1.0.11] - 2026-04-19
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- 新增代码注释规范:注释统一使用中文编写
|
|
107
|
+
|
|
108
|
+
## [1.0.10] - 2026-04-19
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- 新增中文说明文档 `README.zh-CN.md`,补充安装、初始化、同步和监听的使用说明
|
|
113
|
+
|
|
114
|
+
### Changed
|
|
115
|
+
|
|
116
|
+
- README 增加中文说明文档入口
|
|
117
|
+
|
|
118
|
+
## [1.0.9] - 2026-04-19
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
|
|
122
|
+
- 新增 UI 自动化测试规则:UI 自动化测试必须请求真实的后端接口,不允许使用本地 mock 数据或截断真实请求
|
|
123
|
+
|
|
124
|
+
## [1.0.3] - 2026-04-16
|
|
125
|
+
|
|
126
|
+
### Changed
|
|
127
|
+
|
|
128
|
+
- 精简 README,只保留安装说明与自动监听 `AGENTS.private.md` 的使用方式
|
|
129
|
+
- 文档默认引导通过 `pnpm dev` 搭配 `agent-rules watch` 使用
|
|
130
|
+
|
|
131
|
+
## [1.0.2] - 2026-04-16
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- 新增 `agent-rules watch` 命令
|
|
136
|
+
- 支持监听项目根目录下的 `AGENTS.private.md` 变更
|
|
137
|
+
- 监听模式启动时自动执行一次同步,生成最新 `AGENTS.md`
|
|
138
|
+
- 当监听文件尚不存在时,自动监听目录并在文件创建后继续生效
|
|
139
|
+
|
|
140
|
+
### Changed
|
|
141
|
+
|
|
142
|
+
- 更新 CLI 帮助文案,补充 `watch` 子命令说明
|
|
143
|
+
|
|
144
|
+
## [1.0.0] - 2024-01-XX
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
- 初始版本发布
|
|
149
|
+
- 基础规则 AGENTS.base.md
|
|
150
|
+
- 合并工具 merge.js
|
|
151
|
+
- GitHub Actions 自动发布流程
|
|
152
|
+
- 项目接入文档和示例
|
|
153
|
+
|
|
154
|
+
### 说明
|
|
155
|
+
|
|
156
|
+
此版本包含所有 RouterHub 项目的通用编码规范和最佳实践。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 版本升级指南
|
|
161
|
+
|
|
162
|
+
### 从 v0.x 升级到 v1.0.0
|
|
163
|
+
|
|
164
|
+
此版本为首个公开版本,引入了新的规则体系和合并机制。请按照 README.md 中的步骤在各项目中进行集成。
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 未来计划
|
|
169
|
+
|
|
170
|
+
- [ ] 支持更多规则定制选项
|
|
171
|
+
- [ ] 提供 CLI 配置向导
|
|
172
|
+
- [ ] 为不同技术栈(React、Vue、等)提供专用规则
|
|
173
|
+
- [ ] 集成其他 linter 配置(eslint 等)
|
package/merge.js
CHANGED
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
* node merge.js [command]
|
|
8
8
|
*
|
|
9
9
|
* command:
|
|
10
|
-
* init
|
|
11
|
-
* sync
|
|
10
|
+
* init 初始化规则文件,并在缺失时创建 AGENTS.private.md 模板
|
|
11
|
+
* sync 重新生成所有规则输出文件
|
|
12
12
|
* watch 监听 AGENTS.private.md 变化并自动同步
|
|
13
|
+
*
|
|
14
|
+
* 输出文件:
|
|
15
|
+
* AGENTS.md 全量合并(兼容 Cursor/Claude Code)
|
|
16
|
+
* .github/copilot-instructions.md 全局规则(VS Code Copilot)
|
|
17
|
+
* .github/instructions/*.instructions.md 按域条件加载(VS Code Copilot applyTo)
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
const fs = require("fs");
|
|
@@ -26,6 +31,8 @@ function getDefaultConfig() {
|
|
|
26
31
|
return {
|
|
27
32
|
output: path.join(currentRoot, "AGENTS.md"),
|
|
28
33
|
copilotOutput: path.join(currentRoot, ".github", "copilot-instructions.md"),
|
|
34
|
+
instructionsDir: path.join(currentRoot, ".github", "instructions"),
|
|
35
|
+
rulesDir: path.join(packageRoot, "rules"),
|
|
29
36
|
baseRulesPath: path.join(packageRoot, "AGENTS.base.md"),
|
|
30
37
|
privateRulesPath: path.join(currentRoot, "AGENTS.private.md"),
|
|
31
38
|
};
|
|
@@ -58,45 +65,274 @@ function readFileIfExists(filePath) {
|
|
|
58
65
|
return null;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 解析 markdown 文件的 YAML frontmatter
|
|
70
|
+
* 返回 { meta: { name, applyTo, outputName }, content: "去掉 frontmatter 后的正文" }
|
|
71
|
+
*/
|
|
72
|
+
function parseFrontmatter(rawContent) {
|
|
73
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
74
|
+
const matched = rawContent.match(frontmatterRegex);
|
|
75
|
+
if (!matched) {
|
|
76
|
+
return { meta: {}, content: rawContent };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const yamlBlock = matched[1];
|
|
80
|
+
const content = matched[2];
|
|
81
|
+
const meta = {};
|
|
82
|
+
|
|
83
|
+
// 简易 YAML 解析,支持 name/outputName(字符串) 和 applyTo(数组)
|
|
84
|
+
for (const line of yamlBlock.split("\n")) {
|
|
85
|
+
const trimmedLine = line.trim();
|
|
86
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const colonIndex = trimmedLine.indexOf(":");
|
|
91
|
+
if (colonIndex === -1) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const yamlKey = trimmedLine.slice(0, colonIndex).trim();
|
|
96
|
+
const yamlValue = trimmedLine.slice(colonIndex + 1).trim();
|
|
97
|
+
|
|
98
|
+
if (yamlKey === "applyTo") {
|
|
99
|
+
// 解析 JSON 数组格式: ["**/*.go"]
|
|
100
|
+
const arrayMatch = yamlValue.match(/^\[.*\]$/);
|
|
101
|
+
if (arrayMatch) {
|
|
102
|
+
meta[yamlKey] = JSON.parse(yamlValue);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// 去掉引号
|
|
106
|
+
meta[yamlKey] = yamlValue.replace(/^["']|["']$/g, "");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { meta, content };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 扫描 rules/ 目录,加载并分类所有规则文件
|
|
115
|
+
* 返回 { globalRules: [], domainRules: [{ meta, content }] }
|
|
116
|
+
*/
|
|
117
|
+
function loadRules(rulesDir) {
|
|
118
|
+
const globalRules = [];
|
|
119
|
+
const domainRules = [];
|
|
120
|
+
|
|
121
|
+
if (!fs.existsSync(rulesDir)) {
|
|
122
|
+
console.warn(`⚠️ 规则目录不存在: ${rulesDir}`);
|
|
123
|
+
return { globalRules, domainRules };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ruleFiles = fs
|
|
127
|
+
.readdirSync(rulesDir)
|
|
128
|
+
.filter((fileName) => fileName.endsWith(".md"))
|
|
129
|
+
.sort();
|
|
130
|
+
|
|
131
|
+
for (const fileName of ruleFiles) {
|
|
132
|
+
const filePath = path.join(rulesDir, fileName);
|
|
133
|
+
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
134
|
+
const { meta, content } = parseFrontmatter(rawContent);
|
|
135
|
+
|
|
136
|
+
if (meta.applyTo && meta.outputName) {
|
|
137
|
+
domainRules.push({ meta, content, fileName });
|
|
138
|
+
console.log(
|
|
139
|
+
` 📂 域规则: ${fileName} → ${meta.outputName} (${meta.applyTo.join(", ")})`,
|
|
140
|
+
);
|
|
141
|
+
} else {
|
|
142
|
+
globalRules.push({ meta, content, fileName });
|
|
143
|
+
console.log(` 🌐 全局规则: ${fileName}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { globalRules, domainRules };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 解析 AGENTS.private.md 中的域标记,按域分发私有规则
|
|
152
|
+
* 支持格式: <!-- @domain: go-backend -->
|
|
153
|
+
* 未标记的内容归入全局
|
|
154
|
+
*/
|
|
155
|
+
function parsePrivateDomains(privateContent) {
|
|
156
|
+
if (!privateContent) {
|
|
157
|
+
return { global: "", domains: {} };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const domainMarkerRegex = /<!--\s*@domain:\s*([\w-]+)\s*-->/g;
|
|
161
|
+
const sections = [];
|
|
162
|
+
let lastIndex = 0;
|
|
163
|
+
let currentMatch;
|
|
164
|
+
|
|
165
|
+
while ((currentMatch = domainMarkerRegex.exec(privateContent)) !== null) {
|
|
166
|
+
// 前一段归入上一个域(或全局)
|
|
167
|
+
if (currentMatch.index > lastIndex) {
|
|
168
|
+
sections.push({
|
|
169
|
+
domain:
|
|
170
|
+
sections.length === 0
|
|
171
|
+
? null
|
|
172
|
+
: sections[sections.length - 1].nextDomain,
|
|
173
|
+
text: privateContent.slice(lastIndex, currentMatch.index),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// 记录下一个域名
|
|
177
|
+
if (sections.length > 0) {
|
|
178
|
+
sections[sections.length - 1].nextDomain = currentMatch[1];
|
|
179
|
+
} else {
|
|
180
|
+
sections.push({ domain: null, text: "", nextDomain: currentMatch[1] });
|
|
181
|
+
}
|
|
182
|
+
lastIndex = currentMatch.index + currentMatch[0].length;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 最后一段
|
|
186
|
+
if (lastIndex < privateContent.length) {
|
|
187
|
+
const lastDomain =
|
|
188
|
+
sections.length > 0 ? sections[sections.length - 1].nextDomain : null;
|
|
189
|
+
sections.push({
|
|
190
|
+
domain: lastDomain || null,
|
|
191
|
+
text: privateContent.slice(lastIndex),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = { global: "", domains: {} };
|
|
196
|
+
for (const section of sections) {
|
|
197
|
+
const trimmedText = section.text.trim();
|
|
198
|
+
if (!trimmedText) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (!section.domain) {
|
|
202
|
+
result.global += (result.global ? "\n\n" : "") + trimmedText;
|
|
203
|
+
} else {
|
|
204
|
+
if (!result.domains[section.domain]) {
|
|
205
|
+
result.domains[section.domain] = "";
|
|
206
|
+
}
|
|
207
|
+
result.domains[section.domain] +=
|
|
208
|
+
(result.domains[section.domain] ? "\n\n" : "") + trimmedText;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 生成 .github/instructions/*.instructions.md 文件
|
|
217
|
+
* 带 applyTo frontmatter,供 VS Code Copilot 条件加载
|
|
218
|
+
*/
|
|
219
|
+
function generateInstructionFile(
|
|
220
|
+
instructionsDir,
|
|
221
|
+
outputName,
|
|
222
|
+
applyTo,
|
|
223
|
+
content,
|
|
224
|
+
) {
|
|
225
|
+
const outputPath = path.join(
|
|
226
|
+
instructionsDir,
|
|
227
|
+
`${outputName}.instructions.md`,
|
|
228
|
+
);
|
|
229
|
+
const frontmatter = `---\napplyTo: ${JSON.stringify(applyTo)}\n---\n`;
|
|
230
|
+
const fileContent = frontmatter + "\n" + content;
|
|
231
|
+
|
|
232
|
+
ensureParentDir(outputPath);
|
|
233
|
+
fs.writeFileSync(outputPath, fileContent, "utf-8");
|
|
234
|
+
console.log(`✅ 条件规则已生成: ${outputPath}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
61
237
|
function mergeAgents(config) {
|
|
238
|
+
const defaultConfig = getDefaultConfig();
|
|
62
239
|
const {
|
|
63
|
-
baseRulesPath =
|
|
64
|
-
privateRulesPath =
|
|
240
|
+
baseRulesPath = defaultConfig.baseRulesPath,
|
|
241
|
+
privateRulesPath = defaultConfig.privateRulesPath,
|
|
242
|
+
rulesDir = defaultConfig.rulesDir,
|
|
243
|
+
instructionsDir = defaultConfig.instructionsDir,
|
|
65
244
|
} = config;
|
|
66
|
-
|
|
245
|
+
|
|
246
|
+
const agentsOutput = config.output || defaultConfig.output;
|
|
247
|
+
const copilotOutput = config.copilotOutput || defaultConfig.copilotOutput;
|
|
67
248
|
|
|
68
249
|
console.log("📦 开始合并规则文件...");
|
|
69
|
-
console.log(`
|
|
250
|
+
console.log(` 规则目录: ${rulesDir}`);
|
|
70
251
|
console.log(` 私有规则: ${privateRulesPath}`);
|
|
71
|
-
console.log(` 输出文件: ${outputs.join(", ")}`);
|
|
72
252
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
console.error(`❌ 基础规则文件不存在: ${baseRulesPath}`);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
253
|
+
// 1. 加载 rules/ 目录的规则文件
|
|
254
|
+
const { globalRules, domainRules } = loadRules(rulesDir);
|
|
78
255
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
256
|
+
if (globalRules.length === 0 && domainRules.length === 0) {
|
|
257
|
+
// 回退:如果 rules/ 目录为空,使用旧版 AGENTS.base.md
|
|
258
|
+
console.log(" ⚠️ rules/ 目录为空,回退使用 AGENTS.base.md");
|
|
259
|
+
const baseRules = readFileIfExists(baseRulesPath);
|
|
260
|
+
if (!baseRules) {
|
|
261
|
+
console.error(`❌ 基础规则文件不存在: ${baseRulesPath}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
82
264
|
|
|
83
|
-
|
|
84
|
-
merged
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
265
|
+
const privateRules = readFileIfExists(privateRulesPath);
|
|
266
|
+
let merged = baseRules;
|
|
267
|
+
if (privateRules) {
|
|
268
|
+
merged +=
|
|
269
|
+
"\n\n---\n\n# 项目私有规则\n\n以下规则仅适用于本项目,会覆盖基础规则中的相同部分。\n\n" +
|
|
270
|
+
privateRules;
|
|
271
|
+
}
|
|
89
272
|
|
|
90
|
-
|
|
91
|
-
try {
|
|
273
|
+
for (const outputPath of [agentsOutput, copilotOutput]) {
|
|
92
274
|
ensureParentDir(outputPath);
|
|
93
275
|
fs.writeFileSync(outputPath, merged, "utf-8");
|
|
94
276
|
console.log(`✅ 规则已合并到 ${outputPath}`);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error(`❌ 写入文件失败 ${outputPath}:`, e.message);
|
|
97
|
-
process.exit(1);
|
|
98
277
|
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 2. 读取并解析私有规则的域标记
|
|
282
|
+
const privateContent = readFileIfExists(privateRulesPath);
|
|
283
|
+
const privateDomains = parsePrivateDomains(privateContent);
|
|
284
|
+
|
|
285
|
+
// 3. 拼接全局规则内容
|
|
286
|
+
const globalContent = globalRules
|
|
287
|
+
.map((rule) => rule.content.trim())
|
|
288
|
+
.join("\n\n");
|
|
289
|
+
|
|
290
|
+
// 4. 生成 .github/copilot-instructions.md(全局规则 + 私有全局部分)
|
|
291
|
+
let copilotContent = "# Copilot Agent Rules\n\n" + globalContent;
|
|
292
|
+
if (privateDomains.global) {
|
|
293
|
+
copilotContent += "\n\n---\n\n# 项目私有规则\n\n" + privateDomains.global;
|
|
294
|
+
}
|
|
295
|
+
ensureParentDir(copilotOutput);
|
|
296
|
+
fs.writeFileSync(copilotOutput, copilotContent, "utf-8");
|
|
297
|
+
console.log(`✅ 全局规则已生成: ${copilotOutput}`);
|
|
298
|
+
|
|
299
|
+
// 5. 生成 .github/instructions/*.instructions.md(各域规则)
|
|
300
|
+
ensureParentDir(path.join(instructionsDir, "_placeholder"));
|
|
301
|
+
for (const rule of domainRules) {
|
|
302
|
+
let domainContent = rule.content.trim();
|
|
303
|
+
// 追加对应域的私有规则
|
|
304
|
+
const privateDomainContent = privateDomains.domains[rule.meta.outputName];
|
|
305
|
+
if (privateDomainContent) {
|
|
306
|
+
domainContent +=
|
|
307
|
+
"\n\n---\n\n# 项目私有规则(" +
|
|
308
|
+
rule.meta.name +
|
|
309
|
+
")\n\n" +
|
|
310
|
+
privateDomainContent;
|
|
311
|
+
}
|
|
312
|
+
generateInstructionFile(
|
|
313
|
+
instructionsDir,
|
|
314
|
+
rule.meta.outputName,
|
|
315
|
+
rule.meta.applyTo,
|
|
316
|
+
domainContent,
|
|
317
|
+
);
|
|
99
318
|
}
|
|
319
|
+
|
|
320
|
+
// 6. 生成 AGENTS.md(全量合并,兼容 Cursor/Claude Code)
|
|
321
|
+
const allRulesContent = [
|
|
322
|
+
globalContent,
|
|
323
|
+
...domainRules.map((rule) => rule.content.trim()),
|
|
324
|
+
].join("\n\n");
|
|
325
|
+
let agentsMerged =
|
|
326
|
+
"# Copilot Agent Rules - Base\n\n以下为始终生效的核心规则。各项目可通过 `AGENTS.private.md` 添加项目特定规则。\n\n" +
|
|
327
|
+
allRulesContent;
|
|
328
|
+
if (privateContent) {
|
|
329
|
+
agentsMerged +=
|
|
330
|
+
"\n\n---\n\n# 项目私有规则\n\n以下规则仅适用于本项目,会覆盖基础规则中的相同部分。\n\n" +
|
|
331
|
+
privateContent;
|
|
332
|
+
}
|
|
333
|
+
ensureParentDir(agentsOutput);
|
|
334
|
+
fs.writeFileSync(agentsOutput, agentsMerged, "utf-8");
|
|
335
|
+
console.log(`✅ 全量规则已合并到 ${agentsOutput}`);
|
|
100
336
|
}
|
|
101
337
|
|
|
102
338
|
function initAgents() {
|
|
@@ -113,9 +349,11 @@ function initAgents() {
|
|
|
113
349
|
console.log(`✅ 已创建私有规则模板 ${privateOutputPath}`);
|
|
114
350
|
}
|
|
115
351
|
|
|
116
|
-
console.log(
|
|
117
|
-
|
|
118
|
-
);
|
|
352
|
+
console.log("✅ 初始化完成,后续执行 agent-rules sync 即可刷新规则文件");
|
|
353
|
+
console.log(" 输出文件:");
|
|
354
|
+
console.log(" - AGENTS.md(全量,兼容 Cursor/Claude Code)");
|
|
355
|
+
console.log(" - .github/copilot-instructions.md(全局规则)");
|
|
356
|
+
console.log(" - .github/instructions/*.instructions.md(按域条件加载)");
|
|
119
357
|
}
|
|
120
358
|
|
|
121
359
|
function runSync(triggerSource) {
|
|
@@ -192,18 +430,29 @@ function main() {
|
|
|
192
430
|
if (command === "--help" || command === "-h" || command === "help") {
|
|
193
431
|
console.log("agent-rules 使用说明:");
|
|
194
432
|
console.log(
|
|
195
|
-
" agent-rules
|
|
433
|
+
" agent-rules 初始化规则文件,并在缺失时创建 AGENTS.private.md 模板",
|
|
196
434
|
);
|
|
197
435
|
console.log(
|
|
198
|
-
" agent-rules init
|
|
436
|
+
" agent-rules init 初始化规则文件,并在缺失时创建 AGENTS.private.md 模板",
|
|
199
437
|
);
|
|
200
438
|
console.log(
|
|
201
|
-
" agent-rules sync
|
|
439
|
+
" agent-rules sync 重新生成所有规则输出文件(AGENTS.md + copilot-instructions.md + instructions/*.instructions.md)",
|
|
202
440
|
);
|
|
203
441
|
console.log(
|
|
204
|
-
" agent-rules watch 监听 AGENTS.private.md
|
|
442
|
+
" agent-rules watch 监听 AGENTS.private.md,变更后自动同步所有规则文件",
|
|
205
443
|
);
|
|
206
444
|
console.log(" agent-rules help 查看帮助");
|
|
445
|
+
console.log("");
|
|
446
|
+
console.log("输出文件:");
|
|
447
|
+
console.log(
|
|
448
|
+
" AGENTS.md 全量合并(兼容 Cursor/Claude Code)",
|
|
449
|
+
);
|
|
450
|
+
console.log(
|
|
451
|
+
" .github/copilot-instructions.md 全局规则(VS Code Copilot)",
|
|
452
|
+
);
|
|
453
|
+
console.log(
|
|
454
|
+
" .github/instructions/*.instructions.md 按域条件加载(VS Code Copilot applyTo)",
|
|
455
|
+
);
|
|
207
456
|
return;
|
|
208
457
|
}
|
|
209
458
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routerhub/agent-rules",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared Copilot agent rules and guidelines for RouterHub projects",
|
|
5
5
|
"main": "AGENTS.base.md",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"AGENTS.private.example.md",
|
|
14
14
|
"README.zh-CN.md",
|
|
15
15
|
"postinstall.js",
|
|
16
|
-
"package.json"
|
|
16
|
+
"package.json",
|
|
17
|
+
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "node --check merge.js && node --check postinstall.js",
|
package/postinstall.js
CHANGED
|
@@ -36,7 +36,7 @@ initProcess.on("close", (code) => {
|
|
|
36
36
|
|
|
37
37
|
console.log("[agent-rules] 后台监听进程已启动");
|
|
38
38
|
console.log(
|
|
39
|
-
"[agent-rules] AGENTS.private.md 的变更将自动同步到 AGENTS.md 和 .github/
|
|
39
|
+
"[agent-rules] AGENTS.private.md 的变更将自动同步到 AGENTS.md、.github/copilot-instructions.md 和 .github/instructions/*.instructions.md",
|
|
40
40
|
);
|
|
41
41
|
console.log("");
|
|
42
42
|
console.log("[agent-rules] 手动命令:");
|
package/rules/devops.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "DevOps 部署规则"
|
|
3
|
+
applyTo:
|
|
4
|
+
[
|
|
5
|
+
"**/Dockerfile",
|
|
6
|
+
"**/Dockerfile.*",
|
|
7
|
+
"**/docker-compose*.yml",
|
|
8
|
+
"**/docker-compose*.yaml",
|
|
9
|
+
"**/*.sh",
|
|
10
|
+
"**/.gitlab-ci.yml",
|
|
11
|
+
"**/.github/workflows/*.yml",
|
|
12
|
+
"**/.github/workflows/*.yaml",
|
|
13
|
+
]
|
|
14
|
+
outputName: "devops"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Docker 与部署
|
|
18
|
+
|
|
19
|
+
- 默认本地 `docker build`,构建后 `docker push` 到 `ghcr.io`,推送前确保正确 tag。
|
|
20
|
+
- 部署通过 Portainer 管理界面操作,禁止 SSH 手动执行 docker 命令;地址和账号配置在 `AGENTS.private.md` 或环境变量中,禁止硬编码。
|
|
21
|
+
- 始终通过 Portainer API 更新 Stack 环境变量触发重新部署,不要 SSH 手动操作 Portainer 管理的容器。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "前端规则"
|
|
3
|
+
applyTo:
|
|
4
|
+
[
|
|
5
|
+
"**/*.ts",
|
|
6
|
+
"**/*.tsx",
|
|
7
|
+
"**/*.js",
|
|
8
|
+
"**/*.jsx",
|
|
9
|
+
"**/*.vue",
|
|
10
|
+
"**/*.css",
|
|
11
|
+
"**/*.scss",
|
|
12
|
+
]
|
|
13
|
+
outputName: "frontend"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 文件约束
|
|
17
|
+
|
|
18
|
+
- 不随意修改核心业务文件和 API 相关代码,不编写或修改 `README.md`。
|
|
19
|
+
- TypeScript 类型优先复用 `typings.d.ts`,不存在时再自定义。
|
|
20
|
+
|
|
21
|
+
## 命名与类型
|
|
22
|
+
|
|
23
|
+
- 驼峰命名(小驼峰/大驼峰),禁止下划线;变量至少两个单词。
|
|
24
|
+
- 禁止 `as` 和 `any`;`props` 类型优先复用,不重复定义。
|
|
25
|
+
|
|
26
|
+
## 代码风格
|
|
27
|
+
|
|
28
|
+
- 函数式编程,不写 `class`,不写 `try/catch`。
|
|
29
|
+
- 禁止重复实现:复用现有函数/组件/配置,发现重复必须提取封装为公共方法。
|
|
30
|
+
- 同一数据/配置只在一处维护,其余通过引用获取,改一处全局生效。
|
|
31
|
+
- 禁止硬编码数字;常量/枚举定义在 `Const.ts` 或 `constants.ts`,`tab` 索引统一使用常量,时间用 `dayjs`。
|
|
32
|
+
- 遵循 SOLID 原则,避免过度设计;数据回显优先用展开运算符(`...`)。
|
|
33
|
+
- 复用组件通过 `fromType`(值为当前页面名)区分来源,内部差异逻辑基于 `fromType` 分支。
|
|
34
|
+
|
|
35
|
+
## 样式(Tailwind CSS)
|
|
36
|
+
|
|
37
|
+
- 统一 Tailwind CSS,禁止新增 `*.module.scss`、`*.module.css`、`.css` 文件(历史文件可保留)。
|
|
38
|
+
- 项目未集成 Tailwind 时,先完成安装配置再开发。
|
|
39
|
+
- 新增样式通过 utility classes 在 JSX/TSX 中编写;不满足时优先 `tailwind.config` extend → `@apply` 封装 → 行内 `style`。
|
|
40
|
+
- 复杂/重复样式提取为 Tailwind 组件类或 React 组件,避免工具类堆砌。
|
|
41
|
+
- 类名小驼峰,禁止 `span` 标签选择器。
|
|
42
|
+
|
|
43
|
+
## 组件复用
|
|
44
|
+
|
|
45
|
+
- 优先复用已有组件,能通过 `props` 定制就不建新组件;新功能尽量封装为可复用组件。
|
|
46
|
+
|
|
47
|
+
## 前端 API 规范
|
|
48
|
+
|
|
49
|
+
- API 请求严格使用 OpenAPI 生成的方法,禁止手写请求或直接拼接路径。
|
|
50
|
+
- 接口变更后先更新 OpenAPI 定义并重新生成 API 代码,再进行业务开发。
|
|
51
|
+
|
|
52
|
+
## 依赖与构建
|
|
53
|
+
|
|
54
|
+
- 依赖安装统一 `pnpm`,禁止 `npm install`/`yarn install`。
|
|
55
|
+
- 写完代码先 `npm run format`,再 `npm run build`,构建通过才可发版。
|
|
56
|
+
|
|
57
|
+
## 测试
|
|
58
|
+
|
|
59
|
+
- 测试描述、断言使用中文,覆盖中文场景;充值类测试默认用充值 1 的数据。
|
|
60
|
+
- 默认使用真实接口数据,禁止 mock;仅明确同意时允许模拟数据。
|
|
61
|
+
- 写完业务后执行 `pnpm run test:e2e:ui`,告知开发者对应测试用例名称;通过后打开 UI 供手动验证。
|
|
62
|
+
- 端口被占用时自动切换新端口。
|
|
63
|
+
|
|
64
|
+
## 错误日志
|
|
65
|
+
|
|
66
|
+
- 用 `console.error` 记录错误(含函数名/模块名上下文),禁止 `console.log` 输出错误。
|
|
67
|
+
- 提交前移除调试日志和临时代码。
|
package/rules/global.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "通用规则"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 语言与内容
|
|
6
|
+
|
|
7
|
+
- 始终使用中文回答,代码注释使用中文,每行代码配中文注释。
|
|
8
|
+
- 页面 UI 内容(按钮、字段名、提示等)全部英文;如需中文在 `AGENTS.private.md` 中声明。
|
|
9
|
+
|
|
10
|
+
## Git 规范
|
|
11
|
+
|
|
12
|
+
- 分支用 Git Flow(`feature/`、`bugfix/`、`hotfix/`、`refactor/`、`chore/`、`docs/`、`test/`),英文小写中划线分隔。
|
|
13
|
+
- commit 信息必须中文,禁止 `git push --force`。
|
|
14
|
+
|
|
15
|
+
## 安全
|
|
16
|
+
|
|
17
|
+
- 未明确授权不操作生产环境(指令需含"生产"/"线上"/"prod"/"production"),操作前二次确认。
|
|
18
|
+
|
|
19
|
+
## 需求文档
|
|
20
|
+
|
|
21
|
+
- 新需求先在 `docs/` 写 MD 文档,含:目标、范围、功能要求、验收标准。
|
|
22
|
+
|
|
23
|
+
## 优先级
|
|
24
|
+
|
|
25
|
+
1. 项目私有规则(AGENTS.private.md)
|
|
26
|
+
2. 基础规则(此文件)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Go 后端规则"
|
|
3
|
+
applyTo: ["**/*.go"]
|
|
4
|
+
outputName: "go-backend"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Go 语言规则
|
|
8
|
+
|
|
9
|
+
- 优先值传递与局部变量生命周期控制,避免内存逃逸;无必要不返回局部变量指针。
|
|
10
|
+
- 所有删除操作必须使用软删除机制;`DeletedAt` 字段类型必须用 `gorm.DeletedAt`,禁止用 `*time.Time`(后者不实现 `DeleteClausesInterface`,会导致硬删除)。
|
|
11
|
+
|
|
12
|
+
## GORM Model 规范
|
|
13
|
+
|
|
14
|
+
- `DeletedAt` 字段必须使用 `gorm.DeletedAt` 类型,严禁使用 `*time.Time`。`*time.Time` 不会触发 GORM v2 的软删除机制,会导致 `Delete()` 执行物理删除(DELETE FROM)而非软删除(UPDATE SET deleted_at)。
|
|
15
|
+
- 新建 Model 时,优先嵌入已有的公共基础结构体(如包含 ID、CreatedAt、UpdatedAt、DeletedAt 的 BaseModel),避免各 Model 独立定义这些字段导致类型不一致。
|