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