chore: fw2hw

This commit is contained in:
LIghtJUNction
2026-03-20 00:44:08 +08:00
parent 92bae1fdae
commit ef5dac77a2
26 changed files with 278 additions and 333 deletions
+17 -17
View File
@@ -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]
@@ -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(
+1 -71
View File
@@ -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.
+1 -3
View File
@@ -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:
@@ -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(
@@ -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
@@ -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:
@@ -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:
+4 -6
View File
@@ -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:
+3 -3
View File
@@ -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,
)
+15 -9
View File
@@ -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(
+11 -5
View File
@@ -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:
+2 -3
View File
@@ -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):
+2 -2
View File
@@ -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:
+11 -9
View File
@@ -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:
+3 -3
View File
@@ -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:
# 清理可能已生成但无效的临时文件
+7 -2
View File
@@ -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:
+6 -2
View File
@@ -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 模板"""
+2 -1
View File
@@ -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(
+76 -68
View File
@@ -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()
+1 -3
View File
@@ -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} 配置",
+103 -96
View File
@@ -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,
-5
View File
@@ -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
-1
View File
@@ -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
Regular → Executable
+2
View File
@@ -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"' _ {} \;
+2 -1
View File
@@ -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