kotonebot 0.3.1__py3-none-any.whl → 0.4.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/backend/bot.py CHANGED
@@ -13,7 +13,16 @@ from kotonebot.client.host.protocol import Instance
13
13
  from kotonebot.backend.context import init_context, vars
14
14
  from kotonebot.backend.context import task_registry, action_registry, Task, Action
15
15
  from kotonebot.errors import StopCurrentTask, UserFriendlyError
16
- from kotonebot.interop.win.task_dialog import TaskDialog
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
17
26
 
18
27
 
19
28
  @dataclass
@@ -226,19 +235,20 @@ class KotoneBot:
226
235
  # 用户可以自行处理的错误
227
236
  except UserFriendlyError as e:
228
237
  logger.error(f'Task failed: {task.name}')
229
- logger.exception(f'Error: ')
238
+ logger.exception('Error: ')
230
239
  has_error = True
231
240
  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)
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)
242
252
  # 其他错误
243
253
  except Exception as e:
244
254
  logger.error(f'Task failed: {task.name}')
@@ -175,6 +175,8 @@ class ContextGlobalVars:
175
175
  self.__vars = dict[str, Any]()
176
176
  self.flow: FlowController = FlowController()
177
177
  """流程控制器,负责停止、暂停、恢复等操作"""
178
+ self.screenshot_data: MatLike | None = None
179
+ """截图数据"""
178
180
 
179
181
  def __getitem__(self, key: str) -> Any:
180
182
  return self.__vars[key]
@@ -197,6 +199,7 @@ class ContextGlobalVars:
197
199
  def clear(self):
198
200
  self.__vars.clear()
199
201
  self.flow.reset() # 重置流程控制器
202
+ self.screenshot_data = None
200
203
 
201
204
  def check_flow_control():
202
205
  """
@@ -220,45 +223,40 @@ class ContextStackVars:
220
223
  自动截图。即调用 `color`、`image`、`ocr` 上的方法时,会自动更新截图。
221
224
  * `manual`
222
225
  完全手动截图,不自动截图。如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
223
- * `manual-inherit`:
224
- 第一张截图继承自调用者,此后同手动截图。
225
- 如果调用者没有截图数据,则继承的截图数据为空。
226
- 如果在没有截图数据的情况下调用 `color` 等的方法,会抛出异常。
226
+ * ~~`manual-inherit`~~:
227
+ 已废弃。
227
228
  """
228
- self._screenshot: MatLike | None = None
229
- """截图数据"""
230
- self._inherit_screenshot: MatLike | None = None
231
- """继承的截图数据"""
232
229
 
233
230
  @property
234
231
  def screenshot(self) -> MatLike:
235
232
  match self.screenshot_mode:
236
- case 'manual':
237
- if self._screenshot is None:
238
- raise ValueError("No screenshot data found.")
239
- return self._screenshot
240
- case 'manual-inherit':
241
- # TODO: 这一部分要考虑和 device.screenshot() 合并
242
- if self._inherit_screenshot is not None:
243
- self._screenshot = self._inherit_screenshot
244
- self._inherit_screenshot = None
245
- if self._screenshot is None:
246
- raise ValueError("No screenshot data found.")
247
- return self._screenshot
233
+ case 'manual' | 'manual-inherit':
234
+ if vars.screenshot_data is None:
235
+ raise ValueError("No screenshot data found. Did you forget to call `device.screenshot()`?")
236
+ return vars.screenshot_data
248
237
  case 'auto':
249
- self._screenshot = device.screenshot()
250
- return self._screenshot
238
+ device.screenshot()
239
+ if vars.screenshot_data is None:
240
+ raise ValueError("No screenshot data found. Did you forget to call `device.screenshot()`?")
241
+ return vars.screenshot_data
251
242
  case _:
252
243
  raise ValueError(f"Invalid screenshot mode: {self.screenshot_mode}")
253
244
 
245
+ @property
246
+ @deprecated('Use `vars.screenshot_data` instead.')
247
+ def _screenshot(self) -> MatLike | None:
248
+ return vars.screenshot_data
249
+
250
+ @_screenshot.setter
251
+ @deprecated('Use `vars.screenshot_data` instead.')
252
+ def _screenshot(self, value: MatLike | None) -> None:
253
+ vars.screenshot_data = value
254
+
254
255
  @staticmethod
255
256
  def push(*, screenshot_mode: ScreenshotMode | None = None) -> 'ContextStackVars':
256
257
  vars = ContextStackVars()
257
258
  if screenshot_mode is not None:
258
259
  vars.screenshot_mode = screenshot_mode
259
- current = ContextStackVars.current()
260
- if current and vars.screenshot_mode == 'manual-inherit':
261
- vars._inherit_screenshot = current._screenshot
262
260
  ContextStackVars.stack.append(vars)
263
261
  return vars
264
262
 
@@ -266,7 +264,7 @@ class ContextStackVars:
266
264
  def pop() -> 'ContextStackVars':
267
265
  last = ContextStackVars.stack.pop()
268
266
  return last
269
-
267
+
270
268
  @staticmethod
271
269
  def current() -> 'ContextStackVars | None':
272
270
  if len(ContextStackVars.stack) == 0:
@@ -331,7 +329,7 @@ class ContextOcr:
331
329
  )
332
330
  self.context.device.last_find = ret.original_rect if ret else None
333
331
  return ret
334
-
332
+
335
333
  def find_all(
336
334
  self,
337
335
  patterns: Sequence[str | re.Pattern | StringMatchFunction],
@@ -366,7 +364,7 @@ class ContextOcr:
366
364
  ret = engine.expect(ContextStackVars.ensure_current().screenshot, pattern, rect=rect, hint=hint)
367
365
  self.context.device.last_find = ret.original_rect if ret else None
368
366
  return ret
369
-
367
+
370
368
  def expect_wait(
371
369
  self,
372
370
  pattern: str | re.Pattern | StringMatchFunction,
@@ -447,7 +445,7 @@ class ContextImage:
447
445
  等待指定图像出现。
448
446
  """
449
447
  is_manual = is_manual_screenshot_mode()
450
-
448
+
451
449
  start_time = time.time()
452
450
  while True:
453
451
  if is_manual:
@@ -702,7 +700,7 @@ class ContextConfig(Generic[T]):
702
700
  def current(self) -> UserConfig[T]:
703
701
  """
704
702
  当前配置数据。
705
-
703
+
706
704
  如果当前配置不存在,则使用默认值自动创建一个新配置。
707
705
  (不推荐,建议在 UI 中启动前要求用户手动创建,或自行创建一个默认配置。)
708
706
  """
@@ -729,7 +727,7 @@ class Forwarded:
729
727
  if self._FORWARD_getter is None:
730
728
  raise ContextNotInitializedError(f"Forwarded object {self._FORWARD_name} called before initialization.")
731
729
  return getattr(self._FORWARD_getter(), name)
732
-
730
+
733
731
  def __setattr__(self, name: str, value: Any):
734
732
  if name.startswith('_FORWARD_'):
735
733
  return object.__setattr__(self, name, value)
@@ -761,25 +759,20 @@ class ContextDevice(Generic[T_Device], Device):
761
759
  """
762
760
  check_flow_control()
763
761
  global next_wait, last_screenshot_time, next_wait_time
764
- current = ContextStackVars.ensure_current()
765
- if force:
766
- current._inherit_screenshot = None
767
- if current._inherit_screenshot is not None:
768
- img = current._inherit_screenshot
769
- current._inherit_screenshot = None
770
- else:
771
- if self._screenshot_interval is not None:
772
- self._screenshot_interval.wait()
773
-
774
- if next_wait == 'screenshot':
775
- delta = time.time() - last_screenshot_time
776
- if delta < next_wait_time:
777
- sleep(next_wait_time - delta)
778
- last_screenshot_time = time.time()
779
- next_wait_time = 0
780
- next_wait = None
781
- img = self._device.screenshot()
782
- current._screenshot = img
762
+ ContextStackVars.ensure_current()
763
+
764
+ if self._screenshot_interval is not None:
765
+ self._screenshot_interval.wait()
766
+
767
+ if next_wait == 'screenshot':
768
+ delta = time.time() - last_screenshot_time
769
+ if delta < next_wait_time:
770
+ sleep(next_wait_time - delta)
771
+ last_screenshot_time = time.time()
772
+ next_wait_time = 0
773
+ next_wait = None
774
+ img = self._device.screenshot()
775
+ vars.screenshot_data = img
783
776
  return img
784
777
 
785
778
  def __getattribute__(self, name: str):
@@ -787,7 +780,7 @@ class ContextDevice(Generic[T_Device], Device):
787
780
  return object.__getattribute__(self, name)
788
781
  else:
789
782
  return getattr(self._device, name)
790
-
783
+
791
784
  def __setattr__(self, name: str, value: Any):
792
785
  if name in ['_device', 'screenshot', 'of_android', 'of_windows']:
793
786
  return object.__setattr__(self, name, value)
@@ -853,7 +846,7 @@ class Context(Generic[T]):
853
846
  if vars is not None:
854
847
  self.__vars = vars
855
848
  if debug is not None:
856
- self.__debug = debug
849
+ self.__debug = debug
857
850
  if config is not None:
858
851
  self.__config = config
859
852
 
@@ -864,7 +857,7 @@ class Context(Generic[T]):
864
857
  @property
865
858
  def ocr(self) -> 'ContextOcr':
866
859
  return self.__ocr
867
-
860
+
868
861
  @property
869
862
  def image(self) -> 'ContextImage':
870
863
  return self.__image
@@ -876,7 +869,7 @@ class Context(Generic[T]):
876
869
  @property
877
870
  def vars(self) -> 'ContextGlobalVars':
878
871
  return self.__vars
879
-
872
+
880
873
  @property
881
874
  def debug(self) -> 'ContextDebug':
882
875
  return self.__debug
@@ -895,7 +888,7 @@ def rect_expand(rect: Rect, left: int = 0, top: int = 0, right: int = 0, bottom:
895
888
  def use_screenshot(*args: MatLike | None) -> MatLike:
896
889
  for img in args:
897
890
  if img is not None:
898
- ContextStackVars.ensure_current()._screenshot = img # HACK
891
+ vars.screenshot_data = img
899
892
  return img
900
893
  return device.screenshot()
901
894
 
@@ -1006,4 +999,4 @@ def manual_context(screenshot_mode: ScreenshotMode = 'auto') -> ManualContextMan
1006
999
  默认情况下,Context* 类仅允许在 @task/@action 函数中使用。
1007
1000
  如果想要在其他地方使用,使用此函数手动创建一个上下文。
1008
1001
  """
1009
- return ManualContextManager(screenshot_mode)
1002
+ return ManualContextManager(screenshot_mode)
@@ -1,6 +1,8 @@
1
1
  import logging
2
- from typing import Callable, ParamSpec, TypeVar, overload, Literal
2
+ import warnings
3
3
  from dataclasses import dataclass
4
+ from typing_extensions import deprecated
5
+ from typing import Callable, ParamSpec, TypeVar, overload, Literal
4
6
 
5
7
 
6
8
  from .context import ContextStackVars, ScreenshotMode
@@ -93,6 +95,17 @@ def task(
93
95
  @overload
94
96
  def action(func: Callable[P, R]) -> Callable[P, R]: ...
95
97
 
98
+ @deprecated('Use `action` with screenshot_mode=`manual` instead.')
99
+ @overload
100
+ def action(
101
+ name: str,
102
+ *,
103
+ description: str|None = None,
104
+ pass_through: bool = False,
105
+ priority: int = 0,
106
+ screenshot_mode: Literal['manual-inherit'],
107
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
108
+
96
109
  @overload
97
110
  def action(
98
111
  name: str,
@@ -115,9 +128,6 @@ def action(
115
128
  """
116
129
  ...
117
130
 
118
- # TODO: 需要找个地方统一管理这些属性名
119
- ATTR_ORIGINAL_FUNC = '_kb_inner'
120
- ATTR_ACTION_MARK = '__kb_action_mark'
121
131
  def action(*args, **kwargs):
122
132
  def _register(func: Callable, name: str, description: str|None = None, priority: int = 0) -> Action:
123
133
  description = description or func.__doc__ or ''
@@ -136,8 +146,6 @@ def action(*args, **kwargs):
136
146
  ContextStackVars.pop()
137
147
  current_callstack.pop()
138
148
  return ret
139
- setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
140
- setattr(_wrapper, ATTR_ACTION_MARK, True)
141
149
  action.func = _wrapper
142
150
  return _wrapper
143
151
  else:
@@ -146,6 +154,8 @@ def action(*args, **kwargs):
146
154
  pass_through = kwargs.get('pass_through', False)
147
155
  priority = kwargs.get('priority', 0)
148
156
  screenshot_mode = kwargs.get('screenshot_mode', None)
157
+ if screenshot_mode == 'manual-inherit':
158
+ warnings.warn('`screenshot_mode=manual-inherit` is deprecated. Use `screenshot_mode=manual` instead.')
149
159
  def _action_decorator(func: Callable):
150
160
  nonlocal pass_through
151
161
  action = _register(_placeholder, name, description)
@@ -160,8 +170,6 @@ def action(*args, **kwargs):
160
170
  ContextStackVars.pop()
161
171
  current_callstack.pop()
162
172
  return ret
163
- setattr(_wrapper, ATTR_ORIGINAL_FUNC, func)
164
- setattr(_wrapper, ATTR_ACTION_MARK, True)
165
173
  action.func = _wrapper
166
174
  return _wrapper
167
175
  return _action_decorator
kotonebot/backend/core.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  from functools import cache
3
3
  from typing import Callable
4
+ from typing_extensions import deprecated
4
5
 
5
6
  import cv2
6
7
  from cv2.typing import MatLike
@@ -9,6 +10,7 @@ from kotonebot.util import cv2_imread
9
10
  from kotonebot.primitives import RectTuple, Rect, Point
10
11
  from kotonebot.errors import ResourceFileMissingError
11
12
 
13
+ @deprecated('unused')
12
14
  class Ocr:
13
15
  def __init__(
14
16
  self,
@@ -19,7 +21,8 @@ class Ocr:
19
21
  self.text = text
20
22
  self.language = language
21
23
 
22
-
24
+ # TODO: 这个类和 kotonebot.primitives.Image 重复了
25
+ @deprecated('Use kotonebot.primitives.Image instead.')
23
26
  class Image:
24
27
  def __init__(
25
28
  self,
@@ -65,7 +68,7 @@ class Image:
65
68
  else:
66
69
  return f'<Image: "{self.name}" at {self.path}>'
67
70
 
68
-
71
+ # TODO: 这里的其他类应该移动到 primitives 模块下面
69
72
  class HintBox(Rect):
70
73
  def __init__(
71
74
  self,
kotonebot/backend/loop.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import time
2
2
  from functools import lru_cache, partial
3
3
  from typing import Callable, Any, overload, Literal, Generic, TypeVar, cast, get_args, get_origin
4
+ from typing_extensions import deprecated
4
5
 
5
6
  from cv2.typing import MatLike
6
7
 
@@ -9,8 +10,9 @@ from kotonebot import device, image, ocr
9
10
  from kotonebot.backend.core import Image
10
11
  from kotonebot.backend.ocr import TextComparator
11
12
  from kotonebot.client.protocol import ClickableObjectProtocol
13
+ from .context import vars
12
14
 
13
-
15
+ @deprecated('No longer used.')
14
16
  class LoopAction:
15
17
  def __init__(self, loop: 'Loop', func: Callable[[], ClickableObjectProtocol | None]):
16
18
  self.loop = loop
@@ -81,15 +83,16 @@ class Loop:
81
83
  是否在每次循环开始时(Loop.tick() 被调用时)截图。
82
84
  """
83
85
  self.__last_loop: float = -1
84
- self.__interval = Interval(interval)
86
+ self.interval = interval
87
+ """每次循环后等待的时间。"""
85
88
  self.screenshot: MatLike | None = None
86
89
  """上次截图时的图像数据。"""
87
90
  self.__skip_first_wait = skip_first_wait
88
91
  self.__is_first_tick = True
89
92
 
90
93
  def __iter__(self):
91
- self.__interval.reset()
92
94
  self.__is_first_tick = True
95
+ vars.flow.check()
93
96
  return self
94
97
 
95
98
  def __next__(self):
@@ -101,7 +104,7 @@ class Loop:
101
104
 
102
105
  def tick(self):
103
106
  if not (self.__is_first_tick and self.__skip_first_wait):
104
- self.__interval.wait()
107
+ time.sleep(self.interval)
105
108
  self.__is_first_tick = False
106
109
 
107
110
  if self.auto_screenshot:
@@ -117,13 +120,16 @@ class Loop:
117
120
  self.running = False
118
121
 
119
122
  @overload
123
+ @deprecated('Use plain if statement instead.')
120
124
  def when(self, condition: Image) -> LoopAction:
121
125
  ...
122
126
 
123
127
  @overload
128
+ @deprecated('Use plain if statement instead.')
124
129
  def when(self, condition: TextComparator) -> LoopAction:
125
130
  ...
126
131
 
132
+ @deprecated('Use plain if statement instead.')
127
133
  def when(self, condition: Any):
128
134
  """
129
135
  判断某个条件是否成立。
@@ -142,6 +148,7 @@ class Loop:
142
148
  la.do()
143
149
  return la
144
150
 
151
+ @deprecated('Use plain if statement instead.')
145
152
  def until(self, condition: Any):
146
153
  """
147
154
  当满足指定条件时,结束循环。
@@ -150,6 +157,7 @@ class Loop:
150
157
  """
151
158
  return self.when(condition).call(lambda _: self.exit())
152
159
 
160
+ @deprecated('Use image.find() and device.click() instead.')
153
161
  def click_if(self, condition: Any, *, at: tuple[int, int] | None = None):
154
162
  """
155
163
  检测指定对象是否出现,若出现,点击该对象或指定位置。
@@ -198,87 +206,3 @@ class StatedLoop(Loop, Generic[StateType]):
198
206
  self.states = cast(tuple[StateType, ...], state_values)
199
207
  self.state = self.__tmp_initial_state or self.states[0]
200
208
  return state_values
201
-
202
-
203
- def StatedLoop2(states: StateType) -> StatedLoop[StateType]:
204
- state_values = get_args(states)
205
- return cast(StatedLoop[StateType], Loop())
206
-
207
- if __name__ == '__main__':
208
- from kotonebot.kaa.tasks import R
209
- from kotonebot.backend.ocr import contains
210
- from kotonebot.backend.context import manual_context, init_context
211
-
212
- # T = TypeVar('T')
213
- # class Foo(Generic[T]):
214
- # def get_literal_params(self) -> list | None:
215
- # """
216
- # 尝试获取泛型参数 T (如果它是 Literal 类型) 的参数列表。
217
- # """
218
- # # self.__orig_class__ 会是 Foo 的具体参数化类型,
219
- # # 例如 Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]
220
- # if not hasattr(self, '__orig_class__'):
221
- # # 如果 Foo 不是以参数化泛型的方式实例化的,可能没有 __orig_class__
222
- # return None
223
- #
224
- # # generic_type_args 是传递给 Foo 的类型参数元组
225
- # # 例如 (Literal['p0', 'p1', 'p2', 'p3', 'ap'],)
226
- # generic_type_args = get_args(self.__orig_class__)
227
- #
228
- # if not generic_type_args:
229
- # # Foo 没有类型参数
230
- # return None
231
- #
232
- # # T_type 是 Foo 的第一个类型参数
233
- # # 例如 Literal['p0', 'p1', 'p2', 'p3', 'ap']
234
- # t_type = generic_type_args[0]
235
- #
236
- # # 检查 T_type 是否是 Literal 类型
237
- # if get_origin(t_type) is Literal:
238
- # # literal_args 是 Literal 类型的参数元组
239
- # # 例如 ('p0', 'p1', 'p2', 'p3', 'ap')
240
- # literal_args = get_args(t_type)
241
- # return list(literal_args)
242
- # else:
243
- # # T 不是 Literal 类型
244
- # return None
245
- # f = Foo[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
246
- # values = f.get_literal_params()
247
- # 1
248
-
249
- from typing_extensions import reveal_type
250
- slp = StatedLoop[Literal['p0', 'p1', 'p2', 'p3', 'ap']]()
251
- for l in slp:
252
- reveal_type(l.states)
253
-
254
- # init_context()
255
- # manual_context().begin()
256
- # for l in Loop():
257
- # l.when(R.Produce.ButtonUse).click()
258
- # l.when(R.Produce.ButtonRefillAP).click()
259
- # l.when(contains("123")).click()
260
- # l.click_if(contains("!23"), at=(1, 2))
261
-
262
- # State = Literal['p0', 'p1', 'p2', 'p3', 'ap']
263
- # for sl in StatedLoop[State]():
264
- # match sl.state:
265
- # case 'p0':
266
- # sl.click_if(R.Produce.ButtonProduce)
267
- # sl.click_if(contains('master'))
268
- # sl.when(R.Produce.ButtonPIdolOverview).goto('p1')
269
- # # AP 不足
270
- # sl.when(R.Produce.TextAPInsufficient).goto('ap')
271
- # case 'ap':
272
- # pass
273
- # # p1: 选择偶像
274
- # case 'p1':
275
- # sl.call(lambda _: select_idol(idol_skin_id), once=True)
276
- # sl.when(R.Produce.TextAnotherIdolAvailableDialog).call(dialog.no)
277
- # sl.click_if(R.Common.ButtonNextNoIcon)
278
- # sl.until(R.Produce.TextStepIndicator2).goto('p2')
279
- # case 'p2':
280
- # sl.when(contains("123")).click()
281
- # case 'p3':
282
- # sl.click_if(contains("!23"), at=(1, 2))
283
- # case _:
284
- # assert_never(sl.state)
@@ -239,7 +239,8 @@ class Device:
239
239
  调用前确保 `orientation` 属性与设备方向一致,
240
240
  否则点击位置会不正确。
241
241
  """
242
- x, y = self.screen_size[0] // 2, self.screen_size[1] // 2
242
+ size = self.target_resolution or self.screen_size
243
+ x, y = size[0] // 2, size[1] // 2
243
244
  self.click(x, y)
244
245
 
245
246
  @overload
@@ -28,11 +28,15 @@ def connect_adb(
28
28
  if disconnect:
29
29
  logger.debug('adb disconnect %s:%d', ip, port)
30
30
  adb.disconnect(f'{ip}:{port}')
31
+ else:
32
+ logger.debug('Skip adb disconnect.')
31
33
  if connect:
32
34
  logger.debug('adb connect %s:%d', ip, port)
33
35
  result = adb.connect(f'{ip}:{port}')
34
36
  if 'cannot connect to' in result:
35
37
  raise ValueError(result)
38
+ else:
39
+ logger.debug('Skip adb connect.')
36
40
  serial = device_serial or f'{ip}:{port}'
37
41
  logger.debug('adb wait for %s', serial)
38
42
  adb.wait_for(serial, timeout=timeout)
@@ -56,15 +60,20 @@ class CommonAdbCreateDeviceMixin(ABC):
56
60
  self.adb_port: int
57
61
  self.adb_name: str
58
62
 
59
- def create_device(self, recipe: AdbRecipes, config: AdbHostConfig) -> Device:
63
+ def create_device(self, recipe: AdbRecipes, config: AdbHostConfig, *, connect: bool = True, disconnect: bool = True) -> Device:
60
64
  """
61
65
  创建 ADB 设备。
66
+
67
+ :param recipe: 连接方式配方名称。
68
+ :param config: ADB 配置。
69
+ :param connect: 创建设备实例前,是否连接 ADB(执行 adb connect)。
70
+ :param disconnect: 创建设备实例前,是否先断开已有 ADB 连接(执行 adb disconnect)。
62
71
  """
63
72
  connection = connect_adb(
64
73
  self.adb_ip,
65
74
  self.adb_port,
66
- connect=True,
67
- disconnect=True,
75
+ connect=connect,
76
+ disconnect=disconnect,
68
77
  timeout=config.timeout,
69
78
  device_serial=self.adb_name
70
79
  )
@@ -77,7 +77,7 @@ class LeidianInstance(CommonAdbCreateDeviceMixin, Instance[AdbHostConfig]):
77
77
  if self.adb_port is None:
78
78
  raise ValueError("ADB port is not set and is required.")
79
79
 
80
- return super().create_device(impl, host_config)
80
+ return super().create_device(impl, host_config, connect=False, disconnect=False)
81
81
 
82
82
  class LeidianHost(HostProtocol[LeidianRecipes]):
83
83
  @staticmethod
@@ -4,6 +4,7 @@ from typing_extensions import assert_never
4
4
 
5
5
  from kotonebot import logging
6
6
  from kotonebot.client.device import WindowsDevice
7
+ from kotonebot.util import require_windows
7
8
  from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
8
9
 
9
10
  logger = logging.getLogger(__name__)
@@ -19,11 +20,13 @@ class CommonWindowsCreateDeviceMixin(ABC):
19
20
  """
20
21
  def __init__(self, *args, **kwargs) -> None:
21
22
  super().__init__(*args, **kwargs)
23
+ require_windows('CommonWindowsCreateDeviceMixin', self.__class__)
22
24
 
23
25
  def create_device(self, recipe: WindowsRecipes, config: WindowsHostConfigs) -> Device:
24
26
  """
25
27
  创建 Windows 设备。
26
28
  """
29
+ require_windows('CommonWindowsCreateDeviceMixin.create_device', self.__class__)
27
30
  match recipe:
28
31
  case 'windows':
29
32
  if not isinstance(config, WindowsHostConfig):
@@ -1,7 +1,15 @@
1
- # 导入所有内置实现,以触发它们的 @register_impl 装饰器
1
+ from kotonebot.util import is_windows, require_windows
2
+
3
+ # 基础实现
2
4
  from . import adb # noqa: F401
3
5
  from . import adb_raw # noqa: F401
4
- from . import remote_windows # noqa: F401
5
6
  from . import uiautomator2 # noqa: F401
6
- from . import windows # noqa: F401
7
- from . import nemu_ipc # noqa: F401
7
+
8
+ # Windows 实现(默认仅在 Windows 上导入)
9
+ if is_windows():
10
+ try:
11
+ from . import nemu_ipc # noqa: F401
12
+ from . import windows # noqa: F401
13
+ from . import remote_windows # noqa: F401
14
+ except ImportError:
15
+ require_windows('"windows" and "remote_windows" implementations')
@@ -1,10 +1,6 @@
1
- """
2
- Remote Windows implementation using XML-RPC.
3
-
4
- This module provides:
5
- 1. RemoteWindowsImpl - Client implementation that connects to a remote Windows machine
6
- 2. RemoteWindowsServer - Server implementation that exposes a WindowsImpl instance via XML-RPC
7
- """
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('"RemoteWindowsImpl" implementation')
8
4
 
9
5
  import io
10
6
  import base64
@@ -1,3 +1,7 @@
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('"WindowsImpl" implementation')
4
+
1
5
  from ctypes import windll
2
6
  from typing import Literal
3
7
  from importlib import resources
@@ -0,0 +1,3 @@
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('kotonebot.interop.win module')
@@ -1,3 +1,47 @@
1
+ """
2
+ Windows Task Dialog interop module.
3
+
4
+ This module provides Windows TaskDialog functionality and is only available on Windows systems.
5
+ """
6
+
7
+ import platform
8
+ import warnings
9
+
10
+ from kotonebot.util import is_windows
11
+
12
+ # 检查是否在 Windows 平台上
13
+ if not is_windows():
14
+ _WINDOWS_ONLY_MSG = (
15
+ f"TaskDialog is only available on Windows systems. "
16
+ f"Current system: non-Windows\n"
17
+ "To use Windows TaskDialog features, please run this code on a Windows system."
18
+ )
19
+
20
+
21
+ # 提供虚拟类以避免导入错误
22
+ class TaskDialog:
23
+ def __init__(self, *args, **kwargs):
24
+ raise ImportError(_WINDOWS_ONLY_MSG)
25
+
26
+ # 导出所有常量作为 None
27
+ __all__ = [
28
+ "TaskDialog",
29
+ "TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
30
+ "TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
31
+ "IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
32
+ "TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
33
+ ]
34
+
35
+ # 设置所有常量为 None 或保留为模块级变量
36
+ TDCBF_OK_BUTTON = TDCBF_YES_BUTTON = TDCBF_NO_BUTTON = TDCBF_CANCEL_BUTTON = None
37
+ TDCBF_RETRY_BUTTON = TDCBF_CLOSE_BUTTON = None
38
+ IDOK = IDCANCEL = IDABORT = IDRETRY = IDIGNORE = IDYES = IDNO = IDCLOSE = None
39
+ TD_WARNING_ICON = TD_ERROR_ICON = TD_INFORMATION_ICON = TD_SHIELD_ICON = None
40
+
41
+ # 阻止模块加载
42
+ raise ImportError(_WINDOWS_ONLY_MSG)
43
+
44
+ # 如果是 Windows,继续正常加载
1
45
  import ctypes
2
46
  from ctypes import wintypes
3
47
  import time
kotonebot/ui/user.py CHANGED
@@ -4,7 +4,12 @@ import time
4
4
 
5
5
  import cv2
6
6
  from cv2.typing import MatLike
7
- from win11toast import toast
7
+ from kotonebot.util import is_windows
8
+ if is_windows():
9
+ from win11toast import toast
10
+ else:
11
+ def toast(title: str, message: str | None = None, buttons: list[str] | None = None):
12
+ raise ImportError('toast notification is only available on Windows')
8
13
 
9
14
  from .pushkit import Wxpusher
10
15
  from .. import logging
kotonebot/util.py CHANGED
@@ -4,6 +4,7 @@ import pstats
4
4
  import typing
5
5
  import logging
6
6
  import cProfile
7
+ import platform
7
8
  from importlib import resources
8
9
  from functools import lru_cache
9
10
  from typing import Literal, Callable, TYPE_CHECKING, TypeGuard
@@ -17,6 +18,32 @@ if TYPE_CHECKING:
17
18
  from kotonebot.client.protocol import Device
18
19
 
19
20
  logger = logging.getLogger(__name__)
21
+ _WINDOWS_ONLY_MSG = (
22
+ "This feature is only available on Windows. "
23
+ f"You are using {platform.system()}.\n"
24
+ "The requested feature is: {feature_name}\n"
25
+ )
26
+
27
+ def is_windows() -> bool:
28
+ """检查当前是否为 Windows 系统"""
29
+ return platform.system() == 'Windows'
30
+
31
+ def is_linux() -> bool:
32
+ """检查当前是否为 Linux 系统"""
33
+ return platform.system() == 'Linux'
34
+
35
+ def is_macos() -> bool:
36
+ """检查当前是否为 macOS 系统"""
37
+ return platform.system() == 'Darwin'
38
+
39
+ def require_windows(feature_name: str | None = None, class_: type | None = None) -> None:
40
+ """要求必须在 Windows 系统上运行,否则抛出 ImportError"""
41
+ if not is_windows():
42
+ feature_name = feature_name or 'not specified'
43
+ if class_:
44
+ full_name = '.'.join([class_.__module__, class_.__name__])
45
+ feature_name += f' ({full_name})'
46
+ raise ImportError(_WINDOWS_ONLY_MSG.format(feature_name=feature_name))
20
47
 
21
48
 
22
49
 
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kotonebot
3
- Version: 0.3.1
4
- Summary: Kotonebot is game/app automation library based on computer vision technology, works for Windows and Android.
3
+ Version: 0.4.0
4
+ Summary: Kotonebot is game automation library based on computer vision technology, works for Windows and Android.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: opencv-python~=4.10
9
9
  Requires-Dist: rapidocr_onnxruntime~=1.4
10
- Requires-Dist: av~=14.0
11
10
  Requires-Dist: scikit-image~=0.25
12
11
  Requires-Dist: thefuzz~=0.22
13
12
  Requires-Dist: pydantic~=2.10
@@ -17,24 +16,26 @@ Requires-Dist: python-dotenv~=1.0
17
16
  Requires-Dist: onnxruntime~=1.14
18
17
  Requires-Dist: numpy
19
18
  Provides-Extra: android
20
- Requires-Dist: adbutils~=2.8; extra == "android"
21
- Requires-Dist: uiautomator2~=3.2; extra == "android"
19
+ Requires-Dist: adbutils>=2.8; extra == "android"
20
+ Requires-Dist: uiautomator2>=3.2; extra == "android"
22
21
  Provides-Extra: windows
23
22
  Requires-Dist: pywin32; extra == "windows"
24
- Requires-Dist: ahk~=1.8; extra == "windows"
25
- Requires-Dist: win11toast~=0.35; extra == "windows"
26
- Requires-Dist: psutil~=6.1; extra == "windows"
23
+ Requires-Dist: ahk>=1.8; extra == "windows"
24
+ Requires-Dist: win11toast>=0.35; extra == "windows"
25
+ Requires-Dist: psutil>=6.1; extra == "windows"
27
26
  Provides-Extra: dev
28
27
  Requires-Dist: fastapi~=0.115; extra == "dev"
29
28
  Requires-Dist: uvicorn~=0.34; extra == "dev"
30
29
  Requires-Dist: python-multipart~=0.0; extra == "dev"
31
30
  Requires-Dist: websockets~=14.1; extra == "dev"
32
31
  Requires-Dist: psutil~=6.1; extra == "dev"
33
- Requires-Dist: gradio~=5.21; extra == "dev"
34
- Requires-Dist: snakeviz; extra == "dev"
32
+ Requires-Dist: twine~=6.1; extra == "dev"
35
33
  Requires-Dist: build; extra == "dev"
34
+ Requires-Dist: snakeviz; extra == "dev"
35
+ Requires-Dist: jinja2~=3.1; extra == "dev"
36
+ Requires-Dist: tqdm~=4.67; extra == "dev"
36
37
  Provides-Extra: all
37
- Requires-Dist: kotonebot[android,windows]; extra == "all"
38
+ Requires-Dist: kotonebot[android,dev,windows]; extra == "all"
38
39
  Dynamic: license-file
39
40
 
40
41
  # kotonebot
@@ -67,6 +68,10 @@ pip install kotonebot[dev]
67
68
  ## 快速开始
68
69
  WIP
69
70
 
71
+ ### 协同开发
72
+ 有时候你可能想以源码方式安装 kotonebot,以便与自己的项目一起调试修改。此时,如果你以 `pip install -e /path/to/kotonebot` 的方式安装,Pylance 可能无法正常静态分析。
73
+ 解决方案是在 VSCode 里搜索 `python.analysis.extraPaths` 并将其设置为你本地 kotonebot 的根目录。
74
+
70
75
  ## 文档
71
76
  WIP
72
77
 
@@ -1,53 +1,53 @@
1
1
  kotonebot/__init__.py,sha256=DWS8zZAWH-MQZxk6bDm9uNAfY9UVmtd7Q928suujpiQ,636
2
2
  kotonebot/errors.py,sha256=1mZLFhNNxtRkm66y80rPzOeFjCgR0TgxgCDGo9ByZek,2294
3
- kotonebot/util.py,sha256=MyJYVNrKcZy7I9bK0rhW15ivPLoiA3B7_hM9svWIfNc,13360
3
+ kotonebot/util.py,sha256=ftWhlnZ3qJ80x2wVCHYr2wW299JQkYDOREP1mA-n1vg,14370
4
4
  kotonebot/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- kotonebot/backend/bot.py,sha256=q7nVPru_A8kg8_o_MMpMfECchKqU8_AFpGIQk8-q5pY,11585
5
+ kotonebot/backend/bot.py,sha256=Ph0pfAubr_TV4MmqkyYL0P0hLbXdFEkzlQ0V2B1niZI,11870
6
6
  kotonebot/backend/color.py,sha256=KqFISc6puMNbsyB5diu5PcqNjdkLwUeryVsMmeAJG4Q,20125
7
- kotonebot/backend/core.py,sha256=pn1XORbU6_ddfgLJQpLSmiGL2MjnF5iCk5GBdrq3GYE,3783
7
+ kotonebot/backend/core.py,sha256=A0f2i1KO9PXn4ndRDOBbADtB_beVxD1P0g4FYioxUY0,4026
8
8
  kotonebot/backend/dispatch.py,sha256=t3eqkOVNNrdaeXSMLxDbReYhVRoAlSRNxb7oO2clRsQ,7423
9
9
  kotonebot/backend/flow_controller.py,sha256=sbr6JiuYXErevY_BPrzw7hUCfxLGVrz0W_KNbXIxe9Q,6228
10
10
  kotonebot/backend/image.py,sha256=jXYGVXa9K1zBCJBG1Btsf2AG5XI9A0DMy5uYy6KOusk,28111
11
- kotonebot/backend/loop.py,sha256=HlgMON1LcBPZP710w5uHeziMRSF5Dhwv3UKUdjCkouk,9594
11
+ kotonebot/backend/loop.py,sha256=e4STYQ6sbUp53xcYJXyhz4vOyEzXds1MKR0xebqU228,6638
12
12
  kotonebot/backend/ocr.py,sha256=IE_LUCdD4i00a02xgLkSRPcYYo3R2MBfZKKQHjOFfgM,18058
13
13
  kotonebot/backend/preprocessor.py,sha256=YmAbLa-XXES2AchMJtsBpPZwIIGHuYapwpXpw0YSpbA,3423
14
14
  kotonebot/backend/context/__init__.py,sha256=0X9jzM0ftGQUgjoXCk98xf1inoCqHqaUwT0wcHn9P5s,168
15
- kotonebot/backend/context/context.py,sha256=dvGD_kKaThYdwIuC21Gd2P0uYwyh3yRgMYqkZ5_Ap3w,34472
16
- kotonebot/backend/context/task_action.py,sha256=7fKy6ZOtOaDW87hketiub72_eND9yorJqBP3Z5b8fX8,6259
15
+ kotonebot/backend/context/context.py,sha256=R91knq7Ry5MSWaFhOj3HIF13u8bomOOsGk59yPOG4aw,33780
16
+ kotonebot/backend/context/task_action.py,sha256=LdxyeW-1ie71ludnAN36ltlcAw4AX3Lb9ua7I5xK4yY,6444
17
17
  kotonebot/backend/debug/__init__.py,sha256=pcSpwzU2YwGrogOoHmsI035nkA_-kDLfm-lfBxHuQ-c,43
18
18
  kotonebot/backend/debug/entry.py,sha256=_sIGi9_LegbaM2DCcDTvGJhrkyUqF6W2Al4nYmkb9rM,2833
19
19
  kotonebot/backend/debug/mock.py,sha256=0fTiJeqVTanQv6L3TPbldgbJRBPmunZOjlzKtVicJ_M,2118
20
20
  kotonebot/backend/debug/server.py,sha256=9PEpczIrwCnD4c_FAtiojwk27aKkkzTtRySCsdho4Rs,7517
21
21
  kotonebot/backend/debug/vars.py,sha256=3wtbkH2WFNXyT2ZNu-8en3S0pAb40h-SefYyScRMLnM,10912
22
22
  kotonebot/client/__init__.py,sha256=1eXyGopBFpYoucNTNkTo-7nIeDyasyETfHJ8DWuaNTo,192
23
- kotonebot/client/device.py,sha256=5BVa1dXVI_jH5gLCL61tJ_MXG6BVSt_mETqyUVlutWo,19370
23
+ kotonebot/client/device.py,sha256=bb49Gbo-zcF1SaeBbRljxkuKGMp5e9umPiguAbV1kok,19405
24
24
  kotonebot/client/fast_screenshot.py,sha256=q69AX15VXRuB0U2qFJKfoTOBgG4nVBCUcaN1CX0VsUc,13647
25
25
  kotonebot/client/protocol.py,sha256=x05llULFI3MddbvBX_c0sYWlSzCyWa3hped24ktq2Ko,2300
26
26
  kotonebot/client/registration.py,sha256=XK424QEWbJKfNdkiDoIJUh_JIy8ryk4I4I3hrWUXCX8,848
27
27
  kotonebot/client/host/__init__.py,sha256=WsDNAugbr4k1fMCHBdaYZmrifrq6WmSQByp1qOFbV_I,579
28
- kotonebot/client/host/adb_common.py,sha256=b9t5bbiIYGARpqB8qayptAzznQaWtzWt533yqoAAyvQ,3280
28
+ kotonebot/client/host/adb_common.py,sha256=_S2LKpsNnRUREUpzO71OfMIB8Dt0pmxKdSbcmpqss-4,3750
29
29
  kotonebot/client/host/custom.py,sha256=_r2GAtJ20CoEx8JO7wVa4jr6KT8YgO5wzrFHLFtOgZE,4079
30
- kotonebot/client/host/leidian_host.py,sha256=EoogcI1XN1akE1zKLaZCNMvs7uIRrraZsg6ib5cRwAU,7696
30
+ kotonebot/client/host/leidian_host.py,sha256=H1yNhKRBdjzyY8_UjqohuJwjj-gVvN1s3dnQfKgFdW8,7729
31
31
  kotonebot/client/host/mumu12_host.py,sha256=cWlbZiV0HdN2S42L1Nd5Jcv3bw-b1oxjX4_3y5JE-tI,14444
32
32
  kotonebot/client/host/protocol.py,sha256=x2TbnDELDQpqxSKWUHLT3Pez8Qx6IzL4wyUfgf9mFyk,8051
33
- kotonebot/client/host/windows_common.py,sha256=sz2uxfYa4by4uNM7_7qHS1kfvyu9iYxd5DgJ8ThQAKA,2223
34
- kotonebot/client/implements/__init__.py,sha256=w9Yz_nVEnby_7eCS2FT1oYp1IuO3yqSdvhZEu6iJcvk,303
33
+ kotonebot/client/host/windows_common.py,sha256=_Hf4CvHKNgiMIVY3ZPr3wF71r6nMErNi84Y_fswUjBc,2431
34
+ kotonebot/client/implements/__init__.py,sha256=_qZNM-2wEr4Q-_2kWisyuc4AUoqpRB_gAC_hr8_RkSY,511
35
35
  kotonebot/client/implements/adb.py,sha256=OcATiRGj84KMM9mEuQLzLoNyDBaZRJugR-BFrlWjB-c,3145
36
36
  kotonebot/client/implements/adb_raw.py,sha256=zUTI9QscmdN031tbBbInUN8G-LrgQv-cTc_Yl0zaZQU,6307
37
- kotonebot/client/implements/remote_windows.py,sha256=tc9R0kcKAwLwWt4Qu8Gt8wLZJCS94MnFzTvKuCbM9Wo,6902
37
+ kotonebot/client/implements/remote_windows.py,sha256=SRq97cuXdUVvFIhsazZt4djMeGSLfYS88IUyzmlpaww,6756
38
38
  kotonebot/client/implements/uiautomator2.py,sha256=ER4cNLI_cCpIGKWIXeuaUPmVtz50JuFO5Kx-ZwCGI1s,2575
39
- kotonebot/client/implements/windows.py,sha256=-xS2qN5RdHB_X4R1a8Oi5tM2Wh5YI774wNAJ21lYirQ,6623
39
+ kotonebot/client/implements/windows.py,sha256=-P5nFiUbCxz9qMM-WZPtw-Q2xYc3Tmym5MNK3gBwoB4,6738
40
40
  kotonebot/client/implements/nemu_ipc/__init__.py,sha256=vSZzv75bn38Wch86PYs5UDOCLwxxoDGm2v1jrwff_S8,200
41
41
  kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py,sha256=YsfKf0-qorfAf2YvNuxpLb9af-HJFsu97bnXABshhbA,10643
42
42
  kotonebot/client/implements/nemu_ipc/nemu_ipc.py,sha256=LhUUyfB28MDnRg8z2FyGah1hTeOMFiX7w8LZLAAjLF8,12082
43
43
  kotonebot/config/__init__.py,sha256=-jATUOdrpUrBRT9RiTRQho2-2zeet50qQggsVMVpqNE,35
44
44
  kotonebot/config/base_config.py,sha256=NpJuUmzgjUHelzm55bbVvk9x9MdwuKwfxwpaJFp-9iw,3438
45
45
  kotonebot/config/manager.py,sha256=XBtriAU9eo-wv2iKOwyDqu8tzhbKqFCuy0jsAM9T9uU,1061
46
- kotonebot/interop/win/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
+ kotonebot/interop/win/__init__.py,sha256=jH8E0iqT1lOs4C_i5yl4bJ7z7iZh2ieGOF1lu8raJ30,111
47
47
  kotonebot/interop/win/message_box.py,sha256=R06GSu936Bx_Wg7ddn6LOvazD9_Gt3mhz4_oUuIoYO0,8635
48
48
  kotonebot/interop/win/reg.py,sha256=xw35d1xl8ucITT4bOMFgHmMkAUhak7x3lzegR3g3S48,1347
49
49
  kotonebot/interop/win/shortcut.py,sha256=f1u6IWvpw6Kxt014wnHz5Z94rVK1qf4kLtRsI9bJYnk,1764
50
- kotonebot/interop/win/task_dialog.py,sha256=pJMEf94pXzHmVpjhfuCeSZ0ztJ8DARsSHTcDOXa1dLQ,18608
50
+ kotonebot/interop/win/task_dialog.py,sha256=Ezi1CsjFSbnKccvYfIRaJoCGHUsJnnIpHb0gtKR603E,20191
51
51
  kotonebot/logging/__init__.py,sha256=r0q4z59yYy_bQnHTwJYsiPGwOGIgEOrcXH1mNs1h_N0,142
52
52
  kotonebot/logging/log.py,sha256=PLb6r_hlW1mvqU_kx6_89zaQ1IpamgHWG3sQ0ZnlrCI,555
53
53
  kotonebot/primitives/__init__.py,sha256=R11AoTQQU6ql86oaVjZJkH2-kkxf5kr9xY8gnqPjLgM,355
@@ -56,15 +56,15 @@ kotonebot/primitives/visual.py,sha256=fZbNgaM03wCaR1Rb0QgeAQIfNsBBXPoUp2-E2IYFsL
56
56
  kotonebot/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  kotonebot/tools/mirror.py,sha256=jlGpeX_WkFpcZxojxDzT5QqVeIpd_UIDzSMR2K31VJ0,13820
58
58
  kotonebot/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- kotonebot/ui/user.py,sha256=kiEWKpR6XAFh7LUu_HK3NPCioLeA2tEJZRY8iKza6ac,4249
59
+ kotonebot/ui/user.py,sha256=aP6Va3iHaVA0-AHViNa04a3HDpLLiovZjV5_AMGY4cI,4485
60
60
  kotonebot/ui/file_host/sensio.py,sha256=QyH8DpyO3sf2Hz4NuOW9oDt1V3G06XMO7yKfgLwq1oE,914
61
61
  kotonebot/ui/file_host/tmp_send.py,sha256=SwzTUGIjZXaP1_H86qahuLwUd8VeHGWW86XKaC__WKI,1802
62
62
  kotonebot/ui/pushkit/__init__.py,sha256=xDUctRUL3euvge-yl8IhFYMlxIxQXsjxcyGN5tUwPtE,73
63
63
  kotonebot/ui/pushkit/image_host.py,sha256=nB6BCOA5ZgSGi-ntgqQp49H1UZDk8qC41O_PTLPzZ-E,2581
64
64
  kotonebot/ui/pushkit/protocol.py,sha256=KVZ-xr0sMdiuri7AiYqugpZRRtefBsosXm6zouScUR4,266
65
65
  kotonebot/ui/pushkit/wxpusher.py,sha256=U7WKxyf9pVgGvppmBxwMRuBuFkQG3NC3tkdRh7_-IOw,1732
66
- kotonebot-0.3.1.dist-info/licenses/LICENSE,sha256=gcuuhKKc5-dwvyvHsXjlC9oM6N5gZ6umYbC8ewW1Yvg,35821
67
- kotonebot-0.3.1.dist-info/METADATA,sha256=x2hYu3ZdsflywLK_3BJ2PwbNTw7R9PaYuLg8xTOChqQ,2788
68
- kotonebot-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
- kotonebot-0.3.1.dist-info/top_level.txt,sha256=QUWAZdbBndoojkrs6RcNytLAn7a0ns4YNF4tLx2Nc4s,10
70
- kotonebot-0.3.1.dist-info/RECORD,,
66
+ kotonebot-0.4.0.dist-info/licenses/LICENSE,sha256=gcuuhKKc5-dwvyvHsXjlC9oM6N5gZ6umYbC8ewW1Yvg,35821
67
+ kotonebot-0.4.0.dist-info/METADATA,sha256=4P-5GBc-v3VEtDlQqx0F3pUJXC7D9p2b0cmPbVZ91NE,3207
68
+ kotonebot-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
69
+ kotonebot-0.4.0.dist-info/top_level.txt,sha256=QUWAZdbBndoojkrs6RcNytLAn7a0ns4YNF4tLx2Nc4s,10
70
+ kotonebot-0.4.0.dist-info/RECORD,,