提交 345572f2 authored 作者: 陈泽健's avatar 陈泽健

fix: 语义化页面身份识别修复——create_meeting url_patterns 收紧 + meeting_detail fingerprint 修正

步骤15「点击取消会议」报"选择器不能为空"根因:create_meeting 万能 url_pattern
`meetingV3` 与详情页 `meetingV3.*Detail` 双命中(meetingDetail 含大写 Detail)
→ fingerprint 判存失败(详情页实测 0 个 .room、0 个 .topic_item input)
→ recognize_current_page 返回 None → scope 软更新失败 → 语义解析链中断。

修复(page_url_mapping.json):
- create_meeting url_patterns: ["meetingV3"] → ["meetingV3.*CreateMeeting", "meetingV3.*createMeeting"]
- meeting_detail fingerprint: .room:has-text('会议') → .detail_top(详情页 1 / 新建页 0,天然区分)

配套:
- semantic_resolver.py: scope 软更新重识别当前页面后与期望 scope 比对
- test_page_url_service.py: 新增 3 条回归测试(详情页识别/新建页不误判/列表页 None)

验证:语义化验证版用例(16步,剔除 finally 清理)真实执行连跑两次全绿
(exec_1ff18a20 / exec_2eb7c625,约 92s/次),5.44 会议列表零残留。
参考 _PRD_复杂用例通用执行机制_语义化用例执行.md
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 c6a1ca4e
......@@ -7,7 +7,8 @@
"name": "新建会议",
"url": "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FCreateMeeting",
"url_patterns": [
"meetingV3"
"meetingV3.*CreateMeeting",
"meetingV3.*createMeeting"
],
"fingerprint": [
{"selector": ".topic_item input", "min_count": 1}
......@@ -54,7 +55,7 @@
"MeetingDetail"
],
"fingerprint": [
{"selector": ".room:has-text('会议')", "min_count": 1}
{"selector": ".detail_top", "min_count": 1}
]
}
]
......
......@@ -26,6 +26,7 @@ from typing import Any, Dict, List, Optional
from app.config import settings
from app.services.element_mapping_service import get_element_mapping_service
from app.services.claude_service import get_claude_service
from app.services.page_url_service import get_page_url_service
from app.services.keyword_matcher import (
extract_keywords,
match_element_by_keywords,
......@@ -119,11 +120,28 @@ class SemanticResolver:
contains = ctx.render(contains)
contains = str(contains).strip() if contains else None
# 2. scope 校验:期望 scope 与实际不符 → 不覆盖(零行为变化)
# 2. scope 校验:期望 scope 与实际不符 → 软处理。
# 执行器 _current_scope 只在登录/URL直达后同步,普通 click 跳转(如
# 「查看详情」→会议详情页)后不更新 → 这里重新识别当前页面一次:
# 页面确实已跳转到期望 scope 则更新继续解析;否则才拦截(防跨页误配)。
expected_scope = target.get("scope")
if expected_scope and scope and expected_scope != scope:
fresh_scope = None
try:
rec = get_page_url_service().recognize_current_page(page)
fresh_scope = rec["id"] if rec else None
except Exception:
fresh_scope = None
if fresh_scope and fresh_scope == expected_scope:
logger.info(
f"[semantic] scope 软更新: 期望 {expected_scope}, 页面实际识别为 "
f"{fresh_scope}(原 {scope}),继续解析"
)
scope = fresh_scope
else:
logger.debug(
f"[semantic] scope 不符,跳过语义解析: 期望 {expected_scope}, 实际 {scope}"
f"[semantic] scope 不符,跳过语义解析: 期望 {expected_scope}, "
f"实际 {scope}, 重识别 {fresh_scope or '未识别'}"
)
return None
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:create_and_run_semantic_verify.py
模块描述:创建「新建会议-语义化验证版」用例(去掉 finally 步骤)并触发真实执行
阶段 6 真实执行验证用:正式示范用例的 finally 清理端点(POST /api/meeting/query)
是 PRD 占位,5.44 走 MQTT 无 HTTP 端点,直接跑必失败并把主流程翻 failed。
本脚本复制 build_steps() 但弹出 finally 步骤,先验证 16 步主流程 UI 链路。
用法:python scripts/create_and_run_semantic_verify.py
输出:case_id + execution_id(随后轮询 /api/executions/{id})
作者:czj
创建日期:2026-08-18
"""
import asyncio
import sys
import io
import os
import httpx
# 确保可从 scripts 目录 import 到 create_semantic_meeting_case
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from create_semantic_meeting_case import ( # noqa: E402
BASE_URL,
MEETING_MODULE_ID,
CASE_PARAMETERS,
DEFAULT_CONFIG,
build_steps,
)
# 验证版用例名(与正式版区分,幂等先删后建)
VERIFY_CASE_NAME = "新建会议-语义化验证版(临时-无finally)"
def build_verify_steps() -> list:
"""复制正式版步骤,弹出 finally 清理步骤(order 17)"""
steps = build_steps()
assert steps[-1].get("run_on") == "finally", "最后一步应为 finally 清理步骤"
verify_steps = steps[:-1]
print(f" 验证版步骤数: {len(verify_steps)}(已剔除 finally 清理步骤)")
return verify_steps
async def delete_existing(client: httpx.AsyncClient) -> None:
r = await client.get(f"{BASE_URL}/api/cases", params={"limit": 100})
if r.status_code == 200:
items = r.json().get("items", [])
for item in items:
if item.get("name") == VERIFY_CASE_NAME:
await client.delete(f"{BASE_URL}/api/cases/{item['id']}")
print(f" 已删除旧验证版用例: {item['id']}")
async def main() -> None:
async with httpx.AsyncClient(timeout=30) as client:
await delete_existing(client)
# 1. 创建验证版用例
payload = {
"module_id": MEETING_MODULE_ID,
"name": VERIFY_CASE_NAME,
"description": "阶段6真实执行验证版:与正式示范用例一致,剔除 finally 清理步骤,先验证16步主流程。",
"priority": "high",
"tags": ["语义化", "新建会议", "阶段6验证"],
"steps": build_verify_steps(),
"parameters": CASE_PARAMETERS,
"config": DEFAULT_CONFIG,
}
r = await client.post(f"{BASE_URL}/api/cases", json=payload)
if r.status_code != 201:
print(f"用例创建失败: HTTP {r.status_code}\n{r.text[:800]}")
return
case = r.json()
print(f"用例创建成功: {case['id']}")
# 2. 触发执行
r = await client.post(
f"{BASE_URL}/api/executions",
json={
"case_ids": [case["id"]],
"name": "语义化验证版-真实执行",
"trigger_type": "manual",
"trigger_by": "claude",
},
)
if r.status_code != 201:
print(f"执行创建失败: HTTP {r.status_code}\n{r.text[:800]}")
return
exec_data = r.json()
print(f"执行创建成功: {exec_data['id']}")
if __name__ == "__main__":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
asyncio.run(main())
......@@ -33,7 +33,8 @@ REGISTRY = {
"id": "create_meeting",
"name": "新建会议",
"url": "https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FCreateMeeting",
"url_patterns": ["meetingV3"],
# 收紧:不再用万能 meetingV3,避免把会议详情/列表页误识别为新建页
"url_patterns": ["meetingV3.*CreateMeeting", "meetingV3.*createMeeting"],
"fingerprint": [{"selector": ".topic_item input", "min_count": 1}],
"match_rules": [{"type": "step_name_contains", "value": "新建会议"}],
},
......@@ -51,6 +52,14 @@ REGISTRY = {
"url_patterns": ["#/home"],
"fingerprint": [{"selector": ".home_nav_left", "min_count": 1}],
},
{
"id": "meeting_detail",
"name": "会议详情",
"url": "",
"url_patterns": ["meetingV3.*Detail", "MeetingDetail"],
# 详情页独有标题元素(.room:has-text 在详情页不存在,实测 0 命中)
"fingerprint": [{"selector": ".detail_top", "min_count": 1}],
},
],
}
......@@ -111,6 +120,43 @@ def test_recognize_url_pattern_login():
assert rec["id"] == "login"
# ==================== 回归:详情页 vs 新建页识别(2026-08-18 修复) ====================
def test_recognize_meeting_detail_page():
"""会议详情页(meetingDetail 小写 + .detail_top 指纹)→ meeting_detail"""
svc = make_service()
page = FakePage(
url="https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FmeetingDetail%3FmId%25M2x",
matches={".detail_top": 1, ".topic_item input": 0},
)
rec = svc.recognize_current_page(page)
assert rec is not None
assert rec["id"] == "meeting_detail"
def test_recognize_create_page_not_misjudged_as_detail():
"""新建会议页(CreateMeeting 大写)→ create_meeting,绝不误判为 meeting_detail"""
svc = make_service()
page = FakePage(
url="https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FCreateMeeting",
matches={".detail_top": 0, ".topic_item input": 2},
)
rec = svc.recognize_current_page(page)
assert rec is not None
assert rec["id"] == "create_meeting"
def test_recognize_meeting_list_page_returns_none():
"""会议列表页(meetingAll)不属于任何 scope → None(修复前误判 create_meeting)"""
svc = make_service()
page = FakePage(
url="https://192.168.5.44/#/meetingV3?meetingV3=%2FmeetingV3%2F%23%2FmeetingAll",
matches={".detail_top": 0, ".topic_item input": 0},
)
assert svc.recognize_current_page(page) is None
# ==================== recognize_current_page:fingerprint 兜底 ====================
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论