mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-28 02:27:46 +08:00
chore: fw2hw
This commit is contained in:
+17
-17
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
# 清理可能已生成但无效的临时文件
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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} 配置",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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"' _ {} \;
|
||||
|
||||
Vendored
+2
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user