From 27d334df08c334c72d0c72762531f5850f56dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 12 Mar 2026 17:24:37 +0900 Subject: [PATCH] i18n: wire startup and config messages --- bot.py | 122 +++++++++++++++++++++--------------- pyproject.toml | 1 + requirements.txt | 1 + src/config/config.py | 120 ++++++++++++++++++----------------- src/config/config_utils.py | 24 ++++--- src/config/model_configs.py | 14 +++-- src/main.py | 63 ++++++++----------- 7 files changed, 188 insertions(+), 157 deletions(-) diff --git a/bot.py b/bot.py index 4414f551..c4f55dcc 100644 --- a/bot.py +++ b/bot.py @@ -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() 中完成了所有清理工作,这是安全的 diff --git a/pyproject.toml b/pyproject.toml index cc6cb884..46d90c66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.10" dependencies = [ "aiohttp-cors>=0.8.1", "aiohttp>=3.12.14", + "Babel>=2.17.0", "colorama>=0.4.6", "faiss-cpu>=1.11.0", "fastapi>=0.116.0", diff --git a/requirements.txt b/requirements.txt index b65420de..12c390f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiohttp-cors>=0.8.1 aiohttp>=3.12.14 +Babel>=2.17.0 colorama>=0.4.6 faiss-cpu>=1.11.0 fastapi>=0.116.0 diff --git a/src/config/config.py b/src/config/config.py index 70c4a6fb..de196599 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -1,44 +1,44 @@ +from datetime import datetime from pathlib import Path from typing import Any, Callable, Mapping, Sequence, TypeVar -from datetime import datetime + import asyncio import copy - -import tomlkit 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 ( BotConfig, - PersonalityConfig, - ExpressionConfig, ChatConfig, - EmojiConfig, - KeywordReactionConfig, ChineseTypoConfig, + DatabaseConfig, + DebugConfig, + EmojiConfig, + ExperimentalConfig, + ExpressionConfig, + KeywordReactionConfig, + LPMMKnowledgeConfig, + MaiSakaConfig, + MaimMessageConfig, + MemoryConfig, + MessageReceiveConfig, + PersonalityConfig, + RelationshipConfig, ResponsePostProcessConfig, ResponseSplitterConfig, TelemetryConfig, - ExperimentalConfig, - MessageReceiveConfig, - MaimMessageConfig, - LPMMKnowledgeConfig, - RelationshipConfig, ToolConfig, VoiceConfig, - MemoryConfig, - DebugConfig, WebUIConfig, - DatabaseConfig, - MaiSakaConfig, ) -from .model_configs import ModelInfo, ModelTaskConfig, APIProvider -from .config_base import ConfigBase, Field, AttributeData -from .config_utils import recursive_parse_item_to_table, output_config_changes, compare_versions - +from src.common.i18n import t from src.common.logger import get_logger -from src.config.file_watcher import FileChange, FileWatcher """ 如果你想要修改配置文件,请递增version的值 @@ -146,27 +146,33 @@ class ModelConfig(ConfigBase): def model_post_init(self, context: Any = None): if not self.models: - raise ValueError("模型列表不能为空,请在配置中设置有效的模型列表。") + raise ValueError(t("config.models_empty")) if not self.api_providers: - raise ValueError("API提供商列表不能为空,请在配置中设置有效的API提供商列表。") + raise ValueError(t("config.api_providers_empty")) # 检查API提供商名称是否重复 provider_names = [provider.name for provider in self.api_providers] 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] 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} for model in self.models: 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: - 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) @@ -188,11 +194,11 @@ class ConfigManager: self._last_hot_reload_monotonic: float = 0.0 def initialize(self): - logger.info(f"MaiCore当前版本: {MMC_VERSION}") - logger.info("正在品鉴配置文件...") + logger.info(t("config.current_version", version=MMC_VERSION)) + logger.info(t("config.loading")) self.global_config = self.load_global_config() self.model_config = self.load_model_config() - logger.info("非常的新鲜,非常的美味!") + logger.info(t("config.loaded")) def load_global_config(self) -> Config: 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: if self.global_config is None: - raise RuntimeError("global_config 未初始化") + raise RuntimeError(t("config.global_not_initialized")) return self.global_config def get_model_config(self) -> ModelConfig: if self.model_config is None: - raise RuntimeError("model_config 未初始化") + raise RuntimeError(t("config.model_not_initialized")) return self.model_config def register_reload_callback(self, callback: Callable[[], object]) -> None: @@ -240,18 +246,18 @@ class ConfigManager: True, ) except Exception as exc: - logger.error(f"配置重载失败: {exc}") + logger.error(t("config.reload_failed", error=exc)) return False if global_updated or model_updated: - logger.warning("检测到配置版本更新,热重载仅更新内存数据") + logger.warning(t("config.version_update_detected")) self.global_config = global_config_new self.model_config = model_config_new global global_config, model_config global_config = global_config_new model_config = model_config_new - logger.info("配置热重载完成") + logger.info(t("config.hot_reload_completed")) for callback in list(self._reload_callbacks): try: @@ -259,7 +265,7 @@ class ConfigManager: if asyncio.iscoroutine(result): await result except Exception as exc: - logger.warning(f"配置重载回调执行失败: {exc}") + logger.warning(t("config.reload_callback_failed", error=exc)) return True async def start_file_watcher(self) -> None: @@ -277,7 +283,7 @@ class ConfigManager: paths=[self.bot_config_path, self.model_config_path], ) await self._file_watcher.start() - logger.info("配置文件监视器已启动") + logger.info(t("config.file_watcher_started")) async def stop_file_watcher(self) -> None: if self._file_watcher is None: @@ -287,14 +293,16 @@ class ConfigManager: self._file_watcher_subscription_id = None watcher_stats = self._file_watcher.stats logger.info( - "配置文件监视器停止统计: " - f"batches={watcher_stats.batches_seen}, " - f"changes={watcher_stats.changes_seen}, " - f"ok={watcher_stats.callbacks_succeeded}, " - f"failed={watcher_stats.callbacks_failed}, " - f"timeout={watcher_stats.callbacks_timed_out}, " - f"cooldown_skip={watcher_stats.callbacks_skipped_cooldown}, " - f"restart={watcher_stats.restart_count}" + t( + "config.file_watcher_stop_stats", + batches=watcher_stats.batches_seen, + changes=watcher_stats.changes_seen, + cooldown_skip=watcher_stats.callbacks_skipped_cooldown, + failed=watcher_stats.callbacks_failed, + ok=watcher_stats.callbacks_succeeded, + restart=watcher_stats.restart_count, + timeout=watcher_stats.callbacks_timed_out, + ) ) await self._file_watcher.stop() self._file_watcher = None @@ -304,14 +312,14 @@ class ConfigManager: return now_monotonic = asyncio.get_running_loop().time() 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 self._last_hot_reload_monotonic = now_monotonic - logger.info("检测到配置文件变更,触发热重载") + logger.info(t("config.file_change_detected")) try: await asyncio.wait_for(self.reload_config(), timeout=self._hot_reload_timeout_s) 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: @@ -333,10 +341,10 @@ def load_config_from_file( config_data = tomlkit.load(f) inner_table = config_data.get("inner") if not isinstance(inner_table, Mapping): - raise TypeError("配置文件缺少 inner 版本信息") + raise TypeError(t("config.missing_inner_version")) inner_version = inner_table.get("version") if not isinstance(inner_version, str): - raise TypeError("配置文件 inner.version 类型错误") + raise TypeError(t("config.invalid_inner_version")) old_ver: str = inner_version config_data.remove("inner") # 移除 inner 部分,避免干扰后续处理 config_data = config_data.unwrap() # 转换为普通字典,方便后续处理 @@ -352,9 +360,7 @@ def load_config_from_file( # 基于未被部分构造污染的 original_data 做迁移尝试 mig = try_migrate_legacy_bot_config_dict(original_data) if mig.migrated: - logger.warning( - f"检测到旧版配置结构,已尝试自动修复: {mig.reason}。建议稍后检查并保存生成的新配置文件。" - ) + logger.warning(t("config.legacy_migrated", reason=mig.reason)) migrated_data = mig.data target_config = config_class.from_dict(attribute_data, migrated_data) else: @@ -367,7 +373,7 @@ def load_config_from_file( updated = True return target_config, updated except Exception as e: - logger.critical(f"配置文件{config_path.name}解析失败") + logger.critical(t("config.parse_failed", file_name=config_path.name)) raise e @@ -402,11 +408,11 @@ def write_config_to_file( aot = tomlkit.aot() for item in config_field: 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)) full_config_data.add(config_item_name, aot) else: - raise TypeError("配置写入只支持ConfigBase子类") + raise TypeError(t("config.write_unsupported_type")) # 备份旧文件 if config_path.exists(): diff --git a/src/config/config_utils.py b/src/config/config_utils.py index 90c5741f..f2e73291 100644 --- a/src/config/config_utils.py +++ b/src/config/config_utils.py @@ -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 typing import Any, get_args, get_origin, TYPE_CHECKING, Literal, List, Set, Tuple, Dict, Union -import types from tomlkit import items + import tomlkit +import types from .config_base import ConfigBase +from src.common.i18n import t if TYPE_CHECKING: 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): """输出配置变更信息""" - logger.info("-------- 配置文件变更信息 --------") - logger.info(f"新增配置数量: {len(attr_data.missing_attributes)}") + logger.info(t("config.change_summary_header")) + logger.info(t("config.added_count", count=len(attr_data.missing_attributes))) for attr in attr_data.missing_attributes: - logger.info(f"配置文件中新增配置项: {attr}") - logger.info(f"移除配置数量: {len(attr_data.redundant_attributes)}") + logger.info(t("config.added_item", attribute=attr)) + logger.info(t("config.removed_count", count=len(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( - 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, + ) ) diff --git a/src/config/model_configs.py b/src/config/model_configs.py index 9665d9c6..6f10ff83 100644 --- a/src/config/model_configs.py +++ b/src/config/model_configs.py @@ -1,5 +1,7 @@ from typing import Any + from .config_base import ConfigBase, Field +from src.common.i18n import t class APIProvider(ConfigBase): @@ -77,11 +79,11 @@ class APIProvider(ConfigBase): def model_post_init(self, context: Any = None): """确保api_key在repr中不被显示""" 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 - raise ValueError("API基础URL不能为空, 请在配置中设置有效的基础URL。") + raise ValueError(t("config.api_base_url_empty")) if not self.name: - raise ValueError("API提供商名称不能为空, 请在配置中设置有效的名称。") + raise ValueError(t("config.api_provider_name_empty")) return super().model_post_init(context) @@ -178,11 +180,11 @@ class ModelInfo(ConfigBase): def model_post_init(self, context: Any = None): if not self.model_identifier: - raise ValueError("模型标识符不能为空, 请在配置中设置有效的模型标识符。") + raise ValueError(t("config.model_identifier_empty_generic")) if not self.name: - raise ValueError("模型名称不能为空, 请在配置中设置有效的模型名称。") + raise ValueError(t("config.model_name_empty")) if not self.api_provider: - raise ValueError("API提供商不能为空, 请在配置中设置有效的API提供商。") + raise ValueError(t("config.model_api_provider_empty")) return super().model_post_init(context) diff --git a/src/main.py b/src/main.py index 9d8e06a6..91da2d83 100644 --- a/src/main.py +++ b/src/main.py @@ -1,31 +1,30 @@ +from maim_message import MessageServer +from rich.traceback import install + import asyncio import time -from maim_message import MessageServer -from src.common.remote import TelemetryHeartBeatTask -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.bw_learner.expression_auto_check_task import ExpressionAutoCheckTask 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 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.plugin_runtime.integration import get_plugin_runtime_manager - # 导入消息API和traceback模块 -from src.common.message_server import get_global_api -from src.bw_learner.expression_auto_check_task import ExpressionAutoCheckTask - -from src.prompt.prompt_manager import prompt_manager +# from src.chat.utils.token_statistics import TokenStatisticsTask install(extra_lines=3) @@ -47,7 +46,7 @@ class MainSystem: from src.config.config import global_config if not global_config.webui.enabled: - logger.info("WebUI 已禁用") + logger.info(t("startup.webui_disabled")) return try: @@ -56,26 +55,16 @@ class MainSystem: self.webui_server = get_webui_server() except Exception as e: - logger.error(f"❌ 初始化 WebUI 服务器失败: {e}") + logger.error(t("startup.webui_server_init_failed", error=e)) 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()) - logger.info(f""" --------------------------------- -全部系统初始化完成,{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文件 -""") + logger.info(t("startup.initialization_completed_banner", nickname=global_config.bot.nickname)) async def _init_components(self): """初始化其他组件""" @@ -107,13 +96,13 @@ class MainSystem: # 初始化表情管理器 emoji_manager.load_emojis_from_db() - logger.info("表情包管理器初始化成功") + logger.info(t("startup.emoji_manager_initialized")) # 初始化聊天管理器 await chat_manager.initialize() asyncio.create_task(chat_manager.regularly_save_sessions()) - logger.info("聊天管理器初始化成功") + logger.info(t("startup.chat_manager_initialized")) # await asyncio.sleep(0.5) #防止logger输出飞了 @@ -134,9 +123,9 @@ class MainSystem: # logger.info("已触发 ON_START 事件") try: 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: - logger.error(f"启动大脑和外部世界失败: {e}") + logger.error(t("startup.brain_external_world_failed", error=e)) raise async def schedule_tasks(self): @@ -154,7 +143,7 @@ class MainSystem: await asyncio.gather(*tasks) except asyncio.CancelledError: - logger.info("调度任务已取消") + logger.info(t("startup.schedule_cancelled")) raise # async def forget_memory_task(self):