Skip to content

Commit 6322540

Browse files
committed
fix: 指纹双向Jaccard匹配(0.6阈值)+Bug报告智能归因提示
1 parent 1b92105 commit 6322540

2 files changed

Lines changed: 38 additions & 13 deletions

File tree

extension/src/sidebarProvider.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,27 +377,46 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
377377
if (bugs.length > 0) {
378378
bugs.forEach((bug, i) => {
379379
const cat = (bug.category as string) || "";
380+
const bugDesc = (bug.description as string) || "";
381+
const bugTitle = (bug.title as string) || "";
380382
const step = bug.step_number ? ` (步骤#${bug.step_number})` : "";
381-
lines.push(`${i + 1}. [${(bug.severity as string || "medium").toUpperCase()}] ${cat ? cat + " " : ""}${bug.title}${step}`);
382-
if (bug.description) {
383-
const desc = bug.description as string;
384-
const firstLine = desc.split("\n")[0].substring(0, 200);
383+
lines.push(`${i + 1}. [${(bug.severity as string || "medium").toUpperCase()}] ${cat ? cat + " " : ""}${bugTitle}${step}`);
384+
if (bugDesc) {
385+
const firstLine = bugDesc.split("\n")[0].substring(0, 200);
385386
lines.push(` ${firstLine}`);
386387
// 显示根因分析(关键!编程AI需要看到Playwright的真正报错原因)
387-
if (desc.includes("🔍 根因分析")) {
388-
const rootCause = desc.split(" 根因分析: ")[1];
388+
if (bugDesc.includes("🔍 根因分析")) {
389+
const rootCause = bugDesc.split("🔍 根因分析: ")[1];
389390
if (rootCause) {
390391
lines.push(` 🔍 根因分析: ${rootCause.split("\n")[0].substring(0, 200)}`);
391392
}
392393
}
393394
// 显示修复建议
394-
if (desc.includes("💡 修复建议")) {
395-
const suggestion = desc.split("💡 修复建议")[1];
395+
if (bugDesc.includes("💡 修复建议")) {
396+
const suggestion = bugDesc.split("💡 修复建议")[1];
396397
if (suggestion) {
397398
lines.push(` 💡 修复建议${suggestion.split("\n")[0].substring(0, 200)}`);
398399
}
399400
}
400401
}
402+
// ── 智能归因提示(帮助AI判断是蓝本问题还是APP问题)──
403+
// 1. SnackBar/Toast 误判:断言失败但预期文本是瞬态消息关键词
404+
const snackbarKeywords = ["成功", "已添加", "已删除", "已保存", "已更新", "已提交", "完成"];
405+
const isAssertFail = bugDesc.includes("未找到预期文本") || bugTitle.includes("文本断言失败");
406+
const looksLikeSnackbar = snackbarKeywords.some(k => bugDesc.includes(`'${k}`) || bugDesc.includes(`"${k}`));
407+
if (isAssertFail && looksLikeSnackbar && !cat.includes("[蓝本问题]")) {
408+
lines.push(` ⚠️ 归因提示:预期文本可能是 SnackBar/Toast 瞬态消息(2-3秒后消失),Appium检查时已消失。若APP确实显示了该提示,则这是【蓝本问题】,应将 assert_text 改为 screenshot。`);
409+
}
410+
// 2. accessibility_id 使用英文缩写而非实际 Semantics label
411+
const isElementNotFound = bugDesc.includes("元素找不到") || bugDesc.includes("no such element");
412+
const usesEnglishId = /accessibility_id:[a-z_]{3,}/.test(bugDesc) || /accessibility_id:[a-z_]{3,}/.test(bugTitle);
413+
if (isElementNotFound && usesEnglishId && !cat.includes("[蓝本问题]")) {
414+
lines.push(` ⚠️ 归因提示:accessibility_id 使用了英文缩写(如 btn_login),但 Flutter/Android Semantics 通常映射为中文 label。请查看源码 Semantics(label:'xxx') 的实际值,更新蓝本选择器。大概率是【蓝本问题】。`);
415+
}
416+
// 3. XPath[@hint='xxx'] 在 Flutter 中不稳定
417+
if (isElementNotFound && bugDesc.includes("@hint=") && !cat.includes("[蓝本问题]")) {
418+
lines.push(` ⚠️ 归因提示:XPath[@hint='xxx'] 在 Flutter 中不稳定(UiAutomator2 有时无法读取 EditText hint)。建议改用 Semantics label 的 accessibility_id 定位。大概率是【蓝本问题】。`);
419+
}
401420
});
402421
} else {
403422
lines.push((report.report_markdown as string || "").substring(0, 1000));
@@ -408,6 +427,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
408427
"- [应用Bug]:被测应用代码有问题,请修复应用代码",
409428
"- [蓝本问题]:testpilot.json蓝本写错了(选择器错、动作类型错等),请修正蓝本文件",
410429
"- 无标签:需要你自行判断是应用问题还是蓝本问题",
430+
"- ⚠️ 归因提示:TestPilot AI基于规则的自动推断,供参考,请结合实际源码判断",
411431
"",
412432
);
413433
// 闭环指令:包含蓝本路径,让编程AI能直接调用MCP工具重测

src/testing/mobile_blueprint_runner.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,22 @@ def _register_page(self, page_tag: str, fingerprint: set[str],
101101
"""将页面指纹和坐标注册到页面库(自动去重:相似页面合并到同一条目)。"""
102102
if not fingerprint:
103103
return
104-
# 查找是否已有相似页面(指纹重叠 >= 50%),避免同一页面重复注册
104+
# 查找是否已有相似页面(双向Jaccard >= 60%),避免同一页面重复注册
105+
# 双向匹配:要求「缓存覆盖当前」和「当前覆盖缓存」都满足阈值,防止小页面误匹配大页面
105106
merge_tag = None
106107
best_score = 0.0
107108
for tag, (fp, _) in self._page_library.items():
108109
if not fp:
109110
continue
110111
overlap = len(fingerprint & fp)
111-
score = overlap / len(fp)
112+
score_a = overlap / len(fp) # 缓存页面有多少词出现在新页面
113+
score_b = overlap / len(fingerprint) # 新页面有多少词出现在缓存
114+
score = min(score_a, score_b) # 取较小值,双向都要满足
112115
if score > best_score:
113116
best_score = score
114117
merge_tag = tag
115118

116-
if best_score >= 0.5 and merge_tag:
119+
if best_score >= 0.6 and merge_tag:
117120
# 合并到已有页面条目(同一页面不同场景的坐标都汇聚到一起)
118121
existing_fp, existing_coords = self._page_library[merge_tag]
119122
merged_fp = existing_fp | fingerprint
@@ -147,13 +150,15 @@ def _identify_page(self, xml_str: str) -> tuple[str | None, dict[str, tuple[int,
147150
if not fp:
148151
continue
149152
overlap = len(current_fp & fp)
150-
score = overlap / len(fp)
153+
score_a = overlap / len(fp) # 缓存页面有多少词出现在当前页面
154+
score_b = overlap / len(current_fp) # 当前页面有多少词出现在缓存
155+
score = min(score_a, score_b) # 双向最小,防止小缓存误匹配大页面
151156
if score > best_score:
152157
best_score = score
153158
best_tag = tag
154159
best_coords = coords
155160

156-
if best_score >= 0.5:
161+
if best_score >= 0.6:
157162
return best_tag, best_coords
158163
return None, {}
159164

0 commit comments

Comments
 (0)