From 92bae1fdaede63726757b8936ec96b8c429e605c Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Thu, 19 Mar 2026 23:30:25 +0800 Subject: [PATCH] docs: replace prints with logger in upload_doc_images_to_r2.py --- astrbot/__main__.py | 20 +- astrbot/api/all.py | 2 +- astrbot/api/star/__init__.py | 2 +- .../builtin_stars/astrbot/long_term_memory.py | 6 +- astrbot/builtin_stars/astrbot/main.py | 8 +- .../builtin_commands/commands/admin.py | 30 +- .../builtin_commands/commands/alter_cmd.py | 12 +- .../builtin_commands/commands/conversation.py | 54 +- .../builtin_commands/commands/help.py | 2 +- .../builtin_commands/commands/llm.py | 2 +- .../builtin_commands/commands/persona.py | 30 +- .../builtin_commands/commands/plugin.py | 42 +- .../builtin_commands/commands/provider.py | 64 +-- .../builtin_commands/commands/setunset.py | 6 +- .../builtin_commands/commands/sid.py | 14 +- .../builtin_commands/commands/t2i.py | 4 +- .../builtin_commands/commands/tts.py | 6 +- .../builtin_stars/builtin_commands/main.py | 12 +- .../builtin_stars/session_controller/main.py | 12 +- .../web_searcher/engines/__init__.py | 2 +- astrbot/builtin_stars/web_searcher/main.py | 28 +- astrbot/cli/commands/cmd_init.py | 4 +- astrbot/core/agent/context/token_counter.py | 6 +- astrbot/core/agent/context/truncator.py | 26 +- .../agent/runners/coze/coze_agent_runner.py | 22 +- .../agent/runners/coze/coze_api_client.py | 16 +- .../dashscope/dashscope_agent_runner.py | 18 +- .../agent/runners/dify/dify_agent_runner.py | 36 +- .../agent/runners/dify/dify_api_client.py | 8 +- .../agent/runners/tool_loop_agent_runner.py | 26 +- astrbot/core/astr_agent_run_util.py | 28 +- astrbot/core/astr_agent_tool_exec.py | 12 +- astrbot/core/astr_main_agent.py | 30 +- astrbot/core/astr_main_agent_resources.py | 10 +- astrbot/core/astrbot_config_mgr.py | 4 +- astrbot/core/backup/__init__.py | 2 +- astrbot/core/backup/constants.py | 6 +- astrbot/core/backup/exporter.py | 34 +- astrbot/core/backup/importer.py | 86 +-- astrbot/core/computer/booters/bay_manager.py | 2 +- astrbot/core/computer/booters/shipyard_neo.py | 4 +- astrbot/core/computer/computer_client.py | 2 +- astrbot/core/config/astrbot_config.py | 32 +- astrbot/core/config/default.py | 534 +++++++++--------- astrbot/core/config/i18n_utils.py | 10 +- astrbot/core/conversation_mgr.py | 50 +- astrbot/core/core_lifecycle.py | 22 +- astrbot/core/db/migration/helper.py | 10 +- astrbot/core/db/migration/migra_3_to_4.py | 34 +- astrbot/core/db/migration/migra_45_to_46.py | 2 +- .../core/db/migration/migra_token_usage.py | 6 +- .../db/migration/migra_webchat_session.py | 14 +- astrbot/core/db/migration/sqlite_v3.py | 20 +- astrbot/core/db/po.py | 30 +- astrbot/core/db/sqlite.py | 18 +- astrbot/core/db/vec_db/base.py | 10 +- .../db/vec_db/faiss_impl/embedding_storage.py | 2 +- astrbot/core/db/vec_db/faiss_impl/vec_db.py | 12 +- astrbot/core/file_token_service.py | 12 +- astrbot/core/initial_loader.py | 6 +- astrbot/core/knowledge_base/chunking/base.py | 4 +- .../knowledge_base/chunking/fixed_size.py | 4 +- .../core/knowledge_base/chunking/recursive.py | 12 +- astrbot/core/knowledge_base/kb_db_sqlite.py | 2 +- astrbot/core/knowledge_base/kb_helper.py | 44 +- astrbot/core/knowledge_base/models.py | 6 +- astrbot/core/knowledge_base/parsers/base.py | 8 +- .../core/knowledge_base/parsers/pdf_parser.py | 4 +- .../knowledge_base/parsers/text_parser.py | 6 +- .../core/knowledge_base/parsers/url_parser.py | 16 +- .../core/knowledge_base/retrieval/manager.py | 6 +- astrbot/core/log.py | 10 +- astrbot/core/message/components.py | 66 +-- astrbot/core/message/message_event_result.py | 66 +-- astrbot/core/persona_mgr.py | 36 +- .../pipeline/content_safety_check/stage.py | 6 +- .../strategies/baidu_aip.py | 8 +- .../strategies/keywords.py | 2 +- astrbot/core/pipeline/context.py | 2 +- astrbot/core/pipeline/context_utils.py | 14 +- .../core/pipeline/preprocess_stage/stage.py | 6 +- .../process_stage/method/agent_request.py | 2 +- .../method/agent_sub_stages/internal.py | 10 +- .../method/agent_sub_stages/third_party.py | 6 +- .../process_stage/method/star_request.py | 2 +- .../core/pipeline/rate_limit_check/stage.py | 30 +- astrbot/core/pipeline/respond/stage.py | 16 +- .../core/pipeline/result_decorate/stage.py | 30 +- astrbot/core/pipeline/scheduler.py | 10 +- .../pipeline/session_status_check/stage.py | 2 +- astrbot/core/pipeline/stage.py | 6 +- astrbot/core/pipeline/stage_order.py | 4 +- astrbot/core/pipeline/waking_check/stage.py | 16 +- .../core/pipeline/whitelist_check/stage.py | 4 +- astrbot/core/platform/astr_message_event.py | 130 ++--- astrbot/core/platform/astrbot_message.py | 4 +- astrbot/core/platform/manager.py | 16 +- astrbot/core/platform/message_session.py | 6 +- astrbot/core/platform/message_type.py | 4 +- astrbot/core/platform/platform.py | 24 +- astrbot/core/platform/platform_metadata.py | 16 +- astrbot/core/platform/register.py | 16 +- .../aiocqhttp/aiocqhttp_message_event.py | 24 +- .../aiocqhttp/aiocqhttp_platform_adapter.py | 36 +- .../sources/dingtalk/dingtalk_adapter.py | 20 +- .../sources/dingtalk/dingtalk_event.py | 2 +- .../core/platform/sources/discord/client.py | 6 +- .../platform/sources/discord/components.py | 2 +- .../discord/discord_platform_adapter.py | 54 +- .../sources/discord/discord_platform_event.py | 16 +- .../platform/sources/kook/kook_adapter.py | 18 +- .../core/platform/sources/kook/kook_client.py | 22 +- .../core/platform/sources/kook/kook_config.py | 22 +- .../core/platform/sources/kook/kook_event.py | 2 +- .../core/platform/sources/kook/kook_types.py | 24 +- .../platform/sources/lark/lark_adapter.py | 10 +- .../core/platform/sources/lark/lark_event.py | 106 ++-- astrbot/core/platform/sources/lark/server.py | 10 +- .../platform/sources/line/line_adapter.py | 12 +- .../core/platform/sources/line/line_event.py | 4 +- .../sources/misskey/misskey_adapter.py | 26 +- .../platform/sources/misskey/misskey_api.py | 36 +- .../platform/sources/misskey/misskey_event.py | 12 +- .../platform/sources/misskey/misskey_utils.py | 42 +- .../qqofficial/qqofficial_message_event.py | 48 +- .../qqofficial_webhook/qo_webhook_adapter.py | 4 +- .../qqofficial_webhook/qo_webhook_server.py | 6 +- .../platform/sources/satori/satori_adapter.py | 16 +- .../platform/sources/satori/satori_event.py | 8 +- astrbot/core/platform/sources/slack/client.py | 6 +- .../platform/sources/slack/slack_adapter.py | 12 +- .../platform/sources/slack/slack_event.py | 10 +- .../platform/sources/telegram/tg_adapter.py | 14 +- .../platform/sources/telegram/tg_event.py | 78 +-- .../sources/webchat/webchat_adapter.py | 4 +- .../platform/sources/webchat/webchat_event.py | 4 +- .../platform/sources/wecom/wecom_adapter.py | 28 +- .../platform/sources/wecom/wecom_event.py | 14 +- .../core/platform/sources/wecom/wecom_kf.py | 54 +- .../sources/wecom/wecom_kf_message.py | 10 +- .../sources/wecom_ai_bot/WXBizJsonMsgCrypt.py | 48 +- .../sources/wecom_ai_bot/wecomai_adapter.py | 44 +- .../sources/wecom_ai_bot/wecomai_api.py | 30 +- .../sources/wecom_ai_bot/wecomai_event.py | 8 +- .../wecom_ai_bot/wecomai_long_connection.py | 14 +- .../sources/wecom_ai_bot/wecomai_queue_mgr.py | 12 +- .../sources/wecom_ai_bot/wecomai_server.py | 12 +- .../sources/wecom_ai_bot/wecomai_utils.py | 14 +- .../sources/wecom_ai_bot/wecomai_webhook.py | 10 +- .../weixin_offacc_adapter.py | 48 +- .../weixin_offacc_event.py | 10 +- astrbot/core/provider/entities.py | 24 +- astrbot/core/provider/func_tool_manager.py | 46 +- astrbot/core/provider/manager.py | 72 +-- astrbot/core/provider/provider.py | 54 +- astrbot/core/provider/register.py | 2 +- .../core/provider/sources/anthropic_source.py | 26 +- .../core/provider/sources/azure_tts_source.py | 6 +- .../provider/sources/bailian_rerank_source.py | 22 +- .../core/provider/sources/edge_tts_source.py | 6 +- .../sources/fishaudio_tts_api_source.py | 10 +- .../core/provider/sources/gemini_source.py | 64 +-- .../provider/sources/gsv_selfhosted_source.py | 38 +- .../core/provider/sources/gsvi_tts_source.py | 2 +- .../core/provider/sources/openai_source.py | 46 +- .../sources/sensevoice_selfhosted_source.py | 8 +- .../provider/sources/vllm_rerank_source.py | 2 +- .../sources/whisper_selfhosted_source.py | 4 +- astrbot/core/provider/sources/xai_source.py | 2 +- .../sources/xinference_stt_provider.py | 2 +- astrbot/core/star/base.py | 6 +- astrbot/core/star/command_management.py | 32 +- astrbot/core/star/config.py | 44 +- astrbot/core/star/context.py | 192 +++---- astrbot/core/star/error_messages.py | 8 +- astrbot/core/star/filter/command.py | 24 +- astrbot/core/star/filter/command_group.py | 10 +- astrbot/core/star/filter/permission.py | 4 +- astrbot/core/star/filter/regex.py | 2 +- astrbot/core/star/register/star.py | 22 +- astrbot/core/star/register/star_handler.py | 86 +-- astrbot/core/star/session_llm_manager.py | 30 +- astrbot/core/star/session_plugin_manager.py | 14 +- astrbot/core/star/star.py | 10 +- astrbot/core/star/star_handler.py | 20 +- astrbot/core/star/star_manager.py | 214 +++---- astrbot/core/star/star_tools.py | 34 +- astrbot/core/star/updator.py | 10 +- astrbot/core/tools/kb_query.py | 6 +- astrbot/core/umop_config_router.py | 8 +- astrbot/core/updator.py | 22 +- astrbot/core/utils/active_event_registry.py | 18 +- astrbot/core/utils/astrbot_path.py | 22 +- astrbot/core/utils/io.py | 16 +- astrbot/core/utils/media_utils.py | 32 +- astrbot/core/utils/metrics.py | 2 +- astrbot/core/utils/network_utils.py | 6 +- astrbot/core/utils/path_util.py | 14 +- astrbot/core/utils/pip_installer.py | 20 +- astrbot/core/utils/requirements_utils.py | 14 +- astrbot/core/utils/session_waiter.py | 12 +- astrbot/core/utils/shared_preferences.py | 16 +- astrbot/core/utils/t2i/local_strategy.py | 46 +- astrbot/core/utils/t2i/network_strategy.py | 2 +- astrbot/core/utils/t2i/renderer.py | 14 +- astrbot/core/utils/t2i/template_manager.py | 44 +- astrbot/core/utils/tencent_record_helper.py | 12 +- astrbot/core/utils/version_comparator.py | 4 +- astrbot/core/utils/webhook_utils.py | 2 +- astrbot/core/zip_updator.py | 30 +- astrbot/dashboard/routes/auth.py | 14 +- astrbot/dashboard/routes/backup.py | 132 ++--- astrbot/dashboard/routes/chat.py | 28 +- astrbot/dashboard/routes/command.py | 6 +- astrbot/dashboard/routes/config.py | 68 +-- astrbot/dashboard/routes/conversation.py | 6 +- astrbot/dashboard/routes/knowledge_base.py | 32 +- astrbot/dashboard/routes/live_chat.py | 34 +- astrbot/dashboard/routes/log.py | 4 +- astrbot/dashboard/routes/persona.py | 6 +- astrbot/dashboard/routes/platform.py | 6 +- astrbot/dashboard/routes/plugin.py | 62 +- .../dashboard/routes/session_management.py | 52 +- astrbot/dashboard/routes/stat.py | 10 +- astrbot/dashboard/routes/static_file.py | 2 +- astrbot/dashboard/routes/t2i.py | 16 +- astrbot/dashboard/routes/tools.py | 4 +- astrbot/dashboard/routes/update.py | 20 +- astrbot/dashboard/routes/util.py | 26 +- astrbot/dashboard/server.py | 4 +- astrbot/dashboard/utils.py | 4 +- docs/scripts/sync_docs_to_wiki.py | 4 +- docs/scripts/upload_doc_images_to_r2.py | 40 +- scripts/fw2hw.sh | 1 + scripts/hatch_build.py | 2 +- tests/agent/test_context_manager.py | 6 +- tests/agent/test_token_counter.py | 6 +- tests/conftest.py | 38 +- tests/fixtures/__init__.py | 6 +- tests/fixtures/helpers.py | 78 +-- tests/fixtures/mocks/__init__.py | 4 +- tests/fixtures/mocks/aiocqhttp.py | 14 +- tests/fixtures/mocks/discord.py | 14 +- tests/fixtures/mocks/telegram.py | 18 +- tests/fixtures/plugins/fixture_plugin.py | 10 +- tests/test_backup.py | 26 +- tests/test_booter_decoupling.py | 2 +- tests/test_dashboard.py | 12 +- tests/test_pip_installer.py | 4 +- tests/test_profile_aware_tools.py | 14 +- tests/test_skill_metadata_enrichment.py | 16 +- tests/test_tool_loop_agent_runner.py | 22 +- tests/unit/test_astr_main_agent.py | 4 +- 253 files changed, 2880 insertions(+), 2875 deletions(-) mode change 100755 => 100644 astrbot/core/star/filter/command.py mode change 100755 => 100644 astrbot/core/star/filter/command_group.py mode change 100755 => 100644 docs/scripts/upload_doc_images_to_r2.py create mode 100644 scripts/fw2hw.sh diff --git a/astrbot/__main__.py b/astrbot/__main__.py index 743a1a033..6e62f12dd 100644 --- a/astrbot/__main__.py +++ b/astrbot/__main__.py @@ -45,7 +45,7 @@ logo_tmpl = r""" def check_env() -> None: if not (sys.version_info.major == 3 and sys.version_info.minor >= 10): - logger.error("请使用 Python3.10+ 运行本项目。") + logger.error("请使用 Python3.10+ 运行本项目。") exit() astrbot_root = get_astrbot_root() @@ -76,7 +76,7 @@ async def check_dashboard_files(webui_dir: str | None = None): if await anyio.Path(webui_dir).exists(): logger.info(f"使用指定的 WebUI 目录: {webui_dir}") return webui_dir - logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。") + logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。") data_dist_path = os.path.join(get_astrbot_data_path(), "dist") if await anyio.Path(data_dist_path).exists(): @@ -84,30 +84,30 @@ async def check_dashboard_files(webui_dir: str | None = None): if v is not None: # 存在文件 if v == f"v{VERSION}": - logger.info("WebUI 版本已是最新。") + logger.info("WebUI 版本已是最新。") else: logger.warning( - f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。", + f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。", ) return data_dist_path logger.info( - "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。", + "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。", ) try: await download_dashboard(version=f"v{VERSION}", latest=False) except Exception as e: logger.warning( - f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。" + f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。" ) try: await download_dashboard(latest=True) except Exception as e: - logger.critical(f"下载管理面板文件失败: {e}。") + logger.critical(f"下载管理面板文件失败: {e}。") return None - logger.info("管理面板下载完成。") + logger.info("管理面板下载完成。") return data_dist_path @@ -117,8 +117,8 @@ async def main_async(webui_dir_arg: str | None) -> None: webui_dir = await check_dashboard_files(webui_dir_arg) if webui_dir is None: logger.warning( - "管理面板文件检查失败,WebUI 功能将不可用。" - "请检查网络连接或手动指定 --webui-dir 参数。" + "管理面板文件检查失败,WebUI 功能将不可用。" + "请检查网络连接或手动指定 --webui-dir 参数。" ) db = db_helper diff --git a/astrbot/api/all.py b/astrbot/api/all.py index df3e1170f..7c5f9c061 100644 --- a/astrbot/api/all.py +++ b/astrbot/api/all.py @@ -29,7 +29,7 @@ from astrbot.core.star.filter.platform_adapter_type import ( PlatformAdapterType, ) from astrbot.core.star.register import ( - register_star as register, # 注册插件(Star) + register_star as register, # 注册插件(Star) ) from astrbot.core.star import Context, Star from astrbot.core.star.config import * diff --git a/astrbot/api/star/__init__.py b/astrbot/api/star/__init__.py index 63db07a72..9d2dced55 100644 --- a/astrbot/api/star/__init__.py +++ b/astrbot/api/star/__init__.py @@ -1,7 +1,7 @@ from astrbot.core.star import Context, Star, StarTools from astrbot.core.star.config import * from astrbot.core.star.register import ( - register_star as register, # 注册插件(Star) + register_star as register, # 注册插件(Star) ) __all__ = ["Context", "Star", "StarTools", "register"] diff --git a/astrbot/builtin_stars/astrbot/long_term_memory.py b/astrbot/builtin_stars/astrbot/long_term_memory.py index e08cdc515..e271bc741 100644 --- a/astrbot/builtin_stars/astrbot/long_term_memory.py +++ b/astrbot/builtin_stars/astrbot/long_term_memory.py @@ -76,7 +76,7 @@ class LongTermMemory: if not provider: raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商") if not isinstance(provider, Provider): - raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述") + raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述") response = await provider.text_chat( prompt=image_caption_prompt, session_id=uuid.uuid4().hex, @@ -149,7 +149,7 @@ class LongTermMemory: self.session_chats[event.unified_msg_origin].pop(0) async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> None: - """当触发 LLM 请求前,调用此方法修改 req""" + """当触发 LLM 请求前,调用此方法修改 req""" if event.unified_msg_origin not in self.session_chats: return @@ -164,7 +164,7 @@ class LongTermMemory: "Please react to it. Only output your response and do not output any other information. " "You MUST use the SAME language as the chatroom is using." ) - req.contexts = [] # 清空上下文,当使用了主动回复,所有聊天记录都在一个prompt中。 + req.contexts = [] # 清空上下文,当使用了主动回复,所有聊天记录都在一个prompt中。 else: req.system_prompt += ( "You are now in a chatroom. The chat history is as follows: \n" diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index da2a00835..50b3d0686 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -50,7 +50,7 @@ class Main(star.Star): """主动回复""" provider = self.context.get_using_provider(event.unified_msg_origin) if not provider: - logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复") + logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复") return try: conv = None @@ -60,7 +60,7 @@ class Main(star.Star): if not session_curr_cid: logger.error( - "当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。", + "当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。", ) return @@ -72,7 +72,7 @@ class Main(star.Star): prompt = event.message_str if not conv: - logger.error("未找到对话,无法主动回复") + logger.error("未找到对话,无法主动回复") return yield event.request_llm( @@ -88,7 +88,7 @@ class Main(star.Star): async def decorate_llm_req( self, event: AstrMessageEvent, req: ProviderRequest ) -> None: - """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt""" + """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt""" if self.ltm and self.ltm_enabled(event): try: await self.ltm.on_req_llm(event, req) diff --git a/astrbot/builtin_stars/builtin_commands/commands/admin.py b/astrbot/builtin_stars/builtin_commands/commands/admin.py index a4f46b603..0294c8cd8 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/admin.py +++ b/astrbot/builtin_stars/builtin_commands/commands/admin.py @@ -9,56 +9,56 @@ class AdminCommands: self.context = context async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """授权管理员。op """ + """授权管理员。op """ if not admin_id: event.set_result( MessageEventResult().message( - "使用方法: /op 授权管理员;/deop 取消管理员。可通过 /sid 获取 ID。", + "使用方法: /op 授权管理员;/deop 取消管理员。可通过 /sid 获取 ID。", ), ) 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("授权成功。")) async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """取消授权管理员。deop """ + """取消授权管理员。deop """ if not admin_id: event.set_result( MessageEventResult().message( - "使用方法: /deop 取消管理员。可通过 /sid 获取 ID。", + "使用方法: /deop 取消管理员。可通过 /sid 获取 ID。", ), ) 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("取消授权成功。")) except ValueError: event.set_result( - MessageEventResult().message("此用户 ID 不在管理员名单内。"), + MessageEventResult().message("此用户 ID 不在管理员名单内。"), ) async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: - """添加白名单。wl """ + """添加白名单。wl """ if not sid: event.set_result( MessageEventResult().message( - "使用方法: /wl 添加白名单;/dwl 删除白名单。可通过 /sid 获取 ID。", + "使用方法: /wl 添加白名单;/dwl 删除白名单。可通过 /sid 获取 ID。", ), ) 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("添加白名单成功。")) async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None: - """删除白名单。dwl """ + """删除白名单。dwl """ if not sid: event.set_result( MessageEventResult().message( - "使用方法: /dwl 删除白名单。可通过 /sid 获取 ID。", + "使用方法: /dwl 删除白名单。可通过 /sid 获取 ID。", ), ) return @@ -66,12 +66,12 @@ class AdminCommands: 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("删除白名单成功。")) except ValueError: - event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) + event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) async def update_dashboard(self, event: AstrMessageEvent) -> None: """更新管理面板""" await event.send(MessageChain().message("正在尝试更新管理面板...")) await download_dashboard(version=f"v{VERSION}", latest=False) - await event.send(MessageChain().message("管理面板更新完成。")) + await event.send(MessageChain().message("管理面板更新完成。")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py b/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py index ba31c3326..1deed7e36 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +++ b/astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py @@ -31,7 +31,7 @@ class AlterCmdCommands(CommandParserMixin): if token.len < 3: await event.send( MessageChain().message( - "该指令用于设置指令或指令组的权限。\n" + "该指令用于设置指令或指令组的权限。\n" "格式: /alter_cmd \n" "例1: /alter_cmd c1 admin 将 c1 设为管理员指令\n" "例2: /alter_cmd g1 c1 admin 将 g1 指令组的 c1 子指令设为管理员指令\n" @@ -56,11 +56,11 @@ class AlterCmdCommands(CommandParserMixin): private = reset_cfg.get("private", "member") config_menu = f"""reset命令权限细粒度配置 - 当前配置: + 当前配置: 1. 群聊+会话隔离开: {group_unique_on} 2. 群聊+会话隔离关: {group_unique_off} 3. 私聊: {private} - 修改指令格式: + 修改指令格式: /alter_cmd reset scene <场景编号> 例如: /alter_cmd reset scene 2 member""" await event.send(MessageChain().message(config_menu)) @@ -82,7 +82,7 @@ class AlterCmdCommands(CommandParserMixin): if perm_type not in ["admin", "member"]: await event.send( - MessageChain().message("权限类型错误,只能是 admin 或 member"), + MessageChain().message("权限类型错误,只能是 admin 或 member"), ) return @@ -101,7 +101,7 @@ class AlterCmdCommands(CommandParserMixin): if cmd_type not in ["admin", "member"]: await event.send( - MessageChain().message("指令类型错误,可选类型有 admin, member"), + MessageChain().message("指令类型错误,可选类型有 admin, member"), ) return @@ -168,6 +168,6 @@ class AlterCmdCommands(CommandParserMixin): cmd_group_str = "指令组" if cmd_group else "指令" await event.send( MessageChain().message( - f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。", + f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。", ), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 5190a363e..a715ca3b6 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -60,8 +60,8 @@ class ConversationCommands: if required_perm == "admin" and message.role != "admin": message.set_result( MessageEventResult().message( - f"在{scene.name}场景下,reset命令需要管理员权限," - f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。", + f"在{scene.name}场景下,reset命令需要管理员权限," + f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。", ), ) return @@ -74,12 +74,12 @@ class ConversationCommands: scope_id=umo, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("重置对话成功。")) + message.set_result(MessageEventResult().message("重置对话成功。")) return if not self.context.get_using_provider(umo): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), ) return @@ -88,7 +88,7 @@ class ConversationCommands: if not cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 切换或者 /new 创建。", + "当前未处于对话状态,请 /switch 切换或者 /new 创建。", ), ) return @@ -101,7 +101,7 @@ class ConversationCommands: [], ) - ret = "清除聊天历史成功!" + ret = "清除聊天历史成功!" message.set_extra("_clean_ltm_session", True) @@ -124,18 +124,18 @@ class ConversationCommands: if stopped_count > 0: message.set_result( MessageEventResult().message( - f"已请求停止 {stopped_count} 个运行中的任务。" + f"已请求停止 {stopped_count} 个运行中的任务。" ) ) return - message.set_result(MessageEventResult().message("当前会话没有运行中的任务。")) + message.set_result(MessageEventResult().message("当前会话没有运行中的任务。")) 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("未找到任何 LLM 提供商。请先配置。"), ) return @@ -166,7 +166,7 @@ class ConversationCommands: history = "".join(parts) ret = ( - f"当前对话历史记录:" + f"当前对话历史记录:" f"{history or '无历史记录'}\n\n" f"第 {page} 页 | 共 {total_pages} 页\n" f"*输入 /history 2 跳转到第 2 页" @@ -181,7 +181,7 @@ class ConversationCommands: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: message.set_result( MessageEventResult().message( - f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。", + f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。", ), ) return @@ -200,7 +200,7 @@ class ConversationCommands: end_idx = start_idx + size_per_page conversations_paged = conversations_all[start_idx:end_idx] - parts = ["对话列表:\n---\n"] + parts = ["对话列表:\n---\n"] """全局序号从当前页的第一个开始""" global_index = start_idx + 1 @@ -277,7 +277,7 @@ class ConversationCommands: 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("已创建新对话。")) return active_event_registry.stop_all(message.unified_msg_origin, exclude=message) @@ -291,7 +291,7 @@ class ConversationCommands: message.set_extra("_clean_ltm_session", True) message.set_result( - MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), + MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), ) async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: @@ -313,12 +313,12 @@ class ConversationCommands: ) message.set_result( MessageEventResult().message( - f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。", + f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。", ), ) else: message.set_result( - MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"), + MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"), ) async def switch_conv( @@ -329,14 +329,14 @@ class ConversationCommands: """通过 /ls 前面的序号切换对话""" if not isinstance(index, int): message.set_result( - MessageEventResult().message("类型错误,请输入数字对话序号。"), + MessageEventResult().message("类型错误,请输入数字对话序号。"), ) return if index is None: message.set_result( MessageEventResult().message( - "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话", + "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话", ), ) return @@ -345,7 +345,7 @@ class ConversationCommands: ) if index > len(conversations) or index < 1: message.set_result( - MessageEventResult().message("对话序号错误,请使用 /ls 查看"), + MessageEventResult().message("对话序号错误,请使用 /ls 查看"), ) else: conversation = conversations[index - 1] @@ -356,20 +356,20 @@ class ConversationCommands: ) message.set_result( MessageEventResult().message( - f"切换到对话: {title}({conversation.cid[:4]})。", + f"切换到对话: {title}({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("请输入新的对话名称。")) 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("重命名对话成功。")) async def del_conv(self, message: AstrMessageEvent) -> None: """删除当前对话""" @@ -377,10 +377,10 @@ class ConversationCommands: cfg = self.context.get_config(umo=umo) is_unique_session = cfg["platform_settings"]["unique_session"] if message.get_group_id() and not is_unique_session and message.role != "admin": - # 群聊,没开独立会话,发送人不是管理员 + # 群聊,没开独立会话,发送人不是管理员 message.set_result( MessageEventResult().message( - f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。", + f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。", ), ) return @@ -393,7 +393,7 @@ class ConversationCommands: scope_id=umo, key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], ) - message.set_result(MessageEventResult().message("重置对话成功。")) + message.set_result(MessageEventResult().message("重置对话成功。")) return session_curr_cid = ( @@ -403,7 +403,7 @@ class ConversationCommands: if not session_curr_cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。", + "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。", ), ) return @@ -415,6 +415,6 @@ class ConversationCommands: session_curr_cid, ) - ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" + ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" message.set_extra("_clean_ltm_session", True) message.set_result(MessageEventResult().message(ret)) diff --git a/astrbot/builtin_stars/builtin_commands/commands/help.py b/astrbot/builtin_stars/builtin_commands/commands/help.py index ae2f4c787..b2b3283fc 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/help.py +++ b/astrbot/builtin_stars/builtin_commands/commands/help.py @@ -24,7 +24,7 @@ class HelpCommand: async def _build_reserved_command_lines(self) -> list[str]: """ - 使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。 + 使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。 """ try: commands = await command_management.list_commands() diff --git a/astrbot/builtin_stars/builtin_commands/commands/llm.py b/astrbot/builtin_stars/builtin_commands/commands/llm.py index ba9ba5c9b..6430c1040 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/llm.py +++ b/astrbot/builtin_stars/builtin_commands/commands/llm.py @@ -17,4 +17,4 @@ class LLMCommands: cfg["provider_settings"]["enable"] = True status = "开启" cfg.save_config() - await event.send(MessageChain().message(f"{status} LLM 聊天功能。")) + await event.send(MessageChain().message(f"{status} LLM 聊天功能。")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index 0464fffb5..eee2edfec 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -18,10 +18,10 @@ class PersonaCommands: all_personas: list["Persona"], depth: int = 0, ) -> list[str]: - """递归构建树状输出,使用短线条表示层级""" + """递归构建树状输出,使用短线条表示层级""" lines: list[str] = [] - # 使用短线条作为缩进前缀,每层只用 "│" 加一个空格 - prefix = "│ " * depth + # 使用短线条作为缩进前缀,每层只用 "│" 加一个空格 + prefix = "│ " * depth for folder in folder_tree: # 输出文件夹 @@ -31,7 +31,7 @@ class PersonaCommands: folder_personas = [ p for p in all_personas if p.folder_id == folder["folder_id"] ] - child_prefix = "│ " * (depth + 1) + child_prefix = "│ " * (depth + 1) # 输出该文件夹下的人格 for persona in folder_personas: @@ -71,7 +71,7 @@ class PersonaCommands: if conv is None: message.set_result( MessageEventResult().message( - "当前对话不存在,请先使用 /new 新建一个对话。", + "当前对话不存在,请先使用 /new 新建一个对话。", ), ) return @@ -127,16 +127,16 @@ class PersonaCommands: folder_tree = await self.context.persona_manager.get_folder_tree() all_personas = self.context.persona_manager.personas - lines = ["📂 人格列表:\n"] + lines = ["📂 人格列表:\n"] # 构建树状输出 tree_lines = self._build_tree_output(folder_tree, all_personas) lines.extend(tree_lines) - # 输出根目录下的人格(没有文件夹的) + # 输出根目录下的人格(没有文件夹的) root_personas = [p for p in all_personas if p.folder_id is None] if root_personas: - if tree_lines: # 如果有文件夹内容,加个空行 + if tree_lines: # 如果有文件夹内容,加个空行 lines.append("") for persona in root_personas: lines.append(f"👤 {persona.persona_id}") @@ -161,7 +161,7 @@ class PersonaCommands: ), None, ): - msg = f"人格{ps}的详细信息:\n" + msg = f"人格{ps}的详细信息:\n" msg += f"{persona['prompt']}\n" else: msg = f"人格{ps}不存在" @@ -169,20 +169,20 @@ class PersonaCommands: elif parts[1] == "unset": if not cid: message.set_result( - MessageEventResult().message("当前没有对话,无法取消人格。"), + MessageEventResult().message("当前没有对话,无法取消人格。"), ) 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("取消人格成功。")) else: ps = "".join(parts[1:]).strip() if not cid: message.set_result( MessageEventResult().message( - "当前没有对话,请先开始对话或使用 /new 创建一个对话。", + "当前没有对话,请先开始对话或使用 /new 创建一个对话。", ), ) return @@ -200,17 +200,17 @@ class PersonaCommands: force_warn_msg = "" if force_applied_persona_id: force_warn_msg = ( - "提醒:由于自定义规则,您现在切换的人格将不会生效。" + "提醒:由于自定义规则,您现在切换的人格将不会生效。" ) message.set_result( MessageEventResult().message( - f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}", + f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}", ), ) else: message.set_result( MessageEventResult().message( - "不存在该人格情景。使用 /persona list 查看所有。", + "不存在该人格情景。使用 /persona list 查看所有。", ), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/plugin.py b/astrbot/builtin_stars/builtin_commands/commands/plugin.py index a145372d7..323772de8 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/plugin.py +++ b/astrbot/builtin_stars/builtin_commands/commands/plugin.py @@ -11,8 +11,8 @@ class PluginCommands: self.context = context async def plugin_ls(self, event: AstrMessageEvent) -> None: - """获取已经安装的插件列表。""" - parts = ["已加载的插件:\n"] + """获取已经安装的插件列表。""" + parts = ["已加载的插件:\n"] for plugin in self.context.get_all_stars(): line = f"- `{plugin.name}` By {plugin.author}: {plugin.desc}" if not plugin.activated: @@ -20,11 +20,11 @@ class PluginCommands: parts.append(line + "\n") if len(parts) == 1: - plugin_list_info = "没有加载任何插件。" + plugin_list_info = "没有加载任何插件。" else: plugin_list_info = "".join(parts) - plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。" + plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。" event.set_result( MessageEventResult().message(f"{plugin_list_info}").use_t2i(False), ) @@ -32,51 +32,51 @@ class PluginCommands: async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: """禁用插件""" if DEMO_MODE: - event.set_result(MessageEventResult().message("演示模式下无法禁用插件。")) + event.set_result(MessageEventResult().message("演示模式下无法禁用插件。")) return if not plugin_name: event.set_result( - MessageEventResult().message("/plugin off <插件名> 禁用插件。"), + MessageEventResult().message("/plugin off <插件名> 禁用插件。"), ) return if self.context._star_manager is None: - event.set_result(MessageEventResult().message("插件管理器未初始化。")) + event.set_result(MessageEventResult().message("插件管理器未初始化。")) return await self.context._star_manager.turn_off_plugin(plugin_name) - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。")) + event.set_result(MessageEventResult().message(f"插件 {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("演示模式下无法启用插件。")) return if not plugin_name: event.set_result( - MessageEventResult().message("/plugin on <插件名> 启用插件。"), + MessageEventResult().message("/plugin on <插件名> 启用插件。"), ) return if self.context._star_manager is None: - event.set_result(MessageEventResult().message("插件管理器未初始化。")) + event.set_result(MessageEventResult().message("插件管理器未初始化。")) return await self.context._star_manager.turn_on_plugin(plugin_name) - event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。")) + event.set_result(MessageEventResult().message(f"插件 {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("演示模式下无法安装插件。")) return if not plugin_repo: event.set_result( MessageEventResult().message("/plugin get <插件仓库地址> 安装插件"), ) return - logger.info(f"准备从 {plugin_repo} 安装插件。") + logger.info(f"准备从 {plugin_repo} 安装插件。") if self.context._star_manager: star_mgr = self.context._star_manager try: await star_mgr.install_plugin(plugin_repo) - event.set_result(MessageEventResult().message("安装插件成功。")) + event.set_result(MessageEventResult().message("安装插件成功。")) except Exception as e: logger.error(f"安装插件失败: {e}") event.set_result(MessageEventResult().message(f"安装插件失败: {e}")) @@ -86,12 +86,12 @@ class PluginCommands: """获取插件帮助""" if not plugin_name: event.set_result( - MessageEventResult().message("/plugin help <插件名> 查看插件信息。"), + MessageEventResult().message("/plugin help <插件名> 查看插件信息。"), ) return plugin = self.context.get_registered_star(plugin_name) if plugin is None: - event.set_result(MessageEventResult().message("未找到此插件。")) + event.set_result(MessageEventResult().message("未找到此插件。")) return help_msg = "" help_msg += f"\n\n✨ 作者: {plugin.author}\n✨ 版本: {plugin.version}" @@ -111,15 +111,15 @@ class PluginCommands: command_names.append(filter_.group_name) if len(command_handlers) > 0: - parts = ["\n\n🔧 指令列表:\n"] + parts = ["\n\n🔧 指令列表:\n"] for i in range(len(command_handlers)): line = f"- {command_names[i]}" if command_handlers[i].desc: line += f": {command_handlers[i].desc}" parts.append(line + "\n") - parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。") + parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。") help_msg += "".join(parts) - ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg - ret += "更多帮助信息请查看插件仓库 README。" + ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg + ret += "更多帮助信息请查看插件仓库 README。" event.set_result(MessageEventResult().message(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 b5ee75ca2..41ea7986a 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ b/astrbot/builtin_stars/builtin_commands/commands/provider.py @@ -127,7 +127,7 @@ class ProviderCommands: return self.context.get_config(umo).get("provider_settings", {}) or {} except Exception as e: logger.debug( - "读取 provider_settings 失败,使用默认值: %s", + "读取 provider_settings 失败,使用默认值: %s", safe_error("", e), ) return {} @@ -142,7 +142,7 @@ class ProviderCommands: return max(float(raw), 0.0) except Exception as e: logger.debug( - "读取 %s 失败,回退默认值 %r: %s", + "读取 %s 失败,回退默认值 %r: %s", MODEL_LIST_CACHE_TTL_KEY, MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT, safe_error("", e), @@ -159,7 +159,7 @@ class ProviderCommands: value = int(raw) except Exception as e: logger.debug( - "读取 %s 失败,回退默认值 %r: %s", + "读取 %s 失败,回退默认值 %r: %s", MODEL_LOOKUP_MAX_CONCURRENCY_KEY, MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT, safe_error("", e), @@ -209,7 +209,7 @@ class ProviderCommands: ) -> str: prov.set_model(model_name) self.invalidate_provider_models_cache(prov.meta().id, umo=umo) - return f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]" + return f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]" async def _get_provider_models( self, @@ -265,7 +265,7 @@ class ProviderCommands: err_code: str, err_reason: str, ) -> None: - """记录不可达原因到日志。""" + """记录不可达原因到日志。""" meta = provider.meta() logger.warning( "Provider reachability check failed: id=%s type=%s code=%s reason=%s", @@ -358,7 +358,7 @@ class ProviderCommands: provider_id for provider_id, _ in failed_provider_errors ) logger.error( - "跨提供商查找模型 %s 时,所有 %d 个提供商的 get_models() 均失败: %s。请检查配置或网络", + "跨提供商查找模型 %s 时,所有 %d 个提供商的 get_models() 均失败: %s。请检查配置或网络", model_name, len(all_providers), failed_ids, @@ -405,7 +405,7 @@ class ProviderCommands: if all_providers: await event.send( MessageEventResult().message( - "正在进行提供商可达性测试,请稍候..." + "正在进行提供商可达性测试,请稍候..." ) ) check_results = await asyncio.gather( @@ -426,7 +426,7 @@ class ProviderCommands: if isinstance(reachable, asyncio.CancelledError): raise reachable if isinstance(reachable, Exception): - # 异常情况下兜底处理,避免单个 provider 导致列表失败 + # 异常情况下兜底处理,避免单个 provider 导致列表失败 self._log_reachability_failure( p, None, @@ -501,23 +501,23 @@ class ProviderCommands: line += " (当前使用)" parts.append(line + "\n") - parts.append("\n使用 /provider <序号> 切换 LLM 提供商。") + parts.append("\n使用 /provider <序号> 切换 LLM 提供商。") ret = "".join(parts) if ttss: - ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。" + ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。" if stts: - ret += "\n使用 /provider stt <序号> 切换 STT 提供商。" + ret += "\n使用 /provider stt <序号> 切换 STT 提供商。" if not reachability_check_enabled: - ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。" + ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。" event.set_result(MessageEventResult().message(ret)) elif idx == "tts": if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) + event.set_result(MessageEventResult().message("请输入序号。")) return if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) + event.set_result(MessageEventResult().message("无效的提供商序号。")) return provider = self.context.get_all_tts_providers()[idx2 - 1] id_ = provider.meta().id @@ -526,13 +526,13 @@ class ProviderCommands: provider_type=ProviderType.TEXT_TO_SPEECH, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) elif idx == "stt": if idx2 is None: - event.set_result(MessageEventResult().message("请输入序号。")) + event.set_result(MessageEventResult().message("请输入序号。")) return if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1: - event.set_result(MessageEventResult().message("无效的提供商序号。")) + event.set_result(MessageEventResult().message("无效的提供商序号。")) return provider = self.context.get_all_stt_providers()[idx2 - 1] id_ = provider.meta().id @@ -541,10 +541,10 @@ class ProviderCommands: provider_type=ProviderType.SPEECH_TO_TEXT, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(f"成功切换到 {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("无效的提供商序号。")) return provider = self.context.get_all_providers()[idx - 1] id_ = provider.meta().id @@ -553,16 +553,16 @@ class ProviderCommands: provider_type=ProviderType.CHAT_COMPLETION, umo=umo, ) - event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) + event.set_result(MessageEventResult().message(f"成功切换到 {id_}。")) else: - event.set_result(MessageEventResult().message("无效的参数。")) + event.set_result(MessageEventResult().message("无效的参数。")) async def _switch_model_by_name( self, message: AstrMessageEvent, model_name: str, prov: Provider ) -> None: model_name = model_name.strip() if not model_name: - message.set_result(MessageEventResult().message("模型名不能为空。")) + message.set_result(MessageEventResult().message("模型名不能为空。")) return umo = message.unified_msg_origin @@ -574,7 +574,7 @@ class ProviderCommands: prov, config, error_prefix="获取当前提供商模型列表失败: ", - warning_log="获取当前提供商 %s 模型列表失败,停止跨提供商查找: %s", + warning_log="获取当前提供商 %s 模型列表失败,停止跨提供商查找: %s", ) if models is None: return @@ -597,7 +597,7 @@ class ProviderCommands: if target_prov is None or matched_target_model_name is None: message.set_result( MessageEventResult().message( - f"模型 [{model_name}] 未在任何已配置的提供商中找到,或所有提供商模型列表获取失败,请检查配置或网络后重试。", + f"模型 [{model_name}] 未在任何已配置的提供商中找到,或所有提供商模型列表获取失败,请检查配置或网络后重试。", ), ) return @@ -612,7 +612,7 @@ class ProviderCommands: self._apply_model(target_prov, matched_target_model_name, umo=umo) message.set_result( MessageEventResult().message( - f"检测到模型 [{matched_target_model_name}] 属于提供商 [{target_id}],已自动切换提供商并设置模型。", + f"检测到模型 [{matched_target_model_name}] 属于提供商 [{target_id}],已自动切换提供商并设置模型。", ), ) except asyncio.CancelledError: @@ -633,7 +633,7 @@ class ProviderCommands: prov = self.context.get_using_provider(message.unified_msg_origin) if not prov: message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), ) return config = self._get_model_lookup_config(message.unified_msg_origin) @@ -655,7 +655,7 @@ class ProviderCommands: curr_model = prov.get_model() or "无" parts.append(f"\n当前模型: [{curr_model}]") parts.append( - "\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。" + "\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。" ) ret = "".join(parts) @@ -670,7 +670,7 @@ class ProviderCommands: 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("模型序号错误。")) else: try: new_model = models[idx_or_name - 1] @@ -697,7 +697,7 @@ class ProviderCommands: prov = self.context.get_using_provider(message.unified_msg_origin) if not prov: message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), + MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), ) return @@ -710,14 +710,14 @@ class ProviderCommands: parts.append(f"\n当前 Key: {curr_key[:8]}") parts.append("\n当前模型: " + prov.get_model()) - parts.append("\n使用 /key 切换 Key。") + parts.append("\n使用 /key 切换 Key。") ret = "".join(parts) message.set_result(MessageEventResult().message(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("Key 序号错误。")) else: try: new_key = keys_data[index - 1] @@ -726,7 +726,7 @@ class ProviderCommands: prov.meta().id, umo=message.unified_msg_origin, ) - message.set_result(MessageEventResult().message("切换 Key 成功。")) + message.set_result(MessageEventResult().message("切换 Key 成功。")) except Exception as e: message.set_result( MessageEventResult().message( diff --git a/astrbot/builtin_stars/builtin_commands/commands/setunset.py b/astrbot/builtin_stars/builtin_commands/commands/setunset.py index 096698844..d99e275af 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/setunset.py +++ b/astrbot/builtin_stars/builtin_commands/commands/setunset.py @@ -15,7 +15,7 @@ class SetUnsetCommands: event.set_result( MessageEventResult().message( - f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。", + f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。", ), ) @@ -26,11 +26,11 @@ class SetUnsetCommands: if key not in session_var: event.set_result( - MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"), + MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"), ) else: del session_var[key] await sp.session_put(uid, "session_variables", session_var) event.set_result( - MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"), + MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"), ) diff --git a/astrbot/builtin_stars/builtin_commands/commands/sid.py b/astrbot/builtin_stars/builtin_commands/commands/sid.py index e8bdbffb1..4d72a7c1c 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/sid.py +++ b/astrbot/builtin_stars/builtin_commands/commands/sid.py @@ -18,19 +18,19 @@ class SIDCommand: umo_msg_type = event.session.message_type.value umo_session_id = event.session.session_id ret = ( - f"UMO: 「{sid}」 此值可用于设置白名单。\n" - f"UID: 「{user_id}」 此值可用于设置管理员。\n" + f"UMO: 「{sid}」 此值可用于设置白名单。\n" + f"UID: 「{user_id}」 此值可用于设置管理员。\n" f"消息会话来源信息:\n" - f" 机器人 ID: 「{umo_platform}」\n" - f" 消息类型: 「{umo_msg_type}」\n" - f" 会话 ID: 「{umo_session_id}」\n" - f"消息来源可用于配置机器人的配置文件路由。" + f" 机器人 ID: 「{umo_platform}」\n" + f" 消息类型: 「{umo_msg_type}」\n" + f" 会话 ID: 「{umo_session_id}」\n" + f"消息来源可用于配置机器人的配置文件路由。" ) if ( self.context.get_config()["platform_settings"]["unique_session"] and event.get_group_id() ): - ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。" + ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。" event.set_result(MessageEventResult().message(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 78d6b0df7..617c08487 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/t2i.py +++ b/astrbot/builtin_stars/builtin_commands/commands/t2i.py @@ -16,8 +16,8 @@ class T2ICommand: if config["t2i"]: config["t2i"] = False config.save_config() - event.set_result(MessageEventResult().message("已关闭文本转图片模式。")) + event.set_result(MessageEventResult().message("已关闭文本转图片模式。")) return config["t2i"] = True config.save_config() - event.set_result(MessageEventResult().message("已开启文本转图片模式。")) + event.set_result(MessageEventResult().message("已开启文本转图片模式。")) diff --git a/astrbot/builtin_stars/builtin_commands/commands/tts.py b/astrbot/builtin_stars/builtin_commands/commands/tts.py index 13049ac22..a78be731f 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/tts.py +++ b/astrbot/builtin_stars/builtin_commands/commands/tts.py @@ -12,7 +12,7 @@ class TTSCommand: self.context = context async def tts(self, event: AstrMessageEvent) -> None: - """开关文本转语音(会话级别)""" + """开关文本转语音(会话级别)""" umo = event.unified_msg_origin ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo) cfg = self.context.get_config(umo=umo) @@ -27,10 +27,10 @@ class TTSCommand: if new_status and not tts_enable: event.set_result( MessageEventResult().message( - f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。", + f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。", ), ) else: event.set_result( - MessageEventResult().message(f"{status_text}当前会话的文本转语音。"), + MessageEventResult().message(f"{status_text}当前会话的文本转语音。"), ) diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index fb4a83403..a6c2b390c 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -51,7 +51,7 @@ class Main(star.Star): @plugin.command("ls") async def plugin_ls(self, event: AstrMessageEvent) -> None: - """获取已经安装的插件列表。""" + """获取已经安装的插件列表。""" await self.plugin_c.plugin_ls(event) @filter.permission_type(filter.PermissionType.ADMIN) @@ -84,7 +84,7 @@ class Main(star.Star): @filter.command("tts") async def tts(self, event: AstrMessageEvent) -> None: - """开关文本转语音(会话级别)""" + """开关文本转语音(会话级别)""" await self.tts_c.tts(event) @filter.command("sid") @@ -95,25 +95,25 @@ class Main(star.Star): @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("op") async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: - """授权管理员。op """ + """授权管理员。op """ await self.admin_c.op(event, admin_id) @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("deop") async def deop(self, event: AstrMessageEvent, admin_id: str) -> None: - """取消授权管理员。deop """ + """取消授权管理员。deop """ await self.admin_c.deop(event, admin_id) @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("wl") async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: - """添加白名单。wl """ + """添加白名单。wl """ await self.admin_c.wl(event, sid) @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("dwl") async def dwl(self, event: AstrMessageEvent, sid: str) -> None: - """删除白名单。dwl """ + """删除白名单。dwl """ await self.admin_c.dwl(event, sid) @filter.permission_type(filter.PermissionType.ADMIN) diff --git a/astrbot/builtin_stars/session_controller/main.py b/astrbot/builtin_stars/session_controller/main.py index 70081e03a..1e78b1e7f 100644 --- a/astrbot/builtin_stars/session_controller/main.py +++ b/astrbot/builtin_stars/session_controller/main.py @@ -72,9 +72,9 @@ class Main(Star): # 使用 LLM 生成回复 yield event.request_llm( prompt=( - "注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。" - "你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。" - "请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西" + "注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。" + "你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。" + "请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西" ), session_id=curr_cid, contexts=[], @@ -83,8 +83,8 @@ class Main(Star): ) except Exception as e: logger.error(f"LLM response failed: {e!s}") - # LLM 回复失败,使用原始预设回复 - yield event.plain_result("想要问什么呢?😄") + # LLM 回复失败,使用原始预设回复 + yield event.plain_result("想要问什么呢?😄") @session_waiter(60) async def empty_mention_waiter( @@ -106,7 +106,7 @@ class Main(Star): except TimeoutError as _: pass except Exception as e: - yield event.plain_result("发生错误,请联系管理员: " + str(e)) + yield event.plain_result("发生错误,请联系管理员: " + str(e)) finally: event.stop_event() except Exception as e: diff --git a/astrbot/builtin_stars/web_searcher/engines/__init__.py b/astrbot/builtin_stars/web_searcher/engines/__init__.py index 55d2abffd..b9041f282 100644 --- a/astrbot/builtin_stars/web_searcher/engines/__init__.py +++ b/astrbot/builtin_stars/web_searcher/engines/__init__.py @@ -81,7 +81,7 @@ class SearchEngine: return ret def tidy_text(self, text: str) -> str: - """清理文本,去除空格、换行符等""" + """清理文本,去除空格、换行符等""" return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ") def _get_url(self, tag: Tag) -> str: diff --git a/astrbot/builtin_stars/web_searcher/main.py b/astrbot/builtin_stars/web_searcher/main.py index d13ca1579..0ee17c568 100644 --- a/astrbot/builtin_stars/web_searcher/main.py +++ b/astrbot/builtin_stars/web_searcher/main.py @@ -34,14 +34,14 @@ class Main(star.Star): self.bocha_key_index = 0 self.bocha_key_lock = asyncio.Lock() - # 将 str 类型的 key 迁移至 list[str],并保存 + # 将 str 类型的 key 迁移至 list[str],并保存 cfg = self.context.get_config() provider_settings = cfg.get("provider_settings") if provider_settings: tavily_key = provider_settings.get("websearch_tavily_key") if isinstance(tavily_key, str): logger.info( - "检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。", + "检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。", ) if tavily_key: provider_settings["websearch_tavily_key"] = [tavily_key] @@ -62,7 +62,7 @@ class Main(star.Star): self.baidu_initialized = False async def _tidy_text(self, text: str) -> str: - """清理文本,去除空格、换行符等""" + """清理文本,去除空格、换行符等""" return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ") async def _get_from_url(self, url: str) -> str: @@ -124,10 +124,10 @@ class Main(star.Star): return results async def _get_tavily_key(self, cfg: AstrBotConfig) -> str: - """并发安全的从列表中获取并轮换Tavily API密钥。""" + """并发安全的从列表中获取并轮换Tavily API密钥。""" tavily_keys = cfg.get("provider_settings", {}).get("websearch_tavily_key", []) if not tavily_keys: - raise ValueError("错误:Tavily API密钥未在AstrBot中配置。") + raise ValueError("错误:Tavily API密钥未在AstrBot中配置。") async with self.tavily_key_lock: key = tavily_keys[self.tavily_key_index] @@ -203,11 +203,11 @@ class Main(star.Star): query: str, max_results: int = 5, ) -> str: - """搜索网络以回答用户的问题。当用户需要搜索网络以获取即时性的信息时调用此工具。 + """搜索网络以回答用户的问题。当用户需要搜索网络以获取即时性的信息时调用此工具。 Args: - query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。 - max_results(number): 返回的最大搜索结果数量,默认为 5。 + query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。 + max_results(number): 返回的最大搜索结果数量,默认为 5。 """ logger.info(f"web_searcher - search_from_search_engine: {query}") @@ -231,7 +231,7 @@ class Main(star.Star): ret += processed_result if websearch_link: - ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。" + ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。" return ret @@ -384,10 +384,10 @@ class Main(star.Star): return ret async def _get_bocha_key(self, cfg: AstrBotConfig) -> str: - """并发安全的从列表中获取并轮换BoCha API密钥。""" + """并发安全的从列表中获取并轮换BoCha API密钥。""" bocha_keys = cfg.get("provider_settings", {}).get("websearch_bocha_key", []) if not bocha_keys: - raise ValueError("错误:BoCha API密钥未在AstrBot中配置。") + raise ValueError("错误:BoCha API密钥未在AstrBot中配置。") async with self.bocha_key_lock: key = bocha_keys[self.bocha_key_index] @@ -500,18 +500,18 @@ class Main(star.Star): "count": count, } - # freshness:时间范围 + # freshness:时间范围 if freshness: payload["freshness"] = freshness # 是否返回摘要 payload["summary"] = summary - # include:限制搜索域 + # include:限制搜索域 if include: payload["include"] = include - # exclude:排除搜索域 + # exclude:排除搜索域 if exclude: payload["exclude"] = exclude diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index a9af37ff0..5fa761868 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -93,7 +93,7 @@ async def initialize_astrbot( if not backend_only and ( yes or click.confirm( - "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", + "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", default=True, ) ): @@ -103,7 +103,7 @@ async def initialize_astrbot( else: await check_dashboard(astrbot_root) else: - click.echo("你可以使用在线面版(需支持配置后端)来控制。") + click.echo("你可以使用在线面版(需支持配置后端)来控制。") @click.command() diff --git a/astrbot/core/agent/context/token_counter.py b/astrbot/core/agent/context/token_counter.py index 7c60cb23e..8cad5f99b 100644 --- a/astrbot/core/agent/context/token_counter.py +++ b/astrbot/core/agent/context/token_counter.py @@ -28,9 +28,9 @@ class TokenCounter(Protocol): ... -# 图片/音频 token 开销估算值,参考 OpenAI vision pricing: -# low-res ~85 tokens, high-res ~170 per 512px tile, 通常几百到上千。 -# 这里取一个保守中位数,宁可偏高触发压缩也不要偏低导致 API 报错。 +# 图片/音频 token 开销估算值,参考 OpenAI vision pricing: +# low-res ~85 tokens, high-res ~170 per 512px tile, 通常几百到上千。 +# 这里取一个保守中位数,宁可偏高触发压缩也不要偏低导致 API 报错。 IMAGE_TOKEN_ESTIMATE = 765 AUDIO_TOKEN_ESTIMATE = 500 diff --git a/astrbot/core/agent/context/truncator.py b/astrbot/core/agent/context/truncator.py index afd89f2be..c61bc588b 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -13,13 +13,13 @@ class ContextTruncator: ) def fix_messages(self, messages: list[Message]) -> list[Message]: - """修复消息列表,确保 tool call 和 tool response 的配对关系有效。 + """修复消息列表,确保 tool call 和 tool response 的配对关系有效。 - 此方法确保: + 此方法确保: 1. 每个 `tool` 消息前面都有一个包含 tool_calls 的 `assistant` 消息 2. 每个包含 tool_calls 的 `assistant` 消息后面都有对应的 `tool` 响应 - 这是 OpenAI Chat Completions API 规范的要求(Gemini 对此执行严格检查)。 + 这是 OpenAI Chat Completions API 规范的要求(Gemini 对此执行严格检查)。 """ if not messages: return messages @@ -41,17 +41,17 @@ class ContextTruncator: # 只有在有挂起的 assistant(tool_calls) 时才记录 tool 响应 if pending_assistant is not None: pending_tools.append(msg) - # else: 孤立的 tool 消息,直接忽略 + # else: 孤立的 tool 消息,直接忽略 continue if self._has_tool_calls(msg): - # 遇到新的 assistant(tool_calls) 前,先处理旧的 pending 链 + # 遇到新的 assistant(tool_calls) 前,先处理旧的 pending 链 flush_pending_if_valid() pending_assistant = msg continue - # 非 tool,且不含 tool_calls 的消息 - # 先结束任何 pending 链,再正常追加 + # 非 tool,且不含 tool_calls 的消息 + # 先结束任何 pending 链,再正常追加 flush_pending_if_valid() fixed_messages.append(msg) @@ -66,9 +66,9 @@ class ContextTruncator: keep_most_recent_turns: int, drop_turns: int = 1, ) -> list[Message]: - """截断上下文列表,确保不超过最大长度。 - 一个 turn 包含一个 user 消息和一个 assistant 消息。 - 这个方法会保证截断后的上下文列表符合 OpenAI 的上下文格式。 + """截断上下文列表,确保不超过最大长度。 + 一个 turn 包含一个 user 消息和一个 assistant 消息。 + 这个方法会保证截断后的上下文列表符合 OpenAI 的上下文格式。 Args: messages: 上下文列表 @@ -99,7 +99,7 @@ class ContextTruncator: else: truncated_contexts = non_system_messages[-num_to_keep * 2 :] - # 找到第一个 role 为 user 的索引,确保上下文格式正确 + # 找到第一个 role 为 user 的索引,确保上下文格式正确 index = next( (i for i, item in enumerate(truncated_contexts) if item.role == "user"), None, @@ -116,7 +116,7 @@ class ContextTruncator: messages: list[Message], drop_turns: int = 1, ) -> list[Message]: - """丢弃最旧的 N 个对话轮次。""" + """丢弃最旧的 N 个对话轮次。""" if drop_turns <= 0: return messages @@ -151,7 +151,7 @@ class ContextTruncator: self, messages: list[Message], ) -> list[Message]: - """对半砍策略,删除 50% 的消息""" + """对半砍策略,删除 50% 的消息""" if len(messages) <= 2: return messages diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index 08acd6614..2cbadfb59 100644 --- a/astrbot/core/agent/runners/coze/coze_agent_runner.py +++ b/astrbot/core/agent/runners/coze/coze_agent_runner.py @@ -45,17 +45,17 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): self.api_key = provider_config.get("coze_api_key", "") if not self.api_key: - raise Exception("Coze API Key 不能为空。") + raise Exception("Coze API Key 不能为空。") self.bot_id = provider_config.get("bot_id", "") if not self.bot_id: - raise Exception("Coze Bot ID 不能为空。") + raise Exception("Coze Bot ID 不能为空。") 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:// 开头。", + "Coze API Base URL 格式不正确,必须以 http:// 或 https:// 开头。", ) self.timeout = provider_config.get("timeout", 120) @@ -83,7 +83,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): except Exception as e: logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) - # 开始处理,转换到运行状态 + # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) try: @@ -91,15 +91,15 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_coze_request(): yield response except Exception as e: - logger.error(f"Coze 请求失败:{e!s}") + logger.error(f"Coze 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Coze 请求失败:{e!s}" + role="err", completion_text=f"Coze 请求失败:{e!s}" ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Coze 请求失败:{e!s}") + chain=MessageChain().message(f"Coze 请求失败:{e!s}") ), ) finally: @@ -152,7 +152,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): # 处理上下文中的图片 content = ctx["content"] if isinstance(content, list): - # 多模态内容,需要处理图片 + # 多模态内容,需要处理图片 processed_content = [] for item in content: if isinstance(item, dict): @@ -277,7 +277,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): accumulated_content += content message_started = True - # 如果是流式响应,发送增量数据 + # 如果是流式响应,发送增量数据 if self.streaming: yield AgentResponse( type="streaming_delta", @@ -328,7 +328,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): image_url: str, session_id: str | None = None, ) -> str: - """下载图片并上传到 Coze,返回 file_id""" + """下载图片并上传到 Coze,返回 file_id""" import hashlib # 计算哈希实现缓存 @@ -349,7 +349,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): if session_id: self.file_id_cache[session_id][cache_key] = file_id - logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}") + logger.debug(f"[Coze] 图片上传成功并缓存,file_id: {file_id}") return file_id diff --git a/astrbot/core/agent/runners/coze/coze_api_client.py b/astrbot/core/agent/runners/coze/coze_api_client.py index 11d0cdedd..41408ffb0 100644 --- a/astrbot/core/agent/runners/coze/coze_api_client.py +++ b/astrbot/core/agent/runners/coze/coze_api_client.py @@ -66,7 +66,7 @@ class CozeAPIClient: timeout=aiohttp.ClientTimeout(total=60), ) as response: if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception("Coze API 认证失败,请检查 API Key 是否正确") response_text = await response.text() logger.debug( @@ -75,7 +75,7 @@ class CozeAPIClient: if response.status != 200: raise Exception( - f"文件上传失败,状态码: {response.status}, 响应: {response_text}", + f"文件上传失败,状态码: {response.status}, 响应: {response_text}", ) try: @@ -87,7 +87,7 @@ class CozeAPIClient: raise Exception(f"文件上传失败: {result.get('msg', '未知错误')}") file_id = result["data"]["id"] - logger.debug(f"[Coze] 图片上传成功,file_id: {file_id}") + logger.debug(f"[Coze] 图片上传成功,file_id: {file_id}") return file_id except asyncio.TimeoutError: @@ -111,7 +111,7 @@ class CozeAPIClient: try: async with session.get(image_url) as response: if response.status != 200: - raise Exception(f"下载图片失败,状态码: {response.status}") + raise Exception(f"下载图片失败,状态码: {response.status}") image_data = await response.read() return image_data @@ -169,10 +169,10 @@ class CozeAPIClient: timeout=aiohttp.ClientTimeout(total=timeout), ) as response: if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception("Coze API 认证失败,请检查 API Key 是否正确") if response.status != 200: - raise Exception(f"Coze API 流式请求失败,状态码: {response.status}") + raise Exception(f"Coze API 流式请求失败,状态码: {response.status}") # SSE buffer = "" @@ -226,10 +226,10 @@ class CozeAPIClient: response_text = await response.text() if response.status == 401: - raise Exception("Coze API 认证失败,请检查 API Key 是否正确") + raise Exception("Coze API 认证失败,请检查 API Key 是否正确") if response.status != 200: - raise Exception(f"Coze API 请求失败,状态码: {response.status}") + raise Exception(f"Coze API 请求失败,状态码: {response.status}") try: return json.loads(response_text) diff --git a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py index cd016e526..96463f18e 100644 --- a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +++ b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py @@ -49,13 +49,13 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): self.api_key = provider_config.get("dashscope_api_key", "") if not self.api_key: - raise Exception("阿里云百炼 API Key 不能为空。") + raise Exception("阿里云百炼 API Key 不能为空。") self.app_id = provider_config.get("dashscope_app_id", "") if not self.app_id: - raise Exception("阿里云百炼 APP ID 不能为空。") + raise Exception("阿里云百炼 APP ID 不能为空。") self.dashscope_app_type = provider_config.get("dashscope_app_type", "") if not self.dashscope_app_type: - raise Exception("阿里云百炼 APP 类型不能为空。") + raise Exception("阿里云百炼 APP 类型不能为空。") self.variables: dict = provider_config.get("variables", {}) or {} self.rag_options: dict = provider_config.get("rag_options", {}) @@ -95,7 +95,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): except Exception as e: logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) - # 开始处理,转换到运行状态 + # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) try: @@ -103,15 +103,15 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_dashscope_request(): yield response except Exception as e: - logger.error(f"阿里云百炼请求失败:{e!s}") + logger.error(f"阿里云百炼请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"阿里云百炼请求失败:{e!s}" + role="err", completion_text=f"阿里云百炼请求失败:{e!s}" ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"阿里云百炼请求失败:{e!s}") + chain=MessageChain().message(f"阿里云百炼请求失败:{e!s}") ), ) @@ -161,7 +161,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): 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", + f"阿里云百炼请求失败: request_id={chunk.request_id}, code={chunk.status_code}, message={chunk.message}, 请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code", ) self._transition_state(AgentState.ERROR) error_msg = ( @@ -376,7 +376,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): # 检查图片输入 if image_urls: - logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") + logger.warning("阿里云百炼暂不支持图片输入,将自动忽略图片内容。") # 构建请求payload payload = await self._build_request_payload( diff --git a/astrbot/core/agent/runners/dify/dify_agent_runner.py b/astrbot/core/agent/runners/dify/dify_agent_runner.py index 5a6659ae8..60408c437 100644 --- a/astrbot/core/agent/runners/dify/dify_agent_runner.py +++ b/astrbot/core/agent/runners/dify/dify_agent_runner.py @@ -76,7 +76,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): except Exception as e: logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) - # 开始处理,转换到运行状态 + # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) try: @@ -84,15 +84,15 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_dify_request(): yield response except Exception as e: - logger.error(f"Dify 请求失败:{e!s}") + logger.error(f"Dify 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Dify 请求失败:{e!s}" + role="err", completion_text=f"Dify 请求失败:{e!s}" ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Dify 请求失败:{e!s}") + chain=MessageChain().message(f"Dify 请求失败:{e!s}") ), ) finally: @@ -133,10 +133,10 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): mime_type="image/png", file_name="image.png", ) - logger.debug(f"Dify 上传图片响应:{file_response}") + logger.debug(f"Dify 上传图片响应:{file_response}") if "id" not in file_response: logger.warning( - f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" + f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" ) continue files_payload.append( @@ -147,7 +147,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): } ) except Exception as e: - logger.warning(f"上传图片失败:{e}") + logger.warning(f"上传图片失败:{e}") continue # 获得会话变量 @@ -166,7 +166,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): match self.api_type: case "chat" | "agent" | "chatflow": if not prompt: - prompt = "请描述这张图片。" + prompt = "请描述这张图片。" async for chunk in self.api_client.chat_messages( inputs={ @@ -190,7 +190,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): ) conversation_id = chunk["conversation_id"] - # 如果是流式响应,发送增量数据 + # 如果是流式响应,发送增量数据 if self.streaming and chunk["answer"]: yield AgentResponse( type="streaming_delta", @@ -202,7 +202,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): logger.debug("Dify message end") break elif chunk["event"] == "error": - logger.error(f"Dify 出现错误:{chunk}") + logger.error(f"Dify 出现错误:{chunk}") raise Exception( f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}" ) @@ -222,11 +222,11 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): match chunk["event"]: case "workflow_started": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。" + f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。" ) case "node_finished": logger.debug( - f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。" + f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。" ) case "text_chunk": if self.streaming and chunk["data"]["text"]: @@ -242,24 +242,24 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): logger.info( f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束" ) - logger.debug(f"Dify 工作流结果:{chunk}") + logger.debug(f"Dify 工作流结果:{chunk}") if chunk["data"]["error"]: logger.error( - f"Dify 工作流出现错误:{chunk['data']['error']}" + f"Dify 工作流出现错误:{chunk['data']['error']}" ) raise Exception( - f"Dify 工作流出现错误:{chunk['data']['error']}" + f"Dify 工作流出现错误:{chunk['data']['error']}" ) if self.workflow_output_key not in chunk["data"]["outputs"]: raise Exception( - f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}" + f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}" ) result = chunk case _: - raise Exception(f"未知的 Dify API 类型:{self.api_type}") + raise Exception(f"未知的 Dify API 类型:{self.api_type}") if not result: - logger.warning("Dify 请求结果为空,请查看 Debug 日志。") + logger.warning("Dify 请求结果为空,请查看 Debug 日志。") # 解析结果 chain = await self.parse_dify_result(result) diff --git a/astrbot/core/agent/runners/dify/dify_api_client.py b/astrbot/core/agent/runners/dify/dify_api_client.py index df769f94b..bd3949063 100644 --- a/astrbot/core/agent/runners/dify/dify_api_client.py +++ b/astrbot/core/agent/runners/dify/dify_api_client.py @@ -66,7 +66,7 @@ class DifyAPIClient: if resp.status != 200: text = await resp.text() raise Exception( - f"Dify /chat-messages 接口请求失败:{resp.status}. {text}", + f"Dify /chat-messages 接口请求失败:{resp.status}. {text}", ) async for event in _stream_sse(resp): yield event @@ -95,7 +95,7 @@ class DifyAPIClient: if resp.status != 200: text = await resp.text() raise Exception( - f"Dify /workflows/run 接口请求失败:{resp.status}. {text}", + f"Dify /workflows/run 接口请求失败:{resp.status}. {text}", ) async for event in _stream_sse(resp): yield event @@ -149,11 +149,11 @@ class DifyAPIClient: async with self.session.post( url, data=form, - headers=self.headers, # 不包含 Content-Type,让 aiohttp 自动设置 + headers=self.headers, # 不包含 Content-Type,让 aiohttp 自动设置 ) as resp: if resp.status != 200 and resp.status != 201: text = await resp.text() - raise Exception(f"Dify 文件上传失败:{resp.status}. {text}") + raise Exception(f"Dify 文件上传失败:{resp.status}. {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 865e4ab39..8abf35c9f 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -359,7 +359,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): except Exception as e: logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True) - # 开始处理,转换到运行状态 + # 开始处理,转换到运行状态 self._transition_state(AgentState.RUNNING) llm_resp_result = None @@ -466,7 +466,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): llm_resp = llm_resp_result if llm_resp.role == "err": - # 如果 LLM 响应错误,转换到错误状态 + # 如果 LLM 响应错误,转换到错误状态 self.final_llm_resp = llm_resp self.stats.end_time = time.time() self._transition_state(AgentState.ERROR) @@ -484,7 +484,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): return if not llm_resp.tools_call_name: - # 如果没有工具调用,转换到完成状态 + # 如果没有工具调用,转换到完成状态 self.final_llm_resp = llm_resp self._transition_state(AgentState.DONE) self.stats.end_time = time.time() @@ -527,7 +527,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ), ) - # 如果有工具调用,还需处理工具调用 + # 如果有工具调用,还需处理工具调用 if llm_resp.tools_call_name: if self.tool_schema_mode == "lazy_load": llm_resp, _ = await self._resolve_tool_exec(llm_resp) @@ -628,7 +628,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): async for resp in self.step(): yield resp - # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step + # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step if not self.done(): logger.warning( f"Agent reached max steps ({max_step}), forcing a final response." @@ -640,7 +640,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): self.run_context.messages.append( Message( role="user", - content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", + content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", ) ) # 再执行最后一步 @@ -652,7 +652,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): req: ProviderRequest, llm_response: LLMResponse, ) -> T.AsyncGenerator[_HandleFunctionToolsResult, None]: - """处理函数工具调用。""" + """处理函数工具调用。""" tool_call_result_blocks: list[ToolCallMessageSegment] = [] logger.info(f"Agent 使用工具: {llm_response.tools_call_name}") @@ -725,17 +725,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): else: func_tool = req.func_tool.get_tool(func_tool_name) - logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") + logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}") if not func_tool: - logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。") + logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。") _append_tool_call_result( func_tool_id, f"error: Tool {func_tool_name} not found.", ) continue - valid_params = {} # 参数过滤:只传递函数实际需要的参数 + valid_params = {} # 参数过滤:只传递函数实际需要的参数 # 获取实际的 handler 函数 if func_tool.handler: @@ -760,7 +760,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): f"工具 {func_tool_name} 忽略非期望参数: {ignored_params}", ) else: - # 如果没有 handler(如 MCP 工具),使用所有参数 + # 如果没有 handler(如 MCP 工具),使用所有参数 valid_params = func_tool_args try: @@ -830,7 +830,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 这里我们将直接结束 Agent Loop # 发送消息逻辑在 ToolExecutor 中处理了 logger.warning( - f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。" + f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。" ) self._transition_state(AgentState.DONE) self.stats.end_time = time.time() @@ -841,7 +841,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): else: # 不应该出现其他类型 logger.warning( - f"Tool 返回了不支持的类型: {type(resp)}。", + f"Tool 返回了不支持的类型: {type(resp)}。", ) _append_tool_call_result( func_tool_id, diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index 36d409a37..856dca5fa 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -130,7 +130,7 @@ async def run_agent( agent_runner.run_context.messages.append( Message( role="user", - content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", + content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", ) ) @@ -179,7 +179,7 @@ async def run_agent( await astr_event.send( MessageChain(type="tool_call").message(status_msg) ) - # 对于其他情况,暂时先不处理 + # 对于其他情况,暂时先不处理 continue elif resp.type == "tool_call": if agent_runner.streaming: @@ -306,7 +306,7 @@ async def run_live_agent( show_tool_call_result: bool = False, show_reasoning: bool = False, ) -> AsyncGenerator[MessageChain | None, None]: - """Live Mode 的 Agent 运行器,支持流式 TTS + """Live Mode 的 Agent 运行器,支持流式 TTS Args: agent_runner: Agent 运行器 @@ -319,7 +319,7 @@ async def run_live_agent( Yields: MessageChain: 包含文本或音频数据的消息链 """ - # 如果没有 TTS Provider,直接发送文本 + # 如果没有 TTS Provider,直接发送文本 if not tts_provider: async for chain in run_agent( agent_runner, @@ -334,11 +334,11 @@ async def run_live_agent( support_stream = tts_provider.support_stream() if support_stream: - logger.info("[Live Agent] 使用流式 TTS(原生支持 get_audio_stream)") + logger.info("[Live Agent] 使用流式 TTS(原生支持 get_audio_stream)") else: logger.info( - f"[Live Agent] 使用 TTS({tts_provider.meta().type} " - "使用 get_audio,将按句子分块生成音频)" + f"[Live Agent] 使用 TTS({tts_provider.meta().type} " + "使用 get_audio,将按句子分块生成音频)" ) # 统计数据初始化 @@ -351,7 +351,7 @@ async def run_live_agent( # audio_queue stored bytes or (text, bytes) audio_queue: asyncio.Queue[bytes | tuple[str, bytes] | None] = asyncio.Queue() - # 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue + # 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue feeder_task = asyncio.create_task( _run_agent_feeder( agent_runner, @@ -363,7 +363,7 @@ async def run_live_agent( ) ) - # 2. 启动 TTS 任务:负责从 text_queue 读取文本并生成音频到 audio_queue + # 2. 启动 TTS 任务:负责从 text_queue 读取文本并生成音频到 audio_queue if support_stream: tts_task = asyncio.create_task( _safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue) @@ -373,7 +373,7 @@ async def run_live_agent( _simulated_stream_tts(tts_provider, text_queue, audio_queue) ) - # 3. 主循环:从 audio_queue 读取音频并 yield + # 3. 主循环:从 audio_queue 读取音频并 yield try: while True: queue_item = await audio_queue.get() @@ -388,7 +388,7 @@ async def run_live_agent( audio_data = queue_item if not first_chunk_received: - # 记录首帧延迟(从开始处理到收到第一个音频块) + # 记录首帧延迟(从开始处理到收到第一个音频块) tts_first_frame_time = time.time() - tts_start_time first_chunk_received = True @@ -467,9 +467,9 @@ async def _run_agent_feeder( if text: buffer += text - # 分句逻辑:匹配标点符号 - # r"([.。!!??\n]+)" 会保留分隔符 - parts = re.split(r"([.。!!??\n]+)", buffer) + # 分句逻辑:匹配标点符号 + # r"([.。!!??\n]+)" 会保留分隔符 + parts = re.split(r"([.。!!??\n]+)", buffer) if len(parts) > 1: # 处理完整的句子 diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 0e1cc6d8d..a71434d40 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -114,11 +114,11 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @classmethod async def execute(cls, tool, run_context, **tool_args): - """执行函数调用。 + """执行函数调用。 Args: - event (AstrMessageEvent): 事件对象, 当 origin 为 local 时必须提供。 - **kwargs: 函数调用的参数。 + event (AstrMessageEvent): 事件对象, 当 origin 为 local 时必须提供。 + **kwargs: 函数调用的参数。 Returns: AsyncGenerator[None | mcp.types.CallToolResult, None] @@ -738,11 +738,11 @@ async def call_local_llm_tool( except ValueError as e: raise Exception(f"Tool execution ValueError: {e}") from e except TypeError as e: - # 获取函数的签名(包括类型),除了第一个 event/context 参数。 + # 获取函数的签名(包括类型),除了第一个 event/context 参数。 try: sig = inspect.signature(handler) params = list(sig.parameters.values()) - # 跳过第一个参数(event 或 context) + # 跳过第一个参数(event 或 context) if params: params = params[1:] @@ -781,7 +781,7 @@ async def call_local_llm_tool( try: async for ret in ready_to_call: # 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码 - # 返回值只能是 MessageEventResult 或者 None(无返回值) + # 返回值只能是 MessageEventResult 或者 None(无返回值) _has_yielded = True if isinstance(ret, MessageEventResult | CommandResult): # 如果返回值是 MessageEventResult, 设置结果并继续 diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index aab4bff64..8b523aed5 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -140,10 +140,10 @@ 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("未找到指定的提供商: %s。", sel_provider) if not isinstance(provider, Provider): logger.error( - "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) + "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) ) return None return provider @@ -167,7 +167,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("无法创建新的对话。") return conversation @@ -219,7 +219,7 @@ async def _apply_file_extract( if not file_paths: return if not req.prompt: - req.prompt = "总结一下文件里面讲了什么?" + 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") @@ -752,10 +752,10 @@ def _should_disable_streaming_for_webchat_output( def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: - """根据事件中的插件设置,过滤请求中的工具列表。 + """根据事件中的插件设置,过滤请求中的工具列表。 - 注意:没有 handler_module_path 的工具(如 MCP 工具)会被保留, - 因为它们不属于任何插件,不应被插件过滤逻辑影响。 + 注意:没有 handler_module_path 的工具(如 MCP 工具)会被保留, + 因为它们不属于任何插件,不应被插件过滤逻辑影响。 """ if event.plugins_name is not None and req.func_tool: new_tool_set = ToolSet() @@ -766,13 +766,13 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: continue mp = tool.handler_module_path if not mp: - # 没有 plugin 归属信息的工具(如 subagent transfer_to_*) - # 不应受到会话插件过滤影响。 + # 没有 plugin 归属信息的工具(如 subagent transfer_to_*) + # 不应受到会话插件过滤影响。 new_tool_set.add_tool(tool) continue plugin = star_map.get(mp) if not plugin: - # 无法解析插件归属时,保守保留工具,避免误过滤。 + # 无法解析插件归属时,保守保留工具,避免误过滤。 new_tool_set.add_tool(tool) continue if plugin.name in event.plugins_name or plugin.reserved: @@ -841,13 +841,13 @@ def _get_compress_provider( provider = plugin_context.get_provider_by_id(config.llm_compress_provider_id) if provider is None: logger.warning( - "未找到指定的上下文压缩模型 %s,将跳过压缩。", + "未找到指定的上下文压缩模型 %s,将跳过压缩。", config.llm_compress_provider_id, ) return None if not isinstance(provider, Provider): logger.warning( - "指定的上下文压缩模型 %s 不是对话模型,将跳过压缩。", + "指定的上下文压缩模型 %s 不是对话模型,将跳过压缩。", config.llm_compress_provider_id, ) return None @@ -898,20 +898,20 @@ async def build_main_agent( req: ProviderRequest | None = None, apply_reset: bool = True, ) -> MainAgentBuildResult | None: - """构建主对话代理(Main Agent),并且自动 reset。 + """构建主对话代理(Main Agent),并且自动 reset。 If apply_reset is False, will not call reset on the agent runner. """ provider = provider or _select_provider(event, plugin_context) if provider is None: - logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。") + logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。") return None if req is None: if event.get_extra("provider_request"): req = event.get_extra("provider_request") assert isinstance(req, ProviderRequest), ( - "provider_request 必须是 ProviderRequest 类型。" + "provider_request 必须是 ProviderRequest 类型。" ) if req.conversation: req.contexts = json.loads(req.conversation.history) diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index 9e1a0481e..8f21d4140 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -371,7 +371,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return ( f"error: unsupported message type '{msg_type}' at index {idx}." ) - except Exception as exc: # 捕获组件构造异常,避免直接抛出 + except Exception as exc: # 捕获组件构造异常,避免直接抛出 return f"error: failed to build messages[{idx}] component: {exc}" try: @@ -418,7 +418,7 @@ async def retrieve_knowledge_base( # 会话级配置 kb_ids = session_config.get("kb_ids", []) - # 如果配置为空列表,明确表示不使用知识库 + # 如果配置为空列表,明确表示不使用知识库 if not kb_ids: logger.info(f"[知识库] 会话 {umo} 已被配置为不使用知识库") return @@ -444,18 +444,18 @@ async def retrieve_knowledge_base( if not kb_names: return - logger.debug(f"[知识库] 使用会话级配置,知识库数量: {len(kb_names)}") + logger.debug(f"[知识库] 使用会话级配置,知识库数量: {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(f"[知识库] 使用全局配置,知识库数量: {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(f"[知识库] 开始检索知识库,数量: {len(kb_names)}, top_k={top_k}") kb_context = await kb_mgr.retrieve( query=query, kb_names=kb_names, diff --git a/astrbot/core/astrbot_config_mgr.py b/astrbot/core/astrbot_config_mgr.py index c2bfb1c37..ecc6782fd 100644 --- a/astrbot/core/astrbot_config_mgr.py +++ b/astrbot/core/astrbot_config_mgr.py @@ -122,7 +122,7 @@ class AstrBotConfigManager: self.abconf_data = abconf_data def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig: - """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" + """获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。""" if not umo: return self.confs["default"] if isinstance(umo, MessageSession): @@ -268,7 +268,7 @@ class AstrBotConfigManager: key: str | None = None, default: _VT = None, ) -> _VT: - """获取配置项。umo 为 None 时使用默认配置""" + """获取配置项。umo 为 None 时使用默认配置""" if umo is None: return self.confs["default"].get(key, default) conf = self.get_conf(umo) diff --git a/astrbot/core/backup/__init__.py b/astrbot/core/backup/__init__.py index ba96d9edf..f624298ff 100644 --- a/astrbot/core/backup/__init__.py +++ b/astrbot/core/backup/__init__.py @@ -1,6 +1,6 @@ """AstrBot 备份与恢复模块 -提供数据导出和导入功能,支持用户在服务器迁移时一键备份和恢复所有数据。 +提供数据导出和导入功能,支持用户在服务器迁移时一键备份和恢复所有数据。 """ # 从 constants 模块导入共享常量 diff --git a/astrbot/core/backup/constants.py b/astrbot/core/backup/constants.py index be206b307..338c7f57b 100644 --- a/astrbot/core/backup/constants.py +++ b/astrbot/core/backup/constants.py @@ -1,6 +1,6 @@ """AstrBot 备份模块共享常量 -此文件定义了导出器和导入器共享的常量,确保两端配置一致。 +此文件定义了导出器和导入器共享的常量,确保两端配置一致。 """ from sqlmodel import SQLModel @@ -60,10 +60,10 @@ KB_METADATA_MODELS: dict[str, type[SQLModel]] = { def get_backup_directories() -> dict[str, str]: """获取需要备份的目录列表 - 使用 astrbot_path 模块动态获取路径,支持通过环境变量 ASTRBOT_ROOT 自定义根目录。 + 使用 astrbot_path 模块动态获取路径,支持通过环境变量 ASTRBOT_ROOT 自定义根目录。 Returns: - dict: 键为备份文件中的目录名称,值为目录的绝对路径 + dict: 键为备份文件中的目录名称,值为目录的绝对路径 """ return { "plugins": get_astrbot_plugin_path(), # 插件本体 diff --git a/astrbot/core/backup/exporter.py b/astrbot/core/backup/exporter.py index 33f41621a..54ccb880b 100644 --- a/astrbot/core/backup/exporter.py +++ b/astrbot/core/backup/exporter.py @@ -1,7 +1,7 @@ """AstrBot 数据导出器 -负责将所有数据导出为 ZIP 备份文件。 -导出格式为 JSON,这是数据库无关的方案,支持未来向 MySQL/PostgreSQL 迁移。 +负责将所有数据导出为 ZIP 备份文件。 +导出格式为 JSON,这是数据库无关的方案,支持未来向 MySQL/PostgreSQL 迁移。 """ import hashlib @@ -40,19 +40,19 @@ CMD_CONFIG_FILE_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json") class AstrBotExporter: """AstrBot 数据导出器 - 导出内容: - - 主数据库所有表(data/data_v4.db) - - 知识库元数据(data/knowledge_base/kb.db) + 导出内容: + - 主数据库所有表(data/data_v4.db) + - 知识库元数据(data/knowledge_base/kb.db) - 每个知识库的向量文档数据 - - 配置文件(data/cmd_config.json) + - 配置文件(data/cmd_config.json) - 附件文件 - 知识库多媒体文件 - - 插件目录(data/plugins) - - 插件数据目录(data/plugin_data) - - 配置目录(data/config) - - T2I 模板目录(data/t2i_templates) - - WebChat 数据目录(data/webchat) - - 临时文件目录(data/temp) + - 插件目录(data/plugins) + - 插件数据目录(data/plugin_data) + - 配置目录(data/config) + - T2I 模板目录(data/t2i_templates) + - WebChat 数据目录(data/webchat) + - 临时文件目录(data/temp) """ def __init__( @@ -75,7 +75,7 @@ class AstrBotExporter: Args: output_dir: 输出目录 - progress_callback: 进度回调函数,接收参数 (stage, current, total, message) + progress_callback: 进度回调函数,接收参数 (stage, current, total, message) Returns: str: 生成的 ZIP 文件路径 @@ -321,7 +321,7 @@ class AstrBotExporter: for dir_name, dir_path in backup_directories.items(): full_path = Path(dir_path) if not await anyio.Path(full_path).exists(): - logger.debug(f"目录不存在,跳过: {full_path}") + logger.debug(f"目录不存在,跳过: {full_path}") continue file_count = 0 @@ -377,9 +377,9 @@ class AstrBotExporter: def _model_to_dict(self, record: Any) -> dict: """将 SQLModel 实例转换为字典 - 这是数据库无关的序列化方式,支持未来迁移到其他数据库。 + 这是数据库无关的序列化方式,支持未来迁移到其他数据库。 """ - # 使用 SQLModel 内置的 model_dump 方法(如果可用) + # 使用 SQLModel 内置的 model_dump 方法(如果可用) if hasattr(record, "model_dump"): data = record.model_dump(mode="python") # 处理 datetime 类型 @@ -450,7 +450,7 @@ class AstrBotExporter: "version": BACKUP_MANIFEST_VERSION, "astrbot_version": VERSION, "exported_at": datetime.now(timezone.utc).isoformat(), - "origin": "exported", # 标记备份来源:exported=本实例导出, uploaded=用户上传 + "origin": "exported", # 标记备份来源:exported=本实例导出, uploaded=用户上传 "schema_version": { "main_db": "v4", "kb_db": "v1", diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index 53fb00248..f0af47b03 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -1,9 +1,9 @@ """AstrBot 数据导入器 -负责从 ZIP 备份文件恢复所有数据。 -导入时进行版本校验: -- 主版本(前两位)不同时直接拒绝导入 -- 小版本(第三位)不同时提示警告,用户可选择强制导入 +负责从 ZIP 备份文件恢复所有数据。 +导入时进行版本校验: +- 主版本(前两位)不同时直接拒绝导入 +- 小版本(第三位)不同时提示警告,用户可选择强制导入 - 版本匹配时也需要用户确认 """ @@ -40,13 +40,13 @@ if TYPE_CHECKING: def _get_major_version(version_str: str) -> str: - """提取版本的主版本部分(前两位) + """提取版本的主版本部分(前两位) Args: - version_str: 版本字符串,如 "4.9.1", "4.10.0-beta" + version_str: 版本字符串,如 "4.9.1", "4.10.0-beta" Returns: - 主版本字符串,如 "4.9", "4.10" + 主版本字符串,如 "4.9", "4.10" """ if not version_str: return "0.0" @@ -105,14 +105,14 @@ class _InvalidCountWarnLimiter: if self.limit > 0: if self._count < self.limit: logger.warning( - "platform_stats count 非法,已按 0 处理: value=%r, key=%s", + "platform_stats count 非法,已按 0 处理: value=%r, key=%s", value, key_for_log, ) self._count += 1 if self._count == self.limit and not self._suppression_logged: logger.warning( - "platform_stats 非法 count 告警已达到上限 (%d),后续将抑制", + "platform_stats 非法 count 告警已达到上限 (%d),后续将抑制", self.limit, ) self._suppression_logged = True @@ -121,7 +121,7 @@ class _InvalidCountWarnLimiter: if not self._suppression_logged: # limit <= 0: emit only one suppression warning. logger.warning( - "platform_stats 非法 count 告警已达到上限 (%d),后续将抑制", + "platform_stats 非法 count 告警已达到上限 (%d),后续将抑制", self.limit, ) self._suppression_logged = True @@ -131,15 +131,15 @@ class _InvalidCountWarnLimiter: class ImportPreCheckResult: """导入预检查结果 - 用于在实际导入前检查备份文件的版本兼容性, - 并返回确认信息让用户决定是否继续导入。 + 用于在实际导入前检查备份文件的版本兼容性, + 并返回确认信息让用户决定是否继续导入。 """ - # 检查是否通过(文件有效且版本可导入) + # 检查是否通过(文件有效且版本可导入) valid: bool = False - # 是否可以导入(版本兼容) + # 是否可以导入(版本兼容) can_import: bool = False - # 版本状态: match(完全匹配), minor_diff(小版本差异), major_diff(主版本不同,拒绝) + # 版本状态: match(完全匹配), minor_diff(小版本差异), major_diff(主版本不同,拒绝) version_status: str = "" # 备份文件中的 AstrBot 版本 backup_version: str = "" @@ -147,11 +147,11 @@ class ImportPreCheckResult: current_version: str = VERSION # 备份创建时间 backup_time: str = "" - # 确认消息(显示给用户) + # 确认消息(显示给用户) confirm_message: str = "" # 警告消息列表 warnings: list[str] = field(default_factory=list) - # 错误消息(如果检查失败) + # 错误消息(如果检查失败) error: str = "" # 备份包含的内容摘要 backup_summary: dict = field(default_factory=dict) @@ -209,18 +209,18 @@ class DatabaseClearError(RuntimeError): class AstrBotImporter: """AstrBot 数据导入器 - 导入备份文件中的所有数据,包括: + 导入备份文件中的所有数据,包括: - 主数据库所有表 - 知识库元数据和文档 - 配置文件 - 附件文件 - 知识库多媒体文件 - - 插件目录(data/plugins) - - 插件数据目录(data/plugin_data) - - 配置目录(data/config) - - T2I 模板目录(data/t2i_templates) - - WebChat 数据目录(data/webchat) - - 临时文件目录(data/temp) + - 插件目录(data/plugins) + - 插件数据目录(data/plugin_data) + - 配置目录(data/config) + - T2I 模板目录(data/t2i_templates) + - WebChat 数据目录(data/webchat) + - 临时文件目录(data/temp) """ def __init__( @@ -238,8 +238,8 @@ class AstrBotImporter: def pre_check(self, zip_path: str) -> ImportPreCheckResult: """预检查备份文件 - 在实际导入前检查备份文件的有效性和版本兼容性。 - 返回检查结果供前端显示确认对话框。 + 在实际导入前检查备份文件的有效性和版本兼容性。 + 返回检查结果供前端显示确认对话框。 Args: zip_path: ZIP 备份文件路径 @@ -261,7 +261,7 @@ class AstrBotImporter: manifest_data = zf.read("manifest.json") manifest = json.loads(manifest_data) except KeyError: - result.error = "备份文件缺少 manifest.json,不是有效的 AstrBot 备份" + result.error = "备份文件缺少 manifest.json,不是有效的 AstrBot 备份" return result except json.JSONDecodeError as e: result.error = f"manifest.json 格式错误: {e}" @@ -286,7 +286,7 @@ class AstrBotImporter: result.can_import = version_check["can_import"] # 版本信息由前端根据 version_status 和 i18n 生成显示 - # 不再将版本消息添加到 warnings 列表中,避免中文硬编码 + # 不再将版本消息添加到 warnings 列表中,避免中文硬编码 # warnings 列表保留用于其他非版本相关的警告 return result @@ -301,9 +301,9 @@ class AstrBotImporter: def _check_version_compatibility(self, backup_version: str) -> dict: """检查版本兼容性 - 规则: - - 主版本(前两位,如 4.9)必须一致,否则拒绝 - - 小版本(第三位,如 4.9.1 vs 4.9.2)不同时,警告但允许导入 + 规则: + - 主版本(前两位,如 4.9)必须一致,否则拒绝 + - 小版本(第三位,如 4.9.1 vs 4.9.2)不同时,警告但允许导入 Returns: dict: {status, can_import, message} @@ -315,7 +315,7 @@ class AstrBotImporter: "message": "备份文件缺少版本信息", } - # 提取主版本(前两位)进行比较 + # 提取主版本(前两位)进行比较 backup_major = _get_major_version(backup_version) current_major = _get_major_version(VERSION) @@ -325,8 +325,8 @@ class AstrBotImporter: "status": "major_diff", "can_import": False, "message": ( - f"主版本不兼容: 备份版本 {backup_version}, 当前版本 {VERSION}。" - f"跨主版本导入可能导致数据损坏,请使用相同主版本的 AstrBot。" + f"主版本不兼容: 备份版本 {backup_version}, 当前版本 {VERSION}。" + f"跨主版本导入可能导致数据损坏,请使用相同主版本的 AstrBot。" ), } @@ -337,7 +337,7 @@ class AstrBotImporter: "status": "minor_diff", "can_import": True, "message": ( - f"小版本差异: 备份版本 {backup_version}, 当前版本 {VERSION}。" + f"小版本差异: 备份版本 {backup_version}, 当前版本 {VERSION}。" ), } @@ -357,8 +357,8 @@ class AstrBotImporter: Args: zip_path: ZIP 备份文件路径 - mode: 导入模式,目前仅支持 "replace"(清空后导入) - progress_callback: 进度回调函数,接收参数 (stage, current, total, message) + mode: 导入模式,目前仅支持 "replace"(清空后导入) + progress_callback: 进度回调函数,接收参数 (stage, current, total, message) Returns: ImportResult: 导入结果 @@ -497,8 +497,8 @@ class AstrBotImporter: def _validate_version(self, manifest: dict) -> None: """验证版本兼容性 - 仅允许相同主版本导入 - 注意:此方法仅在 import_all 中调用,用于双重校验。 - 前端应先调用 pre_check 获取详细的版本信息并让用户确认。 + 注意:此方法仅在 import_all 中调用,用于双重校验。 + 前端应先调用 pre_check 获取详细的版本信息并让用户确认。 """ backup_version = manifest.get("astrbot_version") if not backup_version: @@ -593,7 +593,7 @@ class AstrBotImporter: duplicate_count = len(rows) - len(normalized_rows) if duplicate_count > 0: logger.warning( - "检测到 %s 重复键 %d 条,已在导入前聚合", + "检测到 %s 重复键 %d 条,已在导入前聚合", table_name, duplicate_count, ) @@ -865,10 +865,10 @@ class AstrBotImporter: """ dir_stats: dict[str, int] = {} - # 检查备份版本是否支持目录备份(需要版本 >= 1.1) + # 检查备份版本是否支持目录备份(需要版本 >= 1.1) backup_version = manifest.get("version", "1.0") if VersionComparator.compare_version(backup_version, "1.1") < 0: - logger.info("备份版本不支持目录备份,跳过目录导入") + logger.info("备份版本不支持目录备份,跳过目录导入") return dir_stats backed_up_dirs = manifest.get("directories", []) @@ -895,7 +895,7 @@ class AstrBotImporter: if not dir_files: continue - # 备份现有目录(如果存在) + # 备份现有目录(如果存在) if await anyio.Path(target_dir).exists(): backup_path = Path(f"{target_dir}.bak") if await anyio.Path(backup_path).exists(): diff --git a/astrbot/core/computer/booters/bay_manager.py b/astrbot/core/computer/booters/bay_manager.py index 61ccc1b3a..96370dc4c 100644 --- a/astrbot/core/computer/booters/bay_manager.py +++ b/astrbot/core/computer/booters/bay_manager.py @@ -96,7 +96,7 @@ class BayContainerManager: "BAY_SERVER__HOST=0.0.0.0", f"BAY_SERVER__PORT={BAY_PORT}", "BAY_DATA_DIR=/app/data", - # allow_anonymous=false → auto-provisions API key + # allow_anonymous=false → auto-provisions API key "BAY_SECURITY__ALLOW_ANONYMOUS=false", ], "HostConfig": { diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index ac9f0b26a..083c31930 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -382,7 +382,7 @@ class ShipyardNeoBooter(ComputerBooter): """Pick the best profile for this session. Resolution order: - 1. User-specified profile (non-empty, non-default) → use as-is. + 1. User-specified profile (non-empty, non-default) → use as-is. 2. Query ``GET /v1/profiles`` and pick the profile with the most capabilities, preferring profiles that include ``"browser"``. 3. Fall back to :attr:`DEFAULT_PROFILE`. @@ -391,7 +391,7 @@ class ShipyardNeoBooter(ComputerBooter): misconfigured token, and silently falling back would just delay the real failure to ``create_sandbox``. """ - # User explicitly set a profile → honour it + # User explicitly set a profile → honour it if self._profile and self._profile != self.DEFAULT_PROFILE: logger.info( "[Computer] profile_selected mode=user profile=%s", diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 67c718892..06527ff46 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -57,7 +57,7 @@ def _discover_bay_credentials(endpoint: str) -> str: candidates.append(Path(bay_data_dir) / "credentials.json") # 2. Mono-repo layout: AstrBot/../pkgs/bay/credentials.json - astrbot_root = Path(__file__).resolve().parents[3] # astrbot/core/computer/ → root + astrbot_root = Path(__file__).resolve().parents[3] # astrbot/core/computer/ → root candidates.append(astrbot_root.parent / "pkgs" / "bay" / "credentials.json") # 3. Current working directory diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 6a415e56c..4aa92bc33 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -17,11 +17,11 @@ class RateLimitStrategy(enum.Enum): class AstrBotConfig(dict): - """从配置文件中加载的配置,支持直接通过点号操作符访问根配置项。 + """从配置文件中加载的配置,支持直接通过点号操作符访问根配置项。 - - 初始化时会将传入的 default_config 与配置文件进行比对,如果配置文件中缺少配置项则会自动插入默认值并进行一次写入操作。会递归检查配置项。 - - 如果配置文件路径对应的文件不存在,则会自动创建并写入默认配置。 - - 如果传入了 schema,将会通过 schema 解析出 default_config,此时传入的 default_config 会被忽略。 + - 初始化时会将传入的 default_config 与配置文件进行比对,如果配置文件中缺少配置项则会自动插入默认值并进行一次写入操作。会递归检查配置项。 + - 如果配置文件路径对应的文件不存在,则会自动创建并写入默认配置。 + - 如果传入了 schema,将会通过 schema 解析出 default_config,此时传入的 default_config 会被忽略。 """ config_path: str @@ -36,7 +36,7 @@ class AstrBotConfig(dict): ) -> None: super().__init__() - # 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件 + # 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件 object.__setattr__(self, "config_path", config_path) object.__setattr__(self, "default_config", default_config) object.__setattr__(self, "schema", schema) @@ -57,7 +57,7 @@ class AstrBotConfig(dict): conf_str = conf_str[1:] conf = json.loads(conf_str) - # 检查配置完整性,并插入 + # 检查配置完整性,并插入 has_new = self.check_config_integrity(default_config, conf) self.update(conf) if has_new: @@ -73,7 +73,7 @@ class AstrBotConfig(dict): for k, v in schema.items(): if v["type"] not in DEFAULT_VALUE_MAP: raise TypeError( - f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}", + f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}", ) if "default" in v: default = v["default"] @@ -93,7 +93,7 @@ class AstrBotConfig(dict): return conf def check_config_integrity(self, refer_conf: dict, conf: dict, path=""): - """检查配置完整性,如果有新的配置项或顺序不一致则返回 True""" + """检查配置完整性,如果有新的配置项或顺序不一致则返回 True""" has_new = False # 创建一个新的有序字典以保持参考配置的顺序 @@ -102,19 +102,19 @@ class AstrBotConfig(dict): # 先按照参考配置的顺序添加配置项 for key, value in refer_conf.items(): if key not in conf: - # 配置项不存在,插入默认值 + # 配置项不存在,插入默认值 path_ = path + "." + key if path else key - logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}") + logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}") new_conf[key] = value has_new = True elif conf[key] is None: - # 配置项为 None,使用默认值 + # 配置项为 None,使用默认值 new_conf[key] = value has_new = True elif isinstance(value, dict): # 递归检查子配置项 if not isinstance(conf[key], dict): - # 类型不匹配,使用默认值 + # 类型不匹配,使用默认值 new_conf[key] = value has_new = True else: @@ -134,15 +134,15 @@ class AstrBotConfig(dict): 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(f"检查到配置项 {path_} 不存在,将从当前配置中删除") has_new = True # 顺序不一致也算作变更 if list(conf.keys()) != list(new_conf.keys()): if path: - logger.info(f"检查到配置项 {path} 的子项顺序不一致,已重新排序") + logger.info(f"检查到配置项 {path} 的子项顺序不一致,已重新排序") else: - logger.info("检查到配置项顺序不一致,已重新排序") + logger.info("检查到配置项顺序不一致,已重新排序") has_new = True # 更新原始配置 @@ -154,7 +154,7 @@ class AstrBotConfig(dict): def save_config(self, replace_config: dict | None = None) -> None: """将配置写入文件 - 如果传入 replace_config,则将配置替换为 replace_config + 如果传入 replace_config,则将配置替换为 replace_config """ if replace_config: self.update(replace_config) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index b2d29af2d..356658be6 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1,4 +1,4 @@ -"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。""" +"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。""" import os from importlib import metadata @@ -51,11 +51,11 @@ DEFAULT_CONFIG = { "log_base": 2.6, "words_count_threshold": 150, "split_mode": "regex", # regex 或 words - "regex": ".*?[。?!~…]+|.+$", + "regex": ".*?[。?!~…]+|.+$", "split_words": [ - "。", - "?", - "!", + "。", + "?", + "!", "~", "…", ], # 当 split_mode 为 words 时使用 @@ -217,7 +217,7 @@ DEFAULT_CONFIG = { }, "platform": [], "platform_specific": { - # 平台特异配置:按平台分类,平台下按功能分组 + # 平台特异配置:按平台分类,平台下按功能分组 "lark": { "pre_ack_emoji": {"enable": False, "emojis": ["Typing"]}, }, @@ -272,14 +272,14 @@ CHAT_PROVIDER_TEMPLATE = { } """ -AstrBot v3 时代的配置元数据,目前仅承担以下功能: +AstrBot v3 时代的配置元数据,目前仅承担以下功能: -1. 保存配置时,配置项的类型验证 +1. 保存配置时,配置项的类型验证 2. WebUI 展示提供商和平台适配器模版 -WebUI 的配置文件在 `CONFIG_METADATA_3` 中。 +WebUI 的配置文件在 `CONFIG_METADATA_3` 中。 -未来将会逐步淘汰此配置元数据。 +未来将会逐步淘汰此配置元数据。 """ CONFIG_METADATA_2 = { "platform_group": { @@ -350,7 +350,7 @@ CONFIG_METADATA_2 = { "企业微信智能机器人": { "id": "wecom_ai_bot", "type": "wecom_ai_bot", - "hint": "如果发现字段有异常,请重新创建", + "hint": "如果发现字段有异常,请重新创建", "enable": True, "wecom_ai_bot_connection_mode": "long_connection", # long_connection, webhook "wecom_ai_bot_name": "", @@ -525,32 +525,32 @@ CONFIG_METADATA_2 = { "satori_api_base_url": { "description": "Satori API 终结点", "type": "string", - "hint": "Satori API 的基础地址。", + "hint": "Satori API 的基础地址。", }, "satori_endpoint": { "description": "Satori WebSocket 终结点", "type": "string", - "hint": "Satori 事件的 WebSocket 端点。", + "hint": "Satori 事件的 WebSocket 端点。", }, "satori_token": { "description": "Satori 令牌", "type": "string", - "hint": "用于 Satori API 身份验证的令牌。", + "hint": "用于 Satori API 身份验证的令牌。", }, "satori_auto_reconnect": { "description": "启用自动重连", "type": "bool", - "hint": "断开连接时是否自动重新连接 WebSocket。", + "hint": "断开连接时是否自动重新连接 WebSocket。", }, "satori_heartbeat_interval": { "description": "Satori 心跳间隔", "type": "int", - "hint": "发送心跳消息的间隔(秒)。", + "hint": "发送心跳消息的间隔(秒)。", }, "satori_reconnect_delay": { "description": "Satori 重连延迟", "type": "int", - "hint": "尝试重新连接前的延迟时间(秒)。", + "hint": "尝试重新连接前的延迟时间(秒)。", }, "slack_connection_mode": { "description": "Slack Connection Mode", @@ -588,108 +588,108 @@ CONFIG_METADATA_2 = { "active_send_mode": { "description": "是否换用主动发送接口", "type": "bool", - "desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。", + "desc": "只有企业认证的公众号才能主动发送。主动发送接口的限制会少一些。", }, "wpp_active_message_poll": { "description": "是否启用主动消息轮询", "type": "bool", - "hint": "只有当你发现微信消息没有按时同步到 AstrBot 时,才需要启用这个功能,默认不启用。", + "hint": "只有当你发现微信消息没有按时同步到 AstrBot 时,才需要启用这个功能,默认不启用。", }, "wpp_active_message_poll_interval": { "description": "主动消息轮询间隔", "type": "int", - "hint": "主动消息轮询间隔,单位为秒,默认 3 秒,最大不要超过 60 秒,否则可能被认为是旧消息。", + "hint": "主动消息轮询间隔,单位为秒,默认 3 秒,最大不要超过 60 秒,否则可能被认为是旧消息。", }, "kf_name": { "description": "微信客服账号名", "type": "string", - "hint": "可选。微信客服账号名(不是 ID)。可在 https://kf.weixin.qq.com/kf/frame#/accounts 获取", + "hint": "可选。微信客服账号名(不是 ID)。可在 https://kf.weixin.qq.com/kf/frame#/accounts 获取", }, "telegram_token": { "description": "Bot Token", "type": "string", - "hint": "如果你的网络环境为中国大陆,请在 `其他配置` 处设置代理或更改 api_base。", + "hint": "如果你的网络环境为中国大陆,请在 `其他配置` 处设置代理或更改 api_base。", }, "misskey_instance_url": { "description": "Misskey 实例 URL", "type": "string", - "hint": "例如 https://misskey.example,填写 Bot 账号所在的 Misskey 实例地址", + "hint": "例如 https://misskey.example,填写 Bot 账号所在的 Misskey 实例地址", }, "misskey_token": { "description": "Misskey Access Token", "type": "string", - "hint": "连接服务设置生成的 API 鉴权访问令牌(Access token)", + "hint": "连接服务设置生成的 API 鉴权访问令牌(Access token)", }, "misskey_default_visibility": { "description": "默认帖子可见性", "type": "string", "options": ["public", "home", "followers"], - "hint": "机器人发帖时的默认可见性设置。public:公开,home:主页时间线,followers:仅关注者。", + "hint": "机器人发帖时的默认可见性设置。public:公开,home:主页时间线,followers:仅关注者。", }, "misskey_local_only": { - "description": "仅限本站(不参与联合)", + "description": "仅限本站(不参与联合)", "type": "bool", - "hint": "启用后,机器人发出的帖子将仅在本实例可见,不会联合到其他实例", + "hint": "启用后,机器人发出的帖子将仅在本实例可见,不会联合到其他实例", }, "misskey_enable_chat": { "description": "启用聊天消息响应", "type": "bool", - "hint": "启用后,机器人将会监听和响应私信聊天消息", + "hint": "启用后,机器人将会监听和响应私信聊天消息", }, "misskey_enable_file_upload": { "description": "启用文件上传到 Misskey", "type": "bool", - "hint": "启用后,适配器会尝试将消息链中的文件上传到 Misskey。URL 文件会先尝试服务器端上传,异步上传失败时会回退到下载后本地上传。", + "hint": "启用后,适配器会尝试将消息链中的文件上传到 Misskey。URL 文件会先尝试服务器端上传,异步上传失败时会回退到下载后本地上传。", }, "misskey_allow_insecure_downloads": { - "description": "允许不安全下载(禁用 SSL 验证)", + "description": "允许不安全下载(禁用 SSL 验证)", "type": "bool", - "hint": "当远端服务器存在证书问题导致无法正常下载时,自动禁用 SSL 验证作为回退方案。适用于某些图床的证书配置问题。启用有安全风险,仅在必要时使用。", + "hint": "当远端服务器存在证书问题导致无法正常下载时,自动禁用 SSL 验证作为回退方案。适用于某些图床的证书配置问题。启用有安全风险,仅在必要时使用。", }, "misskey_download_timeout": { - "description": "远端下载超时时间(秒)", + "description": "远端下载超时时间(秒)", "type": "int", - "hint": "下载远程文件时的超时时间(秒),用于异步上传回退到本地上传的场景。", + "hint": "下载远程文件时的超时时间(秒),用于异步上传回退到本地上传的场景。", }, "misskey_download_chunk_size": { - "description": "流式下载分块大小(字节)", + "description": "流式下载分块大小(字节)", "type": "int", - "hint": "流式下载和计算 MD5 时使用的每次读取字节数,过小会增加开销,过大会占用内存。", + "hint": "流式下载和计算 MD5 时使用的每次读取字节数,过小会增加开销,过大会占用内存。", }, "misskey_max_download_bytes": { - "description": "最大允许下载字节数(超出则中止)", + "description": "最大允许下载字节数(超出则中止)", "type": "int", - "hint": "如果希望限制下载文件的最大大小以防止 OOM,请填写最大字节数;留空或 null 表示不限制。", + "hint": "如果希望限制下载文件的最大大小以防止 OOM,请填写最大字节数;留空或 null 表示不限制。", }, "misskey_upload_concurrency": { "description": "并发上传限制", "type": "int", - "hint": "同时进行的文件上传任务上限(整数,默认 3)。", + "hint": "同时进行的文件上传任务上限(整数,默认 3)。", }, "misskey_upload_folder": { "description": "上传到网盘的目标文件夹 ID", "type": "string", - "hint": "可选:填写 Misskey 网盘中目标文件夹的 ID,上传的文件将放置到该文件夹内。留空则使用账号网盘根目录。", + "hint": "可选:填写 Misskey 网盘中目标文件夹的 ID,上传的文件将放置到该文件夹内。留空则使用账号网盘根目录。", }, "card_template_id": { "description": "卡片模板 ID", "type": "string", - "hint": "可选。钉钉互动卡片模板 ID。启用后将使用互动卡片进行流式回复。", + "hint": "可选。钉钉互动卡片模板 ID。启用后将使用互动卡片进行流式回复。", }, "telegram_command_register": { "description": "Telegram 命令注册", "type": "bool", - "hint": "启用后,AstrBot 将会自动注册 Telegram 命令。", + "hint": "启用后,AstrBot 将会自动注册 Telegram 命令。", }, "telegram_command_auto_refresh": { "description": "Telegram 命令自动刷新", "type": "bool", - "hint": "启用后,AstrBot 将会在运行时自动刷新 Telegram 命令。(单独设置此项无效)", + "hint": "启用后,AstrBot 将会在运行时自动刷新 Telegram 命令。(单独设置此项无效)", }, "telegram_command_register_interval": { "description": "Telegram 命令自动刷新间隔", "type": "int", - "hint": "Telegram 命令自动刷新间隔,单位为秒。", + "hint": "Telegram 命令自动刷新间隔,单位为秒。", }, "id": { "description": "机器人名称", @@ -704,32 +704,32 @@ CONFIG_METADATA_2 = { "enable": { "description": "启用", "type": "bool", - "hint": "是否启用该适配器。未启用的适配器对应的消息平台将不会接收到消息。", + "hint": "是否启用该适配器。未启用的适配器对应的消息平台将不会接收到消息。", }, "appid": { "description": "appid", "type": "string", - "hint": "必填项。QQ 官方机器人平台的 appid。如何获取请参考文档。", + "hint": "必填项。QQ 官方机器人平台的 appid。如何获取请参考文档。", }, "secret": { "description": "secret", "type": "string", - "hint": "必填项。", + "hint": "必填项。", }, "enable_group_c2c": { "description": "启用消息列表单聊", "type": "bool", - "hint": "启用后,机器人可以接收到 QQ 消息列表中的私聊消息。你可能需要在 QQ 机器人平台上通过扫描二维码的方式添加机器人为你的好友。详见文档。", + "hint": "启用后,机器人可以接收到 QQ 消息列表中的私聊消息。你可能需要在 QQ 机器人平台上通过扫描二维码的方式添加机器人为你的好友。详见文档。", }, "enable_guild_direct_message": { "description": "启用频道私聊", "type": "bool", - "hint": "启用后,机器人可以接收到频道的私聊消息。", + "hint": "启用后,机器人可以接收到频道的私聊消息。", }, "ws_reverse_host": { "description": "反向 Websocket 主机", "type": "string", - "hint": "AstrBot 将作为服务器端。", + "hint": "AstrBot 将作为服务器端。", }, "ws_reverse_port": { "description": "反向 Websocket 端口", @@ -738,34 +738,34 @@ CONFIG_METADATA_2 = { "ws_reverse_token": { "description": "反向 Websocket Token", "type": "string", - "hint": "反向 Websocket Token。未设置则不启用 Token 验证。", + "hint": "反向 Websocket Token。未设置则不启用 Token 验证。", }, "wecom_ai_bot_name": { "description": "企业微信智能机器人的名字", "type": "string", - "hint": "请务必填写正确,否则无法使用一些指令。", + "hint": "请务必填写正确,否则无法使用一些指令。", }, "wecom_ai_bot_connection_mode": { "description": "企业微信智能机器人连接模式", "type": "string", "options": ["webhook", "long_connection"], "labels": ["Webhook 回调", "长连接"], - "hint": "Webhook 回调模式需要配置 Token/EncodingAESKey。长连接模式需要配置 BotID/Secret。", + "hint": "Webhook 回调模式需要配置 Token/EncodingAESKey。长连接模式需要配置 BotID/Secret。", }, "wecomaibot_init_respond_text": { "description": "企业微信智能机器人初始响应文本", "type": "string", - "hint": "当机器人收到消息时,首先回复的文本内容。留空则不设置。", + "hint": "当机器人收到消息时,首先回复的文本内容。留空则不设置。", }, "wecomaibot_friend_message_welcome_text": { "description": "企业微信智能机器人私聊欢迎语", "type": "string", - "hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。", + "hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。", }, "wecomaibot_token": { "description": "企业微信智能机器人 Token", "type": "string", - "hint": "用于 Webhook 回调模式的身份验证。", + "hint": "用于 Webhook 回调模式的身份验证。", "condition": { "wecom_ai_bot_connection_mode": "webhook", }, @@ -773,7 +773,7 @@ CONFIG_METADATA_2 = { "wecomaibot_encoding_aes_key": { "description": "企业微信智能机器人 EncodingAESKey", "type": "string", - "hint": "用于 Webhook 回调模式的消息加密解密。", + "hint": "用于 Webhook 回调模式的消息加密解密。", "condition": { "wecom_ai_bot_connection_mode": "webhook", }, @@ -781,17 +781,17 @@ CONFIG_METADATA_2 = { "msg_push_webhook_url": { "description": "企业微信消息推送 Webhook URL", "type": "string", - "hint": "用于 send_by_session 主动消息推送。格式示例: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx", + "hint": "用于 send_by_session 主动消息推送。格式示例: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx", }, "only_use_webhook_url_to_send": { "description": "仅使用 Webhook 发送消息", "type": "bool", - "hint": "启用后,企业微信智能机器人的所有回复都改为通过消息推送 Webhook 发送。消息推送 Webhook 支持更多的消息类型(如图片、文件等)。", + "hint": "启用后,企业微信智能机器人的所有回复都改为通过消息推送 Webhook 发送。消息推送 Webhook 支持更多的消息类型(如图片、文件等)。", }, "wecomaibot_ws_bot_id": { "description": "长连接 BotID", "type": "string", - "hint": "企业微信智能机器人长连接模式凭证 BotID。", + "hint": "企业微信智能机器人长连接模式凭证 BotID。", "condition": { "wecom_ai_bot_connection_mode": "long_connection", }, @@ -799,7 +799,7 @@ CONFIG_METADATA_2 = { "wecomaibot_ws_secret": { "description": "长连接 Secret", "type": "string", - "hint": "企业微信智能机器人长连接模式凭证 Secret。", + "hint": "企业微信智能机器人长连接模式凭证 Secret。", "condition": { "wecom_ai_bot_connection_mode": "long_connection", }, @@ -808,7 +808,7 @@ CONFIG_METADATA_2 = { "description": "长连接 WebSocket 地址", "type": "string", "invisible": True, - "hint": "默认值为 wss://openws.work.weixin.qq.com,一般无需修改。", + "hint": "默认值为 wss://openws.work.weixin.qq.com,一般无需修改。", "condition": { "wecom_ai_bot_connection_mode": "long_connection", }, @@ -817,7 +817,7 @@ CONFIG_METADATA_2 = { "description": "长连接心跳间隔", "type": "int", "invisible": True, - "hint": "长连接模式心跳间隔(秒),建议 30 秒。", + "hint": "长连接模式心跳间隔(秒),建议 30 秒。", "condition": { "wecom_ai_bot_connection_mode": "long_connection", }, @@ -825,7 +825,7 @@ CONFIG_METADATA_2 = { "lark_bot_name": { "description": "飞书机器人的名字", "type": "string", - "hint": "请务必填写正确,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。", + "hint": "请务必填写正确,否则 @ 机器人将无法唤醒,只能通过前缀唤醒。", }, "discord_token": { "description": "Discord Bot Token", @@ -835,22 +835,22 @@ CONFIG_METADATA_2 = { "discord_proxy": { "description": "Discord 代理地址", "type": "string", - "hint": "可选的代理地址:http://ip:port", + "hint": "可选的代理地址:http://ip:port", }, "discord_command_register": { "description": "注册 Discord 指令", - "hint": "启用后,自动将插件指令注册为 Discord 斜杠指令", + "hint": "启用后,自动将插件指令注册为 Discord 斜杠指令", "type": "bool", }, "discord_activity_name": { "description": "Discord 活动名称", "type": "string", - "hint": "可选的 Discord 活动名称。留空则不设置活动。", + "hint": "可选的 Discord 活动名称。留空则不设置活动。", }, "port": { "description": "回调服务器端口", "type": "int", - "hint": "回调服务器端口。留空则不启用回调服务器。", + "hint": "回调服务器端口。留空则不启用回调服务器。", "condition": { "unified_webhook_mode": False, }, @@ -858,7 +858,7 @@ CONFIG_METADATA_2 = { "callback_server_host": { "description": "回调服务器主机", "type": "string", - "hint": "回调服务器主机。留空则不启用回调服务器。", + "hint": "回调服务器主机。留空则不启用回调服务器。", "condition": { "unified_webhook_mode": False, }, @@ -866,53 +866,53 @@ CONFIG_METADATA_2 = { "unified_webhook_mode": { "description": "统一 Webhook 模式", "type": "bool", - "hint": "Webhook 模式下使用 AstrBot 统一 Webhook 入口,无需单独开启端口。回调地址为 /api/platform/webhook/{webhook_uuid}。", + "hint": "Webhook 模式下使用 AstrBot 统一 Webhook 入口,无需单独开启端口。回调地址为 /api/platform/webhook/{webhook_uuid}。", }, "webhook_uuid": { "invisible": True, "description": "Webhook UUID", "type": "string", - "hint": "统一 Webhook 模式下的唯一标识符,创建平台时自动生成。", + "hint": "统一 Webhook 模式下的唯一标识符,创建平台时自动生成。", }, "kook_bot_token": { "description": "机器人 Token", "type": "string", - "hint": "必填项。从 KOOK 开发者平台获取的机器人 Token。", + "hint": "必填项。从 KOOK 开发者平台获取的机器人 Token。", }, "kook_reconnect_delay": { "description": "重连延迟", "type": "int", - "hint": "重连延迟时间(秒),使用指数退避策略。", + "hint": "重连延迟时间(秒),使用指数退避策略。", }, "kook_max_reconnect_delay": { "description": "最大重连延迟", "type": "int", - "hint": "重连延迟的最大值(秒)。", + "hint": "重连延迟的最大值(秒)。", }, "kook_max_retry_delay": { "description": "最大重试延迟", "type": "int", - "hint": "重试的最大延迟时间(秒)。", + "hint": "重试的最大延迟时间(秒)。", }, "kook_heartbeat_interval": { "description": "心跳间隔", "type": "int", - "hint": "心跳检测间隔时间(秒)。", + "hint": "心跳检测间隔时间(秒)。", }, "kook_heartbeat_timeout": { "description": "心跳超时时间", "type": "int", - "hint": "心跳检测超时时间(秒)。", + "hint": "心跳检测超时时间(秒)。", }, "kook_max_heartbeat_failures": { "description": "最大心跳失败次数", "type": "int", - "hint": "允许的最大心跳失败次数,超过后断开连接。", + "hint": "允许的最大心跳失败次数,超过后断开连接。", }, "kook_max_consecutive_failures": { "description": "最大连续失败次数", "type": "int", - "hint": "允许的最大连续失败次数,超过后停止重试。", + "hint": "允许的最大连续失败次数,超过后停止重试。", }, }, }, @@ -935,19 +935,19 @@ CONFIG_METADATA_2 = { }, "no_permission_reply": { "type": "bool", - "hint": "启用后,当用户没有权限执行某个操作时,机器人会回复一条消息。", + "hint": "启用后,当用户没有权限执行某个操作时,机器人会回复一条消息。", }, "empty_mention_waiting": { "type": "bool", - "hint": "启用后,当消息内容只有 @ 机器人时,会触发等待,在 60 秒内的该用户的任意一条消息均会唤醒机器人。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。", + "hint": "启用后,当消息内容只有 @ 机器人时,会触发等待,在 60 秒内的该用户的任意一条消息均会唤醒机器人。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。", }, "empty_mention_waiting_need_reply": { "type": "bool", - "hint": "在上面一个配置项中,如果启用了触发等待,启用此项后,机器人会使用 LLM 生成一条回复。否则,将不回复而只是等待。", + "hint": "在上面一个配置项中,如果启用了触发等待,启用此项后,机器人会使用 LLM 生成一条回复。否则,将不回复而只是等待。", }, "friend_message_needs_wake_prefix": { "type": "bool", - "hint": "启用后,私聊消息需要唤醒前缀才会被处理,同群聊一样。", + "hint": "启用后,私聊消息需要唤醒前缀才会被处理,同群聊一样。", }, "ignore_bot_self_message": { "type": "bool", @@ -955,7 +955,7 @@ CONFIG_METADATA_2 = { }, "ignore_at_all": { "type": "bool", - "hint": "启用后,机器人会忽略 @ 全体成员 的消息事件。", + "hint": "启用后,机器人会忽略 @ 全体成员 的消息事件。", }, "segmented_reply": { "type": "object", @@ -989,11 +989,11 @@ CONFIG_METADATA_2 = { }, "reply_prefix": { "type": "string", - "hint": "机器人回复消息时带有的前缀。", + "hint": "机器人回复消息时带有的前缀。", }, "forward_threshold": { "type": "int", - "hint": "超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。目前仅 QQ 平台适配器适用。", + "hint": "超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。目前仅 QQ 平台适配器适用。", }, "enable_id_white_list": { "type": "bool", @@ -1001,11 +1001,11 @@ CONFIG_METADATA_2 = { "id_whitelist": { "type": "list", "items": {"type": "string"}, - "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可使用 /wl 添加白名单", + "hint": "只处理填写的 ID 发来的消息事件,为空时不启用。可使用 /sid 指令获取在平台上的会话 ID(类似 abc:GroupMessage:123)。管理员可使用 /wl 添加白名单", }, "id_whitelist_log": { "type": "bool", - "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", + "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", }, "wl_ignore_admin_on_group": { "type": "bool", @@ -1015,16 +1015,16 @@ CONFIG_METADATA_2 = { }, "reply_with_mention": { "type": "bool", - "hint": "启用后,机器人回复消息时会 @ 发送者。实际效果以具体的平台适配器为准。", + "hint": "启用后,机器人回复消息时会 @ 发送者。实际效果以具体的平台适配器为准。", }, "reply_with_quote": { "type": "bool", - "hint": "启用后,机器人回复消息时会引用原消息。实际效果以具体的平台适配器为准。", + "hint": "启用后,机器人回复消息时会引用原消息。实际效果以具体的平台适配器为准。", }, "path_mapping": { "type": "list", "items": {"type": "string"}, - "hint": "此功能解决由于文件系统不一致导致路径不存在的问题。格式为 <原路径>:<映射路径>。如 `/app/.config/QQ:/var/lib/docker/volumes/xxxx/_data`。这样,当消息平台下发的事件中图片和语音路径以 `/app/.config/QQ` 开头时,开头被替换为 `/var/lib/docker/volumes/xxxx/_data`。这在 AstrBot 或者平台协议端使用 Docker 部署时特别有用。", + "hint": "此功能解决由于文件系统不一致导致路径不存在的问题。格式为 <原路径>:<映射路径>。如 `/app/.config/QQ:/var/lib/docker/volumes/xxxx/_data`。这样,当消息平台下发的事件中图片和语音路径以 `/app/.config/QQ` 开头时,开头被替换为 `/var/lib/docker/volumes/xxxx/_data`。这在 AstrBot 或者平台协议端使用 Docker 部署时特别有用。", }, }, }, @@ -1033,14 +1033,14 @@ CONFIG_METADATA_2 = { "items": { "also_use_in_response": { "type": "bool", - "hint": "启用后,大模型的响应也会通过内容安全审核。", + "hint": "启用后,大模型的响应也会通过内容安全审核。", }, "baidu_aip": { "type": "object", "items": { "enable": { "type": "bool", - "hint": "启用此功能前,您需要手动在设备中安装 baidu-aip 库。一般来说,安装指令如下: `pip3 install baidu-aip`", + "hint": "启用此功能前,您需要手动在设备中安装 baidu-aip 库。一般来说,安装指令如下: `pip3 install baidu-aip`", }, "app_id": {"description": "APP ID", "type": "string"}, "api_key": {"description": "API Key", "type": "string"}, @@ -1058,7 +1058,7 @@ CONFIG_METADATA_2 = { "extra_keywords": { "type": "list", "items": {"type": "string"}, - "hint": "额外的屏蔽关键词列表,支持正则表达式。", + "hint": "额外的屏蔽关键词列表,支持正则表达式。", }, }, }, @@ -1548,7 +1548,7 @@ CONFIG_METADATA_2 = { "proxy": "", }, "阿里云百炼 TTS(API)": { - "hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition", + "hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition", "id": "dashscope_tts", "provider": "dashscope", "type": "dashscope_tts", @@ -1721,18 +1721,18 @@ CONFIG_METADATA_2 = { "xai_native_search": { "description": "启用原生搜索功能", "type": "bool", - "hint": "启用后,将通过 xAI 的 Chat Completions 原生 Live Search 进行联网检索(按需计费)。仅对 xAI 提供商生效。", + "hint": "启用后,将通过 xAI 的 Chat Completions 原生 Live Search 进行联网检索(按需计费)。仅对 xAI 提供商生效。", "condition": {"provider": "xai"}, }, "rerank_api_base": { "description": "重排序模型 API Base URL", "type": "string", - "hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。", + "hint": "AstrBot 会在请求时在末尾加上 /v1/rerank。", }, "rerank_api_key": { "description": "API Key", "type": "string", - "hint": "如果不需要 API Key, 请留空。", + "hint": "如果不需要 API Key, 请留空。", }, "rerank_model": { "description": "重排序模型名称", @@ -1741,17 +1741,17 @@ CONFIG_METADATA_2 = { "return_documents": { "description": "是否在排序结果中返回文档原文", "type": "bool", - "hint": "默认值false,以减少网络传输开销。", + "hint": "默认值false,以减少网络传输开销。", }, "instruct": { "description": "自定义排序任务类型说明", "type": "string", - "hint": "仅在使用 qwen3-rerank 模型时生效。建议使用英文撰写。", + "hint": "仅在使用 qwen3-rerank 模型时生效。建议使用英文撰写。", }, "launch_model_if_not_running": { "description": "模型未运行时自动启动", "type": "bool", - "hint": "如果模型当前未在 Xinference 服务中运行,是否尝试自动启动它。在生产环境中建议关闭。", + "hint": "如果模型当前未在 Xinference 服务中运行,是否尝试自动启动它。在生产环境中建议关闭。", }, "modalities": { "description": "模型能力", @@ -1760,24 +1760,24 @@ CONFIG_METADATA_2 = { "options": ["text", "image", "tool_use"], "labels": ["文本", "图像", "工具使用"], "render_type": "checkbox", - "hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。", + "hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。", }, "custom_headers": { "description": "自定义添加请求头", "type": "dict", "items": {}, - "hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。", + "hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。", }, "custom_extra_body": { "description": "自定义请求体参数", "type": "dict", "items": {}, - "hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。", + "hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。", "template_schema": { "temperature": { "name": "Temperature", "description": "温度参数", - "hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。", + "hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。", "type": "float", "default": 0.6, "slider": {"min": 0, "max": 2, "step": 0.1}, @@ -1785,7 +1785,7 @@ CONFIG_METADATA_2 = { "top_p": { "name": "Top-p", "description": "Top-p 采样", - "hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。", + "hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。", "type": "float", "default": 1.0, "slider": {"min": 0, "max": 1, "step": 0.01}, @@ -1793,7 +1793,7 @@ CONFIG_METADATA_2 = { "max_tokens": { "name": "Max Tokens", "description": "最大令牌数", - "hint": "生成的最大令牌数。", + "hint": "生成的最大令牌数。", "type": "int", "default": 8192, }, @@ -1806,37 +1806,37 @@ CONFIG_METADATA_2 = { "gpt_weights_path": { "description": "GPT模型文件路径", "type": "string", - "hint": "即“.ckpt”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)", + "hint": "即“.ckpt”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)", }, "sovits_weights_path": { "description": "SoVITS模型文件路径", "type": "string", - "hint": "即“.pth”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)", + "hint": "即“.pth”后缀的文件,请使用绝对路径,路径两端不要带双引号,不填则默认用GPT_SoVITS内置的SoVITS模型(建议直接在GPT_SoVITS中改默认模型)", }, "gsv_default_parms": { "description": "GPT_SoVITS默认参数", - "hint": "参考音频文件路径、参考音频文本必填,其他参数根据个人爱好自行填写", + "hint": "参考音频文件路径、参考音频文本必填,其他参数根据个人爱好自行填写", "type": "object", "items": { "gsv_ref_audio_path": { "description": "参考音频文件路径", "type": "string", - "hint": "必填!请使用绝对路径!路径两端不要带双引号!", + "hint": "必填!请使用绝对路径!路径两端不要带双引号!", }, "gsv_prompt_text": { "description": "参考音频文本", "type": "string", - "hint": "必填!请填写参考音频讲述的文本", + "hint": "必填!请填写参考音频讲述的文本", }, "gsv_prompt_lang": { "description": "参考音频文本语言", "type": "string", - "hint": "请填写参考音频讲述的文本的语言,默认为中文", + "hint": "请填写参考音频讲述的文本的语言,默认为中文", }, "gsv_aux_ref_audio_paths": { "description": "辅助参考音频文件路径", "type": "string", - "hint": "辅助参考音频文件,可不填", + "hint": "辅助参考音频文件,可不填", }, "gsv_text_lang": { "description": "文本语言", @@ -1861,7 +1861,7 @@ CONFIG_METADATA_2 = { "gsv_text_split_method": { "description": "切分文本的方法", "type": "string", - "hint": "可选值: `cut0`:不切分 `cut1`:四句一切 `cut2`:50字一切 `cut3`:按中文句号切 `cut4`:按英文句号切 `cut5`:按标点符号切", + "hint": "可选值: `cut0`:不切分 `cut1`:四句一切 `cut2`:50字一切 `cut3`:按中文句号切 `cut4`:按英文句号切 `cut5`:按标点符号切", "options": [ "cut0", "cut1", @@ -1926,13 +1926,13 @@ CONFIG_METADATA_2 = { "embedding_dimensions": { "description": "嵌入维度", "type": "int", - "hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。", + "hint": "嵌入向量的维度。根据模型不同,可能需要调整,请参考具体模型的文档。此配置项请务必填写正确,否则将导致向量数据库无法正常工作。", "_special": "get_embedding_dim", }, "embedding_model": { "description": "嵌入模型", "type": "string", - "hint": "嵌入模型名称。", + "hint": "嵌入模型名称。", }, "embedding_api_key": { "description": "API Key", @@ -1945,7 +1945,7 @@ CONFIG_METADATA_2 = { "volcengine_cluster": { "type": "string", "description": "火山引擎集群", - "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts", + "hint": "若使用语音复刻大模型,可选volcano_icl或volcano_icl_concurr,默认使用volcano_tts", }, "volcengine_voice_type": { "type": "string", @@ -1955,12 +1955,12 @@ CONFIG_METADATA_2 = { "volcengine_speed_ratio": { "type": "float", "description": "语速设置", - "hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0", + "hint": "语速设置,范围为 0.2 到 3.0,默认值为 1.0", }, "volcengine_volume_ratio": { "type": "float", "description": "音量设置", - "hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0", + "hint": "音量设置,范围为 0.0 到 2.0,默认值为 1.0", }, "azure_tts_voice": { "type": "string", @@ -1970,12 +1970,12 @@ CONFIG_METADATA_2 = { "azure_tts_style": { "type": "string", "description": "风格设置", - "hint": "声音特定的讲话风格。 可以表达快乐、同情和平静等情绪。", + "hint": "声音特定的讲话风格。 可以表达快乐、同情和平静等情绪。", }, "azure_tts_role": { "type": "string", - "description": "模仿设置(可选)", - "hint": "讲话角色扮演。 声音可以模仿不同的年龄和性别,但声音名称不会更改。 例如,男性语音可以提高音调和改变语调来模拟女性语音,但语音名称不会更改。 如果角色缺失或不受声音的支持,则会忽略此属性。", + "description": "模仿设置(可选)", + "hint": "讲话角色扮演。 声音可以模仿不同的年龄和性别,但声音名称不会更改。 例如,男性语音可以提高音调和改变语调来模拟女性语音,但语音名称不会更改。 如果角色缺失或不受声音的支持,则会忽略此属性。", "options": [ "Boy", "Girl", @@ -1991,17 +1991,17 @@ CONFIG_METADATA_2 = { "azure_tts_rate": { "type": "string", "description": "语速设置", - "hint": "指示文本的讲出速率。可在字词或句子层面应用语速。 速率变化应为原始音频的 0.5 到 2 倍。", + "hint": "指示文本的讲出速率。可在字词或句子层面应用语速。 速率变化应为原始音频的 0.5 到 2 倍。", }, "azure_tts_volume": { "type": "string", "description": "语音音量设置", - "hint": "指示语音的音量级别。 可在句子层面应用音量的变化。以从 0.0 到 100.0(从最安静到最大声,例如 75)的数字表示。 默认值为 100.0。", + "hint": "指示语音的音量级别。 可在句子层面应用音量的变化。以从 0.0 到 100.0(从最安静到最大声,例如 75)的数字表示。 默认值为 100.0。", }, "azure_tts_region": { "type": "string", "description": "API 地区", - "hint": "Azure_TTS 处理数据所在区域,具体参考 https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/regions", + "hint": "Azure_TTS 处理数据所在区域,具体参考 https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/regions", "options": [ "southafricanorth", "eastasia", @@ -2038,18 +2038,18 @@ CONFIG_METADATA_2 = { "azure_tts_subscription_key": { "type": "string", "description": "服务订阅密钥", - "hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)", + "hint": "Azure_TTS 服务的订阅密钥(注意不是令牌)", }, "dashscope_tts_voice": {"description": "音色", "type": "string"}, "gm_resp_image_modal": { "description": "启用图片模态", "type": "bool", - "hint": "启用后,将支持返回图片内容。需要模型支持,否则会报错。具体支持模型请查看 Google Gemini 官方网站。温馨提示,如果您需要生成图片,请关闭 `启用群员识别` 配置获得更好的效果。", + "hint": "启用后,将支持返回图片内容。需要模型支持,否则会报错。具体支持模型请查看 Google Gemini 官方网站。温馨提示,如果您需要生成图片,请关闭 `启用群员识别` 配置获得更好的效果。", }, "gm_native_search": { "description": "启用原生搜索功能", "type": "bool", - "hint": "启用后所有函数工具将全部失效,免费次数限制请查阅官方文档", + "hint": "启用后所有函数工具将全部失效,免费次数限制请查阅官方文档", }, "gm_native_coderunner": { "description": "启用原生代码执行器", @@ -2064,7 +2064,7 @@ CONFIG_METADATA_2 = { "gm_safety_settings": { "description": "安全过滤器", "type": "object", - "hint": "设置模型输入的内容安全过滤级别。过滤级别分类为NONE(不屏蔽)、HIGH(高风险时屏蔽)、MEDIUM_AND_ABOVE(中等风险及以上屏蔽)、LOW_AND_ABOVE(低风险及以上时屏蔽),具体参见Gemini API文档。", + "hint": "设置模型输入的内容安全过滤级别。过滤级别分类为NONE(不屏蔽)、HIGH(高风险时屏蔽)、MEDIUM_AND_ABOVE(中等风险及以上屏蔽)、LOW_AND_ABOVE(低风险及以上时屏蔽),具体参见Gemini API文档。", "items": { "harassment": { "description": "骚扰内容", @@ -2080,7 +2080,7 @@ CONFIG_METADATA_2 = { "hate_speech": { "description": "仇恨言论", "type": "string", - "hint": "粗鲁、无礼或亵渎性质内容", + "hint": "粗鲁、无礼或亵渎性质内容", "options": [ "BLOCK_NONE", "BLOCK_ONLY_HIGH", @@ -2102,7 +2102,7 @@ CONFIG_METADATA_2 = { "dangerous_content": { "description": "危险内容", "type": "string", - "hint": "宣扬、助长或鼓励有害行为的信息", + "hint": "宣扬、助长或鼓励有害行为的信息", "options": [ "BLOCK_NONE", "BLOCK_ONLY_HIGH", @@ -2142,18 +2142,18 @@ CONFIG_METADATA_2 = { "description": "思考类型", "type": "string", "options": ["", "adaptive"], - "hint": "Opus 4.6+ / Sonnet 4.6+ 推荐设为 'adaptive'。留空则使用手动 budget 模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking", + "hint": "Opus 4.6+ / Sonnet 4.6+ 推荐设为 'adaptive'。留空则使用手动 budget 模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking", }, "budget": { "description": "思考预算", "type": "int", - "hint": "手动 budget_tokens,需 >= 1024。仅在 type 为空时生效。Opus 4.6 / Sonnet 4.6 上已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking", + "hint": "手动 budget_tokens,需 >= 1024。仅在 type 为空时生效。Opus 4.6 / Sonnet 4.6 上已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking", }, "effort": { "description": "思考深度", "type": "string", "options": ["", "low", "medium", "high", "max"], - "hint": "type 为 'adaptive' 时控制思考深度。默认 'high'。'max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort", + "hint": "type 为 'adaptive' 时控制思考深度。默认 'high'。'max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort", }, }, }, @@ -2165,7 +2165,7 @@ CONFIG_METADATA_2 = { "minimax-langboost": { "type": "string", "description": "指定语言/方言", - "hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现", + "hint": "增强对指定的小语种和方言的识别能力,设置后可以提升在指定小语种/方言场景下的语音表现", "options": [ "Chinese", "Chinese,Yue", @@ -2197,12 +2197,12 @@ CONFIG_METADATA_2 = { "minimax-voice-speed": { "type": "float", "description": "语速", - "hint": "生成声音的语速, 取值[0.5, 2], 默认为1.0, 取值越大,语速越快", + "hint": "生成声音的语速, 取值[0.5, 2], 默认为1.0, 取值越大,语速越快", }, "minimax-voice-vol": { "type": "float", "description": "音量", - "hint": "生成声音的音量, 取值(0, 10], 默认为1.0, 取值越大,音量越高", + "hint": "生成声音的音量, 取值(0, 10], 默认为1.0, 取值越大,音量越高", }, "minimax-voice-pitch": { "type": "int", @@ -2228,7 +2228,7 @@ CONFIG_METADATA_2 = { "minimax-voice-emotion": { "type": "string", "description": "情绪", - "hint": "控制合成语音的情绪。当为 auto 时,将根据文本内容自动选择情绪。", + "hint": "控制合成语音的情绪。当为 auto 时,将根据文本内容自动选择情绪。", "options": [ "auto", "happy", @@ -2250,58 +2250,58 @@ CONFIG_METADATA_2 = { "minimax-voice-english-normalization": { "type": "bool", "description": "支持英语文本规范化", - "hint": "可提升数字阅读场景的性能,但会略微增加延迟", + "hint": "可提升数字阅读场景的性能,但会略微增加延迟", }, "rag_options": { "description": "RAG 选项", "type": "object", - "hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)。阿里云百炼应用开启此功能后将无法多轮对话。", + "hint": "检索知识库设置, 非必填。仅 Agent 应用类型支持(智能体应用, 包括 RAG 应用)。阿里云百炼应用开启此功能后将无法多轮对话。", "items": { "pipeline_ids": { "description": "知识库 ID 列表", "type": "list", "items": {"type": "string"}, - "hint": "对指定知识库内所有文档进行检索, 前往 https://bailian.console.aliyun.com/ 数据应用->知识索引创建和获取 ID。", + "hint": "对指定知识库内所有文档进行检索, 前往 https://bailian.console.aliyun.com/ 数据应用->知识索引创建和获取 ID。", }, "file_ids": { - "description": "非结构化文档 ID, 传入该参数将对指定非结构化文档进行检索。", + "description": "非结构化文档 ID, 传入该参数将对指定非结构化文档进行检索。", "type": "list", "items": {"type": "string"}, - "hint": "对指定非结构化文档进行检索。前往 https://bailian.console.aliyun.com/ 数据管理创建和获取 ID。", + "hint": "对指定非结构化文档进行检索。前往 https://bailian.console.aliyun.com/ 数据管理创建和获取 ID。", }, "output_reference": { "description": "是否输出知识库/文档的引用", "type": "bool", - "hint": "在每次回答尾部加上引用源。默认为 False。", + "hint": "在每次回答尾部加上引用源。默认为 False。", }, }, }, "sensevoice_hint": { "description": "部署SenseVoice", "type": "string", - "hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", + "hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", }, "is_emotion": { "description": "情绪识别", "type": "bool", - "hint": "是否开启情绪识别。happy|sad|angry|neutral|fearful|disgusted|surprised|unknown", + "hint": "是否开启情绪识别。happy|sad|angry|neutral|fearful|disgusted|surprised|unknown", }, "stt_model": { "description": "模型名称", "type": "string", - "hint": "modelscope 上的模型名称。默认:iic/SenseVoiceSmall。", + "hint": "modelscope 上的模型名称。默认:iic/SenseVoiceSmall。", }, "variables": { "description": "工作流固定输入变量", "type": "object", "items": {}, - "hint": "可选。工作流固定输入变量,将会作为工作流的输入。也可以在对话时使用 /set 指令动态设置变量。如果变量名冲突,优先使用动态设置的变量。", + "hint": "可选。工作流固定输入变量,将会作为工作流的输入。也可以在对话时使用 /set 指令动态设置变量。如果变量名冲突,优先使用动态设置的变量。", "invisible": True, }, "dashscope_app_type": { "description": "应用类型", "type": "string", - "hint": "百炼应用的应用类型。", + "hint": "百炼应用的应用类型。", "options": [ "agent", "agent-arrange", @@ -2312,27 +2312,27 @@ CONFIG_METADATA_2 = { "timeout": { "description": "超时时间", "type": "int", - "hint": "超时时间,单位为秒。", + "hint": "超时时间,单位为秒。", }, "openai-tts-voice": { "description": "voice", "type": "string", - "hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'", + "hint": "OpenAI TTS 的声音。OpenAI 默认支持:'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'", }, "fishaudio-tts-character": { "description": "character", "type": "string", - "hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问:https://fish.audio/zh-CN/discovery", + "hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问:https://fish.audio/zh-CN/discovery", }, "fishaudio-tts-reference-id": { "description": "reference_id", "type": "string", - "hint": "fishaudio TTS 的参考模型ID(可选)。如果填入此字段,将直接使用模型ID而不通过角色名称查询。例如:626bb6d3f3364c9cbc3aa6a67300a664。更多模型请访问:https://fish.audio/zh-CN/discovery,进入模型详情界面后可复制模型ID", + "hint": "fishaudio TTS 的参考模型ID(可选)。如果填入此字段,将直接使用模型ID而不通过角色名称查询。例如:626bb6d3f3364c9cbc3aa6a67300a664。更多模型请访问:https://fish.audio/zh-CN/discovery,进入模型详情界面后可复制模型ID", }, "whisper_hint": { "description": "本地部署 Whisper 模型须知", "type": "string", - "hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", + "hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", }, "id": { "description": "ID", @@ -2369,79 +2369,79 @@ CONFIG_METADATA_2 = { "model": { "description": "模型 ID", "type": "string", - "hint": "模型名称,如 gpt-4o-mini, deepseek-chat。", + "hint": "模型名称,如 gpt-4o-mini, deepseek-chat。", }, "max_context_tokens": { "description": "模型上下文窗口大小", "type": "int", - "hint": "模型最大上下文 Token 大小。如果为 0,则会自动从模型元数据填充(如有),也可手动修改。", + "hint": "模型最大上下文 Token 大小。如果为 0,则会自动从模型元数据填充(如有),也可手动修改。", }, "dify_api_key": { "description": "API Key", "type": "string", - "hint": "Dify API Key。此项必填。", + "hint": "Dify API Key。此项必填。", }, "dify_api_base": { "description": "API Base URL", "type": "string", - "hint": "Dify API Base URL。默认为 https://api.dify.ai/v1", + "hint": "Dify API Base URL。默认为 https://api.dify.ai/v1", }, "dify_api_type": { "description": "Dify 应用类型", "type": "string", - "hint": "Dify API 类型。根据 Dify 官网,目前支持 chat, chatflow, agent, workflow 三种应用类型。", + "hint": "Dify API 类型。根据 Dify 官网,目前支持 chat, chatflow, agent, workflow 三种应用类型。", "options": ["chat", "chatflow", "agent", "workflow"], }, "dify_workflow_output_key": { "description": "Dify Workflow 输出变量名", "type": "string", - "hint": "Dify Workflow 输出变量名。当应用类型为 workflow 时才使用。默认为 astrbot_wf_output。", + "hint": "Dify Workflow 输出变量名。当应用类型为 workflow 时才使用。默认为 astrbot_wf_output。", }, "dify_query_input_key": { "description": "Prompt 输入变量名", "type": "string", - "hint": "发送的消息文本内容对应的输入变量名。默认为 astrbot_text_query。", + "hint": "发送的消息文本内容对应的输入变量名。默认为 astrbot_text_query。", "obvious": True, }, "coze_api_key": { "description": "Coze API Key", "type": "string", - "hint": "Coze API 密钥,用于访问 Coze 服务。", + "hint": "Coze API 密钥,用于访问 Coze 服务。", }, "bot_id": { "description": "Bot ID", "type": "string", - "hint": "Coze 机器人的 ID,在 Coze 平台上创建机器人后获得。", + "hint": "Coze 机器人的 ID,在 Coze 平台上创建机器人后获得。", }, "coze_api_base": { "description": "API Base URL", "type": "string", - "hint": "Coze API 的基础 URL 地址,默认为 https://api.coze.cn", + "hint": "Coze API 的基础 URL 地址,默认为 https://api.coze.cn", }, "deerflow_api_base": { "description": "API Base URL", "type": "string", - "hint": "DeerFlow API 网关地址,默认为 http://127.0.0.1:2026", + "hint": "DeerFlow API 网关地址,默认为 http://127.0.0.1:2026", }, "deerflow_api_key": { "description": "DeerFlow API Key", "type": "string", - "hint": "可选。若 DeerFlow 网关配置了 Bearer 鉴权,则在此填写。", + "hint": "可选。若 DeerFlow 网关配置了 Bearer 鉴权,则在此填写。", }, "deerflow_auth_header": { "description": "Authorization Header", "type": "string", - "hint": "可选。自定义 Authorization 请求头,优先级高于 DeerFlow API Key。", + "hint": "可选。自定义 Authorization 请求头,优先级高于 DeerFlow API Key。", }, "deerflow_assistant_id": { "description": "Assistant ID", "type": "string", - "hint": "LangGraph assistant_id,默认为 lead_agent。", + "hint": "LangGraph assistant_id,默认为 lead_agent。", }, "deerflow_model_name": { "description": "模型名称覆盖", "type": "string", - "hint": "可选。覆盖 DeerFlow 默认模型(对应 runtime context 的 model_name)。", + "hint": "可选。覆盖 DeerFlow 默认模型(对应 runtime context 的 model_name)。", }, "deerflow_thinking_enabled": { "description": "启用思考模式", @@ -2450,27 +2450,27 @@ CONFIG_METADATA_2 = { "deerflow_plan_mode": { "description": "启用计划模式", "type": "bool", - "hint": "对应 DeerFlow 的 is_plan_mode。", + "hint": "对应 DeerFlow 的 is_plan_mode。", }, "deerflow_subagent_enabled": { "description": "启用子智能体", "type": "bool", - "hint": "对应 DeerFlow 的 subagent_enabled。", + "hint": "对应 DeerFlow 的 subagent_enabled。", }, "deerflow_max_concurrent_subagents": { "description": "子智能体最大并发数", "type": "int", - "hint": "对应 DeerFlow 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。", + "hint": "对应 DeerFlow 的 max_concurrent_subagents。仅在启用子智能体时生效,默认 3。", }, "deerflow_recursion_limit": { "description": "递归深度上限", "type": "int", - "hint": "对应 LangGraph recursion_limit。", + "hint": "对应 LangGraph recursion_limit。", }, "auto_save_history": { "description": "由 Coze 管理对话记录", "type": "bool", - "hint": "启用后,将由 Coze 进行对话历史记录管理, 此时 AstrBot 本地保存的上下文不会生效(仅供浏览), 对 AstrBot 的上下文进行的操作也不会生效。如果为禁用, 则使用 AstrBot 管理上下文。", + "hint": "启用后,将由 Coze 进行对话历史记录管理, 此时 AstrBot 本地保存的上下文不会生效(仅供浏览), 对 AstrBot 的上下文进行的操作也不会生效。如果为禁用, 则使用 AstrBot 管理上下文。", }, }, }, @@ -2675,7 +2675,7 @@ CONFIG_METADATA_2 = { "description": "直连地址列表", "type": "list", "items": {"type": "string"}, - "hint": "在此处添加不希望通过代理访问的地址,例如内部服务地址。回车添加,可添加多个,如未设置代理请忽略此配置", + "hint": "在此处添加不希望通过代理访问的地址,例如内部服务地址。回车添加,可添加多个,如未设置代理请忽略此配置", }, "timezone": { "type": "string", @@ -2742,12 +2742,12 @@ CONFIG_METADATA_2 = { """ -v4.7.0 之后,name, description, hint 等字段已经实现 i18n 国际化。国际化资源文件位于: +v4.7.0 之后,name, description, hint 等字段已经实现 i18n 国际化。国际化资源文件位于: - dashboard/src/i18n/locales/en-US/features/config-metadata.json - dashboard/src/i18n/locales/zh-CN/features/config-metadata.json -如果在此文件中添加了新的配置字段,请务必同步更新上述两个国际化资源文件。 +如果在此文件中添加了新的配置字段,请务必同步更新上述两个国际化资源文件。 """ CONFIG_METADATA_3 = { "ai_group": { @@ -2755,7 +2755,7 @@ CONFIG_METADATA_3 = { "metadata": { "agent_runner": { "description": "Agent 执行方式", - "hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify、Coze、DeerFlow 等第三方 Agent 执行器,不需要修改此节。", + "hint": "选择 AI 对话的执行器,默认为 AstrBot 内置 Agent 执行器,可使用 AstrBot 内的知识库、人格、工具调用功能。如果不打算接入 Dify、Coze、DeerFlow 等第三方 Agent 执行器,不需要修改此节。", "type": "object", "items": { "provider_settings.enable": { @@ -2818,7 +2818,7 @@ CONFIG_METADATA_3 = { }, "ai": { "description": "模型", - "hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。", + "hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。", "type": "object", "items": { "provider_settings.default_provider_id": { @@ -2832,13 +2832,13 @@ CONFIG_METADATA_3 = { "type": "list", "items": {"type": "string"}, "_special": "select_providers", - "hint": "主聊天模型请求失败时,按顺序切换到这些模型。", + "hint": "主聊天模型请求失败时,按顺序切换到这些模型。", }, "provider_settings.default_image_caption_provider_id": { "description": "默认图片转述模型", "type": "string", "_special": "select_provider", - "hint": "留空代表不使用,可用于非多模态模型", + "hint": "留空代表不使用,可用于非多模态模型", }, "provider_stt_settings.enable": { "description": "启用语音转文本", @@ -2848,7 +2848,7 @@ CONFIG_METADATA_3 = { "provider_stt_settings.provider_id": { "description": "默认语音转文本模型", "type": "string", - "hint": "用户也可使用 /provider 指令单独选择会话的 STT 模型。", + "hint": "用户也可使用 /provider 指令单独选择会话的 STT 模型。", "_special": "select_provider_stt", "condition": { "provider_stt_settings.enable": True, @@ -2920,12 +2920,12 @@ CONFIG_METADATA_3 = { "kb_final_top_k": { "description": "最终返回结果数", "type": "int", - "hint": "从知识库中检索到的结果数量,越大可能获得越多相关信息,但也可能引入噪音。建议根据实际需求调整", + "hint": "从知识库中检索到的结果数量,越大可能获得越多相关信息,但也可能引入噪音。建议根据实际需求调整", }, "kb_agentic_mode": { "description": "Agentic 知识库检索", "type": "bool", - "hint": "启用后,知识库检索将作为 LLM Tool,由模型自主决定何时调用知识库进行查询。需要模型支持函数调用能力。", + "hint": "启用后,知识库检索将作为 LLM Tool,由模型自主决定何时调用知识库进行查询。需要模型支持函数调用能力。", }, }, "condition": { @@ -2954,7 +2954,7 @@ CONFIG_METADATA_3 = { "description": "Tavily API Key", "type": "list", "items": {"type": "string"}, - "hint": "可添加多个 Key 进行轮询。", + "hint": "可添加多个 Key 进行轮询。", "condition": { "provider_settings.websearch_provider": "tavily", "provider_settings.web_search": True, @@ -2964,7 +2964,7 @@ CONFIG_METADATA_3 = { "description": "BoCha API Key", "type": "list", "items": {"type": "string"}, - "hint": "可添加多个 Key 进行轮询。", + "hint": "可添加多个 Key 进行轮询。", "condition": { "provider_settings.websearch_provider": "bocha", "provider_settings.web_search": True, @@ -2973,7 +2973,7 @@ CONFIG_METADATA_3 = { "provider_settings.websearch_baidu_app_builder_key": { "description": "百度千帆智能云 APP Builder API Key", "type": "string", - "hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list", + "hint": "参考:https://console.bce.baidu.com/iam/#/iam/apikey/list", "condition": { "provider_settings.websearch_provider": "baidu_ai_search", }, @@ -3001,12 +3001,12 @@ CONFIG_METADATA_3 = { "type": "string", "options": ["none", "local", "sandbox"], "labels": ["无", "本地", "沙箱"], - "hint": "选择 Computer Use 运行环境。", + "hint": "选择 Computer Use 运行环境。", }, "provider_settings.computer_use_require_admin": { "description": "需要 AstrBot 管理员权限", "type": "bool", - "hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。", + "hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。", }, "provider_settings.sandbox.booter": { "description": "沙箱环境驱动器", @@ -3020,7 +3020,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_neo_endpoint": { "description": "Shipyard Neo API Endpoint", "type": "string", - "hint": "Shipyard Neo(Bay) 服务的 API 地址,默认 http://127.0.0.1:8114。", + "hint": "Shipyard Neo(Bay) 服务的 API 地址,默认 http://127.0.0.1:8114。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard_neo", @@ -3029,7 +3029,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_neo_access_token": { "description": "Shipyard Neo Access Token", "type": "string", - "hint": "Bay 的 API Key(sk-bay-...)。留空时自动从 credentials.json 发现。", + "hint": "Bay 的 API Key(sk-bay-...)。留空时自动从 credentials.json 发现。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard_neo", @@ -3038,7 +3038,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_neo_profile": { "description": "Shipyard Neo Profile", "type": "string", - "hint": "Shipyard Neo 沙箱 profile,如 python-default。", + "hint": "Shipyard Neo 沙箱 profile,如 python-default。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard_neo", @@ -3047,7 +3047,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_neo_ttl": { "description": "Shipyard Neo Sandbox TTL", "type": "int", - "hint": "Shipyard Neo 沙箱生存时间(秒)。", + "hint": "Shipyard Neo 沙箱生存时间(秒)。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard_neo", @@ -3056,7 +3056,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_endpoint": { "description": "Shipyard API Endpoint", "type": "string", - "hint": "Shipyard 服务的 API 访问地址。", + "hint": "Shipyard 服务的 API 访问地址。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard", @@ -3066,7 +3066,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_access_token": { "description": "Shipyard Access Token", "type": "string", - "hint": "用于访问 Shipyard 服务的访问令牌。", + "hint": "用于访问 Shipyard 服务的访问令牌。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard", @@ -3075,7 +3075,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_ttl": { "description": "Shipyard Session TTL", "type": "int", - "hint": "Shipyard 会话的生存时间(秒)。", + "hint": "Shipyard 会话的生存时间(秒)。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard", @@ -3084,7 +3084,7 @@ CONFIG_METADATA_3 = { "provider_settings.sandbox.shipyard_max_sessions": { "description": "Shipyard Max Sessions", "type": "int", - "hint": "Shipyard 最大会话数量。", + "hint": "Shipyard 最大会话数量。", "condition": { "provider_settings.computer_use_runtime": "sandbox", "provider_settings.sandbox.booter": "shipyard", @@ -3134,7 +3134,7 @@ CONFIG_METADATA_3 = { "provider_settings.proactive_capability.add_cron_tools": { "description": "启用", "type": "bool", - "hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务。", + "hint": "启用后,将会传递给 Agent 相关工具来实现主动型 Agent。你可以告诉 AstrBot 未来某个时间要做的事情,它将被定时触发然后执行任务。", }, }, "condition": { @@ -3150,7 +3150,7 @@ CONFIG_METADATA_3 = { "provider_settings.max_context_length": { "description": "最多携带对话轮数", "type": "int", - "hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制", + "hint": "超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条,-1 为不限制", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3176,7 +3176,7 @@ CONFIG_METADATA_3 = { "provider_settings.llm_compress_instruction": { "description": "上下文压缩提示词", "type": "text", - "hint": "如果为空则使用默认提示词。", + "hint": "如果为空则使用默认提示词。", "condition": { "provider_settings.context_limit_reached_strategy": "llm_compress", "provider_settings.agent_runner_type": "local", @@ -3185,7 +3185,7 @@ CONFIG_METADATA_3 = { "provider_settings.llm_compress_keep_recent": { "description": "压缩时保留最近对话轮数", "type": "int", - "hint": "始终保留的最近 N 轮对话。", + "hint": "始终保留的最近 N 轮对话。", "condition": { "provider_settings.context_limit_reached_strategy": "llm_compress", "provider_settings.agent_runner_type": "local", @@ -3195,7 +3195,7 @@ CONFIG_METADATA_3 = { "description": "用于上下文压缩的模型提供商 ID", "type": "string", "_special": "select_provider", - "hint": "留空时将降级为“按对话轮数截断”的策略。", + "hint": "留空时将降级为“按对话轮数截断”的策略。", "condition": { "provider_settings.context_limit_reached_strategy": "llm_compress", "provider_settings.agent_runner_type": "local", @@ -3226,7 +3226,7 @@ CONFIG_METADATA_3 = { "description": "不支持流式回复的平台", "type": "string", "options": ["realtime_segmenting", "turn_off"], - "hint": "选择在不支持流式回复的平台上的处理方式。实时分段回复会在系统接收流式响应检测到诸如标点符号等分段点时,立即发送当前已接收的内容", + "hint": "选择在不支持流式回复的平台上的处理方式。实时分段回复会在系统接收流式响应检测到诸如标点符号等分段点时,立即发送当前已接收的内容", "labels": ["实时分段回复", "关闭流式回复"], "condition": { "provider_settings.streaming_response": True, @@ -3235,13 +3235,13 @@ CONFIG_METADATA_3 = { "provider_settings.llm_safety_mode": { "description": "健康模式", "type": "bool", - "hint": "引导模型输出健康、安全的内容,避免有害或敏感话题。", + "hint": "引导模型输出健康、安全的内容,避免有害或敏感话题。", }, "provider_settings.safety_mode_strategy": { "description": "健康模式策略", "type": "string", "options": ["system_prompt"], - "hint": "选择健康模式的实现策略。", + "hint": "选择健康模式的实现策略。", "condition": { "provider_settings.llm_safety_mode": True, }, @@ -3249,17 +3249,17 @@ CONFIG_METADATA_3 = { "provider_settings.identifier": { "description": "用户识别", "type": "bool", - "hint": "启用后,会在提示词前包含用户 ID 信息。", + "hint": "启用后,会在提示词前包含用户 ID 信息。", }, "provider_settings.group_name_display": { "description": "显示群名称", "type": "bool", - "hint": "启用后,在支持的平台(OneBot v11)上会在提示词前包含群名称信息。", + "hint": "启用后,在支持的平台(OneBot v11)上会在提示词前包含群名称信息。", }, "provider_settings.datetime_system_prompt": { "description": "现实世界时间感知", "type": "bool", - "hint": "启用后,会在系统提示词中附带当前时间信息。", + "hint": "启用后,会在系统提示词中附带当前时间信息。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3274,7 +3274,7 @@ CONFIG_METADATA_3 = { "provider_settings.show_tool_call_result": { "description": "输出函数调用返回结果", "type": "bool", - "hint": "仅在输出函数调用状态启用时生效,展示结果前 70 个字符。", + "hint": "仅在输出函数调用状态启用时生效,展示结果前 70 个字符。", "condition": { "provider_settings.agent_runner_type": "local", "provider_settings.show_tool_use_status": True, @@ -3283,7 +3283,7 @@ CONFIG_METADATA_3 = { "provider_settings.sanitize_context_by_modalities": { "description": "按模型能力清理历史上下文", "type": "bool", - "hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)", + "hint": "开启后,在每次请求 LLM 前会按当前模型提供商中所选择的模型能力删除对话中不支持的图片/工具调用结构(会改变模型看到的历史)", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3296,7 +3296,7 @@ CONFIG_METADATA_3 = { }, }, "provider_settings.tool_call_timeout": { - "description": "工具调用超时时间(秒)", + "description": "工具调用超时时间(秒)", "type": "int", "condition": { "provider_settings.agent_runner_type": "local", @@ -3306,8 +3306,8 @@ CONFIG_METADATA_3 = { "description": "工具调用模式", "type": "string", "options": ["lazy_load", "full"], - "labels": ["Lazy Load(两阶段)", "Full(完整参数)"], - "hint": "Lazy Load 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。", + "labels": ["Lazy Load(两阶段)", "Full(完整参数)"], + "hint": "Lazy Load 先下发工具名称与描述,再下发参数;full 一次性下发完整参数。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3315,12 +3315,12 @@ CONFIG_METADATA_3 = { "provider_settings.wake_prefix": { "description": "LLM 聊天额外唤醒前缀 ", "type": "string", - "hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求", + "hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求", }, "provider_settings.prompt_prefix": { "description": "用户提示词", "type": "string", - "hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。", + "hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。", }, "provider_tts_settings.dual_output": { "description": "开启 TTS 时同时输出语音和文字内容", @@ -3329,12 +3329,12 @@ CONFIG_METADATA_3 = { "provider_settings.reachability_check": { "description": "提供商可达性检测", "type": "bool", - "hint": "/provider 命令列出模型时是否并发检测连通性。开启后会主动调用模型测试连通性,可能产生额外 token 消耗。", + "hint": "/provider 命令列出模型时是否并发检测连通性。开启后会主动调用模型测试连通性,可能产生额外 token 消耗。", }, "provider_settings.max_quoted_fallback_images": { "description": "引用图片回退解析上限", "type": "int", - "hint": "引用/转发消息回退解析图片时的最大注入数量,超出会截断。", + "hint": "引用/转发消息回退解析图片时的最大注入数量,超出会截断。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3342,7 +3342,7 @@ CONFIG_METADATA_3 = { "provider_settings.quoted_message_parser.max_component_chain_depth": { "description": "引用解析组件链深度", "type": "int", - "hint": "解析 Reply 组件链时允许的最大递归深度。", + "hint": "解析 Reply 组件链时允许的最大递归深度。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3350,7 +3350,7 @@ CONFIG_METADATA_3 = { "provider_settings.quoted_message_parser.max_forward_node_depth": { "description": "引用解析转发节点深度", "type": "int", - "hint": "解析合并转发节点时允许的最大递归深度。", + "hint": "解析合并转发节点时允许的最大递归深度。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3358,7 +3358,7 @@ CONFIG_METADATA_3 = { "provider_settings.quoted_message_parser.max_forward_fetch": { "description": "引用解析转发拉取上限", "type": "int", - "hint": "递归拉取 get_forward_msg 的最大次数。", + "hint": "递归拉取 get_forward_msg 的最大次数。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3366,7 +3366,7 @@ CONFIG_METADATA_3 = { "provider_settings.quoted_message_parser.warn_on_action_failure": { "description": "引用解析 action 失败告警", "type": "bool", - "hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。", + "hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。", "condition": { "provider_settings.agent_runner_type": "local", }, @@ -3393,7 +3393,7 @@ CONFIG_METADATA_3 = { "platform_settings.unique_session": { "description": "隔离会话", "type": "bool", - "hint": "启用后,群成员的上下文独立。", + "hint": "启用后,群成员的上下文独立。", }, "wake_prefix": { "description": "唤醒词", @@ -3427,7 +3427,7 @@ CONFIG_METADATA_3 = { "disable_builtin_commands": { "description": "禁用自带指令", "type": "bool", - "hint": "禁用所有 AstrBot 的自带指令,如 help, provider, model 等。", + "hint": "禁用所有 AstrBot 的自带指令,如 help, provider, model 等。", }, }, }, @@ -3438,18 +3438,18 @@ CONFIG_METADATA_3 = { "platform_settings.enable_id_white_list": { "description": "启用白名单", "type": "bool", - "hint": "启用后,只有在白名单内的会话会被响应。", + "hint": "启用后,只有在白名单内的会话会被响应。", }, "platform_settings.id_whitelist": { "description": "白名单 ID 列表", "type": "list", "items": {"type": "string"}, - "hint": "使用 /sid 获取 ID。当白名单列表为空时,代表不启用白名单(即所有 ID 都在白名单内)。", + "hint": "使用 /sid 获取 ID。当白名单列表为空时,代表不启用白名单(即所有 ID 都在白名单内)。", }, "platform_settings.id_whitelist_log": { "description": "输出日志", "type": "bool", - "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", + "hint": "启用后,当一条消息没通过白名单时,会输出 INFO 级别的日志。", }, "platform_settings.wl_ignore_admin_on_group": { "description": "管理员群组消息无视 ID 白名单", @@ -3491,7 +3491,7 @@ CONFIG_METADATA_3 = { "content_safety.baidu_aip.enable": { "description": "使用百度内容安全审核", "type": "bool", - "hint": "您需要手动安装 baidu-aip 库。", + "hint": "您需要手动安装 baidu-aip 库。", }, "content_safety.baidu_aip.app_id": { "description": "App ID", @@ -3522,7 +3522,7 @@ CONFIG_METADATA_3 = { "description": "额外关键词", "type": "list", "items": {"type": "string"}, - "hint": "额外的屏蔽关键词列表,支持正则表达式。", + "hint": "额外的屏蔽关键词列表,支持正则表达式。", }, }, }, @@ -3561,10 +3561,10 @@ CONFIG_METADATA_3 = { "type": "bool", }, "platform_specific.lark.pre_ack_emoji.emojis": { - "description": "表情列表(飞书表情枚举名)", + "description": "表情列表(飞书表情枚举名)", "type": "list", "items": {"type": "string"}, - "hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce", + "hint": "表情枚举名参考:https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce", "condition": { "platform_specific.lark.pre_ack_emoji.enable": True, }, @@ -3574,10 +3574,10 @@ CONFIG_METADATA_3 = { "type": "bool", }, "platform_specific.telegram.pre_ack_emoji.emojis": { - "description": "表情列表(Unicode)", + "description": "表情列表(Unicode)", "type": "list", "items": {"type": "string"}, - "hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9", + "hint": "Telegram 仅支持固定反应集合,参考:https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9", "condition": { "platform_specific.telegram.pre_ack_emoji.enable": True, }, @@ -3587,10 +3587,10 @@ CONFIG_METADATA_3 = { "type": "bool", }, "platform_specific.discord.pre_ack_emoji.emojis": { - "description": "表情列表(Unicode 或自定义表情名)", + "description": "表情列表(Unicode 或自定义表情名)", "type": "list", "items": {"type": "string"}, - "hint": "填写 Unicode 表情符号,例如:👍、🤔、⏳", + "hint": "填写 Unicode 表情符号,例如:👍、🤔、⏳", "condition": { "platform_specific.discord.pre_ack_emoji.enable": True, }, @@ -3609,7 +3609,7 @@ CONFIG_METADATA_3 = { "plugin_set": { "description": "可用插件", "type": "bool", - "hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。", + "hint": "默认启用全部未被禁用的插件。若插件在插件页面被禁用,则此处的选择不会生效。", "_special": "select_plugin_set", }, }, @@ -3632,15 +3632,15 @@ CONFIG_METADATA_3 = { "type": "bool", }, "platform_settings.segmented_reply.interval_method": { - "description": "间隔方法。", - "hint": "random 为随机时间,log 为根据消息长度计算,$y=log_(x)$,x为字数,y的单位为秒。", + "description": "间隔方法。", + "hint": "random 为随机时间,log 为根据消息长度计算,$y=log_(x)$,x为字数,y的单位为秒。", "type": "string", "options": ["random", "log"], }, "platform_settings.segmented_reply.interval": { "description": "随机间隔时间", "type": "string", - "hint": "格式:最小值,最大值(如:1.5,3.5)", + "hint": "格式:最小值,最大值(如:1.5,3.5)", "condition": { "platform_settings.segmented_reply.interval_method": "random", }, @@ -3648,14 +3648,14 @@ CONFIG_METADATA_3 = { "platform_settings.segmented_reply.log_base": { "description": "对数底数", "type": "float", - "hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。", + "hint": "对数间隔的底数,默认为 2.6。取值范围为 1.0-10.0。", "condition": { "platform_settings.segmented_reply.interval_method": "log", }, }, "platform_settings.segmented_reply.words_count_threshold": { "description": "分段回复字数阈值", - "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150", + "hint": "分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 150", "type": "int", }, "platform_settings.segmented_reply.split_mode": { @@ -3666,7 +3666,7 @@ CONFIG_METADATA_3 = { }, "platform_settings.segmented_reply.regex": { "description": "分段正则表达式", - "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'', text)", + "hint": "用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。re.findall(r'', text)", "type": "string", "condition": { "platform_settings.segmented_reply.split_mode": "regex", @@ -3675,7 +3675,7 @@ CONFIG_METADATA_3 = { "platform_settings.segmented_reply.split_words": { "description": "分段词列表", "type": "list", - "hint": "检测到列表中的任意词时进行分段,如:。、?、!等", + "hint": "检测到列表中的任意词时进行分段,如:。、?、!等", "condition": { "platform_settings.segmented_reply.split_mode": "words", }, @@ -3683,12 +3683,12 @@ CONFIG_METADATA_3 = { "platform_settings.segmented_reply.content_cleanup_rule": { "description": "内容过滤正则表达式", "type": "string", - "hint": "移除分段后内容中的指定内容。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。", + "hint": "移除分段后内容中的指定内容。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。", }, }, }, "ltm": { - "description": "群聊上下文感知(原聊天记忆增强)", + "description": "群聊上下文感知(原聊天记忆增强)", "type": "object", "items": { "provider_ltm_settings.group_icl_enable": { @@ -3702,13 +3702,13 @@ CONFIG_METADATA_3 = { "provider_ltm_settings.image_caption": { "description": "自动理解图片", "type": "bool", - "hint": "需要设置群聊图片转述模型。", + "hint": "需要设置群聊图片转述模型。", }, "provider_ltm_settings.image_caption_provider_id": { "description": "群聊图片转述模型", "type": "string", "_special": "select_provider", - "hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。", + "hint": "用于群聊上下文感知的图片理解,与默认图片转述模型分开配置。", "condition": { "provider_ltm_settings.image_caption": True, }, @@ -3738,7 +3738,7 @@ CONFIG_METADATA_3 = { "description": "主动回复白名单", "type": "list", "items": {"type": "string"}, - "hint": "为空时不启用白名单过滤。使用 /sid 获取 ID。", + "hint": "为空时不启用白名单过滤。使用 /sid 获取 ID。", "condition": { "provider_ltm_settings.active_reply.enable": True, }, @@ -3760,7 +3760,7 @@ CONFIG_METADATA_3_SYSTEM = { "t2i_strategy": { "description": "文本转图像策略", "type": "string", - "hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。", + "hint": "文本转图像策略。`remote` 为使用远程基于 HTML 的渲染服务,`local` 为使用 PIL 本地渲染。当使用 local 时,将 ttf 字体命名为 'font.ttf' 放在 data/ 目录下可自定义字体。", "options": ["remote", "local"], }, "t2i_endpoint": { @@ -3774,7 +3774,7 @@ CONFIG_METADATA_3_SYSTEM = { "t2i_template": { "description": "文本转图像自定义模版", "type": "bool", - "hint": "启用后可自定义 HTML 模板用于文转图渲染。", + "hint": "启用后可自定义 HTML 模板用于文转图渲染。", "condition": { "t2i_strategy": "remote", }, @@ -3783,97 +3783,97 @@ CONFIG_METADATA_3_SYSTEM = { "t2i_active_template": { "description": "当前应用的文转图渲染模板", "type": "string", - "hint": "此处的值由文转图模板管理页面进行维护。", + "hint": "此处的值由文转图模板管理页面进行维护。", "invisible": True, }, "log_level": { "description": "控制台日志级别", "type": "string", - "hint": "控制台输出日志的级别。", + "hint": "控制台输出日志的级别。", "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], }, "dashboard.ssl.enable": { "description": "启用 WebUI HTTPS", "type": "bool", - "hint": "启用后,WebUI 将直接使用 HTTPS 提供服务。", + "hint": "启用后,WebUI 将直接使用 HTTPS 提供服务。", }, "dashboard.ssl.cert_file": { "description": "SSL 证书文件路径", "type": "string", - "hint": "证书文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。", + "hint": "证书文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。", "condition": {"dashboard.ssl.enable": True}, }, "dashboard.ssl.key_file": { "description": "SSL 私钥文件路径", "type": "string", - "hint": "私钥文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。", + "hint": "私钥文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。", "condition": {"dashboard.ssl.enable": True}, }, "dashboard.ssl.ca_certs": { "description": "SSL CA 证书文件路径", "type": "string", - "hint": "可选。用于指定 CA 证书文件路径。", + "hint": "可选。用于指定 CA 证书文件路径。", "condition": {"dashboard.ssl.enable": True}, }, "log_file_enable": { "description": "启用文件日志", "type": "bool", - "hint": "开启后会将日志写入指定文件。", + "hint": "开启后会将日志写入指定文件。", }, "log_file_path": { "description": "日志文件路径", "type": "string", - "hint": "相对路径以 data 目录为基准,例如 logs/astrbot.log;支持绝对路径。", + "hint": "相对路径以 data 目录为基准,例如 logs/astrbot.log;支持绝对路径。", }, "log_file_max_mb": { "description": "日志文件大小上限 (MB)", "type": "int", - "hint": "超过大小后自动轮转,默认 20MB。", + "hint": "超过大小后自动轮转,默认 20MB。", }, "temp_dir_max_size": { "description": "临时目录大小上限 (MB)", "type": "int", - "hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。", + "hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。", }, "trace_log_enable": { "description": "启用 Trace 文件日志", "type": "bool", - "hint": "将 Trace 事件写入独立文件(不影响控制台输出)。", + "hint": "将 Trace 事件写入独立文件(不影响控制台输出)。", }, "trace_log_path": { "description": "Trace 日志文件路径", "type": "string", - "hint": "相对路径以 data 目录为基准,例如 logs/astrbot.trace.log;支持绝对路径。", + "hint": "相对路径以 data 目录为基准,例如 logs/astrbot.trace.log;支持绝对路径。", }, "trace_log_max_mb": { "description": "Trace 日志大小上限 (MB)", "type": "int", - "hint": "超过大小后自动轮转,默认 20MB。", + "hint": "超过大小后自动轮转,默认 20MB。", }, "pip_install_arg": { "description": "pip 安装额外参数", "type": "string", - "hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。", + "hint": "安装插件依赖时,会使用 Python 的 pip 工具。这里可以填写额外的参数,如 `--break-system-package` 等。", }, "pypi_index_url": { "description": "PyPI 软件仓库地址", "type": "string", - "hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/", + "hint": "安装 Python 依赖时请求的 PyPI 软件仓库地址。默认为 https://mirrors.aliyun.com/pypi/simple/", }, "callback_api_base": { "description": "对外可达的回调接口地址", "type": "string", - "hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。", + "hint": "外部服务可能会通过 AstrBot 生成的回调链接(如文件下载链接)访问 AstrBot 后端。由于 AstrBot 无法自动判断部署环境中对外可达的主机地址(host),因此需要通过此配置项显式指定 “外部服务如何访问 AstrBot” 的地址。如 http://localhost:6185,https://example.com 等。", }, "timezone": { "description": "时区", "type": "string", - "hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab", + "hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab", }, "http_proxy": { "description": "HTTP 代理", "type": "string", - "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", + "hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`", }, "no_proxy": { "description": "直连地址列表", diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py index cb6b6429b..d8bb5045a 100644 --- a/astrbot/core/config/i18n_utils.py +++ b/astrbot/core/config/i18n_utils.py @@ -16,13 +16,13 @@ class ConfigMetadataI18n: 生成国际化键 Args: - group: 配置组,如 'ai_group', 'platform_group' - section: 配置节,如 'agent_runner', 'general' - field: 字段名,如 'enable', 'default_provider' - attr: 属性类型,如 'description', 'hint', 'labels' + group: 配置组,如 'ai_group', 'platform_group' + section: 配置节,如 'agent_runner', 'general' + field: 字段名,如 'enable', 'default_provider' + attr: 属性类型,如 'description', 'hint', 'labels' Returns: - 国际化键,格式如: 'ai_group.agent_runner.enable.description' + 国际化键,格式如: 'ai_group.agent_runner.enable.description' """ if field: return f"{group}.{section}.{field}.{attr}" diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 2c282867f..fda70bb71 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -15,14 +15,14 @@ from astrbot.core.utils.datetime_utils import to_utc_timestamp class ConversationManager: - """负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。""" + """负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。""" def __init__(self, db_helper: BaseDatabase) -> None: self.session_conversations: dict[str, str] = {} self.db = db_helper self.save_interval = 60 # 每 60 秒保存一次 - # 会话删除回调函数列表(用于级联清理,如知识库配置) + # 会话删除回调函数列表(用于级联清理,如知识库配置) self._on_session_deleted_callbacks: list[Callable[[str], Awaitable[None]]] = [] def register_on_session_deleted( @@ -31,11 +31,11 @@ class ConversationManager: ) -> None: """注册会话删除回调函数. - 其他模块可以注册回调来响应会话删除事件,实现级联清理。 - 例如:知识库模块可以注册回调来清理会话的知识库配置。 + 其他模块可以注册回调来响应会话删除事件,实现级联清理。 + 例如:知识库模块可以注册回调来清理会话的知识库配置。 Args: - callback: 回调函数,接收会话ID (unified_msg_origin) 作为参数 + callback: 回调函数,接收会话ID (unified_msg_origin) 作为参数 """ self._on_session_deleted_callbacks.append(callback) @@ -83,16 +83,16 @@ class ConversationManager: title: str | None = None, persona_id: str | None = None, ) -> str: - """新建对话,并将当前会话的对话转移到新对话. + """新建对话,并将当前会话的对话转移到新对话. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id Returns: conversation_id (str): 对话 ID, 是 uuid 格式的字符串 """ if not platform_id: - # 如果没有提供 platform_id,则从 unified_msg_origin 中解析 + # 如果没有提供 platform_id,则从 unified_msg_origin 中解析 parts = unified_msg_origin.split(":") if len(parts) >= 3: platform_id = parts[0] @@ -115,7 +115,7 @@ class ConversationManager: """切换会话的对话 Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 """ @@ -127,10 +127,10 @@ class ConversationManager: unified_msg_origin: str, conversation_id: str | None = None, ) -> None: - """删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话 + """删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话 Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 """ @@ -147,21 +147,21 @@ class ConversationManager: """删除会话的所有对话 Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id """ await self.db.delete_conversations_by_user_id(user_id=unified_msg_origin) self.session_conversations.pop(unified_msg_origin, None) await sp.session_remove(unified_msg_origin, "sel_conv_id") - # 触发会话删除回调(级联清理) + # 触发会话删除回调(级联清理) await self._trigger_session_deleted(unified_msg_origin) async def get_curr_conversation_id(self, unified_msg_origin: str) -> str | None: """获取会话当前的对话 ID Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id Returns: conversation_id (str): 对话 ID, 是 uuid 格式的字符串 @@ -182,7 +182,7 @@ class ConversationManager: """获取会话的对话. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 create_if_not_exists (bool): 如果对话不存在,是否创建一个新的对话 Returns: @@ -191,7 +191,7 @@ class ConversationManager: """ conv = await self.db.get_conversation_by_id(cid=conversation_id) if not conv and create_if_not_exists: - # 如果对话不存在且需要创建,则新建一个对话 + # 如果对话不存在且需要创建,则新建一个对话 conversation_id = await self.new_conversation(unified_msg_origin) conv = await self.db.get_conversation_by_id(cid=conversation_id) conv_res = None @@ -207,7 +207,7 @@ class ConversationManager: """获取对话列表. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id,可选 + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id,可选 platform_id (str): 平台 ID, 可选参数, 用于过滤对话 Returns: conversations (List[Conversation]): 对话对象列表 @@ -267,14 +267,14 @@ class ConversationManager: """更新会话的对话. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 history (List[Dict]): 对话历史记录, 是一个字典列表, 每个字典包含 role 和 content 字段 - token_usage (int | None): token 使用量。None 表示不更新 + token_usage (int | None): token 使用量。None 表示不更新 """ if not conversation_id: - # 如果没有提供 conversation_id,则获取当前的 + # 如果没有提供 conversation_id,则获取当前的 conversation_id = await self.get_curr_conversation_id(unified_msg_origin) if conversation_id: await self.db.update_conversation( @@ -294,7 +294,7 @@ class ConversationManager: """更新会话的对话标题. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id title (str): 对话标题 conversation_id (str): 对话 ID, 是 uuid 格式的字符串 Deprecated: @@ -316,7 +316,7 @@ class ConversationManager: """更新会话的对话 Persona ID. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id persona_id (str): 对话 Persona ID conversation_id (str): 对话 ID, 是 uuid 格式的字符串 Deprecated: @@ -374,7 +374,7 @@ class ConversationManager: """获取人类可读的上下文. Args: - unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id + unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id conversation_id (str): 对话 ID, 是 uuid 格式的字符串 page (int): 页码 page_size (int): 每页大小 @@ -385,8 +385,8 @@ class ConversationManager: return [], 0 history = json.loads(conversation.history) - # contexts_groups 存放按顺序的段落(每个段落是一个 str 列表), - # 之后会被展平成一个扁平的 str 列表返回。 + # contexts_groups 存放按顺序的段落(每个段落是一个 str 列表), + # 之后会被展平成一个扁平的 str 列表返回。 contexts_groups: list[list[str]] = [] temp_contexts: list[str] = [] for record in history: diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index f18848296..88c06d054 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -1,7 +1,7 @@ -"""Astrbot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作. +"""Astrbot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作. -该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus等。 -该类还负责加载和执行插件, 以及处理事件总线的分发。 +该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus等。 +该类还负责加载和执行插件, 以及处理事件总线的分发。 工作流程: 1. 初始化所有组件 @@ -44,11 +44,11 @@ from .event_bus import EventBus class AstrBotCoreLifecycle: - """AstrBot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作. + """AstrBot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作. - 该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、 - EventBus 等。 - 该类还负责加载和执行插件, 以及处理事件总线的分发。 + 该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、 + EventBus 等。 + 该类还负责加载和执行插件, 以及处理事件总线的分发。 """ def __init__(self, log_broker: LogBroker, db: BaseDatabase) -> None: @@ -100,7 +100,7 @@ class AstrBotCoreLifecycle: async def initialize(self) -> None: """初始化 AstrBot 核心生命周期管理类. - 负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。 + 负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。 """ # 初始化日志代理 logger.info("AstrBot v" + VERSION) @@ -197,7 +197,7 @@ class AstrBotCoreLifecycle: # 初始化插件管理器 self.plugin_manager = PluginManager(self.star_context, self.astrbot_config) - # 扫描、注册插件、实例化插件类 + # 扫描、注册插件、实例化插件类 await self.plugin_manager.reload() # 根据配置实例化各个 Provider @@ -295,7 +295,7 @@ class AstrBotCoreLifecycle: 用load加载事件总线和任务并初始化, 执行启动完成事件钩子 """ self._load() - logger.info("AstrBot 启动完成。") + logger.info("AstrBot 启动完成。") # 执行启动完成事件钩子 handlers = star_handlers_registry.get_handlers_by_event_type( @@ -331,7 +331,7 @@ class AstrBotCoreLifecycle: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {plugin.name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", + f"插件 {plugin.name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", ) await self.provider_manager.terminate() diff --git a/astrbot/core/db/migration/helper.py b/astrbot/core/db/migration/helper.py index df460cb1c..06cd3cc1f 100644 --- a/astrbot/core/db/migration/helper.py +++ b/astrbot/core/db/migration/helper.py @@ -18,9 +18,9 @@ from .migra_3_to_4 import ( async def check_migration_needed_v4(db_helper: BaseDatabase) -> bool: """检查是否需要进行数据库迁移 - 如果存在 data_v3.db 并且 preference 中没有 migration_done_v4,则需要进行迁移。 + 如果存在 data_v3.db 并且 preference 中没有 migration_done_v4,则需要进行迁移。 """ - # 仅当 data 目录下存在旧版本数据(data_v3.db 文件)时才考虑迁移 + # 仅当 data 目录下存在旧版本数据(data_v3.db 文件)时才考虑迁移 data_dir = get_astrbot_data_path() data_v3_db = os.path.join(data_dir, "data_v3.db") @@ -42,8 +42,8 @@ async def do_migration_v4( astrbot_config: AstrBotConfig, ) -> None: """执行数据库迁移 - 迁移旧的 webchat_conversation 表到新的 conversation 表。 - 迁移旧的 platform 到新的 platform_stats 表。 + 迁移旧的 webchat_conversation 表到新的 conversation 表。 + 迁移旧的 platform 到新的 platform_stats 表。 """ if not await check_migration_needed_v4(db_helper): return @@ -68,4 +68,4 @@ async def do_migration_v4( # 标记迁移完成 await sp.put_async("global", "global", "migration_done_v4", True) - logger.info("数据库迁移完成。") + logger.info("数据库迁移完成。") diff --git a/astrbot/core/db/migration/migra_3_to_4.py b/astrbot/core/db/migration/migra_3_to_4.py index 727d97b29..cef7baae5 100644 --- a/astrbot/core/db/migration/migra_3_to_4.py +++ b/astrbot/core/db/migration/migra_3_to_4.py @@ -15,8 +15,8 @@ from .shared_preferences_v3 import sp as sp_v3 from .sqlite_v3 import SQLiteDatabase as SQLiteV3DatabaseV3 """ -1. 迁移旧的 webchat_conversation 表到新的 conversation 表。 -2. 迁移旧的 platform 到新的 platform_stats 表。 +1. 迁移旧的 webchat_conversation 表到新的 conversation 表。 +2. 迁移旧的 platform 到新的 platform_stats 表。 """ @@ -68,7 +68,7 @@ async def migration_conversation_table( ) if not conv: logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", + f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", ) continue if ":" not in conv.user_id: @@ -95,7 +95,7 @@ async def migration_conversation_table( f"迁移旧会话 {conversation.get('cid', 'unknown')} 失败: {e}", exc_info=True, ) - logger.info(f"成功迁移 {total_cnt} 条旧的会话数据到新表。") + logger.info(f"成功迁移 {total_cnt} 条旧的会话数据到新表。") async def migration_platform_table( @@ -110,13 +110,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(f"迁移旧平台数据,offset_sec: {offset_sec} 秒。") stats = db_helper_v3.get_base_stats(offset_sec=offset_sec) logger.info(f"迁移 {len(stats.platform)} 条旧的平台数据到新的表中...") platform_stats_v3 = stats.platform if not platform_stats_v3: - logger.info("没有找到旧平台数据,跳过迁移。") + logger.info("没有找到旧平台数据,跳过迁移。") return first_time_stamp = platform_stats_v3[0].timestamp @@ -174,7 +174,7 @@ async def migration_platform_table( f"迁移平台统计数据失败: {platform_id}, {platform_type}, 时间戳: {bucket_end}", exc_info=True, ) - logger.info(f"成功迁移 {len(platform_stats_v3)} 条旧的平台数据到新表。") + logger.info(f"成功迁移 {len(platform_stats_v3)} 条旧的平台数据到新表。") async def migration_webchat_data( @@ -206,7 +206,7 @@ async def migration_webchat_data( ) if not conv: logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", + f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", ) continue if ":" in conv.user_id: @@ -230,15 +230,15 @@ async def migration_webchat_data( exc_info=True, ) - logger.info(f"成功迁移 {total_cnt} 条旧的 WebChat 会话数据到新表。") + logger.info(f"成功迁移 {total_cnt} 条旧的 WebChat 会话数据到新表。") async def migration_persona_data( db_helper: BaseDatabase, astrbot_config: AstrBotConfig, ) -> None: - """迁移 Persona 数据到新的表中。 - 旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。 + """迁移 Persona 数据到新的表中。 + 旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。 """ v3_persona_config: list[dict] = astrbot_config.get("persona", []) total_personas = len(v3_persona_config) @@ -270,10 +270,10 @@ async def migration_persona_data( begin_dialogs=begin_dialogs, ) logger.info( - f"迁移 Persona {persona['name']}({persona_new.system_prompt[:30]}...) 到新表成功。", + f"迁移 Persona {persona['name']}({persona_new.system_prompt[:30]}...) 到新表成功。", ) except Exception as e: - logger.error(f"解析 Persona 配置失败:{e}") + logger.error(f"解析 Persona 配置失败:{e}") async def migration_preferences( @@ -293,7 +293,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(f"迁移全局偏好设置 {key} 成功,值: {value}") # 2. umo scope migration session_conversation = sp_v3.get("session_conversation", default={}) @@ -305,7 +305,7 @@ 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(f"迁移会话 {umo} 的对话数据到新表成功,平台 ID: {platform_id}") except Exception as e: logger.error(f"迁移会话 {umo} 的对话数据失败: {e}", exc_info=True) @@ -320,7 +320,7 @@ async def migration_preferences( await sp.put_async("umo", str(session), "session_service_config", config) - logger.info(f"迁移会话 {umo} 的服务配置到新表成功,平台 ID: {platform_id}") + logger.info(f"迁移会话 {umo} 的服务配置到新表成功,平台 ID: {platform_id}") except Exception as e: logger.error(f"迁移会话 {umo} 的服务配置失败: {e}", exc_info=True) @@ -353,7 +353,7 @@ async def migration_preferences( provider_id, ) logger.info( - f"迁移会话 {umo} 的提供商偏好到新表成功,平台 ID: {platform_id}", + f"迁移会话 {umo} 的提供商偏好到新表成功,平台 ID: {platform_id}", ) except Exception as e: logger.error(f"迁移会话 {umo} 的提供商偏好失败: {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 58736ab51..d36cc5fe8 100644 --- a/astrbot/core/db/migration/migra_45_to_46.py +++ b/astrbot/core/db/migration/migra_45_to_46.py @@ -13,7 +13,7 @@ async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> ) return - # 如果任何一项带有 umop,则说明需要迁移 + # 如果任何一项带有 umop,则说明需要迁移 need_migration = False for conf_id, conf_info in abconf_data.items(): if isinstance(conf_info, dict) and "umop" in conf_info: diff --git a/astrbot/core/db/migration/migra_token_usage.py b/astrbot/core/db/migration/migra_token_usage.py index 76bf8ce01..87931594e 100644 --- a/astrbot/core/db/migration/migra_token_usage.py +++ b/astrbot/core/db/migration/migra_token_usage.py @@ -24,9 +24,9 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: if migration_done: return - logger.info("开始执行数据库迁移(添加 conversations.token_usage 列)...") + logger.info("开始执行数据库迁移(添加 conversations.token_usage 列)...") - # 这里只适配了 SQLite。因为截止至这一版本,AstrBot 仅支持 SQLite。 + # 这里只适配了 SQLite。因为截止至这一版本,AstrBot 仅支持 SQLite。 try: async with db_helper.get_db() as session: @@ -36,7 +36,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("token_usage 列已存在,跳过迁移") await sp.put_async( "global", "global", "migration_done_token_usage_1", True ) diff --git a/astrbot/core/db/migration/migra_webchat_session.py b/astrbot/core/db/migration/migra_webchat_session.py index 46025fc64..ee84a6948 100644 --- a/astrbot/core/db/migration/migra_webchat_session.py +++ b/astrbot/core/db/migration/migra_webchat_session.py @@ -30,7 +30,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: if migration_done: return - logger.info("开始执行数据库迁移(WebChat 会话迁移)...") + logger.info("开始执行数据库迁移(WebChat 会话迁移)...") try: async with db_helper.get_db() as session: @@ -64,8 +64,8 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: existing_result = await session.execute(existing_query) existing_session_ids = {row[0] for row in existing_result.fetchall()} - # 查询 Conversations 表中的 title,用于设置 display_name - # 对于每个 user_id,对应的 conversation user_id 格式为: webchat:FriendMessage:webchat!astrbot!{user_id} + # 查询 Conversations 表中的 title,用于设置 display_name + # 对于每个 user_id,对应的 conversation user_id 格式为: webchat:FriendMessage:webchat!astrbot!{user_id} user_ids_to_query = [ f"webchat:FriendMessage:webchat!astrbot!{user_id}" for user_id, _, _, _ in webchat_users @@ -88,19 +88,19 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: # user_id 就是 webchat_conv_id (session_id) session_id = user_id - # sender_name 通常是 username,但可能为 None + # sender_name 通常是 username,但可能为 None creator = sender_name if sender_name else "guest" # 检查是否已经存在该会话 if session_id in existing_session_ids: - logger.debug(f"会话 {session_id} 已存在,跳过") + logger.debug(f"会话 {session_id} 已存在,跳过") skipped_count += 1 continue # 从 Conversations 表中获取 display_name display_name = title_map.get(user_id) - # 创建新的 PlatformSession(保留原有的时间戳) + # 创建新的 PlatformSession(保留原有的时间戳) new_session = PlatformSession( session_id=session_id, platform_id="webchat", @@ -118,7 +118,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: await session.commit() logger.info( - f"WebChat 会话迁移完成!成功迁移: {len(sessions_to_add)}, 跳过: {skipped_count}", + f"WebChat 会话迁移完成!成功迁移: {len(sessions_to_add)}, 跳过: {skipped_count}", ) else: logger.info("没有新会话需要迁移") diff --git a/astrbot/core/db/migration/sqlite_v3.py b/astrbot/core/db/migration/sqlite_v3.py index b326ebb44..eb4b9f486 100644 --- a/astrbot/core/db/migration/sqlite_v3.py +++ b/astrbot/core/db/migration/sqlite_v3.py @@ -10,14 +10,14 @@ from astrbot.core.db.po import Platform, Stats class Conversation: """LLM 对话存储 - 对于网页聊天,history 存储了包括指令、回复、图片等在内的所有消息。 - 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 + 对于网页聊天,history 存储了包括指令、回复、图片等在内的所有消息。 + 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 """ user_id: str cid: str history: str = "" - """字符串格式的列表。""" + """字符串格式的列表。""" created_at: int = 0 updated_at: int = 0 title: str = "" @@ -288,7 +288,7 @@ class SQLiteDatabase: return conversations def update_conversation(self, user_id: str, cid: str, history: str) -> None: - """更新对话,并且同时更新时间""" + """更新对话,并且同时更新时间""" updated_at = int(time.time()) self._exec_sql( """ @@ -328,7 +328,7 @@ class SQLiteDatabase: page: int = 1, page_size: int = 20, ) -> tuple[list[dict[str, Any]], int]: - """获取所有对话,支持分页,按更新时间降序排序""" + """获取所有对话,支持分页,按更新时间降序排序""" try: c = self.conn.cursor() except sqlite3.ProgrammingError: @@ -344,7 +344,7 @@ class SQLiteDatabase: # 计算偏移量 offset = (page - 1) * page_size - # 获取分页数据,按更新时间降序排序 + # 获取分页数据,按更新时间降序排序 c.execute( """ SELECT user_id, cid, created_at, updated_at, title, persona_id @@ -361,7 +361,7 @@ class SQLiteDatabase: for row in rows: user_id, cid, created_at, updated_at, title, persona_id = row - # 确保 cid 是字符串类型且至少有8个字符,否则使用一个默认值 + # 确保 cid 是字符串类型且至少有8个字符,否则使用一个默认值 safe_cid = str(cid) if cid else "unknown" display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid @@ -379,7 +379,7 @@ class SQLiteDatabase: return conversations, total_count except Exception as _: - # 返回空列表和0,确保即使出错也有有效的返回值 + # 返回空列表和0,确保即使出错也有有效的返回值 return [], 0 finally: c.close() @@ -477,7 +477,7 @@ class SQLiteDatabase: for row in rows: user_id, cid, created_at, updated_at, title, persona_id = row - # 确保 cid 是字符串类型,否则使用一个默认值 + # 确保 cid 是字符串类型,否则使用一个默认值 safe_cid = str(cid) if cid else "unknown" display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid @@ -495,7 +495,7 @@ class SQLiteDatabase: return conversations, total_count except Exception as _: - # 返回空列表和0,确保即使出错也有有效的返回值 + # 返回空列表和0,确保即使出错也有有效的返回值 return [], 0 finally: c.close() diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index 451f054f6..b972c5a1d 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -73,9 +73,9 @@ class ConversationV2(TimestampMixin, SQLModel, table=True): class PersonaFolder(TimestampMixin, SQLModel, table=True): - """Persona 文件夹,支持递归层级结构。 + """Persona 文件夹,支持递归层级结构。 - 用于组织和管理多个 Persona,类似于文件系统的目录结构。 + 用于组织和管理多个 Persona,类似于文件系统的目录结构。 """ __tablename__: str = "persona_folders" @@ -93,7 +93,7 @@ class PersonaFolder(TimestampMixin, SQLModel, table=True): ) name: str = Field(max_length=255, nullable=False) parent_id: str | None = Field(default=None, max_length=36) - """父文件夹ID,NULL表示根目录""" + """父文件夹ID,NULL表示根目录""" description: str | None = Field(default=None, sa_type=Text) sort_order: int = Field(default=0) @@ -129,7 +129,7 @@ class Persona(TimestampMixin, SQLModel, table=True): custom_error_message: str | None = Field(default=None, sa_type=Text) """Optional custom error message sent to end users when the agent request fails.""" folder_id: str | None = Field(default=None, max_length=36) - """所属文件夹ID,NULL 表示在根目录""" + """所属文件夹ID,NULL 表示在根目录""" sort_order: int = Field(default=0) """排序顺序""" @@ -439,10 +439,10 @@ class CommandConflict(TimestampMixin, SQLModel, table=True): class Conversation: """LLM 对话类 - 对于 WebChat,history 存储了包括指令、回复、图片等在内的所有消息。 - 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 + 对于 WebChat,history 存储了包括指令、回复、图片等在内的所有消息。 + 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 - 在 v4.0.0 版本及之后,WebChat 的历史记录被迁移至 `PlatformMessageHistory` 表中, + 在 v4.0.0 版本及之后,WebChat 的历史记录被迁移至 `PlatformMessageHistory` 表中, """ platform_id: str @@ -450,32 +450,32 @@ class Conversation: cid: str """对话 ID, 是 uuid 格式的字符串""" history: str = "" - """字符串格式的对话列表。""" + """字符串格式的对话列表。""" title: str | None = "" persona_id: str | None = "" created_at: int = 0 updated_at: int = 0 token_usage: int = 0 - """对话的总 token 数量。AstrBot 会保留最近一次 LLM 请求返回的总 token 数,方便统计。token_usage 可能为 0,表示未知。""" + """对话的总 token 数量。AstrBot 会保留最近一次 LLM 请求返回的总 token 数,方便统计。token_usage 可能为 0,表示未知。""" class Personality(TypedDict): - """LLM 人格类。 + """LLM 人格类。 - 在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。 + 在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。 """ prompt: str name: str begin_dialogs: list[str] mood_imitation_dialogs: list[str] - """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。""" + """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。""" tools: list[str] | None - """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" + """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" skills: list[str] | None - """Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills""" + """Skills 列表。None 表示使用所有 Skills,空列表表示不使用任何 Skills""" custom_error_message: str | None - """可选的人格自定义报错回复信息。配置后将优先发送给最终用户。""" + """可选的人格自定义报错回复信息。配置后将优先发送给最终用户。""" # cache _begin_dialogs_processed: list[dict] diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 811bda0c2..44ba5e131 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -55,17 +55,17 @@ class SQLiteDatabase(BaseDatabase): await conn.execute(text("PRAGMA temp_store=MEMORY")) await conn.execute(text("PRAGMA mmap_size=134217728")) await conn.execute(text("PRAGMA optimize")) - # 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容) + # 确保 personas 表有 folder_id、sort_order、skills 列(前向兼容) await self._ensure_persona_folder_columns(conn) await self._ensure_persona_skills_column(conn) await self._ensure_persona_custom_error_message_column(conn) await conn.commit() async def _ensure_persona_folder_columns(self, conn) -> None: - """确保 personas 表有 folder_id 和 sort_order 列。 + """确保 personas 表有 folder_id 和 sort_order 列。 - 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel - 的 metadata.create_all 自动创建这些列。 + 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel + 的 metadata.create_all 自动创建这些列。 """ result = await conn.execute(text("PRAGMA table_info(personas)")) columns = {row[1] for row in result.fetchall()} @@ -82,10 +82,10 @@ class SQLiteDatabase(BaseDatabase): ) async def _ensure_persona_skills_column(self, conn) -> None: - """确保 personas 表有 skills 列。 + """确保 personas 表有 skills 列。 - 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel - 的 metadata.create_all 自动创建这些列。 + 这是为了支持旧版数据库的平滑升级。新版数据库通过 SQLModel + 的 metadata.create_all 自动创建这些列。 """ result = await conn.execute(text("PRAGMA table_info(personas)")) columns = {row[1] for row in result.fetchall()} @@ -94,7 +94,7 @@ class SQLiteDatabase(BaseDatabase): await conn.execute(text("ALTER TABLE personas ADD COLUMN skills JSON")) async def _ensure_persona_custom_error_message_column(self, conn) -> None: - """确保 personas 表有 custom_error_message 列。""" + """确保 personas 表有 custom_error_message 列。""" result = await conn.execute(text("PRAGMA table_info(personas)")) columns = {row[1] for row in result.fetchall()} @@ -398,7 +398,7 @@ class SQLiteDatabase(BaseDatabase): result = await session.execute(result_query) rows = result.fetchall() - # 查询总数(应用相同的筛选条件) + # 查询总数(应用相同的筛选条件) count_base_query = ( select(func.count(col(Preference.scope_id))) .select_from(Preference) diff --git a/astrbot/core/db/vec_db/base.py b/astrbot/core/db/vec_db/base.py index 04f8903b1..9e709cdf7 100644 --- a/astrbot/core/db/vec_db/base.py +++ b/astrbot/core/db/vec_db/base.py @@ -19,7 +19,7 @@ class BaseVecDB: metadata: dict | None = None, id: str | None = None, ) -> int: - """插入一条文本和其对应向量,自动生成 ID 并保持一致性。""" + """插入一条文本和其对应向量,自动生成 ID 并保持一致性。""" ... @abc.abstractmethod @@ -33,10 +33,10 @@ class BaseVecDB: max_retries: int = 3, progress_callback=None, ) -> int: - """批量插入文本和其对应向量,自动生成 ID 并保持一致性。 + """批量插入文本和其对应向量,自动生成 ID 并保持一致性。 Args: - progress_callback: 进度回调函数,接收参数 (current, total) + progress_callback: 进度回调函数,接收参数 (current, total) """ ... @@ -50,7 +50,7 @@ class BaseVecDB: rerank: bool = False, metadata_filters: dict | None = None, ) -> list[Result]: - """搜索最相似的文档。 + """搜索最相似的文档。 Args: query (str): 查询文本 top_k (int): 返回的最相似文档的数量 @@ -61,7 +61,7 @@ class BaseVecDB: @abc.abstractmethod async def delete(self, doc_id: str) -> bool: - """删除指定文档。 + """删除指定文档。 Args: doc_id (str): 要删除的文档 ID Returns: 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 dc6977cf8..32d9d34d0 100644 --- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py @@ -2,7 +2,7 @@ try: import faiss except ModuleNotFoundError: raise ImportError( - "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", + "faiss 未安装。请使用 'pip install faiss-cpu' 或 'pip install faiss-gpu' 安装。", ) import os 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 3fca246ef..09463249b 100644 --- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py +++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py @@ -41,7 +41,7 @@ class FaissVecDB(BaseVecDB): metadata: dict | None = None, id: str | None = None, ) -> int: - """插入一条文本和其对应向量,自动生成 ID 并保持一致性。""" + """插入一条文本和其对应向量,自动生成 ID 并保持一致性。""" metadata = metadata or {} str_id = id or str(uuid.uuid4()) # 使用 UUID 作为原始 ID @@ -65,10 +65,10 @@ class FaissVecDB(BaseVecDB): max_retries: int = 3, progress_callback=None, ) -> list[int]: - """批量插入文本和其对应向量,自动生成 ID 并保持一致性。 + """批量插入文本和其对应向量,自动生成 ID 并保持一致性。 Args: - progress_callback: 进度回调函数,接收参数 (current, total) + progress_callback: 进度回调函数,接收参数 (current, total) """ metadatas = metadatas or [{} for _ in contents] @@ -108,13 +108,13 @@ class FaissVecDB(BaseVecDB): rerank: bool = False, metadata_filters: dict | None = None, ) -> list[Result]: - """搜索最相似的文档。 + """搜索最相似的文档。 Args: query (str): 查询文本 k (int): 返回的最相似文档的数量 fetch_k (int): 在根据 metadata 过滤前从 FAISS 中获取的数量 - rerank (bool): 是否使用重排序。这需要在实例化时提供 rerank_provider, 如果未提供并且 rerank 为 True, 不会抛出异常。 + rerank (bool): 是否使用重排序。这需要在实例化时提供 rerank_provider, 如果未提供并且 rerank 为 True, 不会抛出异常。 metadata_filters (dict): 元数据过滤器 Returns: @@ -166,7 +166,7 @@ class FaissVecDB(BaseVecDB): return top_k_results async def delete(self, doc_id: str) -> None: - """删除一条文档块(chunk)""" + """删除一条文档块(chunk)""" # 获得对应的 int id result = await self.document_storage.get_document_by_doc_id(doc_id) int_id = result["id"] if result else None diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index 1801f6ef9..eca4d65d4 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -8,7 +8,7 @@ import anyio class FileTokenService: - """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" + """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" def __init__(self, default_timeout: float = 300) -> None: self.lock = asyncio.Lock() @@ -32,11 +32,11 @@ class FileTokenService: async def register_file( self, file_path: str, expire_seconds: float | None = None ) -> str: - """向令牌服务注册一个文件。 + """向令牌服务注册一个文件。 Args: file_path(str): 文件路径 - expire_seconds(float): 超时时间,单位秒(可选) + expire_seconds(float): 超时时间,单位秒(可选) Returns: str: 一个单次令牌 @@ -53,10 +53,10 @@ class FileTokenService: if platform.system() == "Windows" and local_path.startswith("/"): local_path = local_path[1:] else: - # 如果没有 file:/// 前缀,则认为是普通路径 + # 如果没有 file:/// 前缀,则认为是普通路径 local_path = file_path except Exception: - # 解析失败时,按原路径处理 + # 解析失败时,按原路径处理 local_path = file_path async with self.lock: @@ -76,7 +76,7 @@ class FileTokenService: return file_token async def handle_file(self, file_token: str) -> str: - """根据令牌获取文件路径,使用后令牌失效。 + """根据令牌获取文件路径,使用后令牌失效。 Args: file_token(str): 注册时返回的令牌 diff --git a/astrbot/core/initial_loader.py b/astrbot/core/initial_loader.py index 3f836a4c4..2a95ac84b 100644 --- a/astrbot/core/initial_loader.py +++ b/astrbot/core/initial_loader.py @@ -1,4 +1,4 @@ -"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。 +"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。 工作流程: 1. 初始化核心生命周期, 传递数据库和日志代理实例到核心生命周期 @@ -15,7 +15,7 @@ from astrbot.dashboard.server import AstrBotDashboard class InitialLoader: - """AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。""" + """AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。""" def __init__(self, db: BaseDatabase, log_broker: LogBroker) -> None: self.db = db @@ -30,7 +30,7 @@ class InitialLoader: await core_lifecycle.initialize() except Exception as e: logger.critical(traceback.format_exc()) - logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!") + logger.critical(f"😭 初始化 AstrBot 失败:{e} !!!") return core_task = core_lifecycle.start() diff --git a/astrbot/core/knowledge_base/chunking/base.py b/astrbot/core/knowledge_base/chunking/base.py index a45d86ad1..0712b4df4 100644 --- a/astrbot/core/knowledge_base/chunking/base.py +++ b/astrbot/core/knowledge_base/chunking/base.py @@ -1,6 +1,6 @@ """文档分块器基类 -定义了文档分块处理的抽象接口。 +定义了文档分块处理的抽象接口。 """ from abc import ABC, abstractmethod @@ -9,7 +9,7 @@ from abc import ABC, abstractmethod class BaseChunker(ABC): """分块器基类 - 所有分块器都应该继承此类并实现 chunk 方法。 + 所有分块器都应该继承此类并实现 chunk 方法。 """ @abstractmethod diff --git a/astrbot/core/knowledge_base/chunking/fixed_size.py b/astrbot/core/knowledge_base/chunking/fixed_size.py index c0eb17865..b04c424f8 100644 --- a/astrbot/core/knowledge_base/chunking/fixed_size.py +++ b/astrbot/core/knowledge_base/chunking/fixed_size.py @@ -1,6 +1,6 @@ """固定大小分块器 -按照固定的字符数将文本分块,支持重叠区域。 +按照固定的字符数将文本分块,支持重叠区域。 """ from .base import BaseChunker @@ -9,7 +9,7 @@ from .base import BaseChunker class FixedSizeChunker(BaseChunker): """固定大小分块器 - 按照固定的字符数分块,并支持块之间的重叠。 + 按照固定的字符数分块,并支持块之间的重叠。 """ def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50) -> None: diff --git a/astrbot/core/knowledge_base/chunking/recursive.py b/astrbot/core/knowledge_base/chunking/recursive.py index e27ffbd1b..542af8063 100644 --- a/astrbot/core/knowledge_base/chunking/recursive.py +++ b/astrbot/core/knowledge_base/chunking/recursive.py @@ -19,7 +19,7 @@ class RecursiveCharacterChunker(BaseChunker): chunk_overlap: 每个文本块之间的重叠部分大小 length_function: 计算文本长度的函数 is_separator_regex: 分隔符是否为正则表达式 - separators: 用于分割文本的分隔符列表,按优先级排序 + separators: 用于分割文本的分隔符列表,按优先级排序 """ self.chunk_size = chunk_size @@ -27,12 +27,12 @@ class RecursiveCharacterChunker(BaseChunker): self.length_function = length_function self.is_separator_regex = is_separator_regex - # 默认分隔符列表,按优先级从高到低 + # 默认分隔符列表,按优先级从高到低 self.separators = separators or [ "\n\n", # 段落 "\n", # 换行 - "。", # 中文句子 - ",", # 中文逗号 + "。", # 中文句子 + ",", # 中文逗号 ". ", # 句子 ", ", # 逗号分隔 " ", # 单词 @@ -67,7 +67,7 @@ class RecursiveCharacterChunker(BaseChunker): if separator in text: splits = text.split(separator) - # 重新添加分隔符(除了最后一个片段) + # 重新添加分隔符(除了最后一个片段) splits = [s + separator for s in splits[:-1]] + [splits[-1]] splits = [s for s in splits if s] if len(splits) == 1: @@ -81,7 +81,7 @@ class RecursiveCharacterChunker(BaseChunker): for split in splits: split_length = self.length_function(split) - # 如果单个分割部分已经超过了chunk_size,需要递归分割 + # 如果单个分割部分已经超过了chunk_size,需要递归分割 if split_length > chunk_size: # 先处理当前积累的块 if current_chunk: diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index 4b9dcf7dd..8dd3bcd52 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -272,7 +272,7 @@ class KBSQLiteDatabase: return {} metadata_map: dict[str, dict] = {} - # SQLite 参数上限为 999,分片查询避免超限 + # SQLite 参数上限为 999,分片查询避免超限 chunk_size = 900 doc_id_list = list(doc_ids) diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index c6b86d1cd..d304260af 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -60,7 +60,7 @@ async def _repair_and_translate_chunk_with_retry( """ Repairs, translates, and optionally re-chunks a single text chunk using the small LLM, with rate limiting. """ - # 为了防止 LLM 上下文污染,在 user_prompt 中也加入明确的指令 + # 为了防止 LLM 上下文污染,在 user_prompt 中也加入明确的指令 user_prompt = f"""IGNORE ALL PREVIOUS INSTRUCTIONS. Your ONLY task is to process the following text chunk according to the system prompt provided. Text chunk to process: @@ -212,7 +212,7 @@ class KBHelper: progress_callback=None, pre_chunked_text: list[str] | None = None, ) -> KBDocument: - """上传并处理文档(带原子性保证和失败清理) + """上传并处理文档(带原子性保证和失败清理) 流程: 1. 保存原始文件 @@ -220,11 +220,11 @@ class KBHelper: 3. 提取多媒体资源 4. 分块处理 5. 生成向量并存储 - 6. 保存元数据(事务) + 6. 保存元数据(事务) 7. 更新统计 Args: - progress_callback: 进度回调函数,接收参数 (stage, current, total) + progress_callback: 进度回调函数,接收参数 (stage, current, total) - stage: 当前阶段 ('parsing', 'chunking', 'embedding') - current: 当前进度 - total: 总数 @@ -244,15 +244,15 @@ class KBHelper: saved_media = [] if pre_chunked_text is not None: - # 如果提供了预分块文本,直接使用 + # 如果提供了预分块文本,直接使用 chunks_text = pre_chunked_text file_size = sum(len(chunk) for chunk in chunks_text) - logger.info(f"使用预分块文本进行上传,共 {len(chunks_text)} 个块。") + logger.info(f"使用预分块文本进行上传,共 {len(chunks_text)} 个块。") else: - # 否则,执行标准的文件解析和分块流程 + # 否则,执行标准的文件解析和分块流程 if file_content is None: raise ValueError( - "当未提供 pre_chunked_text 时,file_content 不能为空。" + "当未提供 pre_chunked_text 时,file_content 不能为空。" ) file_size = len(file_content) @@ -305,7 +305,7 @@ class KBHelper: if progress_callback: await progress_callback("chunking", 100, 100) - # 阶段3: 生成向量(带进度回调) + # 阶段3: 生成向量(带进度回调) async def embedding_progress_callback(current, total) -> None: if progress_callback: await progress_callback("embedding", current, total) @@ -492,7 +492,7 @@ class KBHelper: enable_cleaning: bool = False, cleaning_provider_id: str | None = None, ) -> KBDocument: - """从 URL 上传并处理文档(带原子性保证和失败清理) + """从 URL 上传并处理文档(带原子性保证和失败清理) Args: url: 要提取内容的网页 URL chunk_size: 文本块大小 @@ -500,7 +500,7 @@ class KBHelper: batch_size: 批处理大小 tasks_limit: 并发任务限制 max_retries: 最大重试次数 - progress_callback: 进度回调函数,接收参数 (stage, current, total) + progress_callback: 进度回调函数,接收参数 (stage, current, total) - stage: 当前阶段 ('extracting', 'cleaning', 'parsing', 'chunking', 'embedding') - current: 当前进度 - total: 总数 @@ -549,7 +549,7 @@ class KBHelper: if enable_cleaning and not final_chunks: raise ValueError( - "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。" + "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。" ) # 创建一个虚拟文件名 @@ -557,7 +557,7 @@ class KBHelper: if not Path(file_name).suffix: file_name += ".url" - # 复用现有的 upload_document 方法,但传入预分块文本 + # 复用现有的 upload_document 方法,但传入预分块文本 return await self.upload_document( file_name=file_name, file_content=None, @@ -583,12 +583,12 @@ class KBHelper: chunk_overlap: int = 50, ) -> list[str]: """ - 对从 URL 获取的内容进行清洗、修复、翻译和重新分块。 + 对从 URL 获取的内容进行清洗、修复、翻译和重新分块。 """ if not enable_cleaning: - # 如果不启用清洗,则使用从前端传递的参数进行分块 + # 如果不启用清洗,则使用从前端传递的参数进行分块 logger.info( - f"内容清洗未启用,使用指定参数进行分块: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}" + f"内容清洗未启用,使用指定参数进行分块: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}" ) return await self.chunker.chunk( content, chunk_size=chunk_size, chunk_overlap=chunk_overlap @@ -596,7 +596,7 @@ class KBHelper: if not cleaning_provider_id: logger.warning( - "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。" + "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。" ) return await self.chunker.chunk(content) @@ -612,14 +612,14 @@ class KBHelper: ) # 初步分块 - # 优化分隔符,优先按段落分割,以获得更高质量的文本块 + # 优化分隔符,优先按段落分割,以获得更高质量的文本块 text_splitter = RecursiveCharacterChunker( chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=["\n\n", "\n", " "], # 优先使用段落分隔符 ) initial_chunks = await text_splitter.chunk(content) - logger.info(f"初步分块完成,生成 {len(initial_chunks)} 个块用于修复。") + logger.info(f"初步分块完成,生成 {len(initial_chunks)} 个块用于修复。") # 并发处理所有块 rate_limiter = RateLimiter(repair_max_rpm) @@ -635,13 +635,13 @@ class KBHelper: final_chunks = [] for i, result in enumerate(repaired_results): if isinstance(result, Exception): - logger.warning(f"块 {i} 处理异常: {result!s}. 回退到原始块。") + logger.warning(f"块 {i} 处理异常: {result!s}. 回退到原始块。") final_chunks.append(initial_chunks[i]) elif isinstance(result, list): final_chunks.extend(result) logger.info( - f"文本修复完成: {len(initial_chunks)} 个原始块 -> {len(final_chunks)} 个最终块。" + f"文本修复完成: {len(initial_chunks)} 个原始块 -> {len(final_chunks)} 个最终块。" ) if progress_callback: @@ -651,5 +651,5 @@ class KBHelper: except Exception as e: logger.error(f"使用 Provider '{cleaning_provider_id}' 清洗内容失败: {e}") - # 清洗失败,返回默认分块结果,保证流程不中断 + # 清洗失败,返回默认分块结果,保证流程不中断 return await self.chunker.chunk(content) diff --git a/astrbot/core/knowledge_base/models.py b/astrbot/core/knowledge_base/models.py index da919a384..8b6c1f786 100644 --- a/astrbot/core/knowledge_base/models.py +++ b/astrbot/core/knowledge_base/models.py @@ -11,7 +11,7 @@ class BaseKBModel(SQLModel, table=False): class KnowledgeBase(BaseKBModel, table=True): """知识库表 - 存储知识库的基本信息和统计数据。 + 存储知识库的基本信息和统计数据。 """ __tablename__ = "knowledge_bases" # type: ignore @@ -59,7 +59,7 @@ class KnowledgeBase(BaseKBModel, table=True): class KBDocument(BaseKBModel, table=True): """文档表 - 存储上传到知识库的文档元数据。 + 存储上传到知识库的文档元数据。 """ __tablename__ = "kb_documents" # type: ignore @@ -93,7 +93,7 @@ class KBDocument(BaseKBModel, table=True): class KBMedia(BaseKBModel, table=True): """多媒体资源表 - 存储从文档中提取的图片、视频等多媒体资源。 + 存储从文档中提取的图片、视频等多媒体资源。 """ __tablename__ = "kb_media" # type: ignore diff --git a/astrbot/core/knowledge_base/parsers/base.py b/astrbot/core/knowledge_base/parsers/base.py index 4ffca9c6f..e819bbb43 100644 --- a/astrbot/core/knowledge_base/parsers/base.py +++ b/astrbot/core/knowledge_base/parsers/base.py @@ -1,6 +1,6 @@ """文档解析器基类和数据结构 -定义了文档解析器的抽象接口和相关数据类。 +定义了文档解析器的抽象接口和相关数据类。 """ from abc import ABC, abstractmethod @@ -11,7 +11,7 @@ from dataclasses import dataclass class MediaItem: """多媒体项 - 表示从文档中提取的多媒体资源。 + 表示从文档中提取的多媒体资源。 """ media_type: str # image, video @@ -24,7 +24,7 @@ class MediaItem: class ParseResult: """解析结果 - 包含解析后的文本内容和提取的多媒体资源。 + 包含解析后的文本内容和提取的多媒体资源。 """ text: str @@ -34,7 +34,7 @@ class ParseResult: class BaseParser(ABC): """文档解析器基类 - 所有文档解析器都应该继承此类并实现 parse 方法。 + 所有文档解析器都应该继承此类并实现 parse 方法。 """ @abstractmethod diff --git a/astrbot/core/knowledge_base/parsers/pdf_parser.py b/astrbot/core/knowledge_base/parsers/pdf_parser.py index 15e754baa..0b3b66b93 100644 --- a/astrbot/core/knowledge_base/parsers/pdf_parser.py +++ b/astrbot/core/knowledge_base/parsers/pdf_parser.py @@ -1,6 +1,6 @@ """PDF 文件解析器 -支持解析 PDF 文件中的文本和图片资源。 +支持解析 PDF 文件中的文本和图片资源。 """ import io @@ -17,7 +17,7 @@ from astrbot.core.knowledge_base.parsers.base import ( class PDFParser(BaseParser): """PDF 文档解析器 - 提取 PDF 中的文本内容和嵌入的图片资源。 + 提取 PDF 中的文本内容和嵌入的图片资源。 """ async def parse(self, file_content: bytes, file_name: str) -> ParseResult: diff --git a/astrbot/core/knowledge_base/parsers/text_parser.py b/astrbot/core/knowledge_base/parsers/text_parser.py index bed2d09b8..5130c633d 100644 --- a/astrbot/core/knowledge_base/parsers/text_parser.py +++ b/astrbot/core/knowledge_base/parsers/text_parser.py @@ -1,6 +1,6 @@ """文本文件解析器 -支持解析 TXT 和 Markdown 文件。 +支持解析 TXT 和 Markdown 文件。 """ from astrbot.core.knowledge_base.parsers.base import BaseParser, ParseResult @@ -9,13 +9,13 @@ from astrbot.core.knowledge_base.parsers.base import BaseParser, ParseResult class TextParser(BaseParser): """TXT/MD 文本解析器 - 支持多种字符编码的自动检测。 + 支持多种字符编码的自动检测。 """ async def parse(self, file_content: bytes, file_name: str) -> ParseResult: """解析文本文件 - 尝试使用多种编码解析文件内容。 + 尝试使用多种编码解析文件内容。 Args: file_content: 文件内容 diff --git a/astrbot/core/knowledge_base/parsers/url_parser.py b/astrbot/core/knowledge_base/parsers/url_parser.py index 2867164a9..84b965215 100644 --- a/astrbot/core/knowledge_base/parsers/url_parser.py +++ b/astrbot/core/knowledge_base/parsers/url_parser.py @@ -4,7 +4,7 @@ import aiohttp class URLExtractor: - """URL 内容提取器,封装了 Tavily API 调用和密钥管理""" + """URL 内容提取器,封装了 Tavily API 调用和密钥管理""" def __init__(self, tavily_keys: list[str]) -> None: """ @@ -21,7 +21,7 @@ class URLExtractor: self.tavily_key_lock = asyncio.Lock() async def _get_tavily_key(self) -> str: - """并发安全的从列表中获取并轮换Tavily API密钥。""" + """并发安全的从列表中获取并轮换Tavily API密钥。""" async with self.tavily_key_lock: key = self.tavily_keys[self.tavily_key_index] self.tavily_key_index = (self.tavily_key_index + 1) % len(self.tavily_keys) @@ -29,9 +29,9 @@ class URLExtractor: async def extract_text_from_url(self, url: str) -> str: """ - 使用 Tavily API 从 URL 提取主要文本内容。 - 这是 web_searcher 插件中 tavily_extract_web_page 方法的简化版本, - 专门为知识库模块设计,不依赖 AstrMessageEvent。 + 使用 Tavily API 从 URL 提取主要文本内容。 + 这是 web_searcher 插件中 tavily_extract_web_page 方法的简化版本, + 专门为知识库模块设计,不依赖 AstrMessageEvent。 Args: url: 要提取内容的网页 URL @@ -64,7 +64,7 @@ class URLExtractor: api_url, json=payload, headers=headers, - timeout=30.0, # 增加超时时间,因为内容提取可能需要更长时间 + timeout=30.0, # 增加超时时间,因为内容提取可能需要更长时间 ) as response: if response.status != 200: reason = await response.text() @@ -87,10 +87,10 @@ class URLExtractor: raise OSError(f"Failed to extract content from URL {url}: {e}") from e -# 为了向后兼容,提供一个简单的函数接口 +# 为了向后兼容,提供一个简单的函数接口 async def extract_text_from_url(url: str, tavily_keys: list[str]) -> str: """ - 简单的函数接口,用于从 URL 提取文本内容 + 简单的函数接口,用于从 URL 提取文本内容 Args: url: 要提取内容的网页 URL diff --git a/astrbot/core/knowledge_base/retrieval/manager.py b/astrbot/core/knowledge_base/retrieval/manager.py index 1244e18af..8d12a24f7 100644 --- a/astrbot/core/knowledge_base/retrieval/manager.py +++ b/astrbot/core/knowledge_base/retrieval/manager.py @@ -1,6 +1,6 @@ """检索管理器 -协调稠密检索、稀疏检索和 Rerank,提供统一的检索接口 +协调稠密检索、稀疏检索和 Rerank,提供统一的检索接口 """ import time @@ -35,7 +35,7 @@ class RetrievalManager: """检索管理器 职责: - - 协调稠密检索、稀疏检索和 Rerank + - 协调稠密检索、稀疏检索和 Rerank - 结果融合和排序 """ @@ -201,7 +201,7 @@ class RetrievalManager: ): """稠密检索 (向量相似度) - 为每个知识库使用独立的向量数据库进行检索,然后合并结果。 + 为每个知识库使用独立的向量数据库进行检索,然后合并结果。 Args: query: 查询文本 diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 3dd0719b1..0bee30110 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -1,4 +1,4 @@ -"""日志系统,统一将标准 logging 输出转发到 loguru。""" +"""日志系统,统一将标准 logging 输出转发到 loguru。""" import asyncio import logging @@ -21,7 +21,7 @@ if TYPE_CHECKING: class _RecordEnricherFilter(logging.Filter): - """为 logging.LogRecord 注入 AstrBot 日志字段。""" + """为 logging.LogRecord 注入 AstrBot 日志字段。""" def filter(self, record: logging.LogRecord) -> bool: record.plugin_tag = "[Plug]" if _is_plugin_path(record.pathname) else "[Core]" @@ -94,7 +94,7 @@ _loguru = _raw_loguru_logger.patch(_patch_record) class _LoguruInterceptHandler(logging.Handler): - """将 logging 记录转发到 loguru。""" + """将 logging 记录转发到 loguru。""" def emit(self, record: logging.LogRecord) -> None: try: @@ -124,7 +124,7 @@ class _LoguruInterceptHandler(logging.Handler): class LogBroker: - """日志代理类,用于缓存和分发日志消息。""" + """日志代理类,用于缓存和分发日志消息。""" def __init__(self) -> None: self.log_cache = deque(maxlen=CACHED_SIZE) @@ -148,7 +148,7 @@ class LogBroker: class LogQueueHandler(logging.Handler): - """日志处理器,用于将日志消息发送到 LogBroker。""" + """日志处理器,用于将日志消息发送到 LogBroker。""" def __init__(self, log_broker: LogBroker) -> None: super().__init__() diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 3f8928fee..e75b837b0 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -93,7 +93,7 @@ class BaseMessageComponent(BaseModel): return {"type": self.type.lower(), "data": data} async def to_dict(self) -> dict: - # 默认情况下,回退到旧的同步 toDict() + # 默认情况下,回退到旧的同步 toDict() return self.toDict() @@ -155,10 +155,10 @@ class Record(BaseMessageComponent): return Record(file=f"base64://{bs64_data}", **_) async def convert_to_file_path(self) -> str: - """将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。 + """将这个语音统一转换为本地文件路径。这个方法避免了手动判断语音数据类型,直接返回语音数据的本地路径(如果是网络 URL, 则会自动进行下载)。 Returns: - str: 语音的本地路径,以绝对路径表示。 + str: 语音的本地路径,以绝对路径表示。 """ if not self.file: @@ -182,10 +182,10 @@ class Record(BaseMessageComponent): raise Exception(f"not a valid file: {self.file}") async def convert_to_base64(self) -> str: - """将语音统一转换为 base64 编码。这个方法避免了手动判断语音数据类型,直接返回语音数据的 base64 编码。 + """将语音统一转换为 base64 编码。这个方法避免了手动判断语音数据类型,直接返回语音数据的 base64 编码。 Returns: - str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。 + str: 语音的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。 """ # convert to base64 @@ -206,7 +206,7 @@ class Record(BaseMessageComponent): return bs64_data async def register_to_file_service(self) -> str: - """将语音注册到文件服务。 + """将语音注册到文件服务。 Returns: str: 注册后的URL @@ -218,13 +218,13 @@ class Record(BaseMessageComponent): callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception("未配置 callback_api_base,文件服务不可用") 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(f"已注册:{callback_host}/api/file/{token}") return f"{callback_host}/api/file/{token}" @@ -251,10 +251,10 @@ class Video(BaseMessageComponent): raise Exception("not a valid url") async def convert_to_file_path(self) -> str: - """将这个视频统一转换为本地文件路径。这个方法避免了手动判断视频数据类型,直接返回视频数据的本地路径(如果是网络 URL,则会自动进行下载)。 + """将这个视频统一转换为本地文件路径。这个方法避免了手动判断视频数据类型,直接返回视频数据的本地路径(如果是网络 URL,则会自动进行下载)。 Returns: - str: 视频的本地路径,以绝对路径表示。 + str: 视频的本地路径,以绝对路径表示。 """ url = self.file @@ -273,7 +273,7 @@ class Video(BaseMessageComponent): raise Exception(f"not a valid file: {url}") async def register_to_file_service(self) -> str: - """将视频注册到文件服务。 + """将视频注册到文件服务。 Returns: str: 注册后的URL @@ -285,18 +285,18 @@ class Video(BaseMessageComponent): callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception("未配置 callback_api_base,文件服务不可用") 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(f"已注册:{callback_host}/api/file/{token}") return f"{callback_host}/api/file/{token}" async def to_dict(self): - """需要和 toDict 区分开,toDict 是同步方法""" + """需要和 toDict 区分开,toDict 是同步方法""" url_or_path = self.file if url_or_path.startswith("http"): payload_file = url_or_path @@ -445,10 +445,10 @@ class Image(BaseMessageComponent): return Image.fromBytes(IO.read()) async def convert_to_file_path(self) -> str: - """将这个图片统一转换为本地文件路径。这个方法避免了手动判断图片数据类型,直接返回图片数据的本地路径(如果是网络 URL, 则会自动进行下载)。 + """将这个图片统一转换为本地文件路径。这个方法避免了手动判断图片数据类型,直接返回图片数据的本地路径(如果是网络 URL, 则会自动进行下载)。 Returns: - str: 图片的本地路径,以绝对路径表示。 + str: 图片的本地路径,以绝对路径表示。 """ url = self.url or self.file @@ -473,10 +473,10 @@ class Image(BaseMessageComponent): raise Exception(f"not a valid file: {url}") async def convert_to_base64(self) -> str: - """将这个图片统一转换为 base64 编码。这个方法避免了手动判断图片数据类型,直接返回图片数据的 base64 编码。 + """将这个图片统一转换为 base64 编码。这个方法避免了手动判断图片数据类型,直接返回图片数据的 base64 编码。 Returns: - str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。 + str: 图片的 base64 编码,不以 base64:// 或者 data:image/jpeg;base64, 开头。 """ # convert to base64 @@ -498,7 +498,7 @@ class Image(BaseMessageComponent): return bs64_data async def register_to_file_service(self) -> str: - """将图片注册到文件服务。 + """将图片注册到文件服务。 Returns: str: 注册后的URL @@ -510,13 +510,13 @@ class Image(BaseMessageComponent): callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception("未配置 callback_api_base,文件服务不可用") 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(f"已注册:{callback_host}/api/file/{token}") return f"{callback_host}/api/file/{token}" @@ -660,7 +660,7 @@ class Nodes(BaseMessageComponent): return ret async def to_dict(self) -> dict: - """将 Nodes 转换为字典格式,适用于 OneBot JSON 格式""" + """将 Nodes 转换为字典格式,适用于 OneBot JSON 格式""" ret = {"messages": []} for node in self.nodes: d = await node.to_dict() @@ -692,12 +692,12 @@ class File(BaseMessageComponent): url: str | None = "" # url def __init__(self, name: str, file: str = "", url: str = "") -> None: - """文件消息段。""" + """文件消息段。""" super().__init__(name=name, file_=file, url=url) @property def file(self) -> str: - """获取文件路径,如果文件不存在但有URL,则同步下载文件 + """获取文件路径,如果文件不存在但有URL,则同步下载文件 Returns: str: 文件路径 @@ -712,12 +712,12 @@ class File(BaseMessageComponent): asyncio.get_running_loop() logger.warning( "不可以在异步上下文中同步等待下载! " - "这个警告通常发生于某些逻辑试图通过 .file 获取文件消息段的文件内容。" + "这个警告通常发生于某些逻辑试图通过 .file 获取文件消息段的文件内容。" "请使用 await get_file() 代替直接获取 .file 字段", ) return "" except RuntimeError: - # 没有运行中的 event loop,可以同步执行 + # 没有运行中的 event loop,可以同步执行 try: # 使用 asyncio.run 安全地创建和关闭事件循环 asyncio.run(self._download_file()) @@ -743,11 +743,11 @@ class File(BaseMessageComponent): self.file_ = value async def get_file(self, allow_return_url: bool = False) -> str: - """异步获取文件。请注意在使用后清理下载的文件, 以免占用过多空间 + """异步获取文件。请注意在使用后清理下载的文件, 以免占用过多空间 Args: - allow_return_url: 是否允许以文件 http 下载链接的形式返回,这允许您自行控制是否需要下载文件。 - 注意,如果为 True,也可能返回文件路径。 + allow_return_url: 是否允许以文件 http 下载链接的形式返回,这允许您自行控制是否需要下载文件。 + 注意,如果为 True,也可能返回文件路径。 Returns: str: 文件路径或者 http 下载链接 @@ -805,7 +805,7 @@ class File(BaseMessageComponent): self.file_ = str(await anyio.Path(file_path).resolve()) async def register_to_file_service(self) -> str: - """将文件注册到文件服务。 + """将文件注册到文件服务。 Returns: str: 注册后的URL @@ -817,18 +817,18 @@ class File(BaseMessageComponent): callback_host = astrbot_config.get("callback_api_base") if not callback_host: - raise Exception("未配置 callback_api_base,文件服务不可用") + raise Exception("未配置 callback_api_base,文件服务不可用") 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(f"已注册:{callback_host}/api/file/{token}") return f"{callback_host}/api/file/{token}" async def to_dict(self): - """需要和 toDict 区分开,toDict 是同步方法""" + """需要和 toDict 区分开,toDict 是同步方法""" url_or_path = await self.get_file(allow_return_url=True) if url_or_path.startswith("http"): payload_file = url_or_path diff --git a/astrbot/core/message/message_event_result.py b/astrbot/core/message/message_event_result.py index 0965fe7f7..7fefe1bdb 100644 --- a/astrbot/core/message/message_event_result.py +++ b/astrbot/core/message/message_event_result.py @@ -16,22 +16,22 @@ from astrbot.core.message.components import ( @dataclass class MessageChain: - """MessageChain 描述了一整条消息中带有的所有组件。 - 现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。 + """MessageChain 描述了一整条消息中带有的所有组件。 + 现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。 Attributes: - `chain` (list): 用于顺序存储各个组件。 - `use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 + `chain` (list): 用于顺序存储各个组件。 + `use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 """ chain: list[BaseMessageComponent] = field(default_factory=list) use_t2i_: bool | None = None # None 为跟随用户设置 type: str | None = None - """消息链承载的消息的类型。可选,用于让消息平台区分不同业务场景的消息链。""" + """消息链承载的消息的类型。可选,用于让消息平台区分不同业务场景的消息链。""" def message(self, message: str): - """添加一条文本消息到消息链 `chain` 中。 + """添加一条文本消息到消息链 `chain` 中。 Example: CommandResult().message("Hello ").message("world!") @@ -42,7 +42,7 @@ class MessageChain: return self def at(self, name: str, qq: str | int): - """添加一条 At 消息到消息链 `chain` 中。 + """添加一条 At 消息到消息链 `chain` 中。 Example: CommandResult().at("张三", "12345678910") @@ -53,7 +53,7 @@ class MessageChain: return self def at_all(self): - """添加一条 AtAll 消息到消息链 `chain` 中。 + """添加一条 AtAll 消息到消息链 `chain` 中。 Example: CommandResult().at_all() @@ -63,7 +63,7 @@ class MessageChain: self.chain.append(AtAll()) return self - @deprecated("请使用 message 方法代替。") + @deprecated("请使用 message 方法代替。") def error(self, message: str): """添加一条错误消息到消息链 `chain` 中 @@ -75,10 +75,10 @@ class MessageChain: return self def url_image(self, url: str): - """添加一条图片消息(https 链接)到消息链 `chain` 中。 + """添加一条图片消息(https 链接)到消息链 `chain` 中。 Note: - 如果需要发送本地图片,请使用 `file_image` 方法。 + 如果需要发送本地图片,请使用 `file_image` 方法。 Example: CommandResult().image("https://example.com/image.jpg") @@ -88,10 +88,10 @@ class MessageChain: return self def file_image(self, path: str): - """添加一条图片消息(本地文件路径)到消息链 `chain` 中。 + """添加一条图片消息(本地文件路径)到消息链 `chain` 中。 Note: - 如果需要发送网络图片,请使用 `url_image` 方法。 + 如果需要发送网络图片,请使用 `url_image` 方法。 CommandResult().image("image.jpg") @@ -100,7 +100,7 @@ class MessageChain: return self def base64_image(self, base64_str: str): - """添加一条图片消息(base64 编码字符串)到消息链 `chain` 中。 + """添加一条图片消息(base64 编码字符串)到消息链 `chain` 中。 Example: CommandResult().base64_image("iVBORw0KGgoAAAANSUhEUgAAAAUA...") @@ -109,17 +109,17 @@ class MessageChain: return self def use_t2i(self, use_t2i: bool): - """设置是否使用文本转图片服务。 + """设置是否使用文本转图片服务。 Args: - use_t2i (bool): 是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 + use_t2i (bool): 是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 """ self.use_t2i_ = use_t2i return self def get_plain_text(self, with_other_comps_mark: bool = False) -> str: - """获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。 + """获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。 Args: with_other_comps_mark (bool): 是否在纯文本中标记其他组件的位置 @@ -140,7 +140,7 @@ class MessageChain: return " ".join(texts) def squash_plain(self): - """将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。""" + """将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。""" if not self.chain: return None @@ -165,7 +165,7 @@ class MessageChain: class EventResultType(enum.Enum): - """用于描述事件处理的结果类型。 + """用于描述事件处理的结果类型。 Attributes: CONTINUE: 事件将会继续传播 @@ -178,7 +178,7 @@ class EventResultType(enum.Enum): class ResultContentType(enum.Enum): - """用于描述事件结果的内容的类型。""" + """用于描述事件结果的内容的类型。""" LLM_RESULT = enum.auto() """调用 LLM 产生的结果""" @@ -194,13 +194,13 @@ class ResultContentType(enum.Enum): @dataclass class MessageEventResult(MessageChain): - """MessageEventResult 描述了一整条消息中带有的所有组件以及事件处理的结果。 - 现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。 + """MessageEventResult 描述了一整条消息中带有的所有组件以及事件处理的结果。 + 现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。 Attributes: - `chain` (list): 用于顺序存储各个组件。 - `use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 - `result_type` (EventResultType): 事件处理的结果类型。 + `chain` (list): 用于顺序存储各个组件。 + `use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。 + `result_type` (EventResultType): 事件处理的结果类型。 """ @@ -216,36 +216,36 @@ class MessageEventResult(MessageChain): """异步流""" def stop_event(self) -> "MessageEventResult": - """终止事件传播。""" + """终止事件传播。""" self.result_type = EventResultType.STOP return self def continue_event(self) -> "MessageEventResult": - """继续事件传播。""" + """继续事件传播。""" self.result_type = EventResultType.CONTINUE return self def is_stopped(self) -> bool: - """是否终止事件传播。""" + """是否终止事件传播。""" return self.result_type == EventResultType.STOP def set_async_stream(self, stream: AsyncGenerator) -> "MessageEventResult": - """设置异步流。""" + """设置异步流。""" self.async_stream = stream return self def set_result_content_type(self, typ: ResultContentType) -> "MessageEventResult": - """设置事件处理的结果类型。 + """设置事件处理的结果类型。 Args: - result_type (EventResultType): 事件处理的结果类型。 + result_type (EventResultType): 事件处理的结果类型。 """ self.result_content_type = typ return self def is_llm_result(self) -> bool: - """是否为 LLM 结果。""" + """是否为 LLM 结果。""" return self.result_content_type == ResultContentType.LLM_RESULT def is_model_result(self) -> bool: @@ -256,5 +256,5 @@ class MessageEventResult(MessageChain): ) -# 为了兼容旧版代码,保留 CommandResult 的别名 +# 为了兼容旧版代码,保留 CommandResult 的别名 CommandResult = MessageEventResult diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index 2a893c9c4..da865febd 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -35,7 +35,7 @@ class PersonaManager: 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(f"已加载 {len(self.personas)} 个人格。") async def get_persona(self, persona_id: str): """获取指定 persona 的信息""" @@ -80,7 +80,7 @@ class PersonaManager: platform_name: str, provider_settings: dict | None = None, ) -> tuple[str | None, Personality | None, str | None, bool]: - """解析当前会话最终生效的人格。 + """解析当前会话最终生效的人格。 Returns: tuple: @@ -143,7 +143,7 @@ class PersonaManager: skills: list[str] | None | object = NOT_GIVEN, custom_error_message: str | None | object = NOT_GIVEN, ): - """更新指定 persona 的信息。tools 参数为 None 时表示使用所有工具,空列表表示不使用任何工具""" + """更新指定 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.") @@ -179,7 +179,7 @@ class PersonaManager: """获取指定文件夹中的 personas Args: - folder_id: 文件夹 ID,None 表示根目录 + folder_id: 文件夹 ID,None 表示根目录 """ return await self.db.get_personas_by_folder(folder_id) @@ -190,7 +190,7 @@ class PersonaManager: Args: persona_id: Persona ID - folder_id: 目标文件夹 ID,None 表示移动到根目录 + folder_id: 目标文件夹 ID,None 表示移动到根目录 """ persona = await self.db.move_persona_to_folder(persona_id, folder_id) if persona: @@ -227,7 +227,7 @@ class PersonaManager: """获取文件夹列表 Args: - parent_id: 父文件夹 ID,None 表示获取根目录下的文件夹 + parent_id: 父文件夹 ID,None 表示获取根目录下的文件夹 """ return await self.db.get_persona_folders(parent_id) @@ -263,7 +263,7 @@ class PersonaManager: """批量更新 personas 和/或 folders 的排序顺序 Args: - items: 包含以下键的字典列表: + items: 包含以下键的字典列表: - id: persona_id 或 folder_id - type: "persona" 或 "folder" - sort_order: 新的排序顺序值 @@ -277,7 +277,7 @@ class PersonaManager: """获取文件夹树形结构 Returns: - 树形结构的文件夹列表,每个文件夹包含 children 子列表 + 树形结构的文件夹列表,每个文件夹包含 children 子列表 """ all_folders = await self.get_all_folders() folder_map: dict[str, dict] = {} @@ -323,15 +323,15 @@ class PersonaManager: folder_id: str | None = None, sort_order: int = 0, ) -> Persona: - """创建新的 persona。 + """创建新的 persona。 Args: persona_id: Persona 唯一标识 system_prompt: 系统提示词 begin_dialogs: 预设对话列表 - tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具 - skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills - folder_id: 所属文件夹 ID,None 表示根目录 + tools: 工具列表,None 表示使用所有工具,空列表表示不使用任何工具 + skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills + folder_id: 所属文件夹 ID,None 表示根目录 sort_order: 排序顺序 """ if await self.db.get_persona_by_id(persona_id): @@ -388,12 +388,12 @@ class PersonaManager: def get_v3_persona_data( self, ) -> tuple[list[dict], list[Personality], Personality]: - """获取 AstrBot <4.0.0 版本的 persona 数据。 + """获取 AstrBot <4.0.0 版本的 persona 数据。 Returns: - - list[dict]: 包含 persona 配置的字典列表。 - - list[Personality]: 包含 Personality 对象的列表。 - - Personality: 默认选择的 Personality 对象。 + - list[dict]: 包含 persona 配置的字典列表。 + - list[Personality]: 包含 Personality 对象的列表。 + - Personality: 默认选择的 Personality 对象。 """ v3_persona_config = [ @@ -418,7 +418,7 @@ class PersonaManager: if begin_dialogs: if len(begin_dialogs) % 2 != 0: logger.error( - f"{persona_cfg['name']} 人格情景预设对话格式不对,条数应该为偶数。", + f"{persona_cfg['name']} 人格情景预设对话格式不对,条数应该为偶数。", ) begin_dialogs = [] user_turn = True @@ -442,7 +442,7 @@ class PersonaManager: selected_default_persona = persona personas_v3.append(persona) except Exception as e: - logger.error(f"解析 Persona 配置失败:{e}") + logger.error(f"解析 Persona 配置失败:{e}") if not selected_default_persona and len(personas_v3) > 0: # 默认选择第一个 diff --git a/astrbot/core/pipeline/content_safety_check/stage.py b/astrbot/core/pipeline/content_safety_check/stage.py index 19037eb08..fa195c300 100644 --- a/astrbot/core/pipeline/content_safety_check/stage.py +++ b/astrbot/core/pipeline/content_safety_check/stage.py @@ -13,7 +13,7 @@ from .strategies.strategy import StrategySelector class ContentSafetyCheckStage(Stage): """检查内容安全 - 当前只会检查文本的。 + 当前只会检查文本的。 """ async def initialize(self, ctx: PipelineContext) -> None: @@ -32,10 +32,10 @@ class ContentSafetyCheckStage(Stage): if event.is_at_or_wake_command: event.set_result( MessageEventResult().message( - "你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。", + "你的消息或者大模型的响应中包含不适当的内容,已被屏蔽。", ), ) yield event.stop_event() - logger.info(f"内容安全检查不通过,原因:{info}") + logger.info(f"内容安全检查不通过,原因:{info}") return diff --git a/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py b/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py index dd8ca629e..ca8a9dc8f 100644 --- a/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +++ b/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py @@ -23,10 +23,10 @@ class BaiduAipStrategy(ContentSafetyStrategy): if "data" not in res: return False, "" count = len(res["data"]) - parts = [f"百度审核服务发现 {count} 处违规:\n"] + parts = [f"百度审核服务发现 {count} 处违规:\n"] for i in res["data"]: - # 百度 AIP 返回结构是动态 dict;类型检查时 i 可能被推断为序列,转成 dict 后用 get 取字段 - parts.append(f"{cast(dict[str, Any], i).get('msg', '')};\n") - parts.append("\n判断结果:" + res["conclusion"]) + # 百度 AIP 返回结构是动态 dict;类型检查时 i 可能被推断为序列,转成 dict 后用 get 取字段 + parts.append(f"{cast(dict[str, Any], i).get('msg', '')};\n") + parts.append("\n判断结果:" + res["conclusion"]) info = "".join(parts) return False, info diff --git a/astrbot/core/pipeline/content_safety_check/strategies/keywords.py b/astrbot/core/pipeline/content_safety_check/strategies/keywords.py index 53ad900f7..613cc37f4 100644 --- a/astrbot/core/pipeline/content_safety_check/strategies/keywords.py +++ b/astrbot/core/pipeline/content_safety_check/strategies/keywords.py @@ -20,5 +20,5 @@ class KeywordsStrategy(ContentSafetyStrategy): def check(self, content: str) -> tuple[bool, str]: for keyword in self.keywords: if re.search(keyword, content): - return False, "内容安全检查不通过,匹配到敏感词。" + return False, "内容安全检查不通过,匹配到敏感词。" return True, "" diff --git a/astrbot/core/pipeline/context.py b/astrbot/core/pipeline/context.py index 47cd33b23..b4b9f3689 100644 --- a/astrbot/core/pipeline/context.py +++ b/astrbot/core/pipeline/context.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: @dataclass class PipelineContext: - """上下文对象,包含管道执行所需的上下文信息""" + """上下文对象,包含管道执行所需的上下文信息""" astrbot_config: AstrBotConfig # AstrBot 配置对象 plugin_manager: PluginManager # 插件管理器对象 diff --git a/astrbot/core/pipeline/context_utils.py b/astrbot/core/pipeline/context_utils.py index 9402ce3e6..3e4f87e90 100644 --- a/astrbot/core/pipeline/context_utils.py +++ b/astrbot/core/pipeline/context_utils.py @@ -17,8 +17,8 @@ async def call_handler( ) -> T.AsyncGenerator[T.Any, None]: """执行事件处理函数并处理其返回结果 - 该方法负责调用处理函数并处理不同类型的返回值。它支持两种类型的处理函数: - 1. 异步生成器: 实现洋葱模型,每次 yield 都会将控制权交回上层 + 该方法负责调用处理函数并处理不同类型的返回值。它支持两种类型的处理函数: + 1. 异步生成器: 实现洋葱模型,每次 yield 都会将控制权交回上层 2. 协程: 执行一次并处理返回值 Args: @@ -26,7 +26,7 @@ async def call_handler( handler (Awaitable): 事件处理函数 Returns: - AsyncGenerator[None, None]: 异步生成器,用于在管道中传递控制流 + AsyncGenerator[None, None]: 异步生成器,用于在管道中传递控制流 """ ready_to_call = None # 一个协程或者异步生成器 @@ -36,7 +36,7 @@ async def call_handler( try: ready_to_call = handler(event, *args, **kwargs) except TypeError: - logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True) + logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True) if not ready_to_call: return @@ -46,7 +46,7 @@ async def call_handler( try: async for ret in ready_to_call: # 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码 - # 返回值只能是 MessageEventResult 或者 None(无返回值) + # 返回值只能是 MessageEventResult 或者 None(无返回值) _has_yielded = True if isinstance(ret, MessageEventResult | CommandResult): # 如果返回值是 MessageEventResult, 设置结果并继续 @@ -81,7 +81,7 @@ async def call_event_hook( """调用事件钩子函数 Returns: - bool: 如果事件被终止,返回 True + bool: 如果事件被终止,返回 True # """ @@ -101,7 +101,7 @@ async def call_event_hook( if event.is_stopped(): logger.info( - f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", + f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", ) return True diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index 464f584f8..39e81f489 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -26,7 +26,7 @@ class PreProcessStage(Stage): event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: """在处理事件之前的预处理""" - # 平台特异配置:platform_specific..pre_ack_emoji + # 平台特异配置:platform_specific..pre_ack_emoji supported = {"telegram", "lark", "discord"} platform = event.get_platform_name() cfg = ( @@ -48,7 +48,7 @@ class PreProcessStage(Stage): # 路径映射 if mappings := self.platform_settings.get("path_mapping", []): - # 支持 Record,Image 消息段的路径映射。 + # 支持 Record,Image 消息段的路径映射。 message_chain = event.get_messages() for idx, component in enumerate(message_chain): @@ -71,7 +71,7 @@ class PreProcessStage(Stage): stt_provider = ctx.get_using_stt_provider(event.unified_msg_origin) if not stt_provider: logger.warning( - f"会话 {event.unified_msg_origin} 未配置语音转文本模型。", + f"会话 {event.unified_msg_origin} 未配置语音转文本模型。", ) return message_chain = event.get_messages() diff --git a/astrbot/core/pipeline/process_stage/method/agent_request.py b/astrbot/core/pipeline/process_stage/method/agent_request.py index 9efe53814..f37332f5b 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_request.py +++ b/astrbot/core/pipeline/process_stage/method/agent_request.py @@ -20,7 +20,7 @@ class AgentRequestSubStage(Stage): for bwp in self.bot_wake_prefixs: if self.prov_wake_prefix.startswith(bwp): logger.info( - f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。", + f"识别 LLM 聊天额外唤醒前缀 {self.prov_wake_prefix} 以机器人唤醒前缀 {bwp} 开头,已自动去除。", ) self.prov_wake_prefix = self.prov_wake_prefix[len(bwp) :] 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 aab11b402..2957bc70e 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 @@ -260,7 +260,7 @@ class InternalAgentSubStage(Stage): # 检测 Live Mode if action_type == "live": # Live Mode: 使用 run_live_agent - logger.info("[Internal Agent] 检测到 Live Mode,启用 TTS 处理") + logger.info("[Internal Agent] 检测到 Live Mode,启用 TTS 处理") # 获取 TTS Provider tts_provider = ( @@ -271,10 +271,10 @@ class InternalAgentSubStage(Stage): if not tts_provider: logger.warning( - "[Live Mode] TTS Provider 未配置,将使用普通流式模式" + "[Live Mode] TTS Provider 未配置,将使用普通流式模式" ) - # 使用 run_live_agent,总是使用流式响应 + # 使用 run_live_agent,总是使用流式响应 event.set_result( MessageEventResult() .set_result_content_type(ResultContentType.STREAMING_RESULT) @@ -357,7 +357,7 @@ class InternalAgentSubStage(Stage): resp=final_resp.completion_text if final_resp else None, ) - # 检查事件是否被停止,如果被停止则不保存历史记录 + # 检查事件是否被停止,如果被停止则不保存历史记录 if not event.is_stopped() or agent_runner.was_aborted(): await self._save_to_history( event, @@ -430,7 +430,7 @@ class InternalAgentSubStage(Stage): and not req.tool_calls_result and not user_aborted ): - logger.debug("LLM 响应为空,不保存记录。") + logger.debug("LLM 响应为空,不保存记录。") 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 7ad1de056..505e93afc 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 @@ -66,7 +66,7 @@ async def run_third_party_agent( ) -> AsyncGenerator[tuple[MessageChain, bool], None]: """ 运行第三方 agent runner 并转换响应格式 - 类似于 run_agent 函数,但专门处理第三方 agent runner + 类似于 run_agent 函数,但专门处理第三方 agent runner """ try: async for resp in runner.step_until_done(max_step=30): # type: ignore[misc] @@ -301,11 +301,11 @@ class ThirdPartyAgentSubStage(Stage): {}, ) if not self.prov_id: - logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。") + logger.error("没有填写 Agent Runner 提供商 ID,请前往配置页面配置。") return if not self.prov_cfg: logger.error( - f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。" + f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。" ) return diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index 9422d6317..9bc00975e 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -62,7 +62,7 @@ class StarRequestSubStage(Stage): ) if not event.is_stopped() and event.is_at_or_wake_command: - ret = f":(\n\n在调用插件 {md.name} 的处理函数 {handler.handler_name} 时出现异常:{e}" + ret = f":(\n\n在调用插件 {md.name} 的处理函数 {handler.handler_name} 时出现异常:{e}" event.set_result(MessageEventResult().message(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 392bceff3..6ba29bf2e 100644 --- a/astrbot/core/pipeline/rate_limit_check/stage.py +++ b/astrbot/core/pipeline/rate_limit_check/stage.py @@ -13,23 +13,23 @@ from ..stage import Stage, register_stage @register_stage class RateLimitStage(Stage): - """检查是否需要限制消息发送的限流器。 + """检查是否需要限制消息发送的限流器。 - 使用 Fixed Window 算法。 - 如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。 + 使用 Fixed Window 算法。 + 如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。 """ def __init__(self) -> None: # 存储每个会话的请求时间队列 self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque) - # 为每个会话设置一个锁,避免并发冲突 + # 为每个会话设置一个锁,避免并发冲突 self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) # 限流参数 self.rate_limit_count: int = 0 self.rate_limit_time: timedelta = timedelta(0) async def initialize(self, ctx: PipelineContext) -> None: - """初始化限流器,根据配置设置限流参数。""" + """初始化限流器,根据配置设置限流参数。""" self.rate_limit_count = ctx.astrbot_config["platform_settings"]["rate_limit"][ "count" ] @@ -44,21 +44,21 @@ class RateLimitStage(Stage): self, event: AstrMessageEvent, ) -> None | AsyncGenerator[None, None]: - """检查并处理限流逻辑。如果触发限流,流水线会 stall 并在窗口期后自动恢复。 + """检查并处理限流逻辑。如果触发限流,流水线会 stall 并在窗口期后自动恢复。 Args: - event (AstrMessageEvent): 当前消息事件。 - ctx (PipelineContext): 流水线上下文。 + event (AstrMessageEvent): 当前消息事件。 + ctx (PipelineContext): 流水线上下文。 Returns: - MessageEventResult: 继续或停止事件处理的结果。 + MessageEventResult: 继续或停止事件处理的结果。 """ session_id = event.session_id now = datetime.now() async with self.locks[session_id]: # 确保同一会话不会并发修改队列 - # 检查并处理限流,可能需要多次检查直到满足条件 + # 检查并处理限流,可能需要多次检查直到满足条件 while True: timestamps = self.event_timestamps[session_id] self._remove_expired_timestamps(timestamps, now) @@ -72,13 +72,13 @@ class RateLimitStage(Stage): match self.rl_strategy: case RateLimitStrategy.STALL.value: logger.info( - f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", + f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", ) await asyncio.sleep(stall_duration) now = datetime.now() case RateLimitStrategy.DISCARD.value: logger.info( - f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到限额于 {stall_duration:.2f} 秒后重置。", + f"会话 {session_id} 被限流。根据限流策略,此请求已被丢弃,直到限额于 {stall_duration:.2f} 秒后重置。", ) return event.stop_event() @@ -87,11 +87,11 @@ class RateLimitStage(Stage): timestamps: deque[datetime], now: datetime, ) -> None: - """移除时间窗口外的时间戳。 + """移除时间窗口外的时间戳。 Args: - timestamps (Deque[datetime]): 当前会话的时间戳队列。 - now (datetime): 当前时间,用于计算过期时间。 + timestamps (Deque[datetime]): 当前会话的时间戳队列。 + now (datetime): 当前时间,用于计算过期时间。 """ expiry_threshold: datetime = now - self.rate_limit_time diff --git a/astrbot/core/pipeline/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index 6a884a518..a0353830f 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -44,7 +44,7 @@ class RespondStage(Stage): comp.lat is not None and comp.lon is not None ), # 位置 Comp.Contact: lambda comp: bool(comp._type and comp.id), # 推荐好友 or 群 - Comp.Shake: lambda _: True, # 窗口抖动(戳一戳) + Comp.Shake: lambda _: True, # 窗口抖动(戳一戳) Comp.Dice: lambda _: True, # 掷骰子魔法表情 Comp.RPS: lambda _: True, # 猜拳魔法表情 Comp.Unknown: lambda comp: bool(comp.text and comp.text.strip()), @@ -85,8 +85,8 @@ class RespondStage(Stage): 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(f"解析分段回复的间隔时间失败。{e}") + logger.info(f"分段回复间隔时间:{self.interval}") async def _word_cnt(self, text: str) -> int: """分段回复 统计字数""" @@ -187,7 +187,7 @@ class RespondStage(Stage): if result.result_content_type == ResultContentType.STREAMING_RESULT: if result.async_stream is None: - logger.warning("async_stream 为空,跳过发送。") + logger.warning("async_stream 为空,跳过发送。") return # 流式结果直接交付平台适配器处理 realtime_segmenting = ( @@ -205,14 +205,14 @@ class RespondStage(Stage): if mappings := self.platform_settings.get("path_mapping", []): for idx, component in enumerate(result.chain): if isinstance(component, Comp.File) and component.file: - # 支持 File 消息段的路径映射。 + # 支持 File 消息段的路径映射。 component.file = path_Mapping(mappings, component.file) result.chain[idx] = component # 检查消息链是否为空 try: if await self._is_empty_message_chain(result.chain): - logger.info("消息为空,跳过发送阶段") + logger.info("消息为空,跳过发送阶段") return except Exception as e: logger.warning(f"空内容检查异常: {e}") @@ -239,7 +239,7 @@ class RespondStage(Stage): if not result.chain or len(result.chain) == 0: # may fix #2670 logger.warning( - f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}", + f"实际消息链为空, 跳过发送阶段。header_chain: {header_comps}, actual_chain: {result.chain}", ) return for comp in result.chain: @@ -263,7 +263,7 @@ class RespondStage(Stage): ): # may fix #2670 logger.warning( - f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}", + f"消息链全为 Reply 和 At 消息段, 跳过发送阶段。chain: {result.chain}", ) return sep_comps = self._extract_comp( diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index d6e391c8e..54d355b20 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -73,7 +73,7 @@ class ResultDecorateStage(Stage): self.regex = ctx.astrbot_config["platform_settings"]["segmented_reply"]["regex"] self.split_words = ctx.astrbot_config["platform_settings"][ "segmented_reply" - ].get("split_words", ["。", "?", "!", "~", "…"]) + ].get("split_words", ["。", "?", "!", "~", "…"]) if self.split_words: escaped_words = sorted( [re.escape(word) for word in self.split_words], key=len, reverse=True @@ -167,29 +167,29 @@ class ResultDecorateStage(Stage): ) if is_stream: logger.warning( - "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作", + "启用流式输出时,依赖发送消息前事件钩子的插件可能无法正常工作", ) 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} 将消息结果清空。", + f"hook(on_decorating_result) -> {star_map[handler.handler_module_path].name} - {handler.handler_name} 将消息结果清空。", ) except BaseException: logger.error(traceback.format_exc()) if event.is_stopped(): logger.info( - f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", + f"{star_map[handler.handler_module_path].name} - {handler.handler_name} 终止了事件传播。", ) return # 流式输出不执行下面的逻辑 if is_stream: - logger.info("流式输出已启用,跳过结果装饰阶段") + logger.info("流式输出已启用,跳过结果装饰阶段") return - # 需要再获取一次。插件可能直接对 chain 进行了替换。 + # 需要再获取一次。插件可能直接对 chain 进行了替换。 result = event.get_result() if result is None: return @@ -231,10 +231,10 @@ class ResultDecorateStage(Stage): ) except re.error: logger.error( - f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}", + f"分段回复正则表达式错误,使用默认分段方式: {traceback.format_exc()}", ) split_response = re.findall( - r".*?[。?!~…]+|.+$", + r".*?[。?!~…]+|.+$", comp.text, re.DOTALL | re.MULTILINE, ) @@ -266,7 +266,7 @@ class ResultDecorateStage(Stage): ) if should_tts and not tts_provider: logger.warning( - f"会话 {event.unified_msg_origin} 未配置文本转语音模型。", + f"会话 {event.unified_msg_origin} 未配置文本转语音模型。", ) if ( @@ -288,7 +288,7 @@ class ResultDecorateStage(Stage): logger.info(f"TTS 结果: {audio_path}") if not audio_path: logger.error( - f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}", + f"由于 TTS 音频文件未找到,消息段转语音失败: {comp.text}", ) new_chain.append(comp) continue @@ -309,7 +309,7 @@ class ResultDecorateStage(Stage): audio_path, ) url = f"{callback_api_base}/api/file/{token}" - logger.debug(f"已注册:{url}") + logger.debug(f"已注册:{url}") new_chain.append( Record( @@ -322,7 +322,7 @@ class ResultDecorateStage(Stage): new_chain.append(comp) except Exception: logger.error(traceback.format_exc()) - logger.error("TTS 失败,使用文本发送。") + logger.error("TTS 失败,使用文本发送。") new_chain.append(comp) else: new_chain.append(comp) @@ -349,11 +349,11 @@ class ResultDecorateStage(Stage): template_name=self.t2i_active_template, ) except BaseException: - logger.error("文本转图片失败,使用文本发送。") + logger.error("文本转图片失败,使用文本发送。") return if time.time() - render_start > 3: logger.warning( - "文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。", + "文本转图片耗时超过了 3 秒,如果觉得很慢可以使用 /t2i 关闭文本转图片模式。", ) if url: if url.startswith("http"): @@ -364,7 +364,7 @@ class ResultDecorateStage(Stage): ): 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(f"已注册:{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 ca69b8475..52c6a1349 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -15,7 +15,7 @@ from .stage_order import STAGES_ORDER class PipelineScheduler: - """管道调度器,负责调度各个阶段的执行""" + """管道调度器,负责调度各个阶段的执行""" def __init__(self, context: PipelineContext) -> None: ensure_builtin_stages_registered() @@ -53,7 +53,7 @@ class PipelineScheduler: # 此处是前置处理完成后的暂停点(yield), 下面开始执行后续阶段 if event.is_stopped(): logger.debug( - f"阶段 {stage.__class__.__name__} 已终止事件传播。", + f"阶段 {stage.__class__.__name__} 已终止事件传播。", ) break @@ -63,7 +63,7 @@ class PipelineScheduler: # 此处是后续所有阶段处理完毕后返回的点, 执行后置处理 if event.is_stopped(): logger.debug( - f"阶段 {stage.__class__.__name__} 已终止事件传播。", + f"阶段 {stage.__class__.__name__} 已终止事件传播。", ) break else: @@ -72,7 +72,7 @@ class PipelineScheduler: await coroutine if event.is_stopped(): - logger.debug(f"阶段 {stage.__class__.__name__} 已终止事件传播。") + logger.debug(f"阶段 {stage.__class__.__name__} 已终止事件传播。") break async def execute(self, event: AstrMessageEvent) -> None: @@ -90,6 +90,6 @@ class PipelineScheduler: if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent): await event.send(None) - logger.debug("pipeline 执行完毕。") + logger.debug("pipeline 执行完毕。") 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 26c3c235a..bf91cde2c 100644 --- a/astrbot/core/pipeline/session_status_check/stage.py +++ b/astrbot/core/pipeline/session_status_check/stage.py @@ -22,7 +22,7 @@ class SessionStatusCheckStage(Stage): ) -> None | AsyncGenerator[None, None]: # 检查会话是否整体启用 if not await SessionServiceManager.is_session_enabled(event.unified_msg_origin): - logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。") + logger.debug(f"会话 {event.unified_msg_origin} 已被关闭,已终止事件传播。") # workaround for #2309 conv_id = await self.conv_mgr.get_curr_conversation_id( diff --git a/astrbot/core/pipeline/stage.py b/astrbot/core/pipeline/stage.py index 74aca4ef1..03fd61540 100644 --- a/astrbot/core/pipeline/stage.py +++ b/astrbot/core/pipeline/stage.py @@ -11,7 +11,7 @@ registered_stages: list[type[Stage]] = [] # 维护了所有已注册的 Stage def register_stage(cls): - """一个简单的装饰器,用于注册 pipeline 包下的 Stage 实现类""" + """一个简单的装饰器,用于注册 pipeline 包下的 Stage 实现类""" registered_stages.append(cls) return cls @@ -37,9 +37,9 @@ class Stage(abc.ABC): """处理事件 Args: - event (AstrMessageEvent): 事件对象,包含事件的相关信息 + event (AstrMessageEvent): 事件对象,包含事件的相关信息 Returns: - Union[None, AsyncGenerator[None, None]]: 处理结果,可能是 None 或者异步生成器, 如果为 None 则表示不需要继续处理, 如果为异步生成器则表示需要继续处理(进入下一个阶段) + Union[None, AsyncGenerator[None, None]]: 处理结果,可能是 None 或者异步生成器, 如果为 None 则表示不需要继续处理, 如果为异步生成器则表示需要继续处理(进入下一个阶段) """ raise NotImplementedError diff --git a/astrbot/core/pipeline/stage_order.py b/astrbot/core/pipeline/stage_order.py index f99f57264..d6bb5bbad 100644 --- a/astrbot/core/pipeline/stage_order.py +++ b/astrbot/core/pipeline/stage_order.py @@ -7,8 +7,8 @@ STAGES_ORDER = [ "RateLimitStage", # 检查会话是否超过频率限制 "ContentSafetyCheckStage", # 检查内容安全 "PreProcessStage", # 预处理 - "ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用 - "ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等 + "ProcessStage", # 交由 Stars 处理(a.k.a 插件),或者 LLM 调用 + "ResultDecorateStage", # 处理结果,比如添加回复前缀、t2i、转换为语音 等 "RespondStage", # 发送消息 ] diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index 2dcb840e9..2c9c506b3 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -33,13 +33,13 @@ def build_unique_session_id(event: AstrMessageEvent) -> str | None: @register_stage class WakingCheckStage(Stage): - """检查是否需要唤醒。唤醒机器人有如下几点条件: + """检查是否需要唤醒。唤醒机器人有如下几点条件: 1. 机器人被 @ 了 2. 机器人的消息被提到了 - 3. 以 wake_prefix 前缀开头,并且消息没有以 At 消息段开头 - 4. 插件(Star)的 handler filter 通过 - 5. 私聊情况下,位于 admins_id 列表中的管理员的消息(在白名单阶段中) + 3. 以 wake_prefix 前缀开头,并且消息没有以 At 消息段开头 + 4. 插件(Star)的 handler filter 通过 + 5. 私聊情况下,位于 admins_id 列表中的管理员的消息(在白名单阶段中) """ async def initialize(self, ctx: PipelineContext) -> None: @@ -110,7 +110,7 @@ class WakingCheckStage(Stage): and str(messages[0].qq) != str(event.get_self_id()) and str(messages[0].qq) != "all" ): - # 如果是群聊,且第一个消息段是 At 消息,但不是 At 机器人或 At 全体成员,则不唤醒 + # 如果是群聊,且第一个消息段是 At 消息,但不是 At 机器人或 At 全体成员,则不唤醒 break is_wake = True event.is_at_or_wake_command = True @@ -150,7 +150,7 @@ class WakingCheckStage(Stage): # 将 plugins_name 设置到 event 中 enabled_plugins_name = self.ctx.astrbot_config.get("plugin_set", ["*"]) if enabled_plugins_name == ["*"]: - # 如果是 *,则表示所有插件都启用 + # 如果是 *,则表示所有插件都启用 event.plugins_name = None else: event.plugins_name = enabled_plugins_name @@ -200,11 +200,11 @@ class WakingCheckStage(Stage): if self.no_permission_reply: await event.send( MessageChain().message( - f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。", + f"您(ID: {event.get_sender_id()})的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。", ), ) logger.info( - f"触发 {star_map[handler.handler_module_path].name} 时, 用户(ID={event.get_sender_id()}) 权限不足。", + f"触发 {star_map[handler.handler_module_path].name} 时, 用户(ID={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 ea9c55228..c3cdf038d 100644 --- a/astrbot/core/pipeline/whitelist_check/stage.py +++ b/astrbot/core/pipeline/whitelist_check/stage.py @@ -37,7 +37,7 @@ class WhitelistCheckStage(Stage): return if len(self.whitelist) == 0: - # 白名单为空,不检查 + # 白名单为空,不检查 return if event.get_platform_name() == "webchat": @@ -63,6 +63,6 @@ class WhitelistCheckStage(Stage): ): if self.wl_log: logger.info( - f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。", + f"会话 ID {event.unified_msg_origin} 不在会话白名单中,已终止事件传播。请在配置文件中添加该会话 ID 到白名单。", ) event.stop_event() diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 54271d860..2f056941f 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -43,11 +43,11 @@ class AstrMessageEvent(abc.ABC): self.message_str = message_str """纯文本的消息""" self.message_obj = message_obj - """消息对象, AstrBotMessage。带有完整的消息结构。""" + """消息对象, AstrBotMessage。带有完整的消息结构。""" self.platform_meta = platform_meta - """消息平台的信息, 其中 name 是平台的类型,如 aiocqhttp""" + """消息平台的信息, 其中 name 是平台的类型,如 aiocqhttp""" self.role = "member" - """用户是否是管理员。如果是管理员,这里是 admin""" + """用户是否是管理员。如果是管理员,这里是 admin""" self.is_wake = False """是否唤醒(是否通过 WakingStage)""" self.is_at_or_wake_command = False @@ -69,7 +69,7 @@ class AstrMessageEvent(abc.ABC): session_id=session_id, ) # self.unified_msg_origin = str(self.session) - """统一的消息来源字符串。格式为 platform_name:message_type:session_id""" + """统一的消息来源字符串。格式为 platform_name:message_type:session_id""" self._result: MessageEventResult | None = None """消息事件的结果""" @@ -91,48 +91,48 @@ class AstrMessageEvent(abc.ABC): """是否在此消息事件中禁止默认的 LLM 请求""" self.plugins_name: list[str] | None = None - """该事件启用的插件名称列表。None 表示所有插件都启用。空列表表示没有启用任何插件。""" + """该事件启用的插件名称列表。None 表示所有插件都启用。空列表表示没有启用任何插件。""" # back_compability self.platform = platform_meta @property def unified_msg_origin(self) -> str: - """统一的消息来源字符串。格式为 platform_name:message_type:session_id""" + """统一的消息来源字符串。格式为 platform_name:message_type:session_id""" return str(self.session) @unified_msg_origin.setter def unified_msg_origin(self, value: str) -> None: - """设置统一的消息来源字符串。格式为 platform_name:message_type:session_id""" + """设置统一的消息来源字符串。格式为 platform_name:message_type:session_id""" self.new_session = MessageSession.from_str(value) self.session = self.new_session @property def session_id(self) -> str: - """用户的会话 ID。可以直接使用下面的 unified_msg_origin""" + """用户的会话 ID。可以直接使用下面的 unified_msg_origin""" return self.session.session_id @session_id.setter def session_id(self, value: str) -> None: - """设置用户的会话 ID。可以直接使用下面的 unified_msg_origin""" + """设置用户的会话 ID。可以直接使用下面的 unified_msg_origin""" self.session.session_id = value def get_platform_name(self): - """获取这个事件所属的平台的类型(如 aiocqhttp, slack, discord 等)。 + """获取这个事件所属的平台的类型(如 aiocqhttp, slack, discord 等)。 - NOTE: 用户可能会同时运行多个相同类型的平台适配器。 + NOTE: 用户可能会同时运行多个相同类型的平台适配器。 """ return self.platform_meta.name def get_platform_id(self): - """获取这个事件所属的平台的 ID。 + """获取这个事件所属的平台的 ID。 - NOTE: 用户可能会同时运行多个相同类型的平台适配器,但能确定的是 ID 是唯一的。 + NOTE: 用户可能会同时运行多个相同类型的平台适配器,但能确定的是 ID 是唯一的。 """ return self.platform_meta.id def get_message_str(self) -> str: - """获取消息字符串。""" + """获取消息字符串。""" return self.message_str def _outline_chain(self, chain: list[BaseMessageComponent] | None) -> str: @@ -166,44 +166,44 @@ class AstrMessageEvent(abc.ABC): return "".join(parts) def get_message_outline(self) -> str: - """获取消息概要。 + """获取消息概要。 - 除了文本消息外,其他消息类型会被转换为对应的占位符。如图片消息会被转换为 [图片]。 + 除了文本消息外,其他消息类型会被转换为对应的占位符。如图片消息会被转换为 [图片]。 """ return self._outline_chain(getattr(self.message_obj, "message", None)) def get_messages(self) -> list[BaseMessageComponent]: - """获取消息链。""" + """获取消息链。""" return getattr(self.message_obj, "message", []) def get_message_type(self) -> MessageType: - """获取消息类型。""" + """获取消息类型。""" message_type = getattr(self.message_obj, "type", None) if isinstance(message_type, MessageType): return message_type return self.session.message_type def get_session_id(self) -> str: - """获取会话id。""" + """获取会话id。""" return self.session_id def get_group_id(self) -> str: - """获取群组id。如果不是群组消息,返回空字符串。""" + """获取群组id。如果不是群组消息,返回空字符串。""" return getattr(self.message_obj, "group_id", "") def get_self_id(self) -> str: - """获取机器人自身的id。""" + """获取机器人自身的id。""" return getattr(self.message_obj, "self_id", "") def get_sender_id(self) -> str: - """获取消息发送者的id。""" + """获取消息发送者的id。""" sender = getattr(self.message_obj, "sender", None) if sender and isinstance(getattr(sender, "user_id", None), str): return sender.user_id return "" def get_sender_name(self) -> str: - """获取消息发送者的名称。(可能会返回空字符串)""" + """获取消息发送者的名称。(可能会返回空字符串)""" sender = getattr(self.message_obj, "sender", None) if not sender: return "" @@ -215,34 +215,34 @@ class AstrMessageEvent(abc.ABC): return str(nickname) def set_extra(self, key, value) -> None: - """设置额外的信息。""" + """设置额外的信息。""" self._extras[key] = value def get_extra(self, key: str | None = None, default=None) -> Any: - """获取额外的信息。""" + """获取额外的信息。""" if key is None: return self._extras return self._extras.get(key, default) def clear_extra(self) -> None: - """清除额外的信息。""" + """清除额外的信息。""" logger.info(f"清除 {self.get_platform_name()} 的额外信息: {self._extras}") self._extras.clear() def is_private_chat(self) -> bool: - """是否是私聊。""" + """是否是私聊。""" return self.get_message_type() == MessageType.FRIEND_MESSAGE def is_wake_up(self) -> bool: - """是否是唤醒机器人的事件。""" + """是否是唤醒机器人的事件。""" return self.is_wake def is_admin(self) -> bool: - """是否是管理员。""" + """是否是管理员。""" return self.role == "admin" async def process_buffer(self, buffer: str, pattern: re.Pattern) -> str: - """将消息缓冲区中的文本按指定正则表达式分割后发送至消息平台,作为不支持流式输出平台的Fallback。""" + """将消息缓冲区中的文本按指定正则表达式分割后发送至消息平台,作为不支持流式输出平台的Fallback。""" while True: match = re.search(pattern, buffer) if not match: @@ -258,9 +258,9 @@ class AstrMessageEvent(abc.ABC): generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False, ) -> None: - """发送流式消息到消息平台,使用异步生成器。 - 目前仅支持: telegram,qq official 私聊。 - Fallback仅支持 aiocqhttp。 + """发送流式消息到消息平台,使用异步生成器。 + 目前仅支持: telegram,qq official 私聊。 + Fallback仅支持 aiocqhttp。 """ asyncio.create_task( Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), @@ -268,9 +268,9 @@ class AstrMessageEvent(abc.ABC): self._has_send_oper = True async def send_typing(self) -> None: - """发送输入中状态。 + """发送输入中状态。 - 默认实现为空,由具体平台按需重写。 + 默认实现为空,由具体平台按需重写。 """ async def _pre_send(self) -> None: @@ -280,18 +280,18 @@ class AstrMessageEvent(abc.ABC): """调度器会在执行 send() 后调用该方法 deprecated in v3.5.18""" def set_result(self, result: MessageEventResult | str) -> None: - """设置消息事件的结果。 + """设置消息事件的结果。 Note: - 事件处理器可以通过设置结果来控制事件是否继续传播,并向消息适配器发送消息。 + 事件处理器可以通过设置结果来控制事件是否继续传播,并向消息适配器发送消息。 - 如果没有设置 `MessageEventResult` 中的 result_type,默认为 CONTINUE。即事件将会继续向后面的 listener 或者 command 传播。 + 如果没有设置 `MessageEventResult` 中的 result_type,默认为 CONTINUE。即事件将会继续向后面的 listener 或者 command 传播。 Example: ``` async def ban_handler(self, event: AstrMessageEvent): if event.get_sender_id() in self.blacklist: - event.set_result(MessageEventResult().set_console_log("由于用户在黑名单,因此消息事件中断处理。")).set_result_type(EventResultType.STOP) + event.set_result(MessageEventResult().set_console_log("由于用户在黑名单,因此消息事件中断处理。")).set_result_type(EventResultType.STOP) return async def check_count(self, event: AstrMessageEvent): @@ -303,50 +303,50 @@ class AstrMessageEvent(abc.ABC): """ if isinstance(result, str): result = MessageEventResult().message(result) - # 兼容外部插件或调用方传入的 chain=None 的情况,确保为可迭代列表 + # 兼容外部插件或调用方传入的 chain=None 的情况,确保为可迭代列表 if isinstance(result, MessageEventResult) and result.chain is None: result.chain = [] self._result = result def stop_event(self) -> None: - """终止事件传播。""" + """终止事件传播。""" if self._result is None: self.set_result(MessageEventResult().stop_event()) else: self._result.stop_event() def continue_event(self) -> None: - """继续事件传播。""" + """继续事件传播。""" if self._result is None: self.set_result(MessageEventResult().continue_event()) else: self._result.continue_event() def is_stopped(self) -> bool: - """是否终止事件传播。""" + """是否终止事件传播。""" if self._result is None: return False # 默认是继续传播 return self._result.is_stopped() def should_call_llm(self, call_llm: bool) -> None: - """是否在此消息事件中禁止默认的 LLM 请求。 + """是否在此消息事件中禁止默认的 LLM 请求。 - 只会阻止 AstrBot 默认的 LLM 请求链路,不会阻止插件中的 LLM 请求。 + 只会阻止 AstrBot 默认的 LLM 请求链路,不会阻止插件中的 LLM 请求。 """ self.call_llm = call_llm def get_result(self) -> MessageEventResult | None: - """获取消息事件的结果。""" + """获取消息事件的结果。""" return self._result def clear_result(self) -> None: - """清除消息事件的结果。""" + """清除消息事件的结果。""" self._result = None """消息链相关""" def make_result(self) -> MessageEventResult: - """创建一个空的消息事件结果。 + """创建一个空的消息事件结果。 Example: ```python @@ -361,20 +361,20 @@ class AstrMessageEvent(abc.ABC): return MessageEventResult() def plain_result(self, text: str) -> MessageEventResult: - """创建一个空的消息事件结果,只包含一条文本消息。""" + """创建一个空的消息事件结果,只包含一条文本消息。""" return MessageEventResult().message(text) def image_result(self, url_or_path: str) -> MessageEventResult: - """创建一个空的消息事件结果,只包含一条图片消息。 + """创建一个空的消息事件结果,只包含一条图片消息。 - 根据开头是否包含 http 来判断是网络图片还是本地图片。 + 根据开头是否包含 http 来判断是网络图片还是本地图片。 """ if url_or_path.startswith("http"): return MessageEventResult().url_image(url_or_path) return MessageEventResult().file_image(url_or_path) def chain_result(self, chain: list[BaseMessageComponent]) -> MessageEventResult: - """创建一个空的消息事件结果,包含指定的消息链。""" + """创建一个空的消息事件结果,包含指定的消息链。""" mer = MessageEventResult() mer.chain = chain return mer @@ -392,7 +392,7 @@ class AstrMessageEvent(abc.ABC): system_prompt: str = "", conversation: Conversation | None = None, ) -> ProviderRequest: - """创建一个 LLM 请求。 + """创建一个 LLM 请求。 Examples: ```py @@ -402,15 +402,15 @@ class AstrMessageEvent(abc.ABC): system_prompt: 系统提示词 - session_id: 已经过时,留空即可 + session_id: 已经过时,留空即可 - image_urls: 可以是 base64:// 或者 http:// 开头的图片链接,也可以是本地图片路径。 + image_urls: 可以是 base64:// 或者 http:// 开头的图片链接,也可以是本地图片路径。 - contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。 + contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。 - func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。 + func_tool_manager: [Deprecated] 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。已过时,请使用 tool_set 参数代替。 - conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。 + conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。 """ if image_urls is None: @@ -434,10 +434,10 @@ class AstrMessageEvent(abc.ABC): """平台适配器""" async def send(self, message: MessageChain) -> None: - """发送消息到消息平台。 + """发送消息到消息平台。 Args: - message (MessageChain): 消息链,具体使用方式请参考文档。 + message (MessageChain): 消息链,具体使用方式请参考文档。 """ # Leverage BLAKE2 hash function to generate a non-reversible hash of the sender ID for privacy. @@ -453,16 +453,16 @@ class AstrMessageEvent(abc.ABC): self._has_send_oper = True async def react(self, emoji: str) -> None: - """对消息添加表情回应。 + """对消息添加表情回应。 - 默认实现为发送一条包含该表情的消息。 - 注意:此实现并不一定符合所有平台的原生“表情回应”行为。 - 如需支持平台原生的消息反应功能,请在对应平台的子类中重写本方法。 + 默认实现为发送一条包含该表情的消息。 + 注意:此实现并不一定符合所有平台的原生“表情回应”行为。 + 如需支持平台原生的消息反应功能,请在对应平台的子类中重写本方法。 """ await self.send(MessageChain([Plain(emoji)])) async def get_group(self, group_id: str | None = None, **kwargs) -> Group | None: - """获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。 + """获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。 适配情况: diff --git a/astrbot/core/platform/astrbot_message.py b/astrbot/core/platform/astrbot_message.py index 3db53fd48..8e0c46b17 100644 --- a/astrbot/core/platform/astrbot_message.py +++ b/astrbot/core/platform/astrbot_message.py @@ -52,7 +52,7 @@ class AstrBotMessage: type: MessageType # 消息类型 self_id: str # 机器人的识别id - session_id: str # 会话id。取决于 unique_session 的设置。 + session_id: str # 会话id。取决于 unique_session 的设置。 message_id: str # 消息id group: Group | None # 群组 sender: MessageMember # 发送者 @@ -71,7 +71,7 @@ class AstrBotMessage: @property def group_id(self) -> str: """向后兼容的 group_id 属性 - 群组id,如果为私聊,则为空 + 群组id,如果为私聊,则为空 """ if self.group: return self.group.group_id diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index f02826001..041f37bad 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -49,8 +49,8 @@ class PlatformManager: self.astrbot_config = config self.platforms_config = config["platform"] self.settings = config["platform_settings"] - """NOTE: 这里是 default 的配置文件,以保证最大的兼容性; - 这个配置中的 unique_session 需要特殊处理, + """NOTE: 这里是 default 的配置文件,以保证最大的兼容性; + 这个配置中的 unique_session 需要特殊处理, 约定整个项目中对 unique_session 的引用都从 default 的配置中获取""" self.event_queue = event_queue @@ -129,7 +129,7 @@ class PlatformManager: sanitized_id, changed = self._sanitize_platform_id(platform_id) if sanitized_id and changed: logger.warning( - "平台 ID %r 包含非法字符 ':' 或 '!',已替换为 %r。", + "平台 ID %r 包含非法字符 ':' 或 '!',已替换为 %r。", platform_id, sanitized_id, ) @@ -137,7 +137,7 @@ class PlatformManager: self.astrbot_config.save_config() else: logger.error( - f"平台 ID {platform_id!r} 不能为空,跳过加载该平台适配器。", + f"平台 ID {platform_id!r} 不能为空,跳过加载该平台适配器。", ) return @@ -149,14 +149,14 @@ class PlatformManager: import_module(module_path, package=__package__) except (ImportError, ModuleNotFoundError) as e: logger.error( - f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。", + f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。", ) except Exception as e: - logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。") + logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。") if platform_config["type"] not in platform_cls_map: logger.error( - f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误", + f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误", ) return cls_type = platform_cls_map[platform_config["type"]] @@ -280,7 +280,7 @@ class PlatformManager: elif stat.get("status") == PlatformStatus.ERROR.value: error_count += 1 except Exception as e: - # 如果获取统计信息失败,记录基本信息 + # 如果获取统计信息失败,记录基本信息 logger.warning(f"获取平台统计信息失败: {e}") stats_list.append( { diff --git a/astrbot/core/platform/message_session.py b/astrbot/core/platform/message_session.py index 89639941e..851b6d3b1 100644 --- a/astrbot/core/platform/message_session.py +++ b/astrbot/core/platform/message_session.py @@ -5,12 +5,12 @@ from astrbot.core.platform.message_type import MessageType @dataclass class MessageSession: - """描述一条消息在 AstrBot 中对应的会话的唯一标识。 - 如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。 + """描述一条消息在 AstrBot 中对应的会话的唯一标识。 + 如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。它会在 __post_init__ 中自动设置为 platform_name 的值。 """ platform_name: str - """平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。""" + """平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。""" message_type: MessageType session_id: str platform_id: str = field(init=False) diff --git a/astrbot/core/platform/message_type.py b/astrbot/core/platform/message_type.py index 25b7cdc48..5ebc3b2e7 100644 --- a/astrbot/core/platform/message_type.py +++ b/astrbot/core/platform/message_type.py @@ -3,5 +3,5 @@ from enum import Enum class MessageType(Enum): GROUP_MESSAGE = "GroupMessage" # 群组形式的消息 - FRIEND_MESSAGE = "FriendMessage" # 私聊、好友等单聊消息 - OTHER_MESSAGE = "OtherMessage" # 其他类型的消息,如系统消息等 + FRIEND_MESSAGE = "FriendMessage" # 私聊、好友等单聊消息 + OTHER_MESSAGE = "OtherMessage" # 其他类型的消息,如系统消息等 diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index a7c181217..5c5b57dff 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -38,7 +38,7 @@ class Platform(abc.ABC): super().__init__() # 平台配置 self.config = config - # 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。 + # 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。 self._event_queue = event_queue self.client_self_id = uuid.uuid4().hex @@ -118,15 +118,15 @@ class Platform(abc.ABC): @abc.abstractmethod def run(self) -> Coroutine[Any, Any, None]: - """得到一个平台的运行实例,需要返回一个协程对象。""" + """得到一个平台的运行实例,需要返回一个协程对象。""" raise NotImplementedError async def terminate(self) -> None: - """终止一个平台的运行实例。""" + """终止一个平台的运行实例。""" @abc.abstractmethod def meta(self) -> PlatformMetadata: - """得到一个平台的元数据。""" + """得到一个平台的元数据。""" raise NotImplementedError async def send_by_session( @@ -134,30 +134,30 @@ class Platform(abc.ABC): session: MessageSesion, message_chain: MessageChain, ) -> None: - """通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。 + """通过会话发送消息。该方法旨在让插件能够直接通过**可持久化的会话数据**发送消息,而不需要保存 event 对象。 - 异步方法。 + 异步方法。 """ await Metric.upload(msg_event_tick=1, adapter_name=self.meta().name) def commit_event(self, event: AstrMessageEvent) -> None: - """提交一个事件到事件队列。""" + """提交一个事件到事件队列。""" self._event_queue.put_nowait(event) def get_client(self) -> object: - """获取平台的客户端对象。""" + """获取平台的客户端对象。""" async def webhook_callback(self, request: Any) -> Any: - """统一 Webhook 回调入口。 + """统一 Webhook 回调入口。 - 支持统一 Webhook 模式的平台需要实现此方法。 - 当 Dashboard 收到 /api/platform/webhook/{uuid} 请求时,会调用此方法。 + 支持统一 Webhook 模式的平台需要实现此方法。 + 当 Dashboard 收到 /api/platform/webhook/{uuid} 请求时,会调用此方法。 Args: request: Quart 请求对象 Returns: - 响应内容,格式取决于具体平台的要求 + 响应内容,格式取决于具体平台的要求 Raises: NotImplementedError: 平台未实现统一 Webhook 模式 diff --git a/astrbot/core/platform/platform_metadata.py b/astrbot/core/platform/platform_metadata.py index 2d01b921d..91dfdec47 100644 --- a/astrbot/core/platform/platform_metadata.py +++ b/astrbot/core/platform/platform_metadata.py @@ -4,34 +4,34 @@ from dataclasses import dataclass @dataclass class PlatformMetadata: name: str - """平台的名称,即平台的类型,如 aiocqhttp, discord, slack""" + """平台的名称,即平台的类型,如 aiocqhttp, discord, slack""" description: str """平台的描述""" id: str - """平台的唯一标识符,用于配置中识别特定平台""" + """平台的唯一标识符,用于配置中识别特定平台""" default_config_tmpl: dict | None = None """平台的默认配置模板""" adapter_display_name: str | None = None - """显示在 WebUI 配置页中的平台名称,如空则是 name""" + """显示在 WebUI 配置页中的平台名称,如空则是 name""" logo_path: str | None = None - """平台适配器的 logo 文件路径(相对于插件目录)""" + """平台适配器的 logo 文件路径(相对于插件目录)""" support_streaming_message: bool = True """平台是否支持真实流式传输""" support_proactive_message: bool = True - """平台是否支持主动消息推送(非用户触发)""" + """平台是否支持主动消息推送(非用户触发)""" module_path: str | None = None - """注册该适配器的模块路径,用于插件热重载时清理""" + """注册该适配器的模块路径,用于插件热重载时清理""" i18n_resources: dict[str, dict] | None = None - """国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}} + """国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}} 参考 https://github.com/AstrBotDevs/AstrBot/pull/5045 """ config_metadata: dict | None = None - """配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容 + """配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容 参考 https://github.com/AstrBotDevs/AstrBot/pull/5045 """ diff --git a/astrbot/core/platform/register.py b/astrbot/core/platform/register.py index 62ec5070a..bdf728105 100644 --- a/astrbot/core/platform/register.py +++ b/astrbot/core/platform/register.py @@ -18,17 +18,17 @@ def register_platform_adapter( i18n_resources: dict[str, dict] | None = None, config_metadata: dict | None = None, ): - """用于注册平台适配器的带参装饰器。 + """用于注册平台适配器的带参装饰器。 - default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。 - logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。 - config_metadata 指定了配置项的元数据,用于 WebUI 生成表单。如果不指定,WebUI 将会把配置项渲染为原始的键值对编辑框。 + default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。 + logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。 + config_metadata 指定了配置项的元数据,用于 WebUI 生成表单。如果不指定,WebUI 将会把配置项渲染为原始的键值对编辑框。 """ def decorator(cls): if adapter_name in platform_cls_map: raise ValueError( - f"平台适配器 {adapter_name} 已经注册过了,可能发生了适配器命名冲突。", + f"平台适配器 {adapter_name} 已经注册过了,可能发生了适配器命名冲突。", ) # 添加必备选项 @@ -64,12 +64,12 @@ def register_platform_adapter( def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str]: - """根据模块路径前缀注销平台适配器。 + """根据模块路径前缀注销平台适配器。 - 在插件热重载时调用,用于清理该插件注册的所有平台适配器。 + 在插件热重载时调用,用于清理该插件注册的所有平台适配器。 Args: - module_path_prefix: 模块路径前缀,如 "data.plugins.my_plugin" + module_path_prefix: 模块路径前缀,如 "data.plugins.my_plugin" Returns: 被注销的平台适配器名称列表 diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 686624d11..5c0131048 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -52,13 +52,13 @@ class AiocqhttpMessageEvent(AstrMessageEvent): import pathlib try: - # 使用 pathlib 处理路径,能更好地处理 Windows/Linux 差异 + # 使用 pathlib 处理路径,能更好地处理 Windows/Linux 差异 path_obj = pathlib.Path(file_val) - # 如果是绝对路径且不包含协议头 (://),则转换为标准的 file: URI + # 如果是绝对路径且不包含协议头 (://),则转换为标准的 file: URI if path_obj.is_absolute() and "://" not in file_val: d["data"]["file"] = path_obj.as_uri() except Exception: - # 如果不是合法路径(例如已经是特定的特殊字符串),则跳过转换 + # 如果不是合法路径(例如已经是特定的特殊字符串),则跳过转换 pass return d if isinstance(segment, Video): @@ -73,7 +73,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): ret = [] for segment in message_chain.chain: if isinstance(segment, At): - # At 组件后插入一个空格,避免与后续文本粘连 + # At 组件后插入一个空格,避免与后续文本粘连 d = await AiocqhttpMessageEvent._from_segment_to_dict(segment) ret.append(d) ret.append({"type": "text", "data": {"text": " "}}) @@ -109,7 +109,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): await bot.send(event=event, message=messages) else: raise ValueError( - f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})", + f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})", ) @classmethod @@ -121,17 +121,17 @@ class AiocqhttpMessageEvent(AstrMessageEvent): is_group: bool = False, session_id: str | None = None, ) -> None: - """发送消息至 QQ 协议端(aiocqhttp)。 + """发送消息至 QQ 协议端(aiocqhttp)。 Args: bot (CQHttp): aiocqhttp 机器人实例 message_chain (MessageChain): 要发送的消息链 event (Event | None, optional): aiocqhttp 事件对象. is_group (bool, optional): 是否为群消息. - session_id (str | None, optional): 会话 ID(群号或 QQ 号 + session_id (str | None, optional): 会话 ID(群号或 QQ 号 """ - # 转发消息、文件消息不能和普通消息混在一起发送 + # 转发消息、文件消息不能和普通消息混在一起发送 send_one_by_one = any( isinstance(seg, Node | Nodes | File) for seg in message_chain.chain ) @@ -160,7 +160,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): # 使用 OneBot V11 文件 API 发送文件 file_path = seg.file_ or seg.url if not file_path: - logger.warning("无法发送文件:文件路径或 URL 为空。") + logger.warning("无法发送文件:文件路径或 URL 为空。") continue file_name = seg.name or "file" @@ -169,7 +169,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): ) if session_id_int is None: - logger.warning(f"无法发送文件:无效的 session_id: {session_id}") + logger.warning(f"无法发送文件:无效的 session_id: {session_id}") continue if is_group: @@ -222,14 +222,14 @@ class AiocqhttpMessageEvent(AstrMessageEvent): return await super().send_streaming(generator, use_fallback) buffer = "" - pattern = re.compile(r"[^。?!~…]+[。?!~…]+") + pattern = re.compile(r"[^。?!~…]+[。?!~…]+") async for chain in generator: if isinstance(chain, MessageChain): for comp in chain.chain: if isinstance(comp, Plain): buffer += comp.text - if any(p in buffer for p in "。?!~…"): + if any(p in buffer for p in "。?!~…"): buffer = await self.process_buffer(buffer, pattern) else: await self.send(MessageChain(chain=[comp])) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 299643366..8f98ed2b3 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -36,7 +36,7 @@ from .aiocqhttp_message_event import AiocqhttpMessageEvent @register_platform_adapter( "aiocqhttp", - "适用于 OneBot V11 标准的消息平台适配器,支持反向 WebSockets。", + "适用于 OneBot V11 标准的消息平台适配器,支持反向 WebSockets。", support_streaming_message=False, ) class AiocqhttpAdapter(Platform): @@ -54,7 +54,7 @@ class AiocqhttpAdapter(Platform): self.metadata = PlatformMetadata( name="aiocqhttp", - description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。", + description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。", id=cast(str, self.config.get("id")), support_streaming_message=False, ) @@ -111,7 +111,7 @@ class AiocqhttpAdapter(Platform): @self.bot.on_websocket_connection def on_websocket_connection(_) -> None: - logger.info("aiocqhttp(OneBot v11) 适配器已连接。") + logger.info("aiocqhttp(OneBot v11) 适配器已连接。") async def send_by_session( self, @@ -126,7 +126,7 @@ class AiocqhttpAdapter(Platform): await AiocqhttpMessageEvent.send_message( bot=self.bot, message_chain=message_chain, - event=None, # 这里不需要 event,因为是通过 session 发送的 + event=None, # 这里不需要 event,因为是通过 session 发送的 is_group=is_group, session_id=session_id, ) @@ -210,7 +210,7 @@ class AiocqhttpAdapter(Platform): """OneBot V11 消息类事件 @param event: 事件对象 - @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。 + @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。 """ assert event.sender is not None abm = AstrBotMessage() @@ -237,7 +237,7 @@ class AiocqhttpAdapter(Platform): message_str = "" if not isinstance(event.message, list): - err = f"aiocqhttp: 无法识别的消息类型: {event.message!s},此条消息将被忽略。如果您在使用 go-cqhttp,请将其配置文件中的 message.post-format 更改为 array。" + err = f"aiocqhttp: 无法识别的消息类型: {event.message!s},此条消息将被忽略。如果您在使用 go-cqhttp,请将其配置文件中的 message.post-format 更改为 array。" logger.critical(err) try: await self.bot.send(event, err) @@ -251,7 +251,7 @@ class AiocqhttpAdapter(Platform): if t == "text": current_text = "".join(m["data"]["text"] for m in m_group).strip() if not current_text: - # 如果文本段为空,则跳过 + # 如果文本段为空,则跳过 continue message_str += current_text a = ComponentTypes[t](text=current_text) @@ -287,7 +287,7 @@ class AiocqhttpAdapter(Platform): ) if ret and "url" in ret: file_url = ret["url"] # https - # 优先从 API 返回值获取文件名,其次从原始消息数据获取 + # 优先从 API 返回值获取文件名,其次从原始消息数据获取 file_name = ( ret.get("file_name", "") or ret.get("name", "") @@ -300,9 +300,9 @@ class AiocqhttpAdapter(Platform): logger.error(f"获取文件失败: {ret}") except ActionFailed as e: - logger.error(f"获取文件失败: {e},此消息段将被忽略。") + logger.error(f"获取文件失败: {e},此消息段将被忽略。") except BaseException as e: - logger.error(f"获取文件失败: {e},此消息段将被忽略。") + logger.error(f"获取文件失败: {e},此消息段将被忽略。") elif t == "reply": for m in m_group: @@ -315,7 +315,7 @@ class AiocqhttpAdapter(Platform): action="get_msg", message_id=int(m["data"]["id"]), ) - # 添加必要的 post_type 字段,防止 Event.from_payload 报错 + # 添加必要的 post_type 字段,防止 Event.from_payload 报错 reply_event_data["post_type"] = "message" new_event = Event.from_payload(reply_event_data) if not new_event: @@ -341,7 +341,7 @@ class AiocqhttpAdapter(Platform): abm.message.append(reply_seg) except BaseException as e: - logger.error(f"获取引用消息失败: {e}。") + logger.error(f"获取引用消息失败: {e}。") a = ComponentTypes[t](**m["data"]) abm.message.append(a) elif t == "at": @@ -383,17 +383,17 @@ class AiocqhttpAdapter(Platform): ) if is_at_self and not first_at_self_processed: - # 第一个@是机器人,不添加到message_str + # 第一个@是机器人,不添加到message_str first_at_self_processed = True else: - # 非第一个@机器人或@其他用户,添加到message_str + # 非第一个@机器人或@其他用户,添加到message_str at_parts.append(f" @{nickname}({m['data']['qq']}) ") else: abm.message.append(At(qq=str(m["data"]["qq"]), name="")) except ActionFailed as e: - logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") + logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") except BaseException as e: - logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") + logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。") message_str += "".join(at_parts) elif t == "markdown": @@ -406,7 +406,7 @@ class AiocqhttpAdapter(Platform): try: if t not in ComponentTypes: logger.warning( - f"不支持的消息段类型,已忽略: {t}, data={m['data']}" + f"不支持的消息段类型,已忽略: {t}, data={m['data']}" ) continue a = ComponentTypes[t](**m["data"]) @@ -426,7 +426,7 @@ class AiocqhttpAdapter(Platform): 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", + "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199", ) self.host = "0.0.0.0" self.port = 6199 diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 470766081..b35a876c2 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -111,7 +111,7 @@ class DingtalkPlatformAdapter(Platform): staff_id = await self._get_sender_staff_id(session) if not staff_id: logger.warning( - "钉钉私聊会话缺少 staff_id 映射,回退使用 session_id 作为 userId 发送", + "钉钉私聊会话缺少 staff_id 映射,回退使用 session_id 作为 userId 发送", ) staff_id = session.session_id await self.send_message_chain_to_user( @@ -168,7 +168,7 @@ class DingtalkPlatformAdapter(Platform): abm.raw_message = message if abm.type == MessageType.GROUP_MESSAGE: - # 处理所有被 @ 的用户(包括机器人自己,因 at_users 已包含) + # 处理所有被 @ 的用户(包括机器人自己,因 at_users 已包含) if message.at_users: for user in message.at_users: if id := self._id_to_sid(user.dingtalk_id): @@ -200,7 +200,7 @@ class DingtalkPlatformAdapter(Platform): str, (image_content.download_code if image_content else "") or "" ) if not download_code: - logger.warning("钉钉图片消息缺少 downloadCode,已跳过") + logger.warning("钉钉图片消息缺少 downloadCode,已跳过") else: f_path = await self.download_ding_file( download_code, @@ -210,7 +210,7 @@ class DingtalkPlatformAdapter(Platform): if f_path: abm.message.append(Image.fromFileSystem(f_path)) else: - logger.warning("钉钉图片消息下载失败,无法解析为图片") + logger.warning("钉钉图片消息下载失败,无法解析为图片") case "richText": rtc: dingtalk_stream.RichTextContent = cast( dingtalk_stream.RichTextContent, message.rich_text_content @@ -227,7 +227,7 @@ class DingtalkPlatformAdapter(Platform): download_code = cast(str, content.get("downloadCode") or "") if not download_code: logger.warning( - "钉钉富文本图片消息缺少 downloadCode,已跳过" + "钉钉富文本图片消息缺少 downloadCode,已跳过" ) continue if not robot_code: @@ -246,7 +246,7 @@ class DingtalkPlatformAdapter(Platform): case "audio" | "voice": download_code = cast(str, raw_content.get("downloadCode") or "") if not download_code: - logger.warning("钉钉语音消息缺少 downloadCode,已跳过") + logger.warning("钉钉语音消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉语音消息解析失败: 回调中缺少 robotCode") else: @@ -264,7 +264,7 @@ class DingtalkPlatformAdapter(Platform): case "file": download_code = cast(str, raw_content.get("downloadCode") or "") if not download_code: - logger.warning("钉钉文件消息缺少 downloadCode,已跳过") + logger.warning("钉钉文件消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉文件消息解析失败: 回调中缺少 robotCode") else: @@ -481,7 +481,7 @@ class DingtalkPlatformAdapter(Platform): logger.warning(f"清理临时文件失败: {file_path}, {e}") async def _prepare_voice_for_dingtalk(self, input_path: str) -> tuple[str, bool]: - """优先转换为 OGG(Opus),不可用时回退 AMR。""" + """优先转换为 OGG(Opus),不可用时回退 AMR。""" lower_path = input_path.lower() if lower_path.endswith((".amr", ".ogg")): return input_path, False @@ -490,7 +490,7 @@ class DingtalkPlatformAdapter(Platform): 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(f"钉钉语音转 OGG 失败,回退 AMR: {e}") converted = await convert_audio_format(input_path, "amr") return converted, converted != input_path @@ -747,7 +747,7 @@ class DingtalkPlatformAdapter(Platform): async def run(self) -> None: # await self.client_.start() - # 钉钉的 SDK 并没有实现真正的异步,start() 里面有堵塞方法。 + # 钉钉的 SDK 并没有实现真正的异步,start() 里面有堵塞方法。 def start_client(loop: asyncio.AbstractEventLoop) -> None: try: self._shutdown_event = threading.Event() diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py index 3331c5147..09b7b8a94 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_event.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_event.py @@ -29,7 +29,7 @@ class DingtalkMessageEvent(AstrMessageEvent): await super().send(message) async def send_streaming(self, generator, use_fallback: bool = False): - # 钉钉统一回退为缓冲发送:最终发送仍使用新的 HTTP 消息接口。 + # 钉钉统一回退为缓冲发送:最终发送仍使用新的 HTTP 消息接口。 buffer = None async for chain in generator: if not buffer: diff --git a/astrbot/core/platform/sources/discord/client.py b/astrbot/core/platform/sources/discord/client.py index ebd32c471..d086b154a 100644 --- a/astrbot/core/platform/sources/discord/client.py +++ b/astrbot/core/platform/sources/discord/client.py @@ -19,7 +19,7 @@ class DiscordBotClient(discord.Bot): self.token = token self.proxy = proxy - # 设置Intent权限,遵循权限最小化原则 + # 设置Intent权限,遵循权限最小化原则 intents = discord.Intents.default() intents.message_content = True # 订阅消息内容事件 (Privileged) intents.members = True # 订阅成员事件 (Privileged) @@ -39,7 +39,7 @@ class DiscordBotClient(discord.Bot): return logger.info(f"[Discord] 已作为 {self.user} (ID: {self.user.id}) 登录") - logger.info("[Discord] 客户端已准备就绪。") + logger.info("[Discord] 客户端已准备就绪。") if self.on_ready_once_callback and not self._ready_once_fired: self._ready_once_fired = True @@ -131,7 +131,7 @@ class DiscordBotClient(discord.Bot): return str(interaction_data) async def start_polling(self) -> None: - """开始轮询消息,这是个阻塞方法""" + """开始轮询消息,这是个阻塞方法""" await self.start(self.token) @override diff --git a/astrbot/core/platform/sources/discord/components.py b/astrbot/core/platform/sources/discord/components.py index 433509f5e..91f579627 100644 --- a/astrbot/core/platform/sources/discord/components.py +++ b/astrbot/core/platform/sources/discord/components.py @@ -91,7 +91,7 @@ class DiscordReference(BaseMessageComponent): class DiscordView(BaseMessageComponent): - """Discord视图组件,包含按钮和选择菜单""" + """Discord视图组件,包含按钮和选择菜单""" type: str = "discord_view" diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 1a09294e6..75f17d391 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -68,7 +68,7 @@ class DiscordPlatformAdapter(Platform): """通过会话发送消息""" if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法发送消息" + "[Discord] 客户端未就绪 (self.client.user is None),无法发送消息" ) return @@ -140,7 +140,7 @@ class DiscordPlatformAdapter(Platform): # 初始化 Discord 客户端 token = str(self.config.get("discord_token")) if not token: - logger.error("[Discord] Bot Token 未配置。请在配置文件中正确设置 token。") + logger.error("[Discord] Bot Token 未配置。请在配置文件中正确设置 token。") return proxy = self.config.get("discord_proxy") or None @@ -162,9 +162,9 @@ class DiscordPlatformAdapter(Platform): 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("[Discord] 登录失败。请检查你的 Bot Token 是否正确。") except discord.errors.ConnectionClosed: - logger.warning("[Discord] 与 Discord 的连接已关闭。") + logger.warning("[Discord] 与 Discord 的连接已关闭。") except Exception as e: logger.error(f"[Discord] 适配器运行时发生意外错误: {e}", exc_info=True) @@ -192,7 +192,7 @@ class DiscordPlatformAdapter(Platform): content = message.content - # 如果机器人被@,移除@部分 + # 如果机器人被@,移除@部分 # 剥离 User Mention (<@id>, <@!id>) if self.client and self.client.user: mention_str = f"<@{self.client.user.id}>" @@ -202,7 +202,7 @@ class DiscordPlatformAdapter(Platform): elif content.startswith(mention_str_nickname): content = content[len(mention_str_nickname) :].lstrip() - # 剥离 Role Mention(bot 拥有的任一角色被提及,<@&role_id>) + # 剥离 Role Mention(bot 拥有的任一角色被提及,<@&role_id>) if ( hasattr(message, "role_mentions") and hasattr(message, "guild") @@ -252,7 +252,7 @@ class DiscordPlatformAdapter(Platform): async def convert_message(self, data: dict) -> AstrBotMessage: """将平台消息转换成 AstrBotMessage""" - # 由于 on_interaction 已被禁用,我们只处理普通消息 + # 由于 on_interaction 已被禁用,我们只处理普通消息 return self._convert_message_to_abm(data) async def handle_msg(self, message: AstrBotMessage, followup_webhook=None) -> None: @@ -268,7 +268,7 @@ class DiscordPlatformAdapter(Platform): if self.client.user is None: logger.error( - "[Discord] 客户端未就绪 (self.client.user is None),无法处理消息" + "[Discord] 客户端未就绪 (self.client.user is None),无法处理消息" ) return @@ -282,24 +282,24 @@ class DiscordPlatformAdapter(Platform): self.commit_event(message_event) return - # 2. 处理普通消息(提及检测) - # 确保 raw_message 是 discord.Message 类型,以便静态检查通过 + # 2. 处理普通消息(提及检测) + # 确保 raw_message 是 discord.Message 类型,以便静态检查通过 raw_message = message.raw_message if not isinstance(raw_message, discord.Message): logger.warning( - f"[Discord] 收到非 Message 类型的消息: {type(raw_message)},已忽略。" + f"[Discord] 收到非 Message 类型的消息: {type(raw_message)},已忽略。" ) return - # 检查是否被@(User Mention 或 Bot 拥有的 Role Mention) + # 检查是否被@(User Mention 或 Bot 拥有的 Role Mention) is_mention = False # User Mention - # 此时 Pylance 知道 raw_message 是 discord.Message,具有 mentions 属性 + # 此时 Pylance 知道 raw_message 是 discord.Message,具有 mentions 属性 if self.client.user in raw_message.mentions: is_mention = True - # Role Mention(Bot 拥有的角色被提及) + # Role Mention(Bot 拥有的角色被提及) if not is_mention and raw_message.role_mentions: bot_member = None if raw_message.guild: @@ -319,7 +319,7 @@ class DiscordPlatformAdapter(Platform): ): is_mention = True - # 如果是被@的消息,设置为唤醒状态 + # 如果是被@的消息,设置为唤醒状态 if is_mention: message_event.is_wake = True message_event.is_at_or_wake_command = True @@ -337,17 +337,17 @@ class DiscordPlatformAdapter(Platform): try: await asyncio.wait_for(self._polling_task, timeout=10) except asyncio.CancelledError: - logger.info("[Discord] polling_task 已取消。") + logger.info("[Discord] polling_task 已取消。") except Exception as e: logger.warning(f"[Discord] polling_task 取消异常: {e}") - logger.info("[Discord] 跳过斜杠指令清理,避免重启时重复创建命令。") + logger.info("[Discord] 跳过斜杠指令清理,避免重启时重复创建命令。") logger.info("[Discord] 正在关闭 Discord 客户端... (step 2)") 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.info("[Discord] 适配器已终止。") def register_handler(self, handler_info) -> None: """注册处理器信息""" @@ -399,17 +399,17 @@ class DiscordPlatformAdapter(Platform): f"[Discord] 准备同步 {len(registered_commands)} 个指令: {', '.join(registered_commands)}", ) else: - logger.info("[Discord] 没有发现可注册的指令。") + logger.info("[Discord] 没有发现可注册的指令。") # 使用 Pycord 的方法同步指令 - # 注意:这可能需要一些时间,并且有频率限制 + # 注意:这可能需要一些时间,并且有频率限制 try: await self.client.sync_commands() - logger.info("[Discord] 指令同步完成。") + logger.info("[Discord] 指令同步完成。") except HTTPException as exc: if getattr(exc, "code", None) == 30034: logger.warning( - "[Discord] 跳过指令同步:已达到 Discord 每日 application command create 限额。" + "[Discord] 跳过指令同步:已达到 Discord 每日 application command create 限额。" ) return raise @@ -420,7 +420,7 @@ class DiscordPlatformAdapter(Platform): async def dynamic_callback( ctx: discord.ApplicationContext, params: str | None = None ) -> None: - # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter + # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter logger.debug(f"[Discord] 回调函数触发: {cmd_name}") logger.debug(f"[Discord] 回调函数参数: {ctx}") logger.debug(f"[Discord] 回调函数参数: {params}") @@ -429,12 +429,12 @@ class DiscordPlatformAdapter(Platform): message_str_for_filter += f" {params}" logger.debug( - f"[Discord] 斜杠指令 '{cmd_name}' 被触发。 " + f"[Discord] 斜杠指令 '{cmd_name}' 被触发。 " f"原始参数: '{params}'. " f"构建的指令字符串: '{message_str_for_filter}'", ) - # 尝试立即响应,防止超时 + # 尝试立即响应,防止超时 followup_webhook = None try: await ctx.defer() @@ -449,7 +449,7 @@ class DiscordPlatformAdapter(Platform): abm.type = self._get_message_type(channel, ctx.guild_id) abm.group_id = self._get_channel_id(channel) else: - # 防守式兜底:channel 取不到时,仍能根据 guild_id/channel_id 推断会话信息 + # 防守式兜底:channel 取不到时,仍能根据 guild_id/channel_id 推断会话信息 abm.type = ( MessageType.GROUP_MESSAGE if ctx.guild_id is not None @@ -494,7 +494,7 @@ class DiscordPlatformAdapter(Platform): cmd_filter_instance = event_filter elif isinstance(event_filter, CommandGroupFilter): - # 暂不支持指令组直接注册为斜杠指令,因为它们没有 handle 方法 + # 暂不支持指令组直接注册为斜杠指令,因为它们没有 handle 方法 return None if not 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 02d4dae86..c6b795d8e 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_event.py +++ b/astrbot/core/platform/sources/discord/discord_platform_event.py @@ -24,7 +24,7 @@ from .client import DiscordBotClient from .components import DiscordEmbed, DiscordView -# 自定义Discord视图组件(兼容旧版本) +# 自定义Discord视图组件(兼容旧版本) class DiscordViewComponent(BaseMessageComponent): type: str = "discord_view" @@ -73,7 +73,7 @@ class DiscordPlatformEvent(AstrMessageEvent): 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("[Discord] 尝试发送空消息,已忽略。") return # 根据上下文执行发送/回复操作 @@ -208,7 +208,7 @@ class DiscordPlatformEvent(AstrMessageEvent): ) except (ValueError, TypeError, binascii.Error): logger.debug( - f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}", + f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}", ) path = Path(file_content) if await asyncio.to_thread(path.exists): @@ -224,7 +224,7 @@ class DiscordPlatformEvent(AstrMessageEvent): files.append(discord_file) except Exception: - # 使用 getattr 来安全地访问 i.file,以防 i 本身就是问题 + # 使用 getattr 来安全地访问 i.file,以防 i 本身就是问题 file_info = getattr(i, "file", "未知") logger.error( f"[Discord] 处理图片时发生未知严重错误: {file_info}", @@ -242,7 +242,7 @@ class DiscordPlatformEvent(AstrMessageEvent): ) else: logger.warning( - f"[Discord] 获取文件失败,路径不存在: {file_path_str}", + f"[Discord] 获取文件失败,路径不存在: {file_path_str}", ) else: logger.warning(f"[Discord] 获取文件失败: {i.name}") @@ -252,10 +252,10 @@ class DiscordPlatformEvent(AstrMessageEvent): # Discord Embed消息 embeds.append(i.to_discord_embed()) elif isinstance(i, DiscordView): - # Discord视图组件(按钮、选择菜单等) + # Discord视图组件(按钮、选择菜单等) view = i.to_discord_view() elif isinstance(i, DiscordViewComponent): - # 如果消息链中包含Discord视图组件(兼容旧版本) + # 如果消息链中包含Discord视图组件(兼容旧版本) if isinstance(i.view, discord.ui.View): view = i.view else: @@ -263,7 +263,7 @@ class DiscordPlatformEvent(AstrMessageEvent): content = "".join(content_parts) if len(content) > 2000: - logger.warning("[Discord] 消息内容超过2000字符,将被截断。") + logger.warning("[Discord] 消息内容超过2000字符,将被截断。") content = content[:2000] return content, files, view, embeds, reference_message_id diff --git a/astrbot/core/platform/sources/kook/kook_adapter.py b/astrbot/core/platform/sources/kook/kook_adapter.py index 7095d7447..73090e101 100644 --- a/astrbot/core/platform/sources/kook/kook_adapter.py +++ b/astrbot/core/platform/sources/kook/kook_adapter.py @@ -114,7 +114,7 @@ class KookPlatformAdapter(Platform): await self._cleanup() async def _main_loop(self): - """主循环,处理连接和重连""" + """主循环,处理连接和重连""" consecutive_failures = 0 max_consecutive_failures = self.kook_config.max_consecutive_failures max_retry_delay = self.kook_config.max_retry_delay @@ -127,32 +127,32 @@ class KookPlatformAdapter(Platform): success = await self.client.connect() if success: - logger.info("[KOOK] 连接成功,开始监听消息") + logger.info("[KOOK] 连接成功,开始监听消息") consecutive_failures = 0 # 重置失败计数 - # 等待连接结束(可能是正常关闭或异常) + # 等待连接结束(可能是正常关闭或异常) while self.client.running and self.running: try: - # 等待 client 内部触发 _stop_event,或者超时 1 秒后重试 + # 等待 client 内部触发 _stop_event,或者超时 1 秒后重试 # 使用 wait_for 配合 timeout 是为了防止极端情况下 self.running 变化没被察觉 await asyncio.wait_for( self.client.wait_until_closed(), timeout=1.0 ) except asyncio.TimeoutError: - # 正常超时,继续下一轮 while 检查 + # 正常超时,继续下一轮 while 检查 continue if self.running: - logger.warning("[KOOK] 连接断开,准备重连") + logger.warning("[KOOK] 连接断开,准备重连") else: consecutive_failures += 1 logger.error( - f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}" + f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}" ) if consecutive_failures >= max_consecutive_failures: - logger.error("[KOOK] 连续失败次数过多,停止重连") + logger.error("[KOOK] 连续失败次数过多,停止重连") break # 等待一段时间后重试 @@ -167,7 +167,7 @@ class KookPlatformAdapter(Platform): logger.error(f"[KOOK] 主循环异常: {e}") if consecutive_failures >= max_consecutive_failures: - logger.error("[KOOK] 连续异常次数过多,停止重连") + logger.error("[KOOK] 连续异常次数过多,停止重连") break await asyncio.sleep(5) diff --git a/astrbot/core/platform/sources/kook/kook_client.py b/astrbot/core/platform/sources/kook/kook_client.py index 937917376..984d7a69d 100644 --- a/astrbot/core/platform/sources/kook/kook_client.py +++ b/astrbot/core/platform/sources/kook/kook_client.py @@ -39,7 +39,7 @@ class KookClient: "Authorization": f"Bot {self.config.token}", } ) - self.event_callback = event_callback # 回调函数,用于处理接收到的事件 + self.event_callback = event_callback # 回调函数,用于处理接收到的事件 self.ws = None self.heartbeat_task = None self._stop_event = asyncio.Event() # 用于通知连接结束 @@ -71,7 +71,7 @@ class KookClient: async with self._http_client.get(url) as resp: if resp.status != 200: logger.error( - f"[KOOK] 获取机器人账号信息失败,状态码: {resp.status} , {await resp.text()}" + f"[KOOK] 获取机器人账号信息失败,状态码: {resp.status} , {await resp.text()}" ) return try: @@ -114,7 +114,7 @@ class KookClient: try: async with self._http_client.get(url, params=params) as resp: if resp.status != 200: - logger.error(f"[KOOK] 获取gateway失败,状态码: {resp.status}") + logger.error(f"[KOOK] 获取gateway失败,状态码: {resp.status}") return None resp_content = KookGatewayIndexResponse.from_dict(await resp.json()) @@ -184,7 +184,7 @@ class KookClient: while self.running: try: if self.ws is None: - logger.error("[KOOK] WebSocket 对象丢失,结束监听流程。") + logger.error("[KOOK] WebSocket 对象丢失,结束监听流程。") break msg = await asyncio.wait_for(self.ws.recv(), timeout=10) @@ -208,7 +208,7 @@ class KookClient: continue except asyncio.TimeoutError: - # 超时检查,继续循环 + # 超时检查,继续循环 continue except websockets.exceptions.ConnectionClosed: logger.warning("[KOOK] WebSocket连接已关闭") @@ -258,11 +258,11 @@ class KookClient: if code == 0: self.session_id = data.session_id - logger.info(f"[KOOK] 握手成功,session_id: {self.session_id}") + logger.info(f"[KOOK] 握手成功,session_id: {self.session_id}") else: - logger.error(f"[KOOK] 握手失败,错误码: {code}") + logger.error(f"[KOOK] 握手失败,错误码: {code}") if code == 40103: # token过期 - logger.error("[KOOK] Token已过期,需要重新获取") + logger.error("[KOOK] Token已过期,需要重新获取") self.running = False async def _handle_pong(self): @@ -281,7 +281,7 @@ class KookClient: async def _handle_resume_ack(self, data: KookResumeAckEventData): """处理RESUME确认""" self.session_id = data.session_id - logger.info(f"[KOOK] Resume成功,session_id: {self.session_id}") + logger.info(f"[KOOK] Resume成功,session_id: {self.session_id}") async def _heartbeat_loop(self): """心跳循环""" @@ -309,14 +309,14 @@ class KookClient: ): self.heartbeat_failed_count += 1 logger.warning( - f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}" + f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}" ) if ( self.heartbeat_failed_count >= self.config.max_heartbeat_failures ): - logger.error("[KOOK] 心跳失败次数过多,准备重连") + logger.error("[KOOK] 心跳失败次数过多,准备重连") self.running = False break diff --git a/astrbot/core/platform/sources/kook/kook_config.py b/astrbot/core/platform/sources/kook/kook_config.py index 0b9d180a2..2722eb088 100644 --- a/astrbot/core/platform/sources/kook/kook_config.py +++ b/astrbot/core/platform/sources/kook/kook_config.py @@ -14,7 +14,7 @@ class KookConfig: # 重连配置 reconnect_delay: int = 1 - """重连延迟基数(秒),指数退避""" + """重连延迟基数(秒),指数退避""" max_reconnect_delay: int = 60 """最大重连延迟(秒)""" max_retry_delay: int = 60 @@ -83,24 +83,24 @@ class KookConfig: # # 连接配置 # CONNECTION_CONFIG = { # # 心跳配置 -# "heartbeat_interval": 30, # 心跳间隔(秒) -# "heartbeat_timeout": 6, # 心跳超时时间(秒) +# "heartbeat_interval": 30, # 心跳间隔(秒) +# "heartbeat_timeout": 6, # 心跳超时时间(秒) # "max_heartbeat_failures": 3, # 最大心跳失败次数 # # 重连配置 -# "initial_reconnect_delay": 1, # 初始重连延迟(秒) -# "max_reconnect_delay": 60, # 最大重连延迟(秒) +# "initial_reconnect_delay": 1, # 初始重连延迟(秒) +# "max_reconnect_delay": 60, # 最大重连延迟(秒) # "max_consecutive_failures": 5, # 最大连续失败次数 # # WebSocket配置 -# "websocket_timeout": 10, # WebSocket接收超时(秒) -# "connection_timeout": 30, # 连接超时(秒) +# "websocket_timeout": 10, # WebSocket接收超时(秒) +# "connection_timeout": 30, # 连接超时(秒) # # 消息处理配置 # "enable_compression": True, # 是否启用消息压缩 -# "max_message_size": 1024 * 1024, # 最大消息大小(字节) +# "max_message_size": 1024 * 1024, # 最大消息大小(字节) # } # # 日志配置 # LOGGING_CONFIG = { -# "level": "INFO", # 日志级别:DEBUG, INFO, WARNING, ERROR +# "level": "INFO", # 日志级别:DEBUG, INFO, WARNING, ERROR # "format": "[KOOK] %(message)s", # "enable_heartbeat_logs": False, # 是否启用心跳日志 # "enable_message_logs": False, # 是否启用消息日志 @@ -111,7 +111,7 @@ class KookConfig: # "retry_on_network_error": True, # 网络错误时是否重试 # "retry_on_token_expired": True, # Token过期时是否重试 # "max_retry_attempts": 3, # 最大重试次数 -# "retry_delay_base": 2, # 重试延迟基数(秒) +# "retry_delay_base": 2, # 重试延迟基数(秒) # } # # 性能配置 @@ -127,5 +127,5 @@ class KookConfig: # "verify_ssl": True, # 是否验证SSL证书 # "enable_rate_limiting": True, # 是否启用速率限制 # "rate_limit_requests": 100, # 速率限制请求数 -# "rate_limit_window": 60, # 速率限制窗口(秒) +# "rate_limit_window": 60, # 速率限制窗口(秒) # } diff --git a/astrbot/core/platform/sources/kook/kook_event.py b/astrbot/core/platform/sources/kook/kook_event.py index 884d066d8..c235ded54 100644 --- a/astrbot/core/platform/sources/kook/kook_event.py +++ b/astrbot/core/platform/sources/kook/kook_event.py @@ -164,7 +164,7 @@ class KookEvent(AstrMessageEvent): for index, result in enumerate(tasks_result): if isinstance(result, BaseException): logger.error(f"[Kook] {result}") - # 构造一个虚假的 OrderMessage,让用户知道这里本来有张图但坏了 + # 构造一个虚假的 OrderMessage,让用户知道这里本来有张图但坏了 # 这样后面的 for 循环就能把它当成普通文本发出去 err_node = OrderMessage( index=index, diff --git a/astrbot/core/platform/sources/kook/kook_types.py b/astrbot/core/platform/sources/kook/kook_types.py index ec9da45da..e754cf40d 100644 --- a/astrbot/core/platform/sources/kook/kook_types.py +++ b/astrbot/core/platform/sources/kook/kook_types.py @@ -59,9 +59,9 @@ class KookModuleType(str, Enum): ThemeType = Literal[ "primary", "success", "danger", "warning", "info", "secondary", "none", "invisible" ] -"""主题,可选的值为:primary, success, danger, warning, info, secondary, none.默认为 primary,为 none 时不显示侧边框。""" +"""主题,可选的值为:primary, success, danger, warning, info, secondary, none.默认为 primary,为 none 时不显示侧边框。""" SizeType = Literal["xs", "sm", "md", "lg"] -"""大小,可选值为:xs, sm, md, lg, 一般默认为 lg""" +"""大小,可选值为:xs, sm, md, lg, 一般默认为 lg""" SectionMode = Literal["left", "right"] CountdownMode = Literal["day", "hour", "second"] @@ -144,10 +144,10 @@ class ButtonElement(KookCardModelBase): type: Literal[KookModuleType.BUTTON] = KookModuleType.BUTTON theme: ThemeType = "primary" value: str = "" - """当为 link 时,会跳转到 value 代表的链接; -当为 return-val 时,系统会通过系统消息将消息 id,点击用户 id 和 value 发回给发送者,发送者可以根据自己的需求进行处理,消息事件参见button 点击事件。私聊和频道内均可使用按钮点击事件。""" + """当为 link 时,会跳转到 value 代表的链接; +当为 return-val 时,系统会通过系统消息将消息 id,点击用户 id 和 value 发回给发送者,发送者可以根据自己的需求进行处理,消息事件参见button 点击事件。私聊和频道内均可使用按钮点击事件。""" click: Literal["", "link", "return-val"] = "" - """click 代表用户点击的事件,默认为"",代表无任何事件。""" + """click 代表用户点击的事件,默认为"",代表无任何事件。""" AnyElement = PlainTextElement | KmarkdownElement | ImageElement | ButtonElement | str @@ -180,7 +180,7 @@ class ImageGroupModule(KookCardModelBase): class ContainerModule(KookCardModelBase): - """1 到多张图片的组合,与图片组模块(ImageGroupModule)不同,图片并不会裁切为正方形。多张图片会纵向排列。""" + """1 到多张图片的组合,与图片组模块(ImageGroupModule)不同,图片并不会裁切为正方形。多张图片会纵向排列。""" elements: list[ImageElement] type: Literal[KookModuleType.CONTAINER] = KookModuleType.CONTAINER @@ -216,7 +216,7 @@ class FileModule(KookCardModelBase): class CountdownModule(KookCardModelBase): - """startTime 和 endTime 为毫秒时间戳,startTime 和 endTime 不能小于服务器当前时间戳。""" + """startTime 和 endTime 为毫秒时间戳,startTime 和 endTime 不能小于服务器当前时间戳。""" endTime: int """毫秒时间戳""" @@ -252,7 +252,7 @@ AnyModule = Annotated[ class KookCardMessage(KookBaseDataClass): """卡片定义文档详见 : https://developer.kookapp.cn/doc/cardmessage 此类型不能直接to_json后发送,因为kook要求卡片容器json顶层必须是**列表** - 若要发送卡片消息,请使用KookCardMessageContainer + 若要发送卡片消息,请使用KookCardMessageContainer """ model_config = ConfigDict(arbitrary_types_allowed=True) @@ -262,7 +262,7 @@ class KookCardMessage(KookBaseDataClass): color: str | None = None """16 进制色值""" modules: list[AnyModule] = Field(default_factory=list) - """单个 card 模块数量不限制,但是一条消息中所有卡片的模块数量之和最多是 50""" + """单个 card 模块数量不限制,但是一条消息中所有卡片的模块数量之和最多是 50""" def add_module(self, module: AnyModule): self.modules.append(module) @@ -300,9 +300,9 @@ class KookMessageSignal(IntEnum): HELLO = 1 """server->client 客户端连接 ws 时, 服务端返回握手结果""" PING = 2 - """client->server 心跳,ping""" + """client->server 心跳,ping""" PONG = 3 - """server->client 心跳,pong""" + """server->client 心跳,pong""" RESUME = 4 """client->server resume, 恢复会话""" RECONNECT = 5 @@ -442,7 +442,7 @@ class KookWebsocketEvent(KookBaseDataClass): @model_validator(mode="before") @classmethod def _inject_signal_into_data(cls, data: Any) -> Any: - """在解析前,把外层的 s 同步到内层的 d 中,供 discriminator 使用""" + """在解析前,把外层的 s 同步到内层的 d 中,供 discriminator 使用""" if isinstance(data, dict): s_value = data.get("s") d_value = data.get("d") diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 68512e895..c4cb8b026 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -55,7 +55,7 @@ class LarkPlatformAdapter(Platform): self.connection_mode = platform_config.get("lark_connection_mode", "socket") if not self.bot_name: - logger.warning("未设置飞书机器人名称,@ 机器人可能得不到回复。") + logger.warning("未设置飞书机器人名称,@ 机器人可能得不到回复。") # 初始化 WebSocket 长连接相关配置 async def on_msg_event_recv(event: lark.im.v1.P2ImMessageReceiveV1) -> None: @@ -455,7 +455,7 @@ class LarkPlatformAdapter(Platform): event_id: 事件ID Returns: - True 表示重复事件,False 表示新事件 + True 表示重复事件,False 表示新事件 """ self._clean_expired_events() if event_id in self.event_id_timestamps: @@ -531,7 +531,7 @@ class LarkPlatformAdapter(Platform): for m in message.mentions: if m.id is None: continue - # 飞书 open_id 可能是 None,这里做个防护 + # 飞书 open_id 可能是 None,这里做个防护 open_id = m.id.open_id if m.id.open_id else "" at_list[m.key] = Comp.At(qq=open_id, name=m.name) @@ -625,14 +625,14 @@ class LarkPlatformAdapter(Platform): if self.connection_mode == "webhook": # Webhook 模式 if self.webhook_server is None: - logger.error("[Lark] Webhook 模式已启用,但 webhook_server 未初始化") + logger.error("[Lark] Webhook 模式已启用,但 webhook_server 未初始化") 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("[Lark] Webhook 模式已启用,但未配置 webhook_uuid") else: # 长连接模式 await self.client._connect() diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 9f06be66c..9e449f116 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -76,11 +76,11 @@ class LarkMessageEvent(AstrMessageEvent): Args: lark_client: 飞书客户端 - content: 消息内容(JSON字符串) - msg_type: 消息类型(post/file/audio/media等) - reply_message_id: 回复的消息ID(用于回复消息) - receive_id: 接收者ID(用于主动发送) - receive_id_type: 接收者ID类型(用于主动发送) + content: 消息内容(JSON字符串) + msg_type: 消息类型(post/file/audio/media等) + reply_message_id: 回复的消息ID(用于回复消息) + receive_id: 接收者ID(用于主动发送) + receive_id_type: 接收者ID类型(用于主动发送) Returns: 是否发送成功 @@ -112,7 +112,7 @@ class LarkMessageEvent(AstrMessageEvent): if receive_id_type is None or receive_id is None: logger.error( - "[Lark] 主动发送消息时,receive_id 和 receive_id_type 不能为空", + "[Lark] 主动发送消息时,receive_id 和 receive_id_type 不能为空", ) return False @@ -150,11 +150,11 @@ class LarkMessageEvent(AstrMessageEvent): Args: lark_client: 飞书客户端 path: 文件路径 - file_type: 文件类型(stream/opus/mp4等) - duration: 媒体时长(毫秒),可选 + file_type: 文件类型(stream/opus/mp4等) + duration: 媒体时长(毫秒),可选 Returns: - 成功返回file_key,失败返回None + 成功返回file_key,失败返回None """ path_obj = anyio.Path(path) if not path or not await path_obj.exists(): @@ -162,7 +162,7 @@ class LarkMessageEvent(AstrMessageEvent): return None if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法上传文件") + logger.error("[Lark] API Client im 模块未初始化,无法上传文件") return None try: @@ -235,7 +235,7 @@ class LarkMessageEvent(AstrMessageEvent): if image_file is None: if not file_path: - logger.error("[Lark] 图片路径为空,无法上传") + logger.error("[Lark] 图片路径为空,无法上传") continue try: image_file = await anyio.to_thread.run_sync( @@ -257,7 +257,7 @@ class LarkMessageEvent(AstrMessageEvent): ) if lark_client.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法上传图片") + logger.error("[Lark] API Client im 模块未初始化,无法上传图片") continue response = await lark_client.im.v1.image.acreate(request) @@ -275,16 +275,16 @@ class LarkMessageEvent(AstrMessageEvent): ret.append([{"tag": "img", "image_key": image_key}]) _stage.clear() elif isinstance(comp, File): - # 文件将通过 _send_file_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到文件组件,将单独发送") + # 文件将通过 _send_file_message 方法单独发送,这里跳过 + logger.debug("[Lark] 检测到文件组件,将单独发送") continue elif isinstance(comp, Record): - # 音频将通过 _send_audio_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到音频组件,将单独发送") + # 音频将通过 _send_audio_message 方法单独发送,这里跳过 + logger.debug("[Lark] 检测到音频组件,将单独发送") continue elif isinstance(comp, Video): - # 视频将通过 _send_media_message 方法单独发送,这里跳过 - logger.debug("[Lark] 检测到视频组件,将单独发送") + # 视频将通过 _send_media_message 方法单独发送,这里跳过 + logger.debug("[Lark] 检测到视频组件,将单独发送") continue else: logger.warning(f"飞书 暂时不支持消息段: {comp.type}") @@ -306,15 +306,15 @@ class LarkMessageEvent(AstrMessageEvent): Args: message_chain: 要发送的消息链 lark_client: 飞书客户端 - reply_message_id: 回复的消息ID(用于回复消息) - receive_id: 接收者ID(用于主动发送) - receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送) + reply_message_id: 回复的消息ID(用于回复消息) + receive_id: 接收者ID(用于主动发送) + receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送) """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") return - # 分离文件、音频、视频组件和其他组件 + # 分离文件、音频、视频组件和其他组件 file_components: list[File] = [] audio_components: list[Record] = [] media_components: list[Video] = [] @@ -330,7 +330,7 @@ class LarkMessageEvent(AstrMessageEvent): else: other_components.append(comp) - # 先发送非文件内容(如果有) + # 先发送非文件内容(如果有) if other_components: temp_chain = MessageChain() temp_chain.chain = other_components @@ -369,7 +369,7 @@ class LarkMessageEvent(AstrMessageEvent): ) async def send(self, message: MessageChain) -> None: - """发送消息链到飞书,然后交给父类做框架级发送/记录""" + """发送消息链到飞书,然后交给父类做框架级发送/记录""" await LarkMessageEvent.send_message_chain( message, self.bot, @@ -390,9 +390,9 @@ class LarkMessageEvent(AstrMessageEvent): Args: file_comp: 文件组件 lark_client: 飞书客户端 - reply_message_id: 回复的消息ID(用于回复消息) - receive_id: 接收者ID(用于主动发送) - receive_id_type: 接收者ID类型(用于主动发送) + reply_message_id: 回复的消息ID(用于回复消息) + receive_id: 接收者ID(用于主动发送) + receive_id_type: 接收者ID类型(用于主动发送) """ file_path = file_comp.file or "" file_key = await LarkMessageEvent._upload_lark_file( @@ -424,9 +424,9 @@ class LarkMessageEvent(AstrMessageEvent): Args: audio_comp: 音频组件 lark_client: 飞书客户端 - reply_message_id: 回复的消息ID(用于回复消息) - receive_id: 接收者ID(用于主动发送) - receive_id_type: 接收者ID类型(用于主动发送) + reply_message_id: 回复的消息ID(用于回复消息) + receive_id: 接收者ID(用于主动发送) + receive_id_type: 接收者ID类型(用于主动发送) """ # 获取音频文件路径 try: @@ -446,14 +446,14 @@ class LarkMessageEvent(AstrMessageEvent): converted_audio_path = None try: audio_path = await convert_audio_to_opus(original_audio_path) - # 如果转换后路径与原路径不同,说明生成了新文件 + # 如果转换后路径与原路径不同,说明生成了新文件 if audio_path != original_audio_path: converted_audio_path = audio_path else: audio_path = original_audio_path except Exception as e: - logger.error(f"[Lark] 音频格式转换失败,将尝试直接上传: {e}") - # 如果转换失败,继续尝试直接上传原始文件 + logger.error(f"[Lark] 音频格式转换失败,将尝试直接上传: {e}") + # 如果转换失败,继续尝试直接上传原始文件 audio_path = original_audio_path # 获取音频时长 @@ -500,9 +500,9 @@ class LarkMessageEvent(AstrMessageEvent): Args: media_comp: 视频组件 lark_client: 飞书客户端 - reply_message_id: 回复的消息ID(用于回复消息) - receive_id: 接收者ID(用于主动发送) - receive_id_type: 接收者ID类型(用于主动发送) + reply_message_id: 回复的消息ID(用于回复消息) + receive_id: 接收者ID(用于主动发送) + receive_id_type: 接收者ID类型(用于主动发送) """ # 获取视频文件路径 try: @@ -522,14 +522,14 @@ class LarkMessageEvent(AstrMessageEvent): converted_video_path = None try: video_path = await convert_video_format(original_video_path, "mp4") - # 如果转换后路径与原路径不同,说明生成了新文件 + # 如果转换后路径与原路径不同,说明生成了新文件 if video_path != original_video_path: converted_video_path = video_path else: video_path = original_video_path except Exception as e: - logger.error(f"[Lark] 视频格式转换失败,将尝试直接上传: {e}") - # 如果转换失败,继续尝试直接上传原始文件 + logger.error(f"[Lark] 视频格式转换失败,将尝试直接上传: {e}") + # 如果转换失败,继续尝试直接上传原始文件 video_path = original_video_path # 获取视频时长 @@ -565,7 +565,7 @@ class LarkMessageEvent(AstrMessageEvent): async def react(self, emoji: str) -> None: if self.bot.im is None: - logger.error("[Lark] API Client im 模块未初始化,无法发送表情") + logger.error("[Lark] API Client im 模块未初始化,无法发送表情") return request = ( @@ -585,7 +585,7 @@ class LarkMessageEvent(AstrMessageEvent): return async def _create_streaming_card(self) -> str | None: - """创建一个开启流式更新模式的卡片实体,返回 card_id。""" + """创建一个开启流式更新模式的卡片实体,返回 card_id。""" if self.bot.cardkit is None: logger.error("[Lark] API Client cardkit 模块未初始化") return None @@ -653,7 +653,7 @@ class LarkMessageEvent(AstrMessageEvent): receive_id: str | None = None, receive_id_type: str | None = None, ) -> bool: - """将卡片实体作为 interactive 消息发送。""" + """将卡片实体作为 interactive 消息发送。""" content = json.dumps( {"type": "card", "data": {"card_id": card_id}}, ensure_ascii=False, @@ -673,7 +673,7 @@ class LarkMessageEvent(AstrMessageEvent): content: str, sequence: int, ) -> bool: - """调用 CardKit 流式更新文本接口,向 markdown_1 组件推送全量文本。""" + """调用 CardKit 流式更新文本接口,向 markdown_1 组件推送全量文本。""" if self.bot.cardkit is None: logger.error("[Lark] API Client cardkit 模块未初始化") return False @@ -709,7 +709,7 @@ class LarkMessageEvent(AstrMessageEvent): card_id: str, sequence: int, ) -> None: - """关闭卡片的流式更新模式,使其可正常转发、摘要恢复。""" + """关闭卡片的流式更新模式,使其可正常转发、摘要恢复。""" if self.bot.cardkit is None: logger.error("[Lark] API Client cardkit 模块未初始化") return @@ -744,7 +744,7 @@ class LarkMessageEvent(AstrMessageEvent): logger.debug(f"[Lark] 流式模式已关闭: {card_id}") async def _fallback_send_streaming(self, generator, use_fallback: bool = False): - """回退到非流式发送:缓冲全部文本后一次性发送,并保留父类副作用。""" + """回退到非流式发送:缓冲全部文本后一次性发送,并保留父类副作用。""" buffer = None async for chain in generator: if not buffer: @@ -760,16 +760,16 @@ class LarkMessageEvent(AstrMessageEvent): self._has_send_oper = True async def send_streaming(self, generator, use_fallback: bool = False): - """使用 CardKit 流式卡片实现打字机效果。 + """使用 CardKit 流式卡片实现打字机效果。 - 流程:创建卡片实体 → 发送消息 → 流式更新文本 → 关闭流式模式。 - 使用解耦发送循环,LLM token 到达时只更新 buffer 并唤醒发送协程, - 发送频率由网络 RTT 自然限流。 + 流程:创建卡片实体 → 发送消息 → 流式更新文本 → 关闭流式模式。 + 使用解耦发送循环,LLM token 到达时只更新 buffer 并唤醒发送协程, + 发送频率由网络 RTT 自然限流。 """ # Step 1: 创建流式卡片实体 card_id = await self._create_streaming_card() if not card_id: - logger.warning("[Lark] 无法创建流式卡片,回退到非流式发送") + logger.warning("[Lark] 无法创建流式卡片,回退到非流式发送") await self._fallback_send_streaming(generator, use_fallback) return @@ -779,7 +779,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id=self.message_obj.message_id, ) if not sent: - logger.error("[Lark] 发送流式卡片消息失败,回退到非流式发送") + logger.error("[Lark] 发送流式卡片消息失败,回退到非流式发送") await self._fallback_send_streaming(generator, use_fallback) return @@ -793,7 +793,7 @@ class LarkMessageEvent(AstrMessageEvent): text_changed = asyncio.Event() async def _sender_loop() -> None: - """信号驱动的文本发送循环,有新内容就发,RTT 自然限流。""" + """信号驱动的文本发送循环,有新内容就发,RTT 自然限流。""" nonlocal sequence, last_sent while not done: await text_changed.wait() @@ -815,7 +815,7 @@ class LarkMessageEvent(AstrMessageEvent): continue if chain.type == "break": - # 飞书卡片不支持分段,忽略 break + # 飞书卡片不支持分段,忽略 break continue for comp in chain.chain: diff --git a/astrbot/core/platform/sources/lark/server.py b/astrbot/core/platform/sources/lark/server.py index 52177ebb0..1fdcefd7f 100644 --- a/astrbot/core/platform/sources/lark/server.py +++ b/astrbot/core/platform/sources/lark/server.py @@ -1,6 +1,6 @@ """飞书(Lark) Webhook 服务器实现 -实现飞书事件订阅的 Webhook 模式,支持: +实现飞书事件订阅的 Webhook 模式,支持: 1. 请求 URL 验证 (challenge 验证) 2. 事件加密/解密 (AES-256-CBC) 3. 签名校验 (SHA256) @@ -109,7 +109,7 @@ class LarkWebhookServer: 解密后的事件字典 """ if not self.cipher: - raise ValueError("未配置 encrypt_key,无法解密事件") + raise ValueError("未配置 encrypt_key,无法解密事件") decrypted_str = self.cipher.decrypt_string(encrypted_data) return json.loads(decrypted_str) @@ -129,7 +129,7 @@ class LarkWebhookServer: return {"challenge": challenge} async def handle_callback(self, request) -> tuple[dict, int] | dict: - """处理 webhook 回调,可被统一 webhook 入口复用 + """处理 webhook 回调,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -150,7 +150,7 @@ class LarkWebhookServer: logger.error("[Lark Webhook] 请求体为空") return {"error": "Empty request body"}, 400 - # 如果配置了 encrypt_key,进行签名验证 + # 如果配置了 encrypt_key,进行签名验证 if self.encrypt_key: timestamp = request.headers.get("X-Lark-Request-Timestamp", "") nonce = request.headers.get("X-Lark-Request-Nonce", "") @@ -180,7 +180,7 @@ class LarkWebhookServer: else: token = event_data.get("token", "") if token != self.verification_token: - logger.error("[Lark Webhook] Verification Token 不匹配。") + logger.error("[Lark Webhook] Verification Token 不匹配。") return {"error": "Invalid verification token"}, 401 # 处理 URL 验证 (challenge) diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index c13677b13..a1d331df4 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -28,12 +28,12 @@ LINE_CONFIG_METADATA = { "channel_access_token": { "description": "LINE Channel Access Token", "type": "string", - "hint": "LINE Messaging API 的 channel access token。", + "hint": "LINE Messaging API 的 channel access token。", }, "channel_secret": { "description": "LINE Channel Secret", "type": "string", - "hint": "用于校验 LINE Webhook 签名。", + "hint": "用于校验 LINE Webhook 签名。", }, } @@ -41,11 +41,11 @@ LINE_I18N_RESOURCES = { "zh-CN": { "channel_access_token": { "description": "LINE Channel Access Token", - "hint": "LINE Messaging API 的 channel access token。", + "hint": "LINE Messaging API 的 channel access token。", }, "channel_secret": { "description": "LINE Channel Secret", - "hint": "用于校验 LINE Webhook 签名。", + "hint": "用于校验 LINE Webhook 签名。", }, }, "en-US": { @@ -86,7 +86,7 @@ class LinePlatformAdapter(Platform): 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。", + "LINE 适配器需要 channel_access_token 和 channel_secret。", ) self.line_api = LineAPIClient( @@ -117,7 +117,7 @@ class LinePlatformAdapter(Platform): if webhook_uuid: log_webhook_info(f"{self.meta().id}(LINE)", webhook_uuid) else: - logger.warning("[LINE] webhook_uuid 为空,统一 Webhook 可能无法接收消息。") + logger.warning("[LINE] webhook_uuid 为空,统一 Webhook 可能无法接收消息。") await self.shutdown_event.wait() async def terminate(self) -> None: diff --git a/astrbot/core/platform/sources/line/line_event.py b/astrbot/core/platform/sources/line/line_event.py index d4d1a9cdb..29ab3b889 100644 --- a/astrbot/core/platform/sources/line/line_event.py +++ b/astrbot/core/platform/sources/line/line_event.py @@ -265,14 +265,14 @@ class LineMessageEvent(AstrMessageEvent): return await super().send_streaming(generator, use_fallback) buffer = "" - pattern = re.compile(r"[^。?!~…]+[。?!~…]+") + pattern = re.compile(r"[^。?!~…]+[。?!~…]+") async for chain in generator: if isinstance(chain, MessageChain): for comp in chain.chain: if isinstance(comp, Plain): buffer += comp.text - if any(p in buffer for p in "。?!~…"): + if any(p in buffer for p in "。?!~…"): buffer = await self.process_buffer(buffer, pattern) else: await self.send(MessageChain(chain=[comp])) diff --git a/astrbot/core/platform/sources/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index f0b8292ca..d1b9354d5 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -124,7 +124,7 @@ class MisskeyPlatformAdapter(Platform): async def run(self) -> None: if not self.instance_url or not self.access_token: - logger.error("[Misskey] 配置不完整,无法启动") + logger.error("[Misskey] 配置不完整,无法启动") return self.api = MisskeyAPI( @@ -171,7 +171,7 @@ class MisskeyPlatformAdapter(Platform): session, message_chain, ): - """发送纯文本消息(无文件上传)""" + """发送纯文本消息(无文件上传)""" if not self.api: return await super().send_by_session(session, message_chain) @@ -196,7 +196,7 @@ class MisskeyPlatformAdapter(Platform): poll: dict[str, Any], message_parts: list[str], ) -> None: - """处理投票数据,将其添加到消息中""" + """处理投票数据,将其添加到消息中""" try: if not isinstance(message.raw_message, dict): message.raw_message = {} @@ -409,7 +409,7 @@ class MisskeyPlatformAdapter(Platform): if not text or not text.strip(): if not has_file_components: - logger.warning("[Misskey] 消息内容为空且无文件组件,跳过发送") + logger.warning("[Misskey] 消息内容为空且无文件组件,跳过发送") return await super().send_by_session(session, message_chain) text = "" @@ -438,7 +438,7 @@ class MisskeyPlatformAdapter(Platform): sem = asyncio.Semaphore(upload_concurrency) async def _upload_comp(comp) -> object | None: - """组件上传函数:处理 URL(下载后上传)或本地文件(直接上传)""" + """组件上传函数:处理 URL(下载后上传)或本地文件(直接上传)""" from .misskey_utils import ( resolve_component_url_or_path, upload_local_with_retries, @@ -464,7 +464,7 @@ class MisskeyPlatformAdapter(Platform): None, ) - # URL 上传:下载后本地上传 + # URL 上传:下载后本地上传 if url_candidate: result = await self.api.upload_and_find_file( str(url_candidate), @@ -485,7 +485,7 @@ class MisskeyPlatformAdapter(Platform): if file_id: return file_id - # 所有上传都失败,尝试获取 URL 作为回退 + # 所有上传都失败,尝试获取 URL 作为回退 if hasattr(comp, "register_to_file_service"): try: url = await comp.register_to_file_service() @@ -510,7 +510,7 @@ class MisskeyPlatformAdapter(Platform): except Exception: pass - # 收集所有可能包含文件/URL信息的组件:支持异步接口或同步字段 + # 收集所有可能包含文件/URL信息的组件:支持异步接口或同步字段 file_components = [] for comp in message_chain.chain: try: @@ -531,7 +531,7 @@ class MisskeyPlatformAdapter(Platform): if len(file_components) > MAX_FILE_UPLOAD_COUNT: logger.warning( - f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件", + f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件", ) file_components = file_components[:MAX_FILE_UPLOAD_COUNT] @@ -554,7 +554,7 @@ class MisskeyPlatformAdapter(Platform): except Exception: pass except Exception: - logger.debug("[Misskey] 并发上传过程中出现异常,继续发送文本") + logger.debug("[Misskey] 并发上传过程中出现异常,继续发送文本") if session_id and is_valid_room_session_id(session_id): from .misskey_utils import extract_room_id_from_session_id @@ -580,11 +580,11 @@ class MisskeyPlatformAdapter(Platform): text = (text or "") + appended payload: dict[str, Any] = {"toUserId": user_id, "text": text} if file_ids: - # 聊天消息只支持单个文件,使用 fileId 而不是 fileIds + # 聊天消息只支持单个文件,使用 fileId 而不是 fileIds payload["fileId"] = file_ids[0] if len(file_ids) > 1: logger.warning( - f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件", + f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件", ) await self.api.send_message(payload) else: @@ -594,7 +594,7 @@ class MisskeyPlatformAdapter(Platform): session_id.split("%")[1] if "%" in session_id else session_id ) - # 获取用户缓存信息(包含reply_to_note_id) + # 获取用户缓存信息(包含reply_to_note_id) user_info_for_reply = self._user_cache.get(user_id_for_cache, {}) visibility, visible_user_ids = resolve_message_visibility( diff --git a/astrbot/core/platform/sources/misskey/misskey_api.py b/astrbot/core/platform/sources/misskey/misskey_api.py index ca5e48e0b..97753fff3 100644 --- a/astrbot/core/platform/sources/misskey/misskey_api.py +++ b/astrbot/core/platform/sources/misskey/misskey_api.py @@ -308,7 +308,7 @@ def retry_async( sleep_time = backoff + jitter logger.warning( - f"[Misskey API] {func_name} 第 {attempt} 次重试失败: {e}," + f"[Misskey API] {func_name} 第 {attempt} 次重试失败: {e}," f"{sleep_time:.1f}s后重试", ) await asyncio.sleep(sleep_time) @@ -687,28 +687,28 @@ class MisskeyAPI: max_wait_time: float = 30.0, check_interval: float = 2.0, ) -> dict[str, Any] | None: - """简化的文件上传:尝试 URL 上传,失败则下载后本地上传 + """简化的文件上传:尝试 URL 上传,失败则下载后本地上传 Args: url: 文件URL - name: 文件名(可选) - folder_id: 文件夹ID(可选) - max_wait_time: 保留参数(未使用) - check_interval: 保留参数(未使用) + name: 文件名(可选) + folder_id: 文件夹ID(可选) + max_wait_time: 保留参数(未使用) + check_interval: 保留参数(未使用) Returns: - 包含文件ID和元信息的字典,失败时返回None + 包含文件ID和元信息的字典,失败时返回None """ if not url: raise APIError("URL不能为空") - # 通过本地上传获取即时文件 ID(下载文件 → 上传 → 返回 ID) + # 通过本地上传获取即时文件 ID(下载文件 → 上传 → 返回 ID) try: import os import tempfile - # SSL 验证下载,失败则重试不验证 SSL + # SSL 验证下载,失败则重试不验证 SSL tmp_bytes = None try: tmp_bytes = await self._download_with_existing_session( @@ -717,7 +717,7 @@ class MisskeyAPI: ) 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", + f"[Misskey API] SSL 验证下载失败: {ssl_error},重试不验证 SSL", ) try: tmp_bytes = await self._download_with_existing_session( @@ -755,7 +755,7 @@ class MisskeyAPI: user_id_or_payload: Any, text: str | None = None, ) -> dict[str, Any]: - """发送聊天消息。 + """发送聊天消息。 Accepts either (user_id: str, text: str) or a single dict payload prepared by caller. """ @@ -774,7 +774,7 @@ class MisskeyAPI: room_id_or_payload: Any, text: str | None = None, ) -> dict[str, Any]: - """发送房间消息。 + """发送房间消息。 Accepts either (room_id: str, text: str) or a single dict payload. """ @@ -833,7 +833,7 @@ class MisskeyAPI: local_files: list[str] | None = None, **kwargs, ) -> dict[str, Any]: - """通用消息发送函数:统一处理文本+媒体发送 + """通用消息发送函数:统一处理文本+媒体发送 Args: message_type: 消息类型 ('chat', 'room', 'note') @@ -841,7 +841,7 @@ class MisskeyAPI: text: 文本内容 media_urls: 媒体文件URL列表 local_files: 本地文件路径列表 - **kwargs: 其他参数(如visibility等) + **kwargs: 其他参数(如visibility等) Returns: 发送结果字典 @@ -851,7 +851,7 @@ class MisskeyAPI: """ if not text and not media_urls and not local_files: - raise APIError("消息内容不能为空:需要文本或媒体文件") + raise APIError("消息内容不能为空:需要文本或媒体文件") file_ids = [] @@ -873,7 +873,7 @@ class MisskeyAPI: ) async def _process_media_urls(self, urls: list[str]) -> list[str]: - """处理远程媒体文件URL列表,返回文件ID列表""" + """处理远程媒体文件URL列表,返回文件ID列表""" file_ids = [] for url in urls: try: @@ -885,12 +885,12 @@ class MisskeyAPI: logger.error(f"[Misskey API] URL媒体上传失败: {url}") except Exception as e: logger.error(f"[Misskey API] URL媒体处理失败 {url}: {e}") - # 继续处理其他文件,不中断整个流程 + # 继续处理其他文件,不中断整个流程 continue return file_ids async def _process_local_files(self, file_paths: list[str]) -> list[str]: - """处理本地文件路径列表,返回文件ID列表""" + """处理本地文件路径列表,返回文件ID列表""" file_ids = [] for file_path in file_paths: try: diff --git a/astrbot/core/platform/sources/misskey/misskey_event.py b/astrbot/core/platform/sources/misskey/misskey_event.py index 068f7e7a2..f8addaacb 100644 --- a/astrbot/core/platform/sources/misskey/misskey_event.py +++ b/astrbot/core/platform/sources/misskey/misskey_event.py @@ -41,13 +41,13 @@ class MisskeyPlatformEvent(AstrMessageEvent): return any(message_trimmed.startswith(prefix) for prefix in system_prefixes) async def send(self, message: MessageChain) -> None: - """发送消息,使用适配器的完整上传和发送逻辑""" + """发送消息,使用适配器的完整上传和发送逻辑""" try: logger.debug( - f"[MisskeyEvent] send 方法被调用,消息链包含 {len(message.chain)} 个组件", + f"[MisskeyEvent] send 方法被调用,消息链包含 {len(message.chain)} 个组件", ) - # 使用适配器的 send_by_session 方法,它包含文件上传逻辑 + # 使用适配器的 send_by_session 方法,它包含文件上传逻辑 from astrbot.core.platform.message_session import MessageSession from astrbot.core.platform.message_type import MessageType @@ -78,7 +78,7 @@ class MisskeyPlatformEvent(AstrMessageEvent): content, has_at = serialize_message_chain(message.chain) if not content: - logger.debug("[MisskeyEvent] 内容为空,跳过发送") + logger.debug("[MisskeyEvent] 内容为空,跳过发送") return original_message_id = getattr(self.message_obj, "message_id", None) @@ -145,14 +145,14 @@ class MisskeyPlatformEvent(AstrMessageEvent): return await super().send_streaming(generator, use_fallback) buffer = "" - pattern = re.compile(r"[^。?!~…]+[。?!~…]+") + pattern = re.compile(r"[^。?!~…]+[。?!~…]+") async for chain in generator: if isinstance(chain, MessageChain): for comp in chain.chain: if isinstance(comp, Plain): buffer += comp.text - if any(p in buffer for p in "。?!~…"): + if any(p in buffer for p in "。?!~…"): buffer = await self.process_buffer(buffer, pattern) else: await self.send(MessageChain(chain=[comp])) diff --git a/astrbot/core/platform/sources/misskey/misskey_utils.py b/astrbot/core/platform/sources/misskey/misskey_utils.py index dd02c13c0..e5f183eb5 100644 --- a/astrbot/core/platform/sources/misskey/misskey_utils.py +++ b/astrbot/core/platform/sources/misskey/misskey_utils.py @@ -7,7 +7,7 @@ from astrbot.api.platform import AstrBotMessage, MessageMember, MessageType class FileIDExtractor: - """从 API 响应中提取文件 ID 的帮助类(无状态)。""" + """从 API 响应中提取文件 ID 的帮助类(无状态)。""" @staticmethod def extract_file_id(result: Any) -> str | None: @@ -31,7 +31,7 @@ class FileIDExtractor: class MessagePayloadBuilder: - """构建不同类型消息负载的帮助类(无状态)。""" + """构建不同类型消息负载的帮助类(无状态)。""" @staticmethod def build_chat_payload( @@ -84,14 +84,14 @@ def serialize_message_chain(chain: list[Any]) -> tuple[str, bool]: if isinstance(component, Comp.Plain): return component.text if isinstance(component, Comp.File): - # 为文件组件返回占位符,但适配器仍会处理原组件 + # 为文件组件返回占位符,但适配器仍会处理原组件 return "[文件]" if isinstance(component, Comp.Image): - # 为图片组件返回占位符,但适配器仍会处理原组件 + # 为图片组件返回占位符,但适配器仍会处理原组件 return "[图片]" if isinstance(component, Comp.At): has_at = True - # 优先使用name字段(用户名),如果没有则使用qq字段 + # 优先使用name字段(用户名),如果没有则使用qq字段 # 这样可以避免在Misskey中生成 @ 这样的无效提及 if hasattr(component, "name") and component.name: return f"@{component.name}" @@ -126,7 +126,7 @@ def resolve_message_visibility( ) -> tuple[str, list[str] | None]: """解析 Misskey 消息的可见性设置 - 可以从 user_cache 或 raw_message 中解析,支持两种调用方式: + 可以从 user_cache 或 raw_message 中解析,支持两种调用方式: 1. 基于 user_cache: resolve_message_visibility(user_id, user_cache, self_id) 2. 基于 raw_message: resolve_message_visibility(raw_message=raw_message, self_id=self_id) """ @@ -177,7 +177,7 @@ def resolve_visibility_from_raw_message( raw_message: dict[str, Any], self_id: str | None = None, ) -> tuple[str, list[str] | None]: - """从原始消息数据中解析可见性设置(已弃用,使用 resolve_message_visibility 替代)""" + """从原始消息数据中解析可见性设置(已弃用,使用 resolve_message_visibility 替代)""" return resolve_message_visibility(raw_message=raw_message, self_id=self_id) @@ -246,15 +246,15 @@ def add_at_mention_if_needed( user_info: dict[str, Any] | None, has_at: bool = False, ) -> str: - """如果需要且没有@用户,则添加@用户 + """如果需要且没有@用户,则添加@用户 - 注意:仅在有有效的username时才添加@提及,避免使用用户ID + 注意:仅在有有效的username时才添加@提及,避免使用用户ID """ if has_at or not user_info: return text username = user_info.get("username") - # 如果没有username,则不添加@提及,返回原文本 + # 如果没有username,则不添加@提及,返回原文本 # 这样可以避免生成 @ 这样的无效提及 if not username: return text @@ -286,7 +286,7 @@ def process_files( files: list, include_text_parts: bool = True, ) -> list: - """处理文件列表,添加到消息组件中并返回文本描述""" + """处理文件列表,添加到消息组件中并返回文本描述""" file_parts = [] for file_info in files: component, part_text = create_file_component(file_info) @@ -297,7 +297,7 @@ def process_files( def format_poll(poll: dict[str, Any]) -> str: - """将 Misskey 的 poll 对象格式化为可读字符串。""" + """将 Misskey 的 poll 对象格式化为可读字符串。""" if not poll or not isinstance(poll, dict): return "" multiple = poll.get("multiple", False) @@ -378,7 +378,7 @@ def process_at_mention( bot_username: str, client_self_id: str, ) -> tuple[list[str], str]: - """处理@提及逻辑,返回消息部分列表和处理后的文本""" + """处理@提及逻辑,返回消息部分列表和处理后的文本""" message_parts = [] if not raw_text: @@ -418,7 +418,7 @@ def cache_user_info( "nickname": sender_info["nickname"], "visibility": raw_data.get("visibility", "public"), "visible_user_ids": raw_data.get("visibleUserIds", []), - # 保存原消息ID,用于回复时作为reply_id + # 保存原消息ID,用于回复时作为reply_id "reply_to_note_id": raw_data.get("id"), } @@ -449,16 +449,16 @@ def cache_room_info( async def resolve_component_url_or_path( comp: Any, ) -> tuple[str | None, str | None]: - """尝试从组件解析可上传的远程 URL 或本地路径。 + """尝试从组件解析可上传的远程 URL 或本地路径。 - 返回 (url_candidate, local_path)。两者可能都为 None。 - 这个函数尽量不抛异常,调用方可按需处理 None。 + 返回 (url_candidate, local_path)。两者可能都为 None。 + 这个函数尽量不抛异常,调用方可按需处理 None。 """ url_candidate = None local_path = None async def _get_str_value(coro_or_val): - """辅助函数:统一处理协程或普通值""" + """辅助函数:统一处理协程或普通值""" try: if hasattr(coro_or_val, "__await__"): result = await coro_or_val @@ -513,7 +513,7 @@ async def resolve_component_url_or_path( def summarize_component_for_log(comp: Any) -> dict[str, Any]: - """生成适合日志的组件属性字典(尽量不抛异常)。""" + """生成适合日志的组件属性字典(尽量不抛异常)。""" attrs = {} for a in ("file", "url", "path", "src", "source", "name"): try: @@ -531,7 +531,7 @@ async def upload_local_with_retries( preferred_name: str | None, folder_id: str | None, ) -> str | None: - """尝试本地上传,返回 file id 或 None。如果文件类型不允许则直接失败。""" + """尝试本地上传,返回 file id 或 None。如果文件类型不允许则直接失败。""" try: res = await api.upload_file(local_path, preferred_name, folder_id) if isinstance(res, dict): @@ -541,7 +541,7 @@ async def upload_local_with_retries( if fid: return str(fid) except Exception: - # 上传失败,直接返回 None,让上层处理错误 + # 上传失败,直接返回 None,让上层处理错误 return None return None diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 5e65e9eea..9c05de387 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -71,23 +71,23 @@ class QQOfficialMessageEvent(AstrMessageEvent): await self._post_send() async def send_streaming(self, generator, use_fallback: bool = False): - """流式输出仅支持消息列表私聊(C2C),其他消息源退化为普通发送""" - # 先标记事件层“已执行发送操作”,避免异常路径遗漏 + """流式输出仅支持消息列表私聊(C2C),其他消息源退化为普通发送""" + # 先标记事件层“已执行发送操作”,避免异常路径遗漏 await super().send_streaming(generator, use_fallback) - # QQ C2C 流式协议:开始/中间分片使用 state=1,结束分片使用 state=10 + # QQ C2C 流式协议:开始/中间分片使用 state=1,结束分片使用 state=10 stream_payload = {"state": 1, "id": None, "index": 0, "reset": False} last_edit_time = 0 # 上次发送分片的时间 throttle_interval = 1 # 分片间最短间隔 (秒) ret = None source = ( self.message_obj.raw_message - ) # 提前获取,避免 generator 为空时 NameError + ) # 提前获取,避免 generator 为空时 NameError try: async for chain in generator: source = self.message_obj.raw_message if not isinstance(source, botpy.message.C2CMessage): - # 非 C2C 场景:直接累积,最后统一发 + # 非 C2C 场景:直接累积,最后统一发 if not self.send_buffer: self.send_buffer = chain else: @@ -96,7 +96,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): # ---- C2C 流式场景 ---- - # tool_call break 信号:工具开始执行,先把已有 buffer 以 state=10 结束当前流式段 + # tool_call break 信号:工具开始执行,先把已有 buffer 以 state=10 结束当前流式段 if chain.type == "break": if self.send_buffer: stream_payload["state"] = 10 @@ -104,7 +104,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ret_id = self._extract_response_message_id(ret) if ret_id is not None: stream_payload["id"] = ret_id - # 重置 stream_payload,为下一段流式做准备 + # 重置 stream_payload,为下一段流式做准备 stream_payload = { "state": 1, "id": None, @@ -120,7 +120,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): else: self.send_buffer.chain.extend(chain.chain) - # 节流:按时间间隔发送中间分片 + # 节流:按时间间隔发送中间分片 current_time = asyncio.get_running_loop().time() if current_time - last_edit_time >= throttle_interval: ret = cast( @@ -132,10 +132,10 @@ class QQOfficialMessageEvent(AstrMessageEvent): if ret_id is not None: stream_payload["id"] = ret_id last_edit_time = asyncio.get_running_loop().time() - self.send_buffer = None # 清空已发送的分片,避免下次重复发送旧内容 + self.send_buffer = None # 清空已发送的分片,避免下次重复发送旧内容 if isinstance(source, botpy.message.C2CMessage): - # 结束流式对话,发送 buffer 中剩余内容 + # 结束流式对话,发送 buffer 中剩余内容 stream_payload["state"] = 10 ret = await self._post_send(stream=stream_payload) else: @@ -143,15 +143,15 @@ class QQOfficialMessageEvent(AstrMessageEvent): except Exception as e: logger.error(f"发送流式消息时出错: {e}", exc_info=True) - # 避免累计内容在异常后被整包重复发送:仅清理缓存,不做非流式整包兜底 - # 如需兜底,应该只发送未发送 delta(后续可继续优化) + # 避免累计内容在异常后被整包重复发送:仅清理缓存,不做非流式整包兜底 + # 如需兜底,应该只发送未发送 delta(后续可继续优化) self.send_buffer = None return None @staticmethod def _extract_response_message_id(ret) -> str | None: - """兼容 qq-botpy 返回 Message 对象或 dict 两种形态。""" + """兼容 qq-botpy 返回 Message 对象或 dict 两种形态。""" if ret is None: return None if isinstance(ret, dict): @@ -186,9 +186,9 @@ class QQOfficialMessageEvent(AstrMessageEvent): file_name, ) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer) - # C2C 流式仅用于文本分片,富媒体时降级为普通发送,避免平台侧流式校验报错。 + # C2C 流式仅用于文本分片,富媒体时降级为普通发送,避免平台侧流式校验报错。 if stream and (image_base64 or record_file_path): - logger.debug("[QQOfficial] 检测到富媒体,降级为非流式发送。") + logger.debug("[QQOfficial] 检测到富媒体,降级为非流式发送。") stream = None if ( @@ -201,9 +201,9 @@ class QQOfficialMessageEvent(AstrMessageEvent): ): return None - # QQ C2C 流式 API 说明: - # - 开始/中间分片(state=1):增量追加内容,不需要 \n(加了会导致强制换行) - # - 最终分片(state=10):结束流,content 必须以 \n 结尾(QQ API 要求) + # QQ C2C 流式 API 说明: + # - 开始/中间分片(state=1):增量追加内容,不需要 \n(加了会导致强制换行) + # - 最终分片(state=10):结束流,content 必须以 \n 结尾(QQ API 要求) if ( stream and stream.get("state") == 10 @@ -401,8 +401,8 @@ class QQOfficialMessageEvent(AstrMessageEvent): try: return await send_func(payload) except botpy.errors.ServerError as err: - # QQ 流式 markdown 分片校验:内容必须以换行结尾。 - # 某些边界场景服务端仍可能判定失败,这里做一次修正重试。 + # QQ 流式 markdown 分片校验:内容必须以换行结尾。 + # 某些边界场景服务端仍可能判定失败,这里做一次修正重试。 if stream and self.STREAM_MARKDOWN_NEWLINE_ERROR in str(err): retry_payload = payload.copy() @@ -417,7 +417,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): retry_payload["content"] = content + "\n" logger.warning( - "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。" + "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。" ) return await send_func(retry_payload) @@ -429,7 +429,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): raise logger.warning( - "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" + "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" ) fallback_payload = payload.copy() fallback_payload.pop("markdown", None) @@ -568,7 +568,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): result = await self.bot.api._http.request(route, json=payload) if result is None: - logger.warning("[QQOfficial] post_c2c_message: API 返回 None,跳过本次发送") + logger.warning("[QQOfficial] post_c2c_message: API 返回 None,跳过本次发送") return None if not isinstance(result, dict): logger.error(f"[QQOfficial] post_c2c_message: 响应不是 dict: {result}") @@ -619,7 +619,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): record_file_path = record_tecent_silk_path else: record_file_path = None - logger.error("转换音频格式时出错:音频时长不大于0") + logger.error("转换音频格式时出错:音频时长不大于0") except Exception as e: logger.error(f"处理语音时出错: {e}") record_file_path = None 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 4c73fdf38..7ab835725 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -161,11 +161,11 @@ class QQOfficialWebhookPlatformAdapter(Platform): ) await self.webhook_helper.initialize() - # 如果启用统一 webhook 模式,则不启动独立服务器 + # 如果启用统一 webhook 模式,则不启动独立服务器 webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info(f"{self.meta().id}(QQ 官方机器人 Webhook)", webhook_uuid) - # 保持运行状态,等待 shutdown + # 保持运行状态,等待 shutdown await self.webhook_helper.shutdown_event.wait() else: await self.webhook_helper.start_polling() 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 7af066020..ced55b5da 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -48,7 +48,7 @@ class QQOfficialWebhook: logger.info("正在登录到 QQ 官方机器人...") self.user = await self.http.login(self.token) logger.info(f"已登录 QQ 官方机器人账号: {self.user}") - # 直接注入到 botpy 的 Client,移花接木! + # 直接注入到 botpy 的 Client,移花接木! self.client.api = self.api self.client.http = self.http @@ -89,7 +89,7 @@ class QQOfficialWebhook: return await self.handle_callback(quart.request) async def handle_callback(self, request) -> dict: - """处理 webhook 回调,可被统一 webhook 入口复用 + """处理 webhook 回调,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -139,7 +139,7 @@ class QQOfficialWebhook: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。", + f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。", ) 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 5c2f7a37f..a49b77ab5 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -121,7 +121,7 @@ class SatoriPlatformAdapter(Platform): break if retry_count >= max_retries: - logger.error(f"达到最大重试次数 ({max_retries}),停止重试") + logger.error(f"达到最大重试次数 ({max_retries}),停止重试") break if not self.auto_reconnect: @@ -520,7 +520,7 @@ class SatoriPlatformAdapter(Platform): return None except ET.ParseError as e: - logger.warning(f"XML解析失败,使用正则提取: {e}") + logger.warning(f"XML解析失败,使用正则提取: {e}") return await self._extract_quote_with_regex(content) except Exception as e: logger.error(f"提取标签时发生错误: {e}") @@ -563,7 +563,7 @@ class SatoriPlatformAdapter(Platform): nickname=quote_author.get("nick", quote_author.get("name", "")), ) else: - # 如果没有作者信息,使用默认值 + # 如果没有作者信息,使用默认值 quote_abm.sender = MessageMember( user_id=quote.get("user_id", ""), nickname="内容", @@ -580,7 +580,7 @@ class SatoriPlatformAdapter(Platform): quote_abm.timestamp = int(quote.get("timestamp", time.time())) - # 如果没有任何内容,使用默认文本 + # 如果没有任何内容,使用默认文本 if not quote_abm.message_str.strip(): quote_abm.message_str = "[引用消息]" @@ -621,14 +621,14 @@ class SatoriPlatformAdapter(Platform): await self._parse_xml_node(root, elements) except ET.ParseError as e: logger.warning(f"解析 Satori 元素时发生解析错误: {e}, 错误内容: {content}") - # 如果解析失败,将整个内容当作纯文本 + # 如果解析失败,将整个内容当作纯文本 if content.strip(): elements.append(Plain(text=content)) except Exception as e: logger.error(f"解析 Satori 元素时发生未知错误: {e}") raise e - # 如果没有解析到任何元素,将整个内容当作纯文本 + # 如果没有解析到任何元素,将整个内容当作纯文本 if not elements and content.strip(): elements.append(Plain(text=content)) @@ -640,7 +640,7 @@ class SatoriPlatformAdapter(Platform): elements.append(Plain(text=node.text)) for child in node: - # 获取标签名,去除命名空间前缀 + # 获取标签名,去除命名空间前缀 tag_name = child.tag if "}" in tag_name: tag_name = tag_name.split("}")[1] @@ -711,7 +711,7 @@ class SatoriPlatformAdapter(Platform): elements.append(Plain(text="[JSON卡片]")) else: - # 未知标签,递归处理其内容 + # 未知标签,递归处理其内容 if child.text and child.text.strip(): elements.append(Plain(text=child.text)) await self._parse_xml_node(child, elements) diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 021422283..57d0b311e 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -261,7 +261,7 @@ class SatoriPlatformEvent(AstrMessageEvent): elif isinstance(component, Forward): return f'' - # 对于其他未处理的组件类型,返回空字符串 + # 对于其他未处理的组件类型,返回空字符串 return "" except Exception as e: @@ -282,7 +282,7 @@ class SatoriPlatformEvent(AstrMessageEvent): content = "".join(content_parts) - # 如果内容为空,添加默认内容 + # 如果内容为空,添加默认内容 if not content.strip(): content = "[转发消息]" @@ -354,7 +354,7 @@ class SatoriPlatformEvent(AstrMessageEvent): elif isinstance(component, Forward): return f'' - # 对于其他未处理的组件类型,返回空字符串 + # 对于其他未处理的组件类型,返回空字符串 return "" except Exception as e: @@ -376,7 +376,7 @@ class SatoriPlatformEvent(AstrMessageEvent): content = "".join(content_parts) - # 如果内容为空,添加默认内容 + # 如果内容为空,添加默认内容 if not content.strip(): content = "[转发消息]" diff --git a/astrbot/core/platform/sources/slack/client.py b/astrbot/core/platform/sources/slack/client.py index efd7a6f3d..c10f3cd17 100644 --- a/astrbot/core/platform/sources/slack/client.py +++ b/astrbot/core/platform/sources/slack/client.py @@ -17,7 +17,7 @@ from astrbot.api import logger class SlackWebhookClient: - """Slack Webhook 模式客户端,使用 Quart 作为 Web 服务器""" + """Slack Webhook 模式客户端,使用 Quart 作为 Web 服务器""" def __init__( self, @@ -58,7 +58,7 @@ class SlackWebhookClient: return {"status": "ok", "service": "slack-webhook"} async def handle_callback(self, req): - """处理 Slack 回调请求,可被统一 webhook 入口复用 + """处理 Slack 回调请求,可被统一 webhook 入口复用 Args: req: Quart 请求对象 @@ -108,7 +108,7 @@ class SlackWebhookClient: async def start(self) -> None: """启动 Webhook 服务器""" logger.info( - f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}...", + f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}...", ) await self.app.run_task( diff --git a/astrbot/core/platform/sources/slack/slack_adapter.py b/astrbot/core/platform/sources/slack/slack_adapter.py index 47d4e40e7..9488ad4d4 100644 --- a/astrbot/core/platform/sources/slack/slack_adapter.py +++ b/astrbot/core/platform/sources/slack/slack_adapter.py @@ -29,7 +29,7 @@ from .slack_event import SlackMessageEvent @register_platform_adapter( "slack", - "适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。", + "适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。", support_streaming_message=False, ) class SlackAdapter(Platform): @@ -65,7 +65,7 @@ class SlackAdapter(Platform): self.metadata = PlatformMetadata( name="slack", - description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。", + description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。", id=cast(str, self.config.get("id")), support_streaming_message=False, ) @@ -357,21 +357,21 @@ class SlackAdapter(Platform): self._handle_webhook_event, ) - # 如果启用统一 webhook 模式,则不启动独立服务器 + # 如果启用统一 webhook 模式,则不启动独立服务器 webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info(f"{self.meta().id}(Slack)", webhook_uuid) - # 保持运行状态,等待 shutdown + # 保持运行状态,等待 shutdown await self.webhook_client.shutdown_event.wait() else: logger.info( - f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...", + f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...", ) await self.webhook_client.start() else: raise ValueError( - f"不支持的连接模式: {self.connection_mode},请使用 'socket' 或 'webhook'", + f"不支持的连接模式: {self.connection_mode},请使用 'socket' 或 'webhook'", ) async def _handle_webhook_event(self, event_data: dict) -> None: diff --git a/astrbot/core/platform/sources/slack/slack_event.py b/astrbot/core/platform/sources/slack/slack_event.py index 3f62690b5..8ef14d3ea 100644 --- a/astrbot/core/platform/sources/slack/slack_event.py +++ b/astrbot/core/platform/sources/slack/slack_event.py @@ -100,7 +100,7 @@ class SlackMessageEvent(AstrMessageEvent): if isinstance(segment, Plain): text_content += segment.text else: - # 如果有文本内容,先添加文本块 + # 如果有文本内容,先添加文本块 if text_content.strip(): blocks.append( { @@ -148,7 +148,7 @@ class SlackMessageEvent(AstrMessageEvent): blocks=blocks or None, ) except Exception: - # 如果块发送失败,尝试只发送文本 + # 如果块发送失败,尝试只发送文本 parts = [] for segment in message.chain: if isinstance(segment, Plain): @@ -191,14 +191,14 @@ class SlackMessageEvent(AstrMessageEvent): return await super().send_streaming(generator, use_fallback) buffer = "" - pattern = re.compile(r"[^。?!~…]+[。?!~…]+") + pattern = re.compile(r"[^。?!~…]+[。?!~…]+") async for chain in generator: if isinstance(chain, MessageChain): for comp in chain.chain: if isinstance(comp, Plain): buffer += comp.text - if any(p in buffer for p in "。?!~…"): + if any(p in buffer for p in "。?!~…"): buffer = await self.process_buffer(buffer, pattern) else: await self.send(MessageChain(chain=[comp])) @@ -238,7 +238,7 @@ class SlackMessageEvent(AstrMessageEvent): ), ) except Exception: - # 如果获取用户信息失败,使用默认信息 + # 如果获取用户信息失败,使用默认信息 members.append(MessageMember(user_id=member_id, nickname=member_id)) channel_data = cast(dict, channel_info["channel"]) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 87e21391e..8f7ddefda 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -188,7 +188,7 @@ class TelegramPlatformAdapter(Platform): for cmd_name, description in cmd_info_list: if cmd_name in command_dict: logger.warning( - f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " + f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " f"'{command_dict[cmd_name]}'" ) command_dict.setdefault(cmd_name, description) @@ -202,7 +202,7 @@ class TelegramPlatformAdapter(Platform): handler_metadata, skip_commands: set, ) -> list[tuple[str, str]] | None: - """从事件过滤器中提取指令信息,包括所有别名""" + """从事件过滤器中提取指令信息,包括所有别名""" cmd_names = [] is_group = False if isinstance(event_filter, CommandFilter) and event_filter.command_name: @@ -270,11 +270,11 @@ class TelegramPlatformAdapter(Platform): context: ContextTypes.DEFAULT_TYPE, get_reply=True, ) -> AstrBotMessage | None: - """转换 Telegram 的消息对象为 AstrBotMessage 对象。 + """转换 Telegram 的消息对象为 AstrBotMessage 对象。 - @param update: Telegram 的 Update 对象。 - @param context: Telegram 的 Context 对象。 - @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。 + @param update: Telegram 的 Update 对象。 + @param context: Telegram 的 Context 对象。 + @param get_reply: 是否获取回复消息。这个参数是为了防止多个回复嵌套。 """ if not update.message: logger.warning("Received an update without a message.") @@ -363,7 +363,7 @@ class TelegramPlatformAdapter(Platform): entity.offset + 1 : entity.offset + entity.length ] message.message.append(Comp.At(qq=name, name=name)) - # 如果mention是当前bot则移除;否则保留 + # 如果mention是当前bot则移除;否则保留 if name.lower() == context.bot.username.lower(): plain_text = ( plain_text[: entity.offset] diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index f963969b7..5820afac6 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -42,7 +42,7 @@ class TelegramPlatformEvent(AstrMessageEvent): SPLIT_PATTERNS = { "paragraph": re.compile(r"\n\n"), "line": re.compile(r"\n"), - "sentence": re.compile(r"[.!?。!?]"), + "sentence": re.compile(r"[.!?。!?]"), "word": re.compile(r"\s"), } @@ -52,7 +52,7 @@ class TelegramPlatformEvent(AstrMessageEvent): @classmethod def _allocate_draft_id(cls) -> int: - """分配一个递增的 draft_id,溢出时归 1。""" + """分配一个递增的 draft_id,溢出时归 1。""" cls._next_draft_id = ( 1 if cls._next_draft_id >= cls._TELEGRAM_DRAFT_ID_MAX @@ -60,7 +60,7 @@ class TelegramPlatformEvent(AstrMessageEvent): ) return cls._next_draft_id - # 消息类型到 chat action 的映射,用于优先级判断 + # 消息类型到 chat action 的映射,用于优先级判断 ACTION_BY_TYPE: dict[type, str] = { Record: ChatAction.UPLOAD_VOICE, Video: ChatAction.UPLOAD_VIDEO, @@ -124,7 +124,7 @@ class TelegramPlatformEvent(AstrMessageEvent): @classmethod def _get_chat_action_for_chain(cls, chain: list[Any]) -> ChatAction | str: - """根据消息链中的组件类型确定合适的 chat action(按优先级)""" + """根据消息链中的组件类型确定合适的 chat action(按优先级)""" for seg_type, action in cls.ACTION_BY_TYPE.items(): if any(isinstance(seg, seg_type) for seg in chain): return action @@ -141,7 +141,7 @@ class TelegramPlatformEvent(AstrMessageEvent): message_thread_id: str | None = None, **payload: Any, ) -> None: - """发送媒体时显示 upload action,发送完成后恢复 typing""" + """发送媒体时显示 upload action,发送完成后恢复 typing""" effective_thread_id = message_thread_id or cast( str | None, payload.get("message_thread_id") ) @@ -197,7 +197,7 @@ class TelegramPlatformEvent(AstrMessageEvent): raise logger.warning( "User privacy settings prevent receiving voice messages, falling back to sending an audio file. " - "To enable voice messages, go to Telegram Settings → Privacy and Security → Voice Messages → set to 'Everyone'." + "To enable voice messages, go to Telegram Settings → Privacy and Security → Voice Messages → set to 'Everyone'." ) if use_media_action: media_payload = dict(payload) @@ -339,13 +339,13 @@ class TelegramPlatformEvent(AstrMessageEvent): await super().send(message) async def react(self, emoji: str | None, big: bool = False) -> None: - """给原消息添加 Telegram 反应: - - 普通 emoji:传入 '👍'、'😂' 等 - - 自定义表情:传入其 custom_emoji_id(纯数字字符串) - - 取消本机器人的反应:传入 None 或空字符串 + """给原消息添加 Telegram 反应: + - 普通 emoji:传入 '👍'、'😂' 等 + - 自定义表情:传入其 custom_emoji_id(纯数字字符串) + - 取消本机器人的反应:传入 None 或空字符串 """ try: - # 解析 chat_id(去掉超级群的 "#" 片段) + # 解析 chat_id(去掉超级群的 "#" 片段) if self.get_message_type() == MessageType.GROUP_MESSAGE: chat_id = (self.message_obj.group_id or "").split("#")[0] else: @@ -353,10 +353,10 @@ class TelegramPlatformEvent(AstrMessageEvent): message_id = int(self.message_obj.message_id) - # 组装 reaction 参数(必须是 ReactionType 的列表) + # 组装 reaction 参数(必须是 ReactionType 的列表) if not emoji: # 清空本 bot 的反应 reaction_param = [] # 空列表表示移除本 bot 的反应 - elif emoji.isdigit(): # 自定义表情:传 custom_emoji_id + elif emoji.isdigit(): # 自定义表情:传 custom_emoji_id reaction_param = [ReactionTypeCustomEmoji(emoji)] else: # 普通 emoji reaction_param = [ReactionTypeEmoji(emoji)] @@ -365,7 +365,7 @@ class TelegramPlatformEvent(AstrMessageEvent): chat_id=chat_id, message_id=message_id, reaction=reaction_param, # 注意是列表 - is_big=big, # 可选:大动画 + is_big=big, # 可选:大动画 ) except Exception as e: logger.error(f"[Telegram] 添加反应失败: {e}") @@ -378,16 +378,16 @@ class TelegramPlatformEvent(AstrMessageEvent): message_thread_id: str | None = None, parse_mode: str | None = None, ) -> None: - """通过 Bot.send_message_draft 发送草稿消息(流式推送部分消息)。 + """通过 Bot.send_message_draft 发送草稿消息(流式推送部分消息)。 - 该 API 仅支持私聊。 + 该 API 仅支持私聊。 Args: chat_id: 目标私聊的 chat_id - draft_id: 草稿唯一标识,非零整数;相同 draft_id 的变更会以动画展示 - text: 消息文本,1-4096 字符 - message_thread_id: 可选,目标消息线程 ID - parse_mode: 可选,消息文本的解析模式 + draft_id: 草稿唯一标识,非零整数;相同 draft_id 的变更会以动画展示 + text: 消息文本,1-4096 字符 + message_thread_id: 可选,目标消息线程 ID + parse_mode: 可选,消息文本的解析模式 """ kwargs: dict[str, Any] = {} if message_thread_id: @@ -416,7 +416,7 @@ class TelegramPlatformEvent(AstrMessageEvent): message_thread_id: str | None, on_text: Callable[[str], None], ) -> None: - """处理 MessageChain 中的各类组件,文本通过 on_text 回调追加,媒体直接发送。""" + """处理 MessageChain 中的各类组件,文本通过 on_text 回调追加,媒体直接发送。""" for i in chain.chain: if isinstance(i, Plain): on_text(i.text) @@ -475,7 +475,7 @@ class TelegramPlatformEvent(AstrMessageEvent): logger.warning(f"不支持的消息类型: {type(i)}") async def _send_final_segment(self, delta: str, payload: dict[str, Any]) -> None: - """将累积文本作为 MarkdownV2 真实消息发送,失败时回退到纯文本。""" + """将累积文本作为 MarkdownV2 真实消息发送,失败时回退到纯文本。""" try: markdown_text = telegramify_markdown.markdownify( delta, @@ -486,7 +486,7 @@ class TelegramPlatformEvent(AstrMessageEvent): **cast(Any, payload), ) except Exception as e: - logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") + logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") await self.client.send_message(text=delta, **cast(Any, payload)) async def send_streaming(self, generator, use_fallback: bool = False): @@ -506,7 +506,7 @@ class TelegramPlatformEvent(AstrMessageEvent): if message_thread_id: payload["message_thread_id"] = message_thread_id - # sendMessageDraft 仅支持私聊(显式检查 FRIEND_MESSAGE) + # sendMessageDraft 仅支持私聊(显式检查 FRIEND_MESSAGE) is_private = self.get_message_type() == MessageType.FRIEND_MESSAGE if is_private: @@ -520,7 +520,7 @@ class TelegramPlatformEvent(AstrMessageEvent): user_name, message_thread_id, payload, generator ) - # 内联父类 send_streaming 的副作用(避免传入已消费的 generator) + # 内联父类 send_streaming 的副作用(避免传入已消费的 generator) asyncio.create_task( Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), ) @@ -533,26 +533,26 @@ class TelegramPlatformEvent(AstrMessageEvent): payload: dict[str, Any], generator, ) -> None: - """使用 sendMessageDraft API 进行流式推送(私聊专用)。 + """使用 sendMessageDraft API 进行流式推送(私聊专用)。 - 流式过程中使用 sendMessageDraft 推送草稿动画, - 流式结束后发送一条真实消息保留最终内容(draft 是临时的,会消失)。 - 使用信号驱动的发送循环:每次有新 token 到达时唤醒发送, - 发送频率由网络 RTT 自然限制(最多一个请求 in-flight)。 + 流式过程中使用 sendMessageDraft 推送草稿动画, + 流式结束后发送一条真实消息保留最终内容(draft 是临时的,会消失)。 + 使用信号驱动的发送循环:每次有新 token 到达时唤醒发送, + 发送频率由网络 RTT 自然限制(最多一个请求 in-flight)。 """ draft_id = self._allocate_draft_id() delta = "" last_sent_text = "" - done = False # 信号:生成器已结束 + done = False # 信号:生成器已结束 text_changed = asyncio.Event() # 有新 token 到达时触发 async def _draft_sender_loop() -> None: - """信号驱动的草稿发送循环,有新内容就发,RTT 自然限流。""" + """信号驱动的草稿发送循环,有新内容就发,RTT 自然限流。""" nonlocal last_sent_text while not done: await text_changed.wait() text_changed.clear() - # 发送最新的缓冲区内容(MarkdownV2 渲染,与真实消息一致) + # 发送最新的缓冲区内容(MarkdownV2 渲染,与真实消息一致) if delta and delta != last_sent_text: draft_text = delta[: self.MAX_MESSAGE_LENGTH] if draft_text != last_sent_text: @@ -569,7 +569,7 @@ class TelegramPlatformEvent(AstrMessageEvent): ) last_sent_text = draft_text except Exception: - # markdownify 对未闭合语法可能失败,回退纯文本 + # markdownify 对未闭合语法可能失败,回退纯文本 try: await self._send_message_draft( user_name, @@ -596,9 +596,9 @@ class TelegramPlatformEvent(AstrMessageEvent): continue if chain.type == "break": - # 分割符:发送真实消息保留内容,重置缓冲区 + # 分割符:发送真实消息保留内容,重置缓冲区 if delta: - # 用 emoji 清空 draft 显示,避免 draft 和真实消息同时可见 + # 用 emoji 清空 draft 显示,避免 draft 和真实消息同时可见 await self._send_message_draft( user_name, draft_id, @@ -619,7 +619,7 @@ class TelegramPlatformEvent(AstrMessageEvent): text_changed.set() # 唤醒循环使其退出 await sender_task - # 流式结束:用 emoji 清空 draft,然后发真实消息持久化 + # 流式结束:用 emoji 清空 draft,然后发真实消息持久化 if delta: await self._send_message_draft( user_name, @@ -636,7 +636,7 @@ class TelegramPlatformEvent(AstrMessageEvent): payload: dict[str, Any], generator, ) -> None: - """使用 send_message + edit_message_text 进行流式推送(群聊 fallback)。""" + """使用 send_message + edit_message_text 进行流式推送(群聊 fallback)。""" delta = "" current_content = "" message_id = None @@ -724,7 +724,7 @@ class TelegramPlatformEvent(AstrMessageEvent): parse_mode="MarkdownV2", ) except Exception as e: - logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") + logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") await self.client.edit_message_text( text=delta, chat_id=payload["chat_id"], diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index fe2f5a641..aa6838ba5 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -160,12 +160,12 @@ class WebChatAdapter(Platform): depth: int = 0, max_depth: int = 1, ) -> tuple[list, list[str]]: - """解析消息段列表,返回消息组件列表和纯文本列表 + """解析消息段列表,返回消息组件列表和纯文本列表 Args: message_parts: 消息段列表 depth: 当前递归深度 - max_depth: 最大递归深度(用于处理 reply) + max_depth: 最大递归深度(用于处理 reply) Returns: tuple[list, list[str]]: (消息组件列表, 纯文本列表) diff --git a/astrbot/core/platform/sources/webchat/webchat_event.py b/astrbot/core/platform/sources/webchat/webchat_event.py index fc341c9d2..4643d32e4 100644 --- a/astrbot/core/platform/sources/webchat/webchat_event.py +++ b/astrbot/core/platform/sources/webchat/webchat_event.py @@ -147,9 +147,9 @@ class WebChatMessageEvent(AstrMessageEvent): conversation_id, ) async for chain in generator: - # 处理音频流(Live Mode) + # 处理音频流(Live Mode) if chain.type == "audio_chunk": - # 音频流数据,直接发送 + # 音频流数据,直接发送 audio_b64 = "" text = None diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 3071475c5..804338ceb 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -71,7 +71,7 @@ class WecomServer: return await self.handle_verify(quart.request) async def handle_verify(self, request) -> str: - """处理验证请求,可被统一 webhook 入口复用 + """处理验证请求,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -88,10 +88,10 @@ class WecomServer: args.get("nonce"), args.get("echostr"), ) - logger.info("验证请求有效性成功。") + logger.info("验证请求有效性成功。") return echo_str except InvalidSignatureException: - logger.error("验证请求有效性失败,签名异常,请检查配置。") + logger.error("验证请求有效性失败,签名异常,请检查配置。") raise async def callback_command(self): @@ -99,7 +99,7 @@ class WecomServer: return await self.handle_callback(quart.request) async def handle_callback(self, request) -> str: - """处理回调请求,可被统一 webhook 入口复用 + """处理回调请求,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -114,7 +114,7 @@ class WecomServer: try: xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce) except InvalidSignatureException: - logger.error("解密失败,签名异常,请检查配置。") + logger.error("解密失败,签名异常,请检查配置。") raise else: msg = cast(BaseMessage, parse_message(xml)) @@ -127,7 +127,7 @@ class WecomServer: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。", + f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。", ) await self.server.run_task( host=self.callback_server_host, @@ -220,12 +220,12 @@ class WecomPlatformAdapter(Platform): ) -> None: # 企业微信客服不支持主动发送 if hasattr(self.client, "kf_message"): - logger.warning("企业微信客服模式不支持 send_by_session 主动发送。") + logger.warning("企业微信客服模式不支持 send_by_session 主动发送。") await super().send_by_session(session, message_chain) return if not self.agent_id: logger.warning( - f"send_by_session 失败:无法为会话 {session.session_id} 推断 agent_id。", + f"send_by_session 失败:无法为会话 {session.session_id} 推断 agent_id。", ) await super().send_by_session(session, message_chain) return @@ -278,7 +278,7 @@ class WecomPlatformAdapter(Platform): continue open_kfid = acc.get("open_kfid", None) if not open_kfid: - logger.error("获取微信客服失败,open_kfid 为空。") + logger.error("获取微信客服失败,open_kfid 为空。") logger.debug(f"Found open_kfid: {open_kfid!s}") kf_url = ( await loop.run_in_executor( @@ -289,16 +289,16 @@ class WecomPlatformAdapter(Platform): ) ).get("url", "") logger.info( - f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}", + f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}", ) except Exception as e: logger.error(e) - # 如果启用统一 webhook 模式,则不启动独立服务器 + # 如果启用统一 webhook 模式,则不启动独立服务器 webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info(f"{self.meta().id}(企业微信)", webhook_uuid) - # 保持运行状态,等待 shutdown + # 保持运行状态,等待 shutdown await self.server.shutdown_event.wait() else: await self.server.start_polling() @@ -354,7 +354,7 @@ class WecomPlatformAdapter(Platform): path_wav = os.path.join(temp_dir, f"wecom_{msg.media_id}.wav") path_wav = await convert_audio_to_wav(path, path_wav) except Exception as e: - logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。") + logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。") path_wav = path return @@ -423,7 +423,7 @@ class WecomPlatformAdapter(Platform): path_wav = os.path.join(temp_dir, f"weixinkefu_{media_id}.wav") path_wav = await convert_audio_to_wav(path, path_wav) except Exception as e: - logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。") + logger.error(f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。") path_wav = path return diff --git a/astrbot/core/platform/sources/wecom/wecom_event.py b/astrbot/core/platform/sources/wecom/wecom_event.py index 47c7cdb5e..38432cc33 100644 --- a/astrbot/core/platform/sources/wecom/wecom_event.py +++ b/astrbot/core/platform/sources/wecom/wecom_event.py @@ -57,15 +57,15 @@ class WecomPlatformEvent(AstrMessageEvent): cut_position = end for i in range(end, start, -1): if i < len(plain) and plain[i - 1] in [ - "。", - "!", - "?", + "。", + "!", + "?", ".", "!", "?", "\n", ";", - ";", + ";", ]: cut_position = i break @@ -87,7 +87,7 @@ class WecomPlatformEvent(AstrMessageEvent): # 微信客服 kf_message_api = getattr(self.client, "kf_message", None) if not isinstance(kf_message_api, WeChatKFMessage): - logger.warning("未找到微信客服发送消息方法。") + logger.warning("未找到微信客服发送消息方法。") return user_id = self.get_sender_id() @@ -184,7 +184,7 @@ class WecomPlatformEvent(AstrMessageEvent): response["media_id"], ) else: - logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") + logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") else: # 企业微信应用 for comp in message.chain: @@ -284,7 +284,7 @@ class WecomPlatformEvent(AstrMessageEvent): response["media_id"], ) else: - logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") + logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") await super().send(message) diff --git a/astrbot/core/platform/sources/wecom/wecom_kf.py b/astrbot/core/platform/sources/wecom/wecom_kf.py index 51f4ee14f..dc4fb01b1 100644 --- a/astrbot/core/platform/sources/wecom/wecom_kf.py +++ b/astrbot/core/platform/sources/wecom/wecom_kf.py @@ -31,16 +31,16 @@ class WeChatKF(BaseWeChatAPI): """ def sync_msg(self, token, open_kfid, cursor="", limit=1000): - """微信客户发送的消息、接待人员在企业微信回复的消息、发送消息接口发送失败事件(如被用户拒收) - 、客户点击菜单消息的回复消息,可以通过该接口获取具体的消息内容和事件。不支持读取通过发送消息接口发送的消息。 - 支持的消息类型:文本、图片、语音、视频、文件、位置、链接、名片、小程序、事件。 + """微信客户发送的消息、接待人员在企业微信回复的消息、发送消息接口发送失败事件(如被用户拒收) + 、客户点击菜单消息的回复消息,可以通过该接口获取具体的消息内容和事件。不支持读取通过发送消息接口发送的消息。 + 支持的消息类型:文本、图片、语音、视频、文件、位置、链接、名片、小程序、事件。 - :param token: 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制。不多于128字节 + :param token: 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制。不多于128字节 :param open_kfid: 客服帐号ID - :param cursor: 上一次调用时返回的next_cursor,第一次拉取可以不填。不多于64字节 - :param limit: 期望请求的数据量,默认值和最大值都为1000。 - 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。 + :param cursor: 上一次调用时返回的next_cursor,第一次拉取可以不填。不多于64字节 + :param limit: 期望请求的数据量,默认值和最大值都为1000。 + 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。 :return: 接口调用结果 """ data = { @@ -55,11 +55,11 @@ class WeChatKF(BaseWeChatAPI): """获取会话状态 ID 状态 说明 - 0 未处理 新会话接入。可选择:1.直接用API自动回复消息。2.放进待接入池等待接待人员接待。3.指定接待人员进行接待 - 1 由智能助手接待 可使用API回复消息。可选择转入待接入池或者指定接待人员处理。 - 2 待接入池排队中 在待接入池中排队等待接待人员接入。可选择转为指定人员接待 - 3 由人工接待 人工接待中。可选择结束会话 - 4 已结束 会话已经结束。不允许变更会话状态,等待用户重新发起咨询 + 0 未处理 新会话接入。可选择:1.直接用API自动回复消息。2.放进待接入池等待接待人员接待。3.指定接待人员进行接待 + 1 由智能助手接待 可使用API回复消息。可选择转入待接入池或者指定接待人员处理。 + 2 待接入池排队中 在待接入池中排队等待接待人员接入。可选择转为指定人员接待 + 3 由人工接待 人工接待中。可选择结束会话 + 4 已结束 会话已经结束。不允许变更会话状态,等待用户重新发起咨询 :param open_kfid: 客服帐号ID :param external_userid: 微信客户的external_userid @@ -82,7 +82,7 @@ class WeChatKF(BaseWeChatAPI): :param open_kfid: 客服帐号ID :param external_userid: 微信客户的external_userid - :param service_state: 当前的会话状态,状态定义参考概述中的表格 + :param service_state: 当前的会话状态,状态定义参考概述中的表格 :return: 接口调用结果 """ data = { @@ -107,7 +107,7 @@ class WeChatKF(BaseWeChatAPI): def add_servicer(self, open_kfid, userid_list): """添加接待人员 - 添加指定客服帐号的接待人员。 + 添加指定客服帐号的接待人员。 :param open_kfid: 客服帐号ID :param userid_list: 接待人员userid列表 @@ -164,7 +164,7 @@ class WeChatKF(BaseWeChatAPI): """获取客服帐号链接 :param open_kfid: 客服帐号ID - :param scene: 场景值,字符串类型,由开发者自定义。不多于32字节;字符串取值范围(正则表达式):[0-9a-zA-Z_-]* + :param scene: 场景值,字符串类型,由开发者自定义。不多于32字节;字符串取值范围(正则表达式):[0-9a-zA-Z_-]* :return: 接口调用结果 """ data = {"open_kfid": open_kfid, "scene": scene} @@ -189,9 +189,9 @@ class WeChatKF(BaseWeChatAPI): :param open_kfid: 客服帐号ID :param external_userid: 微信客户的external_userid - :param service_type: 表示是升级到专员服务还是客户群服务。1:专员服务。2:客户群服务 - :param member: 推荐的服务专员,type等于1时有效 - :param groupchat: 推荐的客户群,type等于2时有效 + :param service_type: 表示是升级到专员服务还是客户群服务。1:专员服务。2:客户群服务 + :param member: 推荐的服务专员,type等于1时有效 + :param groupchat: 推荐的客户群,type等于2时有效 :return: 接口调用结果 """ data = { @@ -216,14 +216,14 @@ class WeChatKF(BaseWeChatAPI): return self._post("kf/customer/cancel_upgrade_service", data=data) def send_msg_on_event(self, code, msgtype, msg_content, msgid=None): - """当特定的事件回调消息包含code字段,可以此code为凭证,调用该接口给用户发送相应事件场景下的消息,如客服欢迎语。 - 支持发送消息类型:文本、菜单消息。 + """当特定的事件回调消息包含code字段,可以此code为凭证,调用该接口给用户发送相应事件场景下的消息,如客服欢迎语。 + 支持发送消息类型:文本、菜单消息。 - :param code: 事件响应消息对应的code。通过事件回调下发,仅可使用一次。 - :param msgtype: 消息类型。对不同的msgtype,有相应的结构描述,详见消息类型 - :param msg_content: 目前支持文本与菜单消息,具体查看文档 - :param msgid: 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节; - 字符串取值范围(正则表达式):[0-9a-zA-Z_-]* + :param code: 事件响应消息对应的code。通过事件回调下发,仅可使用一次。 + :param msgtype: 消息类型。对不同的msgtype,有相应的结构描述,详见消息类型 + :param msg_content: 目前支持文本与菜单消息,具体查看文档 + :param msgid: 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节; + 字符串取值范围(正则表达式):[0-9a-zA-Z_-]* :return: 接口调用结果 """ data = {"code": code, "msgtype": msgtype} @@ -233,7 +233,7 @@ class WeChatKF(BaseWeChatAPI): return self._post("kf/send_msg_on_event", data=data) def get_corp_statistic(self, start_time, end_time, open_kfid=None): - """获取「客户数据统计」企业汇总数据 + """获取「客户数据统计」企业汇总数据 :param start_time: 开始时间 :param end_time: 结束时间 @@ -250,7 +250,7 @@ class WeChatKF(BaseWeChatAPI): open_kfid=None, servicer_userid=None, ): - """获取「客户数据统计」接待人员明细数据 + """获取「客户数据统计」接待人员明细数据 :param start_time: 开始时间 :param end_time: 结束时间 diff --git a/astrbot/core/platform/sources/wecom/wecom_kf_message.py b/astrbot/core/platform/sources/wecom/wecom_kf_message.py index d839134ab..710213e69 100644 --- a/astrbot/core/platform/sources/wecom/wecom_kf_message.py +++ b/astrbot/core/platform/sources/wecom/wecom_kf_message.py @@ -30,7 +30,7 @@ class WeChatKFMessage(BaseWeChatAPI): https://work.weixin.qq.com/api/doc/90000/90135/94677 - 支持: + 支持: * 文本消息 * 图片消息 * 语音消息 @@ -43,14 +43,14 @@ class WeChatKFMessage(BaseWeChatAPI): """ def send(self, user_id, open_kfid, msgid="", msg=None): - """当微信客户处于“新接入待处理”或“由智能助手接待”状态下,可调用该接口给用户发送消息。 - 注意仅当微信客户在主动发送消息给客服后的48小时内,企业可发送消息给客户,最多可发送5条消息;若用户继续发送消息,企业可再次下发消息。 - 支持发送消息类型:文本、图片、语音、视频、文件、图文、小程序、菜单消息、地理位置。 + """当微信客户处于“新接入待处理”或“由智能助手接待”状态下,可调用该接口给用户发送消息。 + 注意仅当微信客户在主动发送消息给客服后的48小时内,企业可发送消息给客户,最多可发送5条消息;若用户继续发送消息,企业可再次下发消息。 + 支持发送消息类型:文本、图片、语音、视频、文件、图文、小程序、菜单消息、地理位置。 :param user_id: 指定接收消息的客户UserID :param open_kfid: 指定发送消息的客服帐号ID :param msgid: 指定消息ID - :param tag_ids: 标签ID列表。 + :param tag_ids: 标签ID列表。 :param msg: 发送消息的 dict 对象 :type msg: dict | None :return: 接口调用结果 diff --git a/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py b/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py index 260b950d1..76631be29 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py @@ -21,9 +21,9 @@ from Crypto.Cipher import AES from . import ierror """ -关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 -请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 -下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 +关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案 +请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。 +下载后,按照README中的“Installation”小节的提示进行pycrypto安装。 """ @@ -141,8 +141,8 @@ class Prpcrypt: """提供接收和推送给企业微信消息的加解密接口""" # 16位随机字符串的范围常量 - # randbelow(RANDOM_RANGE) 返回 [0, 8999999999999999](两端都包含,即包含0和8999999999999999) - # 加上 MIN_RANDOM_VALUE 后得到 [1000000000000000, 9999999999999999](两端都包含)即16位数字 + # randbelow(RANDOM_RANGE) 返回 [0, 8999999999999999](两端都包含,即包含0和8999999999999999) + # 加上 MIN_RANDOM_VALUE 后得到 [1000000000000000, 9999999999999999](两端都包含)即16位数字 MIN_RANDOM_VALUE = 1000000000000000 # 最小值: 1000000000000000 (16位) RANDOM_RANGE = 9000000000000000 # 范围大小: 确保最大值为 9999999999999999 (16位) @@ -187,7 +187,7 @@ class Prpcrypt: """ try: cryptor = AES.new(self.key, self.mode, self.key[:16]) # type: ignore - # 使用BASE64对密文进行解码,然后AES-CBC解密 + # 使用BASE64对密文进行解码,然后AES-CBC解密 plain_text = cryptor.decrypt(base64.b64decode(text)) except Exception as e: print(e) @@ -232,12 +232,12 @@ class WXBizJsonMsgCrypt: self.m_sReceiveId = sReceiveId # 验证URL - # @param sMsgSignature: 签名串,对应URL参数的msg_signature - # @param sTimeStamp: 时间戳,对应URL参数的timestamp - # @param sNonce: 随机串,对应URL参数的nonce - # @param sEchoStr: 随机串,对应URL参数的echostr - # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 - # @return:成功0,失败返回对应的错误码 + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sEchoStr: 随机串,对应URL参数的echostr + # @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效 + # @return:成功0,失败返回对应的错误码 def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr): sha1 = SHA1() @@ -252,11 +252,11 @@ class WXBizJsonMsgCrypt: def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None): # 将企业回复用户的消息加密打包 - # @param sReplyMsg: 企业号待回复用户的消息,json格式的字符串 - # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 - # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce - # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串, - # return:成功0,sEncryptMsg,失败返回对应的错误码None + # @param sReplyMsg: 企业号待回复用户的消息,json格式的字符串 + # @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 + # @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce + # sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串, + # return:成功0,sEncryptMsg,失败返回对应的错误码None pc = Prpcrypt(self.key) ret, encrypt = pc.encrypt(sReplyMsg, self.m_sReceiveId) encrypt = encrypt.decode("utf-8") # type: ignore @@ -273,13 +273,13 @@ class WXBizJsonMsgCrypt: return ret, jsonParse.generate(encrypt, signature, timestamp, sNonce) def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce): - # 检验消息的真实性,并且获取解密后的明文 - # @param sMsgSignature: 签名串,对应URL参数的msg_signature - # @param sTimeStamp: 时间戳,对应URL参数的timestamp - # @param sNonce: 随机串,对应URL参数的nonce - # @param sPostData: 密文,对应POST请求的数据 - # json_content: 解密后的原文,当return返回0时有效 - # @return: 成功0,失败返回对应的错误码 + # 检验消息的真实性,并且获取解密后的明文 + # @param sMsgSignature: 签名串,对应URL参数的msg_signature + # @param sTimeStamp: 时间戳,对应URL参数的timestamp + # @param sNonce: 随机串,对应URL参数的nonce + # @param sPostData: 密文,对应POST请求的数据 + # json_content: 解密后的原文,当return返回0时有效 + # @return: 成功0,失败返回对应的错误码 # 验证安全签名 jsonParse = JsonParse() ret, encrypt = jsonParse.extract(sPostData) diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py index 79fe6f8ed..eef80bff5 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py @@ -1,6 +1,6 @@ """企业微信智能机器人平台适配器 -基于企业微信智能机器人 API 的消息平台适配器,支持 HTTP 回调与长连接 -参考webchat_adapter.py的队列机制,实现异步消息处理和流式响应 +基于企业微信智能机器人 API 的消息平台适配器,支持 HTTP 回调与长连接 +参考webchat_adapter.py的队列机制,实现异步消息处理和流式响应 """ import asyncio @@ -44,7 +44,7 @@ from .wecomai_webhook import WecomAIBotWebhookClient, WecomAIBotWebhookError class WecomAIQueueListener: - """企业微信智能机器人队列监听器,参考webchat的QueueListener设计""" + """企业微信智能机器人队列监听器,参考webchat的QueueListener设计""" def __init__( self, @@ -55,7 +55,7 @@ class WecomAIQueueListener: self.callback = callback async def run(self) -> None: - """注册监听回调并定期清理过期响应。""" + """注册监听回调并定期清理过期响应。""" self.queue_mgr.set_listener(self.callback) while True: self.queue_mgr.cleanup_expired_responses() @@ -64,7 +64,7 @@ class WecomAIQueueListener: @register_platform_adapter( "wecom_ai_bot", - "企业微信智能机器人适配器,支持 HTTP 回调接收消息", + "企业微信智能机器人适配器,支持 HTTP 回调接收消息", ) class WecomAIBotAdapter(Platform): """企业微信智能机器人适配器""" @@ -119,7 +119,7 @@ class WecomAIBotAdapter(Platform): # 平台元数据 self.metadata = PlatformMetadata( name="wecom_ai_bot", - description="企业微信智能机器人适配器,支持 HTTP 回调和长连接模式", + description="企业微信智能机器人适配器,支持 HTTP 回调和长连接模式", id=self.config.get("id", "wecom_ai_bot"), support_proactive_message=bool(self.msg_push_webhook_url), ) @@ -131,7 +131,7 @@ class WecomAIBotAdapter(Platform): if self.connection_mode == "long_connection": if not self.long_connection_bot_id or not self.long_connection_secret: logger.warning( - "企业微信智能机器人长连接模式缺少 BotID 或 Secret,连接可能失败" + "企业微信智能机器人长连接模式缺少 BotID 或 Secret,连接可能失败" ) self.long_connection_client = WecomAIBotLongConnectionClient( bot_id=self.long_connection_bot_id, @@ -172,7 +172,7 @@ class WecomAIBotAdapter(Platform): logger.error("企业微信消息推送 webhook 配置无效: %s", e) async def _handle_queued_message(self, data: dict) -> None: - """处理队列中的消息,类似webchat的callback""" + """处理队列中的消息,类似webchat的callback""" try: abm = await self.convert_message(data) await self.handle_msg(abm) @@ -191,7 +191,7 @@ class WecomAIBotAdapter(Platform): callback_params: 回调参数 (nonce, timestamp) Returns: - 加密后的响应消息,无需响应时返回 None + 加密后的响应消息,无需响应时返回 None """ if not self.api_client: @@ -199,7 +199,7 @@ class WecomAIBotAdapter(Platform): return None msgtype = message_data.get("msgtype") if not msgtype: - logger.warning(f"消息类型未知,忽略: {message_data}") + logger.warning(f"消息类型未知,忽略: {message_data}") return None session_id = self._extract_session_id(message_data) if msgtype in ("text", "image", "mixed"): @@ -243,7 +243,7 @@ class WecomAIBotAdapter(Platform): else: logger.warning(f"Cannot find back queue for stream_id: {stream_id}") - # 返回结束标志,告诉微信服务器流已结束 + # 返回结束标志,告诉微信服务器流已结束 end_message = WecomAIBotStreamMessageBuilder.make_text_stream( stream_id, "", @@ -342,7 +342,7 @@ class WecomAIBotAdapter(Platform): elif msgtype == "event": event = message_data.get("event") if event == "enter_chat" and self.friend_message_welcome_text: - # 用户进入会话,发送欢迎消息 + # 用户进入会话,发送欢迎消息 try: resp = WecomAIBotStreamMessageBuilder.make_text( self.friend_message_welcome_text, @@ -360,7 +360,7 @@ class WecomAIBotAdapter(Platform): self, payload: dict[str, Any], ) -> None: - """处理长连接回调消息。""" + """处理长连接回调消息。""" cmd = payload.get("cmd") headers = payload.get("headers") or {} body = payload.get("body") or {} @@ -407,7 +407,7 @@ class WecomAIBotAdapter(Platform): await self._send_long_connection_respond_welcome(req_id) elif event_type == "disconnected_event": logger.warning( - "[WecomAI][LongConn] 收到 disconnected_event,旧连接将被关闭" + "[WecomAI][LongConn] 收到 disconnected_event,旧连接将被关闭" ) async def _send_long_connection_respond_welcome(self, req_id: str) -> bool: @@ -441,7 +441,7 @@ class WecomAIBotAdapter(Platform): def _extract_session_id(self, message_data: dict[str, Any]) -> str: """从消息数据中提取会话ID - 群聊使用 chatid,单聊使用 userid + 群聊使用 chatid,单聊使用 userid """ chattype = message_data.get("chattype", "single") if chattype == "group": @@ -471,7 +471,7 @@ class WecomAIBotAdapter(Platform): logger.debug(f"[WecomAI] 消息已入队: {stream_id}") async def convert_message(self, payload: dict) -> AstrBotMessage: - """转换队列中的消息数据为AstrBotMessage,类似webchat的convert_message""" + """转换队列中的消息数据为AstrBotMessage,类似webchat的convert_message""" message_data = payload["message_data"] session_id = payload["session_id"] # callback_params = payload["callback_params"] # 保留但暂时不使用 @@ -566,10 +566,10 @@ class WecomAIBotAdapter(Platform): session: MessageSesion, message_chain: MessageChain, ) -> None: - """通过消息推送 webhook 发送消息。""" + """通过消息推送 webhook 发送消息。""" if not self.webhook_client: logger.warning( - "主动消息发送失败: 未配置企业微信消息推送 Webhook URL,请前往配置添加。session_id=%s", + "主动消息发送失败: 未配置企业微信消息推送 Webhook URL,请前往配置添加。session_id=%s", session.session_id, ) await super().send_by_session(session, message_chain) @@ -586,7 +586,7 @@ class WecomAIBotAdapter(Platform): await super().send_by_session(session, message_chain) def run(self) -> Awaitable[Any]: - """运行适配器,同时启动HTTP服务器和队列监听器""" + """运行适配器,同时启动HTTP服务器和队列监听器""" async def run_both() -> None: if self.connection_mode == "long_connection": @@ -600,7 +600,7 @@ class WecomAIBotAdapter(Platform): self.queue_listener.run(), ) else: - # 如果启用统一 webhook 模式,则不启动独立服务器 + # 如果启用统一 webhook 模式,则不启动独立服务器 webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info( @@ -612,7 +612,7 @@ class WecomAIBotAdapter(Platform): if not self.server: raise RuntimeError("Webhook 服务器未初始化") logger.info( - "启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port + "启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port ) # 同时运行HTTP服务器和队列监听器 await asyncio.gather( @@ -646,7 +646,7 @@ class WecomAIBotAdapter(Platform): return self.metadata async def handle_msg(self, message: AstrBotMessage) -> None: - """处理消息,创建消息事件并提交到事件队列""" + """处理消息,创建消息事件并提交到事件队列""" try: message_event = WecomAIBotMessageEvent( message_str=message.message_str, diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py index 97831fbb2..682f33a44 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py @@ -1,5 +1,5 @@ """企业微信智能机器人 API 客户端 -处理消息加密解密、API 调用等 +处理消息加密解密、API 调用等 """ import base64 @@ -59,14 +59,14 @@ class WecomAIBotAPIClient: ) if ret != WecomAIBotConstants.SUCCESS: - logger.error(f"消息解密失败,错误码: {ret}") + logger.error(f"消息解密失败,错误码: {ret}") return ret, None # 解析 JSON if decrypted_msg: try: message_data = json.loads(decrypted_msg) - logger.debug(f"解密成功,消息内容: {message_data}") + logger.debug(f"解密成功,消息内容: {message_data}") return WecomAIBotConstants.SUCCESS, message_data except json.JSONDecodeError as e: logger.error(f"JSON 解析失败: {e}, 原始消息: {decrypted_msg}") @@ -93,14 +93,14 @@ class WecomAIBotAPIClient: timestamp: 时间戳 Returns: - 加密后的消息,失败时返回 None + 加密后的消息,失败时返回 None """ try: ret, encrypted_msg = self.wxcpt.EncryptMsg(plain_message, nonce, timestamp) if ret != WecomAIBotConstants.SUCCESS: - logger.error(f"消息加密失败,错误码: {ret}") + logger.error(f"消息加密失败,错误码: {ret}") return None logger.debug("消息加密成功") @@ -138,7 +138,7 @@ class WecomAIBotAPIClient: ) if ret != WecomAIBotConstants.SUCCESS: - logger.error(f"URL 验证失败,错误码: {ret}") + logger.error(f"URL 验证失败,错误码: {ret}") return "verify fail" logger.info("URL 验证成功") @@ -157,7 +157,7 @@ class WecomAIBotAPIClient: Args: image_url: 加密图片的 URL - aes_key_base64: Base64 编码的 AES 密钥,如果为 None 则使用实例的密钥 + aes_key_base64: Base64 编码的 AES 密钥,如果为 None 则使用实例的密钥 Returns: (是否成功, 图片数据或错误信息) @@ -170,12 +170,12 @@ class WecomAIBotAPIClient: async with aiohttp.ClientSession() as session: async with session.get(image_url, timeout=15) as response: if response.status != 200: - error_msg = f"图片下载失败,状态码: {response.status}" + error_msg = f"图片下载失败,状态码: {response.status}" logger.error(error_msg) return False, error_msg encrypted_data = await response.read() - logger.info(f"图片下载成功,大小: {len(encrypted_data)} 字节") + logger.info(f"图片下载成功,大小: {len(encrypted_data)} 字节") # 准备解密密钥 if aes_key_base64 is None: @@ -203,7 +203,7 @@ class WecomAIBotAPIClient: raise ValueError("无效的填充长度 (大于32字节)") decrypted_data = decrypted_data[:-pad_len] - logger.info(f"图片解密成功,解密后大小: {len(decrypted_data)} 字节") + logger.info(f"图片解密成功,解密后大小: {len(decrypted_data)} 字节") return True, decrypted_data @@ -333,7 +333,7 @@ class WecomAIBotMessageParser: data: 消息数据 Returns: - 文本内容,解析失败返回 None + 文本内容,解析失败返回 None """ try: @@ -350,7 +350,7 @@ class WecomAIBotMessageParser: data: 消息数据 Returns: - 图片 URL,解析失败返回 None + 图片 URL,解析失败返回 None """ try: @@ -367,7 +367,7 @@ class WecomAIBotMessageParser: data: 消息数据 Returns: - 流消息数据,解析失败返回 None + 流消息数据,解析失败返回 None """ try: @@ -390,7 +390,7 @@ class WecomAIBotMessageParser: data: 消息数据 Returns: - 消息项列表,解析失败返回 None + 消息项列表,解析失败返回 None """ try: @@ -407,7 +407,7 @@ class WecomAIBotMessageParser: data: 消息数据 Returns: - 事件数据,解析失败返回 None + 事件数据,解析失败返回 None """ try: diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py index f27d4671e..640424af3 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py @@ -1,4 +1,4 @@ -"""企业微信智能机器人事件处理模块,处理消息事件的发送和接收""" +"""企业微信智能机器人事件处理模块,处理消息事件的发送和接收""" import asyncio from collections.abc import Awaitable, Callable @@ -113,7 +113,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): }, ) else: - logger.warning("图片数据为空,跳过") + logger.warning("图片数据为空,跳过") except Exception as e: logger.error("处理图片消息失败: %s", e) else: @@ -204,7 +204,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): await super().send(MessageChain([])) async def send_streaming(self, generator, use_fallback=False) -> None: - """流式发送消息,参考webchat的send_streaming设计""" + """流式发送消息,参考webchat的send_streaming设计""" final_data = "" raw = self.message_obj.raw_message assert isinstance(raw, dict), ( @@ -296,7 +296,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): await super().send_streaming(generator, use_fallback) return - # 企业微信智能机器人不支持增量发送,因此我们需要在这里将增量内容累积起来,按间隔推送 + # 企业微信智能机器人不支持增量发送,因此我们需要在这里将增量内容累积起来,按间隔推送 increment_plain = "" last_stream_update_time = 0.0 diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py index 1017dd230..d4712c0e0 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py @@ -1,4 +1,4 @@ -"""企业微信智能机器人长连接客户端。""" +"""企业微信智能机器人长连接客户端。""" import asyncio import json @@ -12,7 +12,7 @@ from astrbot.api import logger class WecomAIBotLongConnectionClient: - """企业微信智能机器人 WebSocket 长连接客户端。""" + """企业微信智能机器人 WebSocket 长连接客户端。""" def __init__( self, @@ -40,7 +40,7 @@ class WecomAIBotLongConnectionClient: return uuid.uuid4().hex async def start(self) -> None: - """启动长连接并自动重连。""" + """启动长连接并自动重连。""" reconnect_delay = 1 while not self._shutdown_event.is_set(): try: @@ -65,7 +65,7 @@ class WecomAIBotLongConnectionClient: ) as ws: self._ws = ws await self._subscribe() - logger.info("[WecomAI][LongConn] 订阅成功,已建立长连接") + logger.info("[WecomAI][LongConn] 订阅成功,已建立长连接") heartbeat_task = asyncio.create_task(self._heartbeat_loop()) try: @@ -88,7 +88,7 @@ class WecomAIBotLongConnectionClient: self._ws = None async def _subscribe(self) -> None: - """发送 aibot_subscribe,并等待响应。""" + """发送 aibot_subscribe,并等待响应。""" req_id = self.gen_req_id() payload = { "cmd": "aibot_subscribe", @@ -154,7 +154,7 @@ class WecomAIBotLongConnectionClient: req_id: str, body: dict[str, Any] | None, ) -> bool: - """发送长连接命令。""" + """发送长连接命令。""" headers = {"req_id": req_id} payload: dict[str, Any] = {"cmd": cmd, "headers": headers} if body is not None: @@ -177,7 +177,7 @@ class WecomAIBotLongConnectionClient: if errcode == 6000 and attempt < max_retries: backoff = min(0.2 * (2**attempt), 2.0) logger.warning( - "[WecomAI][LongConn] 命令冲突(errcode=6000),将重试。cmd=%s req_id=%s attempt=%d", + "[WecomAI][LongConn] 命令冲突(errcode=6000),将重试。cmd=%s req_id=%s attempt=%d", cmd, req_id, attempt + 1, diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py index efa94b58e..10fea4dfb 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py @@ -1,5 +1,5 @@ """企业微信智能机器人队列管理器 -参考 webchat_queue_mgr.py,为企业微信智能机器人实现队列机制 +参考 webchat_queue_mgr.py,为企业微信智能机器人实现队列机制 支持异步消息处理和流式响应 """ @@ -22,9 +22,9 @@ class WecomAIQueueMgr: """StreamID 到输出队列的映射 - 用于发送机器人响应""" self.pending_responses: dict[str, dict[str, Any]] = {} - """待处理的响应缓存,用于流式响应""" + """待处理的响应缓存,用于流式响应""" self.completed_streams: dict[str, float] = {} - """已结束的 stream 缓存,用于兼容平台后续重复轮询""" + """已结束的 stream 缓存,用于兼容平台后续重复轮询""" self._queue_close_events: dict[str, asyncio.Event] = {} self._listener_tasks: dict[str, asyncio.Task] = {} self._listener_callback: Callable[[dict], Awaitable[None]] | None = None @@ -131,7 +131,7 @@ class WecomAIQueueMgr: Args: session_id: 会话ID - callback_params: 回调参数(nonce, timestamp等) + callback_params: 回调参数(nonce, timestamp等) """ self.pending_responses[session_id] = { @@ -147,7 +147,7 @@ class WecomAIQueueMgr: session_id: 会话ID Returns: - 响应参数,如果不存在则返回None + 响应参数,如果不存在则返回None """ return self.pending_responses.get(session_id) @@ -170,7 +170,7 @@ class WecomAIQueueMgr: """清理过期的待处理响应 Args: - max_age_seconds: 最大存活时间(秒) + max_age_seconds: 最大存活时间(秒) """ current_time = time.monotonic() diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py index 80ec5179e..efd82aa85 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py @@ -63,7 +63,7 @@ class WecomAIBotServer: return await self.handle_verify(quart.request) async def handle_verify(self, request): - """处理 URL 验证请求,可被统一 webhook 入口复用 + """处理 URL 验证请求,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -87,7 +87,7 @@ class WecomAIBotServer: assert nonce is not None assert echostr is not None - logger.info("收到企业微信智能机器人 WebHook URL 验证请求。") + logger.info("收到企业微信智能机器人 WebHook URL 验证请求。") result = self.api_client.verify_url(msg_signature, timestamp, nonce, echostr) return result, 200, {"Content-Type": "text/plain"} @@ -96,7 +96,7 @@ class WecomAIBotServer: return await self.handle_callback(quart.request) async def handle_callback(self, request): - """处理消息回调,可被统一 webhook 入口复用 + """处理消息回调,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -119,7 +119,7 @@ class WecomAIBotServer: assert nonce is not None logger.debug( - f"收到消息回调,msg_signature={msg_signature}, timestamp={timestamp}, nonce={nonce}", + f"收到消息回调,msg_signature={msg_signature}, timestamp={timestamp}, nonce={nonce}", ) try: @@ -139,7 +139,7 @@ class WecomAIBotServer: ) if ret_code != WecomAIBotConstants.SUCCESS or not message_data: - logger.error("消息解密失败,错误码: %d", ret_code) + logger.error("消息解密失败,错误码: %d", ret_code) return "消息解密失败", 400 # 调用消息处理器 @@ -164,7 +164,7 @@ class WecomAIBotServer: async def start_server(self) -> None: """启动服务器""" - logger.info("启动企业微信智能机器人服务器,监听 %s:%d", self.host, self.port) + logger.info("启动企业微信智能机器人服务器,监听 %s:%d", self.host, self.port) try: await self.app.run_task( diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py index f7cbe380d..decf15e0a 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_utils.py @@ -1,5 +1,5 @@ """企业微信智能机器人工具模块 -提供常量定义、工具函数和辅助方法 +提供常量定义、工具函数和辅助方法 """ import asyncio @@ -46,7 +46,7 @@ def generate_random_string(length: int = 10) -> str: """生成随机字符串 Args: - length: 字符串长度,默认为 10 + length: 字符串长度,默认为 10 Returns: 随机字符串 @@ -63,7 +63,7 @@ def calculate_image_md5(image_data: bytes) -> str: image_data: 图片二进制数据 Returns: - MD5 哈希值(十六进制字符串) + MD5 哈希值(十六进制字符串) """ return hashlib.md5(image_data).hexdigest() @@ -162,7 +162,7 @@ async def process_encrypted_image( aes_key_base64: Base64编码的AES密钥(与回调加解密相同) Returns: - Tuple[bool, str]: status 为 True 时 data 是解密后的图片数据的 base64 编码, + Tuple[bool, str]: status 为 True 时 data 是解密后的图片数据的 base64 编码, status 为 False 时 data 是错误信息 """ @@ -173,7 +173,7 @@ async def process_encrypted_image( async with session.get(image_url, timeout=15) as response: response.raise_for_status() encrypted_data = await response.read() - logger.info("图片下载成功,大小: %d 字节", len(encrypted_data)) + logger.info("图片下载成功,大小: %d 字节", len(encrypted_data)) except (aiohttp.ClientError, asyncio.TimeoutError) as e: error_msg = f"下载图片失败: {e!s}" logger.error(error_msg) @@ -200,10 +200,10 @@ async def process_encrypted_image( raise ValueError("无效的填充长度 (大于32字节)") decrypted_data = decrypted_data[:-pad_len] - logger.info("图片解密成功,解密后大小: %d 字节", len(decrypted_data)) + logger.info("图片解密成功,解密后大小: %d 字节", len(decrypted_data)) # 5. 转换为base64编码 base64_data = base64.b64encode(decrypted_data).decode("utf-8") - logger.info("图片已转换为base64编码,编码后长度: %d", len(base64_data)) + logger.info("图片已转换为base64编码,编码后长度: %d", len(base64_data)) return True, base64_data diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py index ce5b90968..d43bfceba 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py @@ -1,4 +1,4 @@ -"""企业微信智能机器人 webhook 推送客户端。""" +"""企业微信智能机器人 webhook 推送客户端。""" from __future__ import annotations @@ -19,11 +19,11 @@ from astrbot.core.utils.media_utils import convert_audio_format class WecomAIBotWebhookError(RuntimeError): - """企业微信 webhook 推送异常。""" + """企业微信 webhook 推送异常。""" class WecomAIBotWebhookClient: - """企业微信智能机器人 webhook 消息推送客户端。""" + """企业微信智能机器人 webhook 消息推送客户端。""" def __init__(self, webhook_url: str, timeout_seconds: int = 15) -> None: self.webhook_url = webhook_url.strip() @@ -191,7 +191,7 @@ class WecomAIBotWebhookClient: await flush_markdown_buffer(markdown_buffer) file_path = await component.get_file() if not file_path: - logger.warning("文件消息缺少有效文件路径,已跳过: %s", component) + logger.warning("文件消息缺少有效文件路径,已跳过: %s", component) continue await self.send_file(Path(file_path)) elif isinstance(component, Video): @@ -220,7 +220,7 @@ class WecomAIBotWebhookClient: ) else: logger.warning( - "企业微信消息推送暂不支持组件类型 %s,已跳过", + "企业微信消息推送暂不支持组件类型 %s,已跳过", type(component).__name__, ) diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py index d31c76b9e..87dc92f93 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_adapter.py @@ -73,14 +73,14 @@ class WeixinOfficialAccountServer: self._wx_msg_time_out = 4.0 # 微信服务器要求 5 秒内回复 self.user_buffer: dict[str, dict[str, Any]] = user_buffer # from_user -> state - self.active_send_mode = False # 是否启用主动发送模式,启用后 callback 将直接返回回复内容,无需等待微信回调 + self.active_send_mode = False # 是否启用主动发送模式,启用后 callback 将直接返回回复内容,无需等待微信回调 async def verify(self): """内部服务器的 GET 验证入口""" return await self.handle_verify(quart.request) async def handle_verify(self, request) -> str: - """处理验证请求,可被统一 webhook 入口复用 + """处理验证请求,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -92,7 +92,7 @@ class WeixinOfficialAccountServer: args = request.args if not args.get("signature", None): - logger.error("未知的响应,请检查回调地址是否填写正确。") + logger.error("未知的响应,请检查回调地址是否填写正确。") return "err" try: check_signature( @@ -101,10 +101,10 @@ class WeixinOfficialAccountServer: args.get("timestamp"), args.get("nonce"), ) - logger.info("验证请求有效性成功。") + logger.info("验证请求有效性成功。") return args.get("echostr", "empty") except InvalidSignatureException: - logger.error("验证请求有效性失败,签名异常,请检查配置。") + logger.error("验证请求有效性失败,签名异常,请检查配置。") return "err" async def callback_command(self): @@ -117,7 +117,7 @@ class WeixinOfficialAccountServer: return xml or "success" def _preview(self, msg: BaseMessage, limit: int = 24) -> str: - """生成消息预览文本,供占位符使用""" + """生成消息预览文本,供占位符使用""" if isinstance(msg, TextMessage): t = cast(str, msg.content).strip() return (t[:limit] + "...") if len(t) > limit else (t or "空消息") @@ -128,7 +128,7 @@ class WeixinOfficialAccountServer: return getattr(msg, "type", "未知消息") async def handle_callback(self, request) -> str: - """处理回调请求,可被统一 webhook 入口复用 + """处理回调请求,可被统一 webhook 入口复用 Args: request: Quart 请求对象 @@ -143,12 +143,12 @@ class WeixinOfficialAccountServer: try: xml = self.crypto.decrypt_message(data, msg_signature, timestamp, nonce) except InvalidSignatureException: - logger.error("解密失败,签名异常,请检查配置。") + logger.error("解密失败,签名异常,请检查配置。") raise else: msg = parse_message(xml) if not msg: - logger.error("解析失败。msg为None。") + logger.error("解析失败。msg为None。") raise logger.info(f"解析成功: {msg}") @@ -187,13 +187,13 @@ class WeixinOfficialAccountServer: else: return _reply_text( cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" + + "\n【后续消息还在缓冲中,回复任意文字继续获取】" ) task: asyncio.Task | None = cast(asyncio.Task | None, state.get("task")) placeholder = ( - f"【正在思考'{state.get('preview', '...')}'中,已思考" - f"{int(time.monotonic() - state.get('started_at', time.monotonic()))}s,回复任意文字尝试获取回复】" + f"【正在思考'{state.get('preview', '...')}'中,已思考" + f"{int(time.monotonic() - state.get('started_at', time.monotonic()))}s,回复任意文字尝试获取回复】" ) # same msgid => WeChat retry: wait a little; new msgid => user trigger: just placeholder @@ -224,7 +224,7 @@ class WeixinOfficialAccountServer: ) return _reply_text( cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" + + "\n【后续消息还在缓冲中,回复任意文字继续获取】" ) logger.info( f"wx finished in window but not final; return placeholder: user={from_user} msg_id={msg_id} " @@ -235,7 +235,7 @@ class WeixinOfficialAccountServer: "wx task failed in passive window", exc_info=True ) self.user_buffer.pop(from_user, None) - return _reply_text("处理消息失败,请稍后再试。") + return _reply_text("处理消息失败,请稍后再试。") logger.info( f"wx passive window timeout: user={from_user} msg_id={msg_id}" @@ -249,7 +249,7 @@ class WeixinOfficialAccountServer: logger.debug(f"wx new trigger: user={from_user} msg_id={msg_id}") preview = self._preview(msg) placeholder = ( - f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" + f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" ) logger.info( f"wx start task: user={from_user} msg_id={msg_id} preview={preview}" @@ -285,7 +285,7 @@ class WeixinOfficialAccountServer: else: return _reply_text( cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" + + "\n【后续消息还在缓冲中,回复任意文字继续获取】" ) logger.info( f"wx not finished in first window; return placeholder: user={from_user} msg_id={msg_id} " @@ -294,14 +294,14 @@ class WeixinOfficialAccountServer: except Exception: logger.critical("wx task failed in first window", exc_info=True) self.user_buffer.pop(from_user, None) - return _reply_text("处理消息失败,请稍后再试。") + return _reply_text("处理消息失败,请稍后再试。") logger.info(f"wx first window timeout: user={from_user} msg_id={msg_id}") return _reply_text(placeholder) async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。", + f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。", ) await self.server.run_task( host=self.callback_server_host, @@ -355,7 +355,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.client.__setattr__("API_BASE_URL", self.api_base_url) - # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重 + # 微信公众号必须 5 秒内进行回复,否则会重试 3 次,我们需要对其进行消息排重 # msgid -> Future self.wexin_event_workers: dict[str, asyncio.Future] = {} @@ -382,7 +382,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): return result except asyncio.TimeoutError: logger.info(f"callback 处理消息超时: message_id={msg.id}") - return create_reply("处理消息超时,请稍后再试。", msg) + return create_reply("处理消息超时,请稍后再试。", msg) except Exception as e: logger.error(f"转换消息时出现异常: {e}") finally: @@ -411,11 +411,11 @@ class WeixinOfficialAccountPlatformAdapter(Platform): @override async def run(self) -> None: - # 如果启用统一 webhook 模式,则不启动独立服务器 + # 如果启用统一 webhook 模式,则不启动独立服务器 webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info(f"{self.meta().id}(微信公众平台)", webhook_uuid) - # 保持运行状态,等待 shutdown + # 保持运行状态,等待 shutdown await self.server.shutdown_event.wait() else: await self.server.start_polling() @@ -480,7 +480,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): path_wav = await convert_audio_to_wav(path, path_wav) except Exception as e: logger.error( - f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。", + f"转换音频失败: {e}。如果没有安装 ffmpeg 请先安装。", ) path_wav = path return @@ -514,7 +514,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): buffer = self.user_buffer.get(message.sender.user_id, None) if buffer is None: logger.critical( - f"用户消息未找到缓冲状态,无法处理消息: user={message.sender.user_id} message_id={message.message_id}" + f"用户消息未找到缓冲状态,无法处理消息: user={message.sender.user_id} message_id={message.message_id}" ) return message_event = WeixinOfficialAccountPlatformEvent( diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py index 7387ca172..c12ddada0 100644 --- a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py +++ b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py @@ -59,15 +59,15 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): cut_position = end for i in range(end, start, -1): if i < len(plain) and plain[i - 1] in [ - "。", - "!", - "?", + "。", + "!", + "?", ".", "!", "?", "\n", ";", - ";", + ";", ]: cut_position = i break @@ -173,7 +173,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): logger.warning(f"删除临时音频文件失败: {e}") else: - logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") + logger.warning(f"还没实现这个消息类型的发送逻辑: {comp.type}。") await super().send(message) diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 30209de39..3de6a9e54 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -95,12 +95,12 @@ class ProviderRequest: image_urls: list[str] = field(default_factory=list) """图片 URL 列表""" extra_user_content_parts: list[ContentPart] = field(default_factory=list) - """额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。支持 dict 或 ContentPart 对象""" + """额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。支持 dict 或 ContentPart 对象""" func_tool: ToolSet | None = None """可用的函数工具""" contexts: list[dict] = field(default_factory=list) """ - OpenAI 格式上下文列表。 + OpenAI 格式上下文列表。 参考 https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages """ system_prompt: str = "" @@ -108,9 +108,9 @@ class ProviderRequest: conversation: Conversation | None = None """关联的对话对象""" tool_calls_result: list[ToolCallsResult] | ToolCallsResult | None = None - """附加的上次请求后工具调用的结果。参考: https://platform.openai.com/docs/guides/function-calling#handling-function-calls""" + """附加的上次请求后工具调用的结果。参考: https://platform.openai.com/docs/guides/function-calling#handling-function-calls""" model: str | None = None - """模型名称,为 None 时使用提供商的默认模型""" + """模型名称,为 None 时使用提供商的默认模型""" def __repr__(self) -> str: return ( @@ -134,7 +134,7 @@ class ProviderRequest: self.tool_calls_result.append(tool_calls_result) def _print_friendly_context(self): - """打印友好的消息上下文。将 image_url 的值替换为 """ + """打印友好的消息上下文。将 image_url 的值替换为 """ if not self.contexts: return f"prompt: {self.prompt}, image_count: {len(self.image_urls or [])}" @@ -169,18 +169,18 @@ class ProviderRequest: return "\n".join(result_parts) async def assemble_context(self) -> dict: - """将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。""" + """将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。""" # 构建内容块列表 content_blocks = [] - # 1. 用户原始发言(OpenAI 建议:用户发言在前) + # 1. 用户原始发言(OpenAI 建议:用户发言在前) if self.prompt and self.prompt.strip(): content_blocks.append({"type": "text", "text": self.prompt}) elif self.image_urls: - # 如果没有文本但有图片,添加占位文本 + # 如果没有文本但有图片,添加占位文本 content_blocks.append({"type": "text", "text": "[图片]"}) - # 2. 额外的内容块(系统提醒、指令等) + # 2. 额外的内容块(系统提醒、指令等) if self.extra_user_content_parts: for part in self.extra_user_content_parts: content_blocks.append(part.model_dump()) @@ -197,13 +197,13 @@ class ProviderRequest: else: image_data = await self._encode_image_bs64(image_url) if not image_data: - logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") + logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") continue content_blocks.append( {"type": "image_url", "image_url": {"url": image_data}}, ) - # 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容 + # 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容 if ( len(content_blocks) == 1 and content_blocks[0]["type"] == "text" @@ -315,7 +315,7 @@ class LLMResponse: Args: role (str): 角色, assistant, tool, err - completion_text (str, optional): 返回的结果文本,已经过时,推荐使用 result_chain. Defaults to "". + completion_text (str, optional): 返回的结果文本,已经过时,推荐使用 result_chain. Defaults to "". result_chain (MessageChain, optional): 返回的消息链. Defaults to None. tools_call_args (List[Dict[str, any]], optional): 工具调用参数. Defaults to None. tools_call_name (List[str], optional): 工具调用名称. Defaults to None. diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index bbd834748..7290f055d 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -48,7 +48,7 @@ class MCPShutdownTimeoutError(asyncio.TimeoutError): def __init__(self, names: list[str], timeout: float) -> None: self.names = names self.timeout = timeout - message = f"MCP 服务关闭超时({timeout:g} 秒):{', '.join(names)}" + message = f"MCP 服务关闭超时({timeout:g} 秒):{', '.join(names)}" super().__init__(message) @@ -100,20 +100,20 @@ def _resolve_timeout( timeout_value = float(timeout) except (TypeError, ValueError): logger.warning( - f"超时配置({source})={timeout!r} 无效,使用默认值 {default:g} 秒。" + f"超时配置({source})={timeout!r} 无效,使用默认值 {default:g} 秒。" ) return default if timeout_value <= 0: logger.warning( - f"超时配置({source})={timeout_value:g} 必须大于 0,使用默认值 {default:g} 秒。" + f"超时配置({source})={timeout_value:g} 必须大于 0,使用默认值 {default:g} 秒。" ) return default if timeout_value > MAX_MCP_TIMEOUT_SECONDS: logger.warning( - f"超时配置({source})={timeout_value:g} 过大,已限制为最大值 " - f"{MAX_MCP_TIMEOUT_SECONDS:g} 秒,以避免长时间等待。" + f"超时配置({source})={timeout_value:g} 过大,已限制为最大值 " + f"{MAX_MCP_TIMEOUT_SECONDS:g} 秒,以避免长时间等待。" ) return MAX_MCP_TIMEOUT_SECONDS @@ -143,7 +143,7 @@ FuncTool = FunctionTool def _prepare_config(config: dict) -> dict: - """准备配置,处理嵌套格式""" + """准备配置,处理嵌套格式""" if config.get("mcpServers"): first_key = next(iter(config["mcpServers"])) config = config["mcpServers"][first_key] @@ -210,7 +210,7 @@ class FunctionToolManager: def __init__(self) -> None: self.func_list: list[FuncTool] = [] self._mcp_server_runtime: dict[str, _MCPServerRuntime] = {} - """MCP 服务运行时状态(唯一事实来源)""" + """MCP 服务运行时状态(唯一事实来源)""" self._mcp_server_runtime_view = MappingProxyType(self._mcp_server_runtime) self._mcp_client_dict_view = _MCPClientDictView(self._mcp_server_runtime) self._timeout_mismatch_warned = False @@ -288,7 +288,7 @@ class FunctionToolManager: """添加函数调用工具 @param name: 函数名 - @param func_args: 函数参数列表,格式为 [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...] + @param func_args: 函数参数列表,格式为 [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...] @param desc: 函数描述 @param func_obj: 处理函数 """ @@ -306,7 +306,7 @@ class FunctionToolManager: logger.info(f"添加函数调用工具: {name}") def remove_func(self, name: str) -> None: - """删除一个函数调用工具。""" + """删除一个函数调用工具。""" for i, f in enumerate(self.func_list): if f.name == name: self.func_list.pop(i) @@ -324,7 +324,7 @@ class FunctionToolManager: @staticmethod def _log_safe_mcp_debug_config(cfg: dict) -> None: - # 仅记录脱敏后的摘要,避免泄露 command/args/url 中的敏感信息 + # 仅记录脱敏后的摘要,避免泄露 command/args/url 中的敏感信息 if "command" in cfg: cmd = cfg["command"] executable = str(cmd[0] if isinstance(cmd, (list, tuple)) and cmd else cmd) @@ -350,7 +350,7 @@ class FunctionToolManager: async def init_mcp_clients( self, raise_on_all_failed: bool = False ) -> MCPInitSummary: - """从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下: + """从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下: ``` { "mcpServers": { @@ -369,8 +369,8 @@ class FunctionToolManager: ``` Timeout behavior: - - 初始化超时使用环境变量 ASTRBOT_MCP_INIT_TIMEOUT 或默认值。 - - 动态启用超时使用 ASTRBOT_MCP_ENABLE_TIMEOUT(独立于初始化超时)。 + - 初始化超时使用环境变量 ASTRBOT_MCP_INIT_TIMEOUT 或默认值。 + - 动态启用超时使用 ASTRBOT_MCP_ENABLE_TIMEOUT(独立于初始化超时)。 """ data_dir = get_astrbot_data_path() @@ -382,7 +382,7 @@ class FunctionToolManager: await f.write( json.dumps(DEFAULT_MCP_CONFIG, ensure_ascii=False, indent=4) ) - logger.info(f"未找到 MCP 服务配置文件,已创建默认配置文件 {mcp_json_file}") + logger.info(f"未找到 MCP 服务配置文件,已创建默认配置文件 {mcp_json_file}") return MCPInitSummary(total=0, success=0, failed=[]) async with aiofiles.open(mcp_json_file, encoding="utf-8") as f: @@ -575,7 +575,7 @@ class FunctionToolManager: async def _cleanup_mcp_client_safely( self, mcp_client: MCPClient, name: str ) -> None: - """安全清理单个 MCP 客户端,避免清理异常中断主流程。""" + """安全清理单个 MCP 客户端,避免清理异常中断主流程。""" try: await mcp_client.cleanup() except Exception as cleanup_exc: # only log here @@ -599,7 +599,7 @@ class FunctionToolManager: logger.debug(f"MCP server {name} list tools response: {tools_res}") tool_names = [tool.name for tool in tools_res.tools] - # 移除该MCP服务之前的工具(如有) + # 移除该MCP服务之前的工具(如有) self.func_list = [ f for f in self.func_list @@ -739,8 +739,8 @@ class FunctionToolManager: if self._timeout_mismatch_warned: return logger.info( - "检测到 MCP 初始化超时与动态启用超时配置不同:" - "初始化使用 %s 秒,动态启用使用 %s 秒。如需一致,请设置相同值。", + "检测到 MCP 初始化超时与动态启用超时配置不同:" + "初始化使用 %s 秒,动态启用使用 %s 秒。如需一致,请设置相同值。", f"{init_timeout:g}", f"{enable_timeout:g}", ) @@ -767,10 +767,10 @@ class FunctionToolManager: return toolset.google_schema() def deactivate_llm_tool(self, name: str) -> bool: - """停用一个已经注册的函数调用工具。 + """停用一个已经注册的函数调用工具。 Returns: - 如果没找到,会返回 False + 如果没找到,会返回 False """ func_tool = self.get_func(name) @@ -795,14 +795,14 @@ class FunctionToolManager: return True return False - # 因为不想解决循环引用,所以这里直接传入 star_map 先了... + # 因为不想解决循环引用,所以这里直接传入 star_map 先了... def activate_llm_tool(self, name: str, star_map: dict) -> bool: func_tool = self.get_func(name) if func_tool is not None: if func_tool.handler_module_path in star_map: if not star_map[func_tool.handler_module_path].activated: raise ValueError( - f"此函数调用工具所属的插件 {star_map[func_tool.handler_module_path].name} 已被禁用,请先在管理面板启用再激活此工具。", + f"此函数调用工具所属的插件 {star_map[func_tool.handler_module_path].name} 已被禁用,请先在管理面板启用再激活此工具。", ) func_tool.active = True @@ -832,7 +832,7 @@ class FunctionToolManager: def load_mcp_config(self): if not os.path.exists(self.mcp_config_path): - # 配置文件不存在,创建默认配置 + # 配置文件不存在,创建默认配置 os.makedirs(os.path.dirname(self.mcp_config_path), exist_ok=True) with open(self.mcp_config_path, "w", encoding="utf-8") as f: json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index bd7179892..874b1c4dc 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -51,7 +51,7 @@ class ProviderManager: self.provider_stt_settings: dict = config.get("provider_stt_settings", {}) self.provider_tts_settings: dict = config.get("provider_tts_settings", {}) - # 人格相关属性,v4.0.0 版本后被废弃,推荐使用 PersonaManager + # 人格相关属性,v4.0.0 版本后被废弃,推荐使用 PersonaManager self.default_persona_name = persona_mgr.default_persona self.provider_insts: list[Provider] = [] @@ -72,11 +72,11 @@ class ProviderManager: self.llm_tools = llm_tools self.curr_provider_inst: Provider | None = None - """默认的 Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" + """默认的 Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" self.curr_stt_provider_inst: STTProvider | None = None - """默认的 Speech To Text Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" + """默认的 Speech To Text Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" self.curr_tts_provider_inst: TTSProvider | None = None - """默认的 Text To Speech Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" + """默认的 Text To Speech Provider 实例。已弃用,请使用 get_using_provider() 方法获取当前使用的 Provider 实例。""" self.db_helper = db_helper self._provider_change_callback: ( Callable[[str, ProviderType, str | None], None] | None @@ -142,7 +142,7 @@ class ProviderManager: @property def selected_default_persona(self): - """动态获取最新的默认选中 persona。已弃用,请使用 context.persona_mgr.get_default_persona_v3()""" + """动态获取最新的默认选中 persona。已弃用,请使用 context.persona_mgr.get_default_persona_v3()""" return self.persona_mgr.selected_default_persona_v3 async def set_provider( @@ -151,18 +151,18 @@ class ProviderManager: provider_type: ProviderType, umo: str | None = None, ) -> None: - """设置提供商。 + """设置提供商。 Args: - provider_id (str): 提供商 ID。 - provider_type (ProviderType): 提供商类型。 - umo (str, optional): 用户会话 ID,用于提供商会话隔离。 + provider_id (str): 提供商 ID。 + provider_type (ProviderType): 提供商类型。 + umo (str, optional): 用户会话 ID,用于提供商会话隔离。 Version 4.0.0: 这个版本下已经默认隔离提供商 """ if provider_id not in self.inst_map: - raise ValueError(f"提供商 {provider_id} 不存在,无法设置。") + raise ValueError(f"提供商 {provider_id} 不存在,无法设置。") if umo: await sp.session_put( umo, @@ -218,14 +218,14 @@ class ProviderManager: def get_using_provider( self, provider_type: ProviderType, umo=None ) -> Providers | None: - """获取正在使用的提供商实例。 + """获取正在使用的提供商实例。 Args: - provider_type (ProviderType): 提供商类型。 - umo (str, optional): 用户会话 ID,用于提供商会话隔离。 + provider_type (ProviderType): 提供商类型。 + umo (str, optional): 用户会话 ID,用于提供商会话隔离。 Returns: - Provider: 正在使用的提供商实例。 + Provider: 正在使用的提供商实例。 """ provider = None @@ -270,7 +270,7 @@ class ProviderManager: if not provider and provider_id: logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" ) return provider @@ -352,10 +352,10 @@ class ProviderManager: """动态导入提供商适配器模块 Args: - type (str): 提供商请求类型。 + type (str): 提供商请求类型。 Raises: - ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。 + ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。 """ match type: case "openai_chat_completion": @@ -469,7 +469,7 @@ class ProviderManager: """获取 provider 配置和 provider_source 配置合并后的结果 Returns: - dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典 + dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典 """ pc = copy.deepcopy(provider_config) provider_source_id = pc.get("provider_source_id", "") @@ -481,9 +481,9 @@ class ProviderManager: break if provider_source: - # 合并配置,provider 的配置优先级更高 + # 合并配置,provider 的配置优先级更高 merged_config = {**provider_source, **pc} - # 保持 id 为 provider 的 id,而不是 source 的 id + # 保持 id 为 provider 的 id,而不是 source 的 id merged_config["id"] = pc["id"] pc = merged_config return pc @@ -503,7 +503,7 @@ class ProviderManager: if env_val is None: provider_id = provider_config.get("id") logger.warning( - f"Provider {provider_id} 配置项 key[{idx}] 使用环境变量 {env_key} 但未设置。", + f"Provider {provider_id} 配置项 key[{idx}] 使用环境变量 {env_key} 但未设置。", ) resolved_keys.append("") else: @@ -516,7 +516,7 @@ class ProviderManager: return provider_config async def load_provider(self, provider_config: dict) -> None: - # 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并 + # 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并 provider_config = self.get_merged_provider_config(provider_config) if provider_config.get("provider_type", "") == "chat_completion": @@ -537,20 +537,20 @@ class ProviderManager: self.dynamic_import_provider(provider_config["type"]) except (ImportError, ModuleNotFoundError) as e: logger.critical( - f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。", + f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。", exc_info=True, ) return except Exception as e: logger.critical( - f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。未知原因", + f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。未知原因", exc_info=True, ) return if provider_config["type"] not in provider_cls_map: logger.error( - f"未找到适用于 {provider_config['type']}({provider_config['id']}) 的提供商适配器,请检查是否已经安装或者名称填写错误。已跳过。", + f"未找到适用于 {provider_config['type']}({provider_config['id']}) 的提供商适配器,请检查是否已经安装或者名称填写错误。已跳过。", exc_info=True, ) return @@ -584,7 +584,7 @@ class ProviderManager: ): self.curr_stt_provider_inst = inst logger.info( - f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。", + f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前语音转文本提供商适配器。", ) if not self.curr_stt_provider_inst: self.curr_stt_provider_inst = inst @@ -607,7 +607,7 @@ class ProviderManager: ): self.curr_tts_provider_inst = inst logger.info( - f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。", + f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前文本转语音提供商适配器。", ) if not self.curr_tts_provider_inst: self.curr_tts_provider_inst = inst @@ -633,7 +633,7 @@ class ProviderManager: ): self.curr_provider_inst = inst logger.info( - f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。", + f"已选择 {provider_config['type']}({provider_config['id']}) 作为当前提供商适配器。", ) if not self.curr_provider_inst: self.curr_provider_inst = inst @@ -657,19 +657,19 @@ class ProviderManager: await inst.initialize() self.rerank_provider_insts.append(inst) case _: - # 未知供应商抛出异常,确保inst初始化 + # 未知供应商抛出异常,确保inst初始化 # Should be unreachable raise Exception( - f"未知的提供商类型:{provider_metadata.provider_type}" + f"未知的提供商类型:{provider_metadata.provider_type}" ) self.inst_map[provider_config["id"]] = inst except Exception as e: logger.error( - f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}", + f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}", ) raise Exception( - f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}", + f"实例化 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}", ) async def reload(self, provider_config: dict) -> None: @@ -692,7 +692,7 @@ class ProviderManager: elif self.curr_provider_inst is None and len(self.provider_insts) > 0: self.curr_provider_inst = self.provider_insts[0] logger.info( - f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。", + f"自动选择 {self.curr_provider_inst.meta().id} 作为当前提供商适配器。", ) if len(self.stt_provider_insts) == 0: @@ -702,7 +702,7 @@ class ProviderManager: ): self.curr_stt_provider_inst = self.stt_provider_insts[0] logger.info( - f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。", + f"自动选择 {self.curr_stt_provider_inst.meta().id} 作为当前语音转文本提供商适配器。", ) if len(self.tts_provider_insts) == 0: @@ -712,7 +712,7 @@ class ProviderManager: ): self.curr_tts_provider_inst = self.tts_provider_insts[0] logger.info( - f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。", + f"自动选择 {self.curr_tts_provider_inst.meta().id} 作为当前文本转语音提供商适配器。", ) def get_insts(self): @@ -773,7 +773,7 @@ class ProviderManager: prov for prov in config["provider"] if prov.get("id") != tpid ] config.save_config() - logger.info(f"Provider {target_prov_ids} 已从配置中删除。") + logger.info(f"Provider {target_prov_ids} 已从配置中删除。") async def update_provider(self, origin_provider_id: str, new_config: dict) -> None: """Update provider config and reload the instance. Config will be saved after update.""" diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index eb76ddf86..2ef5a55f6 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -109,21 +109,21 @@ class Provider(AbstractProvider): extra_user_content_parts: list[ContentPart] | None = None, **kwargs, ) -> LLMResponse: - """获得 LLM 的文本对话结果。会使用当前的模型进行对话。 + """获得 LLM 的文本对话结果。会使用当前的模型进行对话。 Args: - prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中 + prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中 session_id: 会话 ID(此属性已经被废弃) image_urls: 图片 URL 列表 tools: tool set - contexts: 上下文,和 prompt 二选一使用 - tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling - extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等) + contexts: 上下文,和 prompt 二选一使用 + tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling + extra_user_content_parts: 额外的内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等) kwargs: 其他参数 Notes: - - 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。 - - 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。 + - 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。 + - 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。 """ ... @@ -140,20 +140,20 @@ class Provider(AbstractProvider): model: str | None = None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: - """获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。 + """获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。 Args: - prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中 + prompt: 提示词,和 contexts 二选一使用,如果都指定,则会将 prompt(以及可能的 image_urls) 作为最新的一条记录添加到 contexts 中 session_id: 会话 ID(此属性已经被废弃) image_urls: 图片 URL 列表 tools: tool set - contexts: 上下文,和 prompt 二选一使用 - tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling + contexts: 上下文,和 prompt 二选一使用 + tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling kwargs: 其他参数 Notes: - - 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。 - - 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。 + - 如果传入了 image_urls,将会在对话时附上图片。如果模型不支持图片输入,将会抛出错误。 + - 如果传入了 tools,将会使用 tools 进行 Function-calling。如果模型不支持 Function-calling,将会抛出错误。 """ if False: # pragma: no cover - make this an async generator for typing @@ -228,7 +228,7 @@ class TTSProvider(AbstractProvider): """是否支持流式 TTS Returns: - bool: True 表示支持流式处理,False 表示不支持(默认) + bool: True 表示支持流式处理,False 表示不支持(默认) Notes: 子类可以重写此方法返回 True 来启用流式 TTS 支持 @@ -237,7 +237,7 @@ class TTSProvider(AbstractProvider): @abc.abstractmethod async def get_audio(self, text: str) -> str: - """获取文本的音频,返回音频文件路径""" + """获取文本的音频,返回音频文件路径""" raise NotImplementedError async def get_audio_stream( @@ -245,14 +245,14 @@ class TTSProvider(AbstractProvider): text_queue: asyncio.Queue[str | None], audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]", ) -> None: - """流式 TTS 处理方法。 + """流式 TTS 处理方法。 - 从 text_queue 中读取文本片段,将生成的音频数据(WAV 格式的 in-memory bytes)放入 audio_queue。 - 当 text_queue 收到 None 时,表示文本输入结束,此时应该处理完所有剩余文本并向 audio_queue 发送 None 表示结束。 + 从 text_queue 中读取文本片段,将生成的音频数据(WAV 格式的 in-memory bytes)放入 audio_queue。 + 当 text_queue 收到 None 时,表示文本输入结束,此时应该处理完所有剩余文本并向 audio_queue 发送 None 表示结束。 Args: - text_queue: 输入文本队列,None 表示输入结束 - audio_queue: 输出音频队列(bytes 或 (text, bytes)),None 表示输出结束 + text_queue: 输入文本队列,None 表示输入结束 + audio_queue: 输出音频队列(bytes 或 (text, bytes)),None 表示输出结束 Notes: - 默认实现会将文本累积后一次性调用 get_audio 生成完整音频 @@ -265,7 +265,7 @@ class TTSProvider(AbstractProvider): text_part = await text_queue.get() if text_part is None: - # 输入结束,处理累积的文本 + # 输入结束,处理累积的文本 if accumulated_text: try: # 调用原有的 get_audio 方法获取音频文件路径 @@ -337,14 +337,14 @@ class EmbeddingProvider(AbstractProvider): max_retries: int = 3, progress_callback=None, ) -> list[list[float]]: - """批量获取文本的向量,分批处理以节省内存 + """批量获取文本的向量,分批处理以节省内存 Args: texts: 文本列表 batch_size: 每批处理的文本数量 tasks_limit: 并发任务数量限制 max_retries: 失败时的最大重试次数 - progress_callback: 进度回调函数,接收参数 (current, total) + progress_callback: 进度回调函数,接收参数 (current, total) Returns: 向量列表 @@ -369,12 +369,12 @@ class EmbeddingProvider(AbstractProvider): return except Exception as e: if attempt == max_retries - 1: - # 最后一次重试失败,记录失败的批次 + # 最后一次重试失败,记录失败的批次 failed_batches.append((batch_idx, batch_texts)) raise Exception( - f"批次 {batch_idx} 处理失败,已重试 {max_retries} 次: {e!s}", + f"批次 {batch_idx} 处理失败,已重试 {max_retries} 次: {e!s}", ) - # 等待一段时间后重试,使用指数退避 + # 等待一段时间后重试,使用指数退避 await asyncio.sleep(2**attempt) tasks = [] @@ -383,7 +383,7 @@ class EmbeddingProvider(AbstractProvider): batch_idx = i // batch_size tasks.append(process_batch(batch_idx, batch_texts)) - # 收集所有任务的结果,包括失败的任务 + # 收集所有任务的结果,包括失败的任务 results = await asyncio.gather(*tasks, return_exceptions=True) # 检查是否有失败的任务 diff --git a/astrbot/core/provider/register.py b/astrbot/core/provider/register.py index 3ad83784e..fee88fa69 100644 --- a/astrbot/core/provider/register.py +++ b/astrbot/core/provider/register.py @@ -23,7 +23,7 @@ def register_provider_adapter( def decorator(cls): if provider_type_name in provider_cls_map: raise ValueError( - f"检测到大模型提供商适配器 {provider_type_name} 已经注册,可能发生了大模型提供商适配器类型命名冲突。", + f"检测到大模型提供商适配器 {provider_type_name} 已经注册,可能发生了大模型提供商适配器类型命名冲突。", ) # 添加必备选项 diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index e6cbf16b4..5c34a8d8c 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -89,10 +89,10 @@ class ProviderAnthropic(Provider): """准备 Anthropic API 的请求 payload Args: - messages: OpenAI 格式的消息列表,包含用户输入和系统提示等信息 + messages: OpenAI 格式的消息列表,包含用户输入和系统提示等信息 Returns: system_prompt: 系统提示内容 - new_messages: 处理后的消息列表,去除系统提示 + new_messages: 处理后的消息列表,去除系统提示 """ system_prompt = "" @@ -255,7 +255,7 @@ class ProviderAnthropic(Provider): logger.debug(f"completion: {completion}") if len(completion.content) == 0: - raise Exception("API 返回的 completion 为空。") + raise Exception("API 返回的 completion 为空。") llm_response = LLMResponse(role="assistant") @@ -342,7 +342,7 @@ class ProviderAnthropic(Provider): id=id, ) elif event.content_block.type == "tool_use": - # 工具使用块开始,初始化缓冲区 + # 工具使用块开始,初始化缓冲区 tool_use_buffer[event.index] = { "id": event.content_block.id, "name": event.content_block.name, @@ -414,7 +414,7 @@ class ProviderAnthropic(Provider): id=id, ) except json.JSONDecodeError: - # JSON 解析失败,跳过这个工具调用 + # JSON 解析失败,跳过这个工具调用 logger.warning(f"工具调用参数 JSON 解析失败: {tool_info}") # 清理缓冲区 @@ -570,7 +570,7 @@ class ProviderAnthropic(Provider): image_urls: list[str] | None = None, extra_user_content_parts: list[ContentPart] | None = None, ): - """组装上下文,支持文本和图片""" + """组装上下文,支持文本和图片""" async def resolve_image_url(image_url: str) -> dict | None: if image_url.startswith("http"): @@ -583,7 +583,7 @@ class ProviderAnthropic(Provider): image_data, mime_type = await self.encode_image_bs64(image_url) if not image_data: - logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") + logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") return None return { @@ -601,17 +601,17 @@ class ProviderAnthropic(Provider): content = [] - # 1. 用户原始发言(OpenAI 建议:用户发言在前) + # 1. 用户原始发言(OpenAI 建议:用户发言在前) if text: content.append({"type": "text", "text": text}) elif image_urls: - # 如果没有文本但有图片,添加占位文本 + # 如果没有文本但有图片,添加占位文本 content.append({"type": "text", "text": "[图片]"}) elif extra_user_content_parts: - # 如果只有额外内容块,也需要添加占位文本 + # 如果只有额外内容块,也需要添加占位文本 content.append({"type": "text", "text": " "}) - # 2. 额外的内容块(系统提醒、指令等) + # 2. 额外的内容块(系统提醒、指令等) if extra_user_content_parts: for block in extra_user_content_parts: if isinstance(block, TextPart): @@ -630,7 +630,7 @@ class ProviderAnthropic(Provider): if image_dict: content.append(image_dict) - # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 + # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 if ( text and not extra_user_content_parts @@ -644,7 +644,7 @@ class ProviderAnthropic(Provider): return {"role": "user", "content": content} async def encode_image_bs64(self, image_url: str) -> tuple[str, str]: - """将图片转换为 base64,同时检测实际 MIME 类型""" + """将图片转换为 base64,同时检测实际 MIME 类型""" if image_url.startswith("base64://"): raw_base64 = image_url.replace("base64://", "") try: diff --git a/astrbot/core/provider/sources/azure_tts_source.py b/astrbot/core/provider/sources/azure_tts_source.py index fc2bb6c09..15d4e9aa1 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -220,7 +220,7 @@ class AzureTTSProvider(TTSProvider): try: match = re.match(r"other\[(.*)\]", key_value, re.DOTALL) if not match: - raise ValueError("无效的other[...]格式,应形如 other[{...}]") + raise ValueError("无效的other[...]格式,应形如 other[{...}]") json_str = match.group(1).strip() otts_config = json.loads(json_str) required = {"OTTS_SKEY", "OTTS_URL", "OTTS_AUTH_TIME"} @@ -229,7 +229,7 @@ class AzureTTSProvider(TTSProvider): return OTTSProvider(otts_config) except json.JSONDecodeError as e: error_msg = ( - f"JSON解析失败,请检查格式(错误位置:行 {e.lineno} 列 {e.colno})\n" + f"JSON解析失败,请检查格式(错误位置:行 {e.lineno} 列 {e.colno})\n" f"错误详情: {e.msg}\n" f"错误上下文: {json_str[max(0, e.pos - 30) : e.pos + 30]}" ) @@ -238,7 +238,7 @@ class AzureTTSProvider(TTSProvider): raise ValueError(f"配置错误: 缺少必要参数 {e}") from e if re.fullmatch(AZURE_TTS_SUBSCRIPTION_KEY_PATTERN, key_value): return AzureNativeProvider(config, self.provider_settings) - raise ValueError("订阅密钥格式无效,应为32位或84位字母数字或other[...]格式") + raise ValueError("订阅密钥格式无效,应为32位或84位字母数字或other[...]格式") async def get_audio(self, text: str) -> str: if isinstance(self.provider, OTTSProvider): diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py index 9e079d4a9..f6ec040c4 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -43,7 +43,7 @@ class BailianRerankProvider(RerankProvider): "DASHSCOPE_API_KEY", "" ) if not self.api_key: - raise ValueError("阿里云百炼 API Key 不能为空。") + raise ValueError("阿里云百炼 API Key 不能为空。") self.model = provider_config.get("rerank_model", "qwen3-rerank") self.timeout = provider_config.get("timeout", 30) @@ -68,7 +68,7 @@ class BailianRerankProvider(RerankProvider): # 设置模型名称 self.set_model(self.model) - logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}") + logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}") def _build_payload( self, query: str, documents: list[str], top_n: int | None @@ -78,7 +78,7 @@ class BailianRerankProvider(RerankProvider): Args: query: 查询文本 documents: 文档列表 - top_n: 返回前N个结果,如果为None则返回所有结果 + top_n: 返回前N个结果,如果为None则返回所有结果 Returns: 请求载荷字典 @@ -129,7 +129,7 @@ class BailianRerankProvider(RerankProvider): logger.warning(f"百炼 Rerank 返回空结果: {data}") return [] - # 转换为RerankResult对象,使用.get()避免KeyError + # 转换为RerankResult对象,使用.get()避免KeyError rerank_results = [] for idx, result in enumerate(results): try: @@ -137,7 +137,7 @@ class BailianRerankProvider(RerankProvider): relevance_score = result.get("relevance_score", 0.0) if relevance_score is None: - logger.warning(f"结果 {idx} 缺少 relevance_score,使用默认值 0.0") + logger.warning(f"结果 {idx} 缺少 relevance_score,使用默认值 0.0") relevance_score = 0.0 rerank_result = RerankResult( @@ -172,32 +172,32 @@ class BailianRerankProvider(RerankProvider): Args: query: 查询文本 documents: 待排序的文档列表 - top_n: 返回前N个结果,如果为None则使用配置中的默认值 + top_n: 返回前N个结果,如果为None则使用配置中的默认值 Returns: 重排序结果列表 """ if not self.client: - logger.error("百炼 Rerank 客户端会话已关闭,返回空结果") + logger.error("百炼 Rerank 客户端会话已关闭,返回空结果") return [] if not documents: - logger.warning("文档列表为空,返回空结果") + logger.warning("文档列表为空,返回空结果") return [] if not query.strip(): - logger.warning("查询文本为空,返回空结果") + logger.warning("查询文本为空,返回空结果") return [] # 检查限制 if len(documents) > 500: logger.warning( - f"文档数量({len(documents)})超过限制(500),将截断前500个文档" + f"文档数量({len(documents)})超过限制(500),将截断前500个文档" ) documents = documents[:500] try: - # 构建请求载荷,如果top_n为None则返回所有重排序结果 + # 构建请求载荷,如果top_n为None则返回所有重排序结果 payload = self._build_payload(query, documents, top_n) logger.debug( diff --git a/astrbot/core/provider/sources/edge_tts_source.py b/astrbot/core/provider/sources/edge_tts_source.py index afacdc80c..1ea8bde5c 100644 --- a/astrbot/core/provider/sources/edge_tts_source.py +++ b/astrbot/core/provider/sources/edge_tts_source.py @@ -14,11 +14,11 @@ from ..provider import TTSProvider from ..register import register_provider_adapter """ -edge_tts 方式,能够免费、快速生成语音,使用需要先安装edge-tts库 +edge_tts 方式,能够免费、快速生成语音,使用需要先安装edge-tts库 ``` pip install edge_tts ``` -Windows 如果提示找不到指定文件,以管理员身份运行命令行窗口,然后再次运行 AstrBot +Windows 如果提示找不到指定文件,以管理员身份运行命令行窗口,然后再次运行 AstrBot """ @@ -35,7 +35,7 @@ class ProviderEdgeTTS(TTSProvider): ) -> None: super().__init__(provider_config, provider_settings) - # 设置默认语音,如果没有指定则使用中文小萱 + # 设置默认语音,如果没有指定则使用中文小萱 self.voice = provider_config.get("edge-tts-voice", "zh-CN-XiaoxiaoNeural") self.rate = provider_config.get("rate") self.volume = provider_config.get("volume") diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py index 8451a4ded..02b9f1105 100644 --- a/astrbot/core/provider/sources/fishaudio_tts_api_source.py +++ b/astrbot/core/provider/sources/fishaudio_tts_api_source.py @@ -33,9 +33,9 @@ class ServeTTSRequest(BaseModel): # 例如 https://fish.audio/m/626bb6d3f3364c9cbc3aa6a67300a664/ # 其中reference_id为 626bb6d3f3364c9cbc3aa6a67300a664 reference_id: str | None = None - # 对中英文文本进行标准化,这可以提高数字的稳定性 + # 对中英文文本进行标准化,这可以提高数字的稳定性 normalize: bool = True - # 平衡模式将延迟减少到300毫秒,但可能会降低稳定性 + # 平衡模式将延迟减少到300毫秒,但可能会降低稳定性 latency: Literal["normal", "balanced"] = "normal" @@ -122,14 +122,14 @@ class ProviderFishAudioTTSAPI(TTSProvider): return bool(re.match(pattern, reference_id.strip())) async def _generate_request(self, text: str) -> ServeTTSRequest: - # 向前兼容逻辑:优先使用reference_id,如果没有则使用角色名称查询 + # 向前兼容逻辑:优先使用reference_id,如果没有则使用角色名称查询 if self.reference_id and self.reference_id.strip(): # 验证reference_id格式 if not self._validate_reference_id(self.reference_id): raise ValueError( f"无效的FishAudio参考模型ID: '{self.reference_id}'. " - f"请确保ID是32位十六进制字符串(例如: 626bb6d3f3364c9cbc3aa6a67300a664)。" - f"您可以从 https://fish.audio/zh-CN/discovery 获取有效的模型ID。", + f"请确保ID是32位十六进制字符串(例如: 626bb6d3f3364c9cbc3aa6a67300a664)。" + f"您可以从 https://fish.audio/zh-CN/discovery 获取有效的模型ID。", ) reference_id = self.reference_id.strip() else: diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index e7fbc989a..1607ab1fd 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -103,7 +103,7 @@ class ProviderGoogleGenAI(Provider): ] async def _handle_api_error(self, e: APIError, keys: list[str]) -> bool: - """处理API错误,返回是否需要重试""" + """处理API错误,返回是否需要重试""" if e.message is None: e.message = "" @@ -112,12 +112,12 @@ class ProviderGoogleGenAI(Provider): if len(keys) > 0: self.set_key(random.choice(keys)) logger.info( - f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...", + f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试... 当前 Key: {self.chosen_api_key[:12]}...", ) await asyncio.sleep(1) return True logger.error( - f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...", + f"检测到 Key 异常({e.message}),且已没有可用的 Key。 当前 Key: {self.chosen_api_key[:12]}...", ) raise Exception("达到了 Gemini 速率限制, 请稍后再试...") @@ -143,7 +143,7 @@ class ProviderGoogleGenAI(Provider): # 流式输出不支持图片模态 if streaming and "IMAGE" in modalities: - logger.warning("流式输出不支持图片模态,已自动降级为文本模态") + logger.warning("流式输出不支持图片模态,已自动降级为文本模态") modalities = ["TEXT"] tool_list: list[types.Tool] | None = [] @@ -156,10 +156,10 @@ class ProviderGoogleGenAI(Provider): if native_coderunner: tool_list.append(types.Tool(code_execution=types.ToolCodeExecution())) if native_search: - logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具") + logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具") if url_context: logger.warning( - "代码执行工具与URL上下文工具互斥,已忽略URL上下文工具", + "代码执行工具与URL上下文工具互斥,已忽略URL上下文工具", ) else: if native_search: @@ -170,13 +170,13 @@ class ProviderGoogleGenAI(Provider): tool_list.append(types.Tool(url_context=types.UrlContext())) else: logger.warning( - "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", + "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", ) elif "gemini-2.0-lite" in model_name: if native_coderunner or native_search or url_context: logger.warning( - "gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置", + "gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置", ) tool_list = None @@ -184,7 +184,7 @@ class ProviderGoogleGenAI(Provider): if native_coderunner: tool_list.append(types.Tool(code_execution=types.ToolCodeExecution())) if native_search: - logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具") + logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具") elif native_search: tool_list.append(types.Tool(google_search=types.GoogleSearch())) @@ -193,14 +193,14 @@ class ProviderGoogleGenAI(Provider): tool_list.append(types.Tool(url_context=types.UrlContext())) else: logger.warning( - "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", + "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", ) if not tool_list: tool_list = None if tools and tool_list: - logger.warning("已启用原生工具,函数工具将被忽略") + logger.warning("已启用原生工具,函数工具将被忽略") elif tools and (func_desc := tools.get_func_desc_google_genai_style()): tool_list = [ types.Tool(function_declarations=func_desc["function_declarations"]), @@ -284,7 +284,7 @@ class ProviderGoogleGenAI(Provider): def create_text_part(text: str) -> types.Part: content_a = text if text else " " if not text: - logger.warning("文本内容为空,已添加空格占位") + logger.warning("文本内容为空,已添加空格占位") return types.Part.from_text(text=content_a) def process_image_url(image_url_dict: dict) -> types.Part: @@ -381,10 +381,10 @@ class ProviderGoogleGenAI(Provider): parts.append(part) append_or_extend(gemini_contents, parts, types.ModelContent) else: - logger.warning("assistant 角色的消息内容为空,已添加空格占位") + logger.warning("assistant 角色的消息内容为空,已添加空格占位") if native_tool_enabled and "tool_calls" in message: logger.warning( - "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文", + "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文", ) parts = [types.Part.from_text(text=" ")] append_or_extend(gemini_contents, parts, types.ModelContent) @@ -437,7 +437,7 @@ class ProviderGoogleGenAI(Provider): """处理内容部分并构建消息链""" if not candidate.content: logger.warning(f"收到的 candidate.content 为空: {candidate}") - raise Exception("API 返回的 candidate.content 为空。") + raise Exception("API 返回的 candidate.content 为空。") finish_reason = candidate.finish_reason result_parts: list[types.Part] | None = candidate.content.parts @@ -459,7 +459,7 @@ class ProviderGoogleGenAI(Provider): if not result_parts: logger.warning(f"收到的 candidate.content.parts 为空: {candidate}") - raise Exception("API 返回的 candidate.content.parts 为空。") + raise Exception("API 返回的 candidate.content.parts 为空。") # 提取 reasoning content reasoning = self._extract_reasoning_content(candidate) @@ -548,14 +548,14 @@ class ProviderGoogleGenAI(Provider): if not result.candidates: logger.error(f"请求失败, 返回的 candidates 为空: {result}") - raise Exception("请求失败, 返回的 candidates 为空。") + raise Exception("请求失败, 返回的 candidates 为空。") if result.candidates[0].finish_reason == types.FinishReason.RECITATION: if temperature > 2: - raise Exception("温度参数已超过最大值2,仍然发生recitation") + raise Exception("温度参数已超过最大值2,仍然发生recitation") temperature += 0.2 logger.warning( - f"发生了recitation,正在提高温度至{temperature:.1f}重试...", + f"发生了recitation,正在提高温度至{temperature:.1f}重试...", ) continue @@ -566,11 +566,11 @@ class ProviderGoogleGenAI(Provider): e.message = "" if "Developer instruction is not enabled" in e.message: logger.warning( - f"{model} 不支持 system prompt,已自动去除(影响人格设置)", + f"{model} 不支持 system prompt,已自动去除(影响人格设置)", ) system_instruction = None elif "Function calling is not enabled" in e.message: - logger.warning(f"{model} 不支持函数调用,已自动去除") + logger.warning(f"{model} 不支持函数调用,已自动去除") tools = None elif ( "Multi-modal output is not supported" in e.message @@ -579,7 +579,7 @@ class ProviderGoogleGenAI(Provider): or "only supports text output" in e.message ): logger.warning( - f"{model} 不支持多模态输出,降级为文本模态", + f"{model} 不支持多模态输出,降级为文本模态", ) modalities = ["TEXT"] else: @@ -630,11 +630,11 @@ class ProviderGoogleGenAI(Provider): e.message = "" if "Developer instruction is not enabled" in e.message: logger.warning( - f"{model} 不支持 system prompt,已自动去除(影响人格设置)", + f"{model} 不支持 system prompt,已自动去除(影响人格设置)", ) system_instruction = None elif "Function calling is not enabled" in e.message: - logger.warning(f"{model} 不支持函数调用,已自动去除") + logger.warning(f"{model} 不支持函数调用,已自动去除") tools = None else: raise @@ -771,7 +771,7 @@ class ProviderGoogleGenAI(Provider): continue break - raise Exception("请求失败。") + raise Exception("请求失败。") async def text_chat_stream( self, @@ -857,7 +857,7 @@ class ProviderGoogleGenAI(Provider): image_urls: list[str] | None = None, extra_user_content_parts: list[ContentPart] | None = None, ): - """组装上下文。""" + """组装上下文。""" async def resolve_image_part(image_url: str) -> dict | None: if image_url.startswith("http"): @@ -869,7 +869,7 @@ class ProviderGoogleGenAI(Provider): else: image_data = await self.encode_image_bs64(image_url) if not image_data: - logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") + logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") return None return { "type": "image_url", @@ -879,17 +879,17 @@ class ProviderGoogleGenAI(Provider): # 构建内容块列表 content_blocks = [] - # 1. 用户原始发言(OpenAI 建议:用户发言在前) + # 1. 用户原始发言(OpenAI 建议:用户发言在前) if text: content_blocks.append({"type": "text", "text": text}) elif image_urls: - # 如果没有文本但有图片,添加占位文本 + # 如果没有文本但有图片,添加占位文本 content_blocks.append({"type": "text", "text": "[图片]"}) elif extra_user_content_parts: - # 如果只有额外内容块,也需要添加占位文本 + # 如果只有额外内容块,也需要添加占位文本 content_blocks.append({"type": "text", "text": " "}) - # 2. 额外的内容块(系统提醒、指令等) + # 2. 额外的内容块(系统提醒、指令等) if extra_user_content_parts: for part in extra_user_content_parts: if isinstance(part, TextPart): @@ -908,7 +908,7 @@ class ProviderGoogleGenAI(Provider): if image_part: content_blocks.append(image_part) - # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 + # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 if ( text and not extra_user_content_parts diff --git a/astrbot/core/provider/sources/gsv_selfhosted_source.py b/astrbot/core/provider/sources/gsv_selfhosted_source.py index be45c6cf7..74280d247 100644 --- a/astrbot/core/provider/sources/gsv_selfhosted_source.py +++ b/astrbot/core/provider/sources/gsv_selfhosted_source.py @@ -32,7 +32,7 @@ class ProviderGSVTTS(TTSProvider): self.gpt_weights_path: str = provider_config.get("gpt_weights_path", "") self.sovits_weights_path: str = provider_config.get("sovits_weights_path", "") - # TTS 请求的默认参数,移除前缀gsv_ + # TTS 请求的默认参数,移除前缀gsv_ self.default_params: dict = { key.removeprefix("gsv_"): str(value).lower() for key, value in provider_config.get("gsv_default_parms", {}).items() @@ -41,7 +41,7 @@ class ProviderGSVTTS(TTSProvider): self._session: aiohttp.ClientSession | None = None async def initialize(self) -> None: - """异步初始化:在 ProviderManager 中被调用""" + """异步初始化:在 ProviderManager 中被调用""" self._session = aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=self.timeout), ) @@ -49,7 +49,7 @@ class ProviderGSVTTS(TTSProvider): await self._set_model_weights() logger.info("[GSV TTS] 初始化完成") except Exception as e: - logger.error(f"[GSV TTS] 初始化失败:{e}") + logger.error(f"[GSV TTS] 初始化失败:{e}") raise def get_session(self) -> aiohttp.ClientSession: @@ -67,7 +67,7 @@ class ProviderGSVTTS(TTSProvider): ) -> bytes | None: """发起请求""" for attempt in range(retries): - logger.debug(f"[GSV TTS] 请求地址:{endpoint},参数:{params}") + logger.debug(f"[GSV TTS] 请求地址:{endpoint},参数:{params}") try: async with self.get_session().get(endpoint, params=params) as response: if response.status != 200: @@ -79,11 +79,11 @@ class ProviderGSVTTS(TTSProvider): except Exception as e: if attempt < retries - 1: logger.warning( - f"[GSV TTS] 请求 {endpoint} 第 {attempt + 1} 次失败:{e},重试中...", + f"[GSV TTS] 请求 {endpoint} 第 {attempt + 1} 次失败:{e},重试中...", ) await asyncio.sleep(1) else: - logger.error(f"[GSV TTS] 请求 {endpoint} 最终失败:{e}") + logger.error(f"[GSV TTS] 请求 {endpoint} 最终失败:{e}") raise async def _set_model_weights(self) -> None: @@ -94,9 +94,9 @@ class ProviderGSVTTS(TTSProvider): f"{self.api_base}/set_gpt_weights", {"weights_path": self.gpt_weights_path}, ) - logger.info(f"[GSV TTS] 成功设置 GPT 模型路径:{self.gpt_weights_path}") + logger.info(f"[GSV TTS] 成功设置 GPT 模型路径:{self.gpt_weights_path}") else: - logger.info("[GSV TTS] GPT 模型路径未配置,将使用内置 GPT 模型") + logger.info("[GSV TTS] GPT 模型路径未配置,将使用内置 GPT 模型") if self.sovits_weights_path: await self._make_request( @@ -104,17 +104,17 @@ class ProviderGSVTTS(TTSProvider): {"weights_path": self.sovits_weights_path}, ) logger.info( - f"[GSV TTS] 成功设置 SoVITS 模型路径:{self.sovits_weights_path}", + f"[GSV TTS] 成功设置 SoVITS 模型路径:{self.sovits_weights_path}", ) else: - logger.info("[GSV TTS] SoVITS 模型路径未配置,将使用内置 SoVITS 模型") + logger.info("[GSV TTS] SoVITS 模型路径未配置,将使用内置 SoVITS 模型") except aiohttp.ClientError as e: - logger.error(f"[GSV TTS] 设置模型路径时发生网络错误:{e}") + logger.error(f"[GSV TTS] 设置模型路径时发生网络错误:{e}") except Exception as e: - logger.error(f"[GSV TTS] 设置模型路径时发生未知错误:{e}") + logger.error(f"[GSV TTS] 设置模型路径时发生未知错误:{e}") async def get_audio(self, text: str) -> str: - """实现 TTS 核心方法,根据文本内容自动切换情绪""" + """实现 TTS 核心方法,根据文本内容自动切换情绪""" if not text.strip(): raise ValueError("[GSV TTS] TTS 文本不能为空") @@ -126,27 +126,27 @@ class ProviderGSVTTS(TTSProvider): os.makedirs(temp_dir, exist_ok=True) path = os.path.join(temp_dir, f"gsv_tts_{uuid.uuid4().hex}.wav") - logger.debug(f"[GSV TTS] 正在调用语音合成接口,参数:{params}") + logger.debug(f"[GSV TTS] 正在调用语音合成接口,参数:{params}") result = await self._make_request(endpoint, params) if isinstance(result, bytes): async with aiofiles.open(path, "wb") as f: await f.write(result) return path - raise Exception(f"[GSV TTS] 合成失败,输入文本:{text},错误信息:{result}") + raise Exception(f"[GSV TTS] 合成失败,输入文本:{text},错误信息:{result}") def build_synthesis_params(self, text: str) -> dict: - """构建语音合成所需的参数字典。 + """构建语音合成所需的参数字典。 - 当前仅包含默认参数 + 文本,未来可在此基础上动态添加如情绪、角色等语义控制字段。 + 当前仅包含默认参数 + 文本,未来可在此基础上动态添加如情绪、角色等语义控制字段。 """ params = self.default_params.copy() params["text"] = text - # TODO: 在此处添加情绪分析,例如 params["emotion"] = detect_emotion(text) + # TODO: 在此处添加情绪分析,例如 params["emotion"] = detect_emotion(text) return params async def terminate(self) -> None: - """终止释放资源:在 ProviderManager 中被调用""" + """终止释放资源:在 ProviderManager 中被调用""" if self._session and not self._session.closed: await self._session.close() logger.info("[GSV TTS] Session 已关闭") diff --git a/astrbot/core/provider/sources/gsvi_tts_source.py b/astrbot/core/provider/sources/gsvi_tts_source.py index 9737d7360..6ee9a93e3 100644 --- a/astrbot/core/provider/sources/gsvi_tts_source.py +++ b/astrbot/core/provider/sources/gsvi_tts_source.py @@ -54,7 +54,7 @@ class ProviderGSVITTS(TTSProvider): else: error_text = await response.text() raise Exception( - f"GSVI TTS API 请求失败,状态码: {response.status},错误: {error_text}", + f"GSVI TTS API 请求失败,状态码: {response.status},错误: {error_text}", ) return path diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 4429225dc..33d4beb7c 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -146,7 +146,7 @@ class ProviderOpenAIOfficial(Provider): image_fallback_used: bool = False, ) -> tuple: logger.warning( - "检测到图片请求失败(%s),已移除图片并重试(保留文本内容)。", + "检测到图片请求失败(%s),已移除图片并重试(保留文本内容)。", reason, ) new_contexts = await self._remove_image_from_context(context_query) @@ -220,7 +220,7 @@ class ProviderOpenAIOfficial(Provider): models_str.append(model.id) return models_str except NotFoundError as e: - raise Exception(f"获取模型列表失败:{e}") + raise Exception(f"获取模型列表失败:{e}") async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: if tools: @@ -257,7 +257,7 @@ class ProviderOpenAIOfficial(Provider): if not isinstance(completion, ChatCompletion): raise Exception( - f"API 返回的 completion 类型错误:{type(completion)}: {completion}。", + f"API 返回的 completion 类型错误:{type(completion)}: {completion}。", ) logger.debug(f"completion: {completion}") @@ -271,7 +271,7 @@ class ProviderOpenAIOfficial(Provider): payloads: dict, tools: ToolSet | None, ) -> AsyncGenerator[LLMResponse, None]: - """流式查询API,逐步返回结果""" + """流式查询API,逐步返回结果""" if tools: model = payloads.get("model", "").lower() omit_empty_param_field = "gemini" in model @@ -474,7 +474,7 @@ class ProviderOpenAIOfficial(Provider): llm_response = LLMResponse("assistant") if not completion.choices: - raise Exception("API 返回的 completion 为空。") + raise Exception("API 返回的 completion 为空。") choice = completion.choices[0] # parse the text completion @@ -537,11 +537,11 @@ class ProviderOpenAIOfficial(Provider): # specially handle finish reason if choice.finish_reason == "content_filter": raise Exception( - "API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。", + "API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。", ) if llm_response.completion_text is None and not llm_response.tools_call_args: - logger.error(f"API 返回的 completion 无法解析:{completion}。") - raise Exception(f"API 返回的 completion 无法解析:{completion}。") + logger.error(f"API 返回的 completion 无法解析:{completion}。") + raise Exception(f"API 返回的 completion 无法解析:{completion}。") llm_response.raw_completion = completion llm_response.id = completion.id @@ -629,7 +629,7 @@ class ProviderOpenAIOfficial(Provider): """处理API错误并尝试恢复""" if "429" in str(e): logger.warning( - f"API 调用过于频繁,尝试使用其他 Key 重试。当前 Key: {chosen_key[:12]}", + f"API 调用过于频繁,尝试使用其他 Key 重试。当前 Key: {chosen_key[:12]}", ) # 最后一次不等待 if retry_cnt < max_retries - 1: @@ -650,7 +650,7 @@ class ProviderOpenAIOfficial(Provider): raise e if "maximum context length" in str(e): logger.warning( - f"上下文长度超过限制。尝试弹出最早的记录然后重试。当前记录条数: {len(context_query)}", + f"上下文长度超过限制。尝试弹出最早的记录然后重试。当前记录条数: {len(context_query)}", ) await self.pop_record(context_query) payloads["messages"] = context_query @@ -694,9 +694,9 @@ class ProviderOpenAIOfficial(Provider): or ("tool" in str(e).lower() and "support" in str(e).lower()) or ("function" in str(e).lower() and "support" in str(e).lower()) ): - # openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配 + # openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配 logger.info( - f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。", + f"{self.get_model()} 不支持函数工具调用,已自动去除,不影响使用。", ) payloads.pop("tools", None) return ( @@ -708,10 +708,10 @@ class ProviderOpenAIOfficial(Provider): None, image_fallback_used, ) - # logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}") + # logger.error(f"发生了错误。Provider 配置如下: {self.provider_config}") if "tool" in str(e).lower() and "support" in str(e).lower(): - logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all") + logger.error("疑似该模型不支持函数调用工具调用。请输入 /tool off_all") if is_connection_error(e): proxy = self.provider_config.get("proxy", "") @@ -781,7 +781,7 @@ class ProviderOpenAIOfficial(Provider): break if retry_cnt == max_retries - 1 or llm_response is None: - logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") + logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") if last_exception is None: raise Exception("未知错误") raise last_exception @@ -799,7 +799,7 @@ class ProviderOpenAIOfficial(Provider): model=None, **kwargs, ) -> AsyncGenerator[LLMResponse, None]: - """流式对话,与服务商交互并逐步返回结果""" + """流式对话,与服务商交互并逐步返回结果""" payloads, context_query = await self._prepare_chat_payload( prompt, image_urls, @@ -848,7 +848,7 @@ class ProviderOpenAIOfficial(Provider): break if retry_cnt == max_retries - 1: - logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") + logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") if last_exception is None: raise Exception("未知错误") raise last_exception @@ -899,7 +899,7 @@ class ProviderOpenAIOfficial(Provider): else: image_data = await self.encode_image_bs64(image_url) if not image_data: - logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") + logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。") return None return { "type": "image_url", @@ -909,17 +909,17 @@ class ProviderOpenAIOfficial(Provider): # 构建内容块列表 content_blocks = [] - # 1. 用户原始发言(OpenAI 建议:用户发言在前) + # 1. 用户原始发言(OpenAI 建议:用户发言在前) if text: content_blocks.append({"type": "text", "text": text}) elif image_urls: - # 如果没有文本但有图片,添加占位文本 + # 如果没有文本但有图片,添加占位文本 content_blocks.append({"type": "text", "text": "[图片]"}) elif extra_user_content_parts: - # 如果只有额外内容块,也需要添加占位文本 + # 如果只有额外内容块,也需要添加占位文本 content_blocks.append({"type": "text", "text": " "}) - # 2. 额外的内容块(系统提醒、指令等) + # 2. 额外的内容块(系统提醒、指令等) if extra_user_content_parts: for part in extra_user_content_parts: if isinstance(part, TextPart): @@ -938,7 +938,7 @@ class ProviderOpenAIOfficial(Provider): if image_part: content_blocks.append(image_part) - # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 + # 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容 if ( text and not extra_user_content_parts diff --git a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py index 4efeaae8e..e16378ce8 100644 --- a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py +++ b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py @@ -39,7 +39,7 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider): self.is_emotion = provider_config.get("is_emotion", False) async def initialize(self) -> None: - logger.info("下载或者加载 SenseVoice 模型中,这可能需要一些时间 ...") + logger.info("下载或者加载 SenseVoice 模型中,这可能需要一些时间 ...") # 将模型加载放到线程池中执行 self.model = await asyncio.get_running_loop().run_in_executor( @@ -47,7 +47,7 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider): lambda: SenseVoiceSmall(self.model_name, quantize=True, batch_size=16), ) - logger.info("SenseVoice 模型加载完成。") + logger.info("SenseVoice 模型加载完成。") async def get_timestamped_path(self) -> str: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -96,14 +96,14 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider): ) # res = self.model(audio_url, language="auto", use_itn=True) - logger.debug(f"SenseVoice识别到的文案:{res}") + logger.debug(f"SenseVoice识别到的文案:{res}") text = rich_transcription_postprocess(res[0]) if self.is_emotion: # 提取第二个匹配的值 matches = re.findall(r"<\|([^|]+)\|>", res[0]) if len(matches) >= 2: emotion = matches[1] - text = f"(当前的情绪:{emotion}) {text}" + text = f"(当前的情绪:{emotion}) {text}" else: logger.warning("未能提取到情绪信息") return text diff --git a/astrbot/core/provider/sources/vllm_rerank_source.py b/astrbot/core/provider/sources/vllm_rerank_source.py index edd8a5491..fa3a509fa 100644 --- a/astrbot/core/provider/sources/vllm_rerank_source.py +++ b/astrbot/core/provider/sources/vllm_rerank_source.py @@ -54,7 +54,7 @@ class VLLMRerankProvider(RerankProvider): if not results: logger.warning( - f"Rerank API 返回了空的列表数据。原始响应: {response_data}", + f"Rerank API 返回了空的列表数据。原始响应: {response_data}", ) return [ diff --git a/astrbot/core/provider/sources/whisper_selfhosted_source.py b/astrbot/core/provider/sources/whisper_selfhosted_source.py index 75205cc52..fa3100f73 100644 --- a/astrbot/core/provider/sources/whisper_selfhosted_source.py +++ b/astrbot/core/provider/sources/whisper_selfhosted_source.py @@ -33,13 +33,13 @@ class ProviderOpenAIWhisperSelfHost(STTProvider): async def initialize(self) -> None: loop = asyncio.get_running_loop() - logger.info("下载或者加载 Whisper 模型中,这可能需要一些时间 ...") + logger.info("下载或者加载 Whisper 模型中,这可能需要一些时间 ...") self.model = await loop.run_in_executor( None, whisper.load_model, self.model_name, ) - logger.info("Whisper 模型加载完成。") + logger.info("Whisper 模型加载完成。") async def _is_silk_file(self, file_path) -> bool: silk_header = b"SILK" diff --git a/astrbot/core/provider/sources/xai_source.py b/astrbot/core/provider/sources/xai_source.py index b7b432b49..77c5858d5 100644 --- a/astrbot/core/provider/sources/xai_source.py +++ b/astrbot/core/provider/sources/xai_source.py @@ -14,7 +14,7 @@ class ProviderXAI(ProviderOpenAIOfficial): super().__init__(provider_config, provider_settings) def _maybe_inject_xai_search(self, payloads: dict) -> None: - """当开启 xAI 原生搜索时,向请求体注入 Live Search 参数。 + """当开启 xAI 原生搜索时,向请求体注入 Live Search 参数。 - 仅在 provider_config.xai_native_search 为 True 时生效 - 默认注入 {"mode": "auto"} diff --git a/astrbot/core/provider/sources/xinference_stt_provider.py b/astrbot/core/provider/sources/xinference_stt_provider.py index 8dd997593..f67e42191 100644 --- a/astrbot/core/provider/sources/xinference_stt_provider.py +++ b/astrbot/core/provider/sources/xinference_stt_provider.py @@ -156,7 +156,7 @@ class ProviderXinferenceSTT(STTProvider): audio_bytes = await f.read() # 4. Transcribe - # 官方asyncCLient的客户端似乎实现有点问题,这里直接用aiohttp实现openai标准兼容请求,提交issue等待官方修复后再改回来 + # 官方asyncCLient的客户端似乎实现有点问题,这里直接用aiohttp实现openai标准兼容请求,提交issue等待官方修复后再改回来 url = f"{self.base_url}/v1/audio/transcriptions" headers = { "accept": "application/json", diff --git a/astrbot/core/star/base.py b/astrbot/core/star/base.py index dd3ae3f0e..efd2527b1 100644 --- a/astrbot/core/star/base.py +++ b/astrbot/core/star/base.py @@ -13,7 +13,7 @@ logger = logging.getLogger("astrbot") class Star(CommandParserMixin, PluginKVStoreMixin): - """所有插件(Star)的父类,所有插件都应该继承于这个类""" + """所有插件(Star)的父类,所有插件都应该继承于这个类""" author: str name: str @@ -81,7 +81,7 @@ class Star(CommandParserMixin, PluginKVStoreMixin): """当插件被激活时会调用这个方法""" async def terminate(self) -> None: - """当插件被禁用、重载插件时会调用这个方法""" + """当插件被禁用、重载插件时会调用这个方法""" def __del__(self) -> None: - """[Deprecated] 当插件被禁用、重载插件时会调用这个方法""" + """[Deprecated] 当插件被禁用、重载插件时会调用这个方法""" diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index c60af9ea2..6e376b082 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -46,7 +46,7 @@ class CommandDescriptor: async def sync_command_configs() -> None: - """同步指令配置,清理过期配置。""" + """同步指令配置,清理过期配置。""" descriptors = _collect_descriptors(include_sub_commands=False) config_records = await db_helper.get_command_configs() config_map = _bind_configs_to_descriptors(descriptors, config_records) @@ -60,7 +60,7 @@ async def sync_command_configs() -> None: async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescriptor: descriptor = _build_descriptor_by_full_name(handler_full_name) if not descriptor: - raise ValueError("指定的处理函数不存在或不是指令。") + raise ValueError("指定的处理函数不存在或不是指令。") existing_cfg = await db_helper.get_command_config(handler_full_name) config = await db_helper.upsert_command_config( @@ -95,16 +95,16 @@ async def rename_command( ) -> CommandDescriptor: descriptor = _build_descriptor_by_full_name(handler_full_name) if not descriptor: - raise ValueError("指定的处理函数不存在或不是指令。") + raise ValueError("指定的处理函数不存在或不是指令。") new_fragment = new_fragment.strip() if not new_fragment: - raise ValueError("指令名不能为空。") + raise ValueError("指令名不能为空。") # 校验主指令名 candidate_full = _compose_command(descriptor.parent_signature, new_fragment) if _is_command_in_use(handler_full_name, candidate_full): - raise ValueError(f"指令名 '{candidate_full}' 已被其他指令占用。") + raise ValueError(f"指令名 '{candidate_full}' 已被其他指令占用。") # 校验别名 if aliases: @@ -114,7 +114,7 @@ async def rename_command( continue alias_full = _compose_command(descriptor.parent_signature, alias) if _is_command_in_use(handler_full_name, alias_full): - raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。") + raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。") existing_cfg = await db_helper.get_command_config(handler_full_name) merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {} @@ -146,10 +146,10 @@ async def update_command_permission( ) -> CommandDescriptor: descriptor = _build_descriptor_by_full_name(handler_full_name) if not descriptor: - raise ValueError("指定的处理函数不存在或不是指令。") + raise ValueError("指定的处理函数不存在或不是指令。") if permission_type not in ["admin", "member"]: - raise ValueError("权限类型必须为 admin 或 member。") + raise ValueError("权限类型必须为 admin 或 member。") handler = descriptor.handler found_plugin = star_map.get(handler.handler_module_path) @@ -195,7 +195,7 @@ async def list_commands() -> list[dict[str, Any]]: d.handler_full_name for group in conflict_groups.values() for d in group } - # 分类,设置冲突标志,将子指令挂载到父指令组 + # 分类,设置冲突标志,将子指令挂载到父指令组 group_map: dict[str, CommandDescriptor] = {} sub_commands: list[CommandDescriptor] = [] root_commands: list[CommandDescriptor] = [] @@ -215,7 +215,7 @@ async def list_commands() -> list[dict[str, Any]]: else: root_commands.append(sub) - # 指令组 + 普通指令,按 effective_command 字母排序 + # 指令组 + 普通指令,按 effective_command 字母排序 all_commands = list(group_map.values()) + root_commands all_commands.sort(key=lambda d: (d.effective_command or "").lower()) @@ -224,7 +224,7 @@ async def list_commands() -> list[dict[str, Any]]: async def list_command_conflicts() -> list[dict[str, Any]]: - """列出所有冲突的指令组。""" + """列出所有冲突的指令组。""" descriptors = _collect_descriptors(include_sub_commands=False) config_records = await db_helper.get_command_configs() _bind_configs_to_descriptors(descriptors, config_records) @@ -251,7 +251,7 @@ async def list_command_conflicts() -> list[dict[str, Any]]: def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: - """收集指令,按需包含子指令。""" + """收集指令,按需包含子指令。""" descriptors: list[CommandDescriptor] = [] for handler in star_handlers_registry: try: @@ -263,7 +263,7 @@ def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: descriptors.append(desc) except Exception as e: logger.warning( - f"解析指令处理函数 {handler.handler_full_name} 失败,跳过该指令。原因: {e!s}" + f"解析指令处理函数 {handler.handler_full_name} 失败,跳过该指令。原因: {e!s}" ) continue return descriptors @@ -289,7 +289,7 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: ) current_fragment = filter_ref.command_name parent_signature = (filter_ref.parent_command_names or [""])[0].strip() - # 如果是子指令,尝试找到父指令组的 handler_full_name + # 如果是子指令,尝试找到父指令组的 handler_full_name if is_sub_command and parent_signature: parent_group_handler = _find_parent_group_handler( handler.handler_module_path, parent_signature @@ -375,7 +375,7 @@ def _resolve_group_parent_signature(group_filter: CommandGroupFilter) -> str: def _find_parent_group_handler(module_path: str, parent_signature: str) -> str: - """根据模块路径和父级签名,找到对应的指令组 handler_full_name。""" + """根据模块路径和父级签名,找到对应的指令组 handler_full_name。""" parent_sig_normalized = parent_signature.strip() for handler in star_handlers_registry: if handler.handler_module_path != module_path: @@ -534,7 +534,7 @@ def _descriptor_to_dict(desc: CommandDescriptor) -> dict[str, Any]: "has_conflict": desc.has_conflict, "reserved": desc.reserved, } - # 如果是指令组,包含子指令列表 + # 如果是指令组,包含子指令列表 if desc.is_group and desc.sub_commands: result["sub_commands"] = [_descriptor_to_dict(sub) for sub in desc.sub_commands] else: diff --git a/astrbot/core/star/config.py b/astrbot/core/star/config.py index 429a05d5e..e8f350b03 100644 --- a/astrbot/core/star/config.py +++ b/astrbot/core/star/config.py @@ -1,4 +1,4 @@ -"""此功能已过时,参考 https://astrbot.app/dev/plugin.html#%E6%B3%A8%E5%86%8C%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE-beta""" +"""此功能已过时,参考 https://astrbot.app/dev/plugin.html#%E6%B3%A8%E5%86%8C%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE-beta""" import json import os @@ -7,9 +7,9 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path def load_config(namespace: str) -> dict | bool: - """从配置文件中加载配置。 - namespace: str, 配置的唯一识别符,也就是配置文件的名字。 - 返回值: 当配置文件存在时,返回 namespace 对应配置文件的内容dict,否则返回 False。 + """从配置文件中加载配置。 + namespace: str, 配置的唯一识别符,也就是配置文件的名字。 + 返回值: 当配置文件存在时,返回 namespace 对应配置文件的内容dict,否则返回 False。 """ path = os.path.join(get_astrbot_data_path(), "config", f"{namespace}.json") if not os.path.exists(path): @@ -23,23 +23,23 @@ def load_config(namespace: str) -> dict | bool: def put_config(namespace: str, name: str, key: str, value, description: str) -> None: - """将配置项写入以namespace为名字的配置文件,如果key不存在于目标配置文件中。当前 value 仅支持 str, int, float, bool, list 类型(暂不支持 dict)。 - namespace: str, 配置的唯一识别符,也就是配置文件的名字。 - name: str, 配置项的显示名字。 - key: str, 配置项的键。 - value: str, int, float, bool, list, 配置项的值。 - description: str, 配置项的描述。 - 注意:只有当 namespace 为插件名(info 函数中的 name)时,该配置才会显示到可视化面板上。 - 注意:value一定要是该配置项对应类型的值,否则类型判断会乱。 + """将配置项写入以namespace为名字的配置文件,如果key不存在于目标配置文件中。当前 value 仅支持 str, int, float, bool, list 类型(暂不支持 dict)。 + namespace: str, 配置的唯一识别符,也就是配置文件的名字。 + name: str, 配置项的显示名字。 + key: str, 配置项的键。 + value: str, int, float, bool, list, 配置项的值。 + description: str, 配置项的描述。 + 注意:只有当 namespace 为插件名(info 函数中的 name)时,该配置才会显示到可视化面板上。 + 注意:value一定要是该配置项对应类型的值,否则类型判断会乱。 """ if namespace == "": - raise ValueError("namespace 不能为空。") + raise ValueError("namespace 不能为空。") if namespace.startswith("internal_"): - raise ValueError("namespace 不能以 internal_ 开头。") + raise ValueError("namespace 不能以 internal_ 开头。") if not isinstance(key, str): - raise ValueError("key 只支持 str 类型。") + raise ValueError("key 只支持 str 类型。") if not isinstance(value, str | int | float | bool | list): - raise ValueError("value 只支持 str, int, float, bool, list 类型。") + raise ValueError("value 只支持 str, int, float, bool, list 类型。") config_dir = os.path.join(get_astrbot_data_path(), "config") path = os.path.join(config_dir, f"{namespace}.json") @@ -65,19 +65,19 @@ def put_config(namespace: str, name: str, key: str, value, description: str) -> def update_config(namespace: str, key: str, value) -> None: - """更新配置文件中的配置项。 - namespace: str, 配置的唯一识别符,也就是配置文件的名字。 - key: str, 配置项的键。 - value: str, int, float, bool, list, 配置项的值。 + """更新配置文件中的配置项。 + namespace: str, 配置的唯一识别符,也就是配置文件的名字。 + key: str, 配置项的键。 + value: str, int, float, bool, list, 配置项的值。 """ path = os.path.join(get_astrbot_data_path(), "config", f"{namespace}.json") if not os.path.exists(path): - raise FileNotFoundError(f"配置文件 {namespace}.json 不存在。") + raise FileNotFoundError(f"配置文件 {namespace}.json 不存在。") with open(path, encoding="utf-8-sig") as f: d = json.load(f) assert isinstance(d, dict) if key not in d: - raise KeyError(f"配置项 {key} 不存在。") + raise KeyError(f"配置项 {key} 不存在。") d[key]["value"] = value with open(path, "w", encoding="utf-8-sig") as f: json.dump(d, f, indent=2, ensure_ascii=False) diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 8b0d616c8..e421170b9 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -60,7 +60,7 @@ class StarManagerProtocol(Protocol): class Context: - """暴露给插件的接口上下文。""" + """暴露给插件的接口上下文。""" registered_web_apis: list = [] @@ -84,7 +84,7 @@ class Context: subagent_orchestrator: SubAgentOrchestrator | None = None, ) -> None: self._event_queue = event_queue - """事件队列。消息平台通过事件队列传递消息事件。""" + """事件队列。消息平台通过事件队列传递消息事件。""" self._config = config """AstrBot 默认配置""" self._db = db @@ -258,16 +258,16 @@ class Context: return llm_resp async def get_current_chat_provider_id(self, umo: str) -> str: - """获取当前使用的聊天模型 Provider ID。 + """获取当前使用的聊天模型 Provider ID。 Args: - umo: unified_message_origin。消息会话来源 ID。 + umo: unified_message_origin。消息会话来源 ID。 Returns: - 指定消息会话来源当前使用的聊天模型 Provider ID。 + 指定消息会话来源当前使用的聊天模型 Provider ID。 Raises: - ProviderNotFoundError: 未找到。 + ProviderNotFoundError: 未找到。 """ prov = self.get_using_provider(umo) if not prov: @@ -285,31 +285,31 @@ class Context: return star_registry def get_llm_tool_manager(self) -> FunctionToolManager: - """获取 LLM Tool Manager,其用于管理注册的所有的 Function-calling tools""" + """获取 LLM Tool Manager,其用于管理注册的所有的 Function-calling tools""" return self.provider_manager.llm_tools def activate_llm_tool(self, name: str) -> bool: - """激活一个已经注册的函数调用工具。 + """激活一个已经注册的函数调用工具。 Args: - name: 工具名称。 + name: 工具名称。 Returns: - 如果成功激活返回 True,如果没找到工具返回 False。 + 如果成功激活返回 True,如果没找到工具返回 False。 Note: - 注册的工具默认是激活状态。 + 注册的工具默认是激活状态。 """ return self.provider_manager.llm_tools.activate_llm_tool(name, star_map) def deactivate_llm_tool(self, name: str) -> bool: - """停用一个已经注册的函数调用工具。 + """停用一个已经注册的函数调用工具。 Args: - name: 工具名称。 + name: 工具名称。 Returns: - 如果成功停用返回 True,如果没找到工具返回 False。 + 如果成功停用返回 True,如果没找到工具返回 False。 """ return self.provider_manager.llm_tools.deactivate_llm_tool(name) @@ -319,52 +319,52 @@ class Context: ) -> ( Provider | TTSProvider | STTProvider | EmbeddingProvider | RerankProvider | None ): - """通过 ID 获取对应的 LLM Provider。 + """通过 ID 获取对应的 LLM Provider。 Args: - provider_id: 提供者 ID。 + provider_id: 提供者 ID。 Returns: - 提供者实例,如果未找到则返回 None。 + 提供者实例,如果未找到则返回 None。 Note: - 如果提供者 ID 存在但未找到提供者,会记录警告日志。 + 如果提供者 ID 存在但未找到提供者,会记录警告日志。 """ prov = self.provider_manager.inst_map.get(provider_id) if provider_id and not prov: logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" ) return prov def get_all_providers(self) -> list[Provider]: - """获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" + """获取所有用于文本生成任务的 LLM Provider(Chat_Completion 类型)。""" return self.provider_manager.provider_insts def get_all_tts_providers(self) -> list[TTSProvider]: - """获取所有用于 TTS 任务的 Provider。""" + """获取所有用于 TTS 任务的 Provider。""" return self.provider_manager.tts_provider_insts def get_all_stt_providers(self) -> list[STTProvider]: - """获取所有用于 STT 任务的 Provider。""" + """获取所有用于 STT 任务的 Provider。""" return self.provider_manager.stt_provider_insts def get_all_embedding_providers(self) -> list[EmbeddingProvider]: - """获取所有用于 Embedding 任务的 Provider。""" + """获取所有用于 Embedding 任务的 Provider。""" return self.provider_manager.embedding_provider_insts def get_using_provider(self, umo: str | None = None) -> Provider | None: - """获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。 + """获取当前使用的用于文本生成任务的 LLM Provider(Chat_Completion 类型)。 Args: - umo: unified_message_origin 值,如果传入并且用户启用了提供商会话隔离, - 则使用该会话偏好的对话模型(提供商)。 + umo: unified_message_origin 值,如果传入并且用户启用了提供商会话隔离, + 则使用该会话偏好的对话模型(提供商)。 Returns: - 当前使用的对话模型(提供商),如果未设置则返回 None。 + 当前使用的对话模型(提供商),如果未设置则返回 None。 Raises: - ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。 + ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。 """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.CHAT_COMPLETION, @@ -374,21 +374,21 @@ class Context: return None if not isinstance(prov, Provider): raise ValueError( - f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}" + f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}" ) return prov def get_using_tts_provider(self, umo: str | None = None) -> TTSProvider | None: - """获取当前使用的用于 TTS 任务的 Provider。 + """获取当前使用的用于 TTS 任务的 Provider。 Args: - umo: unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 + umo: unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 Returns: - 当前使用的 TTS 提供者,如果未设置则返回 None。 + 当前使用的 TTS 提供者,如果未设置则返回 None。 Raises: - ValueError: 返回的提供者不是 TTSProvider 类型。 + ValueError: 返回的提供者不是 TTSProvider 类型。 """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.TEXT_TO_SPEECH, @@ -399,16 +399,16 @@ class Context: return prov def get_using_stt_provider(self, umo: str | None = None) -> STTProvider | None: - """获取当前使用的用于 STT 任务的 Provider。 + """获取当前使用的用于 STT 任务的 Provider。 Args: - umo: unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 + umo: unified_message_origin 值,如果传入,则使用该会话偏好的提供商。 Returns: - 当前使用的 STT 提供者,如果未设置则返回 None。 + 当前使用的 STT 提供者,如果未设置则返回 None。 Raises: - ValueError: 返回的提供者不是 STTProvider 类型。 + ValueError: 返回的提供者不是 STTProvider 类型。 """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.SPEECH_TO_TEXT, @@ -419,16 +419,16 @@ class Context: return prov def get_config(self, umo: str | None = None) -> AstrBotConfig: - """获取 AstrBot 的配置。 + """获取 AstrBot 的配置。 Args: - umo: unified_message_origin 值,用于获取特定会话的配置。 + umo: unified_message_origin 值,用于获取特定会话的配置。 Returns: - AstrBot 配置对象。 + AstrBot 配置对象。 Note: - 如果不提供 umo 参数,将返回默认配置。 + 如果不提供 umo 参数,将返回默认配置。 """ if not umo: # 使用默认配置 @@ -440,21 +440,21 @@ class Context: session: str | MessageSesion, message_chain: MessageChain, ) -> bool: - """根据 session(unified_msg_origin) 主动发送消息。 + """根据 session(unified_msg_origin) 主动发送消息。 Args: - session: 消息会话。通过 event.session 或者 event.unified_msg_origin 获取。 - message_chain: 消息链。 + session: 消息会话。通过 event.session 或者 event.unified_msg_origin 获取。 + message_chain: 消息链。 Returns: - 是否找到匹配的平台。 + 是否找到匹配的平台。 Raises: - ValueError: session 字符串不合法时抛出。 + ValueError: session 字符串不合法时抛出。 Note: - 当 session 为字符串时,会尝试解析为 MessageSession 对象。(类名为MessageSesion是因为历史遗留拼写错误) - qq_official(QQ 官方 API 平台) 不支持此方法。 + 当 session 为字符串时,会尝试解析为 MessageSession 对象。(类名为MessageSesion是因为历史遗留拼写错误) + qq_official(QQ 官方 API 平台) 不支持此方法。 """ if isinstance(session, str): try: @@ -472,13 +472,13 @@ class Context: return False def add_llm_tools(self, *tools: FunctionTool) -> None: - """添加 LLM 工具。 + """添加 LLM 工具。 Args: - *tools: 要添加的函数工具对象。 + *tools: 要添加的函数工具对象。 Note: - 如果工具已存在,会替换已存在的工具。 + 如果工具已存在,会替换已存在的工具。 """ tool_name = {tool.name for tool in self.provider_manager.llm_tools.func_list} module_path = "" @@ -513,16 +513,16 @@ class Context: methods: list, desc: str, ) -> None: - """注册 Web API。 + """注册 Web API。 Args: - route: API 路由路径。 - view_handler: 异步视图处理函数。 - methods: HTTP 方法列表。 - desc: API 描述。 + route: API 路由路径。 + view_handler: 异步视图处理函数。 + methods: HTTP 方法列表。 + desc: API 描述。 Note: - 如果相同路由和方法已注册,会替换现有的 API。 + 如果相同路由和方法已注册,会替换现有的 API。 """ for idx, api in enumerate(self.registered_web_apis): if api[0] == route and methods == api[2]: @@ -531,25 +531,25 @@ class Context: self.registered_web_apis.append((route, view_handler, methods, desc)) """ - 以下的方法已经不推荐使用。请从 AstrBot 文档查看更好的注册方式。 + 以下的方法已经不推荐使用。请从 AstrBot 文档查看更好的注册方式。 """ def get_event_queue(self) -> Queue: - """获取事件队列。""" + """获取事件队列。""" return self._event_queue @deprecated(version="4.0.0", reason="Use get_platform_inst instead") def get_platform(self, platform_type: PlatformAdapterType | str) -> Platform | None: - """获取指定类型的平台适配器。 + """获取指定类型的平台适配器。 Args: - platform_type: 平台类型或平台名称。 + platform_type: 平台类型或平台名称。 Returns: - 平台适配器实例,如果未找到则返回 None。 + 平台适配器实例,如果未找到则返回 None。 Note: - 该方法已经过时,请使用 get_platform_inst 方法。(>= AstrBot v4.0.0) + 该方法已经过时,请使用 get_platform_inst 方法。(>= AstrBot v4.0.0) """ for platform in self.platform_manager.platform_insts: name = platform.meta().name @@ -563,34 +563,34 @@ class Context: return platform def get_platform_inst(self, platform_id: str) -> Platform | None: - """获取指定 ID 的平台适配器实例。 + """获取指定 ID 的平台适配器实例。 Args: - platform_id: 平台适配器的唯一标识符。 + platform_id: 平台适配器的唯一标识符。 Returns: - 平台适配器实例,如果未找到则返回 None。 + 平台适配器实例,如果未找到则返回 None。 Note: - 可以通过 event.get_platform_id() 获取平台 ID。 + 可以通过 event.get_platform_id() 获取平台 ID。 """ for platform in self.platform_manager.platform_insts: if platform.meta().id == platform_id: return platform def get_db(self) -> BaseDatabase: - """获取 AstrBot 数据库。 + """获取 AstrBot 数据库。 Returns: - 数据库实例。 + 数据库实例。 """ return self._db def register_provider(self, provider: Provider) -> None: - """注册一个 LLM Provider(Chat_Completion 类型)。 + """注册一个 LLM Provider(Chat_Completion 类型)。 Args: - provider: 提供者实例。 + provider: 提供者实例。 """ self.provider_manager.provider_insts.append(provider) @@ -601,18 +601,18 @@ class Context: desc: str, func_obj: Callable[..., Awaitable[Any]], ) -> None: - """[DEPRECATED]为函数调用(function-calling / tools-use)添加工具。 + """[DEPRECATED]为函数调用(function-calling / tools-use)添加工具。 Args: - name: 函数名。 - func_args: 函数参数列表,格式为 - [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...]。 - desc: 函数描述。 - func_obj: 异步处理函数。 + name: 函数名。 + func_args: 函数参数列表,格式为 + [{"type": "string", "name": "arg_name", "description": "arg_description"}, ...]。 + desc: 函数描述。 + func_obj: 异步处理函数。 Note: - 异步处理函数会接收到额外的关键词参数:event: AstrMessageEvent, context: Context。 - 该方法已弃用,请使用新的注册方式。 + 异步处理函数会接收到额外的关键词参数:event: AstrMessageEvent, context: Context。 + 该方法已弃用,请使用新的注册方式。 """ md = StarHandlerMetadata( event_type=EventType.OnLLMRequestEvent, @@ -627,14 +627,14 @@ class Context: self.provider_manager.llm_tools.add_func(name, func_args, desc, func_obj) def unregister_llm_tool(self, name: str) -> None: - """[DEPRECATED]删除一个函数调用工具。 + """[DEPRECATED]删除一个函数调用工具。 Args: - name: 工具名称。 + name: 工具名称。 Note: - 如果再要启用,需要重新注册。 - 该方法已弃用。 + 如果再要启用,需要重新注册。 + 该方法已弃用。 """ self.provider_manager.llm_tools.remove_func(name) @@ -648,19 +648,19 @@ class Context: use_regex=False, ignore_prefix=False, ) -> None: - """[DEPRECATED]注册一个命令。 + """[DEPRECATED]注册一个命令。 Args: - star_name: 插件(Star)名称。 - command_name: 命令名称。 - desc: 命令描述。 - priority: 优先级。1-10。 - awaitable: 异步处理函数。 - use_regex: 是否使用正则表达式匹配命令。 - ignore_prefix: 是否忽略命令前缀。 + star_name: 插件(Star)名称。 + command_name: 命令名称。 + desc: 命令描述。 + priority: 优先级。1-10。 + awaitable: 异步处理函数。 + use_regex: 是否使用正则表达式匹配命令。 + ignore_prefix: 是否忽略命令前缀。 Note: - 推荐使用装饰器注册指令。该方法将在未来的版本中被移除。 + 推荐使用装饰器注册指令。该方法将在未来的版本中被移除。 """ md = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, @@ -680,13 +680,13 @@ class Context: star_handlers_registry.append(md) def register_task(self, task: Awaitable, desc: str) -> None: - """[DEPRECATED]注册一个异步任务。 + """[DEPRECATED]注册一个异步任务。 Args: - task: 异步任务。 - desc: 任务描述。 + task: 异步任务。 + desc: 任务描述。 Note: - 该方法已弃用。 + 该方法已弃用。 """ self._register_tasks.append(task) diff --git a/astrbot/core/star/error_messages.py b/astrbot/core/star/error_messages.py index 99de4d19b..a16092e09 100644 --- a/astrbot/core/star/error_messages.py +++ b/astrbot/core/star/error_messages.py @@ -1,11 +1,11 @@ """Shared plugin error message templates for star manager flows.""" PLUGIN_ERROR_TEMPLATES = { - "not_found_in_failed_list": "插件不存在于失败列表中。", - "reserved_plugin_cannot_uninstall": "该插件是 AstrBot 保留插件,无法卸载。", + "not_found_in_failed_list": "插件不存在于失败列表中。", + "reserved_plugin_cannot_uninstall": "该插件是 AstrBot 保留插件,无法卸载。", "failed_plugin_dir_remove_error": ( - "移除失败插件成功,但是删除插件文件夹失败: {error}。" - "您可以手动删除该文件夹,位于 addons/plugins/ 下。" + "移除失败插件成功,但是删除插件文件夹失败: {error}。" + "您可以手动删除该文件夹,位于 addons/plugins/ 下。" ), } diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py old mode 100755 new mode 100644 index 31949b674..6f128db6e --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -13,11 +13,11 @@ from .custom_filter import CustomFilter class GreedyStr(str): - """标记指令完成其他参数接收后的所有剩余文本。""" + """标记指令完成其他参数接收后的所有剩余文本。""" def unwrap_optional(annotation) -> tuple: - """去掉 Optional[T] / Union[T, None] / T|None,返回 T""" + """去掉 Optional[T] / Union[T, None] / T|None,返回 T""" args = typing.get_args(annotation) non_none_args = [a for a in args if a is not type(None)] if len(non_none_args) == 1: @@ -27,7 +27,7 @@ def unwrap_optional(annotation) -> tuple: return () -# 标准指令受到 wake_prefix 的制约。 +# 标准指令受到 wake_prefix 的制约。 class CommandFilter(HandlerFilter): """标准指令过滤器""" @@ -66,11 +66,11 @@ class CommandFilter(HandlerFilter): def init_handler_md(self, handle_md: StarHandlerMetadata) -> None: self.handler_md = handle_md signature = inspect.signature(self.handler_md.handler) - self.handler_params = {} # 参数名 -> 参数类型,如果有默认值则为默认值 + self.handler_params = {} # 参数名 -> 参数类型,如果有默认值则为默认值 idx = 0 for k, v in signature.parameters.items(): if idx < 2: - # 忽略前两个参数,即 self 和 event + # 忽略前两个参数,即 self 和 event idx += 1 continue if v.default == inspect.Parameter.empty: @@ -95,7 +95,7 @@ class CommandFilter(HandlerFilter): params: list[Any], param_type: dict[str, type], ) -> dict[str, Any]: - """将参数列表 params 根据 param_type 转换为参数字典。""" + """将参数列表 params 根据 param_type 转换为参数字典。""" result = {} param_items = list(param_type.items()) for i, (param_name, param_type_or_default_val) in enumerate(param_items): @@ -105,7 +105,7 @@ class CommandFilter(HandlerFilter): # GreedyStr 必须是最后一个参数 if i != len(param_items) - 1: raise ValueError( - f"参数 '{param_name}' (GreedyStr) 必须是最后一个参数。", + f"参数 '{param_name}' (GreedyStr) 必须是最后一个参数。", ) # 将剩余的所有部分合并成一个字符串 @@ -121,7 +121,7 @@ class CommandFilter(HandlerFilter): ): # 是类型 raise ValueError( - f"必要参数缺失。该指令完整参数: {self.print_types()}", + f"必要参数缺失。该指令完整参数: {self.print_types()}", ) # 是默认值 result[param_name] = param_type_or_default_val @@ -134,7 +134,7 @@ class CommandFilter(HandlerFilter): else: result[param_name] = params[i] elif isinstance(param_type_or_default_val, str): - # 如果 param_type_or_default_val 是字符串,直接赋值 + # 如果 param_type_or_default_val 是字符串,直接赋值 result[param_name] = params[i] elif isinstance(param_type_or_default_val, bool): # 处理布尔类型 @@ -145,7 +145,7 @@ class CommandFilter(HandlerFilter): result[param_name] = False else: raise ValueError( - f"参数 {param_name} 必须是布尔值(true/false, yes/no, 1/0)。", + f"参数 {param_name} 必须是布尔值(true/false, yes/no, 1/0)。", ) elif isinstance(param_type_or_default_val, int): result[param_name] = int(params[i]) @@ -161,14 +161,14 @@ class CommandFilter(HandlerFilter): # 只有一个非 NoneType 类型 result[param_name] = nn_types[0](params[i]) else: - # 没有或者有多个非 NoneType 类型,这里我们暂时直接赋值为原始值。 + # 没有或者有多个非 NoneType 类型,这里我们暂时直接赋值为原始值。 # NOTE: 目前还没有做类型校验 result[param_name] = params[i] else: result[param_name] = param_type_or_default_val(params[i]) except ValueError: raise ValueError( - f"参数 {param_name} 类型错误。完整参数: {self.print_types()}", + f"参数 {param_name} 类型错误。完整参数: {self.print_types()}", ) return result diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py old mode 100755 new mode 100644 index 52fb6a452..59542715c --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -8,7 +8,7 @@ from .command import CommandFilter from .custom_filter import CustomFilter -# 指令组受到 wake_prefix 的制约。 +# 指令组受到 wake_prefix 的制约。 class CommandGroupFilter(HandlerFilter): def __init__( self, @@ -36,9 +36,9 @@ class CommandGroupFilter(HandlerFilter): self.custom_filter_list.append(custom_filter) def get_complete_command_names(self) -> list[str]: - """遍历父节点获取完整的指令名。 + """遍历父节点获取完整的指令名。 - 新版本 v3.4.29 采用预编译指令,不再从指令组递归遍历子指令,因此这个方法是返回包括别名在内的整个指令名列表。 + 新版本 v3.4.29 采用预编译指令,不再从指令组递归遍历子指令,因此这个方法是返回包括别名在内的整个指令名列表。 """ if self._cmpl_cmd_names is not None: return self._cmpl_cmd_names @@ -94,7 +94,7 @@ class CommandGroupFilter(HandlerFilter): parts.append( sub_filter.print_cmd_tree( sub_filter.sub_command_filters, - prefix + "│ ", + prefix + "│ ", event=event, cfg=cfg, ) @@ -129,7 +129,7 @@ class CommandGroupFilter(HandlerFilter): + self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg) ) raise ValueError( - f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree, + f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree, ) return self.startswith(event.message_str) diff --git a/astrbot/core/star/filter/permission.py b/astrbot/core/star/filter/permission.py index a70299fa9..deddfa4f0 100644 --- a/astrbot/core/star/filter/permission.py +++ b/astrbot/core/star/filter/permission.py @@ -7,7 +7,7 @@ from . import HandlerFilter class PermissionType(enum.Flag): - """权限类型。当选择 MEMBER,ADMIN 也可以通过。""" + """权限类型。当选择 MEMBER,ADMIN 也可以通过。""" ADMIN = enum.auto() MEMBER = enum.auto() @@ -25,7 +25,7 @@ class PermissionTypeFilter(HandlerFilter): if self.permission_type == PermissionType.ADMIN: if not event.is_admin(): # event.stop_event() - # raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限操作管理员指令。") + # raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限操作管理员指令。") return False return True diff --git a/astrbot/core/star/filter/regex.py b/astrbot/core/star/filter/regex.py index 605446282..4c2a033ef 100644 --- a/astrbot/core/star/filter/regex.py +++ b/astrbot/core/star/filter/regex.py @@ -6,7 +6,7 @@ from astrbot.core.platform.astr_message_event import AstrMessageEvent from . import HandlerFilter -# 正则表达式过滤器不会受到 wake_prefix 的制约。 +# 正则表达式过滤器不会受到 wake_prefix 的制约。 class RegexFilter(HandlerFilter): """正则表达式过滤器""" diff --git a/astrbot/core/star/register/star.py b/astrbot/core/star/register/star.py index c1a0ce10c..71d99bdc2 100644 --- a/astrbot/core/star/register/star.py +++ b/astrbot/core/star/register/star.py @@ -12,27 +12,27 @@ def register_star( version: str, repo: str | None = None, ): - """注册一个插件(Star)。 + """注册一个插件(Star)。 - [DEPRECATED] 该装饰器已废弃,将在未来版本中移除。 - 在 v3.5.19 版本之后(不含),您不需要使用该装饰器来装饰插件类, - AstrBot 会自动识别继承自 Star 的类并将其作为插件类加载。 + [DEPRECATED] 该装饰器已废弃,将在未来版本中移除。 + 在 v3.5.19 版本之后(不含),您不需要使用该装饰器来装饰插件类, + AstrBot 会自动识别继承自 Star 的类并将其作为插件类加载。 Args: - name: 插件名称。 - author: 作者。 - desc: 插件的简述。 - version: 版本号。 - repo: 仓库地址。如果没有填写仓库地址,将无法更新这个插件。 + name: 插件名称。 + author: 作者。 + desc: 插件的简述。 + version: 版本号。 + repo: 仓库地址。如果没有填写仓库地址,将无法更新这个插件。 - 如果需要为插件填写帮助信息,请使用如下格式: + 如果需要为插件填写帮助信息,请使用如下格式: ```python class MyPlugin(star.Star): \'\'\'这是帮助信息\'\'\' ... - 帮助信息会被自动提取。使用 `/plugin <插件名> 可以查看帮助信息。` + 帮助信息会被自动提取。使用 `/plugin <插件名> 可以查看帮助信息。` """ global _warned_register_star diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 1385b5056..082bcd611 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -96,11 +96,11 @@ def register_command( command_name.parent_group.add_sub_command_filter(new_command) else: logger.warning( - f"注册指令{command_name} 的子指令时未提供 sub_command 参数。", + f"注册指令{command_name} 的子指令时未提供 sub_command 参数。", ) # 裸指令 elif command_name is None: - logger.warning("注册裸指令时未提供 command_name 参数。") + logger.warning("注册裸指令时未提供 command_name 参数。") else: new_command = CommandFilter(command_name, alias, None) add_to_event_filters = True @@ -108,7 +108,7 @@ def register_command( def decorator(awaitable): if not add_to_event_filters: kwargs["sub_command"] = ( - True # 打一个标记,表示这是一个子指令,再 wakingstage 阶段这个 handler 将会直接被跳过(其父指令会接管) + True # 打一个标记,表示这是一个子指令,再 wakingstage 阶段这个 handler 将会直接被跳过(其父指令会接管) ) handler_md = get_handler_or_create( awaitable, @@ -128,16 +128,16 @@ def register_custom_filter(custom_type_filter, *args, **kwargs): Args: custom_type_filter: 在裸指令时为CustomFilter对象 - 在指令组时为父指令的RegisteringCommandable对象,即self或者command_group的返回 - raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True + 在指令组时为父指令的RegisteringCommandable对象,即self或者command_group的返回 + raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True """ add_to_event_filters = False raise_error = True - # 判断是否是指令组,指令组则添加到指令组的CommandGroupFilter对象中在waking_check的时候一起判断 + # 判断是否是指令组,指令组则添加到指令组的CommandGroupFilter对象中在waking_check的时候一起判断 if isinstance(custom_type_filter, RegisteringCommandable): - # 子指令, 此时函数为RegisteringCommandable对象的方法,首位参数为RegisteringCommandable对象的self。 + # 子指令, 此时函数为RegisteringCommandable对象的方法,首位参数为RegisteringCommandable对象的self。 parent_register_commandable = custom_type_filter custom_filter = args[0] if len(args) > 1: @@ -153,11 +153,11 @@ def register_custom_filter(custom_type_filter, *args, **kwargs): custom_filter = custom_filter(raise_error) def decorator(awaitable): - # 裸指令,子指令与指令组的区分,指令组会因为标记跳过wake。 + # 裸指令,子指令与指令组的区分,指令组会因为标记跳过wake。 if ( not add_to_event_filters and isinstance(awaitable, RegisteringCommandable) ) or (add_to_event_filters and isinstance(awaitable, RegisteringCommandable)): - # 指令组 与 根指令组,添加到本层的grouphandle中一起判断 + # 指令组 与 根指令组,添加到本层的grouphandle中一起判断 awaitable.parent_group.add_custom_filter(custom_filter) else: handler_md = get_handler_or_create( @@ -177,8 +177,8 @@ def register_custom_filter(custom_type_filter, *args, **kwargs): ) in parent_register_commandable.parent_group.sub_command_filters: if isinstance(sub_handle, CommandGroupFilter): continue - # 所有符合fullname一致的子指令handle添加自定义过滤器。 - # 不确定是否会有多个子指令有一样的fullname,比如一个方法添加多个command装饰器? + # 所有符合fullname一致的子指令handle添加自定义过滤器。 + # 不确定是否会有多个子指令有一样的fullname,比如一个方法添加多个command装饰器? sub_handle_md = sub_handle.get_handler_md() if ( sub_handle_md @@ -188,7 +188,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs): else: # 裸指令 - # 确保运行时是可调用的 handler,针对类型检查器添加忽略 + # 确保运行时是可调用的 handler,针对类型检查器添加忽略 assert isinstance(awaitable, Callable) handler_md = get_handler_or_create( awaitable, @@ -237,7 +237,7 @@ def register_command_group( handler_md.event_filters.append(new_group) return RegisteringCommandable(new_group) - raise ValueError("注册指令组失败。") + raise ValueError("注册指令组失败。") return decorator @@ -304,7 +304,7 @@ def register_permission_type(permission_type: PermissionType, raise_error: bool Args: permission_type: PermissionType - raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True + raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True """ @@ -339,14 +339,14 @@ def register_on_platform_loaded(**kwargs): def register_on_plugin_error(**kwargs): - """当插件处理消息异常时触发。 + """当插件处理消息异常时触发。 Hook 参数: event, plugin_name, handler_name, error, traceback_text 说明: - 在 hook 中调用 `event.stop_event()` 可屏蔽默认报错回显, - 并由插件自行决定是否转发到其他会话。 + 在 hook 中调用 `event.stop_event()` 可屏蔽默认报错回显, + 并由插件自行决定是否转发到其他会话。 """ def decorator(awaitable): @@ -363,7 +363,7 @@ def register_on_plugin_loaded(**kwargs): metadata 说明: - 当有插件加载完成时,触发该事件并获取到该插件的元数据 + 当有插件加载完成时,触发该事件并获取到该插件的元数据 """ def decorator(awaitable): @@ -380,7 +380,7 @@ def register_on_plugin_unloaded(**kwargs): metadata 说明: - 当有插件卸载完成时,触发该事件并获取到该插件的元数据 + 当有插件卸载完成时,触发该事件并获取到该插件的元数据 """ def decorator(awaitable): @@ -391,10 +391,10 @@ def register_on_plugin_unloaded(**kwargs): def register_on_waiting_llm_request(**kwargs): - """当等待调用 LLM 时的通知事件(在获取锁之前) + """当等待调用 LLM 时的通知事件(在获取锁之前) - 此钩子在消息确定要调用 LLM 但还未开始排队等锁时触发, - 适合用于发送"正在思考中..."等用户反馈提示。 + 此钩子在消息确定要调用 LLM 但还未开始排队等锁时触发, + 适合用于发送"正在思考中..."等用户反馈提示。 Examples: ```py @@ -426,7 +426,7 @@ def register_on_llm_request(**kwargs): request.system_prompt += "你是一个猫娘..." ``` - 请务必接收两个参数:event, request + 请务必接收两个参数:event, request """ @@ -449,7 +449,7 @@ def register_on_llm_response(**kwargs): ... ``` - 请务必接收两个参数:event, request + 请务必接收两个参数:event, request """ @@ -461,8 +461,8 @@ def register_on_llm_response(**kwargs): def register_on_using_llm_tool(**kwargs): - """当调用函数工具前的事件。 - 会传入 tool 和 tool_args 参数。 + """当调用函数工具前的事件。 + 会传入 tool 和 tool_args 参数。 Examples: ```py @@ -473,7 +473,7 @@ def register_on_using_llm_tool(**kwargs): ... ``` - 请务必接收三个参数:event, tool, tool_args + 请务必接收三个参数:event, tool, tool_args """ @@ -485,8 +485,8 @@ def register_on_using_llm_tool(**kwargs): def register_on_llm_tool_respond(**kwargs): - """当调用函数工具后的事件。 - 会传入 tool、tool_args 和 tool 的调用结果 tool_result 参数。 + """当调用函数工具后的事件。 + 会传入 tool、tool_args 和 tool 的调用结果 tool_result 参数。 Examples: ```py @@ -498,7 +498,7 @@ def register_on_llm_tool_respond(**kwargs): ... ``` - 请务必接收四个参数:event, tool, tool_args, tool_result + 请务必接收四个参数:event, tool, tool_args, tool_result """ @@ -510,14 +510,14 @@ def register_on_llm_tool_respond(**kwargs): def register_llm_tool(name: str | None = None, **kwargs): - """为函数调用(function-calling / tools-use)添加工具。 + """为函数调用(function-calling / tools-use)添加工具。 - 请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会尝试解析该函数注释) + 请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会尝试解析该函数注释) ``` - @llm_tool(name="get_weather") # 如果 name 不填,将使用函数名 + @llm_tool(name="get_weather") # 如果 name 不填,将使用函数名 async def get_weather(event: AstrMessageEvent, location: str): - \'\'\'获取天气信息。 + \'\'\'获取天气信息。 Args: location(string): 地点 @@ -525,17 +525,17 @@ def register_llm_tool(name: str | None = None, **kwargs): # 处理逻辑 ``` - 可接受的参数类型有:string, number, object, array, boolean。 + 可接受的参数类型有:string, number, object, array, boolean。 - 返回值: - - 返回 str:结果会被加入下一次 LLM 请求的 prompt 中,用于让 LLM 总结工具返回的结果 - - 返回 None:结果不会被加入下一次 LLM 请求的 prompt 中。 + 返回值: + - 返回 str:结果会被加入下一次 LLM 请求的 prompt 中,用于让 LLM 总结工具返回的结果 + - 返回 None:结果不会被加入下一次 LLM 请求的 prompt 中。 - 可以使用 yield 发送消息、终止事件。 + 可以使用 yield 发送消息、终止事件。 - 发送消息:请参考文档。 + 发送消息:请参考文档。 - 终止事件: + 终止事件: ``` event.stop_event() yield @@ -563,7 +563,7 @@ def register_llm_tool(name: str | None = None, **kwargs): type_name = arg.type_name if not type_name: raise ValueError( - f"LLM 函数工具 {awaitable.__module__}_{llm_tool_name} 的参数 {arg.arg_name} 缺少类型注释。", + f"LLM 函数工具 {awaitable.__module__}_{llm_tool_name} 的参数 {arg.arg_name} 缺少类型注释。", ) # parse type_name to handle cases like "list[string]" match = re.match(r"(\w+)\[(\w+)\]", type_name) @@ -577,7 +577,7 @@ def register_llm_tool(name: str | None = None, **kwargs): sub_type_name and sub_type_name not in SUPPORTED_TYPES ): raise ValueError( - f"LLM 函数工具 {awaitable.__module__}_{llm_tool_name} 不支持的参数类型:{arg.type_name}", + f"LLM 函数工具 {awaitable.__module__}_{llm_tool_name} 不支持的参数类型:{arg.type_name}", ) arg_json_schema = { diff --git a/astrbot/core/star/session_llm_manager.py b/astrbot/core/star/session_llm_manager.py index ad4a473b4..3bebce1af 100644 --- a/astrbot/core/star/session_llm_manager.py +++ b/astrbot/core/star/session_llm_manager.py @@ -1,11 +1,11 @@ -"""会话服务管理器 - 负责管理每个会话的LLM、TTS等服务的启停状态""" +"""会话服务管理器 - 负责管理每个会话的LLM、TTS等服务的启停状态""" from astrbot.core import logger, sp from astrbot.core.platform.astr_message_event import AstrMessageEvent class SessionServiceManager: - """管理会话级别的服务启停状态,包括LLM和TTS""" + """管理会话级别的服务启停状态,包括LLM和TTS""" # ============================================================================= # LLM 相关方法 @@ -19,7 +19,7 @@ class SessionServiceManager: session_id: 会话ID (unified_msg_origin) Returns: - bool: True表示启用,False表示禁用 + bool: True表示启用,False表示禁用 """ # 获取会话服务配置 @@ -30,12 +30,12 @@ class SessionServiceManager: default={}, ) - # 如果配置了该会话的LLM状态,返回该状态 + # 如果配置了该会话的LLM状态,返回该状态 llm_enabled = session_services.get("llm_enabled") if llm_enabled is not None: return llm_enabled - # 如果没有配置,默认为启用(兼容性考虑) + # 如果没有配置,默认为启用(兼容性考虑) return True @staticmethod @@ -44,7 +44,7 @@ class SessionServiceManager: Args: session_id: 会话ID (unified_msg_origin) - enabled: True表示启用,False表示禁用 + enabled: True表示启用,False表示禁用 """ session_config = ( @@ -72,7 +72,7 @@ class SessionServiceManager: event: 消息事件 Returns: - bool: True表示应该处理,False表示跳过 + bool: True表示应该处理,False表示跳过 """ session_id = event.unified_msg_origin @@ -90,7 +90,7 @@ class SessionServiceManager: session_id: 会话ID (unified_msg_origin) Returns: - bool: True表示启用,False表示禁用 + bool: True表示启用,False表示禁用 """ # 获取会话服务配置 @@ -101,12 +101,12 @@ class SessionServiceManager: default={}, ) - # 如果配置了该会话的TTS状态,返回该状态 + # 如果配置了该会话的TTS状态,返回该状态 tts_enabled = session_services.get("tts_enabled") if tts_enabled is not None: return tts_enabled - # 如果没有配置,默认为启用(兼容性考虑) + # 如果没有配置,默认为启用(兼容性考虑) return True @staticmethod @@ -115,7 +115,7 @@ class SessionServiceManager: Args: session_id: 会话ID (unified_msg_origin) - enabled: True表示启用,False表示禁用 + enabled: True表示启用,False表示禁用 """ session_config = ( @@ -147,7 +147,7 @@ class SessionServiceManager: event: 消息事件 Returns: - bool: True表示应该处理,False表示跳过 + bool: True表示应该处理,False表示跳过 """ session_id = event.unified_msg_origin @@ -165,7 +165,7 @@ class SessionServiceManager: session_id: 会话ID (unified_msg_origin) Returns: - bool: True表示启用,False表示禁用 + bool: True表示启用,False表示禁用 """ # 获取会话服务配置 @@ -176,10 +176,10 @@ class SessionServiceManager: default={}, ) - # 如果配置了该会话的整体状态,返回该状态 + # 如果配置了该会话的整体状态,返回该状态 session_enabled = session_services.get("session_enabled") if session_enabled is not None: return session_enabled - # 如果没有配置,默认为启用(兼容性考虑) + # 如果没有配置,默认为启用(兼容性考虑) return True diff --git a/astrbot/core/star/session_plugin_manager.py b/astrbot/core/star/session_plugin_manager.py index a81113415..2daa67edf 100644 --- a/astrbot/core/star/session_plugin_manager.py +++ b/astrbot/core/star/session_plugin_manager.py @@ -19,7 +19,7 @@ class SessionPluginManager: plugin_name: 插件名称 Returns: - bool: True表示启用,False表示禁用 + bool: True表示启用,False表示禁用 """ # 获取会话插件配置 @@ -34,15 +34,15 @@ class SessionPluginManager: enabled_plugins = session_config.get("enabled_plugins", []) disabled_plugins = session_config.get("disabled_plugins", []) - # 如果插件在禁用列表中,返回False + # 如果插件在禁用列表中,返回False if plugin_name in disabled_plugins: return False - # 如果插件在启用列表中,返回True + # 如果插件在启用列表中,返回True if plugin_name in enabled_plugins: return True - # 如果都没有配置,默认为启用(兼容性考虑) + # 如果都没有配置,默认为启用(兼容性考虑) return True @staticmethod @@ -78,11 +78,11 @@ class SessionPluginManager: # 获取处理器对应的插件 plugin = star_map.get(handler.handler_module_path) if not plugin: - # 如果找不到插件元数据,允许执行(可能是系统插件) + # 如果找不到插件元数据,允许执行(可能是系统插件) filtered_handlers.append(handler) continue - # 跳过保留插件(系统插件) + # 跳过保留插件(系统插件) if plugin.reserved: filtered_handlers.append(handler) continue @@ -93,7 +93,7 @@ class SessionPluginManager: # 检查插件是否在当前会话中启用 if plugin.name in disabled_plugins: logger.debug( - f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}", + f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}", ) else: filtered_handlers.append(handler) diff --git a/astrbot/core/star/star.py b/astrbot/core/star/star.py index 8cebbd772..49b6e6bf2 100644 --- a/astrbot/core/star/star.py +++ b/astrbot/core/star/star.py @@ -8,7 +8,7 @@ from astrbot.core.config import AstrBotConfig star_registry: list[StarMetadata] = [] star_map: dict[str, StarMetadata] = {} -"""key 是模块路径,__module__""" +"""key 是模块路径,__module__""" if TYPE_CHECKING: from . import Star @@ -16,9 +16,9 @@ if TYPE_CHECKING: @dataclass class StarMetadata: - """插件的元数据。 + """插件的元数据。 - 当 activated 为 False 时,star_cls 可能为 None,请不要在插件未激活时调用 star_cls 的方法。 + 当 activated 为 False 时,star_cls 可能为 None,请不要在插件未激活时调用 star_cls 的方法。 """ name: str | None = None @@ -62,10 +62,10 @@ class StarMetadata: """插件 Logo 的路径""" support_platforms: list[str] = field(default_factory=list) - """插件声明支持的平台适配器 ID 列表(对应 ADAPTER_NAME_2_TYPE 的 key)""" + """插件声明支持的平台适配器 ID 列表(对应 ADAPTER_NAME_2_TYPE 的 key)""" astrbot_version: str | None = None - """插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)""" + """插件要求的 AstrBot 版本范围(PEP 440 specifier,如 >=4.13.0,<4.17.0)""" def __str__(self) -> str: return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}" diff --git a/astrbot/core/star/star_handler.py b/astrbot/core/star/star_handler.py index 71482a79b..2265ba8b5 100644 --- a/astrbot/core/star/star_handler.py +++ b/astrbot/core/star/star_handler.py @@ -17,7 +17,7 @@ class StarHandlerRegistry(Generic[T]): self._handlers: list[StarHandlerMetadata] = [] def append(self, handler: StarHandlerMetadata) -> None: - """添加一个 Handler,并保持按优先级有序""" + """添加一个 Handler,并保持按优先级有序""" if "priority" not in handler.extras_configs: handler.extras_configs["priority"] = 0 @@ -201,17 +201,17 @@ star_handlers_registry: StarHandlerRegistry = StarHandlerRegistry() class EventType(enum.Enum): - """表示一个 AstrBot 内部事件的类型。如适配器消息事件、LLM 请求事件、发送消息前的事件等 + """表示一个 AstrBot 内部事件的类型。如适配器消息事件、LLM 请求事件、发送消息前的事件等 - 用于对 Handler 的职能分组。 + 用于对 Handler 的职能分组。 """ OnAstrBotLoadedEvent = enum.auto() # AstrBot 加载完成 OnPlatformLoadedEvent = enum.auto() # 平台加载完成 AdapterMessageEvent = enum.auto() # 收到适配器发来的消息 - OnWaitingLLMRequestEvent = enum.auto() # 等待调用 LLM(在获取锁之前,仅通知) - OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件) + OnWaitingLLMRequestEvent = enum.auto() # 等待调用 LLM(在获取锁之前,仅通知) + OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件) OnLLMResponseEvent = enum.auto() # LLM 响应后 OnDecoratingResultEvent = enum.auto() # 发送消息前 OnCallingFuncToolEvent = enum.auto() # 调用函数工具 @@ -228,7 +228,7 @@ H = TypeVar("H", bound=Callable[..., Any]) @dataclass class StarHandlerMetadata(Generic[H]): - """描述一个 Star 所注册的某一个 Handler。""" + """描述一个 Star 所注册的某一个 Handler。""" event_type: EventType """Handler 的事件类型""" @@ -237,16 +237,16 @@ class StarHandlerMetadata(Generic[H]): '''格式为 f"{handler.__module__}_{handler.__name__}"''' handler_name: str - """Handler 的名字,也就是方法名""" + """Handler 的名字,也就是方法名""" handler_module_path: str - """Handler 所在的模块路径。""" + """Handler 所在的模块路径。""" handler: H - """Handler 的函数对象,应当是一个异步函数""" + """Handler 的函数对象,应当是一个异步函数""" event_filters: list[HandlerFilter] - """一个适配器消息事件过滤器,用于描述这个 Handler 能够处理、应该处理的适配器消息事件""" + """一个适配器消息事件过滤器,用于描述这个 Handler 能够处理、应该处理的适配器消息事件""" desc: str = "" """Handler 的描述信息""" diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 386fc6ca3..53c57e41b 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1,4 +1,4 @@ -"""插件的重载、启停、安装、卸载等操作。""" +"""插件的重载、启停、安装、卸载等操作。""" import asyncio import contextlib @@ -55,7 +55,7 @@ try: from watchfiles import PythonFilter, awatch except ImportError: if os.getenv("ASTRBOT_RELOAD", "0") == "1": - logger.warning("未安装 watchfiles,无法实现插件的热重载。") + logger.warning("未安装 watchfiles,无法实现插件的热重载。") class PluginVersionIncompatibleError(Exception): @@ -115,7 +115,7 @@ async def _temporary_filtered_requirements_file( await to_thread.run_sync(os.remove, filtered_requirements_path) except OSError as exc: logger.warning( - "删除临时插件依赖文件失败:%s(路径:%s)", + "删除临时插件依赖文件失败:%s(路径:%s)", exc, filtered_requirements_path, ) @@ -133,20 +133,20 @@ async def _install_requirements_with_precheck( if install_plan is None: logger.info( - f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): " + f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): " f"{requirements_path}" ) await pip_installer.install(requirements_path=requirements_path) return if not install_plan.missing_names: - logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。") + logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。") return if not install_plan.install_lines: fallback_reason = install_plan.fallback_reason or "unknown reason" logger.info( - "检测到插件 %s 缺失依赖,但无法安全裁剪 requirements,回退到完整安装: %s (%s)", + "检测到插件 %s 缺失依赖,但无法安全裁剪 requirements,回退到完整安装: %s (%s)", plugin_label, requirements_path, fallback_reason, @@ -155,7 +155,7 @@ async def _install_requirements_with_precheck( return logger.info( - f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: " + f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: " f"{requirements_path} -> {sorted(install_plan.missing_names)}" ) @@ -177,13 +177,13 @@ class PluginManager: self.config = config self.plugin_store_path = get_astrbot_plugin_path() - """存储插件的路径。即 data/plugins""" + """存储插件的路径。即 data/plugins""" self.plugin_config_path = get_astrbot_config_path() - """存储插件配置的路径。data/config""" + """存储插件配置的路径。data/config""" self.reserved_plugin_path = os.path.join( get_astrbot_path(), "astrbot", "builtin_stars" ) - """保留插件的路径。在 astrbot/builtin_stars 目录下""" + """保留插件的路径。在 astrbot/builtin_stars 目录下""" self.conf_schema_fname = "_conf_schema.json" self.logo_fname = "logo.png" """插件配置 Schema 文件名""" @@ -191,7 +191,7 @@ class PluginManager: """StarManager操作互斥锁""" self.failed_plugin_dict = {} - """加载失败插件的信息,用于后续可能的热重载""" + """加载失败插件的信息,用于后续可能的热重载""" self.failed_plugin_info = "" if os.getenv("ASTRBOT_RELOAD", "0") == "1": @@ -244,14 +244,14 @@ class PluginManager: == os.path.commonpath([plugin_dir_path, file_path]) and plugin_name not in reloaded_plugins ): - logger.info(f"检测到插件 {plugin_name} 文件变化,正在重载...") + logger.info(f"检测到插件 {plugin_name} 文件变化,正在重载...") await self.reload(plugin_name) reloaded_plugins.add(plugin_name) break @staticmethod def _get_classes(arg: ModuleType): - """获取指定模块(可以理解为一个 python 文件)下所有的类""" + """获取指定模块(可以理解为一个 python 文件)下所有的类""" classes = [] clsmembers = inspect.getmembers(arg, inspect.isclass) for name, _ in clsmembers: @@ -265,7 +265,7 @@ class PluginManager: modules = [] dirs = os.listdir(path) - # 遍历文件夹,找到 main.py 或者和文件夹同名的文件 + # 遍历文件夹,找到 main.py 或者和文件夹同名的文件 for d in dirs: if os.path.isdir(os.path.join(path, d)): if os.path.exists(os.path.join(path, d, "main.py")): @@ -273,7 +273,7 @@ class PluginManager: elif os.path.exists(os.path.join(path, d, d + ".py")): module_str = d else: - logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。") + logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。") continue if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists( os.path.join(path, d, d + ".py"), @@ -302,7 +302,7 @@ class PluginManager: self, target_plugin: str | None = None ) -> bool | None: """检查插件的依赖 - 如果 target_plugin 为 None,则检查所有插件的依赖 + 如果 target_plugin 为 None,则检查所有插件的依赖 """ plugin_dir = self.plugin_store_path if not await anyio.Path(plugin_dir).exists(): @@ -359,19 +359,19 @@ class PluginManager: if await anyio.Path(requirements_path).exists(): try: logger.info( - f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}" + f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}" ) pip_installer.prefer_installed_dependencies( requirements_path=requirements_path ) module = __import__(path, fromlist=[module_str]) logger.info( - f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。" + f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。" ) return module except Exception as recover_exc: logger.info( - f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}" + f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}" ) await self._check_plugin_dept_update(target_plugin=root_dir_name) @@ -379,14 +379,14 @@ class PluginManager: @staticmethod def _load_plugin_metadata(plugin_path: str, plugin_obj=None) -> StarMetadata | None: - """先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。 + """先寻找 metadata.yaml 文件,如果不存在,则使用插件对象的 info() 函数获取元数据。 - Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。 + Notes: 旧版本 AstrBot 插件可能使用的是 info() 函数来获取元数据。 """ metadata = None if not os.path.exists(plugin_path): - raise Exception("插件不存在。") + raise Exception("插件不存在。") if os.path.exists(os.path.join(plugin_path, "metadata.yaml")): with open( @@ -409,7 +409,7 @@ class PluginManager: or "author" not in metadata ): raise Exception( - "插件元数据信息不完整。name, desc, version, author 是必须的字段。", + "插件元数据信息不完整。name, desc, version, author 是必须的字段。", ) metadata = StarMetadata( name=metadata["name"], @@ -444,32 +444,32 @@ class PluginManager: def _validate_importable_name(plugin_name: str) -> None: if "/" in plugin_name or "\\" in plugin_name: raise ValueError( - "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" + "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" ) if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name): raise Exception( - "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。" + "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。" ) @staticmethod def _get_plugin_dir_name_from_metadata(plugin_path: str) -> str: metadata_path = os.path.join(plugin_path, "metadata.yaml") if not os.path.exists(metadata_path): - raise Exception("未找到 metadata.yaml,无法获取插件目录名。") + raise Exception("未找到 metadata.yaml,无法获取插件目录名。") with open(metadata_path, encoding="utf-8") as f: metadata = yaml.safe_load(f) if not isinstance(metadata, dict): - raise Exception("metadata.yaml 格式错误。") + raise Exception("metadata.yaml 格式错误。") plugin_name = metadata.get("name") if not isinstance(plugin_name, str) or not plugin_name.strip(): - raise Exception("metadata.yaml 中缺少 name 字段。") + raise Exception("metadata.yaml 中缺少 name 字段。") plugin_dir_name = PluginManager._normalize_plugin_dir_name(plugin_name) if not plugin_dir_name: - raise Exception("metadata.yaml 中 name 字段内容非法。") + raise Exception("metadata.yaml 中 name 字段内容非法。") PluginManager._validate_importable_name(plugin_dir_name) return plugin_dir_name @@ -489,7 +489,7 @@ class PluginManager: except InvalidSpecifier: return ( False, - "astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。", + "astrbot_version 格式无效,请使用 PEP 440 版本范围格式,例如 >=4.16,<5。", ) try: @@ -497,13 +497,13 @@ class PluginManager: except InvalidVersion: return ( False, - f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。", + f"AstrBot 当前版本 {VERSION} 无法被解析,无法校验插件版本范围。", ) if current_version not in specifier: return ( False, - f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}", + f"当前 AstrBot 版本为 {VERSION},不满足插件要求的 astrbot_version: {normalized_spec}", ) return True, None @@ -514,11 +514,11 @@ class PluginManager: ) -> list[str]: """获取与指定插件相关的所有已加载模块名 - 根据插件根目录名和是否为保留插件,从 sys.modules 中筛选出相关的模块名 + 根据插件根目录名和是否为保留插件,从 sys.modules 中筛选出相关的模块名 Args: plugin_root_dir: 插件根目录名 - is_reserved: 是否是保留插件,影响模块路径前缀 + is_reserved: 是否是保留插件,影响模块路径前缀 Returns: list[str]: 与该插件相关的模块名列表 @@ -539,12 +539,12 @@ class PluginManager: ) -> None: """从 sys.modules 中移除指定的模块 - 可以基于模块名模式或插件目录名移除模块,用于清理插件相关的模块缓存 + 可以基于模块名模式或插件目录名移除模块,用于清理插件相关的模块缓存 Args: - module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "astrbot.builtin_stars"]) - root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块 - is_reserved: 插件是否为保留插件(影响模块路径前缀) + module_patterns: 要移除的模块名模式列表(例如 ["data.plugins", "astrbot.builtin_stars"]) + root_dir_name: 插件根目录名,用于移除与该插件相关的所有模块 + is_reserved: 插件是否为保留插件(影响模块路径前缀) """ if module_patterns: @@ -642,27 +642,27 @@ class PluginManager: version = info.get("version") or info.get("astrbot_version") if version: lines.append( - f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。", + f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。", ) else: lines.append( - f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。", + f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。", ) else: error = str(info) - lines.append(f"加载插件目录 {dir_name} 时出现问题,原因:{error}。") + lines.append(f"加载插件目录 {dir_name} 时出现问题,原因:{error}。") self.failed_plugin_info = "\n".join(lines) + "\n" async def reload_failed_plugin(self, dir_name): """ - 重新加载未注册(加载失败)的插件 + 重新加载未注册(加载失败)的插件 Args: - dir_name (str): 要重载的特定插件名称。 + dir_name (str): 要重载的特定插件名称。 Returns: - tuple: 返回 load() 方法的结果,包含 (success, error_message) + tuple: 返回 load() 方法的结果,包含 (success, error_message) - success (bool): 重载是否成功 - - error_message (str|None): 错误信息,成功时为 None + - error_message (str|None): 错误信息,成功时为 None """ async with self._pm_lock: @@ -686,13 +686,13 @@ class PluginManager: """重新加载插件 Args: - specified_plugin_name (str, optional): 要重载的特定插件名称。 - 如果为 None,则重载所有插件。 + specified_plugin_name (str, optional): 要重载的特定插件名称。 + 如果为 None,则重载所有插件。 Returns: - tuple: 返回 load() 方法的结果,包含 (success, error_message) + tuple: 返回 load() 方法的结果,包含 (success, error_message) - success (bool): 重载是否成功 - - error_message (str|None): 错误信息,成功时为 None + - error_message (str|None): 错误信息,成功时为 None """ async with self._pm_lock: @@ -712,7 +712,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", + f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", ) if smd.name and smd.module_path: await self._unbind_plugin(smd.name, smd.module_path) @@ -729,7 +729,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", + f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", ) if smd.name: await self._unbind_plugin(smd.name, specified_module_path) @@ -744,17 +744,17 @@ class PluginManager: specified_dir_name=None, ignore_version_check: bool = False, ): - """载入插件。 - 当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。 + """载入插件。 + 当 specified_module_path 或者 specified_dir_name 不为 None 时,只载入指定的插件。 Args: - specified_module_path (str, optional): 指定要加载的插件模块路径。例如: "data.plugins.my_plugin.main" - specified_dir_name (str, optional): 指定要加载的插件目录名。例如: "my_plugin" + specified_module_path (str, optional): 指定要加载的插件模块路径。例如: "data.plugins.my_plugin.main" + specified_dir_name (str, optional): 指定要加载的插件目录名。例如: "my_plugin" Returns: tuple: (success, error_message) - success (bool): 是否全部加载成功 - - error_message (str|None): 错误信息,成功时为 None + - error_message (str|None): 错误信息,成功时为 None """ inactivated_plugins = await sp.global_get("inactivated_plugins", []) @@ -767,7 +767,7 @@ class PluginManager: has_load_error = False - # 导入插件模块,并尝试实例化插件类 + # 导入插件模块,并尝试实例化插件类 for plugin_module in plugin_modules: try: module_str = plugin_module["module"] @@ -776,7 +776,7 @@ class PluginManager: reserved = plugin_module.get( "reserved", False, - ) # 是否是保留插件。目前在 astrbot/builtin_stars 目录下的都是保留插件。保留插件不可以卸载。 + ) # 是否是保留插件。目前在 astrbot/builtin_stars 目录下的都是保留插件。保留插件不可以卸载。 plugin_dir_path = ( os.path.join(self.plugin_store_path, root_dir_name) if not reserved @@ -806,7 +806,7 @@ class PluginManager: except Exception as e: error_trace = traceback.format_exc() logger.error(error_trace) - logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}") + logger.error(f"插件 {root_dir_name} 导入失败。原因:{e!s}") has_load_error = True self.failed_plugin_dict[root_dir_name] = ( self._build_failed_plugin_record( @@ -818,7 +818,7 @@ class PluginManager: ) ) if path in star_map: - logger.info("失败插件依旧在插件列表中,正在清理...") + logger.info("失败插件依旧在插件列表中,正在清理...") metadata = star_map.pop(path) if metadata in star_registry: star_registry.remove(metadata) @@ -864,7 +864,7 @@ class PluginManager: metadata.astrbot_version = metadata_yaml.astrbot_version except Exception as e: logger.warning( - f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。", + f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。", ) if not ignore_version_check: @@ -885,7 +885,7 @@ class PluginManager: p_author = (metadata.author or "unknown").lower().replace("/", "_") plugin_id = f"{p_author}/{p_name}" - # 在实例化前注入类属性,保证插件 __init__ 可读取这些值 + # 在实例化前注入类属性,保证插件 __init__ 可读取这些值 if metadata.star_cls_type: setattr(metadata.star_cls_type, "name", p_name) setattr(metadata.star_cls_type, "author", p_author) @@ -913,17 +913,17 @@ class PluginManager: setattr(metadata.star_cls, "author", p_author) setattr(metadata.star_cls, "plugin_id", plugin_id) else: - logger.info(f"插件 {metadata.name} 已被禁用。") + logger.info(f"插件 {metadata.name} 已被禁用。") metadata.module = module metadata.root_dir_name = root_dir_name metadata.reserved = reserved assert metadata.module_path is not None, ( - f"插件 {metadata.name} 的模块路径为空。" + f"插件 {metadata.name} 的模块路径为空。" ) assert metadata.star_cls is not None, ( - f"插件 {metadata.name} 的实例为空。" + f"插件 {metadata.name} 的实例为空。" ) # 绑定 handler @@ -965,7 +965,7 @@ class PluginManager: else: # v3.4.0 以前的方式注册插件 logger.debug( - f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。", + f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。", ) classes = self._get_classes(module) @@ -991,7 +991,7 @@ class PluginManager: plugin_obj=obj, ) if not metadata: - raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。") + raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。") if not ignore_version_check: is_valid, error_message = ( @@ -1031,7 +1031,7 @@ class PluginManager: ): full_names.append(handler.handler_full_name) - # 检查并且植入自定义的权限过滤器(alter_cmd) + # 检查并且植入自定义的权限过滤器(alter_cmd) if ( metadata.name in alter_cmd and handler.handler_name in alter_cmd[metadata.name] @@ -1059,7 +1059,7 @@ class PluginManager: ) logger.debug( - f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。", + f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。", ) metadata.star_handler_full_names = full_names @@ -1097,9 +1097,9 @@ class PluginManager: error_trace=errors, ) ) - # 记录注册失败的插件名称,以便后续重载插件 + # 记录注册失败的插件名称,以便后续重载插件 if path in star_map: - logger.info("失败插件依旧在插件列表中,正在清理...") + logger.info("失败插件依旧在插件列表中,正在清理...") metadata = star_map.pop(path) if metadata in star_registry: star_registry.remove(metadata) @@ -1145,7 +1145,7 @@ class PluginManager: logger.warning(f"已清理安装失败的插件目录: {plugin_path}") except Exception as e: logger.warning( - f"清理安装失败插件目录失败: {plugin_path},原因: {e!s}", + f"清理安装失败插件目录失败: {plugin_path},原因: {e!s}", ) plugin_config_path = os.path.join( @@ -1158,7 +1158,7 @@ class PluginManager: logger.warning(f"已清理安装失败插件配置: {plugin_config_path}") except Exception as e: logger.warning( - f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}", + f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}", ) def _cleanup_plugin_optional_artifacts( @@ -1233,17 +1233,17 @@ class PluginManager: ): """从仓库 URL 安装插件 - 从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中 + 从指定的仓库 URL 下载并安装插件,然后加载该插件到系统中 Args: repo_url (str): 要安装的插件仓库 URL - proxy (str, optional): 用于下载的代理服务器。默认为空字符串。 + proxy (str, optional): 用于下载的代理服务器。默认为空字符串。 Returns: dict | None: 安装成功时返回包含插件信息的字典: - repo: 插件的仓库 URL - readme: README.md 文件的内容(如果存在) - 如果找不到插件元数据则返回 None。 + 如果找不到插件元数据则返回 None。 """ # this metric is for displaying plugins installation count in webui @@ -1263,7 +1263,7 @@ class PluginManager: plugin_path = os.path.join(self.plugin_store_path, repo_name) if await anyio.Path(plugin_path).exists(): raise Exception( - f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" ) plugin_path = await self.updator.install(repo_url, proxy) @@ -1278,7 +1278,7 @@ class PluginManager: target_plugin_path != plugin_path and await anyio.Path(target_plugin_path).exists() ): - raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") + raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != plugin_path: os.rename(plugin_path, target_plugin_path) plugin_path = target_plugin_path @@ -1294,7 +1294,7 @@ class PluginManager: if not success: raise Exception( error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" ) # Get the plugin metadata to return repo info @@ -1339,7 +1339,7 @@ class PluginManager: ) if dir_name and plugin_path: logger.warning( - f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}", + f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}", ) raise @@ -1349,23 +1349,23 @@ class PluginManager: delete_config: bool = False, delete_data: bool = False, ) -> None: - """卸载指定的插件。 + """卸载指定的插件。 Args: plugin_name (str): 要卸载的插件名称 - delete_config (bool): 是否删除插件配置文件,默认为 False - delete_data (bool): 是否删除插件数据,默认为 False + delete_config (bool): 是否删除插件配置文件,默认为 False + delete_data (bool): 是否删除插件数据,默认为 False Raises: - Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常 + Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常 """ async with self._pm_lock: plugin = self.context.get_registered_star(plugin_name) if not plugin: - raise Exception("插件不存在。") + raise Exception("插件不存在。") if plugin.reserved: - raise Exception("该插件是 AstrBot 保留插件,无法卸载。") + raise Exception("该插件是 AstrBot 保留插件,无法卸载。") root_dir_name = plugin.root_dir_name ppath = self.plugin_store_path @@ -1375,12 +1375,12 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", + f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", ) # 从 star_registry 和 star_map 中删除 if plugin.module_path is None or root_dir_name is None: - raise Exception(f"插件 {plugin_name} 数据不完整,无法卸载。") + raise Exception(f"插件 {plugin_name} 数据不完整,无法卸载。") await self._unbind_plugin(plugin_name, plugin.module_path) @@ -1389,7 +1389,7 @@ class PluginManager: remove_dir(os.path.join(ppath, root_dir_name)) except Exception as e: raise Exception( - f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。", + f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。", ) self._cleanup_plugin_optional_artifacts( @@ -1405,7 +1405,7 @@ class PluginManager: delete_config: bool = False, delete_data: bool = False, ) -> None: - """卸载加载失败的插件(按目录名)。""" + """卸载加载失败的插件(按目录名)。""" async with self._pm_lock: failed_info = self.failed_plugin_dict.get(dir_name) if not failed_info: @@ -1433,7 +1433,7 @@ class PluginManager: ) else: logger.debug( - "插件目录不存在,视为已部分卸载状态,继续清理失败插件记录和可选产物: %s", + "插件目录不存在,视为已部分卸载状态,继续清理失败插件记录和可选产物: %s", plugin_path, ) @@ -1456,7 +1456,7 @@ class PluginManager: self._rebuild_failed_plugin_info() async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str) -> None: - """解绑并移除一个插件。 + """解绑并移除一个插件。 Args: plugin_name: 要解绑的插件名称 @@ -1522,9 +1522,9 @@ class PluginManager: """升级一个插件""" plugin = self.context.get_registered_star(plugin_name) if not plugin: - raise Exception("插件不存在。") + raise Exception("插件不存在。") if plugin.reserved: - raise Exception("该插件是 AstrBot 保留插件,无法更新。") + raise Exception("该插件是 AstrBot 保留插件,无法更新。") await self.updator.update(plugin, proxy=proxy) if plugin.root_dir_name: @@ -1536,15 +1536,15 @@ class PluginManager: await self.reload(plugin_name) async def turn_off_plugin(self, plugin_name: str) -> None: - """禁用一个插件。 - 调用插件的 terminate() 方法, - 将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。 - 并且同时将插件启用的 llm_tool 禁用。 + """禁用一个插件。 + 调用插件的 terminate() 方法, + 将插件的 module_path 加入到 data/shared_preferences.json 的 inactivated_plugins 列表中。 + 并且同时将插件启用的 llm_tool 禁用。 """ async with self._pm_lock: plugin = self.context.get_registered_star(plugin_name) if not plugin: - raise Exception("插件不存在。") + raise Exception("插件不存在。") # 调用插件的终止方法 await self._terminate_plugin(plugin) @@ -1578,12 +1578,12 @@ class PluginManager: @staticmethod async def _terminate_plugin(star_metadata: StarMetadata) -> None: - """终止插件,调用插件的 terminate() 和 __del__() 方法""" + """终止插件,调用插件的 terminate() 和 __del__() 方法""" logger.info(f"正在终止插件 {star_metadata.name} ...") if not star_metadata.activated: # 说明之前已经被禁用了 - logger.debug(f"插件 {star_metadata.name} 未被激活,不需要终止,跳过。") + logger.debug(f"插件 {star_metadata.name} 未被激活,不需要终止,跳过。") return if star_metadata.star_cls is None: @@ -1601,7 +1601,7 @@ class PluginManager: return if (exc := fut.exception()) is not None: logger.error( - "插件 %s 在 __del__ 中抛出了异常:%r", + "插件 %s 在 __del__ 中抛出了异常:%r", star_metadata.name, exc, ) @@ -1626,7 +1626,7 @@ class PluginManager: async def turn_on_plugin(self, plugin_name: str) -> None: plugin = self.context.get_registered_star(plugin_name) if plugin is None: - raise Exception(f"插件 {plugin_name} 不存在。") + raise Exception(f"插件 {plugin_name} 不存在。") inactivated_plugins: list = await sp.global_get("inactivated_plugins", []) inactivated_llm_tools: list = await sp.global_get("inactivated_llm_tools", []) if plugin.module_path in inactivated_plugins: @@ -1669,7 +1669,7 @@ class PluginManager: target_plugin_path != desti_dir and await anyio.Path(target_plugin_path).exists() ): - raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") + raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != desti_dir: os.rename(desti_dir, target_plugin_path) dir_name = metadata_dir_name @@ -1689,7 +1689,7 @@ class PluginManager: if not success: raise Exception( error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" ) # Get the plugin metadata to return repo info @@ -1739,7 +1739,7 @@ class PluginManager: error=e, ) logger.warning( - f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}", + f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}", ) raise finally: @@ -1751,5 +1751,5 @@ class PluginManager: remove_dir(temp_desti_dir) except Exception as e: logger.warning( - f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}", + f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}", ) diff --git a/astrbot/core/star/star_tools.py b/astrbot/core/star/star_tools.py index 4d85131fc..81d40cae4 100644 --- a/astrbot/core/star/star_tools.py +++ b/astrbot/core/star/star_tools.py @@ -1,5 +1,5 @@ """插件开发工具集 -封装了许多常用的操作,方便插件开发者使用 +封装了许多常用的操作,方便插件开发者使用 说明: @@ -41,14 +41,14 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path class StarTools: """提供给插件使用的便捷工具函数集合 - 这些方法封装了一些常用操作,使插件开发更加简单便捷! + 这些方法封装了一些常用操作,使插件开发更加简单便捷! """ _context: ClassVar[Context | None] = None @classmethod def initialize(cls, context: Context) -> None: - """初始化StarTools,设置context引用 + """初始化StarTools,设置context引用 Args: context: 暴露给插件的上下文 @@ -65,7 +65,7 @@ class StarTools: """根据session(unified_msg_origin)主动发送消息 Args: - session: 消息会话。通过event.session或者event.unified_msg_origin获取 + session: 消息会话。通过event.session或者event.unified_msg_origin获取 message_chain: 消息链 Returns: @@ -96,7 +96,7 @@ class StarTools: type (str): 消息类型, 可选: PrivateMessage, GroupMessage id (str): 目标ID, 例如QQ号, 群号等 message_chain (MessageChain): 消息链 - platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp + platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp """ if cls._context is None: @@ -175,7 +175,7 @@ class StarTools: Args: abm (AstrBotMessage): 要提交的消息对象, 请先使用 create_message 创建 - platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp + platform (str): 可选的平台名称,默认平台(aiocqhttp), 目前只支持 aiocqhttp is_wake (bool): 是否标记为唤醒事件, 默认为 True, 只有唤醒事件才会被 llm 响应 """ @@ -234,13 +234,13 @@ class StarTools: desc: str, func_obj: Callable[..., Awaitable[Any]], ) -> None: - """为函数调用(function-calling/tools-use)添加工具 + """为函数调用(function-calling/tools-use)添加工具 Args: name (str): 工具名称 func_args (list): 函数参数列表 desc (str): 工具描述 - func_obj (Awaitable): 函数对象,必须是异步函数 + func_obj (Awaitable): 函数对象,必须是异步函数 """ if cls._context is None: @@ -250,7 +250,7 @@ class StarTools: @classmethod def unregister_llm_tool(cls, name: str) -> None: """删除一个函数调用工具 - 如果再要启用,需要重新注册 + 如果再要启用,需要重新注册 Args: name (str): 工具名称 @@ -262,22 +262,22 @@ class StarTools: @classmethod def get_data_dir(cls, plugin_name: str | None = None) -> Path: - """返回插件数据目录的绝对路径。 + """返回插件数据目录的绝对路径。 - 此方法会在 data/plugin_data 目录下为插件创建一个专属的数据目录。如果未提供插件名称, - 会自动从调用栈中获取插件信息。 + 此方法会在 data/plugin_data 目录下为插件创建一个专属的数据目录。如果未提供插件名称, + 会自动从调用栈中获取插件信息。 Args: - plugin_name: 可选的插件名称。如果为None,将自动检测调用者的插件名称。 + plugin_name: 可选的插件名称。如果为None,将自动检测调用者的插件名称。 Returns: - Path (Path): 插件数据目录的绝对路径,位于 data/plugin_data/{plugin_name}。 + Path (Path): 插件数据目录的绝对路径,位于 data/plugin_data/{plugin_name}。 Raises: RuntimeError: 当出现以下情况时抛出: - 无法获取调用者模块信息 - 无法获取模块的元数据信息 - - 创建目录失败(权限不足或其他IO错误) + - 创建目录失败(权限不足或其他IO错误) """ if not plugin_name: @@ -308,7 +308,7 @@ class StarTools: data_dir.mkdir(parents=True, exist_ok=True) except OSError as e: if isinstance(e, PermissionError): - raise RuntimeError(f"无法创建目录 {data_dir}:权限不足") from e - raise RuntimeError(f"无法创建目录 {data_dir}:{e!s}") from e + raise RuntimeError(f"无法创建目录 {data_dir}:权限不足") from e + raise RuntimeError(f"无法创建目录 {data_dir}:{e!s}") from e return data_dir.resolve() diff --git a/astrbot/core/star/updator.py b/astrbot/core/star/updator.py index 1a0c5fc26..8ec2e6e17 100644 --- a/astrbot/core/star/updator.py +++ b/astrbot/core/star/updator.py @@ -31,21 +31,21 @@ class PluginUpdator(RepoZipUpdator): repo_url = plugin.repo if not repo_url: - raise Exception(f"插件 {plugin.name} 没有指定仓库地址。") + raise Exception(f"插件 {plugin.name} 没有指定仓库地址。") if not plugin.root_dir_name: - raise Exception(f"插件 {plugin.name} 的根目录名未指定。") + raise Exception(f"插件 {plugin.name} 的根目录名未指定。") plugin_path = os.path.join(self.plugin_store_path, plugin.root_dir_name) - logger.info(f"正在更新插件,路径: {plugin_path},仓库地址: {repo_url}") + logger.info(f"正在更新插件,路径: {plugin_path},仓库地址: {repo_url}") await self.download_from_repo_url(plugin_path, repo_url, proxy=proxy) try: remove_dir(plugin_path) except BaseException as e: logger.error( - f"删除旧版本插件 {plugin_path} 文件夹失败: {e!s},使用覆盖安装。", + f"删除旧版本插件 {plugin_path} 文件夹失败: {e!s},使用覆盖安装。", ) self.unzip_file(plugin_path + ".zip", plugin_path) @@ -77,5 +77,5 @@ class PluginUpdator(RepoZipUpdator): os.remove(zip_path) except BaseException: logger.warning( - f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}", + f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}", ) diff --git a/astrbot/core/tools/kb_query.py b/astrbot/core/tools/kb_query.py index 80a35be1f..7e5e052fa 100644 --- a/astrbot/core/tools/kb_query.py +++ b/astrbot/core/tools/kb_query.py @@ -102,18 +102,18 @@ async def retrieve_knowledge_base( if not kb_names: return - logger.debug(f"[知识库] 使用会话级配置,知识库数量: {len(kb_names)}") + logger.debug(f"[知识库] 使用会话级配置,知识库数量: {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(f"[知识库] 使用全局配置,知识库数量: {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(f"[知识库] 开始检索知识库,数量: {len(kb_names)}, top_k={top_k}") kb_context = await kb_mgr.retrieve( query=query, kb_names=kb_names, diff --git a/astrbot/core/umop_config_router.py b/astrbot/core/umop_config_router.py index c2588e6c2..f7d49e626 100644 --- a/astrbot/core/umop_config_router.py +++ b/astrbot/core/umop_config_router.py @@ -27,7 +27,7 @@ class UmopConfigRouter: @staticmethod def _split_umo(umo: str) -> tuple[str, str, str] | None: - """将 UMO 拆分为 3 个部分,同时保留 session_id 中的 ':'""" + """将 UMO 拆分为 3 个部分,同时保留 session_id 中的 ':'""" if not isinstance(umo, str): return None parts = umo.split(":", 2) @@ -52,7 +52,7 @@ class UmopConfigRouter: umo (str): UMO 字符串 Returns: - str | None: 配置文件 ID,如果没有找到则返回 None + str | None: 配置文件 ID,如果没有找到则返回 None """ for pattern, conf_id in self.umop_to_conf_id.items(): @@ -64,8 +64,8 @@ class UmopConfigRouter: """更新路由表 Args: - new_routing (dict[str, str]): 新的 UMOP 到配置文件 ID 的映射。umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。 - umop 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。 + new_routing (dict[str, str]): 新的 UMOP 到配置文件 ID 的映射。umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。 + umop 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。 Raises: ValueError: 如果 new_routing 中的 key 格式不正确 diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index df2cfb82c..9e4606f12 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -13,9 +13,9 @@ from .zip_updator import ReleaseInfo, RepoZipUpdator class AstrBotUpdator(RepoZipUpdator): - """AstrBot 更新器,继承自 RepoZipUpdator 类 + """AstrBot 更新器,继承自 RepoZipUpdator 类 该类用于处理 AstrBot 的更新操作 - 功能包括检查更新、下载更新文件、解压缩更新文件等 + 功能包括检查更新、下载更新文件、解压缩更新文件等 """ def __init__(self, repo_mirror: str = "") -> None: @@ -25,12 +25,12 @@ class AstrBotUpdator(RepoZipUpdator): def terminate_child_processes(self) -> None: """终止当前进程的所有子进程 - 使用 psutil 库获取当前进程的所有子进程,并尝试终止它们 + 使用 psutil 库获取当前进程的所有子进程,并尝试终止它们 """ try: parent = psutil.Process(os.getpid()) children = parent.children(recursive=True) - logger.info(f"正在终止 {len(children)} 个子进程。") + logger.info(f"正在终止 {len(children)} 个子进程。") for child in children: logger.info(f"正在终止子进程 {child.pid}") child.terminate() @@ -39,7 +39,7 @@ class AstrBotUpdator(RepoZipUpdator): except psutil.NoSuchProcess: continue except psutil.TimeoutExpired: - logger.info(f"子进程 {child.pid} 没有被正常终止, 正在强行杀死。") + logger.info(f"子进程 {child.pid} 没有被正常终止, 正在强行杀死。") child.kill() except psutil.NoSuchProcess: pass @@ -113,7 +113,7 @@ class AstrBotUpdator(RepoZipUpdator): def _reboot(self, delay: int = 3) -> None: """重启当前程序 - 在指定的延迟后,终止所有子进程并重新启动程序 + 在指定的延迟后,终止所有子进程并重新启动程序 这里只能使用 os.exec* 来重启程序 """ time.sleep(delay) @@ -125,7 +125,7 @@ class AstrBotUpdator(RepoZipUpdator): reboot_argv = self._build_reboot_argv(executable) self._exec_reboot(executable, reboot_argv) except Exception as e: - logger.error(f"重启失败({executable}, {e}),请尝试手动重启。") + logger.error(f"重启失败({executable}, {e}),请尝试手动重启。") raise e async def check_update( @@ -156,7 +156,7 @@ class AstrBotUpdator(RepoZipUpdator): if latest: latest_version = update_data[0]["tag_name"] if self.compare_version(VERSION, latest_version) >= 0: - raise Exception("当前已经是最新版本。") + raise Exception("当前已经是最新版本。") file_url = update_data[0]["zipball_url"] elif str(version).startswith("v"): # 更新到指定版本 @@ -164,10 +164,10 @@ class AstrBotUpdator(RepoZipUpdator): if data["tag_name"] == version: file_url = data["zipball_url"] if not file_url: - raise Exception(f"未找到版本号为 {version} 的更新文件。") + raise Exception(f"未找到版本号为 {version} 的更新文件。") else: if len(str(version)) != 40: - raise Exception("commit hash 长度不正确,应为 40") + raise Exception("commit hash 长度不正确,应为 40") file_url = f"https://github.com/AstrBotDevs/AstrBot/archive/{version}.zip" logger.info(f"准备更新至指定版本的 AstrBot Core: {version}") @@ -177,7 +177,7 @@ class AstrBotUpdator(RepoZipUpdator): try: await download_file(file_url, "temp.zip") - logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...") + logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...") self.unzip_file("temp.zip", self.MAIN_PATH) except BaseException as e: raise e diff --git a/astrbot/core/utils/active_event_registry.py b/astrbot/core/utils/active_event_registry.py index d98cdee37..59e61f51a 100644 --- a/astrbot/core/utils/active_event_registry.py +++ b/astrbot/core/utils/active_event_registry.py @@ -8,9 +8,9 @@ if TYPE_CHECKING: class ActiveEventRegistry: - """维护 unified_msg_origin 到活跃事件的映射。 + """维护 unified_msg_origin 到活跃事件的映射。 - 用于在 reset 等场景下终止该会话正在处理的事件。 + 用于在 reset 等场景下终止该会话正在处理的事件。 """ def __init__(self) -> None: @@ -30,14 +30,14 @@ class ActiveEventRegistry: umo: str, exclude: AstrMessageEvent | None = None, ) -> int: - """终止指定 UMO 的所有活跃事件。 + """终止指定 UMO 的所有活跃事件。 Args: - umo: 统一消息来源标识符。 - exclude: 需要排除的事件(通常是发起 reset 的事件本身)。 + umo: 统一消息来源标识符。 + exclude: 需要排除的事件(通常是发起 reset 的事件本身)。 Returns: - 被终止的事件数量。 + 被终止的事件数量。 """ count = 0 for event in list(self._events.get(umo, [])): @@ -51,10 +51,10 @@ class ActiveEventRegistry: umo: str, exclude: AstrMessageEvent | None = None, ) -> int: - """请求停止指定 UMO 的所有活跃事件中的 Agent 运行。 + """请求停止指定 UMO 的所有活跃事件中的 Agent 运行。 - 与 stop_all 不同,这里不会调用 event.stop_event(), - 因此不会中断事件传播,后续流程(如历史记录保存)仍可继续。 + 与 stop_all 不同,这里不会调用 event.stop_event(), + 因此不会中断事件传播,后续流程(如历史记录保存)仍可继续。 """ count = 0 for event in list(self._events.get(umo, [])): diff --git a/astrbot/core/utils/astrbot_path.py b/astrbot/core/utils/astrbot_path.py index 951e414f7..6a8488731 100644 --- a/astrbot/core/utils/astrbot_path.py +++ b/astrbot/core/utils/astrbot_path.py @@ -1,16 +1,16 @@ """Astrbot统一路径获取 -项目路径:固定为源码所在路径 -根目录路径:默认为当前工作目录,可通过环境变量 ASTRBOT_ROOT 指定 -数据目录路径:固定为根目录下的 data 目录 -配置文件路径:固定为数据目录下的 config 目录 -插件目录路径:固定为数据目录下的 plugins 目录 -插件数据目录路径:固定为数据目录下的 plugin_data 目录 -T2I 模板目录路径:固定为数据目录下的 t2i_templates 目录 -WebChat 数据目录路径:固定为数据目录下的 webchat 目录 -临时文件目录路径:固定为数据目录下的 temp 目录 -Skills 目录路径:固定为数据目录下的 skills 目录 -第三方依赖目录路径:固定为数据目录下的 site-packages 目录 +项目路径:固定为源码所在路径 +根目录路径:默认为当前工作目录,可通过环境变量 ASTRBOT_ROOT 指定 +数据目录路径:固定为根目录下的 data 目录 +配置文件路径:固定为数据目录下的 config 目录 +插件目录路径:固定为数据目录下的 plugins 目录 +插件数据目录路径:固定为数据目录下的 plugin_data 目录 +T2I 模板目录路径:固定为数据目录下的 t2i_templates 目录 +WebChat 数据目录路径:固定为数据目录下的 webchat 目录 +临时文件目录路径:固定为数据目录下的 temp 目录 +Skills 目录路径:固定为数据目录下的 skills 目录 +第三方依赖目录路径:固定为数据目录下的 site-packages 目录 """ import os diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index b80c9f000..9ff19931f 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -97,7 +97,7 @@ async def download_image_by_url( await f.write(await resp.read()) return path except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): - # 关闭SSL验证(仅在证书验证失败时作为fallback) + # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( f"SSL certificate verification failed for {url}. " "Disabling SSL verification (CERT_NONE) as a fallback. " @@ -164,9 +164,9 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non end="", ) except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): - # 关闭SSL验证(仅在证书验证失败时作为fallback) + # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( - "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。" + "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。" ) logger.warning( f"SSL certificate verification failed for {url}. " @@ -218,7 +218,7 @@ def get_local_ip_addresses() -> list[IPv4Address | IPv6Address]: if addr.family == socket.AF_INET: network_ips.append(ip_address(addr.address)) elif addr.family == socket.AF_INET6: - # 过滤掉 IPv6 的 link-local 地址(fe80:...) + # 过滤掉 IPv6 的 link-local 地址(fe80:...) ip = ip_address(addr.address.split("%")[0]) # 处理带 zone index 的情况 if not ip.is_link_local: network_ips.append(ip) @@ -303,15 +303,15 @@ async def download_dashboard( try: with zipfile.ZipFile(str(cache_path), "r") as z: if z.testzip() is None: - logger.info("缓存文件校验通过,将直接使用缓存。") + logger.info("缓存文件校验通过,将直接使用缓存。") if str(cache_path) != str(zip_path): shutil.copy(str(cache_path), str(zip_path)) use_cache = True else: - logger.warning("缓存文件损坏,将重新下载。") + logger.warning("缓存文件损坏,将重新下载。") await cache_path.unlink() except zipfile.BadZipFile: - logger.warning("缓存文件损坏 (BadZipFile),将重新下载。") + logger.warning("缓存文件损坏 (BadZipFile),将重新下载。") await cache_path.unlink() if not use_cache: if latest or len(str(version)) != 40: @@ -342,7 +342,7 @@ async def download_dashboard( except Exception as e: if not latest: logger.warning( - f"下载指定版本({version})失败: {e},尝试下载最新版本。" + f"下载指定版本({version})失败: {e},尝试下载最新版本。" ) await download_dashboard( path=path, diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index 66ec110ad..b9f74c344 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -1,6 +1,6 @@ """媒体文件处理工具 -提供音视频格式转换、时长获取等功能。 +提供音视频格式转换、时长获取等功能。 """ import asyncio @@ -21,7 +21,7 @@ async def get_media_duration(file_path: str) -> int | None: file_path: 媒体文件路径 Returns: - 时长(毫秒),如果获取失败返回None + 时长(毫秒),如果获取失败返回None """ try: # 使用ffprobe获取时长 @@ -51,7 +51,7 @@ async def get_media_duration(file_path: str) -> int | None: except FileNotFoundError: logger.warning( - "[Media Utils] ffprobe未安装或不在PATH中,无法获取媒体时长。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffprobe未安装或不在PATH中,无法获取媒体时长。请安装ffmpeg: https://ffmpeg.org/" ) return None except Exception as e: @@ -64,7 +64,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) Args: audio_path: 原始音频文件路径 - output_path: 输出文件路径,如果为None则自动生成 + output_path: 输出文件路径,如果为None则自动生成 Returns: 转换后的opus文件路径 @@ -72,7 +72,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) Raises: Exception: 转换失败时抛出异常 """ - # 如果已经是opus格式,直接返回 + # 如果已经是opus格式,直接返回 if audio_path.lower().endswith(".opus"): return audio_path @@ -127,7 +127,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) except FileNotFoundError: logger.error( - "[Media Utils] ffmpeg未安装或不在PATH中,无法转换音频格式。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffmpeg未安装或不在PATH中,无法转换音频格式。请安装ffmpeg: https://ffmpeg.org/" ) raise Exception("ffmpeg not found") except Exception as e: @@ -142,8 +142,8 @@ async def convert_video_format( Args: video_path: 原始视频文件路径 - output_format: 目标格式,默认mp4 - output_path: 输出文件路径,如果为None则自动生成 + output_format: 目标格式,默认mp4 + output_path: 输出文件路径,如果为None则自动生成 Returns: 转换后的视频文件路径 @@ -151,7 +151,7 @@ async def convert_video_format( Raises: Exception: 转换失败时抛出异常 """ - # 如果已经是目标格式,直接返回 + # 如果已经是目标格式,直接返回 if video_path.lower().endswith(f".{output_format}"): return video_path @@ -204,7 +204,7 @@ async def convert_video_format( except FileNotFoundError: logger.error( - "[Media Utils] ffmpeg未安装或不在PATH中,无法转换视频格式。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffmpeg未安装或不在PATH中,无法转换视频格式。请安装ffmpeg: https://ffmpeg.org/" ) raise Exception("ffmpeg not found") except Exception as e: @@ -217,12 +217,12 @@ async def convert_audio_format( output_format: str = "amr", output_path: str | None = None, ) -> str: - """使用ffmpeg将音频转换为指定格式。 + """使用ffmpeg将音频转换为指定格式。 Args: audio_path: 原始音频文件路径 - output_format: 目标格式,例如 amr / ogg - output_path: 输出文件路径,如果为None则自动生成 + output_format: 目标格式,例如 amr / ogg + output_path: 输出文件路径,如果为None则自动生成 Returns: 转换后的音频文件路径 @@ -264,7 +264,7 @@ async def convert_audio_format( async def convert_audio_to_amr(audio_path: str, output_path: str | None = None) -> str: - """将音频转换为amr格式。""" + """将音频转换为amr格式。""" return await convert_audio_format( audio_path=audio_path, output_format="amr", @@ -273,7 +273,7 @@ async def convert_audio_to_amr(audio_path: str, output_path: str | None = None) async def convert_audio_to_wav(audio_path: str, output_path: str | None = None) -> str: - """将音频转换为wav格式。""" + """将音频转换为wav格式。""" return await convert_audio_format( audio_path=audio_path, output_format="wav", @@ -285,7 +285,7 @@ async def extract_video_cover( video_path: str, output_path: str | None = None, ) -> str: - """从视频中提取封面图(JPG)。""" + """从视频中提取封面图(JPG)。""" if output_path is None: temp_dir = anyio.Path(get_astrbot_temp_path()) await temp_dir.mkdir(parents=True, exist_ok=True) diff --git a/astrbot/core/utils/metrics.py b/astrbot/core/utils/metrics.py index 8fb146428..b7a702807 100644 --- a/astrbot/core/utils/metrics.py +++ b/astrbot/core/utils/metrics.py @@ -41,7 +41,7 @@ class Metric: @staticmethod async def upload(**kwargs) -> None: - """上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。 + """上传相关非敏感的指标以更好地了解 AstrBot 的使用情况。上传的指标不会包含任何有关消息文本、用户信息等敏感信息。 Powered by TickStats. """ diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 727f3762a..880d8c6eb 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -73,11 +73,11 @@ def log_connection_failure( if effective_proxy: logger.error( - f"[{provider_label}] 网络/代理连接失败 ({error_type})。" - f"代理地址: {effective_proxy},错误: {error}" + f"[{provider_label}] 网络/代理连接失败 ({error_type})。" + f"代理地址: {effective_proxy},错误: {error}" ) else: - logger.error(f"[{provider_label}] 网络连接失败 ({error_type})。错误: {error}") + logger.error(f"[{provider_label}] 网络连接失败 ({error_type})。错误: {error}") def create_proxy_client( diff --git a/astrbot/core/utils/path_util.py b/astrbot/core/utils/path_util.py index 9520d481d..f3ec8720d 100644 --- a/astrbot/core/utils/path_util.py +++ b/astrbot/core/utils/path_util.py @@ -4,7 +4,7 @@ from astrbot.core import logger def path_Mapping(mappings, srcPath: str) -> str: - """路径映射处理函数。尝试支援 Windows 和 Linux 的路径映射。 + """路径映射处理函数。尝试支援 Windows 和 Linux 的路径映射。 Args: mappings: 映射规则列表 srcPath: 原路径 @@ -16,24 +16,24 @@ def path_Mapping(mappings, srcPath: str) -> str: if len(rule) == 2: from_, to_ = mapping.split(":") elif len(rule) > 4 or len(rule) == 1: - # 切割后大于4个项目,或者只有1个项目,那肯定是错误的,只能是2,3,4个项目 + # 切割后大于4个项目,或者只有1个项目,那肯定是错误的,只能是2,3,4个项目 logger.warning(f"路径映射规则错误: {mapping}") continue # rule.len == 3 or 4 elif os.path.exists(rule[0] + ":" + rule[1]): - # 前面两个项目合并路径存在,说明是本地Window路径。后面一个或两个项目组成的路径本地大概率无法解析,直接拼接 + # 前面两个项目合并路径存在,说明是本地Window路径。后面一个或两个项目组成的路径本地大概率无法解析,直接拼接 from_ = rule[0] + ":" + rule[1] if len(rule) == 3: to_ = rule[2] else: to_ = rule[2] + ":" + rule[3] else: - # 前面两个项目合并路径不存在,说明第一个项目是本地Linux路径,后面一个或两个项目直接拼接。 + # 前面两个项目合并路径不存在,说明第一个项目是本地Linux路径,后面一个或两个项目直接拼接。 from_ = rule[0] if len(rule) == 3: to_ = rule[1] + ":" + rule[2] else: - # 这种情况下存在四个项目,说明规则也是错误的 + # 这种情况下存在四个项目,说明规则也是错误的 logger.warning(f"路径映射规则错误: {mapping}") continue @@ -52,7 +52,7 @@ def path_Mapping(mappings, srcPath: str) -> str: else: has_replaced_processed = False if srcPath.startswith("."): - # 相对路径处理。如果是相对路径,可能是Linux路径,也可能是Windows路径 + # 相对路径处理。如果是相对路径,可能是Linux路径,也可能是Windows路径 sign = srcPath[1] # 处理两个点的情况 if sign == ".": @@ -64,7 +64,7 @@ def path_Mapping(mappings, srcPath: str) -> str: srcPath = srcPath.replace("/", "\\") has_replaced_processed = True if not has_replaced_processed: - # 如果不是相对路径或不能处理,默认按照Linux路径处理 + # 如果不是相对路径或不能处理,默认按照Linux路径处理 srcPath = srcPath.replace("\\", "/") logger.info(f"路径映射: {url} -> {srcPath}") return srcPath diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index 2ef33331f..b6c71af23 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -459,22 +459,22 @@ def _classify_pip_failure(output_lines: list[str]) -> DependencyConflictError | detail = ( " 冲突详情: " f"{_normalize_conflict_detail_line(context.requested_lines[0])} vs " - f"{_normalize_conflict_detail_line(context.constraint_lines[0])}。" + f"{_normalize_conflict_detail_line(context.constraint_lines[0])}。" ) elif len(context.dependency_detail_lines) >= 2: detail = ( " 冲突详情: " f"{_normalize_conflict_detail_line(context.dependency_detail_lines[0])} vs " - f"{_normalize_conflict_detail_line(context.dependency_detail_lines[1])}。" + f"{_normalize_conflict_detail_line(context.dependency_detail_lines[1])}。" ) if is_core_conflict: message = ( - f"检测到核心依赖版本保护冲突。{detail}插件要求的依赖版本与 AstrBot 核心不兼容," - "为了系统稳定,已阻止该降级行为。请联系插件作者或调整 requirements.txt。" + f"检测到核心依赖版本保护冲突。{detail}插件要求的依赖版本与 AstrBot 核心不兼容," + "为了系统稳定,已阻止该降级行为。请联系插件作者或调整 requirements.txt。" ) else: - message = f"检测到依赖冲突。{detail}" + message = f"检测到依赖冲突。{detail}" return DependencyConflictError( message, @@ -517,7 +517,7 @@ def _collect_candidate_modules( canonical_name = _canonicalize_distribution_name(distribution_name) by_name.setdefault(canonical_name, []).append(distribution) except Exception as exc: - logger.warning("读取 site-packages 元数据失败,使用回退模块名: %s", exc) + logger.warning("读取 site-packages 元数据失败,使用回退模块名: %s", exc) expanded_requirement_names: set[str] = set() pending = deque(requirement_names) @@ -580,7 +580,7 @@ def _ensure_preferred_modules( if unresolved_modules: conflict_message = ( - "检测到插件依赖与当前运行时发生冲突,无法安全加载该插件。" + "检测到插件依赖与当前运行时发生冲突,无法安全加载该插件。" f"冲突模块: {', '.join(unresolved_modules)}" ) raise RuntimeError(conflict_message) @@ -987,7 +987,7 @@ class PipInstaller: package_name, requirements_path, mirror ) if not args: - logger.info("Pip 包管理器跳过安装:未提供有效的包名或 requirements 文件。") + logger.info("Pip 包管理器跳过安装:未提供有效的包名或 requirements 文件。") return target_site_packages = None @@ -1026,7 +1026,7 @@ class PipInstaller: importlib.invalidate_caches() def prefer_installed_dependencies(self, requirements_path: str) -> None: - """优先使用已安装在插件 site-packages 中的依赖,不执行安装。""" + """优先使用已安装在插件 site-packages 中的依赖,不执行安装。""" if not is_packaged_desktop_runtime(): return @@ -1069,4 +1069,4 @@ class PipInstaller: async def _run_pip_with_classification(self, args: list[str]) -> None: result_code = await self._run_pip_in_process(args) if result_code != 0: - raise PipInstallError(f"安装失败,错误码:{result_code}", code=result_code) + raise PipInstallError(f"安装失败,错误码:{result_code}", code=result_code) diff --git a/astrbot/core/utils/requirements_utils.py b/astrbot/core/utils/requirements_utils.py index f2c749db1..630e7ae29 100644 --- a/astrbot/core/utils/requirements_utils.py +++ b/astrbot/core/utils/requirements_utils.py @@ -253,7 +253,7 @@ def _iter_requirement_lines( resolved_path = os.path.realpath(requirements_path) if resolved_path in visited: logger.warning( - "检测到循环依赖的 requirements 包含: %s,将跳过该文件", resolved_path + "检测到循环依赖的 requirements 包含: %s,将跳过该文件", resolved_path ) return visited.add(resolved_path) @@ -304,7 +304,7 @@ def extract_requirement_names(requirements_path: str) -> set[str]: name for name, _ in iter_requirements(requirements_path=requirements_path) } except Exception as exc: - logger.warning("读取依赖文件失败,跳过冲突检测: %s", exc) + logger.warning("读取依赖文件失败,跳过冲突检测: %s", exc) return set() @@ -335,7 +335,7 @@ def collect_installed_distribution_versions(paths: list[str]) -> dict[str, str] continue installed.setdefault(distribution_name, version) except Exception as exc: - logger.warning("读取已安装依赖失败,跳过缺失依赖预检查: %s", exc) + logger.warning("读取已安装依赖失败,跳过缺失依赖预检查: %s", exc) return None return installed @@ -347,7 +347,7 @@ def _load_requirement_lines_for_precheck( requirement_lines = list(_iter_requirement_lines(requirements_path)) except Exception as exc: logger.warning( - "预检查缺失依赖失败,将回退到完整安装: %s (%s)", + "预检查缺失依赖失败,将回退到完整安装: %s (%s)", requirements_path, exc, ) @@ -372,7 +372,7 @@ def _load_requirement_lines_for_precheck( ) if fallback_line is not None: logger.info( - "缺失依赖预检查发现无法安全裁剪的 option/direct-reference 行,将回退到完整安装: %s (%s)", + "缺失依赖预检查发现无法安全裁剪的 option/direct-reference 行,将回退到完整安装: %s (%s)", requirements_path, fallback_line, ) @@ -426,7 +426,7 @@ def build_missing_requirements_install_lines( if parsed is None: if looks_like_direct_reference(line) or line.startswith(("-", "--")): logger.debug( - "缺失依赖行筛选回退到完整安装:requirements 中包含无法安全裁剪的 option/direct-reference 行: %s (%s)", + "缺失依赖行筛选回退到完整安装:requirements 中包含无法安全裁剪的 option/direct-reference 行: %s (%s)", requirements_path, line, ) @@ -462,7 +462,7 @@ def plan_missing_requirements_install( return None if missing and not install_lines: logger.warning( - "预检查缺失依赖成功,但无法映射到可安装 requirement 行,将回退到完整安装: %s -> %s", + "预检查缺失依赖成功,但无法映射到可安装 requirement 行,将回退到完整安装: %s -> %s", requirements_path, sorted(missing), ) diff --git a/astrbot/core/utils/session_waiter.py b/astrbot/core/utils/session_waiter.py index c524aa8f2..9cb8743dd 100644 --- a/astrbot/core/utils/session_waiter.py +++ b/astrbot/core/utils/session_waiter.py @@ -41,8 +41,8 @@ class SessionController: """保持这个会话 Args: - timeout (float): 必填。会话超时时间。 - 当 reset_timeout 设置为 True 时, 代表重置超时时间, timeout 必须 > 0, 如果 <= 0 则立即结束会话。 + timeout (float): 必填。会话超时时间。 + 当 reset_timeout 设置为 True 时, 代表重置超时时间, timeout 必须 > 0, 如果 <= 0 则立即结束会话。 当 reset_timeout 设置为 False 时, 代表继续维持原来的超时时间, 新 timeout = 原来剩余的timeout + timeout (可以 < 0) """ @@ -97,7 +97,7 @@ class SessionFilter: class DefaultSessionFilter(SessionFilter): def filter(self, event: AstrMessageEvent) -> str: - """默认实现,返回统一消息来源字符串作为会话标识符""" + """默认实现,返回统一消息来源字符串作为会话标识符""" return event.unified_msg_origin @@ -188,10 +188,10 @@ class SessionWaiter: def session_waiter(timeout: int = 30, record_history_chains: bool = False): - """装饰器:自动将函数注册为 SessionWaiter 处理函数,并等待外部输入触发执行。 + """装饰器:自动将函数注册为 SessionWaiter 处理函数,并等待外部输入触发执行。 - :param timeout: 超时时间(秒) - :param record_history_chain: 是否自动记录历史消息链。可以通过 controller.get_history_chains() 获取。深拷贝。 + :param timeout: 超时时间(秒) + :param record_history_chain: 是否自动记录历史消息链。可以通过 controller.get_history_chains() 获取。深拷贝。 """ def decorator( diff --git a/astrbot/core/utils/shared_preferences.py b/astrbot/core/utils/shared_preferences.py index 344808cbd..7a8e6eb8d 100644 --- a/astrbot/core/utils/shared_preferences.py +++ b/astrbot/core/utils/shared_preferences.py @@ -62,7 +62,7 @@ class SharedPreferences: key: str | None = None, ) -> list[Preference]: """获取指定范围的偏好设置 - Note: 返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。scope_id 和 key 可以为 None,这时返回该范围下所有的偏好设置。 + Note: 返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。scope_id 和 key 可以为 None,这时返回该范围下所有的偏好设置。 """ ret = await self.db_helper.get_preferences(scope, scope_id, key) return ret @@ -107,7 +107,7 @@ class SharedPreferences: ) -> _VT | list[Preference]: """获取会话范围的偏好设置 - Note: 当 umo 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。 + Note: 当 umo 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。 """ if umo is None or key is None: return await self.range_get_async("umo", umo, key) @@ -126,7 +126,7 @@ class SharedPreferences: ) -> _VT | list[Preference]: """获取全局范围的偏好设置 - Note: 当 scope_id 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。 + Note: 当 scope_id 或者 key 为 None,时,返回 Preference 列表,其中的 value 属性是一个 dict,value["val"] 为值。 """ if key is None: return await self.range_get_async("global", "global", key) @@ -173,7 +173,7 @@ class SharedPreferences: scope: str | None = None, scope_id: str | None = "", ) -> _VT: - """获取偏好设置(已弃用)""" + """获取偏好设置(已弃用)""" if scope_id == "": scope_id = "unknown" if scope_id is None or key is None: @@ -194,7 +194,7 @@ class SharedPreferences: scope_id: str | None = None, key: str | None = None, ) -> list[Preference]: - """获取指定范围的偏好设置(已弃用)""" + """获取指定范围的偏好设置(已弃用)""" result = asyncio.run_coroutine_threadsafe( self.range_get_async(scope, scope_id, key), self._sync_loop, @@ -205,7 +205,7 @@ class SharedPreferences: def put( self, key, value, scope: str | None = None, scope_id: str | None = None ) -> None: - """设置偏好设置(已弃用)""" + """设置偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( self.put_async(scope or "unknown", scope_id or "unknown", key, value), self._sync_loop, @@ -214,14 +214,14 @@ class SharedPreferences: def remove( self, key, scope: str | None = None, scope_id: str | None = None ) -> None: - """删除偏好设置(已弃用)""" + """删除偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( self.remove_async(scope or "unknown", scope_id or "unknown", key), self._sync_loop, ).result() def clear(self, scope: str | None = None, scope_id: str | None = None) -> None: - """清空偏好设置(已弃用)""" + """清空偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( self.clear_async(scope or "unknown", scope_id or "unknown"), self._sync_loop, diff --git a/astrbot/core/utils/t2i/local_strategy.py b/astrbot/core/utils/t2i/local_strategy.py index 2fa235129..7d0398af7 100644 --- a/astrbot/core/utils/t2i/local_strategy.py +++ b/astrbot/core/utils/t2i/local_strategy.py @@ -15,13 +15,13 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path class FontManager: - """字体管理类,负责加载和缓存字体""" + """字体管理类,负责加载和缓存字体""" _font_cache = {} @classmethod def get_font(cls, size: int) -> ImageFont.FreeTypeFont|ImageFont.ImageFont: - """获取指定大小的字体,优先从缓存获取""" + """获取指定大小的字体,优先从缓存获取""" if size in cls._font_cache: return cls._font_cache[size] @@ -53,10 +53,10 @@ class FontManager: except Exception: continue - # 如果所有字体都失败,使用默认字体 + # 如果所有字体都失败,使用默认字体 try: default_font = ImageFont.load_default() - # PIL默认字体大小固定,这里不缓存 + # PIL默认字体大小固定,这里不缓存 return default_font except Exception: raise RuntimeError("无法加载任何字体") @@ -69,7 +69,7 @@ class TextMeasurer: def get_text_size(text: str, font: ImageFont.FreeTypeFont|ImageFont.ImageFont) -> tuple[int, int]: """获取文本的尺寸""" - # 依赖库Pillow>=11.2.1,不再需要考虑<9.0.0 + # 依赖库Pillow>=11.2.1,不再需要考虑<9.0.0 left, top, right, bottom = font.getbbox("Hello world") return int(right - left), int(bottom - top) @@ -77,14 +77,14 @@ class TextMeasurer: def split_text_to_fit_width( text: str, font: ImageFont.FreeTypeFont|ImageFont.ImageFont, max_width: int ) -> list[str]: - """将文本拆分为多行,确保每行不超过指定宽度""" + """将文本拆分为多行,确保每行不超过指定宽度""" lines = [] if not text: return lines remaining_text = text while remaining_text: - # 如果文本宽度小于最大宽度,直接添加 + # 如果文本宽度小于最大宽度,直接添加 text_width = TextMeasurer.get_text_size(remaining_text, font)[0] if text_width <= max_width: lines.append(remaining_text) @@ -98,7 +98,7 @@ class TextMeasurer: remaining_text = remaining_text[i:] break else: - # 如果单个字符都放不下,强制放一个字符 + # 如果单个字符都放不下,强制放一个字符 lines.append(remaining_text[0]) remaining_text = remaining_text[1:] @@ -126,7 +126,7 @@ class MarkdownElement(ABC): image_width: int, font_size: int, ) -> int: - """渲染元素到图像,返回新的y坐标""" + """渲染元素到图像,返回新的y坐标""" pass @@ -186,7 +186,7 @@ class BoldTextElement(MarkdownElement): image_width: int, font_size: int, ) -> int: - # 尝试使用粗体字体,如果没有则绘制两次模拟粗体效果 + # 尝试使用粗体字体,如果没有则绘制两次模拟粗体效果 try: bold_fonts = [ "msyhbd.ttc", # 微软雅黑粗体 (Windows) @@ -210,7 +210,7 @@ class BoldTextElement(MarkdownElement): draw.text((x, y), line, font=bold_font, fill=(0, 0, 0)) y += font_size + 8 else: - # 如果没有粗体字体,则绘制两次文本轻微偏移以模拟粗体 + # 如果没有粗体字体,则绘制两次文本轻微偏移以模拟粗体 font = FontManager.get_font(font_size) lines = TextMeasurer.split_text_to_fit_width( self.content, font, image_width - 20 @@ -220,7 +220,7 @@ class BoldTextElement(MarkdownElement): draw.text((x + 1, y), line, font=font, fill=(0, 0, 0)) y += font_size + 8 except Exception: - # 兜底方案:使用普通字体 + # 兜底方案:使用普通字体 font = FontManager.get_font(font_size) lines = TextMeasurer.split_text_to_fit_width( self.content, font, image_width - 20 @@ -251,7 +251,7 @@ class ItalicTextElement(MarkdownElement): image_width: int, font_size: int, ) -> int: - # 尝试使用斜体字体,如果没有则使用倾斜变换模拟斜体效果 + # 尝试使用斜体字体,如果没有则使用倾斜变换模拟斜体效果 try: italic_fonts = [ "msyhi.ttc", # 微软雅黑斜体 (Windows) @@ -275,7 +275,7 @@ class ItalicTextElement(MarkdownElement): draw.text((x, y), line, font=italic_font, fill=(0, 0, 0)) y += font_size + 8 else: - # 如果没有斜体字体,使用变换 + # 如果没有斜体字体,使用变换 font = FontManager.get_font(font_size) lines = TextMeasurer.split_text_to_fit_width( self.content, font, image_width - 20 @@ -290,7 +290,7 @@ class ItalicTextElement(MarkdownElement): text_draw = ImageDraw.Draw(text_img) text_draw.text((0, 0), line, font=font, fill=(0, 0, 0, 255)) - # 倾斜变换,使用仿射变换实现斜体效果 + # 倾斜变换,使用仿射变换实现斜体效果 # 变换矩阵: [1, 0.2, 0, 0, 1, 0] italic_img = text_img.transform( text_img.size, Image.Transform.AFFINE, (1, 0.2, 0, 0, 1, 0), Image.Resampling.BICUBIC @@ -300,7 +300,7 @@ class ItalicTextElement(MarkdownElement): image.paste(italic_img, (x, y), italic_img) y += font_size + 8 except Exception: - # 兜底方案:使用普通字体 + # 兜底方案:使用普通字体 font = FontManager.get_font(font_size) lines = TextMeasurer.split_text_to_fit_width( self.content, font, image_width - 20 @@ -696,7 +696,7 @@ class ImageElement(MarkdownElement): class MarkdownParser: - """Markdown解析器,将文本解析为元素""" + """Markdown解析器,将文本解析为元素""" @staticmethod async def parse(text: str) -> list[MarkdownElement]: @@ -748,7 +748,7 @@ class MarkdownParser: elements.append(CodeBlockElement(code_lines)) continue - # 检查行内样式(粗体、斜体、下划线、删除线、行内代码) + # 检查行内样式(粗体、斜体、下划线、删除线、行内代码) if re.search( r"(\*\*.*?\*\*)|(\*.*?\*)|(__.*?__)|(_.*?_)|(~~.*?~~)|(`.*?`)", line ): @@ -788,7 +788,7 @@ class MarkdownParser: # 按开始位置排序 markers.sort(key=lambda x: x["start"]) - # 如果没有找到任何匹配,直接添加为普通文本 + # 如果没有找到任何匹配,直接添加为普通文本 if not markers: elements.append(TextElement(line)) i += 1 @@ -835,7 +835,7 @@ class MarkdownParser: class MarkdownRenderer: - """Markdown渲染器,将元素渲染为图像""" + """Markdown渲染器,将元素渲染为图像""" def __init__( self, @@ -870,7 +870,7 @@ class MarkdownRenderer: y = element.render(image, draw, 10, y, self.width, self.font_size) # 添加页脚 - # 克莱因蓝色,近似RGB为(0, 47, 167) + # 克莱因蓝色,近似RGB为(0, 47, 167) klein_blue = (0, 47, 167) # 灰色 grey_color = (130, 130, 130) @@ -891,12 +891,12 @@ class MarkdownRenderer: footer_y = total_height - footer_height - # 绘制"Powered by "(灰色) + # 绘制"Powered by "(灰色) draw.text( (x_start, footer_y), powered_by_text, font=footer_font, fill=grey_color ) - # 绘制"AstrBot"(克莱因蓝) + # 绘制"AstrBot"(克莱因蓝) draw.text( (x_start + powered_by_width, footer_y), astrbot_text, diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 53d9441fa..241d5b155 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -36,7 +36,7 @@ class NetworkRenderStrategy(RenderStrategy): return self.template_manager.get_template(name) async def get_official_endpoints(self) -> None: - """获取官方的 t2i 端点列表。""" + """获取官方的 t2i 端点列表。""" try: async with aiohttp.ClientSession( trust_env=True, diff --git a/astrbot/core/utils/t2i/renderer.py b/astrbot/core/utils/t2i/renderer.py index e3118d7e8..45093cfe1 100644 --- a/astrbot/core/utils/t2i/renderer.py +++ b/astrbot/core/utils/t2i/renderer.py @@ -21,14 +21,14 @@ class HtmlRenderer: return_url: bool = False, options: dict | None = None, ): - """使用自定义文转图模板。该方法会通过网络调用 t2i 终结点图文渲染API。 - @param tmpl_str: HTML Jinja2 模板。 - @param tmpl_data: jinja2 模板数据。 - @param options: 渲染选项。 + """使用自定义文转图模板。该方法会通过网络调用 t2i 终结点图文渲染API。 + @param tmpl_str: HTML Jinja2 模板。 + @param tmpl_data: jinja2 模板数据。 + @param options: 渲染选项。 - @return: 图片 URL 或者文件路径,取决于 return_url 参数。 + @return: 图片 URL 或者文件路径,取决于 return_url 参数。 - @example: 参见 https://astrbot.app 插件开发部分。 + @example: 参见 https://astrbot.app 插件开发部分。 """ return await self.network_strategy.render_custom_template( tmpl_str, @@ -44,7 +44,7 @@ class HtmlRenderer: return_url: bool = False, template_name: str | None = None, ): - """使用默认文转图模板。""" + """使用默认文转图模板。""" if use_network: try: return await self.network_strategy.render( diff --git a/astrbot/core/utils/t2i/template_manager.py b/astrbot/core/utils/t2i/template_manager.py index b3eb0c9ff..5172b7217 100644 --- a/astrbot/core/utils/t2i/template_manager.py +++ b/astrbot/core/utils/t2i/template_manager.py @@ -7,9 +7,9 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_p class TemplateManager: - """负责管理 t2i HTML 模板的 CRUD 和重置操作。 - 采用“用户覆盖内置”策略:用户模板存储在 data 目录中,并优先于内置模板加载。 - 所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。 + """负责管理 t2i HTML 模板的 CRUD 和重置操作。 + 采用“用户覆盖内置”策略:用户模板存储在 data 目录中,并优先于内置模板加载。 + 所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。 """ CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"] @@ -29,7 +29,7 @@ class TemplateManager: self._initialize_user_templates() def _copy_core_templates(self, overwrite: bool = False) -> None: - """从内置目录复制核心模板到用户目录。""" + """从内置目录复制核心模板到用户目录。""" for filename in self.CORE_TEMPLATES: src = os.path.join(self.builtin_template_dir, filename) dst = os.path.join(self.user_template_dir, filename) @@ -37,23 +37,23 @@ class TemplateManager: shutil.copyfile(src, dst) def _initialize_user_templates(self) -> None: - """如果用户目录下缺少核心模板,则进行复制。""" + """如果用户目录下缺少核心模板,则进行复制。""" self._copy_core_templates(overwrite=False) def _get_user_template_path(self, name: str) -> str: - """获取用户模板的完整路径,防止路径遍历漏洞。""" + """获取用户模板的完整路径,防止路径遍历漏洞。""" if ".." in name or "/" in name or "\\" in name: - raise ValueError("模板名称包含非法字符。") + raise ValueError("模板名称包含非法字符。") return os.path.join(self.user_template_dir, f"{name}.html") def _read_file(self, path: str) -> str: - """读取文件内容。""" + """读取文件内容。""" with open(path, encoding="utf-8") as f: return f.read() def list_templates(self) -> list[dict]: - """列出所有可用模板。 - 该列表是内置模板和用户模板的合并视图,用户模板将覆盖同名的内置模板。 + """列出所有可用模板。 + 该列表是内置模板和用户模板的合并视图,用户模板将覆盖同名的内置模板。 """ dirs_to_scan = [self.builtin_template_dir, self.user_template_dir] all_names = { @@ -67,8 +67,8 @@ class TemplateManager: ] def get_template(self, name: str) -> str: - """获取指定模板的内容。 - 优先从用户目录加载,如果不存在则回退到内置目录。 + """获取指定模板的内容。 + 优先从用户目录加载,如果不存在则回退到内置目录。 """ user_path = self._get_user_template_path(name) if os.path.exists(user_path): @@ -78,34 +78,34 @@ class TemplateManager: if os.path.exists(builtin_path): return self._read_file(builtin_path) - raise FileNotFoundError("模板不存在。") + raise FileNotFoundError("模板不存在。") def create_template(self, name: str, content: str) -> None: - """在用户目录中创建一个新的模板文件。""" + """在用户目录中创建一个新的模板文件。""" path = self._get_user_template_path(name) if os.path.exists(path): - raise FileExistsError("同名模板已存在。") + raise FileExistsError("同名模板已存在。") with open(path, "w", encoding="utf-8") as f: f.write(content) def update_template(self, name: str, content: str) -> None: - """更新一个模板。此操作始终写入用户目录。 - 如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本, - 从而实现对内置模板的“覆盖”。 + """更新一个模板。此操作始终写入用户目录。 + 如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本, + 从而实现对内置模板的“覆盖”。 """ path = self._get_user_template_path(name) with open(path, "w", encoding="utf-8") as f: f.write(content) def delete_template(self, name: str) -> None: - """仅删除用户目录中的模板文件。 - 如果删除的是一个覆盖了内置模板的用户模板,这将有效地“恢复”到内置版本。 + """仅删除用户目录中的模板文件。 + 如果删除的是一个覆盖了内置模板的用户模板,这将有效地“恢复”到内置版本。 """ path = self._get_user_template_path(name) if not os.path.exists(path): - raise FileNotFoundError("用户模板不存在,无法删除。") + raise FileNotFoundError("用户模板不存在,无法删除。") os.remove(path) def reset_default_template(self) -> None: - """将核心模板从内置目录强制重置到用户目录。""" + """将核心模板从内置目录强制重置到用户目录。""" self._copy_core_templates(overwrite=True) diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index df4801185..d0dd5c747 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -38,7 +38,7 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int: import pilk except (ImportError, ModuleNotFoundError) as _: raise Exception( - "pilk 模块未安装,请前往管理面板->平台日志->安装pip库 安装 pilk 这个库", + "pilk 模块未安装,请前往管理面板->平台日志->安装pip库 安装 pilk 这个库", ) # with wave.open(wav_path, 'rb') as wav: # wav_data = wav.readframes(wav.getnframes()) @@ -63,8 +63,8 @@ async def wav_to_tencent_silk(wav_path: str, output_path: str) -> int: async def convert_to_pcm_wav(input_path: str, output_path: str) -> str: - """将 MP3 或其他音频格式转换为 PCM 16bit WAV,采样率24000Hz,单声道。 - 若转换失败则抛出异常。 + """将 MP3 或其他音频格式转换为 PCM 16bit WAV,采样率24000Hz,单声道。 + 若转换失败则抛出异常。 """ try: from pyffmpeg import FFmpeg @@ -108,14 +108,14 @@ async def convert_to_pcm_wav(input_path: str, output_path: str) -> str: async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]: - """将 MP3/WAV 文件转为 Tencent Silk 并返回 base64 编码与时长(秒)。 + """将 MP3/WAV 文件转为 Tencent Silk 并返回 base64 编码与时长(秒)。 参数: - - audio_path: 输入音频文件路径(.mp3 或 .wav) + - audio_path: 输入音频文件路径(.mp3 或 .wav) 返回: - silk_b64: Base64 编码的 Silk 字符串 - - duration: 音频时长(秒) + - duration: 音频时长(秒) """ try: import pilk diff --git a/astrbot/core/utils/version_comparator.py b/astrbot/core/utils/version_comparator.py index 4ad2da10e..79248b860 100644 --- a/astrbot/core/utils/version_comparator.py +++ b/astrbot/core/utils/version_comparator.py @@ -4,11 +4,11 @@ import re class VersionComparator: @staticmethod def compare_version(v1: str, v2: str) -> int: - """根据 Semver 语义版本规范来比较版本号的大小。支持不仅局限于 3 个数字的版本号,并处理预发布标签。 + """根据 Semver 语义版本规范来比较版本号的大小。支持不仅局限于 3 个数字的版本号,并处理预发布标签。 参考: https://semver.org/lang/zh-CN/ - 返回 1 表示 v1 > v2,返回 -1 表示 v1 < v2,返回 0 表示 v1 = v2。 + 返回 1 表示 v1 > v2,返回 -1 表示 v1 < v2,返回 0 表示 v1 = v2。 """ v1 = v1.lower().replace("v", "") v2 = v2.lower().replace("v", "") diff --git a/astrbot/core/utils/webhook_utils.py b/astrbot/core/utils/webhook_utils.py index b0c27888f..1d1e35955 100644 --- a/astrbot/core/utils/webhook_utils.py +++ b/astrbot/core/utils/webhook_utils.py @@ -73,7 +73,7 @@ def ensure_platform_webhook_config(platform_cfg: dict) -> bool: platform_cfg (dict): 平台配置字典 Returns: - bool: 如果生成了 webhook_uuid 则返回 True,否则返回 False + bool: 如果生成了 webhook_uuid 则返回 True,否则返回 False """ pt = platform_cfg.get("type", "") if pt in WEBHOOK_SUPPORTED_PLATFORMS and not platform_cfg.get("webhook_uuid"): diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index 6cea6b38d..d4bb0c6a7 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -38,16 +38,16 @@ class RepoZipUpdator: self.rm_on_error = on_error async def fetch_release_info(self, url: str, latest: bool = True) -> list: - """请求版本信息。 - 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 + """请求版本信息。 + 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 """ try: ssl_context = ssl.create_default_context( cafile=certifi.where(), - ) # 新增:创建基于 certifi 的 SSL 上下文 + ) # 新增:创建基于 certifi 的 SSL 上下文 connector = aiohttp.TCPConnector( ssl=ssl_context, - ) # 新增:使用 TCPConnector 指定 SSL 上下文 + ) # 新增:使用 TCPConnector 指定 SSL 上下文 async with ( aiohttp.ClientSession( trust_env=True, @@ -59,9 +59,9 @@ class RepoZipUpdator: if response.status != 200: text = await response.text() logger.error( - f"请求 {url} 失败,状态码: {response.status}, 内容: {text}", + f"请求 {url} 失败,状态码: {response.status}, 内容: {text}", ) - raise Exception(f"请求失败,状态码: {response.status}") + raise Exception(f"请求失败,状态码: {response.status}") result = await response.json() if not result: return [] @@ -86,8 +86,8 @@ class RepoZipUpdator: return ret def github_api_release_parser(self, releases: list) -> list: - """解析 GitHub API 返回的 releases 信息。 - 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 + """解析 GitHub API 返回的 releases 信息。 + 返回一个列表,每个元素是一个字典,包含版本号、发布时间、更新内容、commit hash等信息。 """ ret = [] for release in releases: @@ -126,7 +126,7 @@ class RepoZipUpdator: sel_release_data = update_data[0] else: for data in update_data: - # 跳过带有 alpha、beta 等预发布标签的版本 + # 跳过带有 alpha、beta 等预发布标签的版本 if re.search( r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$", data["tag_name"], @@ -167,11 +167,11 @@ class RepoZipUpdator: releases = await self.fetch_release_info(url=release_url) except Exception as e: logger.warning( - f"获取 {author}/{repo} 的 GitHub Releases 失败: {e},将尝试下载默认分支", + f"获取 {author}/{repo} 的 GitHub Releases 失败: {e},将尝试下载默认分支", ) releases = [] if not releases: - # 如果没有最新版本,下载默认分支 + # 如果没有最新版本,下载默认分支 logger.info(f"正在从默认分支下载 {author}/{repo}") release_url = ( f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip" @@ -183,15 +183,15 @@ class RepoZipUpdator: proxy = proxy.rstrip("/") release_url = f"{proxy}/{release_url}" logger.info( - f"检查到设置了镜像站,将使用镜像站下载 {author}/{repo} 仓库源码: {release_url}", + f"检查到设置了镜像站,将使用镜像站下载 {author}/{repo} 仓库源码: {release_url}", ) await download_file(release_url, target_path + ".zip") def parse_github_url(self, url: str): - """使用正则表达式解析 GitHub 仓库 URL,支持 `.git` 后缀和 `tree/branch` 结构 + """使用正则表达式解析 GitHub 仓库 URL,支持 `.git` 后缀和 `tree/branch` 结构 Returns: - tuple[str, str, str]: 返回作者名、仓库名和分支名 + tuple[str, str, str]: 返回作者名、仓库名和分支名 Raises: ValueError: 如果 URL 格式不正确 """ @@ -232,7 +232,7 @@ class RepoZipUpdator: os.remove(zip_path) except BaseException: logger.warning( - f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}", + f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}", ) def format_name(self, name: str) -> str: diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index f4fa3a429..a57a64257 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -41,10 +41,10 @@ class AuthRoute(Route): and not DEMO_MODE ): change_pwd_hint = True - logger.warning("为了保证安全,请尽快修改默认密码。") + logger.warning("为了保证安全,请尽快修改默认密码。") - # 自动迁移:如果后端当前存储的是 legacy hex(MD5 或 SHA256)的 digest, - # 且客户端此次提交了明文密码并且明文能通过校验,则将其替换为安全哈希(argon2 或 pbkdf2 回退)。 + # 自动迁移:如果后端当前存储的是 legacy hex(MD5 或 SHA256)的 digest, + # 且客户端此次提交了明文密码并且明文能通过校验,则将其替换为安全哈希(argon2 或 pbkdf2 回退)。 try: pwd_plain = str(post_data.get("password", "") or "") s = str(stored_password_hash or "").strip().lower() @@ -59,14 +59,14 @@ class AuthRoute(Route): try: new_hash = hash_dashboard_password_secure(pwd_plain) self.config["dashboard"]["password"] = new_hash - # 保存到配置文件;如果保存失败只是记录警告但不阻止登录流程 + # 保存到配置文件;如果保存失败只是记录警告但不阻止登录流程 try: self.config.save_config() - logger.info("已将旧版密码迁移为安全哈希格式。") + logger.info("已将旧版密码迁移为安全哈希格式。") except Exception: - logger.warning("密码迁移:保存配置失败,迁移未持久化。") + logger.warning("密码迁移:保存配置失败,迁移未持久化。") except Exception as e: - logger.warning(f"密码迁移失败(生成哈希时出错):{e}") + logger.warning(f"密码迁移失败(生成哈希时出错):{e}") except Exception: logger.exception("密码迁移过程中发生意外错误") diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index 232156b18..5c73f6644 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -29,11 +29,11 @@ from .route import Response, Route, RouteContext # 分片上传常量 CHUNK_SIZE = 1024 * 1024 # 1MB -UPLOAD_EXPIRE_SECONDS = 3600 # 上传会话过期时间(1小时) +UPLOAD_EXPIRE_SECONDS = 3600 # 上传会话过期时间(1小时) def secure_filename(filename: str) -> str: - """清洗文件名,移除路径遍历字符和危险字符 + """清洗文件名,移除路径遍历字符和危险字符 Args: filename: 原始文件名 @@ -41,21 +41,21 @@ def secure_filename(filename: str) -> str: Returns: 安全的文件名 """ - # 跨平台处理:先将反斜杠替换为正斜杠,再取文件名 + # 跨平台处理:先将反斜杠替换为正斜杠,再取文件名 filename = filename.replace("\\", "/") - # 仅保留文件名部分,移除路径 + # 仅保留文件名部分,移除路径 filename = os.path.basename(filename) # 替换路径遍历字符 filename = filename.replace("..", "_") - # 仅保留字母、数字、下划线、连字符、点 + # 仅保留字母、数字、下划线、连字符、点 filename = re.sub(r"[^\w\-.]", "_", filename) - # 移除前导点(隐藏文件)和尾部点 + # 移除前导点(隐藏文件)和尾部点 filename = filename.strip(".") - # 如果文件名为空或只包含下划线,生成一个默认名称 + # 如果文件名为空或只包含下划线,生成一个默认名称 if not filename or filename.replace("_", "") == "": filename = "backup" @@ -63,13 +63,13 @@ def secure_filename(filename: str) -> str: def generate_unique_filename(original_filename: str) -> str: - """生成唯一的文件名,在原文件名后添加时间戳后缀避免重名 + """生成唯一的文件名,在原文件名后添加时间戳后缀避免重名 Args: - original_filename: 原始文件名(已清洗) + original_filename: 原始文件名(已清洗) Returns: - 添加了时间戳后缀的唯一文件名,格式为 {原文件名}_{YYYYMMDD_HHMMSS}.{扩展名} + 添加了时间戳后缀的唯一文件名,格式为 {原文件名}_{YYYYMMDD_HHMMSS}.{扩展名} """ name, ext = os.path.splitext(original_filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -79,7 +79,7 @@ def generate_unique_filename(original_filename: str) -> str: class BackupRoute(Route): """备份管理路由 - 提供备份导出、导入、列表等 API 接口 + 提供备份导出、导入、列表等 API 接口 """ def __init__( @@ -110,7 +110,7 @@ class BackupRoute(Route): self.routes = { "/backup/list": ("GET", self.list_backups), "/backup/export": ("POST", self.export_backup), - "/backup/upload": ("POST", self.upload_backup), # 上传文件(兼容小文件) + "/backup/upload": ("POST", self.upload_backup), # 上传文件(兼容小文件) "/backup/upload/init": ("POST", self.upload_init), # 分片上传初始化 "/backup/upload/chunk": ("POST", self.upload_chunk), # 上传分片 "/backup/upload/complete": ("POST", self.upload_complete), # 完成分片上传 @@ -198,20 +198,20 @@ class BackupRoute(Route): return _callback def _ensure_cleanup_task_started(self) -> None: - """确保后台清理任务已启动(在异步上下文中延迟启动)""" + """确保后台清理任务已启动(在异步上下文中延迟启动)""" if self._cleanup_task is None or self._cleanup_task.done(): try: self._cleanup_task = asyncio.create_task( self._cleanup_expired_uploads() ) except RuntimeError: - # 如果没有运行中的事件循环,跳过(等待下次异步调用时启动) + # 如果没有运行中的事件循环,跳过(等待下次异步调用时启动) pass async def _cleanup_expired_uploads(self) -> None: """定期清理过期的上传会话 - 基于 last_activity 字段判断过期,避免清理活跃的上传会话。 + 基于 last_activity 字段判断过期,避免清理活跃的上传会话。 """ while True: try: @@ -220,7 +220,7 @@ class BackupRoute(Route): expired_sessions = [] for upload_id, session in self.upload_sessions.items(): - # 使用 last_activity 判断过期,而非 created_at + # 使用 last_activity 判断过期,而非 created_at last_activity = session.get("last_activity", session["created_at"]) if current_time - last_activity > UPLOAD_EXPIRE_SECONDS: expired_sessions.append(upload_id) @@ -230,7 +230,7 @@ class BackupRoute(Route): logger.info(f"清理过期的上传会话: {upload_id}") except asyncio.CancelledError: - # 任务被取消,正常退出 + # 任务被取消,正常退出 break except Exception as e: logger.error(f"清理过期上传会话失败: {e}") @@ -254,7 +254,7 @@ class BackupRoute(Route): zip_path: ZIP 文件路径 Returns: - dict | None: manifest 内容,如果不是有效备份则返回 None + dict | None: manifest 内容,如果不是有效备份则返回 None """ try: with zipfile.ZipFile(zip_path, "r") as zf: @@ -262,11 +262,11 @@ class BackupRoute(Route): manifest_data = zf.read("manifest.json") return json.loads(manifest_data.decode("utf-8")) else: - # 没有 manifest.json,不是有效的 AstrBot 备份 + # 没有 manifest.json,不是有效的 AstrBot 备份 return None except Exception as e: logger.debug(f"读取备份 manifest 失败: {e}") - return None # 无法读取,不是有效备份 + return None # 无法读取,不是有效备份 async def list_backups(self): # 确保后台清理任务已启动 @@ -288,7 +288,7 @@ class BackupRoute(Route): # 获取所有备份文件 backup_files = [] for filename in os.listdir(self.backup_dir): - # 只处理 .zip 文件,排除隐藏文件和目录 + # 只处理 .zip 文件,排除隐藏文件和目录 if not filename.endswith(".zip") or filename.startswith("."): continue @@ -297,7 +297,7 @@ class BackupRoute(Route): continue # 读取 manifest.json 获取备份信息 - # 如果返回 None,说明不是有效的 AstrBot 备份,跳过 + # 如果返回 None,说明不是有效的 AstrBot 备份,跳过 manifest = self._get_backup_manifest(file_path) if manifest is None: logger.debug(f"跳过无效备份文件: {filename}") @@ -346,7 +346,7 @@ class BackupRoute(Route): """创建备份 返回: - - task_id: 任务ID,用于查询导出进度 + - task_id: 任务ID,用于查询导出进度 """ try: # 生成任务ID @@ -414,8 +414,8 @@ class BackupRoute(Route): async def upload_backup(self): """上传备份文件 - 将备份文件上传到服务器,返回保存的文件名。 - 上传后应调用 check_backup 进行预检查。 + 将备份文件上传到服务器,返回保存的文件名。 + 上传后应调用 check_backup 进行预检查。 Form Data: - file: 备份文件 (.zip) @@ -432,7 +432,7 @@ class BackupRoute(Route): if not file.filename or not file.filename.endswith(".zip"): return Response().error("请上传 ZIP 格式的备份文件").__dict__ - # 清洗文件名并生成唯一名称,防止路径遍历和覆盖 + # 清洗文件名并生成唯一名称,防止路径遍历和覆盖 safe_filename = secure_filename(file.filename) unique_filename = generate_unique_filename(safe_filename) @@ -464,16 +464,16 @@ class BackupRoute(Route): async def upload_init(self): """初始化分片上传 - 创建一个上传会话,返回 upload_id 供后续分片上传使用。 + 创建一个上传会话,返回 upload_id 供后续分片上传使用。 JSON Body: - filename: 原始文件名 - - total_size: 文件总大小(字节) + - total_size: 文件总大小(字节) 返回: - upload_id: 上传会话 ID - - chunk_size: 分片大小(由后端决定) - - total_chunks: 分片总数(由后端根据 total_size 和 chunk_size 计算) + - chunk_size: 分片大小(由后端决定) + - total_chunks: 分片总数(由后端根据 total_size 和 chunk_size 计算) """ try: data = await request.json @@ -489,7 +489,7 @@ class BackupRoute(Route): if total_size <= 0: return Response().error("无效的文件大小").__dict__ - # 由后端计算分片总数,确保前后端一致 + # 由后端计算分片总数,确保前后端一致 import math total_chunks = math.ceil(total_size / CHUNK_SIZE) @@ -543,11 +543,11 @@ class BackupRoute(Route): async def upload_chunk(self): """上传分片 - 上传单个分片数据。 + 上传单个分片数据。 Form Data: - upload_id: 上传会话 ID - - chunk_index: 分片索引(从 0 开始) + - chunk_index: 分片索引(从 0 开始) - chunk: 分片数据 返回: @@ -587,9 +587,9 @@ class BackupRoute(Route): chunk_path = os.path.join(session["chunk_dir"], f"{chunk_index}.part") await chunk_file.save(chunk_path) - # 记录已接收的分片,并更新最后活动时间 + # 记录已接收的分片,并更新最后活动时间 session["received_chunks"].add(chunk_index) - session["last_activity"] = time.time() # 刷新活动时间,防止活跃上传被清理 + session["last_activity"] = time.time() # 刷新活动时间,防止活跃上传被清理 received_count = len(session["received_chunks"]) total_chunks = session["total_chunks"] @@ -616,10 +616,10 @@ class BackupRoute(Route): return Response().error(f"上传分片失败: {e!s}").__dict__ def _mark_backup_as_uploaded(self, zip_path: str) -> None: - """修改备份文件的 manifest.json,将 origin 设置为 uploaded + """修改备份文件的 manifest.json,将 origin 设置为 uploaded - 使用 zipfile 的 append 模式添加新的 manifest.json, - ZIP 规范中后添加的同名文件会覆盖先前的文件。 + 使用 zipfile 的 append 模式添加新的 manifest.json, + ZIP 规范中后添加的同名文件会覆盖先前的文件。 Args: zip_path: ZIP 文件路径 @@ -635,7 +635,7 @@ class BackupRoute(Route): manifest["uploaded_at"] = datetime.now().isoformat() # 使用 append 模式添加新的 manifest.json - # ZIP 规范中,后添加的同名文件会覆盖先前的 + # ZIP 规范中,后添加的同名文件会覆盖先前的 with zipfile.ZipFile(zip_path, "a") as zf: new_manifest = json.dumps(manifest, ensure_ascii=False, indent=2) zf.writestr("manifest.json", new_manifest) @@ -647,7 +647,7 @@ class BackupRoute(Route): async def upload_complete(self): """完成分片上传 - 合并所有分片为完整文件。 + 合并所有分片为完整文件。 JSON Body: - upload_id: 上传会话 ID @@ -677,7 +677,7 @@ class BackupRoute(Route): missing = set(range(total)) - received return ( Response() - .error(f"分片不完整,缺少: {sorted(missing)[:10]}...") + .error(f"分片不完整,缺少: {sorted(missing)[:10]}...") .__dict__ ) @@ -695,7 +695,7 @@ class BackupRoute(Route): async with await anyio.open_file( chunk_path, "rb" ) as chunk_file: - # 分块读取,避免内存溢出 + # 分块读取,避免内存溢出 while True: data_block = await chunk_file.read(8192) if not data_block: @@ -704,7 +704,7 @@ class BackupRoute(Route): file_size = (await anyio.Path(output_path).stat()).st_size - # 标记备份为上传来源(修改 manifest.json 中的 origin 字段) + # 标记备份为上传来源(修改 manifest.json 中的 origin 字段) self._mark_backup_as_uploaded(output_path) logger.info( @@ -726,7 +726,7 @@ class BackupRoute(Route): .__dict__ ) except Exception as e: - # 如果合并失败,删除不完整的文件 + # 如果合并失败,删除不完整的文件 if await anyio.Path(output_path).exists(): await anyio.Path(output_path).unlink() raise e @@ -739,7 +739,7 @@ class BackupRoute(Route): async def upload_abort(self): """取消分片上传 - 取消上传并清理已上传的分片。 + 取消上传并清理已上传的分片。 JSON Body: - upload_id: 上传会话 ID @@ -752,7 +752,7 @@ class BackupRoute(Route): return Response().error("缺少 upload_id 参数").__dict__ if upload_id not in self.upload_sessions: - # 会话已不存在,可能已过期或已完成 + # 会话已不存在,可能已过期或已完成 return Response().ok(message="上传已取消").__dict__ # 清理会话 @@ -769,8 +769,8 @@ class BackupRoute(Route): async def check_backup(self): """预检查备份文件 - 检查备份文件的版本兼容性,返回确认信息。 - 用户确认后调用 import_backup 执行导入。 + 检查备份文件的版本兼容性,返回确认信息。 + 用户确认后调用 import_backup 执行导入。 JSON Body: - filename: 已上传的备份文件名 @@ -792,7 +792,7 @@ class BackupRoute(Route): if not await anyio.Path(zip_path).exists(): return Response().error(f"备份文件不存在: {filename}").__dict__ - # 获取知识库管理器(用于构造 importer) + # 获取知识库管理器(用于构造 importer) kb_manager = getattr(self.core_lifecycle, "kb_manager", None) importer = AstrBotImporter( @@ -813,15 +813,15 @@ class BackupRoute(Route): async def import_backup(self): """执行备份导入 - 在用户确认后执行实际的导入操作。 - 需要先调用 upload_backup 上传文件,再调用 check_backup 预检查。 + 在用户确认后执行实际的导入操作。 + 需要先调用 upload_backup 上传文件,再调用 check_backup 预检查。 JSON Body: - - filename: 已上传的备份文件名(必填) - - confirmed: 用户已确认(必填,必须为 true) + - filename: 已上传的备份文件名(必填) + - confirmed: 用户已确认(必填,必须为 true) 返回: - - task_id: 任务ID,用于查询导入进度 + - task_id: 任务ID,用于查询导入进度 """ try: data = await request.json @@ -834,7 +834,7 @@ class BackupRoute(Route): if not confirmed: return ( Response() - .error("请先确认导入。导入将会清空并覆盖现有数据,此操作不可撤销。") + .error("请先确认导入。导入将会清空并覆盖现有数据,此操作不可撤销。") .__dict__ ) @@ -935,15 +935,15 @@ class BackupRoute(Route): "status": status, } - # 如果任务正在处理,返回进度信息 + # 如果任务正在处理,返回进度信息 if status == "processing" and task_id in self.backup_progress: response_data["progress"] = self.backup_progress[task_id] - # 如果任务完成,返回结果 + # 如果任务完成,返回结果 if status == "completed": response_data["result"] = task_info["result"] - # 如果任务失败,返回错误信息 + # 如果任务失败,返回错误信息 if status == "failed": response_data["error"] = task_info["error"] @@ -958,10 +958,10 @@ class BackupRoute(Route): Query 参数: - filename: 备份文件名 (必填) - - token: JWT token (必填,用于浏览器原生下载鉴权) + - token: JWT token (必填,用于浏览器原生下载鉴权) - 注意: 此路由已被添加到 auth_middleware 白名单中, - 使用 URL 参数中的 token 进行鉴权,以支持浏览器原生下载。 + 注意: 此路由已被添加到 auth_middleware 白名单中, + 使用 URL 参数中的 token 进行鉴权,以支持浏览器原生下载。 """ try: filename = request.args.get("filename") @@ -991,7 +991,7 @@ class BackupRoute(Route): }, ) except jwt.ExpiredSignatureError: - return Response().error("Token 已过期,请刷新页面后重试").__dict__ + return Response().error("Token 已过期,请刷新页面后重试").__dict__ except jwt.InvalidTokenError: return Response().error("Token 无效").__dict__ @@ -1007,7 +1007,7 @@ class BackupRoute(Route): file_path, as_attachment=True, attachment_filename=filename, - conditional=True, # 启用 Range 请求支持(断点续传) + conditional=True, # 启用 Range 请求支持(断点续传) ) except Exception as e: logger.error(f"下载备份失败: {e}") @@ -1046,7 +1046,7 @@ class BackupRoute(Route): Body: - filename: 当前文件名 (必填) - - new_name: 新文件名 (必填,不含扩展名) + - new_name: 新文件名 (必填,不含扩展名) """ try: data = await request.json @@ -1063,10 +1063,10 @@ class BackupRoute(Route): if ".." in filename or "/" in filename or "\\" in filename: return Response().error("无效的文件名").__dict__ - # 清洗新文件名(移除路径和危险字符) + # 清洗新文件名(移除路径和危险字符) new_name = secure_filename(new_name) - # 移除新文件名中的扩展名(如果有的话) + # 移除新文件名中的扩展名(如果有的话) if new_name.endswith(".zip"): new_name = new_name[:-4] diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index b4e7bb831..82b6d39d8 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -44,7 +44,7 @@ async def _poll_webchat_stream_result(back_queue, username: str): except asyncio.TimeoutError: return None, False except asyncio.CancelledError: - logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") + logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") return None, True except Exception as e: logger.error(f"WebChat stream error: {e}") @@ -197,7 +197,7 @@ class ChatRoute(Route): ) async def _build_user_message_parts(self, message: str | list) -> list[dict]: - """构建用户消息的部分列表。""" + """构建用户消息的部分列表。""" return await build_webchat_message_parts( message, get_attachment_by_id=self.db.get_attachment_by_id, @@ -207,7 +207,7 @@ class ChatRoute(Route): async def _create_attachment_from_file( self, filename: str, attach_type: str ) -> dict | None: - """从本地文件创建 attachment 并返回消息部分。""" + """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( filename, attach_type=attach_type, @@ -226,7 +226,7 @@ class ChatRoute(Route): accumulated_parts: 累积的消息部分列表 Returns: - 包含 used 列表的字典,记录被引用的搜索结果 + 包含 used 列表的字典,记录被引用的搜索结果 """ supported = ["web_search_tavily", "web_search_bocha"] # 从 accumulated_parts 中找到所有 web_search_tavily 的工具调用结果 @@ -284,7 +284,7 @@ class ChatRoute(Route): agent_stats: dict, refs: dict, ): - """保存 bot 消息到历史记录,返回保存的记录""" + """保存 bot 消息到历史记录,返回保存的记录""" bot_message_parts = [] bot_message_parts.extend(media_parts) if text: @@ -333,7 +333,7 @@ class ChatRoute(Route): webchat_conv_id = session_id - # 构建用户消息段(包含 path 用于传递给 adapter) + # 构建用户消息段(包含 path 用于传递给 adapter) message_parts = await self._build_user_message_parts(message) if not webchat_message_parts_have_content(message_parts): return ( @@ -404,7 +404,7 @@ class ChatRoute(Route): except Exception as e: if not client_disconnected: logger.debug( - f"[WebChat] 用户 {username} 断开聊天长连接。 {e}" + f"[WebChat] 用户 {username} 断开聊天长连接。 {e}" ) client_disconnected = True @@ -412,7 +412,7 @@ class ChatRoute(Route): if not client_disconnected: await asyncio.sleep(0.05) except asyncio.CancelledError: - logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") + logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") client_disconnected = True # 累积消息部分 @@ -422,7 +422,7 @@ class ChatRoute(Route): tool_call = json.loads(result_text) tool_calls[tool_call.get("id")] = tool_call if accumulated_text: - # 如果累积了文本,则先保存文本 + # 如果累积了文本,则先保存文本 accumulated_parts.append( {"type": "plain", "text": accumulated_text} ) @@ -636,7 +636,7 @@ class ChatRoute(Route): exc, ) - # 清理队列(仅对 webchat) + # 清理队列(仅对 webchat) if session.platform_id == "webchat": webchat_queue_mgr.remove_queues(session_id) @@ -721,7 +721,7 @@ class ChatRoute(Route): return attachment_ids async def _delete_attachments(self, attachment_ids: list[str]) -> None: - """删除附件(包括数据库记录和磁盘文件)""" + """删除附件(包括数据库记录和磁盘文件)""" try: attachments = await self.db.get_attachments(attachment_ids) for attachment in attachments: @@ -746,7 +746,7 @@ class ChatRoute(Route): """Create a new Platform session (default: webchat).""" username = g.get("username", "guest") - # 获取可选的 platform_id 参数,默认为 webchat + # 获取可选的 platform_id 参数,默认为 webchat platform_id = request.args.get("platform_id", "webchat") # 创建新会话 @@ -811,7 +811,7 @@ class ChatRoute(Route): session = await self.db.get_platform_session_by_id(session_id) platform_id = session.platform_id if session else "webchat" - # 获取项目信息(如果会话属于某个项目) + # 获取项目信息(如果会话属于某个项目) username = g.get("username", "guest") project_info = await self.db.get_project_by_session( session_id=session_id, creator=username @@ -832,7 +832,7 @@ class ChatRoute(Route): "is_running": self.running_convs.get(session_id, False), } - # 如果会话属于项目,添加项目信息 + # 如果会话属于项目,添加项目信息 if project_info: response_data["project"] = { "project_id": project_info.project_id, diff --git a/astrbot/dashboard/routes/command.py b/astrbot/dashboard/routes/command.py index cbc565c47..aae82e1b1 100644 --- a/astrbot/dashboard/routes/command.py +++ b/astrbot/dashboard/routes/command.py @@ -48,7 +48,7 @@ class CommandRoute(Route): enabled = data.get("enabled") if handler_full_name is None or enabled is None: - return Response().error("handler_full_name 与 enabled 均为必填。").__dict__ + return Response().error("handler_full_name 与 enabled 均为必填。").__dict__ if isinstance(enabled, str): enabled = enabled.lower() in ("1", "true", "yes", "on") @@ -68,7 +68,7 @@ class CommandRoute(Route): aliases = data.get("aliases") if not handler_full_name or not new_name: - return Response().error("handler_full_name 与 new_name 均为必填。").__dict__ + return Response().error("handler_full_name 与 new_name 均为必填。").__dict__ try: await rename_command_service(handler_full_name, new_name, aliases=aliases) @@ -85,7 +85,7 @@ class CommandRoute(Route): if not handler_full_name or not permission: return ( - Response().error("handler_full_name 与 permission 均为必填。").__dict__ + Response().error("handler_full_name 与 permission 均为必填。").__dict__ ) try: diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 2413a977d..b553d7cb1 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -161,7 +161,7 @@ def validate_config(data, schema: dict, is_core: bool) -> tuple[list[str], dict] and "items" in meta and isinstance(value[0], dict) ): - # 当前仅针对 list[dict] 的情况进行类型校验,以适配 AstrBot 中 platform、provider 的配置 + # 当前仅针对 list[dict] 的情况进行类型校验,以适配 AstrBot 中 platform、provider 的配置 for item in value: validate(item, meta["items"], path=f"{path}{key}.") elif meta["type"] == "object" and isinstance(value, dict): @@ -279,8 +279,8 @@ async def _validate_neo_connectivity( if not access_token: return ( - "⚠️ 未找到 Bay API Key。请填写访问令牌," - "或确保 Bay 的 credentials.json 可被自动发现。" + "⚠️ 未找到 Bay API Key。请填写访问令牌," + "或确保 Bay 的 credentials.json 可被自动发现。" ) # Connectivity check @@ -295,11 +295,11 @@ async def _validate_neo_connectivity( ) as resp: if resp.status != 200: return ( - f"⚠️ Bay 健康检查失败 (HTTP {resp.status})," + f"⚠️ Bay 健康检查失败 (HTTP {resp.status})," f"请确认 Bay 正在运行: {endpoint}" ) except Exception: - return f"⚠️ 无法连接 Bay ({endpoint}),请确认 Bay 已启动。" + return f"⚠️ 无法连接 Bay ({endpoint}),请确认 Bay 已启动。" return None @@ -345,7 +345,7 @@ class ConfigRoute(Route): super().__init__(context) self.core_lifecycle = core_lifecycle self.config: AstrBotConfig = core_lifecycle.astrbot_config - self._logo_token_cache = {} # 缓存logo token,避免重复注册 + self._logo_token_cache = {} # 缓存logo token,避免重复注册 self.acm = core_lifecycle.astrbot_config_mgr self.ucr = core_lifecycle.umop_config_router self.routes = { @@ -393,7 +393,7 @@ class ConfigRoute(Route): self.register_routes() async def delete_provider_source(self): - """删除 provider_source,并更新关联的 providers""" + """删除 provider_source,并更新关联的 providers""" post_data = await request.json if not post_data: return Response().error("缺少配置数据").to_json() @@ -435,7 +435,7 @@ class ConfigRoute(Route): return Response().ok(message="删除 provider source 成功").to_json() async def update_provider_source(self): - """更新或新增 provider_source,并重载关联的 providers""" + """更新或新增 provider_source,并重载关联的 providers""" post_data = await request.json if not post_data: return Response().error("缺少配置数据").to_json() @@ -464,7 +464,7 @@ class ConfigRoute(Route): .to_json() ) - # 查找旧的 provider_source,若不存在则追加为新配置 + # 查找旧的 provider_source,若不存在则追加为新配置 target_idx = next( (i for i, ps in enumerate(provider_sources) if ps.get("id") == original_id), -1, @@ -493,7 +493,7 @@ class ConfigRoute(Route): logger.error(traceback.format_exc()) return Response().error(str(e)).to_json() - # 重载受影响的 providers,使新的 source 配置生效 + # 重载受影响的 providers,使新的 source 配置生效 reload_errors = [] prov_mgr = self.core_lifecycle.provider_manager for provider in affected_providers: @@ -506,7 +506,7 @@ class ConfigRoute(Route): if reload_errors: return ( Response() - .error("更新成功,但部分提供商重载失败: " + ", ".join(reload_errors)) + .error("更新成功,但部分提供商重载失败: " + ", ".join(reload_errors)) .to_json() ) @@ -703,7 +703,7 @@ class ConfigRoute(Route): return Response().error(f"更新配置文件失败: {e!s}").to_json() async def _test_single_provider(self, provider): - """辅助函数:测试单个 provider 的可用性""" + """辅助函数:测试单个 provider 的可用性""" meta = provider.meta() provider_name = provider.provider_config.get("id", "Unknown Provider") provider_capability_type = meta.provider_type @@ -745,7 +745,7 @@ class ConfigRoute(Route): log_fn=logger.error, ): log_fn(message) - # 记录更详细的traceback信息,但只在是严重错误时 + # 记录更详细的traceback信息,但只在是严重错误时 if status_code == 500: log_fn(traceback.format_exc()) return Response().error(message).to_json() @@ -874,7 +874,7 @@ class ConfigRoute(Route): if not provider_type: return Response().error("provider_config 缺少 type 字段").to_json() - # 首次添加某类提供商时,provider_cls_map 可能尚未注册该适配器 + # 首次添加某类提供商时,provider_cls_map 可能尚未注册该适配器 if provider_type not in provider_cls_map: try: self.core_lifecycle.provider_manager.dynamic_import_provider( @@ -885,7 +885,7 @@ class ConfigRoute(Route): return ( Response() .error( - "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志" + "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志" ) .to_json() ) @@ -931,7 +931,7 @@ class ConfigRoute(Route): async def get_provider_source_models(self): """获取指定 provider_source 支持的模型列表 - 本质上会临时初始化一个 Provider 实例,调用 get_models() 获取模型列表,然后销毁实例 + 本质上会临时初始化一个 Provider 实例,调用 get_models() 获取模型列表,然后销毁实例 """ provider_source_id = request.args.get("source_id") if not provider_source_id: @@ -993,7 +993,7 @@ class ConfigRoute(Route): # 临时实例化 provider inst = cls_type(provider_source, {}) - # 如果有 initialize 方法,调用它 + # 如果有 initialize 方法,调用它 init_fn = getattr(inst, "initialize", None) if inspect.iscoroutinefunction(init_fn): await init_fn() @@ -1008,7 +1008,7 @@ class ConfigRoute(Route): if meta: metadata_map[model_id] = meta - # 销毁实例(如果有 terminate 方法) + # 销毁实例(如果有 terminate 方法) terminate_fn = getattr(inst, "terminate", None) if inspect.iscoroutinefunction(terminate_fn): await terminate_fn() @@ -1052,7 +1052,7 @@ class ConfigRoute(Route): # Non-blocking Bay connectivity check warning = await _validate_neo_connectivity(config) if warning: - return Response().ok(None, f"保存成功。{warning}").to_json() + return Response().ok(None, f"保存成功。{warning}").to_json() return Response().ok(None, "保存成功~").to_json() except Exception as e: logger.error(traceback.format_exc()) @@ -1066,7 +1066,7 @@ class ConfigRoute(Route): await self.core_lifecycle.plugin_manager.reload(plugin_name) return ( Response() - .ok(None, f"保存插件 {plugin_name} 成功~ 机器人正在热重载插件。") + .ok(None, f"保存插件 {plugin_name} 成功~ 机器人正在热重载插件。") .to_json() ) except Exception as e: @@ -1081,10 +1081,10 @@ class ConfigRoute(Route): def _resolve_config_file_scope( self, ) -> tuple[str, str, str, StarMetadata, AstrBotConfig]: - """将请求参数解析为一个明确的配置作用域。 + """将请求参数解析为一个明确的配置作用域。 - 当前支持的 scope: - - scope=plugin:name=,key= + 当前支持的 scope: + - scope=plugin:name=,key= """ scope = request.args.get("scope") or "plugin" @@ -1103,7 +1103,7 @@ class ConfigRoute(Route): return scope, name, key_path, md, md.config async def upload_config_file(self): - """上传文件到插件数据目录(用于某个 file 类型配置项)。""" + """上传文件到插件数据目录(用于某个 file 类型配置项)。""" try: _scope, name, key_path, _md, config = self._resolve_config_file_scope() @@ -1182,7 +1182,7 @@ class ConfigRoute(Route): return Response().ok({"uploaded": uploaded, "errors": errors}).to_json() async def delete_config_file(self): - """删除插件数据目录中的文件。""" + """删除插件数据目录中的文件。""" scope = request.args.get("scope") or "plugin" name = request.args.get("name") @@ -1218,7 +1218,7 @@ class ConfigRoute(Route): return Response().ok(None, "Deleted").to_json() async def get_config_file_list(self): - """获取配置项对应目录下的文件列表。""" + """获取配置项对应目录下的文件列表。""" try: _, name, key_path, _, config = self._resolve_config_file_scope() @@ -1262,7 +1262,7 @@ class ConfigRoute(Route): async def post_new_platform(self): new_platform_config = await request.json - # 如果是支持统一 webhook 模式的平台,生成 webhook_uuid + # 如果是支持统一 webhook 模式的平台,生成 webhook_uuid ensure_platform_webhook_config(new_platform_config) self.config["platform"].append(new_platform_config) @@ -1296,7 +1296,7 @@ class ConfigRoute(Route): if origin_platform_id != new_config.get("id", None): return Response().error("机器人名称不允许修改").to_json() - # 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid + # 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid ensure_platform_webhook_config(new_config) for i, platform in enumerate(self.config["platform"]): @@ -1326,7 +1326,7 @@ class ConfigRoute(Route): ) except Exception as e: return Response().error(str(e)).to_json() - return Response().ok(None, "更新成功,已经实时生效~").to_json() + return Response().ok(None, "更新成功,已经实时生效~").to_json() async def post_delete_platform(self): platform_id = await request.json @@ -1356,10 +1356,10 @@ class ConfigRoute(Route): ) except Exception as e: return Response().error(str(e)).to_json() - return Response().ok(None, "删除成功,已经实时生效。").to_json() + return Response().ok(None, "删除成功,已经实时生效。").to_json() async def get_llm_tools(self): - """获取函数调用工具。包含了本地加载的以及 MCP 服务的工具""" + """获取函数调用工具。包含了本地加载的以及 MCP 服务的工具""" tool_mgr = self.core_lifecycle.provider_manager.llm_tools tools = tool_mgr.get_func_desc_openai_style() return Response().ok(tools).to_json() @@ -1434,7 +1434,7 @@ class ConfigRoute(Route): def _inject_platform_metadata_with_i18n( self, platform, metadata, platform_i18n_translations: dict ): - """将配置元数据注入到 metadata 中并处理国际化键转换。""" + """将配置元数据注入到 metadata 中并处理国际化键转换。""" metadata["platform_group"]["metadata"]["platform"].setdefault("items", {}) platform_items_to_inject = copy.deepcopy(platform.config_metadata) @@ -1487,7 +1487,7 @@ class ConfigRoute(Route): platform.default_config_tmpl ) - # 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键) + # 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键) if platform.config_metadata: self._inject_platform_metadata_with_i18n( platform, metadata, platform_i18n_translations @@ -1526,7 +1526,7 @@ class ConfigRoute(Route): break ret["config"] = ( plugin_md.config - ) # 这是自定义的 Dict 类(AstrBotConfig) + ) # 这是自定义的 Dict 类(AstrBotConfig) ret["metadata"] = { plugin_name: { "description": f"{plugin_name} 配置", diff --git a/astrbot/dashboard/routes/conversation.py b/astrbot/dashboard/routes/conversation.py index 68eed7ef1..11ab1c7ec 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -40,7 +40,7 @@ class ConversationRoute(Route): self.register_routes() async def list_conversations(self): - """获取对话列表,支持分页、排序和筛选""" + """获取对话列表,支持分页、排序和筛选""" try: # 获取分页参数 page = request.args.get("page", 1, type=int) @@ -105,7 +105,7 @@ class ConversationRoute(Route): return Response().error(f"获取对话列表失败: {e!s}").__dict__ async def get_conv_detail(self): - """获取指定对话详情(通过POST请求)""" + """获取指定对话详情(通过POST请求)""" try: data = await request.get_json() user_id = data.get("user_id") @@ -211,7 +211,7 @@ class ConversationRoute(Route): message = f"成功删除 {deleted_count} 个对话" if failed_items: - message += f",失败 {len(failed_items)} 个" + message += f",失败 {len(failed_items)} 个" return ( Response() diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index 2b44194a6..fd608c86d 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -22,7 +22,7 @@ from .route import Response, Route, RouteContext class KnowledgeBaseRoute(Route): """知识库管理路由 - 提供知识库、文档、检索、会话配置等 API 接口 + 提供知识库、文档、检索、会话配置等 API 接口 """ def __init__( @@ -256,7 +256,7 @@ class KnowledgeBaseRoute(Route): task_id, file_idx, file_name ) - # 调用 upload_document,传入 pre_chunked_text + # 调用 upload_document,传入 pre_chunked_text doc = await kb_helper.upload_document( file_name=file_name, file_content=None, # 预切片模式下不需要原始内容 @@ -303,7 +303,7 @@ class KnowledgeBaseRoute(Route): Query 参数: - page: 页码 (默认 1) - page_size: 每页数量 (默认 20) - - refresh_stats: 是否刷新统计信息 (默认 false,首次加载时可设为 true) + - refresh_stats: 是否刷新统计信息 (默认 false,首次加载时可设为 true) """ try: kb_manager = self._get_kb_manager() @@ -375,7 +375,7 @@ class KnowledgeBaseRoute(Route): vec = await prv.get_embedding("astrbot") if len(vec) != prv.get_dim(): raise ValueError( - f"嵌入向量维度不匹配,实际是 {len(vec)},然而配置是 {prv.get_dim()}", + f"嵌入向量维度不匹配,实际是 {len(vec)},然而配置是 {prv.get_dim()}", ) except Exception as e: return Response().error(f"测试嵌入模型失败: {e!s}").__dict__ @@ -401,7 +401,7 @@ class KnowledgeBaseRoute(Route): except Exception as e: return ( Response() - .error(f"测试重排序模型失败: {e!s},请检查平台日志输出。") + .error(f"测试重排序模型失败: {e!s},请检查平台日志输出。") .__dict__ ) @@ -642,12 +642,12 @@ class KnowledgeBaseRoute(Route): """上传文档 支持两种方式: - 1. multipart/form-data 文件上传(支持多文件,最多10个) - 2. JSON 格式 base64 编码上传(支持多文件,最多10个) + 1. multipart/form-data 文件上传(支持多文件,最多10个) + 2. JSON 格式 base64 编码上传(支持多文件,最多10个) Form Data (multipart/form-data): - kb_id: 知识库 ID (必填) - - file: 文件对象 (必填,可多个,字段名为 file, file1, file2, ... 或 files[]) + - file: 文件对象 (必填,可多个,字段名为 file, file1, file2, ... 或 files[]) JSON Body (application/json): - kb_id: 知识库 ID (必填) @@ -656,7 +656,7 @@ class KnowledgeBaseRoute(Route): - file_content: base64 编码的文件内容 (必填) 返回: - - task_id: 任务ID,用于查询上传进度和结果 + - task_id: 任务ID,用于查询上传进度和结果 """ try: kb_manager = self._get_kb_manager() @@ -790,7 +790,7 @@ class KnowledgeBaseRoute(Route): for doc in documents: if "file_name" not in doc or "chunks" not in doc: - raise ValueError("文档格式错误,必须包含 file_name 和 chunks") + raise ValueError("文档格式错误,必须包含 file_name 和 chunks") if not isinstance(doc["chunks"], list): raise ValueError("chunks 必须是列表") if not all( @@ -896,11 +896,11 @@ class KnowledgeBaseRoute(Route): "status": status, } - # 如果任务正在处理,返回进度信息 + # 如果任务正在处理,返回进度信息 if status == "processing" and task_id in self.upload_progress: response_data["progress"] = self.upload_progress[task_id] - # 如果任务完成,返回结果 + # 如果任务完成,返回结果 if status == "completed": response_data["result"] = task_info["result"] # 清理已完成的任务 @@ -908,7 +908,7 @@ class KnowledgeBaseRoute(Route): # if task_id in self.upload_progress: # del self.upload_progress[task_id] - # 如果任务失败,返回错误信息 + # 如果任务失败,返回错误信息 if status == "failed": response_data["error"] = task_info["error"] @@ -1073,7 +1073,7 @@ class KnowledgeBaseRoute(Route): - query: 查询文本 (必填) - kb_ids: 知识库 ID 列表 (必填) - top_k: 返回结果数量 (可选, 默认 5) - - debug: 是否启用调试模式,返回 t-SNE 可视化图片 (可选, 默认 False) + - debug: 是否启用调试模式,返回 t-SNE 可视化图片 (可选, 默认 False) """ try: kb_manager = self._get_kb_manager() @@ -1105,7 +1105,7 @@ class KnowledgeBaseRoute(Route): "query": query, } - # Debug 模式:生成 t-SNE 可视化 + # Debug 模式:生成 t-SNE 可视化 if debug: try: img_base64 = await generate_tsne_visualization( @@ -1142,7 +1142,7 @@ class KnowledgeBaseRoute(Route): - max_retries: 最大重试次数 (可选, 默认3) 返回: - - task_id: 任务ID,用于查询上传进度和结果 + - task_id: 任务ID,用于查询上传进度和结果 """ try: kb_manager = self._get_kb_manager() diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 30e8ffc7a..2b84b6ed1 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -57,7 +57,7 @@ class LiveChatSession: self.audio_frames.append(data) async def end_speaking(self, stamp: str) -> tuple[str | None, float]: - """结束说话,返回组装的 WAV 文件路径和耗时""" + """结束说话,返回组装的 WAV 文件路径和耗时""" start_time = time.time() if not self.is_speaking or stamp != self.current_stamp: logger.warning( @@ -77,7 +77,7 @@ class LiveChatSession: os.makedirs(temp_dir, exist_ok=True) audio_path = os.path.join(temp_dir, f"live_audio_{uuid.uuid4()}.wav") - # 假设前端发送的是 PCM 数据,采样率 16000Hz,单声道,16位 + # 假设前端发送的是 PCM 数据,采样率 16000Hz,单声道,16位 with wave.open(audio_path, "wb") as wav_file: wav_file.setnchannels(1) # 单声道 wav_file.setsampwidth(2) # 16位 = 2字节 @@ -130,17 +130,17 @@ class LiveChatRoute(Route): self.app.websocket("/api/unified_chat/ws")(self.unified_chat_ws) async def live_chat_ws(self) -> None: - """Legacy Live Chat WebSocket 处理器(默认 ct=live)""" + """Legacy Live Chat WebSocket 处理器(默认 ct=live)""" await self._unified_ws_loop(force_ct="live") async def unified_chat_ws(self) -> None: - """Unified Chat WebSocket 处理器(支持 ct=live/chat)""" + """Unified Chat WebSocket 处理器(支持 ct=live/chat)""" await self._unified_ws_loop(force_ct=None) async def _unified_ws_loop(self, force_ct: str | None = None) -> None: """统一 WebSocket 循环""" - # WebSocket 不能通过 header 传递 token,需要从 query 参数获取 - # 注意:WebSocket 上下文使用 websocket.args 而不是 request.args + # WebSocket 不能通过 header 传递 token,需要从 query 参数获取 + # 注意:WebSocket 上下文使用 websocket.args 而不是 request.args token = websocket.args.get("token") if not token: await websocket.close(1008, "Missing authentication token") @@ -186,7 +186,7 @@ class LiveChatRoute(Route): async def _create_attachment_from_file( self, filename: str, attach_type: str ) -> dict | None: - """从本地文件创建 attachment 并返回消息部分。""" + """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( filename, attach_type=attach_type, @@ -198,7 +198,7 @@ class LiveChatRoute(Route): def _extract_web_search_refs( self, accumulated_text: str, accumulated_parts: list ) -> dict: - """从消息中提取 web_search 引用。""" + """从消息中提取 web_search 引用。""" supported = ["web_search_tavily", "web_search_bocha"] web_search_results = {} tool_call_parts = [ @@ -252,7 +252,7 @@ class LiveChatRoute(Route): agent_stats: dict, refs: dict, ): - """保存 bot 消息到历史记录。""" + """保存 bot 消息到历史记录。""" bot_message_parts = [] bot_message_parts.extend(media_parts) if text: @@ -340,7 +340,7 @@ class LiveChatRoute(Route): async def _handle_chat_message( self, session: LiveChatSession, message: dict ) -> None: - """处理 Chat Mode 消息(ct=chat)""" + """处理 Chat Mode 消息(ct=chat)""" msg_type = message.get("t") if msg_type == "bind": @@ -655,7 +655,7 @@ class LiveChatRoute(Route): webchat_queue_mgr.remove_back_queue(message_id) async def _build_chat_message_parts(self, message: list[dict]) -> list[dict]: - """构建 chat websocket 用户消息段(复用 webchat 逻辑)""" + """构建 chat websocket 用户消息段(复用 webchat 逻辑)""" return await build_webchat_message_parts( message, get_attachment_by_id=self.db.get_attachment_by_id, @@ -701,7 +701,7 @@ class LiveChatRoute(Route): await websocket.send_json({"t": "error", "data": "音频组装失败"}) return - # 处理音频:STT -> LLM -> TTS + # 处理音频:STT -> LLM -> TTS await self._process_audio(session, audio_path, assemble_duration) elif msg_type == "interrupt": @@ -712,7 +712,7 @@ class LiveChatRoute(Route): async def _process_audio( self, session: LiveChatSession, audio_path: str, assemble_duration: float ) -> None: - """处理音频:STT -> LLM -> 流式 TTS""" + """处理音频:STT -> LLM -> 流式 TTS""" try: # 发送 WAV 组装耗时 await websocket.send_json( @@ -774,7 +774,7 @@ class LiveChatRoute(Route): try: while True: if session.should_interrupt: - # 用户打断,停止处理 + # 用户打断,停止处理 logger.info("[Live Chat] 检测到用户打断") await websocket.send_json({"t": "stop_play"}) # 保存消息并标记为被打断 @@ -882,7 +882,7 @@ class LiveChatRoute(Route): # 处理完成 logger.info(f"[Live Chat] Bot 回复完成: {bot_text}") - # 如果没有音频流,发送 bot 消息文本 + # 如果没有音频流,发送 bot 消息文本 if not audio_playing: await websocket.send_json( { @@ -924,7 +924,7 @@ class LiveChatRoute(Route): interrupted_text = bot_text + " [用户打断]" logger.info(f"[Live Chat] 保存打断消息: {interrupted_text}") - # 简单记录到日志,实际保存逻辑可以后续完善 + # 简单记录到日志,实际保存逻辑可以后续完善 try: timestamp = int(time.time() * 1000) logger.info( @@ -932,7 +932,7 @@ class LiveChatRoute(Route): ) if bot_text: logger.info( - f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})" ) except Exception as e: logger.error(f"[Live Chat] 记录消息失败: {e}", exc_info=True) diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index a34697af6..668781678 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -13,7 +13,7 @@ from .route import Response, Route, RouteContext def _format_log_sse(log: dict, ts: float) -> str: - """辅助函数:格式化 SSE 消息""" + """辅助函数:格式化 SSE 消息""" payload = { "type": "log", **log, @@ -45,7 +45,7 @@ class LogRoute(Route): async def _replay_cached_logs( self, last_event_id: str ) -> AsyncGenerator[str, None]: - """辅助生成器:重放缓存的日志""" + """辅助生成器:重放缓存的日志""" try: last_ts = float(last_event_id) cached_logs = list(self.log_broker.log_cache) diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 31f341faf..a473fcd1b 100644 --- a/astrbot/dashboard/routes/persona.py +++ b/astrbot/dashboard/routes/persona.py @@ -145,7 +145,7 @@ class PersonaRoute(Route): if begin_dialogs and len(begin_dialogs) % 2 != 0: return ( Response() - .error("预设对话数量必须为偶数(用户和助手轮流对话)") + .error("预设对话数量必须为偶数(用户和助手轮流对话)") .__dict__ ) @@ -220,7 +220,7 @@ class PersonaRoute(Route): if begin_dialogs is not None and len(begin_dialogs) % 2 != 0: return ( Response() - .error("预设对话数量必须为偶数(用户和助手轮流对话)") + .error("预设对话数量必须为偶数(用户和助手轮流对话)") .__dict__ ) @@ -339,7 +339,7 @@ class PersonaRoute(Route): """获取文件夹列表""" try: parent_id = request.args.get("parent_id") - # 空字符串视为 None(根目录) + # 空字符串视为 None(根目录) if parent_id == "": parent_id = None folders = await self.persona_mgr.get_folders(parent_id) diff --git a/astrbot/dashboard/routes/platform.py b/astrbot/dashboard/routes/platform.py index 874bc19db..227c71a6c 100644 --- a/astrbot/dashboard/routes/platform.py +++ b/astrbot/dashboard/routes/platform.py @@ -1,6 +1,6 @@ """统一 Webhook 路由 -提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。 +提供统一的 webhook 回调入口,支持多个平台使用同一端口接收回调。 """ from quart import request @@ -28,7 +28,7 @@ class PlatformRoute(Route): def _register_webhook_routes(self) -> None: """注册 webhook 路由""" - # 统一 webhook 入口,支持 GET 和 POST + # 统一 webhook 入口,支持 GET 和 POST self.app.add_url_rule( "/api/platform/webhook/", view_func=self.unified_webhook_callback, @@ -78,7 +78,7 @@ class PlatformRoute(Route): webhook_uuid: webhook UUID Returns: - 平台适配器实例,未找到则返回 None + 平台适配器实例,未找到则返回 None """ for platform in self.platform_manager.platform_insts: if platform.config.get("webhook_uuid") == webhook_uuid: diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index 5ee76dc23..d4a184293 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -118,17 +118,17 @@ class PluginRoute(Route): ) try: data = await request.get_json() - dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 + dir_name = data.get("dir_name") # 这里拿的是目录名,不是插件名 if not dir_name: return Response().error("缺少插件目录名").__dict__ # 调用 star_manager.py 中的函数 - # 注意:传入的是目录名 + # 注意:传入的是目录名 success, err = await self.plugin_manager.reload_failed_plugin(dir_name) if success: - return Response().ok(None, f"插件 {dir_name} 重载成功。").__dict__ + return Response().ok(None, f"插件 {dir_name} 重载成功。").__dict__ else: return Response().error(f"重载失败: {err}").__dict__ @@ -150,7 +150,7 @@ class PluginRoute(Route): success, message = await self.plugin_manager.reload(plugin_name) if not success: return Response().error(message or "插件重载失败").__dict__ - return Response().ok(None, "重载成功。").__dict__ + return Response().ok(None, "重载成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/reload: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -162,14 +162,14 @@ class PluginRoute(Route): # 构建注册表源信息 source = self._build_registry_source(custom) - # 如果不是强制刷新,先检查缓存是否有效 + # 如果不是强制刷新,先检查缓存是否有效 cached_data = None if not force_refresh: - # 先检查MD5是否匹配,如果匹配则使用缓存 + # 先检查MD5是否匹配,如果匹配则使用缓存 if await self._is_cache_valid(source): cached_data = await self._load_plugin_cache(source.cache_file) if cached_data: - logger.debug("缓存MD5匹配,使用缓存的插件市场数据") + logger.debug("缓存MD5匹配,使用缓存的插件市场数据") return Response().ok(cached_data).__dict__ # 尝试获取远程数据 @@ -201,7 +201,7 @@ class PluginRoute(Route): continue # 继续尝试其他URL或使用缓存 logger.info( - f"成功获取远程插件市场数据,包含 {len(remote_data)} 个插件" + f"成功获取远程插件市场数据,包含 {len(remote_data)} 个插件" ) # 获取最新的MD5并保存到缓存 current_md5 = await self._fetch_remote_md5(source.md5_url) @@ -211,19 +211,19 @@ class PluginRoute(Route): current_md5, ) return Response().ok(remote_data).__dict__ - logger.error(f"请求 {url} 失败,状态码:{response.status}") + logger.error(f"请求 {url} 失败,状态码:{response.status}") except Exception as e: - logger.error(f"请求 {url} 失败,错误:{e}") + logger.error(f"请求 {url} 失败,错误:{e}") - # 如果远程获取失败,尝试使用缓存数据 + # 如果远程获取失败,尝试使用缓存数据 if not cached_data: cached_data = await self._load_plugin_cache(source.cache_file) if cached_data: - logger.warning("远程插件市场数据获取失败,使用缓存数据") - return Response().ok(cached_data, "使用缓存数据,可能不是最新版本").__dict__ + logger.warning("远程插件市场数据获取失败,使用缓存数据") + return Response().ok(cached_data, "使用缓存数据,可能不是最新版本").__dict__ - return Response().error("获取插件列表失败,且没有可用的缓存数据").__dict__ + return Response().error("获取插件列表失败,且没有可用的缓存数据").__dict__ def _build_registry_source(self, custom_url: str | None) -> RegistrySource: """构建注册表源信息""" @@ -286,7 +286,7 @@ class PluginRoute(Route): return None async def _is_cache_valid(self, source: RegistrySource) -> bool: - """检查缓存是否有效(基于MD5)""" + """检查缓存是否有效(基于MD5)""" try: cached_md5 = await self._load_cached_md5(source.cache_file) if not cached_md5: @@ -295,8 +295,8 @@ class PluginRoute(Route): remote_md5 = await self._fetch_remote_md5(source.md5_url) if remote_md5 is None: - logger.warning("无法获取远程MD5,将使用缓存") - return True # 如果无法获取远程MD5,认为缓存有效 + logger.warning("无法获取远程MD5,将使用缓存") + return True # 如果无法获取远程MD5,认为缓存有效 is_valid = cached_md5 == remote_md5 logger.debug( @@ -463,7 +463,7 @@ class PluginRoute(Route): has_admin = False for filter in ( handler.event_filters - ): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高 + ): # 正常handler就只有 1~2 个 filter,因此这里时间复杂度不会太高 if isinstance(filter, CommandFilter): info["type"] = "指令" info["cmd"] = ( @@ -522,8 +522,8 @@ class PluginRoute(Route): ignore_version_check=ignore_version_check, ) # self.core_lifecycle.restart() - logger.info(f"安装插件 {repo_url} 成功。") - return Response().ok(plugin_info, "安装成功。").__dict__ + logger.info(f"安装插件 {repo_url} 成功。") + return Response().ok(plugin_info, "安装成功。").__dict__ except PluginVersionIncompatibleError as e: return { "status": "warning", @@ -564,7 +564,7 @@ class PluginRoute(Route): ) # self.core_lifecycle.restart() logger.info(f"安装插件 {file.filename} 成功") - return Response().ok(plugin_info, "安装成功。").__dict__ + return Response().ok(plugin_info, "安装成功。").__dict__ except PluginVersionIncompatibleError as e: return { "status": "warning", @@ -647,8 +647,8 @@ class PluginRoute(Route): await self.plugin_manager.update_plugin(plugin_name, proxy) # self.core_lifecycle.restart() await self.plugin_manager.reload(plugin_name) - logger.info(f"更新插件 {plugin_name} 成功。") - return Response().ok(None, "更新成功。").__dict__ + logger.info(f"更新插件 {plugin_name} 成功。") + return Response().ok(None, "更新成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/update: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -699,9 +699,9 @@ class PluginRoute(Route): failed = [r for r in results if r["status"] == "error"] message = ( - "批量更新完成,全部成功。" + "批量更新完成,全部成功。" if not failed - else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。" + else f"批量更新完成,其中 {len(failed)}/{len(results)} 个插件失败。" ) return Response().ok({"results": results}, message).__dict__ @@ -718,8 +718,8 @@ class PluginRoute(Route): plugin_name = post_data["name"] try: await self.plugin_manager.turn_off_plugin(plugin_name) - logger.info(f"停用插件 {plugin_name} 。") - return Response().ok(None, "停用成功。").__dict__ + logger.info(f"停用插件 {plugin_name} 。") + return Response().ok(None, "停用成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/off: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -736,8 +736,8 @@ class PluginRoute(Route): plugin_name = post_data["name"] try: await self.plugin_manager.turn_on_plugin(plugin_name) - logger.info(f"启用插件 {plugin_name} 。") - return Response().ok(None, "启用成功。").__dict__ + logger.info(f"启用插件 {plugin_name} 。") + return Response().ok(None, "启用成功。").__dict__ except Exception as e: logger.error(f"/api/plugin/on: {traceback.format_exc()}") return Response().error(str(e)).__dict__ @@ -801,7 +801,7 @@ class PluginRoute(Route): async def get_plugin_changelog(self): """获取插件更新日志 - 读取插件目录下的 CHANGELOG.md 文件内容。 + 读取插件目录下的 CHANGELOG.md 文件内容。 """ plugin_name = request.args.get("name") logger.debug(f"正在获取插件 {plugin_name} 的更新日志") @@ -859,7 +859,7 @@ class PluginRoute(Route): logger.error(f"/api/plugin/changelog: {traceback.format_exc()}") return Response().error(f"读取更新日志失败: {e!s}").__dict__ - # 没有找到 changelog 文件,返回 ok 但 content 为 null + # 没有找到 changelog 文件,返回 ok 但 content 为 null logger.warning(f"插件 {plugin_name} 没有更新日志文件") return Response().ok({"content": None}, "该插件没有更新日志文件").__dict__ diff --git a/astrbot/dashboard/routes/session_management.py b/astrbot/dashboard/routes/session_management.py index ffe5372a0..4b4782999 100644 --- a/astrbot/dashboard/routes/session_management.py +++ b/astrbot/dashboard/routes/session_management.py @@ -51,19 +51,19 @@ class SessionManagementRoute(Route): async def _get_umo_rules( self, page: int = 1, page_size: int = 10, search: str = "" ) -> tuple[dict, int]: - """获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。 + """获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。 - 如果某个 umo 在 preference 中有以下字段,则表示有自定义规则: + 如果某个 umo 在 preference 中有以下字段,则表示有自定义规则: - 1. session_service_config (包含了 是否启用这个umo, 这个umo是否启用 llm, 这个umo是否启用tts, umo自定义名称。) + 1. session_service_config (包含了 是否启用这个umo, 这个umo是否启用 llm, 这个umo是否启用tts, umo自定义名称。) 2. session_plugin_config (包含了 这个 umo 的 plugin set) 3. provider_perf_{ProviderType.value} (包含了这个 umo 所选择使用的 provider 信息) 4. kb_config (包含了这个 umo 的知识库相关配置) Args: - page: 页码,从 1 开始 + page: 页码,从 1 开始 page_size: 每页数量 - search: 搜索关键词,匹配 umo 或 custom_name + search: 搜索关键词,匹配 umo 或 custom_name Returns: tuple[dict, int]: (umo_rules, total) - 分页后的 umo 规则和总数 @@ -118,14 +118,14 @@ class SessionManagementRoute(Route): return paginated_rules, total async def list_session_rule(self): - """获取所有自定义的规则(支持分页和搜索) + """获取所有自定义的规则(支持分页和搜索) - 返回已配置规则的 umo 列表及其规则内容,以及可用的 personas 和 providers + 返回已配置规则的 umo 列表及其规则内容,以及可用的 personas 和 providers Query 参数: - page: 页码,默认为 1 - page_size: 每页数量,默认为 10 - search: 搜索关键词,匹配 umo 或 custom_name + page: 页码,默认为 1 + page_size: 每页数量,默认为 10 + search: 搜索关键词,匹配 umo 或 custom_name """ try: # 获取分页和搜索参数 @@ -196,7 +196,7 @@ class SessionManagementRoute(Route): for p in provider_manager.tts_provider_insts ] - # 获取可用的插件列表(排除 reserved 的系统插件) + # 获取可用的插件列表(排除 reserved 的系统插件) plugin_manager = self.core_lifecycle.plugin_manager available_plugins = [ { @@ -255,7 +255,7 @@ class SessionManagementRoute(Route): { "umo": "平台:消息类型:会话ID", "rule_key": "session_service_config" | "session_plugin_config" | "kb_config" | "provider_perf_xxx", - "rule_value": {...} // 规则值,具体结构根据 rule_key 不同而不同 + "rule_value": {...} // 规则值,具体结构根据 rule_key 不同而不同 } """ try: @@ -294,7 +294,7 @@ class SessionManagementRoute(Route): 请求体: { "umo": "平台:消息类型:会话ID", - "rule_key": "session_service_config" | "session_plugin_config" | ... (可选,不传则删除所有规则) + "rule_key": "session_service_config" | "session_plugin_config" | ... (可选,不传则删除所有规则) } """ try: @@ -357,7 +357,7 @@ class SessionManagementRoute(Route): Response() .ok( { - "message": f"已删除 {deleted_count} 条规则,{len(failed_umos)} 条删除失败", + "message": f"已删除 {deleted_count} 条规则,{len(failed_umos)} 条删除失败", "deleted_count": deleted_count, "failed_umos": failed_umos, } @@ -380,9 +380,9 @@ class SessionManagementRoute(Route): return Response().error(f"批量删除会话规则失败: {e!s}").__dict__ async def list_umos(self): - """列出所有有对话记录的 umo,从 Conversations 表中找 + """列出所有有对话记录的 umo,从 Conversations 表中找 - 仅返回 umo 字符串列表,用于用户在创建规则时选择 umo + 仅返回 umo 字符串列表,用于用户在创建规则时选择 umo """ try: # 从 Conversation 表获取所有 distinct user_id (即 umo) @@ -401,11 +401,11 @@ class SessionManagementRoute(Route): return Response().error(f"获取 UMO 列表失败: {e!s}").__dict__ async def list_all_umos_with_status(self): - """获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选) + """获取所有有对话记录的 UMO 及其服务状态(支持分页、搜索、筛选) Query 参数: - page: 页码,默认为 1 - page_size: 每页数量,默认为 20 + page: 页码,默认为 1 + page_size: 每页数量,默认为 20 search: 搜索关键词 message_type: 筛选消息类型 (group/private/all) platform: 筛选平台 @@ -560,10 +560,10 @@ class SessionManagementRoute(Route): 请求体: { - "umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选 - "scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围 + "umos": ["平台:消息类型:会话ID", ...], // 可选,如果不传则根据 scope 筛选 + "scope": "all" | "group" | "private" | "custom_group", // 可选,批量范围 "group_id": "分组ID", // 当 scope 为 custom_group 时必填 - "llm_enabled": true/false/null, // 可选,null表示不修改 + "llm_enabled": true/false/null, // 可选,null表示不修改 "tts_enabled": true/false/null, // 可选 "session_enabled": true/false/null // 可选 } @@ -581,7 +581,7 @@ class SessionManagementRoute(Route): if llm_enabled is None and tts_enabled is None and session_enabled is None: return Response().error("至少需要指定一个要修改的状态").__dict__ - # 如果指定了 scope,获取符合条件的所有 umo + # 如果指定了 scope,获取符合条件的所有 umo if scope and not umos: # 如果是自定义分组 if scope == "custom_group": @@ -713,7 +713,7 @@ class SessionManagementRoute(Route): provider_type_enum = provider_type_map[provider_type] - # 如果指定了 scope,获取符合条件的所有 umo + # 如果指定了 scope,获取符合条件的所有 umo group_id = data.get("group_id", "") if scope and not umos: # 如果是自定义分组 @@ -797,7 +797,7 @@ class SessionManagementRoute(Route): """获取所有分组列表""" try: groups = self._get_groups() - # 转换为列表格式,方便前端使用 + # 转换为列表格式,方便前端使用 groups_list = [] for group_id, group_data in groups.items(): groups_list.append( @@ -857,7 +857,7 @@ class SessionManagementRoute(Route): return Response().error(f"创建分组失败: {e!s}").__dict__ async def update_group(self): - """更新分组(改名、增删成员)""" + """更新分组(改名、增删成员)""" try: data = await request.json group_id = data.get("id") diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 465ea0cb1..5c92c04ef 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -169,7 +169,7 @@ class StatRoute(Route): return Response().error(e.__str__()).__dict__ async def test_ghproxy_connection(self): - """测试 GitHub 代理连接是否可用。""" + """测试 GitHub 代理连接是否可用。""" try: data = await request.get_json() proxy_url: str = data.get("proxy_url") @@ -223,7 +223,7 @@ class StatRoute(Route): changelogs_dir = _resolve_path(Path(project_path) / "changelogs") changelog_path = _resolve_path(changelogs_dir / filename) - # 验证最终路径在预期的 changelogs 目录内(防止路径遍历) + # 验证最终路径在预期的 changelogs 目录内(防止路径遍历) try: changelog_path.relative_to(changelogs_dir) except ValueError: @@ -265,13 +265,13 @@ class StatRoute(Route): versions = [] for filename in os.listdir(changelogs_dir): if filename.endswith(".md") and filename.startswith("v"): - # 提取版本号(去除 v 前缀和 .md 后缀) + # 提取版本号(去除 v 前缀和 .md 后缀) version = filename[1:-3] # 去掉 "v" 和 ".md" # 验证版本号格式 if re.match(r"^[a-zA-Z0-9._-]+$", version): versions.append(version) - # 按版本号排序(降序,最新的在前) + # 按版本号排序(降序,最新的在前) # 使用项目中的 VersionComparator 进行语义化版本号排序 versions.sort( key=cmp_to_key( @@ -285,7 +285,7 @@ class StatRoute(Route): return Response().error(f"Error: {e!s}").__dict__ async def get_first_notice(self): - """读取项目根目录 FIRST_NOTICE.md 内容。""" + """读取项目根目录 FIRST_NOTICE.md 内容。""" try: locale = (request.args.get("locale") or "").strip() if not re.match(r"^[A-Za-z0-9_-]*$", locale): diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index 15fec95d1..01e8a7c04 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -34,7 +34,7 @@ class StaticFileRoute(Route): @self.app.errorhandler(404) async def page_not_found(e) -> str: - return "404 Not found。如果你初次使用打开面板发现 404, 请参考文档: https://astrbot.app/faq.html。如果你正在测试回调地址可达性,显示这段文字说明测试成功了。" + return "404 Not found。如果你初次使用打开面板发现 404, 请参考文档: https://astrbot.app/faq.html。如果你正在测试回调地址可达性,显示这段文字说明测试成功了。" async def index(self): return await self.app.send_static_file("index.html") diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py index 8d06826be..78bd2c35f 100644 --- a/astrbot/dashboard/routes/t2i.py +++ b/astrbot/dashboard/routes/t2i.py @@ -19,7 +19,7 @@ class T2iRoute(Route): self.core_lifecycle = core_lifecycle self.config = core_lifecycle.astrbot_config self.manager = TemplateManager() - # 使用列表保证路由注册顺序,避免 / 路由优先匹配 /reset_default + # 使用列表保证路由注册顺序,避免 / 路由优先匹配 /reset_default self.routes = [ ("/t2i/templates", ("GET", self.list_templates)), ("/t2i/templates/active", ("GET", self.get_active_template)), @@ -130,13 +130,13 @@ class T2iRoute(Route): self.manager.update_template(name, content) - # 检查更新的是否为当前激活的模板,如果是,则热重载 + # 检查更新的是否为当前激活的模板,如果是,则热重载 active_template = self.config.get("t2i_active_template", "base") if name == active_template: await self.core_lifecycle.reload_pipeline_scheduler("default") - message = f"模板 '{name}' 已更新并重新加载。" + message = f"模板 '{name}' 已更新并重新加载。" else: - message = f"模板 '{name}' 已更新。" + message = f"模板 '{name}' 已更新。" return jsonify(asdict(Response().ok(data={"name": name}, message=message))) except ValueError as e: @@ -175,7 +175,7 @@ class T2iRoute(Route): data = await request.json name = data.get("name") if not name: - response = jsonify(asdict(Response().error("模板名称(name)不能为空。"))) + response = jsonify(asdict(Response().error("模板名称(name)不能为空。"))) response.status_code = 400 return response @@ -190,11 +190,11 @@ class T2iRoute(Route): # 热重载以应用更改 await self.core_lifecycle.reload_pipeline_scheduler("default") - return jsonify(asdict(Response().ok(message=f"模板 '{name}' 已成功应用。"))) + return jsonify(asdict(Response().ok(message=f"模板 '{name}' 已成功应用。"))) except FileNotFoundError: response = jsonify( - asdict(Response().error(f"模板 '{name}' 不存在,无法应用。")), + asdict(Response().error(f"模板 '{name}' 不存在,无法应用。")), ) response.status_code = 404 return response @@ -209,7 +209,7 @@ class T2iRoute(Route): try: self.manager.reset_default_template() - # 更新配置,将激活模板也重置为'base' + # 更新配置,将激活模板也重置为'base' config = self.config config["t2i_active_template"] = "base" config.save_config(config) diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index e10c9a69a..067139804 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -100,7 +100,7 @@ class ToolsRoute(Route): if key != "active": # active 已经处理 server_info[key] = value - # 如果MCP客户端已初始化,从客户端获取工具名称 + # 如果MCP客户端已初始化,从客户端获取工具名称 for name_key, runtime in self.tool_mgr.mcp_server_runtime_view.items(): if name_key == name: mcp_client = runtime.client @@ -249,7 +249,7 @@ class ToolsRoute(Route): server_config[key] = value only_update_active = False - # 如果只更新活动状态,保留原始配置 + # 如果只更新活动状态,保留原始配置 if only_update_active and isinstance(old_config, dict): for key, value in old_config.items(): if key != "active": # 除了active之外的所有字段都保留 diff --git a/astrbot/dashboard/routes/update.py b/astrbot/dashboard/routes/update.py index b0520c315..a035423e1 100644 --- a/astrbot/dashboard/routes/update.py +++ b/astrbot/dashboard/routes/update.py @@ -37,7 +37,7 @@ class UpdateRoute(Route): async def do_migration(self): need_migration = await check_migration_needed_v4(self.core_lifecycle.db) if not need_migration: - return Response().ok(None, "不需要进行迁移。").__dict__ + return Response().ok(None, "不需要进行迁移。").__dict__ try: data = await request.json pim = data.get("platform_id_map", {}) @@ -46,7 +46,7 @@ class UpdateRoute(Route): pim, self.core_lifecycle.astrbot_config, ) - return Response().ok(None, "迁移成功。").__dict__ + return Response().ok(None, "迁移成功。").__dict__ except Exception as e: logger.error(f"迁移失败: {traceback.format_exc()}") return Response().error(f"迁移失败: {e!s}").__dict__ @@ -65,7 +65,7 @@ class UpdateRoute(Route): ret = await self.astrbot_updator.check_update(None, None, False) return Response( status="success", - message=str(ret) if ret is not None else "已经是最新版本了。", + message=str(ret) if ret is not None else "已经是最新版本了。", data={ "version": f"v{VERSION}", "has_new_version": ret is not None, @@ -109,7 +109,7 @@ class UpdateRoute(Route): try: await download_dashboard(latest=latest, version=version, proxy=proxy) except Exception as e: - logger.error(f"下载管理面板文件失败: {e}。") + logger.error(f"下载管理面板文件失败: {e}。") # pip 更新依赖 logger.info("更新依赖中...") @@ -122,13 +122,13 @@ class UpdateRoute(Route): await self.core_lifecycle.restart() ret = ( Response() - .ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。") + .ok(None, "更新成功,AstrBot 将在 2 秒内全量重启以应用新的代码。") .__dict__ ) return ret, 200, CLEAR_SITE_DATA_HEADERS ret = ( Response() - .ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。") + .ok(None, "更新成功,AstrBot 将在下次启动时应用新的代码。") .__dict__ ) return ret, 200, CLEAR_SITE_DATA_HEADERS @@ -141,9 +141,9 @@ class UpdateRoute(Route): try: await download_dashboard(version=f"v{VERSION}", latest=False) except Exception as e: - logger.error(f"下载管理面板文件失败: {e}。") + logger.error(f"下载管理面板文件失败: {e}。") return Response().error(f"下载管理面板文件失败: {e}").__dict__ - ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__ + ret = Response().ok(None, "更新成功。刷新页面即可应用新版本面板。").__dict__ return ret, 200, CLEAR_SITE_DATA_HEADERS except Exception as e: logger.error(f"/api/update_dashboard: {traceback.format_exc()}") @@ -161,10 +161,10 @@ class UpdateRoute(Route): package = data.get("package", "") mirror = data.get("mirror", None) if not package: - return Response().error("缺少参数 package 或不合法。").__dict__ + return Response().error("缺少参数 package 或不合法。").__dict__ try: await pip_installer.install(package, mirror=mirror) - return Response().ok(None, "安装成功。").__dict__ + return Response().ok(None, "安装成功。").__dict__ except Exception as e: logger.error(f"/api/update_pip: {traceback.format_exc()}") return Response().error(e.__str__()).__dict__ diff --git a/astrbot/dashboard/routes/util.py b/astrbot/dashboard/routes/util.py index 105619815..7f99c4298 100644 --- a/astrbot/dashboard/routes/util.py +++ b/astrbot/dashboard/routes/util.py @@ -1,8 +1,8 @@ -"""Dashboard 路由工具集。 +"""Dashboard 路由工具集。 -这里放一些 dashboard routes 可复用的小工具函数。 +这里放一些 dashboard routes 可复用的小工具函数。 -目前主要用于「配置文件上传(file 类型配置项)」功能: +目前主要用于「配置文件上传(file 类型配置项)」功能: - 清洗/规范化用户可控的文件名与相对路径 - 将配置 key 映射到配置项独立子目录 """ @@ -11,11 +11,11 @@ import os def get_schema_item(schema: dict | None, key_path: str) -> dict | None: - """按 dot-path 获取 schema 的节点。 + """按 dot-path 获取 schema 的节点。 - 同时支持: - - 扁平 schema(直接 key 命中) - - 嵌套 object schema({type: "object", items: {...}}) + 同时支持: + - 扁平 schema(直接 key 命中) + - 嵌套 object schema({type: "object", items: {...}}) """ if not isinstance(schema, dict) or not key_path: @@ -38,9 +38,9 @@ def get_schema_item(schema: dict | None, key_path: str) -> dict | None: def sanitize_filename(name: str) -> str: - """清洗上传文件名,避免路径穿越与非法名称。 + """清洗上传文件名,避免路径穿越与非法名称。 - - 丢弃目录部分,仅保留 basename + - 丢弃目录部分,仅保留 basename - 将路径分隔符替换为下划线 - 拒绝空字符串 / "." / ".." """ @@ -55,9 +55,9 @@ def sanitize_filename(name: str) -> str: def sanitize_path_segment(segment: str) -> str: - """清洗目录片段(URL/path 安全,避免穿越)。 + """清洗目录片段(URL/path 安全,避免穿越)。 - 仅保留 [A-Za-z0-9_-],其余替换为 "_" + 仅保留 [A-Za-z0-9_-],其余替换为 "_" """ cleaned = [] @@ -80,14 +80,14 @@ def sanitize_path_segment(segment: str) -> str: def config_key_to_folder(key_path: str) -> str: - """将 dot-path 的配置 key 转成稳定的文件夹路径。""" + """将 dot-path 的配置 key 转成稳定的文件夹路径。""" parts = [sanitize_path_segment(p) for p in key_path.split(".") if p] return "/".join(parts) if parts else "_" def normalize_rel_path(rel_path: str | None) -> str | None: - """规范化用户传入的相对路径,并阻止路径穿越。""" + """规范化用户传入的相对路径,并阻止路径穿越。""" if not isinstance(rel_path, str): return None diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 78bd378f9..520385655 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -263,7 +263,7 @@ class AstrBotDashboard: ) def _init_plugin_route_index(self): - """将插件路由索引,避免 O(n) 查找""" + """将插件路由索引,避免 O(n) 查找""" self._plugin_route_map: dict[tuple[str, str], Callable] = {} for ( @@ -562,7 +562,7 @@ class AstrBotDashboard: if not local_ips: parts.append( - "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" ) logger.info("".join(parts)) diff --git a/astrbot/dashboard/utils.py b/astrbot/dashboard/utils.py index 514b02e4c..442061152 100644 --- a/astrbot/dashboard/utils.py +++ b/astrbot/dashboard/utils.py @@ -35,7 +35,7 @@ async def generate_tsne_visualization( from sklearn.manifold import TSNE except ImportError as e: raise Exception( - "缺少必要的库以生成 t-SNE 可视化。请安装 matplotlib 和 scikit-learn: {e}", + "缺少必要的库以生成 t-SNE 可视化。请安装 matplotlib 和 scikit-learn: {e}", ) from e try: @@ -115,7 +115,7 @@ async def generate_tsne_visualization( label="Knowledge Base Vectors", ) - # 绘制查询向量(红色 X) + # 绘制查询向量 红色 X plt.scatter( query_vector_2d[0], query_vector_2d[1], diff --git a/docs/scripts/sync_docs_to_wiki.py b/docs/scripts/sync_docs_to_wiki.py index f717f1f96..5717f0381 100644 --- a/docs/scripts/sync_docs_to_wiki.py +++ b/docs/scripts/sync_docs_to_wiki.py @@ -20,13 +20,13 @@ SOURCE_ALIASES = { LANG_CONFIG = { "zh": { "index_title": "# AstrBot 中文文档", - "index_intro": "该页面由 `AstrBot-docs` 自动同步到 GitHub Wiki。", + "index_intro": "该页面由 `AstrBot-docs` 自动同步到 GitHub Wiki。", "index_links": [ ("关于 AstrBot", "zh-what-is-astrbot"), ("社区", "zh-community"), ("常见问题", "zh-faq"), ], - "home_intro": "该 Wiki 由 `AstrBot-docs` 自动同步生成。", + "home_intro": "该 Wiki 由 `AstrBot-docs` 自动同步生成。", "home_links": [ ("中文文档入口", "zh-index"), ("English Docs", "Home-en"), diff --git a/docs/scripts/upload_doc_images_to_r2.py b/docs/scripts/upload_doc_images_to_r2.py old mode 100755 new mode 100644 index 7db614dc4..e9c9d4bad --- a/docs/scripts/upload_doc_images_to_r2.py +++ b/docs/scripts/upload_doc_images_to_r2.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import logging import re import shutil import subprocess @@ -30,6 +31,8 @@ HTML_IMG_RE = re.compile( r"]*\bsrc\s*=\s*([\"'])([^\"']+)\1[^>]*>", re.IGNORECASE ) +logger = logging.getLogger(__name__) + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( @@ -214,11 +217,10 @@ def run_rclone_upload( if dry_run: cmd.append("--dry-run") - print() if dry_run: - print("Dry-run:", " ".join(cmd)) + logger.info("Dry-run: %s", " ".join(cmd)) else: - print(f"Uploading to: {target}") + logger.info("Uploading to: %s", target) subprocess.run(cmd, check=True) finally: @@ -281,43 +283,45 @@ def rewrite_markdown_files( def main() -> int: args = parse_args() + # Ensure basic logging configuration when run as a script + if not logging.getLogger().handlers: + logging.basicConfig(level=logging.INFO) + if args.rewrite_markdown and not args.public_base_url: - print( - "Error: --public-base-url is required when using --rewrite-markdown", - file=sys.stderr, + logger.error( + "Error: --public-base-url is required when using --rewrite-markdown" ) return 1 root = Path(args.docs_root).resolve() if not root.is_dir(): - print(f"Error: docs root not found: {args.docs_root}", file=sys.stderr) + logger.error("Error: docs root not found: %s", args.docs_root) return 1 if shutil.which("rg") is None: - print("Error: rg (ripgrep) not found in PATH", file=sys.stderr) + logger.error("Error: rg (ripgrep) not found in PATH") return 1 md_files = find_markdown_files(root) images, missing = collect_images(root, md_files) if not images: - print("No local image references found in Markdown docs.") + logger.info("No local image references found in Markdown docs.") return 0 rel_files = sorted(p.relative_to(root).as_posix() for p in images) - print(f"Found {len(rel_files)} image files:") + logger.info("Found %d image files:", len(rel_files)) for rel in rel_files: - print(rel) + logger.info("%s", rel) if missing: - print(file=sys.stderr) - print( - f"Warning: {len(missing)} referenced files were not found (showing up to 20):", - file=sys.stderr, + logger.warning( + "Warning: %d referenced files were not found (showing up to 20):", + len(missing), ) for md, ref in missing[:20]: - print(f"{md}\t{ref}", file=sys.stderr) + logger.warning("%s\t%s", md, ref) if args.list_only: return 0 @@ -334,9 +338,9 @@ def main() -> int: public_base_url=args.public_base_url, backup_ext=args.backup_ext, ) - print(f"Rewrote {changed} markdown files.") + logger.info("Rewrote %d markdown files.", changed) - print("Done.") + logger.info("Done.") return 0 diff --git a/scripts/fw2hw.sh b/scripts/fw2hw.sh new file mode 100644 index 000000000..9db2b7cf5 --- /dev/null +++ b/scripts/fw2hw.sh @@ -0,0 +1 @@ +find . -name "*.py" -exec sh -c 'uconv -x "Fullwidth-Halfwidth" "$1" > "$1.tmp" && mv "$1.tmp" "$1"' _ {} \; diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py index f01d4e54f..a2893d9c8 100644 --- a/scripts/hatch_build.py +++ b/scripts/hatch_build.py @@ -74,5 +74,5 @@ class CustomBuildHook(BuildHookInterface): shutil.rmtree(dist_target) shutil.copytree(dist_src, dist_target) logger.info( - f"[hatch_build] Dashboard dist copied → {dist_target.relative_to(root)}" + f"[hatch_build] Dashboard dist copied → {dist_target.relative_to(root)}" ) diff --git a/tests/agent/test_context_manager.py b/tests/agent/test_context_manager.py index 0b955ff40..498227b19 100644 --- a/tests/agent/test_context_manager.py +++ b/tests/agent/test_context_manager.py @@ -27,12 +27,12 @@ class MockProvider: } async def text_chat(self, **kwargs): - """模拟 LLM 调用,返回摘要""" + """模拟 LLM 调用,返回摘要""" messages = kwargs.get("messages", []) - # 简单的摘要逻辑:返回消息数量统计 + # 简单的摘要逻辑:返回消息数量统计 return LLMResponse( role="assistant", - completion_text=f"历史对话包含 {len(messages) - 1} 条消息,主要讨论了技术话题。", + completion_text=f"历史对话包含 {len(messages) - 1} 条消息,主要讨论了技术话题。", ) def get_model(self): diff --git a/tests/agent/test_token_counter.py b/tests/agent/test_token_counter.py index c68b056e6..098bdbe53 100644 --- a/tests/agent/test_token_counter.py +++ b/tests/agent/test_token_counter.py @@ -58,7 +58,7 @@ class TestMultimodalCounting: assert tokens > 0 def test_mixed_content(self): - """文本 + 图片的多模态消息,token 数 = 文本 token + 图片估算。""" + """文本 + 图片的多模态消息,token 数 = 文本 token + 图片估算。""" text_only = _msg("user", [TextPart(text="describe this image")]) mixed = _msg("user", [ TextPart(text="describe this image"), @@ -69,7 +69,7 @@ class TestMultimodalCounting: assert mixed_tokens == text_tokens + IMAGE_TOKEN_ESTIMATE def test_multiple_images(self): - """多张图片应该各自计算。""" + """多张图片应该各自计算。""" msg = _msg("user", [ ImageURLPart(image_url=ImageURLPart.ImageURL(url="data:image/png;base64,a")), ImageURLPart(image_url=ImageURLPart.ImageURL(url="data:image/png;base64,b")), @@ -81,7 +81,7 @@ class TestMultimodalCounting: class TestTrustedUsage: def test_trusted_overrides(self): - """如果 API 返回了 token 数,直接用它不做估算。""" + """如果 API 返回了 token 数,直接用它不做估算。""" msg = _msg("user", [ TextPart(text="hello"), ImageURLPart(image_url=ImageURLPart.ImageURL(url="data:image/png;base64,x")), diff --git a/tests/conftest.py b/tests/conftest.py index b9807c1de..6b223a3e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ """ AstrBot 测试配置 -提供共享的 pytest fixtures 和测试工具。 +提供共享的 pytest fixtures 和测试工具。 """ import json @@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest import pytest_asyncio -# 使用 tests/fixtures/helpers.py 中的共享工具函数,避免重复定义 +# 使用 tests/fixtures/helpers.py 中的共享工具函数,避免重复定义 from tests.fixtures.helpers import create_mock_llm_response, create_mock_message_component # 将项目根目录添加到 sys.path @@ -34,7 +34,7 @@ os.environ.setdefault("ASTRBOT_TEST_MODE", "true") def pytest_collection_modifyitems(session, config, items): # noqa: ARG001 - """重新排序测试:单元测试优先,集成测试在后。""" + """重新排序测试:单元测试优先,集成测试在后。""" unit_tests = [] integration_tests = [] deselected = [] @@ -79,7 +79,7 @@ def pytest_collection_modifyitems(session, config, items): # noqa: ARG001 def pytest_addoption(parser): - """增加测试执行档位选择。""" + """增加测试执行档位选择。""" parser.addoption( "--test-profile", action="store", @@ -90,7 +90,7 @@ def pytest_addoption(parser): def pytest_configure(config): - """注册自定义标记。""" + """注册自定义标记。""" config.addinivalue_line("markers", "unit: 单元测试") config.addinivalue_line("markers", "integration: 集成测试") config.addinivalue_line("markers", "slow: 慢速测试") @@ -108,7 +108,7 @@ def pytest_configure(config): @pytest.fixture def temp_dir(tmp_path: Path) -> Path: - """创建临时目录用于测试。""" + """创建临时目录用于测试。""" return tmp_path @@ -126,7 +126,7 @@ def platform_settings() -> dict: @pytest.fixture def temp_data_dir(temp_dir: Path) -> Path: - """创建模拟的 data 目录结构。""" + """创建模拟的 data 目录结构。""" data_dir = temp_dir / "data" data_dir.mkdir() @@ -141,7 +141,7 @@ def temp_data_dir(temp_dir: Path) -> Path: @pytest.fixture def temp_config_file(temp_data_dir: Path) -> Path: - """创建临时配置文件。""" + """创建临时配置文件。""" config_path = temp_data_dir / "config" / "cmd_config.json" default_config = { "provider": [], @@ -156,7 +156,7 @@ def temp_config_file(temp_data_dir: Path) -> Path: @pytest.fixture def temp_db_file(temp_data_dir: Path) -> Path: - """创建临时数据库文件路径。""" + """创建临时数据库文件路径。""" return temp_data_dir / "test.db" @@ -167,7 +167,7 @@ def temp_db_file(temp_data_dir: Path) -> Path: @pytest.fixture def mock_provider(): - """创建模拟的 Provider。""" + """创建模拟的 Provider。""" provider = MagicMock() provider.provider_config = { "id": "test-provider", @@ -183,7 +183,7 @@ def mock_provider(): @pytest.fixture def mock_platform(): - """创建模拟的 Platform。""" + """创建模拟的 Platform。""" platform = MagicMock() platform.platform_name = "test_platform" platform.platform_meta = MagicMock() @@ -195,7 +195,7 @@ def mock_platform(): @pytest.fixture def mock_conversation(): - """创建模拟的 Conversation。""" + """创建模拟的 Conversation。""" from astrbot.core.db.po import ConversationV2 return ConversationV2( @@ -209,7 +209,7 @@ def mock_conversation(): @pytest.fixture def mock_event(): - """创建模拟的 AstrMessageEvent。""" + """创建模拟的 AstrMessageEvent。""" event = MagicMock() event.unified_msg_origin = "test_umo" event.session_id = "test_session" @@ -239,7 +239,7 @@ def mock_event(): @pytest.fixture def astrbot_config(temp_config_file: Path): - """创建 AstrBotConfig 实例。""" + """创建 AstrBotConfig 实例。""" from astrbot.core.config.astrbot_config import AstrBotConfig config = AstrBotConfig() @@ -249,7 +249,7 @@ def astrbot_config(temp_config_file: Path): @pytest.fixture def main_agent_build_config(): - """创建 MainAgentBuildConfig 实例。""" + """创建 MainAgentBuildConfig 实例。""" from astrbot.core.astr_main_agent import MainAgentBuildConfig return MainAgentBuildConfig( @@ -274,7 +274,7 @@ def main_agent_build_config(): @pytest_asyncio.fixture async def temp_db(temp_db_file: Path): - """创建临时数据库实例。""" + """创建临时数据库实例。""" from astrbot.core.db.sqlite import SQLiteDatabase db = SQLiteDatabase(str(temp_db_file)) @@ -298,7 +298,7 @@ async def mock_context( mock_provider, mock_platform, ): - """创建模拟的插件上下文。""" + """创建模拟的插件上下文。""" from asyncio import Queue from astrbot.core.star.context import Context @@ -344,7 +344,7 @@ async def mock_context( @pytest.fixture def provider_request(): - """创建 ProviderRequest 实例。""" + """创建 ProviderRequest 实例。""" from astrbot.core.provider.entities import ProviderRequest return ProviderRequest( @@ -362,7 +362,7 @@ def provider_request(): def pytest_runtest_setup(item): - """在测试运行前检查跳过条件。""" + """在测试运行前检查跳过条件。""" # 跳过需要 API Key 但未设置的 Provider 测试 if item.get_closest_marker("provider"): if not os.environ.get("TEST_PROVIDER_API_KEY"): diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index 16e927d2c..5d4312540 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,7 +1,7 @@ """ AstrBot 测试数据 -此目录存放测试用的静态数据和配置文件。 +此目录存放测试用的静态数据和配置文件。 目录结构: - fixtures/ @@ -32,7 +32,7 @@ FIXTURES_DIR = Path(__file__).parent def load_fixture(filename: str) -> dict: - """加载 JSON 格式的测试数据。""" + """加载 JSON 格式的测试数据。""" filepath = FIXTURES_DIR / filename if not filepath.exists(): raise FileNotFoundError(f"Fixture not found: {filepath}") @@ -40,7 +40,7 @@ def load_fixture(filename: str) -> dict: def get_fixture_path(filename: str) -> Path: - """获取测试数据文件路径。""" + """获取测试数据文件路径。""" filepath = FIXTURES_DIR / filename if not filepath.exists(): raise FileNotFoundError(f"Fixture not found: {filepath}") diff --git a/tests/fixtures/helpers.py b/tests/fixtures/helpers.py index 26edb761c..21fa8d468 100644 --- a/tests/fixtures/helpers.py +++ b/tests/fixtures/helpers.py @@ -1,6 +1,6 @@ -"""测试辅助函数和工具类。 +"""测试辅助函数和工具类。 -提供统一的测试辅助工具,减少测试代码重复。 +提供统一的测试辅助工具,减少测试代码重复。 """ import shutil @@ -13,9 +13,9 @@ from astrbot.core.message.components import BaseMessageComponent class NoopAwaitable: - """可等待的空操作对象。 + """可等待的空操作对象。 - 用于 mock 需要返回 awaitable 对象的方法。 + 用于 mock 需要返回 awaitable 对象的方法。 """ def __await__(self): @@ -30,7 +30,7 @@ class NoopAwaitable: def make_platform_config(platform_type: str, **kwargs) -> dict: - """平台配置工厂函数。 + """平台配置工厂函数。 Args: platform_type: 平台类型 (telegram, discord, aiocqhttp 等) @@ -105,7 +105,7 @@ def create_mock_update( message_thread_id: int | None = None, is_topic_message: bool = False, ): - """创建模拟的 Telegram Update 对象。 + """创建模拟的 Telegram Update 对象。 Args: message_text: 消息文本 @@ -168,7 +168,7 @@ def create_mock_update( def create_mock_file(file_path: str = "https://api.telegram.org/file/test.jpg"): - """创建模拟的 Telegram File 对象。 + """创建模拟的 Telegram File 对象。 Args: file_path: 文件路径 @@ -193,7 +193,7 @@ def create_mock_discord_attachment( content_type: str | None = None, size: int = 1024, ): - """创建模拟的 Discord Attachment 对象。 + """创建模拟的 Discord Attachment 对象。 Args: filename: 文件名 @@ -218,7 +218,7 @@ def create_mock_discord_user( display_name: str = "Test User", bot: bool = False, ): - """创建模拟的 Discord User 对象。 + """创建模拟的 Discord User 对象。 Args: user_id: 用户 ID @@ -244,7 +244,7 @@ def create_mock_discord_channel( name: str = "general", guild_id: int | None = 444555666, ): - """创建模拟的 Discord Channel 对象。 + """创建模拟的 Discord Channel 对象。 Args: channel_id: 频道 ID @@ -278,7 +278,7 @@ def create_mock_message_component( component_type: str, **kwargs: Any, ) -> BaseMessageComponent: - """创建模拟的消息组件。 + """创建模拟的消息组件。 Args: component_type: 组件类型 (plain, image, at, reply, file) @@ -311,7 +311,7 @@ def create_mock_llm_response( tools_call_args: list[dict] | None = None, tools_call_ids: list[str] | None = None, ): - """创建模拟的 LLM 响应。 + """创建模拟的 LLM 响应。 Args: completion_text: 完成文本 @@ -342,9 +342,9 @@ def create_mock_llm_response( @dataclass class MockPluginConfig: - """测试插件配置。 + """测试插件配置。 - 用于创建和管理测试用的模拟插件。 + 用于创建和管理测试用的模拟插件。 Attributes: name: 插件名称 @@ -374,26 +374,26 @@ DEFAULT_PLUGIN_MAIN_TEMPLATE = ''' from astrbot.api import star class Main(star.Star): - """测试插件主类。""" + """测试插件主类。""" def __init__(self, context): super().__init__(context) self.name = "{plugin_name}" async def initialize(self): - """初始化插件。""" + """初始化插件。""" pass async def terminate(self): - """终止插件。""" + """终止插件。""" pass ''' class MockPluginBuilder: - """测试插件构建器。 + """测试插件构建器。 - 用于创建、管理和清理测试用的模拟插件。支持任意插件的模拟创建。 + 用于创建、管理和清理测试用的模拟插件。支持任意插件的模拟创建。 Example: # 创建一个简单的测试插件 @@ -413,7 +413,7 @@ class MockPluginBuilder: """ def __init__(self, plugin_store_path: str | Path): - """初始化构建器。 + """初始化构建器。 Args: plugin_store_path: 插件存储路径 (通常是 data/plugins) @@ -426,11 +426,11 @@ class MockPluginBuilder: plugin_config: str | MockPluginConfig | None = None, **kwargs, ) -> Path: - """创建模拟插件。 + """创建模拟插件。 Args: - plugin_config: 插件名称字符串、MockPluginConfig 对象或 None - **kwargs: 如果 plugin_config 是字符串或 None,这些参数用于构建 MockPluginConfig + plugin_config: 插件名称字符串、MockPluginConfig 对象或 None + **kwargs: 如果 plugin_config 是字符串或 None,这些参数用于构建 MockPluginConfig Returns: Path: 创建的插件目录路径 @@ -469,13 +469,13 @@ class MockPluginBuilder: ) (plugin_dir / "main.py").write_text(main_code, encoding="utf-8") - # 创建 requirements.txt(如果有依赖) + # 创建 requirements.txt(如果有依赖) if config.requirements: (plugin_dir / "requirements.txt").write_text( "\n".join(config.requirements) + "\n", encoding="utf-8" ) - # 创建 README.md(如果需要) + # 创建 README.md(如果需要) if config.has_readme: (plugin_dir / "README.md").write_text( config.readme_content, encoding="utf-8" @@ -487,10 +487,10 @@ class MockPluginBuilder: return plugin_dir def cleanup(self, plugin_name: str | None = None) -> None: - """清理插件。 + """清理插件。 Args: - plugin_name: 要清理的插件名称,如果为 None 则清理所有由本构建器创建的插件 + plugin_name: 要清理的插件名称,如果为 None 则清理所有由本构建器创建的插件 """ if plugin_name: plugins_to_clean = {plugin_name} @@ -504,11 +504,11 @@ class MockPluginBuilder: self._created_plugins.discard(name) def cleanup_all(self) -> None: - """清理所有由本构建器创建的插件。""" + """清理所有由本构建器创建的插件。""" self.cleanup(None) def get_plugin_path(self, plugin_name: str) -> Path: - """获取插件路径。 + """获取插件路径。 Args: plugin_name: 插件名称 @@ -520,7 +520,7 @@ class MockPluginBuilder: @property def created_plugins(self) -> set[str]: - """获取已创建的插件名称集合。""" + """获取已创建的插件名称集合。""" return self._created_plugins.copy() @@ -528,24 +528,24 @@ def create_mock_updater_install( plugin_builder: MockPluginBuilder, repo_to_plugin: dict[str, str] | None = None, ) -> Callable: - """创建模拟的 updater.install 方法。 + """创建模拟的 updater.install 方法。 Args: plugin_builder: MockPluginBuilder 实例 - repo_to_plugin: 仓库 URL 到插件名称的映射,格式: {"https://github.com/user/repo": "plugin_name"} + repo_to_plugin: 仓库 URL 到插件名称的映射,格式: {"https://github.com/user/repo": "plugin_name"} Returns: - Callable: 异步函数,可用于 monkeypatch.setattr + Callable: 异步函数,可用于 monkeypatch.setattr """ async def mock_install(repo_url: str, proxy: str = "") -> str: - """Mock updater.install 方法。""" + """Mock updater.install 方法。""" # 查找插件名称 plugin_name = None if repo_to_plugin: plugin_name = repo_to_plugin.get(repo_url) - # 如果没有映射,尝试从 URL 提取插件名 + # 如果没有映射,尝试从 URL 提取插件名 if not plugin_name: # 从 https://github.com/user/plugin_name 提取 plugin_name parts = repo_url.rstrip("/").split("/") @@ -563,18 +563,18 @@ def create_mock_updater_update( plugin_builder: MockPluginBuilder, update_callback: Callable | None = None, ) -> Callable: - """创建模拟的 updater.update 方法。 + """创建模拟的 updater.update 方法。 Args: plugin_builder: MockPluginBuilder 实例 - update_callback: 更新回调函数,接收 plugin 参数 + update_callback: 更新回调函数,接收 plugin 参数 Returns: - Callable: 异步函数,可用于 monkeypatch.setattr + Callable: 异步函数,可用于 monkeypatch.setattr """ async def mock_update(plugin, proxy: str = "") -> None: - """Mock updater.update 方法。""" + """Mock updater.update 方法。""" plugin_dir = plugin_builder.get_plugin_path(plugin.name) # 创建更新标记文件 diff --git a/tests/fixtures/mocks/__init__.py b/tests/fixtures/mocks/__init__.py index c6497f1f2..3ec305430 100644 --- a/tests/fixtures/mocks/__init__.py +++ b/tests/fixtures/mocks/__init__.py @@ -1,6 +1,6 @@ -"""测试 Mock 模块。 +"""测试 Mock 模块。 -提供统一的 mock 工具和 fixture,减少测试代码重复。 +提供统一的 mock 工具和 fixture,减少测试代码重复。 使用方式: # 在测试文件顶部导入需要的 fixture diff --git a/tests/fixtures/mocks/aiocqhttp.py b/tests/fixtures/mocks/aiocqhttp.py index d5e3c8229..025f27a82 100644 --- a/tests/fixtures/mocks/aiocqhttp.py +++ b/tests/fixtures/mocks/aiocqhttp.py @@ -1,6 +1,6 @@ -"""Aiocqhttp 模块 Mock 工具。 +"""Aiocqhttp 模块 Mock 工具。 -提供统一的 aiocqhttp 相关模块 mock 设置,避免在测试文件中重复定义。 +提供统一的 aiocqhttp 相关模块 mock 设置,避免在测试文件中重复定义。 """ import sys @@ -10,7 +10,7 @@ import pytest def create_mock_aiocqhttp_modules(): - """创建 aiocqhttp 相关的 mock 模块。 + """创建 aiocqhttp 相关的 mock 模块。 Returns: dict: 包含 aiocqhttp 和相关模块的 mock 对象 @@ -26,9 +26,9 @@ def create_mock_aiocqhttp_modules(): @pytest.fixture(scope="module", autouse=True) def mock_aiocqhttp_modules(): - """Mock aiocqhttp 相关模块的 fixture。 + """Mock aiocqhttp 相关模块的 fixture。 - 自动应用于使用此 fixture 的测试模块。 + 自动应用于使用此 fixture 的测试模块。 """ mock_aiocqhttp = create_mock_aiocqhttp_modules() monkeypatch = pytest.MonkeyPatch() @@ -40,11 +40,11 @@ def mock_aiocqhttp_modules(): class MockAiocqhttpBuilder: - """构建 aiocqhttp 测试 mock 对象的工具类。""" + """构建 aiocqhttp 测试 mock 对象的工具类。""" @staticmethod def create_bot(): - """创建 mock CQHttp bot 实例。""" + """创建 mock CQHttp bot 实例。""" from tests.fixtures.helpers import NoopAwaitable bot = MagicMock() diff --git a/tests/fixtures/mocks/discord.py b/tests/fixtures/mocks/discord.py index dbee989db..83088cbc4 100644 --- a/tests/fixtures/mocks/discord.py +++ b/tests/fixtures/mocks/discord.py @@ -1,6 +1,6 @@ -"""Discord 模块 Mock 工具。 +"""Discord 模块 Mock 工具。 -提供统一的 Discord 相关模块 mock 设置,避免在测试文件中重复定义。 +提供统一的 Discord 相关模块 mock 设置,避免在测试文件中重复定义。 """ import sys @@ -10,7 +10,7 @@ import pytest def create_mock_discord_modules(): - """创建 Discord 相关的 mock 模块。 + """创建 Discord 相关的 mock 模块。 Returns: dict: 包含 discord 和相关模块的 mock 对象 @@ -100,9 +100,9 @@ def create_mock_discord_modules(): @pytest.fixture(scope="module", autouse=True) def mock_discord_modules(): - """Mock Discord 相关模块的 fixture。 + """Mock Discord 相关模块的 fixture。 - 自动应用于使用此 fixture 的测试模块。 + 自动应用于使用此 fixture 的测试模块。 """ mock_discord = create_mock_discord_modules() monkeypatch = pytest.MonkeyPatch() @@ -123,11 +123,11 @@ def mock_discord_modules(): class MockDiscordBuilder: - """构建 Discord 测试 mock 对象的工具类。""" + """构建 Discord 测试 mock 对象的工具类。""" @staticmethod def create_client(): - """创建 mock Discord client 实例。""" + """创建 mock Discord client 实例。""" client = MagicMock() client.user = MagicMock() client.user.id = 123456789 diff --git a/tests/fixtures/mocks/telegram.py b/tests/fixtures/mocks/telegram.py index 904ec4d09..aa11084f5 100644 --- a/tests/fixtures/mocks/telegram.py +++ b/tests/fixtures/mocks/telegram.py @@ -1,6 +1,6 @@ -"""Telegram 模块 Mock 工具。 +"""Telegram 模块 Mock 工具。 -提供统一的 Telegram 相关模块 mock 设置,避免在测试文件中重复定义。 +提供统一的 Telegram 相关模块 mock 设置,避免在测试文件中重复定义。 """ import sys @@ -10,7 +10,7 @@ import pytest def create_mock_telegram_modules(): - """创建 Telegram 相关的 mock 模块。 + """创建 Telegram 相关的 mock 模块。 Returns: dict: 包含 telegram 和相关模块的 mock 对象 @@ -61,9 +61,9 @@ def create_mock_telegram_modules(): @pytest.fixture(scope="module", autouse=True) def mock_telegram_modules(): - """Mock Telegram 相关模块的 fixture。 + """Mock Telegram 相关模块的 fixture。 - 自动应用于使用此 fixture 的测试模块。 + 自动应用于使用此 fixture 的测试模块。 """ mocks = create_mock_telegram_modules() monkeypatch = pytest.MonkeyPatch() @@ -92,11 +92,11 @@ def mock_telegram_modules(): class MockTelegramBuilder: - """构建 Telegram 测试 mock 对象的工具类。""" + """构建 Telegram 测试 mock 对象的工具类。""" @staticmethod def create_bot(): - """创建 mock Telegram bot 实例。""" + """创建 mock Telegram bot 实例。""" bot = MagicMock() bot.username = "test_bot" bot.id = 12345678 @@ -115,7 +115,7 @@ class MockTelegramBuilder: @staticmethod def create_application(): - """创建 mock Telegram Application 实例。""" + """创建 mock Telegram Application 实例。""" from tests.fixtures.helpers import NoopAwaitable app = MagicMock() @@ -133,7 +133,7 @@ class MockTelegramBuilder: @staticmethod def create_scheduler(): - """创建 mock APScheduler 实例。""" + """创建 mock APScheduler 实例。""" scheduler = MagicMock() scheduler.add_job = MagicMock() scheduler.start = MagicMock() diff --git a/tests/fixtures/plugins/fixture_plugin.py b/tests/fixtures/plugins/fixture_plugin.py index 455b5b759..9fbb75fdb 100644 --- a/tests/fixtures/plugins/fixture_plugin.py +++ b/tests/fixtures/plugins/fixture_plugin.py @@ -1,7 +1,7 @@ """ 测试插件 - 用于插件系统测试 -这是一个最小化的测试插件,用于验证插件系统的功能。 +这是一个最小化的测试插件,用于验证插件系统的功能。 """ from astrbot.api import llm_tool, star @@ -22,19 +22,19 @@ class TestPlugin(star.Star): @filter.command("test_cmd") async def test_command(self, event: AstrMessageEvent) -> None: - """测试命令处理器。""" + """测试命令处理器。""" event.set_result(MessageEventResult().message("测试命令执行成功")) @llm_tool("test_tool") async def test_llm_tool(self, query: str) -> str: - """测试 LLM 工具。 + """测试 LLM 工具。 Args: - query(string): 查询内容。 + query(string): 查询内容。 """ return f"测试工具执行成功: {query}" @filter.regex(r"^test_regex_(.+)$") async def test_regex_handler(self, event: AstrMessageEvent) -> None: - """测试正则处理器。""" + """测试正则处理器。""" event.set_result(MessageEventResult().message("正则匹配成功")) diff --git a/tests/test_backup.py b/tests/test_backup.py index cf3c4d949..d3f2d19a9 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -272,7 +272,7 @@ class TestAstrBotImporter: importer._validate_version(manifest) def test_validate_version_minor_diff_allowed(self): - """测试小版本不同被允许(仅警告)""" + """测试小版本不同被允许(仅警告)""" importer = AstrBotImporter(main_db=MagicMock()) # 获取当前主版本 @@ -295,7 +295,7 @@ class TestAstrBotImporter: """测试 datetime 字段转换""" importer = AstrBotImporter(main_db=MagicMock()) - # 使用 ConversationV2 作为测试模型(它有 created_at 和 updated_at 字段) + # 使用 ConversationV2 作为测试模型(它有 created_at 和 updated_at 字段) row = { "conversation_id": "test-123", "platform_id": "test", @@ -403,7 +403,7 @@ class TestAstrBotImporter: assert by_platform["telegram"]["timestamp"] == "2025-12-13T22:00:00+00:00" def test_merge_platform_stats_rows_warns_on_invalid_count(self): - """测试 platform_stats count 非法时会告警并按 0 处理(含上限)""" + """测试 platform_stats count 非法时会告警并按 0 处理(含上限)""" importer = AstrBotImporter(main_db=MagicMock()) with patch("astrbot.core.backup.importer.logger.warning") as warning_mock: rows = [ @@ -496,7 +496,7 @@ class TestAstrBotImporter: assert warning_mock.call_count == 1 def test_merge_platform_stats_rows_keeps_invalid_timestamps_distinct(self): - """测试空/非法 timestamp 不参与聚合,避免误合并""" + """测试空/非法 timestamp 不参与聚合,避免误合并""" importer = AstrBotImporter(main_db=MagicMock()) rows = [ { @@ -563,7 +563,7 @@ class TestAstrBotImporter: assert len(merged_rows) == 4 def test_merge_platform_stats_rows_preserves_input_order(self): - """测试 platform_stats 聚合后仍保持输入顺序(按首次出现位置)""" + """测试 platform_stats 聚合后仍保持输入顺序(按首次出现位置)""" importer = AstrBotImporter(main_db=MagicMock()) rows = [ { @@ -724,7 +724,7 @@ class TestSecureFilename: assert "*" not in result def test_secure_filename_hidden_file(self): - """测试隐藏文件(前导点)""" + """测试隐藏文件(前导点)""" result = secure_filename(".hidden_backup.zip") assert not result.startswith(".") @@ -790,7 +790,7 @@ class TestVersionComparison: assert VersionComparator.compare_version("1.0", "1.1") == -1 assert ( VersionComparator.compare_version("1.9", "1.10") == -1 - ) # 关键测试:多位数版本比较 + ) # 关键测试:多位数版本比较 assert VersionComparator.compare_version("1.2", "1.10") == -1 assert VersionComparator.compare_version("1.0", "2.0") == -1 @@ -799,7 +799,7 @@ class TestVersionComparison: assert VersionComparator.compare_version("1.1", "1.0") == 1 assert ( VersionComparator.compare_version("1.10", "1.9") == 1 - ) # 关键测试:多位数版本比较 + ) # 关键测试:多位数版本比较 assert VersionComparator.compare_version("1.10", "1.2") == 1 assert VersionComparator.compare_version("2.0", "1.0") == 1 @@ -841,7 +841,7 @@ class TestImportPreCheckResult: can_import=True, version_status="match", backup_version="4.9.0", - confirm_message="确认导入?", + confirm_message="确认导入?", warnings=["警告1"], backup_summary={"tables": ["table1"]}, ) @@ -851,7 +851,7 @@ class TestImportPreCheckResult: assert d["can_import"] is True assert d["version_status"] == "match" assert d["backup_version"] == "4.9.0" - assert d["confirm_message"] == "确认导入?" + assert d["confirm_message"] == "确认导入?" assert "警告1" in d["warnings"] assert d["backup_summary"]["tables"] == ["table1"] @@ -913,7 +913,7 @@ class TestPreCheck: assert result.can_import is True assert result.version_status == "match" assert result.backup_version == VERSION - # confirm_message 现在由前端生成,后端不再生成 + # confirm_message 现在由前端生成,后端不再生成 assert result.backup_summary["has_knowledge_bases"] is True def test_pre_check_minor_version_diff(self, mock_main_db, tmp_path): @@ -939,7 +939,7 @@ class TestPreCheck: assert result.valid is True assert result.can_import is True assert result.version_status == "minor_diff" - # 版本消息由前端 i18n 生成,后端 warnings 列表不再包含版本相关消息 + # 版本消息由前端 i18n 生成,后端 warnings 列表不再包含版本相关消息 # warnings 列表保留用于其他非版本相关的警告 def test_pre_check_major_version_diff(self, mock_main_db, tmp_path): @@ -961,7 +961,7 @@ class TestPreCheck: assert result.valid is True # 文件有效 assert result.can_import is False # 但不能导入 assert result.version_status == "major_diff" - # 版本消息由前端 i18n 生成,后端 warnings 列表不再包含版本相关消息 + # 版本消息由前端 i18n 生成,后端 warnings 列表不再包含版本相关消息 class TestVersionCompatibility: diff --git a/tests/test_booter_decoupling.py b/tests/test_booter_decoupling.py index 5b19c32ab..4ca6a0e7e 100644 --- a/tests/test_booter_decoupling.py +++ b/tests/test_booter_decoupling.py @@ -503,7 +503,7 @@ class TestExecutorCapabilityGuard: with patch( "astrbot.core.computer.computer_client.session_booter", - {}, # no booter registered → caps=None → allow through + {}, # no booter registered → caps=None → allow through ): result = FunctionToolExecutor._check_sandbox_capability(tool, run_context) diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 45a9b9b91..8b6f04ccc 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -39,13 +39,13 @@ async def core_lifecycle_td(tmp_path_factory): try: yield core_lifecycle finally: - # 优先停止核心生命周期以释放资源(包括关闭 MCP 等后台任务) + # 优先停止核心生命周期以释放资源(包括关闭 MCP 等后台任务) try: _stop_res = core_lifecycle.stop() if asyncio.iscoroutine(_stop_res): await _stop_res except Exception: - # 停止过程中如有异常,不影响后续清理 + # 停止过程中如有异常,不影响后续清理 pass @@ -286,7 +286,7 @@ async def test_plugins( core_lifecycle_td: AstrBotCoreLifecycle, monkeypatch, ): - """测试插件 API 端点,使用 Mock 避免真实网络调用。""" + """测试插件 API 端点,使用 Mock 避免真实网络调用。""" test_client = app.test_client() # 已经安装的插件 @@ -455,16 +455,16 @@ async def test_check_update( core_lifecycle_td: AstrBotCoreLifecycle, monkeypatch, ): - """测试检查更新 API,使用 Mock 避免真实网络调用。""" + """测试检查更新 API,使用 Mock 避免真实网络调用。""" test_client = app.test_client() # Mock 更新检查和网络请求 async def mock_check_update(*args, **kwargs): - """Mock 更新检查,返回无新版本。""" + """Mock 更新检查,返回无新版本。""" return None # None 表示没有新版本 async def mock_get_dashboard_version(*args, **kwargs): - """Mock Dashboard 版本获取。""" + """Mock Dashboard 版本获取。""" from astrbot.core.config.default import VERSION return f"v{VERSION}" # 返回当前版本 diff --git a/tests/test_pip_installer.py b/tests/test_pip_installer.py index adbd174e3..39dc955e8 100644 --- a/tests/test_pip_installer.py +++ b/tests/test_pip_installer.py @@ -645,7 +645,7 @@ async def test_install_raises_dedicated_pip_install_error_on_non_conflict_failur installer = PipInstaller("") - with pytest.raises(pip_installer_module.PipInstallError, match="错误码:2"): + with pytest.raises(pip_installer_module.PipInstallError, match="错误码:2"): await installer.install(package_name="demo-package") @@ -661,7 +661,7 @@ async def test_run_pip_with_classification_raises_install_error_on_non_conflict_ installer = PipInstaller("") - with pytest.raises(pip_installer_module.PipInstallError, match="错误码:3"): + with pytest.raises(pip_installer_module.PipInstallError, match="错误码:3"): await installer._run_pip_with_classification(["install", "demo-package"]) diff --git a/tests/test_profile_aware_tools.py b/tests/test_profile_aware_tools.py index 455bc731d..a9672a8b3 100644 --- a/tests/test_profile_aware_tools.py +++ b/tests/test_profile_aware_tools.py @@ -87,7 +87,7 @@ class TestApplySandboxToolsConditional: return {t.name for t in req.func_tool.tools} def test_no_session_registers_all(self): - """First request (no booted session) → all tools including browser.""" + """First request (no booted session) → all tools including browser.""" fn = _import_apply_sandbox_tools() config = _make_config("shipyard_neo") req = _make_req() @@ -125,7 +125,7 @@ class TestApplySandboxToolsConditional: return booter def test_with_browser_capability(self): - """Booted session with browser capability → browser tools registered.""" + """Booted session with browser capability → browser tools registered.""" fn = _import_apply_sandbox_tools() config = _make_config("shipyard_neo") req = _make_req() @@ -153,7 +153,7 @@ class TestApplySandboxToolsConditional: assert "astrbot_execute_browser" in names def test_without_browser_capability(self): - """Booted session WITHOUT browser capability → browser tools NOT registered.""" + """Booted session WITHOUT browser capability → browser tools NOT registered.""" fn = _import_apply_sandbox_tools() config = _make_config("shipyard_neo") req = _make_req() @@ -229,7 +229,7 @@ class TestResolveProfile: @pytest.mark.asyncio async def test_user_specified_profile_honoured(self): - """User explicitly sets a non-default profile → use it directly.""" + """User explicitly sets a non-default profile → use it directly.""" booter = self._make_booter(profile="browser-python") client = SimpleNamespace() # list_profiles should NOT be called result = await booter._resolve_profile(client) @@ -260,7 +260,7 @@ class TestResolveProfile: @pytest.mark.asyncio async def test_falls_back_to_default_on_api_error(self): - """API error → graceful fallback to python-default.""" + """API error → graceful fallback to python-default.""" async def _failing_list_profiles(): raise ConnectionError("Bay unreachable") @@ -272,7 +272,7 @@ class TestResolveProfile: @pytest.mark.asyncio async def test_falls_back_on_empty_profiles(self): - """Empty profile list → python-default.""" + """Empty profile list → python-default.""" async def _empty_list_profiles(): return SimpleNamespace(items=[]) @@ -284,7 +284,7 @@ class TestResolveProfile: @pytest.mark.asyncio async def test_single_profile_selected(self): - """Only one profile available → use it.""" + """Only one profile available → use it.""" async def _single_profile(): return SimpleNamespace( diff --git a/tests/test_skill_metadata_enrichment.py b/tests/test_skill_metadata_enrichment.py index 1d511af02..0de422bd5 100644 --- a/tests/test_skill_metadata_enrichment.py +++ b/tests/test_skill_metadata_enrichment.py @@ -54,12 +54,12 @@ def test_parse_frontmatter_multiline_literal_description(): "---\n" "name: humanizer-zh\n" "description: |\n" - " 去除文本中的 AI 生成痕迹。\n" - " 适用于编辑或审阅文本,使其听起来更自然。\n" + " 去除文本中的 AI 生成痕迹。\n" + " 适用于编辑或审阅文本,使其听起来更自然。\n" "---\n" ) assert _parse_frontmatter_description(text) == ( - "去除文本中的 AI 生成痕迹。\n适用于编辑或审阅文本,使其听起来更自然。" + "去除文本中的 AI 生成痕迹。\n适用于编辑或审阅文本,使其听起来更自然。" ) @@ -68,12 +68,12 @@ def test_parse_frontmatter_multiline_folded_description(): "---\n" "name: humanizer-zh\n" "description: >\n" - " 去除文本中的 AI 生成痕迹。\n" - " 适用于编辑或审阅文本,使其听起来更自然。\n" + " 去除文本中的 AI 生成痕迹。\n" + " 适用于编辑或审阅文本,使其听起来更自然。\n" "---\n" ) assert _parse_frontmatter_description(text) == ( - "去除文本中的 AI 生成痕迹。 适用于编辑或审阅文本,使其听起来更自然。" + "去除文本中的 AI 生成痕迹。 适用于编辑或审阅文本,使其听起来更自然。" ) @@ -325,7 +325,7 @@ def test_build_skills_prompt_preserves_safe_unicode_sandbox_description(): skills = [ SkillInfo( name="sandbox-skill", - description="抓取网页摘要,并总结 café 内容", + description="抓取网页摘要,并总结 café 内容", path="/workspace/skills/sandbox-skill/SKILL.md", active=True, source_type="sandbox_only", @@ -337,7 +337,7 @@ def test_build_skills_prompt_preserves_safe_unicode_sandbox_description(): prompt = build_skills_prompt(skills) - assert "抓取网页摘要,并总结 café 内容" in prompt + assert "抓取网页摘要,并总结 café 内容" in prompt def test_build_skills_prompt_preserves_safe_arabic_sandbox_description(): diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index b8a189374..70cf0ed57 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -42,7 +42,7 @@ class MockProvider(Provider): # 检查工具是否被禁用 func_tool = kwargs.get("func_tool") - # 如果工具被禁用或超过最大调用次数,返回正常响应 + # 如果工具被禁用或超过最大调用次数,返回正常响应 if func_tool is None or self.call_count > self.max_calls_before_normal_response: return LLMResponse( role="assistant", @@ -82,7 +82,7 @@ class MockToolExecutor: @classmethod def execute(cls, tool, run_context, **tool_args): async def generator(): - # 模拟工具返回结果,使用正确的类型 + # 模拟工具返回结果,使用正确的类型 from mcp.types import CallToolResult, TextContent result = CallToolResult( @@ -231,10 +231,10 @@ async def test_max_step_limit_functionality( ): """测试最大步数限制功能""" - # 设置模拟provider,让它总是返回工具调用 + # 设置模拟provider,让它总是返回工具调用 mock_provider.should_call_tools = True mock_provider.max_calls_before_normal_response = ( - 100 # 设置一个很大的值,确保不会自然结束 + 100 # 设置一个很大的值,确保不会自然结束 ) # 初始化runner @@ -258,7 +258,7 @@ async def test_max_step_limit_functionality( # 验证结果 assert runner.done(), "代理应该在达到最大步数后完成" - # 验证工具被禁用(这是最重要的验证点) + # 验证工具被禁用(这是最重要的验证点) assert runner.req.func_tool is None, "达到最大步数后工具应该被禁用" # 验证有最终响应 @@ -274,9 +274,9 @@ async def test_max_step_limit_functionality( async def test_normal_completion_without_max_step( runner, mock_provider, provider_request, mock_tool_executor, mock_hooks ): - """测试正常完成(不触发最大步数限制)""" + """测试正常完成(不触发最大步数限制)""" - # 设置模拟provider,让它在第2次调用时返回正常响应 + # 设置模拟provider,让它在第2次调用时返回正常响应 mock_provider.should_call_tools = True mock_provider.max_calls_before_normal_response = 2 @@ -302,19 +302,19 @@ async def test_normal_completion_without_max_step( assert runner.done(), "代理应该正常完成" # 验证没有触发最大步数限制 - 通过检查provider调用次数 - # mock_provider在第2次调用后返回正常响应,所以不应该达到max_steps(10) + # mock_provider在第2次调用后返回正常响应,所以不应该达到max_steps(10) assert mock_provider.call_count < max_steps, ( f"正常完成时调用次数({mock_provider.call_count})应该小于最大步数({max_steps})" ) - # 验证没有最大步数警告消息(注意:实际注入的是user角色的消息) + # 验证没有最大步数警告消息(注意:实际注入的是user角色的消息) user_messages = [m for m in runner.run_context.messages if m.role == "user"] max_step_messages = [ m for m in user_messages if "工具调用次数已达到上限" in m.content ] assert len(max_step_messages) == 0, "正常完成时不应该有步数限制消息" - # 验证工具仍然可用(没有被禁用) + # 验证工具仍然可用(没有被禁用) assert runner.req.func_tool is not None, "正常完成时工具不应该被禁用" @@ -328,7 +328,7 @@ async def test_max_step_with_streaming( mock_provider.should_call_tools = True mock_provider.max_calls_before_normal_response = 100 - # 初始化runner,启用流式响应 + # 初始化runner,启用流式响应 await runner.reset( provider=mock_provider, request=provider_request, diff --git a/tests/unit/test_astr_main_agent.py b/tests/unit/test_astr_main_agent.py index 7ae50b81a..aaf054c81 100644 --- a/tests/unit/test_astr_main_agent.py +++ b/tests/unit/test_astr_main_agent.py @@ -288,7 +288,7 @@ class TestGetSessionConv: conv_mgr.new_conversation = AsyncMock(return_value="new-conv-id") conv_mgr.get_conversation = AsyncMock(return_value=None) - with pytest.raises(RuntimeError, match="无法创建新的对话。"): + with pytest.raises(RuntimeError, match="无法创建新的对话。"): await module._get_session_conv(mock_event, mock_context) @@ -442,7 +442,7 @@ class TestApplyFileExtract: await module._apply_file_extract(mock_event, req, sample_config) - assert req.prompt == "总结一下文件里面讲了什么?" + assert req.prompt == "总结一下文件里面讲了什么?" @pytest.mark.asyncio async def test_file_extract_no_api_key(self, mock_event):