kotonebot 0.1.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 (70) hide show
  1. kotonebot/__init__.py +40 -0
  2. kotonebot/backend/__init__.py +0 -0
  3. kotonebot/backend/bot.py +302 -0
  4. kotonebot/backend/color.py +525 -0
  5. kotonebot/backend/context/__init__.py +3 -0
  6. kotonebot/backend/context/context.py +1001 -0
  7. kotonebot/backend/context/task_action.py +176 -0
  8. kotonebot/backend/core.py +126 -0
  9. kotonebot/backend/debug/__init__.py +1 -0
  10. kotonebot/backend/debug/entry.py +89 -0
  11. kotonebot/backend/debug/mock.py +79 -0
  12. kotonebot/backend/debug/server.py +223 -0
  13. kotonebot/backend/debug/vars.py +346 -0
  14. kotonebot/backend/dispatch.py +228 -0
  15. kotonebot/backend/flow_controller.py +197 -0
  16. kotonebot/backend/image.py +748 -0
  17. kotonebot/backend/loop.py +277 -0
  18. kotonebot/backend/ocr.py +511 -0
  19. kotonebot/backend/preprocessor.py +103 -0
  20. kotonebot/client/__init__.py +10 -0
  21. kotonebot/client/device.py +500 -0
  22. kotonebot/client/fast_screenshot.py +378 -0
  23. kotonebot/client/host/__init__.py +12 -0
  24. kotonebot/client/host/adb_common.py +94 -0
  25. kotonebot/client/host/custom.py +114 -0
  26. kotonebot/client/host/leidian_host.py +202 -0
  27. kotonebot/client/host/mumu12_host.py +245 -0
  28. kotonebot/client/host/protocol.py +213 -0
  29. kotonebot/client/host/windows_common.py +55 -0
  30. kotonebot/client/implements/__init__.py +7 -0
  31. kotonebot/client/implements/adb.py +85 -0
  32. kotonebot/client/implements/adb_raw.py +159 -0
  33. kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
  34. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
  35. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
  36. kotonebot/client/implements/remote_windows.py +193 -0
  37. kotonebot/client/implements/uiautomator2.py +82 -0
  38. kotonebot/client/implements/windows.py +168 -0
  39. kotonebot/client/protocol.py +69 -0
  40. kotonebot/client/registration.py +24 -0
  41. kotonebot/config/__init__.py +1 -0
  42. kotonebot/config/base_config.py +96 -0
  43. kotonebot/config/manager.py +36 -0
  44. kotonebot/errors.py +72 -0
  45. kotonebot/interop/win/__init__.py +0 -0
  46. kotonebot/interop/win/message_box.py +314 -0
  47. kotonebot/interop/win/reg.py +37 -0
  48. kotonebot/interop/win/shortcut.py +43 -0
  49. kotonebot/interop/win/task_dialog.py +469 -0
  50. kotonebot/logging/__init__.py +2 -0
  51. kotonebot/logging/log.py +18 -0
  52. kotonebot/primitives/__init__.py +17 -0
  53. kotonebot/primitives/geometry.py +290 -0
  54. kotonebot/primitives/visual.py +63 -0
  55. kotonebot/tools/__init__.py +0 -0
  56. kotonebot/tools/mirror.py +354 -0
  57. kotonebot/ui/__init__.py +0 -0
  58. kotonebot/ui/file_host/sensio.py +36 -0
  59. kotonebot/ui/file_host/tmp_send.py +54 -0
  60. kotonebot/ui/pushkit/__init__.py +3 -0
  61. kotonebot/ui/pushkit/image_host.py +87 -0
  62. kotonebot/ui/pushkit/protocol.py +13 -0
  63. kotonebot/ui/pushkit/wxpusher.py +53 -0
  64. kotonebot/ui/user.py +144 -0
  65. kotonebot/util.py +409 -0
  66. kotonebot-0.1.0.dist-info/METADATA +204 -0
  67. kotonebot-0.1.0.dist-info/RECORD +70 -0
  68. kotonebot-0.1.0.dist-info/WHEEL +5 -0
  69. kotonebot-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. kotonebot-0.1.0.dist-info/top_level.txt +1 -0
kotonebot/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ from .backend.context import (
2
+ ContextOcr,
3
+ ContextImage,
4
+ ContextDebug,
5
+ ContextColor,
6
+ device,
7
+ ocr,
8
+ image,
9
+ debug,
10
+ color,
11
+ config,
12
+ rect_expand,
13
+ sleep,
14
+ task,
15
+ action,
16
+ use_screenshot,
17
+ wait
18
+ )
19
+ from .util import (
20
+ cropped,
21
+ AdaptiveWait,
22
+ Countdown,
23
+ Interval,
24
+ until,
25
+ )
26
+ from .backend.color import (
27
+ hsv_cv2web,
28
+ hsv_web2cv,
29
+ rgb_to_hsv,
30
+ hsv_to_rgb
31
+ )
32
+ from .backend.ocr import (
33
+ fuzz,
34
+ regex,
35
+ contains,
36
+ equals,
37
+ )
38
+ from .backend.bot import KotoneBot
39
+ from .backend.loop import Loop
40
+ from .ui import user
File without changes
@@ -0,0 +1,302 @@
1
+ import io
2
+ import os
3
+ import logging
4
+ import pkgutil
5
+ import importlib
6
+ import threading
7
+ from typing_extensions import Self
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Literal, Callable, Generic, TypeVar, ParamSpec
10
+
11
+ from kotonebot.client import Device
12
+ from kotonebot.client.host.protocol import Instance
13
+ from kotonebot.backend.context import init_context, vars
14
+ from kotonebot.backend.context import task_registry, action_registry, Task, Action
15
+ from kotonebot.errors import StopCurrentTask, UserFriendlyError
16
+ from kotonebot.interop.win.task_dialog import TaskDialog
17
+
18
+
19
+ @dataclass
20
+ class PostTaskContext:
21
+ has_error: bool
22
+ exception: Exception | None
23
+
24
+
25
+ log_stream = io.StringIO()
26
+ stream_handler = logging.StreamHandler(log_stream)
27
+ stream_handler.setFormatter(logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] [%(filename)s:%(lineno)d] - %(message)s'))
28
+ logging.getLogger('kotonebot').addHandler(stream_handler)
29
+ logger = logging.getLogger(__name__)
30
+
31
+ TaskStatusValue = Literal['pending', 'running', 'finished', 'error', 'cancelled', 'stopped']
32
+ @dataclass
33
+ class TaskStatus:
34
+ task: Task
35
+ status: TaskStatusValue
36
+
37
+ @dataclass
38
+ class RunStatus:
39
+ running: bool = False
40
+ tasks: list[TaskStatus] = field(default_factory=list)
41
+ current_task: Task | None = None
42
+ callstack: list[Task | Action] = field(default_factory=list)
43
+
44
+ def interrupt(self):
45
+ vars.flow.request_interrupt()
46
+
47
+ # Modified from https://stackoverflow.com/questions/70982565/how-do-i-make-an-event-listener-with-decorators-in-python
48
+ Params = ParamSpec('Params')
49
+ Return = TypeVar('Return')
50
+ class Event(Generic[Params, Return]):
51
+ def __init__(self):
52
+ self.__listeners = []
53
+
54
+ @property
55
+ def on(self):
56
+ def wrapper(func: Callable[Params, Return]):
57
+ self.add_listener(func)
58
+ return func
59
+ return wrapper
60
+
61
+ def add_listener(self, func: Callable[Params, Return]) -> None:
62
+ if func in self.__listeners:
63
+ return
64
+ self.__listeners.append(func)
65
+
66
+ def remove_listener(self, func: Callable[Params, Return]) -> None:
67
+ if func not in self.__listeners:
68
+ return
69
+ self.__listeners.remove(func)
70
+
71
+ def __iadd__(self, func: Callable[Params, Return]) -> Self:
72
+ self.add_listener(func)
73
+ return self
74
+
75
+ def __isub__(self, func: Callable[Params, Return]) -> Self:
76
+ self.remove_listener(func)
77
+ return self
78
+
79
+ def trigger(self, *args: Params.args, **kwargs: Params.kwargs) -> None:
80
+ for func in self.__listeners:
81
+ func(*args, **kwargs)
82
+
83
+ class KotoneBotEvents:
84
+ def __init__(self):
85
+ self.task_status_changed = Event[
86
+ [Task, TaskStatusValue], None
87
+ ]()
88
+ self.task_error = Event[
89
+ [Task, Exception], None
90
+ ]()
91
+ self.finished = Event[[], None]()
92
+
93
+
94
+ class KotoneBot:
95
+ def __init__(
96
+ self,
97
+ module: str,
98
+ config_path: str,
99
+ config_type: type = dict[str, Any],
100
+ *,
101
+ debug: bool = False,
102
+ resume_on_error: bool = False,
103
+ auto_save_error_report: bool = False,
104
+ ):
105
+ """
106
+ 初始化 KotoneBot。
107
+
108
+ :param module: 主模块名。此模块及其所有子模块都会被载入。
109
+ :param config_type: 配置类型。
110
+ :param debug: 调试模式。
111
+ :param resume_on_error: 在错误时是否恢复。
112
+ :param auto_save_error_report: 是否自动保存错误报告。
113
+ """
114
+ self.module = module
115
+ self.config_path = config_path
116
+ self.config_type = config_type
117
+ # HACK: 硬编码
118
+ self.current_config: int | str = 0
119
+ self.debug = debug
120
+ self.resume_on_error = resume_on_error
121
+ self.auto_save_error_report = auto_save_error_report
122
+ self.events = KotoneBotEvents()
123
+ self.backend_instance: Instance | None = None
124
+
125
+ if self.auto_save_error_report:
126
+ raise NotImplementedError('auto_save_error_report not implemented yet.')
127
+
128
+ def initialize(self):
129
+ """
130
+ 初始化并载入所有任务和动作。
131
+ """
132
+ logger.info('Initializing tasks and actions...')
133
+ logger.debug(f'Loading module: {self.module}')
134
+ # 加载主模块
135
+ importlib.import_module(self.module)
136
+
137
+ # 加载所有子模块
138
+ pkg = importlib.import_module(self.module)
139
+ for loader, name, is_pkg in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + '.'):
140
+ logger.debug(f'Loading sub-module: {name}')
141
+ try:
142
+ importlib.import_module(name)
143
+ except Exception:
144
+ logger.error(f'Failed to load sub-module: {name}')
145
+ logger.exception('Error: ')
146
+
147
+ logger.info('Tasks and actions initialized.')
148
+ logger.info(f'{len(task_registry)} task(s) and {len(action_registry)} action(s) loaded.')
149
+
150
+ def _on_create_device(self) -> Device:
151
+ """
152
+ 抽象方法,用于创建 Device 类,在 `run()` 方法执行前会被调用。
153
+
154
+ 所有子类都需要重写该方法。
155
+ """
156
+ raise NotImplementedError('Implement `_create_device` before using Kotonebot.')
157
+
158
+ def _on_init_context(self) -> None:
159
+ """
160
+ 初始化 Context 的钩子方法。子类可以重写此方法来自定义初始化逻辑。
161
+ 默认实现调用 init_context 而不传入 target_screenshot_interval。
162
+ """
163
+ d = self._on_create_device()
164
+ init_context(
165
+ config_path=self.config_path,
166
+ config_type=self.config_type,
167
+ target_device=d
168
+ )
169
+
170
+ def _on_after_init_context(self):
171
+ """
172
+ 抽象方法,在 init_context() 被调用后立即执行。
173
+ """
174
+ pass
175
+
176
+ def run(self, tasks: list[Task], *, by_priority: bool = True):
177
+ """
178
+ 按优先级顺序运行所有任务。
179
+ """
180
+ self._on_init_context()
181
+ self._on_after_init_context()
182
+ vars.flow.clear_interrupt()
183
+
184
+ pre_tasks = [task for task in tasks if task.run_at == 'pre']
185
+ regular_tasks = [task for task in tasks if task.run_at == 'regular']
186
+ post_tasks = [task for task in tasks if task.run_at == 'post']
187
+
188
+ if by_priority:
189
+ pre_tasks = sorted(pre_tasks, key=lambda x: x.priority, reverse=True)
190
+ regular_tasks = sorted(regular_tasks, key=lambda x: x.priority, reverse=True)
191
+ post_tasks = sorted(post_tasks, key=lambda x: x.priority, reverse=True)
192
+
193
+ all_tasks = pre_tasks + regular_tasks + post_tasks
194
+ for task in all_tasks:
195
+ self.events.task_status_changed.trigger(task, 'pending')
196
+
197
+ has_error = False
198
+ exception: Exception | None = None
199
+
200
+ for task in all_tasks:
201
+ logger.info(f'Task started: {task.name}')
202
+ self.events.task_status_changed.trigger(task, 'running')
203
+
204
+ if self.debug:
205
+ if task.run_at == 'post':
206
+ task.func(PostTaskContext(has_error, exception))
207
+ else:
208
+ task.func()
209
+ else:
210
+ try:
211
+ if task.run_at == 'post':
212
+ task.func(PostTaskContext(has_error, exception))
213
+ else:
214
+ task.func()
215
+ self.events.task_status_changed.trigger(task, 'finished')
216
+ except StopCurrentTask:
217
+ logger.info(f'Task skipped/stopped: {task.name}')
218
+ self.events.task_status_changed.trigger(task, 'stopped')
219
+ # 用户中止
220
+ except KeyboardInterrupt as e:
221
+ logger.exception('Keyboard interrupt detected.')
222
+ for task1 in all_tasks[all_tasks.index(task):]:
223
+ self.events.task_status_changed.trigger(task1, 'cancelled')
224
+ vars.flow.clear_interrupt()
225
+ break
226
+ # 用户可以自行处理的错误
227
+ except UserFriendlyError as e:
228
+ logger.error(f'Task failed: {task.name}')
229
+ logger.exception(f'Error: ')
230
+ has_error = True
231
+ exception = e
232
+ dialog = TaskDialog(
233
+ title='琴音小助手',
234
+ common_buttons=0,
235
+ main_instruction='任务执行失败',
236
+ content=e.message,
237
+ custom_buttons=e.action_buttons,
238
+ main_icon='error'
239
+ )
240
+ result_custom, _, _ = dialog.show()
241
+ e.invoke(result_custom)
242
+ # 其他错误
243
+ except Exception as e:
244
+ logger.error(f'Task failed: {task.name}')
245
+ logger.exception(f'Error: ')
246
+ has_error = True
247
+ exception = e
248
+ report_path = None
249
+ if self.auto_save_error_report:
250
+ raise NotImplementedError
251
+ self.events.task_status_changed.trigger(task, 'error')
252
+ if not self.resume_on_error:
253
+ for task1 in all_tasks[all_tasks.index(task)+1:]:
254
+ self.events.task_status_changed.trigger(task1, 'cancelled')
255
+ break
256
+ logger.info(f'Task ended: {task.name}')
257
+ logger.info('All tasks ended.')
258
+ self.events.finished.trigger()
259
+
260
+ def run_all(self) -> None:
261
+ return self.run(list(task_registry.values()), by_priority=True)
262
+
263
+ def start(self, tasks: list[Task], *, by_priority: bool = True) -> RunStatus:
264
+ """
265
+ 在单独的线程中按优先级顺序运行指定的任务。
266
+
267
+ :param tasks: 要运行的任务列表
268
+ :param by_priority: 是否按优先级排序
269
+ :return: 运行状态对象
270
+ """
271
+ run_status = RunStatus(running=True)
272
+ def _on_finished():
273
+ run_status.running = False
274
+ run_status.current_task = None
275
+ run_status.callstack = []
276
+ self.events.finished -= _on_finished
277
+ self.events.task_status_changed -= _on_task_status_changed
278
+
279
+ def _on_task_status_changed(task: Task, status: TaskStatusValue):
280
+ def _find(task: Task) -> TaskStatus:
281
+ for task_status in run_status.tasks:
282
+ if task_status.task == task:
283
+ return task_status
284
+ raise ValueError(f'Task {task.name} not found in run_status.tasks')
285
+ if status == 'pending':
286
+ run_status.tasks.append(TaskStatus(task=task, status='pending'))
287
+ else:
288
+ _find(task).status = status
289
+
290
+ self.events.task_status_changed += _on_task_status_changed
291
+ self.events.finished += _on_finished
292
+ thread = threading.Thread(target=lambda: self.run(tasks, by_priority=by_priority))
293
+ thread.start()
294
+ return run_status
295
+
296
+ def start_all(self) -> RunStatus:
297
+ """
298
+ 在单独的线程中运行所有任务。
299
+
300
+ :return: 运行状态对象
301
+ """
302
+ return self.start(list(task_registry.values()), by_priority=True)