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

View File

@@ -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():

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 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,
)
)

View File

@@ -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)

View File

@@ -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):