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: ...