sap-qa-agent 0.1.0__tar.gz
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.
- sap_qa_agent-0.1.0/.github/workflows/test.yml +31 -0
- sap_qa_agent-0.1.0/.gitignore +18 -0
- sap_qa_agent-0.1.0/PKG-INFO +26 -0
- sap_qa_agent-0.1.0/README.md +81 -0
- sap_qa_agent-0.1.0/app/__init__.py +1 -0
- sap_qa_agent-0.1.0/app/ai/__init__.py +1 -0
- sap_qa_agent-0.1.0/app/ai/glm_client.py +398 -0
- sap_qa_agent-0.1.0/app/ai/prompt_manager.py +193 -0
- sap_qa_agent-0.1.0/app/config.py +127 -0
- sap_qa_agent-0.1.0/app/core/__init__.py +1 -0
- sap_qa_agent-0.1.0/app/core/execution_engine.py +407 -0
- sap_qa_agent-0.1.0/app/core/exploration_engine.py +231 -0
- sap_qa_agent-0.1.0/app/core/orchestrator.py +606 -0
- sap_qa_agent-0.1.0/app/core/result_verifier.py +330 -0
- sap_qa_agent-0.1.0/app/core/reuse_engine.py +211 -0
- sap_qa_agent-0.1.0/app/core/rule_engine.py +191 -0
- sap_qa_agent-0.1.0/app/core/screen_matcher.py +74 -0
- sap_qa_agent-0.1.0/app/core/self_healing_controller.py +206 -0
- sap_qa_agent-0.1.0/app/core/step_splitter.py +97 -0
- sap_qa_agent-0.1.0/app/core/template_matcher.py +82 -0
- sap_qa_agent-0.1.0/app/core/test_case_parser.py +578 -0
- sap_qa_agent-0.1.0/app/data/__init__.py +1 -0
- sap_qa_agent-0.1.0/app/data/fingerprint_store.py +119 -0
- sap_qa_agent-0.1.0/app/data/flow_variable.py +142 -0
- sap_qa_agent-0.1.0/app/data/knowledge_base.py +227 -0
- sap_qa_agent-0.1.0/app/data/models.py +552 -0
- sap_qa_agent-0.1.0/app/data/report_generator.py +253 -0
- sap_qa_agent-0.1.0/app/desktop_apps.py +250 -0
- sap_qa_agent-0.1.0/app/excel_parser.py +104 -0
- sap_qa_agent-0.1.0/app/executor.py +642 -0
- sap_qa_agent-0.1.0/app/main.py +90 -0
- sap_qa_agent-0.1.0/app/mcp_server/__init__.py +15 -0
- sap_qa_agent-0.1.0/app/mcp_server/channel_lock.py +326 -0
- sap_qa_agent-0.1.0/app/mcp_server/cli.py +450 -0
- sap_qa_agent-0.1.0/app/mcp_server/config.py +364 -0
- sap_qa_agent-0.1.0/app/mcp_server/context.py +346 -0
- sap_qa_agent-0.1.0/app/mcp_server/errors.py +235 -0
- sap_qa_agent-0.1.0/app/mcp_server/metrics.py +120 -0
- sap_qa_agent-0.1.0/app/mcp_server/middleware.py +270 -0
- sap_qa_agent-0.1.0/app/mcp_server/prompts.py +126 -0
- sap_qa_agent-0.1.0/app/mcp_server/resources.py +468 -0
- sap_qa_agent-0.1.0/app/mcp_server/safety.py +203 -0
- sap_qa_agent-0.1.0/app/mcp_server/server.py +221 -0
- sap_qa_agent-0.1.0/app/mcp_server/skills_loader/__init__.py +29 -0
- sap_qa_agent-0.1.0/app/mcp_server/skills_loader/cache.py +81 -0
- sap_qa_agent-0.1.0/app/mcp_server/skills_loader/loader.py +353 -0
- sap_qa_agent-0.1.0/app/mcp_server/skills_loader/validator.py +89 -0
- sap_qa_agent-0.1.0/app/mcp_server/token_budget.py +185 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/__init__.py +7 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/atomic.py +662 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/capture.py +154 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/execution.py +398 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/knowledge.py +172 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/parse.py +278 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/recording.py +254 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/session.py +357 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/skills.py +148 -0
- sap_qa_agent-0.1.0/app/mcp_server/tools/tasks.py +379 -0
- sap_qa_agent-0.1.0/app/mcp_server/transport/__init__.py +9 -0
- sap_qa_agent-0.1.0/app/mcp_server/transport/http.py +109 -0
- sap_qa_agent-0.1.0/app/mcp_server/transport/stdio.py +54 -0
- sap_qa_agent-0.1.0/app/models.py +92 -0
- sap_qa_agent-0.1.0/app/prompts/__init__.py +24 -0
- sap_qa_agent-0.1.0/app/prompts/batch_recovery.py +50 -0
- sap_qa_agent-0.1.0/app/prompts/car.py +82 -0
- sap_qa_agent-0.1.0/app/prompts/dialog_analyze.py +27 -0
- sap_qa_agent-0.1.0/app/prompts/element_match.py +20 -0
- sap_qa_agent-0.1.0/app/prompts/exception_handle.py +25 -0
- sap_qa_agent-0.1.0/app/prompts/failure_analysis.py +13 -0
- sap_qa_agent-0.1.0/app/prompts/recovery_plan.py +56 -0
- sap_qa_agent-0.1.0/app/prompts/recovery_script.py +86 -0
- sap_qa_agent-0.1.0/app/prompts/sap.py +74 -0
- sap_qa_agent-0.1.0/app/prompts/script_generate.py +61 -0
- sap_qa_agent-0.1.0/app/prompts/step_adjust.py +44 -0
- sap_qa_agent-0.1.0/app/prompts/step_split.py +281 -0
- sap_qa_agent-0.1.0/app/prompts/verify_result.py +23 -0
- sap_qa_agent-0.1.0/app/recorder.py +132 -0
- sap_qa_agent-0.1.0/app/routes.py +247 -0
- sap_qa_agent-0.1.0/app/sap/__init__.py +1 -0
- sap_qa_agent-0.1.0/app/sap/dialog_handler.py +150 -0
- sap_qa_agent-0.1.0/app/sap/dialog_watchdog.py +128 -0
- sap_qa_agent-0.1.0/app/sap/element_path_finder.py +899 -0
- sap_qa_agent-0.1.0/app/sap/fingerprint_generator.py +302 -0
- sap_qa_agent-0.1.0/app/sap/sap_gui_wrapper.py +502 -0
- sap_qa_agent-0.1.0/app/sap/screen_state_collector.py +99 -0
- sap_qa_agent-0.1.0/app/sap/script_executor.py +1880 -0
- sap_qa_agent-0.1.0/app/sap/vlm_fallback.py +210 -0
- sap_qa_agent-0.1.0/app/sap_routes.py +488 -0
- sap_qa_agent-0.1.0/app/task_store.py +221 -0
- sap_qa_agent-0.1.0/app/utils.py +40 -0
- sap_qa_agent-0.1.0/app/vlm_executor.py +1915 -0
- sap_qa_agent-0.1.0/artifacts/9b539a374682/report.json +17 -0
- sap_qa_agent-0.1.0/artifacts/de9cd8df75b5/report.json +17 -0
- sap_qa_agent-0.1.0/artifacts/mcp-parsed/9c73327f7bb7/case_0.json +15 -0
- sap_qa_agent-0.1.0/artifacts/mcp-parsed/9c73327f7bb7/case_1.json +13 -0
- sap_qa_agent-0.1.0/artifacts/screenshots/step_004.png +0 -0
- sap_qa_agent-0.1.0/artifacts/screenshots/step_005.png +0 -0
- sap_qa_agent-0.1.0/artifacts/screenshots/step_006.png +0 -0
- sap_qa_agent-0.1.0/collect_elements.py +284 -0
- sap_qa_agent-0.1.0/conftest.py +72 -0
- sap_qa_agent-0.1.0/docs/PUBLISHING.md +192 -0
- sap_qa_agent-0.1.0/docs/mcp-integration.md +525 -0
- sap_qa_agent-0.1.0/examples/mcp/claude_desktop_config.json +10 -0
- sap_qa_agent-0.1.0/examples/mcp/kiro_mcp.json +18 -0
- sap_qa_agent-0.1.0/knowledge_base/page_fingerprints.json +88 -0
- sap_qa_agent-0.1.0/knowledge_base/patterns.json +11 -0
- sap_qa_agent-0.1.0/knowledge_base/templates.json +615 -0
- sap_qa_agent-0.1.0/mcp_skills/sap-knowledge-base-reuse/SKILL.md +66 -0
- sap_qa_agent-0.1.0/mcp_skills/sap-parse-and-execute/SKILL.md +76 -0
- sap_qa_agent-0.1.0/mcp_skills/sap-recording-to-testcase/SKILL.md +68 -0
- sap_qa_agent-0.1.0/mcp_skills/sap-self-healing-playbook/SKILL.md +74 -0
- sap_qa_agent-0.1.0/mcp_skills/sap-session-bootstrap/SKILL.md +63 -0
- sap_qa_agent-0.1.0/pyproject.toml +45 -0
- sap_qa_agent-0.1.0/requirements.txt +19 -0
- sap_qa_agent-0.1.0/run.py +28 -0
- sap_qa_agent-0.1.0/run_test_case.py +201 -0
- sap_qa_agent-0.1.0/test_e2e_validation.py +379 -0
- sap_qa_agent-0.1.0/test_login_validation.py +148 -0
- sap_qa_agent-0.1.0/test_sap_com_diag.py +110 -0
- sap_qa_agent-0.1.0/test_sap_connection.py +101 -0
- sap_qa_agent-0.1.0/test_sap_login.py +155 -0
- sap_qa_agent-0.1.0/tests/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/conftest.py +111 -0
- sap_qa_agent-0.1.0/tests/mcp/contract/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/contract/test_initialize.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/contract/test_prompts_list.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/contract/test_resources_list.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/contract/test_tools_list.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/e2e/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/e2e/test_full_flow_real_sap.py +18 -0
- sap_qa_agent-0.1.0/tests/mcp/e2e/test_smoke_checklist.py +245 -0
- sap_qa_agent-0.1.0/tests/mcp/property/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_error_envelope.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_initialize_contract.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_no_secret_leak.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_path_whitelist.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_skill_required_tools.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_task_uniqueness.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/property/test_token_budget.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/resources/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/resources/test_uri_router.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/test_metrics.py +167 -0
- sap_qa_agent-0.1.0/tests/mcp/test_response_scrubbing.py +226 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/__init__.py +1 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_capture.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_execute_test_case.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_knowledge.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_parse_test_case.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_recording.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_session.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_skills.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_tasks.py +4 -0
- sap_qa_agent-0.1.0/tests/mcp/tools/test_token_budget.py +228 -0
- sap_qa_agent-0.1.0/tests/test_dialog_watchdog.py +214 -0
- sap_qa_agent-0.1.0/tests/test_excel_parser.py +134 -0
- sap_qa_agent-0.1.0/tests/test_executor.py +251 -0
- sap_qa_agent-0.1.0/tests/test_exploration_engine.py +123 -0
- sap_qa_agent-0.1.0/tests/test_flow_variable.py +155 -0
- sap_qa_agent-0.1.0/tests/test_knowledge_base.py +286 -0
- sap_qa_agent-0.1.0/tests/test_orchestrator.py +352 -0
- sap_qa_agent-0.1.0/tests/test_prompt_manager.py +127 -0
- sap_qa_agent-0.1.0/tests/test_result_verifier.py +156 -0
- sap_qa_agent-0.1.0/tests/test_reuse_engine.py +105 -0
- sap_qa_agent-0.1.0/tests/test_screen_matcher.py +141 -0
- sap_qa_agent-0.1.0/tests/test_step_splitter.py +273 -0
- sap_qa_agent-0.1.0/tests/test_task_store.py +92 -0
- sap_qa_agent-0.1.0/tests/test_template_matcher.py +68 -0
- sap_qa_agent-0.1.0/tests/test_test_case_parser.py +326 -0
- sap_qa_agent-0.1.0/tests/test_utils.py +81 -0
- sap_qa_agent-0.1.0/uploads/SAP_FB01_/350/264/242/345/212/241/345/207/255/350/257/201/345/275/225/345/205/245/346/265/213/350/257/225.xlsx +0 -0
- sap_qa_agent-0.1.0/uploads/VA02/346/265/213/350/257/225/350/204/232/346/234/254.xlsx +0 -0
- sap_qa_agent-0.1.0/uploads/rules.xlsx +0 -0
- sap_qa_agent-0.1.0/uploads/sample_steps.xlsx +0 -0
- sap_qa_agent-0.1.0/uploads/testcase.xlsx +0 -0
- sap_qa_agent-0.1.0/uv.lock +2639 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python 3.12
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e ".[all]"
|
|
25
|
+
pip install pytest pytest-asyncio hypothesis
|
|
26
|
+
|
|
27
|
+
- name: Validate Skills
|
|
28
|
+
run: sap-qa-mcp validate-skills
|
|
29
|
+
|
|
30
|
+
- name: Run MCP tests
|
|
31
|
+
run: pytest tests/mcp/ -v --tb=short
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sap-qa-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SAP QA Agent — UI automation engine + MCP Server packaging for SAP Public Cloud
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: fastmcp>=0.2.0
|
|
7
|
+
Requires-Dist: httpx
|
|
8
|
+
Requires-Dist: opencv-python
|
|
9
|
+
Requires-Dist: openpyxl>=3.1
|
|
10
|
+
Requires-Dist: pillow
|
|
11
|
+
Requires-Dist: psutil
|
|
12
|
+
Requires-Dist: pydantic>=2.0
|
|
13
|
+
Requires-Dist: python-frontmatter
|
|
14
|
+
Requires-Dist: pywin32; sys_platform == 'win32'
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Requires-Dist: rapidocr-onnxruntime
|
|
17
|
+
Requires-Dist: ultralytics
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
20
|
+
Requires-Dist: python-multipart; extra == 'all'
|
|
21
|
+
Requires-Dist: uvicorn>=0.29; extra == 'all'
|
|
22
|
+
Provides-Extra: backend
|
|
23
|
+
Requires-Dist: fastapi>=0.110; extra == 'backend'
|
|
24
|
+
Requires-Dist: python-multipart; extra == 'backend'
|
|
25
|
+
Requires-Dist: uvicorn>=0.29; extra == 'backend'
|
|
26
|
+
Provides-Extra: ui
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SAP QA Agent
|
|
2
|
+
|
|
3
|
+
基于 AI 驱动的 SAP GUI 自动化测试引擎,支持测试用例解析、智能执行、AI 自愈和操作录制。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 安装
|
|
9
|
+
pip install -e .
|
|
10
|
+
|
|
11
|
+
# 启动 FastAPI 后端(配合 Vue 前端使用)
|
|
12
|
+
sap-qa-backend
|
|
13
|
+
|
|
14
|
+
# 或启动 MCP Server(配合 Claude Desktop / Kiro / Cursor 使用)
|
|
15
|
+
sap-qa-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 核心功能
|
|
19
|
+
|
|
20
|
+
- **测试用例解析** — 支持 Excel (.xlsx) 和 Markdown 格式
|
|
21
|
+
- **SAP GUI 自动化** — 基于 COM Scripting API 驱动 SAP 操作
|
|
22
|
+
- **AI 自愈** — 执行失败时自动分析并修复(基于智谱 GLM)
|
|
23
|
+
- **知识库复用** — 积累脚本模板,加速后续执行
|
|
24
|
+
- **操作录制** — 记录手动操作转化为可复用步骤
|
|
25
|
+
|
|
26
|
+
## 两种使用方式
|
|
27
|
+
|
|
28
|
+
### 1. Web 界面(FastAPI + Vue)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install sap-qa-agent[backend]
|
|
32
|
+
sap-qa-backend
|
|
33
|
+
# 访问 http://localhost:8000
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. 作为 MCP Server 被使用
|
|
37
|
+
|
|
38
|
+
SAP QA Agent 可作为 MCP (Model Context Protocol) Server,被 Claude Desktop、Kiro、Cursor 等 AI Agent 直接调用。
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install sap-qa-agent
|
|
42
|
+
sap-qa-mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
详细接入指南请参阅 **[docs/mcp-integration.md](docs/mcp-integration.md)**,包含:
|
|
46
|
+
|
|
47
|
+
- Claude Desktop / Kiro / Cursor 三种宿主的配置示例
|
|
48
|
+
- SAP Scripting 启用步骤
|
|
49
|
+
- GLM API Key 配置方式
|
|
50
|
+
- 端到端使用示例
|
|
51
|
+
- 可用工具列表与故障排查
|
|
52
|
+
|
|
53
|
+
## 项目结构
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
ui_test_python/
|
|
57
|
+
├── app/ # 应用代码
|
|
58
|
+
│ ├── main.py # FastAPI 入口
|
|
59
|
+
│ ├── mcp_server/ # MCP Server 实现
|
|
60
|
+
│ ├── core/ # 核心引擎(解析、执行、自愈)
|
|
61
|
+
│ ├── sap/ # SAP GUI 封装
|
|
62
|
+
│ ├── ai/ # GLM AI 客户端
|
|
63
|
+
│ └── data/ # 数据模型与知识库
|
|
64
|
+
├── mcp_skills/ # MCP Skills 包
|
|
65
|
+
├── knowledge_base/ # 脚本模板知识库
|
|
66
|
+
├── tests/ # 测试套件
|
|
67
|
+
├── examples/mcp/ # MCP 配置示例
|
|
68
|
+
├── docs/ # 文档
|
|
69
|
+
│ └── mcp-integration.md # MCP 接入指南
|
|
70
|
+
└── pyproject.toml # 项目配置
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 环境要求
|
|
74
|
+
|
|
75
|
+
- Python 3.12+
|
|
76
|
+
- Windows 10/11
|
|
77
|
+
- SAP GUI(已启用 Scripting)
|
|
78
|
+
|
|
79
|
+
## 许可证
|
|
80
|
+
|
|
81
|
+
Private
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# backend app package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI layer modules for GLM integration."""
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""智谱 AI GLM 模型客户端 — 封装所有 AI 调用。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from zhipuai import ZhipuAI
|
|
10
|
+
|
|
11
|
+
from app.config import (
|
|
12
|
+
GLM_API_KEY,
|
|
13
|
+
GLM_BASE_URL,
|
|
14
|
+
GLM_EXCEPTION_TIMEOUT,
|
|
15
|
+
GLM_MODEL,
|
|
16
|
+
GLM_TIMEOUT,
|
|
17
|
+
)
|
|
18
|
+
from app.data.models import ElementInfo, StepActionType, StepResult, TestStep
|
|
19
|
+
from app.ai.prompt_manager import PromptManager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GlmClient:
|
|
25
|
+
"""智谱 AI GLM 模型客户端。
|
|
26
|
+
|
|
27
|
+
所有公开方法均为 async(保持接口兼容),内部直接同步调用 SDK。
|
|
28
|
+
由于执行线程是独立的 worker thread,阻塞调用不会影响 FastAPI 主 loop。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: str = GLM_API_KEY,
|
|
34
|
+
base_url: str = GLM_BASE_URL,
|
|
35
|
+
model: str = GLM_MODEL,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._client = ZhipuAI(api_key=api_key, base_url=base_url)
|
|
38
|
+
self._base_url = base_url
|
|
39
|
+
self._model = model
|
|
40
|
+
self._prompt_manager = PromptManager()
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# Internal helpers
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def _chat_sync(self, prompt: str, timeout: int = GLM_TIMEOUT) -> str:
|
|
47
|
+
"""Synchronous GLM chat call."""
|
|
48
|
+
logger.info("GLM request (model=%s, timeout=%ds):\n%s", self._model, timeout, prompt[:500])
|
|
49
|
+
response = self._client.chat.completions.create(
|
|
50
|
+
model=self._model,
|
|
51
|
+
messages=[{"role": "user", "content": prompt}],
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
)
|
|
54
|
+
content = response.choices[0].message.content
|
|
55
|
+
logger.info("GLM response:\n%s", content[:500])
|
|
56
|
+
return content
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _parse_json(text: str) -> Any:
|
|
60
|
+
"""Extract and parse JSON from GLM response text."""
|
|
61
|
+
cleaned = text.strip()
|
|
62
|
+
if cleaned.startswith("```"):
|
|
63
|
+
lines = cleaned.splitlines()
|
|
64
|
+
inner_lines = []
|
|
65
|
+
started = False
|
|
66
|
+
for line in lines:
|
|
67
|
+
if not started:
|
|
68
|
+
if line.strip().startswith("```"):
|
|
69
|
+
started = True
|
|
70
|
+
continue
|
|
71
|
+
elif line.strip() == "```":
|
|
72
|
+
break
|
|
73
|
+
else:
|
|
74
|
+
inner_lines.append(line)
|
|
75
|
+
cleaned = "\n".join(inner_lines)
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(cleaned)
|
|
78
|
+
except json.JSONDecodeError as exc:
|
|
79
|
+
logger.error("Failed to parse GLM JSON response: %s\nRaw: %s", exc, text[:300])
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Public async API (sync internally, async interface for compatibility)
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
async def split_steps(self, test_case_text: str, test_data: dict[str, str] | None = None) -> list[TestStep]:
|
|
87
|
+
"""调用 GLM 拆分测试步骤,返回 TestStep 列表。"""
|
|
88
|
+
import json as _json
|
|
89
|
+
test_data_str = _json.dumps(test_data, ensure_ascii=False) if test_data else ""
|
|
90
|
+
prompt = self._prompt_manager.render_step_split(
|
|
91
|
+
transaction_code="",
|
|
92
|
+
steps_text=test_case_text,
|
|
93
|
+
test_data=test_data_str,
|
|
94
|
+
)
|
|
95
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
96
|
+
items = self._parse_json(raw)
|
|
97
|
+
steps: list[TestStep] = []
|
|
98
|
+
for item in items:
|
|
99
|
+
# GLM 可能返回不同的字段名,做容错处理
|
|
100
|
+
target = (
|
|
101
|
+
item.get("target_description")
|
|
102
|
+
or item.get("target")
|
|
103
|
+
or item.get("description")
|
|
104
|
+
or item.get("field")
|
|
105
|
+
or ""
|
|
106
|
+
)
|
|
107
|
+
action = item.get("action_type") or item.get("action") or "input"
|
|
108
|
+
value = item.get("value") or item.get("input_value")
|
|
109
|
+
# 解析表格行号和列名(结构化表格输入)
|
|
110
|
+
row = None
|
|
111
|
+
raw_row = item.get("row")
|
|
112
|
+
if raw_row is not None:
|
|
113
|
+
try:
|
|
114
|
+
row = int(raw_row)
|
|
115
|
+
except (ValueError, TypeError):
|
|
116
|
+
pass
|
|
117
|
+
column = item.get("column") # str or None
|
|
118
|
+
steps.append(
|
|
119
|
+
TestStep(
|
|
120
|
+
index=item.get("index", len(steps) + 1),
|
|
121
|
+
action_type=StepActionType(action),
|
|
122
|
+
target_description=target,
|
|
123
|
+
value=value,
|
|
124
|
+
flow_variable_ref=item.get("flow_variable_ref"),
|
|
125
|
+
row=row,
|
|
126
|
+
column=column,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
logger.info("split_steps produced %d steps", len(steps))
|
|
130
|
+
return steps
|
|
131
|
+
|
|
132
|
+
async def generate_script(self, test_steps: list[TestStep], screen_elements: str = "") -> str:
|
|
133
|
+
"""调用 GLM 生成 SAP GUI Scripting API 脚本。"""
|
|
134
|
+
steps_dicts = [
|
|
135
|
+
{
|
|
136
|
+
"index": s.index,
|
|
137
|
+
"action_type": s.action_type.value,
|
|
138
|
+
"target_description": s.target_description,
|
|
139
|
+
"value": s.value,
|
|
140
|
+
"flow_variable_ref": s.flow_variable_ref,
|
|
141
|
+
"row": s.row,
|
|
142
|
+
"column": s.column,
|
|
143
|
+
}
|
|
144
|
+
for s in test_steps
|
|
145
|
+
]
|
|
146
|
+
prompt = self._prompt_manager.render_script_generate(
|
|
147
|
+
test_steps_json=json.dumps(steps_dicts, ensure_ascii=False, indent=2),
|
|
148
|
+
screen_elements=screen_elements,
|
|
149
|
+
)
|
|
150
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
151
|
+
script = raw.strip()
|
|
152
|
+
if script.startswith("```"):
|
|
153
|
+
lines = script.splitlines()
|
|
154
|
+
inner: list[str] = []
|
|
155
|
+
started = False
|
|
156
|
+
for line in lines:
|
|
157
|
+
if not started:
|
|
158
|
+
if line.strip().startswith("```"):
|
|
159
|
+
started = True
|
|
160
|
+
continue
|
|
161
|
+
elif line.strip() == "```":
|
|
162
|
+
break
|
|
163
|
+
else:
|
|
164
|
+
inner.append(line)
|
|
165
|
+
script = "\n".join(inner)
|
|
166
|
+
logger.info("generate_script produced %d chars", len(script))
|
|
167
|
+
return script
|
|
168
|
+
|
|
169
|
+
async def analyze_exception(
|
|
170
|
+
self,
|
|
171
|
+
error_info: str,
|
|
172
|
+
element_tree_snapshot: str,
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""调用 GLM 分析异常并生成恢复方案。"""
|
|
175
|
+
prompt = self._prompt_manager.render_exception_handle(
|
|
176
|
+
error_info=error_info,
|
|
177
|
+
current_step="",
|
|
178
|
+
ui_state=element_tree_snapshot,
|
|
179
|
+
status_bar_message="",
|
|
180
|
+
dialog_content="",
|
|
181
|
+
)
|
|
182
|
+
raw = self._chat_sync(prompt, GLM_EXCEPTION_TIMEOUT)
|
|
183
|
+
plan = self._parse_json(raw)
|
|
184
|
+
logger.info("analyze_exception decision: %s", plan.get("action"))
|
|
185
|
+
return plan
|
|
186
|
+
|
|
187
|
+
async def select_element(
|
|
188
|
+
self,
|
|
189
|
+
candidates: list[ElementInfo],
|
|
190
|
+
step_context: str,
|
|
191
|
+
) -> int:
|
|
192
|
+
"""调用 GLM 从候选元素中选择最佳匹配,返回索引。"""
|
|
193
|
+
candidates_dicts = [
|
|
194
|
+
{
|
|
195
|
+
"id_path": c.id_path,
|
|
196
|
+
"element_type": c.element_type,
|
|
197
|
+
"tooltip": c.tooltip,
|
|
198
|
+
"label_text": c.label_text,
|
|
199
|
+
"current_value": c.current_value,
|
|
200
|
+
}
|
|
201
|
+
for c in candidates
|
|
202
|
+
]
|
|
203
|
+
prompt = self._prompt_manager.render_element_match(
|
|
204
|
+
target_description=step_context,
|
|
205
|
+
step_context=step_context,
|
|
206
|
+
window_context="",
|
|
207
|
+
candidates_json=json.dumps(candidates_dicts, ensure_ascii=False, indent=2),
|
|
208
|
+
)
|
|
209
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
210
|
+
result = self._parse_json(raw)
|
|
211
|
+
selected = int(result["selected_index"])
|
|
212
|
+
logger.info("select_element chose index %d: %s", selected, result.get("reason", ""))
|
|
213
|
+
return selected
|
|
214
|
+
|
|
215
|
+
async def analyze_exception_v2(
|
|
216
|
+
self,
|
|
217
|
+
error_info: str,
|
|
218
|
+
step_description: str,
|
|
219
|
+
screen_snapshot: str,
|
|
220
|
+
retry_history: list[dict],
|
|
221
|
+
) -> dict:
|
|
222
|
+
"""调用 GLM 分析异常并生成 RecoveryPlan(v2)。"""
|
|
223
|
+
history_str = "\n".join(
|
|
224
|
+
f"第{h.get('attempt', '?')}次: 错误={h.get('error', '')}, 快照={h.get('snapshot', '')}"
|
|
225
|
+
for h in retry_history
|
|
226
|
+
) if retry_history else "无"
|
|
227
|
+
|
|
228
|
+
prompt = self._prompt_manager.render_recovery_plan(
|
|
229
|
+
step_description=step_description,
|
|
230
|
+
error_info=error_info,
|
|
231
|
+
screen_snapshot=screen_snapshot,
|
|
232
|
+
retry_history=history_str,
|
|
233
|
+
)
|
|
234
|
+
raw = self._chat_sync(prompt, GLM_EXCEPTION_TIMEOUT)
|
|
235
|
+
plan = self._parse_json(raw)
|
|
236
|
+
logger.info("analyze_exception_v2 decision: %s (confidence=%.2f)",
|
|
237
|
+
plan.get("action"), plan.get("confidence", 0))
|
|
238
|
+
return plan
|
|
239
|
+
|
|
240
|
+
async def analyze_dialog(
|
|
241
|
+
self,
|
|
242
|
+
dialog_title: str,
|
|
243
|
+
dialog_text: str,
|
|
244
|
+
timeout: int = GLM_EXCEPTION_TIMEOUT,
|
|
245
|
+
) -> dict:
|
|
246
|
+
"""调用 GLM 分析未知 SAP 弹窗并返回处理建议。"""
|
|
247
|
+
prompt = self._prompt_manager.render_dialog_analyze(
|
|
248
|
+
dialog_title=dialog_title,
|
|
249
|
+
dialog_text=dialog_text,
|
|
250
|
+
)
|
|
251
|
+
raw = self._chat_sync(prompt, timeout)
|
|
252
|
+
suggestion = self._parse_json(raw)
|
|
253
|
+
logger.info("analyze_dialog action: %s, reason: %s",
|
|
254
|
+
suggestion.get("action"), suggestion.get("reason", ""))
|
|
255
|
+
return suggestion
|
|
256
|
+
|
|
257
|
+
async def analyze_failure(self, step_result: StepResult) -> str:
|
|
258
|
+
"""调用 GLM 生成失败根因分析。"""
|
|
259
|
+
prompt = self._prompt_manager.render_failure_analysis(
|
|
260
|
+
step_description=f"Step {step_result.step_index}",
|
|
261
|
+
error_message=step_result.error_message or "",
|
|
262
|
+
status_bar_message=step_result.status_bar_message or "",
|
|
263
|
+
screenshot_description="",
|
|
264
|
+
)
|
|
265
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
266
|
+
analysis = raw.strip()
|
|
267
|
+
logger.info("analyze_failure result: %s", analysis[:200])
|
|
268
|
+
return analysis
|
|
269
|
+
|
|
270
|
+
async def verify_result(
|
|
271
|
+
self,
|
|
272
|
+
expected: str,
|
|
273
|
+
window_title: str,
|
|
274
|
+
status_bar: str,
|
|
275
|
+
screen_elements: str,
|
|
276
|
+
) -> dict:
|
|
277
|
+
"""调用 GLM 判断当前屏幕状态是否满足测试预期。"""
|
|
278
|
+
prompt = self._prompt_manager.render_verify_result(
|
|
279
|
+
expected=expected,
|
|
280
|
+
window_title=window_title,
|
|
281
|
+
status_bar=status_bar,
|
|
282
|
+
screen_elements=screen_elements,
|
|
283
|
+
)
|
|
284
|
+
raw = self._chat_sync(prompt, timeout=10)
|
|
285
|
+
result = self._parse_json(raw)
|
|
286
|
+
logger.info("verify_result: pass=%s, reason=%s",
|
|
287
|
+
result.get("pass"), result.get("reason", ""))
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
async def adjust_steps(
|
|
291
|
+
self,
|
|
292
|
+
test_case_context: str,
|
|
293
|
+
window_title: str,
|
|
294
|
+
status_bar: str,
|
|
295
|
+
screen_elements: str,
|
|
296
|
+
original_steps: list[dict],
|
|
297
|
+
) -> list[dict]:
|
|
298
|
+
"""根据当前屏幕状态调整步骤列表。
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
调整后的步骤 dict 列表。
|
|
302
|
+
"""
|
|
303
|
+
original_steps_json = json.dumps(original_steps, ensure_ascii=False, indent=2)
|
|
304
|
+
prompt = self._prompt_manager.render_step_adjust(
|
|
305
|
+
test_case_context=test_case_context,
|
|
306
|
+
window_title=window_title,
|
|
307
|
+
status_bar=status_bar,
|
|
308
|
+
screen_elements=screen_elements,
|
|
309
|
+
original_steps=original_steps_json,
|
|
310
|
+
)
|
|
311
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
312
|
+
adjusted = self._parse_json(raw)
|
|
313
|
+
logger.info("adjust_steps: %d → %d steps", len(original_steps), len(adjusted))
|
|
314
|
+
return adjusted
|
|
315
|
+
|
|
316
|
+
async def generate_recovery_script(
|
|
317
|
+
self,
|
|
318
|
+
test_case_context: str,
|
|
319
|
+
step_index: int,
|
|
320
|
+
action_type: str,
|
|
321
|
+
target_description: str,
|
|
322
|
+
value: str,
|
|
323
|
+
error_message: str,
|
|
324
|
+
completed_steps: str,
|
|
325
|
+
window_title: str,
|
|
326
|
+
status_bar: str,
|
|
327
|
+
screen_elements: str,
|
|
328
|
+
fingerprint_info: str = "",
|
|
329
|
+
) -> str:
|
|
330
|
+
"""调用 GLM 生成修复脚本 — 步骤失败时根据当前屏幕状态生成可执行代码。"""
|
|
331
|
+
prompt = self._prompt_manager.render_recovery_script(
|
|
332
|
+
test_case_context=test_case_context,
|
|
333
|
+
step_index=step_index,
|
|
334
|
+
action_type=action_type,
|
|
335
|
+
target_description=target_description,
|
|
336
|
+
value=value,
|
|
337
|
+
error_message=error_message,
|
|
338
|
+
completed_steps=completed_steps,
|
|
339
|
+
window_title=window_title,
|
|
340
|
+
status_bar=status_bar,
|
|
341
|
+
screen_elements=screen_elements,
|
|
342
|
+
fingerprint_info=fingerprint_info,
|
|
343
|
+
)
|
|
344
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
345
|
+
script = raw.strip()
|
|
346
|
+
if script.startswith("```"):
|
|
347
|
+
lines = script.splitlines()
|
|
348
|
+
inner: list[str] = []
|
|
349
|
+
started = False
|
|
350
|
+
for line in lines:
|
|
351
|
+
if not started:
|
|
352
|
+
if line.strip().startswith("```"):
|
|
353
|
+
started = True
|
|
354
|
+
continue
|
|
355
|
+
elif line.strip() == "```":
|
|
356
|
+
break
|
|
357
|
+
else:
|
|
358
|
+
inner.append(line)
|
|
359
|
+
script = "\n".join(inner)
|
|
360
|
+
logger.info("generate_recovery_script produced %d chars", len(script))
|
|
361
|
+
return script
|
|
362
|
+
|
|
363
|
+
async def generate_batch_recovery_script(
|
|
364
|
+
self,
|
|
365
|
+
test_case_context: str,
|
|
366
|
+
batch_steps: str,
|
|
367
|
+
completed_steps: str,
|
|
368
|
+
window_title: str,
|
|
369
|
+
status_bar: str,
|
|
370
|
+
screen_elements: str,
|
|
371
|
+
) -> str:
|
|
372
|
+
"""调用 GLM 生成批量修复脚本 — 多个连续输入步骤一次性处理。"""
|
|
373
|
+
prompt = self._prompt_manager.render_batch_recovery(
|
|
374
|
+
test_case_context=test_case_context,
|
|
375
|
+
batch_steps=batch_steps,
|
|
376
|
+
completed_steps=completed_steps,
|
|
377
|
+
window_title=window_title,
|
|
378
|
+
status_bar=status_bar,
|
|
379
|
+
screen_elements=screen_elements,
|
|
380
|
+
)
|
|
381
|
+
raw = self._chat_sync(prompt, GLM_TIMEOUT)
|
|
382
|
+
script = raw.strip()
|
|
383
|
+
if script.startswith("```"):
|
|
384
|
+
lines = script.splitlines()
|
|
385
|
+
inner: list[str] = []
|
|
386
|
+
started = False
|
|
387
|
+
for line in lines:
|
|
388
|
+
if not started:
|
|
389
|
+
if line.strip().startswith("```"):
|
|
390
|
+
started = True
|
|
391
|
+
continue
|
|
392
|
+
elif line.strip() == "```":
|
|
393
|
+
break
|
|
394
|
+
else:
|
|
395
|
+
inner.append(line)
|
|
396
|
+
script = "\n".join(inner)
|
|
397
|
+
logger.info("generate_batch_recovery_script produced %d chars", len(script))
|
|
398
|
+
return script
|