i18n: wire startup and config messages

This commit is contained in:
春河晴
2026-03-12 17:24:37 +09:00
parent c470fdfd1e
commit 27d334df08
7 changed files with 188 additions and 157 deletions

122
bot.py
View File

@@ -1,21 +1,26 @@
# raise RuntimeError("System Not Ready")
from pathlib import Path
from dotenv import load_dotenv
from rich.traceback import install
import asyncio
import hashlib
import os
import time
import platform
import traceback
import shutil
import sys
import subprocess
from dotenv import load_dotenv
from pathlib import Path
from rich.traceback import install
from src.common.logger import initialize_logging, get_logger, shutdown_logging
import sys
import time
import traceback
from src.common.i18n import set_locale, t, tn
from src.common.logger import get_logger, initialize_logging, shutdown_logging
# 设置工作目录为脚本所在目录
script_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(script_dir)
set_locale(os.getenv("MAIBOT_LOCALE", "zh-CN"))
env_path = Path(__file__).parent / ".env"
template_env_path = Path(__file__).parent / "template" / "template.env"
@@ -26,15 +31,17 @@ else:
try:
if template_env_path.exists():
shutil.copyfile(template_env_path, env_path)
print("未找到.env已从 template/template.env 自动创建")
print(t("startup.env_created"))
load_dotenv(str(env_path), override=True)
else:
print("未找到.env文件也未找到模板 template/template.env")
raise FileNotFoundError(".env 文件不存在,请创建并配置所需的环境变量")
print(t("startup.env_template_missing"))
raise FileNotFoundError(t("startup.env_file_missing"))
except Exception as e:
print(f"自动创建 .env 失败: {e}")
print(t("startup.env_auto_create_failed", error=e))
raise
set_locale(os.getenv("MAIBOT_LOCALE", "zh-CN"))
# 检查是否是 Worker 进程,只在 Worker 进程中输出详细的初始化信息
# Runner 进程只需要基本的日志功能,不需要详细的初始化日志
is_worker = os.environ.get("MAIBOT_WORKER_PROCESS") == "1"
@@ -46,7 +53,7 @@ logger = get_logger("main")
RESTART_EXIT_CODE = 42
print("-----------------------------------------")
print("\n\n\n\n\n")
print("警告Dev进入不稳定开发状态任何插件与WebUI均可能无法正常工作")
print(t("startup.dev_branch_warning"))
print("\n\n\n\n\n")
print("-----------------------------------------")
@@ -64,8 +71,8 @@ def run_runner_process():
env["MAIBOT_WORKER_PROCESS"] = "1"
while True:
logger.info(f"正在启动 {script_file}...")
logger.info("正在编译着色器1/114514")
logger.info(t("startup.launching_script", script_file=script_file))
logger.info(t("startup.compiling_shaders"))
# 启动子进程 (Worker)
# 使用 sys.executable 确保使用相同的 Python 解释器
@@ -78,11 +85,11 @@ def run_runner_process():
return_code = process.wait()
if return_code == RESTART_EXIT_CODE:
logger.info("检测到重启请求 (退出码 42),正在重启...")
logger.info(t("startup.restart_requested", exit_code=RESTART_EXIT_CODE))
time.sleep(1) # 稍作等待
continue
else:
logger.info(f"程序已退出 (退出码 {return_code})")
logger.info(t("startup.program_exited", return_code=return_code))
sys.exit(return_code)
except KeyboardInterrupt:
@@ -94,7 +101,7 @@ def run_runner_process():
process.terminate()
process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("子进程未响应,强制关闭...")
logger.warning(t("startup.child_process_force_kill"))
process.kill()
sys.exit(0)
@@ -128,7 +135,7 @@ from src.manager.async_task_manager import async_task_manager # noqa
# 设置工作目录为脚本所在目录
# script_dir = os.path.dirname(os.path.abspath(__file__))
# os.chdir(script_dir)
logger.info(f"已设置工作目录为: {script_dir}")
logger.info(t("startup.worker_dir_set", script_dir=script_dir))
confirm_logger = get_logger("confirm")
@@ -150,16 +157,16 @@ def print_opensource_notice():
notice_lines = [
"",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.GREEN} ★ MaiBot - 开源 AI 聊天机器人 ★{Style.RESET_ALL}",
f"{Fore.GREEN}{t('startup.opensource_title')}{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.YELLOW} 本项目是完全免费的开源软件,基于 GPL-3.0 协议发布{Style.RESET_ALL}",
f"{Fore.WHITE} 如果有人向你「出售本软件」,你被骗了!{Style.RESET_ALL}",
f"{Fore.YELLOW}{t('startup.opensource_free_notice')}{Style.RESET_ALL}",
f"{Fore.WHITE}{t('startup.opensource_scamming_notice')}{Style.RESET_ALL}",
"",
f"{Fore.WHITE} 官方仓库: {Fore.BLUE}https://github.com/MaiM-with-u/MaiBot {Style.RESET_ALL}",
f"{Fore.WHITE} 官方文档: {Fore.BLUE}https://docs.mai-mai.org {Style.RESET_ALL}",
f"{Fore.WHITE} 官方群聊: {Fore.BLUE}1006149251{Style.RESET_ALL}",
f"{Fore.WHITE}{t('startup.opensource_repo')}{Fore.BLUE}{t('startup.opensource_repo_value')} {Style.RESET_ALL}",
f"{Fore.WHITE}{t('startup.opensource_docs')}{Fore.BLUE}{t('startup.opensource_docs_value')} {Style.RESET_ALL}",
f"{Fore.WHITE}{t('startup.opensource_group')}{Fore.BLUE}{t('startup.opensource_group_value')}{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
f"{Fore.RED}将本软件作为「商品」倒卖、隐瞒开源性质均违反协议!{Style.RESET_ALL}",
f"{Fore.RED}{t('startup.opensource_resale_warning').strip()}{Style.RESET_ALL}",
f"{Fore.CYAN}{'' * 70}{Style.RESET_ALL}",
"",
]
@@ -173,7 +180,7 @@ def easter_egg():
from colorama import init, Fore
init()
text = "多年以后面对AI行刑队张三将会回想起他2023年在会议上讨论人工智能的那个下午"
text = t("startup.easter_egg")
rainbow_colors = [Fore.RED, Fore.YELLOW, Fore.GREEN, Fore.CYAN, Fore.BLUE, Fore.MAGENTA]
rainbow_text = ""
for i, char in enumerate(text):
@@ -183,7 +190,7 @@ def easter_egg():
async def graceful_shutdown(): # sourcery skip: use-named-expression
try:
logger.info("正在优雅关闭麦麦...")
logger.info(t("startup.shutdown_started"))
# 关闭 WebUI 服务器
# try:
@@ -203,16 +210,17 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
# 停止新版本插件运行时
from src.plugin_runtime.integration import get_plugin_runtime_manager
await get_plugin_runtime_manager().stop()
# 停止所有异步任务
await async_task_manager.stop_and_wait_all_tasks()
# 获取所有剩余任务,排除当前任务
remaining_tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
remaining_tasks = [task for task in asyncio.all_tasks() if task is not asyncio.current_task()]
if remaining_tasks:
logger.info(f"正在取消 {len(remaining_tasks)} 个剩余任务...")
logger.info(tn("startup.remaining_tasks_cancelling", len(remaining_tasks)))
# 取消所有剩余任务
for task in remaining_tasks:
@@ -222,23 +230,23 @@ async def graceful_shutdown(): # sourcery skip: use-named-expression
# 等待所有任务完成,设置超时
try:
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=15.0)
logger.info("所有剩余任务已成功取消")
logger.info(t("startup.remaining_tasks_cancelled"))
except asyncio.TimeoutError:
logger.warning("等待任务取消超时,强制继续关闭")
logger.warning(t("startup.remaining_tasks_cancel_timeout"))
except Exception as e:
logger.error(f"等待任务取消时发生异常: {e}")
logger.error(t("startup.remaining_tasks_cancel_error", error=e))
logger.info("麦麦优雅关闭完成")
logger.info(t("startup.shutdown_completed"))
except Exception as e:
logger.error(f"麦麦关闭失败: {e}", exc_info=True)
logger.error(t("startup.shutdown_failed", error=e), exc_info=True)
def _calculate_file_hash(file_path: Path, file_type: str) -> str:
"""计算文件的MD5哈希值"""
if not file_path.exists():
logger.error(f"{file_type} 文件不存在")
raise FileNotFoundError(f"{file_type} 文件不存在")
logger.error(t("startup.file_not_found", file_type=file_type))
raise FileNotFoundError(t("startup.file_not_found", file_type=file_type))
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
@@ -267,26 +275,42 @@ def _check_agreement_status(file_hash: str, confirm_file: Path, env_var: str) ->
def _prompt_user_confirmation(eula_hash: str, privacy_hash: str) -> None:
"""提示用户确认协议"""
confirm_logger.critical("EULA或隐私条款内容已更新请在阅读后重新确认继续运行视为同意更新后的以上两款协议")
confirm_logger.critical(t("startup.agreement_reconfirm"))
confirm_logger.critical(
f'输入"同意""confirmed"或设置环境变量"EULA_AGREE={eula_hash}""PRIVACY_AGREE={privacy_hash}"继续运行'
t(
"startup.agreement_confirm_prompt",
eula_hash=eula_hash,
privacy_hash=privacy_hash,
)
)
while True:
user_input = input().strip().lower()
if user_input in ["同意", "confirmed"]:
return
confirm_logger.critical('请输入"同意""confirmed"以继续运行')
confirm_logger.critical(t("startup.agreement_confirm_retry"))
def _save_confirmations(eula_updated: bool, privacy_updated: bool, eula_hash: str, privacy_hash: str) -> None:
"""保存用户确认结果"""
if eula_updated:
logger.info(f"更新EULA确认文件{eula_hash}")
logger.info(
t(
"startup.agreement_updated",
agreement_name=t("startup.eula_name"),
file_hash=eula_hash,
)
)
Path("eula.confirmed").write_text(eula_hash, encoding="utf-8")
if privacy_updated:
logger.info(f"更新隐私条款确认文件{privacy_hash}")
logger.info(
t(
"startup.agreement_updated",
agreement_name=t("startup.privacy_name"),
file_hash=privacy_hash,
)
)
Path("privacy.confirmed").write_text(privacy_hash, encoding="utf-8")
@@ -321,7 +345,7 @@ def raw_main():
print_opensource_notice()
check_eula()
logger.info("检查EULA和隐私条款完成")
logger.info(t("startup.eula_privacy_checked"))
easter_egg()
@@ -353,7 +377,7 @@ if __name__ == "__main__":
loop.run_until_complete(main_tasks)
except KeyboardInterrupt:
logger.warning("收到中断信号,正在优雅关闭...")
logger.warning(t("startup.interrupt_received"))
# 取消主任务
if "main_tasks" in locals() and main_tasks and not main_tasks.done():
@@ -368,7 +392,7 @@ if __name__ == "__main__":
try:
loop.run_until_complete(graceful_shutdown())
except Exception as ge:
logger.error(f"优雅关闭时发生错误: {ge}")
logger.error(t("startup.graceful_shutdown_error", error=ge))
# 新增:检测外部请求关闭
except SystemExit as e:
@@ -378,24 +402,24 @@ if __name__ == "__main__":
else:
exit_code = 1 if e.code else 0
if exit_code == RESTART_EXIT_CODE:
logger.info("收到重启信号,准备退出并请求重启...")
logger.info(t("startup.restart_signal_received"))
except Exception as e:
logger.error(f"主程序发生异常: {str(e)} {str(traceback.format_exc())}")
logger.error(t("startup.main_error", error=f"{str(e)} {str(traceback.format_exc())}"))
exit_code = 1 # 标记发生错误
finally:
# 确保 loop 在任何情况下都尝试关闭(如果存在且未关闭)
if "loop" in locals() and loop and not loop.is_closed():
loop.close()
print("[主程序] 事件循环已关闭")
print(t("startup.event_loop_closed"))
# 关闭日志系统,释放文件句柄
try:
shutdown_logging()
except Exception as e:
print(f"关闭日志系统时出错: {e}")
print(t("startup.logging_shutdown_error", error=e))
print("[主程序] 准备退出...")
print(t("startup.prepare_exit"))
# 使用 os._exit() 强制退出,避免被阻塞
# 由于已经在 graceful_shutdown() 中完成了所有清理工作,这是安全的