kotonebot 0.5.0__py3-none-any.whl → 0.6.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 (103) 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 +58 -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.py +176 -176
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/client/scaler.py +467 -0
  39. kotonebot/config/base_config.py +96 -96
  40. kotonebot/config/config.py +61 -0
  41. kotonebot/config/manager.py +36 -36
  42. kotonebot/core/__init__.py +13 -0
  43. kotonebot/core/entities/base.py +182 -0
  44. kotonebot/core/entities/compound.py +75 -0
  45. kotonebot/core/entities/ocr.py +117 -0
  46. kotonebot/core/entities/template_match.py +198 -0
  47. kotonebot/devtools/__init__.py +42 -0
  48. kotonebot/devtools/cli/__init__.py +6 -0
  49. kotonebot/devtools/cli/main.py +53 -0
  50. kotonebot/{tools → devtools}/mirror.py +354 -354
  51. kotonebot/devtools/project/project.py +41 -0
  52. kotonebot/devtools/project/scanner.py +202 -0
  53. kotonebot/devtools/project/schema.py +99 -0
  54. kotonebot/devtools/resgen/__init__.py +42 -0
  55. kotonebot/devtools/resgen/codegen.py +331 -0
  56. kotonebot/devtools/resgen/core.py +94 -0
  57. kotonebot/devtools/resgen/parsers.py +360 -0
  58. kotonebot/devtools/resgen/utils.py +158 -0
  59. kotonebot/devtools/resgen/validation.py +115 -0
  60. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  61. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  62. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  63. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  64. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  65. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  66. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  67. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  68. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  69. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  70. kotonebot/devtools/web/dist/index.html +25 -0
  71. kotonebot/devtools/web/server/__init__.py +0 -0
  72. kotonebot/devtools/web/server/rest_api.py +217 -0
  73. kotonebot/devtools/web/server/server.py +85 -0
  74. kotonebot/errors.py +76 -76
  75. kotonebot/interop/win/__init__.py +11 -9
  76. kotonebot/interop/win/_mouse.py +310 -310
  77. kotonebot/interop/win/message_box.py +313 -313
  78. kotonebot/interop/win/reg.py +37 -37
  79. kotonebot/interop/win/shake_mouse.py +224 -0
  80. kotonebot/interop/win/shortcut.py +43 -43
  81. kotonebot/interop/win/task_dialog.py +513 -513
  82. kotonebot/logging/__init__.py +2 -2
  83. kotonebot/logging/log.py +17 -17
  84. kotonebot/primitives/__init__.py +19 -17
  85. kotonebot/primitives/geometry.py +1067 -862
  86. kotonebot/primitives/visual.py +143 -63
  87. kotonebot/ui/file_host/sensio.py +36 -36
  88. kotonebot/ui/file_host/tmp_send.py +54 -54
  89. kotonebot/ui/pushkit/__init__.py +3 -3
  90. kotonebot/ui/pushkit/image_host.py +88 -88
  91. kotonebot/ui/pushkit/protocol.py +13 -13
  92. kotonebot/ui/pushkit/wxpusher.py +54 -54
  93. kotonebot/ui/user.py +148 -148
  94. kotonebot/util.py +436 -436
  95. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
  96. kotonebot-0.6.0.dist-info/RECORD +105 -0
  97. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  98. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
  99. kotonebot/client/implements/adb_raw.py +0 -163
  100. kotonebot-0.5.0.dist-info/RECORD +0 -71
  101. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  102. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  103. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,63 +1,143 @@
1
- import logging
2
-
3
- from cv2.typing import MatLike
4
-
5
- from .geometry import Size
6
- from kotonebot.util import cv2_imread
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
- class Image:
11
- """
12
- 图像类。
13
- """
14
- def __init__(
15
- self,
16
- pixels: MatLike | None = None,
17
- file_path: str | None = None,
18
- lazy_load: bool = False,
19
- name: str | None = None,
20
- description: str | None = None
21
- ):
22
- """
23
- 从内存数据或图像文件创建图像类。
24
-
25
- :param pixels: 图像数据。格式必须为 BGR。
26
- :param file_path: 图像文件路径。
27
- :param lazy_load: 是否延迟加载图像数据。
28
- 若为 False,立即载入,否则仅当访问图像数据时才载入。仅当从文件创建图像类时生效。
29
- :param name: 图像名称。
30
- :param description: 图像描述。
31
- """
32
- self.name: str | None = name
33
- """图像名称。"""
34
- self.description: str | None = description
35
- """图像描述。"""
36
- self.file_path: str | None = file_path
37
- """图像的文件路径。"""
38
- self.__pixels: MatLike | None = None
39
- # 立即加载
40
- if not lazy_load and self.file_path:
41
- _ = self.pixels
42
- # 传入像素数据而不是文件
43
- if pixels is not None:
44
- self.__pixels = pixels
45
-
46
- @property
47
- def pixels(self) -> MatLike:
48
- """图像的像素数据。"""
49
- if self.__pixels is None:
50
- if not self.file_path:
51
- raise ValueError('Either pixels or file_path must be provided.')
52
- logger.debug('Loading image "%s" from %s...', self.name or '(unnamed)', self.file_path)
53
- self.__pixels = cv2_imread(self.file_path)
54
- return self.__pixels
55
-
56
- @property
57
- def size(self) -> Size:
58
- return Size(self.pixels.shape[1], self.pixels.shape[0])
59
-
60
- class Template(Image):
61
- """
62
- 模板图像类。
63
- """
1
+ import logging
2
+ import warnings
3
+ from functools import cache
4
+
5
+ import cv2
6
+ from cv2.typing import MatLike
7
+
8
+ from .geometry import Size, Rect
9
+ from kotonebot.util import cv2_imread
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class Image:
14
+ """
15
+ 图像类。
16
+ """
17
+ def __init__(
18
+ self,
19
+ pixels: MatLike | None = None,
20
+ file_path: str | None = None,
21
+ lazy_load: bool = False,
22
+ name: str | None = None,
23
+ description: str | None = None
24
+ ):
25
+ """
26
+ 从内存数据或图像文件创建图像类。
27
+
28
+ :param pixels: 图像数据。格式必须为 BGR。
29
+ :param file_path: 图像文件路径。
30
+ :param lazy_load: 是否延迟加载图像数据。
31
+ 若为 False,立即载入,否则仅当访问图像数据时才载入。仅当从文件创建图像类时生效。
32
+ :param name: 图像名称。
33
+ :param description: 图像描述。
34
+ """
35
+ self.name: str | None = name
36
+ """图像名称。"""
37
+ self.description: str | None = description
38
+ """图像描述。"""
39
+ self.file_path: str | None = file_path
40
+ """图像的文件路径。"""
41
+ self.__pixels: MatLike | None = None
42
+ # 立即加载
43
+ if not lazy_load and self.file_path:
44
+ _ = self.pixels
45
+ # 传入像素数据而不是文件
46
+ if pixels is not None:
47
+ self.__pixels = pixels
48
+
49
+ @property
50
+ def pixels(self) -> MatLike:
51
+ """图像的像素数据。"""
52
+ if self.__pixels is None:
53
+ if not self.file_path:
54
+ raise ValueError('Either pixels or file_path must be provided.')
55
+ logger.debug('Loading image "%s" from %s...', self.name or '(unnamed)', self.file_path)
56
+ self.__pixels = cv2_imread(self.file_path)
57
+ return self.__pixels
58
+
59
+ @property
60
+ def size(self) -> Size:
61
+ return Size(self.pixels.shape[1], self.pixels.shape[0])
62
+
63
+ # Compatibility with older API (deprecated)
64
+ def __compat_warn(self, name: str) -> None:
65
+ warnings.warn(
66
+ f'`Image.{name}` is deprecated — use `kotonebot.primitives.Image` API instead.',
67
+ DeprecationWarning,
68
+ stacklevel=3,
69
+ )
70
+
71
+ @property
72
+ def path(self) -> str | None:
73
+ """Deprecated alias for `file_path`."""
74
+ self.__compat_warn('path')
75
+ return self.file_path
76
+
77
+ @path.setter
78
+ def path(self, value: str | None) -> None:
79
+ self.__compat_warn('path')
80
+ self.file_path = value
81
+
82
+ @property
83
+ def data(self) -> MatLike:
84
+ """Deprecated alias for `pixels`."""
85
+ self.__compat_warn('data')
86
+ return self.pixels
87
+
88
+ @property
89
+ def data_with_alpha(self) -> MatLike:
90
+ """Deprecated: return image including alpha channel when available."""
91
+ self.__compat_warn('data_with_alpha')
92
+ # If current pixels already contain alpha, return them
93
+ try:
94
+ if self.__pixels is not None and getattr(self.__pixels, 'shape', None) and len(self.__pixels.shape) >= 3 and self.__pixels.shape[2] == 4:
95
+ return self.__pixels
96
+ except Exception:
97
+ pass
98
+ if not self.file_path:
99
+ raise ValueError('Either pixels or file_path must be provided.')
100
+ arr = cv2_imread(self.file_path, cv2.IMREAD_UNCHANGED)
101
+ return arr
102
+
103
+ @cache
104
+ def binary(self) -> 'Image':
105
+ """Deprecated: return a grayscale copy of the image."""
106
+ self.__compat_warn('binary')
107
+ gray = cv2.cvtColor(self.pixels, cv2.COLOR_BGR2GRAY)
108
+ return Image(pixels=gray, name=self.name)
109
+
110
+ def __repr__(self) -> str:
111
+ class_name = self.__class__.__name__
112
+ if self.file_path is None:
113
+ return f'<{class_name}: memory>'
114
+ else:
115
+ return f'<{class_name}: "{self.name or "untitled"}" at {self.file_path}>'
116
+
117
+
118
+ class ImageSlice(Image):
119
+ def __init__(
120
+ self,
121
+ pixels: MatLike | None = None,
122
+ file_path: str | None = None,
123
+ lazy_load: bool = False,
124
+ name: str | None = None,
125
+ description: str | None = None,
126
+ *,
127
+ slice_rect: Rect | None
128
+ ):
129
+ super().__init__(
130
+ pixels=pixels,
131
+ file_path=file_path,
132
+ lazy_load=lazy_load,
133
+ name=name,
134
+ description=description
135
+ )
136
+ self.slice_rect = slice_rect
137
+ """图像切片的矩形区域。"""
138
+
139
+
140
+ class Template(Image):
141
+ """
142
+ 模板图像类。
143
+ """
@@ -1,36 +1,36 @@
1
- import requests
2
- import os
3
-
4
- def upload(file_path: str) -> str:
5
- """
6
- 上传文件到 paste.sensio.no
7
-
8
- Args:
9
- file_path: 要上传的文件路径
10
-
11
- Returns:
12
- str: 上传后的 URL
13
- """
14
- url = 'https://paste.sensio.no/'
15
- headers = {
16
- 'accept': 'text/plain',
17
- 'User-Agent': 'KAA',
18
- 'x-uuid': ''
19
- }
20
-
21
- files = {
22
- 'file': (os.path.basename(file_path), open(file_path, 'rb'))
23
- }
24
-
25
- response = requests.post(url, files=files, headers=headers, allow_redirects=False)
26
-
27
- if response.status_code != 200:
28
- raise Exception(f"Upload failed with status code {response.status_code}")
29
-
30
- return response.text.strip()
31
-
32
- if __name__ == "__main__":
33
- test_file = "version"
34
- if os.path.exists(test_file):
35
- result = upload(test_file)
36
- print(f"Upload result: {result}")
1
+ import requests
2
+ import os
3
+
4
+ def upload(file_path: str) -> str:
5
+ """
6
+ 上传文件到 paste.sensio.no
7
+
8
+ Args:
9
+ file_path: 要上传的文件路径
10
+
11
+ Returns:
12
+ str: 上传后的 URL
13
+ """
14
+ url = 'https://paste.sensio.no/'
15
+ headers = {
16
+ 'accept': 'text/plain',
17
+ 'User-Agent': 'KAA',
18
+ 'x-uuid': ''
19
+ }
20
+
21
+ files = {
22
+ 'file': (os.path.basename(file_path), open(file_path, 'rb'))
23
+ }
24
+
25
+ response = requests.post(url, files=files, headers=headers, allow_redirects=False)
26
+
27
+ if response.status_code != 200:
28
+ raise Exception(f"Upload failed with status code {response.status_code}")
29
+
30
+ return response.text.strip()
31
+
32
+ if __name__ == "__main__":
33
+ test_file = "version"
34
+ if os.path.exists(test_file):
35
+ result = upload(test_file)
36
+ print(f"Upload result: {result}")
@@ -1,54 +1,54 @@
1
- import requests
2
- import os
3
-
4
- def upload(file_path: str) -> str:
5
- url = 'https://tmpsend.com/upload'
6
- headers = {
7
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
8
- 'Referer': 'https://tmpsend.com/',
9
- }
10
-
11
- file_name = os.path.basename(file_path)
12
- file_size = os.path.getsize(file_path)
13
-
14
- # 第一次请求:添加文件信息
15
- files = {
16
- 'action': (None, 'add'),
17
- 'name': (None, file_name),
18
- 'size': (None, str(file_size)),
19
- 'file': (file_name, open(file_path, 'rb'))
20
- }
21
-
22
- response = requests.post(url, headers=headers, files=files)
23
- if response.status_code != 200:
24
- raise Exception(f"Upload failed with status code {response.status_code}")
25
-
26
- result = response.json()
27
- if result.get('hasError'):
28
- raise Exception(result.get('error'))
29
-
30
- file_id = result.get('id')
31
- if not file_id:
32
- raise Exception("Failed to get file ID")
33
-
34
- # 第二次请求:上传实际文件
35
- upload_files = {
36
- 'action': (None, 'upload'),
37
- 'id': (None, file_id),
38
- 'name': (None, file_name),
39
- 'size': (None, str(file_size)),
40
- 'start': (None, '0'),
41
- 'end': (None, str(file_size)),
42
- 'data': (file_name, open(file_path, 'rb'), 'application/octet-stream')
43
- }
44
-
45
- upload_response = requests.post(url, headers=headers, files=upload_files)
46
- if upload_response.status_code != 200:
47
- raise Exception(f"File upload failed with status code {upload_response.status_code}")
48
-
49
- return 'https://tmpsend.com/' + file_id
50
-
51
- if __name__ == "__main__":
52
- file_path = r"主题1.thmx"
53
- print(upload(file_path))
54
-
1
+ import requests
2
+ import os
3
+
4
+ def upload(file_path: str) -> str:
5
+ url = 'https://tmpsend.com/upload'
6
+ headers = {
7
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
8
+ 'Referer': 'https://tmpsend.com/',
9
+ }
10
+
11
+ file_name = os.path.basename(file_path)
12
+ file_size = os.path.getsize(file_path)
13
+
14
+ # 第一次请求:添加文件信息
15
+ files = {
16
+ 'action': (None, 'add'),
17
+ 'name': (None, file_name),
18
+ 'size': (None, str(file_size)),
19
+ 'file': (file_name, open(file_path, 'rb'))
20
+ }
21
+
22
+ response = requests.post(url, headers=headers, files=files)
23
+ if response.status_code != 200:
24
+ raise Exception(f"Upload failed with status code {response.status_code}")
25
+
26
+ result = response.json()
27
+ if result.get('hasError'):
28
+ raise Exception(result.get('error'))
29
+
30
+ file_id = result.get('id')
31
+ if not file_id:
32
+ raise Exception("Failed to get file ID")
33
+
34
+ # 第二次请求:上传实际文件
35
+ upload_files = {
36
+ 'action': (None, 'upload'),
37
+ 'id': (None, file_id),
38
+ 'name': (None, file_name),
39
+ 'size': (None, str(file_size)),
40
+ 'start': (None, '0'),
41
+ 'end': (None, str(file_size)),
42
+ 'data': (file_name, open(file_path, 'rb'), 'application/octet-stream')
43
+ }
44
+
45
+ upload_response = requests.post(url, headers=headers, files=upload_files)
46
+ if upload_response.status_code != 200:
47
+ raise Exception(f"File upload failed with status code {upload_response.status_code}")
48
+
49
+ return 'https://tmpsend.com/' + file_id
50
+
51
+ if __name__ == "__main__":
52
+ file_path = r"主题1.thmx"
53
+ print(upload(file_path))
54
+
@@ -1,3 +1,3 @@
1
- from .protocol import PushkitProtocol
2
- from .wxpusher import Wxpusher
3
-
1
+ from .protocol import PushkitProtocol
2
+ from .wxpusher import Wxpusher
3
+
@@ -1,88 +1,88 @@
1
- import os
2
- import tempfile
3
- from pathlib import Path
4
- from typing import Sequence
5
-
6
- import cv2
7
- import numpy as np
8
- from cv2.typing import MatLike
9
- from dotenv import load_dotenv
10
-
11
- load_dotenv()
12
-
13
- def _save_temp_image(image: MatLike) -> Path:
14
- """将OpenCV图片保存为临时文件"""
15
- temp_file = Path(tempfile.mktemp(suffix='.jpg'))
16
- cv2.imwrite(str(temp_file), image)
17
- return temp_file
18
-
19
- def _upload_single(image: MatLike | str) -> str:
20
- """
21
- 上传单张图片到freeimage.host
22
-
23
- :param image: OpenCV MatLike 或本地图片文件路径
24
- """
25
- import requests
26
-
27
- api_url = 'https://freeimage.host/api/1/upload'
28
- api_key = os.getenv('FREEIMAGEHOST_KEY')
29
-
30
- if not api_key:
31
- raise RuntimeError('Environment variable FREEIMAGEHOST_KEY is not set')
32
-
33
- # 处理输入
34
- temp_file = None
35
- if isinstance(image, str):
36
- # 本地文件路径
37
- files = {'source': open(image, 'rb')}
38
- else:
39
- # 保存OpenCV图片为临时文件
40
- temp_file = _save_temp_image(image)
41
- files = {'source': open(temp_file, 'rb')}
42
-
43
- data = {
44
- 'key': api_key,
45
- 'action': 'upload',
46
- 'format': 'json'
47
- }
48
-
49
- try:
50
- # 发送POST请求
51
- response = requests.post(api_url, data=data, files=files)
52
-
53
- if response.status_code != 200:
54
- raise RuntimeError(f'Upload failed: HTTP {response.status_code}')
55
-
56
- result = response.json()
57
-
58
- if result['status_code'] != 200:
59
- raise RuntimeError(f'Upload failed: API {result["status_txt"]}')
60
-
61
- return result['image']['url']
62
-
63
- finally:
64
- # 清理临时文件
65
- files['source'].close()
66
- if temp_file and temp_file.exists():
67
- temp_file.unlink()
68
-
69
- def upload(images: MatLike | str | Sequence[MatLike | str]) -> list[str]:
70
- """上传一张或多张图片到freeimage.host
71
-
72
- Args:
73
- images: 单张图片或图片列表。每个图片可以是OpenCV图片对象或本地图片文件路径
74
-
75
- Returns:
76
- 上传后的图片URL列表
77
- """
78
- if isinstance(images, (str, np.ndarray)):
79
- _images = [images]
80
- elif isinstance(images, Sequence):
81
- _images = [img for img in images]
82
- else:
83
- raise ValueError("Invalid input type")
84
-
85
- return [_upload_single(img) for img in _images]
86
-
87
- if __name__ == "__main__":
88
- print(upload(cv2.imread("res/sprites/jp/common/button_close.png")))
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+ from typing import Sequence
5
+
6
+ import cv2
7
+ import numpy as np
8
+ from cv2.typing import MatLike
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ def _save_temp_image(image: MatLike) -> Path:
14
+ """将OpenCV图片保存为临时文件"""
15
+ temp_file = Path(tempfile.mktemp(suffix='.jpg'))
16
+ cv2.imwrite(str(temp_file), image)
17
+ return temp_file
18
+
19
+ def _upload_single(image: MatLike | str) -> str:
20
+ """
21
+ 上传单张图片到freeimage.host
22
+
23
+ :param image: OpenCV MatLike 或本地图片文件路径
24
+ """
25
+ import requests
26
+
27
+ api_url = 'https://freeimage.host/api/1/upload'
28
+ api_key = os.getenv('FREEIMAGEHOST_KEY')
29
+
30
+ if not api_key:
31
+ raise RuntimeError('Environment variable FREEIMAGEHOST_KEY is not set')
32
+
33
+ # 处理输入
34
+ temp_file = None
35
+ if isinstance(image, str):
36
+ # 本地文件路径
37
+ files = {'source': open(image, 'rb')}
38
+ else:
39
+ # 保存OpenCV图片为临时文件
40
+ temp_file = _save_temp_image(image)
41
+ files = {'source': open(temp_file, 'rb')}
42
+
43
+ data = {
44
+ 'key': api_key,
45
+ 'action': 'upload',
46
+ 'format': 'json'
47
+ }
48
+
49
+ try:
50
+ # 发送POST请求
51
+ response = requests.post(api_url, data=data, files=files)
52
+
53
+ if response.status_code != 200:
54
+ raise RuntimeError(f'Upload failed: HTTP {response.status_code}')
55
+
56
+ result = response.json()
57
+
58
+ if result['status_code'] != 200:
59
+ raise RuntimeError(f'Upload failed: API {result["status_txt"]}')
60
+
61
+ return result['image']['url']
62
+
63
+ finally:
64
+ # 清理临时文件
65
+ files['source'].close()
66
+ if temp_file and temp_file.exists():
67
+ temp_file.unlink()
68
+
69
+ def upload(images: MatLike | str | Sequence[MatLike | str]) -> list[str]:
70
+ """上传一张或多张图片到freeimage.host
71
+
72
+ Args:
73
+ images: 单张图片或图片列表。每个图片可以是OpenCV图片对象或本地图片文件路径
74
+
75
+ Returns:
76
+ 上传后的图片URL列表
77
+ """
78
+ if isinstance(images, (str, np.ndarray)):
79
+ _images = [images]
80
+ elif isinstance(images, Sequence):
81
+ _images = [img for img in images]
82
+ else:
83
+ raise ValueError("Invalid input type")
84
+
85
+ return [_upload_single(img) for img in _images]
86
+
87
+ if __name__ == "__main__":
88
+ print(upload(cv2.imread("res/sprites/jp/common/button_close.png")))
@@ -1,13 +1,13 @@
1
- from typing import Protocol
2
-
3
- from cv2.typing import MatLike
4
-
5
- class PushkitProtocol(Protocol):
6
- def push(
7
- self,
8
- title: str,
9
- message: str,
10
- *,
11
- images: list[str | MatLike] | None = None,
12
- ) -> None:
13
- ...
1
+ from typing import Protocol
2
+
3
+ from cv2.typing import MatLike
4
+
5
+ class PushkitProtocol(Protocol):
6
+ def push(
7
+ self,
8
+ title: str,
9
+ message: str,
10
+ *,
11
+ images: list[str | MatLike] | None = None,
12
+ ) -> None:
13
+ ...