kotones-auto-assistant/kotonebot/ui/gr.py

820 lines
34 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
import traceback
import importlib.metadata
from functools import partial
from importlib import resources
from datetime import datetime, timedelta
from typing import List, Dict, Tuple, Literal, Generator
import cv2
import gradio as gr
from kotonebot.backend.context import task_registry, ContextStackVars
from kotonebot.config.manager import load_config, save_config
from kotonebot.tasks.common import (
BaseConfig, APShopItems, PurchaseConfig, ActivityFundsConfig,
PresentsConfig, AssignmentConfig, ContestConfig, ProduceConfig,
MissionRewardConfig, PIdol, DailyMoneyShopItems, ProduceAction
)
from kotonebot.config.base_config import UserConfig, BackendConfig
from kotonebot.backend.bot import KotoneBot
# 初始化日志
os.makedirs('logs', exist_ok=True)
log_formatter = logging.Formatter('[%(asctime)s][%(levelname)s][%(name)s] %(message)s')
log_filename = datetime.now().strftime('logs/%y-%m-%d-%H-%M-%S.log')
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
file_handler = logging.FileHandler(log_filename, encoding='utf-8')
file_handler.setFormatter(log_formatter)
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
logging.getLogger("kotonebot").setLevel(logging.DEBUG)
def _save_bug_report(
path: str
) -> Generator[str, None, str]:
"""
保存错误报告
:param path: 保存的路径。若为 `None`,则保存到 `./reports/{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 = ""
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}"
# 打包 reports 文件夹
if os.path.exists('reports'):
for root, dirs, files in os.walk('reports'):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.join('reports', os.path.relpath(file_path, 'reports'))
zipf.write(file_path, arcname)
yield f"### 打包 report 文件:{arcname}"
# 上传报告
from .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) -> None:
self.is_running: bool = False
self.kaa: KotoneBot = KotoneBot(module='kotonebot.tasks', config_type=BaseConfig)
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 save_settings(
self,
adb_ip: str,
adb_port: int,
screenshot_method: Literal['adb', 'adb_raw', 'uiautomator2'],
keep_screenshots: bool,
check_emulator: bool,
emulator_path: str,
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,
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],
mission_reward_enabled: 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.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
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
),
produce=ProduceConfig(
enabled=produce_enabled,
mode=produce_mode,
produce_count=produce_count,
idols=[PIdol[idol] for idol in 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]
),
mission_reward=MissionRewardConfig(
enabled=mission_reward_enabled
)
)
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("## 状态")
progress_bar = gr.Progress()
with gr.Row():
run_btn = gr.Button("启动", scale=1)
debug_btn = gr.Button("调试", scale=1)
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("")
# TODO: 实现任务执行逻辑
def execute_single_task(task_name: str) -> str:
if not task_name:
gr.Warning("请先选择一个任务")
return ""
task = None
for name, task in task_registry.items():
if name == task_name:
task = task
break
if task is None:
gr.Warning(f"任务 {task_name} 未找到")
return ""
gr.Info(f"任务 {task_name} 开始执行。执行结束前,请勿重复点击执行。")
self.kaa.run([task])
gr.Success(f"任务 {task_name} 执行完毕")
return ""
execute_btn.click(
fn=execute_single_task,
inputs=[task_dropdown],
outputs=[task_result]
)
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_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"],
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 = [idol.name for idol in PIdol]
selected_idols = [idol.name for idol in 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
)
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
)
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
)
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
def _create_settings_tab(self) -> None:
with gr.Tab("设置"):
gr.Markdown("## 设置")
# 模拟器设置
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'],
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
)
emulator_path = gr.Textbox(
value=self.current_config.backend.emulator_path,
label="模拟器 exe 文件路径",
info=BackendConfig.model_fields['emulator_path'].description,
interactive=True
)
# 商店购买设置
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()
# 竞赛设置
with gr.Column():
gr.Markdown("### 竞赛设置")
contest = gr.Checkbox(
label="启用竞赛",
value=self.current_config.options.contest.enabled,
info=ContestConfig.model_fields['enabled'].description
)
# 培育设置
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
)
save_btn = gr.Button("保存设置")
result = gr.Markdown()
# 收集所有设置组件
all_settings = [
adb_ip, adb_port, screenshot_impl, keep_screenshots,
check_emulator, emulator_path,
*purchase_settings,
activity_funds,
presents,
*work_settings,
contest,
*produce_settings,
mission_reward,
]
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, path='report.zip'),
outputs=[result_text]
)
def _create_whats_new_tab(self) -> None:
"""创建更新日志标签页,并显示最新版本更新内容"""
with gr.Tab("更新日志"):
from ..tasks.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"):
version = importlib.metadata.version('ksaa')
gr.Markdown(f"# 琴音小助手 v{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() -> None:
ui = KotoneBotUI()
app = ui.create_ui()
app.launch(inbrowser=True)
if __name__ == "__main__":
main()