kotonebot 0.4.0__py3-none-any.whl → 0.5.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 (64) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/task_action.py +183 -183
  6. kotonebot/backend/core.py +129 -129
  7. kotonebot/backend/debug/entry.py +89 -89
  8. kotonebot/backend/debug/mock.py +78 -78
  9. kotonebot/backend/debug/server.py +222 -222
  10. kotonebot/backend/debug/vars.py +351 -351
  11. kotonebot/backend/dispatch.py +227 -227
  12. kotonebot/backend/flow_controller.py +196 -196
  13. kotonebot/backend/ocr.py +535 -529
  14. kotonebot/backend/preprocessor.py +103 -103
  15. kotonebot/client/__init__.py +9 -9
  16. kotonebot/client/device.py +528 -503
  17. kotonebot/client/fast_screenshot.py +377 -377
  18. kotonebot/client/host/__init__.py +43 -12
  19. kotonebot/client/host/adb_common.py +107 -103
  20. kotonebot/client/host/custom.py +118 -114
  21. kotonebot/client/host/leidian_host.py +196 -201
  22. kotonebot/client/host/mumu12_host.py +353 -358
  23. kotonebot/client/host/protocol.py +214 -213
  24. kotonebot/client/host/windows_common.py +58 -58
  25. kotonebot/client/implements/__init__.py +71 -15
  26. kotonebot/client/implements/adb.py +89 -85
  27. kotonebot/client/implements/adb_raw.py +162 -158
  28. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  29. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  30. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  31. kotonebot/client/implements/remote_windows.py +188 -188
  32. kotonebot/client/implements/uiautomator2.py +85 -81
  33. kotonebot/client/implements/windows.py +176 -172
  34. kotonebot/client/protocol.py +69 -69
  35. kotonebot/client/registration.py +24 -24
  36. kotonebot/config/base_config.py +96 -96
  37. kotonebot/config/manager.py +36 -36
  38. kotonebot/errors.py +76 -71
  39. kotonebot/interop/win/__init__.py +10 -3
  40. kotonebot/interop/win/_mouse.py +311 -0
  41. kotonebot/interop/win/message_box.py +313 -313
  42. kotonebot/interop/win/reg.py +37 -37
  43. kotonebot/interop/win/shortcut.py +43 -43
  44. kotonebot/interop/win/task_dialog.py +513 -513
  45. kotonebot/logging/__init__.py +2 -2
  46. kotonebot/logging/log.py +17 -17
  47. kotonebot/primitives/__init__.py +17 -17
  48. kotonebot/primitives/geometry.py +862 -290
  49. kotonebot/primitives/visual.py +63 -63
  50. kotonebot/tools/mirror.py +354 -354
  51. kotonebot/ui/file_host/sensio.py +36 -36
  52. kotonebot/ui/file_host/tmp_send.py +54 -54
  53. kotonebot/ui/pushkit/__init__.py +3 -3
  54. kotonebot/ui/pushkit/image_host.py +88 -87
  55. kotonebot/ui/pushkit/protocol.py +13 -13
  56. kotonebot/ui/pushkit/wxpusher.py +54 -53
  57. kotonebot/ui/user.py +148 -148
  58. kotonebot/util.py +436 -436
  59. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -81
  60. kotonebot-0.5.0.dist-info/RECORD +71 -0
  61. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  62. kotonebot-0.4.0.dist-info/RECORD +0 -70
  63. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  64. {kotonebot-0.4.0.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,223 +1,223 @@
1
- import time
2
- import asyncio
3
- import inspect
4
- import threading
5
- import traceback
6
- import subprocess
7
- from io import StringIO
8
- from pathlib import Path
9
- from typing import Literal
10
- from collections import deque
11
- from contextlib import redirect_stdout
12
-
13
- import cv2
14
- import uvicorn
15
- from thefuzz import fuzz
16
- from pydantic import BaseModel
17
- from fastapi.responses import FileResponse, Response
18
- from fastapi import FastAPI, WebSocket, HTTPException
19
- from fastapi.middleware.cors import CORSMiddleware
20
-
21
- import kotonebot
22
- import kotonebot.backend
23
- import kotonebot.backend.context
24
- from kotonebot.backend.core import HintBox, Image
25
- from ..context import manual_context
26
- from . import vars as debug_vars
27
- from .vars import WSImage, WSMessageData, WSMessage, WSCallstack
28
-
29
- app = FastAPI()
30
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
31
-
32
- # 获取当前文件夹路径
33
- CURRENT_DIR = Path(__file__).parent
34
-
35
- APP_DIR = Path.cwd()
36
-
37
- class File(BaseModel):
38
- name: str
39
- full_path: str
40
- type: Literal["file", "dir"]
41
-
42
- @app.get("/api/read_file")
43
- async def read_file(path: str):
44
- """读取文件内容"""
45
- try:
46
- # 确保路径在当前目录下
47
- full_path = (APP_DIR / path).resolve()
48
- if not Path(full_path).is_relative_to(APP_DIR):
49
- raise HTTPException(status_code=403, detail="Access denied")
50
-
51
- if not full_path.exists():
52
- raise HTTPException(status_code=404, detail="File not found")
53
- # 添加缓存控制头
54
- headers = {
55
- "Cache-Control": "public, max-age=3600", # 缓存1小时
56
- "ETag": f'"{hash(full_path)}"' # 使用full_path的哈希值作为ETag
57
- }
58
- return FileResponse(full_path, headers=headers)
59
- except Exception as e:
60
- raise HTTPException(status_code=500, detail=str(e))
61
-
62
- @app.get("/api/read_memory")
63
- async def read_memory(key: str):
64
- """读取内存中的数据"""
65
- try:
66
- image = None
67
- if (image := debug_vars._read_image(key)) is not None:
68
- pass
69
- else:
70
- raise HTTPException(status_code=404, detail="Key not found")
71
-
72
- # 编码图片
73
- encode_params = [cv2.IMWRITE_PNG_COMPRESSION, 4]
74
- _, buffer = cv2.imencode('.png', image, encode_params)
75
- # 添加缓存控制头
76
- headers = {
77
- "Cache-Control": "public, max-age=3600", # 缓存1小时
78
- "ETag": f'"{hash(key)}"' # 使用key的哈希值作为ETag
79
- }
80
- return Response(
81
- buffer.tobytes(),
82
- media_type="image/jpeg",
83
- headers=headers
84
- )
85
- except Exception as e:
86
- raise HTTPException(status_code=500, detail=str(e))
87
-
88
- @app.get("/api/screenshot")
89
- def screenshot():
90
- from ..context import device
91
- img = device.screenshot()
92
- buff = cv2.imencode('.png', img)[1].tobytes()
93
- return Response(buff, media_type="image/png")
94
-
95
- class RunCodeRequest(BaseModel):
96
- code: str
97
-
98
- @app.post("/api/code/run")
99
- async def run_code(request: RunCodeRequest):
100
- event = asyncio.Event()
101
- stdout = StringIO()
102
- code = f"from kotonebot import *\n" + request.code
103
- result = {}
104
- def _runner():
105
- nonlocal result
106
- from kotonebot.backend.context import vars as context_vars
107
- try:
108
- with manual_context():
109
- global_vars = dict(vars(kotonebot.backend.context))
110
- with redirect_stdout(stdout):
111
- exec(code, global_vars)
112
- result = {"status": "ok", "result": stdout.getvalue()}
113
- except (Exception) as e:
114
- result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
115
- except KeyboardInterrupt as e:
116
- result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
117
- finally:
118
- context_vars.flow.clear_interrupt()
119
- event.set()
120
- threading.Thread(target=_runner, daemon=True).start()
121
- await event.wait()
122
- return result
123
-
124
- @app.get("/api/code/stop")
125
- async def stop_code():
126
- from kotonebot.backend.context import vars
127
- vars.flow.request_interrupt()
128
- while vars.flow.is_interrupted:
129
- await asyncio.sleep(0.1)
130
- return {"status": "ok"}
131
-
132
- @app.get("/api/fs/list_dir")
133
- def list_dir(path: str) -> list[File]:
134
- result = []
135
- for item in Path(path).iterdir():
136
- result.append(File(
137
- name=item.name,
138
- full_path=str(item),
139
- type="file" if item.is_file() else "dir"
140
- ))
141
- return result
142
-
143
- @app.get("/api/resources/autocomplete")
144
- def autocomplete(class_path: str) -> list[str]:
145
- from kotonebot.kaa.tasks import R
146
- class_names = class_path.split(".")[:-1]
147
- target_class = R
148
- # 定位到目标类
149
- for name in class_names:
150
- target_class = getattr(target_class, name, None)
151
- if target_class is None:
152
- return []
153
- # 获取目标类的所有属性
154
- attrs = [attr for attr in dir(target_class) if not attr.startswith("_")]
155
- filtered_attrs = []
156
- for attr in attrs:
157
- if inspect.isclass(getattr(target_class, attr)):
158
- filtered_attrs.append(attr)
159
- elif isinstance(getattr(target_class, attr), (Image, HintBox)):
160
- filtered_attrs.append(attr)
161
- attrs = filtered_attrs
162
- # 排序
163
- attrs.sort(key=lambda x: fuzz.ratio(x, class_path), reverse=True)
164
- return attrs
165
-
166
- @app.get("/api/ping")
167
- async def ping():
168
- return {"status": "ok"}
169
-
170
- message_queue = deque()
171
- @app.websocket("/ws")
172
- async def websocket_endpoint(websocket: WebSocket):
173
- await websocket.accept()
174
- try:
175
- while True:
176
- if len(message_queue) > 0:
177
- message = message_queue.popleft()
178
- await websocket.send_json(message)
179
- await asyncio.sleep(0.1)
180
- except:
181
- await websocket.close()
182
-
183
- def send_ws_message(title: str, image: list[str], text: str = '', callstack: list[WSCallstack] = [], wait: bool = False):
184
- """发送 WebSocket 消息"""
185
- message = WSMessage(
186
- type="visual",
187
- data=WSMessageData(
188
- image=WSImage(type="memory", value=image),
189
- name=title,
190
- details=text,
191
- timestamp=int(time.time() * 1000),
192
- callstack=callstack
193
- )
194
- )
195
- message_queue.append(message.dict())
196
- if wait:
197
- while len(message_queue) > 0:
198
- time.sleep(0.3)
199
-
200
-
201
- thread = None
202
- def start_server():
203
- global thread
204
- def run_server():
205
- uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
206
- if thread is None:
207
- thread = threading.Thread(target=run_server, daemon=True)
208
- thread.start()
209
-
210
- def wait_message_all_done():
211
- global thread
212
- def _wait():
213
- while len(message_queue) > 0:
214
- time.sleep(0.1)
215
- if thread is not None:
216
- threading.Thread(target=_wait, daemon=True).start()
217
-
218
- if __name__ == "__main__":
219
- debug_vars.debug.hide_server_log = False
220
- process = subprocess.Popen(["pylsp", "--port", "5479", "--ws"])
221
- print("LSP started. PID=", process.pid)
222
- uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
1
+ import time
2
+ import asyncio
3
+ import inspect
4
+ import threading
5
+ import traceback
6
+ import subprocess
7
+ from io import StringIO
8
+ from pathlib import Path
9
+ from typing import Literal
10
+ from collections import deque
11
+ from contextlib import redirect_stdout
12
+
13
+ import cv2
14
+ import uvicorn
15
+ from thefuzz import fuzz
16
+ from pydantic import BaseModel
17
+ from fastapi.responses import FileResponse, Response
18
+ from fastapi import FastAPI, WebSocket, HTTPException
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+
21
+ import kotonebot
22
+ import kotonebot.backend
23
+ import kotonebot.backend.context
24
+ from kotonebot.backend.core import HintBox, Image
25
+ from ..context import manual_context
26
+ from . import vars as debug_vars
27
+ from .vars import WSImage, WSMessageData, WSMessage, WSCallstack
28
+
29
+ app = FastAPI()
30
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
31
+
32
+ # 获取当前文件夹路径
33
+ CURRENT_DIR = Path(__file__).parent
34
+
35
+ APP_DIR = Path.cwd()
36
+
37
+ class File(BaseModel):
38
+ name: str
39
+ full_path: str
40
+ type: Literal["file", "dir"]
41
+
42
+ @app.get("/api/read_file")
43
+ async def read_file(path: str):
44
+ """读取文件内容"""
45
+ try:
46
+ # 确保路径在当前目录下
47
+ full_path = (APP_DIR / path).resolve()
48
+ if not Path(full_path).is_relative_to(APP_DIR):
49
+ raise HTTPException(status_code=403, detail="Access denied")
50
+
51
+ if not full_path.exists():
52
+ raise HTTPException(status_code=404, detail="File not found")
53
+ # 添加缓存控制头
54
+ headers = {
55
+ "Cache-Control": "public, max-age=3600", # 缓存1小时
56
+ "ETag": f'"{hash(full_path)}"' # 使用full_path的哈希值作为ETag
57
+ }
58
+ return FileResponse(full_path, headers=headers)
59
+ except Exception as e:
60
+ raise HTTPException(status_code=500, detail=str(e))
61
+
62
+ @app.get("/api/read_memory")
63
+ async def read_memory(key: str):
64
+ """读取内存中的数据"""
65
+ try:
66
+ image = None
67
+ if (image := debug_vars._read_image(key)) is not None:
68
+ pass
69
+ else:
70
+ raise HTTPException(status_code=404, detail="Key not found")
71
+
72
+ # 编码图片
73
+ encode_params = [cv2.IMWRITE_PNG_COMPRESSION, 4]
74
+ _, buffer = cv2.imencode('.png', image, encode_params)
75
+ # 添加缓存控制头
76
+ headers = {
77
+ "Cache-Control": "public, max-age=3600", # 缓存1小时
78
+ "ETag": f'"{hash(key)}"' # 使用key的哈希值作为ETag
79
+ }
80
+ return Response(
81
+ buffer.tobytes(),
82
+ media_type="image/jpeg",
83
+ headers=headers
84
+ )
85
+ except Exception as e:
86
+ raise HTTPException(status_code=500, detail=str(e))
87
+
88
+ @app.get("/api/screenshot")
89
+ def screenshot():
90
+ from ..context import device
91
+ img = device.screenshot()
92
+ buff = cv2.imencode('.png', img)[1].tobytes()
93
+ return Response(buff, media_type="image/png")
94
+
95
+ class RunCodeRequest(BaseModel):
96
+ code: str
97
+
98
+ @app.post("/api/code/run")
99
+ async def run_code(request: RunCodeRequest):
100
+ event = asyncio.Event()
101
+ stdout = StringIO()
102
+ code = f"from kotonebot import *\n" + request.code
103
+ result = {}
104
+ def _runner():
105
+ nonlocal result
106
+ from kotonebot.backend.context import vars as context_vars
107
+ try:
108
+ with manual_context():
109
+ global_vars = dict(vars(kotonebot.backend.context))
110
+ with redirect_stdout(stdout):
111
+ exec(code, global_vars)
112
+ result = {"status": "ok", "result": stdout.getvalue()}
113
+ except (Exception) as e:
114
+ result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
115
+ except KeyboardInterrupt as e:
116
+ result = {"status": "error", "result": stdout.getvalue(), "message": str(e), "traceback": traceback.format_exc()}
117
+ finally:
118
+ context_vars.flow.clear_interrupt()
119
+ event.set()
120
+ threading.Thread(target=_runner, daemon=True).start()
121
+ await event.wait()
122
+ return result
123
+
124
+ @app.get("/api/code/stop")
125
+ async def stop_code():
126
+ from kotonebot.backend.context import vars
127
+ vars.flow.request_interrupt()
128
+ while vars.flow.is_interrupted:
129
+ await asyncio.sleep(0.1)
130
+ return {"status": "ok"}
131
+
132
+ @app.get("/api/fs/list_dir")
133
+ def list_dir(path: str) -> list[File]:
134
+ result = []
135
+ for item in Path(path).iterdir():
136
+ result.append(File(
137
+ name=item.name,
138
+ full_path=str(item),
139
+ type="file" if item.is_file() else "dir"
140
+ ))
141
+ return result
142
+
143
+ @app.get("/api/resources/autocomplete")
144
+ def autocomplete(class_path: str) -> list[str]:
145
+ from kotonebot.kaa.tasks import R
146
+ class_names = class_path.split(".")[:-1]
147
+ target_class = R
148
+ # 定位到目标类
149
+ for name in class_names:
150
+ target_class = getattr(target_class, name, None)
151
+ if target_class is None:
152
+ return []
153
+ # 获取目标类的所有属性
154
+ attrs = [attr for attr in dir(target_class) if not attr.startswith("_")]
155
+ filtered_attrs = []
156
+ for attr in attrs:
157
+ if inspect.isclass(getattr(target_class, attr)):
158
+ filtered_attrs.append(attr)
159
+ elif isinstance(getattr(target_class, attr), (Image, HintBox)):
160
+ filtered_attrs.append(attr)
161
+ attrs = filtered_attrs
162
+ # 排序
163
+ attrs.sort(key=lambda x: fuzz.ratio(x, class_path), reverse=True)
164
+ return attrs
165
+
166
+ @app.get("/api/ping")
167
+ async def ping():
168
+ return {"status": "ok"}
169
+
170
+ message_queue = deque()
171
+ @app.websocket("/ws")
172
+ async def websocket_endpoint(websocket: WebSocket):
173
+ await websocket.accept()
174
+ try:
175
+ while True:
176
+ if len(message_queue) > 0:
177
+ message = message_queue.popleft()
178
+ await websocket.send_json(message)
179
+ await asyncio.sleep(0.1)
180
+ except:
181
+ await websocket.close()
182
+
183
+ def send_ws_message(title: str, image: list[str], text: str = '', callstack: list[WSCallstack] = [], wait: bool = False):
184
+ """发送 WebSocket 消息"""
185
+ message = WSMessage(
186
+ type="visual",
187
+ data=WSMessageData(
188
+ image=WSImage(type="memory", value=image),
189
+ name=title,
190
+ details=text,
191
+ timestamp=int(time.time() * 1000),
192
+ callstack=callstack
193
+ )
194
+ )
195
+ message_queue.append(message.dict())
196
+ if wait:
197
+ while len(message_queue) > 0:
198
+ time.sleep(0.3)
199
+
200
+
201
+ thread = None
202
+ def start_server():
203
+ global thread
204
+ def run_server():
205
+ uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
206
+ if thread is None:
207
+ thread = threading.Thread(target=run_server, daemon=True)
208
+ thread.start()
209
+
210
+ def wait_message_all_done():
211
+ global thread
212
+ def _wait():
213
+ while len(message_queue) > 0:
214
+ time.sleep(0.1)
215
+ if thread is not None:
216
+ threading.Thread(target=_wait, daemon=True).start()
217
+
218
+ if __name__ == "__main__":
219
+ debug_vars.debug.hide_server_log = False
220
+ process = subprocess.Popen(["pylsp", "--port", "5479", "--ws"])
221
+ print("LSP started. PID=", process.pid)
222
+ uvicorn.run(app, host="127.0.0.1", port=8000, log_level='critical' if debug_vars.debug.hide_server_log else None)
223
223
  process.kill()