Skip to content

Commit ffffb4d

Browse files
committed
v14.1: 蓝本规则架构重构 - 单一来源化,模板精简v7,按钮提示词提炼为4行口令
1 parent 2c6d40e commit ffffb4d

10 files changed

Lines changed: 216 additions & 171 deletions

File tree

_fix_memo.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
with open(r'd:\projects\TestPilotAI\开发备忘录.md', 'r', encoding='utf-8') as f:
3+
content = f.read()
4+
5+
idx = content.find('\n## 30. v14.2')
6+
before = content[:idx]
7+
8+
new_section = """
9+
10+
---
11+
12+
## 30. v14.2 上架准备(2026-03-24)
13+
14+
### 30.1 今日完成内容
15+
16+
| 事项 | 状态 |
17+
|------|------|
18+
| 插件侧边栏"引擎未连接"日志去重 | ✅ 完成(`hasLoggedDisconnected` 标志位)|
19+
| `extension/package.json` 完善(英文描述/关键词/分类/repository字段)| ✅ 完成 |
20+
| 官网语言切换按钮移除(Navbar.jsx)| ✅ 完成 |
21+
| 语言改为域名自动判定(LocaleContext.jsx)| ✅ 完成(修复了 `.com` 误判 Bug)|
22+
| Publisher ID 确认为 `wenzhouxinzao` | ✅ 写入 package.json |
23+
24+
### 30.2 域名语言判断规则(已修复)
25+
26+
**正确逻辑**(以 `LocaleContext.jsx` 为准):
27+
- `testpilotai.pages.dev` → 英文(国际站,Cloudflare Pages 托管)
28+
- `xinzaoai.com` / 其他所有域名 → 中文(国内站)
29+
30+
**修复的 Bug**:之前用 `host.endsWith('.com')` 会把 `xinzaoai.com` 也误判为英文版,现已改为只检测 `host.includes('pages.dev')`。
31+
32+
### 30.3 VS Code Marketplace 关键账号
33+
34+
| 项目 | 值 |
35+
|------|---|
36+
| Publisher ID | `wenzhouxinzao`(永久,账号级别,不会因插件不同而改变)|
37+
| 注册邮箱 | `375612929@qq.com` |
38+
| 国际官网 | https://testpilotai.pages.dev |
39+
| 国内官网 | https://xinzaoai.com |
40+
| PAT 令牌 | 待生成(dev.azure.com → Personal Access Tokens → Marketplace Manage 权限)|
41+
42+
### 30.4 待完成(明日继续)
43+
44+
- [ ] 生成 PAT 令牌并执行 `vsce publish` 正式上架 VS Code Marketplace
45+
- [ ] 确认 `/privacy` 页面在 pages.dev 上是否正确展示
46+
- [ ] 一键安装脚本(Windows .ps1 + macOS .sh)
47+
- [ ] Open VSX Registry 上架(覆盖 Cursor/Windsurf)
48+
49+
### 30.5 commit 记录
50+
51+
| commit | 内容 |
52+
|--------|------|
53+
| `af24e7a` | 日志去重 + 插件重打包 v1.3.0 |
54+
| `b408a2c` | v14.0-E 安全增强(完整实现)|
55+
| `44b2497` | 去掉语言切换按钮 + package.json 完善 |
56+
| `6f7451f` | 修复域名判断 Bug + Publisher ID 改为 wenzhouxinzao |
57+
| `d313005` | 更新上架行动清单(记录账号信息)|
58+
"""
59+
60+
with open(r'd:\projects\TestPilotAI\开发备忘录.md', 'w', encoding='utf-8') as f:
61+
f.write(before + new_section)
62+
63+
print('Done')

extension/esbuild.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ esbuild.build({
1515
}).then(() => {
1616
// 自动同步到所有已安装的 IDE 扩展目录(开发便利)
1717
const home = process.env.USERPROFILE || "";
18-
const extName = "wenzhouxinzao.testpilot-ai-1.3.0";
18+
const extName = "wenzhouxinzao.testpilot-ai-1.4.0";
1919
const ideDirs = [
2020
path.join(home, ".vscode", "extensions", extName),
2121
path.join(home, ".trae-cn", "extensions", extName),

extension/src/rulesInjector.ts

Lines changed: 15 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function detectAllIDEs(): string[] {
8181
* 模板版本号。每次更新模板内容时递增。
8282
* rulesInjector 会检测已注入文件的版本号,低于此版本则自动更新。
8383
*/
84-
const TEMPLATE_VERSION = 6;
84+
const TEMPLATE_VERSION = 7;
8585

8686
/** 从文件内容中提取版本号,找不到返回 0(旧版无版本标记) */
8787
function extractVersion(content: string): number {
@@ -210,131 +210,13 @@ function getTemplateContent(): string {
210210
211211
---
212212
213-
## 三点五、连续流模式(flow)— 强制决策
214-
215-
### ⚠️ 必须执行的决策步骤
216-
217-
生成蓝本时,你**必须**对每个 \`page\` 做出 flow 决策,不允许跳过:
218-
219-
**判断规则(按顺序检查):**
220-
1. 该 page 下有 ≥2 个场景,且这些场景都需要先登录才能操作?→ **\`"flow": true\`**
221-
2. 该 page 下有 ≥2 个场景是同一页面内的 Tab 切换或连续操作?→ **\`"flow": true\`**
222-
3. 该 page 下的场景需要互相独立的干净状态(如:正确登录 vs 错误登录 vs 空字段)?→ **\`"flow": false\`**(可省略,默认就是 false)
223-
224-
**常见错误(你必须避免):**
225-
- ❌ 记账台有8个场景(添加交易、删除交易、切换类型…),每个都独立冷启动+登录 → **严重浪费!必须 flow: true**
226-
- ❌ 报表页有6个场景(看利润表、看费用表、下载报表…),每个都冷启动+登录+跳转到报表页 → **严重浪费!必须 flow: true**
227-
- ✅ 登录页有4个场景(正确登录、空用户名、错密码、注册),每个需要干净状态 → flow: false,正确
228-
229-
**简单总结:如果多个场景都要先登录再操作同一个页面,那这个 page 必须设 \`"flow": true\`。**
230-
231-
### 启用方式
232-
233-
在 \`page\` 对象中添加 \`"flow": true\`:
234-
235-
\`\`\`json
236-
{
237-
"pages": [
238-
{
239-
"url": "",
240-
"title": "记账台",
241-
"flow": true,
242-
"scenarios": [
243-
{
244-
"name": "登录进入记账台",
245-
"steps": [
246-
{"action": "navigate", "value": "com.example.app/.MainActivity", "description": "冷启动应用"},
247-
{"action": "wait", "value": "3000"},
248-
{"action": "fill", "target": "...", "value": "admin"},
249-
{"action": "click", "target": "...", "description": "登录"},
250-
{"action": "wait", "value": "3000"},
251-
{"action": "assert_text", "expected": "记账台"}
252-
]
253-
},
254-
{
255-
"name": "添加一笔交易",
256-
"steps": [
257-
{"action": "navigate", "value": "com.example.app/.MainActivity", "description": "(flow模式下自动跳过)"},
258-
{"action": "fill", "target": "...", "value": "50.00", "description": "输入金额"},
259-
{"action": "click", "target": "...", "description": "提交"},
260-
{"action": "assert_text", "expected": "50.00"}
261-
]
262-
},
263-
{
264-
"name": "删除交易",
265-
"steps": [
266-
{"action": "navigate", "value": "com.example.app/.MainActivity", "description": "(flow模式下自动跳过)"},
267-
{"action": "click", "target": "...", "description": "删除"},
268-
{"action": "assert_text", "expected": "已删除"}
269-
]
270-
}
271-
]
272-
}
273-
]
274-
}
275-
\`\`\`
276-
277-
**注意看:** 只有第1个场景做了完整的登录流程,后续场景直接在当前页面操作。navigate 步骤仍然保留(方便单独运行),但 flow 模式下引擎会自动跳过。
278-
279-
**🚨 flow 非首场景禁止冗余步骤(必须遵守!):**
280-
- flow 第2个及之后的场景,navigate 后面**直接写该场景自己的操作步骤**
281-
- **绝对禁止**在非首场景中重复写 wait→fill用户名→fill密码→click登录 等登录步骤
282-
- 引擎跳过 navigate 后从第2步开始执行,如果第2步是 \`fill 用户名\` 但页面已登录 → 找不到输入框 → 连续超时失败 → 场景被熔断
283-
284-
### flow 模式的行为
285-
286-
| 行为 | \`flow: false\`(默认) | \`flow: true\` |
287-
|------|----------------------|---------------|
288-
| 场景首步 navigate | 执行(冷启动) | **仅第1个场景执行**,后续场景的 navigate 自动跳过 |
289-
| 场景间状态 | 清除(应用重启) | **保持**(前一场景的页面状态) |
290-
| 连续失败止损 | 3个场景连续失败 → 终止 | 3个场景连续失败 → **冷启动恢复**后继续 |
291-
292-
---
293-
294-
## 四、蓝本通用自检清单
295-
296-
生成或修改蓝本后,**按顺序**逐项检查:
297-
298-
- [ ] **【最重要】每个 page 都做了 flow 决策**:多场景共享登录状态 → \`"flow": true\`;需要干净状态的独立测试 → 不写 flow
299-
- [ ] **flow 非首场景没有冗余步骤**:只写 navigate + 该场景自己的操作,不重复登录流程
300-
- [ ] 所有用户可交互功能都有对应场景
301-
- [ ] 每个场景的第一步是 navigate(flow 模式下引擎自动跳过非首场景的 navigate)
302-
- [ ] \`platform\` 字段正确
303-
- [ ] **已阅读对应平台规则文件**,选择器/动作/模板符合平台要求
304-
- [ ] \`target\` 选择器在源码中确实存在(已搜索验证)
305-
- [ ] \`expected\` 是界面上实际渲染的持久化文字(不是瞬态提示、不是变量名、不是注释)
306-
- [ ] 异步操作后有足够的 \`wait\` 时间(已检查代码中的延迟/API调用)
307-
- [ ] 页面跳转后有 \`wait\` + \`assert_text\` 验证到达目标页
308-
- [ ] 每个操作后有断言验证结果(不能只操作不验证)
213+
## 四、所有详细规则 → 参见平台规则文件(必须阅读!)
309214
310-
---
311-
312-
## 五、description 最佳实践
313-
314-
每个步骤的 \`description\` 应包含位置和预期变化:
315-
316-
\`\`\`
317-
✅ "点击提交按钮,点击后表单数据提交到后端,页面显示'提交成功'提示"
318-
✅ "在页面中部的用户名输入框输入admin,输入后输入框显示admin"
319-
❌ "点击按钮"
320-
❌ "输入用户名"
321-
\`\`\`
322-
323-
---
215+
**蓝本的选择器、动作表、完整模板、连续流模式(flow)写法、自检清单、描述规范、踩坑清单等全部详细规则,都在平台专属规则文件中定义。**
324216
325-
## 六、选择器、动作表、模板、注意事项 → 参见平台规则
217+
确定 \`platform\` 后,**立即打开并通读** \`.testpilot/platforms/{platform}.md\`,严格遵守该文件中的所有规则。
326218
327-
**禁止在不阅读平台规则的情况下编写蓝本。** 以下内容在各平台规则文件中定义,不在本文件重复:
328-
329-
- 该平台的封闭式动作表(只允许列出的动作)
330-
- 该平台的选择器格式、优先级、禁止列表
331-
- 该平台的完整 JSON 模板
332-
- 该平台的瞬态 UI 不可断言清单
333-
- 该平台的等待时间计算公式
334-
- 该平台的踩坑清单和常见错误
335-
- 该平台的代码稽核要求
336-
337-
请在确定 \`platform\` 后,立即打开 \`.testpilot/platforms/{platform}.md\` 阅读完整规则。
219+
**禁止在不阅读平台规则文件的情况下编写蓝本。**
338220
`;
339221
}
340222

@@ -478,6 +360,16 @@ export function injectRules(
478360
}
479361
}
480362

363+
// 额外:扫描所有已存在的规则文件,如果版本过旧也一并更新(防止跨IDE版本漂移)
364+
for (const [, ruleFile] of Object.entries(IDE_RULES_MAP)) {
365+
if (!filesToInject.includes(ruleFile)) {
366+
const ruleFullPath = path.join(workspaceRoot, ruleFile);
367+
if (fs.existsSync(ruleFullPath)) {
368+
filesToInject.push(ruleFile);
369+
}
370+
}
371+
}
372+
481373
outputChannel?.appendLine(`[TestPilot AI] 检测到IDE: ${detectedIDEs.join(", ")}`);
482374
}
483375

extension/src/sidebarProvider.ts

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
298298
autoRepair: boolean;
299299
projectPath: string;
300300
}): Promise<void> {
301+
const token = await this._context.secrets.get("testpilot.token");
302+
if (!token) {
303+
this._postMessage({ command: "testError", data: { error: "请先登录账号后再运行测试" } });
304+
return;
305+
}
301306
try {
302307
this._client.ensureWsConnected();
303308
this._postMessage({ command: "testStarted" });
@@ -327,6 +332,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
327332
platform?: string;
328333
mobile_session_id?: string;
329334
}): Promise<void> {
335+
const token = await this._context.secrets.get("testpilot.token");
336+
if (!token) {
337+
this._postMessage({ command: "testError", data: { error: "请先登录账号后再运行测试" } });
338+
return;
339+
}
330340
try {
331341
// 测试前确保 WebSocket 已连接,保证步骤进度能实时推送到 WebView
332342
this._client.ensureWsConnected();
@@ -392,6 +402,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
392402
platform?: string;
393403
mobile_session_id?: string;
394404
}): Promise<void> {
405+
const token = await this._context.secrets.get("testpilot.token");
406+
if (!token) {
407+
this._postMessage({ command: "testError", data: { error: "请先登录账号后再运行测试" } });
408+
return;
409+
}
395410
try {
396411
this._client.ensureWsConnected();
397412
this._postMessage({ command: "testStarted" });
@@ -778,37 +793,15 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
778793
const lines: string[] = [];
779794
if (projectDir) {
780795
lines.push(`当前项目路径:${projectDir.replace(/\\/g, "/")}`);
781-
lines.push(`请直接读取该目录下的源代码生成蓝本,无需再次询问项目路径。`);
782796
lines.push(``);
783797
}
784798
lines.push(`请为当前【${pName}】项目生成或更新测试蓝本。`);
785799
lines.push(``);
786-
lines.push(`⚠️ 生成前请先按顺序完成以下步骤(缺一不可):`);
787-
lines.push(`1. 阅读 AGENTS.md(蓝本通用规则)`);
788-
lines.push(`2. 阅读 .testpilot/platforms/${platform}.md(${pName}平台专属规则、选择器规范和完整模板)`);
789-
lines.push(`3. 通读项目源代码,确认已实现的功能列表`);
790-
lines.push(`4. 检查 testpilot/ 目录是否已有蓝本文件:`);
791-
lines.push(` - 若【没有蓝本】:按规则从零生成完整蓝本,保存到 testpilot/ 目录`);
792-
lines.push(` ⚠️ 生成时 start_cwd 必须填当前项目的绝对路径,禁止填 "." 或相对路径`);
793-
lines.push(` - 若【已有蓝本】:必须按以下步骤做增量更新(禁止跳过任意一步):`);
794-
lines.push(` a. 校验配置字段(必须逐项检查):`);
795-
lines.push(` - start_cwd:必须是绝对路径(如 D:\\projects\\my-app 或 /home/user/my-app),禁止是 "." 或相对路径`);
796-
lines.push(` - base_url:必须非空且格式正确(如 http://localhost:5173)`);
797-
lines.push(` - platform:必须与项目类型匹配`);
798-
lines.push(` - start_command:必须能在 start_cwd 目录下独立运行`);
799-
lines.push(` b. 读取现有蓝本,列出其中所有 target 选择器`);
800-
lines.push(` c. 对每个 target,打开对应的源文件(HTML/JSX/TSX/Vue),验证该选择器真实存在:`);
801-
lines.push(` - 找到对应 DOM 元素:保留`);
802-
lines.push(` - 搜不到或已失效:更正为源码中实际存在的选择器`);
803-
lines.push(` ⚠️【选择器陷阱】React/Vue/Angular 中 <Select>、<Modal>、<Dropdown> 等自定义组件名`);
804-
lines.push(` 不会出现在渲染后的 DOM class 中!必须打开该组件的源文件查看其根元素实际渲染的 class/id。`);
805-
lines.push(` 例如:JSX 里 <CategorySelect /> 渲染后根节点可能是 <div class="relative"> 而不是 <div class="CategorySelect">`);
806-
lines.push(` d. 检查每个页面下的场景数量:若某个 page 有 ≥2 个场景且都需要先登录后操作同一页面,`);
807-
lines.push(` 必须给该 page 设置 "flow": true,避免每个场景都重复冷启动+登录(极大浪费时间)`);
808-
lines.push(` e. 检查源码中是否有新增功能未被现有蓝本覆盖,若有则补充对应场景`);
809-
lines.push(` f. 检查已删除的功能,删除对应场景`);
810-
lines.push(` g. 将更新后的完整蓝本覆盖写回原文件(禁止新建额外文件)`);
811-
lines.push(` ⚠️ 禁止仅凭印象或整体扫描就宣布"无需更新"——必须逐个搜索验证每个 target`);
800+
lines.push(`请按顺序完成以下步骤:`);
801+
lines.push(`1. 阅读本项目的 AGENTS.md`);
802+
lines.push(`2. 阅读 .testpilot/platforms/${platform}.md`);
803+
lines.push(`3. 扫描项目源码,确认已实现的功能`);
804+
lines.push(`4. 按两个文件中的规则,生成或更新 testpilot/ 目录下的蓝本`);
812805

813806
const prompt = lines.join("\n");
814807
await vscode.env.clipboard.writeText(prompt);
@@ -1412,22 +1405,22 @@ ${commonRules}`;
14121405
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; img-src data:;">
14131406
<title>TestPilot AI</title>
14141407
<style>
1415-
body { position: relative; }
1408+
body { position: relative; background: var(--bg); }
14161409
:root {
1417-
--bg: var(--vscode-sideBar-background, #1e1e1e);
1418-
--fg: var(--vscode-sideBar-foreground, #cccccc);
1419-
--input-bg: var(--vscode-input-background, #3c3c3c);
1420-
--input-border: var(--vscode-input-border, rgba(255,255,255,0.35));
1421-
--input-fg: var(--vscode-input-foreground, #cccccc);
1422-
--btn-bg: var(--vscode-button-background, #0e639c);
1423-
--btn-fg: var(--vscode-button-foreground, #ffffff);
1424-
--btn-hover: var(--vscode-button-hoverBackground, #1177bb);
1425-
--muted: var(--vscode-descriptionForeground, rgba(204,204,204,0.55));
1410+
--bg: #1e1e1e;
1411+
--fg: #cccccc;
1412+
--input-bg: #3c3c3c;
1413+
--input-border: rgba(255,255,255,0.35);
1414+
--input-fg: #cccccc;
1415+
--btn-bg: #0e639c;
1416+
--btn-fg: #ffffff;
1417+
--btn-hover: #1177bb;
1418+
--muted: rgba(204,204,204,0.55);
14261419
--success: #4ec9b0;
14271420
--error: #f44747;
14281421
--warn: #cca700;
1429-
--info: var(--vscode-descriptionForeground, #9d9d9d);
1430-
--editor-bg: var(--vscode-editor-background, #252526);
1422+
--info: #9d9d9d;
1423+
--editor-bg: #252526;
14311424
}
14321425
body.light-mode {
14331426
--bg: #e8e8e8;
@@ -1525,7 +1518,7 @@ ${commonRules}`;
15251518
padding:0 12px 6px; text-align:right;
15261519
}
15271520
* { margin: 0; padding: 0; box-sizing: border-box; }
1528-
body { font-family: var(--vscode-font-family); font-size: 13px; color: var(--fg); padding: 12px; }
1521+
body { font-family: "Segoe UI", system-ui, -apple-system, sans-serif; font-size: 13px; color: var(--fg); padding: 12px; }
15291522
h2 { font-size: 14px; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
15301523
.section { margin-bottom: 16px; }
15311524
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
@@ -1571,7 +1564,7 @@ ${commonRules}`;
15711564
padding: 8px;
15721565
max-height: 200px;
15731566
overflow-y: auto;
1574-
font-family: var(--vscode-editor-font-family);
1567+
font-family: "Cascadia Code", "Consolas", "Courier New", monospace;
15751568
font-size: 12px;
15761569
line-height: 1.5;
15771570
}

extension/templates/platforms/android.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- TestPilot-Template-Version: 3 -->
1+
<!-- TestPilot-Template-Version: 7 -->
22
# Android/Flutter 平台蓝本规则(platform = "android")
33

44
> 本文件定义 Android 原生应用和 Flutter 应用蓝本的完整规则。
@@ -226,6 +226,19 @@ wait 时间 = 代码中的异步延迟 + 2000ms(预留 Flutter 渲染 + Appium
226226

227227
**重要:** flow 场景仍需写 navigate 步骤(方便单独运行),引擎在 flow 模式下自动跳过非首场景的 navigate。
228228

229+
### 🚨 flow 非首场景写法(极其重要,必须遵守!)
230+
231+
**flow 模式下,第2个及之后的场景只写 navigate + 该场景自己的操作步骤,绝对禁止重复写登录步骤!**
232+
233+
引擎会跳过非首场景的 navigate,直接从第2步开始执行。如果第2步是 `fill 用户名`,但页面此时已经登录在功能页上 → 找不到输入框 → 超时失败 → 连续3步失败 → 整个场景被熔断跳过 → 后续场景全部同样失败。
234+
235+
| ❌ 错误写法(非首场景重复登录) | ✅ 正确写法(非首场景直接操作) |
236+
|---|---|
237+
| 场景2: navigate → wait → fill用户名 → fill密码 → click登录 → wait → 实际操作 | 场景2: navigate → 实际操作 → assert_text |
238+
| 场景3: navigate → wait → fill用户名 → fill密码 → click登录 → wait → 实际操作 | 场景3: navigate → 实际操作 → assert_text |
239+
240+
**核心原则:flow 模式下,只有第1个场景做完整的冷启动+登录流程,后续场景的 navigate 后面直接写该场景自己的操作。**
241+
229242
---
230243

231244
## 七、完整 JSON 模板

0 commit comments

Comments
 (0)