i18n: wire startup and config messages
This commit is contained in:
@@ -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