diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8611e2698..5bdf6bef7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,20 +6,20 @@ ci: autoupdate_schedule: weekly autoupdate_commit_msg: ":balloon: pre-commit autoupdate" repos: -- repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.14.1 - hooks: - # Run the linter. - - id: ruff-check - types_or: [ python, pyi ] - args: [ --fix ] - # Run the formatter. - - id: ruff-format - types_or: [ python, pyi ] - -- repo: https://github.com/asottile/pyupgrade - rev: v3.21.0 - hooks: - - id: pyupgrade - args: [--py310-plus] + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.15.7 + hooks: + # Run the linter. + - id: ruff-check + types_or: [python, pyi] + args: [--fix] + # Run the formatter. + - id: ruff-format + types_or: [python, pyi] + + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: [--py312-plus] diff --git a/astrbot/builtin_stars/builtin_commands/commands/persona.py b/astrbot/builtin_stars/builtin_commands/commands/persona.py index eee2edfec..b39eae3be 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/persona.py +++ b/astrbot/builtin_stars/builtin_commands/commands/persona.py @@ -199,9 +199,7 @@ class PersonaCommands: ) force_warn_msg = "" if force_applied_persona_id: - force_warn_msg = ( - "提醒:由于自定义规则,您现在切换的人格将不会生效。" - ) + force_warn_msg = "提醒:由于自定义规则,您现在切换的人格将不会生效。" message.set_result( MessageEventResult().message( diff --git a/astrbot/cli/commands/cmd_run.py b/astrbot/cli/commands/cmd_run.py index 3bd8cc23d..82f51b879 100644 --- a/astrbot/cli/commands/cmd_run.py +++ b/astrbot/cli/commands/cmd_run.py @@ -47,7 +47,6 @@ import os import re import sys import traceback -from collections.abc import Iterable from pathlib import Path import click @@ -165,68 +164,6 @@ def parse_service_config_file( return parsed -def find_autodetected_template( - candidates: Iterable[Path] | None = None, -) -> Path | None: - """Try to locate a likely 'config.template' automatically. - - Heuristics used (in order): - - If explicit environment variables point to a template, use them. - - Provided candidates (if any). - - Look up from current file location for a sibling 'config.template' within ancestor directories. - - Common system / user locations. - """ - # Env overrides - for envvar in ( - "AUR_CONFIG_PATH", - "ASTRBOT_CONFIG_TEMPLATE", - "SERVICE_CONFIG", - "CONFIG_TEMPLATE", - ): - val = os.environ.get(envvar) - if val: - p = Path(val) - if p.exists(): - return p - - # Provided explicit candidates - if candidates: - for c in candidates: - try: - p = Path(c) - except TypeError: - continue - if p.exists(): - return p - - # Search upward from this file for a 'config.template' (useful for AUR checkout) - here = Path(__file__).resolve() - for parent in [here] + list(here.parents)[:6]: - p = parent / "config.template" - if p.exists(): - return p - # also check a sibling directory named 'astrbot-git' or 'AstrBot' that might contain the file - for sibling in ("astrbot-git", "AstrBot", "astrbot"): - candidate = parent / sibling / "config.template" - if candidate.exists(): - return candidate - - # Common locations - common = [ - Path.cwd() / "config.template", - Path.cwd() / ".env", - Path("/etc/astrbot/config.template"), - Path("/var/lib/astrbot/config.template"), - Path.home() / ".config" / "astrbot" / "config.template", - Path.home() / "astrbot-git" / "config.template", - ] - for p in common: - if p.exists(): - return p - - return None - - async def run_astrbot(astrbot_root: Path) -> None: """Run AstrBot""" from astrbot.core import LogBroker, LogManager, db_helper, logger @@ -315,7 +252,7 @@ def run( # --- Step 1: If a service config is provided, read and parse it to local overrides --- parsed_service: dict[str, str] = {} - # If explicit config path provided, use it. Otherwise attempt autodetection. + # If explicit config path provided, use it. svc_path: Path | None = None if service_config: candidate = Path(service_config) @@ -329,13 +266,6 @@ def run( if svc_path is not None: parsed_service = parse_service_config_file(svc_path) - else: - # Auto-detect possible config.template (AUR or other) - autodetected = find_autodetected_template() - if autodetected: - # prefer 'config.template' but don't force; parse if found - parsed_service = parse_service_config_file(autodetected) - svc_path = autodetected # Local variables (CLI args) should keep the highest precedence. # Apply parsed service values only if CLI didn't supply them. diff --git a/astrbot/core/astr_main_agent.py b/astrbot/core/astr_main_agent.py index 8b523aed5..f154c3c41 100644 --- a/astrbot/core/astr_main_agent.py +++ b/astrbot/core/astr_main_agent.py @@ -142,9 +142,7 @@ def _select_provider( if not provider: logger.error("未找到指定的提供商: %s。", sel_provider) if not isinstance(provider, Provider): - logger.error( - "选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider) - ) + logger.error("选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider)) return None return provider try: diff --git a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py index b35a876c2..4f9e5c58b 100644 --- a/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py +++ b/astrbot/core/platform/sources/dingtalk/dingtalk_adapter.py @@ -226,9 +226,7 @@ class DingtalkPlatformAdapter(Platform): elif "type" in content and content["type"] == "picture": download_code = cast(str, content.get("downloadCode") or "") if not download_code: - logger.warning( - "钉钉富文本图片消息缺少 downloadCode,已跳过" - ) + logger.warning("钉钉富文本图片消息缺少 downloadCode,已跳过") continue if not robot_code: logger.error( diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 9c05de387..864953400 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -428,9 +428,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): ): raise - logger.warning( - "[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。" - ) + logger.warning("[QQOfficial] markdown 发送被拒绝,回退到 content 模式重试。") fallback_payload = payload.copy() fallback_payload.pop("markdown", None) fallback_payload["content"] = plain_text @@ -555,7 +553,7 @@ class QQOfficialMessageEvent(AstrMessageEvent): markdown: message.MarkdownPayload | None = None, keyboard: message.Keyboard | None = None, stream: dict | None = None, - ) -> message.Message: + ) -> message.Message | None: payload = locals() payload.pop("self", None) # QQ API does not accept stream.id=None; remove it when not yet assigned 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 87dc92f93..c173410fb 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 @@ -33,10 +33,7 @@ from astrbot.core.utils.webhook_utils import log_webhook_info from .weixin_offacc_event import WeixinOfficialAccountPlatformEvent -if sys.version_info >= (3, 12): - from typing import override -else: - from typing_extensions import override +from typing import override class WeixinOfficialAccountServer: @@ -186,8 +183,7 @@ class WeixinOfficialAccountServer: return _reply_text(cached_xml) else: return _reply_text( - cached_xml - + "\n【后续消息还在缓冲中,回复任意文字继续获取】" + cached_xml + "\n【后续消息还在缓冲中,回复任意文字继续获取】" ) task: asyncio.Task | None = cast(asyncio.Task | None, state.get("task")) @@ -248,9 +244,7 @@ class WeixinOfficialAccountServer: # create new trigger when state is empty, and store state in buffer logger.debug(f"wx new trigger: user={from_user} msg_id={msg_id}") preview = self._preview(msg) - placeholder = ( - f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" - ) + placeholder = f"【正在思考'{preview}'中,已思考0s,回复任意文字尝试获取回复】" logger.info( f"wx start task: user={from_user} msg_id={msg_id} preview={preview}" ) @@ -380,7 +374,7 @@ class WeixinOfficialAccountPlatformAdapter(Platform): ) # wait for 180s logger.debug(f"Got future result: {result}") return result - except asyncio.TimeoutError: + except TimeoutError: logger.info(f"callback 处理消息超时: message_id={msg.id}") return create_reply("处理消息超时,请稍后再试。", msg) except Exception as e: diff --git a/astrbot/core/provider/sources/bailian_rerank_source.py b/astrbot/core/provider/sources/bailian_rerank_source.py index f6ec040c4..334ca87c9 100644 --- a/astrbot/core/provider/sources/bailian_rerank_source.py +++ b/astrbot/core/provider/sources/bailian_rerank_source.py @@ -191,9 +191,7 @@ class BailianRerankProvider(RerankProvider): # 检查限制 if len(documents) > 500: - logger.warning( - f"文档数量({len(documents)})超过限制(500),将截断前500个文档" - ) + logger.warning(f"文档数量({len(documents)})超过限制(500),将截断前500个文档") documents = documents[:500] try: diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index e421170b9..7018d9feb 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging from asyncio import Queue -from collections.abc import Awaitable, Callable, Coroutine +from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Any, Protocol from deprecated import deprecated @@ -65,7 +65,7 @@ class Context: registered_web_apis: list = [] # 向后兼容的变量 - _register_tasks: list[Coroutine[object, object, object]] = [] + _register_tasks: list[Awaitable[Any]] = [] _star_manager: StarManagerProtocol | None = None def __init__( @@ -373,9 +373,7 @@ class Context: if prov is None: return None if not isinstance(prov, Provider): - raise ValueError( - f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}" - ) + raise ValueError(f"该会话来源的对话模型(提供商)的类型不正确: {type(prov)}") return prov def get_using_tts_provider(self, umo: str | None = None) -> TTSProvider | None: @@ -679,7 +677,7 @@ class Context: ) star_handlers_registry.append(md) - def register_task(self, task: Awaitable, desc: str) -> None: + def register_task(self, task: Awaitable[Any], desc: str) -> None: """[DEPRECATED]注册一个异步任务。 Args: diff --git a/astrbot/core/star/filter/command_group.py b/astrbot/core/star/filter/command_group.py index 59542715c..a73222f3d 100644 --- a/astrbot/core/star/filter/command_group.py +++ b/astrbot/core/star/filter/command_group.py @@ -49,10 +49,10 @@ class CommandGroupFilter(HandlerFilter): if not parent_cmd_names: # 根节点 - return [self.group_name] + list(self.alias) + return [self.group_name, *list(self.alias)] result = [] - candidates = [self.group_name] + list(self.alias) + candidates = [self.group_name, *list(self.alias)] for parent_cmd_name in parent_cmd_names: for candidate in candidates: result.append(parent_cmd_name + " " + candidate) @@ -94,7 +94,7 @@ class CommandGroupFilter(HandlerFilter): parts.append( sub_filter.print_cmd_tree( sub_filter.sub_command_filters, - prefix + "│ ", + prefix + "| ", event=event, cfg=cfg, ) diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index 082bcd611..8e987cf47 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -14,18 +14,24 @@ from astrbot.core.agent.tool import FunctionTool from astrbot.core.message.message_event_result import MessageEventResult from astrbot.core.provider.func_tool_manager import PY_TO_JSON_TYPE, SUPPORTED_TYPES from astrbot.core.provider.register import llm_tools - -from ..filter.command import CommandFilter -from ..filter.command_group import CommandGroupFilter -from ..filter.custom_filter import CustomFilterAnd, CustomFilterOr -from ..filter.event_message_type import EventMessageType, EventMessageTypeFilter -from ..filter.permission import PermissionType, PermissionTypeFilter -from ..filter.platform_adapter_type import ( +from astrbot.core.star.filter.command import CommandFilter +from astrbot.core.star.filter.command_group import CommandGroupFilter +from astrbot.core.star.filter.custom_filter import CustomFilterAnd, CustomFilterOr +from astrbot.core.star.filter.event_message_type import ( + EventMessageType, + EventMessageTypeFilter, +) +from astrbot.core.star.filter.permission import PermissionType, PermissionTypeFilter +from astrbot.core.star.filter.platform_adapter_type import ( PlatformAdapterType, PlatformAdapterTypeFilter, ) -from ..filter.regex import RegexFilter -from ..star_handler import EventType, StarHandlerMetadata, star_handlers_registry +from astrbot.core.star.filter.regex import RegexFilter +from astrbot.core.star.star_handler import ( + EventType, + StarHandlerMetadata, + star_handlers_registry, +) def get_handler_full_name( diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 53c57e41b..afc1150a9 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -169,6 +169,8 @@ class PluginManager: def __init__(self, context: Context, config: AstrBotConfig) -> None: from .star_tools import StarTools + self.tasks = set() + self.updator = PluginUpdator() self.context = context @@ -195,7 +197,9 @@ class PluginManager: self.failed_plugin_info = "" if os.getenv("ASTRBOT_RELOAD", "0") == "1": - asyncio.create_task(self._watch_plugins_changes()) + _watch_plugins_changes = asyncio.create_task(self._watch_plugins_changes()) + self.tasks.add(_watch_plugins_changes) + _watch_plugins_changes.add_done_callback(self.tasks.discard) async def _watch_plugins_changes(self) -> None: """监视插件文件变化""" @@ -1247,12 +1251,13 @@ class PluginManager: """ # this metric is for displaying plugins installation count in webui - asyncio.create_task( + _task_install_star = asyncio.create_task( Metric.upload( et="install_star", repo=repo_url, ), ) + self.tasks.add(_task_install_star) async with self._pm_lock: plugin_path = "" @@ -1688,8 +1693,7 @@ class PluginManager: ) if not success: raise Exception( - error_message - or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" + error_message or f"安装插件 {dir_name} 失败,请检查插件依赖或兼容性。" ) # Get the plugin metadata to return repo info @@ -1724,12 +1728,14 @@ class PluginManager: } if plugin.repo: - asyncio.create_task( + _task_install_star_f = asyncio.create_task( Metric.upload( et="install_star_f", # install star 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: diff --git a/astrbot/core/star/updator.py b/astrbot/core/star/updator.py index 8ec2e6e17..55fd7203c 100644 --- a/astrbot/core/star/updator.py +++ b/astrbot/core/star/updator.py @@ -3,11 +3,10 @@ import shutil import zipfile from astrbot.core import logger +from astrbot.core.updator import RepoZipUpdator from astrbot.core.utils.astrbot_path import get_astrbot_plugin_path from astrbot.core.utils.io import on_error, remove_dir - -from ..star.star import StarMetadata -from ..updator import RepoZipUpdator +from astrbot.star.star import StarMetadata class PluginUpdator(RepoZipUpdator): diff --git a/astrbot/core/tools/send_message.py b/astrbot/core/tools/send_message.py index e6bed231b..171d1ee44 100644 --- a/astrbot/core/tools/send_message.py +++ b/astrbot/core/tools/send_message.py @@ -169,7 +169,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if path: ( local_path, - file_from_sandbox, + _file_from_sandbox, ) = await self._resolve_path_from_sandbox(context, path) components.append(Comp.Video.fromFileSystem(path=local_path)) elif url: @@ -188,7 +188,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]): if path: ( local_path, - file_from_sandbox, + _file_from_sandbox, ) = await self._resolve_path_from_sandbox(context, path) components.append(Comp.File(name=name, file=local_path)) elif url: diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index 9ff19931f..b220136ff 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -144,7 +144,9 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non downloaded_size = 0 start_time = time.time() if show_progress: - print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") + logger.info( + f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}" + ) async with await anyio.open_file(path, "wb") as f: while True: chunk = await resp.content.read(8192) @@ -159,9 +161,8 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non else 1 ) speed = downloaded_size / 1024 / elapsed_time # KB/s - print( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", - end="", + logger.info( + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s" ) except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): # 关闭SSL验证(仅在证书验证失败时作为fallback) @@ -183,7 +184,9 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non downloaded_size = 0 start_time = time.time() if show_progress: - print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}") + logger.info( + f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}" + ) async with await anyio.open_file(path, "wb") as f: while True: chunk = await resp.content.read(8192) @@ -194,12 +197,11 @@ async def download_file(url: str, path: str, show_progress: bool = False) -> Non if show_progress: elapsed_time = time.time() - start_time speed = downloaded_size / 1024 / elapsed_time # KB/s - print( - f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s", - end="", + logger.info( + f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s" ) if show_progress: - print() + logger.info("下载完成") def file_to_base64(file_path: str) -> str: diff --git a/astrbot/core/utils/media_utils.py b/astrbot/core/utils/media_utils.py index b9f74c344..f127ad25c 100644 --- a/astrbot/core/utils/media_utils.py +++ b/astrbot/core/utils/media_utils.py @@ -38,7 +38,7 @@ async def get_media_duration(file_path: str) -> int | None: stderr=subprocess.PIPE, ) - stdout, stderr = await process.communicate() + stdout, _stderr = await process.communicate() if process.returncode == 0 and stdout: duration_seconds = float(stdout.decode().strip()) @@ -105,7 +105,7 @@ async def convert_audio_to_opus(audio_path: str, output_path: str | None = None) stderr=subprocess.PIPE, ) - stdout, stderr = await process.communicate() + _stdout, stderr = await process.communicate() if process.returncode != 0: # 清理可能已生成但无效的临时文件 @@ -180,7 +180,7 @@ async def convert_video_format( stderr=subprocess.PIPE, ) - stdout, stderr = await process.communicate() + _stdout, stderr = await process.communicate() if process.returncode != 0: # 清理可能已生成但无效的临时文件 diff --git a/astrbot/core/utils/session_waiter.py b/astrbot/core/utils/session_waiter.py index 9cb8743dd..841e87ad4 100644 --- a/astrbot/core/utils/session_waiter.py +++ b/astrbot/core/utils/session_waiter.py @@ -19,6 +19,7 @@ class SessionController: """控制一个 Session 是否已经结束""" def __init__(self) -> None: + self.tasks = set() self.future = asyncio.Future() self.current_event: asyncio.Event | None = None """当前正在等待的所用的异步事件""" @@ -69,13 +70,17 @@ class SessionController: self.current_event = new_event self.timeout = timeout - asyncio.create_task(self._holding(new_event, timeout)) # 开始新的 keep + _holding_task = asyncio.create_task( + self._holding(new_event, timeout) + ) # 开始新的 keep + self.tasks.add(_holding_task) + _holding_task.add_done_callback(self.tasks.discard) async def _holding(self, event: asyncio.Event, timeout: float) -> None: """等待事件结束或超时""" try: await asyncio.wait_for(event.wait(), timeout) - except asyncio.TimeoutError: + except TimeoutError: if not self.future.done(): self.future.set_exception(TimeoutError("等待超时")) except asyncio.CancelledError: diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 241d5b155..4717c4f81 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -23,13 +23,17 @@ class NetworkRenderStrategy(RenderStrategy): self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT else: self.BASE_RENDER_URL = self._clean_url(base_url) - + self.tasks = set() self.endpoints = [self.BASE_RENDER_URL] self.template_manager = TemplateManager() async def initialize(self) -> None: if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT: - asyncio.create_task(self.get_official_endpoints()) + _get_official_endpoints_task = asyncio.create_task( + self.get_official_endpoints() + ) + self.tasks.add(_get_official_endpoints_task) + _get_official_endpoints_task.add_done_callback(self.tasks.discard) async def get_template(self, name: str = "base") -> str: """通过名称获取文转图 HTML 模板""" diff --git a/astrbot/core/utils/t2i/template_manager.py b/astrbot/core/utils/t2i/template_manager.py index 5172b7217..72eb2216e 100644 --- a/astrbot/core/utils/t2i/template_manager.py +++ b/astrbot/core/utils/t2i/template_manager.py @@ -2,6 +2,7 @@ import os import shutil +from typing import ClassVar from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_path @@ -12,7 +13,7 @@ class TemplateManager: 所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。 """ - CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"] + CORE_TEMPLATES: ClassVar[tuple[str, ...]] = ("base.html", "astrbot_powershell.html") def __init__(self) -> None: self.builtin_template_dir = os.path.join( diff --git a/astrbot/dashboard/routes/backup.py b/astrbot/dashboard/routes/backup.py index 5c73f6644..bbcea104d 100644 --- a/astrbot/dashboard/routes/backup.py +++ b/astrbot/dashboard/routes/backup.py @@ -89,6 +89,7 @@ class BackupRoute(Route): core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) + self.tasks: set = set() self.db = db self.core_lifecycle = core_lifecycle self.backup_dir = get_astrbot_backups_path() @@ -335,12 +336,12 @@ class BackupRoute(Route): "page_size": page_size, } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"获取备份列表失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取备份列表失败: {e!s}").__dict__ + return Response().error(f"获取备份列表失败: {e!s}").to_json() async def export_backup(self): """创建备份 @@ -356,8 +357,11 @@ class BackupRoute(Route): self._init_task(task_id, "export", "pending") # 启动后台导出任务 - asyncio.create_task(self._background_export_task(task_id)) - + _background_export_task = asyncio.create_task( + self._background_export_task(task_id) + ) + self.tasks.add(_background_export_task) + _background_export_task.add_done_callback(self.tasks.discard) return ( Response() .ok( @@ -366,12 +370,12 @@ class BackupRoute(Route): "message": "export task created, processing in background", } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"创建备份失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"创建备份失败: {e!s}").__dict__ + return Response().error(f"创建备份失败: {e!s}").to_json() async def _background_export_task(self, task_id: str) -> None: """后台导出任务""" @@ -426,11 +430,11 @@ class BackupRoute(Route): try: files = await request.files if "file" not in files: - return Response().error("缺少备份文件").__dict__ + return Response().error("缺少备份文件").to_json() file = files["file"] if not file.filename or not file.filename.endswith(".zip"): - return Response().error("请上传 ZIP 格式的备份文件").__dict__ + return Response().error("请上传 ZIP 格式的备份文件").to_json() # 清洗文件名并生成唯一名称,防止路径遍历和覆盖 safe_filename = secure_filename(file.filename) @@ -454,12 +458,12 @@ class BackupRoute(Route): "size": (await anyio.Path(zip_path).stat()).st_size, } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"上传备份文件失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"上传备份文件失败: {e!s}").__dict__ + return Response().error(f"上传备份文件失败: {e!s}").to_json() async def upload_init(self): """初始化分片上传 @@ -481,13 +485,13 @@ class BackupRoute(Route): total_size = data.get("total_size", 0) if not filename: - return Response().error("缺少 filename 参数").__dict__ + return Response().error("缺少 filename 参数").to_json() if not filename.endswith(".zip"): - return Response().error("请上传 ZIP 格式的备份文件").__dict__ + return Response().error("请上传 ZIP 格式的备份文件").to_json() if total_size <= 0: - return Response().error("无效的文件大小").__dict__ + return Response().error("无效的文件大小").to_json() # 由后端计算分片总数,确保前后端一致 import math @@ -533,12 +537,12 @@ class BackupRoute(Route): "filename": unique_filename, } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"初始化分片上传失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"初始化分片上传失败: {e!s}").__dict__ + return Response().error(f"初始化分片上传失败: {e!s}").to_json() async def upload_chunk(self): """上传分片 @@ -562,25 +566,25 @@ class BackupRoute(Route): chunk_index_str = form.get("chunk_index") if not upload_id or chunk_index_str is None: - return Response().error("缺少必要参数").__dict__ + return Response().error("缺少必要参数").to_json() try: chunk_index = int(chunk_index_str) except ValueError: - return Response().error("无效的分片索引").__dict__ + return Response().error("无效的分片索引").to_json() if "chunk" not in files: - return Response().error("缺少分片数据").__dict__ + return Response().error("缺少分片数据").to_json() # 验证上传会话 if upload_id not in self.upload_sessions: - return Response().error("上传会话不存在或已过期").__dict__ + return Response().error("上传会话不存在或已过期").to_json() session = self.upload_sessions[upload_id] # 验证分片索引 if chunk_index < 0 or chunk_index >= session["total_chunks"]: - return Response().error("分片索引超出范围").__dict__ + return Response().error("分片索引超出范围").to_json() # 保存分片 chunk_file = files["chunk"] @@ -608,12 +612,12 @@ class BackupRoute(Route): "chunk_index": chunk_index, } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"上传分片失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"上传分片失败: {e!s}").__dict__ + return Response().error(f"上传分片失败: {e!s}").to_json() def _mark_backup_as_uploaded(self, zip_path: str) -> None: """修改备份文件的 manifest.json,将 origin 设置为 uploaded @@ -661,11 +665,11 @@ class BackupRoute(Route): upload_id = data.get("upload_id") if not upload_id: - return Response().error("缺少 upload_id 参数").__dict__ + return Response().error("缺少 upload_id 参数").to_json() # 验证上传会话 if upload_id not in self.upload_sessions: - return Response().error("上传会话不存在或已过期").__dict__ + return Response().error("上传会话不存在或已过期").to_json() session = self.upload_sessions[upload_id] @@ -678,7 +682,7 @@ class BackupRoute(Route): return ( Response() .error(f"分片不完整,缺少: {sorted(missing)[:10]}...") - .__dict__ + .to_json() ) # 合并分片 @@ -723,7 +727,7 @@ class BackupRoute(Route): "size": file_size, } ) - .__dict__ + .to_json() ) except Exception as e: # 如果合并失败,删除不完整的文件 @@ -734,7 +738,7 @@ class BackupRoute(Route): except Exception as e: logger.error(f"完成分片上传失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"完成分片上传失败: {e!s}").__dict__ + return Response().error(f"完成分片上传失败: {e!s}").to_json() async def upload_abort(self): """取消分片上传 @@ -749,22 +753,22 @@ class BackupRoute(Route): upload_id = data.get("upload_id") if not upload_id: - return Response().error("缺少 upload_id 参数").__dict__ + return Response().error("缺少 upload_id 参数").to_json() if upload_id not in self.upload_sessions: # 会话已不存在,可能已过期或已完成 - return Response().ok(message="上传已取消").__dict__ + return Response().ok(message="上传已取消").to_json() # 清理会话 await self._cleanup_upload_session(upload_id) logger.info(f"取消分片上传: {upload_id}") - return Response().ok(message="上传已取消").__dict__ + return Response().ok(message="上传已取消").to_json() except Exception as e: logger.error(f"取消上传失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"取消上传失败: {e!s}").__dict__ + return Response().error(f"取消上传失败: {e!s}").to_json() async def check_backup(self): """预检查备份文件 @@ -782,15 +786,15 @@ class BackupRoute(Route): data = await request.json filename = data.get("filename") if not filename: - return Response().error("缺少 filename 参数").__dict__ + return Response().error("缺少 filename 参数").to_json() # 安全检查 - 防止路径遍历 if ".." in filename or "/" in filename or "\\" in filename: - return Response().error("无效的文件名").__dict__ + return Response().error("无效的文件名").to_json() zip_path = os.path.join(self.backup_dir, filename) if not await anyio.Path(zip_path).exists(): - return Response().error(f"备份文件不存在: {filename}").__dict__ + return Response().error(f"备份文件不存在: {filename}").to_json() # 获取知识库管理器(用于构造 importer) kb_manager = getattr(self.core_lifecycle, "kb_manager", None) @@ -804,11 +808,11 @@ class BackupRoute(Route): # 执行预检查 check_result = importer.pre_check(zip_path) - return Response().ok(check_result.to_dict()).__dict__ + return Response().ok(check_result.to_dict()).to_json() except Exception as e: logger.error(f"预检查备份文件失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"预检查备份文件失败: {e!s}").__dict__ + return Response().error(f"预检查备份文件失败: {e!s}").to_json() async def import_backup(self): """执行备份导入 @@ -829,22 +833,22 @@ class BackupRoute(Route): confirmed = data.get("confirmed", False) if not filename: - return Response().error("缺少 filename 参数").__dict__ + return Response().error("缺少 filename 参数").to_json() if not confirmed: return ( Response() .error("请先确认导入。导入将会清空并覆盖现有数据,此操作不可撤销。") - .__dict__ + .to_json() ) # 安全检查 - 防止路径遍历 if ".." in filename or "/" in filename or "\\" in filename: - return Response().error("无效的文件名").__dict__ + return Response().error("无效的文件名").to_json() zip_path = os.path.join(self.backup_dir, filename) if not await anyio.Path(zip_path).exists(): - return Response().error(f"备份文件不存在: {filename}").__dict__ + return Response().error(f"备份文件不存在: {filename}").to_json() # 生成任务ID task_id = str(uuid.uuid4()) @@ -853,7 +857,11 @@ class BackupRoute(Route): self._init_task(task_id, "import", "pending") # 启动后台导入任务 - asyncio.create_task(self._background_import_task(task_id, zip_path)) + _background_import_task = asyncio.create_task( + self._background_import_task(task_id, zip_path) + ) + self.tasks.add(_background_import_task) + _background_import_task.add_done_callback(self.tasks.discard) return ( Response() @@ -863,12 +871,12 @@ class BackupRoute(Route): "message": "import task created, processing in background", } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"导入备份失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"导入备份失败: {e!s}").__dict__ + return Response().error(f"导入备份失败: {e!s}").to_json() async def _background_import_task(self, task_id: str, zip_path: str) -> None: """后台导入任务""" @@ -921,10 +929,10 @@ class BackupRoute(Route): try: task_id = request.args.get("task_id") if not task_id: - return Response().error("缺少参数 task_id").__dict__ + return Response().error("缺少参数 task_id").to_json() if task_id not in self.backup_tasks: - return Response().error("找不到该任务").__dict__ + return Response().error("找不到该任务").to_json() task_info = self.backup_tasks[task_id] status = task_info["status"] @@ -947,11 +955,11 @@ class BackupRoute(Route): if status == "failed": response_data["error"] = task_info["error"] - return Response().ok(response_data).__dict__ + return Response().ok(response_data).to_json() except Exception as e: logger.error(f"获取任务进度失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取任务进度失败: {e!s}").__dict__ + return Response().error(f"获取任务进度失败: {e!s}").to_json() async def download_backup(self): """下载备份文件 @@ -968,16 +976,16 @@ class BackupRoute(Route): token = request.args.get("token") if not filename: - return Response().error("缺少参数 filename").__dict__ + return Response().error("缺少参数 filename").to_json() if not token: - return Response().error("缺少参数 token").__dict__ + return Response().error("缺少参数 token").to_json() # 验证 JWT token try: jwt_secret = self.config.get("dashboard", {}).get("jwt_secret") if not jwt_secret: - return Response().error("服务器配置错误").__dict__ + return Response().error("服务器配置错误").to_json() # Verify JWT token with strict security options jwt.decode( @@ -991,17 +999,17 @@ class BackupRoute(Route): }, ) except jwt.ExpiredSignatureError: - return Response().error("Token 已过期,请刷新页面后重试").__dict__ + return Response().error("Token 已过期,请刷新页面后重试").to_json() except jwt.InvalidTokenError: - return Response().error("Token 无效").__dict__ + return Response().error("Token 无效").to_json() # 安全检查 - 防止路径遍历 if ".." in filename or "/" in filename or "\\" in filename: - return Response().error("无效的文件名").__dict__ + return Response().error("无效的文件名").to_json() file_path = os.path.join(self.backup_dir, filename) if not await anyio.Path(file_path).exists(): - return Response().error("备份文件不存在").__dict__ + return Response().error("备份文件不存在").to_json() return await send_file( file_path, @@ -1012,7 +1020,7 @@ class BackupRoute(Route): except Exception as e: logger.error(f"下载备份失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"下载备份失败: {e!s}").__dict__ + return Response().error(f"下载备份失败: {e!s}").to_json() async def delete_backup(self): """删除备份文件 @@ -1024,22 +1032,22 @@ class BackupRoute(Route): data = await request.json filename = data.get("filename") if not filename: - return Response().error("缺少参数 filename").__dict__ + return Response().error("缺少参数 filename").to_json() # 安全检查 - 防止路径遍历 if ".." in filename or "/" in filename or "\\" in filename: - return Response().error("无效的文件名").__dict__ + return Response().error("无效的文件名").to_json() file_path = os.path.join(self.backup_dir, filename) if not await anyio.Path(file_path).exists(): - return Response().error("备份文件不存在").__dict__ + return Response().error("备份文件不存在").to_json() await anyio.Path(file_path).unlink() - return Response().ok(message="删除备份成功").__dict__ + return Response().ok(message="删除备份成功").to_json() except Exception as e: logger.error(f"删除备份失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"删除备份失败: {e!s}").__dict__ + return Response().error(f"删除备份失败: {e!s}").to_json() async def rename_backup(self): """重命名备份文件 @@ -1054,14 +1062,14 @@ class BackupRoute(Route): new_name = data.get("new_name") if not filename: - return Response().error("缺少参数 filename").__dict__ + return Response().error("缺少参数 filename").to_json() if not new_name: - return Response().error("缺少参数 new_name").__dict__ + return Response().error("缺少参数 new_name").to_json() # 安全检查 - 防止路径遍历 if ".." in filename or "/" in filename or "\\" in filename: - return Response().error("无效的文件名").__dict__ + return Response().error("无效的文件名").to_json() # 清洗新文件名(移除路径和危险字符) new_name = secure_filename(new_name) @@ -1072,7 +1080,7 @@ class BackupRoute(Route): # 验证新文件名不为空 if not new_name or new_name.replace("_", "") == "": - return Response().error("新文件名无效").__dict__ + return Response().error("新文件名无效").to_json() # 强制使用 .zip 扩展名 new_filename = f"{new_name}.zip" @@ -1080,12 +1088,12 @@ class BackupRoute(Route): # 检查原文件是否存在 old_path = os.path.join(self.backup_dir, filename) if not await anyio.Path(old_path).exists(): - return Response().error("备份文件不存在").__dict__ + return Response().error("备份文件不存在").to_json() # 检查新文件名是否已存在 new_path = os.path.join(self.backup_dir, new_filename) if await anyio.Path(new_path).exists(): - return Response().error(f"文件名 '{new_filename}' 已存在").__dict__ + return Response().error(f"文件名 '{new_filename}' 已存在").to_json() # 执行重命名 os.rename(old_path, new_path) @@ -1100,9 +1108,9 @@ class BackupRoute(Route): "new_filename": new_filename, } ) - .__dict__ + .to_json() ) except Exception as e: logger.error(f"重命名备份失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"重命名备份失败: {e!s}").__dict__ + return Response().error(f"重命名备份失败: {e!s}").to_json() diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index b553d7cb1..97509def8 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -1524,9 +1524,7 @@ class ConfigRoute(Route): if plugin_md.name == plugin_name: if not plugin_md.config: break - ret["config"] = ( - plugin_md.config - ) # 这是自定义的 Dict 类(AstrBotConfig) + ret["config"] = plugin_md.config # 这是自定义的 Dict 类(AstrBotConfig) ret["metadata"] = { plugin_name: { "description": f"{plugin_name} 配置", diff --git a/astrbot/dashboard/routes/knowledge_base.py b/astrbot/dashboard/routes/knowledge_base.py index fd608c86d..4673d9e3a 100644 --- a/astrbot/dashboard/routes/knowledge_base.py +++ b/astrbot/dashboard/routes/knowledge_base.py @@ -14,8 +14,8 @@ from astrbot.core import logger from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.provider.provider import EmbeddingProvider, RerankProvider from astrbot.core.utils.astrbot_path import get_astrbot_temp_path +from astrbot.dashboard.utils import generate_tsne_visualization -from ..utils import generate_tsne_visualization from .route import Response, Route, RouteContext @@ -31,6 +31,7 @@ class KnowledgeBaseRoute(Route): core_lifecycle: AstrBotCoreLifecycle, ) -> None: super().__init__(context) + self.tasks: set = set() self.core_lifecycle = core_lifecycle self.kb_manager = None # 延迟初始化 self.kb_db = None @@ -320,14 +321,14 @@ class KnowledgeBaseRoute(Route): return ( Response() .ok({"items": kb_list, "page": page, "page_size": page_size}) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取知识库列表失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取知识库列表失败: {e!s}").__dict__ + return Response().error(f"获取知识库列表失败: {e!s}").to_json() async def create_kb(self): """创建知识库 @@ -349,7 +350,7 @@ class KnowledgeBaseRoute(Route): data = await request.json kb_name = data.get("kb_name") if not kb_name: - return Response().error("知识库名称不能为空").__dict__ + return Response().error("知识库名称不能为空").to_json() description = data.get("description") emoji = data.get("emoji") @@ -363,13 +364,13 @@ class KnowledgeBaseRoute(Route): # pre-check embedding dim if not embedding_provider_id: - return Response().error("缺少参数 embedding_provider_id").__dict__ + return Response().error("缺少参数 embedding_provider_id").to_json() prv = await kb_manager.provider_manager.get_provider_by_id( embedding_provider_id, ) if not prv or not isinstance(prv, EmbeddingProvider): return ( - Response().error(f"嵌入模型不存在或类型错误({type(prv)})").__dict__ + Response().error(f"嵌入模型不存在或类型错误({type(prv)})").to_json() ) try: vec = await prv.get_embedding("astrbot") @@ -378,7 +379,7 @@ class KnowledgeBaseRoute(Route): f"嵌入向量维度不匹配,实际是 {len(vec)},然而配置是 {prv.get_dim()}", ) except Exception as e: - return Response().error(f"测试嵌入模型失败: {e!s}").__dict__ + return Response().error(f"测试嵌入模型失败: {e!s}").to_json() # pre-check rerank if rerank_provider_id: rerank_prv = await kb_manager.provider_manager.get_provider_by_id( @@ -387,9 +388,9 @@ class KnowledgeBaseRoute(Route): if rerank_prv is not None and not isinstance( rerank_prv, RerankProvider ): - return Response().error("重排序模型类型错误").__dict__ + return Response().error("重排序模型类型错误").to_json() if not rerank_prv: - return Response().error("重排序模型不存在").__dict__ + return Response().error("重排序模型不存在").to_json() # 检查重排序模型可用性 try: res = await rerank_prv.rerank( @@ -402,7 +403,7 @@ class KnowledgeBaseRoute(Route): return ( Response() .error(f"测试重排序模型失败: {e!s},请检查平台日志输出。") - .__dict__ + .to_json() ) kb_helper = await kb_manager.create_kb( @@ -419,14 +420,14 @@ class KnowledgeBaseRoute(Route): ) kb = kb_helper.kb - return Response().ok(kb.model_dump(), "创建知识库成功").__dict__ + return Response().ok(kb.model_dump(), "创建知识库成功").to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"创建知识库失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"创建知识库失败: {e!s}").__dict__ + return Response().error(f"创建知识库失败: {e!s}").to_json() async def get_kb(self): """获取知识库详情 @@ -438,21 +439,21 @@ class KnowledgeBaseRoute(Route): kb_manager = self._get_kb_manager() kb_id = request.args.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() kb = kb_helper.kb - return Response().ok(kb.model_dump()).__dict__ + return Response().ok(kb.model_dump()).to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取知识库详情失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取知识库详情失败: {e!s}").__dict__ + return Response().error(f"获取知识库详情失败: {e!s}").to_json() async def update_kb(self): """更新知识库 @@ -476,7 +477,7 @@ class KnowledgeBaseRoute(Route): kb_id = data.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() kb_name = data.get("kb_name") description = data.get("description") @@ -505,7 +506,7 @@ class KnowledgeBaseRoute(Route): top_m_final, ] ): - return Response().error("至少需要提供一个更新字段").__dict__ + return Response().error("至少需要提供一个更新字段").to_json() kb_helper = await kb_manager.update_kb( kb_id=kb_id, @@ -522,17 +523,17 @@ class KnowledgeBaseRoute(Route): ) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() kb = kb_helper.kb - return Response().ok(kb.model_dump(), "更新知识库成功").__dict__ + return Response().ok(kb.model_dump(), "更新知识库成功").to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"更新知识库失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"更新知识库失败: {e!s}").__dict__ + return Response().error(f"更新知识库失败: {e!s}").to_json() async def delete_kb(self): """删除知识库 @@ -546,20 +547,20 @@ class KnowledgeBaseRoute(Route): kb_id = data.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() success = await kb_manager.delete_kb(kb_id) if not success: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() - return Response().ok(message="删除知识库成功").__dict__ + return Response().ok(message="删除知识库成功").to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"删除知识库失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"删除知识库失败: {e!s}").__dict__ + return Response().error(f"删除知识库失败: {e!s}").to_json() async def get_kb_stats(self): """获取知识库统计信息 @@ -571,11 +572,11 @@ class KnowledgeBaseRoute(Route): kb_manager = self._get_kb_manager() kb_id = request.args.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() kb = kb_helper.kb stats = { @@ -587,14 +588,14 @@ class KnowledgeBaseRoute(Route): "updated_at": kb.updated_at.isoformat(), } - return Response().ok(stats).__dict__ + return Response().ok(stats).to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取知识库统计失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取知识库统计失败: {e!s}").__dict__ + return Response().error(f"获取知识库统计失败: {e!s}").to_json() # ===== 文档管理 API ===== @@ -610,10 +611,10 @@ class KnowledgeBaseRoute(Route): kb_manager = self._get_kb_manager() kb_id = request.args.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 100, type=int) @@ -628,15 +629,15 @@ class KnowledgeBaseRoute(Route): return ( Response() .ok({"items": doc_list, "page": page, "page_size": page_size}) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取文档列表失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取文档列表失败: {e!s}").__dict__ + return Response().error(f"获取文档列表失败: {e!s}").to_json() async def upload_document(self): """上传文档 @@ -673,7 +674,7 @@ class KnowledgeBaseRoute(Route): if content_type and "multipart/form-data" not in content_type: return ( - Response().error("Content-Type 须为 multipart/form-data").__dict__ + Response().error("Content-Type 须为 multipart/form-data").to_json() ) form_data = await request.form files = await request.files @@ -685,7 +686,7 @@ class KnowledgeBaseRoute(Route): tasks_limit = int(form_data.get("tasks_limit", 3)) max_retries = int(form_data.get("max_retries", 3)) if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() # 收集所有文件 file_list = [] @@ -696,11 +697,11 @@ class KnowledgeBaseRoute(Route): file_list.extend(file_items) if not file_list: - return Response().error("缺少文件").__dict__ + return Response().error("缺少文件").to_json() # 限制文件数量 if len(file_list) > 10: - return Response().error("最多只能上传10个文件").__dict__ + return Response().error("最多只能上传10个文件").to_json() # 处理每个文件 for file in file_list: @@ -738,7 +739,7 @@ class KnowledgeBaseRoute(Route): # 获取知识库 kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() # 生成任务ID task_id = str(uuid.uuid4()) @@ -747,7 +748,7 @@ class KnowledgeBaseRoute(Route): self._init_task(task_id, status="pending") # 启动后台任务 - asyncio.create_task( + _background_upload_task = asyncio.create_task( self._background_upload_task( task_id=task_id, kb_helper=kb_helper, @@ -759,6 +760,8 @@ class KnowledgeBaseRoute(Route): max_retries=max_retries, ), ) + self.tasks.add(_background_upload_task) + _background_upload_task.add_done_callback(self.tasks.discard) return ( Response() @@ -769,15 +772,15 @@ class KnowledgeBaseRoute(Route): "message": "task created, processing in background", }, ) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"上传文档失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"上传文档失败: {e!s}").__dict__ + return Response().error(f"上传文档失败: {e!s}").to_json() def _validate_import_request(self, data: dict): kb_id = data.get("kb_id") @@ -827,7 +830,7 @@ class KnowledgeBaseRoute(Route): # 获取知识库 kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() # 生成任务ID task_id = str(uuid.uuid4()) @@ -836,7 +839,7 @@ class KnowledgeBaseRoute(Route): self._init_task(task_id, status="pending") # 启动后台任务 - asyncio.create_task( + _background_import_task = asyncio.create_task( self._background_import_task( task_id=task_id, kb_helper=kb_helper, @@ -846,6 +849,8 @@ class KnowledgeBaseRoute(Route): max_retries=max_retries, ), ) + self.tasks.add(_background_import_task) + _background_import_task.add_done_callback(self.tasks.discard) return ( Response() @@ -856,15 +861,15 @@ class KnowledgeBaseRoute(Route): "message": "import task created, processing in background", }, ) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"导入文档失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"导入文档失败: {e!s}").__dict__ + return Response().error(f"导入文档失败: {e!s}").to_json() async def get_upload_progress(self): """获取上传进度和结果 @@ -881,11 +886,11 @@ class KnowledgeBaseRoute(Route): try: task_id = request.args.get("task_id") if not task_id: - return Response().error("缺少参数 task_id").__dict__ + return Response().error("缺少参数 task_id").to_json() # 检查任务是否存在 if task_id not in self.upload_tasks: - return Response().error("找不到该任务").__dict__ + return Response().error("找不到该任务").to_json() task_info = self.upload_tasks[task_id] status = task_info["status"] @@ -912,12 +917,12 @@ class KnowledgeBaseRoute(Route): if status == "failed": response_data["error"] = task_info["error"] - return Response().ok(response_data).__dict__ + return Response().ok(response_data).to_json() except Exception as e: logger.error(f"获取上传进度失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取上传进度失败: {e!s}").__dict__ + return Response().error(f"获取上传进度失败: {e!s}").to_json() async def get_document(self): """获取文档详情 @@ -929,26 +934,26 @@ class KnowledgeBaseRoute(Route): kb_manager = self._get_kb_manager() kb_id = request.args.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() doc_id = request.args.get("doc_id") if not doc_id: - return Response().error("缺少参数 doc_id").__dict__ + return Response().error("缺少参数 doc_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() doc = await kb_helper.get_document(doc_id) if not doc: - return Response().error("文档不存在").__dict__ + return Response().error("文档不存在").to_json() - return Response().ok(doc.model_dump()).__dict__ + return Response().ok(doc.model_dump()).to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取文档详情失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取文档详情失败: {e!s}").__dict__ + return Response().error(f"获取文档详情失败: {e!s}").to_json() async def delete_document(self): """删除文档 @@ -963,24 +968,24 @@ class KnowledgeBaseRoute(Route): kb_id = data.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() doc_id = data.get("doc_id") if not doc_id: - return Response().error("缺少参数 doc_id").__dict__ + return Response().error("缺少参数 doc_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() await kb_helper.delete_document(doc_id) - return Response().ok(message="删除文档成功").__dict__ + return Response().ok(message="删除文档成功").to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"删除文档失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"删除文档失败: {e!s}").__dict__ + return Response().error(f"删除文档失败: {e!s}").to_json() async def delete_chunk(self): """删除文本块 @@ -995,27 +1000,27 @@ class KnowledgeBaseRoute(Route): kb_id = data.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() chunk_id = data.get("chunk_id") if not chunk_id: - return Response().error("缺少参数 chunk_id").__dict__ + return Response().error("缺少参数 chunk_id").to_json() doc_id = data.get("doc_id") if not doc_id: - return Response().error("缺少参数 doc_id").__dict__ + return Response().error("缺少参数 doc_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() await kb_helper.delete_chunk(chunk_id, doc_id) - return Response().ok(message="删除文本块成功").__dict__ + return Response().ok(message="删除文本块成功").to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"删除文本块失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"删除文本块失败: {e!s}").__dict__ + return Response().error(f"删除文本块失败: {e!s}").to_json() async def list_chunks(self): """获取块列表 @@ -1032,14 +1037,14 @@ class KnowledgeBaseRoute(Route): page = request.args.get("page", 1, type=int) page_size = request.args.get("page_size", 100, type=int) if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() if not doc_id: - return Response().error("缺少参数 doc_id").__dict__ + return Response().error("缺少参数 doc_id").to_json() kb_helper = await kb_manager.get_kb(kb_id) offset = (page - 1) * page_size limit = page_size if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() chunk_list = await kb_helper.get_chunks_by_doc_id( doc_id=doc_id, offset=offset, @@ -1055,14 +1060,14 @@ class KnowledgeBaseRoute(Route): "total": await kb_helper.get_chunk_count_by_doc_id(doc_id), }, ) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"获取块列表失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"获取块列表失败: {e!s}").__dict__ + return Response().error(f"获取块列表失败: {e!s}").to_json() # ===== 检索 API ===== @@ -1084,9 +1089,9 @@ class KnowledgeBaseRoute(Route): debug = data.get("debug", False) if not query: - return Response().error("缺少参数 query").__dict__ + return Response().error("缺少参数 query").to_json() if not kb_names or not isinstance(kb_names, list): - return Response().error("缺少参数 kb_names 或格式错误").__dict__ + return Response().error("缺少参数 kb_names 或格式错误").to_json() top_k = data.get("top_k", 5) @@ -1120,14 +1125,14 @@ class KnowledgeBaseRoute(Route): logger.error(traceback.format_exc()) response_data["visualization_error"] = str(e) - return Response().ok(response_data).__dict__ + return Response().ok(response_data).to_json() except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"检索失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"检索失败: {e!s}").__dict__ + return Response().error(f"检索失败: {e!s}").to_json() async def upload_document_from_url(self): """从 URL 上传文档 @@ -1150,11 +1155,11 @@ class KnowledgeBaseRoute(Route): kb_id = data.get("kb_id") if not kb_id: - return Response().error("缺少参数 kb_id").__dict__ + return Response().error("缺少参数 kb_id").to_json() url = data.get("url") if not url: - return Response().error("缺少参数 url").__dict__ + return Response().error("缺少参数 url").to_json() chunk_size = data.get("chunk_size", 512) chunk_overlap = data.get("chunk_overlap", 50) @@ -1167,7 +1172,7 @@ class KnowledgeBaseRoute(Route): # 获取知识库 kb_helper = await kb_manager.get_kb(kb_id) if not kb_helper: - return Response().error("知识库不存在").__dict__ + return Response().error("知识库不存在").to_json() # 生成任务ID task_id = str(uuid.uuid4()) @@ -1176,7 +1181,7 @@ class KnowledgeBaseRoute(Route): self._init_task(task_id, status="pending") # 启动后台任务 - asyncio.create_task( + _background_upload_from_url_task = asyncio.create_task( self._background_upload_from_url_task( task_id=task_id, kb_helper=kb_helper, @@ -1190,6 +1195,8 @@ class KnowledgeBaseRoute(Route): cleaning_provider_id=cleaning_provider_id, ), ) + self.tasks.add(_background_upload_from_url_task) + _background_upload_from_url_task.add_done_callback(self.tasks.discard) return ( Response() @@ -1200,15 +1207,15 @@ class KnowledgeBaseRoute(Route): "message": "URL upload task created, processing in background", }, ) - .__dict__ + .to_json() ) except ValueError as e: - return Response().error(str(e)).__dict__ + return Response().error(str(e)).to_json() except Exception as e: logger.error(f"从URL上传文档失败: {e}") logger.error(traceback.format_exc()) - return Response().error(f"从URL上传文档失败: {e!s}").__dict__ + return Response().error(f"从URL上传文档失败: {e!s}").to_json() async def _background_upload_from_url_task( self, diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 520385655..e9742ea78 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -8,7 +8,6 @@ from collections.abc import Callable from datetime import datetime from ipaddress import IPv4Address, IPv6Address, ip_address from pathlib import Path -from typing import Protocol import anyio import jwt @@ -64,10 +63,6 @@ from .routes.api_key import ALL_OPEN_API_SCOPES _BUNDLED_DIST = Path(__file__).parent / "dist" -class _AddrWithPort(Protocol): - port: int - - APP: Quart diff --git a/docs/scripts/upload_doc_images_to_r2.py b/docs/scripts/upload_doc_images_to_r2.py index e9c9d4bad..704c84e3a 100644 --- a/docs/scripts/upload_doc_images_to_r2.py +++ b/docs/scripts/upload_doc_images_to_r2.py @@ -6,7 +6,6 @@ import logging import re import shutil import subprocess -import sys import tempfile from collections.abc import Iterable, Sequence from pathlib import Path diff --git a/scripts/fw2hw.sh b/scripts/fw2hw.sh old mode 100644 new mode 100755 index 9db2b7cf5..82f0f5af1 --- a/scripts/fw2hw.sh +++ b/scripts/fw2hw.sh @@ -1 +1,3 @@ +echo "全角转半角脚本,依赖uconv工具" +echo "Ctrl + C 结束" find . -name "*.py" -exec sh -c 'uconv -x "Fullwidth-Halfwidth" "$1" > "$1.tmp" && mv "$1.tmp" "$1"' _ {} \; diff --git a/tests/fixtures/helpers.py b/tests/fixtures/helpers.py index 21fa8d468..ec4d09ff1 100644 --- a/tests/fixtures/helpers.py +++ b/tests/fixtures/helpers.py @@ -6,7 +6,8 @@ import shutil from dataclasses import dataclass, field from pathlib import Path -from typing import Any, Callable +from typing import Any +from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock from astrbot.core.message.components import BaseMessageComponent