Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +13 -0
- .github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
- .github/ISSUE_TEMPLATE/feature-request.yaml +34 -0
- .github/PULL_REQUEST_TEMPLATE.md +40 -0
- .gitignore +63 -0
- .pre-commit-config.yaml +23 -0
- LICENSE +201 -0
- README.md +980 -0
- README_coding_agent.md +430 -0
- README_en.md +922 -0
- docs/ios_setup/ios_setup.md +134 -0
- docs/ios_setup/resources/enable-ui-automation.jpg +0 -0
- docs/ios_setup/resources/ios0_WebDriverAgent0.png +3 -0
- docs/ios_setup/resources/ios0_WebDriverAgent1.png +3 -0
- docs/ios_setup/resources/select-your-iphone-device.png +3 -0
- docs/ios_setup/resources/setup-xcode-wda.png +3 -0
- docs/ios_setup/resources/start-wda-testing.png +0 -0
- docs/ios_setup/resources/trust-dev-app.jpg +3 -0
- examples/basic_usage.py +190 -0
- examples/demo_thinking.py +64 -0
- ios.py +550 -0
- main.py +853 -0
- phone_agent/__init__.py +12 -0
- phone_agent/actions/__init__.py +5 -0
- phone_agent/actions/handler.py +399 -0
- phone_agent/actions/handler_ios.py +280 -0
- phone_agent/adb/__init__.py +51 -0
- phone_agent/adb/connection.py +353 -0
- phone_agent/adb/device.py +252 -0
- phone_agent/adb/input.py +109 -0
- phone_agent/adb/screenshot.py +109 -0
- phone_agent/agent.py +253 -0
- phone_agent/agent_ios.py +277 -0
- phone_agent/config/__init__.py +53 -0
- phone_agent/config/apps.py +227 -0
- phone_agent/config/apps_harmonyos.py +266 -0
- phone_agent/config/apps_ios.py +339 -0
- phone_agent/config/i18n.py +81 -0
- phone_agent/config/prompts.py +75 -0
- phone_agent/config/prompts_en.py +79 -0
- phone_agent/config/prompts_zh.py +77 -0
- phone_agent/config/timing.py +167 -0
- phone_agent/device_factory.py +167 -0
- phone_agent/hdc/__init__.py +53 -0
- phone_agent/hdc/connection.py +381 -0
- phone_agent/hdc/device.py +272 -0
- phone_agent/hdc/input.py +149 -0
- phone_agent/hdc/screenshot.py +125 -0
- phone_agent/model/__init__.py +5 -0
- phone_agent/model/client.py +290 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,16 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docs/ios_setup/resources/ios0_WebDriverAgent0.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/ios_setup/resources/ios0_WebDriverAgent1.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
docs/ios_setup/resources/select-your-iphone-device.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
docs/ios_setup/resources/setup-xcode-wda.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
docs/ios_setup/resources/trust-dev-app.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
resources/screenshot-20251209-181423.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
resources/screenshot-20251210-120416.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
resources/screenshot-20251210-120630.png filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
resources/select-your-iphone-device.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
resources/setting.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
resources/setup-xcode-wda.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
resources/trust-dev-app.jpg filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
resources/wechat.jpeg filter=lfs diff=lfs merge=lfs -text
|
.github/ISSUE_TEMPLATE/bug_report.yaml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "\U0001F41B Bug Report"
|
| 2 |
+
description: Submit a bug report to help us improve Open-AutoGLM / 提交一个 Bug 问题报告来帮助我们改进 Open-AutoGLM
|
| 3 |
+
body:
|
| 4 |
+
- type: textarea
|
| 5 |
+
id: system-info
|
| 6 |
+
attributes:
|
| 7 |
+
label: System Info / 系統信息
|
| 8 |
+
description: Your operating environment / 您的运行环境信息
|
| 9 |
+
placeholder: Includes Cuda version, Transformers version, Python version, operating system, hardware information (if you suspect a hardware problem)... / 包括Cuda版本,Transformers版本,Python版本,操作系统,硬件信息(如果您怀疑是硬件方面的问题)...
|
| 10 |
+
validations:
|
| 11 |
+
required: true
|
| 12 |
+
|
| 13 |
+
- type: textarea
|
| 14 |
+
id: who-can-help
|
| 15 |
+
attributes:
|
| 16 |
+
label: Who can help? / 谁可以帮助到您?
|
| 17 |
+
description: |
|
| 18 |
+
Your issue will be replied to more quickly if you can figure out the right person to tag with @
|
| 19 |
+
All issues are read by one of the maintainers, so if you don't know who to tag, just leave this blank and our maintainer will ping the right person.
|
| 20 |
+
|
| 21 |
+
Please tag fewer than 3 people.
|
| 22 |
+
|
| 23 |
+
如果您能找到合适的标签 @,您的问题会更快得到回复。
|
| 24 |
+
所有问题都会由我们的维护者阅读,如果您不知道该标记谁,只需留空,我们的维护人员会找到合适的开发组成员来解决问题。
|
| 25 |
+
|
| 26 |
+
标记的人数应该不超过 3 个人。
|
| 27 |
+
|
| 28 |
+
If it's not a bug in these three subsections, you may not specify the helper. Our maintainer will find the right person in the development group to solve the problem.
|
| 29 |
+
|
| 30 |
+
如果不是这三个子版块的bug,您可以不指明帮助者,我们的维护人员会找到合适的开发组成员来解决问题。
|
| 31 |
+
|
| 32 |
+
placeholder: "@Username ..."
|
| 33 |
+
|
| 34 |
+
- type: checkboxes
|
| 35 |
+
id: information-scripts-examples
|
| 36 |
+
attributes:
|
| 37 |
+
label: Information / 问题信息
|
| 38 |
+
description: 'The problem arises when using: / 问题出现在'
|
| 39 |
+
options:
|
| 40 |
+
- label: "The official example scripts / 官方的示例脚本"
|
| 41 |
+
- label: "My own modified scripts / 我自己修改的脚本和任务"
|
| 42 |
+
|
| 43 |
+
- type: textarea
|
| 44 |
+
id: reproduction
|
| 45 |
+
validations:
|
| 46 |
+
required: true
|
| 47 |
+
attributes:
|
| 48 |
+
label: Reproduction / 复现过程
|
| 49 |
+
description: |
|
| 50 |
+
Please provide a code example that reproduces the problem you encountered, preferably with a minimal reproduction unit.
|
| 51 |
+
If you have code snippets, error messages, stack traces, please provide them here as well.
|
| 52 |
+
Please format your code correctly using code tags. See https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting
|
| 53 |
+
Do not use screenshots, as they are difficult to read and (more importantly) do not allow others to copy and paste your code.
|
| 54 |
+
|
| 55 |
+
请提供能重现您遇到的问题的代码示例,最好是最小复现单元。
|
| 56 |
+
如果您有代码片段、错误信息、堆栈跟踪,也请在此提供。
|
| 57 |
+
请使用代码标签正确格式化您的代码。请参见 https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting
|
| 58 |
+
请勿使用截图,因为截图难以阅读,而且(更重要的是)不允许他人复制粘贴您的代码。
|
| 59 |
+
placeholder: |
|
| 60 |
+
Steps to reproduce the behavior/复现Bug的步骤:
|
| 61 |
+
|
| 62 |
+
1.
|
| 63 |
+
2.
|
| 64 |
+
3.
|
| 65 |
+
|
| 66 |
+
- type: textarea
|
| 67 |
+
id: expected-behavior
|
| 68 |
+
validations:
|
| 69 |
+
required: true
|
| 70 |
+
attributes:
|
| 71 |
+
label: Expected behavior / 期待表现
|
| 72 |
+
description: "A clear and concise description of what you would expect to happen. /简单描述您期望发生的事情。"
|
.github/ISSUE_TEMPLATE/feature-request.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "\U0001F680 Feature request"
|
| 2 |
+
description: Submit a request for a new Open-AutoGLM / 提交一个新的 Open-AutoGLM 的功能建议
|
| 3 |
+
labels: [ "feature" ]
|
| 4 |
+
body:
|
| 5 |
+
- type: textarea
|
| 6 |
+
id: feature-request
|
| 7 |
+
validations:
|
| 8 |
+
required: true
|
| 9 |
+
attributes:
|
| 10 |
+
label: Feature request / 功能建议
|
| 11 |
+
description: |
|
| 12 |
+
A brief description of the functional proposal. Links to corresponding papers and code are desirable.
|
| 13 |
+
对功能建议的简述。最好提供对应的论文和代码链接
|
| 14 |
+
|
| 15 |
+
- type: textarea
|
| 16 |
+
id: motivation
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
attributes:
|
| 20 |
+
label: Motivation / 动机
|
| 21 |
+
description: |
|
| 22 |
+
Your motivation for making the suggestion. If that motivation is related to another GitHub issue, link to it here.
|
| 23 |
+
您提出建议的动机。如果该动机与另一个 GitHub 问题有关,请在此处提供对应的链接。
|
| 24 |
+
|
| 25 |
+
- type: textarea
|
| 26 |
+
id: contribution
|
| 27 |
+
validations:
|
| 28 |
+
required: true
|
| 29 |
+
attributes:
|
| 30 |
+
label: Your contribution / 您的贡献
|
| 31 |
+
description: |
|
| 32 |
+
|
| 33 |
+
Your PR link or any other link you can help with.
|
| 34 |
+
您的PR链接或者其他您能提供帮助的链接。
|
.github/PULL_REQUEST_TEMPLATE.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contribution Guide
|
| 2 |
+
|
| 3 |
+
We welcome your contributions to this repository. To ensure elegant code style and better code quality, we have prepared
|
| 4 |
+
the following contribution guidelines.
|
| 5 |
+
|
| 6 |
+
## What We Accept
|
| 7 |
+
|
| 8 |
+
+ This PR fixes a typo or improves the documentation (if this is the case, you may skip the other checks).
|
| 9 |
+
+ This PR fixes a specific issue — please reference the issue number in the PR description. Make sure your code strictly
|
| 10 |
+
follows the coding standards below.
|
| 11 |
+
+ This PR introduces a new feature — please clearly explain the necessity and implementation of the feature. Make sure
|
| 12 |
+
your code strictly follows the coding standards below.
|
| 13 |
+
|
| 14 |
+
## Code Style Guide
|
| 15 |
+
|
| 16 |
+
Good code style is an art. We have prepared a `pre-commit` hook to enforce consistent code
|
| 17 |
+
formatting across the project. You can clean up your code following the steps below:
|
| 18 |
+
|
| 19 |
+
```shell
|
| 20 |
+
pre-commit run --all-files
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
If your code complies with the standards, you should not see any errors.
|
| 24 |
+
|
| 25 |
+
## Naming Conventions
|
| 26 |
+
|
| 27 |
+
+ Please use **English** for naming; do not use Pinyin or other languages. All comments should also be in English.
|
| 28 |
+
+ Follow **PEP8** naming conventions strictly, and use underscores to separate words. Avoid meaningless names such as
|
| 29 |
+
`a`, `b`, `c`.
|
| 30 |
+
|
| 31 |
+
## For glmv-reward Contributors
|
| 32 |
+
|
| 33 |
+
Before PR, Please run:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
cd glmv-reward/
|
| 37 |
+
uv sync
|
| 38 |
+
uv run poe lint
|
| 39 |
+
uv run poe typecheck
|
| 40 |
+
```
|
.gitignore
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
env/
|
| 27 |
+
.venv/
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.idea/
|
| 31 |
+
.vscode/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
|
| 36 |
+
# Testing
|
| 37 |
+
.pytest_cache/
|
| 38 |
+
.coverage
|
| 39 |
+
htmlcov/
|
| 40 |
+
.tox/
|
| 41 |
+
.nox/
|
| 42 |
+
|
| 43 |
+
# Type checking
|
| 44 |
+
.mypy_cache/
|
| 45 |
+
|
| 46 |
+
# Jupyter
|
| 47 |
+
.ipynb_checkpoints/
|
| 48 |
+
|
| 49 |
+
# OS
|
| 50 |
+
.DS_Store
|
| 51 |
+
Thumbs.db
|
| 52 |
+
|
| 53 |
+
# Project specific
|
| 54 |
+
*.log
|
| 55 |
+
/tmp/
|
| 56 |
+
screenshots/
|
| 57 |
+
|
| 58 |
+
# Keep old files during transition
|
| 59 |
+
call_model.py
|
| 60 |
+
app_package_name.py
|
| 61 |
+
|
| 62 |
+
.claude/
|
| 63 |
+
.venv
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
default_install_hook_types:
|
| 2 |
+
- pre-commit
|
| 3 |
+
- commit-msg
|
| 4 |
+
exclude: '^phone_agent/config/apps\.py$'
|
| 5 |
+
exclude: '^README_en\.md$'
|
| 6 |
+
default_stages:
|
| 7 |
+
- pre-commit # Run locally
|
| 8 |
+
repos:
|
| 9 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 10 |
+
rev: v0.11.7
|
| 11 |
+
hooks:
|
| 12 |
+
- id: ruff
|
| 13 |
+
args: [--output-format, github, --fix, --select, I]
|
| 14 |
+
- id: ruff-format
|
| 15 |
+
- repo: https://github.com/crate-ci/typos
|
| 16 |
+
rev: v1.32.0
|
| 17 |
+
hooks:
|
| 18 |
+
- id: typos
|
| 19 |
+
- repo: https://github.com/jackdewinter/pymarkdown
|
| 20 |
+
rev: v0.9.29
|
| 21 |
+
hooks:
|
| 22 |
+
- id: pymarkdown
|
| 23 |
+
args: [fix]
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright 2025 Zhipu AI
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-AutoGLM
|
| 2 |
+
|
| 3 |
+
[Readme in English](README_en.md)
|
| 4 |
+
|
| 5 |
+
<div align="center">
|
| 6 |
+
<img src=resources/logo.svg width="20%"/>
|
| 7 |
+
</div>
|
| 8 |
+
<p align="center">
|
| 9 |
+
👋 加入我们的 <a href="resources/WECHAT.md" target="_blank">微信</a> 社区
|
| 10 |
+
</p>
|
| 11 |
+
<p align="center">
|
| 12 |
+
🎤 进一步在我们的产品 <a href="https://autoglm.zhipuai.cn/autotyper/" target="_blank">智谱 AI 输入法</a> 体验“用嘴发指令”
|
| 13 |
+
</p
|
| 14 |
+
><p align="center">
|
| 15 |
+
<a href="https://mp.weixin.qq.com/s/wRp22dmRVF23ySEiATiWIQ" target="_blank">AutoGLM 实战派</a> 开发者激励活动火热进行中,跑通、二创即可瓜分数万元现金奖池!成果提交 👉 <a href="https://zhipu-ai.feishu.cn/share/base/form/shrcnE3ZuPD5tlOyVJ7d5Wtir8c?from=navigation" target="_blank">入口</a>
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
## 懒人版快速安装
|
| 19 |
+
|
| 20 |
+
你可以使用Claude Code,配置 [GLM Coding Plan](https://bigmodel.cn/glm-coding) 后,输入以下提示词,快速部署本项目。
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
访问文档,为我安装 AutoGLM
|
| 24 |
+
https://raw.githubusercontent.com/zai-org/Open-AutoGLM/refs/heads/main/README.md
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## 项目介绍
|
| 28 |
+
|
| 29 |
+
Phone Agent 是一个基于 AutoGLM 构建的手机端智能助理框架,它能够以多模态方式理解手机屏幕内容,并通过自动化操作帮助用户完成任务。系统通过
|
| 30 |
+
ADB(Android Debug Bridge)来控制设备,以视觉语言模型进行屏幕感知,再结合智能规划能力生成并执行操作流程。用户只需用自然语言描述需求,如“打开小红书搜索美食”,Phone
|
| 31 |
+
Agent 即可自动解析意图、理解当前界面、规划下一步动作并完成整个流程。系统还内置敏感操作确认机制,并支持在登录或验证码场景下进行人工接管。同时,它提供远程
|
| 32 |
+
ADB 调试能力,可通过 WiFi 或网络连接设备,实现灵活的远程控制与开发。
|
| 33 |
+
|
| 34 |
+
> ⚠️
|
| 35 |
+
> 本项目仅供研究和学习使用。严禁用于非法获取信息、干扰系统或任何违法活动。请仔细审阅 [使用条款](resources/privacy_policy.txt)。
|
| 36 |
+
|
| 37 |
+
## 模型下载地址
|
| 38 |
+
|
| 39 |
+
| Model | Download Links |
|
| 40 |
+
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| 41 |
+
| AutoGLM-Phone-9B | [🤗 Hugging Face](https://huggingface.co/zai-org/AutoGLM-Phone-9B)<br>[🤖 ModelScope](https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B) |
|
| 42 |
+
| AutoGLM-Phone-9B-Multilingual | [🤗 Hugging Face](https://huggingface.co/zai-org/AutoGLM-Phone-9B-Multilingual)<br>[🤖 ModelScope](https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B-Multilingual) |
|
| 43 |
+
|
| 44 |
+
其中,`AutoGLM-Phone-9B` 是针对中文手机应用优化的模型,而 `AutoGLM-Phone-9B-Multilingual` 支持英语场景,适用于包含英文等其他语言内容的应用。
|
| 45 |
+
|
| 46 |
+
## Android 环境准备
|
| 47 |
+
|
| 48 |
+
### 1. Python 环境
|
| 49 |
+
|
| 50 |
+
建议使用 Python 3.10 及以上版本。
|
| 51 |
+
|
| 52 |
+
### 2. 手机调试命令行工具
|
| 53 |
+
|
| 54 |
+
根据你的设备类型选择相应的工具:
|
| 55 |
+
|
| 56 |
+
#### 对于 Android 设备 - 使用 ADB
|
| 57 |
+
|
| 58 |
+
1. 下载官方 ADB [安装包](https://developer.android.com/tools/releases/platform-tools?hl=zh-cn),并解压到自定义路径
|
| 59 |
+
2. 配置环境变量
|
| 60 |
+
|
| 61 |
+
- MacOS 配置方法:在 `Terminal` 或者任何命令行工具里
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# 假设解压后的目录为 ~/Downloads/platform-tools。如果不是请自行调整命令。
|
| 65 |
+
export PATH=${PATH}:~/Downloads/platform-tools
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
- Windows 配置方法:可参考 [第三方教程](https://blog.csdn.net/x2584179909/article/details/108319973) 进行配置。
|
| 69 |
+
|
| 70 |
+
#### 对于鸿蒙设备 (HarmonyOS NEXT版本以上) - 使用 HDC
|
| 71 |
+
|
| 72 |
+
1. 下载 HDC 工具:
|
| 73 |
+
- 从 [HarmonyOS SDK](https://developer.huawei.com/consumer/cn/download/) 下载
|
| 74 |
+
2. 配置环境变量
|
| 75 |
+
|
| 76 |
+
- MacOS/Linux 配置方法:
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
# 假设解压后的目录为 ~/Downloads/harmonyos-sdk/toolchains。请根据实际路径调整。
|
| 80 |
+
export PATH=${PATH}:~/Downloads/harmonyos-sdk/toolchains
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
- Windows 配置方法:将 HDC 工具所在目录添加到系统 PATH 环境变量
|
| 84 |
+
|
| 85 |
+
### 3. Android 7.0+ 或 HarmonyOS 设备,并启用 `开发者模式` 和 `USB 调试`
|
| 86 |
+
|
| 87 |
+
1. 开发者模式启用:通常启用方法是,找到 `设置-关于手机-版本号` 然后连续快速点击 10
|
| 88 |
+
次左右,直到弹出弹窗显示“开发者模式已启用”。不同手机会有些许差别,如果找不到,可以上网搜索一下教程。
|
| 89 |
+
2. USB 调试启用:启用开发者模式之后,会出现 `设置-开发者选项-USB 调试`,勾选启用
|
| 90 |
+
3. 部分机型在设置开发者选项以后, 可能需要重启设备才能生效. 可以测试一下: 将手机用USB数据线连接到电脑后, `adb devices`
|
| 91 |
+
查看是否有设备信息, 如果没有说明连接失败.
|
| 92 |
+
|
| 93 |
+
**请务必仔细检查相关���限**
|
| 94 |
+
|
| 95 |
+

|
| 96 |
+
|
| 97 |
+
### 4. 安装 ADB Keyboard(仅 Android 设备需要,用于文本输入)
|
| 98 |
+
|
| 99 |
+
**注意:鸿蒙设备使用原生输入方法,无需安装 ADB Keyboard。**
|
| 100 |
+
|
| 101 |
+
如果你使用的是 Android 设备:
|
| 102 |
+
|
| 103 |
+
下载 [安装包](https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk) 并在对应的安卓设备中进行安装。
|
| 104 |
+
注意,安装完成后还需要到 `设置-输入法` 或者 `设置-键盘列表` 中启用 `ADB Keyboard` 才能生效(或使用命令`adb shell ime enable com.android.adbkeyboard/.AdbIME`[How-to-use](https://github.com/senzhk/ADBKeyBoard/blob/master/README.md#how-to-use))
|
| 105 |
+
|
| 106 |
+
## iPhone 环境准备
|
| 107 |
+
|
| 108 |
+
如果你使用的是 iPhone 设备,请参考专门的 iOS 配置文档:
|
| 109 |
+
|
| 110 |
+
📱 [iOS 环境配置指南](docs/ios_setup/ios_setup.md)
|
| 111 |
+
|
| 112 |
+
该文档详细介绍了如何配置 WebDriverAgent 和 iPhone 设备,以便在 iOS 上使用 AutoGLM。
|
| 113 |
+
|
| 114 |
+
## 部署准备工作
|
| 115 |
+
|
| 116 |
+
### 1. 安装依赖
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
pip install -r requirements.txt
|
| 120 |
+
pip install -e .
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### 2. 配置 ADB 或 HDC
|
| 124 |
+
|
| 125 |
+
#### 对于 Android 设备
|
| 126 |
+
|
| 127 |
+
确认 **USB数据线具有数据传输功能**, 而不是仅有充电功能
|
| 128 |
+
|
| 129 |
+
确保已安装 ADB 并使用 **USB数据线** 连接设备:
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
# 检查已连接的设备
|
| 133 |
+
adb devices
|
| 134 |
+
|
| 135 |
+
# 输出结果应显示你的设备,如:
|
| 136 |
+
# List of devices attached
|
| 137 |
+
# emulator-5554 device
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
#### 对于鸿蒙设备
|
| 141 |
+
|
| 142 |
+
确认 **USB数据线具有数据传输功能**, 而不是仅有充电功能
|
| 143 |
+
|
| 144 |
+
确保已安装 HDC 并使用 **USB数据线** 连接设备:
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
# 检查已连接的设备
|
| 148 |
+
hdc list targets
|
| 149 |
+
|
| 150 |
+
# 输出结果应显示你的设备,如:
|
| 151 |
+
# 7001005458323933328a01bce01c2500
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### 3. 启动模型服务
|
| 155 |
+
|
| 156 |
+
你可以选择自行部署模型服务,或使用第三方模型服务商。
|
| 157 |
+
|
| 158 |
+
#### 选项 A: 使用第三方模型服务
|
| 159 |
+
|
| 160 |
+
如果你不想自行部署模型,可以使用以下已部署我们模型的第三方服务:
|
| 161 |
+
|
| 162 |
+
**1. 智谱 BigModel**
|
| 163 |
+
|
| 164 |
+
- 文档: https://docs.bigmodel.cn/cn/api/introduction
|
| 165 |
+
- `--base-url`: `https://open.bigmodel.cn/api/paas/v4`
|
| 166 |
+
- `--model`: `autoglm-phone`
|
| 167 |
+
- `--apikey`: 在智谱平台申请你的 API Key
|
| 168 |
+
|
| 169 |
+
**2. ModelScope(魔搭社区)**
|
| 170 |
+
|
| 171 |
+
- 文档: https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B
|
| 172 |
+
- `--base-url`: `https://api-inference.modelscope.cn/v1`
|
| 173 |
+
- `--model`: `ZhipuAI/AutoGLM-Phone-9B`
|
| 174 |
+
- `--apikey`: 在 ModelScope 平台申请你的 API Key
|
| 175 |
+
|
| 176 |
+
使用第三方服务的示例:
|
| 177 |
+
|
| 178 |
+
```bash
|
| 179 |
+
# 使用智谱 BigModel
|
| 180 |
+
python main.py --base-url https://open.bigmodel.cn/api/paas/v4 --model "autoglm-phone" --apikey "your-bigmodel-api-key" "打开美团搜索附近的火锅店"
|
| 181 |
+
|
| 182 |
+
# 使用 ModelScope
|
| 183 |
+
python main.py --base-url https://api-inference.modelscope.cn/v1 --model "ZhipuAI/AutoGLM-Phone-9B" --apikey "your-modelscope-api-key" "打开美团搜索附近的火锅店"
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
#### 选项 B: 自行部署模型
|
| 187 |
+
|
| 188 |
+
如果你希望在本地或自己的服务器上部署模型:
|
| 189 |
+
|
| 190 |
+
1. 按照 `requirements.txt` 中 `For Model Deployment` 章节自行安装推理引擎框架。
|
| 191 |
+
|
| 192 |
+
对于SGLang, 除了使用pip安装,你也可以使用官方docker:
|
| 193 |
+
>
|
| 194 |
+
> ```shell
|
| 195 |
+
> docker pull lmsysorg/sglang:v0.5.6.post1
|
| 196 |
+
> ```
|
| 197 |
+
>
|
| 198 |
+
> 进入容器,执行
|
| 199 |
+
>
|
| 200 |
+
> ```
|
| 201 |
+
> pip install nvidia-cudnn-cu12==9.16.0.29
|
| 202 |
+
> ```
|
| 203 |
+
|
| 204 |
+
对于 vLLM,除了使用pip 安装,你也可以使用官方docker:
|
| 205 |
+
>
|
| 206 |
+
> ```shell
|
| 207 |
+
> docker pull vllm/vllm-openai:v0.12.0
|
| 208 |
+
> ```
|
| 209 |
+
>
|
| 210 |
+
> 进入容器,执行
|
| 211 |
+
>
|
| 212 |
+
> ```
|
| 213 |
+
> pip install -U transformers --pre
|
| 214 |
+
> ```
|
| 215 |
+
|
| 216 |
+
**注意**: 上述步骤出现的关于 transformers 的依赖冲突可以忽略。
|
| 217 |
+
|
| 218 |
+
1. 在对应容器或者实体机中(非容器安装)下载模型,通过 SGlang / vLLM 启动,得到 OpenAI 格式服务。这里提供一个 vLLM部署方案,请严格遵循我们提供的启动参数:
|
| 219 |
+
|
| 220 |
+
- vLLM:
|
| 221 |
+
|
| 222 |
+
```shell
|
| 223 |
+
python3 -m vllm.entrypoints.openai.api_server \
|
| 224 |
+
--served-model-name autoglm-phone-9b \
|
| 225 |
+
--allowed-local-media-path / \
|
| 226 |
+
--mm-encoder-tp-mode data \
|
| 227 |
+
--mm_processor_cache_type shm \
|
| 228 |
+
--mm_processor_kwargs "{\"max_pixels\":5000000}" \
|
| 229 |
+
--max-model-len 25480 \
|
| 230 |
+
--chat-template-content-format string \
|
| 231 |
+
--limit-mm-per-prompt "{\"image\":10}" \
|
| 232 |
+
--model zai-org/AutoGLM-Phone-9B \
|
| 233 |
+
--port 8000
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
- SGLang:
|
| 237 |
+
|
| 238 |
+
```shell
|
| 239 |
+
python3 -m sglang.launch_server --model-path zai-org/AutoGLM-Phone-9B \
|
| 240 |
+
--served-model-name autoglm-phone-9b \
|
| 241 |
+
--context-length 25480 \
|
| 242 |
+
--mm-enable-dp-encoder \
|
| 243 |
+
--mm-process-config '{"image":{"max_pixels":5000000}}' \
|
| 244 |
+
--port 8000
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
- 该模型结构与 `GLM-4.1V-9B-Thinking` 相同, 关于模型部署的详细内容,你也以查看 [GLM-V](https://github.com/zai-org/GLM-V)
|
| 248 |
+
获取模型部署和使用指南。
|
| 249 |
+
|
| 250 |
+
- 运行成功后,将可以通过 `http://localhost:8000/v1` 访问模型服务。 如果您在远程服务器部署模型, 使用该服务器的IP访问模型.
|
| 251 |
+
|
| 252 |
+
### 4. 检查模型部署
|
| 253 |
+
|
| 254 |
+
模型服务启动后,可以使用检查脚本验证部署是否成功:
|
| 255 |
+
|
| 256 |
+
```bash
|
| 257 |
+
python scripts/check_deployment_cn.py --base-url http://你的IP:你的端口/v1 --model 模型名称
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
脚本将发送测试请求并展示模型的推理结果,你可以根据输出判断模型部署是否正常工作。
|
| 261 |
+
|
| 262 |
+
基于给定的任务, 预期输出如下。**如果思维链长度很短, 或者出现了乱码, 很可能是模型部署失败**, 请仔细检查文档要求的配置和依赖。
|
| 263 |
+
|
| 264 |
+
```
|
| 265 |
+
<think>用户想要比较这个洗发水在京东和淘宝上的价格,然后选择最便宜的平台下单。当前在小红书app上,显示的是一个关于LUMMI MOOD洗发水的帖子。
|
| 266 |
+
|
| 267 |
+
我需要:
|
| 268 |
+
1. 先启动京东app,搜索这个洗发水
|
| 269 |
+
2. 查看京东的价格
|
| 270 |
+
3. 再启动淘宝app,搜索这个洗发水
|
| 271 |
+
4. 查看淘宝的价格
|
| 272 |
+
5. 比较价格后,选择最便宜的京东或淘宝下单
|
| 273 |
+
|
| 274 |
+
首先,我需要从当前的小红书界面退出,然后启动京东app。</think>
|
| 275 |
+
<answer>do(action="Launch", app="京东")
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
**参数说明:**
|
| 279 |
+
- `--base-url`: 模型服务地址(根据实际部署地址修改)
|
| 280 |
+
- `--model`: 模型名称
|
| 281 |
+
- `--messages-file`: 可选,指定自定义测试消息文件(默认使用 `scripts/sample_messages.json`)
|
| 282 |
+
|
| 283 |
+
## 使用 AutoGLM
|
| 284 |
+
|
| 285 |
+
### 命令行
|
| 286 |
+
|
| 287 |
+
根据你部署的模型, 设置 `--base-url` 和 `--model` 参数, 设置 `--device-type` 指定是安卓设备或鸿蒙设备 (默认值 adb 表示安卓设备, hdc 表示鸿蒙设备). 例如:
|
| 288 |
+
|
| 289 |
+
```bash
|
| 290 |
+
# Android 设备 - 交互模式
|
| 291 |
+
python main.py --base-url http://localhost:8000/v1 --model "autoglm-phone-9b"
|
| 292 |
+
|
| 293 |
+
# Android 设备 - 指定任务
|
| 294 |
+
python main.py --base-url http://localhost:8000/v1 "打开美团搜索附近的火锅店"
|
| 295 |
+
|
| 296 |
+
# 鸿蒙设备 - 交互模式
|
| 297 |
+
python main.py --device-type hdc --base-url http://localhost:8000/v1 --model "autoglm-phone-9b"
|
| 298 |
+
|
| 299 |
+
# 鸿蒙设备 - 指定任务
|
| 300 |
+
python main.py --device-type hdc --base-url http://localhost:8000/v1 "打开美团搜索附近的火锅店"
|
| 301 |
+
|
| 302 |
+
# 使用 API Key 进行认证
|
| 303 |
+
python main.py --apikey sk-xxxxx
|
| 304 |
+
|
| 305 |
+
# 使用英文 system prompt
|
| 306 |
+
python main.py --lang en --base-url http://localhost:8000/v1 "Open Chrome browser"
|
| 307 |
+
|
| 308 |
+
# 列出支持的应用(Android)
|
| 309 |
+
python main.py --list-apps
|
| 310 |
+
|
| 311 |
+
# 列出支持的应用(鸿蒙)
|
| 312 |
+
python main.py --device-type hdc --list-apps
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
### Python API
|
| 316 |
+
|
| 317 |
+
```python
|
| 318 |
+
from phone_agent import PhoneAgent
|
| 319 |
+
from phone_agent.model import ModelConfig
|
| 320 |
+
|
| 321 |
+
# Configure model
|
| 322 |
+
model_config = ModelConfig(
|
| 323 |
+
base_url="http://localhost:8000/v1",
|
| 324 |
+
model_name="autoglm-phone-9b",
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# 创建 Agent
|
| 328 |
+
agent = PhoneAgent(model_config=model_config)
|
| 329 |
+
|
| 330 |
+
# 执行任务
|
| 331 |
+
result = agent.run("打开淘宝搜索无线耳机")
|
| 332 |
+
print(result)
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
## 远程调试
|
| 336 |
+
|
| 337 |
+
Phone Agent 支持通过 WiFi/网络进行远程 ADB/HDC 调试,无需 USB 连接即可控制设备。
|
| 338 |
+
|
| 339 |
+
### 配置远程调试
|
| 340 |
+
|
| 341 |
+
#### 在手机端开启无线调试
|
| 342 |
+
|
| 343 |
+
##### Android 设备
|
| 344 |
+
|
| 345 |
+
确保手机和电脑在同一个WiFi中,如图所示
|
| 346 |
+
|
| 347 |
+

|
| 348 |
+
|
| 349 |
+
##### 鸿蒙设备
|
| 350 |
+
|
| 351 |
+
确保手机和电脑在同一个WiFi中:
|
| 352 |
+
1. 进入 `设置 > 系统和更新 > 开发者选项`
|
| 353 |
+
2. 开启 `USB 调试` 和 `无线调试`
|
| 354 |
+
3. 记录显示的 IP 地址和端口号
|
| 355 |
+
|
| 356 |
+
#### 在电脑端使用标准 ADB/HDC 命令
|
| 357 |
+
|
| 358 |
+
```bash
|
| 359 |
+
# Android 设备 - 通过 WiFi 连接, 改成手机显示的 IP 地址和端口
|
| 360 |
+
adb connect 192.168.1.100:5555
|
| 361 |
+
|
| 362 |
+
# 验证连接
|
| 363 |
+
adb devices
|
| 364 |
+
# 应显示:192.168.1.100:5555 device
|
| 365 |
+
|
| 366 |
+
# 鸿蒙设备 - 通过 WiFi 连接
|
| 367 |
+
hdc tconn 192.168.1.100:5555
|
| 368 |
+
|
| 369 |
+
# 验证连接
|
| 370 |
+
hdc list targets
|
| 371 |
+
# 应显示:192.168.1.100:5555
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
+
### 设备管理命令
|
| 375 |
+
|
| 376 |
+
#### Android 设备(ADB)
|
| 377 |
+
|
| 378 |
+
```bash
|
| 379 |
+
# 列出所有已连接设备
|
| 380 |
+
adb devices
|
| 381 |
+
|
| 382 |
+
# 连接远程设备
|
| 383 |
+
adb connect 192.168.1.100:5555
|
| 384 |
+
|
| 385 |
+
# 断开指定设备
|
| 386 |
+
adb disconnect 192.168.1.100:5555
|
| 387 |
+
|
| 388 |
+
# 指定设备执行任务
|
| 389 |
+
python main.py --device-id 192.168.1.100:5555 --base-url http://localhost:8000/v1 --model "autoglm-phone-9b" "打开抖音刷视频"
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
#### 鸿蒙设备(HDC)
|
| 393 |
+
|
| 394 |
+
```bash
|
| 395 |
+
# 列出所有已连接设备
|
| 396 |
+
hdc list targets
|
| 397 |
+
|
| 398 |
+
# 连接远程设备
|
| 399 |
+
hdc tconn 192.168.1.100:5555
|
| 400 |
+
|
| 401 |
+
# 断开指定设备
|
| 402 |
+
hdc tdisconn 192.168.1.100:5555
|
| 403 |
+
|
| 404 |
+
# 指定设备执行任务
|
| 405 |
+
python main.py --device-type hdc --device-id 192.168.1.100:5555 --base-url http://localhost:8000/v1 --model "autoglm-phone-9b" "打开抖音刷视频"
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
### Python API 远程连接
|
| 409 |
+
|
| 410 |
+
#### Android 设备(ADB)
|
| 411 |
+
|
| 412 |
+
```python
|
| 413 |
+
from phone_agent.adb import ADBConnection, list_devices
|
| 414 |
+
|
| 415 |
+
# 创建连接管理器
|
| 416 |
+
conn = ADBConnection()
|
| 417 |
+
|
| 418 |
+
# 连接远程设备
|
| 419 |
+
success, message = conn.connect("192.168.1.100:5555")
|
| 420 |
+
print(f"连接状态: {message}")
|
| 421 |
+
|
| 422 |
+
# 列出已连接设备
|
| 423 |
+
devices = list_devices()
|
| 424 |
+
for device in devices:
|
| 425 |
+
print(f"{device.device_id} - {device.connection_type.value}")
|
| 426 |
+
|
| 427 |
+
# 在 USB 设备上启用 TCP/IP
|
| 428 |
+
success, message = conn.enable_tcpip(5555)
|
| 429 |
+
ip = conn.get_device_ip()
|
| 430 |
+
print(f"设备 IP: {ip}")
|
| 431 |
+
|
| 432 |
+
# 断开连接
|
| 433 |
+
conn.disconnect("192.168.1.100:5555")
|
| 434 |
+
```
|
| 435 |
+
|
| 436 |
+
#### 鸿蒙设备(HDC)
|
| 437 |
+
|
| 438 |
+
```python
|
| 439 |
+
from phone_agent.hdc import HDCConnection, list_devices
|
| 440 |
+
|
| 441 |
+
# 创建连接管理器
|
| 442 |
+
conn = HDCConnection()
|
| 443 |
+
|
| 444 |
+
# 连接远程设备
|
| 445 |
+
success, message = conn.connect("192.168.1.100:5555")
|
| 446 |
+
print(f"连接状态: {message}")
|
| 447 |
+
|
| 448 |
+
# 列出已连接设备
|
| 449 |
+
devices = list_devices()
|
| 450 |
+
for device in devices:
|
| 451 |
+
print(f"{device.device_id} - {device.connection_type.value}")
|
| 452 |
+
|
| 453 |
+
# 断开连接
|
| 454 |
+
conn.disconnect("192.168.1.100:5555")
|
| 455 |
+
```
|
| 456 |
+
|
| 457 |
+
### 远程连接问题排查
|
| 458 |
+
|
| 459 |
+
**连接被拒绝:**
|
| 460 |
+
|
| 461 |
+
- 确保设备和电脑在同一网络
|
| 462 |
+
- 检查防火墙是否阻止 5555 端口
|
| 463 |
+
- 确认已启用 TCP/IP 模式:`adb tcpip 5555`
|
| 464 |
+
|
| 465 |
+
**连接断开:**
|
| 466 |
+
|
| 467 |
+
- WiFi 可能断开了,使用 `--connect` 重新连接
|
| 468 |
+
- 部分设备重启后会禁用 TCP/IP,需要通过 USB 重新启用
|
| 469 |
+
|
| 470 |
+
**多设备:**
|
| 471 |
+
|
| 472 |
+
- 使用 `--device-id` 指定要使用的设备
|
| 473 |
+
- 或使用 `--list-devices` 查看所有已连接设备
|
| 474 |
+
|
| 475 |
+
## 配置
|
| 476 |
+
|
| 477 |
+
### 自定义SYSTEM PROMPT
|
| 478 |
+
|
| 479 |
+
系统提供中英文两套 prompt,通过 `--lang` 参数切换:
|
| 480 |
+
|
| 481 |
+
- `--lang cn` - 中文 prompt(默认),配置文件:`phone_agent/config/prompts_zh.py`
|
| 482 |
+
- `--lang en` - 英文 prompt,配置文件:`phone_agent/config/prompts_en.py`
|
| 483 |
+
|
| 484 |
+
可以直接修改对应的配置文件来增强模型在特定领域的能力,或通过注入 app 名称禁用某些 app。
|
| 485 |
+
|
| 486 |
+
### 环境变量
|
| 487 |
+
|
| 488 |
+
| 变量 | 描述 | 默认值 |
|
| 489 |
+
|-----------------------------|------------------------|----------------------------|
|
| 490 |
+
| `PHONE_AGENT_BASE_URL` | 模型 API 地址 | `http://localhost:8000/v1` |
|
| 491 |
+
| `PHONE_AGENT_MODEL` | 模型名称 | `autoglm-phone-9b` |
|
| 492 |
+
| `PHONE_AGENT_API_KEY` | 模型认证 API Key | `EMPTY` |
|
| 493 |
+
| `PHONE_AGENT_MAX_STEPS` | 每个任务最大步数 | `100` |
|
| 494 |
+
| `PHONE_AGENT_DEVICE_ID` | ADB/HDC 设备 ID | (自动检测) |
|
| 495 |
+
| `PHONE_AGENT_DEVICE_TYPE` | 设备类型 (`adb` 或 `hdc`) | `adb` |
|
| 496 |
+
| `PHONE_AGENT_LANG` | 语言 (`cn` 或 `en`) | `cn` |
|
| 497 |
+
|
| 498 |
+
### 模型配置
|
| 499 |
+
|
| 500 |
+
```python
|
| 501 |
+
from phone_agent.model import ModelConfig
|
| 502 |
+
|
| 503 |
+
config = ModelConfig(
|
| 504 |
+
base_url="http://localhost:8000/v1",
|
| 505 |
+
api_key="EMPTY", # API 密钥(如需要)
|
| 506 |
+
model_name="autoglm-phone-9b", # 模型名称
|
| 507 |
+
max_tokens=3000, # 最大输出 token 数
|
| 508 |
+
temperature=0.1, # 采样温度
|
| 509 |
+
frequency_penalty=0.2, # 频率惩罚
|
| 510 |
+
)
|
| 511 |
+
```
|
| 512 |
+
|
| 513 |
+
### Agent 配置
|
| 514 |
+
|
| 515 |
+
```python
|
| 516 |
+
from phone_agent.agent import AgentConfig
|
| 517 |
+
|
| 518 |
+
config = AgentConfig(
|
| 519 |
+
max_steps=100, # 每个任务最大步数
|
| 520 |
+
device_id=None, # ADB 设备 ID(None 为自动检测)
|
| 521 |
+
lang="cn", # 语言选择:cn(中文)或 en(英文)
|
| 522 |
+
verbose=True, # 打印调试信息(包括思考过程和执行动作)
|
| 523 |
+
)
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
### Verbose 模式输出
|
| 527 |
+
|
| 528 |
+
当 `verbose=True` 时,Agent 会在每一步输出详细信息:
|
| 529 |
+
|
| 530 |
+
```
|
| 531 |
+
==================================================
|
| 532 |
+
💭 思考过程:
|
| 533 |
+
--------------------------------------------------
|
| 534 |
+
当前在系统桌面,需要先启动小红书应用
|
| 535 |
+
--------------------------------------------------
|
| 536 |
+
🎯 执行动作:
|
| 537 |
+
{
|
| 538 |
+
"_metadata": "do",
|
| 539 |
+
"action": "Launch",
|
| 540 |
+
"app": "小红书"
|
| 541 |
+
}
|
| 542 |
+
==================================================
|
| 543 |
+
|
| 544 |
+
... (执行动作后继续下一步)
|
| 545 |
+
|
| 546 |
+
==================================================
|
| 547 |
+
💭 思考过程:
|
| 548 |
+
--------------------------------------------------
|
| 549 |
+
小红书已打开,现在需要点击搜索框
|
| 550 |
+
--------------------------------------------------
|
| 551 |
+
🎯 执行动作:
|
| 552 |
+
{
|
| 553 |
+
"_metadata": "do",
|
| 554 |
+
"action": "Tap",
|
| 555 |
+
"element": [500, 100]
|
| 556 |
+
}
|
| 557 |
+
==================================================
|
| 558 |
+
|
| 559 |
+
🎉 ================================================
|
| 560 |
+
✅ 任务完成: 已成功搜索美食攻略
|
| 561 |
+
==================================================
|
| 562 |
+
```
|
| 563 |
+
|
| 564 |
+
这样可以清楚地看到 AI 的推理过程和每一步的具体操作。
|
| 565 |
+
|
| 566 |
+
## 支持的应用
|
| 567 |
+
|
| 568 |
+
### Android 应用
|
| 569 |
+
|
| 570 |
+
Phone Agent 支持 50+ 款主流中文应用:
|
| 571 |
+
|
| 572 |
+
| 分类 | 应用 |
|
| 573 |
+
|------|-----------------|
|
| 574 |
+
| 社交通讯 | 微信、QQ、微博 |
|
| 575 |
+
| 电商购物 | 淘宝、京东、拼多多 |
|
| 576 |
+
| 美食外卖 | 美团、饿了么、肯德基 |
|
| 577 |
+
| 出行旅游 | 携程、12306、滴滴出行 |
|
| 578 |
+
| 视频娱乐 | bilibili、抖音、爱奇艺 |
|
| 579 |
+
| 音乐音频 | 网易云音乐、QQ音乐、喜马拉雅 |
|
| 580 |
+
| 生活服务 | 大众点评、高德地图、百度地图 |
|
| 581 |
+
| 内容社区 | 小红书、知乎、豆瓣 |
|
| 582 |
+
|
| 583 |
+
运行 `python main.py --list-apps` 查看完整列表。
|
| 584 |
+
|
| 585 |
+
### 鸿蒙应用
|
| 586 |
+
|
| 587 |
+
Phone Agent 支持 60+ 款鸿蒙原生应用和系统应用:
|
| 588 |
+
|
| 589 |
+
| 分类 | 应用 |
|
| 590 |
+
|---------|------------------------------------------|
|
| 591 |
+
| 社交通讯 | 微信、QQ、微博、飞书、企业微信 |
|
| 592 |
+
| 电商购物 | 淘宝、京东、拼多多、唯品会、得物、闲鱼 |
|
| 593 |
+
| 美食外卖 | 美团、美团外卖、大众点评、海底捞 |
|
| 594 |
+
| 出行旅游 | 12306、滴滴出行、同程旅行、高德地图、百度地图 |
|
| 595 |
+
| 视频娱乐 | bilibili、抖音、快手、腾讯视频、爱奇艺、芒果TV |
|
| 596 |
+
| 音乐音频 | QQ音乐、汽水音乐、喜马拉雅 |
|
| 597 |
+
| 生活服务 | 小红书、知乎、今日头条、58同城、中国移动 |
|
| 598 |
+
| AI与工具 | 豆包、WPS、UC浏览器、扫描全能王、美图秀秀 |
|
| 599 |
+
| 系统应用 | 浏览器、日历、相机、时钟、云空间、文件管理器、相册、联系人、短信、设置等 |
|
| 600 |
+
| 华为服务 | 应用市场、音乐、视频、阅读、主题、天气 |
|
| 601 |
+
|
| 602 |
+
运行 `python main.py --device-type hdc --list-apps` 查看完整列表。
|
| 603 |
+
|
| 604 |
+
## 可用操作
|
| 605 |
+
|
| 606 |
+
Agent 可以执行以下操作:
|
| 607 |
+
|
| 608 |
+
| 操作 | 描述 |
|
| 609 |
+
|--------------|-----------------|
|
| 610 |
+
| `Launch` | 启动应用 |
|
| 611 |
+
| `Tap` | 点击指定坐标 |
|
| 612 |
+
| `Type` | 输入文本 |
|
| 613 |
+
| `Swipe` | 滑动屏幕 |
|
| 614 |
+
| `Back` | 返回上一页 |
|
| 615 |
+
| `Home` | 返回桌面 |
|
| 616 |
+
| `Long Press` | 长按 |
|
| 617 |
+
| `Double Tap` | 双击 |
|
| 618 |
+
| `Wait` | 等待页面加载 |
|
| 619 |
+
| `Take_over` | 请求人工接管(登录/验证码等) |
|
| 620 |
+
|
| 621 |
+
## 自定义回调
|
| 622 |
+
|
| 623 |
+
处理敏感操作确认和人工接管:
|
| 624 |
+
|
| 625 |
+
```python
|
| 626 |
+
def my_confirmation(message: str) -> bool:
|
| 627 |
+
"""敏感操作确认回调"""
|
| 628 |
+
return input(f"确认执行 {message}?(y/n): ").lower() == "y"
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def my_takeover(message: str) -> None:
|
| 632 |
+
"""人工接管回调"""
|
| 633 |
+
print(f"请手动完成: {message}")
|
| 634 |
+
input("完成后按回车继续...")
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
agent = PhoneAgent(
|
| 638 |
+
confirmation_callback=my_confirmation,
|
| 639 |
+
takeover_callback=my_takeover,
|
| 640 |
+
)
|
| 641 |
+
```
|
| 642 |
+
|
| 643 |
+
## 示例
|
| 644 |
+
|
| 645 |
+
查看 `examples/` 目录获取更多使用示例:
|
| 646 |
+
|
| 647 |
+
- `basic_usage.py` - 基础任务执行
|
| 648 |
+
- 单步调试模式
|
| 649 |
+
- 批量任务执行
|
| 650 |
+
- 自定义回调
|
| 651 |
+
|
| 652 |
+
## 二次开发
|
| 653 |
+
|
| 654 |
+
### 配置开发环境
|
| 655 |
+
|
| 656 |
+
二次开发需要使用开发依赖:
|
| 657 |
+
|
| 658 |
+
```bash
|
| 659 |
+
pip install -e ".[dev]"
|
| 660 |
+
```
|
| 661 |
+
|
| 662 |
+
### 运行测试
|
| 663 |
+
|
| 664 |
+
```bash
|
| 665 |
+
pytest tests/
|
| 666 |
+
```
|
| 667 |
+
|
| 668 |
+
### 完整项目结构
|
| 669 |
+
|
| 670 |
+
```
|
| 671 |
+
phone_agent/
|
| 672 |
+
├── __init__.py # 包导出
|
| 673 |
+
├── agent.py # PhoneAgent 主类
|
| 674 |
+
├── adb/ # ADB 工具
|
| 675 |
+
│ ├── connection.py # 远程/本地连接管理
|
| 676 |
+
│ ├── screenshot.py # 屏幕截图
|
| 677 |
+
│ ├── input.py # 文本输入 (ADB Keyboard)
|
| 678 |
+
│ └── device.py # 设备控制 (点击、滑动等)
|
| 679 |
+
├── actions/ # 操作处理
|
| 680 |
+
│ └── handler.py # 操作执行器
|
| 681 |
+
├── config/ # 配置
|
| 682 |
+
│ ├── apps.py # 支持的应用映射
|
| 683 |
+
│ ├── prompts_zh.py # 中文系统提示词
|
| 684 |
+
│ └── prompts_en.py # 英文系统提示词
|
| 685 |
+
└── model/ # AI 模型客户端
|
| 686 |
+
└── client.py # OpenAI 兼容客户端
|
| 687 |
+
```
|
| 688 |
+
|
| 689 |
+
## 常见问题
|
| 690 |
+
|
| 691 |
+
我们列举了一些常见的问题,以及对应的解决方案:
|
| 692 |
+
|
| 693 |
+
### 设备未找到
|
| 694 |
+
|
| 695 |
+
尝试通过重启 ADB 服务来解决:
|
| 696 |
+
|
| 697 |
+
```bash
|
| 698 |
+
adb kill-server
|
| 699 |
+
adb start-server
|
| 700 |
+
adb devices
|
| 701 |
+
```
|
| 702 |
+
|
| 703 |
+
如果仍然无法识别,请检查:
|
| 704 |
+
|
| 705 |
+
1. USB 调试是否已开启
|
| 706 |
+
2. 数据线是否支持数据传输(部分数据线仅支持充电)
|
| 707 |
+
3. 手机上弹出的授权框是否已点击「允许」
|
| 708 |
+
4. 尝试更换 USB 接口或数据线
|
| 709 |
+
|
| 710 |
+
### 能打开应用,但无法点击
|
| 711 |
+
|
| 712 |
+
部分机型需要同时开启两个调试选项才能正常使用:
|
| 713 |
+
|
| 714 |
+
- **USB 调试**
|
| 715 |
+
- **USB 调试(安全设置)**
|
| 716 |
+
|
| 717 |
+
请在 `设置 → 开发者选项` 中检查这两个选项是否都已启用。
|
| 718 |
+
|
| 719 |
+
### 文本输入不工作
|
| 720 |
+
|
| 721 |
+
1. 确保设备已安装 ADB Keyboard
|
| 722 |
+
2. 在设置 > 系统 > 语言和输入法 > 虚拟键盘 中启用
|
| 723 |
+
3. Agent 会在需要输入时自动切换到 ADB Keyboard
|
| 724 |
+
|
| 725 |
+
### 截图失败(黑屏)
|
| 726 |
+
|
| 727 |
+
这通常意味着应用正在显示敏感页面(支付、密码、银行类应用)。Agent 会自动检测并请求人工接管。
|
| 728 |
+
|
| 729 |
+
### windows 编码异常问题
|
| 730 |
+
|
| 731 |
+
报错信息形如 `UnicodeEncodeError gbk code`
|
| 732 |
+
|
| 733 |
+
解决办法: 在运行代码的命令前面加上环境变量: `PYTHONIOENCODING=utf-8`
|
| 734 |
+
|
| 735 |
+
### 交互模式非TTY环境无法使用
|
| 736 |
+
|
| 737 |
+
报错形如: `EOF when reading a line`
|
| 738 |
+
|
| 739 |
+
解决办法: 使用非交互模式直接指定任务, 或者切换到 TTY 模式的终端应用.
|
| 740 |
+
|
| 741 |
+
### 引用
|
| 742 |
+
|
| 743 |
+
如果你觉得我们的工作有帮助,请引用以下论文:
|
| 744 |
+
|
| 745 |
+
```bibtex
|
| 746 |
+
@article{liu2024autoglm,
|
| 747 |
+
title={Autoglm: Autonomous foundation agents for guis},
|
| 748 |
+
author={Liu, Xiao and Qin, Bo and Liang, Dongzhu and Dong, Guang and Lai, Hanyu and Zhang, Hanchen and Zhao, Hanlin and Iong, Iat Long and Sun, Jiadai and Wang, Jiaqi and others},
|
| 749 |
+
journal={arXiv preprint arXiv:2411.00820},
|
| 750 |
+
year={2024}
|
| 751 |
+
}
|
| 752 |
+
@article{xu2025mobilerl,
|
| 753 |
+
title={MobileRL: Online Agentic Reinforcement Learning for Mobile GUI Agents},
|
| 754 |
+
author={Xu, Yifan and Liu, Xiao and Liu, Xinghan and Fu, Jiaqi and Zhang, Hanchen and Jing, Bohao and Zhang, Shudan and Wang, Yuting and Zhao, Wenyi and Dong, Yuxiao},
|
| 755 |
+
journal={arXiv preprint arXiv:2509.18119},
|
| 756 |
+
year={2025}
|
| 757 |
+
}
|
| 758 |
+
```
|
| 759 |
+
|
| 760 |
+
---
|
| 761 |
+
|
| 762 |
+
## 自动化部署指南(面向 AI)
|
| 763 |
+
|
| 764 |
+
> **本章节专为 AI 助手(如 Claude Code)设计,用于自动化部署 Open-AutoGLM。**
|
| 765 |
+
>
|
| 766 |
+
> 如果你是人类读者,可以跳过本章节,按照上面的文档操作即可。
|
| 767 |
+
|
| 768 |
+
---
|
| 769 |
+
|
| 770 |
+
### 项目概述
|
| 771 |
+
|
| 772 |
+
Open-AutoGLM 是一个手机 Agent 框架:
|
| 773 |
+
- **输入**:用户的自然语言指令(如"打开微信发消息给张三")
|
| 774 |
+
- **输出**:自动操作用户的安卓手机完成任务
|
| 775 |
+
- **原理**:截图 → 视觉模型理解界面 → 输出点击坐标 → ADB 执行操作 → 循环
|
| 776 |
+
|
| 777 |
+
架构分为两部分:
|
| 778 |
+
1. **Agent 代码**(本仓库):运行在用户电脑上,负责调用模型、解析动作、控制手机
|
| 779 |
+
2. **视觉模型服务**:可以是远程 API,也可以本地部署
|
| 780 |
+
|
| 781 |
+
---
|
| 782 |
+
|
| 783 |
+
### 部署前置检查
|
| 784 |
+
|
| 785 |
+
在开始部署前,请逐项向用户确认以下内容:
|
| 786 |
+
|
| 787 |
+
#### 硬件环境
|
| 788 |
+
- [ ] 用户有一台安卓手机(Android 7.0+)
|
| 789 |
+
- [ ] 用户有一根支持数据传输的 USB 数据线(不是仅充电线)
|
| 790 |
+
- [ ] 手机和电脑可以通过数据线连接
|
| 791 |
+
|
| 792 |
+
#### 手机端配置
|
| 793 |
+
- [ ] 手机已开启「开发者模式」(设置 → 关于手机 → 连续点击版本号 7 次)
|
| 794 |
+
- [ ] 手机已开启「USB 调试」(设置 → 开发者选项 → USB 调试)
|
| 795 |
+
- [ ] 部分机型需要同时开启「USB 调试(安全设置)」
|
| 796 |
+
- [ ] 手机已安装 ADB Keyboard 应用(下载地址:https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk)
|
| 797 |
+
- [ ] ADB Keyboard 已在系统设置中启用(设置 → 语言和输入法 → 启用 ADB Keyboard)
|
| 798 |
+
|
| 799 |
+
#### 模型服务确认(二选一)
|
| 800 |
+
|
| 801 |
+
**请明确询问用户:你是否已有可用的 AutoGLM 模型服务?**
|
| 802 |
+
|
| 803 |
+
- **选项 A:使用已部署的模型服务(推荐)**
|
| 804 |
+
- 用户提供模型服务的 URL(如 `http://xxx.xxx.xxx.xxx:8000/v1`)
|
| 805 |
+
- 无需本地 GPU,无需下载模型
|
| 806 |
+
- 直接使用该 URL 作为 `--base-url` 参数
|
| 807 |
+
|
| 808 |
+
- **选项 B:本地部署模型(高配置要求)**
|
| 809 |
+
- 需要 NVIDIA GPU(建议 24GB+ 显存)
|
| 810 |
+
- 需要安装 vLLM 或 SGLang
|
| 811 |
+
- 需要下载约 20GB 的模型文件
|
| 812 |
+
- **如果用户是新手或不确定,强烈建议选择选项 A**
|
| 813 |
+
|
| 814 |
+
---
|
| 815 |
+
|
| 816 |
+
### 部署流程
|
| 817 |
+
|
| 818 |
+
#### 阶段一:环境准备
|
| 819 |
+
|
| 820 |
+
```bash
|
| 821 |
+
# 1. 安装 ADB 工具
|
| 822 |
+
# MacOS:
|
| 823 |
+
brew install android-platform-tools
|
| 824 |
+
# 或手动下载:https://developer.android.com/tools/releases/platform-tools
|
| 825 |
+
|
| 826 |
+
# Windows: 下载后解压,添加到 PATH 环境变量
|
| 827 |
+
|
| 828 |
+
# 2. 验证 ADB 安装
|
| 829 |
+
adb version
|
| 830 |
+
# 应输出版本信息
|
| 831 |
+
|
| 832 |
+
# 3. 连接手机并验证
|
| 833 |
+
# 用数据线连接手机,手机上点击「允许 USB 调试」
|
| 834 |
+
adb devices
|
| 835 |
+
# 应输出设备列表,如:
|
| 836 |
+
# List of devices attached
|
| 837 |
+
# XXXXXXXX device
|
| 838 |
+
```
|
| 839 |
+
|
| 840 |
+
**如果 `adb devices` 显示空列表或 unauthorized:**
|
| 841 |
+
1. 检查手机上是否弹出授权框,点击「允许」
|
| 842 |
+
2. 检查 USB 调试是否开启
|
| 843 |
+
3. 尝试更换数据线或 USB 接口
|
| 844 |
+
4. 执行 `adb kill-server && adb start-server` 后重试
|
| 845 |
+
|
| 846 |
+
#### 阶段二:安装 Agent
|
| 847 |
+
|
| 848 |
+
```bash
|
| 849 |
+
# 1. 克隆仓库(如果还没有克隆)
|
| 850 |
+
git clone https://github.com/zai-org/Open-AutoGLM.git
|
| 851 |
+
cd Open-AutoGLM
|
| 852 |
+
|
| 853 |
+
# 2. 创建虚拟环境(推荐)
|
| 854 |
+
python -m venv venv
|
| 855 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 856 |
+
|
| 857 |
+
# 3. 安装依赖
|
| 858 |
+
pip install -r requirements.txt
|
| 859 |
+
pip install -e .
|
| 860 |
+
```
|
| 861 |
+
|
| 862 |
+
**注意:不需要 clone 模型仓库,模型通过 API 调用。**
|
| 863 |
+
|
| 864 |
+
#### 阶段三:配置模型服务
|
| 865 |
+
|
| 866 |
+
**如果用户选择选项 A(使用已部署的模型):**
|
| 867 |
+
|
| 868 |
+
你可以使用以下第三方模型服务:
|
| 869 |
+
|
| 870 |
+
1. **智谱 BigModel**
|
| 871 |
+
- 文档:https://docs.bigmodel.cn/cn/api/introduction
|
| 872 |
+
- `--base-url`:`https://open.bigmodel.cn/api/paas/v4`
|
| 873 |
+
- `--model`:`autoglm-phone`
|
| 874 |
+
- `--apikey`:在智谱平台申请你的 API Key
|
| 875 |
+
|
| 876 |
+
2. **ModelScope(魔搭社区)**
|
| 877 |
+
- 文档:https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B
|
| 878 |
+
- `--base-url`:`https://api-inference.modelscope.cn/v1`
|
| 879 |
+
- `--model`:`ZhipuAI/AutoGLM-Phone-9B`
|
| 880 |
+
- `--apikey`:在 ModelScope 平台申请你的 API Key
|
| 881 |
+
|
| 882 |
+
使用示例:
|
| 883 |
+
|
| 884 |
+
```bash
|
| 885 |
+
# 使用智谱 BigModel
|
| 886 |
+
python main.py --base-url https://open.bigmodel.cn/api/paas/v4 --model "autoglm-phone" --apikey "your-bigmodel-api-key" "打开美团搜索附近的火锅店"
|
| 887 |
+
|
| 888 |
+
# 使用 ModelScope
|
| 889 |
+
python main.py --base-url https://api-inference.modelscope.cn/v1 --model "ZhipuAI/AutoGLM-Phone-9B" --apikey "your-modelscope-api-key" "打开美团搜索附近的火锅店"
|
| 890 |
+
```
|
| 891 |
+
|
| 892 |
+
或者直接使用用户提供的其他模型服务 URL,跳过本地模型部署步骤。
|
| 893 |
+
|
| 894 |
+
**如果用户选择选项 B(本地部署模型):**
|
| 895 |
+
|
| 896 |
+
```bash
|
| 897 |
+
# 1. 安装 vLLM
|
| 898 |
+
pip install vllm
|
| 899 |
+
|
| 900 |
+
# 2. 启动模型服务(会自动下载模型,约 20GB)
|
| 901 |
+
python3 -m vllm.entrypoints.openai.api_server \
|
| 902 |
+
--served-model-name autoglm-phone-9b \
|
| 903 |
+
--allowed-local-media-path / \
|
| 904 |
+
--mm-encoder-tp-mode data \
|
| 905 |
+
--mm_processor_cache_type shm \
|
| 906 |
+
--mm_processor_kwargs "{\"max_pixels\":5000000}" \
|
| 907 |
+
--max-model-len 25480 \
|
| 908 |
+
--chat-template-content-format string \
|
| 909 |
+
--limit-mm-per-prompt "{\"image\":10}" \
|
| 910 |
+
--model zai-org/AutoGLM-Phone-9B \
|
| 911 |
+
--port 8000
|
| 912 |
+
|
| 913 |
+
# 模型服务 URL 为:http://localhost:8000/v1
|
| 914 |
+
```
|
| 915 |
+
|
| 916 |
+
#### 阶段四:验证部署
|
| 917 |
+
|
| 918 |
+
```bash
|
| 919 |
+
# 在 Open-AutoGLM 目录下执行
|
| 920 |
+
# 将 {MODEL_URL} 替换为实际的模型服务地址
|
| 921 |
+
|
| 922 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b" "打开微信,对文件传输助手发送消息:部署成功"
|
| 923 |
+
```
|
| 924 |
+
|
| 925 |
+
**预期结果:**
|
| 926 |
+
- 手机自动打开微信
|
| 927 |
+
- 自动搜索「文件传输助手」
|
| 928 |
+
- 自动发送消息「部署成功」
|
| 929 |
+
|
| 930 |
+
---
|
| 931 |
+
|
| 932 |
+
### 异常处理
|
| 933 |
+
|
| 934 |
+
| 错误现象 | 可能原因 | 解决方案 |
|
| 935 |
+
|---------|---------|---------|
|
| 936 |
+
| `adb devices` 无输出 | USB 调试未开启或数据线问题 | 检查开发者选项,更换数据线 |
|
| 937 |
+
| `adb devices` 显示 unauthorized | 手机未授权 | 手机上点击「允许 USB 调试」|
|
| 938 |
+
| 能打开应用但无法点击 | 缺少安全调试权限 | 开启「USB 调试(安全设置)」|
|
| 939 |
+
| 中文输入变成乱码或无输入 | ADB Keyboard 未启用 | 在系统设置中启用 ADB Keyboard |
|
| 940 |
+
| 截图返回黑屏 | 敏感页面(支付/银行) | 正常现象,系统会自动处理 |
|
| 941 |
+
| 连接模型服务失败 | URL 错误或服务未启动 | 检查 URL,确认服务正在运行 |
|
| 942 |
+
| `ModuleNotFoundError` | 依赖未安装 | 执行 `pip install -r requirements.txt` |
|
| 943 |
+
|
| 944 |
+
---
|
| 945 |
+
|
| 946 |
+
### 部署要点
|
| 947 |
+
|
| 948 |
+
1. **优先确认手机连接**:在安装任何代码之前,先确保 `adb devices` 能看到设备
|
| 949 |
+
2. **不要跳过 ADB Keyboard**:没有它,中文输入会失败
|
| 950 |
+
3. **模型服务是外部依赖**:Agent 代码本身不包含模型,需要单独的模型服务
|
| 951 |
+
4. **遇到权限问题先检查手机设置**:大部分问题都是手机端配置不完整
|
| 952 |
+
5. **部署完成后用简单任务测试**:建议用「打开微信发消息给文件传输助手」作为验收标准
|
| 953 |
+
|
| 954 |
+
---
|
| 955 |
+
|
| 956 |
+
### 命令速查
|
| 957 |
+
|
| 958 |
+
```bash
|
| 959 |
+
# 检查 ADB 连接
|
| 960 |
+
adb devices
|
| 961 |
+
|
| 962 |
+
# 重启 ADB 服务
|
| 963 |
+
adb kill-server && adb start-server
|
| 964 |
+
|
| 965 |
+
# 安装依赖
|
| 966 |
+
pip install -r requirements.txt && pip install -e .
|
| 967 |
+
|
| 968 |
+
# 运行 Agent(交互模式)
|
| 969 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b"
|
| 970 |
+
|
| 971 |
+
# 运行 Agent(单次任务)
|
| 972 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b" "你的任务描述"
|
| 973 |
+
|
| 974 |
+
# 查看支持的应用列表
|
| 975 |
+
python main.py --list-apps
|
| 976 |
+
```
|
| 977 |
+
|
| 978 |
+
---
|
| 979 |
+
|
| 980 |
+
**部署完成的标志:手机能自动执行用户的自然语言指令。**
|
README_coding_agent.md
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-AutoGLM Quick Start for Coding Agent
|
| 2 |
+
|
| 3 |
+
<div align="center">
|
| 4 |
+
<img src=resources/logo.svg width="20%"/>
|
| 5 |
+
</div>
|
| 6 |
+
|
| 7 |
+
> **本文专为 AI 助手(如 Claude Code)阅读,用于自动化部署 Open-AutoGLM。**
|
| 8 |
+
>
|
| 9 |
+
> **This document is designed for AI assistants (such as Claude Code) to automate the deployment of Open-AutoGLM.**
|
| 10 |
+
>
|
| 11 |
+
> 如果你是人类读者,可以跳过本文,按照 README.md 文档操作即可。
|
| 12 |
+
>
|
| 13 |
+
> If you are a human reader, you can skip this document and follow the README.md instructions instead.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Table of Contents / 目录
|
| 18 |
+
|
| 19 |
+
- [English](#english)
|
| 20 |
+
- [中文](#中文)
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
# English
|
| 25 |
+
|
| 26 |
+
## Prerequisites
|
| 27 |
+
|
| 28 |
+
### 1. Python Environment
|
| 29 |
+
|
| 30 |
+
Python 3.10 or higher is required.
|
| 31 |
+
|
| 32 |
+
### 2. ADB (Android Debug Bridge)
|
| 33 |
+
|
| 34 |
+
1. Download the official ADB [installation package](https://developer.android.com/tools/releases/platform-tools)
|
| 35 |
+
2. Extract and configure environment variables:
|
| 36 |
+
|
| 37 |
+
**macOS:**
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# Assuming extracted to ~/Downloads/platform-tools
|
| 41 |
+
export PATH=${PATH}:~/Downloads/platform-tools
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
**Windows:** Add the extracted folder path to your system PATH. Refer to [this tutorial](https://blog.csdn.net/x2584179909/article/details/108319973) if needed.
|
| 45 |
+
|
| 46 |
+
### 3. Android Device Setup
|
| 47 |
+
|
| 48 |
+
Requirements:
|
| 49 |
+
- Android 7.0+ device or emulator
|
| 50 |
+
- Developer Mode enabled
|
| 51 |
+
- USB Debugging enabled
|
| 52 |
+
|
| 53 |
+
**Enable Developer Mode:**
|
| 54 |
+
1. Go to `Settings > About Phone > Build Number`
|
| 55 |
+
2. Tap rapidly about 10 times until "Developer mode enabled" appears
|
| 56 |
+
|
| 57 |
+
**Enable USB Debugging:**
|
| 58 |
+
1. Go to `Settings > Developer Options > USB Debugging`
|
| 59 |
+
2. Enable the toggle
|
| 60 |
+
3. Some devices may require a restart
|
| 61 |
+
|
| 62 |
+
**Important permissions to check:**
|
| 63 |
+
|
| 64 |
+

|
| 65 |
+
|
| 66 |
+
### 4. Install ADB Keyboard
|
| 67 |
+
|
| 68 |
+
Download and install [ADB Keyboard APK](https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk) on your device.
|
| 69 |
+
|
| 70 |
+
After installation, enable it in `Settings > Input Method` or `Settings > Keyboard List`.
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Installation
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Install dependencies
|
| 78 |
+
pip install -r requirements.txt
|
| 79 |
+
|
| 80 |
+
# Install package
|
| 81 |
+
pip install -e .
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## ADB Configuration
|
| 87 |
+
|
| 88 |
+
**Ensure your USB cable supports data transfer (not charging only).**
|
| 89 |
+
|
| 90 |
+
### Verify Connection
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
# Check connected devices
|
| 94 |
+
adb devices
|
| 95 |
+
|
| 96 |
+
# Expected output:
|
| 97 |
+
# List of devices attached
|
| 98 |
+
# emulator-5554 device
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### Remote Debugging (WiFi)
|
| 102 |
+
|
| 103 |
+
Ensure your phone and computer are on the same WiFi network.
|
| 104 |
+
|
| 105 |
+

|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
# Connect via WiFi (replace with your phone's IP and port)
|
| 109 |
+
adb connect 192.168.1.100:5555
|
| 110 |
+
|
| 111 |
+
# Verify connection
|
| 112 |
+
adb devices
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### Device Management
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
# List all devices
|
| 119 |
+
adb devices
|
| 120 |
+
|
| 121 |
+
# Connect remote device
|
| 122 |
+
adb connect <ip>:<port>
|
| 123 |
+
|
| 124 |
+
# Disconnect device
|
| 125 |
+
adb disconnect <ip>:<port>
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## Usage
|
| 131 |
+
|
| 132 |
+
### Command Line
|
| 133 |
+
|
| 134 |
+
```bash
|
| 135 |
+
# Interactive mode
|
| 136 |
+
python main.py --base-url <MODEL_API_URL> --model <MODEL_NAME>
|
| 137 |
+
|
| 138 |
+
# Execute specific task
|
| 139 |
+
python main.py --base-url <MODEL_API_URL> "Open Chrome browser"
|
| 140 |
+
|
| 141 |
+
# Use API key authentication
|
| 142 |
+
python main.py --apikey sk-xxxxx
|
| 143 |
+
|
| 144 |
+
# English system prompt
|
| 145 |
+
python main.py --lang en --base-url <MODEL_API_URL> "Open Chrome browser"
|
| 146 |
+
|
| 147 |
+
# List supported apps
|
| 148 |
+
python main.py --list-apps
|
| 149 |
+
|
| 150 |
+
# Specify device
|
| 151 |
+
python main.py --device-id 192.168.1.100:5555 --base-url <MODEL_API_URL> "Open TikTok"
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### Python API
|
| 155 |
+
|
| 156 |
+
```python
|
| 157 |
+
from phone_agent import PhoneAgent
|
| 158 |
+
from phone_agent.model import ModelConfig
|
| 159 |
+
|
| 160 |
+
# Configure model
|
| 161 |
+
model_config = ModelConfig(
|
| 162 |
+
base_url="<MODEL_API_URL>",
|
| 163 |
+
model_name="<MODEL_NAME>",
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
# Create Agent
|
| 167 |
+
agent = PhoneAgent(model_config=model_config)
|
| 168 |
+
|
| 169 |
+
# Execute task
|
| 170 |
+
result = agent.run("Open eBay and search for wireless earbuds")
|
| 171 |
+
print(result)
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
## Environment Variables
|
| 177 |
+
|
| 178 |
+
| Variable | Description | Default |
|
| 179 |
+
|---------------------------|---------------------------|------------------------------|
|
| 180 |
+
| `PHONE_AGENT_BASE_URL` | Model API URL | `http://localhost:8000/v1` |
|
| 181 |
+
| `PHONE_AGENT_MODEL` | Model name | `autoglm-phone-9b` |
|
| 182 |
+
| `PHONE_AGENT_API_KEY` | API key | `EMPTY` |
|
| 183 |
+
| `PHONE_AGENT_MAX_STEPS` | Max steps per task | `100` |
|
| 184 |
+
| `PHONE_AGENT_DEVICE_ID` | ADB device ID | (auto-detect) |
|
| 185 |
+
| `PHONE_AGENT_LANG` | Language (`cn`/`en`) | `cn` |
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
## Troubleshooting
|
| 190 |
+
|
| 191 |
+
### Device Not Found
|
| 192 |
+
|
| 193 |
+
```bash
|
| 194 |
+
adb kill-server
|
| 195 |
+
adb start-server
|
| 196 |
+
adb devices
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
Check:
|
| 200 |
+
1. USB debugging enabled
|
| 201 |
+
2. USB cable supports data transfer
|
| 202 |
+
3. Authorization popup approved on phone
|
| 203 |
+
4. Try different USB port/cable
|
| 204 |
+
|
| 205 |
+
### Can Open Apps but Cannot Tap
|
| 206 |
+
|
| 207 |
+
Enable both in `Settings > Developer Options`:
|
| 208 |
+
- **USB Debugging**
|
| 209 |
+
- **USB Debugging (Security Settings)**
|
| 210 |
+
|
| 211 |
+
### Text Input Not Working
|
| 212 |
+
|
| 213 |
+
1. Ensure ADB Keyboard is installed
|
| 214 |
+
2. Enable in `Settings > System > Language & Input > Virtual Keyboard`
|
| 215 |
+
|
| 216 |
+
### Windows Encoding Issues
|
| 217 |
+
|
| 218 |
+
Add environment variable before running:
|
| 219 |
+
|
| 220 |
+
```bash
|
| 221 |
+
PYTHONIOENCODING=utf-8 python main.py ...
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
---
|
| 225 |
+
|
| 226 |
+
# 中文
|
| 227 |
+
|
| 228 |
+
## 环境要求
|
| 229 |
+
|
| 230 |
+
### 1. Python 环境
|
| 231 |
+
|
| 232 |
+
需要 Python 3.10 及以上版本。
|
| 233 |
+
|
| 234 |
+
### 2. ADB (Android Debug Bridge)
|
| 235 |
+
|
| 236 |
+
1. 下载官方 ADB [安装包](https://developer.android.com/tools/releases/platform-tools?hl=zh-cn)
|
| 237 |
+
2. 解压并配置环境变量:
|
| 238 |
+
|
| 239 |
+
**macOS:**
|
| 240 |
+
|
| 241 |
+
```bash
|
| 242 |
+
# 假设解压到 ~/Downloads/platform-tools
|
| 243 |
+
export PATH=${PATH}:~/Downloads/platform-tools
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
**Windows:** 将解压后的文件夹路径添加到系统 PATH。可参考[此教程](https://blog.csdn.net/x2584179909/article/details/108319973)。
|
| 247 |
+
|
| 248 |
+
### 3. 安卓设备配置
|
| 249 |
+
|
| 250 |
+
要求:
|
| 251 |
+
- Android 7.0+ 设备或模拟器
|
| 252 |
+
- 开发者模式已启用
|
| 253 |
+
- USB 调试已启用
|
| 254 |
+
|
| 255 |
+
**启用开发者模式:**
|
| 256 |
+
1. 进入 `设置 > 关于手机 > 版本号`
|
| 257 |
+
2. 连续快速点击约 10 次,直到提示"开发者模式已启用"
|
| 258 |
+
|
| 259 |
+
**启用 USB 调试:**
|
| 260 |
+
1. 进入 `设置 > 开发者选项 > USB 调试`
|
| 261 |
+
2. 开启开关
|
| 262 |
+
3. 部分设备可能需要重启
|
| 263 |
+
|
| 264 |
+
**请务必检查以下权限:**
|
| 265 |
+
|
| 266 |
+

|
| 267 |
+
|
| 268 |
+
### 4. 安装 ADB Keyboard
|
| 269 |
+
|
| 270 |
+
在设备上下载并安装 [ADB Keyboard APK](https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk)。
|
| 271 |
+
|
| 272 |
+
安装后,在 `设置 > 输入法` 或 `设置 > 键盘列表` 中启用。
|
| 273 |
+
|
| 274 |
+
---
|
| 275 |
+
|
| 276 |
+
## 安装
|
| 277 |
+
|
| 278 |
+
```bash
|
| 279 |
+
# 安装依赖
|
| 280 |
+
pip install -r requirements.txt
|
| 281 |
+
|
| 282 |
+
# 安装包
|
| 283 |
+
pip install -e .
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
---
|
| 287 |
+
|
| 288 |
+
## ADB 配置
|
| 289 |
+
|
| 290 |
+
**请确保 USB 数据线支持数据传输(而非仅充电)。**
|
| 291 |
+
|
| 292 |
+
### 验证连接
|
| 293 |
+
|
| 294 |
+
```bash
|
| 295 |
+
# 检查已连接设备
|
| 296 |
+
adb devices
|
| 297 |
+
|
| 298 |
+
# 预期输出:
|
| 299 |
+
# List of devices attached
|
| 300 |
+
# emulator-5554 device
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
### 远程调试(WiFi)
|
| 304 |
+
|
| 305 |
+
确保手机和电脑在同一 WiFi 网络中。
|
| 306 |
+
|
| 307 |
+

|
| 308 |
+
|
| 309 |
+
```bash
|
| 310 |
+
# 通过 WiFi 连接(替换为手机显示的 IP 和端口)
|
| 311 |
+
adb connect 192.168.1.100:5555
|
| 312 |
+
|
| 313 |
+
# 验证连接
|
| 314 |
+
adb devices
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
### 设备管理
|
| 318 |
+
|
| 319 |
+
```bash
|
| 320 |
+
# 列出所有设备
|
| 321 |
+
adb devices
|
| 322 |
+
|
| 323 |
+
# 连接远程设备
|
| 324 |
+
adb connect <ip>:<port>
|
| 325 |
+
|
| 326 |
+
# 断开设备
|
| 327 |
+
adb disconnect <ip>:<port>
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
---
|
| 331 |
+
|
| 332 |
+
## 使用方法
|
| 333 |
+
|
| 334 |
+
### 命令行
|
| 335 |
+
|
| 336 |
+
```bash
|
| 337 |
+
# 交互模式
|
| 338 |
+
python main.py --base-url <模型API地址> --model <模型名称>
|
| 339 |
+
|
| 340 |
+
# 执行指定任务
|
| 341 |
+
python main.py --base-url <模型API地址> "打开美团搜索附近的火锅店"
|
| 342 |
+
|
| 343 |
+
# 使用 API Key 认证
|
| 344 |
+
python main.py --apikey sk-xxxxx
|
| 345 |
+
|
| 346 |
+
# 使用英文系统提示词
|
| 347 |
+
python main.py --lang en --base-url <模型API地址> "Open Chrome browser"
|
| 348 |
+
|
| 349 |
+
# 列出支持的应用
|
| 350 |
+
python main.py --list-apps
|
| 351 |
+
|
| 352 |
+
# 指定设备
|
| 353 |
+
python main.py --device-id 192.168.1.100:5555 --base-url <模型API地址> "打开抖音刷视频"
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
### Python API
|
| 357 |
+
|
| 358 |
+
```python
|
| 359 |
+
from phone_agent import PhoneAgent
|
| 360 |
+
from phone_agent.model import ModelConfig
|
| 361 |
+
|
| 362 |
+
# 配置模型
|
| 363 |
+
model_config = ModelConfig(
|
| 364 |
+
base_url="<模型API地址>",
|
| 365 |
+
model_name="<模型名称>",
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
# 创建 Agent
|
| 369 |
+
agent = PhoneAgent(model_config=model_config)
|
| 370 |
+
|
| 371 |
+
# 执行任务
|
| 372 |
+
result = agent.run("打开淘宝搜索无线耳机")
|
| 373 |
+
print(result)
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
---
|
| 377 |
+
|
| 378 |
+
## 环境变量
|
| 379 |
+
|
| 380 |
+
| 变量 | 描述 | 默认值 |
|
| 381 |
+
|---------------------------|------------------|----------------------------|
|
| 382 |
+
| `PHONE_AGENT_BASE_URL` | 模型 API 地址 | `http://localhost:8000/v1` |
|
| 383 |
+
| `PHONE_AGENT_MODEL` | 模型名称 | `autoglm-phone-9b` |
|
| 384 |
+
| `PHONE_AGENT_API_KEY` | API Key | `EMPTY` |
|
| 385 |
+
| `PHONE_AGENT_MAX_STEPS` | 每个任务最大步数 | `100` |
|
| 386 |
+
| `PHONE_AGENT_DEVICE_ID` | ADB 设备 ID | (自动检测) |
|
| 387 |
+
| `PHONE_AGENT_LANG` | 语言 (`cn`/`en`) | `cn` |
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
## 常见问题
|
| 392 |
+
|
| 393 |
+
### 设备未找到
|
| 394 |
+
|
| 395 |
+
```bash
|
| 396 |
+
adb kill-server
|
| 397 |
+
adb start-server
|
| 398 |
+
adb devices
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
检查:
|
| 402 |
+
1. USB 调试是否已开启
|
| 403 |
+
2. 数据线是否支持数据传输
|
| 404 |
+
3. 手机上的授权弹窗是否已点击「允许」
|
| 405 |
+
4. 尝试更换 USB 接口或数据线
|
| 406 |
+
|
| 407 |
+
### 能打开应用但无法点击
|
| 408 |
+
|
| 409 |
+
在 `设置 > 开发者选项` 中同时启用:
|
| 410 |
+
- **USB 调试**
|
| 411 |
+
- **USB 调试(安全设置)**
|
| 412 |
+
|
| 413 |
+
### 文本输入不工作
|
| 414 |
+
|
| 415 |
+
1. 确保已安装 ADB Keyboard
|
| 416 |
+
2. 在 `设置 > 系统 > 语言和输入法 > 虚拟键盘` 中启用
|
| 417 |
+
|
| 418 |
+
### Windows 编码异常
|
| 419 |
+
|
| 420 |
+
运行代码前添加环境变量:
|
| 421 |
+
|
| 422 |
+
```bash
|
| 423 |
+
PYTHONIOENCODING=utf-8 python main.py ...
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
---
|
| 427 |
+
|
| 428 |
+
## License
|
| 429 |
+
|
| 430 |
+
This project is for research and learning purposes only. See [Terms of Use](resources/privacy_policy.txt) / [使用条款](resources/privacy_policy.txt).
|
README_en.md
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Open-AutoGLM
|
| 2 |
+
|
| 3 |
+
[中文阅读.](./README.md)
|
| 4 |
+
|
| 5 |
+
<div align="center">
|
| 6 |
+
<img src=resources/logo.svg width="20%"/>
|
| 7 |
+
</div>
|
| 8 |
+
<p align="center">
|
| 9 |
+
👋 Join our <a href="resources/WECHAT.md" target="_blank">WeChat</a> or <a href="https://discord.gg/QR7SARHRxK" target="_blank">Discord</a> communities
|
| 10 |
+
</p>
|
| 11 |
+
|
| 12 |
+
## Quick Start
|
| 13 |
+
|
| 14 |
+
You can use Claude Code with [GLM Coding Plan](https://z.ai/subscribe) and enter the following prompt to quickly deploy this project:
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
Access the documentation and install AutoGLM for me
|
| 18 |
+
https://raw.githubusercontent.com/zai-org/Open-AutoGLM/refs/heads/main/README_en.md
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
## Project Introduction
|
| 22 |
+
|
| 23 |
+
Phone Agent is a mobile intelligent assistant framework built on AutoGLM. It understands phone screen content in a multimodal manner and helps users complete tasks through automated operations. The system controls devices via ADB (Android Debug Bridge), perceives screens using vision-language models, and generates and executes operation workflows through intelligent planning. Users simply describe their needs in natural language, such as "Open eBay and search for wireless earphones." and Phone Agent will automatically parse the intent, understand the current interface, plan the next action, and complete the entire workflow. The system also includes a sensitive operation confirmation mechanism and supports manual takeover during login or verification code scenarios. Additionally, it provides remote ADB debugging capabilities, allowing device connection via WiFi or network for flexible remote control and development.
|
| 24 |
+
|
| 25 |
+
> ⚠️ This project is for research and learning purposes only. It is strictly prohibited to use for illegal information acquisition, system interference, or any illegal activities. Please carefully review the [Terms of Use](resources/privacy_policy_en.txt).
|
| 26 |
+
|
| 27 |
+
## Model Download Links
|
| 28 |
+
|
| 29 |
+
| Model | Download Links |
|
| 30 |
+
|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| 31 |
+
| AutoGLM-Phone-9B | [🤗 Hugging Face](https://huggingface.co/zai-org/AutoGLM-Phone-9B)<br>[🤖 ModelScope](https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B) |
|
| 32 |
+
| AutoGLM-Phone-9B-Multilingual | [🤗 Hugging Face](https://huggingface.co/zai-org/AutoGLM-Phone-9B-Multilingual)<br>[🤖 ModelScope](https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B-Multilingual) |
|
| 33 |
+
|
| 34 |
+
`AutoGLM-Phone-9B` is optimized for Chinese mobile applications, while `AutoGLM-Phone-9B-Multilingual` supports English scenarios and is suitable for applications containing English or other language content.
|
| 35 |
+
|
| 36 |
+
## Environment Setup
|
| 37 |
+
|
| 38 |
+
### 1. Python Environment
|
| 39 |
+
|
| 40 |
+
Python 3.10 or higher is recommended.
|
| 41 |
+
|
| 42 |
+
### 2. Device Debug Tools
|
| 43 |
+
|
| 44 |
+
Choose the appropriate tool based on your device type:
|
| 45 |
+
|
| 46 |
+
#### For Android Devices - Using ADB
|
| 47 |
+
|
| 48 |
+
1. Download the official ADB [installation package](https://developer.android.com/tools/releases/platform-tools) and extract it to a custom path
|
| 49 |
+
2. Configure environment variables
|
| 50 |
+
|
| 51 |
+
- MacOS configuration: In `Terminal` or any command line tool
|
| 52 |
+
|
| 53 |
+
```bash
|
| 54 |
+
# Assuming the extracted directory is ~/Downloads/platform-tools. Adjust the command if different.
|
| 55 |
+
export PATH=${PATH}:~/Downloads/platform-tools
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
- Windows configuration: Refer to [third-party tutorials](https://blog.csdn.net/x2584179909/article/details/108319973) for configuration.
|
| 59 |
+
|
| 60 |
+
#### For HarmonyOS Devices - Using HDC
|
| 61 |
+
|
| 62 |
+
1. Download HDC tool:
|
| 63 |
+
- From [HarmonyOS SDK](https://developer.huawei.com/consumer/en/download/)
|
| 64 |
+
2. Configure environment variables
|
| 65 |
+
|
| 66 |
+
- MacOS/Linux configuration:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# Assuming the extracted directory is ~/Downloads/harmonyos-sdk/toolchains. Adjust according to actual path.
|
| 70 |
+
export PATH=${PATH}:~/Downloads/harmonyos-sdk/toolchains
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
- Windows configuration: Add the HDC tool directory to the system PATH environment variable
|
| 74 |
+
|
| 75 |
+
### 3. Android 7.0+ or HarmonyOS Device with `Developer Mode` and `USB Debugging` Enabled
|
| 76 |
+
|
| 77 |
+
1. Enable Developer Mode: The typical method is to find `Settings > About Phone > Build Number` and tap it rapidly about 10 times until a popup shows "Developer mode has been enabled." This may vary slightly between phones; search online for tutorials if you can't find it.
|
| 78 |
+
2. Enable USB Debugging: After enabling Developer Mode, go to `Settings > Developer Options > USB Debugging` and enable it
|
| 79 |
+
3. Some devices may require a restart after setting developer options for them to take effect. You can test by connecting your phone to your computer via USB cable and running `adb devices` to see if device information appears. If not, the connection has failed.
|
| 80 |
+
|
| 81 |
+
**Please carefully check the relevant permissions**
|
| 82 |
+
|
| 83 |
+

|
| 84 |
+
|
| 85 |
+
### 4. Install ADB Keyboard (Required for Android Devices Only, for Text Input)
|
| 86 |
+
|
| 87 |
+
**Note: HarmonyOS devices use native input methods and do not require ADB Keyboard.**
|
| 88 |
+
|
| 89 |
+
If you are using an Android device:
|
| 90 |
+
|
| 91 |
+
Download the [installation package](https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk) and install it on the corresponding Android device.
|
| 92 |
+
Note: After installation, you need to enable `ADB Keyboard` in `Settings > Input Method` or `Settings > Keyboard List` for it to work.(or use command `adb shell ime enable com.android.adbkeyboard/.AdbIME`[How-to-use](https://github.com/senzhk/ADBKeyBoard/blob/master/README.md#how-to-use))
|
| 93 |
+
|
| 94 |
+
## Deployment Preparation
|
| 95 |
+
|
| 96 |
+
### 1. Install Dependencies
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
pip install -r requirements.txt
|
| 100 |
+
pip install -e .
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### 2. Configure ADB or HDC
|
| 104 |
+
|
| 105 |
+
#### For Android Devices
|
| 106 |
+
|
| 107 |
+
Make sure your **USB cable supports data transfer**, not just charging.
|
| 108 |
+
|
| 109 |
+
Ensure ADB is installed and connect the device via **USB cable**:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
# Check connected devices
|
| 113 |
+
adb devices
|
| 114 |
+
|
| 115 |
+
# Output should show your device, e.g.:
|
| 116 |
+
# List of devices attached
|
| 117 |
+
# emulator-5554 device
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
#### For HarmonyOS Devices
|
| 121 |
+
|
| 122 |
+
Make sure your **USB cable supports data transfer**, not just charging.
|
| 123 |
+
|
| 124 |
+
Ensure HDC is installed and connect the device via **USB cable**:
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
# Check connected devices
|
| 128 |
+
hdc list targets
|
| 129 |
+
|
| 130 |
+
# Output should show your device, e.g.:
|
| 131 |
+
# 7001005458323933328a01bce01c2500
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 3. Start Model Service
|
| 135 |
+
|
| 136 |
+
You can choose to deploy the model service yourself or use a third-party model service provider.
|
| 137 |
+
|
| 138 |
+
#### Option A: Use Third-Party Model Services
|
| 139 |
+
|
| 140 |
+
If you don't want to deploy the model yourself, you can use the following third-party services that have already deployed our model:
|
| 141 |
+
|
| 142 |
+
**1. z.ai**
|
| 143 |
+
|
| 144 |
+
- Documentation: https://docs.z.ai/api-reference/introduction
|
| 145 |
+
- `--base-url`: `https://api.z.ai/api/paas/v4`
|
| 146 |
+
- `--model`: `autoglm-phone-multilingual`
|
| 147 |
+
- `--apikey`: Apply for your own API key on the z.ai platform
|
| 148 |
+
|
| 149 |
+
**2. Novita AI**
|
| 150 |
+
|
| 151 |
+
- Documentation: https://novita.ai/models/model-detail/zai-org-autoglm-phone-9b-multilingual
|
| 152 |
+
- `--base-url`: `https://api.novita.ai/openai`
|
| 153 |
+
- `--model`: `zai-org/autoglm-phone-9b-multilingual`
|
| 154 |
+
- `--apikey`: Apply for your own API key on the Novita AI platform
|
| 155 |
+
|
| 156 |
+
**3. Parasail**
|
| 157 |
+
|
| 158 |
+
- Documentation: https://www.saas.parasail.io/serverless?name=auto-glm-9b-multilingual
|
| 159 |
+
- `--base-url`: `https://api.parasail.io/v1`
|
| 160 |
+
- `--model`: `parasail-auto-glm-9b-multilingual`
|
| 161 |
+
- `--apikey`: Apply for your own API key on the Parasail platform
|
| 162 |
+
|
| 163 |
+
Example usage with third-party services:
|
| 164 |
+
|
| 165 |
+
```bash
|
| 166 |
+
# Using z.ai
|
| 167 |
+
python main.py --base-url https://api.z.ai/api/paas/v4 --model "autoglm-phone-multilingual" --apikey "your-z-ai-api-key" "Open Chrome browser"
|
| 168 |
+
|
| 169 |
+
# Using Novita AI
|
| 170 |
+
python main.py --base-url https://api.novita.ai/openai --model "zai-org/autoglm-phone-9b-multilingual" --apikey "your-novita-api-key" "Open Chrome browser"
|
| 171 |
+
|
| 172 |
+
# Using Parasail
|
| 173 |
+
python main.py --base-url https://api.parasail.io/v1 --model "parasail-auto-glm-9b-multilingual" --apikey "your-parasail-api-key" "Open Chrome browser"
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
#### Option B: Deploy Model Yourself
|
| 177 |
+
|
| 178 |
+
If you prefer to deploy the model locally or on your own server:
|
| 179 |
+
|
| 180 |
+
1. Download the model and install the inference engine framework according to the `For Model Deployment` section in `requirements.txt`.
|
| 181 |
+
2. Start via SGlang / vLLM to get an OpenAI-format service. Here's a vLLM deployment solution; please strictly follow the startup parameters we provide:
|
| 182 |
+
|
| 183 |
+
- vLLM:
|
| 184 |
+
|
| 185 |
+
```shell
|
| 186 |
+
python3 -m vllm.entrypoints.openai.api_server \
|
| 187 |
+
--served-model-name autoglm-phone-9b-multilingual \
|
| 188 |
+
--allowed-local-media-path / \
|
| 189 |
+
--mm-encoder-tp-mode data \
|
| 190 |
+
--mm_processor_cache_type shm \
|
| 191 |
+
--mm_processor_kwargs "{\"max_pixels\":5000000}" \
|
| 192 |
+
--max-model-len 25480 \
|
| 193 |
+
--chat-template-content-format string \
|
| 194 |
+
--limit-mm-per-prompt "{\"image\":10}" \
|
| 195 |
+
--model zai-org/AutoGLM-Phone-9B-Multilingual \
|
| 196 |
+
--port 8000
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
- This model has the same architecture as `GLM-4.1V-9B-Thinking`. For detailed information about model deployment, you can also check [GLM-V](https://github.com/zai-org/GLM-V) for model deployment and usage guides.
|
| 200 |
+
|
| 201 |
+
- After successful startup, the model service will be accessible at `http://localhost:8000/v1`. If you deploy the model on a remote server, access it using that server's IP address.
|
| 202 |
+
|
| 203 |
+
### 4. Check Model Deployment
|
| 204 |
+
|
| 205 |
+
After starting the model service, you can use the following command to verify the deployment:
|
| 206 |
+
|
| 207 |
+
```bash
|
| 208 |
+
python scripts/check_deployment_en.py --base-url http://localhost:8000/v1 --model autoglm-phone-9b-multilingual
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
If using a third-party model service:
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
# Novita AI
|
| 215 |
+
python scripts/check_deployment_en.py --base-url https://api.novita.ai/openai --model zai-org/autoglm-phone-9b-multilingual --apikey your-novita-api-key
|
| 216 |
+
|
| 217 |
+
# Parasail
|
| 218 |
+
python scripts/check_deployment_en.py --base-url https://api.parasail.io/v1 --model parasail-auto-glm-9b-multilingual --apikey your-parasail-api-key
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
Upon successful execution, the script will display the model's inference result and token statistics, helping you confirm whether the model deployment is working correctly.
|
| 222 |
+
|
| 223 |
+
## Using AutoGLM
|
| 224 |
+
|
| 225 |
+
### Command Line
|
| 226 |
+
|
| 227 |
+
Set the `--base-url` and `--model` parameters according to your deployed model. For example:
|
| 228 |
+
|
| 229 |
+
```bash
|
| 230 |
+
# Android device - Interactive mode
|
| 231 |
+
python main.py --base-url http://localhost:8000/v1 --model "autoglm-phone-9b-multilingual"
|
| 232 |
+
|
| 233 |
+
# Android device - Specify task
|
| 234 |
+
python main.py --base-url http://localhost:8000/v1 "Open Maps and search for nearby coffee shops"
|
| 235 |
+
|
| 236 |
+
# HarmonyOS device - Interactive mode
|
| 237 |
+
python main.py --device-type hdc --base-url http://localhost:8000/v1 --model "autoglm-phone-9b-multilingual"
|
| 238 |
+
|
| 239 |
+
# HarmonyOS device - Specify task
|
| 240 |
+
python main.py --device-type hdc --base-url http://localhost:8000/v1 "Open Maps and search for nearby coffee shops"
|
| 241 |
+
|
| 242 |
+
# Use API key for authentication
|
| 243 |
+
python main.py --apikey sk-xxxxx
|
| 244 |
+
|
| 245 |
+
# Use English system prompt
|
| 246 |
+
python main.py --lang en --base-url http://localhost:8000/v1 "Open Chrome browser"
|
| 247 |
+
|
| 248 |
+
# List supported apps (Android)
|
| 249 |
+
python main.py --list-apps
|
| 250 |
+
|
| 251 |
+
# List supported apps (HarmonyOS)
|
| 252 |
+
python main.py --device-type hdc --list-apps
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
### Python API
|
| 256 |
+
|
| 257 |
+
```python
|
| 258 |
+
from phone_agent import PhoneAgent
|
| 259 |
+
from phone_agent.model import ModelConfig
|
| 260 |
+
|
| 261 |
+
# Configure model
|
| 262 |
+
model_config = ModelConfig(
|
| 263 |
+
base_url="http://localhost:8000/v1",
|
| 264 |
+
model_name="autoglm-phone-9b-multilingual",
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Create Agent
|
| 268 |
+
agent = PhoneAgent(model_config=model_config)
|
| 269 |
+
|
| 270 |
+
# Execute task
|
| 271 |
+
result = agent.run("Open eBay and search for wireless earphones")
|
| 272 |
+
print(result)
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
## Remote Debugging
|
| 276 |
+
|
| 277 |
+
Phone Agent supports remote ADB/HDC debugging via WiFi/network, allowing device control without a USB connection.
|
| 278 |
+
|
| 279 |
+
### Configure Remote Debugging
|
| 280 |
+
|
| 281 |
+
#### Enable Wireless Debugging on Phone
|
| 282 |
+
|
| 283 |
+
##### Android Devices
|
| 284 |
+
|
| 285 |
+
Ensure the phone and computer are on the same WiFi network, as shown below:
|
| 286 |
+
|
| 287 |
+

|
| 288 |
+
|
| 289 |
+
##### HarmonyOS Devices
|
| 290 |
+
|
| 291 |
+
Ensure the phone and computer are on the same WiFi network:
|
| 292 |
+
1. Go to `Settings > System & Updates > Developer Options`
|
| 293 |
+
2. Enable `USB Debugging` and `Wireless Debugging`
|
| 294 |
+
3. Note the displayed IP address and port number
|
| 295 |
+
|
| 296 |
+
#### Use Standard ADB/HDC Commands on Computer
|
| 297 |
+
|
| 298 |
+
```bash
|
| 299 |
+
# Android device - Connect via WiFi, replace with the IP address and port shown on your phone
|
| 300 |
+
adb connect 192.168.1.100:5555
|
| 301 |
+
|
| 302 |
+
# Verify connection
|
| 303 |
+
adb devices
|
| 304 |
+
# Should show: 192.168.1.100:5555 device
|
| 305 |
+
|
| 306 |
+
# HarmonyOS device - Connect via WiFi
|
| 307 |
+
hdc tconn 192.168.1.100:5555
|
| 308 |
+
|
| 309 |
+
# Verify connection
|
| 310 |
+
hdc list targets
|
| 311 |
+
# Should show: 192.168.1.100:5555
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
### Device Management Commands
|
| 315 |
+
|
| 316 |
+
#### Android Devices (ADB)
|
| 317 |
+
|
| 318 |
+
```bash
|
| 319 |
+
# List all connected devices
|
| 320 |
+
adb devices
|
| 321 |
+
|
| 322 |
+
# Connect to remote device
|
| 323 |
+
adb connect 192.168.1.100:5555
|
| 324 |
+
|
| 325 |
+
# Disconnect specific device
|
| 326 |
+
adb disconnect 192.168.1.100:5555
|
| 327 |
+
|
| 328 |
+
# Execute task on specific device
|
| 329 |
+
python main.py --device-id 192.168.1.100:5555 --base-url http://localhost:8000/v1 --model "autoglm-phone-9b-multilingual" "Open TikTok and browse videos"
|
| 330 |
+
```
|
| 331 |
+
|
| 332 |
+
#### HarmonyOS Devices (HDC)
|
| 333 |
+
|
| 334 |
+
```bash
|
| 335 |
+
# List all connected devices
|
| 336 |
+
hdc list targets
|
| 337 |
+
|
| 338 |
+
# Connect to remote device
|
| 339 |
+
hdc tconn 192.168.1.100:5555
|
| 340 |
+
|
| 341 |
+
# Disconnect specific device
|
| 342 |
+
hdc tdisconn 192.168.1.100:5555
|
| 343 |
+
|
| 344 |
+
# Execute task on specific device
|
| 345 |
+
python main.py --device-type hdc --device-id 192.168.1.100:5555 --base-url http://localhost:8000/v1 --model "autoglm-phone-9b-multilingual" "Open TikTok and browse videos"
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
### Python API Remote Connection
|
| 349 |
+
|
| 350 |
+
#### Android Devices (ADB)
|
| 351 |
+
|
| 352 |
+
```python
|
| 353 |
+
from phone_agent.adb import ADBConnection, list_devices
|
| 354 |
+
|
| 355 |
+
# Create connection manager
|
| 356 |
+
conn = ADBConnection()
|
| 357 |
+
|
| 358 |
+
# Connect to remote device
|
| 359 |
+
success, message = conn.connect("192.168.1.100:5555")
|
| 360 |
+
print(f"Connection status: {message}")
|
| 361 |
+
|
| 362 |
+
# List connected devices
|
| 363 |
+
devices = list_devices()
|
| 364 |
+
for device in devices:
|
| 365 |
+
print(f"{device.device_id} - {device.connection_type.value}")
|
| 366 |
+
|
| 367 |
+
# Enable TCP/IP on USB device
|
| 368 |
+
success, message = conn.enable_tcpip(5555)
|
| 369 |
+
ip = conn.get_device_ip()
|
| 370 |
+
print(f"Device IP: {ip}")
|
| 371 |
+
|
| 372 |
+
# Disconnect
|
| 373 |
+
conn.disconnect("192.168.1.100:5555")
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
#### HarmonyOS Devices (HDC)
|
| 377 |
+
|
| 378 |
+
```python
|
| 379 |
+
from phone_agent.hdc import HDCConnection, list_devices
|
| 380 |
+
|
| 381 |
+
# Create connection manager
|
| 382 |
+
conn = HDCConnection()
|
| 383 |
+
|
| 384 |
+
# Connect to remote device
|
| 385 |
+
success, message = conn.connect("192.168.1.100:5555")
|
| 386 |
+
print(f"Connection status: {message}")
|
| 387 |
+
|
| 388 |
+
# List connected devices
|
| 389 |
+
devices = list_devices()
|
| 390 |
+
for device in devices:
|
| 391 |
+
print(f"{device.device_id} - {device.connection_type.value}")
|
| 392 |
+
|
| 393 |
+
# Disconnect
|
| 394 |
+
conn.disconnect("192.168.1.100:5555")
|
| 395 |
+
```
|
| 396 |
+
|
| 397 |
+
### Remote Connection Troubleshooting
|
| 398 |
+
|
| 399 |
+
**Connection Refused:**
|
| 400 |
+
|
| 401 |
+
- Ensure the device and computer are on the same network
|
| 402 |
+
- Check if the firewall is blocking port 5555
|
| 403 |
+
- Confirm TCP/IP mode is enabled: `adb tcpip 5555`
|
| 404 |
+
|
| 405 |
+
**Connection Dropped:**
|
| 406 |
+
|
| 407 |
+
- WiFi may have disconnected; use `--connect` to reconnect
|
| 408 |
+
- Some devices disable TCP/IP after restart; re-enable via USB
|
| 409 |
+
|
| 410 |
+
**Multiple Devices:**
|
| 411 |
+
|
| 412 |
+
- Use `--device-id` to specify which device to use
|
| 413 |
+
- Or use `--list-devices` to view all connected devices
|
| 414 |
+
|
| 415 |
+
## Configuration
|
| 416 |
+
|
| 417 |
+
### Custom SYSTEM PROMPT
|
| 418 |
+
|
| 419 |
+
The system provides both Chinese and English prompts, switchable via the `--lang` parameter:
|
| 420 |
+
|
| 421 |
+
- `--lang cn` - Chinese prompt (default), config file: `phone_agent/config/prompts_zh.py`
|
| 422 |
+
- `--lang en` - English prompt, config file: `phone_agent/config/prompts_en.py`
|
| 423 |
+
|
| 424 |
+
You can directly modify the corresponding config files to enhance model capabilities in specific domains or disable certain apps by injecting app names.
|
| 425 |
+
|
| 426 |
+
### Environment Variables
|
| 427 |
+
|
| 428 |
+
| Variable | Description | Default Value |
|
| 429 |
+
|-----------------------------|---------------------------|----------------------------|
|
| 430 |
+
| `PHONE_AGENT_BASE_URL` | Model API URL | `http://localhost:8000/v1` |
|
| 431 |
+
| `PHONE_AGENT_MODEL` | Model name | `autoglm-phone-9b` |
|
| 432 |
+
| `PHONE_AGENT_API_KEY` | API key for authentication| `EMPTY` |
|
| 433 |
+
| `PHONE_AGENT_MAX_STEPS` | Maximum steps per task | `100` |
|
| 434 |
+
| `PHONE_AGENT_DEVICE_ID` | ADB/HDC device ID | (auto-detect) |
|
| 435 |
+
| `PHONE_AGENT_DEVICE_TYPE` | Device type (`adb` or `hdc`)| `adb` |
|
| 436 |
+
| `PHONE_AGENT_LANG` | Language (`cn` or `en`) | `en` |
|
| 437 |
+
|
| 438 |
+
### Model Configuration
|
| 439 |
+
|
| 440 |
+
```python
|
| 441 |
+
from phone_agent.model import ModelConfig
|
| 442 |
+
|
| 443 |
+
config = ModelConfig(
|
| 444 |
+
base_url="http://localhost:8000/v1",
|
| 445 |
+
api_key="EMPTY", # API key (if required)
|
| 446 |
+
model_name="autoglm-phone-9b-multilingual", # Model name
|
| 447 |
+
max_tokens=3000, # Maximum output tokens
|
| 448 |
+
temperature=0.1, # Sampling temperature
|
| 449 |
+
frequency_penalty=0.2, # Frequency penalty
|
| 450 |
+
)
|
| 451 |
+
```
|
| 452 |
+
|
| 453 |
+
### Agent Configuration
|
| 454 |
+
|
| 455 |
+
```python
|
| 456 |
+
from phone_agent.agent import AgentConfig
|
| 457 |
+
|
| 458 |
+
config = AgentConfig(
|
| 459 |
+
max_steps=100, # Maximum steps per task
|
| 460 |
+
device_id=None, # ADB device ID (None for auto-detect)
|
| 461 |
+
lang="en", # Language: cn (Chinese) or en (English)
|
| 462 |
+
verbose=True, # Print debug info (including thinking process and actions)
|
| 463 |
+
)
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
### Verbose Mode Output
|
| 467 |
+
|
| 468 |
+
When `verbose=True`, the Agent outputs detailed information at each step:
|
| 469 |
+
|
| 470 |
+
```
|
| 471 |
+
==================================================
|
| 472 |
+
💭 Thinking Process:
|
| 473 |
+
--------------------------------------------------
|
| 474 |
+
Currently on the system desktop, need to launch eBay app first
|
| 475 |
+
--------------------------------------------------
|
| 476 |
+
🎯 Executing Action:
|
| 477 |
+
{
|
| 478 |
+
"_metadata": "do",
|
| 479 |
+
"action": "Launch",
|
| 480 |
+
"app": "eBay"
|
| 481 |
+
}
|
| 482 |
+
==================================================
|
| 483 |
+
|
| 484 |
+
... (continues to next step after executing action)
|
| 485 |
+
|
| 486 |
+
==================================================
|
| 487 |
+
💭 Thinking Process:
|
| 488 |
+
--------------------------------------------------
|
| 489 |
+
eBay is now open, need to tap the search box
|
| 490 |
+
--------------------------------------------------
|
| 491 |
+
🎯 Executing Action:
|
| 492 |
+
{
|
| 493 |
+
"_metadata": "do",
|
| 494 |
+
"action": "Tap",
|
| 495 |
+
"element": [499, 182]
|
| 496 |
+
}
|
| 497 |
+
==================================================
|
| 498 |
+
|
| 499 |
+
🎉 ================================================
|
| 500 |
+
✅ Task Completed: Successfully opened eBay and searched for 'wireless earphones'
|
| 501 |
+
==================================================
|
| 502 |
+
```
|
| 503 |
+
|
| 504 |
+
This allows you to clearly see the AI's reasoning process and specific operations at each step.
|
| 505 |
+
|
| 506 |
+
## Supported Apps
|
| 507 |
+
|
| 508 |
+
### Android Apps
|
| 509 |
+
|
| 510 |
+
Phone Agent supports 50+ mainstream Chinese applications:
|
| 511 |
+
|
| 512 |
+
| Category | Apps |
|
| 513 |
+
|--------------------------|----------------------------------------------------------------------------------------|
|
| 514 |
+
| Social & Messaging | X, Tiktok, WhatsApp, Telegram, FacebookMessenger, GoogleChat, Quora, Reddit, Instagram |
|
| 515 |
+
| Productivity & Office | Gmail, GoogleCalendar, GoogleDrive, GoogleDocs, GoogleTasks, Joplin |
|
| 516 |
+
| Life, Shopping & Finance | Amazon shopping, Temu, Bluecoins, Duolingo, GoogleFit, ebay |
|
| 517 |
+
| Utilities & Media | GoogleClock, Chrome, GooglePlayStore, GooglePlayBooks, FilesbyGoogle |
|
| 518 |
+
| Travel & Navigation | GoogleMaps, Booking.com, Trip.com, Expedia, OpenTracks |
|
| 519 |
+
|
| 520 |
+
Run `python main.py --list-apps` to see the complete list.
|
| 521 |
+
|
| 522 |
+
### HarmonyOS Apps
|
| 523 |
+
|
| 524 |
+
Phone Agent supports 60+ HarmonyOS native apps and system apps:
|
| 525 |
+
|
| 526 |
+
| Category | Apps |
|
| 527 |
+
|--------------------------|----------------------------------------------------------------------------------------|
|
| 528 |
+
| Social & Messaging | WeChat, QQ, Weibo, Feishu, Enterprise WeChat |
|
| 529 |
+
| E-commerce & Shopping | Taobao, JD.com, Pinduoduo, Vipshop, Dewu, Xianyu |
|
| 530 |
+
| Food & Delivery | Meituan, Meituan Waimai, Dianping, Haidilao |
|
| 531 |
+
| Travel & Navigation | 12306, Didi, Tongcheng, Amap, Baidu Maps |
|
| 532 |
+
| Video & Entertainment | Bilibili, Douyin, Kuaishou, Tencent Video, iQIYI, Mango TV |
|
| 533 |
+
| Music & Audio | QQ Music, Qishui Music, Ximalaya |
|
| 534 |
+
| Lifestyle & Social | Xiaohongshu, Zhihu, Toutiao, 58.com, China Mobile |
|
| 535 |
+
| AI & Tools | Doubao, WPS, UC Browser, CamScanner, Meitu |
|
| 536 |
+
| System Apps | Browser, Calendar, Camera, Clock, Cloud, File Manager, Gallery, Contacts, SMS, Settings |
|
| 537 |
+
| Huawei Services | AppGallery, Music, Video, Books, Themes, Weather |
|
| 538 |
+
|
| 539 |
+
Run `python main.py --device-type hdc --list-apps` to see the complete list.
|
| 540 |
+
|
| 541 |
+
## Available Actions
|
| 542 |
+
|
| 543 |
+
The Agent can perform the following actions:
|
| 544 |
+
|
| 545 |
+
| Action | Description |
|
| 546 |
+
|----------------|------------------------------------------|
|
| 547 |
+
| `Launch` | Launch an app |
|
| 548 |
+
| `Tap` | Tap at specified coordinates |
|
| 549 |
+
| `Type` | Input text |
|
| 550 |
+
| `Swipe` | Swipe the screen |
|
| 551 |
+
| `Back` | Go back to previous page |
|
| 552 |
+
| `Home` | Return to home screen |
|
| 553 |
+
| `Long Press` | Long press |
|
| 554 |
+
| `Double Tap` | Double tap |
|
| 555 |
+
| `Wait` | Wait for page to load |
|
| 556 |
+
| `Take_over` | Request manual takeover (login/captcha) |
|
| 557 |
+
|
| 558 |
+
## Custom Callbacks
|
| 559 |
+
|
| 560 |
+
Handle sensitive operation confirmation and manual takeover:
|
| 561 |
+
|
| 562 |
+
```python
|
| 563 |
+
def my_confirmation(message: str) -> bool:
|
| 564 |
+
"""Sensitive operation confirmation callback"""
|
| 565 |
+
return input(f"Confirm execution of {message}? (y/n): ").lower() == "y"
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def my_takeover(message: str) -> None:
|
| 569 |
+
"""Manual takeover callback"""
|
| 570 |
+
print(f"Please complete manually: {message}")
|
| 571 |
+
input("Press Enter after completion...")
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
agent = PhoneAgent(
|
| 575 |
+
confirmation_callback=my_confirmation,
|
| 576 |
+
takeover_callback=my_takeover,
|
| 577 |
+
)
|
| 578 |
+
```
|
| 579 |
+
|
| 580 |
+
## Examples
|
| 581 |
+
|
| 582 |
+
Check the `examples/` directory for more usage examples:
|
| 583 |
+
|
| 584 |
+
- `basic_usage.py` - Basic task execution
|
| 585 |
+
- Single-step debugging mode
|
| 586 |
+
- Batch task execution
|
| 587 |
+
- Custom callbacks
|
| 588 |
+
|
| 589 |
+
## Development
|
| 590 |
+
|
| 591 |
+
### Set Up Development Environment
|
| 592 |
+
|
| 593 |
+
Development requires dev dependencies:
|
| 594 |
+
|
| 595 |
+
```bash
|
| 596 |
+
pip install -e ".[dev]"
|
| 597 |
+
```
|
| 598 |
+
|
| 599 |
+
### Run Tests
|
| 600 |
+
|
| 601 |
+
```bash
|
| 602 |
+
pytest tests/
|
| 603 |
+
```
|
| 604 |
+
|
| 605 |
+
### Complete Project Structure
|
| 606 |
+
|
| 607 |
+
```
|
| 608 |
+
phone_agent/
|
| 609 |
+
├── __init__.py # Package exports
|
| 610 |
+
├── agent.py # PhoneAgent main class
|
| 611 |
+
├── adb/ # ADB utilities
|
| 612 |
+
│ ├── connection.py # Remote/local connection management
|
| 613 |
+
│ ├── screenshot.py # Screen capture
|
| 614 |
+
│ ├── input.py # Text input (ADB Keyboard)
|
| 615 |
+
│ └── device.py # Device control (tap, swipe, etc.)
|
| 616 |
+
├── actions/ # Action handling
|
| 617 |
+
│ └── handler.py # Action executor
|
| 618 |
+
├── config/ # Configuration
|
| 619 |
+
│ ├── apps.py # Supported app mappings
|
| 620 |
+
│ ├── prompts_zh.py # Chinese system prompts
|
| 621 |
+
│ └── prompts_en.py # English system prompts
|
| 622 |
+
└── model/ # AI model client
|
| 623 |
+
└── client.py # OpenAI-compatible client
|
| 624 |
+
```
|
| 625 |
+
|
| 626 |
+
## FAQ
|
| 627 |
+
|
| 628 |
+
Here are some common issues and their solutions:
|
| 629 |
+
|
| 630 |
+
### Device Not Found
|
| 631 |
+
|
| 632 |
+
Try resolving by restarting the ADB service:
|
| 633 |
+
|
| 634 |
+
```bash
|
| 635 |
+
adb kill-server
|
| 636 |
+
adb start-server
|
| 637 |
+
adb devices
|
| 638 |
+
```
|
| 639 |
+
|
| 640 |
+
If the device is still not recognized, please check:
|
| 641 |
+
1. Whether USB debugging is enabled
|
| 642 |
+
2. Whether the USB cable supports data transfer (some cables only support charging)
|
| 643 |
+
3. Whether you have tapped "Allow" on the authorization popup on your phone
|
| 644 |
+
4. Try a different USB port or cable
|
| 645 |
+
|
| 646 |
+
### Can Open Apps but Cannot Tap
|
| 647 |
+
|
| 648 |
+
Some devices require both debugging options to be enabled:
|
| 649 |
+
- **USB Debugging**
|
| 650 |
+
- **USB Debugging (Security Settings)**
|
| 651 |
+
|
| 652 |
+
Please check in `Settings → Developer Options` that both options are enabled.
|
| 653 |
+
|
| 654 |
+
### Text Input Not Working
|
| 655 |
+
|
| 656 |
+
1. Ensure ADB Keyboard is installed on the device
|
| 657 |
+
2. Enable it in Settings > System > Language & Input > Virtual Keyboard
|
| 658 |
+
3. The Agent will automatically switch to ADB Keyboard when input is needed
|
| 659 |
+
|
| 660 |
+
### Screenshot Failed (Black Screen)
|
| 661 |
+
|
| 662 |
+
This usually means the app is displaying a sensitive page (payment, password, banking apps). The Agent will automatically detect this and request manual takeover.
|
| 663 |
+
|
| 664 |
+
### Windows Encoding Issues
|
| 665 |
+
Error message like `UnicodeEncodeError gbk code`
|
| 666 |
+
|
| 667 |
+
Solution: Add the environment variable before running the code: `PYTHONIOENCODING=utf-8`
|
| 668 |
+
|
| 669 |
+
### Interactive Mode Not Working in Non-TTY Environment
|
| 670 |
+
Error like: `EOF when reading a line`
|
| 671 |
+
|
| 672 |
+
Solution: Use non-interactive mode to specify tasks directly, or switch to a TTY-mode terminal application.
|
| 673 |
+
|
| 674 |
+
### Citation
|
| 675 |
+
|
| 676 |
+
If you find our work helpful, please cite the following papers:
|
| 677 |
+
|
| 678 |
+
```bibtex
|
| 679 |
+
@article{liu2024autoglm,
|
| 680 |
+
title={Autoglm: Autonomous foundation agents for guis},
|
| 681 |
+
author={Liu, Xiao and Qin, Bo and Liang, Dongzhu and Dong, Guang and Lai, Hanyu and Zhang, Hanchen and Zhao, Hanlin and Iong, Iat Long and Sun, Jiadai and Wang, Jiaqi and others},
|
| 682 |
+
journal={arXiv preprint arXiv:2411.00820},
|
| 683 |
+
year={2024}
|
| 684 |
+
}
|
| 685 |
+
@article{xu2025mobilerl,
|
| 686 |
+
title={MobileRL: Online Agentic Reinforcement Learning for Mobile GUI Agents},
|
| 687 |
+
author={Xu, Yifan and Liu, Xiao and Liu, Xinghan and Fu, Jiaqi and Zhang, Hanchen and Jing, Bohao and Zhang, Shudan and Wang, Yuting and Zhao, Wenyi and Dong, Yuxiao},
|
| 688 |
+
journal={arXiv preprint arXiv:2509.18119},
|
| 689 |
+
year={2025}
|
| 690 |
+
}
|
| 691 |
+
```
|
| 692 |
+
|
| 693 |
+
---
|
| 694 |
+
|
| 695 |
+
## Automated Deployment Guide (For AI Assistants)
|
| 696 |
+
|
| 697 |
+
> **This section is specifically designed for AI assistants (such as Claude Code) to automate the deployment of Open-AutoGLM.**
|
| 698 |
+
>
|
| 699 |
+
> If you are a human reader, you can skip this section and follow the documentation above.
|
| 700 |
+
|
| 701 |
+
---
|
| 702 |
+
|
| 703 |
+
### Project Overview
|
| 704 |
+
|
| 705 |
+
Open-AutoGLM is a phone agent framework:
|
| 706 |
+
- **Input**: User's natural language instructions (e.g., "Open WhatsApp and send a message to John")
|
| 707 |
+
- **Output**: Automatically operates the user's Android phone to complete tasks
|
| 708 |
+
- **Mechanism**: Screenshot → Vision model understands interface → Outputs tap coordinates → ADB executes actions → Loop
|
| 709 |
+
|
| 710 |
+
The architecture consists of two parts:
|
| 711 |
+
1. **Agent Code** (this repository): Runs on the user's computer, responsible for calling models, parsing actions, and controlling the phone
|
| 712 |
+
2. **Vision Model Service**: Can be a remote API or deployed locally
|
| 713 |
+
|
| 714 |
+
---
|
| 715 |
+
|
| 716 |
+
### Pre-Deployment Checklist
|
| 717 |
+
|
| 718 |
+
Before starting deployment, confirm the following items with the user:
|
| 719 |
+
|
| 720 |
+
#### Hardware Requirements
|
| 721 |
+
- [ ] User has an Android phone (Android 7.0+)
|
| 722 |
+
- [ ] User has a USB cable that supports data transfer (not just charging)
|
| 723 |
+
- [ ] Phone and computer can be connected via USB cable
|
| 724 |
+
|
| 725 |
+
#### Phone Configuration
|
| 726 |
+
- [ ] Phone has Developer Mode enabled (Settings → About Phone → Tap Build Number 7 times)
|
| 727 |
+
- [ ] Phone has USB Debugging enabled (Settings → Developer Options → USB Debugging)
|
| 728 |
+
- [ ] Some models require enabling "USB Debugging (Security Settings)" as well
|
| 729 |
+
- [ ] ADB Keyboard app is installed (Download: https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk)
|
| 730 |
+
- [ ] ADB Keyboard is enabled in system settings (Settings → Language & Input → Enable ADB Keyboard)
|
| 731 |
+
|
| 732 |
+
#### Model Service Confirmation (Choose One)
|
| 733 |
+
|
| 734 |
+
**Ask the user explicitly: Do you already have access to an AutoGLM model service?**
|
| 735 |
+
|
| 736 |
+
- **Option A: Use an already-deployed model service (Recommended)**
|
| 737 |
+
- User provides the model service URL (e.g., `http://xxx.xxx.xxx.xxx:8000/v1`)
|
| 738 |
+
- No local GPU required, no model download needed
|
| 739 |
+
- Use this URL directly as the `--base-url` parameter
|
| 740 |
+
|
| 741 |
+
- **Option B: Deploy model locally (High system requirements)**
|
| 742 |
+
- Requires NVIDIA GPU (24GB+ VRAM recommended)
|
| 743 |
+
- Requires installation of vLLM or SGLang
|
| 744 |
+
- Requires downloading approximately 20GB of model files
|
| 745 |
+
- **If the user is a beginner or unsure, strongly recommend Option A**
|
| 746 |
+
|
| 747 |
+
---
|
| 748 |
+
|
| 749 |
+
### Deployment Process
|
| 750 |
+
|
| 751 |
+
#### Phase 1: Environment Setup
|
| 752 |
+
|
| 753 |
+
```bash
|
| 754 |
+
# 1. Install ADB tools
|
| 755 |
+
# MacOS:
|
| 756 |
+
brew install android-platform-tools
|
| 757 |
+
# Or download manually: https://developer.android.com/tools/releases/platform-tools
|
| 758 |
+
|
| 759 |
+
# Windows: Download, extract, and add to PATH environment variable
|
| 760 |
+
|
| 761 |
+
# 2. Verify ADB installation
|
| 762 |
+
adb version
|
| 763 |
+
# Should output version information
|
| 764 |
+
|
| 765 |
+
# 3. Connect phone and verify
|
| 766 |
+
# Connect phone via USB cable, tap "Allow USB debugging" on phone
|
| 767 |
+
adb devices
|
| 768 |
+
# Should output device list, e.g.:
|
| 769 |
+
# List of devices attached
|
| 770 |
+
# XXXXXXXX device
|
| 771 |
+
```
|
| 772 |
+
|
| 773 |
+
**If `adb devices` shows empty list or unauthorized:**
|
| 774 |
+
1. Check if authorization popup appeared on phone, tap "Allow"
|
| 775 |
+
2. Check if USB debugging is enabled
|
| 776 |
+
3. Try a different cable or USB port
|
| 777 |
+
4. Run `adb kill-server && adb start-server` and retry
|
| 778 |
+
|
| 779 |
+
#### Phase 2: Install Agent
|
| 780 |
+
|
| 781 |
+
```bash
|
| 782 |
+
# 1. Clone repository (if not already cloned)
|
| 783 |
+
git clone https://github.com/zai-org/Open-AutoGLM.git
|
| 784 |
+
cd Open-AutoGLM
|
| 785 |
+
|
| 786 |
+
# 2. Create virtual environment (recommended)
|
| 787 |
+
python -m venv venv
|
| 788 |
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
| 789 |
+
|
| 790 |
+
# 3. Install dependencies
|
| 791 |
+
pip install -r requirements.txt
|
| 792 |
+
pip install -e .
|
| 793 |
+
```
|
| 794 |
+
|
| 795 |
+
**Note: No need to clone model repository; models are called via API.**
|
| 796 |
+
|
| 797 |
+
#### Phase 3: Configure Model Service
|
| 798 |
+
|
| 799 |
+
**If user chooses Option A (using already-deployed model):**
|
| 800 |
+
|
| 801 |
+
You can use the following third-party model services:
|
| 802 |
+
|
| 803 |
+
1. **z.ai**
|
| 804 |
+
- Documentation: https://docs.z.ai/api-reference/introduction
|
| 805 |
+
- `--base-url`: `https://api.z.ai/api/paas/v4`
|
| 806 |
+
- `--model`: `autoglm-phone-multilingual`
|
| 807 |
+
- `--apikey`: Apply for your own API key on the z.ai platform
|
| 808 |
+
|
| 809 |
+
2. **Novita AI**
|
| 810 |
+
- Documentation: https://novita.ai/models/model-detail/zai-org-autoglm-phone-9b-multilingual
|
| 811 |
+
- `--base-url`: `https://api.novita.ai/openai`
|
| 812 |
+
- `--model`: `zai-org/autoglm-phone-9b-multilingual`
|
| 813 |
+
- `--apikey`: Apply for your own API key on the Novita AI platform
|
| 814 |
+
|
| 815 |
+
3. **Parasail**
|
| 816 |
+
- Documentation: https://www.saas.parasail.io/serverless?name=auto-glm-9b-multilingual
|
| 817 |
+
- `--base-url`: `https://api.parasail.io/v1`
|
| 818 |
+
- `--model`: `parasail-auto-glm-9b-multilingual`
|
| 819 |
+
- `--apikey`: Apply for your own API key on the Parasail platform
|
| 820 |
+
|
| 821 |
+
Example usage:
|
| 822 |
+
|
| 823 |
+
```bash
|
| 824 |
+
# Using z.ai
|
| 825 |
+
python main.py --base-url https://api.z.ai/api/paas/v4 --model "autoglm-phone-multilingual" --apikey "your-z-ai-api-key" "Open Chrome browser"
|
| 826 |
+
|
| 827 |
+
# Using Novita AI
|
| 828 |
+
python main.py --base-url https://api.novita.ai/openai --model "zai-org/autoglm-phone-9b-multilingual" --apikey "your-novita-api-key" "Open Chrome browser"
|
| 829 |
+
|
| 830 |
+
# Using Parasail
|
| 831 |
+
python main.py --base-url https://api.parasail.io/v1 --model "parasail-auto-glm-9b-multilingual" --apikey "your-parasail-api-key" "Open Chrome browser"
|
| 832 |
+
```
|
| 833 |
+
|
| 834 |
+
Or use the URL provided by the user directly and skip local model deployment steps.
|
| 835 |
+
|
| 836 |
+
**If user chooses Option B (deploy model locally):**
|
| 837 |
+
|
| 838 |
+
```bash
|
| 839 |
+
# 1. Install vLLM
|
| 840 |
+
pip install vllm
|
| 841 |
+
|
| 842 |
+
# 2. Start model service (will auto-download model, ~20GB)
|
| 843 |
+
python3 -m vllm.entrypoints.openai.api_server \
|
| 844 |
+
--served-model-name autoglm-phone-9b-multilingual \
|
| 845 |
+
--allowed-local-media-path / \
|
| 846 |
+
--mm-encoder-tp-mode data \
|
| 847 |
+
--mm_processor_cache_type shm \
|
| 848 |
+
--mm_processor_kwargs "{\"max_pixels\":5000000}" \
|
| 849 |
+
--max-model-len 25480 \
|
| 850 |
+
--chat-template-content-format string \
|
| 851 |
+
--limit-mm-per-prompt "{\"image\":10}" \
|
| 852 |
+
--model zai-org/AutoGLM-Phone-9B-Multilingual \
|
| 853 |
+
--port 8000
|
| 854 |
+
|
| 855 |
+
# Model service URL: http://localhost:8000/v1
|
| 856 |
+
```
|
| 857 |
+
|
| 858 |
+
#### Phase 4: Verify Deployment
|
| 859 |
+
|
| 860 |
+
```bash
|
| 861 |
+
# Execute in the Open-AutoGLM directory
|
| 862 |
+
# Replace {MODEL_URL} with the actual model service address
|
| 863 |
+
|
| 864 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b-multilingual" "Open Gmail and send an email to File Transfer Assistant: Deployment successful"
|
| 865 |
+
```
|
| 866 |
+
|
| 867 |
+
**Expected Result:**
|
| 868 |
+
- Phone automatically opens Gmail
|
| 869 |
+
- Automatically searches for recipient
|
| 870 |
+
- Automatically sends the message "Deployment successful"
|
| 871 |
+
|
| 872 |
+
---
|
| 873 |
+
|
| 874 |
+
### Troubleshooting
|
| 875 |
+
|
| 876 |
+
| Error Symptom | Possible Cause | Solution |
|
| 877 |
+
|---------------|----------------|----------|
|
| 878 |
+
| `adb devices` shows nothing | USB debugging not enabled or cable issue | Check developer options, replace cable |
|
| 879 |
+
| `adb devices` shows unauthorized | Phone not authorized | Tap "Allow USB debugging" on phone |
|
| 880 |
+
| Can open apps but cannot tap | Missing security debugging permission | Enable "USB Debugging (Security Settings)" |
|
| 881 |
+
| Chinese/text input corrupted or missing | ADB Keyboard not enabled | Enable ADB Keyboard in system settings |
|
| 882 |
+
| Screenshot returns black screen | Sensitive page (payment/banking) | Normal behavior, system will handle automatically |
|
| 883 |
+
| Cannot connect to model service | Wrong URL or service not running | Check URL, confirm service is running |
|
| 884 |
+
| `ModuleNotFoundError` | Dependencies not installed | Run `pip install -r requirements.txt` |
|
| 885 |
+
|
| 886 |
+
---
|
| 887 |
+
|
| 888 |
+
### Deployment Key Points
|
| 889 |
+
|
| 890 |
+
1. **Prioritize confirming phone connection**: Before installing any code, ensure `adb devices` can see the device
|
| 891 |
+
2. **Don't skip ADB Keyboard**: Without it, text input will fail
|
| 892 |
+
3. **Model service is an external dependency**: Agent code doesn't include the model; a separate model service is required
|
| 893 |
+
4. **Check phone settings first for permission issues**: Most problems are due to incomplete phone-side configuration
|
| 894 |
+
5. **Test with simple tasks after deployment**: Recommend using "Open Gmail and send message to File Transfer Assistant" as acceptance criteria
|
| 895 |
+
|
| 896 |
+
---
|
| 897 |
+
|
| 898 |
+
### Command Quick Reference
|
| 899 |
+
|
| 900 |
+
```bash
|
| 901 |
+
# Check ADB connection
|
| 902 |
+
adb devices
|
| 903 |
+
|
| 904 |
+
# Restart ADB service
|
| 905 |
+
adb kill-server && adb start-server
|
| 906 |
+
|
| 907 |
+
# Install dependencies
|
| 908 |
+
pip install -r requirements.txt && pip install -e .
|
| 909 |
+
|
| 910 |
+
# Run Agent (interactive mode)
|
| 911 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b-multilingual"
|
| 912 |
+
|
| 913 |
+
# Run Agent (single task)
|
| 914 |
+
python main.py --base-url {MODEL_URL} --model "autoglm-phone-9b-multilingual" "your task description"
|
| 915 |
+
|
| 916 |
+
# View supported apps list
|
| 917 |
+
python main.py --list-apps
|
| 918 |
+
```
|
| 919 |
+
|
| 920 |
+
---
|
| 921 |
+
|
| 922 |
+
**Deployment success indicator: The phone can automatically execute user's natural language instructions.**
|
docs/ios_setup/ios_setup.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# iOS 环境配置指南
|
| 2 |
+
|
| 3 |
+
本文档介绍如何为 Open-AutoGLM 配置 iOS 设备环境。
|
| 4 |
+
|
| 5 |
+
## 环境要求
|
| 6 |
+
|
| 7 |
+
- macOS 操作系统
|
| 8 |
+
- Xcode(最新版本,在App store中下载)
|
| 9 |
+
- 苹果开发者账号(免费账号即可,无需付费)
|
| 10 |
+
- iOS 设备(iPhone/iPad)
|
| 11 |
+
- USB 数据线或同一 WiFi 网络
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
## WebDriverAgent 配置
|
| 15 |
+
|
| 16 |
+
WebDriverAgent 是 iOS 自动化的核心组件,需要在 iOS 设备上运行。
|
| 17 |
+
|
| 18 |
+
### 1. 克隆 WebDriverAgent
|
| 19 |
+
|
| 20 |
+
```bash
|
| 21 |
+
git clone https://github.com/appium/WebDriverAgent.git
|
| 22 |
+
cd WebDriverAgent
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
直接点击`WebDriverAgent.xcodeproj`即可使用Xcode打开。
|
| 26 |
+
|
| 27 |
+
### 2. 设置 Signing & Capabilities
|
| 28 |
+
|
| 29 |
+
1. 在 Xcode 中选中 `WebDriverAgent`,出现General、Signing&Capabilities等选项。
|
| 30 |
+
2. 进入 `Signing & Capabilities` 选项卡
|
| 31 |
+
3. 勾选 `Automatically manage signing`。在Team中选择自己的开发者账号
|
| 32 |
+
4. 将 Bundle ID 改为唯一标识符,例如:`com.yourname.WebDriverAgentRunner`
|
| 33 |
+

|
| 34 |
+
|
| 35 |
+
5. TARGETS中,建议将WebDriverAgentLib、WebDriverAgentRunner、IntegrationApp的`Signing & Capabilities` 都按照相同方式设置。
|
| 36 |
+

|
| 37 |
+
|
| 38 |
+
### 3. 测试XCode的GUI模式和UI自动化设置
|
| 39 |
+
|
| 40 |
+
建议先测试GUI模式能否成功安装WebDriverAgent,再进行后续步骤。
|
| 41 |
+
Mac和iPhone有USB和WiFi两种连接方式,建议通过USB方式,成功率更高。
|
| 42 |
+
|
| 43 |
+
#### 通过 WiFi 连接
|
| 44 |
+
|
| 45 |
+
需要满足以下条件:
|
| 46 |
+
1. 通过USB连接。在Finder中选中连接的IPhone,在“通用”中勾选"在 WiFi 中显示这台 iPhone"
|
| 47 |
+
2. Mac 与 iPhone 处于同一 WiFi 网络之下
|
| 48 |
+
|
| 49 |
+
#### 具体步骤
|
| 50 |
+
1. 从项目 Target 选择 `WebDriverAgentRunner`
|
| 51 |
+
2. 选择你的设备
|
| 52 |
+
|
| 53 |
+

|
| 54 |
+
|
| 55 |
+
3. 长按"▶️"运行按钮,选择 "Test" 后开始编译并部署到你的 iPhone 上
|
| 56 |
+
|
| 57 |
+

|
| 58 |
+
|
| 59 |
+
部署成功的标志:1. XCode没有报错。2. 你可以在iPhone上找到名为WebDriverAgentRunner的App
|
| 60 |
+
|
| 61 |
+
#### 设备信任配置
|
| 62 |
+
|
| 63 |
+
首次运行时,需要在 iPhone 上完成以下设置,然后重新编译和部署:
|
| 64 |
+
|
| 65 |
+
1. **输入解锁密码**
|
| 66 |
+
2. **信任开发者应用**
|
| 67 |
+
- 进入:设置 → 通用 → VPN与设备管理
|
| 68 |
+
- 在“开发者 App”中选择对应开发者
|
| 69 |
+
- 点击信任“XXX”
|
| 70 |
+
|
| 71 |
+

|
| 72 |
+
|
| 73 |
+
3. **启用 UI 自动化**
|
| 74 |
+
- 进入:设置 → 开发者
|
| 75 |
+
- 打开 UI 自动化设置
|
| 76 |
+
|
| 77 |
+

|
| 78 |
+
|
| 79 |
+
### 4. XCode命令行模式部署
|
| 80 |
+
|
| 81 |
+
1.安装libimobiledevice,用于与 iPhone / iPad 建立连接与通信。
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
brew install libimobiledevice
|
| 85 |
+
# 设备检查
|
| 86 |
+
idevice_id -ln
|
| 87 |
+
```
|
| 88 |
+
2.使用xcodebuild安装WebAgent。命令行也需要进行“设备信任配置”,参考GUI模式下的方法。
|
| 89 |
+
|
| 90 |
+
```
|
| 91 |
+
cd WebDriverAgent
|
| 92 |
+
|
| 93 |
+
xcodebuild -project WebDriverAgent.xcodeproj \
|
| 94 |
+
-scheme WebDriverAgentRunner \
|
| 95 |
+
-destination 'platform=iOS,name=YOUR_PHONE_NAME' \
|
| 96 |
+
test
|
| 97 |
+
```
|
| 98 |
+
这里,YOUR_PHONE_NAME可以在xcode的GUI中看到。
|
| 99 |
+
WebDriverAgent 成功运行后,会在 Xcode 控制台输出类似以下信息:
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
ServerURLHere->http://[设备IP]:8100<-ServerURLHere
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
同时,观察到手机上安装好了WebDriverAgentRunner,屏幕显示Automation Running字样。
|
| 106 |
+
其中,**http://[设备IP]:8100**为WiFi所需的WDA_URL。
|
| 107 |
+
|
| 108 |
+
## 使用 AutoGLM
|
| 109 |
+
|
| 110 |
+
以上配置完成后,先打开一个新终端,在后台建立端口映射(使用WiFi连接则不需要):
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
iproxy 8100 8100
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
之后,打开一个新终端,通过以下命令使用AutoGLM(WiFi则使用上述获得的WDA_URL):
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
python ios.py --base-url "YOUR_BASE_URL" \
|
| 120 |
+
--model "autoglm-phone" \
|
| 121 |
+
--api-key "YOUR_API_KEY" \
|
| 122 |
+
--wda-url http://localhost:8100 \
|
| 123 |
+
"TASK"
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
## 参考资源
|
| 127 |
+
|
| 128 |
+
- [WebDriverAgent 官方仓库](https://github.com/appium/WebDriverAgent)
|
| 129 |
+
- [PR141](https://github.com/zai-org/Open-AutoGLM/pull/141)
|
| 130 |
+
- [Gekowa提供的ios方案](https://github.com/gekowa/Open-AutoGLM/tree/ios-support)
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
如有其他问题,请参考主项目 README 或提交 Issue。
|
docs/ios_setup/resources/enable-ui-automation.jpg
ADDED
|
docs/ios_setup/resources/ios0_WebDriverAgent0.png
ADDED
|
Git LFS Details
|
docs/ios_setup/resources/ios0_WebDriverAgent1.png
ADDED
|
Git LFS Details
|
docs/ios_setup/resources/select-your-iphone-device.png
ADDED
|
Git LFS Details
|
docs/ios_setup/resources/setup-xcode-wda.png
ADDED
|
Git LFS Details
|
docs/ios_setup/resources/start-wda-testing.png
ADDED
|
docs/ios_setup/resources/trust-dev-app.jpg
ADDED
|
Git LFS Details
|
examples/basic_usage.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Phone Agent Usage Examples / Phone Agent 使用示例
|
| 4 |
+
|
| 5 |
+
Demonstrates how to use Phone Agent for phone automation tasks via Python API.
|
| 6 |
+
演示如何通过 Python API 使用 Phone Agent 进行手机自动化任务。
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from phone_agent import PhoneAgent
|
| 10 |
+
from phone_agent.agent import AgentConfig
|
| 11 |
+
from phone_agent.config import get_messages
|
| 12 |
+
from phone_agent.model import ModelConfig
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def example_basic_task(lang: str = "cn"):
|
| 16 |
+
"""Basic task example / 基础任务示例"""
|
| 17 |
+
msgs = get_messages(lang)
|
| 18 |
+
|
| 19 |
+
# Configure model endpoint
|
| 20 |
+
model_config = ModelConfig(
|
| 21 |
+
base_url="http://localhost:8000/v1",
|
| 22 |
+
model_name="autoglm-phone-9b",
|
| 23 |
+
temperature=0.1,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Configure Agent behavior
|
| 27 |
+
agent_config = AgentConfig(
|
| 28 |
+
max_steps=50,
|
| 29 |
+
verbose=True,
|
| 30 |
+
lang=lang,
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Create Agent
|
| 34 |
+
agent = PhoneAgent(
|
| 35 |
+
model_config=model_config,
|
| 36 |
+
agent_config=agent_config,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Execute task
|
| 40 |
+
result = agent.run("打开小红书搜索美食攻略")
|
| 41 |
+
print(f"{msgs['task_result']}: {result}")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def example_with_callbacks(lang: str = "cn"):
|
| 45 |
+
"""Task example with callbacks / 带回调的任务示例"""
|
| 46 |
+
msgs = get_messages(lang)
|
| 47 |
+
|
| 48 |
+
def my_confirmation(message: str) -> bool:
|
| 49 |
+
"""Sensitive operation confirmation callback / 敏感操作确认回调"""
|
| 50 |
+
print(f"\n[{msgs['confirmation_required']}] {message}")
|
| 51 |
+
response = input(f"{msgs['continue_prompt']}: ")
|
| 52 |
+
return response.lower() in ("yes", "y", "是")
|
| 53 |
+
|
| 54 |
+
def my_takeover(message: str) -> None:
|
| 55 |
+
"""Manual takeover callback / 人工接管回调"""
|
| 56 |
+
print(f"\n[{msgs['manual_operation_required']}] {message}")
|
| 57 |
+
print(msgs["manual_operation_hint"])
|
| 58 |
+
input(f"{msgs['press_enter_when_done']}: ")
|
| 59 |
+
|
| 60 |
+
# Create Agent with custom callbacks
|
| 61 |
+
agent_config = AgentConfig(lang=lang)
|
| 62 |
+
agent = PhoneAgent(
|
| 63 |
+
agent_config=agent_config,
|
| 64 |
+
confirmation_callback=my_confirmation,
|
| 65 |
+
takeover_callback=my_takeover,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Execute task that may require confirmation
|
| 69 |
+
result = agent.run("打开淘宝搜索无线耳机并加入购物车")
|
| 70 |
+
print(f"{msgs['task_result']}: {result}")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def example_step_by_step(lang: str = "cn"):
|
| 74 |
+
"""Step-by-step execution example (for debugging) / 单步执行示例(用于调试)"""
|
| 75 |
+
msgs = get_messages(lang)
|
| 76 |
+
|
| 77 |
+
agent_config = AgentConfig(lang=lang)
|
| 78 |
+
agent = PhoneAgent(agent_config=agent_config)
|
| 79 |
+
|
| 80 |
+
# Initialize task
|
| 81 |
+
result = agent.step("打开美团搜索附近的火锅店")
|
| 82 |
+
print(f"{msgs['step']} 1: {result.action}")
|
| 83 |
+
|
| 84 |
+
# Continue if not finished
|
| 85 |
+
while not result.finished and agent.step_count < 10:
|
| 86 |
+
result = agent.step()
|
| 87 |
+
print(f"{msgs['step']} {agent.step_count}: {result.action}")
|
| 88 |
+
print(f" {msgs['thinking']}: {result.thinking[:100]}...")
|
| 89 |
+
|
| 90 |
+
print(f"\n{msgs['final_result']}: {result.message}")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def example_multiple_tasks(lang: str = "cn"):
|
| 94 |
+
"""Batch task example / 批量任务示例"""
|
| 95 |
+
msgs = get_messages(lang)
|
| 96 |
+
|
| 97 |
+
agent_config = AgentConfig(lang=lang)
|
| 98 |
+
agent = PhoneAgent(agent_config=agent_config)
|
| 99 |
+
|
| 100 |
+
tasks = [
|
| 101 |
+
"打开高德地图查看实时路况",
|
| 102 |
+
"打开大众点评搜索附近的咖啡店",
|
| 103 |
+
"打开bilibili搜索Python教程",
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
for task in tasks:
|
| 107 |
+
print(f"\n{'=' * 50}")
|
| 108 |
+
print(f"{msgs['task']}: {task}")
|
| 109 |
+
print("=" * 50)
|
| 110 |
+
|
| 111 |
+
result = agent.run(task)
|
| 112 |
+
print(f"{msgs['result']}: {result}")
|
| 113 |
+
|
| 114 |
+
# Reset Agent state
|
| 115 |
+
agent.reset()
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def example_remote_device(lang: str = "cn"):
|
| 119 |
+
"""Remote device example / 远程设备示例"""
|
| 120 |
+
from phone_agent.adb import ADBConnection
|
| 121 |
+
|
| 122 |
+
msgs = get_messages(lang)
|
| 123 |
+
|
| 124 |
+
# Create connection manager
|
| 125 |
+
conn = ADBConnection()
|
| 126 |
+
|
| 127 |
+
# Connect to remote device
|
| 128 |
+
success, message = conn.connect("192.168.1.100:5555")
|
| 129 |
+
if not success:
|
| 130 |
+
print(f"{msgs['connection_failed']}: {message}")
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
print(f"{msgs['connection_successful']}: {message}")
|
| 134 |
+
|
| 135 |
+
# Create Agent with device specified
|
| 136 |
+
agent_config = AgentConfig(
|
| 137 |
+
device_id="192.168.1.100:5555",
|
| 138 |
+
verbose=True,
|
| 139 |
+
lang=lang,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
agent = PhoneAgent(agent_config=agent_config)
|
| 143 |
+
|
| 144 |
+
# Execute task
|
| 145 |
+
result = agent.run("打开微信查看消息")
|
| 146 |
+
print(f"{msgs['task_result']}: {result}")
|
| 147 |
+
|
| 148 |
+
# Disconnect
|
| 149 |
+
conn.disconnect("192.168.1.100:5555")
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
import argparse
|
| 154 |
+
|
| 155 |
+
parser = argparse.ArgumentParser(description="Phone Agent Usage Examples")
|
| 156 |
+
parser.add_argument(
|
| 157 |
+
"--lang",
|
| 158 |
+
type=str,
|
| 159 |
+
default="cn",
|
| 160 |
+
choices=["cn", "en"],
|
| 161 |
+
help="Language for UI messages (cn=Chinese, en=English)",
|
| 162 |
+
)
|
| 163 |
+
args = parser.parse_args()
|
| 164 |
+
|
| 165 |
+
msgs = get_messages(args.lang)
|
| 166 |
+
|
| 167 |
+
print("Phone Agent Usage Examples")
|
| 168 |
+
print("=" * 50)
|
| 169 |
+
|
| 170 |
+
# Run basic example
|
| 171 |
+
print(f"\n1. Basic Task Example")
|
| 172 |
+
print("-" * 30)
|
| 173 |
+
example_basic_task(args.lang)
|
| 174 |
+
|
| 175 |
+
# Uncomment to run other examples
|
| 176 |
+
# print(f"\n2. Task Example with Callbacks")
|
| 177 |
+
# print("-" * 30)
|
| 178 |
+
# example_with_callbacks(args.lang)
|
| 179 |
+
|
| 180 |
+
# print(f"\n3. Step-by-step Example")
|
| 181 |
+
# print("-" * 30)
|
| 182 |
+
# example_step_by_step(args.lang)
|
| 183 |
+
|
| 184 |
+
# print(f"\n4. Batch Task Example")
|
| 185 |
+
# print("-" * 30)
|
| 186 |
+
# example_multiple_tasks(args.lang)
|
| 187 |
+
|
| 188 |
+
# print(f"\n5. Remote Device Example")
|
| 189 |
+
# print("-" * 30)
|
| 190 |
+
# example_remote_device(args.lang)
|
examples/demo_thinking.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Thinking Output Demo / 演示 thinking 输出的示例
|
| 4 |
+
|
| 5 |
+
This script demonstrates how the Agent outputs both thinking process and actions in verbose mode.
|
| 6 |
+
这个脚本展示了在 verbose 模式下,Agent 会同时输出思考过程和执行动作。
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from phone_agent import PhoneAgent
|
| 10 |
+
from phone_agent.agent import AgentConfig
|
| 11 |
+
from phone_agent.config import get_messages
|
| 12 |
+
from phone_agent.model import ModelConfig
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main(lang: str = "cn"):
|
| 16 |
+
msgs = get_messages(lang)
|
| 17 |
+
|
| 18 |
+
print("=" * 60)
|
| 19 |
+
print("Phone Agent - Thinking Demo")
|
| 20 |
+
print("=" * 60)
|
| 21 |
+
|
| 22 |
+
# Configure model
|
| 23 |
+
model_config = ModelConfig(
|
| 24 |
+
base_url="http://localhost:8000/v1",
|
| 25 |
+
model_name="autoglm-phone-9b",
|
| 26 |
+
temperature=0.1,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Configure Agent (verbose=True enables detailed output)
|
| 30 |
+
agent_config = AgentConfig(
|
| 31 |
+
max_steps=10,
|
| 32 |
+
verbose=True,
|
| 33 |
+
lang=lang,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Create Agent
|
| 37 |
+
agent = PhoneAgent(
|
| 38 |
+
model_config=model_config,
|
| 39 |
+
agent_config=agent_config,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Execute task
|
| 43 |
+
print(f"\n📱 {msgs['starting_task']}...\n")
|
| 44 |
+
result = agent.run("打开小红书搜索美食攻略")
|
| 45 |
+
|
| 46 |
+
print("\n" + "=" * 60)
|
| 47 |
+
print(f"📊 {msgs['final_result']}: {result}")
|
| 48 |
+
print("=" * 60)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
import argparse
|
| 53 |
+
|
| 54 |
+
parser = argparse.ArgumentParser(description="Phone Agent Thinking Demo")
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--lang",
|
| 57 |
+
type=str,
|
| 58 |
+
default="cn",
|
| 59 |
+
choices=["cn", "en"],
|
| 60 |
+
help="Language for UI messages (cn=Chinese, en=English)",
|
| 61 |
+
)
|
| 62 |
+
args = parser.parse_args()
|
| 63 |
+
|
| 64 |
+
main(lang=args.lang)
|
ios.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Phone Agent iOS CLI - AI-powered iOS phone automation.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python ios.py [OPTIONS]
|
| 7 |
+
|
| 8 |
+
Environment Variables:
|
| 9 |
+
PHONE_AGENT_BASE_URL: Model API base URL (default: http://localhost:8000/v1)
|
| 10 |
+
PHONE_AGENT_MODEL: Model name (default: autoglm-phone-9b)
|
| 11 |
+
PHONE_AGENT_MAX_STEPS: Maximum steps per task (default: 100)
|
| 12 |
+
PHONE_AGENT_WDA_URL: WebDriverAgent URL (default: http://localhost:8100)
|
| 13 |
+
PHONE_AGENT_DEVICE_ID: iOS device UDID for multi-device setups
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import subprocess
|
| 20 |
+
import sys
|
| 21 |
+
from urllib.parse import urlparse
|
| 22 |
+
|
| 23 |
+
from openai import OpenAI
|
| 24 |
+
|
| 25 |
+
from phone_agent.agent_ios import IOSAgentConfig, IOSPhoneAgent
|
| 26 |
+
from phone_agent.config.apps_ios import list_supported_apps
|
| 27 |
+
from phone_agent.model import ModelConfig
|
| 28 |
+
from phone_agent.xctest import XCTestConnection, list_devices
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def check_system_requirements(wda_url: str = "http://localhost:8100") -> bool:
|
| 32 |
+
"""
|
| 33 |
+
Check system requirements before running the agent.
|
| 34 |
+
|
| 35 |
+
Checks:
|
| 36 |
+
1. libimobiledevice tools installed
|
| 37 |
+
2. At least one iOS device connected
|
| 38 |
+
3. WebDriverAgent is running
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
wda_url: WebDriverAgent URL to check.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
True if all checks pass, False otherwise.
|
| 45 |
+
"""
|
| 46 |
+
print("🔍 Checking system requirements...")
|
| 47 |
+
print("-" * 50)
|
| 48 |
+
|
| 49 |
+
all_passed = True
|
| 50 |
+
|
| 51 |
+
# Check 1: libimobiledevice installed
|
| 52 |
+
print("1. Checking libimobiledevice installation...", end=" ")
|
| 53 |
+
if shutil.which("idevice_id") is None:
|
| 54 |
+
print("❌ FAILED")
|
| 55 |
+
print(" Error: libimobiledevice is not installed or not in PATH.")
|
| 56 |
+
print(" Solution: Install libimobiledevice:")
|
| 57 |
+
print(" - macOS: brew install libimobiledevice")
|
| 58 |
+
print(" - Linux: sudo apt-get install libimobiledevice-utils")
|
| 59 |
+
all_passed = False
|
| 60 |
+
else:
|
| 61 |
+
# Double check by running idevice_id
|
| 62 |
+
try:
|
| 63 |
+
result = subprocess.run(
|
| 64 |
+
["idevice_id", "-ln"], capture_output=True, text=True, timeout=10
|
| 65 |
+
)
|
| 66 |
+
if result.returncode == 0:
|
| 67 |
+
print("✅ OK")
|
| 68 |
+
else:
|
| 69 |
+
print("❌ FAILED")
|
| 70 |
+
print(" Error: idevice_id command failed to run.")
|
| 71 |
+
all_passed = False
|
| 72 |
+
except FileNotFoundError:
|
| 73 |
+
print("❌ FAILED")
|
| 74 |
+
print(" Error: idevice_id command not found.")
|
| 75 |
+
all_passed = False
|
| 76 |
+
except subprocess.TimeoutExpired:
|
| 77 |
+
print("❌ FAILED")
|
| 78 |
+
print(" Error: idevice_id command timed out.")
|
| 79 |
+
all_passed = False
|
| 80 |
+
|
| 81 |
+
# If libimobiledevice is not installed, skip remaining checks
|
| 82 |
+
if not all_passed:
|
| 83 |
+
print("-" * 50)
|
| 84 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 85 |
+
return False
|
| 86 |
+
|
| 87 |
+
# Check 2: iOS Device connected
|
| 88 |
+
print("2. Checking connected iOS devices...", end=" ")
|
| 89 |
+
try:
|
| 90 |
+
devices = list_devices()
|
| 91 |
+
|
| 92 |
+
if not devices:
|
| 93 |
+
print("❌ FAILED")
|
| 94 |
+
print(" Error: No iOS devices connected.")
|
| 95 |
+
print(" Solution:")
|
| 96 |
+
print(" 1. Connect your iOS device via USB")
|
| 97 |
+
print(" 2. Unlock the device and tap 'Trust This Computer'")
|
| 98 |
+
print(" 3. Verify connection: idevice_id -l")
|
| 99 |
+
print(" 4. Or connect via WiFi using device IP")
|
| 100 |
+
all_passed = False
|
| 101 |
+
else:
|
| 102 |
+
device_names = [
|
| 103 |
+
d.device_name or d.device_id[:8] + "..." for d in devices
|
| 104 |
+
]
|
| 105 |
+
print(f"✅ OK ({len(devices)} device(s): {', '.join(device_names)})")
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print("❌ FAILED")
|
| 108 |
+
print(f" Error: {e}")
|
| 109 |
+
all_passed = False
|
| 110 |
+
|
| 111 |
+
# If no device connected, skip WebDriverAgent check
|
| 112 |
+
if not all_passed:
|
| 113 |
+
print("-" * 50)
|
| 114 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
# Check 3: WebDriverAgent running
|
| 118 |
+
print(f"3. Checking WebDriverAgent ({wda_url})...", end=" ")
|
| 119 |
+
try:
|
| 120 |
+
conn = XCTestConnection(wda_url=wda_url)
|
| 121 |
+
|
| 122 |
+
if conn.is_wda_ready():
|
| 123 |
+
print("✅ OK")
|
| 124 |
+
# Get WDA status for additional info
|
| 125 |
+
status = conn.get_wda_status()
|
| 126 |
+
if status:
|
| 127 |
+
session_id = status.get("sessionId", "N/A")
|
| 128 |
+
print(f" Session ID: {session_id}")
|
| 129 |
+
else:
|
| 130 |
+
print("❌ FAILED")
|
| 131 |
+
print(" Error: WebDriverAgent is not running or not accessible.")
|
| 132 |
+
print(" Solution:")
|
| 133 |
+
print(" 1. Run WebDriverAgent on your iOS device via Xcode")
|
| 134 |
+
print(" 2. For USB: Set up port forwarding: iproxy 8100 8100")
|
| 135 |
+
print(
|
| 136 |
+
" 3. For WiFi: Use device IP, e.g., --wda-url http://192.168.1.100:8100"
|
| 137 |
+
)
|
| 138 |
+
print(" 4. Verify in browser: open http://localhost:8100/status")
|
| 139 |
+
print("\n Quick setup guide:")
|
| 140 |
+
print(
|
| 141 |
+
" git clone https://github.com/appium/WebDriverAgent.git && cd WebDriverAgent"
|
| 142 |
+
)
|
| 143 |
+
print(" ./Scripts/bootstrap.sh")
|
| 144 |
+
print(" open WebDriverAgent.xcodeproj")
|
| 145 |
+
print(" # Configure signing, then Product > Test (Cmd+U)")
|
| 146 |
+
all_passed = False
|
| 147 |
+
except Exception as e:
|
| 148 |
+
print("❌ FAILED")
|
| 149 |
+
print(f" Error: {e}")
|
| 150 |
+
all_passed = False
|
| 151 |
+
|
| 152 |
+
print("-" * 50)
|
| 153 |
+
|
| 154 |
+
if all_passed:
|
| 155 |
+
print("✅ All system checks passed!\n")
|
| 156 |
+
else:
|
| 157 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 158 |
+
|
| 159 |
+
return all_passed
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def check_model_api(base_url: str, api_key: str, model_name: str) -> bool:
|
| 163 |
+
"""
|
| 164 |
+
Check if the model API is accessible and the specified model exists.
|
| 165 |
+
|
| 166 |
+
Checks:
|
| 167 |
+
1. Network connectivity to the API endpoint
|
| 168 |
+
2. Model exists in the available models list
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
base_url: The API base URL
|
| 172 |
+
model_name: The model name to check
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
True if all checks pass, False otherwise.
|
| 176 |
+
"""
|
| 177 |
+
print("🔍 Checking model API...")
|
| 178 |
+
print("-" * 50)
|
| 179 |
+
|
| 180 |
+
all_passed = True
|
| 181 |
+
|
| 182 |
+
# Check 1: Network connectivity
|
| 183 |
+
print(f"1. Checking API connectivity ({base_url})...", end=" ")
|
| 184 |
+
try:
|
| 185 |
+
# Parse the URL to get host and port
|
| 186 |
+
parsed = urlparse(base_url)
|
| 187 |
+
|
| 188 |
+
# Create OpenAI client
|
| 189 |
+
client = OpenAI(base_url=base_url, api_key=api_key, timeout=10.0)
|
| 190 |
+
|
| 191 |
+
# Try to list models (this tests connectivity)
|
| 192 |
+
models_response = client.models.list()
|
| 193 |
+
available_models = [model.id for model in models_response.data]
|
| 194 |
+
|
| 195 |
+
print("✅ OK")
|
| 196 |
+
|
| 197 |
+
# Check 2: Model exists
|
| 198 |
+
print(f"2. Checking model '{model_name}'...", end=" ")
|
| 199 |
+
if model_name in available_models:
|
| 200 |
+
print("✅ OK")
|
| 201 |
+
else:
|
| 202 |
+
print("❌ FAILED")
|
| 203 |
+
print(f" Error: Model '{model_name}' not found.")
|
| 204 |
+
print(f" Available models:")
|
| 205 |
+
for m in available_models[:10]: # Show first 10 models
|
| 206 |
+
print(f" - {m}")
|
| 207 |
+
if len(available_models) > 10:
|
| 208 |
+
print(f" ... and {len(available_models) - 10} more")
|
| 209 |
+
all_passed = False
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
print("❌ FAILED")
|
| 213 |
+
error_msg = str(e)
|
| 214 |
+
|
| 215 |
+
# Provide more specific error messages
|
| 216 |
+
if "Connection refused" in error_msg or "Connection error" in error_msg:
|
| 217 |
+
print(f" Error: Cannot connect to {base_url}")
|
| 218 |
+
print(" Solution:")
|
| 219 |
+
print(" 1. Check if the model server is running")
|
| 220 |
+
print(" 2. Verify the base URL is correct")
|
| 221 |
+
print(f" 3. Try: curl {base_url}/models")
|
| 222 |
+
elif "timed out" in error_msg.lower() or "timeout" in error_msg.lower():
|
| 223 |
+
print(f" Error: Connection to {base_url} timed out")
|
| 224 |
+
print(" Solution:")
|
| 225 |
+
print(" 1. Check your network connection")
|
| 226 |
+
print(" 2. Verify the server is responding")
|
| 227 |
+
elif (
|
| 228 |
+
"Name or service not known" in error_msg
|
| 229 |
+
or "nodename nor servname" in error_msg
|
| 230 |
+
):
|
| 231 |
+
print(f" Error: Cannot resolve hostname")
|
| 232 |
+
print(" Solution:")
|
| 233 |
+
print(" 1. Check the URL is correct")
|
| 234 |
+
print(" 2. Verify DNS settings")
|
| 235 |
+
else:
|
| 236 |
+
print(f" Error: {error_msg}")
|
| 237 |
+
|
| 238 |
+
all_passed = False
|
| 239 |
+
|
| 240 |
+
print("-" * 50)
|
| 241 |
+
|
| 242 |
+
if all_passed:
|
| 243 |
+
print("✅ Model API checks passed!\n")
|
| 244 |
+
else:
|
| 245 |
+
print("❌ Model API check failed. Please fix the issues above.")
|
| 246 |
+
|
| 247 |
+
return all_passed
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def parse_args() -> argparse.Namespace:
|
| 251 |
+
"""Parse command line arguments."""
|
| 252 |
+
parser = argparse.ArgumentParser(
|
| 253 |
+
description="Phone Agent iOS - AI-powered iOS phone automation",
|
| 254 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 255 |
+
epilog="""
|
| 256 |
+
Examples:
|
| 257 |
+
# Run with default settings
|
| 258 |
+
python ios.py
|
| 259 |
+
|
| 260 |
+
# Specify model endpoint
|
| 261 |
+
python ios.py --base-url http://localhost:8000/v1
|
| 262 |
+
|
| 263 |
+
# Run with specific device
|
| 264 |
+
python ios.py --device-id <UDID>
|
| 265 |
+
|
| 266 |
+
# Use WiFi connection
|
| 267 |
+
python ios.py --wda-url http://192.168.1.100:8100
|
| 268 |
+
|
| 269 |
+
# List connected devices
|
| 270 |
+
python ios.py --list-devices
|
| 271 |
+
|
| 272 |
+
# Check device pairing status
|
| 273 |
+
python ios.py --pair
|
| 274 |
+
|
| 275 |
+
# List supported apps
|
| 276 |
+
python ios.py --list-apps
|
| 277 |
+
|
| 278 |
+
# Run a specific task
|
| 279 |
+
python ios.py "Open Safari and search for iPhone tips"
|
| 280 |
+
""",
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Model options
|
| 284 |
+
parser.add_argument(
|
| 285 |
+
"--base-url",
|
| 286 |
+
type=str,
|
| 287 |
+
default=os.getenv("PHONE_AGENT_BASE_URL", "http://localhost:8000/v1"),
|
| 288 |
+
help="Model API base URL",
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
parser.add_argument(
|
| 292 |
+
"--api-key",
|
| 293 |
+
type=str,
|
| 294 |
+
default="EMPTY",
|
| 295 |
+
help="Model API KEY",
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
parser.add_argument(
|
| 299 |
+
"--model",
|
| 300 |
+
type=str,
|
| 301 |
+
default=os.getenv("PHONE_AGENT_MODEL", "autoglm-phone-9b"),
|
| 302 |
+
help="Model name",
|
| 303 |
+
)
|
| 304 |
+
|
| 305 |
+
parser.add_argument(
|
| 306 |
+
"--max-steps",
|
| 307 |
+
type=int,
|
| 308 |
+
default=int(os.getenv("PHONE_AGENT_MAX_STEPS", "100")),
|
| 309 |
+
help="Maximum steps per task",
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
# iOS Device options
|
| 313 |
+
parser.add_argument(
|
| 314 |
+
"--device-id",
|
| 315 |
+
"-d",
|
| 316 |
+
type=str,
|
| 317 |
+
default=os.getenv("PHONE_AGENT_DEVICE_ID"),
|
| 318 |
+
help="iOS device UDID",
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
parser.add_argument(
|
| 322 |
+
"--wda-url",
|
| 323 |
+
type=str,
|
| 324 |
+
default=os.getenv("PHONE_AGENT_WDA_URL", "http://localhost:8100"),
|
| 325 |
+
help="WebDriverAgent URL (default: http://localhost:8100)",
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
parser.add_argument(
|
| 329 |
+
"--list-devices", action="store_true", help="List connected iOS devices and exit"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
parser.add_argument(
|
| 333 |
+
"--pair",
|
| 334 |
+
action="store_true",
|
| 335 |
+
help="Pair with iOS device (required for some operations)",
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
parser.add_argument(
|
| 339 |
+
"--wda-status",
|
| 340 |
+
action="store_true",
|
| 341 |
+
help="Show WebDriverAgent status and exit",
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
# Other options
|
| 345 |
+
parser.add_argument(
|
| 346 |
+
"--quiet", "-q", action="store_true", help="Suppress verbose output"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
parser.add_argument(
|
| 350 |
+
"--list-apps", action="store_true", help="List supported apps and exit"
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
parser.add_argument(
|
| 354 |
+
"--lang",
|
| 355 |
+
type=str,
|
| 356 |
+
choices=["cn", "en"],
|
| 357 |
+
default=os.getenv("PHONE_AGENT_LANG", "cn"),
|
| 358 |
+
help="Language for system prompt (cn or en, default: cn)",
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
parser.add_argument(
|
| 362 |
+
"task",
|
| 363 |
+
nargs="?",
|
| 364 |
+
type=str,
|
| 365 |
+
help="Task to execute (interactive mode if not provided)",
|
| 366 |
+
)
|
| 367 |
+
|
| 368 |
+
return parser.parse_args()
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def handle_device_commands(args) -> bool:
|
| 372 |
+
"""
|
| 373 |
+
Handle iOS device-related commands.
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
True if a device command was handled (should exit), False otherwise.
|
| 377 |
+
"""
|
| 378 |
+
conn = XCTestConnection(wda_url=args.wda_url)
|
| 379 |
+
|
| 380 |
+
# Handle --list-devices
|
| 381 |
+
if args.list_devices:
|
| 382 |
+
devices = list_devices()
|
| 383 |
+
if not devices:
|
| 384 |
+
print("No iOS devices connected.")
|
| 385 |
+
print("\nTroubleshooting:")
|
| 386 |
+
print(" 1. Connect device via USB")
|
| 387 |
+
print(" 2. Unlock device and trust this computer")
|
| 388 |
+
print(" 3. Run: idevice_id -l")
|
| 389 |
+
else:
|
| 390 |
+
print("Connected iOS devices:")
|
| 391 |
+
print("-" * 70)
|
| 392 |
+
for device in devices:
|
| 393 |
+
conn_type = device.connection_type.value
|
| 394 |
+
model_info = f"{device.model}" if device.model else "Unknown"
|
| 395 |
+
ios_info = f"iOS {device.ios_version}" if device.ios_version else ""
|
| 396 |
+
name_info = device.device_name or "Unnamed"
|
| 397 |
+
|
| 398 |
+
print(f" ✓ {name_info}")
|
| 399 |
+
print(f" UDID: {device.device_id}")
|
| 400 |
+
print(f" Model: {model_info}")
|
| 401 |
+
print(f" OS: {ios_info}")
|
| 402 |
+
print(f" Connection: {conn_type}")
|
| 403 |
+
print("-" * 70)
|
| 404 |
+
return True
|
| 405 |
+
|
| 406 |
+
# Handle --pair
|
| 407 |
+
if args.pair:
|
| 408 |
+
print("Pairing with iOS device...")
|
| 409 |
+
success, message = conn.pair_device(args.device_id)
|
| 410 |
+
print(f"{'✓' if success else '✗'} {message}")
|
| 411 |
+
return True
|
| 412 |
+
|
| 413 |
+
# Handle --wda-status
|
| 414 |
+
if args.wda_status:
|
| 415 |
+
print(f"Checking WebDriverAgent status at {args.wda_url}...")
|
| 416 |
+
print("-" * 50)
|
| 417 |
+
|
| 418 |
+
if conn.is_wda_ready():
|
| 419 |
+
print("✓ WebDriverAgent is running")
|
| 420 |
+
|
| 421 |
+
status = conn.get_wda_status()
|
| 422 |
+
if status:
|
| 423 |
+
print(f"\nStatus details:")
|
| 424 |
+
value = status.get("value", {})
|
| 425 |
+
print(f" Session ID: {status.get('sessionId', 'N/A')}")
|
| 426 |
+
print(f" Build: {value.get('build', {}).get('time', 'N/A')}")
|
| 427 |
+
|
| 428 |
+
current_app = value.get("currentApp", {})
|
| 429 |
+
if current_app:
|
| 430 |
+
print(f"\nCurrent App:")
|
| 431 |
+
print(f" Bundle ID: {current_app.get('bundleId', 'N/A')}")
|
| 432 |
+
print(f" Process ID: {current_app.get('pid', 'N/A')}")
|
| 433 |
+
else:
|
| 434 |
+
print("✗ WebDriverAgent is not running")
|
| 435 |
+
print("\nPlease start WebDriverAgent on your iOS device:")
|
| 436 |
+
print(" 1. Open WebDriverAgent.xcodeproj in Xcode")
|
| 437 |
+
print(" 2. Select your device")
|
| 438 |
+
print(" 3. Run WebDriverAgentRunner (Product > Test or Cmd+U)")
|
| 439 |
+
print(f" 4. For USB: Run port forwarding: iproxy 8100 8100")
|
| 440 |
+
|
| 441 |
+
return True
|
| 442 |
+
|
| 443 |
+
return False
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def main():
|
| 447 |
+
"""Main entry point."""
|
| 448 |
+
args = parse_args()
|
| 449 |
+
|
| 450 |
+
# Handle --list-apps (no system check needed)
|
| 451 |
+
if args.list_apps:
|
| 452 |
+
print("Supported iOS apps:")
|
| 453 |
+
print("\nNote: For iOS apps, Bundle IDs are configured in:")
|
| 454 |
+
print(" phone_agent/config/apps_ios.py")
|
| 455 |
+
print("\nCurrently configured apps:")
|
| 456 |
+
for app in sorted(list_supported_apps()):
|
| 457 |
+
print(f" - {app}")
|
| 458 |
+
print(
|
| 459 |
+
"\nTo add iOS apps, find the Bundle ID and add to APP_PACKAGES_IOS dictionary."
|
| 460 |
+
)
|
| 461 |
+
return
|
| 462 |
+
|
| 463 |
+
# Handle device commands (these may need partial system checks)
|
| 464 |
+
if handle_device_commands(args):
|
| 465 |
+
return
|
| 466 |
+
|
| 467 |
+
# Run system requirements check before proceeding
|
| 468 |
+
if not check_system_requirements(wda_url=args.wda_url):
|
| 469 |
+
sys.exit(1)
|
| 470 |
+
|
| 471 |
+
# Check model API connectivity and model availability
|
| 472 |
+
# if not check_model_api(args.base_url, args.api_key, args.model):
|
| 473 |
+
# sys.exit(1)
|
| 474 |
+
|
| 475 |
+
# Create configurations
|
| 476 |
+
model_config = ModelConfig(
|
| 477 |
+
base_url=args.base_url,
|
| 478 |
+
model_name=args.model,
|
| 479 |
+
api_key=args.api_key
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
agent_config = IOSAgentConfig(
|
| 483 |
+
max_steps=args.max_steps,
|
| 484 |
+
wda_url=args.wda_url,
|
| 485 |
+
device_id=args.device_id,
|
| 486 |
+
verbose=not args.quiet,
|
| 487 |
+
lang=args.lang,
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
# Create iOS agent
|
| 491 |
+
agent = IOSPhoneAgent(
|
| 492 |
+
model_config=model_config,
|
| 493 |
+
agent_config=agent_config,
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
# Print header
|
| 497 |
+
print("=" * 50)
|
| 498 |
+
print("Phone Agent iOS - AI-powered iOS automation")
|
| 499 |
+
print("=" * 50)
|
| 500 |
+
print(f"Model: {model_config.model_name}")
|
| 501 |
+
print(f"Base URL: {model_config.base_url}")
|
| 502 |
+
print(f"WDA URL: {args.wda_url}")
|
| 503 |
+
print(f"Max Steps: {agent_config.max_steps}")
|
| 504 |
+
print(f"Language: {agent_config.lang}")
|
| 505 |
+
|
| 506 |
+
# Show device info
|
| 507 |
+
devices = list_devices()
|
| 508 |
+
if agent_config.device_id:
|
| 509 |
+
print(f"Device: {agent_config.device_id}")
|
| 510 |
+
elif devices:
|
| 511 |
+
device = devices[0]
|
| 512 |
+
print(f"Device: {device.device_name or device.device_id[:16]}")
|
| 513 |
+
print(f" {device.model}, iOS {device.ios_version}")
|
| 514 |
+
|
| 515 |
+
print("=" * 50)
|
| 516 |
+
|
| 517 |
+
# Run with provided task or enter interactive mode
|
| 518 |
+
if args.task:
|
| 519 |
+
print(f"\nTask: {args.task}\n")
|
| 520 |
+
result = agent.run(args.task)
|
| 521 |
+
print(f"\nResult: {result}")
|
| 522 |
+
else:
|
| 523 |
+
# Interactive mode
|
| 524 |
+
print("\nEntering interactive mode. Type 'quit' to exit.\n")
|
| 525 |
+
|
| 526 |
+
while True:
|
| 527 |
+
try:
|
| 528 |
+
task = input("Enter your task: ").strip()
|
| 529 |
+
|
| 530 |
+
if task.lower() in ("quit", "exit", "q"):
|
| 531 |
+
print("Goodbye!")
|
| 532 |
+
break
|
| 533 |
+
|
| 534 |
+
if not task:
|
| 535 |
+
continue
|
| 536 |
+
|
| 537 |
+
print()
|
| 538 |
+
result = agent.run(task)
|
| 539 |
+
print(f"\nResult: {result}\n")
|
| 540 |
+
agent.reset()
|
| 541 |
+
|
| 542 |
+
except KeyboardInterrupt:
|
| 543 |
+
print("\n\nInterrupted. Goodbye!")
|
| 544 |
+
break
|
| 545 |
+
except Exception as e:
|
| 546 |
+
print(f"\nError: {e}\n")
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
if __name__ == "__main__":
|
| 550 |
+
main()
|
main.py
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Phone Agent CLI - AI-powered phone automation.
|
| 4 |
+
|
| 5 |
+
Usage:
|
| 6 |
+
python main.py [OPTIONS]
|
| 7 |
+
|
| 8 |
+
Environment Variables:
|
| 9 |
+
PHONE_AGENT_BASE_URL: Model API base URL (default: http://localhost:8000/v1)
|
| 10 |
+
PHONE_AGENT_MODEL: Model name (default: autoglm-phone-9b)
|
| 11 |
+
PHONE_AGENT_API_KEY: API key for model authentication (default: EMPTY)
|
| 12 |
+
PHONE_AGENT_MAX_STEPS: Maximum steps per task (default: 100)
|
| 13 |
+
PHONE_AGENT_DEVICE_ID: ADB device ID for multi-device setups
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import os
|
| 18 |
+
import shutil
|
| 19 |
+
import subprocess
|
| 20 |
+
import sys
|
| 21 |
+
from urllib.parse import urlparse
|
| 22 |
+
|
| 23 |
+
from openai import OpenAI
|
| 24 |
+
|
| 25 |
+
from phone_agent import PhoneAgent
|
| 26 |
+
from phone_agent.agent import AgentConfig
|
| 27 |
+
from phone_agent.agent_ios import IOSAgentConfig, IOSPhoneAgent
|
| 28 |
+
from phone_agent.config.apps import list_supported_apps
|
| 29 |
+
from phone_agent.config.apps_harmonyos import list_supported_apps as list_harmonyos_apps
|
| 30 |
+
from phone_agent.config.apps_ios import list_supported_apps as list_ios_apps
|
| 31 |
+
from phone_agent.device_factory import DeviceType, get_device_factory, set_device_type
|
| 32 |
+
from phone_agent.model import ModelConfig
|
| 33 |
+
from phone_agent.xctest import XCTestConnection
|
| 34 |
+
from phone_agent.xctest import list_devices as list_ios_devices
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def check_system_requirements(
|
| 38 |
+
device_type: DeviceType = DeviceType.ADB, wda_url: str = "http://localhost:8100"
|
| 39 |
+
) -> bool:
|
| 40 |
+
"""
|
| 41 |
+
Check system requirements before running the agent.
|
| 42 |
+
|
| 43 |
+
Checks:
|
| 44 |
+
1. ADB/HDC/iOS tools installed
|
| 45 |
+
2. At least one device connected
|
| 46 |
+
3. ADB Keyboard installed on the device (for ADB only)
|
| 47 |
+
4. WebDriverAgent running (for iOS only)
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
device_type: Type of device tool (ADB, HDC, or IOS).
|
| 51 |
+
wda_url: WebDriverAgent URL (for iOS only).
|
| 52 |
+
|
| 53 |
+
Returns:
|
| 54 |
+
True if all checks pass, False otherwise.
|
| 55 |
+
"""
|
| 56 |
+
print("🔍 Checking system requirements...")
|
| 57 |
+
print("-" * 50)
|
| 58 |
+
|
| 59 |
+
all_passed = True
|
| 60 |
+
|
| 61 |
+
# Determine tool name and command
|
| 62 |
+
if device_type == DeviceType.IOS:
|
| 63 |
+
tool_name = "libimobiledevice"
|
| 64 |
+
tool_cmd = "idevice_id"
|
| 65 |
+
else:
|
| 66 |
+
tool_name = "ADB" if device_type == DeviceType.ADB else "HDC"
|
| 67 |
+
tool_cmd = "adb" if device_type == DeviceType.ADB else "hdc"
|
| 68 |
+
|
| 69 |
+
# Check 1: Tool installed
|
| 70 |
+
print(f"1. Checking {tool_name} installation...", end=" ")
|
| 71 |
+
if shutil.which(tool_cmd) is None:
|
| 72 |
+
print("❌ FAILED")
|
| 73 |
+
print(f" Error: {tool_name} is not installed or not in PATH.")
|
| 74 |
+
print(f" Solution: Install {tool_name}:")
|
| 75 |
+
if device_type == DeviceType.ADB:
|
| 76 |
+
print(" - macOS: brew install android-platform-tools")
|
| 77 |
+
print(" - Linux: sudo apt install android-tools-adb")
|
| 78 |
+
print(
|
| 79 |
+
" - Windows: Download from https://developer.android.com/studio/releases/platform-tools"
|
| 80 |
+
)
|
| 81 |
+
elif device_type == DeviceType.HDC:
|
| 82 |
+
print(
|
| 83 |
+
" - Download from HarmonyOS SDK or https://gitee.com/openharmony/docs"
|
| 84 |
+
)
|
| 85 |
+
print(" - Add to PATH environment variable")
|
| 86 |
+
else: # IOS
|
| 87 |
+
print(" - macOS: brew install libimobiledevice")
|
| 88 |
+
print(" - Linux: sudo apt-get install libimobiledevice-utils")
|
| 89 |
+
all_passed = False
|
| 90 |
+
else:
|
| 91 |
+
# Double check by running version command
|
| 92 |
+
try:
|
| 93 |
+
if device_type == DeviceType.ADB:
|
| 94 |
+
version_cmd = [tool_cmd, "version"]
|
| 95 |
+
elif device_type == DeviceType.HDC:
|
| 96 |
+
version_cmd = [tool_cmd, "-v"]
|
| 97 |
+
else: # IOS
|
| 98 |
+
version_cmd = [tool_cmd, "-ln"]
|
| 99 |
+
|
| 100 |
+
result = subprocess.run(
|
| 101 |
+
version_cmd, capture_output=True, text=True, timeout=10
|
| 102 |
+
)
|
| 103 |
+
if result.returncode == 0:
|
| 104 |
+
version_line = result.stdout.strip().split("\n")[0]
|
| 105 |
+
print(f"✅ OK ({version_line if version_line else 'installed'})")
|
| 106 |
+
else:
|
| 107 |
+
print("❌ FAILED")
|
| 108 |
+
print(f" Error: {tool_name} command failed to run.")
|
| 109 |
+
all_passed = False
|
| 110 |
+
except FileNotFoundError:
|
| 111 |
+
print("❌ FAILED")
|
| 112 |
+
print(f" Error: {tool_name} command not found.")
|
| 113 |
+
all_passed = False
|
| 114 |
+
except subprocess.TimeoutExpired:
|
| 115 |
+
print("❌ FAILED")
|
| 116 |
+
print(f" Error: {tool_name} command timed out.")
|
| 117 |
+
all_passed = False
|
| 118 |
+
|
| 119 |
+
# If ADB is not installed, skip remaining checks
|
| 120 |
+
if not all_passed:
|
| 121 |
+
print("-" * 50)
|
| 122 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 123 |
+
return False
|
| 124 |
+
|
| 125 |
+
# Check 2: Device connected
|
| 126 |
+
print("2. Checking connected devices...", end=" ")
|
| 127 |
+
try:
|
| 128 |
+
if device_type == DeviceType.ADB:
|
| 129 |
+
result = subprocess.run(
|
| 130 |
+
["adb", "devices"], capture_output=True, text=True, timeout=10
|
| 131 |
+
)
|
| 132 |
+
lines = result.stdout.strip().split("\n")
|
| 133 |
+
# Filter out header and empty lines, look for 'device' status
|
| 134 |
+
devices = [
|
| 135 |
+
line for line in lines[1:] if line.strip() and "\tdevice" in line
|
| 136 |
+
]
|
| 137 |
+
elif device_type == DeviceType.HDC:
|
| 138 |
+
result = subprocess.run(
|
| 139 |
+
["hdc", "list", "targets"], capture_output=True, text=True, timeout=10
|
| 140 |
+
)
|
| 141 |
+
lines = result.stdout.strip().split("\n")
|
| 142 |
+
devices = [line for line in lines if line.strip()]
|
| 143 |
+
else: # IOS
|
| 144 |
+
ios_devices = list_ios_devices()
|
| 145 |
+
devices = [d.device_id for d in ios_devices]
|
| 146 |
+
|
| 147 |
+
if not devices:
|
| 148 |
+
print("❌ FAILED")
|
| 149 |
+
print(" Error: No devices connected.")
|
| 150 |
+
print(" Solution:")
|
| 151 |
+
if device_type == DeviceType.ADB:
|
| 152 |
+
print(" 1. Enable USB debugging on your Android device")
|
| 153 |
+
print(" 2. Connect via USB and authorize the connection")
|
| 154 |
+
print(
|
| 155 |
+
" 3. Or connect remotely: python main.py --connect <ip>:<port>"
|
| 156 |
+
)
|
| 157 |
+
elif device_type == DeviceType.HDC:
|
| 158 |
+
print(" 1. Enable USB debugging on your HarmonyOS device")
|
| 159 |
+
print(" 2. Connect via USB and authorize the connection")
|
| 160 |
+
print(
|
| 161 |
+
" 3. Or connect remotely: python main.py --device-type hdc --connect <ip>:<port>"
|
| 162 |
+
)
|
| 163 |
+
else: # IOS
|
| 164 |
+
print(" 1. Connect your iOS device via USB")
|
| 165 |
+
print(" 2. Unlock device and tap 'Trust This Computer'")
|
| 166 |
+
print(" 3. Verify: idevice_id -l")
|
| 167 |
+
print(" 4. Or connect via WiFi using device IP")
|
| 168 |
+
all_passed = False
|
| 169 |
+
else:
|
| 170 |
+
if device_type == DeviceType.ADB:
|
| 171 |
+
device_ids = [d.split("\t")[0] for d in devices]
|
| 172 |
+
elif device_type == DeviceType.HDC:
|
| 173 |
+
device_ids = [d.strip() for d in devices]
|
| 174 |
+
else: # IOS
|
| 175 |
+
device_ids = devices
|
| 176 |
+
print(
|
| 177 |
+
f"✅ OK ({len(devices)} device(s): {', '.join(device_ids[:2])}{'...' if len(device_ids) > 2 else ''})"
|
| 178 |
+
)
|
| 179 |
+
except subprocess.TimeoutExpired:
|
| 180 |
+
print("❌ FAILED")
|
| 181 |
+
print(f" Error: {tool_name} command timed out.")
|
| 182 |
+
all_passed = False
|
| 183 |
+
except Exception as e:
|
| 184 |
+
print("❌ FAILED")
|
| 185 |
+
print(f" Error: {e}")
|
| 186 |
+
all_passed = False
|
| 187 |
+
|
| 188 |
+
# If no device connected, skip ADB Keyboard check
|
| 189 |
+
if not all_passed:
|
| 190 |
+
print("-" * 50)
|
| 191 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 192 |
+
return False
|
| 193 |
+
|
| 194 |
+
# Check 3: ADB Keyboard installed (only for ADB) or WebDriverAgent (for iOS)
|
| 195 |
+
if device_type == DeviceType.ADB:
|
| 196 |
+
print("3. Checking ADB Keyboard...", end=" ")
|
| 197 |
+
try:
|
| 198 |
+
result = subprocess.run(
|
| 199 |
+
["adb", "shell", "ime", "list", "-s"],
|
| 200 |
+
capture_output=True,
|
| 201 |
+
text=True,
|
| 202 |
+
timeout=10,
|
| 203 |
+
)
|
| 204 |
+
ime_list = result.stdout.strip()
|
| 205 |
+
|
| 206 |
+
if "com.android.adbkeyboard/.AdbIME" in ime_list:
|
| 207 |
+
print("✅ OK")
|
| 208 |
+
else:
|
| 209 |
+
print("❌ FAILED")
|
| 210 |
+
print(" Error: ADB Keyboard is not installed on the device.")
|
| 211 |
+
print(" Solution:")
|
| 212 |
+
print(" 1. Download ADB Keyboard APK from:")
|
| 213 |
+
print(
|
| 214 |
+
" https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk"
|
| 215 |
+
)
|
| 216 |
+
print(" 2. Install it on your device: adb install ADBKeyboard.apk")
|
| 217 |
+
print(
|
| 218 |
+
" 3. Enable it in Settings > System > Languages & Input > Virtual Keyboard"
|
| 219 |
+
)
|
| 220 |
+
all_passed = False
|
| 221 |
+
except subprocess.TimeoutExpired:
|
| 222 |
+
print("❌ FAILED")
|
| 223 |
+
print(" Error: ADB command timed out.")
|
| 224 |
+
all_passed = False
|
| 225 |
+
except Exception as e:
|
| 226 |
+
print("❌ FAILED")
|
| 227 |
+
print(f" Error: {e}")
|
| 228 |
+
all_passed = False
|
| 229 |
+
elif device_type == DeviceType.HDC:
|
| 230 |
+
# For HDC, skip keyboard check as it uses different input method
|
| 231 |
+
print("3. Skipping keyboard check for HarmonyOS...", end=" ")
|
| 232 |
+
print("✅ OK (using native input)")
|
| 233 |
+
else: # IOS
|
| 234 |
+
# Check WebDriverAgent
|
| 235 |
+
print(f"3. Checking WebDriverAgent ({wda_url})...", end=" ")
|
| 236 |
+
try:
|
| 237 |
+
conn = XCTestConnection(wda_url=wda_url)
|
| 238 |
+
|
| 239 |
+
if conn.is_wda_ready():
|
| 240 |
+
print("✅ OK")
|
| 241 |
+
# Get WDA status for additional info
|
| 242 |
+
status = conn.get_wda_status()
|
| 243 |
+
if status:
|
| 244 |
+
session_id = status.get("sessionId", "N/A")
|
| 245 |
+
print(f" Session ID: {session_id}")
|
| 246 |
+
else:
|
| 247 |
+
print("❌ FAILED")
|
| 248 |
+
print(" Error: WebDriverAgent is not running or not accessible.")
|
| 249 |
+
print(" Solution:")
|
| 250 |
+
print(" 1. Run WebDriverAgent on your iOS device via Xcode")
|
| 251 |
+
print(" 2. For USB: Set up port forwarding: iproxy 8100 8100")
|
| 252 |
+
print(
|
| 253 |
+
" 3. For WiFi: Use device IP, e.g., --wda-url http://192.168.1.100:8100"
|
| 254 |
+
)
|
| 255 |
+
print(" 4. Verify in browser: open http://localhost:8100/status")
|
| 256 |
+
all_passed = False
|
| 257 |
+
except Exception as e:
|
| 258 |
+
print("❌ FAILED")
|
| 259 |
+
print(f" Error: {e}")
|
| 260 |
+
all_passed = False
|
| 261 |
+
|
| 262 |
+
print("-" * 50)
|
| 263 |
+
|
| 264 |
+
if all_passed:
|
| 265 |
+
print("✅ All system checks passed!\n")
|
| 266 |
+
else:
|
| 267 |
+
print("❌ System check failed. Please fix the issues above.")
|
| 268 |
+
|
| 269 |
+
return all_passed
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def check_model_api(base_url: str, model_name: str, api_key: str = "EMPTY") -> bool:
|
| 273 |
+
"""
|
| 274 |
+
Check if the model API is accessible and the specified model exists.
|
| 275 |
+
|
| 276 |
+
Checks:
|
| 277 |
+
1. Network connectivity to the API endpoint
|
| 278 |
+
2. Model exists in the available models list
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
base_url: The API base URL
|
| 282 |
+
model_name: The model name to check
|
| 283 |
+
api_key: The API key for authentication
|
| 284 |
+
|
| 285 |
+
Returns:
|
| 286 |
+
True if all checks pass, False otherwise.
|
| 287 |
+
"""
|
| 288 |
+
print("🔍 Checking model API...")
|
| 289 |
+
print("-" * 50)
|
| 290 |
+
|
| 291 |
+
all_passed = True
|
| 292 |
+
|
| 293 |
+
# Check 1: Network connectivity using chat API
|
| 294 |
+
print(f"1. Checking API connectivity ({base_url})...", end=" ")
|
| 295 |
+
try:
|
| 296 |
+
# Create OpenAI client
|
| 297 |
+
client = OpenAI(base_url=base_url, api_key=api_key, timeout=30.0)
|
| 298 |
+
|
| 299 |
+
# Use chat completion to test connectivity (more universally supported than /models)
|
| 300 |
+
response = client.chat.completions.create(
|
| 301 |
+
model=model_name,
|
| 302 |
+
messages=[{"role": "user", "content": "Hi"}],
|
| 303 |
+
max_tokens=5,
|
| 304 |
+
temperature=0.0,
|
| 305 |
+
stream=False,
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Check if we got a valid response
|
| 309 |
+
if response.choices and len(response.choices) > 0:
|
| 310 |
+
print("✅ OK")
|
| 311 |
+
else:
|
| 312 |
+
print("❌ FAILED")
|
| 313 |
+
print(" Error: Received empty response from API")
|
| 314 |
+
all_passed = False
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
print("❌ FAILED")
|
| 318 |
+
error_msg = str(e)
|
| 319 |
+
|
| 320 |
+
# Provide more specific error messages
|
| 321 |
+
if "Connection refused" in error_msg or "Connection error" in error_msg:
|
| 322 |
+
print(f" Error: Cannot connect to {base_url}")
|
| 323 |
+
print(" Solution:")
|
| 324 |
+
print(" 1. Check if the model server is running")
|
| 325 |
+
print(" 2. Verify the base URL is correct")
|
| 326 |
+
print(f" 3. Try: curl {base_url}/chat/completions")
|
| 327 |
+
elif "timed out" in error_msg.lower() or "timeout" in error_msg.lower():
|
| 328 |
+
print(f" Error: Connection to {base_url} timed out")
|
| 329 |
+
print(" Solution:")
|
| 330 |
+
print(" 1. Check your network connection")
|
| 331 |
+
print(" 2. Verify the server is responding")
|
| 332 |
+
elif (
|
| 333 |
+
"Name or service not known" in error_msg
|
| 334 |
+
or "nodename nor servname" in error_msg
|
| 335 |
+
):
|
| 336 |
+
print(f" Error: Cannot resolve hostname")
|
| 337 |
+
print(" Solution:")
|
| 338 |
+
print(" 1. Check the URL is correct")
|
| 339 |
+
print(" 2. Verify DNS settings")
|
| 340 |
+
else:
|
| 341 |
+
print(f" Error: {error_msg}")
|
| 342 |
+
|
| 343 |
+
all_passed = False
|
| 344 |
+
|
| 345 |
+
print("-" * 50)
|
| 346 |
+
|
| 347 |
+
if all_passed:
|
| 348 |
+
print("✅ Model API checks passed!\n")
|
| 349 |
+
else:
|
| 350 |
+
print("❌ Model API check failed. Please fix the issues above.")
|
| 351 |
+
|
| 352 |
+
return all_passed
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def parse_args() -> argparse.Namespace:
|
| 356 |
+
"""Parse command line arguments."""
|
| 357 |
+
parser = argparse.ArgumentParser(
|
| 358 |
+
description="Phone Agent - AI-powered phone automation",
|
| 359 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 360 |
+
epilog="""
|
| 361 |
+
Examples:
|
| 362 |
+
# Run with default settings (Android)
|
| 363 |
+
python main.py
|
| 364 |
+
|
| 365 |
+
# Specify model endpoint
|
| 366 |
+
python main.py --base-url http://localhost:8000/v1
|
| 367 |
+
|
| 368 |
+
# Use API key for authentication
|
| 369 |
+
python main.py --apikey sk-xxxxx
|
| 370 |
+
|
| 371 |
+
# Run with specific device
|
| 372 |
+
python main.py --device-id emulator-5554
|
| 373 |
+
|
| 374 |
+
# Connect to remote device
|
| 375 |
+
python main.py --connect 192.168.1.100:5555
|
| 376 |
+
|
| 377 |
+
# List connected devices
|
| 378 |
+
python main.py --list-devices
|
| 379 |
+
|
| 380 |
+
# Enable TCP/IP on USB device and get connection info
|
| 381 |
+
python main.py --enable-tcpip
|
| 382 |
+
|
| 383 |
+
# List supported apps
|
| 384 |
+
python main.py --list-apps
|
| 385 |
+
|
| 386 |
+
# iOS specific examples
|
| 387 |
+
# Run with iOS device
|
| 388 |
+
python main.py --device-type ios "Open Safari and search for iPhone tips"
|
| 389 |
+
|
| 390 |
+
# Use WiFi connection for iOS
|
| 391 |
+
python main.py --device-type ios --wda-url http://192.168.1.100:8100
|
| 392 |
+
|
| 393 |
+
# List connected iOS devices
|
| 394 |
+
python main.py --device-type ios --list-devices
|
| 395 |
+
|
| 396 |
+
# Check WebDriverAgent status
|
| 397 |
+
python main.py --device-type ios --wda-status
|
| 398 |
+
|
| 399 |
+
# Pair with iOS device
|
| 400 |
+
python main.py --device-type ios --pair
|
| 401 |
+
""",
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
# Model options
|
| 405 |
+
parser.add_argument(
|
| 406 |
+
"--base-url",
|
| 407 |
+
type=str,
|
| 408 |
+
default=os.getenv("PHONE_AGENT_BASE_URL", "http://localhost:8000/v1"),
|
| 409 |
+
help="Model API base URL",
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
parser.add_argument(
|
| 413 |
+
"--model",
|
| 414 |
+
type=str,
|
| 415 |
+
default=os.getenv("PHONE_AGENT_MODEL", "autoglm-phone-9b"),
|
| 416 |
+
help="Model name",
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
parser.add_argument(
|
| 420 |
+
"--apikey",
|
| 421 |
+
type=str,
|
| 422 |
+
default=os.getenv("PHONE_AGENT_API_KEY", "EMPTY"),
|
| 423 |
+
help="API key for model authentication",
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
parser.add_argument(
|
| 427 |
+
"--max-steps",
|
| 428 |
+
type=int,
|
| 429 |
+
default=int(os.getenv("PHONE_AGENT_MAX_STEPS", "100")),
|
| 430 |
+
help="Maximum steps per task",
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
# Device options
|
| 434 |
+
parser.add_argument(
|
| 435 |
+
"--device-id",
|
| 436 |
+
"-d",
|
| 437 |
+
type=str,
|
| 438 |
+
default=os.getenv("PHONE_AGENT_DEVICE_ID"),
|
| 439 |
+
help="ADB device ID",
|
| 440 |
+
)
|
| 441 |
+
|
| 442 |
+
parser.add_argument(
|
| 443 |
+
"--connect",
|
| 444 |
+
"-c",
|
| 445 |
+
type=str,
|
| 446 |
+
metavar="ADDRESS",
|
| 447 |
+
help="Connect to remote device (e.g., 192.168.1.100:5555)",
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
parser.add_argument(
|
| 451 |
+
"--disconnect",
|
| 452 |
+
type=str,
|
| 453 |
+
nargs="?",
|
| 454 |
+
const="all",
|
| 455 |
+
metavar="ADDRESS",
|
| 456 |
+
help="Disconnect from remote device (or 'all' to disconnect all)",
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
parser.add_argument(
|
| 460 |
+
"--list-devices", action="store_true", help="List connected devices and exit"
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
parser.add_argument(
|
| 464 |
+
"--enable-tcpip",
|
| 465 |
+
type=int,
|
| 466 |
+
nargs="?",
|
| 467 |
+
const=5555,
|
| 468 |
+
metavar="PORT",
|
| 469 |
+
help="Enable TCP/IP debugging on USB device (default port: 5555)",
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
# iOS specific options
|
| 473 |
+
parser.add_argument(
|
| 474 |
+
"--wda-url",
|
| 475 |
+
type=str,
|
| 476 |
+
default=os.getenv("PHONE_AGENT_WDA_URL", "http://localhost:8100"),
|
| 477 |
+
help="WebDriverAgent URL for iOS (default: http://localhost:8100)",
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
parser.add_argument(
|
| 481 |
+
"--pair",
|
| 482 |
+
action="store_true",
|
| 483 |
+
help="Pair with iOS device (required for some operations)",
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
parser.add_argument(
|
| 487 |
+
"--wda-status",
|
| 488 |
+
action="store_true",
|
| 489 |
+
help="Show WebDriverAgent status and exit (iOS only)",
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# Other options
|
| 493 |
+
parser.add_argument(
|
| 494 |
+
"--quiet", "-q", action="store_true", help="Suppress verbose output"
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
parser.add_argument(
|
| 498 |
+
"--list-apps", action="store_true", help="List supported apps and exit"
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
parser.add_argument(
|
| 502 |
+
"--lang",
|
| 503 |
+
type=str,
|
| 504 |
+
choices=["cn", "en"],
|
| 505 |
+
default=os.getenv("PHONE_AGENT_LANG", "cn"),
|
| 506 |
+
help="Language for system prompt (cn or en, default: cn)",
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
parser.add_argument(
|
| 510 |
+
"--device-type",
|
| 511 |
+
type=str,
|
| 512 |
+
choices=["adb", "hdc", "ios"],
|
| 513 |
+
default=os.getenv("PHONE_AGENT_DEVICE_TYPE", "adb"),
|
| 514 |
+
help="Device type: adb for Android, hdc for HarmonyOS, ios for iPhone (default: adb)",
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
parser.add_argument(
|
| 518 |
+
"task",
|
| 519 |
+
nargs="?",
|
| 520 |
+
type=str,
|
| 521 |
+
help="Task to execute (interactive mode if not provided)",
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
return parser.parse_args()
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
def handle_ios_device_commands(args) -> bool:
|
| 528 |
+
"""
|
| 529 |
+
Handle iOS device-related commands.
|
| 530 |
+
|
| 531 |
+
Returns:
|
| 532 |
+
True if a device command was handled (should exit), False otherwise.
|
| 533 |
+
"""
|
| 534 |
+
conn = XCTestConnection(wda_url=args.wda_url)
|
| 535 |
+
|
| 536 |
+
# Handle --list-devices
|
| 537 |
+
if args.list_devices:
|
| 538 |
+
devices = list_ios_devices()
|
| 539 |
+
if not devices:
|
| 540 |
+
print("No iOS devices connected.")
|
| 541 |
+
print("\nTroubleshooting:")
|
| 542 |
+
print(" 1. Connect device via USB")
|
| 543 |
+
print(" 2. Unlock device and trust this computer")
|
| 544 |
+
print(" 3. Run: idevice_id -l")
|
| 545 |
+
else:
|
| 546 |
+
print("Connected iOS devices:")
|
| 547 |
+
print("-" * 70)
|
| 548 |
+
for device in devices:
|
| 549 |
+
conn_type = device.connection_type.value
|
| 550 |
+
model_info = f"{device.model}" if device.model else "Unknown"
|
| 551 |
+
ios_info = f"iOS {device.ios_version}" if device.ios_version else ""
|
| 552 |
+
name_info = device.device_name or "Unnamed"
|
| 553 |
+
|
| 554 |
+
print(f" ✓ {name_info}")
|
| 555 |
+
print(f" UUID: {device.device_id}")
|
| 556 |
+
print(f" Model: {model_info}")
|
| 557 |
+
print(f" OS: {ios_info}")
|
| 558 |
+
print(f" Connection: {conn_type}")
|
| 559 |
+
print("-" * 70)
|
| 560 |
+
return True
|
| 561 |
+
|
| 562 |
+
# Handle --pair
|
| 563 |
+
if args.pair:
|
| 564 |
+
print("Pairing with iOS device...")
|
| 565 |
+
success, message = conn.pair_device(args.device_id)
|
| 566 |
+
print(f"{'✓' if success else '✗'} {message}")
|
| 567 |
+
return True
|
| 568 |
+
|
| 569 |
+
# Handle --wda-status
|
| 570 |
+
if args.wda_status:
|
| 571 |
+
print(f"Checking WebDriverAgent status at {args.wda_url}...")
|
| 572 |
+
print("-" * 50)
|
| 573 |
+
|
| 574 |
+
if conn.is_wda_ready():
|
| 575 |
+
print("✓ WebDriverAgent is running")
|
| 576 |
+
|
| 577 |
+
status = conn.get_wda_status()
|
| 578 |
+
if status:
|
| 579 |
+
print(f"\nStatus details:")
|
| 580 |
+
value = status.get("value", {})
|
| 581 |
+
print(f" Session ID: {status.get('sessionId', 'N/A')}")
|
| 582 |
+
print(f" Build: {value.get('build', {}).get('time', 'N/A')}")
|
| 583 |
+
|
| 584 |
+
current_app = value.get("currentApp", {})
|
| 585 |
+
if current_app:
|
| 586 |
+
print(f"\nCurrent App:")
|
| 587 |
+
print(f" Bundle ID: {current_app.get('bundleId', 'N/A')}")
|
| 588 |
+
print(f" Process ID: {current_app.get('pid', 'N/A')}")
|
| 589 |
+
else:
|
| 590 |
+
print("✗ WebDriverAgent is not running")
|
| 591 |
+
print("\nPlease start WebDriverAgent on your iOS device:")
|
| 592 |
+
print(" 1. Open WebDriverAgent.xcodeproj in Xcode")
|
| 593 |
+
print(" 2. Select your device")
|
| 594 |
+
print(" 3. Run WebDriverAgentRunner (Product > Test or Cmd+U)")
|
| 595 |
+
print(f" 4. For USB: Run port forwarding: iproxy 8100 8100")
|
| 596 |
+
|
| 597 |
+
return True
|
| 598 |
+
|
| 599 |
+
return False
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def handle_device_commands(args) -> bool:
|
| 603 |
+
"""
|
| 604 |
+
Handle device-related commands.
|
| 605 |
+
|
| 606 |
+
Returns:
|
| 607 |
+
True if a device command was handled (should exit), False otherwise.
|
| 608 |
+
"""
|
| 609 |
+
device_type = (
|
| 610 |
+
DeviceType.ADB
|
| 611 |
+
if args.device_type == "adb"
|
| 612 |
+
else (DeviceType.HDC if args.device_type == "hdc" else DeviceType.IOS)
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
# Handle iOS-specific commands
|
| 616 |
+
if device_type == DeviceType.IOS:
|
| 617 |
+
return handle_ios_device_commands(args)
|
| 618 |
+
|
| 619 |
+
device_factory = get_device_factory()
|
| 620 |
+
ConnectionClass = device_factory.get_connection_class()
|
| 621 |
+
conn = ConnectionClass()
|
| 622 |
+
|
| 623 |
+
# Handle --list-devices
|
| 624 |
+
if args.list_devices:
|
| 625 |
+
devices = device_factory.list_devices()
|
| 626 |
+
if not devices:
|
| 627 |
+
print("No devices connected.")
|
| 628 |
+
else:
|
| 629 |
+
print("Connected devices:")
|
| 630 |
+
print("-" * 60)
|
| 631 |
+
for device in devices:
|
| 632 |
+
status_icon = "✓" if device.status == "device" else "✗"
|
| 633 |
+
conn_type = device.connection_type.value
|
| 634 |
+
model_info = f" ({device.model})" if device.model else ""
|
| 635 |
+
print(
|
| 636 |
+
f" {status_icon} {device.device_id:<30} [{conn_type}]{model_info}"
|
| 637 |
+
)
|
| 638 |
+
return True
|
| 639 |
+
|
| 640 |
+
# Handle --connect
|
| 641 |
+
if args.connect:
|
| 642 |
+
print(f"Connecting to {args.connect}...")
|
| 643 |
+
success, message = conn.connect(args.connect)
|
| 644 |
+
print(f"{'✓' if success else '✗'} {message}")
|
| 645 |
+
if success:
|
| 646 |
+
# Set as default device
|
| 647 |
+
args.device_id = args.connect
|
| 648 |
+
return not success # Continue if connection succeeded
|
| 649 |
+
|
| 650 |
+
# Handle --disconnect
|
| 651 |
+
if args.disconnect:
|
| 652 |
+
if args.disconnect == "all":
|
| 653 |
+
print("Disconnecting all remote devices...")
|
| 654 |
+
success, message = conn.disconnect()
|
| 655 |
+
else:
|
| 656 |
+
print(f"Disconnecting from {args.disconnect}...")
|
| 657 |
+
success, message = conn.disconnect(args.disconnect)
|
| 658 |
+
print(f"{'✓' if success else '✗'} {message}")
|
| 659 |
+
return True
|
| 660 |
+
|
| 661 |
+
# Handle --enable-tcpip
|
| 662 |
+
if args.enable_tcpip:
|
| 663 |
+
port = args.enable_tcpip
|
| 664 |
+
print(f"Enabling TCP/IP debugging on port {port}...")
|
| 665 |
+
|
| 666 |
+
success, message = conn.enable_tcpip(port, args.device_id)
|
| 667 |
+
print(f"{'✓' if success else '✗'} {message}")
|
| 668 |
+
|
| 669 |
+
if success:
|
| 670 |
+
# Try to get device IP
|
| 671 |
+
ip = conn.get_device_ip(args.device_id)
|
| 672 |
+
if ip:
|
| 673 |
+
print(f"\nYou can now connect remotely using:")
|
| 674 |
+
print(f" python main.py --connect {ip}:{port}")
|
| 675 |
+
print(f"\nOr via ADB directly:")
|
| 676 |
+
print(f" adb connect {ip}:{port}")
|
| 677 |
+
else:
|
| 678 |
+
print("\nCould not determine device IP. Check device WiFi settings.")
|
| 679 |
+
return True
|
| 680 |
+
|
| 681 |
+
return False
|
| 682 |
+
|
| 683 |
+
|
| 684 |
+
def main():
|
| 685 |
+
"""Main entry point."""
|
| 686 |
+
args = parse_args()
|
| 687 |
+
|
| 688 |
+
# Set device type globally based on args
|
| 689 |
+
if args.device_type == "adb":
|
| 690 |
+
device_type = DeviceType.ADB
|
| 691 |
+
elif args.device_type == "hdc":
|
| 692 |
+
device_type = DeviceType.HDC
|
| 693 |
+
else: # ios
|
| 694 |
+
device_type = DeviceType.IOS
|
| 695 |
+
|
| 696 |
+
# Set device type globally for non-iOS devices
|
| 697 |
+
if device_type != DeviceType.IOS:
|
| 698 |
+
set_device_type(device_type)
|
| 699 |
+
|
| 700 |
+
# Enable HDC verbose mode if using HDC
|
| 701 |
+
if device_type == DeviceType.HDC:
|
| 702 |
+
from phone_agent.hdc import set_hdc_verbose
|
| 703 |
+
|
| 704 |
+
set_hdc_verbose(True)
|
| 705 |
+
|
| 706 |
+
# Handle --list-apps (no system check needed)
|
| 707 |
+
if args.list_apps:
|
| 708 |
+
if device_type == DeviceType.HDC:
|
| 709 |
+
print("Supported HarmonyOS apps:")
|
| 710 |
+
apps = list_harmonyos_apps()
|
| 711 |
+
elif device_type == DeviceType.IOS:
|
| 712 |
+
print("Supported iOS apps:")
|
| 713 |
+
print("\nNote: For iOS apps, Bundle IDs are configured in:")
|
| 714 |
+
print(" phone_agent/config/apps_ios.py")
|
| 715 |
+
print("\nCurrently configured apps:")
|
| 716 |
+
apps = list_ios_apps()
|
| 717 |
+
else:
|
| 718 |
+
print("Supported Android apps:")
|
| 719 |
+
apps = list_supported_apps()
|
| 720 |
+
|
| 721 |
+
for app in sorted(apps):
|
| 722 |
+
print(f" - {app}")
|
| 723 |
+
|
| 724 |
+
if device_type == DeviceType.IOS:
|
| 725 |
+
print(
|
| 726 |
+
"\nTo add iOS apps, find the Bundle ID and add to APP_PACKAGES_IOS dictionary."
|
| 727 |
+
)
|
| 728 |
+
return
|
| 729 |
+
|
| 730 |
+
# Handle device commands (these may need partial system checks)
|
| 731 |
+
if handle_device_commands(args):
|
| 732 |
+
return
|
| 733 |
+
|
| 734 |
+
# Run system requirements check before proceeding
|
| 735 |
+
if not check_system_requirements(
|
| 736 |
+
device_type,
|
| 737 |
+
wda_url=args.wda_url
|
| 738 |
+
if device_type == DeviceType.IOS
|
| 739 |
+
else "http://localhost:8100",
|
| 740 |
+
):
|
| 741 |
+
sys.exit(1)
|
| 742 |
+
|
| 743 |
+
# Check model API connectivity and model availability
|
| 744 |
+
if not check_model_api(args.base_url, args.model, args.apikey):
|
| 745 |
+
sys.exit(1)
|
| 746 |
+
|
| 747 |
+
# Create configurations and agent based on device type
|
| 748 |
+
model_config = ModelConfig(
|
| 749 |
+
base_url=args.base_url,
|
| 750 |
+
model_name=args.model,
|
| 751 |
+
api_key=args.apikey,
|
| 752 |
+
lang=args.lang,
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
if device_type == DeviceType.IOS:
|
| 756 |
+
# Create iOS agent
|
| 757 |
+
agent_config = IOSAgentConfig(
|
| 758 |
+
max_steps=args.max_steps,
|
| 759 |
+
wda_url=args.wda_url,
|
| 760 |
+
device_id=args.device_id,
|
| 761 |
+
verbose=not args.quiet,
|
| 762 |
+
lang=args.lang,
|
| 763 |
+
)
|
| 764 |
+
|
| 765 |
+
agent = IOSPhoneAgent(
|
| 766 |
+
model_config=model_config,
|
| 767 |
+
agent_config=agent_config,
|
| 768 |
+
)
|
| 769 |
+
else:
|
| 770 |
+
# Create Android/HarmonyOS agent
|
| 771 |
+
agent_config = AgentConfig(
|
| 772 |
+
max_steps=args.max_steps,
|
| 773 |
+
device_id=args.device_id,
|
| 774 |
+
verbose=not args.quiet,
|
| 775 |
+
lang=args.lang,
|
| 776 |
+
)
|
| 777 |
+
|
| 778 |
+
agent = PhoneAgent(
|
| 779 |
+
model_config=model_config,
|
| 780 |
+
agent_config=agent_config,
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
# Print header
|
| 784 |
+
print("=" * 50)
|
| 785 |
+
if device_type == DeviceType.IOS:
|
| 786 |
+
print("Phone Agent iOS - AI-powered iOS automation")
|
| 787 |
+
else:
|
| 788 |
+
print("Phone Agent - AI-powered phone automation")
|
| 789 |
+
print("=" * 50)
|
| 790 |
+
print(f"Model: {model_config.model_name}")
|
| 791 |
+
print(f"Base URL: {model_config.base_url}")
|
| 792 |
+
print(f"Max Steps: {agent_config.max_steps}")
|
| 793 |
+
print(f"Language: {agent_config.lang}")
|
| 794 |
+
print(f"Device Type: {args.device_type.upper()}")
|
| 795 |
+
|
| 796 |
+
# Show iOS-specific config
|
| 797 |
+
if device_type == DeviceType.IOS:
|
| 798 |
+
print(f"WDA URL: {args.wda_url}")
|
| 799 |
+
|
| 800 |
+
# Show device info
|
| 801 |
+
if device_type == DeviceType.IOS:
|
| 802 |
+
devices = list_ios_devices()
|
| 803 |
+
if agent_config.device_id:
|
| 804 |
+
print(f"Device: {agent_config.device_id}")
|
| 805 |
+
elif devices:
|
| 806 |
+
device = devices[0]
|
| 807 |
+
print(f"Device: {device.device_name or device.device_id[:16]}")
|
| 808 |
+
if device.model and device.ios_version:
|
| 809 |
+
print(f" {device.model}, iOS {device.ios_version}")
|
| 810 |
+
else:
|
| 811 |
+
device_factory = get_device_factory()
|
| 812 |
+
devices = device_factory.list_devices()
|
| 813 |
+
if agent_config.device_id:
|
| 814 |
+
print(f"Device: {agent_config.device_id}")
|
| 815 |
+
elif devices:
|
| 816 |
+
print(f"Device: {devices[0].device_id} (auto-detected)")
|
| 817 |
+
|
| 818 |
+
print("=" * 50)
|
| 819 |
+
|
| 820 |
+
# Run with provided task or enter interactive mode
|
| 821 |
+
if args.task:
|
| 822 |
+
print(f"\nTask: {args.task}\n")
|
| 823 |
+
result = agent.run(args.task)
|
| 824 |
+
print(f"\nResult: {result}")
|
| 825 |
+
else:
|
| 826 |
+
# Interactive mode
|
| 827 |
+
print("\nEntering interactive mode. Type 'quit' to exit.\n")
|
| 828 |
+
|
| 829 |
+
while True:
|
| 830 |
+
try:
|
| 831 |
+
task = input("Enter your task: ").strip()
|
| 832 |
+
|
| 833 |
+
if task.lower() in ("quit", "exit", "q"):
|
| 834 |
+
print("Goodbye!")
|
| 835 |
+
break
|
| 836 |
+
|
| 837 |
+
if not task:
|
| 838 |
+
continue
|
| 839 |
+
|
| 840 |
+
print()
|
| 841 |
+
result = agent.run(task)
|
| 842 |
+
print(f"\nResult: {result}\n")
|
| 843 |
+
agent.reset()
|
| 844 |
+
|
| 845 |
+
except KeyboardInterrupt:
|
| 846 |
+
print("\n\nInterrupted. Goodbye!")
|
| 847 |
+
break
|
| 848 |
+
except Exception as e:
|
| 849 |
+
print(f"\nError: {e}\n")
|
| 850 |
+
|
| 851 |
+
|
| 852 |
+
if __name__ == "__main__":
|
| 853 |
+
main()
|
phone_agent/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Phone Agent - An AI-powered phone automation framework.
|
| 3 |
+
|
| 4 |
+
This package provides tools for automating Android and iOS phone interactions
|
| 5 |
+
using AI models for visual understanding and decision making.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from phone_agent.agent import PhoneAgent
|
| 9 |
+
from phone_agent.agent_ios import IOSPhoneAgent
|
| 10 |
+
|
| 11 |
+
__version__ = "0.1.0"
|
| 12 |
+
__all__ = ["PhoneAgent", "IOSPhoneAgent"]
|
phone_agent/actions/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Action handling module for Phone Agent."""
|
| 2 |
+
|
| 3 |
+
from phone_agent.actions.handler import ActionHandler, ActionResult
|
| 4 |
+
|
| 5 |
+
__all__ = ["ActionHandler", "ActionResult"]
|
phone_agent/actions/handler.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Action handler for processing AI model outputs."""
|
| 2 |
+
|
| 3 |
+
import ast
|
| 4 |
+
import re
|
| 5 |
+
import subprocess
|
| 6 |
+
import time
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Any, Callable
|
| 9 |
+
|
| 10 |
+
from phone_agent.config.timing import TIMING_CONFIG
|
| 11 |
+
from phone_agent.device_factory import get_device_factory
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ActionResult:
|
| 16 |
+
"""Result of an action execution."""
|
| 17 |
+
|
| 18 |
+
success: bool
|
| 19 |
+
should_finish: bool
|
| 20 |
+
message: str | None = None
|
| 21 |
+
requires_confirmation: bool = False
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ActionHandler:
|
| 25 |
+
"""
|
| 26 |
+
Handles execution of actions from AI model output.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 30 |
+
confirmation_callback: Optional callback for sensitive action confirmation.
|
| 31 |
+
Should return True to proceed, False to cancel.
|
| 32 |
+
takeover_callback: Optional callback for takeover requests (login, captcha).
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(
|
| 36 |
+
self,
|
| 37 |
+
device_id: str | None = None,
|
| 38 |
+
confirmation_callback: Callable[[str], bool] | None = None,
|
| 39 |
+
takeover_callback: Callable[[str], None] | None = None,
|
| 40 |
+
):
|
| 41 |
+
self.device_id = device_id
|
| 42 |
+
self.confirmation_callback = confirmation_callback or self._default_confirmation
|
| 43 |
+
self.takeover_callback = takeover_callback or self._default_takeover
|
| 44 |
+
|
| 45 |
+
def execute(
|
| 46 |
+
self, action: dict[str, Any], screen_width: int, screen_height: int
|
| 47 |
+
) -> ActionResult:
|
| 48 |
+
"""
|
| 49 |
+
Execute an action from the AI model.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
action: The action dictionary from the model.
|
| 53 |
+
screen_width: Current screen width in pixels.
|
| 54 |
+
screen_height: Current screen height in pixels.
|
| 55 |
+
|
| 56 |
+
Returns:
|
| 57 |
+
ActionResult indicating success and whether to finish.
|
| 58 |
+
"""
|
| 59 |
+
action_type = action.get("_metadata")
|
| 60 |
+
|
| 61 |
+
if action_type == "finish":
|
| 62 |
+
return ActionResult(
|
| 63 |
+
success=True, should_finish=True, message=action.get("message")
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if action_type != "do":
|
| 67 |
+
return ActionResult(
|
| 68 |
+
success=False,
|
| 69 |
+
should_finish=True,
|
| 70 |
+
message=f"Unknown action type: {action_type}",
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
action_name = action.get("action")
|
| 74 |
+
handler_method = self._get_handler(action_name)
|
| 75 |
+
|
| 76 |
+
if handler_method is None:
|
| 77 |
+
return ActionResult(
|
| 78 |
+
success=False,
|
| 79 |
+
should_finish=False,
|
| 80 |
+
message=f"Unknown action: {action_name}",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
return handler_method(action, screen_width, screen_height)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return ActionResult(
|
| 87 |
+
success=False, should_finish=False, message=f"Action failed: {e}"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def _get_handler(self, action_name: str) -> Callable | None:
|
| 91 |
+
"""Get the handler method for an action."""
|
| 92 |
+
handlers = {
|
| 93 |
+
"Launch": self._handle_launch,
|
| 94 |
+
"Tap": self._handle_tap,
|
| 95 |
+
"Type": self._handle_type,
|
| 96 |
+
"Type_Name": self._handle_type,
|
| 97 |
+
"Swipe": self._handle_swipe,
|
| 98 |
+
"Back": self._handle_back,
|
| 99 |
+
"Home": self._handle_home,
|
| 100 |
+
"Double Tap": self._handle_double_tap,
|
| 101 |
+
"Long Press": self._handle_long_press,
|
| 102 |
+
"Wait": self._handle_wait,
|
| 103 |
+
"Take_over": self._handle_takeover,
|
| 104 |
+
"Note": self._handle_note,
|
| 105 |
+
"Call_API": self._handle_call_api,
|
| 106 |
+
"Interact": self._handle_interact,
|
| 107 |
+
}
|
| 108 |
+
return handlers.get(action_name)
|
| 109 |
+
|
| 110 |
+
def _convert_relative_to_absolute(
|
| 111 |
+
self, element: list[int], screen_width: int, screen_height: int
|
| 112 |
+
) -> tuple[int, int]:
|
| 113 |
+
"""Convert relative coordinates (0-1000) to absolute pixels."""
|
| 114 |
+
x = int(element[0] / 1000 * screen_width)
|
| 115 |
+
y = int(element[1] / 1000 * screen_height)
|
| 116 |
+
return x, y
|
| 117 |
+
|
| 118 |
+
def _handle_launch(self, action: dict, width: int, height: int) -> ActionResult:
|
| 119 |
+
"""Handle app launch action."""
|
| 120 |
+
app_name = action.get("app")
|
| 121 |
+
if not app_name:
|
| 122 |
+
return ActionResult(False, False, "No app name specified")
|
| 123 |
+
|
| 124 |
+
device_factory = get_device_factory()
|
| 125 |
+
success = device_factory.launch_app(app_name, self.device_id)
|
| 126 |
+
if success:
|
| 127 |
+
return ActionResult(True, False)
|
| 128 |
+
return ActionResult(False, False, f"App not found: {app_name}")
|
| 129 |
+
|
| 130 |
+
def _handle_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
| 131 |
+
"""Handle tap action."""
|
| 132 |
+
element = action.get("element")
|
| 133 |
+
if not element:
|
| 134 |
+
return ActionResult(False, False, "No element coordinates")
|
| 135 |
+
|
| 136 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 137 |
+
|
| 138 |
+
# Check for sensitive operation
|
| 139 |
+
if "message" in action:
|
| 140 |
+
if not self.confirmation_callback(action["message"]):
|
| 141 |
+
return ActionResult(
|
| 142 |
+
success=False,
|
| 143 |
+
should_finish=True,
|
| 144 |
+
message="User cancelled sensitive operation",
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
device_factory = get_device_factory()
|
| 148 |
+
device_factory.tap(x, y, self.device_id)
|
| 149 |
+
return ActionResult(True, False)
|
| 150 |
+
|
| 151 |
+
def _handle_type(self, action: dict, width: int, height: int) -> ActionResult:
|
| 152 |
+
"""Handle text input action."""
|
| 153 |
+
text = action.get("text", "")
|
| 154 |
+
|
| 155 |
+
device_factory = get_device_factory()
|
| 156 |
+
|
| 157 |
+
# Switch to ADB keyboard
|
| 158 |
+
original_ime = device_factory.detect_and_set_adb_keyboard(self.device_id)
|
| 159 |
+
time.sleep(TIMING_CONFIG.action.keyboard_switch_delay)
|
| 160 |
+
|
| 161 |
+
# Clear existing text and type new text
|
| 162 |
+
device_factory.clear_text(self.device_id)
|
| 163 |
+
time.sleep(TIMING_CONFIG.action.text_clear_delay)
|
| 164 |
+
|
| 165 |
+
# Handle multiline text by splitting on newlines
|
| 166 |
+
device_factory.type_text(text, self.device_id)
|
| 167 |
+
time.sleep(TIMING_CONFIG.action.text_input_delay)
|
| 168 |
+
|
| 169 |
+
# Restore original keyboard
|
| 170 |
+
device_factory.restore_keyboard(original_ime, self.device_id)
|
| 171 |
+
time.sleep(TIMING_CONFIG.action.keyboard_restore_delay)
|
| 172 |
+
|
| 173 |
+
return ActionResult(True, False)
|
| 174 |
+
|
| 175 |
+
def _handle_swipe(self, action: dict, width: int, height: int) -> ActionResult:
|
| 176 |
+
"""Handle swipe action."""
|
| 177 |
+
start = action.get("start")
|
| 178 |
+
end = action.get("end")
|
| 179 |
+
|
| 180 |
+
if not start or not end:
|
| 181 |
+
return ActionResult(False, False, "Missing swipe coordinates")
|
| 182 |
+
|
| 183 |
+
start_x, start_y = self._convert_relative_to_absolute(start, width, height)
|
| 184 |
+
end_x, end_y = self._convert_relative_to_absolute(end, width, height)
|
| 185 |
+
|
| 186 |
+
device_factory = get_device_factory()
|
| 187 |
+
device_factory.swipe(start_x, start_y, end_x, end_y, device_id=self.device_id)
|
| 188 |
+
return ActionResult(True, False)
|
| 189 |
+
|
| 190 |
+
def _handle_back(self, action: dict, width: int, height: int) -> ActionResult:
|
| 191 |
+
"""Handle back button action."""
|
| 192 |
+
device_factory = get_device_factory()
|
| 193 |
+
device_factory.back(self.device_id)
|
| 194 |
+
return ActionResult(True, False)
|
| 195 |
+
|
| 196 |
+
def _handle_home(self, action: dict, width: int, height: int) -> ActionResult:
|
| 197 |
+
"""Handle home button action."""
|
| 198 |
+
device_factory = get_device_factory()
|
| 199 |
+
device_factory.home(self.device_id)
|
| 200 |
+
return ActionResult(True, False)
|
| 201 |
+
|
| 202 |
+
def _handle_double_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
| 203 |
+
"""Handle double tap action."""
|
| 204 |
+
element = action.get("element")
|
| 205 |
+
if not element:
|
| 206 |
+
return ActionResult(False, False, "No element coordinates")
|
| 207 |
+
|
| 208 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 209 |
+
device_factory = get_device_factory()
|
| 210 |
+
device_factory.double_tap(x, y, self.device_id)
|
| 211 |
+
return ActionResult(True, False)
|
| 212 |
+
|
| 213 |
+
def _handle_long_press(self, action: dict, width: int, height: int) -> ActionResult:
|
| 214 |
+
"""Handle long press action."""
|
| 215 |
+
element = action.get("element")
|
| 216 |
+
if not element:
|
| 217 |
+
return ActionResult(False, False, "No element coordinates")
|
| 218 |
+
|
| 219 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 220 |
+
device_factory = get_device_factory()
|
| 221 |
+
device_factory.long_press(x, y, device_id=self.device_id)
|
| 222 |
+
return ActionResult(True, False)
|
| 223 |
+
|
| 224 |
+
def _handle_wait(self, action: dict, width: int, height: int) -> ActionResult:
|
| 225 |
+
"""Handle wait action."""
|
| 226 |
+
duration_str = action.get("duration", "1 seconds")
|
| 227 |
+
try:
|
| 228 |
+
duration = float(duration_str.replace("seconds", "").strip())
|
| 229 |
+
except ValueError:
|
| 230 |
+
duration = 1.0
|
| 231 |
+
|
| 232 |
+
time.sleep(duration)
|
| 233 |
+
return ActionResult(True, False)
|
| 234 |
+
|
| 235 |
+
def _handle_takeover(self, action: dict, width: int, height: int) -> ActionResult:
|
| 236 |
+
"""Handle takeover request (login, captcha, etc.)."""
|
| 237 |
+
message = action.get("message", "User intervention required")
|
| 238 |
+
self.takeover_callback(message)
|
| 239 |
+
return ActionResult(True, False)
|
| 240 |
+
|
| 241 |
+
def _handle_note(self, action: dict, width: int, height: int) -> ActionResult:
|
| 242 |
+
"""Handle note action (placeholder for content recording)."""
|
| 243 |
+
# This action is typically used for recording page content
|
| 244 |
+
# Implementation depends on specific requirements
|
| 245 |
+
return ActionResult(True, False)
|
| 246 |
+
|
| 247 |
+
def _handle_call_api(self, action: dict, width: int, height: int) -> ActionResult:
|
| 248 |
+
"""Handle API call action (placeholder for summarization)."""
|
| 249 |
+
# This action is typically used for content summarization
|
| 250 |
+
# Implementation depends on specific requirements
|
| 251 |
+
return ActionResult(True, False)
|
| 252 |
+
|
| 253 |
+
def _handle_interact(self, action: dict, width: int, height: int) -> ActionResult:
|
| 254 |
+
"""Handle interaction request (user choice needed)."""
|
| 255 |
+
# This action signals that user input is needed
|
| 256 |
+
return ActionResult(True, False, message="User interaction required")
|
| 257 |
+
|
| 258 |
+
def _send_keyevent(self, keycode: str) -> None:
|
| 259 |
+
"""Send a keyevent to the device."""
|
| 260 |
+
from phone_agent.device_factory import DeviceType, get_device_factory
|
| 261 |
+
from phone_agent.hdc.connection import _run_hdc_command
|
| 262 |
+
|
| 263 |
+
device_factory = get_device_factory()
|
| 264 |
+
|
| 265 |
+
# Handle HDC devices with HarmonyOS-specific keyEvent command
|
| 266 |
+
if device_factory.device_type == DeviceType.HDC:
|
| 267 |
+
hdc_prefix = ["hdc", "-t", self.device_id] if self.device_id else ["hdc"]
|
| 268 |
+
|
| 269 |
+
# Map common keycodes to HarmonyOS keyEvent codes
|
| 270 |
+
# KEYCODE_ENTER (66) -> 2054 (HarmonyOS Enter key code)
|
| 271 |
+
if keycode == "KEYCODE_ENTER" or keycode == "66":
|
| 272 |
+
_run_hdc_command(
|
| 273 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2054"],
|
| 274 |
+
capture_output=True,
|
| 275 |
+
text=True,
|
| 276 |
+
)
|
| 277 |
+
else:
|
| 278 |
+
# For other keys, try to use the numeric code directly
|
| 279 |
+
# If keycode is a string like "KEYCODE_ENTER", convert it
|
| 280 |
+
try:
|
| 281 |
+
# Try to extract numeric code from string or use as-is
|
| 282 |
+
if keycode.startswith("KEYCODE_"):
|
| 283 |
+
# For now, only handle ENTER, other keys may need mapping
|
| 284 |
+
if "ENTER" in keycode:
|
| 285 |
+
_run_hdc_command(
|
| 286 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2054"],
|
| 287 |
+
capture_output=True,
|
| 288 |
+
text=True,
|
| 289 |
+
)
|
| 290 |
+
else:
|
| 291 |
+
# Fallback to ADB-style command for unsupported keys
|
| 292 |
+
subprocess.run(
|
| 293 |
+
hdc_prefix + ["shell", "input", "keyevent", keycode],
|
| 294 |
+
capture_output=True,
|
| 295 |
+
text=True,
|
| 296 |
+
)
|
| 297 |
+
else:
|
| 298 |
+
# Assume it's a numeric code
|
| 299 |
+
_run_hdc_command(
|
| 300 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", str(keycode)],
|
| 301 |
+
capture_output=True,
|
| 302 |
+
text=True,
|
| 303 |
+
)
|
| 304 |
+
except Exception:
|
| 305 |
+
# Fallback to ADB-style command
|
| 306 |
+
subprocess.run(
|
| 307 |
+
hdc_prefix + ["shell", "input", "keyevent", keycode],
|
| 308 |
+
capture_output=True,
|
| 309 |
+
text=True,
|
| 310 |
+
)
|
| 311 |
+
else:
|
| 312 |
+
# ADB devices use standard input keyevent command
|
| 313 |
+
cmd_prefix = ["adb", "-s", self.device_id] if self.device_id else ["adb"]
|
| 314 |
+
subprocess.run(
|
| 315 |
+
cmd_prefix + ["shell", "input", "keyevent", keycode],
|
| 316 |
+
capture_output=True,
|
| 317 |
+
text=True,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
@staticmethod
|
| 321 |
+
def _default_confirmation(message: str) -> bool:
|
| 322 |
+
"""Default confirmation callback using console input."""
|
| 323 |
+
response = input(f"Sensitive operation: {message}\nConfirm? (Y/N): ")
|
| 324 |
+
return response.upper() == "Y"
|
| 325 |
+
|
| 326 |
+
@staticmethod
|
| 327 |
+
def _default_takeover(message: str) -> None:
|
| 328 |
+
"""Default takeover callback using console input."""
|
| 329 |
+
input(f"{message}\nPress Enter after completing manual operation...")
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def parse_action(response: str) -> dict[str, Any]:
|
| 333 |
+
"""
|
| 334 |
+
Parse action from model response.
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
response: Raw response string from the model.
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Parsed action dictionary.
|
| 341 |
+
|
| 342 |
+
Raises:
|
| 343 |
+
ValueError: If the response cannot be parsed.
|
| 344 |
+
"""
|
| 345 |
+
print(f"Parsing action: {response}")
|
| 346 |
+
try:
|
| 347 |
+
response = response.strip()
|
| 348 |
+
if response.startswith('do(action="Type"') or response.startswith(
|
| 349 |
+
'do(action="Type_Name"'
|
| 350 |
+
):
|
| 351 |
+
text = response.split("text=", 1)[1][1:-2]
|
| 352 |
+
action = {"_metadata": "do", "action": "Type", "text": text}
|
| 353 |
+
return action
|
| 354 |
+
elif response.startswith("do"):
|
| 355 |
+
# Use AST parsing instead of eval for safety
|
| 356 |
+
try:
|
| 357 |
+
# Escape special characters (newlines, tabs, etc.) for valid Python syntax
|
| 358 |
+
response = response.replace('\n', '\\n')
|
| 359 |
+
response = response.replace('\r', '\\r')
|
| 360 |
+
response = response.replace('\t', '\\t')
|
| 361 |
+
|
| 362 |
+
tree = ast.parse(response, mode="eval")
|
| 363 |
+
if not isinstance(tree.body, ast.Call):
|
| 364 |
+
raise ValueError("Expected a function call")
|
| 365 |
+
|
| 366 |
+
call = tree.body
|
| 367 |
+
# Extract keyword arguments safely
|
| 368 |
+
action = {"_metadata": "do"}
|
| 369 |
+
for keyword in call.keywords:
|
| 370 |
+
key = keyword.arg
|
| 371 |
+
value = ast.literal_eval(keyword.value)
|
| 372 |
+
action[key] = value
|
| 373 |
+
|
| 374 |
+
return action
|
| 375 |
+
except (SyntaxError, ValueError) as e:
|
| 376 |
+
raise ValueError(f"Failed to parse do() action: {e}")
|
| 377 |
+
|
| 378 |
+
elif response.startswith("finish"):
|
| 379 |
+
action = {
|
| 380 |
+
"_metadata": "finish",
|
| 381 |
+
"message": response.replace("finish(message=", "")[1:-2],
|
| 382 |
+
}
|
| 383 |
+
else:
|
| 384 |
+
raise ValueError(f"Failed to parse action: {response}")
|
| 385 |
+
return action
|
| 386 |
+
except Exception as e:
|
| 387 |
+
raise ValueError(f"Failed to parse action: {e}")
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def do(**kwargs) -> dict[str, Any]:
|
| 391 |
+
"""Helper function for creating 'do' actions."""
|
| 392 |
+
kwargs["_metadata"] = "do"
|
| 393 |
+
return kwargs
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def finish(**kwargs) -> dict[str, Any]:
|
| 397 |
+
"""Helper function for creating 'finish' actions."""
|
| 398 |
+
kwargs["_metadata"] = "finish"
|
| 399 |
+
return kwargs
|
phone_agent/actions/handler_ios.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Action handler for iOS automation using WebDriverAgent."""
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any, Callable
|
| 6 |
+
|
| 7 |
+
from phone_agent.xctest import (
|
| 8 |
+
back,
|
| 9 |
+
double_tap,
|
| 10 |
+
home,
|
| 11 |
+
launch_app,
|
| 12 |
+
long_press,
|
| 13 |
+
swipe,
|
| 14 |
+
tap,
|
| 15 |
+
)
|
| 16 |
+
from phone_agent.xctest.input import clear_text, hide_keyboard, type_text
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class ActionResult:
|
| 21 |
+
"""Result of an action execution."""
|
| 22 |
+
|
| 23 |
+
success: bool
|
| 24 |
+
should_finish: bool
|
| 25 |
+
message: str | None = None
|
| 26 |
+
requires_confirmation: bool = False
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class IOSActionHandler:
|
| 30 |
+
"""
|
| 31 |
+
Handles execution of actions from AI model output for iOS devices.
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
wda_url: WebDriverAgent URL.
|
| 35 |
+
session_id: Optional WDA session ID.
|
| 36 |
+
confirmation_callback: Optional callback for sensitive action confirmation.
|
| 37 |
+
Should return True to proceed, False to cancel.
|
| 38 |
+
takeover_callback: Optional callback for takeover requests (login, captcha).
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(
|
| 42 |
+
self,
|
| 43 |
+
wda_url: str = "http://localhost:8100",
|
| 44 |
+
session_id: str | None = None,
|
| 45 |
+
confirmation_callback: Callable[[str], bool] | None = None,
|
| 46 |
+
takeover_callback: Callable[[str], None] | None = None,
|
| 47 |
+
):
|
| 48 |
+
self.wda_url = wda_url
|
| 49 |
+
self.session_id = session_id
|
| 50 |
+
self.confirmation_callback = confirmation_callback or self._default_confirmation
|
| 51 |
+
self.takeover_callback = takeover_callback or self._default_takeover
|
| 52 |
+
|
| 53 |
+
def execute(
|
| 54 |
+
self, action: dict[str, Any], screen_width: int, screen_height: int
|
| 55 |
+
) -> ActionResult:
|
| 56 |
+
"""
|
| 57 |
+
Execute an action from the AI model.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
action: The action dictionary from the model.
|
| 61 |
+
screen_width: Current screen width in pixels.
|
| 62 |
+
screen_height: Current screen height in pixels.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
ActionResult indicating success and whether to finish.
|
| 66 |
+
"""
|
| 67 |
+
action_type = action.get("_metadata")
|
| 68 |
+
|
| 69 |
+
if action_type == "finish":
|
| 70 |
+
return ActionResult(
|
| 71 |
+
success=True, should_finish=True, message=action.get("message")
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
if action_type != "do":
|
| 75 |
+
return ActionResult(
|
| 76 |
+
success=False,
|
| 77 |
+
should_finish=True,
|
| 78 |
+
message=f"Unknown action type: {action_type}",
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
action_name = action.get("action")
|
| 82 |
+
handler_method = self._get_handler(action_name)
|
| 83 |
+
|
| 84 |
+
if handler_method is None:
|
| 85 |
+
return ActionResult(
|
| 86 |
+
success=False,
|
| 87 |
+
should_finish=False,
|
| 88 |
+
message=f"Unknown action: {action_name}",
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
return handler_method(action, screen_width, screen_height)
|
| 93 |
+
except Exception as e:
|
| 94 |
+
return ActionResult(
|
| 95 |
+
success=False, should_finish=False, message=f"Action failed: {e}"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def _get_handler(self, action_name: str) -> Callable | None:
|
| 99 |
+
"""Get the handler method for an action."""
|
| 100 |
+
handlers = {
|
| 101 |
+
"Launch": self._handle_launch,
|
| 102 |
+
"Tap": self._handle_tap,
|
| 103 |
+
"Type": self._handle_type,
|
| 104 |
+
"Type_Name": self._handle_type,
|
| 105 |
+
"Swipe": self._handle_swipe,
|
| 106 |
+
"Back": self._handle_back,
|
| 107 |
+
"Home": self._handle_home,
|
| 108 |
+
"Double Tap": self._handle_double_tap,
|
| 109 |
+
"Long Press": self._handle_long_press,
|
| 110 |
+
"Wait": self._handle_wait,
|
| 111 |
+
"Take_over": self._handle_takeover,
|
| 112 |
+
"Note": self._handle_note,
|
| 113 |
+
"Call_API": self._handle_call_api,
|
| 114 |
+
"Interact": self._handle_interact,
|
| 115 |
+
}
|
| 116 |
+
return handlers.get(action_name)
|
| 117 |
+
|
| 118 |
+
def _convert_relative_to_absolute(
|
| 119 |
+
self, element: list[int], screen_width: int, screen_height: int
|
| 120 |
+
) -> tuple[int, int]:
|
| 121 |
+
"""Convert relative coordinates (0-1000) to absolute pixels."""
|
| 122 |
+
x = int(element[0] / 1000 * screen_width)
|
| 123 |
+
y = int(element[1] / 1000 * screen_height)
|
| 124 |
+
return x, y
|
| 125 |
+
|
| 126 |
+
def _handle_launch(self, action: dict, width: int, height: int) -> ActionResult:
|
| 127 |
+
"""Handle app launch action."""
|
| 128 |
+
app_name = action.get("app")
|
| 129 |
+
if not app_name:
|
| 130 |
+
return ActionResult(False, False, "No app name specified")
|
| 131 |
+
|
| 132 |
+
success = launch_app(
|
| 133 |
+
app_name, wda_url=self.wda_url, session_id=self.session_id
|
| 134 |
+
)
|
| 135 |
+
if success:
|
| 136 |
+
return ActionResult(True, False)
|
| 137 |
+
return ActionResult(False, False, f"App not found: {app_name}")
|
| 138 |
+
|
| 139 |
+
def _handle_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
| 140 |
+
"""Handle tap action."""
|
| 141 |
+
element = action.get("element")
|
| 142 |
+
if not element:
|
| 143 |
+
return ActionResult(False, False, "No element coordinates")
|
| 144 |
+
|
| 145 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 146 |
+
|
| 147 |
+
print(f"Physically tap on ({x}, {y})")
|
| 148 |
+
|
| 149 |
+
# Check for sensitive operation
|
| 150 |
+
if "message" in action:
|
| 151 |
+
if not self.confirmation_callback(action["message"]):
|
| 152 |
+
return ActionResult(
|
| 153 |
+
success=False,
|
| 154 |
+
should_finish=True,
|
| 155 |
+
message="User cancelled sensitive operation",
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
|
| 159 |
+
return ActionResult(True, False)
|
| 160 |
+
|
| 161 |
+
def _handle_type(self, action: dict, width: int, height: int) -> ActionResult:
|
| 162 |
+
"""Handle text input action."""
|
| 163 |
+
text = action.get("text", "")
|
| 164 |
+
|
| 165 |
+
# Clear existing text and type new text
|
| 166 |
+
clear_text(wda_url=self.wda_url, session_id=self.session_id)
|
| 167 |
+
time.sleep(0.5)
|
| 168 |
+
|
| 169 |
+
type_text(text, wda_url=self.wda_url, session_id=self.session_id)
|
| 170 |
+
time.sleep(0.5)
|
| 171 |
+
|
| 172 |
+
# Hide keyboard after typing
|
| 173 |
+
hide_keyboard(wda_url=self.wda_url, session_id=self.session_id)
|
| 174 |
+
time.sleep(0.5)
|
| 175 |
+
|
| 176 |
+
return ActionResult(True, False)
|
| 177 |
+
|
| 178 |
+
def _handle_swipe(self, action: dict, width: int, height: int) -> ActionResult:
|
| 179 |
+
"""Handle swipe action."""
|
| 180 |
+
start = action.get("start")
|
| 181 |
+
end = action.get("end")
|
| 182 |
+
|
| 183 |
+
if not start or not end:
|
| 184 |
+
return ActionResult(False, False, "Missing swipe coordinates")
|
| 185 |
+
|
| 186 |
+
start_x, start_y = self._convert_relative_to_absolute(start, width, height)
|
| 187 |
+
end_x, end_y = self._convert_relative_to_absolute(end, width, height)
|
| 188 |
+
|
| 189 |
+
print(f"Physically scroll from ({start_x}, {start_y}) to ({end_x}, {end_y})")
|
| 190 |
+
|
| 191 |
+
swipe(
|
| 192 |
+
start_x,
|
| 193 |
+
start_y,
|
| 194 |
+
end_x,
|
| 195 |
+
end_y,
|
| 196 |
+
wda_url=self.wda_url,
|
| 197 |
+
session_id=self.session_id,
|
| 198 |
+
)
|
| 199 |
+
return ActionResult(True, False)
|
| 200 |
+
|
| 201 |
+
def _handle_back(self, action: dict, width: int, height: int) -> ActionResult:
|
| 202 |
+
"""Handle back gesture (swipe from left edge)."""
|
| 203 |
+
back(wda_url=self.wda_url, session_id=self.session_id)
|
| 204 |
+
return ActionResult(True, False)
|
| 205 |
+
|
| 206 |
+
def _handle_home(self, action: dict, width: int, height: int) -> ActionResult:
|
| 207 |
+
"""Handle home button action."""
|
| 208 |
+
home(wda_url=self.wda_url, session_id=self.session_id)
|
| 209 |
+
return ActionResult(True, False)
|
| 210 |
+
|
| 211 |
+
def _handle_double_tap(self, action: dict, width: int, height: int) -> ActionResult:
|
| 212 |
+
"""Handle double tap action."""
|
| 213 |
+
element = action.get("element")
|
| 214 |
+
if not element:
|
| 215 |
+
return ActionResult(False, False, "No element coordinates")
|
| 216 |
+
|
| 217 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 218 |
+
double_tap(x, y, wda_url=self.wda_url, session_id=self.session_id)
|
| 219 |
+
return ActionResult(True, False)
|
| 220 |
+
|
| 221 |
+
def _handle_long_press(self, action: dict, width: int, height: int) -> ActionResult:
|
| 222 |
+
"""Handle long press action."""
|
| 223 |
+
element = action.get("element")
|
| 224 |
+
if not element:
|
| 225 |
+
return ActionResult(False, False, "No element coordinates")
|
| 226 |
+
|
| 227 |
+
x, y = self._convert_relative_to_absolute(element, width, height)
|
| 228 |
+
long_press(
|
| 229 |
+
x,
|
| 230 |
+
y,
|
| 231 |
+
duration=3.0,
|
| 232 |
+
wda_url=self.wda_url,
|
| 233 |
+
session_id=self.session_id,
|
| 234 |
+
)
|
| 235 |
+
return ActionResult(True, False)
|
| 236 |
+
|
| 237 |
+
def _handle_wait(self, action: dict, width: int, height: int) -> ActionResult:
|
| 238 |
+
"""Handle wait action."""
|
| 239 |
+
duration_str = action.get("duration", "1 seconds")
|
| 240 |
+
try:
|
| 241 |
+
duration = float(duration_str.replace("seconds", "").strip())
|
| 242 |
+
except ValueError:
|
| 243 |
+
duration = 1.0
|
| 244 |
+
|
| 245 |
+
time.sleep(duration)
|
| 246 |
+
return ActionResult(True, False)
|
| 247 |
+
|
| 248 |
+
def _handle_takeover(self, action: dict, width: int, height: int) -> ActionResult:
|
| 249 |
+
"""Handle takeover request (login, captcha, etc.)."""
|
| 250 |
+
message = action.get("message", "User intervention required")
|
| 251 |
+
self.takeover_callback(message)
|
| 252 |
+
return ActionResult(True, False)
|
| 253 |
+
|
| 254 |
+
def _handle_note(self, action: dict, width: int, height: int) -> ActionResult:
|
| 255 |
+
"""Handle note action (placeholder for content recording)."""
|
| 256 |
+
# This action is typically used for recording page content
|
| 257 |
+
# Implementation depends on specific requirements
|
| 258 |
+
return ActionResult(True, False)
|
| 259 |
+
|
| 260 |
+
def _handle_call_api(self, action: dict, width: int, height: int) -> ActionResult:
|
| 261 |
+
"""Handle API call action (placeholder for summarization)."""
|
| 262 |
+
# This action is typically used for content summarization
|
| 263 |
+
# Implementation depends on specific requirements
|
| 264 |
+
return ActionResult(True, False)
|
| 265 |
+
|
| 266 |
+
def _handle_interact(self, action: dict, width: int, height: int) -> ActionResult:
|
| 267 |
+
"""Handle interaction request (user choice needed)."""
|
| 268 |
+
# This action signals that user input is needed
|
| 269 |
+
return ActionResult(True, False, message="User interaction required")
|
| 270 |
+
|
| 271 |
+
@staticmethod
|
| 272 |
+
def _default_confirmation(message: str) -> bool:
|
| 273 |
+
"""Default confirmation callback using console input."""
|
| 274 |
+
response = input(f"Sensitive operation: {message}\nConfirm? (Y/N): ")
|
| 275 |
+
return response.upper() == "Y"
|
| 276 |
+
|
| 277 |
+
@staticmethod
|
| 278 |
+
def _default_takeover(message: str) -> None:
|
| 279 |
+
"""Default takeover callback using console input."""
|
| 280 |
+
input(f"{message}\nPress Enter after completing manual operation...")
|
phone_agent/adb/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ADB utilities for Android device interaction."""
|
| 2 |
+
|
| 3 |
+
from phone_agent.adb.connection import (
|
| 4 |
+
ADBConnection,
|
| 5 |
+
ConnectionType,
|
| 6 |
+
DeviceInfo,
|
| 7 |
+
list_devices,
|
| 8 |
+
quick_connect,
|
| 9 |
+
)
|
| 10 |
+
from phone_agent.adb.device import (
|
| 11 |
+
back,
|
| 12 |
+
double_tap,
|
| 13 |
+
get_current_app,
|
| 14 |
+
home,
|
| 15 |
+
launch_app,
|
| 16 |
+
long_press,
|
| 17 |
+
swipe,
|
| 18 |
+
tap,
|
| 19 |
+
)
|
| 20 |
+
from phone_agent.adb.input import (
|
| 21 |
+
clear_text,
|
| 22 |
+
detect_and_set_adb_keyboard,
|
| 23 |
+
restore_keyboard,
|
| 24 |
+
type_text,
|
| 25 |
+
)
|
| 26 |
+
from phone_agent.adb.screenshot import get_screenshot
|
| 27 |
+
|
| 28 |
+
__all__ = [
|
| 29 |
+
# Screenshot
|
| 30 |
+
"get_screenshot",
|
| 31 |
+
# Input
|
| 32 |
+
"type_text",
|
| 33 |
+
"clear_text",
|
| 34 |
+
"detect_and_set_adb_keyboard",
|
| 35 |
+
"restore_keyboard",
|
| 36 |
+
# Device control
|
| 37 |
+
"get_current_app",
|
| 38 |
+
"tap",
|
| 39 |
+
"swipe",
|
| 40 |
+
"back",
|
| 41 |
+
"home",
|
| 42 |
+
"double_tap",
|
| 43 |
+
"long_press",
|
| 44 |
+
"launch_app",
|
| 45 |
+
# Connection management
|
| 46 |
+
"ADBConnection",
|
| 47 |
+
"DeviceInfo",
|
| 48 |
+
"ConnectionType",
|
| 49 |
+
"quick_connect",
|
| 50 |
+
"list_devices",
|
| 51 |
+
]
|
phone_agent/adb/connection.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ADB connection management for local and remote devices."""
|
| 2 |
+
|
| 3 |
+
import subprocess
|
| 4 |
+
import time
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from enum import Enum
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
from phone_agent.config.timing import TIMING_CONFIG
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ConnectionType(Enum):
|
| 13 |
+
"""Type of ADB connection."""
|
| 14 |
+
|
| 15 |
+
USB = "usb"
|
| 16 |
+
WIFI = "wifi"
|
| 17 |
+
REMOTE = "remote"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class DeviceInfo:
|
| 22 |
+
"""Information about a connected device."""
|
| 23 |
+
|
| 24 |
+
device_id: str
|
| 25 |
+
status: str
|
| 26 |
+
connection_type: ConnectionType
|
| 27 |
+
model: str | None = None
|
| 28 |
+
android_version: str | None = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ADBConnection:
|
| 32 |
+
"""
|
| 33 |
+
Manages ADB connections to Android devices.
|
| 34 |
+
|
| 35 |
+
Supports USB, WiFi, and remote TCP/IP connections.
|
| 36 |
+
|
| 37 |
+
Example:
|
| 38 |
+
>>> conn = ADBConnection()
|
| 39 |
+
>>> # Connect to remote device
|
| 40 |
+
>>> conn.connect("192.168.1.100:5555")
|
| 41 |
+
>>> # List devices
|
| 42 |
+
>>> devices = conn.list_devices()
|
| 43 |
+
>>> # Disconnect
|
| 44 |
+
>>> conn.disconnect("192.168.1.100:5555")
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, adb_path: str = "adb"):
|
| 48 |
+
"""
|
| 49 |
+
Initialize ADB connection manager.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
adb_path: Path to ADB executable.
|
| 53 |
+
"""
|
| 54 |
+
self.adb_path = adb_path
|
| 55 |
+
|
| 56 |
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
| 57 |
+
"""
|
| 58 |
+
Connect to a remote device via TCP/IP.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
|
| 62 |
+
timeout: Connection timeout in seconds.
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
Tuple of (success, message).
|
| 66 |
+
|
| 67 |
+
Note:
|
| 68 |
+
The remote device must have TCP/IP debugging enabled.
|
| 69 |
+
On the device, run: adb tcpip 5555
|
| 70 |
+
"""
|
| 71 |
+
# Validate address format
|
| 72 |
+
if ":" not in address:
|
| 73 |
+
address = f"{address}:5555" # Default ADB port
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
result = subprocess.run(
|
| 77 |
+
[self.adb_path, "connect", address],
|
| 78 |
+
capture_output=True,
|
| 79 |
+
text=True,
|
| 80 |
+
timeout=timeout,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
output = result.stdout + result.stderr
|
| 84 |
+
|
| 85 |
+
if "connected" in output.lower():
|
| 86 |
+
return True, f"Connected to {address}"
|
| 87 |
+
elif "already connected" in output.lower():
|
| 88 |
+
return True, f"Already connected to {address}"
|
| 89 |
+
else:
|
| 90 |
+
return False, output.strip()
|
| 91 |
+
|
| 92 |
+
except subprocess.TimeoutExpired:
|
| 93 |
+
return False, f"Connection timeout after {timeout}s"
|
| 94 |
+
except Exception as e:
|
| 95 |
+
return False, f"Connection error: {e}"
|
| 96 |
+
|
| 97 |
+
def disconnect(self, address: str | None = None) -> tuple[bool, str]:
|
| 98 |
+
"""
|
| 99 |
+
Disconnect from a remote device.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
address: Device address to disconnect. If None, disconnects all.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
Tuple of (success, message).
|
| 106 |
+
"""
|
| 107 |
+
try:
|
| 108 |
+
cmd = [self.adb_path, "disconnect"]
|
| 109 |
+
if address:
|
| 110 |
+
cmd.append(address)
|
| 111 |
+
|
| 112 |
+
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
|
| 113 |
+
|
| 114 |
+
output = result.stdout + result.stderr
|
| 115 |
+
return True, output.strip() or "Disconnected"
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
return False, f"Disconnect error: {e}"
|
| 119 |
+
|
| 120 |
+
def list_devices(self) -> list[DeviceInfo]:
|
| 121 |
+
"""
|
| 122 |
+
List all connected devices.
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
List of DeviceInfo objects.
|
| 126 |
+
"""
|
| 127 |
+
try:
|
| 128 |
+
result = subprocess.run(
|
| 129 |
+
[self.adb_path, "devices", "-l"],
|
| 130 |
+
capture_output=True,
|
| 131 |
+
text=True,
|
| 132 |
+
timeout=5,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
devices = []
|
| 136 |
+
for line in result.stdout.strip().split("\n")[1:]: # Skip header
|
| 137 |
+
if not line.strip():
|
| 138 |
+
continue
|
| 139 |
+
|
| 140 |
+
parts = line.split()
|
| 141 |
+
if len(parts) >= 2:
|
| 142 |
+
device_id = parts[0]
|
| 143 |
+
status = parts[1]
|
| 144 |
+
|
| 145 |
+
# Determine connection type
|
| 146 |
+
if ":" in device_id:
|
| 147 |
+
conn_type = ConnectionType.REMOTE
|
| 148 |
+
elif "emulator" in device_id:
|
| 149 |
+
conn_type = ConnectionType.USB # Emulator via USB
|
| 150 |
+
else:
|
| 151 |
+
conn_type = ConnectionType.USB
|
| 152 |
+
|
| 153 |
+
# Parse additional info
|
| 154 |
+
model = None
|
| 155 |
+
for part in parts[2:]:
|
| 156 |
+
if part.startswith("model:"):
|
| 157 |
+
model = part.split(":", 1)[1]
|
| 158 |
+
break
|
| 159 |
+
|
| 160 |
+
devices.append(
|
| 161 |
+
DeviceInfo(
|
| 162 |
+
device_id=device_id,
|
| 163 |
+
status=status,
|
| 164 |
+
connection_type=conn_type,
|
| 165 |
+
model=model,
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
return devices
|
| 170 |
+
|
| 171 |
+
except Exception as e:
|
| 172 |
+
print(f"Error listing devices: {e}")
|
| 173 |
+
return []
|
| 174 |
+
|
| 175 |
+
def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
|
| 176 |
+
"""
|
| 177 |
+
Get detailed information about a device.
|
| 178 |
+
|
| 179 |
+
Args:
|
| 180 |
+
device_id: Device ID. If None, uses first available device.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
DeviceInfo or None if not found.
|
| 184 |
+
"""
|
| 185 |
+
devices = self.list_devices()
|
| 186 |
+
|
| 187 |
+
if not devices:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
if device_id is None:
|
| 191 |
+
return devices[0]
|
| 192 |
+
|
| 193 |
+
for device in devices:
|
| 194 |
+
if device.device_id == device_id:
|
| 195 |
+
return device
|
| 196 |
+
|
| 197 |
+
return None
|
| 198 |
+
|
| 199 |
+
def is_connected(self, device_id: str | None = None) -> bool:
|
| 200 |
+
"""
|
| 201 |
+
Check if a device is connected.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
device_id: Device ID to check. If None, checks if any device is connected.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
True if connected, False otherwise.
|
| 208 |
+
"""
|
| 209 |
+
devices = self.list_devices()
|
| 210 |
+
|
| 211 |
+
if not devices:
|
| 212 |
+
return False
|
| 213 |
+
|
| 214 |
+
if device_id is None:
|
| 215 |
+
return any(d.status == "device" for d in devices)
|
| 216 |
+
|
| 217 |
+
return any(d.device_id == device_id and d.status == "device" for d in devices)
|
| 218 |
+
|
| 219 |
+
def enable_tcpip(
|
| 220 |
+
self, port: int = 5555, device_id: str | None = None
|
| 221 |
+
) -> tuple[bool, str]:
|
| 222 |
+
"""
|
| 223 |
+
Enable TCP/IP debugging on a USB-connected device.
|
| 224 |
+
|
| 225 |
+
This allows subsequent wireless connections to the device.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
port: TCP port for ADB (default: 5555).
|
| 229 |
+
device_id: Device ID. If None, uses first available device.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
Tuple of (success, message).
|
| 233 |
+
|
| 234 |
+
Note:
|
| 235 |
+
The device must be connected via USB first.
|
| 236 |
+
After this, you can disconnect USB and connect via WiFi.
|
| 237 |
+
"""
|
| 238 |
+
try:
|
| 239 |
+
cmd = [self.adb_path]
|
| 240 |
+
if device_id:
|
| 241 |
+
cmd.extend(["-s", device_id])
|
| 242 |
+
cmd.extend(["tcpip", str(port)])
|
| 243 |
+
|
| 244 |
+
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=10)
|
| 245 |
+
|
| 246 |
+
output = result.stdout + result.stderr
|
| 247 |
+
|
| 248 |
+
if "restarting" in output.lower() or result.returncode == 0:
|
| 249 |
+
time.sleep(TIMING_CONFIG.connection.adb_restart_delay)
|
| 250 |
+
return True, f"TCP/IP mode enabled on port {port}"
|
| 251 |
+
else:
|
| 252 |
+
return False, output.strip()
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
return False, f"Error enabling TCP/IP: {e}"
|
| 256 |
+
|
| 257 |
+
def get_device_ip(self, device_id: str | None = None) -> str | None:
|
| 258 |
+
"""
|
| 259 |
+
Get the IP address of a connected device.
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
device_id: Device ID. If None, uses first available device.
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
IP address string or None if not found.
|
| 266 |
+
"""
|
| 267 |
+
try:
|
| 268 |
+
cmd = [self.adb_path]
|
| 269 |
+
if device_id:
|
| 270 |
+
cmd.extend(["-s", device_id])
|
| 271 |
+
cmd.extend(["shell", "ip", "route"])
|
| 272 |
+
|
| 273 |
+
result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
|
| 274 |
+
|
| 275 |
+
# Parse IP from route output
|
| 276 |
+
for line in result.stdout.split("\n"):
|
| 277 |
+
if "src" in line:
|
| 278 |
+
parts = line.split()
|
| 279 |
+
for i, part in enumerate(parts):
|
| 280 |
+
if part == "src" and i + 1 < len(parts):
|
| 281 |
+
return parts[i + 1]
|
| 282 |
+
|
| 283 |
+
# Alternative: try wlan0 interface
|
| 284 |
+
cmd[-1] = "ip addr show wlan0"
|
| 285 |
+
result = subprocess.run(
|
| 286 |
+
cmd[:-1] + ["shell", "ip", "addr", "show", "wlan0"],
|
| 287 |
+
capture_output=True,
|
| 288 |
+
text=True,
|
| 289 |
+
encoding="utf-8",
|
| 290 |
+
timeout=5,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
for line in result.stdout.split("\n"):
|
| 294 |
+
if "inet " in line:
|
| 295 |
+
parts = line.strip().split()
|
| 296 |
+
if len(parts) >= 2:
|
| 297 |
+
return parts[1].split("/")[0]
|
| 298 |
+
|
| 299 |
+
return None
|
| 300 |
+
|
| 301 |
+
except Exception as e:
|
| 302 |
+
print(f"Error getting device IP: {e}")
|
| 303 |
+
return None
|
| 304 |
+
|
| 305 |
+
def restart_server(self) -> tuple[bool, str]:
|
| 306 |
+
"""
|
| 307 |
+
Restart the ADB server.
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
Tuple of (success, message).
|
| 311 |
+
"""
|
| 312 |
+
try:
|
| 313 |
+
# Kill server
|
| 314 |
+
subprocess.run(
|
| 315 |
+
[self.adb_path, "kill-server"], capture_output=True, timeout=5
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
time.sleep(TIMING_CONFIG.connection.server_restart_delay)
|
| 319 |
+
|
| 320 |
+
# Start server
|
| 321 |
+
subprocess.run(
|
| 322 |
+
[self.adb_path, "start-server"], capture_output=True, timeout=5
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
return True, "ADB server restarted"
|
| 326 |
+
|
| 327 |
+
except Exception as e:
|
| 328 |
+
return False, f"Error restarting server: {e}"
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def quick_connect(address: str) -> tuple[bool, str]:
|
| 332 |
+
"""
|
| 333 |
+
Quick helper to connect to a remote device.
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
|
| 337 |
+
|
| 338 |
+
Returns:
|
| 339 |
+
Tuple of (success, message).
|
| 340 |
+
"""
|
| 341 |
+
conn = ADBConnection()
|
| 342 |
+
return conn.connect(address)
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def list_devices() -> list[DeviceInfo]:
|
| 346 |
+
"""
|
| 347 |
+
Quick helper to list connected devices.
|
| 348 |
+
|
| 349 |
+
Returns:
|
| 350 |
+
List of DeviceInfo objects.
|
| 351 |
+
"""
|
| 352 |
+
conn = ADBConnection()
|
| 353 |
+
return conn.list_devices()
|
phone_agent/adb/device.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Device control utilities for Android automation."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
import time
|
| 6 |
+
from typing import List, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from phone_agent.config.apps import APP_PACKAGES
|
| 9 |
+
from phone_agent.config.timing import TIMING_CONFIG
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def get_current_app(device_id: str | None = None) -> str:
|
| 13 |
+
"""
|
| 14 |
+
Get the currently focused app name.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
The app name if recognized, otherwise "System Home".
|
| 21 |
+
"""
|
| 22 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 23 |
+
|
| 24 |
+
result = subprocess.run(
|
| 25 |
+
adb_prefix + ["shell", "dumpsys", "window"], capture_output=True, text=True, encoding="utf-8"
|
| 26 |
+
)
|
| 27 |
+
output = result.stdout
|
| 28 |
+
if not output:
|
| 29 |
+
raise ValueError("No output from dumpsys window")
|
| 30 |
+
|
| 31 |
+
# Parse window focus info
|
| 32 |
+
for line in output.split("\n"):
|
| 33 |
+
if "mCurrentFocus" in line or "mFocusedApp" in line:
|
| 34 |
+
for app_name, package in APP_PACKAGES.items():
|
| 35 |
+
if package in line:
|
| 36 |
+
return app_name
|
| 37 |
+
|
| 38 |
+
return "System Home"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def tap(
|
| 42 |
+
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 43 |
+
) -> None:
|
| 44 |
+
"""
|
| 45 |
+
Tap at the specified coordinates.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
x: X coordinate.
|
| 49 |
+
y: Y coordinate.
|
| 50 |
+
device_id: Optional ADB device ID.
|
| 51 |
+
delay: Delay in seconds after tap. If None, uses configured default.
|
| 52 |
+
"""
|
| 53 |
+
if delay is None:
|
| 54 |
+
delay = TIMING_CONFIG.device.default_tap_delay
|
| 55 |
+
|
| 56 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 57 |
+
|
| 58 |
+
subprocess.run(
|
| 59 |
+
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
| 60 |
+
)
|
| 61 |
+
time.sleep(delay)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def double_tap(
|
| 65 |
+
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 66 |
+
) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Double tap at the specified coordinates.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
x: X coordinate.
|
| 72 |
+
y: Y coordinate.
|
| 73 |
+
device_id: Optional ADB device ID.
|
| 74 |
+
delay: Delay in seconds after double tap. If None, uses configured default.
|
| 75 |
+
"""
|
| 76 |
+
if delay is None:
|
| 77 |
+
delay = TIMING_CONFIG.device.default_double_tap_delay
|
| 78 |
+
|
| 79 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 80 |
+
|
| 81 |
+
subprocess.run(
|
| 82 |
+
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
| 83 |
+
)
|
| 84 |
+
time.sleep(TIMING_CONFIG.device.double_tap_interval)
|
| 85 |
+
subprocess.run(
|
| 86 |
+
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
| 87 |
+
)
|
| 88 |
+
time.sleep(delay)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def long_press(
|
| 92 |
+
x: int,
|
| 93 |
+
y: int,
|
| 94 |
+
duration_ms: int = 3000,
|
| 95 |
+
device_id: str | None = None,
|
| 96 |
+
delay: float | None = None,
|
| 97 |
+
) -> None:
|
| 98 |
+
"""
|
| 99 |
+
Long press at the specified coordinates.
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
x: X coordinate.
|
| 103 |
+
y: Y coordinate.
|
| 104 |
+
duration_ms: Duration of press in milliseconds.
|
| 105 |
+
device_id: Optional ADB device ID.
|
| 106 |
+
delay: Delay in seconds after long press. If None, uses configured default.
|
| 107 |
+
"""
|
| 108 |
+
if delay is None:
|
| 109 |
+
delay = TIMING_CONFIG.device.default_long_press_delay
|
| 110 |
+
|
| 111 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 112 |
+
|
| 113 |
+
subprocess.run(
|
| 114 |
+
adb_prefix
|
| 115 |
+
+ ["shell", "input", "swipe", str(x), str(y), str(x), str(y), str(duration_ms)],
|
| 116 |
+
capture_output=True,
|
| 117 |
+
)
|
| 118 |
+
time.sleep(delay)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def swipe(
|
| 122 |
+
start_x: int,
|
| 123 |
+
start_y: int,
|
| 124 |
+
end_x: int,
|
| 125 |
+
end_y: int,
|
| 126 |
+
duration_ms: int | None = None,
|
| 127 |
+
device_id: str | None = None,
|
| 128 |
+
delay: float | None = None,
|
| 129 |
+
) -> None:
|
| 130 |
+
"""
|
| 131 |
+
Swipe from start to end coordinates.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
start_x: Starting X coordinate.
|
| 135 |
+
start_y: Starting Y coordinate.
|
| 136 |
+
end_x: Ending X coordinate.
|
| 137 |
+
end_y: Ending Y coordinate.
|
| 138 |
+
duration_ms: Duration of swipe in milliseconds (auto-calculated if None).
|
| 139 |
+
device_id: Optional ADB device ID.
|
| 140 |
+
delay: Delay in seconds after swipe. If None, uses configured default.
|
| 141 |
+
"""
|
| 142 |
+
if delay is None:
|
| 143 |
+
delay = TIMING_CONFIG.device.default_swipe_delay
|
| 144 |
+
|
| 145 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 146 |
+
|
| 147 |
+
if duration_ms is None:
|
| 148 |
+
# Calculate duration based on distance
|
| 149 |
+
dist_sq = (start_x - end_x) ** 2 + (start_y - end_y) ** 2
|
| 150 |
+
duration_ms = int(dist_sq / 1000)
|
| 151 |
+
duration_ms = max(1000, min(duration_ms, 2000)) # Clamp between 1000-2000ms
|
| 152 |
+
|
| 153 |
+
subprocess.run(
|
| 154 |
+
adb_prefix
|
| 155 |
+
+ [
|
| 156 |
+
"shell",
|
| 157 |
+
"input",
|
| 158 |
+
"swipe",
|
| 159 |
+
str(start_x),
|
| 160 |
+
str(start_y),
|
| 161 |
+
str(end_x),
|
| 162 |
+
str(end_y),
|
| 163 |
+
str(duration_ms),
|
| 164 |
+
],
|
| 165 |
+
capture_output=True,
|
| 166 |
+
)
|
| 167 |
+
time.sleep(delay)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def back(device_id: str | None = None, delay: float | None = None) -> None:
|
| 171 |
+
"""
|
| 172 |
+
Press the back button.
|
| 173 |
+
|
| 174 |
+
Args:
|
| 175 |
+
device_id: Optional ADB device ID.
|
| 176 |
+
delay: Delay in seconds after pressing back. If None, uses configured default.
|
| 177 |
+
"""
|
| 178 |
+
if delay is None:
|
| 179 |
+
delay = TIMING_CONFIG.device.default_back_delay
|
| 180 |
+
|
| 181 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 182 |
+
|
| 183 |
+
subprocess.run(
|
| 184 |
+
adb_prefix + ["shell", "input", "keyevent", "4"], capture_output=True
|
| 185 |
+
)
|
| 186 |
+
time.sleep(delay)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def home(device_id: str | None = None, delay: float | None = None) -> None:
|
| 190 |
+
"""
|
| 191 |
+
Press the home button.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
device_id: Optional ADB device ID.
|
| 195 |
+
delay: Delay in seconds after pressing home. If None, uses configured default.
|
| 196 |
+
"""
|
| 197 |
+
if delay is None:
|
| 198 |
+
delay = TIMING_CONFIG.device.default_home_delay
|
| 199 |
+
|
| 200 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 201 |
+
|
| 202 |
+
subprocess.run(
|
| 203 |
+
adb_prefix + ["shell", "input", "keyevent", "KEYCODE_HOME"], capture_output=True
|
| 204 |
+
)
|
| 205 |
+
time.sleep(delay)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def launch_app(
|
| 209 |
+
app_name: str, device_id: str | None = None, delay: float | None = None
|
| 210 |
+
) -> bool:
|
| 211 |
+
"""
|
| 212 |
+
Launch an app by name.
|
| 213 |
+
|
| 214 |
+
Args:
|
| 215 |
+
app_name: The app name (must be in APP_PACKAGES).
|
| 216 |
+
device_id: Optional ADB device ID.
|
| 217 |
+
delay: Delay in seconds after launching. If None, uses configured default.
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
True if app was launched, False if app not found.
|
| 221 |
+
"""
|
| 222 |
+
if delay is None:
|
| 223 |
+
delay = TIMING_CONFIG.device.default_launch_delay
|
| 224 |
+
|
| 225 |
+
if app_name not in APP_PACKAGES:
|
| 226 |
+
return False
|
| 227 |
+
|
| 228 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 229 |
+
package = APP_PACKAGES[app_name]
|
| 230 |
+
|
| 231 |
+
subprocess.run(
|
| 232 |
+
adb_prefix
|
| 233 |
+
+ [
|
| 234 |
+
"shell",
|
| 235 |
+
"monkey",
|
| 236 |
+
"-p",
|
| 237 |
+
package,
|
| 238 |
+
"-c",
|
| 239 |
+
"android.intent.category.LAUNCHER",
|
| 240 |
+
"1",
|
| 241 |
+
],
|
| 242 |
+
capture_output=True,
|
| 243 |
+
)
|
| 244 |
+
time.sleep(delay)
|
| 245 |
+
return True
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def _get_adb_prefix(device_id: str | None) -> list:
|
| 249 |
+
"""Get ADB command prefix with optional device specifier."""
|
| 250 |
+
if device_id:
|
| 251 |
+
return ["adb", "-s", device_id]
|
| 252 |
+
return ["adb"]
|
phone_agent/adb/input.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Input utilities for Android device text input."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import subprocess
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def type_text(text: str, device_id: str | None = None) -> None:
|
| 9 |
+
"""
|
| 10 |
+
Type text into the currently focused input field using ADB Keyboard.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
text: The text to type.
|
| 14 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 15 |
+
|
| 16 |
+
Note:
|
| 17 |
+
Requires ADB Keyboard to be installed on the device.
|
| 18 |
+
See: https://github.com/nicnocquee/AdbKeyboard
|
| 19 |
+
"""
|
| 20 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 21 |
+
encoded_text = base64.b64encode(text.encode("utf-8")).decode("utf-8")
|
| 22 |
+
|
| 23 |
+
subprocess.run(
|
| 24 |
+
adb_prefix
|
| 25 |
+
+ [
|
| 26 |
+
"shell",
|
| 27 |
+
"am",
|
| 28 |
+
"broadcast",
|
| 29 |
+
"-a",
|
| 30 |
+
"ADB_INPUT_B64",
|
| 31 |
+
"--es",
|
| 32 |
+
"msg",
|
| 33 |
+
encoded_text,
|
| 34 |
+
],
|
| 35 |
+
capture_output=True,
|
| 36 |
+
text=True,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def clear_text(device_id: str | None = None) -> None:
|
| 41 |
+
"""
|
| 42 |
+
Clear text in the currently focused input field.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 46 |
+
"""
|
| 47 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 48 |
+
|
| 49 |
+
subprocess.run(
|
| 50 |
+
adb_prefix + ["shell", "am", "broadcast", "-a", "ADB_CLEAR_TEXT"],
|
| 51 |
+
capture_output=True,
|
| 52 |
+
text=True,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def detect_and_set_adb_keyboard(device_id: str | None = None) -> str:
|
| 57 |
+
"""
|
| 58 |
+
Detect current keyboard and switch to ADB Keyboard if needed.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
The original keyboard IME identifier for later restoration.
|
| 65 |
+
"""
|
| 66 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 67 |
+
|
| 68 |
+
# Get current IME
|
| 69 |
+
result = subprocess.run(
|
| 70 |
+
adb_prefix + ["shell", "settings", "get", "secure", "default_input_method"],
|
| 71 |
+
capture_output=True,
|
| 72 |
+
text=True,
|
| 73 |
+
)
|
| 74 |
+
current_ime = (result.stdout + result.stderr).strip()
|
| 75 |
+
|
| 76 |
+
# Switch to ADB Keyboard if not already set
|
| 77 |
+
if "com.android.adbkeyboard/.AdbIME" not in current_ime:
|
| 78 |
+
subprocess.run(
|
| 79 |
+
adb_prefix + ["shell", "ime", "set", "com.android.adbkeyboard/.AdbIME"],
|
| 80 |
+
capture_output=True,
|
| 81 |
+
text=True,
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
# Warm up the keyboard
|
| 85 |
+
type_text("", device_id)
|
| 86 |
+
|
| 87 |
+
return current_ime
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def restore_keyboard(ime: str, device_id: str | None = None) -> None:
|
| 91 |
+
"""
|
| 92 |
+
Restore the original keyboard IME.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
ime: The IME identifier to restore.
|
| 96 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 97 |
+
"""
|
| 98 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 99 |
+
|
| 100 |
+
subprocess.run(
|
| 101 |
+
adb_prefix + ["shell", "ime", "set", ime], capture_output=True, text=True
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _get_adb_prefix(device_id: str | None) -> list:
|
| 106 |
+
"""Get ADB command prefix with optional device specifier."""
|
| 107 |
+
if device_id:
|
| 108 |
+
return ["adb", "-s", device_id]
|
| 109 |
+
return ["adb"]
|
phone_agent/adb/screenshot.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Screenshot utilities for capturing Android device screen."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import os
|
| 5 |
+
import subprocess
|
| 6 |
+
import tempfile
|
| 7 |
+
import uuid
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from io import BytesIO
|
| 10 |
+
from typing import Tuple
|
| 11 |
+
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class Screenshot:
|
| 17 |
+
"""Represents a captured screenshot."""
|
| 18 |
+
|
| 19 |
+
base64_data: str
|
| 20 |
+
width: int
|
| 21 |
+
height: int
|
| 22 |
+
is_sensitive: bool = False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_screenshot(device_id: str | None = None, timeout: int = 10) -> Screenshot:
|
| 26 |
+
"""
|
| 27 |
+
Capture a screenshot from the connected Android device.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
device_id: Optional ADB device ID for multi-device setups.
|
| 31 |
+
timeout: Timeout in seconds for screenshot operations.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
Screenshot object containing base64 data and dimensions.
|
| 35 |
+
|
| 36 |
+
Note:
|
| 37 |
+
If the screenshot fails (e.g., on sensitive screens like payment pages),
|
| 38 |
+
a black fallback image is returned with is_sensitive=True.
|
| 39 |
+
"""
|
| 40 |
+
temp_path = os.path.join(tempfile.gettempdir(), f"screenshot_{uuid.uuid4()}.png")
|
| 41 |
+
adb_prefix = _get_adb_prefix(device_id)
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
# Execute screenshot command
|
| 45 |
+
result = subprocess.run(
|
| 46 |
+
adb_prefix + ["shell", "screencap", "-p", "/sdcard/tmp.png"],
|
| 47 |
+
capture_output=True,
|
| 48 |
+
text=True,
|
| 49 |
+
timeout=timeout,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Check for screenshot failure (sensitive screen)
|
| 53 |
+
output = result.stdout + result.stderr
|
| 54 |
+
if "Status: -1" in output or "Failed" in output:
|
| 55 |
+
return _create_fallback_screenshot(is_sensitive=True)
|
| 56 |
+
|
| 57 |
+
# Pull screenshot to local temp path
|
| 58 |
+
subprocess.run(
|
| 59 |
+
adb_prefix + ["pull", "/sdcard/tmp.png", temp_path],
|
| 60 |
+
capture_output=True,
|
| 61 |
+
text=True,
|
| 62 |
+
timeout=5,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
if not os.path.exists(temp_path):
|
| 66 |
+
return _create_fallback_screenshot(is_sensitive=False)
|
| 67 |
+
|
| 68 |
+
# Read and encode image
|
| 69 |
+
img = Image.open(temp_path)
|
| 70 |
+
width, height = img.size
|
| 71 |
+
|
| 72 |
+
buffered = BytesIO()
|
| 73 |
+
img.save(buffered, format="PNG")
|
| 74 |
+
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 75 |
+
|
| 76 |
+
# Cleanup
|
| 77 |
+
os.remove(temp_path)
|
| 78 |
+
|
| 79 |
+
return Screenshot(
|
| 80 |
+
base64_data=base64_data, width=width, height=height, is_sensitive=False
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f"Screenshot error: {e}")
|
| 85 |
+
return _create_fallback_screenshot(is_sensitive=False)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _get_adb_prefix(device_id: str | None) -> list:
|
| 89 |
+
"""Get ADB command prefix with optional device specifier."""
|
| 90 |
+
if device_id:
|
| 91 |
+
return ["adb", "-s", device_id]
|
| 92 |
+
return ["adb"]
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _create_fallback_screenshot(is_sensitive: bool) -> Screenshot:
|
| 96 |
+
"""Create a black fallback image when screenshot fails."""
|
| 97 |
+
default_width, default_height = 1080, 2400
|
| 98 |
+
|
| 99 |
+
black_img = Image.new("RGB", (default_width, default_height), color="black")
|
| 100 |
+
buffered = BytesIO()
|
| 101 |
+
black_img.save(buffered, format="PNG")
|
| 102 |
+
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 103 |
+
|
| 104 |
+
return Screenshot(
|
| 105 |
+
base64_data=base64_data,
|
| 106 |
+
width=default_width,
|
| 107 |
+
height=default_height,
|
| 108 |
+
is_sensitive=is_sensitive,
|
| 109 |
+
)
|
phone_agent/agent.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Main PhoneAgent class for orchestrating phone automation."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import traceback
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any, Callable
|
| 7 |
+
|
| 8 |
+
from phone_agent.actions import ActionHandler
|
| 9 |
+
from phone_agent.actions.handler import do, finish, parse_action
|
| 10 |
+
from phone_agent.config import get_messages, get_system_prompt
|
| 11 |
+
from phone_agent.device_factory import get_device_factory
|
| 12 |
+
from phone_agent.model import ModelClient, ModelConfig
|
| 13 |
+
from phone_agent.model.client import MessageBuilder
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class AgentConfig:
|
| 18 |
+
"""Configuration for the PhoneAgent."""
|
| 19 |
+
|
| 20 |
+
max_steps: int = 100
|
| 21 |
+
device_id: str | None = None
|
| 22 |
+
lang: str = "cn"
|
| 23 |
+
system_prompt: str | None = None
|
| 24 |
+
verbose: bool = True
|
| 25 |
+
|
| 26 |
+
def __post_init__(self):
|
| 27 |
+
if self.system_prompt is None:
|
| 28 |
+
self.system_prompt = get_system_prompt(self.lang)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class StepResult:
|
| 33 |
+
"""Result of a single agent step."""
|
| 34 |
+
|
| 35 |
+
success: bool
|
| 36 |
+
finished: bool
|
| 37 |
+
action: dict[str, Any] | None
|
| 38 |
+
thinking: str
|
| 39 |
+
message: str | None = None
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class PhoneAgent:
|
| 43 |
+
"""
|
| 44 |
+
AI-powered agent for automating Android phone interactions.
|
| 45 |
+
|
| 46 |
+
The agent uses a vision-language model to understand screen content
|
| 47 |
+
and decide on actions to complete user tasks.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
model_config: Configuration for the AI model.
|
| 51 |
+
agent_config: Configuration for the agent behavior.
|
| 52 |
+
confirmation_callback: Optional callback for sensitive action confirmation.
|
| 53 |
+
takeover_callback: Optional callback for takeover requests.
|
| 54 |
+
|
| 55 |
+
Example:
|
| 56 |
+
>>> from phone_agent import PhoneAgent
|
| 57 |
+
>>> from phone_agent.model import ModelConfig
|
| 58 |
+
>>>
|
| 59 |
+
>>> model_config = ModelConfig(base_url="http://localhost:8000/v1")
|
| 60 |
+
>>> agent = PhoneAgent(model_config)
|
| 61 |
+
>>> agent.run("Open WeChat and send a message to John")
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def __init__(
|
| 65 |
+
self,
|
| 66 |
+
model_config: ModelConfig | None = None,
|
| 67 |
+
agent_config: AgentConfig | None = None,
|
| 68 |
+
confirmation_callback: Callable[[str], bool] | None = None,
|
| 69 |
+
takeover_callback: Callable[[str], None] | None = None,
|
| 70 |
+
):
|
| 71 |
+
self.model_config = model_config or ModelConfig()
|
| 72 |
+
self.agent_config = agent_config or AgentConfig()
|
| 73 |
+
|
| 74 |
+
self.model_client = ModelClient(self.model_config)
|
| 75 |
+
self.action_handler = ActionHandler(
|
| 76 |
+
device_id=self.agent_config.device_id,
|
| 77 |
+
confirmation_callback=confirmation_callback,
|
| 78 |
+
takeover_callback=takeover_callback,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
self._context: list[dict[str, Any]] = []
|
| 82 |
+
self._step_count = 0
|
| 83 |
+
|
| 84 |
+
def run(self, task: str) -> str:
|
| 85 |
+
"""
|
| 86 |
+
Run the agent to complete a task.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
task: Natural language description of the task.
|
| 90 |
+
|
| 91 |
+
Returns:
|
| 92 |
+
Final message from the agent.
|
| 93 |
+
"""
|
| 94 |
+
self._context = []
|
| 95 |
+
self._step_count = 0
|
| 96 |
+
|
| 97 |
+
# First step with user prompt
|
| 98 |
+
result = self._execute_step(task, is_first=True)
|
| 99 |
+
|
| 100 |
+
if result.finished:
|
| 101 |
+
return result.message or "Task completed"
|
| 102 |
+
|
| 103 |
+
# Continue until finished or max steps reached
|
| 104 |
+
while self._step_count < self.agent_config.max_steps:
|
| 105 |
+
result = self._execute_step(is_first=False)
|
| 106 |
+
|
| 107 |
+
if result.finished:
|
| 108 |
+
return result.message or "Task completed"
|
| 109 |
+
|
| 110 |
+
return "Max steps reached"
|
| 111 |
+
|
| 112 |
+
def step(self, task: str | None = None) -> StepResult:
|
| 113 |
+
"""
|
| 114 |
+
Execute a single step of the agent.
|
| 115 |
+
|
| 116 |
+
Useful for manual control or debugging.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
task: Task description (only needed for first step).
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
StepResult with step details.
|
| 123 |
+
"""
|
| 124 |
+
is_first = len(self._context) == 0
|
| 125 |
+
|
| 126 |
+
if is_first and not task:
|
| 127 |
+
raise ValueError("Task is required for the first step")
|
| 128 |
+
|
| 129 |
+
return self._execute_step(task, is_first)
|
| 130 |
+
|
| 131 |
+
def reset(self) -> None:
|
| 132 |
+
"""Reset the agent state for a new task."""
|
| 133 |
+
self._context = []
|
| 134 |
+
self._step_count = 0
|
| 135 |
+
|
| 136 |
+
def _execute_step(
|
| 137 |
+
self, user_prompt: str | None = None, is_first: bool = False
|
| 138 |
+
) -> StepResult:
|
| 139 |
+
"""Execute a single step of the agent loop."""
|
| 140 |
+
self._step_count += 1
|
| 141 |
+
|
| 142 |
+
# Capture current screen state
|
| 143 |
+
device_factory = get_device_factory()
|
| 144 |
+
screenshot = device_factory.get_screenshot(self.agent_config.device_id)
|
| 145 |
+
current_app = device_factory.get_current_app(self.agent_config.device_id)
|
| 146 |
+
|
| 147 |
+
# Build messages
|
| 148 |
+
if is_first:
|
| 149 |
+
self._context.append(
|
| 150 |
+
MessageBuilder.create_system_message(self.agent_config.system_prompt)
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
| 154 |
+
text_content = f"{user_prompt}\n\n{screen_info}"
|
| 155 |
+
|
| 156 |
+
self._context.append(
|
| 157 |
+
MessageBuilder.create_user_message(
|
| 158 |
+
text=text_content, image_base64=screenshot.base64_data
|
| 159 |
+
)
|
| 160 |
+
)
|
| 161 |
+
else:
|
| 162 |
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
| 163 |
+
text_content = f"** Screen Info **\n\n{screen_info}"
|
| 164 |
+
|
| 165 |
+
self._context.append(
|
| 166 |
+
MessageBuilder.create_user_message(
|
| 167 |
+
text=text_content, image_base64=screenshot.base64_data
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Get model response
|
| 172 |
+
try:
|
| 173 |
+
msgs = get_messages(self.agent_config.lang)
|
| 174 |
+
print("\n" + "=" * 50)
|
| 175 |
+
print(f"💭 {msgs['thinking']}:")
|
| 176 |
+
print("-" * 50)
|
| 177 |
+
response = self.model_client.request(self._context)
|
| 178 |
+
except Exception as e:
|
| 179 |
+
if self.agent_config.verbose:
|
| 180 |
+
traceback.print_exc()
|
| 181 |
+
return StepResult(
|
| 182 |
+
success=False,
|
| 183 |
+
finished=True,
|
| 184 |
+
action=None,
|
| 185 |
+
thinking="",
|
| 186 |
+
message=f"Model error: {e}",
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Parse action from response
|
| 190 |
+
try:
|
| 191 |
+
action = parse_action(response.action)
|
| 192 |
+
except ValueError:
|
| 193 |
+
if self.agent_config.verbose:
|
| 194 |
+
traceback.print_exc()
|
| 195 |
+
action = finish(message=response.action)
|
| 196 |
+
|
| 197 |
+
if self.agent_config.verbose:
|
| 198 |
+
# Print thinking process
|
| 199 |
+
print("-" * 50)
|
| 200 |
+
print(f"🎯 {msgs['action']}:")
|
| 201 |
+
print(json.dumps(action, ensure_ascii=False, indent=2))
|
| 202 |
+
print("=" * 50 + "\n")
|
| 203 |
+
|
| 204 |
+
# Remove image from context to save space
|
| 205 |
+
self._context[-1] = MessageBuilder.remove_images_from_message(self._context[-1])
|
| 206 |
+
|
| 207 |
+
# Execute action
|
| 208 |
+
try:
|
| 209 |
+
result = self.action_handler.execute(
|
| 210 |
+
action, screenshot.width, screenshot.height
|
| 211 |
+
)
|
| 212 |
+
except Exception as e:
|
| 213 |
+
if self.agent_config.verbose:
|
| 214 |
+
traceback.print_exc()
|
| 215 |
+
result = self.action_handler.execute(
|
| 216 |
+
finish(message=str(e)), screenshot.width, screenshot.height
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Add assistant response to context
|
| 220 |
+
self._context.append(
|
| 221 |
+
MessageBuilder.create_assistant_message(
|
| 222 |
+
f"<think>{response.thinking}</think><answer>{response.action}</answer>"
|
| 223 |
+
)
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# Check if finished
|
| 227 |
+
finished = action.get("_metadata") == "finish" or result.should_finish
|
| 228 |
+
|
| 229 |
+
if finished and self.agent_config.verbose:
|
| 230 |
+
msgs = get_messages(self.agent_config.lang)
|
| 231 |
+
print("\n" + "🎉 " + "=" * 48)
|
| 232 |
+
print(
|
| 233 |
+
f"✅ {msgs['task_completed']}: {result.message or action.get('message', msgs['done'])}"
|
| 234 |
+
)
|
| 235 |
+
print("=" * 50 + "\n")
|
| 236 |
+
|
| 237 |
+
return StepResult(
|
| 238 |
+
success=result.success,
|
| 239 |
+
finished=finished,
|
| 240 |
+
action=action,
|
| 241 |
+
thinking=response.thinking,
|
| 242 |
+
message=result.message or action.get("message"),
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def context(self) -> list[dict[str, Any]]:
|
| 247 |
+
"""Get the current conversation context."""
|
| 248 |
+
return self._context.copy()
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def step_count(self) -> int:
|
| 252 |
+
"""Get the current step count."""
|
| 253 |
+
return self._step_count
|
phone_agent/agent_ios.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""iOS PhoneAgent class for orchestrating iOS phone automation."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import traceback
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from typing import Any, Callable
|
| 7 |
+
|
| 8 |
+
from phone_agent.actions.handler import do, finish, parse_action
|
| 9 |
+
from phone_agent.actions.handler_ios import IOSActionHandler
|
| 10 |
+
from phone_agent.config import get_messages, get_system_prompt
|
| 11 |
+
from phone_agent.model import ModelClient, ModelConfig
|
| 12 |
+
from phone_agent.model.client import MessageBuilder
|
| 13 |
+
from phone_agent.xctest import XCTestConnection, get_current_app, get_screenshot
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class IOSAgentConfig:
|
| 18 |
+
"""Configuration for the iOS PhoneAgent."""
|
| 19 |
+
|
| 20 |
+
max_steps: int = 100
|
| 21 |
+
wda_url: str = "http://localhost:8100"
|
| 22 |
+
session_id: str | None = None
|
| 23 |
+
device_id: str | None = None # iOS device UDID
|
| 24 |
+
lang: str = "cn"
|
| 25 |
+
system_prompt: str | None = None
|
| 26 |
+
verbose: bool = True
|
| 27 |
+
|
| 28 |
+
def __post_init__(self):
|
| 29 |
+
if self.system_prompt is None:
|
| 30 |
+
self.system_prompt = get_system_prompt(self.lang)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class StepResult:
|
| 35 |
+
"""Result of a single agent step."""
|
| 36 |
+
|
| 37 |
+
success: bool
|
| 38 |
+
finished: bool
|
| 39 |
+
action: dict[str, Any] | None
|
| 40 |
+
thinking: str
|
| 41 |
+
message: str | None = None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class IOSPhoneAgent:
|
| 45 |
+
"""
|
| 46 |
+
AI-powered agent for automating iOS phone interactions.
|
| 47 |
+
|
| 48 |
+
The agent uses a vision-language model to understand screen content
|
| 49 |
+
and decide on actions to complete user tasks via WebDriverAgent.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
model_config: Configuration for the AI model.
|
| 53 |
+
agent_config: Configuration for the iOS agent behavior.
|
| 54 |
+
confirmation_callback: Optional callback for sensitive action confirmation.
|
| 55 |
+
takeover_callback: Optional callback for takeover requests.
|
| 56 |
+
|
| 57 |
+
Example:
|
| 58 |
+
>>> from phone_agent.agent_ios import IOSPhoneAgent, IOSAgentConfig
|
| 59 |
+
>>> from phone_agent.model import ModelConfig
|
| 60 |
+
>>>
|
| 61 |
+
>>> model_config = ModelConfig(base_url="http://localhost:8000/v1")
|
| 62 |
+
>>> agent_config = IOSAgentConfig(wda_url="http://localhost:8100")
|
| 63 |
+
>>> agent = IOSPhoneAgent(model_config, agent_config)
|
| 64 |
+
>>> agent.run("Open Safari and search for Apple")
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
model_config: ModelConfig | None = None,
|
| 70 |
+
agent_config: IOSAgentConfig | None = None,
|
| 71 |
+
confirmation_callback: Callable[[str], bool] | None = None,
|
| 72 |
+
takeover_callback: Callable[[str], None] | None = None,
|
| 73 |
+
):
|
| 74 |
+
self.model_config = model_config or ModelConfig()
|
| 75 |
+
self.agent_config = agent_config or IOSAgentConfig()
|
| 76 |
+
|
| 77 |
+
self.model_client = ModelClient(self.model_config)
|
| 78 |
+
|
| 79 |
+
# Initialize WDA connection and create session if needed
|
| 80 |
+
self.wda_connection = XCTestConnection(wda_url=self.agent_config.wda_url)
|
| 81 |
+
|
| 82 |
+
# Auto-create session if not provided
|
| 83 |
+
if self.agent_config.session_id is None:
|
| 84 |
+
success, session_id = self.wda_connection.start_wda_session()
|
| 85 |
+
if success and session_id != "session_started":
|
| 86 |
+
self.agent_config.session_id = session_id
|
| 87 |
+
if self.agent_config.verbose:
|
| 88 |
+
print(f"✅ Created WDA session: {session_id}")
|
| 89 |
+
elif self.agent_config.verbose:
|
| 90 |
+
print(f"⚠️ Using default WDA session (no explicit session ID)")
|
| 91 |
+
|
| 92 |
+
self.action_handler = IOSActionHandler(
|
| 93 |
+
wda_url=self.agent_config.wda_url,
|
| 94 |
+
session_id=self.agent_config.session_id,
|
| 95 |
+
confirmation_callback=confirmation_callback,
|
| 96 |
+
takeover_callback=takeover_callback,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
self._context: list[dict[str, Any]] = []
|
| 100 |
+
self._step_count = 0
|
| 101 |
+
|
| 102 |
+
def run(self, task: str) -> str:
|
| 103 |
+
"""
|
| 104 |
+
Run the agent to complete a task.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
task: Natural language description of the task.
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
Final message from the agent.
|
| 111 |
+
"""
|
| 112 |
+
self._context = []
|
| 113 |
+
self._step_count = 0
|
| 114 |
+
|
| 115 |
+
# First step with user prompt
|
| 116 |
+
result = self._execute_step(task, is_first=True)
|
| 117 |
+
|
| 118 |
+
if result.finished:
|
| 119 |
+
return result.message or "Task completed"
|
| 120 |
+
|
| 121 |
+
# Continue until finished or max steps reached
|
| 122 |
+
while self._step_count < self.agent_config.max_steps:
|
| 123 |
+
result = self._execute_step(is_first=False)
|
| 124 |
+
|
| 125 |
+
if result.finished:
|
| 126 |
+
return result.message or "Task completed"
|
| 127 |
+
|
| 128 |
+
return "Max steps reached"
|
| 129 |
+
|
| 130 |
+
def step(self, task: str | None = None) -> StepResult:
|
| 131 |
+
"""
|
| 132 |
+
Execute a single step of the agent.
|
| 133 |
+
|
| 134 |
+
Useful for manual control or debugging.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
task: Task description (only needed for first step).
|
| 138 |
+
|
| 139 |
+
Returns:
|
| 140 |
+
StepResult with step details.
|
| 141 |
+
"""
|
| 142 |
+
is_first = len(self._context) == 0
|
| 143 |
+
|
| 144 |
+
if is_first and not task:
|
| 145 |
+
raise ValueError("Task is required for the first step")
|
| 146 |
+
|
| 147 |
+
return self._execute_step(task, is_first)
|
| 148 |
+
|
| 149 |
+
def reset(self) -> None:
|
| 150 |
+
"""Reset the agent state for a new task."""
|
| 151 |
+
self._context = []
|
| 152 |
+
self._step_count = 0
|
| 153 |
+
|
| 154 |
+
def _execute_step(
|
| 155 |
+
self, user_prompt: str | None = None, is_first: bool = False
|
| 156 |
+
) -> StepResult:
|
| 157 |
+
"""Execute a single step of the agent loop."""
|
| 158 |
+
self._step_count += 1
|
| 159 |
+
|
| 160 |
+
# Capture current screen state
|
| 161 |
+
screenshot = get_screenshot(
|
| 162 |
+
wda_url=self.agent_config.wda_url,
|
| 163 |
+
session_id=self.agent_config.session_id,
|
| 164 |
+
device_id=self.agent_config.device_id,
|
| 165 |
+
)
|
| 166 |
+
current_app = get_current_app(
|
| 167 |
+
wda_url=self.agent_config.wda_url, session_id=self.agent_config.session_id
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Build messages
|
| 171 |
+
if is_first:
|
| 172 |
+
self._context.append(
|
| 173 |
+
MessageBuilder.create_system_message(self.agent_config.system_prompt)
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
| 177 |
+
text_content = f"{user_prompt}\n\n{screen_info}"
|
| 178 |
+
|
| 179 |
+
self._context.append(
|
| 180 |
+
MessageBuilder.create_user_message(
|
| 181 |
+
text=text_content, image_base64=screenshot.base64_data
|
| 182 |
+
)
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
screen_info = MessageBuilder.build_screen_info(current_app)
|
| 186 |
+
text_content = f"** Screen Info **\n\n{screen_info}"
|
| 187 |
+
|
| 188 |
+
self._context.append(
|
| 189 |
+
MessageBuilder.create_user_message(
|
| 190 |
+
text=text_content, image_base64=screenshot.base64_data
|
| 191 |
+
)
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# Get model response
|
| 195 |
+
try:
|
| 196 |
+
response = self.model_client.request(self._context)
|
| 197 |
+
except Exception as e:
|
| 198 |
+
if self.agent_config.verbose:
|
| 199 |
+
traceback.print_exc()
|
| 200 |
+
return StepResult(
|
| 201 |
+
success=False,
|
| 202 |
+
finished=True,
|
| 203 |
+
action=None,
|
| 204 |
+
thinking="",
|
| 205 |
+
message=f"Model error: {e}",
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# Parse action from response
|
| 209 |
+
try:
|
| 210 |
+
action = parse_action(response.action)
|
| 211 |
+
except ValueError:
|
| 212 |
+
if self.agent_config.verbose:
|
| 213 |
+
traceback.print_exc()
|
| 214 |
+
action = finish(message=response.action)
|
| 215 |
+
|
| 216 |
+
if self.agent_config.verbose:
|
| 217 |
+
# Print thinking process
|
| 218 |
+
msgs = get_messages(self.agent_config.lang)
|
| 219 |
+
print("\n" + "=" * 50)
|
| 220 |
+
print(f"💭 {msgs['thinking']}:")
|
| 221 |
+
print("-" * 50)
|
| 222 |
+
print(response.thinking)
|
| 223 |
+
print("-" * 50)
|
| 224 |
+
print(f"🎯 {msgs['action']}:")
|
| 225 |
+
print(json.dumps(action, ensure_ascii=False, indent=2))
|
| 226 |
+
print("=" * 50 + "\n")
|
| 227 |
+
|
| 228 |
+
# Remove image from context to save space
|
| 229 |
+
self._context[-1] = MessageBuilder.remove_images_from_message(self._context[-1])
|
| 230 |
+
|
| 231 |
+
# Execute action
|
| 232 |
+
try:
|
| 233 |
+
result = self.action_handler.execute(
|
| 234 |
+
action, screenshot.width, screenshot.height
|
| 235 |
+
)
|
| 236 |
+
except Exception as e:
|
| 237 |
+
if self.agent_config.verbose:
|
| 238 |
+
traceback.print_exc()
|
| 239 |
+
result = self.action_handler.execute(
|
| 240 |
+
finish(message=str(e)), screenshot.width, screenshot.height
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# Add assistant response to context
|
| 244 |
+
self._context.append(
|
| 245 |
+
MessageBuilder.create_assistant_message(
|
| 246 |
+
f"<think>{response.thinking}</think><answer>{response.action}</answer>"
|
| 247 |
+
)
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# Check if finished
|
| 251 |
+
finished = action.get("_metadata") == "finish" or result.should_finish
|
| 252 |
+
|
| 253 |
+
if finished and self.agent_config.verbose:
|
| 254 |
+
msgs = get_messages(self.agent_config.lang)
|
| 255 |
+
print("\n" + "🎉 " + "=" * 48)
|
| 256 |
+
print(
|
| 257 |
+
f"✅ {msgs['task_completed']}: {result.message or action.get('message', msgs['done'])}"
|
| 258 |
+
)
|
| 259 |
+
print("=" * 50 + "\n")
|
| 260 |
+
|
| 261 |
+
return StepResult(
|
| 262 |
+
success=result.success,
|
| 263 |
+
finished=finished,
|
| 264 |
+
action=action,
|
| 265 |
+
thinking=response.thinking,
|
| 266 |
+
message=result.message or action.get("message"),
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
@property
|
| 270 |
+
def context(self) -> list[dict[str, Any]]:
|
| 271 |
+
"""Get the current conversation context."""
|
| 272 |
+
return self._context.copy()
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def step_count(self) -> int:
|
| 276 |
+
"""Get the current step count."""
|
| 277 |
+
return self._step_count
|
phone_agent/config/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration module for Phone Agent."""
|
| 2 |
+
|
| 3 |
+
from phone_agent.config.apps import APP_PACKAGES
|
| 4 |
+
from phone_agent.config.apps_ios import APP_PACKAGES_IOS
|
| 5 |
+
from phone_agent.config.i18n import get_message, get_messages
|
| 6 |
+
from phone_agent.config.prompts_en import SYSTEM_PROMPT as SYSTEM_PROMPT_EN
|
| 7 |
+
from phone_agent.config.prompts_zh import SYSTEM_PROMPT as SYSTEM_PROMPT_ZH
|
| 8 |
+
from phone_agent.config.timing import (
|
| 9 |
+
TIMING_CONFIG,
|
| 10 |
+
ActionTimingConfig,
|
| 11 |
+
ConnectionTimingConfig,
|
| 12 |
+
DeviceTimingConfig,
|
| 13 |
+
TimingConfig,
|
| 14 |
+
get_timing_config,
|
| 15 |
+
update_timing_config,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_system_prompt(lang: str = "cn") -> str:
|
| 20 |
+
"""
|
| 21 |
+
Get system prompt by language.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
lang: Language code, 'cn' for Chinese, 'en' for English.
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
System prompt string.
|
| 28 |
+
"""
|
| 29 |
+
if lang == "en":
|
| 30 |
+
return SYSTEM_PROMPT_EN
|
| 31 |
+
return SYSTEM_PROMPT_ZH
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# Default to Chinese for backward compatibility
|
| 35 |
+
SYSTEM_PROMPT = SYSTEM_PROMPT_ZH
|
| 36 |
+
|
| 37 |
+
__all__ = [
|
| 38 |
+
"APP_PACKAGES",
|
| 39 |
+
"APP_PACKAGES_IOS",
|
| 40 |
+
"SYSTEM_PROMPT",
|
| 41 |
+
"SYSTEM_PROMPT_ZH",
|
| 42 |
+
"SYSTEM_PROMPT_EN",
|
| 43 |
+
"get_system_prompt",
|
| 44 |
+
"get_messages",
|
| 45 |
+
"get_message",
|
| 46 |
+
"TIMING_CONFIG",
|
| 47 |
+
"TimingConfig",
|
| 48 |
+
"ActionTimingConfig",
|
| 49 |
+
"DeviceTimingConfig",
|
| 50 |
+
"ConnectionTimingConfig",
|
| 51 |
+
"get_timing_config",
|
| 52 |
+
"update_timing_config",
|
| 53 |
+
]
|
phone_agent/config/apps.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""App name to package name mapping for supported applications."""
|
| 2 |
+
|
| 3 |
+
APP_PACKAGES: dict[str, str] = {
|
| 4 |
+
# Social & Messaging
|
| 5 |
+
"微信": "com.tencent.mm",
|
| 6 |
+
"QQ": "com.tencent.mobileqq",
|
| 7 |
+
"微博": "com.sina.weibo",
|
| 8 |
+
# E-commerce
|
| 9 |
+
"淘宝": "com.taobao.taobao",
|
| 10 |
+
"京东": "com.jingdong.app.mall",
|
| 11 |
+
"拼多多": "com.xunmeng.pinduoduo",
|
| 12 |
+
"淘宝闪购": "com.taobao.taobao",
|
| 13 |
+
"京东秒送": "com.jingdong.app.mall",
|
| 14 |
+
# Lifestyle & Social
|
| 15 |
+
"小红书": "com.xingin.xhs",
|
| 16 |
+
"豆瓣": "com.douban.frodo",
|
| 17 |
+
"知乎": "com.zhihu.android",
|
| 18 |
+
# Maps & Navigation
|
| 19 |
+
"高德地图": "com.autonavi.minimap",
|
| 20 |
+
"百度地图": "com.baidu.BaiduMap",
|
| 21 |
+
# Food & Services
|
| 22 |
+
"美团": "com.sankuai.meituan",
|
| 23 |
+
"大众点评": "com.dianping.v1",
|
| 24 |
+
"饿了么": "me.ele",
|
| 25 |
+
"肯德基": "com.yek.android.kfc.activitys",
|
| 26 |
+
# Travel
|
| 27 |
+
"携程": "ctrip.android.view",
|
| 28 |
+
"铁路12306": "com.MobileTicket",
|
| 29 |
+
"12306": "com.MobileTicket",
|
| 30 |
+
"去哪儿": "com.Qunar",
|
| 31 |
+
"去哪儿旅行": "com.Qunar",
|
| 32 |
+
"滴滴出行": "com.sdu.didi.psnger",
|
| 33 |
+
# Video & Entertainment
|
| 34 |
+
"bilibili": "tv.danmaku.bili",
|
| 35 |
+
"抖音": "com.ss.android.ugc.aweme",
|
| 36 |
+
"快手": "com.smile.gifmaker",
|
| 37 |
+
"腾讯视频": "com.tencent.qqlive",
|
| 38 |
+
"爱奇艺": "com.qiyi.video",
|
| 39 |
+
"优酷视频": "com.youku.phone",
|
| 40 |
+
"芒果TV": "com.hunantv.imgo.activity",
|
| 41 |
+
"红果短剧": "com.phoenix.read",
|
| 42 |
+
# Music & Audio
|
| 43 |
+
"网易云音乐": "com.netease.cloudmusic",
|
| 44 |
+
"QQ音乐": "com.tencent.qqmusic",
|
| 45 |
+
"汽水音乐": "com.luna.music",
|
| 46 |
+
"喜马拉雅": "com.ximalaya.ting.android",
|
| 47 |
+
# Reading
|
| 48 |
+
"番茄小说": "com.dragon.read",
|
| 49 |
+
"番茄免费小说": "com.dragon.read",
|
| 50 |
+
"七猫免费小说": "com.kmxs.reader",
|
| 51 |
+
# Productivity
|
| 52 |
+
"飞书": "com.ss.android.lark",
|
| 53 |
+
"QQ邮箱": "com.tencent.androidqqmail",
|
| 54 |
+
# AI & Tools
|
| 55 |
+
"豆包": "com.larus.nova",
|
| 56 |
+
# Health & Fitness
|
| 57 |
+
"keep": "com.gotokeep.keep",
|
| 58 |
+
"美柚": "com.lingan.seeyou",
|
| 59 |
+
# News & Information
|
| 60 |
+
"腾讯新闻": "com.tencent.news",
|
| 61 |
+
"今日头条": "com.ss.android.article.news",
|
| 62 |
+
# Real Estate
|
| 63 |
+
"贝壳找房": "com.lianjia.beike",
|
| 64 |
+
"安居客": "com.anjuke.android.app",
|
| 65 |
+
# Finance
|
| 66 |
+
"同花顺": "com.hexin.plat.android",
|
| 67 |
+
# Games
|
| 68 |
+
"星穹铁道": "com.miHoYo.hkrpg",
|
| 69 |
+
"崩坏:星穹铁道": "com.miHoYo.hkrpg",
|
| 70 |
+
"恋与深空": "com.papegames.lysk.cn",
|
| 71 |
+
"AndroidSystemSettings": "com.android.settings",
|
| 72 |
+
"Android System Settings": "com.android.settings",
|
| 73 |
+
"Android System Settings": "com.android.settings",
|
| 74 |
+
"Android-System-Settings": "com.android.settings",
|
| 75 |
+
"Settings": "com.android.settings",
|
| 76 |
+
"AudioRecorder": "com.android.soundrecorder",
|
| 77 |
+
"audiorecorder": "com.android.soundrecorder",
|
| 78 |
+
"Bluecoins": "com.rammigsoftware.bluecoins",
|
| 79 |
+
"bluecoins": "com.rammigsoftware.bluecoins",
|
| 80 |
+
"Broccoli": "com.flauschcode.broccoli",
|
| 81 |
+
"broccoli": "com.flauschcode.broccoli",
|
| 82 |
+
"Booking.com": "com.booking",
|
| 83 |
+
"Booking": "com.booking",
|
| 84 |
+
"booking.com": "com.booking",
|
| 85 |
+
"booking": "com.booking",
|
| 86 |
+
"BOOKING.COM": "com.booking",
|
| 87 |
+
"Chrome": "com.android.chrome",
|
| 88 |
+
"chrome": "com.android.chrome",
|
| 89 |
+
"Google Chrome": "com.android.chrome",
|
| 90 |
+
"Clock": "com.android.deskclock",
|
| 91 |
+
"clock": "com.android.deskclock",
|
| 92 |
+
"Contacts": "com.android.contacts",
|
| 93 |
+
"contacts": "com.android.contacts",
|
| 94 |
+
"Duolingo": "com.duolingo",
|
| 95 |
+
"duolingo": "com.duolingo",
|
| 96 |
+
"Expedia": "com.expedia.bookings",
|
| 97 |
+
"expedia": "com.expedia.bookings",
|
| 98 |
+
"Files": "com.android.fileexplorer",
|
| 99 |
+
"files": "com.android.fileexplorer",
|
| 100 |
+
"File Manager": "com.android.fileexplorer",
|
| 101 |
+
"file manager": "com.android.fileexplorer",
|
| 102 |
+
"gmail": "com.google.android.gm",
|
| 103 |
+
"Gmail": "com.google.android.gm",
|
| 104 |
+
"GoogleMail": "com.google.android.gm",
|
| 105 |
+
"Google Mail": "com.google.android.gm",
|
| 106 |
+
"GoogleFiles": "com.google.android.apps.nbu.files",
|
| 107 |
+
"googlefiles": "com.google.android.apps.nbu.files",
|
| 108 |
+
"FilesbyGoogle": "com.google.android.apps.nbu.files",
|
| 109 |
+
"GoogleCalendar": "com.google.android.calendar",
|
| 110 |
+
"Google-Calendar": "com.google.android.calendar",
|
| 111 |
+
"Google Calendar": "com.google.android.calendar",
|
| 112 |
+
"google-calendar": "com.google.android.calendar",
|
| 113 |
+
"google calendar": "com.google.android.calendar",
|
| 114 |
+
"GoogleChat": "com.google.android.apps.dynamite",
|
| 115 |
+
"Google Chat": "com.google.android.apps.dynamite",
|
| 116 |
+
"Google-Chat": "com.google.android.apps.dynamite",
|
| 117 |
+
"GoogleClock": "com.google.android.deskclock",
|
| 118 |
+
"Google Clock": "com.google.android.deskclock",
|
| 119 |
+
"Google-Clock": "com.google.android.deskclock",
|
| 120 |
+
"GoogleContacts": "com.google.android.contacts",
|
| 121 |
+
"Google-Contacts": "com.google.android.contacts",
|
| 122 |
+
"Google Contacts": "com.google.android.contacts",
|
| 123 |
+
"google-contacts": "com.google.android.contacts",
|
| 124 |
+
"google contacts": "com.google.android.contacts",
|
| 125 |
+
"GoogleDocs": "com.google.android.apps.docs.editors.docs",
|
| 126 |
+
"Google Docs": "com.google.android.apps.docs.editors.docs",
|
| 127 |
+
"googledocs": "com.google.android.apps.docs.editors.docs",
|
| 128 |
+
"google docs": "com.google.android.apps.docs.editors.docs",
|
| 129 |
+
"Google Drive": "com.google.android.apps.docs",
|
| 130 |
+
"Google-Drive": "com.google.android.apps.docs",
|
| 131 |
+
"google drive": "com.google.android.apps.docs",
|
| 132 |
+
"google-drive": "com.google.android.apps.docs",
|
| 133 |
+
"GoogleDrive": "com.google.android.apps.docs",
|
| 134 |
+
"Googledrive": "com.google.android.apps.docs",
|
| 135 |
+
"googledrive": "com.google.android.apps.docs",
|
| 136 |
+
"GoogleFit": "com.google.android.apps.fitness",
|
| 137 |
+
"googlefit": "com.google.android.apps.fitness",
|
| 138 |
+
"GoogleKeep": "com.google.android.keep",
|
| 139 |
+
"googlekeep": "com.google.android.keep",
|
| 140 |
+
"GoogleMaps": "com.google.android.apps.maps",
|
| 141 |
+
"Google Maps": "com.google.android.apps.maps",
|
| 142 |
+
"googlemaps": "com.google.android.apps.maps",
|
| 143 |
+
"google maps": "com.google.android.apps.maps",
|
| 144 |
+
"Google Play Books": "com.google.android.apps.books",
|
| 145 |
+
"Google-Play-Books": "com.google.android.apps.books",
|
| 146 |
+
"google play books": "com.google.android.apps.books",
|
| 147 |
+
"google-play-books": "com.google.android.apps.books",
|
| 148 |
+
"GooglePlayBooks": "com.google.android.apps.books",
|
| 149 |
+
"googleplaybooks": "com.google.android.apps.books",
|
| 150 |
+
"GooglePlayStore": "com.android.vending",
|
| 151 |
+
"Google Play Store": "com.android.vending",
|
| 152 |
+
"Google-Play-Store": "com.android.vending",
|
| 153 |
+
"GoogleSlides": "com.google.android.apps.docs.editors.slides",
|
| 154 |
+
"Google Slides": "com.google.android.apps.docs.editors.slides",
|
| 155 |
+
"Google-Slides": "com.google.android.apps.docs.editors.slides",
|
| 156 |
+
"GoogleTasks": "com.google.android.apps.tasks",
|
| 157 |
+
"Google Tasks": "com.google.android.apps.tasks",
|
| 158 |
+
"Google-Tasks": "com.google.android.apps.tasks",
|
| 159 |
+
"Joplin": "net.cozic.joplin",
|
| 160 |
+
"joplin": "net.cozic.joplin",
|
| 161 |
+
"McDonald": "com.mcdonalds.app",
|
| 162 |
+
"mcdonald": "com.mcdonalds.app",
|
| 163 |
+
"Osmand": "net.osmand",
|
| 164 |
+
"osmand": "net.osmand",
|
| 165 |
+
"PiMusicPlayer": "com.Project100Pi.themusicplayer",
|
| 166 |
+
"pimusicplayer": "com.Project100Pi.themusicplayer",
|
| 167 |
+
"Quora": "com.quora.android",
|
| 168 |
+
"quora": "com.quora.android",
|
| 169 |
+
"Reddit": "com.reddit.frontpage",
|
| 170 |
+
"reddit": "com.reddit.frontpage",
|
| 171 |
+
"RetroMusic": "code.name.monkey.retromusic",
|
| 172 |
+
"retromusic": "code.name.monkey.retromusic",
|
| 173 |
+
"SimpleCalendarPro": "com.scientificcalculatorplus.simplecalculator.basiccalculator.mathcalc",
|
| 174 |
+
"SimpleSMSMessenger": "com.simplemobiletools.smsmessenger",
|
| 175 |
+
"Telegram": "org.telegram.messenger",
|
| 176 |
+
"temu": "com.einnovation.temu",
|
| 177 |
+
"Temu": "com.einnovation.temu",
|
| 178 |
+
"Tiktok": "com.zhiliaoapp.musically",
|
| 179 |
+
"tiktok": "com.zhiliaoapp.musically",
|
| 180 |
+
"Twitter": "com.twitter.android",
|
| 181 |
+
"twitter": "com.twitter.android",
|
| 182 |
+
"X": "com.twitter.android",
|
| 183 |
+
"VLC": "org.videolan.vlc",
|
| 184 |
+
"WeChat": "com.tencent.mm",
|
| 185 |
+
"wechat": "com.tencent.mm",
|
| 186 |
+
"Whatsapp": "com.whatsapp",
|
| 187 |
+
"WhatsApp": "com.whatsapp",
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def get_package_name(app_name: str) -> str | None:
|
| 192 |
+
"""
|
| 193 |
+
Get the package name for an app.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
app_name: The display name of the app.
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
The Android package name, or None if not found.
|
| 200 |
+
"""
|
| 201 |
+
return APP_PACKAGES.get(app_name)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def get_app_name(package_name: str) -> str | None:
|
| 205 |
+
"""
|
| 206 |
+
Get the app name from a package name.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
package_name: The Android package name.
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
The display name of the app, or None if not found.
|
| 213 |
+
"""
|
| 214 |
+
for name, package in APP_PACKAGES.items():
|
| 215 |
+
if package == package_name:
|
| 216 |
+
return name
|
| 217 |
+
return None
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def list_supported_apps() -> list[str]:
|
| 221 |
+
"""
|
| 222 |
+
Get a list of all supported app names.
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
List of app names.
|
| 226 |
+
"""
|
| 227 |
+
return list(APP_PACKAGES.keys())
|
phone_agent/config/apps_harmonyos.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HarmonyOS application package name mappings.
|
| 2 |
+
|
| 3 |
+
Maps user-friendly app names to HarmonyOS bundle names.
|
| 4 |
+
These bundle names are used with the 'hdc shell aa start -b <bundle>' command.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# Custom ability names for apps that don't use the default "EntryAbility"
|
| 8 |
+
# Maps bundle_name -> ability_name
|
| 9 |
+
# Generated by: python test/find_abilities.py
|
| 10 |
+
APP_ABILITIES: dict[str, str] = {
|
| 11 |
+
# Third-party apps
|
| 12 |
+
"cn.wps.mobileoffice.hap": "DocumentAbility",
|
| 13 |
+
"com.ccb.mobilebank.hm": "CcbMainAbility",
|
| 14 |
+
"com.dewu.hos": "HomeAbility",
|
| 15 |
+
"com.larus.nova.hm": "MainAbility",
|
| 16 |
+
"com.luna.hm.music": "MainAbility",
|
| 17 |
+
"com.meitu.meitupic": "MainAbility",
|
| 18 |
+
"com.ss.hm.article.news": "MainAbility",
|
| 19 |
+
"com.ss.hm.ugc.aweme": "MainAbility",
|
| 20 |
+
"com.taobao.taobao4hmos": "Taobao_mainAbility",
|
| 21 |
+
"com.tencent.videohm": "AppAbility",
|
| 22 |
+
"com.ximalaya.ting.xmharmony": "MainBundleAbility",
|
| 23 |
+
"com.zhihu.hmos": "PhoneAbility",
|
| 24 |
+
|
| 25 |
+
# Huawei system apps
|
| 26 |
+
"com.huawei.hmos.browser": "MainAbility",
|
| 27 |
+
"com.huawei.hmos.calculator": "com.huawei.hmos.calculator.CalculatorAbility",
|
| 28 |
+
"com.huawei.hmos.calendar": "MainAbility",
|
| 29 |
+
"com.huawei.hmos.camera": "com.huawei.hmos.camera.MainAbility",
|
| 30 |
+
"com.huawei.hmos.clock": "com.huawei.hmos.clock.phone",
|
| 31 |
+
"com.huawei.hmos.clouddrive": "MainAbility",
|
| 32 |
+
"com.huawei.hmos.email": "ApplicationAbility",
|
| 33 |
+
"com.huawei.hmos.filemanager": "MainAbility",
|
| 34 |
+
"com.huawei.hmos.health": "Activity_card_entryAbility",
|
| 35 |
+
"com.huawei.hmos.notepad": "MainAbility",
|
| 36 |
+
"com.huawei.hmos.photos": "MainAbility",
|
| 37 |
+
"com.huawei.hmos.screenrecorder": "com.huawei.hmos.screenrecorder.ServiceExtAbility",
|
| 38 |
+
"com.huawei.hmos.screenshot": "com.huawei.hmos.screenshot.ServiceExtAbility",
|
| 39 |
+
"com.huawei.hmos.settings": "com.huawei.hmos.settings.MainAbility",
|
| 40 |
+
"com.huawei.hmos.soundrecorder": "MainAbility",
|
| 41 |
+
"com.huawei.hmos.vassistant": "AiCaptionServiceExtAbility",
|
| 42 |
+
"com.huawei.hmos.wallet": "MainAbility",
|
| 43 |
+
|
| 44 |
+
# Huawei services
|
| 45 |
+
"com.huawei.hmsapp.appgallery": "MainAbility",
|
| 46 |
+
"com.huawei.hmsapp.books": "MainAbility",
|
| 47 |
+
"com.huawei.hmsapp.himovie": "MainAbility",
|
| 48 |
+
"com.huawei.hmsapp.hisearch": "MainAbility",
|
| 49 |
+
"com.huawei.hmsapp.music": "MainAbility",
|
| 50 |
+
"com.huawei.hmsapp.thememanager": "MainAbility",
|
| 51 |
+
"com.huawei.hmsapp.totemweather": "com.huawei.hmsapp.totemweather.MainAbility",
|
| 52 |
+
|
| 53 |
+
# OHOS system apps
|
| 54 |
+
"com.ohos.callui": "com.ohos.callui.ServiceAbility",
|
| 55 |
+
"com.ohos.contacts": "com.ohos.contacts.MainAbility",
|
| 56 |
+
"com.ohos.mms": "com.ohos.mms.MainAbility",
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
APP_PACKAGES: dict[str, str] = {
|
| 60 |
+
# Social & Messaging
|
| 61 |
+
"微信": "com.tencent.wechat",
|
| 62 |
+
"QQ": "com.tencent.mqq",
|
| 63 |
+
"微博": "com.sina.weibo.stage",
|
| 64 |
+
# E-commerce
|
| 65 |
+
"淘宝": "com.taobao.taobao4hmos",
|
| 66 |
+
"京东": "com.jd.hm.mall",
|
| 67 |
+
"拼多多": "com.xunmeng.pinduoduo.hos",
|
| 68 |
+
"淘宝闪购": "com.taobao.taobao4hmos",
|
| 69 |
+
"京东秒送": "com.jd.hm.mall",
|
| 70 |
+
# Lifestyle & Social
|
| 71 |
+
"小红书": "com.xingin.xhs_hos",
|
| 72 |
+
"知乎": "com.zhihu.hmos",
|
| 73 |
+
# "豆瓣": "com.douban.frodo", # 未在 hdc 列表中找到
|
| 74 |
+
# Maps & Navigation
|
| 75 |
+
"高德地图": "com.amap.hmapp",
|
| 76 |
+
"百度地图": "com.baidu.hmmap",
|
| 77 |
+
# Food & Services
|
| 78 |
+
"美团": "com.sankuai.hmeituan",
|
| 79 |
+
"美团外卖": "com.meituan.takeaway",
|
| 80 |
+
"大众点评": "com.sankuai.dianping",
|
| 81 |
+
# "肯德基": "com.yek.android.kfc.activitys", # 未在 hdc 列表中找到
|
| 82 |
+
# Travel
|
| 83 |
+
# "携程": "ctrip.android.view", # 未在 hdc 列表中找到
|
| 84 |
+
"铁路12306": "com.chinarailway.ticketingHM",
|
| 85 |
+
"12306": "com.chinarailway.ticketingHM",
|
| 86 |
+
# "去哪儿": "com.Qunar", # 未在 hdc 列表中找到
|
| 87 |
+
# "去哪儿旅行": "com.Qunar", # 未在 hdc 列表中找到
|
| 88 |
+
"滴滴出行": "com.sdu.didi.hmos.psnger",
|
| 89 |
+
# Video & Entertainment
|
| 90 |
+
"bilibili": "yylx.danmaku.bili",
|
| 91 |
+
"抖音": "com.ss.hm.ugc.aweme",
|
| 92 |
+
"快手": "com.kuaishou.hmapp",
|
| 93 |
+
"腾讯视频": "com.tencent.videohm",
|
| 94 |
+
"爱奇艺": "com.qiyi.video.hmy",
|
| 95 |
+
"芒果TV": "com.mgtv.phone",
|
| 96 |
+
# "优酷视频": "com.youku.phone", # 未在 hdc 列表中找到
|
| 97 |
+
# "红果短剧": "com.phoenix.read", # 未在 hdc 列表中找到
|
| 98 |
+
# Music & Audio
|
| 99 |
+
# "网易云音乐": "com.netease.cloudmusic", # 未在 hdc 列表中找到
|
| 100 |
+
"QQ音乐": "com.tencent.hm.qqmusic",
|
| 101 |
+
"汽水音乐": "com.luna.hm.music",
|
| 102 |
+
"喜马拉雅": "com.ximalaya.ting.xmharmony",
|
| 103 |
+
# Reading
|
| 104 |
+
# "番茄小说": "com.dragon.read", # 未在 hdc 列表中找到
|
| 105 |
+
# "番茄免费小说": "com.dragon.read", # 未在 hdc 列表中找到
|
| 106 |
+
# "七猫免费小说": "com.kmxs.reader", # 未在 hdc 列表中找到
|
| 107 |
+
# Productivity
|
| 108 |
+
"飞书": "com.ss.feishu",
|
| 109 |
+
# "QQ邮箱": "com.tencent.androidqqmail", # 未在 hdc 列表中找到
|
| 110 |
+
# AI & Tools
|
| 111 |
+
"豆包": "com.larus.nova.hm",
|
| 112 |
+
# Health & Fitness
|
| 113 |
+
# "keep": "com.gotokeep.keep", # 未在 hdc 列表中找到
|
| 114 |
+
# "美柚": "com.lingan.seeyou", # 未在 hdc 列表中找到
|
| 115 |
+
# News & Information
|
| 116 |
+
# "腾讯新闻": "com.tencent.news", # 未在 hdc 列表中找到
|
| 117 |
+
"今日头条": "com.ss.hm.article.news",
|
| 118 |
+
# Real Estate
|
| 119 |
+
# "贝壳找房": "com.lianjia.beike", # 未在 hdc 列表中找到
|
| 120 |
+
# "安居客": "com.anjuke.android.app", # 未在 hdc 列表中找到
|
| 121 |
+
# Finance
|
| 122 |
+
# "同花顺": "com.hexin.plat.android", # 未在 hdc 列表中找到
|
| 123 |
+
# Games
|
| 124 |
+
# "星穹铁道": "com.miHoYo.hkrpg", # 未在 hdc 列表中找到
|
| 125 |
+
# "崩坏:星穹铁道": "com.miHoYo.hkrpg", # 未在 hdc 列表中找到
|
| 126 |
+
# "恋与深空": "com.papegames.lysk.cn", # 未在 hdc 列表中找到
|
| 127 |
+
|
| 128 |
+
# HarmonyOS 第三方应用
|
| 129 |
+
"百度": "com.baidu.baiduapp",
|
| 130 |
+
"阿里巴巴": "com.alibaba.wireless_hmos",
|
| 131 |
+
"WPS": "cn.wps.mobileoffice.hap",
|
| 132 |
+
"企业微信": "com.tencent.wework.hmos",
|
| 133 |
+
"同程": "com.tongcheng.hmos",
|
| 134 |
+
"同程旅行": "com.tongcheng.hmos",
|
| 135 |
+
"唯品会": "com.vip.hosapp",
|
| 136 |
+
"支付宝": "com.alipay.mobile.client",
|
| 137 |
+
"UC浏览器": "com.uc.mobile",
|
| 138 |
+
"闲鱼": "com.taobao.idlefish4ohos",
|
| 139 |
+
"转转": "com.zhuanzhuan.hmoszz",
|
| 140 |
+
"迅雷": "com.xunlei.thunder",
|
| 141 |
+
"搜狗输入法": "com.sogou.input",
|
| 142 |
+
"扫描全能王": "com.intsig.camscanner.hap",
|
| 143 |
+
"美图秀秀": "com.meitu.meitupic",
|
| 144 |
+
"58同城": "com.wuba.life",
|
| 145 |
+
"得物": "com.dewu.hos",
|
| 146 |
+
"海底捞": "com.haidilao.haros",
|
| 147 |
+
"中国移动": "com.droi.tong",
|
| 148 |
+
"中国联通": "com.sinovatech.unicom.ha",
|
| 149 |
+
"国家税务总局": "cn.gov.chinatax.gt4.hm",
|
| 150 |
+
"建设银行": "com.ccb.mobilebank.hm",
|
| 151 |
+
"快手极速版": "com.kuaishou.hmnebula",
|
| 152 |
+
|
| 153 |
+
# HarmonyOS 系统应用 - 工具类
|
| 154 |
+
"浏览器": "com.huawei.hmos.browser",
|
| 155 |
+
"计算器": "com.huawei.hmos.calculator",
|
| 156 |
+
"日历": "com.huawei.hmos.calendar",
|
| 157 |
+
"相机": "com.huawei.hmos.camera",
|
| 158 |
+
"时钟": "com.huawei.hmos.clock",
|
| 159 |
+
"云盘": "com.huawei.hmos.clouddrive",
|
| 160 |
+
"云空间": "com.huawei.hmos.clouddrive",
|
| 161 |
+
"邮件": "com.huawei.hmos.email",
|
| 162 |
+
"文件管理器": "com.huawei.hmos.filemanager",
|
| 163 |
+
"文件": "com.huawei.hmos.files",
|
| 164 |
+
"查找设备": "com.huawei.hmos.finddevice",
|
| 165 |
+
"查找手机": "com.huawei.hmos.finddevice",
|
| 166 |
+
"录音机": "com.huawei.hmos.soundrecorder",
|
| 167 |
+
"录音": "com.huawei.hmos.soundrecorder",
|
| 168 |
+
"录屏": "com.huawei.hmos.screenrecorder",
|
| 169 |
+
"截屏": "com.huawei.hmos.screenshot",
|
| 170 |
+
"笔记": "com.huawei.hmos.notepad",
|
| 171 |
+
"备忘录": "com.huawei.hmos.notepad",
|
| 172 |
+
|
| 173 |
+
# HarmonyOS 系统应用 - 媒体类
|
| 174 |
+
"相册": "com.huawei.hmos.photos",
|
| 175 |
+
"图库": "com.huawei.hmos.photos",
|
| 176 |
+
# "视频": "com.huawei.hmos.mediaplayer", # 未在 hdc 列表中找到,但有 com.huawei.hmsapp.himovie
|
| 177 |
+
|
| 178 |
+
# HarmonyOS 系统应用 - 通讯类
|
| 179 |
+
"联系人": "com.ohos.contacts",
|
| 180 |
+
"通讯录": "com.ohos.contacts",
|
| 181 |
+
"短信": "com.ohos.mms",
|
| 182 |
+
"信息": "com.ohos.mms",
|
| 183 |
+
"电话": "com.ohos.callui",
|
| 184 |
+
"拨号": "com.ohos.callui",
|
| 185 |
+
|
| 186 |
+
# HarmonyOS 系统应用 - 设置类
|
| 187 |
+
"设置": "com.huawei.hmos.settings",
|
| 188 |
+
"系统设置": "com.huawei.hmos.settings",
|
| 189 |
+
"AndroidSystemSettings": "com.huawei.hmos.settings",
|
| 190 |
+
"Android System Settings": "com.huawei.hmos.settings",
|
| 191 |
+
"Android System Settings": "com.huawei.hmos.settings",
|
| 192 |
+
"Android-System-Settings": "com.huawei.hmos.settings",
|
| 193 |
+
"Settings": "com.huawei.hmos.settings",
|
| 194 |
+
|
| 195 |
+
# HarmonyOS 系统应用 - 生活服务
|
| 196 |
+
"健康": "com.huawei.hmos.health",
|
| 197 |
+
"运动健康": "com.huawei.hmos.health",
|
| 198 |
+
"地图": "com.huawei.hmos.maps.app",
|
| 199 |
+
"华为地图": "com.huawei.hmos.maps.app",
|
| 200 |
+
"钱包": "com.huawei.hmos.wallet",
|
| 201 |
+
"华为钱包": "com.huawei.hmos.wallet",
|
| 202 |
+
"智慧生活": "com.huawei.hmos.ailife",
|
| 203 |
+
"智能助手": "com.huawei.hmos.vassistant",
|
| 204 |
+
"小艺": "com.huawei.hmos.vassistant",
|
| 205 |
+
|
| 206 |
+
# HarmonyOS 服务
|
| 207 |
+
"应用市场": "com.huawei.hmsapp.appgallery",
|
| 208 |
+
"华为应用市场": "com.huawei.hmsapp.appgallery",
|
| 209 |
+
"音乐": "com.huawei.hmsapp.music",
|
| 210 |
+
"华为音乐": "com.huawei.hmsapp.music",
|
| 211 |
+
"主题": "com.huawei.hmsapp.thememanager",
|
| 212 |
+
"主题管理": "com.huawei.hmsapp.thememanager",
|
| 213 |
+
"天气": "com.huawei.hmsapp.totemweather",
|
| 214 |
+
"华为天气": "com.huawei.hmsapp.totemweather",
|
| 215 |
+
"视频": "com.huawei.hmsapp.himovie",
|
| 216 |
+
"华为视频": "com.huawei.hmsapp.himovie",
|
| 217 |
+
"阅读": "com.huawei.hmsapp.books",
|
| 218 |
+
"华为阅读": "com.huawei.hmsapp.books",
|
| 219 |
+
"游戏中心": "com.huawei.hmsapp.gamecenter",
|
| 220 |
+
"华为游戏中心": "com.huawei.hmsapp.gamecenter",
|
| 221 |
+
"搜索": "com.huawei.hmsapp.hisearch",
|
| 222 |
+
"华为搜索": "com.huawei.hmsapp.hisearch",
|
| 223 |
+
"指南针": "com.huawei.hmsapp.compass",
|
| 224 |
+
"会员中心": "com.huawei.hmos.myhuawei",
|
| 225 |
+
"我的华为": "com.huawei.hmos.myhuawei",
|
| 226 |
+
"华为会员": "com.huawei.hmos.myhuawei",
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def get_package_name(app_name: str) -> str | None:
|
| 231 |
+
"""
|
| 232 |
+
Get the package name for an app.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
app_name: The display name of the app.
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
The HarmonyOS bundle name, or None if not found.
|
| 239 |
+
"""
|
| 240 |
+
return APP_PACKAGES.get(app_name)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def get_app_name(package_name: str) -> str | None:
|
| 244 |
+
"""
|
| 245 |
+
Get the app name from a package name.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
package_name: The HarmonyOS bundle name.
|
| 249 |
+
|
| 250 |
+
Returns:
|
| 251 |
+
The display name of the app, or None if not found.
|
| 252 |
+
"""
|
| 253 |
+
for name, package in APP_PACKAGES.items():
|
| 254 |
+
if package == package_name:
|
| 255 |
+
return name
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def list_supported_apps() -> list[str]:
|
| 260 |
+
"""
|
| 261 |
+
Get a list of all supported app names.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
List of app names.
|
| 265 |
+
"""
|
| 266 |
+
return list(APP_PACKAGES.keys())
|
phone_agent/config/apps_ios.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""App name to iOS bundle ID mapping for supported applications.
|
| 2 |
+
|
| 3 |
+
Based on iOS app bundle ID conventions and common iOS applications.
|
| 4 |
+
Bundle IDs are in the format: com.company.appName
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
APP_PACKAGES_IOS: dict[str, str] = {
|
| 8 |
+
# Tencent Apps (腾讯系)
|
| 9 |
+
"微信": "com.tencent.xin",
|
| 10 |
+
"企业微信": "com.tencent.ww",
|
| 11 |
+
"微信读书": "com.tencent.weread",
|
| 12 |
+
"微信听书": "com.tencent.wehear",
|
| 13 |
+
"QQ": "com.tencent.mqq",
|
| 14 |
+
"QQ音乐": "com.tencent.QQMusic",
|
| 15 |
+
"QQ阅读": "com.tencent.qqreaderiphone",
|
| 16 |
+
"QQ邮箱": "com.tencent.qqmail",
|
| 17 |
+
"QQ浏览器": "com.tencent.mttlite",
|
| 18 |
+
"TIM": "com.tencent.tim",
|
| 19 |
+
"微视": "com.tencent.microvision",
|
| 20 |
+
"腾讯新闻": "com.tencent.info",
|
| 21 |
+
"腾讯视频": "com.tencent.live4iphone",
|
| 22 |
+
"腾讯动漫": "com.tencent.ied.app.comic",
|
| 23 |
+
"腾讯微云": "com.tencent.weiyun",
|
| 24 |
+
"腾讯体育": "com.tencent.sportskbs",
|
| 25 |
+
"腾讯文档": "com.tencent.txdocs",
|
| 26 |
+
"腾讯翻译君": "com.tencent.qqtranslator",
|
| 27 |
+
"腾讯课堂": "com.tencent.edu",
|
| 28 |
+
"腾讯地图": "com.tencent.sosomap",
|
| 29 |
+
"小鹅拼拼": "com.tencent.dwdcoco",
|
| 30 |
+
"全民k歌": "com.tencent.QQKSong",
|
| 31 |
+
# Alibaba Apps (阿里系)
|
| 32 |
+
"支付宝": "com.alipay.iphoneclient",
|
| 33 |
+
"钉钉": "com.laiwang.DingTalk",
|
| 34 |
+
"闲鱼": "com.taobao.fleamarket",
|
| 35 |
+
"淘宝": "com.taobao.taobao4iphone",
|
| 36 |
+
"斗鱼": "tv.douyu.live",
|
| 37 |
+
"天猫": "com.taobao.tmall",
|
| 38 |
+
"口碑": "com.taobao.kbmeishi",
|
| 39 |
+
"饿了么": "me.ele.ios.eleme",
|
| 40 |
+
"高德地图": "com.autonavi.amap",
|
| 41 |
+
"UC浏览器": "com.ucweb.iphone.lowversion",
|
| 42 |
+
"一淘": "com.taobao.etaocoupon",
|
| 43 |
+
"飞猪": "com.taobao.travel",
|
| 44 |
+
"虾米音乐": "com.xiami.spark",
|
| 45 |
+
"淘票票": "com.taobao.movie.MoviePhoneClient",
|
| 46 |
+
"优酷": "com.youku.YouKu",
|
| 47 |
+
"菜鸟裹裹": "com.cainiao.cnwireless",
|
| 48 |
+
"土豆视频": "com.tudou.tudouiphone",
|
| 49 |
+
# ByteDance Apps (字节系)
|
| 50 |
+
"抖音": "com.ss.iphone.ugc.Aweme",
|
| 51 |
+
"抖音极速版": "com.ss.iphone.ugc.aweme.lite",
|
| 52 |
+
"抖音火山版": "com.ss.iphone.ugc.Live",
|
| 53 |
+
"Tiktok": "com.zhiliaoapp.musically",
|
| 54 |
+
"飞书": "com.bytedance.ee.lark",
|
| 55 |
+
"今日头条": "com.ss.iphone.article.News",
|
| 56 |
+
"西瓜视频": "com.ss.iphone.article.Video",
|
| 57 |
+
"皮皮虾": "com.bd.iphone.super",
|
| 58 |
+
# Meituan Apps (美团系)
|
| 59 |
+
"美团": "com.meituan.imeituan",
|
| 60 |
+
"美团外卖": "com.meituan.itakeaway",
|
| 61 |
+
"大众点评": "com.dianping.dpscope",
|
| 62 |
+
"美团优选": "com.meituan.iyouxuan",
|
| 63 |
+
"美团优选团长": "com.meituan.igrocery.gh",
|
| 64 |
+
"美团骑手": "com.meituan.banma.homebrew",
|
| 65 |
+
"美团开店宝": "com.meituan.imerchantbiz",
|
| 66 |
+
"美团拍店": "com.meituan.pai",
|
| 67 |
+
"美团众包": "com.meituan.banma.crowdsource",
|
| 68 |
+
"美团买菜": "com.baobaoaichi.imaicai",
|
| 69 |
+
# JD Apps (京东系)
|
| 70 |
+
"京东": "com.360buy.jdmobile",
|
| 71 |
+
"京东读书": "com.jd.reader",
|
| 72 |
+
# NetEase Apps (网易系)
|
| 73 |
+
"网易新闻": "com.netease.news",
|
| 74 |
+
"网易云音乐": "com.netease.cloudmusic",
|
| 75 |
+
"网易邮箱大师": "com.netease.macmail",
|
| 76 |
+
"网易严选": "com.netease.yanxuan",
|
| 77 |
+
"网易公开课": "com.netease.videoHD",
|
| 78 |
+
"网易有道词典": "youdaoPro",
|
| 79 |
+
"有道云笔记": "com.youdao.note.YoudaoNoteMac",
|
| 80 |
+
# Baidu Apps (百度系)
|
| 81 |
+
"百度": "com.baidu.BaiduMobile",
|
| 82 |
+
"百度网盘": "com.baidu.netdisk",
|
| 83 |
+
"百度贴吧": "com.baidu.tieba",
|
| 84 |
+
"百度地图": "com.baidu.map",
|
| 85 |
+
"百度阅读": "com.baidu.yuedu",
|
| 86 |
+
"百度翻译": "com.baidu.translate",
|
| 87 |
+
"百度文库": "com.baidu.Wenku",
|
| 88 |
+
"百度视频": "com.baidu.videoiphone",
|
| 89 |
+
"百度输入法": "com.baidu.inputMethod",
|
| 90 |
+
# Kuaishou Apps (快手系)
|
| 91 |
+
"快手": "com.jiangjia.gif",
|
| 92 |
+
"快手极速版": "com.kuaishou.nebula",
|
| 93 |
+
# Other Popular Apps
|
| 94 |
+
"哔哩哔哩": "tv.danmaku.bilianime",
|
| 95 |
+
"芒果TV": "com.hunantv.imgotv",
|
| 96 |
+
"苏宁易购": "SuningEMall",
|
| 97 |
+
"微博": "com.sina.weibo",
|
| 98 |
+
"微博极速版": "com.sina.weibolite",
|
| 99 |
+
"微博国际": "com.weibo.international",
|
| 100 |
+
"墨客": "com.moke.moke.iphone",
|
| 101 |
+
"豆瓣": "com.douban.frodo",
|
| 102 |
+
"知乎": "com.zhihu.ios",
|
| 103 |
+
"小红书": "com.xingin.discover",
|
| 104 |
+
"喜马拉雅": "com.gemd.iting",
|
| 105 |
+
"得到": "com.luojilab.LuoJiFM-IOS",
|
| 106 |
+
"得物": "com.siwuai.duapp",
|
| 107 |
+
"起点读书": "m.qidian.QDReaderAppStore",
|
| 108 |
+
"番茄小说": "com.dragon.read",
|
| 109 |
+
"书旗小说": "com.shuqicenter.reader",
|
| 110 |
+
"拼多多": "com.xunmeng.pinduoduo",
|
| 111 |
+
"多点": "com.dmall.dmall",
|
| 112 |
+
"便利蜂": "com.bianlifeng.customer.ios",
|
| 113 |
+
"亿通行": "com.ruubypay.yitongxing",
|
| 114 |
+
"云闪付": "com.unionpay.chsp",
|
| 115 |
+
"大都会Metro": "com.DDH.SHSubway",
|
| 116 |
+
"爱奇艺视频": "com.qiyi.iphone",
|
| 117 |
+
"搜狐视频": "com.sohu.iPhoneVideo",
|
| 118 |
+
"搜狐新闻": "com.sohu.newspaper",
|
| 119 |
+
"搜狗浏览器": "com.sogou.SogouExplorerMobile",
|
| 120 |
+
"虎牙": "com.yy.kiwi",
|
| 121 |
+
"比心": "com.yitan.bixin",
|
| 122 |
+
"转转": "com.wuba.zhuanzhuan",
|
| 123 |
+
"YY": "yyvoice",
|
| 124 |
+
"绿洲": "com.sina.oasis",
|
| 125 |
+
"陌陌": "com.wemomo.momoappdemo1",
|
| 126 |
+
"什么值得买": "com.smzdm.client.ios",
|
| 127 |
+
"美团秀秀": "com.meitu.mtxx",
|
| 128 |
+
"唯品会": "com.vipshop.iphone",
|
| 129 |
+
"唱吧": "com.changba.ktv",
|
| 130 |
+
"酷狗音乐": "com.kugou.kugou1002",
|
| 131 |
+
"CSDN": "net.csdn.CsdnPlus",
|
| 132 |
+
"多抓鱼": "com.duozhuyu.dejavu",
|
| 133 |
+
"自如": "com.ziroom.ZiroomProject",
|
| 134 |
+
"携程": "ctrip.com",
|
| 135 |
+
"去哪儿旅行": "com.qunar.iphoneclient8",
|
| 136 |
+
"Xmind": "net.xmind.brownieapp",
|
| 137 |
+
"印象笔记": "com.yinxiang.iPhone",
|
| 138 |
+
"欧陆词典": "eusoft.eudic.pro",
|
| 139 |
+
"115": "com.115.personal",
|
| 140 |
+
"名片全能王": "com.intsig.camcard.lite",
|
| 141 |
+
"中国银行": "com.boc.BOCMBCI",
|
| 142 |
+
"58同城": "com.taofang.iphone",
|
| 143 |
+
# International Apps
|
| 144 |
+
"Google Chrome": "com.google.chrome.ios",
|
| 145 |
+
"Gmail": "com.google.Gmail",
|
| 146 |
+
"Facebook": "com.facebook.Facebook",
|
| 147 |
+
"Firefox": "org.mozilla.ios.Firefox",
|
| 148 |
+
"Messenger": "com.facebook.Messenger",
|
| 149 |
+
"Instagram": "com.burbn.instagram",
|
| 150 |
+
"Starbucks": "com.starbucks.mystarbucks",
|
| 151 |
+
"Luckin Coffee": "com.bjlc.luckycoffee",
|
| 152 |
+
"Line": "jp.naver.line",
|
| 153 |
+
"Linkedin": "com.linkedin.LinkedIn",
|
| 154 |
+
"Dcard": "com.dcard.app.Dcard",
|
| 155 |
+
"Youtube": "com.google.ios.youtube",
|
| 156 |
+
"Spotify": "com.spotify.client",
|
| 157 |
+
"Netflix": "com.netflix.Netflix",
|
| 158 |
+
"Twitter": "com.atebits.Tweetie2",
|
| 159 |
+
"WhatsApp": "net.whatsapp.WhatsApp",
|
| 160 |
+
# Apple Native Apps (Apple 原生应用)
|
| 161 |
+
"Safari": "com.apple.mobilesafari",
|
| 162 |
+
"App Store": "com.apple.AppStore",
|
| 163 |
+
"设置": "com.apple.Preferences",
|
| 164 |
+
"相机": "com.apple.camera",
|
| 165 |
+
"照片": "com.apple.mobileslideshow",
|
| 166 |
+
"时钟": "com.apple.mobiletimer",
|
| 167 |
+
"闹钟": "com.apple.mobiletimer",
|
| 168 |
+
"备忘录": "com.apple.mobilenotes",
|
| 169 |
+
"提醒事项": "com.apple.reminders",
|
| 170 |
+
"快捷指令": "com.apple.shortcuts",
|
| 171 |
+
"天气": "com.apple.weather",
|
| 172 |
+
"日历": "com.apple.mobilecal",
|
| 173 |
+
"地图": "com.apple.Maps",
|
| 174 |
+
"电话": "com.apple.mobilephone",
|
| 175 |
+
"通讯录": "com.apple.MobileAddressBook",
|
| 176 |
+
"信息": "com.apple.MobileSMS",
|
| 177 |
+
"Facetime": "com.apple.facetime",
|
| 178 |
+
"FaceTime": "com.apple.facetime",
|
| 179 |
+
"计算器": "com.apple.calculator",
|
| 180 |
+
"家庭": "com.apple.Home",
|
| 181 |
+
"健康": "com.apple.Health",
|
| 182 |
+
"钱包": "com.apple.Passbook",
|
| 183 |
+
"股市": "com.apple.stocks",
|
| 184 |
+
"图书": "com.apple.iBooks",
|
| 185 |
+
"新闻": "com.apple.news",
|
| 186 |
+
"视频": "com.apple.tv",
|
| 187 |
+
"文件": "com.apple.DocumentsApp",
|
| 188 |
+
"邮件": "com.apple.mobilemail",
|
| 189 |
+
"查找": "com.apple.findmy",
|
| 190 |
+
"翻译": "com.apple.Translate",
|
| 191 |
+
"音乐": "com.apple.Music",
|
| 192 |
+
"播客": "com.apple.podcasts",
|
| 193 |
+
"库乐队": "com.apple.mobilegarageband",
|
| 194 |
+
"语音备忘录": "com.apple.VoiceMemos",
|
| 195 |
+
"iMovie": "com.apple.iMovie",
|
| 196 |
+
"Watch": "com.apple.Bridge",
|
| 197 |
+
"Apple Store": "com.apple.store.Jolly",
|
| 198 |
+
"TestFlight": "com.apple.TestFlight",
|
| 199 |
+
"Keynote": "com.apple.Keynote",
|
| 200 |
+
"Keynote 讲演": "com.apple.Keynote",
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def get_bundle_id(app_name: str) -> str | None:
|
| 205 |
+
"""
|
| 206 |
+
Get the iOS bundle ID for an app.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
app_name: The display name of the app.
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
The iOS bundle ID, or None if not found.
|
| 213 |
+
"""
|
| 214 |
+
return APP_PACKAGES_IOS.get(app_name)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def get_app_name(bundle_id: str) -> str | None:
|
| 218 |
+
"""
|
| 219 |
+
Get the app name from an iOS bundle ID.
|
| 220 |
+
|
| 221 |
+
Args:
|
| 222 |
+
bundle_id: The iOS bundle ID.
|
| 223 |
+
|
| 224 |
+
Returns:
|
| 225 |
+
The display name of the app, or None if not found.
|
| 226 |
+
"""
|
| 227 |
+
for name, bid in APP_PACKAGES_IOS.items():
|
| 228 |
+
if bid == bundle_id:
|
| 229 |
+
return name
|
| 230 |
+
return None
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def list_supported_apps() -> list[str]:
|
| 234 |
+
"""
|
| 235 |
+
Get a list of all supported iOS app names.
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
List of app names.
|
| 239 |
+
"""
|
| 240 |
+
return list(APP_PACKAGES_IOS.keys())
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def check_app_installed(app_name: str, wda_url: str = "http://localhost:8100") -> bool:
|
| 244 |
+
"""
|
| 245 |
+
Check if an app is installed on the iOS device.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
app_name: The display name of the app.
|
| 249 |
+
wda_url: WebDriverAgent URL.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
True if app is installed, False otherwise.
|
| 253 |
+
|
| 254 |
+
Note:
|
| 255 |
+
This uses the iTunes API to get app information. For actual
|
| 256 |
+
installation check on device, you would need to use WDA's
|
| 257 |
+
app listing capabilities or URL scheme checking.
|
| 258 |
+
"""
|
| 259 |
+
bundle_id = get_bundle_id(app_name)
|
| 260 |
+
if not bundle_id:
|
| 261 |
+
return False
|
| 262 |
+
|
| 263 |
+
try:
|
| 264 |
+
import requests
|
| 265 |
+
|
| 266 |
+
# Query iTunes API for app info
|
| 267 |
+
url = f"https://itunes.apple.com/lookup?bundleId={bundle_id}"
|
| 268 |
+
response = requests.get(url, timeout=10)
|
| 269 |
+
|
| 270 |
+
if response.status_code == 200:
|
| 271 |
+
data = response.json()
|
| 272 |
+
return data.get("resultCount", 0) > 0
|
| 273 |
+
|
| 274 |
+
except ImportError:
|
| 275 |
+
print("Error: requests library required. Install: pip install requests")
|
| 276 |
+
except Exception as e:
|
| 277 |
+
print(f"Error checking app installation: {e}")
|
| 278 |
+
|
| 279 |
+
return False
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def get_app_info_from_itunes(bundle_id: str) -> dict | None:
|
| 283 |
+
"""
|
| 284 |
+
Get app information from iTunes API using bundle ID.
|
| 285 |
+
|
| 286 |
+
Args:
|
| 287 |
+
bundle_id: The iOS bundle ID.
|
| 288 |
+
|
| 289 |
+
Returns:
|
| 290 |
+
Dictionary with app info (name, version, etc.) or None if not found.
|
| 291 |
+
"""
|
| 292 |
+
try:
|
| 293 |
+
import requests
|
| 294 |
+
|
| 295 |
+
url = f"https://itunes.apple.com/lookup?bundleId={bundle_id}"
|
| 296 |
+
response = requests.get(url, timeout=10)
|
| 297 |
+
|
| 298 |
+
if response.status_code == 200:
|
| 299 |
+
data = response.json()
|
| 300 |
+
results = data.get("results", [])
|
| 301 |
+
if results:
|
| 302 |
+
return results[0]
|
| 303 |
+
|
| 304 |
+
except ImportError:
|
| 305 |
+
print("Error: requests library required. Install: pip install requests")
|
| 306 |
+
except Exception as e:
|
| 307 |
+
print(f"Error fetching app info: {e}")
|
| 308 |
+
|
| 309 |
+
return None
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def get_app_info_by_id(app_store_id: str) -> dict | None:
|
| 313 |
+
"""
|
| 314 |
+
Get app information from iTunes API using App Store ID.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
app_store_id: The numeric App Store ID (e.g., "414478124" for WeChat).
|
| 318 |
+
|
| 319 |
+
Returns:
|
| 320 |
+
Dictionary with app info or None if not found.
|
| 321 |
+
"""
|
| 322 |
+
try:
|
| 323 |
+
import requests
|
| 324 |
+
|
| 325 |
+
url = f"https://itunes.apple.com/lookup?id={app_store_id}"
|
| 326 |
+
response = requests.get(url, timeout=10)
|
| 327 |
+
|
| 328 |
+
if response.status_code == 200:
|
| 329 |
+
data = response.json()
|
| 330 |
+
results = data.get("results", [])
|
| 331 |
+
if results:
|
| 332 |
+
return results[0]
|
| 333 |
+
|
| 334 |
+
except ImportError:
|
| 335 |
+
print("Error: requests library required. Install: pip install requests")
|
| 336 |
+
except Exception as e:
|
| 337 |
+
print(f"Error fetching app info by ID: {e}")
|
| 338 |
+
|
| 339 |
+
return None
|
phone_agent/config/i18n.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Internationalization (i18n) module for Phone Agent UI messages."""
|
| 2 |
+
|
| 3 |
+
# Chinese messages
|
| 4 |
+
MESSAGES_ZH = {
|
| 5 |
+
"thinking": "思考过程",
|
| 6 |
+
"action": "执行动作",
|
| 7 |
+
"task_completed": "任务完成",
|
| 8 |
+
"done": "完成",
|
| 9 |
+
"starting_task": "开始执行任务",
|
| 10 |
+
"final_result": "最终结果",
|
| 11 |
+
"task_result": "任务结果",
|
| 12 |
+
"confirmation_required": "需要确认",
|
| 13 |
+
"continue_prompt": "是否继续?(y/n)",
|
| 14 |
+
"manual_operation_required": "需要人工操作",
|
| 15 |
+
"manual_operation_hint": "请手动完成操作...",
|
| 16 |
+
"press_enter_when_done": "完成后按回车继续",
|
| 17 |
+
"connection_failed": "连接失败",
|
| 18 |
+
"connection_successful": "连接成功",
|
| 19 |
+
"step": "步骤",
|
| 20 |
+
"task": "任务",
|
| 21 |
+
"result": "结果",
|
| 22 |
+
"performance_metrics": "性能指标",
|
| 23 |
+
"time_to_first_token": "首 Token 延迟 (TTFT)",
|
| 24 |
+
"time_to_thinking_end": "思考完成延迟",
|
| 25 |
+
"total_inference_time": "总推理时间",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
# English messages
|
| 29 |
+
MESSAGES_EN = {
|
| 30 |
+
"thinking": "Thinking",
|
| 31 |
+
"action": "Action",
|
| 32 |
+
"task_completed": "Task Completed",
|
| 33 |
+
"done": "Done",
|
| 34 |
+
"starting_task": "Starting task",
|
| 35 |
+
"final_result": "Final Result",
|
| 36 |
+
"task_result": "Task Result",
|
| 37 |
+
"confirmation_required": "Confirmation Required",
|
| 38 |
+
"continue_prompt": "Continue? (y/n)",
|
| 39 |
+
"manual_operation_required": "Manual Operation Required",
|
| 40 |
+
"manual_operation_hint": "Please complete the operation manually...",
|
| 41 |
+
"press_enter_when_done": "Press Enter when done",
|
| 42 |
+
"connection_failed": "Connection Failed",
|
| 43 |
+
"connection_successful": "Connection Successful",
|
| 44 |
+
"step": "Step",
|
| 45 |
+
"task": "Task",
|
| 46 |
+
"result": "Result",
|
| 47 |
+
"performance_metrics": "Performance Metrics",
|
| 48 |
+
"time_to_first_token": "Time to First Token (TTFT)",
|
| 49 |
+
"time_to_thinking_end": "Time to Thinking End",
|
| 50 |
+
"total_inference_time": "Total Inference Time",
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_messages(lang: str = "cn") -> dict:
|
| 55 |
+
"""
|
| 56 |
+
Get UI messages dictionary by language.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
lang: Language code, 'cn' for Chinese, 'en' for English.
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Dictionary of UI messages.
|
| 63 |
+
"""
|
| 64 |
+
if lang == "en":
|
| 65 |
+
return MESSAGES_EN
|
| 66 |
+
return MESSAGES_ZH
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def get_message(key: str, lang: str = "cn") -> str:
|
| 70 |
+
"""
|
| 71 |
+
Get a single UI message by key and language.
|
| 72 |
+
|
| 73 |
+
Args:
|
| 74 |
+
key: Message key.
|
| 75 |
+
lang: Language code, 'cn' for Chinese, 'en' for English.
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
Message string.
|
| 79 |
+
"""
|
| 80 |
+
messages = get_messages(lang)
|
| 81 |
+
return messages.get(key, key)
|
phone_agent/config/prompts.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System prompts for the AI agent."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
today = datetime.today()
|
| 6 |
+
formatted_date = today.strftime("%Y年%m月%d日")
|
| 7 |
+
|
| 8 |
+
SYSTEM_PROMPT = (
|
| 9 |
+
"今天的日期是: "
|
| 10 |
+
+ formatted_date
|
| 11 |
+
+ """
|
| 12 |
+
你是一个智能体分析专家,可以根据操作历史和当前状态图执行一系列操作来完成任务。
|
| 13 |
+
你必须严格按照要求输出以下格式:
|
| 14 |
+
<think>{think}</think>
|
| 15 |
+
<answer>{action}</answer>
|
| 16 |
+
|
| 17 |
+
其中:
|
| 18 |
+
- {think} 是对你为什么选择这个操作的简短推理说明。
|
| 19 |
+
- {action} 是本次执行的具体操作指令,必须严格遵循下方定义的指令格式。
|
| 20 |
+
|
| 21 |
+
操作指令及其作用如下:
|
| 22 |
+
- do(action="Launch", app="xxx")
|
| 23 |
+
Launch是启动目标app的操作,这比通过主屏幕导航更快。此操作完成后,您将自动收到结果状态的截图。
|
| 24 |
+
- do(action="Tap", element=[x,y])
|
| 25 |
+
Tap是点击操作,点击屏幕上的特定点。可用此操作点击按钮、选择项目、从主屏幕打开应用程序,或与任何可点击的用户界面元素进行交互。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的截图。
|
| 26 |
+
- do(action="Tap", element=[x,y], message="重要操作")
|
| 27 |
+
基本功能同Tap,点击涉及财产、支付、隐私等敏感按钮时触发。
|
| 28 |
+
- do(action="Type", text="xxx")
|
| 29 |
+
Type是输入操作,在当前聚焦的输入框中输入文本。使用此操作前,请确保输入框已被聚焦(先点击它)。输入的文本将像使用键盘输入一样输入。重要提示:手机可能正在使用 ADB 键盘,该键盘不会像普通键盘那样占用屏幕空间。要确认键盘已激活,请查看屏幕底部是否显示 'ADB Keyboard {ON}' 类似的文本,或者检查输入框是否处于激活/高亮状态。不要仅仅依赖视觉上的键盘显示。自动清除文本:当你使用输入操作时,输入框中现有的任何文本(包括占位符文本和实际输入)都会在输入新文本前自动清除。你无需在输入前手动清除文本——直接使用输入操作输入所需文本即可。操作完成后,你将自动收到结果状态的截图。
|
| 30 |
+
- do(action="Type_Name", text="xxx")
|
| 31 |
+
Type_Name是输入人名的操作,基本功能同Type。
|
| 32 |
+
- do(action="Interact")
|
| 33 |
+
Interact是当有多个满足条件的选项时而触发的交互操作,询问用户如何选择。
|
| 34 |
+
- do(action="Swipe", start=[x1,y1], end=[x2,y2])
|
| 35 |
+
Swipe是滑动操作,通过从起始坐标拖动到结束坐标来执行滑动手势。可用于滚动内容、在屏幕之间导航、下拉通知栏以及项目栏或进行基于手势的导航。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。滑动持续时间会自动调整以实现自然的移动。此操作完成后,您将自动收到结果状态的截图。
|
| 36 |
+
- do(action="Note", message="True")
|
| 37 |
+
记录当前页面内容以便后续总结。
|
| 38 |
+
- do(action="Call_API", instruction="xxx")
|
| 39 |
+
总结或评论当前页面或已记录的内容。
|
| 40 |
+
- do(action="Long Press", element=[x,y])
|
| 41 |
+
Long Pres是长按操作,在屏幕上的特定点长按指定时间。可用于触发上下文菜单、选择文本或激活长按交互。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的屏幕截图。
|
| 42 |
+
- do(action="Double Tap", element=[x,y])
|
| 43 |
+
Double Tap在屏幕上的特定点快速连续点按两次。使用此操作可以激活双击交互,如缩放、选择文本或打开项目。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的截图。
|
| 44 |
+
- do(action="Take_over", message="xxx")
|
| 45 |
+
Take_over是接管操作,表示在登录和验证阶段需要用户协助。
|
| 46 |
+
- do(action="Back")
|
| 47 |
+
导航返回到上一个屏幕或关闭当前对话框。相当于按下 Android 的返回按钮。使用此操作可以从更深的屏幕返回、关闭弹出窗口或退出当前上下文。此操作完成后,您将自动收到结果状态的截图。
|
| 48 |
+
- do(action="Home")
|
| 49 |
+
Home是回到系统桌面的操作,相当于按下 Android 主屏幕按钮。使用此操作可退出当前应用并返回启动器,或从已知状态启动新任务。此操作完成后,您将自动收到结果状态的截图。
|
| 50 |
+
- do(action="Wait", duration="x seconds")
|
| 51 |
+
等待页面加载,x为需要等待多少秒。
|
| 52 |
+
- finish(message="xxx")
|
| 53 |
+
finish是结束任务的操作,表示准确完整完成任务,message是终止信息。
|
| 54 |
+
|
| 55 |
+
必须遵循的规则:
|
| 56 |
+
1. 在执行任何操作前,先检查当前app是否是目标app,如果不是,先执行 Launch。
|
| 57 |
+
2. 如果进入到了无关页面,先执行 Back。如果执行Back后页面没有变化,请点击页面左上角的返回键进行返回,或者右上角的X号关闭。
|
| 58 |
+
3. 如果页面未加载出内容,最多连续 Wait 三次,否则执行 Back重新进入。
|
| 59 |
+
4. 如果页面显示网��问题,需要重新加载,请点击重新加载。
|
| 60 |
+
5. 如果当前页面找不到目标联系人、商品、店铺等信息,可以尝试 Swipe 滑动查找。
|
| 61 |
+
6. 遇到价格区间、时间区间等筛选条件,如果没有完全符合的,可以放宽要求。
|
| 62 |
+
7. 在做小红书总结类任务时一定要筛选图文笔记。
|
| 63 |
+
8. 购物车全选后再点击全选可以把状态设为全不选,在做购物车任务时,如果购物车里已经有商品被选中时,你需要点击全选后再点击取消全选,再去找需要购买或者删除的商品。
|
| 64 |
+
9. 在做外卖任务时,如果相应店铺购物车里已经有其他商品你需要先把购物车清空再去购买用户指定的外卖。
|
| 65 |
+
10. 在做点外卖任务时,如果用户需要点多个外卖,请尽量在同一店铺进行购买,如果无法找到可以下单,并说明某个商品未找到。
|
| 66 |
+
11. 请严格遵循用户意图执行任务,用户的特殊要求可以执行多次搜索,滑动查找。比如(i)用户要求点一杯咖啡,要咸的,你可以直接搜索咸咖啡,或者搜索咖啡后滑动查找咸的咖啡,比如海盐咖啡。(ii)用户要找到XX群,发一条消息,你可以先搜索XX群,找不到结果后,将"群"字去掉,搜索XX重试。(iii)用户要找到宠物友好的餐厅,你可以搜索餐厅,找到筛选,找到设施,选择可带宠物,或者直接搜索可带宠物,必要时可以使用AI搜索。
|
| 67 |
+
12. 在选择日期时,如果原滑动方向与预期日期越来越远,请向反方向滑动查找。
|
| 68 |
+
13. 执行任务过程中如果有多个可选择的项目栏,请逐个查找每个项目栏,直到完成任务,一定不要在同一项目栏多次查找,从而陷入死循环。
|
| 69 |
+
14. 在执行下一步操作前请一定要检查上一步的操作是否生效,如果点击没生效,可能因为app反应较慢,请先稍微等待一下,如果还是不生效请调整一下点击位置重试,如果仍然不生效请跳过这一步继续任务,并在finish message说明点击不生效。
|
| 70 |
+
15. 在执行任务中如果遇到滑动不生效的情况,请调整一下起始点位置,增大滑动距离重试,如果还是不生效,有可能是已经滑到底了,请继续向反方向滑动,直到顶部或底部,如果仍然没有符合要求的结果,请跳过这一步继续任务,并在finish message说明但没找到要求的项目。
|
| 71 |
+
16. 在做游戏任务时如果在战斗页面如果有自动战斗一定要开启自动战斗,如果多轮历史状态相似要检查自动战斗是否开启。
|
| 72 |
+
17. 如果没有合适的搜索结果,可能是因为搜索页面不对,请返回到搜索页面的上一级尝试重新搜索,如果尝试三次返回上一级搜索后仍然没有符合要求的结果,执行 finish(message="原因")。
|
| 73 |
+
18. 在结束任务前请一定要仔细检查任务是否完整准确的完成,如果出现错选、漏选、多选的情况,请返回之前的步骤进行纠正。
|
| 74 |
+
"""
|
| 75 |
+
)
|
phone_agent/config/prompts_en.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System prompts for the AI agent."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
today = datetime.today()
|
| 6 |
+
formatted_date = today.strftime("%Y-%m-%d, %A")
|
| 7 |
+
|
| 8 |
+
SYSTEM_PROMPT = (
|
| 9 |
+
"The current date: "
|
| 10 |
+
+ formatted_date
|
| 11 |
+
+ """
|
| 12 |
+
# Setup
|
| 13 |
+
You are a professional Android operation agent assistant that can fulfill the user's high-level instructions. Given a screenshot of the Android interface at each step, you first analyze the situation, then plan the best course of action using Python-style pseudo-code.
|
| 14 |
+
|
| 15 |
+
# More details about the code
|
| 16 |
+
Your response format must be structured as follows:
|
| 17 |
+
|
| 18 |
+
Think first: Use <think>...</think> to analyze the current screen, identify key elements, and determine the most efficient action.
|
| 19 |
+
Provide the action: Use <answer>...</answer> to return a single line of pseudo-code representing the operation.
|
| 20 |
+
|
| 21 |
+
Your output should STRICTLY follow the format:
|
| 22 |
+
<think>
|
| 23 |
+
[Your thought]
|
| 24 |
+
</think>
|
| 25 |
+
<answer>
|
| 26 |
+
[Your operation code]
|
| 27 |
+
</answer>
|
| 28 |
+
|
| 29 |
+
- **Tap**
|
| 30 |
+
Perform a tap action on a specified screen area. The element is a list of 2 integers, representing the coordinates of the tap point.
|
| 31 |
+
**Example**:
|
| 32 |
+
<answer>
|
| 33 |
+
do(action="Tap", element=[x,y])
|
| 34 |
+
</answer>
|
| 35 |
+
- **Type**
|
| 36 |
+
Enter text into the currently focused input field.
|
| 37 |
+
**Example**:
|
| 38 |
+
<answer>
|
| 39 |
+
do(action="Type", text="Hello World")
|
| 40 |
+
</answer>
|
| 41 |
+
- **Swipe**
|
| 42 |
+
Perform a swipe action with start point and end point.
|
| 43 |
+
**Examples**:
|
| 44 |
+
<answer>
|
| 45 |
+
do(action="Swipe", start=[x1,y1], end=[x2,y2])
|
| 46 |
+
</answer>
|
| 47 |
+
- **Long Press**
|
| 48 |
+
Perform a long press action on a specified screen area.
|
| 49 |
+
You can add the element to the action to specify the long press area. The element is a list of 2 integers, representing the coordinates of the long press point.
|
| 50 |
+
**Example**:
|
| 51 |
+
<answer>
|
| 52 |
+
do(action="Long Press", element=[x,y])
|
| 53 |
+
</answer>
|
| 54 |
+
- **Launch**
|
| 55 |
+
Launch an app. Try to use launch action when you need to launch an app. Check the instruction to choose the right app before you use this action.
|
| 56 |
+
**Example**:
|
| 57 |
+
<answer>
|
| 58 |
+
do(action="Launch", app="Settings")
|
| 59 |
+
</answer>
|
| 60 |
+
- **Back**
|
| 61 |
+
Press the Back button to navigate to the previous screen.
|
| 62 |
+
**Example**:
|
| 63 |
+
<answer>
|
| 64 |
+
do(action="Back")
|
| 65 |
+
</answer>
|
| 66 |
+
- **Finish**
|
| 67 |
+
Terminate the program and optionally print a message.
|
| 68 |
+
**Example**:
|
| 69 |
+
<answer>
|
| 70 |
+
finish(message="Task completed.")
|
| 71 |
+
</answer>
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
REMEMBER:
|
| 75 |
+
- Think before you act: Always analyze the current UI and the best course of action before executing any step, and output in <think> part.
|
| 76 |
+
- Only ONE LINE of action in <answer> part per response: Each step must contain exactly one line of executable code.
|
| 77 |
+
- Generate execution code strictly according to format requirements.
|
| 78 |
+
"""
|
| 79 |
+
)
|
phone_agent/config/prompts_zh.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""System prompts for the AI agent."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
today = datetime.today()
|
| 6 |
+
weekday_names = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
| 7 |
+
weekday = weekday_names[today.weekday()]
|
| 8 |
+
formatted_date = today.strftime("%Y年%m月%d日") + " " + weekday
|
| 9 |
+
|
| 10 |
+
SYSTEM_PROMPT = (
|
| 11 |
+
"今天的日期是: "
|
| 12 |
+
+ formatted_date
|
| 13 |
+
+ """
|
| 14 |
+
你是一个智能体分析专家,可以根据操作历史和当前状态图执行一系列操作来完成任务。
|
| 15 |
+
你必须严格按照要求输出以下格式:
|
| 16 |
+
<think>{think}</think>
|
| 17 |
+
<answer>{action}</answer>
|
| 18 |
+
|
| 19 |
+
其中:
|
| 20 |
+
- {think} 是对你为什么选择这个操作的简短推理说明。
|
| 21 |
+
- {action} 是本次执行的具体操作指令,必须严格遵循下方定义的指令格式。
|
| 22 |
+
|
| 23 |
+
操作指令及其作用如下:
|
| 24 |
+
- do(action="Launch", app="xxx")
|
| 25 |
+
Launch是启动目标app的操作,这比通过主屏幕导航更快。此操作完成后,您将自动收到结果状态的截图。
|
| 26 |
+
- do(action="Tap", element=[x,y])
|
| 27 |
+
Tap是点击操作,点击屏幕上的特定点。可用此操作点击按钮、选择项目、从主屏幕打开应用程序,或与任何可点击的用户界面元素进行交互。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的截图。
|
| 28 |
+
- do(action="Tap", element=[x,y], message="重要操作")
|
| 29 |
+
基本功能同Tap,点击涉及财产、支付、隐私等敏感按钮时触发。
|
| 30 |
+
- do(action="Type", text="xxx")
|
| 31 |
+
Type是输入操作,在当前聚焦的输入框中输入文本。使用此操作前,请确保输入框已被聚焦(先点击它)。输入的文本将像使用键盘输入一样输入。重要提示:手机可能正在使用 ADB 键盘,该键盘不会像普通键盘那样占用屏幕空间。要确认键盘已激活,请查看屏幕底部是否显示 'ADB Keyboard {ON}' 类似的文本,或者检查输入框是否处于激活/高亮状态。不要仅仅依赖视觉上的键盘显示。自动清除文本:当你使用输入操作时,输入框中现有的任何文本(包括占位符文本和实际输入)都会在输入新文本前自动清除。你无需在输入前手动清除文本——直接使用输入操作输入所需文本即可。操作完成后,你将自动收到结果状态的截图。
|
| 32 |
+
- do(action="Type_Name", text="xxx")
|
| 33 |
+
Type_Name是输入人名的操作,基本功能同Type。
|
| 34 |
+
- do(action="Interact")
|
| 35 |
+
Interact是当有多个满足条件的选项时而触发的交互操作,询问用户如何选择。
|
| 36 |
+
- do(action="Swipe", start=[x1,y1], end=[x2,y2])
|
| 37 |
+
Swipe是滑动操作,通过从起始坐标拖动到结束坐标来执行滑动手势。可用于滚动内容、在屏幕之间导航、下拉通知栏以及项目栏或进行基于手势的导航。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。滑动持续时间会自动调整以实现自然的移动。此操作完成后,您将自动收到结果状态的截图。
|
| 38 |
+
- do(action="Note", message="True")
|
| 39 |
+
记录当前页面内容以便后续总结。
|
| 40 |
+
- do(action="Call_API", instruction="xxx")
|
| 41 |
+
总结或评论当前页面或已记录的内容。
|
| 42 |
+
- do(action="Long Press", element=[x,y])
|
| 43 |
+
Long Pres是长按操作,在屏幕上的特定点长按指定时间。可用于触发上下文菜单、选择文本或激活长按交互。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的屏幕截图。
|
| 44 |
+
- do(action="Double Tap", element=[x,y])
|
| 45 |
+
Double Tap在屏幕上的特定点快速连续点按两次。使用此操作可以激活双击交互,如缩放、选择文本或打开项目。坐标系统从左上角 (0,0) 开始到右下角(999,999)结束。此操作完成后,您将自动收到结果状态的截图。
|
| 46 |
+
- do(action="Take_over", message="xxx")
|
| 47 |
+
Take_over是接管操作,表示在登录和验证阶段需要用户协助。
|
| 48 |
+
- do(action="Back")
|
| 49 |
+
导航返回到上一个屏幕或关闭当前对话框。相当于按下 Android 的返回按钮。使用此操作可以从更深的屏幕返回、关闭弹出窗口或退出当前上下文。此操作完成后,您将自动收到结果状态的截图。
|
| 50 |
+
- do(action="Home")
|
| 51 |
+
Home是回到系统桌面的操作,相当于按下 Android 主屏幕按钮。使用此操作可退出当前应用并返回启动器,或从已知状态启动新任务。此操作完成后,您将自动收到结果状态的截图。
|
| 52 |
+
- do(action="Wait", duration="x seconds")
|
| 53 |
+
等待页面加载,x为需要等待多少秒。
|
| 54 |
+
- finish(message="xxx")
|
| 55 |
+
finish是结束任务的操作,表示准确完整完成任务,message是终止信息。
|
| 56 |
+
|
| 57 |
+
必须遵循的规则:
|
| 58 |
+
1. 在执行任何操作前,先检查当前app是否是目标app,如果不是,先执行 Launch。
|
| 59 |
+
2. 如果进入到了无关页面,先执行 Back。如果执行Back后页面没有变化,请点击页面左上角的返回键进���返回,或者右上角的X号关闭。
|
| 60 |
+
3. 如果页面未加载出内容,最多连续 Wait 三次,否则执行 Back重新进入。
|
| 61 |
+
4. 如果页面显示网络问题,需要重新加载,请点击重新加载。
|
| 62 |
+
5. 如果当前页面找不到目标联系人、商品、店铺等信息,可以尝试 Swipe 滑动查找。
|
| 63 |
+
6. 遇到价格区间、时间区间等筛选条件,如果没有完全符合的,可以放宽要求。
|
| 64 |
+
7. 在做小红书总结类任务时一定要筛选图文笔记。
|
| 65 |
+
8. 购物车全选后再点击全选可以把状态设为全不选,在做购物车任务时,如果购物车里已经有商品被选中时,你需要点击全选后再点击取消全选,再去找需要购买或者删除的商品。
|
| 66 |
+
9. 在做外卖任务时,如果相应店铺购物车里已经有其他商品你需要先把购物车清空再去购买用户指定的外卖。
|
| 67 |
+
10. 在做点外卖任务时,如果用户需要点多个外卖,请尽量在同一店铺进行购买,如果无法找到可以下单,并说明某个商品未找到。
|
| 68 |
+
11. 请严格遵循用户意图执行任务,用户的特殊要求可以执行多次搜索,滑动查找。比如(i)用户要求点一杯咖啡,要咸的,你可以直接搜索咸咖啡,或者搜索咖啡后滑动查找咸的咖啡,比如海盐咖啡。(ii)用户要找到XX群,发一条消息,你可以先搜索XX群,找不到结果后,将"群"字去掉,搜索XX重试。(iii)用户要找到宠物友好的餐厅,你可以搜索餐厅,找到筛选,找到设施,选择可带宠物,或者直接搜索可带宠物,必要时可以使用AI搜索。
|
| 69 |
+
12. 在选择日期时,如果原滑动方向与预期日期越来越远,请向反方向滑动查找。
|
| 70 |
+
13. 执行任务过程中如果有多个可选择的项目栏,请逐个查找每个项目栏,直到完成任务,一定不要在同一项目栏多次查找,从而陷入死循环。
|
| 71 |
+
14. 在执行下一步操作前请一定要检查上一步的操作是否生效,如果点击没生效,可能因为app反应较慢,请先稍微等待一下,如果还是不生效请调整一下点击位置重试,如果仍然不生效请跳过这一步继续任务,并在finish message说明点击不生效。
|
| 72 |
+
15. 在执行任务中如果遇到滑动不生效的情况,请调整一下起始点位置,增大滑动距离重试,如果还是不生效,有可能是已经滑到底了,请继续向反方向滑动,直到顶部或底部,如果仍然没有符合要求的结果,请跳过这一步继续任务,并在finish message说明但没找到要求的项目。
|
| 73 |
+
16. 在做游戏任务时如果在战斗页面如果有自动战斗一定要开启自动战斗,如果多轮历史状态相似要检查自动战斗是否开启。
|
| 74 |
+
17. 如果没有合适的搜索结果,可能是因为搜索页面不对,请返回到搜索页面的上一级尝试重新搜索,如果尝试三次返回上一级搜索后仍然没有符合要求的结果,执行 finish(message="原因")。
|
| 75 |
+
18. 在结束任务前请一定要仔细检查任务是否完整准确的完成,如果出现错选、漏选、多选的情况,请返回之前的步骤进行纠正。
|
| 76 |
+
"""
|
| 77 |
+
)
|
phone_agent/config/timing.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Timing configuration for Phone Agent.
|
| 2 |
+
|
| 3 |
+
This module defines all configurable waiting times used throughout the application.
|
| 4 |
+
Users can customize these values by modifying this file or by setting environment variables.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class ActionTimingConfig:
|
| 13 |
+
"""Configuration for action handler timing delays."""
|
| 14 |
+
|
| 15 |
+
# Text input related delays (in seconds)
|
| 16 |
+
keyboard_switch_delay: float = 1.0 # Delay after switching to ADB keyboard
|
| 17 |
+
text_clear_delay: float = 1.0 # Delay after clearing text
|
| 18 |
+
text_input_delay: float = 1.0 # Delay after typing text
|
| 19 |
+
keyboard_restore_delay: float = 1.0 # Delay after restoring original keyboard
|
| 20 |
+
|
| 21 |
+
def __post_init__(self):
|
| 22 |
+
"""Load values from environment variables if present."""
|
| 23 |
+
self.keyboard_switch_delay = float(
|
| 24 |
+
os.getenv("PHONE_AGENT_KEYBOARD_SWITCH_DELAY", self.keyboard_switch_delay)
|
| 25 |
+
)
|
| 26 |
+
self.text_clear_delay = float(
|
| 27 |
+
os.getenv("PHONE_AGENT_TEXT_CLEAR_DELAY", self.text_clear_delay)
|
| 28 |
+
)
|
| 29 |
+
self.text_input_delay = float(
|
| 30 |
+
os.getenv("PHONE_AGENT_TEXT_INPUT_DELAY", self.text_input_delay)
|
| 31 |
+
)
|
| 32 |
+
self.keyboard_restore_delay = float(
|
| 33 |
+
os.getenv("PHONE_AGENT_KEYBOARD_RESTORE_DELAY", self.keyboard_restore_delay)
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass
|
| 38 |
+
class DeviceTimingConfig:
|
| 39 |
+
"""Configuration for device operation timing delays."""
|
| 40 |
+
|
| 41 |
+
# Default delays for various device operations (in seconds)
|
| 42 |
+
default_tap_delay: float = 1.0 # Default delay after tap
|
| 43 |
+
default_double_tap_delay: float = 1.0 # Default delay after double tap
|
| 44 |
+
double_tap_interval: float = 0.1 # Interval between two taps in double tap
|
| 45 |
+
default_long_press_delay: float = 1.0 # Default delay after long press
|
| 46 |
+
default_swipe_delay: float = 1.0 # Default delay after swipe
|
| 47 |
+
default_back_delay: float = 1.0 # Default delay after back button
|
| 48 |
+
default_home_delay: float = 1.0 # Default delay after home button
|
| 49 |
+
default_launch_delay: float = 1.0 # Default delay after launching app
|
| 50 |
+
|
| 51 |
+
def __post_init__(self):
|
| 52 |
+
"""Load values from environment variables if present."""
|
| 53 |
+
self.default_tap_delay = float(
|
| 54 |
+
os.getenv("PHONE_AGENT_TAP_DELAY", self.default_tap_delay)
|
| 55 |
+
)
|
| 56 |
+
self.default_double_tap_delay = float(
|
| 57 |
+
os.getenv("PHONE_AGENT_DOUBLE_TAP_DELAY", self.default_double_tap_delay)
|
| 58 |
+
)
|
| 59 |
+
self.double_tap_interval = float(
|
| 60 |
+
os.getenv("PHONE_AGENT_DOUBLE_TAP_INTERVAL", self.double_tap_interval)
|
| 61 |
+
)
|
| 62 |
+
self.default_long_press_delay = float(
|
| 63 |
+
os.getenv("PHONE_AGENT_LONG_PRESS_DELAY", self.default_long_press_delay)
|
| 64 |
+
)
|
| 65 |
+
self.default_swipe_delay = float(
|
| 66 |
+
os.getenv("PHONE_AGENT_SWIPE_DELAY", self.default_swipe_delay)
|
| 67 |
+
)
|
| 68 |
+
self.default_back_delay = float(
|
| 69 |
+
os.getenv("PHONE_AGENT_BACK_DELAY", self.default_back_delay)
|
| 70 |
+
)
|
| 71 |
+
self.default_home_delay = float(
|
| 72 |
+
os.getenv("PHONE_AGENT_HOME_DELAY", self.default_home_delay)
|
| 73 |
+
)
|
| 74 |
+
self.default_launch_delay = float(
|
| 75 |
+
os.getenv("PHONE_AGENT_LAUNCH_DELAY", self.default_launch_delay)
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@dataclass
|
| 80 |
+
class ConnectionTimingConfig:
|
| 81 |
+
"""Configuration for ADB connection timing delays."""
|
| 82 |
+
|
| 83 |
+
# ADB server and connection delays (in seconds)
|
| 84 |
+
adb_restart_delay: float = 2.0 # Wait time after enabling TCP/IP mode
|
| 85 |
+
server_restart_delay: float = (
|
| 86 |
+
1.0 # Wait time between killing and starting ADB server
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
def __post_init__(self):
|
| 90 |
+
"""Load values from environment variables if present."""
|
| 91 |
+
self.adb_restart_delay = float(
|
| 92 |
+
os.getenv("PHONE_AGENT_ADB_RESTART_DELAY", self.adb_restart_delay)
|
| 93 |
+
)
|
| 94 |
+
self.server_restart_delay = float(
|
| 95 |
+
os.getenv("PHONE_AGENT_SERVER_RESTART_DELAY", self.server_restart_delay)
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@dataclass
|
| 100 |
+
class TimingConfig:
|
| 101 |
+
"""Master timing configuration combining all timing settings."""
|
| 102 |
+
|
| 103 |
+
action: ActionTimingConfig
|
| 104 |
+
device: DeviceTimingConfig
|
| 105 |
+
connection: ConnectionTimingConfig
|
| 106 |
+
|
| 107 |
+
def __init__(self):
|
| 108 |
+
"""Initialize all timing configurations."""
|
| 109 |
+
self.action = ActionTimingConfig()
|
| 110 |
+
self.device = DeviceTimingConfig()
|
| 111 |
+
self.connection = ConnectionTimingConfig()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
# Global timing configuration instance
|
| 115 |
+
# Users can modify these values at runtime or through environment variables
|
| 116 |
+
TIMING_CONFIG = TimingConfig()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def get_timing_config() -> TimingConfig:
|
| 120 |
+
"""
|
| 121 |
+
Get the global timing configuration.
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
The global TimingConfig instance.
|
| 125 |
+
"""
|
| 126 |
+
return TIMING_CONFIG
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def update_timing_config(
|
| 130 |
+
action: ActionTimingConfig | None = None,
|
| 131 |
+
device: DeviceTimingConfig | None = None,
|
| 132 |
+
connection: ConnectionTimingConfig | None = None,
|
| 133 |
+
) -> None:
|
| 134 |
+
"""
|
| 135 |
+
Update the global timing configuration.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
action: New action timing configuration.
|
| 139 |
+
device: New device timing configuration.
|
| 140 |
+
connection: New connection timing configuration.
|
| 141 |
+
|
| 142 |
+
Example:
|
| 143 |
+
>>> from phone_agent.config.timing import update_timing_config, ActionTimingConfig
|
| 144 |
+
>>> custom_action = ActionTimingConfig(
|
| 145 |
+
... keyboard_switch_delay=0.5,
|
| 146 |
+
... text_input_delay=0.5
|
| 147 |
+
... )
|
| 148 |
+
>>> update_timing_config(action=custom_action)
|
| 149 |
+
"""
|
| 150 |
+
global TIMING_CONFIG
|
| 151 |
+
if action is not None:
|
| 152 |
+
TIMING_CONFIG.action = action
|
| 153 |
+
if device is not None:
|
| 154 |
+
TIMING_CONFIG.device = device
|
| 155 |
+
if connection is not None:
|
| 156 |
+
TIMING_CONFIG.connection = connection
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
__all__ = [
|
| 160 |
+
"ActionTimingConfig",
|
| 161 |
+
"DeviceTimingConfig",
|
| 162 |
+
"ConnectionTimingConfig",
|
| 163 |
+
"TimingConfig",
|
| 164 |
+
"TIMING_CONFIG",
|
| 165 |
+
"get_timing_config",
|
| 166 |
+
"update_timing_config",
|
| 167 |
+
]
|
phone_agent/device_factory.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Device factory for selecting ADB or HDC based on device type."""
|
| 2 |
+
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class DeviceType(Enum):
|
| 8 |
+
"""Type of device connection tool."""
|
| 9 |
+
|
| 10 |
+
ADB = "adb"
|
| 11 |
+
HDC = "hdc"
|
| 12 |
+
IOS = "ios"
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class DeviceFactory:
|
| 16 |
+
"""
|
| 17 |
+
Factory class for getting device-specific implementations.
|
| 18 |
+
|
| 19 |
+
This allows the system to work with both Android (ADB) and HarmonyOS (HDC) devices.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, device_type: DeviceType = DeviceType.ADB):
|
| 23 |
+
"""
|
| 24 |
+
Initialize the device factory.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
device_type: The type of device to use (ADB or HDC).
|
| 28 |
+
"""
|
| 29 |
+
self.device_type = device_type
|
| 30 |
+
self._module = None
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def module(self):
|
| 34 |
+
"""Get the appropriate device module (adb or hdc)."""
|
| 35 |
+
if self._module is None:
|
| 36 |
+
if self.device_type == DeviceType.ADB:
|
| 37 |
+
from phone_agent import adb
|
| 38 |
+
|
| 39 |
+
self._module = adb
|
| 40 |
+
elif self.device_type == DeviceType.HDC:
|
| 41 |
+
from phone_agent import hdc
|
| 42 |
+
|
| 43 |
+
self._module = hdc
|
| 44 |
+
else:
|
| 45 |
+
raise ValueError(f"Unknown device type: {self.device_type}")
|
| 46 |
+
return self._module
|
| 47 |
+
|
| 48 |
+
def get_screenshot(self, device_id: str | None = None, timeout: int = 10):
|
| 49 |
+
"""Get screenshot from device."""
|
| 50 |
+
return self.module.get_screenshot(device_id, timeout)
|
| 51 |
+
|
| 52 |
+
def get_current_app(self, device_id: str | None = None) -> str:
|
| 53 |
+
"""Get current app name."""
|
| 54 |
+
return self.module.get_current_app(device_id)
|
| 55 |
+
|
| 56 |
+
def tap(
|
| 57 |
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 58 |
+
):
|
| 59 |
+
"""Tap at coordinates."""
|
| 60 |
+
return self.module.tap(x, y, device_id, delay)
|
| 61 |
+
|
| 62 |
+
def double_tap(
|
| 63 |
+
self, x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 64 |
+
):
|
| 65 |
+
"""Double tap at coordinates."""
|
| 66 |
+
return self.module.double_tap(x, y, device_id, delay)
|
| 67 |
+
|
| 68 |
+
def long_press(
|
| 69 |
+
self,
|
| 70 |
+
x: int,
|
| 71 |
+
y: int,
|
| 72 |
+
duration_ms: int = 3000,
|
| 73 |
+
device_id: str | None = None,
|
| 74 |
+
delay: float | None = None,
|
| 75 |
+
):
|
| 76 |
+
"""Long press at coordinates."""
|
| 77 |
+
return self.module.long_press(x, y, duration_ms, device_id, delay)
|
| 78 |
+
|
| 79 |
+
def swipe(
|
| 80 |
+
self,
|
| 81 |
+
start_x: int,
|
| 82 |
+
start_y: int,
|
| 83 |
+
end_x: int,
|
| 84 |
+
end_y: int,
|
| 85 |
+
duration_ms: int | None = None,
|
| 86 |
+
device_id: str | None = None,
|
| 87 |
+
delay: float | None = None,
|
| 88 |
+
):
|
| 89 |
+
"""Swipe from start to end."""
|
| 90 |
+
return self.module.swipe(
|
| 91 |
+
start_x, start_y, end_x, end_y, duration_ms, device_id, delay
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
def back(self, device_id: str | None = None, delay: float | None = None):
|
| 95 |
+
"""Press back button."""
|
| 96 |
+
return self.module.back(device_id, delay)
|
| 97 |
+
|
| 98 |
+
def home(self, device_id: str | None = None, delay: float | None = None):
|
| 99 |
+
"""Press home button."""
|
| 100 |
+
return self.module.home(device_id, delay)
|
| 101 |
+
|
| 102 |
+
def launch_app(
|
| 103 |
+
self, app_name: str, device_id: str | None = None, delay: float | None = None
|
| 104 |
+
) -> bool:
|
| 105 |
+
"""Launch an app."""
|
| 106 |
+
return self.module.launch_app(app_name, device_id, delay)
|
| 107 |
+
|
| 108 |
+
def type_text(self, text: str, device_id: str | None = None):
|
| 109 |
+
"""Type text."""
|
| 110 |
+
return self.module.type_text(text, device_id)
|
| 111 |
+
|
| 112 |
+
def clear_text(self, device_id: str | None = None):
|
| 113 |
+
"""Clear text."""
|
| 114 |
+
return self.module.clear_text(device_id)
|
| 115 |
+
|
| 116 |
+
def detect_and_set_adb_keyboard(self, device_id: str | None = None) -> str:
|
| 117 |
+
"""Detect and set keyboard."""
|
| 118 |
+
return self.module.detect_and_set_adb_keyboard(device_id)
|
| 119 |
+
|
| 120 |
+
def restore_keyboard(self, ime: str, device_id: str | None = None):
|
| 121 |
+
"""Restore keyboard."""
|
| 122 |
+
return self.module.restore_keyboard(ime, device_id)
|
| 123 |
+
|
| 124 |
+
def list_devices(self):
|
| 125 |
+
"""List connected devices."""
|
| 126 |
+
return self.module.list_devices()
|
| 127 |
+
|
| 128 |
+
def get_connection_class(self):
|
| 129 |
+
"""Get the connection class (ADBConnection or HDCConnection)."""
|
| 130 |
+
if self.device_type == DeviceType.ADB:
|
| 131 |
+
from phone_agent.adb import ADBConnection
|
| 132 |
+
|
| 133 |
+
return ADBConnection
|
| 134 |
+
elif self.device_type == DeviceType.HDC:
|
| 135 |
+
from phone_agent.hdc import HDCConnection
|
| 136 |
+
|
| 137 |
+
return HDCConnection
|
| 138 |
+
else:
|
| 139 |
+
raise ValueError(f"Unknown device type: {self.device_type}")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# Global device factory instance
|
| 143 |
+
_device_factory: DeviceFactory | None = None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def set_device_type(device_type: DeviceType):
|
| 147 |
+
"""
|
| 148 |
+
Set the global device type.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
device_type: The device type to use (ADB or HDC).
|
| 152 |
+
"""
|
| 153 |
+
global _device_factory
|
| 154 |
+
_device_factory = DeviceFactory(device_type)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_device_factory() -> DeviceFactory:
|
| 158 |
+
"""
|
| 159 |
+
Get the global device factory instance.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
The device factory instance.
|
| 163 |
+
"""
|
| 164 |
+
global _device_factory
|
| 165 |
+
if _device_factory is None:
|
| 166 |
+
_device_factory = DeviceFactory(DeviceType.ADB) # Default to ADB
|
| 167 |
+
return _device_factory
|
phone_agent/hdc/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HDC utilities for HarmonyOS device interaction."""
|
| 2 |
+
|
| 3 |
+
from phone_agent.hdc.connection import (
|
| 4 |
+
HDCConnection,
|
| 5 |
+
ConnectionType,
|
| 6 |
+
DeviceInfo,
|
| 7 |
+
list_devices,
|
| 8 |
+
quick_connect,
|
| 9 |
+
set_hdc_verbose,
|
| 10 |
+
)
|
| 11 |
+
from phone_agent.hdc.device import (
|
| 12 |
+
back,
|
| 13 |
+
double_tap,
|
| 14 |
+
get_current_app,
|
| 15 |
+
home,
|
| 16 |
+
launch_app,
|
| 17 |
+
long_press,
|
| 18 |
+
swipe,
|
| 19 |
+
tap,
|
| 20 |
+
)
|
| 21 |
+
from phone_agent.hdc.input import (
|
| 22 |
+
clear_text,
|
| 23 |
+
detect_and_set_adb_keyboard,
|
| 24 |
+
restore_keyboard,
|
| 25 |
+
type_text,
|
| 26 |
+
)
|
| 27 |
+
from phone_agent.hdc.screenshot import get_screenshot
|
| 28 |
+
|
| 29 |
+
__all__ = [
|
| 30 |
+
# Screenshot
|
| 31 |
+
"get_screenshot",
|
| 32 |
+
# Input
|
| 33 |
+
"type_text",
|
| 34 |
+
"clear_text",
|
| 35 |
+
"detect_and_set_adb_keyboard",
|
| 36 |
+
"restore_keyboard",
|
| 37 |
+
# Device control
|
| 38 |
+
"get_current_app",
|
| 39 |
+
"tap",
|
| 40 |
+
"swipe",
|
| 41 |
+
"back",
|
| 42 |
+
"home",
|
| 43 |
+
"double_tap",
|
| 44 |
+
"long_press",
|
| 45 |
+
"launch_app",
|
| 46 |
+
# Connection management
|
| 47 |
+
"HDCConnection",
|
| 48 |
+
"DeviceInfo",
|
| 49 |
+
"ConnectionType",
|
| 50 |
+
"quick_connect",
|
| 51 |
+
"list_devices",
|
| 52 |
+
"set_hdc_verbose",
|
| 53 |
+
]
|
phone_agent/hdc/connection.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HDC connection management for HarmonyOS devices."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from enum import Enum
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
from phone_agent.config.timing import TIMING_CONFIG
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# Global flag to control HDC command output
|
| 14 |
+
_HDC_VERBOSE = os.getenv("HDC_VERBOSE", "false").lower() in ("true", "1", "yes")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _run_hdc_command(cmd: list, **kwargs) -> subprocess.CompletedProcess:
|
| 18 |
+
"""
|
| 19 |
+
Run HDC command with optional verbose output.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
cmd: Command list to execute.
|
| 23 |
+
**kwargs: Additional arguments for subprocess.run.
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
CompletedProcess result.
|
| 27 |
+
"""
|
| 28 |
+
if _HDC_VERBOSE:
|
| 29 |
+
print(f"[HDC] Running command: {' '.join(cmd)}")
|
| 30 |
+
|
| 31 |
+
result = subprocess.run(cmd, **kwargs)
|
| 32 |
+
|
| 33 |
+
if _HDC_VERBOSE and result.returncode != 0:
|
| 34 |
+
print(f"[HDC] Command failed with return code {result.returncode}")
|
| 35 |
+
if hasattr(result, 'stderr') and result.stderr:
|
| 36 |
+
print(f"[HDC] Error: {result.stderr}")
|
| 37 |
+
|
| 38 |
+
return result
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def set_hdc_verbose(verbose: bool):
|
| 42 |
+
"""Set HDC verbose mode globally."""
|
| 43 |
+
global _HDC_VERBOSE
|
| 44 |
+
_HDC_VERBOSE = verbose
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class ConnectionType(Enum):
|
| 48 |
+
"""Type of HDC connection."""
|
| 49 |
+
|
| 50 |
+
USB = "usb"
|
| 51 |
+
WIFI = "wifi"
|
| 52 |
+
REMOTE = "remote"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass
|
| 56 |
+
class DeviceInfo:
|
| 57 |
+
"""Information about a connected device."""
|
| 58 |
+
|
| 59 |
+
device_id: str
|
| 60 |
+
status: str
|
| 61 |
+
connection_type: ConnectionType
|
| 62 |
+
model: str | None = None
|
| 63 |
+
harmony_version: str | None = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class HDCConnection:
|
| 67 |
+
"""
|
| 68 |
+
Manages HDC connections to HarmonyOS devices.
|
| 69 |
+
|
| 70 |
+
Supports USB, WiFi, and remote TCP/IP connections.
|
| 71 |
+
|
| 72 |
+
Example:
|
| 73 |
+
>>> conn = HDCConnection()
|
| 74 |
+
>>> # Connect to remote device
|
| 75 |
+
>>> conn.connect("192.168.1.100:5555")
|
| 76 |
+
>>> # List devices
|
| 77 |
+
>>> devices = conn.list_devices()
|
| 78 |
+
>>> # Disconnect
|
| 79 |
+
>>> conn.disconnect("192.168.1.100:5555")
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
def __init__(self, hdc_path: str = "hdc"):
|
| 83 |
+
"""
|
| 84 |
+
Initialize HDC connection manager.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
hdc_path: Path to HDC executable.
|
| 88 |
+
"""
|
| 89 |
+
self.hdc_path = hdc_path
|
| 90 |
+
|
| 91 |
+
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
|
| 92 |
+
"""
|
| 93 |
+
Connect to a remote device via TCP/IP.
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
|
| 97 |
+
timeout: Connection timeout in seconds.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
Tuple of (success, message).
|
| 101 |
+
|
| 102 |
+
Note:
|
| 103 |
+
The remote device must have TCP/IP debugging enabled.
|
| 104 |
+
"""
|
| 105 |
+
# Validate address format
|
| 106 |
+
if ":" not in address:
|
| 107 |
+
address = f"{address}:5555" # Default HDC port
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
result = _run_hdc_command(
|
| 111 |
+
[self.hdc_path, "tconn", address],
|
| 112 |
+
capture_output=True,
|
| 113 |
+
text=True,
|
| 114 |
+
timeout=timeout,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
output = result.stdout + result.stderr
|
| 118 |
+
|
| 119 |
+
if "Connect OK" in output or "connected" in output.lower():
|
| 120 |
+
return True, f"Connected to {address}"
|
| 121 |
+
elif "already connected" in output.lower():
|
| 122 |
+
return True, f"Already connected to {address}"
|
| 123 |
+
else:
|
| 124 |
+
return False, output.strip()
|
| 125 |
+
|
| 126 |
+
except subprocess.TimeoutExpired:
|
| 127 |
+
return False, f"Connection timeout after {timeout}s"
|
| 128 |
+
except Exception as e:
|
| 129 |
+
return False, f"Connection error: {e}"
|
| 130 |
+
|
| 131 |
+
def disconnect(self, address: str | None = None) -> tuple[bool, str]:
|
| 132 |
+
"""
|
| 133 |
+
Disconnect from a remote device.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
address: Device address to disconnect. If None, disconnects all.
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Tuple of (success, message).
|
| 140 |
+
"""
|
| 141 |
+
try:
|
| 142 |
+
if address:
|
| 143 |
+
cmd = [self.hdc_path, "tdisconn", address]
|
| 144 |
+
else:
|
| 145 |
+
# HDC doesn't have a "disconnect all" command, so we need to list and disconnect each
|
| 146 |
+
devices = self.list_devices()
|
| 147 |
+
for device in devices:
|
| 148 |
+
if ":" in device.device_id: # Remote device
|
| 149 |
+
_run_hdc_command(
|
| 150 |
+
[self.hdc_path, "tdisconn", device.device_id],
|
| 151 |
+
capture_output=True,
|
| 152 |
+
text=True,
|
| 153 |
+
timeout=5
|
| 154 |
+
)
|
| 155 |
+
return True, "Disconnected all remote devices"
|
| 156 |
+
|
| 157 |
+
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
|
| 158 |
+
|
| 159 |
+
output = result.stdout + result.stderr
|
| 160 |
+
return True, output.strip() or "Disconnected"
|
| 161 |
+
|
| 162 |
+
except Exception as e:
|
| 163 |
+
return False, f"Disconnect error: {e}"
|
| 164 |
+
|
| 165 |
+
def list_devices(self) -> list[DeviceInfo]:
|
| 166 |
+
"""
|
| 167 |
+
List all connected devices.
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
List of DeviceInfo objects.
|
| 171 |
+
"""
|
| 172 |
+
try:
|
| 173 |
+
result = _run_hdc_command(
|
| 174 |
+
[self.hdc_path, "list", "targets"],
|
| 175 |
+
capture_output=True,
|
| 176 |
+
text=True,
|
| 177 |
+
timeout=5,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
devices = []
|
| 181 |
+
for line in result.stdout.strip().split("\n"):
|
| 182 |
+
if not line.strip():
|
| 183 |
+
continue
|
| 184 |
+
|
| 185 |
+
# HDC output format: device_id (status)
|
| 186 |
+
# Example: "192.168.1.100:5555" or "FMR0223C13000649"
|
| 187 |
+
device_id = line.strip()
|
| 188 |
+
|
| 189 |
+
# Determine connection type
|
| 190 |
+
if ":" in device_id:
|
| 191 |
+
conn_type = ConnectionType.REMOTE
|
| 192 |
+
else:
|
| 193 |
+
conn_type = ConnectionType.USB
|
| 194 |
+
|
| 195 |
+
# HDC doesn't provide detailed status in list command
|
| 196 |
+
# We assume "Connected" status for devices that appear
|
| 197 |
+
devices.append(
|
| 198 |
+
DeviceInfo(
|
| 199 |
+
device_id=device_id,
|
| 200 |
+
status="device",
|
| 201 |
+
connection_type=conn_type,
|
| 202 |
+
model=None,
|
| 203 |
+
)
|
| 204 |
+
)
|
| 205 |
+
|
| 206 |
+
return devices
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
print(f"Error listing devices: {e}")
|
| 210 |
+
return []
|
| 211 |
+
|
| 212 |
+
def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
|
| 213 |
+
"""
|
| 214 |
+
Get detailed information about a device.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
device_id: Device ID. If None, uses first available device.
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
DeviceInfo or None if not found.
|
| 221 |
+
"""
|
| 222 |
+
devices = self.list_devices()
|
| 223 |
+
|
| 224 |
+
if not devices:
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
if device_id is None:
|
| 228 |
+
return devices[0]
|
| 229 |
+
|
| 230 |
+
for device in devices:
|
| 231 |
+
if device.device_id == device_id:
|
| 232 |
+
return device
|
| 233 |
+
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
def is_connected(self, device_id: str | None = None) -> bool:
|
| 237 |
+
"""
|
| 238 |
+
Check if a device is connected.
|
| 239 |
+
|
| 240 |
+
Args:
|
| 241 |
+
device_id: Device ID to check. If None, checks if any device is connected.
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
True if connected, False otherwise.
|
| 245 |
+
"""
|
| 246 |
+
devices = self.list_devices()
|
| 247 |
+
|
| 248 |
+
if not devices:
|
| 249 |
+
return False
|
| 250 |
+
|
| 251 |
+
if device_id is None:
|
| 252 |
+
return len(devices) > 0
|
| 253 |
+
|
| 254 |
+
return any(d.device_id == device_id for d in devices)
|
| 255 |
+
|
| 256 |
+
def enable_tcpip(
|
| 257 |
+
self, port: int = 5555, device_id: str | None = None
|
| 258 |
+
) -> tuple[bool, str]:
|
| 259 |
+
"""
|
| 260 |
+
Enable TCP/IP debugging on a USB-connected device.
|
| 261 |
+
|
| 262 |
+
This allows subsequent wireless connections to the device.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
port: TCP port for HDC (default: 5555).
|
| 266 |
+
device_id: Device ID. If None, uses first available device.
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
Tuple of (success, message).
|
| 270 |
+
|
| 271 |
+
Note:
|
| 272 |
+
The device must be connected via USB first.
|
| 273 |
+
After this, you can disconnect USB and connect via WiFi.
|
| 274 |
+
"""
|
| 275 |
+
try:
|
| 276 |
+
cmd = [self.hdc_path]
|
| 277 |
+
if device_id:
|
| 278 |
+
cmd.extend(["-t", device_id])
|
| 279 |
+
cmd.extend(["tmode", "port", str(port)])
|
| 280 |
+
|
| 281 |
+
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=10)
|
| 282 |
+
|
| 283 |
+
output = result.stdout + result.stderr
|
| 284 |
+
|
| 285 |
+
if result.returncode == 0 or "success" in output.lower():
|
| 286 |
+
time.sleep(TIMING_CONFIG.connection.adb_restart_delay)
|
| 287 |
+
return True, f"TCP/IP mode enabled on port {port}"
|
| 288 |
+
else:
|
| 289 |
+
return False, output.strip()
|
| 290 |
+
|
| 291 |
+
except Exception as e:
|
| 292 |
+
return False, f"Error enabling TCP/IP: {e}"
|
| 293 |
+
|
| 294 |
+
def get_device_ip(self, device_id: str | None = None) -> str | None:
|
| 295 |
+
"""
|
| 296 |
+
Get the IP address of a connected device.
|
| 297 |
+
|
| 298 |
+
Args:
|
| 299 |
+
device_id: Device ID. If None, uses first available device.
|
| 300 |
+
|
| 301 |
+
Returns:
|
| 302 |
+
IP address string or None if not found.
|
| 303 |
+
"""
|
| 304 |
+
try:
|
| 305 |
+
cmd = [self.hdc_path]
|
| 306 |
+
if device_id:
|
| 307 |
+
cmd.extend(["-t", device_id])
|
| 308 |
+
cmd.extend(["shell", "ifconfig"])
|
| 309 |
+
|
| 310 |
+
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
|
| 311 |
+
|
| 312 |
+
# Parse IP from ifconfig output
|
| 313 |
+
for line in result.stdout.split("\n"):
|
| 314 |
+
if "inet addr:" in line or "inet " in line:
|
| 315 |
+
parts = line.strip().split()
|
| 316 |
+
for i, part in enumerate(parts):
|
| 317 |
+
if "addr:" in part:
|
| 318 |
+
ip = part.split(":")[1]
|
| 319 |
+
# Filter out localhost
|
| 320 |
+
if not ip.startswith("127."):
|
| 321 |
+
return ip
|
| 322 |
+
elif part == "inet" and i + 1 < len(parts):
|
| 323 |
+
ip = parts[i + 1].split("/")[0]
|
| 324 |
+
if not ip.startswith("127."):
|
| 325 |
+
return ip
|
| 326 |
+
|
| 327 |
+
return None
|
| 328 |
+
|
| 329 |
+
except Exception as e:
|
| 330 |
+
print(f"Error getting device IP: {e}")
|
| 331 |
+
return None
|
| 332 |
+
|
| 333 |
+
def restart_server(self) -> tuple[bool, str]:
|
| 334 |
+
"""
|
| 335 |
+
Restart the HDC server.
|
| 336 |
+
|
| 337 |
+
Returns:
|
| 338 |
+
Tuple of (success, message).
|
| 339 |
+
"""
|
| 340 |
+
try:
|
| 341 |
+
# Kill server
|
| 342 |
+
_run_hdc_command(
|
| 343 |
+
[self.hdc_path, "kill"], capture_output=True, timeout=5
|
| 344 |
+
)
|
| 345 |
+
|
| 346 |
+
time.sleep(TIMING_CONFIG.connection.server_restart_delay)
|
| 347 |
+
|
| 348 |
+
# Start server (HDC auto-starts when running commands)
|
| 349 |
+
_run_hdc_command(
|
| 350 |
+
[self.hdc_path, "start", "-r"], capture_output=True, timeout=5
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
return True, "HDC server restarted"
|
| 354 |
+
|
| 355 |
+
except Exception as e:
|
| 356 |
+
return False, f"Error restarting server: {e}"
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def quick_connect(address: str) -> tuple[bool, str]:
|
| 360 |
+
"""
|
| 361 |
+
Quick helper to connect to a remote device.
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
|
| 365 |
+
|
| 366 |
+
Returns:
|
| 367 |
+
Tuple of (success, message).
|
| 368 |
+
"""
|
| 369 |
+
conn = HDCConnection()
|
| 370 |
+
return conn.connect(address)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def list_devices() -> list[DeviceInfo]:
|
| 374 |
+
"""
|
| 375 |
+
Quick helper to list connected devices.
|
| 376 |
+
|
| 377 |
+
Returns:
|
| 378 |
+
List of DeviceInfo objects.
|
| 379 |
+
"""
|
| 380 |
+
conn = HDCConnection()
|
| 381 |
+
return conn.list_devices()
|
phone_agent/hdc/device.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Device control utilities for HarmonyOS automation."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
import time
|
| 6 |
+
from typing import List, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
from phone_agent.config.apps_harmonyos import APP_ABILITIES, APP_PACKAGES
|
| 9 |
+
from phone_agent.config.timing import TIMING_CONFIG
|
| 10 |
+
from phone_agent.hdc.connection import _run_hdc_command
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_current_app(device_id: str | None = None) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Get the currently focused app name.
|
| 16 |
+
|
| 17 |
+
Args:
|
| 18 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 19 |
+
|
| 20 |
+
Returns:
|
| 21 |
+
The app name if recognized, otherwise "System Home".
|
| 22 |
+
"""
|
| 23 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 24 |
+
|
| 25 |
+
result = _run_hdc_command(
|
| 26 |
+
hdc_prefix + ["shell", "hidumper", "-s", "WindowManagerService", "-a", "-a"],
|
| 27 |
+
capture_output=True,
|
| 28 |
+
text=True,
|
| 29 |
+
encoding="utf-8"
|
| 30 |
+
)
|
| 31 |
+
output = result.stdout
|
| 32 |
+
if not output:
|
| 33 |
+
raise ValueError("No output from hidumper")
|
| 34 |
+
|
| 35 |
+
# Parse window focus info
|
| 36 |
+
for line in output.split("\n"):
|
| 37 |
+
if "focused" in line.lower() or "current" in line.lower():
|
| 38 |
+
for app_name, package in APP_PACKAGES.items():
|
| 39 |
+
if package in line:
|
| 40 |
+
return app_name
|
| 41 |
+
|
| 42 |
+
return "System Home"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def tap(
|
| 46 |
+
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 47 |
+
) -> None:
|
| 48 |
+
"""
|
| 49 |
+
Tap at the specified coordinates.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
x: X coordinate.
|
| 53 |
+
y: Y coordinate.
|
| 54 |
+
device_id: Optional HDC device ID.
|
| 55 |
+
delay: Delay in seconds after tap. If None, uses configured default.
|
| 56 |
+
"""
|
| 57 |
+
if delay is None:
|
| 58 |
+
delay = TIMING_CONFIG.device.default_tap_delay
|
| 59 |
+
|
| 60 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 61 |
+
|
| 62 |
+
# HarmonyOS uses uitest uiInput click
|
| 63 |
+
_run_hdc_command(
|
| 64 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "click", str(x), str(y)],
|
| 65 |
+
capture_output=True
|
| 66 |
+
)
|
| 67 |
+
time.sleep(delay)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def double_tap(
|
| 71 |
+
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
| 72 |
+
) -> None:
|
| 73 |
+
"""
|
| 74 |
+
Double tap at the specified coordinates.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
x: X coordinate.
|
| 78 |
+
y: Y coordinate.
|
| 79 |
+
device_id: Optional HDC device ID.
|
| 80 |
+
delay: Delay in seconds after double tap. If None, uses configured default.
|
| 81 |
+
"""
|
| 82 |
+
if delay is None:
|
| 83 |
+
delay = TIMING_CONFIG.device.default_double_tap_delay
|
| 84 |
+
|
| 85 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 86 |
+
|
| 87 |
+
# HarmonyOS uses uitest uiInput doubleClick
|
| 88 |
+
_run_hdc_command(
|
| 89 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "doubleClick", str(x), str(y)],
|
| 90 |
+
capture_output=True
|
| 91 |
+
)
|
| 92 |
+
time.sleep(delay)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def long_press(
|
| 96 |
+
x: int,
|
| 97 |
+
y: int,
|
| 98 |
+
duration_ms: int = 3000,
|
| 99 |
+
device_id: str | None = None,
|
| 100 |
+
delay: float | None = None,
|
| 101 |
+
) -> None:
|
| 102 |
+
"""
|
| 103 |
+
Long press at the specified coordinates.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
x: X coordinate.
|
| 107 |
+
y: Y coordinate.
|
| 108 |
+
duration_ms: Duration of press in milliseconds (note: HarmonyOS longClick may not support duration).
|
| 109 |
+
device_id: Optional HDC device ID.
|
| 110 |
+
delay: Delay in seconds after long press. If None, uses configured default.
|
| 111 |
+
"""
|
| 112 |
+
if delay is None:
|
| 113 |
+
delay = TIMING_CONFIG.device.default_long_press_delay
|
| 114 |
+
|
| 115 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 116 |
+
|
| 117 |
+
# HarmonyOS uses uitest uiInput longClick
|
| 118 |
+
# Note: longClick may have a fixed duration, duration_ms parameter might not be supported
|
| 119 |
+
_run_hdc_command(
|
| 120 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "longClick", str(x), str(y)],
|
| 121 |
+
capture_output=True,
|
| 122 |
+
)
|
| 123 |
+
time.sleep(delay)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def swipe(
|
| 127 |
+
start_x: int,
|
| 128 |
+
start_y: int,
|
| 129 |
+
end_x: int,
|
| 130 |
+
end_y: int,
|
| 131 |
+
duration_ms: int | None = None,
|
| 132 |
+
device_id: str | None = None,
|
| 133 |
+
delay: float | None = None,
|
| 134 |
+
) -> None:
|
| 135 |
+
"""
|
| 136 |
+
Swipe from start to end coordinates.
|
| 137 |
+
|
| 138 |
+
Args:
|
| 139 |
+
start_x: Starting X coordinate.
|
| 140 |
+
start_y: Starting Y coordinate.
|
| 141 |
+
end_x: Ending X coordinate.
|
| 142 |
+
end_y: Ending Y coordinate.
|
| 143 |
+
duration_ms: Duration of swipe in milliseconds (auto-calculated if None).
|
| 144 |
+
device_id: Optional HDC device ID.
|
| 145 |
+
delay: Delay in seconds after swipe. If None, uses configured default.
|
| 146 |
+
"""
|
| 147 |
+
if delay is None:
|
| 148 |
+
delay = TIMING_CONFIG.device.default_swipe_delay
|
| 149 |
+
|
| 150 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 151 |
+
|
| 152 |
+
if duration_ms is None:
|
| 153 |
+
# Calculate duration based on distance
|
| 154 |
+
dist_sq = (start_x - end_x) ** 2 + (start_y - end_y) ** 2
|
| 155 |
+
duration_ms = int(dist_sq / 1000)
|
| 156 |
+
duration_ms = max(500, min(duration_ms, 1000)) # Clamp between 500-1000ms
|
| 157 |
+
|
| 158 |
+
# HarmonyOS uses uitest uiInput swipe
|
| 159 |
+
# Format: swipe startX startY endX endY duration
|
| 160 |
+
_run_hdc_command(
|
| 161 |
+
hdc_prefix
|
| 162 |
+
+ [
|
| 163 |
+
"shell",
|
| 164 |
+
"uitest",
|
| 165 |
+
"uiInput",
|
| 166 |
+
"swipe",
|
| 167 |
+
str(start_x),
|
| 168 |
+
str(start_y),
|
| 169 |
+
str(end_x),
|
| 170 |
+
str(end_y),
|
| 171 |
+
str(duration_ms),
|
| 172 |
+
],
|
| 173 |
+
capture_output=True,
|
| 174 |
+
)
|
| 175 |
+
time.sleep(delay)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def back(device_id: str | None = None, delay: float | None = None) -> None:
|
| 179 |
+
"""
|
| 180 |
+
Press the back button.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
device_id: Optional HDC device ID.
|
| 184 |
+
delay: Delay in seconds after pressing back. If None, uses configured default.
|
| 185 |
+
"""
|
| 186 |
+
if delay is None:
|
| 187 |
+
delay = TIMING_CONFIG.device.default_back_delay
|
| 188 |
+
|
| 189 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 190 |
+
|
| 191 |
+
# HarmonyOS uses uitest uiInput keyEvent Back
|
| 192 |
+
_run_hdc_command(
|
| 193 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "Back"],
|
| 194 |
+
capture_output=True
|
| 195 |
+
)
|
| 196 |
+
time.sleep(delay)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def home(device_id: str | None = None, delay: float | None = None) -> None:
|
| 200 |
+
"""
|
| 201 |
+
Press the home button.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
device_id: Optional HDC device ID.
|
| 205 |
+
delay: Delay in seconds after pressing home. If None, uses configured default.
|
| 206 |
+
"""
|
| 207 |
+
if delay is None:
|
| 208 |
+
delay = TIMING_CONFIG.device.default_home_delay
|
| 209 |
+
|
| 210 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 211 |
+
|
| 212 |
+
# HarmonyOS uses uitest uiInput keyEvent Home
|
| 213 |
+
_run_hdc_command(
|
| 214 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "Home"],
|
| 215 |
+
capture_output=True
|
| 216 |
+
)
|
| 217 |
+
time.sleep(delay)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def launch_app(
|
| 221 |
+
app_name: str, device_id: str | None = None, delay: float | None = None
|
| 222 |
+
) -> bool:
|
| 223 |
+
"""
|
| 224 |
+
Launch an app by name.
|
| 225 |
+
|
| 226 |
+
Args:
|
| 227 |
+
app_name: The app name (must be in APP_PACKAGES).
|
| 228 |
+
device_id: Optional HDC device ID.
|
| 229 |
+
delay: Delay in seconds after launching. If None, uses configured default.
|
| 230 |
+
|
| 231 |
+
Returns:
|
| 232 |
+
True if app was launched, False if app not found.
|
| 233 |
+
"""
|
| 234 |
+
if delay is None:
|
| 235 |
+
delay = TIMING_CONFIG.device.default_launch_delay
|
| 236 |
+
|
| 237 |
+
if app_name not in APP_PACKAGES:
|
| 238 |
+
print(f"[HDC] App '{app_name}' not found in HarmonyOS app list")
|
| 239 |
+
print(f"[HDC] Available apps: {', '.join(sorted(APP_PACKAGES.keys())[:10])}...")
|
| 240 |
+
return False
|
| 241 |
+
|
| 242 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 243 |
+
bundle = APP_PACKAGES[app_name]
|
| 244 |
+
|
| 245 |
+
# Get the ability name for this bundle
|
| 246 |
+
# Default to "EntryAbility" if not specified in APP_ABILITIES
|
| 247 |
+
ability = APP_ABILITIES.get(bundle, "EntryAbility")
|
| 248 |
+
|
| 249 |
+
# HarmonyOS uses 'aa start' command to launch apps
|
| 250 |
+
# Format: aa start -b {bundle} -a {ability}
|
| 251 |
+
_run_hdc_command(
|
| 252 |
+
hdc_prefix
|
| 253 |
+
+ [
|
| 254 |
+
"shell",
|
| 255 |
+
"aa",
|
| 256 |
+
"start",
|
| 257 |
+
"-b",
|
| 258 |
+
bundle,
|
| 259 |
+
"-a",
|
| 260 |
+
ability,
|
| 261 |
+
],
|
| 262 |
+
capture_output=True,
|
| 263 |
+
)
|
| 264 |
+
time.sleep(delay)
|
| 265 |
+
return True
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _get_hdc_prefix(device_id: str | None) -> list:
|
| 269 |
+
"""Get HDC command prefix with optional device specifier."""
|
| 270 |
+
if device_id:
|
| 271 |
+
return ["hdc", "-t", device_id]
|
| 272 |
+
return ["hdc"]
|
phone_agent/hdc/input.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Input utilities for HarmonyOS device text input."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import subprocess
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
from phone_agent.hdc.connection import _run_hdc_command
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def type_text(text: str, device_id: str | None = None) -> None:
|
| 11 |
+
"""
|
| 12 |
+
Type text into the currently focused input field.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
text: The text to type. Supports multi-line text with newline characters.
|
| 16 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 17 |
+
|
| 18 |
+
Note:
|
| 19 |
+
HarmonyOS uses: hdc shell uitest uiInput text "文本内容"
|
| 20 |
+
This command works without coordinates when input field is focused.
|
| 21 |
+
For multi-line text, the function splits by newlines and sends ENTER keyEvents.
|
| 22 |
+
ENTER key code in HarmonyOS: 2054
|
| 23 |
+
Recommendation: Click on the input field first to focus it, then use this function.
|
| 24 |
+
"""
|
| 25 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 26 |
+
|
| 27 |
+
# Handle multi-line text by splitting on newlines
|
| 28 |
+
if '\n' in text:
|
| 29 |
+
lines = text.split('\n')
|
| 30 |
+
for i, line in enumerate(lines):
|
| 31 |
+
if line: # Only process non-empty lines
|
| 32 |
+
# Escape special characters for shell
|
| 33 |
+
escaped_line = line.replace('"', '\\"').replace("$", "\\$")
|
| 34 |
+
|
| 35 |
+
_run_hdc_command(
|
| 36 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "text", escaped_line],
|
| 37 |
+
capture_output=True,
|
| 38 |
+
text=True,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Send ENTER key event after each line except the last one
|
| 42 |
+
if i < len(lines) - 1:
|
| 43 |
+
try:
|
| 44 |
+
_run_hdc_command(
|
| 45 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2054"],
|
| 46 |
+
capture_output=True,
|
| 47 |
+
text=True,
|
| 48 |
+
)
|
| 49 |
+
except Exception as e:
|
| 50 |
+
print(f"[HDC] ENTER keyEvent failed: {e}")
|
| 51 |
+
else:
|
| 52 |
+
# Single line text - original logic
|
| 53 |
+
# Escape special characters for shell (keep quotes for proper text handling)
|
| 54 |
+
# The text will be wrapped in quotes in the command
|
| 55 |
+
escaped_text = text.replace('"', '\\"').replace("$", "\\$")
|
| 56 |
+
|
| 57 |
+
# HarmonyOS uitest uiInput text command
|
| 58 |
+
# Format: hdc shell uitest uiInput text "文本内容"
|
| 59 |
+
_run_hdc_command(
|
| 60 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "text", escaped_text],
|
| 61 |
+
capture_output=True,
|
| 62 |
+
text=True,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def clear_text(device_id: str | None = None) -> None:
|
| 67 |
+
"""
|
| 68 |
+
Clear text in the currently focused input field.
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 72 |
+
|
| 73 |
+
Note:
|
| 74 |
+
This method uses repeated delete key events to clear text.
|
| 75 |
+
For HarmonyOS, you might also use select all + delete for better efficiency.
|
| 76 |
+
"""
|
| 77 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 78 |
+
# Ctrl+A to select all (key code 2072 for Ctrl, 2017 for A)
|
| 79 |
+
# Then delete
|
| 80 |
+
_run_hdc_command(
|
| 81 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2072", "2017"],
|
| 82 |
+
capture_output=True,
|
| 83 |
+
text=True,
|
| 84 |
+
)
|
| 85 |
+
_run_hdc_command(
|
| 86 |
+
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2055"], # Delete key
|
| 87 |
+
capture_output=True,
|
| 88 |
+
text=True,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def detect_and_set_adb_keyboard(device_id: str | None = None) -> str:
|
| 93 |
+
"""
|
| 94 |
+
Detect current keyboard and switch to ADB Keyboard if available.
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
The original keyboard IME identifier for later restoration.
|
| 101 |
+
|
| 102 |
+
Note:
|
| 103 |
+
This is a placeholder. HarmonyOS may not support ADB Keyboard.
|
| 104 |
+
If there's a similar tool for HarmonyOS, integrate it here.
|
| 105 |
+
"""
|
| 106 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 107 |
+
|
| 108 |
+
# Get current IME (if HarmonyOS supports this)
|
| 109 |
+
try:
|
| 110 |
+
result = _run_hdc_command(
|
| 111 |
+
hdc_prefix + ["shell", "settings", "get", "secure", "default_input_method"],
|
| 112 |
+
capture_output=True,
|
| 113 |
+
text=True,
|
| 114 |
+
)
|
| 115 |
+
current_ime = (result.stdout + result.stderr).strip()
|
| 116 |
+
|
| 117 |
+
# If ADB Keyboard equivalent exists for HarmonyOS, switch to it
|
| 118 |
+
# For now, we'll just return the current IME
|
| 119 |
+
return current_ime
|
| 120 |
+
except Exception:
|
| 121 |
+
return ""
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def restore_keyboard(ime: str, device_id: str | None = None) -> None:
|
| 125 |
+
"""
|
| 126 |
+
Restore the original keyboard IME.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
ime: The IME identifier to restore.
|
| 130 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 131 |
+
"""
|
| 132 |
+
if not ime:
|
| 133 |
+
return
|
| 134 |
+
|
| 135 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
_run_hdc_command(
|
| 139 |
+
hdc_prefix + ["shell", "ime", "set", ime], capture_output=True, text=True
|
| 140 |
+
)
|
| 141 |
+
except Exception:
|
| 142 |
+
pass
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _get_hdc_prefix(device_id: str | None) -> list:
|
| 146 |
+
"""Get HDC command prefix with optional device specifier."""
|
| 147 |
+
if device_id:
|
| 148 |
+
return ["hdc", "-t", device_id]
|
| 149 |
+
return ["hdc"]
|
phone_agent/hdc/screenshot.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Screenshot utilities for capturing HarmonyOS device screen."""
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import os
|
| 5 |
+
import subprocess
|
| 6 |
+
import tempfile
|
| 7 |
+
import uuid
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from io import BytesIO
|
| 10 |
+
from typing import Tuple
|
| 11 |
+
|
| 12 |
+
from PIL import Image
|
| 13 |
+
from phone_agent.hdc.connection import _run_hdc_command
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class Screenshot:
|
| 18 |
+
"""Represents a captured screenshot."""
|
| 19 |
+
|
| 20 |
+
base64_data: str
|
| 21 |
+
width: int
|
| 22 |
+
height: int
|
| 23 |
+
is_sensitive: bool = False
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_screenshot(device_id: str | None = None, timeout: int = 10) -> Screenshot:
|
| 27 |
+
"""
|
| 28 |
+
Capture a screenshot from the connected HarmonyOS device.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
device_id: Optional HDC device ID for multi-device setups.
|
| 32 |
+
timeout: Timeout in seconds for screenshot operations.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Screenshot object containing base64 data and dimensions.
|
| 36 |
+
|
| 37 |
+
Note:
|
| 38 |
+
If the screenshot fails (e.g., on sensitive screens like payment pages),
|
| 39 |
+
a black fallback image is returned with is_sensitive=True.
|
| 40 |
+
"""
|
| 41 |
+
temp_path = os.path.join(tempfile.gettempdir(), f"screenshot_{uuid.uuid4()}.png")
|
| 42 |
+
hdc_prefix = _get_hdc_prefix(device_id)
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Execute screenshot command
|
| 46 |
+
# HarmonyOS HDC only supports JPEG format
|
| 47 |
+
remote_path = "/data/local/tmp/tmp_screenshot.jpeg"
|
| 48 |
+
|
| 49 |
+
# Try method 1: hdc shell screenshot (newer HarmonyOS versions)
|
| 50 |
+
result = _run_hdc_command(
|
| 51 |
+
hdc_prefix + ["shell", "screenshot", remote_path],
|
| 52 |
+
capture_output=True,
|
| 53 |
+
text=True,
|
| 54 |
+
timeout=timeout,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# Check for screenshot failure (sensitive screen)
|
| 58 |
+
output = result.stdout + result.stderr
|
| 59 |
+
if "fail" in output.lower() or "error" in output.lower() or "not found" in output.lower():
|
| 60 |
+
# Try method 2: snapshot_display (older versions or different devices)
|
| 61 |
+
result = _run_hdc_command(
|
| 62 |
+
hdc_prefix + ["shell", "snapshot_display", "-f", remote_path],
|
| 63 |
+
capture_output=True,
|
| 64 |
+
text=True,
|
| 65 |
+
timeout=timeout,
|
| 66 |
+
)
|
| 67 |
+
output = result.stdout + result.stderr
|
| 68 |
+
if "fail" in output.lower() or "error" in output.lower():
|
| 69 |
+
return _create_fallback_screenshot(is_sensitive=True)
|
| 70 |
+
|
| 71 |
+
# Pull screenshot to local temp path
|
| 72 |
+
# Note: remote file is JPEG, but PIL can open it regardless of local extension
|
| 73 |
+
_run_hdc_command(
|
| 74 |
+
hdc_prefix + ["file", "recv", remote_path, temp_path],
|
| 75 |
+
capture_output=True,
|
| 76 |
+
text=True,
|
| 77 |
+
timeout=5,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if not os.path.exists(temp_path):
|
| 81 |
+
return _create_fallback_screenshot(is_sensitive=False)
|
| 82 |
+
|
| 83 |
+
# Read JPEG image and convert to PNG for model inference
|
| 84 |
+
# PIL automatically detects the image format from file content
|
| 85 |
+
img = Image.open(temp_path)
|
| 86 |
+
width, height = img.size
|
| 87 |
+
|
| 88 |
+
buffered = BytesIO()
|
| 89 |
+
img.save(buffered, format="PNG")
|
| 90 |
+
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 91 |
+
|
| 92 |
+
# Cleanup
|
| 93 |
+
os.remove(temp_path)
|
| 94 |
+
|
| 95 |
+
return Screenshot(
|
| 96 |
+
base64_data=base64_data, width=width, height=height, is_sensitive=False
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
print(f"Screenshot error: {e}")
|
| 101 |
+
return _create_fallback_screenshot(is_sensitive=False)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _get_hdc_prefix(device_id: str | None) -> list:
|
| 105 |
+
"""Get HDC command prefix with optional device specifier."""
|
| 106 |
+
if device_id:
|
| 107 |
+
return ["hdc", "-t", device_id]
|
| 108 |
+
return ["hdc"]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _create_fallback_screenshot(is_sensitive: bool) -> Screenshot:
|
| 112 |
+
"""Create a black fallback image when screenshot fails."""
|
| 113 |
+
default_width, default_height = 1080, 2400
|
| 114 |
+
|
| 115 |
+
black_img = Image.new("RGB", (default_width, default_height), color="black")
|
| 116 |
+
buffered = BytesIO()
|
| 117 |
+
black_img.save(buffered, format="PNG")
|
| 118 |
+
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 119 |
+
|
| 120 |
+
return Screenshot(
|
| 121 |
+
base64_data=base64_data,
|
| 122 |
+
width=default_width,
|
| 123 |
+
height=default_height,
|
| 124 |
+
is_sensitive=is_sensitive,
|
| 125 |
+
)
|
phone_agent/model/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model client module for AI inference."""
|
| 2 |
+
|
| 3 |
+
from phone_agent.model.client import ModelClient, ModelConfig
|
| 4 |
+
|
| 5 |
+
__all__ = ["ModelClient", "ModelConfig"]
|
phone_agent/model/client.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model client for AI inference using OpenAI-compatible API."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from openai import OpenAI
|
| 9 |
+
|
| 10 |
+
from phone_agent.config.i18n import get_message
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class ModelConfig:
|
| 15 |
+
"""Configuration for the AI model."""
|
| 16 |
+
|
| 17 |
+
base_url: str = "http://localhost:8000/v1"
|
| 18 |
+
api_key: str = "EMPTY"
|
| 19 |
+
model_name: str = "autoglm-phone-9b"
|
| 20 |
+
max_tokens: int = 3000
|
| 21 |
+
temperature: float = 0.0
|
| 22 |
+
top_p: float = 0.85
|
| 23 |
+
frequency_penalty: float = 0.2
|
| 24 |
+
extra_body: dict[str, Any] = field(default_factory=dict)
|
| 25 |
+
lang: str = "cn" # Language for UI messages: 'cn' or 'en'
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class ModelResponse:
|
| 30 |
+
"""Response from the AI model."""
|
| 31 |
+
|
| 32 |
+
thinking: str
|
| 33 |
+
action: str
|
| 34 |
+
raw_content: str
|
| 35 |
+
# Performance metrics
|
| 36 |
+
time_to_first_token: float | None = None # Time to first token (seconds)
|
| 37 |
+
time_to_thinking_end: float | None = None # Time to thinking end (seconds)
|
| 38 |
+
total_time: float | None = None # Total inference time (seconds)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class ModelClient:
|
| 42 |
+
"""
|
| 43 |
+
Client for interacting with OpenAI-compatible vision-language models.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
config: Model configuration.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, config: ModelConfig | None = None):
|
| 50 |
+
self.config = config or ModelConfig()
|
| 51 |
+
self.client = OpenAI(base_url=self.config.base_url, api_key=self.config.api_key)
|
| 52 |
+
|
| 53 |
+
def request(self, messages: list[dict[str, Any]]) -> ModelResponse:
|
| 54 |
+
"""
|
| 55 |
+
Send a request to the model.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
messages: List of message dictionaries in OpenAI format.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
ModelResponse containing thinking and action.
|
| 62 |
+
|
| 63 |
+
Raises:
|
| 64 |
+
ValueError: If the response cannot be parsed.
|
| 65 |
+
"""
|
| 66 |
+
# Start timing
|
| 67 |
+
start_time = time.time()
|
| 68 |
+
time_to_first_token = None
|
| 69 |
+
time_to_thinking_end = None
|
| 70 |
+
|
| 71 |
+
stream = self.client.chat.completions.create(
|
| 72 |
+
messages=messages,
|
| 73 |
+
model=self.config.model_name,
|
| 74 |
+
max_tokens=self.config.max_tokens,
|
| 75 |
+
temperature=self.config.temperature,
|
| 76 |
+
top_p=self.config.top_p,
|
| 77 |
+
frequency_penalty=self.config.frequency_penalty,
|
| 78 |
+
extra_body=self.config.extra_body,
|
| 79 |
+
stream=True,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
raw_content = ""
|
| 83 |
+
buffer = "" # Buffer to hold content that might be part of a marker
|
| 84 |
+
action_markers = ["finish(message=", "do(action="]
|
| 85 |
+
in_action_phase = False # Track if we've entered the action phase
|
| 86 |
+
first_token_received = False
|
| 87 |
+
|
| 88 |
+
for chunk in stream:
|
| 89 |
+
if len(chunk.choices) == 0:
|
| 90 |
+
continue
|
| 91 |
+
if chunk.choices[0].delta.content is not None:
|
| 92 |
+
content = chunk.choices[0].delta.content
|
| 93 |
+
raw_content += content
|
| 94 |
+
|
| 95 |
+
# Record time to first token
|
| 96 |
+
if not first_token_received:
|
| 97 |
+
time_to_first_token = time.time() - start_time
|
| 98 |
+
first_token_received = True
|
| 99 |
+
|
| 100 |
+
if in_action_phase:
|
| 101 |
+
# Already in action phase, just accumulate content without printing
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
buffer += content
|
| 105 |
+
|
| 106 |
+
# Check if any marker is fully present in buffer
|
| 107 |
+
marker_found = False
|
| 108 |
+
for marker in action_markers:
|
| 109 |
+
if marker in buffer:
|
| 110 |
+
# Marker found, print everything before it
|
| 111 |
+
thinking_part = buffer.split(marker, 1)[0]
|
| 112 |
+
print(thinking_part, end="", flush=True)
|
| 113 |
+
print() # Print newline after thinking is complete
|
| 114 |
+
in_action_phase = True
|
| 115 |
+
marker_found = True
|
| 116 |
+
|
| 117 |
+
# Record time to thinking end
|
| 118 |
+
if time_to_thinking_end is None:
|
| 119 |
+
time_to_thinking_end = time.time() - start_time
|
| 120 |
+
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
if marker_found:
|
| 124 |
+
continue # Continue to collect remaining content
|
| 125 |
+
|
| 126 |
+
# Check if buffer ends with a prefix of any marker
|
| 127 |
+
# If so, don't print yet (wait for more content)
|
| 128 |
+
is_potential_marker = False
|
| 129 |
+
for marker in action_markers:
|
| 130 |
+
for i in range(1, len(marker)):
|
| 131 |
+
if buffer.endswith(marker[:i]):
|
| 132 |
+
is_potential_marker = True
|
| 133 |
+
break
|
| 134 |
+
if is_potential_marker:
|
| 135 |
+
break
|
| 136 |
+
|
| 137 |
+
if not is_potential_marker:
|
| 138 |
+
# Safe to print the buffer
|
| 139 |
+
print(buffer, end="", flush=True)
|
| 140 |
+
buffer = ""
|
| 141 |
+
|
| 142 |
+
# Calculate total time
|
| 143 |
+
total_time = time.time() - start_time
|
| 144 |
+
|
| 145 |
+
# Parse thinking and action from response
|
| 146 |
+
thinking, action = self._parse_response(raw_content)
|
| 147 |
+
|
| 148 |
+
# Print performance metrics
|
| 149 |
+
lang = self.config.lang
|
| 150 |
+
print()
|
| 151 |
+
print("=" * 50)
|
| 152 |
+
print(f"⏱️ {get_message('performance_metrics', lang)}:")
|
| 153 |
+
print("-" * 50)
|
| 154 |
+
if time_to_first_token is not None:
|
| 155 |
+
print(
|
| 156 |
+
f"{get_message('time_to_first_token', lang)}: {time_to_first_token:.3f}s"
|
| 157 |
+
)
|
| 158 |
+
if time_to_thinking_end is not None:
|
| 159 |
+
print(
|
| 160 |
+
f"{get_message('time_to_thinking_end', lang)}: {time_to_thinking_end:.3f}s"
|
| 161 |
+
)
|
| 162 |
+
print(
|
| 163 |
+
f"{get_message('total_inference_time', lang)}: {total_time:.3f}s"
|
| 164 |
+
)
|
| 165 |
+
print("=" * 50)
|
| 166 |
+
|
| 167 |
+
return ModelResponse(
|
| 168 |
+
thinking=thinking,
|
| 169 |
+
action=action,
|
| 170 |
+
raw_content=raw_content,
|
| 171 |
+
time_to_first_token=time_to_first_token,
|
| 172 |
+
time_to_thinking_end=time_to_thinking_end,
|
| 173 |
+
total_time=total_time,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
def _parse_response(self, content: str) -> tuple[str, str]:
|
| 177 |
+
"""
|
| 178 |
+
Parse the model response into thinking and action parts.
|
| 179 |
+
|
| 180 |
+
Parsing rules:
|
| 181 |
+
1. If content contains 'finish(message=', everything before is thinking,
|
| 182 |
+
everything from 'finish(message=' onwards is action.
|
| 183 |
+
2. If rule 1 doesn't apply but content contains 'do(action=',
|
| 184 |
+
everything before is thinking, everything from 'do(action=' onwards is action.
|
| 185 |
+
3. Fallback: If content contains '<answer>', use legacy parsing with XML tags.
|
| 186 |
+
4. Otherwise, return empty thinking and full content as action.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
content: Raw response content.
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
Tuple of (thinking, action).
|
| 193 |
+
"""
|
| 194 |
+
# Rule 1: Check for finish(message=
|
| 195 |
+
if "finish(message=" in content:
|
| 196 |
+
parts = content.split("finish(message=", 1)
|
| 197 |
+
thinking = parts[0].strip()
|
| 198 |
+
action = "finish(message=" + parts[1]
|
| 199 |
+
return thinking, action
|
| 200 |
+
|
| 201 |
+
# Rule 2: Check for do(action=
|
| 202 |
+
if "do(action=" in content:
|
| 203 |
+
parts = content.split("do(action=", 1)
|
| 204 |
+
thinking = parts[0].strip()
|
| 205 |
+
action = "do(action=" + parts[1]
|
| 206 |
+
return thinking, action
|
| 207 |
+
|
| 208 |
+
# Rule 3: Fallback to legacy XML tag parsing
|
| 209 |
+
if "<answer>" in content:
|
| 210 |
+
parts = content.split("<answer>", 1)
|
| 211 |
+
thinking = parts[0].replace("<think>", "").replace("</think>", "").strip()
|
| 212 |
+
action = parts[1].replace("</answer>", "").strip()
|
| 213 |
+
return thinking, action
|
| 214 |
+
|
| 215 |
+
# Rule 4: No markers found, return content as action
|
| 216 |
+
return "", content
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class MessageBuilder:
|
| 220 |
+
"""Helper class for building conversation messages."""
|
| 221 |
+
|
| 222 |
+
@staticmethod
|
| 223 |
+
def create_system_message(content: str) -> dict[str, Any]:
|
| 224 |
+
"""Create a system message."""
|
| 225 |
+
return {"role": "system", "content": content}
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def create_user_message(
|
| 229 |
+
text: str, image_base64: str | None = None
|
| 230 |
+
) -> dict[str, Any]:
|
| 231 |
+
"""
|
| 232 |
+
Create a user message with optional image.
|
| 233 |
+
|
| 234 |
+
Args:
|
| 235 |
+
text: Text content.
|
| 236 |
+
image_base64: Optional base64-encoded image.
|
| 237 |
+
|
| 238 |
+
Returns:
|
| 239 |
+
Message dictionary.
|
| 240 |
+
"""
|
| 241 |
+
content = []
|
| 242 |
+
|
| 243 |
+
if image_base64:
|
| 244 |
+
content.append(
|
| 245 |
+
{
|
| 246 |
+
"type": "image_url",
|
| 247 |
+
"image_url": {"url": f"data:image/png;base64,{image_base64}"},
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
content.append({"type": "text", "text": text})
|
| 252 |
+
|
| 253 |
+
return {"role": "user", "content": content}
|
| 254 |
+
|
| 255 |
+
@staticmethod
|
| 256 |
+
def create_assistant_message(content: str) -> dict[str, Any]:
|
| 257 |
+
"""Create an assistant message."""
|
| 258 |
+
return {"role": "assistant", "content": content}
|
| 259 |
+
|
| 260 |
+
@staticmethod
|
| 261 |
+
def remove_images_from_message(message: dict[str, Any]) -> dict[str, Any]:
|
| 262 |
+
"""
|
| 263 |
+
Remove image content from a message to save context space.
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
message: Message dictionary.
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
Message with images removed.
|
| 270 |
+
"""
|
| 271 |
+
if isinstance(message.get("content"), list):
|
| 272 |
+
message["content"] = [
|
| 273 |
+
item for item in message["content"] if item.get("type") == "text"
|
| 274 |
+
]
|
| 275 |
+
return message
|
| 276 |
+
|
| 277 |
+
@staticmethod
|
| 278 |
+
def build_screen_info(current_app: str, **extra_info) -> str:
|
| 279 |
+
"""
|
| 280 |
+
Build screen info string for the model.
|
| 281 |
+
|
| 282 |
+
Args:
|
| 283 |
+
current_app: Current app name.
|
| 284 |
+
**extra_info: Additional info to include.
|
| 285 |
+
|
| 286 |
+
Returns:
|
| 287 |
+
JSON string with screen info.
|
| 288 |
+
"""
|
| 289 |
+
info = {"current_app": current_app, **extra_info}
|
| 290 |
+
return json.dumps(info, ensure_ascii=False)
|