Compare commits

...

5 Commits

Author SHA1 Message Date
LIghtJUNction
381f7f4405 fix: fomat 2025-11-02 19:56:26 +08:00
LIghtJUNction
8f38e748cd Merge branch 'master' into refactor/anyio 2025-11-02 18:47:43 +08:00
LIghtJUNction
f83484a8c0 refactor(astrbot): 注意,此pr完善上一个pr,需要进一步测试
anyio 没有 asyncio的队列,而是使用anyio.streams.memory 进行代替。

BREAKING CHANGE: 可能具有破坏性,稍后我会进行测试
2025-11-02 18:45:31 +08:00
LIghtJUNction
56e3ddd62a refactor(astrbot): asyncio2anyio
还有很多位置没改
2025-11-02 18:36:28 +08:00
LIghtJUNction
80948be41d refactor(main.py): 使用anyio兼容层(默认后端为asyncio) 提高兼容性和可拓展性 2025-11-02 16:15:35 +08:00
29 changed files with 665 additions and 144 deletions

View File

@@ -1,6 +1,6 @@
import asyncio
from pathlib import Path from pathlib import Path
import anyio
import click import click
from filelock import FileLock, Timeout from filelock import FileLock, Timeout
@@ -48,7 +48,7 @@ def init() -> None:
try: try:
with lock.acquire(): with lock.acquire():
asyncio.run(initialize_astrbot(astrbot_root)) anyio.run(initialize_astrbot, astrbot_root)
except Timeout: except Timeout:
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行") raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")

View File

@@ -1,16 +1,16 @@
import asyncio
import os import os
import sys import sys
import traceback import traceback
from pathlib import Path from pathlib import Path
import anyio
import click import click
from filelock import FileLock, Timeout from filelock import FileLock, Timeout
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root 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""" """运行 AstrBot"""
from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.initial_loader import InitialLoader 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_file = astrbot_root / "astrbot.lock"
lock = FileLock(lock_file, timeout=5) lock = FileLock(lock_file, timeout=5)
with lock.acquire(): with lock.acquire():
asyncio.run(run_astrbot(astrbot_root)) anyio.run(run_astrbot, astrbot_root)
except KeyboardInterrupt: except KeyboardInterrupt:
click.echo("AstrBot 已关闭...") click.echo("AstrBot 已关闭...")
except Timeout: except Timeout:

View File

@@ -14,7 +14,8 @@ import os
import threading import threading
import time import time
import traceback import traceback
from asyncio import Queue
import anyio
from astrbot.core import LogBroker, logger, sp from astrbot.core import LogBroker, logger, sp
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
@@ -104,7 +105,9 @@ class AstrBotCoreLifecycle:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# 初始化事件队列 # 初始化事件队列
self.event_queue = Queue() self._event_queue_send, self.event_queue = anyio.create_memory_object_stream[
object
](0)
# 初始化人格管理器 # 初始化人格管理器
self.persona_mgr = PersonaManager(self.db, self.astrbot_config_mgr) self.persona_mgr = PersonaManager(self.db, self.astrbot_config_mgr)
@@ -118,7 +121,9 @@ class AstrBotCoreLifecycle:
) )
# 初始化平台管理器 # 初始化平台管理器
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue) self.platform_manager = PlatformManager(
self.astrbot_config, self._event_queue_send
)
# 初始化对话管理器 # 初始化对话管理器
self.conversation_manager = ConversationManager(self.db) self.conversation_manager = ConversationManager(self.db)
@@ -131,7 +136,7 @@ class AstrBotCoreLifecycle:
# 初始化提供给插件的上下文 # 初始化提供给插件的上下文
self.star_context = Context( self.star_context = Context(
self.event_queue, self._event_queue_send,
self.astrbot_config, self.astrbot_config,
self.db, self.db,
self.provider_manager, self.provider_manager,

View File

@@ -271,7 +271,7 @@ class SQLiteDatabase(BaseDatabase):
async with session.begin(): async with session.begin():
await session.execute( await session.execute(
delete(ConversationV2).where( delete(ConversationV2).where(
col(ConversationV2.user_id) == user_id col(ConversationV2.user_id) == user_id,
), ),
) )

View File

@@ -1,4 +1,5 @@
"""事件总线, 用于处理事件的分发和处理 """事件总线, 用于处理事件的分发和处理.
事件总线是一个异步队列, 用于接收各种消息事件, 并将其发送到Scheduler调度器进行处理 事件总线是一个异步队列, 用于接收各种消息事件, 并将其发送到Scheduler调度器进行处理
其中包含了一个无限循环的调度函数, 用于从事件队列中获取新的事件, 并创建一个新的异步任务来执行管道调度器的处理逻辑 其中包含了一个无限循环的调度函数, 用于从事件队列中获取新的事件, 并创建一个新的异步任务来执行管道调度器的处理逻辑
@@ -10,8 +11,8 @@ class:
2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑 2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑
""" """
import asyncio import anyio
from asyncio import Queue from anyio.streams.memory import MemoryObjectReceiveStream
from astrbot.core import logger from astrbot.core import logger
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
@@ -25,28 +26,29 @@ class EventBus:
def __init__( def __init__(
self, self,
event_queue: Queue, event_queue: MemoryObjectReceiveStream[AstrMessageEvent],
pipeline_scheduler_mapping: dict[str, PipelineScheduler], pipeline_scheduler_mapping: dict[str, PipelineScheduler],
astrbot_config_mgr: AstrBotConfigManager = None, astrbot_config_mgr: AstrBotConfigManager | None = None,
): ) -> None:
self.event_queue = event_queue # 事件队列 self.event_queue = event_queue # 事件队列
# abconf uuid -> scheduler # abconf uuid -> scheduler
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
self.astrbot_config_mgr = astrbot_config_mgr self.astrbot_config_mgr = astrbot_config_mgr
async def dispatch(self): async def dispatch(self) -> None:
while True: 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) conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
self._print_event(event, conf_info["name"]) self._print_event(event, conf_info["name"])
scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"]) 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: Args:
event (AstrMessageEvent): 事件对象 event: 事件对象
conf_name: 配置名称
""" """
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要 # 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要

View File

@@ -1,17 +1,18 @@
import asyncio
import os import os
import platform import platform
import time import time
import uuid import uuid
from urllib.parse import unquote, urlparse from urllib.parse import unquote, urlparse
import anyio
class FileTokenService: class FileTokenService:
"""维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。"""
def __init__(self, default_timeout: float = 300): def __init__(self, default_timeout: float = 300) -> None:
self.lock = asyncio.Lock() self.lock = anyio.Lock()
self.staged_files = {} # token: (file_path, expire_time) self.staged_files: dict = {} # token: (file_path, expire_time)
self.default_timeout = default_timeout self.default_timeout = default_timeout
async def _cleanup_expired_tokens(self): async def _cleanup_expired_tokens(self):

View File

@@ -1,8 +1,9 @@
import asyncio
from collections import defaultdict, deque from collections import defaultdict, deque
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from datetime import datetime, timedelta from datetime import datetime, timedelta
import anyio
from astrbot.core import logger from astrbot.core import logger
from astrbot.core.config.astrbot_config import RateLimitStrategy from astrbot.core.config.astrbot_config import RateLimitStrategy
from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.platform.astr_message_event import AstrMessageEvent
@@ -19,11 +20,11 @@ class RateLimitStage(Stage):
如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。 如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。
""" """
def __init__(self): def __init__(self) -> None:
# 存储每个会话的请求时间队列 # 存储每个会话的请求时间队列
self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque) 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_count: int = 0
self.rate_limit_time: timedelta = timedelta(0) self.rate_limit_time: timedelta = timedelta(0)
@@ -74,7 +75,7 @@ class RateLimitStage(Stage):
logger.info( logger.info(
f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。", f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。",
) )
await asyncio.sleep(stall_duration) await anyio.sleep(stall_duration)
now = datetime.now() now = datetime.now()
case RateLimitStrategy.DISCARD.value: case RateLimitStrategy.DISCARD.value:
logger.info( logger.info(

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
import traceback import traceback
from asyncio import Queue
from anyio.streams.memory import MemoryObjectSendStream
from astrbot.core import logger from astrbot.core import logger
from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.config.astrbot_config import AstrBotConfig
@@ -12,7 +13,7 @@ from .sources.webchat.webchat_adapter import WebChatAdapter
class PlatformManager: class PlatformManager:
def __init__(self, config: AstrBotConfig, event_queue: Queue): def __init__(self, config: AstrBotConfig, event_queue: MemoryObjectSendStream):
self.platform_insts: list[Platform] = [] self.platform_insts: list[Platform] = []
"""加载的 Platform 的实例""" """加载的 Platform 的实例"""

View File

@@ -1,9 +1,10 @@
import abc import abc
import uuid import uuid
from asyncio import Queue
from collections.abc import Awaitable from collections.abc import Awaitable
from typing import Any from typing import Any
from anyio.streams.memory import MemoryObjectSendStream
from astrbot.core.message.message_event_result import MessageChain from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.utils.metrics import Metric from astrbot.core.utils.metrics import Metric
@@ -13,7 +14,7 @@ from .platform_metadata import PlatformMetadata
class Platform(abc.ABC): class Platform(abc.ABC):
def __init__(self, event_queue: Queue): def __init__(self, event_queue: MemoryObjectSendStream):
super().__init__() super().__init__()
# 维护了消息平台的事件队列EventBus 会从这里取出事件并处理。 # 维护了消息平台的事件队列EventBus 会从这里取出事件并处理。
self._event_queue = event_queue self._event_queue = event_queue
@@ -45,7 +46,7 @@ class Platform(abc.ABC):
def commit_event(self, event: AstrMessageEvent): def commit_event(self, event: AstrMessageEvent):
"""提交一个事件到事件队列。""" """提交一个事件到事件队列。"""
self._event_queue.put_nowait(event) self._event_queue.send_nowait(event)
def get_client(self): def get_client(self):
"""获取平台的客户端对象。""" """获取平台的客户端对象。"""

View File

@@ -216,7 +216,7 @@ class DingtalkPlatformAdapter(Platform):
client=self.client, client=self.client,
) )
self._event_queue.put_nowait(event) self._event_queue.send_nowait(event)
async def run(self): async def run(self):
# await self.client_.start() # await self.client_.start()

View File

@@ -224,7 +224,7 @@ class LarkPlatformAdapter(Platform):
bot=self.lark_api, bot=self.lark_api,
) )
self._event_queue.put_nowait(event) self._event_queue.send_nowait(event)
async def run(self): async def run(self):
# self.client.start() # self.client.start()

View File

@@ -1,10 +1,10 @@
import asyncio
import hashlib import hashlib
import hmac import hmac
import json import json
import logging import logging
from collections.abc import Callable from collections.abc import Callable
import anyio
from quart import Quart, Response, request from quart import Quart, Response, request
from slack_sdk.socket_mode.aiohttp import SocketModeClient from slack_sdk.socket_mode.aiohttp import SocketModeClient
from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.socket_mode.request import SocketModeRequest
@@ -40,7 +40,7 @@ class SlackWebhookClient:
logging.getLogger("quart.app").setLevel(logging.WARNING) logging.getLogger("quart.app").setLevel(logging.WARNING)
logging.getLogger("quart.serving").setLevel(logging.WARNING) logging.getLogger("quart.serving").setLevel(logging.WARNING)
self.shutdown_event = asyncio.Event() self.shutdown_event = anyio.Event()
def _setup_routes(self): def _setup_routes(self):
"""设置路由""" """设置路由"""

View File

@@ -1,4 +1,5 @@
"""企业微信智能机器人 API 客户端 """企业微信智能机器人 API 客户端.
处理消息加密解密、API 调用等 处理消息加密解密、API 调用等
""" """

View File

@@ -2,10 +2,10 @@
处理企业微信智能机器人的 HTTP 回调请求 处理企业微信智能机器人的 HTTP 回调请求
""" """
import asyncio
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
import anyio
import quart import quart
from astrbot.api import logger from astrbot.api import logger
@@ -41,7 +41,7 @@ class WecomAIBotServer:
self.app = quart.Quart(__name__) self.app = quart.Quart(__name__)
self._setup_routes() self._setup_routes()
self.shutdown_event = asyncio.Event() self.shutdown_event = anyio.Event()
def _setup_routes(self): def _setup_routes(self):
"""设置 Quart 路由""" """设置 Quart 路由"""

View File

@@ -7,6 +7,7 @@ from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
import aiohttp import aiohttp
import anyio
from astrbot import logger from astrbot import logger
from astrbot.core import sp from astrbot.core import sp
@@ -98,7 +99,7 @@ class FunctionToolManager:
self.func_list: list[FuncTool] = [] self.func_list: list[FuncTool] = []
self.mcp_client_dict: dict[str, MCPClient] = {} self.mcp_client_dict: dict[str, MCPClient] = {}
"""MCP 服务列表""" """MCP 服务列表"""
self.mcp_client_event: dict[str, asyncio.Event] = {} self.mcp_client_event: dict[str, anyio.Event] = {}
def empty(self) -> bool: def empty(self) -> bool:
return len(self.func_list) == 0 return len(self.func_list) == 0
@@ -206,7 +207,7 @@ class FunctionToolManager:
for name in mcp_server_json_obj: for name in mcp_server_json_obj:
cfg = mcp_server_json_obj[name] cfg = mcp_server_json_obj[name]
if cfg.get("active", True): if cfg.get("active", True):
event = asyncio.Event() event = anyio.Event()
asyncio.create_task( asyncio.create_task(
self._init_mcp_client_task_wrapper(name, cfg, event), self._init_mcp_client_task_wrapper(name, cfg, event),
) )
@@ -216,7 +217,7 @@ class FunctionToolManager:
self, self,
name: str, name: str,
cfg: dict, cfg: dict,
event: asyncio.Event, event: anyio.Event,
ready_future: asyncio.Future | None = None, ready_future: asyncio.Future | None = None,
) -> None: ) -> None:
"""初始化 MCP 客户端的包装函数,用于捕获异常""" """初始化 MCP 客户端的包装函数,用于捕获异常"""
@@ -307,7 +308,7 @@ class FunctionToolManager:
self, self,
name: str, name: str,
config: dict, config: dict,
event: asyncio.Event | None = None, event: anyio.Event | None = None,
ready_future: asyncio.Future | None = None, ready_future: asyncio.Future | None = None,
timeout: int = 30, timeout: int = 30,
) -> None: ) -> None:
@@ -316,7 +317,7 @@ class FunctionToolManager:
Args: Args:
name (str): The name of the MCP server. name (str): The name of the MCP server.
config (dict): Configuration for 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. ready_future (asyncio.Future): Future to signal when the MCP client is ready.
timeout (int): Timeout for the initialization. timeout (int): Timeout for the initialization.
@@ -326,7 +327,7 @@ class FunctionToolManager:
""" """
if not event: if not event:
event = asyncio.Event() event = anyio.Event()
if not ready_future: if not ready_future:
ready_future = asyncio.Future() ready_future = asyncio.Future()
if name in self.mcp_client_dict: if name in self.mcp_client_dict:

View File

@@ -1,8 +1,8 @@
import logging import logging
from asyncio import Queue
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
from anyio.streams.memory import MemoryObjectSendStream
from deprecated import deprecated from deprecated import deprecated
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
@@ -50,7 +50,7 @@ class Context:
def __init__( def __init__(
self, self,
event_queue: Queue, event_queue: MemoryObjectSendStream,
config: AstrBotConfig, config: AstrBotConfig,
db: BaseDatabase, db: BaseDatabase,
provider_manager: ProviderManager, provider_manager: ProviderManager,
@@ -193,7 +193,7 @@ class Context:
"""获取 AstrBot 数据库。""" """获取 AstrBot 数据库。"""
return self._db return self._db
def get_event_queue(self) -> Queue: def get_event_queue(self) -> MemoryObjectSendStream:
"""获取事件队列。""" """获取事件队列。"""
return self._event_queue return self._event_queue

View File

@@ -96,7 +96,7 @@ class CommandGroupFilter(HandlerFilter):
prefix + "", prefix + "",
event=event, event=event,
cfg=cfg, cfg=cfg,
) ),
) )
return "".join(parts) return "".join(parts)

View File

@@ -30,7 +30,9 @@ class UmopConfigRouter:
if len(p1_ls) != 3 or len(p2_ls) != 3: if len(p1_ls) != 3 or len(p2_ls) != 3:
return False # 非法格式 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: def get_conf_id_for_umop(self, umo: str) -> str | None:
"""根据 UMO 获取对应的配置文件 ID """根据 UMO 获取对应的配置文件 ID

View File

@@ -1,5 +1,7 @@
"""会话控制""" """会话控制"""
from __future__ import annotations
import abc import abc
import asyncio import asyncio
import copy import copy
@@ -8,11 +10,13 @@ import time
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
import anyio
import astrbot.core.message.components as Comp import astrbot.core.message.components as Comp
from astrbot.core.platform import AstrMessageEvent from astrbot.core.platform import AstrMessageEvent
USER_SESSIONS: dict[str, "SessionWaiter"] = {} # 存储 SessionWaiter 实例 USER_SESSIONS: dict[str, SessionWaiter] = {} # 存储 SessionWaiter 实例
FILTERS: list["SessionFilter"] = [] # 存储 SessionFilter 实例 FILTERS: list[SessionFilter] = [] # 存储 SessionFilter 实例
class SessionController: class SessionController:
@@ -20,16 +24,16 @@ class SessionController:
def __init__(self): def __init__(self):
self.future = asyncio.Future() 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)开始时的时间""" """上次保持(keep)开始时的时间"""
self.timeout: float | int = None self.timeout: float | int | None = None
"""上次保持(keep)开始时的超时时间""" """上次保持(keep)开始时的超时时间"""
self.history_chains: list[list[Comp.BaseMessageComponent]] = [] 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 not self.future.done():
if error: if error:
@@ -53,7 +57,9 @@ class SessionController:
self.stop() self.stop()
return return
else: 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 timeout = left_timeout + timeout
if timeout <= 0: if timeout <= 0:
self.stop() self.stop()
@@ -62,18 +68,19 @@ class SessionController:
if self.current_event and not self.current_event.is_set(): if self.current_event and not self.current_event.is_set():
self.current_event.set() # 通知上一个 keep 结束 self.current_event.set() # 通知上一个 keep 结束
new_event = asyncio.Event() new_event = anyio.Event()
self.ts = new_ts self.ts = new_ts
self.current_event = new_event self.current_event = new_event
self.timeout = timeout 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: try:
await asyncio.wait_for(event.wait(), timeout) with anyio.move_on_after(timeout_seconds):
except asyncio.TimeoutError: await event.wait()
except TimeoutError:
if not self.future.done(): if not self.future.done():
self.future.set_exception(TimeoutError("等待超时")) self.future.set_exception(TimeoutError("等待超时"))
except asyncio.CancelledError: except asyncio.CancelledError:
@@ -105,10 +112,12 @@ class SessionWaiter:
session_filter: SessionFilter, session_filter: SessionFilter,
session_id: str, session_id: str,
record_history_chains: bool, record_history_chains: bool,
): ) -> None:
self.session_id = session_id self.session_id = session_id
self.session_filter = session_filter 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.session_controller = SessionController()
self.record_history_chains = record_history_chains self.record_history_chains = record_history_chains
@@ -119,15 +128,15 @@ class SessionWaiter:
async def register_wait( async def register_wait(
self, self,
handler: Callable[[str], Awaitable[Any]], handler: Callable[[SessionController, AstrMessageEvent], Awaitable[Any]],
timeout: int = 30, timeout_seconds: int = 30,
) -> Any: ) -> Any:
"""等待外部输入并处理""" """等待外部输入并处理"""
self.handler = handler self.handler = handler
USER_SESSIONS[self.session_id] = self USER_SESSIONS[self.session_id] = self
# 开始一个会话保持事件 # 开始一个会话保持事件
self.session_controller.keep(timeout, reset_timeout=True) self.session_controller.keep(timeout_seconds, reset_timeout=True)
try: try:
return await self.session_controller.future return await self.session_controller.future
@@ -137,7 +146,7 @@ class SessionWaiter:
finally: finally:
self._cleanup() self._cleanup()
def _cleanup(self, error: Exception = None): def _cleanup(self, error: Exception | None = None):
"""清理会话""" """清理会话"""
USER_SESSIONS.pop(self.session_id, None) USER_SESSIONS.pop(self.session_id, None)
try: try:
@@ -153,6 +162,10 @@ class SessionWaiter:
if not session or session.session_controller.future.done(): if not session or session.session_controller.future.done():
return return
# 此时 session 不会是 None因为上面的检查
if session is None:
return
async with session._lock: async with session._lock:
if not session.session_controller.future.done(): if not session.session_controller.future.done():
if session.record_history_chains: if session.record_history_chains:
@@ -161,7 +174,8 @@ class SessionWaiter:
) )
try: try:
# TODO: 这里使用 create_task跟踪 task防止超时后这里 handler 仍然在执行 # 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: except Exception as e:
session.session_controller.stop(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() 获取。深拷贝。 :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) @functools.wraps(func)
async def wrapper( async def wrapper(
event: AstrMessageEvent, event: AstrMessageEvent,
session_filter: SessionFilter = None, session_filter: SessionFilter | None = None,
*args, *args,
**kwargs, **kwargs,
): ):

View File

@@ -1,6 +1,6 @@
import asyncio
import datetime import datetime
import anyio
import jwt import jwt
from quart import request from quart import request
@@ -44,7 +44,7 @@ class AuthRoute(Route):
) )
.__dict__ .__dict__
) )
await asyncio.sleep(3) await anyio.sleep(3)
return Response().error("用户名或密码错误").__dict__ return Response().error("用户名或密码错误").__dict__
async def edit_account(self): async def edit_account(self):

View File

@@ -4,6 +4,7 @@ import os
import uuid import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import anyio
from quart import Response as QuartResponse from quart import Response as QuartResponse
from quart import g, make_response, request from quart import g, make_response, request
@@ -188,8 +189,8 @@ class ChatRoute(Route):
try: try:
if not client_disconnected: if not client_disconnected:
await asyncio.sleep(0.05) await anyio.sleep(0.05)
except asyncio.CancelledError: except anyio.get_cancelled_exc_class():
logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。") logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
client_disconnected = True client_disconnected = True

View File

@@ -817,7 +817,8 @@ class ConfigRoute(Route):
cached_token = self._logo_token_cache[cache_key] cached_token = self._logo_token_cache[cache_key]
# 确保platform_default_tmpl[platform.name]存在且为字典 # 确保platform_default_tmpl[platform.name]存在且为字典
if platform.name not in platform_default_tmpl or not isinstance( 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] = {}
platform_default_tmpl[platform.name]["logo_token"] = cached_token platform_default_tmpl[platform.name]["logo_token"] = cached_token
@@ -846,7 +847,8 @@ class ConfigRoute(Route):
# 确保platform_default_tmpl[platform.name]存在且为字典 # 确保platform_default_tmpl[platform.name]存在且为字典
if platform.name not in platform_default_tmpl or not isinstance( 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] = {}

18
main.py
View File

@@ -1,16 +1,20 @@
import argparse import argparse
import asyncio
import mimetypes import mimetypes
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
import anyio
from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.config.default import VERSION from astrbot.core.config.default import VERSION
from astrbot.core.initial_loader import InitialLoader from astrbot.core.initial_loader import InitialLoader
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.utils.io import download_dashboard, get_dashboard_version 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
sys.path.append(Path(__file__).parent.as_posix()) sys.path.append(Path(__file__).parent.as_posix())
@@ -93,7 +97,11 @@ if __name__ == "__main__":
LogManager.set_queue_handler(logger, log_broker) LogManager.set_queue_handler(logger, log_broker)
# 检查仪表板文件 # 检查仪表板文件
webui_dir = asyncio.run(check_dashboard_files(args.webui_dir)) webui_dir = anyio.run(
check_dashboard_files,
args.webui_dir,
backend_options=backend_options,
)
db = db_helper db = db_helper
@@ -102,4 +110,8 @@ if __name__ == "__main__":
core_lifecycle = InitialLoader(db, log_broker) core_lifecycle = InitialLoader(db, log_broker)
core_lifecycle.webui_dir = webui_dir core_lifecycle.webui_dir = webui_dir
asyncio.run(core_lifecycle.start()) logger.info(
"将按以下异步后端启动 AstrBot: %s",
backend_options if backend_options else "asyncio",
)
anyio.run(core_lifecycle.start, backend_options=backend_options)

View File

@@ -164,14 +164,14 @@ class ConversationCommands:
"%m-%d %H:%M", "%m-%d %H:%M",
) )
parts.append( 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 idx += 1
if idx == 1: if idx == 1:
parts.append("没有找到任何对话。") parts.append("没有找到任何对话。")
dify_cid = provider.conversation_ids.get(message.unified_msg_origin, None) dify_cid = provider.conversation_ids.get(message.unified_msg_origin, None)
parts.append( 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) ret = "".join(parts)
message.set_result(MessageEventResult().message(ret)) message.set_result(MessageEventResult().message(ret))
@@ -211,7 +211,7 @@ class ConversationCommands:
persona_id = persona["name"] persona_id = persona["name"]
title = _titles.get(conv.cid, "新对话") title = _titles.get(conv.cid, "新对话")
parts.append( 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 global_index += 1

View File

@@ -136,7 +136,7 @@ class ProviderCommands:
curr_model = prov.get_model() or "" curr_model = prov.get_model() or ""
parts.append(f"\n当前模型: [{curr_model}]") parts.append(f"\n当前模型: [{curr_model}]")
parts.append( parts.append(
"\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。" "\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。",
) )
ret = "".join(parts) ret = "".join(parts)

View File

@@ -9,6 +9,7 @@ from collections import defaultdict
import aiodocker import aiodocker
import aiohttp import aiohttp
import anyio
from astrbot.api import llm_tool, logger, star from astrbot.api import llm_tool, logger, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
@@ -291,7 +292,7 @@ class Main(star.Star):
self.user_waiting[uid] = time.time() self.user_waiting[uid] = time.time()
tip = "文件" tip = "文件"
yield event.plain_result(f"代码执行器: 请在 60s 内上传一个{tip}") yield event.plain_result(f"代码执行器: 请在 60s 内上传一个{tip}")
await asyncio.sleep(60) await anyio.sleep(60)
if uid in self.user_waiting: if uid in self.user_waiting:
yield event.plain_result( yield event.plain_result(
f"代码执行器: {event.get_sender_name()}/{event.get_sender_id()} 未在规定时间内上传{tip}", f"代码执行器: {event.get_sender_name()}/{event.get_sender_id()} 未在规定时间内上传{tip}",

View File

@@ -2,6 +2,7 @@ import asyncio
import random import random
import aiohttp import aiohttp
import anyio
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from readability import Document from readability import Document
@@ -26,7 +27,7 @@ class Main(star.Star):
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context) -> None:
self.context = context self.context = context
self.tavily_key_index = 0 self.tavily_key_index = 0
self.tavily_key_lock = asyncio.Lock() self.tavily_key_lock = anyio.Lock()
# 将 str 类型的 key 迁移至 list[str],并保存 # 将 str 类型的 key 迁移至 list[str],并保存
cfg = self.context.get_config() cfg = self.context.get_config()

View File

@@ -63,6 +63,8 @@ dependencies = [
"jieba>=0.42.1", "jieba>=0.42.1",
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2", "markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
"xinference-client", "xinference-client",
"anyio>=4.11.0",
"uvloop>=0.22.1 ; sys_platform == 'linux'",
] ]
[dependency-groups] [dependency-groups]
@@ -98,8 +100,8 @@ select = [
# "SIM", # flake8-simplify # "SIM", # flake8-simplify
] ]
ignore = [ ignore = [
"F403", "F403",
"F405", "F405",
"E501", "E501",
"ASYNC230" # TODO: handle ASYNC230 in AstrBot "ASYNC230" # TODO: handle ASYNC230 in AstrBot
] ]
@@ -112,6 +114,34 @@ reportMissingImports = false
include = ["astrbot","packages"] include = ["astrbot","packages"]
exclude = ["dashboard", "node_modules", "dist", "data", "tests"] 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] [tool.hatch.version]
source = "uv-dynamic-versioning" source = "uv-dynamic-versioning"
@@ -123,4 +153,3 @@ bump = true
[build-system] [build-system]
requires = ["hatchling", "uv-dynamic-versioning"] requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build" build-backend = "hatchling.build"

View File

@@ -1,54 +1,498 @@
aiocqhttp>=1.4.4 # This file was autogenerated by uv via the following command:
aiodocker>=0.24.0 # uv export --format requirements.txt --no-hashes --no-editable
aiohttp>=3.11.18 .
aiocqhttp>=1.4.4 aiocqhttp==1.4.4
aiodocker>=0.24.0 # via astrbot
aiohttp>=3.11.18 aiodocker==0.24.0
aiosqlite>=0.21.0 # via astrbot
anthropic>=0.51.0 aiofiles==25.1.0
apscheduler>=3.11.0 # via
beautifulsoup4>=4.13.4 # astrbot
certifi>=2025.4.26 # quart
chardet~=5.1.0 aiohappyeyeballs==2.6.1
colorlog>=6.9.0 # via aiohttp
cryptography>=44.0.3 aiohttp==3.13.2
dashscope>=1.23.2 # via
defusedxml>=0.7.1 # aiodocker
deprecated>=1.2.18 # astrbot
dingtalk-stream>=0.22.1 # dashscope
docstring-parser>=0.16 # 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 faiss-cpu==1.10.0
filelock>=3.18.0 # via astrbot
google-genai>=1.14.0 filelock==3.20.0
lark-oapi>=1.4.15 # via astrbot
lxml-html-clean>=0.4.2 flask==3.1.2
mcp>=1.8.0 # via quart
openai>=1.78.0 frozenlist==1.8.0
ormsgpack>=1.9.1 # via
pillow>=11.2.1 # aiohttp
pip>=25.1.1 # aiosignal
psutil>=5.8.0 google-auth==2.42.1
py-cord>=2.6.1 # via google-genai
pydantic~=2.10.3 google-genai==1.47.0
pydub>=0.25.1 # via astrbot
pyjwt>=2.10.1 greenlet==3.2.4
python-telegram-bot>=22.0 # via sqlalchemy
qq-botpy>=1.2.1 h11==0.16.0
quart>=0.20.0 # via
readability-lxml>=0.8.4.1 # httpcore
silk-python>=0.2.6 # hypercorn
slack-sdk>=3.35.0 # uvicorn
sqlalchemy[asyncio]>=2.0.41 # wsproto
sqlmodel>=0.0.24 h2==4.3.0
telegramify-markdown>=0.5.1 # via hypercorn
watchfiles>=1.0.5 hpack==4.1.0
websockets>=15.0.1 # via h2
wechatpy>=1.8.18 httpcore==1.0.9
audioop-lts ; python_full_version >= '3.13' # via httpx
click>=8.2.1 httpx==0.28.1
pypdf>=6.1.1 # via
aiofiles>=25.1.0 # aiocqhttp
rank-bm25>=0.2.2 # anthropic
jieba>=0.42.1 # google-genai
markitdown-no-magika[docx,xls,xlsx]>=0.1.2 # lark-oapi
xinference-client # 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