i18n: wire startup and config messages
This commit is contained in:
122
bot.py
122
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() 中完成了所有清理工作,这是安全的
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
63
src/main.py
63
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):
|
||||
|
||||
Reference in New Issue
Block a user