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.
Files changed (167) hide show
  1. kotonebot/backend/context/context.py +0 -2
  2. kotonebot/client/__init__.py +1 -2
  3. kotonebot/client/device.py +155 -4
  4. kotonebot/client/host/adb_common.py +94 -0
  5. kotonebot/client/host/custom.py +27 -21
  6. kotonebot/client/host/leidian_host.py +14 -23
  7. kotonebot/client/host/mumu12_host.py +70 -24
  8. kotonebot/client/host/protocol.py +4 -1
  9. kotonebot/client/host/windows_common.py +55 -0
  10. kotonebot/client/implements/__init__.py +2 -1
  11. kotonebot/client/implements/adb.py +1 -44
  12. kotonebot/client/implements/adb_raw.py +2 -9
  13. kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
  14. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
  15. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
  16. kotonebot/client/implements/remote_windows.py +8 -14
  17. kotonebot/client/implements/uiautomator2.py +5 -16
  18. kotonebot/client/implements/windows.py +5 -36
  19. kotonebot/client/registration.py +2 -67
  20. kotonebot/config/base_config.py +4 -2
  21. kotonebot/debug_entry.py +4 -2
  22. kotonebot/errors.py +7 -0
  23. kotonebot/kaa/main/dmm_host.py +13 -32
  24. kotonebot/kaa/main/gr.py +114 -27
  25. kotonebot/kaa/main/kaa.py +30 -3
  26. kotonebot/kaa/metadata.py +32 -0
  27. kotonebot/kaa/resources/__pycache__/__init__.cpython-310.pyc +0 -0
  28. kotonebot/kaa/tasks/R.py +132 -132
  29. {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/METADATA +4 -1
  30. {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/RECORD +167 -162
  31. /kotonebot/kaa/sprites/{7f6cb60e-6ab3-4db4-86e1-8558250062a4.png → 019273c4-22ce-486f-8a7d-dbc78f13dcac.png} +0 -0
  32. /kotonebot/kaa/sprites/{bb0ae68d-44d5-47c6-91dd-a529bc34225b.png → 03e34ef7-36b5-4ddc-9545-5b70a2eba72e.png} +0 -0
  33. /kotonebot/kaa/sprites/{faf360e8-7845-4fcf-b0a6-0f5f93c050f1.png → 06ea773b-e2b3-40d4-82f6-b119ce9ca00b.png} +0 -0
  34. /kotonebot/kaa/sprites/{2f74b04f-1992-4584-8dc6-e789b081ddc3.png → 0769f8f4-1c47-4bad-a783-bba5e20250bf.png} +0 -0
  35. /kotonebot/kaa/sprites/{58a5f51d-d1a3-480b-8dde-ecead34d9f02.png → 08946997-4e9e-4242-95e1-e988fa96b919.png} +0 -0
  36. /kotonebot/kaa/sprites/{694d3a07-9697-4bda-984d-967779d530f7.png → 0d60ce10-20ce-4980-8172-b6bac08d6258.png} +0 -0
  37. /kotonebot/kaa/sprites/{1b6be6dd-2711-441a-9fa6-3281a6ad10c7.png → 0f40352f-6443-406e-8e2c-c40d5cfc6c59.png} +0 -0
  38. /kotonebot/kaa/sprites/{94433cf2-dd04-4b74-ae14-e5e3d945f579.png → 0f6d7f89-bdb2-4235-9b6b-e65cb73a4167.png} +0 -0
  39. /kotonebot/kaa/sprites/{9f99b0e9-8cc0-4877-8f8a-d26257ef5386.png → 14d09c44-3897-4c81-b615-19c35d0f3793.png} +0 -0
  40. /kotonebot/kaa/sprites/{b5d4265c-82e0-470d-a48e-8aa4edbdef5a.png → 15191dec-7e33-4721-8068-15c11978e885.png} +0 -0
  41. /kotonebot/kaa/sprites/{42d8b5dc-ef27-4411-b8b5-d981e562990b.png → 15f8f31e-65ce-408d-8556-b799bcefc9d4.png} +0 -0
  42. /kotonebot/kaa/sprites/{d1699baa-467b-46d1-9276-aae78fe8f589.png → 20b01af5-907b-4d49-9f5b-a3700ca9c0c3.png} +0 -0
  43. /kotonebot/kaa/sprites/{faf069e1-f623-47c1-b879-cfa246fbc7df.png → 242c088b-bc73-4d11-aa31-ee7c71477fb4.png} +0 -0
  44. /kotonebot/kaa/sprites/{c25c7ce5-ad8d-4b2c-83eb-ae6d4871a6c3.png → 2f2a7951-a30a-4835-b9f6-3205cfdd2f05.png} +0 -0
  45. /kotonebot/kaa/sprites/{aefdec38-e3b6-4762-aed8-33107686ddac.png → 3031397e-3d12-4f23-8377-006eca5d2627.png} +0 -0
  46. /kotonebot/kaa/sprites/{ed6d3e3d-0f04-4fa4-b0f9-fdbf324649f6.png → 312dbd82-5457-474a-91ce-511f0e4e9c80.png} +0 -0
  47. /kotonebot/kaa/sprites/{c571e1c0-ff85-4b2e-97ed-97d33b8d92e3.png → 31851ad1-df39-4a4f-9ae7-19c1d1c6b51a.png} +0 -0
  48. /kotonebot/kaa/sprites/{c484ca57-6b19-46b5-a107-1805a7488d48.png → 338b699a-d556-4b80-a577-123442c3db45.png} +0 -0
  49. /kotonebot/kaa/sprites/{920a3974-4875-4d2b-bbac-caa175c03ce0.png → 35319890-98e3-4f75-b969-6b8576ef668d.png} +0 -0
  50. /kotonebot/kaa/sprites/{f8eec9b2-55ca-4195-b098-a5393d02a98a.png → 3536f829-70d3-48f9-8fd4-b65eab7502f3.png} +0 -0
  51. /kotonebot/kaa/sprites/{97adee05-1586-4589-93e6-c6c3a9cdae9d.png → 35516b69-a4f4-4ad4-8af4-35043475ee90.png} +0 -0
  52. /kotonebot/kaa/sprites/{c5ea3dfd-6761-466e-b44c-86592ef05323.png → 36d71d33-09c9-4be8-b184-ccf40f700333.png} +0 -0
  53. /kotonebot/kaa/sprites/{dbd537c7-d1d3-4d64-9ccd-43dd4519bedd.png → 379d5084-637f-45e8-8bd8-789eaa919821.png} +0 -0
  54. /kotonebot/kaa/sprites/{29cb7b0f-5684-4516-8fae-4439f7f28e08.png → 3851303a-e3a0-46eb-aa7e-b47fb41946f7.png} +0 -0
  55. /kotonebot/kaa/sprites/{25910d8e-d57f-409f-b0be-ee68c5af4920.png → 3d1aa368-e198-478f-a47f-49818efbaec8.png} +0 -0
  56. /kotonebot/kaa/sprites/{cb24b3df-1ec3-4ffd-95f1-ca4b0dd5b7da.png → 3f5907a9-7fe4-4f46-8e46-a96ac7fa3923.png} +0 -0
  57. /kotonebot/kaa/sprites/{c5e16f75-9b5a-4853-a144-275f3d20c186.png → 40c0f296-d29e-4e05-aaff-a212339b742e.png} +0 -0
  58. /kotonebot/kaa/sprites/{5d3d61e9-bf13-4668-b0c7-fed730ea2ed6.png → 43e72796-a644-4d9b-a817-8add011d9a75.png} +0 -0
  59. /kotonebot/kaa/sprites/{9c7f1bcc-0bc8-454b-95fb-56a803030771.png → 469c2b41-4171-4902-acc5-da0c807886ab.png} +0 -0
  60. /kotonebot/kaa/sprites/{65d2a5e5-48aa-4390-bb87-bfd8e044e4f6.png → 497e7930-f423-457a-8168-b1a1532bb1e9.png} +0 -0
  61. /kotonebot/kaa/sprites/{0ad2f603-a3ad-4a92-a398-c411145fd8e9.png → 49a43af3-17e3-4434-ba4d-519775ccaea9.png} +0 -0
  62. /kotonebot/kaa/sprites/{bfec58df-2628-454f-92aa-769cad186448.png → 49b9a298-e847-4322-99a2-c0588c3542fe.png} +0 -0
  63. /kotonebot/kaa/sprites/{a3b15425-0bfd-4668-a3a9-2ab64da8505f.png → 4a1902aa-01db-46bb-bea5-88ac62c82562.png} +0 -0
  64. /kotonebot/kaa/sprites/{66172bc0-3869-4a48-a26d-0dc3fa404ab8.png → 4a96a728-6303-4ae3-8e0c-9f1e8e5f86a7.png} +0 -0
  65. /kotonebot/kaa/sprites/{8b9c115b-d4ce-492e-88cb-08bd01c43570.png → 4b7c4221-a679-47f6-929d-0ef514894b27.png} +0 -0
  66. /kotonebot/kaa/sprites/{0d4b669e-5e43-4458-b65a-5565797335ae.png → 4d355e36-9169-43e4-bd58-2836ee3e56e0.png} +0 -0
  67. /kotonebot/kaa/sprites/{969eb98c-f761-4007-b8bd-2b3fb8d1f08c.png → 4eb97d68-cd47-4100-9c3b-35b5c86b4bdf.png} +0 -0
  68. /kotonebot/kaa/sprites/{eae83523-e58c-4db9-a356-4335a5da3dc8.png → 515a2ff0-36b8-4f2f-9902-e6c1b9796586.png} +0 -0
  69. /kotonebot/kaa/sprites/{9aef1aa8-41e1-42a7-83ad-b4e212ee3976.png → 51a3ed29-92b9-4429-9e50-783d1b8dc6a7.png} +0 -0
  70. /kotonebot/kaa/sprites/{83f17b06-2792-4035-bc90-1800279ae131.png → 52d3438b-afae-4c7a-abe5-7ed530c7558c.png} +0 -0
  71. /kotonebot/kaa/sprites/{9d5dd50c-3341-41a4-a3dd-ccc00edcf201.png → 540ee3b5-ca8f-4091-80a6-ce774d95e5f7.png} +0 -0
  72. /kotonebot/kaa/sprites/{17a8b0a5-5b71-4612-8a68-16c51f03c71b.png → 54a15298-ecaf-4cc5-b8a2-5b0d8f8add41.png} +0 -0
  73. /kotonebot/kaa/sprites/{7cabdf91-2f15-4991-b165-eb9cdd557af6.png → 554c0ff6-0a05-419d-85f9-5ced260d0d2e.png} +0 -0
  74. /kotonebot/kaa/sprites/{bed9575d-ccad-4f4e-825e-47b2a9cb0925.png → 55e4ef75-ba8e-4230-819c-450cf14b08cd.png} +0 -0
  75. /kotonebot/kaa/sprites/{4e8f3e59-edfa-4523-939d-bc46988fc909.png → 5824ef5f-6776-402c-b78c-ae79cc4cc878.png} +0 -0
  76. /kotonebot/kaa/sprites/{1ccdcacf-4f7d-442c-8e2f-09aa7fe14e56.png → 5a393483-c631-47fd-805c-c460c23b8f9e.png} +0 -0
  77. /kotonebot/kaa/sprites/{ba759205-ce99-4417-8cab-d32fe55dff4c.png → 5b75cae4-a853-402c-acec-edbfef94342a.png} +0 -0
  78. /kotonebot/kaa/sprites/{a1853223-d6b9-4660-af4e-91e59bdddc3c.png → 5c9ae62c-488f-4109-a4d2-d2385c32043d.png} +0 -0
  79. /kotonebot/kaa/sprites/{f72bd3d6-850d-4190-9775-938f474d263c.png → 5cdd71be-c8cd-4310-b6f4-5e97ddbeb46e.png} +0 -0
  80. /kotonebot/kaa/sprites/{4eaa5db2-d624-4df4-b2e4-01f72755f1a4.png → 5d5cb13b-c1fd-4808-b352-b21291a9ff0b.png} +0 -0
  81. /kotonebot/kaa/sprites/{52f7c8b1-af88-43c5-a999-de4da3caa03a.png → 5d6c4462-3d7c-42eb-ab3e-195ebf845a09.png} +0 -0
  82. /kotonebot/kaa/sprites/{7aa2435c-da90-4d52-b831-88dbec57dde9.png → 5d94e628-82ff-4531-b70a-b13297491bea.png} +0 -0
  83. /kotonebot/kaa/sprites/{0fbcb3f6-9ca0-43c7-975b-e7c15c26c897.png → 5dc2dca3-48bd-4eee-af89-1a1d30593bd5.png} +0 -0
  84. /kotonebot/kaa/sprites/{f611bbd9-c062-4989-96a4-65ff6c59673c.png → 5f0c2a57-efd0-4094-9f71-6f6fe97316f6.png} +0 -0
  85. /kotonebot/kaa/sprites/{d5d854fc-1b87-43d4-b32d-66eccb66fa41.png → 610348f2-6262-4cd1-8fc3-e7470f85580c.png} +0 -0
  86. /kotonebot/kaa/sprites/{a8b3fdce-9e4a-495c-839e-0142f67fee69.png → 613e9b10-b2eb-44ea-b6d9-9310c0e1bdcf.png} +0 -0
  87. /kotonebot/kaa/sprites/{aa3b717f-67f8-445f-a6bf-b89f7200d95b.png → 618b21ad-ab8b-436a-8eb8-bd320a198c9f.png} +0 -0
  88. /kotonebot/kaa/sprites/{7516c9a1-87ec-4d2f-ac5e-40d1aec27104.png → 61a09df6-7443-436c-9ef0-1d5a1cae1509.png} +0 -0
  89. /kotonebot/kaa/sprites/{0235ae3d-1741-4a81-8053-f60bc395ee85.png → 64be274e-e8ad-4938-84c1-5d8aabcd10d8.png} +0 -0
  90. /kotonebot/kaa/sprites/{b01c25d2-21bf-4db9-b5b2-ec35817f1b04.png → 671b4a55-023a-434d-b29a-e844fe528e6b.png} +0 -0
  91. /kotonebot/kaa/sprites/{302fde7d-de85-44c7-ba57-fe4b5f6db5fa.png → 6e5e9cf9-80b0-4241-a222-ff50dbd89a0c.png} +0 -0
  92. /kotonebot/kaa/sprites/{080b3cf5-ce35-4382-9bfa-d6a12efb09b3.png → 6ef59649-bbb3-42b6-a6b0-eac4f35a3da4.png} +0 -0
  93. /kotonebot/kaa/sprites/{15465879-eb84-4ca6-ae0f-d3c5ac71e723.png → 75a2093d-8743-427e-8d24-ae9a69fa14c1.png} +0 -0
  94. /kotonebot/kaa/sprites/{e5eeb9af-8003-4981-bf56-2e32e76cec5d.png → 76bde19a-4b1b-4e52-86d9-f56be7529c71.png} +0 -0
  95. /kotonebot/kaa/sprites/{1bb00a74-2fdc-4973-84ae-dc1edee15f81.png → 77d06f4d-8214-4306-9dc5-035c720f7256.png} +0 -0
  96. /kotonebot/kaa/sprites/{efb28e1d-7b52-4e20-aec5-117c798b7fbe.png → 78b642d3-9a2c-4c17-8872-3cc78211f4a0.png} +0 -0
  97. /kotonebot/kaa/sprites/{cdbbb5ba-5117-488a-8172-48cfc92c14c6.png → 7a9d1c39-b306-4cb0-be2f-7a5963099949.png} +0 -0
  98. /kotonebot/kaa/sprites/{f6bfbf6a-6e53-4563-9345-3f3756d6ea9a.png → 7ddda828-c8b0-4331-864f-99b85b7c5557.png} +0 -0
  99. /kotonebot/kaa/sprites/{b1e7f24e-f246-406e-9073-58e2f3e4e819.png → 81399fd9-f254-4d0e-9775-ad451c237a4e.png} +0 -0
  100. /kotonebot/kaa/sprites/{5152e3fa-079c-4592-8df7-656101694df8.png → 81526e97-1b8d-433f-a5c9-907e4e8c85cd.png} +0 -0
  101. /kotonebot/kaa/sprites/{09fb5e41-6caa-4246-a415-8317b12d8d81.png → 818e2370-8fb9-4e6c-ab78-58e86d6525b5.png} +0 -0
  102. /kotonebot/kaa/sprites/{0a6c6b1e-7a80-428e-a483-1858bfb8ac59.png → 8431cc52-0041-4425-808c-dcd4d196506c.png} +0 -0
  103. /kotonebot/kaa/sprites/{6aa34982-e7ab-4001-808f-8c5cf370d087.png → 85a49ff3-6075-4797-8b7f-35851a843698.png} +0 -0
  104. /kotonebot/kaa/sprites/{f528ea0e-1e55-4e41-844f-bb1db9185ef8.png → 86062e91-d6c1-44cf-9f8c-26a8b9dd8686.png} +0 -0
  105. /kotonebot/kaa/sprites/{26580772-ff20-4dcf-8496-f939e0f2890d.png → 860c3d98-e263-482f-95fb-4fc9c472e8b8.png} +0 -0
  106. /kotonebot/kaa/sprites/{a1bba683-a740-4eaf-8c37-3b1042b963b0.png → 8669ce44-dec7-47c2-aafd-3f2fbfd1d083.png} +0 -0
  107. /kotonebot/kaa/sprites/{8e48af2b-c083-4480-a27c-c59c31a7ede1.png → 86b15801-c932-4034-bed8-fd76b0f65916.png} +0 -0
  108. /kotonebot/kaa/sprites/{3cc9685d-6347-4114-9d39-7683c2dc9df6.png → 893b015d-6894-4e74-880e-d22859a25662.png} +0 -0
  109. /kotonebot/kaa/sprites/{66678caa-7b25-4ff7-8c3a-017f67279349.png → 8c0fa5fa-cebb-4ae7-bd1c-b542c9cbf694.png} +0 -0
  110. /kotonebot/kaa/sprites/{e9c45a73-2011-4584-87b7-c4e566eba87f.png → 913a1d17-54fe-4c8f-ac50-ef7f39600b86.png} +0 -0
  111. /kotonebot/kaa/sprites/{d60a395b-d907-416a-b091-36bfee4948b3.png → 964c6b90-f526-4199-9523-d8124373a56b.png} +0 -0
  112. /kotonebot/kaa/sprites/{69b022a8-e42f-4985-bd16-b4a530ad8a20.png → 97c281a9-ebb0-4f54-b644-b7ed7dd65f81.png} +0 -0
  113. /kotonebot/kaa/sprites/{b65724b4-ec8f-48a9-9ab2-e7ce5b8f35b2.png → 998fc84a-af71-4fa5-8aa4-e0bfeb9c53ea.png} +0 -0
  114. /kotonebot/kaa/sprites/{b8aada91-eedd-4c53-99f3-f903d547235e.png → 9cbf7f6e-fe77-463b-9c60-495dc823510a.png} +0 -0
  115. /kotonebot/kaa/sprites/{038c69e6-7cb0-4c8d-99da-b537e48a86b7.png → 9ce7d48e-295d-47b2-96eb-f3ce697c1b37.png} +0 -0
  116. /kotonebot/kaa/sprites/{cf66ae12-48bc-4512-a988-0e5de337c76d.png → 9dcca89b-c92b-4708-8f19-facd40d09a65.png} +0 -0
  117. /kotonebot/kaa/sprites/{264e056f-3bcc-4b16-94d4-f767c78ab6f5.png → 9e369992-ea8c-4c15-88d3-1344319978b0.png} +0 -0
  118. /kotonebot/kaa/sprites/{1aaceb21-dc5c-4a58-96d3-d2c66b2fb357.png → 9f9003ba-468f-406f-a82f-1b7a23ef347c.png} +0 -0
  119. /kotonebot/kaa/sprites/{1639278d-67e0-4452-a2b5-ec4cf4011f42.png → a2bf4e11-fbc3-4172-bc0b-b16f8f59e68c.png} +0 -0
  120. /kotonebot/kaa/sprites/{4a310bdd-0a7d-4669-9d57-6ae69bc8f022.png → a546e254-0490-43df-a291-a9a5c2b4c34a.png} +0 -0
  121. /kotonebot/kaa/sprites/{abcc709c-1f4d-4832-a063-eafbad94ac31.png → aacd229c-d307-4a20-a388-84d8f4afb4a5.png} +0 -0
  122. /kotonebot/kaa/sprites/{84bb4986-0295-4a45-9486-86e31fcdf1f1.png → b003bef5-53db-4032-b1bd-d5075de365a8.png} +0 -0
  123. /kotonebot/kaa/sprites/{d4c47cc7-5b3e-4c3e-8e80-78cb94e7eed9.png → b641a122-4734-43e1-b22f-468464a0cff1.png} +0 -0
  124. /kotonebot/kaa/sprites/{83acbca8-3567-4262-a1e7-50f158348d45.png → bc7303f3-43e9-4ece-9206-5f9c779b813b.png} +0 -0
  125. /kotonebot/kaa/sprites/{534d1310-3503-46d3-8ecd-426d9bcf183e.png → bd7a6754-fffc-4315-987f-5ba32d95b697.png} +0 -0
  126. /kotonebot/kaa/sprites/{8f1e5644-1079-44b1-9d44-97bae3817bc5.png → c39f2006-442b-4ab7-99bb-3dfa0673f05d.png} +0 -0
  127. /kotonebot/kaa/sprites/{f025027f-daa0-4943-8395-5d7d44cc7f15.png → c70f4634-6821-4480-a615-b5f9cdc578c1.png} +0 -0
  128. /kotonebot/kaa/sprites/{a7da51c2-8444-4770-a16b-b6543e40197f.png → c774098e-d782-455c-80a9-5bad3713129d.png} +0 -0
  129. /kotonebot/kaa/sprites/{60be53a6-98e9-4fc6-9285-85506dc0e335.png → c7e3736a-0e0f-474a-9386-f58292a45b92.png} +0 -0
  130. /kotonebot/kaa/sprites/{d3e6e2e9-c5b5-463f-98f4-de72c9320d9d.png → c90b582c-8570-48b4-98c0-62d5b067ca2c.png} +0 -0
  131. /kotonebot/kaa/sprites/{5868ebfb-5e61-4ac6-b270-acb1e89b738f.png → c9d3699b-c8b1-4294-9b48-103df4ab6c65.png} +0 -0
  132. /kotonebot/kaa/sprites/{17414e9e-b046-49e0-9596-cad1f4673a9e.png → cc9483c3-c895-4dcc-b3f2-0934ba1ad42e.png} +0 -0
  133. /kotonebot/kaa/sprites/{dad4ff8c-07f4-4b07-abd3-5d02bbcc4562.png → cec49286-3d48-4fe0-8911-6a1b17d1ac20.png} +0 -0
  134. /kotonebot/kaa/sprites/{7b7dad18-a838-4267-b3c7-6ab43b8361f0.png → cef78712-d9ae-4d6e-8d7d-d047d48fbb98.png} +0 -0
  135. /kotonebot/kaa/sprites/{ad172750-d1a6-4a25-8ce8-9483f4b5affb.png → d06fd859-4678-4593-b096-c86b38810cb7.png} +0 -0
  136. /kotonebot/kaa/sprites/{448ea37d-a10d-4102-ac9f-a58403ee8914.png → d19c37ff-d5c1-4316-8424-e69ccb5c2523.png} +0 -0
  137. /kotonebot/kaa/sprites/{ca2adfb8-1e88-4a31-b3cc-1f70568ce4a3.png → d26b60f7-d15c-4563-bedc-dd6e7e4078b2.png} +0 -0
  138. /kotonebot/kaa/sprites/{a1cf80ee-64bf-4d54-a7db-ffadf3c97026.png → d26bf52c-cb1b-4ebf-9c03-ff3f2d245959.png} +0 -0
  139. /kotonebot/kaa/sprites/{2dd34393-ee6f-4b19-8a9c-76556d1bd7d7.png → d5fd6c73-4ad8-47b5-89be-c7b137a9950f.png} +0 -0
  140. /kotonebot/kaa/sprites/{4b20b364-5f5f-4b34-960e-b0cfd08b1662.png → d696062b-9b58-4af1-835e-2cf5a0a0fbeb.png} +0 -0
  141. /kotonebot/kaa/sprites/{e1e0d751-9006-4fa4-a933-4c0dc5e1f3d2.png → da7a329e-339e-4ee7-8775-bca505a1980d.png} +0 -0
  142. /kotonebot/kaa/sprites/{dde03640-6c3a-4b13-a783-f672816bfec4.png → dedb8499-5881-4add-914f-ff0049285dae.png} +0 -0
  143. /kotonebot/kaa/sprites/{83cf48ba-d1af-4674-a52e-6bb80a626db7.png → dedea21b-f21d-4621-a213-70ad03faec58.png} +0 -0
  144. /kotonebot/kaa/sprites/{e394ff91-09d3-41e1-bbf5-bc42a71a623b.png → dff74cce-ba2d-44aa-b924-183259080a32.png} +0 -0
  145. /kotonebot/kaa/sprites/{f12c2b0b-8273-4197-a8c6-2a8ef2788a9b.png → e0246b0d-f0cf-49d4-8423-7da46e75a575.png} +0 -0
  146. /kotonebot/kaa/sprites/{1f2981d8-5eb6-4332-aa9b-17f4db2d4163.png → e1b6b32b-c797-4f1d-aa2b-e7320083a9a6.png} +0 -0
  147. /kotonebot/kaa/sprites/{6114d53e-dd69-4cee-82a7-7e43b338ff8d.png → e23e3f19-03f6-4507-9128-115191272b90.png} +0 -0
  148. /kotonebot/kaa/sprites/{90def878-cd6e-46e5-9dc4-9c92dd904b80.png → e2eb3bae-6451-40b9-bbff-90f11dca1904.png} +0 -0
  149. /kotonebot/kaa/sprites/{1d5f7454-672d-4e87-a368-ec4bb32f9b1f.png → e46e1b6d-6702-46bf-a07e-aec8d183453f.png} +0 -0
  150. /kotonebot/kaa/sprites/{e5259dfb-3a49-4cfc-9f89-b42958ce6eec.png → e5297036-9934-44a4-a761-a6a49ae06e3e.png} +0 -0
  151. /kotonebot/kaa/sprites/{2d145ade-b926-4276-b872-baec4ef78308.png → e7da5988-0e83-44ad-888b-246365473f9e.png} +0 -0
  152. /kotonebot/kaa/sprites/{c3ec61aa-f62e-46fd-b359-48864ea7a150.png → eb8d208a-dbcc-43ca-a8ac-098e1005ebfa.png} +0 -0
  153. /kotonebot/kaa/sprites/{9cd7e1f1-13c4-471e-abee-223ffd9126cb.png → ebe4ce64-98a9-40f2-9fde-3d5cdae673c7.png} +0 -0
  154. /kotonebot/kaa/sprites/{5ee8e9a8-b133-4ab7-a52c-e7e4e1d1ce7b.png → ef2ad3df-3f89-4a48-8fc3-c70a8adc1044.png} +0 -0
  155. /kotonebot/kaa/sprites/{68838f92-5916-424e-a37e-47e0b3834eb3.png → f05e410d-15b8-4831-bcc1-4cd8af07c882.png} +0 -0
  156. /kotonebot/kaa/sprites/{2e00ac54-d23a-4d51-82f2-829c012552a6.png → f1fac13a-84f3-42bd-9b52-e0ba90bc5255.png} +0 -0
  157. /kotonebot/kaa/sprites/{5dd0cc39-4102-4f0f-8ad3-342bbf825977.png → f7c34ab6-c165-48b7-ad5a-0b60613ce1bc.png} +0 -0
  158. /kotonebot/kaa/sprites/{dfb14c98-f672-4906-80bd-d2a18bdd62cd.png → fa80b14e-953f-44bd-a4ad-99aefd276fd7.png} +0 -0
  159. /kotonebot/kaa/sprites/{a4075991-98da-4d06-bcde-28054cd124d2.png → fe35655e-701b-4081-8df8-c17a7252acef.png} +0 -0
  160. /kotonebot/kaa/sprites/{df97421e-4b74-430e-b3d1-ed9bac6918a7.png → fe853ac7-934e-4b4a-bcc7-b986e057dfcf.png} +0 -0
  161. /kotonebot/kaa/sprites/{1aae0ab3-f619-47e0-8bf6-3d3791f7d445.png → ffda716c-2903-4ec7-8c59-aab5b8ad10bb.png} +0 -0
  162. /kotonebot/kaa/sprites/{6156116e-56dd-4b41-a6de-ccec55dab7c8.png → fff33200-c947-49f2-ab86-3ac98937fa36.png} +0 -0
  163. /kotonebot/kaa/{clear_logs.py → tasks/clear_logs.py} +0 -0
  164. {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/WHEEL +0 -0
  165. {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/entry_points.txt +0 -0
  166. {ksaa-2025.6.23.0.dist-info → ksaa-2025.6.28.0.dist-info}/licenses/LICENSE +0 -0
  167. {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
 
@@ -1,11 +1,10 @@
1
1
  from .device import Device
2
- from .registration import create_device, DeviceImpl
2
+ from .registration import DeviceImpl
3
3
 
4
4
  # 确保所有实现都被注册
5
5
  from . import implements # noqa: F401
6
6
 
7
7
  __all__ = [
8
8
  'Device',
9
- 'create_device',
10
9
  'DeviceImpl',
11
10
  ]
@@ -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
- logger.debug(f"Click: {x}, {y}")
234
+ if self.target_resolution is not None:
235
+ # 输入坐标为逻辑坐标,需要转换为真实坐标
236
+ real_x, real_y = self._scale_pos_target_to_real(x, y)
237
+ else:
238
+ real_x, real_y = x, y
239
+ logger.debug(f"Click: {x}, {y}%s", f"(Physical: {real_x}, {real_y})" if self.target_resolution is not None else "")
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"point: ({x}, {y})"
247
+ message = f"Point: ({x}, {y})"
248
+ if self.target_resolution is not None:
249
+ message += f" physical: ({real_x}, {real_y})"
173
250
  result("device.click", image, message)
174
- self._touch.click(x, y)
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
- return self._screenshot.screen_size
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
@@ -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, cast
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 DeviceImpl
10
- from kotonebot.client.device import Device
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: DeviceImpl, host_config: AdbHostConfig) -> Device:
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
- # ADB 相关的实现创建配置
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 cast
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 DeviceImpl
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
- result = LeidianHost._invoke_manager(['isrunning', '--index', str(self.index)])
73
- return result.strip() == 'running'
72
+ return self.is_running
74
73
 
75
74
  @override
76
- def create_device(self, impl: DeviceImpl, host_config: AdbHostConfig) -> Device:
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
- # ADB 相关的实现创建配置
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, cast
6
+ from typing import Any, Literal, overload
6
7
  from typing_extensions import override
7
8
 
8
9
  from kotonebot import logging
9
- from kotonebot.client import DeviceImpl, Device
10
- from kotonebot.client.registration import AdbBasedImpl, create_device
11
- from kotonebot.client.implements.adb import AdbImplConfig
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
- class Mumu12Instance(Instance[AdbHostConfig]):
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, impl: DeviceImpl, host_config: AdbHostConfig) -> Device:
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
- # ADB 相关的实现创建配置
82
- if impl in ['adb', 'adb_raw', 'uiautomator2']:
83
- config = AdbImplConfig(
84
- addr=f'{self.adb_ip}:{self.adb_port}',
85
- connect=True,
86
- disconnect=True,
87
- device_serial=self.adb_name,
88
- timeout=host_config.timeout
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
- impl = cast(AdbBasedImpl, impl) # make pylance happy
91
- return create_device(impl, config)
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'Unsupported device implementation for MuMu12: {impl}')
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
- Reads the installation path (DisplayIcon) of MuMu Player 12 from the registry.
136
+ r"""
137
+ 从注册表中读取 MuMu Player 12 的安装路径。
101
138
 
102
- :return: The path to the display icon if found, otherwise None.
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
- class HostProtocol(Protocol):
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