feat(task): 竞赛未编成时支持暂停与通知

This commit is contained in:
XcantloadX 2025-07-26 14:03:04 +08:00
parent a167cbfbe1
commit 3be8485795
7 changed files with 108 additions and 25 deletions

View File

@ -95,7 +95,7 @@ class FlowController:
logger.info('Interrupt requested.') logger.info('Interrupt requested.')
self.interrupt_event.set() 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: if not self.paused:
logger.info('Pause requested.') logger.info('Pause requested.')
self.paused = True self.paused = True
if wait_resume:
self.check()
def request_resume(self) -> None: def request_resume(self) -> None:
""" """

View File

@ -64,6 +64,8 @@ class ContestConfig(ConfigBaseModel):
select_which_contestant: Literal[1, 2, 3] = 1 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): class ProduceConfig(ConfigBaseModel):

View File

@ -51,7 +51,7 @@ ConfigKey = Literal[
'mini_live_reassign', 'mini_live_duration', 'mini_live_reassign', 'mini_live_duration',
'online_live_reassign', 'online_live_duration', 'online_live_reassign', 'online_live_duration',
'contest_enabled', 'contest_enabled',
'select_which_contestant', 'select_which_contestant', 'when_no_set',
# produce # produce
'produce_enabled', 'selected_solution_id', 'produce_count', 'produce_enabled', 'selected_solution_id', 'produce_count',
@ -1250,6 +1250,20 @@ class KotoneBotUI:
interactive=True, interactive=True,
info=ContestConfig.model_fields['select_which_contestant'].description 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( contest_enabled.change(
fn=lambda x: gr.Group(visible=x), fn=lambda x: gr.Group(visible=x),
inputs=[contest_enabled], inputs=[contest_enabled],
@ -1259,10 +1273,12 @@ class KotoneBotUI:
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None: def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
config.contest.enabled = data['contest_enabled'] config.contest.enabled = data['contest_enabled']
config.contest.select_which_contestant = data['select_which_contestant'] config.contest.select_which_contestant = data['select_which_contestant']
config.contest.when_no_set = data['when_no_set']
return set_config, { return set_config, {
'contest_enabled': contest_enabled, '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: def _create_produce_settings(self) -> ConfigBuilderReturnValue:

View File

@ -2,12 +2,15 @@
import logging import logging
from gettext import gettext as _ from gettext import gettext as _
from kotonebot.errors import StopCurrentTask
from kotonebot.kaa.tasks import R from kotonebot.kaa.tasks import R
from kotonebot.kaa.config import conf 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.scenes import at_home, goto_home
from ..actions.loading import wait_loading_end 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__) logger = logging.getLogger(__name__)
@ -70,11 +73,39 @@ def handle_challenge() -> bool:
# 记忆未编成 [screenshots/contest/no_memo.png] # 记忆未编成 [screenshots/contest/no_memo.png]
if image.find(R.Daily.TextContestNoMemory): if image.find(R.Daily.TextContestNoMemory):
logger.debug('Memory not set. Using auto-compilation.') logger.debug('Memory not set.')
user.warning('竞赛未编成', _('记忆未编成。将使用自动编成。'), once=True) when_no_set = conf().contest.when_no_set
if image.find(R.Daily.ButtonContestChallenge):
device.click() auto_compilation = False
return True 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] # [screenshots/contest/contest2.png]

View File

@ -4,6 +4,7 @@ import time
import cv2 import cv2
from cv2.typing import MatLike from cv2.typing import MatLike
from win11toast import toast
from .pushkit import Wxpusher from .pushkit import Wxpusher
from .. import logging from .. import logging
@ -25,17 +26,6 @@ def retry(func):
continue continue
return wrapper return wrapper
def ask(
question: str,
options: list[str],
*,
timeout: float = -1,
) -> bool:
"""
询问用户
"""
raise NotImplementedError
def _save_local( def _save_local(
title: str, title: str,
message: str, message: str,
@ -74,6 +64,43 @@ def push(
logger.warning('push remote message failed: %s', e) logger.warning('push remote message failed: %s', e)
_save_local(title, message, images) _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( def info(
title: str, title: str,
message: str | None = None, message: str | None = None,
@ -83,6 +110,7 @@ def info(
): ):
logger.info('user.info: %s', message) logger.info('user.info: %s', message)
push('KAA' + title, message, images=images) push('KAA' + title, message, images=images)
_show_toast('KAA' + title, message)
def warning( def warning(
title: str, title: str,
@ -98,7 +126,8 @@ def warning(
:param once: 每次运行是否只显示一次 :param once: 每次运行是否只显示一次
""" """
logger.warning('user.warning: %s', message) logger.warning('user.warning: %s', message)
push("KAA 警告:" + title, message, images=images) push("琴音小助手警告:" + title, message, images=images)
_show_toast("琴音小助手警告:" + title, message)
def error( def error(
title: str, title: str,
@ -111,4 +140,5 @@ def error(
错误信息 错误信息
""" """
logger.error('user.error: %s', message) logger.error('user.error: %s', message)
push("KAA 错误:" + title, message, images=images) push("琴音小助手错误:" + title, message, images=images)
_show_toast("琴音小助手错误:" + title, message)

View File

@ -41,6 +41,7 @@ dependencies = [
# TODO: move these dependencies to optional-dependencies # TODO: move these dependencies to optional-dependencies
"pywin32==310", "pywin32==310",
"ahk==1.8.3", "ahk==1.8.3",
"win11toast==0.35", # For Windows Toast Notification
] ]

View File

@ -1,4 +1,5 @@
-r requirements.txt -r requirements.txt
pywin32==310 pywin32==310
ahk==1.8.3 ahk==1.8.3
win11toast==0.35