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.
- kotonebot/backend/context/context.py +29 -25
- kotonebot/backend/debug/server.py +0 -2
- kotonebot/backend/dispatch.py +1 -17
- kotonebot/backend/loop.py +277 -0
- kotonebot/client/__init__.py +4 -2
- kotonebot/client/device.py +178 -25
- kotonebot/client/host/__init__.py +2 -1
- kotonebot/client/host/adb_common.py +94 -0
- kotonebot/client/host/custom.py +33 -4
- kotonebot/client/host/leidian_host.py +17 -15
- kotonebot/client/host/mumu12_host.py +79 -11
- kotonebot/client/host/protocol.py +44 -19
- kotonebot/client/host/windows_common.py +55 -0
- kotonebot/client/implements/__init__.py +7 -0
- kotonebot/client/implements/adb.py +24 -7
- kotonebot/client/implements/adb_raw.py +3 -3
- kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
- kotonebot/client/implements/remote_windows.py +17 -21
- kotonebot/client/implements/uiautomator2.py +5 -8
- kotonebot/client/implements/windows.py +28 -36
- kotonebot/client/protocol.py +13 -0
- kotonebot/client/registration.py +24 -0
- kotonebot/config/base_config.py +8 -2
- kotonebot/debug_entry.py +11 -4
- kotonebot/errors.py +7 -0
- kotonebot/kaa/game_ui/common.py +4 -7
- kotonebot/kaa/game_ui/dialog.py +24 -4
- kotonebot/kaa/game_ui/idols_overview.py +28 -12
- kotonebot/kaa/game_ui/primary_button.py +55 -0
- kotonebot/kaa/game_ui/scrollable.py +0 -5
- kotonebot/kaa/main/cli.py +9 -4
- kotonebot/kaa/main/dmm_host.py +15 -8
- kotonebot/kaa/main/gr.py +114 -27
- kotonebot/kaa/main/kaa.py +140 -41
- kotonebot/kaa/metadata.py +47 -0
- kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
- kotonebot/kaa/resources/game.db +0 -0
- kotonebot/kaa/resources/game_ver.txt +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_0.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-kllj-3-012_1.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_0.png +0 -0
- kotonebot/kaa/resources/idol_cards/i_card-skin-ssmk-3-012_1.png +0 -0
- kotonebot/kaa/sprites/24e99232-9434-457f-a9a0-69dd7ecf675f.png +0 -0
- kotonebot/kaa/sprites/2ededcf5-1d80-4e2a-9c83-2a31998331ce.png +0 -0
- kotonebot/kaa/sprites/3b473fe6-e147-477f-b088-9b8fb042a4f6.png +0 -0
- kotonebot/kaa/sprites/55f7db71-0a18-4b3d-b847-57959b8d2e32.png +0 -0
- kotonebot/kaa/sprites/6cd80be8-c9b3-4ba5-bf17-3ffc9b000743.png +0 -0
- kotonebot/kaa/sprites/74ec3510-583d-4a76-ac69-38480fbf1387.png +0 -0
- kotonebot/kaa/sprites/a0bd6a5f-784d-4f0a-9d66-10f4b80c8d3e.png +0 -0
- kotonebot/kaa/sprites/d3424d31-0502-4623-996e-f0194e5085ce.png +0 -0
- kotonebot/kaa/sprites/e6b45405-cd9f-4c6e-a9f1-6ec953747c65.png +0 -0
- kotonebot/kaa/sprites/f5c16d2f-ebc5-4617-9b96-971696af7c52.png +0 -0
- kotonebot/kaa/tasks/R.py +157 -135
- kotonebot/kaa/{clear_logs.py → tasks/clear_logs.py} +0 -3
- kotonebot/kaa/tasks/produce/in_purodyuusu.py +0 -4
- kotonebot/kaa/tasks/produce/non_lesson_actions.py +0 -3
- kotonebot/kaa/tasks/produce/produce.py +119 -70
- kotonebot/kaa/tasks/start_game.py +7 -6
- kotonebot/kaa/util/paths.py +6 -1
- kotonebot/tools/mirror.py +23 -13
- kotonebot/util.py +32 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/METADATA +4 -1
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/RECORD +201 -181
- kotonebot/client/factory.py +0 -92
- kotonebot/kaa/sprites/38b8f119-8de1-49bf-8a5f-a44371de4569.png +0 -0
- /kotonebot/kaa/sprites/{5ca50448-cc21-499a-a685-c5e1c4240c48.png → 019273c4-22ce-486f-8a7d-dbc78f13dcac.png} +0 -0
- /kotonebot/kaa/sprites/{afa0f85f-dcb7-48a1-a1ae-624456b9b65c.png → 03e34ef7-36b5-4ddc-9545-5b70a2eba72e.png} +0 -0
- /kotonebot/kaa/sprites/{a00fcf77-2800-4f7d-993d-4636b36045bb.png → 06ea773b-e2b3-40d4-82f6-b119ce9ca00b.png} +0 -0
- /kotonebot/kaa/sprites/{d58d9987-b1ca-4669-a9a6-9a0fd1847fa6.png → 0769f8f4-1c47-4bad-a783-bba5e20250bf.png} +0 -0
- /kotonebot/kaa/sprites/{82a5e224-a161-43d4-8e15-2946480336dd.png → 08946997-4e9e-4242-95e1-e988fa96b919.png} +0 -0
- /kotonebot/kaa/sprites/{fb927b92-7a82-4869-8c61-581d49cd7201.png → 0d60ce10-20ce-4980-8172-b6bac08d6258.png} +0 -0
- /kotonebot/kaa/sprites/{b1e88fda-434a-4d06-8891-883a7beafa12.png → 0f40352f-6443-406e-8e2c-c40d5cfc6c59.png} +0 -0
- /kotonebot/kaa/sprites/{c4f28132-4aba-40ba-be6c-b1a31d014635.png → 0f6d7f89-bdb2-4235-9b6b-e65cb73a4167.png} +0 -0
- /kotonebot/kaa/sprites/{e866d8b1-4f61-45bf-8170-5229f53c047a.png → 14d09c44-3897-4c81-b615-19c35d0f3793.png} +0 -0
- /kotonebot/kaa/sprites/{a105d17d-d44c-4798-aeff-70808c91cb47.png → 15191dec-7e33-4721-8068-15c11978e885.png} +0 -0
- /kotonebot/kaa/sprites/{869b1771-b2ce-4872-ab75-2157c47c5b13.png → 15f8f31e-65ce-408d-8556-b799bcefc9d4.png} +0 -0
- /kotonebot/kaa/sprites/{994aa6a6-0189-4a79-8125-56136973b581.png → 20b01af5-907b-4d49-9f5b-a3700ca9c0c3.png} +0 -0
- /kotonebot/kaa/sprites/{92b7d830-3db5-434e-b156-0ac855db480d.png → 242c088b-bc73-4d11-aa31-ee7c71477fb4.png} +0 -0
- /kotonebot/kaa/sprites/{9c1fa1d4-2ae5-4093-abaa-cebb51135721.png → 2f2a7951-a30a-4835-b9f6-3205cfdd2f05.png} +0 -0
- /kotonebot/kaa/sprites/{a33ae1c3-ac69-4783-977e-9f0f1b1a2f8d.png → 3031397e-3d12-4f23-8377-006eca5d2627.png} +0 -0
- /kotonebot/kaa/sprites/{e7022542-4317-4a61-8098-f3f733d8400e.png → 312dbd82-5457-474a-91ce-511f0e4e9c80.png} +0 -0
- /kotonebot/kaa/sprites/{cd8d3d55-f102-419e-a6fd-2ad5ecc4fd2e.png → 31851ad1-df39-4a4f-9ae7-19c1d1c6b51a.png} +0 -0
- /kotonebot/kaa/sprites/{826bf80a-112e-4356-9111-4b7e692ccc82.png → 338b699a-d556-4b80-a577-123442c3db45.png} +0 -0
- /kotonebot/kaa/sprites/{d8136469-6d73-4f82-a79c-fee83c3e93a8.png → 35319890-98e3-4f75-b969-6b8576ef668d.png} +0 -0
- /kotonebot/kaa/sprites/{96cf0633-6531-499b-b1cc-6670ab710293.png → 3536f829-70d3-48f9-8fd4-b65eab7502f3.png} +0 -0
- /kotonebot/kaa/sprites/{31a52dc8-b7fb-4ff6-a261-eb90a918603c.png → 35516b69-a4f4-4ad4-8af4-35043475ee90.png} +0 -0
- /kotonebot/kaa/sprites/{d747239f-7620-4f23-9ac7-cf2d34889266.png → 36d71d33-09c9-4be8-b184-ccf40f700333.png} +0 -0
- /kotonebot/kaa/sprites/{b44f5e5b-e999-4427-8a9f-f76c6485f4a5.png → 379d5084-637f-45e8-8bd8-789eaa919821.png} +0 -0
- /kotonebot/kaa/sprites/{0a430873-8c12-4ce2-a5de-ab9790c08344.png → 3851303a-e3a0-46eb-aa7e-b47fb41946f7.png} +0 -0
- /kotonebot/kaa/sprites/{ff77d221-8444-4d0f-8c93-0223a97e2a98.png → 3d1aa368-e198-478f-a47f-49818efbaec8.png} +0 -0
- /kotonebot/kaa/sprites/{333fe6d3-0be7-40fb-a2a3-364f98758f2a.png → 3f5907a9-7fe4-4f46-8e46-a96ac7fa3923.png} +0 -0
- /kotonebot/kaa/sprites/{0decaa6a-b9a9-4d9b-829a-0fc1f67538b4.png → 40c0f296-d29e-4e05-aaff-a212339b742e.png} +0 -0
- /kotonebot/kaa/sprites/{c1ad996a-925e-41b5-b26c-d48b8ded21d2.png → 43e72796-a644-4d9b-a817-8add011d9a75.png} +0 -0
- /kotonebot/kaa/sprites/{e2fae2a4-bb19-4f49-b382-47e504ec26d3.png → 469c2b41-4171-4902-acc5-da0c807886ab.png} +0 -0
- /kotonebot/kaa/sprites/{559df9b9-e74d-45af-a300-77d3424e69c6.png → 497e7930-f423-457a-8168-b1a1532bb1e9.png} +0 -0
- /kotonebot/kaa/sprites/{e41bb5df-164d-4760-9db6-d5d5d763bb5f.png → 49a43af3-17e3-4434-ba4d-519775ccaea9.png} +0 -0
- /kotonebot/kaa/sprites/{f9b2f64d-f829-4b13-9704-c5f00fb956be.png → 49b9a298-e847-4322-99a2-c0588c3542fe.png} +0 -0
- /kotonebot/kaa/sprites/{645744c4-3bbf-4ea2-8f46-4725fff874b3.png → 4a1902aa-01db-46bb-bea5-88ac62c82562.png} +0 -0
- /kotonebot/kaa/sprites/{c0fc7de5-b842-4c70-9cf2-89d833bfe7a6.png → 4a96a728-6303-4ae3-8e0c-9f1e8e5f86a7.png} +0 -0
- /kotonebot/kaa/sprites/{73f4204e-8281-47cc-9dce-6bb8280c4ce5.png → 4b7c4221-a679-47f6-929d-0ef514894b27.png} +0 -0
- /kotonebot/kaa/sprites/{c1b1125f-542b-4e63-9c86-5d2e35a89a10.png → 4d355e36-9169-43e4-bd58-2836ee3e56e0.png} +0 -0
- /kotonebot/kaa/sprites/{ad8ee599-88a8-429c-8982-99086e552dcc.png → 4eb97d68-cd47-4100-9c3b-35b5c86b4bdf.png} +0 -0
- /kotonebot/kaa/sprites/{7a479871-547e-4098-9cdd-6c9bb9f7449a.png → 515a2ff0-36b8-4f2f-9902-e6c1b9796586.png} +0 -0
- /kotonebot/kaa/sprites/{02e0aa0d-4094-4bdc-8625-1a063733615b.png → 51a3ed29-92b9-4429-9e50-783d1b8dc6a7.png} +0 -0
- /kotonebot/kaa/sprites/{22d5a7d5-59a0-4137-b02e-1a7af86d4250.png → 52d3438b-afae-4c7a-abe5-7ed530c7558c.png} +0 -0
- /kotonebot/kaa/sprites/{5bf00cba-9aab-4ffe-b20b-8f5e74279276.png → 540ee3b5-ca8f-4091-80a6-ce774d95e5f7.png} +0 -0
- /kotonebot/kaa/sprites/{05ff5efd-0f76-47ce-bc89-237266bc17c0.png → 54a15298-ecaf-4cc5-b8a2-5b0d8f8add41.png} +0 -0
- /kotonebot/kaa/sprites/{1f9165e6-77ee-403e-9b76-99c114527dbb.png → 554c0ff6-0a05-419d-85f9-5ced260d0d2e.png} +0 -0
- /kotonebot/kaa/sprites/{79bc56c5-eae6-4389-9116-4a9e182d6bf3.png → 55e4ef75-ba8e-4230-819c-450cf14b08cd.png} +0 -0
- /kotonebot/kaa/sprites/{cd4cbd7b-df80-45bb-a053-cc333a806934.png → 5824ef5f-6776-402c-b78c-ae79cc4cc878.png} +0 -0
- /kotonebot/kaa/sprites/{804a4b05-1ca3-4915-bf23-a66ccdc34404.png → 5a393483-c631-47fd-805c-c460c23b8f9e.png} +0 -0
- /kotonebot/kaa/sprites/{e5d15c50-45ee-4de9-8b97-4630cbd8f01a.png → 5b75cae4-a853-402c-acec-edbfef94342a.png} +0 -0
- /kotonebot/kaa/sprites/{1a3d18a4-7d91-4182-9e40-e60d73fed365.png → 5c9ae62c-488f-4109-a4d2-d2385c32043d.png} +0 -0
- /kotonebot/kaa/sprites/{7344e180-251b-465a-a8cf-974477f7ad44.png → 5cdd71be-c8cd-4310-b6f4-5e97ddbeb46e.png} +0 -0
- /kotonebot/kaa/sprites/{67a81c23-4ce4-45e3-bfa8-1136fb14b831.png → 5d5cb13b-c1fd-4808-b352-b21291a9ff0b.png} +0 -0
- /kotonebot/kaa/sprites/{4e21f02d-d5e1-49eb-946a-8edc74d0f1f7.png → 5d6c4462-3d7c-42eb-ab3e-195ebf845a09.png} +0 -0
- /kotonebot/kaa/sprites/{d7f604a6-c663-44db-b1d3-c9792eb3daa4.png → 5d94e628-82ff-4531-b70a-b13297491bea.png} +0 -0
- /kotonebot/kaa/sprites/{74521b6f-de5e-4ba8-a3c9-c6ba01070498.png → 5dc2dca3-48bd-4eee-af89-1a1d30593bd5.png} +0 -0
- /kotonebot/kaa/sprites/{b604e25d-680a-4e02-a012-2e593b5bc276.png → 5f0c2a57-efd0-4094-9f71-6f6fe97316f6.png} +0 -0
- /kotonebot/kaa/sprites/{fd610de1-b023-4747-8c1b-72f89b1c5647.png → 610348f2-6262-4cd1-8fc3-e7470f85580c.png} +0 -0
- /kotonebot/kaa/sprites/{5be4f6c7-2baa-42f3-a5a5-99ca0830cc30.png → 613e9b10-b2eb-44ea-b6d9-9310c0e1bdcf.png} +0 -0
- /kotonebot/kaa/sprites/{72c1804f-db8c-40fa-bc79-6533f2861a5d.png → 618b21ad-ab8b-436a-8eb8-bd320a198c9f.png} +0 -0
- /kotonebot/kaa/sprites/{572077d4-7ee2-449a-b4f0-66239b0d7aff.png → 61a09df6-7443-436c-9ef0-1d5a1cae1509.png} +0 -0
- /kotonebot/kaa/sprites/{321b54c8-c69b-4437-931d-655882dabeed.png → 64be274e-e8ad-4938-84c1-5d8aabcd10d8.png} +0 -0
- /kotonebot/kaa/sprites/{22ee3f1b-ddcb-48bf-85dd-8423f85e3199.png → 671b4a55-023a-434d-b29a-e844fe528e6b.png} +0 -0
- /kotonebot/kaa/sprites/{668002b6-805b-467b-a703-203fb75444f4.png → 6e5e9cf9-80b0-4241-a222-ff50dbd89a0c.png} +0 -0
- /kotonebot/kaa/sprites/{4bd66b38-78d3-4a27-816b-d315039a8bb6.png → 6ef59649-bbb3-42b6-a6b0-eac4f35a3da4.png} +0 -0
- /kotonebot/kaa/sprites/{7b3c6860-fbbe-48aa-aadf-8062fa88f1fe.png → 75a2093d-8743-427e-8d24-ae9a69fa14c1.png} +0 -0
- /kotonebot/kaa/sprites/{371a3192-101c-45ef-9da4-2ed389770c0f.png → 76bde19a-4b1b-4e52-86d9-f56be7529c71.png} +0 -0
- /kotonebot/kaa/sprites/{ef39804e-8d2d-40f6-8d13-bd2877d1353a.png → 77d06f4d-8214-4306-9dc5-035c720f7256.png} +0 -0
- /kotonebot/kaa/sprites/{24cd42fd-bb3b-4fc6-b40d-d3e3e36fe9f1.png → 78b642d3-9a2c-4c17-8872-3cc78211f4a0.png} +0 -0
- /kotonebot/kaa/sprites/{0bab7080-9a37-4c27-9821-8df36d49b678.png → 7a9d1c39-b306-4cb0-be2f-7a5963099949.png} +0 -0
- /kotonebot/kaa/sprites/{b1d1dfab-51f5-4ae9-b718-cdf46774c0ff.png → 7ddda828-c8b0-4331-864f-99b85b7c5557.png} +0 -0
- /kotonebot/kaa/sprites/{5edef075-4843-4dc9-840f-ae445cbe0730.png → 81399fd9-f254-4d0e-9775-ad451c237a4e.png} +0 -0
- /kotonebot/kaa/sprites/{705d9ecf-faf8-40f4-b701-637cec822c1e.png → 81526e97-1b8d-433f-a5c9-907e4e8c85cd.png} +0 -0
- /kotonebot/kaa/sprites/{ecd44a4a-e1ca-4a86-ae0d-2306a672613f.png → 818e2370-8fb9-4e6c-ab78-58e86d6525b5.png} +0 -0
- /kotonebot/kaa/sprites/{4f58031c-36b9-4baf-bccf-11c05d83bd19.png → 8431cc52-0041-4425-808c-dcd4d196506c.png} +0 -0
- /kotonebot/kaa/sprites/{13535ecd-bdf5-4ff7-aa2c-6ecb20fe9c06.png → 85a49ff3-6075-4797-8b7f-35851a843698.png} +0 -0
- /kotonebot/kaa/sprites/{d22ca9e0-001d-43d6-a557-1c76c5a27c81.png → 86062e91-d6c1-44cf-9f8c-26a8b9dd8686.png} +0 -0
- /kotonebot/kaa/sprites/{0bbfb5b0-f0f7-4ad9-86f7-a2adb01a6371.png → 860c3d98-e263-482f-95fb-4fc9c472e8b8.png} +0 -0
- /kotonebot/kaa/sprites/{ed5d2bf3-405c-4165-88d0-9f8a4be968c6.png → 8669ce44-dec7-47c2-aafd-3f2fbfd1d083.png} +0 -0
- /kotonebot/kaa/sprites/{f0786a26-81b5-4e76-b577-db70ab3fab50.png → 86b15801-c932-4034-bed8-fd76b0f65916.png} +0 -0
- /kotonebot/kaa/sprites/{80fb515c-4323-422d-a24c-80a3bf702f1d.png → 893b015d-6894-4e74-880e-d22859a25662.png} +0 -0
- /kotonebot/kaa/sprites/{fdfecda8-2fe0-487e-b5fe-6358274bb9ff.png → 8c0fa5fa-cebb-4ae7-bd1c-b542c9cbf694.png} +0 -0
- /kotonebot/kaa/sprites/{73af305f-840f-4325-abae-b3519fba672a.png → 913a1d17-54fe-4c8f-ac50-ef7f39600b86.png} +0 -0
- /kotonebot/kaa/sprites/{c2667677-cbff-4dd4-a401-e0963e7224f4.png → 964c6b90-f526-4199-9523-d8124373a56b.png} +0 -0
- /kotonebot/kaa/sprites/{db94ffb7-a09d-4a51-b420-b930851c9db4.png → 97c281a9-ebb0-4f54-b644-b7ed7dd65f81.png} +0 -0
- /kotonebot/kaa/sprites/{48c257f8-3583-49f7-b01a-ba7d251086ac.png → 998fc84a-af71-4fa5-8aa4-e0bfeb9c53ea.png} +0 -0
- /kotonebot/kaa/sprites/{469006bc-a011-46e4-bcde-992be888456e.png → 9cbf7f6e-fe77-463b-9c60-495dc823510a.png} +0 -0
- /kotonebot/kaa/sprites/{4d6c198a-0e34-4c80-86d1-7789b2082d64.png → 9ce7d48e-295d-47b2-96eb-f3ce697c1b37.png} +0 -0
- /kotonebot/kaa/sprites/{a3c12040-2b11-4038-b830-215fb7b6f359.png → 9dcca89b-c92b-4708-8f19-facd40d09a65.png} +0 -0
- /kotonebot/kaa/sprites/{fbd816df-7ae6-40a4-b4d4-5708f5ba4a7c.png → 9e369992-ea8c-4c15-88d3-1344319978b0.png} +0 -0
- /kotonebot/kaa/sprites/{54bb5fcf-be61-4af8-bb19-a3c5113cf9e1.png → 9f9003ba-468f-406f-a82f-1b7a23ef347c.png} +0 -0
- /kotonebot/kaa/sprites/{309105d4-073a-47c9-8c5c-d593aa497723.png → a2bf4e11-fbc3-4172-bc0b-b16f8f59e68c.png} +0 -0
- /kotonebot/kaa/sprites/{111a03e6-e401-4ad5-b5dc-d5124e8f1ab0.png → a546e254-0490-43df-a291-a9a5c2b4c34a.png} +0 -0
- /kotonebot/kaa/sprites/{7736a171-9221-4cda-8288-908997abeff2.png → aacd229c-d307-4a20-a388-84d8f4afb4a5.png} +0 -0
- /kotonebot/kaa/sprites/{1bfce48a-b151-406e-a6f5-863cb68cbbf2.png → b003bef5-53db-4032-b1bd-d5075de365a8.png} +0 -0
- /kotonebot/kaa/sprites/{b2f7a430-5777-4861-8435-5419394e5ee8.png → b641a122-4734-43e1-b22f-468464a0cff1.png} +0 -0
- /kotonebot/kaa/sprites/{a82e06fb-a02e-4be5-a7b4-4d2758914fcf.png → bc7303f3-43e9-4ece-9206-5f9c779b813b.png} +0 -0
- /kotonebot/kaa/sprites/{f74fc514-61f1-4068-bdf4-754fcaf8ac89.png → bd7a6754-fffc-4315-987f-5ba32d95b697.png} +0 -0
- /kotonebot/kaa/sprites/{c7aa3f89-8a5e-4685-9297-4fc892bfdd81.png → c39f2006-442b-4ab7-99bb-3dfa0673f05d.png} +0 -0
- /kotonebot/kaa/sprites/{6bf5556a-8f4e-41d7-8ab5-d683ad1b47a8.png → c70f4634-6821-4480-a615-b5f9cdc578c1.png} +0 -0
- /kotonebot/kaa/sprites/{89c0e675-3fd4-456a-8ae7-3bfb248b7281.png → c774098e-d782-455c-80a9-5bad3713129d.png} +0 -0
- /kotonebot/kaa/sprites/{09433024-3dfe-4fcb-9d67-cf442b58f31b.png → c7e3736a-0e0f-474a-9386-f58292a45b92.png} +0 -0
- /kotonebot/kaa/sprites/{48d2f007-8c1b-43fc-bd3f-f0b10d2a0c88.png → c90b582c-8570-48b4-98c0-62d5b067ca2c.png} +0 -0
- /kotonebot/kaa/sprites/{9bb1a400-935a-4c5e-838b-fca502c49aba.png → c9d3699b-c8b1-4294-9b48-103df4ab6c65.png} +0 -0
- /kotonebot/kaa/sprites/{157dd58d-e319-4e0c-a37a-638e1a31c60b.png → cc9483c3-c895-4dcc-b3f2-0934ba1ad42e.png} +0 -0
- /kotonebot/kaa/sprites/{43d65b3f-bbae-4e29-a7bf-539379c1b072.png → cec49286-3d48-4fe0-8911-6a1b17d1ac20.png} +0 -0
- /kotonebot/kaa/sprites/{56a8c0ca-4659-4de2-9bce-55ce86a40d75.png → cef78712-d9ae-4d6e-8d7d-d047d48fbb98.png} +0 -0
- /kotonebot/kaa/sprites/{12e8d3f1-494a-46f5-908f-f621a0c57ec6.png → d06fd859-4678-4593-b096-c86b38810cb7.png} +0 -0
- /kotonebot/kaa/sprites/{ba955365-1328-4018-83a2-bfe818f8f172.png → d19c37ff-d5c1-4316-8424-e69ccb5c2523.png} +0 -0
- /kotonebot/kaa/sprites/{695f8d57-b2c9-4a6d-9ae9-1b49ee3eb22a.png → d26b60f7-d15c-4563-bedc-dd6e7e4078b2.png} +0 -0
- /kotonebot/kaa/sprites/{910d307b-6461-4950-badb-509975d0b797.png → d26bf52c-cb1b-4ebf-9c03-ff3f2d245959.png} +0 -0
- /kotonebot/kaa/sprites/{7e8164f6-a9e4-4600-b776-77b35a490933.png → d5fd6c73-4ad8-47b5-89be-c7b137a9950f.png} +0 -0
- /kotonebot/kaa/sprites/{05bfd235-2077-479a-8c43-11fb49316000.png → d696062b-9b58-4af1-835e-2cf5a0a0fbeb.png} +0 -0
- /kotonebot/kaa/sprites/{03458cf9-8ab0-4908-9a56-9c5c7e06bc9b.png → da7a329e-339e-4ee7-8775-bca505a1980d.png} +0 -0
- /kotonebot/kaa/sprites/{2f81e70a-f2af-420f-9b6b-6338c391911f.png → dedb8499-5881-4add-914f-ff0049285dae.png} +0 -0
- /kotonebot/kaa/sprites/{b3d1f8d4-1393-4206-b78e-a5495d8fb930.png → dedea21b-f21d-4621-a213-70ad03faec58.png} +0 -0
- /kotonebot/kaa/sprites/{f65a4495-f044-44de-bf0d-c869e637d5d3.png → dff74cce-ba2d-44aa-b924-183259080a32.png} +0 -0
- /kotonebot/kaa/sprites/{b3e72de2-e103-49a3-b0f2-8593cff87c4e.png → e0246b0d-f0cf-49d4-8423-7da46e75a575.png} +0 -0
- /kotonebot/kaa/sprites/{624555b9-31e3-46a3-8078-55619eaa40b9.png → e1b6b32b-c797-4f1d-aa2b-e7320083a9a6.png} +0 -0
- /kotonebot/kaa/sprites/{90a9c819-ee6b-4b3e-a0e4-ce484c6e413d.png → e23e3f19-03f6-4507-9128-115191272b90.png} +0 -0
- /kotonebot/kaa/sprites/{cc829886-23e8-4d47-b4c4-37ca20381180.png → e2eb3bae-6451-40b9-bbff-90f11dca1904.png} +0 -0
- /kotonebot/kaa/sprites/{6a7d061e-682b-42c1-97d5-121c52921e8b.png → e46e1b6d-6702-46bf-a07e-aec8d183453f.png} +0 -0
- /kotonebot/kaa/sprites/{de709296-e411-465f-86fc-5daf5ca83cef.png → e5297036-9934-44a4-a761-a6a49ae06e3e.png} +0 -0
- /kotonebot/kaa/sprites/{52620946-b92c-42c7-9fcc-0ff13388fd0d.png → e7da5988-0e83-44ad-888b-246365473f9e.png} +0 -0
- /kotonebot/kaa/sprites/{05f8f996-a38a-45d9-8ff9-720b6c4fd599.png → eb8d208a-dbcc-43ca-a8ac-098e1005ebfa.png} +0 -0
- /kotonebot/kaa/sprites/{8e8a0b57-0231-4ed6-958e-3196f7c9ca7b.png → ebe4ce64-98a9-40f2-9fde-3d5cdae673c7.png} +0 -0
- /kotonebot/kaa/sprites/{4a235eaa-46f1-4f71-b54d-ada0ade44572.png → ef2ad3df-3f89-4a48-8fc3-c70a8adc1044.png} +0 -0
- /kotonebot/kaa/sprites/{f8cbb089-a63e-4e40-9238-26d14da2c459.png → f05e410d-15b8-4831-bcc1-4cd8af07c882.png} +0 -0
- /kotonebot/kaa/sprites/{550fc45d-22c0-4a7c-a324-2c4ff107ab39.png → f1fac13a-84f3-42bd-9b52-e0ba90bc5255.png} +0 -0
- /kotonebot/kaa/sprites/{ad784a29-f0e2-4037-976b-45143db8441c.png → f7c34ab6-c165-48b7-ad5a-0b60613ce1bc.png} +0 -0
- /kotonebot/kaa/sprites/{0439399f-5fca-43f9-a105-99342ff04d2e.png → fa80b14e-953f-44bd-a4ad-99aefd276fd7.png} +0 -0
- /kotonebot/kaa/sprites/{7cea265a-fca5-43d0-b2da-067c8fff4d72.png → fe35655e-701b-4081-8df8-c17a7252acef.png} +0 -0
- /kotonebot/kaa/sprites/{da3e7cee-d77b-4e62-b703-ed257706dd39.png → fe853ac7-934e-4b4a-bcc7-b986e057dfcf.png} +0 -0
- /kotonebot/kaa/sprites/{1f8f8c46-43ec-4449-be9f-2e96da11da1e.png → ffda716c-2903-4ec7-8c59-aab5b8ad10bb.png} +0 -0
- /kotonebot/kaa/sprites/{4c72f39e-1182-4479-867f-8207744ee4fd.png → fff33200-c947-49f2-ab86-3ac98937fa36.png} +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/WHEEL +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/entry_points.txt +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/licenses/LICENSE +0 -0
- {ksaa-2025.6.8.0.dist-info → ksaa-2025.6.28.0.dist-info}/top_level.txt +0 -0
kotonebot/client/device.py
CHANGED
|
@@ -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
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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(
|
|
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
|
kotonebot/client/host/custom.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
3
|
from psutil import process_iter
|
|
4
|
-
from .protocol import
|
|
5
|
-
from typing import
|
|
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
|
|
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
|
|
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
|
-
|
|
70
|
-
return result.strip() == 'running'
|
|
72
|
+
return self.is_running
|
|
71
73
|
|
|
72
74
|
@override
|
|
73
|
-
def create_device(self, impl:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
r"""
|
|
137
|
+
从注册表中读取 MuMu Player 12 的安装路径。
|
|
79
138
|
|
|
80
|
-
|
|
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')
|