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.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}')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue