kotonebot 0.3.1__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.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -302
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +49 -56
- kotonebot/backend/context/task_action.py +183 -175
- kotonebot/backend/core.py +129 -126
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/loop.py +12 -88
- kotonebot/backend/ocr.py +535 -529
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +528 -502
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -12
- kotonebot/client/host/adb_common.py +107 -94
- kotonebot/client/host/custom.py +118 -114
- kotonebot/client/host/leidian_host.py +196 -201
- kotonebot/client/host/mumu12_host.py +353 -358
- kotonebot/client/host/protocol.py +214 -213
- kotonebot/client/host/windows_common.py +58 -55
- kotonebot/client/implements/__init__.py +71 -7
- kotonebot/client/implements/adb.py +89 -85
- kotonebot/client/implements/adb_raw.py +162 -158
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -192
- kotonebot/client/implements/uiautomator2.py +85 -81
- kotonebot/client/implements/windows.py +176 -168
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/config/base_config.py +96 -96
- kotonebot/config/manager.py +36 -36
- kotonebot/errors.py +76 -71
- kotonebot/interop/win/__init__.py +10 -0
- kotonebot/interop/win/_mouse.py +311 -0
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -469
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +17 -17
- kotonebot/primitives/geometry.py +862 -290
- kotonebot/primitives/visual.py +63 -63
- kotonebot/tools/mirror.py +354 -354
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -87
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -53
- kotonebot/ui/user.py +148 -143
- kotonebot/util.py +436 -409
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
- kotonebot-0.5.0.dist-info/RECORD +71 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot-0.3.1.dist-info/RECORD +0 -70
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
kotonebot/backend/debug/vars.py
CHANGED
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import json
|
|
4
|
-
import time
|
|
5
|
-
import uuid
|
|
6
|
-
import shutil
|
|
7
|
-
import hashlib
|
|
8
|
-
import traceback
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from functools import cache
|
|
11
|
-
from datetime import datetime
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import NamedTuple, TextIO, Literal
|
|
14
|
-
import warnings
|
|
15
|
-
|
|
16
|
-
import cv2
|
|
17
|
-
from cv2.typing import MatLike
|
|
18
|
-
from pydantic import BaseModel
|
|
19
|
-
import inspect # 添加此行以导入 inspect 模块
|
|
20
|
-
|
|
21
|
-
from ..core import Image
|
|
22
|
-
from ...util import cv2_imread
|
|
23
|
-
from kotonebot import logging
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
class Result(NamedTuple):
|
|
28
|
-
title: str
|
|
29
|
-
image: list[str]
|
|
30
|
-
description: str
|
|
31
|
-
timestamp: float
|
|
32
|
-
|
|
33
|
-
class ImageData(NamedTuple):
|
|
34
|
-
data: MatLike
|
|
35
|
-
timestamp: float
|
|
36
|
-
|
|
37
|
-
class WSImage(BaseModel):
|
|
38
|
-
type: Literal["memory"]
|
|
39
|
-
value: list[str]
|
|
40
|
-
|
|
41
|
-
class WSCallstack(BaseModel):
|
|
42
|
-
name: str
|
|
43
|
-
file: str
|
|
44
|
-
line: int
|
|
45
|
-
code: str
|
|
46
|
-
type: Literal["function", "method", "module", "lambda"]
|
|
47
|
-
url: str | None
|
|
48
|
-
|
|
49
|
-
class WSMessageData(BaseModel):
|
|
50
|
-
image: WSImage
|
|
51
|
-
name: str
|
|
52
|
-
details: str
|
|
53
|
-
timestamp: int
|
|
54
|
-
callstack: list[WSCallstack]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class WSMessage(BaseModel):
|
|
58
|
-
type: Literal["visual"]
|
|
59
|
-
data: WSMessageData
|
|
60
|
-
|
|
61
|
-
@dataclass
|
|
62
|
-
class _Vars:
|
|
63
|
-
"""调试变量类"""
|
|
64
|
-
enabled: bool = False
|
|
65
|
-
"""是否启用调试结果显示。"""
|
|
66
|
-
|
|
67
|
-
max_results: int = -1
|
|
68
|
-
"""最多保存的结果数量。-1 表示不限制。"""
|
|
69
|
-
|
|
70
|
-
wait_for_message_sent: bool = False
|
|
71
|
-
"""
|
|
72
|
-
是否等待消息发送完成才继续后续代码。
|
|
73
|
-
|
|
74
|
-
默认禁用。启用此选项会显著降低运行速度。
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
hide_server_log: bool = True
|
|
78
|
-
"""是否隐藏服务器日志。"""
|
|
79
|
-
|
|
80
|
-
auto_save_to_folder: str | None = None
|
|
81
|
-
"""
|
|
82
|
-
是否将结果自动保存到指定文件夹。
|
|
83
|
-
|
|
84
|
-
如果为 None,则不保存。
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
hash_image: bool = True
|
|
88
|
-
"""
|
|
89
|
-
是否使用图片的 MD5 值作为图片的唯一标识。
|
|
90
|
-
若禁用,则使用随机 UUID 作为图片的唯一标识
|
|
91
|
-
(可能会导致保存大量重复图片)。
|
|
92
|
-
|
|
93
|
-
此选项默认启用。启用此选项会轻微降低调试时运行速度。
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
debug = _Vars()
|
|
97
|
-
|
|
98
|
-
_results: dict[str, Result] = {}
|
|
99
|
-
_images: dict[str, ImageData] = {}
|
|
100
|
-
"""存放临时图片的字典。"""
|
|
101
|
-
_result_file: TextIO | None = None
|
|
102
|
-
|
|
103
|
-
def _save_image(image: MatLike | Image) -> str:
|
|
104
|
-
"""缓存图片数据到 _images 字典中。返回 key。"""
|
|
105
|
-
if isinstance(image, Image):
|
|
106
|
-
image = image.data
|
|
107
|
-
# 计算 key
|
|
108
|
-
if debug.hash_image:
|
|
109
|
-
key = hashlib.md5(image.tobytes()).hexdigest()
|
|
110
|
-
else:
|
|
111
|
-
key = str(uuid.uuid4())
|
|
112
|
-
# 保存图片
|
|
113
|
-
if key not in _images:
|
|
114
|
-
_images[key] = ImageData(image, time.time())
|
|
115
|
-
if debug.auto_save_to_folder:
|
|
116
|
-
if not os.path.exists(debug.auto_save_to_folder):
|
|
117
|
-
os.makedirs(debug.auto_save_to_folder)
|
|
118
|
-
file_name = f"{key}.png"
|
|
119
|
-
cv2.imwrite(os.path.join(debug.auto_save_to_folder, file_name), image)
|
|
120
|
-
# 当图片 >= 100 张时,删除最早的图片
|
|
121
|
-
while len(_images) >= 100:
|
|
122
|
-
logger.verbose("Debug image buffer is full. Deleting oldest image...")
|
|
123
|
-
_images.pop(next(iter(_images)))
|
|
124
|
-
return key
|
|
125
|
-
|
|
126
|
-
def _read_image(key: str) -> MatLike | None:
|
|
127
|
-
"""从 _images 字典中读取图片。"""
|
|
128
|
-
data = None
|
|
129
|
-
if key in _images:
|
|
130
|
-
data = _images[key].data
|
|
131
|
-
elif debug.auto_save_to_folder:
|
|
132
|
-
path = os.path.join(debug.auto_save_to_folder, f"{key}.png")
|
|
133
|
-
if os.path.exists(path):
|
|
134
|
-
data = cv2_imread(path)
|
|
135
|
-
_images[key] = ImageData(data, time.time())
|
|
136
|
-
return data
|
|
137
|
-
|
|
138
|
-
def _save_images(images: list[MatLike]) -> list[str]:
|
|
139
|
-
"""缓存图片数据到 _images 字典中。返回 key 列表。"""
|
|
140
|
-
return [_save_image(image) for image in images]
|
|
141
|
-
|
|
142
|
-
def img(image: str | MatLike | Image | None) -> str:
|
|
143
|
-
"""
|
|
144
|
-
用于在 `result()` 函数中嵌入图片。
|
|
145
|
-
|
|
146
|
-
:param image: 图片路径或 OpenCV 图片对象。
|
|
147
|
-
:return: 图片的 HTML 代码。
|
|
148
|
-
"""
|
|
149
|
-
if image is None:
|
|
150
|
-
return 'None'
|
|
151
|
-
if debug.auto_save_to_folder:
|
|
152
|
-
if isinstance(image, str):
|
|
153
|
-
image = cv2_imread(image)
|
|
154
|
-
elif isinstance(image, Image):
|
|
155
|
-
image = image.data
|
|
156
|
-
key = _save_image(image)
|
|
157
|
-
return f'[img]{key}[/img]'
|
|
158
|
-
else:
|
|
159
|
-
if isinstance(image, str):
|
|
160
|
-
return f'<img src="/api/read_file?path={image}" />'
|
|
161
|
-
elif isinstance(image, Image) and image.path:
|
|
162
|
-
return f'<img src="/api/read_file?path={image.path}" />'
|
|
163
|
-
else:
|
|
164
|
-
key = _save_image(image)
|
|
165
|
-
return f'<img src="/api/read_memory?key={key}" />'
|
|
166
|
-
|
|
167
|
-
def color(color: str | tuple[int, int, int] | None) -> str:
|
|
168
|
-
"""
|
|
169
|
-
用于在调试结果中嵌入颜色。
|
|
170
|
-
"""
|
|
171
|
-
if color is None:
|
|
172
|
-
return 'None'
|
|
173
|
-
if isinstance(color, tuple):
|
|
174
|
-
color = '#{:02X}{:02X}{:02X}'.format(color[0], color[1], color[2])
|
|
175
|
-
return f'<kbd-color style="display:inline-block; white-space:initial;" color="{color}"></kbd-color>'
|
|
176
|
-
else:
|
|
177
|
-
return f'<kbd-color style="display:inline-block; white-space:initial;" color="{color}"></kbd-color>'
|
|
178
|
-
|
|
179
|
-
def to_html(text: str) -> str:
|
|
180
|
-
"""将文本转换为 HTML 代码。"""
|
|
181
|
-
text = text.replace('<', '<').replace('>', '>')
|
|
182
|
-
text = text.replace('\n', '<br>')
|
|
183
|
-
text = text.replace(' ', ' ')
|
|
184
|
-
return text
|
|
185
|
-
|
|
186
|
-
IDEType = Literal['vscode', 'cursor', 'windsurf']
|
|
187
|
-
|
|
188
|
-
@cache
|
|
189
|
-
def get_current_ide() -> IDEType | None:
|
|
190
|
-
"""获取当前IDE类型"""
|
|
191
|
-
try:
|
|
192
|
-
import psutil
|
|
193
|
-
except ImportError:
|
|
194
|
-
warnings.warn('Not able to detect IDE type. Install psutil for better developer experience.')
|
|
195
|
-
return None
|
|
196
|
-
me = psutil.Process()
|
|
197
|
-
while True:
|
|
198
|
-
parent = me.parent()
|
|
199
|
-
if parent is None:
|
|
200
|
-
break
|
|
201
|
-
name = parent.name()
|
|
202
|
-
if name.lower() == 'code.exe':
|
|
203
|
-
return 'vscode'
|
|
204
|
-
elif name.lower() == 'cursor.exe':
|
|
205
|
-
return 'cursor'
|
|
206
|
-
elif name.lower() == 'windsurf.exe':
|
|
207
|
-
return 'windsurf'
|
|
208
|
-
me = parent
|
|
209
|
-
return None
|
|
210
|
-
|
|
211
|
-
def _make_code_file_url(
|
|
212
|
-
text: str,
|
|
213
|
-
full_path: str,
|
|
214
|
-
line: int = 0,
|
|
215
|
-
) -> str:
|
|
216
|
-
"""
|
|
217
|
-
将代码文本转换为 VSCode 的文件 URL。
|
|
218
|
-
"""
|
|
219
|
-
ide = get_current_ide()
|
|
220
|
-
if ide == 'vscode':
|
|
221
|
-
prefix = 'vscode'
|
|
222
|
-
elif ide == 'cursor':
|
|
223
|
-
prefix = 'cursor'
|
|
224
|
-
elif ide == 'windsurf':
|
|
225
|
-
prefix = 'windsurf'
|
|
226
|
-
else:
|
|
227
|
-
return text
|
|
228
|
-
url = f"{prefix}://file/{full_path}:{line}:0"
|
|
229
|
-
return f'<a href="{url}">{text}</a>'
|
|
230
|
-
|
|
231
|
-
def _make_code_file_url_only(
|
|
232
|
-
text: str,
|
|
233
|
-
full_path: str,
|
|
234
|
-
line: int = 0,
|
|
235
|
-
) -> str:
|
|
236
|
-
"""
|
|
237
|
-
将代码文本转换为 VSCode 的文件 URL。
|
|
238
|
-
"""
|
|
239
|
-
ide = get_current_ide()
|
|
240
|
-
if ide == 'vscode':
|
|
241
|
-
prefix = 'vscode'
|
|
242
|
-
elif ide == 'cursor':
|
|
243
|
-
prefix = 'cursor'
|
|
244
|
-
elif ide == 'windsurf':
|
|
245
|
-
prefix = 'windsurf'
|
|
246
|
-
else:
|
|
247
|
-
return text
|
|
248
|
-
return f"{prefix}://file/{full_path}:{line}:0"
|
|
249
|
-
|
|
250
|
-
def result(
|
|
251
|
-
title: str,
|
|
252
|
-
image: MatLike | list[MatLike],
|
|
253
|
-
text: str = ''
|
|
254
|
-
):
|
|
255
|
-
"""
|
|
256
|
-
显示图片结果。
|
|
257
|
-
|
|
258
|
-
例:
|
|
259
|
-
```python
|
|
260
|
-
result(
|
|
261
|
-
"image.find",
|
|
262
|
-
image,
|
|
263
|
-
f"template: {img(template)} \\n"
|
|
264
|
-
f"matches: {len(matches)} \\n"
|
|
265
|
-
)
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
:param title: 标题。建议使用 `模块.方法` 格式。
|
|
269
|
-
:param image: 图片。
|
|
270
|
-
:param text: 详细文本。可以是 HTML 代码,空格和换行将会保留。如果需要嵌入图片,使用 `img()` 函数。
|
|
271
|
-
"""
|
|
272
|
-
global _result_file
|
|
273
|
-
if not debug.enabled:
|
|
274
|
-
return
|
|
275
|
-
if not isinstance(image, list):
|
|
276
|
-
image = [image]
|
|
277
|
-
|
|
278
|
-
key = 'result_' + title + '_' + str(time.time())
|
|
279
|
-
# 保存图片
|
|
280
|
-
saved_images = _save_images(image)
|
|
281
|
-
current_timestamp = int(time.time() * 1000)
|
|
282
|
-
_results[key] = Result(title, saved_images, text, current_timestamp)
|
|
283
|
-
if len(_results) > debug.max_results:
|
|
284
|
-
_results.pop(next(iter(_results)))
|
|
285
|
-
# 拼接消息
|
|
286
|
-
|
|
287
|
-
callstacks: list[WSCallstack] = []
|
|
288
|
-
for frame in inspect.stack():
|
|
289
|
-
frame_info = frame.frame
|
|
290
|
-
# 跳过标准库和 debugpy 的代码
|
|
291
|
-
if re.search(r'Python\d*[\/\\]lib|debugpy', frame_info.f_code.co_filename):
|
|
292
|
-
break
|
|
293
|
-
lineno = frame_info.f_lineno
|
|
294
|
-
code = frame_info.f_code.co_name
|
|
295
|
-
# 判断第一个参数是否为 self
|
|
296
|
-
if frame_info.f_code.co_argcount > 0 and frame_info.f_code.co_varnames[0] == 'self':
|
|
297
|
-
type = 'method'
|
|
298
|
-
elif '<module>' in code:
|
|
299
|
-
type = 'module'
|
|
300
|
-
elif '<lambda>' in code:
|
|
301
|
-
type = 'lambda'
|
|
302
|
-
else:
|
|
303
|
-
type = 'function' # 默认类型为 function
|
|
304
|
-
callstacks.append(WSCallstack(
|
|
305
|
-
name=frame_info.f_code.co_name,
|
|
306
|
-
file=frame_info.f_code.co_filename,
|
|
307
|
-
line=lineno,
|
|
308
|
-
code=code,
|
|
309
|
-
url=_make_code_file_url_only(frame_info.f_code.co_filename, frame_info.f_code.co_filename, lineno),
|
|
310
|
-
type=type
|
|
311
|
-
))
|
|
312
|
-
|
|
313
|
-
final_text = text
|
|
314
|
-
# 发送 WS 消息
|
|
315
|
-
from .server import send_ws_message
|
|
316
|
-
send_ws_message(title, saved_images, final_text, callstack=callstacks, wait=debug.wait_for_message_sent)
|
|
317
|
-
|
|
318
|
-
# 保存到文件
|
|
319
|
-
if debug.auto_save_to_folder:
|
|
320
|
-
if _result_file is None:
|
|
321
|
-
if not os.path.exists(debug.auto_save_to_folder):
|
|
322
|
-
os.makedirs(debug.auto_save_to_folder)
|
|
323
|
-
log_file_name = f"dump_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json"
|
|
324
|
-
_result_file = open(os.path.join(debug.auto_save_to_folder, log_file_name), "w", encoding="utf-8")
|
|
325
|
-
message = WSMessage(
|
|
326
|
-
type="visual",
|
|
327
|
-
data=WSMessageData(
|
|
328
|
-
image=WSImage(type="memory", value=saved_images),
|
|
329
|
-
name=title,
|
|
330
|
-
details=final_text,
|
|
331
|
-
timestamp=current_timestamp,
|
|
332
|
-
callstack=callstacks
|
|
333
|
-
)
|
|
334
|
-
)
|
|
335
|
-
_result_file.write(message.model_dump_json())
|
|
336
|
-
_result_file.write("\n")
|
|
337
|
-
_result_file.flush()
|
|
338
|
-
|
|
339
|
-
def clear_saved():
|
|
340
|
-
"""
|
|
341
|
-
清空本地保存文件夹中的内容。
|
|
342
|
-
"""
|
|
343
|
-
logger.info("Clearing debug saved files...")
|
|
344
|
-
if debug.auto_save_to_folder:
|
|
345
|
-
try:
|
|
346
|
-
shutil.rmtree(debug.auto_save_to_folder, ignore_errors=True)
|
|
347
|
-
logger.info(f"Cleared debug saved files: {debug.auto_save_to_folder}")
|
|
348
|
-
except PermissionError:
|
|
349
|
-
logger.error(f"Failed to clear debug saved files: {debug.auto_save_to_folder}")
|
|
350
|
-
else:
|
|
351
|
-
logger.info("No auto save folder, skipping...")
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
import shutil
|
|
7
|
+
import hashlib
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from functools import cache
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import NamedTuple, TextIO, Literal
|
|
14
|
+
import warnings
|
|
15
|
+
|
|
16
|
+
import cv2
|
|
17
|
+
from cv2.typing import MatLike
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
import inspect # 添加此行以导入 inspect 模块
|
|
20
|
+
|
|
21
|
+
from ..core import Image
|
|
22
|
+
from ...util import cv2_imread
|
|
23
|
+
from kotonebot import logging
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class Result(NamedTuple):
|
|
28
|
+
title: str
|
|
29
|
+
image: list[str]
|
|
30
|
+
description: str
|
|
31
|
+
timestamp: float
|
|
32
|
+
|
|
33
|
+
class ImageData(NamedTuple):
|
|
34
|
+
data: MatLike
|
|
35
|
+
timestamp: float
|
|
36
|
+
|
|
37
|
+
class WSImage(BaseModel):
|
|
38
|
+
type: Literal["memory"]
|
|
39
|
+
value: list[str]
|
|
40
|
+
|
|
41
|
+
class WSCallstack(BaseModel):
|
|
42
|
+
name: str
|
|
43
|
+
file: str
|
|
44
|
+
line: int
|
|
45
|
+
code: str
|
|
46
|
+
type: Literal["function", "method", "module", "lambda"]
|
|
47
|
+
url: str | None
|
|
48
|
+
|
|
49
|
+
class WSMessageData(BaseModel):
|
|
50
|
+
image: WSImage
|
|
51
|
+
name: str
|
|
52
|
+
details: str
|
|
53
|
+
timestamp: int
|
|
54
|
+
callstack: list[WSCallstack]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WSMessage(BaseModel):
|
|
58
|
+
type: Literal["visual"]
|
|
59
|
+
data: WSMessageData
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class _Vars:
|
|
63
|
+
"""调试变量类"""
|
|
64
|
+
enabled: bool = False
|
|
65
|
+
"""是否启用调试结果显示。"""
|
|
66
|
+
|
|
67
|
+
max_results: int = -1
|
|
68
|
+
"""最多保存的结果数量。-1 表示不限制。"""
|
|
69
|
+
|
|
70
|
+
wait_for_message_sent: bool = False
|
|
71
|
+
"""
|
|
72
|
+
是否等待消息发送完成才继续后续代码。
|
|
73
|
+
|
|
74
|
+
默认禁用。启用此选项会显著降低运行速度。
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
hide_server_log: bool = True
|
|
78
|
+
"""是否隐藏服务器日志。"""
|
|
79
|
+
|
|
80
|
+
auto_save_to_folder: str | None = None
|
|
81
|
+
"""
|
|
82
|
+
是否将结果自动保存到指定文件夹。
|
|
83
|
+
|
|
84
|
+
如果为 None,则不保存。
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
hash_image: bool = True
|
|
88
|
+
"""
|
|
89
|
+
是否使用图片的 MD5 值作为图片的唯一标识。
|
|
90
|
+
若禁用,则使用随机 UUID 作为图片的唯一标识
|
|
91
|
+
(可能会导致保存大量重复图片)。
|
|
92
|
+
|
|
93
|
+
此选项默认启用。启用此选项会轻微降低调试时运行速度。
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
debug = _Vars()
|
|
97
|
+
|
|
98
|
+
_results: dict[str, Result] = {}
|
|
99
|
+
_images: dict[str, ImageData] = {}
|
|
100
|
+
"""存放临时图片的字典。"""
|
|
101
|
+
_result_file: TextIO | None = None
|
|
102
|
+
|
|
103
|
+
def _save_image(image: MatLike | Image) -> str:
|
|
104
|
+
"""缓存图片数据到 _images 字典中。返回 key。"""
|
|
105
|
+
if isinstance(image, Image):
|
|
106
|
+
image = image.data
|
|
107
|
+
# 计算 key
|
|
108
|
+
if debug.hash_image:
|
|
109
|
+
key = hashlib.md5(image.tobytes()).hexdigest()
|
|
110
|
+
else:
|
|
111
|
+
key = str(uuid.uuid4())
|
|
112
|
+
# 保存图片
|
|
113
|
+
if key not in _images:
|
|
114
|
+
_images[key] = ImageData(image, time.time())
|
|
115
|
+
if debug.auto_save_to_folder:
|
|
116
|
+
if not os.path.exists(debug.auto_save_to_folder):
|
|
117
|
+
os.makedirs(debug.auto_save_to_folder)
|
|
118
|
+
file_name = f"{key}.png"
|
|
119
|
+
cv2.imwrite(os.path.join(debug.auto_save_to_folder, file_name), image)
|
|
120
|
+
# 当图片 >= 100 张时,删除最早的图片
|
|
121
|
+
while len(_images) >= 100:
|
|
122
|
+
logger.verbose("Debug image buffer is full. Deleting oldest image...")
|
|
123
|
+
_images.pop(next(iter(_images)))
|
|
124
|
+
return key
|
|
125
|
+
|
|
126
|
+
def _read_image(key: str) -> MatLike | None:
|
|
127
|
+
"""从 _images 字典中读取图片。"""
|
|
128
|
+
data = None
|
|
129
|
+
if key in _images:
|
|
130
|
+
data = _images[key].data
|
|
131
|
+
elif debug.auto_save_to_folder:
|
|
132
|
+
path = os.path.join(debug.auto_save_to_folder, f"{key}.png")
|
|
133
|
+
if os.path.exists(path):
|
|
134
|
+
data = cv2_imread(path)
|
|
135
|
+
_images[key] = ImageData(data, time.time())
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
def _save_images(images: list[MatLike]) -> list[str]:
|
|
139
|
+
"""缓存图片数据到 _images 字典中。返回 key 列表。"""
|
|
140
|
+
return [_save_image(image) for image in images]
|
|
141
|
+
|
|
142
|
+
def img(image: str | MatLike | Image | None) -> str:
|
|
143
|
+
"""
|
|
144
|
+
用于在 `result()` 函数中嵌入图片。
|
|
145
|
+
|
|
146
|
+
:param image: 图片路径或 OpenCV 图片对象。
|
|
147
|
+
:return: 图片的 HTML 代码。
|
|
148
|
+
"""
|
|
149
|
+
if image is None:
|
|
150
|
+
return 'None'
|
|
151
|
+
if debug.auto_save_to_folder:
|
|
152
|
+
if isinstance(image, str):
|
|
153
|
+
image = cv2_imread(image)
|
|
154
|
+
elif isinstance(image, Image):
|
|
155
|
+
image = image.data
|
|
156
|
+
key = _save_image(image)
|
|
157
|
+
return f'[img]{key}[/img]'
|
|
158
|
+
else:
|
|
159
|
+
if isinstance(image, str):
|
|
160
|
+
return f'<img src="/api/read_file?path={image}" />'
|
|
161
|
+
elif isinstance(image, Image) and image.path:
|
|
162
|
+
return f'<img src="/api/read_file?path={image.path}" />'
|
|
163
|
+
else:
|
|
164
|
+
key = _save_image(image)
|
|
165
|
+
return f'<img src="/api/read_memory?key={key}" />'
|
|
166
|
+
|
|
167
|
+
def color(color: str | tuple[int, int, int] | None) -> str:
|
|
168
|
+
"""
|
|
169
|
+
用于在调试结果中嵌入颜色。
|
|
170
|
+
"""
|
|
171
|
+
if color is None:
|
|
172
|
+
return 'None'
|
|
173
|
+
if isinstance(color, tuple):
|
|
174
|
+
color = '#{:02X}{:02X}{:02X}'.format(color[0], color[1], color[2])
|
|
175
|
+
return f'<kbd-color style="display:inline-block; white-space:initial;" color="{color}"></kbd-color>'
|
|
176
|
+
else:
|
|
177
|
+
return f'<kbd-color style="display:inline-block; white-space:initial;" color="{color}"></kbd-color>'
|
|
178
|
+
|
|
179
|
+
def to_html(text: str) -> str:
|
|
180
|
+
"""将文本转换为 HTML 代码。"""
|
|
181
|
+
text = text.replace('<', '<').replace('>', '>')
|
|
182
|
+
text = text.replace('\n', '<br>')
|
|
183
|
+
text = text.replace(' ', ' ')
|
|
184
|
+
return text
|
|
185
|
+
|
|
186
|
+
IDEType = Literal['vscode', 'cursor', 'windsurf']
|
|
187
|
+
|
|
188
|
+
@cache
|
|
189
|
+
def get_current_ide() -> IDEType | None:
|
|
190
|
+
"""获取当前IDE类型"""
|
|
191
|
+
try:
|
|
192
|
+
import psutil
|
|
193
|
+
except ImportError:
|
|
194
|
+
warnings.warn('Not able to detect IDE type. Install psutil for better developer experience.')
|
|
195
|
+
return None
|
|
196
|
+
me = psutil.Process()
|
|
197
|
+
while True:
|
|
198
|
+
parent = me.parent()
|
|
199
|
+
if parent is None:
|
|
200
|
+
break
|
|
201
|
+
name = parent.name()
|
|
202
|
+
if name.lower() == 'code.exe':
|
|
203
|
+
return 'vscode'
|
|
204
|
+
elif name.lower() == 'cursor.exe':
|
|
205
|
+
return 'cursor'
|
|
206
|
+
elif name.lower() == 'windsurf.exe':
|
|
207
|
+
return 'windsurf'
|
|
208
|
+
me = parent
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
def _make_code_file_url(
|
|
212
|
+
text: str,
|
|
213
|
+
full_path: str,
|
|
214
|
+
line: int = 0,
|
|
215
|
+
) -> str:
|
|
216
|
+
"""
|
|
217
|
+
将代码文本转换为 VSCode 的文件 URL。
|
|
218
|
+
"""
|
|
219
|
+
ide = get_current_ide()
|
|
220
|
+
if ide == 'vscode':
|
|
221
|
+
prefix = 'vscode'
|
|
222
|
+
elif ide == 'cursor':
|
|
223
|
+
prefix = 'cursor'
|
|
224
|
+
elif ide == 'windsurf':
|
|
225
|
+
prefix = 'windsurf'
|
|
226
|
+
else:
|
|
227
|
+
return text
|
|
228
|
+
url = f"{prefix}://file/{full_path}:{line}:0"
|
|
229
|
+
return f'<a href="{url}">{text}</a>'
|
|
230
|
+
|
|
231
|
+
def _make_code_file_url_only(
|
|
232
|
+
text: str,
|
|
233
|
+
full_path: str,
|
|
234
|
+
line: int = 0,
|
|
235
|
+
) -> str:
|
|
236
|
+
"""
|
|
237
|
+
将代码文本转换为 VSCode 的文件 URL。
|
|
238
|
+
"""
|
|
239
|
+
ide = get_current_ide()
|
|
240
|
+
if ide == 'vscode':
|
|
241
|
+
prefix = 'vscode'
|
|
242
|
+
elif ide == 'cursor':
|
|
243
|
+
prefix = 'cursor'
|
|
244
|
+
elif ide == 'windsurf':
|
|
245
|
+
prefix = 'windsurf'
|
|
246
|
+
else:
|
|
247
|
+
return text
|
|
248
|
+
return f"{prefix}://file/{full_path}:{line}:0"
|
|
249
|
+
|
|
250
|
+
def result(
|
|
251
|
+
title: str,
|
|
252
|
+
image: MatLike | list[MatLike],
|
|
253
|
+
text: str = ''
|
|
254
|
+
):
|
|
255
|
+
"""
|
|
256
|
+
显示图片结果。
|
|
257
|
+
|
|
258
|
+
例:
|
|
259
|
+
```python
|
|
260
|
+
result(
|
|
261
|
+
"image.find",
|
|
262
|
+
image,
|
|
263
|
+
f"template: {img(template)} \\n"
|
|
264
|
+
f"matches: {len(matches)} \\n"
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
:param title: 标题。建议使用 `模块.方法` 格式。
|
|
269
|
+
:param image: 图片。
|
|
270
|
+
:param text: 详细文本。可以是 HTML 代码,空格和换行将会保留。如果需要嵌入图片,使用 `img()` 函数。
|
|
271
|
+
"""
|
|
272
|
+
global _result_file
|
|
273
|
+
if not debug.enabled:
|
|
274
|
+
return
|
|
275
|
+
if not isinstance(image, list):
|
|
276
|
+
image = [image]
|
|
277
|
+
|
|
278
|
+
key = 'result_' + title + '_' + str(time.time())
|
|
279
|
+
# 保存图片
|
|
280
|
+
saved_images = _save_images(image)
|
|
281
|
+
current_timestamp = int(time.time() * 1000)
|
|
282
|
+
_results[key] = Result(title, saved_images, text, current_timestamp)
|
|
283
|
+
if len(_results) > debug.max_results:
|
|
284
|
+
_results.pop(next(iter(_results)))
|
|
285
|
+
# 拼接消息
|
|
286
|
+
|
|
287
|
+
callstacks: list[WSCallstack] = []
|
|
288
|
+
for frame in inspect.stack():
|
|
289
|
+
frame_info = frame.frame
|
|
290
|
+
# 跳过标准库和 debugpy 的代码
|
|
291
|
+
if re.search(r'Python\d*[\/\\]lib|debugpy', frame_info.f_code.co_filename):
|
|
292
|
+
break
|
|
293
|
+
lineno = frame_info.f_lineno
|
|
294
|
+
code = frame_info.f_code.co_name
|
|
295
|
+
# 判断第一个参数是否为 self
|
|
296
|
+
if frame_info.f_code.co_argcount > 0 and frame_info.f_code.co_varnames[0] == 'self':
|
|
297
|
+
type = 'method'
|
|
298
|
+
elif '<module>' in code:
|
|
299
|
+
type = 'module'
|
|
300
|
+
elif '<lambda>' in code:
|
|
301
|
+
type = 'lambda'
|
|
302
|
+
else:
|
|
303
|
+
type = 'function' # 默认类型为 function
|
|
304
|
+
callstacks.append(WSCallstack(
|
|
305
|
+
name=frame_info.f_code.co_name,
|
|
306
|
+
file=frame_info.f_code.co_filename,
|
|
307
|
+
line=lineno,
|
|
308
|
+
code=code,
|
|
309
|
+
url=_make_code_file_url_only(frame_info.f_code.co_filename, frame_info.f_code.co_filename, lineno),
|
|
310
|
+
type=type
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
final_text = text
|
|
314
|
+
# 发送 WS 消息
|
|
315
|
+
from .server import send_ws_message
|
|
316
|
+
send_ws_message(title, saved_images, final_text, callstack=callstacks, wait=debug.wait_for_message_sent)
|
|
317
|
+
|
|
318
|
+
# 保存到文件
|
|
319
|
+
if debug.auto_save_to_folder:
|
|
320
|
+
if _result_file is None:
|
|
321
|
+
if not os.path.exists(debug.auto_save_to_folder):
|
|
322
|
+
os.makedirs(debug.auto_save_to_folder)
|
|
323
|
+
log_file_name = f"dump_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json"
|
|
324
|
+
_result_file = open(os.path.join(debug.auto_save_to_folder, log_file_name), "w", encoding="utf-8")
|
|
325
|
+
message = WSMessage(
|
|
326
|
+
type="visual",
|
|
327
|
+
data=WSMessageData(
|
|
328
|
+
image=WSImage(type="memory", value=saved_images),
|
|
329
|
+
name=title,
|
|
330
|
+
details=final_text,
|
|
331
|
+
timestamp=current_timestamp,
|
|
332
|
+
callstack=callstacks
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
_result_file.write(message.model_dump_json())
|
|
336
|
+
_result_file.write("\n")
|
|
337
|
+
_result_file.flush()
|
|
338
|
+
|
|
339
|
+
def clear_saved():
|
|
340
|
+
"""
|
|
341
|
+
清空本地保存文件夹中的内容。
|
|
342
|
+
"""
|
|
343
|
+
logger.info("Clearing debug saved files...")
|
|
344
|
+
if debug.auto_save_to_folder:
|
|
345
|
+
try:
|
|
346
|
+
shutil.rmtree(debug.auto_save_to_folder, ignore_errors=True)
|
|
347
|
+
logger.info(f"Cleared debug saved files: {debug.auto_save_to_folder}")
|
|
348
|
+
except PermissionError:
|
|
349
|
+
logger.error(f"Failed to clear debug saved files: {debug.auto_save_to_folder}")
|
|
350
|
+
else:
|
|
351
|
+
logger.info("No auto save folder, skipping...")
|