feat(task): 优化培育方案错误与选人未找到错误的提示
This commit is contained in:
parent
3e544e92a9
commit
b51f9cdaa4
|
@ -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}')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'
|
||||
)
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue