ksaa 2025.6.23.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 +0 -2
- kotonebot/client/__init__.py +1 -2
- kotonebot/client/device.py +155 -4
- kotonebot/client/host/adb_common.py +94 -0
- kotonebot/client/host/custom.py +27 -21
- kotonebot/client/host/leidian_host.py +14 -23
- kotonebot/client/host/mumu12_host.py +70 -24
- kotonebot/client/host/protocol.py +4 -1
- kotonebot/client/host/windows_common.py +55 -0
- kotonebot/client/implements/__init__.py +2 -1
- kotonebot/client/implements/adb.py +1 -44
- kotonebot/client/implements/adb_raw.py +2 -9
- 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 +8 -14
- kotonebot/client/implements/uiautomator2.py +5 -16
- kotonebot/client/implements/windows.py +5 -36
- kotonebot/client/registration.py +2 -67
- kotonebot/config/base_config.py +4 -2
- kotonebot/debug_entry.py +4 -2
- kotonebot/errors.py +7 -0
- kotonebot/kaa/main/dmm_host.py +13 -32
- kotonebot/kaa/main/gr.py +114 -27
- kotonebot/kaa/main/kaa.py +30 -3
- kotonebot/kaa/metadata.py +32 -0
- kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
- kotonebot/kaa/tasks/R.py +132 -132
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/METADATA +4 -1
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/RECORD +167 -162
- /kotonebot/kaa/sprites/{7f6cb60e-6ab3-4db4-86e1-8558250062a4.png → 019273c4-22ce-486f-8a7d-dbc78f13dcac.png} +0 -0
- /kotonebot/kaa/sprites/{bb0ae68d-44d5-47c6-91dd-a529bc34225b.png → 03e34ef7-36b5-4ddc-9545-5b70a2eba72e.png} +0 -0
- /kotonebot/kaa/sprites/{faf360e8-7845-4fcf-b0a6-0f5f93c050f1.png → 06ea773b-e2b3-40d4-82f6-b119ce9ca00b.png} +0 -0
- /kotonebot/kaa/sprites/{2f74b04f-1992-4584-8dc6-e789b081ddc3.png → 0769f8f4-1c47-4bad-a783-bba5e20250bf.png} +0 -0
- /kotonebot/kaa/sprites/{58a5f51d-d1a3-480b-8dde-ecead34d9f02.png → 08946997-4e9e-4242-95e1-e988fa96b919.png} +0 -0
- /kotonebot/kaa/sprites/{694d3a07-9697-4bda-984d-967779d530f7.png → 0d60ce10-20ce-4980-8172-b6bac08d6258.png} +0 -0
- /kotonebot/kaa/sprites/{1b6be6dd-2711-441a-9fa6-3281a6ad10c7.png → 0f40352f-6443-406e-8e2c-c40d5cfc6c59.png} +0 -0
- /kotonebot/kaa/sprites/{94433cf2-dd04-4b74-ae14-e5e3d945f579.png → 0f6d7f89-bdb2-4235-9b6b-e65cb73a4167.png} +0 -0
- /kotonebot/kaa/sprites/{9f99b0e9-8cc0-4877-8f8a-d26257ef5386.png → 14d09c44-3897-4c81-b615-19c35d0f3793.png} +0 -0
- /kotonebot/kaa/sprites/{b5d4265c-82e0-470d-a48e-8aa4edbdef5a.png → 15191dec-7e33-4721-8068-15c11978e885.png} +0 -0
- /kotonebot/kaa/sprites/{42d8b5dc-ef27-4411-b8b5-d981e562990b.png → 15f8f31e-65ce-408d-8556-b799bcefc9d4.png} +0 -0
- /kotonebot/kaa/sprites/{d1699baa-467b-46d1-9276-aae78fe8f589.png → 20b01af5-907b-4d49-9f5b-a3700ca9c0c3.png} +0 -0
- /kotonebot/kaa/sprites/{faf069e1-f623-47c1-b879-cfa246fbc7df.png → 242c088b-bc73-4d11-aa31-ee7c71477fb4.png} +0 -0
- /kotonebot/kaa/sprites/{c25c7ce5-ad8d-4b2c-83eb-ae6d4871a6c3.png → 2f2a7951-a30a-4835-b9f6-3205cfdd2f05.png} +0 -0
- /kotonebot/kaa/sprites/{aefdec38-e3b6-4762-aed8-33107686ddac.png → 3031397e-3d12-4f23-8377-006eca5d2627.png} +0 -0
- /kotonebot/kaa/sprites/{ed6d3e3d-0f04-4fa4-b0f9-fdbf324649f6.png → 312dbd82-5457-474a-91ce-511f0e4e9c80.png} +0 -0
- /kotonebot/kaa/sprites/{c571e1c0-ff85-4b2e-97ed-97d33b8d92e3.png → 31851ad1-df39-4a4f-9ae7-19c1d1c6b51a.png} +0 -0
- /kotonebot/kaa/sprites/{c484ca57-6b19-46b5-a107-1805a7488d48.png → 338b699a-d556-4b80-a577-123442c3db45.png} +0 -0
- /kotonebot/kaa/sprites/{920a3974-4875-4d2b-bbac-caa175c03ce0.png → 35319890-98e3-4f75-b969-6b8576ef668d.png} +0 -0
- /kotonebot/kaa/sprites/{f8eec9b2-55ca-4195-b098-a5393d02a98a.png → 3536f829-70d3-48f9-8fd4-b65eab7502f3.png} +0 -0
- /kotonebot/kaa/sprites/{97adee05-1586-4589-93e6-c6c3a9cdae9d.png → 35516b69-a4f4-4ad4-8af4-35043475ee90.png} +0 -0
- /kotonebot/kaa/sprites/{c5ea3dfd-6761-466e-b44c-86592ef05323.png → 36d71d33-09c9-4be8-b184-ccf40f700333.png} +0 -0
- /kotonebot/kaa/sprites/{dbd537c7-d1d3-4d64-9ccd-43dd4519bedd.png → 379d5084-637f-45e8-8bd8-789eaa919821.png} +0 -0
- /kotonebot/kaa/sprites/{29cb7b0f-5684-4516-8fae-4439f7f28e08.png → 3851303a-e3a0-46eb-aa7e-b47fb41946f7.png} +0 -0
- /kotonebot/kaa/sprites/{25910d8e-d57f-409f-b0be-ee68c5af4920.png → 3d1aa368-e198-478f-a47f-49818efbaec8.png} +0 -0
- /kotonebot/kaa/sprites/{cb24b3df-1ec3-4ffd-95f1-ca4b0dd5b7da.png → 3f5907a9-7fe4-4f46-8e46-a96ac7fa3923.png} +0 -0
- /kotonebot/kaa/sprites/{c5e16f75-9b5a-4853-a144-275f3d20c186.png → 40c0f296-d29e-4e05-aaff-a212339b742e.png} +0 -0
- /kotonebot/kaa/sprites/{5d3d61e9-bf13-4668-b0c7-fed730ea2ed6.png → 43e72796-a644-4d9b-a817-8add011d9a75.png} +0 -0
- /kotonebot/kaa/sprites/{9c7f1bcc-0bc8-454b-95fb-56a803030771.png → 469c2b41-4171-4902-acc5-da0c807886ab.png} +0 -0
- /kotonebot/kaa/sprites/{65d2a5e5-48aa-4390-bb87-bfd8e044e4f6.png → 497e7930-f423-457a-8168-b1a1532bb1e9.png} +0 -0
- /kotonebot/kaa/sprites/{0ad2f603-a3ad-4a92-a398-c411145fd8e9.png → 49a43af3-17e3-4434-ba4d-519775ccaea9.png} +0 -0
- /kotonebot/kaa/sprites/{bfec58df-2628-454f-92aa-769cad186448.png → 49b9a298-e847-4322-99a2-c0588c3542fe.png} +0 -0
- /kotonebot/kaa/sprites/{a3b15425-0bfd-4668-a3a9-2ab64da8505f.png → 4a1902aa-01db-46bb-bea5-88ac62c82562.png} +0 -0
- /kotonebot/kaa/sprites/{66172bc0-3869-4a48-a26d-0dc3fa404ab8.png → 4a96a728-6303-4ae3-8e0c-9f1e8e5f86a7.png} +0 -0
- /kotonebot/kaa/sprites/{8b9c115b-d4ce-492e-88cb-08bd01c43570.png → 4b7c4221-a679-47f6-929d-0ef514894b27.png} +0 -0
- /kotonebot/kaa/sprites/{0d4b669e-5e43-4458-b65a-5565797335ae.png → 4d355e36-9169-43e4-bd58-2836ee3e56e0.png} +0 -0
- /kotonebot/kaa/sprites/{969eb98c-f761-4007-b8bd-2b3fb8d1f08c.png → 4eb97d68-cd47-4100-9c3b-35b5c86b4bdf.png} +0 -0
- /kotonebot/kaa/sprites/{eae83523-e58c-4db9-a356-4335a5da3dc8.png → 515a2ff0-36b8-4f2f-9902-e6c1b9796586.png} +0 -0
- /kotonebot/kaa/sprites/{9aef1aa8-41e1-42a7-83ad-b4e212ee3976.png → 51a3ed29-92b9-4429-9e50-783d1b8dc6a7.png} +0 -0
- /kotonebot/kaa/sprites/{83f17b06-2792-4035-bc90-1800279ae131.png → 52d3438b-afae-4c7a-abe5-7ed530c7558c.png} +0 -0
- /kotonebot/kaa/sprites/{9d5dd50c-3341-41a4-a3dd-ccc00edcf201.png → 540ee3b5-ca8f-4091-80a6-ce774d95e5f7.png} +0 -0
- /kotonebot/kaa/sprites/{17a8b0a5-5b71-4612-8a68-16c51f03c71b.png → 54a15298-ecaf-4cc5-b8a2-5b0d8f8add41.png} +0 -0
- /kotonebot/kaa/sprites/{7cabdf91-2f15-4991-b165-eb9cdd557af6.png → 554c0ff6-0a05-419d-85f9-5ced260d0d2e.png} +0 -0
- /kotonebot/kaa/sprites/{bed9575d-ccad-4f4e-825e-47b2a9cb0925.png → 55e4ef75-ba8e-4230-819c-450cf14b08cd.png} +0 -0
- /kotonebot/kaa/sprites/{4e8f3e59-edfa-4523-939d-bc46988fc909.png → 5824ef5f-6776-402c-b78c-ae79cc4cc878.png} +0 -0
- /kotonebot/kaa/sprites/{1ccdcacf-4f7d-442c-8e2f-09aa7fe14e56.png → 5a393483-c631-47fd-805c-c460c23b8f9e.png} +0 -0
- /kotonebot/kaa/sprites/{ba759205-ce99-4417-8cab-d32fe55dff4c.png → 5b75cae4-a853-402c-acec-edbfef94342a.png} +0 -0
- /kotonebot/kaa/sprites/{a1853223-d6b9-4660-af4e-91e59bdddc3c.png → 5c9ae62c-488f-4109-a4d2-d2385c32043d.png} +0 -0
- /kotonebot/kaa/sprites/{f72bd3d6-850d-4190-9775-938f474d263c.png → 5cdd71be-c8cd-4310-b6f4-5e97ddbeb46e.png} +0 -0
- /kotonebot/kaa/sprites/{4eaa5db2-d624-4df4-b2e4-01f72755f1a4.png → 5d5cb13b-c1fd-4808-b352-b21291a9ff0b.png} +0 -0
- /kotonebot/kaa/sprites/{52f7c8b1-af88-43c5-a999-de4da3caa03a.png → 5d6c4462-3d7c-42eb-ab3e-195ebf845a09.png} +0 -0
- /kotonebot/kaa/sprites/{7aa2435c-da90-4d52-b831-88dbec57dde9.png → 5d94e628-82ff-4531-b70a-b13297491bea.png} +0 -0
- /kotonebot/kaa/sprites/{0fbcb3f6-9ca0-43c7-975b-e7c15c26c897.png → 5dc2dca3-48bd-4eee-af89-1a1d30593bd5.png} +0 -0
- /kotonebot/kaa/sprites/{f611bbd9-c062-4989-96a4-65ff6c59673c.png → 5f0c2a57-efd0-4094-9f71-6f6fe97316f6.png} +0 -0
- /kotonebot/kaa/sprites/{d5d854fc-1b87-43d4-b32d-66eccb66fa41.png → 610348f2-6262-4cd1-8fc3-e7470f85580c.png} +0 -0
- /kotonebot/kaa/sprites/{a8b3fdce-9e4a-495c-839e-0142f67fee69.png → 613e9b10-b2eb-44ea-b6d9-9310c0e1bdcf.png} +0 -0
- /kotonebot/kaa/sprites/{aa3b717f-67f8-445f-a6bf-b89f7200d95b.png → 618b21ad-ab8b-436a-8eb8-bd320a198c9f.png} +0 -0
- /kotonebot/kaa/sprites/{7516c9a1-87ec-4d2f-ac5e-40d1aec27104.png → 61a09df6-7443-436c-9ef0-1d5a1cae1509.png} +0 -0
- /kotonebot/kaa/sprites/{0235ae3d-1741-4a81-8053-f60bc395ee85.png → 64be274e-e8ad-4938-84c1-5d8aabcd10d8.png} +0 -0
- /kotonebot/kaa/sprites/{b01c25d2-21bf-4db9-b5b2-ec35817f1b04.png → 671b4a55-023a-434d-b29a-e844fe528e6b.png} +0 -0
- /kotonebot/kaa/sprites/{302fde7d-de85-44c7-ba57-fe4b5f6db5fa.png → 6e5e9cf9-80b0-4241-a222-ff50dbd89a0c.png} +0 -0
- /kotonebot/kaa/sprites/{080b3cf5-ce35-4382-9bfa-d6a12efb09b3.png → 6ef59649-bbb3-42b6-a6b0-eac4f35a3da4.png} +0 -0
- /kotonebot/kaa/sprites/{15465879-eb84-4ca6-ae0f-d3c5ac71e723.png → 75a2093d-8743-427e-8d24-ae9a69fa14c1.png} +0 -0
- /kotonebot/kaa/sprites/{e5eeb9af-8003-4981-bf56-2e32e76cec5d.png → 76bde19a-4b1b-4e52-86d9-f56be7529c71.png} +0 -0
- /kotonebot/kaa/sprites/{1bb00a74-2fdc-4973-84ae-dc1edee15f81.png → 77d06f4d-8214-4306-9dc5-035c720f7256.png} +0 -0
- /kotonebot/kaa/sprites/{efb28e1d-7b52-4e20-aec5-117c798b7fbe.png → 78b642d3-9a2c-4c17-8872-3cc78211f4a0.png} +0 -0
- /kotonebot/kaa/sprites/{cdbbb5ba-5117-488a-8172-48cfc92c14c6.png → 7a9d1c39-b306-4cb0-be2f-7a5963099949.png} +0 -0
- /kotonebot/kaa/sprites/{f6bfbf6a-6e53-4563-9345-3f3756d6ea9a.png → 7ddda828-c8b0-4331-864f-99b85b7c5557.png} +0 -0
- /kotonebot/kaa/sprites/{b1e7f24e-f246-406e-9073-58e2f3e4e819.png → 81399fd9-f254-4d0e-9775-ad451c237a4e.png} +0 -0
- /kotonebot/kaa/sprites/{5152e3fa-079c-4592-8df7-656101694df8.png → 81526e97-1b8d-433f-a5c9-907e4e8c85cd.png} +0 -0
- /kotonebot/kaa/sprites/{09fb5e41-6caa-4246-a415-8317b12d8d81.png → 818e2370-8fb9-4e6c-ab78-58e86d6525b5.png} +0 -0
- /kotonebot/kaa/sprites/{0a6c6b1e-7a80-428e-a483-1858bfb8ac59.png → 8431cc52-0041-4425-808c-dcd4d196506c.png} +0 -0
- /kotonebot/kaa/sprites/{6aa34982-e7ab-4001-808f-8c5cf370d087.png → 85a49ff3-6075-4797-8b7f-35851a843698.png} +0 -0
- /kotonebot/kaa/sprites/{f528ea0e-1e55-4e41-844f-bb1db9185ef8.png → 86062e91-d6c1-44cf-9f8c-26a8b9dd8686.png} +0 -0
- /kotonebot/kaa/sprites/{26580772-ff20-4dcf-8496-f939e0f2890d.png → 860c3d98-e263-482f-95fb-4fc9c472e8b8.png} +0 -0
- /kotonebot/kaa/sprites/{a1bba683-a740-4eaf-8c37-3b1042b963b0.png → 8669ce44-dec7-47c2-aafd-3f2fbfd1d083.png} +0 -0
- /kotonebot/kaa/sprites/{8e48af2b-c083-4480-a27c-c59c31a7ede1.png → 86b15801-c932-4034-bed8-fd76b0f65916.png} +0 -0
- /kotonebot/kaa/sprites/{3cc9685d-6347-4114-9d39-7683c2dc9df6.png → 893b015d-6894-4e74-880e-d22859a25662.png} +0 -0
- /kotonebot/kaa/sprites/{66678caa-7b25-4ff7-8c3a-017f67279349.png → 8c0fa5fa-cebb-4ae7-bd1c-b542c9cbf694.png} +0 -0
- /kotonebot/kaa/sprites/{e9c45a73-2011-4584-87b7-c4e566eba87f.png → 913a1d17-54fe-4c8f-ac50-ef7f39600b86.png} +0 -0
- /kotonebot/kaa/sprites/{d60a395b-d907-416a-b091-36bfee4948b3.png → 964c6b90-f526-4199-9523-d8124373a56b.png} +0 -0
- /kotonebot/kaa/sprites/{69b022a8-e42f-4985-bd16-b4a530ad8a20.png → 97c281a9-ebb0-4f54-b644-b7ed7dd65f81.png} +0 -0
- /kotonebot/kaa/sprites/{b65724b4-ec8f-48a9-9ab2-e7ce5b8f35b2.png → 998fc84a-af71-4fa5-8aa4-e0bfeb9c53ea.png} +0 -0
- /kotonebot/kaa/sprites/{b8aada91-eedd-4c53-99f3-f903d547235e.png → 9cbf7f6e-fe77-463b-9c60-495dc823510a.png} +0 -0
- /kotonebot/kaa/sprites/{038c69e6-7cb0-4c8d-99da-b537e48a86b7.png → 9ce7d48e-295d-47b2-96eb-f3ce697c1b37.png} +0 -0
- /kotonebot/kaa/sprites/{cf66ae12-48bc-4512-a988-0e5de337c76d.png → 9dcca89b-c92b-4708-8f19-facd40d09a65.png} +0 -0
- /kotonebot/kaa/sprites/{264e056f-3bcc-4b16-94d4-f767c78ab6f5.png → 9e369992-ea8c-4c15-88d3-1344319978b0.png} +0 -0
- /kotonebot/kaa/sprites/{1aaceb21-dc5c-4a58-96d3-d2c66b2fb357.png → 9f9003ba-468f-406f-a82f-1b7a23ef347c.png} +0 -0
- /kotonebot/kaa/sprites/{1639278d-67e0-4452-a2b5-ec4cf4011f42.png → a2bf4e11-fbc3-4172-bc0b-b16f8f59e68c.png} +0 -0
- /kotonebot/kaa/sprites/{4a310bdd-0a7d-4669-9d57-6ae69bc8f022.png → a546e254-0490-43df-a291-a9a5c2b4c34a.png} +0 -0
- /kotonebot/kaa/sprites/{abcc709c-1f4d-4832-a063-eafbad94ac31.png → aacd229c-d307-4a20-a388-84d8f4afb4a5.png} +0 -0
- /kotonebot/kaa/sprites/{84bb4986-0295-4a45-9486-86e31fcdf1f1.png → b003bef5-53db-4032-b1bd-d5075de365a8.png} +0 -0
- /kotonebot/kaa/sprites/{d4c47cc7-5b3e-4c3e-8e80-78cb94e7eed9.png → b641a122-4734-43e1-b22f-468464a0cff1.png} +0 -0
- /kotonebot/kaa/sprites/{83acbca8-3567-4262-a1e7-50f158348d45.png → bc7303f3-43e9-4ece-9206-5f9c779b813b.png} +0 -0
- /kotonebot/kaa/sprites/{534d1310-3503-46d3-8ecd-426d9bcf183e.png → bd7a6754-fffc-4315-987f-5ba32d95b697.png} +0 -0
- /kotonebot/kaa/sprites/{8f1e5644-1079-44b1-9d44-97bae3817bc5.png → c39f2006-442b-4ab7-99bb-3dfa0673f05d.png} +0 -0
- /kotonebot/kaa/sprites/{f025027f-daa0-4943-8395-5d7d44cc7f15.png → c70f4634-6821-4480-a615-b5f9cdc578c1.png} +0 -0
- /kotonebot/kaa/sprites/{a7da51c2-8444-4770-a16b-b6543e40197f.png → c774098e-d782-455c-80a9-5bad3713129d.png} +0 -0
- /kotonebot/kaa/sprites/{60be53a6-98e9-4fc6-9285-85506dc0e335.png → c7e3736a-0e0f-474a-9386-f58292a45b92.png} +0 -0
- /kotonebot/kaa/sprites/{d3e6e2e9-c5b5-463f-98f4-de72c9320d9d.png → c90b582c-8570-48b4-98c0-62d5b067ca2c.png} +0 -0
- /kotonebot/kaa/sprites/{5868ebfb-5e61-4ac6-b270-acb1e89b738f.png → c9d3699b-c8b1-4294-9b48-103df4ab6c65.png} +0 -0
- /kotonebot/kaa/sprites/{17414e9e-b046-49e0-9596-cad1f4673a9e.png → cc9483c3-c895-4dcc-b3f2-0934ba1ad42e.png} +0 -0
- /kotonebot/kaa/sprites/{dad4ff8c-07f4-4b07-abd3-5d02bbcc4562.png → cec49286-3d48-4fe0-8911-6a1b17d1ac20.png} +0 -0
- /kotonebot/kaa/sprites/{7b7dad18-a838-4267-b3c7-6ab43b8361f0.png → cef78712-d9ae-4d6e-8d7d-d047d48fbb98.png} +0 -0
- /kotonebot/kaa/sprites/{ad172750-d1a6-4a25-8ce8-9483f4b5affb.png → d06fd859-4678-4593-b096-c86b38810cb7.png} +0 -0
- /kotonebot/kaa/sprites/{448ea37d-a10d-4102-ac9f-a58403ee8914.png → d19c37ff-d5c1-4316-8424-e69ccb5c2523.png} +0 -0
- /kotonebot/kaa/sprites/{ca2adfb8-1e88-4a31-b3cc-1f70568ce4a3.png → d26b60f7-d15c-4563-bedc-dd6e7e4078b2.png} +0 -0
- /kotonebot/kaa/sprites/{a1cf80ee-64bf-4d54-a7db-ffadf3c97026.png → d26bf52c-cb1b-4ebf-9c03-ff3f2d245959.png} +0 -0
- /kotonebot/kaa/sprites/{2dd34393-ee6f-4b19-8a9c-76556d1bd7d7.png → d5fd6c73-4ad8-47b5-89be-c7b137a9950f.png} +0 -0
- /kotonebot/kaa/sprites/{4b20b364-5f5f-4b34-960e-b0cfd08b1662.png → d696062b-9b58-4af1-835e-2cf5a0a0fbeb.png} +0 -0
- /kotonebot/kaa/sprites/{e1e0d751-9006-4fa4-a933-4c0dc5e1f3d2.png → da7a329e-339e-4ee7-8775-bca505a1980d.png} +0 -0
- /kotonebot/kaa/sprites/{dde03640-6c3a-4b13-a783-f672816bfec4.png → dedb8499-5881-4add-914f-ff0049285dae.png} +0 -0
- /kotonebot/kaa/sprites/{83cf48ba-d1af-4674-a52e-6bb80a626db7.png → dedea21b-f21d-4621-a213-70ad03faec58.png} +0 -0
- /kotonebot/kaa/sprites/{e394ff91-09d3-41e1-bbf5-bc42a71a623b.png → dff74cce-ba2d-44aa-b924-183259080a32.png} +0 -0
- /kotonebot/kaa/sprites/{f12c2b0b-8273-4197-a8c6-2a8ef2788a9b.png → e0246b0d-f0cf-49d4-8423-7da46e75a575.png} +0 -0
- /kotonebot/kaa/sprites/{1f2981d8-5eb6-4332-aa9b-17f4db2d4163.png → e1b6b32b-c797-4f1d-aa2b-e7320083a9a6.png} +0 -0
- /kotonebot/kaa/sprites/{6114d53e-dd69-4cee-82a7-7e43b338ff8d.png → e23e3f19-03f6-4507-9128-115191272b90.png} +0 -0
- /kotonebot/kaa/sprites/{90def878-cd6e-46e5-9dc4-9c92dd904b80.png → e2eb3bae-6451-40b9-bbff-90f11dca1904.png} +0 -0
- /kotonebot/kaa/sprites/{1d5f7454-672d-4e87-a368-ec4bb32f9b1f.png → e46e1b6d-6702-46bf-a07e-aec8d183453f.png} +0 -0
- /kotonebot/kaa/sprites/{e5259dfb-3a49-4cfc-9f89-b42958ce6eec.png → e5297036-9934-44a4-a761-a6a49ae06e3e.png} +0 -0
- /kotonebot/kaa/sprites/{2d145ade-b926-4276-b872-baec4ef78308.png → e7da5988-0e83-44ad-888b-246365473f9e.png} +0 -0
- /kotonebot/kaa/sprites/{c3ec61aa-f62e-46fd-b359-48864ea7a150.png → eb8d208a-dbcc-43ca-a8ac-098e1005ebfa.png} +0 -0
- /kotonebot/kaa/sprites/{9cd7e1f1-13c4-471e-abee-223ffd9126cb.png → ebe4ce64-98a9-40f2-9fde-3d5cdae673c7.png} +0 -0
- /kotonebot/kaa/sprites/{5ee8e9a8-b133-4ab7-a52c-e7e4e1d1ce7b.png → ef2ad3df-3f89-4a48-8fc3-c70a8adc1044.png} +0 -0
- /kotonebot/kaa/sprites/{68838f92-5916-424e-a37e-47e0b3834eb3.png → f05e410d-15b8-4831-bcc1-4cd8af07c882.png} +0 -0
- /kotonebot/kaa/sprites/{2e00ac54-d23a-4d51-82f2-829c012552a6.png → f1fac13a-84f3-42bd-9b52-e0ba90bc5255.png} +0 -0
- /kotonebot/kaa/sprites/{5dd0cc39-4102-4f0f-8ad3-342bbf825977.png → f7c34ab6-c165-48b7-ad5a-0b60613ce1bc.png} +0 -0
- /kotonebot/kaa/sprites/{dfb14c98-f672-4906-80bd-d2a18bdd62cd.png → fa80b14e-953f-44bd-a4ad-99aefd276fd7.png} +0 -0
- /kotonebot/kaa/sprites/{a4075991-98da-4d06-bcde-28054cd124d2.png → fe35655e-701b-4081-8df8-c17a7252acef.png} +0 -0
- /kotonebot/kaa/sprites/{df97421e-4b74-430e-b3d1-ed9bac6918a7.png → fe853ac7-934e-4b4a-bcc7-b986e057dfcf.png} +0 -0
- /kotonebot/kaa/sprites/{1aae0ab3-f619-47e0-8bf6-3d3791f7d445.png → ffda716c-2903-4ec7-8c59-aab5b8ad10bb.png} +0 -0
- /kotonebot/kaa/sprites/{6156116e-56dd-4b41-a6de-ccec55dab7c8.png → fff33200-c947-49f2-ab86-3ac98937fa36.png} +0 -0
- /kotonebot/kaa/{clear_logs.py → tasks/clear_logs.py} +0 -0
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/WHEEL +0 -0
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/entry_points.txt +0 -0
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/licenses/LICENSE +0 -0
- {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/top_level.txt +0 -0
|
@@ -46,12 +46,10 @@ from kotonebot.backend.color import (
|
|
|
46
46
|
from kotonebot.backend.ocr import (
|
|
47
47
|
Ocr, OcrResult, OcrResultList, jp, en, StringMatchFunction
|
|
48
48
|
)
|
|
49
|
-
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
50
49
|
from kotonebot.config.manager import load_config, save_config
|
|
51
50
|
from kotonebot.config.base_config import UserConfig
|
|
52
51
|
from kotonebot.backend.core import Image, HintBox
|
|
53
52
|
from kotonebot.errors import KotonebotWarning
|
|
54
|
-
from kotonebot.client import DeviceImpl
|
|
55
53
|
from kotonebot.backend.preprocessor import PreprocessorProtocol
|
|
56
54
|
from kotonebot.primitives import Rect
|
|
57
55
|
|
kotonebot/client/__init__.py
CHANGED
kotonebot/client/device.py
CHANGED
|
@@ -9,6 +9,7 @@ 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
15
|
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable, AndroidCommandable, WindowsCommandable
|
|
@@ -78,6 +79,31 @@ class Device:
|
|
|
78
79
|
"""
|
|
79
80
|
设备平台名称。
|
|
80
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
|
+
"""
|
|
81
107
|
|
|
82
108
|
@property
|
|
83
109
|
def adb(self) -> AdbUtilsDevice:
|
|
@@ -89,6 +115,50 @@ class Device:
|
|
|
89
115
|
def adb(self, value: AdbUtilsDevice) -> None:
|
|
90
116
|
self._adb = value
|
|
91
117
|
|
|
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
|
+
|
|
92
162
|
@overload
|
|
93
163
|
def click(self) -> None:
|
|
94
164
|
"""
|
|
@@ -161,7 +231,12 @@ class Device:
|
|
|
161
231
|
logger.debug(f"Executing click hook before: ({x}, {y})")
|
|
162
232
|
x, y = hook(x, y)
|
|
163
233
|
logger.debug(f"Click hook before result: ({x}, {y})")
|
|
164
|
-
|
|
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 "")
|
|
165
240
|
from ..backend.context import ContextStackVars
|
|
166
241
|
if ContextStackVars.current() is not None:
|
|
167
242
|
image = ContextStackVars.ensure_current()._screenshot
|
|
@@ -169,9 +244,11 @@ class Device:
|
|
|
169
244
|
image = np.array([])
|
|
170
245
|
if image is not None and image.size > 0:
|
|
171
246
|
cv2.circle(image, (x, y), 10, (0, 0, 255), -1)
|
|
172
|
-
message = f"
|
|
247
|
+
message = f"Point: ({x}, {y})"
|
|
248
|
+
if self.target_resolution is not None:
|
|
249
|
+
message += f" physical: ({real_x}, {real_y})"
|
|
173
250
|
result("device.click", image, message)
|
|
174
|
-
self._touch.click(
|
|
251
|
+
self._touch.click(real_x, real_y)
|
|
175
252
|
|
|
176
253
|
def __click_point_tuple(self, point: Point) -> None:
|
|
177
254
|
self.click(point[0], point[1])
|
|
@@ -232,6 +309,10 @@ class Device:
|
|
|
232
309
|
"""
|
|
233
310
|
滑动屏幕
|
|
234
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)
|
|
235
316
|
self._touch.swipe(x1, y1, x2, y2, duration)
|
|
236
317
|
|
|
237
318
|
def swipe_scaled(self, x1: float, y1: float, x2: float, y2: float, duration: float|None = None) -> None:
|
|
@@ -258,6 +339,7 @@ class Device:
|
|
|
258
339
|
logger.debug("screenshot hook before returned image")
|
|
259
340
|
return img
|
|
260
341
|
img = self.screenshot_raw()
|
|
342
|
+
img = self.__scale_image(img)
|
|
261
343
|
if self.screenshot_hook_after is not None:
|
|
262
344
|
img = self.screenshot_hook_after(img)
|
|
263
345
|
return img
|
|
@@ -296,8 +378,15 @@ class Device:
|
|
|
296
378
|
`self.orientation` 属性默认为竖屏。如果需要自动检测,
|
|
297
379
|
调用 `self.detect_orientation()` 方法。
|
|
298
380
|
如果已知方向,也可以直接设置 `self.orientation` 属性。
|
|
381
|
+
|
|
382
|
+
即使设置了 `self.target_resolution`,返回的分辨率仍然是真实分辨率。
|
|
299
383
|
"""
|
|
300
|
-
|
|
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]
|
|
301
390
|
|
|
302
391
|
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
|
|
303
392
|
"""
|
|
@@ -307,6 +396,68 @@ class Device:
|
|
|
307
396
|
"""
|
|
308
397
|
return self._screenshot.detect_orientation()
|
|
309
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
|
+
|
|
310
461
|
|
|
311
462
|
class AndroidDevice(Device):
|
|
312
463
|
def __init__(self, adb_connection: AdbUtilsDevice | None = None) -> None:
|
|
@@ -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,22 +1,21 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
3
|
from psutil import process_iter
|
|
4
|
-
from .protocol import Instance, AdbHostConfig
|
|
5
|
-
from typing import ParamSpec, TypeVar
|
|
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 import
|
|
10
|
-
from
|
|
11
|
-
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
12
|
-
from kotonebot.client.implements.adb import AdbImplConfig
|
|
9
|
+
from kotonebot.client import Device
|
|
10
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
13
11
|
|
|
14
12
|
logger = logging.getLogger(__name__)
|
|
13
|
+
CustomRecipes = AdbRecipes
|
|
15
14
|
|
|
16
15
|
P = ParamSpec('P')
|
|
17
16
|
T = TypeVar('T')
|
|
18
17
|
|
|
19
|
-
class CustomInstance(Instance[AdbHostConfig]):
|
|
18
|
+
class CustomInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
20
19
|
def __init__(self, exe_path: str | None, emulator_args: str = "", *args, **kwargs):
|
|
21
20
|
super().__init__(*args, **kwargs)
|
|
22
21
|
self.exe_path: str | None = exe_path
|
|
@@ -69,24 +68,12 @@ class CustomInstance(Instance[AdbHostConfig]):
|
|
|
69
68
|
pass
|
|
70
69
|
|
|
71
70
|
@override
|
|
72
|
-
def create_device(self, impl:
|
|
71
|
+
def create_device(self, impl: CustomRecipes, host_config: AdbHostConfig) -> Device:
|
|
73
72
|
"""为自定义实例创建 Device。"""
|
|
74
73
|
if self.adb_port is None:
|
|
75
74
|
raise ValueError("ADB port is not set and is required.")
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
79
|
-
config = AdbImplConfig(
|
|
80
|
-
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
81
|
-
connect=True,
|
|
82
|
-
disconnect=True,
|
|
83
|
-
device_serial=self.adb_name,
|
|
84
|
-
timeout=host_config.timeout
|
|
85
|
-
)
|
|
86
|
-
impl = cast(AdbBasedImpl, impl) # make pylance happy
|
|
87
|
-
return create_device(impl, config)
|
|
88
|
-
else:
|
|
89
|
-
raise ValueError(f'Unsupported device implementation for Custom: {impl}')
|
|
76
|
+
return super().create_device(impl, host_config)
|
|
90
77
|
|
|
91
78
|
def __repr__(self) -> str:
|
|
92
79
|
return f'CustomInstance(#{self.id}# at "{self.exe_path}" with {self.adb_ip}:{self.adb_port})'
|
|
@@ -99,6 +86,25 @@ def _type_check(ins: Instance) -> CustomInstance:
|
|
|
99
86
|
def create(exe_path: str | None, adb_ip: str, adb_port: int, adb_name: str | None, emulator_args: str = "") -> CustomInstance:
|
|
100
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)
|
|
101
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']
|
|
102
108
|
|
|
103
109
|
if __name__ == '__main__':
|
|
104
110
|
ins = create(r'C:\Program Files\BlueStacks_nxt\HD-Player.exe', '127.0.0.1', 5555, '**emulator-name**')
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Literal
|
|
4
4
|
from functools import lru_cache
|
|
5
5
|
from typing_extensions import override
|
|
6
6
|
|
|
7
7
|
from kotonebot import logging
|
|
8
|
-
from kotonebot.client import
|
|
9
|
-
from kotonebot.client.device import Device
|
|
10
|
-
from kotonebot.client.registration import AdbBasedImpl, create_device
|
|
11
|
-
from kotonebot.client.implements.adb import AdbImplConfig
|
|
8
|
+
from kotonebot.client import Device
|
|
12
9
|
from kotonebot.util import Countdown, Interval
|
|
13
10
|
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
11
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin
|
|
14
12
|
|
|
15
13
|
logger = logging.getLogger(__name__)
|
|
14
|
+
LeidianRecipes = AdbRecipes
|
|
16
15
|
|
|
17
16
|
if os.name == 'nt':
|
|
18
17
|
from ...interop.win.reg import read_reg
|
|
@@ -21,7 +20,7 @@ else:
|
|
|
21
20
|
"""Stub for read_reg on non-Windows platforms."""
|
|
22
21
|
return default
|
|
23
22
|
|
|
24
|
-
class LeidianInstance(Instance[AdbHostConfig]):
|
|
23
|
+
class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
|
|
25
24
|
@copy_type(Instance.__init__)
|
|
26
25
|
def __init__(self, *args, **kwargs):
|
|
27
26
|
super().__init__(*args, **kwargs)
|
|
@@ -64,35 +63,23 @@ class LeidianInstance(Instance[AdbHostConfig]):
|
|
|
64
63
|
it = Interval(5)
|
|
65
64
|
while not cd.expired() and not self.running():
|
|
66
65
|
it.wait()
|
|
66
|
+
self.refresh()
|
|
67
67
|
if not self.running():
|
|
68
68
|
raise TimeoutError(f'Leidian instance "{self.name}" is not available.')
|
|
69
69
|
|
|
70
70
|
@override
|
|
71
71
|
def running(self) -> bool:
|
|
72
|
-
|
|
73
|
-
return result.strip() == 'running'
|
|
72
|
+
return self.is_running
|
|
74
73
|
|
|
75
74
|
@override
|
|
76
|
-
def create_device(self, impl:
|
|
75
|
+
def create_device(self, impl: LeidianRecipes, host_config: AdbHostConfig) -> Device:
|
|
77
76
|
"""为雷电模拟器实例创建 Device。"""
|
|
78
77
|
if self.adb_port is None:
|
|
79
78
|
raise ValueError("ADB port is not set and is required.")
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
if impl in ['adb', 'adb_raw', 'uiautomator2']:
|
|
83
|
-
config = AdbImplConfig(
|
|
84
|
-
addr=f'{self.adb_ip}:{self.adb_port}',
|
|
85
|
-
connect=False, # 雷电模拟器不需要 adb connect
|
|
86
|
-
disconnect=False,
|
|
87
|
-
device_serial=self.adb_name,
|
|
88
|
-
timeout=host_config.timeout
|
|
89
|
-
)
|
|
90
|
-
impl = cast(AdbBasedImpl, impl) # make pylance happy
|
|
91
|
-
return create_device(impl, config)
|
|
92
|
-
else:
|
|
93
|
-
raise ValueError(f'Unsupported device implementation for Leidian: {impl}')
|
|
80
|
+
return super().create_device(impl, host_config)
|
|
94
81
|
|
|
95
|
-
class LeidianHost(HostProtocol):
|
|
82
|
+
class LeidianHost(HostProtocol[LeidianRecipes]):
|
|
96
83
|
@staticmethod
|
|
97
84
|
@lru_cache(maxsize=1)
|
|
98
85
|
def _read_install_path() -> str | None:
|
|
@@ -197,6 +184,10 @@ class LeidianHost(HostProtocol):
|
|
|
197
184
|
return instance
|
|
198
185
|
return None
|
|
199
186
|
|
|
187
|
+
@staticmethod
|
|
188
|
+
def recipes() -> 'list[LeidianRecipes]':
|
|
189
|
+
return ['adb', 'adb_raw', 'uiautomator2']
|
|
190
|
+
|
|
200
191
|
if __name__ == '__main__':
|
|
201
192
|
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
202
193
|
print(LeidianHost._read_install_path())
|
|
@@ -1,18 +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.
|
|
11
|
-
from kotonebot.client.implements.adb 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
|
|
12
14
|
from kotonebot.util import Countdown, Interval
|
|
13
15
|
from .protocol import HostProtocol, Instance, copy_type, AdbHostConfig
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger(__name__)
|
|
16
|
+
from .adb_common import AdbRecipes, CommonAdbCreateDeviceMixin, connect_adb, is_adb_recipe
|
|
16
17
|
|
|
17
18
|
if os.name == 'nt':
|
|
18
19
|
from ...interop.win.reg import read_reg
|
|
@@ -21,7 +22,21 @@ else:
|
|
|
21
22
|
"""Stub for read_reg on non-Windows platforms."""
|
|
22
23
|
return default
|
|
23
24
|
|
|
24
|
-
|
|
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]):
|
|
25
40
|
@copy_type(Instance.__init__)
|
|
26
41
|
def __init__(self, *args, **kwargs):
|
|
27
42
|
super().__init__(*args, **kwargs)
|
|
@@ -72,34 +87,58 @@ class Mumu12Instance(Instance[AdbHostConfig]):
|
|
|
72
87
|
def running(self) -> bool:
|
|
73
88
|
return self.is_android_started
|
|
74
89
|
|
|
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
|
+
|
|
75
95
|
@override
|
|
76
|
-
def create_device(self,
|
|
96
|
+
def create_device(self, recipe: MuMu12Recipes, host_config: MuMu12HostConfig | AdbHostConfig) -> Device:
|
|
77
97
|
"""为MuMu12模拟器实例创建 Device。"""
|
|
78
98
|
if self.adb_port is None:
|
|
79
99
|
raise ValueError("ADB port is not set and is required.")
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
112
|
)
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
129
|
else:
|
|
93
|
-
raise ValueError(f'
|
|
130
|
+
raise ValueError(f'Unknown recipe: {recipe}')
|
|
94
131
|
|
|
95
|
-
class Mumu12Host(HostProtocol):
|
|
132
|
+
class Mumu12Host(HostProtocol[MuMu12Recipes]):
|
|
96
133
|
@staticmethod
|
|
97
134
|
@lru_cache(maxsize=1)
|
|
98
135
|
def _read_install_path() -> str | None:
|
|
99
|
-
"""
|
|
100
|
-
|
|
136
|
+
r"""
|
|
137
|
+
从注册表中读取 MuMu Player 12 的安装路径。
|
|
101
138
|
|
|
102
|
-
|
|
139
|
+
返回的路径为根目录。如 `F:\Apps\Netease\MuMuPlayer-12.0`。
|
|
140
|
+
|
|
141
|
+
:return: 若找到,则返回安装路径;否则返回 None。
|
|
103
142
|
"""
|
|
104
143
|
if os.name != 'nt':
|
|
105
144
|
return None
|
|
@@ -116,6 +155,9 @@ class Mumu12Host(HostProtocol):
|
|
|
116
155
|
icon_path = icon_path.replace('"', '')
|
|
117
156
|
path = os.path.dirname(icon_path)
|
|
118
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)
|
|
119
161
|
return path
|
|
120
162
|
return None
|
|
121
163
|
|
|
@@ -130,7 +172,7 @@ class Mumu12Host(HostProtocol):
|
|
|
130
172
|
install_path = Mumu12Host._read_install_path()
|
|
131
173
|
if install_path is None:
|
|
132
174
|
raise RuntimeError('MuMu Player 12 is not installed.')
|
|
133
|
-
manager_path = os.path.join(install_path, 'MuMuManager.exe')
|
|
175
|
+
manager_path = os.path.join(install_path, 'shell', 'MuMuManager.exe')
|
|
134
176
|
logger.debug('MuMuManager execute: %s', repr(args))
|
|
135
177
|
output = subprocess.run(
|
|
136
178
|
[manager_path] + args,
|
|
@@ -184,6 +226,10 @@ class Mumu12Host(HostProtocol):
|
|
|
184
226
|
if instance.id == id:
|
|
185
227
|
return instance
|
|
186
228
|
return None
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def recipes() -> 'list[MuMu12Recipes]':
|
|
232
|
+
return ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc']
|
|
187
233
|
|
|
188
234
|
if __name__ == '__main__':
|
|
189
235
|
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
|
|
@@ -195,7 +195,8 @@ class Instance(Generic[T_HostConfig], ABC):
|
|
|
195
195
|
def __repr__(self) -> str:
|
|
196
196
|
return f'{self.__class__.__name__}(name="{self.name}", id="{self.id}", adb="{self.adb_ip}:{self.adb_port}"({self.adb_name}))'
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
Recipe = TypeVar('Recipe', bound=str)
|
|
199
|
+
class HostProtocol(Generic[Recipe], Protocol):
|
|
199
200
|
@staticmethod
|
|
200
201
|
def installed() -> bool: ...
|
|
201
202
|
|
|
@@ -205,6 +206,8 @@ class HostProtocol(Protocol):
|
|
|
205
206
|
@staticmethod
|
|
206
207
|
def query(*, id: str) -> Instance | None: ...
|
|
207
208
|
|
|
209
|
+
@staticmethod
|
|
210
|
+
def recipes() -> 'list[Recipe]': ...
|
|
208
211
|
|
|
209
212
|
if __name__ == '__main__':
|
|
210
213
|
pass
|