kotonebot 0.5.0__py3-none-any.whl → 0.7.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 (107) 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/context.py +1002 -1002
  6. kotonebot/backend/context/task_action.py +183 -183
  7. kotonebot/backend/core.py +86 -129
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/image.py +36 -5
  15. kotonebot/backend/loop.py +222 -208
  16. kotonebot/backend/ocr.py +535 -535
  17. kotonebot/backend/preprocessor.py +103 -103
  18. kotonebot/client/__init__.py +9 -9
  19. kotonebot/client/device.py +369 -529
  20. kotonebot/client/fast_screenshot.py +377 -377
  21. kotonebot/client/host/__init__.py +43 -43
  22. kotonebot/client/host/adb_common.py +101 -107
  23. kotonebot/client/host/custom.py +118 -118
  24. kotonebot/client/host/leidian_host.py +196 -196
  25. kotonebot/client/host/mumu12_host.py +353 -353
  26. kotonebot/client/host/protocol.py +214 -214
  27. kotonebot/client/host/windows_common.py +73 -58
  28. kotonebot/client/implements/__init__.py +65 -70
  29. kotonebot/client/implements/adb.py +89 -89
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -188
  34. kotonebot/client/implements/uiautomator2.py +85 -85
  35. kotonebot/client/implements/windows/__init__.py +1 -0
  36. kotonebot/client/implements/windows/print_window.py +133 -0
  37. kotonebot/client/implements/windows/send_message.py +324 -0
  38. kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
  39. kotonebot/client/protocol.py +69 -69
  40. kotonebot/client/registration.py +24 -24
  41. kotonebot/client/scaler.py +467 -0
  42. kotonebot/config/base_config.py +103 -96
  43. kotonebot/config/config.py +61 -0
  44. kotonebot/config/manager.py +36 -36
  45. kotonebot/core/__init__.py +13 -0
  46. kotonebot/core/entities/base.py +182 -0
  47. kotonebot/core/entities/compound.py +75 -0
  48. kotonebot/core/entities/ocr.py +117 -0
  49. kotonebot/core/entities/template_match.py +198 -0
  50. kotonebot/devtools/__init__.py +42 -0
  51. kotonebot/devtools/cli/__init__.py +6 -0
  52. kotonebot/devtools/cli/main.py +53 -0
  53. kotonebot/{tools → devtools}/mirror.py +354 -354
  54. kotonebot/devtools/project/project.py +41 -0
  55. kotonebot/devtools/project/scanner.py +202 -0
  56. kotonebot/devtools/project/schema.py +99 -0
  57. kotonebot/devtools/resgen/__init__.py +42 -0
  58. kotonebot/devtools/resgen/codegen.py +331 -0
  59. kotonebot/devtools/resgen/core.py +94 -0
  60. kotonebot/devtools/resgen/parsers.py +360 -0
  61. kotonebot/devtools/resgen/utils.py +158 -0
  62. kotonebot/devtools/resgen/validation.py +115 -0
  63. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  64. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  65. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  66. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  67. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  68. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  69. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  70. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  71. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  72. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  73. kotonebot/devtools/web/dist/index.html +25 -0
  74. kotonebot/devtools/web/server/__init__.py +0 -0
  75. kotonebot/devtools/web/server/rest_api.py +217 -0
  76. kotonebot/devtools/web/server/server.py +85 -0
  77. kotonebot/errors.py +76 -76
  78. kotonebot/interop/win/__init__.py +13 -9
  79. kotonebot/interop/win/_mouse.py +310 -310
  80. kotonebot/interop/win/message_box.py +313 -313
  81. kotonebot/interop/win/reg.py +37 -37
  82. kotonebot/interop/win/shake_mouse.py +224 -0
  83. kotonebot/interop/win/shortcut.py +43 -43
  84. kotonebot/interop/win/task_dialog.py +513 -513
  85. kotonebot/interop/win/window.py +89 -0
  86. kotonebot/logging/__init__.py +2 -2
  87. kotonebot/logging/log.py +17 -17
  88. kotonebot/primitives/__init__.py +19 -17
  89. kotonebot/primitives/geometry.py +1067 -862
  90. kotonebot/primitives/visual.py +143 -63
  91. kotonebot/ui/file_host/sensio.py +36 -36
  92. kotonebot/ui/file_host/tmp_send.py +54 -54
  93. kotonebot/ui/pushkit/__init__.py +3 -3
  94. kotonebot/ui/pushkit/image_host.py +88 -88
  95. kotonebot/ui/pushkit/protocol.py +13 -13
  96. kotonebot/ui/pushkit/wxpusher.py +54 -54
  97. kotonebot/ui/user.py +148 -148
  98. kotonebot/util.py +436 -436
  99. {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
  100. kotonebot-0.7.0.dist-info/RECORD +109 -0
  101. {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
  102. kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
  103. {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
  104. kotonebot/client/implements/adb_raw.py +0 -163
  105. kotonebot-0.5.0.dist-info/RECORD +0 -71
  106. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  107. {kotonebot-0.5.0.dist-info → kotonebot-0.7.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()