From 18e66595f0fbbde5bccd404c51ddc72369cd96ed Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Fri, 10 Apr 2026 20:12:33 +0800 Subject: [PATCH] chore: ruff check --fix && ruff format . --- .../abc/_abp/base_astrbot_abp_client.py | 3 +- .../abc/_lsp/base_astrbot_lsp_client.py | 3 +- astrbot/_internal/geteway/deps.py | 4 +- astrbot/_internal/geteway/server.py | 12 +- astrbot/_internal/geteway/ws_manager.py | 7 +- astrbot/_internal/protocols/_abp/client.py | 5 +- astrbot/_internal/protocols/_acp/client.py | 9 +- astrbot/_internal/protocols/_acp/server.py | 8 +- astrbot/_internal/protocols/_lsp/client.py | 8 +- astrbot/_internal/protocols/_mcp/client.py | 10 +- astrbot/_internal/runtime/orchestrator.py | 3 +- astrbot/_internal/stars/__init__.py | 3 +- .../_internal/stars/runtime_status_star.py | 3 +- astrbot/_internal/tools/base.py | 9 +- astrbot/_internal/tools/registry.py | 29 +++- astrbot/api/tools.py | 3 +- .../builtin_stars/astrbot/long_term_memory.py | 6 +- astrbot/builtin_stars/astrbot/main.py | 8 +- .../builtin_commands/commands/conversation.py | 42 +++-- .../builtin_commands/commands/help.py | 3 +- .../builtin_commands/commands/persona.py | 2 +- .../builtin_commands/commands/provider.py | 35 ++++- .../builtin_stars/builtin_commands/main.py | 4 +- astrbot/cli/__main__.py | 12 +- astrbot/cli/commands/cmd_bk.py | 27 +++- astrbot/cli/commands/cmd_conf.py | 10 +- astrbot/cli/commands/cmd_init.py | 14 +- astrbot/cli/commands/cmd_run.py | 7 +- astrbot/cli/commands/cmd_uninstall.py | 4 +- astrbot/cli/utils/plugin.py | 4 +- astrbot/cli/utils/version_comparator.py | 4 +- astrbot/core/__init__.py | 4 +- astrbot/core/agent/context/compressor.py | 25 ++- astrbot/core/agent/context/manager.py | 19 ++- astrbot/core/agent/context/token_counter.py | 8 +- astrbot/core/agent/context/truncator.py | 15 +- astrbot/core/agent/mcp_client.py | 10 +- astrbot/core/agent/message.py | 4 +- .../agent/runners/coze/coze_agent_runner.py | 9 +- .../dashscope/dashscope_agent_runner.py | 34 ++-- .../runners/deerflow/deerflow_agent_runner.py | 4 +- .../runners/deerflow/deerflow_api_client.py | 3 +- .../deerflow/deerflow_content_mapper.py | 8 +- .../agent/runners/dify/dify_agent_runner.py | 6 +- .../agent/runners/tool_loop_agent_runner.py | 32 ++-- astrbot/core/agent/tool.py | 7 +- astrbot/core/agent/tool_image_cache.py | 4 +- astrbot/core/astr_agent_hooks.py | 15 +- astrbot/core/astr_agent_run_util.py | 14 +- astrbot/core/astr_agent_tool_exec.py | 92 ++++++++--- astrbot/core/astr_main_agent.py | 135 +++++++++++----- astrbot/core/astr_main_agent_resources.py | 15 +- astrbot/core/astrbot_config_mgr.py | 2 +- astrbot/core/backup/exporter.py | 50 ++++-- astrbot/core/backup/importer.py | 27 +++- astrbot/core/computer/booters/bay_manager.py | 9 +- astrbot/core/computer/booters/boxlite.py | 2 +- astrbot/core/computer/booters/bwrap.py | 37 +++-- astrbot/core/computer/booters/local.py | 21 ++- astrbot/core/computer/booters/shipyard.py | 3 +- astrbot/core/computer/booters/shipyard_neo.py | 30 +++- astrbot/core/computer/computer_client.py | 11 +- astrbot/core/computer/olayer/browser.py | 3 +- astrbot/core/computer/olayer/filesystem.py | 18 ++- astrbot/core/computer/olayer/python.py | 3 +- astrbot/core/computer/olayer/shell.py | 3 +- astrbot/core/computer/tools/fs.py | 3 +- astrbot/core/computer/tools/permissions.py | 3 +- astrbot/core/computer/tools/python.py | 14 +- astrbot/core/config/i18n_utils.py | 14 +- astrbot/core/conversation_mgr.py | 4 +- astrbot/core/core_lifecycle.py | 6 +- astrbot/core/cron/manager.py | 29 +++- astrbot/core/db/__init__.py | 20 ++- astrbot/core/db/migration/migra_3_to_4.py | 44 ++++-- .../core/db/migration/migra_token_usage.py | 9 +- .../db/migration/migra_webchat_session.py | 14 +- astrbot/core/db/migration/sqlite_v3.py | 9 +- astrbot/core/db/po.py | 4 +- astrbot/core/db/sqlite.py | 129 ++++++++++++---- astrbot/core/file_token_service.py | 7 +- astrbot/core/knowledge_base/kb_db_sqlite.py | 21 +-- astrbot/core/knowledge_base/kb_helper.py | 22 +-- astrbot/core/log.py | 4 +- astrbot/core/message/components.py | 9 +- astrbot/core/persona_error_reply.py | 9 +- astrbot/core/persona_mgr.py | 30 +++- .../method/agent_sub_stages/internal.py | 36 +++-- .../method/agent_sub_stages/third_party.py | 7 +- .../process_stage/method/star_request.py | 4 +- astrbot/core/pipeline/process_stage/stage.py | 4 +- .../core/pipeline/result_decorate/stage.py | 5 +- astrbot/core/pipeline/scheduler.py | 4 +- astrbot/core/pipeline/waking_check/stage.py | 3 +- astrbot/core/platform/astrbot_message.py | 13 +- astrbot/core/platform/manager.py | 4 +- .../aiocqhttp/aiocqhttp_message_event.py | 20 ++- .../aiocqhttp/aiocqhttp_platform_adapter.py | 28 +++- .../sources/dingtalk/dingtalk_adapter.py | 120 +++++++++++---- .../discord/discord_platform_adapter.py | 16 +- .../sources/discord/discord_platform_event.py | 28 ++-- .../platform/sources/kook/kook_adapter.py | 26 +++- .../core/platform/sources/kook/kook_client.py | 11 +- .../core/platform/sources/kook/kook_event.py | 12 +- .../core/platform/sources/kook/kook_types.py | 38 +++-- .../platform/sources/lark/lark_adapter.py | 37 +++-- .../core/platform/sources/lark/lark_event.py | 41 +++-- astrbot/core/platform/sources/lark/server.py | 6 +- .../platform/sources/line/line_adapter.py | 34 +++- .../core/platform/sources/line/line_event.py | 4 +- .../platform/sources/mattermost/client.py | 8 +- .../sources/misskey/misskey_adapter.py | 47 ++++-- .../platform/sources/misskey/misskey_api.py | 2 +- .../qqofficial/qqofficial_message_event.py | 29 +++- .../qqofficial/qqofficial_platform_adapter.py | 65 +++++--- .../qqofficial_webhook/qo_webhook_adapter.py | 45 ++++-- .../qqofficial_webhook/qo_webhook_server.py | 12 +- .../platform/sources/satori/satori_adapter.py | 4 +- .../platform/sources/satori/satori_event.py | 17 +- astrbot/core/platform/sources/slack/client.py | 8 +- .../platform/sources/slack/slack_adapter.py | 28 +++- .../platform/sources/slack/slack_event.py | 33 ++-- .../platform/sources/telegram/tg_event.py | 77 ++++++++-- .../core/platform/sources/tui/tui_adapter.py | 40 +++-- .../sources/webchat/webchat_adapter.py | 24 ++- .../platform/sources/wecom/wecom_adapter.py | 41 +++-- .../platform/sources/wecom/wecom_event.py | 4 +- .../sources/wecom_ai_bot/wecomai_adapter.py | 108 +++++++++---- .../sources/wecom_ai_bot/wecomai_api.py | 2 +- .../sources/wecom_ai_bot/wecomai_event.py | 9 +- .../wecom_ai_bot/wecomai_long_connection.py | 4 +- .../sources/wecom_ai_bot/wecomai_queue_mgr.py | 4 +- .../sources/wecom_ai_bot/wecomai_webhook.py | 8 +- .../sources/weixin_oc/weixin_oc_adapter.py | 135 +++++++++++----- .../sources/weixin_oc/weixin_oc_client.py | 17 +- .../weixin_offacc_adapter.py | 42 +++-- .../weixin_offacc_event.py | 14 +- astrbot/core/platform_message_history_mgr.py | 5 +- astrbot/core/provider/manager.py | 8 +- astrbot/core/provider/provider.py | 18 ++- .../core/provider/sources/anthropic_source.py | 7 +- .../core/provider/sources/azure_tts_source.py | 9 +- .../provider/sources/bailian_rerank_source.py | 21 ++- .../sources/fishaudio_tts_api_source.py | 7 +- .../sources/gemini_embedding_source.py | 3 +- .../core/provider/sources/gemini_source.py | 40 +++-- astrbot/core/provider/sources/groq_source.py | 3 +- .../core/provider/sources/longcat_source.py | 3 +- .../provider/sources/mimo_tts_api_source.py | 3 +- .../provider/sources/nvidia_rerank_source.py | 23 ++- .../provider/sources/oai_aihubmix_source.py | 3 +- .../core/provider/sources/openai_source.py | 19 ++- .../provider/sources/openai_tts_api_source.py | 15 +- .../provider/sources/openrouter_source.py | 3 +- astrbot/core/provider/sources/xai_source.py | 3 +- .../sources/xinference_rerank_source.py | 21 ++- astrbot/core/skills/neo_skill_sync.py | 3 +- astrbot/core/skills/skill_manager.py | 6 +- astrbot/core/star/command_management.py | 11 +- astrbot/core/star/context.py | 22 ++- astrbot/core/star/filter/command.py | 2 +- astrbot/core/star/filter/command_group.py | 2 +- astrbot/core/star/filter/permission.py | 4 +- astrbot/core/star/register/star_handler.py | 6 +- astrbot/core/star/session_plugin_manager.py | 3 +- astrbot/core/star/star_manager.py | 145 +++++++++++++----- astrbot/core/subagent_orchestrator.py | 4 +- astrbot/core/tools/cron_tools.py | 12 +- astrbot/core/tools/kb_query.py | 4 +- astrbot/core/tools/knowledge_base_tools.py | 4 +- astrbot/core/tools/message_tools.py | 23 ++- astrbot/core/tools/send_message.py | 17 +- astrbot/core/tools/web_search_tools.py | 65 +++++--- astrbot/core/updator.py | 6 +- astrbot/core/utils/auth_password.py | 10 +- astrbot/core/utils/core_constraints.py | 10 +- astrbot/core/utils/io.py | 3 +- astrbot/core/utils/llm_metadata.py | 13 +- astrbot/core/utils/media_utils.py | 4 +- astrbot/core/utils/migra_helper.py | 8 +- astrbot/core/utils/network_utils.py | 3 +- astrbot/core/utils/pip_installer.py | 20 ++- astrbot/core/utils/requirements_utils.py | 4 +- astrbot/core/utils/session_lock.py | 3 +- astrbot/core/utils/shared_preferences.py | 16 +- astrbot/core/utils/temp_dir_cleaner.py | 4 +- astrbot/core/zip_updator.py | 5 +- astrbot/dashboard/routes/auth.py | 7 +- astrbot/dashboard/routes/backup.py | 11 +- astrbot/dashboard/routes/chat.py | 59 +++++-- astrbot/dashboard/routes/config.py | 71 ++++++--- astrbot/dashboard/routes/cron.py | 4 +- astrbot/dashboard/routes/knowledge_base.py | 23 ++- astrbot/dashboard/routes/live_chat.py | 29 +++- astrbot/dashboard/routes/log.py | 11 +- astrbot/dashboard/routes/open_api.py | 23 ++- astrbot/dashboard/routes/persona.py | 11 +- astrbot/dashboard/routes/plugin.py | 11 +- astrbot/dashboard/routes/route.py | 9 +- .../dashboard/routes/session_management.py | 13 +- astrbot/dashboard/routes/skills.py | 27 +++- astrbot/dashboard/routes/stat.py | 13 +- astrbot/dashboard/routes/t2i.py | 4 +- astrbot/dashboard/routes/tools.py | 16 +- astrbot/dashboard/routes/tui_chat.py | 46 ++++-- astrbot/dashboard/server.py | 9 +- astrbot/dashboard/utils.py | 4 +- types/faiss/__init__.pyi | 15 +- types/readability.pyi | 4 +- 209 files changed, 2721 insertions(+), 1070 deletions(-) diff --git a/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py b/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py index ccb87ce4b..b8ffa20a9 100644 --- a/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py +++ b/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py @@ -1,5 +1,4 @@ -"""ABP (AstrBot Protocol) client - in-process star communication. -""" +"""ABP (AstrBot Protocol) client - in-process star communication.""" from __future__ import annotations diff --git a/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py b/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py index 959725fb8..a92338342 100644 --- a/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py +++ b/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py @@ -97,8 +97,7 @@ class BaseAstrbotLspClient(ABC): method: str, params: dict[str, Any] | None = None, ) -> None: - """Send JSON-RPC notification (no response expected). - """ + """Send JSON-RPC notification (no response expected).""" ... @abstractmethod diff --git a/astrbot/_internal/geteway/deps.py b/astrbot/_internal/geteway/deps.py index ecfd20b53..fe2b8bb49 100644 --- a/astrbot/_internal/geteway/deps.py +++ b/astrbot/_internal/geteway/deps.py @@ -1,3 +1 @@ -"""依赖注入 - -""" +"""依赖注入""" diff --git a/astrbot/_internal/geteway/server.py b/astrbot/_internal/geteway/server.py index ffde46e7b..28f03686d 100644 --- a/astrbot/_internal/geteway/server.py +++ b/astrbot/_internal/geteway/server.py @@ -79,7 +79,10 @@ class AstrbotGateway(BaseAstrbotGateway): import uvicorn config = uvicorn.Config( - self._app, host=self._host, port=self._port, log_level="info", + self._app, + host=self._host, + port=self._port, + log_level="info", ) server = uvicorn.Server(config) await server.serve() @@ -137,7 +140,8 @@ class AstrbotGateway(BaseAstrbotGateway): self._app.include_router(memory_router) async def _handle_ws_message( - self, message: dict[str, Any], + self, + message: dict[str, Any], ) -> dict[str, Any] | None: """Handle an incoming WebSocket message. @@ -173,7 +177,9 @@ class AstrbotGateway(BaseAstrbotGateway): } try: result = await self.orchestrator.abp.call_star_tool( - star_name, tool_name, arguments, + star_name, + tool_name, + arguments, ) return {"type": "tool_result", "data": {"result": result}} except Exception as e: diff --git a/astrbot/_internal/geteway/ws_manager.py b/astrbot/_internal/geteway/ws_manager.py index 0cabcd764..b3c82697b 100644 --- a/astrbot/_internal/geteway/ws_manager.py +++ b/astrbot/_internal/geteway/ws_manager.py @@ -1,5 +1,4 @@ -"""WebSocket connection manager for the AstrBot gateway. -""" +"""WebSocket connection manager for the AstrBot gateway.""" from __future__ import annotations @@ -75,7 +74,9 @@ class WebSocketManager: self._connections.discard(conn) async def send_to( - self, websocket: WebSocket, message: str | dict[str, Any], + self, + websocket: WebSocket, + message: str | dict[str, Any], ) -> None: """Send a message to a specific WebSocket. diff --git a/astrbot/_internal/protocols/_abp/client.py b/astrbot/_internal/protocols/_abp/client.py index 911c369d5..00cd7c9b9 100644 --- a/astrbot/_internal/protocols/_abp/client.py +++ b/astrbot/_internal/protocols/_abp/client.py @@ -40,7 +40,10 @@ class AstrbotAbpClient(BaseAstrbotAbpClient): log.info("ABP client connected to internal stars registry.") async def call_star_tool( - self, star_name: str, tool_name: str, arguments: dict[str, Any], + self, + star_name: str, + tool_name: str, + arguments: dict[str, Any], ) -> Any: """Call a tool on a registered star. diff --git a/astrbot/_internal/protocols/_acp/client.py b/astrbot/_internal/protocols/_acp/client.py index 54966b660..65d8a4948 100644 --- a/astrbot/_internal/protocols/_acp/client.py +++ b/astrbot/_internal/protocols/_acp/client.py @@ -137,7 +137,10 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): log.debug(f"ACP notification: {method}") async def call_tool( - self, server_name: str, tool_name: str, arguments: dict[str, Any], + self, + server_name: str, + tool_name: str, + arguments: dict[str, Any], ) -> Any: """Call a tool on an ACP server. @@ -181,7 +184,9 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): await self._writer.drain() async def send_notification( - self, method: str, params: dict[str, Any] | None = None, + self, + method: str, + params: dict[str, Any] | None = None, ) -> None: """Send a one-way notification to the server.""" message = { diff --git a/astrbot/_internal/protocols/_acp/server.py b/astrbot/_internal/protocols/_acp/server.py index dddb625e7..910de7009 100644 --- a/astrbot/_internal/protocols/_acp/server.py +++ b/astrbot/_internal/protocols/_acp/server.py @@ -44,7 +44,9 @@ class AstrbotAcpServer(BaseAstrbotAcpServer): log.debug(f"ACP server registered tool: {name}") def register_notification_handler( - self, name: str, handler: Callable[..., Any], + self, + name: str, + handler: Callable[..., Any], ) -> None: """Register a notification handler. @@ -75,7 +77,9 @@ class AstrbotAcpServer(BaseAstrbotAcpServer): log.info(f"ACP server listening on {host}:{port}") async def _handle_client( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, + self, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, ) -> None: """Handle an incoming ACP client connection.""" addr = writer.get_extra_info("peername") diff --git a/astrbot/_internal/protocols/_lsp/client.py b/astrbot/_internal/protocols/_lsp/client.py index 2b37b9044..27bbbdc23 100644 --- a/astrbot/_internal/protocols/_lsp/client.py +++ b/astrbot/_internal/protocols/_lsp/client.py @@ -96,7 +96,9 @@ class AstrbotLspClient(BaseAstrbotLspClient): log.info(f"LSP client connected to server: {command[0]}") async def send_request( - self, method: str, params: dict[str, Any] | None = None, + self, + method: str, + params: dict[str, Any] | None = None, ) -> Any: """Send an LSP request and wait for response.""" if not self._writer: @@ -135,7 +137,9 @@ class AstrbotLspClient(BaseAstrbotLspClient): raise TimeoutError(f"LSP request {method} timed out") async def send_notification( - self, method: str, params: dict[str, Any] | None = None, + self, + method: str, + params: dict[str, Any] | None = None, ) -> None: """Send an LSP notification (no response expected).""" if not self._writer: diff --git a/astrbot/_internal/protocols/_mcp/client.py b/astrbot/_internal/protocols/_mcp/client.py index c379e63c9..390a792bf 100644 --- a/astrbot/_internal/protocols/_mcp/client.py +++ b/astrbot/_internal/protocols/_mcp/client.py @@ -166,7 +166,10 @@ class McpClient(BaseAstrbotMcpClient): return tools async def call_tool( - self, name: str, arguments: dict[str, Any], read_timeout_seconds: int = 60, + self, + name: str, + arguments: dict[str, Any], + read_timeout_seconds: int = 60, ) -> Any: """Call a tool on the MCP server with reconnection support.""" return await self.call_tool_with_reconnect( @@ -351,7 +354,10 @@ class McpClient(BaseAstrbotMcpClient): self._reconnecting = False async def call_tool_with_reconnect( - self, tool_name: str, arguments: dict, read_timeout_seconds: timedelta, + self, + tool_name: str, + arguments: dict, + read_timeout_seconds: timedelta, ) -> mcp.types.CallToolResult: """Call MCP tool with automatic reconnection on failure, max 2 retries. diff --git a/astrbot/_internal/runtime/orchestrator.py b/astrbot/_internal/runtime/orchestrator.py index e73b1d5bd..e54249628 100644 --- a/astrbot/_internal/runtime/orchestrator.py +++ b/astrbot/_internal/runtime/orchestrator.py @@ -143,8 +143,7 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): self._last_activity_timestamp = time.time() async def shutdown(self) -> None: - """Shutdown the orchestrator and all protocol clients. - """ + """Shutdown the orchestrator and all protocol clients.""" log.info("Shutting down AstrbotOrchestrator...") self._running = False diff --git a/astrbot/_internal/stars/__init__.py b/astrbot/_internal/stars/__init__.py index 7cc9eb82b..1cc5c2193 100644 --- a/astrbot/_internal/stars/__init__.py +++ b/astrbot/_internal/stars/__init__.py @@ -1,5 +1,4 @@ -"""Stars (built-in plugins) for AstrBot runtime. -""" +"""Stars (built-in plugins) for AstrBot runtime.""" from astrbot._internal.stars.runtime_status_star import RuntimeStatusStar diff --git a/astrbot/_internal/stars/runtime_status_star.py b/astrbot/_internal/stars/runtime_status_star.py index e8bd0d89c..ed33ba8cb 100644 --- a/astrbot/_internal/stars/runtime_status_star.py +++ b/astrbot/_internal/stars/runtime_status_star.py @@ -117,7 +117,8 @@ class RuntimeStatusStar: last_ts = getattr(self._orchestrator, "_last_activity_timestamp", None) if last_ts is not None: result["last_activity"] = datetime.fromtimestamp( - last_ts, tz=timezone.utc, + last_ts, + tz=timezone.utc, ).isoformat() else: result["last_activity"] = None diff --git a/astrbot/_internal/tools/base.py b/astrbot/_internal/tools/base.py index ffc0021cc..b88501880 100644 --- a/astrbot/_internal/tools/base.py +++ b/astrbot/_internal/tools/base.py @@ -36,7 +36,8 @@ class ToolSchema: import jsonschema jsonschema.validate( - self.parameters, jsonschema.Draft202012Validator.META_SCHEMA, + self.parameters, + jsonschema.Draft202012Validator.META_SCHEMA, ) return self @@ -196,7 +197,8 @@ class ToolSet: return list(self._tools.values()) def openai_schema( - self, omit_empty_parameter_field: bool = False, + self, + omit_empty_parameter_field: bool = False, ) -> list[dict[str, Any]]: """Convert tools to OpenAI API function calling schema format.""" result: list[dict[str, Any]] = [] @@ -265,7 +267,8 @@ class ToolSet: if target_type in supported_types: result["type"] = target_type if "format" in schema and schema["format"] in supported_formats.get( - result["type"], set(), + result["type"], + set(), ): result["format"] = schema["format"] else: diff --git a/astrbot/_internal/tools/registry.py b/astrbot/_internal/tools/registry.py index 8ad212900..9ae1ace28 100644 --- a/astrbot/_internal/tools/registry.py +++ b/astrbot/_internal/tools/registry.py @@ -121,12 +121,18 @@ class FunctionToolManager: # MCP-related stub methods for base class compatibility async def enable_mcp_server( - self, name: str, config: dict[str, Any], init_timeout: int = 30, + self, + name: str, + config: dict[str, Any], + init_timeout: int = 30, ) -> None: """Enable an MCP server (stub).""" async def disable_mcp_server( - self, name: str = "", timeout: int = 10, shutdown_timeout: int = 10, + self, + name: str = "", + timeout: int = 10, + shutdown_timeout: int = 10, ) -> None: """Disable an MCP server (stub).""" @@ -134,7 +140,8 @@ class FunctionToolManager: """Initialize MCP clients (stub).""" async def test_mcp_server_connection( - self, config: dict[str, Any], + self, + config: dict[str, Any], ) -> tuple[bool, str]: """Test MCP server connection (stub).""" return False, "Not implemented" @@ -266,19 +273,26 @@ class FuncCall(FunctionToolManager): self.remove(name) def get_func_desc_openai_style( - self, omit_empty_parameter_field: bool = False, + self, + omit_empty_parameter_field: bool = False, ) -> list[dict[str, Any]]: """Get tools in OpenAI style (deprecated, use get_full_tool_set().openai_schema()).""" tool_set = self.get_full_tool_set() return tool_set.openai_schema(omit_empty_parameter_field) async def enable_mcp_server( - self, name: str, config: dict[str, Any], init_timeout: int = 30, + self, + name: str, + config: dict[str, Any], + init_timeout: int = 30, ) -> None: """Enable an MCP server (stub implementation).""" async def disable_mcp_server( - self, name: str = "", timeout: int = 10, shutdown_timeout: int = 10, + self, + name: str = "", + timeout: int = 10, + shutdown_timeout: int = 10, ) -> None: """Disable an MCP server (stub implementation).""" @@ -291,7 +305,8 @@ class FuncCall(FunctionToolManager): return True async def test_mcp_server_connection( - self, config: dict[str, Any], + self, + config: dict[str, Any], ) -> tuple[bool, str]: """Test MCP server connection (stub implementation).""" # Import the actual test function if available diff --git a/astrbot/api/tools.py b/astrbot/api/tools.py index b7ba2808d..933e866fe 100644 --- a/astrbot/api/tools.py +++ b/astrbot/api/tools.py @@ -88,7 +88,8 @@ def tool( description: str, parameters: dict[str, Any] | None = None, ) -> Callable[ - [Callable[..., Awaitable[str | None]]], Callable[..., Awaitable[str | None]], + [Callable[..., Awaitable[str | None]]], + Callable[..., Awaitable[str | None]], ]: """Decorator to register an async function as a tool. diff --git a/astrbot/builtin_stars/astrbot/long_term_memory.py b/astrbot/builtin_stars/astrbot/long_term_memory.py index 9e88a7f6c..f530fa4d8 100644 --- a/astrbot/builtin_stars/astrbot/long_term_memory.py +++ b/astrbot/builtin_stars/astrbot/long_term_memory.py @@ -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,9 @@ 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 diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index 789375eb5..7a8c39b74 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -86,7 +86,9 @@ 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 +99,9 @@ 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 76cfd2039..cd1cfb05b 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -64,7 +64,8 @@ class ConversationCommands: if not curr: return None conv = await self.context.conversation_manager.get_conversation( - session_id, curr, + session_id, + curr, ) if not conv: return None @@ -83,7 +84,8 @@ class ConversationCommands: plugin_config = alter_cmd_cfg.get("astrbot", {}) reset_cfg = plugin_config.get("reset", {}) required_perm = reset_cfg.get( - scene.key, "admin" if is_group and (not is_unique_session) else "member", + scene.key, + "admin" if is_group and (not is_unique_session) else "member", ) if required_perm == "admin" and message.role != "admin": message.set_result( @@ -130,7 +132,8 @@ class ConversationCommands: stopped_count = active_event_registry.stop_all(umo, exclude=message) else: stopped_count = active_event_registry.request_agent_stop_all( - umo, exclude=message, + umo, + exclude=message, ) if stopped_count > 0: message.set_result( @@ -154,10 +157,14 @@ class ConversationCommands: session_curr_cid = await conv_mgr.get_curr_conversation_id(umo) if not session_curr_cid: session_curr_cid = await conv_mgr.new_conversation( - umo, message.get_platform_id(), + umo, + message.get_platform_id(), ) contexts, total_pages = await conv_mgr.get_human_readable_context( - umo, session_curr_cid, page, size_per_page, + umo, + session_curr_cid, + page, + size_per_page, ) parts = [] for context in contexts: @@ -198,7 +205,7 @@ class ConversationCommands: "生成所有对话的标题字典" _titles = {} for conv in conversations_all: - title = conv.title if conv.title else "新对话" + title = conv.title or "新对话" _titles[conv.cid] = title "遍历分页后的对话生成列表显示" provider_settings = cfg.get("provider_settings", {}) @@ -266,7 +273,9 @@ class ConversationCommands: active_event_registry.stop_all(message.unified_msg_origin, exclude=message) cpersona = await self._get_current_persona_id(message.unified_msg_origin) cid = await self.context.conversation_manager.new_conversation( - message.unified_msg_origin, message.get_platform_id(), persona_id=cpersona, + message.unified_msg_origin, + message.get_platform_id(), + persona_id=cpersona, ) message.set_extra("_clean_ltm_session", True) message.set_result( @@ -285,7 +294,9 @@ class ConversationCommands: ) cpersona = await self._get_current_persona_id(session) cid = await self.context.conversation_manager.new_conversation( - session, message.get_platform_id(), persona_id=cpersona, + session, + message.get_platform_id(), + persona_id=cpersona, ) message.set_result( MessageEventResult().message( @@ -298,7 +309,9 @@ class ConversationCommands: ) async def switch_conv( - self, message: AstrMessageEvent, index: int | None = None, + self, + message: AstrMessageEvent, + index: int | None = None, ) -> None: """通过 /ls 前面的序号切换对话""" if not isinstance(index, int): @@ -322,9 +335,10 @@ 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, + message.unified_msg_origin, + conversation.cid, ) message.set_result( MessageEventResult().message( @@ -338,7 +352,8 @@ class ConversationCommands: message.set_result(MessageEventResult().message("请输入新的对话名称。")) return await self.context.conversation_manager.update_conversation_title( - message.unified_msg_origin, new_name, + message.unified_msg_origin, + new_name, ) message.set_result(MessageEventResult().message("重命名对话成功。")) @@ -380,7 +395,8 @@ class ConversationCommands: return active_event_registry.stop_all(umo, exclude=message) await self.context.conversation_manager.delete_conversation( - umo, session_curr_cid, + umo, + session_curr_cid, ) ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。" message.set_extra("_clean_ltm_session", True) diff --git a/astrbot/builtin_stars/builtin_commands/commands/help.py b/astrbot/builtin_stars/builtin_commands/commands/help.py index ca6111723..85858f237 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() except BaseException: diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index d051914d0..c3a1779bd 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -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(parts) == 1: diff --git a/astrbot/builtin_stars/builtin_commands/commands/provider.py b/astrbot/builtin_stars/builtin_commands/commands/provider.py index 375f565e5..74887a9d4 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ b/astrbot/builtin_stars/builtin_commands/commands/provider.py @@ -64,7 +64,11 @@ 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 @@ -83,7 +87,10 @@ 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() @@ -122,7 +129,10 @@ 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) @@ -221,7 +231,11 @@ class ProviderCommands: 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) @@ -292,7 +306,8 @@ class ProviderCommands: ) async def _test_provider_capability( - self, provider: ListedProvider, + self, + provider: ListedProvider, ) -> tuple[bool, str | None, str | None]: """测试单个 provider 的可用性""" meta = provider.meta() @@ -305,7 +320,10 @@ 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 @@ -579,7 +597,10 @@ 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: diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index abd69f28f..e416678a1 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -170,7 +170,9 @@ 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/__main__.py b/astrbot/cli/__main__.py index 97dfb5275..54bad9a04 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -34,7 +34,10 @@ def cli() -> None: @click.command() @click.argument("command_name", required=False, type=str) @click.option( - "--all", "-a", is_flag=True, help="Show help for all commands recursively.", + "--all", + "-a", + is_flag=True, + help="Show help for all commands recursively.", ) def help(command_name: str | None, all: bool) -> None: """Display help information for commands @@ -67,7 +70,7 @@ def help(command_name: str | None, all: bool) -> None: command = cli.get_command(ctx, command_name) if command: # Display help for the specific command - parent = ctx.parent if ctx.parent else ctx + parent = ctx.parent or ctx cmd_ctx = click.Context(command, info_name=command.name, parent=parent) click.echo(command.get_help(cmd_ctx)) else: @@ -111,7 +114,10 @@ def completion(shell: str | None) -> None: click.echo(f"No completion support for shell: {shell}", err=True) sys.exit(1) comp = comp_cls( - cli, ctx_args={}, prog_name="astrbot", complete_var="_ASTRBOT_COMPLETE", + cli, + ctx_args={}, + prog_name="astrbot", + complete_var="_ASTRBOT_COMPLETE", ) click.echo(comp.source()) diff --git a/astrbot/cli/commands/cmd_bk.py b/astrbot/cli/commands/cmd_bk.py index 60ece0918..856d091cf 100644 --- a/astrbot/cli/commands/cmd_bk.py +++ b/astrbot/cli/commands/cmd_bk.py @@ -52,7 +52,10 @@ def bk(): @bk.command(name="export") @click.option("--output", "-o", help="Output directory", default=None) @click.option( - "--gpg-sign", "-S", is_flag=True, help="Sign backup with GPG default private key", + "--gpg-sign", + "-S", + is_flag=True, + help="Sign backup with GPG default private key", ) @click.option( "--gpg-encrypt", @@ -61,7 +64,10 @@ def bk(): metavar="RECIPIENT", ) @click.option( - "--gpg-symmetric", "-C", is_flag=True, help="Encrypt with symmetric cipher (GPG)", + "--gpg-symmetric", + "-C", + is_flag=True, + help="Encrypt with symmetric cipher (GPG)", ) @click.option( "--digest", @@ -209,7 +215,8 @@ def export_data( digest_val = hash_func.hexdigest() digest_file = final_path.with_name(final_path.name + f".{digest}") await anyio.Path(digest_file).write_text( - f"{digest_val} *{final_path.name}\n", encoding="utf-8", + f"{digest_val} *{final_path.name}\n", + encoding="utf-8", ) click.echo(click.style(f"Digest generated: {digest_file}", fg="green")) @@ -269,7 +276,9 @@ def import_data_command(backup_file: str, yes: bool): else: click.echo( click.style( - "Digest verification FAILED!", fg="red", bold=True, + "Digest verification FAILED!", + fg="red", + bold=True, ), ) click.echo(f" Expected: {expected_digest}") @@ -284,7 +293,9 @@ def import_data_command(backup_file: str, yes: bool): if not _verify_digest(backup_path): if not yes: if not click.confirm( - "Digest verification failed. Abort import?", default=True, abort=True, + "Digest verification failed. Abort import?", + default=True, + abort=True, ): pass else: @@ -354,12 +365,14 @@ def import_data_command(backup_file: str, yes: bool): try: result = await importer.import_all( - str(zip_path), progress_callback=on_progress, + str(zip_path), + progress_callback=on_progress, ) if result.errors: click.echo( - click.style("\nImport failed with errors:", fg="red"), err=True, + click.style("\nImport failed with errors:", fg="red"), + err=True, ) for err in result.errors: click.echo(f" - {err}", err=True) diff --git a/astrbot/cli/commands/cmd_conf.py b/astrbot/cli/commands/cmd_conf.py index c3ca2c6fb..c072b38d9 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -31,8 +31,7 @@ from astrbot.core.utils.auth_password import ( def is_dashboard_password_hash(value: str) -> bool: - """Heuristic: return True if `value` looks like a supported dashboard password hash. - """ + """Heuristic: return True if `value` looks like a supported dashboard password hash.""" if not isinstance(value, str) or not value: return False return _is_argon2_hash(value) or _is_pbkdf2_hash(value) @@ -130,7 +129,8 @@ def _load_config() -> dict[str, Any]: def _save_config(config: dict[str, Any]) -> None: config_path = astrbot_paths.data / "cmd_config.json" config_path.write_text( - json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig", + json.dumps(config, ensure_ascii=False, indent=2), + encoding="utf-8-sig", ) @@ -176,7 +176,9 @@ def set_dashboard_credentials( ) -> None: if username is not None: _set_nested_item( - config, "dashboard.username", _validate_dashboard_username(username), + config, + "dashboard.username", + _validate_dashboard_username(username), ) if password_hash is not None: if isinstance(password_hash, str) and is_dashboard_password_hash(password_hash): diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index dd1c19683..7ba98535f 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -97,10 +97,13 @@ async def initialize_astrbot( if admin_username: config = ensure_config_file() set_dashboard_credentials( - config, username=effective_admin_username, password_hash=None, + config, + username=effective_admin_username, + password_hash=None, ) config_path.write_text( - json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig", + json.dumps(config, ensure_ascii=False, indent=2), + encoding="utf-8-sig", ) click.echo(f"Configured dashboard admin username: {effective_admin_username}") click.echo( @@ -109,7 +112,8 @@ async def initialize_astrbot( if not backend_only and ( yes or click.confirm( - "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", default=True, + "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", + default=True, ) ): await DashboardManager().ensure_installed(astrbot_root) @@ -171,7 +175,9 @@ def init( click.echo(f"Restoring from backup: {backup}") click.get_current_context().invoke( - import_data_command, backup_file=backup, yes=True, + import_data_command, + backup_file=backup, + yes=True, ) click.echo("Done! You can now run 'astrbot run' to start AstrBot") except Timeout: diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 21271deef..a716ec519 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -66,7 +66,9 @@ _PARAM_EXPAND_RE = re.compile(r"\$\{([^}:]+?)(:-([^}]*))?\}") def _expand_parameter( - match: re.Match, env: dict[str, str], local: dict[str, str], + match: re.Match, + env: dict[str, str], + local: dict[str, str], ) -> str: """Helper to expand a single ${VAR:-default} or ${VAR} occurrence. @@ -338,7 +340,8 @@ def run( while True: try: log_entry = await asyncio.wait_for( - log_queue.get(), timeout=0.5, + log_queue.get(), + timeout=0.5, ) # Format: [LEVEL] message level = log_entry.get("level_name", "INFO") diff --git a/astrbot/cli/commands/cmd_uninstall.py b/astrbot/cli/commands/cmd_uninstall.py index b77a0e1bd..210079ec6 100644 --- a/astrbot/cli/commands/cmd_uninstall.py +++ b/astrbot/cli/commands/cmd_uninstall.py @@ -10,7 +10,9 @@ from astrbot.core.utils.astrbot_path import astrbot_paths @click.command() @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts") @click.option( - "--keep-data", is_flag=True, help="Keep data directory (config, plugins, etc.)", + "--keep-data", + is_flag=True, + help="Keep data directory (config, plugins, etc.)", ) def uninstall(yes: bool, keep_data: bool) -> None: """Remove AstrBot files from the current root directory.""" diff --git a/astrbot/cli/utils/plugin.py b/astrbot/cli/utils/plugin.py index 978b5d302..15ce40d6f 100644 --- a/astrbot/cli/utils/plugin.py +++ b/astrbot/cli/utils/plugin.py @@ -33,7 +33,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) @@ -57,7 +57,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) diff --git a/astrbot/cli/utils/version_comparator.py b/astrbot/cli/utils/version_comparator.py index 2bac3a824..a8267475c 100644 --- a/astrbot/cli/utils/version_comparator.py +++ b/astrbot/cli/utils/version_comparator.py @@ -62,7 +62,9 @@ class VersionComparator: return -1 if isinstance(p1, str) and isinstance(p2, int): return 1 - if (isinstance(p1, int) and isinstance(p2, int)) or (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/__init__.py b/astrbot/core/__init__.py index 2c9a2f707..4cfcbc49b 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -53,7 +53,9 @@ t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2 html_renderer = HtmlRenderer(t2i_base_url) logger = LogManager.GetLogger(log_name="astrbot") LogManager.configure_logger( - logger, astrbot_config, override_level=os.getenv("ASTRBOT_LOG_LEVEL"), + logger, + astrbot_config, + override_level=os.getenv("ASTRBOT_LOG_LEVEL"), ) LogManager.configure_trace_logger(astrbot_config) db_helper = SQLiteDatabase(DB_PATH) diff --git a/astrbot/core/agent/context/compressor.py b/astrbot/core/agent/context/compressor.py index 54826cc1b..8b040684d 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -25,7 +25,10 @@ class ContextCompressor(Protocol): """ 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. @@ -59,7 +62,9 @@ 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. @@ -72,7 +77,10 @@ class TruncateByTurnsCompressor: 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. @@ -100,7 +108,8 @@ 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. @@ -180,7 +189,10 @@ 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. @@ -210,7 +222,8 @@ 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: diff --git a/astrbot/core/agent/context/manager.py b/astrbot/core/agent/context/manager.py index 371a92fe8..e54154a76 100644 --- a/astrbot/core/agent/context/manager.py +++ b/astrbot/core/agent/context/manager.py @@ -43,7 +43,9 @@ class ContextManager: ) 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. @@ -68,11 +70,14 @@ 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) @@ -82,7 +87,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. @@ -111,7 +118,9 @@ 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...", diff --git a/astrbot/core/agent/context/token_counter.py b/astrbot/core/agent/context/token_counter.py index f3e3c0627..6718a70f1 100644 --- a/astrbot/core/agent/context/token_counter.py +++ b/astrbot/core/agent/context/token_counter.py @@ -17,7 +17,9 @@ class TokenCounter(Protocol): """ 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. @@ -50,7 +52,9 @@ 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 85f903f11..ee5602cc3 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -52,7 +52,8 @@ class ContextTruncator: # If a user message exists inside the truncated list, promote it to the front. index_in_truncated = next( - (i for i, m in enumerate(truncated) if m.role == "user"), None, + (i for i, m in enumerate(truncated) if m.role == "user"), + None, ) if index_in_truncated is not None: # Build a new truncated list that places the found user message first, @@ -164,7 +165,9 @@ 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) @@ -193,7 +196,9 @@ 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) @@ -222,6 +227,8 @@ 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 6d27c295a..7f03c1c8a 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -485,7 +485,11 @@ 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, @@ -498,7 +502,9 @@ class MCPTool(FunctionTool, Generic[TContext]): self.source = "mcp" 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 e8b0605d4..4f46ff6ec 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -38,7 +38,9 @@ class ContentPart(BaseModel): @classmethod def __get_pydantic_core_schema__( - cls, source_type: object, handler: GetCoreSchemaHandler, + cls, + source_type: object, + handler: GetCoreSchemaHandler, ) -> core_schema.CoreSchema: # If we're dealing with the base ContentPart class, use custom validation if cls.__name__ == "ContentPart": diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index e9d94145e..b12bb51e9 100644 --- a/astrbot/core/agent/runners/coze/coze_agent_runner.py +++ b/astrbot/core/agent/runners/coze/coze_agent_runner.py @@ -86,8 +86,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.") @@ -108,7 +107,8 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): logger.error(f"Coze 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Coze 请求失败:{e!s}", + role="err", + completion_text=f"Coze 请求失败:{e!s}", ) yield AgentResponse( type="err", @@ -178,7 +178,8 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): if url: file_id = ( await self._download_and_upload_image( - url, session_id, + url, + session_id, ) ) processed_content.append( diff --git a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py index e1888a5a2..4c189fc7e 100644 --- a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py +++ b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py @@ -98,8 +98,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.") @@ -120,7 +119,8 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): logger.error(f"阿里云百炼请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"阿里云百炼请求失败:{e!s}", + role="err", + completion_text=f"阿里云百炼请求失败:{e!s}", ) yield AgentResponse( type="err", @@ -136,7 +136,9 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): yield resp def _consume_sync_generator( - self, response: Any, response_queue: queue.Queue, + self, + response: Any, + response_queue: queue.Queue, ) -> None: """在线程中消费同步generator,将结果放入队列 @@ -157,7 +159,9 @@ 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 @@ -233,7 +237,11 @@ 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 @@ -295,7 +303,9 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): return payload async def _handle_streaming_response( - self, response: Any, session_id: str, + self, + response: Any, + session_id: str, ) -> AsyncGenerator[AgentResponse, None]: """处理流式响应 @@ -320,7 +330,10 @@ 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 @@ -402,7 +415,10 @@ 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 9e2c6ed5f..2f0b5f25e 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py @@ -134,7 +134,9 @@ 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, diff --git a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py index caf61699f..5b23dd2e2 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py @@ -170,7 +170,8 @@ 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. diff --git a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py index 87cb691f4..dbb7e893e 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py @@ -164,14 +164,18 @@ 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 72a892847..34c6e9537 100644 --- a/astrbot/core/agent/runners/dify/dify_agent_runner.py +++ b/astrbot/core/agent/runners/dify/dify_agent_runner.py @@ -78,8 +78,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.") @@ -100,7 +99,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): logger.error(f"Dify 请求失败:{e!s}") self._transition_state(AgentState.ERROR) self.final_llm_resp = LLMResponse( - role="err", completion_text=f"Dify 请求失败:{e!s}", + role="err", + completion_text=f"Dify 请求失败:{e!s}", ) yield AgentResponse( type="err", diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index f7b805563..de224bf38 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -72,7 +72,8 @@ 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) @@ -276,7 +277,9 @@ 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, ) -> AsyncGenerator[LLMResponse, None]: """Yields chunks *and* a final LLMResponse.""" payload: dict[str, Any] = { @@ -527,7 +530,8 @@ 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]") @@ -719,7 +723,8 @@ 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 @@ -977,7 +982,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): 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, ), ) @@ -994,7 +1000,8 @@ 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: @@ -1006,7 +1013,8 @@ 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, ), ) @@ -1027,7 +1035,8 @@ 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, ), ) @@ -1057,7 +1066,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ) def _build_tool_requery_context( - self, tool_names: list[str], extra_instruction: str | None = None, + self, + tool_names: list[str], + extra_instruction: str | None = None, ) -> list[dict[str, Any]]: """Build contexts for re-querying LLM with param-only tool schemas.""" contexts: list[dict[str, Any]] = [] @@ -1105,7 +1116,8 @@ 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) diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 8a1093d12..227b95db5 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -44,7 +44,8 @@ class ToolSchema: def validate_parameters(self) -> "ToolSchema": if self.parameters is not None: jsonschema.validate( - self.parameters, jsonschema.Draft202012Validator.META_SCHEMA, + self.parameters, + jsonschema.Draft202012Validator.META_SCHEMA, ) return self @@ -106,7 +107,9 @@ class FunctionTool(ToolSchema, Generic[TContext]): return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})" async def call( - self, context: ContextWrapper[TContext], **kwargs: Any, + self, + context: ContextWrapper[TContext], + **kwargs: Any, ) -> ToolExecResult: """Run the tool with the given arguments. The handler field has priority.""" raise NotImplementedError( diff --git a/astrbot/core/agent/tool_image_cache.py b/astrbot/core/agent/tool_image_cache.py index 63320bd1f..5531cfa92 100644 --- a/astrbot/core/agent/tool_image_cache.py +++ b/astrbot/core/agent/tool_image_cache.py @@ -114,7 +114,9 @@ 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. diff --git a/astrbot/core/astr_agent_hooks.py b/astrbot/core/astr_agent_hooks.py index 6548801dd..7617fdbdc 100644 --- a/astrbot/core/astr_agent_hooks.py +++ b/astrbot/core/astr_agent_hooks.py @@ -34,7 +34,8 @@ 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( @@ -43,7 +44,9 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): llm_response, ) sdk_plugin_bridge = getattr( - run_context.context.context, "sdk_plugin_bridge", None, + run_context.context.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: @@ -80,7 +83,9 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): tool_args, ) sdk_plugin_bridge = getattr( - run_context.context.context, "sdk_plugin_bridge", None, + run_context.context.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: @@ -113,7 +118,9 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): tool_result, ) sdk_plugin_bridge = getattr( - run_context.context.context, "sdk_plugin_bridge", None, + run_context.context.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: diff --git a/astrbot/core/astr_agent_run_util.py b/astrbot/core/astr_agent_run_util.py index 5b0e43224..de53e305a 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -49,7 +49,8 @@ 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 @@ -67,7 +68,8 @@ 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 = "" @@ -174,7 +176,8 @@ 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), @@ -194,7 +197,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) @@ -283,7 +286,8 @@ 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") diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index a16971458..29a4fefaf 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -58,7 +58,8 @@ 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) @@ -83,7 +84,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @classmethod async def _collect_handoff_image_urls( - cls, run_context: ContextWrapper[AstrAgentContext], image_urls_raw: Any, + cls, + run_context: ContextWrapper[AstrAgentContext], + image_urls_raw: Any, ) -> list[str]: candidates: list[str] = [] candidates.extend(cls._collect_image_urls_from_args(image_urls_raw)) @@ -126,7 +129,9 @@ 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 @@ -143,16 +148,21 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): async def _run_in_background() -> None: try: await cls._execute_background( - tool=tool, run_context=run_context, task_id=task_id, **tool_args, + tool=tool, + run_context=run_context, + task_id=task_id, + **tool_args, ) except Exception as e: logger.error( - f"Background task {task_id} failed: {e!s}", exc_info=True, + f"Background task {task_id} failed: {e!s}", + exc_info=True, ) asyncio.create_task(_run_in_background()) text_content = mcp.types.TextContent( - type="text", text=f"Background task submitted. task_id={task_id}", + type="text", + text=f"Background task submitted. task_id={task_id}", ) yield mcp.types.CallToolResult(content=[text_content]) return @@ -175,7 +185,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @classmethod def _check_sandbox_capability( - cls, tool: FunctionTool, run_context: ContextWrapper[AstrAgentContext], + cls, + tool: FunctionTool, + run_context: ContextWrapper[AstrAgentContext], ) -> mcp.types.CallToolResult | None: """Return a rejection result if the tool requires a sandbox capability that is not available, or None if the tool may proceed. @@ -191,23 +203,31 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): if "browser" not in caps: msg = f"Tool '{tool.name}' requires browser capability, but the current sandbox profile does not include it (capabilities: {list(caps)}). Please ask the administrator to switch to a sandbox profile with browser support, or use shell/python tools instead." logger.warning( - "[ToolExec] capability_rejected tool=%s caps=%s", tool.name, list(caps), + "[ToolExec] capability_rejected tool=%s caps=%s", + tool.name, + list(caps), ) return mcp.types.CallToolResult( - content=[mcp.types.TextContent(type="text", text=msg)], isError=True, + content=[mcp.types.TextContent(type="text", text=msg)], + isError=True, ) return None @classmethod def _get_runtime_computer_tools( - cls, runtime: str, sandbox_cfg: dict | None = None, session_id: str = "", + cls, + runtime: str, + sandbox_cfg: dict | None = None, + session_id: str = "", ) -> dict[str, ToolSchema]: from astrbot.core.computer.computer_tool_provider import ComputerToolProvider from astrbot.core.tool_provider import ToolProviderContext provider = ComputerToolProvider() ctx = ToolProviderContext( - computer_use_runtime=runtime, sandbox_cfg=sandbox_cfg, session_id=session_id, + computer_use_runtime=runtime, + sandbox_cfg=sandbox_cfg, + session_id=session_id, ) tools = provider.get_tools(ctx) result = {tool.name: tool for tool in tools} @@ -232,7 +252,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): runtime = str(provider_settings.get("computer_use_runtime", "local")) sandbox_cfg = provider_settings.get("sandbox", {}) runtime_computer_tools = cls._get_runtime_computer_tools( - runtime, sandbox_cfg=sandbox_cfg, session_id=event.unified_msg_origin, + runtime, + sandbox_cfg=sandbox_cfg, + session_id=event.unified_msg_origin, ) if tools is None: toolset = ToolSet() @@ -283,7 +305,8 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): image_urls = [] else: image_urls = await cls._collect_handoff_image_urls( - run_context, tool_args.get("image_urls"), + run_context, + tool_args.get("image_urls"), ) tool_args["image_urls"] = image_urls toolset = cls._build_handoff_toolset(run_context, tool.agent.tools) @@ -291,7 +314,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): event = run_context.context.event umo = event.unified_msg_origin prov_id = getattr( - tool, "provider_id", None, + tool, + "provider_id", + None, ) or await ctx.get_current_chat_provider_id(umo) contexts = None dialogs = tool.agent.begin_dialogs @@ -345,7 +370,10 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): async def _run_handoff_in_background() -> None: try: await cls._do_handoff_background( - tool=tool, run_context=run_context, task_id=task_id, **tool_args, + tool=tool, + run_context=run_context, + task_id=task_id, + **tool_args, ) except Exception as e: logger.error( @@ -372,11 +400,15 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): result_text = "" tool_args = dict(tool_args) tool_args["image_urls"] = await cls._collect_handoff_image_urls( - run_context, tool_args.get("image_urls"), + run_context, + tool_args.get("image_urls"), ) try: async for r in cls._execute_handoff( - tool, run_context, image_urls_prepared=True, **tool_args, + tool, + run_context, + image_urls_prepared=True, + **tool_args, ): if isinstance(r, mcp.types.CallToolResult): for content in r.content: @@ -410,7 +442,10 @@ 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, ): if isinstance(r, mcp.types.CallToolResult): result_text = "" @@ -499,7 +534,10 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): req.func_tool = ToolSet() req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL) 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}.") @@ -557,7 +595,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): if awaitable is None: raise ValueError("Tool must have a valid handler or override 'run' method.") sdk_plugin_bridge = getattr( - run_context.context.context, "sdk_plugin_bridge", None, + run_context.context.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: @@ -579,7 +619,10 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): | AsyncGenerator[MessageEventResult | CommandResult | str | None, None], ] wrapper = call_local_llm_tool( - context=run_context, handler=awaitable, method_name=method_name, **tool_args, + context=run_context, + handler=awaitable, + method_name=method_name, + **tool_args, ) while True: try: @@ -592,7 +635,8 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): yield resp else: text_content = mcp.types.TextContent( - type="text", text=str(resp), + type="text", + text=str(resp), ) yield mcp.types.CallToolResult(content=[text_content]) else: @@ -600,7 +644,9 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): if res and res.chain: try: await event.send( - MessageChain(chain=res.chain, type="tool_direct_result"), + MessageChain( + chain=res.chain, type="tool_direct_result" + ), ) except Exception as e: logger.error(f"Tool 直接发送消息失败: {e}", exc_info=True) diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 17dc3778e..9e317300e 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -130,7 +130,8 @@ 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") @@ -150,7 +151,8 @@ 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 @@ -177,7 +179,9 @@ async def _apply_kb( return try: kb_result = await retrieve_knowledge_base( - query=req.prompt, umo=event.unified_msg_origin, context=plugin_context, + query=req.prompt, + umo=event.unified_msg_origin, + context=plugin_context, ) if not kb_result: return @@ -194,7 +198,9 @@ async def _apply_kb( async def _apply_file_extract( - event: AstrMessageEvent, req: ProviderRequest, config: MainAgentBuildConfig, + event: AstrMessageEvent, + req: ProviderRequest, + config: MainAgentBuildConfig, ) -> None: file_paths = [] file_names = [] @@ -229,7 +235,8 @@ async def _apply_file_extract( { "role": "system", "content": FILE_EXTRACT_CONTEXT_TEMPLATE.format( - file_content=file_content, file_name=file_name or "Unknown", + file_content=file_content, + file_name=file_name or "Unknown", ), }, ) @@ -246,7 +253,10 @@ def _apply_prompt_prefix(req: ProviderRequest, cfg: dict) -> None: async def _ensure_persona_and_skills( - req: ProviderRequest, cfg: dict, plugin_context: Context, event: AstrMessageEvent, + req: ProviderRequest, + cfg: dict, + plugin_context: Context, + event: AstrMessageEvent, ) -> None: """Ensure persona and skills are applied to the request's system prompt or user prompt.""" if not req.conversation: @@ -263,7 +273,8 @@ async def _ensure_persona_and_skills( provider_settings=cfg, ) 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: if prompt := persona["prompt"]: @@ -368,7 +379,10 @@ async def _ensure_persona_and_skills( async def _request_img_caption( - provider_id: str, cfg: dict, image_urls: list[str], plugin_context: Context, + provider_id: str, + cfg: dict, + image_urls: list[str], + plugin_context: Context, ) -> str: prov = plugin_context.get_provider_by_id(provider_id) if prov is None: @@ -400,7 +414,10 @@ async def _ensure_img_caption( if _is_generated_compressed_image_path(url, compressed_url): event.track_temporary_local_file(compressed_url) caption = await _request_img_caption( - image_caption_provider, cfg, compressed_urls, plugin_context, + image_caption_provider, + cfg, + compressed_urls, + plugin_context, ) if caption: req.extra_user_content_parts.append( @@ -468,7 +485,8 @@ def _get_image_compress_args( async def _compress_image_for_provider( - url_or_path: str, provider_settings: dict[str, object] | None, + url_or_path: str, + provider_settings: dict[str, object] | None, ) -> str: try: enabled, max_size, quality = _get_image_compress_args(provider_settings) @@ -505,7 +523,8 @@ def _get_image_compress_args( async def _compress_image_for_provider( - url_or_path: str, provider_settings: dict[str, object] | None, + url_or_path: str, + provider_settings: dict[str, object] | None, ) -> str: try: enabled, max_size, quality = _get_image_compress_args(provider_settings) @@ -518,7 +537,8 @@ async def _compress_image_for_provider( def _is_generated_compressed_image_path( - original_path: str, compressed_path: str | None, + original_path: str, + compressed_path: str | None, ) -> bool: if not compressed_path or compressed_path == original_path: return False @@ -552,7 +572,8 @@ def _get_image_compress_args( async def _compress_image_for_provider( - url_or_path: str, provider_settings: dict[str, object] | None, + url_or_path: str, + provider_settings: dict[str, object] | None, ) -> str: try: enabled, max_size, quality = _get_image_compress_args(provider_settings) @@ -565,7 +586,8 @@ async def _compress_image_for_provider( def _is_generated_compressed_image_path( - original_path: str, compressed_path: str | None, + original_path: str, + compressed_path: str | None, ) -> bool: if not compressed_path or compressed_path == original_path: return False @@ -593,7 +615,9 @@ async def _process_quote_message( sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else "" message_str = ( await extract_quoted_message_text( - event, quote, settings=quoted_message_settings, + event, + quote, + settings=quoted_message_settings, ) or quote.message_str or "[Empty Text]" @@ -617,7 +641,8 @@ async def _process_quote_message( if prov and isinstance(prov, Provider): path = await image_seg.convert_to_file_path() compress_path = await _compress_image_for_provider( - path, config.provider_settings if config else None, + path, + config.provider_settings if config else None, ) if path and _is_generated_compressed_image_path(path, compress_path): event.track_temporary_local_file(compress_path) @@ -646,7 +671,10 @@ async def _process_quote_message( def _append_system_reminders( - event: AstrMessageEvent, req: ProviderRequest, cfg: dict, timezone: str | None, + event: AstrMessageEvent, + req: ProviderRequest, + cfg: dict, + timezone: str | None, ) -> None: system_parts: list[str] = [] if cfg.get("identifier"): @@ -701,7 +729,12 @@ async def _decorate_llm_request( img_cap_prov_id = cfg.get("default_image_caption_provider_id") or "" quoted_message_settings = _get_quoted_message_parser_settings(cfg) await _process_quote_message( - event, req, img_cap_prov_id, plugin_context, quoted_message_settings, config, + event, + req, + img_cap_prov_id, + plugin_context, + quoted_message_settings, + config, ) tz = config.timezone if tz is None: @@ -714,7 +747,8 @@ 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) @@ -727,7 +761,8 @@ 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) @@ -740,13 +775,16 @@ 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 def _sanitize_context_by_modalities( - config: MainAgentBuildConfig, provider: Provider, req: ProviderRequest, + config: MainAgentBuildConfig, + provider: Provider, + req: ProviderRequest, ) -> None: if not config.sanitize_context_by_modalities: return @@ -844,14 +882,17 @@ def _model_outputs_image(provider: Provider, req: ProviderRequest) -> bool: def _should_disable_streaming_for_webchat_output( - event: AstrMessageEvent, provider: Provider, req: ProviderRequest, + event: AstrMessageEvent, + provider: Provider, + req: ProviderRequest, ) -> bool: if event.get_platform_name() != "webchat": return False provider_cfg = provider.provider_config provider_type = provider_cfg.get("type", "") if provider_type == "googlegenai_chat_completion" and provider_cfg.get( - "gm_resp_image_modal", False, + "gm_resp_image_modal", + False, ): return True if _model_outputs_image(provider, req): @@ -885,7 +926,9 @@ 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 @@ -906,7 +949,9 @@ async def _handle_webchat( ) except Exception as e: logger.exception( - "Failed to generate webchat title for session %s: %s", chatui_session_id, e, + "Failed to generate webchat title for session %s: %s", + chatui_session_id, + e, ) return if llm_resp and llm_resp.completion_text: @@ -914,10 +959,13 @@ 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, display_name=title, + session_id=chatui_session_id, + display_name=title, ) @@ -926,12 +974,14 @@ def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) - req.system_prompt = f"{LLM_SAFETY_MODE_SYSTEM_PROMPT}\n\n{req.system_prompt}" else: logger.warning( - "Unsupported llm_safety_mode strategy: %s.", config.safety_mode_strategy, + "Unsupported llm_safety_mode strategy: %s.", + config.safety_mode_strategy, ) 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 @@ -940,7 +990,8 @@ def _get_compress_provider( provider = plugin_context.get_provider_by_id(config.llm_compress_provider_id) if provider is None: logger.warning( - "未找到指定的上下文压缩模型 %s,将跳过压缩。", config.llm_compress_provider_id, + "未找到指定的上下文压缩模型 %s,将跳过压缩。", + config.llm_compress_provider_id, ) return None if not isinstance(provider, Provider): @@ -953,7 +1004,9 @@ 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): @@ -1026,7 +1079,8 @@ async def build_main_agent( if isinstance(comp, Image): path = await comp.convert_to_file_path() image_path = await _compress_image_for_provider( - path, config.provider_settings, + path, + config.provider_settings, ) if _is_generated_compressed_image_path(path, image_path): event.track_temporary_local_file(image_path) @@ -1061,7 +1115,8 @@ async def build_main_agent( has_embedded_image = True path = await reply_comp.convert_to_file_path() image_path = await _compress_image_for_provider( - path, config.provider_settings, + path, + config.provider_settings, ) if _is_generated_compressed_image_path(path, image_path): event.track_temporary_local_file(image_path) @@ -1083,7 +1138,9 @@ async def build_main_agent( try: fallback_images = normalize_and_dedupe_strings( await extract_quoted_message_images( - event, comp, settings=quoted_message_settings, + event, + comp, + settings=quoted_message_settings, ), ) remaining_limit = max( @@ -1198,7 +1255,9 @@ async def build_main_agent( req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n" streaming_response = config.streaming_response if streaming_response and _should_disable_streaming_for_webchat_output( - event, provider, req, + event, + provider, + req, ): logger.info( "Disable streaming for webchat direct media output. provider=%s model=%s", @@ -1224,7 +1283,9 @@ 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, ), ) if apply_reset: diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index 1da01d9a3..ef304f0af 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -65,7 +65,9 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs, + self, + context: ContextWrapper[AstrAgentContext], + **kwargs, ) -> ToolExecResult: query = kwargs.get("query", "") if not query: @@ -134,7 +136,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): ) async def _resolve_path_from_sandbox( - self, context: ContextWrapper[AstrAgentContext], path: str, + self, + context: ContextWrapper[AstrAgentContext], + path: str, ) -> tuple[str, bool]: """If the path exists locally, return it directly. Otherwise, check if it exists in the sandbox and download it. @@ -156,7 +160,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): # Download the file from sandbox 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}") @@ -168,7 +173,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return path, False async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs: Any, + self, + context: ContextWrapper[AstrAgentContext], + **kwargs: Any, ) -> ToolExecResult: session = kwargs.get("session") or context.context.event.unified_msg_origin messages_raw: list[dict[str, Any]] | None = kwargs.get("messages") diff --git a/astrbot/core/astrbot_config_mgr.py b/astrbot/core/astrbot_config_mgr.py index 6289fc55d..9bc69cc16 100644 --- a/astrbot/core/astrbot_config_mgr.py +++ b/astrbot/core/astrbot_config_mgr.py @@ -113,7 +113,7 @@ class AstrBotConfigManager: scope="global", scope_id="global", ) - abconf_data: dict[str, dict[str, str]] = raw_abconf if raw_abconf else {} + abconf_data: dict[str, dict[str, str]] = raw_abconf or {} random_word = abconf_name or uuid.uuid4().hex[:8] abconf_data[abconf_id] = { "path": abconf_path, diff --git a/astrbot/core/backup/exporter.py b/astrbot/core/backup/exporter.py index f7ade85b4..8c65a16d0 100644 --- a/astrbot/core/backup/exporter.py +++ b/astrbot/core/backup/exporter.py @@ -100,7 +100,10 @@ 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) @@ -116,17 +119,26 @@ 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, + "知识库元数据导出完成", ) # 导出每个知识库的文档数据 @@ -142,7 +154,10 @@ 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) @@ -156,7 +171,10 @@ class AstrBotExporter: if progress_callback: await progress_callback( - "kb_documents", total_kbs, total_kbs, "知识库文档导出完成", + "kb_documents", + total_kbs, + total_kbs, + "知识库文档导出完成", ) # 3. 导出配置文件 @@ -164,7 +182,8 @@ class AstrBotExporter: await progress_callback("config", 0, 100, "正在导出配置文件...") if await anyio.Path(self.config_path).exists(): async with await anyio.open_file( - self.config_path, encoding="utf-8", + self.config_path, + encoding="utf-8", ) as f: config_content = await f.read() zf.writestr("config/cmd_config.json", config_content) @@ -182,7 +201,10 @@ 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: @@ -290,7 +312,10 @@ 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: @@ -309,7 +334,8 @@ class AstrBotExporter: logger.warning(f"导出知识库媒体文件失败: {e}") async def _export_directories( - self, zf: zipfile.ZipFile, + self, + zf: zipfile.ZipFile, ) -> dict[str, dict[str, int]]: """导出插件和其他数据目录 @@ -361,7 +387,9 @@ 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 4b455015b..d78e2cc69 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -469,7 +469,8 @@ 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 @@ -479,7 +480,10 @@ class AstrBotImporter: # 6. 导入插件和其他目录 if progress_callback: await progress_callback( - "directories", 0, 100, "正在导入插件和数据目录...", + "directories", + 0, + 100, + "正在导入插件和数据目录...", ) dir_stats = await self._import_directories(zf, manifest, result) @@ -557,7 +561,8 @@ 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] = {} @@ -587,7 +592,9 @@ class AstrBotImporter: 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) @@ -602,7 +609,8 @@ 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. @@ -770,7 +778,8 @@ class AstrBotImporter: rel_path = name[len(media_prefix) :] target_path = kb_dir / rel_path await anyio.Path(target_path.parent).mkdir( - parents=True, exist_ok=True, + parents=True, + exist_ok=True, ) with zf.open(name) as src: content = src.read() @@ -836,7 +845,8 @@ class AstrBotImporter: target_path = attachments_dir / os.path.basename(name) await anyio.Path(target_path.parent).mkdir( - parents=True, exist_ok=True, + parents=True, + exist_ok=True, ) with zf.open(name) as src: content = src.read() @@ -918,7 +928,8 @@ class AstrBotImporter: target_path = target_dir / rel_path await anyio.Path(target_path.parent).mkdir( - parents=True, exist_ok=True, + parents=True, + exist_ok=True, ) with zf.open(name) as src: diff --git a/astrbot/core/computer/booters/bay_manager.py b/astrbot/core/computer/booters/bay_manager.py index c5c211c7c..56bf03265 100644 --- a/astrbot/core/computer/booters/bay_manager.py +++ b/astrbot/core/computer/booters/bay_manager.py @@ -110,7 +110,8 @@ 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) @@ -128,7 +129,8 @@ 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") @@ -201,7 +203,8 @@ 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 447810e02..99d3bb468 100644 --- a/astrbot/core/computer/booters/boxlite.py +++ b/astrbot/core/computer/booters/boxlite.py @@ -65,7 +65,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) diff --git a/astrbot/core/computer/booters/bwrap.py b/astrbot/core/computer/booters/bwrap.py index 548835b55..2ac55f38d 100644 --- a/astrbot/core/computer/booters/bwrap.py +++ b/astrbot/core/computer/booters/bwrap.py @@ -128,7 +128,7 @@ class BwrapShellComponent(ShellComponent): if env: run_env.update({str(k): str(v) for k, v in env.items()}) - working_dir = cwd if cwd else self.config.workspace_dir + working_dir = cwd or self.config.workspace_dir # Use /bin/sh -c to run the evaluated command # The command must be run inside bwrap @@ -147,7 +147,8 @@ class BwrapShellComponent(ShellComponent): result = subprocess.run( bwrap_cmd, - check=False, cwd=working_dir, + check=False, + cwd=working_dir, env=run_env, timeout=timeout, capture_output=True, @@ -174,12 +175,14 @@ class BwrapPythonComponent(PythonComponent): ) -> dict[str, Any]: def _run() -> dict[str, Any]: bwrap_cmd = build_bwrap_cmd( - self.config, [os.environ.get("PYTHON", "python3"), "-c", code], + self.config, + [os.environ.get("PYTHON", "python3"), "-c", code], ) try: result = subprocess.run( bwrap_cmd, - check=False, timeout=timeout, + check=False, + timeout=timeout, capture_output=True, text=True, ) @@ -221,7 +224,10 @@ class HostBackedFileSystemComponent(FileSystemComponent): return path async def create_file( - self, path: str, content: str = "", mode: int = 0o644, + self, + path: str, + content: str = "", + mode: int = 0o644, ) -> dict[str, Any]: p = self._safe_path(path) await asyncio.to_thread(os.makedirs, os.path.dirname(p), exist_ok=True) @@ -238,7 +244,11 @@ class HostBackedFileSystemComponent(FileSystemComponent): return {"success": False, "error": str(e)} 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]: p = self._safe_path(path) await asyncio.to_thread(os.makedirs, os.path.dirname(p), exist_ok=True) @@ -260,7 +270,9 @@ class HostBackedFileSystemComponent(FileSystemComponent): return {"success": False, "error": str(e)} async def list_dir( - self, path: str = ".", show_hidden: bool = False, + self, + path: str = ".", + show_hidden: bool = False, ) -> dict[str, Any]: p = self._safe_path(path) try: @@ -274,7 +286,9 @@ class HostBackedFileSystemComponent(FileSystemComponent): class BwrapBooter(ComputerBooter): def __init__( - self, rw_binds: list[str] | None = None, ro_binds: list[str] | None = None, + self, + rw_binds: list[str] | None = None, + ro_binds: list[str] | None = None, ): self._rw_binds = rw_binds or [] self._ro_binds = ro_binds or [] @@ -307,7 +321,8 @@ class BwrapBooter(ComputerBooter): async def boot(self, session_id: str) -> None: workspace_dir = os.path.join( - get_astrbot_temp_path(), f"sandbox_workspace_{session_id}", + get_astrbot_temp_path(), + f"sandbox_workspace_{session_id}", ) await asyncio.to_thread(os.makedirs, workspace_dir, exist_ok=True) @@ -342,7 +357,9 @@ class BwrapBooter(ComputerBooter): return if await asyncio.to_thread(os.path.exists, config.workspace_dir): await asyncio.to_thread( - shutil.rmtree, config.workspace_dir, ignore_errors=True, + shutil.rmtree, + config.workspace_dir, + ignore_errors=True, ) async def upload_file(self, path: str, file_name: str) -> dict: diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 2ad2a8773..41ac40657 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -133,7 +133,8 @@ class LocalShellComponent(ShellComponent): # Safety relies on `_is_safe_command()` and the allowed-root checks. result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit command, - check=False, shell=shell, + check=False, + shell=shell, cwd=working_dir, env=run_env, timeout=timeout, @@ -161,7 +162,8 @@ class LocalPythonComponent(PythonComponent): try: result = subprocess.run( [os.environ.get("PYTHON", sys.executable), "-c", code], - check=False, timeout=timeout, + check=False, + timeout=timeout, capture_output=True, text=True, ) @@ -187,7 +189,10 @@ 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) @@ -213,7 +218,11 @@ 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) @@ -236,7 +245,9 @@ 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) diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index f77b5576a..aad51f39e 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -49,7 +49,8 @@ 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 f202cce07..efe29241e 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -132,7 +132,10 @@ class NeoFileSystemComponent(FileSystemComponent): self._sandbox = sandbox async def create_file( - self, path: str, content: str = "", mode: int = 420, + self, + path: str, + content: str = "", + mode: int = 420, ) -> dict[str, Any]: _ = mode await self._sandbox.filesystem.write_file(path, content) @@ -144,7 +147,11 @@ class NeoFileSystemComponent(FileSystemComponent): return {"success": True, "path": path, "content": content} 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]: _ = mode _ = encoding @@ -156,7 +163,9 @@ class NeoFileSystemComponent(FileSystemComponent): return {"success": True, "path": path} async def list_dir( - self, path: str = ".", show_hidden: bool = False, + self, + path: str = ".", + show_hidden: bool = False, ) -> dict[str, Any]: entries = await self._sandbox.filesystem.list_dir(path) data = [] @@ -301,7 +310,8 @@ class ShipyardNeoBooter(ComputerBooter): if not self._access_token: self._access_token = await self._bay_manager.read_credentials() logger.info( - "[Computer] bay_autostart status=ready endpoint=%s", self._endpoint_url, + "[Computer] bay_autostart status=ready endpoint=%s", + self._endpoint_url, ) if not self._endpoint_url or not self._access_token: if self._bay_manager is not None: @@ -314,12 +324,14 @@ class ShipyardNeoBooter(ComputerBooter): from shipyard_neo import BayClient self._client = BayClient( - endpoint_url=self._endpoint_url, access_token=self._access_token, + endpoint_url=self._endpoint_url, + access_token=self._access_token, ) await self._client.__aenter__() resolved_profile = await self._resolve_profile(self._client) self._sandbox = await self._client.create_sandbox( - profile=resolved_profile, ttl=self._ttl, + profile=resolved_profile, + ttl=self._ttl, ) self._fs = NeoFileSystemComponent(self._sandbox) self._python = NeoPythonComponent(self._sandbox) @@ -351,7 +363,8 @@ class ShipyardNeoBooter(ComputerBooter): """ if self._profile and self._profile != self.DEFAULT_PROFILE: logger.info( - "[Computer] profile_selected mode=user profile=%s", self._profile, + "[Computer] profile_selected mode=user profile=%s", + self._profile, ) return self._profile from shipyard_neo.errors import ForbiddenError, UnauthorizedError @@ -434,7 +447,8 @@ class ShipyardNeoBooter(ComputerBooter): remote_path = file_name.lstrip("/") await self._sandbox.filesystem.upload(remote_path, content) logger.info( - "[Computer] file_upload booter=shipyard_neo remote_path=%s", remote_path, + "[Computer] file_upload booter=shipyard_neo remote_path=%s", + remote_path, ) return { "success": True, diff --git a/astrbot/core/computer/computer_client.py b/astrbot/core/computer/computer_client.py index fb684cebd..18f96966d 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -358,7 +358,8 @@ async def _apply_skills_to_sandbox(booter: ComputerBooter) -> None: if not _shell_exec_succeeded(apply_result): detail = _format_exec_error_detail(apply_result) logger.error( - "[Computer] sandbox_sync phase=apply status=failed detail=%s", detail, + "[Computer] sandbox_sync phase=apply status=failed detail=%s", + detail, ) raise RuntimeError(f"Failed to apply sandbox skill sync strategy: {detail}") logger.info("[Computer] sandbox_sync phase=apply status=done") @@ -371,7 +372,8 @@ async def _scan_sandbox_skills(booter: ComputerBooter) -> dict | None: if not _shell_exec_succeeded(scan_result): detail = _format_exec_error_detail(scan_result) logger.error( - "[Computer] sandbox_sync phase=scan status=failed detail=%s", detail, + "[Computer] sandbox_sync phase=scan status=failed detail=%s", + detail, ) raise RuntimeError(f"Failed to scan sandbox skills after sync: {detail}") @@ -477,7 +479,10 @@ 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 diff --git a/astrbot/core/computer/olayer/browser.py b/astrbot/core/computer/olayer/browser.py index 857367698..3b92665f4 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 c50173803..86d2b4b36 100644 --- a/astrbot/core/computer/olayer/filesystem.py +++ b/astrbot/core/computer/olayer/filesystem.py @@ -1,12 +1,14 @@ -"""File system component -""" +"""File system component""" 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""" ... @@ -16,7 +18,11 @@ 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""" ... @@ -26,7 +32,9 @@ 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 fbcf397c4..78bdad0bf 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 82eb9845c..fb9ccdcae 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/fs.py b/astrbot/core/computer/tools/fs.py index 7526137e0..69d2b29b6 100644 --- a/astrbot/core/computer/tools/fs.py +++ b/astrbot/core/computer/tools/fs.py @@ -186,7 +186,8 @@ 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 diff --git a/astrbot/core/computer/tools/permissions.py b/astrbot/core/computer/tools/permissions.py index e8287e423..bd1aef4c9 100644 --- a/astrbot/core/computer/tools/permissions.py +++ b/astrbot/core/computer/tools/permissions.py @@ -3,7 +3,8 @@ 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, diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index f3e828aea..ea098b5de 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -46,7 +46,9 @@ 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", ), ) @@ -68,7 +70,10 @@ class PythonTool(FunctionTool): parameters: dict = field(default_factory=lambda: param_schema) async def call( # type: ignore[override] - 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 @@ -94,7 +99,10 @@ class LocalPythonTool(FunctionTool): parameters: dict = field(default_factory=lambda: param_schema) async def call( # type: ignore[override] - 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 diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py index 1d9c4d1a0..f046ebbb4 100644 --- a/astrbot/core/config/i18n_utils.py +++ b/astrbot/core/config/i18n_utils.py @@ -50,7 +50,10 @@ class ConfigMetadataI18n: result: dict[str, I18nGroup] = {} def convert_items( - group: str, section: str, items: dict[str, object], prefix: str = "", + group: str, + section: str, + items: dict[str, object], + prefix: str = "", ) -> dict[str, object]: items_result: dict[str, object] = {} @@ -82,7 +85,10 @@ class ConfigMetadataI18n: field_items = field_data.get("items") if _is_str_keyed_dict(field_items): field_result["items"] = convert_items( - group, section, field_items, field_path, + group, + section, + field_items, + field_path, ) template_schema = field_data.get("template_schema") @@ -130,7 +136,9 @@ class ConfigMetadataI18n: section_items = section_data.get("items") if _is_str_keyed_dict(section_items): section_result["items"] = convert_items( - group_key, section_key, section_items, + group_key, + section_key, + section_items, ) group_metadata[section_key] = section_result diff --git a/astrbot/core/conversation_mgr.py b/astrbot/core/conversation_mgr.py index b60cd6e1d..e978415c0 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -110,7 +110,9 @@ 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: """切换会话的对话 diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index e8eb9fc41..42be089de 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -346,7 +346,9 @@ 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: @@ -550,7 +552,7 @@ class AstrBotCoreLifecycle: task_name = getattr(task, "__name__", task.__class__.__name__) extra_tasks.append(asyncio.create_task(task, name=task_name)) - 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 c021b1cdd..c7597018f 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -184,7 +184,8 @@ class CronJobManager: trigger = IntervalTrigger(seconds=interval_seconds, timezone=tzinfo) else: trigger = CronTrigger.from_crontab( - job.cron_expression, timezone=tzinfo, + job.cron_expression, + timezone=tzinfo, ) self.scheduler.add_job( self._run_job, @@ -196,7 +197,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: @@ -212,7 +214,10 @@ 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 @@ -270,11 +275,17 @@ class CronJobManager: "cron_payload": payload, } await self._woke_main_agent( - message=note, session_str=session_str, extras=extras, + message=note, + session_str=session_str, + extras=extras, ) async def _woke_main_agent( - self, *, message: str, session_str: str, extras: dict, + self, + *, + message: str, + session_str: str, + extras: dict, ) -> None: """Woke the main agent to handle the cron job message.""" from astrbot.core.astr_main_agent import ( @@ -317,7 +328,8 @@ class CronJobManager: from astrbot.core.computer.computer_tool_provider import ComputerToolProvider tool_call_timeout = cfg.get("provider_settings", {}).get( - "tool_call_timeout", 120, + "tool_call_timeout", + 120, ) config = MainAgentBuildConfig( tool_call_timeout=tool_call_timeout, @@ -345,7 +357,10 @@ class CronJobManager: req.func_tool = ToolSet() req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL) 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 42dc6cf50..c7d9b09c4 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -452,7 +452,8 @@ 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.""" ... @@ -481,14 +482,17 @@ 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.""" ... @@ -721,14 +725,16 @@ 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.""" ... @@ -850,7 +856,9 @@ 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_3_to_4.py b/astrbot/core/db/migration/migra_3_to_4.py index a34e2e00a..3d0e8176f 100644 --- a/astrbot/core/db/migration/migra_3_to_4.py +++ b/astrbot/core/db/migration/migra_3_to_4.py @@ -17,7 +17,8 @@ from .sqlite_v3 import SQLiteDatabase as SQLiteV3DatabaseV3 def get_platform_id( - platform_id_map: dict[str, dict[str, str]], old_platform_name: str, + platform_id_map: dict[str, dict[str, str]], + old_platform_name: str, ) -> str: return platform_id_map.get( old_platform_name, @@ -26,7 +27,8 @@ def get_platform_id( def get_platform_type( - platform_id_map: dict[str, dict[str, str]], old_platform_name: str, + platform_id_map: dict[str, dict[str, str]], + old_platform_name: str, ) -> str: return platform_id_map.get( old_platform_name, @@ -35,13 +37,15 @@ def get_platform_type( async def migration_conversation_table( - db_helper: BaseDatabase, platform_id_map: dict[str, dict[str, str]], + db_helper: BaseDatabase, + platform_id_map: dict[str, dict[str, str]], ) -> None: db_helper_v3 = SQLiteV3DatabaseV3( db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), ) conversations, total_cnt = db_helper_v3.get_all_conversations( - page=1, page_size=10000000, + page=1, + page_size=10000000, ) logger.info(f"迁移 {total_cnt} 条旧的会话数据到新的表中...") async with db_helper.get_db() as dbsession, dbsession.begin(): @@ -64,7 +68,8 @@ async def migration_conversation_table( continue session = MessageSesion.from_str(session_str=conv.user_id) platform_id = get_platform_id( - platform_id_map, session.platform_name, + platform_id_map, + session.platform_name, ) session.platform_id = platform_id conv_v2 = ConversationV2( @@ -87,7 +92,8 @@ async def migration_conversation_table( async def migration_platform_table( - db_helper: BaseDatabase, platform_id_map: dict[str, dict[str, str]], + db_helper: BaseDatabase, + platform_id_map: dict[str, dict[str, str]], ) -> None: db_helper_v3 = SQLiteV3DatabaseV3( db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), @@ -126,10 +132,12 @@ async def migration_platform_table( if cnt == 0: continue platform_id = get_platform_id( - platform_id_map, platform_stats_v3[idx].name, + platform_id_map, + platform_stats_v3[idx].name, ) platform_type = get_platform_type( - platform_id_map, platform_stats_v3[idx].name, + platform_id_map, + platform_stats_v3[idx].name, ) try: await dbsession.execute( @@ -138,7 +146,8 @@ async def migration_platform_table( ), { "timestamp": datetime.datetime.fromtimestamp( - bucket_end, tz=datetime.timezone.utc, + bucket_end, + tz=datetime.timezone.utc, ), "platform_id": platform_id, "platform_type": platform_type, @@ -154,14 +163,16 @@ async def migration_platform_table( async def migration_webchat_data( - db_helper: BaseDatabase, platform_id_map: dict[str, dict[str, str]], + db_helper: BaseDatabase, + platform_id_map: dict[str, dict[str, str]], ) -> None: """迁移 WebChat 的历史记录到新的 PlatformMessageHistory 表中""" db_helper_v3 = SQLiteV3DatabaseV3( db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), ) conversations, total_cnt = db_helper_v3.get_all_conversations( - page=1, page_size=10000000, + page=1, + page_size=10000000, ) logger.info(f"迁移 {total_cnt} 条旧的 WebChat 会话数据到新的表中...") async with db_helper.get_db() as dbsession, dbsession.begin(): @@ -203,7 +214,8 @@ async def migration_webchat_data( async def migration_persona_data( - db_helper: BaseDatabase, astrbot_config: AstrBotConfig, + db_helper: BaseDatabase, + astrbot_config: AstrBotConfig, ) -> None: """迁移 Persona 数据到新的表中。 旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。 @@ -244,7 +256,8 @@ async def migration_persona_data( async def migration_preferences( - db_helper: BaseDatabase, platform_id_map: dict[str, dict[str, str]], + db_helper: BaseDatabase, + platform_id_map: dict[str, dict[str, str]], ) -> None: keys = [ "inactivated_llm_tools", @@ -305,7 +318,10 @@ async def migration_preferences( perf_dict = perf for provider_type, provider_id in perf_dict.items(): await sp.put_async( - "umo", str(session), f"provider_perf_{provider_type}", provider_id, + "umo", + str(session), + f"provider_perf_{provider_type}", + provider_id, ) logger.info(f"迁移会话 {umo} 的提供商偏好到新表成功,平台 ID: {platform_id}") except Exception as e: diff --git a/astrbot/core/db/migration/migra_token_usage.py b/astrbot/core/db/migration/migra_token_usage.py index 3a6a35785..62fcb512f 100644 --- a/astrbot/core/db/migration/migra_token_usage.py +++ b/astrbot/core/db/migration/migra_token_usage.py @@ -19,7 +19,9 @@ 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,7 +40,10 @@ 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 diff --git a/astrbot/core/db/migration/migra_webchat_session.py b/astrbot/core/db/migration/migra_webchat_session.py index 45eb6f194..56d4106b9 100644 --- a/astrbot/core/db/migration/migra_webchat_session.py +++ b/astrbot/core/db/migration/migra_webchat_session.py @@ -25,7 +25,9 @@ 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 +55,10 @@ 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 +76,8 @@ 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 +95,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 539bad1cd..07dc6facd 100644 --- a/astrbot/core/db/migration/sqlite_v3.py +++ b/astrbot/core/db/migration/sqlite_v3.py @@ -227,7 +227,9 @@ 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 +308,10 @@ 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 cae6fb5ae..10e61c868 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -438,7 +438,9 @@ class CommandConflict(TimestampMixin, SQLModel, table=True): __tablename__ = "command_conflicts" 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 11264b6bf..6eff5061a 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -97,14 +97,20 @@ class SQLiteDatabase(BaseDatabase): ) async def insert_platform_stats( - self, platform_id, platform_type, count=1, timestamp=None, + self, + platform_id, + platform_type, + count=1, + timestamp=None, ) -> None: """Insert a new platform statistic record.""" async with self.get_db() as session: async with session.begin(): if timestamp is None: timestamp = datetime.now().replace( - minute=0, second=0, microsecond=0, + minute=0, + second=0, + microsecond=0, ) current_hour = timestamp await session.execute( @@ -212,7 +218,12 @@ class SQLiteDatabase(BaseDatabase): return result.scalars().all() async def get_filtered_conversations( - self, page=1, page_size=20, platform_ids=None, search_query="", **kwargs, + self, + page=1, + page_size=20, + platform_ids=None, + search_query="", + **kwargs, ): async with self.get_db() as session: base_query = select(ConversationV2) @@ -327,7 +338,11 @@ class SQLiteDatabase(BaseDatabase): ) async def get_session_conversations( - self, page=1, page_size=20, search_query=None, platform=None, + self, + page=1, + page_size=20, + search_query=None, + platform=None, ) -> tuple[list[dict], int]: """Get paginated session conversations with joined conversation and persona details.""" async with self.get_db() as session: @@ -349,7 +364,8 @@ class SQLiteDatabase(BaseDatabase): == ConversationV2.conversation_id, ) .outerjoin( - Persona, col(ConversationV2.persona_id) == Persona.persona_id, + Persona, + col(ConversationV2.persona_id) == Persona.persona_id, ) .where(Preference.scope == "umo", Preference.key == "sel_conv_id") ) @@ -380,7 +396,8 @@ class SQLiteDatabase(BaseDatabase): == ConversationV2.conversation_id, ) .outerjoin( - Persona, col(ConversationV2.persona_id) == Persona.persona_id, + Persona, + col(ConversationV2.persona_id) == Persona.persona_id, ) .where(Preference.scope == "umo", Preference.key == "sel_conv_id") ) @@ -413,7 +430,12 @@ class SQLiteDatabase(BaseDatabase): return (sessions_data, total) async def insert_platform_message_history( - self, platform_id, user_id, content, sender_id=None, sender_name=None, + self, + platform_id, + user_id, + content, + sender_id=None, + sender_name=None, ): """Insert a new platform message history record.""" async with self.get_db() as session, session.begin(): @@ -428,7 +450,10 @@ class SQLiteDatabase(BaseDatabase): return new_history async def delete_platform_message_offset( - self, platform_id, user_id, offset_sec=86400, + self, + platform_id, + user_id, + offset_sec=86400, ) -> None: """Delete platform message history records newer than the specified offset.""" async with self.get_db() as session, session.begin(): @@ -443,7 +468,11 @@ class SQLiteDatabase(BaseDatabase): ) async def get_platform_message_history( - self, platform_id, user_id, page=1, page_size=20, + self, + platform_id, + user_id, + page=1, + page_size=20, ): """Get platform message history records.""" async with self.get_db() as session: @@ -460,7 +489,12 @@ class SQLiteDatabase(BaseDatabase): return result.scalars().all() async def list_sdk_platform_message_history( - self, platform_id, user_id, cursor_id=None, limit=50, include_total=False, + self, + platform_id, + user_id, + cursor_id=None, + limit=50, + include_total=False, ): """List SDK message history records ordered by descending id.""" async with self.get_db() as session: @@ -525,7 +559,10 @@ class SQLiteDatabase(BaseDatabase): return int(getattr(result, "rowcount", 0) or 0) async def find_platform_message_history_by_idempotency_key( - self, platform_id, user_id, idempotency_key, + self, + platform_id, + user_id, + idempotency_key, ) -> PlatformMessageHistory | None: """Find a SDK message history record by its idempotency key.""" async with self.get_db() as session: @@ -535,7 +572,8 @@ class SQLiteDatabase(BaseDatabase): PlatformMessageHistory.platform_id == platform_id, PlatformMessageHistory.user_id == user_id, func.json_extract( - PlatformMessageHistory.content, "$.idempotency_key", + PlatformMessageHistory.content, + "$.idempotency_key", ) == str(idempotency_key), ) @@ -545,7 +583,8 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() 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: @@ -792,7 +831,8 @@ 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. @@ -821,7 +861,8 @@ class SQLiteDatabase(BaseDatabase): """Get all persona folders.""" async with self.get_db() as session: 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()) @@ -873,7 +914,9 @@ class SQLiteDatabase(BaseDatabase): ) 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, session.begin(): @@ -885,7 +928,8 @@ class SQLiteDatabase(BaseDatabase): 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. @@ -955,7 +999,10 @@ class SQLiteDatabase(BaseDatabase): existing_preference.value = value else: new_preference = Preference( - scope=scope, scope_id=scope_id, key=key, value=value, + scope=scope, + scope_id=scope_id, + key=key, + value=value, ) session.add(new_preference) return existing_preference or new_preference @@ -1008,7 +1055,8 @@ class SQLiteDatabase(BaseDatabase): await session.commit() async def _run_in_tx( - self, fn: Callable[[AsyncSession], Awaitable[TxResult]], + self, + fn: Callable[[AsyncSession], Awaitable[TxResult]], ) -> TxResult: async with self.get_db() as session, session.begin(): return await fn(session) @@ -1158,7 +1206,8 @@ class SQLiteDatabase(BaseDatabase): await self._run_in_tx(_op) async def list_command_conflicts( - self, status: str | None = None, + self, + status: str | None = None, ) -> list[CommandConflict]: async with self.get_db() as session: query = select(CommandConflict) @@ -1343,7 +1392,8 @@ 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: @@ -1354,7 +1404,8 @@ 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: @@ -1466,7 +1517,9 @@ class SQLiteDatabase(BaseDatabase): return (sessions_with_projects, total) async def update_platform_session( - self, session_id: str, display_name: str | None = None, + self, + session_id: str, + display_name: str | None = None, ) -> None: """Update a Platform session's updated_at timestamp and optionally display_name.""" async with self.get_db() as session, session.begin(): @@ -1498,7 +1551,10 @@ class SQLiteDatabase(BaseDatabase): """Create a new ChatUI project.""" async with self.get_db() as session, session.begin(): project = ChatUIProject( - creator=creator, title=title, emoji=emoji, description=description, + creator=creator, + title=title, + emoji=emoji, + description=description, ) session.add(project) await session.flush() @@ -1509,12 +1565,17 @@ class SQLiteDatabase(BaseDatabase): """Get a ChatUI project by its ID.""" async with self.get_db() as session: result = await session.execute( - select(ChatUIProject).where(col(ChatUIProject.project_id) == project_id), + select(ChatUIProject).where( + col(ChatUIProject.project_id) == project_id + ), ) return result.scalar_one_or_none() async def get_chatui_projects_by_creator( - self, creator: str, page: int = 1, page_size: int = 100, + self, + creator: str, + page: int = 1, + page_size: int = 100, ) -> list[ChatUIProject]: """Get all ChatUI projects for a specific creator.""" async with self.get_db() as session: @@ -1565,7 +1626,9 @@ class SQLiteDatabase(BaseDatabase): ) async def add_session_to_project( - self, session_id: str, project_id: str, + self, + session_id: str, + project_id: str, ) -> SessionProjectRelation: """Add a session to a project.""" async with self.get_db() as session, session.begin(): @@ -1575,7 +1638,8 @@ class SQLiteDatabase(BaseDatabase): ), ) relation = SessionProjectRelation( - session_id=session_id, project_id=project_id, + session_id=session_id, + project_id=project_id, ) session.add(relation) await session.flush() @@ -1592,7 +1656,10 @@ class SQLiteDatabase(BaseDatabase): ) async def get_project_sessions( - self, project_id: str, page: int = 1, page_size: int = 100, + self, + project_id: str, + page: int = 1, + page_size: int = 100, ) -> list[PlatformSession]: """Get all sessions in a project.""" async with self.get_db() as session: @@ -1612,7 +1679,9 @@ 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: diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index 44ca862d5..0837f6757 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -13,7 +13,8 @@ class FileTokenService: def __init__(self, default_timeout: float = 300) -> None: self.lock = asyncio.Lock() self.staged_files: dict[ - str, tuple[str, float], + str, + tuple[str, float], ] = {} # token: (file_path, expire_time) self.default_timeout = default_timeout @@ -32,7 +33,9 @@ class FileTokenService: return file_token not in self.staged_files async def register_file( - self, file_path: str, expire_seconds: float | None = None, + self, + file_path: str, + expire_seconds: float | None = None, ) -> str: """向令牌服务注册一个文件。 diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index e36389116..d487c13ee 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -89,8 +89,7 @@ class KBSQLiteDatabase: # 创建知识库表索引 await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_kb_kb_id " - "ON knowledge_bases(kb_id)", + "CREATE INDEX IF NOT EXISTS idx_kb_kb_id ON knowledge_bases(kb_id)", ), ) await session.execute( @@ -109,20 +108,17 @@ class KBSQLiteDatabase: # 创建文档表索引 await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_doc_doc_id " - "ON kb_documents(doc_id)", + "CREATE INDEX IF NOT EXISTS idx_doc_doc_id ON kb_documents(doc_id)", ), ) await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_doc_kb_id " - "ON kb_documents(kb_id)", + "CREATE INDEX IF NOT EXISTS idx_doc_kb_id ON kb_documents(kb_id)", ), ) await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_doc_name " - "ON kb_documents(doc_name)", + "CREATE INDEX IF NOT EXISTS idx_doc_name ON kb_documents(doc_name)", ), ) await session.execute( @@ -147,8 +143,7 @@ class KBSQLiteDatabase: ) await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_media_doc_id " - "ON kb_media(doc_id)", + "CREATE INDEX IF NOT EXISTS idx_media_doc_id ON kb_media(doc_id)", ), ) await session.execute( @@ -158,8 +153,7 @@ class KBSQLiteDatabase: ) await session.execute( text( - "CREATE INDEX IF NOT EXISTS idx_media_type " - "ON kb_media(media_type)", + "CREATE INDEX IF NOT EXISTS idx_media_type ON kb_media(media_type)", ), ) @@ -258,7 +252,8 @@ 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 f1c1f12da..52691ec53 100644 --- a/astrbot/core/knowledge_base/kb_helper.py +++ b/astrbot/core/knowledge_base/kb_helper.py @@ -57,8 +57,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. @@ -71,7 +70,8 @@ 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 @@ -527,7 +527,8 @@ 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( @@ -567,7 +568,7 @@ class KBHelper: ) # 创建一个虚拟文件名 - 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" @@ -596,15 +597,16 @@ 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}", ) 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: @@ -638,7 +640,9 @@ 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 ] diff --git a/astrbot/core/log.py b/astrbot/core/log.py index f34ea2aad..b13327dc7 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -137,7 +137,9 @@ 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"), diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index acd3675bf..5cc2d135a 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -167,7 +167,8 @@ 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", ) async with await anyio.open_file(file_path, "wb") as f: await f.write(image_bytes) @@ -256,7 +257,8 @@ 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 await anyio.Path(video_file_path).exists(): @@ -452,7 +454,8 @@ 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", ) async with await anyio.open_file(image_file_path, "wb") as f: await f.write(image_bytes) diff --git a/astrbot/core/persona_error_reply.py b/astrbot/core/persona_error_reply.py index e6ea1bb10..82244f517 100644 --- a/astrbot/core/persona_error_reply.py +++ b/astrbot/core/persona_error_reply.py @@ -35,7 +35,8 @@ 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,7 +71,8 @@ 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( @@ -79,7 +81,8 @@ async def resolve_event_conversation_persona_id( 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 3056995a1..19b2dcf7b 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -60,12 +60,14 @@ class PersonaManager: ) async def get_default_persona_v3( - self, umo: str | MessageSession | None = None, + self, + umo: str | MessageSession | None = None, ) -> Personality: """获取默认 persona""" cfg = self.acm.get_conf(umo) default_persona_id = cfg.get("provider_settings", {}).get( - "default_personality", "default", + "default_personality", + "default", ) return self.get_persona_v3_by_id(default_persona_id) or DEFAULT_PERSONALITY @@ -89,7 +91,10 @@ class PersonaManager: """ session_service_config = ( await sp.get_async( - scope="umo", scope_id=str(umo), key="session_service_config", default={}, + scope="umo", + scope_id=str(umo), + key="session_service_config", + default={}, ) or {} ) @@ -102,7 +107,8 @@ class PersonaManager: elif persona_id is None: persona_id = (provider_settings or {}).get("default_personality") persona = next( - (item for item in self.personas_v3 if item["name"] == persona_id), None, + (item for item in self.personas_v3 if item["name"] == persona_id), + None, ) use_webchat_special_default = False if not persona and platform_name == "webchat" and (persona_id != "[%None]"): @@ -144,7 +150,10 @@ class PersonaManager: if custom_error_message is not NOT_GIVEN: update_kwargs["custom_error_message"] = custom_error_message persona = await self.db.update_persona( - persona_id, system_prompt, begin_dialogs, **update_kwargs, + persona_id, + system_prompt, + begin_dialogs, + **update_kwargs, ) if persona: for i, p in enumerate(self.personas): @@ -159,7 +168,8 @@ 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 @@ -170,7 +180,9 @@ class PersonaManager: 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 到指定文件夹 @@ -332,7 +344,9 @@ class PersonaManager: return new_persona async def clone_persona( - self, source_persona_id: str, new_persona_id: str, + self, + source_persona_id: str, + new_persona_id: str, ) -> Persona: """Clone an existing persona with a new ID. 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 fb65f074d..04e0cdd76 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 @@ -69,34 +69,41 @@ class InternalAgentSubStage(Stage): self.show_tool_call_result: bool = settings.get("show_tool_call_result", False) self.show_reasoning = settings.get("display_reasoning_text", False) self.sanitize_context_by_modalities: bool = settings.get( - "sanitize_context_by_modalities", False, + "sanitize_context_by_modalities", + False, ) self.kb_agentic_mode: bool = conf.get("kb_agentic_mode", False) file_extract_conf: dict = settings.get("file_extract", {}) 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"] self.dequeue_context_length: int = min( - max(1, settings["dequeue_context_length"]), self.max_context_length - 1, + max(1, settings["dequeue_context_length"]), + self.max_context_length - 1, ) if self.dequeue_context_length <= 0: self.dequeue_context_length = 1 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") self.sandbox_cfg = settings.get("sandbox", {}) @@ -178,12 +185,15 @@ class InternalAgentSubStage(Stage): logger.warning("send_typing failed", exc_info=True) await call_event_hook(event, EventType.OnWaitingLLMRequestEvent) sdk_plugin_bridge = getattr( - self.ctx.plugin_manager.context, "sdk_plugin_bridge", None, + self.ctx.plugin_manager.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: await sdk_plugin_bridge.dispatch_message_event( - "waiting_llm_request", event, + "waiting_llm_request", + event, ) except Exception as exc: logger.warning("SDK waiting_llm_request dispatch failed: %s", exc) @@ -342,7 +352,10 @@ class InternalAgentSubStage(Stage): ) asyncio.create_task( _record_internal_agent_stats( - event, req, agent_runner, final_resp, + event, + req, + agent_runner, + final_resp, ), ) if not event.is_stopped() or agent_runner.was_aborted(): @@ -408,7 +421,8 @@ class InternalAgentSubStage(Stage): if not user_aborted: return llm_response = LLMResponse( - role="assistant", completion_text=llm_response.completion_text or "", + role="assistant", + completion_text=llm_response.completion_text or "", ) elif llm_response is None: llm_response = LLMResponse(role="assistant", completion_text="") 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 35979c4aa..3b5b2c235 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 @@ -181,7 +181,8 @@ 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( @@ -326,7 +327,9 @@ class ThirdPartyAgentSubStage(Stage): if await call_event_hook(event, EventType.OnLLMRequestEvent, req): return sdk_plugin_bridge = getattr( - self.ctx.plugin_manager.context, "sdk_plugin_bridge", None, + self.ctx.plugin_manager.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index ced4c5594..410682df6 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -60,7 +60,9 @@ class StarRequestSubStage(Stage): traceback_text, ) sdk_plugin_bridge = getattr( - self.ctx.plugin_manager.context, "sdk_plugin_bridge", None, + self.ctx.plugin_manager.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: try: diff --git a/astrbot/core/pipeline/process_stage/stage.py b/astrbot/core/pipeline/process_stage/stage.py index 346703b7c..1ff62e9b1 100644 --- a/astrbot/core/pipeline/process_stage/stage.py +++ b/astrbot/core/pipeline/process_stage/stage.py @@ -17,7 +17,9 @@ class ProcessStage(Stage): self.config = ctx.astrbot_config self.plugin_manager = ctx.plugin_manager self.sdk_plugin_bridge = getattr( - ctx.plugin_manager.context, "sdk_plugin_bridge", None, + ctx.plugin_manager.context, + "sdk_plugin_bridge", + None, ) # initialize agent sub stage diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 3f57518e2..4d183d98d 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -88,7 +88,8 @@ class ResultDecorateStage(Stage): escaped_words_list.sort(key=len, reverse=True) escaped_words = escaped_words_list self.split_words_pattern = re.compile( - f"(.*?({'|'.join(escaped_words)})|.+$)", re.DOTALL, + f"(.*?({'|'.join(escaped_words)})|.+$)", + re.DOTALL, ) else: self.split_words_pattern = None @@ -127,7 +128,7 @@ class ResultDecorateStage(Stage): break if content.strip(): result.append(content) - return result if result else [text] + return result or [text] async def process( self, diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index 4a81e02d5..0ff892410 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -94,7 +94,9 @@ class PipelineScheduler: logger.debug("pipeline 执行完毕。") finally: sdk_plugin_bridge = getattr( - self.ctx.plugin_manager.context, "sdk_plugin_bridge", None, + self.ctx.plugin_manager.context, + "sdk_plugin_bridge", + None, ) if sdk_plugin_bridge is not None: sdk_plugin_bridge.close_request_overlay_for_event(event) diff --git a/astrbot/core/pipeline/waking_check/stage.py b/astrbot/core/pipeline/waking_check/stage.py index 5307071e0..21c4174ab 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -68,7 +68,8 @@ 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/astrbot_message.py b/astrbot/core/platform/astrbot_message.py index 8e0c46b17..8d682d728 100644 --- a/astrbot/core/platform/astrbot_message.py +++ b/astrbot/core/platform/astrbot_message.py @@ -13,10 +13,7 @@ class MessageMember: def __str__(self) -> str: # 使用 f-string 来构建返回的字符串表示形式 - return ( - f"User ID: {self.user_id}," - f"Nickname: {self.nickname if self.nickname else 'N/A'}" - ) + return f"User ID: {self.user_id},Nickname: {self.nickname or 'N/A'}" @dataclass @@ -38,10 +35,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 1adb16ce8..f7bfababf 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -252,7 +252,9 @@ 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: diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 36f1ec725..f99adfcb2 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -159,7 +159,8 @@ class AiocqhttpMessageEvent(AstrMessageEvent): } resp = await bot.call_action("upload_file_stream", **params) if not cls._is_upload_success_response( - resp, expected_statuses=("chunk_received", "file_complete"), + resp, + expected_statuses=("chunk_received", "file_complete"), ): raise OSError(f"上传分片 {i} 失败: {resp}") @@ -167,7 +168,8 @@ class AiocqhttpMessageEvent(AstrMessageEvent): complete_params = {"stream_id": stream_id, "is_complete": True} resp = await bot.call_action("upload_file_stream", **complete_params) if not cls._is_upload_success_response( - resp, expected_statuses=("file_complete",), + resp, + expected_statuses=("file_complete",), ): raise OSError(f"文件合并失败: {resp}") @@ -313,7 +315,11 @@ class AiocqhttpMessageEvent(AstrMessageEvent): # 其他异常:尝试流式重试 try: success = await cls._send_with_stream_retry( - bot, message_chain, event, is_group, session_id, + bot, + message_chain, + event, + is_group, + session_id, ) if success: return @@ -357,11 +363,15 @@ class AiocqhttpMessageEvent(AstrMessageEvent): if is_group: await bot.send_group_file( - group_id=session_id_int, file=file_path, name=file_name, + group_id=session_id_int, + file=file_path, + name=file_name, ) else: await bot.send_private_file( - user_id=session_id_int, file=file_path, name=file_name, + user_id=session_id_int, + file=file_path, + name=file_name, ) else: messages = await cls._parse_onebot_json(MessageChain([seg])) diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py index a551f08e3..12a082320 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -34,7 +34,10 @@ from .aiocqhttp_message_event import AiocqhttpMessageEvent ) class AiocqhttpAdapter(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.settings = platform_settings @@ -99,7 +102,9 @@ class AiocqhttpAdapter(Platform): logger.info("aiocqhttp(OneBot v11) 适配器已连接。") async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: is_group = session.message_type == MessageType.GROUP_MESSAGE if is_group: @@ -132,7 +137,8 @@ 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"): @@ -157,7 +163,8 @@ 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"): @@ -181,7 +188,9 @@ class AiocqhttpAdapter(Platform): return abm async def _convert_handle_message_event( - self, event: Event, get_reply=True, + self, + event: Event, + get_reply=True, ) -> AstrBotMessage: """OneBot V11 消息类事件 @@ -276,7 +285,8 @@ class AiocqhttpAdapter(Platform): else: try: reply_event_data = await self.bot.call_action( - action="get_msg", message_id=int(m["data"]["id"]), + action="get_msg", + message_id=int(m["data"]["id"]), ) reply_event_data["post_type"] = "message" new_event = Event.from_payload(reply_event_data) @@ -286,7 +296,8 @@ class AiocqhttpAdapter(Platform): ) continue abm_reply = await self._convert_handle_message_event( - new_event, get_reply=False, + new_event, + get_reply=False, ) reply_seg = Reply( id=abm_reply.message_id, @@ -326,7 +337,8 @@ class AiocqhttpAdapter(Platform): no_cache=False, ) nickname = at_info.get("nick", "") or at_info.get( - "nickname", "", + "nickname", + "", ) is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"} abm.message.append(At(qq=m["data"]["qq"], name=nickname)) diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 52036853b..6bd01365b 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -41,11 +41,16 @@ 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__( - 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.client_id = platform_config["client_id"] @@ -65,7 +70,8 @@ class DingtalkPlatformAdapter(Platform): client = dingtalk_stream.DingTalkStreamClient(credential, logger=logger) client.register_all_event_handler(MyEventHandler()) client.register_callback_handler( - dingtalk_stream.ChatbotMessage.TOPIC, self.client, + dingtalk_stream.ChatbotMessage.TOPIC, + self.client, ) self.client_ = client self._shutdown_event: threading.Event | None = None @@ -79,7 +85,9 @@ class DingtalkPlatformAdapter(Platform): return dingtalk_id or "unknown" async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: robot_code = self.client_id if session.message_type == MessageType.GROUP_MESSAGE: @@ -97,17 +105,23 @@ class DingtalkPlatformAdapter(Platform): ) staff_id = session.session_id await self.send_message_chain_to_user( - staff_id=staff_id, robot_code=robot_code, message_chain=message_chain, + staff_id=staff_id, + robot_code=robot_code, + message_chain=message_chain, ) await super().send_by_session(session, message_chain) async def send_with_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: await self.send_by_session(session, message_chain) async def send_with_sesison( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: await self.send_by_session(session, message_chain) @@ -121,7 +135,8 @@ class DingtalkPlatformAdapter(Platform): ) async def convert_msg( - self, message: dingtalk_stream.ChatbotMessage, + self, + message: dingtalk_stream.ChatbotMessage, ) -> AstrBotMessage: abm = AstrBotMessage() abm.message = [] @@ -133,7 +148,8 @@ class DingtalkPlatformAdapter(Platform): else MessageType.FRIEND_MESSAGE ) abm.sender = MessageMember( - user_id=self._id_to_sid(message.sender_id), nickname=message.sender_nick, + user_id=self._id_to_sid(message.sender_id), + nickname=message.sender_nick, ) abm.self_id = self._id_to_sid(message.chatbot_user_id) abm.message_id = message.message_id @@ -169,7 +185,9 @@ class DingtalkPlatformAdapter(Platform): logger.warning("钉钉图片消息缺少 downloadCode,已跳过") else: f_path = await self.download_ding_file( - download_code, robot_code, "jpg", + download_code, + robot_code, + "jpg", ) if f_path: abm.message.append(Image.fromFileSystem(f_path)) @@ -196,7 +214,9 @@ class DingtalkPlatformAdapter(Platform): ) continue f_path = await self.download_ding_file( - download_code, robot_code, "jpg", + download_code, + robot_code, + "jpg", ) if f_path: abm.message.append(Image.fromFileSystem(f_path)) @@ -213,7 +233,9 @@ class DingtalkPlatformAdapter(Platform): voice_ext = "amr" voice_ext = voice_ext.lstrip(".") f_path = await self.download_ding_file( - download_code, robot_code, voice_ext, + download_code, + robot_code, + voice_ext, ) if f_path: abm.message.append(Record.fromFileSystem(f_path)) @@ -231,7 +253,9 @@ class DingtalkPlatformAdapter(Platform): if not file_ext: file_ext = "file" f_path = await self.download_ding_file( - download_code, robot_code, file_ext, + download_code, + robot_code, + file_ext, ) if f_path: if not file_name: @@ -241,7 +265,9 @@ class DingtalkPlatformAdapter(Platform): return abm async def _remember_sender_binding( - self, message: dingtalk_stream.ChatbotMessage, abm: AstrBotMessage, + self, + message: dingtalk_stream.ChatbotMessage, + abm: AstrBotMessage, ) -> None: try: if abm.type == MessageType.FRIEND_MESSAGE: @@ -256,13 +282,19 @@ class DingtalkPlatformAdapter(Platform): ), ) await sp.put_async( - "global", umo, "dingtalk_staffid", sender_staff_id, + "global", + umo, + "dingtalk_staffid", + sender_staff_id, ) except Exception as e: logger.warning(f"保存钉钉会话映射失败: {e}") async def download_ding_file( - self, download_code: str, robot_code: str, ext: str, + self, + download_code: str, + robot_code: str, + ext: str, ) -> str: """下载钉钉文件 @@ -304,7 +336,8 @@ class DingtalkPlatformAdapter(Platform): async def get_access_token(self) -> str: try: access_token = await asyncio.get_running_loop().run_in_executor( - None, self.client_.get_access_token, + None, + self.client_.get_access_token, ) if access_token: return access_token @@ -313,7 +346,8 @@ class DingtalkPlatformAdapter(Platform): payload = {"appKey": self.client_id, "appSecret": self.client_secret} async with aiohttp.ClientSession() as session: async with session.post( - "https://api.dingtalk.com/v1.0/oauth2/accessToken", json=payload, + "https://api.dingtalk.com/v1.0/oauth2/accessToken", + json=payload, ) as resp: if resp.status != 200: logger.error( @@ -326,7 +360,10 @@ class DingtalkPlatformAdapter(Platform): async def _get_sender_staff_id(self, session: MessageSesion) -> str: try: staff_id = await sp.get_async( - "global", str(session), "dingtalk_staffid", "", + "global", + str(session), + "dingtalk_staffid", + "", ) return staff_id or "" except Exception as e: @@ -334,7 +371,11 @@ class DingtalkPlatformAdapter(Platform): return "" async def _send_group_message( - self, open_conversation_id: str, robot_code: str, msg_key: str, msg_param: dict, + self, + open_conversation_id: str, + robot_code: str, + msg_key: str, + msg_param: dict, ) -> None: access_token = await self.get_access_token() if not access_token: @@ -350,18 +391,25 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session, session.post( - "https://api.dingtalk.com/v1.0/robot/groupMessages/send", - headers=headers, - json=payload, - ) as resp: + 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, staff_id: str, robot_code: str, msg_key: str, msg_param: dict, + self, + staff_id: str, + robot_code: str, + msg_key: str, + msg_param: dict, ) -> None: access_token = await self.get_access_token() if not access_token: @@ -377,11 +425,14 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session, session.post( - "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", - headers=headers, - json=payload, - ) as resp: + 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()}", @@ -485,7 +536,8 @@ class DingtalkPlatformAdapter(Platform): if not photo_url: continue await send_message( - msg_key="sampleImageMsg", msg_param={"photoURL": photo_url}, + msg_key="sampleImageMsg", + msg_param={"photoURL": photo_url}, ) elif isinstance(segment, Record): converted_audio = None @@ -623,7 +675,9 @@ class DingtalkPlatformAdapter(Platform): logger.error("钉钉私聊回复失败: 缺少 sender_staff_id") return await self.send_message_chain_to_user( - staff_id=staff_id, robot_code=robot_code, message_chain=message_chain, + staff_id=staff_id, + robot_code=robot_code, + message_chain=message_chain, ) async def handle_msg(self, abm: AstrBotMessage) -> None: diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 7e86c5594..14e8fa3a5 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -39,7 +39,9 @@ else: # 注册平台适配器 @register_platform_adapter( - "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False, + "discord", + "Discord 适配器 (基于 Pycord)", + support_streaming_message=False, ) class DiscordPlatformAdapter(Platform): def __init__( @@ -160,7 +162,8 @@ 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 @@ -193,7 +196,8 @@ 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)) @@ -245,7 +249,8 @@ class DiscordPlatformAdapter(Platform): # 如果机器人被 @,在 message_chain 开头添加 At 组件 if self.client and self.client.user and bot_was_mentioned: message_chain.insert( - 0, At(qq=str(self.client.user.id), name=self.client.user.name), + 0, + At(qq=str(self.client.user.id), name=self.client.user.name), ) if abm.message_str: message_chain.append(Plain(text=abm.message_str)) @@ -508,7 +513,8 @@ 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}") diff --git a/astrbot/core/platform/sources/discord/discord_platform_event.py b/astrbot/core/platform/sources/discord/discord_platform_event.py index 05820200a..7071c0c30 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_event.py +++ b/astrbot/core/platform/sources/discord/discord_platform_event.py @@ -86,7 +86,9 @@ 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: @@ -114,7 +116,8 @@ class DiscordPlatformEvent(AstrMessageEvent): return None async def _parse_to_discord( - self, message: MessageChain, + self, + message: MessageChain, ) -> tuple[ str, list[discord.File], @@ -155,7 +158,8 @@ class DiscordPlatformEvent(AstrMessageEvent): if await asyncio.to_thread(path.exists): file_bytes = await asyncio.to_thread(path.read_bytes) discord_file = discord.File( - BytesIO(file_bytes), filename=filename or path.name, + BytesIO(file_bytes), + filename=filename or path.name, ) else: logger.warning(f"[Discord] 图片文件不存在: {path}") @@ -167,7 +171,8 @@ class DiscordPlatformEvent(AstrMessageEvent): b64_data += "=" * (4 - missing_padding) img_bytes = base64.b64decode(b64_data) discord_file = discord.File( - BytesIO(img_bytes), filename=filename or "image.png", + BytesIO(img_bytes), + filename=filename or "image.png", ) else: try: @@ -178,7 +183,8 @@ class DiscordPlatformEvent(AstrMessageEvent): b64_data += "=" * (4 - missing_padding) img_bytes = base64.b64decode(b64_data) discord_file = discord.File( - BytesIO(img_bytes), filename=filename or "image.png", + BytesIO(img_bytes), + filename=filename or "image.png", ) except (ValueError, TypeError, binascii.Error): logger.debug( @@ -188,7 +194,8 @@ class DiscordPlatformEvent(AstrMessageEvent): if await asyncio.to_thread(path.exists): file_bytes = await asyncio.to_thread(path.read_bytes) discord_file = discord.File( - BytesIO(file_bytes), filename=filename or path.name, + BytesIO(file_bytes), + filename=filename or path.name, ) else: logger.warning(f"[Discord] 图片文件不存在: {path}") @@ -237,7 +244,8 @@ class DiscordPlatformEvent(AstrMessageEvent): """对原消息添加反应""" try: if hasattr(self.message_obj, "raw_message") and hasattr( - self.message_obj.raw_message, "add_reaction", + self.message_obj.raw_message, + "add_reaction", ): await self.message_obj.raw_message.add_reaction(emoji) except Exception as e: @@ -274,7 +282,8 @@ class DiscordPlatformEvent(AstrMessageEvent): def is_mentioned(self) -> bool: """判断机器人是否被@""" if hasattr(self.message_obj, "raw_message") and hasattr( - self.message_obj.raw_message, "mentions", + self.message_obj.raw_message, + "mentions", ): return any( mention.id == int(self.message_obj.self_id) @@ -285,7 +294,8 @@ class DiscordPlatformEvent(AstrMessageEvent): def get_mention_clean_content(self) -> str: """获取去除@后的清洁内容""" if hasattr(self.message_obj, "raw_message") and hasattr( - self.message_obj.raw_message, "clean_content", + self.message_obj.raw_message, + "clean_content", ): return 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 f778c22a1..3fdeb5467 100644 --- a/astrbot/core/platform/sources/kook/kook_adapter.py +++ b/astrbot/core/platform/sources/kook/kook_adapter.py @@ -43,7 +43,10 @@ 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 +58,9 @@ 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 +76,9 @@ 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: @@ -136,7 +143,8 @@ 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 检查 @@ -157,7 +165,8 @@ 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,7 +201,9 @@ 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 "" @@ -341,7 +352,8 @@ 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 68034e2c0..dae4c8e75 100644 --- a/astrbot/core/platform/sources/kook/kook_client.py +++ b/astrbot/core/platform/sources/kook/kook_client.py @@ -123,7 +123,9 @@ 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: @@ -147,7 +149,9 @@ 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() @@ -289,7 +293,8 @@ 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) diff --git a/astrbot/core/platform/sources/kook/kook_event.py b/astrbot/core/platform/sources/kook/kook_event.py index 2ba0c74d6..e21cde527 100644 --- a/astrbot/core/platform/sources/kook/kook_event.py +++ b/astrbot/core/platform/sources/kook/kook_event.py @@ -48,10 +48,14 @@ 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 +97,9 @@ 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 diff --git a/astrbot/core/platform/sources/kook/kook_types.py b/astrbot/core/platform/sources/kook/kook_types.py index 47e11d266..5faada4bb 100644 --- a/astrbot/core/platform/sources/kook/kook_types.py +++ b/astrbot/core/platform/sources/kook/kook_types.py @@ -57,7 +57,14 @@ 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 +283,9 @@ 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 @@ -351,7 +360,8 @@ class KookExtra(KookBaseDataClass): class KookMessageEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.MESSAGE] = Field( - KookMessageSignal.MESSAGE, exclude=True, + KookMessageSignal.MESSAGE, + exclude=True, ) """only for type hint""" @@ -369,7 +379,8 @@ class KookMessageEventData(KookBaseDataClass): class KookHelloEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.HELLO] = Field( - KookMessageSignal.HELLO, exclude=True, + KookMessageSignal.HELLO, + exclude=True, ) """only for type hint""" @@ -379,28 +390,32 @@ 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""" @@ -410,7 +425,8 @@ 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""" @@ -421,7 +437,9 @@ 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 4ed4c201b..352251155 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -32,11 +32,16 @@ from .server import LarkWebhookServer @register_platform_adapter( - "lark", "飞书机器人官方 API 适配器", support_streaming_message=True, + "lark", + "飞书机器人官方 API 适配器", + support_streaming_message=True, ) class LarkPlatformAdapter(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.appid = platform_config["app_id"] @@ -81,7 +86,11 @@ class LarkPlatformAdapter(Platform): self.event_id_timestamps: dict[str, float] = {} async def _download_message_resource( - self, *, message_id: str, file_key: str, resource_type: str, + self, + *, + message_id: str, + file_key: str, + resource_type: str, ) -> bytes | None: if self.lark_api.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -212,7 +221,9 @@ class LarkPlatformAdapter(Platform): logger.error("[Lark] 图片消息缺少 message_id") continue image_bytes = await self._download_message_resource( - message_id=message_id, file_key=image_key, resource_type="image", + message_id=message_id, + file_key=image_key, + resource_type="image", ) if image_bytes is None: continue @@ -295,7 +306,8 @@ class LarkPlatformAdapter(Platform): return components async def _build_reply_from_parent_id( - self, parent_message_id: str, + self, + parent_message_id: str, ) -> Comp.Reply | None: if self.lark_api.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -366,7 +378,9 @@ class LarkPlatformAdapter(Platform): default_suffix: str = ".bin", ) -> str | None: file_bytes = await self._download_message_resource( - message_id=message_id, file_key=file_key, resource_type="file", + message_id=message_id, + file_key=file_key, + resource_type="file", ) if file_bytes is None: return None @@ -407,7 +421,9 @@ class LarkPlatformAdapter(Platform): return False async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: if session.message_type == MessageType.GROUP_MESSAGE: id_type = "chat_id" @@ -418,7 +434,10 @@ class LarkPlatformAdapter(Platform): id_type = "open_id" receive_id = session.session_id await LarkMessageEvent.send_message_chain( - message_chain, self.lark_api, receive_id=receive_id, receive_id_type=id_type, + message_chain, + self.lark_api, + receive_id=receive_id, + receive_id_type=id_type, ) await super().send_by_session(session, message_chain) @@ -462,7 +481,7 @@ class LarkPlatformAdapter(Platform): for m in message.mentions: if m.id is None: continue - 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: if m.id.open_id is not None: diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index 3a31c0c73..b170e601c 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -211,7 +211,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) @@ -228,7 +228,7 @@ class LarkMessageEvent(AstrMessageEvent): await asyncio.to_thread(_write_image) else: - file_path = comp.file if comp.file else "" + file_path = comp.file or "" if image_file is None: if not file_path: @@ -567,17 +567,29 @@ 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: @@ -609,7 +621,9 @@ class LarkMessageEvent(AstrMessageEvent): """ 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 @@ -650,7 +664,8 @@ class LarkMessageEvent(AstrMessageEvent): return if not original_audio_path or not await asyncio.to_thread( - os.path.exists, original_audio_path, + os.path.exists, + original_audio_path, ): logger.error(f"[Lark] 音频文件不存在: {original_audio_path}") return @@ -682,7 +697,8 @@ class LarkMessageEvent(AstrMessageEvent): # 清理转换后的临时音频文件 if converted_audio_path and await asyncio.to_thread( - os.path.exists, converted_audio_path, + os.path.exists, + converted_audio_path, ): try: await asyncio.to_thread(os.remove, converted_audio_path) @@ -728,7 +744,8 @@ class LarkMessageEvent(AstrMessageEvent): return if not original_video_path or not await asyncio.to_thread( - os.path.exists, original_video_path, + os.path.exists, + original_video_path, ): logger.error(f"[Lark] 视频文件不存在: {original_video_path}") return @@ -760,7 +777,8 @@ class LarkMessageEvent(AstrMessageEvent): # 清理转换后的临时视频文件 if converted_video_path and await asyncio.to_thread( - os.path.exists, converted_video_path, + os.path.exists, + converted_video_path, ): try: await asyncio.to_thread(os.remove, converted_video_path) @@ -1100,7 +1118,8 @@ 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 f10ce260a..f7e715092 100644 --- a/astrbot/core/platform/sources/lark/server.py +++ b/astrbot/core/platform/sources/lark/server.py @@ -163,7 +163,11 @@ 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 diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index 14319ecb4..3ed40dbc0 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -69,7 +69,10 @@ LINE_I18N_RESOURCES = { ) class LinePlatformAdapter(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.config["unified_webhook_mode"] = True @@ -82,11 +85,14 @@ class LinePlatformAdapter(Platform): if not channel_access_token or not channel_secret: raise ValueError("LINE 适配器需要 channel_access_token 和 channel_secret。") self.line_api = LineAPIClient( - channel_access_token=channel_access_token, channel_secret=channel_secret, + channel_access_token=channel_access_token, + channel_secret=channel_secret, ) async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: messages = await LineMessageEvent.build_line_messages(message_chain) if messages: @@ -265,7 +271,9 @@ class LinePlatformAdapter(Platform): return ret async def _build_image_component( - self, message_id: str, message: dict[str, Any], + self, + message_id: str, + message: dict[str, Any], ) -> Image | None: external_url = self._get_external_content_url(message) if external_url: @@ -277,7 +285,9 @@ class LinePlatformAdapter(Platform): return Image.fromBytes(content_bytes) async def _build_video_component( - self, message_id: str, message: dict[str, Any], + self, + message_id: str, + message: dict[str, Any], ) -> Video | None: external_url = self._get_external_content_url(message) if external_url: @@ -291,7 +301,9 @@ class LinePlatformAdapter(Platform): return Video(file=file_path, path=file_path) async def _build_audio_component( - self, message_id: str, message: dict[str, Any], + self, + message_id: str, + message: dict[str, Any], ) -> Record | None: external_url = self._get_external_content_url(message) if external_url: @@ -305,7 +317,9 @@ class LinePlatformAdapter(Platform): return Record(file=file_path, url=file_path) async def _build_file_component( - self, message_id: str, message: dict[str, Any], + self, + message_id: str, + message: dict[str, Any], ) -> File | None: content = await self.line_api.get_message_content(message_id) if not content: @@ -315,7 +329,11 @@ class LinePlatformAdapter(Platform): suffix = Path(default_name).suffix or self._guess_suffix(content_type, ".bin") final_name = filename or default_name file_path = self._store_temp_content( - "file", message_id, content_bytes, suffix, original_name=final_name, + "file", + message_id, + content_bytes, + suffix, + original_name=final_name, ) return File(name=final_name, file=file_path, url=file_path) diff --git a/astrbot/core/platform/sources/line/line_event.py b/astrbot/core/platform/sources/line/line_event.py index 05babbab0..545f08e6e 100644 --- a/astrbot/core/platform/sources/line/line_event.py +++ b/astrbot/core/platform/sources/line/line_event.py @@ -232,7 +232,9 @@ class LineMessageEvent(AstrMessageEvent): await super().send(message) async def send_streaming( - self, generator: AsyncGenerator, use_fallback: bool = False, + self, + generator: AsyncGenerator, + use_fallback: bool = False, ): if not use_fallback: buffer = None diff --git a/astrbot/core/platform/sources/mattermost/client.py b/astrbot/core/platform/sources/mattermost/client.py index 1e3a1744d..37ba33474 100644 --- a/astrbot/core/platform/sources/mattermost/client.py +++ b/astrbot/core/platform/sources/mattermost/client.py @@ -139,7 +139,9 @@ 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) @@ -218,7 +220,9 @@ 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/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index cb6d812ec..5336191b2 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -44,11 +44,16 @@ DEFAULT_UPLOAD_CONCURRENCY = 3 @register_platform_adapter( - "misskey", "Misskey 平台适配器", support_streaming_message=False, + "misskey", + "Misskey 平台适配器", + support_streaming_message=False, ) class MisskeyPlatformAdapter(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 or {}, event_queue) self.settings = platform_settings or {} @@ -56,7 +61,8 @@ class MisskeyPlatformAdapter(Platform): self.access_token = self.config.get("misskey_token", "") self.max_message_length = self.config.get("max_message_length", 3000) self.default_visibility = self.config.get( - "misskey_default_visibility", "public", + "misskey_default_visibility", + "public", ) self.local_only = self.config.get("misskey_local_only", False) self.enable_chat = self.config.get("misskey_enable_chat", True) @@ -141,12 +147,17 @@ class MisskeyPlatformAdapter(Platform): if self.enable_chat: streaming.add_message_handler("newChatMessage", self._handle_chat_message) streaming.add_message_handler( - "messaging:newChatMessage", self._handle_chat_message, + "messaging:newChatMessage", + self._handle_chat_message, ) streaming.add_message_handler("_debug", self._debug_handler) async def _send_text_only_message( - self, session_id: str, text: str, session, message_chain, + self, + session_id: str, + text: str, + session, + message_chain, ): """发送纯文本消息(无文件上传)""" if not self.api: @@ -166,7 +177,10 @@ class MisskeyPlatformAdapter(Platform): return await super().send_by_session(session, message_chain) def _process_poll_data( - self, message: AstrBotMessage, poll: dict[str, Any], message_parts: list[str], + self, + message: AstrBotMessage, + poll: dict[str, Any], + message_parts: list[str], ) -> None: """处理投票数据,将其添加到消息中""" try: @@ -190,7 +204,8 @@ class MisskeyPlatformAdapter(Platform): fields["cw"] = comp.cw break if hasattr(session, "extra_data") and isinstance( - getattr(session, "extra_data", None), dict, + getattr(session, "extra_data", None), + dict, ): extra_data = session.extra_data fields.update( @@ -323,7 +338,9 @@ class MisskeyPlatformAdapter(Platform): return False async def send_by_session( - self, session: MessageSession, message_chain: MessageChain, + self, + session: MessageSession, + message_chain: MessageChain, ) -> None: if not self.api: logger.error("[Misskey] API 客户端未初始化") @@ -362,12 +379,16 @@ class MisskeyPlatformAdapter(Platform): fallback_urls: list[str] = [] if not self.enable_file_upload: return await self._send_text_only_message( - session_id, text, session, message_chain, + session_id, + text, + session, + message_chain, ) MAX_UPLOAD_CONCURRENCY = 10 upload_concurrency = int( self.config.get( - "misskey_upload_concurrency", DEFAULT_UPLOAD_CONCURRENCY, + "misskey_upload_concurrency", + DEFAULT_UPLOAD_CONCURRENCY, ), ) upload_concurrency = min(upload_concurrency, MAX_UPLOAD_CONCURRENCY) @@ -391,7 +412,9 @@ class MisskeyPlatformAdapter(Platform): if not url_candidate and (not local_path): return None preferred_name = getattr(comp, "name", None) or getattr( - comp, "file", None, + comp, + "file", + None, ) if url_candidate: result = await self.api.upload_and_find_file( @@ -601,7 +624,7 @@ class MisskeyPlatformAdapter(Platform): message.message.append(Comp.Plain(raw_text)) 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 f6cdf2091..14f822a2e 100644 --- a/astrbot/core/platform/sources/misskey/misskey_api.py +++ b/astrbot/core/platform/sources/misskey/misskey_api.py @@ -552,7 +552,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 7ae0a2c81..781100efe 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -239,7 +239,8 @@ class QQOfficialMessageEvent(AstrMessageEvent): payload["content"] = plain_text or None ret = await self._send_with_markdown_fallback( send_func=lambda retry_payload: self.bot.api.post_group_message( - group_openid=source.group_openid or "", **retry_payload, + group_openid=source.group_openid or "", + **retry_payload, ), payload=payload, plain_text=plain_text, @@ -304,7 +305,8 @@ class QQOfficialMessageEvent(AstrMessageEvent): else: ret = await self._send_with_markdown_fallback( send_func=lambda retry_payload: self.post_c2c_message( - openid=source.author.user_openid, **retry_payload, + openid=source.author.user_openid, + **retry_payload, ), payload=payload, plain_text=plain_text, @@ -317,7 +319,8 @@ class QQOfficialMessageEvent(AstrMessageEvent): payload.pop("msg_type", None) ret = await self._send_with_markdown_fallback( send_func=lambda retry_payload: self.bot.api.post_message( - channel_id=source.channel_id, **retry_payload, + channel_id=source.channel_id, + **retry_payload, ), payload=payload, plain_text=plain_text, @@ -329,7 +332,8 @@ class QQOfficialMessageEvent(AstrMessageEvent): payload.pop("msg_type", None) ret = await self._send_with_markdown_fallback( send_func=lambda retry_payload: self.bot.api.post_dms( - guild_id=source.guild_id, **retry_payload, + guild_id=source.guild_id, + **retry_payload, ), payload=payload, plain_text=plain_text, @@ -342,7 +346,11 @@ class QQOfficialMessageEvent(AstrMessageEvent): return ret async def _send_with_markdown_fallback( - self, send_func, payload: dict, plain_text: str, stream: dict | None = None, + self, + send_func, + payload: dict, + plain_text: str, + stream: dict | None = None, ): try: return await send_func(payload) @@ -380,7 +388,10 @@ class QQOfficialMessageEvent(AstrMessageEvent): return await send_func(fallback_payload) async def upload_group_and_c2c_image( - self, image_base64: str, file_type: int, **kwargs, + self, + image_base64: str, + file_type: int, + **kwargs, ) -> botpy.types.message.Media: payload = { "file_data": image_base64, @@ -522,11 +533,13 @@ class QQOfficialMessageEvent(AstrMessageEvent): record_wav_path = await i.convert_to_file_path() temp_dir = get_astrbot_temp_path() record_tecent_silk_path = os.path.join( - temp_dir, f"qqofficial_{uuid.uuid4()}.silk", + temp_dir, + f"qqofficial_{uuid.uuid4()}.silk", ) try: duration = await wav_to_tencent_silk( - record_wav_path, record_tecent_silk_path, + record_wav_path, + record_tecent_silk_path, ) if duration > 0: record_file_path = record_tecent_silk_path diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py index 9ab243591..0038d9b9c 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -42,10 +42,12 @@ class botClient(Client): self.platform = platform 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, + message, + MessageType.GROUP_MESSAGE, ) abm.group_id = message.group_openid abm.session_id = abm.group_id @@ -54,7 +56,8 @@ class botClient(Client): async def on_at_message_create(self, message: botpy.message.Message) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.GROUP_MESSAGE, + message, + MessageType.GROUP_MESSAGE, ) abm.group_id = message.channel_id abm.session_id = abm.group_id @@ -62,10 +65,12 @@ class botClient(Client): self._commit(abm) async def on_direct_message_create( - self, message: botpy.message.DirectMessage, + self, + message: botpy.message.DirectMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.FRIEND_MESSAGE, + message, + MessageType.FRIEND_MESSAGE, ) abm.session_id = abm.sender.user_id self.platform.remember_session_scene(abm.session_id, "friend") @@ -73,7 +78,8 @@ class botClient(Client): async def on_c2c_message_create(self, message: botpy.message.C2CMessage) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.FRIEND_MESSAGE, + message, + MessageType.FRIEND_MESSAGE, ) abm.session_id = abm.sender.user_id self.platform.remember_session_scene(abm.session_id, "friend") @@ -95,7 +101,10 @@ class botClient(Client): @register_platform_adapter("qq_official", "QQ 机器人官方 API 适配器") class QQOfficialPlatformAdapter(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.appid = platform_config["appid"] @@ -110,7 +119,8 @@ class QQOfficialPlatformAdapter(Platform): ) else: self.intents = botpy.Intents( - public_guild_messages=True, direct_message=guild_dm, + public_guild_messages=True, + direct_message=guild_dm, ) self.client = botClient(intents=self.intents, bot_log=False, timeout=20) self.client.set_platform(self) @@ -119,12 +129,16 @@ class QQOfficialPlatformAdapter(Platform): self.test_mode = os.environ.get("TEST_MODE", "off") == "on" async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: await self._send_by_session_common(session, message_chain) async def _send_by_session_common( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: ( plain_text, @@ -201,13 +215,15 @@ class QQOfficialPlatformAdapter(Platform): payload["msg_type"] = 7 payload.pop("msg_id", None) ret = await self.client.api.post_group_message( - group_openid=session.session_id, **payload, + group_openid=session.session_id, + **payload, ) else: if image_path: payload["file_image"] = image_path ret = await self.client.api.post_message( - channel_id=session.session_id, **payload, + channel_id=session.session_id, + **payload, ) elif session.message_type == MessageType.FRIEND_MESSAGE: payload.pop("msg_id", None) @@ -253,7 +269,9 @@ class QQOfficialPlatformAdapter(Platform): payload["media"] = media payload["msg_type"] = 7 ret = await QQOfficialMessageEvent.post_c2c_message( - send_helper, openid=session.session_id, **payload, + send_helper, + openid=session.session_id, + **payload, ) else: logger.warning( @@ -313,7 +331,8 @@ class QQOfficialPlatformAdapter(Platform): @staticmethod async def _append_attachments( - msg: list[BaseMessageComponent], attachments: list | None, + msg: list[BaseMessageComponent], + attachments: list | None, ) -> None: if not attachments: return @@ -340,7 +359,8 @@ class QQOfficialPlatformAdapter(Platform): try: msg.append( await QQOfficialPlatformAdapter._prepare_audio_attachment( - url, filename, + url, + filename, ), ) except Exception as e: @@ -410,7 +430,8 @@ class QQOfficialPlatformAdapter(Platform): abm.message_id = message.id msg: list[BaseMessageComponent] = [] if isinstance(message, botpy.message.GroupMessage) or isinstance( - message, botpy.message.C2CMessage, + message, + botpy.message.C2CMessage, ): if isinstance(message, botpy.message.GroupMessage): abm.sender = MessageMember(message.author.member_openid, "") @@ -424,11 +445,13 @@ class QQOfficialPlatformAdapter(Platform): 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 elif isinstance(message, botpy.message.Message) or isinstance( - message, botpy.message.DirectMessage, + message, + botpy.message.DirectMessage, ): if isinstance(message, botpy.message.Message): abm.self_id = str(message.mentions[0].id) @@ -438,12 +461,14 @@ class QQOfficialPlatformAdapter(Platform): message.content.replace("<@!" + str(abm.self_id) + ">", "").strip(), ) await QQOfficialPlatformAdapter._append_attachments( - msg, message.attachments, + msg, + message.attachments, ) abm.message = msg abm.message_str = plain_content abm.sender = MessageMember( - str(message.author.id), str(message.author.username), + str(message.author.id), + str(message.author.username), ) msg.append(At(qq="qq_official")) msg.append(Plain(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 493a71ce8..07e52a0dd 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_adapter.py @@ -28,10 +28,12 @@ class botClient(Client): self.platform = platform 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, + message, + MessageType.GROUP_MESSAGE, ) abm.group_id = message.group_openid abm.session_id = abm.group_id @@ -40,7 +42,8 @@ class botClient(Client): async def on_at_message_create(self, message: botpy.message.Message) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.GROUP_MESSAGE, + message, + MessageType.GROUP_MESSAGE, ) abm.group_id = message.channel_id abm.session_id = abm.group_id @@ -48,10 +51,12 @@ class botClient(Client): self._commit(abm) async def on_direct_message_create( - self, message: botpy.message.DirectMessage, + self, + message: botpy.message.DirectMessage, ) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.FRIEND_MESSAGE, + message, + MessageType.FRIEND_MESSAGE, ) abm.session_id = abm.sender.user_id self.platform.remember_session_scene(abm.session_id, "friend") @@ -59,7 +64,8 @@ class botClient(Client): async def on_c2c_message_create(self, message: botpy.message.C2CMessage) -> None: abm = await QQOfficialPlatformAdapter._parse_from_qqofficial( - message, MessageType.FRIEND_MESSAGE, + message, + MessageType.FRIEND_MESSAGE, ) abm.session_id = abm.sender.user_id self.platform.remember_session_scene(abm.session_id, "friend") @@ -69,7 +75,11 @@ class botClient(Client): self.platform.remember_session_message_id(abm.session_id, abm.message_id) self.platform.commit_event( QQOfficialWebhookMessageEvent( - abm.message_str, abm, self.platform.meta(), abm.session_id, self, + abm.message_str, + abm, + self.platform.meta(), + abm.session_id, + self, ), ) @@ -77,14 +87,19 @@ class botClient(Client): @register_platform_adapter("qq_official_webhook", "QQ 机器人官方 API 适配器(Webhook)") class QQOfficialWebhookPlatformAdapter(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.appid = platform_config["appid"] self.secret = platform_config["secret"] self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False) intents = botpy.Intents( - public_messages=True, public_guild_messages=True, direct_message=True, + public_messages=True, + public_guild_messages=True, + direct_message=True, ) self.client = botClient(intents=intents, bot_log=False, timeout=20) self.client.set_platform(self) @@ -93,10 +108,14 @@ class QQOfficialWebhookPlatformAdapter(Platform): self._session_scene: dict[str, str] = {} async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: await QQOfficialPlatformAdapter._send_by_session_common( - self, session, message_chain, + self, + session, + message_chain, ) def remember_session_message_id(self, session_id: str, message_id: str) -> None: @@ -128,7 +147,9 @@ class QQOfficialWebhookPlatformAdapter(Platform): async def run(self) -> None: self.webhook_helper = QQOfficialWebhook( - self.config, self._event_queue, self.client, + self.config, + self._event_queue, + self.client, ) await self.webhook_helper.initialize() webhook_uuid = self.config.get("webhook_uuid") 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 d18481ba0..b07ab76d2 100644 --- a/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py +++ b/astrbot/core/platform/sources/qqofficial_webhook/qo_webhook_server.py @@ -14,7 +14,10 @@ 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"] @@ -28,7 +31,9 @@ class QQOfficialWebhook: self.token = Token(self.appid, self.secret) self.server = quart.Quart(__name__) self.server.add_url_rule( - "/astrbot-qo-webhook/callback", view_func=self.callback, methods=["POST"], + "/astrbot-qo-webhook/callback", + view_func=self.callback, + methods=["POST"], ) self.client = botpy_client self.event_queue = event_queue @@ -64,7 +69,8 @@ class QQOfficialWebhook: seed = await self.repeat_seed(self.secret) private_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed) msg = validation_payload.get("event_ts", "") + validation_payload.get( - "plain_token", "", + "plain_token", + "", ) signature = private_key.sign(msg.encode()).hex() response = { diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index a38fcb632..3c92974e2 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -30,7 +30,9 @@ 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/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 05ea55c62..57033c043 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -53,7 +53,10 @@ class SatoriPlatformEvent(AstrMessageEvent): @classmethod async def send_with_adapter( - cls, adapter: "SatoriPlatformAdapter", message: MessageChain, session_id: str, + cls, + adapter: "SatoriPlatformAdapter", + message: MessageChain, + session_id: str, ): try: content_parts = [] @@ -82,7 +85,11 @@ class SatoriPlatformEvent(AstrMessageEvent): user = current_login.get("user", {}) user_id = user.get("id", "") if user else "" result = await adapter.send_http_request( - "POST", "/message.create", data, platform, user_id, + "POST", + "/message.create", + data, + platform, + user_id, ) if result: return result @@ -118,7 +125,11 @@ class SatoriPlatformEvent(AstrMessageEvent): channel_id = self.session_id data = {"channel_id": channel_id, "content": content} result = await self.adapter.send_http_request( - "POST", "/message.create", data, platform, user_id, + "POST", + "/message.create", + data, + platform, + user_id, ) if not result: logger.error("Satori 消息发送失败") diff --git a/astrbot/core/platform/sources/slack/client.py b/astrbot/core/platform/sources/slack/client.py index 131bacf87..6e44215af 100644 --- a/astrbot/core/platform/sources/slack/client.py +++ b/astrbot/core/platform/sources/slack/client.py @@ -127,7 +127,9 @@ class SlackSocketClient: self.socket_client = None async def _handle_events( - self, _: AsyncBaseSocketModeClient, req: SocketModeRequest, + self, + _: AsyncBaseSocketModeClient, + req: SocketModeRequest, ) -> None: """处理 Socket Mode 事件""" try: @@ -143,7 +145,9 @@ class SlackSocketClient: async def start(self) -> None: """启动 Socket Mode 连接""" self.socket_client = SocketModeClient( - app_token=self.app_token, logger=logger, web_client=self.web_client, + app_token=self.app_token, + logger=logger, + web_client=self.web_client, ) self.socket_client.socket_mode_request_listeners.append(self._handle_events) logger.info("Slack Socket Mode 客户端启动中...") diff --git a/astrbot/core/platform/sources/slack/slack_adapter.py b/astrbot/core/platform/sources/slack/slack_adapter.py index 624890b3c..510bee956 100644 --- a/astrbot/core/platform/sources/slack/slack_adapter.py +++ b/astrbot/core/platform/sources/slack/slack_adapter.py @@ -34,7 +34,10 @@ from .slack_event import SlackMessageEvent ) class SlackAdapter(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.settings = platform_settings @@ -46,7 +49,8 @@ class SlackAdapter(Platform): self.webhook_host = platform_config.get("slack_webhook_host", "0.0.0.0") self.webhook_port = platform_config.get("slack_webhook_port", 3000) self.webhook_path = platform_config.get( - "slack_webhook_path", "/astrbot-slack-webhook/callback", + "slack_webhook_path", + "/astrbot-slack-webhook/callback", ) if not self.bot_token: raise ValueError("Slack bot_token 是必需的") @@ -66,10 +70,13 @@ class SlackAdapter(Platform): self.bot_self_id = None async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: blocks, text = await SlackMessageEvent._parse_slack_blocks( - message_chain=message_chain, web_client=self.web_client, + message_chain=message_chain, + web_client=self.web_client, ) try: if session.message_type == MessageType.GROUP_MESSAGE: @@ -79,13 +86,15 @@ class SlackAdapter(Platform): else session.session_id ) await self.web_client.chat_postMessage( - channel=channel_id, text=text, blocks=blocks if blocks else None, + channel=channel_id, + text=text, + 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}") @@ -138,7 +147,8 @@ class SlackAdapter(Platform): mentioned_user = await self.web_client.users_info(user=mention) user_data = mentioned_user["user"] user_name = user_data.get("real_name") or user_data.get( - "name", mention, + "name", + mention, ) abm.message.append(At(qq=mention, name=user_name)) except Exception: @@ -262,7 +272,9 @@ class SlackAdapter(Platform): if not self.app_token: raise ValueError("Socket Mode 需要 app_token") self.socket_client = SlackSocketClient( - self.web_client, self.app_token, self._handle_socket_event, + self.web_client, + self.app_token, + self._handle_socket_event, ) logger.info("Slack 适配器 (Socket Mode) 启动中...") await self.socket_client.start() diff --git a/astrbot/core/platform/sources/slack/slack_event.py b/astrbot/core/platform/sources/slack/slack_event.py index 6343c7b16..da90922ac 100644 --- a/astrbot/core/platform/sources/slack/slack_event.py +++ b/astrbot/core/platform/sources/slack/slack_event.py @@ -24,7 +24,8 @@ class SlackMessageEvent(AstrMessageEvent): @staticmethod async def _from_segment_to_slack_block( - segment: BaseMessageComponent, web_client: AsyncWebClient, + segment: BaseMessageComponent, + web_client: AsyncWebClient, ) -> dict | None: """将消息段转换为 Slack 块格式""" if isinstance(segment, Plain): @@ -51,7 +52,8 @@ class SlackMessageEvent(AstrMessageEvent): if isinstance(segment, File): url = segment.url or segment.file response = await web_client.files_upload_v2( - file=url, filename=segment.name or "file", + file=url, + filename=segment.name or "file", ) if not response["ok"]: logger.error(f"Slack file upload failed: {response['error']}") @@ -71,7 +73,8 @@ class SlackMessageEvent(AstrMessageEvent): @staticmethod async def _parse_slack_blocks( - message_chain: MessageChain, web_client: AsyncWebClient, + message_chain: MessageChain, + web_client: AsyncWebClient, ): """解析成 Slack 块格式""" blocks = [] @@ -89,7 +92,8 @@ class SlackMessageEvent(AstrMessageEvent): ) text_content = "" block = await SlackMessageEvent._from_segment_to_slack_block( - segment, web_client, + segment, + web_client, ) if block: blocks.append(block) @@ -101,16 +105,21 @@ class SlackMessageEvent(AstrMessageEvent): async def send(self, message: MessageChain) -> None: blocks, text = await SlackMessageEvent._parse_slack_blocks( - message, self.web_client, + message, + self.web_client, ) try: if self.get_group_id(): await self.web_client.chat_postMessage( - channel=self.get_group_id(), text=text, blocks=blocks or None, + channel=self.get_group_id(), + text=text, + blocks=blocks or None, ) else: await self.web_client.chat_postMessage( - channel=self.get_sender_id(), text=text, blocks=blocks or None, + channel=self.get_sender_id(), + text=text, + blocks=blocks or None, ) except Exception: parts = [] @@ -124,16 +133,20 @@ class SlackMessageEvent(AstrMessageEvent): fallback_text = "".join(parts) if self.get_group_id(): await self.web_client.chat_postMessage( - channel=self.get_group_id(), text=fallback_text, + channel=self.get_group_id(), + text=fallback_text, ) else: await self.web_client.chat_postMessage( - channel=self.get_sender_id(), text=fallback_text, + channel=self.get_sender_id(), + text=fallback_text, ) await super().send(message) async def send_streaming( - self, generator: AsyncGenerator, use_fallback: bool = False, + self, + generator: AsyncGenerator, + use_fallback: bool = False, ): if not use_fallback: buffer = None diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index 4344443d1..ec752874a 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -126,14 +126,20 @@ class TelegramPlatformEvent(AstrMessageEvent): """发送媒体时显示 upload action,发送完成后恢复 typing""" effective_thread_id = message_thread_id or 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 @@ -193,11 +199,16 @@ class TelegramPlatformEvent(AstrMessageEvent): await client.send_document(document=path, caption=caption, **payload) async def _ensure_typing( - self, user_name: str, message_thread_id: str | None = None, + self, + user_name: str, + message_thread_id: str | None = None, ) -> 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: @@ -212,7 +223,10 @@ class TelegramPlatformEvent(AstrMessageEvent): @classmethod async def send_with_client( - cls, client: ExtBot, message: MessageChain, user_name: str, + cls, + client: ExtBot, + message: MessageChain, + user_name: str, ) -> None: image_path = None has_reply = False @@ -245,7 +259,9 @@ class TelegramPlatformEvent(AstrMessageEvent): try: md_text = telegramify_markdown.markdownify(chunk) await client.send_message( - text=md_text, parse_mode="MarkdownV2", **payload, + text=md_text, + parse_mode="MarkdownV2", + **payload, ) except Exception as e: logger.warning( @@ -277,7 +293,9 @@ class TelegramPlatformEvent(AstrMessageEvent): elif isinstance(i, Video): path = await i.convert_to_file_path() await client.send_video( - video=path, caption=getattr(i, "text", None) or None, **payload, + video=path, + caption=getattr(i, "text", None) or None, + **payload, ) async def send(self, message: MessageChain) -> None: @@ -347,7 +365,10 @@ class TelegramPlatformEvent(AstrMessageEvent): 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), draft_id=draft_id, text=text, **kwargs, + chat_id=int(chat_id), + draft_id=draft_id, + text=text, + **kwargs, ) except Exception as e: logger.warning(f"[Telegram] sendMessageDraft 失败: {e!s}") @@ -423,7 +444,9 @@ class TelegramPlatformEvent(AstrMessageEvent): try: markdown_text = telegramify_markdown.markdownify(delta) await self.client.send_message( - text=markdown_text, parse_mode="MarkdownV2", **payload, + text=markdown_text, + parse_mode="MarkdownV2", + **payload, ) except Exception as e: logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") @@ -444,12 +467,18 @@ 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, ) asyncio.create_task( Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), @@ -498,7 +527,10 @@ class TelegramPlatformEvent(AstrMessageEvent): except Exception: try: await self._send_message_draft( - user_name, draft_id, draft_text, message_thread_id, + user_name, + draft_id, + draft_text, + message_thread_id, ) last_sent_text = draft_text except Exception as e2: @@ -520,7 +552,10 @@ class TelegramPlatformEvent(AstrMessageEvent): if chain.type == "break": if delta: await self._send_message_draft( - user_name, draft_id, "⏳", message_thread_id, + user_name, + draft_id, + "⏳", + message_thread_id, ) await self._send_final_segment(delta, payload) delta = "" @@ -528,7 +563,11 @@ class TelegramPlatformEvent(AstrMessageEvent): draft_id = self._allocate_draft_id() 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 @@ -577,7 +616,11 @@ class TelegramPlatformEvent(AstrMessageEvent): delta = "" continue await self._process_chain_items( - chain, payload, user_name, message_thread_id, _append_text, + chain, + payload, + user_name, + message_thread_id, + _append_text, ) if message_id and len(delta) <= self.MAX_MESSAGE_LENGTH: current_time = asyncio.get_running_loop().time() @@ -622,7 +665,9 @@ class TelegramPlatformEvent(AstrMessageEvent): except Exception as e: logger.warning(f"Markdown转换失败,使用普通文本: {e!s}") await self.client.edit_message_text( - text=delta, chat_id=payload["chat_id"], message_id=message_id, + text=delta, + chat_id=payload["chat_id"], + message_id=message_id, ) except Exception as e: logger.warning(f"编辑消息失败(streaming): {e!s}") diff --git a/astrbot/core/platform/sources/tui/tui_adapter.py b/astrbot/core/platform/sources/tui/tui_adapter.py index fd42a0c11..64ee3ad11 100644 --- a/astrbot/core/platform/sources/tui/tui_adapter.py +++ b/astrbot/core/platform/sources/tui/tui_adapter.py @@ -38,7 +38,10 @@ def _extract_conversation_id(session_id: str) -> str: class QueueListener: def __init__( - self, tui_queue_mgr: TUIQueueMgr, callback: Callable, stop_event: asyncio.Event, + self, + tui_queue_mgr: TUIQueueMgr, + callback: Callable, + stop_event: asyncio.Event, ) -> None: self.tui_queue_mgr = tui_queue_mgr self.callback = callback @@ -56,7 +59,10 @@ class QueueListener: @register_platform_adapter("tui", "tui") class TUIAdapter(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.settings = platform_settings @@ -65,13 +71,18 @@ class TUIAdapter(Platform): os.makedirs(self.imgs_dir, exist_ok=True) os.makedirs(self.attachments_dir, exist_ok=True) self.metadata = PlatformMetadata( - name="tui", description="tui", id="tui", support_proactive_message=True, + name="tui", + description="tui", + id="tui", + support_proactive_message=True, ) self._shutdown_event = asyncio.Event() self._tui_queue_mgr = tui_queue_mgr async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: conversation_id = _extract_conversation_id(session.session_id) active_request_ids = self._tui_queue_mgr.list_back_request_ids(conversation_id) @@ -84,7 +95,8 @@ class TUIAdapter(Platform): await self._save_proactive_message(conversation_id, message_chain) except Exception as e: logger.error( - f"[TUIAdapter] Failed to save proactive message: {e}", exc_info=True, + f"[TUIAdapter] Failed to save proactive message: {e}", + exc_info=True, ) await super().send_by_session(session, message_chain) return @@ -101,12 +113,15 @@ class TUIAdapter(Platform): await self._save_proactive_message(conversation_id, message_chain) except Exception as e: logger.error( - f"[TUIAdapter] Failed to save proactive message: {e}", exc_info=True, + f"[TUIAdapter] Failed to save proactive message: {e}", + exc_info=True, ) await super().send_by_session(session, message_chain) async def _save_proactive_message( - self, conversation_id: str, message_chain: MessageChain, + self, + conversation_id: str, + message_chain: MessageChain, ) -> None: message_parts = await message_chain_to_storage_message_parts( message_chain, @@ -124,12 +139,16 @@ class TUIAdapter(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) async def _parse_message_parts( - self, message_parts: list, depth: int = 0, max_depth: int = 1, + self, + message_parts: list, + depth: int = 0, + max_depth: int = 1, ) -> tuple[list, list[str]]: """Parse message parts list, return message components and plain text lists.""" @@ -194,7 +213,8 @@ class TUIAdapter(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")) self.commit_event(message_event) diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index 3e704d03c..0267f1b3f 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -60,7 +60,10 @@ class QueueListener: @register_platform_adapter("webchat", "webchat") class WebChatAdapter(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.settings = platform_settings @@ -78,7 +81,9 @@ class WebChatAdapter(Platform): self._webchat_queue_mgr = webchat_queue_mgr async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: conversation_id = _extract_conversation_id(session.session_id) active_request_ids = self._webchat_queue_mgr.list_back_request_ids( @@ -117,7 +122,9 @@ class WebChatAdapter(Platform): await super().send_by_session(session, message_chain) async def _save_proactive_message( - self, conversation_id: str, message_chain: MessageChain, + self, + conversation_id: str, + message_chain: MessageChain, ) -> None: message_parts = await message_chain_to_storage_message_parts( message_chain, @@ -135,12 +142,16 @@ 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) async def _parse_message_parts( - self, message_parts: list, depth: int = 0, max_depth: int = 1, + self, + message_parts: list, + depth: int = 0, + max_depth: int = 1, ) -> tuple[list, list[str]]: """解析消息段列表,返回消息组件列表和纯文本列表 @@ -215,7 +226,8 @@ 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")) self.commit_event(message_event) diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index 934770281..8479a9624 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -45,10 +45,14 @@ class WecomServer: self.port = int(config.get("port")) self.callback_server_host = config.get("callback_server_host", "0.0.0.0") self.server.add_url_rule( - "/callback/command", view_func=self.verify, methods=["GET"], + "/callback/command", + view_func=self.verify, + methods=["GET"], ) self.server.add_url_rule( - "/callback/command", view_func=self.callback_command, methods=["POST"], + "/callback/command", + view_func=self.callback_command, + methods=["POST"], ) self.event_queue = event_queue self.crypto = WeChatCrypto( @@ -135,12 +139,16 @@ class WecomServer: @register_platform_adapter("wecom", "wecom 适配器", support_streaming_message=False) class WecomPlatformAdapter(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.settingss = platform_settings self.api_base_url = platform_config.get( - "api_base_url", "https://qyapi.weixin.qq.com/cgi-bin/", + "api_base_url", + "https://qyapi.weixin.qq.com/cgi-bin/", ) self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False) if not self.api_base_url: @@ -153,7 +161,8 @@ class WecomPlatformAdapter(Platform): self.server = WecomServer(self._event_queue, self.config) self.agent_id: str | None = None self.client = WeChatClient( - self.config["corpid"].strip(), self.config["secret"].strip(), + self.config["corpid"].strip(), + self.config["secret"].strip(), ) self.kf_name = self.config.get("kf_name", None) if self.kf_name: @@ -180,7 +189,8 @@ class WecomPlatformAdapter(Platform): return None msg_new = await asyncio.get_running_loop().run_in_executor( - None, get_latest_msg_item, + None, + get_latest_msg_item, ) if msg_new: await self.convert_wechat_kf_message(msg_new) @@ -191,7 +201,9 @@ class WecomPlatformAdapter(Platform): @override async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: if hasattr(self.client, "kf_message"): logger.warning("企业微信客服模式不支持 send_by_session 主动发送。") @@ -239,7 +251,8 @@ class WecomPlatformAdapter(Platform): try: acc_list = ( await loop.run_in_executor( - None, self.wechat_kf_api.get_account_list, + None, + self.wechat_kf_api.get_account_list, ) ).get("account_list", []) logger.debug(f"获取到微信客服列表: {acc_list!s}") @@ -301,7 +314,9 @@ class WecomPlatformAdapter(Platform): abm.raw_message = msg elif isinstance(msg, VoiceMessage): resp = await asyncio.get_running_loop().run_in_executor( - None, self.client.media.download, msg.media_id, + None, + self.client.media.download, + msg.media_id, ) temp_dir = get_astrbot_temp_path() path = os.path.join(temp_dir, f"wecom_{msg.media_id}.amr") @@ -350,7 +365,9 @@ class WecomPlatformAdapter(Platform): elif msgtype == "image": media_id = msg.get("image", {}).get("media_id", "") resp = await asyncio.get_running_loop().run_in_executor( - None, self.client.media.download, media_id, + None, + self.client.media.download, + media_id, ) temp_dir = get_astrbot_temp_path() path = os.path.join(temp_dir, f"weixinkefu_{media_id}.jpg") @@ -360,7 +377,9 @@ class WecomPlatformAdapter(Platform): elif msgtype == "voice": media_id = msg.get("voice", {}).get("media_id", "") resp = await asyncio.get_running_loop().run_in_executor( - None, self.client.media.download, media_id, + None, + self.client.media.download, + media_id, ) temp_dir = get_astrbot_temp_path() path = os.path.join(temp_dir, f"weixinkefu_{media_id}.amr") diff --git a/astrbot/core/platform/sources/wecom/wecom_event.py b/astrbot/core/platform/sources/wecom/wecom_event.py index b926d0404..ee2bef7cb 100644 --- a/astrbot/core/platform/sources/wecom/wecom_event.py +++ b/astrbot/core/platform/sources/wecom/wecom_event.py @@ -106,7 +106,9 @@ class WecomPlatformEvent(AstrMessageEvent): 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 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 541f593ca..f938d5b26 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py @@ -47,7 +47,9 @@ class WecomAIQueueListener: """企业微信智能机器人队列监听器,参考webchat的QueueListener设计""" def __init__( - self, queue_mgr: WecomAIQueueMgr, callback: Callable[[dict], Awaitable[None]], + self, + queue_mgr: WecomAIQueueMgr, + callback: Callable[[dict], Awaitable[None]], ) -> None: self.queue_mgr = queue_mgr self.callback = callback @@ -61,29 +63,36 @@ class WecomAIQueueListener: @register_platform_adapter( - "wecom_ai_bot", "企业微信智能机器人适配器,支持 HTTP 回调接收消息", + "wecom_ai_bot", + "企业微信智能机器人适配器,支持 HTTP 回调接收消息", ) class WecomAIBotAdapter(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.settings = platform_settings 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") self.bot_name = self.config.get("wecom_ai_bot_name", "") self.initial_respond_text = self.config.get("wecomaibot_init_respond_text", "") self.friend_message_welcome_text = self.config.get( - "wecomaibot_friend_message_welcome_text", "", + "wecomaibot_friend_message_welcome_text", + "", ) self.unified_webhook_mode = self.config.get("unified_webhook_mode", False) self.msg_push_webhook_url = self.config.get("msg_push_webhook_url", "").strip() @@ -91,13 +100,16 @@ 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", "wss://openws.work.weixin.qq.com", + "wecomaibot_ws_url", + "wss://openws.work.weixin.qq.com", ) self.long_connection_heartbeat_interval = int( self.config.get("wecomaibot_heartbeat_interval", 30), @@ -134,7 +146,8 @@ class WecomAIBotAdapter(Platform): self.shutdown_event = asyncio.Event() self.queue_mgr = WecomAIQueueMgr() self.queue_listener = WecomAIQueueListener( - self.queue_mgr, self._handle_queued_message, + self.queue_mgr, + self._handle_queued_message, ) self._stream_plain_cache: dict[str, str] = {} self.webhook_client: WecomAIBotWebhookClient | None = None @@ -153,7 +166,9 @@ class WecomAIBotAdapter(Platform): logger.error(f"处理队列消息时发生异常: {e}") async def _process_message( - self, message_data: dict[str, Any], callback_params: dict[str, str], + self, + message_data: dict[str, Any], + callback_params: dict[str, str], ) -> str | None: """处理接收到的消息 @@ -177,17 +192,24 @@ class WecomAIBotAdapter(Platform): try: stream_id = f"{session_id}_{generate_random_string(10)}" await self._enqueue_message( - message_data, callback_params, stream_id, session_id, + message_data, + callback_params, + stream_id, + session_id, ) self.queue_mgr.set_pending_response(stream_id, callback_params) if self.only_use_webhook_url_to_send and self.webhook_client: return None if self.initial_respond_text: resp = WecomAIBotStreamMessageBuilder.make_text_stream( - stream_id, self.initial_respond_text, False, + stream_id, + self.initial_respond_text, + False, ) return await self.api_client.encrypt_message( - resp, callback_params["nonce"], callback_params["timestamp"], + resp, + callback_params["nonce"], + callback_params["timestamp"], ) except Exception as e: logger.error("处理消息时发生异常: %s", e) @@ -203,10 +225,14 @@ class WecomAIBotAdapter(Platform): else: logger.warning(f"Cannot find back queue for stream_id: {stream_id}") end_message = WecomAIBotStreamMessageBuilder.make_text_stream( - stream_id, "", True, + stream_id, + "", + True, ) resp = await self.api_client.encrypt_message( - end_message, callback_params["nonce"], callback_params["timestamp"], + end_message, + callback_params["nonce"], + callback_params["timestamp"], ) return resp queue = self.queue_mgr.get_or_create_back_queue(stream_id) @@ -244,10 +270,14 @@ class WecomAIBotAdapter(Platform): self._stream_plain_cache[stream_id] = cached_plain_content if finish and (not latest_plain_content) and (not image_base64): end_message = WecomAIBotStreamMessageBuilder.make_text_stream( - stream_id, "", True, + stream_id, + "", + True, ) return await self.api_client.encrypt_message( - end_message, callback_params["nonce"], callback_params["timestamp"], + end_message, + callback_params["nonce"], + callback_params["timestamp"], ) if latest_plain_content or image_base64: msg_items = [] @@ -263,7 +293,10 @@ class WecomAIBotAdapter(Platform): ) image_base64 = [] plain_message = WecomAIBotStreamMessageBuilder.make_mixed_stream( - stream_id, latest_plain_content, msg_items, finish, + stream_id, + latest_plain_content, + msg_items, + finish, ) encrypted_message = await self.api_client.encrypt_message( plain_message, @@ -286,7 +319,9 @@ class WecomAIBotAdapter(Platform): self.friend_message_welcome_text, ) return await self.api_client.encrypt_message( - resp, callback_params["nonce"], callback_params["timestamp"], + resp, + callback_params["nonce"], + callback_params["timestamp"], ) except Exception as e: logger.error("处理欢迎消息时发生异常: %s", e) @@ -306,7 +341,10 @@ 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, @@ -353,13 +391,17 @@ class WecomAIBotAdapter(Platform): ) async def _send_long_connection_respond_msg( - self, req_id: str, body: dict[str, Any], + self, + req_id: str, + body: dict[str, Any], ) -> bool: client = self.long_connection_client if not client: return False return await client.send_command( - cmd="aibot_respond_msg", req_id=req_id, body=body, + cmd="aibot_respond_msg", + req_id=req_id, + body=body, ) def _extract_session_id(self, message_data: dict[str, Any]) -> str: @@ -465,7 +507,9 @@ class WecomAIBotAdapter(Platform): return abm async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: """通过消息推送 webhook 发送消息。""" if not self.webhook_client: @@ -489,26 +533,32 @@ 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(), self.queue_listener.run(), + self.long_connection_client.start(), + self.queue_listener.run(), ) else: 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() else: if not self.server: raise RuntimeError("Webhook 服务器未初始化") logger.info( - "启动企业微信智能机器人适配器,监听 %s:%d", self.host, self.port, + "启动企业微信智能机器人适配器,监听 %s:%d", + self.host, + self.port, ) await asyncio.gather( - self.server.start_server(), self.queue_listener.run(), + self.server.start_server(), + self.queue_listener.run(), ) return run_both() 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 682f33a44..40c3640f2 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 4bf2408c8..0bcb948dc 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py @@ -154,7 +154,8 @@ class WecomAIBotMessageEvent(AstrMessageEvent): return if self.webhook_client and message: await self.webhook_client.send_message_chain( - message, unsupported_only=True, + message, + unsupported_only=True, ) content = self._extract_plain_text_from_chain(message) await self.long_connection_sender( @@ -222,7 +223,8 @@ 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, ) chain.squash_plain() chunk_text = self._extract_plain_text_from_chain(chain) @@ -282,7 +284,8 @@ 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: if increment_plain: 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 fc2ca4155..e8299fa8b 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,9 @@ 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() 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 44a3cc63d..67bc16769 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 @@ -125,7 +125,9 @@ 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_webhook.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py index b72f7d1d6..1bfaba722 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py @@ -102,7 +102,9 @@ class WecomAIBotWebhookClient: ) async def upload_media( - self, file_path: Path, media_type: Literal["file", "voice"], + self, + file_path: Path, + media_type: Literal["file", "voice"], ) -> str: file_path_anyio = anyio.Path(file_path) if not await file_path_anyio.exists() or not await file_path_anyio.is_file(): @@ -216,7 +218,9 @@ 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 a90fd2b99..d3badb884 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py @@ -68,7 +68,10 @@ class WeixinOCAdapter(Platform): FILE_UPLOAD_TYPE = 3 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.settings = platform_settings @@ -77,7 +80,8 @@ class WeixinOCAdapter(Platform): ).rstrip("/") self.bot_type = str(platform_config.get("weixin_oc_bot_type", "3")) self.qr_poll_interval = max( - 1, int(platform_config.get("weixin_oc_qr_poll_interval", 1)), + 1, + int(platform_config.get("weixin_oc_qr_poll_interval", 1)), ) self.long_poll_timeout_ms = int( platform_config.get("weixin_oc_long_poll_timeout_ms", 35000), @@ -87,7 +91,8 @@ class WeixinOCAdapter(Platform): ) self.cdn_base_url = str( platform_config.get( - "weixin_oc_cdn_base_url", "https://novac2c.cdn.weixin.qq.com/c2c", + "weixin_oc_cdn_base_url", + "https://novac2c.cdn.weixin.qq.com/c2c", ), ).rstrip("/") self.metadata = PlatformMetadata( @@ -104,10 +109,12 @@ class WeixinOCAdapter(Platform): self._typing_states: dict[str, TypingSessionState] = {} self._last_inbound_error = "" self._typing_keepalive_interval_s = max( - 1, int(platform_config.get("weixin_oc_typing_keepalive_interval", 5)), + 1, + int(platform_config.get("weixin_oc_typing_keepalive_interval", 5)), ) self._typing_ticket_ttl_s = max( - 5, int(platform_config.get("weixin_oc_typing_ticket_ttl", 60)), + 5, + int(platform_config.get("weixin_oc_typing_ticket_ttl", 60)), ) self.token = str(platform_config.get("weixin_oc_token", "")).strip() or None self.account_id = ( @@ -123,7 +130,8 @@ class WeixinOCAdapter(Platform): ) if self.token: logger.info( - "weixin_oc adapter %s loaded with token from config.", self.meta().id, + "weixin_oc adapter %s loaded with token from config.", + self.meta().id, ) def _sync_client_state(self) -> None: @@ -163,7 +171,9 @@ class WeixinOCAdapter(Platform): logger.warning(log_message, *log_args, exc_info=True) async def _ensure_typing_ticket( - self, user_id: str, state: TypingSessionState, + self, + user_id: str, + state: TypingSessionState, ) -> str | None: now = time.monotonic() context_token = self._context_tokens.get(user_id) @@ -193,7 +203,11 @@ class WeixinOCAdapter(Platform): return ticket async def _send_typing_state( - self, user_id: str, ticket: str, *, cancel: bool, + self, + user_id: str, + ticket: str, + *, + cancel: bool, ) -> None: payload = await self.client.send_typing_state(user_id, ticket, cancel=cancel) if int(payload.get("ret") or 0) != 0: @@ -451,7 +465,8 @@ 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 @@ -468,10 +483,16 @@ class WeixinOCAdapter(Platform): return normalized or fallback_name def _save_inbound_media( - self, content: bytes, *, prefix: str, file_name: str, fallback_suffix: str, + self, + content: bytes, + *, + prefix: str, + file_name: str, + fallback_suffix: str, ) -> Path: normalized_name = self._normalize_inbound_filename( - file_name, f"{prefix}{fallback_suffix}", + file_name, + f"{prefix}{fallback_suffix}", ) stem = Path(normalized_name).stem or prefix suffix = Path(normalized_name).suffix or fallback_suffix @@ -531,7 +552,11 @@ class WeixinOCAdapter(Platform): upload_param = str(payload.get("upload_param", "")).strip() upload_full_url = str(payload.get("upload_full_url", "")).strip() encrypted_query_param = await self.client.upload_to_cdn( - upload_full_url, upload_param, file_key, aes_key_hex, media_path, + upload_full_url, + upload_param, + file_key, + aes_key_hex, + media_path, ) logger.debug( "weixin_oc(%s): prepared media item type=%s file=%s user=%s mid_size=%s upload_param_len=%s query_len=%s", @@ -570,7 +595,8 @@ class WeixinOCAdapter(Platform): } async def _resolve_inbound_media_component( - self, item: dict[str, Any], + self, + item: dict[str, Any], ) -> Image | Video | File | Record | None: item_type = int(item.get("type") or 0) if item_type == self.IMAGE_ITEM_TYPE: @@ -588,7 +614,8 @@ class WeixinOCAdapter(Platform): aes_key_value = str(media.get("aes_key", "")).strip() if aes_key_value: content = await self.client.download_and_decrypt_media( - encrypted_query_param, aes_key_value, + encrypted_query_param, + aes_key_value, ) else: content = await self.client.download_cdn_bytes(encrypted_query_param) @@ -607,7 +634,8 @@ class WeixinOCAdapter(Platform): if not encrypted_query_param or not aes_key_value: return None content = await self.client.download_and_decrypt_media( - encrypted_query_param, aes_key_value, + encrypted_query_param, + aes_key_value, ) video_path = self._save_inbound_media( content, @@ -624,10 +652,12 @@ class WeixinOCAdapter(Platform): if not encrypted_query_param or not aes_key_value: return None file_name = self._normalize_inbound_filename( - str(file_item.get("file_name", "")).strip(), "file.bin", + str(file_item.get("file_name", "")).strip(), + "file.bin", ) content = await self.client.download_and_decrypt_media( - encrypted_query_param, aes_key_value, + encrypted_query_param, + aes_key_value, ) file_path = self._save_inbound_media( content, @@ -644,7 +674,8 @@ class WeixinOCAdapter(Platform): if not encrypted_query_param or not aes_key_value: return None content = await self.client.download_and_decrypt_media( - encrypted_query_param, aes_key_value, + encrypted_query_param, + aes_key_value, ) voice_path = self._save_inbound_media( content, @@ -656,7 +687,8 @@ 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): @@ -678,14 +710,17 @@ class WeixinOCAdapter(Platform): return media_path async def _send_items_to_session( - self, user_id: str, item_list: list[dict[str, Any]], + self, + user_id: str, + item_list: list[dict[str, Any]], ) -> bool: if not self.token: logger.warning("weixin_oc(%s): missing token, skip send", self.meta().id) return False if not item_list: logger.warning( - "weixin_oc(%s): empty message payload is ignored", self.meta().id, + "weixin_oc(%s): empty message payload is ignored", + self.meta().id, ) return False context_token = self._context_tokens.get(user_id) @@ -717,11 +752,15 @@ class WeixinOCAdapter(Platform): return True async def _send_media_segment( - self, user_id: str, segment: Image | Video | File, text: str | None = None, + self, + user_id: str, + segment: Image | Video | File, + text: str | None = None, ) -> 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) @@ -746,14 +785,19 @@ class WeixinOCAdapter(Platform): ) try: media_item = await self._prepare_media_item( - user_id, media_path, upload_media_type, item_type, file_name, + user_id, + media_path, + upload_media_type, + item_type, + file_name, ) except Exception as e: logger.error("weixin_oc(%s): prepare media failed: %s", self.meta().id, e) return False if text: await self._send_items_to_session( - user_id, [self._build_plain_text_item(text)], + user_id, + [self._build_plain_text_item(text)], ) return await self._send_items_to_session(user_id, [media_item]) @@ -762,7 +806,11 @@ class WeixinOCAdapter(Platform): params = {"bot_type": self.bot_type} logger.info("weixin_oc(%s): request QR code from %s", self.meta().id, endpoint) data = await self.client.request_json( - "GET", endpoint, params=params, token_required=False, timeout_ms=15000, + "GET", + endpoint, + params=params, + token_required=False, + timeout_ms=15000, ) qrcode = str(data.get("qrcode", "")).strip() qrcode_url = str(data.get("qrcode_img_content", "")).strip() @@ -849,7 +897,8 @@ 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 "" @@ -883,7 +932,8 @@ 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 [] @@ -899,7 +949,9 @@ class WeixinOCAdapter(Platform): media_component = await self._resolve_inbound_media_component(item) except Exception as e: logger.warning( - "weixin_oc(%s): resolve inbound media failed: %s", self.meta().id, e, + "weixin_oc(%s): resolve inbound media failed: %s", + self.meta().id, + e, ) media_component = None if media_component is not None: @@ -994,21 +1046,28 @@ 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 [])) if not text: logger.warning( - "weixin_oc(%s): message without plain text is ignored", self.meta().id, + "weixin_oc(%s): message without plain text is ignored", + self.meta().id, ) return False return await self._send_items_to_session( - user_id, [self._build_plain_text_item(text)], + user_id, + [self._build_plain_text_item(text)], ) async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: target_user = session.session_id pending_text = "" @@ -1020,7 +1079,9 @@ class WeixinOCAdapter(Platform): if isinstance(segment, (Image, Video, File)): has_supported_segment = True await self._send_media_segment( - target_user, segment, text=pending_text.strip() or None, + target_user, + segment, + text=pending_text.strip() or None, ) pending_text = "" continue @@ -1065,7 +1126,8 @@ class WeixinOCAdapter(Platform): await self._poll_qr_status(current_login) except asyncio.TimeoutError: logger.debug( - "weixin_oc(%s): qr status long-poll timeout", self.meta().id, + "weixin_oc(%s): qr status long-poll timeout", + self.meta().id, ) except Exception as e: logger.error( @@ -1091,7 +1153,8 @@ class WeixinOCAdapter(Platform): await self._poll_inbound_updates() except asyncio.TimeoutError: logger.debug( - "weixin_oc(%s): inbound long-poll timeout", self.meta().id, + "weixin_oc(%s): inbound long-poll timeout", + self.meta().id, ) except Exception as e: logger.error( 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 5d8312628..a81f57a94 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py @@ -188,7 +188,8 @@ class WeixinOCClient: assert self._http_session is not None timeout = aiohttp.ClientTimeout(total=self.api_timeout_ms / 1000) async with self._http_session.get( - self._build_cdn_download_url(encrypted_query_param), timeout=timeout, + self._build_cdn_download_url(encrypted_query_param), + timeout=timeout, ) as resp: if resp.status >= 400: detail = await resp.text() @@ -198,7 +199,9 @@ class WeixinOCClient: return await resp.read() async def download_and_decrypt_media( - self, encrypted_query_param: str, aes_key_value: str, + self, + encrypted_query_param: str, + aes_key_value: str, ) -> bytes: encrypted = await self.download_cdn_bytes(encrypted_query_param) key = self.parse_media_aes_key(aes_key_value) @@ -238,7 +241,9 @@ class WeixinOCClient: return json.loads(text) async def get_typing_config( - self, user_id: str, context_token: str, + self, + user_id: str, + context_token: str, ) -> dict[str, Any]: return await self.request_json( "POST", @@ -253,7 +258,11 @@ class WeixinOCClient: ) async def send_typing_state( - self, user_id: str, typing_ticket: str, *, cancel: bool, + self, + user_id: str, + typing_ticket: str, + *, + cancel: bool, ) -> dict[str, Any]: return await self.request_json( "POST", 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 bdd2b1860..c3272d468 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 @@ -46,10 +46,14 @@ class WeixinOfficialAccountServer: self.encoding_aes_key = config.get("encoding_aes_key") self.appid = config.get("appid") self.server.add_url_rule( - "/callback/command", view_func=self.verify, methods=["GET"], + "/callback/command", + view_func=self.verify, + methods=["GET"], ) self.server.add_url_rule( - "/callback/command", view_func=self.callback_command, methods=["POST"], + "/callback/command", + view_func=self.callback_command, + methods=["POST"], ) self.crypto = WeChatCrypto(self.token, self.encoding_aes_key, self.appid) self.event_queue = event_queue @@ -202,7 +206,8 @@ class WeixinOfficialAccountServer: 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("处理消息失败,请稍后再试。") @@ -272,16 +277,22 @@ class WeixinOfficialAccountServer: @register_platform_adapter( - "weixin_official_account", "微信公众平台 适配器", support_streaming_message=False, + "weixin_official_account", + "微信公众平台 适配器", + support_streaming_message=False, ) class WeixinOfficialAccountPlatformAdapter(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.settingss = platform_settings self.api_base_url = platform_config.get( - "api_base_url", "https://api.weixin.qq.com/cgi-bin/", + "api_base_url", + "https://api.weixin.qq.com/cgi-bin/", ) self.active_send_mode = self.config.get("active_send_mode", False) self.unified_webhook_mode = platform_config.get("unified_webhook_mode", False) @@ -294,10 +305,13 @@ class WeixinOfficialAccountPlatformAdapter(Platform): self.api_base_url += "/" self.user_buffer: dict[str, dict[str, Any]] = {} self.server = WeixinOfficialAccountServer( - self._event_queue, self.config, self.user_buffer, + self._event_queue, + self.config, + self.user_buffer, ) self.client = WeChatClient( - self.config["appid"].strip(), self.config["secret"].strip(), + self.config["appid"].strip(), + self.config["secret"].strip(), ) self.client.__setattr__("API_BASE_URL", self.api_base_url) self.wexin_event_workers: dict[str, asyncio.Future] = {} @@ -331,7 +345,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): @override async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain, + self, + session: MessageSesion, + message_chain: MessageChain, ) -> None: await super().send_by_session(session, message_chain) @@ -361,7 +377,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): return await self.server.handle_callback(request) async def convert_message( - self, msg, future: asyncio.Future | None = None, + self, + msg, + future: asyncio.Future | None = None, ) -> AstrBotMessage | None: abm = AstrBotMessage() if isinstance(msg, TextMessage): @@ -386,7 +404,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform): elif msg.type == "voice": assert isinstance(msg, VoiceMessage) resp: Response = await asyncio.get_running_loop().run_in_executor( - None, self.client.media.download, msg.media_id, + None, + self.client.media.download, + msg.media_id, ) temp_dir = get_astrbot_temp_path() path = os.path.join(temp_dir, f"weixin_offacc_{msg.media_id}.amr") 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 8d566a43c..898cc2bd9 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 @@ -29,7 +29,9 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): @staticmethod async def send_with_client( - client: WeChatClient, message: MessageChain, user_name: str, + client: WeChatClient, + message: MessageChain, + user_name: str, ) -> None: pass @@ -100,7 +102,8 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): logger.debug(f"微信公众平台上传图片返回: {response}") if active_send_mode: self.client.message.send_image( - message_obj.sender.user_id, response["media_id"], + message_obj.sender.user_id, + response["media_id"], ) else: reply = ImageReply( @@ -121,13 +124,16 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): except Exception as e: logger.error(f"微信公众平台上传语音失败: {e}") await self.send( - MessageChain().message(f"微信公众平台上传语音失败: {e}"), + MessageChain().message( + f"微信公众平台上传语音失败: {e}" + ), ) return logger.info(f"微信公众平台上传语音返回: {response}") if active_send_mode: self.client.message.send_voice( - message_obj.sender.user_id, response["media_id"], + message_obj.sender.user_id, + response["media_id"], ) else: reply = VoiceReply( diff --git a/astrbot/core/platform_message_history_mgr.py b/astrbot/core/platform_message_history_mgr.py index 37a6feb3d..b3ac69ac3 100644 --- a/astrbot/core/platform_message_history_mgr.py +++ b/astrbot/core/platform_message_history_mgr.py @@ -41,7 +41,10 @@ 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/manager.py b/astrbot/core/provider/manager.py index a9c164074..90d9ab0f4 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -223,7 +223,9 @@ 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: """获取正在使用的提供商实例。 @@ -786,7 +788,9 @@ 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: diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index bec8ca606..230660155 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -19,7 +19,11 @@ from astrbot.core.provider.register import provider_cls_map from astrbot.core.utils.astrbot_path import get_astrbot_path Providers: TypeAlias = Union[ - "Provider", "STTProvider", "TTSProvider", "EmbeddingProvider", "RerankProvider", + "Provider", + "STTProvider", + "TTSProvider", + "EmbeddingProvider", + "RerankProvider", ] @@ -179,7 +183,8 @@ class Provider(AbstractProvider): context.pop(idx) def _ensure_message_to_dicts( - self, messages: list[dict] | list[Message] | None, + self, + messages: list[dict] | list[Message] | None, ) -> list[dict]: """Convert a list of Message objects to a list of dictionaries.""" if not messages: @@ -210,7 +215,9 @@ class STTProvider(AbstractProvider): async def test(self) -> None: sample_audio_path = os.path.join( - get_astrbot_path(), "samples", "stt_health_check.wav", + get_astrbot_path(), + "samples", + "stt_health_check.wav", ) await self.get_text(sample_audio_path) @@ -383,7 +390,10 @@ class RerankProvider(AbstractProvider): @abc.abstractmethod async def rerank( - self, query: str, documents: list[str], top_n: int | None = None, + self, + query: str, + documents: list[str], + top_n: int | None = None, ) -> list[RerankResult]: """获取查询和文档的重排序分数""" ... diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 42f011945..1ee910166 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -309,7 +309,9 @@ 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", "") @@ -404,7 +406,8 @@ 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 ff8de45e9..332be4d65 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -46,7 +46,8 @@ class OTTSProvider: 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 @@ -151,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 @@ -212,7 +213,9 @@ 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 5ebb528db..bc18b9c60 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -12,19 +12,18 @@ class BailianRerankError(Exception): """百炼重排序服务异常基类""" - class BailianAPIError(BailianRerankError): """百炼API返回错误""" - class BailianNetworkError(BailianRerankError): """百炼网络请求错误""" - @register_provider_adapter( - "bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK, + "bailian_rerank", + "阿里云百炼文本排序适配器", + provider_type=ProviderType.RERANK, ) class BailianRerankProvider(RerankProvider): """阿里云百炼文本重排序适配器.""" @@ -38,7 +37,8 @@ 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 不能为空。") @@ -60,7 +60,8 @@ class BailianRerankProvider(RerankProvider): } self.client: aiohttp.ClientSession | None = aiohttp.ClientSession( - headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout), + headers=headers, + timeout=aiohttp.ClientTimeout(total=self.timeout), ) # 设置模型名称 @@ -69,7 +70,10 @@ 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: """构建请求载荷 @@ -158,7 +162,8 @@ 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: diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py index f0d1f412c..912beff5c 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} @@ -156,7 +156,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", @@ -164,7 +164,8 @@ 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/"): async with aiofiles.open(path, "wb") as f: async for chunk in response.aiter_bytes(): diff --git a/astrbot/core/provider/sources/gemini_embedding_source.py b/astrbot/core/provider/sources/gemini_embedding_source.py index d5cd4dee9..6ccd5fb53 100644 --- a/astrbot/core/provider/sources/gemini_embedding_source.py +++ b/astrbot/core/provider/sources/gemini_embedding_source.py @@ -31,7 +31,8 @@ class GeminiEmbeddingProvider(EmbeddingProvider): logger.info(f"[Gemini Embedding] 使用代理: {proxy}") self.client = genai.Client(api_key=api_key, http_options=http_options).aio self.model = provider_config.get( - "embedding_model", "gemini-embedding-exp-03-07", + "embedding_model", + "gemini-embedding-exp-03-07", ) async def get_embedding(self, text: str) -> list[float]: diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 84c96be00..ca76f645f 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -39,7 +39,8 @@ logging.getLogger("google_genai.types").addFilter(SuppressNonTextPartsWarning()) @register_provider_adapter( - "googlegenai_chat_completion", "Google Gemini Chat Completion 提供商适配器", + "googlegenai_chat_completion", + "Google Gemini Chat Completion 提供商适配器", ) class ProviderGoogleGenAI(Provider): CATEGORY_MAPPING: ClassVar[dict[str, types.HarmCategory]] = { @@ -71,13 +72,15 @@ class ProviderGoogleGenAI(Provider): """初始化Gemini客户端""" proxy = self.provider_config.get("proxy", "") http_options = types.HttpOptions( - base_url=self.api_base, timeout=self.timeout * 1000, + base_url=self.api_base, + timeout=self.timeout * 1000, ) if proxy: http_options.async_client_args = {"proxy": proxy} logger.info(f"[Gemini] 使用代理: {proxy}") self.client = genai.Client( - api_key=self.chosen_api_key, http_options=http_options, + api_key=self.chosen_api_key, + http_options=http_options, ).aio def _init_safety_settings(self) -> None: @@ -85,7 +88,8 @@ class ProviderGoogleGenAI(Provider): user_safety_config = self.provider_config.get("gm_safety_settings", {}) self.safety_settings = [ types.SafetySetting( - category=harm_category, threshold=self.THRESHOLD_MAPPING[threshold_str], + category=harm_category, + threshold=self.THRESHOLD_MAPPING[threshold_str], ) for config_key, harm_category in self.CATEGORY_MAPPING.items() if (threshold_str := user_safety_config.get(config_key)) @@ -199,7 +203,8 @@ class ProviderGoogleGenAI(Provider): "gemini-live-2.5-flash-preview-native-audio-09-2025", ]: thinking_budget = self.provider_config.get("gm_thinking_config", {}).get( - "budget", 0, + "budget", + 0, ) if thinking_budget is not None: thinking_config = types.ThinkingConfig( @@ -211,7 +216,8 @@ 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() @@ -245,7 +251,7 @@ class ProviderGoogleGenAI(Provider): response_modalities=modalities, tools=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, @@ -256,7 +262,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) @@ -385,7 +391,8 @@ 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( @@ -530,7 +537,9 @@ class ProviderGoogleGenAI(Provider): streaming=False, ) result = await self.client.models.generate_content( - model=model, contents=conversation, config=config, + model=model, + contents=conversation, + config=config, ) logger.debug(f"genai result: {result}") if not result.candidates: @@ -570,7 +579,8 @@ class ProviderGoogleGenAI(Provider): llm_response = LLMResponse("assistant") llm_response.raw_completion = result llm_response.result_chain = self._process_content_parts( - result.candidates[0], llm_response, + result.candidates[0], + llm_response, ) llm_response.id = result.response_id if result.usage_metadata: @@ -578,7 +588,9 @@ class ProviderGoogleGenAI(Provider): return llm_response async def _query_stream( - self, payloads: dict, tools: ToolSet | None, + self, + payloads: dict, + tools: ToolSet | None, ) -> AsyncGenerator[LLMResponse, None]: """流式请求 Gemini API""" system_instruction = next( @@ -598,7 +610,9 @@ class ProviderGoogleGenAI(Provider): streaming=True, ) result = await self.client.models.generate_content_stream( - model=model, contents=conversation, config=config, + model=model, + contents=conversation, + config=config, ) break except APIError as e: diff --git a/astrbot/core/provider/sources/groq_source.py b/astrbot/core/provider/sources/groq_source.py index c82ab5883..9dfd98c37 100644 --- a/astrbot/core/provider/sources/groq_source.py +++ b/astrbot/core/provider/sources/groq_source.py @@ -4,7 +4,8 @@ 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 21d8043b9..be46fb883 100644 --- a/astrbot/core/provider/sources/longcat_source.py +++ b/astrbot/core/provider/sources/longcat_source.py @@ -3,7 +3,8 @@ 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_tts_api_source.py b/astrbot/core/provider/sources/mimo_tts_api_source.py index b4e93368d..8173e56b9 100644 --- a/astrbot/core/provider/sources/mimo_tts_api_source.py +++ b/astrbot/core/provider/sources/mimo_tts_api_source.py @@ -40,7 +40,8 @@ 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) diff --git a/astrbot/core/provider/sources/nvidia_rerank_source.py b/astrbot/core/provider/sources/nvidia_rerank_source.py index 0c76e73b9..826997f58 100644 --- a/astrbot/core/provider/sources/nvidia_rerank_source.py +++ b/astrbot/core/provider/sources/nvidia_rerank_source.py @@ -8,21 +8,26 @@ 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,7 +42,8 @@ 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 @@ -74,7 +80,9 @@ 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", []) @@ -133,7 +141,8 @@ 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 f4a7018bb..5a01e20ed 100644 --- a/astrbot/core/provider/sources/oai_aihubmix_source.py +++ b/astrbot/core/provider/sources/oai_aihubmix_source.py @@ -4,7 +4,8 @@ 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__(self, provider_config: dict, provider_settings: dict) -> None: diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 211568c24..24e4efdda 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -372,7 +372,8 @@ 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( @@ -402,7 +403,8 @@ 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) @@ -494,7 +496,8 @@ 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 @@ -805,7 +808,9 @@ 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") @@ -967,7 +972,8 @@ 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 @@ -991,7 +997,8 @@ 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( diff --git a/astrbot/core/provider/sources/openai_tts_api_source.py b/astrbot/core/provider/sources/openai_tts_api_source.py index 8349c812e..2e6c58a9f 100644 --- a/astrbot/core/provider/sources/openai_tts_api_source.py +++ b/astrbot/core/provider/sources/openai_tts_api_source.py @@ -48,12 +48,15 @@ class ProviderOpenAITTSAPI(TTSProvider): async def get_audio(self, text: str) -> str: temp_dir = get_astrbot_temp_path() path = os.path.join(temp_dir, f"openai_tts_api_{uuid.uuid4()}.wav") - async with self.client.audio.speech.with_streaming_response.create( - model=self.model_name, - voice=self.voice, - response_format="wav", - input=text, - ) as response, aiofiles.open(path, "wb") as f: + async with ( + self.client.audio.speech.with_streaming_response.create( + model=self.model_name, + voice=self.voice, + response_format="wav", + input=text, + ) as response, + aiofiles.open(path, "wb") as f, + ): async for chunk in response.iter_bytes(chunk_size=1024): await f.write(chunk) return path diff --git a/astrbot/core/provider/sources/openrouter_source.py b/astrbot/core/provider/sources/openrouter_source.py index 38e8764e2..d736130a1 100644 --- a/astrbot/core/provider/sources/openrouter_source.py +++ b/astrbot/core/provider/sources/openrouter_source.py @@ -4,7 +4,8 @@ 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__(self, provider_config: dict, provider_settings: dict) -> None: diff --git a/astrbot/core/provider/sources/xai_source.py b/astrbot/core/provider/sources/xai_source.py index d975047e0..02d519631 100644 --- a/astrbot/core/provider/sources/xai_source.py +++ b/astrbot/core/provider/sources/xai_source.py @@ -4,7 +4,8 @@ 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 fcca5befc..efe565547 100644 --- a/astrbot/core/provider/sources/xinference_rerank_source.py +++ b/astrbot/core/provider/sources/xinference_rerank_source.py @@ -10,7 +10,9 @@ from astrbot.core.provider.register import register_provider_adapter @register_provider_adapter( - "xinference_rerank", "Xinference Rerank 适配器", provider_type=ProviderType.RERANK, + "xinference_rerank", + "Xinference Rerank 适配器", + provider_type=ProviderType.RERANK, ) class XinferenceRerankProvider(RerankProvider): def __init__(self, provider_config: dict, provider_settings: dict) -> None: @@ -23,7 +25,8 @@ class XinferenceRerankProvider(RerankProvider): self.model_name = provider_config.get("rerank_model", "BAAI/bge-reranker-base") self.api_key = provider_config.get("rerank_api_key") self.launch_model_if_not_running = provider_config.get( - "launch_model_if_not_running", False, + "launch_model_if_not_running", + False, ) self.client: Client | None = None self.model: AsyncRESTfulRerankModelHandle | None = None @@ -50,7 +53,8 @@ class XinferenceRerankProvider(RerankProvider): if self.launch_model_if_not_running: logger.info(f"Launching {self.model_name} model...") self.model_uid = await client.launch_model( - model_name=self.model_name, model_type="rerank", + model_name=self.model_name, + model_type="rerank", ) logger.info("Model launched.") else: @@ -63,12 +67,16 @@ class XinferenceRerankProvider(RerankProvider): except Exception as e: logger.error(f"Failed to initialize Xinference model: {e}") logger.debug( - f"Xinference initialization failed with exception: {e}", exc_info=True, + f"Xinference initialization failed with exception: {e}", + exc_info=True, ) self.model = None async def rerank( - self, query: str, documents: list[str], top_n: int | None = None, + self, + query: str, + documents: list[str], + top_n: int | None = None, ) -> list[RerankResult]: if not self.model: logger.error("Xinference rerank model is not initialized.") @@ -83,7 +91,8 @@ class XinferenceRerankProvider(RerankProvider): ) return [ RerankResult( - index=result["index"], relevance_score=result["relevance_score"], + index=result["index"], + relevance_score=result["relevance_score"], ) for result in results ] diff --git a/astrbot/core/skills/neo_skill_sync.py b/astrbot/core/skills/neo_skill_sync.py index fa7098c44..e70993e58 100644 --- a/astrbot/core/skills/neo_skill_sync.py +++ b/astrbot/core/skills/neo_skill_sync.py @@ -242,7 +242,8 @@ 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.") diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index 82bdf620a..75d234c79 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -339,7 +339,8 @@ 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, @@ -389,7 +390,8 @@ 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 diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index 46bf57929..2cf2dcee2 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -286,18 +286,23 @@ 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) diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index b8b877d18..7773b23c1 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -318,7 +318,8 @@ class Context: return self.provider_manager.llm_tools.deactivate_llm_tool(name) def get_provider_by_id( - self, provider_id: str, + self, + provider_id: str, ) -> ( Provider | TTSProvider | STTProvider | EmbeddingProvider | RerankProvider | None ): @@ -372,7 +373,8 @@ class Context: """ prov = self.provider_manager.get_using_provider( - provider_type=ProviderType.CHAT_COMPLETION, umo=umo, + provider_type=ProviderType.CHAT_COMPLETION, + umo=umo, ) if prov is None: return None @@ -394,7 +396,8 @@ class Context: """ prov = self.provider_manager.get_using_provider( - provider_type=ProviderType.TEXT_TO_SPEECH, umo=umo, + provider_type=ProviderType.TEXT_TO_SPEECH, + umo=umo, ) if prov and (not isinstance(prov, TTSProvider)): raise ValueError("返回的 Provider 不是 TTSProvider 类型") @@ -414,7 +417,8 @@ class Context: """ prov = self.provider_manager.get_using_provider( - provider_type=ProviderType.SPEECH_TO_TEXT, umo=umo, + provider_type=ProviderType.SPEECH_TO_TEXT, + umo=umo, ) if prov and (not isinstance(prov, STTProvider)): raise ValueError("返回的 Provider 不是 STTProvider 类型") @@ -438,7 +442,9 @@ class Context: return self.astrbot_config_mgr.get_conf(umo) async def send_message( - self, session: str | MessageSesion, message_chain: MessageChain, + self, + session: str | MessageSesion, + message_chain: MessageChain, ) -> bool: """根据 session(unified_msg_origin) 主动发送消息。 @@ -507,7 +513,11 @@ class Context: self.provider_manager.llm_tools.func_list.append(tool) def register_web_api( - self, route: str, view_handler: Awaitable, methods: list, desc: str, + self, + route: str, + view_handler: Awaitable, + methods: list, + desc: str, ) -> None: """注册 Web API。 diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 3b7b807ff..b9b36d5ac 100644 --- 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 328749bf7..18a93dc15 100644 --- 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] = [] diff --git a/astrbot/core/star/filter/permission.py b/astrbot/core/star/filter/permission.py index f60c93ac9..fec0d4bcd 100644 --- a/astrbot/core/star/filter/permission.py +++ b/astrbot/core/star/filter/permission.py @@ -15,7 +15,9 @@ 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/star_handler.py b/astrbot/core/star/register/star_handler.py index 975b05b78..3d3752649 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -415,7 +415,9 @@ def register_on_waiting_llm_request(**kwargs): def decorator(awaitable): _ = get_handler_or_create( - awaitable, EventType.OnWaitingLLMRequestEvent, **kwargs, + awaitable, + EventType.OnWaitingLLMRequestEvent, + **kwargs, ) return awaitable @@ -562,7 +564,7 @@ def register_llm_tool(name: str | None = None, **kwargs): | Awaitable[MessageEventResult | str | None], ], ): - llm_tool_name = name_ if name_ else getattr(awaitable, "__name__", "") + llm_tool_name = name_ or getattr(awaitable, "__name__", "") func_doc = awaitable.__doc__ or "" docstring = docstring_parser.parse(func_doc) args = [] diff --git a/astrbot/core/star/session_plugin_manager.py b/astrbot/core/star/session_plugin_manager.py index 2a95d4a98..1e4ffaf60 100644 --- a/astrbot/core/star/session_plugin_manager.py +++ b/astrbot/core/star/session_plugin_manager.py @@ -68,7 +68,8 @@ class SessionPluginManager: @staticmethod async def filter_handlers_by_session( - event: AstrMessageEvent, handlers: list, + event: AstrMessageEvent, + handlers: list, ) -> list: """根据会话配置过滤处理器列表 diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index b2fabe961..9a9a932f6 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -60,7 +60,11 @@ class PluginDependencyInstallError(Exception): """Raised when plugin dependency installation fails.""" def __init__( - self, *, plugin_label: str, requirements_path: str, error: Exception, + self, + *, + plugin_label: str, + requirements_path: str, + error: Exception, ) -> None: message = f"插件 {plugin_label} 依赖安装失败: {error!s}" super().__init__(message) @@ -108,7 +112,9 @@ async def _temporary_filtered_requirements_file(*, install_lines: tuple[str, ... async def _install_requirements_with_precheck( - *, plugin_label: str, requirements_path: str, + *, + plugin_label: str, + requirements_path: str, ) -> None: install_plan = plan_missing_requirements_install(requirements_path) if install_plan is None: @@ -154,7 +160,9 @@ 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" @@ -197,11 +205,13 @@ class PluginManager: continue if star.reserved: plugin_dir_path = os.path.join( - self.reserved_plugin_path, star.root_dir_name, + self.reserved_plugin_path, + star.root_dir_name, ) else: plugin_dir_path = os.path.join( - self.plugin_store_path, star.root_dir_name, + self.plugin_store_path, + star.root_dir_name, ) plugins_to_check.append((plugin_dir_path, star.name)) reloaded_plugins = set() @@ -266,7 +276,8 @@ 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,则检查所有插件的依赖 @@ -286,14 +297,17 @@ class PluginManager: return True async def _ensure_plugin_requirements( - self, plugin_dir_path: str, plugin_label: str, + self, + plugin_dir_path: str, + plugin_label: str, ) -> None: requirements_path = os.path.join(plugin_dir_path, "requirements.txt") if not await anyio.Path(requirements_path).exists(): return try: await _install_requirements_with_precheck( - plugin_label=plugin_label, requirements_path=requirements_path, + plugin_label=plugin_label, + requirements_path=requirements_path, ) except asyncio.CancelledError: raise @@ -302,13 +316,19 @@ class PluginManager: raise except Exception as e: dependency_error = PluginDependencyInstallError( - plugin_label=plugin_label, requirements_path=requirements_path, error=e, + plugin_label=plugin_label, + requirements_path=requirements_path, + error=e, ) logger.exception(str(dependency_error)) raise dependency_error from e async def _import_plugin_with_dependency_recovery( - self, path: str, module_str: str, root_dir_name: str, requirements_path: str, + self, + path: str, + module_str: str, + root_dir_name: str, + requirements_path: str, ) -> ModuleType: try: return __import__(path, fromlist=[module_str]) @@ -344,7 +364,8 @@ class PluginManager: raise Exception("插件不存在。") if os.path.exists(os.path.join(plugin_path, "metadata.yaml")): with open( - os.path.join(plugin_path, "metadata.yaml"), encoding="utf-8", + os.path.join(plugin_path, "metadata.yaml"), + encoding="utf-8", ) as f: metadata = yaml.safe_load(f) elif plugin_obj and hasattr(plugin_obj, "info"): @@ -446,7 +467,8 @@ class PluginManager: @staticmethod def _get_plugin_related_modules( - plugin_root_dir: str, is_reserved: bool = False, + plugin_root_dir: str, + is_reserved: bool = False, ) -> list[str]: """获取与指定插件相关的所有已加载模块名 @@ -491,7 +513,8 @@ class PluginManager: logger.debug(f"删除模块 {key}") if root_dir_name: for module_name in self._get_plugin_related_modules( - root_dir_name, is_reserved, + root_dir_name, + is_reserved, ): try: del sys.modules[module_name] @@ -741,15 +764,18 @@ class PluginManager: continue plugin_config = None plugin_schema_path = os.path.join( - plugin_dir_path, self.conf_schema_fname, + plugin_dir_path, + self.conf_schema_fname, ) if await anyio.Path(plugin_schema_path).exists(): async with await anyio.open_file( - plugin_schema_path, encoding="utf-8", + plugin_schema_path, + encoding="utf-8", ) as f: plugin_config = AstrBotConfig( config_path=os.path.join( - self.plugin_config_path, f"{root_dir_name}_config.json", + self.plugin_config_path, + f"{root_dir_name}_config.json", ), schema=json.loads(await f.read()), ) @@ -797,7 +823,8 @@ class PluginManager: if plugin_config and metadata.star_cls_type: try: metadata.star_cls = metadata.star_cls_type( - context=self.context, config=plugin_config, + context=self.context, + config=plugin_config, ) except TypeError as _: metadata.star_cls = metadata.star_cls_type( @@ -829,7 +856,8 @@ class PluginManager: ) for handler in related_handlers: handler.handler = functools.partial( - handler.handler, metadata.star_cls, + handler.handler, + metadata.star_cls, ) for func_tool in llm_tools.func_list: if isinstance(func_tool, HandoffTool): @@ -849,7 +877,8 @@ class PluginManager: ): ft.handler_module_path = metadata.module_path ft.handler = functools.partial( - ft.handler, metadata.star_cls, + ft.handler, + metadata.star_cls, ) if ft.name in inactivated_llm_tools: ft.active = False @@ -862,14 +891,16 @@ class PluginManager: if plugin_config: try: obj = getattr(module, classes[0])( - context=self.context, config=plugin_config, + context=self.context, + config=plugin_config, ) except TypeError as _: obj = getattr(module, classes[0])(context=self.context) else: obj = getattr(module, classes[0])(context=self.context) metadata = self._load_plugin_metadata( - plugin_path=plugin_dir_path, plugin_obj=obj, + plugin_path=plugin_dir_path, + plugin_obj=obj, ) if not metadata: raise Exception(f"无法找到插件 {plugin_dir_path} 的元数据。") @@ -908,7 +939,8 @@ class PluginManager: and handler.handler_name in alter_cmd[metadata.name] ): cmd_type = alter_cmd[metadata.name][handler.handler_name].get( - "permission", "member", + "permission", + "member", ) found_permission_filter = False for filter_ in handler.event_filters: @@ -991,7 +1023,9 @@ class PluginManager: return (True, None) async def _cleanup_failed_plugin_install( - self, dir_name: str, plugin_path: str, + self, + dir_name: str, + plugin_path: str, ) -> None: plugin = None for star in self.context.get_all_stars(): @@ -1014,7 +1048,8 @@ class PluginManager: except Exception as e: logger.warning(f"清理安装失败插件目录失败: {plugin_path},原因: {e!s}") plugin_config_path = os.path.join( - self.plugin_config_path, f"{dir_name}_config.json", + self.plugin_config_path, + f"{dir_name}_config.json", ) if await anyio.Path(plugin_config_path).exists(): try: @@ -1035,7 +1070,8 @@ class PluginManager: ) -> None: if delete_config: config_file = os.path.join( - self.plugin_config_path, f"{root_dir_name}_config.json", + self.plugin_config_path, + f"{root_dir_name}_config.json", ) if os.path.exists(config_file): try: @@ -1047,7 +1083,9 @@ class PluginManager: data_base_dir = os.path.dirname(self.plugin_store_path) for data_dir_name in ("plugin_data", "plugins_data"): plugin_data_dir = os.path.join( - data_base_dir, data_dir_name, root_dir_name, + data_base_dir, + data_dir_name, + root_dir_name, ) if os.path.exists(plugin_data_dir): try: @@ -1061,7 +1099,11 @@ class PluginManager: ) def _track_failed_install_dir( - self, *, dir_name: str, plugin_path: str, error: Exception, + self, + *, + dir_name: str, + plugin_path: str, + error: Exception, ) -> None: if ( not dir_name @@ -1083,7 +1125,10 @@ 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 安装插件 @@ -1119,7 +1164,8 @@ class PluginManager: dir_name = os.path.basename(plugin_path) metadata_dir_name = self._get_plugin_dir_name_from_metadata(plugin_path) target_plugin_path = os.path.join( - self.plugin_store_path, metadata_dir_name, + self.plugin_store_path, + metadata_dir_name, ) if ( target_plugin_path != plugin_path @@ -1169,7 +1215,9 @@ class PluginManager: return plugin_info except Exception as e: self._track_failed_install_dir( - dir_name=dir_name, plugin_path=plugin_path, error=e, + dir_name=dir_name, + plugin_path=plugin_path, + error=e, ) if dir_name and plugin_path: logger.warning( @@ -1178,7 +1226,10 @@ class PluginManager: raise async def uninstall_plugin( - self, plugin_name: str, delete_config: bool = False, delete_data: bool = False, + self, + plugin_name: str, + delete_config: bool = False, + delete_data: bool = False, ) -> None: """卸载指定的插件。 @@ -1223,7 +1274,10 @@ class PluginManager: ) async def uninstall_failed_plugin( - self, dir_name: str, delete_config: bool = False, delete_data: bool = False, + self, + dir_name: str, + delete_config: bool = False, + delete_data: bool = False, ) -> None: """卸载加载失败的插件(按目录名)。""" async with self._pm_lock: @@ -1240,7 +1294,8 @@ class PluginManager: except Exception as e: raise Exception( format_plugin_error( - "failed_plugin_dir_remove_error", error=f"{e!s}", + "failed_plugin_dir_remove_error", + error=f"{e!s}", ), ) from e else: @@ -1313,7 +1368,8 @@ class PluginManager: if plugin is None: return self._purge_modules( - root_dir_name=plugin.root_dir_name, is_reserved=plugin.reserved, + root_dir_name=plugin.root_dir_name, + is_reserved=plugin.reserved, ) async def update_plugin(self, plugin_name: str, proxy="") -> None: @@ -1381,7 +1437,9 @@ class PluginManager: return if (exc := fut.exception()) is not None: logger.error( - "插件 %s 在 __del__ 中抛出了异常:%r", star_metadata.name, exc, + "插件 %s 在 __del__ 中抛出了异常:%r", + star_metadata.name, + exc, ) future.add_done_callback(_log_del_exception) @@ -1445,11 +1503,14 @@ 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 try: @@ -1471,11 +1532,13 @@ class PluginManager: logger.warning(f"删除插件压缩包失败: {e!s}") await self._ensure_plugin_requirements(desti_dir, dir_name) success, error_message = await self.load( - specified_dir_name=dir_name, ignore_version_check=ignore_version_check, + specified_dir_name=dir_name, + ignore_version_check=ignore_version_check, ) if not success: raise Exception( - error_message or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。", + error_message + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。", ) plugin = self.context.get_registered_star(dir_name) if not plugin: @@ -1510,7 +1573,9 @@ class PluginManager: return plugin_info except Exception as e: self._track_failed_install_dir( - dir_name=dir_name, plugin_path=desti_dir, error=e, + dir_name=dir_name, + plugin_path=desti_dir, + error=e, ) logger.warning(f"安装插件 {dir_name} 失败,插件安装目录:{desti_dir}") raise diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index 426f6172d..7e5032482 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -21,7 +21,9 @@ 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 diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index 735dbe624..cef8aae48 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -54,7 +54,9 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]): ) 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: @@ -123,7 +125,9 @@ class DeleteCronJobTool(FunctionTool[AstrAgentContext]): ) 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: @@ -158,7 +162,9 @@ class ListCronJobsTool(FunctionTool[AstrAgentContext]): ) 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: diff --git a/astrbot/core/tools/kb_query.py b/astrbot/core/tools/kb_query.py index d3acd97af..c9a02ee8d 100644 --- a/astrbot/core/tools/kb_query.py +++ b/astrbot/core/tools/kb_query.py @@ -42,7 +42,9 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): ) 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/knowledge_base_tools.py b/astrbot/core/tools/knowledge_base_tools.py index a3b7fb837..94da77404 100644 --- a/astrbot/core/tools/knowledge_base_tools.py +++ b/astrbot/core/tools/knowledge_base_tools.py @@ -107,7 +107,9 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): ) 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 9c4e6ca71..3789677e0 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -75,7 +75,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): ) 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 +92,8 @@ 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 +104,9 @@ 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 +133,8 @@ 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 +146,8 @@ 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 +159,8 @@ 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 +178,8 @@ 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/send_message.py b/astrbot/core/tools/send_message.py index a4ecc5641..a84b6532f 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -57,7 +57,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): ) async def _resolve_path_from_sandbox( - self, context: ContextWrapper[AstrAgentContext], path: str, + self, + context: ContextWrapper[AstrAgentContext], + path: str, ) -> tuple[str, bool]: """If the path exists locally, return it directly. Otherwise, check if it exists in the sandbox and download it. @@ -68,7 +70,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return (path, False) try: sb = await get_booter( - context.context.context, context.context.event.unified_msg_origin, + context.context.context, + context.context.event.unified_msg_origin, ) import shlex @@ -78,7 +81,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if "_&exists_" in json.dumps(result): name = anyio.Path(path).name 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}") @@ -88,7 +92,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return (path, False) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs: Any, + self, + context: ContextWrapper[AstrAgentContext], + **kwargs: Any, ) -> ToolExecResult: session: str | MessageSession = ( kwargs.get("session") or context.context.event.unified_msg_origin @@ -189,7 +195,8 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): except Exception as e: return f"error: invalid session: {e}" await context.context.context.send_message( - target_session, MessageChain(chain=components), + target_session, + MessageChain(chain=components), ) return f"Message sent to session {target_session}" diff --git a/astrbot/core/tools/web_search_tools.py b/astrbot/core/tools/web_search_tools.py index ee94f4b63..f2d595245 100644 --- a/astrbot/core/tools/web_search_tools.py +++ b/astrbot/core/tools/web_search_tools.py @@ -124,11 +124,14 @@ async def _tavily_search( "Authorization": f"Bearer {tavily_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session, session.post( - "https://api.tavily.com/search", - json=payload, - headers=header, - ) as response: + 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( @@ -152,11 +155,14 @@ 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, session.post( - "https://api.tavily.com/extract", - json=payload, - headers=header, - ) as response: + 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( @@ -180,11 +186,14 @@ async def _bocha_search( "Authorization": f"Bearer {bocha_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session, session.post( - "https://api.bochaai.com/v1/web-search", - json=payload, - headers=header, - ) as response: + 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( @@ -212,11 +221,14 @@ async def _brave_search( "Accept": "application/json", "X-Subscription-Token": brave_key, } - 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: + 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( @@ -247,11 +259,14 @@ async def _baidu_search( "X-Appbuilder-Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } - 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: + 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( diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index 55d8b1ec7..c6175d733 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -145,7 +145,11 @@ class AstrBotUpdator(RepoZipUpdator): return await self.fetch_release_info(self.ASTRBOT_RELEASE_API) async def update( # type: ignore[override] - self, reboot=False, latest=True, version=None, proxy="", + self, + reboot=False, + latest=True, + version=None, + proxy="", ) -> None: update_data = await self.fetch_release_info(self.ASTRBOT_RELEASE_API, latest) file_url = None diff --git a/astrbot/core/utils/auth_password.py b/astrbot/core/utils/auth_password.py index 4f9f4f718..db5a686c2 100644 --- a/astrbot/core/utils/auth_password.py +++ b/astrbot/core/utils/auth_password.py @@ -107,7 +107,9 @@ def get_dashboard_login_challenge(stored_hash: str) -> dict[str, Any]: def verify_dashboard_login_proof( - stored_hash: str, challenge_nonce: str, proof: str, + stored_hash: str, + challenge_nonce: str, + proof: str, ) -> bool: """Verify an HMAC-SHA256 login proof generated from the stored password secret.""" if ( @@ -160,7 +162,8 @@ def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool # new clients send plain password, old clients may send md5 of it. candidate_md5 = hashlib.md5(candidate_password.encode("utf-8")).hexdigest() return hmac.compare_digest( - stored_hash.lower(), candidate_md5.lower(), + stored_hash.lower(), + candidate_md5.lower(), ) or hmac.compare_digest( stored_hash.lower(), candidate_password.lower(), @@ -193,7 +196,8 @@ def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool candidate_password.encode("utf-8"), ).hexdigest() return hmac.compare_digest( - stored_hash.lower(), candidate_sha256.lower(), + stored_hash.lower(), + candidate_sha256.lower(), ) or hmac.compare_digest(stored_hash.lower(), candidate_password.lower()) return False diff --git a/astrbot/core/utils/core_constraints.py b/astrbot/core/utils/core_constraints.py index f829a9067..ab1e09710 100644 --- a/astrbot/core/utils/core_constraints.py +++ b/astrbot/core/utils/core_constraints.py @@ -113,7 +113,10 @@ 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 @@ -149,7 +152,10 @@ class CoreConstraintsProvider: def _make_tmp() -> str: 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)) return f.name diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 8bea4793d..3f23cdc1d 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -146,7 +146,8 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non connector=connector, ) as session: async with session.get( - url, timeout=aiohttp.ClientTimeout(total=1800), + url, + timeout=aiohttp.ClientTimeout(total=1800), ) as resp: if resp.status != 200: raise Exception(f"下载文件失败: {resp.status}") diff --git a/astrbot/core/utils/llm_metadata.py b/astrbot/core/utils/llm_metadata.py index 254446bc9..54e45fc48 100644 --- a/astrbot/core/utils/llm_metadata.py +++ b/astrbot/core/utils/llm_metadata.py @@ -33,9 +33,13 @@ LLM_METADATAS: dict[str, LLMMetadata] = {} 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, session.get(url) as response: + async with ( + aiohttp.ClientSession( + trust_env=True, + connector=build_tls_connector(), + ) as session, + session.get(url) as response, + ): data = await response.json() global LLM_METADATAS models = {} @@ -51,7 +55,8 @@ async def update_llm_metadata() -> None: knowledge=model.get("knowledge", "none"), release_date=model.get("release_date", ""), modalities=model.get( - "modalities", {"input": [], "output": []}, + "modalities", + {"input": [], "output": []}, ), open_weights=model.get("open_weights", False), limit=model.get("limit", {"context": 0, "output": 0}), diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index ab1098458..2827b0460 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -147,7 +147,9 @@ 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转换视频格式 diff --git a/astrbot/core/utils/migra_helper.py b/astrbot/core/utils/migra_helper.py index e1c4c8367..3cfc0a77d 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"] if default_prov_id in ids_map: @@ -127,7 +126,10 @@ 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. btw, i really don't like migration :( diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 51a5ca187..03a870418 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -70,7 +70,8 @@ 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: diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index 92bb9d431..a53964b4b 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -54,7 +54,11 @@ 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 @@ -559,7 +563,8 @@ 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 +624,8 @@ 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(".")) @@ -914,7 +920,9 @@ 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, ) @@ -984,7 +992,9 @@ class PipInstaller: mirror: str | None = None, ) -> 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 文件。") diff --git a/astrbot/core/utils/requirements_utils.py b/astrbot/core/utils/requirements_utils.py index 7012a3ac4..362a64566 100644 --- a/astrbot/core/utils/requirements_utils.py +++ b/astrbot/core/utils/requirements_utils.py @@ -21,7 +21,6 @@ class RequirementsPrecheckFailed(Exception): """Raised when the pre-check of requirements fails.""" - @dataclass(frozen=True) class ParsedPackageInput: specs: tuple[str, ...] @@ -252,7 +251,8 @@ 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) diff --git a/astrbot/core/utils/session_lock.py b/astrbot/core/utils/session_lock.py index 684fc0240..eb4e8fe1e 100644 --- a/astrbot/core/utils/session_lock.py +++ b/astrbot/core/utils/session_lock.py @@ -36,7 +36,8 @@ 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 300df9d45..6bade846a 100644 --- a/astrbot/core/utils/shared_preferences.py +++ b/astrbot/core/utils/shared_preferences.py @@ -32,7 +32,10 @@ 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 +206,11 @@ 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 +219,10 @@ 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 5b9a39515..d25282a47 100644 --- a/astrbot/core/utils/temp_dir_cleaner.py +++ b/astrbot/core/utils/temp_dir_cleaner.py @@ -81,7 +81,9 @@ 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/zip_updator.py b/astrbot/core/zip_updator.py index 30500d28f..bc15e45ea 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -170,7 +170,10 @@ 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/auth.py b/astrbot/dashboard/routes/auth.py index 4eb55b53a..f40513a1c 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -87,7 +87,8 @@ class AuthRoute(Route): login_verified = False if isinstance(req_password, str): login_verified = req_username == username and verify_dashboard_password( - password, req_password, + password, + req_password, ) elif isinstance(req_challenge_id, str) and isinstance(req_password_proof, str): challenge_nonce = self._consume_login_challenge(req_challenge_id) @@ -95,7 +96,9 @@ class AuthRoute(Route): req_username == username and isinstance(challenge_nonce, str) and verify_dashboard_login_proof( - password, challenge_nonce, req_password_proof, + password, + challenge_nonce, + req_password_proof, ) ) else: diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index a7add7401..15fa46f0f 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -187,7 +187,10 @@ 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, @@ -313,7 +316,8 @@ 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"), @@ -700,7 +704,8 @@ class BackupRoute(Route): for i in range(total): chunk_path = os.path.join(chunk_dir, f"{i}.part") async with await anyio.open_file( - chunk_path, "rb", + chunk_path, + "rb", ) as chunk_file: # 分块读取,避免内存溢出 while True: diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index e428cb44d..c2e615498 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -109,7 +109,8 @@ class ChatRoute(Route): resolved_base_dir = _resolve_path(self.attachments_dir) if not await anyio.Path(resolved_file_path).exists(): file_path = os.path.join( - self.legacy_img_dir, os.path.basename(filename), + self.legacy_img_dir, + os.path.basename(filename), ) if await anyio.Path(file_path).exists(): resolved_file_path = _resolve_path(file_path) @@ -139,7 +140,8 @@ class ChatRoute(Route): file_path = attachment.path resolved_file_path = _resolve_path(file_path) return await send_file( - str(resolved_file_path), mimetype=attachment.mime_type, + str(resolved_file_path), + mimetype=attachment.mime_type, ) except (FileNotFoundError, OSError): return Response().error("File access error").to_json() @@ -163,7 +165,9 @@ class ChatRoute(Route): path = os.path.join(self.attachments_dir, filename) await file.save(path) attachment = await self.db.insert_attachment( - path=path, type=attach_type, mime_type=content_type, + path=path, + type=attach_type, + mime_type=content_type, ) if not attachment: return Response().error("Failed to create attachment").to_json() @@ -183,11 +187,15 @@ class ChatRoute(Route): async def _build_user_message_parts(self, message: str | list) -> list[dict]: """构建用户消息的部分列表。""" return await build_webchat_message_parts( - message, get_attachment_by_id=self.db.get_attachment_by_id, strict=False, + message, + get_attachment_by_id=self.db.get_attachment_by_id, + strict=False, ) 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( @@ -199,7 +207,9 @@ 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 的引用 @@ -309,7 +319,8 @@ class ChatRoute(Route): ) message_id = str(uuid.uuid4()) back_queue = webchat_queue_mgr.get_or_create_back_queue( - message_id, webchat_conv_id, + message_id, + webchat_conv_id, ) async def stream(): @@ -330,7 +341,8 @@ 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 @@ -404,21 +416,24 @@ 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) elif msg_type == "file": filename = result_text.replace("[FILE]", "") part = await self._create_attachment_from_file( - filename, "file", + filename, + "file", ) if part: accumulated_parts.append(part) @@ -432,7 +447,8 @@ class ChatRoute(Route): continue try: refs = self._extract_web_search_refs( - accumulated_text, accumulated_parts, + accumulated_text, + accumulated_parts, ) except Exception as e: logger.exception( @@ -548,7 +564,9 @@ class ChatRoute(Route): if attachment_ids: await self._delete_attachments(attachment_ids) await mgr.delete( - platform_id=session.platform_id, user_id=session_id, offset_sec=99999999, + platform_id=session.platform_id, + user_id=session_id, + offset_sec=99999999, ) try: router = self.umop_config_router @@ -658,7 +676,9 @@ class ChatRoute(Route): username = g.get("username", "guest") platform_id = request.args.get("platform_id", "webchat") session = await self.db.create_platform_session( - creator=username, platform_id=platform_id, is_group=0, + creator=username, + platform_id=platform_id, + is_group=0, ) return ( Response() @@ -707,12 +727,16 @@ class ChatRoute(Route): platform_id = session.platform_id if session else "webchat" username = g.get("username", "guest") project_info = await self.db.get_project_by_session( - session_id=session_id, creator=username, + session_id=session_id, + creator=username, ) mgr = self.platform_history_mgr assert mgr is not None history_ls = await mgr.get( - platform_id=platform_id, user_id=session_id, page=1, page_size=1000, + platform_id=platform_id, + user_id=session_id, + page=1, + page_size=1000, ) history_res = [history.model_dump() for history in history_ls] response_data: dict[str, Any] = { @@ -743,6 +767,7 @@ class ChatRoute(Route): if session.creator != username: return Response().error("Permission denied").to_json() await self.db.update_platform_session( - session_id=session_id, display_name=display_name, + session_id=session_id, + display_name=display_name, ) return Response().ok().to_json() diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index ce446f28f..9b32bac56 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -50,11 +50,10 @@ def try_cast(value: Any, type_: str): except (ValueError, TypeError): return None elif ( - (type_ == "float" + type_ == "float" and isinstance(value, str) - and value.replace(".", "", 1).isdigit()) - or (type_ == "float" and isinstance(value, int)) - ): + and value.replace(".", "", 1).isdigit() + ) or (type_ == "float" and isinstance(value, int)): return float(value) elif type_ == "float": try: @@ -250,7 +249,8 @@ async def _validate_neo_connectivity(post_config: dict) -> str | None: try: async with aiohttp.ClientSession() as session: async with session.get( - health_url, timeout=aiohttp.ClientTimeout(total=5), + health_url, + timeout=aiohttp.ClientTimeout(total=5), ) as resp: if resp.status != 200: return f"⚠️ Bay 健康检查失败 (HTTP {resp.status}),请确认 Bay 正在运行: {endpoint}" @@ -260,7 +260,9 @@ async def _validate_neo_connectivity(post_config: dict) -> str | None: def save_config( - post_config: dict, config: AstrBotConfig, is_core: bool = False, + post_config: dict, + config: AstrBotConfig, + is_core: bool = False, ) -> None: """验证并保存配置""" errors = None @@ -270,11 +272,15 @@ def save_config( try: if is_core: errors, post_config = validate_config( - post_config, CONFIG_METADATA_2, is_core, + post_config, + CONFIG_METADATA_2, + is_core, ) 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()) @@ -287,7 +293,9 @@ def save_config( class ConfigRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, + self, + context: RouteContext, + core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle @@ -650,7 +658,10 @@ class ConfigRoute(Route): return status_info def _error_response( - self, message: str, status_code: int = 500, log_fn=logger.error, + self, + message: str, + status_code: int = 500, + log_fn=logger.error, ): log_fn(message) if status_code == 500: @@ -662,7 +673,9 @@ class ConfigRoute(Route): provider_id = request.args.get("id") if not provider_id: return self._error_response( - "Missing provider_id parameter", 400, logger.warning, + "Missing provider_id parameter", + 400, + logger.warning, ) logger.info(f"API call: /config/provider/check_one id={provider_id}") try: @@ -683,7 +696,8 @@ class ConfigRoute(Route): return Response().ok(result).to_json() except Exception as e: return self._error_response( - f"Critical error checking provider {provider_id}: {e}", 500, + f"Critical error checking provider {provider_id}: {e}", + 500, ) async def get_configs(self): @@ -1017,7 +1031,9 @@ class ConfigRoute(Route): return ( Response() .error( - "Upload failed: " + ", ".join(errors) if errors else "Upload failed", + "Upload failed: " + ", ".join(errors) + if errors + else "Upload failed", ) .to_json() ) @@ -1205,7 +1221,8 @@ class ConfigRoute(Route): if cache_key in self._logo_token_cache: cached_token = self._logo_token_cache[cache_key] 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 @@ -1220,10 +1237,12 @@ class ConfigRoute(Route): logo_file_path = os.path.join(plugin_dir, platform.logo_path) if await anyio.Path(logo_file_path).exists(): logo_token = await file_token_service.register_file( - logo_file_path, expire_seconds=3600, + logo_file_path, + expire_seconds=3600, ) 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"] = logo_token @@ -1245,7 +1264,10 @@ 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", {}) @@ -1254,7 +1276,8 @@ class ConfigRoute(Route): i18n_prefix = f"platform_group.platform.{platform.name}" 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(): for key in ("description", "hint", "labels"): @@ -1291,7 +1314,9 @@ class ConfigRoute(Route): ) if platform.config_metadata: self._inject_platform_metadata_with_i18n( - platform, metadata, platform_i18n_translations, + platform, + metadata, + platform_i18n_translations, ) if platform.logo_path: logo_registration_tasks.append( @@ -1329,7 +1354,9 @@ 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 not self.acm or conf_id not in self.acm.confs: @@ -1355,7 +1382,9 @@ class ConfigRoute(Route): assert md.config is not None 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/cron.py b/astrbot/dashboard/routes/cron.py index cac511d3c..1644078f8 100644 --- a/astrbot/dashboard/routes/cron.py +++ b/astrbot/dashboard/routes/cron.py @@ -11,7 +11,9 @@ 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 diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index 146fb58f9..81cd2abc1 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -38,10 +38,12 @@ class KnowledgeBaseRoute(Route): self.session_config_db = None # 会话配置数据库 self.retrieval_manager = None self.upload_progress: dict[ - str, Any, + str, + Any, ] = {} # 存储上传进度 {task_id: {status, file_index, file_total, stage, current, total}} self.upload_tasks: dict[ - str, Any, + str, + Any, ] = {} # 存储后台上传任务 {task_id: {"status", "result", "error"}} # 注册路由 @@ -83,7 +85,11 @@ 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, @@ -176,7 +182,9 @@ 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( @@ -258,7 +266,9 @@ 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 @@ -395,7 +405,8 @@ class KnowledgeBaseRoute(Route): rerank_provider_id, ) if rerank_prv is not None and not isinstance( - rerank_prv, RerankProvider, + rerank_prv, + RerankProvider, ): return Response().error("重排序模型类型错误").to_json() if not rerank_prv: diff --git a/astrbot/dashboard/routes/live_chat.py b/astrbot/dashboard/routes/live_chat.py index 0523a63ab..eef4dba74 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -254,7 +254,9 @@ 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( @@ -266,7 +268,9 @@ 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 = ["web_search_tavily", "web_search_bocha"] @@ -357,7 +361,8 @@ 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: @@ -414,7 +419,9 @@ 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") @@ -791,7 +798,10 @@ 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: @@ -869,7 +879,9 @@ 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(): @@ -1012,7 +1024,10 @@ class LiveChatRoute(Route): 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 + " [用户打断]" diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index d2663cdbc..9e8a2a52c 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -34,10 +34,14 @@ class LogRoute(Route): self.log_broker = log_broker self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"]) self.app.add_url_rule( - "/api/log-history", view_func=self.log_history, methods=["GET"], + "/api/log-history", + view_func=self.log_history, + methods=["GET"], ) self.app.add_url_rule( - "/api/trace/settings", view_func=self.get_trace_settings, methods=["GET"], + "/api/trace/settings", + view_func=self.get_trace_settings, + methods=["GET"], ) self.app.add_url_rule( "/api/trace/settings", @@ -46,7 +50,8 @@ class LogRoute(Route): ) async def _replay_cached_logs( - self, last_event_id: str, + self, + last_event_id: str, ) -> AsyncGenerator[str, None]: """辅助生成器:重放缓存的日志""" try: diff --git a/astrbot/dashboard/routes/open_api.py b/astrbot/dashboard/routes/open_api.py index 8e85cf053..5113c14f1 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -300,7 +300,8 @@ class OpenApiRoute(Route): ) 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 @@ -443,7 +444,10 @@ 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": @@ -455,28 +459,32 @@ 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) @@ -526,7 +534,8 @@ 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) diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 9e80002d0..4dce93a9c 100644 --- a/astrbot/dashboard/routes/persona.py +++ b/astrbot/dashboard/routes/persona.py @@ -47,7 +47,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() @@ -159,9 +159,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, @@ -217,7 +217,8 @@ 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("自定义报错回复信息必须是字符串").to_json() if isinstance(custom_error_message, str): diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index a56a5c514..f349eb178 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -336,7 +336,10 @@ class PluginRoute(Route): return None async def _save_plugin_cache( - self, cache_file: str, data, md5: str | None = None, + self, + cache_file: str, + data, + md5: str | None = None, ) -> None: """保存插件市场数据到本地缓存""" try: @@ -363,7 +366,8 @@ class PluginRoute(Route): if not await file_token_service.check_token_expired(token): return self._logo_cache[logo_path] token = await file_token_service.register_file( - logo_path, expire_seconds=300, + logo_path, + expire_seconds=300, ) self._logo_cache[logo_path] = token return token @@ -858,7 +862,8 @@ class PluginRoute(Route): if await anyio.Path(changelog_path).is_file(): try: async with await anyio.open_file( - changelog_path, encoding="utf-8", + changelog_path, + encoding="utf-8", ) as f: changelog_content = await f.read() return ( diff --git a/astrbot/dashboard/routes/route.py b/astrbot/dashboard/routes/route.py index 57dc0f9f7..4bed30c32 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -16,7 +16,9 @@ RUNTIME_FAILED_MESSAGE = "Runtime bootstrap failed. Please check logs and retry. def is_runtime_request_ready(core_lifecycle: "AstrBotCoreLifecycle") -> bool: return getattr( - core_lifecycle, "runtime_request_ready", core_lifecycle.runtime_ready, + core_lifecycle, + "runtime_request_ready", + core_lifecycle.runtime_ready, ) @@ -107,7 +109,10 @@ class Route: full_path = f"/api{path}" endpoint = f"{self.__class__.__name__.lower()}_{func.__name__}" self.app.add_url_rule( - full_path, view_func=func, methods=[method], endpoint=endpoint, + full_path, + view_func=func, + methods=[method], + endpoint=endpoint, ) # 兼容字典和列表两种格式 diff --git a/astrbot/dashboard/routes/session_management.py b/astrbot/dashboard/routes/session_management.py index a73be8b7c..b194cfd55 100644 --- a/astrbot/dashboard/routes/session_management.py +++ b/astrbot/dashboard/routes/session_management.py @@ -50,7 +50,10 @@ 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 及其规则内容(支持分页和搜索)。 @@ -126,7 +129,9 @@ class SessionManagementRoute(Route): page_size = 10 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, ) rules_list = [] for umo, rules in umo_rules.items(): @@ -264,9 +269,7 @@ class SessionManagementRoute(Route): .to_json() ) await sp.clear_async("umo", umo) - return ( - Response().ok({"message": "所有规则已删除", "umo": umo}).to_json() - ) + return Response().ok({"message": "所有规则已删除", "umo": umo}).to_json() except Exception as e: logger.error(f"删除会话规则失败: {e!s}") return Response().error(f"删除会话规则失败: {e!s}").to_json() diff --git a/astrbot/dashboard/routes/skills.py b/astrbot/dashboard/routes/skills.py index 49323b581..3aab90a31 100644 --- a/astrbot/dashboard/routes/skills.py +++ b/astrbot/dashboard/routes/skills.py @@ -100,12 +100,18 @@ class SkillsRoute(Route): 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) @@ -134,12 +140,15 @@ 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() @@ -183,12 +192,15 @@ 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 @@ -265,7 +277,8 @@ 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( diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 83bdac3cc..429cbc6ab 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -218,10 +218,15 @@ 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) @@ -274,7 +279,9 @@ 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 diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py index 35784ceaa..88021f70f 100644 --- a/astrbot/dashboard/routes/t2i.py +++ b/astrbot/dashboard/routes/t2i.py @@ -13,7 +13,9 @@ 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 6eebb881e..1f9e00fd7 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -17,7 +17,6 @@ class EmptyMcpServersError(ValueError): """Raised when mcpServers is empty.""" - def _extract_mcp_server_config(mcp_servers_value: object) -> dict: """Extract server configuration from user-submitted mcpServers field. @@ -39,7 +38,9 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: class ToolsRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, + self, + context: RouteContext, + core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle @@ -145,7 +146,9 @@ class ToolsRoute(Route): if self.tool_mgr.save_mcp_config(config): try: await self.tool_mgr.enable_mcp_server( - name, server_config, init_timeout=30, + name, + server_config, + init_timeout=30, ) except TimeoutError: rollback_ok = self._rollback_mcp_server(name) @@ -221,7 +224,8 @@ class ToolsRoute(Route): ): try: await self.tool_mgr.disable_mcp_server( - old_name, shutdown_timeout=10, + old_name, + shutdown_timeout=10, ) except TimeoutError as e: return ( @@ -242,7 +246,9 @@ class ToolsRoute(Route): ) try: await self.tool_mgr.enable_mcp_server( - name, config["mcpServers"][name], init_timeout=30, + name, + config["mcpServers"][name], + init_timeout=30, ) except TimeoutError: return ( diff --git a/astrbot/dashboard/routes/tui_chat.py b/astrbot/dashboard/routes/tui_chat.py index f174db9f7..6449e0ad3 100644 --- a/astrbot/dashboard/routes/tui_chat.py +++ b/astrbot/dashboard/routes/tui_chat.py @@ -124,7 +124,8 @@ class TUIChatRoute(Route): file_path = attachment.path resolved_file_path = _resolve_path(file_path) return await send_file( - str(resolved_file_path), mimetype=attachment.mime_type, + str(resolved_file_path), + mimetype=attachment.mime_type, ) except (FileNotFoundError, OSError): return Response().error("File access error").to_json() @@ -148,7 +149,9 @@ class TUIChatRoute(Route): path = os.path.join(self.attachments_dir, filename) await file.save(path) attachment = await self.db.insert_attachment( - path=path, type=attach_type, mime_type=content_type, + path=path, + type=attach_type, + mime_type=content_type, ) if not attachment: return Response().error("Failed to create attachment").to_json() @@ -168,11 +171,15 @@ class TUIChatRoute(Route): async def _build_user_message_parts(self, message: str | list) -> list[dict]: """Build user message parts list.""" return await build_webchat_message_parts( - message, get_attachment_by_id=self.db.get_attachment_by_id, strict=False, + message, + get_attachment_by_id=self.db.get_attachment_by_id, + strict=False, ) async def _create_attachment_from_file( - self, filename: str, attach_type: str, + self, + filename: str, + attach_type: str, ) -> dict | None: """Create attachment from local file and return message part.""" return await create_attachment_part_from_existing_file( @@ -262,7 +269,8 @@ class TUIChatRoute(Route): async with track_conversation(self.running_convs, tui_conv_id): while True: result, should_break = await _poll_tui_stream_result( - back_queue, username, + back_queue, + username, ) if should_break: client_disconnected = True @@ -332,21 +340,24 @@ class TUIChatRoute(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) elif msg_type == "file": filename = result_text.replace("[FILE]", "") part = await self._create_attachment_from_file( - filename, "file", + filename, + "file", ) if part: accumulated_parts.append(part) @@ -469,7 +480,9 @@ class TUIChatRoute(Route): if attachment_ids: await self._delete_attachments(attachment_ids) await mgr.delete( - platform_id=session.platform_id, user_id=session_id, offset_sec=99999999, + platform_id=session.platform_id, + user_id=session_id, + offset_sec=99999999, ) try: router = self.umop_config_router @@ -581,7 +594,9 @@ class TUIChatRoute(Route): """Create a new Platform session for TUI.""" username = g.get("username", "guest") session = await self.db.create_platform_session( - creator=username, platform_id="tui", is_group=0, + creator=username, + platform_id="tui", + is_group=0, ) return ( Response() @@ -630,12 +645,16 @@ class TUIChatRoute(Route): platform_id = session.platform_id if session else "tui" 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, ) mgr = self.platform_history_mgr assert mgr is not None history_ls = await mgr.get( - platform_id=platform_id, user_id=session_id, page=1, page_size=1000, + platform_id=platform_id, + user_id=session_id, + page=1, + page_size=1000, ) history_res = [history.model_dump() for history in history_ls] response_data: dict[str, Any] = { @@ -666,6 +685,7 @@ class TUIChatRoute(Route): if session.creator != username: return Response().error("Permission denied").to_json() await self.db.update_platform_session( - session_id=session_id, display_name=display_name, + session_id=session_id, + display_name=display_name, ) return Response().ok().to_json() diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index f6c5fb8ff..082e04174 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -319,7 +319,9 @@ class AstrBotDashboard: self.subagent_route = SubAgentRoute(self.context, self.core_lifecycle) self.skills_route = SkillsRoute(self.context, self.core_lifecycle) self.conversation_route = ConversationRoute( - self.context, db, self.core_lifecycle, + self.context, + db, + self.core_lifecycle, ) self.file_route = FileRoute(self.context) self.session_management_route = SessionManagementRoute( @@ -368,7 +370,10 @@ class AstrBotDashboard: self._jwt_secret = dashboard_cfg["jwt_secret"] async def guarded_srv_plug_route( - self, subpath: str, *args, **kwargs, + self, + subpath: str, + *args, + **kwargs, ) -> ResponseReturnValue: guard_resp = self._maybe_runtime_guard(request.path) if guard_resp is not None: diff --git a/astrbot/dashboard/utils.py b/astrbot/dashboard/utils.py index c240e9f20..64ac2a866 100644 --- a/astrbot/dashboard/utils.py +++ b/astrbot/dashboard/utils.py @@ -8,7 +8,9 @@ from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager async def generate_tsne_visualization( - query: str, kb_names: list[str], kb_manager: KnowledgeBaseManager, + query: str, + kb_names: list[str], + kb_manager: KnowledgeBaseManager, ) -> str | None: """生成 t-SNE 可视化图片 diff --git a/types/faiss/__init__.pyi b/types/faiss/__init__.pyi index 299ed7718..165274f3d 100644 --- a/types/faiss/__init__.pyi +++ b/types/faiss/__init__.pyi @@ -25,17 +25,26 @@ 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: ... diff --git a/types/readability.pyi b/types/readability.pyi index 7dd5760d4..4a7111b2d 100644 --- a/types/readability.pyi +++ b/types/readability.pyi @@ -17,6 +17,8 @@ class Document: def author(self) -> str: ... def short_title(self) -> str: ... def summary( - self, html_partial: bool = False, keep_all_images: bool = False, + self, + html_partial: bool = False, + keep_all_images: bool = False, ) -> str: ... def get_clean_html(self) -> str: ...