From e781c5eaa521369b3e667782e4996501dcac44a2 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Fri, 10 Apr 2026 20:05:08 +0800 Subject: [PATCH] chore: ruff check --fix --select ALL --- astrbot/api/event/filter/__init__.py | 6 +- .../builtin_stars/astrbot/long_term_memory.py | 10 +- astrbot/builtin_stars/astrbot/main.py | 4 +- .../builtin_commands/commands/conversation.py | 10 +- .../builtin_commands/commands/help.py | 3 +- .../builtin_commands/commands/persona.py | 4 +- .../builtin_commands/commands/provider.py | 34 +-- .../builtin_stars/builtin_commands/main.py | 2 +- astrbot/cli/commands/cmd_conf.py | 4 +- astrbot/cli/commands/cmd_init.py | 2 +- astrbot/cli/commands/cmd_plug.py | 8 +- astrbot/cli/commands/cmd_run.py | 2 +- astrbot/cli/utils/plugin.py | 8 +- astrbot/cli/utils/version_comparator.py | 7 +- astrbot/core/agent/context/compressor.py | 26 +- astrbot/core/agent/context/manager.py | 20 +- astrbot/core/agent/context/token_counter.py | 8 +- astrbot/core/agent/context/truncator.py | 11 +- astrbot/core/agent/mcp_client.py | 22 +- astrbot/core/agent/message.py | 21 +- astrbot/core/agent/runners/base.py | 2 +- .../agent/runners/coze/coze_agent_runner.py | 25 +- .../dashscope/dashscope_agent_runner.py | 48 ++-- .../runners/deerflow/deerflow_agent_runner.py | 20 +- .../runners/deerflow/deerflow_api_client.py | 4 +- .../deerflow/deerflow_content_mapper.py | 6 +- .../agent/runners/dify/dify_agent_runner.py | 35 ++- .../agent/runners/dify/dify_api_client.py | 2 + .../agent/runners/tool_loop_agent_runner.py | 82 +++--- astrbot/core/agent/tool.py | 8 +- astrbot/core/agent/tool_image_cache.py | 5 +- astrbot/core/astr_agent_hooks.py | 2 +- astrbot/core/astr_agent_run_util.py | 44 ++-- astrbot/core/astr_agent_tool_exec.py | 26 +- astrbot/core/astr_main_agent.py | 71 +++--- astrbot/core/backup/__init__.py | 6 +- astrbot/core/backup/constants.py | 1 + astrbot/core/backup/exporter.py | 28 ++- astrbot/core/backup/importer.py | 122 ++++----- astrbot/core/computer/booters/base.py | 3 - astrbot/core/computer/booters/bay_manager.py | 23 +- astrbot/core/computer/booters/boxlite.py | 36 ++- astrbot/core/computer/booters/local.py | 18 +- astrbot/core/computer/booters/shipyard.py | 2 +- astrbot/core/computer/booters/shipyard_neo.py | 8 +- astrbot/core/computer/computer_client.py | 15 +- astrbot/core/computer/olayer/__init__.py | 4 +- astrbot/core/computer/olayer/browser.py | 3 +- astrbot/core/computer/olayer/filesystem.py | 9 +- astrbot/core/computer/olayer/python.py | 3 +- astrbot/core/computer/olayer/shell.py | 3 +- astrbot/core/computer/tools/__init__.py | 28 +-- astrbot/core/computer/tools/browser.py | 14 +- astrbot/core/computer/tools/fs.py | 12 +- astrbot/core/computer/tools/neo_skills.py | 30 +-- astrbot/core/computer/tools/permissions.py | 4 +- astrbot/core/computer/tools/python.py | 12 +- astrbot/core/computer/tools/shell.py | 4 +- astrbot/core/config/astrbot_config.py | 3 +- astrbot/core/config/i18n_utils.py | 22 +- astrbot/core/conversation_mgr.py | 3 +- astrbot/core/core_lifecycle.py | 4 +- astrbot/core/cron/manager.py | 14 +- astrbot/core/db/__init__.py | 15 +- .../core/db/migration/migra_token_usage.py | 8 +- .../db/migration/migra_webchat_session.py | 8 +- astrbot/core/db/migration/sqlite_v3.py | 4 +- astrbot/core/db/po.py | 2 +- astrbot/core/db/sqlite.py | 75 +++--- astrbot/core/db/vec_db/faiss_impl/vec_db.py | 2 +- astrbot/core/event_bus.py | 2 +- astrbot/core/knowledge_base/kb_db_sqlite.py | 2 +- astrbot/core/knowledge_base/kb_helper.py | 41 ++- astrbot/core/knowledge_base/kb_mgr.py | 1 + .../core/knowledge_base/parsers/url_parser.py | 14 +- astrbot/core/log.py | 4 +- astrbot/core/message/components.py | 6 +- astrbot/core/message/message_event_result.py | 22 +- astrbot/core/persona_error_reply.py | 8 +- astrbot/core/persona_mgr.py | 11 +- astrbot/core/pipeline/__init__.py | 2 +- .../pipeline/content_safety_check/stage.py | 2 +- .../strategies/baidu_aip.py | 2 +- .../core/pipeline/process_stage/follow_up.py | 2 +- .../process_stage/method/agent_request.py | 4 +- .../method/agent_sub_stages/internal.py | 20 +- .../method/agent_sub_stages/third_party.py | 15 +- astrbot/core/pipeline/respond/stage.py | 2 +- .../core/pipeline/result_decorate/stage.py | 6 +- astrbot/core/pipeline/waking_check/stage.py | 2 +- astrbot/core/platform/astr_message_event.py | 2 +- astrbot/core/platform/astrbot_message.py | 10 +- astrbot/core/platform/manager.py | 5 +- astrbot/core/platform/platform.py | 3 +- astrbot/core/platform/register.py | 1 + .../aiocqhttp/aiocqhttp_platform_adapter.py | 10 +- .../sources/dingtalk/dingtalk_adapter.py | 98 ++++---- .../discord/discord_platform_adapter.py | 30 +-- .../sources/discord/discord_platform_event.py | 18 +- .../platform/sources/kook/kook_adapter.py | 22 +- .../core/platform/sources/kook/kook_client.py | 36 ++- .../core/platform/sources/kook/kook_event.py | 16 +- .../core/platform/sources/kook/kook_types.py | 23 +- .../platform/sources/lark/lark_adapter.py | 10 +- .../core/platform/sources/lark/lark_event.py | 54 ++-- astrbot/core/platform/sources/lark/server.py | 8 +- .../platform/sources/line/line_adapter.py | 4 +- .../core/platform/sources/line/line_event.py | 2 +- .../platform/sources/mattermost/client.py | 16 +- .../sources/mattermost/mattermost_adapter.py | 8 +- .../sources/mattermost/mattermost_event.py | 8 +- .../sources/misskey/misskey_adapter.py | 4 +- .../platform/sources/misskey/misskey_api.py | 2 +- .../qqofficial/qqofficial_message_event.py | 18 +- .../qqofficial/qqofficial_platform_adapter.py | 25 +- .../qqofficial_webhook/qo_webhook_adapter.py | 10 +- .../qqofficial_webhook/qo_webhook_server.py | 5 +- .../platform/sources/satori/satori_adapter.py | 2 +- astrbot/core/platform/sources/slack/client.py | 5 +- .../platform/sources/slack/slack_adapter.py | 16 +- .../platform/sources/slack/slack_event.py | 10 +- .../platform/sources/telegram/tg_adapter.py | 33 +-- .../platform/sources/telegram/tg_event.py | 51 ++-- .../sources/webchat/message_parts_helper.py | 11 +- .../sources/webchat/webchat_adapter.py | 7 +- .../platform/sources/webchat/webchat_event.py | 6 +- .../sources/webchat/webchat_queue_mgr.py | 8 +- .../platform/sources/wecom/wecom_adapter.py | 29 +-- .../platform/sources/wecom/wecom_event.py | 8 +- .../sources/wecom_ai_bot/WXBizJsonMsgCrypt.py | 2 +- .../sources/wecom_ai_bot/wecomai_adapter.py | 32 ++- .../sources/wecom_ai_bot/wecomai_api.py | 2 +- .../sources/wecom_ai_bot/wecomai_event.py | 15 +- .../wecom_ai_bot/wecomai_long_connection.py | 4 +- .../sources/wecom_ai_bot/wecomai_queue_mgr.py | 4 +- .../sources/wecom_ai_bot/wecomai_server.py | 2 + .../sources/wecom_ai_bot/wecomai_webhook.py | 20 +- .../sources/weixin_oc/weixin_oc_adapter.py | 51 ++-- .../sources/weixin_oc/weixin_oc_client.py | 14 +- .../weixin_offacc_adapter.py | 104 ++++---- .../weixin_offacc_event.py | 18 +- astrbot/core/platform_message_history_mgr.py | 2 +- astrbot/core/provider/entities.py | 2 +- astrbot/core/provider/func_tool_manager.py | 29 +-- astrbot/core/provider/manager.py | 24 +- astrbot/core/provider/provider.py | 12 +- .../core/provider/sources/anthropic_source.py | 28 +-- .../core/provider/sources/azure_tts_source.py | 10 +- .../provider/sources/bailian_rerank_source.py | 28 +-- .../sources/fishaudio_tts_api_source.py | 8 +- .../sources/gemini_embedding_source.py | 2 +- .../core/provider/sources/gemini_source.py | 42 ++-- astrbot/core/provider/sources/groq_source.py | 2 +- .../core/provider/sources/longcat_source.py | 2 +- .../provider/sources/mimo_stt_api_source.py | 2 +- .../provider/sources/mimo_tts_api_source.py | 8 +- .../sources/minimax_tts_api_source.py | 4 +- .../provider/sources/nvidia_rerank_source.py | 24 +- .../provider/sources/oai_aihubmix_source.py | 2 +- .../sources/openai_embedding_source.py | 4 +- .../core/provider/sources/openai_source.py | 24 +- .../provider/sources/openrouter_source.py | 2 +- .../sources/sensevoice_selfhosted_source.py | 4 +- .../provider/sources/whisper_api_source.py | 4 +- .../sources/whisper_selfhosted_source.py | 2 +- astrbot/core/provider/sources/xai_source.py | 2 +- .../sources/xinference_rerank_source.py | 2 +- .../sources/xinference_stt_provider.py | 2 +- astrbot/core/skills/neo_skill_sync.py | 12 +- astrbot/core/skills/skill_manager.py | 30 +-- astrbot/core/star/command_management.py | 10 +- astrbot/core/star/context.py | 29 ++- astrbot/core/star/filter/command.py | 2 +- astrbot/core/star/filter/command_group.py | 4 +- astrbot/core/star/filter/permission.py | 2 +- astrbot/core/star/register/__init__.py | 6 +- astrbot/core/star/register/star_handler.py | 4 +- astrbot/core/star/star_manager.py | 67 +++-- astrbot/core/subagent_orchestrator.py | 4 +- astrbot/core/tools/cron_tools.py | 6 +- astrbot/core/tools/knowledge_base_tools.py | 4 +- astrbot/core/tools/message_tools.py | 16 +- astrbot/core/tools/web_search_tools.py | 235 +++++++++--------- astrbot/core/umop_config_router.py | 2 +- astrbot/core/updator.py | 2 +- astrbot/core/utils/active_event_registry.py | 1 + astrbot/core/utils/core_constraints.py | 2 +- astrbot/core/utils/error_redaction.py | 10 +- astrbot/core/utils/io.py | 6 +- astrbot/core/utils/llm_metadata.py | 53 ++-- astrbot/core/utils/media_utils.py | 31 +-- astrbot/core/utils/migra_helper.py | 11 +- astrbot/core/utils/network_utils.py | 7 +- astrbot/core/utils/pip_installer.py | 26 +- astrbot/core/utils/quoted_message/__init__.py | 2 +- .../core/utils/quoted_message/chain_parser.py | 8 +- .../core/utils/quoted_message/extractor.py | 10 +- .../utils/quoted_message/image_resolver.py | 4 +- astrbot/core/utils/quoted_message_parser.py | 2 +- astrbot/core/utils/requirements_utils.py | 13 +- astrbot/core/utils/session_lock.py | 2 +- astrbot/core/utils/shared_preferences.py | 6 +- astrbot/core/utils/temp_dir_cleaner.py | 6 +- astrbot/core/utils/webhook_utils.py | 5 +- astrbot/core/zip_updator.py | 2 +- astrbot/dashboard/routes/__init__.py | 2 +- astrbot/dashboard/routes/api_key.py | 2 +- astrbot/dashboard/routes/backup.py | 44 ++-- astrbot/dashboard/routes/chat.py | 39 +-- astrbot/dashboard/routes/chatui_project.py | 4 +- astrbot/dashboard/routes/config.py | 81 +++--- astrbot/dashboard/routes/conversation.py | 4 +- astrbot/dashboard/routes/cron.py | 32 +-- astrbot/dashboard/routes/knowledge_base.py | 6 +- astrbot/dashboard/routes/live_chat.py | 60 ++--- astrbot/dashboard/routes/log.py | 4 +- astrbot/dashboard/routes/open_api.py | 53 ++-- astrbot/dashboard/routes/persona.py | 10 +- astrbot/dashboard/routes/platform.py | 5 +- astrbot/dashboard/routes/plugin.py | 15 +- .../dashboard/routes/session_management.py | 74 +++--- astrbot/dashboard/routes/skills.py | 38 +-- astrbot/dashboard/routes/stat.py | 12 +- astrbot/dashboard/routes/subagent.py | 2 +- astrbot/dashboard/routes/t2i.py | 2 +- astrbot/dashboard/routes/tools.py | 20 +- astrbot/dashboard/routes/util.py | 5 - astrbot/dashboard/server.py | 2 +- docs/scripts/sync_docs_to_wiki.py | 15 +- docs/scripts/upload_doc_images_to_r2.py | 14 +- main.py | 2 +- scripts/hatch_build.py | 3 +- typings/faiss/__init__.pyi | 12 +- 233 files changed, 1854 insertions(+), 1837 deletions(-) diff --git a/astrbot/api/event/filter/__init__.py b/astrbot/api/event/filter/__init__.py index f5ab15ed0..71b21a445 100644 --- a/astrbot/api/event/filter/__init__.py +++ b/astrbot/api/event/filter/__init__.py @@ -55,14 +55,14 @@ __all__ = [ "on_decorating_result", "on_llm_request", "on_llm_response", + "on_llm_tool_respond", + "on_platform_loaded", "on_plugin_error", "on_plugin_loaded", "on_plugin_unloaded", - "on_platform_loaded", + "on_using_llm_tool", "on_waiting_llm_request", "permission_type", "platform_adapter_type", "regex", - "on_using_llm_tool", - "on_llm_tool_respond", ] diff --git a/astrbot/builtin_stars/astrbot/long_term_memory.py b/astrbot/builtin_stars/astrbot/long_term_memory.py index e08cdc515..ed97e408a 100644 --- a/astrbot/builtin_stars/astrbot/long_term_memory.py +++ b/astrbot/builtin_stars/astrbot/long_term_memory.py @@ -32,10 +32,10 @@ class LongTermMemory: max_cnt = 300 image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"] image_caption_provider_id = cfg["provider_ltm_settings"].get( - "image_caption_provider_id" + "image_caption_provider_id", ) image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool( - image_caption_provider_id + image_caption_provider_id, ) active_reply = cfg["provider_ltm_settings"]["active_reply"] enable_active_reply = active_reply.get("enable", False) @@ -126,7 +126,7 @@ class LongTermMemory: elif isinstance(comp, Image): if cfg["image_caption"]: try: - url = comp.url if comp.url else comp.file + url = comp.url or comp.file if not url: raise Exception("图片 URL 为空") caption = await self.get_image_caption( @@ -172,7 +172,7 @@ class LongTermMemory: req.system_prompt += chats_str async def after_req_llm( - self, event: AstrMessageEvent, llm_resp: LLMResponse + self, event: AstrMessageEvent, llm_resp: LLMResponse, ) -> None: if event.unified_msg_origin not in self.session_chats: return @@ -180,7 +180,7 @@ class LongTermMemory: if llm_resp.completion_text: final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {llm_resp.completion_text}" logger.debug( - f"Recorded AI response: {event.unified_msg_origin} | {final_message}" + f"Recorded AI response: {event.unified_msg_origin} | {final_message}", ) self.session_chats[event.unified_msg_origin].append(final_message) cfg = self.cfg(event) diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index da2a00835..b5b793ca4 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -86,7 +86,7 @@ class Main(star.Star): @filter.on_llm_request() async def decorate_llm_req( - self, event: AstrMessageEvent, req: ProviderRequest + self, event: AstrMessageEvent, req: ProviderRequest, ) -> None: """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt""" if self.ltm and self.ltm_enabled(event): @@ -97,7 +97,7 @@ class Main(star.Star): @filter.on_llm_response() async def record_llm_resp_to_ltm( - self, event: AstrMessageEvent, resp: LLMResponse + self, event: AstrMessageEvent, resp: LLMResponse, ) -> None: """在 LLM 响应后记录对话""" if self.ltm and self.ltm_enabled(event): diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 5190a363e..cc771840c 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -124,8 +124,8 @@ class ConversationCommands: if stopped_count > 0: message.set_result( MessageEventResult().message( - f"已请求停止 {stopped_count} 个运行中的任务。" - ) + f"已请求停止 {stopped_count} 个运行中的任务。", + ), ) return @@ -207,7 +207,7 @@ class ConversationCommands: """生成所有对话的标题字典""" _titles = {} for conv in conversations_all: - title = conv.title if conv.title else "新对话" + title = conv.title or "新对话" _titles[conv.cid] = title """遍历分页后的对话生成列表显示""" @@ -237,7 +237,7 @@ class ConversationCommands: title = _titles.get(conv.cid, "新对话") parts.append( - f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n" + f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n", ) global_index += 1 @@ -349,7 +349,7 @@ class ConversationCommands: ) else: conversation = conversations[index - 1] - title = conversation.title if conversation.title else "新对话" + title = conversation.title or "新对话" await self.context.conversation_manager.switch_conversation( message.unified_msg_origin, conversation.cid, diff --git a/astrbot/builtin_stars/builtin_commands/commands/help.py b/astrbot/builtin_stars/builtin_commands/commands/help.py index ae2f4c787..f147af160 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/help.py +++ b/astrbot/builtin_stars/builtin_commands/commands/help.py @@ -23,8 +23,7 @@ class HelpCommand: return "" 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/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index 7a7416bba..d1b8be24e 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -45,7 +45,7 @@ class PersonaCommands: children, all_personas, depth + 1, - ) + ), ) return lines @@ -100,7 +100,7 @@ class PersonaCommands: if force_applied_persona_id: curr_persona_name = f"{curr_persona_name} (自定义规则)" - curr_cid_title = conv.title if conv.title else "新对话" + curr_cid_title = conv.title or "新对话" curr_cid_title += f"({cid[:4]})" if len(l) == 1: diff --git a/astrbot/builtin_stars/builtin_commands/commands/provider.py b/astrbot/builtin_stars/builtin_commands/commands/provider.py index b5ee75ca2..166bf6907 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ b/astrbot/builtin_stars/builtin_commands/commands/provider.py @@ -48,7 +48,7 @@ class _ModelCache: return models def set( - self, provider_id: str, umo: str | None, models: list[str], ttl: float + self, provider_id: str, umo: str | None, models: list[str], ttl: float, ) -> None: if ttl <= 0: return @@ -67,7 +67,7 @@ class _ModelCache: self._store.pop(key, None) def invalidate( - self, provider_id: str | None = None, *, umo: str | None = None + self, provider_id: str | None = None, *, umo: str | None = None, ) -> None: if provider_id is None: self._store.clear() @@ -106,7 +106,7 @@ class ProviderCommands: register_change_hook(self._on_provider_manager_changed) def invalidate_provider_models_cache( - self, provider_id: str | None = None, *, umo: str | None = None + self, provider_id: str | None = None, *, umo: str | None = None, ) -> None: """Public hook for cache invalidation on external provider config changes.""" self._model_cache.invalidate(provider_id, umo=umo) @@ -198,14 +198,14 @@ class ProviderCommands: for candidate in models: cand_norm = candidate.casefold() if cand_norm.endswith(f"/{requested_norm}") or cand_norm.endswith( - f":{requested_norm}" + f":{requested_norm}", ): return candidate return None def _apply_model( - self, prov: Provider, model_name: str, *, umo: str | None = None + self, prov: Provider, model_name: str, *, umo: str | None = None, ) -> str: prov.set_model(model_name) self.invalidate_provider_models_cache(prov.meta().id, umo=umo) @@ -287,7 +287,7 @@ class ProviderCommands: err_code = "TEST_FAILED" err_reason = safe_error("", e) self._log_reachability_failure( - provider, provider_capability_type, err_code, err_reason + provider, provider_capability_type, err_code, err_reason, ) return False, err_code, err_reason @@ -339,7 +339,7 @@ class ProviderCommands: return provider, None, err results = await asyncio.gather( - *(fetch_models(provider) for provider in all_providers) + *(fetch_models(provider) for provider in all_providers), ) failed_provider_errors: list[tuple[str, str]] = [] for provider, models, err in results: @@ -405,8 +405,8 @@ class ProviderCommands: if all_providers: await event.send( MessageEventResult().message( - "正在进行提供商可达性测试,请稍候..." - ) + "正在进行提供商可达性测试,请稍候...", + ), ) check_results = await asyncio.gather( *[self._test_provider_capability(p) for p, _ in all_providers], @@ -463,7 +463,7 @@ class ProviderCommands: "info": info, "mark": mark, "provider": p, - } + }, ) # 分组输出 @@ -558,7 +558,7 @@ class ProviderCommands: event.set_result(MessageEventResult().message("无效的参数。")) async def _switch_model_by_name( - self, message: AstrMessageEvent, model_name: str, prov: Provider + self, message: AstrMessageEvent, model_name: str, prov: Provider, ) -> None: model_name = model_name.strip() if not model_name: @@ -583,7 +583,7 @@ class ProviderCommands: if matched_model_name is not None: message.set_result( MessageEventResult().message( - self._apply_model(prov, matched_model_name, umo=umo) + self._apply_model(prov, matched_model_name, umo=umo), ), ) return @@ -620,7 +620,7 @@ class ProviderCommands: except Exception as e: message.set_result( MessageEventResult().message( - safe_error("跨提供商切换并设置模型失败: ", e) + safe_error("跨提供商切换并设置模型失败: ", e), ), ) @@ -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) @@ -680,13 +680,13 @@ class ProviderCommands: prov, new_model, umo=message.unified_msg_origin, - ) + ), ), ) except Exception as e: message.set_result( MessageEventResult().message( - safe_error("切换模型未知错误: ", e) + safe_error("切换模型未知错误: ", e), ), ) return @@ -730,7 +730,7 @@ class ProviderCommands: except Exception as e: message.set_result( MessageEventResult().message( - safe_error("切换 Key 未知错误: ", e) + safe_error("切换 Key 未知错误: ", e), ), ) return diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index fb4a83403..3e4b911b3 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -170,7 +170,7 @@ class Main(star.Star): @filter.command("switch") async def switch_conv( - self, message: AstrMessageEvent, index: int | None = None + self, message: AstrMessageEvent, index: int | None = None, ) -> None: """通过 /ls 前面的序号切换对话""" await self.conversation_c.switch_conv(message, index) diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index 5a39cb2f7..fea1a2316 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -50,7 +50,7 @@ def _validate_timezone(value: str) -> str: zoneinfo.ZoneInfo(value) except Exception: raise click.ClickException( - f"Invalid timezone: {value}. Please use a valid IANA timezone name" + f"Invalid timezone: {value}. Please use a valid IANA timezone name", ) return value @@ -59,7 +59,7 @@ def _validate_callback_api_base(value: str) -> str: """Validate callback API base URL""" if not value.startswith("http://") and not value.startswith("https://"): raise click.ClickException( - "Callback API base must start with http:// or https://" + "Callback API base must start with http:// or https://", ) return value diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index e7e047cca..44a68a5fe 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -48,7 +48,7 @@ def init() -> None: click.echo("Done! You can now run 'astrbot run' to start AstrBot") except Timeout: raise click.ClickException( - "Cannot acquire lock file. Please check if another instance is running" + "Cannot acquire lock file. Please check if another instance is running", ) except Exception as e: diff --git a/astrbot/cli/commands/cmd_plug.py b/astrbot/cli/commands/cmd_plug.py index 46057fc6b..9b7a69f06 100644 --- a/astrbot/cli/commands/cmd_plug.py +++ b/astrbot/cli/commands/cmd_plug.py @@ -33,7 +33,7 @@ def display_plugins(plugins, title=None, color=None) -> None: click.echo(click.style(title, fg=color, bold=True)) click.echo( - f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}" + f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}", ) click.echo("-" * 85) @@ -84,7 +84,7 @@ def new(name: str) -> None: # Rewrite README.md with open(plug_path / "README.md", "w", encoding="utf-8") as f: f.write( - f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://astrbot.app)\n" + f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://astrbot.app)\n", ) # Rewrite main.py @@ -180,7 +180,7 @@ def remove(name: str) -> None: plugin_path = plugin["local_path"] click.confirm( - f"Are you sure you want to uninstall plugin {name}?", default=False, abort=True + f"Are you sure you want to uninstall plugin {name}?", default=False, abort=True, ) try: @@ -211,7 +211,7 @@ def update(name: str, proxy: str | None) -> None: if not plugin: raise click.ClickException( - f"Plugin {name} does not need updating or cannot be updated" + f"Plugin {name} does not need updating or cannot be updated", ) manage_plugin(plugin, plug_path, is_update=True, proxy=proxy) diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index de09e5852..b250f5b11 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -58,7 +58,7 @@ def run(reload: bool, port: str) -> None: click.echo("AstrBot has been shut down.") except Timeout: raise click.ClickException( - "Cannot acquire lock file. Please check if another instance is running" + "Cannot acquire lock file. Please check if another instance is running", ) except Exception as e: raise click.ClickException(f"Runtime error: {e}\n{traceback.format_exc()}") diff --git a/astrbot/cli/utils/plugin.py b/astrbot/cli/utils/plugin.py index c06dda350..6eb71331d 100644 --- a/astrbot/cli/utils/plugin.py +++ b/astrbot/cli/utils/plugin.py @@ -32,7 +32,7 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None: release_url = f"https://api.github.com/repos/{author}/{repo}/releases" try: with httpx.Client( - proxy=proxy if proxy else None, + proxy=proxy or None, follow_redirects=True, ) as client: resp = client.get(release_url) @@ -56,7 +56,7 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None: # Download and extract with httpx.Client( - proxy=proxy if proxy else None, + proxy=proxy or None, follow_redirects=True, ) as client: resp = client.get(download_url) @@ -219,7 +219,7 @@ def manage_plugin( # Check if plugin exists if is_update and not target_path.exists(): raise click.ClickException( - f"Plugin {plugin_name} is not installed and cannot be updated" + f"Plugin {plugin_name} is not installed and cannot be updated", ) # Backup existing plugin @@ -238,7 +238,7 @@ def manage_plugin( if is_update and backup_path is not None and backup_path.exists(): shutil.rmtree(backup_path) click.echo( - f"Plugin {plugin_name} {'updated' if is_update else 'installed'} successfully" + f"Plugin {plugin_name} {'updated' if is_update else 'installed'} successfully", ) except Exception as e: if target_path.exists(): diff --git a/astrbot/cli/utils/version_comparator.py b/astrbot/cli/utils/version_comparator.py index 1f236946c..2bac3a824 100644 --- a/astrbot/cli/utils/version_comparator.py +++ b/astrbot/cli/utils/version_comparator.py @@ -62,12 +62,7 @@ class VersionComparator: return -1 if isinstance(p1, str) and isinstance(p2, int): return 1 - if isinstance(p1, int) and isinstance(p2, int): - if p1 > p2: - return 1 - if p1 < p2: - return -1 - elif isinstance(p1, str) and isinstance(p2, str): + if (isinstance(p1, int) and isinstance(p2, int)) or (isinstance(p1, str) and isinstance(p2, str)): if p1 > p2: return 1 if p1 < p2: diff --git a/astrbot/core/agent/context/compressor.py b/astrbot/core/agent/context/compressor.py index 31a0b0b48..6f6e6e2d7 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -20,13 +20,12 @@ from ..context.truncator import ContextTruncator @runtime_checkable class ContextCompressor(Protocol): - """ - Protocol for context compressors. + """Protocol for context compressors. Provides an interface for compressing message lists. """ def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -37,6 +36,7 @@ class ContextCompressor(Protocol): Returns: True if compression is needed, False otherwise. + """ ... @@ -48,6 +48,7 @@ class ContextCompressor(Protocol): Returns: The compressed message list. + """ ... @@ -58,19 +59,20 @@ class TruncateByTurnsCompressor: """ def __init__( - self, truncate_turns: int = 1, compression_threshold: float = 0.82 + self, truncate_turns: int = 1, compression_threshold: float = 0.82, ) -> None: """Initialize the truncate by turns compressor. Args: truncate_turns: The number of turns to remove when truncating (default: 1). compression_threshold: The compression trigger threshold (default: 0.82). + """ self.truncate_turns = truncate_turns self.compression_threshold = compression_threshold def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -81,6 +83,7 @@ class TruncateByTurnsCompressor: Returns: True if compression is needed, False otherwise. + """ if max_tokens <= 0 or current_tokens <= 0: return False @@ -97,7 +100,7 @@ class TruncateByTurnsCompressor: def split_history( - messages: list[Message], keep_recent: int + messages: list[Message], keep_recent: int, ) -> tuple[list[Message], list[Message], list[Message]]: """Split the message list into system messages, messages to summarize, and recent messages. @@ -109,6 +112,7 @@ def split_history( Returns: tuple: (system_messages, messages_to_summarize, recent_messages) + """ # keep the system messages first_non_system = 0 @@ -162,6 +166,7 @@ class LLMSummaryCompressor: keep_recent: The number of latest messages to keep (default: 4). instruction_text: Custom instruction for summary generation. compression_threshold: The compression trigger threshold (default: 0.82). + """ self.provider = provider self.keep_recent = keep_recent @@ -176,7 +181,7 @@ class LLMSummaryCompressor: ) def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -187,6 +192,7 @@ class LLMSummaryCompressor: Returns: True if compression is needed, False otherwise. + """ if max_tokens <= 0 or current_tokens <= 0: return False @@ -205,7 +211,7 @@ class LLMSummaryCompressor: return messages system_messages, messages_to_summarize, recent_messages = split_history( - messages, self.keep_recent + messages, self.keep_recent, ) if not messages_to_summarize: @@ -231,13 +237,13 @@ class LLMSummaryCompressor: Message( role="user", content=f"Our previous history conversation summary: {summary_content}", - ) + ), ) result.append( Message( role="assistant", content="Acknowledged the summary of our previous conversation history.", - ) + ), ) result.extend(recent_messages) diff --git a/astrbot/core/agent/context/manager.py b/astrbot/core/agent/context/manager.py index 216a3e7e1..49550b423 100644 --- a/astrbot/core/agent/context/manager.py +++ b/astrbot/core/agent/context/manager.py @@ -22,6 +22,7 @@ class ContextManager: Args: config: The context configuration. + """ self.config = config @@ -38,11 +39,11 @@ class ContextManager: ) else: self.compressor = TruncateByTurnsCompressor( - truncate_turns=config.truncate_turns + truncate_turns=config.truncate_turns, ) async def process( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> list[Message]: """Process the messages. @@ -51,6 +52,7 @@ class ContextManager: Returns: The processed message list. + """ try: result = messages @@ -66,11 +68,11 @@ class ContextManager: # 2. 基于 token 的压缩 if self.config.max_context_tokens > 0: total_tokens = self.token_counter.count_tokens( - result, trusted_token_usage + result, trusted_token_usage, ) if self.compressor.should_compress( - result, total_tokens, self.config.max_context_tokens + result, total_tokens, self.config.max_context_tokens, ): result = await self._run_compression(result, total_tokens) @@ -80,10 +82,9 @@ class ContextManager: return messages async def _run_compression( - self, messages: list[Message], prev_tokens: int + self, messages: list[Message], prev_tokens: int, ) -> list[Message]: - """ - Compress/truncate the messages. + """Compress/truncate the messages. Args: messages: The original message list. @@ -91,6 +92,7 @@ class ContextManager: Returns: The compressed/truncated message list. + """ logger.debug("Compress triggered, starting compression...") @@ -109,10 +111,10 @@ class ContextManager: # last check if self.compressor.should_compress( - messages, tokens_after_summary, self.config.max_context_tokens + messages, tokens_after_summary, self.config.max_context_tokens, ): logger.info( - "Context still exceeds max tokens after compression, applying halving truncation..." + "Context still exceeds max tokens after compression, applying halving truncation...", ) # still need compress, truncate by half messages = self.truncator.truncate_by_halving(messages) diff --git a/astrbot/core/agent/context/token_counter.py b/astrbot/core/agent/context/token_counter.py index 7c60cb23e..ec9e53da7 100644 --- a/astrbot/core/agent/context/token_counter.py +++ b/astrbot/core/agent/context/token_counter.py @@ -6,13 +6,12 @@ from ..message import AudioURLPart, ImageURLPart, Message, TextPart, ThinkPart @runtime_checkable class TokenCounter(Protocol): - """ - Protocol for token counters. + """Protocol for token counters. Provides an interface for counting tokens in message lists. """ def count_tokens( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> int: """Count the total tokens in the message list. @@ -24,6 +23,7 @@ class TokenCounter(Protocol): Returns: The total token count. + """ ... @@ -44,7 +44,7 @@ class EstimateTokenCounter: """ def count_tokens( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> int: if trusted_token_usage > 0: return trusted_token_usage diff --git a/astrbot/core/agent/context/truncator.py b/astrbot/core/agent/context/truncator.py index 9abf57433..bc1538216 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -20,6 +20,7 @@ class ContextTruncator: Returns: tuple: (system_messages, non_system_messages) + """ first_non_system = 0 for i, msg in enumerate(messages): @@ -103,8 +104,7 @@ class ContextTruncator: keep_most_recent_turns: int, drop_turns: int = 1, ) -> list[Message]: - """ - Turn-based truncation strategy, which drops the oldest turns while keeping the most recent N turns. + """Turn-based truncation strategy, which drops the oldest turns while keeping the most recent N turns. A turn consists of a user message and an assistant message. This method ensures that the truncated context list conforms to OpenAI's context format. @@ -115,6 +115,7 @@ class ContextTruncator: Returns: The truncated list of messages. + """ if keep_most_recent_turns == -1: return messages @@ -139,7 +140,7 @@ class ContextTruncator: truncated_contexts = truncated_contexts[index:] result = self._ensure_user_message( - system_messages, truncated_contexts, messages + system_messages, truncated_contexts, messages, ) return self.fix_messages(result) @@ -168,7 +169,7 @@ class ContextTruncator: truncated_non_system = truncated_non_system[index:] result = self._ensure_user_message( - system_messages, truncated_non_system, messages + system_messages, truncated_non_system, messages, ) return self.fix_messages(result) @@ -197,6 +198,6 @@ class ContextTruncator: truncated_non_system = truncated_non_system[index:] result = self._ensure_user_message( - system_messages, truncated_non_system, messages + system_messages, truncated_non_system, messages, ) return self.fix_messages(result) diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index aae1aaa77..a5ef336f6 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -27,7 +27,7 @@ try: from mcp.client.sse import sse_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency, MCP services will be unavailable." + "Warning: Missing 'mcp' dependency, MCP services will be unavailable.", ) try: @@ -179,7 +179,7 @@ class MCPClient: # Handle MCP service error logs if isinstance(msg, mcp.types.LoggingMessageNotificationParams): if msg.level in ("warning", "error", "critical", "alert", "emergency"): - log_msg = f"[{msg.level.upper()}] {str(msg.data)}" + log_msg = f"[{msg.level.upper()}] {msg.data!s}" self.server_errlogs.append(log_msg) if "url" in cfg: @@ -258,7 +258,7 @@ class MCPClient: "alert", "emergency", ): - log_msg = f"[{msg.level.upper()}] {str(msg.data)}" + log_msg = f"[{msg.level.upper()}] {msg.data!s}" self.server_errlogs.append(log_msg) stdio_transport = await self.exit_stack.enter_async_context( @@ -294,12 +294,13 @@ class MCPClient: Raises: Exception: raised when reconnection fails + """ async with self._reconnect_lock: # Check if already reconnecting (useful for logging) if self._reconnecting: logger.debug( - f"MCP Client {self._server_name} is already reconnecting, skipping" + f"MCP Client {self._server_name} is already reconnecting, skipping", ) return @@ -309,7 +310,7 @@ class MCPClient: self._reconnecting = True try: logger.info( - f"Attempting to reconnect to MCP server {self._server_name}..." + f"Attempting to reconnect to MCP server {self._server_name}...", ) # Save old exit_stack for later cleanup (don't close it now to avoid cancel scope issues) @@ -327,11 +328,11 @@ class MCPClient: await self.list_tools_and_save() logger.info( - f"Successfully reconnected to MCP server {self._server_name}" + f"Successfully reconnected to MCP server {self._server_name}", ) except Exception as e: logger.error( - f"Failed to reconnect to MCP server {self._server_name}: {e}" + f"Failed to reconnect to MCP server {self._server_name}: {e}", ) raise finally: @@ -356,6 +357,7 @@ class MCPClient: Raises: ValueError: MCP session is not available anyio.ClosedResourceError: raised after reconnection failure + """ @retry( @@ -377,7 +379,7 @@ class MCPClient: ) except anyio.ClosedResourceError: logger.warning( - f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect..." + f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect...", ) # Attempt to reconnect await self._reconnect() @@ -407,7 +409,7 @@ class MCPTool(FunctionTool, Generic[TContext]): """A function tool that calls an MCP service.""" def __init__( - self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs + self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs, ) -> None: super().__init__( name=mcp_tool.name, @@ -419,7 +421,7 @@ class MCPTool(FunctionTool, Generic[TContext]): self.mcp_server_name = mcp_server_name async def call( - self, context: ContextWrapper[TContext], **kwargs + self, context: ContextWrapper[TContext], **kwargs, ) -> mcp.types.CallToolResult: return await self.mcp_client.call_tool_with_reconnect( tool_name=self.mcp_tool.name, diff --git a/astrbot/core/agent/message.py b/astrbot/core/agent/message.py index bde6353ff..e51ff96ab 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -33,7 +33,7 @@ class ContentPart(BaseModel): @classmethod def __get_pydantic_core_schema__( - cls, source_type: Any, handler: GetCoreSchemaHandler + cls, source_type: Any, handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: # If we're dealing with the base ContentPart class, use custom validation if cls.__name__ == "ContentPart": @@ -45,7 +45,7 @@ class ContentPart(BaseModel): # if it's a dict with a type field, dispatch to the appropriate subclass if isinstance(value, dict) and "type" in value: - type_value: Any | None = cast(dict[str, Any], value).get("type") + type_value: Any | None = cast("dict[str, Any]", value).get("type") if not isinstance(type_value, str): raise ValueError(f"Cannot validate {value} as ContentPart") target_class = cls.__content_part_registry[type_value] @@ -60,8 +60,7 @@ class ContentPart(BaseModel): class TextPart(ContentPart): - """ - >>> TextPart(text="Hello, world!").model_dump() + """>>> TextPart(text="Hello, world!").model_dump() {'type': 'text', 'text': 'Hello, world!'} """ @@ -70,8 +69,7 @@ class TextPart(ContentPart): class ThinkPart(ContentPart): - """ - >>> ThinkPart(think="I think I need to think about this.").model_dump() + """>>> ThinkPart(think="I think I need to think about this.").model_dump() {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None} """ @@ -92,8 +90,7 @@ class ThinkPart(ContentPart): class ImageURLPart(ContentPart): - """ - >>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() + """>>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() {'type': 'image_url', 'image_url': 'http://example.com/image.jpg'} """ @@ -108,8 +105,7 @@ class ImageURLPart(ContentPart): class AudioURLPart(ContentPart): - """ - >>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() + """>>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() {'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}} """ @@ -124,8 +120,7 @@ class AudioURLPart(ContentPart): class ToolCall(BaseModel): - """ - A tool call requested by the assistant. + """A tool call requested by the assistant. >>> ToolCall( ... id="123", @@ -195,7 +190,7 @@ class Message(BaseModel): # other all cases: content is required if self.content is None: raise ValueError( - "content is required unless role='assistant' and tool_calls is not None" + "content is required unless role='assistant' and tool_calls is not None", ) return self diff --git a/astrbot/core/agent/runners/base.py b/astrbot/core/agent/runners/base.py index 21e796433..ac9eae8f1 100644 --- a/astrbot/core/agent/runners/base.py +++ b/astrbot/core/agent/runners/base.py @@ -39,7 +39,7 @@ class BaseAgentRunner(T.Generic[TContext]): @abc.abstractmethod async def step_until_done( - self, max_step: int + self, max_step: int, ) -> T.AsyncGenerator[AgentResponse, None]: """Process steps until the agent is done.""" ... diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index a8300bb71..4ab4c9267 100644 --- a/astrbot/core/agent/runners/coze/coze_agent_runner.py +++ b/astrbot/core/agent/runners/coze/coze_agent_runner.py @@ -71,8 +71,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): @override async def step(self): - """ - 执行 Coze Agent 的一个步骤 + """执行 Coze Agent 的一个步骤 """ if not self.req: raise ValueError("Request is not set. Please call reset() first.") @@ -91,15 +90,15 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_coze_request(): yield response except Exception as e: - logger.error(f"Coze 请求失败:{str(e)}") + logger.error(f"Coze 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Coze 请求失败:{str(e)}" + role="err", completion_text=f"Coze 请求失败:{e!s}", ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Coze 请求失败:{str(e)}") + chain=MessageChain().message(f"Coze 请求失败:{e!s}"), ), ) finally: @@ -107,7 +106,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): @override async def step_until_done( - self, max_step: int = 30 + self, max_step: int = 30, ) -> T.AsyncGenerator[AgentResponse, None]: while not self.done(): async for resp in self.step(): @@ -166,7 +165,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): if url: file_id = ( await self._download_and_upload_image( - url, session_id + url, session_id, ) ) processed_content.append( @@ -174,7 +173,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "type": "file", "file_id": file_id, "file_url": url, - } + }, ) except Exception as e: logger.warning(f"处理上下文图片失败: {e}") @@ -186,7 +185,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": ctx["role"], "content": processed_content, "content_type": "object_string", - } + }, ) else: # 纯文本内容 @@ -195,7 +194,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": ctx["role"], "content": content, "content_type": "text", - } + }, ) # 构建当前消息 @@ -215,7 +214,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): { "type": "image", "file_id": file_id, - } + }, ) except Exception as e: logger.warning(f"处理图片失败 {url}: {e}") @@ -228,7 +227,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": "user", "content": content, "content_type": "object_string", - } + }, ) elif prompt: # 纯文本 @@ -282,7 +281,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): yield AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(content) + chain=MessageChain().message(content), ), ) diff --git a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py index 8169a678c..14b7201ab 100644 --- a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +++ b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py @@ -83,8 +83,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): @override async def step(self): - """ - 执行 Dashscope Agent 的一个步骤 + """执行 Dashscope Agent 的一个步骤 """ if not self.req: raise ValueError("Request is not set. Please call reset() first.") @@ -103,28 +102,28 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_dashscope_request(): yield response except Exception as e: - logger.error(f"阿里云百炼请求失败:{str(e)}") + logger.error(f"阿里云百炼请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"阿里云百炼请求失败:{str(e)}" + role="err", completion_text=f"阿里云百炼请求失败:{e!s}", ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"阿里云百炼请求失败:{str(e)}") + chain=MessageChain().message(f"阿里云百炼请求失败:{e!s}"), ), ) @override async def step_until_done( - self, max_step: int = 30 + self, max_step: int = 30, ) -> T.AsyncGenerator[AgentResponse, None]: while not self.done(): async for resp in self.step(): yield resp def _consume_sync_generator( - self, response: T.Any, response_queue: queue.Queue + self, response: T.Any, response_queue: queue.Queue, ) -> None: """在线程中消费同步generator,将结果放入队列 @@ -145,7 +144,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): response_queue.put(("done", None)) async def _process_stream_chunk( - self, chunk: ApplicationResponse, output_text: str + self, chunk: ApplicationResponse, output_text: str, ) -> tuple[str, list | None, AgentResponse | None]: """处理流式响应的单个chunk @@ -217,7 +216,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): return f"\n\n回答来源:\n{ref_str}" async def _build_request_payload( - self, prompt: str, session_id: str, contexts: list, system_prompt: str + self, prompt: str, session_id: str, contexts: list, system_prompt: str, ) -> dict: """构建请求payload @@ -263,22 +262,21 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): if conversation_id: p["session_id"] = conversation_id return p - else: - # 不支持多轮对话的 - payload = { - "app_id": self.app_id, - "prompt": prompt, - "api_key": self.api_key, - "biz_params": payload_vars or None, - "stream": self.streaming, - "incremental_output": True, - } - if self.rag_options: - payload["rag_options"] = self.rag_options - return payload + # 不支持多轮对话的 + payload = { + "app_id": self.app_id, + "prompt": prompt, + "api_key": self.api_key, + "biz_params": payload_vars or None, + "stream": self.streaming, + "incremental_output": True, + } + if self.rag_options: + payload["rag_options"] = self.rag_options + return payload async def _handle_streaming_response( - self, response: T.Any, session_id: str + self, response: T.Any, session_id: str, ) -> T.AsyncGenerator[AgentResponse, None]: """处理流式响应 @@ -303,7 +301,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): while True: try: item_type, item_data = await asyncio.get_running_loop().run_in_executor( - None, response_queue.get, True, 1 + None, response_queue.get, True, 1, ) except queue.Empty: continue @@ -380,7 +378,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): # 构建请求payload payload = await self._build_request_payload( - prompt, session_id, contexts, system_prompt + prompt, session_id, contexts, system_prompt, ) if not self.streaming: diff --git a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py index 50ec7c826..58740fe2e 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py @@ -130,7 +130,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) async def _finish_with_result( - self, chain: MessageChain, role: str + self, chain: MessageChain, role: str, ) -> AgentResponse: self.final_llm_resp = LLMResponse( role=role, @@ -247,7 +247,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): await old_client.close() except Exception as e: logger.warning( - f"Failed to close previous DeerFlow API client cleanly: {e}" + f"Failed to close previous DeerFlow API client cleanly: {e}", ) self.api_client = DeerFlowAPIClient( @@ -304,7 +304,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): @override async def step_until_done( - self, max_step: int = 30 + self, max_step: int = 30, ) -> T.AsyncGenerator[AgentResponse, None]: if max_step <= 0: raise ValueError("max_step must be greater than 0") @@ -317,7 +317,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): if not self.done(): raise RuntimeError( - f"DeerFlow agent reached max_step ({max_step}) without completion." + f"DeerFlow agent reached max_step ({max_step}) without completion.", ) def _extract_new_messages_from_values( @@ -382,7 +382,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): thread_id = thread.get("thread_id", "") if not thread_id: raise Exception( - f"DeerFlow create thread returned invalid payload: {thread}" + f"DeerFlow create thread returned invalid payload: {thread}", ) await sp.put_async( @@ -468,7 +468,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): AgentResponse( type="streaming_delta", data=AgentResponseData(chain=MessageChain().message(delta)), - ) + ), ] if delta_text: @@ -478,9 +478,9 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(delta_text) + chain=MessageChain().message(delta_text), ), - ) + ), ] return [] @@ -532,7 +532,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): self._update_text_and_maybe_stream( state=state, new_full_text=latest_text or None, - ) + ), ) return responses @@ -549,7 +549,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): self._update_text_and_maybe_stream( state=state, delta_text=delta, - ) + ), ) maybe_clarification = extract_clarification_from_event_data(data) diff --git a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py index 37a23f243..3d7a557ef 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py @@ -168,7 +168,7 @@ class DeerFlowAPIClient: input_payload = payload.get("input") message_count = 0 if isinstance(input_payload, dict) and isinstance( - input_payload.get("messages"), list + input_payload.get("messages"), list, ): message_count = len(input_payload["messages"]) # Log only a minimal summary to avoid exposing sensitive user content. @@ -237,7 +237,7 @@ class DeerFlowAPIClient: return logger.warning( "DeerFlowAPIClient garbage collected with unclosed session; " - "explicit close() should be called by runner lifecycle (or `async with`)." + "explicit close() should be called by runner lifecycle (or `async with`).", ) @property diff --git a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py index 2477adbb9..87cb691f4 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py @@ -58,7 +58,7 @@ def build_user_content(prompt: str, image_urls: list[str]) -> Any: if not is_likely_base64_image(url): skipped_invalid_images += 1 logger.debug( - "Skipped DeerFlow image input because it is neither URL/data URI nor valid base64." + "Skipped DeerFlow image input because it is neither URL/data URI nor valid base64.", ) continue compact_base64 = url.replace("\n", "").replace("\r", "") @@ -164,14 +164,14 @@ def append_components_from_content( if "content" in content: append_components_from_content( - content.get("content"), components, image_resolver + content.get("content"), components, image_resolver, ) return kwargs = content.get("kwargs") if isinstance(kwargs, dict) and "content" in kwargs: append_components_from_content( - kwargs.get("content"), components, image_resolver + kwargs.get("content"), components, image_resolver, ) diff --git a/astrbot/core/agent/runners/dify/dify_agent_runner.py b/astrbot/core/agent/runners/dify/dify_agent_runner.py index 93f8d3570..b438c182c 100644 --- a/astrbot/core/agent/runners/dify/dify_agent_runner.py +++ b/astrbot/core/agent/runners/dify/dify_agent_runner.py @@ -64,8 +64,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): @override async def step(self): - """ - 执行 Dify Agent 的一个步骤 + """执行 Dify Agent 的一个步骤 """ if not self.req: raise ValueError("Request is not set. Please call reset() first.") @@ -84,15 +83,15 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): async for response in self._execute_dify_request(): yield response except Exception as e: - logger.error(f"Dify 请求失败:{str(e)}") + logger.error(f"Dify 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Dify 请求失败:{str(e)}" + role="err", completion_text=f"Dify 请求失败:{e!s}", ) yield AgentResponse( type="err", data=AgentResponseData( - chain=MessageChain().message(f"Dify 请求失败:{str(e)}") + chain=MessageChain().message(f"Dify 请求失败:{e!s}"), ), ) finally: @@ -100,7 +99,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): @override async def step_until_done( - self, max_step: int = 30 + self, max_step: int = 30, ) -> T.AsyncGenerator[AgentResponse, None]: while not self.done(): async for resp in self.step(): @@ -136,7 +135,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): 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( @@ -144,7 +143,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): "type": "image", "transfer_method": "local_file", "upload_file_id": file_response["id"], - } + }, ) except Exception as e: logger.warning(f"上传图片失败:{e}") @@ -195,7 +194,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): yield AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(chunk["answer"]) + chain=MessageChain().message(chunk["answer"]), ), ) elif chunk["event"] == "message_end": @@ -204,7 +203,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): elif chunk["event"] == "error": logger.error(f"Dify 出现错误:{chunk}") raise Exception( - f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}" + f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}", ) case "workflow": @@ -222,11 +221,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"]: @@ -234,25 +233,25 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): type="streaming_delta", data=AgentResponseData( chain=MessageChain().message( - chunk["data"]["text"] - ) + chunk["data"]["text"], + ), ), ) case "workflow_finished": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束" + f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束", ) 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 _: diff --git a/astrbot/core/agent/runners/dify/dify_api_client.py b/astrbot/core/agent/runners/dify/dify_api_client.py index 26da6dfe9..9ef427172 100644 --- a/astrbot/core/agent/runners/dify/dify_api_client.py +++ b/astrbot/core/agent/runners/dify/dify_api_client.py @@ -114,8 +114,10 @@ class DifyAPIClient: file_path: The path to the file to upload. file_data: The file data in bytes. file_name: Optional file name when using file_data. + Returns: A dictionary containing the uploaded file information. + """ url = f"{self.api_base}/files/upload" diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 9d0b0ffce..a47f4ff4b 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -72,7 +72,7 @@ class _HandleFunctionToolsResult: @classmethod def from_tool_call_result_blocks( - cls, blocks: list[ToolCallMessageSegment] + cls, blocks: list[ToolCallMessageSegment], ) -> "_HandleFunctionToolsResult": return cls(kind="tool_call_result_blocks", tool_call_result_blocks=blocks) @@ -169,7 +169,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -299,7 +299,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): self.stats.start_time = time.time() async def _iter_llm_responses( - self, *, include_model: bool = True + self, *, include_model: bool = True, ) -> T.AsyncGenerator[LLMResponse, None]: """Yields chunks *and* a final LLMResponse.""" payload = { @@ -355,7 +355,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): with attempt: try: async for resp in self._iter_llm_responses( - include_model=idx == 0 + include_model=idx == 0, ): if resp.is_chunk: has_stream_output = True @@ -522,7 +522,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): token_usage = self.req.conversation.token_usage if self.req.conversation else 0 self._simple_print_message_role("[BefCompact]") self.run_context.messages = await self.context_manager.process( - self.run_context.messages, trusted_token_usage=token_usage + self.run_context.messages, trusted_token_usage=token_usage, ) self._simple_print_message_role("[AftCompact]") @@ -625,7 +625,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): llm_resp, _ = await self._resolve_tool_exec(llm_resp) if not llm_resp.tools_call_name: logger.warning( - "skills_like tool re-query returned no tool calls; fallback to assistant response." + "skills_like tool re-query returned no tool calls; fallback to assistant response.", ) if llm_resp.result_chain: yield AgentResponse( @@ -677,7 +677,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -692,7 +692,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ) # record the assistant message with tool calls self.run_context.messages.extend( - tool_calls_result.to_openai_messages_model() + tool_calls_result.to_openai_messages_model(), ) # If there are cached images and the model supports image input, @@ -705,35 +705,35 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): image_parts = [] for cached_img in cached_images: img_data = tool_image_cache.get_image_base64_by_path( - cached_img.file_path, cached_img.mime_type + cached_img.file_path, cached_img.mime_type, ) if img_data: base64_data, mime_type = img_data image_parts.append( TextPart( - text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']" - ) + text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']", + ), ) image_parts.append( ImageURLPart( image_url=ImageURLPart.ImageURL( url=f"data:{mime_type};base64,{base64_data}", id=cached_img.file_path, - ) - ) + ), + ), ) if image_parts: self.run_context.messages.append( - Message(role="user", content=image_parts) + Message(role="user", content=image_parts), ) logger.debug( - f"Appended {len(cached_images)} cached image(s) to context for LLM review" + f"Appended {len(cached_images)} cached image(s) to context for LLM review", ) self.req.append_tool_calls_result(tool_calls_result) async def step_until_done( - self, max_step: int + self, max_step: int, ) -> T.AsyncGenerator[AgentResponse, None]: """Process steps until the agent is done.""" step_count = 0 @@ -745,7 +745,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step if not self.done(): logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + f"Agent reached max steps ({max_step}), forcing a final response.", ) # 拔掉所有工具 if self.req: @@ -755,7 +755,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): Message( role="user", content=self.MAX_STEPS_REACHED_PROMPT, - ) + ), ) # 再执行最后一步 async for resp in self.step(): @@ -796,10 +796,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "name": func_tool_name, "args": func_tool_args, "ts": time.time(), - } - ) + }, + ), ], - ) + ), ) try: if not req.func_tool: @@ -896,11 +896,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility (will be handled in step()) yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) elif isinstance(content_item, EmbeddedResource): resource = content_item.resource @@ -922,22 +922,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) else: result_parts.append( - "The tool has returned a data type that is not supported." + "The tool has returned a data type that is not supported.", ) if result_parts: _append_tool_call_result( func_tool_id, "\n\n".join(result_parts) + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -946,7 +946,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() @@ -954,7 +954,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "The tool has no return value, or has sent the result directly to the user." + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) else: @@ -966,7 +966,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -987,7 +987,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, f"error: {e!s}" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -1003,17 +1003,17 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "id": func_tool_id, "ts": time.time(), "result": last_tcr_content, - } - ) + }, + ), ], - ) + ), ) logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}") # 处理函数调用响应 if tool_call_result_blocks: yield _HandleFunctionToolsResult.from_tool_call_result_blocks( - tool_call_result_blocks + tool_call_result_blocks, ) def _build_tool_requery_context( @@ -1029,7 +1029,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): elif isinstance(msg, dict): contexts.append(copy.deepcopy(msg)) instruction = self.SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE.format( - tool_names=", ".join(tool_names) + tool_names=", ".join(tool_names), ) if extra_instruction: instruction = f"{instruction}\n{extra_instruction}" @@ -1072,7 +1072,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if isinstance(self._tool_schema_param_set, ToolSet): param_subset = self._build_tool_subset( - self._tool_schema_param_set, tool_names + self._tool_schema_param_set, tool_names, ) if param_subset.tools and tool_names: contexts = self._build_tool_requery_context(tool_names) @@ -1096,7 +1096,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): and not self._has_meaningful_assistant_reply(llm_resp) ): logger.warning( - "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction." + "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction.", ) repair_contexts = self._build_tool_requery_context( tool_names, @@ -1155,7 +1155,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -1188,7 +1188,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if self._is_stop_requested(): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted before reading the next tool result." + "Tool execution interrupted before reading the next tool result.", ) next_result_task = asyncio.create_task(anext(executor)) @@ -1208,7 +1208,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted by a stop request." + "Tool execution interrupted by a stop request.", ) try: diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 4cee6ba6d..d5a8f6338 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -32,7 +32,7 @@ class ToolSchema: @model_validator(mode="after") def validate_parameters(self) -> "ToolSchema": jsonschema.validate( - self.parameters, jsonschema.Draft202012Validator.META_SCHEMA + self.parameters, jsonschema.Draft202012Validator.META_SCHEMA, ) return self @@ -70,7 +70,7 @@ class FunctionTool(ToolSchema, Generic[TContext]): async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult: """Run the tool with the given arguments. The handler field has priority.""" raise NotImplementedError( - "FunctionTool.call() must be implemented by subclasses or set a handler." + "FunctionTool.call() must be implemented by subclasses or set a handler.", ) @@ -134,7 +134,7 @@ class ToolSet: parameters=light_params, description=tool.description, handler=None, - ) + ), ) return ToolSet(light_tools) @@ -155,7 +155,7 @@ class ToolSet: parameters=params, description="", handler=None, - ) + ), ) return ToolSet(param_tools) diff --git a/astrbot/core/agent/tool_image_cache.py b/astrbot/core/agent/tool_image_cache.py index 72e22dd52..0bcf44791 100644 --- a/astrbot/core/agent/tool_image_cache.py +++ b/astrbot/core/agent/tool_image_cache.py @@ -86,6 +86,7 @@ class ToolImageCache: Returns: CachedImage object with file path. + """ ext = self._get_file_extension(mime_type) file_name = f"{tool_call_id}_{index}{ext}" @@ -109,7 +110,7 @@ class ToolImageCache: ) def get_image_base64_by_path( - self, file_path: str, mime_type: str = "image/png" + self, file_path: str, mime_type: str = "image/png", ) -> tuple[str, str] | None: """Read an image file and return its base64 encoded data. @@ -119,6 +120,7 @@ class ToolImageCache: Returns: Tuple of (base64_data, mime_type) if found, None otherwise. + """ if not os.path.exists(file_path): return None @@ -137,6 +139,7 @@ class ToolImageCache: Returns: Number of images cleaned up. + """ now = time.time() cleaned = 0 diff --git a/astrbot/core/astr_agent_hooks.py b/astrbot/core/astr_agent_hooks.py index a67d7b49d..852f8f6c1 100644 --- a/astrbot/core/astr_agent_hooks.py +++ b/astrbot/core/astr_agent_hooks.py @@ -17,7 +17,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): if llm_response and llm_response.reasoning_content: # we will use this in result_decorate stage to inject reasoning content to chain run_context.context.event.set_extra( - "_llm_reasoning_content", llm_response.reasoning_content + "_llm_reasoning_content", llm_response.reasoning_content, ) await call_event_hook( diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index eca24699a..02c29a925 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -47,7 +47,7 @@ def _extract_chain_json_data(msg_chain: MessageChain) -> dict | None: def _record_tool_call_name( - tool_info: dict | None, tool_name_by_call_id: dict[str, str] + tool_info: dict | None, tool_name_by_call_id: dict[str, str], ) -> None: if not isinstance(tool_info, dict): return @@ -65,7 +65,7 @@ def _build_tool_call_status_message(tool_info: dict | None) -> str: def _build_tool_result_status_message( - msg_chain: MessageChain, tool_name_by_call_id: dict[str, str] + msg_chain: MessageChain, tool_name_by_call_id: dict[str, str], ) -> str: tool_name = "unknown" tool_result = "" @@ -103,7 +103,7 @@ async def run_agent( if step_idx == max_step + 1: logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + f"Agent reached max steps ({max_step}), forcing a final response.", ) if not agent_runner.done(): # 拔掉所有工具 @@ -114,7 +114,7 @@ async def run_agent( Message( role="user", content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", - ) + ), ) stop_watcher = asyncio.create_task( @@ -145,7 +145,7 @@ async def run_agent( astr_event.trace.record( "agent_tool_result", tool_result=msg_chain.get_plain_text( - with_other_comps_mark=True + with_other_comps_mark=True, ), ) @@ -157,10 +157,10 @@ async def run_agent( await astr_event.send(msg_chain) elif show_tool_use and show_tool_call_result: status_msg = _build_tool_result_status_message( - msg_chain, tool_name_by_call_id + msg_chain, tool_name_by_call_id, ) await astr_event.send( - MessageChain(type="tool_call").message(status_msg) + MessageChain(type="tool_call").message(status_msg), ) # 对于其他情况,暂时先不处理 continue @@ -177,7 +177,7 @@ async def run_agent( tool_info = _extract_chain_json_data(resp.data["chain"]) astr_event.trace.record( "agent_tool_call", - tool_name=tool_info if tool_info else "unknown", + tool_name=tool_info or "unknown", ) _record_tool_call_name(tool_info, tool_name_by_call_id) @@ -188,7 +188,7 @@ async def run_agent( # Delay tool status notification until tool_call_result. continue chain = MessageChain(type="tool_call").message( - _build_tool_call_status_message(tool_info) + _build_tool_call_status_message(tool_info), ) await astr_event.send(chain) continue @@ -229,7 +229,7 @@ async def run_agent( MessageChain( type="agent_stats", chain=[Json(data=agent_runner.stats.to_dict())], - ) + ), ) break @@ -244,7 +244,7 @@ async def run_agent( logger.error(traceback.format_exc()) custom_error_message = extract_persona_custom_error_message_from_event( - astr_event + astr_event, ) if custom_error_message: err_msg = custom_error_message @@ -252,7 +252,7 @@ async def run_agent( err_msg = ( f"Error occurred during AI execution.\n" f"Error Type: {type(e).__name__}\n" - f"Error Message: {str(e)}" + f"Error Message: {e!s}" ) error_llm_response = LLMResponse( @@ -261,7 +261,7 @@ async def run_agent( ) try: await agent_runner.agent_hooks.on_agent_done( - agent_runner.run_context, error_llm_response + agent_runner.run_context, error_llm_response, ) except Exception: logger.exception("Error in on_agent_done hook") @@ -301,6 +301,7 @@ async def run_live_agent( Yields: MessageChain: 包含文本或音频数据的消息链 + """ # 如果没有 TTS Provider,直接发送文本 if not tts_provider: @@ -321,7 +322,7 @@ async def run_live_agent( else: logger.info( f"[Live Agent] 使用 TTS({tts_provider.meta().type} " - "使用 get_audio,将按句子分块生成音频)" + "使用 get_audio,将按句子分块生成音频)", ) # 统计数据初始化 @@ -343,17 +344,17 @@ async def run_live_agent( show_tool_use, show_tool_call_result, show_reasoning, - ) + ), ) # 2. 启动 TTS 任务:负责从 text_queue 读取文本并生成音频到 audio_queue if support_stream: tts_task = asyncio.create_task( - _safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue) + _safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue), ) else: tts_task = asyncio.create_task( - _simulated_stream_tts(tts_provider, text_queue, audio_queue) + _simulated_stream_tts(tts_provider, text_queue, audio_queue), ) # 3. 主循环:从 audio_queue 读取音频并 yield @@ -395,7 +396,6 @@ async def run_live_agent( tts_task.cancel() # 确保队列被消费 - pass tts_end_time = time.time() @@ -414,10 +414,10 @@ async def run_live_agent( "tts_first_frame_time": tts_first_frame_time, "tts": tts_provider.meta().type, "chat_model": agent_runner.provider.get_model(), - } - ) + }, + ), ], - ) + ), ) except Exception as e: logger.error(f"发送 TTS 统计信息失败: {e}") @@ -519,7 +519,7 @@ async def _simulated_stream_tts( await audio_queue.put((text, audio_data)) except Exception as e: logger.error( - f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}" + f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}", ) # 继续处理下一句 diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 18ac1a446..40becfa45 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -53,7 +53,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): return [image_urls_raw] if isinstance(image_urls_raw, (Sequence, AbstractSet)) and not isinstance( - image_urls_raw, (str, bytes, bytearray) + image_urls_raw, (str, bytes, bytearray), ): return [item for item in image_urls_raw if isinstance(item, str)] @@ -65,7 +65,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @classmethod async def _collect_image_urls_from_message( - cls, run_context: ContextWrapper[AstrAgentContext] + cls, run_context: ContextWrapper[AstrAgentContext], ) -> list[str]: urls: list[str] = [] event = getattr(run_context.context, "event", None) @@ -133,7 +133,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): is_bg = tool_args.pop("background_task", False) if is_bg: async for r in cls._execute_handoff_background( - tool, run_context, **tool_args + tool, run_context, **tool_args, ): yield r return @@ -273,7 +273,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): # Use per-subagent provider override if configured; otherwise fall back # to the current/default provider resolution. prov_id = getattr( - tool, "provider_id", None + tool, "provider_id", None, ) or await ctx.get_current_chat_provider_id(umo) # prepare begin dialogs @@ -286,7 +286,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): contexts.append( dialog if isinstance(dialog, Message) - else Message.model_validate(dialog) + else Message.model_validate(dialog), ) except Exception: continue @@ -307,7 +307,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): stream=stream, ) yield mcp.types.CallToolResult( - content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)] + content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)], ) @classmethod @@ -412,7 +412,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): result_text = "" try: async for r in cls._execute_local( - tool, run_context, tool_call_timeout=3600, **tool_args + tool, run_context, tool_call_timeout=3600, **tool_args, ): # collect results, currently we just collect the text results if isinstance(r, mcp.types.CallToolResult): @@ -503,7 +503,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): bg = json.dumps(extras["background_task_result"], ensure_ascii=False) req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format( - background_task_result=bg + background_task_result=bg, ) req.prompt = ( "Proceed according to your system instructions. " @@ -516,11 +516,11 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): if not req.func_tool: req.func_tool = ToolSet() req.func_tool.add_tool( - ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool) + ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool), ) result = await build_main_agent( - event=cron_event, plugin_context=ctx, config=config, req=req + event=cron_event, plugin_context=ctx, config=config, req=req, ) if not result: logger.error(f"Failed to build main agent for background task {tool_name}.") @@ -583,7 +583,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): awaitable = tool.call method_name = "call" elif hasattr(tool, "run"): - awaitable = getattr(tool, "run") + awaitable = tool.run method_name = "run" if awaitable is None: raise ValueError("Tool must have a valid handler or override 'run' method.") @@ -620,7 +620,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): MessageChain( chain=res.chain, type="tool_direct_result", - ) + ), ) except Exception as e: logger.error( @@ -705,7 +705,7 @@ async def call_local_llm_tool( handler_param_str = "(unable to inspect signature)" raise Exception( - f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}" + f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}", ) from e except Exception as e: trace_ = traceback.format_exc() diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index ebb95baa4..9126c0ded 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -96,7 +96,8 @@ from astrbot.core.utils.string_utils import normalize_and_dedupe_strings @dataclass(slots=True) class MainAgentBuildConfig: """The main agent build configuration. - Most of the configs can be found in the cmd_config.json""" + Most of the configs can be found in the cmd_config.json + """ tool_call_timeout: int """The timeout (in seconds) for a tool call. @@ -160,7 +161,7 @@ class MainAgentBuildResult: def _select_provider( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, plugin_context: Context, ) -> Provider | None: """Select chat provider for the event.""" sel_provider = event.get_extra("selected_provider") @@ -170,7 +171,7 @@ def _select_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 @@ -182,7 +183,7 @@ def _select_provider( async def _get_session_conv( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, plugin_context: Context, ) -> Conversation: conv_mgr = plugin_context.conversation_manager umo = event.unified_msg_origin @@ -226,8 +227,8 @@ async def _apply_kb( req.func_tool = ToolSet() req.func_tool.add_tool( plugin_context.get_llm_tool_manager().get_builtin_tool( - KnowledgeBaseQueryTool - ) + KnowledgeBaseQueryTool, + ), ) @@ -262,7 +263,7 @@ async def _apply_file_extract( config.file_extract_msh_api_key, ) for file_path in file_paths - ] + ], ) else: logger.error("Unsupported file extract provider: %s", config.file_extract_prov) @@ -336,7 +337,7 @@ async def _ensure_persona_and_skills( ) set_persona_custom_error_message_on_event( - event, extract_persona_custom_error_message_from_persona(persona) + event, extract_persona_custom_error_message_from_persona(persona), ) if persona: @@ -417,7 +418,7 @@ async def _ensure_persona_and_skills( tool.name for tool in tmgr.func_list if not isinstance(tool, HandoffTool) - ] + ], ) continue if not isinstance(tools, list): @@ -509,7 +510,7 @@ async def _ensure_img_caption( ) if caption: req.extra_user_content_parts.append( - TextPart(text=f"{caption}") + TextPart(text=f"{caption}"), ) req.image_urls = [] except Exception as exc: # noqa: BLE001 @@ -521,19 +522,19 @@ async def _ensure_img_caption( def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment in quoted message: path {image_path}]") + TextPart(text=f"[Image Attachment in quoted message: path {image_path}]"), ) def _append_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment: path {audio_path}]") + TextPart(text=f"[Audio Attachment: path {audio_path}]"), ) def _append_quoted_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]") + TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]"), ) @@ -659,7 +660,7 @@ async def _process_quote_message( ) if llm_resp.completion_text: content_parts.append( - f"[Image Caption in quoted message]: {llm_resp.completion_text}" + f"[Image Caption in quoted message]: {llm_resp.completion_text}", ) else: logger.warning("No provider found for image captioning in quote.") @@ -732,7 +733,7 @@ async def _decorate_llm_request( config: MainAgentBuildConfig, ) -> None: cfg = config.provider_settings or plugin_context.get_config( - umo=event.unified_msg_origin + umo=event.unified_msg_origin, ).get("provider_settings", {}) _apply_prompt_prefix(req, cfg) @@ -772,7 +773,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["image"]) if "image" not in provider_cfg: logger.debug( - "Provider %s does not support image, using placeholder.", provider + "Provider %s does not support image, using placeholder.", provider, ) image_count = len(req.image_urls) placeholder = " ".join(["[Image]"] * image_count) @@ -785,7 +786,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["audio"]) if "audio" not in provider_cfg: logger.debug( - "Provider %s does not support audio, using placeholder.", provider + "Provider %s does not support audio, using placeholder.", provider, ) audio_count = len(req.audio_urls) placeholder = " ".join(["[Audio]"] * audio_count) @@ -798,7 +799,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["tool_use"]) if "tool_use" not in provider_cfg: logger.debug( - "Provider %s does not support tool_use, clearing tools.", provider + "Provider %s does not support tool_use, clearing tools.", provider, ) req.func_tool = None @@ -927,7 +928,7 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: async def _handle_webchat( - event: AstrMessageEvent, req: ProviderRequest, prov: Provider + event: AstrMessageEvent, req: ProviderRequest, prov: Provider, ) -> None: from astrbot.core import db_helper @@ -962,7 +963,7 @@ async def _handle_webchat( if not title or "" in title: return logger.info( - "Generated chatui title for session %s: %s", chatui_session_id, title + "Generated chatui title for session %s: %s", chatui_session_id, title, ) await db_helper.update_platform_session( session_id=chatui_session_id, @@ -981,7 +982,7 @@ def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) - def _apply_sandbox_tools( - config: MainAgentBuildConfig, req: ProviderRequest, session_id: str + config: MainAgentBuildConfig, req: ProviderRequest, session_id: str, ) -> None: if req.func_tool is None: req.func_tool = ToolSet() @@ -1092,7 +1093,7 @@ async def _apply_web_search_tools( def _get_compress_provider( - config: MainAgentBuildConfig, plugin_context: Context + config: MainAgentBuildConfig, plugin_context: Context, ) -> Provider | None: if not config.llm_compress_provider_id: return None @@ -1115,12 +1116,12 @@ def _get_compress_provider( def _get_fallback_chat_providers( - provider: Provider, plugin_context: Context, provider_settings: dict + provider: Provider, plugin_context: Context, provider_settings: dict, ) -> list[Provider]: fallback_ids = provider_settings.get("fallback_chat_models", []) if not isinstance(fallback_ids, list): logger.warning( - "fallback_chat_models setting is not a list, skip fallback providers." + "fallback_chat_models setting is not a list, skip fallback providers.", ) return [] @@ -1183,7 +1184,7 @@ async def build_main_agent( if sel_model := event.get_extra("selected_model"): req.model = sel_model if config.provider_wake_prefix and not event.message_str.startswith( - config.provider_wake_prefix + config.provider_wake_prefix, ): return None @@ -1201,7 +1202,7 @@ async def build_main_agent( event.track_temporary_local_file(image_path) req.image_urls.append(image_path) req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment: path {image_path}]") + TextPart(text=f"[Image Attachment: path {image_path}]"), ) elif isinstance(comp, Record): audio_path = await comp.convert_to_file_path() @@ -1212,15 +1213,15 @@ async def build_main_agent( file_name = comp.name or os.path.basename(file_path) req.extra_user_content_parts.append( TextPart( - text=f"[File Attachment: name {file_name}, path {file_path}]" - ) + text=f"[File Attachment: name {file_name}, path {file_path}]", + ), ) # quoted message attachments reply_comps = [ comp for comp in event.message_obj.message if isinstance(comp, Reply) ] quoted_message_settings = _get_quoted_message_parser_settings( - config.provider_settings + config.provider_settings, ) fallback_quoted_image_count = 0 for comp in reply_comps: @@ -1250,8 +1251,8 @@ async def build_main_agent( text=( f"[File Attachment in quoted message: " f"name {file_name}, path {file_path}]" - ) - ) + ), + ), ) # Fallback quoted image extraction for reply-id-only payloads, or when @@ -1263,7 +1264,7 @@ async def build_main_agent( event, comp, settings=quoted_message_settings, - ) + ), ) remaining_limit = max( config.max_quoted_fallback_images @@ -1357,8 +1358,8 @@ async def build_main_agent( req.func_tool = ToolSet() req.func_tool.add_tool( plugin_context.get_llm_tool_manager().get_builtin_tool( - SendMessageToUserTool - ) + SendMessageToUserTool, + ), ) if provider.provider_config.get("max_context_tokens", 0) <= 0: @@ -1400,7 +1401,7 @@ async def build_main_agent( enforce_max_turns=config.max_context_length, tool_schema_mode=config.tool_schema_mode, fallback_providers=_get_fallback_chat_providers( - provider, plugin_context, config.provider_settings + provider, plugin_context, config.provider_settings, ), ) diff --git a/astrbot/core/backup/__init__.py b/astrbot/core/backup/__init__.py index 8e33ef970..ba96d9edf 100644 --- a/astrbot/core/backup/__init__.py +++ b/astrbot/core/backup/__init__.py @@ -16,11 +16,11 @@ from .exporter import AstrBotExporter from .importer import AstrBotImporter, ImportPreCheckResult __all__ = [ + "BACKUP_MANIFEST_VERSION", + "KB_METADATA_MODELS", + "MAIN_DB_MODELS", "AstrBotExporter", "AstrBotImporter", "ImportPreCheckResult", - "MAIN_DB_MODELS", - "KB_METADATA_MODELS", "get_backup_directories", - "BACKUP_MANIFEST_VERSION", ] diff --git a/astrbot/core/backup/constants.py b/astrbot/core/backup/constants.py index b832a1b72..6a26e0cdd 100644 --- a/astrbot/core/backup/constants.py +++ b/astrbot/core/backup/constants.py @@ -68,6 +68,7 @@ def get_backup_directories() -> dict[str, str]: Returns: dict: 键为备份文件中的目录名称,值为目录的绝对路径 + """ return { "plugins": get_astrbot_plugin_path(), # 插件本体 diff --git a/astrbot/core/backup/exporter.py b/astrbot/core/backup/exporter.py index a92237599..cbae0b8af 100644 --- a/astrbot/core/backup/exporter.py +++ b/astrbot/core/backup/exporter.py @@ -78,6 +78,7 @@ class AstrBotExporter: Returns: str: 生成的 ZIP 文件路径 + """ if output_dir is None: output_dir = get_astrbot_backups_path() @@ -98,7 +99,7 @@ class AstrBotExporter: await progress_callback("main_db", 0, 100, "正在导出主数据库...") main_data = await self._export_main_database() main_db_json = json.dumps( - main_data, ensure_ascii=False, indent=2, default=str + main_data, ensure_ascii=False, indent=2, default=str, ) zf.writestr("databases/main_db.json", main_db_json) self._add_checksum("databases/main_db.json", main_db_json) @@ -114,17 +115,17 @@ class AstrBotExporter: if self.kb_manager: if progress_callback: await progress_callback( - "kb_metadata", 0, 100, "正在导出知识库元数据..." + "kb_metadata", 0, 100, "正在导出知识库元数据...", ) kb_meta_data = await self._export_kb_metadata() kb_meta_json = json.dumps( - kb_meta_data, ensure_ascii=False, indent=2, default=str + kb_meta_data, ensure_ascii=False, indent=2, default=str, ) zf.writestr("databases/kb_metadata.json", kb_meta_json) self._add_checksum("databases/kb_metadata.json", kb_meta_json) if progress_callback: await progress_callback( - "kb_metadata", 100, 100, "知识库元数据导出完成" + "kb_metadata", 100, 100, "知识库元数据导出完成", ) # 导出每个知识库的文档数据 @@ -140,7 +141,7 @@ class AstrBotExporter: ) doc_data = await self._export_kb_documents(kb_helper) doc_json = json.dumps( - doc_data, ensure_ascii=False, indent=2, default=str + doc_data, ensure_ascii=False, indent=2, default=str, ) doc_path = f"databases/kb_{kb_id}/documents.json" zf.writestr(doc_path, doc_json) @@ -154,7 +155,7 @@ class AstrBotExporter: if progress_callback: await progress_callback( - "kb_documents", total_kbs, total_kbs, "知识库文档导出完成" + "kb_documents", total_kbs, total_kbs, "知识库文档导出完成", ) # 3. 导出配置文件 @@ -178,7 +179,7 @@ class AstrBotExporter: # 5. 导出插件和其他目录 if progress_callback: await progress_callback( - "directories", 0, 100, "正在导出插件和数据目录..." + "directories", 0, 100, "正在导出插件和数据目录...", ) dir_stats = await self._export_directories(zf) if progress_callback: @@ -216,7 +217,7 @@ class AstrBotExporter: self._model_to_dict(record) for record in records ] logger.debug( - f"导出表 {table_name}: {len(export_data[table_name])} 条记录" + f"导出表 {table_name}: {len(export_data[table_name])} 条记录", ) except Exception as e: logger.warning(f"导出表 {table_name} 失败: {e}") @@ -240,7 +241,7 @@ class AstrBotExporter: self._model_to_dict(record) for record in records ] logger.debug( - f"导出知识库表 {table_name}: {len(export_data[table_name])} 条记录" + f"导出知识库表 {table_name}: {len(export_data[table_name])} 条记录", ) except Exception as e: logger.warning(f"导出知识库表 {table_name} 失败: {e}") @@ -286,7 +287,7 @@ class AstrBotExporter: logger.warning(f"导出 FAISS 索引失败: {e}") async def _export_kb_media_files( - self, zf: zipfile.ZipFile, kb_helper: Any, kb_id: str + self, zf: zipfile.ZipFile, kb_helper: Any, kb_id: str, ) -> None: """导出知识库的多媒体文件""" try: @@ -305,12 +306,13 @@ class AstrBotExporter: logger.warning(f"导出知识库媒体文件失败: {e}") async def _export_directories( - self, zf: zipfile.ZipFile + self, zf: zipfile.ZipFile, ) -> dict[str, dict[str, int]]: """导出插件和其他数据目录 Returns: dict: 每个目录的统计信息 {dir_name: {"files": count, "size": bytes}} + """ stats: dict[str, dict[str, int]] = {} backup_directories = get_backup_directories() @@ -347,7 +349,7 @@ class AstrBotExporter: stats[dir_name] = {"files": file_count, "size": total_size} logger.debug( - f"导出目录 {dir_name}: {file_count} 个文件, {total_size} 字节" + f"导出目录 {dir_name}: {file_count} 个文件, {total_size} 字节", ) except Exception as e: logger.warning(f"导出目录 {dir_path} 失败: {e}") @@ -356,7 +358,7 @@ class AstrBotExporter: return stats async def _export_attachments( - self, zf: zipfile.ZipFile, attachments: list[dict] + self, zf: zipfile.ZipFile, attachments: list[dict], ) -> None: """导出附件文件""" for attachment in attachments: diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index b51c7d956..182c28a1f 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -46,6 +46,7 @@ def _get_major_version(version_str: str) -> str: Returns: 主版本字符串,如 "4.9", "4.10" + """ if not version_str: return "0.0" @@ -54,7 +55,7 @@ def _get_major_version(version_str: str) -> str: parts = [p for p in version.split(".") if p] # 过滤空字符串 if len(parts) >= 2: return f"{parts[0]}.{parts[1]}" - elif len(parts) == 1 and parts[0]: + if len(parts) == 1 and parts[0]: return f"{parts[0]}.0" return "0.0" @@ -245,6 +246,7 @@ class AstrBotImporter: Returns: ImportPreCheckResult: 预检查结果 + """ result = ImportPreCheckResult() result.current_version = VERSION @@ -306,6 +308,7 @@ class AstrBotImporter: Returns: dict: {status, can_import, message} + """ if not backup_version: return { @@ -361,6 +364,7 @@ class AstrBotImporter: Returns: ImportResult: 导入结果 + """ result = ImportResult() @@ -464,7 +468,7 @@ class AstrBotImporter: await progress_callback("attachments", 0, 100, "正在导入附件...") attachment_count = await self._import_attachments( - zf, main_data.get("attachments", []) + zf, main_data.get("attachments", []), ) result.imported_files["attachments"] = attachment_count @@ -474,7 +478,7 @@ class AstrBotImporter: # 6. 导入插件和其他目录 if progress_callback: await progress_callback( - "directories", 0, 100, "正在导入插件和数据目录..." + "directories", 0, 100, "正在导入插件和数据目录...", ) dir_stats = await self._import_directories(zf, manifest, result) @@ -515,16 +519,15 @@ class AstrBotImporter: async def _clear_main_db(self) -> None: """清空主数据库所有表""" - async with self.main_db.get_db() as session: - async with session.begin(): - for table_name, model_class in MAIN_DB_MODELS.items(): - try: - await session.execute(delete(model_class)) - logger.debug(f"已清空表 {table_name}") - except Exception as e: - raise DatabaseClearError( - f"清空表 {table_name} 失败: {e}" - ) from e + async with self.main_db.get_db() as session, session.begin(): + for table_name, model_class in MAIN_DB_MODELS.items(): + try: + await session.execute(delete(model_class)) + logger.debug(f"已清空表 {table_name}") + except Exception as e: + raise DatabaseClearError( + f"清空表 {table_name} 失败: {e}", + ) from e async def _clear_kb_data(self) -> None: """清空知识库数据""" @@ -532,14 +535,13 @@ class AstrBotImporter: return # 清空知识库元数据表 - async with self.kb_manager.kb_db.get_db() as session: - async with session.begin(): - for table_name, model_class in KB_METADATA_MODELS.items(): - try: - await session.execute(delete(model_class)) - logger.debug(f"已清空知识库表 {table_name}") - except Exception as e: - logger.warning(f"清空知识库表 {table_name} 失败: {e}") + async with self.kb_manager.kb_db.get_db() as session, session.begin(): + for table_name, model_class in KB_METADATA_MODELS.items(): + try: + await session.execute(delete(model_class)) + logger.debug(f"已清空知识库表 {table_name}") + except Exception as e: + logger.warning(f"清空知识库表 {table_name} 失败: {e}") # 删除知识库文件目录 for kb_id in list(self.kb_manager.kb_insts.keys()): @@ -554,38 +556,37 @@ class AstrBotImporter: self.kb_manager.kb_insts.clear() async def _import_main_database( - self, data: dict[str, list[dict]] + self, data: dict[str, list[dict]], ) -> dict[str, int]: """导入主数据库数据""" imported: dict[str, int] = {} - async with self.main_db.get_db() as session: - async with session.begin(): - for table_name, rows in data.items(): - model_class = MAIN_DB_MODELS.get(table_name) - if not model_class: - logger.warning(f"未知的表: {table_name}") - continue - normalized_rows = self._preprocess_main_table_rows(table_name, rows) + async with self.main_db.get_db() as session, session.begin(): + for table_name, rows in data.items(): + model_class = MAIN_DB_MODELS.get(table_name) + if not model_class: + logger.warning(f"未知的表: {table_name}") + continue + normalized_rows = self._preprocess_main_table_rows(table_name, rows) - count = 0 - for row in normalized_rows: - try: - # 转换 datetime 字符串为 datetime 对象 - row = self._convert_datetime_fields(row, model_class) - obj = model_class(**row) - session.add(obj) - count += 1 - except Exception as e: - logger.warning(f"导入记录到 {table_name} 失败: {e}") + count = 0 + for row in normalized_rows: + try: + # 转换 datetime 字符串为 datetime 对象 + row = self._convert_datetime_fields(row, model_class) + obj = model_class(**row) + session.add(obj) + count += 1 + except Exception as e: + logger.warning(f"导入记录到 {table_name} 失败: {e}") - imported[table_name] = count - logger.debug(f"导入表 {table_name}: {count} 条记录") + imported[table_name] = count + logger.debug(f"导入表 {table_name}: {count} 条记录") return imported def _preprocess_main_table_rows( - self, table_name: str, rows: list[dict[str, Any]] + self, table_name: str, rows: list[dict[str, Any]], ) -> list[dict[str, Any]]: if table_name == "platform_stats": normalized_rows = self._merge_platform_stats_rows(rows) @@ -600,7 +601,7 @@ class AstrBotImporter: return rows def _merge_platform_stats_rows( - self, rows: list[dict[str, Any]] + self, rows: list[dict[str, Any]], ) -> list[dict[str, Any]]: """Merge duplicate platform_stats rows by normalized timestamp/platform key. @@ -608,6 +609,7 @@ class AstrBotImporter: - Invalid/empty timestamps are kept as distinct rows to avoid accidental merging. - Non-string platform_id/platform_type are kept as distinct rows. - Invalid count warnings are rate-limited per function invocation. + """ merged: dict[tuple[str, str, str], dict[str, Any]] = {} result: list[dict[str, Any]] = [] @@ -707,24 +709,23 @@ class AstrBotImporter: return # 1. 导入知识库元数据 - async with self.kb_manager.kb_db.get_db() as session: - async with session.begin(): - for table_name, rows in kb_meta_data.items(): - model_class = KB_METADATA_MODELS.get(table_name) - if not model_class: - continue + async with self.kb_manager.kb_db.get_db() as session, session.begin(): + for table_name, rows in kb_meta_data.items(): + model_class = KB_METADATA_MODELS.get(table_name) + if not model_class: + continue - count = 0 - for row in rows: - try: - row = self._convert_datetime_fields(row, model_class) - obj = model_class(**row) - session.add(obj) - count += 1 - except Exception as e: - logger.warning(f"导入知识库记录到 {table_name} 失败: {e}") + count = 0 + for row in rows: + try: + row = self._convert_datetime_fields(row, model_class) + obj = model_class(**row) + session.add(obj) + count += 1 + except Exception as e: + logger.warning(f"导入知识库记录到 {table_name} 失败: {e}") - result.imported_tables[f"kb_{table_name}"] = count + result.imported_tables[f"kb_{table_name}"] = count # 2. 导入每个知识库的文档和文件 for kb_data in kb_meta_data.get("knowledge_bases", []): @@ -851,6 +852,7 @@ class AstrBotImporter: Returns: dict: 每个目录导入的文件数量 + """ dir_stats: dict[str, int] = {} diff --git a/astrbot/core/computer/booters/base.py b/astrbot/core/computer/booters/base.py index 4c74e5edd..2b7fb4dfc 100644 --- a/astrbot/core/computer/booters/base.py +++ b/astrbot/core/computer/booters/base.py @@ -38,12 +38,9 @@ class ComputerBooter: Should return a dict with `success` (bool) and `file_path` (str) keys. """ - ... async def download_file(self, remote_path: str, local_path: str) -> None: """Download file from the computer.""" - ... async def available(self) -> bool: """Check if the computer is available.""" - ... diff --git a/astrbot/core/computer/booters/bay_manager.py b/astrbot/core/computer/booters/bay_manager.py index 61ccc1b3a..4fecda157 100644 --- a/astrbot/core/computer/booters/bay_manager.py +++ b/astrbot/core/computer/booters/bay_manager.py @@ -60,7 +60,7 @@ class BayContainerManager: raise RuntimeError( "Failed to connect to Docker daemon. " "Ensure Docker is installed and running, or configure " - "an explicit Bay endpoint instead of auto-start mode." + "an explicit Bay endpoint instead of auto-start mode.", ) from exc # 1. Look for an existing managed container @@ -72,13 +72,12 @@ class BayContainerManager: logger.info("[BayManager] Reusing existing Bay container: %s", cid) self._container = await self._docker.containers.get(existing["Id"]) return f"http://127.0.0.1:{self._host_port}" - else: - # Container exists but stopped — restart it - logger.info("[BayManager] Restarting stopped Bay container") - container = await self._docker.containers.get(existing["Id"]) - await container.start() - self._container = container - return f"http://127.0.0.1:{self._host_port}" + # Container exists but stopped — restart it + logger.info("[BayManager] Restarting stopped Bay container") + container = await self._docker.containers.get(existing["Id"]) + await container.start() + self._container = container + return f"http://127.0.0.1:{self._host_port}" # 2. Pull image if needed await self._pull_image_if_needed() @@ -111,7 +110,7 @@ class BayContainerManager: }, } self._container = await self._docker.containers.create_or_replace( - BAY_CONTAINER_NAME, config + BAY_CONTAINER_NAME, config, ) await self._container.start() logger.info("[BayManager] Bay container started: %s", BAY_CONTAINER_NAME) @@ -129,7 +128,7 @@ class BayContainerManager: while loop.time() < deadline: try: async with session.get( - url, timeout=aiohttp.ClientTimeout(total=3) + url, timeout=aiohttp.ClientTimeout(total=3), ) as resp: if resp.status == 200: logger.info("[BayManager] Bay is healthy") @@ -141,7 +140,7 @@ class BayContainerManager: await asyncio.sleep(HEALTH_POLL_INTERVAL_S) raise TimeoutError( - f"Bay did not become healthy within {timeout}s (last error: {last_error})" + f"Bay did not become healthy within {timeout}s (last error: {last_error})", ) async def read_credentials(self) -> str: @@ -202,7 +201,7 @@ class BayContainerManager: return api_key except Exception as exc: logger.debug( - "[BayManager] Failed to read credentials from container: %s", exc + "[BayManager] Failed to read credentials from container: %s", exc, ) return "" diff --git a/astrbot/core/computer/booters/boxlite.py b/astrbot/core/computer/booters/boxlite.py index 70064fdd4..8eb4807cf 100644 --- a/astrbot/core/computer/booters/boxlite.py +++ b/astrbot/core/computer/booters/boxlite.py @@ -34,11 +34,10 @@ class MockShipyardSandboxClient: ) as response: if response.status == 200: return await response.json() - else: - error_text = await response.text() - raise Exception( - f"Failed to exec operation: {response.status} {error_text}" - ) + error_text = await response.text() + raise Exception( + f"Failed to exec operation: {response.status} {error_text}", + ) async def upload_file(self, path: str, remote_path: str) -> dict: """Upload a file to the sandbox""" @@ -54,7 +53,7 @@ class MockShipyardSandboxClient: data.add_field( "file", file_content, - filename=remote_path.split("/")[-1], + filename=remote_path.rsplit("/", maxsplit=1)[-1], content_type="application/octet-stream", ) data.add_field("file_path", remote_path) @@ -73,19 +72,18 @@ class MockShipyardSandboxClient: "message": "File uploaded successfully", "file_path": remote_path, } - else: - error_text = await response.text() - return { - "success": False, - "error": f"Server returned {response.status}: {error_text}", - "message": "File upload failed", - } + error_text = await response.text() + return { + "success": False, + "error": f"Server returned {response.status}: {error_text}", + "message": "File upload failed", + } except aiohttp.ClientError as e: logger.error(f"Failed to upload file: {e}") return { "success": False, - "error": f"Connection error: {str(e)}", + "error": f"Connection error: {e!s}", "message": "File upload failed", } except asyncio.TimeoutError: @@ -105,7 +103,7 @@ class MockShipyardSandboxClient: logger.error(f"Unexpected error uploading file: {e}") return { "success": False, - "error": f"Internal error: {str(e)}", + "error": f"Internal error: {e!s}", "message": "File upload failed", } @@ -115,7 +113,7 @@ class MockShipyardSandboxClient: while loop > 0: try: logger.info( - f"Checking health for sandbox {ship_id} on {self.sb_url}..." + f"Checking health for sandbox {ship_id} on {self.sb_url}...", ) url = f"{self.sb_url}/health" async with aiohttp.ClientSession() as session: @@ -131,7 +129,7 @@ class MockShipyardSandboxClient: class BoxliteBooter(ComputerBooter): async def boot(self, session_id: str) -> None: logger.info( - f"Booting(Boxlite) for session: {session_id}, this may take a while..." + f"Booting(Boxlite) for session: {session_id}, this may take a while...", ) random_port = random.randint(20000, 30000) self.box = boxlite.SimpleBox( @@ -142,13 +140,13 @@ class BoxliteBooter(ComputerBooter): { "host_port": random_port, "guest_port": 8123, - } + }, ], ) await self.box.start() logger.info(f"Boxlite booter started for session: {session_id}") self.mocked = MockShipyardSandboxClient( - sb_url=f"http://127.0.0.1:{random_port}" + sb_url=f"http://127.0.0.1:{random_port}", ) self._fs = ShipyardFileSystemComponent( client=self.mocked, # type: ignore diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index f11bc329f..be2ba56da 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -115,7 +115,7 @@ class LocalShellComponent(ShellComponent): # `command` is intentionally executed through the current shell so # local computer-use behavior matches existing tool semantics. # Safety relies on `_is_safe_command()` and the allowed-root checks. - proc = subprocess.Popen( # noqa: S602 # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit + proc = subprocess.Popen( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit command, shell=shell, cwd=working_dir, @@ -127,7 +127,7 @@ class LocalShellComponent(ShellComponent): # `command` is intentionally executed through the current shell so # local computer-use behavior matches existing tool semantics. # Safety relies on `_is_safe_command()` and the allowed-root checks. - result = subprocess.run( # noqa: S602 # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit + result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit command, shell=shell, cwd=working_dir, @@ -167,14 +167,14 @@ class LocalPythonComponent(PythonComponent): "data": { "output": {"text": stdout, "images": []}, "error": stderr, - } + }, } except subprocess.TimeoutExpired: return { "data": { "output": {"text": "", "images": []}, "error": "Execution timed out.", - } + }, } return await asyncio.to_thread(_run) @@ -183,7 +183,7 @@ class LocalPythonComponent(PythonComponent): @dataclass class LocalFileSystemComponent(FileSystemComponent): async def create_file( - self, path: str, content: str = "", mode: int = 0o644 + self, path: str, content: str = "", mode: int = 0o644, ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -209,7 +209,7 @@ class LocalFileSystemComponent(FileSystemComponent): return await asyncio.to_thread(_run) async def write_file( - self, path: str, content: str, mode: str = "w", encoding: str = "utf-8" + self, path: str, content: str, mode: str = "w", encoding: str = "utf-8", ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -232,7 +232,7 @@ class LocalFileSystemComponent(FileSystemComponent): return await asyncio.to_thread(_run) async def list_dir( - self, path: str = ".", show_hidden: bool = False + self, path: str = ".", show_hidden: bool = False, ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -270,12 +270,12 @@ class LocalBooter(ComputerBooter): async def upload_file(self, path: str, file_name: str) -> dict: raise NotImplementedError( - "LocalBooter does not support upload_file operation. Use shell instead." + "LocalBooter does not support upload_file operation. Use shell instead.", ) async def download_file(self, remote_path: str, local_path: str) -> None: raise NotImplementedError( - "LocalBooter does not support download_file operation. Use shell instead." + "LocalBooter does not support download_file operation. Use shell instead.", ) async def available(self) -> bool: diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 6379d1e48..f403f8575 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -15,7 +15,7 @@ class ShipyardBooter(ComputerBooter): session_num: int = 10, ) -> None: self._sandbox_client = ShipyardClient( - endpoint_url=endpoint_url, access_token=access_token + endpoint_url=endpoint_url, access_token=access_token, ) self._ttl = ttl self._session_num = session_num diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index 6304696ad..c7af0f55f 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -328,12 +328,12 @@ class ShipyardNeoBooter(ComputerBooter): if self._bay_manager is not None: raise ValueError( "Bay container started but credentials could not be read. " - "Ensure Bay generated credentials.json, or set access_token manually." + "Ensure Bay generated credentials.json, or set access_token manually.", ) raise ValueError( "Shipyard Neo sandbox configuration is incomplete. " "Set endpoint (default http://127.0.0.1:8114) and access token, " - "or ensure Bay's credentials.json is accessible for auto-discovery." + "or ensure Bay's credentials.json is accessible for auto-discovery.", ) from shipyard_neo import BayClient @@ -428,7 +428,7 @@ class ShipyardNeoBooter(ComputerBooter): if self._client is not None: sandbox_id = getattr(self._sandbox, "id", "unknown") logger.info( - "[Computer] Shutting down Shipyard Neo sandbox: id=%s", sandbox_id + "[Computer] Shutting down Shipyard Neo sandbox: id=%s", sandbox_id, ) await self._client.__aexit__(None, None, None) self._client = None @@ -487,7 +487,7 @@ class ShipyardNeoBooter(ComputerBooter): if local_dir: os.makedirs(local_dir, exist_ok=True) with open(local_path, "wb") as f: - f.write(cast(bytes, content)) + f.write(cast("bytes", content)) logger.info( "[Computer] File downloaded from Neo sandbox: %s -> %s", remote_path, diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index 715f93867..bd06f346e 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -41,6 +41,7 @@ def _discover_bay_credentials(endpoint: str) -> str: Returns: API key string, or empty string if not found. + """ candidates: list[Path] = [] @@ -405,7 +406,7 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None: raise RuntimeError("Failed to upload skills bundle to sandbox.") else: logger.info( - "No local skills found. Keeping sandbox built-ins and refreshing metadata." + "No local skills found. Keeping sandbox built-ins and refreshing metadata.", ) await booter.shell.exec(f"rm -f {SANDBOX_SKILLS_ROOT}/skills.zip") @@ -436,7 +437,7 @@ async def get_booter( runtime = config.get("provider_settings", {}).get("computer_use_runtime", "local") if runtime == "local": return get_local_booter() - elif runtime == "none": + if runtime == "none": raise RuntimeError("Sandbox runtime is disabled by configuration.") sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {}) @@ -450,7 +451,7 @@ async def get_booter( if session_id not in session_booter: uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex logger.info( - f"[Computer] Initializing booter: type={booter_type}, session={session_id}" + f"[Computer] Initializing booter: type={booter_type}, session={session_id}", ) if booter_type == "shipyard": from .booters.shipyard import ShipyardBooter @@ -461,7 +462,7 @@ async def get_booter( max_sessions = sandbox_cfg.get("shipyard_max_sessions", 10) client = ShipyardBooter( - endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions + endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions, ) elif booter_type == "shipyard_neo": from .booters.shipyard_neo import ShipyardNeoBooter @@ -476,7 +477,7 @@ async def get_booter( token = _discover_bay_credentials(ep) logger.info( - f"[Computer] Shipyard Neo config: endpoint={ep}, profile={profile}, ttl={ttl}" + f"[Computer] Shipyard Neo config: endpoint={ep}, profile={profile}, ttl={ttl}", ) client = ShipyardNeoBooter( endpoint_url=ep, @@ -494,7 +495,7 @@ async def get_booter( try: await client.boot(uuid_str) logger.info( - f"[Computer] Sandbox booted successfully: type={booter_type}, session={session_id}" + f"[Computer] Sandbox booted successfully: type={booter_type}, session={session_id}", ) await _sync_skills_to_sandbox(client) except Exception as e: @@ -508,7 +509,7 @@ async def get_booter( async def sync_skills_to_active_sandboxes() -> None: """Best-effort skills synchronization for all active sandbox sessions.""" logger.info( - "[Computer] Syncing skills to %d active sandbox(es)", len(session_booter) + "[Computer] Syncing skills to %d active sandbox(es)", len(session_booter), ) for session_id, booter in list(session_booter.items()): try: diff --git a/astrbot/core/computer/olayer/__init__.py b/astrbot/core/computer/olayer/__init__.py index e2348671e..261f9de9c 100644 --- a/astrbot/core/computer/olayer/__init__.py +++ b/astrbot/core/computer/olayer/__init__.py @@ -4,8 +4,8 @@ from .python import PythonComponent from .shell import ShellComponent __all__ = [ + "BrowserComponent", + "FileSystemComponent", "PythonComponent", "ShellComponent", - "FileSystemComponent", - "BrowserComponent", ] diff --git a/astrbot/core/computer/olayer/browser.py b/astrbot/core/computer/olayer/browser.py index aa69f4501..942bd9aa3 100644 --- a/astrbot/core/computer/olayer/browser.py +++ b/astrbot/core/computer/olayer/browser.py @@ -1,5 +1,4 @@ -""" -Browser automation component +"""Browser automation component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/olayer/filesystem.py b/astrbot/core/computer/olayer/filesystem.py index 21f36d111..c50173803 100644 --- a/astrbot/core/computer/olayer/filesystem.py +++ b/astrbot/core/computer/olayer/filesystem.py @@ -1,5 +1,4 @@ -""" -File system component +"""File system component """ from typing import Any, Protocol @@ -7,7 +6,7 @@ from typing import Any, Protocol class FileSystemComponent(Protocol): async def create_file( - self, path: str, content: str = "", mode: int = 0o644 + self, path: str, content: str = "", mode: int = 0o644, ) -> dict[str, Any]: """Create a file with the specified content""" ... @@ -17,7 +16,7 @@ class FileSystemComponent(Protocol): ... async def write_file( - self, path: str, content: str, mode: str = "w", encoding: str = "utf-8" + self, path: str, content: str, mode: str = "w", encoding: str = "utf-8", ) -> dict[str, Any]: """Write content to file""" ... @@ -27,7 +26,7 @@ class FileSystemComponent(Protocol): ... async def list_dir( - self, path: str = ".", show_hidden: bool = False + self, path: str = ".", show_hidden: bool = False, ) -> dict[str, Any]: """List directory contents""" ... diff --git a/astrbot/core/computer/olayer/python.py b/astrbot/core/computer/olayer/python.py index 625504146..fbcf397c4 100644 --- a/astrbot/core/computer/olayer/python.py +++ b/astrbot/core/computer/olayer/python.py @@ -1,5 +1,4 @@ -""" -Python/IPython component +"""Python/IPython component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/olayer/shell.py b/astrbot/core/computer/olayer/shell.py index df2263b65..82eb9845c 100644 --- a/astrbot/core/computer/olayer/shell.py +++ b/astrbot/core/computer/olayer/shell.py @@ -1,5 +1,4 @@ -""" -Shell component +"""Shell component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/tools/__init__.py b/astrbot/core/computer/tools/__init__.py index 598abbb6e..9563f146e 100644 --- a/astrbot/core/computer/tools/__init__.py +++ b/astrbot/core/computer/tools/__init__.py @@ -17,23 +17,23 @@ from .python import LocalPythonTool, PythonTool from .shell import ExecuteShellTool __all__ = [ - "BrowserExecTool", - "BrowserBatchExecTool", - "RunBrowserSkillTool", - "GetExecutionHistoryTool", "AnnotateExecutionTool", - "CreateSkillPayloadTool", - "GetSkillPayloadTool", + "BrowserBatchExecTool", + "BrowserExecTool", "CreateSkillCandidateTool", - "ListSkillCandidatesTool", + "CreateSkillPayloadTool", "EvaluateSkillCandidateTool", - "PromoteSkillCandidateTool", - "ListSkillReleasesTool", - "RollbackSkillReleaseTool", - "SyncSkillReleaseTool", - "FileUploadTool", - "PythonTool", - "LocalPythonTool", "ExecuteShellTool", "FileDownloadTool", + "FileUploadTool", + "GetExecutionHistoryTool", + "GetSkillPayloadTool", + "ListSkillCandidatesTool", + "ListSkillReleasesTool", + "LocalPythonTool", + "PromoteSkillCandidateTool", + "PythonTool", + "RollbackSkillReleaseTool", + "RunBrowserSkillTool", + "SyncSkillReleaseTool", ] diff --git a/astrbot/core/computer/tools/browser.py b/astrbot/core/computer/tools/browser.py index cd8484acb..cb1eb9115 100644 --- a/astrbot/core/computer/tools/browser.py +++ b/astrbot/core/computer/tools/browser.py @@ -24,7 +24,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A if browser is None: raise RuntimeError( "Current sandbox booter does not support browser capability. " - "Please switch to shipyard_neo." + "Please switch to shipyard_neo.", ) return browser @@ -56,7 +56,7 @@ class BrowserExecTool(FunctionTool): }, }, "required": ["cmd"], - } + }, ) async def call( @@ -83,7 +83,7 @@ class BrowserExecTool(FunctionTool): ) return _to_json(result) except Exception as e: - return f"Error executing browser command: {str(e)}" + return f"Error executing browser command: {e!s}" @dataclass @@ -118,7 +118,7 @@ class BrowserBatchExecTool(FunctionTool): }, }, "required": ["commands"], - } + }, ) async def call( @@ -147,7 +147,7 @@ class BrowserBatchExecTool(FunctionTool): ) return _to_json(result) except Exception as e: - return f"Error executing browser batch command: {str(e)}" + return f"Error executing browser batch command: {e!s}" @dataclass @@ -166,7 +166,7 @@ class RunBrowserSkillTool(FunctionTool): "tags": {"type": "string"}, }, "required": ["skill_key"], - } + }, ) async def call( @@ -193,4 +193,4 @@ class RunBrowserSkillTool(FunctionTool): ) return _to_json(result) except Exception as e: - return f"Error running browser skill: {str(e)}" + return f"Error running browser skill: {e!s}" diff --git a/astrbot/core/computer/tools/fs.py b/astrbot/core/computer/tools/fs.py index f2a698f76..50e16ff77 100644 --- a/astrbot/core/computer/tools/fs.py +++ b/astrbot/core/computer/tools/fs.py @@ -100,7 +100,7 @@ class FileUploadTool(FunctionTool): # }, }, "required": ["local_path"], - } + }, ) async def call( @@ -139,7 +139,7 @@ class FileUploadTool(FunctionTool): return f"File uploaded successfully to {file_path}" except Exception as e: logger.error(f"Error uploading file {local_path}: {e}") - return f"Error uploading file: {str(e)}" + return f"Error uploading file: {e!s}" @dataclass @@ -164,7 +164,7 @@ class FileDownloadTool(FunctionTool): }, }, "required": ["remote_path"], - } + }, ) async def call( @@ -183,7 +183,7 @@ class FileDownloadTool(FunctionTool): name = os.path.basename(remote_path) local_path = os.path.join( - get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" + get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}", ) # Download file from sandbox @@ -194,7 +194,7 @@ class FileDownloadTool(FunctionTool): try: name = os.path.basename(local_path) await context.context.event.send( - MessageChain(chain=[File(name=name, file=local_path)]) + MessageChain(chain=[File(name=name, file=local_path)]), ) except Exception as e: logger.error(f"Error sending file message: {e}") @@ -210,4 +210,4 @@ class FileDownloadTool(FunctionTool): return f"File downloaded successfully to {local_path}" except Exception as e: logger.error(f"Error downloading file {remote_path}: {e}") - return f"Error downloading file: {str(e)}" + return f"Error downloading file: {e!s}" diff --git a/astrbot/core/computer/tools/neo_skills.py b/astrbot/core/computer/tools/neo_skills.py index 327f14472..6519c285f 100644 --- a/astrbot/core/computer/tools/neo_skills.py +++ b/astrbot/core/computer/tools/neo_skills.py @@ -39,7 +39,7 @@ async def _get_neo_context( if client is None or sandbox is None: raise RuntimeError( "Current sandbox booter does not support Neo skill lifecycle APIs. " - "Please switch to shipyard_neo." + "Please switch to shipyard_neo.", ) return client, sandbox @@ -61,7 +61,7 @@ class NeoSkillToolBase(FunctionTool): result = await neo_call(client, sandbox) return _to_json_text(result) except Exception as e: - return f"{self.error_prefix} {error_action}: {str(e)}" + return f"{self.error_prefix} {error_action}: {e!s}" @dataclass @@ -81,7 +81,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase): "has_description": {"type": "boolean", "default": False}, }, "required": [], - } + }, ) async def call( @@ -124,7 +124,7 @@ class AnnotateExecutionTool(NeoSkillToolBase): "notes": {"type": "string"}, }, "required": ["execution_id"], - } + }, ) async def call( @@ -175,7 +175,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase): }, }, "required": ["payload"], - } + }, ) async def call( @@ -205,7 +205,7 @@ class GetSkillPayloadTool(NeoSkillToolBase): "payload_ref": {"type": "string"}, }, "required": ["payload_ref"], - } + }, ) async def call( @@ -250,7 +250,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase): }, }, "required": ["skill_key", "source_execution_ids"], - } + }, ) async def call( @@ -287,7 +287,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase): "offset": {"type": "integer", "default": 0}, }, "required": [], - } + }, ) async def call( @@ -325,7 +325,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase): "report": {"type": "string"}, }, "required": ["candidate_id", "passed"], - } + }, ) async def call( @@ -377,7 +377,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): }, }, "required": ["candidate_id"], - } + }, ) async def call( @@ -414,10 +414,10 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): "release": result.get("release"), "sync": result.get("sync"), "rollback": result.get("rollback"), - } + }, ) except Exception as e: - return f"Error promoting skill candidate: {str(e)}" + return f"Error promoting skill candidate: {e!s}" @dataclass @@ -435,7 +435,7 @@ class ListSkillReleasesTool(NeoSkillToolBase): "offset": {"type": "integer", "default": 0}, }, "required": [], - } + }, ) async def call( @@ -471,7 +471,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase): "release_id": {"type": "string"}, }, "required": ["release_id"], - } + }, ) async def call( @@ -501,7 +501,7 @@ class SyncSkillReleaseTool(NeoSkillToolBase): "require_stable": {"type": "boolean", "default": True}, }, "required": [], - } + }, ) async def call( diff --git a/astrbot/core/computer/tools/permissions.py b/astrbot/core/computer/tools/permissions.py index 489f485f9..e8287e423 100644 --- a/astrbot/core/computer/tools/permissions.py +++ b/astrbot/core/computer/tools/permissions.py @@ -3,10 +3,10 @@ from astrbot.core.astr_agent_context import AstrAgentContext def check_admin_permission( - context: ContextWrapper[AstrAgentContext], operation_name: str + context: ContextWrapper[AstrAgentContext], operation_name: str, ) -> str | None: cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin + umo=context.context.event.unified_msg_origin, ) provider_settings = cfg.get("provider_settings", {}) require_admin = provider_settings.get("computer_use_require_admin", True) diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index bf9aaa14e..2d92034d0 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -46,8 +46,8 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult for img in images: resp.content.append( mcp.types.ImageContent( - type="image", data=img["image/png"], mimeType="image/png" - ) + type="image", data=img["image/png"], mimeType="image/png", + ), ) if event.get_platform_name() == "webchat": @@ -68,7 +68,7 @@ class PythonTool(FunctionTool): parameters: dict = field(default_factory=lambda: param_schema) async def call( - self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False + self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Python execution"): return permission_error @@ -80,7 +80,7 @@ class PythonTool(FunctionTool): result = await sb.python.exec(code, silent=silent) return await handle_result(result, context.context.event) except Exception as e: - return f"Error executing code: {str(e)}" + return f"Error executing code: {e!s}" @dataclass @@ -94,7 +94,7 @@ class LocalPythonTool(FunctionTool): parameters: dict = field(default_factory=lambda: param_schema) async def call( - self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False + self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Python execution"): return permission_error @@ -103,4 +103,4 @@ class LocalPythonTool(FunctionTool): result = await sb.python.exec(code, silent=silent) return await handle_result(result, context.context.event) except Exception as e: - return f"Error executing code: {str(e)}" + return f"Error executing code: {e!s}" diff --git a/astrbot/core/computer/tools/shell.py b/astrbot/core/computer/tools/shell.py index b5009d30f..657110902 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/computer/tools/shell.py @@ -35,7 +35,7 @@ class ExecuteShellTool(FunctionTool): }, }, "required": ["command"], - } + }, ) is_local: bool = False @@ -61,4 +61,4 @@ class ExecuteShellTool(FunctionTool): result = await sb.shell.exec(command, background=background, env=env) return json.dumps(result) except Exception as e: - return f"Error executing command: {str(e)}" + return f"Error executing command: {e!s}" diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 77c298cac..42e16c9c6 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -53,8 +53,7 @@ class AstrBotConfig(dict): with open(config_path, encoding="utf-8-sig") as f: conf_str = f.read() # Handle UTF-8 BOM if present - if conf_str.startswith("\ufeff"): - conf_str = conf_str[1:] + conf_str = conf_str.removeprefix("\ufeff") conf = json.loads(conf_str) # 检查配置完整性,并插入 diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py index cb6b6429b..429a1fdf9 100644 --- a/astrbot/core/config/i18n_utils.py +++ b/astrbot/core/config/i18n_utils.py @@ -1,5 +1,4 @@ -""" -配置元数据国际化工具 +"""配置元数据国际化工具 提供配置元数据的国际化键转换功能 """ @@ -12,8 +11,7 @@ class ConfigMetadataI18n: @staticmethod def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str: - """ - 生成国际化键 + """生成国际化键 Args: group: 配置组,如 'ai_group', 'platform_group' @@ -23,27 +21,27 @@ class ConfigMetadataI18n: Returns: 国际化键,格式如: 'ai_group.agent_runner.enable.description' + """ if field: return f"{group}.{section}.{field}.{attr}" - else: - return f"{group}.{section}.{attr}" + return f"{group}.{section}.{attr}" @staticmethod def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]: - """ - 将配置元数据转换为使用国际化键 + """将配置元数据转换为使用国际化键 Args: metadata: 原始配置元数据字典 Returns: 使用国际化键的配置元数据字典 + """ result = {} def convert_items( - group: str, section: str, items: dict[str, Any], prefix: str = "" + group: str, section: str, items: dict[str, Any], prefix: str = "", ) -> dict[str, Any]: items_result: dict[str, Any] = {} @@ -74,11 +72,11 @@ class ConfigMetadataI18n: if "items" in field_data and isinstance(field_data["items"], dict): field_result["items"] = convert_items( - group, section, field_data["items"], field_path + group, section, field_data["items"], field_path, ) if "template_schema" in field_data and isinstance( - field_data["template_schema"], dict + field_data["template_schema"], dict, ): field_result["template_schema"] = convert_items( group, @@ -110,7 +108,7 @@ class ConfigMetadataI18n: if "items" in section_data and isinstance(section_data["items"], dict): section_result["items"] = convert_items( - group_key, section_key, section_data["items"] + group_key, section_key, section_data["items"], ) group_result["metadata"][section_key] = section_result diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index 2c282867f..25c7af57e 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -110,7 +110,7 @@ class ConversationManager: return conv.conversation_id async def switch_conversation( - self, unified_msg_origin: str, conversation_id: str + self, unified_msg_origin: str, conversation_id: str, ) -> None: """切换会话的对话 @@ -344,6 +344,7 @@ class ConversationManager: Raises: Exception: If the conversation with the given ID is not found + """ conv = await self.db.get_conversation_by_id(cid=cid) if not conv: diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index fe6b1c351..1c1369b9a 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -106,7 +106,7 @@ class AstrBotCoreLifecycle: logger.info("AstrBot v" + VERSION) if os.environ.get("TESTING", ""): LogManager.configure_logger( - logger, self.astrbot_config, override_level="DEBUG" + logger, self.astrbot_config, override_level="DEBUG", ) LogManager.configure_trace_logger(self.astrbot_config) else: @@ -258,7 +258,7 @@ class AstrBotCoreLifecycle: for task in self.star_context._register_tasks: extra_tasks.append(asyncio.create_task(task, name=task.__name__)) # type: ignore - tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])] + tasks_ = [event_bus_task, *(extra_tasks or [])] if cron_task: tasks_.append(cron_task) if temp_dir_cleaner_task: diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index c86fc160f..477a63ff1 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -178,8 +178,8 @@ class CronJobManager: ) asyncio.create_task( self.db.update_cron_job( - job.job_id, next_run_time=self._get_next_run_time(job.job_id) - ) + job.job_id, next_run_time=self._get_next_run_time(job.job_id), + ), ) except Exception as e: logger.error(f"Failed to schedule cron job {job.job_id}: {e!s}") @@ -194,7 +194,7 @@ class CronJobManager: return start_time = datetime.now(timezone.utc) await self.db.update_cron_job( - job_id, status="running", last_run_at=start_time, last_error=None + job_id, status="running", last_run_at=start_time, last_error=None, ) status = "completed" last_error = None @@ -308,7 +308,7 @@ class CronJobManager: cron_event.role = "admin" tool_call_timeout = cfg.get("provider_settings", {}).get( - "tool_call_timeout", 120 + "tool_call_timeout", 120, ) config = MainAgentBuildConfig( tool_call_timeout=tool_call_timeout, @@ -332,7 +332,7 @@ class CronJobManager: ) cron_job_str = json.dumps(extras.get("cron_job", {}), ensure_ascii=False) req.system_prompt += PROACTIVE_AGENT_CRON_WOKE_SYSTEM_PROMPT.format( - cron_job=cron_job_str + cron_job=cron_job_str, ) req.prompt = ( "You are now responding to a scheduled task. " @@ -343,11 +343,11 @@ class CronJobManager: if not req.func_tool: req.func_tool = ToolSet() req.func_tool.add_tool( - self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool) + self.ctx.get_llm_tool_manager().get_builtin_tool(SendMessageToUserTool), ) result = await build_main_agent( - event=cron_event, plugin_context=self.ctx, config=config, req=req + event=cron_event, plugin_context=self.ctx, config=config, req=req, ) if not result: logger.error("Failed to build main agent for cron job.") diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 087aa625b..6eae9462d 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -345,6 +345,7 @@ class BaseDatabase(abc.ABC): custom_error_message: Optional persona-level fallback error message folder_id: Optional folder ID to place the persona in (None means root) sort_order: Sort order within the folder (default 0) + """ ... @@ -398,7 +399,7 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_persona_folders( - self, parent_id: str | None = None + self, parent_id: str | None = None, ) -> list[PersonaFolder]: """Get all persona folders, optionally filtered by parent_id.""" ... @@ -427,14 +428,14 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """Move a persona to a folder (or root if folder_id is None).""" ... @abc.abstractmethod async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """Get all personas in a specific folder.""" ... @@ -451,6 +452,7 @@ class BaseDatabase(abc.ABC): - id: The persona_id or folder_id - type: Either "persona" or "folder" - sort_order: The new sort_order value + """ ... @@ -666,14 +668,14 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_platform_session_by_id( - self, session_id: str + self, session_id: str, ) -> PlatformSession | None: """Get a Platform session by its ID.""" ... @abc.abstractmethod async def get_platform_sessions_by_ids( - self, session_ids: list[str] + self, session_ids: list[str], ) -> list[PlatformSession]: """Get platform sessions by IDs.""" ... @@ -705,6 +707,7 @@ class BaseDatabase(abc.ABC): Returns: tuple[list[dict], int]: (sessions_with_project_info, total_count) + """ ... @@ -794,7 +797,7 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_project_by_session( - self, session_id: str, creator: str + self, session_id: str, creator: str, ) -> ChatUIProject | None: """Get the project that a session belongs to.""" ... diff --git a/astrbot/core/db/migration/migra_token_usage.py b/astrbot/core/db/migration/migra_token_usage.py index 76bf8ce01..920171ff9 100644 --- a/astrbot/core/db/migration/migra_token_usage.py +++ b/astrbot/core/db/migration/migra_token_usage.py @@ -19,7 +19,7 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: """ # 检查是否已经完成迁移 migration_done = await db_helper.get_preference( - "global", "global", "migration_done_token_usage_1" + "global", "global", "migration_done_token_usage_1", ) if migration_done: return @@ -38,15 +38,15 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: if "token_usage" in column_names: logger.info("token_usage 列已存在,跳过迁移") await sp.put_async( - "global", "global", "migration_done_token_usage_1", True + "global", "global", "migration_done_token_usage_1", True, ) return # 添加 token_usage 列 await session.execute( text( - "ALTER TABLE conversations ADD COLUMN token_usage INTEGER NOT NULL DEFAULT 0" - ) + "ALTER TABLE conversations ADD COLUMN token_usage INTEGER NOT NULL DEFAULT 0", + ), ) await session.commit() diff --git a/astrbot/core/db/migration/migra_webchat_session.py b/astrbot/core/db/migration/migra_webchat_session.py index 46025fc64..ddfd20e5d 100644 --- a/astrbot/core/db/migration/migra_webchat_session.py +++ b/astrbot/core/db/migration/migra_webchat_session.py @@ -25,7 +25,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: """ # 检查是否已经完成迁移 migration_done = await db_helper.get_preference( - "global", "global", "migration_done_webchat_session_1" + "global", "global", "migration_done_webchat_session_1", ) if migration_done: return @@ -53,7 +53,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: if not webchat_users: logger.info("没有找到需要迁移的 WebChat 数据") await sp.put_async( - "global", "global", "migration_done_webchat_session_1", True + "global", "global", "migration_done_webchat_session_1", True, ) return @@ -71,7 +71,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: for user_id, _, _, _ in webchat_users ] conv_query = select( - col(ConversationV2.user_id), col(ConversationV2.title) + col(ConversationV2.user_id), col(ConversationV2.title), ).where(col(ConversationV2.user_id).in_(user_ids_to_query)) conv_result = await session.execute(conv_query) # 创建 user_id -> title 的映射字典 @@ -89,7 +89,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: session_id = user_id # sender_name 通常是 username,但可能为 None - creator = sender_name if sender_name else "guest" + creator = sender_name or "guest" # 检查是否已经存在该会话 if session_id in existing_session_ids: diff --git a/astrbot/core/db/migration/sqlite_v3.py b/astrbot/core/db/migration/sqlite_v3.py index b326ebb44..cd48795d3 100644 --- a/astrbot/core/db/migration/sqlite_v3.py +++ b/astrbot/core/db/migration/sqlite_v3.py @@ -227,7 +227,7 @@ class SQLiteDatabase: return Stats(platform) def get_conversation_by_user_id( - self, user_id: str, cid: str + self, user_id: str, cid: str, ) -> Conversation | None: try: c = self.conn.cursor() @@ -306,7 +306,7 @@ class SQLiteDatabase: ) def update_conversation_persona_id( - self, user_id: str, cid: str, persona_id: str + self, user_id: str, cid: str, persona_id: str, ) -> None: self._exec_sql( """ diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index cabc3432c..126f3ddb5 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -438,7 +438,7 @@ class CommandConflict(TimestampMixin, SQLModel, table=True): __tablename__ = "command_conflicts" # type: ignore id: int | None = Field( - default=None, primary_key=True, sa_column_kwargs={"autoincrement": True} + default=None, primary_key=True, sa_column_kwargs={"autoincrement": True}, ) conflict_key: str = Field(nullable=False, max_length=255) handler_full_name: str = Field(nullable=False, max_length=512) diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index fd6668c0c..b14a4c5be 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -74,12 +74,12 @@ class SQLiteDatabase(BaseDatabase): if "folder_id" not in columns: await conn.execute( text( - "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL" - ) + "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL", + ), ) if "sort_order" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0") + text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0"), ) async def _ensure_persona_skills_column(self, conn) -> None: @@ -101,7 +101,7 @@ class SQLiteDatabase(BaseDatabase): if "custom_error_message" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT") + text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT"), ) # ==== @@ -340,7 +340,7 @@ class SQLiteDatabase(BaseDatabase): return new_conversation async def update_conversation( - self, cid, title=None, persona_id=None, content=None, token_usage=None + self, cid, title=None, persona_id=None, content=None, token_usage=None, ): async with self.get_db() as session: session: AsyncSession @@ -379,7 +379,7 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(ConversationV2).where( - col(ConversationV2.user_id) == user_id + col(ConversationV2.user_id) == user_id, ), ) @@ -557,13 +557,13 @@ class SQLiteDatabase(BaseDatabase): return result.scalars().all() async def get_platform_message_history_by_id( - self, message_id: int + self, message_id: int, ) -> PlatformMessageHistory | None: """Get a platform message history record by its ID.""" async with self.get_db() as session: session: AsyncSession query = select(PlatformMessageHistory).where( - PlatformMessageHistory.id == message_id + PlatformMessageHistory.id == message_id, ) result = await session.execute(query) return result.scalar_one_or_none() @@ -596,7 +596,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) + col(Attachment.attachment_id).in_(attachment_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -610,9 +610,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = delete(Attachment).where( - col(Attachment.attachment_id) == attachment_id + col(Attachment.attachment_id) == attachment_id, ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount > 0 async def delete_attachments(self, attachment_ids: list[str]) -> int: @@ -626,9 +626,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = delete(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) + col(Attachment.attachment_id).in_(attachment_ids), ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount async def create_api_key( @@ -662,7 +662,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(ApiKey).order_by(desc(ApiKey.created_at)) + select(ApiKey).order_by(desc(ApiKey.created_at)), ) return list(result.scalars().all()) @@ -671,7 +671,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(ApiKey).where(ApiKey.key_id == key_id) + select(ApiKey).where(ApiKey.key_id == key_id), ) return result.scalar_one_or_none() @@ -709,7 +709,7 @@ class SQLiteDatabase(BaseDatabase): .where(col(ApiKey.key_id) == key_id) .values(revoked_at=datetime.now(timezone.utc)) ) - result = T.cast(CursorResult, await session.execute(query)) + result = T.cast("CursorResult", await session.execute(query)) return result.rowcount > 0 async def delete_api_key(self, key_id: str) -> bool: @@ -718,9 +718,9 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): result = T.cast( - CursorResult, + "CursorResult", await session.execute( - delete(ApiKey).where(col(ApiKey.key_id) == key_id) + delete(ApiKey).where(col(ApiKey.key_id) == key_id), ), ) return result.rowcount > 0 @@ -846,13 +846,14 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() async def get_persona_folders( - self, parent_id: str | None = None + self, parent_id: str | None = None, ) -> list[PersonaFolder]: """Get all persona folders, optionally filtered by parent_id. Args: parent_id: If None, returns root folders only. If specified, returns children of that folder. + """ async with self.get_db() as session: session: AsyncSession @@ -877,7 +878,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(PersonaFolder).order_by( - col(PersonaFolder.sort_order), col(PersonaFolder.name) + col(PersonaFolder.sort_order), col(PersonaFolder.name), ) result = await session.execute(query) return list(result.scalars().all()) @@ -895,7 +896,7 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): query = update(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id + col(PersonaFolder.folder_id) == folder_id, ) values: dict[str, T.Any] = {} if name is not None: @@ -925,17 +926,17 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.folder_id) == folder_id) - .values(folder_id=None) + .values(folder_id=None), ) # Delete the folder await session.execute( delete(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id + col(PersonaFolder.folder_id) == folder_id, ), ) async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """Move a persona to a folder (or root if folder_id is None).""" async with self.get_db() as session: @@ -944,17 +945,18 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.persona_id) == persona_id) - .values(folder_id=folder_id) + .values(folder_id=folder_id), ) return await self.get_persona_by_id(persona_id) async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """Get all personas in a specific folder. Args: folder_id: If None, returns personas in root directory. + """ async with self.get_db() as session: session: AsyncSession @@ -984,6 +986,7 @@ class SQLiteDatabase(BaseDatabase): - id: The persona_id or folder_id - type: Either "persona" or "folder" - sort_order: The new sort_order value + """ if not items: return @@ -1003,13 +1006,13 @@ class SQLiteDatabase(BaseDatabase): await session.execute( update(Persona) .where(col(Persona.persona_id) == item_id) - .values(sort_order=sort_order) + .values(sort_order=sort_order), ) elif item_type == "folder": await session.execute( update(PersonaFolder) .where(col(PersonaFolder.folder_id) == item_id) - .values(sort_order=sort_order) + .values(sort_order=sort_order), ) async def insert_preference_or_update(self, scope, scope_id, key, value): @@ -1452,7 +1455,7 @@ class SQLiteDatabase(BaseDatabase): return new_session async def get_platform_session_by_id( - self, session_id: str + self, session_id: str, ) -> PlatformSession | None: """Get a Platform session by its ID.""" async with self.get_db() as session: @@ -1464,7 +1467,7 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() async def get_platform_sessions_by_ids( - self, session_ids: list[str] + self, session_ids: list[str], ) -> list[PlatformSession]: """Get platform sessions by IDs.""" if not session_ids: @@ -1473,7 +1476,7 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: session: AsyncSession query = select(PlatformSession).where( - col(PlatformSession.session_id).in_(session_ids) + col(PlatformSession.session_id).in_(session_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -1572,7 +1575,7 @@ class SQLiteDatabase(BaseDatabase): ) total_result = await session.execute( - select(func.count()).select_from(base_query.subquery()) + select(func.count()).select_from(base_query.subquery()), ) total = int(total_result.scalar_one() or 0) @@ -1776,7 +1779,7 @@ class SQLiteDatabase(BaseDatabase): return list(result.scalars().all()) async def get_project_by_session( - self, session_id: str, creator: str + self, session_id: str, creator: str, ) -> ChatUIProject | None: """Get the project that a session belongs to.""" async with self.get_db() as session: @@ -1883,7 +1886,7 @@ class SQLiteDatabase(BaseDatabase): ) await session.execute(stmt) result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) + select(CronJob).where(col(CronJob.job_id) == job_id), ) return result.scalar_one_or_none() @@ -1892,14 +1895,14 @@ class SQLiteDatabase(BaseDatabase): session: AsyncSession async with session.begin(): await session.execute( - delete(CronJob).where(col(CronJob.job_id) == job_id) + delete(CronJob).where(col(CronJob.job_id) == job_id), ) async def get_cron_job(self, job_id: str) -> CronJob | None: async with self.get_db() as session: session: AsyncSession result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) + select(CronJob).where(col(CronJob.job_id) == job_id), ) return result.scalar_one_or_none() 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 bc729aac8..44fe671a5 100644 --- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py +++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py @@ -76,7 +76,7 @@ class FaissVecDB(BaseVecDB): if not contents: logger.debug( - "No contents provided for batch insert; skipping embedding generation." + "No contents provided for batch insert; skipping embedding generation.", ) return [] diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 70b5f054e..76e4fc262 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -44,7 +44,7 @@ class EventBus: scheduler = self.pipeline_scheduler_mapping.get(conf_id) if not scheduler: logger.error( - f"PipelineScheduler not found for id: {conf_id}, event ignored." + f"PipelineScheduler not found for id: {conf_id}, event ignored.", ) continue asyncio.create_task(scheduler.execute(event)) diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index 6a2cb5e0a..efb594d90 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -260,7 +260,7 @@ class KBSQLiteDatabase: } async def get_documents_with_metadata_batch( - self, doc_ids: set[str] + self, doc_ids: set[str], ) -> dict[str, dict]: """批量获取文档及其所属知识库元数据 diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index 0863f7e6d..9b75132cd 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -61,8 +61,7 @@ async def _repair_and_translate_chunk_with_retry( rate_limiter: RateLimiter, max_retries: int = 2, ) -> list[str]: - """ - Repairs, translates, and optionally re-chunks a single text chunk using the small LLM, with rate limiting. + """Repairs, translates, and optionally re-chunks a single text chunk using the small LLM, with rate limiting. """ # 为了防止 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. @@ -76,7 +75,7 @@ Text chunk to process: try: async with rate_limiter: response = await repair_llm_service.text_chat( - prompt=user_prompt, system_prompt=TEXT_REPAIR_SYSTEM_PROMPT + prompt=user_prompt, system_prompt=TEXT_REPAIR_SYSTEM_PROMPT, ) llm_output = response.completion_text @@ -94,16 +93,15 @@ Text chunk to process: if matches: # Further cleaning to ensure no empty strings are returned return [m.strip() for m in matches if m.strip()] - else: - # If no valid tags and not explicitly discarded, discard it to be safe. - return [] + # If no valid tags and not explicitly discarded, discard it to be safe. + return [] except Exception as e: logger.warning( - f" - LLM call failed on attempt {attempt + 1}/{max_retries + 1}. Error: {str(e)}" + f" - LLM call failed on attempt {attempt + 1}/{max_retries + 1}. Error: {e!s}", ) logger.error( - f" - Failed to process chunk after {max_retries + 1} attempts. Using original text." + f" - Failed to process chunk after {max_retries + 1} attempts. Using original text.", ) return [chunk] @@ -255,7 +253,7 @@ class KBHelper: # 否则,执行标准的文件解析和分块流程 if file_content is None: raise ValueError( - "当未提供 pre_chunked_text 时,file_content 不能为空。" + "当未提供 pre_chunked_text 时,file_content 不能为空。", ) file_size = len(file_content) @@ -516,11 +514,11 @@ class KBHelper: # 获取 Tavily API 密钥 config = self.prov_mgr.acm.default_conf tavily_keys = config.get("provider_settings", {}).get( - "websearch_tavily_key", [] + "websearch_tavily_key", [], ) if not tavily_keys: raise ValueError( - "Error: Tavily API key is not configured in provider_settings." + "Error: Tavily API key is not configured in provider_settings.", ) # 阶段1: 从 URL 提取内容 @@ -552,11 +550,11 @@ class KBHelper: if enable_cleaning and not final_chunks: raise ValueError( - "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。" + "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。", ) # 创建一个虚拟文件名 - file_name = url.split("/")[-1] or f"document_from_{url}" + file_name = url.rsplit("/", maxsplit=1)[-1] or f"document_from_{url}" if not Path(file_name).suffix: file_name += ".url" @@ -585,21 +583,20 @@ class KBHelper: chunk_size: int = 512, 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 + content, chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) if not cleaning_provider_id: logger.warning( - "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。" + "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。", ) return await self.chunker.chunk(content) @@ -611,7 +608,7 @@ class KBHelper: llm_provider = await self.prov_mgr.get_provider_by_id(cleaning_provider_id) if not llm_provider or not isinstance(llm_provider, LLMProvider): raise ValueError( - f"无法找到 ID 为 {cleaning_provider_id} 的 LLM Provider 或类型不正确" + f"无法找到 ID 为 {cleaning_provider_id} 的 LLM Provider 或类型不正确", ) # 初步分块 @@ -628,7 +625,7 @@ class KBHelper: rate_limiter = RateLimiter(repair_max_rpm) tasks = [ _repair_and_translate_chunk_with_retry( - chunk, llm_provider, rate_limiter + chunk, llm_provider, rate_limiter, ) for chunk in initial_chunks ] @@ -638,13 +635,13 @@ class KBHelper: final_chunks = [] for i, result in enumerate(repaired_results): if isinstance(result, Exception): - logger.warning(f"块 {i} 处理异常: {str(result)}. 回退到原始块。") + 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: diff --git a/astrbot/core/knowledge_base/kb_mgr.py b/astrbot/core/knowledge_base/kb_mgr.py index 8dea163cb..848999593 100644 --- a/astrbot/core/knowledge_base/kb_mgr.py +++ b/astrbot/core/knowledge_base/kb_mgr.py @@ -386,6 +386,7 @@ class KnowledgeBaseManager: Raises: ValueError: 如果知识库不存在或 URL 为空 IOError: 如果网络请求失败 + """ kb_helper = await self.get_kb(kb_id) if not kb_helper: diff --git a/astrbot/core/knowledge_base/parsers/url_parser.py b/astrbot/core/knowledge_base/parsers/url_parser.py index 2867164a9..c87b5c55e 100644 --- a/astrbot/core/knowledge_base/parsers/url_parser.py +++ b/astrbot/core/knowledge_base/parsers/url_parser.py @@ -7,11 +7,11 @@ class URLExtractor: """URL 内容提取器,封装了 Tavily API 调用和密钥管理""" def __init__(self, tavily_keys: list[str]) -> None: - """ - 初始化 URL 提取器 + """初始化 URL 提取器 Args: tavily_keys: Tavily API 密钥列表 + """ if not tavily_keys: raise ValueError("Error: Tavily API keys are not configured.") @@ -28,8 +28,7 @@ class URLExtractor: return key async def extract_text_from_url(self, url: str) -> str: - """ - 使用 Tavily API 从 URL 提取主要文本内容。 + """使用 Tavily API 从 URL 提取主要文本内容。 这是 web_searcher 插件中 tavily_extract_web_page 方法的简化版本, 专门为知识库模块设计,不依赖 AstrMessageEvent。 @@ -42,6 +41,7 @@ class URLExtractor: Raises: ValueError: 如果 URL 为空或 API 密钥未配置 IOError: 如果请求失败或返回错误 + """ if not url: raise ValueError("Error: url must be a non-empty string.") @@ -69,7 +69,7 @@ class URLExtractor: if response.status != 200: reason = await response.text() raise OSError( - f"Tavily web extraction failed: {reason}, status: {response.status}" + f"Tavily web extraction failed: {reason}, status: {response.status}", ) data = await response.json() @@ -89,8 +89,7 @@ class URLExtractor: # 为了向后兼容,提供一个简单的函数接口 async def extract_text_from_url(url: str, tavily_keys: list[str]) -> str: - """ - 简单的函数接口,用于从 URL 提取文本内容 + """简单的函数接口,用于从 URL 提取文本内容 Args: url: 要提取内容的网页 URL @@ -98,6 +97,7 @@ async def extract_text_from_url(url: str, tavily_keys: list[str]) -> str: Returns: 提取的文本内容 + """ extractor = URLExtractor(tavily_keys) return await extractor.extract_text_from_url(url) diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 3dd0719b1..bbac91f86 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -111,7 +111,7 @@ class _LoguruInterceptHandler(logging.Handler): ), "astrbot_version_tag": getattr(record, "astrbot_version_tag", ""), "source_file": getattr( - record, "source_file", _build_source_file(record.pathname) + record, "source_file", _build_source_file(record.pathname), ), "source_line": getattr(record, "source_line", record.lineno), "is_trace": getattr(record, "is_trace", record.name == "astrbot.trace"), @@ -387,7 +387,7 @@ class LogManager: enable = bool( config.get("trace_log_enable") - or (config.get("log_file", {}) or {}).get("trace_enable", False) + or (config.get("log_file", {}) or {}).get("trace_enable", False), ) path = config.get("trace_log_path") max_mb = config.get("trace_log_max_mb") diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 2f19434c9..9dcd65a63 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -157,7 +157,7 @@ class Record(BaseMessageComponent): bs64_data = self.file.removeprefix("base64://") image_bytes = base64.b64decode(bs64_data) file_path = os.path.join( - get_astrbot_temp_path(), f"recordseg_{uuid.uuid4()}.jpg" + get_astrbot_temp_path(), f"recordseg_{uuid.uuid4()}.jpg", ) with open(file_path, "wb") as f: f.write(image_bytes) @@ -246,7 +246,7 @@ class Video(BaseMessageComponent): return url[8:] if url and url.startswith("http"): video_file_path = os.path.join( - get_astrbot_temp_path(), f"videoseg_{uuid.uuid4().hex}" + get_astrbot_temp_path(), f"videoseg_{uuid.uuid4().hex}", ) await download_file(url, video_file_path) if os.path.exists(video_file_path): @@ -442,7 +442,7 @@ class Image(BaseMessageComponent): bs64_data = url.removeprefix("base64://") image_bytes = base64.b64decode(bs64_data) image_file_path = os.path.join( - get_astrbot_temp_path(), f"imgseg_{uuid.uuid4()}.jpg" + get_astrbot_temp_path(), f"imgseg_{uuid.uuid4()}.jpg", ) with open(image_file_path, "wb") as f: f.write(image_bytes) diff --git a/astrbot/core/message/message_event_result.py b/astrbot/core/message/message_event_result.py index 0965fe7f7..65ec0deff 100644 --- a/astrbot/core/message/message_event_result.py +++ b/astrbot/core/message/message_event_result.py @@ -123,21 +123,21 @@ class MessageChain: Args: with_other_comps_mark (bool): 是否在纯文本中标记其他组件的位置 + """ if not with_other_comps_mark: return " ".join( - [comp.text for comp in self.chain if isinstance(comp, Plain)] + [comp.text for comp in self.chain if isinstance(comp, Plain)], ) - else: - texts = [] - for comp in self.chain: - if isinstance(comp, Plain): - texts.append(comp.text) - elif isinstance(comp, Json): - texts.append(f"{comp.data}") - else: - texts.append(f"[{comp.__class__.__name__}]") - return " ".join(texts) + texts = [] + for comp in self.chain: + if isinstance(comp, Plain): + texts.append(comp.text) + elif isinstance(comp, Json): + texts.append(f"{comp.data}") + else: + texts.append(f"[{comp.__class__.__name__}]") + return " ".join(texts) def squash_plain(self): """将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。""" diff --git a/astrbot/core/persona_error_reply.py b/astrbot/core/persona_error_reply.py index 5a99e0918..e6ea1bb10 100644 --- a/astrbot/core/persona_error_reply.py +++ b/astrbot/core/persona_error_reply.py @@ -35,7 +35,7 @@ def extract_persona_custom_error_message_from_event(event: Any) -> str | None: def set_persona_custom_error_message_on_event( - event: Any, message: object + event: Any, message: object, ) -> str | None: """Normalize and store persona custom error reply text into event extras.""" normalized = normalize_persona_custom_error_message(message) @@ -70,16 +70,16 @@ async def resolve_persona_custom_error_message( async def resolve_event_conversation_persona_id( - event: Any, conversation_manager: Any + event: Any, conversation_manager: Any, ) -> str | None: """Resolve current conversation persona_id from event and conversation manager.""" curr_cid = await conversation_manager.get_curr_conversation_id( - event.unified_msg_origin + event.unified_msg_origin, ) if not curr_cid: return None conversation = await conversation_manager.get_conversation( - event.unified_msg_origin, curr_cid + event.unified_msg_origin, curr_cid, ) if not conversation: return None diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index 6320ac3bb..eb57183de 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -88,6 +88,7 @@ class PersonaManager: - selected persona object - force applied persona_id from session rule - whether use webchat special default persona + """ session_service_config = ( await sp.get_async( @@ -174,23 +175,25 @@ class PersonaManager: return await self.db.get_personas() async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """获取指定文件夹中的 personas Args: folder_id: 文件夹 ID,None 表示根目录 + """ return await self.db.get_personas_by_folder(folder_id) async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """移动 persona 到指定文件夹 Args: persona_id: Persona ID folder_id: 目标文件夹 ID,None 表示移动到根目录 + """ persona = await self.db.move_persona_to_folder(persona_id, folder_id) if persona: @@ -228,6 +231,7 @@ class PersonaManager: Args: parent_id: 父文件夹 ID,None 表示获取根目录下的文件夹 + """ return await self.db.get_persona_folders(parent_id) @@ -267,6 +271,7 @@ class PersonaManager: - id: persona_id 或 folder_id - type: "persona" 或 "folder" - sort_order: 新的排序顺序值 + """ await self.db.batch_update_sort_order(items) # 刷新缓存 @@ -278,6 +283,7 @@ class PersonaManager: Returns: 树形结构的文件夹列表,每个文件夹包含 children 子列表 + """ all_folders = await self.get_all_folders() folder_map: dict[str, dict] = {} @@ -333,6 +339,7 @@ class PersonaManager: skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills folder_id: 所属文件夹 ID,None 表示根目录 sort_order: 排序顺序 + """ if await self.db.get_persona_by_id(persona_id): raise ValueError(f"Persona with ID {persona_id} already exists.") diff --git a/astrbot/core/pipeline/__init__.py b/astrbot/core/pipeline/__init__.py index 6a6069ff7..4d851c2f7 100644 --- a/astrbot/core/pipeline/__init__.py +++ b/astrbot/core/pipeline/__init__.py @@ -80,6 +80,7 @@ if TYPE_CHECKING: from .whitelist_check.stage import WhitelistCheckStage __all__ = [ + "STAGES_ORDER", "ContentSafetyCheckStage", "EventResultType", "MessageEventResult", @@ -89,7 +90,6 @@ __all__ = [ "RespondStage", "ResultDecorateStage", "SessionStatusCheckStage", - "STAGES_ORDER", "WakingCheckStage", "WhitelistCheckStage", ] diff --git a/astrbot/core/pipeline/content_safety_check/stage.py b/astrbot/core/pipeline/content_safety_check/stage.py index 19037eb08..4e8d5d8ab 100644 --- a/astrbot/core/pipeline/content_safety_check/stage.py +++ b/astrbot/core/pipeline/content_safety_check/stage.py @@ -26,7 +26,7 @@ class ContentSafetyCheckStage(Stage): check_text: str | None = None, ) -> AsyncGenerator[None, None]: """检查内容安全""" - text = check_text if check_text else event.get_message_str() + text = check_text or event.get_message_str() ok, info = self.strategy_selector.check(text) if not ok: if event.is_at_or_wake_command: 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..eb8d6f8f0 100644 --- a/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py +++ b/astrbot/core/pipeline/content_safety_check/strategies/baidu_aip.py @@ -26,7 +26,7 @@ class BaiduAipStrategy(ContentSafetyStrategy): 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(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/process_stage/follow_up.py b/astrbot/core/pipeline/process_stage/follow_up.py index 79ec16a85..da650b827 100644 --- a/astrbot/core/pipeline/process_stage/follow_up.py +++ b/astrbot/core/pipeline/process_stage/follow_up.py @@ -185,7 +185,7 @@ def try_capture_follow_up(event: AstrMessageEvent) -> FollowUpCapture | None: event.unified_msg_origin, ticket, order_seq, - ) + ), ) logger.info( "Captured follow-up message for active agent run, umo=%s, order_seq=%s", diff --git a/astrbot/core/pipeline/process_stage/method/agent_request.py b/astrbot/core/pipeline/process_stage/method/agent_request.py index 9efe53814..be8c970fd 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_request.py +++ b/astrbot/core/pipeline/process_stage/method/agent_request.py @@ -34,13 +34,13 @@ class AgentRequestSubStage(Stage): async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]: if not self.ctx.astrbot_config["provider_settings"]["enable"]: logger.debug( - "This pipeline does not enable AI capability, skip processing." + "This pipeline does not enable AI capability, skip processing.", ) return if not await SessionServiceManager.should_process_llm_request(event): logger.debug( - f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing." + f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing.", ) return diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index e0ba2463c..08167b3b8 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 @@ -77,19 +77,19 @@ class InternalAgentSubStage(Stage): self.file_extract_enabled: bool = file_extract_conf.get("enable", False) self.file_extract_prov: str = file_extract_conf.get("provider", "moonshotai") self.file_extract_msh_api_key: str = file_extract_conf.get( - "moonshotai_api_key", "" + "moonshotai_api_key", "", ) # 上下文管理相关 self.context_limit_reached_strategy: str = settings.get( - "context_limit_reached_strategy", "truncate_by_turns" + "context_limit_reached_strategy", "truncate_by_turns", ) self.llm_compress_instruction: str = settings.get( - "llm_compress_instruction", "" + "llm_compress_instruction", "", ) self.llm_compress_keep_recent: int = settings.get("llm_compress_keep_recent", 4) self.llm_compress_provider_id: str = settings.get( - "llm_compress_provider_id", "" + "llm_compress_provider_id", "", ) self.max_context_length = settings["max_context_length"] # int self.dequeue_context_length: int = min( @@ -101,7 +101,7 @@ class InternalAgentSubStage(Stage): self.llm_safety_mode = settings.get("llm_safety_mode", True) self.safety_mode_strategy = settings.get( - "safety_mode_strategy", "system_prompt" + "safety_mode_strategy", "system_prompt", ) self.computer_use_runtime = settings.get("computer_use_runtime") @@ -139,7 +139,7 @@ class InternalAgentSubStage(Stage): ) async def process( - self, event: AstrMessageEvent, provider_wake_prefix: str + self, event: AstrMessageEvent, provider_wake_prefix: str, ) -> AsyncGenerator[None, None]: follow_up_capture: FollowUpCapture | None = None follow_up_consumed_marked = False @@ -259,13 +259,13 @@ class InternalAgentSubStage(Stage): # 获取 TTS Provider tts_provider = ( self.ctx.plugin_manager.context.get_using_tts_provider( - event.unified_msg_origin + event.unified_msg_origin, ) ) if not tts_provider: logger.warning( - "[Live Mode] TTS Provider 未配置,将使用普通流式模式" + "[Live Mode] TTS Provider 未配置,将使用普通流式模式", ) # 使用 run_live_agent,总是使用流式响应 @@ -357,7 +357,7 @@ class InternalAgentSubStage(Stage): req, agent_runner, final_resp, - ) + ), ) # 检查事件是否被停止,如果被停止则不保存历史记录 @@ -385,7 +385,7 @@ class InternalAgentSubStage(Stage): except Exception as e: logger.error(f"Error occurred while processing agent: {e}") custom_error_message = extract_persona_custom_error_message_from_event( - event + event, ) error_text = custom_error_message or ( f"Error occurred while processing agent request: {e}" 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 9ab315779..91321acb4 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 @@ -64,8 +64,7 @@ async def run_third_party_agent( stream_to_general: bool = False, custom_error_message: str | None = None, ) -> AsyncGenerator[tuple[MessageChain, bool], None]: - """ - 运行第三方 agent runner 并转换响应格式 + """运行第三方 agent runner 并转换响应格式 类似于 run_agent 函数,但专门处理第三方 agent runner """ try: @@ -86,7 +85,7 @@ async def run_third_party_agent( err_msg = ( f"Error occurred during AI execution.\n" f"Error Type: {type(e).__name__} (3rd party)\n" - f"Error Message: {str(e)}" + f"Error Message: {e!s}" ) yield MessageChain().message(err_msg), True @@ -187,7 +186,7 @@ class ThirdPartyAgentSubStage(Stage): ) async def _resolve_persona_custom_error_message( - self, event: AstrMessageEvent + self, event: AstrMessageEvent, ) -> str | None: try: conversation_persona_id = await resolve_event_conversation_persona_id( @@ -241,7 +240,7 @@ class ThirdPartyAgentSubStage(Stage): if runner.done(): final_chain, is_runner_error = aggregator.finalize( - runner.get_final_llm_resp() + runner.get_final_llm_resp(), ) event.set_extra(THIRD_PARTY_RUNNER_ERROR_EXTRA_KEY, is_runner_error) event.set_result( @@ -287,12 +286,12 @@ class ThirdPartyAgentSubStage(Stage): yield async def process( - self, event: AstrMessageEvent, provider_wake_prefix: str + self, event: AstrMessageEvent, provider_wake_prefix: str, ) -> AsyncGenerator[None, None]: req: ProviderRequest | None = None if provider_wake_prefix and not event.message_str.startswith( - provider_wake_prefix + provider_wake_prefix, ): return @@ -305,7 +304,7 @@ class ThirdPartyAgentSubStage(Stage): 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/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index aea6a74b3..715e01430 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -40,7 +40,7 @@ class RespondStage(Stage): ), # 音乐分享 Comp.Forward: lambda comp: bool(comp.id), # 合并转发 Comp.Location: lambda comp: bool( - comp.lat is not None and comp.lon is not None + 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, # 窗口抖动(戳一戳) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 4ee746130..3e0a4d3c8 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -76,10 +76,10 @@ class ResultDecorateStage(Stage): ].get("split_words", ["。", "?", "!", "~", "…"]) if self.split_words: escaped_words = sorted( - [re.escape(word) for word in self.split_words], key=len, reverse=True + [re.escape(word) for word in self.split_words], key=len, reverse=True, ) self.split_words_pattern = re.compile( - f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL + f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL, ) else: self.split_words_pattern = None @@ -121,7 +121,7 @@ class ResultDecorateStage(Stage): result.append(content) elif seg and seg.strip(): result.append(seg) - return result if result else [text] + return result or [text] async def process( self, diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index ddc2a6cb8..26134bdb2 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -69,7 +69,7 @@ class WakingCheckStage(Stage): False, ) self.disable_builtin_commands = self.ctx.astrbot_config.get( - "disable_builtin_commands", False + "disable_builtin_commands", False, ) platform_settings = self.ctx.astrbot_config.get("platform_settings", {}) self.unique_session = platform_settings.get("unique_session", False) diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 645436702..5b6882e71 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -60,7 +60,7 @@ class AstrMessageEvent(abc.ABC): except (ValueError, TypeError, AttributeError): logger.warning( f"Failed to convert message type {message_obj.type!r} to MessageType. " - f"Falling back to FRIEND_MESSAGE." + f"Falling back to FRIEND_MESSAGE.", ) message_type = MessageType.FRIEND_MESSAGE self.session = MessageSession( diff --git a/astrbot/core/platform/astrbot_message.py b/astrbot/core/platform/astrbot_message.py index 3db53fd48..05da0b8c5 100644 --- a/astrbot/core/platform/astrbot_message.py +++ b/astrbot/core/platform/astrbot_message.py @@ -15,7 +15,7 @@ class MessageMember: # 使用 f-string 来构建返回的字符串表示形式 return ( f"User ID: {self.user_id}," - f"Nickname: {self.nickname if self.nickname else 'N/A'}" + f"Nickname: {self.nickname or 'N/A'}" ) @@ -38,10 +38,10 @@ class Group: # 使用 f-string 来构建返回的字符串表示形式 return ( f"Group ID: {self.group_id}\n" - f"Name: {self.group_name if self.group_name else 'N/A'}\n" - f"Avatar: {self.group_avatar if self.group_avatar else 'N/A'}\n" - f"Owner ID: {self.group_owner if self.group_owner else 'N/A'}\n" - f"Admin IDs: {self.group_admins if self.group_admins else 'N/A'}\n" + f"Name: {self.group_name or 'N/A'}\n" + f"Avatar: {self.group_avatar or 'N/A'}\n" + f"Owner ID: {self.group_owner or 'N/A'}\n" + f"Admin IDs: {self.group_admins or 'N/A'}\n" f"Members Len: {len(self.members) if self.members else 0}\n" f"First Member: {self.members[0] if self.members else 'N/A'}\n" ) diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index d592eb2fb..cff2bfe95 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -228,7 +228,7 @@ class PlatformManager: logger.error(traceback.format_exc()) async def _task_wrapper( - self, task: asyncio.Task, platform: Platform | None = None + self, task: asyncio.Task, platform: Platform | None = None, ) -> None: # 设置平台状态为运行中 if platform: @@ -309,6 +309,7 @@ class PlatformManager: Returns: 包含所有平台统计信息的字典 + """ stats_list = [] total_errors = 0 @@ -334,7 +335,7 @@ class PlatformManager: "status": "unknown", "error_count": 0, "last_error": None, - } + }, ) return { diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index a7c181217..dea6b73c0 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -84,7 +84,7 @@ class Platform(abc.ABC): """是否正在使用统一 Webhook 模式""" return bool( self.config.get("unified_webhook_mode", False) - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) def get_stats(self) -> dict: @@ -161,5 +161,6 @@ class Platform(abc.ABC): Raises: NotImplementedError: 平台未实现统一 Webhook 模式 + """ raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式") diff --git a/astrbot/core/platform/register.py b/astrbot/core/platform/register.py index 62ec5070a..32d6b6b3e 100644 --- a/astrbot/core/platform/register.py +++ b/astrbot/core/platform/register.py @@ -73,6 +73,7 @@ def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str] Returns: 被注销的平台适配器名称列表 + """ unregistered = [] to_remove = [] diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index 7110199af..d023d7421 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -48,7 +48,7 @@ class AiocqhttpAdapter(Platform): self.metadata = PlatformMetadata( name="aiocqhttp", description="适用于 OneBot 标准的消息平台适配器,支持反向 WebSockets。", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_streaming_message=False, ) @@ -145,7 +145,7 @@ class AiocqhttpAdapter(Platform): abm = AstrBotMessage() abm.self_id = str(event.self_id) abm.sender = MessageMember( - user_id=str(event.user_id), nickname=str(event.user_id) + user_id=str(event.user_id), nickname=str(event.user_id), ) abm.type = MessageType.OTHER_MESSAGE if event.get("group_id"): @@ -170,7 +170,7 @@ class AiocqhttpAdapter(Platform): abm = AstrBotMessage() abm.self_id = str(event.self_id) abm.sender = MessageMember( - user_id=str(event.user_id), nickname=str(event.user_id) + user_id=str(event.user_id), nickname=str(event.user_id), ) abm.type = MessageType.OTHER_MESSAGE if event.get("group_id"): @@ -399,14 +399,14 @@ 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"]) abm.message.append(a) except Exception as e: logger.exception( - f"消息段解析失败: type={t}, data={m['data']}. {e}" + f"消息段解析失败: type={t}, data={m['data']}. {e}", ) continue diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 37c3b09ab..d35dc6489 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -47,7 +47,7 @@ class MyEventHandler(dingtalk_stream.EventHandler): @register_platform_adapter( - "dingtalk", "钉钉机器人官方 API 适配器", support_streaming_message=True + "dingtalk", "钉钉机器人官方 API 适配器", support_streaming_message=True, ) class DingtalkPlatformAdapter(Platform): def __init__( @@ -140,7 +140,7 @@ class DingtalkPlatformAdapter(Platform): return PlatformMetadata( name="dingtalk", description="钉钉机器人官方 API 适配器", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_streaming_message=True, support_proactive_message=True, ) @@ -152,7 +152,7 @@ class DingtalkPlatformAdapter(Platform): abm = AstrBotMessage() abm.message = [] abm.message_str = "" - abm.timestamp = int(cast(int, message.create_at) / 1000) + abm.timestamp = int(cast("int", message.create_at) / 1000) abm.type = ( MessageType.GROUP_MESSAGE if message.conversation_type == "2" @@ -163,7 +163,7 @@ class DingtalkPlatformAdapter(Platform): nickname=message.sender_nick, ) abm.self_id = self._id_to_sid(message.chatbot_user_id) - abm.message_id = cast(str, message.message_id) + abm.message_id = cast("str", message.message_id) abm.raw_message = message if abm.type == MessageType.GROUP_MESSAGE: @@ -177,9 +177,9 @@ class DingtalkPlatformAdapter(Platform): else: abm.session_id = abm.sender.user_id - message_type: str = cast(str, message.message_type) - robot_code = cast(str, message.robot_code or "") - raw_content = cast(dict, message.extensions.get("content") or {}) + message_type: str = cast("str", message.message_type) + robot_code = cast("str", message.robot_code or "") + raw_content = cast("dict", message.extensions.get("content") or {}) if not isinstance(raw_content, dict): raw_content = {} match message_type: @@ -192,11 +192,11 @@ class DingtalkPlatformAdapter(Platform): await self._remember_sender_binding(message, abm) return abm image_content = cast( - dingtalk_stream.ImageContent | None, + "dingtalk_stream.ImageContent | None", message.image_content, ) download_code = cast( - str, (image_content.download_code if image_content else "") or "" + "str", (image_content.download_code if image_content else "") or "", ) if not download_code: logger.warning("钉钉图片消息缺少 downloadCode,已跳过") @@ -212,26 +212,26 @@ class DingtalkPlatformAdapter(Platform): logger.warning("钉钉图片消息下载失败,无法解析为图片") case "richText": rtc: dingtalk_stream.RichTextContent = cast( - dingtalk_stream.RichTextContent, message.rich_text_content + "dingtalk_stream.RichTextContent", message.rich_text_content, ) - contents: list[dict] = cast(list[dict], rtc.rich_text_list) + contents: list[dict] = cast("list[dict]", rtc.rich_text_list) plain_parts: list[str] = [] for content in contents: if "text" in content: - plain_text = cast(str, content.get("text") or "") + plain_text = cast("str", content.get("text") or "") if plain_text: plain_parts.append(plain_text) abm.message.append(Plain(plain_text)) elif "type" in content and content["type"] == "picture": - download_code = cast(str, content.get("downloadCode") or "") + download_code = cast("str", content.get("downloadCode") or "") if not download_code: logger.warning( - "钉钉富文本图片消息缺少 downloadCode,已跳过" + "钉钉富文本图片消息缺少 downloadCode,已跳过", ) continue if not robot_code: logger.error( - "钉钉富文本图片消息解析失败: 回调中缺少 robotCode" + "钉钉富文本图片消息解析失败: 回调中缺少 robotCode", ) continue f_path = await self.download_ding_file( @@ -243,13 +243,13 @@ class DingtalkPlatformAdapter(Platform): abm.message.append(Image.fromFileSystem(f_path)) abm.message_str = "".join(plain_parts).strip() case "audio" | "voice": - download_code = cast(str, raw_content.get("downloadCode") or "") + download_code = cast("str", raw_content.get("downloadCode") or "") if not download_code: logger.warning("钉钉语音消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉语音消息解析失败: 回调中缺少 robotCode") else: - voice_ext = cast(str, raw_content.get("fileExtension") or "") + voice_ext = cast("str", raw_content.get("fileExtension") or "") if not voice_ext: voice_ext = "amr" voice_ext = voice_ext.lstrip(".") @@ -261,16 +261,16 @@ class DingtalkPlatformAdapter(Platform): if f_path: abm.message.append(Record.fromFileSystem(f_path)) case "file": - download_code = cast(str, raw_content.get("downloadCode") or "") + download_code = cast("str", raw_content.get("downloadCode") or "") if not download_code: logger.warning("钉钉文件消息缺少 downloadCode,已跳过") elif not robot_code: logger.error("钉钉文件消息解析失败: 回调中缺少 robotCode") else: - file_name = cast(str, raw_content.get("fileName") or "") + file_name = cast("str", raw_content.get("fileName") or "") file_ext = Path(file_name).suffix.lstrip(".") if file_name else "" if not file_ext: - file_ext = cast(str, raw_content.get("fileExtension") or "") + file_ext = cast("str", raw_content.get("fileExtension") or "") if not file_ext: file_ext = "file" f_path = await self.download_ding_file( @@ -294,14 +294,14 @@ class DingtalkPlatformAdapter(Platform): try: if abm.type == MessageType.FRIEND_MESSAGE: sender_id = abm.sender.user_id - sender_staff_id = cast(str, message.sender_staff_id or "") + sender_staff_id = cast("str", message.sender_staff_id or "") if sender_staff_id: umo = str( MessageSesion( platform_name=self.meta().id, message_type=abm.type, session_id=sender_id, - ) + ), ) await sp.put_async( "global", @@ -352,7 +352,7 @@ class DingtalkPlatformAdapter(Platform): return "" resp_data = await resp.json() download_url = cast( - str, + "str", ( resp_data.get("downloadUrl") or resp_data.get("data", {}).get("downloadUrl") @@ -388,7 +388,7 @@ class DingtalkPlatformAdapter(Platform): ) return "" data = await resp.json() - return cast(str, data.get("data", {}).get("accessToken", "")) + return cast("str", data.get("data", {}).get("accessToken", "")) async def _get_sender_staff_id(self, session: MessageSesion) -> str: try: @@ -398,7 +398,7 @@ class DingtalkPlatformAdapter(Platform): "dingtalk_staffid", "", ) - return cast(str, staff_id or "") + return cast("str", staff_id or "") except Exception as e: logger.warning(f"读取钉钉 staff_id 映射失败: {e}") return "" @@ -425,16 +425,15 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( - "https://api.dingtalk.com/v1.0/robot/groupMessages/send", - headers=headers, - json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", - ) + async with aiohttp.ClientSession() as session, session.post( + "https://api.dingtalk.com/v1.0/robot/groupMessages/send", + headers=headers, + json=payload, + ) as resp: + if resp.status != 200: + logger.error( + f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", + ) async def _send_private_message( self, @@ -458,16 +457,15 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( - "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", - headers=headers, - json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", - ) + async with aiohttp.ClientSession() as session, session.post( + "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", + headers=headers, + json=payload, + ) as resp: + if resp.status != 200: + logger.error( + f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", + ) def _safe_remove_file(self, file_path: str | None) -> None: if not file_path: @@ -514,14 +512,14 @@ class DingtalkPlatformAdapter(Platform): ) as resp: if resp.status != 200: logger.error( - f"钉钉媒体上传失败: {resp.status}, {await resp.text()}" + f"钉钉媒体上传失败: {resp.status}, {await resp.text()}", ) return "" data = await resp.json() if data.get("errcode") != 0: logger.error(f"钉钉媒体上传失败: {data}") return "" - return cast(str, data.get("media_id", "")) + return cast("str", data.get("media_id", "")) async def upload_image(self, image: Image) -> str: image_file_path = await image.convert_to_file_path() @@ -693,8 +691,8 @@ class DingtalkPlatformAdapter(Platform): robot_code = self.client_id # at_list: list[str] = [] - sender_id = cast(str, incoming_message.sender_id or "") - sender_staff_id = cast(str, incoming_message.sender_staff_id or "") + sender_id = cast("str", incoming_message.sender_id or "") + sender_staff_id = cast("str", incoming_message.sender_staff_id or "") normalized_sender_id = self._id_to_sid(sender_id) # 现在用的发消息接口不支持 at # for segment in message_chain.chain: @@ -710,7 +708,7 @@ class DingtalkPlatformAdapter(Platform): if incoming_message.conversation_type == "2": await self.send_message_chain_to_group( - open_conversation_id=cast(str, incoming_message.conversation_id), + open_conversation_id=cast("str", incoming_message.conversation_id), robot_code=robot_code, message_chain=message_chain, # at_str=at_str, diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 19b9c81b4..e1268420e 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -35,7 +35,7 @@ else: # 注册平台适配器 @register_platform_adapter( - "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False + "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False, ) class DiscordPlatformAdapter(Platform): def __init__( @@ -64,7 +64,7 @@ class DiscordPlatformAdapter(Platform): """通过会话发送消息""" if self.client.user is None: logger.error( - "[Discord] Client is not ready (self.client.user is None); message send skipped" + "[Discord] Client is not ready (self.client.user is None); message send skipped", ) return @@ -95,7 +95,7 @@ class DiscordPlatformAdapter(Platform): user_id=str(self.bot_self_id), nickname=self.client.user.display_name, ) - message_obj.self_id = cast(str, self.bot_self_id) + message_obj.self_id = cast("str", self.bot_self_id) message_obj.session_id = session.session_id message_obj.message = message_chain.chain @@ -116,7 +116,7 @@ class DiscordPlatformAdapter(Platform): return PlatformMetadata( "discord", "Discord Adapter", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), default_config_tmpl=self.config, support_streaming_message=False, ) @@ -137,7 +137,7 @@ class DiscordPlatformAdapter(Platform): token = str(self.config.get("discord_token")) if not token: logger.error( - "[Discord] Bot token is not configured. Please set a valid token in the config file." + "[Discord] Bot token is not configured. Please set a valid token in the config file.", ) return @@ -156,7 +156,7 @@ class DiscordPlatformAdapter(Platform): ) except Exception as e: logger.error( - f"[Discord] on_ready_once_callback err: {e}", exc_info=True + f"[Discord] on_ready_once_callback err: {e}", exc_info=True, ) self.client.on_ready_once_callback = callback @@ -166,7 +166,7 @@ class DiscordPlatformAdapter(Platform): await self.shutdown_event.wait() except discord.errors.LoginFailure: logger.error( - "[Discord] Login failed. Please check whether the bot token is correct." + "[Discord] Login failed. Please check whether the bot token is correct.", ) except discord.errors.ConnectionClosed: logger.warning("[Discord] Connection with Discord has been closed.") @@ -189,7 +189,7 @@ class DiscordPlatformAdapter(Platform): return MessageType.GROUP_MESSAGE def _get_channel_id( - self, channel: Messageable | GuildChannel | PrivateChannel + self, channel: Messageable | GuildChannel | PrivateChannel, ) -> str: """根据 channel 对象获取ID""" return str(getattr(channel, "id", None)) @@ -253,7 +253,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = message_chain abm.raw_message = message - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = cast("str", self.bot_self_id) abm.session_id = str(message.channel.id) abm.message_id = str(message.id) return abm @@ -276,7 +276,7 @@ class DiscordPlatformAdapter(Platform): if self.client.user is None: logger.error( - "[Discord] Client is not ready (self.client.user is None); message handling skipped" + "[Discord] Client is not ready (self.client.user is None); message handling skipped", ) return @@ -295,7 +295,7 @@ class DiscordPlatformAdapter(Platform): raw_message = message.raw_message if not isinstance(raw_message, discord.Message): logger.warning( - f"[Discord] Non-Message type received and ignored: {type(raw_message)}" + f"[Discord] Non-Message type received and ignored: {type(raw_message)}", ) return @@ -351,7 +351,7 @@ class DiscordPlatformAdapter(Platform): logger.info("[Discord] Commands cleaned up successfully.") except Exception as e: logger.warning( - f"[Discord] Error occurred while cleaning up commands: {e}" + f"[Discord] Error occurred while cleaning up commands: {e}", ) if self._polling_task: @@ -362,7 +362,7 @@ class DiscordPlatformAdapter(Platform): logger.info("[Discord] Polling task cancelled successfully.") except Exception as e: logger.warning( - f"[Discord] Error occurred while cancelling polling task: {e}" + f"[Discord] Error occurred while cancelling polling task: {e}", ) logger.info("[Discord] Closing client connection...") if self.client and hasattr(self.client, "close"): @@ -433,7 +433,7 @@ class DiscordPlatformAdapter(Platform): """为每个指令动态创建一个异步回调函数""" async def dynamic_callback( - ctx: discord.ApplicationContext, params: str | None = None + ctx: discord.ApplicationContext, params: str | None = None, ) -> None: # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter logger.debug(f"[Discord] Callback triggered: {cmd_name}") @@ -479,7 +479,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = cast("str", self.bot_self_id) abm.session_id = str(ctx.channel_id) abm.message_id = str(ctx.interaction.id) diff --git a/astrbot/core/platform/sources/discord/discord_platform_event.py b/astrbot/core/platform/sources/discord/discord_platform_event.py index 02d4dae86..4b8ea6f9f 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_event.py +++ b/astrbot/core/platform/sources/discord/discord_platform_event.py @@ -98,7 +98,7 @@ class DiscordPlatformEvent(AstrMessageEvent): await super().send(message) async def send_streaming( - self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False + self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False, ): buffer = None async for chain in generator: @@ -274,8 +274,8 @@ class DiscordPlatformEvent(AstrMessageEvent): self.message_obj.raw_message, "add_reaction", ): - await cast(discord.Message, self.message_obj.raw_message).add_reaction( - emoji + await cast("discord.Message", self.message_obj.raw_message).add_reaction( + emoji, ) except Exception as e: logger.error(f"[Discord] 添加反应失败: {e}") @@ -285,7 +285,7 @@ class DiscordPlatformEvent(AstrMessageEvent): return ( hasattr(self.message_obj, "raw_message") and hasattr(self.message_obj.raw_message, "type") - and cast(discord.Interaction, self.message_obj.raw_message).type + and cast("discord.Interaction", self.message_obj.raw_message).type == discord.InteractionType.application_command ) @@ -294,7 +294,7 @@ class DiscordPlatformEvent(AstrMessageEvent): return ( hasattr(self.message_obj, "raw_message") and hasattr(self.message_obj.raw_message, "type") - and cast(discord.Interaction, self.message_obj.raw_message).type + and cast("discord.Interaction", self.message_obj.raw_message).type == discord.InteractionType.component ) @@ -303,8 +303,8 @@ class DiscordPlatformEvent(AstrMessageEvent): if self.is_button_interaction(): try: return cast( - ComponentInteractionData, - cast(discord.Interaction, self.message_obj.raw_message).data, + "ComponentInteractionData", + cast("discord.Interaction", self.message_obj.raw_message).data, ).get("custom_id", "") except Exception: pass @@ -319,7 +319,7 @@ class DiscordPlatformEvent(AstrMessageEvent): return any( mention.id == int(self.message_obj.self_id) for mention in cast( - discord.Message, self.message_obj.raw_message + "discord.Message", self.message_obj.raw_message, ).mentions ) return False @@ -330,5 +330,5 @@ class DiscordPlatformEvent(AstrMessageEvent): self.message_obj.raw_message, "clean_content", ): - return cast(discord.Message, self.message_obj.raw_message).clean_content + return cast("discord.Message", self.message_obj.raw_message).clean_content return self.message_str diff --git a/astrbot/core/platform/sources/kook/kook_adapter.py b/astrbot/core/platform/sources/kook/kook_adapter.py index 7095d7447..010d0d97e 100644 --- a/astrbot/core/platform/sources/kook/kook_adapter.py +++ b/astrbot/core/platform/sources/kook/kook_adapter.py @@ -43,7 +43,7 @@ KOOK_AT_SELECTOR_REGEX = re.compile(r"\(met\)([^()]+)\(met\)") ) class KookPlatformAdapter(Platform): def __init__( - self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue + self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue, ) -> None: super().__init__(platform_config, event_queue) self.kook_config = KookConfig.from_dict(platform_config) @@ -55,7 +55,7 @@ class KookPlatformAdapter(Platform): self._main_task = None async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain + self, session: MessageSesion, message_chain: MessageChain, ): inner_message = AstrBotMessage() inner_message.session_id = session.session_id @@ -71,7 +71,7 @@ class KookPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - name="kook", description="KOOK 适配器", id=self.kook_config.id + name="kook", description="KOOK 适配器", id=self.kook_config.id, ) def _should_ignore_event_by_bot_nickname(self, author_id: str) -> bool: @@ -79,7 +79,7 @@ class KookPlatformAdapter(Platform): async def _on_received(self, event: KookMessageEventData): logger.debug( - f'[KOOK] 收到来自"{event.channel_type.name}"渠道的消息, 消息类型为: {event.type.name}({event.type.value})' + f'[KOOK] 收到来自"{event.channel_type.name}"渠道的消息, 消息类型为: {event.type.name}({event.type.value})', ) event_type = event.type if event_type in (KookMessageType.KMARKDOWN, KookMessageType.CARD): @@ -136,7 +136,7 @@ class KookPlatformAdapter(Platform): # 等待 client 内部触发 _stop_event,或者超时 1 秒后重试 # 使用 wait_for 配合 timeout 是为了防止极端情况下 self.running 变化没被察觉 await asyncio.wait_for( - self.client.wait_until_closed(), timeout=1.0 + self.client.wait_until_closed(), timeout=1.0, ) except asyncio.TimeoutError: # 正常超时,继续下一轮 while 检查 @@ -148,7 +148,7 @@ class KookPlatformAdapter(Platform): else: consecutive_failures += 1 logger.error( - f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}" + f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}", ) if consecutive_failures >= max_consecutive_failures: @@ -157,7 +157,7 @@ class KookPlatformAdapter(Platform): # 等待一段时间后重试 wait_time = min( - 2**consecutive_failures, max_retry_delay + 2**consecutive_failures, max_retry_delay, ) # 指数退避 logger.info(f"[KOOK] 等待 {wait_time} 秒后重试...") await asyncio.sleep(wait_time) @@ -192,13 +192,13 @@ class KookPlatformAdapter(Platform): logger.info("[KOOK] 资源清理完成") def _parse_kmarkdown_text_message( - self, data: KookMessageEventData, self_id: str + self, data: KookMessageEventData, self_id: str, ) -> tuple[list, str]: kmarkdown = data.extra.kmarkdown content = data.content or "" if kmarkdown is None: logger.error( - f'[KOOK] 无法转换"{KookMessageType.KMARKDOWN.name}"消息, 消息中找不到kmarkdown字段' + f'[KOOK] 无法转换"{KookMessageType.KMARKDOWN.name}"消息, 消息中找不到kmarkdown字段', ) logger.error(f"[KOOK] 原始消息内容: {data.to_json()}") return [], "" @@ -237,7 +237,7 @@ class KookPlatformAdapter(Platform): At( qq=mention_target, name=mention_name_map.get(mention_target, ""), - ) + ), ) cursor = match.end() @@ -341,7 +341,7 @@ class KookPlatformAdapter(Platform): return "" def _handle_image_group( - self, module: ContainerModule | ImageGroupModule + self, module: ContainerModule | ImageGroupModule, ) -> list[str]: """专门处理图片组/容器里的合法 URL 提取""" valid_urls = [] diff --git a/astrbot/core/platform/sources/kook/kook_client.py b/astrbot/core/platform/sources/kook/kook_client.py index 32874f78a..b9213b80b 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: self._http_client = aiohttp.ClientSession( headers={ "Authorization": f"Bot {self.config.token}", - } + }, ) self.event_callback = event_callback # 回调函数,用于处理接收到的事件 self.ws = None @@ -73,21 +73,21 @@ 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: resp_content = KookUserMeResponse.from_dict(await resp.json()) except pydantic.ValidationError as e: logger.error( - f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}" + f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}", ) logger.error(f"[KOOK] 响应内容: {await resp.text()}") return if not resp_content.success(): logger.error( - f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}" + f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}", ) return @@ -125,7 +125,7 @@ class KookClient: return None gateway_url: str = resp_content.data.url - logger.info(f"[KOOK] 获取gateway成功: {gateway_url.split('?')[0]}") + logger.info(f"[KOOK] 获取gateway成功: {gateway_url.split('?', maxsplit=1)[0]}") return gateway_url except pydantic.ValidationError as e: @@ -149,7 +149,7 @@ class KookClient: try: # 获取gateway地址 gateway_url = await self.get_gateway_url( - resume=resume, sn=self.last_sn, session_id=self.session_id + resume=resume, sn=self.last_sn, session_id=self.session_id, ) await self.get_bot_info() @@ -251,7 +251,7 @@ class KookClient: case _: logger.debug( - f"[KOOK] 未处理的信令类型: {event.signal.name}({event.signal.value})" + f"[KOOK] 未处理的信令类型: {event.signal.name}({event.signal.value})", ) async def _handle_hello(self, data: KookHelloEventData): @@ -293,7 +293,7 @@ class KookClient: try: # 随机化心跳间隔 (±5秒) interval = max( - 1, self.config.heartbeat_interval + random.randint(-5, 5) + 1, self.config.heartbeat_interval + random.randint(-5, 5), ) await asyncio.sleep(interval) @@ -313,7 +313,7 @@ class KookClient: ): self.heartbeat_failed_count += 1 logger.warning( - f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}" + f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}", ) if ( @@ -376,19 +376,19 @@ class KookClient: result = await resp.json() if result.get("code") != 0: raise RuntimeError( - f'发送kook消息类型 "{kook_message_type.name}" 失败: {result}' + f'发送kook消息类型 "{kook_message_type.name}" 失败: {result}', ) # else: # logger.info("[KOOK] 发送消息成功") else: raise RuntimeError( - f'发送kook消息类型 "{kook_message_type.name}" HTTP错误: {resp.status} , 响应内容 : {await resp.text()}' + f'发送kook消息类型 "{kook_message_type.name}" HTTP错误: {resp.status} , 响应内容 : {await resp.text()}', ) except RuntimeError: raise except Exception as e: logger.error( - f'[KOOK] 发送kook消息类型 "{kook_message_type.name}" 异常: {e}' + f'[KOOK] 发送kook消息类型 "{kook_message_type.name}" 异常: {e}', ) async def upload_asset(self, file_url: str | None) -> str: @@ -418,7 +418,7 @@ class KookClient: except Exception as exp: logger.error(f'[KOOK] 获取文件 "{file_url}" 绝对路径失败: "{exp}"') raise FileNotFoundError( - f'获取文件 "{file_url}" 绝对路径失败: "{exp}"' + f'获取文件 "{file_url}" 绝对路径失败: "{exp}"', ) from exp if not target_path.is_file(): @@ -445,12 +445,10 @@ class KookClient: remote_url = result["data"]["url"] logger.debug(f"[KOOK] 文件远端URL: {remote_url}") return remote_url - else: - raise RuntimeError(f"上传文件到kook服务器失败: {result}") - else: - raise RuntimeError( - f"上传文件到kook服务器 HTTP错误: {resp.status} , {await resp.text()}" - ) + raise RuntimeError(f"上传文件到kook服务器失败: {result}") + raise RuntimeError( + f"上传文件到kook服务器 HTTP错误: {resp.status} , {await resp.text()}", + ) except RuntimeError: raise except Exception as e: diff --git a/astrbot/core/platform/sources/kook/kook_event.py b/astrbot/core/platform/sources/kook/kook_event.py index 884d066d8..bbe757eb5 100644 --- a/astrbot/core/platform/sources/kook/kook_event.py +++ b/astrbot/core/platform/sources/kook/kook_event.py @@ -48,10 +48,10 @@ class KookEvent(AstrMessageEvent): self._file_message_counter = 0 def _wrap_message( - self, index: int, message_component: BaseMessageComponent + self, index: int, message_component: BaseMessageComponent, ) -> Coroutine[Any, Any, OrderMessage]: async def wrap_upload( - index: int, message_type: KookMessageType, upload_coro + index: int, message_type: KookMessageType, upload_coro, ) -> OrderMessage: url = await upload_coro return OrderMessage(index=index, text=url, type=message_type) @@ -93,7 +93,7 @@ class KookEvent(AstrMessageEvent): f_data = await f_item.get_file() url = await self.client.upload_asset(f_data) return OrderMessage( - index=index, text=url, type=KookMessageType.FILE + index=index, text=url, type=KookMessageType.FILE, ) self._file_message_counter += 1 @@ -115,10 +115,10 @@ class KookEvent(AstrMessageEvent): type=KookModuleType.AUDIO, title=title, src=url, - ) - ] - ) - ] + ), + ], + ), + ], ).to_json(), type=KookMessageType.CARD, ) @@ -147,7 +147,7 @@ class KookEvent(AstrMessageEvent): ) case _: raise NotImplementedError( - f'kook适配器尚未实现对 "{message_component.type}" 消息类型的支持' + f'kook适配器尚未实现对 "{message_component.type}" 消息类型的支持', ) async def send(self, message: MessageChain): diff --git a/astrbot/core/platform/sources/kook/kook_types.py b/astrbot/core/platform/sources/kook/kook_types.py index 5efaf2a14..d7a7c9fc9 100644 --- a/astrbot/core/platform/sources/kook/kook_types.py +++ b/astrbot/core/platform/sources/kook/kook_types.py @@ -57,7 +57,7 @@ class KookModuleType(str, Enum): ThemeType = Literal[ - "primary", "success", "danger", "warning", "info", "secondary", "none", "invisible" + "primary", "success", "danger", "warning", "info", "secondary", "none", "invisible", ] """主题,可选的值为:primary, success, danger, warning, info, secondary, none.默认为 primary,为 none 时不显示侧边框。""" SizeType = Literal["xs", "sm", "md", "lg"] @@ -276,7 +276,7 @@ class KookCardMessageContainer(list[KookCardMessage]): def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str: return json.dumps( - [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii + [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii, ) @classmethod @@ -293,7 +293,8 @@ class OrderMessage(BaseModel): class KookMessageSignal(IntEnum): """KOOK WebSocket 信令类型 - ws文档: https://developer.kookapp.cn/doc/websocket""" # noqa: W291 + ws文档: https://developer.kookapp.cn/doc/websocket + """ MESSAGE = 0 """server->client 消息(s包含聊天和通知消息)""" @@ -350,7 +351,7 @@ class KookExtra(KookBaseDataClass): class KookMessageEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.MESSAGE] = Field( - KookMessageSignal.MESSAGE, exclude=True + KookMessageSignal.MESSAGE, exclude=True, ) """only for type hint""" @@ -368,7 +369,7 @@ class KookMessageEventData(KookBaseDataClass): class KookHelloEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.HELLO] = Field( - KookMessageSignal.HELLO, exclude=True + KookMessageSignal.HELLO, exclude=True, ) """only for type hint""" @@ -378,28 +379,28 @@ class KookHelloEventData(KookBaseDataClass): class KookPingEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.PING] = Field( - KookMessageSignal.PING, exclude=True + KookMessageSignal.PING, exclude=True, ) """only for type hint""" class KookPongEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.PONG] = Field( - KookMessageSignal.PONG, exclude=True + KookMessageSignal.PONG, exclude=True, ) """only for type hint""" class KookResumeEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RESUME] = Field( - KookMessageSignal.RESUME, exclude=True + KookMessageSignal.RESUME, exclude=True, ) """only for type hint""" class KookReconnectEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RECONNECT] = Field( - KookMessageSignal.RECONNECT, exclude=True + KookMessageSignal.RECONNECT, exclude=True, ) """only for type hint""" @@ -409,7 +410,7 @@ class KookReconnectEventData(KookBaseDataClass): class KookResumeAckEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RESUME_ACK] = Field( - KookMessageSignal.RESUME_ACK, exclude=True + KookMessageSignal.RESUME_ACK, exclude=True, ) """only for type hint""" @@ -420,7 +421,7 @@ class KookWebsocketEvent(KookBaseDataClass): """KOOK WebSocket 原始推送结构""" signal: KookMessageSignal = Field( - ..., validation_alias="s", serialization_alias="s" + ..., validation_alias="s", serialization_alias="s", ) """信令类型""" data: Annotated[ diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 60e8e0d93..15bb93a2f 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -34,7 +34,7 @@ from .server import LarkWebhookServer @register_platform_adapter( - "lark", "飞书机器人官方 API 适配器", support_streaming_message=True + "lark", "飞书机器人官方 API 适配器", support_streaming_message=True, ) class LarkPlatformAdapter(Platform): def __init__( @@ -455,6 +455,7 @@ class LarkPlatformAdapter(Platform): Returns: True 表示重复事件,False 表示新事件 + """ self._clean_expired_events() if event_id in self.event_id_timestamps: @@ -490,7 +491,7 @@ class LarkPlatformAdapter(Platform): return PlatformMetadata( name="lark", description="飞书机器人官方 API 适配器", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_streaming_message=True, ) @@ -531,7 +532,7 @@ class LarkPlatformAdapter(Platform): if m.id is None: continue # 飞书 open_id 可能是 None,这里做个防护 - open_id = m.id.open_id if m.id.open_id else "" + open_id = m.id.open_id or "" at_list[m.key] = Comp.At(qq=open_id, name=m.name) if m.name == self.bot_name: @@ -603,6 +604,7 @@ class LarkPlatformAdapter(Platform): Args: event_data: Webhook 事件数据 + """ try: header = event_data.get("header", {}) @@ -654,5 +656,5 @@ class LarkPlatformAdapter(Platform): def unified_webhook(self) -> bool: return bool( self.config.get("lark_connection_mode", "") == "webhook" - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 1c7dd0b43..f10ca7cc6 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -74,6 +74,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 是否发送成功 + """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -89,7 +90,7 @@ class LarkMessageEvent(AstrMessageEvent): .msg_type(msg_type) .uuid(str(uuid.uuid4())) .reply_in_thread(False) - .build() + .build(), ) .build() ) @@ -115,7 +116,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .msg_type(msg_type) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -145,6 +146,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 成功返回file_key,失败返回None + """ if not path or not os.path.exists(path): logger.error(f"[Lark] 文件不存在: {path}") @@ -174,7 +176,7 @@ class LarkMessageEvent(AstrMessageEvent): if not response.success(): logger.error( - f"[Lark] 无法上传文件({response.code}): {response.msg}" + f"[Lark] 无法上传文件({response.code}): {response.msg}", ) return None @@ -207,7 +209,7 @@ class LarkMessageEvent(AstrMessageEvent): file_path = comp.file.replace("file:///", "") elif comp.file and comp.file.startswith("http"): image_file_path = await download_image_by_url(comp.file) - file_path = image_file_path if image_file_path else "" + file_path = image_file_path or "" elif comp.file and comp.file.startswith("base64://"): base64_str = comp.file.removeprefix("base64://") image_data = base64.b64decode(base64_str) @@ -220,7 +222,7 @@ class LarkMessageEvent(AstrMessageEvent): with open(file_path, "wb") as f: f.write(BytesIO(image_data).getvalue()) else: - file_path = comp.file if comp.file else "" + file_path = comp.file or "" if image_file is None: if not file_path: @@ -307,7 +309,7 @@ class LarkMessageEvent(AstrMessageEvent): { "tag": "markdown", "content": reasoning_content, - } + }, ], } @@ -321,8 +323,8 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=title, expanded=False, - ) - ] + ), + ], }, } @@ -341,7 +343,7 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=str(comp.data.get("title", "💭 Thinking")), expanded=bool(comp.data.get("expanded", False)), - ) + ), ) elif isinstance(comp, Plain): if comp.text: @@ -379,9 +381,9 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) - .build() + .build(), ) except Exception as e: logger.error(f"[Lark] 创建卡片失败: {e}") @@ -446,6 +448,7 @@ class LarkMessageEvent(AstrMessageEvent): 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 模块未初始化") @@ -554,17 +557,17 @@ class LarkMessageEvent(AstrMessageEvent): # 发送附件 for file_comp in file_components: await LarkMessageEvent._send_file_message( - file_comp, lark_client, reply_message_id, receive_id, receive_id_type + file_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) for audio_comp in audio_components: await LarkMessageEvent._send_audio_message( - audio_comp, lark_client, reply_message_id, receive_id, receive_id_type + audio_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) for media_comp in media_components: await LarkMessageEvent._send_media_message( - media_comp, lark_client, reply_message_id, receive_id, receive_id_type + media_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) async def send(self, message: MessageChain) -> None: @@ -592,10 +595,11 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ file_path = file_comp.file or "" file_key = await LarkMessageEvent._upload_lark_file( - lark_client, path=file_path, file_type="stream" + lark_client, path=file_path, file_type="stream", ) if not file_key: return @@ -626,6 +630,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取音频文件路径 try: @@ -699,6 +704,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取视频文件路径 try: @@ -803,8 +809,8 @@ class LarkMessageEvent(AstrMessageEvent): "tag": "markdown", "content": "", "element_id": "markdown_1", - } - ] + }, + ], }, } @@ -814,7 +820,7 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) .build() ) @@ -827,7 +833,7 @@ class LarkMessageEvent(AstrMessageEvent): if not response.success(): logger.error( - f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}" + f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}", ) return None @@ -880,7 +886,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -920,7 +926,7 @@ class LarkMessageEvent(AstrMessageEvent): .settings(settings_json) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -1046,7 +1052,7 @@ class LarkMessageEvent(AstrMessageEvent): card_id = await self._create_streaming_card() if not card_id: logger.warning( - "[Lark] 无法创建流式卡片,回退到非流式发送" + "[Lark] 无法创建流式卡片,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1057,7 +1063,7 @@ class LarkMessageEvent(AstrMessageEvent): ) if not sent: logger.error( - "[Lark] 发送流式卡片消息失败,回退到非流式发送" + "[Lark] 发送流式卡片消息失败,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1076,7 +1082,7 @@ class LarkMessageEvent(AstrMessageEvent): if card_id is None: if not fallback_used: await Metric.upload( - msg_event_tick=1, adapter_name=self.platform_meta.name + msg_event_tick=1, adapter_name=self.platform_meta.name, ) self._has_send_oper = True return diff --git a/astrbot/core/platform/sources/lark/server.py b/astrbot/core/platform/sources/lark/server.py index 52177ebb0..a81a92893 100644 --- a/astrbot/core/platform/sources/lark/server.py +++ b/astrbot/core/platform/sources/lark/server.py @@ -58,6 +58,7 @@ class LarkWebhookServer: Args: config: 飞书配置 event_queue: 事件队列 + """ self.app_id = config["app_id"] self.app_secret = config["app_secret"] @@ -91,6 +92,7 @@ class LarkWebhookServer: Returns: 签名是否有效 + """ # 拼接字符串: timestamp + nonce + encrypt_key + body bytes_b1 = (timestamp + nonce + encrypt_key).encode("utf-8") @@ -107,6 +109,7 @@ class LarkWebhookServer: Returns: 解密后的事件字典 + """ if not self.cipher: raise ValueError("未配置 encrypt_key,无法解密事件") @@ -122,6 +125,7 @@ class LarkWebhookServer: Returns: 包含 challenge 的响应 + """ challenge = event_data.get("challenge", "") logger.info(f"[Lark Webhook] 收到 challenge 验证请求: {challenge}") @@ -136,6 +140,7 @@ class LarkWebhookServer: Returns: 响应数据 + """ # 获取原始请求体 body = await request.get_data() @@ -158,7 +163,7 @@ class LarkWebhookServer: if timestamp and nonce and signature: if not self.verify_signature( - timestamp, nonce, self.encrypt_key, body, signature + timestamp, nonce, self.encrypt_key, body, signature, ): logger.error("[Lark Webhook] 签名验证失败") return {"error": "Invalid signature"}, 401 @@ -202,5 +207,6 @@ class LarkWebhookServer: Args: callback: 处理事件的异步函数 + """ self.callback = callback diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index c13677b13..bf9d47da1 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -108,7 +108,7 @@ class LinePlatformAdapter(Platform): return PlatformMetadata( name="line", description="LINE Messaging API 适配器", - id=cast(str, self.config.get("id", "line")), + id=cast("str", self.config.get("id", "line")), support_streaming_message=False, ) @@ -193,7 +193,7 @@ class LinePlatformAdapter(Platform): message.get("id") or event.get("webhookEventId") or event.get("deliveryContext", {}).get("deliveryId", "") - or uuid.uuid4().hex + or uuid.uuid4().hex, ) event_timestamp = event.get("timestamp") diff --git a/astrbot/core/platform/sources/line/line_event.py b/astrbot/core/platform/sources/line/line_event.py index 8b82ad182..1b5766d99 100644 --- a/astrbot/core/platform/sources/line/line_event.py +++ b/astrbot/core/platform/sources/line/line_event.py @@ -220,7 +220,7 @@ class LineMessageEvent(AstrMessageEvent): if len(messages) > 5: logger.warning( - "[LINE] message count exceeds 5, extra segments will be dropped." + "[LINE] message count exceeds 5, extra segments will be dropped.", ) messages = messages[:5] return messages diff --git a/astrbot/core/platform/sources/mattermost/client.py b/astrbot/core/platform/sources/mattermost/client.py index 88619e215..1e3a1744d 100644 --- a/astrbot/core/platform/sources/mattermost/client.py +++ b/astrbot/core/platform/sources/mattermost/client.py @@ -45,7 +45,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost GET {path} failed: {resp.status} {body}" + f"Mattermost GET {path} failed: {resp.status} {body}", ) data = await resp.json() if not isinstance(data, dict): @@ -59,7 +59,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost POST {path} failed: {resp.status} {body}" + f"Mattermost POST {path} failed: {resp.status} {body}", ) data = await resp.json() if not isinstance(data, dict): @@ -82,7 +82,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost download file {file_id} failed: {resp.status} {body}" + f"Mattermost download file {file_id} failed: {resp.status} {body}", ) return await resp.read() @@ -107,7 +107,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost upload file failed: {resp.status} {body}" + f"Mattermost upload file failed: {resp.status} {body}", ) data = await resp.json() file_infos = data.get("file_infos", []) @@ -139,7 +139,7 @@ class MattermostClient: async def ws_connect(self) -> aiohttp.ClientWebSocketResponse: session = await self.ensure_session() ws_url = self.base_url.replace("https://", "wss://", 1).replace( - "http://", "ws://", 1 + "http://", "ws://", 1, ) ws_url = f"{ws_url}/api/v4/websocket" return await session.ws_connect(ws_url, heartbeat=30.0) @@ -173,7 +173,7 @@ class MattermostClient: file_bytes, file_path.name, mimetypes.guess_type(file_path.name)[0] or "image/jpeg", - ) + ), ) elif isinstance(segment, (File, Record, Video)): if isinstance(segment, File): @@ -190,7 +190,7 @@ class MattermostClient: file_bytes, filename, mimetypes.guess_type(filename)[0] or "application/octet-stream", - ) + ), ) else: logger.debug( @@ -218,7 +218,7 @@ class MattermostClient: file_bytes = await self.download_file(file_id) except Exception as exc: logger.warning( - "Mattermost fetch attachment failed %s: %s", file_id, exc + "Mattermost fetch attachment failed %s: %s", file_id, exc, ) continue diff --git a/astrbot/core/platform/sources/mattermost/mattermost_adapter.py b/astrbot/core/platform/sources/mattermost/mattermost_adapter.py index 583e0d0af..85ba2df91 100644 --- a/astrbot/core/platform/sources/mattermost/mattermost_adapter.py +++ b/astrbot/core/platform/sources/mattermost/mattermost_adapter.py @@ -41,7 +41,7 @@ class MattermostPlatformAdapter(Platform): self.base_url = str(platform_config.get("mattermost_url", "")).rstrip("/") self.bot_token = str(platform_config.get("mattermost_bot_token", "")).strip() self.reconnect_delay = float( - platform_config.get("mattermost_reconnect_delay", 5.0) + platform_config.get("mattermost_reconnect_delay", 5.0), ) if not self.base_url: @@ -53,7 +53,7 @@ class MattermostPlatformAdapter(Platform): self.metadata = PlatformMetadata( name="mattermost", description="Mattermost 平台适配器", - id=cast(str, self.config.get("id", "mattermost")), + id=cast("str", self.config.get("id", "mattermost")), support_streaming_message=False, ) self.bot_self_id = "" @@ -112,7 +112,7 @@ class MattermostPlatformAdapter(Platform): "seq": 1, "action": "authentication_challenge", "data": {"token": self.bot_token}, - } + }, ) async for message in ws: @@ -229,7 +229,7 @@ class MattermostPlatformAdapter(Platform): temp_paths, ) = await self.client.parse_post_attachments(file_ids) abm.message.extend(attachment_components) - setattr(abm, "temporary_file_paths", temp_paths) + abm.temporary_file_paths = temp_paths abm.message_str = self._build_message_str( abm.message, diff --git a/astrbot/core/platform/sources/mattermost/mattermost_event.py b/astrbot/core/platform/sources/mattermost/mattermost_event.py index 5faaf7134..1d67c3d92 100644 --- a/astrbot/core/platform/sources/mattermost/mattermost_event.py +++ b/astrbot/core/platform/sources/mattermost/mattermost_event.py @@ -44,10 +44,10 @@ class MattermostMessageEvent(AstrMessageEvent): else: message_buffer.chain.extend(chain.chain) if not message_buffer: - return None + return message_buffer.squash_plain() await self.send(message_buffer) - return None + return text_buffer = "" @@ -67,7 +67,7 @@ class MattermostMessageEvent(AstrMessageEvent): if text_buffer.strip(): await self.send(MessageChain([Plain(text_buffer)])) - return None + return async def get_group(self, group_id=None, **kwargs): channel_id = group_id or self.get_group_id() @@ -83,6 +83,6 @@ class MattermostMessageEvent(AstrMessageEvent): MessageMember( user_id=self.get_sender_id(), nickname=self.get_sender_name(), - ) + ), ], ) diff --git a/astrbot/core/platform/sources/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index 1692c251c..c6ee19fb7 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -45,7 +45,7 @@ DEFAULT_UPLOAD_CONCURRENCY = 3 @register_platform_adapter( - "misskey", "Misskey 平台适配器", support_streaming_message=False + "misskey", "Misskey 平台适配器", support_streaming_message=False, ) class MisskeyPlatformAdapter(Platform): def __init__( @@ -703,7 +703,7 @@ class MisskeyPlatformAdapter(Platform): files = raw_data.get("files", []) process_files(message, files, include_text_parts=False) - message.message_str = raw_text if raw_text else "" + message.message_str = raw_text or "" return message async def convert_room_message(self, raw_data: dict[str, Any]) -> AstrBotMessage: diff --git a/astrbot/core/platform/sources/misskey/misskey_api.py b/astrbot/core/platform/sources/misskey/misskey_api.py index 3e5eb9a90..6b6cb3df2 100644 --- a/astrbot/core/platform/sources/misskey/misskey_api.py +++ b/astrbot/core/platform/sources/misskey/misskey_api.py @@ -550,7 +550,7 @@ class MisskeyAPI: form.add_field("i", self.access_token) try: - filename = name or file_path.split("/")[-1] + filename = name or file_path.rsplit("/", maxsplit=1)[-1] if folder_id: form.add_field("folderId", str(folder_id)) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 97b2b2fb4..9ed31888a 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -32,12 +32,11 @@ def _patch_qq_botpy_formdata() -> None: aiohttp.FormData to have a private flag named _is_processed, which is no longer present in newer aiohttp versions. """ - try: from botpy.http import _FormData # type: ignore if not hasattr(_FormData, "_is_processed"): - setattr(_FormData, "_is_processed", False) + _FormData._is_processed = False except Exception: logger.debug("[QQOfficial] Skip botpy FormData patch.") @@ -123,7 +122,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): current_time = asyncio.get_running_loop().time() if current_time - last_edit_time >= throttle_interval: ret = cast( - message.Message, + "message.Message", await self._post_send(stream=stream_payload), ) stream_payload["index"] += 1 @@ -146,7 +145,6 @@ class QQOfficialMessageEvent(AstrMessageEvent): # 如需兜底,应该只发送未发送 delta(后续可继续优化) self.send_buffer = None - return None @staticmethod def _extract_response_message_id(ret) -> str | None: @@ -407,16 +405,16 @@ class QQOfficialMessageEvent(AstrMessageEvent): markdown_payload = retry_payload.get("markdown") if isinstance(markdown_payload, dict): - md_content = cast(str, markdown_payload.get("content", "") or "") + md_content = cast("str", markdown_payload.get("content", "") or "") if md_content and not md_content.endswith("\n"): retry_payload["markdown"] = {"content": md_content + "\n"} - content = cast(str | None, retry_payload.get("content")) + content = cast("str | None", retry_payload.get("content")) if content and not content.endswith("\n"): retry_payload["content"] = content + "\n" logger.warning( - "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。" + "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。", ) return await send_func(retry_payload) @@ -428,7 +426,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): raise logger.warning( - "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" + "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。", ) fallback_payload = payload.copy() fallback_payload.pop("markdown", None) @@ -436,7 +434,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): if fallback_payload.get("msg_type") == 2: fallback_payload["msg_type"] = 0 if stream: - fallback_content = cast(str, fallback_payload.get("content") or "") + fallback_content = cast("str", fallback_payload.get("content") or "") if fallback_content and not fallback_content.endswith("\n"): fallback_payload["content"] = fallback_content + "\n" return await send_func(fallback_payload) @@ -471,7 +469,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): if not isinstance(result, dict): raise RuntimeError( - f"Failed to upload image, response is not dict: {result}" + f"Failed to upload image, response is not dict: {result}", ) return Media( diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 3037ab2d8..6821d7382 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -71,13 +71,13 @@ class botClient(Client): # 收到群消息 async def on_group_at_message_create( - self, message: botpy.message.GroupMessage + self, message: botpy.message.GroupMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( message, MessageType.GROUP_MESSAGE, ) - abm.group_id = cast(str, message.group_openid) + abm.group_id = cast("str", message.group_openid) abm.session_id = abm.group_id self.platform.remember_session_scene(abm.session_id, "group") self._commit(abm) @@ -95,7 +95,7 @@ class botClient(Client): # 收到私聊消息 async def on_direct_message_create( - self, message: botpy.message.DirectMessage + self, message: botpy.message.DirectMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( message, @@ -378,7 +378,7 @@ class QQOfficialPlatformAdapter(Platform): return PlatformMetadata( name="qq_official", description="QQ 机器人官方 API 适配器", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_proactive_message=True, ) @@ -415,11 +415,11 @@ class QQOfficialPlatformAdapter(Platform): for attachment in attachments: content_type = cast( - str, + "str", getattr(attachment, "content_type", "") or "", ).lower() url = QQOfficialPlatformAdapter._normalize_attachment_url( - cast(str | None, getattr(attachment, "url", None)) + cast("str | None", getattr(attachment, "url", None)), ) if not url: continue @@ -428,7 +428,7 @@ class QQOfficialPlatformAdapter(Platform): msg.append(Image.fromURL(url)) else: filename = cast( - str, + "str", getattr(attachment, "filename", None) or getattr(attachment, "name", None) or "attachment", @@ -457,7 +457,7 @@ class QQOfficialPlatformAdapter(Platform): await QQOfficialPlatformAdapter._prepare_audio_attachment( url, filename, - ) + ), ) except Exception as e: logger.warning( @@ -488,6 +488,7 @@ class QQOfficialPlatformAdapter(Platform): Returns: Content with face tags replaced by readable emoji descriptions. + """ import base64 import json @@ -541,13 +542,13 @@ class QQOfficialPlatformAdapter(Platform): abm.sender = MessageMember(message.author.user_openid, "") # Parse face messages to readable text abm.message_str = QQOfficialPlatformAdapter._parse_face_message( - message.content.strip() + message.content.strip(), ) abm.self_id = "unknown_selfid" msg.append(At(qq="qq_official")) msg.append(Plain(abm.message_str)) await QQOfficialPlatformAdapter._append_attachments( - msg, message.attachments + msg, message.attachments, ) abm.message = msg @@ -564,11 +565,11 @@ class QQOfficialPlatformAdapter(Platform): message.content.replace( "<@!" + str(abm.self_id) + ">", "", - ).strip() + ).strip(), ) await QQOfficialPlatformAdapter._append_attachments( - msg, message.attachments + msg, message.attachments, ) abm.message = msg abm.message_str = plain_content 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 d2e14826a..b117751a5 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -29,13 +29,13 @@ class botClient(Client): # 收到群消息 async def on_group_at_message_create( - self, message: botpy.message.GroupMessage + self, message: botpy.message.GroupMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( message, MessageType.GROUP_MESSAGE, ) - abm.group_id = cast(str, message.group_openid) + abm.group_id = cast("str", message.group_openid) abm.session_id = abm.group_id self.platform.remember_session_scene(abm.session_id, "group") self._commit(abm) @@ -53,7 +53,7 @@ class botClient(Client): # 收到私聊消息 async def on_direct_message_create( - self, message: botpy.message.DirectMessage + self, message: botpy.message.DirectMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( message, @@ -121,7 +121,7 @@ class QQOfficialWebhookPlatformAdapter(Platform): message_chain: MessageChain, ) -> None: await QQOfficialPlatformAdapter._send_by_session_common( - cast(Any, self), + cast("Any", self), session, message_chain, ) @@ -149,7 +149,7 @@ class QQOfficialWebhookPlatformAdapter(Platform): return PlatformMetadata( name="qq_official_webhook", description="QQ 机器人官方 API 适配器", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_proactive_message=True, ) 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..7671c9af6 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -16,7 +16,7 @@ for handler in logging.root.handlers[:]: class QQOfficialWebhook: def __init__( - self, config: dict, event_queue: asyncio.Queue, botpy_client: Client + self, config: dict, event_queue: asyncio.Queue, botpy_client: Client, ) -> None: self.appid = config["appid"] self.secret = config["secret"] @@ -96,6 +96,7 @@ class QQOfficialWebhook: Returns: 响应数据 + """ msg: dict = await request.json logger.debug(f"收到 qq_official_webhook 回调: {msg}") @@ -106,7 +107,7 @@ class QQOfficialWebhook: if opcode == 13: # validation - signed = await self.webhook_validation(cast(dict, data)) + signed = await self.webhook_validation(cast("dict", data)) print(signed) return signed diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index 5c2f7a37f..c1bf6b150 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -29,7 +29,7 @@ from astrbot.core.platform.astr_message_event import MessageSession @register_platform_adapter( - "satori", "Satori 协议适配器", support_streaming_message=False + "satori", "Satori 协议适配器", support_streaming_message=False, ) class SatoriPlatformAdapter(Platform): def __init__( diff --git a/astrbot/core/platform/sources/slack/client.py b/astrbot/core/platform/sources/slack/client.py index efd7a6f3d..5f0b7af22 100644 --- a/astrbot/core/platform/sources/slack/client.py +++ b/astrbot/core/platform/sources/slack/client.py @@ -65,10 +65,11 @@ class SlackWebhookClient: Returns: Response 对象或字典 + """ try: # 获取请求体和头部 - body = cast(bytes, await req.get_data()) + body = cast("bytes", await req.get_data()) event_data = json.loads(body.decode("utf-8")) # Verify Slack request signature @@ -142,7 +143,7 @@ class SlackSocketClient: self.socket_client = None async def _handle_events( - self, _: AsyncBaseSocketModeClient, req: SocketModeRequest + self, _: AsyncBaseSocketModeClient, req: SocketModeRequest, ) -> None: """处理 Socket Mode 事件""" try: diff --git a/astrbot/core/platform/sources/slack/slack_adapter.py b/astrbot/core/platform/sources/slack/slack_adapter.py index 13e317e49..ddf5b5eb6 100644 --- a/astrbot/core/platform/sources/slack/slack_adapter.py +++ b/astrbot/core/platform/sources/slack/slack_adapter.py @@ -66,7 +66,7 @@ class SlackAdapter(Platform): self.metadata = PlatformMetadata( name="slack", description="适用于 Slack 的消息平台适配器,支持 Socket Mode 和 Webhook Mode。", - id=cast(str, self.config.get("id")), + id=cast("str", self.config.get("id")), support_streaming_message=False, ) @@ -98,14 +98,14 @@ class SlackAdapter(Platform): await self.web_client.chat_postMessage( channel=channel_id, text=text, - blocks=blocks if blocks else None, + blocks=blocks or None, ) else: # 发送私信 await self.web_client.chat_postMessage( channel=session.session_id, text=text, - blocks=blocks if blocks else None, + blocks=blocks or None, ) except Exception as e: logger.error(f"Slack 发送消息失败: {e}") @@ -116,13 +116,13 @@ class SlackAdapter(Platform): logger.debug(f"[slack] RawMessage {event}") abm = AstrBotMessage() - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = cast("str", self.bot_self_id) # 获取用户信息 user_id = event.get("user", "") try: user_info = await self.web_client.users_info(user=user_id) - user_data = cast(dict, user_info["user"]) + user_data = cast("dict", user_info["user"]) user_name = user_data.get("real_name") or user_data.get("name", user_id) except Exception: user_name = user_id @@ -133,7 +133,7 @@ class SlackAdapter(Platform): channel_id = event.get("channel", "") try: channel_info = await self.web_client.conversations_info(channel=channel_id) - is_im = cast(dict, channel_info["channel"])["is_im"] + is_im = cast("dict", channel_info["channel"])["is_im"] if is_im: abm.type = MessageType.FRIEND_MESSAGE @@ -174,7 +174,7 @@ class SlackAdapter(Platform): for mention in mentions: try: mentioned_user = await self.web_client.users_info(user=mention) - user_data = cast(dict, mentioned_user["user"]) + user_data = cast("dict", mentioned_user["user"]) user_name = user_data.get("real_name") or user_data.get( "name", mention, @@ -429,5 +429,5 @@ class SlackAdapter(Platform): return bool( self.config.get("unified_webhook_mode", False) and self.config.get("slack_connection_mode", "") == "webhook" - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) diff --git a/astrbot/core/platform/sources/slack/slack_event.py b/astrbot/core/platform/sources/slack/slack_event.py index 3f62690b5..5be6a5ea4 100644 --- a/astrbot/core/platform/sources/slack/slack_event.py +++ b/astrbot/core/platform/sources/slack/slack_event.py @@ -56,7 +56,7 @@ class SlackMessageEvent(AstrMessageEvent): "type": "section", "text": {"type": "mrkdwn", "text": "图片上传失败"}, } - image_url = cast(list, response["files"])[0]["url_private"] + image_url = cast("list", response["files"])[0]["url_private"] logger.debug(f"Slack file upload response: {response}") return { "type": "image", @@ -78,7 +78,7 @@ class SlackMessageEvent(AstrMessageEvent): "type": "section", "text": {"type": "mrkdwn", "text": "文件上传失败"}, } - file_url = cast(list, response["files"])[0]["permalink"] + file_url = cast("list", response["files"])[0]["permalink"] return { "type": "section", "text": { @@ -226,10 +226,10 @@ class SlackMessageEvent(AstrMessageEvent): ) members = [] - for member_id in cast(Iterable, members_response["members"]): + for member_id in cast("Iterable", members_response["members"]): try: user_info = await self.web_client.users_info(user=member_id) - user_data = cast(dict, user_info["user"]) + user_data = cast("dict", user_info["user"]) members.append( MessageMember( user_id=member_id, @@ -241,7 +241,7 @@ class SlackMessageEvent(AstrMessageEvent): # 如果获取用户信息失败,使用默认信息 members.append(MessageMember(user_id=member_id, nickname=member_id)) - channel_data = cast(dict, channel_info["channel"]) + channel_data = cast("dict", channel_info["channel"]) return Group( group_id=channel_id, group_name=channel_data.get("name", ""), diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py index 46887344d..9a2cedca7 100644 --- a/astrbot/core/platform/sources/telegram/tg_adapter.py +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -118,10 +118,10 @@ class TelegramPlatformAdapter(Platform): # Cache structure: {media_group_id: {"created_at": datetime, "items": [(update, context), ...]}} self.media_group_cache: dict[str, dict] = {} self.media_group_timeout = self.config.get( - "telegram_media_group_timeout", 2.5 + "telegram_media_group_timeout", 2.5, ) # seconds - debounce delay between messages self.media_group_max_wait = self.config.get( - "telegram_media_group_max_wait", 10.0 + "telegram_media_group_max_wait", 10.0, ) # max seconds - hard cap to prevent indefinite delay @override @@ -169,7 +169,7 @@ class TelegramPlatformAdapter(Platform): try: logger.info("Starting Telegram polling...") await self.application.updater.start_polling( - error_callback=self._on_polling_error + error_callback=self._on_polling_error, ) logger.info("Telegram Platform Adapter is running.") while self.application.updater.running and not self._terminating: # noqa: ASYNC110 @@ -178,13 +178,13 @@ class TelegramPlatformAdapter(Platform): if not self._terminating: logger.warning( "Telegram polling loop exited unexpectedly, " - f"retrying in {self._polling_restart_delay}s." + f"retrying in {self._polling_restart_delay}s.", ) except asyncio.CancelledError: raise except (Forbidden, InvalidToken) as e: logger.error( - f"Telegram token is invalid or unauthorized: {e}. Polling stopped." + f"Telegram token is invalid or unauthorized: {e}. Polling stopped.", ) break except Exception as e: @@ -246,7 +246,7 @@ class TelegramPlatformAdapter(Platform): if cmd_name in command_dict: logger.warning( f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " - f"'{command_dict[cmd_name]}'" + f"'{command_dict[cmd_name]}'", ) command_dict.setdefault(cmd_name, description) @@ -293,7 +293,7 @@ class TelegramPlatformAdapter(Platform): description = description[:30] + "..." result.append((cmd_name, description)) - return result if result else None + return result or None async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not update.effective_chat: @@ -307,7 +307,7 @@ class TelegramPlatformAdapter(Platform): ) async def message_handler( - self, update: Update, context: ContextTypes.DEFAULT_TYPE + self, update: Update, context: ContextTypes.DEFAULT_TYPE, ) -> None: logger.debug(f"Telegram message: {update.message}") @@ -452,10 +452,10 @@ class TelegramPlatformAdapter(Platform): elif update.message.voice: file = await update.message.voice.get_file() - file_basename = os.path.basename(cast(str, file.file_path)) + file_basename = os.path.basename(cast("str", file.file_path)) temp_dir = get_astrbot_temp_path() temp_path = os.path.join(temp_dir, file_basename) - await download_file(cast(str, file.file_path), path=temp_path) + await download_file(cast("str", file.file_path), path=temp_path) path_wav = os.path.join( temp_dir, f"{file_basename}.wav", @@ -491,7 +491,7 @@ class TelegramPlatformAdapter(Platform): ) else: message.message.append( - Comp.File(file=file_path, name=file_name, url=file_path) + Comp.File(file=file_path, name=file_name, url=file_path), ) _apply_caption() @@ -510,7 +510,7 @@ class TelegramPlatformAdapter(Platform): return message async def handle_media_group_message( - self, update: Update, context: ContextTypes.DEFAULT_TYPE + self, update: Update, context: ContextTypes.DEFAULT_TYPE, ): """Handle messages that are part of a media group (album). @@ -556,7 +556,7 @@ class TelegramPlatformAdapter(Platform): delay = self.media_group_timeout logger.debug( f"Scheduled media group {media_group_id} to be processed in {delay} seconds " - f"(already waited {elapsed:.1f}s)" + f"(already waited {elapsed:.1f}s)", ) # Schedule/reschedule processing (replace_existing=True handles debounce) @@ -575,6 +575,7 @@ class TelegramPlatformAdapter(Platform): Args: media_group_id: The unique identifier for this media group + """ if media_group_id not in self.media_group_cache: logger.warning(f"Media group {media_group_id} not found in cache") @@ -587,7 +588,7 @@ class TelegramPlatformAdapter(Platform): return logger.info( - f"Processing media group {media_group_id}, total {len(updates_and_contexts)} items" + f"Processing media group {media_group_id}, total {len(updates_and_contexts)} items", ) # Use the first update to create the base message (with reply, caption, etc.) @@ -596,7 +597,7 @@ class TelegramPlatformAdapter(Platform): if not abm: logger.warning( - f"Failed to convert the first message of media group {media_group_id}" + f"Failed to convert the first message of media group {media_group_id}", ) return @@ -610,7 +611,7 @@ class TelegramPlatformAdapter(Platform): # Merge only the message components (keep base session/meta from first) abm.message.extend(extra.message) logger.debug( - f"Added {len(extra.message)} components to media group {media_group_id}" + f"Added {len(extra.message)} components to media group {media_group_id}", ) # Process the merged message diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index 8445a8ea1..db4440a86 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -121,13 +121,13 @@ class TelegramPlatformEvent(AstrMessageEvent): await client.send_message( text=markdown_text, parse_mode="MarkdownV2", - **cast(Any, payload), + **cast("Any", payload), ) except (ValueError, BadRequest) as e: logger.warning( - f"Failed to convert message to Markdown,using normal text: {e!s}" + f"Failed to convert message to Markdown,using normal text: {e!s}", ) - await client.send_message(text=chunk, **cast(Any, payload)) + await client.send_message(text=chunk, **cast("Any", payload)) @classmethod async def _send_chat_action( @@ -167,17 +167,17 @@ class TelegramPlatformEvent(AstrMessageEvent): ) -> None: """发送媒体时显示 upload action,发送完成后恢复 typing""" effective_thread_id = message_thread_id or cast( - str | None, payload.get("message_thread_id") + "str | None", payload.get("message_thread_id"), ) await cls._send_chat_action( - client, user_name, upload_action, effective_thread_id + client, user_name, upload_action, effective_thread_id, ) send_payload = dict(payload) if effective_thread_id and "message_thread_id" not in send_payload: send_payload["message_thread_id"] = effective_thread_id await send_coro(**send_payload) await cls._send_chat_action( - client, user_name, ChatAction.TYPING, effective_thread_id + client, user_name, ChatAction.TYPING, effective_thread_id, ) @classmethod @@ -210,10 +210,10 @@ class TelegramPlatformEvent(AstrMessageEvent): client.send_voice, user_name=user_name, voice=path, - **cast(Any, media_payload), + **cast("Any", media_payload), ) else: - await client.send_voice(voice=path, **cast(Any, payload)) + await client.send_voice(voice=path, **cast("Any", payload)) except BadRequest as e: # python-telegram-bot raises BadRequest for Voice_messages_forbidden; # distinguish the voice-privacy case via the API error message. @@ -221,7 +221,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) @@ -234,13 +234,13 @@ class TelegramPlatformEvent(AstrMessageEvent): user_name=user_name, document=path, caption=caption, - **cast(Any, media_payload), + **cast("Any", media_payload), ) else: await client.send_document( document=path, caption=caption, - **cast(Any, payload), + **cast("Any", payload), ) async def _ensure_typing( @@ -250,7 +250,7 @@ class TelegramPlatformEvent(AstrMessageEvent): ) -> None: """确保显示 typing 状态""" await self._send_chat_action( - self.client, user_name, ChatAction.TYPING, message_thread_id + self.client, user_name, ChatAction.TYPING, message_thread_id, ) async def send_typing(self) -> None: @@ -316,12 +316,12 @@ class TelegramPlatformEvent(AstrMessageEvent): else: send_coro = client.send_photo media_kwarg = {"photo": image_path} - await send_coro(**media_kwarg, **cast(Any, payload)) + await send_coro(**media_kwarg, **cast("Any", payload)) elif isinstance(i, File): path = await i.get_file() name = i.name or os.path.basename(path) await client.send_document( - document=path, filename=name, **cast(Any, payload) + document=path, filename=name, **cast("Any", payload), ) elif isinstance(i, Record): path = await i.convert_to_file_path() @@ -337,7 +337,7 @@ class TelegramPlatformEvent(AstrMessageEvent): await client.send_video( video=path, caption=getattr(i, "text", None) or None, - **cast(Any, payload), + **cast("Any", payload), ) async def send(self, message: MessageChain) -> None: @@ -397,6 +397,7 @@ class TelegramPlatformEvent(AstrMessageEvent): text: 消息文本,1-4096 字符 message_thread_id: 可选,目标消息线程 ID parse_mode: 可选,消息文本的解析模式 + """ if not text or not text.strip(): return @@ -409,7 +410,7 @@ class TelegramPlatformEvent(AstrMessageEvent): try: logger.debug( - f"[Telegram] sendMessageDraft: chat_id={chat_id}, draft_id={draft_id}, text_len={len(text)}" + f"[Telegram] sendMessageDraft: chat_id={chat_id}, draft_id={draft_id}, text_len={len(text)}", ) await self.client.send_message_draft( chat_id=int(chat_id), @@ -448,7 +449,7 @@ class TelegramPlatformEvent(AstrMessageEvent): send_coro, user_name=user_name, **media_kwarg, - **cast(Any, payload), + **cast("Any", payload), ) elif isinstance(i, File): path = await i.get_file() @@ -460,7 +461,7 @@ class TelegramPlatformEvent(AstrMessageEvent): user_name=user_name, document=path, filename=name, - **cast(Any, payload), + **cast("Any", payload), ) elif isinstance(i, Record): path = await i.convert_to_file_path() @@ -481,7 +482,7 @@ class TelegramPlatformEvent(AstrMessageEvent): self.client.send_video, user_name=user_name, video=path, - **cast(Any, payload), + **cast("Any", payload), ) else: logger.warning(f"不支持的消息类型: {type(i)}") @@ -513,12 +514,12 @@ class TelegramPlatformEvent(AstrMessageEvent): if is_private: logger.info("[Telegram] 流式输出: 使用 sendMessageDraft (私聊)") await self._send_streaming_draft( - user_name, message_thread_id, payload, generator + user_name, message_thread_id, payload, generator, ) else: logger.info("[Telegram] 流式输出: 使用 edit_message_text fallback (群聊)") await self._send_streaming_edit( - user_name, message_thread_id, payload, generator + user_name, message_thread_id, payload, generator, ) # 内联父类 send_streaming 的副作用(避免传入已消费的 generator) @@ -581,7 +582,7 @@ class TelegramPlatformEvent(AstrMessageEvent): last_sent_text = draft_text except Exception as e2: logger.debug( - f"[Telegram] sendMessageDraft failed (ignored): {e2!s}" + f"[Telegram] sendMessageDraft failed (ignored): {e2!s}", ) sender_task = asyncio.create_task(_draft_sender_loop()) @@ -613,7 +614,7 @@ class TelegramPlatformEvent(AstrMessageEvent): continue await self._process_chain_items( - chain, payload, user_name, message_thread_id, _append_text + chain, payload, user_name, message_thread_id, _append_text, ) finally: done = True @@ -674,7 +675,7 @@ class TelegramPlatformEvent(AstrMessageEvent): continue await self._process_chain_items( - chain, payload, user_name, message_thread_id, _append_text + chain, payload, user_name, message_thread_id, _append_text, ) # 编辑或发送消息 @@ -704,7 +705,7 @@ class TelegramPlatformEvent(AstrMessageEvent): last_chat_action_time = current_time try: msg = await self.client.send_message( - text=delta, **cast(Any, payload) + text=delta, **cast("Any", payload), ) current_content = delta except Exception as e: diff --git a/astrbot/core/platform/sources/webchat/message_parts_helper.py b/astrbot/core/platform/sources/webchat/message_parts_helper.py index 43072ec1c..5a24a704f 100644 --- a/astrbot/core/platform/sources/webchat/message_parts_helper.py +++ b/astrbot/core/platform/sources/webchat/message_parts_helper.py @@ -56,6 +56,7 @@ async def parse_webchat_message_parts( Returns: tuple[list, list[str], bool]: (components, plain_text_parts, has_non_reply_content) + """ components = [] text_parts: list[str] = [] @@ -123,7 +124,7 @@ async def parse_webchat_message_parts( chain=reply_chain, sender_id=sender_id, sender_nickname=sender_name, - ) + ), ) continue @@ -201,7 +202,7 @@ async def build_webchat_message_parts( "type": "reply", "message_id": message_id, "selected_text": str(part.get("selected_text", "")), - } + }, ) continue @@ -229,7 +230,7 @@ async def build_webchat_message_parts( "attachment_id": attachment.attachment_id, "filename": attachment_path.name, "path": str(attachment_path), - } + }, ) return message_parts @@ -268,7 +269,7 @@ def webchat_message_parts_to_message_chain( id=str(message_id), message_str=str(part.get("selected_text", "")), chain=[], - ) + ), ) continue @@ -377,7 +378,7 @@ async def message_chain_to_storage_message_parts( if isinstance(comp, Json): parts.append( - {"type": "plain", "text": json.dumps(comp.data, ensure_ascii=False)} + {"type": "plain", "text": json.dumps(comp.data, ensure_ascii=False)}, ) continue diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index 26b434573..e0ca40a0a 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -89,7 +89,7 @@ class WebChatAdapter(Platform): ) -> None: conversation_id = _extract_conversation_id(session.session_id) active_request_ids = self._webchat_queue_mgr.list_back_request_ids( - conversation_id + conversation_id, ) stream_request_ids = [ req_id for req_id in active_request_ids if not req_id.startswith("ws_sub_") @@ -154,7 +154,7 @@ class WebChatAdapter(Platform): ) async def _get_message_history( - self, message_id: int + self, message_id: int, ) -> PlatformMessageHistory | None: return await db_helper.get_platform_message_history_by_id(message_id) @@ -173,6 +173,7 @@ class WebChatAdapter(Platform): Returns: tuple[list, list[str]]: (消息组件列表, 纯文本列表) + """ async def get_reply_parts( @@ -247,7 +248,7 @@ class WebChatAdapter(Platform): message_event.set_extra("selected_provider", payload.get("selected_provider")) message_event.set_extra("selected_model", payload.get("selected_model")) message_event.set_extra( - "enable_streaming", payload.get("enable_streaming", True) + "enable_streaming", payload.get("enable_streaming", True), ) message_event.set_extra("action_type", payload.get("action_type")) diff --git a/astrbot/core/platform/sources/webchat/webchat_event.py b/astrbot/core/platform/sources/webchat/webchat_event.py index bc1e1a6bc..8d1db88e1 100644 --- a/astrbot/core/platform/sources/webchat/webchat_event.py +++ b/astrbot/core/platform/sources/webchat/webchat_event.py @@ -51,7 +51,7 @@ class WebChatMessageEvent(AstrMessageEvent): "message_id": message_id, }, # end means this request is finished ) - return + return None data = "" for comp in message.chain: @@ -78,7 +78,7 @@ class WebChatMessageEvent(AstrMessageEvent): ) elif isinstance(comp, Image): # save image to local - filename = f"{str(uuid.uuid4())}.jpg" + filename = f"{uuid.uuid4()!s}.jpg" path = os.path.join(attachments_dir, filename) image_base64 = await comp.convert_to_base64() with open(path, "wb") as f: @@ -94,7 +94,7 @@ class WebChatMessageEvent(AstrMessageEvent): ) elif isinstance(comp, Record): # save record to local - filename = f"{str(uuid.uuid4())}.wav" + filename = f"{uuid.uuid4()!s}.wav" path = os.path.join(attachments_dir, filename) record_base64 = await comp.convert_to_base64() with open(path, "wb") as f: diff --git a/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py index f3ade1589..1f7a178fc 100644 --- a/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +++ b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py @@ -34,7 +34,7 @@ class WebChatQueueMgr: """Get or create a back queue for the given request ID""" if request_id not in self.back_queues: self.back_queues[request_id] = asyncio.Queue( - maxsize=self.back_queue_maxsize + maxsize=self.back_queue_maxsize, ) if conversation_id: self._request_conversation[request_id] = conversation_id @@ -57,7 +57,7 @@ class WebChatQueueMgr: def remove_queues(self, conversation_id: str) -> None: """Remove queues for the given conversation ID""" for request_id in list( - self._conversation_back_requests.get(conversation_id, set()) + self._conversation_back_requests.get(conversation_id, set()), ): self.remove_back_queue(request_id) self._conversation_back_requests.pop(conversation_id, None) @@ -121,7 +121,7 @@ class WebChatQueueMgr: ) self._listener_tasks[conversation_id] = task task.add_done_callback( - lambda _: self._listener_tasks.pop(conversation_id, None) + lambda _: self._listener_tasks.pop(conversation_id, None), ) logger.debug(f"Started listener for conversation: {conversation_id}") @@ -150,7 +150,7 @@ class WebChatQueueMgr: await self._listener_callback(data) except Exception as e: logger.error( - f"Error processing message from conversation {conversation_id}: {e}" + f"Error processing message from conversation {conversation_id}: {e}", ) except asyncio.CancelledError: break diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index a30afcd8f..4f09ba273 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -42,7 +42,7 @@ else: class WecomServer: def __init__(self, event_queue: asyncio.Queue, config: dict) -> None: self.server = quart.Quart(__name__) - self.port = int(cast(str, config.get("port"))) + self.port = int(cast("str", config.get("port"))) self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.server.add_url_rule( "/callback/command", @@ -77,6 +77,7 @@ class WecomServer: Returns: 验证响应 + """ logger.info(f"验证请求有效性: {request.args}") args = request.args @@ -105,6 +106,7 @@ class WecomServer: Returns: 响应内容 + """ data = await request.get_data() msg_signature = request.args.get("msg_signature") @@ -116,7 +118,7 @@ class WecomServer: logger.error("解密失败,签名异常,请检查配置。") raise else: - msg = cast(BaseMessage, parse_message(xml)) + msg = cast("BaseMessage", parse_message(xml)) logger.info(f"解析成功: {msg}") if self.callback: @@ -306,8 +308,7 @@ class WecomPlatformAdapter(Platform): # 根据请求方法分发到不同的处理函数 if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None: abm = AstrBotMessage() @@ -317,11 +318,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Plain(msg.content)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg elif isinstance(msg, ImageMessage): @@ -330,11 +331,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Image(file=msg.image, url=msg.image)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg elif isinstance(msg, VoiceMessage): @@ -361,11 +362,11 @@ class WecomPlatformAdapter(Platform): abm.message = [Record(file=path_wav, url=path_wav)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) abm.message_id = str(msg.id) - abm.timestamp = int(cast(int | str, msg.time)) + abm.timestamp = int(cast("int | str", msg.time)) abm.session_id = abm.sender.user_id abm.raw_message = msg else: @@ -378,7 +379,7 @@ class WecomPlatformAdapter(Platform): async def convert_wechat_kf_message(self, msg: dict) -> AstrBotMessage | None: msgtype = msg.get("msgtype") - external_userid = cast(str, msg.get("external_userid")) + external_userid = cast("str", msg.get("external_userid")) abm = AstrBotMessage() abm.raw_message = msg abm.raw_message["_wechat_kf_flag"] = None # 方便处理 diff --git a/astrbot/core/platform/sources/wecom/wecom_event.py b/astrbot/core/platform/sources/wecom/wecom_event.py index 265b41014..1589775ae 100644 --- a/astrbot/core/platform/sources/wecom/wecom_event.py +++ b/astrbot/core/platform/sources/wecom/wecom_event.py @@ -102,10 +102,10 @@ class WecomPlatformEvent(AstrMessageEvent): if getattr(e, "errcode", None) == 40096: # 40096: invalid external userid, fallback to regular message API logger.warning( - f"kf API error 40096 for user {user_id}, falling back to regular message API" + f"kf API error 40096 for user {user_id}, falling back to regular message API", ) self.client.message.send_text( - self.get_self_id(), user_id, chunk + self.get_self_id(), user_id, chunk, ) else: raise @@ -140,7 +140,7 @@ class WecomPlatformEvent(AstrMessageEvent): logger.error(f"微信客服上传语音失败: {e}") await self.send( MessageChain().message( - f"微信客服上传语音失败: {e}" + f"微信客服上传语音失败: {e}", ), ) return @@ -239,7 +239,7 @@ class WecomPlatformEvent(AstrMessageEvent): logger.error(f"企业微信上传语音失败: {e}") await self.send( MessageChain().message( - f"企业微信上传语音失败: {e}" + f"企业微信上传语音失败: {e}", ), ) 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..520ea80fc 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py @@ -215,7 +215,7 @@ class Prpcrypt: @return: 16位字符串 """ return str( - secrets.randbelow(self.RANDOM_RANGE) + self.MIN_RANDOM_VALUE + secrets.randbelow(self.RANDOM_RANGE) + self.MIN_RANDOM_VALUE, ).encode() 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..952d07d2e 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py @@ -80,11 +80,11 @@ class WecomAIBotAdapter(Platform): # 初始化配置参数 self.connection_mode = self.config.get( - "wecom_ai_bot_connection_mode", "webhook" + "wecom_ai_bot_connection_mode", "webhook", ) self.token = self.config.get("token", self.config.get("wecomaibot_token", "")) self.encoding_aes_key = self.config.get( - "encoding_aes_key", self.config.get("wecomaibot_encoding_aes_key", "") + "encoding_aes_key", self.config.get("wecomaibot_encoding_aes_key", ""), ) self.port = int(self.config["port"]) self.host = self.config.get("callback_server_host", "0.0.0.0") @@ -103,10 +103,10 @@ class WecomAIBotAdapter(Platform): self.config.get("only_use_webhook_url_to_send", False), ) self.long_connection_bot_id = self.config.get( - "wecomaibot_ws_bot_id", self.config.get("long_connection_bot_id", "") + "wecomaibot_ws_bot_id", self.config.get("long_connection_bot_id", ""), ) self.long_connection_secret = self.config.get( - "wecomaibot_ws_secret", self.config.get("long_connection_secret", "") + "wecomaibot_ws_secret", self.config.get("long_connection_secret", ""), ) self.long_connection_ws_url = self.config.get( "wecomaibot_ws_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, @@ -238,7 +238,7 @@ class WecomAIBotAdapter(Platform): self._stream_plain_cache.pop(stream_id, None) if self.queue_mgr.is_stream_finished(stream_id): logger.debug( - f"Stream already finished, returning end message: {stream_id}" + f"Stream already finished, returning end message: {stream_id}", ) else: logger.warning(f"Cannot find back queue for stream_id: {stream_id}") @@ -372,7 +372,7 @@ class WecomAIBotAdapter(Platform): session_id = self._extract_session_id(body) stream_id = f"{session_id}_{generate_random_string(10)}" await self._enqueue_message( - body, {"req_id": req_id or ""}, stream_id, session_id + body, {"req_id": req_id or ""}, stream_id, session_id, ) self.queue_mgr.set_pending_response( stream_id, @@ -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: @@ -447,9 +447,8 @@ class WecomAIBotAdapter(Platform): if chattype == "group": chat_id = message_data.get("chatid", "default_group") return format_session_id("wecomai", chat_id) - else: - user_id = message_data.get("from", {}).get("userid", "default_user") - return format_session_id("wecomai", user_id) + user_id = message_data.get("from", {}).get("userid", "default_user") + return format_session_id("wecomai", user_id) async def _enqueue_message( self, @@ -505,7 +504,7 @@ class WecomAIBotAdapter(Platform): image_url = image_payload.get("url", "") if image_url: _img_url_to_process.append( - (image_url, image_payload.get("aeskey")) + (image_url, image_payload.get("aeskey")), ) content = " ".join(text_parts) if text_parts else "" else: @@ -593,7 +592,7 @@ class WecomAIBotAdapter(Platform): if not self.long_connection_client: raise RuntimeError("长连接客户端未初始化") logger.info( - "启动企业微信智能机器人长连接模式: %s", self.long_connection_ws_url + "启动企业微信智能机器人长连接模式: %s", self.long_connection_ws_url, ) await asyncio.gather( self.long_connection_client.start(), @@ -604,7 +603,7 @@ class WecomAIBotAdapter(Platform): webhook_uuid = self.config.get("webhook_uuid") if self.unified_webhook_mode and webhook_uuid: log_webhook_info( - f"{self.meta().id}(企业微信智能机器人)", webhook_uuid + f"{self.meta().id}(企业微信智能机器人)", webhook_uuid, ) # 只运行队列监听器 await self.queue_listener.run() @@ -612,7 +611,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( @@ -629,8 +628,7 @@ class WecomAIBotAdapter(Platform): # 根据请求方法分发到不同的处理函数 if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def terminate(self) -> None: """终止适配器""" 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..929c06cae 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py @@ -142,7 +142,7 @@ class WecomAIBotAPIClient: return "verify fail" logger.info("URL 验证成功") - return echo_result if echo_result else "verify fail" + return echo_result or "verify fail" except Exception as e: logger.error(f"URL 验证发生异常: {e}") 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..f1b700a48 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py @@ -116,11 +116,10 @@ class WecomAIBotMessageEvent(AstrMessageEvent): logger.warning("图片数据为空,跳过") except Exception as e: logger.error("处理图片消息失败: %s", e) - else: - if not suppress_unsupported_log: - logger.warning( - f"[WecomAI] 不支持的消息组件类型: {type(comp)}, 跳过" - ) + elif not suppress_unsupported_log: + logger.warning( + f"[WecomAI] 不支持的消息组件类型: {type(comp)}, 跳过", + ) return data @@ -147,7 +146,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): stream_id = raw.get("stream_id", self.session_id) pending_response = self.queue_mgr.get_pending_response(stream_id) or {} connection_mode = pending_response.get("callback_params", {}).get( - "connection_mode" + "connection_mode", ) req_id = pending_response.get("callback_params", {}).get("req_id") @@ -213,7 +212,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): stream_id = raw.get("stream_id", self.session_id) pending_response = self.queue_mgr.get_pending_response(stream_id) or {} connection_mode = pending_response.get("callback_params", {}).get( - "connection_mode" + "connection_mode", ) req_id = pending_response.get("callback_params", {}).get("req_id") back_queue = self.queue_mgr.get_or_create_back_queue(stream_id) @@ -315,7 +314,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): async for chain in generator: if self.webhook_client: await self.webhook_client.send_message_chain( - chain, unsupported_only=True + chain, unsupported_only=True, ) if chain.type == "break" and final_data: 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..82663aca0 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 @@ -61,7 +61,7 @@ class WecomAIBotLongConnectionClient: self._session = session logger.info("[WecomAI][LongConn] 正在连接: %s", self.ws_url) async with session.ws_connect( - self.ws_url, heartbeat=None, autoping=True + self.ws_url, heartbeat=None, autoping=True, ) as ws: self._ws = ws await self._subscribe() @@ -107,7 +107,7 @@ class WecomAIBotLongConnectionClient: data = json.loads(reply.data) if data.get("errcode") != 0: raise RuntimeError( - f"订阅失败 errcode={data.get('errcode')} errmsg={data.get('errmsg')}" + f"订阅失败 errcode={data.get('errcode')} errmsg={data.get('errmsg')}", ) async def _heartbeat_loop(self) -> None: 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..aa701609d 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 @@ -60,7 +60,7 @@ class WecomAIQueueMgr: """ if session_id not in self.back_queues: self.back_queues[session_id] = asyncio.Queue( - maxsize=self.back_queue_maxsize + maxsize=self.back_queue_maxsize, ) logger.debug(f"[WecomAI] 创建输出队列: {session_id}") return self.back_queues[session_id] @@ -125,7 +125,7 @@ class WecomAIQueueMgr: return session_id in self.back_queues def set_pending_response( - self, session_id: str, callback_params: dict[str, str] + self, session_id: str, callback_params: dict[str, str], ) -> None: """设置待处理的响应参数 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..ce4770996 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py @@ -70,6 +70,7 @@ class WecomAIBotServer: Returns: 验证响应元组 (content, status_code, headers) + """ args = request.args msg_signature = args.get("msg_signature") @@ -103,6 +104,7 @@ class WecomAIBotServer: Returns: 响应元组 (content, status_code, headers) + """ args = request.args msg_signature = args.get("msg_signature") 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 6f42f264b..797b22a43 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py @@ -69,12 +69,12 @@ class WecomAIBotWebhookClient: text = await response.text() if response.status != 200: raise WecomAIBotWebhookError( - f"Webhook 请求失败: HTTP {response.status}, {text}" + f"Webhook 请求失败: HTTP {response.status}, {text}", ) result = await response.json(content_type=None) if result.get("errcode") != 0: raise WecomAIBotWebhookError( - f"Webhook 返回错误: {result.get('errcode')} {result.get('errmsg')}" + f"Webhook 返回错误: {result.get('errcode')} {result.get('errmsg')}", ) logger.debug("企业微信消息推送成功: %s", payload.get("msgtype", "unknown")) @@ -84,7 +84,7 @@ class WecomAIBotWebhookClient: { "msgtype": "markdown_v2", "markdown_v2": {"content": chunk}, - } + }, ) async def send_image_base64(self, image_base64: str) -> None: @@ -97,11 +97,11 @@ class WecomAIBotWebhookClient: "base64": image_base64, "md5": md5, }, - } + }, ) async def upload_media( - self, file_path: Path, media_type: Literal["file", "voice"] + self, file_path: Path, media_type: Literal["file", "voice"], ) -> str: if not file_path.exists() or not file_path.is_file(): raise WecomAIBotWebhookError(f"文件不存在: {file_path}") @@ -126,12 +126,12 @@ class WecomAIBotWebhookClient: text = await response.text() if response.status != 200: raise WecomAIBotWebhookError( - f"上传媒体失败: HTTP {response.status}, {text}" + f"上传媒体失败: HTTP {response.status}, {text}", ) result = await response.json(content_type=None) if result.get("errcode") != 0: raise WecomAIBotWebhookError( - f"上传媒体失败: {result.get('errcode')} {result.get('errmsg')}" + f"上传媒体失败: {result.get('errcode')} {result.get('errmsg')}", ) media_id = result.get("media_id", "") if not media_id: @@ -144,7 +144,7 @@ class WecomAIBotWebhookClient: { "msgtype": "file", "file": {"media_id": media_id}, - } + }, ) async def send_voice(self, file_path: Path) -> None: @@ -153,7 +153,7 @@ class WecomAIBotWebhookClient: { "msgtype": "voice", "voice": {"media_id": media_id}, - } + }, ) @staticmethod @@ -214,7 +214,7 @@ class WecomAIBotWebhookClient: target_voice_path.unlink() except Exception as e: logger.warning( - "清理临时语音文件失败 %s: %s", target_voice_path, e + "清理临时语音文件失败 %s: %s", target_voice_path, e, ) else: logger.warning( diff --git a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py index b317cb5b8..9ad469d88 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py @@ -8,7 +8,7 @@ import time import uuid from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from urllib.parse import quote import qrcode as qrcode_lib @@ -31,9 +31,6 @@ from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from .weixin_oc_client import WeixinOCClient from .weixin_oc_event import WeixinOCMessageEvent -if TYPE_CHECKING: # pragma: no cover - typing-only helper - pass - @dataclass class OpenClawLoginSession: @@ -84,7 +81,7 @@ class WeixinOCAdapter(Platform): self.settings = platform_settings self.base_url = str( - platform_config.get("weixin_oc_base_url", "https://ilinkai.weixin.qq.com") + platform_config.get("weixin_oc_base_url", "https://ilinkai.weixin.qq.com"), ).rstrip("/") self.bot_type = str(platform_config.get("weixin_oc_bot_type", "3")) self.qr_poll_interval = max( @@ -101,13 +98,13 @@ class WeixinOCAdapter(Platform): platform_config.get( "weixin_oc_cdn_base_url", "https://novac2c.cdn.weixin.qq.com/c2c", - ) + ), ).rstrip("/") self.metadata = PlatformMetadata( name="weixin_oc", description="个人微信", - id=cast(str, self.config.get("id", "weixin_oc")), + id=cast("str", self.config.get("id", "weixin_oc")), support_streaming_message=False, ) @@ -229,7 +226,7 @@ class WeixinOCAdapter(Platform): payload = await self.client.send_typing_state(user_id, ticket, cancel=cancel) if int(payload.get("ret") or 0) != 0: raise RuntimeError( - f"sendtyping failed for {user_id}: {payload.get('errmsg', '')}" + f"sendtyping failed for {user_id}: {payload.get('errmsg', '')}", ) async def _run_typing_keepalive(self, user_id: str) -> None: @@ -270,7 +267,7 @@ class WeixinOCAdapter(Platform): if not state.owners or state.keepalive_task is not None: return state.keepalive_task = asyncio.create_task( - self._run_typing_keepalive(user_id) + self._run_typing_keepalive(user_id), ) async def _typing_keepalive_loop(self, user_id: str) -> None: @@ -421,7 +418,7 @@ class WeixinOCAdapter(Platform): if ticket: if state.cancel_task is None or state.cancel_task.done(): state.cancel_task = asyncio.create_task( - self._delayed_cancel_typing(user_id, ticket) + self._delayed_cancel_typing(user_id, ticket), ) async def _cleanup_typing_tasks(self) -> None: @@ -501,7 +498,7 @@ class WeixinOCAdapter(Platform): astrbot_config.save_config() def _is_login_session_valid( - self, login_session: OpenClawLoginSession | None + self, login_session: OpenClawLoginSession | None, ) -> bool: if not login_session: return False @@ -654,15 +651,15 @@ class WeixinOCAdapter(Platform): item_type = int(item.get("type") or 0) if item_type == self.IMAGE_ITEM_TYPE: - image_item = cast(dict[str, Any], item.get("image_item", {}) or {}) - media = cast(dict[str, Any], image_item.get("media", {}) or {}) + image_item = cast("dict[str, Any]", item.get("image_item", {}) or {}) + media = cast("dict[str, Any]", image_item.get("media", {}) or {}) encrypted_query_param = str(media.get("encrypt_query_param", "")).strip() if not encrypted_query_param: return None image_aes_key = str(image_item.get("aeskey", "")).strip() if image_aes_key: aes_key_value = base64.b64encode(bytes.fromhex(image_aes_key)).decode( - "utf-8" + "utf-8", ) else: aes_key_value = str(media.get("aes_key", "")).strip() @@ -682,8 +679,8 @@ class WeixinOCAdapter(Platform): return Image.fromFileSystem(str(image_path)) if item_type == self.VIDEO_ITEM_TYPE: - video_item = cast(dict[str, Any], item.get("video_item", {}) or {}) - media = cast(dict[str, Any], video_item.get("media", {}) or {}) + video_item = cast("dict[str, Any]", item.get("video_item", {}) or {}) + media = cast("dict[str, Any]", video_item.get("media", {}) or {}) encrypted_query_param = str(media.get("encrypt_query_param", "")).strip() aes_key_value = str(media.get("aes_key", "")).strip() if not encrypted_query_param or not aes_key_value: @@ -701,8 +698,8 @@ class WeixinOCAdapter(Platform): return Video.fromFileSystem(str(video_path)) if item_type == self.FILE_ITEM_TYPE: - file_item = cast(dict[str, Any], item.get("file_item", {}) or {}) - media = cast(dict[str, Any], file_item.get("media", {}) or {}) + file_item = cast("dict[str, Any]", item.get("file_item", {}) or {}) + media = cast("dict[str, Any]", file_item.get("media", {}) or {}) encrypted_query_param = str(media.get("encrypt_query_param", "")).strip() aes_key_value = str(media.get("aes_key", "")).strip() if not encrypted_query_param or not aes_key_value: @@ -724,8 +721,8 @@ class WeixinOCAdapter(Platform): return File(name=file_name, file=str(file_path)) if item_type == self.VOICE_ITEM_TYPE: - voice_item = cast(dict[str, Any], item.get("voice_item", {}) or {}) - media = cast(dict[str, Any], voice_item.get("media", {}) or {}) + voice_item = cast("dict[str, Any]", item.get("voice_item", {}) or {}) + media = cast("dict[str, Any]", voice_item.get("media", {}) or {}) encrypted_query_param = str(media.get("encrypt_query_param", "")).strip() aes_key_value = str(media.get("aes_key", "")).strip() if not encrypted_query_param or not aes_key_value: @@ -745,7 +742,7 @@ class WeixinOCAdapter(Platform): return None async def _resolve_media_file_path( - self, segment: Image | Video | File + self, segment: Image | Video | File, ) -> Path | None: try: if isinstance(segment, File): @@ -817,7 +814,7 @@ class WeixinOCAdapter(Platform): ) -> bool: if not self.token: logger.warning( - "weixin_oc(%s): missing token, skip media send", self.meta().id + "weixin_oc(%s): missing token, skip media send", self.meta().id, ) return False media_path = await self._resolve_media_file_path(segment) @@ -961,7 +958,7 @@ class WeixinOCAdapter(Platform): await self._save_account_state() def _message_text_from_item_list( - self, item_list: list[dict[str, Any]] | None + self, item_list: list[dict[str, Any]] | None, ) -> str: if not item_list: return "" @@ -995,7 +992,7 @@ class WeixinOCAdapter(Platform): return "\n".join(text_parts).strip() async def _item_list_to_components( - self, item_list: list[dict[str, Any]] | None + self, item_list: list[dict[str, Any]] | None, ) -> list[Any]: if not item_list: return [] @@ -1030,7 +1027,7 @@ class WeixinOCAdapter(Platform): if context_token: self._context_tokens[from_user_id] = context_token - item_list = cast(list[dict[str, Any]], msg.get("item_list", [])) + item_list = cast("list[dict[str, Any]]", msg.get("item_list", [])) components = await self._item_list_to_components(item_list) text = self._message_text_from_item_list(item_list) message_id = str(msg.get("message_id") or msg.get("msg_id") or uuid.uuid4().hex) @@ -1060,7 +1057,7 @@ class WeixinOCAdapter(Platform): platform_meta=self.meta(), session_id=abm.session_id, platform=self, - ) + ), ) async def _poll_inbound_updates(self) -> None: @@ -1116,7 +1113,7 @@ class WeixinOCAdapter(Platform): return text.strip() async def _send_to_session( - self, user_id: str, text: str, _components: list[Any] | None = None + self, user_id: str, text: str, _components: list[Any] | None = None, ) -> bool: if not text: text = self._message_chain_to_text(MessageChain(_components or [])) diff --git a/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py b/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py index 1ea44df52..cdfb40bb3 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py @@ -46,7 +46,7 @@ class WeixinOCClient: "Content-Type": "application/json", "AuthorizationType": "ilink_bot_token", "X-WECHAT-UIN": base64.b64encode( - str(random.getrandbits(32)).encode("utf-8") + str(random.getrandbits(32)).encode("utf-8"), ).decode("utf-8"), } if token_required and self.token: @@ -120,7 +120,7 @@ class WeixinOCClient: cdn_url = self._build_cdn_upload_url(upload_param, file_key) else: raise ValueError( - "CDN upload URL missing (need upload_full_url or upload_param)" + "CDN upload URL missing (need upload_full_url or upload_param)", ) raw_data = media_path.read_bytes() @@ -164,16 +164,16 @@ class WeixinOCClient: ) if resp.status >= 400 and resp.status < 500: raise RuntimeError( - f"upload media to cdn failed: {resp.status} {detail}" + f"upload media to cdn failed: {resp.status} {detail}", ) if resp.status != 200: raise RuntimeError( - f"upload media to cdn failed: {resp.status} {detail}" + f"upload media to cdn failed: {resp.status} {detail}", ) download_param = resp.headers.get("x-encrypted-param") if not download_param: raise RuntimeError( - "upload media to cdn failed: missing x-encrypted-param" + "upload media to cdn failed: missing x-encrypted-param", ) return download_param @@ -188,7 +188,7 @@ class WeixinOCClient: if resp.status >= 400: detail = await resp.text() raise RuntimeError( - f"download media from cdn failed: {resp.status} {detail}" + f"download media from cdn failed: {resp.status} {detail}", ) return await resp.read() @@ -234,7 +234,7 @@ class WeixinOCClient: raise RuntimeError(f"{method} {endpoint} failed: {resp.status} {text}") if not text: return {} - return cast(dict[str, Any], json.loads(text)) + return cast("dict[str, Any]", json.loads(text)) async def get_typing_config( self, 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 8b646e43f..6be122e6c 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 @@ -45,7 +45,7 @@ class WeixinOfficialAccountServer: user_buffer: dict[Any, dict[str, Any]], ) -> None: self.server = quart.Quart(__name__) - self.port = int(cast(int | str, config.get("port"))) + self.port = int(cast("int | str", config.get("port"))) self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.token = config.get("token") self.encoding_aes_key = config.get("encoding_aes_key") @@ -85,6 +85,7 @@ class WeixinOfficialAccountServer: Returns: 验证响应 + """ logger.info(f"验证请求有效性: {request.args}") @@ -117,7 +118,7 @@ class WeixinOfficialAccountServer: def _preview(self, msg: BaseMessage, limit: int = 24) -> str: """生成消息预览文本,供占位符使用""" if isinstance(msg, TextMessage): - t = cast(str, msg.content).strip() + t = cast("str", msg.content).strip() return (t[:limit] + "...") if len(t) > limit else (t or "空消息") if isinstance(msg, ImageMessage): return "图片" @@ -133,6 +134,7 @@ class WeixinOfficialAccountServer: Returns: 响应内容 + """ data = await request.get_data() msg_signature = request.args.get("msg_signature") @@ -163,7 +165,7 @@ class WeixinOfficialAccountServer: # passive reply from_user = str(getattr(msg, "source", "")) - msg_id = str(cast(str | int, getattr(msg, "id", ""))) + msg_id = str(cast("str | int", getattr(msg, "id", ""))) state = self.user_buffer.get(from_user) def _reply_text(text: str) -> str: @@ -182,13 +184,12 @@ class WeixinOfficialAccountServer: if len(cached) == 0: self.user_buffer.pop(from_user, None) return _reply_text(cached_xml) - else: - return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + return _reply_text( + cached_xml + + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) - task: asyncio.Task | None = cast(asyncio.Task | None, state.get("task")) + 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,回复任意文字尝试获取回复】" @@ -207,36 +208,35 @@ class WeixinOfficialAccountServer: # send one cached each time, if cached is empty after pop, remove the buffer if cached and len(cached) > 0: logger.info( - f"wx buffer hit on retry window: user={from_user}" + f"wx buffer hit on retry window: user={from_user}", ) cached_xml = cached.pop(0) if len(cached) == 0: self.user_buffer.pop(from_user, None) logger.debug( - f"wx finished message sending in passive window: user={from_user} msg_id={msg_id} " + f"wx finished message sending in passive window: user={from_user} msg_id={msg_id} ", ) return _reply_text(cached_xml) - else: - logger.debug( - f"wx finished message sending in passive window but not final: user={from_user} msg_id={msg_id} " - ) - return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + logger.debug( + f"wx finished message sending in passive window but not final: user={from_user} msg_id={msg_id} ", + ) + return _reply_text( + cached_xml + + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) logger.info( - f"wx finished in window but not final; return placeholder: user={from_user} msg_id={msg_id} " + f"wx finished in window but not final; return placeholder: user={from_user} msg_id={msg_id} ", ) return _reply_text(placeholder) except Exception: logger.critical( - "wx task failed in passive window", exc_info=True + "wx task failed in passive window", exc_info=True, ) self.user_buffer.pop(from_user, None) return _reply_text("处理消息失败,请稍后再试。") logger.info( - f"wx passive window timeout: user={from_user} msg_id={msg_id}" + f"wx passive window timeout: user={from_user} msg_id={msg_id}", ) return _reply_text(placeholder) @@ -250,7 +250,7 @@ class WeixinOfficialAccountServer: f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" ) logger.info( - f"wx start task: user={from_user} msg_id={msg_id} preview={preview}" + f"wx start task: user={from_user} msg_id={msg_id} preview={preview}", ) self.user_buffer[from_user] = state = { @@ -261,7 +261,7 @@ class WeixinOfficialAccountServer: "started_at": time.monotonic(), } self.user_buffer[from_user]["task"] = task = asyncio.create_task( - self.callback(msg) + self.callback(msg), ) # immediate return if done @@ -272,7 +272,7 @@ class WeixinOfficialAccountServer: ) if done: try: - cached = state.get("cached_xml", None) + cached = state.get("cached_xml") # send one cached each time, if cached is empty after pop, remove the buffer if cached and len(cached) > 0: logger.info(f"wx buffer hit immediately: user={from_user}") @@ -280,13 +280,12 @@ class WeixinOfficialAccountServer: if len(cached) == 0: self.user_buffer.pop(from_user, None) return _reply_text(cached_xml) - else: - return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + return _reply_text( + cached_xml + + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) logger.info( - f"wx not finished in first window; return placeholder: user={from_user} msg_id={msg_id} " + f"wx not finished in first window; return placeholder: user={from_user} msg_id={msg_id} ", ) return _reply_text(placeholder) except Exception: @@ -312,7 +311,7 @@ class WeixinOfficialAccountServer: @register_platform_adapter( - "weixin_official_account", "微信公众平台 适配器", support_streaming_message=False + "weixin_official_account", "微信公众平台 适配器", support_streaming_message=False, ) class WeixinOfficialAccountPlatformAdapter(Platform): def __init__( @@ -342,7 +341,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.user_buffer: dict[str, dict[str, Any]] = {} # from_user -> state self.server = WeixinOfficialAccountServer( - self._event_queue, self.config, self.user_buffer + self._event_queue, self.config, self.user_buffer, ) self.client = WeChatClient( @@ -362,7 +361,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): await self.convert_message(msg, None) return None - msg_id = str(cast(str | int, msg.id)) + msg_id = str(cast("str | int", msg.id)) future = self.wexin_event_workers.get(msg_id) if future: logger.debug(f"duplicate message id checked: {msg.id}") @@ -383,7 +382,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): except Exception as e: logger.error(f"转换消息时出现异常: {e}") finally: - self.wexin_event_workers.pop(str(cast(str | int, msg.id)), None) + self.wexin_event_workers.pop(str(cast("str | int", msg.id)), None) self.server.callback = callback self.server.active_send_mode = self.active_send_mode @@ -422,8 +421,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): # 根据请求方法分发到不同的处理函数 if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def convert_message( self, @@ -432,29 +430,29 @@ class WeixinOfficialAccountPlatformAdapter(Platform): ) -> AstrBotMessage | None: abm = AstrBotMessage() if isinstance(msg, TextMessage): - abm.message_str = cast(str, msg.content) + abm.message_str = cast("str", msg.content) abm.self_id = str(msg.target) - abm.message = [Plain(cast(str, msg.content))] + abm.message = [Plain(cast("str", msg.content))] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) - abm.message_id = str(cast(str | int, msg.id)) - abm.timestamp = cast(int, msg.time) + abm.message_id = str(cast("str | int", msg.id)) + abm.timestamp = cast("int", msg.time) abm.session_id = abm.sender.user_id elif msg.type == "image": assert isinstance(msg, ImageMessage) abm.message_str = "[图片]" abm.self_id = str(msg.target) - abm.message = [Image(file=cast(str, msg.image), url=cast(str, msg.image))] + abm.message = [Image(file=cast("str", msg.image), url=cast("str", msg.image))] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) - abm.message_id = str(cast(str | int, msg.id)) - abm.timestamp = cast(int, msg.time) + abm.message_id = str(cast("str | int", msg.id)) + abm.timestamp = cast("int", msg.time) abm.session_id = abm.sender.user_id elif msg.type == "voice": assert isinstance(msg, VoiceMessage) @@ -487,11 +485,11 @@ class WeixinOfficialAccountPlatformAdapter(Platform): abm.message = [Record(file=path_wav, url=path_wav)] abm.type = MessageType.FRIEND_MESSAGE abm.sender = MessageMember( - cast(str, msg.source), - cast(str, msg.source), + cast("str", msg.source), + cast("str", msg.source), ) - abm.message_id = str(cast(str | int, msg.id)) - abm.timestamp = cast(int, msg.time) + abm.message_id = str(cast("str | int", msg.id)) + abm.timestamp = cast("int", msg.time) abm.session_id = abm.sender.user_id else: logger.warning(f"暂未实现的事件: {msg.type}") @@ -511,7 +509,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 ae536593c..f0053933e 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 @@ -82,8 +82,8 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): async def send(self, message: MessageChain) -> None: message_obj = self.message_obj - active_send_mode = cast(dict, message_obj.raw_message).get( - "active_send_mode", False + active_send_mode = cast("dict", message_obj.raw_message).get( + "active_send_mode", False, ) for comp in message.chain: if isinstance(comp, Plain): @@ -95,7 +95,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): else: # disable passive sending, just store the chunks in logger.debug( - f"split plain into {len(plain_chunks)} chunks for passive reply. Message not sent." + f"split plain into {len(plain_chunks)} chunks for passive reply. Message not sent.", ) self.message_out["cached_xml"] = plain_chunks elif isinstance(comp, Image): @@ -120,10 +120,10 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): else: reply = ImageReply( media_id=response["media_id"], - message=cast(dict, self.message_obj.raw_message)["message"], + message=cast("dict", self.message_obj.raw_message)["message"], ) xml = reply.render() - future = cast(dict, self.message_obj.raw_message)["future"] + future = cast("dict", self.message_obj.raw_message)["future"] assert isinstance(future, asyncio.Future) future.set_result(xml) @@ -139,7 +139,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): logger.error(f"微信公众平台上传语音失败: {e}") await self.send( MessageChain().message( - f"微信公众平台上传语音失败: {e}" + f"微信公众平台上传语音失败: {e}", ), ) return @@ -153,17 +153,17 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): else: reply = VoiceReply( media_id=response["media_id"], - message=cast(dict, self.message_obj.raw_message)[ + message=cast("dict", self.message_obj.raw_message)[ "message" ], ) xml = reply.render() - future = cast(dict, self.message_obj.raw_message)["future"] + future = cast("dict", self.message_obj.raw_message)["future"] assert isinstance(future, asyncio.Future) future.set_result(xml) finally: if record_path_amr != record_path and os.path.exists( - record_path_amr + record_path_amr, ): try: os.remove(record_path_amr) diff --git a/astrbot/core/platform_message_history_mgr.py b/astrbot/core/platform_message_history_mgr.py index ad8bb44f6..37a6feb3d 100644 --- a/astrbot/core/platform_message_history_mgr.py +++ b/astrbot/core/platform_message_history_mgr.py @@ -41,7 +41,7 @@ class PlatformMessageHistoryManager: return history async def delete( - self, platform_id: str, user_id: str, offset_sec: int = 86400 + self, platform_id: str, user_id: str, offset_sec: int = 86400, ) -> None: """Delete platform message history records older than the specified offset.""" await self.db.delete_platform_message_offset( diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index b27775cdd..42b8743c2 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -479,7 +479,7 @@ class LLMResponse: ), # the extra_content will not serialize if it's None when calling ToolCall.model_dump() extra_content=self.tools_call_extra_content.get( - self.tools_call_ids[idx] + self.tools_call_ids[idx], ), ), ) diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index bf16a3ec9..67f4c6987 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -88,7 +88,7 @@ class _MCPClientDictView(Mapping[str, MCPClient]): def _resolve_timeout( - timeout: float | int | str | None = None, + timeout: float | str | None = None, *, env_name: str = MCP_INIT_TIMEOUT_ENV, default: float = DEFAULT_MCP_INIT_TIMEOUT_SECONDS, @@ -104,20 +104,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"{MAX_MCP_TIMEOUT_SECONDS:g} 秒,以避免长时间等待。", ) return MAX_MCP_TIMEOUT_SECONDS @@ -415,7 +415,7 @@ class FunctionToolManager: logger.debug(f" 主机: {scheme}://{host}{port}") async def init_mcp_clients( - self, raise_on_all_failed: bool = False + self, raise_on_all_failed: bool = False, ) -> MCPInitSummary: """从项目根目录读取 mcp_server.json 文件,初始化 MCP 服务列表。文件格式如下: ``` @@ -487,7 +487,7 @@ class FunctionToolManager: if isinstance(result, Exception): if isinstance(result, MCPInitTimeoutError): logger.error( - f"Connected to MCP server {name} timeout ({timeout_display} seconds)" + f"Connected to MCP server {name} timeout ({timeout_display} seconds)", ) else: logger.error(f"Failed to initialize MCP server {name}: {result}") @@ -502,14 +502,14 @@ class FunctionToolManager: if failed_services: logger.warning( f"The following MCP services failed to initialize: {', '.join(failed_services)}. " - f"Please check the mcp_server.json file and server availability." + f"Please check the mcp_server.json file and server availability.", ) summary = MCPInitSummary( - total=len(active_configs), success=success_count, failed=failed_services + total=len(active_configs), success=success_count, failed=failed_services, ) logger.info( - f"MCP services initialization completed: {summary.success}/{summary.total} successful, {len(summary.failed)} failed." + f"MCP services initialization completed: {summary.success}/{summary.total} successful, {len(summary.failed)} failed.", ) if summary.total > 0 and summary.success == 0: msg = "All MCP services failed to initialize, please check the mcp_server.json and server availability." @@ -534,7 +534,7 @@ class FunctionToolManager: async with self._runtime_lock: if name in self._mcp_server_runtime or name in self._mcp_starting: logger.warning( - f"Connected to MCP server {name}, ignoring this startup request (timeout={timeout:g})." + f"Connected to MCP server {name}, ignoring this startup request (timeout={timeout:g}).", ) self._log_safe_mcp_debug_config(cfg) return @@ -551,7 +551,7 @@ class FunctionToolManager: ) except asyncio.TimeoutError as exc: raise MCPInitTimeoutError( - f"Connected to MCP server {name} timeout ({timeout:g} seconds)" + f"Connected to MCP server {name} timeout ({timeout:g} seconds)", ) from exc except Exception: logger.error(f"Failed to initialize MCP client {name}", exc_info=True) @@ -635,14 +635,14 @@ class FunctionToolManager: return [] async def _cleanup_mcp_client_safely( - self, mcp_client: MCPClient, name: str + self, mcp_client: MCPClient, name: str, ) -> None: """安全清理单个 MCP 客户端,避免清理异常中断主流程。""" try: await mcp_client.cleanup() except Exception as cleanup_exc: # noqa: BLE001 - only log here logger.error( - f"Failed to cleanup MCP client resources {name}: {cleanup_exc}" + f"Failed to cleanup MCP client resources {name}: {cleanup_exc}", ) async def _init_mcp_client(self, name: str, config: dict) -> MCPClient: @@ -732,7 +732,7 @@ class FunctionToolManager: name: str, config: dict, shutdown_event: asyncio.Event | None = None, - timeout: float | int | str | None = None, + timeout: float | str | None = None, ) -> None: """Enable a new MCP server and initialize it. @@ -746,6 +746,7 @@ class FunctionToolManager: Raises: MCPInitTimeoutError: If initialization does not complete within timeout. Exception: If there is an error during initialization. + """ if timeout is None: timeout_value = self._enable_timeout_default diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index c40d851d7..3e5bf8385 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -211,7 +211,7 @@ class ProviderManager: return self.inst_map.get(provider_id) def get_using_provider( - self, provider_type: ProviderType, umo=None + self, provider_type: ProviderType, umo=None, ) -> Providers | None: """获取正在使用的提供商实例。 @@ -269,7 +269,7 @@ class ProviderManager: if not provider and provider_id: logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。", ) return provider @@ -355,6 +355,7 @@ class ProviderManager: Raises: ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。 + """ match type: case "openai_chat_completion": @@ -487,6 +488,7 @@ class ProviderManager: Returns: dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典 + """ pc = copy.deepcopy(provider_config) provider_source_id = pc.get("provider_source_id", "") @@ -587,7 +589,7 @@ class ProviderManager: # STT 任务 if not issubclass(cls_type, STTProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of STTProvider" + f"Provider class {cls_type} is not a subclass of STTProvider", ) inst = cls_type(provider_config, self.provider_settings) @@ -610,7 +612,7 @@ class ProviderManager: # TTS 任务 if not issubclass(cls_type, TTSProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of TTSProvider" + f"Provider class {cls_type} is not a subclass of TTSProvider", ) inst = cls_type(provider_config, self.provider_settings) @@ -633,7 +635,7 @@ class ProviderManager: # 文本生成任务 if not issubclass(cls_type, Provider): raise TypeError( - f"Provider class {cls_type} is not a subclass of Provider" + f"Provider class {cls_type} is not a subclass of Provider", ) inst = cls_type( provider_config, @@ -658,7 +660,7 @@ class ProviderManager: case ProviderType.EMBEDDING: if not issubclass(cls_type, EmbeddingProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of EmbeddingProvider" + f"Provider class {cls_type} is not a subclass of EmbeddingProvider", ) inst = cls_type(provider_config, self.provider_settings) if isinstance(inst, HasInitialize): @@ -667,7 +669,7 @@ class ProviderManager: case ProviderType.RERANK: if not issubclass(cls_type, RerankProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of RerankProvider" + f"Provider class {cls_type} is not a subclass of RerankProvider", ) inst = cls_type(provider_config, self.provider_settings) if isinstance(inst, HasInitialize): @@ -677,7 +679,7 @@ class ProviderManager: # 未知供应商抛出异常,确保inst初始化 # Should be unreachable raise Exception( - f"未知的提供商类型:{provider_metadata.provider_type}" + f"未知的提供商类型:{provider_metadata.provider_type}", ) self.inst_map[provider_config["id"]] = inst @@ -770,7 +772,7 @@ class ProviderManager: del self.inst_map[provider_id] async def delete_provider( - self, provider_id: str | None = None, provider_source_id: str | None = None + self, provider_id: str | None = None, provider_source_id: str | None = None, ) -> None: """Delete provider and/or provider source from config and terminate the instances. Config will be saved after deletion.""" async with self.resource_lock: @@ -794,7 +796,7 @@ class ProviderManager: 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.""" async with self.resource_lock: - npid = new_config.get("id", None) + npid = new_config.get("id") if not npid: raise ValueError("New provider config must have an 'id' field") config = self.acm.default_conf @@ -818,7 +820,7 @@ class ProviderManager: async def create_provider(self, new_config: dict) -> None: """Add new provider config and load the instance. Config will be saved after addition.""" async with self.resource_lock: - npid = new_config.get("id", None) + npid = new_config.get("id") if not npid: raise ValueError("New provider config must have an 'id' field") config = self.acm.default_conf diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index f2571b506..9aaa1ae5a 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -55,12 +55,12 @@ class AbstractProvider(abc.ABC): return meta async def test(self) -> None: - """test the provider is a + """Test the provider is a - raises: + Raises: Exception: if the provider is not available + """ - ... class Provider(AbstractProvider): @@ -165,7 +165,7 @@ class Provider(AbstractProvider): """ if False: # pragma: no cover - make this an async generator for typing yield None # type: ignore - raise NotImplementedError() + raise NotImplementedError async def pop_record(self, context: list) -> None: """弹出 context 第一条非系统提示词对话记录""" @@ -239,6 +239,7 @@ class TTSProvider(AbstractProvider): Notes: 子类可以重写此方法返回 True 来启用流式 TTS 支持 + """ return False @@ -265,6 +266,7 @@ class TTSProvider(AbstractProvider): - 默认实现会将文本累积后一次性调用 get_audio 生成完整音频 - 子类可以重写此方法实现真正的流式 TTS - 音频数据应该是 WAV 格式的 bytes + """ accumulated_text = "" @@ -301,7 +303,7 @@ class TTSProvider(AbstractProvider): if file_size == 0: raise Exception( "TTS test failed: generated audio file is empty (0 bytes). " - "Please check your TTS provider configuration, especially required parameters like group_id for MiniMax." + "Please check your TTS provider configuration, especially required parameters like group_id for MiniMax.", ) # 清理测试文件 diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 83f2e16db..ffdfd40e1 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -44,7 +44,7 @@ class ProviderAnthropic(Provider): return raise EmptyModelOutputError( "Anthropic completion has no usable output. " - f"completion_id={completion_id}, stop_reason={stop_reason}" + f"completion_id={completion_id}, stop_reason={stop_reason}", ) @staticmethod @@ -222,7 +222,7 @@ class ProviderAnthropic(Provider): # Detect actual image format from binary data image_bytes = base64.b64decode(base64_data) media_type = self._detect_image_mime_type( - image_bytes + image_bytes, ) converted_content.append( { @@ -232,22 +232,22 @@ class ProviderAnthropic(Provider): "media_type": media_type, "data": base64_data, }, - } + }, ) except ValueError: logger.warning( - f"Failed to parse image data URI: {url[:50]}..." + f"Failed to parse image data URI: {url[:50]}...", ) else: logger.warning( - f"Unsupported image URL format for Anthropic: {url[:50]}..." + f"Unsupported image URL format for Anthropic: {url[:50]}...", ) elif part.get("type") == "audio_url": converted_content.append( { "type": "text", "text": "[Audio Attachment]", - } + }, ) else: converted_content.append(part) @@ -255,7 +255,7 @@ class ProviderAnthropic(Provider): { "role": "user", "content": converted_content, - } + }, ) else: new_messages.append(message) @@ -287,7 +287,7 @@ class ProviderAnthropic(Provider): payloads["tool_choice"] = { "type": "any" if payloads.get("tool_choice") == "required" - else "auto" + else "auto", } extra_body = self.provider_config.get("custom_extra_body", {}) @@ -298,7 +298,7 @@ class ProviderAnthropic(Provider): try: completion = await self.client.messages.create( - **payloads, stream=False, extra_body=extra_body + **payloads, stream=False, extra_body=extra_body, ) except httpx.RequestError as e: proxy = self.provider_config.get("proxy", "") @@ -315,7 +315,7 @@ class ProviderAnthropic(Provider): if len(completion.content) == 0: raise EmptyModelOutputError( - f"Anthropic completion is empty. completion_id={completion.id}" + f"Anthropic completion is empty. completion_id={completion.id}", ) llm_response = LLMResponse(role="assistant") @@ -346,13 +346,13 @@ class ProviderAnthropic(Provider): if not llm_response.reasoning_content: raise EmptyModelOutputError( "Anthropic completion has no usable output. " - f"completion_id={completion.id}, stop_reason={completion.stop_reason}" + f"completion_id={completion.id}, stop_reason={completion.stop_reason}", ) # We have reasoning content (ThinkingBlock) - this is valid stop_reason = getattr(completion, "stop_reason", "unknown") logger.debug( - f"Completion contains only ThinkingBlock (stop_reason={stop_reason})" + f"Completion contains only ThinkingBlock (stop_reason={stop_reason})", ) llm_response.completion_text = "" # Ensure empty string, not None @@ -374,7 +374,7 @@ class ProviderAnthropic(Provider): payloads["tool_choice"] = { "type": "any" if payloads.get("tool_choice") == "required" - else "auto" + else "auto", } # 用于累积工具调用信息 @@ -393,7 +393,7 @@ class ProviderAnthropic(Provider): self._apply_thinking_config(payloads) async with self.client.messages.stream( - **payloads, extra_body=extra_body + **payloads, extra_body=extra_body, ) as stream: assert isinstance(stream, anthropic.AsyncMessageStream) async for event in stream: diff --git a/astrbot/core/provider/sources/azure_tts_source.py b/astrbot/core/provider/sources/azure_tts_source.py index fc2bb6c09..57ea9fbb4 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -41,13 +41,13 @@ class OTTSProvider: def client(self) -> AsyncClient: if self._client is None: raise RuntimeError( - "Client not initialized. Please use 'async with' context." + "Client not initialized. Please use 'async with' context.", ) return self._client async def __aenter__(self): self._client = AsyncClient( - timeout=self.timeout, proxy=self.proxy if self.proxy else None + timeout=self.timeout, proxy=self.proxy or None, ) return self @@ -141,7 +141,7 @@ class AzureNativeProvider(TTSProvider): def client(self) -> AsyncClient: if self._client is None: raise RuntimeError( - "Client not initialized. Please use 'async with' context." + "Client not initialized. Please use 'async with' context.", ) return self._client @@ -152,7 +152,7 @@ class AzureNativeProvider(TTSProvider): "Content-Type": "application/ssml+xml", "X-Microsoft-OutputFormat": "riff-48khz-16bit-mono-pcm", }, - proxy=self.proxy if self.proxy else None, + proxy=self.proxy or None, ) return self @@ -213,7 +213,7 @@ class AzureTTSProvider(TTSProvider): self.provider = self._parse_provider(key_value, provider_config) def _parse_provider( - self, key_value: str, config: dict + self, key_value: str, config: dict, ) -> OTTSProvider | AzureNativeProvider: if key_value.lower().startswith("other["): json_str = "" diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py index 4513c2c85..17ec5bbdf 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -12,23 +12,20 @@ from ..register import register_provider_adapter class BailianRerankError(Exception): """百炼重排序服务异常基类""" - pass class BailianAPIError(BailianRerankError): """百炼API返回错误""" - pass class BailianNetworkError(BailianRerankError): """百炼网络请求错误""" - pass @register_provider_adapter( - "bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK + "bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK, ) class BailianRerankProvider(RerankProvider): """阿里云百炼文本重排序适配器.""" @@ -42,7 +39,7 @@ class BailianRerankProvider(RerankProvider): # API配置 self.api_key = provider_config.get("rerank_api_key") or os.getenv( - "DASHSCOPE_API_KEY", "" + "DASHSCOPE_API_KEY", "", ) if not self.api_key: raise ValueError("阿里云百炼 API Key 不能为空。") @@ -64,7 +61,7 @@ class BailianRerankProvider(RerankProvider): } self.client = aiohttp.ClientSession( - headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout) + headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout), ) # 设置模型名称 @@ -73,7 +70,7 @@ class BailianRerankProvider(RerankProvider): logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}") def _build_payload( - self, query: str, documents: list[str], top_n: int | None + self, query: str, documents: list[str], top_n: int | None, ) -> dict: """构建请求载荷 @@ -84,6 +81,7 @@ class BailianRerankProvider(RerankProvider): Returns: 请求载荷字典 + """ normalized_model = self.model.strip().lower() normalized_top_n = top_n if top_n is not None and top_n > 0 else None @@ -103,7 +101,7 @@ class BailianRerankProvider(RerankProvider): if self.return_documents: logger.warning( "qwen3-rerank does not support return_documents; " - "this option will be ignored." + "this option will be ignored.", ) return payload @@ -135,11 +133,12 @@ class BailianRerankProvider(RerankProvider): Raises: BailianAPIError: API返回错误 KeyError: 结果缺少必要字段 + """ # 检查响应状态 if data.get("code", "200") != "200": raise BailianAPIError( - f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}" + f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}", ) # 兼容旧版 API (output.results) 和新版 compatible API (results) @@ -160,7 +159,7 @@ class BailianRerankProvider(RerankProvider): relevance_score = 0.0 rerank_result = RerankResult( - index=index, relevance_score=relevance_score + index=index, relevance_score=relevance_score, ) rerank_results.append(rerank_result) except Exception as e: @@ -174,6 +173,7 @@ class BailianRerankProvider(RerankProvider): Args: data: API响应数据 + """ tokens = data.get("usage", {}).get("total_tokens", 0) if tokens > 0: @@ -185,8 +185,7 @@ class BailianRerankProvider(RerankProvider): documents: list[str], top_n: int | None = None, ) -> list[RerankResult]: - """ - 对文档进行重排序 + """对文档进行重排序 Args: query: 查询文本 @@ -195,6 +194,7 @@ class BailianRerankProvider(RerankProvider): Returns: 重排序结果列表 + """ if not self.client: logger.error("百炼 Rerank 客户端会话已关闭,返回空结果") @@ -211,7 +211,7 @@ class BailianRerankProvider(RerankProvider): # 检查限制 if len(documents) > 500: logger.warning( - f"文档数量({len(documents)})超过限制(500),将截断前500个文档" + f"文档数量({len(documents)})超过限制(500),将截断前500个文档", ) documents = documents[:500] @@ -220,7 +220,7 @@ class BailianRerankProvider(RerankProvider): payload = self._build_payload(query, documents, top_n) logger.debug( - f"百炼 Rerank 请求: query='{query[:50]}...', 文档数量={len(documents)}" + f"百炼 Rerank 请求: query='{query[:50]}...', 文档数量={len(documents)}", ) # 发送请求 diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py index 35945b7b6..645cd050b 100644 --- a/astrbot/core/provider/sources/fishaudio_tts_api_source.py +++ b/astrbot/core/provider/sources/fishaudio_tts_api_source.py @@ -85,7 +85,7 @@ class ProviderFishAudioTTSAPI(TTSProvider): sort_options = ["score", "task_count", "created_at"] async with AsyncClient( base_url=self.api_base.replace("/v1", ""), - proxy=self.proxy if self.proxy else None, + proxy=self.proxy or None, ) as client: for sort_by in sort_options: params = {"title": character, "sort_by": sort_by} @@ -149,7 +149,7 @@ class ProviderFishAudioTTSAPI(TTSProvider): async with AsyncClient( base_url=self.api_base, timeout=self.timeout, - proxy=self.proxy if self.proxy else None, + proxy=self.proxy or None, ).stream( "POST", "/tts", @@ -157,7 +157,7 @@ class ProviderFishAudioTTSAPI(TTSProvider): content=ormsgpack.packb(request, option=ormsgpack.OPT_SERIALIZE_PYDANTIC), ) as response: if response.status_code == 200 and response.headers.get( - "content-type", "" + "content-type", "", ).startswith("audio/"): with open(path, "wb") as f: async for chunk in response.aiter_bytes(): @@ -166,5 +166,5 @@ class ProviderFishAudioTTSAPI(TTSProvider): error_bytes = await response.aread() error_text = error_bytes.decode("utf-8", errors="replace")[:1024] raise Exception( - f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}" + f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}", ) diff --git a/astrbot/core/provider/sources/gemini_embedding_source.py b/astrbot/core/provider/sources/gemini_embedding_source.py index 61ba9cadb..d8f67eb4e 100644 --- a/astrbot/core/provider/sources/gemini_embedding_source.py +++ b/astrbot/core/provider/sources/gemini_embedding_source.py @@ -63,7 +63,7 @@ class GeminiEmbeddingProvider(EmbeddingProvider): try: result = await self.client.models.embed_content( model=self.model, - contents=cast(types.ContentListUnion, text), + contents=cast("types.ContentListUnion", text), config=types.EmbedContentConfig( output_dimensionality=self.get_dim(), ), diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 158170d51..da8c047f5 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -155,7 +155,7 @@ class ProviderGoogleGenAI(Provider): modalities = ["TEXT"] tool_list: list[types.Tool] | None = [] - model_name = cast(str, payloads.get("model", self.get_model())) + model_name = cast("str", payloads.get("model", self.get_model())) native_coderunner = self.provider_config.get("gm_native_coderunner", False) native_search = self.provider_config.get("gm_native_search", False) url_context = self.provider_config.get("gm_url_context", False) @@ -223,8 +223,8 @@ class ProviderGoogleGenAI(Provider): types.FunctionCallingConfigMode.ANY if tool_choice == "required" else types.FunctionCallingConfigMode.AUTO - ) - ) + ), + ), ) # oper thinking config @@ -241,7 +241,7 @@ class ProviderGoogleGenAI(Provider): ]: # The thinkingBudget parameter, introduced with the Gemini 2.5 series thinking_budget = self.provider_config.get("gm_thinking_config", {}).get( - "budget", 0 + "budget", 0, ) if thinking_budget is not None: thinking_config = types.ThinkingConfig( @@ -253,19 +253,19 @@ class ProviderGoogleGenAI(Provider): # covered without needing to keep an exhaustive list up to date. # Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead. thinking_level = self.provider_config.get("gm_thinking_config", {}).get( - "level", "HIGH" + "level", "HIGH", ) if thinking_level and isinstance(thinking_level, str): thinking_level = thinking_level.upper() if thinking_level not in ["MINIMAL", "LOW", "MEDIUM", "HIGH"]: logger.warning( - f"Invalid thinking level: {thinking_level}, using HIGH" + f"Invalid thinking level: {thinking_level}, using HIGH", ) thinking_level = "HIGH" level = types.ThinkingLevel(thinking_level) thinking_config = types.ThinkingConfig() if not hasattr(types.ThinkingConfig, "thinking_level"): - setattr(types.ThinkingConfig, "thinking_level", level) + types.ThinkingConfig.thinking_level = level else: thinking_config.thinking_level = level @@ -286,9 +286,9 @@ class ProviderGoogleGenAI(Provider): logprobs=payloads.get("logprobs"), seed=payloads.get("seed"), response_modalities=modalities, - tools=cast(types.ToolListUnion | None, tool_list), + tools=cast("types.ToolListUnion | None", tool_list), tool_config=tool_config, - safety_settings=self.safety_settings if self.safety_settings else None, + safety_settings=self.safety_settings or None, thinking_config=thinking_config, automatic_function_calling=types.AutomaticFunctionCallingConfig( disable=True, @@ -299,7 +299,7 @@ class ProviderGoogleGenAI(Provider): """准备 Gemini SDK 的 Content 列表""" def create_text_part(text: str) -> types.Part: - content_a = text if text else " " + content_a = text or " " if not text: logger.warning("文本内容为空,已添加空格占位") return types.Part.from_text(text=content_a) @@ -383,7 +383,7 @@ class ProviderGoogleGenAI(Provider): types.Part( text=text, thought_signature=thinking_signature, - ) + ), ) append_or_extend(gemini_contents, parts, types.ModelContent) @@ -397,7 +397,7 @@ class ProviderGoogleGenAI(Provider): # we should set thought_signature back to part if exists # for more info about thought_signature, see: # https://ai.google.dev/gemini-api/docs/thought-signatures - if "extra_content" in tool and tool["extra_content"]: + if tool.get("extra_content"): ts_bs64 = ( tool["extra_content"] .get("google", {}) @@ -445,7 +445,7 @@ class ProviderGoogleGenAI(Provider): return "".join(thought_buf).strip() def _extract_usage( - self, usage_metadata: types.GenerateContentResponseUsageMetadata + self, usage_metadata: types.GenerateContentResponseUsageMetadata, ) -> TokenUsage: """Extract usage from candidate""" return TokenUsage( @@ -468,7 +468,7 @@ class ProviderGoogleGenAI(Provider): return raise EmptyModelOutputError( "Gemini completion has no usable output. " - f"response_id={response_id}, finish_reason={finish_reason}" + f"response_id={response_id}, finish_reason={finish_reason}", ) def _process_content_parts( @@ -484,7 +484,7 @@ class ProviderGoogleGenAI(Provider): if validate_output: raise EmptyModelOutputError( "Gemini candidate content is empty. " - f"finish_reason={candidate.finish_reason}" + f"finish_reason={candidate.finish_reason}", ) llm_response.result_chain = MessageChain(chain=[]) return llm_response.result_chain @@ -512,7 +512,7 @@ class ProviderGoogleGenAI(Provider): if validate_output: raise EmptyModelOutputError( "Gemini candidate content parts are empty. " - f"finish_reason={candidate.finish_reason}" + f"finish_reason={candidate.finish_reason}", ) llm_response.result_chain = MessageChain(chain=[]) return llm_response.result_chain @@ -556,7 +556,7 @@ class ProviderGoogleGenAI(Provider): if part.thought_signature: ts_bs64 = base64.b64encode(part.thought_signature).decode("utf-8") llm_response.tools_call_extra_content[tool_call_id] = { - "google": {"thought_signature": ts_bs64} + "google": {"thought_signature": ts_bs64}, } if ( @@ -609,7 +609,7 @@ class ProviderGoogleGenAI(Provider): ) result = await self.client.models.generate_content( model=model, - contents=cast(types.ContentListUnion, conversation), + contents=cast("types.ContentListUnion", conversation), config=config, ) logger.debug(f"genai result: {result}") @@ -689,7 +689,7 @@ class ProviderGoogleGenAI(Provider): ) result = await self.client.models.generate_content_stream( model=model, - contents=cast(types.ContentListUnion, conversation), + contents=cast("types.ContentListUnion", conversation), config=config, ) break @@ -970,7 +970,7 @@ class ProviderGoogleGenAI(Provider): temp_dir = Path(get_astrbot_temp_path()) temp_dir.mkdir(parents=True, exist_ok=True) resolved_path = str( - temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}" + temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}", ) await download_file(audio_path, resolved_path) elif audio_path.startswith("file:///"): @@ -987,7 +987,7 @@ class ProviderGoogleGenAI(Provider): audio_bytes = Path(resolved_path).read_bytes() except OSError as exc: logger.warning( - f"Failed to read audio file {resolved_path}, skipping. Error: {exc}" + f"Failed to read audio file {resolved_path}, skipping. Error: {exc}", ) return None diff --git a/astrbot/core/provider/sources/groq_source.py b/astrbot/core/provider/sources/groq_source.py index af4029f67..fb8b77c03 100644 --- a/astrbot/core/provider/sources/groq_source.py +++ b/astrbot/core/provider/sources/groq_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "groq_chat_completion", "Groq Chat Completion Provider Adapter" + "groq_chat_completion", "Groq Chat Completion Provider Adapter", ) class ProviderGroq(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/longcat_source.py b/astrbot/core/provider/sources/longcat_source.py index e251fb310..21d8043b9 100644 --- a/astrbot/core/provider/sources/longcat_source.py +++ b/astrbot/core/provider/sources/longcat_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "longcat_chat_completion", "LongCat Chat Completion Provider Adapter" + "longcat_chat_completion", "LongCat Chat Completion Provider Adapter", ) class ProviderLongCat(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/mimo_stt_api_source.py b/astrbot/core/provider/sources/mimo_stt_api_source.py index 9b03e2efc..725802400 100644 --- a/astrbot/core/provider/sources/mimo_stt_api_source.py +++ b/astrbot/core/provider/sources/mimo_stt_api_source.py @@ -82,7 +82,7 @@ class ProviderMiMoSTTAPI(STTProvider): except Exception as exc: error_text = response.text[:1024] raise MiMoAPIError( - f"MiMo STT API request failed: HTTP {response.status_code}, response: {error_text}" + f"MiMo STT API request failed: HTTP {response.status_code}, response: {error_text}", ) from exc data = response.json() diff --git a/astrbot/core/provider/sources/mimo_tts_api_source.py b/astrbot/core/provider/sources/mimo_tts_api_source.py index 2966bfb7d..88c1ee742 100644 --- a/astrbot/core/provider/sources/mimo_tts_api_source.py +++ b/astrbot/core/provider/sources/mimo_tts_api_source.py @@ -39,7 +39,7 @@ class ProviderMiMoTTSAPI(TTSProvider): self.style_prompt = provider_config.get("mimo-tts-style-prompt", "") self.dialect = provider_config.get("mimo-tts-dialect", "") self.seed_text = provider_config.get( - "mimo-tts-seed-text", DEFAULT_MIMO_TTS_SEED_TEXT + "mimo-tts-seed-text", DEFAULT_MIMO_TTS_SEED_TEXT, ) self.set_model(provider_config.get("model", DEFAULT_MIMO_TTS_MODEL)) self.client = create_http_client(self.timeout, self.proxy) @@ -78,14 +78,14 @@ class ProviderMiMoTTSAPI(TTSProvider): { "role": "user", "content": user_prompt, - } + }, ) messages.append( { "role": "assistant", "content": self._build_assistant_content(text), - } + }, ) return { @@ -109,7 +109,7 @@ class ProviderMiMoTTSAPI(TTSProvider): except Exception as exc: error_text = response.text[:1024] raise MiMoAPIError( - f"MiMo TTS API request failed: HTTP {response.status_code}, response: {error_text}" + f"MiMo TTS API request failed: HTTP {response.status_code}, response: {error_text}", ) from exc data = response.json() diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index f40cb968a..4f5927bf3 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -121,7 +121,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider): if "extra_info" in data: continue audio: str | None = data.get("data", {}).get( - "audio" + "audio", ) if audio is not None: yield audio @@ -159,7 +159,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider): raise Exception( "MiniMax TTS API returned empty audio data. " "Please verify your configuration, especially the 'group_id' parameter. " - "You can find your group_id in Account Management -> Basic Information on the MiniMax platform." + "You can find your group_id in Account Management -> Basic Information on the MiniMax platform.", ) # 结果保存至文件 diff --git a/astrbot/core/provider/sources/nvidia_rerank_source.py b/astrbot/core/provider/sources/nvidia_rerank_source.py index c168da4a6..0c76e73b9 100644 --- a/astrbot/core/provider/sources/nvidia_rerank_source.py +++ b/astrbot/core/provider/sources/nvidia_rerank_source.py @@ -8,21 +8,21 @@ from ..register import register_provider_adapter @register_provider_adapter( - "nvidia_rerank", "NVIDIA Rerank 适配器", provider_type=ProviderType.RERANK + "nvidia_rerank", "NVIDIA Rerank 适配器", provider_type=ProviderType.RERANK, ) class NvidiaRerankProvider(RerankProvider): def __init__(self, provider_config: dict, provider_settings: dict) -> None: super().__init__(provider_config, provider_settings) self.api_key = provider_config.get("nvidia_rerank_api_key", "") self.base_url = provider_config.get( - "nvidia_rerank_api_base", "https://ai.api.nvidia.com/v1/retrieval" + "nvidia_rerank_api_base", "https://ai.api.nvidia.com/v1/retrieval", ).rstrip("/") self.timeout = provider_config.get("timeout", 20) self.model = provider_config.get( - "nvidia_rerank_model", "nv-rerank-qa-mistral-4b:1" + "nvidia_rerank_model", "nv-rerank-qa-mistral-4b:1", ) self.model_endpoint = provider_config.get( - "nvidia_rerank_model_endpoint", "/reranking" + "nvidia_rerank_model_endpoint", "/reranking", ) self.truncate = provider_config.get("nvidia_rerank_truncate", "") @@ -37,13 +37,12 @@ class NvidiaRerankProvider(RerankProvider): "Accept": "application/json", } self.client = aiohttp.ClientSession( - headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout) + headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout), ) return self.client def _get_endpoint(self) -> str: - """ - 构建完整API URL。 + """构建完整API URL。 根据 Nvidia Rerank API 文档来看,当前URL存在不同模型格式不一致的问题。 这里针对模型名做一个基础判断用以适配,后续要等Nvidia统一API格式后再做调整。 @@ -55,7 +54,6 @@ class NvidiaRerankProvider(RerankProvider): 模型: nvidia/llama-nemotron-rerank-1b-v2 URL: .../v1/retrieval/nvidia/llama-nemotron-rerank-1b-v2/reranking """ - model_path = "nvidia" logger.debug(f"[NVIDIA Rerank] Building endpoint for model: {self.model}") if "/" in self.model: @@ -76,7 +74,7 @@ class NvidiaRerankProvider(RerankProvider): return payload def _parse_results( - self, response_data: dict, top_n: int | None + self, response_data: dict, top_n: int | None, ) -> list[RerankResult]: """解析响应数据""" results = response_data.get("rankings", []) @@ -90,11 +88,11 @@ class NvidiaRerankProvider(RerankProvider): index = item.get("index", idx) score = item.get("relevance_score", item.get("logit", 0.0)) rerank_results.append( - RerankResult(index=index, relevance_score=float(score)) + RerankResult(index=index, relevance_score=float(score)), ) except Exception as e: logger.warning( - f"[NVIDIA Rerank] Result parsing error: {e}, Data={item}" + f"[NVIDIA Rerank] Result parsing error: {e}, Data={item}", ) rerank_results.sort(key=lambda x: x.relevance_score, reverse=True) @@ -122,7 +120,7 @@ class NvidiaRerankProvider(RerankProvider): if not documents or not query.strip(): logger.warning( - "[NVIDIA Rerank] Input data is invalid, query or documents are empty" + "[NVIDIA Rerank] Input data is invalid, query or documents are empty", ) return [] @@ -135,7 +133,7 @@ class NvidiaRerankProvider(RerankProvider): try: response_data = await response.json() error_detail = response_data.get( - "detail", response_data.get("message", "Unknown Error") + "detail", response_data.get("message", "Unknown Error"), ) except Exception: diff --git a/astrbot/core/provider/sources/oai_aihubmix_source.py b/astrbot/core/provider/sources/oai_aihubmix_source.py index ca8ad5959..60da20c74 100644 --- a/astrbot/core/provider/sources/oai_aihubmix_source.py +++ b/astrbot/core/provider/sources/oai_aihubmix_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "aihubmix_chat_completion", "AIHubMix Chat Completion Provider Adapter" + "aihubmix_chat_completion", "AIHubMix Chat Completion Provider Adapter", ) class ProviderAIHubMix(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py index ae531996a..cd5abb393 100644 --- a/astrbot/core/provider/sources/openai_embedding_source.py +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -70,7 +70,7 @@ class OpenAIEmbeddingProvider(EmbeddingProvider): kwargs["dimensions"] = int(self.provider_config["embedding_dimensions"]) except (ValueError, TypeError): logger.warning( - f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored." + f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored.", ) return kwargs @@ -81,7 +81,7 @@ class OpenAIEmbeddingProvider(EmbeddingProvider): return int(self.provider_config["embedding_dimensions"]) except (ValueError, TypeError): logger.warning( - f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored." + f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored.", ) return 0 diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index b19f3460d..f1ebd9411 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -95,7 +95,7 @@ class ProviderOpenAIOfficial(Provider): if not text: return candidates.append( - ProviderOpenAIOfficial._truncate_error_text_candidate(text) + ProviderOpenAIOfficial._truncate_error_text_candidate(text), ) _append_candidate(str(error)) @@ -104,7 +104,7 @@ class ProviderOpenAIOfficial(Provider): if isinstance(body, dict): err_obj = body.get("error") body_text = ProviderOpenAIOfficial._safe_json_dump( - {"error": err_obj} if isinstance(err_obj, dict) else body + {"error": err_obj} if isinstance(err_obj, dict) else body, ) _append_candidate(body_text) if isinstance(err_obj, dict): @@ -374,7 +374,7 @@ class ProviderOpenAIOfficial(Provider): try: resolved_part = await self._resolve_image_part( - url, image_detail=image_detail + url, image_detail=image_detail, ) except Exception as exc: logger.warning( @@ -404,7 +404,7 @@ class ProviderOpenAIOfficial(Provider): return {**message, "content": new_content} async def _materialize_context_image_parts( - self, context_query: list[dict] + self, context_query: list[dict], ) -> list[dict]: return [ await self._materialize_message_image_parts(message) @@ -495,7 +495,7 @@ class ProviderOpenAIOfficial(Provider): return bool(value) def _apply_provider_specific_extra_body_overrides( - self, extra_body: dict[str, Any] + self, extra_body: dict[str, Any], ) -> None: if self.provider_config.get("provider") != "ollama": return @@ -729,6 +729,7 @@ class ProviderOpenAIOfficial(Provider): Returns: Normalized plain text string. + """ # Handle dict format (e.g., {"type": "text", "text": "..."}) if isinstance(raw_content, dict): @@ -795,7 +796,7 @@ class ProviderOpenAIOfficial(Provider): text_val = part.get("text", "") # Coerce to str in case text is null or non-string text_parts.append( - str(text_val) if text_val is not None else "" + str(text_val) if text_val is not None else "", ) if text_parts: return "".join(text_parts) @@ -805,14 +806,14 @@ class ProviderOpenAIOfficial(Provider): return str(raw_content) if raw_content is not None else "" async def _parse_openai_completion( - self, completion: ChatCompletion, tools: ToolSet | None + self, completion: ChatCompletion, tools: ToolSet | None, ) -> LLMResponse: """Parse OpenAI ChatCompletion into LLMResponse""" llm_response = LLMResponse("assistant") if not completion.choices: raise EmptyModelOutputError( - f"OpenAI completion has no choices. response_id={completion.id}" + f"OpenAI completion has no choices. response_id={completion.id}", ) choice = completion.choices[0] @@ -892,7 +893,7 @@ class ProviderOpenAIOfficial(Provider): logger.error(f"OpenAI completion has no usable output: {completion}.") raise EmptyModelOutputError( "OpenAI completion has no usable output. " - f"response_id={completion.id}, finish_reason={choice.finish_reason}" + f"response_id={completion.id}, finish_reason={choice.finish_reason}", ) llm_response.raw_completion = completion @@ -962,7 +963,7 @@ class ProviderOpenAIOfficial(Provider): for message in payloads.get("messages", []): if message.get("role") == "assistant" and isinstance( - message.get("content"), list + message.get("content"), list, ): reasoning_content = "" new_content = [] # not including think part @@ -986,7 +987,7 @@ class ProviderOpenAIOfficial(Provider): json.loads(content) except (json.JSONDecodeError, ValueError): message["content"] = json.dumps( - {"result": content}, ensure_ascii=False + {"result": content}, ensure_ascii=False, ) async def _handle_api_error( @@ -1286,7 +1287,6 @@ class ProviderOpenAIOfficial(Provider): extra_user_content_parts: list[ContentPart] | None = None, ) -> dict: """组装成符合 OpenAI 格式的 role 为 user 的消息段""" - # 构建内容块列表 content_blocks = [] diff --git a/astrbot/core/provider/sources/openrouter_source.py b/astrbot/core/provider/sources/openrouter_source.py index e49d0c929..3cbdd3eee 100644 --- a/astrbot/core/provider/sources/openrouter_source.py +++ b/astrbot/core/provider/sources/openrouter_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "openrouter_chat_completion", "OpenRouter Chat Completion Provider Adapter" + "openrouter_chat_completion", "OpenRouter Chat Completion Provider Adapter", ) class ProviderOpenRouter(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py index d41ebaf62..d3018df83 100644 --- a/astrbot/core/provider/sources/sensevoice_selfhosted_source.py +++ b/astrbot/core/provider/sources/sensevoice_selfhosted_source.py @@ -91,8 +91,8 @@ class ProviderSenseVoiceSTTSelfHost(STTProvider): loop = asyncio.get_running_loop() res = await loop.run_in_executor( None, # 使用默认的线程池 - lambda: cast(SenseVoiceSmall, self.model)( - audio_url, language="auto", use_itn=True + lambda: cast("SenseVoiceSmall", self.model)( + audio_url, language="auto", use_itn=True, ), ) diff --git a/astrbot/core/provider/sources/whisper_api_source.py b/astrbot/core/provider/sources/whisper_api_source.py index df5e8fc6b..88bfb2265 100644 --- a/astrbot/core/provider/sources/whisper_api_source.py +++ b/astrbot/core/provider/sources/whisper_api_source.py @@ -105,12 +105,12 @@ class ProviderOpenAIWhisperAPI(STTProvider): if file_format == "silk": logger.info( - "Converting silk file to wav using tencent_silk_to_wav..." + "Converting silk file to wav using tencent_silk_to_wav...", ) await tencent_silk_to_wav(audio_url, output_path) elif file_format == "amr": logger.info( - "Converting amr file to wav using convert_to_pcm_wav..." + "Converting amr file to wav using convert_to_pcm_wav...", ) await convert_to_pcm_wav(audio_url, output_path) diff --git a/astrbot/core/provider/sources/whisper_selfhosted_source.py b/astrbot/core/provider/sources/whisper_selfhosted_source.py index 0622df6fc..c633c156d 100644 --- a/astrbot/core/provider/sources/whisper_selfhosted_source.py +++ b/astrbot/core/provider/sources/whisper_selfhosted_source.py @@ -103,4 +103,4 @@ class ProviderOpenAIWhisperSelfHost(STTProvider): raise RuntimeError("Whisper 模型未初始化") result = await loop.run_in_executor(None, self.model.transcribe, audio_url) - return cast(str, result["text"]) + return cast("str", result["text"]) diff --git a/astrbot/core/provider/sources/xai_source.py b/astrbot/core/provider/sources/xai_source.py index b7b432b49..9dea66f20 100644 --- a/astrbot/core/provider/sources/xai_source.py +++ b/astrbot/core/provider/sources/xai_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "xai_chat_completion", "xAI Chat Completion Provider Adapter" + "xai_chat_completion", "xAI Chat Completion Provider Adapter", ) class ProviderXAI(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/xinference_rerank_source.py b/astrbot/core/provider/sources/xinference_rerank_source.py index 9c3a77c15..22230cb05 100644 --- a/astrbot/core/provider/sources/xinference_rerank_source.py +++ b/astrbot/core/provider/sources/xinference_rerank_source.py @@ -71,7 +71,7 @@ class XinferenceRerankProvider(RerankProvider): if self.model_uid: self.model = cast( - AsyncRESTfulRerankModelHandle, + "AsyncRESTfulRerankModelHandle", await self.client.get_model(self.model_uid), ) diff --git a/astrbot/core/provider/sources/xinference_stt_provider.py b/astrbot/core/provider/sources/xinference_stt_provider.py index 0a22e456e..07965c2b4 100644 --- a/astrbot/core/provider/sources/xinference_stt_provider.py +++ b/astrbot/core/provider/sources/xinference_stt_provider.py @@ -128,7 +128,7 @@ class ProviderXinferenceSTT(STTProvider): # 3. Perform conversion if needed if conversion_type: logger.info( - f"Audio requires conversion ({conversion_type}), using temporary files..." + f"Audio requires conversion ({conversion_type}), using temporary files...", ) temp_dir = get_astrbot_temp_path() os.makedirs(temp_dir, exist_ok=True) diff --git a/astrbot/core/skills/neo_skill_sync.py b/astrbot/core/skills/neo_skill_sync.py index 5fe2b7832..fa7098c44 100644 --- a/astrbot/core/skills/neo_skill_sync.py +++ b/astrbot/core/skills/neo_skill_sync.py @@ -224,7 +224,7 @@ class NeoSkillSyncManager: items = page_json.get("items", []) if not isinstance(items, list) or not items: raise ValueError( - f"No active stable release found for skill_key: {skill_key}" + f"No active stable release found for skill_key: {skill_key}", ) if not isinstance(items[0], dict): raise ValueError("Unexpected release payload format.") @@ -242,7 +242,7 @@ class NeoSkillSyncManager: release = await self._find_release(client, release_id=release_id) elif skill_key: release = await self._find_active_stable_release( - client, skill_key=skill_key + client, skill_key=skill_key, ) else: raise ValueError("release_id or skill_key is required for sync.") @@ -259,7 +259,7 @@ class NeoSkillSyncManager: if require_stable and release_stage != "stable": raise ValueError( "Only stable releases can be synced to local SKILL.md " - f"(got: {release_stage_raw})." + f"(got: {release_stage_raw}).", ) candidate = await client.skills.get_candidate(candidate_id) @@ -277,7 +277,7 @@ class NeoSkillSyncManager: skill_markdown = payload.get("skill_markdown") if not isinstance(skill_markdown, str) or not skill_markdown.strip(): raise ValueError( - "payload.skill_markdown is required for stable sync to local skill." + "payload.skill_markdown is required for stable sync to local skill.", ) mapping = self._load_map() @@ -348,7 +348,7 @@ class NeoSkillSyncManager: sync_error = str(err) try: rollback = await client.skills.rollback_release( - str(release_json.get("id", "")) + str(release_json.get("id", "")), ) rollback_json = _to_jsonable(rollback) except Exception as rollback_err: @@ -361,7 +361,7 @@ class NeoSkillSyncManager: else: raise RuntimeError( "stable release synced failed and auto rollback also failed; " - f"sync_error={sync_error}; rollback_error={rollback_err}" + f"sync_error={sync_error}; rollback_error={rollback_err}", ) from rollback_err return { diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index a8121c42a..4a9e858f9 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -223,7 +223,7 @@ def build_skills_prompt(skills: list[SkillInfo]) -> str: rendered_path = "//SKILL.md" skills_lines.append( - f"- **{display_name}**: {description}\n File: `{rendered_path}`" + f"- **{display_name}**: {description}\n File: `{rendered_path}`", ) if not example_path: example_path = rendered_path @@ -331,7 +331,7 @@ class SkillManager: continue description = str(item.get("description", "") or "") path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, str(item.get("path", "") or ""), ) deduped[name] = { "name": name, @@ -381,7 +381,7 @@ class SkillManager: continue name = str(item.get("name", "") or "").strip() path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, str(item.get("path", "") or ""), ) if not name or not _SKILL_NAME_RE.match(name): continue @@ -414,7 +414,7 @@ class SkillManager: source_label = "synced" if sandbox_exists else "local" if runtime == "sandbox" and show_sandbox_path: path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) else: path_str = str(skill_md) @@ -453,7 +453,7 @@ class SkillManager: # since there is no local path to show. Always prefer the # actual path from sandbox cache. path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) skills_by_name[skill_name] = SkillInfo( name=skill_name, @@ -491,7 +491,7 @@ class SkillManager: def set_skill_active(self, name: str, active: bool) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be enabled/disabled from local skill management." + "Sandbox preset skill cannot be enabled/disabled from local skill management.", ) config = self._load_config() config.setdefault("skills", {}) @@ -519,7 +519,7 @@ class SkillManager: def delete_skill(self, name: str) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be deleted from local skill management." + "Sandbox preset skill cannot be deleted from local skill management.", ) skill_dir = Path(self.skills_root) / name @@ -571,7 +571,7 @@ class SkillManager: if skill_name_hint is not None: archive_skill_name = _normalize_skill_name(skill_name_hint) if archive_skill_name and not _SKILL_NAME_RE.fullmatch( - archive_skill_name + archive_skill_name, ): raise ValueError("Invalid skill name.") @@ -596,7 +596,7 @@ class SkillManager: candidate_name = _normalize_skill_name(src_dir_name) if not candidate_name or not _SKILL_NAME_RE.fullmatch( - candidate_name + candidate_name, ): continue @@ -613,7 +613,7 @@ class SkillManager: raise FileExistsError( "One or more skills from the archive already exist and " "overwrite=False. No skills were installed. Conflicting " - f"paths: {', '.join(conflict_dirs)}" + f"paths: {', '.join(conflict_dirs)}", ) with tempfile.TemporaryDirectory(dir=get_astrbot_temp_path()) as tmp_dir: @@ -625,7 +625,7 @@ class SkillManager: if root_mode: archive_hint = _normalize_skill_name( - archive_skill_name or zip_path_obj.stem + archive_skill_name or zip_path_obj.stem, ) if not archive_hint or not _SKILL_NAME_RE.fullmatch(archive_hint): raise ValueError("Invalid skill name.") @@ -635,7 +635,7 @@ class SkillManager: normalized_path = _normalize_skill_markdown_path(src_dir) if normalized_path is None: raise ValueError( - "SKILL.md not found in the root of the zip archive." + "SKILL.md not found in the root of the zip archive.", ) dest_dir = Path(self.skills_root) / skill_name @@ -655,7 +655,7 @@ class SkillManager: for archive_root_name in top_dirs: archive_root_name_normalized = _normalize_skill_name( - archive_root_name + archive_root_name, ) if ( @@ -683,7 +683,7 @@ class SkillManager: if dest_dir.exists(): if not overwrite: raise FileExistsError( - f"Skill {skill_name} already exists." + f"Skill {skill_name} already exists.", ) shutil.rmtree(dest_dir) @@ -693,7 +693,7 @@ class SkillManager: if not installed_skills: raise ValueError( - "No valid SKILL.md found in any folder of the zip archive." + "No valid SKILL.md found in any folder of the zip archive.", ) return ", ".join(installed_skills) diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index c60af9ea2..3f2b31a1f 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -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 @@ -285,18 +285,18 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: if isinstance(filter_ref, CommandFilter): raw_fragment = getattr( - filter_ref, "_original_command_name", filter_ref.command_name + filter_ref, "_original_command_name", filter_ref.command_name, ) current_fragment = filter_ref.command_name parent_signature = (filter_ref.parent_command_names or [""])[0].strip() # 如果是子指令,尝试找到父指令组的 handler_full_name if is_sub_command and parent_signature: parent_group_handler = _find_parent_group_handler( - handler.handler_module_path, parent_signature + handler.handler_module_path, parent_signature, ) else: raw_fragment = getattr( - filter_ref, "_original_group_name", filter_ref.group_name + filter_ref, "_original_group_name", filter_ref.group_name, ) current_fragment = filter_ref.group_name parent_signature = _resolve_group_parent_signature(filter_ref) @@ -491,7 +491,7 @@ def _set_filter_aliases( current_aliases = getattr(filter_ref, "alias", set()) if set(aliases) == current_aliases: return - setattr(filter_ref, "alias", set(aliases)) + filter_ref.alias = set(aliases) if hasattr(filter_ref, "_cmpl_cmd_names"): filter_ref._cmpl_cmd_names = None diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 058cf61e5..988eeabc5 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -130,6 +130,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ prov = await self.provider_manager.get_provider_by_id(chat_provider_id) if not prov or not isinstance(prov, Provider): @@ -187,6 +188,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ # Import here to avoid circular imports from astrbot.core.astr_agent_context import ( @@ -263,6 +265,7 @@ class Context: Raises: ProviderNotFoundError: 未找到。 + """ prov = self.get_using_provider(umo) if not prov: @@ -294,6 +297,7 @@ class Context: Note: 注册的工具默认是激活状态。 + """ return self.provider_manager.llm_tools.activate_llm_tool(name, star_map) @@ -305,6 +309,7 @@ class Context: Returns: 如果成功停用返回 True,如果没找到工具返回 False。 + """ return self.provider_manager.llm_tools.deactivate_llm_tool(name) @@ -324,11 +329,12 @@ class Context: Note: 如果提供者 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 @@ -360,6 +366,7 @@ class Context: Raises: ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.CHAT_COMPLETION, @@ -369,7 +376,7 @@ class Context: return None if not isinstance(prov, Provider): raise ValueError( - f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}" + f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}", ) return prov @@ -384,6 +391,7 @@ class Context: Raises: ValueError: 返回的提供者不是 TTSProvider 类型。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.TEXT_TO_SPEECH, @@ -404,6 +412,7 @@ class Context: Raises: ValueError: 返回的提供者不是 STTProvider 类型。 + """ prov = self.provider_manager.get_using_provider( provider_type=ProviderType.SPEECH_TO_TEXT, @@ -424,6 +433,7 @@ class Context: Note: 如果不提供 umo 参数,将返回默认配置。 + """ if not umo: # 使用默认配置 @@ -450,6 +460,7 @@ class Context: Note: 当 session 为字符串时,会尝试解析为 MessageSession 对象。(类名为MessageSesion是因为历史遗留拼写错误) qq_official(QQ 官方 API 平台) 不支持此方法。 + """ if isinstance(session, str): try: @@ -462,7 +473,7 @@ class Context: await platform.send_by_session(session, message_chain) return True logger.warning( - f"cannot find platform for session {str(session)}, message not sent" + f"cannot find platform for session {session!s}, message not sent", ) return False @@ -474,6 +485,7 @@ class Context: Note: 如果工具已存在,会替换已存在的工具。 + """ tool_name = {tool.name for tool in self.provider_manager.llm_tools.func_list} module_path = "" @@ -493,7 +505,7 @@ class Context: else: tool.handler_module_path = module_path logger.info( - f"plugin(module_path {module_path}) added LLM tool: {tool.name}" + f"plugin(module_path {module_path}) added LLM tool: {tool.name}", ) if tool.name in tool_name: @@ -518,6 +530,7 @@ class Context: Note: 如果相同路由和方法已注册,会替换现有的 API。 + """ for idx, api in enumerate(self.registered_web_apis): if api[0] == route and methods == api[2]: @@ -545,6 +558,7 @@ class Context: Note: 该方法已经过时,请使用 get_platform_inst 方法。(>= AstrBot v4.0.0) + """ for platform in self.platform_manager.platform_insts: name = platform.meta().name @@ -568,6 +582,7 @@ class Context: Note: 可以通过 event.get_platform_id() 获取平台 ID。 + """ for platform in self.platform_manager.platform_insts: if platform.meta().id == platform_id: @@ -578,6 +593,7 @@ class Context: Returns: 数据库实例。 + """ return self._db @@ -586,6 +602,7 @@ class Context: Args: provider: 提供者实例。 + """ self.provider_manager.provider_insts.append(provider) @@ -608,6 +625,7 @@ class Context: Note: 异步处理函数会接收到额外的关键词参数:event: AstrMessageEvent, context: Context。 该方法已弃用,请使用新的注册方式。 + """ md = StarHandlerMetadata( event_type=EventType.OnLLMRequestEvent, @@ -630,6 +648,7 @@ class Context: Note: 如果再要启用,需要重新注册。 该方法已弃用。 + """ self.provider_manager.llm_tools.remove_func(name) @@ -656,6 +675,7 @@ class Context: Note: 推荐使用装饰器注册指令。该方法将在未来的版本中被移除。 + """ md = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, @@ -683,5 +703,6 @@ class Context: Note: 该方法已弃用。 + """ self._register_tasks.append(task) diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 31949b674..a26cd4c84 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -39,7 +39,7 @@ class CommandFilter(HandlerFilter): parent_command_names: list[str] | None = None, ) -> None: self.command_name = command_name - self.alias = alias if alias else set() + self.alias = alias or set() self._original_command_name = command_name self.parent_command_names = ( parent_command_names if parent_command_names is not None else [""] diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py index 52fb6a452..52f58924a 100755 --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -17,7 +17,7 @@ class CommandGroupFilter(HandlerFilter): parent_group: CommandGroupFilter | None = None, ) -> None: self.group_name = group_name - self.alias = alias if alias else set() + self.alias = alias or set() self._original_group_name = group_name self.sub_command_filters: list[CommandFilter | CommandGroupFilter] = [] self.custom_filter_list: list[CustomFilter] = [] @@ -97,7 +97,7 @@ class CommandGroupFilter(HandlerFilter): prefix + "│ ", event=event, cfg=cfg, - ) + ), ) return "".join(parts) diff --git a/astrbot/core/star/filter/permission.py b/astrbot/core/star/filter/permission.py index a70299fa9..4870025d6 100644 --- a/astrbot/core/star/filter/permission.py +++ b/astrbot/core/star/filter/permission.py @@ -15,7 +15,7 @@ class PermissionType(enum.Flag): class PermissionTypeFilter(HandlerFilter): def __init__( - self, permission_type: PermissionType, raise_error: bool = True + self, permission_type: PermissionType, raise_error: bool = True, ) -> None: self.permission_type = permission_type self.raise_error = raise_error diff --git a/astrbot/core/star/register/__init__.py b/astrbot/core/star/register/__init__.py index 5e99948cd..88b576de9 100644 --- a/astrbot/core/star/register/__init__.py +++ b/astrbot/core/star/register/__init__.py @@ -35,15 +35,15 @@ __all__ = [ "register_on_decorating_result", "register_on_llm_request", "register_on_llm_response", + "register_on_llm_tool_respond", + "register_on_platform_loaded", "register_on_plugin_error", "register_on_plugin_loaded", "register_on_plugin_unloaded", - "register_on_platform_loaded", + "register_on_using_llm_tool", "register_on_waiting_llm_request", "register_permission_type", "register_platform_adapter_type", "register_regex", "register_star", - "register_on_using_llm_tool", - "register_on_llm_tool_respond", ] diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 1385b5056..808cb0999 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -407,7 +407,7 @@ def register_on_waiting_llm_request(**kwargs): def decorator(awaitable): _ = get_handler_or_create( - awaitable, EventType.OnWaitingLLMRequestEvent, **kwargs + awaitable, EventType.OnWaitingLLMRequestEvent, **kwargs, ) return awaitable @@ -554,7 +554,7 @@ def register_llm_tool(name: str | None = None, **kwargs): | Awaitable[MessageEventResult | str | None], ], ): - llm_tool_name = name_ if name_ else awaitable.__name__ + llm_tool_name = name_ or awaitable.__name__ func_doc = awaitable.__doc__ or "" docstring = docstring_parser.parse(func_doc) args = [] diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 1ec0b8366..ea8dfc640 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -136,7 +136,7 @@ async def _install_requirements_with_precheck( if install_plan is None: logger.info( f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): " - f"{requirements_path}" + f"{requirements_path}", ) await pip_installer.install(requirements_path=requirements_path) return @@ -161,7 +161,7 @@ async def _install_requirements_with_precheck( logger.info( f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: " - f"{requirements_path} -> {sorted(install_plan.missing_names)}" + f"{requirements_path} -> {sorted(install_plan.missing_names)}", ) with _temporary_filtered_requirements_file( @@ -189,7 +189,7 @@ class PluginManager: self.plugin_config_path = get_astrbot_config_path() """存储插件配置的路径。data/config""" self.reserved_plugin_path = os.path.join( - get_astrbot_path(), "astrbot", "builtin_stars" + get_astrbot_path(), "astrbot", "builtin_stars", ) """保留插件的路径。在 astrbot/builtin_stars 目录下""" self.conf_schema_fname = "_conf_schema.json" @@ -307,7 +307,7 @@ class PluginManager: return plugins async def _check_plugin_dept_update( - self, target_plugin: str | None = None + self, target_plugin: str | None = None, ) -> bool | None: """检查插件的依赖 如果 target_plugin 为 None,则检查所有插件的依赖 @@ -366,7 +366,7 @@ class PluginManager: install_plan = plan_missing_requirements_install(requirements_path) if install_plan is None: return ImportDependencyRecoveryState( - ImportDependencyRecoveryMode.RECOVER_ON_FAILURE + ImportDependencyRecoveryMode.RECOVER_ON_FAILURE, ) if install_plan.version_mismatch_names: return ImportDependencyRecoveryState( @@ -389,19 +389,19 @@ class PluginManager: ) -> ModuleType | None: 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 + 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 (ImportError, ModuleNotFoundError) as recover_exc: logger.info( - f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}" + f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}", ) return None @@ -422,11 +422,11 @@ class PluginManager: if recovery_state.mode is ImportDependencyRecoveryMode.PRELOAD_AND_RECOVER: try: pip_installer.prefer_installed_dependencies( - requirements_path=requirements_path + requirements_path=requirements_path, ) except Exception as preload_exc: logger.info( - f"插件 {root_dir_name} 预加载已安装依赖失败,将继续常规导入: {preload_exc!s}" + f"插件 {root_dir_name} 预加载已安装依赖失败,将继续常规导入: {preload_exc!s}", ) try: @@ -525,11 +525,11 @@ 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 @@ -701,7 +701,7 @@ class PluginManager: "display_name": metadata.display_name, "support_platforms": metadata.support_platforms, "astrbot_version": metadata.astrbot_version, - } + }, ) except Exception as metadata_error: logger.debug( @@ -736,8 +736,7 @@ class PluginManager: self.failed_plugin_info = "\n".join(lines) + "\n" async def reload_failed_plugin(self, dir_name): - """ - 重新加载未注册(加载失败)的插件 + """重新加载未注册(加载失败)的插件 Args: dir_name (str): 要重载的特定插件名称。 Returns: @@ -745,7 +744,6 @@ class PluginManager: - success (bool): 重载是否成功 - error_message (str|None): 错误信息,成功时为 None """ - async with self._pm_lock: if dir_name not in self.failed_plugin_dict: return False, "插件不存在于失败列表中" @@ -760,8 +758,7 @@ class PluginManager: self.failed_plugin_dict.pop(dir_name, None) self._rebuild_failed_plugin_info() return success, None - else: - return False, error + return False, error async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -956,7 +953,7 @@ class PluginManager: if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) logger.info(metadata) @@ -967,9 +964,9 @@ class PluginManager: # 在实例化前注入类属性,保证插件 __init__ 可读取这些值 if metadata.star_cls_type: - setattr(metadata.star_cls_type, "name", p_name) - setattr(metadata.star_cls_type, "author", p_author) - setattr(metadata.star_cls_type, "plugin_id", plugin_id) + metadata.star_cls_type.name = p_name + metadata.star_cls_type.author = p_author + metadata.star_cls_type.plugin_id = plugin_id if path not in inactivated_plugins: # 只有没有禁用插件时才实例化插件类 @@ -989,9 +986,9 @@ class PluginManager: ) if metadata.star_cls: - setattr(metadata.star_cls, "name", p_name) - setattr(metadata.star_cls, "author", p_author) - setattr(metadata.star_cls, "plugin_id", plugin_id) + metadata.star_cls.name = p_name + metadata.star_cls.author = p_author + metadata.star_cls.plugin_id = plugin_id else: logger.info(f"插件 {metadata.name} 已被禁用。") @@ -1079,7 +1076,7 @@ class PluginManager: if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) metadata.star_cls = obj @@ -1306,7 +1303,7 @@ class PluginManager: self._rebuild_failed_plugin_info() async def install_plugin( - self, repo_url: str, proxy: str = "", ignore_version_check: bool = False + self, repo_url: str, proxy: str = "", ignore_version_check: bool = False, ): """从仓库 URL 安装插件 @@ -1340,7 +1337,7 @@ class PluginManager: plugin_path = os.path.join(self.plugin_store_path, repo_name) if os.path.exists(plugin_path): raise Exception( - f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。", ) plugin_path = await self.updator.install(repo_url, proxy) @@ -1352,7 +1349,7 @@ class PluginManager: metadata_dir_name, ) if target_plugin_path != plugin_path and os.path.exists( - target_plugin_path + target_plugin_path, ): raise Exception(f"安装失败:目录 {metadata_dir_name} 已存在。") if target_plugin_path != plugin_path: @@ -1370,7 +1367,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 @@ -1578,7 +1575,7 @@ class PluginManager: module_prefix = ".".join(plugin_module_path.split(".")[:-1]) if module_prefix: unregistered_adapters = unregister_platform_adapters_by_module( - module_prefix + module_prefix, ) for adapter_name in unregistered_adapters: logger.info( @@ -1725,11 +1722,11 @@ class PluginManager: await self.reload(plugin_name) async def install_plugin_from_file( - self, zip_file_path: str, ignore_version_check: bool = False + self, zip_file_path: str, ignore_version_check: bool = False, ): dir_name = os.path.splitext(os.path.basename(zip_file_path))[0] desti_dir = tempfile.mkdtemp( - dir=self.plugin_store_path, prefix="plugin_upload_" + dir=self.plugin_store_path, prefix="plugin_upload_", ) temp_desti_dir = desti_dir @@ -1761,7 +1758,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 diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index c6c595dfc..b54cb393d 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -20,7 +20,7 @@ class SubAgentOrchestrator: """ def __init__( - self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager + self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager, ) -> None: self._tool_mgr = tool_mgr self._persona_mgr = persona_mgr @@ -68,7 +68,7 @@ class SubAgentOrchestrator: if prompt: instructions = prompt begin_dialogs = copy.deepcopy( - persona_data.get("_begin_dialogs_processed") + persona_data.get("_begin_dialogs_processed"), ) tools = persona_data.get("tools") if public_description == "" and prompt: diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index ccc93172d..eea6b0c4d 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -70,11 +70,11 @@ class FutureTaskTool(FunctionTool[AstrAgentContext]): }, }, "required": ["action"], - } + }, ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: cron_mgr = context.context.context.cron_manager if cron_mgr is None: @@ -219,7 +219,7 @@ class FutureTaskTool(FunctionTool[AstrAgentContext]): lines = [] for j in jobs: lines.append( - f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}" + f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}", ) return "\n".join(lines) diff --git a/astrbot/core/tools/knowledge_base_tools.py b/astrbot/core/tools/knowledge_base_tools.py index e27a883d4..a3b7fb837 100644 --- a/astrbot/core/tools/knowledge_base_tools.py +++ b/astrbot/core/tools/knowledge_base_tools.py @@ -103,11 +103,11 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: query = kwargs.get("query", "") if not query: diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 020c1ad5a..9c4e6ca71 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -71,11 +71,11 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): }, }, "required": ["messages"], - } + }, ) async def _resolve_path_from_sandbox( - self, context: ContextWrapper[AstrAgentContext], path: str + self, context: ContextWrapper[AstrAgentContext], path: str, ) -> tuple[str, bool]: if os.path.exists(path): return path, False @@ -90,7 +90,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if "_&exists_" in json.dumps(result): name = os.path.basename(path) local_path = os.path.join( - get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" + get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}", ) await sb.download_file(path, local_path) logger.info(f"Downloaded file from sandbox: {path} -> {local_path}") @@ -101,7 +101,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return path, False async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: session = kwargs.get("session") or context.context.event.unified_msg_origin messages = kwargs.get("messages") @@ -128,7 +128,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Image.fromFileSystem(path=local_path)) elif url: @@ -140,7 +140,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Record.fromFileSystem(path=local_path)) elif url: @@ -152,7 +152,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Video.fromFileSystem(path=local_path)) elif url: @@ -170,7 +170,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): ) if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.File(name=name, file=local_path)) elif url: diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py index 5ca8c3e08..ee94f4b63 100644 --- a/astrbot/core/tools/web_search_tools.py +++ b/astrbot/core/tools/web_search_tools.py @@ -41,7 +41,7 @@ class _KeyRotator: keys = provider_settings.get(self.setting_name, []) if not keys: raise ValueError( - f"Error: {self.provider_name} API key is not configured in AstrBot." + f"Error: {self.provider_name} API key is not configured in AstrBot.", ) async with self.lock: @@ -62,7 +62,7 @@ def normalize_legacy_web_search_config(cfg) -> None: changed = False if provider_settings.get( - "websearch_provider" + "websearch_provider", ) == "default" and provider_settings.get("web_search", False): provider_settings["web_search"] = False changed = True @@ -109,7 +109,7 @@ def _search_result_payload(results: list[SearchResult]) -> str: "url": f"{result.url}", "snippet": f"{result.snippet}", "index": index, - } + }, ) _cache_favicon(result.url, result.favicon) return json.dumps({"results": ret_ls}, ensure_ascii=False) @@ -124,27 +124,26 @@ async def _tavily_search( "Authorization": f"Bearer {tavily_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.tavily.com/search", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - return [ - SearchResult( - title=item.get("title"), - url=item.get("url"), - snippet=item.get("content"), - favicon=item.get("favicon"), - ) - for item in data.get("results", []) - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.tavily.com/search", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Tavily web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + return [ + SearchResult( + title=item.get("title"), + url=item.get("url"), + snippet=item.get("content"), + favicon=item.get("favicon"), + ) + for item in data.get("results", []) + ] async def _tavily_extract(provider_settings: dict, payload: dict) -> list[dict]: @@ -153,24 +152,23 @@ async def _tavily_extract(provider_settings: dict, payload: dict) -> list[dict]: "Authorization": f"Bearer {tavily_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.tavily.com/extract", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - results: list[dict] = data.get("results", []) - if not results: - raise ValueError( - "Error: Tavily web searcher does not return any results." - ) - return results + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.tavily.com/extract", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Tavily web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + results: list[dict] = data.get("results", []) + if not results: + raise ValueError( + "Error: Tavily web searcher does not return any results.", + ) + return results async def _bocha_search( @@ -182,28 +180,27 @@ async def _bocha_search( "Authorization": f"Bearer {bocha_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.bochaai.com/v1/web-search", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"BoCha web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - rows = data["data"]["webPages"]["value"] - return [ - SearchResult( - title=item.get("name"), - url=item.get("url"), - snippet=item.get("snippet"), - favicon=item.get("siteIcon"), - ) - for item in rows - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.bochaai.com/v1/web-search", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"BoCha web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data["data"]["webPages"]["value"] + return [ + SearchResult( + title=item.get("name"), + url=item.get("url"), + snippet=item.get("snippet"), + favicon=item.get("siteIcon"), + ) + for item in rows + ] async def _brave_search( @@ -215,27 +212,26 @@ async def _brave_search( "Accept": "application/json", "X-Subscription-Token": brave_key, } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.get( - "https://api.search.brave.com/res/v1/web/search", - params=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Brave web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - rows = data.get("web", {}).get("results", []) - return [ - SearchResult( - title=item.get("title", ""), - url=item.get("url", ""), - snippet=item.get("description", ""), - ) - for item in rows - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.get( + "https://api.search.brave.com/res/v1/web/search", + params=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Brave web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data.get("web", {}).get("results", []) + return [ + SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("description", ""), + ) + for item in rows + ] async def _baidu_search( @@ -251,29 +247,28 @@ async def _baidu_search( "X-Appbuilder-Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://qianfan.baidubce.com/v2/ai_search/web_search", - json=payload, - headers=headers, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Baidu AI Search failed: {reason}, status: {response.status}", - ) - data = await response.json() - references = data.get("references", []) - return [ - SearchResult( - title=item.get("title", ""), - url=item.get("url", ""), - snippet=item.get("content", ""), - favicon=item.get("icon"), - ) - for item in references - if item.get("url") - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://qianfan.baidubce.com/v2/ai_search/web_search", + json=payload, + headers=headers, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Baidu AI Search failed: {reason}, status: {response.status}", + ) + data = await response.json() + references = data.get("references", []) + return [ + SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("content", ""), + favicon=item.get("icon"), + ) + for item in references + if item.get("url") + ] @builtin_tool @@ -319,7 +314,7 @@ class TavilyWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -378,7 +373,7 @@ class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]): }, }, "required": ["url"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -444,7 +439,7 @@ class BochaWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -498,7 +493,7 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -507,10 +502,8 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]): return "Error: Brave API key is not configured in AstrBot." count = int(kwargs.get("count", 10)) - if count < 1: - count = 1 - if count > 20: - count = 20 + count = max(count, 1) + count = min(count, 20) payload = { "q": kwargs["query"], @@ -555,7 +548,7 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -564,10 +557,8 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): return "Error: Baidu AI Search API key is not configured in AstrBot." top_k = int(kwargs.get("top_k", 10)) - if top_k < 1: - top_k = 1 - if top_k > 50: - top_k = 50 + top_k = max(top_k, 1) + top_k = min(top_k, 50) payload = { "messages": [{"role": "user", "content": str(kwargs["query"])[:72]}], @@ -592,11 +583,11 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): __all__ = [ + "WEB_SEARCH_TOOL_NAMES", "BaiduWebSearchTool", "BochaWebSearchTool", "BraveWebSearchTool", "TavilyExtractWebPageTool", "TavilyWebSearchTool", - "WEB_SEARCH_TOOL_NAMES", "normalize_legacy_web_search_config", ] diff --git a/astrbot/core/umop_config_router.py b/astrbot/core/umop_config_router.py index c2588e6c2..f734966be 100644 --- a/astrbot/core/umop_config_router.py +++ b/astrbot/core/umop_config_router.py @@ -107,8 +107,8 @@ class UmopConfigRouter: Raises: ValueError: 当 umo 格式不正确时抛出 - """ + """ if self._split_umo(umo) is None: raise ValueError( "umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all", diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index df2cfb82c..e67bf891f 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -150,7 +150,7 @@ class AstrBotUpdator(RepoZipUpdator): if os.environ.get("ASTRBOT_CLI") or os.environ.get("ASTRBOT_LAUNCHER"): raise Exception( - "Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot." + "Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot.", ) # 避免版本管理混乱 if latest: diff --git a/astrbot/core/utils/active_event_registry.py b/astrbot/core/utils/active_event_registry.py index d98cdee37..e6933d04a 100644 --- a/astrbot/core/utils/active_event_registry.py +++ b/astrbot/core/utils/active_event_registry.py @@ -38,6 +38,7 @@ class ActiveEventRegistry: Returns: 被终止的事件数量。 + """ count = 0 for event in list(self._events.get(umo, [])): diff --git a/astrbot/core/utils/core_constraints.py b/astrbot/core/utils/core_constraints.py index b43f00122..4b0c36daf 100644 --- a/astrbot/core/utils/core_constraints.py +++ b/astrbot/core/utils/core_constraints.py @@ -103,7 +103,7 @@ class CoreConstraintsProvider: import tempfile with tempfile.NamedTemporaryFile( - mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8" + mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8", ) as f: f.write("\n".join(constraints)) path = f.name diff --git a/astrbot/core/utils/error_redaction.py b/astrbot/core/utils/error_redaction.py index dcab07ac5..46b977787 100644 --- a/astrbot/core/utils/error_redaction.py +++ b/astrbot/core/utils/error_redaction.py @@ -5,19 +5,19 @@ _SECRET_KEYS = ( ) _JSON_FIELD_PATTERN = re.compile( - rf"(?i)(?P(?P['\"]){_SECRET_KEYS}(?P=kq)\s*:\s*)(?P['\"])(?P[^'\"]+)(?P=vq)" + rf"(?i)(?P(?P['\"]){_SECRET_KEYS}(?P=kq)\s*:\s*)(?P['\"])(?P[^'\"]+)(?P=vq)", ) _AUTH_JSON_FIELD_PATTERN = re.compile( - r"(?i)(?P(?P['\"])authorization(?P=kq)\s*:\s*)(?P['\"])bearer\s+[^'\"]+(?P=vq)" + r"(?i)(?P(?P['\"])authorization(?P=kq)\s*:\s*)(?P['\"])bearer\s+[^'\"]+(?P=vq)", ) _QUERY_FIELD_PATTERN = re.compile( - rf"(?i)(?P{_SECRET_KEYS}\s*=\s*)(?P[^&'\" ]+)" + rf"(?i)(?P{_SECRET_KEYS}\s*=\s*)(?P[^&'\" ]+)", ) _QUERY_PARAM_PATTERN = re.compile( - r"(?i)(?P[?&](?:api_?key|key|access_?token|auth_?token)=)(?P[^&'\" ]+)" + r"(?i)(?P[?&](?:api_?key|key|access_?token|auth_?token)=)(?P[^&'\" ]+)", ) _AUTH_HEADER_PATTERN = re.compile( - r"(?i)(?P\bauthorization\s*:\s*bearer\s+)(?P[A-Za-z0-9._\-]+)" + r"(?i)(?P\bauthorization\s*:\s*bearer\s+)(?P[A-Za-z0-9._\-]+)", ) _BEARER_PATTERN = re.compile(r"(?i)(?P\bbearer\s+)(?P[A-Za-z0-9._\-]+)") _SK_PATTERN = re.compile(r"\bsk-[A-Za-z0-9]{16,}\b") diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index b56592674..c422d12d7 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -99,7 +99,7 @@ async def download_image_by_url( f"SSL certificate verification failed for {url}. " "Disabling SSL verification (CERT_NONE) as a fallback. " "This is insecure and exposes the application to man-in-the-middle attacks. " - "Please investigate and resolve certificate issues." + "Please investigate and resolve certificate issues.", ) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False @@ -163,13 +163,13 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( - "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。" + "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。", ) logger.warning( f"SSL certificate verification failed for {url}. " "Falling back to unverified connection (CERT_NONE). " "This is insecure and exposes the application to man-in-the-middle attacks. " - "Please investigate certificate issues with the remote server." + "Please investigate certificate issues with the remote server.", ) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False diff --git a/astrbot/core/utils/llm_metadata.py b/astrbot/core/utils/llm_metadata.py index ef88e9490..254446bc9 100644 --- a/astrbot/core/utils/llm_metadata.py +++ b/astrbot/core/utils/llm_metadata.py @@ -34,33 +34,32 @@ async def update_llm_metadata() -> None: url = "https://models.dev/api.json" try: async with aiohttp.ClientSession( - trust_env=True, connector=build_tls_connector() - ) as session: - async with session.get(url) as response: - data = await response.json() - global LLM_METADATAS - models = {} - for info in data.values(): - for model in info.get("models", {}).values(): - model_id = model.get("id") - if not model_id: - continue - models[model_id] = LLMMetadata( - id=model_id, - reasoning=model.get("reasoning", False), - tool_call=model.get("tool_call", False), - knowledge=model.get("knowledge", "none"), - release_date=model.get("release_date", ""), - modalities=model.get( - "modalities", {"input": [], "output": []} - ), - open_weights=model.get("open_weights", False), - limit=model.get("limit", {"context": 0, "output": 0}), - ) - # Replace the global cache in-place so references remain valid - LLM_METADATAS.clear() - LLM_METADATAS.update(models) - logger.info(f"Successfully fetched metadata for {len(models)} LLMs.") + trust_env=True, connector=build_tls_connector(), + ) as session, session.get(url) as response: + data = await response.json() + global LLM_METADATAS + models = {} + for info in data.values(): + for model in info.get("models", {}).values(): + model_id = model.get("id") + if not model_id: + continue + models[model_id] = LLMMetadata( + id=model_id, + reasoning=model.get("reasoning", False), + tool_call=model.get("tool_call", False), + knowledge=model.get("knowledge", "none"), + release_date=model.get("release_date", ""), + modalities=model.get( + "modalities", {"input": [], "output": []}, + ), + open_weights=model.get("open_weights", False), + limit=model.get("limit", {"context": 0, "output": 0}), + ) + # Replace the global cache in-place so references remain valid + LLM_METADATAS.clear() + LLM_METADATAS.update(models) + logger.info(f"Successfully fetched metadata for {len(models)} LLMs.") except Exception as e: logger.error(f"Failed to fetch LLM metadata: {e}") return diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index 40f1e6049..df2b34bda 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -30,6 +30,7 @@ async def get_media_duration(file_path: str) -> int | None: Returns: 时长(毫秒),如果获取失败返回None + """ try: # 使用ffprobe获取时长 @@ -53,13 +54,12 @@ async def get_media_duration(file_path: str) -> int | None: duration_ms = int(duration_seconds * 1000) logger.debug(f"[Media Utils] 获取媒体时长: {duration_ms}ms") return duration_ms - else: - logger.warning(f"[Media Utils] 无法获取媒体文件时长: {file_path}") - return None + logger.warning(f"[Media Utils] 无法获取媒体文件时长: {file_path}") + return 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: @@ -79,6 +79,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) Raises: Exception: 转换失败时抛出异常 + """ # 如果已经是opus格式,直接返回 if audio_path.lower().endswith(".opus"): @@ -121,7 +122,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) try: os.remove(output_path) logger.debug( - f"[Media Utils] 已清理失败的opus输出文件: {output_path}" + f"[Media Utils] 已清理失败的opus输出文件: {output_path}", ) except OSError as e: logger.warning(f"[Media Utils] 清理失败的opus输出文件时出错: {e}") @@ -135,7 +136,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: @@ -144,7 +145,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) async def convert_video_format( - video_path: str, output_format: str = "mp4", output_path: str | None = None + video_path: str, output_format: str = "mp4", output_path: str | None = None, ) -> str: """使用ffmpeg转换视频格式 @@ -158,6 +159,7 @@ async def convert_video_format( Raises: Exception: 转换失败时抛出异常 + """ # 如果已经是目标格式,直接返回 if video_path.lower().endswith(f".{output_format}"): @@ -196,11 +198,11 @@ async def convert_video_format( try: os.remove(output_path) logger.debug( - f"[Media Utils] 已清理失败的{output_format}输出文件: {output_path}" + f"[Media Utils] 已清理失败的{output_format}输出文件: {output_path}", ) except OSError as e: logger.warning( - f"[Media Utils] 清理失败的{output_format}输出文件时出错: {e}" + f"[Media Utils] 清理失败的{output_format}输出文件时出错: {e}", ) error_msg = stderr.decode() if stderr else "未知错误" @@ -212,7 +214,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: @@ -234,6 +236,7 @@ async def convert_audio_format( Returns: 转换后的音频文件路径 + """ if audio_path.lower().endswith(f".{output_format}"): return audio_path @@ -246,9 +249,7 @@ async def convert_audio_format( args = ["ffmpeg", "-y", "-i", audio_path] if output_format == "amr": args.extend(["-ac", "1", "-ar", "8000", "-ab", "12.2k"]) - elif output_format == "ogg": - args.extend(["-acodec", "libopus", "-ac", "1", "-ar", "16000"]) - elif output_format == "opus": + elif output_format == "ogg" or output_format == "opus": args.extend(["-acodec", "libopus", "-ac", "1", "-ar", "16000"]) args.append(output_path) @@ -296,7 +297,6 @@ async def ensure_wav(audio_path: str, output_path: str | None = None) -> str: If the file appears to already be wav, return it directly to avoid extra conversion. """ - if not audio_path: return audio_path @@ -430,6 +430,7 @@ async def compress_image( Returns: The compressed image path. Returns the original path if compression fails or the source does not need compression. + """ max_size = max(int(max_size), 1) quality = min(max(int(quality), 1), 100) @@ -439,7 +440,7 @@ async def compress_image( # Skip compression for remote images and return the original value. if url_or_path.startswith("http"): return url_or_path - elif url_or_path.startswith("data:image"): + if url_or_path.startswith("data:image"): _header, encoded = url_or_path.split(",", 1) data = base64.b64decode(encoded) if len(data) < min_file_size_bytes: diff --git a/astrbot/core/utils/migra_helper.py b/astrbot/core/utils/migra_helper.py index 40b899620..e1c4c8367 100644 --- a/astrbot/core/utils/migra_helper.py +++ b/astrbot/core/utils/migra_helper.py @@ -12,8 +12,7 @@ from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_sess def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None: - """ - Migra agent runner configs from provider configs. + """Migra agent runner configs from provider configs. """ try: default_prov_id = conf["provider_settings"]["default_provider_id"] @@ -43,8 +42,7 @@ def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None: def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None: - """ - Migrate old provider structure to new provider-source separation. + """Migrate old provider structure to new provider-source separation. Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body All other fields move to provider_sources. """ @@ -129,10 +127,9 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None: async def migra( - db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager + db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager, ) -> None: - """ - Stores the migration logic here. + """Stores the migration logic here. btw, i really don't like migration :( """ # 4.5 to 4.6 migration for umop_config_router diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 727f3762a..dad6465ba 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -16,6 +16,7 @@ def is_connection_error(exc: BaseException) -> bool: Returns: True if the exception is a connection/network error + """ # Check for httpx network errors if isinstance( @@ -59,6 +60,7 @@ def log_connection_failure( provider_label: The provider name for log prefix (e.g., "OpenAI", "Gemini") error: The exception that occurred proxy: The proxy address if configured, or None/empty string + """ import os @@ -68,13 +70,13 @@ def log_connection_failure( effective_proxy = proxy if not effective_proxy: effective_proxy = os.environ.get( - "http_proxy", os.environ.get("https_proxy", "") + "http_proxy", os.environ.get("https_proxy", ""), ) if effective_proxy: logger.error( f"[{provider_label}] 网络/代理连接失败 ({error_type})。" - f"代理地址: {effective_proxy},错误: {error}" + f"代理地址: {effective_proxy},错误: {error}", ) else: logger.error(f"[{provider_label}] 网络连接失败 ({error_type})。错误: {error}") @@ -95,6 +97,7 @@ def create_proxy_client( Returns: An httpx.AsyncClient configured with the proxy, or None if no proxy + """ if proxy: logger.info(f"[{provider_label}] 使用代理: {proxy}") diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index e5f713820..fd3197eb4 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -45,7 +45,7 @@ _PIP_FAILURE_PATTERNS = { "dependency_detail": re.compile(r"\bdepends on\b", re.IGNORECASE), } _SENSITIVE_PIP_VALUE_KEYS = frozenset( - {"password", "passwd", "pass", "api_token", "token", "auth_token"} + {"password", "passwd", "pass", "api_token", "token", "auth_token"}, ) _MAX_PIP_OUTPUT_LINES = 200 @@ -54,7 +54,7 @@ class DependencyConflictError(Exception): """Raised when pip encounters a dependency conflict.""" def __init__( - self, message: str, errors: list[str], *, is_core_conflict: bool + self, message: str, errors: list[str], *, is_core_conflict: bool, ) -> None: super().__init__(message) self.errors = errors @@ -90,7 +90,7 @@ def _get_pip_main(): "pip module is unavailable " f"(sys.executable={sys.executable}, " f"frozen={getattr(sys, 'frozen', False)}, " - f"ASTRBOT_DESKTOP_CLIENT={os.environ.get('ASTRBOT_DESKTOP_CLIENT')})" + f"ASTRBOT_DESKTOP_CLIENT={os.environ.get('ASTRBOT_DESKTOP_CLIENT')})", ) from exc return pip_main @@ -330,7 +330,7 @@ def _build_packaged_windows_runtime_build_env( return {} include_dir = _normalize_windows_native_build_path( - ntpath.join(runtime_dir, "include") + ntpath.join(runtime_dir, "include"), ) libs_dir = _normalize_windows_native_build_path(ntpath.join(runtime_dir, "libs")) include_exists = os.path.isdir(include_dir) @@ -559,7 +559,7 @@ def _ensure_preferred_modules( site_packages_path: str, ) -> None: unresolved_prefer_reasons = _prefer_modules_from_site_packages( - module_names, site_packages_path + module_names, site_packages_path, ) unresolved_modules: list[str] = [] @@ -619,7 +619,7 @@ def _is_module_loaded_from_site_packages( def _prefer_module_from_site_packages( - module_name: str, site_packages_path: str + module_name: str, site_packages_path: str, ) -> bool: with _SITE_PACKAGES_IMPORT_LOCK: base_path = os.path.join(site_packages_path, *module_name.split(".")) @@ -886,12 +886,12 @@ def _patch_distlib_finder_for_frozen_runtime() -> None: if not isinstance(finder_registry, dict): logger.warning( - "Skip patching distlib finder because _finder_registry is unavailable." + "Skip patching distlib finder because _finder_registry is unavailable.", ) return if not callable(register_finder) or resource_finder is None: logger.warning( - "Skip patching distlib finder because register API is unavailable." + "Skip patching distlib finder because register API is unavailable.", ) return @@ -914,7 +914,7 @@ def _patch_distlib_finder_for_frozen_runtime() -> None: package_name, ): finder_registry = getattr( - distlib_resources, "_finder_registry", finder_registry + distlib_resources, "_finder_registry", finder_registry, ) @@ -944,7 +944,7 @@ class PipInstaller: if package_name and normalized_requirements_path: raise ValueError( - "package_name and requirements_path cannot be used together" + "package_name and requirements_path cannot be used together", ) if package_name: @@ -955,7 +955,7 @@ class PipInstaller: elif normalized_requirements_path: args = ["install", "-r", normalized_requirements_path] requested_requirements = extract_requirement_names( - normalized_requirements_path + normalized_requirements_path, ) if not args: @@ -985,7 +985,7 @@ class PipInstaller: allow_target_upgrade: bool = True, ) -> None: args, requested_requirements = self._build_pip_args( - package_name, requirements_path, mirror + package_name, requirements_path, mirror, ) if not args: logger.info("Pip 包管理器跳过安装:未提供有效的包名或 requirements 文件。") @@ -1005,7 +1005,7 @@ class PipInstaller: "--upgrade", "--upgrade-strategy", "only-if-needed", - ] + ], ) with self._core_constraints.constraints_file() as constraints_file_path: diff --git a/astrbot/core/utils/quoted_message/__init__.py b/astrbot/core/utils/quoted_message/__init__.py index 8421898fd..a9e24391c 100644 --- a/astrbot/core/utils/quoted_message/__init__.py +++ b/astrbot/core/utils/quoted_message/__init__.py @@ -3,6 +3,6 @@ from __future__ import annotations from .extractor import extract_quoted_message_images, extract_quoted_message_text __all__ = [ - "extract_quoted_message_text", "extract_quoted_message_images", + "extract_quoted_message_text", ] diff --git a/astrbot/core/utils/quoted_message/chain_parser.py b/astrbot/core/utils/quoted_message/chain_parser.py index 528ce14b8..7847061bd 100644 --- a/astrbot/core/utils/quoted_message/chain_parser.py +++ b/astrbot/core/utils/quoted_message/chain_parser.py @@ -89,7 +89,7 @@ def _extract_image_refs_from_component_chain( seg, depth=depth + 1, settings=settings, - ) + ), ) elif isinstance(seg, Node): image_refs.extend( @@ -97,7 +97,7 @@ def _extract_image_refs_from_component_chain( seg.content, depth=depth + 1, settings=settings, - ) + ), ) elif isinstance(seg, Nodes): for node in seg.nodes: @@ -106,7 +106,7 @@ def _extract_image_refs_from_component_chain( node.content, depth=depth + 1, settings=settings, - ) + ), ) return normalize_and_dedupe_strings(image_refs) @@ -308,7 +308,7 @@ def _parse_onebot_segments( isinstance(candidate_file, str) and candidate_file.strip() and looks_like_image_file_name( - seg_data.get("name") or seg_data.get("file_name") or candidate_file + seg_data.get("name") or seg_data.get("file_name") or candidate_file, ) ): image_refs.append(candidate_file.strip()) diff --git a/astrbot/core/utils/quoted_message/extractor.py b/astrbot/core/utils/quoted_message/extractor.py index 83570d66c..3a3ad5e69 100644 --- a/astrbot/core/utils/quoted_message/extractor.py +++ b/astrbot/core/utils/quoted_message/extractor.py @@ -93,14 +93,14 @@ class QuotedMessageExtractor: fetch_remote: bool, ) -> QuotedMessageContent | None: reply = reply_component or self._reply_parser.find_first_reply_component( - self._event + self._event, ) if not reply: return None embedded_text = self._reply_parser.extract_text_from_reply_component(reply) embedded_image_refs = list( - self._reply_parser.extract_image_refs_from_reply_component(reply) + self._reply_parser.extract_image_refs_from_reply_component(reply), ) reply_id = getattr(reply, "id", None) @@ -156,7 +156,7 @@ class QuotedMessageExtractor: if ( embedded_content.embedded_text and not self._reply_parser.is_forward_placeholder_only_text( - embedded_content.embedded_text + embedded_content.embedded_text, ) ): return embedded_content.embedded_text @@ -197,7 +197,7 @@ async def extract_quoted_message_text( settings: QuotedMessageParserSettings | None = None, ) -> str | None: return await QuotedMessageExtractor(event, settings=settings or SETTINGS).text( - reply_component + reply_component, ) @@ -207,5 +207,5 @@ async def extract_quoted_message_images( settings: QuotedMessageParserSettings | None = None, ) -> list[str]: return await QuotedMessageExtractor(event, settings=settings or SETTINGS).images( - reply_component + reply_component, ) diff --git a/astrbot/core/utils/quoted_message/image_resolver.py b/astrbot/core/utils/quoted_message/image_resolver.py index 5a4c21fb2..b777cd2dd 100644 --- a/astrbot/core/utils/quoted_message/image_resolver.py +++ b/astrbot/core/utils/quoted_message/image_resolver.py @@ -36,7 +36,7 @@ def _build_image_resolve_actions( ("get_image", {"image": candidate}), ("get_file", {"file_id": candidate}), ("get_file", {"file": candidate}), - ] + ], ) try: @@ -53,7 +53,7 @@ def _build_image_resolve_actions( ( "get_group_file_url", {"group_id": group_id_value, "file_id": candidate}, - ) + ), ) for candidate in candidates: actions.append(("get_private_file_url", {"file_id": candidate})) diff --git a/astrbot/core/utils/quoted_message_parser.py b/astrbot/core/utils/quoted_message_parser.py index fa6ac18dd..c14e7f884 100644 --- a/astrbot/core/utils/quoted_message_parser.py +++ b/astrbot/core/utils/quoted_message_parser.py @@ -6,6 +6,6 @@ from astrbot.core.utils.quoted_message.extractor import ( ) __all__ = [ - "extract_quoted_message_text", "extract_quoted_message_images", + "extract_quoted_message_text", ] diff --git a/astrbot/core/utils/requirements_utils.py b/astrbot/core/utils/requirements_utils.py index 969976a4f..2a0f821cd 100644 --- a/astrbot/core/utils/requirements_utils.py +++ b/astrbot/core/utils/requirements_utils.py @@ -20,7 +20,6 @@ logger = logging.getLogger("astrbot") class RequirementsPrecheckFailed(Exception): """Raised when the pre-check of requirements fails.""" - pass @dataclass(frozen=True) @@ -66,7 +65,7 @@ def _looks_like_local_path_reference(token: str) -> bool: if not candidate: return False return candidate in {".", ".."} or candidate.startswith( - ("./", "../", "/", "~/", ".\\", "..\\", "\\") + ("./", "../", "/", "~/", ".\\", "..\\", "\\"), ) @@ -196,7 +195,7 @@ def _extract_requirement_names_from_package_tokens(tokens: list[str]) -> frozens "--trusted-host=", "--requirement=", "--constraint=", - ) + ), ): continue @@ -237,7 +236,7 @@ def parse_package_install_input(raw_input: str) -> ParsedPackageInput: continue specs.extend(tokens) requirement_names.update( - _extract_requirement_names_from_package_tokens(tokens) + _extract_requirement_names_from_package_tokens(tokens), ) continue @@ -260,7 +259,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) @@ -390,7 +389,7 @@ def _load_requirement_lines_for_precheck( def find_missing_requirements(requirements_path: str) -> set[str] | None: can_precheck, requirement_lines = _load_requirement_lines_for_precheck( - requirements_path + requirements_path, ) if not can_precheck or requirement_lines is None: return None @@ -466,7 +465,7 @@ def plan_missing_requirements_install( requirements_path: str, ) -> MissingRequirementsPlan | None: can_precheck, requirement_lines = _load_requirement_lines_for_precheck( - requirements_path + requirements_path, ) if not can_precheck or requirement_lines is None: return None diff --git a/astrbot/core/utils/session_lock.py b/astrbot/core/utils/session_lock.py index 732a29b72..684fc0240 100644 --- a/astrbot/core/utils/session_lock.py +++ b/astrbot/core/utils/session_lock.py @@ -36,7 +36,7 @@ class SessionLockManager: def __init__(self) -> None: self._state_guard = threading.Lock() self._loop_managers: weakref.WeakKeyDictionary[ - asyncio.AbstractEventLoop, _PerLoopSessionLockManager + asyncio.AbstractEventLoop, _PerLoopSessionLockManager, ] = weakref.WeakKeyDictionary() def _get_loop_manager(self) -> _PerLoopSessionLockManager: diff --git a/astrbot/core/utils/shared_preferences.py b/astrbot/core/utils/shared_preferences.py index 344808cbd..d8b8226f7 100644 --- a/astrbot/core/utils/shared_preferences.py +++ b/astrbot/core/utils/shared_preferences.py @@ -32,7 +32,7 @@ class SharedPreferences: self._scheduler = BackgroundScheduler() self._scheduler.add_job( - self._clear_temporary_cache, "interval", hours=24, id="clear_sp_temp_cache" + self._clear_temporary_cache, "interval", hours=24, id="clear_sp_temp_cache", ) self._scheduler.start() @@ -203,7 +203,7 @@ class SharedPreferences: return result def put( - self, key, value, scope: str | None = None, scope_id: str | None = None + self, key, value, scope: str | None = None, scope_id: str | None = None, ) -> None: """设置偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( @@ -212,7 +212,7 @@ class SharedPreferences: ).result() def remove( - self, key, scope: str | None = None, scope_id: str | None = None + self, key, scope: str | None = None, scope_id: str | None = None, ) -> None: """删除偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( diff --git a/astrbot/core/utils/temp_dir_cleaner.py b/astrbot/core/utils/temp_dir_cleaner.py index c0c060098..5b9a39515 100644 --- a/astrbot/core/utils/temp_dir_cleaner.py +++ b/astrbot/core/utils/temp_dir_cleaner.py @@ -7,7 +7,7 @@ from astrbot import logger from astrbot.core.utils.astrbot_path import get_astrbot_temp_path -def parse_size_to_bytes(value: str | int | float | None) -> int: +def parse_size_to_bytes(value: str | float | None) -> int: """Parse size in MB to bytes.""" if value is None: return 0 @@ -72,7 +72,7 @@ class TempDirCleaner: continue total_size += stat.st_size files.append( - TempFileInfo(path=path, size=stat.st_size, mtime=stat.st_mtime) + TempFileInfo(path=path, size=stat.st_size, mtime=stat.st_mtime), ) return total_size, files @@ -81,7 +81,7 @@ class TempDirCleaner: if not self._temp_dir.exists(): return for path in sorted( - self._temp_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True + self._temp_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True, ): if not path.is_dir(): continue diff --git a/astrbot/core/utils/webhook_utils.py b/astrbot/core/utils/webhook_utils.py index 40dada3cb..cac96b4c3 100644 --- a/astrbot/core/utils/webhook_utils.py +++ b/astrbot/core/utils/webhook_utils.py @@ -23,7 +23,7 @@ def _get_dashboard_port() -> int: def _is_dashboard_ssl_enabled() -> bool: env_ssl = os.environ.get("DASHBOARD_SSL_ENABLE") or os.environ.get( - "ASTRBOT_DASHBOARD_SSL_ENABLE" + "ASTRBOT_DASHBOARD_SSL_ENABLE", ) if env_ssl is not None: return env_ssl.strip().lower() in {"1", "true", "yes", "on"} @@ -41,8 +41,8 @@ def log_webhook_info(platform_name: str, webhook_uuid: str) -> None: Args: platform_name: 平台名称 webhook_uuid: webhook 的 UUID - """ + """ callback_base = _get_callback_api_base() if not callback_base: @@ -74,6 +74,7 @@ def ensure_platform_webhook_config(platform_cfg: dict) -> bool: Returns: 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..65b5c4b86 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -150,7 +150,7 @@ class RepoZipUpdator: ) async def download_from_repo_url( - self, target_path: str, repo_url: str, proxy="" + self, target_path: str, repo_url: str, proxy="", ) -> None: author, repo, branch = self.parse_github_url(repo_url) diff --git a/astrbot/dashboard/routes/__init__.py b/astrbot/dashboard/routes/__init__.py index fbbd0c7a0..4c2b9211e 100644 --- a/astrbot/dashboard/routes/__init__.py +++ b/astrbot/dashboard/routes/__init__.py @@ -40,10 +40,10 @@ __all__ = [ "PlatformRoute", "PluginRoute", "SessionManagementRoute", + "SkillsRoute", "StatRoute", "StaticFileRoute", "SubAgentRoute", "ToolsRoute", - "SkillsRoute", "UpdateRoute", ] diff --git a/astrbot/dashboard/routes/api_key.py b/astrbot/dashboard/routes/api_key.py index 4b957fe8e..e5d5e6a20 100644 --- a/astrbot/dashboard/routes/api_key.py +++ b/astrbot/dashboard/routes/api_key.py @@ -99,7 +99,7 @@ class ApiKeyRoute(Route): Response().error("expires_in_days must be greater than 0").__dict__ ) expires_at = datetime.now(timezone.utc) + timedelta( - days=expires_in_days_int + days=expires_in_days_int, ) raw_key = f"abk_{secrets.token_urlsafe(32)}" diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index ecc5dbfc8..ca08d7faa 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -40,6 +40,7 @@ def secure_filename(filename: str) -> str: Returns: 安全的文件名 + """ # 跨平台处理:先将反斜杠替换为正斜杠,再取文件名 filename = filename.replace("\\", "/") @@ -70,6 +71,7 @@ def generate_unique_filename(original_filename: str) -> str: Returns: 添加了时间戳后缀的唯一文件名,格式为 {原文件名}_{YYYYMMDD_HHMMSS}.{扩展名} + """ name, ext = os.path.splitext(original_filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -184,7 +186,7 @@ class BackupRoute(Route): """创建进度回调函数""" async def _callback( - stage: str, current: int, total: int, message: str = "" + stage: str, current: int, total: int, message: str = "", ) -> None: self._update_progress( task_id, @@ -202,7 +204,7 @@ class BackupRoute(Route): if self._cleanup_task is None or self._cleanup_task.done(): try: self._cleanup_task = asyncio.create_task( - self._cleanup_expired_uploads() + self._cleanup_expired_uploads(), ) except RuntimeError: # 如果没有运行中的事件循环,跳过(等待下次异步调用时启动) @@ -255,15 +257,15 @@ class BackupRoute(Route): Returns: dict | None: manifest 内容,如果不是有效备份则返回 None + """ try: with zipfile.ZipFile(zip_path, "r") as zf: if "manifest.json" in zf.namelist(): manifest_data = zf.read("manifest.json") return json.loads(manifest_data.decode("utf-8")) - else: - # 没有 manifest.json,不是有效的 AstrBot 备份 - return None + # 没有 manifest.json,不是有效的 AstrBot 备份 + return None except Exception as e: logger.debug(f"读取备份 manifest 失败: {e}") return None # 无法读取,不是有效备份 @@ -310,11 +312,11 @@ class BackupRoute(Route): "size": stat.st_size, "created_at": stat.st_mtime, "type": manifest.get( - "origin", "exported" + "origin", "exported", ), # 老版本没有 origin 默认为 exported "astrbot_version": manifest.get("astrbot_version", "未知"), "exported_at": manifest.get("exported_at"), - } + }, ) # 按创建时间倒序排序 @@ -333,7 +335,7 @@ class BackupRoute(Route): "total": len(backup_files), "page": page, "page_size": page_size, - } + }, ) .__dict__ ) @@ -364,7 +366,7 @@ class BackupRoute(Route): { "task_id": task_id, "message": "export task created, processing in background", - } + }, ) .__dict__ ) @@ -442,7 +444,7 @@ class BackupRoute(Route): await file.save(zip_path) logger.info( - f"上传的备份文件已保存: {unique_filename} (原始名称: {file.filename})" + f"上传的备份文件已保存: {unique_filename} (原始名称: {file.filename})", ) return ( @@ -452,7 +454,7 @@ class BackupRoute(Route): "filename": unique_filename, "original_filename": file.filename, "size": os.path.getsize(zip_path), - } + }, ) .__dict__ ) @@ -520,7 +522,7 @@ class BackupRoute(Route): logger.info( f"初始化分片上传: upload_id={upload_id}, " - f"filename={unique_filename}, total_chunks={total_chunks}" + f"filename={unique_filename}, total_chunks={total_chunks}", ) return ( @@ -531,7 +533,7 @@ class BackupRoute(Route): "chunk_size": CHUNK_SIZE, "total_chunks": total_chunks, "filename": unique_filename, - } + }, ) .__dict__ ) @@ -596,7 +598,7 @@ class BackupRoute(Route): logger.debug( f"接收分片: upload_id={upload_id}, " - f"chunk={chunk_index + 1}/{total_chunks}" + f"chunk={chunk_index + 1}/{total_chunks}", ) return ( @@ -606,7 +608,7 @@ class BackupRoute(Route): "received": received_count, "total": total_chunks, "chunk_index": chunk_index, - } + }, ) .__dict__ ) @@ -623,6 +625,7 @@ class BackupRoute(Route): Args: zip_path: ZIP 文件路径 + """ try: # 读取原有 manifest @@ -706,7 +709,7 @@ class BackupRoute(Route): self._mark_backup_as_uploaded(output_path) logger.info( - f"分片上传完成: {filename}, size={file_size}, chunks={total}" + f"分片上传完成: {filename}, size={file_size}, chunks={total}", ) # 清理分片目录 @@ -719,7 +722,7 @@ class BackupRoute(Route): "filename": filename, "original_filename": session["original_filename"], "size": file_size, - } + }, ) .__dict__ ) @@ -859,7 +862,7 @@ class BackupRoute(Route): { "task_id": task_id, "message": "import task created, processing in background", - } + }, ) .__dict__ ) @@ -1065,8 +1068,7 @@ class BackupRoute(Route): new_name = secure_filename(new_name) # 移除新文件名中的扩展名(如果有的话) - if new_name.endswith(".zip"): - new_name = new_name[:-4] + new_name = new_name.removesuffix(".zip") # 验证新文件名不为空 if not new_name or new_name.replace("_", "") == "": @@ -1096,7 +1098,7 @@ class BackupRoute(Route): { "old_filename": filename, "new_filename": new_filename, - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 5e6f77db9..cf975d8fb 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -107,7 +107,7 @@ class ChatRoute(Route): if not os.path.exists(real_file_path): # try legacy file_path = os.path.join( - self.legacy_img_dir, os.path.basename(filename) + self.legacy_img_dir, os.path.basename(filename), ) if os.path.exists(file_path): real_file_path = os.path.realpath(file_path) @@ -187,7 +187,7 @@ class ChatRoute(Route): "attachment_id": attachment.attachment_id, "filename": filename, "type": attach_type, - } + }, ) .__dict__ ) @@ -201,7 +201,7 @@ class ChatRoute(Route): ) async def _create_attachment_from_file( - self, filename: str, attach_type: str + self, filename: str, attach_type: str, ) -> dict | None: """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( @@ -213,7 +213,7 @@ class ChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, accumulated_text: str, accumulated_parts: list, ) -> dict: """从消息中提取 web_search_tavily 的引用 @@ -223,6 +223,7 @@ class ChatRoute(Route): Returns: 包含 used 列表的字典,记录被引用的搜索结果 + """ supported = [ "web_search_baidu", @@ -241,7 +242,7 @@ class ChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -369,7 +370,7 @@ class ChatRoute(Route): async with track_conversation(self.running_convs, webchat_conv_id): while True: result, should_break = await _poll_webchat_stream_result( - back_queue, username + back_queue, username, ) if should_break: client_disconnected = True @@ -410,7 +411,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 @@ -430,7 +431,7 @@ class ChatRoute(Route): if accumulated_text: # 如果累积了文本,则先保存文本 accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -443,7 +444,7 @@ class ChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -455,14 +456,14 @@ class ChatRoute(Route): elif msg_type == "image": filename = result_text.replace("[IMAGE]", "") part = await self._create_attachment_from_file( - filename, "image" + filename, "image", ) if part: accumulated_parts.append(part) elif msg_type == "record": filename = result_text.replace("[RECORD]", "") part = await self._create_attachment_from_file( - filename, "record" + filename, "record", ) if part: accumulated_parts.append(part) @@ -470,7 +471,7 @@ class ChatRoute(Route): # 格式: [FILE]filename filename = result_text.replace("[FILE]", "") part = await self._create_attachment_from_file( - filename, "file" + filename, "file", ) if part: accumulated_parts.append(part) @@ -515,7 +516,7 @@ class ChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, } @@ -561,7 +562,7 @@ class ChatRoute(Route): ) response = cast( - QuartResponse, + "QuartResponse", await make_response( stream(), { @@ -708,7 +709,7 @@ class ChatRoute(Route): "deleted_count": deleted_count, "failed_count": len(failed_items), "failed_items": failed_items, - } + }, ) .__dict__ ) @@ -737,7 +738,7 @@ class ChatRoute(Route): os.remove(attachment.path) except OSError as e: logger.warning( - f"Failed to delete attachment file {attachment.path}: {e}" + f"Failed to delete attachment file {attachment.path}: {e}", ) except Exception as e: logger.warning(f"Failed to get attachments: {e}") @@ -768,7 +769,7 @@ class ChatRoute(Route): data={ "session_id": session.session_id, "platform_id": session.platform_id, - } + }, ) .__dict__ ) @@ -802,7 +803,7 @@ class ChatRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return Response().ok(data=sessions_data).__dict__ @@ -820,7 +821,7 @@ class ChatRoute(Route): # 获取项目信息(如果会话属于某个项目) username = g.get("username", "guest") project_info = await self.db.get_project_by_session( - session_id=session_id, creator=username + session_id=session_id, creator=username, ) # Get platform message history using session_id diff --git a/astrbot/dashboard/routes/chatui_project.py b/astrbot/dashboard/routes/chatui_project.py index 6ba570f55..487fab53d 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -54,7 +54,7 @@ class ChatUIProjectRoute(Route): "description": project.description, "created_at": to_utc_isoformat(project.created_at), "updated_at": to_utc_isoformat(project.updated_at), - } + }, ) .__dict__ ) @@ -105,7 +105,7 @@ class ChatUIProjectRoute(Route): "description": project.description, "created_at": to_utc_isoformat(project.created_at), "updated_at": to_utc_isoformat(project.updated_at), - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bcd7e075c..5d5e606a5 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -63,7 +63,7 @@ def _expect_type(value, expected_type, path_key, errors, expected_name=None) -> if not isinstance(value, expected_type): errors.append( f"错误的类型 {path_key}: 期望是 {expected_name or expected_type.__name__}, " - f"得到了 {type(value).__name__}" + f"得到了 {type(value).__name__}", ) return False return True @@ -283,16 +283,15 @@ async def _validate_neo_connectivity( health_url = f"{endpoint}/health" try: - async with aiohttp.ClientSession() as session: - async with session.get( - health_url, - timeout=aiohttp.ClientTimeout(total=5), - ) as resp: - if resp.status != 200: - return ( - f"⚠️ Bay 健康检查失败 (HTTP {resp.status})," - f"请确认 Bay 正在运行: {endpoint}" - ) + async with aiohttp.ClientSession() as session, session.get( + health_url, + timeout=aiohttp.ClientTimeout(total=5), + ) as resp: + if resp.status != 200: + return ( + f"⚠️ Bay 健康检查失败 (HTTP {resp.status})," + f"请确认 Bay 正在运行: {endpoint}" + ) except Exception: return f"⚠️ 无法连接 Bay ({endpoint}),请确认 Bay 已启动。" @@ -300,7 +299,7 @@ async def _validate_neo_connectivity( def save_config( - post_config: dict, config: AstrBotConfig, is_core: bool = False + post_config: dict, config: AstrBotConfig, is_core: bool = False, ) -> None: """验证并保存配置""" errors = None @@ -319,7 +318,7 @@ def save_config( ) else: errors, post_config = validate_config( - post_config, getattr(config, "schema", {}), is_core + post_config, getattr(config, "schema", {}), is_core, ) except BaseException as e: logger.error(traceback.format_exc()) @@ -418,7 +417,7 @@ class ConfigRoute(Route): # 删除引用了该 provider_source 的 providers await self.core_lifecycle.provider_manager.delete_provider( - provider_source_id=provider_source_id + provider_source_id=provider_source_id, ) try: @@ -514,13 +513,13 @@ class ConfigRoute(Route): "metadata": { "provider": CONFIG_METADATA_2["provider_group"]["metadata"][ "provider" - ] - } - } - } + ], + }, + }, + }, ) config_schema = { - "provider": provider_metadata["provider_group"]["metadata"]["provider"] + "provider": provider_metadata["provider_group"]["metadata"]["provider"], } data = { "config_schema": config_schema, @@ -626,7 +625,7 @@ class ConfigRoute(Route): if system_config: abconf = self.acm.confs["default"] metadata = ConfigMetadataI18n.convert_to_i18n_keys( - CONFIG_METADATA_3_SYSTEM + CONFIG_METADATA_3_SYSTEM, ) return Response().ok({"config": abconf, "metadata": metadata}).__dict__ if abconf_id is None: @@ -792,7 +791,7 @@ class ConfigRoute(Route): ): # chat prov = self.core_lifecycle.provider_manager.get_merged_provider_config( - provider + provider, ) provider_list.append(prov) elif not ps_id and provider.get("provider_type", "") in provider_type_ls: @@ -865,7 +864,7 @@ class ConfigRoute(Route): return ( Response() .error( - "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志" + "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志", ) .__dict__ ) @@ -942,7 +941,7 @@ class ConfigRoute(Route): try: self.core_lifecycle.provider_manager.dynamic_import_provider( - provider_type + provider_type, ) except ImportError as e: logger.error(traceback.format_exc()) @@ -1066,7 +1065,6 @@ class ConfigRoute(Route): 当前支持的 scope: - scope=plugin:name=,key= """ - scope = request.args.get("scope") or "plugin" name = request.args.get("name") key_path = request.args.get("key") @@ -1084,7 +1082,6 @@ class ConfigRoute(Route): async def upload_config_file(self): """上传文件到插件数据目录(用于某个 file 类型配置项)。""" - try: scope, name, key_path, md, config = self._resolve_config_file_scope() except ValueError as e: @@ -1163,7 +1160,6 @@ class ConfigRoute(Route): async def delete_config_file(self): """删除插件数据目录中的文件。""" - scope = request.args.get("scope") or "plugin" name = request.args.get("name") if not name: @@ -1199,7 +1195,6 @@ class ConfigRoute(Route): async def get_config_file_list(self): """获取配置项对应目录下的文件列表。""" - try: _, name, key_path, _, config = self._resolve_config_file_scope() except ValueError as e: @@ -1260,7 +1255,7 @@ class ConfigRoute(Route): try: await self.core_lifecycle.provider_manager.create_provider( - new_provider_config + new_provider_config, ) except Exception as e: return Response().error(str(e)).__dict__ @@ -1302,7 +1297,7 @@ class ConfigRoute(Route): try: await self.core_lifecycle.provider_manager.update_provider( - origin_provider_id, new_config + origin_provider_id, new_config, ) except Exception as e: return Response().error(str(e)).__dict__ @@ -1332,7 +1327,7 @@ class ConfigRoute(Route): try: await self.core_lifecycle.provider_manager.delete_provider( - provider_id=provider_id + provider_id=provider_id, ) except Exception as e: return Response().error(str(e)).__dict__ @@ -1356,7 +1351,7 @@ class ConfigRoute(Route): cached_token = self._logo_token_cache[cache_key] # 确保platform_default_tmpl[platform.name]存在且为字典 if platform.name not in platform_default_tmpl or not isinstance( - platform_default_tmpl[platform.name], dict + platform_default_tmpl[platform.name], dict, ): platform_default_tmpl[platform.name] = {} platform_default_tmpl[platform.name]["logo_token"] = cached_token @@ -1385,7 +1380,7 @@ class ConfigRoute(Route): # 确保platform_default_tmpl[platform.name]存在且为字典 if platform.name not in platform_default_tmpl or not isinstance( - platform_default_tmpl[platform.name], dict + platform_default_tmpl[platform.name], dict, ): platform_default_tmpl[platform.name] = {} @@ -1412,7 +1407,7 @@ class ConfigRoute(Route): ) def _inject_platform_metadata_with_i18n( - self, platform, metadata, platform_i18n_translations: dict + self, platform, metadata, platform_i18n_translations: dict, ): """将配置元数据注入到 metadata 中并处理国际化键转换。""" metadata["platform_group"]["metadata"]["platform"].setdefault("items", {}) @@ -1423,7 +1418,7 @@ class ConfigRoute(Route): for lang, lang_data in platform.i18n_resources.items(): platform_i18n_translations.setdefault(lang, {}).setdefault( - "platform_group", {} + "platform_group", {}, ).setdefault("platform", {})[platform.name] = lang_data for field_key, field_value in platform_items_to_inject.items(): @@ -1432,7 +1427,7 @@ class ConfigRoute(Route): field_value[key] = f"{i18n_prefix}.{field_key}.{key}" metadata["platform_group"]["metadata"]["platform"]["items"].update( - platform_items_to_inject + platform_items_to_inject, ) async def _get_astrbot_config(self): @@ -1442,10 +1437,10 @@ class ConfigRoute(Route): { "platform_group": { "metadata": { - "platform": metadata["platform_group"]["metadata"]["platform"] - } - } - } + "platform": metadata["platform_group"]["metadata"]["platform"], + }, + }, + }, ) metadata["platform_group"]["metadata"]["platform"] = platform_i18n[ "platform_group" @@ -1464,13 +1459,13 @@ class ConfigRoute(Route): for platform in platform_registry: if platform.default_config_tmpl: platform_default_tmpl[platform.name] = copy.deepcopy( - platform.default_config_tmpl + platform.default_config_tmpl, ) # 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键) if platform.config_metadata: self._inject_platform_metadata_with_i18n( - platform, metadata, platform_i18n_translations + platform, metadata, platform_i18n_translations, ) # 收集logo注册任务 @@ -1519,7 +1514,7 @@ class ConfigRoute(Route): return ret async def _save_astrbot_configs( - self, post_configs: dict, conf_id: str | None = None + self, post_configs: dict, conf_id: str | None = None, ) -> None: try: if conf_id not in self.acm.confs: @@ -1550,7 +1545,7 @@ class ConfigRoute(Route): try: errors, post_configs = validate_config( - post_configs, getattr(md.config, "schema", {}), is_core=False + post_configs, getattr(md.config, "schema", {}), is_core=False, ) if errors: raise ValueError(f"格式校验未通过: {errors}") diff --git a/astrbot/dashboard/routes/conversation.py b/astrbot/dashboard/routes/conversation.py index 68eed7ef1..0dda96725 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -321,7 +321,7 @@ class ConversationRoute(Route): if not conversation: failed_items.append( - f"user_id:{user_id}, cid:{cid} - 对话不存在" + f"user_id:{user_id}, cid:{cid} - 对话不存在", ) continue @@ -347,7 +347,7 @@ class ConversationRoute(Route): except Exception as e: failed_items.append(f"user_id:{user_id}, cid:{cid} - {e!s}") logger.error( - f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}" + f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}", ) if exported_count == 0: diff --git a/astrbot/dashboard/routes/cron.py b/astrbot/dashboard/routes/cron.py index af26c6525..64f499b95 100644 --- a/astrbot/dashboard/routes/cron.py +++ b/astrbot/dashboard/routes/cron.py @@ -11,7 +11,7 @@ from .route import Response, Route, RouteContext class CronRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle + self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle @@ -42,7 +42,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").__dict__ + Response().error("Cron manager not initialized").__dict__, ) job_type = request.args.get("type") jobs = await cron_mgr.list_jobs(job_type) @@ -57,7 +57,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").__dict__ + Response().error("Cron manager not initialized").__dict__, ) payload = await request.json @@ -79,13 +79,13 @@ class CronRoute(Route): return jsonify(Response().error("session is required").__dict__) if run_once and not run_at: return jsonify( - Response().error("run_at is required when run_once=true").__dict__ + Response().error("run_at is required when run_once=true").__dict__, ) if (not run_once) and not cron_expression: return jsonify( Response() .error("cron_expression is required when run_once=false") - .__dict__ + .__dict__, ) if run_once and cron_expression: cron_expression = None # ignore cron when run_once specified @@ -95,7 +95,7 @@ class CronRoute(Route): run_at_dt = datetime.fromisoformat(str(run_at)) except Exception: return jsonify( - Response().error("run_at must be ISO datetime").__dict__ + Response().error("run_at must be ISO datetime").__dict__, ) job_payload = { @@ -128,7 +128,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").__dict__ + Response().error("Cron manager not initialized").__dict__, ) payload = await request.json @@ -170,7 +170,7 @@ class CronRoute(Route): session = str(payload.get("session") or "").strip() if not session: return jsonify( - Response().error("session cannot be empty").__dict__ + Response().error("session cannot be empty").__dict__, ) merged_payload["session"] = session @@ -179,7 +179,7 @@ class CronRoute(Route): note = str(payload.get("note") or "").strip() if not note: return jsonify( - Response().error("note cannot be empty").__dict__ + Response().error("note cannot be empty").__dict__, ) merged_payload["note"] = note updates["description"] = note @@ -188,7 +188,7 @@ class CronRoute(Route): description = str(payload.get("description") or "").strip() if not description: return jsonify( - Response().error("description cannot be empty").__dict__ + Response().error("description cannot be empty").__dict__, ) updates["description"] = description merged_payload["note"] = description @@ -196,7 +196,7 @@ class CronRoute(Route): if not note_updated and updates.get("description") is None: existing_note = str( - merged_payload.get("note") or job.description or "" + merged_payload.get("note") or job.description or "", ).strip() if existing_note: merged_payload["note"] = existing_note @@ -220,7 +220,7 @@ class CronRoute(Route): run_at_iso = datetime.fromisoformat(str(run_at_raw)).isoformat() except Exception: return jsonify( - Response().error("run_at must be ISO datetime").__dict__ + Response().error("run_at must be ISO datetime").__dict__, ) if next_run_once: @@ -228,7 +228,7 @@ class CronRoute(Route): return jsonify( Response() .error("run_at is required when run_once=true") - .__dict__ + .__dict__, ) next_cron_expression = None merged_payload["run_at"] = run_at_iso @@ -237,7 +237,7 @@ class CronRoute(Route): return jsonify( Response() .error("cron_expression is required when run_once=false") - .__dict__ + .__dict__, ) merged_payload.pop("run_at", None) @@ -249,7 +249,7 @@ class CronRoute(Route): cron_expression = str(payload.get("cron_expression") or "").strip() if not cron_expression: return jsonify( - Response().error("cron_expression cannot be empty").__dict__ + Response().error("cron_expression cannot be empty").__dict__, ) updates["cron_expression"] = cron_expression @@ -270,7 +270,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").__dict__ + Response().error("Cron manager not initialized").__dict__, ) await cron_mgr.delete_job(job_id) return jsonify(Response().ok(message="deleted").__dict__) diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index b1b154238..eebb87ffb 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -77,7 +77,7 @@ class KnowledgeBaseRoute(Route): } def _set_task_result( - self, task_id: str, status: str, result: Any = None, error: str | None = None + self, task_id: str, status: str, result: Any = None, error: str | None = None, ) -> None: self.upload_tasks[task_id] = { "status": status, @@ -170,7 +170,7 @@ class KnowledgeBaseRoute(Route): # 创建进度回调函数 progress_callback = self._make_progress_callback( - task_id, file_idx, file_info["file_name"] + task_id, file_idx, file_info["file_name"], ) doc = await kb_helper.upload_document( @@ -252,7 +252,7 @@ class KnowledgeBaseRoute(Route): # 创建进度回调函数 progress_callback = self._make_progress_callback( - task_id, file_idx, file_name + task_id, file_idx, file_name, ) # 调用 upload_document,传入 pre_chunked_text diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index dafb3c2f8..2d6fd6c62 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -60,7 +60,7 @@ class LiveChatSession: start_time = time.time() if not self.is_speaking or stamp != self.current_stamp: logger.warning( - f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}" + f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}", ) return None, 0.0 @@ -86,7 +86,7 @@ class LiveChatSession: self.temp_audio_path = audio_path logger.info( - f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes" + f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {os.path.getsize(audio_path)} bytes", ) return audio_path, time.time() - start_time @@ -183,7 +183,7 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] WebSocket 连接关闭: {username}") async def _create_attachment_from_file( - self, filename: str, attach_type: str + self, filename: str, attach_type: str, ) -> dict | None: """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( @@ -195,7 +195,7 @@ class LiveChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, accumulated_text: str, accumulated_parts: list, ) -> dict: """从消息中提取 web_search 引用。""" supported = [ @@ -214,7 +214,7 @@ class LiveChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -289,7 +289,7 @@ class LiveChatRoute(Route): request_id: str, ) -> None: back_queue = webchat_queue_mgr.get_or_create_back_queue( - request_id, chat_session_id + request_id, chat_session_id, ) try: while True: @@ -342,7 +342,7 @@ class LiveChatRoute(Route): session.chat_subscription_tasks.clear() async def _handle_chat_message( - self, session: LiveChatSession, message: dict + self, session: LiveChatSession, message: dict, ) -> None: """处理 Chat Mode 消息(ct=chat)""" msg_type = message.get("t") @@ -534,7 +534,7 @@ class LiveChatRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" except Exception: @@ -550,7 +550,7 @@ class LiveChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) except Exception: @@ -589,7 +589,7 @@ class LiveChatRoute(Route): or accumulated_text or accumulated_reasoning or refs - or agent_stats + or agent_stats, ) elif (streaming and msg_type == "complete") or not streaming: if chain_type not in ( @@ -628,7 +628,7 @@ class LiveChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, }, @@ -650,7 +650,7 @@ class LiveChatRoute(Route): { "ct": "chat", "t": "error", - "data": f"处理失败: {str(e)}", + "data": f"处理失败: {e!s}", "code": "PROCESSING_ERROR", }, ) @@ -714,13 +714,13 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] 用户打断: {session.username}") async def _process_audio( - self, session: LiveChatSession, audio_path: str, assemble_duration: float + self, session: LiveChatSession, audio_path: str, assemble_duration: float, ) -> None: """处理音频:STT -> LLM -> 流式 TTS""" try: # 发送 WAV 组装耗时 await websocket.send_json( - {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}} + {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}}, ) wav_assembly_finish_time = time.time() @@ -737,7 +737,7 @@ class LiveChatRoute(Route): return await websocket.send_json( - {"t": "metrics", "data": {"stt": stt_provider.meta().type}} + {"t": "metrics", "data": {"stt": stt_provider.meta().type}}, ) user_text = await stt_provider.get_text(audio_path) @@ -751,7 +751,7 @@ class LiveChatRoute(Route): { "t": "user_msg", "data": {"text": user_text, "ts": int(time.time() * 1000)}, - } + }, ) # 2. 构造消息事件并发送到 pipeline @@ -783,7 +783,7 @@ class LiveChatRoute(Route): await websocket.send_json({"t": "stop_play"}) # 保存消息并标记为被打断 await self._save_interrupted_message( - session, user_text, bot_text + session, user_text, bot_text, ) # 清空队列中未处理的消息 while not back_queue.empty(): @@ -804,7 +804,7 @@ class LiveChatRoute(Route): result_message_id = result.get("message_id") if result_message_id != message_id: logger.warning( - f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}" + f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}", ) continue @@ -823,7 +823,7 @@ class LiveChatRoute(Route): "llm_total_time": stats.get("end_time", 0) - stats.get("start_time", 0), }, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 AgentStats 失败: {e}") @@ -836,7 +836,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": stats, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 TTSStats 失败: {e}") @@ -860,9 +860,9 @@ class LiveChatRoute(Route): { "t": "metrics", "data": { - "speak_to_first_frame": speak_to_first_frame_latency + "speak_to_first_frame": speak_to_first_frame_latency, }, - } + }, ) text = result.get("text") @@ -871,7 +871,7 @@ class LiveChatRoute(Route): { "t": "bot_text_chunk", "data": {"text": text}, - } + }, ) # 发送音频数据给前端 @@ -879,7 +879,7 @@ class LiveChatRoute(Route): { "t": "response", "data": data, # base64 编码的音频数据 - } + }, ) elif result_type in ["complete", "end"]: @@ -895,7 +895,7 @@ class LiveChatRoute(Route): "text": bot_text, "ts": int(time.time() * 1000), }, - } + }, ) # 发送结束标记 @@ -907,7 +907,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": {"wav_to_tts_total_time": wav_to_tts_duration}, - } + }, ) break finally: @@ -915,14 +915,14 @@ class LiveChatRoute(Route): except Exception as e: logger.error(f"[Live Chat] 处理音频失败: {e}", exc_info=True) - await websocket.send_json({"t": "error", "data": f"处理失败: {str(e)}"}) + await websocket.send_json({"t": "error", "data": f"处理失败: {e!s}"}) finally: session.is_processing = False session.should_interrupt = False async def _save_interrupted_message( - self, session: LiveChatSession, user_text: str, bot_text: str + self, session: LiveChatSession, user_text: str, bot_text: str, ) -> None: """保存被打断的消息""" interrupted_text = bot_text + " [用户打断]" @@ -932,11 +932,11 @@ class LiveChatRoute(Route): try: timestamp = int(time.time() * 1000) logger.info( - f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})", ) 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 e7eebef6e..a6317f853 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -43,7 +43,7 @@ class LogRoute(Route): ) async def _replay_cached_logs( - self, last_event_id: str + self, last_event_id: str, ) -> AsyncGenerator[str, None]: """辅助生成器:重放缓存的日志""" try: @@ -86,7 +86,7 @@ class LogRoute(Route): self.log_broker.unregister(queue) response = cast( - QuartResponse, + "QuartResponse", await make_response( stream(), { diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index 8f2047326..58e38888a 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -73,7 +73,7 @@ class OpenApiRoute(Route): "name": str(conf_info.get("name", "")).strip(), "path": str(conf_info.get("path", "")).strip(), "is_default": conf_id == "default", - } + }, ) return result @@ -141,7 +141,7 @@ class OpenApiRoute(Route): async def chat_send(self): post_data = await request.get_json(silent=True) or {} effective_username, username_err = self._resolve_open_username( - post_data.get("username") + post_data.get("username"), ) if username_err: return Response().error(username_err).__dict__ @@ -173,7 +173,7 @@ class OpenApiRoute(Route): await self.core_lifecycle.umop_config_router.delete_route(umo) else: await self.core_lifecycle.umop_config_router.update_route( - umo, config_id + umo, config_id, ) except Exception as e: logger.error( @@ -241,7 +241,7 @@ class OpenApiRoute(Route): "type": "error", "code": code, "data": message, - } + }, ) async def _update_session_config_route( @@ -260,7 +260,7 @@ class OpenApiRoute(Route): await self.core_lifecycle.umop_config_router.delete_route(umo) else: await self.core_lifecycle.umop_config_router.update_route( - umo, config_id + umo, config_id, ) except Exception as e: logger.error( @@ -275,11 +275,11 @@ class OpenApiRoute(Route): async def _handle_chat_ws_send(self, post_data: dict) -> None: effective_username, username_err = self._resolve_open_username( - post_data.get("username") + post_data.get("username"), ) if username_err or not effective_username: await self._send_chat_ws_error( - username_err or "Invalid username", "BAD_USER" + username_err or "Invalid username", "BAD_USER", ) return @@ -342,7 +342,7 @@ class OpenApiRoute(Route): "enable_streaming": enable_streaming, "message_id": message_id, }, - ) + ), ) message_parts_for_storage = strip_message_parts_path_fields(message_parts) @@ -360,7 +360,7 @@ class OpenApiRoute(Route): "data": None, "session_id": session_id, "message_id": message_id, - } + }, ) accumulated_parts = [] @@ -407,7 +407,7 @@ class OpenApiRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -417,7 +417,7 @@ class OpenApiRoute(Route): tool_calls[tc_id]["result"] = tcr.get("result") tool_calls[tc_id]["finished_ts"] = tcr.get("ts") accumulated_parts.append( - {"type": "tool_call", "tool_calls": [tool_calls[tc_id]]} + {"type": "tool_call", "tool_calls": [tool_calls[tc_id]]}, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -429,28 +429,28 @@ class OpenApiRoute(Route): elif msg_type == "image": filename = str(result_text).replace("[IMAGE]", "") part = await self.chat_route._create_attachment_from_file( - filename, "image" + filename, "image", ) if part: accumulated_parts.append(part) elif msg_type == "record": filename = str(result_text).replace("[RECORD]", "") part = await self.chat_route._create_attachment_from_file( - filename, "record" + filename, "record", ) if part: accumulated_parts.append(part) elif msg_type == "file": filename = str(result_text).replace("[FILE]", "") part = await self.chat_route._create_attachment_from_file( - filename, "file" + filename, "file", ) if part: accumulated_parts.append(part) elif msg_type == "video": filename = str(result_text).replace("[VIDEO]", "") part = await self.chat_route._create_attachment_from_file( - filename, "video" + filename, "video", ) if part: accumulated_parts.append(part) @@ -486,11 +486,11 @@ class OpenApiRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, "session_id": session_id, - } + }, ) accumulated_parts = [] accumulated_text = "" @@ -500,7 +500,7 @@ class OpenApiRoute(Route): except Exception as e: logger.exception(f"Open API WS chat failed: {e}", exc_info=True) await self._send_chat_ws_error( - f"Failed to process message: {e}", "PROCESSING_ERROR" + f"Failed to process message: {e}", "PROCESSING_ERROR", ) finally: webchat_queue_mgr.remove_back_queue(message_id) @@ -545,7 +545,7 @@ class OpenApiRoute(Route): async def get_chat_sessions(self): username, username_err = self._resolve_open_username( - request.args.get("username") + request.args.get("username"), ) if username_err: return Response().error(username_err).__dict__ @@ -558,12 +558,9 @@ class OpenApiRoute(Route): except ValueError: return Response().error("page and page_size must be integers").__dict__ - if page < 1: - page = 1 - if page_size < 1: - page_size = 1 - if page_size > 100: - page_size = 100 + page = max(page, 1) + page_size = max(page_size, 1) + page_size = min(page_size, 100) platform_id = request.args.get("platform_id") @@ -590,7 +587,7 @@ class OpenApiRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return ( @@ -601,7 +598,7 @@ class OpenApiRoute(Route): "page": page, "page_size": page_size, "total": total, - } + }, ) .__dict__ ) @@ -653,7 +650,7 @@ class OpenApiRoute(Route): try: message_chain = await self._build_message_chain_from_payload( - message_payload + message_payload, ) await platform_inst.send_by_session(session, message_chain) return Response().ok().__dict__ diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 56c14fe61..835375734 100644 --- a/astrbot/dashboard/routes/persona.py +++ b/astrbot/dashboard/routes/persona.py @@ -44,7 +44,7 @@ class PersonaRoute(Route): folder_id = request.args.get("folder_id") if folder_id is not None: personas = await self.persona_mgr.get_personas_by_folder( - folder_id if folder_id else None + folder_id or None, ) else: personas = await self.persona_mgr.get_all_personas() @@ -151,9 +151,9 @@ class PersonaRoute(Route): persona = await self.persona_mgr.create_persona( persona_id=persona_id, system_prompt=system_prompt, - begin_dialogs=begin_dialogs if begin_dialogs else None, - tools=tools if tools else None, - skills=skills if skills else None, + begin_dialogs=begin_dialogs or None, + tools=tools or None, + skills=skills or None, custom_error_message=custom_error_message, folder_id=folder_id, sort_order=sort_order, @@ -209,7 +209,7 @@ class PersonaRoute(Route): if has_custom_error_message: if custom_error_message is not None and not isinstance( - custom_error_message, str + custom_error_message, str, ): return Response().error("自定义报错回复信息必须是字符串").__dict__ if isinstance(custom_error_message, str): diff --git a/astrbot/dashboard/routes/platform.py b/astrbot/dashboard/routes/platform.py index 874bc19db..85f44f7c5 100644 --- a/astrbot/dashboard/routes/platform.py +++ b/astrbot/dashboard/routes/platform.py @@ -50,6 +50,7 @@ class PlatformRoute(Route): Returns: 根据平台适配器返回相应的响应 + """ # 根据 webhook_uuid 查找对应的平台 platform_adapter = self._find_platform_by_uuid(webhook_uuid) @@ -64,7 +65,7 @@ class PlatformRoute(Route): return result except NotImplementedError: logger.error( - f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法" + f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法", ) return Response().error("平台未支持统一 Webhook 模式").__dict__, 500 except Exception as e: @@ -79,6 +80,7 @@ class PlatformRoute(Route): Returns: 平台适配器实例,未找到则返回 None + """ for platform in self.platform_manager.platform_insts: if platform.config.get("webhook_uuid") == webhook_uuid: @@ -91,6 +93,7 @@ class PlatformRoute(Route): Returns: 包含平台统计信息的响应 + """ try: stats = self.platform_manager.get_all_stats() diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index d151bbe6f..31a832ee9 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -92,7 +92,7 @@ class PluginRoute(Route): data = await request.get_json() version_spec = data.get("astrbot_version", "") is_valid, message = self.plugin_manager._validate_astrbot_version_specifier( - version_spec + version_spec, ) return ( Response() @@ -101,7 +101,7 @@ class PluginRoute(Route): "compatible": is_valid, "message": message, "astrbot_version": version_spec, - } + }, ) .__dict__ ) @@ -128,8 +128,7 @@ class PluginRoute(Route): if success: return Response().ok(None, f"插件 {dir_name} 重载成功。").__dict__ - else: - return Response().error(f"重载失败: {err}").__dict__ + return Response().error(f"重载失败: {err}").__dict__ except Exception as e: logger.error(f"/api/plugin/reload-failed: {traceback.format_exc()}") @@ -200,7 +199,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) @@ -360,7 +359,7 @@ class PluginRoute(Route): base_dir = Path( self.plugin_manager.reserved_plugin_path if plugin.reserved - else self.plugin_manager.plugin_store_path + else self.plugin_manager.plugin_store_path, ) plugin_dir = base_dir / plugin.root_dir_name if not plugin_dir.is_dir(): @@ -416,7 +415,7 @@ class PluginRoute(Route): plugin.desc, plugin.version, plugin.display_name, - ] + ], ): continue _plugin_resp.append(_t) @@ -685,7 +684,7 @@ class PluginRoute(Route): raise result if isinstance(result, BaseException): results.append( - {"name": name, "status": "error", "message": str(result)} + {"name": name, "status": "error", "message": str(result)}, ) else: results.append(result) diff --git a/astrbot/dashboard/routes/session_management.py b/astrbot/dashboard/routes/session_management.py index fc632d1f5..7c3d4b0ce 100644 --- a/astrbot/dashboard/routes/session_management.py +++ b/astrbot/dashboard/routes/session_management.py @@ -49,7 +49,7 @@ class SessionManagementRoute(Route): self.register_routes() async def _get_umo_rules( - self, page: int = 1, page_size: int = 10, search: str = "" + self, page: int = 1, page_size: int = 10, search: str = "", ) -> tuple[dict, int]: """获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。 @@ -67,6 +67,7 @@ class SessionManagementRoute(Route): Returns: tuple[dict, int]: (umo_rules, total) - 分页后的 umo 规则和总数 + """ umo_rules = {} async with self.db_helper.get_db() as session: @@ -75,7 +76,7 @@ class SessionManagementRoute(Route): select(Preference).where( col(Preference.scope) == "umo", col(Preference.key).in_(AVAILABLE_SESSION_RULE_KEYS), - ) + ), ) prefs = result.scalars().all() for pref in prefs: @@ -134,15 +135,13 @@ class SessionManagementRoute(Route): search = request.args.get("search", "", type=str).strip() # 参数校验 - if page < 1: - page = 1 + page = max(page, 1) if page_size < 1: page_size = 10 - if page_size > 100: - page_size = 100 + page_size = min(page_size, 100) umo_rules, total = await self._get_umo_rules( - page=page, page_size=page_size, search=search + page=page, page_size=page_size, search=search, ) # 构建规则列表 @@ -240,7 +239,7 @@ class SessionManagementRoute(Route): "available_plugins": available_plugins, "available_kbs": available_kbs, "available_rule_keys": AVAILABLE_SESSION_RULE_KEYS, - } + }, ) .__dict__ ) @@ -315,10 +314,9 @@ class SessionManagementRoute(Route): .ok({"message": f"规则 {rule_key} 已删除", "umo": umo}) .__dict__ ) - else: - # 删除该 umo 的所有规则 - await sp.clear_async("umo", umo) - return Response().ok({"message": "所有规则已删除", "umo": umo}).__dict__ + # 删除该 umo 的所有规则 + await sp.clear_async("umo", umo) + return Response().ok({"message": "所有规则已删除", "umo": umo}).__dict__ except Exception as e: logger.error(f"删除会话规则失败: {e!s}") return Response().error(f"删除会话规则失败: {e!s}").__dict__ @@ -334,7 +332,6 @@ class SessionManagementRoute(Route): "rule_key": "session_service_config" | ... (可选,不传则删除所有规则) } """ - try: data = await request.get_json() umos = data.get("umos", []) @@ -356,7 +353,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] @@ -410,21 +407,20 @@ class SessionManagementRoute(Route): "message": f"{message},{len(failed_umos)} 条删除失败", "success_count": success_count, "failed_umos": failed_umos, - } + }, ) .__dict__ ) - else: - return ( - Response() - .ok( - { - "message": message, - "success_count": success_count, - } - ) - .__dict__ + return ( + Response() + .ok( + { + "message": message, + "success_count": success_count, + }, ) + .__dict__ + ) except Exception as e: logger.error(f"批量删除会话规则失败: {e!s}") return Response().error(f"批量删除会话规则失败: {e!s}").__dict__ @@ -441,7 +437,7 @@ class SessionManagementRoute(Route): result = await session.execute( select(ConversationV2.user_id) .distinct() - .order_by(ConversationV2.user_id) + .order_by(ConversationV2.user_id), ) umos = [row[0] for row in result.fetchall()] @@ -467,12 +463,10 @@ class SessionManagementRoute(Route): message_type = request.args.get("message_type", "all", type=str) platform = request.args.get("platform", "", type=str) - if page < 1: - page = 1 + page = max(page, 1) if page_size < 1: page_size = 20 - if page_size > 100: - page_size = 100 + page_size = min(page_size, 100) # 从 Conversation 表获取所有 distinct user_id (即 umo) async with self.db_helper.get_db() as session: @@ -480,7 +474,7 @@ class SessionManagementRoute(Route): result = await session.execute( select(ConversationV2.user_id) .distinct() - .order_by(ConversationV2.user_id) + .order_by(ConversationV2.user_id), ) all_umos = [row[0] for row in result.fetchall()] @@ -558,7 +552,7 @@ class SessionManagementRoute(Route): "chat_provider": rules.get(chat_provider_key), "tts_provider": rules.get(tts_provider_key), "stt_provider": rules.get(stt_provider_key), - } + }, ) # 分页 @@ -597,7 +591,7 @@ class SessionManagementRoute(Route): "available_chat_providers": available_chat_providers, "available_tts_providers": available_tts_providers, "available_stt_providers": available_stt_providers, - } + }, ) .__dict__ ) @@ -645,7 +639,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] @@ -715,7 +709,7 @@ class SessionManagementRoute(Route): "success_count": success_count, "failed_count": len(failed_umos), "failed_umos": failed_umos, - } + }, ) .__dict__ ) @@ -778,7 +772,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] @@ -825,7 +819,7 @@ class SessionManagementRoute(Route): "success_count": success_count, "failed_count": len(failed_umos), "failed_umos": failed_umos, - } + }, ) .__dict__ ) @@ -856,7 +850,7 @@ class SessionManagementRoute(Route): "name": group_data.get("name", ""), "umos": group_data.get("umos", []), "umo_count": len(group_data.get("umos", [])), - } + }, ) return Response().ok({"groups": groups_list}).__dict__ except Exception as e: @@ -898,7 +892,7 @@ class SessionManagementRoute(Route): "umos": umos, "umo_count": len(umos), }, - } + }, ) .__dict__ ) @@ -955,7 +949,7 @@ class SessionManagementRoute(Route): "umos": group["umos"], "umo_count": len(group["umos"]), }, - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/skills.py b/astrbot/dashboard/routes/skills.py index abae13e33..fa0ae7c03 100644 --- a/astrbot/dashboard/routes/skills.py +++ b/astrbot/dashboard/routes/skills.py @@ -93,17 +93,17 @@ class SkillsRoute(Route): if not endpoint or not access_token: raise ValueError( "Shipyard Neo endpoint or access token not configured. " - "Set them in Dashboard or ensure Bay's credentials.json is accessible." + "Set them in Dashboard or ensure Bay's credentials.json is accessible.", ) return endpoint, access_token async def _delete_neo_release( - self, client: Any, release_id: str, reason: str | None + self, client: Any, release_id: str, reason: str | None, ): return await client.skills.delete_release(release_id, reason=reason) async def _delete_neo_candidate( - self, client: Any, candidate_id: str, reason: str | None + self, client: Any, candidate_id: str, reason: str | None, ): return await client.skills.delete_candidate(candidate_id, reason=reason) @@ -132,12 +132,12 @@ class SkillsRoute(Route): async def get_skills(self): try: provider_settings = self.core_lifecycle.astrbot_config.get( - "provider_settings", {} + "provider_settings", {}, ) runtime = provider_settings.get("computer_use_runtime", "local") skill_mgr = SkillManager() skills = skill_mgr.list_skills( - active_only=False, runtime=runtime, show_sandbox_path=False + active_only=False, runtime=runtime, show_sandbox_path=False, ) return ( Response() @@ -146,7 +146,7 @@ class SkillsRoute(Route): "skills": [skill.__dict__ for skill in skills], "runtime": runtime, "sandbox_cache": skill_mgr.get_sandbox_skills_cache_status(), - } + }, ) .__dict__ ) @@ -181,12 +181,12 @@ class SkillsRoute(Route): try: try: skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False, skill_name_hint=Path(filename).stem + temp_path, overwrite=False, skill_name_hint=Path(filename).stem, ) except TypeError: # Backward compatibility for callers that do not accept skill_name_hint skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False + temp_path, overwrite=False, ) except Exception: # Keep behavior consistent with previous implementation @@ -246,7 +246,7 @@ class SkillsRoute(Route): { "filename": filename, "error": "Only .zip files are supported", - } + }, ) continue @@ -263,7 +263,7 @@ class SkillsRoute(Route): # Backward compatibility for monkeypatched implementations in tests try: skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False + temp_path, overwrite=False, ) except FileExistsError: skipped.append( @@ -271,7 +271,7 @@ class SkillsRoute(Route): "filename": filename, "name": Path(filename).stem, "error": "Skill already exists.", - } + }, ) skill_name = None except FileExistsError: @@ -280,7 +280,7 @@ class SkillsRoute(Route): "filename": filename, "name": Path(filename).stem, "error": "Skill already exists.", - } + }, ) skill_name = None @@ -302,7 +302,7 @@ class SkillsRoute(Route): await sync_skills_to_active_sandboxes() except Exception: logger.warning( - "Failed to sync uploaded skills to active sandboxes." + "Failed to sync uploaded skills to active sandboxes.", ) total = len(file_list) @@ -383,7 +383,7 @@ class SkillsRoute(Route): return ( Response() .error( - "Sandbox preset skill cannot be downloaded from local skill files." + "Sandbox preset skill cannot be downloaded from local skill files.", ) .__dict__ ) @@ -539,7 +539,7 @@ class SkillsRoute(Route): report=data.get("report"), ) logger.info( - f"[Neo] Candidate evaluated: id={candidate_id}, passed={passed}" + f"[Neo] Candidate evaluated: id={candidate_id}, passed={passed}", ) return Response().ok(_to_jsonable(result)).__dict__ @@ -577,13 +577,13 @@ class SkillsRoute(Route): did_sync_to_local = bool(sync_json) if did_sync_to_local: logger.info( - f"[Neo] Stable release synced to local: skill={sync_json.get('local_skill_name', '')}" + f"[Neo] Stable release synced to local: skill={sync_json.get('local_skill_name', '')}", ) if result.get("sync_error"): resp = Response().error( "Stable promote synced failed and has been rolled back. " - f"sync_error={result['sync_error']}" + f"sync_error={result['sync_error']}", ) resp.data = { "release": release_json, @@ -647,7 +647,7 @@ class SkillsRoute(Route): ) logger.info( f"[Neo] Release synced to local: skill={result.local_skill_name}, " - f"release_id={result.release_id}" + f"release_id={result.release_id}", ) return ( Response() @@ -660,7 +660,7 @@ class SkillsRoute(Route): "payload_ref": result.payload_ref, "map_path": result.map_path, "synced_at": result.synced_at, - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 2eb3cd400..0ce3408ee 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -211,10 +211,10 @@ class StatRoute(Route): local_tz = datetime.now().astimezone().tzinfo or timezone.utc now_local = datetime.now(local_tz) range_start_local = (now_local - timedelta(days=days)).replace( - minute=0, second=0, microsecond=0 + minute=0, second=0, microsecond=0, ) today_start_local = now_local.replace( - hour=0, minute=0, second=0, microsecond=0 + hour=0, minute=0, second=0, microsecond=0, ) query_start_local = min(range_start_local, today_start_local) query_start_utc = query_start_local.astimezone(timezone.utc) @@ -226,7 +226,7 @@ class StatRoute(Route): ProviderStat.agent_type == "internal", ProviderStat.created_at >= query_start_utc, ) - .order_by(ProviderStat.created_at.asc()) + .order_by(ProviderStat.created_at.asc()), ) records = result.scalars().all() @@ -237,7 +237,7 @@ class StatRoute(Route): bucket_cursor += timedelta(hours=1) trend_by_provider: dict[str, dict[int, int]] = defaultdict( - lambda: defaultdict(int) + lambda: defaultdict(int), ) total_by_provider: dict[str, int] = defaultdict(int) total_by_umo: dict[str, int] = defaultdict(int) @@ -267,7 +267,7 @@ class StatRoute(Route): if created_at_local >= range_start_local: bucket_local = created_at_local.replace( - minute=0, second=0, microsecond=0 + minute=0, second=0, microsecond=0, ) bucket_ts = int(bucket_local.timestamp() * 1000) trend_by_provider[provider_id][bucket_ts] += token_total @@ -386,7 +386,7 @@ class StatRoute(Route): "today_total_calls": today_total_calls, "today_by_model": today_by_model_data, "today_by_provider": today_by_provider_data, - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/subagent.py b/astrbot/dashboard/routes/subagent.py index e3d77f73a..92cb2ae3e 100644 --- a/astrbot/dashboard/routes/subagent.py +++ b/astrbot/dashboard/routes/subagent.py @@ -109,7 +109,7 @@ class SubAgentRoute(Route): "parameters": tool.parameters, "active": tool.active, "handler_module_path": tool.handler_module_path, - } + }, ) return jsonify(Response().ok(data=tools_dict).__dict__) except Exception as e: diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py index 634828e95..2a84b58ab 100644 --- a/astrbot/dashboard/routes/t2i.py +++ b/astrbot/dashboard/routes/t2i.py @@ -13,7 +13,7 @@ from .route import Response, Route, RouteContext class T2iRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle + self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 33b74deff..45a082c83 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -15,7 +15,6 @@ DEFAULT_MCP_CONFIG = {"mcpServers": {}} class EmptyMcpServersError(ValueError): """Raised when mcpServers is empty.""" - pass def _extract_mcp_server_config(mcp_servers_value: object) -> dict: @@ -23,6 +22,7 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: Raises: ValueError: Invalid configuration + """ if not isinstance(mcp_servers_value, dict): raise ValueError("mcpServers must be a JSON object") @@ -33,7 +33,7 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: if not isinstance(extracted, dict): raise ValueError( "Invalid mcpServers format. Ensure each key in mcpServers is a server name, " - "and each value is an object containing fields like command/url." + "and each value is an object containing fields like command/url.", ) return extracted @@ -78,7 +78,7 @@ class ToolsRoute(Route): if not isinstance(mcp_servers, dict): logger.warning( - f"Invalid MCP server config type: {type(mcp_servers).__name__}. Expected object/dict; skipped all MCP servers." + f"Invalid MCP server config type: {type(mcp_servers).__name__}. Expected object/dict; skipped all MCP servers.", ) mcp_servers = {} @@ -86,7 +86,7 @@ class ToolsRoute(Route): for name, server_config in mcp_servers.items(): if not isinstance(server_config, dict): logger.warning( - f"Invalid config for MCP server '{name}' (type: {type(server_config).__name__}); skipped." + f"Invalid config for MCP server '{name}' (type: {type(server_config).__name__}); skipped.", ) continue @@ -137,7 +137,7 @@ class ToolsRoute(Route): if key == "mcpServers": try: server_config = _extract_mcp_server_config( - server_data["mcpServers"] + server_data["mcpServers"], ) except ValueError as e: return Response().error(f"{e!s}").__dict__ @@ -241,7 +241,7 @@ class ToolsRoute(Route): if key == "mcpServers": try: server_config = _extract_mcp_server_config( - server_data["mcpServers"] + server_data["mcpServers"], ) except ValueError as e: return Response().error(f"{e!s}").__dict__ @@ -276,7 +276,7 @@ class ToolsRoute(Route): return ( Response() .error( - f"Timed out while disabling MCP server {old_name} before enabling: {e!s}" + f"Timed out while disabling MCP server {old_name} before enabling: {e!s}", ) .__dict__ ) @@ -285,7 +285,7 @@ class ToolsRoute(Route): return ( Response() .error( - f"Failed to disable MCP server {old_name} before enabling: {e!s}" + f"Failed to disable MCP server {old_name} before enabling: {e!s}", ) .__dict__ ) @@ -393,7 +393,7 @@ class ToolsRoute(Route): return ( Response() .error( - "Only one MCP server configuration can be tested at a time" + "Only one MCP server configuration can be tested at a time", ) .__dict__ ) @@ -445,7 +445,7 @@ class ToolsRoute(Route): origin = "mcp" origin_name = tool.mcp_server_name elif tool.handler_module_path and star_map.get( - tool.handler_module_path + tool.handler_module_path, ): star = star_map[tool.handler_module_path] origin = "plugin" diff --git a/astrbot/dashboard/routes/util.py b/astrbot/dashboard/routes/util.py index 105619815..bd60e6e61 100644 --- a/astrbot/dashboard/routes/util.py +++ b/astrbot/dashboard/routes/util.py @@ -17,7 +17,6 @@ def get_schema_item(schema: dict | None, key_path: str) -> dict | None: - 扁平 schema(直接 key 命中) - 嵌套 object schema({type: "object", items: {...}}) """ - if not isinstance(schema, dict) or not key_path: return None if key_path in schema: @@ -44,7 +43,6 @@ def sanitize_filename(name: str) -> str: - 将路径分隔符替换为下划线 - 拒绝空字符串 / "." / ".." """ - cleaned = os.path.basename(name).strip() if not cleaned or cleaned in {".", ".."}: return "" @@ -59,7 +57,6 @@ def sanitize_path_segment(segment: str) -> str: 仅保留 [A-Za-z0-9_-],其余替换为 "_" """ - cleaned = [] for ch in segment: if ( @@ -81,14 +78,12 @@ def sanitize_path_segment(segment: str) -> str: def config_key_to_folder(key_path: str) -> str: """将 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 rel = rel_path.replace("\\", "/").lstrip("/") diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index b3dc75945..fce043fc5 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -273,7 +273,7 @@ class AstrBotDashboard: """获取占用端口的进程详细信息""" try: for conn in psutil.net_connections(kind="inet"): - if cast(_AddrWithPort, conn.laddr).port == port: + if cast("_AddrWithPort", conn.laddr).port == port: try: process = psutil.Process(conn.pid) # 获取详细信息 diff --git a/docs/scripts/sync_docs_to_wiki.py b/docs/scripts/sync_docs_to_wiki.py index f717f1f96..70f3881b0 100644 --- a/docs/scripts/sync_docs_to_wiki.py +++ b/docs/scripts/sync_docs_to_wiki.py @@ -147,7 +147,6 @@ def iter_markdown_links(content: str): This scanner intentionally handles inline `[]()` links used in the docs tree. It does not parse reference-style links or arbitrary HTML. """ - index = 0 while index < len(content): label_start = content.find("[", index) @@ -292,7 +291,7 @@ class LinkResolver: self.source_pages = discover_source_pages(str(self.source_root)) def resolve_base_target( - self, base_target: str, source_path: str + self, base_target: str, source_path: str, ) -> ResolutionResult: return resolve_link_path( base_target=base_target, @@ -302,7 +301,7 @@ class LinkResolver: ) def resolve_markdown_target( - self, target: str, source_path: str + self, target: str, source_path: str, ) -> tuple[str | None, str]: parsed_target = parse_doc_target(target) if parsed_target is None: @@ -386,7 +385,7 @@ def rewrite_links( segment.text, source_path=source_path, resolver=resolver, - ) + ), ) continue @@ -404,7 +403,7 @@ def find_unresolved_doc_links(source_root: Path) -> list[str]: content = (root / source_path).read_text(encoding="utf-8") for link in iter_markdown_links(content): resolved_path, _ = resolver.resolve_markdown_target( - link.target, source_path + link.target, source_path, ) if resolved_path is not None: continue @@ -523,7 +522,7 @@ def build_sidebar(page_infos: list[PageInfo]) -> str: def build_page_info( - source_root: Path, source_path: str, resolver: LinkResolver + source_root: Path, source_path: str, resolver: LinkResolver, ) -> PageInfo: source_file = source_root / source_path content = source_file.read_text(encoding="utf-8") @@ -608,7 +607,7 @@ def sync_docs_to_wiki(source_root: Path, wiki_root: Path) -> None: def main() -> int: parser = argparse.ArgumentParser( - description="Sync AstrBot docs content to GitHub wiki pages." + description="Sync AstrBot docs content to GitHub wiki pages.", ) parser.add_argument( "--source-root", @@ -635,7 +634,7 @@ def main() -> int: return 0 sync_docs_to_wiki( - source_root=Path(args.source_root), wiki_root=Path(args.wiki_root) + source_root=Path(args.source_root), wiki_root=Path(args.wiki_root), ) return 0 diff --git a/docs/scripts/upload_doc_images_to_r2.py b/docs/scripts/upload_doc_images_to_r2.py index 7db614dc4..c541fc157 100755 --- a/docs/scripts/upload_doc_images_to_r2.py +++ b/docs/scripts/upload_doc_images_to_r2.py @@ -27,13 +27,13 @@ IMAGE_EXTS = { MD_IMAGE_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)") HTML_IMG_RE = re.compile( - r"]*\bsrc\s*=\s*([\"'])([^\"']+)\1[^>]*>", re.IGNORECASE + r"]*\bsrc\s*=\s*([\"'])([^\"']+)\1[^>]*>", re.IGNORECASE, ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Upload all locally referenced images from Markdown docs to Cloudflare R2 using rclone." + description="Upload all locally referenced images from Markdown docs to Cloudflare R2 using rclone.", ) parser.add_argument("--remote", required=True, help="rclone remote name, e.g. r2") parser.add_argument("--bucket", default="", help="bucket name in remote path") @@ -48,10 +48,10 @@ def parse_args() -> argparse.Namespace: help="docs root to scan for .md files (default: current directory)", ) parser.add_argument( - "--dry-run", action="store_true", help="preview uploads without sending files" + "--dry-run", action="store_true", help="preview uploads without sending files", ) parser.add_argument( - "--list-only", action="store_true", help="only print matched image files" + "--list-only", action="store_true", help="only print matched image files", ) parser.add_argument( "--rewrite-markdown", @@ -135,7 +135,7 @@ def find_markdown_files(root: Path) -> list[Path]: def collect_images( - root: Path, md_files: Sequence[Path] + root: Path, md_files: Sequence[Path], ) -> tuple[set[Path], list[tuple[Path, str]]]: images: set[Path] = set() missing: list[tuple[Path, str]] = [] @@ -191,7 +191,7 @@ def build_public_url(base: str, object_path: str) -> str: def run_rclone_upload( - root: Path, target: str, rel_files: Iterable[str], dry_run: bool + root: Path, target: str, rel_files: Iterable[str], dry_run: bool, ) -> None: if shutil.which("rclone") is None: raise RuntimeError("rclone not found in PATH") @@ -262,7 +262,7 @@ def rewrite_markdown_files( if not url: return match.group(0) return match.group(0).replace( - f"src={quote_ch}{raw}{quote_ch}", f"src={quote_ch}{url}{quote_ch}", 1 + f"src={quote_ch}{raw}{quote_ch}", f"src={quote_ch}{url}{quote_ch}", 1, ) updated = MD_IMAGE_RE.sub(md_repl, text) diff --git a/main.py b/main.py index 14e0c23a8..3f4b14168 100644 --- a/main.py +++ b/main.py @@ -108,7 +108,7 @@ async def main_async(webui_dir_arg: str | None) -> None: if webui_dir is None: logger.warning( "管理面板文件检查失败,WebUI 功能将不可用。" - "请检查网络连接或手动指定 --webui-dir 参数。" + "请检查网络连接或手动指定 --webui-dir 参数。", ) db = db_helper diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py index 431465b54..5e9c1cbf1 100644 --- a/scripts/hatch_build.py +++ b/scripts/hatch_build.py @@ -1,5 +1,4 @@ -""" -Custom Hatchling build hook. +"""Custom Hatchling build hook. Only runs when the environment variable ASTRBOT_BUILD_DASHBOARD=1 is set, so that `uv sync` / editable installs are never affected. diff --git a/typings/faiss/__init__.pyi b/typings/faiss/__init__.pyi index 6f2bace36..1ebf7e1b2 100644 --- a/typings/faiss/__init__.pyi +++ b/typings/faiss/__init__.pyi @@ -32,17 +32,17 @@ class Index: @overload def reconstruct(self, key: int, x: np.ndarray) -> None: ... def reconstruct( - self, key: int, x: np.ndarray | None = ... + self, key: int, x: np.ndarray | None = ..., ) -> np.ndarray | None: ... @overload def reconstruct_n(self, n0: int, ni: int) -> np.ndarray: ... @overload def reconstruct_n(self, n0: int, ni: int, x: np.ndarray) -> None: ... def reconstruct_n( - self, n0: int = ..., ni: int = ..., x: np.ndarray | None = ... + self, n0: int = ..., ni: int = ..., x: np.ndarray | None = ..., ) -> np.ndarray | None: ... def range_search( - self, x: np.ndarray, thresh: float, *, params: Any = ... + self, x: np.ndarray, thresh: float, *, params: Any = ..., ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... def add_sa_codes(self, codes: np.ndarray, ids: np.ndarray | None = ...) -> None: ... def sa_encode(self, x: np.ndarray) -> np.ndarray: ... @@ -70,13 +70,13 @@ class InvertedLists: def __len__(self) -> int: ... class AdditiveQuantizer: - pass + ... class Quantizer: - pass + ... class VectorTransform: - pass + ... # SWIG-provided downcast helpers (present in some faiss Python builds). def downcast_IndexBinary(obj: Any) -> IndexBinary: ...