Skip to content

Commit e12d20b

Browse files
committed
feat: 插件自动注入编程AI规则文件(9种IDE) - rulesInjector.ts + 手动/自动注入命令
1 parent 5803000 commit e12d20b

3 files changed

Lines changed: 351 additions & 0 deletions

File tree

extension/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@
142142
"command": "testpilot-ai.connectDevice.en",
143143
"title": "TestPilot AI: Connect Mobile Device",
144144
"icon": "$(device-mobile)"
145+
},
146+
{
147+
"command": "testpilot-ai.injectRules",
148+
"title": "TestPilot AI: 注入蓝本规则到项目",
149+
"icon": "$(file-symlink-directory)"
150+
},
151+
{
152+
"command": "testpilot-ai.injectRules.en",
153+
"title": "TestPilot AI: Inject Blueprint Rules",
154+
"icon": "$(file-symlink-directory)"
145155
}
146156
],
147157
"menus": {
@@ -212,6 +222,12 @@
212222
},
213223
{
214224
"command": "testpilot-ai.connectDevice.en"
225+
},
226+
{
227+
"command": "testpilot-ai.injectRules"
228+
},
229+
{
230+
"command": "testpilot-ai.injectRules.en"
215231
}
216232
]
217233
},

extension/src/extension.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import * as vscode from "vscode";
1212
import { EngineClient } from "./engineClient";
1313
import { SidebarProvider } from "./sidebarProvider";
14+
import { autoInjectOnActivate, injectRules } from "./rulesInjector";
1415

1516
let client: EngineClient;
1617
let outputChannel: vscode.OutputChannel;
@@ -506,6 +507,7 @@ export function activate(context: vscode.ExtensionContext): void {
506507
["testpilot-ai.launchEngine.en", "testpilot-ai.launchEngine"],
507508
["testpilot-ai.stopEngine.en", "testpilot-ai.stopEngine"],
508509
["testpilot-ai.connectDevice.en", "testpilot-ai.connectDevice"],
510+
["testpilot-ai.injectRules.en", "testpilot-ai.injectRules"],
509511
];
510512

511513
for (const [alias, target] of commandAliases) {
@@ -514,6 +516,42 @@ export function activate(context: vscode.ExtensionContext): void {
514516
);
515517
}
516518

519+
// ── 手动注入规则命令 ──
520+
context.subscriptions.push(
521+
vscode.commands.registerCommand("testpilot-ai.injectRules", async () => {
522+
const folders = vscode.workspace.workspaceFolders;
523+
if (!folders || folders.length === 0) {
524+
vscode.window.showWarningMessage("请先打开一个项目文件夹");
525+
return;
526+
}
527+
528+
let targetFolder: vscode.WorkspaceFolder;
529+
if (folders.length === 1) {
530+
targetFolder = folders[0];
531+
} else {
532+
const picked = await vscode.window.showWorkspaceFolderPick({
533+
placeHolder: "选择要注入规则的项目",
534+
});
535+
if (!picked) { return; }
536+
targetFolder = picked;
537+
}
538+
539+
const result = injectRules(targetFolder.uri.fsPath, outputChannel);
540+
if (result.created.length > 0) {
541+
vscode.window.showInformationMessage(
542+
`✅ 已注入 ${result.created.length} 个规则文件到 ${targetFolder.name}`,
543+
);
544+
} else {
545+
vscode.window.showInformationMessage(
546+
`${targetFolder.name} 已有所有规则文件,无需重复注入`,
547+
);
548+
}
549+
}),
550+
);
551+
552+
// ── 插件激活时自动注入规则到工作区 ──
553+
autoInjectOnActivate(outputChannel);
554+
517555
outputChannel.appendLine("[TestPilot AI] 所有命令已注册(中文+英文)");
518556
outputChannel.appendLine("[TestPilot AI] 💡 Tip: Search 'TestPilot AI' in Command Palette (Ctrl+Shift+P) for all commands");
519557
}

extension/src/rulesInjector.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
/**
2+
* TestPilot AI — 编程AI规则文件注入器
3+
*
4+
* 插件激活时自动在用户项目中创建规则文件,
5+
* 让所有主流IDE的编程AI都能读到蓝本生成规则。
6+
*
7+
* 覆盖9种IDE:
8+
* - AGENTS.md(跨工具通用:Cursor/Cline/Augment/Kilo Code)
9+
* - .github/copilot-instructions.md(VS Code Copilot)
10+
* - .cursor/rules/testpilot.md(Cursor)
11+
* - .windsurf/rules/testpilot.md(Windsurf)
12+
* - .trae/rules/testpilot.md(Trae / 字节跳动)
13+
* - .clinerules/testpilot.md(Cline)
14+
* - .aiassistant/rules/testpilot.md(JetBrains AI)
15+
* - .augment/rules/testpilot.md(Augment Code)
16+
* - CLAUDE.md(Claude Code / Anthropic CLI)
17+
*/
18+
19+
import * as vscode from "vscode";
20+
import * as fs from "fs";
21+
import * as path from "path";
22+
23+
/** 需要注入的规则文件相对路径列表 */
24+
const RULES_FILES: string[] = [
25+
"AGENTS.md",
26+
"CLAUDE.md",
27+
".github/copilot-instructions.md",
28+
".cursor/rules/testpilot.md",
29+
".windsurf/rules/testpilot.md",
30+
".trae/rules/testpilot.md",
31+
".clinerules/testpilot.md",
32+
".aiassistant/rules/testpilot.md",
33+
".augment/rules/testpilot.md",
34+
];
35+
36+
/** 规则模板内容(内嵌,避免依赖外部文件) */
37+
function getTemplateContent(): string {
38+
return `# TestPilot AI — 编程AI蓝本自动生成规则
39+
40+
> 本文件由 TestPilot AI 插件自动注入,指导编程AI在开发过程中自动生成和维护测试蓝本。
41+
> 你可以根据项目需求自由修改本文件内容。
42+
43+
---
44+
45+
## 一、蓝本自动生成(最重要)
46+
47+
当项目中**不存在** \`testpilot.json\` 或 \`testpilot/\` 目录时,你在完成功能开发后**必须**自动生成蓝本文件。
48+
49+
### 生成时机
50+
51+
| 时机 | 动作 |
52+
|------|------|
53+
| 新项目创建完成 | 生成完整蓝本,覆盖所有已实现功能 |
54+
| 新增一个完整功能模块 | 在蓝本中追加对应场景 |
55+
| 项目初始蓝本不存在 | 扫描现有代码,生成功能全覆盖的蓝本 |
56+
57+
### 生成规则
58+
59+
1. 蓝本文件固定命名 \`testpilot.json\`,放在项目根目录或 \`testpilot/\` 子目录下
60+
2. 必须覆盖项目的**所有用户可交互功能**,不允许遗漏
61+
3. 每个功能模块对应一个 \`page\`,每个用户场景对应一个 \`scenario\`
62+
4. 如果项目功能较多,按模块拆分为多个蓝本文件放在 \`testpilot/\` 目录下:
63+
- \`testpilot/auth.testpilot.json\` — 登录注册
64+
- \`testpilot/product.testpilot.json\` — 商品管理
65+
- \`testpilot/order.testpilot.json\` — 订单流程
66+
5. 每个蓝本必须包含 \`app_name\`、\`description\`、\`platform\`、\`base_url\` 字段
67+
68+
---
69+
70+
## 二、蓝本增量维护(日常开发)
71+
72+
当项目已有蓝本文件时,以下6种代码变更**必须**同步更新蓝本:
73+
74+
| # | 触发条件 | 蓝本更新动作 |
75+
|---|---------|------------|
76+
| 1 | 新增/删除 UI 元素 | 添加/删除对应场景和步骤 |
77+
| 2 | 修改元素 id/class/选择器 | 更新蓝本中所有用到该选择器的 \`target\` |
78+
| 3 | 修改文本内容(按钮文字/提示/错误信息) | 更新 \`assert_text\` 的 \`expected\` |
79+
| 4 | 修改业务逻辑(表单验证/跳转/计算) | 更新断言和预期结果 |
80+
| 5 | 修复 Bug | 更新蓝本中对应断言,确保能检测到该Bug |
81+
| 6 | 修改应用配置(URL/端口/路由/启动命令) | 更新 \`base_url\` / \`start_command\` |
82+
83+
**不触发更新的情况**:纯CSS样式调整、代码注释修改、内部重构(不影响用户可见行为)、测试文件修改。
84+
85+
---
86+
87+
## 三、蓝本基本结构
88+
89+
\`\`\`json
90+
{
91+
"app_name": "应用名称",
92+
"description": "应用功能的完整描述(50-200字)",
93+
"base_url": "http://localhost:3000",
94+
"platform": "web",
95+
"start_command": "npm start",
96+
"start_cwd": ".",
97+
"pages": [
98+
{
99+
"url": "/",
100+
"name": "首页",
101+
"scenarios": [
102+
{
103+
"name": "场景名称",
104+
"description": "测试目标",
105+
"steps": [
106+
{"action": "navigate", "value": "/", "description": "打开首页"},
107+
{"action": "fill", "target": "#username", "value": "testuser", "description": "在用户名输入框输入"},
108+
{"action": "click", "target": "#loginBtn", "description": "点击登录按钮"},
109+
{"action": "assert_text", "expected": "欢迎", "description": "验证登录成功显示欢迎信息"},
110+
{"action": "screenshot", "description": "登录成功后的页面"}
111+
]
112+
}
113+
]
114+
}
115+
]
116+
}
117+
\`\`\`
118+
119+
### platform 取值
120+
121+
| 值 | 适用场景 |
122+
|----|---------|
123+
| \`web\` | 网页应用(React/Vue/Angular/纯HTML) |
124+
| \`desktop\` | Windows桌面应用(需额外填 \`window_title\`) |
125+
| \`android\` | Android应用(需额外填 \`app_package\`、\`app_activity\`) |
126+
| \`ios\` | iOS应用(需额外填 \`bundle_id\`) |
127+
| \`miniprogram\` | 微信小程序(\`base_url\` 格式为 \`miniprogram://项目路径\`) |
128+
129+
### 步骤动作
130+
131+
| 动作 | 必填参数 | 说明 |
132+
|------|---------|------|
133+
| \`click\` | \`target\`, \`description\` | 点击元素 |
134+
| \`fill\` | \`target\`, \`value\`, \`description\` | 输入文本 |
135+
| \`screenshot\` | \`description\` | 截图 |
136+
| \`assert_text\` | \`expected\`, \`description\` | 断言页面包含文本 |
137+
| \`wait\` | \`description\` | 等待(可用 \`value\` 指定毫秒) |
138+
| \`navigate\` | \`value\`(URL), \`description\` | 页面跳转 |
139+
140+
### target 写法
141+
142+
- **Web/小程序**:CSS选择器,如 \`#loginBtn\`、\`.submit-btn\`、\`input[name="email"]\`
143+
- **桌面应用**:\`name:屏幕上可见的原文\`,如 \`name:Login\`、\`name:确定\`
144+
- ⚠️ **禁止**在 \`name:\` 后加中文后缀(按钮/输入框/列表项等)
145+
- **Android/iOS**:\`accessibility_id:xxx\` 或 \`id:xxx\`
146+
147+
### description 最佳实践
148+
149+
每个步骤的 \`description\` 应包含:
150+
1. **位置**:上方/下方/左侧/右侧
151+
2. **预期变化**:点击后页面会发生什么(编程AI已读过源码,应预测页面变化)
152+
153+
\`\`\`
154+
✅ "点击提交按钮,点击后表单数据提交到后端,页面显示'提交成功'提示"
155+
❌ "点击按钮"
156+
\`\`\`
157+
158+
---
159+
160+
## 四、边写代码边维护蓝本(推荐工作流)
161+
162+
**最佳实践**:不要等项目全部写完才生成蓝本,而是**每实现一个功能就追加一个场景**。
163+
164+
\`\`\`
165+
实现登录功能 → 立即在蓝本中添加"登录成功"和"登录失败"两个场景
166+
实现商品列表 → 立即添加"商品展示"和"搜索过滤"场景
167+
实现购物车 → 立即添加"加入购物车"和"修改数量"和"删除商品"场景
168+
\`\`\`
169+
170+
这样做的好处:
171+
- 功能不会遗漏(写一个测一个)
172+
- 蓝本的选择器和预期值一定是准确的(刚写完代码,记忆最清晰)
173+
- 用户随时可以跑测试验证
174+
175+
---
176+
177+
## 五、蓝本自检清单
178+
179+
生成或修改蓝本后,逐项检查:
180+
181+
- [ ] 所有用户可交互功能都有对应场景
182+
- [ ] \`target\` 选择器与实际代码中的 id/class 一致
183+
- [ ] \`expected\` 是界面上实际会显示的文字(不是描述性文字)
184+
- [ ] 页面切换后有 \`wait\` 步骤
185+
- [ ] 场景间状态连贯(后一个场景基于前一个的结束状态)
186+
- [ ] \`platform\` 字段正确
187+
- [ ] \`base_url\` 和 \`start_command\` 填写正确
188+
`;
189+
}
190+
191+
/**
192+
* 在指定工作区目录中注入规则文件
193+
* @param workspaceRoot 工作区根目录绝对路径
194+
* @param outputChannel 日志输出通道
195+
* @returns 注入结果 { created: string[], skipped: string[] }
196+
*/
197+
export function injectRules(
198+
workspaceRoot: string,
199+
outputChannel?: vscode.OutputChannel,
200+
): { created: string[]; skipped: string[] } {
201+
const template = getTemplateContent();
202+
const created: string[] = [];
203+
const skipped: string[] = [];
204+
205+
for (const relPath of RULES_FILES) {
206+
const fullPath = path.join(workspaceRoot, relPath);
207+
208+
// 已存在则跳过(不覆盖用户自定义内容)
209+
if (fs.existsSync(fullPath)) {
210+
skipped.push(relPath);
211+
continue;
212+
}
213+
214+
try {
215+
// 创建目录
216+
const dir = path.dirname(fullPath);
217+
if (!fs.existsSync(dir)) {
218+
fs.mkdirSync(dir, { recursive: true });
219+
}
220+
221+
// 写入规则文件
222+
fs.writeFileSync(fullPath, template, "utf-8");
223+
created.push(relPath);
224+
} catch (err) {
225+
const msg = err instanceof Error ? err.message : String(err);
226+
outputChannel?.appendLine(`[TestPilot AI] ⚠️ 创建 ${relPath} 失败: ${msg}`);
227+
}
228+
}
229+
230+
if (created.length > 0) {
231+
outputChannel?.appendLine(
232+
`[TestPilot AI] ✅ 已注入 ${created.length} 个规则文件: ${created.join(", ")}`,
233+
);
234+
}
235+
if (skipped.length > 0) {
236+
outputChannel?.appendLine(
237+
`[TestPilot AI] ⏭️ 跳过 ${skipped.length} 个已存在文件: ${skipped.join(", ")}`,
238+
);
239+
}
240+
241+
return { created, skipped };
242+
}
243+
244+
/**
245+
* 检查工作区是否需要注入规则(没有任何规则文件时才需要)
246+
*/
247+
export function needsInjection(workspaceRoot: string): boolean {
248+
return !RULES_FILES.some((relPath) =>
249+
fs.existsSync(path.join(workspaceRoot, relPath)),
250+
);
251+
}
252+
253+
/**
254+
* 插件激活时自动注入规则到所有工作区
255+
* 只在首次(工作区没有任何规则文件时)自动注入,不会重复打扰用户
256+
*/
257+
export async function autoInjectOnActivate(
258+
outputChannel?: vscode.OutputChannel,
259+
): Promise<void> {
260+
const folders = vscode.workspace.workspaceFolders;
261+
if (!folders) { return; }
262+
263+
for (const folder of folders) {
264+
const root = folder.uri.fsPath;
265+
266+
// 跳过 TestPilotAI 项目本身(我们自己的项目已有规则)
267+
if (fs.existsSync(path.join(root, "cli.py")) && fs.existsSync(path.join(root, "src", "app.py"))) {
268+
outputChannel?.appendLine(`[TestPilot AI] 跳过 TestPilotAI 项目本身: ${root}`);
269+
continue;
270+
}
271+
272+
// 检查是否需要注入
273+
if (needsInjection(root)) {
274+
outputChannel?.appendLine(`[TestPilot AI] 检测到 ${folder.name} 没有蓝本规则,自动注入中...`);
275+
const result = injectRules(root, outputChannel);
276+
277+
if (result.created.length > 0) {
278+
vscode.window.showInformationMessage(
279+
`TestPilot AI: 已为 ${folder.name} 注入 ${result.created.length} 个编程AI规则文件,编程AI将自动生成测试蓝本`,
280+
"查看详情",
281+
).then((action) => {
282+
if (action === "查看详情") {
283+
// 打开 AGENTS.md 让用户看看注入了什么
284+
const agentsPath = path.join(root, "AGENTS.md");
285+
if (fs.existsSync(agentsPath)) {
286+
vscode.workspace.openTextDocument(agentsPath).then((doc) => {
287+
vscode.window.showTextDocument(doc, { preview: true });
288+
});
289+
}
290+
}
291+
});
292+
}
293+
} else {
294+
outputChannel?.appendLine(`[TestPilot AI] ${folder.name} 已有规则文件,跳过注入`);
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)