kotones-auto-assistant/kotonebot/kaa/main/gr.py

1188 lines
50 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import zipfile
import logging
from functools import partial
from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Literal, Generator
import cv2
import gradio as gr
from kotonebot.kaa.main import Kaa
from kotonebot.kaa.db import IdolCard
from kotonebot.config.manager import load_config, save_config
from kotonebot.config.base_config import UserConfig, BackendConfig
from kotonebot.backend.context import task_registry, ContextStackVars
from kotonebot.kaa.common import (
BaseConfig, APShopItems, CapsuleToysConfig, ClubRewardConfig, PurchaseConfig, ActivityFundsConfig,
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
MissionRewardConfig, DailyMoneyShopItems, ProduceAction,
RecommendCardDetectionMode, TraceConfig, StartGameConfig, EndGameConfig, UpgradeSupportCardConfig,
)
logger = logging.getLogger(__name__)
def _save_bug_report(
version: str,
path: str | None = None
) -> Generator[str, None, str]:
"""
保存报告
:param path: 保存的路径。若为 `None`,则保存到 `./reports/bug-{YY-MM-DD HH-MM-SS}.zip`。
:return: 保存的路径
"""
from kotonebot import device
from kotonebot.backend.context import ContextStackVars
# 确保目录存在
os.makedirs('logs', exist_ok=True)
os.makedirs('reports', exist_ok=True)
error = ""
if path is None:
path = f'./reports/bug-{datetime.now().strftime("%y-%m-%d %H-%M-%S")}.zip'
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
# 打包截图
yield "### 打包上次截图..."
try:
stack = ContextStackVars.current()
screenshot = None
if stack is not None:
screenshot = stack._screenshot
if screenshot is not None:
img = cv2.imencode('.png', screenshot)[1].tobytes()
zipf.writestr('last_screenshot.png', img)
if screenshot is None:
error += "无上次截图数据\n"
except Exception as e:
error += f"保存上次截图失败:{str(e)}\n"
# 打包当前截图
yield "### 打包当前截图..."
try:
screenshot = device.screenshot()
img = cv2.imencode('.png', screenshot)[1].tobytes()
zipf.writestr('current_screenshot.png', img)
except Exception as e:
error += f"保存当前截图失败:{str(e)}\n"
# 打包配置文件
yield "### 打包配置文件..."
try:
with open('config.json', 'r', encoding='utf-8') as f:
zipf.writestr('config.json', f.read())
except Exception as e:
error += f"保存配置文件失败:{str(e)}\n"
# 打包 logs 文件夹
if os.path.exists('logs'):
for root, dirs, files in os.walk('logs'):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.join('logs', os.path.relpath(file_path, 'logs'))
zipf.write(file_path, arcname)
yield f"### 打包 log 文件:{arcname}"
# 写出版本号
zipf.writestr('version.txt', version)
# 上传报告
from kotonebot.ui.file_host.sensio import upload
yield "### 上传报告..."
url = ''
try:
url = upload(path)
except Exception as e:
yield f"### 上传报告失败:{str(e)}\n\n"
return ''
final_msg = f"### 报告导出成功:{url}\n\n"
expire_time = datetime.now() + timedelta(days=7)
if error:
final_msg += f"### 但发生了以下错误\n\n"
final_msg += '\n* '.join(error.strip().split('\n'))
final_msg += '\n'
final_msg += f"### 此链接将于 {expire_time.strftime('%Y-%m-%d %H:%M:%S')}7 天后)过期\n\n"
final_msg += '### 复制以上文本并反馈给开发者'
yield final_msg
return path
class KotoneBotUI:
def __init__(self, kaa: Kaa) -> None:
self.is_running: bool = False
self.single_task_running: bool = False
self._kaa = kaa
self._load_config()
self._setup_kaa()
def _setup_kaa(self) -> None:
from kotonebot.backend.debug.vars import debug, clear_saved
self._kaa.initialize()
if self.current_config.keep_screenshots:
debug.auto_save_to_folder = 'dumps'
debug.enabled = True
clear_saved()
else:
debug.auto_save_to_folder = None
debug.enabled = False
def export_dumps(self) -> str:
"""导出 dumps 文件夹为 zip 文件"""
if not os.path.exists('dumps'):
return "dumps 文件夹不存在"
timestamp = datetime.now().strftime('%y-%m-%d-%H-%M-%S')
zip_filename = f'dumps-{timestamp}.zip'
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
for root, dirs, files in os.walk('dumps'):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, 'dumps')
zipf.write(file_path, arcname)
return f"已导出到 {zip_filename}"
def export_logs(self) -> str:
"""导出 logs 文件夹为 zip 文件"""
if not os.path.exists('logs'):
return "logs 文件夹不存在"
timestamp = datetime.now().strftime('%y-%m-%d-%H-%M-%S')
zip_filename = f'logs-{timestamp}.zip'
with zipfile.ZipFile(zip_filename, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
for root, dirs, files in os.walk('logs'):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, 'logs')
zipf.write(file_path, arcname)
return f"已导出到 {zip_filename}"
def get_button_status(self) -> str:
if not hasattr(self, 'run_status'):
return "启动"
if not self.run_status.running:
self.is_running = False
return "启动"
return "停止"
def update_task_status(self) -> List[List[str]]:
status_list: List[List[str]] = []
if not hasattr(self, 'run_status'):
for task_name, task in task_registry.items():
status_list.append([task.name, "等待中"])
return status_list
for task_status in self.run_status.tasks:
status_text = {
'pending': '等待中',
'running': '运行中',
'finished': '已完成',
'error': '出错',
'cancelled': '已取消'
}.get(task_status.status, '未知')
status_list.append([task_status.task.name, status_text])
return status_list
def toggle_run(self) -> Tuple[str, List[List[str]]]:
if not self.is_running:
return self.start_run()
return self.stop_run()
def start_run(self) -> Tuple[str, List[List[str]]]:
self.is_running = True
self.run_status = self._kaa.start_all()
return "停止", self.update_task_status()
def stop_run(self) -> Tuple[str, List[List[str]]]:
self.is_running = False
if self._kaa:
self.run_status.interrupt()
return "启动", self.update_task_status()
def start_single_task(self, task_name: str) -> Tuple[str, str]:
if not task_name:
gr.Warning("请先选择一个任务")
return "执行任务", ""
task = None
for name, t in task_registry.items():
if name == task_name:
task = t
break
if task is None:
gr.Warning(f"任务 {task_name} 未找到")
return "执行任务", ""
gr.Info(f"任务 {task_name} 开始执行")
self.single_task_running = True
self.run_status = self._kaa.start([task])
return "停止任务", f"正在执行任务: {task_name}"
def stop_single_task(self) -> Tuple[str, str]:
self.single_task_running = False
if hasattr(self, 'run_status') and self._kaa:
self.run_status.interrupt()
gr.Info("任务已停止")
return "执行任务", "任务已停止"
def save_settings(
self,
adb_ip: str,
adb_port: int,
screenshot_method: Literal['adb', 'adb_raw', 'uiautomator2', 'windows'],
keep_screenshots: bool,
check_emulator: bool,
emulator_path: str,
adb_emulator_name: str,
emulator_args: str,
trace_recommend_card_detection: bool,
purchase_enabled: bool,
money_enabled: bool,
ap_enabled: bool,
ap_items: List[str],
money_items: List[DailyMoneyShopItems],
activity_funds_enabled: bool,
presents_enabled: bool,
assignment_enabled: bool,
mini_live_reassign: bool,
mini_live_duration: Literal[4, 6, 12],
online_live_reassign: bool,
online_live_duration: Literal[4, 6, 12],
contest_enabled: bool,
select_which_contestant: Literal[1, 2, 3],
produce_enabled: bool,
produce_mode: Literal["regular"],
produce_count: int,
produce_idols: List[str],
memory_sets: List[str],
auto_set_memory: bool,
auto_set_support: bool,
use_pt_boost: bool,
use_note_boost: bool,
follow_producer: bool,
self_study_lesson: Literal['dance', 'visual', 'vocal'],
prefer_lesson_ap: bool,
actions_order: List[str],
recommend_card_detection_mode: str,
use_ap_drink: bool,
skip_commu: bool,
mission_reward_enabled: bool,
# club reward
club_reward_enabled: bool,
selected_note: DailyMoneyShopItems,
# upgrade support card
upgrade_support_card_enabled: bool,
# capsule toys
capsule_toys_enabled: bool,
friend_capsule_toys_count: int,
sense_capsule_toys_count: int,
logic_capsule_toys_count: int,
anomaly_capsule_toys_count: int,
# start game
start_game_enabled: bool,
start_through_kuyo: bool,
game_package_name: str,
kuyo_package_name: str,
# end game
exit_kaa: bool,
kill_game: bool,
kill_dmm: bool,
kill_emulator: bool,
shutdown: bool,
hibernate: bool,
) -> str:
ap_items_enum: List[Literal[0, 1, 2, 3]] = []
ap_items_map: Dict[str, APShopItems] = {
"支援强化点数提升": APShopItems.PRODUCE_PT_UP,
"笔记数提升": APShopItems.PRODUCE_NOTE_UP,
"重新挑战券": APShopItems.RECHALLENGE,
"回忆再生成券": APShopItems.REGENERATE_MEMORY
}
for item in ap_items:
if item in ap_items_map:
ap_items_enum.append(ap_items_map[item].value) # type: ignore
self.current_config.backend.adb_ip = adb_ip
self.current_config.backend.adb_port = adb_port
self.current_config.backend.adb_emulator_name = adb_emulator_name
self.current_config.backend.screenshot_impl = screenshot_method
self.current_config.keep_screenshots = keep_screenshots
self.current_config.backend.check_emulator = check_emulator
self.current_config.backend.emulator_path = emulator_path
self.current_config.backend.emulator_args = emulator_args
options = BaseConfig(
purchase=PurchaseConfig(
enabled=purchase_enabled,
money_enabled=money_enabled,
money_items=money_items,
ap_enabled=ap_enabled,
ap_items=ap_items_enum
),
activity_funds=ActivityFundsConfig(
enabled=activity_funds_enabled
),
presents=PresentsConfig(
enabled=presents_enabled
),
assignment=AssignmentConfig(
enabled=assignment_enabled,
mini_live_reassign_enabled=mini_live_reassign,
mini_live_duration=mini_live_duration,
online_live_reassign_enabled=online_live_reassign,
online_live_duration=online_live_duration
),
contest=ContestConfig(
enabled=contest_enabled,
select_which_contestant=select_which_contestant
),
produce=ProduceConfig(
enabled=produce_enabled,
mode=produce_mode,
produce_count=produce_count,
idols=produce_idols,
memory_sets=[int(i) for i in memory_sets],
auto_set_memory=auto_set_memory,
auto_set_support_card=auto_set_support,
use_pt_boost=use_pt_boost,
use_note_boost=use_note_boost,
follow_producer=follow_producer,
self_study_lesson=self_study_lesson,
prefer_lesson_ap=prefer_lesson_ap,
actions_order=[ProduceAction(action) for action in actions_order],
recommend_card_detection_mode=RecommendCardDetectionMode(recommend_card_detection_mode),
use_ap_drink=use_ap_drink,
skip_commu=skip_commu
),
mission_reward=MissionRewardConfig(
enabled=mission_reward_enabled
),
club_reward=ClubRewardConfig(
enabled=club_reward_enabled,
selected_note=selected_note
),
upgrade_support_card=UpgradeSupportCardConfig(
enabled=upgrade_support_card_enabled
),
capsule_toys=CapsuleToysConfig(
enabled=capsule_toys_enabled,
friend_capsule_toys_count=friend_capsule_toys_count,
sense_capsule_toys_count=sense_capsule_toys_count,
logic_capsule_toys_count=logic_capsule_toys_count,
anomaly_capsule_toys_count=anomaly_capsule_toys_count
),
start_game=StartGameConfig(
enabled=start_game_enabled,
start_through_kuyo=start_through_kuyo,
game_package_name=game_package_name,
kuyo_package_name=kuyo_package_name
),
end_game=EndGameConfig(
exit_kaa=exit_kaa,
kill_game=kill_game,
kill_dmm=kill_dmm,
kill_emulator=kill_emulator,
shutdown=shutdown,
hibernate=hibernate
),
trace=TraceConfig(
recommend_card_detection=trace_recommend_card_detection
)
)
self.current_config.options = options
try:
save_config(self.config, "config.json")
gr.Success("设置已保存,请重启程序!")
return ""
except Exception as e:
gr.Warning(f"保存设置失败:{str(e)}")
return ""
def _create_status_tab(self) -> None:
with gr.Tab("状态"):
gr.Markdown("## 状态")
with gr.Row():
run_btn = gr.Button("启动", scale=1)
if self._kaa.upgrade_msg:
gr.Markdown('### 配置升级报告')
gr.Markdown(self._kaa.upgrade_msg)
gr.Markdown('脚本报错或者卡住?点击"日志"选项卡中的"一键导出报告"可以快速反馈!')
task_status = gr.Dataframe(
headers=["任务", "状态"],
value=self.update_task_status(),
label="任务状态"
)
def on_run_click(evt: gr.EventData) -> Tuple[str, List[List[str]]]:
return self.toggle_run()
run_btn.click(
fn=on_run_click,
outputs=[run_btn, task_status]
)
# 添加定时器,分别更新按钮状态和任务状态
gr.Timer(1.0).tick(
fn=self.get_button_status,
outputs=[run_btn]
)
gr.Timer(1.0).tick(
fn=self.update_task_status,
outputs=[task_status]
)
def _create_task_tab(self) -> None:
with gr.Tab("任务"):
gr.Markdown("## 执行任务")
# 创建任务选择下拉框
task_choices = [task.name for task in task_registry.values()]
task_dropdown = gr.Dropdown(
choices=task_choices,
label="选择要执行的任务",
info="选择一个要单独执行的任务",
type="value",
value=None
)
# 创建执行按钮
execute_btn = gr.Button("执行任务")
task_result = gr.Markdown("")
def toggle_single_task(task_name: str) -> Tuple[str, str]:
if self.single_task_running:
return self.stop_single_task()
else:
return self.start_single_task(task_name)
def get_task_button_status() -> str:
if not hasattr(self, 'run_status') or not self.run_status.running:
self.single_task_running = False
return "执行任务"
return "停止任务"
def get_single_task_status() -> str:
if not hasattr(self, 'run_status'):
return ""
if not self.run_status.running and self.single_task_running:
# 任务已结束但状态未更新
self.single_task_running = False
# 检查任务状态
for task_status in self.run_status.tasks:
status = task_status.status
task_name = task_status.task.name
if status == 'finished':
return f"任务 {task_name} 已完成"
elif status == 'error':
return f"任务 {task_name} 出错"
elif status == 'cancelled':
return f"任务 {task_name} 已取消"
return "任务已结束"
if self.single_task_running:
for task_status in self.run_status.tasks:
if task_status.status == 'running':
return f"正在执行任务: {task_status.task.name}"
return "正在准备执行任务..."
return ""
execute_btn.click(
fn=toggle_single_task,
inputs=[task_dropdown],
outputs=[execute_btn, task_result]
)
# 添加定时器更新按钮状态和任务状态
gr.Timer(1.0).tick(
fn=get_task_button_status,
outputs=[execute_btn]
)
gr.Timer(1.0).tick(
fn=get_single_task_status,
outputs=[task_result]
)
def _create_emulator_settings(self) -> Tuple[gr.Textbox, gr.Number, gr.Dropdown, gr.Checkbox, gr.Checkbox, gr.Textbox, gr.Textbox, gr.Textbox]:
with gr.Column():
gr.Markdown("### 模拟器设置")
adb_ip = gr.Textbox(
value=self.current_config.backend.adb_ip,
label="ADB IP 地址",
info=BackendConfig.model_fields['adb_ip'].description,
interactive=True
)
adb_port = gr.Number(
value=self.current_config.backend.adb_port,
label="ADB 端口",
info=BackendConfig.model_fields['adb_port'].description,
minimum=1,
maximum=65535,
step=1,
interactive=True
)
screenshot_impl = gr.Dropdown(
choices=['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows'],
value=self.current_config.backend.screenshot_impl,
label="截图方法",
info=BackendConfig.model_fields['screenshot_impl'].description,
interactive=True
)
keep_screenshots = gr.Checkbox(
label="保留截图数据",
value=self.current_config.keep_screenshots,
info=UserConfig.model_fields['keep_screenshots'].description,
interactive=True
)
check_emulator = gr.Checkbox(
label="检查并启动模拟器",
value=self.current_config.backend.check_emulator,
info=BackendConfig.model_fields['check_emulator'].description,
interactive=True
)
with gr.Group(visible=self.current_config.backend.check_emulator) as check_emulator_group:
emulator_path = gr.Textbox(
value=self.current_config.backend.emulator_path,
label="模拟器 exe 文件路径",
info=BackendConfig.model_fields['emulator_path'].description,
interactive=True
)
adb_emulator_name = gr.Textbox(
value=self.current_config.backend.adb_emulator_name,
label="ADB 模拟器名称",
info=BackendConfig.model_fields['adb_emulator_name'].description,
interactive=True
)
emulator_args = gr.Textbox(
value=self.current_config.backend.emulator_args,
label="模拟器启动参数",
info=BackendConfig.model_fields['emulator_args'].description,
interactive=True
)
check_emulator.change(
fn=lambda x: gr.Group(visible=x),
inputs=[check_emulator],
outputs=[check_emulator_group]
)
return adb_ip, adb_port, screenshot_impl, keep_screenshots, check_emulator, emulator_path, adb_emulator_name, emulator_args
def _create_purchase_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Dropdown]:
with gr.Column():
gr.Markdown("### 商店购买设置")
purchase_enabled = gr.Checkbox(
label="启用商店购买",
value=self.current_config.options.purchase.enabled,
info=PurchaseConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.purchase.enabled) as purchase_group:
money_enabled = gr.Checkbox(
label="启用金币购买",
value=self.current_config.options.purchase.money_enabled,
info=PurchaseConfig.model_fields['money_enabled'].description
)
# 添加金币商店商品选择
money_items = gr.Dropdown(
multiselect=True,
choices=list(DailyMoneyShopItems.all()),
value=self.current_config.options.purchase.money_items,
label="金币商店购买物品",
info=PurchaseConfig.model_fields['money_items'].description
)
ap_enabled = gr.Checkbox(
label="启用AP购买",
value=self.current_config.options.purchase.ap_enabled,
info=PurchaseConfig.model_fields['ap_enabled'].description
)
# 转换枚举值为显示文本
selected_items: List[str] = []
ap_items_map = {
APShopItems.PRODUCE_PT_UP: "支援强化点数提升",
APShopItems.PRODUCE_NOTE_UP: "笔记数提升",
APShopItems.RECHALLENGE: "重新挑战券",
APShopItems.REGENERATE_MEMORY: "回忆再生成券"
}
for item_value in self.current_config.options.purchase.ap_items:
item_enum = APShopItems(item_value)
if item_enum in ap_items_map:
selected_items.append(ap_items_map[item_enum])
ap_items = gr.Dropdown(
multiselect=True,
choices=list(ap_items_map.values()),
value=selected_items,
label="AP商店购买物品",
info=PurchaseConfig.model_fields['ap_items'].description
)
purchase_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[purchase_enabled],
outputs=[purchase_group]
)
return purchase_enabled, money_enabled, ap_enabled, ap_items, money_items
def _create_work_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Dropdown, gr.Checkbox, gr.Dropdown]:
with gr.Column():
gr.Markdown("### 工作设置")
assignment_enabled = gr.Checkbox(
label="启用工作",
value=self.current_config.options.assignment.enabled,
info=AssignmentConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.assignment.enabled) as work_group:
with gr.Row():
with gr.Column():
mini_live_reassign = gr.Checkbox(
label="启用重新分配 MiniLive",
value=self.current_config.options.assignment.mini_live_reassign_enabled,
info=AssignmentConfig.model_fields['mini_live_reassign_enabled'].description
)
mini_live_duration = gr.Dropdown(
choices=[4, 6, 12],
value=self.current_config.options.assignment.mini_live_duration,
label="MiniLive 工作时长",
interactive=True,
info=AssignmentConfig.model_fields['mini_live_duration'].description
)
with gr.Column():
online_live_reassign = gr.Checkbox(
label="启用重新分配 OnlineLive",
value=self.current_config.options.assignment.online_live_reassign_enabled,
info=AssignmentConfig.model_fields['online_live_reassign_enabled'].description
)
online_live_duration = gr.Dropdown(
choices=[4, 6, 12],
value=self.current_config.options.assignment.online_live_duration,
label="OnlineLive 工作时长",
interactive=True,
info=AssignmentConfig.model_fields['online_live_duration'].description
)
assignment_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[assignment_enabled],
outputs=[work_group]
)
return assignment_enabled, mini_live_reassign, mini_live_duration, online_live_reassign, online_live_duration
def _create_contest_settings(self) -> Tuple[gr.Checkbox, gr.Dropdown]:
with gr.Column():
gr.Markdown("### 竞赛设置")
contest_enabled = gr.Checkbox(
label="启用竞赛",
value=self.current_config.options.contest.enabled,
info=ContestConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.contest.enabled) as contest_group:
select_which_contestant = gr.Dropdown(
choices=[1, 2, 3],
value=self.current_config.options.contest.select_which_contestant,
label="选择第几位竞赛目标",
interactive=True,
info=ContestConfig.model_fields['select_which_contestant'].description
)
contest_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[contest_enabled],
outputs=[contest_group]
)
return contest_enabled, select_which_contestant
def _create_produce_settings(self):
with gr.Column():
gr.Markdown("### 培育设置")
produce_enabled = gr.Checkbox(
label="启用培育",
value=self.current_config.options.produce.enabled,
info=ProduceConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.produce.enabled) as produce_group:
produce_mode = gr.Dropdown(
choices=["regular", "pro", "master"],
value=self.current_config.options.produce.mode,
label="培育模式",
info=ProduceConfig.model_fields['mode'].description
)
produce_count = gr.Number(
minimum=1,
value=self.current_config.options.produce.produce_count,
label="培育次数",
interactive=True,
info=ProduceConfig.model_fields['produce_count'].description
)
# 添加偶像选择
idol_choices = []
for idol in IdolCard.all():
if idol.is_another:
idol_choices.append((f'{idol.name} 「{idol.another_name}', idol.skin_id))
else:
idol_choices.append((f'{idol.name}', idol.skin_id))
selected_idols = self.current_config.options.produce.idols
produce_idols = gr.Dropdown(
choices=idol_choices,
value=selected_idols,
label="选择要培育的偶像",
multiselect=True,
interactive=True,
info=ProduceConfig.model_fields['idols'].description
)
has_kotone = any("藤田ことね" in idol for idol in selected_idols)
is_strict_mode = self.current_config.options.produce.recommend_card_detection_mode == RecommendCardDetectionMode.STRICT
kotone_warning = gr.Markdown(
visible=has_kotone and not is_strict_mode,
value="使用「藤田ことね」进行培育时,确保将「推荐卡检测模式」设置为「严格模式」"
)
auto_set_memory = gr.Checkbox(
label="自动编成回忆",
value=self.current_config.options.produce.auto_set_memory,
info=ProduceConfig.model_fields['auto_set_memory'].description
)
with gr.Group(visible=not self.current_config.options.produce.auto_set_memory) as memory_sets_group:
memory_sets = gr.Dropdown(
choices=[str(i) for i in range(1, 11)], # 假设最多10个编成位
value=[str(i) for i in self.current_config.options.produce.memory_sets],
label="回忆编成编号",
multiselect=True,
interactive=True,
info=ProduceConfig.model_fields['memory_sets'].description
)
# 添加偶像选择变化时的回调
def update_kotone_warning(selected_idols, recommend_card_detection_mode):
has_kotone = any("藤田ことね" in idol for idol in selected_idols)
is_strict_mode = recommend_card_detection_mode == RecommendCardDetectionMode.STRICT.value
return gr.Markdown(visible=has_kotone and not is_strict_mode)
auto_set_support = gr.Checkbox(
label="自动编成支援卡",
value=self.current_config.options.produce.auto_set_support_card,
info=ProduceConfig.model_fields['auto_set_support_card'].description
)
use_pt_boost = gr.Checkbox(
label="使用支援强化 Pt 提升",
value=self.current_config.options.produce.use_pt_boost,
info=ProduceConfig.model_fields['use_pt_boost'].description
)
use_note_boost = gr.Checkbox(
label="使用笔记数提升",
value=self.current_config.options.produce.use_note_boost,
info=ProduceConfig.model_fields['use_note_boost'].description
)
follow_producer = gr.Checkbox(
label="关注租借了支援卡的制作人",
value=self.current_config.options.produce.follow_producer,
info=ProduceConfig.model_fields['follow_producer'].description
)
self_study_lesson = gr.Dropdown(
choices=['dance', 'visual', 'vocal'],
value=self.current_config.options.produce.self_study_lesson,
label='文化课自习时选项',
info='选择自习课类型'
)
prefer_lesson_ap = gr.Checkbox(
label="SP 课程优先",
value=self.current_config.options.produce.prefer_lesson_ap,
info=ProduceConfig.model_fields['prefer_lesson_ap'].description
)
actions_order = gr.Dropdown(
choices=[(action.display_name, action.value) for action in ProduceAction],
value=[action.value for action in self.current_config.options.produce.actions_order],
label="行动优先级",
info="设置每周行动的优先级顺序",
multiselect=True
)
recommend_card_detection_mode = gr.Dropdown(
choices=[
(RecommendCardDetectionMode.NORMAL.display_name, RecommendCardDetectionMode.NORMAL.value),
(RecommendCardDetectionMode.STRICT.display_name, RecommendCardDetectionMode.STRICT.value)
],
value=self.current_config.options.produce.recommend_card_detection_mode.value,
label="推荐卡检测模式",
info=ProduceConfig.model_fields['recommend_card_detection_mode'].description
)
use_ap_drink = gr.Checkbox(
label="AP 不足时自动使用 AP 饮料",
value=self.current_config.options.produce.use_ap_drink,
info=ProduceConfig.model_fields['use_ap_drink'].description
)
skip_commu = gr.Checkbox(
label="检测并跳过交流",
value=self.current_config.options.produce.skip_commu,
info=ProduceConfig.model_fields['skip_commu'].description
)
recommend_card_detection_mode.change(
fn=update_kotone_warning,
inputs=[produce_idols, recommend_card_detection_mode],
outputs=kotone_warning
)
produce_idols.change(
fn=update_kotone_warning,
inputs=[produce_idols, recommend_card_detection_mode],
outputs=kotone_warning
)
produce_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[produce_enabled],
outputs=[produce_group]
)
auto_set_memory.change(
fn=lambda x: gr.Group(visible=not x),
inputs=[auto_set_memory],
outputs=[memory_sets_group]
)
return produce_enabled, produce_mode, produce_count, produce_idols, memory_sets, auto_set_memory, auto_set_support, use_pt_boost, use_note_boost, follow_producer, self_study_lesson, prefer_lesson_ap, actions_order, recommend_card_detection_mode, use_ap_drink, skip_commu
def _create_club_reward_settings(self) -> Tuple[gr.Checkbox, gr.Dropdown]:
with gr.Column():
gr.Markdown("### 社团奖励设置")
club_reward_enabled = gr.Checkbox(
label="启用社团奖励",
value=self.current_config.options.club_reward.enabled,
info=ClubRewardConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.club_reward.enabled) as club_reward_group:
selected_note = gr.Dropdown(
choices=list(DailyMoneyShopItems.note_items()),
value=self.current_config.options.club_reward.selected_note,
label="想在社团奖励中获取到的笔记",
interactive=True,
info=ClubRewardConfig.model_fields['selected_note'].description
)
club_reward_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[club_reward_enabled],
outputs=[club_reward_group]
)
return club_reward_enabled, selected_note
def _create_capsule_toys_settings(self) -> Tuple[gr.Checkbox, gr.Number, gr.Number, gr.Number, gr.Number]:
with gr.Column():
gr.Markdown("### 扭蛋设置")
capsule_toys_enabled = gr.Checkbox(
label="是否启用自动扭蛋机",
value=self.current_config.options.capsule_toys.enabled,
info=CapsuleToysConfig.model_fields['enabled'].description
)
min_value = 0
max_value = 10
with gr.Group(visible=self.current_config.options.capsule_toys.enabled) as capsule_toys_group:
friend_capsule_toys_count = gr.Number(
value=self.current_config.options.capsule_toys.friend_capsule_toys_count,
label="好友扭蛋机的扭蛋次数",
info=CapsuleToysConfig.model_fields['friend_capsule_toys_count'].description,
minimum=0,
maximum=5
)
sense_capsule_toys_count = gr.Number(
value=self.current_config.options.capsule_toys.sense_capsule_toys_count,
label="感性扭蛋机的扭蛋次数",
info=CapsuleToysConfig.model_fields['sense_capsule_toys_count'].description,
minimum=0,
maximum=5
)
logic_capsule_toys_count = gr.Number(
value=self.current_config.options.capsule_toys.logic_capsule_toys_count,
label="逻辑扭蛋机的扭蛋次数",
info=CapsuleToysConfig.model_fields['logic_capsule_toys_count'].description,
minimum=0,
maximum=5
)
anomaly_capsule_toys_count = gr.Number(
value=self.current_config.options.capsule_toys.anomaly_capsule_toys_count,
label="非凡扭蛋机的扭蛋次数",
info=CapsuleToysConfig.model_fields['anomaly_capsule_toys_count'].description,
minimum=0,
maximum=5
)
capsule_toys_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[capsule_toys_enabled],
outputs=[capsule_toys_group]
)
return capsule_toys_enabled, friend_capsule_toys_count, sense_capsule_toys_count, logic_capsule_toys_count, anomaly_capsule_toys_count
def _create_start_game_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Textbox, gr.Textbox]:
with gr.Column():
gr.Markdown("### 启动游戏设置")
start_game_enabled = gr.Checkbox(
label="是否启用 自动启动游戏",
value=self.current_config.options.start_game.enabled,
info=StartGameConfig.model_fields['enabled'].description
)
with gr.Group(visible=self.current_config.options.start_game.enabled) as start_game_group:
start_through_kuyo = gr.Checkbox(
label="是否通过Kuyo启动游戏",
value=self.current_config.options.start_game.start_through_kuyo,
info=StartGameConfig.model_fields['start_through_kuyo'].description
)
game_package_name = gr.Textbox(
value=self.current_config.options.start_game.game_package_name,
label="游戏包名",
info=StartGameConfig.model_fields['game_package_name'].description
)
kuyo_package_name = gr.Textbox(
value=self.current_config.options.start_game.kuyo_package_name,
label="Kuyo包名",
info=StartGameConfig.model_fields['kuyo_package_name'].description
)
start_game_enabled.change(
fn=lambda x: gr.Group(visible=x),
inputs=[start_game_enabled],
outputs=[start_game_group]
)
return start_game_enabled, start_through_kuyo, game_package_name, kuyo_package_name
def _create_end_game_settings(self) -> Tuple[gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Checkbox, gr.Checkbox]:
with gr.Column():
gr.Markdown("### 关闭游戏设置")
gr.Markdown("在所有任务执行完毕后执行下面这些操作:\n(执行单个任务时不会触发)")
exit_kaa = gr.Checkbox(
label="退出 kaa",
value=self.current_config.options.end_game.exit_kaa,
info=EndGameConfig.model_fields['exit_kaa'].description
)
kill_game = gr.Checkbox(
label="关闭游戏",
value=self.current_config.options.end_game.kill_game,
info=EndGameConfig.model_fields['kill_game'].description
)
kill_dmm = gr.Checkbox(
label="关闭 DMM",
value=self.current_config.options.end_game.kill_dmm,
info=EndGameConfig.model_fields['kill_dmm'].description
)
kill_emulator = gr.Checkbox(
label="关闭模拟器",
value=self.current_config.options.end_game.kill_emulator,
info=EndGameConfig.model_fields['kill_emulator'].description
)
shutdown = gr.Checkbox(
label="关闭系统",
value=self.current_config.options.end_game.shutdown,
info=EndGameConfig.model_fields['shutdown'].description
)
hibernate = gr.Checkbox(
label="休眠系统",
value=self.current_config.options.end_game.hibernate,
info=EndGameConfig.model_fields['hibernate'].description
)
return exit_kaa, kill_game, kill_dmm, kill_emulator, shutdown, hibernate
def _create_settings_tab(self) -> None:
with gr.Tab("设置"):
gr.Markdown("## 设置")
# 模拟器设置
emulator_settings = self._create_emulator_settings()
# 商店购买设置
purchase_settings = self._create_purchase_settings()
# 活动费设置
with gr.Column():
gr.Markdown("### 活动费设置")
activity_funds = gr.Checkbox(
label="启用收取活动费",
value=self.current_config.options.activity_funds.enabled,
info=ActivityFundsConfig.model_fields['enabled'].description
)
# 礼物设置
with gr.Column():
gr.Markdown("### 礼物设置")
presents = gr.Checkbox(
label="启用收取礼物",
value=self.current_config.options.presents.enabled,
info=PresentsConfig.model_fields['enabled'].description
)
# 工作设置
work_settings = self._create_work_settings()
# 竞赛设置
contest_settings = self._create_contest_settings()
# 培育设置
produce_settings = self._create_produce_settings()
# 任务奖励设置
with gr.Column():
gr.Markdown("### 任务奖励设置")
mission_reward = gr.Checkbox(
label="启用领取任务奖励",
value=self.current_config.options.mission_reward.enabled,
info=MissionRewardConfig.model_fields['enabled'].description
)
# 社团奖励设置
club_reward_settings = self._create_club_reward_settings()
# 升级支援卡设置
with gr.Column():
gr.Markdown("### 升级支援卡设置")
upgrade_support_card_enabled = gr.Checkbox(
label="启用升级支援卡",
value=self.current_config.options.upgrade_support_card.enabled,
info=UpgradeSupportCardConfig.model_fields['enabled'].description
)
capsule_toys_settings = self._create_capsule_toys_settings()
# 跟踪设置
with gr.Column():
gr.Markdown("### 跟踪设置")
gr.Markdown("跟踪功能会记录指定模块的运行情况,并保存截图。")
trace_recommend_card_detection = gr.Checkbox(
label="跟踪推荐卡检测",
value=self.current_config.options.trace.recommend_card_detection,
info=TraceConfig.model_fields['recommend_card_detection'].description,
interactive=True
)
# 启动游戏设置
start_game_settings = self._create_start_game_settings()
# 关闭游戏设置
end_game_settings = self._create_end_game_settings()
save_btn = gr.Button("保存设置")
result = gr.Markdown()
# 收集所有设置组件
all_settings = [
*emulator_settings,
trace_recommend_card_detection,
*purchase_settings,
activity_funds,
presents,
*work_settings,
*contest_settings,
*produce_settings,
mission_reward,
*club_reward_settings,
upgrade_support_card_enabled,
*capsule_toys_settings,
*start_game_settings,
*end_game_settings
]
save_btn.click(
fn=self.save_settings,
inputs=all_settings,
outputs=result
)
def _create_log_tab(self) -> None:
with gr.Tab("日志"):
gr.Markdown("## 日志")
with gr.Column():
with gr.Row():
export_dumps_btn = gr.Button("导出 dump")
export_logs_btn = gr.Button("导出日志")
with gr.Row():
save_report_btn = gr.Button("一键导出报告")
result_text = gr.Markdown("等待操作\n\n\n")
export_dumps_btn.click(
fn=self.export_dumps,
outputs=[result_text]
)
export_logs_btn.click(
fn=self.export_logs,
outputs=[result_text]
)
save_report_btn.click(
fn=partial(_save_bug_report, version=self._kaa.version),
outputs=[result_text]
)
def _create_whats_new_tab(self) -> None:
"""创建更新日志标签页,并显示最新版本更新内容"""
with gr.Tab("更新日志"):
from kotonebot.kaa.metadata import WHATS_NEW
gr.Markdown(WHATS_NEW)
def _create_screen_tab(self) -> None:
with gr.Tab("画面"):
gr.Markdown("## 当前设备画面")
WIDTH = 720 // 3
HEIGHT = 1280 // 3
last_update_text = gr.Markdown("上次更新时间:无数据")
screenshot_display = gr.Image(type="numpy", width=WIDTH, height=HEIGHT)
def update_screenshot():
ctx = ContextStackVars.current()
if ctx is None:
return [None, last_update_text.value]
screenshot = ctx._screenshot
if screenshot is None:
return [None, last_update_text.value]
screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2RGB)
return screenshot, f"上次更新时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
gr.Timer(0.3).tick(
fn=update_screenshot,
outputs=[screenshot_display, last_update_text]
)
def _load_config(self) -> None:
# 加载配置文件
config_path = "config.json"
self.config = load_config(config_path, type=BaseConfig, use_default_if_not_found=True)
if not self.config.user_configs:
# 如果没有用户配置,创建一个默认配置
default_config = UserConfig[BaseConfig](
name="默认配置",
category="default",
description="默认配置",
backend=BackendConfig(),
options=BaseConfig()
)
self.config.user_configs.append(default_config)
self.current_config = self.config.user_configs[0]
def create_ui(self) -> gr.Blocks:
with gr.Blocks(title="琴音小助手", css="#container { max-width: 800px; margin: auto; padding: 20px; }") as app:
with gr.Column(elem_id="container"):
gr.Markdown(f"# 琴音小助手 v{self._kaa.version}")
with gr.Tabs():
self._create_status_tab()
self._create_task_tab()
self._create_settings_tab()
self._create_log_tab()
self._create_whats_new_tab()
self._create_screen_tab()
return app
def main(kaa: Kaa | None = None) -> None:
kaa = kaa or Kaa()
ui = KotoneBotUI(kaa)
app = ui.create_ui()
app.launch(inbrowser=True, show_error=True)
if __name__ == "__main__":
main()