diff --git a/.github/workflows/i18n-check.yml b/.github/workflows/i18n-check.yml new file mode 100644 index 0000000000..87460eda31 --- /dev/null +++ b/.github/workflows/i18n-check.yml @@ -0,0 +1,29 @@ +name: I18n Usage Check +env: + PYTHONPATH: . +on: + pull_request: + paths: + - '**.py' + +jobs: + i18n-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install fluent.runtime pytest pytest_asyncio + + - name: Run I18n Check + run: | + python -m pytest --tb=line tests/test_i18n.py diff --git a/astrbot/builtin_stars/astrbot/long_term_memory.py b/astrbot/builtin_stars/astrbot/long_term_memory.py index e08cdc5157..d5e5935bca 100644 --- a/astrbot/builtin_stars/astrbot/long_term_memory.py +++ b/astrbot/builtin_stars/astrbot/long_term_memory.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import datetime import random import uuid @@ -28,7 +29,7 @@ def cfg(self, event: AstrMessageEvent): try: max_cnt = int(cfg["provider_ltm_settings"]["group_message_max_cnt"]) except BaseException as e: - logger.error(e) + logger.error(t("msg-5bdf8f5c", e=e)) max_cnt = 300 image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"] image_caption_provider_id = cfg["provider_ltm_settings"].get( @@ -74,9 +75,9 @@ async def get_image_caption( else: provider = self.context.get_provider_by_id(image_caption_provider_id) if not provider: - raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商") + raise Exception(t("msg-8e11fa57", image_caption_provider_id=image_caption_provider_id)) if not isinstance(provider, Provider): - raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述") + raise Exception(t("msg-8ebaa397", res=type(provider))) response = await provider.text_chat( prompt=image_caption_prompt, session_id=uuid.uuid4().hex, @@ -128,7 +129,7 @@ async def handle_message(self, event: AstrMessageEvent) -> None: try: url = comp.url if comp.url else comp.file if not url: - raise Exception("图片 URL 为空") + raise Exception(t("msg-30954f77")) caption = await self.get_image_caption( url, cfg["image_caption_provider_id"], @@ -136,14 +137,14 @@ async def handle_message(self, event: AstrMessageEvent) -> None: ) parts.append(f" [Image: {caption}]") except Exception as e: - logger.error(f"获取图片描述失败: {e}") + logger.error(t("msg-62de0c3e", e=e)) else: parts.append(" [Image]") elif isinstance(comp, At): parts.append(f" [At: {comp.name}]") final_message = "".join(parts) - logger.debug(f"ltm | {event.unified_msg_origin} | {final_message}") + logger.debug(t("msg-d0647999", res=event.unified_msg_origin, final_message=final_message)) self.session_chats[event.unified_msg_origin].append(final_message) if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]: self.session_chats[event.unified_msg_origin].pop(0) @@ -180,7 +181,7 @@ async def after_req_llm( if llm_resp.completion_text: final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {llm_resp.completion_text}" logger.debug( - f"Recorded AI response: {event.unified_msg_origin} | {final_message}" + t("msg-133c1f1d", res=event.unified_msg_origin, final_message=final_message) ) self.session_chats[event.unified_msg_origin].append(final_message) cfg = self.cfg(event) diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index da2a008354..b1c887a52a 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import traceback from astrbot.api import star @@ -16,7 +17,7 @@ def __init__(self, context: star.Context) -> None: try: self.ltm = LongTermMemory(self.context.astrbot_config_mgr, self.context) except BaseException as e: - logger.error(f"聊天增强 err: {e}") + logger.error(t("msg-3df554a1", e=e)) def ltm_enabled(self, event: AstrMessageEvent): ltmse = self.context.get_config(umo=event.unified_msg_origin)[ @@ -44,13 +45,13 @@ async def on_message(self, event: AstrMessageEvent): try: await self.ltm.handle_message(event) except BaseException as e: - logger.error(e) + logger.error(t("msg-5bdf8f5c", e=e)) if need_active: """主动回复""" provider = self.context.get_using_provider(event.unified_msg_origin) if not provider: - logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复") + logger.error(t("msg-bb6ff036")) return try: conv = None @@ -60,7 +61,7 @@ async def on_message(self, event: AstrMessageEvent): if not session_curr_cid: logger.error( - "当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。", + t("msg-afa050be"), ) return @@ -72,7 +73,7 @@ async def on_message(self, event: AstrMessageEvent): prompt = event.message_str if not conv: - logger.error("未找到对话,无法主动回复") + logger.error(t("msg-9a6a6b2e")) return yield event.request_llm( @@ -81,8 +82,8 @@ async def on_message(self, event: AstrMessageEvent): conversation=conv, ) except BaseException as e: - logger.error(traceback.format_exc()) - logger.error(f"主动回复失败: {e}") + logger.error(t("msg-78b9c276", res=traceback.format_exc())) + logger.error(t("msg-b177e640", e=e)) @filter.on_llm_request() async def decorate_llm_req( @@ -93,7 +94,7 @@ async def decorate_llm_req( try: await self.ltm.on_req_llm(event, req) except BaseException as e: - logger.error(f"ltm: {e}") + logger.error(t("msg-24d2f380", e=e)) @filter.on_llm_response() async def record_llm_resp_to_ltm( @@ -104,7 +105,7 @@ async def record_llm_resp_to_ltm( try: await self.ltm.after_req_llm(event, resp) except Exception as e: - logger.error(f"ltm: {e}") + logger.error(t("msg-24d2f380", e=e)) @filter.after_message_sent() async def after_message_sent(self, event: AstrMessageEvent) -> None: @@ -115,4 +116,4 @@ async def after_message_sent(self, event: AstrMessageEvent) -> None: if clean_session: await self.ltm.remove_session(event) except Exception as e: - logger.error(f"ltm: {e}") + logger.error(t("msg-24d2f380", e=e)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/admin.py b/astrbot/builtin_stars/builtin_commands/commands/admin.py index a4f46b6036..75a94aca11 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/admin.py +++ b/astrbot/builtin_stars/builtin_commands/commands/admin.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageChain, MessageEventResult from astrbot.core.config.default import VERSION @@ -13,30 +14,30 @@ async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: if not admin_id: event.set_result( MessageEventResult().message( - "使用方法: /op 授权管理员;/deop 取消管理员。可通过 /sid 获取 ID。", + t("msg-ad019976"), ), ) return self.context.get_config()["admins_id"].append(str(admin_id)) self.context.get_config().save_config() - event.set_result(MessageEventResult().message("授权成功。")) + event.set_result(MessageEventResult().message(t("msg-1235330f"))) async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None: """取消授权管理员。deop """ if not admin_id: event.set_result( MessageEventResult().message( - "使用方法: /deop 取消管理员。可通过 /sid 获取 ID。", + t("msg-e78847e0"), ), ) return try: self.context.get_config()["admins_id"].remove(str(admin_id)) self.context.get_config().save_config() - event.set_result(MessageEventResult().message("取消授权成功。")) + event.set_result(MessageEventResult().message(t("msg-012152c1"))) except ValueError: event.set_result( - MessageEventResult().message("此用户 ID 不在管理员名单内。"), + MessageEventResult().message(t("msg-5e076026")), ) async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: @@ -44,21 +45,21 @@ async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: if not sid: event.set_result( MessageEventResult().message( - "使用方法: /wl 添加白名单;/dwl 删除白名单。可通过 /sid 获取 ID。", + t("msg-7f8eedde"), ), ) return cfg = self.context.get_config(umo=event.unified_msg_origin) cfg["platform_settings"]["id_whitelist"].append(str(sid)) cfg.save_config() - event.set_result(MessageEventResult().message("添加白名单成功。")) + event.set_result(MessageEventResult().message(t("msg-de1b0a87"))) async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None: """删除白名单。dwl """ if not sid: event.set_result( MessageEventResult().message( - "使用方法: /dwl 删除白名单。可通过 /sid 获取 ID。", + t("msg-59d6fcbe"), ), ) return @@ -66,12 +67,12 @@ async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None: cfg = self.context.get_config(umo=event.unified_msg_origin) cfg["platform_settings"]["id_whitelist"].remove(str(sid)) cfg.save_config() - event.set_result(MessageEventResult().message("删除白名单成功。")) + event.set_result(MessageEventResult().message(t("msg-4638580f"))) except ValueError: - event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) + event.set_result(MessageEventResult().message(t("msg-278fb868"))) async def update_dashboard(self, event: AstrMessageEvent) -> None: """更新管理面板""" - await event.send(MessageChain().message("正在尝试更新管理面板...")) + await event.send(MessageChain().message(t("msg-1dee5007"))) await download_dashboard(version=f"v{VERSION}", latest=False) - await event.send(MessageChain().message("管理面板更新完成。")) + await event.send(MessageChain().message(t("msg-76bea66c"))) diff --git a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py b/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py index ba31c3326c..87c4dc6152 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +++ b/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.core.star.filter.command import CommandFilter @@ -31,11 +32,7 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: if token.len < 3: await event.send( MessageChain().message( - "该指令用于设置指令或指令组的权限。\n" - "格式: /alter_cmd \n" - "例1: /alter_cmd c1 admin 将 c1 设为管理员指令\n" - "例2: /alter_cmd g1 c1 admin 将 g1 指令组的 c1 子指令设为管理员指令\n" - "/alter_cmd reset config 打开 reset 权限配置", + t("msg-d7a36c19"), ), ) return @@ -63,7 +60,7 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: 修改指令格式: /alter_cmd reset scene <场景编号> 例如: /alter_cmd reset scene 2 member""" - await event.send(MessageChain().message(config_menu)) + await event.send(MessageChain().message(t("msg-afe0fa58", config_menu=config_menu))) return if cmd_name == "reset" and cmd_type == "scene" and token.len >= 4: @@ -71,18 +68,18 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: perm_type = token.get(4) if scene_num is None or perm_type is None: - await event.send(MessageChain().message("场景编号和权限类型不能为空")) + await event.send(MessageChain().message(t("msg-0c85d498"))) return if not scene_num.isdigit() or int(scene_num) < 1 or int(scene_num) > 3: await event.send( - MessageChain().message("场景编号必须是 1-3 之间的数字"), + MessageChain().message(t("msg-4e0afcd1")), ) return if perm_type not in ["admin", "member"]: await event.send( - MessageChain().message("权限类型错误,只能是 admin 或 member"), + MessageChain().message(t("msg-830d6eb8")), ) return @@ -94,14 +91,14 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: await event.send( MessageChain().message( - f"已将 reset 命令在{scene.name}场景下的权限设为{perm_type}", + t("msg-d1180ead", res=scene.name, perm_type=perm_type), ), ) return if cmd_type not in ["admin", "member"]: await event.send( - MessageChain().message("指令类型错误,可选类型有 admin, member"), + MessageChain().message(t("msg-8d9bc364")), ) return @@ -124,7 +121,7 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: break if not found_command: - await event.send(MessageChain().message("未找到该指令")) + await event.send(MessageChain().message(t("msg-1f2f65e0"))) return found_plugin = star_map[found_command.handler_module_path] @@ -168,6 +165,6 @@ async def alter_cmd(self, event: AstrMessageEvent) -> None: cmd_group_str = "指令组" if cmd_group else "指令" await event.send( MessageChain().message( - f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。", + t("msg-cd271581", cmd_name=cmd_name, cmd_group_str=cmd_group_str, cmd_type=cmd_type), ), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 5190a363ee..383e5f0786 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import datetime from astrbot.api import sp, star @@ -60,8 +61,7 @@ async def reset(self, message: AstrMessageEvent) -> None: if required_perm == "admin" and message.role != "admin": message.set_result( MessageEventResult().message( - f"在{scene.name}场景下,reset命令需要管理员权限," - f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。", + t("msg-63fe9607", res=scene.name, res_2=message.get_sender_id()), ), ) return @@ -74,12 +74,12 @@ async def reset(self, message: AstrMessageEvent) -> None: scope_id=umo, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("重置对话成功。")) + message.set_result(MessageEventResult().message(t("msg-6f4bbe27"))) return if not self.context.get_using_provider(umo): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message(t("msg-4cdd042d")), ) return @@ -88,7 +88,7 @@ async def reset(self, message: AstrMessageEvent) -> None: if not cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 切换或者 /new 创建。", + t("msg-69ed45be"), ), ) return @@ -105,7 +105,7 @@ async def reset(self, message: AstrMessageEvent) -> None: message.set_extra("_clean_ltm_session", True) - message.set_result(MessageEventResult().message(ret)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret))) async def stop(self, message: AstrMessageEvent) -> None: """停止当前会话正在运行的 Agent""" @@ -124,18 +124,18 @@ async def stop(self, message: AstrMessageEvent) -> None: if stopped_count > 0: message.set_result( MessageEventResult().message( - f"已请求停止 {stopped_count} 个运行中的任务。" + t("msg-772ec1fa", stopped_count=stopped_count) ) ) return - message.set_result(MessageEventResult().message("当前会话没有运行中的任务。")) + message.set_result(MessageEventResult().message(t("msg-8d42cd8a"))) async def his(self, message: AstrMessageEvent, page: int = 1) -> None: """查看对话记录""" if not self.context.get_using_provider(message.unified_msg_origin): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message(t("msg-4cdd042d")), ) return @@ -172,7 +172,7 @@ async def his(self, message: AstrMessageEvent, page: int = 1) -> None: f"*输入 /history 2 跳转到第 2 页" ) - message.set_result(MessageEventResult().message(ret).use_t2i(False)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: """查看对话列表""" @@ -181,7 +181,7 @@ async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: message.set_result( MessageEventResult().message( - f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。", + t("msg-efdfbe3e", THIRD_PARTY_AGENT_RUNNER_STR=THIRD_PARTY_AGENT_RUNNER_STR), ), ) return @@ -263,7 +263,7 @@ async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: ret += f"\n第 {page} 页 | 共 {total_pages} 页" ret += "\n*输入 /ls 2 跳转到第 2 页" - message.set_result(MessageEventResult().message(ret).use_t2i(False)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) return async def new_conv(self, message: AstrMessageEvent) -> None: @@ -277,7 +277,7 @@ async def new_conv(self, message: AstrMessageEvent) -> None: scope_id=message.unified_msg_origin, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("已创建新对话。")) + message.set_result(MessageEventResult().message(t("msg-492c2c02"))) return active_event_registry.stop_all(message.unified_msg_origin, exclude=message) @@ -291,7 +291,7 @@ async def new_conv(self, message: AstrMessageEvent) -> None: message.set_extra("_clean_ltm_session", True) message.set_result( - MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), + MessageEventResult().message(t("msg-c7dc838d", res=cid[:4])), ) async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: @@ -313,12 +313,12 @@ async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: ) message.set_result( MessageEventResult().message( - f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。", + t("msg-6da01230", session=session, res=cid[:4]), ), ) else: message.set_result( - MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"), + MessageEventResult().message(t("msg-f356d65a")), ) async def switch_conv( @@ -329,14 +329,14 @@ async def switch_conv( """通过 /ls 前面的序号切换对话""" if not isinstance(index, int): message.set_result( - MessageEventResult().message("类型错误,请输入数字对话序号。"), + MessageEventResult().message(t("msg-7e442185")), ) return if index is None: message.set_result( MessageEventResult().message( - "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话", + t("msg-00dbe29c"), ), ) return @@ -345,7 +345,7 @@ async def switch_conv( ) if index > len(conversations) or index < 1: message.set_result( - MessageEventResult().message("对话序号错误,请使用 /ls 查看"), + MessageEventResult().message(t("msg-a848ccf6")), ) else: conversation = conversations[index - 1] @@ -356,20 +356,20 @@ async def switch_conv( ) message.set_result( MessageEventResult().message( - f"切换到对话: {title}({conversation.cid[:4]})。", + t("msg-1ec33cf6", title=title, res=conversation.cid[:4]), ), ) async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None: """重命名对话""" if not new_name: - message.set_result(MessageEventResult().message("请输入新的对话名称。")) + message.set_result(MessageEventResult().message(t("msg-68e5dd6c"))) return await self.context.conversation_manager.update_conversation_title( message.unified_msg_origin, new_name, ) - message.set_result(MessageEventResult().message("重命名对话成功。")) + message.set_result(MessageEventResult().message(t("msg-c8dd6158"))) async def del_conv(self, message: AstrMessageEvent) -> None: """删除当前对话""" @@ -380,7 +380,7 @@ async def del_conv(self, message: AstrMessageEvent) -> None: # 群聊,没开独立会话,发送人不是管理员 message.set_result( MessageEventResult().message( - f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。", + t("msg-1f1fa2f2", res=message.get_sender_id()), ), ) return @@ -393,7 +393,7 @@ async def del_conv(self, message: AstrMessageEvent) -> None: scope_id=umo, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("重置对话成功。")) + message.set_result(MessageEventResult().message(t("msg-6f4bbe27"))) return session_curr_cid = ( @@ -403,7 +403,7 @@ async def del_conv(self, message: AstrMessageEvent) -> None: if not session_curr_cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。", + t("msg-6a1dc4b7"), ), ) return @@ -417,4 +417,4 @@ async def del_conv(self, message: AstrMessageEvent) -> None: ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" message.set_extra("_clean_ltm_session", True) - message.set_result(MessageEventResult().message(ret)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret))) diff --git a/astrbot/builtin_stars/builtin_commands/commands/help.py b/astrbot/builtin_stars/builtin_commands/commands/help.py index ae2f4c787e..afa2451191 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/help.py +++ b/astrbot/builtin_stars/builtin_commands/commands/help.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import aiohttp from astrbot.api import star @@ -85,4 +86,4 @@ async def help(self, event: AstrMessageEvent) -> None: msg_parts.append(notice) msg = "\n".join(msg_parts) - event.set_result(MessageEventResult().message(msg).use_t2i(False)) + event.set_result(MessageEventResult().message(t("msg-c046b6e4", msg=msg)).use_t2i(False)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/llm.py b/astrbot/builtin_stars/builtin_commands/commands/llm.py index ba9ba5c9b2..bbfcd1a2c1 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/llm.py +++ b/astrbot/builtin_stars/builtin_commands/commands/llm.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageChain @@ -17,4 +18,4 @@ async def llm(self, event: AstrMessageEvent) -> None: cfg["provider_settings"]["enable"] = True status = "开启" cfg.save_config() - await event.send(MessageChain().message(f"{status} LLM 聊天功能。")) + await event.send(MessageChain().message(t("msg-72cd5f57", status=status))) diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index 7a7416bbaf..31f889c483 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import builtins from typing import TYPE_CHECKING @@ -71,7 +72,7 @@ async def persona(self, message: AstrMessageEvent) -> None: if conv is None: message.set_result( MessageEventResult().message( - "当前对话不存在,请先使用 /new 新建一个对话。", + t("msg-4f52d0dd"), ), ) return @@ -107,18 +108,7 @@ async def persona(self, message: AstrMessageEvent) -> None: message.set_result( MessageEventResult() .message( - f"""[Persona] - -- 人格情景列表: `/persona list` -- 设置人格情景: `/persona 人格` -- 人格情景详细信息: `/persona view 人格` -- 取消人格: `/persona unset` - -默认人格情景: {default_persona["name"]} -当前对话 {curr_cid_title} 的人格情景: {curr_persona_name} - -配置人格情景请前往管理面板-配置页 -""", + t("msg-e092b97c", res=default_persona['name'], curr_cid_title=curr_cid_title, curr_persona_name=curr_persona_name), ) .use_t2i(False), ) @@ -148,10 +138,10 @@ async def persona(self, message: AstrMessageEvent) -> None: lines.append("*使用 `/persona view <人格名>` 查看详细信息") msg = "\n".join(lines) - message.set_result(MessageEventResult().message(msg).use_t2i(False)) + message.set_result(MessageEventResult().message(t("msg-c046b6e4", msg=msg)).use_t2i(False)) elif l[1] == "view": if len(l) == 2: - message.set_result(MessageEventResult().message("请输入人格情景名")) + message.set_result(MessageEventResult().message(t("msg-99139ef8"))) return ps = l[2].strip() if persona := next( @@ -165,24 +155,24 @@ async def persona(self, message: AstrMessageEvent) -> None: msg += f"{persona['prompt']}\n" else: msg = f"人格{ps}不存在" - message.set_result(MessageEventResult().message(msg)) + message.set_result(MessageEventResult().message(t("msg-c046b6e4", msg=msg))) elif l[1] == "unset": if not cid: message.set_result( - MessageEventResult().message("当前没有对话,无法取消人格。"), + MessageEventResult().message(t("msg-a44c7ec0")), ) return await self.context.conversation_manager.update_conversation_persona_id( message.unified_msg_origin, "[%None]", ) - message.set_result(MessageEventResult().message("取消人格成功。")) + message.set_result(MessageEventResult().message(t("msg-a90c75d4"))) else: ps = "".join(l[1:]).strip() if not cid: message.set_result( MessageEventResult().message( - "当前没有对话,请先开始对话或使用 /new 创建一个对话。", + t("msg-a712d71a"), ), ) return @@ -205,12 +195,12 @@ async def persona(self, message: AstrMessageEvent) -> None: message.set_result( MessageEventResult().message( - f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}", + t("msg-4e4e746d", force_warn_msg=force_warn_msg), ), ) else: message.set_result( MessageEventResult().message( - "不存在该人格情景。使用 /persona list 查看所有。", + t("msg-ab60a2e7"), ), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/plugin.py b/astrbot/builtin_stars/builtin_commands/commands/plugin.py index 49bee94627..6b1a169fad 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/plugin.py +++ b/astrbot/builtin_stars/builtin_commands/commands/plugin.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.core import DEMO_MODE, logger @@ -27,66 +28,66 @@ async def plugin_ls(self, event: AstrMessageEvent) -> None: plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。" event.set_result( - MessageEventResult().message(f"{plugin_list_info}").use_t2i(False), + MessageEventResult().message(t("msg-9cae24f5", plugin_list_info=plugin_list_info)).use_t2i(False), ) async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: """禁用插件""" if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法禁用插件。")) + event.set_result(MessageEventResult().message(t("msg-3f3a6087"))) return if not plugin_name: event.set_result( - MessageEventResult().message("/plugin off <插件名> 禁用插件。"), + MessageEventResult().message(t("msg-90e17cd4")), ) return await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。")) + event.set_result(MessageEventResult().message(t("msg-d29d6d57", plugin_name=plugin_name))) async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None: """启用插件""" if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法启用插件。")) + event.set_result(MessageEventResult().message(t("msg-f90bbe20"))) return if not plugin_name: event.set_result( - MessageEventResult().message("/plugin on <插件名> 启用插件。"), + MessageEventResult().message(t("msg-b897048f")), ) return await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。")) + event.set_result(MessageEventResult().message(t("msg-ebfb93bb", plugin_name=plugin_name))) async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None: """安装插件""" if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法安装插件。")) + event.set_result(MessageEventResult().message(t("msg-9cd74a8d"))) return if not plugin_repo: event.set_result( - MessageEventResult().message("/plugin get <插件仓库地址> 安装插件"), + MessageEventResult().message(t("msg-d79ad78d")), ) return - logger.info(f"准备从 {plugin_repo} 安装插件。") + logger.info(t("msg-4f293fe1", plugin_repo=plugin_repo)) if self.context._star_manager: star_mgr: PluginManager = self.context._star_manager try: await star_mgr.install_plugin(plugin_repo) # type: ignore - event.set_result(MessageEventResult().message("安装插件成功。")) + event.set_result(MessageEventResult().message(t("msg-d40e7065"))) except Exception as e: - logger.error(f"安装插件失败: {e}") - event.set_result(MessageEventResult().message(f"安装插件失败: {e}")) + logger.error(t("msg-feff82c6", e=e)) + event.set_result(MessageEventResult().message(t("msg-feff82c6", e=e))) return async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None: """获取插件帮助""" if not plugin_name: event.set_result( - MessageEventResult().message("/plugin help <插件名> 查看插件信息。"), + MessageEventResult().message(t("msg-5bfe9d3d")), ) return plugin = self.context.get_registered_star(plugin_name) if plugin is None: - event.set_result(MessageEventResult().message("未找到此插件。")) + event.set_result(MessageEventResult().message(t("msg-02627a9b"))) return help_msg = "" help_msg += f"\n\n✨ 作者: {plugin.author}\n✨ 版本: {plugin.version}" @@ -117,4 +118,4 @@ async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> N ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg ret += "更多帮助信息请查看插件仓库 README。" - event.set_result(MessageEventResult().message(ret).use_t2i(False)) + event.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/provider.py b/astrbot/builtin_stars/builtin_commands/commands/provider.py index b5ee75ca24..3b188c6d16 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ b/astrbot/builtin_stars/builtin_commands/commands/provider.py @@ -9,6 +9,7 @@ from astrbot import logger from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult +from astrbot.core.lang import t from astrbot.core.provider.entities import ProviderType from astrbot.core.utils.error_redaction import safe_error @@ -268,11 +269,7 @@ def _log_reachability_failure( """记录不可达原因到日志。""" meta = provider.meta() logger.warning( - "Provider reachability check failed: id=%s type=%s code=%s reason=%s", - meta.id, - provider_capability_type.name if provider_capability_type else "unknown", - err_code, - err_reason, + t("msg-7717d729", res=meta.id, res_2=provider_capability_type.name if provider_capability_type else 'unknown', err_code=err_code, err_reason=err_reason), ) async def _test_provider_capability(self, provider): @@ -405,7 +402,7 @@ async def provider( if all_providers: await event.send( MessageEventResult().message( - "正在进行提供商可达性测试,请稍候..." + t("msg-f4cfd3ab") ) ) check_results = await asyncio.gather( @@ -511,13 +508,13 @@ async def provider( if not reachability_check_enabled: ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。" - event.set_result(MessageEventResult().message(ret)) + event.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret))) elif idx == "tts": if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) + event.set_result(MessageEventResult().message(t("msg-f3d8988e"))) return if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) + event.set_result(MessageEventResult().message(t("msg-284759bb"))) return provider = self.context.get_all_tts_providers()[idx2 - 1] id_ = provider.meta().id @@ -526,13 +523,13 @@ async def provider( provider_type=ProviderType.TEXT_TO_SPEECH, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(t("msg-092d9956", id_=id_))) elif idx == "stt": if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) + event.set_result(MessageEventResult().message(t("msg-f3d8988e"))) return if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) + event.set_result(MessageEventResult().message(t("msg-284759bb"))) return provider = self.context.get_all_stt_providers()[idx2 - 1] id_ = provider.meta().id @@ -541,10 +538,10 @@ async def provider( provider_type=ProviderType.SPEECH_TO_TEXT, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(t("msg-092d9956", id_=id_))) elif isinstance(idx, int): if idx > len(self.context.get_all_providers()) or idx < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) + event.set_result(MessageEventResult().message(t("msg-284759bb"))) return provider = self.context.get_all_providers()[idx - 1] id_ = provider.meta().id @@ -553,9 +550,9 @@ async def provider( provider_type=ProviderType.CHAT_COMPLETION, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(t("msg-092d9956", id_=id_))) else: - event.set_result(MessageEventResult().message("无效的参数。")) + event.set_result(MessageEventResult().message(t("msg-bf9eb668"))) async def _switch_model_by_name( self, message: AstrMessageEvent, model_name: str, prov: Provider @@ -633,7 +630,7 @@ async def model_ls( prov = self.context.get_using_provider(message.unified_msg_origin) if not prov: message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message(t("msg-4cdd042d")), ) return config = self._get_model_lookup_config(message.unified_msg_origin) @@ -659,7 +656,7 @@ async def model_ls( ) ret = "".join(parts) - message.set_result(MessageEventResult().message(ret).use_t2i(False)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) elif isinstance(idx_or_name, int): models = await self._get_models_or_reply_error( message, @@ -670,7 +667,7 @@ async def model_ls( if models is None: return if idx_or_name > len(models) or idx_or_name < 1: - message.set_result(MessageEventResult().message("模型序号错误。")) + message.set_result(MessageEventResult().message(t("msg-cb218e86"))) else: try: new_model = models[idx_or_name - 1] @@ -697,7 +694,7 @@ async def key(self, message: AstrMessageEvent, index: int | None = None) -> None prov = self.context.get_using_provider(message.unified_msg_origin) if not prov: message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message(t("msg-4cdd042d")), ) return @@ -713,11 +710,11 @@ async def key(self, message: AstrMessageEvent, index: int | None = None) -> None parts.append("\n使用 /key 切换 Key。") ret = "".join(parts) - message.set_result(MessageEventResult().message(ret).use_t2i(False)) + message.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) else: keys_data = prov.get_keys() if index > len(keys_data) or index < 1: - message.set_result(MessageEventResult().message("Key 序号错误。")) + message.set_result(MessageEventResult().message(t("msg-584ca956"))) else: try: new_key = keys_data[index - 1] @@ -730,7 +727,7 @@ async def key(self, message: AstrMessageEvent, index: int | None = None) -> None except Exception as e: message.set_result( MessageEventResult().message( - safe_error("切换 Key 未知错误: ", e) + safe_error(t("msg-f52481b8"), e) ), ) return diff --git a/astrbot/builtin_stars/builtin_commands/commands/setunset.py b/astrbot/builtin_stars/builtin_commands/commands/setunset.py index 096698844d..8942c8de16 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/setunset.py +++ b/astrbot/builtin_stars/builtin_commands/commands/setunset.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import sp, star from astrbot.api.event import AstrMessageEvent, MessageEventResult @@ -15,7 +16,7 @@ async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> N event.set_result( MessageEventResult().message( - f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。", + t("msg-8b56b437", uid=uid, key=key), ), ) @@ -26,11 +27,11 @@ async def unset_variable(self, event: AstrMessageEvent, key: str) -> None: if key not in session_var: event.set_result( - MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"), + MessageEventResult().message(t("msg-dfd31d9d")), ) else: del session_var[key] await sp.session_put(uid, "session_variables", session_var) event.set_result( - MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"), + MessageEventResult().message(t("msg-bf181241", uid=uid, key=key)), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/sid.py b/astrbot/builtin_stars/builtin_commands/commands/sid.py index e8bdbffb19..7797359df3 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/sid.py +++ b/astrbot/builtin_stars/builtin_commands/commands/sid.py @@ -1,4 +1,5 @@ """会话ID命令""" +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult @@ -33,4 +34,4 @@ async def sid(self, event: AstrMessageEvent) -> None: ): ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。" - event.set_result(MessageEventResult().message(ret).use_t2i(False)) + event.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret)).use_t2i(False)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/t2i.py b/astrbot/builtin_stars/builtin_commands/commands/t2i.py index 78d6b0df7b..b9a12224ce 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/t2i.py +++ b/astrbot/builtin_stars/builtin_commands/commands/t2i.py @@ -1,4 +1,5 @@ """文本转图片命令""" +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult @@ -16,8 +17,8 @@ async def t2i(self, event: AstrMessageEvent) -> None: if config["t2i"]: config["t2i"] = False config.save_config() - event.set_result(MessageEventResult().message("已关闭文本转图片模式。")) + event.set_result(MessageEventResult().message(t("msg-855d5cf3"))) return config["t2i"] = True config.save_config() - event.set_result(MessageEventResult().message("已开启文本转图片模式。")) + event.set_result(MessageEventResult().message(t("msg-64da24f4"))) diff --git a/astrbot/builtin_stars/builtin_commands/commands/tts.py b/astrbot/builtin_stars/builtin_commands/commands/tts.py index 13049ac22e..193fe8022e 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/tts.py +++ b/astrbot/builtin_stars/builtin_commands/commands/tts.py @@ -1,4 +1,5 @@ """文本转语音命令""" +from astrbot.core.lang import t from astrbot.api import star from astrbot.api.event import AstrMessageEvent, MessageEventResult @@ -27,10 +28,10 @@ async def tts(self, event: AstrMessageEvent) -> None: if new_status and not tts_enable: event.set_result( MessageEventResult().message( - f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。", + t("msg-ef1b2145", status_text=status_text), ), ) else: event.set_result( - MessageEventResult().message(f"{status_text}当前会话的文本转语音。"), + MessageEventResult().message(t("msg-deee9deb", status_text=status_text)), ) diff --git a/astrbot/builtin_stars/session_controller/main.py b/astrbot/builtin_stars/session_controller/main.py index 70081e03a6..e522fbb09b 100644 --- a/astrbot/builtin_stars/session_controller/main.py +++ b/astrbot/builtin_stars/session_controller/main.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import copy from sys import maxsize @@ -82,7 +83,7 @@ async def handle_empty_mention(self, event: AstrMessageEvent): conversation=conversation, ) except Exception as e: - logger.error(f"LLM response failed: {e!s}") + logger.error(t("msg-b48bf3fe", e=e)) # LLM 回复失败,使用原始预设回复 yield event.plain_result("想要问什么呢?😄") diff --git a/astrbot/builtin_stars/web_searcher/engines/bing.py b/astrbot/builtin_stars/web_searcher/engines/bing.py index 7565e5df36..9087599648 100644 --- a/astrbot/builtin_stars/web_searcher/engines/bing.py +++ b/astrbot/builtin_stars/web_searcher/engines/bing.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from . import USER_AGENT_BING, SearchEngine @@ -27,4 +28,4 @@ async def _get_next_page(self, query) -> str: except Exception as _: self.base_url = base_url continue - raise Exception("Bing search failed") + raise Exception(t("msg-e3b4d1e9")) diff --git a/astrbot/builtin_stars/web_searcher/main.py b/astrbot/builtin_stars/web_searcher/main.py index d13ca15792..9cfaf3adad 100644 --- a/astrbot/builtin_stars/web_searcher/main.py +++ b/astrbot/builtin_stars/web_searcher/main.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json import random @@ -41,7 +42,7 @@ def __init__(self, context: star.Context) -> None: tavily_key = provider_settings.get("websearch_tavily_key") if isinstance(tavily_key, str): logger.info( - "检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。", + t("msg-7f5fd92b"), ) if tavily_key: provider_settings["websearch_tavily_key"] = [tavily_key] @@ -85,7 +86,7 @@ async def _process_search_result( websearch_link: bool, ) -> str: """处理单个搜索结果""" - logger.info(f"web_searcher - scraping web: {result.title} - {result.url}") + logger.info(t("msg-bed9def5", res=result.title, res_2=result.url)) try: site_result = await self._get_from_url(result.url) except BaseException: @@ -110,15 +111,15 @@ async def _web_search_default( try: results = await self.bing_search.search(query, num_results) except Exception as e: - logger.error(f"bing search error: {e}, try the next one...") + logger.error(t("msg-8214760c", e=e)) if len(results) == 0: - logger.debug("search bing failed") + logger.debug(t("msg-8676b5aa")) try: results = await self.sogo_search.search(query, num_results) except Exception as e: - logger.error(f"sogo search error: {e}") + logger.error(t("msg-3fb6d6ad", e=e)) if len(results) == 0: - logger.debug("search sogo failed") + logger.debug(t("msg-fe9b336f")) return [] return results @@ -127,7 +128,7 @@ async def _get_tavily_key(self, cfg: AstrBotConfig) -> str: """并发安全的从列表中获取并轮换Tavily API密钥。""" tavily_keys = cfg.get("provider_settings", {}).get("websearch_tavily_key", []) if not tavily_keys: - raise ValueError("错误:Tavily API密钥未在AstrBot中配置。") + raise ValueError(t("msg-c991b022")) async with self.tavily_key_lock: key = tavily_keys[self.tavily_key_index] @@ -155,7 +156,7 @@ async def _web_search_tavily( if response.status != 200: reason = await response.text() raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", + t("msg-b4fbb4a9", reason=reason, res=response.status), ) data = await response.json() results = [] @@ -186,13 +187,13 @@ async def _extract_tavily(self, cfg: AstrBotConfig, payload: dict) -> list[dict] if response.status != 200: reason = await response.text() raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", + t("msg-b4fbb4a9", reason=reason, res=response.status), ) data = await response.json() results: list[dict] = data.get("results", []) if not results: raise ValueError( - "Error: Tavily web searcher does not return any results.", + t("msg-6769aba9"), ) return results @@ -210,7 +211,7 @@ async def search_from_search_engine( max_results(number): 返回的最大搜索结果数量,默认为 5。 """ - logger.info(f"web_searcher - search_from_search_engine: {query}") + logger.info(t("msg-b1877974", query=query)) cfg = self.context.get_config(umo=event.unified_msg_origin) websearch_link = cfg["provider_settings"].get("web_search_link", False) @@ -226,7 +227,7 @@ async def search_from_search_engine( ret = "" for processed_result in processed_results: if isinstance(processed_result, BaseException): - logger.error(f"Error processing search result: {processed_result}") + logger.error(t("msg-2360df6b", processed_result=processed_result)) continue ret += processed_result @@ -245,7 +246,7 @@ async def ensure_baidu_ai_search_mcp(self, umo: str | None = None) -> None: ) if not key: raise ValueError( - "Error: Baidu AI Search API key is not configured in AstrBot.", + t("msg-359d0443"), ) func_tool_mgr = self.context.get_llm_tool_manager() await func_tool_mgr.enable_mcp_server( @@ -258,7 +259,7 @@ async def ensure_baidu_ai_search_mcp(self, umo: str | None = None) -> None: }, ) self.baidu_initialized = True - logger.info("Successfully initialized Baidu AI Search MCP server.") + logger.info(t("msg-94351632")) @llm_tool(name="fetch_url") async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str: @@ -298,11 +299,11 @@ async def search_from_tavily( end_date(string): Optional. The end date for the search results in the format 'YYYY-MM-DD'. """ - logger.info(f"web_searcher - search_from_tavily: {query}") + logger.info(t("msg-5a7207c1", query=query)) cfg = self.context.get_config(umo=event.unified_msg_origin) # websearch_link = cfg["provider_settings"].get("web_search_link", False) if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []): - raise ValueError("Error: Tavily API key is not configured in AstrBot.") + raise ValueError(t("msg-b36134c9")) # build payload payload = {"query": query, "max_results": max_results, "include_favicon": True} @@ -363,10 +364,10 @@ async def tavily_extract_web_page( """ cfg = self.context.get_config(umo=event.unified_msg_origin) if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []): - raise ValueError("Error: Tavily API key is not configured in AstrBot.") + raise ValueError(t("msg-b36134c9")) if not url: - raise ValueError("Error: url must be a non-empty string.") + raise ValueError(t("msg-98ed69f4")) if extract_depth not in ["basic", "advanced"]: extract_depth = "basic" payload = { @@ -387,7 +388,7 @@ async def _get_bocha_key(self, cfg: AstrBotConfig) -> str: """并发安全的从列表中获取并轮换BoCha API密钥。""" bocha_keys = cfg.get("provider_settings", {}).get("websearch_bocha_key", []) if not bocha_keys: - raise ValueError("错误:BoCha API密钥未在AstrBot中配置。") + raise ValueError(t("msg-51edd9ee")) async with self.bocha_key_lock: key = bocha_keys[self.bocha_key_index] @@ -415,7 +416,7 @@ async def _web_search_bocha( if response.status != 200: reason = await response.text() raise Exception( - f"BoCha web search failed: {reason}, status: {response.status}", + t("msg-73964067", reason=reason, res=response.status), ) data = await response.json() data = data["data"]["webPages"]["value"] @@ -488,11 +489,11 @@ async def search_from_bocha( The actual number of returned results may be less than the specified count. """ - logger.info(f"web_searcher - search_from_bocha: {query}") + logger.info(t("msg-34417720", query=query)) cfg = self.context.get_config(umo=event.unified_msg_origin) # websearch_link = cfg["provider_settings"].get("web_search_link", False) if not cfg.get("provider_settings", {}).get("websearch_bocha_key", []): - raise ValueError("Error: BoCha API key is not configured in AstrBot.") + raise ValueError(t("msg-b798883b")) # build payload payload = { @@ -591,7 +592,7 @@ async def edit_web_search_tools( await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin) aisearch_tool = func_tool_mgr.get_func("AIsearch") if not aisearch_tool: - raise ValueError("Cannot get Baidu AI Search MCP tool.") + raise ValueError(t("msg-22993708")) tool_set.add_tool(aisearch_tool) tool_set.remove_tool("web_search") tool_set.remove_tool("fetch_url") @@ -599,7 +600,7 @@ async def edit_web_search_tools( tool_set.remove_tool("tavily_extract_web_page") tool_set.remove_tool("web_search_bocha") except Exception as e: - logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}") + logger.error(t("msg-6f8d62a4", e=e)) elif provider == "bocha": web_search_bocha = func_tool_mgr.get_func("web_search_bocha") if web_search_bocha: diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index 40c46de79d..21ac1b562b 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -1,4 +1,5 @@ """AstrBot CLI入口""" +from astrbot.core.lang import t import sys @@ -21,9 +22,9 @@ @click.version_option(__version__, prog_name="AstrBot") def cli() -> None: """The AstrBot CLI""" - click.echo(logo_tmpl) - click.echo("Welcome to AstrBot CLI!") - click.echo(f"AstrBot CLI version: {__version__}") + click.echo(t("msg-fe494da6", logo_tmpl=logo_tmpl)) + click.echo(t("msg-c8b2ff67")) + click.echo(t("msg-d79e1ff9", __version__=__version__)) @click.command() @@ -40,13 +41,13 @@ def help(command_name: str | None) -> None: command = cli.get_command(ctx, command_name) if command: # 显示特定命令的帮助信息 - click.echo(command.get_help(ctx)) + click.echo(t("msg-78b9c276", res=command.get_help(ctx))) else: - click.echo(f"Unknown command: {command_name}") + click.echo(t("msg-14dd710d", command_name=command_name)) sys.exit(1) else: # 显示通用帮助信息 - click.echo(cli.get_help(ctx)) + click.echo(t("msg-78b9c276", res=cli.get_help(ctx))) cli.add_command(init) diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index 703c9b8995..46ada7241c 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import hashlib import json import zoneinfo @@ -14,7 +15,7 @@ def _validate_log_level(value: str) -> str: value = value.upper() if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: raise click.ClickException( - "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一", + t("msg-635b8763"), ) return value @@ -24,23 +25,23 @@ def _validate_dashboard_port(value: str) -> int: try: port = int(value) if port < 1 or port > 65535: - raise click.ClickException("端口必须在 1-65535 范围内") + raise click.ClickException(t("msg-ebc250dc")) return port except ValueError: - raise click.ClickException("端口必须是数字") + raise click.ClickException(t("msg-6ec400b6")) def _validate_dashboard_username(value: str) -> str: """验证 Dashboard 用户名""" if not value: - raise click.ClickException("用户名不能为空") + raise click.ClickException(t("msg-0b62b5ce")) return value def _validate_dashboard_password(value: str) -> str: """验证 Dashboard 密码""" if not value: - raise click.ClickException("密码不能为空") + raise click.ClickException(t("msg-89b5d3d5")) return hashlib.md5(value.encode()).hexdigest() @@ -49,14 +50,14 @@ def _validate_timezone(value: str) -> str: try: zoneinfo.ZoneInfo(value) except Exception: - raise click.ClickException(f"无效的时区: {value},请使用有效的IANA时区名称") + raise click.ClickException(t("msg-92e7c8ad", value=value)) return value def _validate_callback_api_base(value: str) -> str: """验证回调接口基址""" if not value.startswith("http://") and not value.startswith("https://"): - raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头") + raise click.ClickException(t("msg-e470e37d")) return value @@ -76,7 +77,7 @@ def _load_config() -> dict[str, Any]: root = get_astrbot_root() if not check_astrbot_root(root): raise click.ClickException( - f"{root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init", + t("msg-6b615721", root=root), ) config_path = root / "data" / "cmd_config.json" @@ -91,7 +92,7 @@ def _load_config() -> dict[str, Any]: try: return json.loads(config_path.read_text(encoding="utf-8-sig")) except json.JSONDecodeError as e: - raise click.ClickException(f"配置文件解析失败: {e!s}") + raise click.ClickException(t("msg-f74c517c", e=e)) def _save_config(config: dict[str, Any]) -> None: @@ -112,7 +113,7 @@ def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None: obj[part] = {} elif not isinstance(obj[part], dict): raise click.ClickException( - f"配置路径冲突: {'.'.join(parts[: parts.index(part) + 1])} 不是字典", + t("msg-d7c58bcc", res='.'.join(parts[:parts.index(part) + 1])), ) obj = obj[part] obj[parts[-1]] = value @@ -152,7 +153,7 @@ def conf() -> None: def set_config(key: str, value: str) -> None: """设置配置项的值""" if key not in CONFIG_VALIDATORS: - raise click.ClickException(f"不支持的配置项: {key}") + raise click.ClickException(t("msg-e16816cc", key=key)) config = _load_config() @@ -162,18 +163,18 @@ def set_config(key: str, value: str) -> None: _set_nested_item(config, key, validated_value) _save_config(config) - click.echo(f"配置已更新: {key}") + click.echo(t("msg-e9cce750", key=key)) if key == "dashboard.password": - click.echo(" 原值: ********") - click.echo(" 新值: ********") + click.echo(t("msg-1ed565aa")) + click.echo(t("msg-1bf9569a")) else: - click.echo(f" 原值: {old_value}") - click.echo(f" 新值: {validated_value}") + click.echo(t("msg-f2a20ab3", old_value=old_value)) + click.echo(t("msg-0c104905", validated_value=validated_value)) except KeyError: - raise click.ClickException(f"未知的配置项: {key}") + raise click.ClickException(t("msg-ea9b4e2c", key=key)) except Exception as e: - raise click.UsageError(f"设置配置失败: {e!s}") + raise click.UsageError(t("msg-4450e3b1", e=e)) @conf.command(name="get") @@ -184,19 +185,19 @@ def get_config(key: str | None = None) -> None: if key: if key not in CONFIG_VALIDATORS: - raise click.ClickException(f"不支持的配置项: {key}") + raise click.ClickException(t("msg-e16816cc", key=key)) try: value = _get_nested_item(config, key) if key == "dashboard.password": value = "********" - click.echo(f"{key}: {value}") + click.echo(t("msg-ba464bee", key=key, value=value)) except KeyError: - raise click.ClickException(f"未知的配置项: {key}") + raise click.ClickException(t("msg-ea9b4e2c", key=key)) except Exception as e: - raise click.UsageError(f"获取配置失败: {e!s}") + raise click.UsageError(t("msg-72aab576", e=e)) else: - click.echo("当前配置:") + click.echo(t("msg-c1693d1d")) for key in CONFIG_VALIDATORS: try: value = ( @@ -204,6 +205,6 @@ def get_config(key: str | None = None) -> None: if key == "dashboard.password" else _get_nested_item(config, key) ) - click.echo(f" {key}: {value}") + click.echo(t("msg-50be9b74", key=key, value=value)) except (KeyError, TypeError): pass diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index 6c0c34b99c..d4bbc92ab1 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio from pathlib import Path @@ -12,9 +13,9 @@ async def initialize_astrbot(astrbot_root: Path) -> None: dot_astrbot = astrbot_root / ".astrbot" if not dot_astrbot.exists(): - click.echo(f"Current Directory: {astrbot_root}") + click.echo(t("msg-a90a250e", astrbot_root=astrbot_root)) click.echo( - "如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。", + t("msg-4deda62e"), ) if click.confirm( f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}", @@ -22,7 +23,7 @@ async def initialize_astrbot(astrbot_root: Path) -> None: abort=True, ): dot_astrbot.touch() - click.echo(f"Created {dot_astrbot}") + click.echo(t("msg-3319bf71", dot_astrbot=dot_astrbot)) paths = { "data": astrbot_root / "data", @@ -33,7 +34,7 @@ async def initialize_astrbot(astrbot_root: Path) -> None: for name, path in paths.items(): path.mkdir(parents=True, exist_ok=True) - click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}") + click.echo(t("msg-7054f44f", res='Created' if not path.exists() else 'Directory exists', path=path)) await check_dashboard(astrbot_root / "data") @@ -41,7 +42,7 @@ async def initialize_astrbot(astrbot_root: Path) -> None: @click.command() def init() -> None: """初始化 AstrBot""" - click.echo("Initializing AstrBot...") + click.echo(t("msg-b19edc8a")) astrbot_root = get_astrbot_root() lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) @@ -50,7 +51,7 @@ def init() -> None: with lock.acquire(): asyncio.run(initialize_astrbot(astrbot_root)) except Timeout: - raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") + raise click.ClickException(t("msg-eebc39e3")) except Exception as e: - raise click.ClickException(f"初始化失败: {e!s}") + raise click.ClickException(t("msg-e16da80f", e=e)) diff --git a/astrbot/cli/commands/cmd_plug.py b/astrbot/cli/commands/cmd_plug.py index 9cf94365af..d8e497360a 100644 --- a/astrbot/cli/commands/cmd_plug.py +++ b/astrbot/cli/commands/cmd_plug.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import re import shutil from pathlib import Path @@ -23,23 +24,22 @@ def _get_data_path() -> Path: base = get_astrbot_root() if not check_astrbot_root(base): raise click.ClickException( - f"{base}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init", + t("msg-cbd8802b", base=base), ) return (base / "data").resolve() def display_plugins(plugins, title=None, color=None) -> None: if title: - click.echo(click.style(title, fg=color, bold=True)) + click.echo(t("msg-78b9c276", res=click.style(title, fg=color, bold=True))) - click.echo(f"{'名称':<20} {'版本':<10} {'状态':<10} {'作者':<15} {'描述':<30}") + click.echo(t("msg-83664fcf", val='名称')) click.echo("-" * 85) for p in plugins: desc = p["desc"][:30] + ("..." if len(p["desc"]) > 30 else "") click.echo( - f"{p['name']:<20} {p['version']:<10} {p['status']:<10} " - f"{p['author']:<15} {desc:<30}", + t("msg-56f3f0bf", res=p['name'], res_2=p['version'], res_3=p['status'], res_4=p['author'], desc=desc), ) @@ -51,24 +51,24 @@ def new(name: str) -> None: plug_path = base_path / "plugins" / name if plug_path.exists(): - raise click.ClickException(f"插件 {name} 已存在") + raise click.ClickException(t("msg-1d802ff2", name=name)) author = click.prompt("请输入插件作者", type=str) desc = click.prompt("请输入插件描述", type=str) version = click.prompt("请输入插件版本", type=str) if not re.match(r"^\d+\.\d+(\.\d+)?$", version.lower().lstrip("v")): - raise click.ClickException("版本号必须为 x.y 或 x.y.z 格式") + raise click.ClickException(t("msg-a7be9d23")) repo = click.prompt("请输入插件仓库:", type=str) if not repo.startswith("http"): - raise click.ClickException("仓库地址必须以 http 开头") + raise click.ClickException(t("msg-4d81299b")) - click.echo("下载插件模板...") + click.echo(t("msg-93289755")) get_git_repo( "https://github.com/Soulter/helloworld", plug_path, ) - click.echo("重写插件信息...") + click.echo(t("msg-b21682dd")) # 重写 metadata.yaml with open(plug_path / "metadata.yaml", "w", encoding="utf-8") as f: f.write( @@ -95,7 +95,7 @@ def new(name: str) -> None: with open(plug_path / "main.py", "w", encoding="utf-8") as f: f.write(new_content) - click.echo(f"插件 {name} 创建成功") + click.echo(t("msg-bffc8bfa", name=name)) @plug.command() @@ -135,7 +135,7 @@ def list(all: bool) -> None: not any([not_published_plugins, need_update_plugins, installed_plugins]) and not all ): - click.echo("未安装任何插件") + click.echo(t("msg-08eae1e3")) @plug.command() @@ -157,7 +157,7 @@ def install(name: str, proxy: str | None) -> None: ) if not plugin: - raise click.ClickException(f"未找到可安装的插件 {name},可能是不存在或已安装") + raise click.ClickException(t("msg-1a021bf4", name=name)) manage_plugin(plugin, plug_path, is_update=False, proxy=proxy) @@ -171,7 +171,7 @@ def remove(name: str) -> None: plugin = next((p for p in plugins if p["name"] == name), None) if not plugin or not plugin.get("local_path"): - raise click.ClickException(f"插件 {name} 不存在或未安装") + raise click.ClickException(t("msg-c120bafd", name=name)) plugin_path = plugin["local_path"] @@ -179,9 +179,9 @@ def remove(name: str) -> None: try: shutil.rmtree(plugin_path) - click.echo(f"插件 {name} 已卸载") + click.echo(t("msg-63da4867", name=name)) except Exception as e: - raise click.ClickException(f"卸载插件 {name} 失败: {e}") + raise click.ClickException(t("msg-e4925708", name=name, e=e)) @plug.command() @@ -204,7 +204,7 @@ def update(name: str, proxy: str | None) -> None: ) if not plugin: - raise click.ClickException(f"插件 {name} 不需要更新或无法更新") + raise click.ClickException(t("msg-f4d15a87", name=name)) manage_plugin(plugin, plug_path, is_update=True, proxy=proxy) else: @@ -213,13 +213,13 @@ def update(name: str, proxy: str | None) -> None: ] if not need_update_plugins: - click.echo("没有需要更新的插件") + click.echo(t("msg-94b035f7")) return - click.echo(f"发现 {len(need_update_plugins)} 个插件需要更新") + click.echo(t("msg-0766d599", res=len(need_update_plugins))) for plugin in need_update_plugins: plugin_name = plugin["name"] - click.echo(f"正在更新插件 {plugin_name}...") + click.echo(t("msg-bd5ab99c", plugin_name=plugin_name)) manage_plugin(plugin, plug_path, is_update=True, proxy=proxy) @@ -239,7 +239,7 @@ def search(query: str) -> None: ] if not matched_plugins: - click.echo(f"未找到匹配 '{query}' 的插件") + click.echo(t("msg-e32912b8", query=query)) return display_plugins(matched_plugins, f"搜索结果: '{query}'", "cyan") diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 23665dff3d..68a5a7262f 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import os import sys @@ -37,7 +38,7 @@ def run(reload: bool, port: str) -> None: if not check_astrbot_root(astrbot_root): raise click.ClickException( - f"{astrbot_root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init", + t("msg-41ecc632", astrbot_root=astrbot_root), ) os.environ["ASTRBOT_ROOT"] = str(astrbot_root) @@ -47,7 +48,7 @@ def run(reload: bool, port: str) -> None: os.environ["DASHBOARD_PORT"] = port if reload: - click.echo("启用插件自动重载") + click.echo(t("msg-0ccaca23")) os.environ["ASTRBOT_RELOAD"] = "1" lock_file = astrbot_root / "astrbot.lock" @@ -55,8 +56,8 @@ def run(reload: bool, port: str) -> None: with lock.acquire(): asyncio.run(run_astrbot(astrbot_root)) except KeyboardInterrupt: - click.echo("AstrBot 已关闭...") + click.echo(t("msg-220914e7")) except Timeout: - raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") + raise click.ClickException(t("msg-eebc39e3")) except Exception as e: - raise click.ClickException(f"运行时出现错误: {e}\n{traceback.format_exc()}") + raise click.ClickException(t("msg-85f241d3", e=e, res=traceback.format_exc())) diff --git a/astrbot/cli/utils/basic.py b/astrbot/cli/utils/basic.py index 5dbe290065..048a8d8cb5 100644 --- a/astrbot/cli/utils/basic.py +++ b/astrbot/cli/utils/basic.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from pathlib import Path import click @@ -30,28 +31,28 @@ async def check_dashboard(astrbot_root: Path) -> None: dashboard_version = await get_dashboard_version() match dashboard_version: case None: - click.echo("未安装管理面板") + click.echo(t("msg-f4e0fd7b")) if click.confirm( "是否安装管理面板?", default=True, abort=True, ): - click.echo("正在安装管理面板...") + click.echo(t("msg-2d090cc3")) await download_dashboard( path="data/dashboard.zip", extract_path=str(astrbot_root), version=f"v{VERSION}", latest=False, ) - click.echo("管理面板安装完成") + click.echo(t("msg-2eeb67e0")) case str(): if VersionComparator.compare_version(VERSION, dashboard_version) <= 0: - click.echo("管理面板已是最新版本") + click.echo(t("msg-9c727dca")) return try: version = dashboard_version.split("v")[1] - click.echo(f"管理面板版本: {version}") + click.echo(t("msg-11b49913", version=version)) await download_dashboard( path="data/dashboard.zip", extract_path=str(astrbot_root), @@ -59,10 +60,10 @@ async def check_dashboard(astrbot_root: Path) -> None: latest=False, ) except Exception as e: - click.echo(f"下载管理面板失败: {e}") + click.echo(t("msg-f0b6145e", e=e)) return except FileNotFoundError: - click.echo("初始化管理面板目录...") + click.echo(t("msg-9504d173")) try: await download_dashboard( path=str(astrbot_root / "dashboard.zip"), @@ -70,7 +71,7 @@ async def check_dashboard(astrbot_root: Path) -> None: version=f"v{VERSION}", latest=False, ) - click.echo("管理面板初始化完成") + click.echo(t("msg-699e2509")) except Exception as e: - click.echo(f"下载管理面板失败: {e}") + click.echo(t("msg-f0b6145e", e=e)) return diff --git a/astrbot/cli/utils/plugin.py b/astrbot/cli/utils/plugin.py index 81f59e0bfc..9809a40c99 100644 --- a/astrbot/cli/utils/plugin.py +++ b/astrbot/cli/utils/plugin.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import shutil import tempfile from enum import Enum @@ -44,10 +45,10 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None: download_url = releases[0]["zipball_url"] else: # 没有 release,使用默认分支 - click.echo(f"正在从默认分支下载 {author}/{repo}") + click.echo(t("msg-e327bc14", author=author, repo=repo)) download_url = f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip" except Exception as e: - click.echo(f"获取 release 信息失败: {e},将直接使用提供的 URL") + click.echo(t("msg-c804f59f", e=e)) download_url = url # 应用代理 @@ -65,7 +66,7 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None: and "archive/refs/heads/master.zip" in download_url ): alt_url = download_url.replace("master.zip", "main.zip") - click.echo("master 分支不存在,尝试下载 main 分支") + click.echo(t("msg-aa398bd5")) resp = client.get(alt_url) resp.raise_for_status() else: @@ -98,7 +99,7 @@ def load_yaml_metadata(plugin_dir: Path) -> dict: try: return yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {} except Exception as e: - click.echo(f"读取 {yaml_path} 失败: {e}", err=True) + click.echo(t("msg-5587d9fb", yaml_path=yaml_path, e=e), err=True) return {} @@ -160,7 +161,7 @@ def build_plug_list(plugins_dir: Path) -> list: }, ) except Exception as e: - click.echo(f"获取在线插件列表失败: {e}", err=True) + click.echo(t("msg-8dbce791", e=e), err=True) # 与在线插件比对,更新状态 online_plugin_names = {plugin["name"] for plugin in online_plugins} @@ -218,7 +219,7 @@ def manage_plugin( # 检查插件是否存在 if is_update and not target_path.exists(): - raise click.ClickException(f"插件 {plugin_name} 未安装,无法更新") + raise click.ClickException(t("msg-6999155d", plugin_name=plugin_name)) # 备份现有插件 if is_update and backup_path is not None and backup_path.exists(): @@ -228,19 +229,19 @@ def manage_plugin( try: click.echo( - f"正在从 {repo_url} {'更新' if is_update else '下载'}插件 {plugin_name}...", + t("msg-fa5e129a", repo_url=repo_url, res='更新' if is_update else '下载', plugin_name=plugin_name), ) get_git_repo(repo_url, target_path, proxy) # 更新成功,删除备份 if is_update and backup_path is not None and backup_path.exists(): shutil.rmtree(backup_path) - click.echo(f"插件 {plugin_name} {'更新' if is_update else '安装'}成功") + click.echo(t("msg-9ac1f4db", plugin_name=plugin_name, res='更新' if is_update else '安装')) except Exception as e: if target_path.exists(): shutil.rmtree(target_path, ignore_errors=True) if is_update and backup_path is not None and backup_path.exists(): shutil.move(backup_path, target_path) raise click.ClickException( - f"{'更新' if is_update else '安装'}插件 {plugin_name} 时出错: {e}", + t("msg-b9c719ae", res='更新' if is_update else '安装', plugin_name=plugin_name, e=e), ) diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index 6400d6fa44..ff39311ed3 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -8,6 +8,7 @@ from astrbot.core.utils.shared_preferences import SharedPreferences from astrbot.core.utils.t2i.renderer import HtmlRenderer +from .lang import t from .log import LogBroker, LogManager # noqa from .utils.astrbot_path import get_astrbot_data_path @@ -17,6 +18,15 @@ DEMO_MODE = os.getenv("DEMO_MODE", False) astrbot_config = AstrBotConfig() +saved_locale = astrbot_config.get("i18n", {}).get("locale", "zh-cn") +normalized_locale = t.normalize_locale(str(saved_locale).lower()) +if normalized_locale is None: + t.load_locale(locale="zh-cn", files=None) + i18n_config = astrbot_config.setdefault("i18n", {}) + i18n_config["locale"] = "zh-cn" + astrbot_config.save_config() +else: + t.load_locale(locale=normalized_locale, files=None) t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img") html_renderer = HtmlRenderer(t2i_base_url) logger = LogManager.GetLogger(log_name="astrbot") diff --git a/astrbot/core/agent/context/compressor.py b/astrbot/core/agent/context/compressor.py index 31a0b0b48d..c07da26a44 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from typing import TYPE_CHECKING, Protocol, runtime_checkable from ..message import Message @@ -220,7 +221,7 @@ async def __call__(self, messages: list[Message]) -> list[Message]: response = await self.provider.text_chat(contexts=llm_payload) summary_content = response.completion_text except Exception as e: - logger.error(f"Failed to generate summary: {e}") + logger.error(t("msg-6c75531b", e=e)) return messages # build result diff --git a/astrbot/core/agent/context/manager.py b/astrbot/core/agent/context/manager.py index 216a3e7e15..30cc5434de 100644 --- a/astrbot/core/agent/context/manager.py +++ b/astrbot/core/agent/context/manager.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot import logger from ..message import Message @@ -76,7 +77,7 @@ async def process( return result except Exception as e: - logger.error(f"Error during context processing: {e}", exc_info=True) + logger.error(t("msg-59241964", e=e), exc_info=True) return messages async def _run_compression( @@ -92,7 +93,7 @@ async def _run_compression( Returns: The compressed/truncated message list. """ - logger.debug("Compress triggered, starting compression...") + logger.debug(t("msg-a0d672dc")) messages = await self.compressor(messages) @@ -102,9 +103,7 @@ async def _run_compression( # calculate compress rate compress_rate = (tokens_after_summary / self.config.max_context_tokens) * 100 logger.info( - f"Compress completed." - f" {prev_tokens} -> {tokens_after_summary} tokens," - f" compression rate: {compress_rate:.2f}%.", + t("msg-e6ef66f0", prev_tokens=prev_tokens, tokens_after_summary=tokens_after_summary, compress_rate=compress_rate), ) # last check @@ -112,7 +111,7 @@ async def _run_compression( messages, tokens_after_summary, self.config.max_context_tokens ): logger.info( - "Context still exceeds max tokens after compression, applying halving truncation..." + t("msg-3fe644eb") ) # still need compress, truncate by half messages = self.truncator.truncate_by_halving(messages) diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index 18f4d47e04..7ba6c2b60a 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import logging from contextlib import AsyncExitStack @@ -25,14 +26,14 @@ from mcp.client.sse import sse_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency, MCP services will be unavailable." + t("msg-6a61ca88") ) try: from mcp.client.streamable_http import streamablehttp_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency or MCP library version too old, Streamable HTTP connection unavailable.", + t("msg-45995cdb"), ) @@ -61,7 +62,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]: elif "type" in cfg: transport_type = cfg["type"] else: - raise Exception("MCP connection config missing transport or type field") + raise Exception(t("msg-2866b896")) async with aiohttp.ClientSession() as session: if transport_type == "streamable_http": @@ -146,20 +147,20 @@ async def connect_to_server(self, mcp_server_config: dict, name: str) -> None: def logging_callback(msg: str) -> None: # Handle MCP service error logs - print(f"MCP Server {name} Error: {msg}") + print(t("msg-3bf7776b", name=name, msg=msg)) self.server_errlogs.append(msg) if "url" in cfg: success, error_msg = await _quick_test_mcp_connection(cfg) if not success: - raise Exception(error_msg) + raise Exception(t("msg-10f72727", error_msg=error_msg)) if "transport" in cfg: transport_type = cfg["transport"] elif "type" in cfg: transport_type = cfg["type"] else: - raise Exception("MCP connection config missing transport or type field") + raise Exception(t("msg-2866b896")) if transport_type != "streamable_http": # SSE transport method @@ -239,7 +240,7 @@ def callback(msg: str) -> None: async def list_tools_and_save(self) -> mcp.ListToolsResult: """List all tools from the server and save them to self.tools""" if not self.session: - raise Exception("MCP Client is not initialized") + raise Exception(t("msg-19c9b509")) response = await self.session.list_tools() self.tools = response.tools return response @@ -256,17 +257,17 @@ async def _reconnect(self) -> None: # Check if already reconnecting (useful for logging) if self._reconnecting: logger.debug( - f"MCP Client {self._server_name} is already reconnecting, skipping" + t("msg-5b9b4918", res=self._server_name) ) return if not self._mcp_server_config or not self._server_name: - raise Exception("Cannot reconnect: missing connection configuration") + raise Exception(t("msg-c1008866")) self._reconnecting = True try: logger.info( - f"Attempting to reconnect to MCP server {self._server_name}..." + t("msg-7c3fe178", res=self._server_name) ) # Save old exit_stack for later cleanup (don't close it now to avoid cancel scope issues) @@ -284,11 +285,11 @@ async def _reconnect(self) -> None: await self.list_tools_and_save() logger.info( - f"Successfully reconnected to MCP server {self._server_name}" + t("msg-783f3b85", res=self._server_name) ) except Exception as e: logger.error( - f"Failed to reconnect to MCP server {self._server_name}: {e}" + t("msg-da7361ff", res=self._server_name, e=e) ) raise finally: @@ -324,7 +325,7 @@ async def call_tool_with_reconnect( ) async def _call_with_retry(): if not self.session: - raise ValueError("MCP session is not available for MCP function tools.") + raise ValueError(t("msg-c0fd612e")) try: return await self.session.call_tool( @@ -334,7 +335,7 @@ async def _call_with_retry(): ) except anyio.ClosedResourceError: logger.warning( - f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect..." + t("msg-8236c58c", tool_name=tool_name) ) # Attempt to reconnect await self._reconnect() @@ -349,7 +350,7 @@ async def cleanup(self) -> None: try: await self.exit_stack.aclose() except Exception as e: - logger.debug(f"Error closing current exit stack: {e}") + logger.debug(t("msg-044046ec", e=e)) # Don't close old exit stacks as they may be in different task contexts # They will be garbage collected naturally diff --git a/astrbot/core/agent/message.py b/astrbot/core/agent/message.py index bde6353ff3..3ec4495a32 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t # Inspired by MoonshotAI/kosong, credits to MoonshotAI/kosong authors for the original implementation. # License: Apache License 2.0 @@ -27,7 +28,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: type_value = getattr(cls, "type", None) if type_value is None or not isinstance(type_value, str): - raise ValueError(invalid_subclass_error_msg) + raise ValueError(t("msg-d38656d7", invalid_subclass_error_msg=invalid_subclass_error_msg)) cls.__content_part_registry[type_value] = cls @@ -47,11 +48,11 @@ def validate_content_part(value: Any) -> Any: if isinstance(value, dict) and "type" in value: type_value: Any | None = cast(dict[str, Any], value).get("type") if not isinstance(type_value, str): - raise ValueError(f"Cannot validate {value} as ContentPart") + raise ValueError(t("msg-42d5a315", value=value)) target_class = cls.__content_part_registry[type_value] return target_class.model_validate(value) - raise ValueError(f"Cannot validate {value} as ContentPart") + raise ValueError(t("msg-42d5a315", value=value)) return core_schema.no_info_plain_validator_function(validate_content_part) @@ -195,7 +196,7 @@ def check_content_required(self): # other all cases: content is required if self.content is None: raise ValueError( - "content is required unless role='assistant' and tool_calls is not None" + t("msg-ffc376d0") ) return self diff --git a/astrbot/core/agent/runners/base.py b/astrbot/core/agent/runners/base.py index 21e7964335..384f299585 100644 --- a/astrbot/core/agent/runners/base.py +++ b/astrbot/core/agent/runners/base.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import abc import typing as T from enum import Enum, auto @@ -61,5 +62,5 @@ def get_final_llm_resp(self) -> LLMResponse | None: def _transition_state(self, new_state: AgentState) -> None: """Transition the agent state.""" if self._state != new_state: - logger.debug(f"Agent state transition: {self._state} -> {new_state}") + logger.debug(t("msg-24eb2b08", res=self._state, new_state=new_state)) self._state = new_state diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index a8300bb711..7164a1a440 100644 --- a/astrbot/core/agent/runners/coze/coze_agent_runner.py +++ b/astrbot/core/agent/runners/coze/coze_agent_runner.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import base64 import json import sys @@ -45,17 +46,17 @@ async def reset( self.api_key = provider_config.get("coze_api_key", "") if not self.api_key: - raise Exception("Coze API Key 不能为空。") + raise Exception(t("msg-448549b0")) self.bot_id = provider_config.get("bot_id", "") if not self.bot_id: - raise Exception("Coze Bot ID 不能为空。") + raise Exception(t("msg-b88724b0")) self.api_base: str = provider_config.get("coze_api_base", "https://api.coze.cn") if not isinstance(self.api_base, str) or not self.api_base.startswith( ("http://", "https://"), ): raise Exception( - "Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。", + t("msg-ea5a135a"), ) self.timeout = provider_config.get("timeout", 120) @@ -75,13 +76,13 @@ async def step(self): 执行 Coze Agent 的一个步骤 """ if not self.req: - raise ValueError("Request is not set. Please call reset() first.") + raise ValueError(t("msg-55333301")) if self._state == AgentState.IDLE: try: await self.agent_hooks.on_agent_begin(self.run_context) except Exception as e: - logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) + logger.error(t("msg-d3b77736", e=e), exc_info=True) # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) @@ -91,7 +92,7 @@ async def step(self): async for response in self._execute_coze_request(): yield response except Exception as e: - logger.error(f"Coze 请求失败:{str(e)}") + logger.error(t("msg-5aa3eb1c", res=str(e))) self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( role="err", completion_text=f"Coze 请求失败:{str(e)}" @@ -99,7 +100,7 @@ async def step(self): yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Coze 请求失败:{str(e)}") + chain=MessageChain().message(t("msg-5aa3eb1c", res=str(e))) ), ) finally: @@ -177,7 +178,7 @@ async def _execute_coze_request(self): } ) except Exception as e: - logger.warning(f"处理上下文图片失败: {e}") + logger.warning(t("msg-333354c6", e=e)) continue if processed_content: @@ -218,7 +219,7 @@ async def _execute_coze_request(self): } ) except Exception as e: - logger.warning(f"处理图片失败 {url}: {e}") + logger.warning(t("msg-2d9e1c08", url=url, e=e)) continue if object_string_content: @@ -282,29 +283,29 @@ async def _execute_coze_request(self): yield AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(content) + chain=MessageChain().message(t("msg-1f50979d", content=content)) ), ) elif event_type == "conversation.message.completed": # 消息完成 - logger.debug("Coze message completed") + logger.debug(t("msg-6fe5588b")) message_started = True elif event_type == "conversation.chat.completed": # 对话完成 - logger.debug("Coze chat completed") + logger.debug(t("msg-d2802f3b")) break elif event_type == "error": # 错误处理 error_msg = data.get("msg", "未知错误") error_code = data.get("code", "UNKNOWN") - logger.error(f"Coze 出现错误: {error_code} - {error_msg}") - raise Exception(f"Coze 出现错误: {error_code} - {error_msg}") + logger.error(t("msg-ba4afcda", error_code=error_code, error_msg=error_msg)) + raise Exception(t("msg-ba4afcda", error_code=error_code, error_msg=error_msg)) if not message_started and not accumulated_content: - logger.warning("Coze 未返回任何内容") + logger.warning(t("msg-ee300f25")) accumulated_content = "" # 创建最终响应 @@ -315,7 +316,7 @@ async def _execute_coze_request(self): try: await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) # 返回最终结果 yield AgentResponse( @@ -340,7 +341,7 @@ async def _download_and_upload_image( if cache_key in self.file_id_cache[session_id]: file_id = self.file_id_cache[session_id][cache_key] - logger.debug(f"[Coze] 使用缓存的 file_id: {file_id}") + logger.debug(t("msg-034c1858", file_id=file_id)) return file_id try: @@ -349,13 +350,13 @@ async def _download_and_upload_image( if session_id: self.file_id_cache[session_id][cache_key] = file_id - logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}") + logger.debug(t("msg-475d8a41", file_id=file_id)) return file_id except Exception as e: - logger.error(f"处理图片失败 {image_url}: {e!s}") - raise Exception(f"处理图片失败: {e!s}") + logger.error(t("msg-696dad99", image_url=image_url, e=e)) + raise Exception(t("msg-7793a347", e=e)) @override def done(self) -> bool: diff --git a/astrbot/core/agent/runners/coze/coze_api_client.py b/astrbot/core/agent/runners/coze/coze_api_client.py index f5799dfbb7..4281531b79 100644 --- a/astrbot/core/agent/runners/coze/coze_api_client.py +++ b/astrbot/core/agent/runners/coze/coze_api_client.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import io import json @@ -66,36 +67,36 @@ async def upload_file( timeout=aiohttp.ClientTimeout(total=60), ) as response: if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception(t("msg-76f97104")) response_text = await response.text() logger.debug( - f"文件上传响应状态: {response.status}, 内容: {response_text}", + t("msg-3653b652", res=response.status, response_text=response_text), ) if response.status != 200: raise Exception( - f"文件上传失败,状态码: {response.status}, 响应: {response_text}", + t("msg-13fe060c", res=response.status, response_text=response_text), ) try: result = await response.json() except json.JSONDecodeError: - raise Exception(f"文件上传响应解析失败: {response_text}") + raise Exception(t("msg-5604b862", response_text=response_text)) if result.get("code") != 0: - raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}") + raise Exception(t("msg-c0373c50", res=result.get('msg', '未知错误'))) file_id = result["data"]["id"] - logger.debug(f"[Coze] 图片上传成功,file_id: {file_id}") + logger.debug(t("msg-010e4299", file_id=file_id)) return file_id except asyncio.TimeoutError: - logger.error("文件上传超时") - raise Exception("文件上传超时") + logger.error(t("msg-719f13cb")) + raise Exception(t("msg-719f13cb")) except Exception as e: - logger.error(f"文件上传失败: {e!s}") - raise Exception(f"文件上传失败: {e!s}") + logger.error(t("msg-121c11fb", e=e)) + raise Exception(t("msg-121c11fb", e=e)) async def download_image(self, image_url: str) -> bytes: """下载图片并返回字节数据 @@ -111,14 +112,14 @@ async def download_image(self, image_url: str) -> bytes: try: async with session.get(image_url) as response: if response.status != 200: - raise Exception(f"下载图片失败,状态码: {response.status}") + raise Exception(t("msg-f6101892", res=response.status)) image_data = await response.read() return image_data except Exception as e: - logger.error(f"下载图片失败 {image_url}: {e!s}") - raise Exception(f"下载图片失败: {e!s}") + logger.error(t("msg-c09c56c9", image_url=image_url, e=e)) + raise Exception(t("msg-15211c7c", e=e)) async def chat_messages( self, @@ -159,7 +160,7 @@ async def chat_messages( if conversation_id: params["conversation_id"] = conversation_id - logger.debug(f"Coze chat_messages payload: {payload}, params: {params}") + logger.debug(t("msg-2245219f", payload=payload, params=params)) try: async with session.post( @@ -169,10 +170,10 @@ async def chat_messages( timeout=aiohttp.ClientTimeout(total=timeout), ) as response: if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception(t("msg-76f97104")) if response.status != 200: - raise Exception(f"Coze API 流式请求失败,状态码: {response.status}") + raise Exception(t("msg-d8fd415c", res=response.status)) # SSE buffer = "" @@ -204,9 +205,9 @@ async def chat_messages( event_data = {"content": data_str} except asyncio.TimeoutError: - raise Exception(f"Coze API 流式请求超时 ({timeout}秒)") + raise Exception(t("msg-f5cc7604", timeout=timeout)) except Exception as e: - raise Exception(f"Coze API 流式请求失败: {e!s}") + raise Exception(t("msg-30c0a9d6", e=e)) async def clear_context(self, conversation_id: str): """清空会话上下文 @@ -226,20 +227,20 @@ async def clear_context(self, conversation_id: str): response_text = await response.text() if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception(t("msg-76f97104")) if response.status != 200: - raise Exception(f"Coze API 请求失败,状态码: {response.status}") + raise Exception(t("msg-11509aba", res=response.status)) try: return json.loads(response_text) except json.JSONDecodeError: - raise Exception("Coze API 返回非JSON格式") + raise Exception(t("msg-002af11d")) except asyncio.TimeoutError: - raise Exception("Coze API 请求超时") + raise Exception(t("msg-c0b8fc7c")) except aiohttp.ClientError as e: - raise Exception(f"Coze API 请求失败: {e!s}") + raise Exception(t("msg-a68a33fa", e=e)) async def get_message_list( self, @@ -274,8 +275,8 @@ async def get_message_list( return await response.json() except Exception as e: - logger.error(f"获取Coze消息列表失败: {e!s}") - raise Exception(f"获取Coze消息列表失败: {e!s}") + logger.error(t("msg-c26e068e", e=e)) + raise Exception(t("msg-c26e068e", e=e)) async def close(self) -> None: """关闭会话""" @@ -297,7 +298,7 @@ async def test_coze_api_client() -> None: with open("README.md", "rb") as f: file_data = f.read() file_id = await client.upload_file(file_data) - print(f"Uploaded file_id: {file_id}") + print(t("msg-5bc0a49d", file_id=file_id)) async for event in client.chat_messages( bot_id=bot_id, user_id="test_user", @@ -316,7 +317,7 @@ async def test_coze_api_client() -> None: ], stream=True, ): - print(f"Event: {event}") + print(t("msg-7c08bdaf", event=event)) finally: await client.close() diff --git a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py index 1aaf6e3b9c..a9b190afd2 100644 --- a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +++ b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import functools import queue @@ -49,13 +50,13 @@ async def reset( self.api_key = provider_config.get("dashscope_api_key", "") if not self.api_key: - raise Exception("阿里云百炼 API Key 不能为空。") + raise Exception(t("msg-dc1a9e6e")) self.app_id = provider_config.get("dashscope_app_id", "") if not self.app_id: - raise Exception("阿里云百炼 APP ID 不能为空。") + raise Exception(t("msg-c492cbbc")) self.dashscope_app_type = provider_config.get("dashscope_app_type", "") if not self.dashscope_app_type: - raise Exception("阿里云百炼 APP 类型不能为空。") + raise Exception(t("msg-bcc8e027")) self.variables: dict = provider_config.get("variables", {}) or {} self.rag_options: dict = provider_config.get("rag_options", {}) @@ -87,13 +88,13 @@ async def step(self): 执行 Dashscope Agent 的一个步骤 """ if not self.req: - raise ValueError("Request is not set. Please call reset() first.") + raise ValueError(t("msg-55333301")) if self._state == AgentState.IDLE: try: await self.agent_hooks.on_agent_begin(self.run_context) except Exception as e: - logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) + logger.error(t("msg-d3b77736", e=e), exc_info=True) # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) @@ -103,7 +104,7 @@ async def step(self): async for response in self._execute_dashscope_request(): yield response except Exception as e: - logger.error(f"阿里云百炼请求失败:{str(e)}") + logger.error(t("msg-e3af4efd", res=str(e))) self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( role="err", completion_text=f"阿里云百炼请求失败:{str(e)}" @@ -111,7 +112,7 @@ async def step(self): yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"阿里云百炼请求失败:{str(e)}") + chain=MessageChain().message(t("msg-e3af4efd", res=str(e))) ), ) @@ -157,11 +158,11 @@ async def _process_stream_chunk( (更新后的output_text, doc_references, AgentResponse或None) """ - logger.debug(f"dashscope stream chunk: {chunk}") + logger.debug(t("msg-fccf5004", chunk=chunk)) if chunk.status_code != 200: logger.error( - f"阿里云百炼请求失败: request_id={chunk.request_id}, code={chunk.status_code}, message={chunk.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code", + t("msg-100d7d7e", res=chunk.request_id, res_2=chunk.status_code, res_3=chunk.message), ) self._transition_state(AgentState.ERROR) error_msg = ( @@ -169,14 +170,14 @@ async def _process_stream_chunk( ) self.final_llm_resp = LLMResponse( role="err", - result_chain=MessageChain().message(error_msg), + result_chain=MessageChain().message(t("msg-10f72727", error_msg=error_msg)), ) return ( output_text, None, AgentResponse( type="err", - data=AgentResponseData(chain=MessageChain().message(error_msg)), + data=AgentResponseData(chain=MessageChain().message(t("msg-10f72727", error_msg=error_msg))), ), ) @@ -189,7 +190,7 @@ async def _process_stream_chunk( output_text += chunk_text response = AgentResponse( type="streaming_delta", - data=AgentResponseData(chain=MessageChain().message(chunk_text)), + data=AgentResponseData(chain=MessageChain().message(t("msg-e8615101", chunk_text=chunk_text))), ) # 获取文档引用 @@ -347,7 +348,7 @@ async def _handle_streaming_response( if self.streaming: yield AgentResponse( type="streaming_delta", - data=AgentResponseData(chain=MessageChain().message(ref_text)), + data=AgentResponseData(chain=MessageChain().message(t("msg-dfb132c4", ref_text=ref_text))), ) # 创建最终响应 @@ -358,7 +359,7 @@ async def _handle_streaming_response( try: await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) # 返回最终结果 yield AgentResponse( @@ -376,7 +377,7 @@ async def _execute_dashscope_request(self): # 检查图片输入 if image_urls: - logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") + logger.warning(t("msg-650b47e1")) # 构建请求payload payload = await self._build_request_payload( diff --git a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py index 50ec7c8262..0fae3cc6a2 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import hashlib import json @@ -116,8 +117,7 @@ async def close(self) -> None: await api_client.close() except Exception as e: logger.warning( - "Failed to close DeerFlowAPIClient during runner shutdown: %s", - e, + t("msg-d5533e66", e=e), exc_info=True, ) @@ -127,7 +127,7 @@ async def _notify_agent_done_hook(self) -> None: try: await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) async def _finish_with_result( self, chain: MessageChain, role: str @@ -145,7 +145,7 @@ async def _finish_with_result( async def _finish_with_error(self, err_msg: str) -> AgentResponse: err_text = f"DeerFlow request failed: {err_msg}" - err_chain = MessageChain().message(err_text) + err_chain = MessageChain().message(t("msg-6ac10910", err_text=err_text)) self.final_llm_resp = LLMResponse( role="err", completion_text=err_text, @@ -166,7 +166,7 @@ def _parse_runner_config(self, provider_config: dict) -> _RunnerConfig: ("http://", "https://"), ): raise ValueError( - "DeerFlow API Base URL format is invalid. It must start with http:// or https://.", + t("msg-e4ca153b"), ) proxy = provider_config.get("proxy", "") @@ -247,7 +247,7 @@ async def _load_config_and_client(self, provider_config: dict) -> None: await old_client.close() except Exception as e: logger.warning( - f"Failed to close previous DeerFlow API client cleanly: {e}" + t("msg-d6691163", e=e) ) self.api_client = DeerFlowAPIClient( @@ -279,7 +279,7 @@ async def reset( @override async def step(self): if not self.req: - raise ValueError("Request is not set. Please call reset() first.") + raise ValueError(t("msg-55333301")) if self.done(): return @@ -287,7 +287,7 @@ async def step(self): try: await self.agent_hooks.on_agent_begin(self.run_context) except Exception as e: - logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) + logger.error(t("msg-d3b77736", e=e), exc_info=True) self._transition_state(AgentState.RUNNING) @@ -299,7 +299,7 @@ async def step(self): raise except Exception as e: err_msg = self._format_exception(e) - logger.error(f"DeerFlow request failed: {err_msg}", exc_info=True) + logger.error(t("msg-940b0a9f", err_msg=err_msg), exc_info=True) yield await self._finish_with_error(err_msg) @override @@ -307,7 +307,7 @@ async def step_until_done( self, max_step: int = 30 ) -> T.AsyncGenerator[AgentResponse, None]: if max_step <= 0: - raise ValueError("max_step must be greater than 0") + raise ValueError(t("msg-20f437c9")) step_count = 0 while not self.done() and step_count < max_step: @@ -317,7 +317,7 @@ async def step_until_done( if not self.done(): raise RuntimeError( - f"DeerFlow agent reached max_step ({max_step}) without completion." + t("msg-adeda135", max_step=max_step) ) def _extract_new_messages_from_values( @@ -382,7 +382,7 @@ async def _ensure_thread_id(self, session_id: str) -> str: thread_id = thread.get("thread_id", "") if not thread_id: raise Exception( - f"DeerFlow create thread returned invalid payload: {thread}" + t("msg-7449f8a7", thread=thread) ) await sp.put_async( @@ -467,7 +467,7 @@ def _update_text_and_maybe_stream( return [ AgentResponse( type="streaming_delta", - data=AgentResponseData(chain=MessageChain().message(delta)), + data=AgentResponseData(chain=MessageChain().message(t("msg-3bde4a11", delta=delta))), ) ] @@ -478,7 +478,7 @@ def _update_text_and_maybe_stream( AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(delta_text) + chain=MessageChain().message(t("msg-6c9836cd", delta_text=delta_text)) ), ) ] @@ -581,7 +581,7 @@ def _build_final_result(self, state: _StreamState) -> _FinalResult: failures_only = True if not final_chain.chain: - logger.warning("DeerFlow returned no text content in stream events.") + logger.warning(t("msg-e6e01cca")) final_chain = MessageChain( chain=[Comp.Plain("DeerFlow returned an empty response.")], ) @@ -662,15 +662,13 @@ async def _execute_deerflow_request(self): continue if event_type == "error": - raise Exception(f"DeerFlow stream returned error event: {data}") + raise Exception(t("msg-1a5b13c5", data=data)) if event_type == "end": break except (asyncio.TimeoutError, TimeoutError): logger.warning( - "DeerFlow stream timed out after %ss for thread_id=%s; returning partial result.", - self.timeout, - thread_id, + t("msg-298cca9c", res=self.timeout, thread_id=thread_id), ) state.timed_out = True diff --git a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py index 37a23f2432..a1e2db4cb3 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import codecs import json from collections.abc import AsyncGenerator @@ -72,9 +73,7 @@ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict[str, Any], No if len(buffer) > SSE_MAX_BUFFER_CHARS: logger.warning( - "DeerFlow SSE parser buffer exceeded %d chars without delimiter; " - "flushing oversized block to prevent unbounded memory growth.", - SSE_MAX_BUFFER_CHARS, + t("msg-8f689453", SSE_MAX_BUFFER_CHARS=SSE_MAX_BUFFER_CHARS), ) parsed = _parse_sse_block(buffer) if parsed is not None: @@ -123,7 +122,7 @@ def __init__( def _get_session(self) -> ClientSession: if self._closed: - raise RuntimeError("DeerFlowAPIClient is already closed.") + raise RuntimeError(t("msg-d1db013a")) if self._session is None or self._session.closed: self._session = ClientSession(trust_env=True) return self._session @@ -153,7 +152,7 @@ async def create_thread(self, timeout: float = 20) -> dict[str, Any]: if resp.status not in (200, 201): text = await resp.text() raise Exception( - f"DeerFlow create thread failed: {resp.status}. {text}", + t("msg-8b9e7967", res=resp.status, text=text), ) return await resp.json() @@ -173,11 +172,7 @@ async def stream_run( message_count = len(input_payload["messages"]) # Log only a minimal summary to avoid exposing sensitive user content. logger.debug( - "deerflow stream_run payload summary: thread_id=%s, keys=%s, message_count=%d, stream_mode=%s", - thread_id, - list(payload.keys()), - message_count, - payload.get("stream_mode"), + t("msg-93a10841", thread_id=thread_id, res=list(payload.keys()), message_count=message_count, res_2=payload.get('stream_mode')), ) # For long-running SSE streams, avoid aiohttp total timeout. # Use socket read timeout so active heartbeats/chunks can keep the stream alive. @@ -201,7 +196,7 @@ async def stream_run( if resp.status != 200: text = await resp.text() raise Exception( - f"DeerFlow runs/stream request failed: {resp.status}. {text}", + t("msg-9a9d9119", res=resp.status, text=text), ) async for event in _stream_sse(resp): yield event @@ -221,8 +216,7 @@ async def close(self) -> None: await session.close() except Exception as e: logger.warning( - "Failed to close DeerFlowAPIClient session cleanly: %s", - e, + t("msg-7746c84c", e=e), exc_info=True, ) finally: @@ -236,8 +230,7 @@ def __del__(self) -> None: if closed or session is None or session.closed: return logger.warning( - "DeerFlowAPIClient garbage collected with unclosed session; " - "explicit close() should be called by runner lifecycle (or `async with`)." + t("msg-e15f3d95") ) @property diff --git a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py index 2477adbb92..fe442640e8 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import base64 from collections.abc import Callable from typing import Any @@ -42,14 +43,13 @@ def build_user_content(prompt: str, image_urls: list[str]) -> Any: if not isinstance(url, str): skipped_invalid_images += 1 logger.debug( - "Skipped DeerFlow image input because value is not a string: %r", - type(image_url).__name__, + t("msg-3958eaa0", res=type(image_url).__name__), ) continue url = url.strip() if not url: skipped_invalid_images += 1 - logger.debug("Skipped DeerFlow image input because value is empty.") + logger.debug(t("msg-582f6f32")) continue if url.startswith(("http://", "https://", "data:")): content.append({"type": "image_url", "image_url": {"url": url}}) @@ -58,7 +58,7 @@ def build_user_content(prompt: str, image_urls: list[str]) -> Any: if not is_likely_base64_image(url): skipped_invalid_images += 1 logger.debug( - "Skipped DeerFlow image input because it is neither URL/data URI nor valid base64." + t("msg-935c7c66") ) continue compact_base64 = url.replace("\n", "").replace("\r", "") @@ -79,17 +79,14 @@ def build_user_content(prompt: str, image_urls: list[str]) -> Any: content.insert(0, {"type": "text", "text": note_text}) if not any_valid_image: logger.warning( - "All %d provided DeerFlow image inputs were rejected as invalid or unsupported.", - skipped_invalid_images, + t("msg-764cafe0", skipped_invalid_images=skipped_invalid_images), ) else: logger.info( - "%d DeerFlow image input(s) were rejected as invalid or unsupported.", - skipped_invalid_images, + t("msg-7d6f7e4d", skipped_invalid_images=skipped_invalid_images), ) logger.debug( - "Skipped %d DeerFlow image inputs that were neither URL/data URI nor valid base64.", - skipped_invalid_images, + t("msg-67438dc2", skipped_invalid_images=skipped_invalid_images), ) return content diff --git a/astrbot/core/agent/runners/dify/dify_agent_runner.py b/astrbot/core/agent/runners/dify/dify_agent_runner.py index 93f8d3570d..461ee32205 100644 --- a/astrbot/core/agent/runners/dify/dify_agent_runner.py +++ b/astrbot/core/agent/runners/dify/dify_agent_runner.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import base64 import os import sys @@ -68,13 +69,13 @@ async def step(self): 执行 Dify Agent 的一个步骤 """ if not self.req: - raise ValueError("Request is not set. Please call reset() first.") + raise ValueError(t("msg-55333301")) if self._state == AgentState.IDLE: try: await self.agent_hooks.on_agent_begin(self.run_context) except Exception as e: - logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) + logger.error(t("msg-d3b77736", e=e), exc_info=True) # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) @@ -84,7 +85,7 @@ async def step(self): async for response in self._execute_dify_request(): yield response except Exception as e: - logger.error(f"Dify 请求失败:{str(e)}") + logger.error(t("msg-0d493427", res=str(e))) self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( role="err", completion_text=f"Dify 请求失败:{str(e)}" @@ -92,7 +93,7 @@ async def step(self): yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Dify 请求失败:{str(e)}") + chain=MessageChain().message(t("msg-0d493427", res=str(e))) ), ) finally: @@ -133,10 +134,10 @@ async def _execute_dify_request(self): mime_type="image/png", file_name="image.png", ) - logger.debug(f"Dify 上传图片响应:{file_response}") + logger.debug(t("msg-fe594f21", file_response=file_response)) if "id" not in file_response: logger.warning( - f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" + t("msg-3534b306", file_response=file_response) ) continue files_payload.append( @@ -147,7 +148,7 @@ async def _execute_dify_request(self): } ) except Exception as e: - logger.warning(f"上传图片失败:{e}") + logger.warning(t("msg-08441fdf", e=e)) continue # 获得会话变量 @@ -178,7 +179,7 @@ async def _execute_dify_request(self): files=files_payload, timeout=self.timeout, ): - logger.debug(f"dify resp chunk: {chunk}") + logger.debug(t("msg-3972f693", chunk=chunk)) if chunk["event"] == "message" or chunk["event"] == "agent_message": result += chunk["answer"] if not conversation_id: @@ -199,12 +200,12 @@ async def _execute_dify_request(self): ), ) elif chunk["event"] == "message_end": - logger.debug("Dify message end") + logger.debug(t("msg-6c74267b")) break elif chunk["event"] == "error": - logger.error(f"Dify 出现错误:{chunk}") + logger.error(t("msg-1ce260ba", chunk=chunk)) raise Exception( - f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}" + t("msg-a12417dd", res=chunk['status'], res_2=chunk['message']) ) case "workflow": @@ -218,15 +219,15 @@ async def _execute_dify_request(self): files=files_payload, timeout=self.timeout, ): - logger.debug(f"dify workflow resp chunk: {chunk}") + logger.debug(t("msg-f8530ee9", chunk=chunk)) match chunk["event"]: case "workflow_started": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。" + t("msg-386a282e", res=chunk['workflow_run_id']) ) case "node_finished": logger.debug( - f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。" + t("msg-0bc1299b", res=chunk['data']['node_id'], res_2=chunk['data'].get('title', '')) ) case "text_chunk": if self.streaming and chunk["data"]["text"]: @@ -240,26 +241,26 @@ async def _execute_dify_request(self): ) case "workflow_finished": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束" + t("msg-5cf24248", res=chunk['workflow_run_id']) ) - logger.debug(f"Dify 工作流结果:{chunk}") + logger.debug(t("msg-e2c2159f", chunk=chunk)) if chunk["data"]["error"]: logger.error( - f"Dify 工作流出现错误:{chunk['data']['error']}" + t("msg-4fa60ef1", res=chunk['data']['error']) ) raise Exception( - f"Dify 工作流出现错误:{chunk['data']['error']}" + t("msg-4fa60ef1", res=chunk['data']['error']) ) if self.workflow_output_key not in chunk["data"]["outputs"]: raise Exception( - f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}" + t("msg-1f786836", res=self.workflow_output_key) ) result = chunk case _: - raise Exception(f"未知的 Dify API 类型:{self.api_type}") + raise Exception(t("msg-c4a70ffb", res=self.api_type)) if not result: - logger.warning("Dify 请求结果为空,请查看 Debug 日志。") + logger.warning(t("msg-51d321fd")) # 解析结果 chain = await self.parse_dify_result(result) @@ -271,7 +272,7 @@ async def _execute_dify_request(self): try: await self.agent_hooks.on_agent_done(self.run_context, self.final_llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) # 返回最终结果 yield AgentResponse( diff --git a/astrbot/core/agent/runners/dify/dify_api_client.py b/astrbot/core/agent/runners/dify/dify_api_client.py index 26da6dfe9a..403432f732 100644 --- a/astrbot/core/agent/runners/dify/dify_api_client.py +++ b/astrbot/core/agent/runners/dify/dify_api_client.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import codecs import json from collections.abc import AsyncGenerator @@ -19,7 +20,7 @@ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]: try: yield json.loads(block[5:]) except json.JSONDecodeError: - logger.warning(f"Drop invalid dify json data: {block[5:]}") + logger.warning(t("msg-cd6cd7ac", res=block[5:])) continue # flush any remaining text buffer += decoder.decode(b"", final=True) @@ -27,7 +28,7 @@ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]: try: yield json.loads(buffer[5:]) except json.JSONDecodeError: - logger.warning(f"Drop invalid dify json data: {buffer[5:]}") + logger.warning(t("msg-cd6cd7ac", res=buffer[5:])) class DifyAPIClient: @@ -55,7 +56,7 @@ async def chat_messages( payload = locals() payload.pop("self") payload.pop("timeout") - logger.info(f"chat_messages payload: {payload}") + logger.info(t("msg-3654a12d", payload=payload)) async with self.session.post( url, json=payload, @@ -65,7 +66,7 @@ async def chat_messages( if resp.status != 200: text = await resp.text() raise Exception( - f"Dify /chat-messages 接口请求失败:{resp.status}. {text}", + t("msg-8e865c52", res=resp.status, text=text), ) async for event in _stream_sse(resp): yield event @@ -84,7 +85,7 @@ async def workflow_run( payload = locals() payload.pop("self") payload.pop("timeout") - logger.info(f"workflow_run payload: {payload}") + logger.info(t("msg-2d7534b8", payload=payload)) async with self.session.post( url, json=payload, @@ -94,7 +95,7 @@ async def workflow_run( if resp.status != 200: text = await resp.text() raise Exception( - f"Dify /workflows/run 接口请求失败:{resp.status}. {text}", + t("msg-89918ba5", res=resp.status, text=text), ) async for event in _stream_sse(resp): yield event @@ -143,7 +144,7 @@ async def file_upload( content_type=mime_type or "application/octet-stream", ) else: - raise ValueError("file_path 和 file_data 不能同时为 None") + raise ValueError(t("msg-8bf17938")) async with self.session.post( url, @@ -152,7 +153,7 @@ async def file_upload( ) as resp: if resp.status != 200 and resp.status != 201: text = await resp.text() - raise Exception(f"Dify 文件上传失败:{resp.status}. {text}") + raise Exception(t("msg-b6ee8f38", res=resp.status, text=text)) return await resp.json() # {"id": "xxx", ...} async def close(self) -> None: diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 743b280070..eae35f86f0 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -19,6 +19,7 @@ from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart from astrbot.core.agent.tool import ToolSet from astrbot.core.agent.tool_image_cache import tool_image_cache +from astrbot.core.lang import t from astrbot.core.message.components import Json from astrbot.core.message.message_event_result import ( MessageChain, @@ -233,9 +234,11 @@ async def _iter_llm_responses_with_fallback( is_last_candidate = idx == total_candidates - 1 if idx > 0: logger.warning( - "Switched from %s to fallback chat provider: %s", - self.provider.provider_config.get("id", ""), - candidate_id, + t( + "msg-ec018aef", + res=self.provider.provider_config.get("id", ""), + candidate_id=candidate_id, + ), ) self.provider = candidate has_stream_output = False @@ -253,8 +256,7 @@ async def _iter_llm_responses_with_fallback( ): last_err_response = resp logger.warning( - "Chat Model %s returns error response, trying fallback to next provider.", - candidate_id, + t("msg-24b29511", candidate_id=candidate_id), ) break @@ -266,9 +268,7 @@ async def _iter_llm_responses_with_fallback( except Exception as exc: # noqa: BLE001 last_exception = exc logger.warning( - "Chat Model %s request error: %s", - candidate_id, - exc, + t("msg-9af066fa", candidate_id=candidate_id, exc=exc), exc_info=True, ) continue @@ -294,7 +294,7 @@ def _simple_print_message_role(self, tag: str = ""): roles = [] for message in self.run_context.messages: roles.append(message.role) - logger.debug(f"{tag} RunCtx.messages -> [{len(roles)}] {','.join(roles)}") + logger.debug(t("msg-81b2aeae", tag=tag, res=len(roles), res_2=",".join(roles))) def follow_up( self, @@ -351,13 +351,13 @@ async def step(self): This method should return the result of the step. """ if not self.req: - raise ValueError("Request is not set. Please call reset() first.") + raise ValueError(t("msg-55333301")) if self._state == AgentState.IDLE: try: await self.agent_hooks.on_agent_begin(self.run_context) except Exception as e: - logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) + logger.error(t("msg-d3b77736", e=e), exc_info=True) # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) @@ -423,7 +423,7 @@ async def step(self): return if self._stop_requested: - logger.info("Agent execution was requested to stop by user.") + logger.info(t("msg-61de315c")) llm_resp = llm_resp_result if llm_resp.role != "assistant": llm_resp = LLMResponse( @@ -453,7 +453,7 @@ async def step(self): try: await self.agent_hooks.on_agent_done(self.run_context, llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) yield AgentResponse( type="aborted", @@ -478,7 +478,7 @@ async def step(self): yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(error_text), + chain=MessageChain().message(t("msg-76945a59", error_text=error_text)), ), ) return @@ -501,16 +501,14 @@ async def step(self): if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) if len(parts) == 0: - logger.warning( - "LLM returned empty assistant message with no tool calls." - ) + logger.warning(t("msg-ed80313d")) self.run_context.messages.append(Message(role="assistant", content=parts)) # call the on_agent_done hook try: await self.agent_hooks.on_agent_done(self.run_context, llm_resp) except Exception as e: - logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) + logger.error(t("msg-8eb53be3", e=e), exc_info=True) self._resolve_unconsumed_follow_ups() # 返回 LLM 结果 @@ -612,9 +610,7 @@ async def step(self): self.run_context.messages.append( Message(role="user", content=image_parts) ) - logger.debug( - f"Appended {len(cached_images)} cached image(s) to context for LLM review" - ) + logger.debug(t("msg-970947ae", res=len(cached_images))) self.req.append_tool_calls_result(tool_calls_result) @@ -630,9 +626,7 @@ async def step_until_done( # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step if not self.done(): - logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." - ) + logger.warning(t("msg-6b326889", max_step=max_step)) # 拔掉所有工具 if self.req: self.req.func_tool = None @@ -654,7 +648,7 @@ async def _handle_function_tools( ) -> T.AsyncGenerator[_HandleFunctionToolsResult, None]: """处理函数工具调用。""" tool_call_result_blocks: list[ToolCallMessageSegment] = [] - logger.info(f"Agent 使用工具: {llm_response.tools_call_name}") + logger.info(t("msg-948ea4b7", res=llm_response.tools_call_name)) def _append_tool_call_result(tool_call_id: str, content: str) -> None: tool_call_result_blocks.append( @@ -700,10 +694,16 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: else: func_tool = req.func_tool.get_tool(func_tool_name) - logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") + logger.info( + t( + "msg-a27ad3d1", + func_tool_name=func_tool_name, + func_tool_args=func_tool_args, + ) + ) if not func_tool: - logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。") + logger.warning(t("msg-812ad241", func_tool_name=func_tool_name)) _append_tool_call_result( func_tool_id, f"error: Tool {func_tool_name} not found.", @@ -715,7 +715,11 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: # 获取实际的 handler 函数 if func_tool.handler: logger.debug( - f"工具 {func_tool_name} 期望的参数: {func_tool.parameters}", + t( + "msg-20b4f143", + func_tool_name=func_tool_name, + res=func_tool.parameters, + ), ) if func_tool.parameters and func_tool.parameters.get("properties"): expected_params = set(func_tool.parameters["properties"].keys()) @@ -732,7 +736,11 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: ) if ignored_params: logger.warning( - f"工具 {func_tool_name} 忽略非期望参数: {ignored_params}", + t( + "msg-78f6833c", + func_tool_name=func_tool_name, + ignored_params=ignored_params, + ), ) else: # 如果没有 handler(如 MCP 工具),使用所有参数 @@ -745,7 +753,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: valid_params, ) except Exception as e: - logger.error(f"Error in on_tool_start hook: {e}", exc_info=True) + logger.error(t("msg-2b523f8c", e=e), exc_info=True) executor = self.tool_executor.execute( tool=func_tool, @@ -826,9 +834,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: # Tool 直接请求发送消息给用户 # 这里我们将直接结束 Agent Loop # 发送消息逻辑在 ToolExecutor 中处理了 - logger.warning( - f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。" - ) + logger.warning(t("msg-ec868b73", func_tool_name=func_tool_name)) self._transition_state(AgentState.DONE) self.stats.end_time = time.time() _append_tool_call_result( @@ -838,7 +844,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: else: # 不应该出现其他类型 logger.warning( - f"Tool 返回了不支持的类型: {type(resp)}。", + t("msg-6b61e4f1", res=type(resp)), ) _append_tool_call_result( func_tool_id, @@ -853,9 +859,9 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: _final_resp, ) except Exception as e: - logger.error(f"Error in on_tool_end hook: {e}", exc_info=True) + logger.error(t("msg-34c13e02", e=e), exc_info=True) except Exception as e: - logger.warning(traceback.format_exc()) + logger.warning(t("msg-78b9c276", res=traceback.format_exc())) _append_tool_call_result( func_tool_id, f"error: {e!s}", @@ -878,7 +884,13 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: ], ) ) - logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}") + logger.info( + t( + "msg-a1493b6d", + func_tool_name=func_tool_name, + last_tcr_content=last_tcr_content, + ) + ) # 处理函数调用响应 if tool_call_result_blocks: diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index c2536708e6..69475b7ad4 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import copy from collections.abc import AsyncGenerator, Awaitable, Callable from typing import Any, Generic @@ -70,7 +71,7 @@ def __repr__(self) -> str: async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult: """Run the tool with the given arguments. The handler field has priority.""" raise NotImplementedError( - "FunctionTool.call() must be implemented by subclasses or set a handler." + t("msg-983bc802") ) diff --git a/astrbot/core/agent/tool_image_cache.py b/astrbot/core/agent/tool_image_cache.py index 72e22dd52e..f091e8f748 100644 --- a/astrbot/core/agent/tool_image_cache.py +++ b/astrbot/core/agent/tool_image_cache.py @@ -2,6 +2,7 @@ This module allows LLM to review images before deciding whether to send them to users. """ +from astrbot.core.lang import t import base64 import os @@ -52,7 +53,7 @@ def __init__(self) -> None: self._initialized = True self._cache_dir = os.path.join(get_astrbot_temp_path(), self.CACHE_DIR_NAME) os.makedirs(self._cache_dir, exist_ok=True) - logger.debug(f"ToolImageCache initialized, cache dir: {self._cache_dir}") + logger.debug(t("msg-45da4af7", res=self._cache_dir)) def _get_file_extension(self, mime_type: str) -> str: """Get file extension from MIME type.""" @@ -96,9 +97,9 @@ def save_image( image_bytes = base64.b64decode(base64_data) with open(file_path, "wb") as f: f.write(image_bytes) - logger.debug(f"Saved tool image to: {file_path}") + logger.debug(t("msg-017bde96", file_path=file_path)) except Exception as e: - logger.error(f"Failed to save tool image: {e}") + logger.error(t("msg-29398f55", e=e)) raise return CachedImage( @@ -129,7 +130,7 @@ def get_image_base64_by_path( base64_data = base64.b64encode(image_bytes).decode("utf-8") return base64_data, mime_type except Exception as e: - logger.error(f"Failed to read cached image {file_path}: {e}") + logger.error(t("msg-128aa08a", file_path=file_path, e=e)) return None def cleanup_expired(self) -> int: @@ -150,10 +151,10 @@ def cleanup_expired(self) -> int: os.remove(file_path) cleaned += 1 except Exception as e: - logger.warning(f"Error during cache cleanup: {e}") + logger.warning(t("msg-3c111d1f", e=e)) if cleaned: - logger.info(f"Cleaned up {cleaned} expired cached images") + logger.info(t("msg-eeb1b849", cleaned=cleaned)) return cleaned diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index dd65f92e69..fdf1317c1c 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import re import time @@ -103,7 +104,7 @@ async def run_agent( if step_idx == max_step + 1: logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + t("msg-6b326889", max_step=max_step) ) if not agent_runner.done(): # 拔掉所有工具 @@ -160,7 +161,7 @@ async def run_agent( msg_chain, tool_name_by_call_id ) await astr_event.send( - MessageChain(type="tool_call").message(status_msg) + MessageChain(type="tool_call").message(t("msg-bb15e9c7", status_msg=status_msg)) ) # 对于其他情况,暂时先不处理 continue @@ -236,7 +237,7 @@ async def run_agent( await stop_watcher except asyncio.CancelledError: pass - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) custom_error_message = extract_persona_custom_error_message_from_event( astr_event @@ -259,12 +260,12 @@ async def run_agent( agent_runner.run_context, error_llm_response ) except Exception: - logger.exception("Error in on_agent_done hook") + logger.exception(t("msg-9c246298")) if agent_runner.streaming: - yield MessageChain().message(err_msg) + yield MessageChain().message(t("msg-34f164d4", err_msg=err_msg)) else: - astr_event.set_result(MessageEventResult().message(err_msg)) + astr_event.set_result(MessageEventResult().message(t("msg-34f164d4", err_msg=err_msg))) return @@ -312,11 +313,10 @@ async def run_live_agent( support_stream = tts_provider.support_stream() if support_stream: - logger.info("[Live Agent] 使用流式 TTS(原生支持 get_audio_stream)") + logger.info(t("msg-6d9553b2")) else: logger.info( - f"[Live Agent] 使用 TTS({tts_provider.meta().type} " - "使用 get_audio,将按句子分块生成音频)" + t("msg-becf71bf", res=tts_provider.meta().type) ) # 统计数据初始化 @@ -381,7 +381,7 @@ async def run_live_agent( yield chain except Exception as e: - logger.error(f"[Live Agent] 运行时发生错误: {e}", exc_info=True) + logger.error(t("msg-21723afb", e=e), exc_info=True) finally: # 清理任务 if not feeder_task.done(): @@ -415,7 +415,7 @@ async def run_live_agent( ) ) except Exception as e: - logger.error(f"发送 TTS 统计信息失败: {e}") + logger.error(t("msg-ca1bf0d7", e=e)) async def _run_agent_feeder( @@ -461,7 +461,7 @@ async def _run_agent_feeder( if len(temp_buffer) >= 10: if temp_buffer.strip(): - logger.info(f"[Live Agent Feeder] 分句: {temp_buffer}") + logger.info(t("msg-5ace3d96", temp_buffer=temp_buffer)) await text_queue.put(temp_buffer) temp_buffer = "" @@ -473,7 +473,7 @@ async def _run_agent_feeder( await text_queue.put(buffer) except Exception as e: - logger.error(f"[Live Agent Feeder] Error: {e}", exc_info=True) + logger.error(t("msg-bc1826ea", e=e), exc_info=True) finally: # 发送结束信号 await text_queue.put(None) @@ -488,7 +488,7 @@ async def _safe_tts_stream_wrapper( try: await tts_provider.get_audio_stream(text_queue, audio_queue) except Exception as e: - logger.error(f"[Live TTS Stream] Error: {e}", exc_info=True) + logger.error(t("msg-a92774c9", e=e), exc_info=True) finally: await audio_queue.put(None) @@ -514,11 +514,11 @@ async def _simulated_stream_tts( await audio_queue.put((text, audio_data)) except Exception as e: logger.error( - f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}" + t("msg-d7b3bbae", res=text[:20], e=e) ) # 继续处理下一句 except Exception as e: - logger.error(f"[Live TTS Simulated] Critical Error: {e}", exc_info=True) + logger.error(t("msg-035bca5f", e=e), exc_info=True) finally: await audio_queue.put(None) diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 0dc8b9eeb7..b4bf664228 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import inspect import json @@ -159,7 +160,7 @@ async def _run_in_background() -> None: ) except Exception as e: # noqa: BLE001 logger.error( - f"Background task {task_id} failed: {e!s}", + t("msg-e5f2fb34", task_id=task_id, e=e), exc_info=True, ) @@ -336,7 +337,7 @@ async def _run_handoff_in_background() -> None: ) except Exception as e: # noqa: BLE001 logger.error( - f"Background handoff {task_id} ({tool.name}) failed: {e!s}", + t("msg-c54b2335", task_id=task_id, res=tool.name, e=e), exc_info=True, ) @@ -520,7 +521,7 @@ async def _wake_main_agent_for_background_result( event=cron_event, plugin_context=ctx, config=config, req=req ) if not result: - logger.error(f"Failed to build main agent for background task {tool_name}.") + logger.error(t("msg-8c2fe51d", tool_name=tool_name)) return runner = result.agent_runner @@ -545,7 +546,7 @@ async def _wake_main_agent_for_background_result( summary_note=summary_note, ) if not llm_resp: - logger.warning("background task agent got no response") + logger.warning(t("msg-c6d4e4a6")) return @classmethod @@ -559,7 +560,7 @@ async def _execute_local( ): event = run_context.context.event if not event: - raise ValueError("Event must be provided for local function tools.") + raise ValueError(t("msg-0b3711f1")) is_override_call = False for ty in type(tool).mro(): @@ -569,7 +570,7 @@ async def _execute_local( # 检查 tool 下有没有 run 方法 if not tool.handler and not hasattr(tool, "run") and not is_override_call: - raise ValueError("Tool must have a valid handler or override 'run' method.") + raise ValueError(t("msg-8c19e27a")) awaitable = None method_name = "" @@ -583,7 +584,7 @@ async def _execute_local( awaitable = getattr(tool, "run") method_name = "run" if awaitable is None: - raise ValueError("Tool must have a valid handler or override 'run' method.") + raise ValueError(t("msg-8c19e27a")) wrapper = call_local_llm_tool( context=run_context, @@ -621,13 +622,13 @@ async def _execute_local( ) except Exception as e: logger.error( - f"Tool 直接发送消息失败: {e}", + t("msg-24053a5f", e=e), exc_info=True, ) yield None except asyncio.TimeoutError: raise Exception( - f"tool {tool.name} execution timeout after {tool_call_timeout or run_context.tool_call_timeout} seconds.", + t("msg-f940b51e", res=tool.name, res_2=tool_call_timeout or run_context.tool_call_timeout), ) except StopAsyncIteration: break @@ -669,9 +670,9 @@ async def call_local_llm_tool( elif method_name == "call": ready_to_call = handler(context, *args, **kwargs) else: - raise ValueError(f"未知的方法名: {method_name}") + raise ValueError(t("msg-7e22fc8e", method_name=method_name)) except ValueError as e: - raise Exception(f"Tool execution ValueError: {e}") from e + raise Exception(t("msg-c285315c", e=e)) from e except TypeError as e: # 获取函数的签名(包括类型),除了第一个 event/context 参数。 try: @@ -702,11 +703,11 @@ async def call_local_llm_tool( handler_param_str = "(unable to inspect signature)" raise Exception( - f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}" + t("msg-41366b74", handler_param_str=handler_param_str, e=e) ) from e except Exception as e: trace_ = traceback.format_exc() - raise Exception(f"Tool execution error: {e}. Traceback: {trace_}") from e + raise Exception(t("msg-e8cadf8e", e=e, trace_=trace_)) from e if not ready_to_call: return @@ -730,7 +731,7 @@ async def call_local_llm_tool( # 如果这个异步生成器没有执行到 yield 分支 yield except Exception as e: - logger.error(f"Previous Error: {trace_}") + logger.error(t("msg-d7b4aa84", trace_=trace_)) raise e elif inspect.iscoroutine(ready_to_call): # 如果只是一个协程, 直接执行 diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 0f51a29c05..24234805ba 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -1,4 +1,5 @@ from __future__ import annotations +from astrbot.core.lang import t import asyncio import copy @@ -156,17 +157,17 @@ def _select_provider( if sel_provider and isinstance(sel_provider, str): provider = plugin_context.get_provider_by_id(sel_provider) if not provider: - logger.error("未找到指定的提供商: %s。", sel_provider) + logger.error(t("msg-3d3f3df8", sel_provider=sel_provider)) if not isinstance(provider, Provider): logger.error( - "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) + t("msg-23d02c04", res=type(provider)) ) return None return provider try: return plugin_context.get_using_provider(umo=event.unified_msg_origin) except ValueError as exc: - logger.error("Error occurred while selecting provider: %s", exc) + logger.error(t("msg-97d98ea8", exc=exc)) return None @@ -183,7 +184,7 @@ async def _get_session_conv( cid = await conv_mgr.new_conversation(umo, event.get_platform_id()) conversation = await conv_mgr.get_conversation(umo, cid) if not conversation: - raise RuntimeError("无法创建新的对话。") + raise RuntimeError(t("msg-507853eb")) return conversation @@ -209,7 +210,7 @@ async def _apply_kb( f"\n\n[Related Knowledge Base Results]:\n{kb_result}" ) except Exception as exc: # noqa: BLE001 - logger.error("Error occurred while retrieving knowledge base: %s", exc) + logger.error(t("msg-24bd9273", exc=exc)) else: if req.func_tool is None: req.func_tool = ToolSet() @@ -238,7 +239,7 @@ async def _apply_file_extract( req.prompt = "总结一下文件里面讲了什么?" if config.file_extract_prov == "moonshotai": if not config.file_extract_msh_api_key: - logger.error("Moonshot AI API key for file extract is not set") + logger.error(t("msg-36dc1409")) return file_contents = await asyncio.gather( *[ @@ -250,7 +251,7 @@ async def _apply_file_extract( ] ) else: - logger.error("Unsupported file extract provider: %s", config.file_extract_prov) + logger.error(t("msg-b41a7a58", res=config.file_extract_prov)) return for file_content, file_name in zip(file_contents, file_names): @@ -458,18 +459,18 @@ async def _request_img_caption( prov = plugin_context.get_provider_by_id(provider_id) if prov is None: raise ValueError( - f"Cannot get image caption because provider `{provider_id}` is not exist.", + t("msg-f2ea29f4", provider_id=provider_id), ) if not isinstance(prov, Provider): raise ValueError( - f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.", + t("msg-91a70615", provider_id=provider_id, res=type(prov)), ) img_cap_prompt = cfg.get( "image_caption_prompt", "Please describe the image.", ) - logger.debug("Processing image caption with provider: %s", provider_id) + logger.debug(t("msg-6097bd34", provider_id=provider_id)) llm_resp = await prov.text_chat( prompt=img_cap_prompt, image_urls=image_urls, @@ -496,7 +497,7 @@ async def _ensure_img_caption( ) req.image_urls = [] except Exception as exc: # noqa: BLE001 - logger.error("处理图片描述失败: %s", exc) + logger.error(t("msg-7f5e3367", exc=exc)) def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None: @@ -569,9 +570,9 @@ async def _process_quote_message( f"[Image Caption in quoted message]: {llm_resp.completion_text}" ) else: - logger.warning("No provider found for image captioning in quote.") + logger.warning(t("msg-719d5e4d")) except BaseException as exc: - logger.error("处理引用图片失败: %s", exc) + logger.error(t("msg-633f992f", exc=exc)) quoted_content = "\n".join(content_parts) quoted_text = f"\n{quoted_content}\n" @@ -593,8 +594,7 @@ def _append_system_reminders( if cfg.get("group_name_display") and event.message_obj.group_id: if not event.message_obj.group: logger.error( - "Group name display enabled but group object is None. Group ID: %s", - event.message_obj.group_id, + t("msg-1891edf8", res=event.message_obj.group_id), ) else: group_name = event.message_obj.group.group_name @@ -608,7 +608,7 @@ def _append_system_reminders( now = datetime.datetime.now(zoneinfo.ZoneInfo(timezone)) current_time = now.strftime("%Y-%m-%d %H:%M (%Z)") except Exception as exc: # noqa: BLE001 - logger.error("时区设置错误: %s, 使用本地时区", exc) + logger.error(t("msg-7d93dc13", exc=exc)) if not current_time: current_time = ( datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)") @@ -667,7 +667,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["image"]) if "image" not in provider_cfg: logger.debug( - "Provider %s does not support image, using placeholder.", provider + t("msg-09eb6259", provider=provider) ) image_count = len(req.image_urls) placeholder = " ".join(["[图片]"] * image_count) @@ -680,7 +680,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["tool_use"]) if "tool_use" not in provider_cfg: logger.debug( - "Provider %s does not support tool_use, clearing tools.", provider + t("msg-f57d475e", provider=provider) ) req.func_tool = None @@ -754,11 +754,7 @@ def _sanitize_context_by_modalities( if removed_image_blocks or removed_tool_messages or removed_tool_calls: logger.debug( - "sanitize_context_by_modalities applied: " - "removed_image_blocks=%s, removed_tool_messages=%s, removed_tool_calls=%s", - removed_image_blocks, - removed_tool_messages, - removed_tool_calls, + t("msg-2e3df24a", removed_image_blocks=removed_image_blocks, removed_tool_messages=removed_tool_messages, removed_tool_calls=removed_tool_calls), ) req.contexts = sanitized_contexts @@ -813,9 +809,7 @@ async def _handle_webchat( ) except Exception as e: logger.exception( - "Failed to generate webchat title for session %s: %s", - chatui_session_id, - e, + t("msg-7a34e35a", chatui_session_id=chatui_session_id, e=e), ) return if llm_resp and llm_resp.completion_text: @@ -823,7 +817,7 @@ async def _handle_webchat( if not title or "" in title: return logger.info( - "Generated chatui title for session %s: %s", chatui_session_id, title + t("msg-5becd564", chatui_session_id=chatui_session_id, title=title) ) await db_helper.update_platform_session( session_id=chatui_session_id, @@ -836,8 +830,7 @@ def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) - req.system_prompt = f"{LLM_SAFETY_MODE_SYSTEM_PROMPT}\n\n{req.system_prompt}" else: logger.warning( - "Unsupported llm_safety_mode strategy: %s.", - config.safety_mode_strategy, + t("msg-d8cff4db", res=config.safety_mode_strategy), ) @@ -851,7 +844,7 @@ def _apply_sandbox_tools( ep = config.sandbox_cfg.get("shipyard_endpoint", "") at = config.sandbox_cfg.get("shipyard_access_token", "") if not ep or not at: - logger.error("Shipyard sandbox configuration is incomplete.") + logger.error(t("msg-7ea2c5d3")) return os.environ["SHIPYARD_ENDPOINT"] = ep os.environ["SHIPYARD_ACCESS_TOKEN"] = at @@ -933,14 +926,12 @@ def _get_compress_provider( provider = plugin_context.get_provider_by_id(config.llm_compress_provider_id) if provider is None: logger.warning( - "未找到指定的上下文压缩模型 %s,将跳过压缩。", - config.llm_compress_provider_id, + t("msg-8271b0d7", res=config.llm_compress_provider_id), ) return None if not isinstance(provider, Provider): logger.warning( - "指定的上下文压缩模型 %s 不是对话模型,将跳过压缩。", - config.llm_compress_provider_id, + t("msg-bf48c713", res=config.llm_compress_provider_id), ) return None return provider @@ -952,7 +943,7 @@ def _get_fallback_chat_providers( fallback_ids = provider_settings.get("fallback_chat_models", []) if not isinstance(fallback_ids, list): logger.warning( - "fallback_chat_models setting is not a list, skip fallback providers." + t("msg-c6c9d989") ) return [] @@ -967,13 +958,11 @@ def _get_fallback_chat_providers( continue fallback_provider = plugin_context.get_provider_by_id(fallback_id) if fallback_provider is None: - logger.warning("Fallback chat provider `%s` not found, skip.", fallback_id) + logger.warning(t("msg-c48173dd", fallback_id=fallback_id)) continue if not isinstance(fallback_provider, Provider): logger.warning( - "Fallback chat provider `%s` is invalid type: %s, skip.", - fallback_id, - type(fallback_provider), + t("msg-88fd7233", fallback_id=fallback_id, res=type(fallback_provider)), ) continue fallbacks.append(fallback_provider) @@ -996,7 +985,7 @@ async def build_main_agent( """ provider = provider or _select_provider(event, plugin_context) if provider is None: - logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。") + logger.info(t("msg-ee979399")) return None if req is None: @@ -1083,18 +1072,12 @@ async def build_main_agent( ) if remaining_limit <= 0 and fallback_images: logger.warning( - "Skip quoted fallback images due to limit=%d for umo=%s", - config.max_quoted_fallback_images, - event.unified_msg_origin, + t("msg-d003c63c", res=config.max_quoted_fallback_images, res_2=event.unified_msg_origin), ) continue if len(fallback_images) > remaining_limit: logger.warning( - "Truncate quoted fallback images for umo=%s, reply_id=%s from %d to %d", - event.unified_msg_origin, - getattr(comp, "id", None), - len(fallback_images), - remaining_limit, + t("msg-65bb0f30", res=event.unified_msg_origin, res_2=getattr(comp, 'id', None), res_3=len(fallback_images), remaining_limit=remaining_limit), ) fallback_images = fallback_images[:remaining_limit] for image_ref in fallback_images: @@ -1105,10 +1088,7 @@ async def build_main_agent( _append_quoted_image_attachment(req, image_ref) except Exception as exc: # noqa: BLE001 logger.warning( - "Failed to resolve fallback quoted images for umo=%s, reply_id=%s: %s", - event.unified_msg_origin, - getattr(comp, "id", None), - exc, + t("msg-617040f3", res=event.unified_msg_origin, res_2=getattr(comp, 'id', None), exc=exc), exc_info=True, ) @@ -1125,7 +1105,7 @@ async def build_main_agent( try: await _apply_file_extract(event, req, config) except Exception as exc: # noqa: BLE001 - logger.error("Error occurred while applying file extract: %s", exc) + logger.error(t("msg-d4c7199d", exc=exc)) if not req.prompt and not req.image_urls: if not event.get_group_id() and req.extra_user_content_parts: diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index 2e0d8b0aa7..201afbd146 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import base64 import json import os @@ -259,10 +260,10 @@ async def _resolve_path_from_sandbox( get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" ) await sb.download_file(path, local_path) - logger.info(f"Downloaded file from sandbox: {path} -> {local_path}") + logger.info(t("msg-509829d8", path=path, local_path=local_path)) return local_path, True except Exception as e: - logger.warning(f"Failed to check/download file from sandbox: {e}") + logger.warning(t("msg-b462b60d", e=e)) # Return the original path (will likely fail later, but that's expected) return path, False @@ -401,7 +402,7 @@ async def retrieve_knowledge_base( # 如果配置为空列表,明确表示不使用知识库 if not kb_ids: - logger.info(f"[知识库] 会话 {umo} 已被配置为不使用知识库") + logger.info(t("msg-0b3144f1", umo=umo)) return top_k = session_config.get("top_k", 5) @@ -414,29 +415,29 @@ async def retrieve_knowledge_base( if kb_helper: kb_names.append(kb_helper.kb.kb_name) else: - logger.warning(f"[知识库] 知识库不存在或未加载: {kb_id}") + logger.warning(t("msg-97e13f98", kb_id=kb_id)) invalid_kb_ids.append(kb_id) if invalid_kb_ids: logger.warning( - f"[知识库] 会话 {umo} 配置的以下知识库无效: {invalid_kb_ids}", + t("msg-312d09c7", umo=umo, invalid_kb_ids=invalid_kb_ids), ) if not kb_names: return - logger.debug(f"[知识库] 使用会话级配置,知识库数量: {len(kb_names)}") + logger.debug(t("msg-42b0e9f8", res=len(kb_names))) else: kb_names = config.get("kb_names", []) top_k = config.get("kb_final_top_k", 5) - logger.debug(f"[知识库] 使用全局配置,知识库数量: {len(kb_names)}") + logger.debug(t("msg-08167007", res=len(kb_names))) top_k_fusion = config.get("kb_fusion_top_k", 20) if not kb_names: return - logger.debug(f"[知识库] 开始检索知识库,数量: {len(kb_names)}, top_k={top_k}") + logger.debug(t("msg-a00becc3", res=len(kb_names), top_k=top_k)) kb_context = await kb_mgr.retrieve( query=query, kb_names=kb_names, @@ -450,7 +451,7 @@ async def retrieve_knowledge_base( formatted = kb_context.get("context_text", "") if formatted: results = kb_context.get("results", []) - logger.debug(f"[知识库] 为会话 {umo} 注入了 {len(results)} 条相关知识块") + logger.debug(t("msg-199e71b7", umo=umo, res=len(results))) return formatted diff --git a/astrbot/core/astrbot_config_mgr.py b/astrbot/core/astrbot_config_mgr.py index c2bfb1c37b..248b754753 100644 --- a/astrbot/core/astrbot_config_mgr.py +++ b/astrbot/core/astrbot_config_mgr.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import os import uuid from typing import TypedDict, TypeVar @@ -68,7 +69,7 @@ def _load_all_configs(self) -> None: self.confs[uuid_] = conf else: logger.warning( - f"Config file {conf_path} for UUID {uuid_} does not exist, skipping.", + t("msg-7875e5bd", conf_path=conf_path, uuid_=uuid_), ) continue @@ -188,7 +189,7 @@ def delete_conf(self, conf_id: str) -> bool: """ if conf_id == "default": - raise ValueError("不能删除默认配置文件") + raise ValueError(t("msg-39c4fd49")) # 从映射中移除 abconf_data = self.sp.get( @@ -198,7 +199,7 @@ def delete_conf(self, conf_id: str) -> bool: scope_id="global", ) if conf_id not in abconf_data: - logger.warning(f"配置文件 {conf_id} 不存在于映射中") + logger.warning(t("msg-cf7b8991", conf_id=conf_id)) return False # 获取配置文件路径 @@ -211,9 +212,9 @@ def delete_conf(self, conf_id: str) -> bool: try: if os.path.exists(conf_path): os.remove(conf_path) - logger.info(f"已删除配置文件: {conf_path}") + logger.info(t("msg-2aad13a4", conf_path=conf_path)) except Exception as e: - logger.error(f"删除配置文件 {conf_path} 失败: {e}") + logger.error(t("msg-94c359ef", conf_path=conf_path, e=e)) return False # 从内存中移除 @@ -225,7 +226,7 @@ def delete_conf(self, conf_id: str) -> bool: self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.abconf_data = abconf_data - logger.info(f"成功删除配置文件 {conf_id}") + logger.info(t("msg-44f0b770", conf_id=conf_id)) return True def update_conf_info(self, conf_id: str, name: str | None = None) -> bool: @@ -240,7 +241,7 @@ def update_conf_info(self, conf_id: str, name: str | None = None) -> bool: """ if conf_id == "default": - raise ValueError("不能更新默认配置文件的信息") + raise ValueError(t("msg-737da44e")) abconf_data = self.sp.get( "abconf_mapping", @@ -249,7 +250,7 @@ def update_conf_info(self, conf_id: str, name: str | None = None) -> bool: scope_id="global", ) if conf_id not in abconf_data: - logger.warning(f"配置文件 {conf_id} 不存在于映射中") + logger.warning(t("msg-cf7b8991", conf_id=conf_id)) return False # 更新名称 @@ -259,7 +260,7 @@ def update_conf_info(self, conf_id: str, name: str | None = None) -> bool: # 保存更新 self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global") self.abconf_data = abconf_data - logger.info(f"成功更新配置文件 {conf_id} 的信息") + logger.info(t("msg-9d496709", conf_id=conf_id)) return True def g( diff --git a/astrbot/core/backup/exporter.py b/astrbot/core/backup/exporter.py index a922375998..804175c148 100644 --- a/astrbot/core/backup/exporter.py +++ b/astrbot/core/backup/exporter.py @@ -3,6 +3,7 @@ 负责将所有数据导出为 ZIP 备份文件。 导出格式为 JSON,这是数据库无关的方案,支持未来向 MySQL/PostgreSQL 迁移。 """ +from astrbot.core.lang import t import hashlib import json @@ -89,7 +90,7 @@ async def export_all( zip_filename = f"astrbot_backup_{timestamp}.zip" zip_path = os.path.join(output_dir, zip_filename) - logger.info(f"开始导出备份到 {zip_path}") + logger.info(t("msg-c7ed7177", zip_path=zip_path)) try: with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: @@ -193,11 +194,11 @@ async def export_all( if progress_callback: await progress_callback("manifest", 100, 100, "清单生成完成") - logger.info(f"备份导出完成: {zip_path}") + logger.info(t("msg-8099b694", zip_path=zip_path)) return zip_path except Exception as e: - logger.error(f"备份导出失败: {e}") + logger.error(t("msg-75a4910d", e=e)) # 清理失败的文件 if os.path.exists(zip_path): os.remove(zip_path) @@ -216,10 +217,10 @@ async def _export_main_database(self) -> dict[str, list[dict]]: self._model_to_dict(record) for record in records ] logger.debug( - f"导出表 {table_name}: {len(export_data[table_name])} 条记录" + t("msg-2821fc92", table_name=table_name, res=len(export_data[table_name])) ) except Exception as e: - logger.warning(f"导出表 {table_name} 失败: {e}") + logger.warning(t("msg-52b7c242", table_name=table_name, e=e)) export_data[table_name] = [] return export_data @@ -240,10 +241,10 @@ async def _export_kb_metadata(self) -> dict[str, list[dict]]: self._model_to_dict(record) for record in records ] logger.debug( - f"导出知识库表 {table_name}: {len(export_data[table_name])} 条记录" + t("msg-56310830", table_name=table_name, res=len(export_data[table_name])) ) except Exception as e: - logger.warning(f"导出知识库表 {table_name} 失败: {e}") + logger.warning(t("msg-f4e8f57e", table_name=table_name, e=e)) export_data[table_name] = [] return export_data @@ -266,7 +267,7 @@ async def _export_kb_documents(self, kb_helper: Any) -> dict[str, Any]: return {"documents": docs} except Exception as e: - logger.warning(f"导出知识库文档失败: {e}") + logger.warning(t("msg-8e4ddd12", e=e)) return {"documents": []} async def _export_faiss_index( @@ -281,9 +282,9 @@ async def _export_faiss_index( if index_path.exists(): archive_path = f"databases/kb_{kb_id}/index.faiss" zf.write(str(index_path), archive_path) - logger.debug(f"导出 FAISS 索引: {archive_path}") + logger.debug(t("msg-c1960618", archive_path=archive_path)) except Exception as e: - logger.warning(f"导出 FAISS 索引失败: {e}") + logger.warning(t("msg-314bf920", e=e)) async def _export_kb_media_files( self, zf: zipfile.ZipFile, kb_helper: Any, kb_id: str @@ -302,7 +303,7 @@ async def _export_kb_media_files( archive_path = f"files/kb_media/{kb_id}/{rel_path}" zf.write(str(file_path), archive_path) except Exception as e: - logger.warning(f"导出知识库媒体文件失败: {e}") + logger.warning(t("msg-528757b2", e=e)) async def _export_directories( self, zf: zipfile.ZipFile @@ -318,7 +319,7 @@ async def _export_directories( for dir_name, dir_path in backup_directories.items(): full_path = Path(dir_path) if not full_path.exists(): - logger.debug(f"目录不存在,跳过: {full_path}") + logger.debug(t("msg-d89d6dfe", full_path=full_path)) continue file_count = 0 @@ -343,14 +344,14 @@ async def _export_directories( file_count += 1 total_size += file_path.stat().st_size except Exception as e: - logger.warning(f"导出文件 {file_path} 失败: {e}") + logger.warning(t("msg-94527edd", file_path=file_path, e=e)) stats[dir_name] = {"files": file_count, "size": total_size} logger.debug( - f"导出目录 {dir_name}: {file_count} 个文件, {total_size} 字节" + t("msg-cb773e24", dir_name=dir_name, file_count=file_count, total_size=total_size) ) except Exception as e: - logger.warning(f"导出目录 {dir_path} 失败: {e}") + logger.warning(t("msg-ae929510", dir_path=dir_path, e=e)) stats[dir_name] = {"files": 0, "size": 0} return stats @@ -369,7 +370,7 @@ async def _export_attachments( archive_path = f"files/attachments/{attachment_id}{ext}" zf.write(file_path, archive_path) except Exception as e: - logger.warning(f"导出附件失败: {e}") + logger.warning(t("msg-93e331d2", e=e)) def _model_to_dict(self, record: Any) -> dict: """将 SQLModel 实例转换为字典 diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index b51c7d9560..f89f080262 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -6,6 +6,7 @@ - 小版本(第三位)不同时提示警告,用户可选择强制导入 - 版本匹配时也需要用户确认 """ +from astrbot.core.lang import t import json import os @@ -183,12 +184,12 @@ def __init__(self) -> None: def add_warning(self, msg: str) -> None: self.warnings.append(msg) - logger.warning(msg) + logger.warning(t("msg-c046b6e4", msg=msg)) def add_error(self, msg: str) -> None: self.errors.append(msg) self.success = False - logger.error(msg) + logger.error(t("msg-c046b6e4", msg=msg)) def to_dict(self) -> dict: return { @@ -368,7 +369,7 @@ async def import_all( result.add_error(f"备份文件不存在: {zip_path}") return result - logger.info(f"开始从 {zip_path} 导入备份") + logger.info(t("msg-0e6f1f5d", zip_path=zip_path)) try: with zipfile.ZipFile(zip_path, "r") as zf: @@ -483,7 +484,7 @@ async def import_all( if progress_callback: await progress_callback("directories", 100, 100, "目录导入完成") - logger.info(f"备份导入完成: {result.to_dict()}") + logger.info(t("msg-2bf97ca0", res=result.to_dict())) return result except zipfile.BadZipFile: @@ -501,7 +502,7 @@ def _validate_version(self, manifest: dict) -> None: """ backup_version = manifest.get("astrbot_version") if not backup_version: - raise ValueError("备份文件缺少版本信息") + raise ValueError(t("msg-e67dda98")) # 使用新的版本兼容性检查 version_check = self._check_version_compatibility(backup_version) @@ -511,7 +512,7 @@ def _validate_version(self, manifest: dict) -> None: # minor_diff 和 match 都允许导入 if version_check["status"] == "minor_diff": - logger.warning(f"版本差异警告: {version_check['message']}") + logger.warning(t("msg-8f871d9f", res=version_check['message'])) async def _clear_main_db(self) -> None: """清空主数据库所有表""" @@ -520,11 +521,9 @@ async def _clear_main_db(self) -> None: for table_name, model_class in MAIN_DB_MODELS.items(): try: await session.execute(delete(model_class)) - logger.debug(f"已清空表 {table_name}") + logger.debug(t("msg-2d6da12a", table_name=table_name)) except Exception as e: - raise DatabaseClearError( - f"清空表 {table_name} 失败: {e}" - ) from e + logger.warning(t("msg-7d21b23a", table_name=table_name, e=e)) async def _clear_kb_data(self) -> None: """清空知识库数据""" @@ -537,9 +536,9 @@ async def _clear_kb_data(self) -> None: for table_name, model_class in KB_METADATA_MODELS.items(): try: await session.execute(delete(model_class)) - logger.debug(f"已清空知识库表 {table_name}") + logger.debug(t("msg-ab0f09db", table_name=table_name)) except Exception as e: - logger.warning(f"清空知识库表 {table_name} 失败: {e}") + logger.warning(t("msg-7bcdfaee", table_name=table_name, e=e)) # 删除知识库文件目录 for kb_id in list(self.kb_manager.kb_insts.keys()): @@ -549,7 +548,7 @@ async def _clear_kb_data(self) -> None: if kb_helper.kb_dir.exists(): shutil.rmtree(kb_helper.kb_dir) except Exception as e: - logger.warning(f"清理知识库 {kb_id} 失败: {e}") + logger.warning(t("msg-43f008f1", kb_id=kb_id, e=e)) self.kb_manager.kb_insts.clear() @@ -564,7 +563,7 @@ async def _import_main_database( for table_name, rows in data.items(): model_class = MAIN_DB_MODELS.get(table_name) if not model_class: - logger.warning(f"未知的表: {table_name}") + logger.warning(t("msg-985cae66", table_name=table_name)) continue normalized_rows = self._preprocess_main_table_rows(table_name, rows) @@ -577,10 +576,10 @@ async def _import_main_database( session.add(obj) count += 1 except Exception as e: - logger.warning(f"导入记录到 {table_name} 失败: {e}") + logger.warning(t("msg-dfa8b605", table_name=table_name, e=e)) imported[table_name] = count - logger.debug(f"导入表 {table_name}: {count} 条记录") + logger.debug(t("msg-89a2120c", table_name=table_name, count=count)) return imported @@ -722,7 +721,7 @@ async def _import_knowledge_bases( session.add(obj) count += 1 except Exception as e: - logger.warning(f"导入知识库记录到 {table_name} 失败: {e}") + logger.warning(t("msg-f1dec753", table_name=table_name, e=e)) result.imported_tables[f"kb_{table_name}"] = count @@ -795,7 +794,7 @@ async def _import_kb_documents(self, kb_id: str, doc_data: dict) -> None: metadata=json.loads(doc.get("metadata", "{}")), ) except Exception as e: - logger.warning(f"导入文档块失败: {e}") + logger.warning(t("msg-9807bcd8", e=e)) finally: await doc_storage.close() @@ -832,7 +831,7 @@ async def _import_attachments( dst.write(src.read()) count += 1 except Exception as e: - logger.warning(f"导入附件 {name} 失败: {e}") + logger.warning(t("msg-98a66293", name=name, e=e)) return count @@ -857,7 +856,7 @@ async def _import_directories( # 检查备份版本是否支持目录备份(需要版本 >= 1.1) backup_version = manifest.get("version", "1.0") if VersionComparator.compare_version(backup_version, "1.1") < 0: - logger.info("备份版本不支持目录备份,跳过目录导入") + logger.info(t("msg-39f2325f")) return dir_stats backed_up_dirs = manifest.get("directories", []) @@ -890,7 +889,7 @@ async def _import_directories( if backup_path.exists(): shutil.rmtree(backup_path) shutil.move(str(target_dir), str(backup_path)) - logger.debug(f"已备份现有目录 {target_dir} 到 {backup_path}") + logger.debug(t("msg-689050b6", target_dir=target_dir, backup_path=backup_path)) # 创建目标目录 target_dir.mkdir(parents=True, exist_ok=True) @@ -913,7 +912,7 @@ async def _import_directories( result.add_warning(f"导入文件 {name} 失败: {e}") dir_stats[dir_name] = file_count - logger.debug(f"导入目录 {dir_name}: {file_count} 个文件") + logger.debug(t("msg-d51b3536", dir_name=dir_name, file_count=file_count)) except Exception as e: result.add_warning(f"导入目录 {dir_name} 失败: {e}") diff --git a/astrbot/core/computer/booters/boxlite.py b/astrbot/core/computer/booters/boxlite.py index 70064fdd48..4878dba4dd 100644 --- a/astrbot/core/computer/booters/boxlite.py +++ b/astrbot/core/computer/booters/boxlite.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import random from typing import Any @@ -37,7 +38,7 @@ async def _exec_operation( else: error_text = await response.text() raise Exception( - f"Failed to exec operation: {response.status} {error_text}" + t("msg-019c4d18", res=response.status, error_text=error_text) ) async def upload_file(self, path: str, remote_path: str) -> dict: @@ -82,7 +83,7 @@ async def upload_file(self, path: str, remote_path: str) -> dict: } except aiohttp.ClientError as e: - logger.error(f"Failed to upload file: {e}") + logger.error(t("msg-b135b7bd", e=e)) return { "success": False, "error": f"Connection error: {str(e)}", @@ -95,14 +96,14 @@ async def upload_file(self, path: str, remote_path: str) -> dict: "message": "File upload failed", } except FileNotFoundError: - logger.error(f"File not found: {path}") + logger.error(t("msg-873ed1c8", path=path)) return { "success": False, "error": f"File not found: {path}", "message": "File upload failed", } except Exception as e: - logger.error(f"Unexpected error uploading file: {e}") + logger.error(t("msg-f58ceec6", e=e)) return { "success": False, "error": f"Internal error: {str(e)}", @@ -115,13 +116,13 @@ async def wait_healthy(self, ship_id: str, session_id: str) -> None: while loop > 0: try: logger.info( - f"Checking health for sandbox {ship_id} on {self.sb_url}..." + t("msg-900ab999", ship_id=ship_id, res=self.sb_url) ) url = f"{self.sb_url}/health" async with aiohttp.ClientSession() as session: async with session.get(url) as response: if response.status == 200: - logger.info(f"Sandbox {ship_id} is healthy") + logger.info(t("msg-2a50d6f3", ship_id=ship_id)) return except Exception: await asyncio.sleep(1) @@ -131,7 +132,7 @@ async def wait_healthy(self, ship_id: str, session_id: str) -> None: class BoxliteBooter(ComputerBooter): async def boot(self, session_id: str) -> None: logger.info( - f"Booting(Boxlite) for session: {session_id}, this may take a while..." + t("msg-fbdbe32f", session_id=session_id) ) random_port = random.randint(20000, 30000) self.box = boxlite.SimpleBox( @@ -146,7 +147,7 @@ async def boot(self, session_id: str) -> None: ], ) await self.box.start() - logger.info(f"Boxlite booter started for session: {session_id}") + logger.info(t("msg-b1f13f5f", session_id=session_id)) self.mocked = MockShipyardSandboxClient( sb_url=f"http://127.0.0.1:{random_port}" ) @@ -169,9 +170,9 @@ async def boot(self, session_id: str) -> None: await self.mocked.wait_healthy(self.box.id, session_id) async def shutdown(self) -> None: - logger.info(f"Shutting down Boxlite booter for ship: {self.box.id}") + logger.info(t("msg-e93d0c30", res=self.box.id)) self.box.shutdown() - logger.info(f"Boxlite booter for ship: {self.box.id} stopped") + logger.info(t("msg-6deea473", res=self.box.id)) @property def fs(self) -> FileSystemComponent: diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index a80ef0da28..27eea1829c 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -1,4 +1,5 @@ from __future__ import annotations +from astrbot.core.lang import t import asyncio import os @@ -48,7 +49,7 @@ def _ensure_safe_path(path: str) -> str: os.path.abspath(get_astrbot_temp_path()), ] if not any(abs_path.startswith(root) for root in allowed_roots): - raise PermissionError("Path is outside the allowed computer roots.") + raise PermissionError(t("msg-487d0c91")) return abs_path @@ -64,7 +65,7 @@ async def exec( background: bool = False, ) -> dict[str, Any]: if not _is_safe_command(command): - raise PermissionError("Blocked unsafe shell command.") + raise PermissionError(t("msg-e5eb5377")) def _run() -> dict[str, Any]: run_env = os.environ.copy() @@ -203,10 +204,10 @@ def __init__(self) -> None: self._shell = LocalShellComponent() async def boot(self, session_id: str) -> None: - logger.info(f"Local computer booter initialized for session: {session_id}") + logger.info(t("msg-9e1e117f", session_id=session_id)) async def shutdown(self) -> None: - logger.info("Local computer booter shutdown complete.") + logger.info(t("msg-2d7f95de")) @property def fs(self) -> FileSystemComponent: @@ -222,12 +223,12 @@ def shell(self) -> ShellComponent: async def upload_file(self, path: str, file_name: str) -> dict: raise NotImplementedError( - "LocalBooter does not support upload_file operation. Use shell instead." + t("msg-82a45196") ) async def download_file(self, remote_path: str, local_path: str) -> None: raise NotImplementedError( - "LocalBooter does not support download_file operation. Use shell instead." + t("msg-0457524a") ) async def available(self) -> bool: diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 6379d1e48b..6c04ebab55 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from shipyard import ShipyardClient, Spec from astrbot.api import logger @@ -27,7 +28,7 @@ async def boot(self, session_id: str) -> None: max_session_num=self._session_num, session_id=session_id, ) - logger.info(f"Got sandbox ship: {ship.id} for session: {session_id}") + logger.info(t("msg-b03115b0", res=ship.id, session_id=session_id)) self._ship = ship async def shutdown(self) -> None: @@ -80,5 +81,5 @@ async def available(self) -> bool: ) return health except Exception as e: - logger.error(f"Error checking Shipyard sandbox availability: {e}") + logger.error(t("msg-c5ce8bde", e=e)) return False diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 1853abf75c..45ad9b5924 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -4,6 +4,7 @@ from pathlib import Path from astrbot.api import logger +from astrbot.core.lang import t from astrbot.core.skills.skill_manager import SANDBOX_SKILLS_ROOT, SkillManager from astrbot.core.star.context import Context from astrbot.core.utils.astrbot_path import ( @@ -383,38 +384,32 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None: zip_path = zip_base.with_suffix(".zip") try: - if local_skill_dirs: - if zip_path.exists(): - zip_path.unlink() - shutil.make_archive(str(zip_base), "zip", str(skills_root)) - remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip" - logger.info("Uploading skills bundle to sandbox...") - await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}") - upload_result = await booter.upload_file(str(zip_path), str(remote_zip)) - if not upload_result.get("success", False): - raise RuntimeError("Failed to upload skills bundle to sandbox.") - else: - logger.info( - "No local skills found. Keeping sandbox built-ins and refreshing metadata." - ) - await booter.shell.exec(f"rm -f {SANDBOX_SKILLS_ROOT}/skills.zip") - - # Keep backward-compatible behavior while splitting lifecycle into two - # observable phases: apply (filesystem mutation) + scan (metadata read). - await _apply_skills_to_sandbox(booter) - payload = await _scan_sandbox_skills(booter) - _update_sandbox_skills_cache(payload) - managed = payload.get("managed_skills", []) if isinstance(payload, dict) else [] - logger.info( - "[Computer] Sandbox skill sync complete: managed=%d", - len(managed), + if os.path.exists(zip_path): + os.remove(zip_path) + shutil.make_archive(zip_base, "zip", skills_root) + remote_zip = Path(SANDBOX_SKILLS_ROOT) / "skills.zip" + logger.info(t("msg-7cb974b8")) + await booter.shell.exec(f"mkdir -p {SANDBOX_SKILLS_ROOT}") + upload_result = await booter.upload_file(zip_path, str(remote_zip)) + if not upload_result.get("success", False): + raise RuntimeError(t("msg-130cf3e3")) + # Use -n flag to never overwrite existing files, fallback to Python if unzip unavailable + await booter.shell.exec( + f"unzip -n {remote_zip} -d {SANDBOX_SKILLS_ROOT} || " + f"python3 -c \"import zipfile, os, pathlib; z=zipfile.ZipFile('{remote_zip}'); " + f"[z.extract(m, '{SANDBOX_SKILLS_ROOT}') for m in z.namelist() " + f"if not os.path.exists(os.path.join('{SANDBOX_SKILLS_ROOT}', m))]\" || " + f"python -c \"import zipfile, os, pathlib; z=zipfile.ZipFile('{remote_zip}'); " + f"[z.extract(m, '{SANDBOX_SKILLS_ROOT}') for m in z.namelist() " + f"if not os.path.exists(os.path.join('{SANDBOX_SKILLS_ROOT}', m))]\"; " + f"rm -f {remote_zip}" ) finally: if zip_path.exists(): try: zip_path.unlink() except Exception: - logger.warning(f"Failed to remove temp skills zip: {zip_path}") + logger.warning(t("msg-99188d69", zip_path=zip_path)) async def get_booter( @@ -473,7 +468,7 @@ async def get_booter( client = BoxliteBooter() else: - raise ValueError(f"Unknown booter type: {booter_type}") + raise ValueError(t("msg-3f3c81da", booter_type=booter_type)) try: await client.boot(uuid_str) @@ -482,7 +477,7 @@ async def get_booter( ) await _sync_skills_to_sandbox(client) except Exception as e: - logger.error(f"Error booting sandbox for session {session_id}: {e}") + logger.error(t("msg-e20cc33a", session_id=session_id, e=e)) raise e session_booter[session_id] = client diff --git a/astrbot/core/computer/tools/fs.py b/astrbot/core/computer/tools/fs.py index 31b7f3f513..8fc7feff0d 100644 --- a/astrbot/core/computer/tools/fs.py +++ b/astrbot/core/computer/tools/fs.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import os import uuid from dataclasses import dataclass, field @@ -122,18 +123,18 @@ async def call( # Upload file to sandbox result = await sb.upload_file(local_path, remote_path) - logger.debug(f"Upload result: {result}") + logger.debug(t("msg-99ab0efe", result=result)) success = result.get("success", False) if not success: return f"Error uploading file: {result.get('message', 'Unknown error')}" file_path = result.get("file_path", "") - logger.info(f"File {local_path} uploaded to sandbox at {file_path}") + logger.info(t("msg-bca9d578", local_path=local_path, file_path=file_path)) return f"File uploaded successfully to {file_path}" except Exception as e: - logger.error(f"Error uploading file {local_path}: {e}") + logger.error(t("msg-da21a6a5", local_path=local_path, e=e)) return f"Error uploading file: {str(e)}" @@ -179,7 +180,7 @@ async def call( # Download file from sandbox await sb.download_file(remote_path, local_path) - logger.info(f"File {remote_path} downloaded from sandbox to {local_path}") + logger.info(t("msg-93476abb", remote_path=remote_path, local_path=local_path)) if also_send_to_user: try: @@ -188,7 +189,7 @@ async def call( MessageChain(chain=[File(name=name, file=local_path)]) ) except Exception as e: - logger.error(f"Error sending file message: {e}") + logger.error(t("msg-079c5972", e=e)) # remove # try: @@ -200,5 +201,5 @@ async def call( return f"File downloaded successfully to {local_path}" except Exception as e: - logger.error(f"Error downloading file {remote_path}: {e}") + logger.error(t("msg-ce35bb2c", remote_path=remote_path, e=e)) return f"Error downloading file: {str(e)}" diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 6a415e56c9..8cfa5f4b01 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import enum import json import logging @@ -73,7 +74,7 @@ def _parse_schema(schema: dict, conf: dict) -> None: for k, v in schema.items(): if v["type"] not in DEFAULT_VALUE_MAP: raise TypeError( - f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}", + t("msg-e0a69978", res=v['type'], res_2=DEFAULT_VALUE_MAP.keys()), ) if "default" in v: default = v["default"] @@ -104,7 +105,7 @@ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""): if key not in conf: # 配置项不存在,插入默认值 path_ = path + "." + key if path else key - logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}") + logger.info(t("msg-b9583fc9", path_=path_, value=value)) new_conf[key] = value has_new = True elif conf[key] is None: @@ -134,15 +135,15 @@ def check_config_integrity(self, refer_conf: dict, conf: dict, path=""): for key in list(conf.keys()): if key not in refer_conf: path_ = path + "." + key if path else key - logger.info(f"检查到配置项 {path_} 不存在,将从当前配置中删除") + logger.info(t("msg-ee26e40e", path_=path_)) has_new = True # 顺序不一致也算作变更 if list(conf.keys()) != list(new_conf.keys()): if path: - logger.info(f"检查到配置项 {path} 的子项顺序不一致,已重新排序") + logger.info(t("msg-2d7497a5", path=path)) else: - logger.info("检查到配置项顺序不一致,已重新排序") + logger.info(t("msg-5fdad937")) has_new = True # 更新原始配置 @@ -172,7 +173,7 @@ def __delattr__(self, key) -> None: del self[key] self.save_config() except KeyError: - raise AttributeError(f"没有找到 Key: '{key}'") + raise AttributeError(t("msg-555373b0", key=key)) def __setattr__(self, key, value) -> None: self[key] = value diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 3ba68fa898..adac764902 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -210,6 +210,9 @@ "ca_certs": "", }, }, + "i18n": { + "locale": "zh-cn", + }, "platform": [], "platform_specific": { # 平台特异配置:按平台分类,平台下按功能分组 diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 2c282867f9..8a0198212d 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -3,6 +3,7 @@ 在 AstrBot 中, 会话和对话是独立的, 会话用于标记对话窗口, 例如群聊"123456789"可以建立一个会话, 在一个会话中可以建立多个对话, 并且支持对话的切换和删除 """ +from astrbot.core.lang import t import json from collections.abc import Awaitable, Callable @@ -54,7 +55,7 @@ async def _trigger_session_deleted(self, unified_msg_origin: str) -> None: from astrbot.core import logger logger.error( - f"会话删除回调执行失败 (session: {unified_msg_origin}): {e}", + t("msg-86f404dd", unified_msg_origin=unified_msg_origin, e=e), ) def _convert_conv_from_v2_to_v1(self, conv_v2: ConversationV2) -> Conversation: @@ -347,7 +348,7 @@ async def add_message_pair( """ conv = await self.db.get_conversation_by_id(cid=cid) if not conv: - raise Exception(f"Conversation with id {cid} not found") + raise Exception(t("msg-57dcc41f", cid=cid)) history = conv.content or [] if isinstance(user_message, UserMessageSegment): user_msg_dict = user_message.model_dump() diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index fe6b1c351d..486c607517 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -8,6 +8,7 @@ 2. 启动事件总线和任务, 所有任务都在这里运行 3. 执行启动完成事件钩子 """ +from astrbot.core.lang import t import asyncio import os @@ -65,7 +66,7 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None: if proxy_config != "": os.environ["https_proxy"] = proxy_config os.environ["http_proxy"] = proxy_config - logger.debug(f"Using proxy: {proxy_config}") + logger.debug(t("msg-9967ec8b", proxy_config=proxy_config)) # 设置 no_proxy no_proxy_list = self.astrbot_config.get("no_proxy", []) os.environ["no_proxy"] = ",".join(no_proxy_list) @@ -77,7 +78,7 @@ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None: del os.environ["http_proxy"] if "no_proxy" in os.environ: del os.environ["no_proxy"] - logger.debug("HTTP proxy cleared") + logger.debug(t("msg-5a29b73d")) async def _init_or_reload_subagent_orchestrator(self) -> None: """Create (if needed) and reload the subagent orchestrator from config. @@ -95,7 +96,7 @@ async def _init_or_reload_subagent_orchestrator(self) -> None: self.astrbot_config.get("subagent_orchestrator", {}), ) except Exception as e: - logger.error(f"Subagent orchestrator init failed: {e}", exc_info=True) + logger.error(t("msg-fafb87ce", e=e), exc_info=True) async def initialize(self) -> None: """初始化 AstrBot 核心生命周期管理类. @@ -143,8 +144,8 @@ async def initialize(self) -> None: self.astrbot_config_mgr, ) except Exception as e: - logger.error(f"AstrBot migration failed: {e!s}") - logger.error(traceback.format_exc()) + logger.error(t("msg-f7861f86", e=e)) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) # 初始化事件队列 self.event_queue = Queue() @@ -283,10 +284,10 @@ async def _task_wrapper(self, task: asyncio.Task) -> None: pass # 任务被取消, 静默处理 except Exception as e: # 获取完整的异常堆栈信息, 按行分割并记录到日志中 - logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}") + logger.error(t("msg-967606fd", res=task.get_name(), e=e)) for line in traceback.format_exc().split("\n"): - logger.error(f"| {line}") - logger.error("-------") + logger.error(t("msg-a2cd77f3", line=line)) + logger.error(t("msg-1f686eeb")) async def start(self) -> None: """启动 AstrBot 核心生命周期管理类. @@ -294,7 +295,7 @@ async def start(self) -> None: 用load加载事件总线和任务并初始化, 执行启动完成事件钩子 """ self._load() - logger.info("AstrBot 启动完成。") + logger.info(t("msg-9556d279")) # 执行启动完成事件钩子 handlers = star_handlers_registry.get_handlers_by_event_type( @@ -303,11 +304,11 @@ async def start(self) -> None: for handler in handlers: try: logger.info( - f"hook(on_astrbot_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", + t("msg-daaf690b", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) await handler.handler() except BaseException: - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) # 同时运行curr_tasks中的所有任务 await asyncio.gather(*self.curr_tasks, return_exceptions=True) @@ -328,9 +329,9 @@ async def stop(self) -> None: try: await self.plugin_manager._terminate_plugin(plugin) except Exception as e: - logger.warning(traceback.format_exc()) + logger.warning(t("msg-78b9c276", res=traceback.format_exc())) logger.warning( - f"插件 {plugin.name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", + t("msg-4719cb33", res=plugin.name, e=e), ) await self.provider_manager.terminate() @@ -345,7 +346,7 @@ async def stop(self) -> None: except asyncio.CancelledError: pass except Exception as e: - logger.error(f"任务 {task.get_name()} 发生错误: {e}") + logger.error(t("msg-c3bbfa1d", res=task.get_name(), e=e)) async def restart(self) -> None: """重启 AstrBot 核心生命周期管理类, 终止各个管理器并重新加载平台实例""" @@ -397,7 +398,7 @@ async def reload_pipeline_scheduler(self, conf_id: str) -> None: """ ab_config = self.astrbot_config_mgr.confs.get(conf_id) if not ab_config: - raise ValueError(f"配置文件 {conf_id} 不存在") + raise ValueError(t("msg-af06ccab", conf_id=conf_id)) scheduler = PipelineScheduler( PipelineContext(ab_config, self.plugin_manager, conf_id), ) diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index d12878be3e..b4f12f7af0 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json from collections.abc import Awaitable, Callable @@ -55,8 +56,7 @@ async def sync_from_db(self) -> None: continue if job.job_type == "basic" and job.job_id not in self._basic_handlers: logger.warning( - "Skip scheduling basic cron job %s due to missing handler.", - job.job_id, + t("msg-2a752c91", res=job.job_id), ) continue self._schedule_job(job) @@ -151,9 +151,7 @@ def _schedule_job(self, job: CronJob) -> None: tzinfo = ZoneInfo(job.timezone) except Exception: logger.warning( - "Invalid timezone %s for cron job %s, fallback to system.", - job.timezone, - job.job_id, + t("msg-d5c33112", res=job.timezone, res_2=job.job_id), ) if job.run_once: run_at_str = None @@ -161,7 +159,7 @@ def _schedule_job(self, job: CronJob) -> None: run_at_str = job.payload.get("run_at") run_at_str = run_at_str or job.cron_expression if not run_at_str: - raise ValueError("run_once job missing run_at timestamp") + raise ValueError(t("msg-e71c28d3")) run_at = datetime.fromisoformat(run_at_str) if run_at.tzinfo is None and tzinfo is not None: run_at = run_at.replace(tzinfo=tzinfo) @@ -182,7 +180,7 @@ def _schedule_job(self, job: CronJob) -> None: ) ) except Exception as e: - logger.error(f"Failed to schedule cron job {job.job_id}: {e!s}") + logger.error(t("msg-dd46e69f", res=job.job_id, e=e)) def _get_next_run_time(self, job_id: str): aps_job = self.scheduler.get_job(job_id) @@ -204,11 +202,11 @@ async def _run_job(self, job_id: str) -> None: elif job.job_type == "active_agent": await self._run_active_agent_job(job, start_time=start_time) else: - raise ValueError(f"Unknown cron job type: {job.job_type}") + raise ValueError(t("msg-aa2e4688", res=job.job_type)) except Exception as e: # noqa: BLE001 status = "failed" last_error = str(e) - logger.error(f"Cron job {job_id} failed: {e!s}", exc_info=True) + logger.error(t("msg-186627d9", job_id=job_id, e=e), exc_info=True) finally: next_run = self._get_next_run_time(job_id) await self.db.update_cron_job( @@ -225,7 +223,7 @@ async def _run_job(self, job_id: str) -> None: async def _run_basic_job(self, job: CronJob) -> None: handler = self._basic_handlers.get(job.job_id) if not handler: - raise RuntimeError(f"Basic cron job handler not found for {job.job_id}") + raise RuntimeError(t("msg-cb955de0", res=job.job_id)) payload = job.payload or {} result = handler(**payload) if payload else handler() if asyncio.iscoroutine(result): @@ -235,7 +233,7 @@ async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> Non payload = job.payload or {} session_str = payload.get("session") if not session_str: - raise ValueError("ActiveAgentCronJob missing session.") + raise ValueError(t("msg-2029c4b2")) note = payload.get("note") or job.description or job.name extras = { @@ -285,7 +283,7 @@ async def _woke_main_agent( else MessageSession.from_str(session_str) ) except Exception as e: # noqa: BLE001 - logger.error(f"Invalid session for cron job: {e}") + logger.error(t("msg-6babddc9", e=e)) return cron_event = CronMessageEvent( @@ -345,7 +343,7 @@ async def _woke_main_agent( event=cron_event, plugin_context=self.ctx, config=config, req=req ) if not result: - logger.error("Failed to build main agent for cron job.") + logger.error(t("msg-865a2b07")) return runner = result.agent_runner @@ -370,7 +368,7 @@ async def _woke_main_agent( summary_note=summary_note, ) if not llm_resp: - logger.warning("Cron job agent got no response") + logger.warning(t("msg-27c9c6b3")) return diff --git a/astrbot/core/db/migration/helper.py b/astrbot/core/db/migration/helper.py index d7bca30678..4de9d20dad 100644 --- a/astrbot/core/db/migration/helper.py +++ b/astrbot/core/db/migration/helper.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import os from astrbot.api import logger, sp @@ -46,7 +47,7 @@ async def do_migration_v4( if not await check_migration_needed_v4(db_helper): return - logger.info("开始执行数据库迁移...") + logger.info(t("msg-a48f4752")) # 执行会话表迁移 await migration_conversation_table(db_helper, platform_id_map) @@ -66,4 +67,4 @@ async def do_migration_v4( # 标记迁移完成 await sp.put_async("global", "global", "migration_done_v4", True) - logger.info("数据库迁移完成。") + logger.info(t("msg-45e31e8e")) diff --git a/astrbot/core/db/migration/migra_3_to_4.py b/astrbot/core/db/migration/migra_3_to_4.py index 727d97b29b..9dd66be675 100644 --- a/astrbot/core/db/migration/migra_3_to_4.py +++ b/astrbot/core/db/migration/migra_3_to_4.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import datetime import json @@ -51,7 +52,7 @@ async def migration_conversation_table( page=1, page_size=10000000, ) - logger.info(f"迁移 {total_cnt} 条旧的会话数据到新的表中...") + logger.info(t("msg-7805b529", total_cnt=total_cnt)) async with db_helper.get_db() as dbsession: dbsession: AsyncSession @@ -60,7 +61,7 @@ async def migration_conversation_table( if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: progress = int((idx + 1) / total_cnt * 100) if progress % 10 == 0: - logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") + logger.info(t("msg-6f232b73", progress=progress, res=idx + 1, total_cnt=total_cnt)) try: conv = db_helper_v3.get_conversation_by_user_id( user_id=conversation.get("user_id", "unknown"), @@ -68,7 +69,7 @@ async def migration_conversation_table( ) if not conv: logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", + t("msg-6b1def31", conversation=conversation), ) continue if ":" not in conv.user_id: @@ -92,10 +93,10 @@ async def migration_conversation_table( dbsession.add(conv_v2) except Exception as e: logger.error( - f"迁移旧会话 {conversation.get('cid', 'unknown')} 失败: {e}", + t("msg-b008c93f", res=conversation.get('cid', 'unknown'), e=e), exc_info=True, ) - logger.info(f"成功迁移 {total_cnt} 条旧的会话数据到新表。") + logger.info(t("msg-6ac6313b", total_cnt=total_cnt)) async def migration_platform_table( @@ -110,13 +111,13 @@ async def migration_platform_table( - datetime.datetime(2023, 4, 10, tzinfo=datetime.timezone.utc) ).total_seconds() offset_sec = int(secs_from_2023_4_10_to_now) - logger.info(f"迁移旧平台数据,offset_sec: {offset_sec} 秒。") + logger.info(t("msg-6b72e89b", offset_sec=offset_sec)) stats = db_helper_v3.get_base_stats(offset_sec=offset_sec) - logger.info(f"迁移 {len(stats.platform)} 条旧的平台数据到新的表中...") + logger.info(t("msg-bdc90b84", res=len(stats.platform))) platform_stats_v3 = stats.platform if not platform_stats_v3: - logger.info("没有找到旧平台数据,跳过迁移。") + logger.info(t("msg-e6caca5c")) return first_time_stamp = platform_stats_v3[0].timestamp @@ -133,7 +134,7 @@ async def migration_platform_table( for bucket_idx, bucket_end in enumerate(range(start_time, end_time, 3600)): if bucket_idx % 500 == 0: progress = int((bucket_idx + 1) / total_buckets * 100) - logger.info(f"进度: {progress}% ({bucket_idx + 1}/{total_buckets})") + logger.info(t("msg-1e824a79", progress=progress, res=bucket_idx + 1, total_buckets=total_buckets)) cnt = 0 while ( idx < len(platform_stats_v3) @@ -171,10 +172,10 @@ async def migration_platform_table( ) except Exception: logger.error( - f"迁移平台统计数据失败: {platform_id}, {platform_type}, 时间戳: {bucket_end}", + t("msg-813384e2", platform_id=platform_id, platform_type=platform_type, bucket_end=bucket_end), exc_info=True, ) - logger.info(f"成功迁移 {len(platform_stats_v3)} 条旧的平台数据到新表。") + logger.info(t("msg-27ab191d", res=len(platform_stats_v3))) async def migration_webchat_data( @@ -189,7 +190,7 @@ async def migration_webchat_data( page=1, page_size=10000000, ) - logger.info(f"迁移 {total_cnt} 条旧的 WebChat 会话数据到新的表中...") + logger.info(t("msg-8e6280ed", total_cnt=total_cnt)) async with db_helper.get_db() as dbsession: dbsession: AsyncSession @@ -198,7 +199,7 @@ async def migration_webchat_data( if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: progress = int((idx + 1) / total_cnt * 100) if progress % 10 == 0: - logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") + logger.info(t("msg-6f232b73", progress=progress, res=idx + 1, total_cnt=total_cnt)) try: conv = db_helper_v3.get_conversation_by_user_id( user_id=conversation.get("user_id", "unknown"), @@ -206,7 +207,7 @@ async def migration_webchat_data( ) if not conv: logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", + t("msg-6b1def31", conversation=conversation), ) continue if ":" in conv.user_id: @@ -226,11 +227,11 @@ async def migration_webchat_data( except Exception: logger.error( - f"迁移旧 WebChat 会话 {conversation.get('cid', 'unknown')} 失败", + t("msg-cad66fe1", res=conversation.get('cid', 'unknown')), exc_info=True, ) - logger.info(f"成功迁移 {total_cnt} 条旧的 WebChat 会话数据到新表。") + logger.info(t("msg-63748a46", total_cnt=total_cnt)) async def migration_persona_data( @@ -242,13 +243,13 @@ async def migration_persona_data( """ v3_persona_config: list[dict] = astrbot_config.get("persona", []) total_personas = len(v3_persona_config) - logger.info(f"迁移 {total_personas} 个 Persona 配置到新表中...") + logger.info(t("msg-dfc93fa4", total_personas=total_personas)) for idx, persona in enumerate(v3_persona_config): if total_personas > 0 and (idx + 1) % max(1, total_personas // 10) == 0: progress = int((idx + 1) / total_personas * 100) if progress % 10 == 0: - logger.info(f"进度: {progress}% ({idx + 1}/{total_personas})") + logger.info(t("msg-ff85e45c", progress=progress, res=idx + 1, total_personas=total_personas)) try: begin_dialogs = persona.get("begin_dialogs", []) mood_imitation_dialogs = persona.get("mood_imitation_dialogs", []) @@ -270,10 +271,10 @@ async def migration_persona_data( begin_dialogs=begin_dialogs, ) logger.info( - f"迁移 Persona {persona['name']}({persona_new.system_prompt[:30]}...) 到新表成功。", + t("msg-c346311e", res=persona['name'], res_2=persona_new.system_prompt[:30]), ) except Exception as e: - logger.error(f"解析 Persona 配置失败:{e}") + logger.error(t("msg-b6292b94", e=e)) async def migration_preferences( @@ -293,7 +294,7 @@ async def migration_preferences( value = sp_v3.get(key) if value is not None: await sp.put_async("global", "global", key, value) - logger.info(f"迁移全局偏好设置 {key} 成功,值: {value}") + logger.info(t("msg-90e5039e", key=key, value=value)) # 2. umo scope migration session_conversation = sp_v3.get("session_conversation", default={}) @@ -305,9 +306,9 @@ async def migration_preferences( platform_id = get_platform_id(platform_id_map, session.platform_name) session.platform_id = platform_id await sp.put_async("umo", str(session), "sel_conv_id", conversation_id) - logger.info(f"迁移会话 {umo} 的对话数据到新表成功,平台 ID: {platform_id}") + logger.info(t("msg-d538da1c", umo=umo, platform_id=platform_id)) except Exception as e: - logger.error(f"迁移会话 {umo} 的对话数据失败: {e}", exc_info=True) + logger.error(t("msg-ee03c001", umo=umo, e=e), exc_info=True) session_service_config = sp_v3.get("session_service_config", default={}) for umo, config in session_service_config.items(): @@ -320,9 +321,9 @@ async def migration_preferences( await sp.put_async("umo", str(session), "session_service_config", config) - logger.info(f"迁移会话 {umo} 的服务配置到新表成功,平台 ID: {platform_id}") + logger.info(t("msg-5c4339cd", umo=umo, platform_id=platform_id)) except Exception as e: - logger.error(f"迁移会话 {umo} 的服务配置失败: {e}", exc_info=True) + logger.error(t("msg-4ce2a0b2", umo=umo, e=e), exc_info=True) session_variables = sp_v3.get("session_variables", default={}) for umo, variables in session_variables.items(): @@ -334,7 +335,7 @@ async def migration_preferences( session.platform_id = platform_id await sp.put_async("umo", str(session), "session_variables", variables) except Exception as e: - logger.error(f"迁移会话 {umo} 的变量失败: {e}", exc_info=True) + logger.error(t("msg-2e62dab9", umo=umo, e=e), exc_info=True) session_provider_perf = sp_v3.get("session_provider_perf", default={}) for umo, perf in session_provider_perf.items(): @@ -353,7 +354,7 @@ async def migration_preferences( provider_id, ) logger.info( - f"迁移会话 {umo} 的提供商偏好到新表成功,平台 ID: {platform_id}", + t("msg-afbf819e", umo=umo, platform_id=platform_id), ) except Exception as e: - logger.error(f"迁移会话 {umo} 的提供商偏好失败: {e}", exc_info=True) + logger.error(t("msg-959bb068", umo=umo, e=e), exc_info=True) diff --git a/astrbot/core/db/migration/migra_45_to_46.py b/astrbot/core/db/migration/migra_45_to_46.py index 58736ab51f..d8629e8c76 100644 --- a/astrbot/core/db/migration/migra_45_to_46.py +++ b/astrbot/core/db/migration/migra_45_to_46.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.api import logger, sp from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.umop_config_router import UmopConfigRouter @@ -9,7 +10,7 @@ async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> if not isinstance(abconf_data, dict): # should be unreachable logger.warning( - f"migrate_45_to_46: abconf_data is not a dict (type={type(abconf_data)}). Value: {abconf_data!r}", + t("msg-782b01c1", res=type(abconf_data), abconf_data=abconf_data), ) return @@ -23,7 +24,7 @@ async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> if not need_migration: return - logger.info("Starting migration from version 4.5 to 4.6") + logger.info(t("msg-49e09620")) # extract umo->conf_id mapping umo_to_conf_id = {} @@ -41,4 +42,4 @@ async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> # update the umop config router await ucr.update_routing_data(umo_to_conf_id) - logger.info("Migration from version 45 to 46 completed successfully") + logger.info(t("msg-791b79f8")) diff --git a/astrbot/core/db/migration/migra_token_usage.py b/astrbot/core/db/migration/migra_token_usage.py index 76bf8ce01c..d3ddc003e5 100644 --- a/astrbot/core/db/migration/migra_token_usage.py +++ b/astrbot/core/db/migration/migra_token_usage.py @@ -5,6 +5,7 @@ Changes: - Adds token_usage column to conversations table (default: 0) """ +from astrbot.core.lang import t from sqlalchemy import text @@ -24,7 +25,7 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: if migration_done: return - logger.info("开始执行数据库迁移(添加 conversations.token_usage 列)...") + logger.info(t("msg-c3e53a4f")) # 这里只适配了 SQLite。因为截止至这一版本,AstrBot 仅支持 SQLite。 @@ -36,7 +37,7 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: column_names = [col[1] for col in columns] if "token_usage" in column_names: - logger.info("token_usage 列已存在,跳过迁移") + logger.info(t("msg-ccbd0a41")) await sp.put_async( "global", "global", "migration_done_token_usage_1", True ) @@ -50,12 +51,12 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: ) await session.commit() - logger.info("token_usage 列添加成功") + logger.info(t("msg-39f60232")) # 标记迁移完成 await sp.put_async("global", "global", "migration_done_token_usage_1", True) - logger.info("token_usage 迁移完成") + logger.info(t("msg-4f9d3876")) except Exception as e: - logger.error(f"迁移过程中发生错误: {e}", exc_info=True) + logger.error(t("msg-91571aaf", e=e), exc_info=True) raise diff --git a/astrbot/core/db/migration/migra_webchat_session.py b/astrbot/core/db/migration/migra_webchat_session.py index 46025fc646..3e39935cd9 100644 --- a/astrbot/core/db/migration/migra_webchat_session.py +++ b/astrbot/core/db/migration/migra_webchat_session.py @@ -8,6 +8,7 @@ - Adds display_name field - Session_id format: {platform_id}_{uuid} """ +from astrbot.core.lang import t from sqlalchemy import func, select from sqlmodel import col @@ -30,7 +31,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: if migration_done: return - logger.info("开始执行数据库迁移(WebChat 会话迁移)...") + logger.info(t("msg-53fad3d0")) try: async with db_helper.get_db() as session: @@ -51,13 +52,13 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: webchat_users = result.all() if not webchat_users: - logger.info("没有找到需要迁移的 WebChat 数据") + logger.info(t("msg-7674efb0")) await sp.put_async( "global", "global", "migration_done_webchat_session_1", True ) return - logger.info(f"找到 {len(webchat_users)} 个 WebChat 会话需要迁移") + logger.info(t("msg-139e39ee", res=len(webchat_users))) # 检查已存在的会话 existing_query = select(col(PlatformSession.session_id)) @@ -93,7 +94,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: # 检查是否已经存在该会话 if session_id in existing_session_ids: - logger.debug(f"会话 {session_id} 已存在,跳过") + logger.debug(t("msg-cf287e58", session_id=session_id)) skipped_count += 1 continue @@ -118,14 +119,14 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: await session.commit() logger.info( - f"WebChat 会话迁移完成!成功迁移: {len(sessions_to_add)}, 跳过: {skipped_count}", + t("msg-062c72fa", res=len(sessions_to_add), skipped_count=skipped_count), ) else: - logger.info("没有新会话需要迁移") + logger.info(t("msg-a516cc9f")) # 标记迁移完成 await sp.put_async("global", "global", "migration_done_webchat_session_1", True) except Exception as e: - logger.error(f"迁移过程中发生错误: {e}", exc_info=True) + logger.error(t("msg-91571aaf", e=e), exc_info=True) raise diff --git a/astrbot/core/db/vec_db/faiss_impl/document_storage.py b/astrbot/core/db/vec_db/faiss_impl/document_storage.py index 2adae69ccc..5de921c18b 100644 --- a/astrbot/core/db/vec_db/faiss_impl/document_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/document_storage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import json import os from contextlib import asynccontextmanager @@ -121,7 +122,7 @@ async def get_documents( """ if self.engine is None: logger.warning( - "Database connection is not initialized, returning empty result", + t("msg-c2dc1d2b"), ) return [] @@ -278,7 +279,7 @@ async def delete_documents(self, metadata_filters: dict) -> None: """ if self.engine is None: logger.warning( - "Database connection is not initialized, skipping delete operation", + t("msg-51fa7426"), ) return @@ -307,7 +308,7 @@ async def count_documents(self, metadata_filters: dict | None = None) -> int: """ if self.engine is None: - logger.warning("Database connection is not initialized, returning 0") + logger.warning(t("msg-43d1f69f")) return 0 async with self.get_session() as session: diff --git a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py index dc6977cf8a..296e55d92c 100644 --- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py @@ -1,8 +1,9 @@ +from astrbot.core.lang import t try: import faiss except ModuleNotFoundError: raise ImportError( - "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", + t("msg-8e5fe535"), ) import os @@ -33,7 +34,7 @@ async def insert(self, vector: np.ndarray, id: int) -> None: assert self.index is not None, "FAISS index is not initialized." if vector.shape[0] != self.dimension: raise ValueError( - f"向量维度不匹配, 期望: {self.dimension}, 实际: {vector.shape[0]}", + t("msg-9aa7b941", res=self.dimension, res_2=vector.shape[0]), ) self.index.add_with_ids(vector.reshape(1, -1), np.array([id])) await self.save_index() @@ -51,7 +52,7 @@ async def insert_batch(self, vectors: np.ndarray, ids: list[int]) -> None: assert self.index is not None, "FAISS index is not initialized." if vectors.shape[1] != self.dimension: raise ValueError( - f"向量维度不匹配, 期望: {self.dimension}, 实际: {vectors.shape[1]}", + t("msg-9aa7b941", res=self.dimension, res_2=vectors.shape[1]), ) self.index.add_with_ids(vectors, np.array(ids)) await self.save_index() diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py index 3fca246ef5..2229833be0 100644 --- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py +++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import time import uuid @@ -75,7 +76,7 @@ async def insert_batch( ids = ids or [str(uuid.uuid4()) for _ in contents] start = time.time() - logger.debug(f"Generating embeddings for {len(contents)} contents...") + logger.debug(t("msg-9f9765dc", res=len(contents))) vectors = await self.embedding_provider.get_embeddings_batch( contents, batch_size=batch_size, @@ -85,7 +86,7 @@ async def insert_batch( ) end = time.time() logger.debug( - f"Generated embeddings for {len(contents)} contents in {end - start:.2f} seconds.", + t("msg-385bc50a", res=len(contents), res_2=end - start), ) # 使用 DocumentStorage 的批量插入方法 diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 70b5f054ed..b30280309b 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -9,6 +9,7 @@ 1. 维护一个异步队列, 来接受各种消息事件 2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑 """ +from astrbot.core.lang import t import asyncio from asyncio import Queue @@ -44,7 +45,7 @@ async def dispatch(self) -> None: scheduler = self.pipeline_scheduler_mapping.get(conf_id) if not scheduler: logger.error( - f"PipelineScheduler not found for id: {conf_id}, event ignored." + t("msg-da466871", res=conf_id) ) continue asyncio.create_task(scheduler.execute(event)) @@ -59,10 +60,10 @@ def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None: # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 if event.get_sender_name(): logger.info( - f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_name()}/{event.get_sender_id()}: {event.get_message_outline()}", + t("msg-7eccffa5", conf_name=conf_name, res=event.get_platform_id(), res_2=event.get_platform_name(), res_3=event.get_sender_name(), res_4=event.get_sender_id(), res_5=event.get_message_outline()), ) # 没有发送者名称: [平台名] 发送者ID: 消息概要 else: logger.info( - f"[{conf_name}] [{event.get_platform_id()}({event.get_platform_name()})] {event.get_sender_id()}: {event.get_message_outline()}", + t("msg-88bc26f2", conf_name=conf_name, res=event.get_platform_id(), res_2=event.get_platform_name(), res_3=event.get_sender_id(), res_4=event.get_message_outline()), ) diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index 42fbd23dfe..90bb41b30d 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import os import platform @@ -61,7 +62,7 @@ async def register_file(self, file_path: str, timeout: float | None = None) -> s if not os.path.exists(local_path): raise FileNotFoundError( - f"文件不存在: {local_path} (原始输入: {file_path})", + t("msg-0e444e51", local_path=local_path, file_path=file_path), ) file_token = str(uuid.uuid4()) @@ -90,9 +91,9 @@ async def handle_file(self, file_token: str) -> str: await self._cleanup_expired_tokens() if file_token not in self.staged_files: - raise KeyError(f"无效或过期的文件 token: {file_token}") + raise KeyError(t("msg-f61a5322", file_token=file_token)) file_path, _ = self.staged_files.pop(file_token) if not os.path.exists(file_path): - raise FileNotFoundError(f"文件不存在: {file_path}") + raise FileNotFoundError(t("msg-73d3e179", file_path=file_path)) return file_path diff --git a/astrbot/core/initial_loader.py b/astrbot/core/initial_loader.py index 3f836a4c42..c156c28755 100644 --- a/astrbot/core/initial_loader.py +++ b/astrbot/core/initial_loader.py @@ -4,6 +4,7 @@ 1. 初始化核心生命周期, 传递数据库和日志代理实例到核心生命周期 2. 运行核心生命周期任务和仪表板服务器 """ +from astrbot.core.lang import t import asyncio import traceback @@ -29,8 +30,8 @@ async def start(self) -> None: try: await core_lifecycle.initialize() except Exception as e: - logger.critical(traceback.format_exc()) - logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!") + logger.critical(t("msg-78b9c276", res=traceback.format_exc())) + logger.critical(t("msg-58525c23", e=e)) return core_task = core_lifecycle.start() @@ -53,5 +54,5 @@ async def start(self) -> None: try: await task # 整个AstrBot在这里运行 except asyncio.CancelledError: - logger.info("🌈 正在关闭 AstrBot...") + logger.info(t("msg-002cc3e8")) await core_lifecycle.stop() diff --git a/astrbot/core/knowledge_base/chunking/recursive.py b/astrbot/core/knowledge_base/chunking/recursive.py index e27ffbd1b7..9208b8b4ab 100644 --- a/astrbot/core/knowledge_base/chunking/recursive.py +++ b/astrbot/core/knowledge_base/chunking/recursive.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import Callable from .base import BaseChunker @@ -154,11 +155,11 @@ def _split_by_character( if overlap is None: overlap = self.chunk_overlap if chunk_size <= 0: - raise ValueError("chunk_size must be greater than 0") + raise ValueError(t("msg-21db456a")) if overlap < 0: - raise ValueError("chunk_overlap must be non-negative") + raise ValueError(t("msg-c0656f4e")) if overlap >= chunk_size: - raise ValueError("chunk_overlap must be less than chunk_size") + raise ValueError(t("msg-82bd199c")) result = [] for i in range(0, len(text), chunk_size - overlap): end = min(i + chunk_size, len(text)) diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index 4b9dcf7dd0..792c3e3a22 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from contextlib import asynccontextmanager from pathlib import Path @@ -167,7 +168,7 @@ async def migrate_to_v1(self) -> None: async def close(self) -> None: """关闭数据库连接""" await self.engine.dispose() - logger.info(f"知识库数据库已关闭: {self.db_path}") + logger.info(t("msg-b850e5d8", res=self.db_path)) async def get_kb_by_id(self, kb_id: str) -> KnowledgeBase | None: """根据 ID 获取知识库""" diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index 1e9127d72a..2bad45e27a 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json import re @@ -96,11 +97,11 @@ async def _repair_and_translate_chunk_with_retry( return [] except Exception as e: logger.warning( - f" - LLM call failed on attempt {attempt + 1}/{max_retries + 1}. Error: {str(e)}" + t("msg-7b3dc642", attempt=attempt + 1, res=attempt + 1, res_2=max_retries + 1, res_3=str(e)) ) logger.error( - f" - Failed to process chunk after {max_retries + 1} attempts. Using original text." + t("msg-4ba9530f", res=max_retries + 1) ) return [chunk] @@ -135,13 +136,13 @@ async def initialize(self) -> None: async def get_ep(self) -> EmbeddingProvider: if not self.kb.embedding_provider_id: - raise ValueError(f"知识库 {self.kb.kb_name} 未配置 Embedding Provider") + raise ValueError(t("msg-77670a3a", res=self.kb.kb_name)) ep: EmbeddingProvider = await self.prov_mgr.get_provider_by_id( self.kb.embedding_provider_id, ) # type: ignore if not ep: raise ValueError( - f"无法找到 ID 为 {self.kb.embedding_provider_id} 的 Embedding Provider", + t("msg-8e9eb3f9", res=self.kb.embedding_provider_id), ) return ep @@ -153,13 +154,13 @@ async def get_rp(self) -> RerankProvider | None: ) # type: ignore if not rp: raise ValueError( - f"无法找到 ID 为 {self.kb.rerank_provider_id} 的 Rerank Provider", + t("msg-3e426806", res=self.kb.rerank_provider_id), ) return rp async def _ensure_vec_db(self) -> FaissVecDB: if not self.kb.embedding_provider_id: - raise ValueError(f"知识库 {self.kb.kb_name} 未配置 Embedding Provider") + raise ValueError(t("msg-77670a3a", res=self.kb.kb_name)) ep = await self.get_ep() rp = await self.get_rp() @@ -234,12 +235,12 @@ async def upload_document( # 如果提供了预分块文本,直接使用 chunks_text = pre_chunked_text file_size = sum(len(chunk) for chunk in chunks_text) - logger.info(f"使用预分块文本进行上传,共 {len(chunks_text)} 个块。") + logger.info(t("msg-6e780e1e", res=len(chunks_text))) else: # 否则,执行标准的文件解析和分块流程 if file_content is None: raise ValueError( - "当未提供 pre_chunked_text 时,file_content 不能为空。" + t("msg-f4b82f18") ) file_size = len(file_content) @@ -333,7 +334,7 @@ async def embedding_progress_callback(current, total) -> None: await self.refresh_document(doc_id) return doc except Exception as e: - logger.error(f"上传文档失败: {e}") + logger.error(t("msg-975f06d7", e=e)) # if file_path.exists(): # file_path.unlink() @@ -342,7 +343,7 @@ async def embedding_progress_callback(current, total) -> None: if media_path.exists(): media_path.unlink() except Exception as me: - logger.warning(f"清理多媒体文件失败 {media_path}: {me}") + logger.warning(t("msg-969b17ca", media_path=media_path, me=me)) raise e @@ -393,7 +394,7 @@ async def refresh_document(self, doc_id: str) -> None: """更新文档的元数据""" doc = await self.get_document(doc_id) if not doc: - raise ValueError(f"无法找到 ID 为 {doc_id} 的文档") + raise ValueError(t("msg-18d25e55", doc_id=doc_id)) chunk_count = await self.get_chunk_count_by_doc_id(doc_id) doc.chunk_count = chunk_count async with self.kb_db.get_db() as session: @@ -504,7 +505,7 @@ async def upload_from_url( ) if not tavily_keys: raise ValueError( - "Error: Tavily API key is not configured in provider_settings." + t("msg-f5d7c34c") ) # 阶段1: 从 URL 提取内容 @@ -514,11 +515,11 @@ async def upload_from_url( try: text_content = await extract_text_from_url(url, tavily_keys) except Exception as e: - logger.error(f"Failed to extract content from URL {url}: {e}") - raise OSError(f"Failed to extract content from URL {url}: {e}") from e + logger.error(t("msg-975d88e0", url=url, e=e)) + raise OSError(t("msg-975d88e0", url=url, e=e)) from e if not text_content: - raise ValueError(f"No content extracted from URL: {url}") + raise ValueError(t("msg-cfe431b3", url=url)) if progress_callback: await progress_callback("extracting", 100, 100) @@ -536,7 +537,7 @@ async def upload_from_url( if enable_cleaning and not final_chunks: raise ValueError( - "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。" + t("msg-e7f5f836") ) # 创建一个虚拟文件名 @@ -575,7 +576,7 @@ async def _clean_and_rechunk_content( if not enable_cleaning: # 如果不启用清洗,则使用从前端传递的参数进行分块 logger.info( - f"内容清洗未启用,使用指定参数进行分块: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}" + t("msg-693aa5c5", chunk_size=chunk_size, chunk_overlap=chunk_overlap) ) return await self.chunker.chunk( content, chunk_size=chunk_size, chunk_overlap=chunk_overlap @@ -583,7 +584,7 @@ async def _clean_and_rechunk_content( if not cleaning_provider_id: logger.warning( - "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。" + t("msg-947d8f46") ) return await self.chunker.chunk(content) @@ -595,7 +596,7 @@ async def _clean_and_rechunk_content( llm_provider = await self.prov_mgr.get_provider_by_id(cleaning_provider_id) if not llm_provider or not isinstance(llm_provider, LLMProvider): raise ValueError( - f"无法找到 ID 为 {cleaning_provider_id} 的 LLM Provider 或类型不正确" + t("msg-31963d3f", cleaning_provider_id=cleaning_provider_id) ) # 初步分块 @@ -606,7 +607,7 @@ async def _clean_and_rechunk_content( separators=["\n\n", "\n", " "], # 优先使用段落分隔符 ) initial_chunks = await text_splitter.chunk(content) - logger.info(f"初步分块完成,生成 {len(initial_chunks)} 个块用于修复。") + logger.info(t("msg-82728272", res=len(initial_chunks))) # 并发处理所有块 rate_limiter = RateLimiter(repair_max_rpm) @@ -622,13 +623,13 @@ async def _clean_and_rechunk_content( final_chunks = [] for i, result in enumerate(repaired_results): if isinstance(result, Exception): - logger.warning(f"块 {i} 处理异常: {str(result)}. 回退到原始块。") + logger.warning(t("msg-6fa5fdca", i=i, res=str(result))) final_chunks.append(initial_chunks[i]) elif isinstance(result, list): final_chunks.extend(result) logger.info( - f"文本修复完成: {len(initial_chunks)} 个原始块 -> {len(final_chunks)} 个最终块。" + t("msg-6780e950", res=len(initial_chunks), res_2=len(final_chunks)) ) if progress_callback: @@ -637,6 +638,6 @@ async def _clean_and_rechunk_content( return final_chunks except Exception as e: - logger.error(f"使用 Provider '{cleaning_provider_id}' 清洗内容失败: {e}") + logger.error(t("msg-79056c76", cleaning_provider_id=cleaning_provider_id, e=e)) # 清洗失败,返回默认分块结果,保证流程不中断 return await self.chunker.chunk(content) diff --git a/astrbot/core/knowledge_base/kb_mgr.py b/astrbot/core/knowledge_base/kb_mgr.py index f26409e56e..9eb0b9e64d 100644 --- a/astrbot/core/knowledge_base/kb_mgr.py +++ b/astrbot/core/knowledge_base/kb_mgr.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import traceback from pathlib import Path @@ -37,7 +38,7 @@ def __init__( async def initialize(self) -> None: """初始化知识库模块""" try: - logger.info("正在初始化知识库模块...") + logger.info(t("msg-98bfa670")) # 初始化数据库 await self._init_kb_database() @@ -53,17 +54,17 @@ async def initialize(self) -> None: await self.load_kbs() except ImportError as e: - logger.error(f"知识库模块导入失败: {e}") - logger.warning("请确保已安装所需依赖: pypdf, aiofiles, Pillow, rank-bm25") + logger.error(t("msg-7da7ae15", e=e)) + logger.warning(t("msg-842a3c65")) except Exception as e: - logger.error(f"知识库模块初始化失败: {e}") - logger.error(traceback.format_exc()) + logger.error(t("msg-c9e943f7", e=e)) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) async def _init_kb_database(self) -> None: self.kb_db = KBSQLiteDatabase(DB_PATH.as_posix()) await self.kb_db.initialize() await self.kb_db.migrate_to_v1() - logger.info(f"KnowledgeBase database initialized: {DB_PATH}") + logger.info(t("msg-9349e112", DB_PATH=DB_PATH)) async def load_kbs(self) -> None: """加载所有知识库实例""" @@ -94,7 +95,7 @@ async def create_kb( ) -> KBHelper: """创建新的知识库实例""" if embedding_provider_id is None: - raise ValueError("创建知识库时必须提供embedding_provider_id") + raise ValueError(t("msg-7605893e")) kb = KnowledgeBase( kb_name=kb_name, description=description, @@ -125,7 +126,7 @@ async def create_kb( return kb_helper except Exception as e: if "kb_name" in str(e): - raise ValueError(f"知识库名称 '{kb_name}' 已存在") + raise ValueError(t("msg-0b632cbd", kb_name=kb_name)) raise async def get_kb(self, kb_id: str) -> KBHelper | None: @@ -282,7 +283,7 @@ async def terminate(self) -> None: try: await kb_helper.terminate() except Exception as e: - logger.error(f"关闭知识库 {kb_id} 失败: {e}") + logger.error(t("msg-ca30330f", kb_id=kb_id, e=e)) self.kb_insts.clear() @@ -291,7 +292,7 @@ async def terminate(self) -> None: try: await self.kb_db.close() except Exception as e: - logger.error(f"关闭知识库元数据数据库失败: {e}") + logger.error(t("msg-00262e1f", e=e)) async def upload_from_url( self, @@ -325,7 +326,7 @@ async def upload_from_url( """ kb_helper = await self.get_kb(kb_id) if not kb_helper: - raise ValueError(f"Knowledge base with id {kb_id} not found.") + raise ValueError(t("msg-3fc9ef0b", kb_id=kb_id)) return await kb_helper.upload_from_url( url=url, diff --git a/astrbot/core/knowledge_base/parsers/text_parser.py b/astrbot/core/knowledge_base/parsers/text_parser.py index bed2d09b8b..3d46e48dd3 100644 --- a/astrbot/core/knowledge_base/parsers/text_parser.py +++ b/astrbot/core/knowledge_base/parsers/text_parser.py @@ -2,6 +2,7 @@ 支持解析 TXT 和 Markdown 文件。 """ +from astrbot.core.lang import t from astrbot.core.knowledge_base.parsers.base import BaseParser, ParseResult @@ -36,7 +37,7 @@ async def parse(self, file_content: bytes, file_name: str) -> ParseResult: except UnicodeDecodeError: continue else: - raise ValueError(f"无法解码文件: {file_name}") + raise ValueError(t("msg-70cbd40d", file_name=file_name)) # 文本文件无多媒体资源 return ParseResult(text=text, media=[]) diff --git a/astrbot/core/knowledge_base/parsers/url_parser.py b/astrbot/core/knowledge_base/parsers/url_parser.py index 2867164a96..8990dde592 100644 --- a/astrbot/core/knowledge_base/parsers/url_parser.py +++ b/astrbot/core/knowledge_base/parsers/url_parser.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import aiohttp @@ -14,7 +15,7 @@ def __init__(self, tavily_keys: list[str]) -> None: tavily_keys: Tavily API 密钥列表 """ if not tavily_keys: - raise ValueError("Error: Tavily API keys are not configured.") + raise ValueError(t("msg-2de85bf5")) self.tavily_keys = tavily_keys self.tavily_key_index = 0 @@ -44,7 +45,7 @@ async def extract_text_from_url(self, url: str) -> str: IOError: 如果请求失败或返回错误 """ if not url: - raise ValueError("Error: url must be a non-empty string.") + raise ValueError(t("msg-98ed69f4")) tavily_key = await self._get_tavily_key() api_url = "https://api.tavily.com/extract" @@ -69,22 +70,22 @@ async def extract_text_from_url(self, url: str) -> str: if response.status != 200: reason = await response.text() raise OSError( - f"Tavily web extraction failed: {reason}, status: {response.status}" + t("msg-7b14cdb7", reason=reason, res=response.status) ) data = await response.json() results = data.get("results", []) if not results: - raise ValueError(f"No content extracted from URL: {url}") + raise ValueError(t("msg-cfe431b3", url=url)) # 返回第一个结果的内容 return results[0].get("raw_content", "") except aiohttp.ClientError as e: - raise OSError(f"Failed to fetch URL {url}: {e}") from e + raise OSError(t("msg-b0897365", url=url, e=e)) from e except Exception as e: - raise OSError(f"Failed to extract content from URL {url}: {e}") from e + raise OSError(t("msg-975d88e0", url=url, e=e)) from e # 为了向后兼容,提供一个简单的函数接口 diff --git a/astrbot/core/knowledge_base/parsers/util.py b/astrbot/core/knowledge_base/parsers/util.py index 7a44632022..0d99c93b9b 100644 --- a/astrbot/core/knowledge_base/parsers/util.py +++ b/astrbot/core/knowledge_base/parsers/util.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from .base import BaseParser @@ -10,4 +11,4 @@ async def select_parser(ext: str) -> BaseParser: from .pdf_parser import PDFParser return PDFParser() - raise ValueError(f"暂时不支持的文件格式: {ext}") + raise ValueError(t("msg-398b3580", ext=ext)) diff --git a/astrbot/core/knowledge_base/retrieval/manager.py b/astrbot/core/knowledge_base/retrieval/manager.py index 1244e18af1..d023f690c8 100644 --- a/astrbot/core/knowledge_base/retrieval/manager.py +++ b/astrbot/core/knowledge_base/retrieval/manager.py @@ -2,6 +2,7 @@ 协调稠密检索、稀疏检索和 Rerank,提供统一的检索接口 """ +from astrbot.core.lang import t import time from dataclasses import dataclass @@ -102,7 +103,7 @@ async def retrieve( } new_kb_ids.append(kb_id) else: - logger.warning(f"知识库 ID {kb_id} 实例未找到, 已跳过该知识库的检索") + logger.warning(t("msg-fcc0dde2", kb_id=kb_id)) kb_ids = new_kb_ids @@ -115,7 +116,7 @@ async def retrieve( ) time_end = time.time() logger.debug( - f"Dense retrieval across {len(kb_ids)} bases took {time_end - time_start:.2f}s and returned {len(dense_results)} results.", + t("msg-320cfcff", res=len(kb_ids), res_2=time_end - time_start, res_3=len(dense_results)), ) # 2. 稀疏检索 @@ -127,7 +128,7 @@ async def retrieve( ) time_end = time.time() logger.debug( - f"Sparse retrieval across {len(kb_ids)} bases took {time_end - time_start:.2f}s and returned {len(sparse_results)} results.", + t("msg-90ffcfc8", res=len(kb_ids), res_2=time_end - time_start, res_3=len(sparse_results)), ) # 3. 结果融合 @@ -139,7 +140,7 @@ async def retrieve( ) time_end = time.time() logger.debug( - f"Rank fusion took {time_end - time_start:.2f}s and returned {len(fused_results)} results.", + t("msg-12bcf404", res=time_end - time_start, res_2=len(fused_results)), ) # 4. 转换为 RetrievalResult (批量获取元数据) @@ -171,7 +172,7 @@ async def retrieve( for kb_id in kb_ids: vec_db = kb_options[kb_id]["vec_db"] if not isinstance(vec_db, FaissVecDB): - logger.warning(f"vec_db for kb_id {kb_id} is not FaissVecDB") + logger.warning(t("msg-28c084bc", kb_id=kb_id)) continue rerank_pi = kb_options[kb_id]["rerank_provider_id"] @@ -231,7 +232,7 @@ async def _dense_retrieve( except Exception as e: from astrbot.core import logger - logger.warning(f"知识库 {kb_id} 稠密检索失败: {e}") + logger.warning(t("msg-cc0230a3", kb_id=kb_id, e=e)) continue # 按相似度排序并返回 top_k diff --git a/astrbot/core/lang.py b/astrbot/core/lang.py new file mode 100644 index 0000000000..079b83e851 --- /dev/null +++ b/astrbot/core/lang.py @@ -0,0 +1,241 @@ +# lang.py +import threading +from pathlib import Path + +from fluent.runtime import FluentLocalization, FluentResourceLoader + +from astrbot.core.utils.astrbot_path import get_astrbot_path + + +class Lang: + def __init__( + self, + locale: str = "zh-cn", + files: list[str] | None = None, + namespace_paths: dict[str, str | Path] | None = None, + namespace_files: dict[str, list[str]] | None = None, + default_namespace: str = "default", + ): + self._lock = threading.RLock() + self.locale = locale + self.files = files + self.default_namespace = default_namespace + + base_dir = self._get_core_locales_dir() + if namespace_paths is None: + self.namespace_paths: dict[str, Path] = {self.default_namespace: base_dir} + else: + self.namespace_paths = { + namespace: Path(path) for namespace, path in namespace_paths.items() + } + if self.default_namespace not in self.namespace_paths: + self.namespace_paths[self.default_namespace] = base_dir + + self.namespace_files = namespace_files or {} + self.available_locales: list[str] = [] + self.available_locales_by_namespace: dict[str, list[str]] = {} + self._l10n_map: dict[str, FluentLocalization] = {} + self.load_locale(self.locale, self.files) + + @staticmethod + def _get_core_locales_dir() -> Path: + return Path(get_astrbot_path()) / "astrbot" / "i18n" / "locales" + + @staticmethod + def _validate_namespace(namespace: str) -> None: + if not namespace: + raise ValueError("Namespace must not be empty.") + if "." in namespace: + raise ValueError(t("msg-f66527da")) + + @staticmethod + def _collect_files(base_dir: Path, files: list[str] | None) -> list[str]: + if files is not None: + return files + + files_set = set() + for locale_dir in (d for d in base_dir.iterdir() if d.is_dir()): + for ftl_file in locale_dir.glob("*.ftl"): + files_set.add(ftl_file.name) + return sorted(files_set) + + @staticmethod + def _match_locale(available_locales: list[str], locale: str) -> str: + return next( + ( + locale_name + for locale_name in available_locales + if locale_name.lower() == locale.lower() + ), + locale, + ) + + @staticmethod + def _normalize_locale(available_locales: list[str], locale: str) -> str | None: + return next( + ( + locale_name + for locale_name in available_locales + if locale_name.lower() == locale.lower() + ), + None, + ) + + def _build_localization( + self, base_dir: Path, locale: str, files: list[str] | None + ) -> tuple[FluentLocalization, list[str]]: + if not base_dir.exists() or not base_dir.is_dir(): + raise ValueError(t("msg-b3665aee", base_dir=base_dir)) + + available_locales = [d.name for d in base_dir.iterdir() if d.is_dir()] + if not available_locales: + raise ValueError(t("msg-3fe89e6a", base_dir=base_dir)) + + matched_locale = self._match_locale(available_locales, locale) + merged_files = self._collect_files(base_dir, files) + loader = FluentResourceLoader(str(base_dir / "{locale}")) + + locales_preference = [matched_locale] + if "zh-cn" in available_locales and matched_locale.lower() != "zh-cn": + locales_preference.append("zh-cn") + + return FluentLocalization( + locales_preference, merged_files, loader + ), available_locales + + def _update_available_locales(self) -> None: + if self.default_namespace in self.available_locales_by_namespace: + self.available_locales = self.available_locales_by_namespace[ + self.default_namespace + ] + return + + all_locales: set[str] = set() + for locales in self.available_locales_by_namespace.values(): + all_locales.update(locales) + self.available_locales = sorted(all_locales) + + def _refresh_namespace(self, namespace: str) -> None: + base_dir = self.namespace_paths[namespace] + ns_files = self.namespace_files.get(namespace, self.files) + l10n, available_locales = self._build_localization( + base_dir, self.locale, ns_files + ) + self._l10n_map[namespace] = l10n + self.available_locales_by_namespace[namespace] = available_locales + self._update_available_locales() + + def load_locale( + self, + locale: str = "zh-cn", + files: list[str] | None = None, + namespace_paths: dict[str, str | Path] | None = None, + namespace_files: dict[str, list[str]] | None = None, + ): + with self._lock: + if namespace_paths is not None: + self.namespace_paths = { + namespace: Path(path) for namespace, path in namespace_paths.items() + } + if self.default_namespace not in self.namespace_paths: + self.namespace_paths[self.default_namespace] = ( + self._get_core_locales_dir() + ) + if namespace_files is not None: + self.namespace_files = namespace_files + + self.locale = locale + if files is not None: + self.files = files + + l10n_map: dict[str, FluentLocalization] = {} + available_by_namespace: dict[str, list[str]] = {} + for namespace, base_dir in self.namespace_paths.items(): + ns_files = self.namespace_files.get(namespace, self.files) + l10n, available_locales = self._build_localization( + base_dir, locale, ns_files + ) + l10n_map[namespace] = l10n + available_by_namespace[namespace] = available_locales + + self._l10n_map = l10n_map + self.available_locales_by_namespace = available_by_namespace + self._update_available_locales() + + def register_namespace( + self, + namespace: str, + path: str | Path, + files: list[str] | None = None, + replace: bool = False, + ) -> None: + self._validate_namespace(namespace) + with self._lock: + if namespace in self.namespace_paths and not replace: + raise ValueError(t("msg-c79b2c75", namespace=namespace)) + + self.namespace_paths[namespace] = Path(path) + if files is None: + self.namespace_files.pop(namespace, None) + else: + self.namespace_files[namespace] = files + self._refresh_namespace(namespace) + + def unregister_namespace(self, namespace: str) -> None: + self._validate_namespace(namespace) + with self._lock: + if namespace == self.default_namespace: + raise ValueError(t("msg-7db3fccf")) + if namespace not in self.namespace_paths: + raise ValueError(t("msg-3d066f64", namespace=namespace)) + + self.namespace_paths.pop(namespace, None) + self.namespace_files.pop(namespace, None) + self._l10n_map.pop(namespace, None) + self.available_locales_by_namespace.pop(namespace, None) + self._update_available_locales() + + def list_namespaces(self) -> list[str]: + with self._lock: + return sorted(self.namespace_paths.keys()) + + def get_namespace_meta(self) -> dict[str, dict[str, object]]: + with self._lock: + return { + namespace: { + "path": str(path), + "files": self.namespace_files.get(namespace), + "available_locales": self.available_locales_by_namespace.get( + namespace, [] + ), + } + for namespace, path in self.namespace_paths.items() + } + + def _resolve_key(self, key: str) -> tuple[str, str]: + if "." not in key: + return self.default_namespace, key + + namespace, real_key = key.split(".", 1) + if namespace in self._l10n_map and real_key: + return namespace, real_key + return self.default_namespace, key + + def normalize_locale(self, locale: str | None) -> str | None: + if not locale: + return None + with self._lock: + return self._normalize_locale(self.available_locales, str(locale)) + + def __call__(self, key: str, **kwargs) -> str: + if not key: + return "" + with self._lock: + namespace, real_key = self._resolve_key(key) + l10n = ( + self._l10n_map.get(namespace) or self._l10n_map[self.default_namespace] + ) + return l10n.format_value(real_key, kwargs) + + +t = Lang(locale="zh-cn") diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 66a2f31543..a4f5e2a1cf 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -1,4 +1,5 @@ """日志系统,统一将标准 logging 输出转发到 loguru。""" +from astrbot.core.lang import t import asyncio import logging @@ -374,7 +375,7 @@ def configure_logger( trace=False, ) except Exception as e: - logger.error(f"Failed to add file sink: {e}") + logger.error(t("msg-80a186b8", e=e)) @classmethod def configure_trace_logger(cls, config: dict | None) -> None: diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 15265c38d1..5c3af65f37 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -20,6 +20,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from astrbot.core.lang import t import asyncio import base64 @@ -139,7 +140,7 @@ def fromFileSystem(path, **_): def fromURL(url: str, **_): if url.startswith("http://") or url.startswith("https://"): return Record(file=url, **_) - raise Exception("not a valid url") + raise Exception(t("msg-afb10076")) @staticmethod def fromBase64(bs64_data: str, **_): @@ -153,7 +154,7 @@ async def convert_to_file_path(self) -> str: """ if not self.file: - raise Exception(f"not a valid file: {self.file}") + raise Exception(t("msg-fe4c33a0", res=self.file)) if self.file.startswith("file:///"): return self.file[8:] if self.file.startswith("http"): @@ -170,7 +171,7 @@ async def convert_to_file_path(self) -> str: return os.path.abspath(file_path) if os.path.exists(self.file): return os.path.abspath(self.file) - raise Exception(f"not a valid file: {self.file}") + raise Exception(t("msg-fe4c33a0", res=self.file)) async def convert_to_base64(self) -> str: """将语音统一转换为 base64 编码。这个方法避免了手动判断语音数据类型,直接返回语音数据的 base64 编码。 @@ -181,7 +182,7 @@ async def convert_to_base64(self) -> str: """ # convert to base64 if not self.file: - raise Exception(f"not a valid file: {self.file}") + raise Exception(t("msg-fe4c33a0", res=self.file)) if self.file.startswith("file:///"): bs64_data = file_to_base64(self.file[8:]) elif self.file.startswith("http"): @@ -192,7 +193,7 @@ async def convert_to_base64(self) -> str: elif os.path.exists(self.file): bs64_data = file_to_base64(self.file) else: - raise Exception(f"not a valid file: {self.file}") + raise Exception(t("msg-fe4c33a0", res=self.file)) bs64_data = bs64_data.removeprefix("base64://") return bs64_data @@ -209,13 +210,13 @@ async def register_to_file_service(self) -> str: callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception(t("msg-24d98e13")) file_path = await self.convert_to_file_path() token = await file_token_service.register_file(file_path) - logger.debug(f"已注册:{callback_host}/api/file/{token}") + logger.debug(t("msg-a5c69cc9", callback_host=callback_host, token=token)) return f"{callback_host}/api/file/{token}" @@ -239,7 +240,7 @@ def fromFileSystem(path, **_): def fromURL(url: str, **_): if url.startswith("http://") or url.startswith("https://"): return Video(file=url, **_) - raise Exception("not a valid url") + raise Exception(t("msg-afb10076")) async def convert_to_file_path(self) -> str: """将这个视频统一转换为本地文件路径。这个方法避免了手动判断视频数据类型,直接返回视频数据的本地路径(如果是网络 URL,则会自动进行下载)。 @@ -258,10 +259,10 @@ async def convert_to_file_path(self) -> str: await download_file(url, video_file_path) if os.path.exists(video_file_path): return os.path.abspath(video_file_path) - raise Exception(f"download failed: {url}") + raise Exception(t("msg-3cddc5ef", url=url)) if os.path.exists(url): return os.path.abspath(url) - raise Exception(f"not a valid file: {url}") + raise Exception(t("msg-1921aa47", url=url)) async def register_to_file_service(self) -> str: """将视频注册到文件服务。 @@ -276,13 +277,13 @@ async def register_to_file_service(self) -> str: callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception(t("msg-24d98e13")) file_path = await self.convert_to_file_path() token = await file_token_service.register_file(file_path) - logger.debug(f"已注册:{callback_host}/api/file/{token}") + logger.debug(t("msg-a5c69cc9", callback_host=callback_host, token=token)) return f"{callback_host}/api/file/{token}" @@ -295,7 +296,7 @@ async def to_dict(self): callback_host = str(callback_host).removesuffix("/") token = await file_token_service.register_file(url_or_path) payload_file = f"{callback_host}/api/file/{token}" - logger.debug(f"Generated video file callback link: {payload_file}") + logger.debug(t("msg-2ee3827c", payload_file=payload_file)) else: payload_file = url_or_path return { @@ -417,7 +418,7 @@ def __init__(self, file: str | None, **_) -> None: def fromURL(url: str, **_): if url.startswith("http://") or url.startswith("https://"): return Image(file=url, **_) - raise Exception("not a valid url") + raise Exception(t("msg-afb10076")) @staticmethod def fromFileSystem(path, **_): @@ -444,7 +445,7 @@ async def convert_to_file_path(self) -> str: """ url = self.url or self.file if not url: - raise ValueError("No valid file or URL provided") + raise ValueError(t("msg-32f4fc78")) if url.startswith("file:///"): return url[8:] if url.startswith("http"): @@ -461,7 +462,7 @@ async def convert_to_file_path(self) -> str: return os.path.abspath(image_file_path) if os.path.exists(url): return os.path.abspath(url) - raise Exception(f"not a valid file: {url}") + raise Exception(t("msg-1921aa47", url=url)) async def convert_to_base64(self) -> str: """将这个图片统一转换为 base64 编码。这个方法避免了手动判断图片数据类型,直接返回图片数据的 base64 编码。 @@ -473,7 +474,7 @@ async def convert_to_base64(self) -> str: # convert to base64 url = self.url or self.file if not url: - raise ValueError("No valid file or URL provided") + raise ValueError(t("msg-32f4fc78")) if url.startswith("file:///"): bs64_data = file_to_base64(url[8:]) elif url.startswith("http"): @@ -484,7 +485,7 @@ async def convert_to_base64(self) -> str: elif os.path.exists(url): bs64_data = file_to_base64(url) else: - raise Exception(f"not a valid file: {url}") + raise Exception(t("msg-1921aa47", url=url)) bs64_data = bs64_data.removeprefix("base64://") return bs64_data @@ -501,13 +502,13 @@ async def register_to_file_service(self) -> str: callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception(t("msg-24d98e13")) file_path = await self.convert_to_file_path() token = await file_token_service.register_file(file_path) - logger.debug(f"已注册:{callback_host}/api/file/{token}") + logger.debug(t("msg-a5c69cc9", callback_host=callback_host, token=token)) return f"{callback_host}/api/file/{token}" @@ -679,9 +680,7 @@ def file(self) -> str: loop = asyncio.get_event_loop() if loop.is_running(): logger.warning( - "不可以在异步上下文中同步等待下载! " - "这个警告通常发生于某些逻辑试图通过 .file 获取文件消息段的文件内容。" - "请使用 await get_file() 代替直接获取 .file 字段", + t("msg-36375f4c"), ) return "" # 等待下载完成 @@ -690,7 +689,7 @@ def file(self) -> str: if self.file_ and os.path.exists(self.file_): return os.path.abspath(self.file_) except Exception as e: - logger.error(f"文件下载失败: {e}") + logger.error(t("msg-4a987754", e=e)) return "" @@ -758,7 +757,7 @@ async def get_file(self, allow_return_url: bool = False) -> str: async def _download_file(self) -> None: """下载文件""" if not self.url: - raise ValueError("Download failed: No URL provided in File component.") + raise ValueError(t("msg-7c1935ee")) download_dir = get_astrbot_temp_path() if self.name: name, ext = os.path.splitext(self.name) @@ -782,13 +781,13 @@ async def register_to_file_service(self) -> str: callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception(t("msg-24d98e13")) file_path = await self.get_file() token = await file_token_service.register_file(file_path) - logger.debug(f"已注册:{callback_host}/api/file/{token}") + logger.debug(t("msg-a5c69cc9", callback_host=callback_host, token=token)) return f"{callback_host}/api/file/{token}" @@ -801,7 +800,7 @@ async def to_dict(self): callback_host = str(callback_host).removesuffix("/") token = await file_token_service.register_file(url_or_path) payload_file = f"{callback_host}/api/file/{token}" - logger.debug(f"Generated file callback link: {payload_file}") + logger.debug(t("msg-35bb8d53", payload_file=payload_file)) else: payload_file = url_or_path return { diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index d141f40e43..1c8fb2e7b3 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -3,6 +3,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.db import BaseDatabase from astrbot.core.db.po import Persona, PersonaFolder, Personality +from astrbot.core.lang import t from astrbot.core.platform.message_session import MessageSession from astrbot.core.sentinels import NOT_GIVEN @@ -35,13 +36,13 @@ def __init__(self, db_helper: BaseDatabase, acm: AstrBotConfigManager) -> None: async def initialize(self) -> None: self.personas = await self.get_all_personas() self.get_v3_persona_data() - logger.info(f"已加载 {len(self.personas)} 个人格。") + logger.info(t("msg-51a854e6", res=len(self.personas))) async def get_persona(self, persona_id: str): """获取指定 persona 的信息""" persona = await self.db.get_persona_by_id(persona_id) if not persona: - raise ValueError(f"Persona with ID {persona_id} does not exist.") + raise ValueError(t("msg-1ea88f45", persona_id=persona_id)) return persona async def get_default_persona_v3( @@ -118,7 +119,7 @@ async def resolve_selected_persona( async def delete_persona(self, persona_id: str) -> None: """删除指定 persona""" if not await self.db.get_persona_by_id(persona_id): - raise ValueError(f"Persona with ID {persona_id} does not exist.") + raise ValueError(t("msg-1ea88f45", persona_id=persona_id)) await self.db.delete_persona(persona_id) self.personas = [p for p in self.personas if p.persona_id != persona_id] self.get_v3_persona_data() @@ -135,7 +136,7 @@ async def update_persona( """更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具""" existing_persona = await self.db.get_persona_by_id(persona_id) if not existing_persona: - raise ValueError(f"Persona with ID {persona_id} does not exist.") + raise ValueError(t("msg-1ea88f45", persona_id=persona_id)) update_kwargs = {} if tools is not NOT_GIVEN: update_kwargs["tools"] = tools @@ -324,7 +325,7 @@ async def create_persona( sort_order: 排序顺序 """ if await self.db.get_persona_by_id(persona_id): - raise ValueError(f"Persona with ID {persona_id} already exists.") + raise ValueError(t("msg-28104dff", persona_id=persona_id)) new_persona = await self.db.insert_persona( persona_id, system_prompt, @@ -372,7 +373,7 @@ def get_v3_persona_data( if begin_dialogs: if len(begin_dialogs) % 2 != 0: logger.error( - f"{persona_cfg['name']} 人格情景预设对话格式不对,条数应该为偶数。", + t("msg-08ecfd42", res=persona_cfg["name"]), ) begin_dialogs = [] user_turn = True @@ -396,7 +397,7 @@ def get_v3_persona_data( selected_default_persona = persona personas_v3.append(persona) except Exception as e: - logger.error(f"解析 Persona 配置失败:{e}") + logger.error(t("msg-b6292b94", e=e)) if not selected_default_persona and len(personas_v3) > 0: # 默认选择第一个 diff --git a/astrbot/core/pipeline/__init__.py b/astrbot/core/pipeline/__init__.py index 6a6069ff77..7cfe9c639a 100644 --- a/astrbot/core/pipeline/__init__.py +++ b/astrbot/core/pipeline/__init__.py @@ -6,6 +6,7 @@ """ from __future__ import annotations +from astrbot.core.lang import t from importlib import import_module from typing import TYPE_CHECKING, Any @@ -97,7 +98,7 @@ def __getattr__(name: str) -> Any: if name not in _LAZY_EXPORTS: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + raise AttributeError(t("msg-1c9fc93d", __name__=__name__, name=name)) module_path, attr_name = _LAZY_EXPORTS[name] module = import_module(module_path) value = getattr(module, attr_name) diff --git a/astrbot/core/pipeline/content_safety_check/stage.py b/astrbot/core/pipeline/content_safety_check/stage.py index 19037eb081..2f719278da 100644 --- a/astrbot/core/pipeline/content_safety_check/stage.py +++ b/astrbot/core/pipeline/content_safety_check/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator from astrbot.core import logger @@ -32,10 +33,10 @@ async def process( if event.is_at_or_wake_command: event.set_result( MessageEventResult().message( - "你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。", + t("msg-c733275f"), ), ) yield event.stop_event() - logger.info(f"内容安全检查不通过,原因:{info}") + logger.info(t("msg-46c80f28", info=info)) return diff --git a/astrbot/core/pipeline/content_safety_check/strategies/strategy.py b/astrbot/core/pipeline/content_safety_check/strategies/strategy.py index c971ef26ff..2d66a1685e 100644 --- a/astrbot/core/pipeline/content_safety_check/strategies/strategy.py +++ b/astrbot/core/pipeline/content_safety_check/strategies/strategy.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot import logger from . import ContentSafetyStrategy @@ -16,7 +17,7 @@ def __init__(self, config: dict) -> None: try: from .baidu_aip import BaiduAipStrategy except ImportError: - logger.warning("使用百度内容审核应该先 pip install baidu-aip") + logger.warning(t("msg-27a700e0")) return self.enabled_strategies.append( BaiduAipStrategy( diff --git a/astrbot/core/pipeline/context_utils.py b/astrbot/core/pipeline/context_utils.py index 9402ce3e62..1450a2e5e0 100644 --- a/astrbot/core/pipeline/context_utils.py +++ b/astrbot/core/pipeline/context_utils.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import inspect import traceback import typing as T @@ -36,7 +37,7 @@ async def call_handler( try: ready_to_call = handler(event, *args, **kwargs) except TypeError: - logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True) + logger.error(t("msg-49f260d3"), exc_info=True) if not ready_to_call: return @@ -60,7 +61,7 @@ async def call_handler( # 如果这个异步生成器没有执行到 yield 分支 yield except Exception as e: - logger.error(f"Previous Error: {trace_}") + logger.error(t("msg-d7b4aa84", trace_=trace_)) raise e elif inspect.iscoroutine(ready_to_call): # 如果只是一个协程, 直接执行 @@ -93,15 +94,15 @@ async def call_event_hook( try: assert inspect.iscoroutinefunction(handler.handler) logger.debug( - f"hook({hook_type.name}) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", + t("msg-eb8619cb", res=hook_type.name, res_2=star_map[handler.handler_module_path].name, res_3=handler.handler_name), ) await handler.handler(event, *args, **kwargs) except BaseException: - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) if event.is_stopped(): logger.info( - f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", + t("msg-add19f94", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) return True diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index 464f584f8e..8be8711927 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import random import traceback @@ -44,7 +45,7 @@ async def process( try: await event.react(random.choice(emojis)) except Exception as e: - logger.warning(f"{platform} 预回应表情发送失败: {e}") + logger.warning(t("msg-7b9074fa", platform=platform, e=e)) # 路径映射 if mappings := self.platform_settings.get("path_mapping", []): @@ -61,7 +62,7 @@ async def process( url = component.url.removeprefix("file://") if url.startswith(from_): component.url = url.replace(from_, to_, 1) - logger.debug(f"路径映射: {url} -> {component.url}") + logger.debug(t("msg-43f1b4ed", url=url, res=component.url)) message_chain[idx] = component # STT @@ -71,7 +72,7 @@ async def process( stt_provider = ctx.get_using_stt_provider(event.unified_msg_origin) if not stt_provider: logger.warning( - f"会话 {event.unified_msg_origin} 未配置语音转文本模型。", + t("msg-9549187d", res=event.unified_msg_origin), ) return message_chain = event.get_messages() @@ -90,11 +91,11 @@ async def process( break except FileNotFoundError as e: # napcat workaround - logger.warning(e) - logger.warning(f"重试中: {i + 1}/{retry}") + logger.warning(t("msg-5bdf8f5c", e=e)) + logger.warning(t("msg-ad90e19e", res=i + 1, retry=retry)) await asyncio.sleep(0.5) continue except BaseException as e: - logger.error(traceback.format_exc()) - logger.error(f"语音转文本失败: {e}") + logger.error(t("msg-78b9c276", res=traceback.format_exc())) + logger.error(t("msg-4f3245bf", e=e)) break diff --git a/astrbot/core/pipeline/process_stage/follow_up.py b/astrbot/core/pipeline/process_stage/follow_up.py index 6c1a4fa06b..ce92085734 100644 --- a/astrbot/core/pipeline/process_stage/follow_up.py +++ b/astrbot/core/pipeline/process_stage/follow_up.py @@ -1,4 +1,5 @@ from __future__ import annotations +from astrbot.core.lang import t import asyncio from dataclasses import dataclass @@ -185,9 +186,7 @@ def try_capture_follow_up(event: AstrMessageEvent) -> FollowUpCapture | None: ) ) logger.info( - "Captured follow-up message for active agent run, umo=%s, order_seq=%s", - event.unified_msg_origin, - order_seq, + t("msg-12767505", res=event.unified_msg_origin, order_seq=order_seq), ) return FollowUpCapture( umo=event.unified_msg_origin, diff --git a/astrbot/core/pipeline/process_stage/method/agent_request.py b/astrbot/core/pipeline/process_stage/method/agent_request.py index 9efe538146..d83b447601 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_request.py +++ b/astrbot/core/pipeline/process_stage/method/agent_request.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator from astrbot.core import logger @@ -20,7 +21,7 @@ async def initialize(self, ctx: PipelineContext) -> None: for bwp in self.bot_wake_prefixs: if self.prov_wake_prefix.startswith(bwp): logger.info( - f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。", + t("msg-3267978a", res=self.prov_wake_prefix, bwp=bwp), ) self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :] @@ -34,13 +35,13 @@ async def initialize(self, ctx: PipelineContext) -> None: async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]: if not self.ctx.astrbot_config["provider_settings"]["enable"]: logger.debug( - "This pipeline does not enable AI capability, skip processing." + t("msg-97a4d573") ) return if not await SessionServiceManager.should_process_llm_request(event): logger.debug( - f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing." + t("msg-f1a11d2b", res=event.unified_msg_origin) ) return diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 523d758a0a..0a9a73b7c4 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -13,6 +13,7 @@ MainAgentBuildResult, build_main_agent, ) +from astrbot.core.lang import t from astrbot.core.message.components import File, Image from astrbot.core.message.message_event_result import ( MessageChain, @@ -58,8 +59,7 @@ async def initialize(self, ctx: PipelineContext) -> None: self.tool_schema_mode: str = settings.get("tool_schema_mode", "full") if self.tool_schema_mode not in ("skills_like", "full"): logger.warning( - "Unsupported tool_schema_mode: %s, fallback to skills_like", - self.tool_schema_mode, + t("msg-73bf9e45", res=self.tool_schema_mode), ) self.tool_schema_mode = "full" if isinstance(self.max_step, bool): # workaround: #2622 @@ -160,10 +160,10 @@ async def process( and not has_valid_message and not has_media_content ): - logger.debug("skip llm request: empty message and no provider_request") + logger.debug(t("msg-9cdb2b6e")) return - logger.debug("ready to request llm provider") + logger.debug(t("msg-e461e5af")) follow_up_capture = try_capture_follow_up(event) if follow_up_capture: ( @@ -172,9 +172,11 @@ async def process( ) = await prepare_follow_up_capture(follow_up_capture) if follow_up_consumed_marked: logger.info( - "Follow-up ticket already consumed, stopping processing. umo=%s, seq=%s", - event.unified_msg_origin, - follow_up_capture.ticket.seq, + t( + "msg-4d2645f7", + res=event.unified_msg_origin, + res_2=follow_up_capture.ticket.seq, + ), ) return @@ -182,7 +184,7 @@ async def process( await call_event_hook(event, EventType.OnWaitingLLMRequestEvent) async with session_lock_manager.acquire_lock(event.unified_msg_origin): - logger.debug("acquired session lock for llm request") + logger.debug(t("msg-abd5ccbc")) agent_runner: AgentRunner | None = None runner_registered = False try: @@ -211,8 +213,7 @@ async def process( for host in decoded_blocked: if host in api_base: logger.error( - "Provider API base %s is blocked due to security reasons. Please use another ai provider.", - api_base, + t("msg-abc0d82d", api_base=api_base), ) return @@ -248,7 +249,7 @@ async def process( # 检测 Live Mode if action_type == "live": # Live Mode: 使用 run_live_agent - logger.info("[Internal Agent] 检测到 Live Mode,启用 TTS 处理") + logger.info(t("msg-3247374d")) # 获取 TTS Provider tts_provider = ( @@ -258,9 +259,7 @@ async def process( ) if not tts_provider: - logger.warning( - "[Live Mode] TTS Provider 未配置,将使用普通流式模式" - ) + logger.warning(t("msg-dae92399")) # 使用 run_live_agent,总是使用流式响应 event.set_result( @@ -368,14 +367,14 @@ async def process( unregister_active_runner(event.unified_msg_origin, agent_runner) except Exception as e: - logger.error(f"Error occurred while processing agent: {e}") + logger.error(t("msg-1b1af61e", e=e)) custom_error_message = extract_persona_custom_error_message_from_event( event ) error_text = custom_error_message or ( f"Error occurred while processing agent request: {e}" ) - await event.send(MessageChain().message(error_text)) + await event.send(MessageChain().message(t("msg-76945a59", error_text=error_text))) finally: if follow_up_capture: await finalize_follow_up_capture( @@ -414,7 +413,7 @@ async def _save_to_history( and not req.tool_calls_result and not user_aborted ): - logger.debug("LLM 响应为空,不保存记录。") + logger.debug(t("msg-ee7e792b")) return message_to_save = [] diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py index ffaec00b49..2e3d4fd83c 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py @@ -17,6 +17,7 @@ ) from astrbot.core.agent.runners.dify.dify_agent_runner import DifyAgentRunner from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS +from astrbot.core.lang import t from astrbot.core.message.components import Image from astrbot.core.message.message_event_result import ( MessageChain, @@ -80,7 +81,7 @@ async def run_third_party_agent( elif resp.type == "err": yield resp.data["chain"], True except Exception as e: - logger.error(f"Third party agent runner error: {e}") + logger.error(t("msg-5e551baf", e=e)) err_msg = custom_error_message if not err_msg: err_msg = ( @@ -88,7 +89,7 @@ async def run_third_party_agent( f"Error Type: {type(e).__name__} (3rd party)\n" f"Error Message: {str(e)}" ) - yield MessageChain().message(err_msg), True + yield MessageChain().message(t("msg-34f164d4", err_msg=err_msg)), True class _RunnerResultAggregator: @@ -107,12 +108,12 @@ def finalize( ) -> tuple[list, bool]: if not final_resp or not final_resp.result_chain: if self.merged_chain: - logger.warning(RUNNER_NO_FINAL_RESPONSE_LOG) + logger.warning(t("msg-67c22b5b", RUNNER_NO_FINAL_RESPONSE_LOG=RUNNER_NO_FINAL_RESPONSE_LOG)) return self.merged_chain, self.has_error - logger.warning(RUNNER_NO_RESULT_LOG) + logger.warning(t("msg-e9587c7e", RUNNER_NO_RESULT_LOG=RUNNER_NO_RESULT_LOG)) fallback_error_chain = MessageChain().message( - RUNNER_NO_RESULT_FALLBACK_MESSAGE, + t("msg-cdb7e5b6", RUNNER_NO_RESULT_FALLBACK_MESSAGE=RUNNER_NO_RESULT_FALLBACK_MESSAGE), ) return fallback_error_chain.chain or [], True @@ -134,14 +135,13 @@ async def _watchdog() -> None: return if not is_stream_consumed(): logger.warning( - "Third-party runner stream was never consumed in %ss; closing runner to avoid resource leak.", - timeout_sec, + t("msg-13ea140b", timeout_sec=timeout_sec), ) try: await close_runner_once() except Exception: logger.warning( - "Exception while closing third-party runner from stream watchdog.", + t("msg-87a7a566"), exc_info=True, ) @@ -158,7 +158,7 @@ async def _close_runner_if_supported(runner: "BaseAgentRunner") -> None: if inspect.isawaitable(close_result): await close_result except Exception as e: - logger.warning(f"Failed to close third-party runner cleanly: {e}") + logger.warning(t("msg-966b8ef7", e=e)) class ThirdPartyAgentSubStage(Stage): @@ -201,7 +201,7 @@ async def _resolve_persona_custom_error_message( conversation_persona_id=conversation_persona_id, ) except Exception as e: - logger.debug("Failed to resolve persona custom error message: %s", e) + logger.debug(t("msg-371b6b3d", e=e)) return None async def _handle_streaming_response( @@ -301,12 +301,10 @@ async def process( {}, ) if not self.prov_id: - logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。") + logger.error(t("msg-f9d76893")) return if not self.prov_cfg: - logger.error( - f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。" - ) + logger.error(t("msg-0f856470", res=self.prov_id)) return # make provider request @@ -338,7 +336,7 @@ async def process( runner = DeerFlowAgentRunner[AstrAgentContext]() else: raise ValueError( - f"Unsupported third party agent runner type: {self.runner_type}", + t("msg-b3f25c81", res=self.runner_type), ) astr_agent_ctx = AstrAgentContext( diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index 9422d6317a..a523ec2483 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -1,4 +1,5 @@ """本地 Agent 模式的 AstrBot 插件调用 Stage""" +from astrbot.core.lang import t import traceback from collections.abc import AsyncGenerator @@ -38,10 +39,10 @@ async def process( md = star_map.get(handler.handler_module_path) if not md: logger.warning( - f"Cannot find plugin for given handler module path: {handler.handler_module_path}", + t("msg-f0144031", res=handler.handler_module_path), ) continue - logger.debug(f"plugin -> {md.name} - {handler.handler_name}") + logger.debug(t("msg-1e8939dd", res=md.name, res_2=handler.handler_name)) try: wrapper = call_handler(event, handler.handler, **params) async for ret in wrapper: @@ -49,8 +50,8 @@ async def process( event.clear_result() # 清除上一个 handler 的结果 except Exception as e: traceback_text = traceback.format_exc() - logger.error(traceback_text) - logger.error(f"Star {handler.handler_full_name} handle error: {e}") + logger.error(t("msg-6be73b5e", traceback_text=traceback_text)) + logger.error(t("msg-d919bd27", res=handler.handler_full_name, e=e)) await call_event_hook( event, @@ -63,7 +64,7 @@ async def process( if not event.is_stopped() and event.is_at_or_wake_command: ret = f":(\n\n在调用插件 {md.name} 的处理函数 {handler.handler_name} 时出现异常:{e}" - event.set_result(MessageEventResult().message(ret)) + event.set_result(MessageEventResult().message(t("msg-ed8dcc22", ret=ret))) yield event.clear_result() diff --git a/astrbot/core/pipeline/rate_limit_check/stage.py b/astrbot/core/pipeline/rate_limit_check/stage.py index 392bceff30..1a4a7b1cf9 100644 --- a/astrbot/core/pipeline/rate_limit_check/stage.py +++ b/astrbot/core/pipeline/rate_limit_check/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio from collections import defaultdict, deque from collections.abc import AsyncGenerator @@ -72,13 +73,13 @@ async def process( match self.rl_strategy: case RateLimitStrategy.STALL.value: logger.info( - f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", + t("msg-18092978", session_id=session_id, stall_duration=stall_duration), ) await asyncio.sleep(stall_duration) now = datetime.now() case RateLimitStrategy.DISCARD.value: logger.info( - f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到限额于 {stall_duration:.2f} 秒后重置。", + t("msg-4962387a", session_id=session_id, stall_duration=stall_duration), ) return event.stop_event() diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index bd307f8b77..339010a743 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import math import random @@ -85,8 +86,8 @@ async def initialize(self, ctx: PipelineContext) -> None: try: self.interval = [float(t) for t in interval_str_ls] except BaseException as e: - logger.error(f"解析分段回复的间隔时间失败。{e}") - logger.info(f"分段回复间隔时间:{self.interval}") + logger.error(t("msg-59539c6e", e=e)) + logger.info(t("msg-4ddee754", res=self.interval)) async def _word_cnt(self, text: str) -> int: """分段回复 统计字数""" @@ -182,12 +183,12 @@ async def process( return logger.info( - f"Prepare to send - {event.get_sender_name()}/{event.get_sender_id()}: {event._outline_chain(result.chain)}", + t("msg-5e2371a9", res=event.get_sender_name(), res_2=event.get_sender_id(), res_3=event._outline_chain(result.chain)), ) if result.result_content_type == ResultContentType.STREAMING_RESULT: if result.async_stream is None: - logger.warning("async_stream 为空,跳过发送。") + logger.warning(t("msg-df92ac24")) return # 流式结果直接交付平台适配器处理 realtime_segmenting = ( @@ -197,7 +198,7 @@ async def process( ) == "realtime_segmenting" ) - logger.info(f"应用流式输出({event.get_platform_id()})") + logger.info(t("msg-858b0e4f", res=event.get_platform_id())) await event.send_streaming(result.async_stream, realtime_segmenting) return if len(result.chain) > 0: @@ -212,10 +213,10 @@ async def process( # 检查消息链是否为空 try: if await self._is_empty_message_chain(result.chain): - logger.info("消息为空,跳过发送阶段") + logger.info(t("msg-22c7a672")) return except Exception as e: - logger.warning(f"空内容检查异常: {e}") + logger.warning(t("msg-e6ab7a25", e=e)) # 将 Plain 为空的消息段移除 result.chain = [ @@ -239,7 +240,7 @@ async def process( if not result.chain or len(result.chain) == 0: # may fix #2670 logger.warning( - f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}", + t("msg-b29b99c1", header_comps=header_comps, res=result.chain), ) return for comp in result.chain: @@ -253,7 +254,7 @@ async def process( header_comps.clear() except Exception as e: logger.error( - f"发送消息链失败: chain = {MessageChain([comp])}, error = {e}", + t("msg-842df577", res=MessageChain([comp]), e=e), exc_info=True, ) else: @@ -263,7 +264,7 @@ async def process( ): # may fix #2670 logger.warning( - f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}", + t("msg-f35465cf", res=result.chain), ) return sep_comps = self._extract_comp( @@ -277,7 +278,7 @@ async def process( await event.send(chain) except Exception as e: logger.error( - f"发送消息链失败: chain = {chain}, error = {e}", + t("msg-784e8a67", chain=chain, e=e), exc_info=True, ) chain = MessageChain(result.chain) @@ -286,7 +287,7 @@ async def process( await event.send(chain) except Exception as e: logger.error( - f"发送消息链失败: chain = {chain}, error = {e}", + t("msg-784e8a67", chain=chain, e=e), exc_info=True, ) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index f2fe8161b5..e8dbd8748c 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import random import re import time @@ -163,30 +164,30 @@ async def process( for handler in handlers: try: logger.debug( - f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", + t("msg-7ec898fd", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) if is_stream: logger.warning( - "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作", + t("msg-5e27dae6"), ) await handler.handler(event) if (result := event.get_result()) is None or not result.chain: logger.debug( - f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。", + t("msg-caaaec29", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) except BaseException: - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) if event.is_stopped(): logger.info( - f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", + t("msg-add19f94", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) return # 流式输出不执行下面的逻辑 if is_stream: - logger.info("流式输出已启用,跳过结果装饰阶段") + logger.info(t("msg-813a44bb")) return # 需要再获取一次。插件可能直接对 chain 进行了替换。 @@ -231,7 +232,7 @@ async def process( ) except re.error: logger.error( - f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}", + t("msg-891aa43a", res=traceback.format_exc()), ) split_response = re.findall( r".*?[。?!~…]+|.+$", @@ -266,7 +267,7 @@ async def process( ) if should_tts and not tts_provider: logger.warning( - f"会话 {event.unified_msg_origin} 未配置文本转语音模型。", + t("msg-82bb9025", res=event.unified_msg_origin), ) if ( @@ -283,12 +284,12 @@ async def process( for comp in result.chain: if isinstance(comp, Plain) and len(comp.text) > 1: try: - logger.info(f"TTS 请求: {comp.text}") + logger.info(t("msg-fb1c757a", res=comp.text)) audio_path = await tts_provider.get_audio(comp.text) - logger.info(f"TTS 结果: {audio_path}") + logger.info(t("msg-06341d25", audio_path=audio_path)) if not audio_path: logger.error( - f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}", + t("msg-2057f670", res=comp.text), ) new_chain.append(comp) continue @@ -309,7 +310,7 @@ async def process( audio_path, ) url = f"{callback_api_base}/api/file/{token}" - logger.debug(f"已注册:{url}") + logger.debug(t("msg-f26725cf", url=url)) new_chain.append( Record( @@ -321,8 +322,8 @@ async def process( if dual_output: new_chain.append(comp) except Exception: - logger.error(traceback.format_exc()) - logger.error("TTS 失败,使用文本发送。") + logger.error(t("msg-78b9c276", res=traceback.format_exc())) + logger.error(t("msg-47716aec")) new_chain.append(comp) else: new_chain.append(comp) @@ -349,11 +350,11 @@ async def process( template_name=self.t2i_active_template, ) except BaseException: - logger.error("文本转图片失败,使用文本发送。") + logger.error(t("msg-ffe054a9")) return if time.time() - render_start > 3: logger.warning( - "文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。", + t("msg-06c1aedc"), ) if url: if url.startswith("http"): @@ -364,7 +365,7 @@ async def process( ): token = await file_token_service.register_file(url) url = f"{self.ctx.astrbot_config['callback_api_base']}/api/file/{token}" - logger.debug(f"已注册:{url}") + logger.debug(t("msg-f26725cf", url=url)) result.chain = [Image.fromURL(url)] else: result.chain = [Image.fromFileSystem(url)] diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index ffb9c5c99c..d4d2f130d0 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator from astrbot.core import logger @@ -53,7 +54,7 @@ async def _process_stages(self, event: AstrMessageEvent, from_stage=0) -> None: # 此处是前置处理完成后的暂停点(yield), 下面开始执行后续阶段 if event.is_stopped(): logger.debug( - f"阶段 {stage.__class__.__name__} 已终止事件传播。", + t("msg-c240d574", res=stage.__class__.__name__), ) break @@ -63,7 +64,7 @@ async def _process_stages(self, event: AstrMessageEvent, from_stage=0) -> None: # 此处是后续所有阶段处理完毕后返回的点, 执行后置处理 if event.is_stopped(): logger.debug( - f"阶段 {stage.__class__.__name__} 已终止事件传播。", + t("msg-c240d574", res=stage.__class__.__name__), ) break else: @@ -72,7 +73,7 @@ async def _process_stages(self, event: AstrMessageEvent, from_stage=0) -> None: await coroutine if event.is_stopped(): - logger.debug(f"阶段 {stage.__class__.__name__} 已终止事件传播。") + logger.debug(t("msg-c240d574", res=stage.__class__.__name__)) break async def execute(self, event: AstrMessageEvent) -> None: @@ -90,6 +91,6 @@ async def execute(self, event: AstrMessageEvent) -> None: if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent): await event.send(None) - logger.debug("pipeline 执行完毕。") + logger.debug(t("msg-609a1ac5")) finally: active_event_registry.unregister(event) diff --git a/astrbot/core/pipeline/session_status_check/stage.py b/astrbot/core/pipeline/session_status_check/stage.py index 26c3c235a3..f97da430b8 100644 --- a/astrbot/core/pipeline/session_status_check/stage.py +++ b/astrbot/core/pipeline/session_status_check/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator from astrbot.core import logger @@ -22,7 +23,7 @@ async def process( ) -> None | AsyncGenerator[None, None]: # 检查会话是否整体启用 if not await SessionServiceManager.is_session_enabled(event.unified_msg_origin): - logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。") + logger.debug(t("msg-f9aba737", res=event.unified_msg_origin)) # workaround for #2309 conv_id = await self.conv_mgr.get_curr_conversation_id( diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index 2dcb840e91..64d7efcbbf 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator, Callable from astrbot import logger @@ -154,7 +155,7 @@ async def process( event.plugins_name = None else: event.plugins_name = enabled_plugins_name - logger.debug(f"enabled_plugins_name: {enabled_plugins_name}") + logger.debug(t("msg-df815938", enabled_plugins_name=enabled_plugins_name)) for handler in star_handlers_registry.get_handlers_by_event_type( EventType.AdapterMessageEvent, @@ -186,7 +187,7 @@ async def process( except Exception as e: await event.send( MessageEventResult().message( - f"插件 {star_map[handler.handler_module_path].name}: {e}", + t("msg-51182733", res=star_map[handler.handler_module_path].name, e=e), ), ) event.stop_event() @@ -200,11 +201,11 @@ async def process( if self.no_permission_reply: await event.send( MessageChain().message( - f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。", + t("msg-e0dcf0b8", res=event.get_sender_id()), ), ) logger.info( - f"触发 {star_map[handler.handler_module_path].name} 时, 用户(ID={event.get_sender_id()}) 权限不足。", + t("msg-a3c3706f", res=star_map[handler.handler_module_path].name, res_2=event.get_sender_id()), ) event.stop_event() return diff --git a/astrbot/core/pipeline/whitelist_check/stage.py b/astrbot/core/pipeline/whitelist_check/stage.py index ea9c55228e..e85ee53aa6 100644 --- a/astrbot/core/pipeline/whitelist_check/stage.py +++ b/astrbot/core/pipeline/whitelist_check/stage.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from collections.abc import AsyncGenerator from astrbot.core import logger @@ -63,6 +64,6 @@ async def process( ): if self.wl_log: logger.info( - f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。", + t("msg-8282c664", res=event.unified_msg_origin), ) event.stop_event() diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 021a4bff7c..66fe396571 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import abc import asyncio import hashlib @@ -58,8 +59,7 @@ def __init__( message_type = MessageType(str(message_type)) except (ValueError, TypeError, AttributeError): logger.warning( - f"Failed to convert message type {message_obj.type!r} to MessageType. " - f"Falling back to FRIEND_MESSAGE." + t("msg-b593f13f", res=message_obj.type) ) message_type = MessageType.FRIEND_MESSAGE self.session = MessageSession( @@ -225,7 +225,7 @@ def get_extra(self, key: str | None = None, default=None) -> Any: def clear_extra(self) -> None: """清除额外的信息。""" - logger.info(f"清除 {self.get_platform_name()} 的额外信息: {self._extras}") + logger.info(t("msg-98bb33b7", res=self.get_platform_name(), res_2=self._extras)) self._extras.clear() def is_private_chat(self) -> bool: @@ -301,7 +301,7 @@ async def check_count(self, event: AstrMessageEvent): """ if isinstance(result, str): - result = MessageEventResult().message(result) + result = MessageEventResult().message(t("msg-0def44e2", result=result)) # 兼容外部插件或调用方传入的 chain=None 的情况,确保为可迭代列表 if isinstance(result, MessageEventResult) and result.chain is None: result.chain = [] @@ -361,7 +361,7 @@ def make_result(self) -> MessageEventResult: def plain_result(self, text: str) -> MessageEventResult: """创建一个空的消息事件结果,只包含一条文本消息。""" - return MessageEventResult().message(text) + return MessageEventResult().message(t("msg-8e7dc862", text=text)) def image_result(self, url_or_path: str) -> MessageEventResult: """创建一个空的消息事件结果,只包含一条图片消息。 diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index 0238779dad..4f9c9ff86c 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import traceback from asyncio import Queue @@ -76,11 +77,9 @@ async def _terminate_inst_and_tasks(self, inst: Platform) -> None: raise except Exception as e: logger.error( - "终止平台适配器失败: client_id=%s, error=%s", - client_id, - e, + t("msg-61bd87ae", client_id=client_id, e=e), ) - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) finally: await self._stop_platform_task(client_id) @@ -92,7 +91,7 @@ async def initialize(self) -> None: self.astrbot_config.save_config() await self.load_platform(platform) except Exception as e: - logger.error(f"初始化 {platform} 平台适配器失败: {e}") + logger.error(t("msg-563a0a74", platform=platform, e=e)) # 网页聊天 webchat_inst = WebChatAdapter({}, self.settings, self.event_queue) @@ -110,20 +109,18 @@ async def load_platform(self, platform_config: dict) -> None: sanitized_id, changed = self._sanitize_platform_id(platform_id) if sanitized_id and changed: logger.warning( - "平台 ID %r 包含非法字符 ':' 或 '!',已替换为 %r。", - platform_id, - sanitized_id, + t("msg-3398495c", platform_id=platform_id, sanitized_id=sanitized_id), ) platform_config["id"] = sanitized_id self.astrbot_config.save_config() else: logger.error( - f"平台 ID {platform_id!r} 不能为空,跳过加载该平台适配器。", + t("msg-31361418", platform_id=platform_id), ) return logger.info( - f"载入 {platform_config['type']}({platform_config['id']}) 平台适配器 ...", + t("msg-e395bbcc", res=platform_config['type'], res_2=platform_config['id']), ) match platform_config["type"]: case "aiocqhttp": @@ -182,14 +179,14 @@ async def load_platform(self, platform_config: dict) -> None: ) except (ImportError, ModuleNotFoundError) as e: logger.error( - f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。", + t("msg-b4b29344", res=platform_config['type'], e=e), ) except Exception as e: - logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。") + logger.error(t("msg-18f0e1fe", res=platform_config['type'], e=e)) if platform_config["type"] not in platform_cls_map: logger.error( - f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误", + t("msg-2636a882", res=platform_config['type'], res_2=platform_config['id']), ) return cls_type = platform_cls_map[platform_config["type"]] @@ -209,11 +206,11 @@ async def load_platform(self, platform_config: dict) -> None: for handler in handlers: try: logger.info( - f"hook(on_platform_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", + t("msg-c4a38b85", res=star_map[handler.handler_module_path].name, res_2=handler.handler_name), ) await handler.handler() except Exception: - logger.error(traceback.format_exc()) + logger.error(t("msg-78b9c276", res=traceback.format_exc())) async def _task_wrapper( self, task: asyncio.Task, platform: Platform | None = None @@ -230,10 +227,10 @@ async def _task_wrapper( except Exception as e: error_msg = str(e) tb_str = traceback.format_exc() - logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}") + logger.error(t("msg-967606fd", res=task.get_name(), e=e)) for line in tb_str.split("\n"): - logger.error(f"| {line}") - logger.error("-------") + logger.error(t("msg-a2cd77f3", line=line)) + logger.error(t("msg-1f686eeb")) # 记录错误到平台实例 if platform: @@ -252,7 +249,7 @@ async def reload(self, platform_config: dict) -> None: async def terminate_platform(self, platform_id: str) -> None: if platform_id in self._inst_map: - logger.info(f"正在尝试终止 {platform_id} 平台适配器 ...") + logger.info(t("msg-38723ea8", platform_id=platform_id)) # client_id = self._inst_map.pop(platform_id, None) info = self._inst_map.pop(platform_id) @@ -267,7 +264,7 @@ async def terminate_platform(self, platform_id: str) -> None: ), ) except Exception: - logger.warning(f"可能未完全移除 {platform_id} 平台适配器") + logger.warning(t("msg-63f684c6", platform_id=platform_id)) await self._terminate_inst_and_tasks(inst) @@ -314,7 +311,7 @@ def get_all_stats(self) -> dict: error_count += 1 except Exception as e: # 如果获取统计信息失败,记录基本信息 - logger.warning(f"获取平台统计信息失败: {e}") + logger.warning(t("msg-136a952f", e=e)) stats_list.append( { "id": getattr(inst, "config", {}).get("id", "unknown"), diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index a7c181217d..07ed242f43 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import abc import uuid from asyncio import Queue @@ -162,4 +163,4 @@ async def webhook_callback(self, request: Any) -> Any: Raises: NotImplementedError: 平台未实现统一 Webhook 模式 """ - raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式") + raise NotImplementedError(t("msg-30fc9871", res=self.meta().name)) diff --git a/astrbot/core/platform/register.py b/astrbot/core/platform/register.py index 62ec5070ab..84cd0198bf 100644 --- a/astrbot/core/platform/register.py +++ b/astrbot/core/platform/register.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from astrbot.core import logger from .platform_metadata import PlatformMetadata @@ -28,7 +29,7 @@ def register_platform_adapter( def decorator(cls): if adapter_name in platform_cls_map: raise ValueError( - f"平台适配器 {adapter_name} 已经注册过了,可能发生了适配器命名冲突。", + t("msg-eecf0aa8", adapter_name=adapter_name), ) # 添加必备选项 @@ -57,7 +58,7 @@ def decorator(cls): ) platform_registry.append(pm) platform_cls_map[adapter_name] = cls - logger.debug(f"平台适配器 {adapter_name} 已注册") + logger.debug(t("msg-614a55eb", adapter_name=adapter_name)) return cls return decorator @@ -86,6 +87,6 @@ def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str] platform_registry.remove(pm) if pm.name in platform_cls_map: del platform_cls_map[pm.name] - logger.debug(f"平台适配器 {pm.name} 已注销 (来自模块 {pm.module_path})") + logger.debug(t("msg-bb06a88d", res=pm.name, res_2=pm.module_path)) return unregistered diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 7e42a0fd86..bb6191bb94 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import re from collections.abc import AsyncGenerator @@ -99,7 +100,7 @@ async def _dispatch_send( await bot.send(event=event, message=messages) else: raise ValueError( - f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})", + t("msg-0db8227d", session_id=session_id, event=event), ) @classmethod diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 45114382fa..cf0a063248 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import inspect import itertools @@ -69,7 +70,7 @@ async def request(event: Event) -> None: return await self.handle_msg(abm) except Exception as e: - logger.exception(f"Handle request message failed: {e}") + logger.exception(t("msg-859d480d", e=e)) return @self.bot.on_notice() @@ -79,7 +80,7 @@ async def notice(event: Event) -> None: if abm: await self.handle_msg(abm) except Exception as e: - logger.exception(f"Handle notice message failed: {e}") + logger.exception(t("msg-6fb672e1", e=e)) return @self.bot.on_message("group") @@ -89,7 +90,7 @@ async def group(event: Event) -> None: if abm: await self.handle_msg(abm) except Exception as e: - logger.exception(f"Handle group message failed: {e}") + logger.exception(t("msg-cf4687a3", e=e)) return @self.bot.on_message("private") @@ -99,12 +100,12 @@ async def private(event: Event) -> None: if abm: await self.handle_msg(abm) except Exception as e: - logger.exception(f"Handle private message failed: {e}") + logger.exception(t("msg-3a9853e3", e=e)) return @self.bot.on_websocket_connection def on_websocket_connection(_) -> None: - logger.info("aiocqhttp(OneBot v11) 适配器已连接。") + logger.info(t("msg-ec06dc3d")) async def send_by_session( self, @@ -126,7 +127,7 @@ async def send_by_session( await super().send_by_session(session, message_chain) async def convert_message(self, event: Event) -> AstrBotMessage | None: - logger.debug(f"[aiocqhttp] RawMessage {event}") + logger.debug(t("msg-1304a54d", event=event)) if event["post_type"] == "message": abm = await self._convert_handle_message_event(event) @@ -231,12 +232,12 @@ async def _convert_handle_message_event( message_str = "" if not isinstance(event.message, list): err = f"aiocqhttp: 无法识别的消息类型: {event.message!s},此条消息将被忽略。如果您在使用 go-cqhttp,请将其配置文件中的 message.post-format 更改为 array。" - logger.critical(err) + logger.critical(t("msg-93cbb9fa", err=err)) try: await self.bot.send(event, err) except BaseException as e: - logger.error(f"回复消息失败: {e}") - raise ValueError(err) + logger.error(t("msg-a4487a03", e=e)) + raise ValueError(t("msg-93cbb9fa", err=err)) # 按消息段类型类型适配 for t, m_group in itertools.groupby(event.message, key=lambda x: x["type"]): @@ -254,7 +255,7 @@ async def _convert_handle_message_event( for m in m_group: if m["data"].get("url") and m["data"].get("url").startswith("http"): # Lagrange - logger.info("guessing lagrange") + logger.info(t("msg-48bc7bff")) # 检查多个可能的文件名字段 file_name = ( m["data"].get("file_name", "") @@ -290,12 +291,12 @@ async def _convert_handle_message_event( a = File(name=file_name, url=file_url) abm.message.append(a) else: - logger.error(f"获取文件失败: {ret}") + logger.error(t("msg-6ab145a1", ret=ret)) except ActionFailed as e: - logger.error(f"获取文件失败: {e},此消息段将被忽略。") + logger.error(t("msg-457454d7", e=e)) except BaseException as e: - logger.error(f"获取文件失败: {e},此消息段将被忽略。") + logger.error(t("msg-457454d7", e=e)) elif t == "reply": for m in m_group: @@ -313,7 +314,7 @@ async def _convert_handle_message_event( new_event = Event.from_payload(reply_event_data) if not new_event: logger.error( - f"无法从回复消息数据构造 Event 对象: {reply_event_data}", + t("msg-7a299806", reply_event_data=reply_event_data), ) continue abm_reply = await self._convert_handle_message_event( @@ -334,7 +335,7 @@ async def _convert_handle_message_event( abm.message.append(reply_seg) except BaseException as e: - logger.error(f"获取引用消息失败: {e}。") + logger.error(t("msg-e6633a51", e=e)) a = ComponentTypes[t](**m["data"]) abm.message.append(a) elif t == "at": @@ -384,9 +385,9 @@ async def _convert_handle_message_event( else: abm.message.append(At(qq=str(m["data"]["qq"]), name="")) except ActionFailed as e: - logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") + logger.error(t("msg-6e99cb8d", e=e)) except BaseException as e: - logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") + logger.error(t("msg-6e99cb8d", e=e)) message_str += "".join(at_parts) elif t == "markdown": @@ -399,14 +400,14 @@ async def _convert_handle_message_event( try: if t not in ComponentTypes: logger.warning( - f"不支持的消息段类型,已忽略: {t}, data={m['data']}" + t("msg-cf15fd40", t=t, res=m['data']) ) continue a = ComponentTypes[t](**m["data"]) abm.message.append(a) except Exception as e: logger.exception( - f"消息段解析失败: type={t}, data={m['data']}. {e}" + t("msg-45d126ad", t=t, res=m['data'], e=e) ) continue @@ -419,7 +420,7 @@ async def _convert_handle_message_event( def run(self) -> Awaitable[Any]: if not self.host or not self.port: logger.warning( - "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199", + t("msg-394a20ae"), ) self.host = "0.0.0.0" self.port = 6199 @@ -476,7 +477,7 @@ async def _close_reverse_ws_connections(self) -> None: async def shutdown_trigger_placeholder(self) -> None: await self.shutdown_event.wait() - logger.info("aiocqhttp 适配器已被关闭") + logger.info(t("msg-7414707c")) def meta(self) -> PlatformMetadata: return self.metadata diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 2d9b45cc19..6301190f90 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json import threading @@ -37,11 +38,7 @@ class MyEventHandler(dingtalk_stream.EventHandler): async def process(self, event: dingtalk_stream.EventMessage): print( - "2", - event.headers.event_type, - event.headers.event_id, - event.headers.event_born_time, - event.data, + t("msg-c81e728d"), ) return AckMessage.STATUS_OK, "OK" @@ -65,7 +62,7 @@ def __init__( class AstrCallbackClient(dingtalk_stream.ChatbotHandler): async def process(self, message: dingtalk_stream.CallbackMessage): - logger.debug(f"dingtalk: {message.data}") + logger.debug(t("msg-d6371313", res=message.data)) im = dingtalk_stream.ChatbotMessage.from_dict(message.data) abm = await outer_self.convert_msg(im) await outer_self.handle_msg(abm) @@ -110,7 +107,7 @@ async def send_by_session( staff_id = await self._get_sender_staff_id(session) if not staff_id: logger.warning( - "钉钉私聊会话缺少 staff_id 映射,回退使用 session_id 作为 userId 发送", + t("msg-a1c8b5b1"), ) staff_id = session.session_id await self.send_message_chain_to_user( @@ -229,7 +226,7 @@ async def _remember_sender_binding( sender_staff_id, ) except Exception as e: - logger.warning(f"保存钉钉会话映射失败: {e}") + logger.warning(t("msg-2abb842f", e=e)) async def download_ding_file( self, @@ -266,7 +263,7 @@ async def download_ding_file( ): if resp.status != 200: logger.error( - f"下载钉钉文件失败: {resp.status}, {await resp.text()}", + t("msg-46988861", res=resp.status, res_2=await resp.text()), ) return "" resp_data = await resp.json() @@ -283,7 +280,7 @@ async def get_access_token(self) -> str: if access_token: return access_token except Exception as e: - logger.warning(f"通过 dingtalk_stream 获取 access_token 失败: {e}") + logger.warning(t("msg-ba9e1288", e=e)) payload = {"appKey": self.client_id, "appSecret": self.client_secret} async with aiohttp.ClientSession() as session: @@ -293,7 +290,7 @@ async def get_access_token(self) -> str: ) as resp: if resp.status != 200: logger.error( - f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}", + t("msg-835b1ce6", res=resp.status, res_2=await resp.text()), ) return "" data = await resp.json() @@ -309,7 +306,7 @@ async def _get_sender_staff_id(self, session: MessageSesion) -> str: ) return cast(str, staff_id or "") except Exception as e: - logger.warning(f"读取钉钉 staff_id 映射失败: {e}") + logger.warning(t("msg-331fcb1f", e=e)) return "" async def _send_group_message( @@ -321,7 +318,7 @@ async def _send_group_message( ) -> None: access_token = await self.get_access_token() if not access_token: - logger.error("钉钉群消息发送失败: access_token 为空") + logger.error(t("msg-ba183a34")) return payload = { @@ -342,7 +339,7 @@ async def _send_group_message( ) as resp: if resp.status != 200: logger.error( - f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", + t("msg-b8aaa69b", res=resp.status, res_2=await resp.text()), ) async def _send_private_message( @@ -354,7 +351,7 @@ async def _send_private_message( ) -> None: access_token = await self.get_access_token() if not access_token: - logger.error("钉钉私聊消息发送失败: access_token 为空") + logger.error(t("msg-cfb35bf5")) return payload = { @@ -375,7 +372,7 @@ async def _send_private_message( ) as resp: if resp.status != 200: logger.error( - f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", + t("msg-7553c219", res=resp.status, res_2=await resp.text()), ) def _safe_remove_file(self, file_path: str | None) -> None: @@ -386,7 +383,7 @@ def _safe_remove_file(self, file_path: str | None) -> None: if p.exists() and p.is_file(): p.unlink() except Exception as e: - logger.warning(f"清理临时文件失败: {file_path}, {e}") + logger.warning(t("msg-5ab2d58d", file_path=file_path, e=e)) async def _prepare_voice_for_dingtalk(self, input_path: str) -> tuple[str, bool]: """优先转换为 OGG(Opus),不可用时回退 AMR。""" @@ -398,7 +395,7 @@ async def _prepare_voice_for_dingtalk(self, input_path: str) -> tuple[str, bool] converted = await convert_audio_format(input_path, "ogg") return converted, converted != input_path except Exception as e: - logger.warning(f"钉钉语音转 OGG 失败,回退 AMR: {e}") + logger.warning(t("msg-c0c40912", e=e)) converted = await convert_audio_format(input_path, "amr") return converted, converted != input_path @@ -406,7 +403,7 @@ async def upload_media(self, file_path: str, media_type: str) -> str: media_file_path = Path(file_path) access_token = await self.get_access_token() if not access_token: - logger.error("钉钉媒体上传失败: access_token 为空") + logger.error(t("msg-21c73eca")) return "" form = aiohttp.FormData() @@ -423,12 +420,12 @@ async def upload_media(self, file_path: str, media_type: str) -> str: ) as resp: if resp.status != 200: logger.error( - f"钉钉媒体上传失败: {resp.status}, {await resp.text()}" + t("msg-24e3054f", res=resp.status, res_2=await resp.text()) ) return "" data = await resp.json() if data.get("errcode") != 0: - logger.error(f"钉钉媒体上传失败: {data}") + logger.error(t("msg-34d0a11d", data=data)) return "" return cast(str, data.get("media_id", "")) @@ -504,7 +501,7 @@ async def send_message(msg_key: str, msg_param: dict) -> None: }, ) except Exception as e: - logger.warning(f"钉钉语音发送失败: {e}") + logger.warning(t("msg-3b0d4fb5", e=e)) continue finally: if converted_audio: @@ -535,7 +532,7 @@ async def send_message(msg_key: str, msg_param: dict) -> None: }, ) except Exception as e: - logger.warning(f"钉钉视频发送失败: {e}") + logger.warning(t("msg-7187f424", e=e)) continue finally: self._safe_remove_file(cover_path) @@ -610,7 +607,7 @@ async def send_message_chain_with_incoming( ) staff_id = sender_staff_id or await self._get_sender_staff_id(session) if not staff_id: - logger.error("钉钉私聊回复失败: 缺少 sender_staff_id") + logger.error(t("msg-e40cc45f")) return await self.send_message_chain_to_user( staff_id=staff_id, @@ -643,9 +640,9 @@ def start_client(loop: asyncio.AbstractEventLoop) -> None: task.result() except Exception as e: if "Graceful shutdown" in str(e): - logger.info("钉钉适配器已被关闭") + logger.info(t("msg-be63618a")) return - logger.error(f"钉钉机器人启动失败: {e}") + logger.error(t("msg-0ab22b13", e=e)) loop = asyncio.get_event_loop() await loop.run_in_executor(None, start_client, loop) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index 3331c51476..558b75d474 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from typing import Any from astrbot import logger @@ -20,7 +21,7 @@ def __init__( async def send(self, message: MessageChain) -> None: if not self.adapter: - logger.error("钉钉消息发送失败: 缺少 adapter") + logger.error(t("msg-eaa1f3e4")) return await self.adapter.send_message_chain_with_incoming( incoming_message=self.message_obj.raw_message, diff --git a/astrbot/core/platform/sources/discord/client.py b/astrbot/core/platform/sources/discord/client.py index ebd32c471a..eb0dd198c7 100644 --- a/astrbot/core/platform/sources/discord/client.py +++ b/astrbot/core/platform/sources/discord/client.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import sys from collections.abc import Awaitable, Callable @@ -35,11 +36,11 @@ def __init__(self, token: str, proxy: str | None = None) -> None: async def on_ready(self) -> None: """当机器人成功连接并准备就绪时触发""" if self.user is None: - logger.error("[Discord] 客户端未正确加载用户信息 (self.user is None)") + logger.error(t("msg-940888cb")) return - logger.info(f"[Discord] 已作为 {self.user} (ID: {self.user.id}) 登录") - logger.info("[Discord] 客户端已准备就绪。") + logger.info(t("msg-9a3c1925", res=self.user, res_2=self.user.id)) + logger.info(t("msg-30c1f1c8")) if self.on_ready_once_callback and not self._ready_once_fired: self._ready_once_fired = True @@ -47,14 +48,14 @@ async def on_ready(self) -> None: await self.on_ready_once_callback() except Exception as e: logger.error( - f"[Discord] on_ready_once_callback 执行失败: {e}", + t("msg-d8c03bdf", e=e), exc_info=True, ) def _create_message_data(self, message: discord.Message) -> dict: """从 discord.Message 创建数据字典""" if self.user is None: - raise RuntimeError("Bot is not ready: self.user is None") + raise RuntimeError(t("msg-c9601653")) is_mentioned = self.user in message.mentions return { @@ -74,10 +75,10 @@ def _create_message_data(self, message: discord.Message) -> dict: def _create_interaction_data(self, interaction: discord.Interaction) -> dict: """从 discord.Interaction 创建数据字典""" if self.user is None: - raise RuntimeError("Bot is not ready: self.user is None") + raise RuntimeError(t("msg-c9601653")) if interaction.user is None: - raise ValueError("Interaction received without a valid user") + raise ValueError(t("msg-4b017a7c")) return { "interaction": interaction, @@ -99,7 +100,7 @@ async def on_message(self, message: discord.Message) -> None: return logger.debug( - f"[Discord] 收到原始消息 from {message.author.name}: {message.content}", + t("msg-3067bdce", res=message.author.name, res_2=message.content), ) if self.on_message_received: diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 7657962a11..50014e055c 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import re import sys @@ -64,7 +65,7 @@ async def send_by_session( """通过会话发送消息""" if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法发送消息" + t("msg-7ea23347") ) return @@ -78,14 +79,14 @@ async def send_by_session( channel_id = int(channel_id_str) channel = self.client.get_channel(channel_id) except (ValueError, TypeError): - logger.warning(f"[Discord] Invalid channel ID format: {channel_id_str}") + logger.warning(t("msg-ff6611ce", channel_id_str=channel_id_str)) if channel: message_obj.type = self._get_message_type(channel) message_obj.group_id = self._get_channel_id(channel) else: logger.warning( - f"[Discord] Can't get channel info for {channel_id_str}, will guess message type.", + t("msg-5e4e5d63", channel_id_str=channel_id_str), ) message_obj.type = MessageType.GROUP_MESSAGE message_obj.group_id = session.session_id @@ -127,7 +128,7 @@ async def run(self) -> None: # 初始化回调函数 async def on_received(message_data) -> None: - logger.debug(f"[Discord] 收到消息: {message_data}") + logger.debug(t("msg-32d4751b", message_data=message_data)) if self.client_self_id is None: self.client_self_id = message_data.get("bot_id") abm = await self.convert_message(data=message_data) @@ -136,7 +137,7 @@ async def on_received(message_data) -> None: # 初始化 Discord 客户端 token = str(self.config.get("discord_token")) if not token: - logger.error("[Discord] Bot Token 未配置。请在配置文件中正确设置 token。") + logger.error(t("msg-8296c994")) return proxy = self.config.get("discord_proxy") or None @@ -158,11 +159,11 @@ async def callback() -> None: self._polling_task = asyncio.create_task(self.client.start_polling()) await self.shutdown_event.wait() except discord.errors.LoginFailure: - logger.error("[Discord] 登录失败。请检查你的 Bot Token 是否正确。") + logger.error(t("msg-170b31df")) except discord.errors.ConnectionClosed: - logger.warning("[Discord] 与 Discord 的连接已关闭。") + logger.warning(t("msg-6678fbd3")) except Exception as e: - logger.error(f"[Discord] 适配器运行时发生意外错误: {e}", exc_info=True) + logger.error(t("msg-cd8c35d2", e=e), exc_info=True) def _get_message_type( self, @@ -264,7 +265,7 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法处理消息" + t("msg-4df30f1d") ) return @@ -283,7 +284,7 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No raw_message = message.raw_message if not isinstance(raw_message, discord.Message): logger.warning( - f"[Discord] 收到非 Message 类型的消息: {type(raw_message)},已忽略。" + t("msg-f7803502", res=type(raw_message)) ) return @@ -325,7 +326,7 @@ async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> No @override async def terminate(self) -> None: """终止适配器""" - logger.info("[Discord] 正在终止适配器... (step 1: cancel polling task)") + logger.info(t("msg-134e70e9")) self.shutdown_event.set() # 优先 cancel polling_task if self._polling_task: @@ -333,10 +334,10 @@ async def terminate(self) -> None: try: await asyncio.wait_for(self._polling_task, timeout=10) except asyncio.CancelledError: - logger.info("[Discord] polling_task 已取消。") + logger.info(t("msg-5c01a092")) except Exception as e: - logger.warning(f"[Discord] polling_task 取消异常: {e}") - logger.info("[Discord] 正在清理已注册的斜杠指令... (step 2)") + logger.warning(t("msg-77f8ca59", e=e)) + logger.info(t("msg-528b6618")) # 清理指令 if self.enable_command_register and self.client: try: @@ -347,16 +348,16 @@ async def terminate(self) -> None: ), timeout=10, ) - logger.info("[Discord] 指令清理完成。") + logger.info(t("msg-d0b832e6")) except Exception as e: - logger.error(f"[Discord] 清理指令时发生错误: {e}", exc_info=True) - logger.info("[Discord] 正在关闭 Discord 客户端... (step 3)") + logger.error(t("msg-43383f5e", e=e), exc_info=True) + logger.info(t("msg-b960ed33")) if self.client and hasattr(self.client, "close"): try: await asyncio.wait_for(self.client.close(), timeout=10) except Exception as e: - logger.warning(f"[Discord] 客户端关闭异常: {e}") - logger.info("[Discord] 适配器已终止。") + logger.warning(t("msg-5e58f8a2", e=e)) + logger.info(t("msg-d1271bf1")) def register_handler(self, handler_info) -> None: """注册处理器信息""" @@ -364,7 +365,7 @@ def register_handler(self, handler_info) -> None: async def _collect_and_register_commands(self) -> None: """收集所有指令并注册到Discord""" - logger.info("[Discord] 开始收集并注册斜杠指令...") + logger.info(t("msg-c374da7a")) registered_commands = [] for handler_md in star_handlers_registry: @@ -405,15 +406,15 @@ async def _collect_and_register_commands(self) -> None: if registered_commands: logger.info( - f"[Discord] 准备同步 {len(registered_commands)} 个指令: {', '.join(registered_commands)}", + t("msg-a6d37e4d", res=len(registered_commands), res_2=', '.join(registered_commands)), ) else: - logger.info("[Discord] 没有发现可注册的指令。") + logger.info(t("msg-dbcaf095")) # 使用 Pycord 的方法同步指令 # 注意:这可能需要一些时间,并且有频率限制 await self.client.sync_commands() - logger.info("[Discord] 指令同步完成。") + logger.info(t("msg-09209f2f")) def _create_dynamic_callback(self, cmd_name: str): """为每个指令动态创建一个异步回调函数""" @@ -422,17 +423,15 @@ async def dynamic_callback( ctx: discord.ApplicationContext, params: str | None = None ) -> None: # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter - logger.debug(f"[Discord] 回调函数触发: {cmd_name}") - logger.debug(f"[Discord] 回调函数参数: {ctx}") - logger.debug(f"[Discord] 回调函数参数: {params}") + logger.debug(t("msg-a95055fd", cmd_name=cmd_name)) + logger.debug(t("msg-55b13b1e", ctx=ctx)) + logger.debug(t("msg-79f72e4e", params=params)) message_str_for_filter = cmd_name if params: message_str_for_filter += f" {params}" logger.debug( - f"[Discord] 斜杠指令 '{cmd_name}' 被触发。 " - f"原始参数: '{params}'. " - f"构建的指令字符串: '{message_str_for_filter}'", + t("msg-22add467", cmd_name=cmd_name, params=params, message_str_for_filter=message_str_for_filter), ) # 尝试立即响应,防止超时 @@ -441,7 +440,7 @@ async def dynamic_callback( await ctx.defer() followup_webhook = ctx.followup except Exception as e: - logger.warning(f"[Discord] 指令 '{cmd_name}' defer 失败: {e}") + logger.warning(t("msg-ccffc74a", cmd_name=cmd_name, e=e)) # 2. 构建 AstrBotMessage channel = ctx.channel @@ -503,7 +502,7 @@ def _extract_command_info( # Discord 斜杠指令名称规范 if not re.match(r"^[a-z0-9_-]{1,32}$", cmd_name): - logger.debug(f"[Discord] 跳过不符合规范的指令: {cmd_name}") + logger.debug(t("msg-13402a28", cmd_name=cmd_name)) return None description = handler_metadata.desc or f"指令: {cmd_name}" diff --git a/astrbot/core/platform/sources/discord/discord_platform_event.py b/astrbot/core/platform/sources/discord/discord_platform_event.py index 02d4dae868..faa8f50306 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_event.py +++ b/astrbot/core/platform/sources/discord/discord_platform_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import base64 import binascii @@ -58,7 +59,7 @@ async def send(self, message: MessageChain) -> None: reference_message_id, ) = await self._parse_to_discord(message) except Exception as e: - logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True) + logger.error(t("msg-0056366b", e=e), exc_info=True) return kwargs = {} @@ -73,7 +74,7 @@ async def send(self, message: MessageChain) -> None: if reference_message_id and not self.interaction_followup_webhook: kwargs["reference"] = self.client.get_message(int(reference_message_id)) if not kwargs: - logger.debug("[Discord] 尝试发送空消息,已忽略。") + logger.debug(t("msg-fa0a9e40")) return # 根据上下文执行发送/回复操作 @@ -88,12 +89,12 @@ async def send(self, message: MessageChain) -> None: if not channel: return if not isinstance(channel, discord.abc.Messageable): - logger.error(f"[Discord] 频道 {channel.id} 不是可发送消息的类型") + logger.error(t("msg-5ccebf9a", res=channel.id)) return await channel.send(**kwargs) except Exception as e: - logger.error(f"[Discord] 发送消息时发生未知错误: {e}", exc_info=True) + logger.error(t("msg-1550c1eb", e=e), exc_info=True) await super().send(message) @@ -122,7 +123,7 @@ async def _get_channel( channel_id, ) or await self.client.fetch_channel(channel_id) except (ValueError, discord.errors.NotFound, discord.errors.Forbidden): - logger.error(f"[Discord] 无法获取频道 {self.session_id}") + logger.error(t("msg-7857133d", res=self.session_id)) return None async def _parse_to_discord( @@ -149,27 +150,27 @@ async def _parse_to_discord( elif isinstance(i, At): content_parts.append(f"<@{i.qq}>") elif isinstance(i, Image): - logger.debug(f"[Discord] 开始处理 Image 组件: {i}") + logger.debug(t("msg-050aa8d6", i=i)) try: filename = getattr(i, "filename", None) file_content = getattr(i, "file", None) if not file_content: - logger.warning(f"[Discord] Image 组件没有 file 属性: {i}") + logger.warning(t("msg-57c802ef", i=i)) continue discord_file = None # 1. URL if file_content.startswith("http"): - logger.debug(f"[Discord] 处理 URL 图片: {file_content}") + logger.debug(t("msg-f2bea7ac", file_content=file_content)) embed = discord.Embed().set_image(url=file_content) embeds.append(embed) continue # 2. File URI if file_content.startswith("file:///"): - logger.debug(f"[Discord] 处理 File URI: {file_content}") + logger.debug(t("msg-c3eae1f1", file_content=file_content)) path = Path(file_content[8:]) if await asyncio.to_thread(path.exists): file_bytes = await asyncio.to_thread(path.read_bytes) @@ -178,11 +179,11 @@ async def _parse_to_discord( filename=filename or path.name, ) else: - logger.warning(f"[Discord] 图片文件不存在: {path}") + logger.warning(t("msg-6201da92", path=path)) # 3. Base64 URI elif file_content.startswith("base64://"): - logger.debug("[Discord] 处理 Base64 URI") + logger.debug(t("msg-2a6f0cd4")) b64_data = file_content.split("base64://", 1)[1] missing_padding = len(b64_data) % 4 if missing_padding: @@ -196,7 +197,7 @@ async def _parse_to_discord( # 4. 裸 Base64 或本地路径 else: try: - logger.debug("[Discord] 尝试作为裸 Base64 处理") + logger.debug(t("msg-b589c643")) b64_data = file_content missing_padding = len(b64_data) % 4 if missing_padding: @@ -208,7 +209,7 @@ async def _parse_to_discord( ) except (ValueError, TypeError, binascii.Error): logger.debug( - f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}", + t("msg-41dd4b8f", file_content=file_content), ) path = Path(file_content) if await asyncio.to_thread(path.exists): @@ -218,7 +219,7 @@ async def _parse_to_discord( filename=filename or path.name, ) else: - logger.warning(f"[Discord] 图片文件不存在: {path}") + logger.warning(t("msg-6201da92", path=path)) if discord_file: files.append(discord_file) @@ -227,7 +228,7 @@ async def _parse_to_discord( # 使用 getattr 来安全地访问 i.file,以防 i 本身就是问题 file_info = getattr(i, "file", "未知") logger.error( - f"[Discord] 处理图片时发生未知严重错误: {file_info}", + t("msg-f59778a1", file_info=file_info), exc_info=True, ) elif isinstance(i, File): @@ -242,12 +243,12 @@ async def _parse_to_discord( ) else: logger.warning( - f"[Discord] 获取文件失败,路径不存在: {file_path_str}", + t("msg-85665612", file_path_str=file_path_str), ) else: - logger.warning(f"[Discord] 获取文件失败: {i.name}") + logger.warning(t("msg-e55956fb", res=i.name)) except Exception as e: - logger.warning(f"[Discord] 处理文件失败: {i.name}, 错误: {e}") + logger.warning(t("msg-56cc0d48", res=i.name, e=e)) elif isinstance(i, DiscordEmbed): # Discord Embed消息 embeds.append(i.to_discord_embed()) @@ -259,11 +260,11 @@ async def _parse_to_discord( if isinstance(i.view, discord.ui.View): view = i.view else: - logger.debug(f"[Discord] 忽略了不支持的消息组件: {i.type}") + logger.debug(t("msg-c0705d4e", res=i.type)) content = "".join(content_parts) if len(content) > 2000: - logger.warning("[Discord] 消息内容超过2000字符,将被截断。") + logger.warning(t("msg-0417d127")) content = content[:2000] return content, files, view, embeds, reference_message_id @@ -278,7 +279,7 @@ async def react(self, emoji: str) -> None: emoji ) except Exception as e: - logger.error(f"[Discord] 添加反应失败: {e}") + logger.error(t("msg-6277510f", e=e)) def is_slash_command(self) -> bool: """判断是否为斜杠命令""" diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index be1c81c26e..27eb695063 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import base64 import json @@ -54,7 +55,7 @@ def __init__( self.connection_mode = platform_config.get("lark_connection_mode", "socket") if not self.bot_name: - logger.warning("未设置飞书机器人名称,@ 机器人可能得不到回复。") + logger.warning(t("msg-06ce76eb")) # 初始化 WebSocket 长连接相关配置 async def on_msg_event_recv(event: lark.im.v1.P2ImMessageReceiveV1) -> None: @@ -103,7 +104,7 @@ async def _download_message_resource( resource_type: str, ) -> bytes | None: if self.lark_api.im is None: - logger.error("[Lark] API Client im 模块未初始化") + logger.error(t("msg-eefbe737")) return None request = ( @@ -116,13 +117,12 @@ async def _download_message_resource( response = await self.lark_api.im.v1.message_resource.aget(request) if not response.success(): logger.error( - f"[Lark] 下载消息资源失败 type={resource_type}, key={file_key}, " - f"code={response.code}, msg={response.msg}", + t("msg-236bcaad", resource_type=resource_type, file_key=file_key, res=response.code, res_2=response.msg), ) return None if response.file is None: - logger.error(f"[Lark] 消息资源响应中不包含文件流: {file_key}") + logger.error(t("msg-ef9a61fe", file_key=file_key)) return None return response.file.read() @@ -245,7 +245,7 @@ async def _parse_message_components( if not image_key: continue if not message_id: - logger.error("[Lark] 图片消息缺少 message_id") + logger.error(t("msg-7b69a8d4")) continue image_bytes = await self._download_message_resource( message_id=message_id, @@ -264,7 +264,7 @@ async def _parse_message_components( if not file_key: continue if not message_id: - logger.error("[Lark] 富文本视频消息缺少 message_id") + logger.error(t("msg-59f1694d")) continue file_path = await self._download_file_resource_to_temp( message_id=message_id, @@ -282,10 +282,10 @@ async def _parse_message_components( file_key = str(content.get("file_key", "")).strip() file_name = str(content.get("file_name", "")).strip() or "lark_file" if not message_id: - logger.error("[Lark] 文件消息缺少 message_id") + logger.error(t("msg-af8f391d")) return components if not file_key: - logger.error("[Lark] 文件消息缺少 file_key") + logger.error(t("msg-d4080b76")) return components file_path = await self._download_file_resource_to_temp( message_id=message_id, @@ -300,10 +300,10 @@ async def _parse_message_components( if message_type == "audio": file_key = str(content.get("file_key", "")).strip() if not message_id: - logger.error("[Lark] 音频消息缺少 message_id") + logger.error(t("msg-ab21318a")) return components if not file_key: - logger.error("[Lark] 音频消息缺少 file_key") + logger.error(t("msg-9ec2c30a")) return components file_path = await self._download_file_resource_to_temp( message_id=message_id, @@ -319,10 +319,10 @@ async def _parse_message_components( file_key = str(content.get("file_key", "")).strip() file_name = str(content.get("file_name", "")).strip() or "lark_media.mp4" if not message_id: - logger.error("[Lark] 视频消息缺少 message_id") + logger.error(t("msg-0fa9ed18")) return components if not file_key: - logger.error("[Lark] 视频消息缺少 file_key") + logger.error(t("msg-ae884c5c")) return components file_path = await self._download_file_resource_to_temp( message_id=message_id, @@ -342,21 +342,20 @@ async def _build_reply_from_parent_id( parent_message_id: str, ) -> Comp.Reply | None: if self.lark_api.im is None: - logger.error("[Lark] API Client im 模块未初始化") + logger.error(t("msg-eefbe737")) return None request = GetMessageRequest.builder().message_id(parent_message_id).build() response = await self.lark_api.im.v1.message.aget(request) if not response.success(): logger.error( - f"[Lark] 获取引用消息失败 id={parent_message_id}, " - f"code={response.code}, msg={response.msg}", + t("msg-dac98a62", parent_message_id=parent_message_id, res=response.code, res_2=response.msg), ) return None if response.data is None or not response.data.items: logger.error( - f"[Lark] 引用消息响应为空 id={parent_message_id}", + t("msg-7ee9f7dc", parent_message_id=parent_message_id), ) return None @@ -385,7 +384,7 @@ async def _build_reply_from_parent_id( quoted_content_json = parsed except json.JSONDecodeError: logger.warning( - f"[Lark] 解析引用消息内容失败 id={quoted_message_id}", + t("msg-2b3b2db9", quoted_message_id=quoted_message_id), ) quoted_at_map = self._build_at_map(parent_message.mentions) @@ -496,11 +495,11 @@ def meta(self) -> PlatformMetadata: async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1) -> None: if event.event is None: - logger.debug("[Lark] 收到空事件(event.event is None)") + logger.debug(t("msg-c5d54255")) return message = event.event.message if message is None: - logger.debug("[Lark] 事件中没有消息体(message is None)") + logger.debug(t("msg-82f041c4")) return abm = AstrBotMessage() @@ -539,20 +538,20 @@ async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1) -> None: abm.self_id = m.id.open_id if message.content is None: - logger.warning("[Lark] 消息内容为空") + logger.warning(t("msg-206c3506")) return try: content_json_b = json.loads(message.content) except json.JSONDecodeError: - logger.error(f"[Lark] 解析消息内容失败: {message.content}") + logger.error(t("msg-876aa1d2", res=message.content)) return if not isinstance(content_json_b, dict): - logger.error(f"[Lark] 消息内容不是 JSON Object: {message.content}") + logger.error(t("msg-514230f3", res=message.content)) return - logger.debug(f"[Lark] 解析消息内容: {content_json_b}") + logger.debug(t("msg-0898cf8b", content_json_b=content_json_b)) parsed_components = await self._parse_message_components( message_id=message.message_id, message_type=message.message_type or "unknown", @@ -563,7 +562,7 @@ async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1) -> None: abm.message_str = self._build_message_str_from_components(parsed_components) if message.message_id is None: - logger.error("[Lark] 消息缺少 message_id") + logger.error(t("msg-6a8bc661")) return if ( @@ -571,7 +570,7 @@ async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1) -> None: or event.event.sender.sender_id is None or event.event.sender.sender_id.open_id is None ): - logger.error("[Lark] 消息发送者信息不完整") + logger.error(t("msg-26554571")) return abm.message_id = message.message_id @@ -608,7 +607,7 @@ async def handle_webhook_event(self, event_data: dict) -> None: header = event_data.get("header", {}) event_id = header.get("event_id", "") if event_id and self._is_duplicate_event(event_id): - logger.debug(f"[Lark Webhook] 跳过重复事件: {event_id}") + logger.debug(t("msg-007d863a", event_id=event_id)) return event_type = header.get("event_type", "") if event_type == "im.message.receive_v1": @@ -616,22 +615,22 @@ async def handle_webhook_event(self, event_data: dict) -> None: data = (processor.type())(event_data) processor.do(data) else: - logger.debug(f"[Lark Webhook] 未处理的事件类型: {event_type}") + logger.debug(t("msg-6ce17e71", event_type=event_type)) except Exception as e: - logger.error(f"[Lark Webhook] 处理事件失败: {e}", exc_info=True) + logger.error(t("msg-8689a644", e=e), exc_info=True) async def run(self) -> None: if self.connection_mode == "webhook": # Webhook 模式 if self.webhook_server is None: - logger.error("[Lark] Webhook 模式已启用,但 webhook_server 未初始化") + logger.error(t("msg-20688453")) return webhook_uuid = self.config.get("webhook_uuid") if webhook_uuid: log_webhook_info(f"{self.meta().id}(飞书 Webhook)", webhook_uuid) else: - logger.warning("[Lark] Webhook 模式已启用,但未配置 webhook_uuid") + logger.warning(t("msg-f46171bc")) else: # 长连接模式 await self.client._connect() @@ -646,7 +645,7 @@ async def webhook_callback(self, request: Any) -> Any: async def terminate(self) -> None: if self.connection_mode == "socket": await self.client._disconnect() - logger.info("飞书(Lark) 适配器已关闭") + logger.info(t("msg-dd90a367")) def get_client(self) -> lark.ws.Client: return self.client diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 92e3a32b9e..6432ea3d69 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import base64 import json import os @@ -66,7 +67,7 @@ async def _send_im_message( 是否发送成功 """ if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化") + logger.error(t("msg-eefbe737")) return False if reply_message_id: @@ -92,7 +93,7 @@ async def _send_im_message( if receive_id_type is None or receive_id is None: logger.error( - "[Lark] 主动发送消息时,receive_id 和 receive_id_type 不能为空", + t("msg-a21f93fa"), ) return False @@ -112,7 +113,7 @@ async def _send_im_message( response = await lark_client.im.v1.message.acreate(request) if not response.success(): - logger.error(f"[Lark] 发送飞书消息失败({response.code}): {response.msg}") + logger.error(t("msg-f456e468", res=response.code, res_2=response.msg)) return False return True @@ -137,11 +138,11 @@ async def _upload_lark_file( 成功返回file_key,失败返回None """ if not path or not os.path.exists(path): - logger.error(f"[Lark] 文件不存在: {path}") + logger.error(t("msg-1eb66d14", path=path)) return None if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法上传文件") + logger.error(t("msg-1df39b24")) return None try: @@ -164,20 +165,20 @@ async def _upload_lark_file( if not response.success(): logger.error( - f"[Lark] 无法上传文件({response.code}): {response.msg}" + t("msg-2ee721dd", res=response.code, res_2=response.msg) ) return None if response.data is None: - logger.error("[Lark] 上传文件成功但未返回数据(data is None)") + logger.error(t("msg-a04abf78")) return None file_key = response.data.file_key - logger.debug(f"[Lark] 文件上传成功: {file_key}") + logger.debug(t("msg-959e78a4", file_key=file_key)) return file_key except Exception as e: - logger.error(f"[Lark] 无法打开或上传文件: {e}") + logger.error(t("msg-901a2f60", e=e)) return None @staticmethod @@ -214,12 +215,12 @@ async def _convert_to_lark(message: MessageChain, lark_client: lark.Client) -> l if image_file is None: if not file_path: - logger.error("[Lark] 图片路径为空,无法上传") + logger.error(t("msg-13065327")) continue try: image_file = open(file_path, "rb") except Exception as e: - logger.error(f"[Lark] 无法打开图片文件: {e}") + logger.error(t("msg-37245892", e=e)) continue request = ( @@ -234,37 +235,37 @@ async def _convert_to_lark(message: MessageChain, lark_client: lark.Client) -> l ) if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法上传图片") + logger.error(t("msg-ad63bf53")) continue response = await lark_client.im.v1.image.acreate(request) if not response.success(): - logger.error(f"无法上传飞书图片({response.code}): {response.msg}") + logger.error(t("msg-ef90038b", res=response.code, res_2=response.msg)) continue if response.data is None: - logger.error("[Lark] 上传图片成功但未返回数据(data is None)") + logger.error(t("msg-d2065832")) continue image_key = response.data.image_key - logger.debug(image_key) + logger.debug(t("msg-dbb635c2", image_key=image_key)) ret.append(_stage) ret.append([{"tag": "img", "image_key": image_key}]) _stage.clear() elif isinstance(comp, File): # 文件将通过 _send_file_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到文件组件,将单独发送") + logger.debug(t("msg-d4810504")) continue elif isinstance(comp, Record): # 音频将通过 _send_audio_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到音频组件,将单独发送") + logger.debug(t("msg-45556717")) continue elif isinstance(comp, Video): # 视频将通过 _send_media_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到视频组件,将单独发送") + logger.debug(t("msg-959070b5")) continue else: - logger.warning(f"飞书 暂时不支持消息段: {comp.type}") + logger.warning(t("msg-4e2aa152", res=comp.type)) if _stage: ret.append(_stage) @@ -288,7 +289,7 @@ async def send_message_chain( receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送) """ if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化") + logger.error(t("msg-eefbe737")) return # 分离文件、音频、视频组件和其他组件 @@ -409,11 +410,11 @@ async def _send_audio_message( try: original_audio_path = await audio_comp.convert_to_file_path() except Exception as e: - logger.error(f"[Lark] 无法获取音频文件路径: {e}") + logger.error(t("msg-20d7c64b", e=e)) return if not original_audio_path or not os.path.exists(original_audio_path): - logger.error(f"[Lark] 音频文件不存在: {original_audio_path}") + logger.error(t("msg-2f6f35e6", original_audio_path=original_audio_path)) return # 转换为opus格式 @@ -426,7 +427,7 @@ async def _send_audio_message( else: audio_path = original_audio_path except Exception as e: - logger.error(f"[Lark] 音频格式转换失败,将尝试直接上传: {e}") + logger.error(t("msg-528b968d", e=e)) # 如果转换失败,继续尝试直接上传原始文件 audio_path = original_audio_path @@ -445,9 +446,9 @@ async def _send_audio_message( if converted_audio_path and os.path.exists(converted_audio_path): try: os.remove(converted_audio_path) - logger.debug(f"[Lark] 已删除转换后的音频文件: {converted_audio_path}") + logger.debug(t("msg-fbc7efb9", converted_audio_path=converted_audio_path)) except Exception as e: - logger.warning(f"[Lark] 删除转换后的音频文件失败: {e}") + logger.warning(t("msg-09840299", e=e)) if not file_key: return @@ -482,11 +483,11 @@ async def _send_media_message( try: original_video_path = await media_comp.convert_to_file_path() except Exception as e: - logger.error(f"[Lark] 无法获取视频文件路径: {e}") + logger.error(t("msg-e073ff1c", e=e)) return if not original_video_path or not os.path.exists(original_video_path): - logger.error(f"[Lark] 视频文件不存在: {original_video_path}") + logger.error(t("msg-47e52913", original_video_path=original_video_path)) return # 转换为mp4格式 @@ -499,7 +500,7 @@ async def _send_media_message( else: video_path = original_video_path except Exception as e: - logger.error(f"[Lark] 视频格式转换失败,将尝试直接上传: {e}") + logger.error(t("msg-85ded1eb", e=e)) # 如果转换失败,继续尝试直接上传原始文件 video_path = original_video_path @@ -518,9 +519,9 @@ async def _send_media_message( if converted_video_path and os.path.exists(converted_video_path): try: os.remove(converted_video_path) - logger.debug(f"[Lark] 已删除转换后的视频文件: {converted_video_path}") + logger.debug(t("msg-b3bee05d", converted_video_path=converted_video_path)) except Exception as e: - logger.warning(f"[Lark] 删除转换后的视频文件失败: {e}") + logger.warning(t("msg-775153f6", e=e)) if not file_key: return @@ -536,7 +537,7 @@ async def _send_media_message( async def react(self, emoji: str) -> None: if self.bot.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法发送表情") + logger.error(t("msg-45038ba7")) return request = ( @@ -552,7 +553,7 @@ async def react(self, emoji: str) -> None: response = await self.bot.im.v1.message_reaction.acreate(request) if not response.success(): - logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}") + logger.error(t("msg-8d475b01", res=response.code, res_2=response.msg)) return async def send_streaming(self, generator, use_fallback: bool = False): diff --git a/astrbot/core/platform/sources/lark/server.py b/astrbot/core/platform/sources/lark/server.py index 52177ebb0c..9f6d6ef55b 100644 --- a/astrbot/core/platform/sources/lark/server.py +++ b/astrbot/core/platform/sources/lark/server.py @@ -6,6 +6,7 @@ 3. 签名校验 (SHA256) 4. 事件接收和处理 """ +from astrbot.core.lang import t import asyncio import base64 @@ -109,7 +110,7 @@ def decrypt_event(self, encrypted_data: str) -> dict: 解密后的事件字典 """ if not self.cipher: - raise ValueError("未配置 encrypt_key,无法解密事件") + raise ValueError(t("msg-2f3bccf1")) decrypted_str = self.cipher.decrypt_string(encrypted_data) return json.loads(decrypted_str) @@ -124,7 +125,7 @@ async def handle_challenge(self, event_data: dict) -> dict: 包含 challenge 的响应 """ challenge = event_data.get("challenge", "") - logger.info(f"[Lark Webhook] 收到 challenge 验证请求: {challenge}") + logger.info(t("msg-e77104e2", challenge=challenge)) return {"challenge": challenge} @@ -143,11 +144,11 @@ async def handle_callback(self, request) -> tuple[dict, int] | dict: try: event_data = await request.json except Exception as e: - logger.error(f"[Lark Webhook] 解析请求体失败: {e}") + logger.error(t("msg-34b24fa1", e=e)) return {"error": "Invalid JSON"}, 400 if not event_data: - logger.error("[Lark Webhook] 请求体为空") + logger.error(t("msg-ec0fe13e")) return {"error": "Empty request body"}, 400 # 如果配置了 encrypt_key,进行签名验证 @@ -160,16 +161,16 @@ async def handle_callback(self, request) -> tuple[dict, int] | dict: if not self.verify_signature( timestamp, nonce, self.encrypt_key, body, signature ): - logger.error("[Lark Webhook] 签名验证失败") + logger.error(t("msg-f69ebbdb")) return {"error": "Invalid signature"}, 401 # 检查是否是加密事件 if "encrypt" in event_data: try: event_data = self.decrypt_event(event_data["encrypt"]) - logger.debug(f"[Lark Webhook] 解密后的事件: {event_data}") + logger.debug(t("msg-7ece4036", event_data=event_data)) except Exception as e: - logger.error(f"[Lark Webhook] 解密事件失败: {e}") + logger.error(t("msg-f2cb4b46", e=e)) return {"error": "Decryption failed"}, 400 # 验证 token @@ -180,7 +181,7 @@ async def handle_callback(self, request) -> tuple[dict, int] | dict: else: token = event_data.get("token", "") if token != self.verification_token: - logger.error("[Lark Webhook] Verification Token 不匹配。") + logger.error(t("msg-ef9f8906")) return {"error": "Invalid verification token"}, 401 # 处理 URL 验证 (challenge) @@ -192,7 +193,7 @@ async def handle_callback(self, request) -> tuple[dict, int] | dict: try: await self.callback(event_data) except Exception as e: - logger.error(f"[Lark Webhook] 处理事件回调失败: {e}", exc_info=True) + logger.error(t("msg-bedb2071", e=e), exc_info=True) return {"error": "Event processing failed"}, 500 return {} diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index c13677b13b..58b9376315 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import mimetypes import time @@ -86,7 +87,7 @@ def __init__( channel_secret = str(platform_config.get("channel_secret", "")) if not channel_access_token or not channel_secret: raise ValueError( - "LINE 适配器需要 channel_access_token 和 channel_secret。", + t("msg-68539775"), ) self.line_api = LineAPIClient( @@ -117,7 +118,7 @@ async def run(self) -> None: if webhook_uuid: log_webhook_info(f"{self.meta().id}(LINE)", webhook_uuid) else: - logger.warning("[LINE] webhook_uuid 为空,统一 Webhook 可能无法接收消息。") + logger.warning(t("msg-30c67081")) await self.shutdown_event.wait() async def terminate(self) -> None: @@ -128,13 +129,13 @@ async def webhook_callback(self, request: Any) -> Any: raw_body = await request.get_data() signature = request.headers.get("x-line-signature") if not self.line_api.verify_signature(raw_body, signature): - logger.warning("[LINE] invalid webhook signature") + logger.warning(t("msg-64e92929")) return "invalid signature", 400 try: payload = await request.get_json(force=True, silent=False) except Exception as e: - logger.warning("[LINE] invalid webhook body: %s", e) + logger.warning(t("msg-321afd59", e=e)) return "bad request", 400 if not isinstance(payload, dict): @@ -158,7 +159,7 @@ async def handle_webhook_event(self, payload: dict[str, Any]) -> None: event_id = str(event.get("webhookEventId", "")) if event_id and self._is_duplicate_event(event_id): - logger.debug("[LINE] duplicate event skipped: %s", event_id) + logger.debug(t("msg-1079248e", event_id=event_id)) continue abm = await self.convert_message(event) diff --git a/astrbot/core/platform/sources/line/line_api.py b/astrbot/core/platform/sources/line/line_api.py index 32204bd6ee..1dd8f716cb 100644 --- a/astrbot/core/platform/sources/line/line_api.py +++ b/astrbot/core/platform/sources/line/line_api.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import base64 import hmac @@ -102,14 +103,11 @@ async def _post_json( return True body = await resp.text() logger.error( - "[LINE] %s message failed: status=%s body=%s", - op_name, - resp.status, - body, + t("msg-dc6656f8", op_name=op_name, res=resp.status, body=body), ) return False except Exception as e: - logger.error("[LINE] %s message request failed: %s", op_name, e) + logger.error(t("msg-10996a43", op_name=op_name, e=e)) return False async def get_message_content( @@ -128,10 +126,7 @@ async def get_message_content( if retry_resp.status != 200: body = await retry_resp.text() logger.warning( - "[LINE] get content retry failed: message_id=%s status=%s body=%s", - message_id, - retry_resp.status, - body, + t("msg-5aa92977", message_id=message_id, res=retry_resp.status, body=body), ) return None return await self._read_content_response(retry_resp) @@ -139,10 +134,7 @@ async def get_message_content( if resp.status != 200: body = await resp.text() logger.warning( - "[LINE] get content failed: message_id=%s status=%s body=%s", - message_id, - resp.status, - body, + t("msg-cf700d79", message_id=message_id, res=resp.status, body=body), ) return None return await self._read_content_response(resp) diff --git a/astrbot/core/platform/sources/line/line_event.py b/astrbot/core/platform/sources/line/line_event.py index 04be53922b..01d65eb4bd 100644 --- a/astrbot/core/platform/sources/line/line_event.py +++ b/astrbot/core/platform/sources/line/line_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import os import re @@ -109,7 +110,7 @@ async def _resolve_image_url(segment: Image) -> str: try: return await segment.register_to_file_service() except Exception as e: - logger.debug("[LINE] resolve image url failed: %s", e) + logger.debug(t("msg-a491ddd0", e=e)) return "" @staticmethod @@ -120,7 +121,7 @@ async def _resolve_record_url(segment: Record) -> str: try: return await segment.register_to_file_service() except Exception as e: - logger.debug("[LINE] resolve record url failed: %s", e) + logger.debug(t("msg-ca47546c", e=e)) return "" @staticmethod @@ -131,7 +132,7 @@ async def _resolve_record_duration(segment: Record) -> int: if isinstance(duration_ms, int) and duration_ms > 0: return duration_ms except Exception as e: - logger.debug("[LINE] resolve record duration failed: %s", e) + logger.debug(t("msg-616e5840", e=e)) return 1000 @staticmethod @@ -142,7 +143,7 @@ async def _resolve_video_url(segment: Video) -> str: try: return await segment.register_to_file_service() except Exception as e: - logger.debug("[LINE] resolve video url failed: %s", e) + logger.debug(t("msg-c953a061", e=e)) return "" @staticmethod @@ -158,7 +159,7 @@ async def _resolve_video_preview_url(segment: Video) -> str: cover_seg = Image(file=cover_candidate) return await cover_seg.register_to_file_service() except Exception as e: - logger.debug("[LINE] resolve video cover failed: %s", e) + logger.debug(t("msg-19078257", e=e)) try: video_path = await segment.convert_to_file_path() @@ -186,7 +187,7 @@ async def _resolve_video_preview_url(segment: Video) -> str: cover_seg = Image.fromFileSystem(str(thumb_path)) return await cover_seg.register_to_file_service() except Exception as e: - logger.debug("[LINE] generate video preview failed: %s", e) + logger.debug(t("msg-eccdecff", e=e)) return "" @staticmethod @@ -196,7 +197,7 @@ async def _resolve_file_url(segment: File) -> str: try: return await segment.register_to_file_service() except Exception as e: - logger.debug("[LINE] resolve file url failed: %s", e) + logger.debug(t("msg-b833dc32", e=e)) return "" @staticmethod @@ -206,7 +207,7 @@ async def _resolve_file_size(segment: File) -> int: if file_path and os.path.exists(file_path): return int(os.path.getsize(file_path)) except Exception as e: - logger.debug("[LINE] resolve file size failed: %s", e) + logger.debug(t("msg-60290793", e=e)) return 0 @classmethod @@ -222,7 +223,7 @@ async def build_line_messages(cls, message_chain: MessageChain) -> list[dict]: if len(messages) > 5: logger.warning( - "[LINE] message count exceeds 5, extra segments will be dropped." + t("msg-d6443173") ) messages = messages[:5] return messages diff --git a/astrbot/core/platform/sources/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index fd61c3e506..a67e10cd61 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import os import random @@ -123,7 +124,7 @@ def meta(self) -> PlatformMetadata: async def run(self) -> None: if not self.instance_url or not self.access_token: - logger.error("[Misskey] 配置不完整,无法启动") + logger.error(t("msg-7bacee77")) return self.api = MisskeyAPI( @@ -141,10 +142,10 @@ async def run(self) -> None: self.client_self_id = str(user_info.get("id", "")) self._bot_username = user_info.get("username", "") logger.info( - f"[Misskey] 已连接用户: {self._bot_username} (ID: {self.client_self_id})", + t("msg-99cdf3d3", res=self._bot_username, res_2=self.client_self_id), ) except Exception as e: - logger.error(f"[Misskey] 获取用户信息失败: {e}") + logger.error(t("msg-5579c974", e=e)) self._running = False return @@ -243,7 +244,7 @@ async def _start_websocket_connection(self) -> None: try: connection_attempts += 1 if not self.api: - logger.error("[Misskey] API 客户端未初始化") + logger.error(t("msg-d9547102")) break streaming = self.api.get_streaming_client() @@ -251,32 +252,32 @@ async def _start_websocket_connection(self) -> None: if await streaming.connect(): logger.info( - f"[Misskey] WebSocket 已连接 (尝试 #{connection_attempts})", + t("msg-341b0aa0", connection_attempts=connection_attempts), ) connection_attempts = 0 await streaming.subscribe_channel("main") if self.enable_chat: await streaming.subscribe_channel("messaging") await streaming.subscribe_channel("messagingIndex") - logger.info("[Misskey] 聊天频道已订阅") + logger.info(t("msg-c77d157b")) backoff_delay = 1.0 await streaming.listen() else: logger.error( - f"[Misskey] WebSocket 连接失败 (尝试 #{connection_attempts})", + t("msg-a0c5edc0", connection_attempts=connection_attempts), ) except Exception as e: logger.error( - f"[Misskey] WebSocket 异常 (尝试 #{connection_attempts}): {e}", + t("msg-1958faa8", connection_attempts=connection_attempts, e=e), ) if self._running: jitter = random.uniform(0, 1.0) sleep_time = backoff_delay + jitter logger.info( - f"[Misskey] {sleep_time:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})", + t("msg-1b47382d", sleep_time=sleep_time, res=connection_attempts + 1), ) await asyncio.sleep(sleep_time) backoff_delay = min(backoff_delay * backoff_multiplier, max_backoff) @@ -285,13 +286,13 @@ async def _handle_notification(self, data: dict[str, Any]) -> None: try: notification_type = data.get("type") logger.debug( - f"[Misskey] 收到通知事件: type={notification_type}, user_id={data.get('userId', 'unknown')}", + t("msg-a10a224d", notification_type=notification_type, res=data.get('userId', 'unknown')), ) if notification_type in ["mention", "reply", "quote"]: note = data.get("note") if note and self._is_bot_mentioned(note): logger.info( - f"[Misskey] 处理贴文提及: {note.get('text', '')[:50]}...", + t("msg-7f0abf4a", res=note.get('text', '')[:50]), ) message = await self.convert_message(note) event = MisskeyPlatformEvent( @@ -303,7 +304,7 @@ async def _handle_notification(self, data: dict[str, Any]) -> None: ) self.commit_event(event) except Exception as e: - logger.error(f"[Misskey] 处理通知失败: {e}") + logger.error(t("msg-2da7cdf5", e=e)) async def _handle_chat_message(self, data: dict[str, Any]) -> None: try: @@ -312,7 +313,7 @@ async def _handle_chat_message(self, data: dict[str, Any]) -> None: ) room_id = data.get("toRoomId") logger.debug( - f"[Misskey] 收到聊天事件: sender_id={sender_id}, room_id={room_id}, is_self={sender_id == self.client_self_id}", + t("msg-6c21d412", sender_id=sender_id, room_id=room_id, res=sender_id == self.client_self_id), ) if sender_id == self.client_self_id: return @@ -320,14 +321,14 @@ async def _handle_chat_message(self, data: dict[str, Any]) -> None: if room_id: raw_text = data.get("text", "") logger.debug( - f"[Misskey] 检查群聊消息: '{raw_text}', 机器人用户名: '{self._bot_username}'", + t("msg-68269731", raw_text=raw_text, res=self._bot_username), ) message = await self.convert_room_message(data) - logger.info(f"[Misskey] 处理群聊消息: {message.message_str[:50]}...") + logger.info(t("msg-585aa62b", res=message.message_str[:50])) else: message = await self.convert_chat_message(data) - logger.info(f"[Misskey] 处理私聊消息: {message.message_str[:50]}...") + logger.info(t("msg-426c7874", res=message.message_str[:50])) event = MisskeyPlatformEvent( message_str=message.message_str, @@ -338,12 +339,12 @@ async def _handle_chat_message(self, data: dict[str, Any]) -> None: ) self.commit_event(event) except Exception as e: - logger.error(f"[Misskey] 处理聊天消息失败: {e}") + logger.error(t("msg-f5aff493", e=e)) async def _debug_handler(self, data: dict[str, Any]) -> None: event_type = data.get("type", "unknown") logger.debug( - f"[Misskey] 收到未处理事件: type={event_type}, channel={data.get('channel', 'unknown')}", + t("msg-ea465183", event_type=event_type, res=data.get('channel', 'unknown')), ) def _is_bot_mentioned(self, note: dict[str, Any]) -> bool: @@ -371,7 +372,7 @@ async def send_by_session( message_chain: MessageChain, ) -> None: if not self.api: - logger.error("[Misskey] API 客户端未初始化") + logger.error(t("msg-d9547102")) return await super().send_by_session(session, message_chain) try: @@ -408,7 +409,7 @@ async def send_by_session( if not text or not text.strip(): if not has_file_components: - logger.warning("[Misskey] 消息内容为空且无文件组件,跳过发送") + logger.warning(t("msg-8b69eb93")) return await super().send_by_session(session, message_chain) text = "" @@ -504,7 +505,7 @@ async def _upload_comp(comp) -> object | None: ): try: os.remove(local_path) - logger.debug(f"[Misskey] 已清理临时文件: {local_path}") + logger.debug(t("msg-9ba9c4e5", local_path=local_path)) except Exception: pass @@ -529,7 +530,7 @@ async def _upload_comp(comp) -> object | None: if len(file_components) > MAX_FILE_UPLOAD_COUNT: logger.warning( - f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件", + t("msg-91af500e", res=len(file_components), MAX_FILE_UPLOAD_COUNT=MAX_FILE_UPLOAD_COUNT), ) file_components = file_components[:MAX_FILE_UPLOAD_COUNT] @@ -552,7 +553,7 @@ async def _upload_comp(comp) -> object | None: except Exception: pass except Exception: - logger.debug("[Misskey] 并发上传过程中出现异常,继续发送文本") + logger.debug(t("msg-9746d7f5")) if session_id and is_valid_room_session_id(session_id): from .misskey_utils import extract_room_id_from_session_id @@ -582,7 +583,7 @@ async def _upload_comp(comp) -> object | None: payload["fileId"] = file_ids[0] if len(file_ids) > 1: logger.warning( - f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件", + t("msg-d6dc928c", res=len(file_ids) - 1), ) await self.api.send_message(payload) else: @@ -602,7 +603,7 @@ async def _upload_comp(comp) -> object | None: default_visibility=self.default_visibility, ) logger.debug( - f"[Misskey] 解析可见性: visibility={visibility}, visible_user_ids={visible_user_ids}, session_id={session_id}, user_id_for_cache={user_id_for_cache}", + t("msg-af584ae8", visibility=visibility, visible_user_ids=visible_user_ids, session_id=session_id, user_id_for_cache=user_id_for_cache), ) fields = self._extract_additional_fields(session, message_chain) @@ -627,7 +628,7 @@ async def _upload_comp(comp) -> object | None: ) except Exception as e: - logger.error(f"[Misskey] 发送消息失败: {e}") + logger.error(t("msg-1a176905", e=e)) return await super().send_by_session(session, message_chain) diff --git a/astrbot/core/platform/sources/misskey/misskey_api.py b/astrbot/core/platform/sources/misskey/misskey_api.py index 3e5eb9a90e..70992a2e50 100644 --- a/astrbot/core/platform/sources/misskey/misskey_api.py +++ b/astrbot/core/platform/sources/misskey/misskey_api.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json import random @@ -10,7 +11,7 @@ import websockets except ImportError as e: raise ImportError( - "aiohttp and websockets are required for Misskey API. Please install them with: pip install aiohttp websockets", + t("msg-fab20f57", e=e), ) from e from astrbot.api import logger @@ -70,7 +71,7 @@ async def connect(self) -> bool: self.is_connected = True self._running = True - logger.info("[Misskey WebSocket] 已连接") + logger.info(t("msg-f2eea8e1")) if self.desired_channels: try: desired = list(self.desired_channels.items()) @@ -79,14 +80,14 @@ async def connect(self) -> bool: await self.subscribe_channel(channel_type, params) except Exception as e: logger.warning( - f"[Misskey WebSocket] 重新订阅 {channel_type} 失败: {e}", + t("msg-5efd11a2", channel_type=channel_type, e=e), ) except Exception: pass return True except Exception as e: - logger.error(f"[Misskey WebSocket] 连接失败: {e}") + logger.error(t("msg-b70e2176", e=e)) self.is_connected = False return False @@ -96,7 +97,7 @@ async def disconnect(self) -> None: await self.websocket.close() self.websocket = None self.is_connected = False - logger.info("[Misskey WebSocket] 连接已断开") + logger.info(t("msg-b9f3ee06")) async def subscribe_channel( self, @@ -104,7 +105,7 @@ async def subscribe_channel( params: dict | None = None, ) -> str: if not self.is_connected or not self.websocket: - raise WebSocketError("WebSocket 未连接") + raise WebSocketError(t("msg-7cd98e54")) channel_id = str(uuid.uuid4()) message = { @@ -141,7 +142,7 @@ def add_message_handler( async def listen(self) -> None: if not self.is_connected or not self.websocket: - raise WebSocketError("WebSocket 未连接") + raise WebSocketError(t("msg-7cd98e54")) try: async for message in self.websocket: @@ -152,12 +153,12 @@ async def listen(self) -> None: data = json.loads(message) await self._handle_message(data) except json.JSONDecodeError as e: - logger.warning(f"[Misskey WebSocket] 无法解析消息: {e}") + logger.warning(t("msg-43566304", e=e)) except Exception as e: - logger.error(f"[Misskey WebSocket] 处理消息失败: {e}") + logger.error(t("msg-e617e390", e=e)) except websockets.exceptions.ConnectionClosedError as e: - logger.warning(f"[Misskey WebSocket] 连接意外关闭: {e}") + logger.warning(t("msg-c60715cf", e=e)) self.is_connected = False try: await self.disconnect() @@ -165,7 +166,7 @@ async def listen(self) -> None: pass except websockets.exceptions.ConnectionClosed as e: logger.warning( - f"[Misskey WebSocket] 连接已关闭 (代码: {e.code}, 原因: {e.reason})", + t("msg-da9a2a17", res=e.code, res_2=e.reason), ) self.is_connected = False try: @@ -173,14 +174,14 @@ async def listen(self) -> None: except Exception: pass except websockets.exceptions.InvalidHandshake as e: - logger.error(f"[Misskey WebSocket] 握手失败: {e}") + logger.error(t("msg-bbf6a42e", e=e)) self.is_connected = False try: await self.disconnect() except Exception: pass except Exception as e: - logger.error(f"[Misskey WebSocket] 监听消息失败: {e}") + logger.error(t("msg-254f0237", e=e)) self.is_connected = False try: await self.disconnect() @@ -219,7 +220,7 @@ def _build_channel_summary(message_type: str | None, body: Any) -> str: return f"[Misskey WebSocket] 收到消息类型: {message_type}" channel_summary = _build_channel_summary(message_type, body) - logger.info(channel_summary) + logger.info(t("msg-49f7e90e", channel_summary=channel_summary)) if message_type == "channel": channel_id = body.get("id") @@ -227,7 +228,7 @@ def _build_channel_summary(message_type: str | None, body: Any) -> str: event_body = body.get("body", {}) logger.debug( - f"[Misskey WebSocket] 频道消息: {channel_id}, 事件类型: {event_type}", + t("msg-630a4832", channel_id=channel_id, event_type=event_type), ) if channel_id in self.channels: @@ -235,14 +236,14 @@ def _build_channel_summary(message_type: str | None, body: Any) -> str: handler_key = f"{channel_type}:{event_type}" if handler_key in self.message_handlers: - logger.debug(f"[Misskey WebSocket] 使用处理器: {handler_key}") + logger.debug(t("msg-0dc61a4d", handler_key=handler_key)) await self.message_handlers[handler_key](event_body) elif event_type in self.message_handlers: - logger.debug(f"[Misskey WebSocket] 使用事件处理器: {event_type}") + logger.debug(t("msg-012666fc", event_type=event_type)) await self.message_handlers[event_type](event_body) else: logger.debug( - f"[Misskey WebSocket] 未找到处理器: {handler_key} 或 {event_type}", + t("msg-e202168a", handler_key=handler_key, event_type=event_type), ) if "_debug" in self.message_handlers: await self.message_handlers["_debug"]( @@ -254,10 +255,10 @@ def _build_channel_summary(message_type: str | None, body: Any) -> str: ) elif message_type in self.message_handlers: - logger.debug(f"[Misskey WebSocket] 直接消息处理器: {message_type}") + logger.debug(t("msg-a397eef1", message_type=message_type)) await self.message_handlers[message_type](body) else: - logger.debug(f"[Misskey WebSocket] 未处理的消息类型: {message_type}") + logger.debug(t("msg-a5f12225", message_type=message_type)) if "_debug" in self.message_handlers: await self.message_handlers["_debug"](data) @@ -290,7 +291,7 @@ async def wrapper(*args, **kwargs): last_exc = e if attempt == max_retries: logger.error( - f"[Misskey API] {func_name} 重试 {max_retries} 次后仍失败: {e}", + t("msg-ad61d480", func_name=func_name, max_retries=max_retries, e=e), ) break @@ -306,14 +307,13 @@ async def wrapper(*args, **kwargs): sleep_time = backoff + jitter logger.warning( - f"[Misskey API] {func_name} 第 {attempt} 次重试失败: {e}," - f"{sleep_time:.1f}s后重试", + t("msg-7de2ca49", func_name=func_name, attempt=attempt, e=e, sleep_time=sleep_time), ) await asyncio.sleep(sleep_time) continue except Exception as e: # 非可重试异常直接抛出 - logger.error(f"[Misskey API] {func_name} 遇到不可重试异常: {e}") + logger.error(t("msg-f5aecf37", func_name=func_name, e=e)) raise if last_exc: @@ -361,7 +361,7 @@ async def close(self) -> None: if self._session: await self._session.close() self._session = None - logger.debug("[Misskey API] 客户端已关闭") + logger.debug(t("msg-e5852be5")) def get_streaming_client(self) -> StreamingClient: if not self.streaming: @@ -378,37 +378,37 @@ def session(self) -> aiohttp.ClientSession: def _handle_response_status(self, status: int, endpoint: str) -> NoReturn: """处理 HTTP 响应状态码""" if status == 400: - logger.error(f"[Misskey API] 请求参数错误: {endpoint} (HTTP {status})") - raise APIError(f"Bad request for {endpoint}") + logger.error(t("msg-21fc185c", endpoint=endpoint, status=status)) + raise APIError(t("msg-5b106def", endpoint=endpoint)) if status == 401: - logger.error(f"[Misskey API] 未授权访问: {endpoint} (HTTP {status})") - raise AuthenticationError(f"Unauthorized access for {endpoint}") + logger.error(t("msg-28afff67", endpoint=endpoint, status=status)) + raise AuthenticationError(t("msg-e12f2d28", endpoint=endpoint)) if status == 403: - logger.error(f"[Misskey API] 访问被禁止: {endpoint} (HTTP {status})") - raise AuthenticationError(f"Forbidden access for {endpoint}") + logger.error(t("msg-beda662d", endpoint=endpoint, status=status)) + raise AuthenticationError(t("msg-795ca227", endpoint=endpoint)) if status == 404: - logger.error(f"[Misskey API] 资源不存在: {endpoint} (HTTP {status})") - raise APIError(f"Resource not found for {endpoint}") + logger.error(t("msg-5c6ba873", endpoint=endpoint, status=status)) + raise APIError(t("msg-74f2bac2", endpoint=endpoint)) if status == 413: - logger.error(f"[Misskey API] 请求体过大: {endpoint} (HTTP {status})") - raise APIError(f"Request entity too large for {endpoint}") + logger.error(t("msg-9ceafe4c", endpoint=endpoint, status=status)) + raise APIError(t("msg-3e336b73", endpoint=endpoint)) if status == 429: - logger.warning(f"[Misskey API] 请求频率限制: {endpoint} (HTTP {status})") - raise APIRateLimitError(f"Rate limit exceeded for {endpoint}") + logger.warning(t("msg-a47067de", endpoint=endpoint, status=status)) + raise APIRateLimitError(t("msg-901dc2da", endpoint=endpoint)) if status == 500: - logger.error(f"[Misskey API] 服务器内部错误: {endpoint} (HTTP {status})") - raise APIConnectionError(f"Internal server error for {endpoint}") + logger.error(t("msg-2bea8c2e", endpoint=endpoint, status=status)) + raise APIConnectionError(t("msg-ae8d3725", endpoint=endpoint)) if status == 502: - logger.error(f"[Misskey API] 网关错误: {endpoint} (HTTP {status})") - raise APIConnectionError(f"Bad gateway for {endpoint}") + logger.error(t("msg-7b028462", endpoint=endpoint, status=status)) + raise APIConnectionError(t("msg-978414ef", endpoint=endpoint)) if status == 503: - logger.error(f"[Misskey API] 服务不可用: {endpoint} (HTTP {status})") - raise APIConnectionError(f"Service unavailable for {endpoint}") + logger.error(t("msg-50895a69", endpoint=endpoint, status=status)) + raise APIConnectionError(t("msg-62adff89", endpoint=endpoint)) if status == 504: - logger.error(f"[Misskey API] 网关超时: {endpoint} (HTTP {status})") - raise APIConnectionError(f"Gateway timeout for {endpoint}") - logger.error(f"[Misskey API] 未知错误: {endpoint} (HTTP {status})") - raise APIConnectionError(f"HTTP {status} for {endpoint}") + logger.error(t("msg-1cf15497", endpoint=endpoint, status=status)) + raise APIConnectionError(t("msg-a8a2578d", endpoint=endpoint)) + logger.error(t("msg-c012110a", endpoint=endpoint, status=status)) + raise APIConnectionError(t("msg-dc96bbb8", status=status, endpoint=endpoint)) async def _process_response( self, @@ -429,23 +429,23 @@ async def _process_response( ) if notifications_data: logger.debug( - f"[Misskey API] 获取到 {len(notifications_data)} 条新通知", + t("msg-4c7598b6", res=len(notifications_data)), ) else: - logger.debug(f"[Misskey API] 请求成功: {endpoint}") + logger.debug(t("msg-851a2a54", endpoint=endpoint)) return result except json.JSONDecodeError as e: - logger.error(f"[Misskey API] 响应格式错误: {e}") - raise APIConnectionError("Invalid JSON response") from e + logger.error(t("msg-5f5609b6", e=e)) + raise APIConnectionError(t("msg-c8f7bbeb", e=e)) from e else: try: error_text = await response.text() logger.error( - f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}, 响应: {error_text}", + t("msg-82748b31", endpoint=endpoint, res=response.status, error_text=error_text), ) except Exception: logger.error( - f"[Misskey API] 请求失败: {endpoint} - HTTP {response.status}", + t("msg-c6de3320", endpoint=endpoint, res=response.status), ) self._handle_response_status(response.status, endpoint) @@ -468,8 +468,8 @@ async def _make_request( async with self.session.post(url, json=payload) as response: return await self._process_response(response, endpoint) except aiohttp.ClientError as e: - logger.error(f"[Misskey API] HTTP 请求错误: {e}") - raise APIConnectionError(f"HTTP request failed: {e}") from e + logger.error(t("msg-affb19a7", e=e)) + raise APIConnectionError(t("msg-9f1286b3", e=e)) from e async def create_note( self, @@ -532,7 +532,7 @@ async def create_note( if isinstance(result, dict) else "unknown" ) - logger.debug(f"[Misskey API] 发帖成功: {note_id}") + logger.debug(t("msg-44f91be2", note_id=note_id)) return result async def upload_file( @@ -543,7 +543,7 @@ async def upload_file( ) -> dict[str, Any]: """Upload a file to Misskey drive/files/create and return a dict containing id and raw result.""" if not file_path: - raise APIError("No file path provided for upload") + raise APIError(t("msg-fbafd3db")) url = f"{self.instance_url}/api/drive/files/create" form = aiohttp.FormData() @@ -557,8 +557,8 @@ async def upload_file( try: f = open(file_path, "rb") except FileNotFoundError as e: - logger.error(f"[Misskey API] 本地文件不存在: {file_path}") - raise APIError(f"File not found: {file_path}") from e + logger.error(t("msg-872d8419", file_path=file_path)) + raise APIError(t("msg-37186dea", file_path=file_path, e=e)) from e try: form.add_field("file", f, filename=filename) @@ -566,31 +566,31 @@ async def upload_file( result = await self._process_response(resp, "drive/files/create") file_id = FileIDExtractor.extract_file_id(result) logger.debug( - f"[Misskey API] 本地文件上传成功: {filename} -> {file_id}", + t("msg-65ef68e0", filename=filename, file_id=file_id), ) return {"id": file_id, "raw": result} finally: f.close() except aiohttp.ClientError as e: - logger.error(f"[Misskey API] 文件上传网络错误: {e}") - raise APIConnectionError(f"Upload failed: {e}") from e + logger.error(t("msg-0951db67", e=e)) + raise APIConnectionError(t("msg-e3a322f5", e=e)) from e async def find_files_by_hash(self, md5_hash: str) -> list[dict[str, Any]]: """Find files by MD5 hash""" if not md5_hash: - raise APIError("No MD5 hash provided for find-by-hash") + raise APIError(t("msg-f28772b9")) data = {"md5": md5_hash} try: - logger.debug(f"[Misskey API] find-by-hash 请求: md5={md5_hash}") + logger.debug(t("msg-25e566ef", md5_hash=md5_hash)) result = await self._make_request("drive/files/find-by-hash", data) logger.debug( - f"[Misskey API] find-by-hash 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件", + t("msg-a036a942", res=len(result) if isinstance(result, list) else 0), ) return result if isinstance(result, list) else [] except Exception as e: - logger.error(f"[Misskey API] 根据哈希查找文件失败: {e}") + logger.error(t("msg-ea3581d5", e=e)) raise async def find_files_by_name( @@ -600,21 +600,21 @@ async def find_files_by_name( ) -> list[dict[str, Any]]: """Find files by name""" if not name: - raise APIError("No name provided for find") + raise APIError(t("msg-1d2a84ff")) data: dict[str, Any] = {"name": name} if folder_id: data["folderId"] = folder_id try: - logger.debug(f"[Misskey API] find 请求: name={name}, folder_id={folder_id}") + logger.debug(t("msg-f25e28b4", name=name, folder_id=folder_id)) result = await self._make_request("drive/files/find", data) logger.debug( - f"[Misskey API] find 响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件", + t("msg-cd43861a", res=len(result) if isinstance(result, list) else 0), ) return result if isinstance(result, list) else [] except Exception as e: - logger.error(f"[Misskey API] 根据名称查找文件失败: {e}") + logger.error(t("msg-05cd55ef", e=e)) raise async def find_files( @@ -632,15 +632,15 @@ async def find_files( try: logger.debug( - f"[Misskey API] 列表文件请求: limit={limit}, folder_id={folder_id}, type={type}", + t("msg-c01052a4", limit=limit, folder_id=folder_id, type=type), ) result = await self._make_request("drive/files", data) logger.debug( - f"[Misskey API] 列表文件响应: 找到 {len(result) if isinstance(result, list) else 0} 个文件", + t("msg-7c81620d", res=len(result) if isinstance(result, list) else 0), ) return result if isinstance(result, list) else [] except Exception as e: - logger.error(f"[Misskey API] 列表文件失败: {e}") + logger.error(t("msg-a187a089", e=e)) raise async def _download_with_existing_session( @@ -650,7 +650,7 @@ async def _download_with_existing_session( ) -> bytes | None: """使用现有会话下载文件""" if not (hasattr(self, "session") and self.session): - raise APIConnectionError("No existing session available") + raise APIConnectionError(t("msg-9e776259")) async with self.session.get( url, @@ -699,7 +699,7 @@ async def upload_and_find_file( """ if not url: - raise APIError("URL不能为空") + raise APIError(t("msg-de18c220")) # 通过本地上传获取即时文件 ID(下载文件 → 上传 → 返回 ID) try: @@ -715,7 +715,7 @@ async def upload_and_find_file( ) or await self._download_with_temp_session(url, ssl_verify=True) except Exception as ssl_error: logger.debug( - f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL", + t("msg-25b15b61", ssl_error=ssl_error), ) try: tmp_bytes = await self._download_with_existing_session( @@ -732,7 +732,7 @@ async def upload_and_find_file( try: result = await self.upload_file(tmp_path, name, folder_id) - logger.debug(f"[Misskey API] 本地上传成功: {result.get('id')}") + logger.debug(t("msg-b6cbeef6", res=result.get('id'))) return result finally: try: @@ -740,7 +740,7 @@ async def upload_and_find_file( except Exception: pass except Exception as e: - logger.error(f"[Misskey API] 本地上传失败: {e}") + logger.error(t("msg-a4a898e2", e=e)) return None @@ -764,7 +764,7 @@ async def send_message( result = await self._make_request("chat/messages/create-to-user", data) message_id = result.get("id", "unknown") - logger.debug(f"[Misskey API] 聊天消息发送成功: {message_id}") + logger.debug(t("msg-46b7ea4b", message_id=message_id)) return result async def send_room_message( @@ -783,7 +783,7 @@ async def send_room_message( result = await self._make_request("chat/messages/create-to-room", data) message_id = result.get("id", "unknown") - logger.debug(f"[Misskey API] 房间消息发送成功: {message_id}") + logger.debug(t("msg-32f71df4", message_id=message_id)) return result async def get_messages( @@ -800,7 +800,7 @@ async def get_messages( result = await self._make_request("chat/messages/user-timeline", data) if isinstance(result, list): return result - logger.warning(f"[Misskey API] 聊天消息响应格式异常: {type(result)}") + logger.warning(t("msg-7829f3b3", res=type(result))) return [] async def get_mentions( @@ -819,7 +819,7 @@ async def get_mentions( return result if isinstance(result, dict) and "notifications" in result: return result["notifications"] - logger.warning(f"[Misskey API] 提及通知响应格式异常: {type(result)}") + logger.warning(t("msg-d74c86a1", res=type(result))) return [] async def send_message_with_media( @@ -849,7 +849,7 @@ async def send_message_with_media( """ if not text and not media_urls and not local_files: - raise APIError("消息内容不能为空:需要文本或媒体文件") + raise APIError(t("msg-65ccb697")) file_ids = [] @@ -878,11 +878,11 @@ async def _process_media_urls(self, urls: list[str]) -> list[str]: result = await self.upload_and_find_file(url) if result and result.get("id"): file_ids.append(result["id"]) - logger.debug(f"[Misskey API] URL媒体上传成功: {result['id']}") + logger.debug(t("msg-b6afb123", res=result['id'])) else: - logger.error(f"[Misskey API] URL媒体上传失败: {url}") + logger.error(t("msg-4e62bcdc", url=url)) except Exception as e: - logger.error(f"[Misskey API] URL媒体处理失败 {url}: {e}") + logger.error(t("msg-71cc9d61", url=url, e=e)) # 继续处理其他文件,不中断整个流程 continue return file_ids @@ -895,11 +895,11 @@ async def _process_local_files(self, file_paths: list[str]) -> list[str]: result = await self.upload_file(file_path) if result and result.get("id"): file_ids.append(result["id"]) - logger.debug(f"[Misskey API] 本地文件上传成功: {result['id']}") + logger.debug(t("msg-75890c2b", res=result['id'])) else: - logger.error(f"[Misskey API] 本地文件上传失败: {file_path}") + logger.error(t("msg-024d0ed5", file_path=file_path)) except Exception as e: - logger.error(f"[Misskey API] 本地文件处理失败 {file_path}: {e}") + logger.error(t("msg-f1fcb5e1", file_path=file_path, e=e)) continue return file_ids @@ -960,4 +960,4 @@ async def _dispatch_message( note_kwargs.update(kwargs) return await self.create_note(**note_kwargs) - raise APIError(f"不支持的消息类型: {message_type}") + raise APIError(t("msg-1ee80a6b", message_type=message_type)) diff --git a/astrbot/core/platform/sources/misskey/misskey_event.py b/astrbot/core/platform/sources/misskey/misskey_event.py index 068f7e7a28..93d1170e1b 100644 --- a/astrbot/core/platform/sources/misskey/misskey_event.py +++ b/astrbot/core/platform/sources/misskey/misskey_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import re from collections.abc import AsyncGenerator @@ -44,7 +45,7 @@ async def send(self, message: MessageChain) -> None: """发送消息,使用适配器的完整上传和发送逻辑""" try: logger.debug( - f"[MisskeyEvent] send 方法被调用,消息链包含 {len(message.chain)} 个组件", + t("msg-85cb7d49", res=len(message.chain)), ) # 使用适配器的 send_by_session 方法,它包含文件上传逻辑 @@ -66,19 +67,19 @@ async def send(self, message: MessageChain) -> None: ) logger.debug( - f"[MisskeyEvent] 检查适配器方法: hasattr(self.client, 'send_by_session') = {hasattr(self.client, 'send_by_session')}", + t("msg-252c2fca", res=hasattr(self.client, 'send_by_session')), ) # 调用适配器的 send_by_session 方法 if hasattr(self.client, "send_by_session"): - logger.debug("[MisskeyEvent] 调用适配器的 send_by_session 方法") + logger.debug(t("msg-44d7a060")) await self.client.send_by_session(session, message) else: # 回退到原来的简化发送逻辑 content, has_at = serialize_message_chain(message.chain) if not content: - logger.debug("[MisskeyEvent] 内容为空,跳过发送") + logger.debug(t("msg-b6e08872")) return original_message_id = getattr(self.message_obj, "message_id", None) @@ -118,13 +119,13 @@ async def send(self, message: MessageChain) -> None: visible_user_ids=visible_user_ids, ) elif hasattr(self.client, "create_note"): - logger.debug("[MisskeyEvent] 创建新帖子") + logger.debug(t("msg-8cfebc9c")) await self.client.create_note(content) await super().send(message) except Exception as e: - logger.error(f"[MisskeyEvent] 发送失败: {e}") + logger.error(t("msg-ed0d2ed5", e=e)) async def send_streaming( self, diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 868ec8a657..538b2d986c 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import base64 import os @@ -39,7 +40,7 @@ def _patch_qq_botpy_formdata() -> None: if not hasattr(_FormData, "_is_processed"): setattr(_FormData, "_is_processed", False) except Exception: - logger.debug("[QQOfficial] Skip botpy FormData patch.") + logger.debug(t("msg-28a74d9d")) _patch_qq_botpy_formdata() @@ -100,7 +101,7 @@ async def send_streaming(self, generator, use_fallback: bool = False): ret = await self._post_send() except Exception as e: - logger.error(f"发送流式消息时出错: {e}", exc_info=True) + logger.error(t("msg-c0b123f6", e=e), exc_info=True) self.send_buffer = None return await super().send_streaming(generator, use_fallback) @@ -118,7 +119,7 @@ async def _post_send(self, stream: dict | None = None): | botpy.message.DirectMessage | botpy.message.C2CMessage, ): - logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}") + logger.warning(t("msg-05d6bba5", res=type(source))) return None ( @@ -151,7 +152,7 @@ async def _post_send(self, stream: dict | None = None): match source: case botpy.message.GroupMessage(): if not source.group_openid: - logger.error("[QQOfficial] GroupMessage 缺少 group_openid") + logger.error(t("msg-e5339577")) return None if image_base64: @@ -223,7 +224,7 @@ async def _post_send(self, stream: dict | None = None): payload=payload, plain_text=plain_text, ) - logger.debug(f"Message sent to C2C: {ret}") + logger.debug(t("msg-71275806", ret=ret)) case botpy.message.Message(): if image_path: @@ -279,7 +280,7 @@ async def _send_with_markdown_fallback( raise logger.warning( - "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" + t("msg-040e7942") ) fallback_payload = payload.copy() fallback_payload["markdown"] = None @@ -314,11 +315,11 @@ async def upload_group_and_c2c_image( ) result = await self.bot.api._http.request(route, json=payload) else: - raise ValueError("Invalid upload parameters") + raise ValueError(t("msg-9000f8f7")) if not isinstance(result, dict): raise RuntimeError( - f"Failed to upload image, response is not dict: {result}" + t("msg-d72cffe7", result=result) ) return Media( @@ -369,7 +370,7 @@ async def upload_group_and_c2c_record( if result: if not isinstance(result, dict): - logger.error(f"上传文件响应格式错误: {result}") + logger.error(t("msg-5944a27c", result=result)) return None return Media( @@ -378,7 +379,7 @@ async def upload_group_and_c2c_record( ttl=result.get("ttl", 0), ) except Exception as e: - logger.error(f"上传请求错误: {e}") + logger.error(t("msg-1e513ee5", e=e)) return None @@ -405,7 +406,7 @@ async def post_c2c_message( if not isinstance(result, dict): raise RuntimeError( - f"Failed to post c2c message, response is not dict: {result}" + t("msg-f1f1733c", result=result) ) return message.Message(**result) @@ -431,7 +432,7 @@ async def _parse_to_qqofficial(message: MessageChain): elif i.file: image_base64 = file_to_base64(i.file) else: - raise ValueError("Unsupported image file format") + raise ValueError(t("msg-9b8f9f70")) image_base64 = image_base64.removeprefix("base64://") elif isinstance(i, Record): if i.file: @@ -450,10 +451,10 @@ async def _parse_to_qqofficial(message: MessageChain): record_file_path = record_tecent_silk_path else: record_file_path = None - logger.error("转换音频格式时出错:音频时长不大于0") + logger.error(t("msg-24eb302a")) except Exception as e: - logger.error(f"处理语音时出错: {e}") + logger.error(t("msg-b49e55f9", e=e)) record_file_path = None else: - logger.debug(f"qq_official 忽略 {i.type}") + logger.debug(t("msg-6e716579", res=i.type)) return plain_text, image_base64, image_file_path, record_file_path diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 603bc8f58b..f35757c783 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -1,4 +1,5 @@ from __future__ import annotations +from astrbot.core.lang import t import asyncio import logging @@ -131,7 +132,7 @@ async def send_by_session( session: MessageSesion, message_chain: MessageChain, ) -> None: - raise NotImplementedError("QQ 机器人官方 API 适配器不支持 send_by_session") + raise NotImplementedError(t("msg-8af45ba1")) def meta(self) -> PlatformMetadata: return PlatformMetadata( @@ -235,7 +236,7 @@ def _parse_from_qqofficial( if isinstance(message, botpy.message.Message): abm.group_id = message.channel_id else: - raise ValueError(f"Unknown message type: {message_type}") + raise ValueError(t("msg-8ebd1249", message_type=message_type)) abm.self_id = "qq_official" return abm @@ -247,4 +248,4 @@ def get_client(self) -> botClient: async def terminate(self) -> None: await self.client.close() - logger.info("QQ 官方机器人接口 适配器已被优雅地关闭") + logger.info(t("msg-c165744d")) diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py index 6aae6b9ce0..78e09036ad 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import logging import random @@ -135,8 +136,7 @@ async def send_by_session( msg_id = self._session_last_message_id.get(session.session_id) if not msg_id: logger.warning( - "[QQOfficialWebhook] No cached msg_id for session: %s, skip send_by_session", - session.session_id, + t("msg-6721010c", res=session.session_id), ) return @@ -203,8 +203,7 @@ async def send_by_session( ) else: logger.warning( - "[QQOfficialWebhook] Unsupported message type for send_by_session: %s", - session.message_type, + t("msg-296dfcad", res=session.message_type), ) return @@ -277,7 +276,7 @@ async def terminate(self) -> None: await self.webhook_helper.server.shutdown() except Exception as exc: logger.warning( - f"Exception occurred during QQOfficialWebhook server shutdown: {exc}", + t("msg-6fa95bb3", exc=exc), exc_info=True, ) - logger.info("QQ 机器人官方 API 适配器已经被优雅地关闭") + logger.info(t("msg-6f83eea0")) diff --git a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py index 5f35471eea..2a1eb70bb3 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import logging from typing import cast @@ -41,9 +42,9 @@ def __init__( self.shutdown_event = asyncio.Event() async def initialize(self) -> None: - logger.info("正在登录到 QQ 官方机器人...") + logger.info(t("msg-41a3e59d")) self.user = await self.http.login(self.token) - logger.info(f"已登录 QQ 官方机器人账号: {self.user}") + logger.info(t("msg-66040e15", res=self.user)) # 直接注入到 botpy 的 Client,移花接木! self.client.api = self.api self.client.http = self.http @@ -94,7 +95,7 @@ async def handle_callback(self, request) -> dict: 响应数据 """ msg: dict = await request.json - logger.debug(f"收到 qq_official_webhook 回调: {msg}") + logger.debug(t("msg-6ed59b60", msg=msg)) event = msg.get("t") opcode = msg.get("op") @@ -103,7 +104,7 @@ async def handle_callback(self, request) -> dict: if opcode == 13: # validation signed = await self.webhook_validation(cast(dict, data)) - print(signed) + print(t("msg-ad355b59", signed=signed)) return signed if event and opcode == BotWebSocket.WS_DISPATCH_EVENT: @@ -111,7 +112,7 @@ async def handle_callback(self, request) -> dict: try: func = self._connection.parser[event] except KeyError: - logger.error("_parser unknown event %s.", event) + logger.error(t("msg-4bf0bff8", event=event)) else: func(msg) @@ -119,7 +120,7 @@ async def handle_callback(self, request) -> dict: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。", + t("msg-cef08b17", res=self.callback_server_host, res_2=self.port), ) await self.server.run_task( host=self.callback_server_host, diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 5c2f7a37f3..462a294d35 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t import asyncio import json import time @@ -111,17 +112,17 @@ async def run(self) -> None: await self.connect_websocket() retry_count = 0 except websockets.exceptions.ConnectionClosed as e: - logger.warning(f"Satori WebSocket 连接关闭: {e}") + logger.warning(t("msg-ab7db6d9", e=e)) retry_count += 1 except Exception as e: - logger.error(f"Satori WebSocket 连接失败: {e}") + logger.error(t("msg-4ef42cd1", e=e)) retry_count += 1 if not self.running: break if retry_count >= max_retries: - logger.error(f"达到最大重试次数 ({max_retries}),停止重试") + logger.error(t("msg-b50d159b", max_retries=max_retries)) break if not self.auto_reconnect: @@ -134,12 +135,12 @@ async def run(self) -> None: await self.session.close() async def connect_websocket(self) -> None: - logger.info(f"Satori 适配器正在连接到 WebSocket: {self.endpoint}") - logger.info(f"Satori 适配器 HTTP API 地址: {self.api_base_url}") + logger.info(t("msg-89de477c", res=self.endpoint)) + logger.info(t("msg-cfa5b059", res=self.api_base_url)) if not self.endpoint.startswith(("ws://", "wss://")): - logger.error(f"无效的WebSocket URL: {self.endpoint}") - raise ValueError(f"WebSocket URL必须以ws://或wss://开头: {self.endpoint}") + logger.error(t("msg-d534864b", res=self.endpoint)) + raise ValueError(t("msg-a110f9f7", res=self.endpoint)) try: websocket = await connect( @@ -160,13 +161,13 @@ async def connect_websocket(self) -> None: try: await self.handle_message(message) # type: ignore except Exception as e: - logger.error(f"Satori 处理消息异常: {e}") + logger.error(t("msg-bf43ccb6", e=e)) except websockets.exceptions.ConnectionClosed as e: - logger.warning(f"Satori WebSocket 连接关闭: {e}") + logger.warning(t("msg-ab7db6d9", e=e)) raise except Exception as e: - logger.error(f"Satori WebSocket 连接异常: {e}") + logger.error(t("msg-89081a1a", e=e)) raise finally: if self.heartbeat_task: @@ -179,14 +180,14 @@ async def connect_websocket(self) -> None: try: await self.ws.close() except Exception as e: - logger.error(f"Satori WebSocket 关闭异常: {e}") + logger.error(t("msg-5c04bfcd", e=e)) async def send_identify(self) -> None: if not self.ws: - raise Exception("WebSocket连接未建立") + raise Exception(t("msg-b67bcee0")) if self._is_websocket_closed(self.ws): - raise Exception("WebSocket连接已关闭") + raise Exception(t("msg-89ea8b76")) identify_payload = { "op": 3, # IDENTIFY @@ -203,10 +204,10 @@ async def send_identify(self) -> None: message_str = json.dumps(identify_payload, ensure_ascii=False) await self.ws.send(message_str) except websockets.exceptions.ConnectionClosed as e: - logger.error(f"发送 IDENTIFY 信令时连接关闭: {e}") + logger.error(t("msg-4c8a40e3", e=e)) raise except Exception as e: - logger.error(f"发送 IDENTIFY 信令失败: {e}") + logger.error(t("msg-05a6b99d", e=e)) raise async def heartbeat_loop(self) -> None: @@ -222,17 +223,17 @@ async def heartbeat_loop(self) -> None: } await self.ws.send(json.dumps(ping_payload, ensure_ascii=False)) except websockets.exceptions.ConnectionClosed as e: - logger.error(f"Satori WebSocket 连接关闭: {e}") + logger.error(t("msg-ab7db6d9", e=e)) break except Exception as e: - logger.error(f"Satori WebSocket 发送心跳失败: {e}") + logger.error(t("msg-c9b1b774", e=e)) break else: break except asyncio.CancelledError: pass except Exception as e: - logger.error(f"心跳任务异常: {e}") + logger.error(t("msg-61edb4f3", e=e)) async def handle_message(self, message: str) -> None: try: @@ -252,7 +253,7 @@ async def handle_message(self, message: str) -> None: user_id = user.get("id", "") user_name = user.get("name", "") logger.info( - f"Satori 连接成功 - Bot {i + 1}: platform={platform}, user_id={user_id}, user_name={user_name}", + t("msg-7db44899", res=i + 1, platform=platform, user_id=user_id, user_name=user_name), ) if "sn" in body: @@ -271,9 +272,9 @@ async def handle_message(self, message: str) -> None: self.sequence = body["sn"] except json.JSONDecodeError as e: - logger.error(f"解析 WebSocket 消息失败: {e}, 消息内容: {message}") + logger.error(t("msg-01564612", e=e, message=message)) except Exception as e: - logger.error(f"处理 WebSocket 消息异常: {e}") + logger.error(t("msg-3a1657ea", e=e)) async def handle_event(self, event_data: dict) -> None: try: @@ -305,7 +306,7 @@ async def handle_event(self, event_data: dict) -> None: await self.handle_msg(abm) except Exception as e: - logger.error(f"处理事件失败: {e}") + logger.error(t("msg-dc6b459c", e=e)) async def convert_satori_message( self, @@ -358,7 +359,7 @@ async def convert_satori_message( quote = quote_info["quote"] content_for_parsing = quote_info["content_without_quote"] except Exception as e: - logger.error(f"解析标签时发生错误: {e}, 错误内容: {content}") + logger.error(t("msg-6524f582", e=e, content=content)) if quote: # 引用消息 @@ -400,7 +401,7 @@ async def convert_satori_message( return abm except Exception as e: - logger.error(f"转换 Satori 消息失败: {e}") + logger.error(t("msg-3be535c3", e=e)) return None def _extract_namespace_prefixes(self, content: str) -> set: @@ -520,10 +521,10 @@ async def _extract_quote_element(self, content: str) -> dict | None: return None except ET.ParseError as e: - logger.warning(f"XML解析失败,使用正则提取: {e}") + logger.warning(t("msg-be17caf1", e=e)) return await self._extract_quote_with_regex(content) except Exception as e: - logger.error(f"提取标签时发生错误: {e}") + logger.error(t("msg-f6f41d74", e=e)) return None async def _extract_quote_with_regex(self, content: str) -> dict | None: @@ -586,7 +587,7 @@ async def _convert_quote_message(self, quote: dict) -> AstrBotMessage | None: return quote_abm except Exception as e: - logger.error(f"转换引用消息失败: {e}") + logger.error(t("msg-ca6dca7f", e=e)) return None async def parse_satori_elements(self, content: str) -> list: @@ -620,12 +621,12 @@ async def parse_satori_elements(self, content: str) -> list: root = ET.fromstring(processed_content) await self._parse_xml_node(root, elements) except ET.ParseError as e: - logger.warning(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}") + logger.warning(t("msg-cd3b067e", e=e, content=content)) # 如果解析失败,将整个内容当作纯文本 if content.strip(): elements.append(Plain(text=content)) except Exception as e: - logger.error(f"解析 Satori 元素时发生未知错误: {e}") + logger.error(t("msg-03071274", e=e)) raise e # 如果没有解析到任何元素,将整个内容当作纯文本 @@ -741,7 +742,7 @@ async def send_http_request( user_id: str | None = None, ) -> dict: if not self.session: - raise Exception("HTTP session 未初始化") + raise Exception(t("msg-775cd5c0")) headers = { "Content-Type": "application/json", @@ -777,7 +778,7 @@ async def send_http_request( return result return {} except Exception as e: - logger.error(f"Satori HTTP 请求异常: {e}") + logger.error(t("msg-e354c8d1", e=e)) return {} async def terminate(self) -> None: @@ -790,7 +791,7 @@ async def terminate(self) -> None: try: await self.ws.close() except Exception as e: - logger.error(f"Satori WebSocket 关闭异常: {e}") + logger.error(t("msg-5c04bfcd", e=e)) if self.session: await self.session.close() diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 0214222837..c24f65d592 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -1,3 +1,4 @@ +from astrbot.core.lang import t from typing import TYPE_CHECKING from astrbot.api import logger @@ -107,7 +108,7 @@ async def send_with_adapter( return None except Exception as e: - logger.error(f"Satori 消息发送异常: {e}") + logger.error(t("msg-c063ab8a", e=e)) return None async def send(self, message: MessageChain) -> None: @@ -154,9 +155,9 @@ async def send(self, message: MessageChain) -> None: user_id, ) if not result: - logger.error("Satori 消息发送失败") + logger.error(t("msg-9bc42a8d")) except Exception as e: - logger.error(f"Satori 消息发送异常: {e}") + logger.error(t("msg-c063ab8a", e=e)) await super().send(message) @@ -195,7 +196,7 @@ async def send_streaming(self, generator, use_fallback: bool = False): ) await self.send(img_chain) except Exception as e: - logger.error(f"图片转换为base64失败: {e}") + logger.error(t("msg-dbf77ca2", e=e)) else: content_parts.append(str(component)) @@ -205,7 +206,7 @@ async def send_streaming(self, generator, use_fallback: bool = False): await self.send(temp_chain) except Exception as e: - logger.error(f"Satori 流式消息发送异常: {e}") + logger.error(t("msg-8b6100fb", e=e)) return await super().send_streaming(generator, use_fallback) @@ -232,7 +233,7 @@ async def _convert_component_to_satori(self, component) -> str: if image_base64: return f'' except Exception as e: - logger.error(f"图片转换为base64失败: {e}") + logger.error(t("msg-dbf77ca2", e=e)) elif isinstance(component, File): return ( @@ -245,7 +246,7 @@ async def _convert_component_to_satori(self, component) -> str: if record_base64: return f'