diff --git a/astrbot/cli/commands/cmd_init.py b/astrbot/cli/commands/cmd_init.py index 993995a6..1b9e9590 100644 --- a/astrbot/cli/commands/cmd_init.py +++ b/astrbot/cli/commands/cmd_init.py @@ -1,5 +1,4 @@ -import asyncio - +import anyio import click from filelock import FileLock, Timeout @@ -47,7 +46,7 @@ def init() -> None: try: with lock.acquire(): - asyncio.run(initialize_astrbot(astrbot_root)) + anyio.run(initialize_astrbot, astrbot_root) except Timeout: raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 9333f1b8..c5905c79 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -1,16 +1,16 @@ -import asyncio import os import sys import traceback from pathlib import Path +import anyio import click from filelock import FileLock, Timeout from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root -async def run_astrbot(astrbot_root: Path): +async def run_astrbot(astrbot_root: Path) -> None: """运行 AstrBot""" from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core.initial_loader import InitialLoader @@ -53,7 +53,7 @@ def run(reload: bool, port: str) -> None: lock_file = astrbot_root / "astrbot.lock" lock = FileLock(lock_file, timeout=5) with lock.acquire(): - asyncio.run(run_astrbot(astrbot_root)) + anyio.run(run_astrbot, astrbot_root) except KeyboardInterrupt: click.echo("AstrBot 已关闭...") except Timeout: diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index 457a4ab3..62bc0636 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -271,7 +271,7 @@ class SQLiteDatabase(BaseDatabase): async with session.begin(): await session.execute( delete(ConversationV2).where( - col(ConversationV2.user_id) == user_id + col(ConversationV2.user_id) == user_id, ), ) diff --git a/astrbot/core/event_bus.py b/astrbot/core/event_bus.py index 749df753..62a6917b 100644 --- a/astrbot/core/event_bus.py +++ b/astrbot/core/event_bus.py @@ -1,4 +1,5 @@ -"""事件总线, 用于处理事件的分发和处理 +"""事件总线, 用于处理事件的分发和处理. + 事件总线是一个异步队列, 用于接收各种消息事件, 并将其发送到Scheduler调度器进行处理 其中包含了一个无限循环的调度函数, 用于从事件队列中获取新的事件, 并创建一个新的异步任务来执行管道调度器的处理逻辑 @@ -10,8 +11,8 @@ class: 2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑 """ -import asyncio -from asyncio import Queue +import anyio +from anyio.streams.memory import MemoryObjectReceiveStream from astrbot.core import logger from astrbot.core.astrbot_config_mgr import AstrBotConfigManager @@ -25,28 +26,29 @@ class EventBus: def __init__( self, - event_queue: Queue, + event_queue: MemoryObjectReceiveStream[AstrMessageEvent], pipeline_scheduler_mapping: dict[str, PipelineScheduler], - astrbot_config_mgr: AstrBotConfigManager = None, - ): + astrbot_config_mgr: AstrBotConfigManager | None = None, + ) -> None: self.event_queue = event_queue # 事件队列 # abconf uuid -> scheduler self.pipeline_scheduler_mapping = pipeline_scheduler_mapping self.astrbot_config_mgr = astrbot_config_mgr - async def dispatch(self): + async def dispatch(self) -> None: while True: - event: AstrMessageEvent = await self.event_queue.get() + event: AstrMessageEvent = await self.event_queue.receive() conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin) self._print_event(event, conf_info["name"]) scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"]) - asyncio.create_task(scheduler.execute(event)) + anyio.create_task(scheduler.execute(event)) - def _print_event(self, event: AstrMessageEvent, conf_name: str): + def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None: """用于记录事件信息 Args: - event (AstrMessageEvent): 事件对象 + event: 事件对象 + conf_name: 配置名称 """ # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 diff --git a/astrbot/core/file_token_service.py b/astrbot/core/file_token_service.py index ea97759c..cd3f9dca 100644 --- a/astrbot/core/file_token_service.py +++ b/astrbot/core/file_token_service.py @@ -1,17 +1,18 @@ -import asyncio import os import platform import time import uuid from urllib.parse import unquote, urlparse +import anyio + class FileTokenService: """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" - def __init__(self, default_timeout: float = 300): - self.lock = asyncio.Lock() - self.staged_files = {} # token: (file_path, expire_time) + def __init__(self, default_timeout: float = 300) -> None: + self.lock = anyio.Lock() + self.staged_files: dict = {} # token: (file_path, expire_time) self.default_timeout = default_timeout async def _cleanup_expired_tokens(self): diff --git a/astrbot/core/pipeline/rate_limit_check/stage.py b/astrbot/core/pipeline/rate_limit_check/stage.py index 64e21dd7..5e23c2b1 100644 --- a/astrbot/core/pipeline/rate_limit_check/stage.py +++ b/astrbot/core/pipeline/rate_limit_check/stage.py @@ -1,8 +1,9 @@ -import asyncio from collections import defaultdict, deque from collections.abc import AsyncGenerator from datetime import datetime, timedelta +import anyio + from astrbot.core import logger from astrbot.core.config.astrbot_config import RateLimitStrategy from astrbot.core.platform.astr_message_event import AstrMessageEvent @@ -19,11 +20,11 @@ class RateLimitStage(Stage): 如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。 """ - def __init__(self): + def __init__(self) -> None: # 存储每个会话的请求时间队列 self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque) # 为每个会话设置一个锁,避免并发冲突 - self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock) + self.locks: defaultdict[str, anyio.Lock] = defaultdict(anyio.Lock) # 限流参数 self.rate_limit_count: int = 0 self.rate_limit_time: timedelta = timedelta(0) @@ -74,7 +75,7 @@ class RateLimitStage(Stage): logger.info( f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", ) - await asyncio.sleep(stall_duration) + await anyio.sleep(stall_duration) now = datetime.now() case RateLimitStrategy.DISCARD.value: logger.info( diff --git a/astrbot/core/platform/sources/slack/client.py b/astrbot/core/platform/sources/slack/client.py index 0411f73a..2a4882a2 100644 --- a/astrbot/core/platform/sources/slack/client.py +++ b/astrbot/core/platform/sources/slack/client.py @@ -1,10 +1,10 @@ -import asyncio import hashlib import hmac import json import logging from collections.abc import Callable +import anyio from quart import Quart, Response, request from slack_sdk.socket_mode.aiohttp import SocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest @@ -40,7 +40,7 @@ class SlackWebhookClient: logging.getLogger("quart.app").setLevel(logging.WARNING) logging.getLogger("quart.serving").setLevel(logging.WARNING) - self.shutdown_event = asyncio.Event() + self.shutdown_event = anyio.Event() def _setup_routes(self): """设置路由""" diff --git a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py index 6c448a97..5b39c08a 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_api.py @@ -1,4 +1,5 @@ -"""企业微信智能机器人 API 客户端 +"""企业微信智能机器人 API 客户端. + 处理消息加密解密、API 调用等 """ 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 35acd906..1c6481e9 100644 --- a/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py +++ b/astrbot/core/platform/sources/wecom_ai_bot/wecomai_server.py @@ -2,10 +2,10 @@ 处理企业微信智能机器人的 HTTP 回调请求 """ -import asyncio from collections.abc import Callable from typing import Any +import anyio import quart from astrbot.api import logger @@ -41,7 +41,7 @@ class WecomAIBotServer: self.app = quart.Quart(__name__) self._setup_routes() - self.shutdown_event = asyncio.Event() + self.shutdown_event = anyio.Event() def _setup_routes(self): """设置 Quart 路由""" diff --git a/astrbot/core/provider/func_tool_manager.py b/astrbot/core/provider/func_tool_manager.py index b3ef1ed5..32b68e35 100644 --- a/astrbot/core/provider/func_tool_manager.py +++ b/astrbot/core/provider/func_tool_manager.py @@ -7,6 +7,7 @@ from collections.abc import Awaitable, Callable from typing import Any import aiohttp +import anyio from astrbot import logger from astrbot.core import sp @@ -98,7 +99,7 @@ class FunctionToolManager: self.func_list: list[FuncTool] = [] self.mcp_client_dict: dict[str, MCPClient] = {} """MCP 服务列表""" - self.mcp_client_event: dict[str, asyncio.Event] = {} + self.mcp_client_event: dict[str, anyio.Event] = {} def empty(self) -> bool: return len(self.func_list) == 0 @@ -206,7 +207,7 @@ class FunctionToolManager: for name in mcp_server_json_obj: cfg = mcp_server_json_obj[name] if cfg.get("active", True): - event = asyncio.Event() + event = anyio.Event() asyncio.create_task( self._init_mcp_client_task_wrapper(name, cfg, event), ) @@ -216,7 +217,7 @@ class FunctionToolManager: self, name: str, cfg: dict, - event: asyncio.Event, + event: anyio.Event, ready_future: asyncio.Future | None = None, ) -> None: """初始化 MCP 客户端的包装函数,用于捕获异常""" @@ -310,7 +311,7 @@ class FunctionToolManager: self, name: str, config: dict, - event: asyncio.Event | None = None, + event: anyio.Event | None = None, ready_future: asyncio.Future | None = None, timeout: int = 30, ) -> None: @@ -319,7 +320,7 @@ class FunctionToolManager: Args: name (str): The name of the MCP server. config (dict): Configuration for the MCP server. - event (asyncio.Event): Event to signal when the MCP client is ready. + event (anyio.Event): Event to signal when the MCP client is ready. ready_future (asyncio.Future): Future to signal when the MCP client is ready. timeout (int): Timeout for the initialization. @@ -329,7 +330,7 @@ class FunctionToolManager: """ if not event: - event = asyncio.Event() + event = anyio.Event() if not ready_future: ready_future = asyncio.Future() if name in self.mcp_client_dict: diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py index e1c2efb2..4d731507 100755 --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -96,7 +96,7 @@ class CommandGroupFilter(HandlerFilter): prefix + "│ ", event=event, cfg=cfg, - ) + ), ) return "".join(parts) diff --git a/astrbot/core/umop_config_router.py b/astrbot/core/umop_config_router.py index 07858da5..6c0e69d3 100644 --- a/astrbot/core/umop_config_router.py +++ b/astrbot/core/umop_config_router.py @@ -30,7 +30,9 @@ class UmopConfigRouter: if len(p1_ls) != 3 or len(p2_ls) != 3: return False # 非法格式 - return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls)) + return all( + p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls, strict=False) + ) def get_conf_id_for_umop(self, umo: str) -> str | None: """根据 UMO 获取对应的配置文件 ID diff --git a/astrbot/core/utils/session_waiter.py b/astrbot/core/utils/session_waiter.py index 33b7cb17..21209ed5 100644 --- a/astrbot/core/utils/session_waiter.py +++ b/astrbot/core/utils/session_waiter.py @@ -1,5 +1,7 @@ """会话控制""" +from __future__ import annotations + import abc import asyncio import copy @@ -8,11 +10,13 @@ import time from collections.abc import Awaitable, Callable from typing import Any +import anyio + import astrbot.core.message.components as Comp from astrbot.core.platform import AstrMessageEvent -USER_SESSIONS: dict[str, "SessionWaiter"] = {} # 存储 SessionWaiter 实例 -FILTERS: list["SessionFilter"] = [] # 存储 SessionFilter 实例 +USER_SESSIONS: dict[str, SessionWaiter] = {} # 存储 SessionWaiter 实例 +FILTERS: list[SessionFilter] = [] # 存储 SessionFilter 实例 class SessionController: @@ -20,16 +24,16 @@ class SessionController: def __init__(self): self.future = asyncio.Future() - self.current_event: asyncio.Event = None + self.current_event: anyio.Event | None = None """当前正在等待的所用的异步事件""" - self.ts: float = None + self.ts: float | None = None """上次保持(keep)开始时的时间""" - self.timeout: float | int = None + self.timeout: float | int | None = None """上次保持(keep)开始时的超时时间""" self.history_chains: list[list[Comp.BaseMessageComponent]] = [] - def stop(self, error: Exception = None): + def stop(self, error: Exception | None = None): """立即结束这个会话""" if not self.future.done(): if error: @@ -53,7 +57,9 @@ class SessionController: self.stop() return else: - left_timeout = self.timeout - (new_ts - self.ts) + current_timeout = self.timeout if self.timeout is not None else 0 + current_ts = self.ts if self.ts is not None else new_ts + left_timeout = current_timeout - (new_ts - current_ts) timeout = left_timeout + timeout if timeout <= 0: self.stop() @@ -62,18 +68,19 @@ class SessionController: if self.current_event and not self.current_event.is_set(): self.current_event.set() # 通知上一个 keep 结束 - new_event = asyncio.Event() + new_event = anyio.Event() self.ts = new_ts self.current_event = new_event self.timeout = timeout - asyncio.create_task(self._holding(new_event, timeout)) # 开始新的 keep + anyio.create_task(self._holding(new_event, timeout)) # 开始新的 keep - async def _holding(self, event: asyncio.Event, timeout: int): + async def _holding(self, event: anyio.Event, timeout_seconds: float): """等待事件结束或超时""" try: - await asyncio.wait_for(event.wait(), timeout) - except asyncio.TimeoutError: + with anyio.move_on_after(timeout_seconds): + await event.wait() + except TimeoutError: if not self.future.done(): self.future.set_exception(TimeoutError("等待超时")) except asyncio.CancelledError: @@ -105,10 +112,12 @@ class SessionWaiter: session_filter: SessionFilter, session_id: str, record_history_chains: bool, - ): + ) -> None: self.session_id = session_id self.session_filter = session_filter - self.handler: Callable[[str], Awaitable[Any]] | None = None # 处理函数 + self.handler: ( + Callable[[SessionController, AstrMessageEvent], Awaitable[Any]] | None + ) = None # 处理函数 self.session_controller = SessionController() self.record_history_chains = record_history_chains @@ -119,15 +128,15 @@ class SessionWaiter: async def register_wait( self, - handler: Callable[[str], Awaitable[Any]], - timeout: int = 30, + handler: Callable[[SessionController, AstrMessageEvent], Awaitable[Any]], + timeout_seconds: int = 30, ) -> Any: """等待外部输入并处理""" self.handler = handler USER_SESSIONS[self.session_id] = self # 开始一个会话保持事件 - self.session_controller.keep(timeout, reset_timeout=True) + self.session_controller.keep(timeout_seconds, reset_timeout=True) try: return await self.session_controller.future @@ -137,7 +146,7 @@ class SessionWaiter: finally: self._cleanup() - def _cleanup(self, error: Exception = None): + def _cleanup(self, error: Exception | None = None): """清理会话""" USER_SESSIONS.pop(self.session_id, None) try: @@ -153,6 +162,10 @@ class SessionWaiter: if not session or session.session_controller.future.done(): return + # 此时 session 不会是 None,因为上面的检查 + if session is None: + return + async with session._lock: if not session.session_controller.future.done(): if session.record_history_chains: @@ -161,7 +174,8 @@ class SessionWaiter: ) try: # TODO: 这里使用 create_task,跟踪 task,防止超时后这里 handler 仍然在执行 - await session.handler(session.session_controller, event) + if session.handler is not None: + await session.handler(session.session_controller, event) except Exception as e: session.session_controller.stop(e) @@ -173,11 +187,13 @@ def session_waiter(timeout: int = 30, record_history_chains: bool = False): :param record_history_chain: 是否自动记录历史消息链。可以通过 controller.get_history_chains() 获取。深拷贝。 """ - def decorator(func: Callable[[str], Awaitable[Any]]): + def decorator( + func: Callable[[SessionController, AstrMessageEvent], Awaitable[Any]], + ): @functools.wraps(func) async def wrapper( event: AstrMessageEvent, - session_filter: SessionFilter = None, + session_filter: SessionFilter | None = None, *args, **kwargs, ): diff --git a/astrbot/dashboard/routes/auth.py b/astrbot/dashboard/routes/auth.py index 4ee0d57d..82ecb02e 100644 --- a/astrbot/dashboard/routes/auth.py +++ b/astrbot/dashboard/routes/auth.py @@ -1,6 +1,6 @@ -import asyncio import datetime +import anyio import jwt from quart import request @@ -44,7 +44,7 @@ class AuthRoute(Route): ) .__dict__ ) - await asyncio.sleep(3) + await anyio.sleep(3) return Response().error("用户名或密码错误").__dict__ async def edit_account(self): diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 5156e14e..e30239f2 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -4,6 +4,7 @@ import os import uuid from contextlib import asynccontextmanager +import anyio from quart import Response as QuartResponse from quart import g, make_response, request @@ -188,8 +189,8 @@ class ChatRoute(Route): try: if not client_disconnected: - await asyncio.sleep(0.05) - except asyncio.CancelledError: + await anyio.sleep(0.05) + except anyio.get_cancelled_exc_class(): logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") client_disconnected = True diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 59b07e47..9c659727 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -817,7 +817,8 @@ class ConfigRoute(Route): cached_token = self._logo_token_cache[cache_key] # 确保platform_default_tmpl[platform.name]存在且为字典 if platform.name not in platform_default_tmpl or not isinstance( - platform_default_tmpl[platform.name], dict + platform_default_tmpl[platform.name], + dict, ): platform_default_tmpl[platform.name] = {} platform_default_tmpl[platform.name]["logo_token"] = cached_token @@ -846,7 +847,8 @@ class ConfigRoute(Route): # 确保platform_default_tmpl[platform.name]存在且为字典 if platform.name not in platform_default_tmpl or not isinstance( - platform_default_tmpl[platform.name], dict + platform_default_tmpl[platform.name], + dict, ): platform_default_tmpl[platform.name] = {} diff --git a/main.py b/main.py index 63ce21d3..16832ff0 100644 --- a/main.py +++ b/main.py @@ -12,6 +12,9 @@ from astrbot.core.initial_loader import InitialLoader from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.io import download_dashboard, get_dashboard_version +# uvloop 仅在非 Windows 平台可用 +backend_options = {"use_uvloop": True} if sys.platform != "win32" else {} + # 将父目录添加到 sys.path sys.path.append(Path(__file__).parent.as_posix()) @@ -94,7 +97,11 @@ if __name__ == "__main__": LogManager.set_queue_handler(logger, log_broker) # 检查仪表板文件 - webui_dir = anyio.run(check_dashboard_files, args.webui_dir) + webui_dir = anyio.run( + check_dashboard_files, + args.webui_dir, + backend_options=backend_options, + ) db = db_helper @@ -103,4 +110,8 @@ if __name__ == "__main__": core_lifecycle = InitialLoader(db, log_broker) core_lifecycle.webui_dir = webui_dir - anyio.run(core_lifecycle.start) + logger.info( + "将按以下异步后端启动 AstrBot: %s", + backend_options if backend_options else "asyncio", + ) + anyio.run(core_lifecycle.start, backend_options=backend_options) diff --git a/packages/astrbot/commands/conversation.py b/packages/astrbot/commands/conversation.py index 9538d8f5..5a4f1421 100644 --- a/packages/astrbot/commands/conversation.py +++ b/packages/astrbot/commands/conversation.py @@ -164,14 +164,14 @@ class ConversationCommands: "%m-%d %H:%M", ) parts.append( - f"{idx}. {conv['name']}({conv['id'][:4]})\n 上次更新:{ts_h}\n" + f"{idx}. {conv['name']}({conv['id'][:4]})\n 上次更新:{ts_h}\n", ) idx += 1 if idx == 1: parts.append("没有找到任何对话。") dify_cid = provider.conversation_ids.get(message.unified_msg_origin, None) parts.append( - f"\n\n用户: {message.unified_msg_origin}\n当前对话: {dify_cid}\n使用 /switch <序号> 切换对话。" + f"\n\n用户: {message.unified_msg_origin}\n当前对话: {dify_cid}\n使用 /switch <序号> 切换对话。", ) ret = "".join(parts) message.set_result(MessageEventResult().message(ret)) @@ -211,7 +211,7 @@ class ConversationCommands: persona_id = persona["name"] title = _titles.get(conv.cid, "新对话") parts.append( - f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n" + f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n", ) global_index += 1 diff --git a/packages/astrbot/commands/provider.py b/packages/astrbot/commands/provider.py index 8db7324e..fdbcfe2b 100644 --- a/packages/astrbot/commands/provider.py +++ b/packages/astrbot/commands/provider.py @@ -136,7 +136,7 @@ class ProviderCommands: curr_model = prov.get_model() or "无" parts.append(f"\n当前模型: [{curr_model}]") parts.append( - "\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。" + "\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。", ) ret = "".join(parts) diff --git a/packages/python_interpreter/main.py b/packages/python_interpreter/main.py index 35a2f269..8385cc5b 100644 --- a/packages/python_interpreter/main.py +++ b/packages/python_interpreter/main.py @@ -9,6 +9,7 @@ from collections import defaultdict import aiodocker import aiohttp +import anyio from astrbot.api import llm_tool, logger, star from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter @@ -291,7 +292,7 @@ class Main(star.Star): self.user_waiting[uid] = time.time() tip = "文件" yield event.plain_result(f"代码执行器: 请在 60s 内上传一个{tip}。") - await asyncio.sleep(60) + await anyio.sleep(60) if uid in self.user_waiting: yield event.plain_result( f"代码执行器: {event.get_sender_name()}/{event.get_sender_id()} 未在规定时间内上传{tip}。", diff --git a/packages/web_searcher/main.py b/packages/web_searcher/main.py index 118ef248..3a8580e5 100644 --- a/packages/web_searcher/main.py +++ b/packages/web_searcher/main.py @@ -2,6 +2,7 @@ import asyncio import random import aiohttp +import anyio from bs4 import BeautifulSoup from readability import Document @@ -26,7 +27,7 @@ class Main(star.Star): def __init__(self, context: star.Context) -> None: self.context = context self.tavily_key_index = 0 - self.tavily_key_lock = asyncio.Lock() + self.tavily_key_lock = anyio.Lock() # 将 str 类型的 key 迁移至 list[str],并保存 cfg = self.context.get_config() diff --git a/pyproject.toml b/pyproject.toml index c83fdf2d..66429ab7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,8 @@ dependencies = [ "jieba>=0.42.1", "markitdown-no-magika[docx,xls,xlsx]>=0.1.2", "xinference-client", + "anyio>=4.11.0", + "uvloop>=0.22.1 ; sys_platform == 'linux'", ] [dependency-groups] @@ -98,8 +100,8 @@ select = [ # "SIM", # flake8-simplify ] ignore = [ - "F403", - "F405", + "F403", + "F405", "E501", "ASYNC230" # TODO: handle ASYNC230 in AstrBot ] @@ -112,6 +114,34 @@ reportMissingImports = false include = ["astrbot","packages"] exclude = ["dashboard", "node_modules", "dist", "data", "tests"] +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false +check_untyped_defs = true +disallow_untyped_decorators = false +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true +show_error_codes = true +ignore_missing_imports = true +explicit_package_bases = true +namespace_packages = true +files = ["astrbot", "packages"] +exclude = [ + "dashboard", + "node_modules", + "dist", + "data", + "tests", + "packages/.*/.*", +] + [tool.hatch.version] source = "uv-dynamic-versioning" @@ -123,4 +153,3 @@ bump = true [build-system] requires = ["hatchling", "uv-dynamic-versioning"] build-backend = "hatchling.build" - diff --git a/requirements.txt b/requirements.txt index e8b3dee3..f6948bf2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,498 @@ -aiocqhttp>=1.4.4 -aiodocker>=0.24.0 -aiohttp>=3.11.18 -aiocqhttp>=1.4.4 -aiodocker>=0.24.0 -aiohttp>=3.11.18 -aiosqlite>=0.21.0 -anthropic>=0.51.0 -apscheduler>=3.11.0 -beautifulsoup4>=4.13.4 -certifi>=2025.4.26 -chardet~=5.1.0 -colorlog>=6.9.0 -cryptography>=44.0.3 -dashscope>=1.23.2 -defusedxml>=0.7.1 -deprecated>=1.2.18 -dingtalk-stream>=0.22.1 -docstring-parser>=0.16 +# This file was autogenerated by uv via the following command: +# uv export --format requirements.txt --no-hashes --no-editable +. +aiocqhttp==1.4.4 + # via astrbot +aiodocker==0.24.0 + # via astrbot +aiofiles==25.1.0 + # via + # astrbot + # quart +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.13.2 + # via + # aiodocker + # astrbot + # dashscope + # dingtalk-stream + # py-cord + # qq-botpy + # xinference-client +aiosignal==1.4.0 + # via aiohttp +aiosqlite==0.21.0 + # via astrbot +annotated-types==0.7.0 + # via pydantic +anthropic==0.72.0 + # via astrbot +anyio==4.11.0 + # via + # anthropic + # astrbot + # google-genai + # httpx + # mcp + # openai + # sse-starlette + # starlette + # watchfiles +apscheduler==3.11.1 + # via + # astrbot + # qq-botpy +argcomplete==3.6.3 + # via commitizen +async-timeout==5.0.1 ; python_full_version < '3.11' + # via aiohttp +attrs==25.4.0 + # via + # aiohttp + # jsonschema + # referencing +audioop-lts==0.2.2 ; python_full_version >= '3.13' + # via astrbot +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' + # via pytest-asyncio +beautifulsoup4==4.14.2 + # via + # astrbot + # markdownify + # markitdown-no-magika +blinker==1.9.0 + # via + # flask + # quart +cachetools==6.2.1 + # via google-auth +certifi==2025.10.5 + # via + # astrbot + # dashscope + # httpcore + # httpx + # requests +cffi==2.0.0 + # via + # cryptography + # silk-python +chardet==5.1.0 + # via + # astrbot + # readability-lxml +charset-normalizer==3.4.4 + # via + # commitizen + # markitdown-no-magika + # requests +click==8.3.0 + # via + # astrbot + # flask + # quart + # uvicorn +cobble==0.1.4 + # via mammoth +colorama==0.4.6 + # via + # click + # colorlog + # commitizen + # pytest + # tqdm +colorlog==6.10.1 + # via astrbot +commitizen==4.9.1 +coverage==7.11.0 + # via pytest-cov +cryptography==46.0.3 + # via + # astrbot + # dashscope +cssselect==1.3.0 + # via readability-lxml +dashscope==1.24.9 + # via astrbot +decli==0.6.3 + # via commitizen +defusedxml==0.7.1 + # via + # astrbot + # markitdown-no-magika +deprecated==1.3.1 + # via + # astrbot + # commitizen +dingtalk-stream==0.24.3 + # via astrbot +distro==1.9.0 + # via + # anthropic + # openai +docstring-parser==0.17.0 + # via + # anthropic + # astrbot +et-xmlfile==2.0.0 + # via openpyxl +exceptiongroup==1.3.0 ; python_full_version < '3.11' + # via + # anyio + # hypercorn + # pytest + # taskgroup faiss-cpu==1.10.0 -filelock>=3.18.0 -google-genai>=1.14.0 -lark-oapi>=1.4.15 -lxml-html-clean>=0.4.2 -mcp>=1.8.0 -openai>=1.78.0 -ormsgpack>=1.9.1 -pillow>=11.2.1 -pip>=25.1.1 -psutil>=5.8.0 -py-cord>=2.6.1 -pydantic~=2.10.3 -pydub>=0.25.1 -pyjwt>=2.10.1 -python-telegram-bot>=22.0 -qq-botpy>=1.2.1 -quart>=0.20.0 -readability-lxml>=0.8.4.1 -silk-python>=0.2.6 -slack-sdk>=3.35.0 -sqlalchemy[asyncio]>=2.0.41 -sqlmodel>=0.0.24 -telegramify-markdown>=0.5.1 -watchfiles>=1.0.5 -websockets>=15.0.1 -wechatpy>=1.8.18 -audioop-lts ; python_full_version >= '3.13' -click>=8.2.1 -pypdf>=6.1.1 -aiofiles>=25.1.0 -rank-bm25>=0.2.2 -jieba>=0.42.1 -markitdown-no-magika[docx,xls,xlsx]>=0.1.2 -xinference-client + # via astrbot +filelock==3.20.0 + # via astrbot +flask==3.1.2 + # via quart +frozenlist==1.8.0 + # via + # aiohttp + # aiosignal +google-auth==2.42.1 + # via google-genai +google-genai==1.47.0 + # via astrbot +greenlet==3.2.4 + # via sqlalchemy +h11==0.16.0 + # via + # httpcore + # hypercorn + # uvicorn + # wsproto +h2==4.3.0 + # via hypercorn +hpack==4.1.0 + # via h2 +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # aiocqhttp + # anthropic + # google-genai + # lark-oapi + # mcp + # openai + # python-telegram-bot +httpx-sse==0.4.3 + # via mcp +hypercorn==0.17.3 + # via quart +hyperframe==6.1.0 + # via h2 +idna==3.11 + # via + # anyio + # httpx + # requests + # yarl +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via + # flask + # quart +jieba==0.42.1 + # via astrbot +jinja2==3.1.6 + # via + # commitizen + # flask + # quart +jiter==0.11.1 + # via + # anthropic + # openai +jsonschema==4.25.1 + # via mcp +jsonschema-specifications==2025.9.1 + # via jsonschema +lark-oapi==1.4.23 + # via astrbot +lxml==6.0.2 + # via + # lxml-html-clean + # markitdown-no-magika + # readability-lxml +lxml-html-clean==0.4.3 + # via + # astrbot + # lxml + # readability-lxml +mammoth==1.11.0 + # via markitdown-no-magika +markdownify==1.2.0 + # via markitdown-no-magika +markitdown-no-magika==0.1.2 + # via astrbot +markupsafe==3.0.3 + # via + # flask + # jinja2 + # quart + # werkzeug +mcp==1.12.4 + # via astrbot +mistletoe==1.4.0 + # via telegramify-markdown +multidict==6.7.0 + # via + # aiohttp + # yarl +numpy==2.2.6 ; python_full_version < '3.11' + # via + # faiss-cpu + # pandas + # rank-bm25 +numpy==2.3.4 ; python_full_version >= '3.11' + # via + # faiss-cpu + # pandas + # rank-bm25 +openai==2.6.1 + # via astrbot +openpyxl==3.1.5 + # via markitdown-no-magika +optionaldict==0.1.2 + # via wechatpy +ormsgpack==1.11.0 + # via astrbot +packaging==25.0 + # via + # commitizen + # faiss-cpu + # pytest +pandas==2.3.3 + # via markitdown-no-magika +pillow==12.0.0 + # via astrbot +pip==25.3 + # via astrbot +pluggy==1.6.0 + # via + # pytest + # pytest-cov +priority==2.0.0 + # via hypercorn +prompt-toolkit==3.0.51 + # via + # commitizen + # questionary +propcache==0.4.1 + # via + # aiohttp + # yarl +psutil==7.1.2 + # via astrbot +py-cord==2.6.1 + # via astrbot +pyasn1==0.6.1 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 + # via google-auth +pycparser==2.23 ; implementation_name != 'PyPy' + # via cffi +pycryptodome==3.23.0 + # via lark-oapi +pydantic==2.10.6 + # via + # anthropic + # astrbot + # google-genai + # mcp + # openai + # pydantic-settings + # sqlmodel + # xinference-client +pydantic-core==2.27.2 + # via pydantic +pydantic-settings==2.11.0 + # via mcp +pydub==0.25.1 + # via astrbot +pygments==2.19.2 + # via pytest +pyjwt==2.10.1 + # via astrbot +pypdf==6.1.3 + # via astrbot +pytest==8.4.2 + # via + # pytest-asyncio + # pytest-cov +pytest-asyncio==1.2.0 +pytest-cov==7.0.0 +python-dateutil==2.9.0.post0 + # via + # pandas + # wechatpy +python-dotenv==1.2.1 + # via pydantic-settings +python-multipart==0.0.20 + # via mcp +python-telegram-bot==22.5 + # via astrbot +pytz==2025.2 + # via pandas +pywin32==311 ; sys_platform == 'win32' + # via mcp +pyyaml==6.0.3 + # via + # commitizen + # qq-botpy +qq-botpy==1.2.1 + # via astrbot +quart==0.20.0 + # via + # aiocqhttp + # astrbot +questionary==2.1.1 + # via commitizen +rank-bm25==0.2.2 + # via astrbot +readability-lxml==0.8.4.1 + # via astrbot +referencing==0.37.0 + # via + # jsonschema + # jsonschema-specifications +requests==2.32.5 + # via + # dashscope + # dingtalk-stream + # google-genai + # lark-oapi + # markitdown-no-magika + # requests-toolbelt + # wechatpy + # xinference-client +requests-toolbelt==1.0.0 + # via lark-oapi +rpds-py==0.28.0 + # via + # jsonschema + # referencing +rsa==4.9.1 + # via google-auth +ruff==0.14.3 +silk-python==0.2.7 + # via astrbot +six==1.17.0 + # via + # markdownify + # python-dateutil + # wechatpy +slack-sdk==3.37.0 + # via astrbot +sniffio==1.3.1 + # via + # anthropic + # anyio + # openai +soupsieve==2.8 + # via beautifulsoup4 +sqlalchemy==2.0.44 + # via + # astrbot + # sqlmodel +sqlmodel==0.0.27 + # via astrbot +sse-starlette==3.0.3 + # via mcp +starlette==0.50.0 + # via mcp +taskgroup==0.2.2 ; python_full_version < '3.11' + # via hypercorn +telegramify-markdown==0.5.2 + # via astrbot +tenacity==9.1.2 + # via google-genai +termcolor==3.2.0 + # via commitizen +tomli==2.3.0 ; python_full_version <= '3.11' + # via + # coverage + # hypercorn + # pytest +tomlkit==0.13.3 + # via commitizen +tqdm==4.67.1 + # via openai +typing-extensions==4.15.0 + # via + # aiosignal + # aiosqlite + # anthropic + # anyio + # beautifulsoup4 + # commitizen + # cryptography + # exceptiongroup + # google-genai + # hypercorn + # multidict + # openai + # py-cord + # pydantic + # pydantic-core + # pypdf + # pytest-asyncio + # referencing + # sqlalchemy + # starlette + # taskgroup + # typing-inspection + # uvicorn + # xinference-client +typing-inspection==0.4.2 + # via pydantic-settings +tzdata==2025.2 + # via + # pandas + # tzlocal +tzlocal==5.3.1 + # via apscheduler +urllib3==2.5.0 + # via requests +uvicorn==0.38.0 ; sys_platform != 'emscripten' + # via mcp +uvloop==0.22.1 ; sys_platform == 'linux' + # via astrbot +watchfiles==1.1.1 + # via astrbot +wcwidth==0.2.14 + # via prompt-toolkit +websocket-client==1.9.0 + # via dashscope +websockets==15.0.1 + # via + # astrbot + # dingtalk-stream + # google-genai + # lark-oapi +wechatpy==1.8.18 + # via astrbot +werkzeug==3.1.3 + # via + # flask + # quart +wrapt==2.0.0 + # via deprecated +wsproto==1.2.0 + # via hypercorn +xinference-client==1.11.0.post1 + # via astrbot +xlrd==2.0.2 + # via markitdown-no-magika +xmltodict==1.0.2 + # via wechatpy +yarl==1.22.0 + # via aiohttp