From b51f9cdaa47c99a723a4dc8df998050bb4b04850 Mon Sep 17 00:00:00 2001 From: XcantloadX <3188996979@qq.com> Date: Sat, 26 Jul 2025 15:48:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(task):=20=E4=BC=98=E5=8C=96=E5=9F=B9?= =?UTF-8?q?=E8=82=B2=E6=96=B9=E6=A1=88=E9=94=99=E8=AF=AF=E4=B8=8E=E9=80=89?= =?UTF-8?q?=E4=BA=BA=E6=9C=AA=E6=89=BE=E5=88=B0=E9=94=99=E8=AF=AF=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kotonebot/backend/bot.py | 17 +++++++++++- kotonebot/errors.py | 32 ++++++++++++++++++++++ kotonebot/kaa/config/produce.py | 14 ++++++---- kotonebot/kaa/errors.py | 38 ++++++++++++++++++++++++++ kotonebot/kaa/main/gr.py | 5 ++-- kotonebot/kaa/tasks/produce/produce.py | 3 +- tests/kaa/test_produce_config.py | 5 ++-- 7 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 kotonebot/kaa/errors.py diff --git a/kotonebot/backend/bot.py b/kotonebot/backend/bot.py index fd146a1..ae5e6bc 100644 --- a/kotonebot/backend/bot.py +++ b/kotonebot/backend/bot.py @@ -12,7 +12,8 @@ from kotonebot.client import Device from kotonebot.client.host.protocol import Instance from kotonebot.backend.context import init_context, vars from kotonebot.backend.context import task_registry, action_registry, Task, Action -from kotonebot.errors import StopCurrentTask +from kotonebot.errors import StopCurrentTask, UserFriendlyError +from kotonebot.interop.win.task_dialog import TaskDialog log_stream = io.StringIO() stream_handler = logging.StreamHandler(log_stream) @@ -198,6 +199,20 @@ class KotoneBot: self.events.task_status_changed.trigger(task1, 'cancelled') vars.flow.clear_interrupt() break + # 用户可以自行处理的错误 + except UserFriendlyError as e: + logger.error(f'Task failed: {task.name}') + logger.exception(f'Error: ') + dialog = TaskDialog( + title='琴音小助手', + common_buttons=0, + main_instruction='任务执行失败', + content=e.message, + custom_buttons=e.action_buttons, + main_icon='error' + ) + result_custom, _, _ = dialog.show() + e.invoke(result_custom) # 其他错误 except Exception as e: logger.error(f'Task failed: {task.name}') diff --git a/kotonebot/errors.py b/kotonebot/errors.py index 924b52a..fcdbba0 100644 --- a/kotonebot/errors.py +++ b/kotonebot/errors.py @@ -1,9 +1,41 @@ +from typing import Callable + + class KotonebotError(Exception): pass class KotonebotWarning(Warning): pass +class UserFriendlyError(KotonebotError): + def __init__( + self, + message: str, + actions: list[tuple[int, str, Callable[[], None]]] = [], + *args, **kwargs + ) -> None: + super().__init__(*args, **kwargs) + self.message = message + self.actions = actions or [] + + @property + def action_buttons(self) -> list[tuple[int, str]]: + """ + 以 (id: int, btn_text: str) 的形式返回所有按钮定义。 + """ + return [(id, text) for id, text, _ in self.actions] + + def invoke(self, action_id: int): + """ + 执行指定 ID 的 action。 + """ + for id, _, func in self.actions: + if id == action_id: + func() + break + else: + raise ValueError(f'Action with id {action_id} not found.') + class UnrecoverableError(KotonebotError): pass diff --git a/kotonebot/kaa/config/produce.py b/kotonebot/kaa/config/produce.py index 832859d..f4c941e 100644 --- a/kotonebot/kaa/config/produce.py +++ b/kotonebot/kaa/config/produce.py @@ -4,7 +4,9 @@ import uuid import re import logging from typing import Literal -from pydantic import BaseModel, ConfigDict, field_serializer, field_validator +from pydantic import BaseModel, ConfigDict, ValidationError, field_serializer, field_validator + +from kotonebot.kaa.errors import ProduceSolutionInvalidError, ProduceSolutionNotFoundError from .const import ProduceAction, RecommendCardDetectionMode @@ -217,17 +219,17 @@ class ProduceSolutionManager: :param id: 方案ID :return: 方案对象 - :raises FileNotFoundError: 当方案不存在时 + :raises ProduceSloutionNotFoundError: 当方案不存在时 """ file_path = self._find_file_path_by_id(id) if not file_path: - raise FileNotFoundError(f"Solution with id '{id}' not found") + raise ProduceSolutionNotFoundError(id) try: with open(file_path, 'r', encoding='utf-8') as f: return ProduceSolution.model_validate_json(f.read()) - except Exception as e: - raise FileNotFoundError(f"Failed to read solution with id '{id}': {e}") + except ValidationError as e: + raise ProduceSolutionInvalidError(id, file_path, e) def duplicate(self, id: str) -> ProduceSolution: """ @@ -235,7 +237,7 @@ class ProduceSolutionManager: :param id: 要复制的方案ID :return: 新的方案对象(具有新的ID和名称) - :raises FileNotFoundError: 当原方案不存在时 + :raises ProduceSolutionNotFoundError: 当原方案不存在时 """ original = self.read(id) diff --git a/kotonebot/kaa/errors.py b/kotonebot/kaa/errors.py new file mode 100644 index 0000000..544681a --- /dev/null +++ b/kotonebot/kaa/errors.py @@ -0,0 +1,38 @@ +import os +from kotonebot.errors import UserFriendlyError + + +class KaaError(Exception): + pass + +class KaaUserFriendlyError(UserFriendlyError, KaaError): + def __init__(self, message: str, help_link: str): + super().__init__(message, [ + (0, '打开帮助', lambda: os.startfile(help_link)), + (1, '知道了', lambda: None) + ]) + +class ProduceSolutionNotFoundError(KaaUserFriendlyError): + def __init__(self, solution_id: str): + self.solution_id = solution_id + super().__init__( + f'培育方案「{solution_id}」不存在,请检查设置是否正确。', + 'https://kdocs.cn/l/cetCY8mGKHLj?linkname=saPrDAmMd4' + ) + +class ProduceSolutionInvalidError(KaaUserFriendlyError): + def __init__(self, solution_id: str, file_path: str, reason: Exception): + self.solution_id = solution_id + self.reason = reason + super().__init__( + f'培育方案「{solution_id}」(路径 {file_path})存在无效配置,载入失败。', + 'https://kdocs.cn/l/cetCY8mGKHLj?linkname=xnLUW1YYKz' + ) + +class IdolCardNotFoundError(KaaUserFriendlyError): + def __init__(self, skin_id: str): + self.skin_id = skin_id + super().__init__( + f'未找到 ID 为「{skin_id}」的偶像卡。请检查游戏内偶像皮肤与培育方案中偶像皮肤是否一致。', + 'https://kdocs.cn/l/cetCY8mGKHLj?linkname=cySASqoPGj' + ) diff --git a/kotonebot/kaa/main/gr.py b/kotonebot/kaa/main/gr.py index a4bc5fd..c291cc6 100644 --- a/kotonebot/kaa/main/gr.py +++ b/kotonebot/kaa/main/gr.py @@ -14,6 +14,7 @@ from typing import List, Dict, Tuple, Literal, Generator, Callable, Any, get_arg import cv2 import gradio as gr +from kotonebot.kaa.errors import ProduceSolutionNotFoundError from kotonebot.kaa.main import Kaa from kotonebot.kaa.db import IdolCard from kotonebot.backend.context.context import vars @@ -1369,7 +1370,7 @@ class KotoneBotUI: if selected_solution_id: try: current_solution = solution_manager.read(selected_solution_id) - except FileNotFoundError: + except ProduceSolutionNotFoundError: pass if current_solution is None: @@ -1691,7 +1692,7 @@ class KotoneBotUI: gr.Checkbox(value=solution.data.skip_commu, visible=True), gr.Button(visible=True), # save_solution_btn ] - except FileNotFoundError: + except ProduceSolutionNotFoundError: gr.Warning(f"培育方案 {solution_id} 不存在") return on_solution_change(None) except Exception as e: diff --git a/kotonebot/kaa/tasks/produce/produce.py b/kotonebot/kaa/tasks/produce/produce.py index 141fa00..6fed17f 100644 --- a/kotonebot/kaa/tasks/produce/produce.py +++ b/kotonebot/kaa/tasks/produce/produce.py @@ -15,6 +15,7 @@ from kotonebot.kaa.game_ui.idols_overview import locate_idol, match_idol from ..produce.in_purodyuusu import hajime_pro, hajime_regular, hajime_master, resume_pro_produce, resume_regular_produce, \ resume_master_produce from kotonebot import device, image, ocr, task, action, sleep, contains, regex +from kotonebot.kaa.errors import IdolCardNotFoundError logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ def select_idol(skin_id: str): # 选择偶像 pos = locate_idol(skin_id) if pos is None: - raise ValueError(f"Idol {skin_id} not found.") + raise IdolCardNotFoundError(skin_id) # 确认 it.reset() while btn_confirm := image.find(R.Common.ButtonConfirmNoIcon): diff --git a/tests/kaa/test_produce_config.py b/tests/kaa/test_produce_config.py index 9a06723..24a2875 100644 --- a/tests/kaa/test_produce_config.py +++ b/tests/kaa/test_produce_config.py @@ -12,6 +12,7 @@ from kotonebot.kaa.config.produce import ( ProduceSolutionManager ) from kotonebot.kaa.config.const import ProduceAction, RecommendCardDetectionMode +from kotonebot.kaa.errors import ProduceSolutionNotFoundError class TestProduceData(TestCase): @@ -360,7 +361,7 @@ class TestProduceSolutionManager(TestCase): def test_read_nonexistent_solution(self): """测试读取不存在的方案""" - with self.assertRaises(FileNotFoundError) as context: + with self.assertRaises(ProduceSolutionNotFoundError) as context: self.manager.read('nonexistent_id') self.assertIn("Solution with id 'nonexistent_id' not found", str(context.exception)) @@ -429,7 +430,7 @@ class TestProduceSolutionManager(TestCase): def test_duplicate_nonexistent_solution(self): """测试复制不存在的方案""" - with self.assertRaises(FileNotFoundError): + with self.assertRaises(ProduceSolutionNotFoundError): self.manager.duplicate('nonexistent_id') def test_corrupted_json_handling(self):