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