ksaa 2025.6.8.0__py3-none-any.whl → 2025.6.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. kotonebot/backend/context/context.py +29 -25
  2. kotonebot/backend/debug/server.py +0 -2
  3. kotonebot/backend/dispatch.py +1 -17
  4. kotonebot/backend/loop.py +277 -0
  5. kotonebot/client/__init__.py +4 -2
  6. kotonebot/client/device.py +178 -25
  7. kotonebot/client/host/__init__.py +2 -1
  8. kotonebot/client/host/adb_common.py +94 -0
  9. kotonebot/client/host/custom.py +33 -4
  10. kotonebot/client/host/leidian_host.py +17 -15
  11. kotonebot/client/host/mumu12_host.py +79 -11
  12. kotonebot/client/host/protocol.py +44 -19
  13. kotonebot/client/host/windows_common.py +55 -0
  14. kotonebot/client/implements/__init__.py +7 -0
  15. kotonebot/client/implements/adb.py +24 -7
  16. kotonebot/client/implements/adb_raw.py +3 -3
  17. kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
  18. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
  19. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
  20. kotonebot/client/implements/remote_windows.py +17 -21
  21. kotonebot/client/implements/uiautomator2.py +5 -8
  22. kotonebot/client/implements/windows.py +28 -36
  23. kotonebot/client/protocol.py +13 -0
  24. kotonebot/client/registration.py +24 -0
  25. kotonebot/config/base_config.py +8 -2
  26. kotonebot/debug_entry.py +11 -4
  27. kotonebot/errors.py +7 -0
  28. kotonebot/kaa/game_ui/common.py +4 -7
  29. kotonebot/kaa/game_ui/dialog.py +24 -4
  30. kotonebot/kaa/game_ui/idols_overview.py +28 -12
  31. kotonebot/kaa/game_ui/primary_button.py +55 -0
  32. kotonebot/kaa/game_ui/scrollable.py +0 -5
  33. kotonebot/kaa/main/cli.py +9 -4
  34. kotonebot/kaa/main/dmm_host.py +15 -8
  35. kotonebot/kaa/main/gr.py +114 -27
  36. kotonebot/kaa/main/kaa.py +140 -41
  37. kotonebot/kaa/metadata.py +47 -0
  38. kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
  39. kotonebot/kaa/resources/game.db +0 -0
  40. kotonebot/kaa/resources/game_ver.txt +0 -0
  41. kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_0.png +0 -0
  42. kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_1.png +0 -0
  43. kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_0.png +0 -0
  44. kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_1.png +0 -0
  45. kotonebot/kaa/sprites/24e99232-9434-457f-a9a0-69dd7ecf675f.png +0 -0
  46. kotonebot/kaa/sprites/2ededcf5-1d80-4e2a-9c83-2a31998331ce.png +0 -0
  47. kotonebot/kaa/sprites/3b473fe6-e147-477f-b088-9b8fb042a4f6.png +0 -0
  48. kotonebot/kaa/sprites/55f7db71-0a18-4b3d-b847-57959b8d2e32.png +0 -0
  49. kotonebot/kaa/sprites/6cd80be8-c9b3-4ba5-bf17-3ffc9b000743.png +0 -0
  50. kotonebot/kaa/sprites/74ec3510-583d-4a76-ac69-38480fbf1387.png +0 -0
  51. kotonebot/kaa/sprites/a0bd6a5f-784d-4f0a-9d66-10f4b80c8d3e.png +0 -0
  52. kotonebot/kaa/sprites/d3424d31-0502-4623-996e-f0194e5085ce.png +0 -0
  53. kotonebot/kaa/sprites/e6b45405-cd9f-4c6e-a9f1-6ec953747c65.png +0 -0
  54. kotonebot/kaa/sprites/f5c16d2f-ebc5-4617-9b96-971696af7c52.png +0 -0
  55. kotonebot/kaa/tasks/R.py +157 -135
  56. kotonebot/kaa/{clear_logs.py → tasks/clear_logs.py} +0 -3
  57. kotonebot/kaa/tasks/produce/in_purodyuusu.py +0 -4
  58. kotonebot/kaa/tasks/produce/non_lesson_actions.py +0 -3
  59. kotonebot/kaa/tasks/produce/produce.py +119 -70
  60. kotonebot/kaa/tasks/start_game.py +7 -6
  61. kotonebot/kaa/util/paths.py +6 -1
  62. kotonebot/tools/mirror.py +23 -13
  63. kotonebot/util.py +32 -0
  64. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/METADATA +4 -1
  65. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/RECORD +201 -181
  66. kotonebot/client/factory.py +0 -92
  67. kotonebot/kaa/sprites/38b8f119-8de1-49bf-8a5f-a44371de4569.png +0 -0
  68. /kotonebot/kaa/sprites/{5ca50448-cc21-499a-a685-c5e1c4240c48.png → 019273c4-22ce-486f-8a7d-dbc78f13dcac.png} +0 -0
  69. /kotonebot/kaa/sprites/{afa0f85f-dcb7-48a1-a1ae-624456b9b65c.png → 03e34ef7-36b5-4ddc-9545-5b70a2eba72e.png} +0 -0
  70. /kotonebot/kaa/sprites/{a00fcf77-2800-4f7d-993d-4636b36045bb.png → 06ea773b-e2b3-40d4-82f6-b119ce9ca00b.png} +0 -0
  71. /kotonebot/kaa/sprites/{d58d9987-b1ca-4669-a9a6-9a0fd1847fa6.png → 0769f8f4-1c47-4bad-a783-bba5e20250bf.png} +0 -0
  72. /kotonebot/kaa/sprites/{82a5e224-a161-43d4-8e15-2946480336dd.png → 08946997-4e9e-4242-95e1-e988fa96b919.png} +0 -0
  73. /kotonebot/kaa/sprites/{fb927b92-7a82-4869-8c61-581d49cd7201.png → 0d60ce10-20ce-4980-8172-b6bac08d6258.png} +0 -0
  74. /kotonebot/kaa/sprites/{b1e88fda-434a-4d06-8891-883a7beafa12.png → 0f40352f-6443-406e-8e2c-c40d5cfc6c59.png} +0 -0
  75. /kotonebot/kaa/sprites/{c4f28132-4aba-40ba-be6c-b1a31d014635.png → 0f6d7f89-bdb2-4235-9b6b-e65cb73a4167.png} +0 -0
  76. /kotonebot/kaa/sprites/{e866d8b1-4f61-45bf-8170-5229f53c047a.png → 14d09c44-3897-4c81-b615-19c35d0f3793.png} +0 -0
  77. /kotonebot/kaa/sprites/{a105d17d-d44c-4798-aeff-70808c91cb47.png → 15191dec-7e33-4721-8068-15c11978e885.png} +0 -0
  78. /kotonebot/kaa/sprites/{869b1771-b2ce-4872-ab75-2157c47c5b13.png → 15f8f31e-65ce-408d-8556-b799bcefc9d4.png} +0 -0
  79. /kotonebot/kaa/sprites/{994aa6a6-0189-4a79-8125-56136973b581.png → 20b01af5-907b-4d49-9f5b-a3700ca9c0c3.png} +0 -0
  80. /kotonebot/kaa/sprites/{92b7d830-3db5-434e-b156-0ac855db480d.png → 242c088b-bc73-4d11-aa31-ee7c71477fb4.png} +0 -0
  81. /kotonebot/kaa/sprites/{9c1fa1d4-2ae5-4093-abaa-cebb51135721.png → 2f2a7951-a30a-4835-b9f6-3205cfdd2f05.png} +0 -0
  82. /kotonebot/kaa/sprites/{a33ae1c3-ac69-4783-977e-9f0f1b1a2f8d.png → 3031397e-3d12-4f23-8377-006eca5d2627.png} +0 -0
  83. /kotonebot/kaa/sprites/{e7022542-4317-4a61-8098-f3f733d8400e.png → 312dbd82-5457-474a-91ce-511f0e4e9c80.png} +0 -0
  84. /kotonebot/kaa/sprites/{cd8d3d55-f102-419e-a6fd-2ad5ecc4fd2e.png → 31851ad1-df39-4a4f-9ae7-19c1d1c6b51a.png} +0 -0
  85. /kotonebot/kaa/sprites/{826bf80a-112e-4356-9111-4b7e692ccc82.png → 338b699a-d556-4b80-a577-123442c3db45.png} +0 -0
  86. /kotonebot/kaa/sprites/{d8136469-6d73-4f82-a79c-fee83c3e93a8.png → 35319890-98e3-4f75-b969-6b8576ef668d.png} +0 -0
  87. /kotonebot/kaa/sprites/{96cf0633-6531-499b-b1cc-6670ab710293.png → 3536f829-70d3-48f9-8fd4-b65eab7502f3.png} +0 -0
  88. /kotonebot/kaa/sprites/{31a52dc8-b7fb-4ff6-a261-eb90a918603c.png → 35516b69-a4f4-4ad4-8af4-35043475ee90.png} +0 -0
  89. /kotonebot/kaa/sprites/{d747239f-7620-4f23-9ac7-cf2d34889266.png → 36d71d33-09c9-4be8-b184-ccf40f700333.png} +0 -0
  90. /kotonebot/kaa/sprites/{b44f5e5b-e999-4427-8a9f-f76c6485f4a5.png → 379d5084-637f-45e8-8bd8-789eaa919821.png} +0 -0
  91. /kotonebot/kaa/sprites/{0a430873-8c12-4ce2-a5de-ab9790c08344.png → 3851303a-e3a0-46eb-aa7e-b47fb41946f7.png} +0 -0
  92. /kotonebot/kaa/sprites/{ff77d221-8444-4d0f-8c93-0223a97e2a98.png → 3d1aa368-e198-478f-a47f-49818efbaec8.png} +0 -0
  93. /kotonebot/kaa/sprites/{333fe6d3-0be7-40fb-a2a3-364f98758f2a.png → 3f5907a9-7fe4-4f46-8e46-a96ac7fa3923.png} +0 -0
  94. /kotonebot/kaa/sprites/{0decaa6a-b9a9-4d9b-829a-0fc1f67538b4.png → 40c0f296-d29e-4e05-aaff-a212339b742e.png} +0 -0
  95. /kotonebot/kaa/sprites/{c1ad996a-925e-41b5-b26c-d48b8ded21d2.png → 43e72796-a644-4d9b-a817-8add011d9a75.png} +0 -0
  96. /kotonebot/kaa/sprites/{e2fae2a4-bb19-4f49-b382-47e504ec26d3.png → 469c2b41-4171-4902-acc5-da0c807886ab.png} +0 -0
  97. /kotonebot/kaa/sprites/{559df9b9-e74d-45af-a300-77d3424e69c6.png → 497e7930-f423-457a-8168-b1a1532bb1e9.png} +0 -0
  98. /kotonebot/kaa/sprites/{e41bb5df-164d-4760-9db6-d5d5d763bb5f.png → 49a43af3-17e3-4434-ba4d-519775ccaea9.png} +0 -0
  99. /kotonebot/kaa/sprites/{f9b2f64d-f829-4b13-9704-c5f00fb956be.png → 49b9a298-e847-4322-99a2-c0588c3542fe.png} +0 -0
  100. /kotonebot/kaa/sprites/{645744c4-3bbf-4ea2-8f46-4725fff874b3.png → 4a1902aa-01db-46bb-bea5-88ac62c82562.png} +0 -0
  101. /kotonebot/kaa/sprites/{c0fc7de5-b842-4c70-9cf2-89d833bfe7a6.png → 4a96a728-6303-4ae3-8e0c-9f1e8e5f86a7.png} +0 -0
  102. /kotonebot/kaa/sprites/{73f4204e-8281-47cc-9dce-6bb8280c4ce5.png → 4b7c4221-a679-47f6-929d-0ef514894b27.png} +0 -0
  103. /kotonebot/kaa/sprites/{c1b1125f-542b-4e63-9c86-5d2e35a89a10.png → 4d355e36-9169-43e4-bd58-2836ee3e56e0.png} +0 -0
  104. /kotonebot/kaa/sprites/{ad8ee599-88a8-429c-8982-99086e552dcc.png → 4eb97d68-cd47-4100-9c3b-35b5c86b4bdf.png} +0 -0
  105. /kotonebot/kaa/sprites/{7a479871-547e-4098-9cdd-6c9bb9f7449a.png → 515a2ff0-36b8-4f2f-9902-e6c1b9796586.png} +0 -0
  106. /kotonebot/kaa/sprites/{02e0aa0d-4094-4bdc-8625-1a063733615b.png → 51a3ed29-92b9-4429-9e50-783d1b8dc6a7.png} +0 -0
  107. /kotonebot/kaa/sprites/{22d5a7d5-59a0-4137-b02e-1a7af86d4250.png → 52d3438b-afae-4c7a-abe5-7ed530c7558c.png} +0 -0
  108. /kotonebot/kaa/sprites/{5bf00cba-9aab-4ffe-b20b-8f5e74279276.png → 540ee3b5-ca8f-4091-80a6-ce774d95e5f7.png} +0 -0
  109. /kotonebot/kaa/sprites/{05ff5efd-0f76-47ce-bc89-237266bc17c0.png → 54a15298-ecaf-4cc5-b8a2-5b0d8f8add41.png} +0 -0
  110. /kotonebot/kaa/sprites/{1f9165e6-77ee-403e-9b76-99c114527dbb.png → 554c0ff6-0a05-419d-85f9-5ced260d0d2e.png} +0 -0
  111. /kotonebot/kaa/sprites/{79bc56c5-eae6-4389-9116-4a9e182d6bf3.png → 55e4ef75-ba8e-4230-819c-450cf14b08cd.png} +0 -0
  112. /kotonebot/kaa/sprites/{cd4cbd7b-df80-45bb-a053-cc333a806934.png → 5824ef5f-6776-402c-b78c-ae79cc4cc878.png} +0 -0
  113. /kotonebot/kaa/sprites/{804a4b05-1ca3-4915-bf23-a66ccdc34404.png → 5a393483-c631-47fd-805c-c460c23b8f9e.png} +0 -0
  114. /kotonebot/kaa/sprites/{e5d15c50-45ee-4de9-8b97-4630cbd8f01a.png → 5b75cae4-a853-402c-acec-edbfef94342a.png} +0 -0
  115. /kotonebot/kaa/sprites/{1a3d18a4-7d91-4182-9e40-e60d73fed365.png → 5c9ae62c-488f-4109-a4d2-d2385c32043d.png} +0 -0
  116. /kotonebot/kaa/sprites/{7344e180-251b-465a-a8cf-974477f7ad44.png → 5cdd71be-c8cd-4310-b6f4-5e97ddbeb46e.png} +0 -0
  117. /kotonebot/kaa/sprites/{67a81c23-4ce4-45e3-bfa8-1136fb14b831.png → 5d5cb13b-c1fd-4808-b352-b21291a9ff0b.png} +0 -0
  118. /kotonebot/kaa/sprites/{4e21f02d-d5e1-49eb-946a-8edc74d0f1f7.png → 5d6c4462-3d7c-42eb-ab3e-195ebf845a09.png} +0 -0
  119. /kotonebot/kaa/sprites/{d7f604a6-c663-44db-b1d3-c9792eb3daa4.png → 5d94e628-82ff-4531-b70a-b13297491bea.png} +0 -0
  120. /kotonebot/kaa/sprites/{74521b6f-de5e-4ba8-a3c9-c6ba01070498.png → 5dc2dca3-48bd-4eee-af89-1a1d30593bd5.png} +0 -0
  121. /kotonebot/kaa/sprites/{b604e25d-680a-4e02-a012-2e593b5bc276.png → 5f0c2a57-efd0-4094-9f71-6f6fe97316f6.png} +0 -0
  122. /kotonebot/kaa/sprites/{fd610de1-b023-4747-8c1b-72f89b1c5647.png → 610348f2-6262-4cd1-8fc3-e7470f85580c.png} +0 -0
  123. /kotonebot/kaa/sprites/{5be4f6c7-2baa-42f3-a5a5-99ca0830cc30.png → 613e9b10-b2eb-44ea-b6d9-9310c0e1bdcf.png} +0 -0
  124. /kotonebot/kaa/sprites/{72c1804f-db8c-40fa-bc79-6533f2861a5d.png → 618b21ad-ab8b-436a-8eb8-bd320a198c9f.png} +0 -0
  125. /kotonebot/kaa/sprites/{572077d4-7ee2-449a-b4f0-66239b0d7aff.png → 61a09df6-7443-436c-9ef0-1d5a1cae1509.png} +0 -0
  126. /kotonebot/kaa/sprites/{321b54c8-c69b-4437-931d-655882dabeed.png → 64be274e-e8ad-4938-84c1-5d8aabcd10d8.png} +0 -0
  127. /kotonebot/kaa/sprites/{22ee3f1b-ddcb-48bf-85dd-8423f85e3199.png → 671b4a55-023a-434d-b29a-e844fe528e6b.png} +0 -0
  128. /kotonebot/kaa/sprites/{668002b6-805b-467b-a703-203fb75444f4.png → 6e5e9cf9-80b0-4241-a222-ff50dbd89a0c.png} +0 -0
  129. /kotonebot/kaa/sprites/{4bd66b38-78d3-4a27-816b-d315039a8bb6.png → 6ef59649-bbb3-42b6-a6b0-eac4f35a3da4.png} +0 -0
  130. /kotonebot/kaa/sprites/{7b3c6860-fbbe-48aa-aadf-8062fa88f1fe.png → 75a2093d-8743-427e-8d24-ae9a69fa14c1.png} +0 -0
  131. /kotonebot/kaa/sprites/{371a3192-101c-45ef-9da4-2ed389770c0f.png → 76bde19a-4b1b-4e52-86d9-f56be7529c71.png} +0 -0
  132. /kotonebot/kaa/sprites/{ef39804e-8d2d-40f6-8d13-bd2877d1353a.png → 77d06f4d-8214-4306-9dc5-035c720f7256.png} +0 -0
  133. /kotonebot/kaa/sprites/{24cd42fd-bb3b-4fc6-b40d-d3e3e36fe9f1.png → 78b642d3-9a2c-4c17-8872-3cc78211f4a0.png} +0 -0
  134. /kotonebot/kaa/sprites/{0bab7080-9a37-4c27-9821-8df36d49b678.png → 7a9d1c39-b306-4cb0-be2f-7a5963099949.png} +0 -0
  135. /kotonebot/kaa/sprites/{b1d1dfab-51f5-4ae9-b718-cdf46774c0ff.png → 7ddda828-c8b0-4331-864f-99b85b7c5557.png} +0 -0
  136. /kotonebot/kaa/sprites/{5edef075-4843-4dc9-840f-ae445cbe0730.png → 81399fd9-f254-4d0e-9775-ad451c237a4e.png} +0 -0
  137. /kotonebot/kaa/sprites/{705d9ecf-faf8-40f4-b701-637cec822c1e.png → 81526e97-1b8d-433f-a5c9-907e4e8c85cd.png} +0 -0
  138. /kotonebot/kaa/sprites/{ecd44a4a-e1ca-4a86-ae0d-2306a672613f.png → 818e2370-8fb9-4e6c-ab78-58e86d6525b5.png} +0 -0
  139. /kotonebot/kaa/sprites/{4f58031c-36b9-4baf-bccf-11c05d83bd19.png → 8431cc52-0041-4425-808c-dcd4d196506c.png} +0 -0
  140. /kotonebot/kaa/sprites/{13535ecd-bdf5-4ff7-aa2c-6ecb20fe9c06.png → 85a49ff3-6075-4797-8b7f-35851a843698.png} +0 -0
  141. /kotonebot/kaa/sprites/{d22ca9e0-001d-43d6-a557-1c76c5a27c81.png → 86062e91-d6c1-44cf-9f8c-26a8b9dd8686.png} +0 -0
  142. /kotonebot/kaa/sprites/{0bbfb5b0-f0f7-4ad9-86f7-a2adb01a6371.png → 860c3d98-e263-482f-95fb-4fc9c472e8b8.png} +0 -0
  143. /kotonebot/kaa/sprites/{ed5d2bf3-405c-4165-88d0-9f8a4be968c6.png → 8669ce44-dec7-47c2-aafd-3f2fbfd1d083.png} +0 -0
  144. /kotonebot/kaa/sprites/{f0786a26-81b5-4e76-b577-db70ab3fab50.png → 86b15801-c932-4034-bed8-fd76b0f65916.png} +0 -0
  145. /kotonebot/kaa/sprites/{80fb515c-4323-422d-a24c-80a3bf702f1d.png → 893b015d-6894-4e74-880e-d22859a25662.png} +0 -0
  146. /kotonebot/kaa/sprites/{fdfecda8-2fe0-487e-b5fe-6358274bb9ff.png → 8c0fa5fa-cebb-4ae7-bd1c-b542c9cbf694.png} +0 -0
  147. /kotonebot/kaa/sprites/{73af305f-840f-4325-abae-b3519fba672a.png → 913a1d17-54fe-4c8f-ac50-ef7f39600b86.png} +0 -0
  148. /kotonebot/kaa/sprites/{c2667677-cbff-4dd4-a401-e0963e7224f4.png → 964c6b90-f526-4199-9523-d8124373a56b.png} +0 -0
  149. /kotonebot/kaa/sprites/{db94ffb7-a09d-4a51-b420-b930851c9db4.png → 97c281a9-ebb0-4f54-b644-b7ed7dd65f81.png} +0 -0
  150. /kotonebot/kaa/sprites/{48c257f8-3583-49f7-b01a-ba7d251086ac.png → 998fc84a-af71-4fa5-8aa4-e0bfeb9c53ea.png} +0 -0
  151. /kotonebot/kaa/sprites/{469006bc-a011-46e4-bcde-992be888456e.png → 9cbf7f6e-fe77-463b-9c60-495dc823510a.png} +0 -0
  152. /kotonebot/kaa/sprites/{4d6c198a-0e34-4c80-86d1-7789b2082d64.png → 9ce7d48e-295d-47b2-96eb-f3ce697c1b37.png} +0 -0
  153. /kotonebot/kaa/sprites/{a3c12040-2b11-4038-b830-215fb7b6f359.png → 9dcca89b-c92b-4708-8f19-facd40d09a65.png} +0 -0
  154. /kotonebot/kaa/sprites/{fbd816df-7ae6-40a4-b4d4-5708f5ba4a7c.png → 9e369992-ea8c-4c15-88d3-1344319978b0.png} +0 -0
  155. /kotonebot/kaa/sprites/{54bb5fcf-be61-4af8-bb19-a3c5113cf9e1.png → 9f9003ba-468f-406f-a82f-1b7a23ef347c.png} +0 -0
  156. /kotonebot/kaa/sprites/{309105d4-073a-47c9-8c5c-d593aa497723.png → a2bf4e11-fbc3-4172-bc0b-b16f8f59e68c.png} +0 -0
  157. /kotonebot/kaa/sprites/{111a03e6-e401-4ad5-b5dc-d5124e8f1ab0.png → a546e254-0490-43df-a291-a9a5c2b4c34a.png} +0 -0
  158. /kotonebot/kaa/sprites/{7736a171-9221-4cda-8288-908997abeff2.png → aacd229c-d307-4a20-a388-84d8f4afb4a5.png} +0 -0
  159. /kotonebot/kaa/sprites/{1bfce48a-b151-406e-a6f5-863cb68cbbf2.png → b003bef5-53db-4032-b1bd-d5075de365a8.png} +0 -0
  160. /kotonebot/kaa/sprites/{b2f7a430-5777-4861-8435-5419394e5ee8.png → b641a122-4734-43e1-b22f-468464a0cff1.png} +0 -0
  161. /kotonebot/kaa/sprites/{a82e06fb-a02e-4be5-a7b4-4d2758914fcf.png → bc7303f3-43e9-4ece-9206-5f9c779b813b.png} +0 -0
  162. /kotonebot/kaa/sprites/{f74fc514-61f1-4068-bdf4-754fcaf8ac89.png → bd7a6754-fffc-4315-987f-5ba32d95b697.png} +0 -0
  163. /kotonebot/kaa/sprites/{c7aa3f89-8a5e-4685-9297-4fc892bfdd81.png → c39f2006-442b-4ab7-99bb-3dfa0673f05d.png} +0 -0
  164. /kotonebot/kaa/sprites/{6bf5556a-8f4e-41d7-8ab5-d683ad1b47a8.png → c70f4634-6821-4480-a615-b5f9cdc578c1.png} +0 -0
  165. /kotonebot/kaa/sprites/{89c0e675-3fd4-456a-8ae7-3bfb248b7281.png → c774098e-d782-455c-80a9-5bad3713129d.png} +0 -0
  166. /kotonebot/kaa/sprites/{09433024-3dfe-4fcb-9d67-cf442b58f31b.png → c7e3736a-0e0f-474a-9386-f58292a45b92.png} +0 -0
  167. /kotonebot/kaa/sprites/{48d2f007-8c1b-43fc-bd3f-f0b10d2a0c88.png → c90b582c-8570-48b4-98c0-62d5b067ca2c.png} +0 -0
  168. /kotonebot/kaa/sprites/{9bb1a400-935a-4c5e-838b-fca502c49aba.png → c9d3699b-c8b1-4294-9b48-103df4ab6c65.png} +0 -0
  169. /kotonebot/kaa/sprites/{157dd58d-e319-4e0c-a37a-638e1a31c60b.png → cc9483c3-c895-4dcc-b3f2-0934ba1ad42e.png} +0 -0
  170. /kotonebot/kaa/sprites/{43d65b3f-bbae-4e29-a7bf-539379c1b072.png → cec49286-3d48-4fe0-8911-6a1b17d1ac20.png} +0 -0
  171. /kotonebot/kaa/sprites/{56a8c0ca-4659-4de2-9bce-55ce86a40d75.png → cef78712-d9ae-4d6e-8d7d-d047d48fbb98.png} +0 -0
  172. /kotonebot/kaa/sprites/{12e8d3f1-494a-46f5-908f-f621a0c57ec6.png → d06fd859-4678-4593-b096-c86b38810cb7.png} +0 -0
  173. /kotonebot/kaa/sprites/{ba955365-1328-4018-83a2-bfe818f8f172.png → d19c37ff-d5c1-4316-8424-e69ccb5c2523.png} +0 -0
  174. /kotonebot/kaa/sprites/{695f8d57-b2c9-4a6d-9ae9-1b49ee3eb22a.png → d26b60f7-d15c-4563-bedc-dd6e7e4078b2.png} +0 -0
  175. /kotonebot/kaa/sprites/{910d307b-6461-4950-badb-509975d0b797.png → d26bf52c-cb1b-4ebf-9c03-ff3f2d245959.png} +0 -0
  176. /kotonebot/kaa/sprites/{7e8164f6-a9e4-4600-b776-77b35a490933.png → d5fd6c73-4ad8-47b5-89be-c7b137a9950f.png} +0 -0
  177. /kotonebot/kaa/sprites/{05bfd235-2077-479a-8c43-11fb49316000.png → d696062b-9b58-4af1-835e-2cf5a0a0fbeb.png} +0 -0
  178. /kotonebot/kaa/sprites/{03458cf9-8ab0-4908-9a56-9c5c7e06bc9b.png → da7a329e-339e-4ee7-8775-bca505a1980d.png} +0 -0
  179. /kotonebot/kaa/sprites/{2f81e70a-f2af-420f-9b6b-6338c391911f.png → dedb8499-5881-4add-914f-ff0049285dae.png} +0 -0
  180. /kotonebot/kaa/sprites/{b3d1f8d4-1393-4206-b78e-a5495d8fb930.png → dedea21b-f21d-4621-a213-70ad03faec58.png} +0 -0
  181. /kotonebot/kaa/sprites/{f65a4495-f044-44de-bf0d-c869e637d5d3.png → dff74cce-ba2d-44aa-b924-183259080a32.png} +0 -0
  182. /kotonebot/kaa/sprites/{b3e72de2-e103-49a3-b0f2-8593cff87c4e.png → e0246b0d-f0cf-49d4-8423-7da46e75a575.png} +0 -0
  183. /kotonebot/kaa/sprites/{624555b9-31e3-46a3-8078-55619eaa40b9.png → e1b6b32b-c797-4f1d-aa2b-e7320083a9a6.png} +0 -0
  184. /kotonebot/kaa/sprites/{90a9c819-ee6b-4b3e-a0e4-ce484c6e413d.png → e23e3f19-03f6-4507-9128-115191272b90.png} +0 -0
  185. /kotonebot/kaa/sprites/{cc829886-23e8-4d47-b4c4-37ca20381180.png → e2eb3bae-6451-40b9-bbff-90f11dca1904.png} +0 -0
  186. /kotonebot/kaa/sprites/{6a7d061e-682b-42c1-97d5-121c52921e8b.png → e46e1b6d-6702-46bf-a07e-aec8d183453f.png} +0 -0
  187. /kotonebot/kaa/sprites/{de709296-e411-465f-86fc-5daf5ca83cef.png → e5297036-9934-44a4-a761-a6a49ae06e3e.png} +0 -0
  188. /kotonebot/kaa/sprites/{52620946-b92c-42c7-9fcc-0ff13388fd0d.png → e7da5988-0e83-44ad-888b-246365473f9e.png} +0 -0
  189. /kotonebot/kaa/sprites/{05f8f996-a38a-45d9-8ff9-720b6c4fd599.png → eb8d208a-dbcc-43ca-a8ac-098e1005ebfa.png} +0 -0
  190. /kotonebot/kaa/sprites/{8e8a0b57-0231-4ed6-958e-3196f7c9ca7b.png → ebe4ce64-98a9-40f2-9fde-3d5cdae673c7.png} +0 -0
  191. /kotonebot/kaa/sprites/{4a235eaa-46f1-4f71-b54d-ada0ade44572.png → ef2ad3df-3f89-4a48-8fc3-c70a8adc1044.png} +0 -0
  192. /kotonebot/kaa/sprites/{f8cbb089-a63e-4e40-9238-26d14da2c459.png → f05e410d-15b8-4831-bcc1-4cd8af07c882.png} +0 -0
  193. /kotonebot/kaa/sprites/{550fc45d-22c0-4a7c-a324-2c4ff107ab39.png → f1fac13a-84f3-42bd-9b52-e0ba90bc5255.png} +0 -0
  194. /kotonebot/kaa/sprites/{ad784a29-f0e2-4037-976b-45143db8441c.png → f7c34ab6-c165-48b7-ad5a-0b60613ce1bc.png} +0 -0
  195. /kotonebot/kaa/sprites/{0439399f-5fca-43f9-a105-99342ff04d2e.png → fa80b14e-953f-44bd-a4ad-99aefd276fd7.png} +0 -0
  196. /kotonebot/kaa/sprites/{7cea265a-fca5-43d0-b2da-067c8fff4d72.png → fe35655e-701b-4081-8df8-c17a7252acef.png} +0 -0
  197. /kotonebot/kaa/sprites/{da3e7cee-d77b-4e62-b703-ed257706dd39.png → fe853ac7-934e-4b4a-bcc7-b986e057dfcf.png} +0 -0
  198. /kotonebot/kaa/sprites/{1f8f8c46-43ec-4449-be9f-2e96da11da1e.png → ffda716c-2903-4ec7-8c59-aab5b8ad10bb.png} +0 -0
  199. /kotonebot/kaa/sprites/{4c72f39e-1182-4479-867f-8207744ee4fd.png → fff33200-c947-49f2-ab86-3ac98937fa36.png} +0 -0
  200. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/WHEEL +0 -0
  201. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/entry_points.txt +0 -0
  202. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/licenses/LICENSE +0 -0
  203. {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/top_level.txt +0 -0
@@ -9,9 +9,10 @@ from cv2.typing import MatLike
9
9
  from adbutils._device import AdbDevice as AdbUtilsDevice
10
10
 
11
11
  from ..backend.debug import result
12
+ from ..errors import UnscalableResolutionError
12
13
  from kotonebot.backend.core import HintBox
13
14
  from kotonebot.primitives import Rect, Point, is_point
14
- from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable
15
+ from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable, AndroidCommandable, WindowsCommandable
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -71,7 +72,6 @@ class Device:
71
72
  横屏时为 'landscape',竖屏时为 'portrait'。
72
73
  """
73
74
 
74
- self._command: Commandable
75
75
  self._touch: Touchable
76
76
  self._screenshot: Screenshotable
77
77
 
@@ -79,6 +79,31 @@ class Device:
79
79
  """
80
80
  设备平台名称。
81
81
  """
82
+ self.target_resolution: tuple[int, int] | None = None
83
+ """
84
+ 目标分辨率。
85
+
86
+ 若设置,则在截图、点击、滑动等时会缩放到目标分辨率。
87
+ 仅支持等比例缩放,若无法等比例缩放,则会抛出异常 `UnscalableResolutionError`。
88
+ """
89
+ self.match_rotation: bool = True
90
+ """
91
+ 分辨率缩放是否自动匹配旋转。
92
+
93
+ 当目标与真实分辨率的宽高比不一致时,是否允许通过旋转(交换宽高)后再进行匹配。
94
+ 为 True 则忽略方向差异,只要宽高比一致就视为可缩放;False 则必须匹配旋转。
95
+
96
+ 例如,当目标分辨率为 1920x1080,而真实分辨率为 1080x1920 时,
97
+ ``match_rotation`` 为 True 则认为可以缩放,为 False 则会抛出异常。
98
+ """
99
+ self.aspect_ratio_tolerance: float = 0.1
100
+ """
101
+ 宽高比容差阈值。
102
+
103
+ 判断两分辨率宽高比差异是否接受的阈值。
104
+ 该值越小,对比例一致性的要求越严格。
105
+ 默认为 0.1(即 10% 容差)。
106
+ """
82
107
 
83
108
  @property
84
109
  def adb(self) -> AdbUtilsDevice:
@@ -90,12 +115,50 @@ class Device:
90
115
  def adb(self, value: AdbUtilsDevice) -> None:
91
116
  self._adb = value
92
117
 
93
- def launch_app(self, package_name: str) -> None:
94
- """
95
- 根据包名启动 app
96
- """
97
- self._command.launch_app(package_name)
98
-
118
+ def _scale_pos_real_to_target(self, real_x: int, real_y: int) -> tuple[int, int]:
119
+ """将真实屏幕坐标缩放到目标逻辑坐标"""
120
+ if self.target_resolution is None:
121
+ return real_x, real_y
122
+
123
+ real_w, real_h = self.screen_size
124
+ target_w, target_h = self.target_resolution
125
+
126
+ # 校验分辨率是否可缩放并获取调整后的目标分辨率
127
+ adjusted_target_w, adjusted_target_h = self.__assert_scalable((real_w, real_h), (target_w, target_h))
128
+
129
+ scale_w = adjusted_target_w / real_w
130
+ scale_h = adjusted_target_h / real_h
131
+
132
+ return int(real_x * scale_w), int(real_y * scale_h)
133
+
134
+ def _scale_pos_target_to_real(self, target_x: int, target_y: int) -> tuple[int, int]:
135
+ """将目标逻辑坐标缩放到真实屏幕坐标"""
136
+ if self.target_resolution is None:
137
+ return target_x, target_y # 输入坐标已是真实坐标
138
+
139
+ real_w, real_h = self.screen_size
140
+ target_w, target_h = self.target_resolution
141
+
142
+ # 校验分辨率是否可缩放并获取调整后的目标分辨率
143
+ adjusted_target_w, adjusted_target_h = self.__assert_scalable((real_w, real_h), (target_w, target_h))
144
+
145
+ scale_to_real_w = real_w / adjusted_target_w
146
+ scale_to_real_h = real_h / adjusted_target_h
147
+
148
+ return int(target_x * scale_to_real_w), int(target_y * scale_to_real_h)
149
+
150
+ def __scale_image (self, img: MatLike) -> MatLike:
151
+ if self.target_resolution is None:
152
+ return img
153
+
154
+ target_w, target_h = self.target_resolution
155
+ h, w = img.shape[:2]
156
+
157
+ # 校验分辨率是否可缩放并获取调整后的目标分辨率
158
+ adjusted_target = self.__assert_scalable((w, h), (target_w, target_h))
159
+
160
+ return cv2.resize(img, adjusted_target)
161
+
99
162
  @overload
100
163
  def click(self) -> None:
101
164
  """
@@ -168,7 +231,12 @@ class Device:
168
231
  logger.debug(f"Executing click hook before: ({x}, {y})")
169
232
  x, y = hook(x, y)
170
233
  logger.debug(f"Click hook before result: ({x}, {y})")
171
- logger.debug(f"Click: {x}, {y}")
234
+ if self.target_resolution is not None:
235
+ # 输入坐标为逻辑坐标,需要转换为真实坐标
236
+ real_x, real_y = self._scale_pos_target_to_real(x, y)
237
+ else:
238
+ real_x, real_y = x, y
239
+ logger.debug(f"Click: {x}, {y}%s", f"(Physical: {real_x}, {real_y})" if self.target_resolution is not None else "")
172
240
  from ..backend.context import ContextStackVars
173
241
  if ContextStackVars.current() is not None:
174
242
  image = ContextStackVars.ensure_current()._screenshot
@@ -176,9 +244,11 @@ class Device:
176
244
  image = np.array([])
177
245
  if image is not None and image.size > 0:
178
246
  cv2.circle(image, (x, y), 10, (0, 0, 255), -1)
179
- message = f"point: ({x}, {y})"
247
+ message = f"Point: ({x}, {y})"
248
+ if self.target_resolution is not None:
249
+ message += f" physical: ({real_x}, {real_y})"
180
250
  result("device.click", image, message)
181
- self._touch.click(x, y)
251
+ self._touch.click(real_x, real_y)
182
252
 
183
253
  def __click_point_tuple(self, point: Point) -> None:
184
254
  self.click(point[0], point[1])
@@ -239,6 +309,10 @@ class Device:
239
309
  """
240
310
  滑动屏幕
241
311
  """
312
+ if self.target_resolution is not None:
313
+ # 输入坐标为逻辑坐标,需要转换为真实坐标
314
+ x1, y1 = self._scale_pos_target_to_real(x1, y1)
315
+ x2, y2 = self._scale_pos_target_to_real(x2, y2)
242
316
  self._touch.swipe(x1, y1, x2, y2, duration)
243
317
 
244
318
  def swipe_scaled(self, x1: float, y1: float, x2: float, y2: float, duration: float|None = None) -> None:
@@ -265,6 +339,7 @@ class Device:
265
339
  logger.debug("screenshot hook before returned image")
266
340
  return img
267
341
  img = self.screenshot_raw()
342
+ img = self.__scale_image(img)
268
343
  if self.screenshot_hook_after is not None:
269
344
  img = self.screenshot_hook_after(img)
270
345
  return img
@@ -303,19 +378,15 @@ class Device:
303
378
  `self.orientation` 属性默认为竖屏。如果需要自动检测,
304
379
  调用 `self.detect_orientation()` 方法。
305
380
  如果已知方向,也可以直接设置 `self.orientation` 属性。
381
+
382
+ 即使设置了 `self.target_resolution`,返回的分辨率仍然是真实分辨率。
306
383
  """
307
- return self._screenshot.screen_size
308
-
309
- def current_package(self) -> str | None:
310
- """
311
- 获取前台 APP 的包名。
312
-
313
- :return: 前台 APP 的包名。如果获取失败,则返回 None。
314
- :exception: 如果设备不支持此功能,则抛出 NotImplementedError。
315
- """
316
- ret = self._command.current_package()
317
- logger.debug("current_package: %s", ret)
318
- return ret
384
+ size = self._screenshot.screen_size
385
+ if self.orientation == 'landscape':
386
+ size = sorted(size, reverse=True)
387
+ else:
388
+ size = sorted(size, reverse=False)
389
+ return size[0], size[1]
319
390
 
320
391
  def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
321
392
  """
@@ -325,15 +396,97 @@ class Device:
325
396
  """
326
397
  return self._screenshot.detect_orientation()
327
398
 
399
+ def __aspect_ratio_compatible(self, src_size: tuple[int, int], tgt_size: tuple[int, int]) -> bool:
400
+ """
401
+ 判断两个尺寸在宽高比意义上是否兼容
402
+
403
+ 若 ``self.match_rotation`` 为 True,忽略方向(长边/短边)进行比较。
404
+ 判断标准由 ``self.aspect_ratio_tolerance`` 决定(默认 0.1)。
405
+ """
406
+ src_w, src_h = src_size
407
+ tgt_w, tgt_h = tgt_size
408
+
409
+ # 尺寸必须为正
410
+ if src_w <= 0 or src_h <= 0:
411
+ raise ValueError(f"Source size dimensions must be positive for scaling: {src_size}")
412
+ if tgt_w <= 0 or tgt_h <= 0:
413
+ raise ValueError(f"Target size dimensions must be positive for scaling: {tgt_size}")
414
+
415
+ tolerant = self.aspect_ratio_tolerance
416
+
417
+ # 直接比较宽高比
418
+ if abs((tgt_w / src_w) - (tgt_h / src_h)) <= tolerant:
419
+ return True
420
+
421
+ # 尝试忽略方向差异
422
+ if self.match_rotation:
423
+ ratio_src = max(src_w, src_h) / min(src_w, src_h)
424
+ ratio_tgt = max(tgt_w, tgt_h) / min(tgt_w, tgt_h)
425
+ return abs(ratio_src - ratio_tgt) <= tolerant
426
+
427
+ return False
428
+
429
+ def __assert_scalable(self, source: tuple[int, int], target: tuple[int, int]) -> tuple[int, int]:
430
+ """
431
+ 校验分辨率是否可缩放,并返回调整后的目标分辨率。
432
+
433
+ 当 match_rotation 为 True 且源分辨率与目标分辨率的旋转方向不一致时,
434
+ 自动交换目标分辨率的宽高,使其与源分辨率的方向保持一致。
435
+
436
+ :param src_size: 源分辨率 (width, height)
437
+ :param tgt_size: 目标分辨率 (width, height)
438
+ :return: 调整后的目标分辨率 (width, height)
439
+ :raises UnscalableResolutionError: 若宽高比不兼容
440
+ """
441
+ # 智能调整目标分辨率方向
442
+ adjusted_tgt_size = target
443
+ if self.match_rotation:
444
+ src_w, src_h = source
445
+ tgt_w, tgt_h = target
446
+
447
+ # 判断源分辨率和目标分辨率的方向
448
+ src_is_landscape = src_w > src_h
449
+ tgt_is_landscape = tgt_w > tgt_h
450
+
451
+ # 如果方向不一致,交换目标分辨率的宽高
452
+ if src_is_landscape != tgt_is_landscape:
453
+ adjusted_tgt_size = (tgt_h, tgt_w)
454
+
455
+ # 校验调整后的分辨率是否兼容
456
+ if not self.__aspect_ratio_compatible(source, adjusted_tgt_size):
457
+ raise UnscalableResolutionError(target, source)
458
+
459
+ return adjusted_tgt_size
460
+
328
461
 
329
462
  class AndroidDevice(Device):
330
463
  def __init__(self, adb_connection: AdbUtilsDevice | None = None) -> None:
331
464
  super().__init__('android')
332
465
  self._adb: AdbUtilsDevice | None = adb_connection
466
+ self.commands: AndroidCommandable
467
+
468
+ def current_package(self) -> str | None:
469
+ """
470
+ 获取前台 APP 的包名。
471
+
472
+ :return: 前台 APP 的包名。如果获取失败,则返回 None。
473
+ :exception: 如果设备不支持此功能,则抛出 NotImplementedError。
474
+ """
475
+ ret = self.commands.current_package()
476
+ logger.debug("current_package: %s", ret)
477
+ return ret
478
+
479
+ def launch_app(self, package_name: str) -> None:
480
+ """
481
+ 根据包名启动 app
482
+ """
483
+ self.commands.launch_app(package_name)
484
+
333
485
 
334
486
  class WindowsDevice(Device):
335
487
  def __init__(self) -> None:
336
488
  super().__init__('windows')
489
+ self.commands: WindowsCommandable
337
490
 
338
491
 
339
492
  if __name__ == "__main__":
@@ -346,10 +499,10 @@ if __name__ == "__main__":
346
499
  d = adb.device_list()[-1]
347
500
  d.shell("dumpsys activity top | grep ACTIVITY | tail -n 1")
348
501
  dd = AndroidDevice(d)
349
- adb_imp = AdbRawImpl(dd)
350
- dd._command = adb_imp
502
+ adb_imp = AdbRawImpl(d)
351
503
  dd._touch = adb_imp
352
504
  dd._screenshot = adb_imp
505
+ dd.commands = adb_imp
353
506
  # dd._screenshot = MinicapScreenshotImpl(dd)
354
507
  # dd._screenshot = UiAutomator2Impl(dd)
355
508
 
@@ -1,10 +1,11 @@
1
- from .protocol import HostProtocol, Instance
1
+ from .protocol import HostProtocol, Instance, AdbHostConfig, WindowsHostConfig, RemoteWindowsHostConfig
2
2
  from .custom import CustomInstance, create as create_custom
3
3
  from .mumu12_host import Mumu12Host, Mumu12Instance
4
4
  from .leidian_host import LeidianHost, LeidianInstance
5
5
 
6
6
  __all__ = [
7
7
  'HostProtocol', 'Instance',
8
+ 'AdbHostConfig', 'WindowsHostConfig', 'RemoteWindowsHostConfig',
8
9
  'CustomInstance', 'create_custom',
9
10
  'Mumu12Host', 'Mumu12Instance',
10
11
  'LeidianHost', 'LeidianInstance'
@@ -0,0 +1,94 @@
1
+ from abc import ABC
2
+ from typing import Any, Literal, TypeGuard, TypeVar, get_args
3
+ from typing_extensions import assert_never
4
+
5
+ from adbutils import adb
6
+ from adbutils._device import AdbDevice
7
+ from kotonebot import logging
8
+ from kotonebot.client.device import AndroidDevice
9
+ from .protocol import Instance, AdbHostConfig, Device
10
+
11
+ logger = logging.getLogger(__name__)
12
+ AdbRecipes = Literal['adb', 'adb_raw', 'uiautomator2']
13
+
14
+ def is_adb_recipe(recipe: Any) -> TypeGuard[AdbRecipes]:
15
+ return recipe in get_args(AdbRecipes)
16
+
17
+ def connect_adb(
18
+ ip: str,
19
+ port: int,
20
+ connect: bool = True,
21
+ disconnect: bool = True,
22
+ timeout: float = 180,
23
+ device_serial: str | None = None
24
+ ) -> AdbDevice:
25
+ """
26
+ 创建 ADB 连接。
27
+ """
28
+ if disconnect:
29
+ logger.debug('adb disconnect %s:%d', ip, port)
30
+ adb.disconnect(f'{ip}:{port}')
31
+ if connect:
32
+ logger.debug('adb connect %s:%d', ip, port)
33
+ result = adb.connect(f'{ip}:{port}')
34
+ if 'cannot connect to' in result:
35
+ raise ValueError(result)
36
+ serial = device_serial or f'{ip}:{port}'
37
+ logger.debug('adb wait for %s', serial)
38
+ adb.wait_for(serial, timeout=timeout)
39
+ devices = adb.device_list()
40
+ logger.debug('adb device_list: %s', devices)
41
+ d = [d for d in devices if d.serial == serial]
42
+ if len(d) == 0:
43
+ raise ValueError(f"Device {serial} not found")
44
+ d = d[0]
45
+ return d
46
+
47
+ class CommonAdbCreateDeviceMixin(ABC):
48
+ """
49
+ 通用 ADB 创建设备的 Mixin。
50
+ 该 Mixin 定义了创建 ADB 设备的通用接口。
51
+ """
52
+ def __init__(self, *args, **kwargs) -> None:
53
+ super().__init__(*args, **kwargs)
54
+ # 下面的属性只是为了让类型检查通过,无实际实现
55
+ self.adb_ip: str
56
+ self.adb_port: int
57
+ self.adb_name: str
58
+
59
+ def create_device(self, recipe: AdbRecipes, config: AdbHostConfig) -> Device:
60
+ """
61
+ 创建 ADB 设备。
62
+ """
63
+ connection = connect_adb(
64
+ self.adb_ip,
65
+ self.adb_port,
66
+ connect=True,
67
+ disconnect=True,
68
+ timeout=config.timeout,
69
+ device_serial=self.adb_name
70
+ )
71
+ d = AndroidDevice(connection)
72
+ match recipe:
73
+ case 'adb':
74
+ from kotonebot.client.implements.adb import AdbImpl
75
+ impl = AdbImpl(connection)
76
+ d._screenshot = impl
77
+ d._touch = impl
78
+ d.commands = impl
79
+ case 'adb_raw':
80
+ from kotonebot.client.implements.adb_raw import AdbRawImpl
81
+ impl = AdbRawImpl(connection)
82
+ d._screenshot = impl
83
+ d._touch = impl
84
+ d.commands = impl
85
+ case 'uiautomator2':
86
+ from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
87
+ from kotonebot.client.implements.adb import AdbImpl
88
+ impl = UiAutomator2Impl(connection)
89
+ d._screenshot = impl
90
+ d._touch = impl
91
+ d.commands = AdbImpl(connection)
92
+ case _:
93
+ assert_never(f'Unsupported ADB recipe: {recipe}')
94
+ return d
@@ -1,19 +1,21 @@
1
1
  import os
2
2
  import subprocess
3
3
  from psutil import process_iter
4
- from .protocol import HostProtocol, Instance
5
- from typing import Optional, ParamSpec, TypeVar, TypeGuard
4
+ from .protocol import Instance, AdbHostConfig, HostProtocol
5
+ from typing import ParamSpec, TypeVar
6
6
  from typing_extensions import override
7
7
 
8
8
  from kotonebot import logging
9
- from kotonebot.client.device import Device
9
+ from kotonebot.client import Device
10
+ from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
10
11
 
11
12
  logger = logging.getLogger(__name__)
13
+ CustomRecipes = AdbRecipes
12
14
 
13
15
  P = ParamSpec('P')
14
16
  T = TypeVar('T')
15
17
 
16
- class CustomInstance(Instance):
18
+ class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
17
19
  def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
18
20
  super().__init__(*args, **kwargs)
19
21
  self.exe_path: str | None = exe_path
@@ -65,6 +67,14 @@ class CustomInstance(Instance):
65
67
  def refresh(self):
66
68
  pass
67
69
 
70
+ @override
71
+ def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
72
+ """为自定义实例创建 Device。"""
73
+ if self.adb_port is None:
74
+ raise ValueError("ADB port is not set and is required.")
75
+
76
+ return super().create_device(impl, host_config)
77
+
68
78
  def __repr__(self) -> str:
69
79
  return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
70
80
 
@@ -76,6 +86,25 @@ def _type_check(ins: Instance) -> CustomInstance:
76
86
  def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
77
87
  return CustomInstance(exe_path, emulator_args=emulator_args, id='custom', name='Custom', adb_ip=adb_ip, adb_port=adb_port, adb_name=adb_name)
78
88
 
89
+ class CustomHost(HostProtocol[CustomRecipes]):
90
+ @staticmethod
91
+ def installed() -> bool:
92
+ # Custom instances don't have a specific installation requirement
93
+ return True
94
+
95
+ @staticmethod
96
+ def list() -> list[Instance]:
97
+ # Custom instances are created manually, not discovered
98
+ return []
99
+
100
+ @staticmethod
101
+ def query(*, id: str) -> Instance | None:
102
+ # Custom instances are created manually, not discovered
103
+ return None
104
+
105
+ @staticmethod
106
+ def recipes() -> 'list[CustomRecipes]':
107
+ return ['adb', 'adb_raw', 'uiautomator2']
79
108
 
80
109
  if __name__ == '__main__':
81
110
  ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
@@ -1,15 +1,17 @@
1
1
  import os
2
2
  import subprocess
3
+ from typing import Literal
3
4
  from functools import lru_cache
4
5
  from typing_extensions import override
5
6
 
6
7
  from kotonebot import logging
7
- from kotonebot.client import DeviceImpl, create_device
8
- from kotonebot.client.device import Device
8
+ from kotonebot.client import Device
9
9
  from kotonebot.util import Countdown, Interval
10
- from .protocol import HostProtocol, Instance, copy_type
10
+ from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
11
+ from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
11
12
 
12
13
  logger = logging.getLogger(__name__)
14
+ LeidianRecipes = AdbRecipes
13
15
 
14
16
  if os.name == 'nt':
15
17
  from ...interop.win.reg import read_reg
@@ -18,7 +20,7 @@ else:
18
20
  """Stub for read_reg on non-Windows platforms."""
19
21
  return default
20
22
 
21
- class LeidianInstance(Instance):
23
+ class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
22
24
  @copy_type(Instance.__init__)
23
25
  def __init__(self, *args, **kwargs):
24
26
  super().__init__(*args, **kwargs)
@@ -61,27 +63,23 @@ class LeidianInstance(Instance):
61
63
  it = Interval(5)
62
64
  while not cd.expired() and not self.running():
63
65
  it.wait()
66
+ self.refresh()
64
67
  if not self.running():
65
68
  raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
66
69
 
67
70
  @override
68
71
  def running(self) -> bool:
69
- result = LeidianHost._invoke_manager(['isrunning', '--index', str(self.index)])
70
- return result.strip() == 'running'
72
+ return self.is_running
71
73
 
72
74
  @override
73
- def create_device(self, impl: DeviceImpl, *, timeout: float = 180) -> Device:
75
+ def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
76
+ """为雷电模拟器实例创建 Device。"""
74
77
  if self.adb_port is None:
75
78
  raise ValueError("ADB port is not set and is required.")
76
- return create_device(
77
- addr=f'{self.adb_ip}:{self.adb_port}',
78
- impl=impl,
79
- device_serial=self.adb_name,
80
- connect=False,
81
- timeout=timeout
82
- )
83
79
 
84
- class LeidianHost(HostProtocol):
80
+ return super().create_device(impl, host_config)
81
+
82
+ class LeidianHost(HostProtocol[LeidianRecipes]):
85
83
  @staticmethod
86
84
  @lru_cache(maxsize=1)
87
85
  def _read_install_path() -> str | None:
@@ -186,6 +184,10 @@ class LeidianHost(HostProtocol):
186
184
  return instance
187
185
  return None
188
186
 
187
+ @staticmethod
188
+ def recipes() -> 'list[LeidianRecipes]':
189
+ return ['adb', 'adb_raw', 'uiautomator2']
190
+
189
191
  if __name__ == '__main__':
190
192
  logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
191
193
  print(LeidianHost._read_install_path())
@@ -1,16 +1,19 @@
1
+ from dataclasses import dataclass
1
2
  import os
2
3
  import json
3
4
  import subprocess
4
5
  from functools import lru_cache
5
- from typing import Any
6
+ from typing import Any, Literal, overload
6
7
  from typing_extensions import override
7
8
 
8
9
  from kotonebot import logging
9
- from kotonebot.client import DeviceImpl, Device
10
+ from kotonebot.client import Device
11
+ from kotonebot.client.device import AndroidDevice
12
+ from kotonebot.client.implements.adb import AdbImpl
13
+ from kotonebot.client.implements.nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
10
14
  from kotonebot.util import Countdown, Interval
11
- from .protocol import HostProtocol, Instance, copy_type
12
-
13
- logger = logging.getLogger(__name__)
15
+ from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
16
+ from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin, connect_adb, is_adb_recipe
14
17
 
15
18
  if os.name == 'nt':
16
19
  from ...interop.win.reg import read_reg
@@ -19,7 +22,21 @@ else:
19
22
  """Stub for read_reg on non-Windows platforms."""
20
23
  return default
21
24
 
22
- class Mumu12Instance(Instance):
25
+ logger = logging.getLogger(__name__)
26
+ MuMu12Recipes = AdbRecipes | Literal['nemu_ipc']
27
+
28
+ @dataclass
29
+ class MuMu12HostConfig(AdbHostConfig):
30
+ """nemu_ipc 能力的配置模型。"""
31
+ display_id: int | None = 0
32
+ """目标显示器 ID,默认为 0(主显示器)。若为 None 且设置了 target_package_name,则自动获取对应的 display_id。"""
33
+ target_package_name: str | None = None
34
+ """目标应用包名,用于自动获取 display_id。"""
35
+ app_index: int = 0
36
+ """多开应用索引,传给 get_display_id 方法。"""
37
+
38
+
39
+ class Mumu12Instance(CommonAdbCreateDeviceMixin, Instance[MuMu12HostConfig]):
23
40
  @copy_type(Instance.__init__)
24
41
  def __init__(self, *args, **kwargs):
25
42
  super().__init__(*args, **kwargs)
@@ -70,14 +87,58 @@ class Mumu12Instance(Instance):
70
87
  def running(self) -> bool:
71
88
  return self.is_android_started
72
89
 
73
- class Mumu12Host(HostProtocol):
90
+ @overload
91
+ def create_device(self, recipe: Literal['nemu_ipc'], host_config: MuMu12HostConfig) -> Device: ...
92
+ @overload
93
+ def create_device(self, recipe: AdbRecipes, host_config: AdbHostConfig) -> Device: ...
94
+
95
+ @override
96
+ def create_device(self, recipe: MuMu12Recipes, host_config: MuMu12HostConfig | AdbHostConfig) -> Device:
97
+ """为MuMu12模拟器实例创建 Device。"""
98
+ if self.adb_port is None:
99
+ raise ValueError("ADB port is not set and is required.")
100
+
101
+ if recipe == 'nemu_ipc' and isinstance(host_config, MuMu12HostConfig):
102
+ # NemuImpl
103
+ nemu_path = Mumu12Host._read_install_path()
104
+ if not nemu_path:
105
+ raise RuntimeError("无法找到 MuMu12 的安装路径。")
106
+ nemu_config = NemuIpcImplConfig(
107
+ nemu_folder=nemu_path,
108
+ instance_id=int(self.id),
109
+ display_id=host_config.display_id,
110
+ target_package_name=host_config.target_package_name,
111
+ app_index=host_config.app_index
112
+ )
113
+ nemu_impl = NemuIpcImpl(nemu_config)
114
+ # AdbImpl
115
+ adb_impl = AdbImpl(connect_adb(
116
+ self.adb_ip,
117
+ self.adb_port,
118
+ timeout=host_config.timeout,
119
+ device_serial=self.adb_name
120
+ ))
121
+ device = AndroidDevice()
122
+ device._screenshot = nemu_impl
123
+ device._touch = nemu_impl
124
+ device.commands = adb_impl
125
+
126
+ return device
127
+ elif isinstance(host_config, AdbHostConfig) and is_adb_recipe(recipe):
128
+ return super().create_device(recipe, host_config)
129
+ else:
130
+ raise ValueError(f'Unknown recipe: {recipe}')
131
+
132
+ class Mumu12Host(HostProtocol[MuMu12Recipes]):
74
133
  @staticmethod
75
134
  @lru_cache(maxsize=1)
76
135
  def _read_install_path() -> str | None:
77
- """
78
- Reads the installation path (DisplayIcon) of MuMu Player 12 from the registry.
136
+ r"""
137
+ 从注册表中读取 MuMu Player 12 的安装路径。
79
138
 
80
- :return: The path to the display icon if found, otherwise None.
139
+ 返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
140
+
141
+ :return: 若找到,则返回安装路径;否则返回 None。
81
142
  """
82
143
  if os.name != 'nt':
83
144
  return None
@@ -94,6 +155,9 @@ class Mumu12Host(HostProtocol):
94
155
  icon_path = icon_path.replace('"', '')
95
156
  path = os.path.dirname(icon_path)
96
157
  logger.debug('MuMu Player 12 installation path: %s', path)
158
+ # 返回根目录(去掉 shell 子目录)
159
+ if os.path.basename(path).lower() == 'shell':
160
+ path = os.path.dirname(path)
97
161
  return path
98
162
  return None
99
163
 
@@ -108,7 +172,7 @@ class Mumu12Host(HostProtocol):
108
172
  install_path = Mumu12Host._read_install_path()
109
173
  if install_path is None:
110
174
  raise RuntimeError('MuMu Player 12 is not installed.')
111
- manager_path = os.path.join(install_path, 'MuMuManager.exe')
175
+ manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
112
176
  logger.debug('MuMuManager execute: %s', repr(args))
113
177
  output = subprocess.run(
114
178
  [manager_path] + args,
@@ -162,6 +226,10 @@ class Mumu12Host(HostProtocol):
162
226
  if instance.id == id:
163
227
  return instance
164
228
  return None
229
+
230
+ @staticmethod
231
+ def recipes() -> 'list[MuMu12Recipes]':
232
+ return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
165
233
 
166
234
  if __name__ == '__main__':
167
235
  logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')