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.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}')

View File

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

View File

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

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 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:

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, \
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):

View File

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