diff --git a/kotonebot/backend/flow_controller.py b/kotonebot/backend/flow_controller.py index c674a65..c30983e 100644 --- a/kotonebot/backend/flow_controller.py +++ b/kotonebot/backend/flow_controller.py @@ -95,7 +95,7 @@ class FlowController: logger.info('Interrupt requested.') self.interrupt_event.set() - def request_pause(self) -> None: + def request_pause(self, *, wait_resume: bool = False) -> None: """ 请求暂停任务。 @@ -106,6 +106,8 @@ class FlowController: if not self.paused: logger.info('Pause requested.') self.paused = True + if wait_resume: + self.check() def request_resume(self) -> None: """ diff --git a/kotonebot/kaa/config/schema.py b/kotonebot/kaa/config/schema.py index ae0b6e0..db0e1ef 100644 --- a/kotonebot/kaa/config/schema.py +++ b/kotonebot/kaa/config/schema.py @@ -64,6 +64,8 @@ class ContestConfig(ConfigBaseModel): select_which_contestant: Literal[1, 2, 3] = 1 """选择第几个挑战者""" + when_no_set: Literal['remind', 'wait', 'auto_set', 'auto_set_silent'] = 'remind' + """竞赛队伍未编成时应该:remind=通知我并跳过竞赛,wait=提醒我并等待手动编成,auto_set=使用自动编成并提醒,auto_set_silent=使用自动编成不提醒""" class ProduceConfig(ConfigBaseModel): diff --git a/kotonebot/kaa/main/gr.py b/kotonebot/kaa/main/gr.py index 43a371d..a4bc5fd 100644 --- a/kotonebot/kaa/main/gr.py +++ b/kotonebot/kaa/main/gr.py @@ -51,7 +51,7 @@ ConfigKey = Literal[ 'mini_live_reassign', 'mini_live_duration', 'online_live_reassign', 'online_live_duration', 'contest_enabled', - 'select_which_contestant', + 'select_which_contestant', 'when_no_set', # produce 'produce_enabled', 'selected_solution_id', 'produce_count', @@ -1250,6 +1250,20 @@ class KotoneBotUI: interactive=True, info=ContestConfig.model_fields['select_which_contestant'].description ) + + when_no_set_choices = [ + ("通知我并跳过竞赛", "remind"), + ("提醒我并等待手动编成", "wait"), + ("使用自动编成并提醒我", "auto_set"), + ("使用自动编成", "auto_set_silent") + ] + when_no_set = gr.Dropdown( + choices=when_no_set_choices, + value=self.current_config.options.contest.when_no_set, + label="竞赛队伍未编成时", + interactive=True, + info=ContestConfig.model_fields['when_no_set'].description + ) contest_enabled.change( fn=lambda x: gr.Group(visible=x), inputs=[contest_enabled], @@ -1259,10 +1273,12 @@ class KotoneBotUI: def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None: config.contest.enabled = data['contest_enabled'] config.contest.select_which_contestant = data['select_which_contestant'] - + config.contest.when_no_set = data['when_no_set'] + return set_config, { 'contest_enabled': contest_enabled, - 'select_which_contestant': select_which_contestant + 'select_which_contestant': select_which_contestant, + 'when_no_set': when_no_set } def _create_produce_settings(self) -> ConfigBuilderReturnValue: diff --git a/kotonebot/kaa/tasks/daily/contest.py b/kotonebot/kaa/tasks/daily/contest.py index 904b98f..86bc5b6 100644 --- a/kotonebot/kaa/tasks/daily/contest.py +++ b/kotonebot/kaa/tasks/daily/contest.py @@ -2,12 +2,15 @@ import logging from gettext import gettext as _ +from kotonebot.errors import StopCurrentTask from kotonebot.kaa.tasks import R from kotonebot.kaa.config import conf -from kotonebot.kaa.game_ui import WhiteFilter +from kotonebot.kaa.game_ui import WhiteFilter, dialog from ..actions.scenes import at_home, goto_home from ..actions.loading import wait_loading_end -from kotonebot import device, image, ocr, color, action, task, user, rect_expand, sleep, contains, Interval +from kotonebot import device, image, ocr, color, action, task, rect_expand, sleep, contains, Interval +from kotonebot.backend.context.context import vars +from kotonebot.ui import user as ui_user logger = logging.getLogger(__name__) @@ -70,11 +73,39 @@ def handle_challenge() -> bool: # 记忆未编成 [screenshots/contest/no_memo.png] if image.find(R.Daily.TextContestNoMemory): - logger.debug('Memory not set. Using auto-compilation.') - user.warning('竞赛未编成', _('记忆未编成。将使用自动编成。'), once=True) - if image.find(R.Daily.ButtonContestChallenge): - device.click() - return True + logger.debug('Memory not set.') + when_no_set = conf().contest.when_no_set + + auto_compilation = False + match when_no_set: + case 'remind': + # 关闭编成提示弹窗 + dialog.expect_no(msg='Closed memory not set dialog.') + ui_user.warning('竞赛未编成', '已跳过此次竞赛任务。') + logger.info('Contest skipped due to memory not set (remind mode).') + raise StopCurrentTask + case 'wait': + dialog.expect_no(msg='Closed memory not set dialog.') + ui_user.warning('竞赛未编成', '已自动暂停,请手动编成后返回至挑战开始页,并点击网页上「恢复」按钮或使用快捷键继续执行。') + vars.flow.request_pause(wait_resume=True) + logger.info('Contest paused due to memory not set (wait mode).') + return True + case 'auto_set' | 'auto_set_silent': + if when_no_set == 'auto_set': + ui_user.warning('竞赛未编成', '将使用自动编成。', once=True) + logger.debug('Using auto-compilation with notification.') + else: # auto_set_silent + logger.debug('Using auto-compilation silently.') + auto_compilation = True + case _: + logger.warning(f'Unknown value for contest.when_no_set: {when_no_set}, fallback to auto.') + logger.debug('Using auto-compilation silently.') + auto_compilation = True + + if auto_compilation: + if image.find(R.Daily.ButtonContestChallenge): + device.click() + return True # 勾选跳过所有 # [screenshots/contest/contest2.png] diff --git a/kotonebot/ui/user.py b/kotonebot/ui/user.py index 47ee162..83bf12e 100644 --- a/kotonebot/ui/user.py +++ b/kotonebot/ui/user.py @@ -4,6 +4,7 @@ import time import cv2 from cv2.typing import MatLike +from win11toast import toast from .pushkit import Wxpusher from .. import logging @@ -25,17 +26,6 @@ def retry(func): continue return wrapper -def ask( - question: str, - options: list[str], - *, - timeout: float = -1, -) -> bool: - """ - 询问用户 - """ - raise NotImplementedError - def _save_local( title: str, message: str, @@ -74,6 +64,43 @@ def push( logger.warning('push remote message failed: %s', e) _save_local(title, message, images) +def _show_toast(title: str, message: str | None = None, buttons: list[str] | None = None): + """ + 统一的 Toast 通知函数 + + :param title: 通知标题 + :param message: 通知消息内容 + :param buttons: 按钮列表,如果提供则显示带按钮的通知 + """ + try: + if buttons: + logger.verbose('showing toast notification with buttons: %s - %s', title, message or '') + toast(title, message or '', buttons=buttons) + else: + # 如果没有 message,只显示 title + if message: + logger.verbose('showing toast notification: %s - %s', title, message) + toast(title, message) + else: + logger.verbose('showing toast notification: %s', title) + toast(title) + except Exception as e: + logger.warning('toast notification failed: %s', e) + +def ask( + question: str, + options: list[tuple[str, str]], + *, + timeout: float = -1, +) -> str: + """ + 询问用户 + """ + # 将选项转换为按钮列表 + buttons = [option[1] for option in options] + _show_toast("琴音小助手询问", question, buttons=buttons) + raise NotImplementedError + def info( title: str, message: str | None = None, @@ -83,6 +110,7 @@ def info( ): logger.info('user.info: %s', message) push('KAA:' + title, message, images=images) + _show_toast('KAA:' + title, message) def warning( title: str, @@ -98,7 +126,8 @@ def warning( :param once: 每次运行是否只显示一次。 """ logger.warning('user.warning: %s', message) - push("KAA 警告:" + title, message, images=images) + push("琴音小助手警告:" + title, message, images=images) + _show_toast("琴音小助手警告:" + title, message) def error( title: str, @@ -111,4 +140,5 @@ def error( 错误信息。 """ logger.error('user.error: %s', message) - push("KAA 错误:" + title, message, images=images) + push("琴音小助手错误:" + title, message, images=images) + _show_toast("琴音小助手错误:" + title, message) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4e8329d..e7d8174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ # TODO: move these dependencies to optional-dependencies "pywin32==310", "ahk==1.8.3", + "win11toast==0.35", # For Windows Toast Notification ] diff --git a/requirements.win.txt b/requirements.win.txt index 19adf15..41707fc 100644 --- a/requirements.win.txt +++ b/requirements.win.txt @@ -1,4 +1,5 @@ -r requirements.txt pywin32==310 -ahk==1.8.3 \ No newline at end of file +ahk==1.8.3 +win11toast==0.35 \ No newline at end of file