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

View File

@@ -6,6 +6,7 @@ requires-python = ">=3.10"
dependencies = [ dependencies = [
"aiohttp-cors>=0.8.1", "aiohttp-cors>=0.8.1",
"aiohttp>=3.12.14", "aiohttp>=3.12.14",
"Babel>=2.17.0",
"colorama>=0.4.6", "colorama>=0.4.6",
"faiss-cpu>=1.11.0", "faiss-cpu>=1.11.0",
"fastapi>=0.116.0", "fastapi>=0.116.0",

View File

@@ -1,5 +1,6 @@
aiohttp-cors>=0.8.1 aiohttp-cors>=0.8.1
aiohttp>=3.12.14 aiohttp>=3.12.14
Babel>=2.17.0
colorama>=0.4.6 colorama>=0.4.6
faiss-cpu>=1.11.0 faiss-cpu>=1.11.0
fastapi>=0.116.0 fastapi>=0.116.0

View File

@@ -1,44 +1,44 @@
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Mapping, Sequence, TypeVar from typing import Any, Callable, Mapping, Sequence, TypeVar
from datetime import datetime
import asyncio import asyncio
import copy import copy
import tomlkit
import sys import sys
from .legacy_migration import try_migrate_legacy_bot_config_dict import tomlkit
from .config_base import AttributeData, ConfigBase, Field
from .config_utils import compare_versions, output_config_changes, recursive_parse_item_to_table
from .file_watcher import FileChange, FileWatcher
from .legacy_migration import try_migrate_legacy_bot_config_dict
from .model_configs import APIProvider, ModelInfo, ModelTaskConfig
from .official_configs import ( from .official_configs import (
BotConfig, BotConfig,
PersonalityConfig,
ExpressionConfig,
ChatConfig, ChatConfig,
EmojiConfig,
KeywordReactionConfig,
ChineseTypoConfig, ChineseTypoConfig,
DatabaseConfig,
DebugConfig,
EmojiConfig,
ExperimentalConfig,
ExpressionConfig,
KeywordReactionConfig,
LPMMKnowledgeConfig,
MaiSakaConfig,
MaimMessageConfig,
MemoryConfig,
MessageReceiveConfig,
PersonalityConfig,
RelationshipConfig,
ResponsePostProcessConfig, ResponsePostProcessConfig,
ResponseSplitterConfig, ResponseSplitterConfig,
TelemetryConfig, TelemetryConfig,
ExperimentalConfig,
MessageReceiveConfig,
MaimMessageConfig,
LPMMKnowledgeConfig,
RelationshipConfig,
ToolConfig, ToolConfig,
VoiceConfig, VoiceConfig,
MemoryConfig,
DebugConfig,
WebUIConfig, WebUIConfig,
DatabaseConfig,
MaiSakaConfig,
) )
from .model_configs import ModelInfo, ModelTaskConfig, APIProvider from src.common.i18n import t
from .config_base import ConfigBase, Field, AttributeData
from .config_utils import recursive_parse_item_to_table, output_config_changes, compare_versions
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.file_watcher import FileChange, FileWatcher
""" """
如果你想要修改配置文件请递增version的值 如果你想要修改配置文件请递增version的值
@@ -146,27 +146,33 @@ class ModelConfig(ConfigBase):
def model_post_init(self, context: Any = None): def model_post_init(self, context: Any = None):
if not self.models: if not self.models:
raise ValueError("模型列表不能为空,请在配置中设置有效的模型列表。") raise ValueError(t("config.models_empty"))
if not self.api_providers: if not self.api_providers:
raise ValueError("API提供商列表不能为空请在配置中设置有效的API提供商列表。") raise ValueError(t("config.api_providers_empty"))
# 检查API提供商名称是否重复 # 检查API提供商名称是否重复
provider_names = [provider.name for provider in self.api_providers] provider_names = [provider.name for provider in self.api_providers]
if len(provider_names) != len(set(provider_names)): if len(provider_names) != len(set(provider_names)):
raise ValueError("API提供商名称存在重复请检查配置文件。") raise ValueError(t("config.api_provider_name_duplicate"))
# 检查模型名称是否重复 # 检查模型名称是否重复
model_names = [model.name for model in self.models] model_names = [model.name for model in self.models]
if len(model_names) != len(set(model_names)): if len(model_names) != len(set(model_names)):
raise ValueError("模型名称存在重复,请检查配置文件。") raise ValueError(t("config.model_name_duplicate"))
api_providers_dict = {provider.name: provider for provider in self.api_providers} api_providers_dict = {provider.name: provider for provider in self.api_providers}
for model in self.models: for model in self.models:
if not model.model_identifier: if not model.model_identifier:
raise ValueError(f"模型 '{model.name}' 的 model_identifier 不能为空") raise ValueError(t("config.model_identifier_empty", model_name=model.name))
if not model.api_provider or model.api_provider not in api_providers_dict: if not model.api_provider or model.api_provider not in api_providers_dict:
raise ValueError(f"模型 '{model.name}' 的 api_provider '{model.api_provider}' 不存在") raise ValueError(
t(
"config.model_api_provider_missing",
api_provider=model.api_provider,
model_name=model.name,
)
)
return super().model_post_init(context) return super().model_post_init(context)
@@ -188,11 +194,11 @@ class ConfigManager:
self._last_hot_reload_monotonic: float = 0.0 self._last_hot_reload_monotonic: float = 0.0
def initialize(self): def initialize(self):
logger.info(f"MaiCore当前版本: {MMC_VERSION}") logger.info(t("config.current_version", version=MMC_VERSION))
logger.info("正在品鉴配置文件...") logger.info(t("config.loading"))
self.global_config = self.load_global_config() self.global_config = self.load_global_config()
self.model_config = self.load_model_config() self.model_config = self.load_model_config()
logger.info("非常的新鲜,非常的美味!") logger.info(t("config.loaded"))
def load_global_config(self) -> Config: def load_global_config(self) -> Config:
config, updated = load_config_from_file(Config, self.bot_config_path, CONFIG_VERSION) config, updated = load_config_from_file(Config, self.bot_config_path, CONFIG_VERSION)
@@ -208,12 +214,12 @@ class ConfigManager:
def get_global_config(self) -> Config: def get_global_config(self) -> Config:
if self.global_config is None: if self.global_config is None:
raise RuntimeError("global_config 未初始化") raise RuntimeError(t("config.global_not_initialized"))
return self.global_config return self.global_config
def get_model_config(self) -> ModelConfig: def get_model_config(self) -> ModelConfig:
if self.model_config is None: if self.model_config is None:
raise RuntimeError("model_config 未初始化") raise RuntimeError(t("config.model_not_initialized"))
return self.model_config return self.model_config
def register_reload_callback(self, callback: Callable[[], object]) -> None: def register_reload_callback(self, callback: Callable[[], object]) -> None:
@@ -240,18 +246,18 @@ class ConfigManager:
True, True,
) )
except Exception as exc: except Exception as exc:
logger.error(f"配置重载失败: {exc}") logger.error(t("config.reload_failed", error=exc))
return False return False
if global_updated or model_updated: if global_updated or model_updated:
logger.warning("检测到配置版本更新,热重载仅更新内存数据") logger.warning(t("config.version_update_detected"))
self.global_config = global_config_new self.global_config = global_config_new
self.model_config = model_config_new self.model_config = model_config_new
global global_config, model_config global global_config, model_config
global_config = global_config_new global_config = global_config_new
model_config = model_config_new model_config = model_config_new
logger.info("配置热重载完成") logger.info(t("config.hot_reload_completed"))
for callback in list(self._reload_callbacks): for callback in list(self._reload_callbacks):
try: try:
@@ -259,7 +265,7 @@ class ConfigManager:
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):
await result await result
except Exception as exc: except Exception as exc:
logger.warning(f"配置重载回调执行失败: {exc}") logger.warning(t("config.reload_callback_failed", error=exc))
return True return True
async def start_file_watcher(self) -> None: async def start_file_watcher(self) -> None:
@@ -277,7 +283,7 @@ class ConfigManager:
paths=[self.bot_config_path, self.model_config_path], paths=[self.bot_config_path, self.model_config_path],
) )
await self._file_watcher.start() await self._file_watcher.start()
logger.info("配置文件监视器已启动") logger.info(t("config.file_watcher_started"))
async def stop_file_watcher(self) -> None: async def stop_file_watcher(self) -> None:
if self._file_watcher is None: if self._file_watcher is None:
@@ -287,14 +293,16 @@ class ConfigManager:
self._file_watcher_subscription_id = None self._file_watcher_subscription_id = None
watcher_stats = self._file_watcher.stats watcher_stats = self._file_watcher.stats
logger.info( logger.info(
"配置文件监视器停止统计: " t(
f"batches={watcher_stats.batches_seen}, " "config.file_watcher_stop_stats",
f"changes={watcher_stats.changes_seen}, " batches=watcher_stats.batches_seen,
f"ok={watcher_stats.callbacks_succeeded}, " changes=watcher_stats.changes_seen,
f"failed={watcher_stats.callbacks_failed}, " cooldown_skip=watcher_stats.callbacks_skipped_cooldown,
f"timeout={watcher_stats.callbacks_timed_out}, " failed=watcher_stats.callbacks_failed,
f"cooldown_skip={watcher_stats.callbacks_skipped_cooldown}, " ok=watcher_stats.callbacks_succeeded,
f"restart={watcher_stats.restart_count}" restart=watcher_stats.restart_count,
timeout=watcher_stats.callbacks_timed_out,
)
) )
await self._file_watcher.stop() await self._file_watcher.stop()
self._file_watcher = None self._file_watcher = None
@@ -304,14 +312,14 @@ class ConfigManager:
return return
now_monotonic = asyncio.get_running_loop().time() now_monotonic = asyncio.get_running_loop().time()
if now_monotonic - self._last_hot_reload_monotonic < self._hot_reload_min_interval_s: if now_monotonic - self._last_hot_reload_monotonic < self._hot_reload_min_interval_s:
logger.debug("文件变更触发过于频繁,已跳过本次重载") logger.debug(t("config.reload_skipped_too_frequent"))
return return
self._last_hot_reload_monotonic = now_monotonic self._last_hot_reload_monotonic = now_monotonic
logger.info("检测到配置文件变更,触发热重载") logger.info(t("config.file_change_detected"))
try: try:
await asyncio.wait_for(self.reload_config(), timeout=self._hot_reload_timeout_s) await asyncio.wait_for(self.reload_config(), timeout=self._hot_reload_timeout_s)
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.error(f"配置热重载超时(>{self._hot_reload_timeout_s}s") logger.error(t("config.reload_timeout", timeout_seconds=self._hot_reload_timeout_s))
def generate_new_config_file(config_class: type[T], config_path: Path, inner_config_version: str) -> None: def generate_new_config_file(config_class: type[T], config_path: Path, inner_config_version: str) -> None:
@@ -333,10 +341,10 @@ def load_config_from_file(
config_data = tomlkit.load(f) config_data = tomlkit.load(f)
inner_table = config_data.get("inner") inner_table = config_data.get("inner")
if not isinstance(inner_table, Mapping): if not isinstance(inner_table, Mapping):
raise TypeError("配置文件缺少 inner 版本信息") raise TypeError(t("config.missing_inner_version"))
inner_version = inner_table.get("version") inner_version = inner_table.get("version")
if not isinstance(inner_version, str): if not isinstance(inner_version, str):
raise TypeError("配置文件 inner.version 类型错误") raise TypeError(t("config.invalid_inner_version"))
old_ver: str = inner_version old_ver: str = inner_version
config_data.remove("inner") # 移除 inner 部分,避免干扰后续处理 config_data.remove("inner") # 移除 inner 部分,避免干扰后续处理
config_data = config_data.unwrap() # 转换为普通字典,方便后续处理 config_data = config_data.unwrap() # 转换为普通字典,方便后续处理
@@ -352,9 +360,7 @@ def load_config_from_file(
# 基于未被部分构造污染的 original_data 做迁移尝试 # 基于未被部分构造污染的 original_data 做迁移尝试
mig = try_migrate_legacy_bot_config_dict(original_data) mig = try_migrate_legacy_bot_config_dict(original_data)
if mig.migrated: if mig.migrated:
logger.warning( logger.warning(t("config.legacy_migrated", reason=mig.reason))
f"检测到旧版配置结构,已尝试自动修复: {mig.reason}。建议稍后检查并保存生成的新配置文件。"
)
migrated_data = mig.data migrated_data = mig.data
target_config = config_class.from_dict(attribute_data, migrated_data) target_config = config_class.from_dict(attribute_data, migrated_data)
else: else:
@@ -367,7 +373,7 @@ def load_config_from_file(
updated = True updated = True
return target_config, updated return target_config, updated
except Exception as e: except Exception as e:
logger.critical(f"配置文件{config_path.name}解析失败") logger.critical(t("config.parse_failed", file_name=config_path.name))
raise e raise e
@@ -402,11 +408,11 @@ def write_config_to_file(
aot = tomlkit.aot() aot = tomlkit.aot()
for item in config_field: for item in config_field:
if not isinstance(item, ConfigBase): if not isinstance(item, ConfigBase):
raise TypeError("配置写入只支持ConfigBase子类") raise TypeError(t("config.write_unsupported_type"))
aot.append(recursive_parse_item_to_table(item, override_repr=override_repr)) aot.append(recursive_parse_item_to_table(item, override_repr=override_repr))
full_config_data.add(config_item_name, aot) full_config_data.add(config_item_name, aot)
else: else:
raise TypeError("配置写入只支持ConfigBase子类") raise TypeError(t("config.write_unsupported_type"))
# 备份旧文件 # 备份旧文件
if config_path.exists(): if config_path.exists():

View File

@@ -1,10 +1,13 @@
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Set, Tuple, Union, get_args, get_origin
from pydantic.fields import FieldInfo from pydantic.fields import FieldInfo
from typing import Any, get_args, get_origin, TYPE_CHECKING, Literal, List, Set, Tuple, Dict, Union
import types
from tomlkit import items from tomlkit import items
import tomlkit import tomlkit
import types
from .config_base import ConfigBase from .config_base import ConfigBase
from src.common.i18n import t
if TYPE_CHECKING: if TYPE_CHECKING:
from .config_base import AttributeData from .config_base import AttributeData
@@ -132,15 +135,20 @@ def convert_field(config_item_name: str, config_item_info: FieldInfo, value: Any
def output_config_changes(attr_data: "AttributeData", logger, old_ver: str, new_ver: str, file_name: str): def output_config_changes(attr_data: "AttributeData", logger, old_ver: str, new_ver: str, file_name: str):
"""输出配置变更信息""" """输出配置变更信息"""
logger.info("-------- 配置文件变更信息 --------") logger.info(t("config.change_summary_header"))
logger.info(f"新增配置数量: {len(attr_data.missing_attributes)}") logger.info(t("config.added_count", count=len(attr_data.missing_attributes)))
for attr in attr_data.missing_attributes: for attr in attr_data.missing_attributes:
logger.info(f"配置文件中新增配置项: {attr}") logger.info(t("config.added_item", attribute=attr))
logger.info(f"移除配置数量: {len(attr_data.redundant_attributes)}") logger.info(t("config.removed_count", count=len(attr_data.redundant_attributes)))
for attr in attr_data.redundant_attributes: for attr in attr_data.redundant_attributes:
logger.warning(f"移除配置项: {attr}") logger.warning(t("config.removed_item", attribute=attr))
logger.info( logger.info(
f"{file_name}配置文件已经更新. Old: {old_ver} -> New: {new_ver} 建议检查新配置文件中的内容, 以免丢失重要信息" t(
"config.file_updated",
file_name=file_name,
new_version=new_ver,
old_version=old_ver,
)
) )

View File

@@ -1,5 +1,7 @@
from typing import Any from typing import Any
from .config_base import ConfigBase, Field from .config_base import ConfigBase, Field
from src.common.i18n import t
class APIProvider(ConfigBase): class APIProvider(ConfigBase):
@@ -77,11 +79,11 @@ class APIProvider(ConfigBase):
def model_post_init(self, context: Any = None): def model_post_init(self, context: Any = None):
"""确保api_key在repr中不被显示""" """确保api_key在repr中不被显示"""
if not self.api_key: if not self.api_key:
raise ValueError("API密钥不能为空, 请在配置中设置有效的API密钥。") raise ValueError(t("config.api_key_empty"))
if not self.base_url and self.client_type != "gemini": # TODO: 允许gemini使用base_url if not self.base_url and self.client_type != "gemini": # TODO: 允许gemini使用base_url
raise ValueError("API基础URL不能为空, 请在配置中设置有效的基础URL。") raise ValueError(t("config.api_base_url_empty"))
if not self.name: if not self.name:
raise ValueError("API提供商名称不能为空, 请在配置中设置有效的名称。") raise ValueError(t("config.api_provider_name_empty"))
return super().model_post_init(context) return super().model_post_init(context)
@@ -178,11 +180,11 @@ class ModelInfo(ConfigBase):
def model_post_init(self, context: Any = None): def model_post_init(self, context: Any = None):
if not self.model_identifier: if not self.model_identifier:
raise ValueError("模型标识符不能为空, 请在配置中设置有效的模型标识符。") raise ValueError(t("config.model_identifier_empty_generic"))
if not self.name: if not self.name:
raise ValueError("模型名称不能为空, 请在配置中设置有效的模型名称。") raise ValueError(t("config.model_name_empty"))
if not self.api_provider: if not self.api_provider:
raise ValueError("API提供商不能为空, 请在配置中设置有效的API提供商。") raise ValueError(t("config.model_api_provider_empty"))
return super().model_post_init(context) return super().model_post_init(context)

View File

@@ -1,31 +1,30 @@
from maim_message import MessageServer
from rich.traceback import install
import asyncio import asyncio
import time import time
from maim_message import MessageServer
from src.common.remote import TelemetryHeartBeatTask from src.bw_learner.expression_auto_check_task import ExpressionAutoCheckTask
from src.manager.async_task_manager import async_task_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
# from src.chat.utils.token_statistics import TokenStatisticsTask
from src.chat.emoji_system.emoji_manager import emoji_manager from src.chat.emoji_system.emoji_manager import emoji_manager
from src.chat.message_receive.chat_manager import chat_manager
from src.config.config import config_manager, global_config
from src.chat.message_receive.bot import chat_bot
from src.common.logger import get_logger
from src.common.message_server.server import get_global_server, Server
from src.chat.knowledge import lpmm_start_up from src.chat.knowledge import lpmm_start_up
from rich.traceback import install from src.chat.message_receive.bot import chat_bot
from src.chat.message_receive.chat_manager import chat_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from src.common.i18n import t
from src.common.logger import get_logger
from src.common.message_server import get_global_api
from src.common.message_server.server import Server, get_global_server
from src.common.remote import TelemetryHeartBeatTask
from src.config.config import config_manager, global_config
from src.manager.async_task_manager import async_task_manager
from src.plugin_runtime.integration import get_plugin_runtime_manager
from src.prompt.prompt_manager import prompt_manager
# from src.api.main import start_api_server # from src.api.main import start_api_server
# 导入插件运行时 # 导入插件运行时
from src.plugin_runtime.integration import get_plugin_runtime_manager
# 导入消息API和traceback模块 # 导入消息API和traceback模块
from src.common.message_server import get_global_api # from src.chat.utils.token_statistics import TokenStatisticsTask
from src.bw_learner.expression_auto_check_task import ExpressionAutoCheckTask
from src.prompt.prompt_manager import prompt_manager
install(extra_lines=3) install(extra_lines=3)
@@ -47,7 +46,7 @@ class MainSystem:
from src.config.config import global_config from src.config.config import global_config
if not global_config.webui.enabled: if not global_config.webui.enabled:
logger.info("WebUI 已禁用") logger.info(t("startup.webui_disabled"))
return return
try: try:
@@ -56,26 +55,16 @@ class MainSystem:
self.webui_server = get_webui_server() self.webui_server = get_webui_server()
except Exception as e: except Exception as e:
logger.error(f"❌ 初始化 WebUI 服务器失败: {e}") logger.error(t("startup.webui_server_init_failed", error=e))
async def initialize(self): async def initialize(self):
"""初始化系统组件""" """初始化系统组件"""
logger.info(f"正在唤醒{global_config.bot.nickname}......") logger.info(t("startup.waking_up", nickname=global_config.bot.nickname))
# 其他初始化任务 # 其他初始化任务
await asyncio.gather(self._init_components()) await asyncio.gather(self._init_components())
logger.info(f""" logger.info(t("startup.initialization_completed_banner", nickname=global_config.bot.nickname))
--------------------------------
全部系统初始化完成,{global_config.bot.nickname}已成功唤醒
--------------------------------
如果想要自定义{global_config.bot.nickname}的功能,请查阅https://docs.mai-mai.org/manual/usage/
或者遇到了问题,请访问我们的文档:https://docs.mai-mai.org/
--------------------------------
如果你想要编写或了解插件相关内容请访问开发文档https://docs.mai-mai.org/develop/
--------------------------------
如果你需要查阅模型的消耗以及麦麦的统计数据请访问根目录的maibot_statistics.html文件
""")
async def _init_components(self): async def _init_components(self):
"""初始化其他组件""" """初始化其他组件"""
@@ -107,13 +96,13 @@ class MainSystem:
# 初始化表情管理器 # 初始化表情管理器
emoji_manager.load_emojis_from_db() emoji_manager.load_emojis_from_db()
logger.info("表情包管理器初始化成功") logger.info(t("startup.emoji_manager_initialized"))
# 初始化聊天管理器 # 初始化聊天管理器
await chat_manager.initialize() await chat_manager.initialize()
asyncio.create_task(chat_manager.regularly_save_sessions()) asyncio.create_task(chat_manager.regularly_save_sessions())
logger.info("聊天管理器初始化成功") logger.info(t("startup.chat_manager_initialized"))
# await asyncio.sleep(0.5) #防止logger输出飞了 # await asyncio.sleep(0.5) #防止logger输出飞了
@@ -134,9 +123,9 @@ class MainSystem:
# logger.info("已触发 ON_START 事件") # logger.info("已触发 ON_START 事件")
try: try:
init_time = int(1000 * (time.time() - init_start_time)) init_time = int(1000 * (time.time() - init_start_time))
logger.info(f"初始化完成,神经元放电{init_time}") logger.info(t("startup.initialization_completed_cycles", init_time=init_time))
except Exception as e: except Exception as e:
logger.error(f"启动大脑和外部世界失败: {e}") logger.error(t("startup.brain_external_world_failed", error=e))
raise raise
async def schedule_tasks(self): async def schedule_tasks(self):
@@ -154,7 +143,7 @@ class MainSystem:
await asyncio.gather(*tasks) await asyncio.gather(*tasks)
except asyncio.CancelledError: except asyncio.CancelledError:
logger.info("调度任务已取消") logger.info(t("startup.schedule_cancelled"))
raise raise
# async def forget_memory_task(self): # async def forget_memory_task(self):