1914 lines
83 KiB
Python
1914 lines
83 KiB
Python
import os
|
||
import traceback
|
||
import zipfile
|
||
import logging
|
||
import copy
|
||
import sys
|
||
import subprocess
|
||
import json
|
||
from functools import partial
|
||
from itertools import chain
|
||
from datetime import datetime, timedelta
|
||
from typing import List, Dict, Tuple, Literal, Generator, Callable, Any, get_args, cast
|
||
|
||
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.backend.context.context import vars
|
||
from kotonebot.client.host import Mumu12Host, LeidianHost
|
||
from kotonebot.kaa.common import (
|
||
BaseConfig, APShopItems, CapsuleToysConfig, ClubRewardConfig, PurchaseConfig, ActivityFundsConfig,
|
||
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
|
||
MissionRewardConfig, DailyMoneyShopItems, ProduceAction,
|
||
RecommendCardDetectionMode, TraceConfig, StartGameConfig, EndGameConfig, UpgradeSupportCardConfig, MiscConfig,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
GradioInput = gr.Textbox | gr.Number | gr.Checkbox | gr.Dropdown | gr.Radio | gr.Slider | gr.Tabs | gr.Tab
|
||
ConfigKey = Literal[
|
||
# backend
|
||
'adb_ip', 'adb_port',
|
||
'screenshot_method', 'keep_screenshots',
|
||
'check_emulator', 'emulator_path',
|
||
'adb_emulator_name', 'emulator_args',
|
||
'_mumu_index', '_leidian_index',
|
||
'mumu_background_mode', 'target_screenshot_interval',
|
||
|
||
# purchase
|
||
'purchase_enabled',
|
||
'money_enabled', 'ap_enabled',
|
||
'ap_items', 'money_items', 'money_refresh',
|
||
|
||
# assignment
|
||
'assignment_enabled',
|
||
'mini_live_reassign', 'mini_live_duration',
|
||
'online_live_reassign', 'online_live_duration',
|
||
'contest_enabled',
|
||
'select_which_contestant',
|
||
|
||
# produce
|
||
'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',
|
||
'mission_reward_enabled',
|
||
|
||
# club reward
|
||
'club_reward_enabled',
|
||
'selected_note',
|
||
|
||
# upgrade support card
|
||
'upgrade_support_card_enabled',
|
||
|
||
# capsule toys
|
||
'capsule_toys_enabled', 'friend_capsule_toys_count',
|
||
'sense_capsule_toys_count', 'logic_capsule_toys_count',
|
||
'anomaly_capsule_toys_count',
|
||
|
||
# start game
|
||
'start_game_enabled', 'start_through_kuyo',
|
||
'game_package_name', 'kuyo_package_name',
|
||
'disable_gakumas_localify', 'dmm_game_path',
|
||
|
||
# end game
|
||
'exit_kaa', 'kill_game', 'kill_dmm',
|
||
'kill_emulator', 'shutdown', 'hibernate',
|
||
'restore_gakumas_localify',
|
||
|
||
'activity_funds',
|
||
'presents',
|
||
'mission_reward',
|
||
'activity_funds_enabled',
|
||
'presents_enabled',
|
||
'trace_recommend_card_detection',
|
||
|
||
# misc
|
||
'check_update', 'auto_install_update',
|
||
|
||
'_selected_backend_index'
|
||
|
||
]
|
||
CONFIG_KEY_VALUE: tuple[str] = get_args(ConfigKey)
|
||
ConfigSetFunction = Callable[[BaseConfig, Dict[ConfigKey, Any]], None]
|
||
ConfigBuilderReturnValue = Tuple[ConfigSetFunction, Dict[ConfigKey, GradioInput]]
|
||
|
||
def _save_bug_report(
|
||
title: str,
|
||
description: str,
|
||
version: str,
|
||
upload: bool,
|
||
path: str | None = None
|
||
) -> Generator[str, None, str]:
|
||
"""
|
||
保存报告
|
||
|
||
:param title: 标题
|
||
:param description: 描述
|
||
:param version: 版本号
|
||
:param upload: 是否上传
|
||
:param path: 保存的路径。若为 `None`,则保存到 `./reports/bug-YY-MM-DD HH-MM-SS_标题.zip`。
|
||
:return: 保存的路径
|
||
"""
|
||
from kotonebot import device
|
||
from kotonebot.backend.context import ContextStackVars
|
||
import re
|
||
|
||
# 过滤标题中的非法文件名字符
|
||
def sanitize_filename(s: str) -> str:
|
||
# 替换 \/:*?"<>| 为空或下划线
|
||
return re.sub(r'[\\/:*?"<>|]', '_', s)
|
||
|
||
# 确保目录存在
|
||
os.makedirs('logs', exist_ok=True)
|
||
os.makedirs('reports', exist_ok=True)
|
||
|
||
error = ""
|
||
if path is None:
|
||
safe_title = sanitize_filename(title)[:30] or "无标题"
|
||
timestamp = datetime.now().strftime("%y-%m-%d-%H-%M-%S")
|
||
path = f'./reports/bug_{timestamp}_{safe_title}.zip'
|
||
with zipfile.ZipFile(path, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
|
||
# 打包描述文件
|
||
yield "### 打包描述文件..."
|
||
try:
|
||
description_content = f"标题:{title}\n类型:bug\n内容:\n{description}"
|
||
zipf.writestr('description.txt', description_content.encode('utf-8'))
|
||
except Exception as e:
|
||
error += f"保存描述文件失败:{str(e)}\n"
|
||
|
||
# 打包截图
|
||
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)
|
||
|
||
if not upload:
|
||
yield f"### 报告已保存至 {os.path.abspath(path)}"
|
||
return path
|
||
|
||
# 上传报告
|
||
from kotonebot.ui.file_host.sensio import upload as upload_file
|
||
yield "### 上传报告..."
|
||
url = ''
|
||
try:
|
||
url = upload_file(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 += "### 但发生了以下错误\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.is_stopping: bool = False # 新增:标记是否正在停止过程中
|
||
self.is_single_task_stopping: 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) -> Tuple[str, bool]:
|
||
"""获取按钮状态和交互性"""
|
||
if not hasattr(self, 'run_status'):
|
||
return "启动", True
|
||
|
||
if not self.run_status.running:
|
||
self.is_running = False
|
||
self.is_stopping = False # 重置停止状态
|
||
return "启动", True
|
||
|
||
if self.is_stopping:
|
||
return "停止中...", False # 停止中时禁用按钮
|
||
|
||
return "停止", True
|
||
|
||
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()
|
||
|
||
# 如果正在停止过程中,忽略重复点击
|
||
if self.is_stopping:
|
||
return "停止中...", self.update_task_status()
|
||
|
||
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_stopping = True # 设置停止状态
|
||
|
||
# 如果当前处于暂停状态,先恢复再停止
|
||
if vars.flow.is_paused:
|
||
gr.Info("检测到任务暂停,正在恢复后停止...")
|
||
vars.flow.request_resume()
|
||
|
||
self.is_running = False
|
||
if self._kaa:
|
||
self.run_status.interrupt()
|
||
gr.Info("正在停止任务...")
|
||
|
||
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.is_single_task_stopping = True # 设置单个任务停止状态
|
||
|
||
# 如果当前处于暂停状态,先恢复再停止
|
||
if vars.flow.is_paused:
|
||
gr.Info("检测到任务暂停,正在恢复后停止...")
|
||
vars.flow.request_resume()
|
||
|
||
self.single_task_running = False
|
||
if hasattr(self, 'run_status') and self._kaa:
|
||
self.run_status.interrupt()
|
||
gr.Info("正在停止任务...")
|
||
return "停止中...", "正在停止任务..."
|
||
|
||
def toggle_pause(self) -> str:
|
||
"""切换暂停/恢复状态"""
|
||
if vars.flow.is_paused:
|
||
vars.flow.request_resume()
|
||
gr.Info("任务已恢复")
|
||
return "暂停"
|
||
else:
|
||
vars.flow.request_pause()
|
||
gr.Info("任务已暂停")
|
||
return "恢复"
|
||
|
||
def get_pause_button_status(self) -> str:
|
||
"""获取暂停按钮的状态"""
|
||
if vars.flow.is_paused:
|
||
return "恢复"
|
||
else:
|
||
return "暂停"
|
||
|
||
def get_pause_button_with_interactive(self) -> gr.Button:
|
||
"""获取暂停按钮的状态和交互性"""
|
||
try:
|
||
text = "恢复" if vars.flow.is_paused else "暂停"
|
||
except ValueError:
|
||
# ValueError: Forwarded object vars called before initialization.
|
||
# TODO: vars.flow.is_paused 应该要可以在脚本正式启动前就能访问
|
||
text = '未启动'
|
||
# 如果正在停止过程中,禁用暂停按钮
|
||
interactive = not (self.is_stopping or self.is_single_task_stopping)
|
||
return gr.Button(value=text, interactive=interactive)
|
||
|
||
def save_settings2(self, return_values: list[ConfigBuilderReturnValue], *args) -> str:
|
||
options = BaseConfig()
|
||
# return_values: (set_func, { 'key': component })
|
||
keys = list(chain.from_iterable([list(data.keys()) for _, data in return_values]))
|
||
# 根据 keys 与 *args 构建 data 字典
|
||
data = dict[ConfigKey, Any]()
|
||
assert len(keys) == len(args), "keys 与 args 长度不一致"
|
||
for key, value in zip(keys, args):
|
||
assert key in CONFIG_KEY_VALUE, f"未知的配置项:{key}"
|
||
key = cast(ConfigKey, key)
|
||
data[key] = value
|
||
|
||
# 先设置options
|
||
for (set_func, _) in return_values:
|
||
set_func(options, data)
|
||
|
||
# 验证规则1:截图方法验证
|
||
screenshot_method = self.current_config.backend.screenshot_impl
|
||
backend_type = self.current_config.backend.type
|
||
|
||
valid_screenshot_methods = {
|
||
'mumu12': ['adb', 'adb_raw', 'uiautomator2', 'nemu_ipc'],
|
||
'leidian': ['adb', 'adb_raw', 'uiautomator2'],
|
||
'custom': ['adb', 'adb_raw', 'uiautomator2'],
|
||
'dmm': ['remote_windows', 'windows']
|
||
}
|
||
|
||
if screenshot_method not in valid_screenshot_methods.get(backend_type, []):
|
||
gr.Warning(f"截图方法 '{screenshot_method}' 不适用于当前选择的模拟器类型,配置未保存。")
|
||
return ""
|
||
|
||
# 验证规则2:若启用培育,那么培育偶像不能为空
|
||
if options.produce.enabled and not options.produce.idols:
|
||
gr.Warning("启用培育时,培育偶像不能为空,配置未保存。")
|
||
return ""
|
||
|
||
# 验证规则3:若启用AP/金币购买,对应的商品不能为空
|
||
if options.purchase.ap_enabled and not options.purchase.ap_items:
|
||
gr.Warning("启用AP购买时,AP商店购买物品不能为空,配置未保存。")
|
||
return ""
|
||
|
||
if options.purchase.money_enabled and not options.purchase.money_items:
|
||
gr.Warning("启用金币购买时,金币商店购买物品不能为空,配置未保存。")
|
||
return ""
|
||
|
||
# 验证通过,保存配置
|
||
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=2)
|
||
pause_btn = gr.Button("暂停", scale=1)
|
||
if self._kaa.upgrade_msg:
|
||
gr.Markdown('### 配置升级报告')
|
||
gr.Markdown(self._kaa.upgrade_msg)
|
||
gr.Markdown('脚本报错或者卡住?前往"反馈"选项卡可以快速导出报告!')
|
||
|
||
# 添加调试模式警告
|
||
if self.current_config.keep_screenshots:
|
||
gr.Markdown(
|
||
'<div style="color: red; font-size: larger;">当前启用了调试功能「保留截图数据」,调试结束后正常使用时建议关闭此选项!</div>',
|
||
elem_classes=["debug-warning"]
|
||
)
|
||
|
||
task_status = gr.Dataframe(
|
||
headers=["任务", "状态"],
|
||
value=self.update_task_status(),
|
||
label="任务状态"
|
||
)
|
||
|
||
def on_run_click(evt: gr.EventData) -> Tuple[gr.Button, List[List[str]]]:
|
||
result = self.toggle_run()
|
||
# 如果正在停止,禁用按钮
|
||
interactive = not self.is_stopping
|
||
button = gr.Button(value=result[0], interactive=interactive)
|
||
return button, result[1]
|
||
|
||
def on_pause_click(evt: gr.EventData) -> str:
|
||
return self.toggle_pause()
|
||
|
||
run_btn.click(
|
||
fn=on_run_click,
|
||
outputs=[run_btn, task_status]
|
||
)
|
||
|
||
pause_btn.click(
|
||
fn=on_pause_click,
|
||
outputs=[pause_btn]
|
||
)
|
||
|
||
# 添加定时器,分别更新按钮状态和任务状态
|
||
def update_run_button_status():
|
||
text, interactive = self.get_button_status()
|
||
return gr.Button(value=text, interactive=interactive)
|
||
|
||
gr.Timer(1.0).tick(
|
||
fn=update_run_button_status,
|
||
outputs=[run_btn]
|
||
)
|
||
gr.Timer(1.0).tick(
|
||
fn=self.get_pause_button_with_interactive,
|
||
outputs=[pause_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
|
||
)
|
||
|
||
# 创建执行按钮和暂停按钮
|
||
with gr.Row():
|
||
execute_btn = gr.Button("执行任务", scale=2)
|
||
pause_btn = gr.Button("暂停", scale=1)
|
||
task_result = gr.Markdown("")
|
||
|
||
def toggle_single_task(task_name: str) -> Tuple[gr.Button, str]:
|
||
if self.single_task_running:
|
||
# 如果正在停止过程中,忽略重复点击
|
||
if self.is_single_task_stopping:
|
||
return gr.Button(value="停止中...", interactive=False), "正在停止任务..."
|
||
|
||
result = self.stop_single_task()
|
||
return gr.Button(value=result[0], interactive=False), result[1]
|
||
else:
|
||
result = self.start_single_task(task_name)
|
||
return gr.Button(value=result[0], interactive=True), result[1]
|
||
|
||
def get_task_button_status() -> gr.Button:
|
||
if not hasattr(self, 'run_status') or not self.run_status.running:
|
||
self.single_task_running = False
|
||
self.is_single_task_stopping = False # 重置停止状态
|
||
return gr.Button(value="执行任务", interactive=True)
|
||
|
||
if self.is_single_task_stopping:
|
||
return gr.Button(value="停止中...", interactive=False) # 停止中时禁用按钮
|
||
|
||
return gr.Button(value="停止任务", interactive=True)
|
||
|
||
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 ""
|
||
|
||
def on_pause_click(evt: gr.EventData) -> str:
|
||
return self.toggle_pause()
|
||
|
||
execute_btn.click(
|
||
fn=toggle_single_task,
|
||
inputs=[task_dropdown],
|
||
outputs=[execute_btn, task_result]
|
||
)
|
||
|
||
pause_btn.click(
|
||
fn=on_pause_click,
|
||
outputs=[pause_btn]
|
||
)
|
||
|
||
# 添加定时器更新按钮状态和任务状态
|
||
gr.Timer(1.0).tick(
|
||
fn=get_task_button_status,
|
||
outputs=[execute_btn]
|
||
)
|
||
gr.Timer(1.0).tick(
|
||
fn=self.get_pause_button_with_interactive,
|
||
outputs=[pause_btn]
|
||
)
|
||
gr.Timer(1.0).tick(
|
||
fn=get_single_task_status,
|
||
outputs=[task_result]
|
||
)
|
||
|
||
def _create_emulator_settings(self) -> ConfigBuilderReturnValue:
|
||
gr.Markdown("### 模拟器设置")
|
||
has_mumu12 = Mumu12Host.installed()
|
||
has_leidian = LeidianHost.installed()
|
||
current_tab = 0
|
||
|
||
def _update_emulator_tab_options(impl_value: str, selected_index: int):
|
||
nonlocal current_tab
|
||
current_tab = selected_index
|
||
|
||
# if selected_index == 3: # DMM
|
||
# choices = ['windows', 'remote_windows']
|
||
# else: # Mumu, Leidian, Custom
|
||
# choices = ['adb', 'adb_raw', 'uiautomator2']
|
||
# else:
|
||
# raise ValueError(f'Unsupported backend type: {type_in_config}')
|
||
choices = ['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
||
if impl_value not in choices:
|
||
new_value = choices[0]
|
||
else:
|
||
new_value = impl_value
|
||
|
||
return gr.Dropdown(choices=choices, value=new_value)
|
||
|
||
with gr.Tabs(selected=self.current_config.backend.type):
|
||
with gr.Tab("MuMu 12", interactive=has_mumu12, id="mumu12") as tab_mumu12:
|
||
gr.Markdown("已选中 MuMu 12 模拟器")
|
||
if has_mumu12:
|
||
try:
|
||
instances = Mumu12Host.list()
|
||
is_mumu12 = self.current_config.backend.type == 'mumu12'
|
||
mumu_instance = gr.Dropdown(
|
||
label="选择多开实例",
|
||
value=self.current_config.backend.instance_id if is_mumu12 else None,
|
||
choices=[(i.name, i.id) for i in instances],
|
||
interactive=True
|
||
)
|
||
mumu_background_mode = gr.Checkbox(
|
||
label="MuMu12 模拟器后台保活模式",
|
||
value=self.current_config.backend.mumu_background_mode,
|
||
info=BackendConfig.model_fields['mumu_background_mode'].description,
|
||
interactive=True
|
||
)
|
||
except: # noqa: E722
|
||
logger.exception('Failed to list installed MuMu12')
|
||
gr.Markdown('获取 MuMu12 模拟器列表失败,请升级模拟器到最新版本。若问题依旧,前往 QQ 群、QQ 频道或 Github 反馈 bug。')
|
||
mumu_instance = gr.Dropdown(visible=False)
|
||
mumu_background_mode = gr.Checkbox(visible=False)
|
||
else:
|
||
# 为了让 return 收集组件时不报错
|
||
mumu_instance = gr.Dropdown(visible=False)
|
||
mumu_background_mode = gr.Checkbox(visible=False)
|
||
|
||
with gr.Tab("雷电", interactive=has_leidian, id="leidian") as tab_leidian:
|
||
gr.Markdown("已选中雷电模拟器")
|
||
if has_leidian:
|
||
try:
|
||
instances = LeidianHost.list()
|
||
is_leidian = self.current_config.backend.type == 'leidian'
|
||
leidian_instance = gr.Dropdown(
|
||
label="选择多开实例",
|
||
value=self.current_config.backend.instance_id if is_leidian else None,
|
||
choices=[(i.name, i.id) for i in instances],
|
||
interactive=True
|
||
)
|
||
except: # noqa: E722
|
||
logger.exception('Failed to list installed Leidian')
|
||
gr.Markdown('获取雷电模拟器列表失败,请前往 QQ 群、QQ 频道或 Github 反馈 bug。')
|
||
leidian_instance = gr.Dropdown(visible=False)
|
||
else:
|
||
leidian_instance = gr.Dropdown(visible=False)
|
||
|
||
with gr.Tab("自定义", id="custom") as tab_custom:
|
||
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
|
||
)
|
||
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]
|
||
)
|
||
|
||
with gr.Tab("DMM", id="dmm") as tab_dmm:
|
||
gr.Markdown("已选中 DMM")
|
||
|
||
# type_in_config = self.current_config.backend.type
|
||
# if type_in_config in ['dmm']:
|
||
# choices = ['windows', 'remote_windows']
|
||
# elif type_in_config in ['mumu12', 'leidian', 'custom']:
|
||
# choices = ['adb', 'adb_raw', 'uiautomator2']
|
||
# else:
|
||
# raise ValueError(f'Unsupported backend type: {type_in_config}')
|
||
choices = ['adb', 'adb_raw', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
||
screenshot_impl = gr.Dropdown(
|
||
choices=choices,
|
||
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
|
||
)
|
||
|
||
target_screenshot_interval = gr.Number(
|
||
label="最小截图间隔(秒)",
|
||
value=self.current_config.backend.target_screenshot_interval,
|
||
info=BackendConfig.model_fields['target_screenshot_interval'].description,
|
||
minimum=0,
|
||
step=0.1,
|
||
interactive=True
|
||
)
|
||
|
||
tab_mumu12.select(fn=partial(_update_emulator_tab_options, selected_index=0), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||
tab_leidian.select(fn=partial(_update_emulator_tab_options, selected_index=1), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||
tab_custom.select(fn=partial(_update_emulator_tab_options, selected_index=2), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||
tab_dmm.select(fn=partial(_update_emulator_tab_options, selected_index=3), inputs=[screenshot_impl], outputs=[screenshot_impl])
|
||
|
||
# 初值
|
||
if self.current_config.backend.type == 'mumu12':
|
||
_update_emulator_tab_options(
|
||
impl_value=self.current_config.backend.screenshot_impl,
|
||
selected_index=0
|
||
)
|
||
elif self.current_config.backend.type == 'leidian':
|
||
_update_emulator_tab_options(
|
||
impl_value=self.current_config.backend.screenshot_impl,
|
||
selected_index=1
|
||
)
|
||
elif self.current_config.backend.type == 'custom':
|
||
_update_emulator_tab_options(
|
||
impl_value=self.current_config.backend.screenshot_impl,
|
||
selected_index=2
|
||
)
|
||
elif self.current_config.backend.type == 'dmm':
|
||
_update_emulator_tab_options(
|
||
impl_value=self.current_config.backend.screenshot_impl,
|
||
selected_index=3
|
||
)
|
||
else:
|
||
gr.Warning(f"未知的模拟器类型:{self.current_config.backend.type}")
|
||
|
||
def set_config(_: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
# current_tab is updated by _update_emulator_tab_options
|
||
if current_tab == 0: # Mumu
|
||
self.current_config.backend.type = 'mumu12'
|
||
self.current_config.backend.instance_id = data['_mumu_index']
|
||
self.current_config.backend.mumu_background_mode = data['mumu_background_mode']
|
||
elif current_tab == 1: # Leidian
|
||
self.current_config.backend.type = 'leidian'
|
||
self.current_config.backend.instance_id = data['_leidian_index']
|
||
elif current_tab == 2: # Custom
|
||
self.current_config.backend.type = 'custom'
|
||
self.current_config.backend.instance_id = None
|
||
self.current_config.backend.adb_ip = data['adb_ip']
|
||
self.current_config.backend.adb_port = data['adb_port']
|
||
self.current_config.backend.adb_emulator_name = data['adb_emulator_name']
|
||
self.current_config.backend.check_emulator = data['check_emulator']
|
||
self.current_config.backend.emulator_path = data['emulator_path']
|
||
self.current_config.backend.emulator_args = data['emulator_args']
|
||
elif current_tab == 3: # DMM
|
||
self.current_config.backend.type = 'dmm'
|
||
self.current_config.backend.instance_id = None # DMM doesn't use instance_id here
|
||
|
||
# Common settings for all backend types
|
||
self.current_config.backend.screenshot_impl = data['screenshot_method']
|
||
self.current_config.backend.target_screenshot_interval = data['target_screenshot_interval']
|
||
self.current_config.keep_screenshots = data['keep_screenshots'] # This is a UserConfig field
|
||
|
||
return set_config, {
|
||
'adb_ip': adb_ip,
|
||
'adb_port': adb_port,
|
||
'screenshot_method': screenshot_impl, # screenshot_impl is the component
|
||
'target_screenshot_interval': target_screenshot_interval,
|
||
'keep_screenshots': keep_screenshots,
|
||
'check_emulator': check_emulator,
|
||
'emulator_path': emulator_path,
|
||
'adb_emulator_name': adb_emulator_name,
|
||
'emulator_args': emulator_args,
|
||
'_mumu_index': mumu_instance,
|
||
'_leidian_index': leidian_instance,
|
||
'mumu_background_mode': mumu_background_mode
|
||
}
|
||
|
||
def _create_purchase_settings(self) -> ConfigBuilderReturnValue:
|
||
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
|
||
)
|
||
|
||
money_refresh = gr.Checkbox(
|
||
label="每日一次免费刷新金币商店",
|
||
value=self.current_config.options.purchase.money_refresh,
|
||
info=PurchaseConfig.model_fields['money_refresh'].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]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.purchase.enabled = data['purchase_enabled']
|
||
config.purchase.money_enabled = data['money_enabled']
|
||
config.purchase.money_items = [DailyMoneyShopItems(x) for x in data['money_items']]
|
||
config.purchase.money_refresh = data['money_refresh']
|
||
config.purchase.ap_enabled = data['ap_enabled']
|
||
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 data['ap_items']:
|
||
if item in ap_items_map:
|
||
ap_items_enum.append(ap_items_map[item].value) # type: ignore
|
||
config.purchase.ap_items = ap_items_enum
|
||
|
||
return set_config, {
|
||
'purchase_enabled': purchase_enabled,
|
||
'money_enabled': money_enabled,
|
||
'ap_enabled': ap_enabled,
|
||
'ap_items': ap_items,
|
||
'money_items': money_items,
|
||
'money_refresh': money_refresh
|
||
}
|
||
|
||
def _create_work_settings(self) -> ConfigBuilderReturnValue:
|
||
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 set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.assignment.enabled = data['assignment_enabled']
|
||
config.assignment.mini_live_reassign_enabled = data['mini_live_reassign']
|
||
config.assignment.mini_live_duration = data['mini_live_duration']
|
||
config.assignment.online_live_reassign_enabled = data['online_live_reassign']
|
||
config.assignment.online_live_duration = data['online_live_duration']
|
||
|
||
return set_config, {
|
||
'assignment_enabled': assignment_enabled,
|
||
'mini_live_reassign': mini_live_reassign,
|
||
'mini_live_duration': mini_live_duration,
|
||
'online_live_reassign': online_live_reassign,
|
||
'online_live_duration': online_live_duration
|
||
}
|
||
|
||
|
||
def _create_contest_settings(self) -> ConfigBuilderReturnValue:
|
||
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]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.contest.enabled = data['contest_enabled']
|
||
config.contest.select_which_contestant = data['select_which_contestant']
|
||
|
||
return set_config, {
|
||
'contest_enabled': contest_enabled,
|
||
'select_which_contestant': select_which_contestant
|
||
}
|
||
|
||
def _create_produce_settings(self) -> ConfigBuilderReturnValue:
|
||
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]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.produce.enabled = data['produce_enabled']
|
||
config.produce.mode = data['produce_mode']
|
||
config.produce.produce_count = data['produce_count']
|
||
config.produce.idols = data['produce_idols']
|
||
config.produce.memory_sets = [int(i) for i in data['memory_sets']]
|
||
config.produce.auto_set_memory = data['auto_set_memory']
|
||
config.produce.auto_set_support_card = data['auto_set_support']
|
||
config.produce.use_pt_boost = data['use_pt_boost']
|
||
config.produce.use_note_boost = data['use_note_boost']
|
||
config.produce.follow_producer = data['follow_producer']
|
||
config.produce.self_study_lesson = data['self_study_lesson']
|
||
config.produce.prefer_lesson_ap = data['prefer_lesson_ap']
|
||
config.produce.actions_order = [ProduceAction(action) for action in data['actions_order']]
|
||
config.produce.recommend_card_detection_mode = RecommendCardDetectionMode(data['recommend_card_detection_mode'])
|
||
config.produce.use_ap_drink = data['use_ap_drink']
|
||
config.produce.skip_commu = data['skip_commu']
|
||
|
||
return set_config, {
|
||
'produce_enabled': produce_enabled,
|
||
'produce_mode': produce_mode,
|
||
'produce_count': produce_count,
|
||
'produce_idols': produce_idols,
|
||
'memory_sets': memory_sets,
|
||
'auto_set_memory': auto_set_memory,
|
||
'auto_set_support': 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': actions_order,
|
||
'recommend_card_detection_mode': recommend_card_detection_mode,
|
||
'use_ap_drink': use_ap_drink,
|
||
'skip_commu': skip_commu
|
||
}
|
||
|
||
def _create_club_reward_settings(self) -> ConfigBuilderReturnValue:
|
||
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]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.club_reward.enabled = data['club_reward_enabled']
|
||
config.club_reward.selected_note = DailyMoneyShopItems(data['selected_note'])
|
||
|
||
return set_config, {
|
||
'club_reward_enabled': club_reward_enabled,
|
||
'selected_note': selected_note
|
||
}
|
||
|
||
def _create_capsule_toys_settings(self) -> ConfigBuilderReturnValue:
|
||
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]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.capsule_toys.enabled = data['capsule_toys_enabled']
|
||
config.capsule_toys.friend_capsule_toys_count = data['friend_capsule_toys_count']
|
||
config.capsule_toys.sense_capsule_toys_count = data['sense_capsule_toys_count']
|
||
config.capsule_toys.logic_capsule_toys_count = data['logic_capsule_toys_count']
|
||
config.capsule_toys.anomaly_capsule_toys_count = data['anomaly_capsule_toys_count']
|
||
|
||
return set_config, {
|
||
'capsule_toys_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
|
||
}
|
||
|
||
|
||
def _create_start_game_settings(self) -> ConfigBuilderReturnValue:
|
||
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
|
||
)
|
||
disable_gakumas_localify = gr.Checkbox(
|
||
label="禁用 Gakumas Localify 汉化插件",
|
||
value=self.current_config.options.start_game.disable_gakumas_localify,
|
||
info=StartGameConfig.model_fields['disable_gakumas_localify'].description
|
||
)
|
||
dmm_game_path = gr.Textbox(
|
||
value=self.current_config.options.start_game.dmm_game_path or "",
|
||
label="DMM 版游戏路径",
|
||
info=StartGameConfig.model_fields['dmm_game_path'].description,
|
||
placeholder="例:F:\\Games\\gakumas\\gakumas.exe"
|
||
)
|
||
start_game_enabled.change(
|
||
fn=lambda x: gr.Group(visible=x),
|
||
inputs=[start_game_enabled],
|
||
outputs=[start_game_group]
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.start_game.enabled = data['start_game_enabled']
|
||
config.start_game.start_through_kuyo = data['start_through_kuyo']
|
||
config.start_game.game_package_name = data['game_package_name']
|
||
config.start_game.kuyo_package_name = data['kuyo_package_name']
|
||
config.start_game.disable_gakumas_localify = data['disable_gakumas_localify']
|
||
config.start_game.dmm_game_path = data['dmm_game_path'] if data['dmm_game_path'] else None
|
||
|
||
return set_config, {
|
||
'start_game_enabled': start_game_enabled,
|
||
'start_through_kuyo': start_through_kuyo,
|
||
'game_package_name': game_package_name,
|
||
'kuyo_package_name': kuyo_package_name,
|
||
'disable_gakumas_localify': disable_gakumas_localify,
|
||
'dmm_game_path': dmm_game_path
|
||
}
|
||
|
||
|
||
def _create_end_game_settings(self) -> ConfigBuilderReturnValue:
|
||
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
|
||
)
|
||
restore_gakumas_localify = gr.Checkbox(
|
||
label="恢复 Gakumas Localify 汉化插件状态",
|
||
value=self.current_config.options.end_game.restore_gakumas_localify,
|
||
info=EndGameConfig.model_fields['restore_gakumas_localify'].description
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.end_game.exit_kaa = data['exit_kaa']
|
||
config.end_game.kill_game = data['kill_game']
|
||
config.end_game.kill_dmm = data['kill_dmm']
|
||
config.end_game.kill_emulator = data['kill_emulator']
|
||
config.end_game.shutdown = data['shutdown']
|
||
config.end_game.hibernate = data['hibernate']
|
||
config.end_game.restore_gakumas_localify = data['restore_gakumas_localify']
|
||
|
||
return set_config, {
|
||
'exit_kaa': exit_kaa,
|
||
'kill_game': kill_game,
|
||
'kill_dmm': kill_dmm,
|
||
'kill_emulator': kill_emulator,
|
||
'shutdown': shutdown,
|
||
'hibernate': hibernate,
|
||
'restore_gakumas_localify': restore_gakumas_localify
|
||
}
|
||
|
||
def _create_activity_funds_settings(self) -> ConfigBuilderReturnValue:
|
||
with gr.Column():
|
||
gr.Markdown("### 活动费设置")
|
||
activity_funds = gr.Checkbox(
|
||
label="启用收取活动费",
|
||
value=self.current_config.options.activity_funds.enabled,
|
||
info=ActivityFundsConfig.model_fields['enabled'].description
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.activity_funds.enabled = data['activity_funds']
|
||
|
||
return set_config, {
|
||
'activity_funds': activity_funds
|
||
}
|
||
|
||
def _create_presents_settings(self) -> ConfigBuilderReturnValue:
|
||
with gr.Column():
|
||
gr.Markdown("### 礼物设置")
|
||
presents = gr.Checkbox(
|
||
label="启用收取礼物",
|
||
value=self.current_config.options.presents.enabled,
|
||
info=PresentsConfig.model_fields['enabled'].description
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.presents.enabled = data['presents']
|
||
|
||
return set_config, {
|
||
'presents': presents
|
||
}
|
||
|
||
def _create_mission_reward_settings(self) -> ConfigBuilderReturnValue:
|
||
with gr.Column():
|
||
gr.Markdown("### 任务奖励设置")
|
||
mission_reward = gr.Checkbox(
|
||
label="启用领取任务奖励",
|
||
value=self.current_config.options.mission_reward.enabled,
|
||
info=MissionRewardConfig.model_fields['enabled'].description
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.mission_reward.enabled = data['mission_reward']
|
||
|
||
return set_config, {
|
||
'mission_reward': mission_reward
|
||
}
|
||
|
||
def _create_upgrade_support_card_settings(self) -> ConfigBuilderReturnValue:
|
||
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
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.upgrade_support_card.enabled = data['upgrade_support_card_enabled']
|
||
|
||
return set_config, {
|
||
'upgrade_support_card_enabled': upgrade_support_card_enabled
|
||
}
|
||
|
||
def _create_trace_settings(self) -> ConfigBuilderReturnValue:
|
||
with gr.Column():
|
||
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
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.trace.recommend_card_detection = data['trace_recommend_card_detection']
|
||
|
||
return set_config, {
|
||
'trace_recommend_card_detection': trace_recommend_card_detection
|
||
}
|
||
|
||
def _create_misc_settings(self) -> ConfigBuilderReturnValue:
|
||
with gr.Column():
|
||
gr.Markdown("### 杂项设置")
|
||
check_update = gr.Dropdown(
|
||
choices=[
|
||
("启动时检查更新", "startup"),
|
||
("从不检查更新", "never")
|
||
],
|
||
value=self.current_config.options.misc.check_update,
|
||
label="检查更新时机",
|
||
info=MiscConfig.model_fields['check_update'].description,
|
||
interactive=True
|
||
)
|
||
auto_install_update = gr.Checkbox(
|
||
label="自动安装更新",
|
||
value=self.current_config.options.misc.auto_install_update,
|
||
info=MiscConfig.model_fields['auto_install_update'].description,
|
||
interactive=True
|
||
)
|
||
|
||
def set_config(config: BaseConfig, data: dict[ConfigKey, Any]) -> None:
|
||
config.misc.check_update = data['check_update']
|
||
config.misc.auto_install_update = data['auto_install_update']
|
||
|
||
return set_config, {
|
||
'check_update': check_update,
|
||
'auto_install_update': auto_install_update
|
||
}
|
||
|
||
def _create_settings_tab(self) -> None:
|
||
with gr.Tab("设置"):
|
||
gr.Markdown("## 设置")
|
||
|
||
# 模拟器设置
|
||
emulator_settings = self._create_emulator_settings()
|
||
|
||
# 商店购买设置
|
||
purchase_settings = self._create_purchase_settings()
|
||
|
||
# 活动费设置
|
||
activity_funds_settings = self._create_activity_funds_settings()
|
||
|
||
# 礼物设置
|
||
presents_settings = self._create_presents_settings()
|
||
|
||
# 工作设置
|
||
work_settings = self._create_work_settings()
|
||
|
||
# 竞赛设置
|
||
contest_settings = self._create_contest_settings()
|
||
|
||
# 培育设置
|
||
produce_settings = self._create_produce_settings()
|
||
|
||
# 任务奖励设置
|
||
mission_reward_settings = self._create_mission_reward_settings()
|
||
|
||
# 社团奖励设置
|
||
club_reward_settings = self._create_club_reward_settings()
|
||
|
||
# 升级支援卡设置
|
||
capsule_toys_settings = self._create_capsule_toys_settings()
|
||
|
||
# 跟踪设置
|
||
trace_settings = self._create_trace_settings()
|
||
|
||
# 杂项设置
|
||
misc_settings = self._create_misc_settings()
|
||
|
||
# 启动游戏设置
|
||
start_game_settings = self._create_start_game_settings()
|
||
|
||
# 关闭游戏设置
|
||
end_game_settings = self._create_end_game_settings()
|
||
|
||
save_btn = gr.Button("保存设置")
|
||
result = gr.Markdown()
|
||
|
||
# 收集所有设置组件
|
||
all_return_values = [
|
||
emulator_settings,
|
||
purchase_settings,
|
||
activity_funds_settings,
|
||
presents_settings,
|
||
work_settings,
|
||
contest_settings,
|
||
produce_settings,
|
||
mission_reward_settings,
|
||
club_reward_settings,
|
||
capsule_toys_settings,
|
||
start_game_settings,
|
||
end_game_settings,
|
||
trace_settings,
|
||
misc_settings
|
||
] # list of (set_func, { 'key': component, ... })
|
||
all_components = [list(ret[1].values()) for ret in all_return_values] # [[c1, c2], [c3], ...]
|
||
all_components = list(chain(*all_components)) # [c1, c2, c3, ...]
|
||
save_btn.click(
|
||
fn=partial(self.save_settings2, all_return_values),
|
||
inputs=all_components,
|
||
outputs=result
|
||
)
|
||
|
||
def _create_log_tab(self) -> None:
|
||
with gr.Tab("反馈"):
|
||
gr.Markdown("## 反馈")
|
||
gr.Markdown('脚本报错或者卡住?在这里填写信息可以快速反馈!')
|
||
with gr.Column():
|
||
report_title = gr.Textbox(label="标题", placeholder="用一句话概括问题")
|
||
report_type = gr.Dropdown(label="反馈类型", choices=["bug"], value="bug", interactive=False)
|
||
report_description = gr.Textbox(label="描述", lines=5, placeholder="详细描述问题。例如:什么时候出错、是否每次都出错、出错时的步骤是什么")
|
||
with gr.Row():
|
||
upload_report_btn = gr.Button("上传")
|
||
save_local_report_btn = gr.Button("保存至本地")
|
||
|
||
result_text = gr.Markdown("等待操作\n\n\n")
|
||
|
||
def on_upload_click(title: str, description: str):
|
||
yield from _save_bug_report(title, description, self._kaa.version, upload=True)
|
||
|
||
def on_save_local_click(title: str, description: str):
|
||
yield from _save_bug_report(title, description, self._kaa.version, upload=False)
|
||
|
||
upload_report_btn.click(
|
||
fn=on_upload_click,
|
||
inputs=[report_title, report_description],
|
||
outputs=[result_text]
|
||
)
|
||
save_local_report_btn.click(
|
||
fn=on_save_local_click,
|
||
inputs=[report_title, report_description],
|
||
outputs=[result_text]
|
||
)
|
||
|
||
def _create_whats_new_tab(self) -> None:
|
||
"""创建更新标签页"""
|
||
with gr.Tab("更新"):
|
||
gr.Markdown("## 版本管理")
|
||
|
||
# 更新日志
|
||
with gr.Accordion("更新日志", open=False):
|
||
from kotonebot.kaa.metadata import WHATS_NEW
|
||
gr.Markdown(WHATS_NEW)
|
||
|
||
# 载入信息按钮
|
||
load_info_btn = gr.Button("载入信息", variant="primary")
|
||
|
||
# 状态信息
|
||
status_text = gr.Markdown("")
|
||
|
||
# 版本选择下拉框(用于安装)
|
||
version_dropdown = gr.Dropdown(
|
||
label="选择要安装的版本",
|
||
choices=[],
|
||
value=None,
|
||
visible=False,
|
||
interactive=True
|
||
)
|
||
|
||
# 安装选定版本按钮
|
||
install_selected_btn = gr.Button("安装选定版本", visible=False)
|
||
|
||
def list_all_versions():
|
||
"""列出所有可用版本"""
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
try:
|
||
# 构建命令,使用清华镜像源
|
||
cmd = [
|
||
sys.executable, "-m", "pip", "index", "versions", "ksaa", "--json",
|
||
"--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",
|
||
"--trusted-host", "mirrors.tuna.tsinghua.edu.cn"
|
||
]
|
||
logger.info(f"执行命令: {' '.join(cmd)}")
|
||
|
||
# 使用 pip index versions --json 来获取版本信息
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30
|
||
)
|
||
|
||
logger.info(f"命令返回码: {result.returncode}")
|
||
if result.stdout:
|
||
logger.info(f"命令输出: {result.stdout[:500]}...") # 只记录前500字符
|
||
if result.stderr:
|
||
logger.warning(f"命令错误输出: {result.stderr}")
|
||
|
||
if result.returncode != 0:
|
||
error_msg = f"获取版本列表失败: {result.stderr}"
|
||
logger.error(error_msg)
|
||
return (
|
||
error_msg,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
|
||
# 解析 JSON 输出
|
||
try:
|
||
data = json.loads(result.stdout)
|
||
versions = data.get("versions", [])
|
||
latest_version = data.get("latest", "")
|
||
installed_version = data.get("installed_version", "")
|
||
|
||
logger.info(f"解析到 {len(versions)} 个版本")
|
||
logger.info(f"最新版本: {latest_version}")
|
||
logger.info(f"已安装版本: {installed_version}")
|
||
|
||
except json.JSONDecodeError as e:
|
||
error_msg = f"解析版本信息失败: {str(e)}"
|
||
logger.error(error_msg)
|
||
return (
|
||
error_msg,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
|
||
if not versions:
|
||
error_msg = "未找到可用版本"
|
||
logger.warning(error_msg)
|
||
return (
|
||
error_msg,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
|
||
# 构建状态信息
|
||
status_info = []
|
||
if installed_version:
|
||
status_info.append(f"**当前安装版本:** {installed_version}")
|
||
if latest_version:
|
||
status_info.append(f"**最新版本:** {latest_version}")
|
||
status_info.append(f"**找到 {len(versions)} 个可用版本**")
|
||
|
||
status_message = "\n\n".join(status_info)
|
||
logger.info(f"版本信息载入完成: {status_message}")
|
||
|
||
# 返回更新后的组件
|
||
return (
|
||
status_message,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(choices=versions, value=versions[0] if versions else None, visible=True, label="选择要安装的版本"),
|
||
gr.Button(visible=True, value="安装选定版本")
|
||
)
|
||
|
||
except subprocess.TimeoutExpired:
|
||
error_msg = "获取版本列表超时"
|
||
logger.error(error_msg)
|
||
return (
|
||
error_msg,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
except Exception as e:
|
||
error_msg = f"获取版本列表失败: {str(e)}"
|
||
logger.error(error_msg)
|
||
return (
|
||
error_msg,
|
||
gr.Button(value="载入信息", interactive=True),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
|
||
def install_selected_version(selected_version: str):
|
||
"""安装选定的版本"""
|
||
import logging
|
||
import threading
|
||
import time
|
||
logger = logging.getLogger(__name__)
|
||
|
||
if not selected_version:
|
||
error_msg = "请先选择一个版本"
|
||
logger.warning(error_msg)
|
||
return error_msg
|
||
|
||
def install_and_exit():
|
||
"""在后台线程中执行安装并退出程序"""
|
||
try:
|
||
# 等待一小段时间确保UI响应已返回
|
||
time.sleep(1)
|
||
|
||
# 构建启动器命令
|
||
bootstrap_path = os.path.join(os.getcwd(), "bootstrap.pyz")
|
||
cmd = [sys.executable, bootstrap_path, f"--install-version={selected_version}"]
|
||
logger.info(f"开始通过启动器安装版本 {selected_version}")
|
||
logger.info(f"执行命令: {' '.join(cmd)}")
|
||
|
||
# 启动启动器进程(不等待完成)
|
||
subprocess.Popen(
|
||
cmd,
|
||
cwd=os.getcwd(),
|
||
creationflags=subprocess.CREATE_NEW_CONSOLE if os.name == 'nt' else 0
|
||
)
|
||
|
||
# 等待一小段时间确保启动器启动
|
||
time.sleep(2)
|
||
|
||
# 退出当前程序
|
||
logger.info("安装即将开始,正在退出当前程序...")
|
||
os._exit(0)
|
||
|
||
except Exception as e:
|
||
raise
|
||
|
||
try:
|
||
# 在后台线程中执行安装和退出
|
||
install_thread = threading.Thread(target=install_and_exit, daemon=True)
|
||
install_thread.start()
|
||
|
||
return f"正在启动器中安装版本 {selected_version},程序将自动重启..."
|
||
|
||
except Exception as e:
|
||
error_msg = f"启动安装进程失败: {str(e)}"
|
||
logger.error(error_msg)
|
||
return error_msg
|
||
|
||
def load_info_with_button_state():
|
||
"""载入信息并管理按钮状态"""
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
|
||
logger.info("开始载入版本信息")
|
||
|
||
# 先禁用按钮
|
||
yield (
|
||
"正在载入版本信息...",
|
||
gr.Button(value="载入中...", interactive=False),
|
||
gr.Dropdown(visible=False),
|
||
gr.Button(visible=False)
|
||
)
|
||
|
||
# 执行载入操作
|
||
result = list_all_versions()
|
||
logger.info("版本信息载入操作完成")
|
||
yield result
|
||
|
||
# 绑定事件
|
||
load_info_btn.click(
|
||
fn=load_info_with_button_state,
|
||
outputs=[status_text, load_info_btn, version_dropdown, install_selected_btn]
|
||
)
|
||
|
||
install_selected_btn.click(
|
||
fn=install_selected_version,
|
||
inputs=[version_dropdown],
|
||
outputs=[status_text]
|
||
)
|
||
|
||
def _create_screen_tab(self) -> None:
|
||
with gr.Tab("画面"):
|
||
gr.Markdown("## 当前设备画面")
|
||
refresh_btn = gr.Button("刷新画面", variant="primary")
|
||
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, "上次更新时间:无上下文数据"]
|
||
screenshot = ctx._screenshot
|
||
if screenshot is None:
|
||
return [None, "上次更新时间:无截图数据"]
|
||
screenshot = cv2.cvtColor(screenshot, cv2.COLOR_BGR2RGB)
|
||
return screenshot, f"上次更新时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||
|
||
refresh_btn.click(
|
||
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('./config.json')
|
||
ui = KotoneBotUI(kaa)
|
||
app = ui.create_ui()
|
||
app.launch(inbrowser=True, show_error=True)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|