AbdulElahGwaith commited on
Commit
90fad3d
·
verified ·
1 Parent(s): 2379ca1

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +13 -0
  2. .github/ISSUE_TEMPLATE/bug_report.yaml +72 -0
  3. .github/ISSUE_TEMPLATE/feature-request.yaml +34 -0
  4. .github/PULL_REQUEST_TEMPLATE.md +40 -0
  5. .gitignore +63 -0
  6. .pre-commit-config.yaml +23 -0
  7. LICENSE +201 -0
  8. README.md +980 -0
  9. README_coding_agent.md +430 -0
  10. README_en.md +922 -0
  11. docs/ios_setup/ios_setup.md +134 -0
  12. docs/ios_setup/resources/enable-ui-automation.jpg +0 -0
  13. docs/ios_setup/resources/ios0_WebDriverAgent0.png +3 -0
  14. docs/ios_setup/resources/ios0_WebDriverAgent1.png +3 -0
  15. docs/ios_setup/resources/select-your-iphone-device.png +3 -0
  16. docs/ios_setup/resources/setup-xcode-wda.png +3 -0
  17. docs/ios_setup/resources/start-wda-testing.png +0 -0
  18. docs/ios_setup/resources/trust-dev-app.jpg +3 -0
  19. examples/basic_usage.py +190 -0
  20. examples/demo_thinking.py +64 -0
  21. ios.py +550 -0
  22. main.py +853 -0
  23. phone_agent/__init__.py +12 -0
  24. phone_agent/actions/__init__.py +5 -0
  25. phone_agent/actions/handler.py +399 -0
  26. phone_agent/actions/handler_ios.py +280 -0
  27. phone_agent/adb/__init__.py +51 -0
  28. phone_agent/adb/connection.py +353 -0
  29. phone_agent/adb/device.py +252 -0
  30. phone_agent/adb/input.py +109 -0
  31. phone_agent/adb/screenshot.py +109 -0
  32. phone_agent/agent.py +253 -0
  33. phone_agent/agent_ios.py +277 -0
  34. phone_agent/config/__init__.py +53 -0
  35. phone_agent/config/apps.py +227 -0
  36. phone_agent/config/apps_harmonyos.py +266 -0
  37. phone_agent/config/apps_ios.py +339 -0
  38. phone_agent/config/i18n.py +81 -0
  39. phone_agent/config/prompts.py +75 -0
  40. phone_agent/config/prompts_en.py +79 -0
  41. phone_agent/config/prompts_zh.py +77 -0
  42. phone_agent/config/timing.py +167 -0
  43. phone_agent/device_factory.py +167 -0
  44. phone_agent/hdc/__init__.py +53 -0
  45. phone_agent/hdc/connection.py +381 -0
  46. phone_agent/hdc/device.py +272 -0
  47. phone_agent/hdc/input.py +149 -0
  48. phone_agent/hdc/screenshot.py +125 -0
  49. phone_agent/model/__init__.py +5 -0
  50. 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
+ ![权限](resources/screenshot-20251209-181423.png)
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
+ ![开启无线调试](resources/setting.png)
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
+ ![Permissions](resources/screenshot-20251210-120416.png)
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
+ ![Enable Wireless Debugging](resources/screenshot-20251210-120630.png)
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
+ ![权限](resources/screenshot-20251209-181423.png)
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
+ ![开启无线调试](resources/setting.png)
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
+ ![Permissions](resources/screenshot-20251210-120416.png)
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
+ ![Enable Wireless Debugging](resources/screenshot-20251210-120630.png)
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
+ ![设置签名1](resources/ios0_WebDriverAgent0.png)
34
+
35
+ 5. TARGETS中,建议将WebDriverAgentLib、WebDriverAgentRunner、IntegrationApp的`Signing & Capabilities` 都按照相同方式设置。
36
+ ![设置签名1](resources/ios0_WebDriverAgent1.png)
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
+ ![选择设备](resources/select-your-iphone-device.png)
54
+
55
+ 3. 长按"▶️"运行按钮,选择 "Test" 后开始编译并部署到你的 iPhone 上
56
+
57
+ ![开始测试](resources/start-wda-testing.png)
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
+ ![信任设备](resources/trust-dev-app.jpg)
72
+
73
+ 3. **启用 UI 自动化**
74
+ - 进入:设置 → 开发者
75
+ - 打开 UI 自动化设置
76
+
77
+ ![启用UI自动化](resources/enable-ui-automation.jpg)
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

  • SHA256: 7eaf35b2f17b681134de4320a3adf266098632ed32ca163193770fd5d8ad2e52
  • Pointer size: 132 Bytes
  • Size of remote file: 1.18 MB
docs/ios_setup/resources/ios0_WebDriverAgent1.png ADDED

Git LFS Details

  • SHA256: 45be0a734fbe13fbb0808d07909ea9255a3100a5312d6af45cac8d52253eadf1
  • Pointer size: 131 Bytes
  • Size of remote file: 595 kB
docs/ios_setup/resources/select-your-iphone-device.png ADDED

Git LFS Details

  • SHA256: 9f40747162f089fd0cac6f5242ddcce2e4fed22f62c925cbf7daa171d47718d3
  • Pointer size: 131 Bytes
  • Size of remote file: 170 kB
docs/ios_setup/resources/setup-xcode-wda.png ADDED

Git LFS Details

  • SHA256: 00bfa225596e39b1acc6f594afc69894af19923ec686ae3562423d1f9c31c808
  • Pointer size: 131 Bytes
  • Size of remote file: 134 kB
docs/ios_setup/resources/start-wda-testing.png ADDED
docs/ios_setup/resources/trust-dev-app.jpg ADDED

Git LFS Details

  • SHA256: cdb9bd2ba82a776164893700b1a3dc6570f0e04444c2d428236df7a5898299a3
  • Pointer size: 131 Bytes
  • Size of remote file: 182 kB
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)