screenforge 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. cli/__init__.py +0 -0
  2. cli/_version.py +1 -0
  3. cli/dispatch.py +266 -0
  4. cli/doctor.py +487 -0
  5. cli/modes/__init__.py +0 -0
  6. cli/modes/action.py +262 -0
  7. cli/modes/default.py +248 -0
  8. cli/modes/demo.py +162 -0
  9. cli/modes/dry_run.py +237 -0
  10. cli/modes/init.py +133 -0
  11. cli/modes/plan.py +148 -0
  12. cli/modes/workflow.py +354 -0
  13. cli/parser.py +305 -0
  14. cli/reporter.py +207 -0
  15. cli/session.py +146 -0
  16. cli/shared.py +427 -0
  17. cli/shorthand.py +90 -0
  18. cli/tool_protocol_handlers.py +446 -0
  19. common/__init__.py +0 -0
  20. common/adapters/__init__.py +21 -0
  21. common/adapters/android_adapter.py +273 -0
  22. common/adapters/base_adapter.py +24 -0
  23. common/adapters/ios_adapter.py +278 -0
  24. common/adapters/web_adapter.py +271 -0
  25. common/ai.py +277 -0
  26. common/ai_autonomous.py +273 -0
  27. common/ai_heal.py +222 -0
  28. common/cache/__init__.py +15 -0
  29. common/cache/cache_hash.py +57 -0
  30. common/cache/cache_manager.py +300 -0
  31. common/cache/cache_stats.py +133 -0
  32. common/cache/cache_storage.py +79 -0
  33. common/cache/embedding_loader.py +150 -0
  34. common/capabilities.py +121 -0
  35. common/case_memory.py +327 -0
  36. common/error_codes.py +61 -0
  37. common/exceptions.py +18 -0
  38. common/executor.py +1504 -0
  39. common/failure_diagnosis.py +138 -0
  40. common/history_manager.py +75 -0
  41. common/logs.py +168 -0
  42. common/mcp_server.py +467 -0
  43. common/preflight.py +496 -0
  44. common/progress.py +37 -0
  45. common/run_reporter.py +415 -0
  46. common/run_resume.py +149 -0
  47. common/runtime_modes.py +35 -0
  48. common/tool_protocol.py +196 -0
  49. common/visual_fallback.py +71 -0
  50. common/workflow_schema.py +150 -0
  51. config/__init__.py +0 -0
  52. config/config.py +167 -0
  53. config/env_loader.py +76 -0
  54. screenforge-0.4.0.dist-info/METADATA +43 -0
  55. screenforge-0.4.0.dist-info/RECORD +64 -0
  56. screenforge-0.4.0.dist-info/WHEEL +5 -0
  57. screenforge-0.4.0.dist-info/entry_points.txt +2 -0
  58. screenforge-0.4.0.dist-info/licenses/LICENSE +21 -0
  59. screenforge-0.4.0.dist-info/top_level.txt +4 -0
  60. utils/__init__.py +0 -0
  61. utils/screenshot_annotator.py +60 -0
  62. utils/utils_ios.py +195 -0
  63. utils/utils_web.py +304 -0
  64. utils/utils_xml.py +218 -0
@@ -0,0 +1,273 @@
1
+ import json
2
+
3
+ import config.config as config
4
+ from common.ai import AIBrain
5
+ from common.logs import log
6
+ from common.progress import ai_status
7
+
8
+
9
+ class AutonomousBrain(AIBrain):
10
+ def get_execution_plan(
11
+ self,
12
+ goal: str,
13
+ context: str,
14
+ ui_json: str,
15
+ history: list,
16
+ platform: str = "android",
17
+ screenshot_base64: str = None,
18
+ ) -> dict:
19
+ try:
20
+ json.loads(ui_json)
21
+ except json.JSONDecodeError:
22
+ ui_json = '{"ui_elements": []}'
23
+
24
+ history_str = "无"
25
+ if history:
26
+ history_str = "\n".join(
27
+ [
28
+ f"第{i + 1}步: {step['action_description']}"
29
+ for i, step in enumerate(history)
30
+ ]
31
+ )
32
+
33
+ system_prompt = f"""
34
+ 你是一个{platform} 自动化测试规划专家。
35
+ 你需要根据用户目标、上下文、历史步骤和当前页面 UI 树,输出一个执行前计划。
36
+
37
+ 你必须输出纯 JSON 对象,不要包含 markdown,结构如下:
38
+ {{
39
+ "current_state_summary": "当前页面状态摘要",
40
+ "planned_steps": ["步骤1", "步骤2", "步骤3"],
41
+ "suggested_assertion": "最终建议断言",
42
+ "risks": ["风险1", "风险2"]
43
+ }}
44
+ """
45
+
46
+ user_prompt = f"""
47
+ 【宏观测试目标】: {goal}
48
+ 【参考上下文(PRD/用例)】: {context if context else '无'}
49
+ 【已执行的历史步骤】:
50
+ {history_str}
51
+ 【当前屏幕 UI 树】:
52
+ {ui_json}
53
+ """
54
+
55
+ user_message_content = [{"type": "text", "text": user_prompt}]
56
+ if screenshot_base64:
57
+ user_message_content.append(
58
+ {
59
+ "type": "image_url",
60
+ "image_url": {"url": f"data:image/png;base64,{screenshot_base64}"},
61
+ }
62
+ )
63
+
64
+ if screenshot_base64:
65
+ active_client = getattr(self, "vision_client", None) or getattr(
66
+ self, "text_client", None
67
+ )
68
+ active_model = config.VISION_MODEL_NAME
69
+ else:
70
+ active_client = getattr(self, "text_client", None) or getattr(
71
+ self, "client", None
72
+ )
73
+ active_model = config.MODEL_NAME
74
+
75
+ if not active_client:
76
+ log.error("[Error] No model client available for plan generation")
77
+ return {
78
+ "current_state_summary": "模型客户端未初始化",
79
+ "planned_steps": [],
80
+ "suggested_assertion": "",
81
+ "risks": ["模型客户端未初始化"],
82
+ }
83
+
84
+ result_text = ""
85
+ try:
86
+ with ai_status("Planning execution steps..."):
87
+ response = active_client.chat.completions.create(
88
+ model=active_model,
89
+ messages=[
90
+ {"role": "system", "content": system_prompt},
91
+ {"role": "user", "content": user_message_content},
92
+ ],
93
+ temperature=0.1,
94
+ )
95
+ result_text = response.choices[0].message.content.strip()
96
+
97
+ if "```json" in result_text:
98
+ result_text = result_text.split("```json")[1].split("```")[0].strip()
99
+ elif "```" in result_text:
100
+ result_text = result_text.replace("```", "").strip()
101
+
102
+ parsed_json = json.loads(result_text)
103
+ parsed_json.setdefault("current_state_summary", "")
104
+ parsed_json.setdefault("planned_steps", [])
105
+ parsed_json.setdefault("suggested_assertion", "")
106
+ parsed_json.setdefault("risks", [])
107
+ return parsed_json
108
+ except Exception as e:
109
+ log.error(f"[Error] Plan model request or parse failed: {e}\nRaw response: {result_text}")
110
+ return {
111
+ "current_state_summary": "计划生成失败",
112
+ "planned_steps": [],
113
+ "suggested_assertion": "",
114
+ "risks": ["计划生成失败"],
115
+ }
116
+
117
+ def get_next_autonomous_action(
118
+ self,
119
+ goal: str,
120
+ context: str,
121
+ ui_json: str,
122
+ history: list,
123
+ platform: str = "android",
124
+ last_error: str = "",
125
+ screenshot_base64: str = None,
126
+ ) -> dict:
127
+ """
128
+ 向大模型发送宏观目标、当前状态、前置报错及视觉截图,自主决策下一步动作
129
+ """
130
+ try:
131
+ json.loads(ui_json)
132
+ except json.JSONDecodeError:
133
+ ui_json = '{"ui_elements": []}'
134
+
135
+ history_str = "无"
136
+ if history:
137
+ history_str = "\n".join(
138
+ [
139
+ f"第{i + 1}步: {step['action_description']}"
140
+ for i, step in enumerate(history)
141
+ ]
142
+ )
143
+
144
+ error_prompt = ""
145
+ if last_error:
146
+ error_prompt = f"\n⚠️ 【特别注意 - 上一步执行失败】:\n{last_error}\n请在本次思考中分析失败原因,尝试换一种动作或定位器。\n"
147
+
148
+ vision_prompt = ""
149
+ if screenshot_base64:
150
+ vision_prompt = "\n👁️ 【视觉辅助】: 你同时收到了一张真实屏幕截图。请结合视觉画面与 UI 树,更精准地理解页面布局、按钮状态。如果 XML 树混乱,请以视觉画面为准。"
151
+
152
+ system_prompt = f"""
153
+ 你是一个完全自主的{platform} {'多模态视觉' if screenshot_base64 else '纯文本'} 高级自动化测试 Agent。
154
+ 你需要根据用户的【宏观测试目标】、【参考上下文】、【已执行的历史步骤】以及【当前屏幕 UI 树】{'和【屏幕截图】' if screenshot_base64 else ''},自主决定下一步需要执行什么动作。
155
+ {vision_prompt}
156
+
157
+ 允许的 action 类型:
158
+ - "goto": 导航到指定 URL (仅 Web 端)。必须在 extra_value 填入目标 URL (如 "https://www.google.com")。此时 locator_type 填 "global",locator_value 填 "global"。
159
+ - "click": 点击元素
160
+ - "long_click": 长按元素
161
+ - "hover": 悬停元素 (针对 Web 端,触发下拉菜单等交互)
162
+ - "input": 在输入框中输入内容 (需通过 extra_value 参数提供内容)
163
+ - "swipe": 滑动屏幕寻找不在视口内的元素。必须在 extra_value 填入 "up", "down", "left" 或 "right"。此时 locator_type 填 "global"。
164
+ - "press": 模拟键盘或物理系统按键。必须在 extra_value 填入按键名 (如 "Enter", "Back")。此时 locator_type 填 "global"。
165
+ - "scroll_into_view": (仅 Web) 将元素滚动到视口内 (元素级,优于盲目 swipe)。
166
+ - "select": (仅 Web) 原生 <select> 下拉框选择。extra_value 填选项文本或 value。
167
+ - "upload": (仅 Web) 文件 <input> 上传。extra_value 填文件路径。
168
+ - "double_click" / "right_click": (仅 Web) 双击 / 右键点击元素。
169
+ - "drag": (仅 Web) 拖拽。locator 定位源,extra_value 填目标 (css 或文本)。
170
+ - "wait_for": 显式等待元素出现或消失 (替代死等)。extra_value 填 "visible"(默认) 或 "hidden"。
171
+ - "assert_exist": 校验某个元素是否在页面上出现
172
+ - "assert_not_exist": 校验某个元素已消失/不存在 (如加载动画消失、弹窗关闭)
173
+ - "assert_text_equals": 校验某个元素的文本是否与期望值【完全相等】
174
+ - "assert_text_contains": 校验某个元素的文本【包含】指定子串 (动态文本首选)。extra_value 填子串。
175
+ - "assert_value": 校验输入框/表单字段的当前值。extra_value 填期望值。
176
+ - "assert_url": (仅 Web) 校验当前页面 URL 包含子串。locator_type/value 填 "global",extra_value 填 URL 子串。
177
+
178
+ 定位器 (locator_type) 优先级: css > resourceId > text > description
179
+ 🚨 警告: 若 resourceId 是动态随机的,必须降级使用 text 或 description!
180
+ 💡 多个元素 text 相同时 (如每行一个 "Delete"),选带 `scope` 字段的那个 —— scope 是
181
+ 它所在行/区块的标识文本 (如 "Bob Jones"),引擎会据此生成稳定的作用域定位器,
182
+ 精确命中正确的那一行 (而非永远点第一行)。
183
+
184
+ 【思考与状态决策】
185
+ 你需要先思考 (thought),然后评估当前状态 (status):
186
+ - "running": 目标尚未完成,需要执行下一步动作。
187
+ - "success": 目标已达到最终校验阶段。⚠️ 强烈要求:宣告成功时,你必须在 result 中提供一个断言动作 (`assert_exist` / `assert_text_contains` / `assert_text_equals` / `assert_value` / `assert_url` 之一),底层引擎会执行该断言并固化到测试脚本中。优先选择最能证明"目标已达成"的断言 (如登录成功后断言 URL 含 "/dashboard",或断言欢迎文本包含用户名)。
188
+ - "failed": 遇到了无法克服的阻塞性错误,无法继续。
189
+
190
+ 【强制输出格式】
191
+ 必须输出纯 JSON 对象,不要包含任何 markdown 代码块标记,结构严格如下:
192
+ {{
193
+ "thought": "我现在的思考过程,我看到了什么,我接下来要干什么",
194
+ "status": "running" | "success" | "failed",
195
+ "result": {{"action": "...", "locator_type": "...", "locator_value": "...", "extra_value": "..."}}
196
+ }}
197
+ """
198
+
199
+ user_prompt = f"""
200
+ 【宏观测试目标】: {goal}
201
+ 【参考上下文(PRD/用例)】: {context if context else '无'}
202
+ 【已执行的历史步骤】:
203
+ {history_str}
204
+ {error_prompt}
205
+ 【当前屏幕 UI 树】:
206
+ {ui_json}
207
+ """
208
+
209
+ user_message_content = [{"type": "text", "text": user_prompt}]
210
+ if screenshot_base64:
211
+ user_message_content.append(
212
+ {
213
+ "type": "image_url",
214
+ "image_url": {"url": f"data:image/png;base64,{screenshot_base64}"},
215
+ }
216
+ )
217
+
218
+ if screenshot_base64:
219
+ active_client = getattr(self, "vision_client", None) or getattr(
220
+ self, "text_client", None
221
+ ) # 兼容配置
222
+ active_model = config.VISION_MODEL_NAME
223
+ else:
224
+ active_client = getattr(self, "text_client", None) or getattr(
225
+ self, "client", None
226
+ )
227
+ active_model = config.MODEL_NAME
228
+
229
+ if not active_client:
230
+ log.error("[Error] No model client available for autonomous decision")
231
+ return {
232
+ "status": "failed",
233
+ "thought": "模型客户端未初始化",
234
+ "result": {},
235
+ }
236
+
237
+ log.info(f"[Autonomous] Reasoning with model [{active_model}]...")
238
+
239
+ result_text = ""
240
+ try:
241
+ with ai_status("Reasoning about next action..."):
242
+ response = active_client.chat.completions.create(
243
+ model=active_model,
244
+ messages=[
245
+ {"role": "system", "content": system_prompt},
246
+ {"role": "user", "content": user_message_content},
247
+ ],
248
+ temperature=0.1,
249
+ )
250
+
251
+ result_text = response.choices[0].message.content.strip()
252
+
253
+ if "```json" in result_text:
254
+ result_text = result_text.split("```json")[1].split("```")[0].strip()
255
+ elif "```" in result_text:
256
+ result_text = result_text.replace("```", "").strip()
257
+
258
+ parsed_json = json.loads(result_text)
259
+
260
+ thought = parsed_json.get("thought", "无")
261
+ status = parsed_json.get("status", "failed")
262
+ log.info(f"[Agent] Thought: {thought}")
263
+ log.info(f"[Agent] Status: {status}")
264
+
265
+ return parsed_json
266
+
267
+ except Exception as e:
268
+ log.error(f"[Error] Autonomous model request or parse failed: {e}\nRaw response: {result_text}")
269
+ return {
270
+ "status": "failed",
271
+ "thought": "模型返回格式异常或请求失败",
272
+ "result": {},
273
+ }
common/ai_heal.py ADDED
@@ -0,0 +1,222 @@
1
+ """AI Self-Healing Engine — structured JSON output with confidence scoring."""
2
+
3
+ import ast
4
+ import json
5
+ import re
6
+ import time
7
+ from dataclasses import dataclass
8
+
9
+ from common.logs import log
10
+
11
+
12
+ @dataclass
13
+ class HealResult:
14
+ """Self-heal attempt result."""
15
+
16
+ confidence: float # 0.0 - 1.0
17
+ fix_description: str
18
+ fixed_code: str
19
+
20
+ @property
21
+ def is_valid_syntax(self) -> bool:
22
+ try:
23
+ ast.parse(self.fixed_code)
24
+ return True
25
+ except SyntaxError:
26
+ return False
27
+
28
+
29
+ def _parse_heal_response(raw: str) -> HealResult:
30
+ """Parse LLM response into HealResult. Tries multiple extraction strategies."""
31
+
32
+ # Strategy 1: direct JSON parse
33
+ try:
34
+ data = json.loads(raw)
35
+ return HealResult(
36
+ confidence=float(data.get("confidence", 0.0)),
37
+ fix_description=str(data.get("fix_description", "")),
38
+ fixed_code=str(data.get("fixed_code", "")),
39
+ )
40
+ except (json.JSONDecodeError, TypeError, ValueError):
41
+ pass
42
+
43
+ # Strategy 2: extract JSON from markdown code fence or surrounding text
44
+ # First try ```json ... ``` blocks
45
+ json_fence = re.search(r"```(?:json)?\s*\n(\{[\s\S]*?\})\s*\n```", raw)
46
+ if json_fence:
47
+ try:
48
+ data = json.loads(json_fence.group(1))
49
+ if "fixed_code" in data:
50
+ return HealResult(
51
+ confidence=float(data.get("confidence", 0.0)),
52
+ fix_description=str(data.get("fix_description", "")),
53
+ fixed_code=str(data.get("fixed_code", "")),
54
+ )
55
+ except (json.JSONDecodeError, TypeError, ValueError):
56
+ pass
57
+
58
+ # Then try balanced-brace extraction from raw text
59
+ for m in re.finditer(r"\{", raw):
60
+ start = m.start()
61
+ depth = 0
62
+ in_str = False
63
+ escape = False
64
+ for i in range(start, len(raw)):
65
+ c = raw[i]
66
+ if escape:
67
+ escape = False
68
+ continue
69
+ if c == "\\":
70
+ escape = True
71
+ continue
72
+ if c == '"' and not escape:
73
+ in_str = not in_str
74
+ continue
75
+ if in_str:
76
+ continue
77
+ if c == "{":
78
+ depth += 1
79
+ elif c == "}":
80
+ depth -= 1
81
+ if depth == 0:
82
+ candidate = raw[start : i + 1]
83
+ try:
84
+ data = json.loads(candidate)
85
+ if "fixed_code" in data:
86
+ return HealResult(
87
+ confidence=float(data.get("confidence", 0.0)),
88
+ fix_description=str(data.get("fix_description", "")),
89
+ fixed_code=str(data.get("fixed_code", "")),
90
+ )
91
+ except (json.JSONDecodeError, TypeError, ValueError):
92
+ pass
93
+ break
94
+
95
+ # Strategy 3: fallback — extract python code block (legacy format), low confidence
96
+ code_match = re.search(r"```python\n(.*?)\n```", raw, re.DOTALL)
97
+ if code_match:
98
+ return HealResult(
99
+ confidence=0.3,
100
+ fix_description="(fallback: extracted from markdown code block)",
101
+ fixed_code=code_match.group(1).strip(),
102
+ )
103
+
104
+ # Strategy 4: last resort — strip backticks
105
+ stripped = raw.replace("```python", "").replace("```", "").strip()
106
+ if "def test_" in stripped:
107
+ return HealResult(
108
+ confidence=0.2,
109
+ fix_description="(fallback: raw text extraction)",
110
+ fixed_code=stripped,
111
+ )
112
+
113
+ return HealResult(confidence=0.0, fix_description="failed to parse response", fixed_code="")
114
+
115
+
116
+ class HealerBrain:
117
+ """AI Self-Healing Engine."""
118
+
119
+ def __init__(self):
120
+ from openai import OpenAI
121
+
122
+ import config.config as config
123
+
124
+ self.client = OpenAI(
125
+ api_key=config.VISION_API_KEY, base_url=config.VISION_BASE_URL
126
+ )
127
+ self.model_name = config.VISION_MODEL_NAME
128
+
129
+ def heal_script(
130
+ self,
131
+ script_content: str,
132
+ error_msg: str,
133
+ error_line_num: int,
134
+ ui_json: str,
135
+ screenshot_base64: str,
136
+ platform: str,
137
+ ) -> HealResult:
138
+ """Analyze failure and generate fix. Returns HealResult (never None)."""
139
+ log.info(
140
+ f"🧠 [HealerBrain] Analyzing {platform} failure at line {error_line_num}..."
141
+ )
142
+ start_time = time.time()
143
+
144
+ system_prompt = """你是一个自动化测试自愈引擎。当测试用例执行失败时,你负责分析原因并生成修复代码。
145
+
146
+ 【输入】
147
+ 1. 报错行号和异常堆栈
148
+ 2. 案发瞬间的 UI 元素树 (JSON) 和截图
149
+ 3. 原始测试脚本
150
+
151
+ 【思考步骤】
152
+ 1. 分析报错:元素找不到?Strict Mode 多元素冲突?弹窗遮挡?
153
+ 2. 观察 UI 树和截图,找到目标元素当前的实际状态
154
+ 3. 在保证业务流完整性的前提下,修改失败的定位器代码
155
+
156
+ 【输出格式 — 必须返回 JSON】
157
+ {
158
+ "confidence": 0.0到1.0的浮点数,
159
+ "fix_description": "简短描述修复了什么",
160
+ "fixed_code": "完整的修复后 Python 脚本代码"
161
+ }
162
+
163
+ confidence 含义:
164
+ - 0.9-1.0: 明确找到了元素变化,修复方案确定
165
+ - 0.7-0.8: 找到了可能的匹配,修复方案较有把握
166
+ - 0.5-0.6: 不太确定,但做了最佳猜测
167
+ - <0.5: 不确定修复是否正确
168
+
169
+ 注意:fixed_code 中的换行用 \\n 表示,确保 JSON 可解析。只返回 JSON,不要其他文字。"""
170
+
171
+ user_prompt = f"""【报错平台】: {platform}
172
+ 【报错行号】: 第 {error_line_num} 行
173
+ 【异常信息】: {error_msg}
174
+
175
+ 【UI 树】:
176
+ {ui_json}
177
+
178
+ 【原始脚本】:
179
+ {script_content}
180
+
181
+ 请返回 JSON 格式的修复方案。"""
182
+
183
+ messages = [{"role": "system", "content": system_prompt}]
184
+ user_content = [{"type": "text", "text": user_prompt}]
185
+ if screenshot_base64:
186
+ user_content.append(
187
+ {
188
+ "type": "image_url",
189
+ "image_url": {"url": f"data:image/png;base64,{screenshot_base64}"},
190
+ }
191
+ )
192
+ messages.append({"role": "user", "content": user_content})
193
+
194
+ try:
195
+ response = self.client.chat.completions.create(
196
+ model=self.model_name,
197
+ messages=messages,
198
+ temperature=0.1,
199
+ )
200
+
201
+ raw = response.choices[0].message.content.strip()
202
+ result = _parse_heal_response(raw)
203
+
204
+ # Syntax validation — invalid syntax forces confidence to 0
205
+ if result.fixed_code and not result.is_valid_syntax:
206
+ log.warning("⚠️ [HealerBrain] Generated code has syntax errors, rejecting")
207
+ result = HealResult(
208
+ confidence=0.0,
209
+ fix_description=f"syntax error in generated code: {result.fix_description}",
210
+ fixed_code="",
211
+ )
212
+
213
+ latency = time.time() - start_time
214
+ log.info(
215
+ f"⏱️ [HealerBrain] Done in {latency:.2f}s "
216
+ f"(confidence={result.confidence:.2f}, desc={result.fix_description[:80]})"
217
+ )
218
+ return result
219
+
220
+ except Exception as e:
221
+ log.error(f"❌ [HealerBrain] API call failed: {e}")
222
+ return HealResult(confidence=0.0, fix_description=f"API error: {e}", fixed_code="")
@@ -0,0 +1,15 @@
1
+ from .cache_hash import compute_instruction_hash, compute_ui_hash
2
+ from .cache_manager import CacheManager
3
+ from .cache_stats import CacheStats
4
+ from .cache_storage import cleanup_expired_entries, get_cache_filename, load_cache, save_cache
5
+
6
+ __all__ = [
7
+ "compute_ui_hash",
8
+ "compute_instruction_hash",
9
+ "get_cache_filename",
10
+ "load_cache",
11
+ "save_cache",
12
+ "cleanup_expired_entries",
13
+ "CacheStats",
14
+ "CacheManager"
15
+ ]
@@ -0,0 +1,57 @@
1
+ import hashlib
2
+ import json
3
+ import re
4
+ from typing import Any, Dict
5
+
6
+
7
+ def _extract_semantic_fingerprint(ui_json: Dict[str, Any]) -> list:
8
+ """
9
+ 提取页面锚点指纹,免疫动态数据和渲染顺序波动。
10
+ """
11
+ fingerprint_features = set()
12
+ elements = ui_json.get("ui_elements", [])
13
+
14
+ for el in elements:
15
+ raw_text = el.get("text", "") or el.get("desc", "")
16
+ # 抹除所有数字、字母、符号,只保留纯汉字
17
+ cn_text = re.sub(r"[^\u4e00-\u9fa5]", "", raw_text)
18
+
19
+ # 标准的 UI 导航或按钮,通常在 2 到 6 个汉字之间
20
+ if 2 <= len(cn_text) <= 6:
21
+ # 动态黑名单
22
+ if cn_text in [
23
+ "加密货币",
24
+ "比特币",
25
+ "meme币",
26
+ "美股代币",
27
+ "贵金属代币",
28
+ "热门资产",
29
+ "已上线",
30
+ "已上架",
31
+ "上架",
32
+ "下架",
33
+ "公告",
34
+ "活动",
35
+ ]:
36
+ continue
37
+ fingerprint_features.add(f"{el.get('class')}|{cn_text}")
38
+
39
+ # 强制排序并转为列表,保证哈希的绝对一致性
40
+ return sorted(list(fingerprint_features))
41
+
42
+
43
+ def compute_ui_hash(ui_json: Dict[str, Any]) -> str:
44
+ """计算用于混合缓存匹配的页面骨架 Hash"""
45
+ fingerprint = _extract_semantic_fingerprint(ui_json)
46
+ fingerprint_str = json.dumps(fingerprint)
47
+ hash_obj = hashlib.sha256()
48
+ hash_obj.update(fingerprint_str.encode("utf-8"))
49
+ return hash_obj.hexdigest()
50
+
51
+
52
+ def compute_instruction_hash(instruction: str) -> str:
53
+ """计算用于混合缓存 O(1) 精确匹配的指令 Hash"""
54
+ normalized_inst = re.sub(r"\s+", " ", instruction).strip().lower()
55
+ hash_obj = hashlib.sha256()
56
+ hash_obj.update(normalized_inst.encode("utf-8"))
57
+ return hash_obj.hexdigest()