Skip to content

Commit dcd416a

Browse files
committed
fix: v1.5.5 - 修复三大Bug:规则重复注入、stopEngine命令、Playwright浏览器环境变量注入
- 规则注入:平台模板版本 v10v16,修复重复注入死循环 - stopEngine 命令:改为调用 engineManager.dispose() - 引擎浏览器修复: * engineManager 启动子进程时注入 PLAYWRIGHT_BROWSERS_PATH * 检测已运行引擎是否浏览器可用,不可用时重启 * 健康检查新增 browser_available 字段 * 启动超时 30s120s(首次需装浏览器) * main.py 设置环境变量,首次运行自动 playwright install - 版本更新:1.5.41.5.5,版本检查接口已更新
1 parent ba50998 commit dcd416a

20 files changed

Lines changed: 905 additions & 196 deletions

build_log.txt

27.4 KB
Binary file not shown.

ci_win.log

Lines changed: 291 additions & 0 deletions
Large diffs are not rendered by default.

ci_win2.log

Lines changed: 323 additions & 0 deletions
Large diffs are not rendered by default.

extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "testpilot-ai",
33
"displayName": "TestPilot AI",
44
"description": "AI-powered automated testing robot — operates UI like a human, finds bugs, and auto-fixes them. Supports Web, Android, iOS, Mini Program & Desktop.",
5-
"version": "1.5.4",
5+
"version": "1.5.5",
66
"publisher": "wenzhouxinzao",
77
"engines": {
88
"vscode": "^1.85.0"

extension/src/engineManager.ts

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,18 @@ export class EngineManager {
111111
// 0. 版本检查(异步,不阻塞启动,但强制更新会提示)
112112
this._checkVersion().catch(() => {});
113113

114-
// 1. 已有引擎在跑,直接复用
114+
// 1. 已有引擎在跑
115115
if (await this.isEngineRunning()) {
116-
this._outputChannel.appendLine("[引擎] 检测到引擎已运行,直接连接");
117-
this._setStatus("ready");
118-
return;
116+
// 检查 Playwright 浏览器是否可用,如果不可用说明是旧版引擎(未设置PLAYWRIGHT_BROWSERS_PATH)
117+
const browserOk = await this._checkBrowserAvailable();
118+
if (browserOk) {
119+
this._outputChannel.appendLine("[引擎] 检测到引擎已运行,直接连接");
120+
this._setStatus("ready");
121+
return;
122+
}
123+
// 浏览器不可用 → 停掉旧引擎,用新参数重新启动
124+
this._outputChannel.appendLine("[引擎] 检测到引擎已运行但浏览器不可用,重新启动以修复...");
125+
await this._killExistingEngine();
119126
}
120127

121128
// 2. globalStorage 目录必须存在
@@ -135,6 +142,54 @@ export class EngineManager {
135142
this._setStatus("ready");
136143
}
137144

145+
/** 检查 Playwright 浏览器是否存在于系统缓存 */
146+
private _checkBrowserAvailable(): Promise<boolean> {
147+
return new Promise((resolve) => {
148+
const url = getEngineUrl() + "/api/v1/health";
149+
const mod = url.startsWith("https") ? https : http;
150+
const req = mod.get(url, { timeout: 2000 }, (res) => {
151+
let body = "";
152+
res.on("data", (chunk: Buffer) => { body += chunk.toString(); });
153+
res.on("end", () => {
154+
try {
155+
const data = JSON.parse(body);
156+
resolve(data.browser_available !== false);
157+
} catch {
158+
resolve(true); // 旧版引擎无此字段,不强制重启
159+
}
160+
});
161+
res.resume();
162+
});
163+
req.on("error", () => resolve(true));
164+
req.on("timeout", () => { req.destroy(); resolve(true); });
165+
});
166+
}
167+
168+
/** 杀掉端口 8900 上正在运行的引擎进程 */
169+
private _killExistingEngine(): Promise<void> {
170+
return new Promise((resolve) => {
171+
if (this._engineProc) {
172+
this._engineProc.kill();
173+
this._engineProc = null;
174+
setTimeout(resolve, 1000);
175+
return;
176+
}
177+
// 用 HTTP DELETE 或直接让它自然停止(引擎暂无 shutdown 接口)
178+
// 通过端口找进程并杀掉(仅 Windows)
179+
if (process.platform === "win32") {
180+
child_process.exec(
181+
`for /f "tokens=5" %a in ('netstat -ano ^| findstr :8900') do taskkill /F /PID %a`,
182+
() => setTimeout(resolve, 1500),
183+
);
184+
} else {
185+
child_process.exec(
186+
`fuser -k 8900/tcp`,
187+
() => setTimeout(resolve, 1500),
188+
);
189+
}
190+
});
191+
}
192+
138193
/** 从 GitHub Release 下载二进制(带进度条通知) */
139194
private async _downloadBinary(): Promise<void> {
140195
const fileName = getPlatformBinaryName();
@@ -218,10 +273,21 @@ export class EngineManager {
218273
return new Promise((resolve, reject) => {
219274
this._outputChannel.appendLine(`[引擎] 启动: ${this.binaryPath}`);
220275

276+
// 注入 PLAYWRIGHT_BROWSERS_PATH,确保 PyInstaller 打包的引擎能找到已安装的浏览器
277+
// 避免 Playwright 在 _MEIxxxxxx 临时目录中搜索浏览器
278+
const browsersPath = process.platform === "win32"
279+
? path.join(process.env["LOCALAPPDATA"] || os.homedir(), "ms-playwright")
280+
: path.join(os.homedir(), ".cache", "ms-playwright");
281+
this._outputChannel.appendLine(`[引擎] PLAYWRIGHT_BROWSERS_PATH=${browsersPath}`);
282+
221283
this._engineProc = child_process.spawn(this.binaryPath, [], {
222284
cwd: os.homedir(), // 工作目录设为用户主目录,让 data/ logs/ 写到那里
223285
detached: false,
224286
stdio: ["ignore", "pipe", "pipe"],
287+
env: {
288+
...process.env,
289+
PLAYWRIGHT_BROWSERS_PATH: browsersPath,
290+
},
225291
});
226292

227293
const onData = (chunk: Buffer) => {
@@ -248,8 +314,8 @@ export class EngineManager {
248314
this._setStatus("offline");
249315
});
250316

251-
// 超时保护:30 秒内未就绪视为失败
252-
setTimeout(() => reject(new Error("引擎启动超时(30s),请检查 Output 面板日志")), 30000);
317+
// 超时保护:120 秒内未就绪视为失败(首次启动可能需要下载 Playwright 浏览器)
318+
setTimeout(() => reject(new Error("引擎启动超时(120s),请检查 Output 面板日志")), 120000);
253319
});
254320
}
255321

extension/src/extension.ts

Lines changed: 20 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -331,105 +331,41 @@ export function activate(context: vscode.ExtensionContext): void {
331331
}),
332332
);
333333

334-
// ── 一键启动引擎(v10.0)──
334+
// ── 一键启动引擎(v15.0:自动下载+启动二进制,无需选择目录)──
335335
context.subscriptions.push(
336336
vscode.commands.registerCommand("testpilot-ai.launchEngine", async () => {
337-
// 查找项目根目录(优先用工作区,否则用插件设置)
338-
const folders = vscode.workspace.workspaceFolders;
339-
let projectRoot = "";
340-
341-
// 尝试在工作区中找到 TestPilotAI 项目(有 cli.py 的目录)
342-
if (folders) {
343-
for (const f of folders) {
344-
const cliPath = vscode.Uri.joinPath(f.uri, "cli.py");
345-
// 使用 fs.stat 检测文件是否存在
346-
try {
347-
const fs = require("fs");
348-
if (fs.existsSync(cliPath.fsPath)) {
349-
projectRoot = f.uri.fsPath;
350-
break;
351-
}
352-
} catch {
353-
// 忽略
354-
}
355-
}
356-
}
357-
358-
if (!projectRoot) {
359-
// 兜底:从配置中读取
360-
const config = vscode.workspace.getConfiguration("testpilotAI");
361-
projectRoot = config.get<string>("projectRoot", "");
362-
}
363-
364-
if (!projectRoot) {
365-
// 弹出目录选择框让用户手动指定
366-
const selected = await vscode.window.showOpenDialog({
367-
canSelectFiles: false,
368-
canSelectFolders: true,
369-
canSelectMany: false,
370-
title: "选择 TestPilot AI 项目根目录(包含 cli.py 的文件夹)",
371-
});
372-
if (!selected || selected.length === 0) {
373-
vscode.window.showErrorMessage(
374-
"⚠️ 未选择目录,引擎启动取消。也可在 设置 → testpilotAI.projectRoot 中填入项目路径。",
375-
);
376-
return;
377-
}
378-
projectRoot = selected[0].fsPath;
379-
// 验证选中的目录包含 cli.py
380-
const fs = require("fs");
381-
if (!fs.existsSync(require("path").join(projectRoot, "cli.py"))) {
382-
vscode.window.showErrorMessage(
383-
`❌ 所选目录不含 cli.py,请选择正确的 TestPilot AI 项目根目录。\n当前选择:${projectRoot}`,
384-
);
385-
return;
386-
}
387-
// 保存到设置,下次直接用
388-
await vscode.workspace.getConfiguration("testpilotAI").update(
389-
"projectRoot",
390-
projectRoot,
391-
vscode.ConfigurationTarget.Global,
392-
);
393-
outputChannel.appendLine(`[TestPilot AI] 已保存项目路径: ${projectRoot}`);
337+
// 先检查引擎是否已在运行
338+
if (await engineManager.isEngineRunning()) {
339+
vscode.window.showInformationMessage("✅ TestPilot AI 引擎已在运行中");
340+
client.connectWs();
341+
return;
394342
}
395343

396-
// 若旧终端存在,先关闭(避免复用失效终端)
397-
const existingTerminal = vscode.window.terminals.find(
398-
(t) => t.name === "TestPilot Engine",
399-
);
400-
if (existingTerminal) {
401-
existingTerminal.dispose();
402-
outputChannel.appendLine("[TestPilot AI] 关闭旧引擎终端,准备重启");
344+
// 调用 ensureRunning:自动从服务器下载引擎二进制并启动
345+
// 首次安装时会显示下载进度条;已下载过则直接启动
346+
try {
347+
await engineManager.ensureRunning();
348+
outputChannel.appendLine("[TestPilot AI] 引擎就绪");
349+
client.connectWs();
350+
vscode.window.showInformationMessage("✅ TestPilot AI 引擎启动成功");
351+
} catch (err: unknown) {
352+
const message = err instanceof Error ? err.message : String(err);
353+
vscode.window.showErrorMessage(`TestPilot AI 引擎启动失败: ${message}\n请检查网络连接后重试。`);
403354
}
404-
405-
const terminal = vscode.window.createTerminal({
406-
name: "TestPilot Engine",
407-
cwd: projectRoot,
408-
});
409-
terminal.show();
410-
terminal.sendText("poetry run python cli.py serve --force");
411-
outputChannel.appendLine(`[TestPilot AI] 引擎启动中... 目录: ${projectRoot}`);
412-
vscode.window.showInformationMessage("🚀 TestPilot AI 引擎正在启动...");
413355
}),
414356
);
415357

416358
// ── 一键关闭引擎(v10.1)──
417359
context.subscriptions.push(
418360
vscode.commands.registerCommand("testpilot-ai.stopEngine", async () => {
419-
const engineTerminal = vscode.window.terminals.find(
420-
(t) => t.name === "TestPilot Engine",
421-
);
422-
if (engineTerminal) {
423-
engineTerminal.dispose();
424-
outputChannel.appendLine("[TestPilot AI] 引擎终端已关闭");
425-
} else {
426-
outputChannel.appendLine("[TestPilot AI] 未找到 TestPilot Engine 终端");
427-
}
361+
// 通过 engineManager 杀掉引擎子进程
362+
engineManager.dispose();
363+
outputChannel.appendLine("[TestPilot AI] 引擎进程已停止");
428364
// 断开 WebSocket 连接
429365
client.disconnectWs();
430366
// 通知 Sidebar 更新状态
431367
sidebarProvider.postMessage({ command: "engineStatus", data: { connected: false } });
432-
vscode.window.showInformationMessage("⏹ TestPilot AI 引擎已断开");
368+
vscode.window.showInformationMessage("⏹ TestPilot AI 引擎已停止");
433369
}),
434370
);
435371

extension/src/rulesInjector.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,10 @@ export function injectRules(
416416
// 强制注入所有IDE规则(手动触发时)
417417
filesToInject = ["AGENTS.md", ...Object.values(IDE_RULES_MAP)];
418418
} else {
419-
// 智能注入:AGENTS.md + CLAUDE.md + 当前IDE + 已安装的AI扩展
419+
// 智能注入:AGENTS.md + 当前IDE + 已安装的AI扩展
420+
// (CLAUDE.md 只在检测到 Claude Code 扩展时才注入,不强制创建)
420421
const detectedIDEs = detectAllIDEs();
421-
filesToInject = ["AGENTS.md", "CLAUDE.md"];
422+
filesToInject = ["AGENTS.md"];
422423

423424
for (const ide of detectedIDEs) {
424425
const ruleFile = IDE_RULES_MAP[ide];

extension/src/sidebarProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3247,6 +3247,9 @@ ${commonRules}`;
32473247
if (!wasConnected) {
32483248
wasConnected = true;
32493249
addLog("引擎连接成功 | v" + data.version, "success");
3250+
if (data.browser_available === false) {
3251+
addLog("⚠️ 浏览器未就绪,首次运行测试前引擎将自动安装 Chromium", "warning");
3252+
}
32503253
vscode.postMessage({ command: "scanBlueprints" });
32513254
}
32523255
} else {

extension/templates/platforms/android.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- TestPilot-Template-Version: 10 -->
1+
<!-- TestPilot-Template-Version: 16 -->
22
# Platform Blueprint Rules (platform = "android")
33

44
> This file defines all rules for Android/Flutter application blueprints.

extension/templates/platforms/desktop.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- TestPilot-Template-Version: 10 -->
1+
<!-- TestPilot-Template-Version: 16 -->
22
# Platform Blueprint Rules (platform = "desktop")
33

44
> This file defines all rules for Windows desktop application blueprints.

0 commit comments

Comments
 (0)