feat(task): 优化培育方案错误与选人未找到错误的提示

This commit is contained in:
XcantloadX 2025-07-26 15:48:41 +08:00
parent 3e544e92a9
commit b51f9cdaa4
7 changed files with 102 additions and 12 deletions

View File

@ -12,7 +12,8 @@ from kotonebot.client import Device
from kotonebot.client.host.protocol import Instance from kotonebot.client.host.protocol import Instance
from kotonebot.backend.context import init_context, vars from kotonebot.backend.context import init_context, vars
from kotonebot.backend.context import task_registry, action_registry, Task, Action 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() log_stream = io.StringIO()
stream_handler = logging.StreamHandler(log_stream) stream_handler = logging.StreamHandler(log_stream)
@ -198,6 +199,20 @@ class KotoneBot:
self.events.task_status_changed.trigger(task1, 'cancelled') self.events.task_status_changed.trigger(task1, 'cancelled')
vars.flow.clear_interrupt() vars.flow.clear_interrupt()
break 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: except Exception as e:
logger.error(f'Task failed: {task.name}') logger.error(f'Task failed: {task.name}')

View File

@ -1,9 +1,41 @@
from typing import Callable
class KotonebotError(Exception): class KotonebotError(Exception):
pass pass
class KotonebotWarning(Warning): class KotonebotWarning(Warning):
pass 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): class UnrecoverableError(KotonebotError):
pass pass

View File

@ -4,7 +4,9 @@ import uuid
import re import re
import logging import logging
from typing import Literal 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 from .const import ProduceAction, RecommendCardDetectionMode
@ -217,17 +219,17 @@ class ProduceSolutionManager:
:param id: 方案ID :param id: 方案ID
:return: 方案对象 :return: 方案对象
:raises FileNotFoundError: 当方案不存在时 :raises ProduceSloutionNotFoundError: 当方案不存在时
""" """
file_path = self._find_file_path_by_id(id) file_path = self._find_file_path_by_id(id)
if not file_path: if not file_path:
raise FileNotFoundError(f"Solution with id '{id}' not found") raise ProduceSolutionNotFoundError(id)
try: try:
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
return ProduceSolution.model_validate_json(f.read()) return ProduceSolution.model_validate_json(f.read())
except Exception as e: except ValidationError as e:
raise FileNotFoundError(f"Failed to read solution with id '{id}': {e}") raise ProduceSolutionInvalidError(id, file_path, e)
def duplicate(self, id: str) -> ProduceSolution: def duplicate(self, id: str) -> ProduceSolution:
""" """
@ -235,7 +237,7 @@ class ProduceSolutionManager:
:param id: 要复制的方案ID :param id: 要复制的方案ID
:return: 新的方案对象具有新的ID和名称 :return: 新的方案对象具有新的ID和名称
:raises FileNotFoundError: 当原方案不存在时 :raises ProduceSolutionNotFoundError: 当原方案不存在时
""" """
original = self.read(id) original = self.read(id)

38
kotonebot/kaa/errors.py Normal file
View File

@ -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'
)

View File

@ -14,6 +14,7 @@ from typing import List, Dict, Tuple, Literal, Generator, Callable, Any, get_arg
import cv2 import cv2
import gradio as gr import gradio as gr
from kotonebot.kaa.errors import ProduceSolutionNotFoundError
from kotonebot.kaa.main import Kaa from kotonebot.kaa.main import Kaa
from kotonebot.kaa.db import IdolCard from kotonebot.kaa.db import IdolCard
from kotonebot.backend.context.context import vars from kotonebot.backend.context.context import vars
@ -1369,7 +1370,7 @@ class KotoneBotUI:
if selected_solution_id: if selected_solution_id:
try: try:
current_solution = solution_manager.read(selected_solution_id) current_solution = solution_manager.read(selected_solution_id)
except FileNotFoundError: except ProduceSolutionNotFoundError:
pass pass
if current_solution is None: if current_solution is None:
@ -1691,7 +1692,7 @@ class KotoneBotUI:
gr.Checkbox(value=solution.data.skip_commu, visible=True), gr.Checkbox(value=solution.data.skip_commu, visible=True),
gr.Button(visible=True), # save_solution_btn gr.Button(visible=True), # save_solution_btn
] ]
except FileNotFoundError: except ProduceSolutionNotFoundError:
gr.Warning(f"培育方案 {solution_id} 不存在") gr.Warning(f"培育方案 {solution_id} 不存在")
return on_solution_change(None) return on_solution_change(None)
except Exception as e: except Exception as e:

View File

@ -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, \ from ..produce.in_purodyuusu import hajime_pro, hajime_regular, hajime_master, resume_pro_produce, resume_regular_produce, \
resume_master_produce resume_master_produce
from kotonebot import device, image, ocr, task, action, sleep, contains, regex from kotonebot import device, image, ocr, task, action, sleep, contains, regex
from kotonebot.kaa.errors import IdolCardNotFoundError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,7 +59,7 @@ def select_idol(skin_id: str):
# 选择偶像 # 选择偶像
pos = locate_idol(skin_id) pos = locate_idol(skin_id)
if pos is None: if pos is None:
raise ValueError(f"Idol {skin_id} not found.") raise IdolCardNotFoundError(skin_id)
# 确认 # 确认
it.reset() it.reset()
while btn_confirm := image.find(R.Common.ButtonConfirmNoIcon): while btn_confirm := image.find(R.Common.ButtonConfirmNoIcon):

View File

@ -12,6 +12,7 @@ from kotonebot.kaa.config.produce import (
ProduceSolutionManager ProduceSolutionManager
) )
from kotonebot.kaa.config.const import ProduceAction, RecommendCardDetectionMode from kotonebot.kaa.config.const import ProduceAction, RecommendCardDetectionMode
from kotonebot.kaa.errors import ProduceSolutionNotFoundError
class TestProduceData(TestCase): class TestProduceData(TestCase):
@ -360,7 +361,7 @@ class TestProduceSolutionManager(TestCase):
def test_read_nonexistent_solution(self): def test_read_nonexistent_solution(self):
"""测试读取不存在的方案""" """测试读取不存在的方案"""
with self.assertRaises(FileNotFoundError) as context: with self.assertRaises(ProduceSolutionNotFoundError) as context:
self.manager.read('nonexistent_id') self.manager.read('nonexistent_id')
self.assertIn("Solution with id 'nonexistent_id' not found", str(context.exception)) 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): def test_duplicate_nonexistent_solution(self):
"""测试复制不存在的方案""" """测试复制不存在的方案"""
with self.assertRaises(FileNotFoundError): with self.assertRaises(ProduceSolutionNotFoundError):
self.manager.duplicate('nonexistent_id') self.manager.duplicate('nonexistent_id')
def test_corrupted_json_handling(self): def test_corrupted_json_handling(self):