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,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('<', '&lt;').replace('>', '&gt;')
182
- text = text.replace('\n', '<br>')
183
- text = text.replace(' ', '&nbsp;')
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('<', '&lt;').replace('>', '&gt;')
182
+ text = text.replace('\n', '<br>')
183
+ text = text.replace(' ', '&nbsp;')
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...")