Skip to content

Commit c1970b5

Browse files
committed
fix: WS broadcast safety + AI timeout/token limits + remove useless screenshots
- routes.py: wrap all send_test_done in try-except so WS failure doesn't crash HTTP response (fixes bug details missing in sidebar) - websocket.py: add default=str and try-except for json.dumps in broadcast - mobile_blueprint_runner.py: add timeout=45/max_tokens=500 for visual fallback, timeout=60/max_tokens=2000 for page pre-analysis - mobile_blueprint_runner.py: remove useless screenshots for successful assert_text steps - mobile_blueprint_runner.py: fingerprint-first caching + persistent disk cache (from prior session) - blueprint.py: add source_path field for persistent cache key
1 parent b4dd78a commit c1970b5

5 files changed

Lines changed: 256 additions & 71 deletions

File tree

extension/src/sidebarProvider.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
7777
await this._handleBrowseBlueprint();
7878
break;
7979
case "copyBlueprintPrompt":
80-
await this._handleCopyBlueprintPrompt(msg.platform || "web");
80+
await this._handleCopyBlueprintPrompt(msg.platform || "web", msg.projectDir || "");
8181
break;
8282
case "platformPrecheck":
8383
await this._handlePlatformPrecheck(msg);
@@ -494,7 +494,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
494494
}
495495
}
496496

497-
private async _handleCopyBlueprintPrompt(platform: string): Promise<void> {
497+
private async _handleCopyBlueprintPrompt(platform: string, projectDir: string = ""): Promise<void> {
498498
const commonRules = `══════ 测试设计黄金规则(必须严格遵守) ══════
499499
500500
【铁律0:每个项目必须有独立蓝本】
@@ -875,11 +875,16 @@ ${commonRules}`;
875875
${commonRules}`;
876876
}
877877

878+
if (projectDir) {
879+
const dirForPrompt = projectDir.replace(/\\/g, "/");
880+
prompt = `当前项目路径:${dirForPrompt}\n请直接读取该目录下的源代码生成蓝本,无需再次询问项目路径。\n\n` + prompt;
881+
}
878882
await vscode.env.clipboard.writeText(prompt);
879883
const platformNames: Record<string, string> = { web: "Web", miniprogram: "微信小程序", android: "Android", desktop: "Windows桌面", ios: "iOS" };
880884
const pName = platformNames[platform] || "Web";
881885
vscode.window.showInformationMessage(`✅ ${pName}蓝本提示词已复制!请粘贴到 Cursor / Windsurf,让编程AI读取源码生成蓝本。`);
882886
}
887+
}
883888

884889
private _guessProjectPathFromBlueprint(blueprintPath: string): string {
885890
const normalized = blueprintPath.replace(/\\/g, "/");
@@ -1707,8 +1712,10 @@ ${commonRules}`;
17071712
// 复制蓝本生成提示词(根据当前选中项目的平台生成不同提示词)
17081713
document.getElementById("btnCopyBlueprintPrompt").addEventListener("click", () => {
17091714
const sel = document.getElementById("projectSelect");
1710-
const platform = sel && sel.selectedOptions[0] ? (sel.selectedOptions[0].dataset.platform || "web") : "web";
1711-
vscode.postMessage({ command: "copyBlueprintPrompt", platform: platform });
1715+
const selOpt = sel && sel.selectedOptions[0];
1716+
const platform = selOpt ? (selOpt.dataset.platform || "web") : "web";
1717+
const projectDir = selOpt ? (selOpt.dataset.projectDir || "") : "";
1718+
vscode.postMessage({ command: "copyBlueprintPrompt", platform: platform, projectDir: projectDir });
17121719
});
17131720
17141721
// 探索测试
@@ -1856,6 +1863,7 @@ ${commonRules}`;
18561863
var opt = document.createElement("option");
18571864
opt.value = String(i);
18581865
opt.dataset.platform = proj.platform || "web";
1866+
opt.dataset.projectDir = proj.projectDir || "";
18591867
var pName = platformNames[proj.platform] || proj.platform || "Web";
18601868
// iOS项目在Windows下灰色不可选
18611869
if (proj.platform === "ios" && isWindows) {

src/api/routes.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,10 @@ async def run_test(req: RunTestRequest) -> TestReportResponse:
320320
auto_repair=req.auto_repair,
321321
project_path=req.project_path,
322322
)
323-
await ws_manager.send_test_done(report.pass_rate, len(report.bugs))
323+
try:
324+
await ws_manager.send_test_done(report.pass_rate, len(report.bugs))
325+
except Exception as ws_err:
326+
logger.warning("WS推送test_done失败: {}", str(ws_err)[:100])
324327

325328
# 构建响应
326329
repair_summary = None
@@ -420,10 +423,13 @@ async def _on_step_m(step: int, status: str, desc: str) -> None:
420423
await ws_manager.send_log(f"手机蓝本测试开始: {blueprint.app_name}")
421424
await ws_manager.send_test_started() # 通知插件显示控制按钮
422425
report = await runner.run(blueprint)
423-
await ws_manager.send_test_done(
424-
report.passed_steps / report.total_steps * 100 if report.total_steps > 0 else 0,
425-
len(report.bugs),
426-
)
426+
try:
427+
await ws_manager.send_test_done(
428+
report.passed_steps / report.total_steps * 100 if report.total_steps > 0 else 0,
429+
len(report.bugs),
430+
)
431+
except Exception as ws_err:
432+
logger.warning("WS推送test_done失败: {}", str(ws_err)[:100])
427433
from src.api.models import StepDetail, BugDetail
428434
stopped = test_controller.was_stopped
429435
if stopped:
@@ -569,7 +575,10 @@ async def _run_test():
569575
report_dict = response.model_dump()
570576
except AttributeError:
571577
report_dict = response.dict()
572-
await ws_manager.send_test_done(pass_rate, len(report.bugs), full_report=report_dict)
578+
try:
579+
await ws_manager.send_test_done(pass_rate, len(report.bugs), full_report=report_dict)
580+
except Exception as ws_err:
581+
logger.warning("WS推送test_done失败(不影响结果): {}", str(ws_err)[:100])
573582
result_holder["data"] = report_dict
574583
except Exception as e:
575584
logger.error("蓝本测试执行失败: {}", e)
@@ -738,11 +747,15 @@ async def _on_step(step: int, status: str, desc: str) -> None:
738747
],
739748
)
740749
# 通过WebSocket推送完整报告(含bugs+steps),插件侧边栏才能渲染Bug详情
750+
# WS推送失败不能影响HTTP返回(否则客户端拿不到bug详情)
741751
try:
742752
report_dict = response.model_dump()
743753
except AttributeError:
744754
report_dict = response.dict()
745-
await ws_manager.send_test_done(pass_rate, len(report.bugs), full_report=report_dict)
755+
try:
756+
await ws_manager.send_test_done(pass_rate, len(report.bugs), full_report=report_dict)
757+
except Exception as ws_err:
758+
logger.warning("WS推送test_done失败(不影响HTTP返回): {}", str(ws_err)[:100])
746759
return response
747760
except Exception as e:
748761
logger.error("手机蓝本测试执行失败: {}", e)
@@ -955,7 +968,10 @@ def _to_action(s: str) -> ActionType:
955968
)
956969

957970
pass_rate = passed / total * 100 if total > 0 else 0
958-
await ws_manager.send_test_done(pass_rate, len(bugs))
971+
try:
972+
await ws_manager.send_test_done(pass_rate, len(bugs))
973+
except Exception as ws_err:
974+
logger.warning("WS推送test_done失败: {}", str(ws_err)[:100])
959975

960976
from src.api.models import StepDetail, BugDetail
961977
return TestReportResponse(
@@ -1303,10 +1319,13 @@ async def explore_test(req: ExploreRequest) -> TestReportResponse:
13031319
description=req.description,
13041320
max_actions=req.max_actions,
13051321
)
1306-
await ws_manager.send_test_done(
1307-
report.passed_steps / report.total_steps * 100 if report.total_steps > 0 else 0,
1308-
len(report.bugs),
1309-
)
1322+
try:
1323+
await ws_manager.send_test_done(
1324+
report.passed_steps / report.total_steps * 100 if report.total_steps > 0 else 0,
1325+
len(report.bugs),
1326+
)
1327+
except Exception as ws_err:
1328+
logger.warning("WS推送test_done失败: {}", str(ws_err)[:100])
13101329

13111330
return TestReportResponse(
13121331
test_name=report.test_name,

src/api/websocket.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,15 @@ async def broadcast(self, message_type: str, data: dict[str, Any]) -> None:
8888
if not self._connections:
8989
return
9090

91-
payload = json.dumps(
92-
{"type": message_type, "data": data},
93-
ensure_ascii=False,
94-
)
91+
try:
92+
payload = json.dumps(
93+
{"type": message_type, "data": data},
94+
ensure_ascii=False,
95+
default=str, # 兜底:遇到不可序列化对象时转为字符串
96+
)
97+
except Exception as e:
98+
logger.warning("WS广播序列化失败: {} | type={}", str(e)[:80], message_type)
99+
return
95100

96101
dead: list[WebSocket] = []
97102
for ws in self._connections:

src/testing/blueprint.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ class BlueprintPage(BaseModel):
5151

5252
class Blueprint(BaseModel):
5353
"""测试蓝本(testpilot.json 的完整结构)。"""
54+
model_config = {"arbitrary_types_allowed": True}
5455

5556
app_name: str = Field(description="应用名称")
57+
source_path: Optional[Path] = Field(default=None, exclude=True, description="蓝本文件的磁盘路径(运行时填充,不序列化)")
5658
description: str = Field(default="", description="蓝本功能说明(50-200字,描述本蓝本覆盖的功能范围)")
5759
base_url: str = Field(default="", description="基础URL(如 http://localhost:3001)")
5860
version: str = Field(default="1.0", description="蓝本版本")
@@ -103,7 +105,9 @@ def parse_file(filepath: str | Path) -> Blueprint:
103105
except json.JSONDecodeError as e:
104106
raise ValueError(f"蓝本JSON格式错误: {e}")
105107

106-
return BlueprintParser.parse_dict(data)
108+
bp = BlueprintParser.parse_dict(data)
109+
bp.source_path = path
110+
return bp
107111

108112
@staticmethod
109113
def parse_dict(data: dict) -> Blueprint:

0 commit comments

Comments
 (0)