diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 000000000..639b90da7 --- /dev/null +++ b/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json new file mode 100644 index 000000000..72e1c3f5d --- /dev/null +++ b/.obsidian/workspace.json @@ -0,0 +1,192 @@ +{ + "main": { + "id": "f98f11a1ba9b1b69", + "type": "split", + "children": [ + { + "id": "4347c65e925426f6", + "type": "tabs", + "children": [ + { + "id": "7ddf9ff7a14ac645", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "README_zh.md", + "mode": "source", + "source": false + }, + "icon": "lucide-file", + "title": "README_zh" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "3873c1543132d010", + "type": "split", + "children": [ + { + "id": "98d0304b3093fe36", + "type": "tabs", + "children": [ + { + "id": "0765d293c9935b2a", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "文件列表" + } + }, + { + "id": "1dbfe7984144b1ea", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "搜索" + } + }, + { + "id": "66c197bef01cb226", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "书签" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "bef5d27b73e38648", + "type": "split", + "children": [ + { + "id": "076ab49585b131f0", + "type": "tabs", + "children": [ + { + "id": "9d5e8d1459abee95", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "README_zh.md", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "README_zh 的反向链接列表" + } + }, + { + "id": "9a91f0eb932cd56e", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "README_zh.md", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "README_zh 的出链列表" + } + }, + { + "id": "5c9c48f356751800", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "标签" + } + }, + { + "id": "5b41ca46073ca7d6", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "添加笔记属性" + } + }, + { + "id": "65fe5368baf4273d", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "README_zh.md", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "README_zh 的大纲" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "left-ribbon": { + "hiddenItems": { + "switcher:打开快速切换": false, + "graph:查看关系图谱": false, + "canvas:新建白板": false, + "daily-notes:打开/创建今天的日记": false, + "templates:插入模板": false, + "command-palette:打开命令面板": false, + "bases:新建数据库": false + } + }, + "active": "9d5e8d1459abee95", + "lastOpenFiles": [ + "README_fr.md", + "README.md", + "AGENTS.md", + "docs/skills/astrbot-dev-guide.md" + ] +} \ No newline at end of file diff --git a/astrbot/__main__.py b/astrbot/__main__.py index 854d3901a..fbd5f822b 100644 --- a/astrbot/__main__.py +++ b/astrbot/__main__.py @@ -99,7 +99,7 @@ async def check_dashboard_files(webui_dir: str | None = None): await download_dashboard(version=f"v{VERSION}", latest=False) except Exception as e: logger.warning( - f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。" + f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。", ) try: await download_dashboard(latest=True) @@ -118,7 +118,7 @@ async def main_async(webui_dir_arg: str | None, log_broker: LogBroker) -> None: if webui_dir is None: logger.warning( "管理面板文件检查失败,WebUI 功能将不可用。" - "请检查网络连接或手动指定 --webui-dir 参数。" + "请检查网络连接或手动指定 --webui-dir 参数。", ) db = db_helper diff --git a/astrbot/_internal/__init__.py b/astrbot/_internal/__init__.py index 7331d163d..9d40e6d56 100644 --- a/astrbot/_internal/__init__.py +++ b/astrbot/_internal/__init__.py @@ -1,5 +1,4 @@ -""" -Astbot内部实现 +"""Astbot内部实现 外部模块请勿导入 """ diff --git a/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py b/astrbot/_internal/abc/_abp/base_astrbot_abp_client.py index 07397e983..ccb87ce4b 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 @@ -9,8 +8,7 @@ from typing import Any class BaseAstrbotAbpClient(ABC): - """ - ABP client: in-process star (plugin) communication. + """ABP client: in-process star (plugin) communication. Stars register themselves; client delegates calls to registered instances. diff --git a/astrbot/_internal/abc/_acp/base_astrbot_acp_client.py b/astrbot/_internal/abc/_acp/base_astrbot_acp_client.py index 3085631e6..91f5207a0 100644 --- a/astrbot/_internal/abc/_acp/base_astrbot_acp_client.py +++ b/astrbot/_internal/abc/_acp/base_astrbot_acp_client.py @@ -1,5 +1,4 @@ -""" -ACP (AstrBot Communication Protocol) client. +"""ACP (AstrBot Communication Protocol) client. Transport: TCP | Unix Socket Messages: JSON with Content-Length header @@ -12,8 +11,7 @@ from typing import Any class BaseAstrbotAcpClient(ABC): - """ - ACP client: connects to ACP servers via TCP or Unix socket. + """ACP client: connects to ACP servers via TCP or Unix socket. Subclass must implement: - connect() -> None diff --git a/astrbot/_internal/abc/_acp/base_astrbot_acp_server.py b/astrbot/_internal/abc/_acp/base_astrbot_acp_server.py index 86ad51052..698ae1427 100644 --- a/astrbot/_internal/abc/_acp/base_astrbot_acp_server.py +++ b/astrbot/_internal/abc/_acp/base_astrbot_acp_server.py @@ -1,5 +1,4 @@ -""" -ACP (AstrBot Communication Protocol) server. +"""ACP (AstrBot Communication Protocol) server. Transport: TCP listening socket Messages: JSON with Content-Length header @@ -13,8 +12,7 @@ from typing import Any class BaseAstrbotAcpServer(ABC): - """ - ACP server: listens for client connections, exposes tools. + """ACP server: listens for client connections, exposes tools. Subclass must implement: - start(host, port) -> None diff --git a/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py b/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py index 6aa38aace..959725fb8 100644 --- a/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py +++ b/astrbot/_internal/abc/_lsp/base_astrbot_lsp_client.py @@ -1,5 +1,4 @@ -""" -LSP (Language Server Protocol) client. +"""LSP (Language Server Protocol) client. Transport: stdio subprocess Messages: JSON-RPC 2.0 with Content-Length header @@ -8,10 +7,7 @@ Messages: JSON-RPC 2.0 with Content-Length header from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - pass +from typing import Any class LspMessage: @@ -43,8 +39,7 @@ class LspNotification(LspMessage): class BaseAstrbotLspClient(ABC): - """ - LSP client: connects to LSP servers via stdio subprocess. + """LSP client: connects to LSP servers via stdio subprocess. Subclass must implement: - connect() -> None @@ -71,8 +66,7 @@ class BaseAstrbotLspClient(ABC): command: list[str], workspace_uri: str, ) -> None: - """ - Start LSP server subprocess and complete handshake. + """Start LSP server subprocess and complete handshake. Steps: 1. Spawn subprocess with stdin/stdout pipes @@ -88,12 +82,12 @@ class BaseAstrbotLspClient(ABC): method: str, params: dict[str, Any] | None = None, ) -> Any: - """ - Send JSON-RPC request and return result. + """Send JSON-RPC request and return result. Raises: RuntimeError: not connected Exception: server returned error + """ ... @@ -103,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). """ ... diff --git a/astrbot/_internal/abc/_mcp/base_astrbot_mcp_client.py b/astrbot/_internal/abc/_mcp/base_astrbot_mcp_client.py index 091f704aa..e68fa4426 100644 --- a/astrbot/_internal/abc/_mcp/base_astrbot_mcp_client.py +++ b/astrbot/_internal/abc/_mcp/base_astrbot_mcp_client.py @@ -1,5 +1,4 @@ -""" -MCP (Model Context Protocol) client. +"""MCP (Model Context Protocol) client. Transport: stdio | SSE | streamable_http Messages: JSON-RPC 2.0 @@ -8,10 +7,7 @@ Messages: JSON-RPC 2.0 from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Literal, TypedDict - -if TYPE_CHECKING: - pass +from typing import Any, Literal, TypedDict class McpServerConfig(TypedDict, total=False): @@ -38,8 +34,7 @@ class McpToolInfo(TypedDict): class BaseAstrbotMcpClient(ABC): - """ - MCP client: connects to MCP servers for external tools. + """MCP client: connects to MCP servers for external tools. Subclass must implement: - connect() -> None @@ -66,8 +61,7 @@ class BaseAstrbotMcpClient(ABC): config: McpServerConfig, name: str, ) -> None: - """ - Connect to MCP server. + """Connect to MCP server. Stdio: {"command": "python", "args": ["server.py"], "env": {...}} HTTP: {"url": "https://...", "transport": "sse"} diff --git a/astrbot/_internal/abc/base_astrbot_gateway.py b/astrbot/_internal/abc/base_astrbot_gateway.py index c67c498f9..990622b08 100644 --- a/astrbot/_internal/abc/base_astrbot_gateway.py +++ b/astrbot/_internal/abc/base_astrbot_gateway.py @@ -1,5 +1,4 @@ -""" -AstrBot Gateway - HTTP/WebSocket API server. +"""AstrBot Gateway - HTTP/WebSocket API server. Built on FastAPI, provides: - HTTP REST API (stats, inspector, config) @@ -14,8 +13,7 @@ from abc import ABC, abstractmethod class BaseAstrbotGateway(ABC): - """ - Gateway: HTTP/WebSocket server built on FastAPI. + """Gateway: HTTP/WebSocket server built on FastAPI. ┌─────────────────────────────────────────────────────────┐ │ FastAPI App │ @@ -56,8 +54,7 @@ class BaseAstrbotGateway(ABC): @abstractmethod async def serve(self) -> None: - """ - Start gateway server - blocks until shutdown. + """Start gateway server - blocks until shutdown. Should: 1. Create FastAPI app with routes @@ -69,5 +66,6 @@ class BaseAstrbotGateway(ABC): Raises: OSError: address already in use + """ ... diff --git a/astrbot/_internal/abc/base_astrbot_orchestrator.py b/astrbot/_internal/abc/base_astrbot_orchestrator.py index a60358f16..b297fbada 100644 --- a/astrbot/_internal/abc/base_astrbot_orchestrator.py +++ b/astrbot/_internal/abc/base_astrbot_orchestrator.py @@ -1,5 +1,4 @@ -""" -AstrBot Orchestrator - core runtime lifecycle manager. +"""AstrBot Orchestrator - core runtime lifecycle manager. Architecture ============ @@ -165,8 +164,7 @@ DEFAULT_SLEEP_INTERVAL: float = 5.0 class BaseAstrbotOrchestrator(ABC): - """ - Core runtime: owns lifecycle of all protocol clients and stars. + """Core runtime: owns lifecycle of all protocol clients and stars. ┌────────────────────────────────────────────────────────────┐ │ Protocol Clients (always present, never None after init) │ @@ -217,8 +215,7 @@ class BaseAstrbotOrchestrator(ABC): abp: AstrbotAbpClient def __init__(self) -> None: - """ - Initialize orchestrator and all protocol clients. + """Initialize orchestrator and all protocol clients. After __init__, all clients exist but are not connected. Call start() or run_loop() to begin operation. @@ -232,6 +229,7 @@ class BaseAstrbotOrchestrator(ABC): self.abp = AstrbotAbpClient() self._stars: dict[str, Any] = {} self._running = False + """ self._stars: dict[str, Any] = {} self._running: bool = False @@ -243,8 +241,7 @@ class BaseAstrbotOrchestrator(ABC): @abstractmethod async def start(self) -> None: - """ - Initialize all protocol clients. + """Initialize all protocol clients. Called once before run_loop(). Should: 1. Call lsp.connect() @@ -255,13 +252,13 @@ class BaseAstrbotOrchestrator(ABC): Raises: Exception: if any client fails to initialize + """ ... @abstractmethod async def run_loop(self) -> None: - """ - Main event loop - blocks until shutdown. + """Main event loop - blocks until shutdown. Execution: self._running = True @@ -286,13 +283,13 @@ class BaseAstrbotOrchestrator(ABC): Note: Subclass defines _heartbeat() for periodic tasks. This method only handles the loop control. + """ ... @abstractmethod async def register_star(self, name: str, star_instance: Any) -> None: - """ - Register a star (plugin) with the orchestrator. + """Register a star (plugin) with the orchestrator. Args: name: Unique identifier for the star @@ -304,13 +301,13 @@ class BaseAstrbotOrchestrator(ABC): Raises: ValueError: if name already registered + """ ... @abstractmethod async def unregister_star(self, name: str) -> None: - """ - Unregister a star (plugin) from the orchestrator. + """Unregister a star (plugin) from the orchestrator. Args: name: Identifier of star to remove @@ -321,6 +318,7 @@ class BaseAstrbotOrchestrator(ABC): Note: Idempotent - does nothing if name not found. + """ ... @@ -336,8 +334,7 @@ class BaseAstrbotOrchestrator(ABC): @abstractmethod async def shutdown(self) -> None: - """ - Graceful shutdown of orchestrator and all clients. + """Graceful shutdown of orchestrator and all clients. Execution order: 1. self._running = False (stop run_loop) diff --git a/astrbot/_internal/geteway/deps.py b/astrbot/_internal/geteway/deps.py index 73e648216..ecfd20b53 100644 --- a/astrbot/_internal/geteway/deps.py +++ b/astrbot/_internal/geteway/deps.py @@ -1,4 +1,3 @@ -""" -依赖注入 +"""依赖注入 """ diff --git a/astrbot/_internal/geteway/server.py b/astrbot/_internal/geteway/server.py index 5868a0029..ffde46e7b 100644 --- a/astrbot/_internal/geteway/server.py +++ b/astrbot/_internal/geteway/server.py @@ -1,5 +1,4 @@ -""" -AstrBot Gateway - FastAPI server for the dashboard backend. +"""AstrBot Gateway - FastAPI server for the dashboard backend. Provides REST API endpoints and WebSocket connections for the frontend dashboard. The gateway acts as the communication bridge between the dashboard and the orchestrator. @@ -32,8 +31,7 @@ log = logger class AstrbotGateway(BaseAstrbotGateway): - """ - FastAPI-based gateway server for AstrBot. + """FastAPI-based gateway server for AstrBot. Handles: - REST API endpoints for configuration and stats @@ -49,8 +47,7 @@ class AstrbotGateway(BaseAstrbotGateway): self._port = 8765 async def serve(self) -> None: - """ - Start the gateway server. + """Start the gateway server. Creates and runs a FastAPI application with WebSocket support. """ @@ -82,7 +79,7 @@ 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() @@ -140,16 +137,16 @@ 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. + """Handle an incoming WebSocket message. Args: message: Parsed JSON message from the client Returns: Response message to send back, or None for no response + """ msg_type = message.get("type") data = message.get("data", {}) @@ -176,7 +173,7 @@ 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 bcfd95a1b..0cabcd764 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 @@ -22,8 +21,7 @@ log = logger class WebSocketManager: - """ - Manages all active WebSocket connections. + """Manages all active WebSocket connections. Provides connection/disconnection handling and broadcast capabilities. """ @@ -46,12 +44,12 @@ class WebSocketManager: log.debug(f"WebSocket disconnected. Total: {len(self._connections)}") async def send_json(self, websocket: WebSocket, data: dict[str, Any]) -> None: - """ - Send JSON data to a specific WebSocket. + """Send JSON data to a specific WebSocket. Args: websocket: Target WebSocket connection data: Data to send (must be JSON-serializable) + """ try: await websocket.send_json(data) @@ -60,11 +58,11 @@ class WebSocketManager: await self.disconnect(websocket) async def broadcast(self, data: dict[str, Any]) -> None: - """ - Broadcast JSON data to all connected WebSockets. + """Broadcast JSON data to all connected WebSockets. Args: data: Data to broadcast (must be JSON-serializable) + """ async with self._lock: connections = list(self._connections) @@ -77,14 +75,14 @@ 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. + """Send a message to a specific WebSocket. Args: websocket: Target WebSocket connection message: Message to send (string or dict) + """ try: if isinstance(message, str): diff --git a/astrbot/_internal/protocols/_abp/client.py b/astrbot/_internal/protocols/_abp/client.py index a62c47157..911c369d5 100644 --- a/astrbot/_internal/protocols/_abp/client.py +++ b/astrbot/_internal/protocols/_abp/client.py @@ -1,5 +1,4 @@ -""" -ABP (AstrBot Protocol) client implementation. +"""ABP (AstrBot Protocol) client implementation. ABP is the built-in plugin protocol where the orchestrator acts as client connecting to internal stars (plugins) embedded in the runtime. @@ -16,8 +15,7 @@ log = logger class AstrbotAbpClient(BaseAstrbotAbpClient): - """ - ABP client for communicating with internal stars (built-in plugins). + """ABP client for communicating with internal stars (built-in plugins). The orchestrator acts as the client, sending requests to and receiving notifications from stars running within the same process. @@ -42,10 +40,9 @@ 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. + """Call a tool on a registered star. Args: star_name: Name of the star (plugin) @@ -54,6 +51,7 @@ class AstrbotAbpClient(BaseAstrbotAbpClient): Returns: Tool call result + """ if not self._connected: raise RuntimeError("ABP client is not connected") diff --git a/astrbot/_internal/protocols/_acp/client.py b/astrbot/_internal/protocols/_acp/client.py index c56da2e51..54966b660 100644 --- a/astrbot/_internal/protocols/_acp/client.py +++ b/astrbot/_internal/protocols/_acp/client.py @@ -1,5 +1,4 @@ -""" -ACP (AstrBot Communication Protocol) client implementation. +"""ACP (AstrBot Communication Protocol) client implementation. ACP is a client-server protocol for inter-service communication, similar to MCP but designed specifically for AstrBot's architecture. @@ -18,8 +17,7 @@ log = logger class AstrbotAcpClient(BaseAstrbotAcpClient): - """ - ACP client for communicating with ACP servers. + """ACP client for communicating with ACP servers. The orchestrator acts as an ACP client, connecting to external ACP-compatible services. @@ -40,8 +38,7 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): return self._connected async def connect(self) -> None: - """ - Connect to configured ACP servers. + """Connect to configured ACP servers. ACP servers can be accessed via TCP (host:port) or Unix socket. """ @@ -51,12 +48,12 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): log.info("ACP client initialized.") async def connect_to_server(self, host: str, port: int) -> None: - """ - Connect to an ACP server via TCP. + """Connect to an ACP server via TCP. Args: host: Server hostname or IP port: Server port + """ self._server_url = f"{host}:{port}" self._reader, self._writer = await asyncio.open_connection(host, port) @@ -68,11 +65,11 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): log.info(f"ACP client connected to {self._server_url}") async def connect_to_unix_socket(self, socket_path: str) -> None: - """ - Connect to an ACP server via Unix socket. + """Connect to an ACP server via Unix socket. Args: socket_path: Path to the Unix socket + """ self._server_url = f"unix://{socket_path}" self._reader, self._writer = await asyncio.open_unix_connection(socket_path) @@ -140,10 +137,9 @@ 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. + """Call a tool on an ACP server. Args: server_name: Name of the ACP server @@ -152,6 +148,7 @@ class AstrbotAcpClient(BaseAstrbotAcpClient): Returns: Tool call result + """ if not self._connected: raise RuntimeError("ACP client is not connected") @@ -184,7 +181,7 @@ 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 349c19dc0..dddb625e7 100644 --- a/astrbot/_internal/protocols/_acp/server.py +++ b/astrbot/_internal/protocols/_acp/server.py @@ -1,5 +1,4 @@ -""" -ACP (AstrBot Communication Protocol) server implementation. +"""ACP (AstrBot Communication Protocol) server implementation. ACP servers listen for connections from ACP clients and provide services/tools to the orchestrator. @@ -19,8 +18,7 @@ log = logger class AstrbotAcpServer(BaseAstrbotAcpServer): - """ - ACP server for accepting connections from ACP clients. + """ACP server for accepting connections from ACP clients. ACP servers expose tools/notifications that can be called by clients. """ @@ -35,36 +33,36 @@ class AstrbotAcpServer(BaseAstrbotAcpServer): self._notification_handlers: dict[str, Callable[..., Any]] = {} def register_tool(self, name: str, handler: Callable[..., Any]) -> None: - """ - Register a tool handler. + """Register a tool handler. Args: name: Tool name handler: Async callable that handles tool calls + """ self._tool_handlers[name] = handler 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. + """Register a notification handler. Args: name: Notification method name handler: Async callable that handles notifications + """ self._notification_handlers[name] = handler log.debug(f"ACP server registered notification handler: {name}") async def start(self, host: str = "127.0.0.1", port: int = 8765) -> None: - """ - Start the ACP server. + """Start the ACP server. Args: host: Host to bind to port: Port to listen on + """ self._host = host self._port = port @@ -77,7 +75,7 @@ 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") @@ -180,12 +178,12 @@ class AstrbotAcpServer(BaseAstrbotAcpServer): return response async def broadcast_notification(self, method: str, params: dict[str, Any]) -> None: - """ - Broadcast a notification to all connected clients. + """Broadcast a notification to all connected clients. Args: method: Notification method name params: Notification parameters + """ message = { "jsonrpc": "2.0", diff --git a/astrbot/_internal/protocols/_lsp/client.py b/astrbot/_internal/protocols/_lsp/client.py index 6a535a0fd..2b37b9044 100644 --- a/astrbot/_internal/protocols/_lsp/client.py +++ b/astrbot/_internal/protocols/_lsp/client.py @@ -1,5 +1,4 @@ -""" -LSP (Language Server Protocol) client implementation. +"""LSP (Language Server Protocol) client implementation. The orchestrator acts as an LSP client, connecting to LSP servers that provide language intelligence features (completions, diagnostics, etc.). @@ -20,8 +19,7 @@ log = logger class AstrbotLspClient(BaseAstrbotLspClient): - """ - LSP client for communicating with LSP servers. + """LSP client for communicating with LSP servers. Implements the Microsoft Language Server Protocol for connecting to external language intelligence services. @@ -44,8 +42,7 @@ class AstrbotLspClient(BaseAstrbotLspClient): return self._connected async def connect(self) -> None: - """ - Connect to configured LSP servers. + """Connect to configured LSP servers. LSP servers are typically stdio-based subprocesses. This method establishes the communication channel. @@ -57,12 +54,12 @@ class AstrbotLspClient(BaseAstrbotLspClient): log.info("LSP client initialized.") async def connect_to_server(self, command: list[str], workspace_uri: str) -> None: - """ - Connect to an LSP server subprocess. + """Connect to an LSP server subprocess. Args: command: Command line to start the LSP server (e.g., ["python", "lsp_server.py"]) workspace_uri: Root URI of the workspace to serve + """ log.debug(f"Starting LSP server: {' '.join(command)}") @@ -99,7 +96,7 @@ 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: @@ -138,7 +135,7 @@ 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 cad225a65..c379e63c9 100644 --- a/astrbot/_internal/protocols/_mcp/client.py +++ b/astrbot/_internal/protocols/_mcp/client.py @@ -30,13 +30,13 @@ try: from mcp.client.sse import sse_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency, MCP services will be unavailable." + "Warning: Missing 'mcp' dependency, MCP services will be unavailable.", ) try: from mcp.client.streamable_http import streamablehttp_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency or MCP library version too old, Streamable HTTP connection unavailable." + "Warning: Missing 'mcp' dependency or MCP library version too old, Streamable HTTP connection unavailable.", ) @@ -166,7 +166,7 @@ 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( @@ -236,7 +236,7 @@ class McpClient(BaseAstrbotMcpClient): sse_read_timeout=cfg.get("sse_read_timeout", 60 * 5), ) streams = await self.exit_stack.enter_async_context( - self._streams_context + self._streams_context, ) read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60)) self.session = await self.exit_stack.enter_async_context( @@ -244,12 +244,12 @@ class McpClient(BaseAstrbotMcpClient): *streams, read_timeout_seconds=read_timeout, logging_callback=logging_callback, - ) + ), ) else: timeout = timedelta(seconds=cfg.get("timeout", 30)) sse_read_timeout = timedelta( - seconds=cfg.get("sse_read_timeout", 60 * 5) + seconds=cfg.get("sse_read_timeout", 60 * 5), ) self._streams_context = streamablehttp_client( url=cfg["url"], @@ -259,7 +259,7 @@ class McpClient(BaseAstrbotMcpClient): terminate_on_close=cfg.get("terminate_on_close", True), ) read_s, write_s, _ = await self.exit_stack.enter_async_context( - self._streams_context + self._streams_context, ) read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60)) self.session = await self.exit_stack.enter_async_context( @@ -268,7 +268,7 @@ class McpClient(BaseAstrbotMcpClient): write_stream=write_s, read_timeout_seconds=read_timeout, logging_callback=logging_callback, - ) + ), ) else: cfg = _prepare_stdio_env(cfg) @@ -295,11 +295,11 @@ class McpClient(BaseAstrbotMcpClient): identifier=f"MCPServer-{name}", callback=callback, ), - ) + ), ) self.process_pid = self._extract_stdio_process_pid(stdio_transport) self.session = await self.exit_stack.enter_async_context( - mcp.ClientSession(*stdio_transport) + mcp.ClientSession(*stdio_transport), ) await self.session.initialize() @@ -318,11 +318,12 @@ class McpClient(BaseAstrbotMcpClient): Raises: Exception: raised when reconnection fails + """ async with self._reconnect_lock: if self._reconnecting: logger.debug( - f"MCP Client {self._server_name} is already reconnecting, skipping" + f"MCP Client {self._server_name} is already reconnecting, skipping", ) return if not self._mcp_server_config or not self._server_name: @@ -330,7 +331,7 @@ class McpClient(BaseAstrbotMcpClient): self._reconnecting = True try: logger.info( - f"Attempting to reconnect to MCP server {self._server_name}..." + f"Attempting to reconnect to MCP server {self._server_name}...", ) if self.exit_stack: self._old_exit_stacks.append(self.exit_stack) @@ -339,18 +340,18 @@ class McpClient(BaseAstrbotMcpClient): await self.connect_to_server(self._mcp_server_config, self._server_name) await self.list_tools_and_save() logger.info( - f"Successfully reconnected to MCP server {self._server_name}" + f"Successfully reconnected to MCP server {self._server_name}", ) except Exception as e: logger.error( - f"Failed to reconnect to MCP server {self._server_name}: {e}" + f"Failed to reconnect to MCP server {self._server_name}: {e}", ) raise finally: 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. @@ -365,6 +366,7 @@ class McpClient(BaseAstrbotMcpClient): Raises: ValueError: MCP session is not available anyio.ClosedResourceError: raised after reconnection failure + """ @retry( @@ -385,7 +387,7 @@ class McpClient(BaseAstrbotMcpClient): ) except anyio.ClosedResourceError: logger.warning( - f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect..." + f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect...", ) await self._reconnect() raise diff --git a/astrbot/_internal/runtime/orchestrator.py b/astrbot/_internal/runtime/orchestrator.py index 74da57185..e73b1d5bd 100644 --- a/astrbot/_internal/runtime/orchestrator.py +++ b/astrbot/_internal/runtime/orchestrator.py @@ -1,5 +1,4 @@ -""" -AstrBot Orchestrator - core runtime that coordinates all protocol clients. +"""AstrBot Orchestrator - core runtime that coordinates all protocol clients. The orchestrator manages the lifecycle of LSP, MCP, ACP, and ABP clients, and runs the main event loop that dispatches messages between components. @@ -23,8 +22,7 @@ log = logger class AstrbotOrchestrator(BaseAstrbotOrchestrator): - """ - Core runtime orchestrator for AstrBot. + """Core runtime orchestrator for AstrBot. Manages: - LSP client: Language Server Protocol for editor integrations @@ -54,8 +52,7 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): log.debug("AstrbotOrchestrator initialized.") async def start(self) -> None: - """ - Initialize all protocol clients. + """Initialize all protocol clients. Calls connect() on all protocol clients to prepare them for use. """ @@ -70,8 +67,7 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): log.info("AstrbotOrchestrator started.") async def run_loop(self) -> None: - """ - Main orchestrator event loop. + """Main orchestrator event loop. This loop runs continuously, handling: - Periodic health checks of protocol clients @@ -109,23 +105,23 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): log.info("AstrbotOrchestrator run loop stopped.") async def register_star(self, name: str, star_instance: Any) -> None: - """ - Register a star (plugin) with the orchestrator. + """Register a star (plugin) with the orchestrator. Args: name: Unique name for the star star_instance: Star plugin instance + """ self._stars[name] = star_instance self.abp.register_star(name, star_instance) log.info(f"Star '{name}' registered.") async def unregister_star(self, name: str) -> None: - """ - Unregister a star (plugin) from the orchestrator. + """Unregister a star (plugin) from the orchestrator. Args: name: Name of the star to unregister + """ self._stars.pop(name, None) self.abp.unregister_star(name) @@ -147,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 2e44bc8db..7cc9eb82b 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 eaa396ed4..e8bd0d89c 100644 --- a/astrbot/_internal/stars/runtime_status_star.py +++ b/astrbot/_internal/stars/runtime_status_star.py @@ -1,5 +1,4 @@ -""" -RuntimeStatusStar - ABP plugin that exposes core runtime internal state. +"""RuntimeStatusStar - ABP plugin that exposes core runtime internal state. This star provides tools for querying: - Runtime status (running state, uptime) @@ -18,8 +17,7 @@ from typing import Any @dataclass class RuntimeStatusStar: - """ - ABP star that exposes core runtime internal state as callable tools. + """ABP star that exposes core runtime internal state as callable tools. Tools provided: - get_runtime_status: Returns running state and uptime @@ -39,8 +37,7 @@ class RuntimeStatusStar: self._orchestrator = orchestrator async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: - """ - Handle tool calls from ABP client. + """Handle tool calls from ABP client. Args: tool_name: Name of the tool to call @@ -48,17 +45,17 @@ class RuntimeStatusStar: Returns: Tool result + """ if tool_name == "get_runtime_status": return self._get_runtime_status() - elif tool_name == "get_protocol_status": + if tool_name == "get_protocol_status": return await self._get_protocol_status() - elif tool_name == "get_star_registry": + if tool_name == "get_star_registry": return await self._get_star_registry() - elif tool_name == "get_stats": + if tool_name == "get_stats": return self._get_stats() - else: - raise ValueError(f"Unknown tool: {tool_name}") + raise ValueError(f"Unknown tool: {tool_name}") def _get_runtime_status(self) -> dict[str, Any]: """Get overall runtime state.""" @@ -120,7 +117,7 @@ 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 4eea09c63..ffc0021cc 100644 --- a/astrbot/_internal/tools/base.py +++ b/astrbot/_internal/tools/base.py @@ -36,7 +36,7 @@ class ToolSchema: import jsonschema jsonschema.validate( - self.parameters, jsonschema.Draft202012Validator.META_SCHEMA + self.parameters, jsonschema.Draft202012Validator.META_SCHEMA, ) return self @@ -75,13 +75,12 @@ class FunctionTool(ToolSchema): async def call(self, **kwargs: Any) -> Any: """Run the tool with the given arguments. The handler field has priority.""" raise NotImplementedError( - "FunctionTool.call() must be implemented by subclasses or set a handler." + "FunctionTool.call() must be implemented by subclasses or set a handler.", ) class ToolSet: - """ - A collection of FunctionTools grouped under a namespace. + """A collection of FunctionTools grouped under a namespace. ToolSets allow organizing related tools together. The LLM sees tools as "namespace/tool_name" when calling. @@ -166,7 +165,7 @@ class ToolSet: description=tool.description, parameters={"type": "object", "properties": {}}, handler=None, - ) + ), ) return ToolSet("default", light_tools) @@ -187,7 +186,7 @@ class ToolSet: description="", parameters=params, handler=None, - ) + ), ) return ToolSet("default", param_tools) @@ -197,7 +196,7 @@ 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]] = [] @@ -266,7 +265,7 @@ 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/builtin.py b/astrbot/_internal/tools/builtin.py index c2d823a9a..791dd94af 100644 --- a/astrbot/_internal/tools/builtin.py +++ b/astrbot/_internal/tools/builtin.py @@ -1,5 +1,4 @@ -""" -Builtin tools for AstrBot - re-exports from core.tools for backward compatibility. +"""Builtin tools for AstrBot - re-exports from core.tools for backward compatibility. This module re-exports the builtin tools (cron, send_message, kb_query) from the deprecated core.tools module for backward compatibility. diff --git a/astrbot/_internal/tools/registry.py b/astrbot/_internal/tools/registry.py index 682d807e6..8ad212900 100644 --- a/astrbot/_internal/tools/registry.py +++ b/astrbot/_internal/tools/registry.py @@ -121,30 +121,26 @@ 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).""" - pass 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).""" - pass async def init_mcp_clients(self) -> None: """Initialize MCP clients (stub).""" - pass 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" async def sync_modelscope_mcp_servers(self, access_token: str = "") -> None: """Sync ModelScope MCP servers (stub).""" - pass def load_mcp_config(self) -> dict[str, Any]: """Load MCP configuration (stub).""" @@ -201,7 +197,6 @@ class FuncCall(FunctionToolManager): async def init_mcp_clients(self) -> None: """Initialize MCP clients (stub implementation).""" - pass def add_func( self, @@ -271,23 +266,21 @@ 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).""" - pass 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).""" - pass def load_mcp_config(self) -> dict[str, Any]: """Load MCP configuration (stub implementation).""" @@ -298,7 +291,7 @@ 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 @@ -316,7 +309,6 @@ class FuncCall(FunctionToolManager): async def sync_modelscope_mcp_servers(self, access_token: str = "") -> None: """Sync ModelScope MCP servers (stub implementation).""" - pass def get_full_tool_set(self) -> ToolSet: """Return a ToolSet with all active tools.""" diff --git a/astrbot/api/__init__.py b/astrbot/api/__init__.py index f6eae3b62..e9f27c690 100644 --- a/astrbot/api/__init__.py +++ b/astrbot/api/__init__.py @@ -1,5 +1,4 @@ -""" -AstrBot Public API. +"""AstrBot Public API. This package exposes the public interface for extending and integrating with AstrBot. All exports from this module are guaranteed to be stable across diff --git a/astrbot/api/mcp.py b/astrbot/api/mcp.py index 190a0dd18..39265636a 100644 --- a/astrbot/api/mcp.py +++ b/astrbot/api/mcp.py @@ -1,5 +1,4 @@ -""" -MCP (Model Context Protocol) Public API for AstrBot. +"""MCP (Model Context Protocol) Public API for AstrBot. This module provides a simple, stable interface for MCP server management, delegating to the _internal package. @@ -23,6 +22,7 @@ Example: url="http://localhost:8080/sse", transport="sse", ) + """ from __future__ import annotations @@ -71,6 +71,7 @@ async def register_mcp_server( Example - Stdio: await register_mcp_server(name="weather", command="uv", args=["tool", "run", "weather-mcp"]) + """ from astrbot.core.provider.register import llm_tools as func_tool_manager diff --git a/astrbot/api/skills.py b/astrbot/api/skills.py index a74e584b7..60d0a17c5 100644 --- a/astrbot/api/skills.py +++ b/astrbot/api/skills.py @@ -1,5 +1,4 @@ -""" -Skills Public API for AstrBot. +"""Skills Public API for AstrBot. This module provides a simple, stable interface for skill management, delegating to the _internal package. @@ -19,6 +18,7 @@ Example: tool_skills = [s for s in skills if s.input_schema] if tool_skills: func_tool = skill_to_tool(tool_skills[0]) + """ from __future__ import annotations @@ -45,6 +45,7 @@ def skill_to_tool(skill: SkillInfo) -> FunctionTool | None: Returns: A FunctionTool, or None if the skill has no input_schema + """ if not skill.input_schema: return None diff --git a/astrbot/api/tools.py b/astrbot/api/tools.py index 856e2833b..b7ba2808d 100644 --- a/astrbot/api/tools.py +++ b/astrbot/api/tools.py @@ -1,5 +1,4 @@ -""" -Tools Public API for AstrBot. +"""Tools Public API for AstrBot. This module provides a simple, stable interface for tool registration and management. All implementations are delegated to the _internal package. @@ -13,6 +12,7 @@ Example: registry = get_registry() tools = registry.list_tools() + """ from __future__ import annotations @@ -28,10 +28,10 @@ from astrbot._internal.tools.registry import FunctionToolManager __all__ = [ "FunctionTool", "ToolRegistry", + "ToolSchema", "ToolSet", "get_registry", "tool", - "ToolSchema", ] @@ -88,7 +88,7 @@ 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. @@ -101,6 +101,7 @@ def tool( @tool(name="weather", description="Get weather for a city", parameters={...}) async def get_weather(city: str) -> str: return f"The weather in {city} is sunny" + """ if parameters is None: parameters = {"type": "object", "properties": {}} diff --git a/astrbot/builtin_stars/astrbot/long_term_memory.py b/astrbot/builtin_stars/astrbot/long_term_memory.py index e271bc741..9e88a7f6c 100644 --- a/astrbot/builtin_stars/astrbot/long_term_memory.py +++ b/astrbot/builtin_stars/astrbot/long_term_memory.py @@ -32,10 +32,10 @@ class LongTermMemory: max_cnt = 300 image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"] image_caption_provider_id = cfg["provider_ltm_settings"].get( - "image_caption_provider_id" + "image_caption_provider_id", ) image_caption = cfg["provider_ltm_settings"]["image_caption"] and bool( - image_caption_provider_id + image_caption_provider_id, ) active_reply = cfg["provider_ltm_settings"]["active_reply"] enable_active_reply = active_reply.get("enable", False) @@ -172,7 +172,7 @@ class LongTermMemory: req.system_prompt += chats_str async def after_req_llm( - self, event: AstrMessageEvent, llm_resp: LLMResponse + self, event: AstrMessageEvent, llm_resp: LLMResponse, ) -> None: if event.unified_msg_origin not in self.session_chats: return @@ -180,7 +180,7 @@ class LongTermMemory: if llm_resp.completion_text: final_message = f"[You/{datetime.datetime.now().strftime('%H:%M:%S')}]: {llm_resp.completion_text}" logger.debug( - f"Recorded AI response: {event.unified_msg_origin} | {final_message}" + f"Recorded AI response: {event.unified_msg_origin} | {final_message}", ) self.session_chats[event.unified_msg_origin].append(final_message) cfg = self.cfg(event) diff --git a/astrbot/builtin_stars/astrbot/main.py b/astrbot/builtin_stars/astrbot/main.py index 50b3d0686..789375eb5 100644 --- a/astrbot/builtin_stars/astrbot/main.py +++ b/astrbot/builtin_stars/astrbot/main.py @@ -86,7 +86,7 @@ class Main(star.Star): @filter.on_llm_request() async def decorate_llm_req( - self, event: AstrMessageEvent, req: ProviderRequest + self, event: AstrMessageEvent, req: ProviderRequest, ) -> None: """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt""" if self.ltm and self.ltm_enabled(event): @@ -97,7 +97,7 @@ class Main(star.Star): @filter.on_llm_response() async def record_llm_resp_to_ltm( - self, event: AstrMessageEvent, resp: LLMResponse + self, event: AstrMessageEvent, resp: LLMResponse, ) -> None: """在 LLM 响应后记录对话""" if self.ltm and self.ltm_enabled(event): diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index ad69b8cb5..76cfd2039 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -59,12 +59,12 @@ class ConversationCommands: async def _get_current_persona_id(self, session_id): curr = await self.context.conversation_manager.get_curr_conversation_id( - session_id + session_id, ) if not curr: return None conv = await self.context.conversation_manager.get_conversation( - session_id, curr + session_id, curr, ) if not conv: return None @@ -78,18 +78,18 @@ class ConversationCommands: is_group = bool(message.get_group_id()) scene = RstScene.get_scene(is_group, is_unique_session) alter_cmd_cfg = _normalize_alter_cmd_config( - await sp.get_async("global", "global", "alter_cmd", {}) + await sp.get_async("global", "global", "alter_cmd", {}), ) 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( MessageEventResult().message( - f"在{scene.name}场景下,reset命令需要管理员权限,您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。" - ) + f"在{scene.name}场景下,reset命令需要管理员权限,您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。", + ), ) return agent_runner_type = cfg["provider_settings"]["agent_runner_type"] @@ -104,15 +104,15 @@ class ConversationCommands: return if not self.context.get_using_provider(umo): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。") + MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), ) return cid = await self.context.conversation_manager.get_curr_conversation_id(umo) if not cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 切换或者 /new 创建。" - ) + "当前未处于对话状态,请 /switch 切换或者 /new 创建。", + ), ) return active_event_registry.stop_all(umo, exclude=message) @@ -130,13 +130,13 @@ 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( MessageEventResult().message( - f"已请求停止 {stopped_count} 个运行中的任务。" - ) + f"已请求停止 {stopped_count} 个运行中的任务。", + ), ) return message.set_result(MessageEventResult().message("当前会话没有运行中的任务。")) @@ -145,7 +145,7 @@ class ConversationCommands: """查看对话记录""" if not self.context.get_using_provider(message.unified_msg_origin): message.set_result( - MessageEventResult().message("未找到任何 LLM 提供商。请先配置。") + MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"), ) return size_per_page = 6 @@ -154,10 +154,10 @@ 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: @@ -175,14 +175,14 @@ class ConversationCommands: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: message.set_result( MessageEventResult().message( - f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。" - ) + f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。", + ), ) return size_per_page = 6 "获取所有对话列表" conversations_all = await self.context.conversation_manager.get_conversations( - message.unified_msg_origin + message.unified_msg_origin, ) "计算总页数" total_pages = (len(conversations_all) + size_per_page - 1) // size_per_page @@ -225,13 +225,13 @@ class ConversationCommands: persona_name = f"{persona_name} (自定义规则)" title = _titles.get(conv.cid, "新对话") parts.append( - f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n" + f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n", ) global_index += 1 parts.append("---\n") ret = "".join(parts) curr_cid = await self.context.conversation_manager.get_curr_conversation_id( - message.unified_msg_origin + message.unified_msg_origin, ) if curr_cid: "从所有对话的标题字典中获取标题" @@ -266,11 +266,11 @@ 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( - MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。") + MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), ) async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: @@ -281,55 +281,55 @@ class ConversationCommands: platform_name=message.platform_meta.id, message_type=MessageType("GroupMessage"), session_id=sid, - ) + ), ) 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( - f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。" - ) + f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。", + ), ) else: message.set_result( - MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。") + MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"), ) async def switch_conv( - self, message: AstrMessageEvent, index: int | None = None + self, message: AstrMessageEvent, index: int | None = None, ) -> None: """通过 /ls 前面的序号切换对话""" if not isinstance(index, int): message.set_result( - MessageEventResult().message("类型错误,请输入数字对话序号。") + MessageEventResult().message("类型错误,请输入数字对话序号。"), ) return if index is None: message.set_result( MessageEventResult().message( - "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话" - ) + "请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话", + ), ) return conversations = await self.context.conversation_manager.get_conversations( - message.unified_msg_origin + message.unified_msg_origin, ) if index > len(conversations) or index < 1: message.set_result( - MessageEventResult().message("对话序号错误,请使用 /ls 查看") + MessageEventResult().message("对话序号错误,请使用 /ls 查看"), ) else: conversation = conversations[index - 1] title = conversation.title if conversation.title else "新对话" await self.context.conversation_manager.switch_conversation( - message.unified_msg_origin, conversation.cid + message.unified_msg_origin, conversation.cid, ) message.set_result( MessageEventResult().message( - f"切换到对话: {title}({conversation.cid[:4]})。" - ) + f"切换到对话: {title}({conversation.cid[:4]})。", + ), ) async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None: @@ -338,7 +338,7 @@ 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("重命名对话成功。")) @@ -354,8 +354,8 @@ class ConversationCommands: ): message.set_result( MessageEventResult().message( - f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。" - ) + f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。", + ), ) return agent_runner_type = cfg["provider_settings"]["agent_runner_type"] @@ -374,13 +374,13 @@ class ConversationCommands: if not session_curr_cid: message.set_result( MessageEventResult().message( - "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。" - ) + "当前未处于对话状态,请 /switch 序号 切换或 /new 创建。", + ), ) 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 b2b3283fc..ca6111723 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/help.py +++ b/astrbot/builtin_stars/builtin_commands/commands/help.py @@ -23,8 +23,7 @@ class HelpCommand: return "" async def _build_reserved_command_lines(self) -> list[str]: - """ - 使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。 + """使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。 """ try: commands = await command_management.list_commands() diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index b0d7ecd31..d051914d0 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -45,7 +45,7 @@ class PersonaCommands: children, all_personas, depth + 1, - ) + ), ) return lines diff --git a/astrbot/builtin_stars/builtin_commands/commands/provider.py b/astrbot/builtin_stars/builtin_commands/commands/provider.py index 4943462d4..375f565e5 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/provider.py +++ b/astrbot/builtin_stars/builtin_commands/commands/provider.py @@ -64,7 +64,7 @@ class _ModelCache: return models def set( - self, provider_id: str, umo: str | None, models: list[str], ttl: float + self, provider_id: str, umo: str | None, models: list[str], ttl: float, ) -> None: if ttl <= 0: return @@ -83,7 +83,7 @@ class _ModelCache: self._store.pop(key, None) def invalidate( - self, provider_id: str | None = None, *, umo: str | None = None + self, provider_id: str | None = None, *, umo: str | None = None, ) -> None: if provider_id is None: self._store.clear() @@ -122,7 +122,7 @@ class ProviderCommands: register_change_hook(self._on_provider_manager_changed) def invalidate_provider_models_cache( - self, provider_id: str | None = None, *, umo: str | None = None + self, provider_id: str | None = None, *, umo: str | None = None, ) -> None: """Public hook for cache invalidation on external provider config changes.""" self._model_cache.invalidate(provider_id, umo=umo) @@ -214,14 +214,14 @@ class ProviderCommands: for candidate in models: cand_norm = candidate.casefold() if cand_norm.endswith(f"/{requested_norm}") or cand_norm.endswith( - f":{requested_norm}" + f":{requested_norm}", ): return candidate return None def _apply_model( - self, prov: Provider, model_name: str, *, umo: str | None = None + self, prov: Provider, model_name: str, *, umo: str | None = None, ) -> str: prov.set_model(model_name) self.invalidate_provider_models_cache(prov.meta().id, umo=umo) @@ -292,7 +292,7 @@ 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 +305,7 @@ class ProviderCommands: err_code = "TEST_FAILED" err_reason = safe_error("", e) self._log_reachability_failure( - provider, provider_capability_type, err_code, err_reason + provider, provider_capability_type, err_code, err_reason, ) return False, err_code, err_reason @@ -357,7 +357,7 @@ class ProviderCommands: return provider, None, err results = await asyncio.gather( - *(fetch_models(provider) for provider in all_providers) + *(fetch_models(provider) for provider in all_providers), ) failed_provider_errors: list[tuple[str, str]] = [] for provider, models, err in results: @@ -425,8 +425,8 @@ class ProviderCommands: if all_providers: await event.send( MessageEventResult().message( - "正在进行提供商可达性测试,请稍候..." - ) + "正在进行提供商可达性测试,请稍候...", + ), ) check_results: list[ReachabilityCheckResult] = await asyncio.gather( *[self._test_provider_capability(p) for p, _ in all_providers], @@ -484,7 +484,7 @@ class ProviderCommands: "info": info, "mark": mark, "provider": p, - } + }, ) # 分组输出 @@ -579,7 +579,7 @@ class ProviderCommands: event.set_result(MessageEventResult().message("无效的参数。")) async def _switch_model_by_name( - self, message: AstrMessageEvent, model_name: str, prov: Provider + self, message: AstrMessageEvent, model_name: str, prov: Provider, ) -> None: model_name = model_name.strip() if not model_name: @@ -604,7 +604,7 @@ class ProviderCommands: if matched_model_name is not None: message.set_result( MessageEventResult().message( - self._apply_model(prov, matched_model_name, umo=umo) + self._apply_model(prov, matched_model_name, umo=umo), ), ) return @@ -641,7 +641,7 @@ class ProviderCommands: except Exception as e: message.set_result( MessageEventResult().message( - safe_error("跨提供商切换并设置模型失败: ", e) + safe_error("跨提供商切换并设置模型失败: ", e), ), ) @@ -676,7 +676,7 @@ class ProviderCommands: curr_model = prov.get_model() or "无" parts.append(f"\n当前模型: [{curr_model}]") parts.append( - "\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。" + "\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。", ) ret = "".join(parts) @@ -701,13 +701,13 @@ class ProviderCommands: prov, new_model, umo=message.unified_msg_origin, - ) + ), ), ) except Exception as e: message.set_result( MessageEventResult().message( - safe_error("切换模型未知错误: ", e) + safe_error("切换模型未知错误: ", e), ), ) return @@ -751,7 +751,7 @@ class ProviderCommands: except Exception as e: message.set_result( MessageEventResult().message( - safe_error("切换 Key 未知错误: ", e) + safe_error("切换 Key 未知错误: ", e), ), ) return diff --git a/astrbot/builtin_stars/builtin_commands/commands/setunset.py b/astrbot/builtin_stars/builtin_commands/commands/setunset.py index 424393664..f7174ad5c 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/setunset.py +++ b/astrbot/builtin_stars/builtin_commands/commands/setunset.py @@ -20,7 +20,7 @@ class SetUnsetCommands: """设置会话变量""" uid = event.unified_msg_origin session_var = _normalize_session_variables( - await sp.session_get(uid, "session_variables", {}) + await sp.session_get(uid, "session_variables", {}), ) session_var[key] = value await sp.session_put(uid, "session_variables", session_var) @@ -35,7 +35,7 @@ class SetUnsetCommands: """移除会话变量""" uid = event.unified_msg_origin session_var = _normalize_session_variables( - await sp.session_get(uid, "session_variables", {}) + await sp.session_get(uid, "session_variables", {}), ) if key not in session_var: diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index a6c2b390c..abd69f28f 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -170,7 +170,7 @@ class Main(star.Star): @filter.command("switch") async def switch_conv( - self, message: AstrMessageEvent, index: int | None = None + self, message: AstrMessageEvent, index: int | None = None, ) -> None: """通过 /ls 前面的序号切换对话""" await self.conversation_c.switch_conv(message, index) diff --git a/astrbot/builtin_stars/web_searcher/engines/comet.py b/astrbot/builtin_stars/web_searcher/engines/comet.py index 642db7bd9..af42bff2b 100644 --- a/astrbot/builtin_stars/web_searcher/engines/comet.py +++ b/astrbot/builtin_stars/web_searcher/engines/comet.py @@ -11,6 +11,7 @@ class Comet(SearchEngine): Note: - This endpoint is often protected by anti-bot challenges. - We intentionally treat failures as non-fatal and rely on fallback engines. + """ NAME = "comet" diff --git a/astrbot/builtin_stars/web_searcher/main.py b/astrbot/builtin_stars/web_searcher/main.py index fcf3d7df2..d09b13669 100644 --- a/astrbot/builtin_stars/web_searcher/main.py +++ b/astrbot/builtin_stars/web_searcher/main.py @@ -379,7 +379,7 @@ class Main(star.Star): "snippet": f"{result.snippet}", # TODO: do not need ref for non-webchat platform adapter "index": index, - } + }, ) if result.favicon: sp.temporary_cache["_ws_favicon"][result.url] = result.favicon @@ -481,8 +481,7 @@ class Main(star.Star): exclude: str = "", count: int = 10, ) -> str: - """ - A web search tool based on Bocha Search API, used to retrieve web pages + """A web search tool based on Bocha Search API, used to retrieve web pages related to the user's query. Args: @@ -511,14 +510,16 @@ class Main(star.Star): include (string): Optional. Specifies the domains to include in the search. Multiple domains can be separated by "|" or ",". A maximum of 100 domains is allowed. - Examples: + + Examples: - "qq.com" - "qq.com|m.163.com" exclude (string): Optional. Specifies the domains to exclude from the search. Multiple domains can be separated by "|" or ",". A maximum of 100 domains is allowed. - Examples: + + Examples: - "qq.com" - "qq.com|m.163.com" @@ -527,6 +528,7 @@ class Main(star.Star): - Default: 10 The actual number of returned results may be less than the specified count. + """ logger.info(f"web_searcher - search_from_bocha: {query}") cfg = self.context.get_config(umo=event.unified_msg_origin) @@ -569,7 +571,7 @@ class Main(star.Star): "url": f"{result.url}", "snippet": f"{result.snippet}", "index": index, - } + }, ) if result.favicon: sp.temporary_cache["_ws_favicon"][result.url] = result.favicon @@ -592,7 +594,7 @@ class Main(star.Star): DEFAULT_WEB_SEARCH_PROVIDER, ) branch_provider, is_known_provider = normalize_websearch_provider_for_tools( - raw_provider + raw_provider, ) tool_set = req.func_tool diff --git a/astrbot/builtin_stars/web_searcher/provider_routing.py b/astrbot/builtin_stars/web_searcher/provider_routing.py index 744cf43f2..d2e704ce2 100644 --- a/astrbot/builtin_stars/web_searcher/provider_routing.py +++ b/astrbot/builtin_stars/web_searcher/provider_routing.py @@ -54,7 +54,7 @@ _WEB_SEARCH_PROVIDER_ALIASES.update( "bochaai": "bocha", # ZeroClaw compatibility: AstrBot has no Brave provider yet, so downgrade to default. "brave": DEFAULT_WEB_SEARCH_PROVIDER, - } + }, ) diff --git a/astrbot/cli/__main__.py b/astrbot/cli/__main__.py index 9245226fe..97dfb5275 100644 --- a/astrbot/cli/__main__.py +++ b/astrbot/cli/__main__.py @@ -34,7 +34,7 @@ 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 @@ -111,7 +111,7 @@ 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 c47945e5c..60ece0918 100644 --- a/astrbot/cli/commands/cmd_bk.py +++ b/astrbot/cli/commands/cmd_bk.py @@ -47,13 +47,12 @@ async def _get_kb_manager(): @click.group(name="bk") def bk(): """Backup management (Export/Import)""" - pass @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", @@ -62,7 +61,7 @@ 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", @@ -83,7 +82,6 @@ def export_data( and saved with a .gpg extension. Examples: - \b 1. Standard Export: astrbot bk export @@ -113,8 +111,8 @@ def export_data( 5. Signed and Encrypted with Digest: astrbot bk export -S -E "bob@example.com" -d sha256 -> Signs, encrypts for Bob, and generates a SHA256 checksum file. - """ + """ # Handle case where -E consumes the next flag (e.g. -E -S) if gpg_encrypt and gpg_encrypt.startswith("-"): consumed_flag = gpg_encrypt @@ -122,7 +120,7 @@ def export_data( click.style( f"Warning: Flag '{consumed_flag}' was interpreted as the recipient for -E.", fg="yellow", - ) + ), ) # Recover flags @@ -140,7 +138,7 @@ def export_data( if gpg_sign or gpg_encrypt or gpg_symmetric: if not shutil.which("gpg"): raise click.ClickException( - "GPG tool not found. Please install GnuPG to use encryption/signing features." + "GPG tool not found. Please install GnuPG to use encryption/signing features.", ) exporter = AstrBotExporter(db_helper) @@ -152,7 +150,7 @@ def export_data( path_str = await exporter.export_all(output, progress_callback=on_progress) final_path = Path(path_str) click.echo( - click.style(f"\nRaw backup exported to: {final_path}", fg="green") + click.style(f"\nRaw backup exported to: {final_path}", fg="green"), ) # GPG Operations @@ -168,7 +166,7 @@ def export_data( click.style( "Warning: Symmetric encryption selected, ignoring asymmetric recipient.", fg="yellow", - ) + ), ) cmd.append("--symmetric") # No --batch to allow interactive passphrase entry on TTY @@ -196,7 +194,7 @@ def export_data( await anyio.Path(final_path).unlink() final_path = gpg_output click.echo( - click.style(f"Processed backup created: {final_path}", fg="green") + click.style(f"Processed backup created: {final_path}", fg="green"), ) # Digest Generation @@ -211,7 +209,7 @@ 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")) @@ -266,13 +264,13 @@ def import_data_command(backup_file: str, yes: bool): if calculated_digest == expected_digest: click.echo( - click.style("Digest verification PASSED.", fg="green") + click.style("Digest verification PASSED.", fg="green"), ) 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}") click.echo(f" Actual: {calculated_digest}") @@ -286,7 +284,7 @@ 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: @@ -294,7 +292,7 @@ def import_data_command(backup_file: str, yes: bool): click.style( "Warning: Digest verification failed. Continuing due to --yes.", fg="yellow", - ) + ), ) if not yes: @@ -312,7 +310,7 @@ def import_data_command(backup_file: str, yes: bool): if backup_path.suffix == ".gpg": if not shutil.which("gpg"): raise click.ClickException( - "GPG tool not found. Cannot decrypt .gpg file." + "GPG tool not found. Cannot decrypt .gpg file.", ) # Remove .gpg extension for output @@ -356,12 +354,12 @@ 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 dcf48e41f..c3ca2c6fb 100644 --- a/astrbot/cli/commands/cmd_conf.py +++ b/astrbot/cli/commands/cmd_conf.py @@ -1,5 +1,4 @@ -""" -Configuration CLI for AstrBot. +"""Configuration CLI for AstrBot. This module provides: - secure hashing utilities for the dashboard password (argon2) @@ -32,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 @@ -106,14 +104,13 @@ CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = { def _load_config() -> dict[str, Any]: - """ - Load or initialize the CLI config file (data/cmd_config.json). + """Load or initialize the CLI config file (data/cmd_config.json). Ensures the astrbot root is valid before proceeding. """ root = astrbot_paths.root if not astrbot_paths.is_root: raise click.ClickException( - f"{root} is not a valid AstrBot root directory. Use 'astrbot init' to initialize" + f"{root} is not a valid AstrBot root directory. Use 'astrbot init' to initialize", ) config_path = astrbot_paths.data / "cmd_config.json" @@ -133,7 +130,7 @@ 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", ) @@ -149,7 +146,7 @@ def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None: cur[part] = {} elif not isinstance(cur[part], dict): raise click.ClickException( - f"Config path conflict: {'.'.join(parts[: parts.index(part) + 1])} is not a dict" + f"Config path conflict: {'.'.join(parts[: parts.index(part) + 1])} is not a dict", ) cur = cur[part] cur[parts[-1]] = value @@ -179,7 +176,7 @@ 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): @@ -189,7 +186,7 @@ def set_dashboard_credentials( raise click.ClickException( "Storing legacy dashboard password hashes is no longer supported. " "Please provide the plaintext password (it will be hashed securely), " - "or provide an Argon2-encoded hash string." + "or provide an Argon2-encoded hash string.", ) _set_nested_item( config, @@ -200,8 +197,7 @@ def set_dashboard_credentials( @click.group(name="conf") def conf() -> None: - """ - Configuration management commands. + """Configuration management commands. Supported config keys: - timezone @@ -211,7 +207,6 @@ def conf() -> None: - dashboard.password - callback_api_base """ - pass @conf.command(name="set") @@ -286,7 +281,7 @@ def _check_astrbot_not_running() -> None: except Timeout: raise click.ClickException( "AstrBot is currently running. " - "Please stop it first before changing the password via CLI." + "Please stop it first before changing the password via CLI.", ) from None else: lock.release() @@ -301,8 +296,7 @@ def _check_astrbot_not_running() -> None: help="Set admain password directly without interactive prompt", ) def set_dashboard_password(username: str | None, password: str | None) -> None: - """ - Interactively set dashboard password (with confirmation) or set directly with -p. + """Interactively set dashboard password (with confirmation) or set directly with -p. Acceptable inputs: - Plaintext password (recommended): it will be hashed securely before storage. @@ -319,7 +313,7 @@ def set_dashboard_password(username: str | None, password: str | None) -> None: raise click.ClickException( "Providing legacy dashboard password hashes is no longer supported. " "Please supply the plaintext password (it will be hashed securely), " - "or provide an Argon2-encoded hash string." + "or provide an Argon2-encoded hash string.", ) password_hash = _validate_dashboard_password(password) else: diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index ed1f17876..dd1c19683 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -42,7 +42,7 @@ async def initialize_astrbot( for name, path in paths.items(): path.mkdir(parents=True, exist_ok=True) click.echo( - f"{('Created' if not path.exists() else f'{name} Directory exists')}: {path}" + f"{('Created' if not path.exists() else f'{name} Directory exists')}: {path}", ) config_path = astrbot_root / "data" / "cmd_config.json" if not config_path.exists(): @@ -87,7 +87,7 @@ async def initialize_astrbot( click.echo("No config.template found; skipping .env generation") if admin_password is not None: raise click.ClickException( - "--admin-password is no longer supported during init. Run 'astrbot conf admin' after initialization." + "--admin-password is no longer supported during init. Run 'astrbot conf admin' after initialization.", ) effective_admin_username = ( admin_username.strip() @@ -97,19 +97,19 @@ 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( - "Dashboard password is not initialized for interactive use. Run 'astrbot conf admin' before the first login." + "Dashboard password is not initialized for interactive use. Run 'astrbot conf admin' before the first login.", ) if not backend_only and ( yes or click.confirm( - "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", default=True + "是否需要集成式 WebUI?(个人电脑推荐,服务器不推荐)", default=True, ) ): await DashboardManager().ensure_installed(astrbot_root) @@ -164,19 +164,19 @@ def init( backend_only=backend_only, admin_username=admin_username, admin_password=admin_password, - ) + ), ) if backup: from .cmd_bk import import_data_command 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: raise click.ClickException( - "Cannot acquire lock file. Please check if another instance is running" + "Cannot acquire lock file. Please check if another instance is running", ) except Exception as e: raise click.ClickException(f"Initialization failed: {e!s}") diff --git a/astrbot/cli/commands/cmd_plug.py b/astrbot/cli/commands/cmd_plug.py index 765f8bd73..ec5b6668d 100644 --- a/astrbot/cli/commands/cmd_plug.py +++ b/astrbot/cli/commands/cmd_plug.py @@ -22,7 +22,7 @@ def display_plugins(plugins, title=None, color=None) -> None: click.echo(click.style(title, fg=color, bold=True)) click.echo( - f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}" + f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}", ) click.echo("-" * 85) @@ -75,7 +75,7 @@ def new(name: str) -> None: # Rewrite README.md with open(plug_path / "README.md", "w", encoding="utf-8") as f: f.write( - f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://astrbot.app)\n" + f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://astrbot.app)\n", ) # Rewrite main.py @@ -183,7 +183,7 @@ def remove(name: str) -> None: click.echo(t("plugin_uninstall_success", name=name)) except Exception as e: raise click.ClickException( - t("plugin_uninstall_failed_ex", name=name, error=str(e)) + t("plugin_uninstall_failed_ex", name=name, error=str(e)), ) @@ -210,7 +210,7 @@ def update(name: str, proxy: str | None) -> None: if not plugin: raise click.ClickException( - f"Plugin {name} does not need updating or cannot be updated" + f"Plugin {name} does not need updating or cannot be updated", ) manage_plugin(plugin, plug_path, is_update=True, proxy=proxy) diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index af4b66b31..21271deef 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -66,7 +66,7 @@ _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. @@ -302,7 +302,7 @@ def run( click.echo(f" {click.style(key, fg='cyan')}: {val}") if svc_path: click.echo( - f" {click.style('SERVICE_CONFIG', fg='cyan')}: {svc_path!s}" + f" {click.style('SERVICE_CONFIG', fg='cyan')}: {svc_path!s}", ) click.echo("") @@ -338,7 +338,7 @@ 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") @@ -385,10 +385,10 @@ def run( click.echo("AstrBot has been shut down.") except Timeout: raise click.ClickException( - "Cannot acquire lock file. Please check if another instance is running" + "Cannot acquire lock file. Please check if another instance is running", ) from None except Exception as e: # Keep original traceback visible for diagnostics raise click.ClickException( - f"Runtime error: {e}\n{traceback.format_exc()}" + f"Runtime error: {e}\n{traceback.format_exc()}", ) from e diff --git a/astrbot/cli/commands/cmd_uninstall.py b/astrbot/cli/commands/cmd_uninstall.py index 06e8b5340..b77a0e1bd 100644 --- a/astrbot/cli/commands/cmd_uninstall.py +++ b/astrbot/cli/commands/cmd_uninstall.py @@ -10,11 +10,10 @@ 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.""" - if os.environ.get("ASTRBOT_SYSTEMD") == "1": yes = True diff --git a/astrbot/cli/i18n.py b/astrbot/cli/i18n.py index 07c58e40d..5b6662b83 100644 --- a/astrbot/cli/i18n.py +++ b/astrbot/cli/i18n.py @@ -245,6 +245,7 @@ def t(translation_key: str, **kwargs: str) -> str: Returns: Translated string, or the key itself if not found + """ result = _t_cached(translation_key, get_current_language()) if kwargs: diff --git a/astrbot/cli/utils/plugin.py b/astrbot/cli/utils/plugin.py index 4086b31c2..978b5d302 100644 --- a/astrbot/cli/utils/plugin.py +++ b/astrbot/cli/utils/plugin.py @@ -226,7 +226,7 @@ def manage_plugin( # Check if plugin exists if is_update and not target_path.exists(): raise click.ClickException( - f"Plugin {plugin_name} is not installed and cannot be updated" + f"Plugin {plugin_name} is not installed and cannot be updated", ) # Backup existing plugin @@ -245,7 +245,7 @@ def manage_plugin( if is_update and backup_path is not None and backup_path.exists(): shutil.rmtree(backup_path) click.echo( - f"Plugin {plugin_name} {'updated' if is_update else 'installed'} successfully" + f"Plugin {plugin_name} {'updated' if is_update else 'installed'} successfully", ) except Exception as e: if target_path.exists(): diff --git a/astrbot/cli/utils/version_comparator.py b/astrbot/cli/utils/version_comparator.py index 1f236946c..2bac3a824 100644 --- a/astrbot/cli/utils/version_comparator.py +++ b/astrbot/cli/utils/version_comparator.py @@ -62,12 +62,7 @@ class VersionComparator: return -1 if isinstance(p1, str) and isinstance(p2, int): return 1 - if isinstance(p1, int) and isinstance(p2, int): - if p1 > p2: - return 1 - if p1 < p2: - return -1 - elif isinstance(p1, str) and isinstance(p2, str): + if (isinstance(p1, int) and isinstance(p2, int)) or (isinstance(p1, str) and isinstance(p2, str)): if p1 > p2: return 1 if p1 < p2: diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index a4f9d8081..2c9a2f707 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -53,7 +53,7 @@ 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 307232465..54826cc1b 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -20,13 +20,12 @@ from astrbot.core.agent.context.truncator import ContextTruncator @runtime_checkable class ContextCompressor(Protocol): - """ - Protocol for context compressors. + """Protocol for context compressors. Provides an interface for compressing message lists. """ def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -37,6 +36,7 @@ class ContextCompressor(Protocol): Returns: True if compression is needed, False otherwise. + """ ... @@ -48,6 +48,7 @@ class ContextCompressor(Protocol): Returns: The compressed message list. + """ ... @@ -58,19 +59,20 @@ class TruncateByTurnsCompressor: """ def __init__( - self, truncate_turns: int = 1, compression_threshold: float = 0.82 + self, truncate_turns: int = 1, compression_threshold: float = 0.82, ) -> None: """Initialize the truncate by turns compressor. Args: truncate_turns: The number of turns to remove when truncating (default: 1). compression_threshold: The compression trigger threshold (default: 0.82). + """ self.truncate_turns = truncate_turns self.compression_threshold = compression_threshold def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -81,6 +83,7 @@ class TruncateByTurnsCompressor: Returns: True if compression is needed, False otherwise. + """ if max_tokens <= 0 or current_tokens <= 0: return False @@ -97,7 +100,7 @@ class TruncateByTurnsCompressor: def split_history( - messages: list[Message], keep_recent: int + messages: list[Message], keep_recent: int, ) -> tuple[list[Message], list[Message], list[Message]]: """Split the message list into system messages, messages to summarize, and recent messages. @@ -109,6 +112,7 @@ def split_history( Returns: tuple: (system_messages, messages_to_summarize, recent_messages) + """ # keep the system messages first_non_system = 0 @@ -161,6 +165,7 @@ class LLMSummaryCompressor: keep_recent: The number of latest messages to keep (default: 4). instruction_text: Custom instruction for summary generation. compression_threshold: The compression trigger threshold (default: 0.82). + """ self.provider = provider self.keep_recent = keep_recent @@ -175,7 +180,7 @@ class LLMSummaryCompressor: ) def should_compress( - self, messages: list[Message], current_tokens: int, max_tokens: int + self, messages: list[Message], current_tokens: int, max_tokens: int, ) -> bool: """Check if compression is needed. @@ -186,6 +191,7 @@ class LLMSummaryCompressor: Returns: True if compression is needed, False otherwise. + """ if max_tokens <= 0 or current_tokens <= 0: return False @@ -204,7 +210,7 @@ class LLMSummaryCompressor: return messages system_messages, messages_to_summarize, recent_messages = split_history( - messages, self.keep_recent + messages, self.keep_recent, ) if not messages_to_summarize: @@ -230,13 +236,13 @@ class LLMSummaryCompressor: Message( role="user", content=f"Our previous history conversation summary: {summary_content}", - ) + ), ) result.append( Message( role="assistant", content="Acknowledged the summary of our previous conversation history.", - ) + ), ) result.extend(recent_messages) diff --git a/astrbot/core/agent/context/manager.py b/astrbot/core/agent/context/manager.py index bc40c7147..371a92fe8 100644 --- a/astrbot/core/agent/context/manager.py +++ b/astrbot/core/agent/context/manager.py @@ -22,6 +22,7 @@ class ContextManager: Args: config: The context configuration. + """ self.config = config @@ -38,11 +39,11 @@ class ContextManager: ) else: self.compressor = TruncateByTurnsCompressor( - truncate_turns=config.truncate_turns + truncate_turns=config.truncate_turns, ) async def process( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> list[Message]: """Process the messages. @@ -51,6 +52,7 @@ class ContextManager: Returns: The processed message list. + """ try: result = messages @@ -66,11 +68,11 @@ class ContextManager: # 2. 基于 token 的压缩 if self.config.max_context_tokens > 0: total_tokens = self.token_counter.count_tokens( - result, trusted_token_usage + result, trusted_token_usage, ) if self.compressor.should_compress( - result, total_tokens, self.config.max_context_tokens + result, total_tokens, self.config.max_context_tokens, ): result = await self._run_compression(result, total_tokens) @@ -80,10 +82,9 @@ class ContextManager: return messages async def _run_compression( - self, messages: list[Message], prev_tokens: int + self, messages: list[Message], prev_tokens: int, ) -> list[Message]: - """ - Compress/truncate the messages. + """Compress/truncate the messages. Args: messages: The original message list. @@ -91,6 +92,7 @@ class ContextManager: Returns: The compressed/truncated message list. + """ logger.debug("Compress triggered, starting compression...") @@ -109,10 +111,10 @@ class ContextManager: # last check if self.compressor.should_compress( - messages, tokens_after_summary, self.config.max_context_tokens + messages, tokens_after_summary, self.config.max_context_tokens, ): logger.info( - "Context still exceeds max tokens after compression, applying halving truncation..." + "Context still exceeds max tokens after compression, applying halving truncation...", ) # still need compress, truncate by half messages = self.truncator.truncate_by_halving(messages) diff --git a/astrbot/core/agent/context/token_counter.py b/astrbot/core/agent/context/token_counter.py index bbcde7e50..f3e3c0627 100644 --- a/astrbot/core/agent/context/token_counter.py +++ b/astrbot/core/agent/context/token_counter.py @@ -12,13 +12,12 @@ from astrbot.core.agent.message import ( @runtime_checkable class TokenCounter(Protocol): - """ - Protocol for token counters. + """Protocol for token counters. Provides an interface for counting tokens in message lists. """ def count_tokens( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> int: """Count the total tokens in the message list. @@ -30,6 +29,7 @@ class TokenCounter(Protocol): Returns: The total token count. + """ ... @@ -50,7 +50,7 @@ class EstimateTokenCounter: """ def count_tokens( - self, messages: list[Message], trusted_token_usage: int = 0 + self, messages: list[Message], trusted_token_usage: int = 0, ) -> int: if trusted_token_usage > 0: return trusted_token_usage diff --git a/astrbot/core/agent/context/truncator.py b/astrbot/core/agent/context/truncator.py index 33e760a92..85f903f11 100644 --- a/astrbot/core/agent/context/truncator.py +++ b/astrbot/core/agent/context/truncator.py @@ -20,6 +20,7 @@ class ContextTruncator: Returns: tuple: (system_messages, non_system_messages) + """ first_non_system = 0 for i, msg in enumerate(messages): @@ -51,7 +52,7 @@ 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, @@ -127,8 +128,7 @@ class ContextTruncator: keep_most_recent_turns: int, drop_turns: int = 1, ) -> list[Message]: - """ - Turn-based truncation strategy, which drops the oldest turns while keeping the most recent N turns. + """Turn-based truncation strategy, which drops the oldest turns while keeping the most recent N turns. A turn consists of a user message and an assistant message. This method ensures that the truncated context list conforms to OpenAI's context format. @@ -139,6 +139,7 @@ class ContextTruncator: Returns: The truncated list of messages. + """ if keep_most_recent_turns == -1: return messages @@ -163,7 +164,7 @@ class ContextTruncator: truncated_contexts = truncated_contexts[index:] result = self._ensure_user_message( - system_messages, truncated_contexts, messages + system_messages, truncated_contexts, messages, ) return self.fix_messages(result) @@ -192,7 +193,7 @@ class ContextTruncator: truncated_non_system = truncated_non_system[index:] result = self._ensure_user_message( - system_messages, truncated_non_system, messages + system_messages, truncated_non_system, messages, ) return self.fix_messages(result) @@ -221,6 +222,6 @@ class ContextTruncator: truncated_non_system = truncated_non_system[index:] result = self._ensure_user_message( - system_messages, truncated_non_system, messages + system_messages, truncated_non_system, messages, ) return self.fix_messages(result) diff --git a/astrbot/core/agent/mcp_client.py b/astrbot/core/agent/mcp_client.py index e82e4af40..6d27c295a 100644 --- a/astrbot/core/agent/mcp_client.py +++ b/astrbot/core/agent/mcp_client.py @@ -1,5 +1,4 @@ -""" -MCP client - DEPRECATED +"""MCP client - DEPRECATED .. deprecated:: This module has been moved to :mod:`astrbot._internal.mcp`. @@ -51,7 +50,7 @@ try: from mcp.client.sse import sse_client except (ModuleNotFoundError, ImportError): logger.warning( - "Warning: Missing 'mcp' dependency, MCP services will be unavailable." + "Warning: Missing 'mcp' dependency, MCP services will be unavailable.", ) try: @@ -277,7 +276,7 @@ class MCPClient: write_stream=write_stream, read_timeout_seconds=read_timeout, logging_callback=logging_callback, - ) + ), ) self.session = session else: @@ -304,7 +303,7 @@ class MCPClient: write_stream=write_s, read_timeout_seconds=read_timeout, logging_callback=logging_callback, - ) + ), ) self.session = session @@ -333,10 +332,10 @@ class MCPClient: logger=logger, identifier=f"MCPServer-{name}", callback=callback, - ) + ), ) errlog_stream: TextIO = self.exit_stack.enter_context( - os.fdopen(os.dup(log_pipe.fileno()), "w") + os.fdopen(os.dup(log_pipe.fileno()), "w"), ) stdio_transport = await self.exit_stack.enter_async_context( mcp.stdio_client( @@ -370,12 +369,13 @@ class MCPClient: Raises: Exception: raised when reconnection fails + """ async with self._reconnect_lock: # Check if already reconnecting (useful for logging) if self._reconnecting: logger.debug( - f"MCP Client {self._server_name} is already reconnecting, skipping" + f"MCP Client {self._server_name} is already reconnecting, skipping", ) return @@ -385,7 +385,7 @@ class MCPClient: self._reconnecting = True try: logger.info( - f"Attempting to reconnect to MCP server {self._server_name}..." + f"Attempting to reconnect to MCP server {self._server_name}...", ) # Save old exit_stack for later cleanup (don't close it now to avoid cancel scope issues) @@ -403,11 +403,11 @@ class MCPClient: await self.list_tools_and_save() logger.info( - f"Successfully reconnected to MCP server {self._server_name}" + f"Successfully reconnected to MCP server {self._server_name}", ) except Exception as e: logger.error( - f"Failed to reconnect to MCP server {self._server_name}: {e}" + f"Failed to reconnect to MCP server {self._server_name}: {e}", ) raise finally: @@ -432,6 +432,7 @@ class MCPClient: Raises: ValueError: MCP session is not available anyio.ClosedResourceError: raised after reconnection failure + """ @retry( @@ -453,7 +454,7 @@ class MCPClient: ) except anyio.ClosedResourceError: logger.warning( - f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect..." + f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect...", ) # Attempt to reconnect await self._reconnect() @@ -484,7 +485,7 @@ class MCPTool(FunctionTool, Generic[TContext]): """A function tool that calls an MCP service.""" def __init__( - self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs + self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs, ) -> None: super().__init__( name=mcp_tool.name, @@ -497,7 +498,7 @@ 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 87c60f42c..e8b0605d4 100644 --- a/astrbot/core/agent/message.py +++ b/astrbot/core/agent/message.py @@ -38,7 +38,7 @@ 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": @@ -65,8 +65,7 @@ class ContentPart(BaseModel): class TextPart(ContentPart): - """ - >>> TextPart(text="Hello, world!").model_dump() + """>>> TextPart(text="Hello, world!").model_dump() {'type': 'text', 'text': 'Hello, world!'} """ @@ -75,8 +74,7 @@ class TextPart(ContentPart): class ThinkPart(ContentPart): - """ - >>> ThinkPart(think="I think I need to think about this.").model_dump() + """>>> ThinkPart(think="I think I need to think about this.").model_dump() {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None} """ @@ -97,8 +95,7 @@ class ThinkPart(ContentPart): class ImageURLPart(ContentPart): - """ - >>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() + """>>> ImageURLPart(image_url="http://example.com/image.jpg").model_dump() {'type': 'image_url', 'image_url': 'http://example.com/image.jpg'} """ @@ -113,8 +110,7 @@ class ImageURLPart(ContentPart): class AudioURLPart(ContentPart): - """ - >>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() + """>>> AudioURLPart(audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")).model_dump() {'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}} """ @@ -129,8 +125,7 @@ class AudioURLPart(ContentPart): class ToolCall(BaseModel): - """ - A tool call requested by the assistant. + """A tool call requested by the assistant. >>> ToolCall( ... id="123", @@ -200,7 +195,7 @@ class Message(BaseModel): # other all cases: content is required if self.content is None: raise ValueError( - "content is required unless role='assistant' and tool_calls is not None" + "content is required unless role='assistant' and tool_calls is not None", ) return self diff --git a/astrbot/core/agent/runners/coze/coze_agent_runner.py b/astrbot/core/agent/runners/coze/coze_agent_runner.py index 055ec651e..e9d94145e 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.") @@ -109,12 +108,12 @@ 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", data=AgentResponseData( - chain=MessageChain().message(f"Coze 请求失败:{e!s}") + chain=MessageChain().message(f"Coze 请求失败:{e!s}"), ), ) finally: @@ -179,7 +178,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): if url: file_id = ( await self._download_and_upload_image( - url, session_id + url, session_id, ) ) processed_content.append( @@ -187,7 +186,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "type": "file", "file_id": file_id, "file_url": url, - } + }, ) except Exception as e: logger.warning(f"处理上下文图片失败: {e}") @@ -199,7 +198,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": ctx["role"], "content": processed_content, "content_type": "object_string", - } + }, ) else: # 纯文本内容 @@ -208,7 +207,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": ctx["role"], "content": content, "content_type": "text", - } + }, ) # 构建当前消息 @@ -228,7 +227,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): { "type": "image", "file_id": file_id, - } + }, ) except Exception as e: logger.warning(f"处理图片失败 {url}: {e}") @@ -241,7 +240,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): "role": "user", "content": content, "content_type": "object_string", - } + }, ) elif prompt: # 纯文本 @@ -295,7 +294,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]): yield AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(content) + chain=MessageChain().message(content), ), ) diff --git a/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py b/astrbot/core/agent/runners/dashscope/dashscope_agent_runner.py index 5a81088e9..e1888a5a2 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.") @@ -121,12 +120,12 @@ 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", data=AgentResponseData( - chain=MessageChain().message(f"阿里云百炼请求失败:{e!s}") + chain=MessageChain().message(f"阿里云百炼请求失败:{e!s}"), ), ) @@ -137,7 +136,7 @@ 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,将结果放入队列 @@ -158,7 +157,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): response_queue.put(("done", None)) async def _process_stream_chunk( - self, chunk: ApplicationResponse, output_text: str + self, chunk: ApplicationResponse, output_text: str, ) -> tuple[str, list | None, AgentResponse | None]: """处理流式响应的单个chunk @@ -234,7 +233,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): return f"\n\n回答来源:\n{ref_str}" async def _build_request_payload( - self, prompt: str, session_id: str, contexts: list, system_prompt: str + self, prompt: str, session_id: str, contexts: list, system_prompt: str, ) -> dict: """构建请求payload @@ -282,22 +281,21 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): if conversation_id: p["session_id"] = conversation_id return p - else: - # 不支持多轮对话的 - payload = { - "app_id": self.app_id, - "prompt": prompt, - "api_key": self.api_key, - "biz_params": payload_vars or None, - "stream": self.streaming, - "incremental_output": True, - } - if self.rag_options: - payload["rag_options"] = self.rag_options - return payload + # 不支持多轮对话的 + payload = { + "app_id": self.app_id, + "prompt": prompt, + "api_key": self.api_key, + "biz_params": payload_vars or None, + "stream": self.streaming, + "incremental_output": True, + } + if self.rag_options: + payload["rag_options"] = self.rag_options + return payload async def _handle_streaming_response( - self, response: Any, session_id: str + self, response: Any, session_id: str, ) -> AsyncGenerator[AgentResponse, None]: """处理流式响应 @@ -322,7 +320,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): while True: try: item_type, item_data = await asyncio.get_running_loop().run_in_executor( - None, response_queue.get, True, 1 + None, response_queue.get, True, 1, ) except queue.Empty: continue @@ -332,7 +330,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): elif item_type == "error": if not isinstance(item_data, BaseException): raise RuntimeError( - f"Unexpected Dashscope error payload: {item_data!r}" + f"Unexpected Dashscope error payload: {item_data!r}", ) raise item_data elif item_type == "data": @@ -404,7 +402,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]): # 构建请求payload payload = await self._build_request_payload( - prompt, session_id, contexts, system_prompt + prompt, session_id, contexts, system_prompt, ) if not self.streaming: diff --git a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py index 1eabb6c23..9e2c6ed5f 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_agent_runner.py @@ -134,7 +134,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): logger.error(f"Error in on_agent_done hook: {e}", exc_info=True) async def _finish_with_result( - self, chain: MessageChain, role: str + self, chain: MessageChain, role: str, ) -> AgentResponse: self.final_llm_resp = LLMResponse( role=role, @@ -251,7 +251,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): await old_client.close() except Exception as e: logger.warning( - f"Failed to close previous DeerFlow API client cleanly: {e}" + f"Failed to close previous DeerFlow API client cleanly: {e}", ) self.api_client = DeerFlowAPIClient( @@ -331,7 +331,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): if not self.done(): raise RuntimeError( - f"DeerFlow agent reached max_step ({max_step}) without completion." + f"DeerFlow agent reached max_step ({max_step}) without completion.", ) def _extract_new_messages_from_values( @@ -396,7 +396,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): thread_id = thread.get("thread_id", "") if not thread_id: raise Exception( - f"DeerFlow create thread returned invalid payload: {thread}" + f"DeerFlow create thread returned invalid payload: {thread}", ) await sp.put_async( @@ -482,7 +482,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): AgentResponse( type="streaming_delta", data=AgentResponseData(chain=MessageChain().message(delta)), - ) + ), ] if delta_text: @@ -492,9 +492,9 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(delta_text) + chain=MessageChain().message(delta_text), ), - ) + ), ] return [] @@ -546,7 +546,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): self._update_text_and_maybe_stream( state=state, new_full_text=latest_text or None, - ) + ), ) return responses @@ -563,7 +563,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]): self._update_text_and_maybe_stream( state=state, delta_text=delta, - ) + ), ) maybe_clarification = extract_clarification_from_event_data(data) diff --git a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py index 17999657f..caf61699f 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_api_client.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_api_client.py @@ -170,7 +170,7 @@ class DeerFlowAPIClient: input_payload = payload.get("input") message_count = 0 if isinstance(input_payload, dict) and isinstance( - input_payload.get("messages"), list + input_payload.get("messages"), list, ): message_count = len(input_payload["messages"]) # Log only a minimal summary to avoid exposing sensitive user content. @@ -239,7 +239,7 @@ class DeerFlowAPIClient: return logger.warning( "DeerFlowAPIClient garbage collected with unclosed session; " - "explicit close() should be called by runner lifecycle (or `async with`)." + "explicit close() should be called by runner lifecycle (or `async with`).", ) @property diff --git a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py index 2477adbb9..87cb691f4 100644 --- a/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py +++ b/astrbot/core/agent/runners/deerflow/deerflow_content_mapper.py @@ -58,7 +58,7 @@ def build_user_content(prompt: str, image_urls: list[str]) -> Any: if not is_likely_base64_image(url): skipped_invalid_images += 1 logger.debug( - "Skipped DeerFlow image input because it is neither URL/data URI nor valid base64." + "Skipped DeerFlow image input because it is neither URL/data URI nor valid base64.", ) continue compact_base64 = url.replace("\n", "").replace("\r", "") @@ -164,14 +164,14 @@ def append_components_from_content( if "content" in content: append_components_from_content( - content.get("content"), components, image_resolver + content.get("content"), components, image_resolver, ) return kwargs = content.get("kwargs") if isinstance(kwargs, dict) and "content" in kwargs: append_components_from_content( - kwargs.get("content"), components, image_resolver + kwargs.get("content"), components, image_resolver, ) diff --git a/astrbot/core/agent/runners/dify/dify_agent_runner.py b/astrbot/core/agent/runners/dify/dify_agent_runner.py index cd19c900e..72a892847 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.") @@ -101,12 +100,12 @@ 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", data=AgentResponseData( - chain=MessageChain().message(f"Dify 请求失败:{e!s}") + chain=MessageChain().message(f"Dify 请求失败:{e!s}"), ), ) finally: @@ -148,7 +147,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): logger.debug(f"Dify 上传图片响应:{file_response}") if "id" not in file_response: logger.warning( - f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。" + f"上传图片后得到未知的 Dify 响应:{file_response},图片将忽略。", ) continue files_payload.append( @@ -156,7 +155,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): "type": "image", "transfer_method": "local_file", "upload_file_id": file_response["id"], - } + }, ) except Exception as e: logger.warning(f"上传图片失败:{e}") @@ -210,7 +209,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): yield AgentResponse( type="streaming_delta", data=AgentResponseData( - chain=MessageChain().message(chunk["answer"]) + chain=MessageChain().message(chunk["answer"]), ), ) elif chunk["event"] == "message_end": @@ -219,7 +218,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): elif chunk["event"] == "error": logger.error(f"Dify 出现错误:{chunk}") raise Exception( - f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}" + f"Dify 出现错误 status: {chunk['status']} message: {chunk['message']}", ) case "workflow": @@ -237,11 +236,11 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): match chunk["event"]: case "workflow_started": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。" + f"Dify 工作流(ID: {chunk['workflow_run_id']})开始运行。", ) case "node_finished": logger.debug( - f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。" + f"Dify 工作流节点(ID: {chunk['data']['node_id']} Title: {chunk['data'].get('title', '')})运行结束。", ) case "text_chunk": if self.streaming and chunk["data"]["text"]: @@ -249,25 +248,25 @@ class DifyAgentRunner(BaseAgentRunner[TContext]): type="streaming_delta", data=AgentResponseData( chain=MessageChain().message( - chunk["data"]["text"] - ) + chunk["data"]["text"], + ), ), ) case "workflow_finished": logger.info( - f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束" + f"Dify 工作流(ID: {chunk['workflow_run_id']})运行结束", ) logger.debug(f"Dify 工作流结果:{chunk}") if chunk["data"]["error"]: logger.error( - f"Dify 工作流出现错误:{chunk['data']['error']}" + f"Dify 工作流出现错误:{chunk['data']['error']}", ) raise Exception( - f"Dify 工作流出现错误:{chunk['data']['error']}" + f"Dify 工作流出现错误:{chunk['data']['error']}", ) if self.workflow_output_key not in chunk["data"]["outputs"]: raise Exception( - f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}" + f"Dify 工作流的输出不包含指定的键名:{self.workflow_output_key}", ) result = chunk case _: diff --git a/astrbot/core/agent/runners/dify/dify_api_client.py b/astrbot/core/agent/runners/dify/dify_api_client.py index ace2ea384..f78a2965e 100644 --- a/astrbot/core/agent/runners/dify/dify_api_client.py +++ b/astrbot/core/agent/runners/dify/dify_api_client.py @@ -123,8 +123,10 @@ class DifyAPIClient: file_path: The path to the file to upload. file_data: The file data in bytes. file_name: Optional file name when using file_data. + Returns: A dictionary containing the uploaded file information. + """ url = f"{self.api_base}/files/upload" diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 9c5c360d2..f7b805563 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -72,7 +72,7 @@ class _HandleFunctionToolsResult: @classmethod def from_tool_call_result_blocks( - cls, blocks: list[ToolCallMessageSegment] + cls, blocks: list[ToolCallMessageSegment], ) -> "_HandleFunctionToolsResult": return cls(kind="tool_call_result_blocks", tool_call_result_blocks=blocks) @@ -276,7 +276,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): self.stats.start_time = time.time() async def _iter_llm_responses( - self, *, include_model: bool = True + self, *, include_model: bool = True, ) -> AsyncGenerator[LLMResponse, None]: """Yields chunks *and* a final LLMResponse.""" payload: dict[str, Any] = { @@ -374,7 +374,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): await asyncio.sleep(wait_time) try: async for resp in self._iter_llm_responses( - include_model=idx == 0 + include_model=idx == 0, ): if resp.is_chunk: has_stream_output = True @@ -527,7 +527,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): token_usage = self.req.conversation.token_usage if self.req.conversation else 0 self._simple_print_message_role("[BefCompact]") self.run_context.messages = await self.context_manager.process( - self.run_context.messages, trusted_token_usage=token_usage + self.run_context.messages, trusted_token_usage=token_usage, ) self._simple_print_message_role("[AftCompact]") @@ -620,13 +620,13 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) if len(parts) == 0: logger.warning( - "LLM returned empty assistant message with no tool calls." + "LLM returned empty assistant message with no tool calls.", ) self.run_context.messages.append(Message(role="assistant", content=parts)) @@ -691,7 +691,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -706,7 +706,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ) # record the assistant message with tool calls self.run_context.messages.extend( - tool_calls_result.to_openai_messages_model() + tool_calls_result.to_openai_messages_model(), ) # If there are cached images and the model supports image input, @@ -719,29 +719,29 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): image_parts = [] for cached_img in cached_images: img_data = tool_image_cache.get_image_base64_by_path( - cached_img.file_path, cached_img.mime_type + cached_img.file_path, cached_img.mime_type, ) if img_data: base64_data, mime_type = img_data image_parts.append( TextPart( - text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']" - ) + text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']", + ), ) image_parts.append( ImageURLPart( image_url=ImageURLPart.ImageURL( url=f"data:{mime_type};base64,{base64_data}", id=cached_img.file_path, - ) - ) + ), + ), ) if image_parts: self.run_context.messages.append( - Message(role="user", content=image_parts) + Message(role="user", content=image_parts), ) logger.debug( - f"Appended {len(cached_images)} cached image(s) to context for LLM review" + f"Appended {len(cached_images)} cached image(s) to context for LLM review", ) self.req.append_tool_calls_result(tool_calls_result) @@ -758,7 +758,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step if not self.done(): logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + f"Agent reached max steps ({max_step}), forcing a final response.", ) # 拔掉所有工具 if self.req: @@ -768,7 +768,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): Message( role="user", content=self.MAX_STEPS_REACHED_PROMPT, - ) + ), ) # 再执行最后一步 async for resp in self.step(): @@ -835,10 +835,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "name": func_tool_name, "args": func_tool_args, "ts": time.time(), - } - ) + }, + ), ], - ) + ), ) try: if not req.func_tool: @@ -936,11 +936,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility (will be handled in step()) yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) elif isinstance(content_item, EmbeddedResource): resource = content_item.resource @@ -962,22 +962,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): result_parts.append( f"Image returned and cached at path='{cached_img.file_path}'. " f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." + f"with type='image' and path='{cached_img.file_path}'.", ) # Yield image info for LLM visibility yield _HandleFunctionToolsResult.from_cached_image( - cached_img + cached_img, ) else: result_parts.append( - "The tool has returned a data type that is not supported." + "The tool has returned a data type that is not supported.", ) if result_parts: _append_tool_call_result( func_tool_id, "\n\n".join(result_parts) + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -986,7 +986,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): # 这里我们将直接结束 Agent Loop # 发送消息逻辑在 ToolExecutor 中处理了 logger.warning( - f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。" + f"{func_tool_name} 没有返回值,或者已将结果直接发送给用户。", ) self._transition_state(AgentState.DONE) self.stats.end_time = time.time() @@ -994,7 +994,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "The tool has no return value, or has sent the result directly to the user." + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) else: @@ -1006,7 +1006,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, "*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -1027,7 +1027,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): func_tool_id, f"error: {e!s}" + self._build_repeated_tool_call_guidance( - func_tool_name, tool_call_streak + func_tool_name, tool_call_streak, ), ) @@ -1043,21 +1043,21 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): "id": func_tool_id, "ts": time.time(), "result": last_tcr_content, - } - ) + }, + ), ], - ) + ), ) logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}") # 处理函数调用响应 if tool_call_result_blocks: yield _HandleFunctionToolsResult.from_tool_call_result_blocks( - tool_call_result_blocks + tool_call_result_blocks, ) def _build_tool_requery_context( - 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]] = [] @@ -1067,7 +1067,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): elif isinstance(msg, dict): contexts.append(copy.deepcopy(msg)) instruction = self.SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE.format( - tool_names=", ".join(tool_names) + tool_names=", ".join(tool_names), ) if extra_instruction: instruction = f"{instruction}\n{extra_instruction}" @@ -1105,7 +1105,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if isinstance(self._tool_schema_param_set, ToolSet): param_subset = self._build_tool_subset( - self._tool_schema_param_set, tool_names + self._tool_schema_param_set, tool_names, ) if param_subset.tools and tool_names: contexts = self._build_tool_requery_context(tool_names) @@ -1128,7 +1128,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): and not self._has_meaningful_assistant_reply(llm_resp) ): logger.warning( - "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction." + "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction.", ) repair_contexts = self._build_tool_requery_context( tool_names, @@ -1187,7 +1187,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): ThinkPart( think=llm_resp.reasoning_content, encrypted=llm_resp.reasoning_signature, - ) + ), ) if llm_resp.completion_text: parts.append(TextPart(text=llm_resp.completion_text)) @@ -1220,7 +1220,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): if self._is_stop_requested(): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted before reading the next tool result." + "Tool execution interrupted before reading the next tool result.", ) async def _get_next(): @@ -1247,7 +1247,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]): await self._close_executor(executor) raise _ToolExecutionInterrupted( - "Tool execution interrupted by a stop request." + "Tool execution interrupted by a stop request.", ) try: diff --git a/astrbot/core/agent/tool.py b/astrbot/core/agent/tool.py index 365e24475..8a1093d12 100644 --- a/astrbot/core/agent/tool.py +++ b/astrbot/core/agent/tool.py @@ -44,7 +44,7 @@ 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,11 +106,11 @@ 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( - "FunctionTool.call() must be implemented by subclasses or set a handler." + "FunctionTool.call() must be implemented by subclasses or set a handler.", ) @@ -184,7 +184,7 @@ class ToolSet: description=tool.description, parameters=light_params, handler=None, - ) + ), ) return ToolSet(light_tools) @@ -205,7 +205,7 @@ class ToolSet: description="", parameters=params, handler=None, - ) + ), ) return ToolSet(param_tools) diff --git a/astrbot/core/agent/tool_image_cache.py b/astrbot/core/agent/tool_image_cache.py index 9c99b3509..63320bd1f 100644 --- a/astrbot/core/agent/tool_image_cache.py +++ b/astrbot/core/agent/tool_image_cache.py @@ -90,6 +90,7 @@ class ToolImageCache: Returns: CachedImage object with file path. + """ ext = self._get_file_extension(mime_type) file_name = f"{tool_call_id}_{index}{ext}" @@ -113,7 +114,7 @@ class ToolImageCache: ) def get_image_base64_by_path( - self, file_path: str, mime_type: str = "image/png" + self, file_path: str, mime_type: str = "image/png", ) -> tuple[str, str] | None: """Read an image file and return its base64 encoded data. @@ -123,6 +124,7 @@ class ToolImageCache: Returns: Tuple of (base64_data, mime_type) if found, None otherwise. + """ if not os.path.exists(file_path): return None @@ -141,6 +143,7 @@ class ToolImageCache: Returns: Number of images cleaned up. + """ now = time.time() cleaned = 0 diff --git a/astrbot/core/agent/tool_session_manager.py b/astrbot/core/agent/tool_session_manager.py index e185bc734..686fa21f0 100644 --- a/astrbot/core/agent/tool_session_manager.py +++ b/astrbot/core/agent/tool_session_manager.py @@ -1,5 +1,4 @@ -""" -ToolSessionManager - Session-level state management for stateful tools. +"""ToolSessionManager - Session-level state management for stateful tools. Provides per-(UMO, tool_name) session state that persists across conversation turns within the same session, with optional persistence via SharedPreferences. @@ -14,8 +13,7 @@ from astrbot.core.utils.shared_preferences import SharedPreferences @dataclass class ToolSessionState(MutableMapping[str, Any]): - """ - Represents the session state for a single tool within a session. + """Represents the session state for a single tool within a session. Acts like a dict but supports persistence markers. Use `set_persistent(key)` to mark keys that survive session clear. @@ -51,8 +49,7 @@ class ToolSessionState(MutableMapping[str, Any]): class ToolSessionManager: - """ - Central manager for all tool session states. + """Central manager for all tool session states. Maintains in-memory state per (umo, tool_name) combination. Optional SharedPreferences integration for persistence across sessions. @@ -62,6 +59,7 @@ class ToolSessionManager: state = mgr.get_state(umo, "shell") state["cwd"] = "/tmp" state.set_persistent("env") # env survives session clear + """ def __init__(self, sp: SharedPreferences | None = None) -> None: @@ -104,8 +102,7 @@ class ToolSessionManager: state.set_persistent(actual_key) def clear_session(self, umo: str) -> None: - """ - Clear non-persistent state for all tools in a session. + """Clear non-persistent state for all tools in a session. Persistent keys (marked via `set_persistent`) are preserved. """ diff --git a/astrbot/core/astr_agent_hooks.py b/astrbot/core/astr_agent_hooks.py index ca6dc8f44..6548801dd 100644 --- a/astrbot/core/astr_agent_hooks.py +++ b/astrbot/core/astr_agent_hooks.py @@ -34,7 +34,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): if llm_response and llm_response.reasoning_content: # we will use this in result_decorate stage to inject reasoning content to chain run_context.context.event.set_extra( - "_llm_reasoning_content", llm_response.reasoning_content + "_llm_reasoning_content", llm_response.reasoning_content, ) await call_event_hook( @@ -43,7 +43,7 @@ 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 +80,7 @@ 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 +113,7 @@ 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 113ca67c1..5b0e43224 100644 --- a/astrbot/core/astr_agent_run_util.py +++ b/astrbot/core/astr_agent_run_util.py @@ -49,7 +49,7 @@ def _extract_chain_json_data(msg_chain: MessageChain) -> dict | None: def _record_tool_call_name( - tool_info: dict | None, tool_name_by_call_id: dict[str, str] + tool_info: dict | None, tool_name_by_call_id: dict[str, str], ) -> None: if not isinstance(tool_info, dict): return @@ -67,7 +67,7 @@ def _build_tool_call_status_message(tool_info: dict | None) -> str: def _build_tool_result_status_message( - msg_chain: MessageChain, tool_name_by_call_id: dict[str, str] + msg_chain: MessageChain, tool_name_by_call_id: dict[str, str], ) -> str: tool_name = "unknown" tool_result = "" @@ -120,7 +120,7 @@ async def run_agent( if step_idx == max_step + 1: logger.warning( - f"Agent reached max steps ({max_step}), forcing a final response." + f"Agent reached max steps ({max_step}), forcing a final response.", ) if not agent_runner.done(): # 拔掉所有工具 @@ -131,7 +131,7 @@ async def run_agent( Message( role="user", content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。", - ) + ), ) stop_watcher = asyncio.create_task( @@ -162,7 +162,7 @@ async def run_agent( astr_event.trace.record( "agent_tool_result", tool_result=msg_chain.get_plain_text( - with_other_comps_mark=True + with_other_comps_mark=True, ), ) @@ -174,10 +174,10 @@ async def run_agent( await astr_event.send(msg_chain) elif show_tool_use and show_tool_call_result: status_msg = _build_tool_result_status_message( - msg_chain, tool_name_by_call_id + msg_chain, tool_name_by_call_id, ) await astr_event.send( - MessageChain(type="tool_call").message(status_msg) + MessageChain(type="tool_call").message(status_msg), ) # 对于其他情况,暂时先不处理 continue @@ -205,7 +205,7 @@ async def run_agent( # Delay tool status notification until tool_call_result. continue chain = MessageChain(type="tool_call").message( - _build_tool_call_status_message(tool_info) + _build_tool_call_status_message(tool_info), ) await astr_event.send(chain) continue @@ -235,7 +235,7 @@ async def run_agent( yield resp.data["chain"] # MessageChain elif resp.type == "llm_result": if final_chain := _extract_final_streaming_chain( - resp.data["chain"] + resp.data["chain"], ): yield final_chain if not stop_watcher.done(): @@ -251,7 +251,7 @@ async def run_agent( MessageChain( type="agent_stats", chain=[Json(data=agent_runner.stats.to_dict())], - ) + ), ) break @@ -266,7 +266,7 @@ async def run_agent( logger.error(traceback.format_exc()) custom_error_message = extract_persona_custom_error_message_from_event( - astr_event + astr_event, ) if custom_error_message: err_msg = custom_error_message @@ -283,7 +283,7 @@ async def run_agent( ) try: await agent_runner.agent_hooks.on_agent_done( - agent_runner.run_context, error_llm_response + agent_runner.run_context, error_llm_response, ) except Exception: logger.exception("Error in on_agent_done hook") @@ -323,6 +323,7 @@ async def run_live_agent( Yields: MessageChain: 包含文本或音频数据的消息链 + """ # 如果没有 TTS Provider,直接发送文本 if not tts_provider: @@ -343,7 +344,7 @@ async def run_live_agent( else: logger.info( f"[Live Agent] 使用 TTS({tts_provider.meta().type} " - "使用 get_audio,将按句子分块生成音频)" + "使用 get_audio,将按句子分块生成音频)", ) # 统计数据初始化 @@ -365,17 +366,17 @@ async def run_live_agent( show_tool_use, show_tool_call_result, show_reasoning, - ) + ), ) # 2. 启动 TTS 任务:负责从 text_queue 读取文本并生成音频到 audio_queue if support_stream: tts_task = asyncio.create_task( - _safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue) + _safe_tts_stream_wrapper(tts_provider, text_queue, audio_queue), ) else: tts_task = asyncio.create_task( - _simulated_stream_tts(tts_provider, text_queue, audio_queue) + _simulated_stream_tts(tts_provider, text_queue, audio_queue), ) # 3. 主循环:从 audio_queue 读取音频并 yield @@ -417,7 +418,6 @@ async def run_live_agent( tts_task.cancel() # 确保队列被消费 - pass tts_end_time = time.time() @@ -436,10 +436,10 @@ async def run_live_agent( "tts_first_frame_time": tts_first_frame_time, "tts": tts_provider.meta().type, "chat_model": agent_runner.provider.get_model(), - } - ) + }, + ), ], - ) + ), ) except Exception as e: logger.error(f"发送 TTS 统计信息失败: {e}") @@ -541,7 +541,7 @@ async def _simulated_stream_tts( await audio_queue.put((text, audio_data)) except Exception as e: logger.error( - f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}" + f"[Live TTS Simulated] Error processing text '{text[:20]}...': {e}", ) # 继续处理下一句 diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 615bbd436..a16971458 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -58,7 +58,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): @classmethod async def _collect_image_urls_from_message( - cls, run_context: ContextWrapper[AstrAgentContext] + cls, run_context: ContextWrapper[AstrAgentContext], ) -> list[str]: urls: list[str] = [] event = getattr(run_context.context, "event", None) @@ -83,7 +83,7 @@ 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 +126,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): is_bg = tool_args.pop("background_task", False) if is_bg: async for r in cls._execute_handoff_background( - tool, run_context, **tool_args + tool, run_context, **tool_args, ): yield r return @@ -143,16 +143,16 @@ 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 @@ -170,15 +170,16 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): "astrbot_execute_browser", "astrbot_execute_browser_batch", "astrbot_run_browser_skill", - } + }, ) @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.""" + that is not available, or None if the tool may proceed. + """ if tool.name not in cls._BROWSER_TOOL_NAMES: return None from astrbot.core.computer.computer_client import get_sandbox_capabilities @@ -190,23 +191,23 @@ 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} @@ -231,7 +232,7 @@ 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() @@ -282,7 +283,7 @@ 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) @@ -290,7 +291,7 @@ 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 @@ -301,7 +302,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): contexts.append( dialog if isinstance(dialog, Message) - else Message.model_validate(dialog) + else Message.model_validate(dialog), ) except Exception: continue @@ -321,7 +322,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): stream=stream, ) yield mcp.types.CallToolResult( - content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)] + content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)], ) @classmethod @@ -344,7 +345,7 @@ 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( @@ -371,11 +372,11 @@ 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: @@ -409,7 +410,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): result_text = "" try: async for r in cls._execute_local( - tool, run_context, tool_call_timeout=3600, **tool_args + tool, run_context, tool_call_timeout=3600, **tool_args, ): if isinstance(r, mcp.types.CallToolResult): result_text = "" @@ -491,14 +492,14 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): req.system_prompt += CONVERSATION_HISTORY_INJECT_PREFIX + context_dump bg = json.dumps(extras["background_task_result"], ensure_ascii=False) req.system_prompt += BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT.format( - background_task_result=bg + background_task_result=bg, ) req.prompt = BACKGROUND_TASK_WOKE_USER_PROMPT if not req.func_tool: 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}.") @@ -556,7 +557,7 @@ 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: @@ -566,7 +567,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): { "tool_name": tool.name, "tool_args": json.loads( - json.dumps(tool_args, ensure_ascii=False, default=str) + json.dumps(tool_args, ensure_ascii=False, default=str), ), }, ) @@ -578,7 +579,7 @@ 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: @@ -591,7 +592,7 @@ 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: @@ -599,7 +600,7 @@ 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) @@ -610,12 +611,12 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]): mcp.types.TextContent( type="text", text="Tool executed successfully with no output.", - ) - ] + ), + ], ) except asyncio.TimeoutError: raise Exception( - f"tool {tool.name} execution timeout after {tool_call_timeout or run_context.tool_call_timeout} seconds." + f"tool {tool.name} execution timeout after {tool_call_timeout or run_context.tool_call_timeout} seconds.", ) from None except StopAsyncIteration: break @@ -681,7 +682,7 @@ async def call_local_llm_tool( except Exception: handler_param_str = "(unable to inspect signature)" raise Exception( - f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}" + f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}", ) from e except Exception as e: trace_ = traceback.format_exc() diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index ed20512e6..17dc3778e 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -71,7 +71,8 @@ from astrbot.core.utils.string_utils import normalize_and_dedupe_strings @dataclass(slots=True) class MainAgentBuildConfig: """The main agent build configuration. - Most of the configs can be found in the cmd_config.json""" + Most of the configs can be found in the cmd_config.json + """ tool_call_timeout: int "The timeout (in seconds) for a tool call.\n When the tool call exceeds this time,\n a timeout error as a tool result will be returned.\n " @@ -129,7 +130,7 @@ class MainAgentBuildResult: def _select_provider( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, plugin_context: Context, ) -> Provider | None: """Select chat provider for the event.""" sel_provider = event.get_extra("selected_provider") @@ -149,7 +150,7 @@ def _select_provider( async def _get_session_conv( - event: AstrMessageEvent, plugin_context: Context + event: AstrMessageEvent, plugin_context: Context, ) -> Conversation: conv_mgr = plugin_context.conversation_manager umo = event.unified_msg_origin @@ -176,7 +177,7 @@ 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 @@ -193,7 +194,7 @@ 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 = [] @@ -218,7 +219,7 @@ async def _apply_file_extract( *[ extract_file_moonshotai(file_path, config.file_extract_msh_api_key) for file_path in file_paths - ] + ], ) else: logger.error("Unsupported file extract provider: %s", config.file_extract_prov) @@ -228,9 +229,9 @@ 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", ), - } + }, ) @@ -245,7 +246,7 @@ 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: @@ -262,7 +263,7 @@ 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"]: @@ -286,7 +287,7 @@ async def _ensure_persona_and_skills( if runtime == "none": req.system_prompt += COMPUTER_USE_DISABLED_PROMPT tmgr = plugin_context.get_llm_tool_manager() - if persona and persona.get("tools") is None or not persona: + if (persona and persona.get("tools") is None) or not persona: persona_toolset = tmgr.get_full_tool_set() for tool in list(persona_toolset): if not tool.active: @@ -329,7 +330,7 @@ async def _ensure_persona_and_skills( tool.name for tool in tmgr.func_list if not isinstance(tool, HandoffTool) - ] + ], ) continue if not isinstance(tools, list): @@ -367,16 +368,16 @@ 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: raise ValueError( - f"Cannot get image caption because provider `{provider_id}` is not exist." + f"Cannot get image caption because provider `{provider_id}` is not exist.", ) if not isinstance(prov, Provider): raise ValueError( - f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}." + f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.", ) img_cap_prompt = cfg.get("image_caption_prompt", IMAGE_CAPTION_DEFAULT_PROMPT) logger.debug("Processing image caption with provider: %s", provider_id) @@ -399,11 +400,11 @@ 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( - TextPart(text=f"{caption}") + TextPart(text=f"{caption}"), ) req.image_urls = [] except Exception as exc: @@ -415,19 +416,19 @@ async def _ensure_img_caption( def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment in quoted message: path {image_path}]") + TextPart(text=f"[Image Attachment in quoted message: path {image_path}]"), ) def _append_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment: path {audio_path}]") + TextPart(text=f"[Audio Attachment: path {audio_path}]"), ) def _append_quoted_audio_attachment(req: ProviderRequest, audio_path: str) -> None: req.extra_user_content_parts.append( - TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]") + TextPart(text=f"[Audio Attachment in quoted message: path {audio_path}]"), ) @@ -467,7 +468,7 @@ 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) @@ -504,7 +505,7 @@ 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) @@ -517,7 +518,7 @@ 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 @@ -551,7 +552,7 @@ 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) @@ -564,7 +565,7 @@ 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 @@ -592,7 +593,7 @@ 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]" @@ -616,7 +617,7 @@ 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) @@ -626,7 +627,7 @@ async def _process_quote_message( ) if llm_resp.completion_text: content_parts.append( - f"[Image Caption in quoted message]: {llm_resp.completion_text}" + f"[Image Caption in quoted message]: {llm_resp.completion_text}", ) else: logger.warning("No provider found for image captioning in quote.") @@ -645,7 +646,7 @@ 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"): @@ -689,7 +690,7 @@ async def _decorate_llm_request( config: MainAgentBuildConfig, ) -> None: cfg = config.provider_settings or plugin_context.get_config( - umo=event.unified_msg_origin + umo=event.unified_msg_origin, ).get("provider_settings", {}) _apply_prompt_prefix(req, cfg) if req.conversation: @@ -700,7 +701,7 @@ 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: @@ -713,7 +714,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["image"]) if "image" not in provider_cfg: logger.debug( - "Provider %s does not support image, using placeholder.", provider + "Provider %s does not support image, using placeholder.", provider, ) image_count = len(req.image_urls) placeholder = " ".join(["[Image]"] * image_count) @@ -726,7 +727,7 @@ def _modalities_fix(provider: Provider, req: ProviderRequest) -> None: provider_cfg = provider.provider_config.get("modalities", ["audio"]) if "audio" not in provider_cfg: logger.debug( - "Provider %s does not support audio, using placeholder.", provider + "Provider %s does not support audio, using placeholder.", provider, ) audio_count = len(req.audio_urls) placeholder = " ".join(["[Audio]"] * audio_count) @@ -739,13 +740,13 @@ 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 @@ -843,14 +844,14 @@ 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): @@ -884,7 +885,7 @@ def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None: async def _handle_webchat( - event: AstrMessageEvent, req: ProviderRequest, prov: Provider + event: AstrMessageEvent, req: ProviderRequest, prov: Provider, ) -> None: from astrbot.core import db_helper @@ -905,7 +906,7 @@ 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: @@ -913,10 +914,10 @@ 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, ) @@ -925,12 +926,12 @@ 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 @@ -939,7 +940,7 @@ 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): @@ -952,12 +953,12 @@ def _get_compress_provider( def _get_fallback_chat_providers( - provider: Provider, plugin_context: Context, provider_settings: dict + provider: Provider, plugin_context: Context, provider_settings: dict, ) -> list[Provider]: fallback_ids = provider_settings.get("fallback_chat_models", []) if not isinstance(fallback_ids, list): logger.warning( - "fallback_chat_models setting is not a list, skip fallback providers." + "fallback_chat_models setting is not a list, skip fallback providers.", ) return [] provider_id = str(provider.provider_config.get("id", "")) @@ -1025,13 +1026,13 @@ 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) req.image_urls.append(image_path) req.extra_user_content_parts.append( - TextPart(text=f"[Image Attachment: path {image_path}]") + TextPart(text=f"[Image Attachment: path {image_path}]"), ) elif isinstance(comp, Record): audio_path = await comp.convert_to_file_path() @@ -1042,14 +1043,14 @@ async def build_main_agent( file_name = comp.name or os.path.basename(file_path) req.extra_user_content_parts.append( TextPart( - text=f"[File Attachment: name {file_name}, path {file_path}]" - ) + text=f"[File Attachment: name {file_name}, path {file_path}]", + ), ) reply_comps = [ comp for comp in event.message_obj.message if isinstance(comp, Reply) ] quoted_message_settings = _get_quoted_message_parser_settings( - config.provider_settings + config.provider_settings, ) fallback_quoted_image_count = 0 for comp in reply_comps: @@ -1060,7 +1061,7 @@ 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) @@ -1075,15 +1076,15 @@ async def build_main_agent( file_name = reply_comp.name or os.path.basename(file_path) req.extra_user_content_parts.append( TextPart( - text=f"[File Attachment in quoted message: name {file_name}, path {file_path}]" - ) + text=f"[File Attachment in quoted message: name {file_name}, path {file_path}]", + ), ) if not has_embedded_image: 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( config.max_quoted_fallback_images @@ -1156,7 +1157,7 @@ async def build_main_agent( session_id=req.session_id or "", ) _inactivated: set[str] = set( - str(sp.get("inactivated_llm_tools", [], scope="global", scope_id="global")) + str(sp.get("inactivated_llm_tools", [], scope="global", scope_id="global")), ) for _tp in config.tool_providers: _tp_tools = _tp.get_tools(_provider_ctx) @@ -1197,7 +1198,7 @@ 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", @@ -1223,7 +1224,7 @@ async def build_main_agent( enforce_max_turns=config.max_context_length, tool_schema_mode=config.tool_schema_mode, fallback_providers=_get_fallback_chat_providers( - provider, plugin_context, config.provider_settings + provider, plugin_context, config.provider_settings, ), ) if apply_reset: diff --git a/astrbot/core/astr_main_agent_resources.py b/astrbot/core/astr_main_agent_resources.py index cc1115ad8..1da01d9a3 100644 --- a/astrbot/core/astr_main_agent_resources.py +++ b/astrbot/core/astr_main_agent_resources.py @@ -61,11 +61,11 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: query = kwargs.get("query", "") if not query: @@ -130,14 +130,13 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): }, }, "required": ["messages"], - } + }, ) async def _resolve_path_from_sandbox( - self, context: ContextWrapper[AstrAgentContext], path: str + self, context: ContextWrapper[AstrAgentContext], path: str, ) -> tuple[str, bool]: - """ - If the path exists locally, return it directly. + """If the path exists locally, return it directly. Otherwise, check if it exists in the sandbox and download it. bool: indicates whether the file was downloaded from sandbox. @@ -157,7 +156,7 @@ 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}") @@ -169,7 +168,7 @@ 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") @@ -315,6 +314,7 @@ async def retrieve_knowledge_base( Args: umo: Unique message object (session ID) p_ctx: Pipeline context + """ kb_mgr = context.kb_manager config = context.get_config(umo=umo) diff --git a/astrbot/core/backup/constants.py b/astrbot/core/backup/constants.py index 4782b54e5..5ff7eb3ec 100644 --- a/astrbot/core/backup/constants.py +++ b/astrbot/core/backup/constants.py @@ -68,6 +68,7 @@ def get_backup_directories() -> dict[str, str]: Returns: dict: 键为备份文件中的目录名称,值为目录的绝对路径 + """ return { "plugins": get_astrbot_plugin_path(), # 插件本体 diff --git a/astrbot/core/backup/exporter.py b/astrbot/core/backup/exporter.py index 54ccb880b..f7ade85b4 100644 --- a/astrbot/core/backup/exporter.py +++ b/astrbot/core/backup/exporter.py @@ -79,6 +79,7 @@ class AstrBotExporter: Returns: str: 生成的 ZIP 文件路径 + """ if output_dir is None: output_dir = get_astrbot_backups_path() @@ -99,7 +100,7 @@ class AstrBotExporter: await progress_callback("main_db", 0, 100, "正在导出主数据库...") main_data = await self._export_main_database() main_db_json = json.dumps( - main_data, ensure_ascii=False, indent=2, default=str + main_data, ensure_ascii=False, indent=2, default=str, ) zf.writestr("databases/main_db.json", main_db_json) self._add_checksum("databases/main_db.json", main_db_json) @@ -115,17 +116,17 @@ class AstrBotExporter: if self.kb_manager: if progress_callback: await progress_callback( - "kb_metadata", 0, 100, "正在导出知识库元数据..." + "kb_metadata", 0, 100, "正在导出知识库元数据...", ) kb_meta_data = await self._export_kb_metadata() kb_meta_json = json.dumps( - kb_meta_data, ensure_ascii=False, indent=2, default=str + kb_meta_data, ensure_ascii=False, indent=2, default=str, ) zf.writestr("databases/kb_metadata.json", kb_meta_json) self._add_checksum("databases/kb_metadata.json", kb_meta_json) if progress_callback: await progress_callback( - "kb_metadata", 100, 100, "知识库元数据导出完成" + "kb_metadata", 100, 100, "知识库元数据导出完成", ) # 导出每个知识库的文档数据 @@ -141,7 +142,7 @@ class AstrBotExporter: ) doc_data = await self._export_kb_documents(kb_helper) doc_json = json.dumps( - doc_data, ensure_ascii=False, indent=2, default=str + doc_data, ensure_ascii=False, indent=2, default=str, ) doc_path = f"databases/kb_{kb_id}/documents.json" zf.writestr(doc_path, doc_json) @@ -155,7 +156,7 @@ class AstrBotExporter: if progress_callback: await progress_callback( - "kb_documents", total_kbs, total_kbs, "知识库文档导出完成" + "kb_documents", total_kbs, total_kbs, "知识库文档导出完成", ) # 3. 导出配置文件 @@ -163,7 +164,7 @@ 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) @@ -181,7 +182,7 @@ class AstrBotExporter: # 5. 导出插件和其他目录 if progress_callback: await progress_callback( - "directories", 0, 100, "正在导出插件和数据目录..." + "directories", 0, 100, "正在导出插件和数据目录...", ) dir_stats = await self._export_directories(zf) if progress_callback: @@ -219,7 +220,7 @@ class AstrBotExporter: self._model_to_dict(record) for record in records ] logger.debug( - f"导出表 {table_name}: {len(export_data[table_name])} 条记录" + f"导出表 {table_name}: {len(export_data[table_name])} 条记录", ) except Exception as e: logger.warning(f"导出表 {table_name} 失败: {e}") @@ -243,7 +244,7 @@ class AstrBotExporter: self._model_to_dict(record) for record in records ] logger.debug( - f"导出知识库表 {table_name}: {len(export_data[table_name])} 条记录" + f"导出知识库表 {table_name}: {len(export_data[table_name])} 条记录", ) except Exception as e: logger.warning(f"导出知识库表 {table_name} 失败: {e}") @@ -289,7 +290,7 @@ class AstrBotExporter: logger.warning(f"导出 FAISS 索引失败: {e}") async def _export_kb_media_files( - self, zf: zipfile.ZipFile, kb_helper: Any, kb_id: str + self, zf: zipfile.ZipFile, kb_helper: Any, kb_id: str, ) -> None: """导出知识库的多媒体文件""" try: @@ -308,12 +309,13 @@ class AstrBotExporter: logger.warning(f"导出知识库媒体文件失败: {e}") async def _export_directories( - self, zf: zipfile.ZipFile + self, zf: zipfile.ZipFile, ) -> dict[str, dict[str, int]]: """导出插件和其他数据目录 Returns: dict: 每个目录的统计信息 {dir_name: {"files": count, "size": bytes}} + """ stats: dict[str, dict[str, int]] = {} backup_directories = get_backup_directories() @@ -350,7 +352,7 @@ class AstrBotExporter: stats[dir_name] = {"files": file_count, "size": total_size} logger.debug( - f"导出目录 {dir_name}: {file_count} 个文件, {total_size} 字节" + f"导出目录 {dir_name}: {file_count} 个文件, {total_size} 字节", ) except Exception as e: logger.warning(f"导出目录 {dir_path} 失败: {e}") @@ -359,7 +361,7 @@ class AstrBotExporter: return stats async def _export_attachments( - self, zf: zipfile.ZipFile, attachments: list[dict] + self, zf: zipfile.ZipFile, attachments: list[dict], ) -> None: """导出附件文件""" for attachment in attachments: diff --git a/astrbot/core/backup/importer.py b/astrbot/core/backup/importer.py index 20d31c4b0..4b455015b 100644 --- a/astrbot/core/backup/importer.py +++ b/astrbot/core/backup/importer.py @@ -47,6 +47,7 @@ def _get_major_version(version_str: str) -> str: Returns: 主版本字符串,如 "4.9", "4.10" + """ if not version_str: return "0.0" @@ -55,7 +56,7 @@ def _get_major_version(version_str: str) -> str: parts = [p for p in version.split(".") if p] # 过滤空字符串 if len(parts) >= 2: return f"{parts[0]}.{parts[1]}" - elif len(parts) == 1 and parts[0]: + if len(parts) == 1 and parts[0]: return f"{parts[0]}.0" return "0.0" @@ -246,6 +247,7 @@ class AstrBotImporter: Returns: ImportPreCheckResult: 预检查结果 + """ result = ImportPreCheckResult() result.current_version = VERSION @@ -307,6 +309,7 @@ class AstrBotImporter: Returns: dict: {status, can_import, message} + """ if not backup_version: return { @@ -362,6 +365,7 @@ class AstrBotImporter: Returns: ImportResult: 导入结果 + """ result = ImportResult() @@ -465,7 +469,7 @@ class AstrBotImporter: await progress_callback("attachments", 0, 100, "正在导入附件...") attachment_count = await self._import_attachments( - zf, main_data.get("attachments", []) + zf, main_data.get("attachments", []), ) result.imported_files["attachments"] = attachment_count @@ -475,7 +479,7 @@ class AstrBotImporter: # 6. 导入插件和其他目录 if progress_callback: await progress_callback( - "directories", 0, 100, "正在导入插件和数据目录..." + "directories", 0, 100, "正在导入插件和数据目录...", ) dir_stats = await self._import_directories(zf, manifest, result) @@ -516,16 +520,15 @@ class AstrBotImporter: async def _clear_main_db(self) -> None: """清空主数据库所有表""" - async with self.main_db.get_db() as session: - async with session.begin(): - for table_name, model_class in MAIN_DB_MODELS.items(): - try: - await session.execute(delete(model_class)) - logger.debug(f"已清空表 {table_name}") - except Exception as e: - raise DatabaseClearError( - f"清空表 {table_name} 失败: {e}" - ) from e + async with self.main_db.get_db() as session, session.begin(): + for table_name, model_class in MAIN_DB_MODELS.items(): + try: + await session.execute(delete(model_class)) + logger.debug(f"已清空表 {table_name}") + except Exception as e: + raise DatabaseClearError( + f"清空表 {table_name} 失败: {e}", + ) from e async def _clear_kb_data(self) -> None: """清空知识库数据""" @@ -533,14 +536,13 @@ class AstrBotImporter: return # 清空知识库元数据表 - async with self.kb_manager.kb_db.get_db() as session: - async with session.begin(): - for table_name, model_class in KB_METADATA_MODELS.items(): - try: - await session.execute(delete(model_class)) - logger.debug(f"已清空知识库表 {table_name}") - except Exception as e: - logger.warning(f"清空知识库表 {table_name} 失败: {e}") + async with self.kb_manager.kb_db.get_db() as session, session.begin(): + for table_name, model_class in KB_METADATA_MODELS.items(): + try: + await session.execute(delete(model_class)) + logger.debug(f"已清空知识库表 {table_name}") + except Exception as e: + logger.warning(f"清空知识库表 {table_name} 失败: {e}") # 删除知识库文件目录 for kb_id in list(self.kb_manager.kb_insts.keys()): @@ -555,38 +557,37 @@ class AstrBotImporter: self.kb_manager.kb_insts.clear() async def _import_main_database( - self, data: dict[str, list[dict]] + self, data: dict[str, list[dict]], ) -> dict[str, int]: """导入主数据库数据""" imported: dict[str, int] = {} - async with self.main_db.get_db() as session: - async with session.begin(): - for table_name, rows in data.items(): - model_class = MAIN_DB_MODELS.get(table_name) - if not model_class: - logger.warning(f"未知的表: {table_name}") - continue - normalized_rows = self._preprocess_main_table_rows(table_name, rows) + async with self.main_db.get_db() as session, session.begin(): + for table_name, rows in data.items(): + model_class = MAIN_DB_MODELS.get(table_name) + if not model_class: + logger.warning(f"未知的表: {table_name}") + continue + normalized_rows = self._preprocess_main_table_rows(table_name, rows) - count = 0 - for row in normalized_rows: - try: - # 转换 datetime 字符串为 datetime 对象 - row = self._convert_datetime_fields(row, model_class) - obj = model_class(**row) - session.add(obj) - count += 1 - except Exception as e: - logger.warning(f"导入记录到 {table_name} 失败: {e}") + count = 0 + for row in normalized_rows: + try: + # 转换 datetime 字符串为 datetime 对象 + row = self._convert_datetime_fields(row, model_class) + obj = model_class(**row) + session.add(obj) + count += 1 + except Exception as e: + logger.warning(f"导入记录到 {table_name} 失败: {e}") - imported[table_name] = count - logger.debug(f"导入表 {table_name}: {count} 条记录") + imported[table_name] = count + logger.debug(f"导入表 {table_name}: {count} 条记录") return imported def _preprocess_main_table_rows( - self, table_name: str, rows: list[dict[str, Any]] + self, table_name: str, rows: list[dict[str, Any]], ) -> list[dict[str, Any]]: if table_name == "platform_stats": normalized_rows = self._merge_platform_stats_rows(rows) @@ -601,7 +602,7 @@ class AstrBotImporter: return rows def _merge_platform_stats_rows( - self, rows: list[dict[str, Any]] + self, rows: list[dict[str, Any]], ) -> list[dict[str, Any]]: """Merge duplicate platform_stats rows by normalized timestamp/platform key. @@ -609,6 +610,7 @@ class AstrBotImporter: - Invalid/empty timestamps are kept as distinct rows to avoid accidental merging. - Non-string platform_id/platform_type are kept as distinct rows. - Invalid count warnings are rate-limited per function invocation. + """ merged: dict[tuple[str, str, str], dict[str, Any]] = {} result: list[dict[str, Any]] = [] @@ -708,24 +710,23 @@ class AstrBotImporter: return # 1. 导入知识库元数据 - async with self.kb_manager.kb_db.get_db() as session: - async with session.begin(): - for table_name, rows in kb_meta_data.items(): - model_class = KB_METADATA_MODELS.get(table_name) - if not model_class: - continue + async with self.kb_manager.kb_db.get_db() as session, session.begin(): + for table_name, rows in kb_meta_data.items(): + model_class = KB_METADATA_MODELS.get(table_name) + if not model_class: + continue - count = 0 - for row in rows: - try: - row = self._convert_datetime_fields(row, model_class) - obj = model_class(**row) - session.add(obj) - count += 1 - except Exception as e: - logger.warning(f"导入知识库记录到 {table_name} 失败: {e}") + count = 0 + for row in rows: + try: + row = self._convert_datetime_fields(row, model_class) + obj = model_class(**row) + session.add(obj) + count += 1 + except Exception as e: + logger.warning(f"导入知识库记录到 {table_name} 失败: {e}") - result.imported_tables[f"kb_{table_name}"] = count + result.imported_tables[f"kb_{table_name}"] = count # 2. 导入每个知识库的文档和文件 for kb_data in kb_meta_data.get("knowledge_bases", []): @@ -769,7 +770,7 @@ 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() @@ -835,7 +836,7 @@ 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() @@ -862,6 +863,7 @@ class AstrBotImporter: Returns: dict: 每个目录导入的文件数量 + """ dir_stats: dict[str, int] = {} @@ -916,7 +918,7 @@ 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/base.py b/astrbot/core/computer/booters/base.py index 929127ff4..49a1713c0 100644 --- a/astrbot/core/computer/booters/base.py +++ b/astrbot/core/computer/booters/base.py @@ -74,7 +74,8 @@ class ComputerBooter(abc.ABC): def get_tools(self) -> list[ToolSchema]: """Capability-filtered tool list (post-boot). - Defaults to get_default_tools().""" + Defaults to get_default_tools(). + """ return self.__class__.get_default_tools() @classmethod diff --git a/astrbot/core/computer/booters/bay_manager.py b/astrbot/core/computer/booters/bay_manager.py index 96370dc4c..c5c211c7c 100644 --- a/astrbot/core/computer/booters/bay_manager.py +++ b/astrbot/core/computer/booters/bay_manager.py @@ -60,7 +60,7 @@ class BayContainerManager: raise RuntimeError( "Failed to connect to Docker daemon. " "Ensure Docker is installed and running, or configure " - "an explicit Bay endpoint instead of auto-start mode." + "an explicit Bay endpoint instead of auto-start mode.", ) from exc # 1. Look for an existing managed container @@ -72,13 +72,12 @@ class BayContainerManager: logger.info("[BayManager] Reusing existing Bay container: %s", cid) self._container = await self._docker.containers.get(existing["Id"]) return f"http://127.0.0.1:{self._host_port}" - else: - # Container exists but stopped — restart it - logger.info("[BayManager] Restarting stopped Bay container") - container = await self._docker.containers.get(existing["Id"]) - await container.start() - self._container = container - return f"http://127.0.0.1:{self._host_port}" + # Container exists but stopped — restart it + logger.info("[BayManager] Restarting stopped Bay container") + container = await self._docker.containers.get(existing["Id"]) + await container.start() + self._container = container + return f"http://127.0.0.1:{self._host_port}" # 2. Pull image if needed await self._pull_image_if_needed() @@ -111,7 +110,7 @@ class BayContainerManager: }, } self._container = await self._docker.containers.create_or_replace( - BAY_CONTAINER_NAME, config + BAY_CONTAINER_NAME, config, ) await self._container.start() logger.info("[BayManager] Bay container started: %s", BAY_CONTAINER_NAME) @@ -129,7 +128,7 @@ class BayContainerManager: while loop.time() < deadline: try: async with session.get( - url, timeout=aiohttp.ClientTimeout(total=3) + url, timeout=aiohttp.ClientTimeout(total=3), ) as resp: if resp.status == 200: logger.info("[BayManager] Bay is healthy") @@ -141,7 +140,7 @@ class BayContainerManager: await asyncio.sleep(HEALTH_POLL_INTERVAL_S) raise TimeoutError( - f"Bay did not become healthy within {timeout}s (last error: {last_error})" + f"Bay did not become healthy within {timeout}s (last error: {last_error})", ) async def read_credentials(self) -> str: @@ -202,7 +201,7 @@ class BayContainerManager: return api_key except Exception as exc: logger.debug( - "[BayManager] Failed to read credentials from container: %s", exc + "[BayManager] Failed to read credentials from container: %s", exc, ) return "" diff --git a/astrbot/core/computer/booters/boxlite.py b/astrbot/core/computer/booters/boxlite.py index dfb720f9b..447810e02 100644 --- a/astrbot/core/computer/booters/boxlite.py +++ b/astrbot/core/computer/booters/boxlite.py @@ -46,11 +46,10 @@ class MockShipyardSandboxClient: ) as response: if response.status == 200: return await response.json() - else: - error_text = await response.text() - raise Exception( - f"Failed to exec operation: {response.status} {error_text}" - ) + error_text = await response.text() + raise Exception( + f"Failed to exec operation: {response.status} {error_text}", + ) async def upload_file(self, path: str, remote_path: str) -> dict: """Upload a file to the sandbox""" @@ -85,18 +84,17 @@ class MockShipyardSandboxClient: "message": "File uploaded successfully", "file_path": remote_path, } - else: - error_text = await response.text() - logger.warning( - "[Computer] file_upload_failed booter=boxlite error=http_status status=%s remote_path=%s", - response.status, - remote_path, - ) - return { - "success": False, - "error": f"Server returned {response.status}: {error_text}", - "message": "File upload failed", - } + error_text = await response.text() + logger.warning( + "[Computer] file_upload_failed booter=boxlite error=http_status status=%s remote_path=%s", + response.status, + remote_path, + ) + return { + "success": False, + "error": f"Server returned {response.status}: {error_text}", + "message": "File upload failed", + } except aiohttp.ClientError as e: logger.error("[Computer] file_upload_failed booter=boxlite error=%s", e) @@ -127,7 +125,7 @@ class MockShipyardSandboxClient: } except Exception as exc: logger.exception( - "[Computer] file_upload_failed booter=boxlite error=unexpected" + "[Computer] file_upload_failed booter=boxlite error=unexpected", ) return { "success": False, @@ -186,7 +184,7 @@ class BoxliteBooter(ComputerBooter): { "host_port": random_port, "guest_port": 8123, - } + }, ], ) await self.box.start() @@ -196,7 +194,7 @@ class BoxliteBooter(ComputerBooter): self.box.id, ) self.mocked = MockShipyardSandboxClient( - sb_url=f"http://127.0.0.1:{random_port}" + sb_url=f"http://127.0.0.1:{random_port}", ) self._fs = ShipyardFileSystemComponent( client=self.mocked, # type: ignore[arg-type] diff --git a/astrbot/core/computer/booters/bwrap.py b/astrbot/core/computer/booters/bwrap.py index fbf2d40d6..548835b55 100644 --- a/astrbot/core/computer/booters/bwrap.py +++ b/astrbot/core/computer/booters/bwrap.py @@ -102,7 +102,7 @@ def build_bwrap_cmd(config: BwrapConfig, script_cmd: list[str]) -> list[str]: "--bind", config.workspace_dir, config.workspace_dir, - ] + ], ) cmd.extend(["--"]) @@ -147,7 +147,7 @@ class BwrapShellComponent(ShellComponent): result = subprocess.run( bwrap_cmd, - cwd=working_dir, + check=False, cwd=working_dir, env=run_env, timeout=timeout, capture_output=True, @@ -174,12 +174,12 @@ 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, - timeout=timeout, + check=False, timeout=timeout, capture_output=True, text=True, ) @@ -221,7 +221,7 @@ 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 +238,7 @@ 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 +260,7 @@ 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 +274,7 @@ 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 +307,7 @@ 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) @@ -321,19 +321,19 @@ class BwrapBooter(ComputerBooter): self._shell = BwrapShellComponent(self.config) if not await self.available(): raise RuntimeError( - "BubbleWrap sandbox unavailable on current machine for no bwrap executable." + "BubbleWrap sandbox unavailable on current machine for no bwrap executable.", ) test_shl = await self._shell.exec(command="ls > /dev/null") if test_shl["exit_code"] != 0: raise RuntimeError( """BubbleWrap sandbox fails to exec test shell command "ls > /dev/null" with stderr: -{}""".format(test_shl["stderr"]) +{}""".format(test_shl["stderr"]), ) test_py = await self._python.exec(code="print('Yes')") if test_py["exit_code"] != 0: raise RuntimeError( """BubbleWrap sandbox fails to exec test python code "print('Yes')" with stderr: -{}""".format(test_py["stderr"]) +{}""".format(test_py["stderr"]), ) async def shutdown(self) -> None: @@ -342,7 +342,7 @@ 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 f547e096c..2ad2a8773 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -133,7 +133,7 @@ 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, - shell=shell, + check=False, shell=shell, cwd=working_dir, env=run_env, timeout=timeout, @@ -161,7 +161,7 @@ class LocalPythonComponent(PythonComponent): try: result = subprocess.run( [os.environ.get("PYTHON", sys.executable), "-c", code], - timeout=timeout, + check=False, timeout=timeout, capture_output=True, text=True, ) @@ -171,14 +171,14 @@ class LocalPythonComponent(PythonComponent): "data": { "output": {"text": stdout, "images": []}, "error": stderr, - } + }, } except subprocess.TimeoutExpired: return { "data": { "output": {"text": "", "images": []}, "error": "Execution timed out.", - } + }, } return await asyncio.to_thread(_run) @@ -187,7 +187,7 @@ class LocalPythonComponent(PythonComponent): @dataclass class LocalFileSystemComponent(FileSystemComponent): async def create_file( - self, path: str, content: str = "", mode: int = 0o644 + self, path: str, content: str = "", mode: int = 0o644, ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -213,7 +213,7 @@ class LocalFileSystemComponent(FileSystemComponent): return await asyncio.to_thread(_run) async def write_file( - self, path: str, content: str, mode: str = "w", encoding: str = "utf-8" + self, path: str, content: str, mode: str = "w", encoding: str = "utf-8", ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -236,7 +236,7 @@ class LocalFileSystemComponent(FileSystemComponent): return await asyncio.to_thread(_run) async def list_dir( - self, path: str = ".", show_hidden: bool = False + self, path: str = ".", show_hidden: bool = False, ) -> dict[str, Any]: def _run() -> dict[str, Any]: abs_path = _ensure_safe_path(path) @@ -274,12 +274,12 @@ class LocalBooter(ComputerBooter): async def upload_file(self, path: str, file_name: str) -> dict: raise NotImplementedError( - "LocalBooter does not support upload_file operation. Use shell instead." + "LocalBooter does not support upload_file operation. Use shell instead.", ) async def download_file(self, remote_path: str, local_path: str) -> None: raise NotImplementedError( - "LocalBooter does not support download_file operation. Use shell instead." + "LocalBooter does not support download_file operation. Use shell instead.", ) async def available(self) -> bool: diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index e9470bb07..f77b5576a 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -49,7 +49,7 @@ class ShipyardBooter(ComputerBooter): session_num: int = 10, ) -> None: self._sandbox_client = ShipyardClient( - endpoint_url=endpoint_url, access_token=access_token + endpoint_url=endpoint_url, access_token=access_token, ) self._ttl = ttl self._session_num = session_num diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index 863cc032c..f202cce07 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -89,7 +89,7 @@ class NeoShellComponent(ShellComponent): run_command = command if env: env_prefix = " ".join( - (f"{k}={shlex.quote(str(v))}" for k, v in sorted(env.items())) + (f"{k}={shlex.quote(str(v))}" for k, v in sorted(env.items())), ) run_command = f"{env_prefix} {run_command}" if background: @@ -132,7 +132,7 @@ 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 +144,7 @@ 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 +156,7 @@ 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,25 +301,25 @@ 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: raise ValueError( - "Bay container started but credentials could not be read. Ensure Bay generated credentials.json, or set access_token manually." + "Bay container started but credentials could not be read. Ensure Bay generated credentials.json, or set access_token manually.", ) raise ValueError( - "Shipyard Neo sandbox configuration is incomplete. Set endpoint (default http://127.0.0.1:8114) and access token, or ensure Bay's credentials.json is accessible for auto-discovery." + "Shipyard Neo sandbox configuration is incomplete. Set endpoint (default http://127.0.0.1:8114) and access token, or ensure Bay's credentials.json is accessible for auto-discovery.", ) 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 +351,7 @@ 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 +434,7 @@ 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 2afc4ac23..fb684cebd 100644 --- a/astrbot/core/computer/computer_client.py +++ b/astrbot/core/computer/computer_client.py @@ -48,6 +48,7 @@ def _discover_bay_credentials(endpoint: str) -> str: Returns: API key string, or empty string if not found. + """ candidates: list[Path] = [] @@ -357,7 +358,7 @@ 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") @@ -370,7 +371,7 @@ 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}") @@ -415,7 +416,7 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None: logger.info("[Computer] sandbox_sync phase=upload status=done") else: logger.info( - "[Computer] sandbox_sync phase=upload status=skipped reason=no_local_skills" + "[Computer] sandbox_sync phase=upload status=skipped reason=no_local_skills", ) await booter.shell.exec(f"rm -f {SANDBOX_SKILLS_ROOT}/skills.zip") @@ -449,7 +450,7 @@ async def get_booter( runtime = config.get("provider_settings", {}).get("computer_use_runtime", "local") if runtime == "local": return get_local_booter() - elif runtime == "none": + if runtime == "none": raise RuntimeError("Sandbox runtime is disabled by configuration.") sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {}) @@ -476,7 +477,7 @@ async def get_booter( max_sessions = sandbox_cfg.get("shipyard_max_sessions", 10) client = ShipyardBooter( - endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions + endpoint_url=ep, access_token=token, ttl=ttl, session_num=max_sessions, ) elif booter_type == "shipyard_neo": from .booters.shipyard_neo import ShipyardNeoBooter @@ -491,7 +492,7 @@ async def get_booter( token = _discover_bay_credentials(ep) logger.info( - f"[Computer] Shipyard Neo config: endpoint={ep}, profile={profile}, ttl={ttl}" + f"[Computer] Shipyard Neo config: endpoint={ep}, profile={profile}, ttl={ttl}", ) client = ShipyardNeoBooter( endpoint_url=ep, @@ -563,11 +564,11 @@ def _get_booter_class(booter_type: str) -> type[ComputerBooter] | None: from .booters.shipyard import ShipyardBooter return ShipyardBooter - elif booter_type == BOOTER_SHIPYARD_NEO: + if booter_type == BOOTER_SHIPYARD_NEO: from .booters.shipyard_neo import ShipyardNeoBooter return ShipyardNeoBooter - elif booter_type == BOOTER_BOXLITE: + if booter_type == BOOTER_BOXLITE: from .booters.boxlite import BoxliteBooter return BoxliteBooter diff --git a/astrbot/core/computer/olayer/browser.py b/astrbot/core/computer/olayer/browser.py index 5bc40a446..857367698 100644 --- a/astrbot/core/computer/olayer/browser.py +++ b/astrbot/core/computer/olayer/browser.py @@ -1,5 +1,4 @@ -""" -Browser automation component +"""Browser automation component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/olayer/filesystem.py b/astrbot/core/computer/olayer/filesystem.py index 21f36d111..c50173803 100644 --- a/astrbot/core/computer/olayer/filesystem.py +++ b/astrbot/core/computer/olayer/filesystem.py @@ -1,5 +1,4 @@ -""" -File system component +"""File system component """ from typing import Any, Protocol @@ -7,7 +6,7 @@ from typing import Any, Protocol class FileSystemComponent(Protocol): async def create_file( - self, path: str, content: str = "", mode: int = 0o644 + self, path: str, content: str = "", mode: int = 0o644, ) -> dict[str, Any]: """Create a file with the specified content""" ... @@ -17,7 +16,7 @@ class FileSystemComponent(Protocol): ... async def write_file( - self, path: str, content: str, mode: str = "w", encoding: str = "utf-8" + self, path: str, content: str, mode: str = "w", encoding: str = "utf-8", ) -> dict[str, Any]: """Write content to file""" ... @@ -27,7 +26,7 @@ class FileSystemComponent(Protocol): ... async def list_dir( - self, path: str = ".", show_hidden: bool = False + self, path: str = ".", show_hidden: bool = False, ) -> dict[str, Any]: """List directory contents""" ... diff --git a/astrbot/core/computer/olayer/python.py b/astrbot/core/computer/olayer/python.py index 625504146..fbcf397c4 100644 --- a/astrbot/core/computer/olayer/python.py +++ b/astrbot/core/computer/olayer/python.py @@ -1,5 +1,4 @@ -""" -Python/IPython component +"""Python/IPython component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/olayer/shell.py b/astrbot/core/computer/olayer/shell.py index df2263b65..82eb9845c 100644 --- a/astrbot/core/computer/olayer/shell.py +++ b/astrbot/core/computer/olayer/shell.py @@ -1,5 +1,4 @@ -""" -Shell component +"""Shell component """ from typing import Any, Protocol diff --git a/astrbot/core/computer/tools/browser.py b/astrbot/core/computer/tools/browser.py index dfbf541c4..3aace4a3a 100644 --- a/astrbot/core/computer/tools/browser.py +++ b/astrbot/core/computer/tools/browser.py @@ -24,7 +24,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A if browser is None: raise RuntimeError( "Current sandbox booter does not support browser capability. " - "Please switch to shipyard_neo." + "Please switch to shipyard_neo.", ) return browser @@ -56,7 +56,7 @@ class BrowserExecTool(FunctionTool): }, }, "required": ["cmd"], - } + }, ) async def call( # type: ignore[override] @@ -119,7 +119,7 @@ class BrowserBatchExecTool(FunctionTool): }, }, "required": ["commands"], - } + }, ) async def call( # type: ignore[override] @@ -168,7 +168,7 @@ class RunBrowserSkillTool(FunctionTool): "tags": {"type": "string"}, }, "required": ["skill_key"], - } + }, ) async def call( # type: ignore[override] diff --git a/astrbot/core/computer/tools/fs.py b/astrbot/core/computer/tools/fs.py index e8fec58bb..7526137e0 100644 --- a/astrbot/core/computer/tools/fs.py +++ b/astrbot/core/computer/tools/fs.py @@ -102,7 +102,7 @@ class FileUploadTool(FunctionTool): # }, }, "required": ["local_path"], - } + }, ) async def call( # type: ignore[override] @@ -167,7 +167,7 @@ class FileDownloadTool(FunctionTool): }, }, "required": ["remote_path"], - } + }, ) async def call( # type: ignore[override] @@ -186,7 +186,7 @@ class FileDownloadTool(FunctionTool): name = os.path.basename(remote_path) local_path = os.path.join( - get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" + get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}", ) # Download file from sandbox @@ -197,7 +197,7 @@ class FileDownloadTool(FunctionTool): try: name = os.path.basename(local_path) await context.context.event.send( - MessageChain(chain=[File(name=name, file=local_path)]) + MessageChain(chain=[File(name=name, file=local_path)]), ) except Exception as e: logger.error(f"Error sending file message: {e}") diff --git a/astrbot/core/computer/tools/neo_skills.py b/astrbot/core/computer/tools/neo_skills.py index f87f6155f..2cfa647dc 100644 --- a/astrbot/core/computer/tools/neo_skills.py +++ b/astrbot/core/computer/tools/neo_skills.py @@ -39,7 +39,7 @@ async def _get_neo_context( if client is None or sandbox is None: raise RuntimeError( "Current sandbox booter does not support Neo skill lifecycle APIs. " - "Please switch to shipyard_neo." + "Please switch to shipyard_neo.", ) return client, sandbox @@ -81,7 +81,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase): "has_description": {"type": "boolean", "default": False}, }, "required": [], - } + }, ) async def call( # type: ignore[override] @@ -124,7 +124,7 @@ class AnnotateExecutionTool(NeoSkillToolBase): "notes": {"type": "string"}, }, "required": ["execution_id"], - } + }, ) async def call( # type: ignore[override] @@ -175,7 +175,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase): }, }, "required": ["payload"], - } + }, ) async def call( # type: ignore[override] @@ -205,7 +205,7 @@ class GetSkillPayloadTool(NeoSkillToolBase): "payload_ref": {"type": "string"}, }, "required": ["payload_ref"], - } + }, ) async def call( # type: ignore[override] @@ -250,7 +250,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase): }, }, "required": ["skill_key", "source_execution_ids"], - } + }, ) async def call( # type: ignore[override] @@ -287,7 +287,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase): "offset": {"type": "integer", "default": 0}, }, "required": [], - } + }, ) async def call( # type: ignore[override] @@ -325,7 +325,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase): "report": {"type": "string"}, }, "required": ["candidate_id", "passed"], - } + }, ) async def call( # type: ignore[override] @@ -377,7 +377,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): }, }, "required": ["candidate_id"], - } + }, ) async def call( # type: ignore[override] @@ -414,7 +414,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase): "release": result.get("release"), "sync": result.get("sync"), "rollback": result.get("rollback"), - } + }, ) except Exception as e: return f"Error promoting skill candidate: {e!s}" @@ -435,7 +435,7 @@ class ListSkillReleasesTool(NeoSkillToolBase): "offset": {"type": "integer", "default": 0}, }, "required": [], - } + }, ) async def call( # type: ignore[override] @@ -471,7 +471,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase): "release_id": {"type": "string"}, }, "required": ["release_id"], - } + }, ) async def call( # type: ignore[override] @@ -501,7 +501,7 @@ class SyncSkillReleaseTool(NeoSkillToolBase): "require_stable": {"type": "boolean", "default": True}, }, "required": [], - } + }, ) async def call( # type: ignore[override] diff --git a/astrbot/core/computer/tools/permissions.py b/astrbot/core/computer/tools/permissions.py index 489f485f9..e8287e423 100644 --- a/astrbot/core/computer/tools/permissions.py +++ b/astrbot/core/computer/tools/permissions.py @@ -3,10 +3,10 @@ from astrbot.core.astr_agent_context import AstrAgentContext def check_admin_permission( - context: ContextWrapper[AstrAgentContext], operation_name: str + context: ContextWrapper[AstrAgentContext], operation_name: str, ) -> str | None: cfg = context.context.context.get_config( - umo=context.context.event.unified_msg_origin + umo=context.context.event.unified_msg_origin, ) provider_settings = cfg.get("provider_settings", {}) require_admin = provider_settings.get("computer_use_require_admin", True) diff --git a/astrbot/core/computer/tools/python.py b/astrbot/core/computer/tools/python.py index 0564da3d7..f3e828aea 100644 --- a/astrbot/core/computer/tools/python.py +++ b/astrbot/core/computer/tools/python.py @@ -46,8 +46,8 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult for img in images: resp.content.append( mcp.types.ImageContent( - type="image", data=img["image/png"], mimeType="image/png" - ) + type="image", data=img["image/png"], mimeType="image/png", + ), ) if event.get_platform_name() == "webchat": @@ -68,7 +68,7 @@ class PythonTool(FunctionTool): parameters: dict = field(default_factory=lambda: param_schema) async def call( # 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 +94,7 @@ 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/computer/tools/shell.py b/astrbot/core/computer/tools/shell.py index b5009d30f..657110902 100644 --- a/astrbot/core/computer/tools/shell.py +++ b/astrbot/core/computer/tools/shell.py @@ -35,7 +35,7 @@ class ExecuteShellTool(FunctionTool): }, }, "required": ["command"], - } + }, ) is_local: bool = False @@ -61,4 +61,4 @@ class ExecuteShellTool(FunctionTool): result = await sb.shell.exec(command, background=background, env=env) return json.dumps(result) except Exception as e: - return f"Error executing command: {str(e)}" + return f"Error executing command: {e!s}" diff --git a/astrbot/core/config/astrbot_config.py b/astrbot/core/config/astrbot_config.py index 89ad754fb..7ef3942a0 100644 --- a/astrbot/core/config/astrbot_config.py +++ b/astrbot/core/config/astrbot_config.py @@ -57,8 +57,7 @@ class AstrBotConfig(dict): with open(config_path, encoding="utf-8-sig") as f: conf_str = f.read() # Handle UTF-8 BOM if present - if conf_str.startswith("\ufeff"): - conf_str = conf_str[1:] + conf_str = conf_str.removeprefix("\ufeff") conf = json.loads(conf_str) # 检查配置完整性,并插入 diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py index e1ae41670..1d9c4d1a0 100644 --- a/astrbot/core/config/i18n_utils.py +++ b/astrbot/core/config/i18n_utils.py @@ -1,5 +1,4 @@ -""" -配置元数据国际化工具 +"""配置元数据国际化工具 提供配置元数据的国际化键转换功能 """ @@ -21,8 +20,7 @@ class ConfigMetadataI18n: @staticmethod def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str: - """ - 生成国际化键 + """生成国际化键 Args: group: 配置组,如 'ai_group', 'platform_group' @@ -32,27 +30,27 @@ class ConfigMetadataI18n: Returns: 国际化键,格式如: 'ai_group.agent_runner.enable.description' + """ if field: return f"{group}.{section}.{field}.{attr}" - else: - return f"{group}.{section}.{attr}" + return f"{group}.{section}.{attr}" @staticmethod def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, I18nGroup]: - """ - 将配置元数据转换为使用国际化键 + """将配置元数据转换为使用国际化键 Args: metadata: 原始配置元数据字典 Returns: 使用国际化键的配置元数据字典 + """ 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] = {} @@ -84,7 +82,7 @@ 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") @@ -132,7 +130,7 @@ 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 d67cce624..b60cd6e1d 100644 --- a/astrbot/core/conversation_mgr.py +++ b/astrbot/core/conversation_mgr.py @@ -110,7 +110,7 @@ class ConversationManager: return conv.conversation_id async def switch_conversation( - self, unified_msg_origin: str, conversation_id: str + self, unified_msg_origin: str, conversation_id: str, ) -> None: """切换会话的对话 @@ -337,7 +337,6 @@ class ConversationManager: conversation_id: str | None = None, ) -> None: """Clear the conversation-specific persona override and fall back to default.""" - await self.update_conversation( unified_msg_origin=unified_msg_origin, conversation_id=conversation_id, @@ -359,6 +358,7 @@ class ConversationManager: Raises: Exception: If the conversation with the given ID is not found + """ conv = await self.db.get_conversation_by_id(cid=cid) if not conv: diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index f100cc2c9..e8eb9fc41 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -346,7 +346,7 @@ class AstrBotCoreLifecycle: logger.info("AstrBot v" + VERSION) if os.environ.get("TESTING", ""): LogManager.configure_logger( - logger, self.astrbot_config, override_level="DEBUG" + logger, self.astrbot_config, override_level="DEBUG", ) LogManager.configure_trace_logger(self.astrbot_config) else: diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index a045baff7..c021b1cdd 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -77,7 +77,7 @@ class CronJobManager: ) -> CronJob: if (cron_expression is None) == (interval_seconds is None): raise ValueError( - "cron_expression and interval_seconds must have exactly one value" + "cron_expression and interval_seconds must have exactly one value", ) payload_data = dict(payload or {}) if interval_seconds is not None: @@ -184,7 +184,7 @@ 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,8 +196,8 @@ class CronJobManager: ) asyncio.create_task( self.db.update_cron_job( - job.job_id, next_run_time=self._get_next_run_time(job.job_id) - ) + job.job_id, next_run_time=self._get_next_run_time(job.job_id), + ), ) except Exception as e: logger.error(f"Failed to schedule cron job {job.job_id}: {e!s}") @@ -212,7 +212,7 @@ class CronJobManager: return start_time = datetime.now(timezone.utc) await self.db.update_cron_job( - job_id, status="running", last_run_at=start_time, last_error=None + job_id, status="running", last_run_at=start_time, last_error=None, ) status = "completed" last_error = None @@ -270,11 +270,11 @@ 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 +317,7 @@ 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, @@ -338,14 +338,14 @@ class CronJobManager: ) cron_job_str = json.dumps(extras.get("cron_job", {}), ensure_ascii=False) req.system_prompt += PROACTIVE_AGENT_CRON_WOKE_SYSTEM_PROMPT.format( - cron_job=cron_job_str + cron_job=cron_job_str, ) req.prompt = CRON_TASK_WOKE_USER_PROMPT if not req.func_tool: 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 dde991bbf..42dc6cf50 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -398,6 +398,7 @@ class BaseDatabase(abc.ABC): custom_error_message: Optional persona-level fallback error message folder_id: Optional folder ID to place the persona in (None means root) sort_order: Sort order within the folder (default 0) + """ ... @@ -451,7 +452,7 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_persona_folders( - self, parent_id: str | None = None + self, parent_id: str | None = None, ) -> list[PersonaFolder]: """Get all persona folders, optionally filtered by parent_id.""" ... @@ -480,14 +481,14 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """Move a persona to a folder (or root if folder_id is None).""" ... @abc.abstractmethod async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """Get all personas in a specific folder.""" ... @@ -504,6 +505,7 @@ class BaseDatabase(abc.ABC): - id: The persona_id or folder_id - type: Either "persona" or "folder" - sort_order: The new sort_order value + """ ... @@ -719,14 +721,14 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_platform_session_by_id( - self, session_id: str + self, session_id: str, ) -> PlatformSession | None: """Get a Platform session by its ID.""" ... @abc.abstractmethod async def get_platform_sessions_by_ids( - self, session_ids: list[str] + self, session_ids: list[str], ) -> list[PlatformSession]: """Get platform sessions by IDs.""" ... @@ -758,6 +760,7 @@ class BaseDatabase(abc.ABC): Returns: tuple[list[dict], int]: (sessions_with_project_info, total_count) + """ ... @@ -847,7 +850,7 @@ class BaseDatabase(abc.ABC): @abc.abstractmethod async def get_project_by_session( - self, session_id: str, creator: str + self, session_id: str, creator: str, ) -> ChatUIProject | None: """Get the project that a session belongs to.""" ... diff --git a/astrbot/core/db/migration/migra_3_to_4.py b/astrbot/core/db/migration/migra_3_to_4.py index a47b29b39..a34e2e00a 100644 --- a/astrbot/core/db/migration/migra_3_to_4.py +++ b/astrbot/core/db/migration/migra_3_to_4.py @@ -17,7 +17,7 @@ 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 +26,7 @@ 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,63 +35,62 @@ 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") + 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: - async with dbsession.begin(): - for idx, conversation in enumerate(conversations): - if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: - progress = int((idx + 1) / total_cnt * 100) - if progress % 10 == 0: - logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") - try: - conv = db_helper_v3.get_conversation_by_user_id( - user_id=conversation.get("user_id", "unknown"), - cid=conversation.get("cid", "unknown"), - ) - if not conv: - logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。" - ) - continue - if ":" not in conv.user_id: - continue - session = MessageSesion.from_str(session_str=conv.user_id) - platform_id = get_platform_id( - platform_id_map, session.platform_name - ) - session.platform_id = platform_id - conv_v2 = ConversationV2( - user_id=str(session), - content=json.loads(conv.history) if conv.history else [], - platform_id=platform_id, - title=conv.title, - persona_id=conv.persona_id, - conversation_id=conv.cid, - created_at=datetime.datetime.fromtimestamp(conv.created_at), - updated_at=datetime.datetime.fromtimestamp(conv.updated_at), - ) - dbsession.add(conv_v2) - except Exception as e: - logger.error( - f"迁移旧会话 {conversation.get('cid', 'unknown')} 失败: {e}", - exc_info=True, + async with db_helper.get_db() as dbsession, dbsession.begin(): + for idx, conversation in enumerate(conversations): + if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: + progress = int((idx + 1) / total_cnt * 100) + if progress % 10 == 0: + logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") + try: + conv = db_helper_v3.get_conversation_by_user_id( + user_id=conversation.get("user_id", "unknown"), + cid=conversation.get("cid", "unknown"), + ) + if not conv: + logger.info( + f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", ) + continue + if ":" not in conv.user_id: + continue + session = MessageSesion.from_str(session_str=conv.user_id) + platform_id = get_platform_id( + platform_id_map, session.platform_name, + ) + session.platform_id = platform_id + conv_v2 = ConversationV2( + user_id=str(session), + content=json.loads(conv.history) if conv.history else [], + platform_id=platform_id, + title=conv.title, + persona_id=conv.persona_id, + conversation_id=conv.cid, + created_at=datetime.datetime.fromtimestamp(conv.created_at), + updated_at=datetime.datetime.fromtimestamp(conv.updated_at), + ) + dbsession.add(conv_v2) + except Exception as e: + logger.error( + f"迁移旧会话 {conversation.get('cid', 'unknown')} 失败: {e}", + exc_info=True, + ) logger.info(f"成功迁移 {total_cnt} 条旧的会话数据到新表。") 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") + db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), ) secs_from_2023_4_10_to_now = ( datetime.datetime.now(datetime.timezone.utc) @@ -127,19 +126,19 @@ 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( text( - "\n INSERT INTO platform_stats (timestamp, platform_id, platform_type, count)\n VALUES (:timestamp, :platform_id, :platform_type, :count)\n ON CONFLICT(timestamp, platform_id, platform_type) DO UPDATE SET\n count = platform_stats.count + EXCLUDED.count\n " + "\n INSERT INTO platform_stats (timestamp, platform_id, platform_type, count)\n VALUES (:timestamp, :platform_id, :platform_type, :count)\n ON CONFLICT(timestamp, platform_id, platform_type) DO UPDATE SET\n count = platform_stats.count + EXCLUDED.count\n ", ), { "timestamp": datetime.datetime.fromtimestamp( - bucket_end, tz=datetime.timezone.utc + bucket_end, tz=datetime.timezone.utc, ), "platform_id": platform_id, "platform_type": platform_type, @@ -155,57 +154,56 @@ 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") + 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: - async with dbsession.begin(): - for idx, conversation in enumerate(conversations): - if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: - progress = int((idx + 1) / total_cnt * 100) - if progress % 10 == 0: - logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") - try: - conv = db_helper_v3.get_conversation_by_user_id( - user_id=conversation.get("user_id", "unknown"), - cid=conversation.get("cid", "unknown"), + async with db_helper.get_db() as dbsession, dbsession.begin(): + for idx, conversation in enumerate(conversations): + if total_cnt > 0 and (idx + 1) % max(1, total_cnt // 10) == 0: + progress = int((idx + 1) / total_cnt * 100) + if progress % 10 == 0: + logger.info(f"进度: {progress}% ({idx + 1}/{total_cnt})") + try: + conv = db_helper_v3.get_conversation_by_user_id( + user_id=conversation.get("user_id", "unknown"), + cid=conversation.get("cid", "unknown"), + ) + if not conv: + logger.info( + f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。", ) - if not conv: - logger.info( - f"未找到该条旧会话对应的具体数据: {conversation}, 跳过。" - ) - continue - if ":" in conv.user_id: - continue - platform_id = "webchat" - history = json.loads(conv.history) if conv.history else [] - for msg in history: - type_ = msg.get("type") - new_history = PlatformMessageHistory( - platform_id=platform_id, - user_id=conv.cid, - content=msg, - sender_id=type_, - sender_name=type_, - ) - dbsession.add(new_history) - except Exception: - logger.error( - f"迁移旧 WebChat 会话 {conversation.get('cid', 'unknown')} 失败", - exc_info=True, + continue + if ":" in conv.user_id: + continue + platform_id = "webchat" + history = json.loads(conv.history) if conv.history else [] + for msg in history: + type_ = msg.get("type") + new_history = PlatformMessageHistory( + platform_id=platform_id, + user_id=conv.cid, + content=msg, + sender_id=type_, + sender_name=type_, ) + dbsession.add(new_history) + except Exception: + logger.error( + f"迁移旧 WebChat 会话 {conversation.get('cid', 'unknown')} 失败", + exc_info=True, + ) logger.info(f"成功迁移 {total_cnt} 条旧的 WebChat 会话数据到新表。") async def migration_persona_data( - db_helper: BaseDatabase, astrbot_config: AstrBotConfig + db_helper: BaseDatabase, astrbot_config: AstrBotConfig, ) -> None: """迁移 Persona 数据到新的表中。 旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。 @@ -239,14 +237,14 @@ async def migration_persona_data( begin_dialogs=begin_dialogs, ) logger.info( - f"迁移 Persona {persona['name']}({persona_new.system_prompt[:30]}...) 到新表成功。" + f"迁移 Persona {persona['name']}({persona_new.system_prompt[:30]}...) 到新表成功。", ) except Exception as e: logger.error(f"解析 Persona 配置失败:{e}") 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", @@ -307,7 +305,7 @@ 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 87931594e..3a6a35785 100644 --- a/astrbot/core/db/migration/migra_token_usage.py +++ b/astrbot/core/db/migration/migra_token_usage.py @@ -19,7 +19,7 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: """ # 检查是否已经完成迁移 migration_done = await db_helper.get_preference( - "global", "global", "migration_done_token_usage_1" + "global", "global", "migration_done_token_usage_1", ) if migration_done: return @@ -38,15 +38,15 @@ async def migrate_token_usage(db_helper: BaseDatabase) -> None: if "token_usage" in column_names: logger.info("token_usage 列已存在,跳过迁移") await sp.put_async( - "global", "global", "migration_done_token_usage_1", True + "global", "global", "migration_done_token_usage_1", True, ) return # 添加 token_usage 列 await session.execute( text( - "ALTER TABLE conversations ADD COLUMN token_usage INTEGER NOT NULL DEFAULT 0" - ) + "ALTER TABLE conversations ADD COLUMN token_usage INTEGER NOT NULL DEFAULT 0", + ), ) await session.commit() diff --git a/astrbot/core/db/migration/migra_webchat_session.py b/astrbot/core/db/migration/migra_webchat_session.py index ee84a6948..45eb6f194 100644 --- a/astrbot/core/db/migration/migra_webchat_session.py +++ b/astrbot/core/db/migration/migra_webchat_session.py @@ -25,7 +25,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: """ # 检查是否已经完成迁移 migration_done = await db_helper.get_preference( - "global", "global", "migration_done_webchat_session_1" + "global", "global", "migration_done_webchat_session_1", ) if migration_done: return @@ -53,7 +53,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: if not webchat_users: logger.info("没有找到需要迁移的 WebChat 数据") await sp.put_async( - "global", "global", "migration_done_webchat_session_1", True + "global", "global", "migration_done_webchat_session_1", True, ) return @@ -71,7 +71,7 @@ async def migrate_webchat_session(db_helper: BaseDatabase) -> None: for user_id, _, _, _ in webchat_users ] conv_query = select( - col(ConversationV2.user_id), col(ConversationV2.title) + col(ConversationV2.user_id), col(ConversationV2.title), ).where(col(ConversationV2.user_id).in_(user_ids_to_query)) conv_result = await session.execute(conv_query) # 创建 user_id -> title 的映射字典 diff --git a/astrbot/core/db/migration/sqlite_v3.py b/astrbot/core/db/migration/sqlite_v3.py index ba9abf990..539bad1cd 100644 --- a/astrbot/core/db/migration/sqlite_v3.py +++ b/astrbot/core/db/migration/sqlite_v3.py @@ -227,7 +227,7 @@ class SQLiteDatabase: return Stats(platform) def get_conversation_by_user_id( - self, user_id: str, cid: str + self, user_id: str, cid: str, ) -> Conversation | None: try: c = self.conn.cursor() @@ -306,7 +306,7 @@ class SQLiteDatabase: ) def update_conversation_persona_id( - self, user_id: str, cid: str, persona_id: str + self, user_id: str, cid: str, persona_id: str, ) -> None: self._exec_sql( """ diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index b8df2f8ef..cae6fb5ae 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -438,7 +438,7 @@ class CommandConflict(TimestampMixin, SQLModel, table=True): __tablename__ = "command_conflicts" 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 96e9e3bd3..11264b6bf 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -68,12 +68,12 @@ class SQLiteDatabase(BaseDatabase): if "folder_id" not in columns: await conn.execute( text( - "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL" - ) + "ALTER TABLE personas ADD COLUMN folder_id VARCHAR(36) DEFAULT NULL", + ), ) if "sort_order" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0") + text("ALTER TABLE personas ADD COLUMN sort_order INTEGER DEFAULT 0"), ) async def _ensure_persona_skills_column(self, conn) -> None: @@ -93,23 +93,23 @@ class SQLiteDatabase(BaseDatabase): columns = {row[1] for row in result.fetchall()} if "custom_error_message" not in columns: await conn.execute( - text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT") + text("ALTER TABLE personas ADD COLUMN custom_error_message TEXT"), ) 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( text( - "\n INSERT INTO platform_stats (timestamp, platform_id, platform_type, count)\n VALUES (:timestamp, :platform_id, :platform_type, :count)\n ON CONFLICT(timestamp, platform_id, platform_type) DO UPDATE SET\n count = platform_stats.count + EXCLUDED.count\n " + "\n INSERT INTO platform_stats (timestamp, platform_id, platform_type, count)\n VALUES (:timestamp, :platform_id, :platform_type, :count)\n ON CONFLICT(timestamp, platform_id, platform_type) DO UPDATE SET\n count = platform_stats.count + EXCLUDED.count\n ", ), { "timestamp": current_hour, @@ -124,8 +124,8 @@ class SQLiteDatabase(BaseDatabase): async with self.get_db() as session: result = await session.execute( select(func.count(col(PlatformStat.platform_id))).select_from( - PlatformStat - ) + PlatformStat, + ), ) count = result.scalar_one_or_none() return count if count is not None else 0 @@ -137,7 +137,7 @@ class SQLiteDatabase(BaseDatabase): start_time = now - timedelta(seconds=offset_sec) result = await session.execute( text( - "\n SELECT * FROM platform_stats\n WHERE timestamp >= :start_time\n GROUP BY platform_id\n ORDER BY timestamp DESC\n " + "\n SELECT * FROM platform_stats\n WHERE timestamp >= :start_time\n GROUP BY platform_id\n ORDER BY timestamp DESC\n ", ), {"start_time": start_time}, ) @@ -163,26 +163,25 @@ class SQLiteDatabase(BaseDatabase): start_time = float(stats.get("start_time", 0.0) or 0.0) end_time = float(stats.get("end_time", 0.0) or 0.0) time_to_first_token = float(stats.get("time_to_first_token", 0.0) or 0.0) - async with self.get_db() as session: - async with session.begin(): - record = ProviderStat( - agent_type=agent_type, - status=status, - umo=umo, - conversation_id=conversation_id, - provider_id=provider_id, - provider_model=provider_model, - token_input_other=token_input_other, - token_input_cached=token_input_cached, - token_output=token_output, - start_time=start_time, - end_time=end_time, - time_to_first_token=time_to_first_token, - ) - session.add(record) - await session.flush() - await session.refresh(record) - return record + async with self.get_db() as session, session.begin(): + record = ProviderStat( + agent_type=agent_type, + status=status, + umo=umo, + conversation_id=conversation_id, + provider_id=provider_id, + provider_model=provider_model, + token_input_other=token_input_other, + token_input_cached=token_input_cached, + token_output=token_output, + start_time=start_time, + end_time=end_time, + time_to_first_token=time_to_first_token, + ) + session.add(record) + await session.flush() + await session.refresh(record) + return record async def get_conversations(self, user_id=None, platform_id=None): async with self.get_db() as session: @@ -208,18 +207,18 @@ class SQLiteDatabase(BaseDatabase): select(ConversationV2) .order_by(desc(ConversationV2.created_at)) .offset(offset) - .limit(page_size) + .limit(page_size), ) 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) if platform_ids: base_query = base_query.where( - col(ConversationV2.platform_id).in_(platform_ids) + col(ConversationV2.platform_id).in_(platform_ids), ) if search_query: search_query = search_query.encode("unicode_escape").decode("utf-8") @@ -229,16 +228,16 @@ class SQLiteDatabase(BaseDatabase): col(ConversationV2.content).ilike(f"%{search_query}%"), col(ConversationV2.user_id).ilike(f"%{search_query}%"), col(ConversationV2.conversation_id).ilike(f"%{search_query}%"), - ) + ), ) if "message_types" in kwargs and len(kwargs["message_types"]) > 0: for msg_type in kwargs["message_types"]: base_query = base_query.where( - col(ConversationV2.user_id).ilike(f"%:{msg_type}:%") + col(ConversationV2.user_id).ilike(f"%:{msg_type}:%"), ) if "platforms" in kwargs and len(kwargs["platforms"]) > 0: base_query = base_query.where( - col(ConversationV2.platform_id).in_(kwargs["platforms"]) + col(ConversationV2.platform_id).in_(kwargs["platforms"]), ) count_query = select(func.count()).select_from(base_query.subquery()) total_count = await session.execute(count_query) @@ -271,18 +270,17 @@ class SQLiteDatabase(BaseDatabase): kwargs["created_at"] = created_at if updated_at: kwargs["updated_at"] = updated_at - async with self.get_db() as session: - async with session.begin(): - new_conversation = ConversationV2( - user_id=user_id, - content=content or [], - platform_id=platform_id, - title=title, - persona_id=persona_id, - **kwargs, - ) - session.add(new_conversation) - return new_conversation + async with self.get_db() as session, session.begin(): + new_conversation = ConversationV2( + user_id=user_id, + content=content or [], + platform_id=platform_id, + title=title, + persona_id=persona_id, + **kwargs, + ) + session.add(new_conversation) + return new_conversation async def update_conversation( self, @@ -293,46 +291,43 @@ class SQLiteDatabase(BaseDatabase): content=None, token_usage=None, ): - async with self.get_db() as session: - async with session.begin(): - query = update(ConversationV2).where( - col(ConversationV2.conversation_id) == cid - ) - values = {} - if title is not None: - values["title"] = title - if clear_persona: - values["persona_id"] = None - elif persona_id is not None: - values["persona_id"] = persona_id - if content is not None: - values["content"] = content - if token_usage is not None: - values["token_usage"] = token_usage - if not values: - return None - query = query.values(**values) - await session.execute(query) + async with self.get_db() as session, session.begin(): + query = update(ConversationV2).where( + col(ConversationV2.conversation_id) == cid, + ) + values = {} + if title is not None: + values["title"] = title + if clear_persona: + values["persona_id"] = None + elif persona_id is not None: + values["persona_id"] = persona_id + if content is not None: + values["content"] = content + if token_usage is not None: + values["token_usage"] = token_usage + if not values: + return None + query = query.values(**values) + await session.execute(query) return await self.get_conversation_by_id(cid) async def delete_conversation(self, cid) -> None: - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(ConversationV2).where( - col(ConversationV2.conversation_id) == cid - ) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(ConversationV2).where( + col(ConversationV2.conversation_id) == cid, + ), + ) async def delete_conversations_by_user_id(self, user_id: str) -> None: - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(ConversationV2).where(col(ConversationV2.user_id) == user_id) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(ConversationV2).where(col(ConversationV2.user_id) == user_id), + ) 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: @@ -341,7 +336,7 @@ class SQLiteDatabase(BaseDatabase): select( col(Preference.scope_id).label("session_id"), func.json_extract(Preference.value, "$.val").label( - "conversation_id" + "conversation_id", ), col(ConversationV2.persona_id).label("persona_id"), col(ConversationV2.title).label("title"), @@ -354,7 +349,7 @@ 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") ) @@ -365,12 +360,12 @@ class SQLiteDatabase(BaseDatabase): col(Preference.scope_id).ilike(search_pattern), col(ConversationV2.title).ilike(search_pattern), col(Persona.persona_id).ilike(search_pattern), - ) + ), ) if platform: platform_pattern = f"{platform}:%" base_query = base_query.where( - col(Preference.scope_id).like(platform_pattern) + col(Preference.scope_id).like(platform_pattern), ) base_query = base_query.order_by(Preference.scope_id) result_query = base_query.offset(offset).limit(page_size) @@ -385,7 +380,7 @@ 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") ) @@ -396,12 +391,12 @@ class SQLiteDatabase(BaseDatabase): col(Preference.scope_id).ilike(search_pattern), col(ConversationV2.title).ilike(search_pattern), col(Persona.persona_id).ilike(search_pattern), - ) + ), ) if platform: platform_pattern = f"{platform}:%" count_base_query = count_base_query.where( - col(Preference.scope_id).like(platform_pattern) + col(Preference.scope_id).like(platform_pattern), ) total_result = await session.execute(count_base_query) total = total_result.scalar() or 0 @@ -418,39 +413,37 @@ 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: - async with session.begin(): - new_history = PlatformMessageHistory( - platform_id=platform_id, - user_id=user_id, - content=content, - sender_id=sender_id, - sender_name=sender_name, - ) - session.add(new_history) - return new_history + async with self.get_db() as session, session.begin(): + new_history = PlatformMessageHistory( + platform_id=platform_id, + user_id=user_id, + content=content, + sender_id=sender_id, + sender_name=sender_name, + ) + session.add(new_history) + 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: - async with session.begin(): - now = datetime.now() - cutoff_time = now - timedelta(seconds=offset_sec) - await session.execute( - delete(PlatformMessageHistory).where( - col(PlatformMessageHistory.platform_id) == platform_id, - col(PlatformMessageHistory.user_id) == user_id, - col(PlatformMessageHistory.created_at) >= cutoff_time, - ) - ) + async with self.get_db() as session, session.begin(): + now = datetime.now() + cutoff_time = now - timedelta(seconds=offset_sec) + await session.execute( + delete(PlatformMessageHistory).where( + col(PlatformMessageHistory.platform_id) == platform_id, + col(PlatformMessageHistory.user_id) == user_id, + col(PlatformMessageHistory.created_at) >= cutoff_time, + ), + ) 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: @@ -467,7 +460,7 @@ 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: @@ -498,44 +491,41 @@ class SQLiteDatabase(BaseDatabase): async def delete_platform_message_before(self, platform_id, user_id, before) -> int: """Delete platform message history records strictly older than the boundary.""" - async with self.get_db() as session: - async with session.begin(): - result = await session.execute( - delete(PlatformMessageHistory).where( - col(PlatformMessageHistory.platform_id) == platform_id, - col(PlatformMessageHistory.user_id) == user_id, - col(PlatformMessageHistory.created_at) < before, - ) - ) - return int(getattr(result, "rowcount", 0) or 0) + async with self.get_db() as session, session.begin(): + result = await session.execute( + delete(PlatformMessageHistory).where( + col(PlatformMessageHistory.platform_id) == platform_id, + col(PlatformMessageHistory.user_id) == user_id, + col(PlatformMessageHistory.created_at) < before, + ), + ) + return int(getattr(result, "rowcount", 0) or 0) async def delete_platform_message_after(self, platform_id, user_id, after) -> int: """Delete platform message history records strictly newer than the boundary.""" - async with self.get_db() as session: - async with session.begin(): - result = await session.execute( - delete(PlatformMessageHistory).where( - col(PlatformMessageHistory.platform_id) == platform_id, - col(PlatformMessageHistory.user_id) == user_id, - col(PlatformMessageHistory.created_at) > after, - ) - ) - return int(getattr(result, "rowcount", 0) or 0) + async with self.get_db() as session, session.begin(): + result = await session.execute( + delete(PlatformMessageHistory).where( + col(PlatformMessageHistory.platform_id) == platform_id, + col(PlatformMessageHistory.user_id) == user_id, + col(PlatformMessageHistory.created_at) > after, + ), + ) + return int(getattr(result, "rowcount", 0) or 0) async def delete_all_platform_message_history(self, platform_id, user_id) -> int: """Delete all platform message history records for a specific user.""" - async with self.get_db() as session: - async with session.begin(): - result = await session.execute( - delete(PlatformMessageHistory).where( - col(PlatformMessageHistory.platform_id) == platform_id, - col(PlatformMessageHistory.user_id) == user_id, - ) - ) - return int(getattr(result, "rowcount", 0) or 0) + async with self.get_db() as session, session.begin(): + result = await session.execute( + delete(PlatformMessageHistory).where( + col(PlatformMessageHistory.platform_id) == platform_id, + col(PlatformMessageHistory.user_id) == user_id, + ), + ) + 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: @@ -545,7 +535,7 @@ 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), ) @@ -555,23 +545,22 @@ 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: query = select(PlatformMessageHistory).where( - PlatformMessageHistory.id == message_id + PlatformMessageHistory.id == message_id, ) result = await session.execute(query) return result.scalar_one_or_none() async def insert_attachment(self, path, type, mime_type): """Insert a new attachment record.""" - async with self.get_db() as session: - async with session.begin(): - new_attachment = Attachment(path=path, type=type, mime_type=mime_type) - session.add(new_attachment) - return new_attachment + async with self.get_db() as session, session.begin(): + new_attachment = Attachment(path=path, type=type, mime_type=mime_type) + session.add(new_attachment) + return new_attachment async def get_attachment_by_id(self, attachment_id): """Get an attachment by its ID.""" @@ -586,7 +575,7 @@ class SQLiteDatabase(BaseDatabase): return [] async with self.get_db() as session: query = select(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) + col(Attachment.attachment_id).in_(attachment_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -596,13 +585,12 @@ class SQLiteDatabase(BaseDatabase): Returns True if the attachment was deleted, False if it was not found. """ - async with self.get_db() as session: - async with session.begin(): - query = delete(Attachment).where( - col(Attachment.attachment_id) == attachment_id - ) - result = await session.execute(query) - return getattr(result, "rowcount", 0) > 0 + async with self.get_db() as session, session.begin(): + query = delete(Attachment).where( + col(Attachment.attachment_id) == attachment_id, + ) + result = await session.execute(query) + return getattr(result, "rowcount", 0) > 0 async def delete_attachments(self, attachment_ids: list[str]) -> int: """Delete multiple attachments by their IDs. @@ -611,13 +599,12 @@ class SQLiteDatabase(BaseDatabase): """ if not attachment_ids: return 0 - async with self.get_db() as session: - async with session.begin(): - query = delete(Attachment).where( - col(Attachment.attachment_id).in_(attachment_ids) - ) - result = await session.execute(query) - return getattr(result, "rowcount", 0) + async with self.get_db() as session, session.begin(): + query = delete(Attachment).where( + col(Attachment.attachment_id).in_(attachment_ids), + ) + result = await session.execute(query) + return getattr(result, "rowcount", 0) async def create_api_key( self, @@ -629,26 +616,25 @@ class SQLiteDatabase(BaseDatabase): expires_at: datetime | None = None, ) -> ApiKey: """Create a new API key record.""" - async with self.get_db() as session: - async with session.begin(): - api_key = ApiKey( - name=name, - key_hash=key_hash, - key_prefix=key_prefix, - scopes=scopes, - created_by=created_by, - expires_at=expires_at, - ) - session.add(api_key) - await session.flush() - await session.refresh(api_key) - return api_key + async with self.get_db() as session, session.begin(): + api_key = ApiKey( + name=name, + key_hash=key_hash, + key_prefix=key_prefix, + scopes=scopes, + created_by=created_by, + expires_at=expires_at, + ) + session.add(api_key) + await session.flush() + await session.refresh(api_key) + return api_key async def list_api_keys(self) -> list[ApiKey]: """List all API keys.""" async with self.get_db() as session: result = await session.execute( - select(ApiKey).order_by(desc(ApiKey.created_at)) + select(ApiKey).order_by(desc(ApiKey.created_at)), ) return list(result.scalars().all()) @@ -656,7 +642,7 @@ class SQLiteDatabase(BaseDatabase): """Get an API key by key_id.""" async with self.get_db() as session: result = await session.execute( - select(ApiKey).where(ApiKey.key_id == key_id) + select(ApiKey).where(ApiKey.key_id == key_id), ) return result.scalar_one_or_none() @@ -674,34 +660,31 @@ class SQLiteDatabase(BaseDatabase): async def touch_api_key(self, key_id: str) -> None: """Update last_used_at of an API key.""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - update(ApiKey) - .where(col(ApiKey.key_id) == key_id) - .values(last_used_at=datetime.now(timezone.utc)) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + update(ApiKey) + .where(col(ApiKey.key_id) == key_id) + .values(last_used_at=datetime.now(timezone.utc)), + ) async def revoke_api_key(self, key_id: str) -> bool: """Revoke an API key.""" - async with self.get_db() as session: - async with session.begin(): - query = ( - update(ApiKey) - .where(col(ApiKey.key_id) == key_id) - .values(revoked_at=datetime.now(timezone.utc)) - ) - result = await session.execute(query) - return getattr(result, "rowcount", 0) > 0 + async with self.get_db() as session, session.begin(): + query = ( + update(ApiKey) + .where(col(ApiKey.key_id) == key_id) + .values(revoked_at=datetime.now(timezone.utc)) + ) + result = await session.execute(query) + return getattr(result, "rowcount", 0) > 0 async def delete_api_key(self, key_id: str) -> bool: """Delete an API key.""" - async with self.get_db() as session: - async with session.begin(): - result = await session.execute( - delete(ApiKey).where(col(ApiKey.key_id) == key_id) - ) - return getattr(result, "rowcount", 0) > 0 + async with self.get_db() as session, session.begin(): + result = await session.execute( + delete(ApiKey).where(col(ApiKey.key_id) == key_id), + ) + return getattr(result, "rowcount", 0) > 0 async def insert_persona( self, @@ -715,22 +698,21 @@ class SQLiteDatabase(BaseDatabase): sort_order=0, ): """Insert a new persona record.""" - async with self.get_db() as session: - async with session.begin(): - new_persona = Persona( - persona_id=persona_id, - system_prompt=system_prompt, - begin_dialogs=begin_dialogs or [], - tools=tools, - skills=skills, - custom_error_message=custom_error_message, - folder_id=folder_id, - sort_order=sort_order, - ) - session.add(new_persona) - await session.flush() - await session.refresh(new_persona) - return new_persona + async with self.get_db() as session, session.begin(): + new_persona = Persona( + persona_id=persona_id, + system_prompt=system_prompt, + begin_dialogs=begin_dialogs or [], + tools=tools, + skills=skills, + custom_error_message=custom_error_message, + folder_id=folder_id, + sort_order=sort_order, + ) + session.add(new_persona) + await session.flush() + await session.refresh(new_persona) + return new_persona async def get_persona_by_id(self, persona_id): """Get a persona by its ID.""" @@ -756,33 +738,31 @@ class SQLiteDatabase(BaseDatabase): custom_error_message=NOT_GIVEN, ): """Update a persona's system prompt or begin dialogs.""" - async with self.get_db() as session: - async with session.begin(): - query = update(Persona).where(col(Persona.persona_id) == persona_id) - values = {} - if system_prompt is not None: - values["system_prompt"] = system_prompt - if begin_dialogs is not None: - values["begin_dialogs"] = begin_dialogs - if tools is not NOT_GIVEN: - values["tools"] = tools - if skills is not NOT_GIVEN: - values["skills"] = skills - if custom_error_message is not NOT_GIVEN: - values["custom_error_message"] = custom_error_message - if not values: - return None - query = query.values(**values) - await session.execute(query) + async with self.get_db() as session, session.begin(): + query = update(Persona).where(col(Persona.persona_id) == persona_id) + values = {} + if system_prompt is not None: + values["system_prompt"] = system_prompt + if begin_dialogs is not None: + values["begin_dialogs"] = begin_dialogs + if tools is not NOT_GIVEN: + values["tools"] = tools + if skills is not NOT_GIVEN: + values["skills"] = skills + if custom_error_message is not NOT_GIVEN: + values["custom_error_message"] = custom_error_message + if not values: + return None + query = query.values(**values) + await session.execute(query) return await self.get_persona_by_id(persona_id) async def delete_persona(self, persona_id) -> None: """Delete a persona by its ID.""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(Persona).where(col(Persona.persona_id) == persona_id) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(Persona).where(col(Persona.persona_id) == persona_id), + ) async def insert_persona_folder( self, @@ -792,18 +772,17 @@ class SQLiteDatabase(BaseDatabase): sort_order: int = 0, ) -> PersonaFolder: """Insert a new persona folder.""" - async with self.get_db() as session: - async with session.begin(): - new_folder = PersonaFolder( - name=name, - parent_id=parent_id, - description=description, - sort_order=sort_order, - ) - session.add(new_folder) - await session.flush() - await session.refresh(new_folder) - return new_folder + async with self.get_db() as session, session.begin(): + new_folder = PersonaFolder( + name=name, + parent_id=parent_id, + description=description, + sort_order=sort_order, + ) + session.add(new_folder) + await session.flush() + await session.refresh(new_folder) + return new_folder async def get_persona_folder_by_id(self, folder_id: str) -> PersonaFolder | None: """Get a persona folder by its folder_id.""" @@ -813,13 +792,14 @@ class SQLiteDatabase(BaseDatabase): return result.scalar_one_or_none() async def get_persona_folders( - self, parent_id: str | None = None + self, parent_id: str | None = None, ) -> list[PersonaFolder]: """Get all persona folders, optionally filtered by parent_id. Args: parent_id: If None, returns root folders only. If specified, returns children of that folder. + """ async with self.get_db() as session: if parent_id is None: @@ -841,7 +821,7 @@ 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()) @@ -855,24 +835,23 @@ class SQLiteDatabase(BaseDatabase): sort_order: int | None = None, ) -> PersonaFolder | None: """Update a persona folder.""" - async with self.get_db() as session: - async with session.begin(): - query = update(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id - ) - values: dict[str, Any] = {} - if name is not None: - values["name"] = name - if parent_id is not NOT_GIVEN: - values["parent_id"] = parent_id - if description is not NOT_GIVEN: - values["description"] = description - if sort_order is not None: - values["sort_order"] = sort_order - if not values: - return None - query = query.values(**values) - await session.execute(query) + async with self.get_db() as session, session.begin(): + query = update(PersonaFolder).where( + col(PersonaFolder.folder_id) == folder_id, + ) + values: dict[str, Any] = {} + if name is not None: + values["name"] = name + if parent_id is not NOT_GIVEN: + values["parent_id"] = parent_id + if description is not NOT_GIVEN: + values["description"] = description + if sort_order is not None: + values["sort_order"] = sort_order + if not values: + return None + query = query.values(**values) + await session.execute(query) return await self.get_persona_folder_by_id(folder_id) async def delete_persona_folder(self, folder_id: str) -> None: @@ -881,39 +860,38 @@ class SQLiteDatabase(BaseDatabase): Note: This will also set folder_id to NULL for all personas in this folder, moving them to the root directory. """ - async with self.get_db() as session: - async with session.begin(): - await session.execute( - update(Persona) - .where(col(Persona.folder_id) == folder_id) - .values(folder_id=None) - ) - await session.execute( - delete(PersonaFolder).where( - col(PersonaFolder.folder_id) == folder_id - ) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + update(Persona) + .where(col(Persona.folder_id) == folder_id) + .values(folder_id=None), + ) + await session.execute( + delete(PersonaFolder).where( + col(PersonaFolder.folder_id) == folder_id, + ), + ) async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """Move a persona to a folder (or root if folder_id is None).""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - update(Persona) - .where(col(Persona.persona_id) == persona_id) - .values(folder_id=folder_id) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + update(Persona) + .where(col(Persona.persona_id) == persona_id) + .values(folder_id=folder_id), + ) return await self.get_persona_by_id(persona_id) async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """Get all personas in a specific folder. Args: folder_id: If None, returns personas in root directory. + """ async with self.get_db() as session: if folder_id is None: @@ -939,49 +917,48 @@ class SQLiteDatabase(BaseDatabase): - id: The persona_id or folder_id - type: Either "persona" or "folder" - sort_order: The new sort_order value + """ if not items: return - async with self.get_db() as session: - async with session.begin(): - for item in items: - item_id = item.get("id") - item_type = item.get("type") - sort_order = item.get("sort_order") - if item_id is None or item_type is None or sort_order is None: - continue - if item_type == "persona": - await session.execute( - update(Persona) - .where(col(Persona.persona_id) == item_id) - .values(sort_order=sort_order) - ) - elif item_type == "folder": - await session.execute( - update(PersonaFolder) - .where(col(PersonaFolder.folder_id) == item_id) - .values(sort_order=sort_order) - ) + async with self.get_db() as session, session.begin(): + for item in items: + item_id = item.get("id") + item_type = item.get("type") + sort_order = item.get("sort_order") + if item_id is None or item_type is None or sort_order is None: + continue + if item_type == "persona": + await session.execute( + update(Persona) + .where(col(Persona.persona_id) == item_id) + .values(sort_order=sort_order), + ) + elif item_type == "folder": + await session.execute( + update(PersonaFolder) + .where(col(PersonaFolder.folder_id) == item_id) + .values(sort_order=sort_order), + ) async def insert_preference_or_update(self, scope, scope_id, key, value): """Insert a new preference record or update if it exists.""" - async with self.get_db() as session: - async with session.begin(): - query = select(Preference).where( - Preference.scope == scope, - Preference.scope_id == scope_id, - Preference.key == key, + async with self.get_db() as session, session.begin(): + query = select(Preference).where( + Preference.scope == scope, + Preference.scope_id == scope_id, + Preference.key == key, + ) + result = await session.execute(query) + existing_preference = result.scalar_one_or_none() + if existing_preference: + existing_preference.value = value + else: + new_preference = Preference( + scope=scope, scope_id=scope_id, key=key, value=value, ) - result = await session.execute(query) - existing_preference = result.scalar_one_or_none() - if existing_preference: - existing_preference.value = value - else: - new_preference = Preference( - scope=scope, scope_id=scope_id, key=key, value=value - ) - session.add(new_preference) - return existing_preference or new_preference + session.add(new_preference) + return existing_preference or new_preference async def get_preference(self, scope, scope_id, key): """Get a preference by key.""" @@ -1014,7 +991,7 @@ class SQLiteDatabase(BaseDatabase): col(Preference.scope) == scope, col(Preference.scope_id) == scope_id, col(Preference.key) == key, - ) + ), ) await session.commit() @@ -1026,16 +1003,15 @@ class SQLiteDatabase(BaseDatabase): delete(Preference).where( col(Preference.scope) == scope, col(Preference.scope_id) == scope_id, - ) + ), ) 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: - async with session.begin(): - return await fn(session) + async with self.get_db() as session, session.begin(): + return await fn(session) @staticmethod def _apply_updates(model, **updates) -> None: @@ -1175,14 +1151,14 @@ class SQLiteDatabase(BaseDatabase): async def _op(session: AsyncSession) -> None: await session.execute( delete(CommandConfig).where( - col(CommandConfig.handler_full_name).in_(handler_full_names) - ) + col(CommandConfig.handler_full_name).in_(handler_full_names), + ), ) 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) @@ -1209,7 +1185,7 @@ class SQLiteDatabase(BaseDatabase): select(CommandConflict).where( CommandConflict.conflict_key == conflict_key, CommandConflict.handler_full_name == handler_full_name, - ) + ), ) record = result.scalar_one_or_none() if not record: @@ -1248,7 +1224,7 @@ class SQLiteDatabase(BaseDatabase): async def _op(session: AsyncSession) -> None: await session.execute( - delete(CommandConflict).where(col(CommandConflict.id).in_(ids)) + delete(CommandConflict).where(col(CommandConflict.id).in_(ids)), ) await self._run_in_tx(_op) @@ -1261,7 +1237,7 @@ class SQLiteDatabase(BaseDatabase): now = datetime.now() start_time = now - timedelta(seconds=offset_sec) result = await session.execute( - select(PlatformStat).where(PlatformStat.timestamp >= start_time) + select(PlatformStat).where(PlatformStat.timestamp >= start_time), ) all_datas = result.scalars().all() deprecated_stats = DeprecatedStats() @@ -1271,7 +1247,7 @@ class SQLiteDatabase(BaseDatabase): name=data.platform_id, count=data.count, timestamp=int(data.timestamp.timestamp()), - ) + ), ) return deprecated_stats @@ -1292,7 +1268,7 @@ class SQLiteDatabase(BaseDatabase): async def _inner(): async with self.get_db() as session: result = await session.execute( - select(func.sum(PlatformStat.count)).select_from(PlatformStat) + select(func.sum(PlatformStat.count)).select_from(PlatformStat), ) total_count = result.scalar_one_or_none() return total_count if total_count is not None else 0 @@ -1316,7 +1292,7 @@ class SQLiteDatabase(BaseDatabase): result = await session.execute( select(PlatformStat.platform_id, func.sum(PlatformStat.count)) .where(PlatformStat.timestamp >= start_time) - .group_by(PlatformStat.platform_id) + .group_by(PlatformStat.platform_id), ) grouped_stats = result.all() deprecated_stats = DeprecatedStats() @@ -1326,7 +1302,7 @@ class SQLiteDatabase(BaseDatabase): name=platform_id, count=count, timestamp=int(start_time.timestamp()), - ) + ), ) return deprecated_stats @@ -1353,40 +1329,39 @@ class SQLiteDatabase(BaseDatabase): kwargs = {} if session_id: kwargs["session_id"] = session_id - async with self.get_db() as session: - async with session.begin(): - new_session = PlatformSession( - creator=creator, - platform_id=platform_id, - display_name=display_name, - is_group=is_group, - **kwargs, - ) - session.add(new_session) - await session.flush() - await session.refresh(new_session) - return new_session + async with self.get_db() as session, session.begin(): + new_session = PlatformSession( + creator=creator, + platform_id=platform_id, + display_name=display_name, + is_group=is_group, + **kwargs, + ) + session.add(new_session) + await session.flush() + await session.refresh(new_session) + 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: query = select(PlatformSession).where( - PlatformSession.session_id == session_id + PlatformSession.session_id == session_id, ) result = await session.execute(query) 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: return [] async with self.get_db() as session: query = select(PlatformSession).where( - col(PlatformSession.session_id).in_(session_ids) + col(PlatformSession.session_id).in_(session_ids), ) result = await session.execute(query) return list(result.scalars().all()) @@ -1478,7 +1453,7 @@ class SQLiteDatabase(BaseDatabase): exclude_project_sessions=exclude_project_sessions, ) total_result = await session.execute( - select(func.count()).select_from(base_query.subquery()) + select(func.count()).select_from(base_query.subquery()), ) total = int(total_result.scalar_one() or 0) result_query = ( @@ -1491,29 +1466,27 @@ 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: - async with session.begin(): - values: dict[str, Any] = {"updated_at": datetime.now(timezone.utc)} - if display_name is not None: - values["display_name"] = display_name - await session.execute( - update(PlatformSession) - .where(col(PlatformSession.session_id) == session_id) - .values(**values) - ) + async with self.get_db() as session, session.begin(): + values: dict[str, Any] = {"updated_at": datetime.now(timezone.utc)} + if display_name is not None: + values["display_name"] = display_name + await session.execute( + update(PlatformSession) + .where(col(PlatformSession.session_id) == session_id) + .values(**values), + ) async def delete_platform_session(self, session_id: str) -> None: """Delete a Platform session by its ID.""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(PlatformSession).where( - col(PlatformSession.session_id) == session_id - ) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(PlatformSession).where( + col(PlatformSession.session_id) == session_id, + ), + ) async def create_chatui_project( self, @@ -1523,26 +1496,25 @@ class SQLiteDatabase(BaseDatabase): description: str | None = None, ) -> ChatUIProject: """Create a new ChatUI project.""" - async with self.get_db() as session: - async with session.begin(): - project = ChatUIProject( - creator=creator, title=title, emoji=emoji, description=description - ) - session.add(project) - await session.flush() - await session.refresh(project) - return project + async with self.get_db() as session, session.begin(): + project = ChatUIProject( + creator=creator, title=title, emoji=emoji, description=description, + ) + session.add(project) + await session.flush() + await session.refresh(project) + return project async def get_chatui_project_by_id(self, project_id: str) -> ChatUIProject | None: """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: @@ -1552,7 +1524,7 @@ class SQLiteDatabase(BaseDatabase): .where(col(ChatUIProject.creator) == creator) .order_by(desc(ChatUIProject.updated_at)) .limit(page_size) - .offset(offset) + .offset(offset), ) return list(result.scalars().all()) @@ -1564,67 +1536,63 @@ class SQLiteDatabase(BaseDatabase): description: str | None = None, ) -> None: """Update a ChatUI project.""" - async with self.get_db() as session: - async with session.begin(): - values: dict[str, Any] = {"updated_at": datetime.now(timezone.utc)} - if title is not None: - values["title"] = title - if emoji is not None: - values["emoji"] = emoji - if description is not None: - values["description"] = description - await session.execute( - update(ChatUIProject) - .where(col(ChatUIProject.project_id) == project_id) - .values(**values) - ) + async with self.get_db() as session, session.begin(): + values: dict[str, Any] = {"updated_at": datetime.now(timezone.utc)} + if title is not None: + values["title"] = title + if emoji is not None: + values["emoji"] = emoji + if description is not None: + values["description"] = description + await session.execute( + update(ChatUIProject) + .where(col(ChatUIProject.project_id) == project_id) + .values(**values), + ) async def delete_chatui_project(self, project_id: str) -> None: """Delete a ChatUI project by its ID.""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(SessionProjectRelation).where( - col(SessionProjectRelation.project_id) == project_id - ) - ) - await session.execute( - delete(ChatUIProject).where( - col(ChatUIProject.project_id) == project_id - ) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(SessionProjectRelation).where( + col(SessionProjectRelation.project_id) == project_id, + ), + ) + await session.execute( + delete(ChatUIProject).where( + col(ChatUIProject.project_id) == project_id, + ), + ) 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: - async with session.begin(): - await session.execute( - delete(SessionProjectRelation).where( - col(SessionProjectRelation.session_id) == session_id - ) - ) - relation = SessionProjectRelation( - session_id=session_id, project_id=project_id - ) - session.add(relation) - await session.flush() - await session.refresh(relation) - return relation + async with self.get_db() as session, session.begin(): + await session.execute( + delete(SessionProjectRelation).where( + col(SessionProjectRelation.session_id) == session_id, + ), + ) + relation = SessionProjectRelation( + session_id=session_id, project_id=project_id, + ) + session.add(relation) + await session.flush() + await session.refresh(relation) + return relation async def remove_session_from_project(self, session_id: str) -> None: """Remove a session from its project.""" - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(SessionProjectRelation).where( - col(SessionProjectRelation.session_id) == session_id - ) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(SessionProjectRelation).where( + col(SessionProjectRelation.session_id) == session_id, + ), + ) 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: @@ -1639,12 +1607,12 @@ class SQLiteDatabase(BaseDatabase): .where(col(SessionProjectRelation.project_id) == project_id) .order_by(desc(PlatformSession.updated_at)) .limit(page_size) - .offset(offset) + .offset(offset), ) 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: @@ -1658,7 +1626,7 @@ class SQLiteDatabase(BaseDatabase): .where( col(SessionProjectRelation.session_id) == session_id, col(ChatUIProject.creator) == creator, - ) + ), ) return result.scalar_one_or_none() @@ -1677,26 +1645,25 @@ class SQLiteDatabase(BaseDatabase): status: str | None = None, job_id: str | None = None, ) -> CronJob: - async with self.get_db() as session: - async with session.begin(): - job = CronJob( - name=name, - job_type=job_type, - cron_expression=cron_expression, - timezone=timezone, - payload=payload or {}, - description=description, - enabled=enabled, - persistent=persistent, - run_once=run_once, - status=status or "scheduled", - ) - if job_id: - job.job_id = job_id - session.add(job) - await session.flush() - await session.refresh(job) - return job + async with self.get_db() as session, session.begin(): + job = CronJob( + name=name, + job_type=job_type, + cron_expression=cron_expression, + timezone=timezone, + payload=payload or {}, + description=description, + enabled=enabled, + persistent=persistent, + run_once=run_once, + status=status or "scheduled", + ) + if job_id: + job.job_id = job_id + session.add(job) + await session.flush() + await session.refresh(job) + return job async def update_cron_job( self, @@ -1715,49 +1682,47 @@ class SQLiteDatabase(BaseDatabase): last_run_at: datetime | None | object = CRON_FIELD_NOT_SET, last_error: str | None | object = CRON_FIELD_NOT_SET, ) -> CronJob | None: - async with self.get_db() as session: - async with session.begin(): - updates: dict = {} - for key, val in { - "name": name, - "cron_expression": cron_expression, - "timezone": timezone, - "payload": payload, - "description": description, - "enabled": enabled, - "persistent": persistent, - "run_once": run_once, - "status": status, - "next_run_time": next_run_time, - "last_run_at": last_run_at, - "last_error": last_error, - }.items(): - if val is CRON_FIELD_NOT_SET: - continue - updates[key] = val - stmt = ( - update(CronJob) - .where(col(CronJob.job_id) == job_id) - .values(**updates) - .execution_options(synchronize_session="fetch") - ) - await session.execute(stmt) - result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) - ) - return result.scalar_one_or_none() + async with self.get_db() as session, session.begin(): + updates: dict = {} + for key, val in { + "name": name, + "cron_expression": cron_expression, + "timezone": timezone, + "payload": payload, + "description": description, + "enabled": enabled, + "persistent": persistent, + "run_once": run_once, + "status": status, + "next_run_time": next_run_time, + "last_run_at": last_run_at, + "last_error": last_error, + }.items(): + if val is CRON_FIELD_NOT_SET: + continue + updates[key] = val + stmt = ( + update(CronJob) + .where(col(CronJob.job_id) == job_id) + .values(**updates) + .execution_options(synchronize_session="fetch") + ) + await session.execute(stmt) + result = await session.execute( + select(CronJob).where(col(CronJob.job_id) == job_id), + ) + return result.scalar_one_or_none() async def delete_cron_job(self, job_id: str) -> None: - async with self.get_db() as session: - async with session.begin(): - await session.execute( - delete(CronJob).where(col(CronJob.job_id) == job_id) - ) + async with self.get_db() as session, session.begin(): + await session.execute( + delete(CronJob).where(col(CronJob.job_id) == job_id), + ) async def get_cron_job(self, job_id: str) -> CronJob | None: async with self.get_db() as session: result = await session.execute( - select(CronJob).where(col(CronJob.job_id) == job_id) + select(CronJob).where(col(CronJob.job_id) == job_id), ) return result.scalar_one_or_none() diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py index 6a9eb3d88..64a749aa3 100644 --- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py +++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py @@ -76,7 +76,7 @@ class FaissVecDB(BaseVecDB): if not contents: logger.debug( - "No contents provided for batch insert; skipping embedding generation." + "No contents provided for batch insert; skipping embedding generation.", ) return [] diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index a9f388af4..6d0cf21ee 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -44,7 +44,7 @@ class EventBus: scheduler = self.pipeline_scheduler_mapping.get(conf_id) if not scheduler: logger.error( - f"PipelineScheduler not found for id: {conf_id}, event ignored." + f"PipelineScheduler not found for id: {conf_id}, event ignored.", ) continue asyncio.create_task(scheduler.execute(event)) # noqa: RUF006 diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index b7e96f8e5..44ca862d5 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -13,7 +13,7 @@ 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 +32,7 @@ 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 babcfa259..e36389116 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -85,86 +85,85 @@ class KBSQLiteDatabase: 创建所有必要的索引以优化查询性能 """ - async with self.get_db() as session: - async with session.begin(): - # 创建知识库表索引 - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_kb_kb_id " - "ON knowledge_bases(kb_id)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_kb_name " - "ON knowledge_bases(kb_name)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_kb_created_at " - "ON knowledge_bases(created_at)", - ), - ) + async with self.get_db() as session, session.begin(): + # 创建知识库表索引 + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_kb_kb_id " + "ON knowledge_bases(kb_id)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_kb_name " + "ON knowledge_bases(kb_name)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_kb_created_at " + "ON knowledge_bases(created_at)", + ), + ) - # 创建文档表索引 - await session.execute( - text( - "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)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_doc_name " - "ON kb_documents(doc_name)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_doc_type " - "ON kb_documents(file_type)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_doc_created_at " - "ON kb_documents(created_at)", - ), - ) + # 创建文档表索引 + await session.execute( + text( + "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)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_doc_name " + "ON kb_documents(doc_name)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_doc_type " + "ON kb_documents(file_type)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_doc_created_at " + "ON kb_documents(created_at)", + ), + ) - # 创建多媒体表索引 - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_media_media_id " - "ON kb_media(media_id)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_media_doc_id " - "ON kb_media(doc_id)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_media_kb_id ON kb_media(kb_id)", - ), - ) - await session.execute( - text( - "CREATE INDEX IF NOT EXISTS idx_media_type " - "ON kb_media(media_type)", - ), - ) + # 创建多媒体表索引 + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_media_media_id " + "ON kb_media(media_id)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_media_doc_id " + "ON kb_media(doc_id)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_media_kb_id ON kb_media(kb_id)", + ), + ) + await session.execute( + text( + "CREATE INDEX IF NOT EXISTS idx_media_type " + "ON kb_media(media_type)", + ), + ) - await session.commit() + await session.commit() async def close(self) -> None: """关闭数据库连接""" @@ -259,7 +258,7 @@ class KBSQLiteDatabase: } async def get_documents_with_metadata_batch( - self, doc_ids: set[str] + self, doc_ids: set[str], ) -> dict[str, dict]: """批量获取文档及其所属知识库元数据 diff --git a/astrbot/core/knowledge_base/kb_helper.py b/astrbot/core/knowledge_base/kb_helper.py index 28e3dcfaf..f1c1f12da 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. @@ -72,7 +71,7 @@ Text chunk to process: try: async with rate_limiter: response = await repair_llm_service.text_chat( - prompt=user_prompt, system_prompt=TEXT_REPAIR_SYSTEM_PROMPT + prompt=user_prompt, system_prompt=TEXT_REPAIR_SYSTEM_PROMPT, ) llm_output = response.completion_text @@ -90,16 +89,15 @@ Text chunk to process: if matches: # Further cleaning to ensure no empty strings are returned return [m.strip() for m in matches if m.strip()] - else: - # If no valid tags and not explicitly discarded, discard it to be safe. - return [] + # If no valid tags and not explicitly discarded, discard it to be safe. + return [] except Exception as e: logger.warning( - f" - LLM call failed on attempt {attempt + 1}/{max_retries + 1}. Error: {e!s}" + f" - LLM call failed on attempt {attempt + 1}/{max_retries + 1}. Error: {e!s}", ) logger.error( - f" - Failed to process chunk after {max_retries + 1} attempts. Using original text." + f" - Failed to process chunk after {max_retries + 1} attempts. Using original text.", ) return [chunk] @@ -164,12 +162,12 @@ class KBHelper: ) if not rp: logger.warning( - f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 的 Rerank Provider({self.kb.rerank_provider_id}) 不可用,将跳过重排序。" + f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 的 Rerank Provider({self.kb.rerank_provider_id}) 不可用,将跳过重排序。", ) return None if not isinstance(rp, RerankProvider): raise ValueError( - f"Provider {self.kb.rerank_provider_id} is not a Rerank Provider" + f"Provider {self.kb.rerank_provider_id} is not a Rerank Provider", ) return rp @@ -260,7 +258,7 @@ class KBHelper: # 否则,执行标准的文件解析和分块流程 if file_content is None: raise ValueError( - "当未提供 pre_chunked_text 时,file_content 不能为空。" + "当未提供 pre_chunked_text 时,file_content 不能为空。", ) file_size = len(file_content) @@ -362,7 +360,7 @@ class KBHelper: saved_file_path.unlink() except Exception as file_error: logger.warning( - f"清理原始文档文件失败 {saved_file_path}: {file_error}" + f"清理原始文档文件失败 {saved_file_path}: {file_error}", ) for media_path in media_paths: @@ -507,6 +505,7 @@ class KBHelper: cleaning_provider_id: str | None = None, ) -> KBDocument: """从 URL 上传并处理文档(带原子性保证和失败清理) + Args: url: 要提取内容的网页 URL chunk_size: 文本块大小 @@ -523,15 +522,16 @@ class KBHelper: Raises: ValueError: 如果 URL 为空或无法提取内容 IOError: 如果网络请求失败 + """ # 获取 Tavily API 密钥 config = self.prov_mgr.acm.default_conf tavily_keys = config.get("provider_settings", {}).get( - "websearch_tavily_key", [] + "websearch_tavily_key", [], ) if not tavily_keys: raise ValueError( - "Error: Tavily API key is not configured in provider_settings." + "Error: Tavily API key is not configured in provider_settings.", ) # 阶段1: 从 URL 提取内容 @@ -563,7 +563,7 @@ class KBHelper: if enable_cleaning and not final_chunks: raise ValueError( - "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。" + "内容清洗后未提取到有效文本。请尝试关闭内容清洗功能,或更换更高性能的LLM模型后重试。", ) # 创建一个虚拟文件名 @@ -596,21 +596,20 @@ class KBHelper: chunk_size: int = 512, chunk_overlap: int = 50, ) -> list[str]: - """ - 对从 URL 获取的内容进行清洗、修复、翻译和重新分块。 + """对从 URL 获取的内容进行清洗、修复、翻译和重新分块。 """ if not enable_cleaning: # 如果不启用清洗,则使用从前端传递的参数进行分块 logger.info( - f"内容清洗未启用,使用指定参数进行分块: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}" + f"内容清洗未启用,使用指定参数进行分块: chunk_size={chunk_size}, chunk_overlap={chunk_overlap}", ) return await self.chunker.chunk( - content, chunk_size=chunk_size, chunk_overlap=chunk_overlap + content, chunk_size=chunk_size, chunk_overlap=chunk_overlap, ) if not cleaning_provider_id: logger.warning( - "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。" + "启用了内容清洗,但未提供 cleaning_provider_id,跳过清洗并使用默认分块。", ) return await self.chunker.chunk(content) @@ -622,7 +621,7 @@ class KBHelper: llm_provider = await self.prov_mgr.get_provider_by_id(cleaning_provider_id) if not llm_provider or not isinstance(llm_provider, LLMProvider): raise ValueError( - f"无法找到 ID 为 {cleaning_provider_id} 的 LLM Provider 或类型不正确" + f"无法找到 ID 为 {cleaning_provider_id} 的 LLM Provider 或类型不正确", ) # 初步分块 @@ -639,7 +638,7 @@ class KBHelper: rate_limiter = RateLimiter(repair_max_rpm) tasks = [ _repair_and_translate_chunk_with_retry( - chunk, llm_provider, rate_limiter + chunk, llm_provider, rate_limiter, ) for chunk in initial_chunks ] @@ -655,7 +654,7 @@ class KBHelper: final_chunks.extend(result) logger.info( - f"文本修复完成: {len(initial_chunks)} 个原始块 -> {len(final_chunks)} 个最终块。" + f"文本修复完成: {len(initial_chunks)} 个原始块 -> {len(final_chunks)} 个最终块。", ) if progress_callback: diff --git a/astrbot/core/knowledge_base/kb_mgr.py b/astrbot/core/knowledge_base/kb_mgr.py index 4bd24b932..39a07520b 100644 --- a/astrbot/core/knowledge_base/kb_mgr.py +++ b/astrbot/core/knowledge_base/kb_mgr.py @@ -394,6 +394,7 @@ class KnowledgeBaseManager: Raises: ValueError: 如果知识库不存在或 URL 为空 IOError: 如果网络请求失败 + """ kb_helper = await self.get_kb(kb_id) if not kb_helper: diff --git a/astrbot/core/knowledge_base/parsers/url_parser.py b/astrbot/core/knowledge_base/parsers/url_parser.py index c0526ea76..af67dbcc0 100644 --- a/astrbot/core/knowledge_base/parsers/url_parser.py +++ b/astrbot/core/knowledge_base/parsers/url_parser.py @@ -8,11 +8,11 @@ class URLExtractor: """URL 内容提取器,封装了 Tavily API 调用和密钥管理""" def __init__(self, tavily_keys: list[str]) -> None: - """ - 初始化 URL 提取器 + """初始化 URL 提取器 Args: tavily_keys: Tavily API 密钥列表 + """ if not tavily_keys: raise ValueError("Error: Tavily API keys are not configured.") @@ -29,8 +29,7 @@ class URLExtractor: return key async def extract_text_from_url(self, url: str) -> str: - """ - 使用 Tavily API 从 URL 提取主要文本内容。 + """使用 Tavily API 从 URL 提取主要文本内容。 这是 web_searcher 插件中 tavily_extract_web_page 方法的简化版本, 专门为知识库模块设计,不依赖 AstrMessageEvent。 @@ -43,6 +42,7 @@ class URLExtractor: Raises: ValueError: 如果 URL 为空或 API 密钥未配置 IOError: 如果请求失败或返回错误 + """ if not url: raise ValueError("Error: url must be a non-empty string.") @@ -66,13 +66,13 @@ class URLExtractor: json=payload, headers=headers, timeout=ClientTimeout( - total=30 + total=30, ), # 增加超时时间,因为内容提取可能需要更长时间 ) as response: if response.status != 200: reason = await response.text() raise OSError( - f"Tavily web extraction failed: {reason}, status: {response.status}" + f"Tavily web extraction failed: {reason}, status: {response.status}", ) data = await response.json() @@ -92,8 +92,7 @@ class URLExtractor: # 为了向后兼容,提供一个简单的函数接口 async def extract_text_from_url(url: str, tavily_keys: list[str]) -> str: - """ - 简单的函数接口,用于从 URL 提取文本内容 + """简单的函数接口,用于从 URL 提取文本内容 Args: url: 要提取内容的网页 URL @@ -101,6 +100,7 @@ async def extract_text_from_url(url: str, tavily_keys: list[str]) -> str: Returns: 提取的文本内容 + """ extractor = URLExtractor(tavily_keys) return await extractor.extract_text_from_url(url) diff --git a/astrbot/core/log.py b/astrbot/core/log.py index 3999da930..f34ea2aad 100644 --- a/astrbot/core/log.py +++ b/astrbot/core/log.py @@ -137,7 +137,7 @@ class _LoguruInterceptHandler(logging.Handler): ), "astrbot_version_tag": getattr(record, "astrbot_version_tag", ""), "source_file": getattr( - record, "source_file", _build_source_file(record.pathname) + record, "source_file", _build_source_file(record.pathname), ), "source_line": getattr(record, "source_line", record.lineno), "is_trace": getattr(record, "is_trace", record.name == "astrbot.trace"), @@ -415,7 +415,7 @@ class LogManager: enable = bool( config.get("trace_log_enable") - or (config.get("log_file", {}) or {}).get("trace_enable", False) + or (config.get("log_file", {}) or {}).get("trace_enable", False), ) path = config.get("trace_log_path") max_mb = config.get("trace_log_max_mb") diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index f40cab05c..acd3675bf 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -167,7 +167,7 @@ class Record(BaseMessageComponent): bs64_data = self.file.removeprefix("base64://") image_bytes = base64.b64decode(bs64_data) file_path = os.path.join( - get_astrbot_temp_path(), f"recordseg_{uuid.uuid4()}.jpg" + get_astrbot_temp_path(), f"recordseg_{uuid.uuid4()}.jpg", ) async with await anyio.open_file(file_path, "wb") as f: await f.write(image_bytes) @@ -256,7 +256,7 @@ class Video(BaseMessageComponent): return url[8:] if url and url.startswith("http"): video_file_path = os.path.join( - get_astrbot_temp_path(), f"videoseg_{uuid.uuid4().hex}" + get_astrbot_temp_path(), f"videoseg_{uuid.uuid4().hex}", ) await download_file(url, video_file_path) if await anyio.Path(video_file_path).exists(): @@ -452,7 +452,7 @@ class Image(BaseMessageComponent): bs64_data = url.removeprefix("base64://") image_bytes = base64.b64decode(bs64_data) image_file_path = os.path.join( - get_astrbot_temp_path(), f"imgseg_{uuid.uuid4()}.jpg" + get_astrbot_temp_path(), f"imgseg_{uuid.uuid4()}.jpg", ) async with await anyio.open_file(image_file_path, "wb") as f: await f.write(image_bytes) diff --git a/astrbot/core/message/message_event_result.py b/astrbot/core/message/message_event_result.py index 7fefe1bdb..1fd5a0be1 100644 --- a/astrbot/core/message/message_event_result.py +++ b/astrbot/core/message/message_event_result.py @@ -123,21 +123,21 @@ class MessageChain: Args: with_other_comps_mark (bool): 是否在纯文本中标记其他组件的位置 + """ if not with_other_comps_mark: return " ".join( - [comp.text for comp in self.chain if isinstance(comp, Plain)] + [comp.text for comp in self.chain if isinstance(comp, Plain)], ) - else: - texts = [] - for comp in self.chain: - if isinstance(comp, Plain): - texts.append(comp.text) - elif isinstance(comp, Json): - texts.append(f"{comp.data}") - else: - texts.append(f"[{comp.__class__.__name__}]") - return " ".join(texts) + texts = [] + for comp in self.chain: + if isinstance(comp, Plain): + texts.append(comp.text) + elif isinstance(comp, Json): + texts.append(f"{comp.data}") + else: + texts.append(f"[{comp.__class__.__name__}]") + return " ".join(texts) def squash_plain(self): """将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。""" diff --git a/astrbot/core/persona_error_reply.py b/astrbot/core/persona_error_reply.py index 5a99e0918..e6ea1bb10 100644 --- a/astrbot/core/persona_error_reply.py +++ b/astrbot/core/persona_error_reply.py @@ -35,7 +35,7 @@ def extract_persona_custom_error_message_from_event(event: Any) -> str | None: def set_persona_custom_error_message_on_event( - event: Any, message: object + event: Any, message: object, ) -> str | None: """Normalize and store persona custom error reply text into event extras.""" normalized = normalize_persona_custom_error_message(message) @@ -70,16 +70,16 @@ async def resolve_persona_custom_error_message( async def resolve_event_conversation_persona_id( - event: Any, conversation_manager: Any + event: Any, conversation_manager: Any, ) -> str | None: """Resolve current conversation persona_id from event and conversation manager.""" curr_cid = await conversation_manager.get_curr_conversation_id( - event.unified_msg_origin + event.unified_msg_origin, ) if not curr_cid: return None conversation = await conversation_manager.get_conversation( - event.unified_msg_origin, curr_cid + event.unified_msg_origin, curr_cid, ) if not conversation: return None diff --git a/astrbot/core/persona_mgr.py b/astrbot/core/persona_mgr.py index b28e5d1f7..3056995a1 100644 --- a/astrbot/core/persona_mgr.py +++ b/astrbot/core/persona_mgr.py @@ -60,12 +60,12 @@ 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 @@ -85,10 +85,11 @@ class PersonaManager: - selected persona object - force applied persona_id from session rule - whether use webchat special default persona + """ session_service_config = ( await sp.get_async( - scope="umo", scope_id=str(umo), key="session_service_config", default={} + scope="umo", scope_id=str(umo), key="session_service_config", default={}, ) or {} ) @@ -101,7 +102,7 @@ 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]"): @@ -143,7 +144,7 @@ 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): @@ -158,23 +159,25 @@ class PersonaManager: return await self.db.get_personas() async def get_personas_by_folder( - self, folder_id: str | None = None + self, folder_id: str | None = None, ) -> list[Persona]: """获取指定文件夹中的 personas Args: folder_id: 文件夹 ID,None 表示根目录 + """ return await self.db.get_personas_by_folder(folder_id) async def move_persona_to_folder( - self, persona_id: str, folder_id: str | None + self, persona_id: str, folder_id: str | None, ) -> Persona | None: """移动 persona 到指定文件夹 Args: persona_id: Persona ID folder_id: 目标文件夹 ID,None 表示移动到根目录 + """ persona = await self.db.move_persona_to_folder(persona_id, folder_id) if persona: @@ -208,6 +211,7 @@ class PersonaManager: Args: parent_id: 父文件夹 ID,None 表示获取根目录下的文件夹 + """ return await self.db.get_persona_folders(parent_id) @@ -247,6 +251,7 @@ class PersonaManager: - id: persona_id 或 folder_id - type: "persona" 或 "folder" - sort_order: 新的排序顺序值 + """ await self.db.batch_update_sort_order(items) self.personas = await self.get_all_personas() @@ -257,6 +262,7 @@ class PersonaManager: Returns: 树形结构的文件夹列表,每个文件夹包含 children 子列表 + """ all_folders = await self.get_all_folders() folder_map: dict[str, dict] = {} @@ -307,6 +313,7 @@ class PersonaManager: skills: Skills 列表,None 表示使用所有 Skills,空列表表示不使用任何 Skills folder_id: 所属文件夹 ID,None 表示根目录 sort_order: 排序顺序 + """ if await self.db.get_persona_by_id(persona_id): raise ValueError(f"Persona with ID {persona_id} already exists.") @@ -325,7 +332,7 @@ 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. @@ -335,6 +342,7 @@ class PersonaManager: Returns: The newly created persona clone + """ source_persona = await self.db.get_persona_by_id(source_persona_id) if not source_persona: @@ -384,7 +392,7 @@ class PersonaManager: if begin_dialogs: if len(begin_dialogs) % 2 != 0: logger.error( - f"{persona_cfg['name']} 人格情景预设对话格式不对,条数应该为偶数。" + f"{persona_cfg['name']} 人格情景预设对话格式不对,条数应该为偶数。", ) begin_dialogs = [] user_turn = True @@ -394,7 +402,7 @@ class PersonaManager: "role": "user" if user_turn else "assistant", "content": dialog, "_no_save": True, - } + }, ) user_turn = not user_turn try: diff --git a/astrbot/core/pipeline/process_stage/follow_up.py b/astrbot/core/pipeline/process_stage/follow_up.py index 6fd49c72a..824e76b6a 100644 --- a/astrbot/core/pipeline/process_stage/follow_up.py +++ b/astrbot/core/pipeline/process_stage/follow_up.py @@ -192,7 +192,7 @@ def try_capture_follow_up(event: AstrMessageEvent) -> FollowUpCapture | None: event.unified_msg_origin, ticket, order_seq, - ) + ), ) logger.info( "Captured follow-up message for active agent run, umo=%s, order_seq=%s", diff --git a/astrbot/core/pipeline/process_stage/method/agent_request.py b/astrbot/core/pipeline/process_stage/method/agent_request.py index 1eec0884f..2b6e65303 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_request.py +++ b/astrbot/core/pipeline/process_stage/method/agent_request.py @@ -35,13 +35,13 @@ class AgentRequestSubStage(Stage): async def process(self, event: AstrMessageEvent) -> AsyncGenerator[None, None]: if not self.ctx.astrbot_config["provider_settings"]["enable"]: logger.debug( - "This pipeline does not enable AI capability, skip processing." + "This pipeline does not enable AI capability, skip processing.", ) return if not await SessionServiceManager.should_process_llm_request(event): logger.debug( - f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing." + f"The session {event.unified_msg_origin} has disabled AI capability, skipping processing.", ) return diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index d4f172a9c..fb65f074d 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,34 @@ 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 +178,12 @@ 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) @@ -258,12 +258,12 @@ class InternalAgentSubStage(Stage): logger.info("[Internal Agent] 检测到 Live Mode,启用 TTS 处理") tts_provider = ( self.ctx.plugin_manager.context.get_using_tts_provider( - event.unified_msg_origin + event.unified_msg_origin, ) ) if not tts_provider: logger.warning( - "[Live Mode] TTS Provider 未配置,将使用普通流式模式" + "[Live Mode] TTS Provider 未配置,将使用普通流式模式", ) event.set_result( MessageEventResult() @@ -276,8 +276,8 @@ class InternalAgentSubStage(Stage): self.show_tool_use, self.show_tool_call_result, show_reasoning=self.show_reasoning, - ) - ) + ), + ), ) yield None if agent_runner.done() and ( @@ -302,8 +302,8 @@ class InternalAgentSubStage(Stage): self.show_tool_use, self.show_tool_call_result, show_reasoning=self.show_reasoning, - ) - ) + ), + ), ) yield None if agent_runner.done(): @@ -322,7 +322,7 @@ class InternalAgentSubStage(Stage): MessageEventResult( chain=chain, result_content_type=ResultContentType.STREAMING_FINISH, - ) + ), ) else: async for _ in run_agent( @@ -342,8 +342,8 @@ 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(): await self._save_to_history( @@ -359,7 +359,7 @@ class InternalAgentSubStage(Stage): llm_tick=1, model_name=agent_runner.provider.get_model(), provider_type=agent_runner.provider.meta().type, - ) + ), ) finally: if runner_registered and agent_runner is not None: @@ -371,7 +371,7 @@ class InternalAgentSubStage(Stage): get_astrbot_skills_path(), ) custom_error_message = extract_persona_custom_error_message_from_event( - event + event, ) error_text = ( custom_error_message @@ -408,7 +408,7 @@ 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 dd91334f1..35979c4aa 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 @@ -56,8 +56,7 @@ async def run_third_party_agent( stream_to_general: bool = False, custom_error_message: str | None = None, ) -> AsyncGenerator[tuple[MessageChain, bool], None]: - """ - 运行第三方 agent runner 并转换响应格式 + """运行第三方 agent runner 并转换响应格式 类似于 run_agent 函数,但专门处理第三方 agent runner """ try: @@ -182,7 +181,7 @@ class ThirdPartyAgentSubStage(Stage): ) async def _resolve_persona_custom_error_message( - self, event: AstrMessageEvent + self, event: AstrMessageEvent, ) -> str | None: try: conversation_persona_id = await resolve_event_conversation_persona_id( @@ -236,7 +235,7 @@ class ThirdPartyAgentSubStage(Stage): if runner.done(): final_chain, is_runner_error = aggregator.finalize( - runner.get_final_llm_resp() + runner.get_final_llm_resp(), ) event.set_extra(THIRD_PARTY_RUNNER_ERROR_EXTRA_KEY, is_runner_error) event.set_result( @@ -288,7 +287,7 @@ class ThirdPartyAgentSubStage(Stage): req: ProviderRequest | None = None if self.provider_wake_prefix and not event.message_str.startswith( - self.provider_wake_prefix + self.provider_wake_prefix, ): return @@ -301,7 +300,7 @@ class ThirdPartyAgentSubStage(Stage): return if not self.prov_cfg: logger.error( - f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。" + f"Agent Runner 提供商 {self.prov_id} 配置不存在,请前往配置页面修改配置。", ) return @@ -327,7 +326,7 @@ 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: @@ -414,7 +413,7 @@ class ThirdPartyAgentSubStage(Stage): ) if provider is None: raise ValueError( - "No active provider is available for third-party runner" + "No active provider is available for third-party runner", ) await runner.reset( diff --git a/astrbot/core/pipeline/process_stage/method/star_request.py b/astrbot/core/pipeline/process_stage/method/star_request.py index dbada63c3..ced4c5594 100644 --- a/astrbot/core/pipeline/process_stage/method/star_request.py +++ b/astrbot/core/pipeline/process_stage/method/star_request.py @@ -60,7 +60,7 @@ 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 2714aface..346703b7c 100644 --- a/astrbot/core/pipeline/process_stage/stage.py +++ b/astrbot/core/pipeline/process_stage/stage.py @@ -17,7 +17,7 @@ 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/respond/stage.py b/astrbot/core/pipeline/respond/stage.py index 179700a7a..d3651f78f 100644 --- a/astrbot/core/pipeline/respond/stage.py +++ b/astrbot/core/pipeline/respond/stage.py @@ -38,7 +38,7 @@ class RespondStage(Stage): ), # 音乐分享 Comp.Forward: lambda comp: bool(comp.id), # 合并转发 Comp.Location: lambda comp: bool( - comp.lat is not None and comp.lon is not None + comp.lat is not None and comp.lon is not None, ), # 位置 Comp.Contact: lambda comp: bool(comp._type and comp.id), # 推荐好友 or 群 Comp.Shake: lambda _: True, # 窗口抖动(戳一戳) diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 9546b6cd5..3f57518e2 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -88,7 +88,7 @@ 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 diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index e49dbd14b..4a81e02d5 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -94,7 +94,7 @@ 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 b40359129..5307071e0 100644 --- a/astrbot/core/pipeline/waking_check/stage.py +++ b/astrbot/core/pipeline/waking_check/stage.py @@ -68,7 +68,7 @@ class WakingCheckStage(Stage): False, ) self.disable_builtin_commands = self.ctx.astrbot_config.get( - "disable_builtin_commands", False + "disable_builtin_commands", False, ) platform_settings = self.ctx.astrbot_config.get("platform_settings", {}) self.unique_session = platform_settings.get("unique_session", False) diff --git a/astrbot/core/platform/astr_message_event.py b/astrbot/core/platform/astr_message_event.py index 59818260e..b750d3592 100644 --- a/astrbot/core/platform/astr_message_event.py +++ b/astrbot/core/platform/astr_message_event.py @@ -65,7 +65,7 @@ class AstrMessageEvent(abc.ABC): except (ValueError, TypeError, AttributeError): logger.warning( f"Failed to convert message type {message_obj.type!r} to MessageType. " - f"Falling back to FRIEND_MESSAGE." + f"Falling back to FRIEND_MESSAGE.", ) message_type = MessageType.FRIEND_MESSAGE self.session = MessageSession( diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index b27735265..1adb16ce8 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -252,7 +252,7 @@ class PlatformManager: logger.error(traceback.format_exc()) async def _task_wrapper( - self, task: asyncio.Task, platform: Platform | None = None + self, task: asyncio.Task, platform: Platform | None = None, ) -> None: # 设置平台状态为运行中 if platform: @@ -333,6 +333,7 @@ class PlatformManager: Returns: 包含所有平台统计信息的字典 + """ stats_list = [] total_errors = 0 @@ -358,7 +359,7 @@ class PlatformManager: "status": "unknown", "error_count": 0, "last_error": None, - } + }, ) return { diff --git a/astrbot/core/platform/platform.py b/astrbot/core/platform/platform.py index 5c5b57dff..eb233a666 100644 --- a/astrbot/core/platform/platform.py +++ b/astrbot/core/platform/platform.py @@ -84,7 +84,7 @@ class Platform(abc.ABC): """是否正在使用统一 Webhook 模式""" return bool( self.config.get("unified_webhook_mode", False) - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) def get_stats(self) -> dict: @@ -161,5 +161,6 @@ class Platform(abc.ABC): Raises: NotImplementedError: 平台未实现统一 Webhook 模式 + """ raise NotImplementedError(f"平台 {self.meta().name} 未实现统一 Webhook 模式") diff --git a/astrbot/core/platform/register.py b/astrbot/core/platform/register.py index bdf728105..6167cdbb7 100644 --- a/astrbot/core/platform/register.py +++ b/astrbot/core/platform/register.py @@ -73,6 +73,7 @@ def unregister_platform_adapters_by_module(module_path_prefix: str) -> list[str] Returns: 被注销的平台适配器名称列表 + """ unregistered = [] to_remove = [] diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 099c79d13..36f1ec725 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -68,8 +68,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): is_group: bool, session_id: str | None, ) -> bool: - """ - 尝试普通发送,若失败且消息中包含本地文件,则尝试通过流式上传重发。 + """尝试普通发送,若失败且消息中包含本地文件,则尝试通过流式上传重发。 返回 True 表示发送成功(含重试成功),False 表示失败且无需继续。 抛出异常表示需要上层处理(如取消任务等)。 """ @@ -88,7 +87,7 @@ class AiocqhttpMessageEvent(AstrMessageEvent): modified = True except Exception as upload_err: raise RuntimeError( - f"NapCat 文件流式上传失败: {upload_err}" + f"NapCat 文件流式上传失败: {upload_err}", ) from upload_err new_chain.chain.append(new_seg) if not modified: @@ -160,7 +159,7 @@ 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}") @@ -168,7 +167,7 @@ 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}") @@ -314,7 +313,7 @@ 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 @@ -358,11 +357,11 @@ 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 ae63370f6..a551f08e3 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_platform_adapter.py @@ -34,7 +34,7 @@ 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 +99,7 @@ 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 +132,7 @@ class AiocqhttpAdapter(Platform): abm = AstrBotMessage() abm.self_id = str(event.self_id) abm.sender = MessageMember( - user_id=str(event.user_id), nickname=str(event.user_id) + user_id=str(event.user_id), nickname=str(event.user_id), ) abm.type = MessageType.OTHER_MESSAGE if event.get("group_id"): @@ -157,7 +157,7 @@ class AiocqhttpAdapter(Platform): abm = AstrBotMessage() abm.self_id = str(event.self_id) abm.sender = MessageMember( - user_id=str(event.user_id), nickname=str(event.user_id) + user_id=str(event.user_id), nickname=str(event.user_id), ) abm.type = MessageType.OTHER_MESSAGE if event.get("group_id"): @@ -181,7 +181,7 @@ 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,17 +276,17 @@ 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) if not new_event: logger.error( - f"无法从回复消息数据构造 Event 对象: {reply_event_data}" + f"无法从回复消息数据构造 Event 对象: {reply_event_data}", ) 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 +326,7 @@ 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)) @@ -351,14 +351,14 @@ class AiocqhttpAdapter(Platform): try: if t not in ComponentTypes: logger.warning( - f"不支持的消息段类型,已忽略: {t}, data={m['data']}" + f"不支持的消息段类型,已忽略: {t}, data={m['data']}", ) continue a = ComponentTypes[t](**m["data"]) abm.message.append(a) except Exception as e: logger.exception( - f"消息段解析失败: type={t}, data={m['data']}. {e}" + f"消息段解析失败: type={t}, data={m['data']}. {e}", ) continue abm.timestamp = int(time.time()) @@ -369,7 +369,7 @@ class AiocqhttpAdapter(Platform): def run(self) -> Coroutine[Any, Any, None]: if not self.host or not self.port: logger.warning( - "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199" + "aiocqhttp: 未配置 ws_reverse_host 或 ws_reverse_port,将使用默认值:http://0.0.0.0:6199", ) self.host = "0.0.0.0" self.port = 6199 diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index 13fa7ad8c..52036853b 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -41,11 +41,11 @@ 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 +65,7 @@ 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 +79,7 @@ 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: @@ -93,21 +93,21 @@ class DingtalkPlatformAdapter(Platform): staff_id = await self._get_sender_staff_id(session) if not staff_id: logger.warning( - "钉钉私聊会话缺少 staff_id 映射,回退使用 session_id 作为 userId 发送" + "钉钉私聊会话缺少 staff_id 映射,回退使用 session_id 作为 userId 发送", ) staff_id = session.session_id await self.send_message_chain_to_user( - 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 +121,7 @@ class DingtalkPlatformAdapter(Platform): ) async def convert_msg( - self, message: dingtalk_stream.ChatbotMessage + self, message: dingtalk_stream.ChatbotMessage, ) -> AstrBotMessage: abm = AstrBotMessage() abm.message = [] @@ -133,7 +133,7 @@ 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 +169,7 @@ 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)) @@ -192,11 +192,11 @@ class DingtalkPlatformAdapter(Platform): continue if not robot_code: logger.error( - "钉钉富文本图片消息解析失败: 回调中缺少 robotCode" + "钉钉富文本图片消息解析失败: 回调中缺少 robotCode", ) 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 +213,7 @@ 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 +231,7 @@ 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 +241,7 @@ 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: @@ -253,16 +253,16 @@ class DingtalkPlatformAdapter(Platform): platform_name=self.meta().id, message_type=abm.type, session_id=sender_id, - ) + ), ) 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 +304,7 @@ 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,11 +313,11 @@ 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( - f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}" + f"获取钉钉机器人 access_token 失败: {resp.status}, {await resp.text()}", ) return "" data = await resp.json() @@ -326,7 +326,7 @@ 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 +334,7 @@ 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,19 +350,18 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( - "https://api.dingtalk.com/v1.0/robot/groupMessages/send", - headers=headers, - json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉群消息发送失败: {resp.status}, {await resp.text()}" - ) + async with aiohttp.ClientSession() as session, session.post( + "https://api.dingtalk.com/v1.0/robot/groupMessages/send", + headers=headers, + json=payload, + ) as resp: + if resp.status != 200: + logger.error( + f"钉钉群消息发送失败: {resp.status}, {await resp.text()}", + ) async def _send_private_message( - self, 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: @@ -378,16 +377,15 @@ class DingtalkPlatformAdapter(Platform): "Content-Type": "application/json", "x-acs-dingtalk-access-token": access_token, } - async with aiohttp.ClientSession() as session: - async with session.post( - "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", - headers=headers, - json=payload, - ) as resp: - if resp.status != 200: - logger.error( - f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}" - ) + async with aiohttp.ClientSession() as session, session.post( + "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend", + headers=headers, + json=payload, + ) as resp: + if resp.status != 200: + logger.error( + f"钉钉私聊消息发送失败: {resp.status}, {await resp.text()}", + ) def _safe_remove_file(self, file_path: str | None) -> None: if not file_path: @@ -432,7 +430,7 @@ class DingtalkPlatformAdapter(Platform): ) as resp: if resp.status != 200: logger.error( - f"钉钉媒体上传失败: {resp.status}, {await resp.text()}" + f"钉钉媒体上传失败: {resp.status}, {await resp.text()}", ) return "" data = await resp.json() @@ -487,7 +485,7 @@ 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 @@ -625,7 +623,7 @@ 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 66d502523..7e86c5594 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -39,7 +39,7 @@ else: # 注册平台适配器 @register_platform_adapter( - "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False + "discord", "Discord 适配器 (基于 Pycord)", support_streaming_message=False, ) class DiscordPlatformAdapter(Platform): def __init__( @@ -68,7 +68,7 @@ class DiscordPlatformAdapter(Platform): """通过会话发送消息""" if self.client.user is None: logger.error( - "[Discord] Client is not ready (self.client.user is None); message send skipped" + "[Discord] Client is not ready (self.client.user is None); message send skipped", ) return @@ -99,7 +99,7 @@ class DiscordPlatformAdapter(Platform): user_id=str(self.bot_self_id), nickname=self.client.user.display_name, ) - message_obj.self_id = cast(str, self.bot_self_id) + message_obj.self_id = cast("str", self.bot_self_id) message_obj.session_id = session.session_id message_obj.message = message_chain.chain @@ -141,7 +141,7 @@ class DiscordPlatformAdapter(Platform): token = str(self.config.get("discord_token")) if not token: logger.error( - "[Discord] Bot token is not configured. Please set a valid token in the config file." + "[Discord] Bot token is not configured. Please set a valid token in the config file.", ) return @@ -160,7 +160,7 @@ class DiscordPlatformAdapter(Platform): ) except Exception as e: logger.error( - f"[Discord] on_ready_once_callback err: {e}", exc_info=True + f"[Discord] on_ready_once_callback err: {e}", exc_info=True, ) self.client.on_ready_once_callback = callback @@ -170,7 +170,7 @@ class DiscordPlatformAdapter(Platform): await self.shutdown_event.wait() except discord.errors.LoginFailure: logger.error( - "[Discord] Login failed. Please check whether the bot token is correct." + "[Discord] Login failed. Please check whether the bot token is correct.", ) except discord.errors.ConnectionClosed: logger.warning("[Discord] Connection with Discord has been closed.") @@ -193,14 +193,13 @@ 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)) def _convert_message_to_abm(self, data: dict) -> AstrBotMessage: """将普通消息转换为 AstrBotMessage""" - message = data["message"] content = message.content # 如果机器人被@,移除@部分 @@ -246,7 +245,7 @@ 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)) @@ -264,7 +263,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = message_chain abm.raw_message = message - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = cast("str", self.bot_self_id) abm.session_id = str(message.channel.id) abm.message_id = str(message.id) return abm @@ -287,7 +286,7 @@ class DiscordPlatformAdapter(Platform): if self.client.user is None: logger.error( - "[Discord] Client is not ready (self.client.user is None); message handling skipped" + "[Discord] Client is not ready (self.client.user is None); message handling skipped", ) return @@ -306,7 +305,7 @@ class DiscordPlatformAdapter(Platform): raw_message = message.raw_message if not isinstance(raw_message, discord.Message): logger.warning( - f"[Discord] Non-Message type received and ignored: {type(raw_message)}" + f"[Discord] Non-Message type received and ignored: {type(raw_message)}", ) return @@ -362,7 +361,7 @@ class DiscordPlatformAdapter(Platform): logger.info("[Discord] Commands cleaned up successfully.") except Exception as e: logger.warning( - f"[Discord] Error occurred while cleaning up commands: {e}" + f"[Discord] Error occurred while cleaning up commands: {e}", ) if self._polling_task: @@ -373,7 +372,7 @@ class DiscordPlatformAdapter(Platform): logger.info("[Discord] Polling task cancelled successfully.") except Exception as e: logger.warning( - f"[Discord] Error occurred while cancelling polling task: {e}" + f"[Discord] Error occurred while cancelling polling task: {e}", ) logger.info("[Discord] Closing client connection...") if self.client and hasattr(self.client, "close"): @@ -431,7 +430,7 @@ class DiscordPlatformAdapter(Platform): except HTTPException as exc: if getattr(exc, "code", None) == 30034: logger.warning( - "[Discord] 跳过指令同步:已达到 Discord 每日 application command create 限额。" + "[Discord] 跳过指令同步:已达到 Discord 每日 application command create 限额。", ) return raise @@ -453,7 +452,7 @@ class DiscordPlatformAdapter(Platform): if cmd_name in command_dict: logger.warning( f"命令名 '{cmd_name}' 重复注册,将使用首次注册的定义: " - f"'{command_dict[cmd_name]}'" + f"'{command_dict[cmd_name]}'", ) command_dict.setdefault(cmd_name, description) @@ -509,7 +508,7 @@ class DiscordPlatformAdapter(Platform): """为每个指令动态创建一个异步回调函数""" async def dynamic_callback( - ctx: discord.ApplicationContext, params: str | None = None + ctx: discord.ApplicationContext, params: str | None = None, ) -> None: # 将平台特定的前缀'/'剥离,以适配通用的CommandFilter logger.debug(f"[Discord] Callback triggered: {cmd_name}") @@ -566,7 +565,7 @@ class DiscordPlatformAdapter(Platform): ) abm.message = [Plain(text=message_str_for_filter)] abm.raw_message = ctx.interaction - abm.self_id = cast(str, self.bot_self_id) + abm.self_id = cast("str", self.bot_self_id) abm.session_id = str(ctx.channel_id) abm.message_id = ( str(getattr(ctx.interaction, "id", ctx.interaction)) diff --git a/astrbot/core/platform/sources/discord/discord_platform_event.py b/astrbot/core/platform/sources/discord/discord_platform_event.py index 96f2fd17c..05820200a 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_event.py +++ b/astrbot/core/platform/sources/discord/discord_platform_event.py @@ -86,7 +86,7 @@ class DiscordPlatformEvent(AstrMessageEvent): await super().send(message) async def send_streaming( - self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False + self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False, ): buffer = None async for chain in generator: @@ -107,14 +107,14 @@ class DiscordPlatformEvent(AstrMessageEvent): try: channel_id = int(self.session_id) return self.client.get_channel( - channel_id + channel_id, ) or await self.client.fetch_channel(channel_id) except (ValueError, discord.errors.NotFound, discord.errors.Forbidden): logger.error(f"[Discord] 无法获取频道 {self.session_id}") return None async def _parse_to_discord( - self, message: MessageChain + self, message: MessageChain, ) -> tuple[ str, list[discord.File], @@ -155,7 +155,7 @@ 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 +167,7 @@ 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,17 +178,17 @@ 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( - f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}" + f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}", ) path = Path(file_content) 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}") @@ -208,11 +208,11 @@ class DiscordPlatformEvent(AstrMessageEvent): if await asyncio.to_thread(path.exists): file_bytes = await asyncio.to_thread(path.read_bytes) files.append( - discord.File(BytesIO(file_bytes), filename=i.name) + discord.File(BytesIO(file_bytes), filename=i.name), ) else: logger.warning( - f"[Discord] 获取文件失败,路径不存在: {file_path_str}" + f"[Discord] 获取文件失败,路径不存在: {file_path_str}", ) else: logger.warning(f"[Discord] 获取文件失败: {i.name}") @@ -237,7 +237,7 @@ 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 +274,7 @@ 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 +285,7 @@ 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 73090e101..f778c22a1 100644 --- a/astrbot/core/platform/sources/kook/kook_adapter.py +++ b/astrbot/core/platform/sources/kook/kook_adapter.py @@ -43,7 +43,7 @@ KOOK_AT_SELECTOR_REGEX = re.compile(r"\(met\)([^()]+)\(met\)") ) class KookPlatformAdapter(Platform): def __init__( - self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue + self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue, ) -> None: super().__init__(platform_config, event_queue) self.kook_config = KookConfig.from_dict(platform_config) @@ -55,7 +55,7 @@ class KookPlatformAdapter(Platform): self._main_task = None async def send_by_session( - self, session: MessageSesion, message_chain: MessageChain + self, session: MessageSesion, message_chain: MessageChain, ): inner_message = AstrBotMessage() inner_message.session_id = session.session_id @@ -71,7 +71,7 @@ class KookPlatformAdapter(Platform): def meta(self) -> PlatformMetadata: return PlatformMetadata( - name="kook", description="KOOK 适配器", id=self.kook_config.id + name="kook", description="KOOK 适配器", id=self.kook_config.id, ) def _should_ignore_event_by_bot_nickname(self, author_id: str) -> bool: @@ -79,7 +79,7 @@ class KookPlatformAdapter(Platform): async def _on_received(self, event: KookMessageEventData): logger.debug( - f'[KOOK] 收到来自"{event.channel_type.name}"渠道的消息, 消息类型为: {event.type.name}({event.type.value})' + f'[KOOK] 收到来自"{event.channel_type.name}"渠道的消息, 消息类型为: {event.type.name}({event.type.value})', ) event_type = event.type if event_type in (KookMessageType.KMARKDOWN, KookMessageType.CARD): @@ -136,7 +136,7 @@ class KookPlatformAdapter(Platform): # 等待 client 内部触发 _stop_event,或者超时 1 秒后重试 # 使用 wait_for 配合 timeout 是为了防止极端情况下 self.running 变化没被察觉 await asyncio.wait_for( - self.client.wait_until_closed(), timeout=1.0 + self.client.wait_until_closed(), timeout=1.0, ) except asyncio.TimeoutError: # 正常超时,继续下一轮 while 检查 @@ -148,7 +148,7 @@ class KookPlatformAdapter(Platform): else: consecutive_failures += 1 logger.error( - f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}" + f"[KOOK] 连接失败,连续失败次数: {consecutive_failures}", ) if consecutive_failures >= max_consecutive_failures: @@ -157,7 +157,7 @@ class KookPlatformAdapter(Platform): # 等待一段时间后重试 wait_time = min( - 2**consecutive_failures, max_retry_delay + 2**consecutive_failures, max_retry_delay, ) # 指数退避 logger.info(f"[KOOK] 等待 {wait_time} 秒后重试...") await asyncio.sleep(wait_time) @@ -192,13 +192,13 @@ class KookPlatformAdapter(Platform): logger.info("[KOOK] 资源清理完成") def _parse_kmarkdown_text_message( - self, data: KookMessageEventData, self_id: str + self, data: KookMessageEventData, self_id: str, ) -> tuple[list, str]: kmarkdown = data.extra.kmarkdown content = data.content or "" if kmarkdown is None: logger.error( - f'[KOOK] 无法转换"{KookMessageType.KMARKDOWN.name}"消息, 消息中找不到kmarkdown字段' + f'[KOOK] 无法转换"{KookMessageType.KMARKDOWN.name}"消息, 消息中找不到kmarkdown字段', ) logger.error(f"[KOOK] 原始消息内容: {data.to_json()}") return [], "" @@ -237,7 +237,7 @@ class KookPlatformAdapter(Platform): At( qq=mention_target, name=mention_name_map.get(mention_target, ""), - ) + ), ) cursor = match.end() @@ -341,7 +341,7 @@ class KookPlatformAdapter(Platform): return "" def _handle_image_group( - self, module: ContainerModule | ImageGroupModule + self, module: ContainerModule | ImageGroupModule, ) -> list[str]: """专门处理图片组/容器里的合法 URL 提取""" valid_urls = [] diff --git a/astrbot/core/platform/sources/kook/kook_client.py b/astrbot/core/platform/sources/kook/kook_client.py index 11a70e1de..68034e2c0 100644 --- a/astrbot/core/platform/sources/kook/kook_client.py +++ b/astrbot/core/platform/sources/kook/kook_client.py @@ -37,7 +37,7 @@ class KookClient: self._http_client = aiohttp.ClientSession( headers={ "Authorization": f"Bot {self.config.token}", - } + }, ) self.event_callback = event_callback # 回调函数,用于处理接收到的事件 self.ws = None @@ -71,21 +71,21 @@ class KookClient: async with self._http_client.get(url) as resp: if resp.status != 200: logger.error( - f"[KOOK] 获取机器人账号信息失败,状态码: {resp.status} , {await resp.text()}" + f"[KOOK] 获取机器人账号信息失败,状态码: {resp.status} , {await resp.text()}", ) return try: resp_content = KookUserMeResponse.from_dict(await resp.json()) except pydantic.ValidationError as e: logger.error( - f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}" + f"[KOOK] 获取机器人账号信息失败, 响应数据格式错误: \n{e}", ) logger.error(f"[KOOK] 响应内容: {await resp.text()}") return if not resp_content.success(): logger.error( - f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}" + f"[KOOK] 获取机器人账号信息失败: {resp_content.model_dump_json()}", ) return @@ -147,7 +147,7 @@ class KookClient: try: # 获取gateway地址 gateway_url = await self.get_gateway_url( - resume=resume, sn=self.last_sn, session_id=self.session_id + resume=resume, sn=self.last_sn, session_id=self.session_id, ) await self.get_bot_info() @@ -249,7 +249,7 @@ class KookClient: case _: logger.debug( - f"[KOOK] 未处理的信令类型: {event.signal.name}({event.signal.value})" + f"[KOOK] 未处理的信令类型: {event.signal.name}({event.signal.value})", ) async def _handle_hello(self, data: KookHelloEventData): @@ -289,7 +289,7 @@ class KookClient: try: # 随机化心跳间隔 (±5秒) interval = max( - 1, self.config.heartbeat_interval + random.randint(-5, 5) + 1, self.config.heartbeat_interval + random.randint(-5, 5), ) await asyncio.sleep(interval) @@ -309,7 +309,7 @@ class KookClient: ): self.heartbeat_failed_count += 1 logger.warning( - f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}" + f"[KOOK] 心跳超时,失败次数: {self.heartbeat_failed_count}", ) if ( @@ -372,19 +372,19 @@ class KookClient: result = await resp.json() if result.get("code") != 0: raise RuntimeError( - f'发送kook消息类型 "{kook_message_type.name}" 失败: {result}' + f'发送kook消息类型 "{kook_message_type.name}" 失败: {result}', ) # else: # logger.info("[KOOK] 发送消息成功") else: raise RuntimeError( - f'发送kook消息类型 "{kook_message_type.name}" HTTP错误: {resp.status} , 响应内容 : {await resp.text()}' + f'发送kook消息类型 "{kook_message_type.name}" HTTP错误: {resp.status} , 响应内容 : {await resp.text()}', ) except RuntimeError: raise except Exception as e: logger.error( - f'[KOOK] 发送kook消息类型 "{kook_message_type.name}" 异常: {e}' + f'[KOOK] 发送kook消息类型 "{kook_message_type.name}" 异常: {e}', ) async def upload_asset(self, file_url: str | None) -> str: @@ -414,7 +414,7 @@ class KookClient: except Exception as exp: logger.error(f'[KOOK] 获取文件 "{file_url}" 绝对路径失败: "{exp}"') raise FileNotFoundError( - f'获取文件 "{file_url}" 绝对路径失败: "{exp}"' + f'获取文件 "{file_url}" 绝对路径失败: "{exp}"', ) from exp if not await target_path.is_file(): @@ -441,12 +441,10 @@ class KookClient: remote_url = result["data"]["url"] logger.debug(f"[KOOK] 文件远端URL: {remote_url}") return remote_url - else: - raise RuntimeError(f"上传文件到kook服务器失败: {result}") - else: - raise RuntimeError( - f"上传文件到kook服务器 HTTP错误: {resp.status} , {await resp.text()}" - ) + raise RuntimeError(f"上传文件到kook服务器失败: {result}") + raise RuntimeError( + f"上传文件到kook服务器 HTTP错误: {resp.status} , {await resp.text()}", + ) except RuntimeError: raise except Exception as e: diff --git a/astrbot/core/platform/sources/kook/kook_event.py b/astrbot/core/platform/sources/kook/kook_event.py index c235ded54..2ba0c74d6 100644 --- a/astrbot/core/platform/sources/kook/kook_event.py +++ b/astrbot/core/platform/sources/kook/kook_event.py @@ -48,10 +48,10 @@ class KookEvent(AstrMessageEvent): self._file_message_counter = 0 def _wrap_message( - self, index: int, message_component: BaseMessageComponent + self, index: int, message_component: BaseMessageComponent, ) -> Coroutine[Any, Any, OrderMessage]: async def wrap_upload( - index: int, message_type: KookMessageType, upload_coro + index: int, message_type: KookMessageType, upload_coro, ) -> OrderMessage: url = await upload_coro return OrderMessage(index=index, text=url, type=message_type) @@ -93,7 +93,7 @@ class KookEvent(AstrMessageEvent): f_data = await f_item.get_file() url = await self.client.upload_asset(f_data) return OrderMessage( - index=index, text=url, type=KookMessageType.FILE + index=index, text=url, type=KookMessageType.FILE, ) self._file_message_counter += 1 @@ -115,10 +115,10 @@ class KookEvent(AstrMessageEvent): type=KookModuleType.AUDIO, title=title, src=url, - ) - ] - ) - ] + ), + ], + ), + ], ).to_json(), type=KookMessageType.CARD, ) @@ -147,7 +147,7 @@ class KookEvent(AstrMessageEvent): ) case _: raise NotImplementedError( - f'kook适配器尚未实现对 "{message_component.type}" 消息类型的支持' + f'kook适配器尚未实现对 "{message_component.type}" 消息类型的支持', ) async def send(self, message: MessageChain): diff --git a/astrbot/core/platform/sources/kook/kook_types.py b/astrbot/core/platform/sources/kook/kook_types.py index 23442db57..47e11d266 100644 --- a/astrbot/core/platform/sources/kook/kook_types.py +++ b/astrbot/core/platform/sources/kook/kook_types.py @@ -57,7 +57,7 @@ class KookModuleType(str, Enum): ThemeType = Literal[ - "primary", "success", "danger", "warning", "info", "secondary", "none", "invisible" + "primary", "success", "danger", "warning", "info", "secondary", "none", "invisible", ] """主题,可选的值为:primary, success, danger, warning, info, secondary, none.默认为 primary,为 none 时不显示侧边框。""" SizeType = Literal["xs", "sm", "md", "lg"] @@ -276,7 +276,7 @@ class KookCardMessageContainer(list[KookCardMessage]): def to_json(self, indent: int | None = None, ensure_ascii: bool = True) -> str: return json.dumps( - [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii + [i.to_dict() for i in self], indent=indent, ensure_ascii=ensure_ascii, ) @classmethod @@ -293,7 +293,8 @@ class OrderMessage(BaseModel): class KookMessageSignal(IntEnum): """KOOK WebSocket 信令类型 - ws文档: https://developer.kookapp.cn/doc/websocket""" + ws文档: https://developer.kookapp.cn/doc/websocket + """ MESSAGE = 0 """server->client 消息(s包含聊天和通知消息)""" @@ -350,7 +351,7 @@ class KookExtra(KookBaseDataClass): class KookMessageEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.MESSAGE] = Field( - KookMessageSignal.MESSAGE, exclude=True + KookMessageSignal.MESSAGE, exclude=True, ) """only for type hint""" @@ -368,7 +369,7 @@ class KookMessageEventData(KookBaseDataClass): class KookHelloEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.HELLO] = Field( - KookMessageSignal.HELLO, exclude=True + KookMessageSignal.HELLO, exclude=True, ) """only for type hint""" @@ -378,28 +379,28 @@ class KookHelloEventData(KookBaseDataClass): class KookPingEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.PING] = Field( - KookMessageSignal.PING, exclude=True + KookMessageSignal.PING, exclude=True, ) """only for type hint""" class KookPongEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.PONG] = Field( - KookMessageSignal.PONG, exclude=True + KookMessageSignal.PONG, exclude=True, ) """only for type hint""" class KookResumeEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RESUME] = Field( - KookMessageSignal.RESUME, exclude=True + KookMessageSignal.RESUME, exclude=True, ) """only for type hint""" class KookReconnectEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RECONNECT] = Field( - KookMessageSignal.RECONNECT, exclude=True + KookMessageSignal.RECONNECT, exclude=True, ) """only for type hint""" @@ -409,7 +410,7 @@ class KookReconnectEventData(KookBaseDataClass): class KookResumeAckEventData(KookBaseDataClass): signal: Literal[KookMessageSignal.RESUME_ACK] = Field( - KookMessageSignal.RESUME_ACK, exclude=True + KookMessageSignal.RESUME_ACK, exclude=True, ) """only for type hint""" @@ -420,7 +421,7 @@ class KookWebsocketEvent(KookBaseDataClass): """KOOK WebSocket 原始推送结构""" signal: KookMessageSignal = Field( - ..., validation_alias="s", serialization_alias="s" + ..., validation_alias="s", serialization_alias="s", ) """信令类型""" data: Annotated[ diff --git a/astrbot/core/platform/sources/lark/lark_adapter.py b/astrbot/core/platform/sources/lark/lark_adapter.py index 36eeab2c8..4ed4c201b 100644 --- a/astrbot/core/platform/sources/lark/lark_adapter.py +++ b/astrbot/core/platform/sources/lark/lark_adapter.py @@ -32,11 +32,11 @@ 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 +81,7 @@ 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 模块未初始化") @@ -96,7 +96,7 @@ class LarkPlatformAdapter(Platform): response = await self.lark_api.im.v1.message_resource.aget(request) if not response.success(): logger.error( - f"[Lark] 下载消息资源失败 type={resource_type}, key={file_key}, code={response.code}, msg={response.msg}" + f"[Lark] 下载消息资源失败 type={resource_type}, key={file_key}, code={response.code}, msg={response.msg}", ) return None if response.file is None: @@ -212,7 +212,7 @@ 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 +295,7 @@ 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 模块未初始化") @@ -304,7 +304,7 @@ class LarkPlatformAdapter(Platform): response = await self.lark_api.im.v1.message.aget(request) if not response.success(): logger.error( - f"[Lark] 获取引用消息失败 id={parent_message_id}, code={response.code}, msg={response.msg}" + f"[Lark] 获取引用消息失败 id={parent_message_id}, code={response.code}, msg={response.msg}", ) return None if response.data is None or not response.data.items: @@ -366,7 +366,7 @@ 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 @@ -398,6 +398,7 @@ class LarkPlatformAdapter(Platform): Returns: True 表示重复事件,False 表示新事件 + """ self._clean_expired_events() if event_id in self.event_id_timestamps: @@ -406,7 +407,7 @@ 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" @@ -417,7 +418,7 @@ 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) @@ -523,6 +524,7 @@ class LarkPlatformAdapter(Platform): Args: event_data: Webhook 事件数据 + """ try: header = event_data.get("header", {}) @@ -570,5 +572,5 @@ class LarkPlatformAdapter(Platform): def unified_webhook(self) -> bool: return bool( self.config.get("lark_connection_mode", "") == "webhook" - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) diff --git a/astrbot/core/platform/sources/lark/lark_event.py b/astrbot/core/platform/sources/lark/lark_event.py index e916b1ec7..3a31c0c73 100644 --- a/astrbot/core/platform/sources/lark/lark_event.py +++ b/astrbot/core/platform/sources/lark/lark_event.py @@ -74,6 +74,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 是否发送成功 + """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -89,7 +90,7 @@ class LarkMessageEvent(AstrMessageEvent): .msg_type(msg_type) .uuid(str(uuid.uuid4())) .reply_in_thread(False) - .build() + .build(), ) .build() ) @@ -115,7 +116,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .msg_type(msg_type) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -145,6 +146,7 @@ class LarkMessageEvent(AstrMessageEvent): Returns: 成功返回file_key,失败返回None + """ if not path or not await asyncio.to_thread(os.path.exists, path): logger.error(f"[Lark] 文件不存在: {path}") @@ -317,7 +319,7 @@ class LarkMessageEvent(AstrMessageEvent): { "tag": "markdown", "content": reasoning_content, - } + }, ], } @@ -331,8 +333,8 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=title, expanded=False, - ) - ] + ), + ], }, } @@ -351,7 +353,7 @@ class LarkMessageEvent(AstrMessageEvent): reasoning_content=reasoning_content, title=str(comp.data.get("title", "💭 Thinking")), expanded=bool(comp.data.get("expanded", False)), - ) + ), ) elif isinstance(comp, Plain): if comp.text: @@ -389,9 +391,9 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) - .build() + .build(), ) except Exception as e: logger.error(f"[Lark] 创建卡片失败: {e}") @@ -456,6 +458,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型,如 'open_id', 'chat_id'(用于主动发送) + """ if lark_client.im is None: logger.error("[Lark] API Client im 模块未初始化") @@ -564,17 +567,17 @@ class LarkMessageEvent(AstrMessageEvent): # 发送附件 for file_comp in file_components: await LarkMessageEvent._send_file_message( - file_comp, lark_client, reply_message_id, receive_id, receive_id_type + file_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) for audio_comp in audio_components: await LarkMessageEvent._send_audio_message( - audio_comp, lark_client, reply_message_id, receive_id, receive_id_type + audio_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) for media_comp in media_components: await LarkMessageEvent._send_media_message( - media_comp, lark_client, reply_message_id, receive_id, receive_id_type + media_comp, lark_client, reply_message_id, receive_id, receive_id_type, ) async def send(self, message: MessageChain) -> None: @@ -602,10 +605,11 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ file_path = file_comp.file or "" file_key = await LarkMessageEvent._upload_lark_file( - lark_client, path=file_path, file_type="stream" + lark_client, path=file_path, file_type="stream", ) if not file_key: return @@ -636,6 +640,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取音频文件路径 try: @@ -645,7 +650,7 @@ 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 @@ -677,7 +682,7 @@ 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) @@ -713,6 +718,7 @@ class LarkMessageEvent(AstrMessageEvent): reply_message_id: 回复的消息ID(用于回复消息) receive_id: 接收者ID(用于主动发送) receive_id_type: 接收者ID类型(用于主动发送) + """ # 获取视频文件路径 try: @@ -722,7 +728,7 @@ 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 @@ -754,7 +760,7 @@ 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) @@ -821,8 +827,8 @@ class LarkMessageEvent(AstrMessageEvent): "tag": "markdown", "content": "", "element_id": "markdown_1", - } - ] + }, + ], }, } @@ -832,7 +838,7 @@ class LarkMessageEvent(AstrMessageEvent): CreateCardRequestBody.builder() .type("card_json") .data(json.dumps(card_json, ensure_ascii=False)) - .build() + .build(), ) .build() ) @@ -845,7 +851,7 @@ class LarkMessageEvent(AstrMessageEvent): if not response.success(): logger.error( - f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}" + f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}", ) return None @@ -898,7 +904,7 @@ class LarkMessageEvent(AstrMessageEvent): .content(content) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -938,7 +944,7 @@ class LarkMessageEvent(AstrMessageEvent): .settings(settings_json) .sequence(sequence) .uuid(str(uuid.uuid4())) - .build() + .build(), ) .build() ) @@ -1064,7 +1070,7 @@ class LarkMessageEvent(AstrMessageEvent): card_id = await self._create_streaming_card() if not card_id: logger.warning( - "[Lark] 无法创建流式卡片,回退到非流式发送" + "[Lark] 无法创建流式卡片,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1075,7 +1081,7 @@ class LarkMessageEvent(AstrMessageEvent): ) if not sent: logger.error( - "[Lark] 发送流式卡片消息失败,回退到非流式发送" + "[Lark] 发送流式卡片消息失败,回退到非流式发送", ) await _consume_rest_and_fallback(generator, delta) return @@ -1094,7 +1100,7 @@ class LarkMessageEvent(AstrMessageEvent): if card_id is None: if not fallback_used: await Metric.upload( - msg_event_tick=1, adapter_name=self.platform_meta.name + msg_event_tick=1, adapter_name=self.platform_meta.name, ) self._has_send_oper = True return diff --git a/astrbot/core/platform/sources/lark/server.py b/astrbot/core/platform/sources/lark/server.py index 1fdcefd7f..f10ce260a 100644 --- a/astrbot/core/platform/sources/lark/server.py +++ b/astrbot/core/platform/sources/lark/server.py @@ -58,6 +58,7 @@ class LarkWebhookServer: Args: config: 飞书配置 event_queue: 事件队列 + """ self.app_id = config["app_id"] self.app_secret = config["app_secret"] @@ -91,6 +92,7 @@ class LarkWebhookServer: Returns: 签名是否有效 + """ # 拼接字符串: timestamp + nonce + encrypt_key + body bytes_b1 = (timestamp + nonce + encrypt_key).encode("utf-8") @@ -107,6 +109,7 @@ class LarkWebhookServer: Returns: 解密后的事件字典 + """ if not self.cipher: raise ValueError("未配置 encrypt_key,无法解密事件") @@ -122,6 +125,7 @@ class LarkWebhookServer: Returns: 包含 challenge 的响应 + """ challenge = event_data.get("challenge", "") logger.info(f"[Lark Webhook] 收到 challenge 验证请求: {challenge}") @@ -136,6 +140,7 @@ class LarkWebhookServer: Returns: 响应数据 + """ # 获取原始请求体 body = await request.get_data() @@ -158,7 +163,7 @@ class LarkWebhookServer: if timestamp and nonce and signature: if not self.verify_signature( - timestamp, nonce, self.encrypt_key, body, signature + timestamp, nonce, self.encrypt_key, body, signature, ): logger.error("[Lark Webhook] 签名验证失败") return {"error": "Invalid signature"}, 401 @@ -202,5 +207,6 @@ class LarkWebhookServer: Args: callback: 处理事件的异步函数 + """ self.callback = callback diff --git a/astrbot/core/platform/sources/line/line_adapter.py b/astrbot/core/platform/sources/line/line_adapter.py index cd6622127..14319ecb4 100644 --- a/astrbot/core/platform/sources/line/line_adapter.py +++ b/astrbot/core/platform/sources/line/line_adapter.py @@ -69,7 +69,7 @@ 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 +82,11 @@ 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: @@ -171,7 +171,7 @@ class LinePlatformAdapter(Platform): message.get("id") or event.get("webhookEventId") or event.get("deliveryContext", {}).get("deliveryId", "") - or uuid.uuid4().hex + or uuid.uuid4().hex, ) event_timestamp = event.get("timestamp") if isinstance(event_timestamp, int): @@ -265,7 +265,7 @@ 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 +277,7 @@ 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 +291,7 @@ 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 +305,7 @@ 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 +315,7 @@ 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 f0cb16b52..05babbab0 100644 --- a/astrbot/core/platform/sources/line/line_event.py +++ b/astrbot/core/platform/sources/line/line_event.py @@ -208,7 +208,7 @@ class LineMessageEvent(AstrMessageEvent): return [] if len(messages) > 5: logger.warning( - "[LINE] message count exceeds 5, extra segments will be dropped." + "[LINE] message count exceeds 5, extra segments will be dropped.", ) messages = messages[:5] return messages @@ -232,7 +232,7 @@ 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 88619e215..1e3a1744d 100644 --- a/astrbot/core/platform/sources/mattermost/client.py +++ b/astrbot/core/platform/sources/mattermost/client.py @@ -45,7 +45,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost GET {path} failed: {resp.status} {body}" + f"Mattermost GET {path} failed: {resp.status} {body}", ) data = await resp.json() if not isinstance(data, dict): @@ -59,7 +59,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost POST {path} failed: {resp.status} {body}" + f"Mattermost POST {path} failed: {resp.status} {body}", ) data = await resp.json() if not isinstance(data, dict): @@ -82,7 +82,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost download file {file_id} failed: {resp.status} {body}" + f"Mattermost download file {file_id} failed: {resp.status} {body}", ) return await resp.read() @@ -107,7 +107,7 @@ class MattermostClient: if resp.status >= 400: body = await resp.text() raise RuntimeError( - f"Mattermost upload file failed: {resp.status} {body}" + f"Mattermost upload file failed: {resp.status} {body}", ) data = await resp.json() file_infos = data.get("file_infos", []) @@ -139,7 +139,7 @@ class MattermostClient: async def ws_connect(self) -> aiohttp.ClientWebSocketResponse: session = await self.ensure_session() ws_url = self.base_url.replace("https://", "wss://", 1).replace( - "http://", "ws://", 1 + "http://", "ws://", 1, ) ws_url = f"{ws_url}/api/v4/websocket" return await session.ws_connect(ws_url, heartbeat=30.0) @@ -173,7 +173,7 @@ class MattermostClient: file_bytes, file_path.name, mimetypes.guess_type(file_path.name)[0] or "image/jpeg", - ) + ), ) elif isinstance(segment, (File, Record, Video)): if isinstance(segment, File): @@ -190,7 +190,7 @@ class MattermostClient: file_bytes, filename, mimetypes.guess_type(filename)[0] or "application/octet-stream", - ) + ), ) else: logger.debug( @@ -218,7 +218,7 @@ class MattermostClient: file_bytes = await self.download_file(file_id) except Exception as exc: logger.warning( - "Mattermost fetch attachment failed %s: %s", file_id, exc + "Mattermost fetch attachment failed %s: %s", file_id, exc, ) continue diff --git a/astrbot/core/platform/sources/mattermost/mattermost_adapter.py b/astrbot/core/platform/sources/mattermost/mattermost_adapter.py index 583e0d0af..85ba2df91 100644 --- a/astrbot/core/platform/sources/mattermost/mattermost_adapter.py +++ b/astrbot/core/platform/sources/mattermost/mattermost_adapter.py @@ -41,7 +41,7 @@ class MattermostPlatformAdapter(Platform): self.base_url = str(platform_config.get("mattermost_url", "")).rstrip("/") self.bot_token = str(platform_config.get("mattermost_bot_token", "")).strip() self.reconnect_delay = float( - platform_config.get("mattermost_reconnect_delay", 5.0) + platform_config.get("mattermost_reconnect_delay", 5.0), ) if not self.base_url: @@ -53,7 +53,7 @@ class MattermostPlatformAdapter(Platform): self.metadata = PlatformMetadata( name="mattermost", description="Mattermost 平台适配器", - id=cast(str, self.config.get("id", "mattermost")), + id=cast("str", self.config.get("id", "mattermost")), support_streaming_message=False, ) self.bot_self_id = "" @@ -112,7 +112,7 @@ class MattermostPlatformAdapter(Platform): "seq": 1, "action": "authentication_challenge", "data": {"token": self.bot_token}, - } + }, ) async for message in ws: @@ -229,7 +229,7 @@ class MattermostPlatformAdapter(Platform): temp_paths, ) = await self.client.parse_post_attachments(file_ids) abm.message.extend(attachment_components) - setattr(abm, "temporary_file_paths", temp_paths) + abm.temporary_file_paths = temp_paths abm.message_str = self._build_message_str( abm.message, diff --git a/astrbot/core/platform/sources/mattermost/mattermost_event.py b/astrbot/core/platform/sources/mattermost/mattermost_event.py index 5faaf7134..1d67c3d92 100644 --- a/astrbot/core/platform/sources/mattermost/mattermost_event.py +++ b/astrbot/core/platform/sources/mattermost/mattermost_event.py @@ -44,10 +44,10 @@ class MattermostMessageEvent(AstrMessageEvent): else: message_buffer.chain.extend(chain.chain) if not message_buffer: - return None + return message_buffer.squash_plain() await self.send(message_buffer) - return None + return text_buffer = "" @@ -67,7 +67,7 @@ class MattermostMessageEvent(AstrMessageEvent): if text_buffer.strip(): await self.send(MessageChain([Plain(text_buffer)])) - return None + return async def get_group(self, group_id=None, **kwargs): channel_id = group_id or self.get_group_id() @@ -83,6 +83,6 @@ class MattermostMessageEvent(AstrMessageEvent): MessageMember( user_id=self.get_sender_id(), nickname=self.get_sender_name(), - ) + ), ], ) diff --git a/astrbot/core/platform/sources/misskey/misskey_adapter.py b/astrbot/core/platform/sources/misskey/misskey_adapter.py index 3a331e531..cb6d812ec 100644 --- a/astrbot/core/platform/sources/misskey/misskey_adapter.py +++ b/astrbot/core/platform/sources/misskey/misskey_adapter.py @@ -44,11 +44,11 @@ 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,14 +56,14 @@ 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) self.enable_file_upload = self.config.get("misskey_enable_file_upload", True) self.upload_folder = self.config.get("misskey_upload_folder") self.allow_insecure_downloads = bool( - self.config.get("misskey_allow_insecure_downloads", False) + self.config.get("misskey_allow_insecure_downloads", False), ) _dt = self.config.get("misskey_download_timeout") try: @@ -141,12 +141,12 @@ 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 +166,7 @@ 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 +190,7 @@ 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( @@ -198,7 +198,7 @@ class MisskeyPlatformAdapter(Platform): "poll": extra_data.get("poll"), "renote_id": extra_data.get("renote_id"), "channel_id": extra_data.get("channel_id"), - } + }, ) return fields @@ -217,7 +217,7 @@ class MisskeyPlatformAdapter(Platform): self._register_event_handlers(streaming) if await streaming.connect(): logger.info( - f"[Misskey] WebSocket 已连接 (尝试 #{connection_attempts})" + f"[Misskey] WebSocket 已连接 (尝试 #{connection_attempts})", ) connection_attempts = 0 await streaming.subscribe_channel("main") @@ -229,17 +229,17 @@ class MisskeyPlatformAdapter(Platform): await streaming.listen() else: logger.error( - f"[Misskey] WebSocket 连接失败 (尝试 #{connection_attempts})" + f"[Misskey] WebSocket 连接失败 (尝试 #{connection_attempts})", ) except Exception as e: logger.error( - f"[Misskey] WebSocket 异常 (尝试 #{connection_attempts}): {e}" + f"[Misskey] WebSocket 异常 (尝试 #{connection_attempts}): {e}", ) if self._running: jitter = random.uniform(0, 1.0) sleep_time = backoff_delay + jitter logger.info( - f"[Misskey] {sleep_time:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})" + f"[Misskey] {sleep_time:.1f}秒后重连 (下次尝试 #{connection_attempts + 1})", ) await asyncio.sleep(sleep_time) backoff_delay = min(backoff_delay * backoff_multiplier, max_backoff) @@ -248,13 +248,13 @@ class MisskeyPlatformAdapter(Platform): try: notification_type = data.get("type") logger.debug( - f"[Misskey] 收到通知事件: type={notification_type}, user_id={data.get('userId', 'unknown')}" + f"[Misskey] 收到通知事件: type={notification_type}, user_id={data.get('userId', 'unknown')}", ) if notification_type in ["mention", "reply", "quote"]: note = data.get("note") if note and self._is_bot_mentioned(note): logger.info( - f"[Misskey] 处理贴文提及: {note.get('text', '')[:50]}..." + f"[Misskey] 处理贴文提及: {note.get('text', '')[:50]}...", ) message = await self.convert_message(note) event = MisskeyPlatformEvent( @@ -271,7 +271,7 @@ class MisskeyPlatformAdapter(Platform): async def _handle_chat_message(self, data: dict[str, Any]) -> None: try: sender_id = str( - data.get("fromUserId", "") or data.get("fromUser", {}).get("id", "") + data.get("fromUserId", "") or data.get("fromUser", {}).get("id", ""), ) room_id = data.get("toRoomId") logger.debug( @@ -282,7 +282,7 @@ class MisskeyPlatformAdapter(Platform): if room_id: raw_text = data.get("text", "") logger.debug( - f"[Misskey] 检查群聊消息: '{raw_text}', 机器人用户名: '{self._bot_username}'" + f"[Misskey] 检查群聊消息: '{raw_text}', 机器人用户名: '{self._bot_username}'", ) message = await self.convert_room_message(data) logger.info(f"[Misskey] 处理群聊消息: {message.message_str[:50]}...") @@ -303,7 +303,7 @@ class MisskeyPlatformAdapter(Platform): async def _debug_handler(self, data: dict[str, Any]) -> None: event_type = data.get("type", "unknown") logger.debug( - f"[Misskey] 收到未处理事件: type={event_type}, channel={data.get('channel', 'unknown')}" + f"[Misskey] 收到未处理事件: type={event_type}, channel={data.get('channel', 'unknown')}", ) def _is_bot_mentioned(self, note: dict[str, Any]) -> bool: @@ -323,7 +323,7 @@ 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,13 +362,13 @@ 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) sem = asyncio.Semaphore(upload_concurrency) @@ -386,12 +386,12 @@ class MisskeyPlatformAdapter(Platform): if not self.api: return None url_candidate, local_path = await resolve_component_url_or_path( - comp + comp, ) 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( @@ -449,7 +449,7 @@ class MisskeyPlatformAdapter(Platform): continue if len(file_components) > MAX_FILE_UPLOAD_COUNT: logger.warning( - f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件" + f"[Misskey] 文件数量超过限制 ({len(file_components)} > {MAX_FILE_UPLOAD_COUNT}),只上传前{MAX_FILE_UPLOAD_COUNT}个文件", ) file_components = file_components[:MAX_FILE_UPLOAD_COUNT] upload_tasks = [_upload_comp(comp) for comp in file_components] @@ -499,7 +499,7 @@ class MisskeyPlatformAdapter(Platform): payload["fileId"] = file_ids[0] if len(file_ids) > 1: logger.warning( - f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件" + f"[Misskey] 聊天消息只支持单个文件,忽略其余 {len(file_ids) - 1} 个文件", ) await self.api.send_message(payload) else: @@ -514,7 +514,7 @@ class MisskeyPlatformAdapter(Platform): default_visibility=self.default_visibility, ) logger.debug( - f"[Misskey] 解析可见性: visibility={visibility}, visible_user_ids={visible_user_ids}, session_id={session_id}, user_id_for_cache={user_id_for_cache}" + f"[Misskey] 解析可见性: visibility={visibility}, visible_user_ids={visible_user_ids}, session_id={session_id}, user_id_for_cache={user_id_for_cache}", ) fields = self._extract_additional_fields(session, message_chain) if fallback_urls: diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 611d95c0e..7ae0a2c81 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -127,7 +127,6 @@ class QQOfficialMessageEvent(AstrMessageEvent): except Exception as e: logger.error(f"发送流式消息时出错: {e}", exc_info=True) self.send_buffer = None - return None @staticmethod def _extract_response_message_id(ret) -> str | None: @@ -240,7 +239,7 @@ 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, @@ -305,7 +304,7 @@ 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, @@ -318,7 +317,7 @@ 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, @@ -330,7 +329,7 @@ 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, @@ -343,7 +342,7 @@ 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) @@ -359,7 +358,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): if content and (not content.endswith("\n")): retry_payload["content"] = content + "\n" logger.warning( - "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。" + "[QQOfficial] 流式 markdown 分片换行校验失败,已修正后重试一次。", ) return await send_func(retry_payload) if ( @@ -381,7 +380,7 @@ 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, @@ -405,7 +404,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): raise ValueError("Invalid upload parameters") if not isinstance(result, dict): raise RuntimeError( - f"Failed to upload image, response is not dict: {result}" + f"Failed to upload image, response is not dict: {result}", ) return Media( file_uuid=result["file_uuid"], @@ -523,11 +522,11 @@ 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 8a3a40289..9ab243591 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py @@ -42,10 +42,10 @@ 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 +54,7 @@ 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 +62,10 @@ 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 +73,7 @@ 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") @@ -88,14 +88,14 @@ class botClient(Client): self.platform.meta(), abm.session_id, self.platform.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 +110,7 @@ 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 +119,12 @@ 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 +201,13 @@ 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 +253,7 @@ 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,14 +313,14 @@ 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 for attachment in attachments: content_type = (getattr(attachment, "content_type", "") or "").lower() url = QQOfficialPlatformAdapter._normalize_attachment_url( - getattr(attachment, "url", None) + getattr(attachment, "url", None), ) if not url: continue @@ -340,8 +340,8 @@ class QQOfficialPlatformAdapter(Platform): try: msg.append( await QQOfficialPlatformAdapter._prepare_audio_attachment( - url, filename - ) + url, filename, + ), ) except Exception as e: logger.warning( @@ -372,6 +372,7 @@ class QQOfficialPlatformAdapter(Platform): Returns: Content with face tags replaced by readable emoji descriptions. + """ import base64 import json @@ -409,7 +410,7 @@ 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, "") @@ -417,32 +418,32 @@ class QQOfficialPlatformAdapter(Platform): else: abm.sender = MessageMember(message.author.user_openid, "") abm.message_str = QQOfficialPlatformAdapter._parse_face_message( - message.content.strip() + message.content.strip(), ) abm.self_id = "unknown_selfid" msg.append(At(qq="qq_official")) msg.append(Plain(abm.message_str)) await QQOfficialPlatformAdapter._append_attachments( - msg, message.attachments + msg, message.attachments, ) abm.message = msg 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) else: abm.self_id = "" plain_content = QQOfficialPlatformAdapter._parse_face_message( - message.content.replace("<@!" + str(abm.self_id) + ">", "").strip() + 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 e75c4b7e3..493a71ce8 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,10 @@ 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 +40,7 @@ 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 +48,10 @@ 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 +59,7 @@ 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,22 +69,22 @@ 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, + ), ) @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 +93,10 @@ 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 +128,7 @@ 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 eb92b6c79..d18481ba0 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,7 @@ for handler in logging.root.handlers[:]: class QQOfficialWebhook: def __init__( - self, config: dict, event_queue: asyncio.Queue, botpy_client: Client + self, config: dict, event_queue: asyncio.Queue, botpy_client: Client, ) -> None: self.appid = config["appid"] self.secret = config["secret"] @@ -28,7 +28,7 @@ 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 +64,7 @@ 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 = { @@ -85,6 +85,7 @@ class QQOfficialWebhook: Returns: 响应数据 + """ msg: dict = await request.json logger.debug(f"收到 qq_official_webhook 回调: {msg}") @@ -120,7 +121,7 @@ class QQOfficialWebhook: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。" + f"将在 {self.callback_server_host}:{self.port} 端口启动 QQ 官方机器人 webhook 适配器。", ) await self.server.run_task( host=self.callback_server_host, diff --git a/astrbot/core/platform/sources/satori/satori_adapter.py b/astrbot/core/platform/sources/satori/satori_adapter.py index ce9b7a882..a38fcb632 100644 --- a/astrbot/core/platform/sources/satori/satori_adapter.py +++ b/astrbot/core/platform/sources/satori/satori_adapter.py @@ -30,7 +30,7 @@ from astrbot.core.platform.astr_message_event import MessageSession @register_platform_adapter( - "satori", "Satori 协议适配器", support_streaming_message=False + "satori", "Satori 协议适配器", support_streaming_message=False, ) class SatoriPlatformAdapter(Platform): def __init__( @@ -194,7 +194,7 @@ class SatoriPlatformAdapter(Platform): "body": dict[str, Any]( { "token": str(self.token) if self.token else "", # 字符串 - } + }, ), } diff --git a/astrbot/core/platform/sources/satori/satori_event.py b/astrbot/core/platform/sources/satori/satori_event.py index 6773cce51..05ea55c62 100644 --- a/astrbot/core/platform/sources/satori/satori_event.py +++ b/astrbot/core/platform/sources/satori/satori_event.py @@ -53,13 +53,13 @@ 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 = [] for component in message.chain: component_content = await cls._convert_component_to_satori_static( - component + component, ) if component_content: content_parts.append(component_content) @@ -82,7 +82,7 @@ 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 +118,7 @@ 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 消息发送失败") @@ -153,9 +153,9 @@ class SatoriPlatformEvent(AstrMessageEvent): img_chain = MessageChain( [ Plain( - text=f'' - ) - ] + text=f'', + ), + ], ) await self.send(img_chain) except Exception as e: @@ -224,7 +224,7 @@ class SatoriPlatformEvent(AstrMessageEvent): if node.content: for content_component in node.content: component_content = await self._convert_component_to_satori( - content_component + content_component, ) if component_content: content_parts.append(component_content) @@ -298,7 +298,7 @@ class SatoriPlatformEvent(AstrMessageEvent): if node.content: for content_component in node.content: component_content = await cls._convert_component_to_satori_static( - content_component + content_component, ) if component_content: content_parts.append(component_content) diff --git a/astrbot/core/platform/sources/slack/client.py b/astrbot/core/platform/sources/slack/client.py index a843681b6..131bacf87 100644 --- a/astrbot/core/platform/sources/slack/client.py +++ b/astrbot/core/platform/sources/slack/client.py @@ -60,6 +60,7 @@ class SlackWebhookClient: Returns: Response 对象或字典 + """ try: body = await req.get_data() @@ -93,7 +94,7 @@ class SlackWebhookClient: async def start(self) -> None: """启动 Webhook 服务器""" logger.info( - f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}..." + f"Slack Webhook 服务器启动中,监听 {self.host}:{self.port}{self.path}...", ) await self.app.run_task( host=self.host, @@ -126,7 +127,7 @@ class SlackSocketClient: self.socket_client = None async def _handle_events( - self, _: AsyncBaseSocketModeClient, req: SocketModeRequest + self, _: AsyncBaseSocketModeClient, req: SocketModeRequest, ) -> None: """处理 Socket Mode 事件""" try: @@ -142,7 +143,7 @@ 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 8ff272c1b..624890b3c 100644 --- a/astrbot/core/platform/sources/slack/slack_adapter.py +++ b/astrbot/core/platform/sources/slack/slack_adapter.py @@ -34,7 +34,7 @@ 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 +46,7 @@ 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 +66,10 @@ 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,7 +79,7 @@ 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 if blocks else None, ) else: await self.web_client.chat_postMessage( @@ -138,7 +138,7 @@ 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: @@ -156,7 +156,7 @@ class SlackAdapter(Platform): abm.message.append(Image.fromBase64(base64=file_url)) else: abm.message.append( - File(name=file_name, file=file_url, url=file_url) + File(name=file_name, file=file_url, url=file_url), ) abm.raw_message = event return abm @@ -182,7 +182,7 @@ class SlackAdapter(Platform): text_content = "".join(text_parts) if text_content.strip(): message_components.append( - Plain(text=text_content) + Plain(text=text_content), ) text_parts = [] message_components.append(At(qq=user_id, name="")) @@ -251,7 +251,7 @@ class SlackAdapter(Platform): base64_content = base64.b64encode(content).decode("utf-8") return base64_content logger.error( - f"Failed to download slack file: {resp.status} {await resp.text()}" + f"Failed to download slack file: {resp.status} {await resp.text()}", ) raise Exception(f"下载文件失败: {resp.status}") @@ -262,7 +262,7 @@ 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() @@ -283,12 +283,12 @@ class SlackAdapter(Platform): await self.webhook_client.shutdown_event.wait() else: logger.info( - f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}..." + f"Slack 适配器 (Webhook Mode) 启动中,监听 {self.webhook_host}:{self.webhook_port}{self.webhook_path}...", ) await self.webhook_client.start() else: raise ValueError( - f"不支持的连接模式: {self.connection_mode},请使用 'socket' 或 'webhook'" + f"不支持的连接模式: {self.connection_mode},请使用 'socket' 或 'webhook'", ) async def _handle_webhook_event(self, event_data: dict) -> None: @@ -340,5 +340,5 @@ class SlackAdapter(Platform): return bool( self.config.get("unified_webhook_mode", False) and self.config.get("slack_connection_mode", "") == "webhook" - and self.config.get("webhook_uuid") + and self.config.get("webhook_uuid"), ) diff --git a/astrbot/core/platform/sources/slack/slack_event.py b/astrbot/core/platform/sources/slack/slack_event.py index 0e116ad44..6343c7b16 100644 --- a/astrbot/core/platform/sources/slack/slack_event.py +++ b/astrbot/core/platform/sources/slack/slack_event.py @@ -24,7 +24,7 @@ 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 +51,7 @@ 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 +71,7 @@ class SlackMessageEvent(AstrMessageEvent): @staticmethod async def _parse_slack_blocks( - message_chain: MessageChain, web_client: AsyncWebClient + message_chain: MessageChain, web_client: AsyncWebClient, ): """解析成 Slack 块格式""" blocks = [] @@ -85,32 +85,32 @@ class SlackMessageEvent(AstrMessageEvent): { "type": "section", "text": {"type": "mrkdwn", "text": text_content}, - } + }, ) text_content = "" block = await SlackMessageEvent._from_segment_to_slack_block( - segment, web_client + segment, web_client, ) if block: blocks.append(block) if text_content.strip(): blocks.append( - {"type": "section", "text": {"type": "mrkdwn", "text": text_content}} + {"type": "section", "text": {"type": "mrkdwn", "text": text_content}}, ) return (blocks, "" if blocks else text_content) 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 +124,16 @@ 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 @@ -173,7 +173,7 @@ class SlackMessageEvent(AstrMessageEvent): try: channel_info = await self.web_client.conversations_info(channel=channel_id) members_response = await self.web_client.conversations_members( - channel=channel_id + channel=channel_id, ) members = [] for member_id in members_response["members"]: @@ -185,7 +185,7 @@ class SlackMessageEvent(AstrMessageEvent): user_id=member_id, nickname=user_data.get("real_name") or user_data.get("name", member_id), - ) + ), ) except Exception: members.append(MessageMember(user_id=member_id, nickname=member_id)) diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index e747d3c2f..4344443d1 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -126,14 +126,14 @@ 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 @@ -174,7 +174,7 @@ class TelegramPlatformEvent(AstrMessageEvent): if "Voice_messages_forbidden" not in e.message: raise logger.warning( - "User privacy settings prevent receiving voice messages, falling back to sending an audio file. To enable voice messages, go to Telegram Settings → Privacy and Security → Voice Messages → set to 'Everyone'." + "User privacy settings prevent receiving voice messages, falling back to sending an audio file. To enable voice messages, go to Telegram Settings → Privacy and Security → Voice Messages → set to 'Everyone'.", ) if use_media_action: media_payload = dict(payload) @@ -193,11 +193,11 @@ 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 +212,7 @@ 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,11 +245,11 @@ 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( - f"MarkdownV2 send failed: {e}. Using plain text instead." + f"MarkdownV2 send failed: {e}. Using plain text instead.", ) await client.send_message(text=chunk, **payload) elif isinstance(i, Image): @@ -277,7 +277,7 @@ 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: @@ -332,6 +332,7 @@ class TelegramPlatformEvent(AstrMessageEvent): text: 消息文本,1-4096 字符 message_thread_id: 可选,目标消息线程 ID parse_mode: 可选,消息文本的解析模式 + """ if not text or not text.strip(): return @@ -343,10 +344,10 @@ class TelegramPlatformEvent(AstrMessageEvent): kwargs["parse_mode"] = parse_mode try: logger.debug( - f"[Telegram] sendMessageDraft: chat_id={chat_id}, draft_id={draft_id}, text_len={len(text)}" + f"[Telegram] sendMessageDraft: chat_id={chat_id}, draft_id={draft_id}, text_len={len(text)}", ) await self.client.send_message_draft( - chat_id=int(chat_id), 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}") @@ -422,7 +423,7 @@ 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}") @@ -443,15 +444,15 @@ 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) + Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name), ) self._has_send_oper = True @@ -497,12 +498,12 @@ 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: logger.debug( - f"[Telegram] sendMessageDraft failed (ignored): {e2!s}" + f"[Telegram] sendMessageDraft failed (ignored): {e2!s}", ) sender_task = asyncio.create_task(_draft_sender_loop()) @@ -519,7 +520,7 @@ 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 = "" @@ -527,7 +528,7 @@ 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 @@ -576,7 +577,7 @@ 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() @@ -621,7 +622,7 @@ 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 cab03081d..fd42a0c11 100644 --- a/astrbot/core/platform/sources/tui/tui_adapter.py +++ b/astrbot/core/platform/sources/tui/tui_adapter.py @@ -38,7 +38,7 @@ 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 +56,7 @@ 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 +65,13 @@ 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 +84,7 @@ 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 +101,12 @@ 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 +124,12 @@ 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 +194,7 @@ 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/tui/tui_event.py b/astrbot/core/platform/sources/tui/tui_event.py index 6ea7ec7eb..4e2d19952 100644 --- a/astrbot/core/platform/sources/tui/tui_event.py +++ b/astrbot/core/platform/sources/tui/tui_event.py @@ -53,7 +53,7 @@ class TUIMessageEvent(AstrMessageEvent): "message_id": message_id, }, ) - return + return None data = "" for comp in message.chain: diff --git a/astrbot/core/platform/sources/tui/tui_queue_mgr.py b/astrbot/core/platform/sources/tui/tui_queue_mgr.py index ac770f820..5f14e9c46 100644 --- a/astrbot/core/platform/sources/tui/tui_queue_mgr.py +++ b/astrbot/core/platform/sources/tui/tui_queue_mgr.py @@ -34,7 +34,7 @@ class TUIQueueMgr: """Get or create a back queue for the given request ID""" if request_id not in self.back_queues: self.back_queues[request_id] = asyncio.Queue( - maxsize=self.back_queue_maxsize + maxsize=self.back_queue_maxsize, ) if conversation_id: self._request_conversation[request_id] = conversation_id @@ -57,7 +57,7 @@ class TUIQueueMgr: def remove_queues(self, conversation_id: str) -> None: """Remove queues for the given conversation ID""" for request_id in list( - self._conversation_back_requests.get(conversation_id, set()) + self._conversation_back_requests.get(conversation_id, set()), ): self.remove_back_queue(request_id) self._conversation_back_requests.pop(conversation_id, None) @@ -121,7 +121,7 @@ class TUIQueueMgr: ) self._listener_tasks[conversation_id] = task task.add_done_callback( - lambda _: self._listener_tasks.pop(conversation_id, None) + lambda _: self._listener_tasks.pop(conversation_id, None), ) logger.debug(f"Started listener for TUI conversation: {conversation_id}") @@ -150,7 +150,7 @@ class TUIQueueMgr: await self._listener_callback(data) except Exception as e: logger.error( - f"Error processing message from TUI conversation {conversation_id}: {e}" + f"Error processing message from TUI conversation {conversation_id}: {e}", ) except asyncio.CancelledError: break diff --git a/astrbot/core/platform/sources/webchat/message_parts_helper.py b/astrbot/core/platform/sources/webchat/message_parts_helper.py index 6e99bf3d3..5dc255ca0 100644 --- a/astrbot/core/platform/sources/webchat/message_parts_helper.py +++ b/astrbot/core/platform/sources/webchat/message_parts_helper.py @@ -59,6 +59,7 @@ async def parse_webchat_message_parts( Returns: tuple[list, list[str], bool]: (components, plain_text_parts, has_non_reply_content) + """ components: list[BaseMessageComponent] = [] text_parts: list[str] = [] @@ -126,7 +127,7 @@ async def parse_webchat_message_parts( chain=reply_chain, sender_id=sender_id, sender_nickname=sender_name, - ) + ), ) continue @@ -205,7 +206,7 @@ async def build_webchat_message_parts( "type": "reply", "message_id": message_id, "selected_text": str(part.get("selected_text", "")), - } + }, ) continue @@ -233,7 +234,7 @@ async def build_webchat_message_parts( "attachment_id": attachment.attachment_id, "filename": attachment_path.name, "path": str(attachment_path), - } + }, ) return message_parts @@ -272,7 +273,7 @@ def webchat_message_parts_to_message_chain( id=str(message_id), message_str=str(part.get("selected_text", "")), chain=[], - ) + ), ) continue @@ -385,7 +386,7 @@ async def message_chain_to_storage_message_parts( if isinstance(comp, Json): parts.append( - {"type": "plain", "text": json.dumps(comp.data, ensure_ascii=False)} + {"type": "plain", "text": json.dumps(comp.data, ensure_ascii=False)}, ) continue diff --git a/astrbot/core/platform/sources/webchat/webchat_adapter.py b/astrbot/core/platform/sources/webchat/webchat_adapter.py index 28f1b52b1..3e704d03c 100644 --- a/astrbot/core/platform/sources/webchat/webchat_adapter.py +++ b/astrbot/core/platform/sources/webchat/webchat_adapter.py @@ -60,7 +60,7 @@ 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,11 +78,11 @@ 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( - conversation_id + conversation_id, ) stream_request_ids = [ req_id for req_id in active_request_ids if not req_id.startswith("ws_sub_") @@ -117,7 +117,7 @@ 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 +135,12 @@ 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]]: """解析消息段列表,返回消息组件列表和纯文本列表 @@ -151,6 +151,7 @@ class WebChatAdapter(Platform): Returns: tuple[list, list[str]]: (消息组件列表, 纯文本列表) + """ async def get_reply_parts( @@ -214,7 +215,7 @@ class WebChatAdapter(Platform): message_event.set_extra("selected_provider", payload.get("selected_provider")) message_event.set_extra("selected_model", payload.get("selected_model")) message_event.set_extra( - "enable_streaming", payload.get("enable_streaming", True) + "enable_streaming", payload.get("enable_streaming", True), ) message_event.set_extra("action_type", payload.get("action_type")) self.commit_event(message_event) diff --git a/astrbot/core/platform/sources/webchat/webchat_event.py b/astrbot/core/platform/sources/webchat/webchat_event.py index da4759ecb..516c156be 100644 --- a/astrbot/core/platform/sources/webchat/webchat_event.py +++ b/astrbot/core/platform/sources/webchat/webchat_event.py @@ -53,7 +53,7 @@ class WebChatMessageEvent(AstrMessageEvent): "message_id": message_id, }, # end means this request is finished ) - return + return None data = "" for comp in message.chain: diff --git a/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py index f3ade1589..1f7a178fc 100644 --- a/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py +++ b/astrbot/core/platform/sources/webchat/webchat_queue_mgr.py @@ -34,7 +34,7 @@ class WebChatQueueMgr: """Get or create a back queue for the given request ID""" if request_id not in self.back_queues: self.back_queues[request_id] = asyncio.Queue( - maxsize=self.back_queue_maxsize + maxsize=self.back_queue_maxsize, ) if conversation_id: self._request_conversation[request_id] = conversation_id @@ -57,7 +57,7 @@ class WebChatQueueMgr: def remove_queues(self, conversation_id: str) -> None: """Remove queues for the given conversation ID""" for request_id in list( - self._conversation_back_requests.get(conversation_id, set()) + self._conversation_back_requests.get(conversation_id, set()), ): self.remove_back_queue(request_id) self._conversation_back_requests.pop(conversation_id, None) @@ -121,7 +121,7 @@ class WebChatQueueMgr: ) self._listener_tasks[conversation_id] = task task.add_done_callback( - lambda _: self._listener_tasks.pop(conversation_id, None) + lambda _: self._listener_tasks.pop(conversation_id, None), ) logger.debug(f"Started listener for conversation: {conversation_id}") @@ -150,7 +150,7 @@ class WebChatQueueMgr: await self._listener_callback(data) except Exception as e: logger.error( - f"Error processing message from conversation {conversation_id}: {e}" + f"Error processing message from conversation {conversation_id}: {e}", ) except asyncio.CancelledError: break diff --git a/astrbot/core/platform/sources/wecom/wecom_adapter.py b/astrbot/core/platform/sources/wecom/wecom_adapter.py index eb60de730..934770281 100644 --- a/astrbot/core/platform/sources/wecom/wecom_adapter.py +++ b/astrbot/core/platform/sources/wecom/wecom_adapter.py @@ -45,10 +45,10 @@ 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( @@ -71,6 +71,7 @@ class WecomServer: Returns: 验证响应 + """ logger.info(f"验证请求有效性: {request.args}") args = request.args @@ -99,6 +100,7 @@ class WecomServer: Returns: 响应内容 + """ data = await request.get_data() msg_signature = request.args.get("msg_signature") @@ -118,7 +120,7 @@ class WecomServer: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。" + f"将在 {self.callback_server_host}:{self.port} 端口启动 企业微信 适配器。", ) await self.server.run_task( host=self.callback_server_host, @@ -133,12 +135,12 @@ 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: @@ -151,7 +153,7 @@ 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: @@ -178,7 +180,7 @@ 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) @@ -189,7 +191,7 @@ 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 主动发送。") @@ -197,7 +199,7 @@ class WecomPlatformAdapter(Platform): return if not self.agent_id: logger.warning( - f"send_by_session 失败:无法为会话 {session.session_id} 推断 agent_id。" + f"send_by_session 失败:无法为会话 {session.session_id} 推断 agent_id。", ) await super().send_by_session(session, message_chain) return @@ -237,7 +239,7 @@ 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}") @@ -258,7 +260,7 @@ class WecomPlatformAdapter(Platform): ) ).get("url", "") logger.info( - f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}" + f"请打开以下链接,在微信扫码以获取客服微信: https://api.cl2wm.cn/api/qrcode/code?text={kf_url}", ) except Exception as e: logger.error(e) @@ -273,8 +275,7 @@ class WecomPlatformAdapter(Platform): """统一 Webhook 回调入口""" if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def convert_message(self, msg: BaseMessage) -> AstrBotMessage | None: abm = AstrBotMessage() @@ -300,7 +301,7 @@ 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") @@ -349,7 +350,7 @@ 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") @@ -359,7 +360,7 @@ 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 4ffcb13c5..b926d0404 100644 --- a/astrbot/core/platform/sources/wecom/wecom_event.py +++ b/astrbot/core/platform/sources/wecom/wecom_event.py @@ -103,10 +103,10 @@ class WecomPlatformEvent(AstrMessageEvent): if getattr(e, "errcode", None) == 40096: # 40096: invalid external userid, fallback to regular message API logger.warning( - f"kf API error 40096 for user {user_id}, falling back to regular message API" + f"kf API error 40096 for user {user_id}, falling back to regular message API", ) self.client.message.send_text( - self.get_self_id(), user_id, chunk + self.get_self_id(), user_id, chunk, ) else: raise @@ -141,7 +141,7 @@ class WecomPlatformEvent(AstrMessageEvent): logger.error(f"微信客服上传语音失败: {e}") await self.send( MessageChain().message( - f"微信客服上传语音失败: {e}" + f"微信客服上传语音失败: {e}", ), ) return @@ -241,7 +241,7 @@ class WecomPlatformEvent(AstrMessageEvent): logger.error(f"企业微信上传语音失败: {e}") await self.send( MessageChain().message( - f"企业微信上传语音失败: {e}" + f"企业微信上传语音失败: {e}", ), ) return diff --git a/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py b/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py index b48fecb48..3ff6e7dff 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/WXBizJsonMsgCrypt.py @@ -210,7 +210,7 @@ class Prpcrypt: @return: 16位字符串 """ return str( - secrets.randbelow(self.RANDOM_RANGE) + self.MIN_RANDOM_VALUE + secrets.randbelow(self.RANDOM_RANGE) + self.MIN_RANDOM_VALUE, ).encode() diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_adapter.py index 137c058bd..541f593ca 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,7 @@ 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,46 +61,46 @@ 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() self.only_use_webhook_url_to_send = bool( - self.config.get("only_use_webhook_url_to_send", False) + 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) + self.config.get("wecomaibot_heartbeat_interval", 30), ) self.metadata = PlatformMetadata( name="wecom_ai_bot", @@ -114,7 +114,7 @@ class WecomAIBotAdapter(Platform): if self.connection_mode == "long_connection": if not self.long_connection_bot_id or not self.long_connection_secret: logger.warning( - "企业微信智能机器人长连接模式缺少 BotID 或 Secret,连接可能失败" + "企业微信智能机器人长连接模式缺少 BotID 或 Secret,连接可能失败", ) self.long_connection_client = WecomAIBotLongConnectionClient( bot_id=self.long_connection_bot_id, @@ -134,7 +134,7 @@ 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 +153,7 @@ 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 +177,17 @@ 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) @@ -198,21 +198,21 @@ class WecomAIBotAdapter(Platform): self._stream_plain_cache.pop(stream_id, None) if self.queue_mgr.is_stream_finished(stream_id): logger.debug( - f"Stream already finished, returning end message: {stream_id}" + f"Stream already finished, returning end message: {stream_id}", ) else: logger.warning(f"Cannot find back queue for stream_id: {stream_id}") 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) if queue.empty(): logger.debug( - f"No new messages in back queue for stream_id: {stream_id}" + f"No new messages in back queue for stream_id: {stream_id}", ) return None cached_plain_content = self._stream_plain_cache.get(stream_id, "") @@ -238,16 +238,16 @@ class WecomAIBotAdapter(Platform): self._stream_plain_cache.pop(stream_id, None) break logger.debug( - f"Aggregated content: {latest_plain_content}, image: {len(image_base64)}, finish: {finish}" + f"Aggregated content: {latest_plain_content}, image: {len(image_base64)}, finish: {finish}", ) if not finish: 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 = [] @@ -259,11 +259,11 @@ class WecomAIBotAdapter(Platform): { "msgtype": WecomAIBotConstants.MSG_TYPE_IMAGE, "image": {"base64": img_b64, "md5": img_md5}, - } + }, ) 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, @@ -272,7 +272,7 @@ class WecomAIBotAdapter(Platform): ) if encrypted_message: logger.debug( - f"Stream message sent successfully, stream_id: {stream_id}" + f"Stream message sent successfully, stream_id: {stream_id}", ) else: logger.error("消息加密失败") @@ -283,10 +283,10 @@ class WecomAIBotAdapter(Platform): if event == "enter_chat" and self.friend_message_welcome_text: try: resp = WecomAIBotStreamMessageBuilder.make_text( - self.friend_message_welcome_text + 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 +306,7 @@ class WecomAIBotAdapter(Platform): session_id = self._extract_session_id(body) stream_id = f"{session_id}_{generate_random_string(10)}" await self._enqueue_message( - body, {"req_id": req_id or ""}, stream_id, session_id + body, {"req_id": req_id or ""}, stream_id, session_id, ) self.queue_mgr.set_pending_response( stream_id, @@ -336,7 +336,7 @@ class WecomAIBotAdapter(Platform): await self._send_long_connection_respond_welcome(req_id) elif event_type == "disconnected_event": logger.warning( - "[WecomAI][LongConn] 收到 disconnected_event,旧连接将被关闭" + "[WecomAI][LongConn] 收到 disconnected_event,旧连接将被关闭", ) async def _send_long_connection_respond_welcome(self, req_id: str) -> bool: @@ -353,13 +353,13 @@ 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: @@ -370,9 +370,8 @@ class WecomAIBotAdapter(Platform): if chattype == "group": chat_id = message_data.get("chatid", "default_group") return format_session_id("wecomai", chat_id) - else: - user_id = message_data.get("from", {}).get("userid", "default_user") - return format_session_id("wecomai", user_id) + user_id = message_data.get("from", {}).get("userid", "default_user") + return format_session_id("wecomai", user_id) async def _enqueue_message( self, @@ -422,7 +421,7 @@ class WecomAIBotAdapter(Platform): image_url = image_payload.get("url", "") if image_url: _img_url_to_process.append( - (image_url, image_payload.get("aeskey")) + (image_url, image_payload.get("aeskey")), ) content = " ".join(text_parts) if text_parts else "" else: @@ -466,7 +465,7 @@ 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: @@ -490,26 +489,26 @@ 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() @@ -520,8 +519,7 @@ class WecomAIBotAdapter(Platform): return ("long_connection mode does not accept webhook callbacks", 400) if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def terminate(self) -> None: """终止适配器""" diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py index 4384dce11..4bf2408c8 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_event.py @@ -58,7 +58,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": "", "streaming": False, "session_id": stream_id, - } + }, ) @staticmethod @@ -83,7 +83,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": data, "streaming": streaming, "session_id": stream_id, - } + }, ) elif isinstance(comp, Plain): data = comp.text @@ -93,7 +93,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": data, "streaming": streaming, "session_id": stream_id, - } + }, ) elif isinstance(comp, Image): try: @@ -105,7 +105,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "image_data": image_base64, "streaming": streaming, "session_id": stream_id, - } + }, ) else: logger.warning("图片数据为空,跳过") @@ -139,7 +139,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): stream_id = raw.get("stream_id", self.session_id) pending_response = self.queue_mgr.get_pending_response(stream_id) or {} connection_mode = pending_response.get("callback_params", {}).get( - "connection_mode" + "connection_mode", ) req_id = pending_response.get("callback_params", {}).get("req_id") if ( @@ -154,7 +154,7 @@ 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( @@ -192,7 +192,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): stream_id = raw.get("stream_id", self.session_id) pending_response = self.queue_mgr.get_pending_response(stream_id) or {} connection_mode = pending_response.get("callback_params", {}).get( - "connection_mode" + "connection_mode", ) req_id = pending_response.get("callback_params", {}).get("req_id") back_queue = self.queue_mgr.get_or_create_back_queue(stream_id) @@ -222,7 +222,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): async for chain in generator: if self.webhook_client: await self.webhook_client.send_message_chain( - chain, unsupported_only=True + chain, unsupported_only=True, ) chain.squash_plain() chunk_text = self._extract_plain_text_from_chain(chain) @@ -276,13 +276,13 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": text, "streaming": True, "session_id": stream_id, - } + }, ) 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: @@ -293,7 +293,7 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": final_data, "streaming": True, "session_id": stream_id, - } + }, ) final_data = "" increment_plain = "" @@ -323,6 +323,6 @@ class WecomAIBotMessageEvent(AstrMessageEvent): "data": final_data, "streaming": True, "session_id": stream_id, - } + }, ) await super().send_streaming(generator, use_fallback) 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 d4712c0e0..fc2ca4155 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_long_connection.py @@ -61,7 +61,7 @@ class WecomAIBotLongConnectionClient: self._session = session logger.info("[WecomAI][LongConn] 正在连接: %s", self.ws_url) async with session.ws_connect( - self.ws_url, heartbeat=None, autoping=True + self.ws_url, heartbeat=None, autoping=True, ) as ws: self._ws = ws await self._subscribe() @@ -107,7 +107,7 @@ class WecomAIBotLongConnectionClient: data = json.loads(reply.data) if data.get("errcode") != 0: raise RuntimeError( - f"订阅失败 errcode={data.get('errcode')} errmsg={data.get('errmsg')}" + f"订阅失败 errcode={data.get('errcode')} errmsg={data.get('errmsg')}", ) async def _heartbeat_loop(self) -> None: diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py index 10fea4dfb..44a3cc63d 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_queue_mgr.py @@ -60,7 +60,7 @@ class WecomAIQueueMgr: """ if session_id not in self.back_queues: self.back_queues[session_id] = asyncio.Queue( - maxsize=self.back_queue_maxsize + maxsize=self.back_queue_maxsize, ) logger.debug(f"[WecomAI] 创建输出队列: {session_id}") return self.back_queues[session_id] @@ -125,7 +125,7 @@ class WecomAIQueueMgr: return session_id in self.back_queues def set_pending_response( - self, session_id: str, callback_params: dict[str, str] + self, session_id: str, callback_params: dict[str, str], ) -> None: """设置待处理的响应参数 diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py index efd82aa85..e2fcc6ce1 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py @@ -70,6 +70,7 @@ class WecomAIBotServer: Returns: 验证响应元组 (content, status_code, headers) + """ args = request.args msg_signature = args.get("msg_signature") @@ -103,6 +104,7 @@ class WecomAIBotServer: Returns: 响应元组 (content, status_code, headers) + """ args = request.args msg_signature = args.get("msg_signature") diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py index d43bfceba..b72f7d1d6 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_webhook.py @@ -70,12 +70,12 @@ class WecomAIBotWebhookClient: text = await response.text() if response.status != 200: raise WecomAIBotWebhookError( - f"Webhook 请求失败: HTTP {response.status}, {text}" + f"Webhook 请求失败: HTTP {response.status}, {text}", ) result = await response.json(content_type=None) if result.get("errcode") != 0: raise WecomAIBotWebhookError( - f"Webhook 返回错误: {result.get('errcode')} {result.get('errmsg')}" + f"Webhook 返回错误: {result.get('errcode')} {result.get('errmsg')}", ) logger.debug("企业微信消息推送成功: %s", payload.get("msgtype", "unknown")) @@ -85,7 +85,7 @@ class WecomAIBotWebhookClient: { "msgtype": "markdown_v2", "markdown_v2": {"content": chunk}, - } + }, ) async def send_image_base64(self, image_base64: str) -> None: @@ -98,11 +98,11 @@ class WecomAIBotWebhookClient: "base64": image_base64, "md5": md5, }, - } + }, ) async def upload_media( - self, file_path: Path, media_type: Literal["file", "voice"] + self, file_path: Path, media_type: Literal["file", "voice"], ) -> str: file_path_anyio = anyio.Path(file_path) if not await file_path_anyio.exists() or not await file_path_anyio.is_file(): @@ -128,12 +128,12 @@ class WecomAIBotWebhookClient: text = await response.text() if response.status != 200: raise WecomAIBotWebhookError( - f"上传媒体失败: HTTP {response.status}, {text}" + f"上传媒体失败: HTTP {response.status}, {text}", ) result = await response.json(content_type=None) if result.get("errcode") != 0: raise WecomAIBotWebhookError( - f"上传媒体失败: {result.get('errcode')} {result.get('errmsg')}" + f"上传媒体失败: {result.get('errcode')} {result.get('errmsg')}", ) media_id = result.get("media_id", "") if not media_id: @@ -146,7 +146,7 @@ class WecomAIBotWebhookClient: { "msgtype": "file", "file": {"media_id": media_id}, - } + }, ) async def send_voice(self, file_path: Path) -> None: @@ -155,7 +155,7 @@ class WecomAIBotWebhookClient: { "msgtype": "voice", "voice": {"media_id": media_id}, - } + }, ) @staticmethod @@ -216,7 +216,7 @@ class WecomAIBotWebhookClient: target_voice_path.unlink() except Exception as e: logger.warning( - "清理临时语音文件失败 %s: %s", target_voice_path, e + "清理临时语音文件失败 %s: %s", target_voice_path, e, ) else: logger.warning( diff --git a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py index d026e2cf6..a90fd2b99 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_adapter.py @@ -8,7 +8,7 @@ import time import uuid from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import Any from urllib.parse import quote import qrcode as qrcode_lib @@ -31,9 +31,6 @@ from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from .weixin_oc_client import WeixinOCClient from .weixin_oc_event import WeixinOCMessageEvent -if TYPE_CHECKING: - pass - @dataclass class OpenClawLoginSession: @@ -71,27 +68,27 @@ 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 self.base_url = str( - platform_config.get("weixin_oc_base_url", "https://ilinkai.weixin.qq.com") + platform_config.get("weixin_oc_base_url", "https://ilinkai.weixin.qq.com"), ).rstrip("/") self.bot_type = str(platform_config.get("weixin_oc_bot_type", "3")) self.qr_poll_interval = max( - 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) + platform_config.get("weixin_oc_long_poll_timeout_ms", 35000), ) self.api_timeout_ms = int( - platform_config.get("weixin_oc_api_timeout_ms", 15000) + platform_config.get("weixin_oc_api_timeout_ms", 15000), ) 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( name="weixin_oc", @@ -107,10 +104,10 @@ 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 = ( @@ -126,7 +123,7 @@ 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: @@ -166,7 +163,7 @@ 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) @@ -196,12 +193,12 @@ 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: raise RuntimeError( - f"sendtyping failed for {user_id}: {payload.get('errmsg', '')}" + f"sendtyping failed for {user_id}: {payload.get('errmsg', '')}", ) async def _run_typing_keepalive(self, user_id: str) -> None: @@ -239,7 +236,7 @@ class WeixinOCAdapter(Platform): if not state.owners or state.keepalive_task is not None: return state.keepalive_task = asyncio.create_task( - self._run_typing_keepalive(user_id) + self._run_typing_keepalive(user_id), ) async def _typing_keepalive_loop(self, user_id: str) -> None: @@ -378,7 +375,7 @@ class WeixinOCAdapter(Platform): if ticket: if state.cancel_task is None or state.cancel_task.done(): state.cancel_task = asyncio.create_task( - self._delayed_cancel_typing(user_id, ticket) + self._delayed_cancel_typing(user_id, ticket), ) async def _cleanup_typing_tasks(self) -> None: @@ -454,7 +451,7 @@ class WeixinOCAdapter(Platform): astrbot_config.save_config() def _is_login_session_valid( - self, login_session: OpenClawLoginSession | None + self, login_session: OpenClawLoginSession | None, ) -> bool: if not login_session: return False @@ -471,10 +468,10 @@ 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 @@ -534,7 +531,7 @@ 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", @@ -573,7 +570,7 @@ 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: @@ -585,13 +582,13 @@ class WeixinOCAdapter(Platform): image_aes_key = str(image_item.get("aeskey", "")).strip() if image_aes_key: aes_key_value = base64.b64encode(bytes.fromhex(image_aes_key)).decode( - "utf-8" + "utf-8", ) else: aes_key_value = str(media.get("aes_key", "")).strip() 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) @@ -610,7 +607,7 @@ 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, @@ -627,10 +624,10 @@ 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, @@ -647,7 +644,7 @@ 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, @@ -659,7 +656,7 @@ class WeixinOCAdapter(Platform): return None async def _resolve_media_file_path( - self, segment: Image | Video | File + self, segment: Image | Video | File, ) -> Path | None: try: if isinstance(segment, File): @@ -681,14 +678,14 @@ 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) @@ -720,11 +717,11 @@ 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) @@ -749,14 +746,14 @@ 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]) @@ -765,7 +762,7 @@ 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() @@ -852,7 +849,7 @@ class WeixinOCAdapter(Platform): await self._save_account_state() def _message_text_from_item_list( - self, item_list: list[dict[str, Any]] | None + self, item_list: list[dict[str, Any]] | None, ) -> str: if not item_list: return "" @@ -886,7 +883,7 @@ class WeixinOCAdapter(Platform): return "\n".join(text_parts).strip() async def _item_list_to_components( - self, item_list: list[dict[str, Any]] | None + self, item_list: list[dict[str, Any]] | None, ) -> list[Any]: if not item_list: return [] @@ -902,7 +899,7 @@ 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: @@ -945,7 +942,7 @@ class WeixinOCAdapter(Platform): platform_meta=self.meta(), session_id=abm.session_id, platform=self, - ) + ), ) async def _poll_inbound_updates(self) -> None: @@ -997,21 +994,21 @@ 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 = "" @@ -1023,7 +1020,7 @@ 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 @@ -1068,7 +1065,7 @@ 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( @@ -1094,7 +1091,7 @@ 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 92e3ddf7f..5d8312628 100644 --- a/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py +++ b/astrbot/core/platform/sources/weixin_oc/weixin_oc_client.py @@ -47,7 +47,7 @@ class WeixinOCClient: "Content-Type": "application/json", "AuthorizationType": "ilink_bot_token", "X-WECHAT-UIN": base64.b64encode( - str(random.getrandbits(32)).encode("utf-8") + str(random.getrandbits(32)).encode("utf-8"), ).decode("utf-8"), } if token_required and self.token: @@ -129,7 +129,7 @@ class WeixinOCClient: cdn_url = self._build_cdn_upload_url(upload_param, file_key) else: raise ValueError( - "CDN upload URL missing (need upload_full_url or upload_param)" + "CDN upload URL missing (need upload_full_url or upload_param)", ) raw_data = await asyncio.to_thread(media_path.read_bytes) logger.debug( @@ -170,16 +170,16 @@ class WeixinOCClient: ) if resp.status >= 400 and resp.status < 500: raise RuntimeError( - f"upload media to cdn failed: {resp.status} {detail}" + f"upload media to cdn failed: {resp.status} {detail}", ) if resp.status != 200: raise RuntimeError( - f"upload media to cdn failed: {resp.status} {detail}" + f"upload media to cdn failed: {resp.status} {detail}", ) download_param = resp.headers.get("x-encrypted-param") if not download_param: raise RuntimeError( - "upload media to cdn failed: missing x-encrypted-param" + "upload media to cdn failed: missing x-encrypted-param", ) return download_param @@ -188,17 +188,17 @@ 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() raise RuntimeError( - f"download media from cdn failed: {resp.status} {detail}" + f"download media from cdn failed: {resp.status} {detail}", ) 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 +238,7 @@ 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 +253,7 @@ 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 3198fa8d2..bdd2b1860 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,10 @@ 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 @@ -73,6 +73,7 @@ class WeixinOfficialAccountServer: Returns: 验证响应 + """ logger.info(f"验证请求有效性: {request.args}") args = request.args @@ -120,6 +121,7 @@ class WeixinOfficialAccountServer: Returns: 响应内容 + """ data = await request.get_data() msg_signature = request.args.get("msg_signature") @@ -162,10 +164,9 @@ class WeixinOfficialAccountServer: if len(cached) == 0: self.user_buffer.pop(from_user, None) return _reply_text(cached_xml) - else: - return _reply_text( - cached_xml + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + return _reply_text( + cached_xml + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) task: asyncio.Task | None = state.get("task") placeholder = f"【正在思考'{state.get('preview', '...')}'中,已思考{int(time.monotonic() - state.get('started_at', time.monotonic()))}s,回复任意文字尝试获取回复】" if task and state.get("msg_id") == msg_id: @@ -179,35 +180,34 @@ class WeixinOfficialAccountServer: cached = state.get("cached_xml") if cached and len(cached) > 0: logger.info( - f"wx buffer hit on retry window: user={from_user}" + f"wx buffer hit on retry window: user={from_user}", ) cached_xml = cached.pop(0) if len(cached) == 0: self.user_buffer.pop(from_user, None) logger.debug( - f"wx finished message sending in passive window: user={from_user} msg_id={msg_id} " + f"wx finished message sending in passive window: user={from_user} msg_id={msg_id} ", ) return _reply_text(cached_xml) - else: - logger.debug( - f"wx finished message sending in passive window but not final: user={from_user} msg_id={msg_id} " - ) - return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + logger.debug( + f"wx finished message sending in passive window but not final: user={from_user} msg_id={msg_id} ", + ) + return _reply_text( + cached_xml + + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) logger.info( - f"wx finished in window but not final; return placeholder: user={from_user} msg_id={msg_id} " + f"wx finished in window but not final; return placeholder: user={from_user} msg_id={msg_id} ", ) return _reply_text(placeholder) except Exception: logger.critical( - "wx task failed in passive window", exc_info=True + "wx task failed in passive window", exc_info=True, ) self.user_buffer.pop(from_user, None) return _reply_text("处理消息失败,请稍后再试。") logger.info( - f"wx passive window timeout: user={from_user} msg_id={msg_id}" + f"wx passive window timeout: user={from_user} msg_id={msg_id}", ) return _reply_text(placeholder) logger.debug(f"wx trigger while thinking: user={from_user}") @@ -216,7 +216,7 @@ class WeixinOfficialAccountServer: preview = self._preview(msg) placeholder = f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" logger.info( - f"wx start task: user={from_user} msg_id={msg_id} preview={preview}" + f"wx start task: user={from_user} msg_id={msg_id} preview={preview}", ) self.user_buffer[from_user] = state = { "msg_id": msg_id, @@ -226,7 +226,7 @@ class WeixinOfficialAccountServer: "started_at": time.monotonic(), } self.user_buffer[from_user]["task"] = task = asyncio.create_task( - self.callback(msg) + self.callback(msg), ) done, _ = await asyncio.wait( {task}, @@ -235,20 +235,19 @@ class WeixinOfficialAccountServer: ) if done: try: - cached = state.get("cached_xml", None) + cached = state.get("cached_xml") if cached and isinstance(cached, list) and (len(cached) > 0): logger.info(f"wx buffer hit immediately: user={from_user}") cached_xml = cached.pop(0) if len(cached) == 0: self.user_buffer.pop(from_user, None) return _reply_text(cached_xml) - else: - return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" - ) + return _reply_text( + cached_xml + + "\n【后续消息还在缓冲中,回复任意文字继续获取】", + ) logger.info( - f"wx not finished in first window; return placeholder: user={from_user} msg_id={msg_id} " + f"wx not finished in first window; return placeholder: user={from_user} msg_id={msg_id} ", ) return _reply_text(placeholder) except Exception: @@ -260,7 +259,7 @@ class WeixinOfficialAccountServer: async def start_polling(self) -> None: logger.info( - f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。" + f"将在 {self.callback_server_host}:{self.port} 端口启动 微信公众平台 适配器。", ) await self.server.run_task( host=self.callback_server_host, @@ -273,16 +272,16 @@ 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) @@ -295,10 +294,10 @@ 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] = {} @@ -332,7 +331,7 @@ 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) @@ -359,11 +358,10 @@ class WeixinOfficialAccountPlatformAdapter(Platform): """统一 Webhook 回调入口""" if request.method == "GET": return await self.server.handle_verify(request) - else: - return await self.server.handle_callback(request) + return await self.server.handle_callback(request) async def convert_message( - self, msg, future: asyncio.Future | None = None + self, msg, future: asyncio.Future | None = None, ) -> AstrBotMessage | None: abm = AstrBotMessage() if isinstance(msg, TextMessage): @@ -388,7 +386,7 @@ 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") @@ -413,7 +411,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): logger.warning(f"暂未实现的事件: {msg.type}") if future: future.set_result(None) - return + return None abm.raw_message = { "message": msg, "future": future, @@ -427,7 +425,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): buffer = self.user_buffer.get(message.sender.user_id, None) if buffer is None: logger.critical( - f"用户消息未找到缓冲状态,无法处理消息: user={message.sender.user_id} message_id={message.message_id}" + f"用户消息未找到缓冲状态,无法处理消息: user={message.sender.user_id} message_id={message.message_id}", ) return message_event = WeixinOfficialAccountPlatformEvent( diff --git a/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py b/astrbot/core/platform/sources/weixin_official_account/weixin_offacc_event.py index 9ac2a4940..8d566a43c 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,7 @@ 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 @@ -83,7 +83,7 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): self.client.message.send_text(message_obj.sender.user_id, chunk) else: logger.debug( - f"split plain into {len(plain_chunks)} chunks for passive reply. Message not sent." + f"split plain into {len(plain_chunks)} chunks for passive reply. Message not sent.", ) self.message_out["cached_xml"] = plain_chunks elif isinstance(comp, Image): @@ -94,13 +94,13 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent): except Exception as e: logger.error(f"微信公众平台上传图片失败: {e}") await self.send( - MessageChain().message(f"微信公众平台上传图片失败: {e}") + MessageChain().message(f"微信公众平台上传图片失败: {e}"), ) return 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 +121,13 @@ 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 ad8bb44f6..37a6feb3d 100644 --- a/astrbot/core/platform_message_history_mgr.py +++ b/astrbot/core/platform_message_history_mgr.py @@ -41,7 +41,7 @@ class PlatformMessageHistoryManager: return history async def delete( - self, platform_id: str, user_id: str, offset_sec: int = 86400 + self, platform_id: str, user_id: str, offset_sec: int = 86400, ) -> None: """Delete platform message history records older than the specified offset.""" await self.db.delete_platform_message_offset( diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index cdcb731f7..2e095cc3d 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -484,7 +484,7 @@ class LLMResponse: ), # the extra_content will not serialize if it's None when calling ToolCall.model_dump() extra_content=self.tools_call_extra_content.get( - self.tools_call_ids[idx] + self.tools_call_ids[idx], ), ), ) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 86899287c..a9c164074 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -223,7 +223,7 @@ class ProviderManager: return self.inst_map.get(provider_id) def get_using_provider( - self, provider_type: ProviderType, umo=None + self, provider_type: ProviderType, umo=None, ) -> Providers | None: """获取正在使用的提供商实例。 @@ -281,7 +281,7 @@ class ProviderManager: if not provider and provider_id: logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。", ) return provider @@ -367,6 +367,7 @@ class ProviderManager: Raises: ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。 + """ match type: case "openai_chat_completion": @@ -499,6 +500,7 @@ class ProviderManager: Returns: dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典 + """ pc = copy.deepcopy(provider_config) provider_source_id = pc.get("provider_source_id", "") @@ -599,7 +601,7 @@ class ProviderManager: # STT 任务 if not issubclass(cls_type, STTProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of STTProvider" + f"Provider class {cls_type} is not a subclass of STTProvider", ) inst = cls_type(provider_config, self.provider_settings) @@ -622,7 +624,7 @@ class ProviderManager: # TTS 任务 if not issubclass(cls_type, TTSProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of TTSProvider" + f"Provider class {cls_type} is not a subclass of TTSProvider", ) inst = cls_type(provider_config, self.provider_settings) @@ -645,7 +647,7 @@ class ProviderManager: # 文本生成任务 if not issubclass(cls_type, Provider): raise TypeError( - f"Provider class {cls_type} is not a subclass of Provider" + f"Provider class {cls_type} is not a subclass of Provider", ) inst = cls_type( provider_config, @@ -670,7 +672,7 @@ class ProviderManager: case ProviderType.EMBEDDING: if not issubclass(cls_type, EmbeddingProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of EmbeddingProvider" + f"Provider class {cls_type} is not a subclass of EmbeddingProvider", ) inst = cls_type(provider_config, self.provider_settings) if isinstance(inst, HasInitialize): @@ -679,7 +681,7 @@ class ProviderManager: case ProviderType.RERANK: if not issubclass(cls_type, RerankProvider): raise TypeError( - f"Provider class {cls_type} is not a subclass of RerankProvider" + f"Provider class {cls_type} is not a subclass of RerankProvider", ) inst = cls_type(provider_config, self.provider_settings) if isinstance(inst, HasInitialize): @@ -689,7 +691,7 @@ class ProviderManager: # 未知供应商抛出异常,确保inst初始化 # Should be unreachable raise Exception( - f"未知的提供商类型:{provider_metadata.provider_type}" + f"未知的提供商类型:{provider_metadata.provider_type}", ) self.inst_map[provider_config["id"]] = inst @@ -784,7 +786,7 @@ class ProviderManager: del self.inst_map[provider_id] async def delete_provider( - self, provider_id: str | None = None, provider_source_id: str | None = None + self, provider_id: str | None = None, provider_source_id: str | None = None, ) -> None: """Delete provider and/or provider source from config and terminate the instances. Config will be saved after deletion.""" async with self.resource_lock: @@ -808,7 +810,7 @@ class ProviderManager: async def update_provider(self, origin_provider_id: str, new_config: dict) -> None: """Update provider config and reload the instance. Config will be saved after update.""" async with self.resource_lock: - npid = new_config.get("id", None) + npid = new_config.get("id") if not npid: raise ValueError("New provider config must have an 'id' field") config = self.acm.default_conf @@ -832,7 +834,7 @@ class ProviderManager: async def create_provider(self, new_config: dict) -> None: """Add new provider config and load the instance. Config will be saved after addition.""" async with self.resource_lock: - npid = new_config.get("id", None) + npid = new_config.get("id") if not npid: raise ValueError("New provider config must have an 'id' field") config = self.acm.default_conf diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index 7943927ad..bec8ca606 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -19,7 +19,7 @@ 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", ] @@ -54,12 +54,12 @@ class AbstractProvider(abc.ABC): return meta async def test(self) -> None: - """test the provider is a + """Test the provider is a - raises: + Raises: Exception: if the provider is not available + """ - ... class Provider(AbstractProvider): @@ -162,7 +162,7 @@ class Provider(AbstractProvider): """ if False: yield None - raise NotImplementedError() + raise NotImplementedError async def pop_record(self, context: list) -> None: """弹出 context 第一条非系统提示词对话记录""" @@ -179,7 +179,7 @@ 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 +210,7 @@ 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) @@ -229,6 +229,7 @@ class TTSProvider(AbstractProvider): Notes: 子类可以重写此方法返回 True 来启用流式 TTS 支持 + """ return False @@ -255,6 +256,7 @@ class TTSProvider(AbstractProvider): - 默认实现会将文本累积后一次性调用 get_audio 生成完整音频 - 子类可以重写此方法实现真正的流式 TTS - 音频数据应该是 WAV 格式的 bytes + """ accumulated_text = "" while True: @@ -280,7 +282,7 @@ class TTSProvider(AbstractProvider): file_size = (await audio_path_obj.stat()).st_size if file_size == 0: raise Exception( - "TTS test failed: generated audio file is empty (0 bytes). Please check your TTS provider configuration, especially required parameters like group_id for MiniMax." + "TTS test failed: generated audio file is empty (0 bytes). Please check your TTS provider configuration, especially required parameters like group_id for MiniMax.", ) try: os.remove(audio_path) @@ -354,7 +356,7 @@ class EmbeddingProvider(AbstractProvider): if attempt == max_retries - 1: failed_batches.append((batch_idx, batch_texts)) raise Exception( - f"批次 {batch_idx} 处理失败,已重试 {max_retries} 次: {e!s}" + f"批次 {batch_idx} 处理失败,已重试 {max_retries} 次: {e!s}", ) await asyncio.sleep(2**attempt) @@ -381,7 +383,7 @@ 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 1f3f9c36d..42f011945 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -55,7 +55,7 @@ class ProviderAnthropic(Provider): return raise EmptyModelOutputError( "Anthropic completion has no usable output. " - f"completion_id={completion_id}, stop_reason={stop_reason}" + f"completion_id={completion_id}, stop_reason={stop_reason}", ) @staticmethod @@ -233,7 +233,7 @@ class ProviderAnthropic(Provider): # Detect actual image format from binary data image_bytes = base64.b64decode(base64_data) media_type = self._detect_image_mime_type( - image_bytes + image_bytes, ) converted_content.append( { @@ -243,22 +243,22 @@ class ProviderAnthropic(Provider): "media_type": media_type, "data": base64_data, }, - } + }, ) except ValueError: logger.warning( - f"Failed to parse image data URI: {url[:50]}..." + f"Failed to parse image data URI: {url[:50]}...", ) else: logger.warning( - f"Unsupported image URL format for Anthropic: {url[:50]}..." + f"Unsupported image URL format for Anthropic: {url[:50]}...", ) elif part.get("type") == "audio_url": converted_content.append( { "type": "text", "text": "[Audio Attachment]", - } + }, ) else: converted_content.append(part) @@ -266,7 +266,7 @@ class ProviderAnthropic(Provider): { "role": "user", "content": converted_content, - } + }, ) else: new_messages.append(message) @@ -298,7 +298,7 @@ class ProviderAnthropic(Provider): payloads["tool_choice"] = { "type": "any" if payloads.get("tool_choice") == "required" - else "auto" + else "auto", } extra_body = self.provider_config.get("custom_extra_body", {}) @@ -309,7 +309,7 @@ class ProviderAnthropic(Provider): try: completion = await self.client.messages.create( - **payloads, stream=False, extra_body=extra_body + **payloads, stream=False, extra_body=extra_body, ) except httpx.RequestError as e: proxy = self.provider_config.get("proxy", "") @@ -326,7 +326,7 @@ class ProviderAnthropic(Provider): if len(completion.content) == 0: raise EmptyModelOutputError( - f"Anthropic completion is empty. completion_id={completion.id}" + f"Anthropic completion is empty. completion_id={completion.id}", ) llm_response = LLMResponse(role="assistant") @@ -357,13 +357,13 @@ class ProviderAnthropic(Provider): if not llm_response.reasoning_content: raise EmptyModelOutputError( "Anthropic completion has no usable output. " - f"completion_id={completion.id}, stop_reason={completion.stop_reason}" + f"completion_id={completion.id}, stop_reason={completion.stop_reason}", ) # We have reasoning content (ThinkingBlock) - this is valid stop_reason = getattr(completion, "stop_reason", "unknown") logger.debug( - f"Completion contains only ThinkingBlock (stop_reason={stop_reason})" + f"Completion contains only ThinkingBlock (stop_reason={stop_reason})", ) llm_response.completion_text = "" # Ensure empty string, not None @@ -385,7 +385,7 @@ class ProviderAnthropic(Provider): payloads["tool_choice"] = { "type": "any" if payloads.get("tool_choice") == "required" - else "auto" + else "auto", } # 用于累积工具调用信息 @@ -404,7 +404,7 @@ class ProviderAnthropic(Provider): self._apply_thinking_config(payloads) async with self.client.messages.stream( - **payloads, extra_body=extra_body + **payloads, extra_body=extra_body, ) as stream: assert isinstance(stream, anthropic.AsyncMessageStream) async for event in stream: diff --git a/astrbot/core/provider/sources/azure_tts_source.py b/astrbot/core/provider/sources/azure_tts_source.py index dfd5b2c2b..ff8de45e9 100644 --- a/astrbot/core/provider/sources/azure_tts_source.py +++ b/astrbot/core/provider/sources/azure_tts_source.py @@ -40,13 +40,13 @@ class OTTSProvider: def client(self) -> AsyncClient: if self._client is None: raise RuntimeError( - "Client not initialized. Please use 'async with' context." + "Client not initialized. Please use 'async with' context.", ) return self._client async def __aenter__(self): self._client = AsyncClient( - timeout=self.timeout, proxy=self.proxy if self.proxy else None + timeout=self.timeout, proxy=self.proxy if self.proxy else None, ) return self @@ -140,7 +140,7 @@ class AzureNativeProvider(TTSProvider): def client(self) -> AsyncClient: if self._client is None: raise RuntimeError( - "Client not initialized. Please use 'async with' context." + "Client not initialized. Please use 'async with' context.", ) return self._client @@ -212,7 +212,7 @@ class AzureTTSProvider(TTSProvider): self.provider = self._parse_provider(key_value, provider_config) def _parse_provider( - self, key_value: str, config: dict + self, key_value: str, config: dict, ) -> OTTSProvider | AzureNativeProvider: if key_value.lower().startswith("other["): json_str = "" diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py index e98be7200..5ebb528db 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -11,23 +11,20 @@ from astrbot.core.provider.register import register_provider_adapter class BailianRerankError(Exception): """百炼重排序服务异常基类""" - pass class BailianAPIError(BailianRerankError): """百炼API返回错误""" - pass class BailianNetworkError(BailianRerankError): """百炼网络请求错误""" - pass @register_provider_adapter( - "bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK + "bailian_rerank", "阿里云百炼文本排序适配器", provider_type=ProviderType.RERANK, ) class BailianRerankProvider(RerankProvider): """阿里云百炼文本重排序适配器.""" @@ -41,7 +38,7 @@ class BailianRerankProvider(RerankProvider): # API配置 self.api_key = provider_config.get("rerank_api_key") or os.getenv( - "DASHSCOPE_API_KEY", "" + "DASHSCOPE_API_KEY", "", ) if not self.api_key: raise ValueError("阿里云百炼 API Key 不能为空。") @@ -63,7 +60,7 @@ 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), ) # 设置模型名称 @@ -72,7 +69,7 @@ class BailianRerankProvider(RerankProvider): logger.info(f"AstrBot 百炼 Rerank 初始化完成。模型: {self.model}") def _build_payload( - self, query: str, documents: list[str], top_n: int | None + self, query: str, documents: list[str], top_n: int | None, ) -> dict: """构建请求载荷 @@ -83,6 +80,7 @@ class BailianRerankProvider(RerankProvider): Returns: 请求载荷字典 + """ normalized_model = self.model.strip().lower() normalized_top_n = top_n if top_n is not None and top_n > 0 else None @@ -102,7 +100,7 @@ class BailianRerankProvider(RerankProvider): if self.return_documents: logger.warning( "qwen3-rerank does not support return_documents; " - "this option will be ignored." + "this option will be ignored.", ) return payload @@ -134,11 +132,12 @@ class BailianRerankProvider(RerankProvider): Raises: BailianAPIError: API返回错误 KeyError: 结果缺少必要字段 + """ # 检查响应状态 if data.get("code", "200") != "200": raise BailianAPIError( - f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}" + f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}", ) # 兼容旧版 API (output.results) 和新版 compatible API (results) @@ -159,7 +158,7 @@ class BailianRerankProvider(RerankProvider): relevance_score = 0.0 rerank_result = RerankResult( - index=index, relevance_score=relevance_score + index=index, relevance_score=relevance_score, ) rerank_results.append(rerank_result) except Exception as e: @@ -173,6 +172,7 @@ class BailianRerankProvider(RerankProvider): Args: data: API响应数据 + """ tokens = data.get("usage", {}).get("total_tokens", 0) if tokens > 0: @@ -184,8 +184,7 @@ class BailianRerankProvider(RerankProvider): documents: list[str], top_n: int | None = None, ) -> list[RerankResult]: - """ - 对文档进行重排序 + """对文档进行重排序 Args: query: 查询文本 @@ -194,6 +193,7 @@ class BailianRerankProvider(RerankProvider): Returns: 重排序结果列表 + """ if not self.client: logger.error("百炼 Rerank 客户端会话已关闭,返回空结果") @@ -217,7 +217,7 @@ class BailianRerankProvider(RerankProvider): payload = self._build_payload(query, documents, top_n) logger.debug( - f"百炼 Rerank 请求: query='{query[:50]}...', 文档数量={len(documents)}" + f"百炼 Rerank 请求: query='{query[:50]}...', 文档数量={len(documents)}", ) # 发送请求 diff --git a/astrbot/core/provider/sources/fishaudio_tts_api_source.py b/astrbot/core/provider/sources/fishaudio_tts_api_source.py index 0223dd138..f0d1f412c 100644 --- a/astrbot/core/provider/sources/fishaudio_tts_api_source.py +++ b/astrbot/core/provider/sources/fishaudio_tts_api_source.py @@ -134,11 +134,11 @@ class ProviderFishAudioTTSAPI(TTSProvider): else: # 回退到原来的角色名称查询逻辑 fetched_reference_id = await self._get_reference_id_by_character( - self.character + self.character, ) if fetched_reference_id is None: raise ValueError( - f"未找到 FishAudio 角色 '{self.character}' 对应的参考模型ID。" + f"未找到 FishAudio 角色 '{self.character}' 对应的参考模型ID。", ) resolved_reference_id = fetched_reference_id @@ -164,7 +164,7 @@ class ProviderFishAudioTTSAPI(TTSProvider): content=ormsgpack.packb(request, option=ormsgpack.OPT_SERIALIZE_PYDANTIC), ) as response: if response.status_code == 200 and response.headers.get( - "content-type", "" + "content-type", "", ).startswith("audio/"): async with aiofiles.open(path, "wb") as f: async for chunk in response.aiter_bytes(): @@ -173,5 +173,5 @@ class ProviderFishAudioTTSAPI(TTSProvider): error_bytes = await response.aread() error_text = error_bytes.decode("utf-8", errors="replace")[:1024] raise Exception( - f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}" + f"Fish Audio API请求失败: 状态码 {response.status_code}, 响应内容: {error_text}", ) diff --git a/astrbot/core/provider/sources/gemini_embedding_source.py b/astrbot/core/provider/sources/gemini_embedding_source.py index 59459826c..d5cd4dee9 100644 --- a/astrbot/core/provider/sources/gemini_embedding_source.py +++ b/astrbot/core/provider/sources/gemini_embedding_source.py @@ -31,7 +31,7 @@ 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 96e404510..84c96be00 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -6,7 +6,7 @@ import random import uuid from collections.abc import AsyncGenerator from pathlib import Path -from typing import Literal, cast +from typing import Literal from urllib.parse import urlparse import aiofiles @@ -39,7 +39,7 @@ 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 +71,13 @@ 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 +85,7 @@ 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)) @@ -101,7 +101,7 @@ class ProviderGoogleGenAI(Provider): if len(keys) > 0: self.set_key(random.choice(keys)) logger.info( - f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试..." + f"检测到 Key 异常({e.message}),正在尝试更换 API Key 重试...", ) await asyncio.sleep(1) return True @@ -141,7 +141,7 @@ class ProviderGoogleGenAI(Provider): logger.warning("代码执行工具与搜索工具互斥,已忽略搜索工具") if url_context: logger.warning( - "代码执行工具与URL上下文工具互斥,已忽略URL上下文工具" + "代码执行工具与URL上下文工具互斥,已忽略URL上下文工具", ) else: if native_search: @@ -151,12 +151,12 @@ class ProviderGoogleGenAI(Provider): tool_list.append(types.Tool(url_context=types.UrlContext())) else: logger.warning( - "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包" + "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", ) elif "gemini-2.0-lite" in model_name: if native_coderunner or native_search or url_context: logger.warning( - "gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置" + "gemini-2.0-lite 不支持代码执行、搜索工具和URL上下文,将忽略这些设置", ) else: if native_coderunner: @@ -170,13 +170,13 @@ class ProviderGoogleGenAI(Provider): tool_list.append(types.Tool(url_context=types.UrlContext())) else: logger.warning( - "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包" + "当前 SDK 版本不支持 URL 上下文工具,已忽略该设置,请升级 google-genai 包", ) if tools and tool_list: logger.warning("已启用原生工具,函数工具将被忽略") elif tools and (func_desc := tools.get_func_desc_google_genai_style()): tool_list = [ - types.Tool(function_declarations=func_desc["function_declarations"]) + types.Tool(function_declarations=func_desc["function_declarations"]), ] tool_config = None if tools and tool_list: @@ -184,8 +184,8 @@ class ProviderGoogleGenAI(Provider): function_calling_config=types.FunctionCallingConfig( mode=types.FunctionCallingConfigMode.ANY if tool_choice == "required" - else types.FunctionCallingConfigMode.AUTO - ) + else types.FunctionCallingConfigMode.AUTO, + ), ) thinking_config = None if model_name in [ @@ -199,7 +199,7 @@ 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,13 +211,13 @@ class ProviderGoogleGenAI(Provider): # covered without needing to keep an exhaustive list up to date. # Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead. thinking_level = self.provider_config.get("gm_thinking_config", {}).get( - "level", "HIGH" + "level", "HIGH", ) if thinking_level and isinstance(thinking_level, str): thinking_level = thinking_level.upper() if thinking_level not in ["MINIMAL", "LOW", "MEDIUM", "HIGH"]: logger.warning( - f"Invalid thinking level: {thinking_level}, using HIGH" + f"Invalid thinking level: {thinking_level}, using HIGH", ) thinking_level = "HIGH" level = types.ThinkingLevel(thinking_level) @@ -248,7 +248,7 @@ class ProviderGoogleGenAI(Provider): safety_settings=self.safety_settings if self.safety_settings else None, thinking_config=thinking_config, automatic_function_calling=types.AutomaticFunctionCallingConfig( - disable=True + disable=True, ), ) @@ -289,7 +289,7 @@ class ProviderGoogleGenAI(Provider): [ self.provider_config.get("gm_native_coderunner", False), self.provider_config.get("gm_native_search", False), - ] + ], ) for message in payloads["messages"]: role, content = (message["role"], message.get("content")) @@ -333,7 +333,7 @@ class ProviderGoogleGenAI(Provider): ) thinking_signature = None parts.append( - types.Part(text=text, thought_signature=thinking_signature) + types.Part(text=text, thought_signature=thinking_signature), ) append_or_extend(gemini_contents, parts, types.ModelContent) elif not native_tool_enabled and "tool_calls" in message: @@ -357,7 +357,7 @@ class ProviderGoogleGenAI(Provider): logger.warning("assistant 角色的消息内容为空,已添加空格占位") if native_tool_enabled and "tool_calls" in message: logger.warning( - "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文" + "检测到启用Gemini原生工具,且上下文中存在函数调用,建议使用 /reset 重置上下文", ) parts = [types.Part.from_text(text=" ")] append_or_extend(gemini_contents, parts, types.ModelContent) @@ -385,7 +385,7 @@ class ProviderGoogleGenAI(Provider): return "".join(thought_buf).strip() def _extract_usage( - self, usage_metadata: types.GenerateContentResponseUsageMetadata + self, usage_metadata: types.GenerateContentResponseUsageMetadata, ) -> TokenUsage: """Extract usage from candidate""" return TokenUsage( @@ -407,7 +407,7 @@ class ProviderGoogleGenAI(Provider): if has_text_output or has_reasoning_output or has_tool_output: return raise EmptyModelOutputError( - f"Gemini completion has no usable output. response_id={response_id}, finish_reason={finish_reason}" + f"Gemini completion has no usable output. response_id={response_id}, finish_reason={finish_reason}", ) def _process_content_parts( @@ -423,7 +423,7 @@ class ProviderGoogleGenAI(Provider): if validate_output: raise EmptyModelOutputError( "Gemini candidate content is empty. " - f"finish_reason={candidate.finish_reason}" + f"finish_reason={candidate.finish_reason}", ) llm_response.result_chain = MessageChain(chain=[]) return llm_response.result_chain @@ -446,7 +446,7 @@ class ProviderGoogleGenAI(Provider): if validate_output: raise EmptyModelOutputError( "Gemini candidate content parts are empty. " - f"finish_reason={candidate.finish_reason}" + f"finish_reason={candidate.finish_reason}", ) llm_response.result_chain = MessageChain(chain=[]) return llm_response.result_chain @@ -484,7 +484,7 @@ class ProviderGoogleGenAI(Provider): if part.thought_signature: ts_bs64 = base64.b64encode(part.thought_signature).decode("utf-8") llm_response.tools_call_extra_content[tool_call_id] = { - "google": {"thought_signature": ts_bs64} + "google": {"thought_signature": ts_bs64}, } if ( part.inline_data @@ -530,7 +530,7 @@ 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: @@ -541,7 +541,7 @@ class ProviderGoogleGenAI(Provider): raise Exception("温度参数已超过最大值2,仍然发生recitation") temperature += 0.2 logger.warning( - f"发生了recitation,正在提高温度至{temperature:.1f}重试..." + f"发生了recitation,正在提高温度至{temperature:.1f}重试...", ) continue break @@ -550,7 +550,7 @@ class ProviderGoogleGenAI(Provider): e.message = "" if "Developer instruction is not enabled" in e.message: logger.warning( - f"{model} 不支持 system prompt,已自动去除(影响人格设置)" + f"{model} 不支持 system prompt,已自动去除(影响人格设置)", ) system_instruction = None elif "Function calling is not enabled" in e.message: @@ -570,7 +570,7 @@ 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 +578,7 @@ 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 +598,7 @@ 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: @@ -606,7 +606,7 @@ class ProviderGoogleGenAI(Provider): e.message = "" if "Developer instruction is not enabled" in e.message: logger.warning( - f"{model} 不支持 system prompt,已自动去除(影响人格设置)" + f"{model} 不支持 system prompt,已自动去除(影响人格设置)", ) system_instruction = None elif "Function calling is not enabled" in e.message: @@ -672,7 +672,7 @@ class ProviderGoogleGenAI(Provider): final_response.reasoning_content = accumulated_reasoning if accumulated_text: final_response.result_chain = MessageChain( - chain=[Comp.Plain(accumulated_text)] + chain=[Comp.Plain(accumulated_text)], ) self._ensure_usable_response( final_response, @@ -844,7 +844,7 @@ class ProviderGoogleGenAI(Provider): temp_dir = Path(get_astrbot_temp_path()) temp_dir.mkdir(parents=True, exist_ok=True) resolved_path = str( - temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}" + temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}", ) await download_file(audio_path, resolved_path) elif audio_path.startswith("file:///"): @@ -861,7 +861,7 @@ class ProviderGoogleGenAI(Provider): audio_bytes = Path(resolved_path).read_bytes() except OSError as exc: logger.warning( - f"Failed to read audio file {resolved_path}, skipping. Error: {exc}" + f"Failed to read audio file {resolved_path}, skipping. Error: {exc}", ) return None diff --git a/astrbot/core/provider/sources/groq_source.py b/astrbot/core/provider/sources/groq_source.py index 4fc8a9dc4..c82ab5883 100644 --- a/astrbot/core/provider/sources/groq_source.py +++ b/astrbot/core/provider/sources/groq_source.py @@ -4,7 +4,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "groq_chat_completion", "Groq Chat Completion Provider Adapter" + "groq_chat_completion", "Groq Chat Completion Provider Adapter", ) class ProviderGroq(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/longcat_source.py b/astrbot/core/provider/sources/longcat_source.py index e251fb310..21d8043b9 100644 --- a/astrbot/core/provider/sources/longcat_source.py +++ b/astrbot/core/provider/sources/longcat_source.py @@ -3,7 +3,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "longcat_chat_completion", "LongCat Chat Completion Provider Adapter" + "longcat_chat_completion", "LongCat Chat Completion Provider Adapter", ) class ProviderLongCat(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/mimo_stt_api_source.py b/astrbot/core/provider/sources/mimo_stt_api_source.py index c30b7992a..074e91ff7 100644 --- a/astrbot/core/provider/sources/mimo_stt_api_source.py +++ b/astrbot/core/provider/sources/mimo_stt_api_source.py @@ -83,7 +83,7 @@ class ProviderMiMoSTTAPI(STTProvider): except Exception as exc: error_text = response.text[:1024] raise MiMoAPIError( - f"MiMo STT API request failed: HTTP {response.status_code}, response: {error_text}" + f"MiMo STT API request failed: HTTP {response.status_code}, response: {error_text}", ) from exc data = response.json() diff --git a/astrbot/core/provider/sources/mimo_tts_api_source.py b/astrbot/core/provider/sources/mimo_tts_api_source.py index 679966ce7..b4e93368d 100644 --- a/astrbot/core/provider/sources/mimo_tts_api_source.py +++ b/astrbot/core/provider/sources/mimo_tts_api_source.py @@ -40,7 +40,7 @@ class ProviderMiMoTTSAPI(TTSProvider): self.style_prompt = provider_config.get("mimo-tts-style-prompt", "") self.dialect = provider_config.get("mimo-tts-dialect", "") self.seed_text = provider_config.get( - "mimo-tts-seed-text", DEFAULT_MIMO_TTS_SEED_TEXT + "mimo-tts-seed-text", DEFAULT_MIMO_TTS_SEED_TEXT, ) self.set_model(provider_config.get("model", DEFAULT_MIMO_TTS_MODEL)) self.client = create_http_client(self.timeout, self.proxy) @@ -79,14 +79,14 @@ class ProviderMiMoTTSAPI(TTSProvider): { "role": "user", "content": user_prompt, - } + }, ) messages.append( { "role": "assistant", "content": self._build_assistant_content(text), - } + }, ) return { @@ -110,7 +110,7 @@ class ProviderMiMoTTSAPI(TTSProvider): except Exception as exc: error_text = response.text[:1024] raise MiMoAPIError( - f"MiMo TTS API request failed: HTTP {response.status_code}, response: {error_text}" + f"MiMo TTS API request failed: HTTP {response.status_code}, response: {error_text}", ) from exc data = response.json() diff --git a/astrbot/core/provider/sources/minimax_tts_api_source.py b/astrbot/core/provider/sources/minimax_tts_api_source.py index 1d7f0c388..d4684483e 100644 --- a/astrbot/core/provider/sources/minimax_tts_api_source.py +++ b/astrbot/core/provider/sources/minimax_tts_api_source.py @@ -121,7 +121,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider): if "extra_info" in data: continue audio: str | None = data.get("data", {}).get( - "audio" + "audio", ) if audio is not None: yield audio @@ -159,7 +159,7 @@ class ProviderMiniMaxTTSAPI(TTSProvider): raise Exception( "MiniMax TTS API returned empty audio data. " "Please verify your configuration, especially the 'group_id' parameter. " - "You can find your group_id in Account Management -> Basic Information on the MiniMax platform." + "You can find your group_id in Account Management -> Basic Information on the MiniMax platform.", ) # 结果保存至文件 diff --git a/astrbot/core/provider/sources/nvidia_rerank_source.py b/astrbot/core/provider/sources/nvidia_rerank_source.py index c168da4a6..0c76e73b9 100644 --- a/astrbot/core/provider/sources/nvidia_rerank_source.py +++ b/astrbot/core/provider/sources/nvidia_rerank_source.py @@ -8,21 +8,21 @@ from ..register import register_provider_adapter @register_provider_adapter( - "nvidia_rerank", "NVIDIA Rerank 适配器", provider_type=ProviderType.RERANK + "nvidia_rerank", "NVIDIA Rerank 适配器", provider_type=ProviderType.RERANK, ) class NvidiaRerankProvider(RerankProvider): def __init__(self, provider_config: dict, provider_settings: dict) -> None: super().__init__(provider_config, provider_settings) self.api_key = provider_config.get("nvidia_rerank_api_key", "") self.base_url = provider_config.get( - "nvidia_rerank_api_base", "https://ai.api.nvidia.com/v1/retrieval" + "nvidia_rerank_api_base", "https://ai.api.nvidia.com/v1/retrieval", ).rstrip("/") self.timeout = provider_config.get("timeout", 20) self.model = provider_config.get( - "nvidia_rerank_model", "nv-rerank-qa-mistral-4b:1" + "nvidia_rerank_model", "nv-rerank-qa-mistral-4b:1", ) self.model_endpoint = provider_config.get( - "nvidia_rerank_model_endpoint", "/reranking" + "nvidia_rerank_model_endpoint", "/reranking", ) self.truncate = provider_config.get("nvidia_rerank_truncate", "") @@ -37,13 +37,12 @@ class NvidiaRerankProvider(RerankProvider): "Accept": "application/json", } self.client = aiohttp.ClientSession( - headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout) + headers=headers, timeout=aiohttp.ClientTimeout(total=self.timeout), ) return self.client def _get_endpoint(self) -> str: - """ - 构建完整API URL。 + """构建完整API URL。 根据 Nvidia Rerank API 文档来看,当前URL存在不同模型格式不一致的问题。 这里针对模型名做一个基础判断用以适配,后续要等Nvidia统一API格式后再做调整。 @@ -55,7 +54,6 @@ class NvidiaRerankProvider(RerankProvider): 模型: nvidia/llama-nemotron-rerank-1b-v2 URL: .../v1/retrieval/nvidia/llama-nemotron-rerank-1b-v2/reranking """ - model_path = "nvidia" logger.debug(f"[NVIDIA Rerank] Building endpoint for model: {self.model}") if "/" in self.model: @@ -76,7 +74,7 @@ class NvidiaRerankProvider(RerankProvider): return payload def _parse_results( - self, response_data: dict, top_n: int | None + self, response_data: dict, top_n: int | None, ) -> list[RerankResult]: """解析响应数据""" results = response_data.get("rankings", []) @@ -90,11 +88,11 @@ class NvidiaRerankProvider(RerankProvider): index = item.get("index", idx) score = item.get("relevance_score", item.get("logit", 0.0)) rerank_results.append( - RerankResult(index=index, relevance_score=float(score)) + RerankResult(index=index, relevance_score=float(score)), ) except Exception as e: logger.warning( - f"[NVIDIA Rerank] Result parsing error: {e}, Data={item}" + f"[NVIDIA Rerank] Result parsing error: {e}, Data={item}", ) rerank_results.sort(key=lambda x: x.relevance_score, reverse=True) @@ -122,7 +120,7 @@ class NvidiaRerankProvider(RerankProvider): if not documents or not query.strip(): logger.warning( - "[NVIDIA Rerank] Input data is invalid, query or documents are empty" + "[NVIDIA Rerank] Input data is invalid, query or documents are empty", ) return [] @@ -135,7 +133,7 @@ class NvidiaRerankProvider(RerankProvider): try: response_data = await response.json() error_detail = response_data.get( - "detail", response_data.get("message", "Unknown Error") + "detail", response_data.get("message", "Unknown Error"), ) except Exception: diff --git a/astrbot/core/provider/sources/oai_aihubmix_source.py b/astrbot/core/provider/sources/oai_aihubmix_source.py index fc6b0e3c0..f4a7018bb 100644 --- a/astrbot/core/provider/sources/oai_aihubmix_source.py +++ b/astrbot/core/provider/sources/oai_aihubmix_source.py @@ -4,7 +4,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "aihubmix_chat_completion", "AIHubMix Chat Completion Provider Adapter" + "aihubmix_chat_completion", "AIHubMix Chat Completion Provider Adapter", ) class ProviderAIHubMix(ProviderOpenAIOfficial): def __init__(self, provider_config: dict, provider_settings: dict) -> None: diff --git a/astrbot/core/provider/sources/openai_embedding_source.py b/astrbot/core/provider/sources/openai_embedding_source.py index 7a503aeb4..60227b471 100644 --- a/astrbot/core/provider/sources/openai_embedding_source.py +++ b/astrbot/core/provider/sources/openai_embedding_source.py @@ -69,7 +69,7 @@ class OpenAIEmbeddingProvider(EmbeddingProvider): kwargs["dimensions"] = int(self.provider_config["embedding_dimensions"]) except (ValueError, TypeError): logger.warning( - f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored." + f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored.", ) return kwargs @@ -80,7 +80,7 @@ class OpenAIEmbeddingProvider(EmbeddingProvider): return int(self.provider_config["embedding_dimensions"]) except (ValueError, TypeError): logger.warning( - f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored." + f"embedding_dimensions in embedding configs is not a valid integer: '{self.provider_config['embedding_dimensions']}', ignored.", ) return 0 diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 55735605d..211568c24 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -93,7 +93,7 @@ class ProviderOpenAIOfficial(Provider): if not text: return candidates.append( - ProviderOpenAIOfficial._truncate_error_text_candidate(text) + ProviderOpenAIOfficial._truncate_error_text_candidate(text), ) _append_candidate(str(error)) @@ -102,7 +102,7 @@ class ProviderOpenAIOfficial(Provider): if isinstance(body, dict): err_obj = body.get("error") body_text = ProviderOpenAIOfficial._safe_json_dump( - {"error": err_obj} if isinstance(err_obj, dict) else body + {"error": err_obj} if isinstance(err_obj, dict) else body, ) _append_candidate(body_text) if isinstance(err_obj, dict): @@ -372,7 +372,7 @@ class ProviderOpenAIOfficial(Provider): try: resolved_part = await self._resolve_image_part( - url, image_detail=image_detail + url, image_detail=image_detail, ) except Exception as exc: logger.warning( @@ -402,7 +402,7 @@ class ProviderOpenAIOfficial(Provider): return {**message, "content": new_content} async def _materialize_context_image_parts( - self, context_query: list[dict] + self, context_query: list[dict], ) -> list[dict]: return [ await self._materialize_message_image_parts(message) @@ -494,7 +494,7 @@ class ProviderOpenAIOfficial(Provider): return bool(value) def _apply_provider_specific_extra_body_overrides( - self, extra_body: dict[str, Any] + self, extra_body: dict[str, Any], ) -> None: if self.provider_config.get("provider") != "ollama": return @@ -728,6 +728,7 @@ class ProviderOpenAIOfficial(Provider): Returns: Normalized plain text string. + """ # Handle dict format (e.g., {"type": "text", "text": "..."}) if isinstance(raw_content, dict): @@ -794,7 +795,7 @@ class ProviderOpenAIOfficial(Provider): text_val = part.get("text", "") # Coerce to str in case text is null or non-string text_parts.append( - str(text_val) if text_val is not None else "" + str(text_val) if text_val is not None else "", ) if text_parts: return "".join(text_parts) @@ -804,14 +805,14 @@ class ProviderOpenAIOfficial(Provider): return str(raw_content) if raw_content is not None else "" async def _parse_openai_completion( - self, completion: ChatCompletion, tools: ToolSet | None + self, completion: ChatCompletion, tools: ToolSet | None, ) -> LLMResponse: """Parse OpenAI ChatCompletion into LLMResponse""" llm_response = LLMResponse("assistant") if not completion.choices: raise EmptyModelOutputError( - f"OpenAI completion has no choices. response_id={completion.id}" + f"OpenAI completion has no choices. response_id={completion.id}", ) choice = completion.choices[0] @@ -896,7 +897,7 @@ class ProviderOpenAIOfficial(Provider): logger.error(f"OpenAI completion has no usable output: {completion}.") raise EmptyModelOutputError( "OpenAI completion has no usable output. " - f"response_id={completion.id}, finish_reason={choice.finish_reason}" + f"response_id={completion.id}, finish_reason={choice.finish_reason}", ) llm_response.raw_completion = completion @@ -966,7 +967,7 @@ class ProviderOpenAIOfficial(Provider): for message in payloads.get("messages", []): if message.get("role") == "assistant" and isinstance( - message.get("content"), list + message.get("content"), list, ): reasoning_content = "" new_content = [] # not including think part @@ -990,7 +991,7 @@ class ProviderOpenAIOfficial(Provider): json.loads(content) except (json.JSONDecodeError, ValueError): message["content"] = json.dumps( - {"result": content}, ensure_ascii=False + {"result": content}, ensure_ascii=False, ) async def _handle_api_error( @@ -1291,7 +1292,6 @@ class ProviderOpenAIOfficial(Provider): extra_user_content_parts: list[ContentPart] | None = None, ) -> dict: """组装成符合 OpenAI 格式的 role 为 user 的消息段""" - # 构建内容块列表 content_blocks = [] @@ -1354,7 +1354,6 @@ class ProviderOpenAIOfficial(Provider): async def encode_image_bs64(self, image_url: str) -> str: """将图片转换为 base64""" - image_data = await self._image_ref_to_data_url(image_url, mode="strict") if image_data is None: raise RuntimeError(f"Failed to encode image data: {image_url}") diff --git a/astrbot/core/provider/sources/openai_tts_api_source.py b/astrbot/core/provider/sources/openai_tts_api_source.py index abc472da8..8349c812e 100644 --- a/astrbot/core/provider/sources/openai_tts_api_source.py +++ b/astrbot/core/provider/sources/openai_tts_api_source.py @@ -53,10 +53,9 @@ class ProviderOpenAITTSAPI(TTSProvider): voice=self.voice, response_format="wav", input=text, - ) as response: - async with aiofiles.open(path, "wb") as f: - async for chunk in response.iter_bytes(chunk_size=1024): - await f.write(chunk) + ) as response, aiofiles.open(path, "wb") as f: + async for chunk in response.iter_bytes(chunk_size=1024): + await f.write(chunk) return path async def terminate(self): diff --git a/astrbot/core/provider/sources/openrouter_source.py b/astrbot/core/provider/sources/openrouter_source.py index d7389528e..38e8764e2 100644 --- a/astrbot/core/provider/sources/openrouter_source.py +++ b/astrbot/core/provider/sources/openrouter_source.py @@ -4,7 +4,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "openrouter_chat_completion", "OpenRouter Chat Completion Provider Adapter" + "openrouter_chat_completion", "OpenRouter Chat Completion Provider Adapter", ) class ProviderOpenRouter(ProviderOpenAIOfficial): def __init__(self, provider_config: dict, provider_settings: dict) -> None: diff --git a/astrbot/core/provider/sources/volcengine_tts.py b/astrbot/core/provider/sources/volcengine_tts.py index 2183e18fc..7962feb58 100644 --- a/astrbot/core/provider/sources/volcengine_tts.py +++ b/astrbot/core/provider/sources/volcengine_tts.py @@ -123,7 +123,7 @@ class ProviderVolcengineTTS(TTSProvider): # Keep the request metadata useful for debugging without exposing secrets. logger.debug(f"请求 URL: {self.api_base}") logger.debug( - f"请求体: {json.dumps(loggable_payload, ensure_ascii=False)[:100]}..." + f"请求体: {json.dumps(loggable_payload, ensure_ascii=False)[:100]}...", ) try: diff --git a/astrbot/core/provider/sources/whisper_api_source.py b/astrbot/core/provider/sources/whisper_api_source.py index 37329c283..da65ba72c 100644 --- a/astrbot/core/provider/sources/whisper_api_source.py +++ b/astrbot/core/provider/sources/whisper_api_source.py @@ -109,12 +109,12 @@ class ProviderOpenAIWhisperAPI(STTProvider): if file_format == "silk": logger.info( - "Converting silk file to wav using tencent_silk_to_wav..." + "Converting silk file to wav using tencent_silk_to_wav...", ) await tencent_silk_to_wav(audio_url, output_path) elif file_format == "amr": logger.info( - "Converting amr file to wav using convert_to_pcm_wav..." + "Converting amr file to wav using convert_to_pcm_wav...", ) await convert_to_pcm_wav(audio_url, output_path) diff --git a/astrbot/core/provider/sources/xai_source.py b/astrbot/core/provider/sources/xai_source.py index f18551e8d..d975047e0 100644 --- a/astrbot/core/provider/sources/xai_source.py +++ b/astrbot/core/provider/sources/xai_source.py @@ -4,7 +4,7 @@ from .openai_source import ProviderOpenAIOfficial @register_provider_adapter( - "xai_chat_completion", "xAI Chat Completion Provider Adapter" + "xai_chat_completion", "xAI Chat Completion Provider Adapter", ) class ProviderXAI(ProviderOpenAIOfficial): def __init__( diff --git a/astrbot/core/provider/sources/xinference_rerank_source.py b/astrbot/core/provider/sources/xinference_rerank_source.py index 9b5f540ed..fcca5befc 100644 --- a/astrbot/core/provider/sources/xinference_rerank_source.py +++ b/astrbot/core/provider/sources/xinference_rerank_source.py @@ -10,7 +10,7 @@ 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 +23,7 @@ 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 @@ -42,7 +42,7 @@ class XinferenceRerankProvider(RerankProvider): for uid, model_spec in running_models.items(): if model_spec.get("model_name") == self.model_name: logger.info( - f"Model '{self.model_name}' is already running with UID: {uid}" + f"Model '{self.model_name}' is already running with UID: {uid}", ) self.model_uid = uid break @@ -50,12 +50,12 @@ 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: logger.warning( - f"Model '{self.model_name}' is not running and auto-launch is disabled. Provider will not be available." + f"Model '{self.model_name}' is not running and auto-launch is disabled. Provider will not be available.", ) return if self.model_uid: @@ -63,12 +63,12 @@ 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.") @@ -79,11 +79,11 @@ class XinferenceRerankProvider(RerankProvider): logger.debug(f"Rerank API response: {response}") if not results: logger.warning( - f"Rerank API returned an empty list. Original response: {response}" + f"Rerank API returned an empty list. Original response: {response}", ) 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/provider/sources/xinference_stt_provider.py b/astrbot/core/provider/sources/xinference_stt_provider.py index 27713cc2b..b207a2ef0 100644 --- a/astrbot/core/provider/sources/xinference_stt_provider.py +++ b/astrbot/core/provider/sources/xinference_stt_provider.py @@ -129,16 +129,16 @@ class ProviderXinferenceSTT(STTProvider): # 3. Perform conversion if needed if conversion_type: logger.info( - f"Audio requires conversion ({conversion_type}), using temporary files..." + f"Audio requires conversion ({conversion_type}), using temporary files...", ) temp_dir = anyio.Path(get_astrbot_temp_path()) await temp_dir.mkdir(parents=True, exist_ok=True) input_path = str( - temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.input" + temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.input", ) output_path = str( - temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.wav" + temp_dir / f"xinference_stt_{uuid.uuid4().hex[:8]}.wav", ) temp_files.extend([input_path, output_path]) diff --git a/astrbot/core/skills/neo_skill_sync.py b/astrbot/core/skills/neo_skill_sync.py index 5fe2b7832..fa7098c44 100644 --- a/astrbot/core/skills/neo_skill_sync.py +++ b/astrbot/core/skills/neo_skill_sync.py @@ -224,7 +224,7 @@ class NeoSkillSyncManager: items = page_json.get("items", []) if not isinstance(items, list) or not items: raise ValueError( - f"No active stable release found for skill_key: {skill_key}" + f"No active stable release found for skill_key: {skill_key}", ) if not isinstance(items[0], dict): raise ValueError("Unexpected release payload format.") @@ -242,7 +242,7 @@ class NeoSkillSyncManager: release = await self._find_release(client, release_id=release_id) elif skill_key: release = await self._find_active_stable_release( - client, skill_key=skill_key + client, skill_key=skill_key, ) else: raise ValueError("release_id or skill_key is required for sync.") @@ -259,7 +259,7 @@ class NeoSkillSyncManager: if require_stable and release_stage != "stable": raise ValueError( "Only stable releases can be synced to local SKILL.md " - f"(got: {release_stage_raw})." + f"(got: {release_stage_raw}).", ) candidate = await client.skills.get_candidate(candidate_id) @@ -277,7 +277,7 @@ class NeoSkillSyncManager: skill_markdown = payload.get("skill_markdown") if not isinstance(skill_markdown, str) or not skill_markdown.strip(): raise ValueError( - "payload.skill_markdown is required for stable sync to local skill." + "payload.skill_markdown is required for stable sync to local skill.", ) mapping = self._load_map() @@ -348,7 +348,7 @@ class NeoSkillSyncManager: sync_error = str(err) try: rollback = await client.skills.rollback_release( - str(release_json.get("id", "")) + str(release_json.get("id", "")), ) rollback_json = _to_jsonable(rollback) except Exception as rollback_err: @@ -361,7 +361,7 @@ class NeoSkillSyncManager: else: raise RuntimeError( "stable release synced failed and auto rollback also failed; " - f"sync_error={sync_error}; rollback_error={rollback_err}" + f"sync_error={sync_error}; rollback_error={rollback_err}", ) from rollback_err return { diff --git a/astrbot/core/skills/skill_manager.py b/astrbot/core/skills/skill_manager.py index d0b60de1a..82bdf620a 100644 --- a/astrbot/core/skills/skill_manager.py +++ b/astrbot/core/skills/skill_manager.py @@ -283,7 +283,7 @@ class SkillManager: self.skills_root = skills_root or str(self.astrbot_paths.skills) self.config_path = str(self.astrbot_paths.config / SKILLS_CONFIG_FILENAME) self.sandbox_skills_cache_path = str( - self.astrbot_paths.data / SANDBOX_SKILLS_CACHE_FILENAME + self.astrbot_paths.data / SANDBOX_SKILLS_CACHE_FILENAME, ) os.makedirs(self.skills_root, exist_ok=True) @@ -339,7 +339,7 @@ class SkillManager: continue description = str(item.get("description", "") or "") path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, str(item.get("path", "") or ""), ) deduped[name] = { "name": name, @@ -389,7 +389,7 @@ class SkillManager: continue name = str(item.get("name", "") or "").strip() path = _normalize_cached_sandbox_skill_path( - name, str(item.get("path", "") or "") + name, str(item.get("path", "") or ""), ) if not name or not _SKILL_NAME_RE.match(name): continue @@ -430,7 +430,7 @@ class SkillManager: source_label = "synced" if sandbox_exists else "local" if runtime == "sandbox" and show_sandbox_path: path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) else: path_str = str(skill_md) @@ -471,7 +471,7 @@ class SkillManager: # since there is no local path to show. Always prefer the # actual path from sandbox cache. path_str = sandbox_cached_paths.get( - skill_name + skill_name, ) or _default_sandbox_skill_path(skill_name) skills_by_name[skill_name] = SkillInfo( name=skill_name, @@ -509,7 +509,7 @@ class SkillManager: def set_skill_active(self, name: str, active: bool) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be enabled/disabled from local skill management." + "Sandbox preset skill cannot be enabled/disabled from local skill management.", ) config = self._load_config() config.setdefault("skills", {}) @@ -537,7 +537,7 @@ class SkillManager: def delete_skill(self, name: str) -> None: if self.is_sandbox_only_skill(name): raise PermissionError( - "Sandbox preset skill cannot be deleted from local skill management." + "Sandbox preset skill cannot be deleted from local skill management.", ) skill_dir = Path(self.skills_root) / name @@ -589,7 +589,7 @@ class SkillManager: if skill_name_hint is not None: archive_skill_name = _normalize_skill_name(skill_name_hint) if archive_skill_name and not _SKILL_NAME_RE.fullmatch( - archive_skill_name + archive_skill_name, ): raise ValueError("Invalid skill name.") @@ -614,7 +614,7 @@ class SkillManager: candidate_name = _normalize_skill_name(src_dir_name) if not candidate_name or not _SKILL_NAME_RE.fullmatch( - candidate_name + candidate_name, ): continue @@ -631,7 +631,7 @@ class SkillManager: raise FileExistsError( "One or more skills from the archive already exist and " "overwrite=False. No skills were installed. Conflicting " - f"paths: {', '.join(conflict_dirs)}" + f"paths: {', '.join(conflict_dirs)}", ) with tempfile.TemporaryDirectory(dir=str(astrbot_paths.temp)) as tmp_dir: @@ -643,7 +643,7 @@ class SkillManager: if root_mode: archive_hint = _normalize_skill_name( - archive_skill_name or zip_path_obj.stem + archive_skill_name or zip_path_obj.stem, ) if not archive_hint or not _SKILL_NAME_RE.fullmatch(archive_hint): raise ValueError("Invalid skill name.") @@ -653,7 +653,7 @@ class SkillManager: normalized_path = _normalize_skill_markdown_path(src_dir) if normalized_path is None: raise ValueError( - "SKILL.md not found in the root of the zip archive." + "SKILL.md not found in the root of the zip archive.", ) dest_dir = Path(self.skills_root) / skill_name @@ -673,7 +673,7 @@ class SkillManager: for archive_root_name in top_dirs: archive_root_name_normalized = _normalize_skill_name( - archive_root_name + archive_root_name, ) if ( @@ -701,7 +701,7 @@ class SkillManager: if dest_dir.exists(): if not overwrite: raise FileExistsError( - f"Skill {skill_name} already exists." + f"Skill {skill_name} already exists.", ) shutil.rmtree(dest_dir) @@ -711,7 +711,7 @@ class SkillManager: if not installed_skills: raise ValueError( - "No valid SKILL.md found in any folder of the zip archive." + "No valid SKILL.md found in any folder of the zip archive.", ) return ", ".join(installed_skills) diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index 56c15cfcb..46bf57929 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -264,7 +264,7 @@ def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]: descriptors.append(desc) except Exception as e: logger.warning( - f"解析指令处理函数 {handler.handler_full_name} 失败,跳过该指令。原因: {e!s}" + f"解析指令处理函数 {handler.handler_full_name} 失败,跳过该指令。原因: {e!s}", ) continue return descriptors @@ -286,18 +286,18 @@ def _build_descriptor(handler: StarHandlerMetadata) -> CommandDescriptor | None: if isinstance(filter_ref, CommandFilter): raw_fragment = getattr( - filter_ref, "_original_command_name", filter_ref.command_name + filter_ref, "_original_command_name", filter_ref.command_name, ) current_fragment = filter_ref.command_name parent_signature = (filter_ref.parent_command_names or [""])[0].strip() # 如果是子指令,尝试找到父指令组的 handler_full_name if is_sub_command and parent_signature: parent_group_handler = _find_parent_group_handler( - handler.handler_module_path, parent_signature + handler.handler_module_path, parent_signature, ) else: raw_fragment = getattr( - filter_ref, "_original_group_name", filter_ref.group_name + filter_ref, "_original_group_name", filter_ref.group_name, ) current_fragment = filter_ref.group_name parent_signature = _resolve_group_parent_signature(filter_ref) diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index 4cb71d19b..b8b877d18 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -147,6 +147,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ prov = await self.provider_manager.get_provider_by_id(chat_provider_id) if not prov or not isinstance(prov, Provider): @@ -205,6 +206,7 @@ class Context: Raises: ChatProviderNotFoundError: If the specified chat provider ID is not found Exception: For other errors during LLM generation + """ from astrbot.core.agent.tool_session_manager import ToolSessionManager from astrbot.core.astr_agent_context import ( @@ -266,6 +268,7 @@ class Context: Raises: ProviderNotFoundError: 未找到。 + """ prov = self.get_using_provider(umo) if not prov: @@ -298,6 +301,7 @@ class Context: Note: 注册的工具默认是激活状态。 + """ return self.provider_manager.llm_tools.activate_llm_tool(name) @@ -309,11 +313,12 @@ class Context: Returns: 如果成功停用返回 True,如果没找到工具返回 False。 + """ 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 ): @@ -327,11 +332,12 @@ class Context: Note: 如果提供者 ID 存在但未找到提供者,会记录警告日志。 + """ prov = self.provider_manager.inst_map.get(provider_id) if provider_id and (not prov): logger.warning( - f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。" + f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。", ) return prov @@ -363,9 +369,10 @@ class Context: Raises: ValueError: 该会话来源配置的的对话模型(提供商)的类型不正确。 + """ 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 @@ -384,9 +391,10 @@ class Context: Raises: ValueError: 返回的提供者不是 TTSProvider 类型。 + """ 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 类型") @@ -403,9 +411,10 @@ class Context: Raises: ValueError: 返回的提供者不是 STTProvider 类型。 + """ 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 类型") @@ -422,13 +431,14 @@ class Context: Note: 如果不提供 umo 参数,将返回默认配置。 + """ if not umo: return self._config 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) 主动发送消息。 @@ -445,6 +455,7 @@ class Context: Note: 当 session 为字符串时,会尝试解析为 MessageSession 对象。(类名为MessageSesion是因为历史遗留拼写错误) qq_official(QQ 官方 API 平台) 不支持此方法。 + """ if isinstance(session, str): try: @@ -456,7 +467,7 @@ class Context: await platform.send_by_session(session, message_chain) return True logger.warning( - f"cannot find platform for session {session!s}, message not sent" + f"cannot find platform for session {session!s}, message not sent", ) return False @@ -468,6 +479,7 @@ class Context: Note: 如果工具已存在,会替换已存在的工具。 + """ tool_name = {tool.name for tool in self.provider_manager.llm_tools.func_list} module_path = "" @@ -487,7 +499,7 @@ class Context: else: tool.handler_module_path = module_path logger.info( - f"plugin(module_path {module_path}) added LLM tool: {tool.name}" + f"plugin(module_path {module_path}) added LLM tool: {tool.name}", ) if tool.name in tool_name: logger.warning("替换已存在的 LLM 工具: " + tool.name) @@ -495,7 +507,7 @@ 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。 @@ -507,6 +519,7 @@ class Context: Note: 如果相同路由和方法已注册,会替换现有的 API。 + """ if self.registered_web_apis is None: self.registered_web_apis = [] @@ -534,6 +547,7 @@ class Context: Note: 该方法已经过时,请使用 get_platform_inst 方法。(>= AstrBot v4.0.0) + """ for platform in self.platform_manager.platform_insts: name = platform.meta().name @@ -558,6 +572,7 @@ class Context: Note: 可以通过 event.get_platform_id() 获取平台 ID。 + """ for platform in self.platform_manager.platform_insts: if platform.meta().id == platform_id: @@ -569,6 +584,7 @@ class Context: Returns: 数据库实例。 + """ return self._db @@ -577,6 +593,7 @@ class Context: Args: provider: 提供者实例。 + """ self.provider_manager.provider_insts.append(provider) @@ -599,6 +616,7 @@ class Context: Note: 异步处理函数会接收到额外的关键词参数:event: AstrMessageEvent, context: Context。 该方法已弃用,请使用新的注册方式。 + """ md = StarHandlerMetadata( event_type=EventType.OnLLMRequestEvent, @@ -623,6 +641,7 @@ class Context: Note: 如果再要启用,需要重新注册。 该方法已弃用。 + """ self.provider_manager.llm_tools.remove_func(name) @@ -649,6 +668,7 @@ class Context: Note: 推荐使用装饰器注册指令。该方法将在未来的版本中被移除。 + """ md = StarHandlerMetadata( event_type=EventType.AdapterMessageEvent, @@ -665,7 +685,7 @@ class Context: md.event_filters.append(RegexFilter(regex=command_name)) else: md.event_filters.append( - CommandFilter(command_name=command_name, handler_md=md) + CommandFilter(command_name=command_name, handler_md=md), ) star_handlers_registry.append(md) @@ -678,6 +698,7 @@ class Context: Note: 该方法已弃用。 + """ if self._register_tasks is None: self._register_tasks = [] diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py index a73222f3d..328749bf7 100644 --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -97,7 +97,7 @@ class CommandGroupFilter(HandlerFilter): prefix + "| ", event=event, cfg=cfg, - ) + ), ) return "".join(parts) diff --git a/astrbot/core/star/filter/permission.py b/astrbot/core/star/filter/permission.py index deddfa4f0..f60c93ac9 100644 --- a/astrbot/core/star/filter/permission.py +++ b/astrbot/core/star/filter/permission.py @@ -15,7 +15,7 @@ class PermissionType(enum.Flag): class PermissionTypeFilter(HandlerFilter): def __init__( - self, permission_type: PermissionType, raise_error: bool = True + self, permission_type: PermissionType, raise_error: bool = True, ) -> None: self.permission_type = permission_type self.raise_error = raise_error diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 67402fd35..975b05b78 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -415,7 +415,7 @@ def register_on_waiting_llm_request(**kwargs): def decorator(awaitable): _ = get_handler_or_create( - awaitable, EventType.OnWaitingLLMRequestEvent, **kwargs + awaitable, EventType.OnWaitingLLMRequestEvent, **kwargs, ) return awaitable diff --git a/astrbot/core/star/session_llm_manager.py b/astrbot/core/star/session_llm_manager.py index c3c664862..90c4c301d 100644 --- a/astrbot/core/star/session_llm_manager.py +++ b/astrbot/core/star/session_llm_manager.py @@ -49,7 +49,7 @@ class SessionServiceManager: scope_id=session_id, key="session_service_config", default={}, - ) + ), ) llm_enabled = session_services.get("llm_enabled") if llm_enabled is not None: @@ -71,7 +71,7 @@ class SessionServiceManager: scope_id=session_id, key="session_service_config", default={}, - ) + ), ) session_config["llm_enabled"] = enabled await sp.put_async( @@ -112,7 +112,7 @@ class SessionServiceManager: scope_id=session_id, key="session_service_config", default={}, - ) + ), ) tts_enabled = session_services.get("tts_enabled") if tts_enabled is not None: @@ -134,7 +134,7 @@ class SessionServiceManager: scope_id=session_id, key="session_service_config", default={}, - ) + ), ) session_config["tts_enabled"] = enabled await sp.put_async( @@ -144,7 +144,7 @@ class SessionServiceManager: value=session_config, ) logger.info( - f"会话 {session_id} 的TTS状态已更新为: {('启用' if enabled else '禁用')}" + f"会话 {session_id} 的TTS状态已更新为: {('启用' if enabled else '禁用')}", ) @staticmethod @@ -178,7 +178,7 @@ class SessionServiceManager: scope_id=session_id, key="session_service_config", default={}, - ) + ), ) session_enabled = session_services.get("session_enabled") if session_enabled is not None: diff --git a/astrbot/core/star/session_plugin_manager.py b/astrbot/core/star/session_plugin_manager.py index eea19bb8c..2a95d4a98 100644 --- a/astrbot/core/star/session_plugin_manager.py +++ b/astrbot/core/star/session_plugin_manager.py @@ -55,7 +55,7 @@ class SessionPluginManager: scope_id=session_id, key="session_plugin_config", default={}, - ) + ), ) session_config = session_plugin_config.get(session_id, {}) enabled_plugins = session_config.get("enabled_plugins", []) @@ -68,7 +68,7 @@ class SessionPluginManager: @staticmethod async def filter_handlers_by_session( - event: AstrMessageEvent, handlers: list + event: AstrMessageEvent, handlers: list, ) -> list: """根据会话配置过滤处理器列表 @@ -90,7 +90,7 @@ class SessionPluginManager: scope_id=session_id, key="session_plugin_config", default={}, - ) + ), ) session_config = session_plugin_config.get(session_id, {}) disabled_plugins = session_config.get("disabled_plugins", []) @@ -106,7 +106,7 @@ class SessionPluginManager: continue if plugin.name in disabled_plugins: logger.debug( - f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}" + f"插件 {plugin.name} 在会话 {session_id} 中被禁用,跳过处理器 {handler.handler_name}", ) else: filtered_handlers.append(handler) diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 4d4705dc4..b2fabe961 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -60,7 +60,7 @@ 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,12 +108,12 @@ 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: logger.info( - f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): {requirements_path}" + f"正在安装插件 {plugin_label} 的依赖库(缺失依赖预检查不可裁剪,回退到完整安装): {requirements_path}", ) await pip_installer.install(requirements_path=requirements_path) return @@ -131,10 +131,10 @@ async def _install_requirements_with_precheck( await pip_installer.install(requirements_path=requirements_path) return logger.info( - f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: {requirements_path} -> {sorted(install_plan.missing_names)}" + f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: {requirements_path} -> {sorted(install_plan.missing_names)}", ) async with _temporary_filtered_requirements_file( - install_lines=install_plan.install_lines + install_lines=install_plan.install_lines, ) as filtered_requirements_path: await pip_installer.install(requirements_path=filtered_requirements_path) @@ -154,7 +154,7 @@ class PluginManager: self.plugin_config_path = get_astrbot_config_path() "存储插件配置的路径。data/config" self.reserved_plugin_path = os.path.join( - get_astrbot_path(), "astrbot", "builtin_stars" + get_astrbot_path(), "astrbot", "builtin_stars", ) "保留插件的路径。在 astrbot/builtin_stars 目录下" self.conf_schema_fname = "_conf_schema.json" @@ -197,11 +197,11 @@ 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() @@ -243,14 +243,14 @@ class PluginManager: logger.info(f"插件 {d} 未找到 main.py 或者 {d}.py,跳过。") continue if os.path.exists(os.path.join(path, d, "main.py")) or os.path.exists( - os.path.join(path, d, d + ".py") + os.path.join(path, d, d + ".py"), ): modules.append( { "pname": d, "module": module_str, "module_path": os.path.join(path, d, module_str), - } + }, ) return modules @@ -266,7 +266,7 @@ class PluginManager: return plugins async def _check_plugin_dept_update( - self, target_plugin: str | None = None + self, target_plugin: str | None = None, ) -> bool | None: """检查插件的依赖 如果 target_plugin 为 None,则检查所有插件的依赖 @@ -286,14 +286,14 @@ 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 +302,13 @@ 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]) @@ -316,19 +316,19 @@ class PluginManager: if await anyio.Path(requirements_path).exists(): try: logger.info( - f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}" + f"插件 {root_dir_name} 导入失败,尝试从已安装依赖恢复: {import_exc!s}", ) pip_installer.prefer_installed_dependencies( - requirements_path=requirements_path + requirements_path=requirements_path, ) module = __import__(path, fromlist=[module_str]) logger.info( - f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。" + f"插件 {root_dir_name} 已从 site-packages 恢复依赖,跳过重新安装。", ) return module except Exception as recover_exc: logger.info( - f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}" + f"插件 {root_dir_name} 已安装依赖恢复失败,将重新安装依赖: {recover_exc!s}", ) await self._check_plugin_dept_update(target_plugin=root_dir_name) return __import__(path, fromlist=[module_str]) @@ -344,7 +344,7 @@ 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"): @@ -359,7 +359,7 @@ class PluginManager: or ("author" not in metadata) ): raise Exception( - "插件元数据信息不完整。name, desc, version, author 是必须的字段。" + "插件元数据信息不完整。name, desc, version, author 是必须的字段。", ) metadata = StarMetadata( name=metadata["name"], @@ -389,11 +389,11 @@ class PluginManager: def _validate_importable_name(plugin_name: str) -> None: if "/" in plugin_name or "\\" in plugin_name: raise ValueError( - "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。" + "metadata.yaml 中 name 含有路径分隔符,不可用于 importlib 加载。", ) if not plugin_name.isidentifier() or keyword.iskeyword(plugin_name): raise Exception( - "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。" + "metadata.yaml 中 name 不是合法的模块名称(应为合法 Python 标识符且非关键字)。", ) @staticmethod @@ -446,7 +446,7 @@ 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 +491,7 @@ 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] @@ -547,7 +547,7 @@ class PluginManager: "display_name": metadata.display_name, "support_platforms": metadata.support_platforms, "astrbot_version": metadata.astrbot_version, - } + }, ) except Exception as metadata_error: logger.debug(f"读取失败插件 {root_dir_name} 元数据失败: {metadata_error!s}") @@ -565,11 +565,11 @@ class PluginManager: version = info.get("version") or info.get("astrbot_version") if version: lines.append( - f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。" + f"加载插件「{display_name}」(目录: {dir_name}, 版本: {version}) 时出现问题,原因:{error}。", ) else: lines.append( - f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。" + f"加载插件「{display_name}」(目录: {dir_name}) 时出现问题,原因:{error}。", ) else: error = str(info) @@ -577,8 +577,7 @@ class PluginManager: self.failed_plugin_info = "\n".join(lines) + "\n" async def reload_failed_plugin(self, dir_name): - """ - 重新加载未注册(加载失败)的插件 + """重新加载未注册(加载失败)的插件 Args: dir_name (str): 要重载的特定插件名称。 Returns: @@ -597,8 +596,7 @@ class PluginManager: self.failed_plugin_dict.pop(dir_name, None) self._rebuild_failed_plugin_info() return (success, None) - else: - return (False, error) + return (False, error) async def reload(self, specified_plugin_name=None): """重新加载插件 @@ -627,7 +625,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。" + f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", ) if smd.name and smd.module_path: await self._unbind_plugin(smd.name, smd.module_path) @@ -642,7 +640,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。" + f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", ) if smd.name: await self._unbind_plugin(smd.name, specified_module_path) @@ -658,7 +656,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。" + f"插件 {smd.name} 未被正常终止: {e!s}, 可能会导致该插件运行不正常。", ) if smd.name and smd.module_path: await self._unbind_plugin(smd.name, smd.module_path) @@ -743,15 +741,15 @@ 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()), ) @@ -760,7 +758,7 @@ class PluginManager: metadata = star_map[path] try: metadata_yaml = self._load_plugin_metadata( - plugin_path=plugin_dir_path + plugin_path=plugin_dir_path, ) if metadata_yaml: metadata.name = metadata_yaml.name @@ -773,18 +771,18 @@ class PluginManager: metadata.astrbot_version = metadata_yaml.astrbot_version except Exception as e: logger.warning( - f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。" + f"插件 {root_dir_name} 元数据载入失败: {e!s}。使用默认元数据。", ) if not ignore_version_check: is_valid, error_message = ( self._validate_astrbot_version_specifier( - metadata.astrbot_version + metadata.astrbot_version, ) ) if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) logger.info(metadata) metadata.config = plugin_config @@ -799,15 +797,15 @@ 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( - context=self.context + context=self.context, ) elif metadata.star_cls_type: metadata.star_cls = metadata.star_cls_type( - context=self.context + context=self.context, ) if metadata.star_cls: metadata.star_cls.name = p_name @@ -826,12 +824,12 @@ class PluginManager: ) related_handlers = ( star_handlers_registry.get_handlers_by_module_name( - metadata.module_path + metadata.module_path, ) ) 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): @@ -851,40 +849,40 @@ 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 else: logger.debug( - f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。" + f"插件 {path} 未通过装饰器注册。尝试通过旧版本方式载入。", ) classes = self._get_classes(module) if path not in inactivated_plugins: 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} 的元数据。") if not ignore_version_check: is_valid, error_message = ( self._validate_astrbot_version_specifier( - metadata.astrbot_version + metadata.astrbot_version, ) ) if not is_valid: raise PluginVersionIncompatibleError( error_message - or "The plugin is not compatible with the current AstrBot version." + or "The plugin is not compatible with the current AstrBot version.", ) metadata.star_cls = obj metadata.config = plugin_config @@ -902,7 +900,7 @@ class PluginManager: assert metadata.module_path, f"插件 {metadata.name} 模块路径为空" full_names = [] for handler in star_handlers_registry.get_handlers_by_module_name( - metadata.module_path + metadata.module_path, ): full_names.append(handler.handler_full_name) if ( @@ -910,7 +908,7 @@ 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: @@ -926,22 +924,22 @@ class PluginManager: PermissionTypeFilter( PermissionType.ADMIN if cmd_type == "admin" - else PermissionType.MEMBER - ) + else PermissionType.MEMBER, + ), ) logger.debug( - f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。" + f"插入权限过滤器 {cmd_type} 到 {metadata.name} 的 {handler.handler_name} 方法。", ) metadata.star_handler_full_names = full_names if hasattr(metadata.star_cls, "initialize") and metadata.star_cls: await metadata.star_cls.initialize() handlers = star_handlers_registry.get_handlers_by_event_type( - EventType.OnPluginLoadedEvent + EventType.OnPluginLoadedEvent, ) for handler in handlers: try: logger.info( - f"hook(on_plugin_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}" + f"hook(on_plugin_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", ) await handler.handler(metadata) except Exception: @@ -993,7 +991,7 @@ 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(): @@ -1016,7 +1014,7 @@ 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: @@ -1024,7 +1022,7 @@ class PluginManager: logger.warning(f"已清理安装失败插件配置: {plugin_config_path}") except Exception as e: logger.warning( - f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}" + f"清理安装失败插件配置失败: {plugin_config_path},原因: {e!s}", ) def _cleanup_plugin_optional_artifacts( @@ -1037,7 +1035,7 @@ 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: @@ -1049,21 +1047,21 @@ 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: remove_dir(plugin_data_dir) logger.info( - f"已删除插件 {plugin_label} 的持久化数据 ({data_dir_name})" + f"已删除插件 {plugin_label} 的持久化数据 ({data_dir_name})", ) except Exception as e: logger.warning( - f"删除插件持久化数据失败 ({data_dir_name}, {plugin_label}): {e!s}" + f"删除插件持久化数据失败 ({data_dir_name}, {plugin_label}): {e!s}", ) 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 @@ -1085,7 +1083,7 @@ class PluginManager: self._rebuild_failed_plugin_info() async def install_plugin( - self, repo_url: str, proxy: str = "", ignore_version_check: bool = False + self, repo_url: str, proxy: str = "", ignore_version_check: bool = False, ): """从仓库 URL 安装插件 @@ -1103,7 +1101,7 @@ class PluginManager: """ _task_install_star = asyncio.create_task( - Metric.upload(et="install_star", repo=repo_url) + Metric.upload(et="install_star", repo=repo_url), ) self.tasks.add(_task_install_star) async with self._pm_lock: @@ -1115,13 +1113,13 @@ class PluginManager: plugin_path = os.path.join(self.plugin_store_path, repo_name) if await anyio.Path(plugin_path).exists(): raise Exception( - f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。" + f"安装失败:目录 {os.path.basename(plugin_path)} 已存在。", ) plugin_path = await self.updator.install(repo_url, proxy) 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 @@ -1140,7 +1138,7 @@ class PluginManager: if not success: raise Exception( error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。", ) plugin = self.context.get_registered_star(dir_name) if not plugin: @@ -1155,11 +1153,11 @@ class PluginManager: if await anyio.Path(readme_path).exists(): try: readme_content = await anyio.Path(readme_path).read_text( - encoding="utf-8" + encoding="utf-8", ) except Exception as e: logger.warning( - f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}" + f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}", ) plugin_info = None if plugin: @@ -1171,16 +1169,16 @@ 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( - f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}" + f"安装插件 {dir_name} 失败,插件安装目录:{plugin_path}", ) 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: """卸载指定的插件。 @@ -1206,7 +1204,7 @@ class PluginManager: except Exception as e: logger.warning(traceback.format_exc()) logger.warning( - f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。" + f"插件 {plugin_name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。", ) if plugin.module_path is None or root_dir_name is None: raise Exception(f"插件 {plugin_name} 数据不完整,无法卸载。") @@ -1215,7 +1213,7 @@ class PluginManager: remove_dir(os.path.join(ppath, root_dir_name)) except Exception as e: raise Exception( - f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。" + f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。", ) from e self._cleanup_plugin_optional_artifacts( root_dir_name=root_dir_name, @@ -1225,7 +1223,7 @@ 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: @@ -1242,8 +1240,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: logger.debug( @@ -1282,10 +1280,10 @@ class PluginManager: del star_registry[i] break for handler in star_handlers_registry.get_handlers_by_module_name( - plugin_module_path + plugin_module_path, ): logger.info( - f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})" + f"移除了插件 {plugin_name} 的处理函数 {handler.handler_name} ({len(star_handlers_registry)})", ) star_handlers_registry.remove(handler) for k in [ @@ -1308,14 +1306,14 @@ class PluginManager: module_prefix = ".".join(plugin_module_path.split(".")[:-1]) if module_prefix: unregistered_adapters = unregister_platform_adapters_by_module( - module_prefix + module_prefix, ) for adapter_name in unregistered_adapters: logger.info(f"移除了插件 {plugin_name} 的平台适配器 {adapter_name}") 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: @@ -1348,7 +1346,7 @@ class PluginManager: if plugin.module_path not in inactivated_plugins: inactivated_plugins.append(plugin.module_path) inactivated_llm_tools: list[Any] = list( - set(await sp.global_get("inactivated_llm_tools", []) or []) + set(await sp.global_get("inactivated_llm_tools", []) or []), ) for func_tool in llm_tools.func_list: mp = getattr(func_tool, "handler_module_path", None) @@ -1383,19 +1381,19 @@ 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) elif "terminate" in star_metadata.star_cls_type.__dict__: await star_metadata.star_cls.terminate() handlers = star_handlers_registry.get_handlers_by_event_type( - EventType.OnPluginUnloadedEvent + EventType.OnPluginUnloadedEvent, ) for handler in handlers: try: logger.info( - f"hook(on_plugin_unloaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}" + f"hook(on_plugin_unloaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}", ) await handler.handler(star_metadata) except Exception: @@ -1447,11 +1445,11 @@ class PluginManager: await self.reload(plugin_name) async def install_plugin_from_file( - self, zip_file_path: str, ignore_version_check: bool = False + self, zip_file_path: str, ignore_version_check: bool = False, ): dir_name = os.path.splitext(os.path.basename(zip_file_path))[0] desti_dir = tempfile.mkdtemp( - dir=self.plugin_store_path, prefix="plugin_upload_" + dir=self.plugin_store_path, prefix="plugin_upload_", ) temp_desti_dir = desti_dir try: @@ -1473,11 +1471,11 @@ 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: @@ -1492,7 +1490,7 @@ class PluginManager: if await anyio.Path(readme_path).exists(): try: readme_content = await anyio.Path(readme_path).read_text( - encoding="utf-8" + encoding="utf-8", ) except Exception as e: logger.warning(f"读取插件 {dir_name} 的 README.md 文件失败: {e!s}") @@ -1505,14 +1503,14 @@ class PluginManager: } if plugin.repo: _task_install_star_f = asyncio.create_task( - Metric.upload(et="install_star_f", repo=plugin.repo) + Metric.upload(et="install_star_f", repo=plugin.repo), ) self.tasks.add(_task_install_star_f) _task_install_star_f.add_done_callback(self.tasks.discard) 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 @@ -1525,5 +1523,5 @@ class PluginManager: remove_dir(temp_desti_dir) except Exception as e: logger.warning( - f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}" + f"清理临时插件解压目录失败: {temp_desti_dir},原因: {e!s}", ) diff --git a/astrbot/core/subagent_orchestrator.py b/astrbot/core/subagent_orchestrator.py index 14c9643ef..426f6172d 100644 --- a/astrbot/core/subagent_orchestrator.py +++ b/astrbot/core/subagent_orchestrator.py @@ -21,7 +21,7 @@ class SubAgentOrchestrator: """ def __init__( - self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager + self, tool_mgr: FunctionToolManager, persona_mgr: PersonaManager, ) -> None: self._tool_mgr = tool_mgr self._persona_mgr = persona_mgr @@ -69,7 +69,7 @@ class SubAgentOrchestrator: if prompt: instructions = prompt begin_dialogs = copy.deepcopy( - persona_data.get("_begin_dialogs_processed") + persona_data.get("_begin_dialogs_processed"), ) persona_tools = persona_data.get("tools") if isinstance(persona_tools, list): diff --git a/astrbot/core/tools/cron_tools.py b/astrbot/core/tools/cron_tools.py index 0fa2a8bf4..735dbe624 100644 --- a/astrbot/core/tools/cron_tools.py +++ b/astrbot/core/tools/cron_tools.py @@ -50,11 +50,11 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]): }, }, "required": ["note"], - } + }, ) 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: @@ -116,14 +116,14 @@ class DeleteCronJobTool(FunctionTool[AstrAgentContext]): "job_id": { "type": "string", "description": "The job_id returned when the job was created.", - } + }, }, "required": ["job_id"], - } + }, ) 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: @@ -152,13 +152,13 @@ class ListCronJobsTool(FunctionTool[AstrAgentContext]): "job_type": { "type": "string", "description": "Optional filter: basic or active_agent.", - } + }, }, - } + }, ) 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: @@ -175,7 +175,7 @@ class ListCronJobsTool(FunctionTool[AstrAgentContext]): lines = [] for j in jobs: lines.append( - f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}" + f"{j.job_id} | {j.name} | {j.job_type} | run_once={getattr(j, 'run_once', False)} | enabled={j.enabled} | next={j.next_run_time}", ) return "\n".join(lines) diff --git a/astrbot/core/tools/kb_query.py b/astrbot/core/tools/kb_query.py index 60a9bef65..d3acd97af 100644 --- a/astrbot/core/tools/kb_query.py +++ b/astrbot/core/tools/kb_query.py @@ -38,11 +38,11 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: query = kwargs.get("query", "") if not query: @@ -68,6 +68,7 @@ async def retrieve_knowledge_base( query: The search query string umo: Unique message object (session ID) context: Star context + """ kb_mgr = context.kb_manager config = context.get_config(umo=umo) diff --git a/astrbot/core/tools/knowledge_base_tools.py b/astrbot/core/tools/knowledge_base_tools.py index e27a883d4..a3b7fb837 100644 --- a/astrbot/core/tools/knowledge_base_tools.py +++ b/astrbot/core/tools/knowledge_base_tools.py @@ -103,11 +103,11 @@ class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: query = kwargs.get("query", "") if not query: diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 020c1ad5a..9c4e6ca71 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -71,11 +71,11 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): }, }, "required": ["messages"], - } + }, ) async def _resolve_path_from_sandbox( - self, context: ContextWrapper[AstrAgentContext], path: str + self, context: ContextWrapper[AstrAgentContext], path: str, ) -> tuple[str, bool]: if os.path.exists(path): return path, False @@ -90,7 +90,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if "_&exists_" in json.dumps(result): name = os.path.basename(path) local_path = os.path.join( - get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}" + get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}", ) await sb.download_file(path, local_path) logger.info(f"Downloaded file from sandbox: {path} -> {local_path}") @@ -101,7 +101,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): return path, False async def call( - self, context: ContextWrapper[AstrAgentContext], **kwargs + self, context: ContextWrapper[AstrAgentContext], **kwargs, ) -> ToolExecResult: session = kwargs.get("session") or context.context.event.unified_msg_origin messages = kwargs.get("messages") @@ -128,7 +128,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Image.fromFileSystem(path=local_path)) elif url: @@ -140,7 +140,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Record.fromFileSystem(path=local_path)) elif url: @@ -152,7 +152,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): url = msg.get("url") if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.Video.fromFileSystem(path=local_path)) elif url: @@ -170,7 +170,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): ) if path: local_path, _ = await self._resolve_path_from_sandbox( - context, path + context, path, ) components.append(Comp.File(name=name, file=local_path)) elif url: diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py index f0eb54898..a4ecc5641 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -50,17 +50,16 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): "type": "object", "additionalProperties": {"type": "string"}, }, - } + }, }, "required": ["messages"], - } + }, ) 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. + """If the path exists locally, return it directly. Otherwise, check if it exists in the sandbox and download it. bool: indicates whether the file was downloaded from sandbox. @@ -69,17 +68,17 @@ 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 result = await sb.shell.exec( - f"test -f {shlex.quote(path)} && echo '_&exists_'" + f"test -f {shlex.quote(path)} && echo '_&exists_'", ) 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}") @@ -89,7 +88,7 @@ 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 @@ -190,7 +189,7 @@ 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 5ca8c3e08..ee94f4b63 100644 --- a/astrbot/core/tools/web_search_tools.py +++ b/astrbot/core/tools/web_search_tools.py @@ -41,7 +41,7 @@ class _KeyRotator: keys = provider_settings.get(self.setting_name, []) if not keys: raise ValueError( - f"Error: {self.provider_name} API key is not configured in AstrBot." + f"Error: {self.provider_name} API key is not configured in AstrBot.", ) async with self.lock: @@ -62,7 +62,7 @@ def normalize_legacy_web_search_config(cfg) -> None: changed = False if provider_settings.get( - "websearch_provider" + "websearch_provider", ) == "default" and provider_settings.get("web_search", False): provider_settings["web_search"] = False changed = True @@ -109,7 +109,7 @@ def _search_result_payload(results: list[SearchResult]) -> str: "url": f"{result.url}", "snippet": f"{result.snippet}", "index": index, - } + }, ) _cache_favicon(result.url, result.favicon) return json.dumps({"results": ret_ls}, ensure_ascii=False) @@ -124,27 +124,26 @@ async def _tavily_search( "Authorization": f"Bearer {tavily_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.tavily.com/search", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - return [ - SearchResult( - title=item.get("title"), - url=item.get("url"), - snippet=item.get("content"), - favicon=item.get("favicon"), - ) - for item in data.get("results", []) - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.tavily.com/search", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Tavily web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + return [ + SearchResult( + title=item.get("title"), + url=item.get("url"), + snippet=item.get("content"), + favicon=item.get("favicon"), + ) + for item in data.get("results", []) + ] async def _tavily_extract(provider_settings: dict, payload: dict) -> list[dict]: @@ -153,24 +152,23 @@ async def _tavily_extract(provider_settings: dict, payload: dict) -> list[dict]: "Authorization": f"Bearer {tavily_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.tavily.com/extract", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Tavily web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - results: list[dict] = data.get("results", []) - if not results: - raise ValueError( - "Error: Tavily web searcher does not return any results." - ) - return results + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.tavily.com/extract", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Tavily web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + results: list[dict] = data.get("results", []) + if not results: + raise ValueError( + "Error: Tavily web searcher does not return any results.", + ) + return results async def _bocha_search( @@ -182,28 +180,27 @@ async def _bocha_search( "Authorization": f"Bearer {bocha_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://api.bochaai.com/v1/web-search", - json=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"BoCha web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - rows = data["data"]["webPages"]["value"] - return [ - SearchResult( - title=item.get("name"), - url=item.get("url"), - snippet=item.get("snippet"), - favicon=item.get("siteIcon"), - ) - for item in rows - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://api.bochaai.com/v1/web-search", + json=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"BoCha web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data["data"]["webPages"]["value"] + return [ + SearchResult( + title=item.get("name"), + url=item.get("url"), + snippet=item.get("snippet"), + favicon=item.get("siteIcon"), + ) + for item in rows + ] async def _brave_search( @@ -215,27 +212,26 @@ async def _brave_search( "Accept": "application/json", "X-Subscription-Token": brave_key, } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.get( - "https://api.search.brave.com/res/v1/web/search", - params=payload, - headers=header, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Brave web search failed: {reason}, status: {response.status}", - ) - data = await response.json() - rows = data.get("web", {}).get("results", []) - return [ - SearchResult( - title=item.get("title", ""), - url=item.get("url", ""), - snippet=item.get("description", ""), - ) - for item in rows - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.get( + "https://api.search.brave.com/res/v1/web/search", + params=payload, + headers=header, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Brave web search failed: {reason}, status: {response.status}", + ) + data = await response.json() + rows = data.get("web", {}).get("results", []) + return [ + SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("description", ""), + ) + for item in rows + ] async def _baidu_search( @@ -251,29 +247,28 @@ async def _baidu_search( "X-Appbuilder-Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } - async with aiohttp.ClientSession(trust_env=True) as session: - async with session.post( - "https://qianfan.baidubce.com/v2/ai_search/web_search", - json=payload, - headers=headers, - ) as response: - if response.status != 200: - reason = await response.text() - raise Exception( - f"Baidu AI Search failed: {reason}, status: {response.status}", - ) - data = await response.json() - references = data.get("references", []) - return [ - SearchResult( - title=item.get("title", ""), - url=item.get("url", ""), - snippet=item.get("content", ""), - favicon=item.get("icon"), - ) - for item in references - if item.get("url") - ] + async with aiohttp.ClientSession(trust_env=True) as session, session.post( + "https://qianfan.baidubce.com/v2/ai_search/web_search", + json=payload, + headers=headers, + ) as response: + if response.status != 200: + reason = await response.text() + raise Exception( + f"Baidu AI Search failed: {reason}, status: {response.status}", + ) + data = await response.json() + references = data.get("references", []) + return [ + SearchResult( + title=item.get("title", ""), + url=item.get("url", ""), + snippet=item.get("content", ""), + favicon=item.get("icon"), + ) + for item in references + if item.get("url") + ] @builtin_tool @@ -319,7 +314,7 @@ class TavilyWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -378,7 +373,7 @@ class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]): }, }, "required": ["url"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -444,7 +439,7 @@ class BochaWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -498,7 +493,7 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -507,10 +502,8 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]): return "Error: Brave API key is not configured in AstrBot." count = int(kwargs.get("count", 10)) - if count < 1: - count = 1 - if count > 20: - count = 20 + count = max(count, 1) + count = min(count, 20) payload = { "q": kwargs["query"], @@ -555,7 +548,7 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): }, }, "required": ["query"], - } + }, ) async def call(self, context, **kwargs) -> ToolExecResult: @@ -564,10 +557,8 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): return "Error: Baidu AI Search API key is not configured in AstrBot." top_k = int(kwargs.get("top_k", 10)) - if top_k < 1: - top_k = 1 - if top_k > 50: - top_k = 50 + top_k = max(top_k, 1) + top_k = min(top_k, 50) payload = { "messages": [{"role": "user", "content": str(kwargs["query"])[:72]}], @@ -592,11 +583,11 @@ class BaiduWebSearchTool(FunctionTool[AstrAgentContext]): __all__ = [ + "WEB_SEARCH_TOOL_NAMES", "BaiduWebSearchTool", "BochaWebSearchTool", "BraveWebSearchTool", "TavilyExtractWebPageTool", "TavilyWebSearchTool", - "WEB_SEARCH_TOOL_NAMES", "normalize_legacy_web_search_config", ] diff --git a/astrbot/core/umop_config_router.py b/astrbot/core/umop_config_router.py index 97a9d6eaf..3315a6791 100644 --- a/astrbot/core/umop_config_router.py +++ b/astrbot/core/umop_config_router.py @@ -111,8 +111,8 @@ class UmopConfigRouter: Raises: ValueError: 当 umo 格式不正确时抛出 - """ + """ if self._split_umo(umo) is None: raise ValueError( "umop must be a string in the format [platform_id]:[message_type]:[session_id], with optional wildcards * or empty for all", diff --git a/astrbot/core/updator.py b/astrbot/core/updator.py index dba0b1d12..55d8b1ec7 100644 --- a/astrbot/core/updator.py +++ b/astrbot/core/updator.py @@ -145,14 +145,14 @@ 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 if os.environ.get("ASTRBOT_CLI") or os.environ.get("ASTRBOT_LAUNCHER"): raise Exception( - "Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot." + "Error: You are running AstrBot via CLI, please use `pip` or `uv tool upgrade` to update AstrBot.", ) # 避免版本管理混乱 if latest: diff --git a/astrbot/core/utils/active_event_registry.py b/astrbot/core/utils/active_event_registry.py index 59e61f51a..ad4531aea 100644 --- a/astrbot/core/utils/active_event_registry.py +++ b/astrbot/core/utils/active_event_registry.py @@ -38,6 +38,7 @@ class ActiveEventRegistry: Returns: 被终止的事件数量。 + """ count = 0 for event in list(self._events.get(umo, [])): diff --git a/astrbot/core/utils/astrbot_path.py b/astrbot/core/utils/astrbot_path.py index a0931191e..32e4e0468 100644 --- a/astrbot/core/utils/astrbot_path.py +++ b/astrbot/core/utils/astrbot_path.py @@ -63,7 +63,6 @@ class AstrbotPaths: @property def is_root(self) -> bool: """Check if the path is an AstrBot root directory""" - if not self.root.exists() or not self.root.is_dir(): return False if not (self.root / ".astrbot").exists(): diff --git a/astrbot/core/utils/auth_password.py b/astrbot/core/utils/auth_password.py index 12721fe3a..4f9f4f718 100644 --- a/astrbot/core/utils/auth_password.py +++ b/astrbot/core/utils/auth_password.py @@ -50,7 +50,7 @@ def validate_dashboard_password(raw_password: str) -> None: raise ValueError("Password cannot be empty") if len(raw_password) < _DASHBOARD_PASSWORD_MIN_LENGTH: raise ValueError( - f"Password must be at least {_DASHBOARD_PASSWORD_MIN_LENGTH} characters long" + f"Password must be at least {_DASHBOARD_PASSWORD_MIN_LENGTH} characters long", ) if not re.search(r"[A-Z]", raw_password): @@ -107,7 +107,7 @@ 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 +160,7 @@ 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(), @@ -190,10 +190,10 @@ def verify_dashboard_password(stored_hash: str, candidate_password: str) -> bool c in "0123456789abcdefABCDEF" for c in stored_hash ): candidate_sha256 = hashlib.sha256( - candidate_password.encode("utf-8") + 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 e9ad346f0..f829a9067 100644 --- a/astrbot/core/utils/core_constraints.py +++ b/astrbot/core/utils/core_constraints.py @@ -101,7 +101,8 @@ class CoreConstraintsProvider: @contextlib.contextmanager def constraints_file(self) -> Iterator[str | None]: """Synchronous context manager kept for backward compatibility with tests and - synchronous callers. Creates a temporary constraints file and yields its path.""" + synchronous callers. Creates a temporary constraints file and yields its path. + """ constraints = _get_core_constraints(self._core_dist_name) if not constraints: yield None @@ -112,7 +113,7 @@ class CoreConstraintsProvider: import tempfile with tempfile.NamedTemporaryFile( - mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8" + mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8", ) as f: f.write("\n".join(constraints)) path = f.name @@ -148,7 +149,7 @@ 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/error_redaction.py b/astrbot/core/utils/error_redaction.py index dcab07ac5..46b977787 100644 --- a/astrbot/core/utils/error_redaction.py +++ b/astrbot/core/utils/error_redaction.py @@ -5,19 +5,19 @@ _SECRET_KEYS = ( ) _JSON_FIELD_PATTERN = re.compile( - rf"(?i)(?P(?P['\"]){_SECRET_KEYS}(?P=kq)\s*:\s*)(?P['\"])(?P[^'\"]+)(?P=vq)" + rf"(?i)(?P(?P['\"]){_SECRET_KEYS}(?P=kq)\s*:\s*)(?P['\"])(?P[^'\"]+)(?P=vq)", ) _AUTH_JSON_FIELD_PATTERN = re.compile( - r"(?i)(?P(?P['\"])authorization(?P=kq)\s*:\s*)(?P['\"])bearer\s+[^'\"]+(?P=vq)" + r"(?i)(?P(?P['\"])authorization(?P=kq)\s*:\s*)(?P['\"])bearer\s+[^'\"]+(?P=vq)", ) _QUERY_FIELD_PATTERN = re.compile( - rf"(?i)(?P{_SECRET_KEYS}\s*=\s*)(?P[^&'\" ]+)" + rf"(?i)(?P{_SECRET_KEYS}\s*=\s*)(?P[^&'\" ]+)", ) _QUERY_PARAM_PATTERN = re.compile( - r"(?i)(?P[?&](?:api_?key|key|access_?token|auth_?token)=)(?P[^&'\" ]+)" + r"(?i)(?P[?&](?:api_?key|key|access_?token|auth_?token)=)(?P[^&'\" ]+)", ) _AUTH_HEADER_PATTERN = re.compile( - r"(?i)(?P\bauthorization\s*:\s*bearer\s+)(?P[A-Za-z0-9._\-]+)" + r"(?i)(?P\bauthorization\s*:\s*bearer\s+)(?P[A-Za-z0-9._\-]+)", ) _BEARER_PATTERN = re.compile(r"(?i)(?P\bbearer\s+)(?P[A-Za-z0-9._\-]+)") _SK_PATTERN = re.compile(r"\bsk-[A-Za-z0-9]{16,}\b") diff --git a/astrbot/core/utils/file_extract.py b/astrbot/core/utils/file_extract.py index 02cb7e6de..dd78cd56c 100644 --- a/astrbot/core/utils/file_extract.py +++ b/astrbot/core/utils/file_extract.py @@ -34,7 +34,7 @@ async def extract_file_moonshotai(file_path: str, api_key: str) -> str: source_path.name, source_bytes, "application/octet-stream", - ) + ), }, ) upload_response.raise_for_status() diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index cafebc4d4..8bea4793d 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -109,7 +109,7 @@ async def download_image_by_url( f"SSL certificate verification failed for {url}. " "Disabling SSL verification (CERT_NONE) as a fallback. " "This is insecure and exposes the application to man-in-the-middle attacks. " - "Please investigate and resolve certificate issues." + "Please investigate and resolve certificate issues.", ) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False @@ -146,7 +146,7 @@ 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}") @@ -155,7 +155,7 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non start_time = time.time() if show_progress: logger.info( - f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}" + f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}", ) async with await anyio.open_file(path, "wb") as f: while True: @@ -172,18 +172,18 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non ) speed = downloaded_size / 1024 / elapsed_time # KB/s logger.info( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s" + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", ) except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证(仅在证书验证失败时作为fallback) logger.warning( - "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。" + "SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。", ) logger.warning( f"SSL certificate verification failed for {url}. " "Falling back to unverified connection (CERT_NONE). " "This is insecure and exposes the application to man-in-the-middle attacks. " - "Please investigate certificate issues with the remote server." + "Please investigate certificate issues with the remote server.", ) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False @@ -199,7 +199,7 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non start_time = time.time() if show_progress: logger.info( - f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}" + f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}", ) async with await anyio.open_file(path, "wb") as f: while True: @@ -212,7 +212,7 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non elapsed_time = time.time() - start_time speed = downloaded_size / 1024 / elapsed_time # KB/s logger.info( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s" + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", ) if show_progress: logger.info("下载完成") @@ -358,7 +358,7 @@ async def download_dashboard( except Exception as e: if not latest: logger.warning( - f"下载指定版本({version})失败: {e},尝试下载最新版本。" + f"下载指定版本({version})失败: {e},尝试下载最新版本。", ) await download_dashboard( path=path, diff --git a/astrbot/core/utils/llm_metadata.py b/astrbot/core/utils/llm_metadata.py index ef88e9490..254446bc9 100644 --- a/astrbot/core/utils/llm_metadata.py +++ b/astrbot/core/utils/llm_metadata.py @@ -34,33 +34,32 @@ async def update_llm_metadata() -> None: url = "https://models.dev/api.json" try: async with aiohttp.ClientSession( - trust_env=True, connector=build_tls_connector() - ) as session: - async with session.get(url) as response: - data = await response.json() - global LLM_METADATAS - models = {} - for info in data.values(): - for model in info.get("models", {}).values(): - model_id = model.get("id") - if not model_id: - continue - models[model_id] = LLMMetadata( - id=model_id, - reasoning=model.get("reasoning", False), - tool_call=model.get("tool_call", False), - knowledge=model.get("knowledge", "none"), - release_date=model.get("release_date", ""), - modalities=model.get( - "modalities", {"input": [], "output": []} - ), - open_weights=model.get("open_weights", False), - limit=model.get("limit", {"context": 0, "output": 0}), - ) - # Replace the global cache in-place so references remain valid - LLM_METADATAS.clear() - LLM_METADATAS.update(models) - logger.info(f"Successfully fetched metadata for {len(models)} LLMs.") + trust_env=True, connector=build_tls_connector(), + ) as session, session.get(url) as response: + data = await response.json() + global LLM_METADATAS + models = {} + for info in data.values(): + for model in info.get("models", {}).values(): + model_id = model.get("id") + if not model_id: + continue + models[model_id] = LLMMetadata( + id=model_id, + reasoning=model.get("reasoning", False), + tool_call=model.get("tool_call", False), + knowledge=model.get("knowledge", "none"), + release_date=model.get("release_date", ""), + modalities=model.get( + "modalities", {"input": [], "output": []}, + ), + open_weights=model.get("open_weights", False), + limit=model.get("limit", {"context": 0, "output": 0}), + ) + # Replace the global cache in-place so references remain valid + LLM_METADATAS.clear() + LLM_METADATAS.update(models) + logger.info(f"Successfully fetched metadata for {len(models)} LLMs.") except Exception as e: logger.error(f"Failed to fetch LLM metadata: {e}") return diff --git a/astrbot/core/utils/log_pipe.py b/astrbot/core/utils/log_pipe.py index dac43698a..a90af4ef0 100644 --- a/astrbot/core/utils/log_pipe.py +++ b/astrbot/core/utils/log_pipe.py @@ -48,7 +48,6 @@ class LogPipe(threading.Thread, io.TextIOBase): def flush(self) -> None: """No-op for compatibility - log writes are immediate.""" - pass def close(self) -> None: """Close the write end of the pipe.""" diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index 16ae20b7b..ab1098458 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -32,6 +32,7 @@ async def get_media_duration(file_path: str) -> int | None: Returns: 时长(毫秒),如果获取失败返回None + """ try: # 使用ffprobe获取时长 @@ -55,13 +56,12 @@ async def get_media_duration(file_path: str) -> int | None: duration_ms = int(duration_seconds * 1000) logger.debug(f"[Media Utils] 获取媒体时长: {duration_ms}ms") return duration_ms - else: - logger.warning(f"[Media Utils] 无法获取媒体文件时长: {file_path}") - return None + logger.warning(f"[Media Utils] 无法获取媒体文件时长: {file_path}") + return None except FileNotFoundError: logger.warning( - "[Media Utils] ffprobe未安装或不在PATH中,无法获取媒体时长。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffprobe未安装或不在PATH中,无法获取媒体时长。请安装ffmpeg: https://ffmpeg.org/", ) return None except Exception as e: @@ -81,6 +81,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) Raises: Exception: 转换失败时抛出异常 + """ # 如果已经是opus格式,直接返回 if audio_path.lower().endswith(".opus"): @@ -123,7 +124,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) try: await anyio.Path(output_path).unlink() logger.debug( - f"[Media Utils] 已清理失败的opus输出文件: {output_path}" + f"[Media Utils] 已清理失败的opus输出文件: {output_path}", ) except OSError as e: logger.warning(f"[Media Utils] 清理失败的opus输出文件时出错: {e}") @@ -137,7 +138,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) except FileNotFoundError: logger.error( - "[Media Utils] ffmpeg未安装或不在PATH中,无法转换音频格式。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffmpeg未安装或不在PATH中,无法转换音频格式。请安装ffmpeg: https://ffmpeg.org/", ) raise Exception("ffmpeg not found") except Exception as e: @@ -146,7 +147,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) async def convert_video_format( - video_path: str, output_format: str = "mp4", output_path: str | None = None + video_path: str, output_format: str = "mp4", output_path: str | None = None, ) -> str: """使用ffmpeg转换视频格式 @@ -160,6 +161,7 @@ async def convert_video_format( Raises: Exception: 转换失败时抛出异常 + """ # 如果已经是目标格式,直接返回 if video_path.lower().endswith(f".{output_format}"): @@ -198,11 +200,11 @@ async def convert_video_format( try: await anyio.Path(output_path).unlink() logger.debug( - f"[Media Utils] 已清理失败的{output_format}输出文件: {output_path}" + f"[Media Utils] 已清理失败的{output_format}输出文件: {output_path}", ) except OSError as e: logger.warning( - f"[Media Utils] 清理失败的{output_format}输出文件时出错: {e}" + f"[Media Utils] 清理失败的{output_format}输出文件时出错: {e}", ) error_msg = stderr.decode() if stderr else "未知错误" @@ -214,7 +216,7 @@ async def convert_video_format( except FileNotFoundError: logger.error( - "[Media Utils] ffmpeg未安装或不在PATH中,无法转换视频格式。请安装ffmpeg: https://ffmpeg.org/" + "[Media Utils] ffmpeg未安装或不在PATH中,无法转换视频格式。请安装ffmpeg: https://ffmpeg.org/", ) raise Exception("ffmpeg not found") except Exception as e: @@ -236,6 +238,7 @@ async def convert_audio_format( Returns: 转换后的音频文件路径 + """ if audio_path.lower().endswith(f".{output_format}"): return audio_path @@ -248,9 +251,7 @@ async def convert_audio_format( args = ["ffmpeg", "-y", "-i", audio_path] if output_format == "amr": args.extend(["-ac", "1", "-ar", "8000", "-ab", "12.2k"]) - elif output_format == "ogg": - args.extend(["-acodec", "libopus", "-ac", "1", "-ar", "16000"]) - elif output_format == "opus": + elif output_format == "ogg" or output_format == "opus": args.extend(["-acodec", "libopus", "-ac", "1", "-ar", "16000"]) args.append(output_path) @@ -298,7 +299,6 @@ async def ensure_wav(audio_path: str, output_path: str | None = None) -> str: If the file appears to already be wav, return it directly to avoid extra conversion. """ - if not audio_path: return audio_path @@ -432,6 +432,7 @@ async def compress_image( Returns: The compressed image path. Returns the original path if compression fails or the source does not need compression. + """ max_size = max(int(max_size), 1) quality = min(max(int(quality), 1), 100) @@ -441,7 +442,7 @@ async def compress_image( # Skip compression for remote images and return the original value. if url_or_path.startswith("http"): return url_or_path - elif url_or_path.startswith("data:image"): + if url_or_path.startswith("data:image"): _header, encoded = url_or_path.split(",", 1) data = base64.b64decode(encoded) if len(data) < min_file_size_bytes: diff --git a/astrbot/core/utils/migra_helper.py b/astrbot/core/utils/migra_helper.py index 40b899620..e1c4c8367 100644 --- a/astrbot/core/utils/migra_helper.py +++ b/astrbot/core/utils/migra_helper.py @@ -12,8 +12,7 @@ from astrbot.core.db.migration.migra_webchat_session import migrate_webchat_sess def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None: - """ - Migra agent runner configs from provider configs. + """Migra agent runner configs from provider configs. """ try: default_prov_id = conf["provider_settings"]["default_provider_id"] @@ -43,8 +42,7 @@ def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None: def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None: - """ - Migrate old provider structure to new provider-source separation. + """Migrate old provider structure to new provider-source separation. Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body All other fields move to provider_sources. """ @@ -129,10 +127,9 @@ def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None: async def migra( - db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager + db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager, ) -> None: - """ - Stores the migration logic here. + """Stores the migration logic here. btw, i really don't like migration :( """ # 4.5 to 4.6 migration for umop_config_router diff --git a/astrbot/core/utils/network_utils.py b/astrbot/core/utils/network_utils.py index 880d8c6eb..51a5ca187 100644 --- a/astrbot/core/utils/network_utils.py +++ b/astrbot/core/utils/network_utils.py @@ -16,6 +16,7 @@ def is_connection_error(exc: BaseException) -> bool: Returns: True if the exception is a connection/network error + """ # Check for httpx network errors if isinstance( @@ -59,6 +60,7 @@ def log_connection_failure( provider_label: The provider name for log prefix (e.g., "OpenAI", "Gemini") error: The exception that occurred proxy: The proxy address if configured, or None/empty string + """ import os @@ -68,13 +70,13 @@ def log_connection_failure( effective_proxy = proxy if not effective_proxy: effective_proxy = os.environ.get( - "http_proxy", os.environ.get("https_proxy", "") + "http_proxy", os.environ.get("https_proxy", ""), ) if effective_proxy: logger.error( f"[{provider_label}] 网络/代理连接失败 ({error_type})。" - f"代理地址: {effective_proxy},错误: {error}" + f"代理地址: {effective_proxy},错误: {error}", ) else: logger.error(f"[{provider_label}] 网络连接失败 ({error_type})。错误: {error}") @@ -95,6 +97,7 @@ def create_proxy_client( Returns: An httpx.AsyncClient configured with the proxy, or None if no proxy + """ if proxy: logger.info(f"[{provider_label}] 使用代理: {proxy}") diff --git a/astrbot/core/utils/pip_installer.py b/astrbot/core/utils/pip_installer.py index ff259fcbc..92bb9d431 100644 --- a/astrbot/core/utils/pip_installer.py +++ b/astrbot/core/utils/pip_installer.py @@ -45,7 +45,7 @@ _PIP_FAILURE_PATTERNS = { "dependency_detail": re.compile(r"\bdepends on\b", re.IGNORECASE), } _SENSITIVE_PIP_VALUE_KEYS = frozenset( - {"password", "passwd", "pass", "api_token", "token", "auth_token"} + {"password", "passwd", "pass", "api_token", "token", "auth_token"}, ) _MAX_PIP_OUTPUT_LINES = 200 @@ -54,7 +54,7 @@ class DependencyConflictError(Exception): """Raised when pip encounters a dependency conflict.""" def __init__( - self, message: str, errors: list[str], *, is_core_conflict: bool + self, message: str, errors: list[str], *, is_core_conflict: bool, ) -> None: super().__init__(message) self.errors = errors @@ -90,7 +90,7 @@ def _get_pip_main(): "pip module is unavailable " f"(sys.executable={sys.executable}, " f"frozen={getattr(sys, 'frozen', False)}, " - f"ASTRBOT_DESKTOP_CLIENT={os.environ.get('ASTRBOT_DESKTOP_CLIENT')})" + f"ASTRBOT_DESKTOP_CLIENT={os.environ.get('ASTRBOT_DESKTOP_CLIENT')})", ) from exc return pip_main @@ -330,7 +330,7 @@ def _build_packaged_windows_runtime_build_env( return {} include_dir = _normalize_windows_native_build_path( - ntpath.join(runtime_dir, "include") + ntpath.join(runtime_dir, "include"), ) libs_dir = _normalize_windows_native_build_path(ntpath.join(runtime_dir, "libs")) include_exists = os.path.isdir(include_dir) @@ -559,7 +559,7 @@ def _ensure_preferred_modules( site_packages_path: str, ) -> None: unresolved_prefer_reasons = _prefer_modules_from_site_packages( - module_names, site_packages_path + module_names, site_packages_path, ) unresolved_modules: list[str] = [] @@ -619,7 +619,7 @@ def _is_module_loaded_from_site_packages( def _prefer_module_from_site_packages( - module_name: str, site_packages_path: str + module_name: str, site_packages_path: str, ) -> bool: with _SITE_PACKAGES_IMPORT_LOCK: base_path = os.path.join(site_packages_path, *module_name.split(".")) @@ -886,12 +886,12 @@ def _patch_distlib_finder_for_frozen_runtime() -> None: if not isinstance(finder_registry, dict): logger.warning( - "Skip patching distlib finder because _finder_registry is unavailable." + "Skip patching distlib finder because _finder_registry is unavailable.", ) return if not callable(register_finder) or resource_finder is None: logger.warning( - "Skip patching distlib finder because register API is unavailable." + "Skip patching distlib finder because register API is unavailable.", ) return @@ -914,7 +914,7 @@ def _patch_distlib_finder_for_frozen_runtime() -> None: package_name, ): finder_registry = getattr( - distlib_resources, "_finder_registry", finder_registry + distlib_resources, "_finder_registry", finder_registry, ) @@ -944,7 +944,7 @@ class PipInstaller: if package_name and normalized_requirements_path: raise ValueError( - "package_name and requirements_path cannot be used together" + "package_name and requirements_path cannot be used together", ) if package_name: @@ -955,7 +955,7 @@ class PipInstaller: elif normalized_requirements_path: args = ["install", "-r", normalized_requirements_path] requested_requirements = extract_requirement_names( - normalized_requirements_path + normalized_requirements_path, ) if not args: @@ -984,7 +984,7 @@ 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 文件。") @@ -1002,7 +1002,7 @@ class PipInstaller: "--upgrade", "--upgrade-strategy", "only-if-needed", - ] + ], ) async with ( diff --git a/astrbot/core/utils/quoted_message/chain_parser.py b/astrbot/core/utils/quoted_message/chain_parser.py index 528ce14b8..7847061bd 100644 --- a/astrbot/core/utils/quoted_message/chain_parser.py +++ b/astrbot/core/utils/quoted_message/chain_parser.py @@ -89,7 +89,7 @@ def _extract_image_refs_from_component_chain( seg, depth=depth + 1, settings=settings, - ) + ), ) elif isinstance(seg, Node): image_refs.extend( @@ -97,7 +97,7 @@ def _extract_image_refs_from_component_chain( seg.content, depth=depth + 1, settings=settings, - ) + ), ) elif isinstance(seg, Nodes): for node in seg.nodes: @@ -106,7 +106,7 @@ def _extract_image_refs_from_component_chain( node.content, depth=depth + 1, settings=settings, - ) + ), ) return normalize_and_dedupe_strings(image_refs) @@ -308,7 +308,7 @@ def _parse_onebot_segments( isinstance(candidate_file, str) and candidate_file.strip() and looks_like_image_file_name( - seg_data.get("name") or seg_data.get("file_name") or candidate_file + seg_data.get("name") or seg_data.get("file_name") or candidate_file, ) ): image_refs.append(candidate_file.strip()) diff --git a/astrbot/core/utils/quoted_message/extractor.py b/astrbot/core/utils/quoted_message/extractor.py index 83570d66c..3a3ad5e69 100644 --- a/astrbot/core/utils/quoted_message/extractor.py +++ b/astrbot/core/utils/quoted_message/extractor.py @@ -93,14 +93,14 @@ class QuotedMessageExtractor: fetch_remote: bool, ) -> QuotedMessageContent | None: reply = reply_component or self._reply_parser.find_first_reply_component( - self._event + self._event, ) if not reply: return None embedded_text = self._reply_parser.extract_text_from_reply_component(reply) embedded_image_refs = list( - self._reply_parser.extract_image_refs_from_reply_component(reply) + self._reply_parser.extract_image_refs_from_reply_component(reply), ) reply_id = getattr(reply, "id", None) @@ -156,7 +156,7 @@ class QuotedMessageExtractor: if ( embedded_content.embedded_text and not self._reply_parser.is_forward_placeholder_only_text( - embedded_content.embedded_text + embedded_content.embedded_text, ) ): return embedded_content.embedded_text @@ -197,7 +197,7 @@ async def extract_quoted_message_text( settings: QuotedMessageParserSettings | None = None, ) -> str | None: return await QuotedMessageExtractor(event, settings=settings or SETTINGS).text( - reply_component + reply_component, ) @@ -207,5 +207,5 @@ async def extract_quoted_message_images( settings: QuotedMessageParserSettings | None = None, ) -> list[str]: return await QuotedMessageExtractor(event, settings=settings or SETTINGS).images( - reply_component + reply_component, ) diff --git a/astrbot/core/utils/quoted_message/image_resolver.py b/astrbot/core/utils/quoted_message/image_resolver.py index d216968e4..8b4f6f39b 100644 --- a/astrbot/core/utils/quoted_message/image_resolver.py +++ b/astrbot/core/utils/quoted_message/image_resolver.py @@ -36,7 +36,7 @@ def _build_image_resolve_actions( ("get_image", {"image": candidate}), ("get_file", {"file_id": candidate}), ("get_file", {"file": candidate}), - ] + ], ) try: @@ -53,7 +53,7 @@ def _build_image_resolve_actions( ( "get_group_file_url", {"group_id": group_id_value, "file_id": candidate}, - ) + ), ) for candidate in candidates: actions.append(("get_private_file_url", {"file_id": candidate})) diff --git a/astrbot/core/utils/requirements_utils.py b/astrbot/core/utils/requirements_utils.py index 630e7ae29..7012a3ac4 100644 --- a/astrbot/core/utils/requirements_utils.py +++ b/astrbot/core/utils/requirements_utils.py @@ -20,7 +20,6 @@ logger = logging.getLogger("astrbot") class RequirementsPrecheckFailed(Exception): """Raised when the pre-check of requirements fails.""" - pass @dataclass(frozen=True) @@ -59,7 +58,7 @@ def _looks_like_local_path_reference(token: str) -> bool: if not candidate: return False return candidate in {".", ".."} or candidate.startswith( - ("./", "../", "/", "~/", ".\\", "..\\", "\\") + ("./", "../", "/", "~/", ".\\", "..\\", "\\"), ) @@ -189,7 +188,7 @@ def _extract_requirement_names_from_package_tokens(tokens: list[str]) -> frozens "--trusted-host=", "--requirement=", "--constraint=", - ) + ), ): continue @@ -230,7 +229,7 @@ def parse_package_install_input(raw_input: str) -> ParsedPackageInput: continue specs.extend(tokens) requirement_names.update( - _extract_requirement_names_from_package_tokens(tokens) + _extract_requirement_names_from_package_tokens(tokens), ) continue @@ -253,7 +252,7 @@ def _iter_requirement_lines( resolved_path = os.path.realpath(requirements_path) if resolved_path in visited: logger.warning( - "检测到循环依赖的 requirements 包含: %s,将跳过该文件", resolved_path + "检测到循环依赖的 requirements 包含: %s,将跳过该文件", resolved_path, ) return visited.add(resolved_path) @@ -383,7 +382,7 @@ def _load_requirement_lines_for_precheck( def find_missing_requirements(requirements_path: str) -> set[str] | None: can_precheck, requirement_lines = _load_requirement_lines_for_precheck( - requirements_path + requirements_path, ) if not can_precheck or requirement_lines is None: return None @@ -444,7 +443,7 @@ def plan_missing_requirements_install( requirements_path: str, ) -> MissingRequirementsPlan | None: can_precheck, requirement_lines = _load_requirement_lines_for_precheck( - requirements_path + requirements_path, ) if not can_precheck or requirement_lines is None: return None diff --git a/astrbot/core/utils/session_lock.py b/astrbot/core/utils/session_lock.py index 732a29b72..684fc0240 100644 --- a/astrbot/core/utils/session_lock.py +++ b/astrbot/core/utils/session_lock.py @@ -36,7 +36,7 @@ class SessionLockManager: def __init__(self) -> None: self._state_guard = threading.Lock() self._loop_managers: weakref.WeakKeyDictionary[ - asyncio.AbstractEventLoop, _PerLoopSessionLockManager + asyncio.AbstractEventLoop, _PerLoopSessionLockManager, ] = weakref.WeakKeyDictionary() def _get_loop_manager(self) -> _PerLoopSessionLockManager: diff --git a/astrbot/core/utils/session_waiter.py b/astrbot/core/utils/session_waiter.py index 24dd2ad47..0ada3b0fe 100644 --- a/astrbot/core/utils/session_waiter.py +++ b/astrbot/core/utils/session_waiter.py @@ -71,7 +71,7 @@ class SessionController: self.timeout = timeout _holding_task = asyncio.create_task( - self._holding(new_event, timeout) + self._holding(new_event, timeout), ) # 开始新的 keep self.tasks.add(_holding_task) _holding_task.add_done_callback(self.tasks.discard) diff --git a/astrbot/core/utils/shared_preferences.py b/astrbot/core/utils/shared_preferences.py index 8ba6e6928..300df9d45 100644 --- a/astrbot/core/utils/shared_preferences.py +++ b/astrbot/core/utils/shared_preferences.py @@ -32,7 +32,7 @@ class SharedPreferences: self._scheduler = BackgroundScheduler() self._scheduler.add_job( - self._clear_temporary_cache, "interval", hours=24, id="clear_sp_temp_cache" + self._clear_temporary_cache, "interval", hours=24, id="clear_sp_temp_cache", ) self._scheduler.start() @@ -203,7 +203,7 @@ class SharedPreferences: return result def put( - self, key, value, scope: str | None = None, scope_id: str | None = None + self, key, value, scope: str | None = None, scope_id: str | None = None, ) -> None: """设置偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( @@ -212,7 +212,7 @@ class SharedPreferences: ).result() def remove( - self, key, scope: str | None = None, scope_id: str | None = None + self, key, scope: str | None = None, scope_id: str | None = None, ) -> None: """删除偏好设置(已弃用)""" asyncio.run_coroutine_threadsafe( diff --git a/astrbot/core/utils/storage_cleaner.py b/astrbot/core/utils/storage_cleaner.py index fc4d647c8..2a3ae415a 100644 --- a/astrbot/core/utils/storage_cleaner.py +++ b/astrbot/core/utils/storage_cleaner.py @@ -20,7 +20,7 @@ class StorageCleaner: TARGET_LOGS = "logs" TARGET_CACHE = "cache" VALID_TARGETS: ClassVar[frozenset[str]] = frozenset( - {TARGET_LOGS, TARGET_CACHE, "all"} + {TARGET_LOGS, TARGET_CACHE, "all"}, ) def __init__( diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 5fe3aab90..cad157849 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -34,7 +34,7 @@ class NetworkRenderStrategy(RenderStrategy): async def initialize(self) -> None: if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT: _get_official_endpoints_task = asyncio.create_task( - self.get_official_endpoints() + self.get_official_endpoints(), ) self.tasks.add(_get_official_endpoints_task) _get_official_endpoints_task.add_done_callback(self.tasks.discard) diff --git a/astrbot/core/utils/temp_dir_cleaner.py b/astrbot/core/utils/temp_dir_cleaner.py index ec7e2bc61..5b9a39515 100644 --- a/astrbot/core/utils/temp_dir_cleaner.py +++ b/astrbot/core/utils/temp_dir_cleaner.py @@ -72,7 +72,7 @@ class TempDirCleaner: continue total_size += stat.st_size files.append( - TempFileInfo(path=path, size=stat.st_size, mtime=stat.st_mtime) + TempFileInfo(path=path, size=stat.st_size, mtime=stat.st_mtime), ) return total_size, files @@ -81,7 +81,7 @@ class TempDirCleaner: if not self._temp_dir.exists(): return for path in sorted( - self._temp_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True + self._temp_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True, ): if not path.is_dir(): continue diff --git a/astrbot/core/utils/webhook_utils.py b/astrbot/core/utils/webhook_utils.py index 1d1e35955..7dd04d45c 100644 --- a/astrbot/core/utils/webhook_utils.py +++ b/astrbot/core/utils/webhook_utils.py @@ -23,7 +23,7 @@ def _get_dashboard_port() -> int: def _is_dashboard_ssl_enabled() -> bool: env_ssl = os.environ.get("ASTRBOT_SSL_ENABLE") or os.environ.get( - "DASHBOARD_SSL_ENABLE" + "DASHBOARD_SSL_ENABLE", ) if env_ssl is not None: return env_ssl.strip().lower() in {"1", "true", "yes", "on"} @@ -41,8 +41,8 @@ def log_webhook_info(platform_name: str, webhook_uuid: str) -> None: Args: platform_name: 平台名称 webhook_uuid: webhook 的 UUID - """ + """ callback_base = _get_callback_api_base() if not callback_base: @@ -74,6 +74,7 @@ def ensure_platform_webhook_config(platform_cfg: dict) -> bool: Returns: bool: 如果生成了 webhook_uuid 则返回 True,否则返回 False + """ pt = platform_cfg.get("type", "") if pt in WEBHOOK_SUPPORTED_PLATFORMS and not platform_cfg.get("webhook_uuid"): diff --git a/astrbot/core/zip_updator.py b/astrbot/core/zip_updator.py index b97a242dd..30500d28f 100644 --- a/astrbot/core/zip_updator.py +++ b/astrbot/core/zip_updator.py @@ -119,7 +119,7 @@ class RepoZipUpdator: r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$", version, re.IGNORECASE, - ) + ), ) async def check_update( @@ -170,7 +170,7 @@ class RepoZipUpdator: ) async def download_from_repo_url( - self, target_path: str, repo_url: str, proxy="" + self, target_path: str, repo_url: str, proxy="", ) -> None: author, repo, branch = self.parse_github_url(repo_url) diff --git a/astrbot/dashboard/routes/api_key.py b/astrbot/dashboard/routes/api_key.py index 28872c10c..8154c425f 100644 --- a/astrbot/dashboard/routes/api_key.py +++ b/astrbot/dashboard/routes/api_key.py @@ -102,7 +102,7 @@ class ApiKeyRoute(Route): Response().error("expires_in_days must be greater than 0").to_json() ) expires_at = datetime.now(timezone.utc) + timedelta( - days=expires_in_days_int + days=expires_in_days_int, ) raw_key = f"abk_{secrets.token_urlsafe(32)}" diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index 9277279b8..4eb55b53a 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -59,7 +59,7 @@ class AuthRoute(Route): "challenge_id": challenge_id, "nonce": nonce, **challenge, - } + }, ) .__dict__ ) @@ -87,7 +87,7 @@ 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 +95,7 @@ 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: @@ -111,7 +111,7 @@ class AuthRoute(Route): ): change_pwd_hint = True logger.warning( - "The dashboard is using the default password, please change it immediately to ensure security." + "The dashboard is using the default password, please change it immediately to ensure security.", ) legacy_pwd_hint = True diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index bbcea104d..a7add7401 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -40,6 +40,7 @@ def secure_filename(filename: str) -> str: Returns: 安全的文件名 + """ # 跨平台处理:先将反斜杠替换为正斜杠,再取文件名 filename = filename.replace("\\", "/") @@ -70,6 +71,7 @@ def generate_unique_filename(original_filename: str) -> str: Returns: 添加了时间戳后缀的唯一文件名,格式为 {原文件名}_{YYYYMMDD_HHMMSS}.{扩展名} + """ name, ext = os.path.splitext(original_filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") @@ -185,7 +187,7 @@ class BackupRoute(Route): """创建进度回调函数""" async def _callback( - stage: str, current: int, total: int, message: str = "" + stage: str, current: int, total: int, message: str = "", ) -> None: self._update_progress( task_id, @@ -203,7 +205,7 @@ class BackupRoute(Route): if self._cleanup_task is None or self._cleanup_task.done(): try: self._cleanup_task = asyncio.create_task( - self._cleanup_expired_uploads() + self._cleanup_expired_uploads(), ) except RuntimeError: # 如果没有运行中的事件循环,跳过(等待下次异步调用时启动) @@ -256,15 +258,15 @@ class BackupRoute(Route): Returns: dict | None: manifest 内容,如果不是有效备份则返回 None + """ try: with zipfile.ZipFile(zip_path, "r") as zf: if "manifest.json" in zf.namelist(): manifest_data = zf.read("manifest.json") return json.loads(manifest_data.decode("utf-8")) - else: - # 没有 manifest.json,不是有效的 AstrBot 备份 - return None + # 没有 manifest.json,不是有效的 AstrBot 备份 + return None except Exception as e: logger.debug(f"读取备份 manifest 失败: {e}") return None # 无法读取,不是有效备份 @@ -311,11 +313,11 @@ class BackupRoute(Route): "size": stat.st_size, "created_at": stat.st_mtime, "type": manifest.get( - "origin", "exported" + "origin", "exported", ), # 老版本没有 origin 默认为 exported "astrbot_version": manifest.get("astrbot_version", "未知"), "exported_at": manifest.get("exported_at"), - } + }, ) # 按创建时间倒序排序 @@ -334,7 +336,7 @@ class BackupRoute(Route): "total": len(backup_files), "page": page, "page_size": page_size, - } + }, ) .to_json() ) @@ -358,7 +360,7 @@ class BackupRoute(Route): # 启动后台导出任务 _background_export_task = asyncio.create_task( - self._background_export_task(task_id) + self._background_export_task(task_id), ) self.tasks.add(_background_export_task) _background_export_task.add_done_callback(self.tasks.discard) @@ -368,7 +370,7 @@ class BackupRoute(Route): { "task_id": task_id, "message": "export task created, processing in background", - } + }, ) .to_json() ) @@ -446,7 +448,7 @@ class BackupRoute(Route): await file.save(zip_path) logger.info( - f"上传的备份文件已保存: {unique_filename} (原始名称: {file.filename})" + f"上传的备份文件已保存: {unique_filename} (原始名称: {file.filename})", ) return ( @@ -456,7 +458,7 @@ class BackupRoute(Route): "filename": unique_filename, "original_filename": file.filename, "size": (await anyio.Path(zip_path).stat()).st_size, - } + }, ) .to_json() ) @@ -524,7 +526,7 @@ class BackupRoute(Route): logger.info( f"初始化分片上传: upload_id={upload_id}, " - f"filename={unique_filename}, total_chunks={total_chunks}" + f"filename={unique_filename}, total_chunks={total_chunks}", ) return ( @@ -535,7 +537,7 @@ class BackupRoute(Route): "chunk_size": CHUNK_SIZE, "total_chunks": total_chunks, "filename": unique_filename, - } + }, ) .to_json() ) @@ -600,7 +602,7 @@ class BackupRoute(Route): logger.debug( f"接收分片: upload_id={upload_id}, " - f"chunk={chunk_index + 1}/{total_chunks}" + f"chunk={chunk_index + 1}/{total_chunks}", ) return ( @@ -610,7 +612,7 @@ class BackupRoute(Route): "received": received_count, "total": total_chunks, "chunk_index": chunk_index, - } + }, ) .to_json() ) @@ -627,6 +629,7 @@ class BackupRoute(Route): Args: zip_path: ZIP 文件路径 + """ try: # 读取原有 manifest @@ -697,7 +700,7 @@ 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: @@ -712,7 +715,7 @@ class BackupRoute(Route): self._mark_backup_as_uploaded(output_path) logger.info( - f"分片上传完成: {filename}, size={file_size}, chunks={total}" + f"分片上传完成: {filename}, size={file_size}, chunks={total}", ) # 清理分片目录 @@ -725,7 +728,7 @@ class BackupRoute(Route): "filename": filename, "original_filename": session["original_filename"], "size": file_size, - } + }, ) .to_json() ) @@ -858,7 +861,7 @@ class BackupRoute(Route): # 启动后台导入任务 _background_import_task = asyncio.create_task( - self._background_import_task(task_id, zip_path) + self._background_import_task(task_id, zip_path), ) self.tasks.add(_background_import_task) _background_import_task.add_done_callback(self.tasks.discard) @@ -869,7 +872,7 @@ class BackupRoute(Route): { "task_id": task_id, "message": "import task created, processing in background", - } + }, ) .to_json() ) @@ -1075,8 +1078,7 @@ class BackupRoute(Route): new_name = secure_filename(new_name) # 移除新文件名中的扩展名(如果有的话) - if new_name.endswith(".zip"): - new_name = new_name[:-4] + new_name = new_name.removesuffix(".zip") # 验证新文件名不为空 if not new_name or new_name.replace("_", "") == "": @@ -1106,7 +1108,7 @@ class BackupRoute(Route): { "old_filename": filename, "new_filename": new_filename, - } + }, ) .to_json() ) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 663af5c9b..e428cb44d 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -109,7 +109,7 @@ 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 +139,7 @@ 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 +163,7 @@ 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() @@ -175,7 +175,7 @@ class ChatRoute(Route): "attachment_id": attachment.attachment_id, "filename": filename, "type": attach_type, - } + }, ) .to_json() ) @@ -183,11 +183,11 @@ 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 +199,7 @@ class ChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, accumulated_text: str, accumulated_parts: list, ) -> dict: """从消息中提取 web_search_tavily 的引用 @@ -209,6 +209,7 @@ class ChatRoute(Route): Returns: 包含 used 列表的字典,记录被引用的搜索结果 + """ supported = ["web_search_tavily", "web_search_bocha"] web_search_results = {} @@ -220,7 +221,7 @@ class ChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -308,7 +309,7 @@ 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(): @@ -329,7 +330,7 @@ class ChatRoute(Route): async with track_conversation(self.running_convs, webchat_conv_id): while True: result, should_break = await _poll_webchat_stream_result( - back_queue, username + back_queue, username, ) if should_break: client_disconnected = True @@ -362,7 +363,7 @@ class ChatRoute(Route): except Exception as e: if not client_disconnected: logger.debug( - f"[WebChat] 用户 {username} 断开聊天长连接。 {e}" + f"[WebChat] 用户 {username} 断开聊天长连接。 {e}", ) client_disconnected = True try: @@ -378,7 +379,7 @@ class ChatRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -391,7 +392,7 @@ class ChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -403,27 +404,27 @@ 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) if msg_type == "end": break - elif streaming and msg_type == "complete" or not streaming: + elif (streaming and msg_type == "complete") or not streaming: if ( chain_type == "tool_call" or chain_type == "tool_call_result" @@ -431,7 +432,7 @@ 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( @@ -452,7 +453,7 @@ class ChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, } @@ -482,7 +483,7 @@ class ChatRoute(Route): "enable_streaming": enable_streaming, "message_id": message_id, }, - ) + ), ) message_parts_for_storage = strip_message_parts_path_fields(message_parts) await self.platform_history_mgr.insert( @@ -547,7 +548,7 @@ 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 @@ -614,7 +615,7 @@ class ChatRoute(Route): "deleted_count": deleted_count, "failed_count": len(failed_items), "failed_items": failed_items, - } + }, ) .to_json() ) @@ -643,7 +644,7 @@ class ChatRoute(Route): await anyio.Path(attachment.path).unlink() except OSError as e: logger.warning( - f"Failed to delete attachment file {attachment.path}: {e}" + f"Failed to delete attachment file {attachment.path}: {e}", ) except Exception as e: logger.warning(f"Failed to get attachments: {e}") @@ -657,7 +658,7 @@ 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() @@ -665,7 +666,7 @@ class ChatRoute(Route): data={ "session_id": session.session_id, "platform_id": session.platform_id, - } + }, ) .to_json() ) @@ -693,7 +694,7 @@ class ChatRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return Response().ok(data=sessions_data).to_json() @@ -706,12 +707,12 @@ 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] = { @@ -742,6 +743,6 @@ 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/chatui_project.py b/astrbot/dashboard/routes/chatui_project.py index 2b9c6b6c0..94b702446 100644 --- a/astrbot/dashboard/routes/chatui_project.py +++ b/astrbot/dashboard/routes/chatui_project.py @@ -54,7 +54,7 @@ class ChatUIProjectRoute(Route): "description": project.description, "created_at": to_utc_isoformat(project.created_at), "updated_at": to_utc_isoformat(project.updated_at), - } + }, ) .to_json() ) @@ -105,7 +105,7 @@ class ChatUIProjectRoute(Route): "description": project.description, "created_at": to_utc_isoformat(project.created_at), "updated_at": to_utc_isoformat(project.updated_at), - } + }, ) .to_json() ) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 1de11db08..ce446f28f 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -50,9 +50,9 @@ 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() + and value.replace(".", "", 1).isdigit()) or (type_ == "float" and isinstance(value, int)) ): return float(value) @@ -66,7 +66,7 @@ def try_cast(value: Any, type_: str): def _expect_type(value, expected_type, path_key, errors, expected_name=None) -> bool: if not isinstance(value, expected_type): errors.append( - f"错误的类型 {path_key}: 期望是 {expected_name or expected_type.__name__}, 得到了 {type(value).__name__}" + f"错误的类型 {path_key}: 期望是 {expected_name or expected_type.__name__}, 得到了 {type(value).__name__}", ) return False return True @@ -116,7 +116,7 @@ def validate_config(data, schema: dict, is_core: bool) -> tuple[list[str], dict] for idx, item in enumerate(value): if not isinstance(item, str): errors.append( - f"Invalid type {path}{key}[{idx}]: expected string, got {type(item).__name__}" + f"Invalid type {path}{key}[{idx}]: expected string, got {type(item).__name__}", ) continue normalized = normalize_rel_path(item) @@ -133,7 +133,7 @@ def validate_config(data, schema: dict, is_core: bool) -> tuple[list[str], dict] continue if meta["type"] == "list" and (not isinstance(value, list)): errors.append( - f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}", ) elif ( meta["type"] == "list" @@ -150,31 +150,31 @@ def validate_config(data, schema: dict, is_core: bool) -> tuple[list[str], dict] casted = try_cast(value, "int") if casted is None: errors.append( - f"错误的类型 {path}{key}: 期望是 int, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 int, 得到了 {type(value).__name__}", ) data[key] = casted elif meta["type"] == "float" and (not isinstance(value, float)): casted = try_cast(value, "float") if casted is None: errors.append( - f"错误的类型 {path}{key}: 期望是 float, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 float, 得到了 {type(value).__name__}", ) data[key] = casted elif meta["type"] == "bool" and (not isinstance(value, bool)): errors.append( - f"错误的类型 {path}{key}: 期望是 bool, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 bool, 得到了 {type(value).__name__}", ) elif meta["type"] in ["string", "text"] and (not isinstance(value, str)): errors.append( - f"错误的类型 {path}{key}: 期望是 string, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 string, 得到了 {type(value).__name__}", ) elif meta["type"] == "list" and (not isinstance(value, list)): errors.append( - f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}", ) elif meta["type"] == "object" and (not isinstance(value, dict)): errors.append( - f"错误的类型 {path}{key}: 期望是 dict, 得到了 {type(value).__name__}" + f"错误的类型 {path}{key}: 期望是 dict, 得到了 {type(value).__name__}", ) if is_core: @@ -250,7 +250,7 @@ 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,7 @@ 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 +270,11 @@ 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 +287,7 @@ 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 @@ -381,7 +381,7 @@ class ConfigRoute(Route): return ( Response() .error( - f"Provider source ID '{new_source_config['id']}' exists already, please try another ID." + f"Provider source ID '{new_source_config['id']}' exists already, please try another ID.", ) .to_json() ) @@ -428,10 +428,10 @@ class ConfigRoute(Route): _provider_group: Any = _cfg["provider_group"] _provider_meta: Any = _provider_group["metadata"]["provider"] converted: dict[str, Any] = ConfigMetadataI18n.convert_to_i18n_keys( - {"provider_group": {"metadata": {"provider": _provider_meta}}} + {"provider_group": {"metadata": {"provider": _provider_meta}}}, ) config_schema = { - "provider": converted["provider_group"]["metadata"]["provider"] + "provider": converted["provider_group"]["metadata"]["provider"], } data = { "config_schema": config_schema, @@ -554,7 +554,7 @@ class ConfigRoute(Route): schema=abconf.schema, ) metadata = ConfigMetadataI18n.convert_to_i18n_keys( - CONFIG_METADATA_3_SYSTEM + CONFIG_METADATA_3_SYSTEM, ) return Response().ok({"config": abconf, "metadata": metadata}).to_json() if abconf_id is None: @@ -630,27 +630,27 @@ class ConfigRoute(Route): "error": None, } logger.debug( - f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})" + f"Attempting to check provider: {status_info['name']} (ID: {status_info['id']}, Type: {status_info['type']}, Model: {status_info['model']})", ) try: await provider.test() status_info["status"] = "available" logger.info( - f"Provider {status_info['name']} (ID: {status_info['id']}) is available." + f"Provider {status_info['name']} (ID: {status_info['id']}) is available.", ) except Exception as e: error_message = str(e) status_info["error"] = error_message logger.warning( - f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}" + f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}", ) logger.debug( - f"Traceback for {status_info['name']}:\n{traceback.format_exc()}" + f"Traceback for {status_info['name']}:\n{traceback.format_exc()}", ) 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 +662,7 @@ 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: @@ -672,7 +672,7 @@ class ConfigRoute(Route): target = prov_mgr.inst_map.get(provider_id) if not target: logger.warning( - f"Provider with id '{provider_id}' not found in provider_manager." + f"Provider with id '{provider_id}' not found in provider_manager.", ) return ( Response() @@ -683,7 +683,7 @@ 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): @@ -780,7 +780,7 @@ class ConfigRoute(Route): return ( Response() .error( - "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志" + "提供商适配器加载失败,请检查提供商类型配置或查看服务端日志", ) .to_json() ) @@ -803,7 +803,7 @@ class ConfigRoute(Route): vec = await inst.get_embedding("echo") dim = len(vec) logger.info( - f"检测到 {provider_config.get('id', 'unknown')} 的嵌入向量维度为 {dim}" + f"检测到 {provider_config.get('id', 'unknown')} 的嵌入向量维度为 {dim}", ) return Response().ok({"embedding_dimensions": dim}).to_json() except Exception as e: @@ -875,7 +875,7 @@ class ConfigRoute(Route): if inspect.iscoroutinefunction(terminate_fn): await terminate_fn() logger.info( - f"获取到 provider_source {provider_source_id} 的模型列表: {models}" + f"获取到 provider_source {provider_source_id} 的模型列表: {models}", ) return ( Response() @@ -1017,7 +1017,7 @@ 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 +1205,7 @@ 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 +1220,10 @@ 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 @@ -1231,21 +1231,21 @@ class ConfigRoute(Route): logger.debug(f"Logo token registered for platform {platform.name}") else: logger.warning( - f"Platform {platform.name} logo file not found: {logo_file_path}" + f"Platform {platform.name} logo file not found: {logo_file_path}", ) except (ImportError, AttributeError) as e: logger.warning( - f"Failed to import required modules for platform {platform.name}: {e}" + f"Failed to import required modules for platform {platform.name}: {e}", ) except OSError as e: logger.warning(f"File system error for platform {platform.name} logo: {e}") except Exception as e: logger.warning( - f"Unexpected error registering logo for platform {platform.name}: {e}" + f"Unexpected error registering logo for platform {platform.name}: {e}", ) 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,14 +1254,14 @@ 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"): if key in field_value: field_value[key] = f"{i18n_prefix}.{field_key}.{key}" metadata["platform_group"]["metadata"]["platform"]["items"].update( - platform_items_to_inject + platform_items_to_inject, ) async def _get_astrbot_config(self): @@ -1271,7 +1271,7 @@ class ConfigRoute(Route): _pg_meta: Any = _pg["metadata"] _platform_meta: Any = _pg_meta["platform"] platform_i18n = ConfigMetadataI18n.convert_to_i18n_keys( - {"platform_group": {"metadata": {"platform": _platform_meta}}} + {"platform_group": {"metadata": {"platform": _platform_meta}}}, ) _target: Any = _pg_meta _platform_i18n_dict: Any = platform_i18n @@ -1287,15 +1287,15 @@ class ConfigRoute(Route): for platform in platform_registry: if platform.default_config_tmpl: platform_default_tmpl[platform.name] = copy.deepcopy( - platform.default_config_tmpl + platform.default_config_tmpl, ) 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( - self._register_platform_logo(platform, platform_default_tmpl) + self._register_platform_logo(platform, platform_default_tmpl), ) if logo_registration_tasks: await asyncio.gather(*logo_registration_tasks, return_exceptions=True) @@ -1323,13 +1323,13 @@ class ConfigRoute(Route): "description": f"{plugin_name} 配置", "type": "object", "items": plugin_md.config.schema, - } + }, } break 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 +1355,7 @@ 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/conversation.py b/astrbot/dashboard/routes/conversation.py index f8849eabe..fe682a896 100644 --- a/astrbot/dashboard/routes/conversation.py +++ b/astrbot/dashboard/routes/conversation.py @@ -213,7 +213,7 @@ class ConversationRoute(Route): conv_mgr = self.core_lifecycle.conversation_manager if conv_mgr is None: failed_items.append( - f"user_id:{user_id}, cid:{cid} - conversation manager not available" + f"user_id:{user_id}, cid:{cid} - conversation manager not available", ) continue await conv_mgr.delete_conversation( @@ -345,7 +345,7 @@ class ConversationRoute(Route): if not conversation: failed_items.append( - f"user_id:{user_id}, cid:{cid} - 对话不存在" + f"user_id:{user_id}, cid:{cid} - 对话不存在", ) continue @@ -371,7 +371,7 @@ class ConversationRoute(Route): except Exception as e: failed_items.append(f"user_id:{user_id}, cid:{cid} - {e!s}") logger.error( - f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}" + f"导出对话失败: user_id={user_id}, cid={cid}, error={e!s}", ) if exported_count == 0: diff --git a/astrbot/dashboard/routes/cron.py b/astrbot/dashboard/routes/cron.py index 8b1344f2d..cac511d3c 100644 --- a/astrbot/dashboard/routes/cron.py +++ b/astrbot/dashboard/routes/cron.py @@ -11,7 +11,7 @@ from .route import Response, Route, RouteContext class CronRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle + self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle @@ -42,7 +42,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").to_json() + Response().error("Cron manager not initialized").to_json(), ) job_type = request.args.get("type") jobs = await cron_mgr.list_jobs(job_type) @@ -57,7 +57,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").to_json() + Response().error("Cron manager not initialized").to_json(), ) payload = await request.json @@ -79,13 +79,13 @@ class CronRoute(Route): return jsonify(Response().error("session is required").to_json()) if run_once and not run_at: return jsonify( - Response().error("run_at is required when run_once=true").to_json() + Response().error("run_at is required when run_once=true").to_json(), ) if (not run_once) and not cron_expression: return jsonify( Response() .error("cron_expression is required when run_once=false") - .to_json() + .to_json(), ) if run_once and cron_expression: cron_expression = None # ignore cron when run_once specified @@ -95,7 +95,7 @@ class CronRoute(Route): run_at_dt = datetime.fromisoformat(str(run_at)) except Exception: return jsonify( - Response().error("run_at must be ISO datetime").to_json() + Response().error("run_at must be ISO datetime").to_json(), ) job_payload = { @@ -128,7 +128,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").to_json() + Response().error("Cron manager not initialized").to_json(), ) payload = await request.json @@ -165,7 +165,7 @@ class CronRoute(Route): cron_mgr = self.core_lifecycle.cron_manager if cron_mgr is None: return jsonify( - Response().error("Cron manager not initialized").to_json() + Response().error("Cron manager not initialized").to_json(), ) await cron_mgr.delete_job(job_id) return jsonify(Response().ok(message="deleted").to_json()) diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index 57fc22880..146fb58f9 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -38,10 +38,10 @@ 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 +83,7 @@ class KnowledgeBaseRoute(Route): } def _set_task_result( - self, task_id: str, status: str, result: Any = None, error: str | None = None + self, task_id: str, status: str, result: Any = None, error: str | None = None, ) -> None: self.upload_tasks[task_id] = { "status": status, @@ -176,7 +176,7 @@ class KnowledgeBaseRoute(Route): # 创建进度回调函数 progress_callback = self._make_progress_callback( - task_id, file_idx, file_info["file_name"] + task_id, file_idx, file_info["file_name"], ) doc = await kb_helper.upload_document( @@ -258,7 +258,7 @@ class KnowledgeBaseRoute(Route): # 创建进度回调函数 progress_callback = self._make_progress_callback( - task_id, file_idx, file_name + task_id, file_idx, file_name, ) # 调用 upload_document,传入 pre_chunked_text @@ -395,7 +395,7 @@ 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 1d5fe049e..0523a63ab 100644 --- a/astrbot/dashboard/routes/live_chat.py +++ b/astrbot/dashboard/routes/live_chat.py @@ -80,7 +80,7 @@ class LiveChatSession: start_time = time.time() if not self.is_speaking or stamp != self.current_stamp: logger.warning( - f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}" + f"[Live Chat] stamp 不匹配或未在说话状态: {stamp} vs {self.current_stamp}", ) return None, 0.0 @@ -106,7 +106,7 @@ class LiveChatSession: self.temp_audio_path = audio_path logger.info( - f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {(await anyio.Path(audio_path).stat()).st_size} bytes" + f"[Live Chat] 音频文件已保存: {audio_path}, 大小: {(await anyio.Path(audio_path).stat()).st_size} bytes", ) return audio_path, time.time() - start_time @@ -254,7 +254,7 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] WebSocket 连接关闭: {username}") async def _create_attachment_from_file( - self, filename: str, attach_type: str + self, filename: str, attach_type: str, ) -> dict | None: """从本地文件创建 attachment 并返回消息部分。""" return await create_attachment_part_from_existing_file( @@ -266,7 +266,7 @@ class LiveChatRoute(Route): ) def _extract_web_search_refs( - self, accumulated_text: str, accumulated_parts: list + self, accumulated_text: str, accumulated_parts: list, ) -> dict: """从消息中提取 web_search 引用。""" supported = ["web_search_tavily", "web_search_bocha"] @@ -280,7 +280,7 @@ class LiveChatRoute(Route): for part in tool_call_parts: for tool_call in part["tool_calls"]: if tool_call.get("name") not in supported or not tool_call.get( - "result" + "result", ): continue try: @@ -357,7 +357,7 @@ class LiveChatRoute(Route): request_id: str, ) -> None: back_queue = webchat_queue_mgr.get_or_create_back_queue( - request_id, chat_session_id + request_id, chat_session_id, ) try: while True: @@ -414,7 +414,7 @@ class LiveChatRoute(Route): session.chat_subscription_tasks.clear() async def _handle_chat_message( - self, session: LiveChatSession, message: dict + self, session: LiveChatSession, message: dict, ) -> None: """处理 Chat Mode 消息(ct=chat)""" msg_type = message.get("t") @@ -611,7 +611,7 @@ class LiveChatRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" except Exception: @@ -627,7 +627,7 @@ class LiveChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) except Exception: @@ -666,7 +666,7 @@ class LiveChatRoute(Route): or accumulated_text or accumulated_reasoning or refs - or agent_stats + or agent_stats, ) elif (streaming and msg_type == "complete") or not streaming: if chain_type not in ( @@ -705,7 +705,7 @@ class LiveChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, }, @@ -791,13 +791,13 @@ class LiveChatRoute(Route): logger.info(f"[Live Chat] 用户打断: {session.username}") async def _process_audio( - self, session: LiveChatSession, audio_path: str, assemble_duration: float + self, session: LiveChatSession, audio_path: str, assemble_duration: float, ) -> None: """处理音频:STT -> LLM -> 流式 TTS""" try: # 发送 WAV 组装耗时 await websocket.send_json( - {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}} + {"t": "metrics", "data": {"wav_assemble_time": assemble_duration}}, ) wav_assembly_finish_time = time.time() @@ -809,7 +809,7 @@ class LiveChatRoute(Route): if pm is None or pm.context is None: logger.error("[Live Chat] Plugin manager not available") await websocket.send_json( - {"t": "error", "data": "Plugin manager not available"} + {"t": "error", "data": "Plugin manager not available"}, ) return ctx = pm.context @@ -821,7 +821,7 @@ class LiveChatRoute(Route): return await websocket.send_json( - {"t": "metrics", "data": {"stt": stt_provider.meta().type}} + {"t": "metrics", "data": {"stt": stt_provider.meta().type}}, ) user_text = await stt_provider.get_text(audio_path) @@ -835,7 +835,7 @@ class LiveChatRoute(Route): { "t": "user_msg", "data": {"text": user_text, "ts": int(time.time() * 1000)}, - } + }, ) # 2. 构造消息事件并发送到 pipeline @@ -869,7 +869,7 @@ class LiveChatRoute(Route): await websocket.send_json({"t": "stop_play"}) # 保存消息并标记为被打断 await self._save_interrupted_message( - session, user_text, bot_text + session, user_text, bot_text, ) # 清空队列中未处理的消息 while not back_queue.empty(): @@ -894,7 +894,7 @@ class LiveChatRoute(Route): result_message_id = result.get("message_id") if result_message_id != message_id: logger.warning( - f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}" + f"[Live Chat] 消息 ID 不匹配: {result_message_id} != {message_id}", ) continue @@ -913,7 +913,7 @@ class LiveChatRoute(Route): "llm_total_time": stats.get("end_time", 0) - stats.get("start_time", 0), }, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 AgentStats 失败: {e}") @@ -926,7 +926,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": stats, - } + }, ) except Exception as e: logger.error(f"[Live Chat] 解析 TTSStats 失败: {e}") @@ -950,9 +950,9 @@ class LiveChatRoute(Route): { "t": "metrics", "data": { - "speak_to_first_frame": speak_to_first_frame_latency + "speak_to_first_frame": speak_to_first_frame_latency, }, - } + }, ) text = result.get("text") @@ -961,7 +961,7 @@ class LiveChatRoute(Route): { "t": "bot_text_chunk", "data": {"text": text}, - } + }, ) # 发送音频数据给前端 @@ -969,7 +969,7 @@ class LiveChatRoute(Route): { "t": "response", "data": data, # base64 编码的音频数据 - } + }, ) elif result_type in ["complete", "end"]: @@ -985,7 +985,7 @@ class LiveChatRoute(Route): "text": bot_text, "ts": int(time.time() * 1000), }, - } + }, ) # 发送结束标记 @@ -997,7 +997,7 @@ class LiveChatRoute(Route): { "t": "metrics", "data": {"wav_to_tts_total_time": wav_to_tts_duration}, - } + }, ) break finally: @@ -1012,7 +1012,7 @@ 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 + " [用户打断]" @@ -1022,11 +1022,11 @@ class LiveChatRoute(Route): try: timestamp = int(time.time() * 1000) logger.info( - f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] 用户消息: {user_text} (session: {session.session_id}, ts: {timestamp})", ) if bot_text: logger.info( - f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})" + f"[Live Chat] Bot 消息(打断): {interrupted_text} (session: {session.session_id}, ts: {timestamp})", ) except Exception as e: logger.error(f"[Live Chat] 记录消息失败: {e}", exc_info=True) diff --git a/astrbot/dashboard/routes/log.py b/astrbot/dashboard/routes/log.py index 1dfa48cb3..d2663cdbc 100644 --- a/astrbot/dashboard/routes/log.py +++ b/astrbot/dashboard/routes/log.py @@ -34,10 +34,10 @@ 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 +46,7 @@ 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 310529f42..8e85cf053 100644 --- a/astrbot/dashboard/routes/open_api.py +++ b/astrbot/dashboard/routes/open_api.py @@ -82,7 +82,7 @@ class OpenApiRoute(Route): "name": str(conf_info.get("name", "")).strip(), "path": str(conf_info.get("path", "")).strip(), "is_default": conf_id == "default", - } + }, ) return result @@ -150,7 +150,7 @@ class OpenApiRoute(Route): async def chat_send(self): post_data = await request.get_json(silent=True) or {} effective_username, username_err = self._resolve_open_username( - post_data.get("username") + post_data.get("username"), ) if username_err: return Response().error(username_err).to_json() @@ -253,7 +253,7 @@ class OpenApiRoute(Route): "type": "error", "code": code, "data": message, - } + }, ) async def _ensure_runtime_ready(self) -> bool: @@ -296,11 +296,11 @@ class OpenApiRoute(Route): async def _handle_chat_ws_send(self, post_data: dict) -> None: effective_username, username_err = self._resolve_open_username( - post_data.get("username") + post_data.get("username"), ) if username_err or not effective_username: await self._send_chat_ws_error( - username_err or "Invalid username", "BAD_USER" + username_err or "Invalid username", "BAD_USER", ) return @@ -363,7 +363,7 @@ class OpenApiRoute(Route): "enable_streaming": enable_streaming, "message_id": message_id, }, - ) + ), ) message_parts_for_storage = strip_message_parts_path_fields(message_parts) @@ -381,7 +381,7 @@ class OpenApiRoute(Route): "data": None, "session_id": session_id, "message_id": message_id, - } + }, ) accumulated_parts = [] @@ -433,7 +433,7 @@ class OpenApiRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -443,7 +443,7 @@ class OpenApiRoute(Route): tool_calls[tc_id]["result"] = tcr.get("result") tool_calls[tc_id]["finished_ts"] = tcr.get("ts") accumulated_parts.append( - {"type": "tool_call", "tool_calls": [tool_calls[tc_id]]} + {"type": "tool_call", "tool_calls": [tool_calls[tc_id]]}, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -455,28 +455,28 @@ class OpenApiRoute(Route): elif msg_type == "image": filename = str(result_text).replace("[IMAGE]", "") part = await self.chat_route._create_attachment_from_file( - filename, "image" + filename, "image", ) if part: accumulated_parts.append(part) elif msg_type == "record": filename = str(result_text).replace("[RECORD]", "") part = await self.chat_route._create_attachment_from_file( - filename, "record" + filename, "record", ) if part: accumulated_parts.append(part) elif msg_type == "file": filename = str(result_text).replace("[FILE]", "") part = await self.chat_route._create_attachment_from_file( - filename, "file" + filename, "file", ) if part: accumulated_parts.append(part) elif msg_type == "video": filename = str(result_text).replace("[VIDEO]", "") part = await self.chat_route._create_attachment_from_file( - filename, "video" + filename, "video", ) if part: accumulated_parts.append(part) @@ -512,11 +512,11 @@ class OpenApiRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, "session_id": session_id, - } + }, ) accumulated_parts = [] accumulated_text = "" @@ -526,7 +526,7 @@ class OpenApiRoute(Route): except Exception as e: logger.exception(f"Open API WS chat failed: {e}", exc_info=True) await self._send_chat_ws_error( - f"Failed to process message: {e}", "PROCESSING_ERROR" + f"Failed to process message: {e}", "PROCESSING_ERROR", ) finally: webchat_queue_mgr.remove_back_queue(message_id) @@ -578,7 +578,7 @@ class OpenApiRoute(Route): async def get_chat_sessions(self): username, username_err = self._resolve_open_username( - request.args.get("username") + request.args.get("username"), ) if username_err: return Response().error(username_err).to_json() @@ -591,12 +591,9 @@ class OpenApiRoute(Route): except ValueError: return Response().error("page and page_size must be integers").to_json() - if page < 1: - page = 1 - if page_size < 1: - page_size = 1 - if page_size > 100: - page_size = 100 + page = max(page, 1) + page_size = max(page_size, 1) + page_size = min(page_size, 100) platform_id = request.args.get("platform_id") @@ -623,7 +620,7 @@ class OpenApiRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return ( @@ -634,7 +631,7 @@ class OpenApiRoute(Route): "page": page, "page_size": page_size, "total": total, - } + }, ) .to_json() ) @@ -689,7 +686,7 @@ class OpenApiRoute(Route): try: message_chain = await self._build_message_chain_from_payload( - message_payload + message_payload, ) await platform_inst.send_by_session(session, message_chain) return Response().ok().to_json() diff --git a/astrbot/dashboard/routes/persona.py b/astrbot/dashboard/routes/persona.py index 9298d4907..9e80002d0 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 if folder_id else None, ) else: personas = await self.persona_mgr.get_all_personas() @@ -217,7 +217,7 @@ class PersonaRoute(Route): if has_custom_error_message: if custom_error_message is not None and not isinstance( - custom_error_message, str + custom_error_message, str, ): return Response().error("自定义报错回复信息必须是字符串").to_json() if isinstance(custom_error_message, str): diff --git a/astrbot/dashboard/routes/platform.py b/astrbot/dashboard/routes/platform.py index 941a08d3e..50917d161 100644 --- a/astrbot/dashboard/routes/platform.py +++ b/astrbot/dashboard/routes/platform.py @@ -50,6 +50,7 @@ class PlatformRoute(Route): Returns: 根据平台适配器返回相应的响应 + """ # 根据 webhook_uuid 查找对应的平台 platform_adapter = self._find_platform_by_uuid(webhook_uuid) @@ -64,7 +65,7 @@ class PlatformRoute(Route): return result except NotImplementedError: logger.error( - f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法" + f"平台 {platform_adapter.meta().name} 未实现 webhook_callback 方法", ) return Response().error("平台未支持统一 Webhook 模式").to_json(), 500 except Exception as e: @@ -79,6 +80,7 @@ class PlatformRoute(Route): Returns: 平台适配器实例,未找到则返回 None + """ if self.platform_manager is None: return None @@ -93,6 +95,7 @@ class PlatformRoute(Route): Returns: 包含平台统计信息的响应 + """ try: mgr = self.platform_manager diff --git a/astrbot/dashboard/routes/plugin.py b/astrbot/dashboard/routes/plugin.py index e5d2cf7d3..a56a5c514 100644 --- a/astrbot/dashboard/routes/plugin.py +++ b/astrbot/dashboard/routes/plugin.py @@ -105,7 +105,7 @@ class PluginRoute(Route): data = await request.get_json() version_spec = data.get("astrbot_version", "") is_valid, message = self.plugin_manager._validate_astrbot_version_specifier( - version_spec + version_spec, ) return ( Response() @@ -114,7 +114,7 @@ class PluginRoute(Route): "compatible": is_valid, "message": message, "astrbot_version": version_spec, - } + }, ) .to_json() ) @@ -141,8 +141,7 @@ class PluginRoute(Route): if success: return Response().ok(None, f"插件 {dir_name} 重载成功。").to_json() - else: - return Response().error(f"重载失败: {err}").to_json() + return Response().error(f"重载失败: {err}").to_json() except Exception as e: logger.error(f"/api/plugin/reload-failed: {traceback.format_exc()}") @@ -213,7 +212,7 @@ class PluginRoute(Route): continue # 继续尝试其他URL或使用缓存 logger.info( - f"成功获取远程插件市场数据,包含 {len(remote_data)} 个插件" + f"成功获取远程插件市场数据,包含 {len(remote_data)} 个插件", ) # 获取最新的MD5并保存到缓存 current_md5 = await self._fetch_remote_md5(source.md5_url) @@ -337,7 +336,7 @@ 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: @@ -364,7 +363,7 @@ 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 @@ -379,7 +378,7 @@ class PluginRoute(Route): base_dir = Path( self.plugin_manager.reserved_plugin_path if plugin.reserved - else self.plugin_manager.plugin_store_path + else self.plugin_manager.plugin_store_path, ) plugin_dir = base_dir / plugin.root_dir_name if not plugin_dir.is_dir(): @@ -435,7 +434,7 @@ class PluginRoute(Route): plugin.desc, plugin.version, plugin.display_name, - ] + ], ): continue _plugin_resp.append(_t) @@ -704,7 +703,7 @@ class PluginRoute(Route): raise result if isinstance(result, BaseException): results.append( - {"name": name, "status": "error", "message": str(result)} + {"name": name, "status": "error", "message": str(result)}, ) else: results.append(result) @@ -859,7 +858,7 @@ 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 4c449decd..57dc0f9f7 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -16,7 +16,7 @@ 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, ) @@ -59,7 +59,7 @@ def runtime_status_response( core_lifecycle, include_failure_details=include_failure_details, ), - ).to_json() + ).to_json(), ) response.status_code = status_code return response @@ -107,7 +107,7 @@ 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 302565fe9..a73be8b7c 100644 --- a/astrbot/dashboard/routes/session_management.py +++ b/astrbot/dashboard/routes/session_management.py @@ -50,7 +50,7 @@ class SessionManagementRoute(Route): self.register_routes() async def _get_umo_rules( - self, page: int = 1, page_size: int = 10, search: str = "" + self, page: int = 1, page_size: int = 10, search: str = "", ) -> tuple[dict, int]: """获取所有带有自定义规则的 umo 及其规则内容(支持分页和搜索)。 @@ -68,6 +68,7 @@ class SessionManagementRoute(Route): Returns: tuple[dict, int]: (umo_rules, total) - 分页后的 umo 规则和总数 + """ umo_rules: dict[str, Any] = {} async with self.db_helper.get_db() as session: @@ -75,7 +76,7 @@ class SessionManagementRoute(Route): select(Preference).where( col(Preference.scope) == "umo", col(Preference.key).in_(AVAILABLE_SESSION_RULE_KEYS), - ) + ), ) prefs = result.scalars().all() for pref in prefs: @@ -120,14 +121,12 @@ class SessionManagementRoute(Route): page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 10, type=int) search = request.args.get("search", "", type=str).strip() - if page < 1: - page = 1 + page = max(page, 1) if page_size < 1: page_size = 10 - if page_size > 100: - page_size = 100 + page_size = min(page_size, 100) umo_rules, total = await self._get_umo_rules( - page=page, page_size=page_size, search=search + page=page, page_size=page_size, search=search, ) rules_list = [] for umo, rules in umo_rules.items(): @@ -199,7 +198,7 @@ class SessionManagementRoute(Route): "available_plugins": available_plugins, "available_kbs": available_kbs, "available_rule_keys": AVAILABLE_SESSION_RULE_KEYS, - } + }, ) .to_json() ) @@ -264,11 +263,10 @@ class SessionManagementRoute(Route): .ok({"message": f"规则 {rule_key} 已删除", "umo": umo}) .to_json() ) - else: - await sp.clear_async("umo", umo) - return ( - Response().ok({"message": "所有规则已删除", "umo": umo}).to_json() - ) + await sp.clear_async("umo", umo) + 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() @@ -302,7 +300,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] if scope == "group": @@ -348,16 +346,15 @@ class SessionManagementRoute(Route): "message": f"{message},{len(failed_umos)} 条删除失败", "success_count": success_count, "failed_umos": failed_umos, - } + }, ) .to_json() ) - else: - return ( - Response() - .ok({"message": message, "success_count": success_count}) - .to_json() - ) + return ( + Response() + .ok({"message": message, "success_count": success_count}) + .to_json() + ) except Exception as e: logger.error(f"批量删除会话规则失败: {e!s}") return Response().error(f"批量删除会话规则失败: {e!s}").to_json() @@ -373,7 +370,7 @@ class SessionManagementRoute(Route): result = await session.execute( select(ConversationV2.user_id) .distinct() - .order_by(ConversationV2.user_id) + .order_by(ConversationV2.user_id), ) umos = [row[0] for row in result.fetchall()] return Response().ok({"umos": umos}).to_json() @@ -397,18 +394,16 @@ class SessionManagementRoute(Route): search = request.args.get("search", "", type=str).strip() message_type = request.args.get("message_type", "all", type=str) platform = request.args.get("platform", "", type=str) - if page < 1: - page = 1 + page = max(page, 1) if page_size < 1: page_size = 20 - if page_size > 100: - page_size = 100 + page_size = min(page_size, 100) async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( select(ConversationV2.user_id) .distinct() - .order_by(ConversationV2.user_id) + .order_by(ConversationV2.user_id), ) all_umos = [row[0] for row in result.fetchall()] umo_rules, _ = await self._get_umo_rules(page=1, page_size=99999, search="") @@ -470,7 +465,7 @@ class SessionManagementRoute(Route): "chat_provider": rules.get(chat_provider_key), "tts_provider": rules.get(tts_provider_key), "stt_provider": rules.get(stt_provider_key), - } + }, ) total = len(umos_with_status) start_idx = (page - 1) * page_size @@ -506,7 +501,7 @@ class SessionManagementRoute(Route): "available_chat_providers": available_chat_providers, "available_tts_providers": available_tts_providers, "available_stt_providers": available_stt_providers, - } + }, ) .to_json() ) @@ -553,7 +548,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] if scope == "group": @@ -611,7 +606,7 @@ class SessionManagementRoute(Route): "success_count": success_count, "failed_count": len(failed_umos), "failed_umos": failed_umos, - } + }, ) .to_json() ) @@ -667,7 +662,7 @@ class SessionManagementRoute(Route): async with self.db_helper.get_db() as session: session: AsyncSession result = await session.execute( - select(ConversationV2.user_id).distinct() + select(ConversationV2.user_id).distinct(), ) all_umos = [row[0] for row in result.fetchall()] if scope == "group": @@ -710,7 +705,7 @@ class SessionManagementRoute(Route): "success_count": success_count, "failed_count": len(failed_umos), "failed_umos": failed_umos, - } + }, ) .to_json() ) @@ -738,7 +733,7 @@ class SessionManagementRoute(Route): "name": group_data.get("name", ""), "umos": group_data.get("umos", []), "umo_count": len(group_data.get("umos", [])), - } + }, ) return Response().ok({"groups": groups_list}).to_json() except Exception as e: @@ -770,7 +765,7 @@ class SessionManagementRoute(Route): "umos": umos, "umo_count": len(umos), }, - } + }, ) .to_json() ) @@ -816,7 +811,7 @@ class SessionManagementRoute(Route): "umos": group["umos"], "umo_count": len(group["umos"]), }, - } + }, ) .to_json() ) diff --git a/astrbot/dashboard/routes/skills.py b/astrbot/dashboard/routes/skills.py index c10074a6e..49323b581 100644 --- a/astrbot/dashboard/routes/skills.py +++ b/astrbot/dashboard/routes/skills.py @@ -95,17 +95,17 @@ class SkillsRoute(Route): if not endpoint or not access_token: raise ValueError( "Shipyard Neo endpoint or access token not configured. " - "Set them in Dashboard or ensure Bay's credentials.json is accessible." + "Set them in Dashboard or ensure Bay's credentials.json is accessible.", ) return endpoint, access_token async def _delete_neo_release( - self, client: Any, release_id: str, reason: str | None + self, client: Any, release_id: str, reason: str | None, ): return await client.skills.delete_release(release_id, reason=reason) async def _delete_neo_candidate( - self, client: Any, candidate_id: str, reason: str | None + self, client: Any, candidate_id: str, reason: str | None, ): return await client.skills.delete_candidate(candidate_id, reason=reason) @@ -134,12 +134,12 @@ class SkillsRoute(Route): async def get_skills(self): try: provider_settings = self.core_lifecycle.astrbot_config.get( - "provider_settings", {} + "provider_settings", {}, ) runtime = provider_settings.get("computer_use_runtime", "local") skill_mgr = SkillManager() skills = skill_mgr.list_skills( - active_only=False, runtime=runtime, show_sandbox_path=False + active_only=False, runtime=runtime, show_sandbox_path=False, ) return ( Response() @@ -148,7 +148,7 @@ class SkillsRoute(Route): "skills": [asdict(skill) for skill in skills], "runtime": runtime, "sandbox_cache": skill_mgr.get_sandbox_skills_cache_status(), - } + }, ) .to_json() ) @@ -183,12 +183,12 @@ class SkillsRoute(Route): try: try: skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False, skill_name_hint=Path(filename).stem + temp_path, overwrite=False, skill_name_hint=Path(filename).stem, ) except TypeError: # Backward compatibility for callers that do not accept skill_name_hint skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False + temp_path, overwrite=False, ) except Exception: # Keep behavior consistent with previous implementation @@ -248,7 +248,7 @@ class SkillsRoute(Route): { "filename": filename, "error": "Only .zip files are supported", - } + }, ) continue @@ -265,7 +265,7 @@ class SkillsRoute(Route): # Backward compatibility for monkeypatched implementations in tests try: skill_name = skill_mgr.install_skill_from_zip( - temp_path, overwrite=False + temp_path, overwrite=False, ) except FileExistsError: skipped.append( @@ -273,7 +273,7 @@ class SkillsRoute(Route): "filename": filename, "name": Path(filename).stem, "error": "Skill already exists.", - } + }, ) skill_name = None except FileExistsError: @@ -282,7 +282,7 @@ class SkillsRoute(Route): "filename": filename, "name": Path(filename).stem, "error": "Skill already exists.", - } + }, ) skill_name = None @@ -304,7 +304,7 @@ class SkillsRoute(Route): await sync_skills_to_active_sandboxes() except Exception: logger.warning( - "Failed to sync uploaded skills to active sandboxes." + "Failed to sync uploaded skills to active sandboxes.", ) total = len(file_list) @@ -385,7 +385,7 @@ class SkillsRoute(Route): return ( Response() .error( - "Sandbox preset skill cannot be downloaded from local skill files." + "Sandbox preset skill cannot be downloaded from local skill files.", ) .to_json() ) @@ -541,7 +541,7 @@ class SkillsRoute(Route): report=data.get("report"), ) logger.info( - f"[Neo] Candidate evaluated: id={candidate_id}, passed={passed}" + f"[Neo] Candidate evaluated: id={candidate_id}, passed={passed}", ) return Response().ok(_to_jsonable(result)).to_json() @@ -579,13 +579,13 @@ class SkillsRoute(Route): did_sync_to_local = bool(sync_json) if did_sync_to_local: logger.info( - f"[Neo] Stable release synced to local: skill={sync_json.get('local_skill_name', '')}" + f"[Neo] Stable release synced to local: skill={sync_json.get('local_skill_name', '')}", ) if result.get("sync_error"): resp = Response().error( "Stable promote synced failed and has been rolled back. " - f"sync_error={result['sync_error']}" + f"sync_error={result['sync_error']}", ) resp.data = { "release": release_json, @@ -649,7 +649,7 @@ class SkillsRoute(Route): ) logger.info( f"[Neo] Release synced to local: skill={result.local_skill_name}, " - f"release_id={result.release_id}" + f"release_id={result.release_id}", ) return ( Response() @@ -662,7 +662,7 @@ class SkillsRoute(Route): "payload_ref": result.payload_ref, "map_path": result.map_path, "synced_at": result.synced_at, - } + }, ) .to_json() ) diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index efeb0631e..83bdac3cc 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -95,7 +95,7 @@ class StatRoute(Route): "dashboard_version": await get_dashboard_version(), "change_pwd_hint": self.is_default_cred(), "legacy_pwd_hint": is_legacy_dashboard_password( - self.config["dashboard"]["password"] + self.config["dashboard"]["password"], ), "need_migration": need_migration, }, @@ -218,10 +218,10 @@ class StatRoute(Route): local_tz = datetime.now().astimezone().tzinfo or timezone.utc now_local = datetime.now(local_tz) range_start_local = (now_local - timedelta(days=days)).replace( - minute=0, second=0, microsecond=0 + minute=0, second=0, microsecond=0, ) today_start_local = now_local.replace( - hour=0, minute=0, second=0, microsecond=0 + hour=0, minute=0, second=0, microsecond=0, ) query_start_local = min(range_start_local, today_start_local) query_start_utc = query_start_local.astimezone(timezone.utc) @@ -233,7 +233,7 @@ class StatRoute(Route): ProviderStat.agent_type == "internal", ProviderStat.created_at >= query_start_utc, ) - .order_by(col(ProviderStat.created_at).asc()) + .order_by(col(ProviderStat.created_at).asc()), ) records = result.scalars().all() @@ -244,7 +244,7 @@ class StatRoute(Route): bucket_cursor += timedelta(hours=1) trend_by_provider: dict[str, dict[int, int]] = defaultdict( - lambda: defaultdict(int) + lambda: defaultdict(int), ) total_by_provider: dict[str, int] = defaultdict(int) total_by_umo: dict[str, int] = defaultdict(int) @@ -274,7 +274,7 @@ class StatRoute(Route): if created_at_local >= range_start_local: bucket_local = created_at_local.replace( - minute=0, second=0, microsecond=0 + minute=0, second=0, microsecond=0, ) bucket_ts = int(bucket_local.timestamp() * 1000) trend_by_provider[provider_id][bucket_ts] += token_total @@ -393,7 +393,7 @@ class StatRoute(Route): "today_total_calls": today_total_calls, "today_by_model": today_by_model_data, "today_by_provider": today_by_provider_data, - } + }, ) .__dict__ ) diff --git a/astrbot/dashboard/routes/subagent.py b/astrbot/dashboard/routes/subagent.py index 655203359..f80164ca3 100644 --- a/astrbot/dashboard/routes/subagent.py +++ b/astrbot/dashboard/routes/subagent.py @@ -114,7 +114,7 @@ class SubAgentRoute(Route): "parameters": tool.parameters, "active": tool.active, "handler_module_path": tool_handler_module_path, - } + }, ) return jsonify(Response().ok(data=tools_dict).to_json()) except Exception as e: diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py index 3a63e7bcd..35784ceaa 100644 --- a/astrbot/dashboard/routes/t2i.py +++ b/astrbot/dashboard/routes/t2i.py @@ -13,7 +13,7 @@ from .route import Response, Route, RouteContext class T2iRoute(Route): def __init__( - self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle + self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) self.core_lifecycle = core_lifecycle diff --git a/astrbot/dashboard/routes/tools.py b/astrbot/dashboard/routes/tools.py index 62f2e1102..6eebb881e 100644 --- a/astrbot/dashboard/routes/tools.py +++ b/astrbot/dashboard/routes/tools.py @@ -16,7 +16,6 @@ DEFAULT_MCP_CONFIG: dict[str, Any] = {"mcpServers": {}} class EmptyMcpServersError(ValueError): """Raised when mcpServers is empty.""" - pass def _extract_mcp_server_config(mcp_servers_value: object) -> dict: @@ -24,6 +23,7 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: Raises: ValueError: Invalid configuration + """ if not isinstance(mcp_servers_value, dict): raise ValueError("mcpServers must be a JSON object") @@ -32,14 +32,14 @@ def _extract_mcp_server_config(mcp_servers_value: object) -> dict: extracted = list(mcp_servers_value.values())[0] if not isinstance(extracted, dict): raise ValueError( - "Invalid mcpServers format. Ensure each key in mcpServers is a server name, and each value is an object containing fields like command/url." + "Invalid mcpServers format. Ensure each key in mcpServers is a server name, and each value is an object containing fields like command/url.", ) return extracted 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 @@ -77,13 +77,13 @@ class ToolsRoute(Route): mcp_servers = config.get("mcpServers", {}) if not isinstance(mcp_servers, dict): logger.warning( - f"Invalid MCP server config type: {type(mcp_servers).__name__}. Expected object/dict; skipped all MCP servers." + f"Invalid MCP server config type: {type(mcp_servers).__name__}. Expected object/dict; skipped all MCP servers.", ) mcp_servers = {} for name, server_config in mcp_servers.items(): if not isinstance(server_config, dict): logger.warning( - f"Invalid config for MCP server '{name}' (type: {type(server_config).__name__}); skipped." + f"Invalid config for MCP server '{name}' (type: {type(server_config).__name__}); skipped.", ) continue server_info = { @@ -120,7 +120,7 @@ class ToolsRoute(Route): if key == "mcpServers": try: server_config = _extract_mcp_server_config( - server_data["mcpServers"] + server_data["mcpServers"], ) except ValueError as e: return Response().error(f"{e!s}").to_json() @@ -145,7 +145,7 @@ 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) @@ -196,7 +196,7 @@ class ToolsRoute(Route): if key == "mcpServers": try: server_config = _extract_mcp_server_config( - server_data["mcpServers"] + server_data["mcpServers"], ) except ValueError as e: return Response().error(f"{e!s}").to_json() @@ -221,13 +221,13 @@ 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 ( Response() .error( - f"Timed out while disabling MCP server {old_name} before enabling: {e!s}" + f"Timed out while disabling MCP server {old_name} before enabling: {e!s}", ) .to_json() ) @@ -236,13 +236,13 @@ class ToolsRoute(Route): return ( Response() .error( - f"Failed to disable MCP server {old_name} before enabling: {e!s}" + f"Failed to disable MCP server {old_name} before enabling: {e!s}", ) .to_json() ) 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 ( @@ -333,7 +333,7 @@ class ToolsRoute(Route): return ( Response() .error( - "Only one MCP server configuration can be tested at a time" + "Only one MCP server configuration can be tested at a time", ) .to_json() ) @@ -377,7 +377,7 @@ class ToolsRoute(Route): origin = "internal" origin_name = "AstrBot" elif getattr(tool, "handler_module_path", None) and star_map.get( - getattr(tool, "handler_module_path", None) + getattr(tool, "handler_module_path", None), ): handler_path = getattr(tool, "handler_module_path", None) star = star_map[handler_path] diff --git a/astrbot/dashboard/routes/tui_chat.py b/astrbot/dashboard/routes/tui_chat.py index eeb339083..f174db9f7 100644 --- a/astrbot/dashboard/routes/tui_chat.py +++ b/astrbot/dashboard/routes/tui_chat.py @@ -124,7 +124,7 @@ 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 +148,7 @@ 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() @@ -160,7 +160,7 @@ class TUIChatRoute(Route): "attachment_id": attachment.attachment_id, "filename": filename, "type": attach_type, - } + }, ) .to_json() ) @@ -168,11 +168,11 @@ 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 +262,7 @@ 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 @@ -307,7 +307,7 @@ class TUIChatRoute(Route): tool_calls[tool_call.get("id")] = tool_call if accumulated_text: accumulated_parts.append( - {"type": "plain", "text": accumulated_text} + {"type": "plain", "text": accumulated_text}, ) accumulated_text = "" elif chain_type == "tool_call_result": @@ -320,7 +320,7 @@ class TUIChatRoute(Route): { "type": "tool_call", "tool_calls": [tool_calls[tc_id]], - } + }, ) tool_calls.pop(tc_id, None) elif chain_type == "reasoning": @@ -332,27 +332,27 @@ 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) if msg_type == "end": break - elif streaming and msg_type == "complete" or not streaming: + elif (streaming and msg_type == "complete") or not streaming: if ( chain_type == "tool_call" or chain_type == "tool_call_result" @@ -372,7 +372,7 @@ class TUIChatRoute(Route): "data": { "id": saved_record.id, "created_at": to_utc_isoformat( - saved_record.created_at + saved_record.created_at, ), }, } @@ -402,7 +402,7 @@ class TUIChatRoute(Route): "enable_streaming": enable_streaming, "message_id": message_id, }, - ) + ), ) message_parts_for_storage = strip_message_parts_path_fields(message_parts) mgr = self.platform_history_mgr @@ -469,13 +469,13 @@ 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 if router is None: logger.warning( - "UMOP config router not available during session cleanup" + "UMOP config router not available during session cleanup", ) else: await router.delete_route(unified_msg_origin) @@ -539,7 +539,7 @@ class TUIChatRoute(Route): "deleted_count": deleted_count, "failed_count": len(failed_items), "failed_items": failed_items, - } + }, ) .to_json() ) @@ -568,7 +568,7 @@ class TUIChatRoute(Route): await anyio.Path(attachment.path).unlink() except OSError as e: logger.warning( - f"Failed to delete attachment file {attachment.path}: {e}" + f"Failed to delete attachment file {attachment.path}: {e}", ) except Exception as e: logger.warning(f"Failed to get attachments: {e}") @@ -581,7 +581,7 @@ 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() @@ -589,7 +589,7 @@ class TUIChatRoute(Route): data={ "session_id": session.session_id, "platform_id": session.platform_id, - } + }, ) .to_json() ) @@ -617,7 +617,7 @@ class TUIChatRoute(Route): "is_group": session.is_group, "created_at": to_utc_isoformat(session.created_at), "updated_at": to_utc_isoformat(session.updated_at), - } + }, ) return Response().ok(data=sessions_data).to_json() @@ -630,12 +630,12 @@ 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 +666,6 @@ 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/routes/util.py b/astrbot/dashboard/routes/util.py index 7f99c4298..8c92da1cf 100644 --- a/astrbot/dashboard/routes/util.py +++ b/astrbot/dashboard/routes/util.py @@ -17,7 +17,6 @@ def get_schema_item(schema: dict | None, key_path: str) -> dict | None: - 扁平 schema(直接 key 命中) - 嵌套 object schema({type: "object", items: {...}}) """ - if not isinstance(schema, dict) or not key_path: return None if key_path in schema: @@ -44,7 +43,6 @@ def sanitize_filename(name: str) -> str: - 将路径分隔符替换为下划线 - 拒绝空字符串 / "." / ".." """ - cleaned = os.path.basename(name).strip() if not cleaned or cleaned in {".", ".."}: return "" @@ -59,7 +57,6 @@ def sanitize_path_segment(segment: str) -> str: 仅保留 [A-Za-z0-9_-],其余替换为 "_" """ - cleaned = [] for ch in segment: if ( @@ -81,14 +78,12 @@ def sanitize_path_segment(segment: str) -> str: def config_key_to_folder(key_path: str) -> str: """将 dot-path 的配置 key 转成稳定的文件夹路径。""" - parts = [sanitize_path_segment(p) for p in key_path.split(".") if p] return "/".join(parts) if parts else "_" def normalize_rel_path(rel_path: str | None) -> str | None: """规范化用户传入的相对路径,并阻止路径穿越。""" - if not isinstance(rel_path, str): return None rel = rel_path.replace("\\", "/").lstrip("/") diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 6fc2751ee..f6c5fb8ff 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -100,7 +100,7 @@ _RUNTIME_FAILED_RECOVERY_ENDPOINT_PREFIXES = ( APP: Quart _ENV_PLACEHOLDER_RE = re.compile( - r"\$(?:\{(?P[A-Za-z_][A-Za-z0-9_]*)(?::-(?P[^}]*))?\}|(?P[A-Za-z_][A-Za-z0-9_]*))" + r"\$(?:\{(?P[A-Za-z_][A-Za-z0-9_]*)(?::-(?P[^}]*))?\}|(?P[A-Za-z_][A-Za-z0-9_]*))", ) @@ -128,7 +128,7 @@ def _expand_env_placeholders(value: str, field_name: str) -> str: if missing_vars: missing = ", ".join(sorted(set(missing_vars))) raise ValueError( - f"Unresolved environment variable(s) in dashboard {field_name}: {missing}" + f"Unresolved environment variable(s) in dashboard {field_name}: {missing}", ) return expanded @@ -212,7 +212,7 @@ class AstrBotDashboard: if self.enable_webui and not (Path(self.data_path) / "index.html").exists(): logger.warning( f"前端未内置或未初始化 (index.html missing in {self.data_path}), " - "回退到仅启动后端. 请访问在线面板: dash.astrbot.men" + "回退到仅启动后端. 请访问在线面板: dash.astrbot.men", ) self.enable_webui = False self._webui_fallback = True @@ -319,7 +319,7 @@ 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 +368,7 @@ 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: @@ -635,7 +635,7 @@ class AstrBotDashboard: async def run(self) -> None: if self._webui_fallback: logger.warning( - "前端未内置或未初始化, 回退到仅启动后端. 请访问在线面板: dash.astrbot.men" + "前端未内置或未初始化, 回退到仅启动后端. 请访问在线面板: dash.astrbot.men", ) elif not self.enable_webui: logger.warning("前端已禁用, 请访问在线面板: dash.astrbot.men") @@ -783,11 +783,10 @@ class AstrBotDashboard: display_url = f"{scheme}://{ip}:{port}" parts.append(f" ➜ 网络: {display_url}\n") + elif ":" in host: + parts.append(f" ➜ 指定监听: {scheme}://[{host}]:{port}\n") else: - if ":" in host: - parts.append(f" ➜ 指定监听: {scheme}://[{host}]:{port}\n") - else: - parts.append(f" ➜ 指定监听: {scheme}://{host}:{port}\n") + parts.append(f" ➜ 指定监听: {scheme}://{host}:{port}\n") if enable_webui: parts.append(" ➜ 默认用户名和密码: astrbot\n") @@ -795,7 +794,7 @@ class AstrBotDashboard: if not local_ips: parts.append( - "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n" + "可在 data/cmd_config.json 中配置 dashboard.host 以便远程访问。\n", ) logger.info("".join(parts)) diff --git a/astrbot/dashboard/utils.py b/astrbot/dashboard/utils.py index f1c59e954..c240e9f20 100644 --- a/astrbot/dashboard/utils.py +++ b/astrbot/dashboard/utils.py @@ -1,18 +1,14 @@ import base64 import traceback from io import BytesIO -from typing import TYPE_CHECKING from astrbot.api import logger from astrbot.core.knowledge_base.kb_helper import KBHelper from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager -if TYPE_CHECKING: - from astrbot.core.db.vec_db.faiss_impl import FaissVecDB - 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 可视化图片 @@ -35,7 +31,7 @@ async def generate_tsne_visualization( from sklearn.manifold import TSNE except ImportError as e: raise Exception( - "缺少必要的库以生成 t-SNE 可视化。请安装 matplotlib 和 scikit-learn: {e}" + "缺少必要的库以生成 t-SNE 可视化。请安装 matplotlib 和 scikit-learn: {e}", ) from e try: kb_helper: KBHelper | None = None diff --git a/main.py b/main.py index 5c0a2af70..6afb3b9ff 100644 --- a/main.py +++ b/main.py @@ -28,7 +28,7 @@ if __name__ == "__main__": "不推荐通过uv run main.py的方式运行" "推荐使用: uv tool install -e . --force 将astrbot安装至当前环境,直接通过astrbot run命令启动," "后续修改代码可及时生效,无需再次安装,无需担心导入问题,并且支持更加完备的命令行功能." - "main.py仅作兼容保留." + "main.py仅作兼容保留.", ) # 只使用一次 asyncio.run() asyncio.run(main_async(args.webui_dir, log_broker=log_broker)) diff --git a/types/faiss/__init__.pyi b/types/faiss/__init__.pyi index bdb0e8d44..299ed7718 100644 --- a/types/faiss/__init__.pyi +++ b/types/faiss/__init__.pyi @@ -25,17 +25,17 @@ class Index: @overload def reconstruct(self, key: int, x: np.ndarray) -> None: ... def reconstruct( - self, key: int, x: np.ndarray | None = ... + self, key: int, x: np.ndarray | None = ..., ) -> np.ndarray | None: ... @overload def reconstruct_n(self, n0: int, ni: int) -> np.ndarray: ... @overload def reconstruct_n(self, n0: int, ni: int, x: np.ndarray) -> None: ... def reconstruct_n( - self, n0: int = ..., ni: int = ..., x: np.ndarray | None = ... + self, n0: int = ..., ni: int = ..., x: np.ndarray | None = ..., ) -> np.ndarray | None: ... def range_search( - self, x: np.ndarray, thresh: float, *, params: Any = ... + self, x: np.ndarray, thresh: float, *, params: Any = ..., ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... def add_sa_codes(self, codes: np.ndarray, ids: np.ndarray | None = ...) -> None: ... def sa_encode(self, x: np.ndarray) -> np.ndarray: ... diff --git a/types/readability.pyi b/types/readability.pyi index b46e5ab5f..7dd5760d4 100644 --- a/types/readability.pyi +++ b/types/readability.pyi @@ -17,6 +17,6 @@ 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: ...