mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 08:57:09 +08:00
refactor: 收口 V3 分层架构与插件兼容边界
This commit is contained in:
+27
-21
@@ -25,6 +25,16 @@ from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryI
|
||||
from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo
|
||||
from app.schemas.dashboard import ProcessInfo as _SchemaProcessInfo
|
||||
from version import APP_VERSION
|
||||
from app.foundation.environment import (
|
||||
is_aarch,
|
||||
is_aarch64,
|
||||
is_docker,
|
||||
is_frozen,
|
||||
is_macos,
|
||||
is_windows,
|
||||
is_x86_32,
|
||||
is_x86_64,
|
||||
)
|
||||
|
||||
|
||||
# Linux amd64/arm64 UAPI: _IOR(BTRFS_IOCTL_MAGIC, 31, struct btrfs_ioctl_fs_info_args)
|
||||
@@ -142,7 +152,7 @@ class SystemUtils:
|
||||
"""
|
||||
判断是否为Docker环境
|
||||
"""
|
||||
return Path("/.dockerenv").exists()
|
||||
return is_docker()
|
||||
|
||||
@staticmethod
|
||||
def is_synology() -> bool:
|
||||
@@ -158,50 +168,49 @@ class SystemUtils:
|
||||
"""
|
||||
判断是否为Windows系统
|
||||
"""
|
||||
return os.name == "nt"
|
||||
return is_windows()
|
||||
|
||||
@staticmethod
|
||||
def is_frozen() -> bool:
|
||||
"""
|
||||
判断是否为冻结的二进制文件
|
||||
"""
|
||||
return getattr(sys, 'frozen', False)
|
||||
return is_frozen()
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""
|
||||
判断是否为MacOS系统
|
||||
"""
|
||||
return platform.system() == 'Darwin'
|
||||
return is_macos()
|
||||
|
||||
@staticmethod
|
||||
def is_aarch64() -> bool:
|
||||
"""
|
||||
判断是否为ARM64架构
|
||||
"""
|
||||
return platform.machine().lower() in ('aarch64', 'arm64')
|
||||
return is_aarch64()
|
||||
|
||||
@staticmethod
|
||||
def is_aarch() -> bool:
|
||||
"""
|
||||
判断是否为ARM32架构
|
||||
"""
|
||||
arch_name = platform.machine().lower()
|
||||
return arch_name.startswith(('arm', 'aarch')) and arch_name not in ('aarch64', 'arm64')
|
||||
return is_aarch()
|
||||
|
||||
@staticmethod
|
||||
def is_x86_64() -> bool:
|
||||
"""
|
||||
判断是否为AMD64架构
|
||||
"""
|
||||
return platform.machine().lower() in ('amd64', 'x86_64')
|
||||
return is_x86_64()
|
||||
|
||||
@staticmethod
|
||||
def is_x86_32() -> bool:
|
||||
"""
|
||||
判断是否为AMD32架构
|
||||
"""
|
||||
return platform.machine().lower() in ('i386', 'i686', 'x86', '386', 'x86_32')
|
||||
return is_x86_32()
|
||||
|
||||
@staticmethod
|
||||
def platform() -> str:
|
||||
@@ -224,14 +233,13 @@ class SystemUtils:
|
||||
"""
|
||||
if SystemUtils.is_x86_64():
|
||||
return "x86_64"
|
||||
elif SystemUtils.is_x86_32():
|
||||
if SystemUtils.is_x86_32():
|
||||
return "x86_32"
|
||||
elif SystemUtils.is_aarch64():
|
||||
if SystemUtils.is_aarch64():
|
||||
return "Arm64"
|
||||
elif SystemUtils.is_aarch():
|
||||
if SystemUtils.is_aarch():
|
||||
return "Arm32"
|
||||
else:
|
||||
return platform.machine()
|
||||
return platform.machine()
|
||||
|
||||
@staticmethod
|
||||
def copy(src: Path, dest: Path) -> Tuple[int, str]:
|
||||
@@ -895,16 +903,14 @@ class SystemUtils:
|
||||
"""
|
||||
获取配置路径
|
||||
"""
|
||||
if not config_dir:
|
||||
config_dir = os.getenv("CONFIG_DIR")
|
||||
if config_dir:
|
||||
return Path(config_dir)
|
||||
configured = config_dir or os.getenv("CONFIG_DIR")
|
||||
if configured:
|
||||
return Path(configured)
|
||||
if SystemUtils.is_docker():
|
||||
return Path("/config")
|
||||
elif SystemUtils.is_frozen():
|
||||
if SystemUtils.is_frozen():
|
||||
return Path(sys.executable).parent / "config"
|
||||
else:
|
||||
return Path(__file__).resolve().parents[3] / "config"
|
||||
return Path(__file__).resolve().parents[3] / "config"
|
||||
|
||||
@staticmethod
|
||||
def get_env_path() -> Path:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Web 传输层认证适配器。"""
|
||||
@@ -0,0 +1,267 @@
|
||||
"""把应用安全能力适配为 FastAPI 认证依赖和 Cookie 行为。"""
|
||||
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
from typing import Annotated, Any, Callable, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import HTTPException, Request, Response, Security, status
|
||||
from fastapi.security import (
|
||||
APIKeyCookie,
|
||||
APIKeyHeader,
|
||||
APIKeyQuery,
|
||||
HTTPBearer,
|
||||
OAuth2PasswordBearer,
|
||||
)
|
||||
|
||||
from app.runtime.cache import cached
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.token import TokenPayload
|
||||
|
||||
SuperuserTokenPayloadProvider = Callable[[], TokenPayload]
|
||||
TokenEncoder = Callable[..., str]
|
||||
TokenDecoder = Callable[[str | None, str], TokenPayload]
|
||||
_superuser_token_payload_provider: Optional[SuperuserTokenPayloadProvider] = None
|
||||
_token_encoder: Optional[TokenEncoder] = None
|
||||
_token_decoder: Optional[TokenDecoder] = None
|
||||
JWT_ALGORITHM = "HS256"
|
||||
|
||||
oauth2_scheme_manual_error = OAuth2PasswordBearer(
|
||||
auto_error=False,
|
||||
tokenUrl=f"{settings.API_V1_STR}/login/access-token",
|
||||
)
|
||||
resource_token_cookie = APIKeyCookie(
|
||||
name=settings.PROJECT_NAME,
|
||||
auto_error=False,
|
||||
scheme_name="resource_token_cookie",
|
||||
)
|
||||
api_token_query = APIKeyQuery(
|
||||
name="token",
|
||||
auto_error=False,
|
||||
scheme_name="api_token_query",
|
||||
)
|
||||
api_key_header = APIKeyHeader(
|
||||
name="X-API-KEY",
|
||||
auto_error=False,
|
||||
scheme_name="api_key_header",
|
||||
)
|
||||
api_key_query = APIKeyQuery(
|
||||
name="apikey",
|
||||
auto_error=False,
|
||||
scheme_name="api_key_query",
|
||||
)
|
||||
openai_bearer_scheme = HTTPBearer(auto_error=False)
|
||||
anthropic_api_key_header = APIKeyHeader(
|
||||
name="x-api-key",
|
||||
auto_error=False,
|
||||
scheme_name="anthropic_api_key_header",
|
||||
)
|
||||
|
||||
|
||||
def set_superuser_token_payload_provider(
|
||||
provider: SuperuserTokenPayloadProvider,
|
||||
) -> None:
|
||||
"""由启动组合根注入 API 密钥认证使用的超级用户载荷来源。"""
|
||||
global _superuser_token_payload_provider
|
||||
_superuser_token_payload_provider = provider
|
||||
|
||||
|
||||
def configure_token_codec(
|
||||
encoder: TokenEncoder,
|
||||
decoder: TokenDecoder,
|
||||
) -> None:
|
||||
"""由组合根注入框架无关的令牌编码与解码能力。"""
|
||||
global _token_encoder, _token_decoder
|
||||
_token_encoder = encoder
|
||||
_token_decoder = decoder
|
||||
|
||||
|
||||
def _encode_token(**claims: Any) -> str:
|
||||
"""使用已注入编码器创建令牌,未装配时给出明确错误。"""
|
||||
if _token_encoder is None:
|
||||
raise RuntimeError("Web 认证令牌编码器尚未配置")
|
||||
return _token_encoder(**claims)
|
||||
|
||||
|
||||
def _decode_token(token: str | None, purpose: str) -> TokenPayload:
|
||||
"""使用已注入解码器验证令牌,未装配时给出明确错误。"""
|
||||
if _token_decoder is None:
|
||||
raise RuntimeError("Web 认证令牌解码器尚未配置")
|
||||
return _token_decoder(token, purpose)
|
||||
|
||||
|
||||
def _get_api_token(
|
||||
token_query: Annotated[str | None, Security(api_token_query)] = None,
|
||||
) -> str | None:
|
||||
"""从 URL 查询参数读取兼容 API Token。"""
|
||||
return token_query
|
||||
|
||||
|
||||
def _get_api_key(
|
||||
key_query: Annotated[str | None, Security(api_key_query)] = None,
|
||||
key_header: Annotated[str | None, Security(api_key_header)] = None,
|
||||
) -> str | None:
|
||||
"""优先从请求头、其次从查询参数读取兼容 API Key。"""
|
||||
return key_header or key_query
|
||||
|
||||
|
||||
@cached(maxsize=1, ttl=600)
|
||||
def _create_superuser_token_payload() -> TokenPayload:
|
||||
"""使用组合根提供器创建 API 密钥调用的超级用户载荷。"""
|
||||
if not _superuser_token_payload_provider:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="认证服务尚未初始化",
|
||||
)
|
||||
try:
|
||||
return _superuser_token_payload_provider()
|
||||
except PermissionError as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(error) or "用户权限不足",
|
||||
) from error
|
||||
|
||||
|
||||
def set_or_refresh_resource_token_cookie(
|
||||
request: Request,
|
||||
response: Response,
|
||||
payload: TokenPayload,
|
||||
) -> None:
|
||||
"""复用匹配的资源令牌,或为当前身份写入新的安全 Cookie。"""
|
||||
resource_token = request.cookies.get(settings.PROJECT_NAME)
|
||||
if resource_token:
|
||||
try:
|
||||
decoded = jwt.decode(
|
||||
resource_token,
|
||||
settings.RESOURCE_SECRET_KEY,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
exp = decoded.get("exp")
|
||||
if exp:
|
||||
remaining_time = datetime.datetime.fromtimestamp(
|
||||
exp,
|
||||
tz=datetime.UTC,
|
||||
) - datetime.datetime.now(datetime.UTC)
|
||||
if remaining_time < timedelta(
|
||||
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS / 3
|
||||
):
|
||||
raise jwt.ExpiredSignatureError
|
||||
expected_claims = {
|
||||
"sub": str(payload.sub),
|
||||
"username": payload.username,
|
||||
"super_user": payload.super_user,
|
||||
"level": payload.level,
|
||||
"purpose": "resource",
|
||||
}
|
||||
if any(
|
||||
decoded.get(claim) != value
|
||||
for claim, value in expected_claims.items()
|
||||
):
|
||||
raise jwt.InvalidTokenError("资源令牌身份或权限上下文不匹配")
|
||||
except jwt.PyJWTError:
|
||||
logger.debug("Token error occurred. refreshing token")
|
||||
except Exception as error:
|
||||
logger.debug(
|
||||
f"Unexpected error occurred while decoding token: {error}"
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
resource_token = _encode_token(
|
||||
userid=payload.sub,
|
||||
username=payload.username or "",
|
||||
super_user=payload.super_user,
|
||||
expires_delta=timedelta(
|
||||
seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
),
|
||||
level=payload.level,
|
||||
purpose="resource",
|
||||
)
|
||||
is_https = (
|
||||
request.url.scheme == "https"
|
||||
or request.headers.get("x-forwarded-proto", "").lower() == "https"
|
||||
)
|
||||
response.set_cookie(
|
||||
key=settings.PROJECT_NAME,
|
||||
value=resource_token,
|
||||
httponly=True,
|
||||
secure=is_https,
|
||||
samesite="lax",
|
||||
)
|
||||
|
||||
|
||||
def _decode_or_http_error(
|
||||
token: str | None,
|
||||
purpose: str,
|
||||
) -> TokenPayload:
|
||||
"""把应用层令牌校验错误转换为 HTTP 403。"""
|
||||
try:
|
||||
return _decode_token(token, purpose)
|
||||
except ValueError as error:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(error),
|
||||
) from error
|
||||
|
||||
|
||||
def verify_token(
|
||||
request: Request,
|
||||
response: Response,
|
||||
jwt_token: Annotated[
|
||||
str | None,
|
||||
Security(oauth2_scheme_manual_error),
|
||||
],
|
||||
api_key: Annotated[str | None, Security(_get_api_key)],
|
||||
api_token: Annotated[str | None, Security(_get_api_token)],
|
||||
) -> TokenPayload:
|
||||
"""验证 JWT、API Key 或 API Token,并维护资源 Cookie。"""
|
||||
if jwt_token:
|
||||
payload = _decode_or_http_error(jwt_token, "authentication")
|
||||
set_or_refresh_resource_token_cookie(request, response, payload)
|
||||
return payload
|
||||
if api_key:
|
||||
verify_apikey(api_key)
|
||||
return _create_superuser_token_payload()
|
||||
if api_token:
|
||||
verify_apitoken(api_token)
|
||||
return _create_superuser_token_payload()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
def verify_resource_token(
|
||||
resource_token: Annotated[
|
||||
str | None,
|
||||
Security(resource_token_cookie),
|
||||
],
|
||||
) -> TokenPayload:
|
||||
"""验证 Cookie 中携带的资源访问令牌。"""
|
||||
return _decode_or_http_error(resource_token, "resource")
|
||||
|
||||
|
||||
def _verify_key(key: str | None, expected_key: str, key_type: str) -> str:
|
||||
"""校验受信第三方集成使用的固定 API 凭据。"""
|
||||
if not key or key != expected_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"{key_type} 校验不通过",
|
||||
)
|
||||
return key
|
||||
|
||||
|
||||
def verify_apitoken(
|
||||
token: Annotated[str | None, Security(_get_api_token)],
|
||||
) -> str:
|
||||
"""校验 URL 查询参数中的兼容 API Token。"""
|
||||
return _verify_key(token, settings.API_TOKEN, "token")
|
||||
|
||||
|
||||
def verify_apikey(
|
||||
apikey: Annotated[str | None, Security(_get_api_key)],
|
||||
) -> str:
|
||||
"""校验请求头或查询参数中的兼容 API Key。"""
|
||||
return _verify_key(apikey, settings.API_TOKEN, "apikey")
|
||||
@@ -1,24 +1,7 @@
|
||||
"""Agent 内部使用的 LLM 适配层,公开对象按需解析。"""
|
||||
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.agent.llm.capability import (
|
||||
AgentCapabilityManager,
|
||||
AgentCapabilityProvider,
|
||||
AudioCapabilityProvider,
|
||||
MiMoAudioProvider,
|
||||
OpenAIAudioProvider,
|
||||
OpenAIChatAudioProvider,
|
||||
)
|
||||
from app.agent.llm.helper import LLMHelper, LLMTestError, LLMTestTimeout
|
||||
from app.agent.llm.provider import (
|
||||
LLMProviderAuthError,
|
||||
LLMProviderError,
|
||||
LLMProviderManager,
|
||||
render_auth_result_html,
|
||||
)
|
||||
from typing import Any
|
||||
|
||||
|
||||
_EXPORT_MODULES = {
|
||||
|
||||
@@ -799,7 +799,7 @@ class AgentCapabilityManager:
|
||||
if not source:
|
||||
return False
|
||||
|
||||
from app.runtime.extensions.service_registry import ServiceConfigHelper
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
|
||||
for config in ServiceConfigHelper.get_notification_configs():
|
||||
if config.name != source:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""LLM helper 与 provider 实现之间的运行时端口。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class LLMProviderRuntimePort(Protocol):
|
||||
"""声明 LLM helper 所需的最小 provider 运行时能力。"""
|
||||
|
||||
def resolve_cached_model_metadata(self, **kwargs: Any) -> dict[str, Any] | None:
|
||||
"""从本地目录缓存解析模型元数据。"""
|
||||
...
|
||||
|
||||
async def resolve_runtime(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""解析创建模型客户端所需的统一运行时参数。"""
|
||||
...
|
||||
|
||||
def create_bedrock_client(self, *args: Any, **kwargs: Any) -> Any:
|
||||
"""创建带统一认证和网络配置的 Bedrock 客户端。"""
|
||||
...
|
||||
|
||||
async def list_models(self, **kwargs: Any) -> list[dict[str, Any]]:
|
||||
"""返回 provider 可用的模型目录。"""
|
||||
...
|
||||
|
||||
def resolve_model_list_base_url(self, **kwargs: Any) -> str | None:
|
||||
"""解析兼容接口用于查询模型列表的基础地址。"""
|
||||
...
|
||||
|
||||
|
||||
LLMProviderRuntimeFactory = Callable[[], LLMProviderRuntimePort]
|
||||
_provider_runtime_factory: LLMProviderRuntimeFactory | None = None
|
||||
|
||||
|
||||
def register_llm_provider_runtime(
|
||||
factory: LLMProviderRuntimeFactory | None,
|
||||
) -> LLMProviderRuntimeFactory | None:
|
||||
"""注册 provider 运行时工厂,并返回先前工厂供隔离测试恢复。"""
|
||||
global _provider_runtime_factory
|
||||
previous = _provider_runtime_factory
|
||||
_provider_runtime_factory = factory
|
||||
return previous
|
||||
|
||||
|
||||
def resolve_llm_provider_runtime() -> LLMProviderRuntimePort:
|
||||
"""解析已组装的 provider 运行时,未注册时给出明确边界错误。"""
|
||||
if _provider_runtime_factory is None:
|
||||
raise RuntimeError("LLM provider 运行时尚未由启动层完成组装")
|
||||
return _provider_runtime_factory()
|
||||
+6
-17
@@ -10,6 +10,7 @@ from urllib.parse import urlsplit
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk
|
||||
|
||||
from app.agent.llm.gateway import resolve_llm_provider_runtime
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
|
||||
@@ -793,9 +794,7 @@ class LLMHelper:
|
||||
return None
|
||||
|
||||
try:
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
metadata = LLMProviderManager().resolve_cached_model_metadata(
|
||||
metadata = resolve_llm_provider_runtime().resolve_cached_model_metadata(
|
||||
provider_id=provider_name,
|
||||
model_id=model_name,
|
||||
base_url=base_url if base_url is not None else settings.LLM_BASE_URL,
|
||||
@@ -1197,11 +1196,7 @@ class LLMHelper:
|
||||
thinking_level=thinking_level,
|
||||
)
|
||||
try:
|
||||
# 延迟导入,避免单测在最小 stub 环境下 import `llm.py` 时被 provider
|
||||
# 目录依赖链拖住。
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
runtime = await LLMProviderManager().resolve_runtime(
|
||||
runtime = await resolve_llm_provider_runtime().resolve_runtime(
|
||||
provider_id=provider_name,
|
||||
model=model_name,
|
||||
api_key=api_key_value,
|
||||
@@ -1328,8 +1323,6 @@ class LLMHelper:
|
||||
elif runtime["runtime"] == "bedrock":
|
||||
from langchain_aws import ChatBedrockConverse
|
||||
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
bedrock_model_cls = ChatBedrockConverse
|
||||
if (
|
||||
str(prompt_cache_key or "").strip()
|
||||
@@ -1344,7 +1337,7 @@ class LLMHelper:
|
||||
aws_auth = runtime.get("aws_auth") or {}
|
||||
# Bearer 认证需要跳过 SigV4 签名并注入 Authorization 头,SigV4 认证
|
||||
# 直接以 AK/SK 签名;两种方式统一由 provider 管理器构造 boto3 客户端。
|
||||
bedrock_client = LLMProviderManager().create_bedrock_client(
|
||||
bedrock_client = resolve_llm_provider_runtime().create_bedrock_client(
|
||||
"bedrock-runtime",
|
||||
region=aws_region,
|
||||
credentials=aws_auth,
|
||||
@@ -1558,9 +1551,7 @@ class LLMHelper:
|
||||
"""
|
||||
logger.info(f"获取 {provider} 模型列表...")
|
||||
try:
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
models = await LLMProviderManager().list_models(
|
||||
models = await resolve_llm_provider_runtime().list_models(
|
||||
provider_id=provider,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
@@ -1589,10 +1580,8 @@ class LLMHelper:
|
||||
base_url=base_url,
|
||||
)
|
||||
try:
|
||||
from app.agent.llm.provider import LLMProviderManager
|
||||
|
||||
model_list_base_url = (
|
||||
LLMProviderManager().resolve_model_list_base_url(
|
||||
resolve_llm_provider_runtime().resolve_model_list_base_url(
|
||||
provider_id=provider,
|
||||
base_url=base_url,
|
||||
base_url_preset_id=base_url_preset,
|
||||
|
||||
@@ -21,7 +21,7 @@ import httpx
|
||||
import jwt
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import LlmProviderAction, SystemConfigKey
|
||||
from app.foundation.singleton import Singleton
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.agent import (
|
||||
AgentMcpServerConfig,
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Dict, List, Optional
|
||||
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.application.agentdata import AgentChatPort as AgentChatOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.agent import ConversationMemory
|
||||
|
||||
|
||||
@@ -68,10 +68,15 @@ from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
|
||||
from app.chain.agent import AgentChain
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.events import eventmanager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.db.oper.user import UserOper
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
|
||||
|
||||
def _get_plugin_tools_revision() -> int:
|
||||
"""读取插件工具目录修订号,避免 Agent 编排依赖具体管理器类型。"""
|
||||
return get_plugin_manager().get_plugin_agent_tools_revision()
|
||||
from app.application.agentdata import AgentChatPort as AgentChatOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
from app.application.agentdata import UserPort as UserOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.event import AgentLLMProviderEventData
|
||||
from app.schemas.event import AgentTokensUsageEventData
|
||||
@@ -1567,7 +1572,7 @@ class MoviePilotAgent:
|
||||
from app.agent.runtime_loader import get_tool_factory
|
||||
|
||||
tool_factory = get_tool_factory()
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
for _attempt in range(tool_factory.CATALOG_BUILD_MAX_ATTEMPTS):
|
||||
before_revision = plugin_manager.get_plugin_agent_tools_revision()
|
||||
tools = self._initialize_tools()
|
||||
@@ -1663,7 +1668,7 @@ class MoviePilotAgent:
|
||||
if tool_catalog is not None and subagent_catalog is not None
|
||||
else (
|
||||
self._tool_factory_revision(),
|
||||
PluginManager().get_plugin_agent_tools_revision(),
|
||||
_get_plugin_tools_revision(),
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -1782,7 +1787,7 @@ class MoviePilotAgent:
|
||||
"""
|
||||
try:
|
||||
runtime_config = await self._resolve_llm_runtime_config()
|
||||
plugin_revision = PluginManager().get_plugin_agent_tools_revision()
|
||||
plugin_revision = _get_plugin_tools_revision()
|
||||
mcp_config_signature = agent_mcp_manager.config_signature()
|
||||
cached_bundle = self._compiled_agent_bundle
|
||||
catalog_is_fresh = bool(
|
||||
|
||||
@@ -1,68 +1,55 @@
|
||||
"""MoviePilot Agent 宿主策略公共内部入口。"""
|
||||
|
||||
from app.agent.policy.contracts import (
|
||||
ActionEffect,
|
||||
ActionPolicy,
|
||||
AuthSource,
|
||||
ConfirmationMode,
|
||||
ExecutionOutcome,
|
||||
ExecutionReceipt,
|
||||
MigrationState,
|
||||
PolicyDecision,
|
||||
PolicyObservation,
|
||||
PolicyPrincipal,
|
||||
PrincipalRole,
|
||||
PrincipalType,
|
||||
RecoveryMode,
|
||||
ResultSensitivity,
|
||||
ToolInvocation,
|
||||
ToolOrigin,
|
||||
ToolPolicyContext,
|
||||
ToolRevision,
|
||||
)
|
||||
from app.agent.policy.orchestrator import (
|
||||
DEFAULT_TOOL_POLICY_ORCHESTRATOR,
|
||||
AgentToolPolicyOrchestrator,
|
||||
call_policy_hook,
|
||||
)
|
||||
from app.agent.policy.registry import DEFAULT_TOOL_POLICY_REGISTRY, ToolPolicyRegistry
|
||||
from app.agent.policy.sanitizer import (
|
||||
REDACTED_VALUE,
|
||||
sanitize_for_host,
|
||||
stable_type_name,
|
||||
summarize_error,
|
||||
summarize_input,
|
||||
summarize_result,
|
||||
)
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
"ActionEffect",
|
||||
"ActionPolicy",
|
||||
"AgentToolPolicyOrchestrator",
|
||||
"AuthSource",
|
||||
"ConfirmationMode",
|
||||
"DEFAULT_TOOL_POLICY_ORCHESTRATOR",
|
||||
"DEFAULT_TOOL_POLICY_REGISTRY",
|
||||
"ExecutionOutcome",
|
||||
"ExecutionReceipt",
|
||||
"MigrationState",
|
||||
"PolicyDecision",
|
||||
"PolicyObservation",
|
||||
"PolicyPrincipal",
|
||||
"PrincipalRole",
|
||||
"PrincipalType",
|
||||
"REDACTED_VALUE",
|
||||
"RecoveryMode",
|
||||
"ResultSensitivity",
|
||||
"ToolInvocation",
|
||||
"ToolOrigin",
|
||||
"ToolPolicyContext",
|
||||
"ToolRevision",
|
||||
"ToolPolicyRegistry",
|
||||
"call_policy_hook",
|
||||
"sanitize_for_host",
|
||||
"stable_type_name",
|
||||
"summarize_error",
|
||||
"summarize_input",
|
||||
"summarize_result",
|
||||
]
|
||||
|
||||
_EXPORT_MODULES = {
|
||||
"ActionEffect": "app.agent.policy.contracts",
|
||||
"ActionPolicy": "app.agent.policy.contracts",
|
||||
"AuthSource": "app.agent.policy.contracts",
|
||||
"ConfirmationMode": "app.agent.policy.contracts",
|
||||
"ExecutionOutcome": "app.agent.policy.contracts",
|
||||
"ExecutionReceipt": "app.agent.policy.contracts",
|
||||
"MigrationState": "app.agent.policy.contracts",
|
||||
"PolicyDecision": "app.agent.policy.contracts",
|
||||
"PolicyObservation": "app.agent.policy.contracts",
|
||||
"PolicyPrincipal": "app.agent.policy.contracts",
|
||||
"PrincipalRole": "app.agent.policy.contracts",
|
||||
"PrincipalType": "app.agent.policy.contracts",
|
||||
"RecoveryMode": "app.agent.policy.contracts",
|
||||
"ResultSensitivity": "app.agent.policy.contracts",
|
||||
"ToolInvocation": "app.agent.policy.contracts",
|
||||
"ToolOrigin": "app.agent.policy.contracts",
|
||||
"ToolPolicyContext": "app.agent.policy.contracts",
|
||||
"ToolRevision": "app.agent.policy.contracts",
|
||||
"AgentToolPolicyOrchestrator": "app.agent.policy.orchestrator",
|
||||
"DEFAULT_TOOL_POLICY_ORCHESTRATOR": "app.agent.policy.orchestrator",
|
||||
"call_policy_hook": "app.agent.policy.orchestrator",
|
||||
"DEFAULT_TOOL_POLICY_REGISTRY": "app.agent.policy.registry",
|
||||
"ToolPolicyRegistry": "app.agent.policy.registry",
|
||||
"REDACTED_VALUE": "app.agent.policy.sanitizer",
|
||||
"sanitize_for_host": "app.agent.policy.sanitizer",
|
||||
"stable_type_name": "app.agent.policy.sanitizer",
|
||||
"summarize_error": "app.agent.policy.sanitizer",
|
||||
"summarize_input": "app.agent.policy.sanitizer",
|
||||
"summarize_result": "app.agent.policy.sanitizer",
|
||||
}
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
"""首次访问公开策略对象时只加载其所属模块。"""
|
||||
module_name = _EXPORT_MODULES.get(name)
|
||||
if module_name is None:
|
||||
raise AttributeError(f"module 'app.agent.policy' has no attribute {name!r}")
|
||||
value = getattr(import_module(module_name), name)
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
"""让惰性公开对象继续支持交互式发现。"""
|
||||
return sorted(set(globals()) | set(_EXPORT_MODULES))
|
||||
|
||||
|
||||
__all__ = list(_EXPORT_MODULES)
|
||||
|
||||
@@ -19,7 +19,7 @@ from app.agent.tools.tags import ToolTag
|
||||
from app.chain import ChainBase
|
||||
from app.runtime.config import settings
|
||||
from app.application.messaging.agent import matches_channel_admin
|
||||
from app.runtime.extensions.service_registry import ServiceConfigHelper
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.message import Message
|
||||
from app.schemas.types import NotificationChannel, MessageType
|
||||
|
||||
@@ -88,7 +88,7 @@ from app.agent.tools.impl.update_custom_identifiers import UpdateCustomIdentifie
|
||||
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
|
||||
from app.agent.tools.impl.update_system_settings import UpdateSystemSettingsTool
|
||||
from app.agent.llm.capability import AgentCapabilityManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.notification import ChannelCapabilityManager
|
||||
from app.schemas.types import NotificationChannel
|
||||
@@ -96,6 +96,18 @@ from .base import MoviePilotTool
|
||||
from .catalog import ToolCatalogError, ToolCatalogSnapshot
|
||||
|
||||
|
||||
def _get_plugin_agent_tools() -> list[dict]:
|
||||
"""读取当前插件工具投影,隔离 Agent 工具工厂与 Runtime 管理器。"""
|
||||
try:
|
||||
return get_plugin_manager().get_plugin_agent_tools()
|
||||
except RuntimeError as error:
|
||||
# 纯工具目录探针可以在启动组合根之前运行;此时只跳过可选插件工具,
|
||||
# 不隐式创建 PluginManager,避免冷导入重新引入 Runtime 定位器。
|
||||
if "尚未由启动组合根装配" not in str(error):
|
||||
raise
|
||||
return []
|
||||
|
||||
|
||||
class MoviePilotToolFactory:
|
||||
"""
|
||||
MoviePilot工具工厂
|
||||
@@ -288,7 +300,7 @@ class MoviePilotToolFactory:
|
||||
|
||||
# 加载插件提供的工具
|
||||
plugin_tools_count = 0
|
||||
plugin_tools_info = PluginManager().get_plugin_agent_tools()
|
||||
plugin_tools_info = _get_plugin_agent_tools()
|
||||
for plugin_info in plugin_tools_info:
|
||||
plugin_id = plugin_info.get("plugin_id")
|
||||
plugin_name = plugin_info.get("plugin_name")
|
||||
@@ -337,7 +349,18 @@ class MoviePilotToolFactory:
|
||||
@classmethod
|
||||
def create_catalog(cls, **tool_kwargs) -> ToolCatalogSnapshot:
|
||||
"""在插件目录稳定窗口内构造一份完整本地工具快照。"""
|
||||
plugin_manager = PluginManager()
|
||||
try:
|
||||
plugin_manager = get_plugin_manager()
|
||||
except RuntimeError as error:
|
||||
# 没有启动上下文时仍允许构造内置工具目录;插件工具会在正式启动
|
||||
# 后由组合根提供的 Runtime 中重新物化。
|
||||
if "尚未由启动组合根装配" not in str(error):
|
||||
raise
|
||||
return ToolCatalogSnapshot.from_tools(
|
||||
cls.create_tools(**tool_kwargs),
|
||||
plugin_revision=0,
|
||||
factory_revision=cls.catalog_factory_revision(),
|
||||
)
|
||||
for _attempt in range(cls.CATALOG_BUILD_MAX_ATTEMPTS):
|
||||
before_revision = plugin_manager.get_plugin_agent_tools_revision()
|
||||
tools = cls.create_tools(**tool_kwargs)
|
||||
|
||||
@@ -5,8 +5,8 @@ import re
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from app.runtime.events import eventmanager
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.agentdata import SubscribePort as SubscribeOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.application.rules import RuleHelper
|
||||
from app.application.rules import RuleParser
|
||||
from app.application.rules import BUILTIN_RULE_SET
|
||||
|
||||
@@ -5,9 +5,9 @@ import shutil
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.application.plugin.install import PluginInstallCommand
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.adapters.external.market import PluginHelper
|
||||
from app.adapters.system.plugin.package import PluginPackageManager
|
||||
@@ -26,7 +26,7 @@ def get_plugin_snapshot(plugin_id: str) -> Optional[dict[str, Any]]:
|
||||
"""
|
||||
获取已安装插件的基础信息快照。
|
||||
"""
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
for plugin in plugin_manager.get_local_plugins():
|
||||
if plugin.id == plugin_id:
|
||||
return {
|
||||
@@ -81,7 +81,7 @@ def refresh_plugin_registrations(plugin_id: str) -> None:
|
||||
|
||||
def reload_plugin_runtime(plugin_id: str) -> None:
|
||||
"""重载插件实例并重新注册其命令、定时任务和 API。"""
|
||||
PluginManager().reload_plugin(plugin_id)
|
||||
get_plugin_manager().reload_plugin(plugin_id)
|
||||
refresh_plugin_registrations(plugin_id)
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ async def enrich_installed_plugin_sources(
|
||||
if not missing_source_plugins:
|
||||
return installed_plugins
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
local_repo_map = _map_plugins_by_id(plugin_manager.get_local_repo_plugins())
|
||||
for plugin in missing_source_plugins:
|
||||
source_plugin = local_repo_map.get(getattr(plugin, "id", None))
|
||||
@@ -184,7 +184,7 @@ async def load_market_plugins(force_refresh: bool = False) -> list[Any]:
|
||||
"""
|
||||
聚合插件市场与本地插件仓库中的候选插件。
|
||||
"""
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
online_plugins = await plugin_manager.async_get_online_plugins(force=force_refresh)
|
||||
local_repo_plugins = plugin_manager.get_local_repo_plugins()
|
||||
if not online_plugins and not local_repo_plugins:
|
||||
@@ -196,7 +196,7 @@ def list_installed_plugins() -> list[Any]:
|
||||
"""
|
||||
返回当前已安装插件列表。
|
||||
"""
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
return [plugin for plugin in plugin_manager.get_local_plugins() if plugin.installed]
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ async def install_plugin_runtime(
|
||||
"""
|
||||
按现有插件接口的行为安装插件,并刷新运行态注册信息。
|
||||
"""
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
plugin_helper = PluginHelper()
|
||||
package_manager = PluginPackageManager(plugin_helper)
|
||||
|
||||
@@ -395,7 +395,7 @@ async def uninstall_plugin_runtime(plugin_id: str) -> dict[str, Any]:
|
||||
remove_plugin_api(plugin_id)
|
||||
remove_plugin_job(plugin_id)
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
plugin_class = plugin_manager.plugins.get(plugin_id)
|
||||
was_clone = bool(getattr(plugin_class, "is_clone", False))
|
||||
clone_files_removed = False
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.chain.search import SearchChain
|
||||
from app.runtime.config import settings
|
||||
from app.domain.context import Context
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.application.directory import DirectoryHelper, validate_download_save_path
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.file import FileURI
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.db.oper.user import UserOper
|
||||
from app.application.agentdata import UserPort as UserOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaSource, MediaType, NotificationChannel
|
||||
from app.domain.media import normalize_music_type
|
||||
|
||||
@@ -8,8 +8,8 @@ from pydantic import BaseModel, Field, model_validator
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.application.agentdata import AgentChatPort as AgentChatOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
from app.runtime.scheduling import TimerUtils
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
|
||||
|
||||
class DeleteAgentTaskInput(BaseModel):
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.application.agentdata import DownloadHistoryPort as DownloadHistoryOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.events import eventmanager
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.application.agentdata import SubscribePort as SubscribeOper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.storage import StorageChain
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.application.agentdata import TransferHistoryPort as TransferHistoryOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.workflow import FileItem
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
|
||||
|
||||
class QueryAgentTasksInput(BaseModel):
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.download import DownloadChain
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.application.agentdata import DownloadHistoryPort as DownloadHistoryOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.transfer import DownloaderTorrent
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, TorrentQueryStatus, media_type_to_agent
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.runtime.extensions.service_registry import ServiceConfigHelper
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.runtime.log import logger
|
||||
|
||||
PAGE_SIZE = 20
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class QueryPluginCapabilitiesTool(MoviePilotTool):
|
||||
@staticmethod
|
||||
def _load_plugin_capabilities(plugin_id: Optional[str] = None) -> dict:
|
||||
"""读取运行中插件实例暴露的内存能力信息。"""
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
result = {}
|
||||
|
||||
commands = plugin_manager.get_plugin_commands(pid=plugin_id)
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class QueryPluginConfigTool(MoviePilotTool):
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
saved_config = plugin_manager.get_plugin_config(plugin_id) or {}
|
||||
result = {
|
||||
"success": True,
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.agent.tools.impl._plugin_tool_utils import (
|
||||
build_preview_payload,
|
||||
get_plugin_snapshot,
|
||||
)
|
||||
from app.db.oper.plugindata import PluginDataOper
|
||||
from app.application.agentdata import PluginDataPort as PluginDataOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
SITE_USERDATA_DETAIL_PREVIEW_LIMIT = 10
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.application.agentdata import SubscribeHistoryPort as SubscribeHistoryOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaType, media_type_to_agent
|
||||
from app.domain.media import normalize_music_type
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.application.agentdata import SubscribePort as SubscribeOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.subscribe import Subscribe as SubscribeSchema
|
||||
from app.schemas.types import (
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.agent.tools.impl._system_setting_utils import (
|
||||
should_redact_setting,
|
||||
)
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.application.agentdata import TransferHistoryPort as TransferHistoryOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import media_type_to_agent
|
||||
from app.foundation.text import cut as jieba_cut
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.application.agentdata import WorkflowPort as WorkflowOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
|
||||
|
||||
class RunAgentTaskInput(BaseModel):
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.workflow import WorkflowChain
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.application.workflow import get_configured_workflow_query
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class RunWorkflowTool(MoviePilotTool):
|
||||
)
|
||||
|
||||
try:
|
||||
workflow = await WorkflowOper().async_get(workflow_id)
|
||||
workflow = await get_configured_workflow_query().get(workflow_id)
|
||||
|
||||
if not workflow:
|
||||
return f"未找到工作流:{workflow_id},请使用 query_workflows 工具查询可用的工作流"
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.application.agentdata import SubscribePort as SubscribeOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import media_type_to_agent
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.search import SearchChain
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MediaSource, MediaType, SystemConfigKey
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.site import SiteChain
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, model_validator
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.application.agentdata import AgentTaskPort as AgentTaskOper
|
||||
from app.runtime.scheduling import TimerUtils
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.domain.metainfo import clear_rust_parse_options_cache
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.agent.tools.impl._plugin_tool_utils import get_plugin_snapshot
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class UpdatePluginConfigTool(MoviePilotTool):
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
current_config = dict(plugin_manager.get_plugin_config(plugin_id) or {})
|
||||
|
||||
# merge 模式以当前保存值为基准,replace 模式则从空配置开始重建。
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.events import eventmanager
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
from app.foundation import url as url_tools
|
||||
|
||||
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.chain.site import SiteChain
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.application.agentdata import SitePort as SiteOper
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field
|
||||
from app.agent.tools.base import MoviePilotTool
|
||||
from app.agent.tools.tags import ToolTag
|
||||
from app.runtime.events import eventmanager
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.application.agentdata import SubscribePort as SubscribeOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.event import SubscribeModifiedEventData
|
||||
from app.schemas.types import EventType, media_type_to_agent
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.agent.tools.impl._system_setting_utils import (
|
||||
)
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.events import eventmanager
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config as SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.event import ConfigChangeEventData
|
||||
from app.schemas.types import EventType
|
||||
|
||||
@@ -118,9 +118,9 @@ class MoviePilotToolsManager:
|
||||
self._load_tools_locked()
|
||||
return
|
||||
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
plugin_manager = get_plugin_manager()
|
||||
if (
|
||||
self._plugin_agent_tools_revision
|
||||
== plugin_manager.get_plugin_agent_tools_revision()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""API 请求数据端口注册表。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator, Callable, Generator
|
||||
from typing import Any
|
||||
|
||||
|
||||
SessionProvider = Callable[[], Generator[Any, None, None]]
|
||||
AsyncSessionProvider = Callable[[], AsyncGenerator[Any, None]]
|
||||
RepositoryFactory = Callable[[Any], Any]
|
||||
StandaloneFactory = Callable[[], Any]
|
||||
UnitOfWorkFactory = Callable[[Any], Any]
|
||||
|
||||
|
||||
class ApiDataPorts:
|
||||
"""保存 API 依赖所需的会话、仓储和事务端口。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: SessionProvider,
|
||||
async_session: AsyncSessionProvider,
|
||||
repositories: dict[str, RepositoryFactory],
|
||||
standalone: dict[str, StandaloneFactory],
|
||||
unit_of_work: dict[str, UnitOfWorkFactory],
|
||||
) -> None:
|
||||
"""保存由启动组合根提供的具体实现工厂。"""
|
||||
self.sync_session = sync_session
|
||||
self.async_session = async_session
|
||||
self.repositories = repositories
|
||||
self.standalone = standalone
|
||||
self.unit_of_work = unit_of_work
|
||||
|
||||
def repository(self, name: str, session: Any) -> Any:
|
||||
"""按能力名构造请求级仓储。"""
|
||||
return self.repositories[name](session)
|
||||
|
||||
def standalone_repository(self, name: str) -> Any:
|
||||
"""构造不绑定请求会话的持久化端口。"""
|
||||
return self.standalone[name]()
|
||||
|
||||
def transaction(self, name: str, session: Any) -> Any:
|
||||
"""构造请求级事务端口。"""
|
||||
return self.unit_of_work[name](session)
|
||||
|
||||
|
||||
_ports: ApiDataPorts | None = None
|
||||
|
||||
|
||||
def configure_api_data_ports(
|
||||
*,
|
||||
sync_session: SessionProvider,
|
||||
async_session: AsyncSessionProvider,
|
||||
repositories: dict[str, RepositoryFactory],
|
||||
standalone: dict[str, StandaloneFactory],
|
||||
unit_of_work: dict[str, UnitOfWorkFactory],
|
||||
) -> None:
|
||||
"""由启动组合根登记 API 数据实现,切断 API 对数据库实现包的直接导入。"""
|
||||
global _ports
|
||||
_ports = ApiDataPorts(
|
||||
sync_session=sync_session,
|
||||
async_session=async_session,
|
||||
repositories=repositories,
|
||||
standalone=standalone,
|
||||
unit_of_work=unit_of_work,
|
||||
)
|
||||
|
||||
|
||||
def get_api_data_ports() -> ApiDataPorts:
|
||||
"""返回当前 API 数据端口集合。"""
|
||||
if _ports is None:
|
||||
raise RuntimeError("API 数据端口尚未由启动组合根配置")
|
||||
return _ports
|
||||
|
||||
|
||||
def get_db() -> Generator[Any, None, None]:
|
||||
"""向 FastAPI 暴露同步请求会话依赖。"""
|
||||
yield from get_api_data_ports().sync_session()
|
||||
|
||||
|
||||
async def get_async_db() -> AsyncGenerator[Any, None]:
|
||||
"""向 FastAPI 暴露异步请求会话依赖。"""
|
||||
async for session in get_api_data_ports().async_session():
|
||||
yield session
|
||||
+201
-47
@@ -6,6 +6,8 @@ HTTPException 表达。它们此前住在 app/db/oper/user.py 里,与数据访
|
||||
鉴权是 HTTP 层的关注点,产出的是 403/400 而不是数据。放在 db 包里既让数据层反向
|
||||
依赖了 fastapi,也使这部分逻辑无法与数据访问分开度量。
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -16,13 +18,24 @@ from app.application.subscription.identity import (
|
||||
DeleteSubscriptionsByIdentityCommand,
|
||||
)
|
||||
from app.application.subscription.search import SearchSubscriptionsCommand
|
||||
from app.application.subscription.query import SubscriptionQueryService
|
||||
from app.application.subscription.mutation import SubscriptionMutationService
|
||||
from app.application.site.mutation import SiteMutationCommand
|
||||
from app.application.site.query import SiteQueryService
|
||||
from app.application.workflow import (
|
||||
WorkflowDefinitionCommand,
|
||||
WorkflowMutationCommand,
|
||||
WorkflowQueryService,
|
||||
)
|
||||
from app.application.messaging.message import MessageQueryService
|
||||
from app.application.messaging.chat import AgentChatService
|
||||
from app.application.mediaserver import MediaServerQueryService
|
||||
from app.application.servarr import ServarrSubscriptionService
|
||||
from app.application.dashboard import DashboardQueryService
|
||||
from app.application.history import (
|
||||
DownloadHistoryMutationCommand,
|
||||
HistoryQueryService,
|
||||
TransferHistoryLookupService,
|
||||
TransferHistoryMutationCommand,
|
||||
clear_transfer_failures,
|
||||
)
|
||||
@@ -30,32 +43,42 @@ from app.application.plugin.config import PluginConfigCommand
|
||||
from app.application.commands import init_commands
|
||||
from app.application.plugins import register_plugin_api
|
||||
from app.application.scheduling import update_plugin_job
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.application.security.user import UserService
|
||||
from app.application.security.auth import AuthService
|
||||
from app.application.security.passkeys import PasskeyService
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.db import get_async_db, get_db
|
||||
from app.db.models.user import User
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.api.data import get_api_data_ports, get_async_db, get_db
|
||||
from app.runtime.events import eventmanager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager as PluginManager
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.event import PluginDataResetEventData
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
from app.scheduler import Scheduler
|
||||
from app.application.scheduling import Scheduler
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.domain import site as site_rules
|
||||
from app.foundation import url as url_tools
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.runtime.config import global_vars
|
||||
from app.workflow import WorkFlowManager
|
||||
from app.chain.storage import StorageChain
|
||||
from app.schemas.workflow import FileItem as _SchemaFileItem
|
||||
|
||||
|
||||
def _repository(name: str, session: Any) -> Any:
|
||||
"""构造绑定当前请求会话的数据仓储。"""
|
||||
return get_api_data_ports().repository(name, session)
|
||||
|
||||
|
||||
def _standalone_repository(name: str) -> Any:
|
||||
"""构造无需绑定请求会话的数据端口。"""
|
||||
return get_api_data_ports().standalone_repository(name)
|
||||
|
||||
|
||||
def _transaction(name: str, session: Any) -> Any:
|
||||
"""构造绑定当前请求会话的事务端口。"""
|
||||
return get_api_data_ports().transaction(name, session)
|
||||
|
||||
|
||||
async def _publish_subscribe_deleted(
|
||||
subscribe_id: int,
|
||||
subscribe_info: dict,
|
||||
@@ -72,8 +95,8 @@ def get_delete_subscribe_command(
|
||||
) -> DeleteSubscribeCommand:
|
||||
"""组装请求级订阅删除用例及其具体适配器。"""
|
||||
return DeleteSubscribeCommand(
|
||||
repository=SubscribeOper(db),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(db),
|
||||
repository=_repository("subscribe", db),
|
||||
unit_of_work=_transaction("async", db),
|
||||
publish_deleted=_publish_subscribe_deleted,
|
||||
report_deleted=MoviePilotServerHelper.sub_done_async,
|
||||
)
|
||||
@@ -95,8 +118,8 @@ def get_delete_subscriptions_by_identity_command(
|
||||
) -> DeleteSubscriptionsByIdentityCommand:
|
||||
"""组装请求级按媒体身份删除订阅用例。"""
|
||||
return DeleteSubscriptionsByIdentityCommand(
|
||||
repository=SubscribeOper(db),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(db),
|
||||
repository=_repository("subscribe", db),
|
||||
unit_of_work=_transaction("async", db),
|
||||
publish_deleted=_publish_subscribe_deleted,
|
||||
handle_event_error=_log_subscribe_deleted_event_error,
|
||||
)
|
||||
@@ -118,11 +141,71 @@ def get_search_subscriptions_command(
|
||||
)
|
||||
|
||||
return SearchSubscriptionsCommand(
|
||||
repository=SubscribeOper(db),
|
||||
repository=_repository("subscribe", db),
|
||||
schedule_search=schedule_search,
|
||||
)
|
||||
|
||||
|
||||
def get_subscription_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> SubscriptionQueryService:
|
||||
"""组装订阅和订阅历史异步查询服务。"""
|
||||
return SubscriptionQueryService(
|
||||
repository=_repository("subscribe", db),
|
||||
async_repository=_repository("subscribe", db),
|
||||
history_repository=_repository("subscribe_history", db),
|
||||
)
|
||||
|
||||
|
||||
def get_user_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> UserService:
|
||||
"""组装用户管理应用服务。"""
|
||||
return UserService(repository=_repository("user", db))
|
||||
|
||||
|
||||
def get_auth_service() -> AuthService:
|
||||
"""组装同步认证应用服务。"""
|
||||
return AuthService(
|
||||
users=_standalone_repository("user"),
|
||||
config=_standalone_repository("system_config"),
|
||||
passkeys=_standalone_repository("passkey"),
|
||||
)
|
||||
|
||||
|
||||
def get_passkey_service() -> PasskeyService:
|
||||
"""组装 PassKey 应用服务。"""
|
||||
return PasskeyService(repository=_standalone_repository("passkey"))
|
||||
|
||||
|
||||
def get_subscription_mutation_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> SubscriptionMutationService:
|
||||
"""组装异步订阅写服务。"""
|
||||
return SubscriptionMutationService(
|
||||
repository=_repository("subscribe", db),
|
||||
history_repository=_repository("subscribe_history", db),
|
||||
)
|
||||
|
||||
|
||||
def get_subscription_sync_mutation_service(
|
||||
db: Session = Depends(get_db),
|
||||
) -> SubscriptionMutationService:
|
||||
"""组装同步订阅查询服务,供文件信息接口使用。"""
|
||||
return SubscriptionMutationService(repository=_repository("subscribe", db))
|
||||
|
||||
|
||||
def get_servarr_subscription_service(
|
||||
async_db: AsyncSession = Depends(get_async_db),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ServarrSubscriptionService:
|
||||
"""组装 Servarr 兼容路由的请求级订阅数据用例。"""
|
||||
return ServarrSubscriptionService(
|
||||
async_repository=_repository("subscribe", async_db),
|
||||
sync_repository=_repository("subscribe", db),
|
||||
)
|
||||
|
||||
|
||||
async def _publish_site_updated(payload: dict) -> None:
|
||||
"""发布已提交的站点更新事件。"""
|
||||
await eventmanager.async_send_event(EventType.SiteUpdated, payload)
|
||||
@@ -145,8 +228,8 @@ def get_site_mutation_command(
|
||||
return f"{scheme}://{netloc}/"
|
||||
|
||||
return SiteMutationCommand(
|
||||
repository=SiteOper(db),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(db),
|
||||
repository=_repository("site", db),
|
||||
unit_of_work=_transaction("async", db),
|
||||
auth_level_provider=lambda: sites_helper.auth_level,
|
||||
indexer_loader=sites_helper.async_get_indexer,
|
||||
domain_extractor=site_rules.extract_domain,
|
||||
@@ -156,6 +239,20 @@ def get_site_mutation_command(
|
||||
)
|
||||
|
||||
|
||||
def get_site_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> SiteQueryService:
|
||||
"""组装站点异步查询服务。"""
|
||||
return SiteQueryService(repository=_repository("site", db))
|
||||
|
||||
|
||||
def get_site_sync_query_service(
|
||||
db: Session = Depends(get_db),
|
||||
) -> SiteQueryService:
|
||||
"""组装站点同步查询服务,用于同步 Chain 路由。"""
|
||||
return SiteQueryService(repository=_repository("site", db))
|
||||
|
||||
|
||||
def get_workflow_mutation_command(
|
||||
db: Session = Depends(get_db),
|
||||
) -> WorkflowMutationCommand:
|
||||
@@ -163,15 +260,15 @@ def get_workflow_mutation_command(
|
||||
scheduler = Scheduler()
|
||||
workflow_manager = WorkFlowManager()
|
||||
return WorkflowMutationCommand(
|
||||
repository=WorkflowOper(db),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(db),
|
||||
repository=_repository("workflow", db),
|
||||
unit_of_work=_transaction("sync", db),
|
||||
add_timer=scheduler.update_workflow_job,
|
||||
remove_timer=scheduler.remove_workflow_job,
|
||||
load_event=workflow_manager.load_workflow_events,
|
||||
remove_event=workflow_manager.remove_workflow_event,
|
||||
refresh_event=workflow_manager.update_workflow_event,
|
||||
stop_running=global_vars.stop_workflow,
|
||||
delete_cache=lambda workflow_id: SystemConfigOper().delete(
|
||||
delete_cache=lambda workflow_id: _standalone_repository("system_config").delete(
|
||||
f"WorkflowCache-{workflow_id}"
|
||||
),
|
||||
)
|
||||
@@ -182,35 +279,92 @@ def get_workflow_definition_command(
|
||||
) -> WorkflowDefinitionCommand:
|
||||
"""组装工作流创建、复用和重置的异步写用例。"""
|
||||
return WorkflowDefinitionCommand(
|
||||
repository=WorkflowOper(db),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(db),
|
||||
repository=_repository("workflow", db),
|
||||
unit_of_work=_transaction("async", db),
|
||||
stop_running=global_vars.stop_workflow,
|
||||
delete_cache=lambda workflow_id: SystemConfigOper().delete(
|
||||
delete_cache=lambda workflow_id: _standalone_repository("system_config").delete(
|
||||
f"WorkflowCache-{workflow_id}"
|
||||
),
|
||||
report_fork=MoviePilotServerHelper.async_workflow_fork_by_id,
|
||||
)
|
||||
|
||||
|
||||
def get_workflow_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> WorkflowQueryService:
|
||||
"""组装工作流只读查询用例,避免端点直接持有数据库操作器。"""
|
||||
return WorkflowQueryService(repository=_repository("workflow", db))
|
||||
|
||||
|
||||
def get_message_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> MessageQueryService:
|
||||
"""组装消息历史异步查询服务。"""
|
||||
return MessageQueryService(repository=_repository("message", db))
|
||||
|
||||
|
||||
def get_agent_chat_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> AgentChatService:
|
||||
"""组装 Agent 会话历史查询和删除服务。"""
|
||||
return AgentChatService(repository=_repository("agent_chat", db))
|
||||
|
||||
|
||||
def get_mediaserver_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> MediaServerQueryService:
|
||||
"""组装媒体服务器本地条目异步查询服务。"""
|
||||
return MediaServerQueryService(repository=_repository("media_server", db))
|
||||
|
||||
|
||||
def get_dashboard_query_service(
|
||||
db: Session = Depends(get_db),
|
||||
) -> DashboardQueryService:
|
||||
"""组装 Dashboard 媒体与整理历史统计查询服务。"""
|
||||
from app.chain.dashboard import DashboardChain
|
||||
|
||||
return DashboardQueryService(
|
||||
repository=_repository("transfer_history", db),
|
||||
media_statistics=DashboardChain().media_statistic,
|
||||
)
|
||||
|
||||
|
||||
def get_download_history_mutation_command(
|
||||
db: Session = Depends(get_db),
|
||||
) -> DownloadHistoryMutationCommand:
|
||||
"""组装下载历史删除用例及其请求级事务。"""
|
||||
return DownloadHistoryMutationCommand(
|
||||
repository=DownloadHistoryOper(db),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(db),
|
||||
repository=_repository("download_history", db),
|
||||
unit_of_work=_transaction("sync", db),
|
||||
)
|
||||
|
||||
|
||||
def get_history_query_service(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
) -> HistoryQueryService:
|
||||
"""组装历史列表和详情异步查询服务。"""
|
||||
return HistoryQueryService(
|
||||
download_repository=_repository("download_history", db),
|
||||
transfer_repository=_repository("transfer_history", db),
|
||||
)
|
||||
|
||||
|
||||
def get_transfer_history_lookup_service(
|
||||
db: Session = Depends(get_db),
|
||||
) -> TransferHistoryLookupService:
|
||||
"""组装手动整理使用的同步历史投影服务。"""
|
||||
return TransferHistoryLookupService(_repository("transfer_history", db))
|
||||
|
||||
|
||||
def get_transfer_history_mutation_command(
|
||||
db: Session = Depends(get_db),
|
||||
) -> TransferHistoryMutationCommand:
|
||||
"""组装整理历史删除、文件处理和事件发布用例。"""
|
||||
storage_chain = StorageChain()
|
||||
return TransferHistoryMutationCommand(
|
||||
repository=TransferHistoryOper(db),
|
||||
download_repository=DownloadHistoryOper(db),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(db),
|
||||
repository=_repository("transfer_history", db),
|
||||
download_repository=_repository("download_history", db),
|
||||
unit_of_work=_transaction("sync", db),
|
||||
file_item_factory=lambda payload: _SchemaFileItem(**payload),
|
||||
delete_media_file=storage_chain.delete_media_file,
|
||||
publish_download_file_deleted=lambda payload: eventmanager.send_event(
|
||||
@@ -257,11 +411,11 @@ def get_plugin_config_command() -> PluginConfigCommand:
|
||||
def get_current_user(
|
||||
db: Session = Depends(get_db),
|
||||
token_data: _SchemaTokenPayload = Depends(verify_token)
|
||||
) -> User:
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前用户
|
||||
"""
|
||||
user = User.get(db, rid=token_data.sub)
|
||||
user = _repository("user", db).get_by_id(token_data.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=403, detail="用户不存在")
|
||||
return user
|
||||
@@ -270,19 +424,19 @@ def get_current_user(
|
||||
async def get_current_user_async(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
token_data: _SchemaTokenPayload = Depends(verify_token)
|
||||
) -> User:
|
||||
) -> Any:
|
||||
"""
|
||||
异步获取当前用户
|
||||
"""
|
||||
user = await User.async_get(db, rid=token_data.sub)
|
||||
user = await _repository("user", db).async_get_by_id(token_data.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=403, detail="用户不存在")
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前激活用户
|
||||
"""
|
||||
@@ -292,8 +446,8 @@ def get_current_active_user(
|
||||
|
||||
|
||||
async def get_current_active_user_async(
|
||||
current_user: User = Depends(get_current_user_async),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
异步获取当前激活用户
|
||||
"""
|
||||
@@ -302,7 +456,7 @@ async def get_current_active_user_async(
|
||||
return current_user
|
||||
|
||||
|
||||
def _ensure_manage_user(current_user: User) -> User:
|
||||
def _ensure_manage_user(current_user: Any) -> Any:
|
||||
"""
|
||||
校验用户具备全局管理权限。
|
||||
"""
|
||||
@@ -315,8 +469,8 @@ def _ensure_manage_user(current_user: User) -> User:
|
||||
|
||||
|
||||
def get_current_active_manage_user(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前拥有管理权限的激活用户。
|
||||
"""
|
||||
@@ -324,8 +478,8 @@ def get_current_active_manage_user(
|
||||
|
||||
|
||||
async def get_current_active_manage_user_async(
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
异步获取当前拥有管理权限的激活用户。
|
||||
"""
|
||||
@@ -333,8 +487,8 @@ async def get_current_active_manage_user_async(
|
||||
|
||||
|
||||
def get_current_active_superuser(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前激活超级管理员
|
||||
"""
|
||||
@@ -346,8 +500,8 @@ def get_current_active_superuser(
|
||||
|
||||
|
||||
async def get_current_active_superuser_async(
|
||||
current_user: User = Depends(get_current_user_async),
|
||||
) -> User:
|
||||
current_user: Any = Depends(get_current_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
异步获取当前激活超级管理员
|
||||
"""
|
||||
|
||||
+72
-71
@@ -16,7 +16,6 @@ from typing import Any, AsyncIterator, Callable, Optional, Union
|
||||
from fastapi import Depends, File, Form, HTTPException, Request, UploadFile, status
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.agent import AgentChatDisplaySaveRequest as _SchemaAgentChatDisplaySaveRequest
|
||||
from app.schemas.agent import AgentChatSessionDetail as _SchemaAgentChatSessionDetail
|
||||
@@ -45,12 +44,14 @@ from app.chain.message import MessageChain
|
||||
from app.command import Command
|
||||
from app.runtime.config import global_vars, settings
|
||||
from app.runtime.events import Event, EventManager
|
||||
from app.db import get_async_db
|
||||
from app.db.oper.agentchat import AgentChatOper
|
||||
from app.db.models import User
|
||||
from app.db.models.agentchat import AgentChat
|
||||
from app.db.oper.user import UserOper
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.deps import get_agent_chat_service, get_current_active_user
|
||||
from app.application.messaging.chat import (
|
||||
AgentChatRecord,
|
||||
AgentChatService,
|
||||
get_configured_agent_chat_service,
|
||||
)
|
||||
from app.application.security.user import get_configured_user_id_lookup
|
||||
from app.application.messaging.agent import attach_web_agent_edit_queue, detach_web_agent_edit_queue
|
||||
from app.application.messaging.agent import agent_interaction_manager
|
||||
from app.application.messaging.agent import (
|
||||
@@ -187,7 +188,7 @@ class _WebAgentEventPublisher:
|
||||
self._pending_signal.clear()
|
||||
|
||||
|
||||
def _ensure_superuser(user: User) -> None:
|
||||
def _ensure_superuser(user: ApiPrincipal) -> None:
|
||||
"""校验当前用户是否为超级管理员。"""
|
||||
if not getattr(user, "is_superuser", False):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
@@ -199,7 +200,7 @@ def _ensure_superuser(user: User) -> None:
|
||||
response_model=_SchemaResponse[_SchemaAgentMcpServerListData],
|
||||
)
|
||||
async def list_agent_mcp_servers(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
查询 Agent 外部 MCP 服务器配置。
|
||||
@@ -224,7 +225,7 @@ async def list_agent_mcp_servers(
|
||||
)
|
||||
async def save_agent_mcp_servers(
|
||||
request: _SchemaAgentMcpServersSaveRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
保存 Agent 外部 MCP 服务器配置。
|
||||
@@ -244,7 +245,7 @@ async def save_agent_mcp_servers(
|
||||
)
|
||||
async def test_agent_mcp_server(
|
||||
request: _SchemaAgentMcpServerTestRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
测试 Agent 外部 MCP 服务器连接并读取工具列表。
|
||||
@@ -431,7 +432,7 @@ class _WebAgentMoviePilotAgentMixin:
|
||||
if not self.user_id:
|
||||
return False
|
||||
try:
|
||||
user = await UserOper().async_get_by_id(int(self.user_id))
|
||||
user = get_configured_user_id_lookup()(int(self.user_id))
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
except Exception as e:
|
||||
@@ -486,7 +487,7 @@ def _get_web_agent_type() -> type:
|
||||
return _WEB_AGENT_TYPE
|
||||
|
||||
|
||||
def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
|
||||
def _build_web_agent_session_id(user: ApiPrincipal, session_id: Optional[str]) -> str:
|
||||
"""
|
||||
构建前端 Agent 会话 ID。
|
||||
|
||||
@@ -498,8 +499,8 @@ def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
|
||||
if seed.startswith(WEB_AGENT_SESSION_PREFIX):
|
||||
return seed
|
||||
try:
|
||||
existing_chat = AgentChatOper().get(session_id=seed)
|
||||
if existing_chat and _can_access_agent_chat(existing_chat, user):
|
||||
existing_chat = get_configured_agent_chat_service().get_sync(seed)
|
||||
if existing_chat and AgentChatService.can_access(existing_chat, user):
|
||||
return seed
|
||||
except Exception as e:
|
||||
logger.debug(f"读取WebAgent历史会话失败: {e}")
|
||||
@@ -508,7 +509,7 @@ def _build_web_agent_session_id(user: User, session_id: Optional[str]) -> str:
|
||||
return f"{WEB_AGENT_SESSION_PREFIX}{digest[:32]}"
|
||||
|
||||
|
||||
def _can_access_agent_chat(chat: AgentChat, user: User) -> bool:
|
||||
def _can_access_agent_chat(chat: Any, user: ApiPrincipal) -> bool:
|
||||
"""
|
||||
判断当前登录用户是否可以访问指定 Agent 会话。
|
||||
|
||||
@@ -524,15 +525,14 @@ def _can_access_agent_chat(chat: AgentChat, user: User) -> bool:
|
||||
|
||||
|
||||
async def _get_accessible_agent_chat(
|
||||
oper: AgentChatOper, session_id: str, user: User
|
||||
) -> Optional[AgentChat]:
|
||||
service: AgentChatService,
|
||||
session_id: str,
|
||||
user: ApiPrincipal,
|
||||
) -> Optional[AgentChatRecord]:
|
||||
"""
|
||||
读取当前用户可访问的 Agent 会话。
|
||||
"""
|
||||
chat = await oper.async_get(session_id=session_id)
|
||||
if not chat or not _can_access_agent_chat(chat, user):
|
||||
return None
|
||||
return chat
|
||||
return await service.get_accessible(session_id, user)
|
||||
|
||||
|
||||
def _append_web_agent_text_segment(assistant_message: dict, content: str) -> None:
|
||||
@@ -631,7 +631,7 @@ def _apply_web_agent_display_event(event: dict, assistant_message: dict) -> None
|
||||
def _save_web_agent_display_snapshot(
|
||||
*,
|
||||
session_id: str,
|
||||
current_user: User,
|
||||
current_user: ApiPrincipal,
|
||||
messages: list[dict],
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> None:
|
||||
@@ -639,9 +639,9 @@ def _save_web_agent_display_snapshot(
|
||||
保存 WebAgent 当前展示消息快照。
|
||||
"""
|
||||
try:
|
||||
oper = AgentChatOper()
|
||||
existing_chat = oper.get(session_id=session_id)
|
||||
AgentChatOper().save_display_messages(
|
||||
service = get_configured_agent_chat_service()
|
||||
existing_chat = service.get_sync(session_id)
|
||||
service.save_display_sync(
|
||||
session_id=session_id,
|
||||
user_id=(existing_chat.user_id if existing_chat else str(current_user.id)),
|
||||
username=(existing_chat.username if existing_chat else current_user.name),
|
||||
@@ -716,7 +716,7 @@ def _sanitize_web_agent_upload_name(
|
||||
return safe_name
|
||||
|
||||
|
||||
def _get_web_agent_upload_dir(user: User, session_id: Optional[str]) -> Path:
|
||||
def _get_web_agent_upload_dir(user: ApiPrincipal, session_id: Optional[str]) -> Path:
|
||||
"""
|
||||
计算当前 Web Agent 会话的临时附件目录。
|
||||
|
||||
@@ -1425,7 +1425,7 @@ def _get_web_agent_unknown_command_message(text: str) -> Optional[str]:
|
||||
return f"命令不存在:{command}"
|
||||
|
||||
|
||||
def _ensure_web_agent_command_allowed(current_user: User) -> Optional[str]:
|
||||
def _ensure_web_agent_command_allowed(current_user: ApiPrincipal) -> Optional[str]:
|
||||
"""
|
||||
校验当前 Web 用户是否可以执行传统斜杠命令。
|
||||
|
||||
@@ -1440,7 +1440,7 @@ def _ensure_web_agent_command_allowed(current_user: User) -> Optional[str]:
|
||||
async def _collect_web_agent_traditional_events(
|
||||
*,
|
||||
text: str,
|
||||
current_user: User,
|
||||
current_user: ApiPrincipal,
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
original_chat_id: Optional[Union[str, int]] = None,
|
||||
) -> list[dict]:
|
||||
@@ -1637,7 +1637,7 @@ async def download_web_agent_file(file_id: str) -> FileResponse:
|
||||
async def upload_web_agent_file(
|
||||
file: UploadFile = File(...),
|
||||
session_id: Optional[str] = Form(None),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
上传 Web 智能助手对话附件。
|
||||
@@ -1680,7 +1680,7 @@ async def upload_web_agent_file(
|
||||
)
|
||||
async def web_agent_callback(
|
||||
payload: _SchemaAgentWebChoiceRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
接收 Web 智能助手选择卡片回调。
|
||||
@@ -1717,7 +1717,7 @@ async def web_agent_callback(
|
||||
response_model=_SchemaResponse[list[_SchemaAgentWebCommandInfo]],
|
||||
)
|
||||
async def list_web_agent_commands(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
获取当前 Web 智能助手可补全的斜杠命令。
|
||||
@@ -1737,8 +1737,8 @@ async def list_web_agent_commands(
|
||||
response_model=_SchemaResponse[list[_SchemaAgentChatSessionSummary]],
|
||||
)
|
||||
async def list_agent_chat_sessions(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
service: AgentChatService = Depends(get_agent_chat_service),
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
) -> _SchemaResponse:
|
||||
@@ -1746,23 +1746,17 @@ async def list_agent_chat_sessions(
|
||||
获取当前用户可访问的 Agent 历史会话列表。
|
||||
|
||||
:param current_user: 当前登录用户
|
||||
:param db: 异步数据库会话
|
||||
:param service: Agent 会话应用服务
|
||||
:param page: 页码
|
||||
:param count: 每页数量
|
||||
:return: 会话摘要列表
|
||||
"""
|
||||
user_id = None if current_user.is_superuser else str(current_user.id)
|
||||
username = None if current_user.is_superuser else current_user.name
|
||||
chats = await AgentChatOper(db).async_list_by_page(
|
||||
chats = await service.list(
|
||||
current_user,
|
||||
page=page,
|
||||
count=count,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
)
|
||||
return _SchemaResponse(
|
||||
success=True,
|
||||
data=[AgentChatOper.to_summary(chat) for chat in chats],
|
||||
)
|
||||
return _SchemaResponse(success=True, data=chats)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -1772,24 +1766,27 @@ async def list_agent_chat_sessions(
|
||||
)
|
||||
async def get_agent_chat_session(
|
||||
session_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
service: AgentChatService = Depends(get_agent_chat_service),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
获取一条 Agent 历史会话详情。
|
||||
|
||||
:param session_id: Agent 会话 ID
|
||||
:param current_user: 当前登录用户
|
||||
:param db: 异步数据库会话
|
||||
:param service: Agent 会话应用服务
|
||||
:return: 会话详情
|
||||
"""
|
||||
oper = AgentChatOper(db)
|
||||
chat = await _get_accessible_agent_chat(oper, session_id, current_user)
|
||||
chat = await _get_accessible_agent_chat(service, session_id, current_user)
|
||||
server_session_id = session_id
|
||||
if not chat:
|
||||
server_session_id = _build_web_agent_session_id(current_user, session_id)
|
||||
if server_session_id != session_id:
|
||||
chat = await _get_accessible_agent_chat(oper, server_session_id, current_user)
|
||||
chat = await _get_accessible_agent_chat(
|
||||
service,
|
||||
server_session_id,
|
||||
current_user,
|
||||
)
|
||||
if not chat:
|
||||
manager = get_running_agent_manager()
|
||||
if manager and manager.is_session_busy(server_session_id):
|
||||
@@ -1803,7 +1800,7 @@ async def get_agent_chat_session(
|
||||
},
|
||||
)
|
||||
return _SchemaResponse(success=False, message="会话不存在或无权访问")
|
||||
data = AgentChatOper.to_detail(chat)
|
||||
data = service.to_detail(chat).model_dump()
|
||||
manager = get_running_agent_manager()
|
||||
data["is_processing"] = bool(
|
||||
manager and manager.is_session_busy(chat.session_id)
|
||||
@@ -1819,8 +1816,8 @@ async def get_agent_chat_session(
|
||||
async def save_agent_chat_display(
|
||||
session_id: str,
|
||||
payload: _SchemaAgentChatDisplaySaveRequest,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
service: AgentChatService = Depends(get_agent_chat_service),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
保存前端聚合后的 Agent 展示消息。
|
||||
@@ -1828,12 +1825,15 @@ async def save_agent_chat_display(
|
||||
:param session_id: Agent 会话 ID
|
||||
:param payload: 展示消息保存请求
|
||||
:param current_user: 当前登录用户
|
||||
:param db: 异步数据库会话
|
||||
:param service: Agent 会话应用服务
|
||||
:return: 保存后的会话摘要
|
||||
"""
|
||||
oper = AgentChatOper(db)
|
||||
existing_chat = await oper.async_get(session_id=session_id)
|
||||
if existing_chat and not _can_access_agent_chat(existing_chat, current_user):
|
||||
existing_chat = await service.get_accessible(session_id, current_user)
|
||||
if existing_chat is None:
|
||||
unrestricted_chat = await service.get(session_id)
|
||||
else:
|
||||
unrestricted_chat = existing_chat
|
||||
if unrestricted_chat and existing_chat is None:
|
||||
return _SchemaResponse(success=False, message="会话不存在或无权访问")
|
||||
|
||||
messages = [
|
||||
@@ -1847,10 +1847,10 @@ async def save_agent_chat_display(
|
||||
messages=messages,
|
||||
client_session_id=existing_chat.client_session_id if existing_chat else session_id,
|
||||
)
|
||||
chat = await oper.async_get(session_id=session_id)
|
||||
chat = await service.get_accessible(session_id, current_user)
|
||||
if not chat:
|
||||
return _SchemaResponse(success=False, message="会话保存失败")
|
||||
return _SchemaResponse(success=True, data=AgentChatOper.to_summary(chat))
|
||||
return _SchemaResponse(success=True, data=service.to_summary(chat))
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -1860,22 +1860,21 @@ async def save_agent_chat_display(
|
||||
)
|
||||
async def delete_agent_chat_session(
|
||||
session_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
service: AgentChatService = Depends(get_agent_chat_service),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
删除一条 Agent 历史会话。
|
||||
|
||||
:param session_id: Agent 会话 ID
|
||||
:param current_user: 当前登录用户
|
||||
:param db: 异步数据库会话
|
||||
:param service: Agent 会话应用服务
|
||||
:return: 删除结果
|
||||
"""
|
||||
oper = AgentChatOper(db)
|
||||
chat = await _get_accessible_agent_chat(oper, session_id, current_user)
|
||||
chat = await _get_accessible_agent_chat(service, session_id, current_user)
|
||||
if not chat:
|
||||
return _SchemaResponse(success=False, message="会话不存在或无权访问")
|
||||
deleted = await oper.async_delete(session_id=session_id)
|
||||
deleted = await service.delete(session_id, current_user)
|
||||
return _SchemaResponse(success=deleted, message="删除成功" if deleted else "删除失败")
|
||||
|
||||
|
||||
@@ -1886,23 +1885,25 @@ async def delete_agent_chat_session(
|
||||
)
|
||||
async def stop_web_agent_session_task(
|
||||
session_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
service: AgentChatService = Depends(get_agent_chat_service),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
停止当前 Web 智能助手会话正在执行的任务。
|
||||
|
||||
:param session_id: Agent 会话 ID
|
||||
:param current_user: 当前登录用户
|
||||
:param db: 异步数据库会话
|
||||
:param service: Agent 会话应用服务
|
||||
:return: 停止结果
|
||||
"""
|
||||
server_session_id = _build_web_agent_session_id(current_user, session_id)
|
||||
chat = await _get_accessible_agent_chat(
|
||||
AgentChatOper(db), server_session_id, current_user
|
||||
service,
|
||||
server_session_id,
|
||||
current_user,
|
||||
)
|
||||
if not chat and server_session_id != session_id:
|
||||
chat = await _get_accessible_agent_chat(AgentChatOper(db), session_id, current_user)
|
||||
chat = await _get_accessible_agent_chat(service, session_id, current_user)
|
||||
if chat and not _can_access_agent_chat(chat, current_user):
|
||||
return _SchemaResponse(success=False, message="会话不存在或无权访问")
|
||||
|
||||
@@ -1930,7 +1931,7 @@ async def stop_web_agent_session_task(
|
||||
async def web_agent_stream(
|
||||
payload: _SchemaAgentWebChatRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
Web 智能助手流式对话。
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.anilist import AniListChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from app.api.openai_utils import (
|
||||
)
|
||||
from app.agent.runtime_loader import get_running_agent_manager
|
||||
from app.runtime.config import settings
|
||||
from app.application.security.access import anthropic_api_key_header
|
||||
from app.adapters.web.security.access import anthropic_api_key_header
|
||||
|
||||
ANTHROPIC_ERROR_RESPONSES = {
|
||||
400: {"model": _SchemaAnthropicErrorResponse, "description": "请求格式错误"},
|
||||
|
||||
+14
-12
@@ -1,15 +1,14 @@
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.token import Token as _SchemaToken
|
||||
from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo
|
||||
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
|
||||
from app.application.security.auth import build_token_response, consume_plugin_auth_ticket
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.db.models.passkey import PassKey
|
||||
from app.db.models.user import User
|
||||
from app.application.security.auth import AuthService, consume_plugin_auth_ticket
|
||||
from app.application.plugin.runtime import get_plugin_manager as PluginManager
|
||||
from app.api.deps import get_auth_service
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
@@ -22,13 +21,13 @@ class AuthExchangeRequest(BaseModel):
|
||||
ticket: str
|
||||
|
||||
|
||||
def _system_auth_providers() -> list[dict[str, Any]]:
|
||||
def _system_auth_providers(service: AuthService) -> list[dict[str, Any]]:
|
||||
"""
|
||||
获取系统内建的匿名登录方式摘要。
|
||||
|
||||
:return: 系统认证提供方列表
|
||||
"""
|
||||
has_passkey = bool(PassKey.list(db=None))
|
||||
has_passkey = service.has_passkey()
|
||||
return [
|
||||
{
|
||||
"id": "system:passkey",
|
||||
@@ -46,13 +45,13 @@ def _system_auth_providers() -> list[dict[str, Any]]:
|
||||
summary="查询登录认证提供方",
|
||||
response_model=list[_SchemaAuthProviderInfo],
|
||||
)
|
||||
def auth_providers() -> list[dict[str, Any]]:
|
||||
def auth_providers(service: AuthService = Depends(get_auth_service)) -> list[dict[str, Any]]:
|
||||
"""
|
||||
查询系统和插件提供的登录认证入口。
|
||||
|
||||
:return: 认证提供方摘要列表
|
||||
"""
|
||||
providers = _system_auth_providers()
|
||||
providers = _system_auth_providers(service)
|
||||
providers.extend(PluginManager().get_plugin_auth_providers())
|
||||
return [provider for provider in providers if provider.get("enabled", True)]
|
||||
|
||||
@@ -63,7 +62,10 @@ def auth_providers() -> list[dict[str, Any]]:
|
||||
response_model=_SchemaToken,
|
||||
openapi_extra={RAW_RESPONSE_OPENAPI_KEY: True},
|
||||
)
|
||||
def auth_exchange(body: AuthExchangeRequest) -> _SchemaToken:
|
||||
def auth_exchange(
|
||||
body: AuthExchangeRequest,
|
||||
service: AuthService = Depends(get_auth_service),
|
||||
) -> _SchemaToken:
|
||||
"""
|
||||
将插件认证成功后生成的一次性票据兑换为系统 Token。
|
||||
|
||||
@@ -74,8 +76,8 @@ def auth_exchange(body: AuthExchangeRequest) -> _SchemaToken:
|
||||
if not ticket_data:
|
||||
raise HTTPException(status_code=401, detail="认证票据无效或已过期")
|
||||
|
||||
user = User.get(db=None, rid=ticket_data.get("user_id"))
|
||||
user = service.get_user_by_id(ticket_data.get("user_id"))
|
||||
if not user or not user.is_active:
|
||||
raise HTTPException(status_code=403, detail="用户不存在或已禁用")
|
||||
|
||||
return build_token_response(user)
|
||||
return service.build_token_response(user)
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.bangumi import BangumiChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from pathlib import Path
|
||||
from typing import Any, List, Optional, Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.dashboard import DashboardMemoryInfo as _SchemaDashboardMemoryInfo
|
||||
from app.schemas.dashboard import DashboardSystemInfo as _SchemaDashboardSystemInfo
|
||||
@@ -17,53 +16,17 @@ from app.api.response import ResponseAPIRouter
|
||||
from app.chain.dashboard import DashboardChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.runtime.config import settings
|
||||
from app.application.security.access import verify_apitoken
|
||||
from app.db import get_db
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.api.deps import get_current_active_superuser
|
||||
from app.adapters.web.security.access import verify_apitoken
|
||||
from app.api.deps import get_current_active_superuser, get_dashboard_query_service
|
||||
from app.application.dashboard import DashboardQueryService
|
||||
from app.schemas.types import StorageAction
|
||||
from app.application.directory import DirectoryHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.application.scheduling import Scheduler
|
||||
from app.adapters.system.host import SystemUtils
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
def _build_statistic(db: Session, name: Optional[str] = None) -> _SchemaStatistic:
|
||||
"""
|
||||
构建媒体数量统计信息。
|
||||
"""
|
||||
media_statistics: Optional[List[_SchemaStatistic]] = (
|
||||
DashboardChain().media_statistic(name)
|
||||
)
|
||||
if media_statistics:
|
||||
# 汇总各媒体库统计信息
|
||||
ret_statistic = _SchemaStatistic()
|
||||
has_episode_count = False
|
||||
for media_statistic in media_statistics:
|
||||
ret_statistic.movie_count += media_statistic.movie_count or 0
|
||||
ret_statistic.tv_count += media_statistic.tv_count or 0
|
||||
ret_statistic.music_count += media_statistic.music_count or 0
|
||||
ret_statistic.user_count += media_statistic.user_count or 0
|
||||
if media_statistic.episode_count is not None:
|
||||
ret_statistic.episode_count += media_statistic.episode_count or 0
|
||||
has_episode_count = True
|
||||
if not has_episode_count:
|
||||
# 所有媒体服务都未提供剧集统计时,返回 None 供前端展示“未获取”。
|
||||
ret_statistic.episode_count = None
|
||||
else:
|
||||
ret_statistic = _SchemaStatistic()
|
||||
|
||||
movie_count_month, tv_count_month, episode_count_month, music_count_month = (
|
||||
TransferHistory.monthly_media_statistics(db)
|
||||
)
|
||||
ret_statistic.movie_count_month = movie_count_month
|
||||
ret_statistic.tv_count_month = tv_count_month
|
||||
ret_statistic.episode_count_month = episode_count_month
|
||||
ret_statistic.music_count_month = music_count_month
|
||||
return ret_statistic
|
||||
|
||||
|
||||
def _build_storage() -> _SchemaStorage:
|
||||
"""
|
||||
构建本地存储空间信息。
|
||||
@@ -114,13 +77,13 @@ def _build_downloader(name: Optional[str] = None) -> _SchemaDownloaderInfo:
|
||||
@router.get("/statistic", summary="媒体数量统计", response_model=_SchemaStatistic)
|
||||
def statistic(
|
||||
name: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
service: DashboardQueryService = Depends(get_dashboard_query_service),
|
||||
_: Any = Depends(get_current_active_superuser),
|
||||
) -> Any:
|
||||
"""
|
||||
查询媒体数量统计信息
|
||||
"""
|
||||
return _build_statistic(db, name)
|
||||
return service.statistic(name)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -128,12 +91,12 @@ def statistic(
|
||||
)
|
||||
def statistic2(
|
||||
_: Annotated[str, Depends(verify_apitoken)],
|
||||
db: Session = Depends(get_db),
|
||||
service: DashboardQueryService = Depends(get_dashboard_query_service),
|
||||
) -> Any:
|
||||
"""
|
||||
查询媒体数量统计信息 API_TOKEN认证(?token=xxx)
|
||||
"""
|
||||
return _build_statistic(db)
|
||||
return service.statistic()
|
||||
|
||||
|
||||
@router.get("/storage", summary="本地存储空间", response_model=_SchemaStorage)
|
||||
@@ -249,14 +212,13 @@ async def schedule_progress2(
|
||||
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
|
||||
async def transfer(
|
||||
days: Optional[int] = 7,
|
||||
db: Session = Depends(get_db),
|
||||
service: DashboardQueryService = Depends(get_dashboard_query_service),
|
||||
_: Any = Depends(get_current_active_superuser),
|
||||
) -> Any:
|
||||
"""
|
||||
查询文件整理统计信息
|
||||
"""
|
||||
transfer_stat = await TransferHistory.async_statistic(db, days)
|
||||
return [stat[1] for stat in transfer_stat]
|
||||
return await service.transfer(days)
|
||||
|
||||
|
||||
@router.get("/cpu", summary="获取当前CPU使用率", response_model=float)
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.chain.bangumi import BangumiChain
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.runtime.events import eventmanager
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.schemas.event import DiscoverSourceEventData
|
||||
from app.schemas.types import ChainEventType, MediaType
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
@@ -20,11 +20,14 @@ from app.chain.media import MediaChain
|
||||
from app.domain.context import Context, MediaInfo, MusicInfo, SubtitleInfo, TorrentInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.application.site.query import (
|
||||
SiteQueryService,
|
||||
get_configured_site_query_service,
|
||||
)
|
||||
from app.api.deps import get_current_active_user, get_site_sync_query_service
|
||||
from app.application.directory import DirectoryHelper
|
||||
from app.schemas.types import (
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
@@ -39,7 +42,10 @@ from app.application.security.url import SecurityUtils
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
|
||||
def _prepare_subtitle_download(
|
||||
subtitle: SubtitleInfo,
|
||||
query: SiteQueryService | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
校验字幕下载签名,并用服务端站点配置覆盖请求凭据。
|
||||
"""
|
||||
@@ -53,7 +59,8 @@ def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
|
||||
if not clean_url:
|
||||
return False, "字幕下载链接签名无效"
|
||||
|
||||
site = SiteOper().get(subtitle.site)
|
||||
site_query = query or get_configured_site_query_service()
|
||||
site = site_query.get_sync(subtitle.site)
|
||||
if not site:
|
||||
return False, "字幕站点信息不存在"
|
||||
|
||||
@@ -84,7 +91,7 @@ def download(
|
||||
torrent_in: _SchemaTorrentInfo,
|
||||
downloader: Annotated[str | None, Body()] = None,
|
||||
save_path: Annotated[str | None, Body()] = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
添加下载任务(含媒体信息)
|
||||
@@ -130,7 +137,7 @@ def add(
|
||||
downloader: Annotated[str | None, Body()] = None,
|
||||
# 保存路径, 支持<storage>:<path>, 如rclone:/MP, smb:/server/share/Movies等
|
||||
save_path: Annotated[str | None, Body()] = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
添加下载任务(不含媒体信息)
|
||||
@@ -213,14 +220,20 @@ def download_subtitle(
|
||||
media_source: Annotated[MediaSource, Body()],
|
||||
media_id: Annotated[str, Body()],
|
||||
save_path: Annotated[str | None, Body()] = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
) -> Any:
|
||||
"""
|
||||
下载字幕资源。
|
||||
"""
|
||||
subtitle_info = SubtitleInfo()
|
||||
subtitle_info.from_dict(subtitle_in.model_dump())
|
||||
valid, message = _prepare_subtitle_download(subtitle_info)
|
||||
# 直接调用 endpoint 的旧测试/插件入口不会经过 FastAPI 依赖解析;此时让
|
||||
# 应用查询端口自行提供服务,仍保留真实请求中的注入对象。
|
||||
if not hasattr(query, "get_sync"):
|
||||
valid, message = _prepare_subtitle_download(subtitle_info)
|
||||
else:
|
||||
valid, message = _prepare_subtitle_download(subtitle_info, query)
|
||||
if not valid:
|
||||
return _SchemaResponse(success=False, message=message)
|
||||
|
||||
@@ -273,7 +286,7 @@ async def clients(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询可用下载器
|
||||
"""
|
||||
downloaders: List[dict] = SystemConfigOper().get(SystemConfigKey.Downloaders)
|
||||
downloaders: List[dict] = get_configured_system_config().get(SystemConfigKey.Downloaders)
|
||||
if downloaders:
|
||||
return [
|
||||
{"name": d.get("name"), "type": d.get("type")}
|
||||
|
||||
@@ -3,8 +3,6 @@ import time
|
||||
from typing import List, Any, Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.common import BatchProgressKeyData as _SchemaBatchProgressKeyData
|
||||
from app.schemas.common import ProgressKeyData as _SchemaProgressKeyData
|
||||
@@ -22,23 +20,20 @@ from app.agent.prompt.transfer_redo import (
|
||||
build_manual_redo_prompt,
|
||||
)
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.application.security.access import verify_token
|
||||
from app.db import get_async_db, get_db
|
||||
from app.db.models import User
|
||||
from app.db.models.downloadhistory import DownloadHistory
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.api.deps import (
|
||||
get_current_active_manage_user,
|
||||
get_current_active_superuser,
|
||||
get_download_history_mutation_command,
|
||||
get_history_query_service,
|
||||
get_transfer_history_mutation_command,
|
||||
)
|
||||
from app.runtime.progress import ProgressHelper
|
||||
from app.application.history import (
|
||||
DownloadHistoryMutationCommand,
|
||||
HistoryQueryService,
|
||||
TransferHistoryMutationCommand,
|
||||
)
|
||||
from app.foundation.text import cut as jieba_cut
|
||||
from app.runtime.log import logger
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
@@ -155,13 +150,13 @@ def _start_batch_ai_redo_task(
|
||||
async def download_history(
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
按下载时间倒序查询下载历史记录
|
||||
"""
|
||||
return await DownloadHistory.async_list_by_page(db, page, count)
|
||||
return await query.list_download(page=page, count=count)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -183,14 +178,6 @@ def delete_download_history(
|
||||
return _SchemaResponse(success=result.success, message=result.message)
|
||||
|
||||
|
||||
def _glob_to_like(pattern: str) -> str:
|
||||
"""
|
||||
将 glob 通配符模式转换为 SQL LIKE 模式(使用 \\ 作为转义字符)
|
||||
"""
|
||||
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
return result.replace("*", "%").replace("?", "_")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/transfer",
|
||||
summary="查询整理记录",
|
||||
@@ -201,50 +188,19 @@ async def transfer_history(
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
status: Optional[bool] = None,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*)
|
||||
"""
|
||||
if title == "失败":
|
||||
title = None
|
||||
status = False
|
||||
elif title == "成功":
|
||||
title = None
|
||||
status = True
|
||||
|
||||
if title:
|
||||
if "*" in title or "?" in title:
|
||||
like_pattern = _glob_to_like(title)
|
||||
total = await TransferHistory.async_count_by_title(
|
||||
db, title=like_pattern, status=status, wildcard=True
|
||||
)
|
||||
result = await TransferHistory.async_list_by_title(
|
||||
db, title=like_pattern, page=page, count=count, status=status, wildcard=True
|
||||
)
|
||||
else:
|
||||
words = jieba_cut(title, HMM=False)
|
||||
like_pattern = "%".join(words)
|
||||
total = await TransferHistory.async_count_by_title(
|
||||
db, title=like_pattern, status=status
|
||||
)
|
||||
result = await TransferHistory.async_list_by_title(
|
||||
db, title=like_pattern, page=page, count=count, status=status
|
||||
)
|
||||
else:
|
||||
result = await TransferHistory.async_list_by_page(
|
||||
db, page=page, count=count, status=status
|
||||
)
|
||||
total = await TransferHistory.async_count(db, status=status)
|
||||
|
||||
return _SchemaResponse(
|
||||
success=True,
|
||||
data={
|
||||
"list": [item.to_dict() for item in result],
|
||||
"total": total,
|
||||
},
|
||||
result = await query.list_transfer(
|
||||
title=title,
|
||||
page=page,
|
||||
count=count,
|
||||
status=status,
|
||||
)
|
||||
return _SchemaResponse(success=True, data=result)
|
||||
|
||||
|
||||
@router.delete("/transfer", summary="删除整理记录", response_model=_SchemaResponse[None])
|
||||
@@ -255,7 +211,7 @@ def delete_transfer_history(
|
||||
command: TransferHistoryMutationCommand = Depends(
|
||||
get_transfer_history_mutation_command
|
||||
),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
删除整理记录。
|
||||
@@ -273,10 +229,10 @@ def delete_transfer_history(
|
||||
summary="智能助手重新整理",
|
||||
response_model=_SchemaResponse[_SchemaProgressKeyData],
|
||||
)
|
||||
def ai_redo_transfer_history(
|
||||
async def ai_redo_transfer_history(
|
||||
history_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
手动触发单条历史记录的 AI 重新整理,并返回进度键。
|
||||
@@ -284,7 +240,7 @@ def ai_redo_transfer_history(
|
||||
if not settings.AI_AGENT_ENABLE:
|
||||
return _SchemaResponse(success=False, message="MoviePilot智能助手未启用")
|
||||
|
||||
history = TransferHistory.get(db, history_id)
|
||||
history = await query.get_transfer(history_id)
|
||||
if not history:
|
||||
return _SchemaResponse(success=False, message="整理记录不存在")
|
||||
|
||||
@@ -304,10 +260,10 @@ def ai_redo_transfer_history(
|
||||
summary="智能助手批量重新整理",
|
||||
response_model=_SchemaResponse[_SchemaBatchProgressKeyData],
|
||||
)
|
||||
def batch_ai_redo_transfer_history(
|
||||
async def batch_ai_redo_transfer_history(
|
||||
payload: _SchemaBatchTransferHistoryRedoRequest,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
query: HistoryQueryService = Depends(get_history_query_service),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
手动触发多条历史记录的 AI 批量重新整理,并返回进度键。
|
||||
@@ -319,14 +275,7 @@ def batch_ai_redo_transfer_history(
|
||||
if not history_ids:
|
||||
return _SchemaResponse(success=False, message="未提供有效的整理记录")
|
||||
|
||||
histories = []
|
||||
missing_ids = []
|
||||
for history_id in history_ids:
|
||||
history = TransferHistory.get(db, history_id)
|
||||
if not history:
|
||||
missing_ids.append(history_id)
|
||||
continue
|
||||
histories.append(history)
|
||||
histories, missing_ids = await query.get_transfers(history_ids)
|
||||
|
||||
if missing_ids:
|
||||
return _SchemaResponse(
|
||||
@@ -358,7 +307,7 @@ def empty_transfer_history(
|
||||
command: TransferHistoryMutationCommand = Depends(
|
||||
get_transfer_history_mutation_command
|
||||
),
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: object = Depends(get_current_active_superuser),
|
||||
) -> Any:
|
||||
"""
|
||||
清空整理记录
|
||||
|
||||
@@ -6,7 +6,6 @@ from fastapi.responses import HTMLResponse
|
||||
from app.schemas.common import ManageRequest as _SchemaManageRequest
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.db.models import User
|
||||
from app.api.deps import get_current_active_superuser_async
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
@@ -29,7 +28,7 @@ def _get_llm_provider_manager_type() -> type:
|
||||
async def manage_provider(
|
||||
request: Request,
|
||||
payload: _SchemaManageRequest,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""
|
||||
LLM 提供商统一管理入口:前端上送 target/action/params 原样透传,
|
||||
|
||||
@@ -11,9 +11,10 @@ from app.schemas.token import Token as _SchemaToken
|
||||
from app.schemas.token import TokenPayload as _SchemaTokenPayload
|
||||
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
|
||||
from app.chain.user import MfaRequired, UserChain
|
||||
from app.application.security import access as security
|
||||
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
|
||||
from app.application.security.token import create_access_token
|
||||
from app.runtime.config import settings
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.application.image import WallpaperHelper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
@@ -67,17 +68,17 @@ def login_access_token(
|
||||
level = SitesHelper().auth_level
|
||||
# 是否显示配置向导
|
||||
show_wizard = (
|
||||
not SystemConfigOper().get(SystemConfigKey.SetupWizardState)
|
||||
not get_configured_system_config().get(SystemConfigKey.SetupWizardState)
|
||||
and not settings.ADVANCED_MODE
|
||||
)
|
||||
access_token = security.create_access_token(
|
||||
access_token = create_access_token(
|
||||
userid=user_or_message.id,
|
||||
username=user_or_message.name,
|
||||
super_user=user_or_message.is_superuser,
|
||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
||||
level=level,
|
||||
)
|
||||
security.set_or_refresh_resource_token_cookie(
|
||||
set_or_refresh_resource_token_cookie(
|
||||
request,
|
||||
response,
|
||||
_SchemaTokenPayload(
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.schemas.mcp import ToolCallRequest as _SchemaToolCallRequest
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
|
||||
from app.agent.tools.manager import moviepilot_tool_manager
|
||||
from app.application.security.access import verify_apikey
|
||||
from app.adapters.web.security.access import verify_apikey
|
||||
from app.runtime.log import logger
|
||||
|
||||
# 导入版本号
|
||||
|
||||
@@ -25,8 +25,7 @@ from app.domain.context import Context, MusicInfo
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo, MetaInfoPath
|
||||
from app.application.security.access import verify_token, verify_apitoken
|
||||
from app.db.models import User
|
||||
from app.adapters.web.security.access import verify_token, verify_apitoken
|
||||
from app.api.deps import get_current_active_user, get_current_active_superuser
|
||||
from app.schemas.category import CategoryConfig
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
|
||||
@@ -440,7 +439,7 @@ def scrape(
|
||||
summary="获取分类策略配置",
|
||||
response_model=_SchemaResponse[_SchemaCategoryConfig],
|
||||
)
|
||||
def get_category_config(_: User = Depends(get_current_active_user)):
|
||||
def get_category_config(_: object = Depends(get_current_active_user)):
|
||||
"""
|
||||
获取分类策略配置
|
||||
"""
|
||||
@@ -452,7 +451,7 @@ def get_category_config(_: User = Depends(get_current_active_user)):
|
||||
"/category/config", summary="保存分类策略配置", response_model=_SchemaResponse[None]
|
||||
)
|
||||
def save_category_config(
|
||||
config: CategoryConfig, _: User = Depends(get_current_active_superuser)
|
||||
config: CategoryConfig, _: object = Depends(get_current_active_superuser)
|
||||
):
|
||||
"""
|
||||
保存分类策略配置
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.common import ServiceClientInfo as _SchemaServiceClientInfo
|
||||
from app.schemas.mediaserver import ExistMediaInfo as _SchemaExistMediaInfo
|
||||
@@ -19,12 +18,10 @@ from app.chain.download import DownloadChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.db import get_async_db
|
||||
from app.db.oper.mediaserver import MediaServerOper
|
||||
from app.db.models import MediaServerItem
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.mediaserver import MediaServerHelper
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.application.mediaserver import MediaServerHelper, MediaServerQueryService
|
||||
from app.api.deps import get_mediaserver_query_service
|
||||
from app.schemas.mediaserver import NotExistMediaInfo
|
||||
from app.schemas.types import MediaSource, MediaType, SystemConfigKey
|
||||
from app.schemas.media import build_media_key, resolve_media_identity
|
||||
@@ -90,7 +87,7 @@ async def exists_local(
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
season: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: MediaServerQueryService = Depends(get_mediaserver_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
@@ -107,7 +104,7 @@ async def exists_local(
|
||||
# 返回对象
|
||||
ret_info = {}
|
||||
# 本地数据库是否存在
|
||||
exist: MediaServerItem = await MediaServerOper(db).async_exists(
|
||||
item_id = await service.find_item_id(
|
||||
title=meta.name if meta else None,
|
||||
year=year,
|
||||
mtype=mtype,
|
||||
@@ -115,8 +112,8 @@ async def exists_local(
|
||||
media_id=media_id,
|
||||
season=season,
|
||||
)
|
||||
if exist:
|
||||
ret_info = {"id": exist.item_id}
|
||||
if item_id:
|
||||
ret_info = {"id": item_id}
|
||||
return _SchemaResponse(success=True, data={"item": ret_info})
|
||||
|
||||
|
||||
@@ -251,7 +248,7 @@ async def clients(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询可用媒体服务器
|
||||
"""
|
||||
mediaservers: List[dict] = SystemConfigOper().get(SystemConfigKey.MediaServers)
|
||||
mediaservers: List[dict] = get_configured_system_config().get(SystemConfigKey.MediaServers)
|
||||
if mediaservers:
|
||||
return [
|
||||
{"name": d.get("name"), "type": d.get("type")}
|
||||
|
||||
@@ -5,7 +5,6 @@ import time
|
||||
from typing import Protocol, Union, Any, List, Optional
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import PlainTextResponse
|
||||
|
||||
from app.schemas.message import MessageClearBefore as _SchemaMessageClearBefore
|
||||
@@ -20,13 +19,12 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.message import MessageChain
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.application.security.access import verify_token, verify_apitoken
|
||||
from app.db import get_async_db
|
||||
from app.db.models import User
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.api.deps import get_current_active_superuser
|
||||
from app.runtime.extensions.service_registry import ServiceConfigHelper
|
||||
from app.adapters.web.security.access import verify_token, verify_apitoken
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import get_current_active_superuser, get_message_query_service
|
||||
from app.application.messaging.message import MessageQueryService
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.runtime.log import logger
|
||||
from app.adapters.external.wechat_crypt import WXBizMsgCrypt
|
||||
from app.schemas.types import NotificationChannel, SystemConfigKey
|
||||
@@ -83,7 +81,7 @@ def _get_notification_clear_before() -> _SchemaMessageClearBefore:
|
||||
"""
|
||||
读取通知中心清理时间配置。
|
||||
"""
|
||||
value = SystemConfigOper().get(SystemConfigKey.NotificationClearBefore)
|
||||
value = get_configured_system_config().get(SystemConfigKey.NotificationClearBefore)
|
||||
if isinstance(value, dict):
|
||||
return _SchemaMessageClearBefore(
|
||||
all=_normalize_notification_clear_timestamp(value.get("all")),
|
||||
@@ -156,7 +154,7 @@ async def user_message(
|
||||
async def web_message(
|
||||
request: Request,
|
||||
text: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_active_superuser),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
):
|
||||
"""
|
||||
WEB消息响应
|
||||
@@ -194,28 +192,20 @@ async def web_message(
|
||||
@router.get("/web", summary="获取WEB消息", response_model=List[_SchemaWebMessageItem])
|
||||
async def get_web_message(
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: MessageQueryService = Depends(get_message_query_service),
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 20,
|
||||
):
|
||||
"""
|
||||
获取WEB消息列表
|
||||
"""
|
||||
ret_messages = []
|
||||
messages = await MessageOper(db).async_list_by_page(page=page, count=count)
|
||||
for message in messages:
|
||||
try:
|
||||
ret_messages.append(message.to_dict())
|
||||
except Exception as e:
|
||||
logger.error(f"获取WEB消息列表失败: {str(e)}")
|
||||
continue
|
||||
return ret_messages
|
||||
return await service.list_web(page=page, count=count)
|
||||
|
||||
|
||||
@router.get("/notification", summary="获取通知消息", response_model=List[_SchemaMessageHistoryItem])
|
||||
async def get_notification_message(
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: MessageQueryService = Depends(get_message_query_service),
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 20,
|
||||
):
|
||||
@@ -223,14 +213,14 @@ async def get_notification_message(
|
||||
获取系统发送的通知消息列表。
|
||||
"""
|
||||
clear_before = _get_notification_clear_before()
|
||||
messages = await MessageOper(db).async_list_sent_by_page(
|
||||
messages = await service.list_notifications(
|
||||
page=page,
|
||||
count=count,
|
||||
all_clear_before=_format_notification_clear_time(clear_before.all),
|
||||
system_clear_before=_format_notification_clear_time(clear_before.system),
|
||||
media_clear_before=_format_notification_clear_time(clear_before.media),
|
||||
)
|
||||
return [_SchemaMessageHistoryItem(**message.to_dict()) for message in messages]
|
||||
return [_SchemaMessageHistoryItem(**message) for message in messages]
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -248,7 +238,7 @@ async def clear_notification_message(
|
||||
clear_before = _get_notification_clear_before()
|
||||
value = clear_before.model_dump()
|
||||
value[scope.value] = int(time.time() * 1000)
|
||||
await SystemConfigOper().async_set(SystemConfigKey.NotificationClearBefore, value)
|
||||
await get_configured_system_config().async_set(SystemConfigKey.NotificationClearBefore, value)
|
||||
return _SchemaResponse(success=True, data={"clear_before": value})
|
||||
|
||||
|
||||
|
||||
+75
-77
@@ -4,12 +4,9 @@ MFA (Multi-Factor Authentication) API 端点
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from typing import Any, Annotated, Optional
|
||||
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from fastapi import Depends, HTTPException, Body, Request, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.mcp import BaseModel as _SchemaBaseModel
|
||||
from app.schemas.mcp import JsonData as _SchemaJsonData
|
||||
@@ -21,13 +18,24 @@ from app.schemas.response import Response as _SchemaResponse
|
||||
from app.schemas.token import Token as _SchemaToken
|
||||
from app.schemas.token import TokenPayload as _SchemaTokenPayload
|
||||
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
|
||||
from app.application.security import access as security
|
||||
from app.runtime.config import settings
|
||||
from app.db import get_async_db
|
||||
from app.db.models.passkey import PassKey
|
||||
from app.db.models.user import User
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.api.deps import get_current_active_user, get_current_active_user_async
|
||||
from app.adapters.web.security.access import set_or_refresh_resource_token_cookie
|
||||
from app.application.security.token import verify_password
|
||||
from app.application.security.auth import get_configured_auth_service
|
||||
from app.application.security.user import UserService
|
||||
from app.application.security.user import (
|
||||
get_configured_user_id_lookup,
|
||||
get_configured_user_name_lookup,
|
||||
)
|
||||
from app.application.security.passkeys import (
|
||||
PasskeyService,
|
||||
)
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.deps import (
|
||||
get_current_active_user,
|
||||
get_current_active_user_async,
|
||||
get_user_service,
|
||||
get_passkey_service,
|
||||
)
|
||||
from app.application.security.passkey import (
|
||||
PassKeyHelper,
|
||||
PassKeyRegistrationOriginMismatchError,
|
||||
@@ -35,7 +43,6 @@ from app.application.security.passkey import (
|
||||
PasskeyChallengeStore,
|
||||
)
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.application.security.otp import OtpUtils
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
@@ -43,7 +50,7 @@ router = ResponseAPIRouter()
|
||||
# ==================== 辅助函数 ====================
|
||||
|
||||
|
||||
def _build_credential_list(passkeys: list[PassKey]) -> list[dict[str, Any]]:
|
||||
def _build_credential_list(passkeys: list[Any]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
构建凭证列表
|
||||
|
||||
@@ -75,7 +82,10 @@ def _extract_and_standardize_credential_id(credential: dict) -> str:
|
||||
|
||||
|
||||
def _verify_passkey_and_update(
|
||||
credential: dict, challenge: str, passkey: PassKey
|
||||
credential: dict,
|
||||
challenge: str,
|
||||
passkey: Any,
|
||||
service: PasskeyService,
|
||||
) -> tuple[bool, int]:
|
||||
"""
|
||||
验证 PassKey 并更新使用时间和签名计数
|
||||
@@ -93,7 +103,7 @@ def _verify_passkey_and_update(
|
||||
)
|
||||
|
||||
if success:
|
||||
passkey.update_last_used(db=None, sign_count=new_sign_count)
|
||||
service.update_last_used(passkey, new_sign_count)
|
||||
|
||||
return success, new_sign_count
|
||||
|
||||
@@ -129,11 +139,14 @@ class PassKeyDeleteRequest(_SchemaBaseModel):
|
||||
summary="判断用户是否开启二次验证",
|
||||
response_model=_SchemaResponse[_SchemaMfaStatusData],
|
||||
)
|
||||
async def mfa_status(username: str, db: AsyncSession = Depends(get_async_db)) -> Any:
|
||||
async def mfa_status(
|
||||
username: str,
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> Any:
|
||||
"""
|
||||
检查指定用户是否启用了二次验证
|
||||
"""
|
||||
user: User = await User.async_get_by_name(db, username)
|
||||
user = await service.get_by_name(username)
|
||||
if not user:
|
||||
return _SchemaResponse(success=False, message="用户不存在")
|
||||
|
||||
@@ -152,7 +165,7 @@ async def mfa_status(username: str, db: AsyncSession = Depends(get_async_db)) ->
|
||||
response_model=_SchemaResponse[_SchemaOtpGenerateData],
|
||||
)
|
||||
def otp_generate(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
|
||||
) -> Any:
|
||||
"""生成 OTP 密钥及对应的 URI"""
|
||||
secret, uri = OtpUtils.generate_secret_key(current_user.name)
|
||||
@@ -162,14 +175,16 @@ def otp_generate(
|
||||
@router.post("/otp/verify", summary="绑定并验证 OTP", response_model=_SchemaResponse[None])
|
||||
async def otp_verify(
|
||||
data: OtpVerifyRequest,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
service: UserService = Depends(get_user_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""验证用户输入的 OTP 码,验证通过后正式开启 OTP 验证"""
|
||||
if not OtpUtils.is_legal(data.uri, data.otpPassword):
|
||||
return _SchemaResponse(success=False, message="验证码错误")
|
||||
await current_user.async_update_otp_by_name(
|
||||
db, current_user.name, True, OtpUtils.get_secret(data.uri)
|
||||
await service.update_otp(
|
||||
current_user.name,
|
||||
True,
|
||||
OtpUtils.get_secret(data.uri),
|
||||
)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
@@ -181,14 +196,14 @@ async def otp_verify(
|
||||
)
|
||||
async def otp_disable(
|
||||
data: OtpDisableRequest,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
service: UserService = Depends(get_user_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""关闭当前用户的 OTP 验证功能"""
|
||||
# 验证密码
|
||||
if not security.verify_password(data.password, str(current_user.hashed_password)):
|
||||
if not verify_password(data.password, str(current_user.hashed_password)):
|
||||
return _SchemaResponse(success=False, message="密码错误")
|
||||
await current_user.async_update_otp_by_name(db, current_user.name, False, "")
|
||||
await service.update_otp(current_user.name, False, "")
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@@ -228,12 +243,13 @@ class PassKeyAuthenticationFinish(_SchemaBaseModel):
|
||||
response_model=_SchemaResponse[_SchemaPasskeyStartData],
|
||||
)
|
||||
def passkey_register_start(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""开始注册 PassKey - 生成注册选项"""
|
||||
try:
|
||||
# 获取用户已有的PassKey
|
||||
existing_passkeys = PassKey.get_by_user_id(db=None, user_id=current_user.id)
|
||||
existing_passkeys = service.list_by_user_id(current_user.id)
|
||||
existing_credentials = (
|
||||
_build_credential_list(existing_passkeys) if existing_passkeys else None
|
||||
)
|
||||
@@ -272,7 +288,8 @@ def passkey_register_start(
|
||||
)
|
||||
def passkey_register_finish(
|
||||
passkey_req: PassKeyRegistrationFinish,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""完成注册 PassKey - 验证并保存凭证"""
|
||||
try:
|
||||
@@ -303,16 +320,15 @@ def passkey_register_finish(
|
||||
transports = ",".join(passkey_req.credential["response"]["transports"])
|
||||
|
||||
# 保存到数据库
|
||||
passkey = PassKey(
|
||||
user_id=current_user.id,
|
||||
credential_id=credential_id,
|
||||
public_key=public_key,
|
||||
sign_count=sign_count,
|
||||
name=passkey_req.name or "通行密钥",
|
||||
aaguid=aaguid,
|
||||
transports=transports,
|
||||
)
|
||||
passkey.create()
|
||||
service.create({
|
||||
"user_id": current_user.id,
|
||||
"credential_id": credential_id,
|
||||
"public_key": public_key,
|
||||
"sign_count": sign_count,
|
||||
"name": passkey_req.name or "通行密钥",
|
||||
"aaguid": aaguid,
|
||||
"transports": transports,
|
||||
})
|
||||
|
||||
logger.info(f"用户 {current_user.name} 成功注册PassKey: {passkey_req.name}")
|
||||
|
||||
@@ -339,6 +355,7 @@ def passkey_register_finish(
|
||||
)
|
||||
def passkey_authenticate_start(
|
||||
passkey_req: PassKeyAuthenticationStart = Body(...),
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""开始 PassKey 认证 - 生成认证选项"""
|
||||
try:
|
||||
@@ -347,9 +364,9 @@ def passkey_authenticate_start(
|
||||
|
||||
# 如果指定了用户名,只允许该用户的PassKey
|
||||
if passkey_req.username:
|
||||
user = User.get_by_name(db=None, name=passkey_req.username)
|
||||
user = get_configured_user_name_lookup()(passkey_req.username)
|
||||
existing_passkeys = (
|
||||
PassKey.get_by_user_id(db=None, user_id=user.id) if user else None
|
||||
service.list_by_user_id(user.id) if user else None
|
||||
)
|
||||
|
||||
if not user or not existing_passkeys:
|
||||
@@ -387,7 +404,10 @@ def passkey_authenticate_start(
|
||||
openapi_extra={RAW_RESPONSE_OPENAPI_KEY: True},
|
||||
)
|
||||
def passkey_authenticate_finish(
|
||||
request: Request, response: Response, passkey_req: PassKeyAuthenticationFinish
|
||||
request: Request,
|
||||
response: Response,
|
||||
passkey_req: PassKeyAuthenticationFinish,
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""完成 PassKey 认证 - 验证凭证并返回 token"""
|
||||
try:
|
||||
@@ -408,8 +428,8 @@ def passkey_authenticate_finish(
|
||||
raise HTTPException(status_code=401, detail="认证失败")
|
||||
|
||||
# 查找PassKey并获取用户
|
||||
passkey = PassKey.get_by_credential_id(db=None, credential_id=credential_id)
|
||||
user = User.get_by_id(db=None, user_id=passkey.user_id) if passkey else None
|
||||
passkey = service.get_by_credential_id(credential_id)
|
||||
user = get_configured_user_id_lookup()(passkey.user_id) if passkey else None
|
||||
if not passkey or not user or not user.is_active:
|
||||
raise HTTPException(status_code=401, detail="认证失败")
|
||||
if challenge_state.user_id is not None and challenge_state.user_id != user.id:
|
||||
@@ -420,6 +440,7 @@ def passkey_authenticate_finish(
|
||||
credential=passkey_req.credential,
|
||||
challenge=challenge_state.challenge,
|
||||
passkey=passkey,
|
||||
service=service,
|
||||
)
|
||||
|
||||
if not success:
|
||||
@@ -428,42 +449,19 @@ def passkey_authenticate_finish(
|
||||
logger.info(f"用户 {user.name} 通过PassKey认证成功")
|
||||
|
||||
# 生成token
|
||||
level = SitesHelper().auth_level
|
||||
show_wizard = (
|
||||
not SystemConfigOper().get(SystemConfigKey.SetupWizardState)
|
||||
and not settings.ADVANCED_MODE
|
||||
)
|
||||
|
||||
access_token = security.create_access_token(
|
||||
userid=user.id,
|
||||
username=user.name,
|
||||
super_user=user.is_superuser,
|
||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
||||
level=level,
|
||||
)
|
||||
security.set_or_refresh_resource_token_cookie(
|
||||
token = get_configured_auth_service().build_token_response(user)
|
||||
set_or_refresh_resource_token_cookie(
|
||||
request,
|
||||
response,
|
||||
_SchemaTokenPayload(
|
||||
sub=user.id,
|
||||
username=user.name,
|
||||
super_user=user.is_superuser,
|
||||
level=level,
|
||||
level=token.level,
|
||||
purpose="authentication",
|
||||
),
|
||||
)
|
||||
|
||||
return _SchemaToken(
|
||||
access_token=access_token,
|
||||
token_type="bearer",
|
||||
super_user=user.is_superuser,
|
||||
user_id=user.id,
|
||||
user_name=user.name,
|
||||
avatar=user.avatar,
|
||||
level=level,
|
||||
permissions=user.permissions or {},
|
||||
wizard=show_wizard,
|
||||
)
|
||||
return token
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -477,11 +475,12 @@ def passkey_authenticate_finish(
|
||||
response_model=_SchemaResponse[list[_SchemaPasskeyInfo]],
|
||||
)
|
||||
def passkey_list(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
current_user: Annotated[ApiPrincipal, Depends(get_current_active_user)],
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""获取当前用户的所有 PassKey"""
|
||||
try:
|
||||
passkeys = PassKey.get_by_user_id(db=None, user_id=current_user.id)
|
||||
passkeys = service.list_by_user_id(current_user.id)
|
||||
|
||||
key_list = (
|
||||
[
|
||||
@@ -514,19 +513,18 @@ def passkey_list(
|
||||
)
|
||||
async def passkey_delete(
|
||||
data: PassKeyDeleteRequest,
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
service: PasskeyService = Depends(get_passkey_service),
|
||||
) -> Any:
|
||||
"""删除指定的 PassKey"""
|
||||
try:
|
||||
# 验证密码
|
||||
if not security.verify_password(
|
||||
if not verify_password(
|
||||
data.password, str(current_user.hashed_password)
|
||||
):
|
||||
return _SchemaResponse(success=False, message="密码错误")
|
||||
|
||||
success = PassKey.delete_by_id(
|
||||
db=None, passkey_id=data.passkey_id, user_id=current_user.id
|
||||
)
|
||||
success = service.delete_by_id(data.passkey_id, current_user.id)
|
||||
|
||||
if success:
|
||||
logger.info(f"用户 {current_user.name} 删除了PassKey: {data.passkey_id}")
|
||||
|
||||
@@ -14,8 +14,7 @@ from app.chain.media import MediaChain
|
||||
from app.chain.recommend import RecommendChain
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
|
||||
from app.application.security.access import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.api.deps import get_current_active_superuser_async
|
||||
from app.chain.listenbrainz import (
|
||||
LISTENBRAINZ_CHART_RANGES,
|
||||
@@ -114,7 +113,7 @@ async def recognize_music(
|
||||
response_model=_SchemaResponse[_SchemaMusicRecognitionCacheData],
|
||||
)
|
||||
async def music_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""查询可管理的 MusicBrainz 识别缓存。"""
|
||||
cache_items = MusicBrainzChain().cache_items()
|
||||
@@ -137,7 +136,7 @@ async def music_recognition_cache(
|
||||
)
|
||||
async def delete_music_recognition_cache(
|
||||
cache_key: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""按缓存键删除单条 MusicBrainz 识别缓存。"""
|
||||
deleted_item = MusicBrainzChain().delete_cache(cache_key)
|
||||
@@ -150,7 +149,7 @@ async def delete_music_recognition_cache(
|
||||
"/cache", summary="清空音乐识别缓存", response_model=_SchemaResponse[None]
|
||||
)
|
||||
async def clear_music_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""清空全部 MusicBrainz 识别缓存。"""
|
||||
MusicBrainzChain().clear_cache()
|
||||
|
||||
@@ -6,7 +6,6 @@ from app.schemas.common import ManageRequest as _SchemaManageRequest
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.notification import NotificationChain
|
||||
from app.db.models import User
|
||||
from app.api.deps import get_current_active_superuser
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
@@ -19,7 +18,7 @@ router = ResponseAPIRouter()
|
||||
)
|
||||
def manage_channel(
|
||||
request: _SchemaManageRequest,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: object = Depends(get_current_active_superuser),
|
||||
):
|
||||
"""
|
||||
通知渠道统一管理入口
|
||||
|
||||
@@ -32,7 +32,7 @@ from app.agent.runtime_loader import (
|
||||
)
|
||||
from app.agent.contracts import ReplyMode
|
||||
from app.runtime.config import settings
|
||||
from app.application.security.access import openai_bearer_scheme
|
||||
from app.adapters.web.security.access import openai_bearer_scheme
|
||||
from app.schemas.types import NotificationChannel
|
||||
|
||||
OPENAI_ERROR_RESPONSES = {
|
||||
|
||||
+52
-43
@@ -35,14 +35,18 @@ from app.application.commands import init_commands
|
||||
from app.application.scheduling import remove_plugin_job, update_plugin_job
|
||||
from app.runtime.cache import async_fresh
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.security.access import (
|
||||
from app.application.plugin.runtime import get_plugin_manager as PluginManager
|
||||
from app.runtime.extensions.plugin.contracts import (
|
||||
PluginDashboardError,
|
||||
PluginNotFoundError,
|
||||
)
|
||||
from app.adapters.web.security.access import (
|
||||
resource_token_cookie,
|
||||
verify_resource_token,
|
||||
verify_token,
|
||||
)
|
||||
from app.db.models import User
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import (
|
||||
get_current_active_superuser,
|
||||
get_current_active_superuser_async,
|
||||
@@ -240,7 +244,7 @@ async def _get_plugin_history_detail(
|
||||
|
||||
@router.get("/", summary="所有插件", response_model=List[_SchemaPlugin])
|
||||
async def all_plugins(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
state: Optional[str] = "all",
|
||||
force: bool = False,
|
||||
) -> List[_SchemaPlugin]:
|
||||
@@ -298,17 +302,17 @@ async def all_plugins(
|
||||
|
||||
|
||||
@router.get("/installed", summary="已安装插件", response_model=List[str])
|
||||
async def installed(_: User = Depends(get_current_active_superuser_async)) -> Any:
|
||||
async def installed(_: ApiPrincipal = Depends(get_current_active_superuser_async)) -> Any:
|
||||
"""
|
||||
查询用户已安装插件清单
|
||||
"""
|
||||
return SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
return get_configured_system_config().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
|
||||
|
||||
@router.get("/history/{plugin_id}", summary="获取插件更新说明", response_model=_SchemaPlugin)
|
||||
async def plugin_history(
|
||||
plugin_id: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
force: bool = True,
|
||||
) -> _SchemaPlugin:
|
||||
"""
|
||||
@@ -330,7 +334,7 @@ async def plugin_history(
|
||||
)
|
||||
async def plugin_releases(
|
||||
plugin_id: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
repo_url: Optional[str] = "",
|
||||
force: bool = False,
|
||||
) -> dict:
|
||||
@@ -405,7 +409,7 @@ async def statistic(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
)
|
||||
async def plugin_ratings(
|
||||
plugin_ids: Optional[str] = None,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Dict[str, _SchemaPluginRating]:
|
||||
"""
|
||||
批量查询插件平均分、评分人数和当前安装实例评分。
|
||||
@@ -425,7 +429,7 @@ async def plugin_ratings(
|
||||
)
|
||||
async def plugin_rating(
|
||||
plugin_id: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaPluginRating:
|
||||
"""
|
||||
查询单个插件平均分、评分人数和当前安装实例评分。
|
||||
@@ -442,12 +446,12 @@ async def plugin_rating(
|
||||
async def rate_plugin(
|
||||
plugin_id: str,
|
||||
payload: _SchemaPluginRatingRequest,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
为已安装插件新增或更新当前安装实例评分。
|
||||
"""
|
||||
installed_plugins = SystemConfigOper().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
installed_plugins = get_configured_system_config().get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
if plugin_id not in installed_plugins:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -467,7 +471,7 @@ async def rate_plugin(
|
||||
"/reload/{plugin_id}", summary="重新加载插件", response_model=_SchemaResponse[None]
|
||||
)
|
||||
def reload_plugin(
|
||||
plugin_id: str, _: User = Depends(get_current_active_superuser)
|
||||
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
重新加载插件
|
||||
@@ -485,7 +489,7 @@ async def install(
|
||||
repo_url: Optional[str] = "",
|
||||
release_version: Optional[str] = None,
|
||||
force: Optional[bool] = False,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
安装插件
|
||||
@@ -495,7 +499,7 @@ async def install(
|
||||
|
||||
async def save_installed_plugins(plugin_ids: List[str]) -> object:
|
||||
"""保存安装用例确认后的插件列表。"""
|
||||
return await SystemConfigOper().async_set(
|
||||
return await get_configured_system_config().async_set(
|
||||
SystemConfigKey.UserInstalledPlugins,
|
||||
plugin_ids,
|
||||
)
|
||||
@@ -523,7 +527,7 @@ async def install(
|
||||
return await run_in_threadpool(register_plugin, target_id)
|
||||
|
||||
command = PluginInstallCommand(
|
||||
installed_plugins_reader=lambda: SystemConfigOper().get(
|
||||
installed_plugins_reader=lambda: get_configured_system_config().get(
|
||||
SystemConfigKey.UserInstalledPlugins
|
||||
) or [],
|
||||
installed_plugins_writer=save_installed_plugins,
|
||||
@@ -585,7 +589,7 @@ def plugin_sidebar_nav(_: _SchemaTokenPayload = Depends(verify_token)) -> Any:
|
||||
response_model=_SchemaJsonObject,
|
||||
)
|
||||
def plugin_form(
|
||||
plugin_id: str, _: User = Depends(get_current_active_superuser)
|
||||
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> dict:
|
||||
"""
|
||||
根据插件ID获取插件配置表单或Vue组件URL
|
||||
@@ -621,7 +625,7 @@ def plugin_form(
|
||||
response_model=_SchemaJsonObject,
|
||||
)
|
||||
def plugin_page(
|
||||
plugin_id: str, _: User = Depends(get_current_active_superuser)
|
||||
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> dict:
|
||||
"""
|
||||
根据插件ID获取插件数据页面
|
||||
@@ -649,7 +653,7 @@ def plugin_page(
|
||||
response_model=List[_SchemaPluginDashboardMetaItem],
|
||||
)
|
||||
def plugin_dashboard_meta(
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
) -> List[dict]:
|
||||
"""
|
||||
获取所有插件仪表板元信息
|
||||
@@ -662,19 +666,24 @@ def plugin_dashboard_by_key(
|
||||
plugin_id: str,
|
||||
key: str,
|
||||
user_agent: Annotated[str | None, Header()] = None,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
) -> Optional[_SchemaPluginDashboard]:
|
||||
"""
|
||||
根据插件ID获取插件仪表板
|
||||
"""
|
||||
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
|
||||
try:
|
||||
return PluginManager().get_plugin_dashboard(plugin_id, key, user_agent)
|
||||
except PluginNotFoundError as error:
|
||||
raise HTTPException(status_code=404, detail=str(error)) from error
|
||||
except PluginDashboardError as error:
|
||||
raise HTTPException(status_code=500, detail=str(error)) from error
|
||||
|
||||
|
||||
@router.get("/dashboard/{plugin_id}", summary="获取插件仪表板配置")
|
||||
def plugin_dashboard(
|
||||
plugin_id: str,
|
||||
user_agent: Annotated[str | None, Header()] = None,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
) -> Optional[_SchemaPluginDashboard]:
|
||||
"""
|
||||
根据插件ID获取插件仪表板
|
||||
@@ -687,7 +696,7 @@ def plugin_dashboard(
|
||||
)
|
||||
def reset_plugin(
|
||||
plugin_id: str,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
command: PluginConfigCommand = Depends(get_plugin_config_command),
|
||||
) -> Any:
|
||||
"""
|
||||
@@ -798,13 +807,13 @@ async def plugin_static_file(
|
||||
response_model=_SchemaPluginFoldersData,
|
||||
)
|
||||
async def get_plugin_folders(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> dict:
|
||||
"""
|
||||
获取插件文件夹分组配置
|
||||
"""
|
||||
try:
|
||||
result = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
|
||||
result = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"[文件夹API] 获取文件夹配置失败: {str(e)}")
|
||||
@@ -813,13 +822,13 @@ async def get_plugin_folders(
|
||||
|
||||
@router.post("/folders", summary="保存插件文件夹配置", response_model=_SchemaResponse[None])
|
||||
async def save_plugin_folders(
|
||||
folders: dict, _: User = Depends(get_current_active_superuser_async)
|
||||
folders: dict, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
) -> Any:
|
||||
"""
|
||||
保存插件文件夹分组配置
|
||||
"""
|
||||
try:
|
||||
SystemConfigOper().set(SystemConfigKey.PluginFolders, folders)
|
||||
get_configured_system_config().set(SystemConfigKey.PluginFolders, folders)
|
||||
return _SchemaResponse(success=True)
|
||||
except Exception as e:
|
||||
logger.error(f"[文件夹API] 保存文件夹配置失败: {str(e)}")
|
||||
@@ -830,15 +839,15 @@ async def save_plugin_folders(
|
||||
"/folders/{folder_name}", summary="创建插件文件夹", response_model=_SchemaResponse[None]
|
||||
)
|
||||
async def create_plugin_folder(
|
||||
folder_name: str, _: User = Depends(get_current_active_superuser_async)
|
||||
folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
) -> Any:
|
||||
"""
|
||||
创建新的插件文件夹
|
||||
"""
|
||||
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
|
||||
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
|
||||
if folder_name not in folders:
|
||||
folders[folder_name] = []
|
||||
SystemConfigOper().set(SystemConfigKey.PluginFolders, folders)
|
||||
get_configured_system_config().set(SystemConfigKey.PluginFolders, folders)
|
||||
return _SchemaResponse(
|
||||
success=True, message=f"文件夹 '{folder_name}' 创建成功"
|
||||
)
|
||||
@@ -850,15 +859,15 @@ async def create_plugin_folder(
|
||||
"/folders/{folder_name}", summary="删除插件文件夹", response_model=_SchemaResponse[None]
|
||||
)
|
||||
async def delete_plugin_folder(
|
||||
folder_name: str, _: User = Depends(get_current_active_superuser_async)
|
||||
folder_name: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
) -> Any:
|
||||
"""
|
||||
删除插件文件夹
|
||||
"""
|
||||
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
|
||||
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
|
||||
if folder_name in folders:
|
||||
del folders[folder_name]
|
||||
await SystemConfigOper().async_set(SystemConfigKey.PluginFolders, folders)
|
||||
await get_configured_system_config().async_set(SystemConfigKey.PluginFolders, folders)
|
||||
return _SchemaResponse(
|
||||
success=True, message=f"文件夹 '{folder_name}' 删除成功"
|
||||
)
|
||||
@@ -874,14 +883,14 @@ async def delete_plugin_folder(
|
||||
async def update_folder_plugins(
|
||||
folder_name: str,
|
||||
plugin_ids: List[str],
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
更新指定文件夹中的插件列表
|
||||
"""
|
||||
folders = SystemConfigOper().get(SystemConfigKey.PluginFolders) or {}
|
||||
folders = get_configured_system_config().get(SystemConfigKey.PluginFolders) or {}
|
||||
folders[folder_name] = plugin_ids
|
||||
await SystemConfigOper().async_set(SystemConfigKey.PluginFolders, folders)
|
||||
await get_configured_system_config().async_set(SystemConfigKey.PluginFolders, folders)
|
||||
return _SchemaResponse(
|
||||
success=True, message=f"文件夹 '{folder_name}' 中的插件已更新"
|
||||
)
|
||||
@@ -891,7 +900,7 @@ async def update_folder_plugins(
|
||||
"/clone/{plugin_id}", summary="创建插件分身", response_model=_SchemaResponse[None]
|
||||
)
|
||||
def clone_plugin(
|
||||
plugin_id: str, clone_data: dict, _: User = Depends(get_current_active_superuser)
|
||||
plugin_id: str, clone_data: dict, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
创建插件分身
|
||||
@@ -925,7 +934,7 @@ def clone_plugin(
|
||||
response_model=_SchemaJsonObject,
|
||||
)
|
||||
async def plugin_config(
|
||||
plugin_id: str, _: User = Depends(get_current_active_superuser_async)
|
||||
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
) -> dict:
|
||||
"""
|
||||
根据插件ID获取插件配置信息
|
||||
@@ -937,7 +946,7 @@ async def plugin_config(
|
||||
def set_plugin_config(
|
||||
plugin_id: str,
|
||||
conf: dict,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
command: PluginConfigCommand = Depends(get_plugin_config_command),
|
||||
) -> Any:
|
||||
"""
|
||||
@@ -949,12 +958,12 @@ def set_plugin_config(
|
||||
|
||||
@router.delete("/{plugin_id}", summary="卸载插件", response_model=_SchemaResponse[None])
|
||||
def uninstall_plugin(
|
||||
plugin_id: str, _: User = Depends(get_current_active_superuser)
|
||||
plugin_id: str, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
卸载插件
|
||||
"""
|
||||
config_oper = SystemConfigOper()
|
||||
config_oper = get_configured_system_config()
|
||||
# 删除已安装信息
|
||||
install_plugins = config_oper.get(SystemConfigKey.UserInstalledPlugins) or []
|
||||
for plugin in install_plugins:
|
||||
@@ -995,7 +1004,7 @@ def _add_clone_to_plugin_folder(original_plugin_id: str, clone_plugin_id: str):
|
||||
:param clone_plugin_id: 分身插件ID
|
||||
"""
|
||||
try:
|
||||
config_oper = SystemConfigOper()
|
||||
config_oper = get_configured_system_config()
|
||||
# 获取插件文件夹配置
|
||||
folders = config_oper.get(SystemConfigKey.PluginFolders) or {}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.recommend import RecommendChain
|
||||
from app.runtime.events import eventmanager
|
||||
from app.application.security.access import verify_token
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.schemas.exception import TMDbException
|
||||
from app.schemas.event import RecommendSourceEventData
|
||||
from app.schemas.types import ChainEventType
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
|
||||
from app.schemas.workflow import Context as _SchemaContext
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.search import SearchChain
|
||||
from app.application.security.access import verify_resource_token, verify_token
|
||||
from app.adapters.web.security.access import verify_resource_token, verify_token
|
||||
from app.runtime.localization import LocaleHelper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
|
||||
+80
-93
@@ -1,8 +1,6 @@
|
||||
from typing import List, Any, Dict, Optional
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from app.schemas.common import JsonObject as _SchemaJsonObject
|
||||
@@ -19,32 +17,28 @@ from app.schemas.token import TokenPayload as _SchemaTokenPayload
|
||||
from app.schemas.workflow import Site as _SchemaSite
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.site.mutation import SiteMutationCommand
|
||||
from app.application.site.query import SiteQueryService
|
||||
from app.api.endpoints.plugin import register_plugin_api
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.command import Command
|
||||
from app.runtime.events import eventmanager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.security.access import verify_token
|
||||
from app.db import get_db, get_async_db
|
||||
from app.db.models import User
|
||||
from app.db.models.site import Site
|
||||
from app.db.models.siteicon import SiteIcon
|
||||
from app.db.models.sitestatistic import SiteStatistic
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.plugin.runtime import get_plugin_manager as PluginManager
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import (
|
||||
get_current_active_manage_user,
|
||||
get_current_active_manage_user_async,
|
||||
get_current_active_superuser,
|
||||
get_current_active_superuser_async,
|
||||
get_site_mutation_command,
|
||||
get_site_query_service,
|
||||
get_site_sync_query_service,
|
||||
)
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
|
||||
from app.runtime.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, EventType, MediaType
|
||||
from app.application.scheduling import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, MediaType
|
||||
from app.domain import site as site_rules
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
@@ -88,13 +82,13 @@ def _indexer_supports_media_type(indexer: dict, media_type: MediaType) -> bool:
|
||||
|
||||
@router.get("/", summary="所有站点", response_model=List[_SchemaSite])
|
||||
async def read_sites(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> List[dict]:
|
||||
"""
|
||||
获取站点列表
|
||||
"""
|
||||
return await Site.async_list_order_by_pri(db)
|
||||
return await query.list_ordered()
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -104,14 +98,14 @@ async def read_sites(
|
||||
)
|
||||
async def read_sites_by_media_type(
|
||||
media_type: str,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
) -> List[Site]:
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> List[_SchemaSite]:
|
||||
"""
|
||||
获取支持指定媒体类型的已配置启用站点。
|
||||
|
||||
:param media_type: Agent 媒体类型名称或中文媒体类型
|
||||
:param db: 异步数据库会话
|
||||
:param query: 站点查询服务
|
||||
:return: 按优先级排序的可搜索站点
|
||||
"""
|
||||
target_media_type = MediaType.from_agent(media_type)
|
||||
@@ -134,7 +128,7 @@ async def read_sites_by_media_type(
|
||||
if domain:
|
||||
supported_domains.add(domain)
|
||||
|
||||
sites = await Site.async_list_order_by_pri(db)
|
||||
sites = await query.list_ordered()
|
||||
return [
|
||||
site
|
||||
for site in sites
|
||||
@@ -148,7 +142,7 @@ async def add_site(
|
||||
*,
|
||||
site_in: _SchemaSite,
|
||||
command: SiteMutationCommand = Depends(get_site_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
新增站点
|
||||
@@ -162,7 +156,7 @@ async def update_site(
|
||||
*,
|
||||
site_in: _SchemaSite,
|
||||
command: SiteMutationCommand = Depends(get_site_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
更新站点信息
|
||||
@@ -174,7 +168,7 @@ async def update_site(
|
||||
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=_SchemaResponse[None])
|
||||
async def cookie_cloud_sync(
|
||||
background_tasks: BackgroundTasks,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
运行CookieCloud同步站点信息
|
||||
@@ -184,20 +178,20 @@ async def cookie_cloud_sync(
|
||||
|
||||
|
||||
@router.get("/reset", summary="重置站点", response_model=_SchemaResponse[None])
|
||||
def reset(
|
||||
db: AsyncSession = Depends(get_db), _: User = Depends(get_current_active_superuser)
|
||||
async def reset(
|
||||
command: SiteMutationCommand = Depends(get_site_mutation_command),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
清空所有站点数据并重新同步CookieCloud站点信息
|
||||
"""
|
||||
Site.reset(db)
|
||||
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
|
||||
SystemConfigOper().set(SystemConfigKey.RssSites, [])
|
||||
result = await command.reset()
|
||||
get_configured_system_config().set(SystemConfigKey.IndexerSites, [])
|
||||
get_configured_system_config().set(SystemConfigKey.RssSites, [])
|
||||
# 启动定时服务
|
||||
Scheduler().start("cookiecloud", manual=True)
|
||||
# 插件站点删除
|
||||
eventmanager.send_event(EventType.SiteDeleted, {"site_id": "*"})
|
||||
return _SchemaResponse(success=True, message="站点已重置!")
|
||||
return _SchemaResponse(success=result.success, message="站点已重置!")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -206,7 +200,7 @@ def reset(
|
||||
async def update_sites_priority(
|
||||
priorities: List[dict],
|
||||
command: SiteMutationCommand = Depends(get_site_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
批量更新站点优先级
|
||||
@@ -220,7 +214,7 @@ def _update_site_cookie(
|
||||
username: str,
|
||||
password: str,
|
||||
code: Optional[str],
|
||||
db: Session,
|
||||
query: SiteQueryService,
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
执行站点 Cookie 与 UA 更新。
|
||||
@@ -229,10 +223,10 @@ def _update_site_cookie(
|
||||
:param username: 站点登录用户名
|
||||
:param password: 站点登录密码
|
||||
:param code: 二步验证码或密钥
|
||||
:param db: 数据库会话
|
||||
:param query: 站点查询服务
|
||||
:return: 更新结果
|
||||
"""
|
||||
site_info = Site.get(db, site_id)
|
||||
site_info = query.get_sync(site_id)
|
||||
if not site_info:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -255,8 +249,8 @@ def _update_site_cookie(
|
||||
def update_cookie_by_body(
|
||||
site_id: int,
|
||||
site_cookie_update: _SchemaSiteCookieUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
使用请求体中的用户密码更新站点Cookie
|
||||
@@ -266,7 +260,7 @@ def update_cookie_by_body(
|
||||
username=site_cookie_update.username,
|
||||
password=site_cookie_update.password,
|
||||
code=site_cookie_update.code,
|
||||
db=db,
|
||||
query=query,
|
||||
)
|
||||
|
||||
|
||||
@@ -278,8 +272,8 @@ def update_cookie(
|
||||
username: str,
|
||||
password: str,
|
||||
code: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
使用用户密码更新站点Cookie
|
||||
@@ -289,7 +283,7 @@ def update_cookie(
|
||||
username=username,
|
||||
password=password,
|
||||
code=code,
|
||||
db=db,
|
||||
query=query,
|
||||
)
|
||||
|
||||
|
||||
@@ -300,13 +294,13 @@ def update_cookie(
|
||||
)
|
||||
def refresh_userdata(
|
||||
site_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
刷新站点用户数据
|
||||
"""
|
||||
site = Site.get(db, site_id)
|
||||
site = query.get_sync(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -327,16 +321,13 @@ def refresh_userdata(
|
||||
response_model=List[_SchemaSiteUserData],
|
||||
)
|
||||
async def read_userdata_latest(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询所有站点最新用户数据
|
||||
"""
|
||||
user_datas = await SiteUserData.async_get_latest(db)
|
||||
if not user_datas:
|
||||
return []
|
||||
return [user_data.to_dict() for user_data in user_datas]
|
||||
return await query.userdata_latest()
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -347,36 +338,34 @@ async def read_userdata_latest(
|
||||
async def read_userdata(
|
||||
site_id: int,
|
||||
workdate: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询站点用户数据
|
||||
"""
|
||||
site = await Site.async_get(db, site_id)
|
||||
site = await query.get(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"站点 {site_id} 不存在",
|
||||
)
|
||||
user_datas = await SiteUserData.async_get_by_domain(
|
||||
db, domain=site.domain, workdate=workdate
|
||||
)
|
||||
user_datas = await query.userdata(site.domain, workdate)
|
||||
if not user_datas:
|
||||
return _SchemaResponse(success=False, data=[])
|
||||
return _SchemaResponse(success=True, data=[data.to_dict() for data in user_datas])
|
||||
return _SchemaResponse(success=True, data=user_datas)
|
||||
|
||||
|
||||
@router.get("/test/{site_id}", summary="连接测试", response_model=_SchemaResponse[None])
|
||||
def test_site(
|
||||
site_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
测试站点是否可用
|
||||
"""
|
||||
site = Site.get(db, site_id)
|
||||
site = query.get_sync(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -393,24 +382,22 @@ def test_site(
|
||||
)
|
||||
async def site_icon(
|
||||
site_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
获取站点图标:base64或者url
|
||||
"""
|
||||
site = await Site.async_get(db, site_id)
|
||||
site = await query.get(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"站点 {site_id} 不存在",
|
||||
)
|
||||
icon = await SiteIcon.async_get_by_domain(db, site.domain)
|
||||
icon = await query.icon(site.domain)
|
||||
if not icon:
|
||||
return _SchemaResponse(success=False, message="站点图标不存在!")
|
||||
return _SchemaResponse(
|
||||
success=True, data={"icon": icon.base64 if icon.base64 else icon.url}
|
||||
)
|
||||
return _SchemaResponse(success=True, data=icon.model_dump())
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -418,13 +405,13 @@ async def site_icon(
|
||||
)
|
||||
async def site_category(
|
||||
site_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
获取站点分类
|
||||
"""
|
||||
site = await Site.async_get(db, site_id)
|
||||
site = await query.get(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -456,13 +443,13 @@ async def site_resource(
|
||||
mtype: Optional[str] = None,
|
||||
cat: Optional[str] = None,
|
||||
page: Optional[int] = 0,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
浏览站点资源
|
||||
"""
|
||||
site = await Site.async_get(db, site_id)
|
||||
site = await query.get(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -483,14 +470,14 @@ async def site_resource(
|
||||
@router.get("/domain/{site_url}", summary="站点详情", response_model=_SchemaSite)
|
||||
async def read_site_by_domain(
|
||||
site_url: str,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
通过域名获取站点信息
|
||||
"""
|
||||
domain = site_rules.extract_domain(site_url)
|
||||
site = await Site.async_get_by_domain(db, domain)
|
||||
site = await query.get_by_domain(domain)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -506,45 +493,42 @@ async def read_site_by_domain(
|
||||
)
|
||||
async def read_statistic_by_domain(
|
||||
site_url: str,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
通过域名获取站点统计信息
|
||||
"""
|
||||
domain = site_rules.extract_domain(site_url)
|
||||
sitestatistic = await SiteStatistic.async_get_by_domain(db, domain)
|
||||
if sitestatistic:
|
||||
return sitestatistic
|
||||
return _SchemaSiteStatistic(domain=domain)
|
||||
return await query.statistic(domain)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/statistic", summary="所有站点统计信息", response_model=List[_SchemaSiteStatistic]
|
||||
)
|
||||
async def read_statistics(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""
|
||||
获取所有站点统计信息
|
||||
"""
|
||||
return await SiteStatistic.async_list(db)
|
||||
return await query.statistics()
|
||||
|
||||
|
||||
@router.get("/rss", summary="所有订阅站点", response_model=List[_SchemaSite])
|
||||
async def read_rss_sites(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: _SchemaTokenPayload = Depends(verify_token),
|
||||
) -> List[dict]:
|
||||
"""
|
||||
获取站点列表
|
||||
"""
|
||||
# 选中的rss站点
|
||||
selected_sites = SystemConfigOper().get(SystemConfigKey.RssSites) or []
|
||||
selected_sites = get_configured_system_config().get(SystemConfigKey.RssSites) or []
|
||||
|
||||
# 所有站点
|
||||
all_site = await Site.async_list_order_by_pri(db)
|
||||
all_site = await query.list_ordered()
|
||||
if not selected_sites:
|
||||
return all_site
|
||||
|
||||
@@ -563,7 +547,7 @@ async def read_auth_sites(_: _SchemaTokenPayload = Depends(verify_token)) -> dic
|
||||
|
||||
@router.post("/auth", summary="用户站点认证", response_model=_SchemaResponse[None])
|
||||
def auth_site(
|
||||
auth_info: _SchemaSiteAuth, _: User = Depends(get_current_active_superuser)
|
||||
auth_info: _SchemaSiteAuth, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
用户站点认证
|
||||
@@ -571,7 +555,7 @@ def auth_site(
|
||||
if not auth_info or not auth_info.site or not auth_info.params:
|
||||
return _SchemaResponse(success=False, message="请输入认证站点和认证参数")
|
||||
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
|
||||
SystemConfigOper().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
|
||||
get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
|
||||
# 认证成功后,重新初始化插件
|
||||
PluginManager().init_config()
|
||||
Scheduler().init_plugin_jobs()
|
||||
@@ -585,12 +569,15 @@ def auth_site(
|
||||
summary="获取站点域名到名称的映射",
|
||||
response_model=_SchemaResponse[_SchemaSiteMappingData],
|
||||
)
|
||||
async def site_mapping(_: User = Depends(get_current_active_superuser_async)):
|
||||
async def site_mapping(
|
||||
query: SiteQueryService = Depends(get_site_sync_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""
|
||||
获取站点域名到名称的映射关系
|
||||
"""
|
||||
try:
|
||||
sites = await SiteOper().async_list()
|
||||
sites = query.list_sync()
|
||||
mapping = {}
|
||||
for site in sites:
|
||||
mapping[site.domain] = site.name
|
||||
@@ -604,7 +591,7 @@ async def site_mapping(_: User = Depends(get_current_active_superuser_async)):
|
||||
summary="获取支持的站点列表",
|
||||
response_model=_SchemaJsonObject,
|
||||
)
|
||||
async def support_sites(_: User = Depends(get_current_active_superuser_async)):
|
||||
async def support_sites(_: ApiPrincipal = Depends(get_current_active_superuser_async)):
|
||||
"""
|
||||
获取支持的站点列表
|
||||
"""
|
||||
@@ -614,13 +601,13 @@ async def support_sites(_: User = Depends(get_current_active_superuser_async)):
|
||||
@router.get("/{site_id}", summary="站点详情", response_model=_SchemaSite)
|
||||
async def read_site(
|
||||
site_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: SiteQueryService = Depends(get_site_query_service),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
通过ID获取站点信息
|
||||
"""
|
||||
site = await Site.async_get(db, site_id)
|
||||
site = await query.get(site_id)
|
||||
if not site:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
@@ -633,7 +620,7 @@ async def read_site(
|
||||
async def delete_site(
|
||||
site_id: int,
|
||||
command: SiteMutationCommand = Depends(get_site_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
删除站点
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.chain.media import MediaChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.runtime.config import settings
|
||||
from app.db.models import User
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.api.deps import (
|
||||
get_current_active_manage_user,
|
||||
get_current_active_superuser,
|
||||
@@ -31,7 +31,7 @@ router = ResponseAPIRouter()
|
||||
"/manage", summary="网盘存储统一管理", response_model=_SchemaResponse[Dict[str, Any]]
|
||||
)
|
||||
def manage(
|
||||
request: _SchemaManageRequest, _: User = Depends(get_current_active_superuser)
|
||||
request: _SchemaManageRequest, _: ApiPrincipal = Depends(get_current_active_superuser)
|
||||
) -> Any:
|
||||
"""
|
||||
网盘存储统一管理入口
|
||||
@@ -56,7 +56,7 @@ def list_files(
|
||||
fileitem: _SchemaFileItem,
|
||||
sort: Optional[str] = "updated_at",
|
||||
keyword: Optional[str] = None,
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
查询当前目录下所有目录和文件
|
||||
@@ -82,7 +82,7 @@ def list_files(
|
||||
def mkdir(
|
||||
fileitem: _SchemaFileItem,
|
||||
name: str,
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
创建目录
|
||||
@@ -100,7 +100,7 @@ def mkdir(
|
||||
|
||||
@router.post("/delete", summary="删除文件或目录", response_model=_SchemaResponse[None])
|
||||
def delete(
|
||||
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
|
||||
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
|
||||
) -> Any:
|
||||
"""
|
||||
删除文件或目录
|
||||
@@ -131,7 +131,7 @@ def delete(
|
||||
},
|
||||
)
|
||||
def download(
|
||||
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
|
||||
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
|
||||
) -> Any:
|
||||
"""
|
||||
下载文件或目录
|
||||
@@ -160,7 +160,7 @@ def download(
|
||||
},
|
||||
)
|
||||
def image(
|
||||
fileitem: _SchemaFileItem, _: User = Depends(get_current_active_manage_user)
|
||||
fileitem: _SchemaFileItem, _: ApiPrincipal = Depends(get_current_active_manage_user)
|
||||
) -> Any:
|
||||
"""
|
||||
下载文件或目录
|
||||
@@ -179,7 +179,7 @@ def rename(
|
||||
fileitem: _SchemaFileItem,
|
||||
new_name: str,
|
||||
recursive: Optional[bool] = False,
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: ApiPrincipal = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
重命名文件或目录
|
||||
|
||||
+125
-178
@@ -2,8 +2,6 @@ from typing import List, Any, Annotated, Optional
|
||||
|
||||
import cn2an
|
||||
from fastapi import Request, BackgroundTasks, Depends, HTTPException, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.common import IdData as _SchemaIdData
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
@@ -19,7 +17,7 @@ from app.runtime.config import settings
|
||||
from app.domain.context import MediaInfo
|
||||
from app.runtime.events import eventmanager
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.application.security.access import verify_token, verify_apitoken
|
||||
from app.adapters.web.security.access import verify_token, verify_apitoken
|
||||
from app.application.subscription.delete import (
|
||||
DeleteSubscribeCommand,
|
||||
SubscribeDeletionActor,
|
||||
@@ -31,20 +29,25 @@ from app.application.subscription.search import (
|
||||
SearchSubscriptionsCommand,
|
||||
SubscribeSearchActor,
|
||||
)
|
||||
from app.db import get_async_db, get_db
|
||||
from app.db.models.subscribe import Subscribe
|
||||
from app.db.models.subscribehistory import SubscribeHistory
|
||||
from app.db.models.user import User
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.subscription.query import SubscriptionQueryService
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionActor,
|
||||
SubscriptionMutationService,
|
||||
)
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import (
|
||||
get_current_active_user,
|
||||
get_current_active_user_async,
|
||||
get_delete_subscribe_command,
|
||||
get_delete_subscriptions_by_identity_command,
|
||||
get_search_subscriptions_command,
|
||||
get_subscription_query_service,
|
||||
get_subscription_mutation_service,
|
||||
get_subscription_sync_mutation_service,
|
||||
)
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.application.scheduling import Scheduler
|
||||
from app.schemas.event import SubscribeModifiedEventData
|
||||
from app.schemas.types import (
|
||||
MUSIC_ENTITY_ALBUM,
|
||||
@@ -82,16 +85,15 @@ def start_subscribe_add(
|
||||
)
|
||||
|
||||
|
||||
def build_subscribe_event_payload(subscribe: Subscribe) -> dict:
|
||||
def build_subscribe_event_payload(subscribe: Any) -> dict:
|
||||
"""
|
||||
从 ORM 已加载字段构造订阅事件快照,避免异步接口里属性懒加载触发隐式 IO。
|
||||
"""
|
||||
values = subscribe.__dict__
|
||||
return {column.name: values.get(column.name) for column in subscribe.__table__.columns}
|
||||
return subscribe.to_dict()
|
||||
|
||||
|
||||
def can_access_subscribe(
|
||||
subscribe: Subscribe | SubscribeHistory | None, current_user: User
|
||||
subscribe: Any, current_user: ApiPrincipal
|
||||
) -> bool:
|
||||
"""
|
||||
判断当前用户是否可访问订阅及其历史记录。
|
||||
@@ -107,33 +109,9 @@ def can_access_subscribe(
|
||||
return bool(username) and username == current_user.name
|
||||
|
||||
|
||||
async def get_accessible_subscribe(
|
||||
db: AsyncSession, subscribe_id: int, current_user: User
|
||||
) -> Subscribe | None:
|
||||
"""
|
||||
按订阅 ID 读取当前用户可访问的订阅行。
|
||||
"""
|
||||
subscribe = await Subscribe.async_get(db, subscribe_id)
|
||||
if can_access_subscribe(subscribe, current_user):
|
||||
return subscribe
|
||||
return None
|
||||
|
||||
|
||||
def get_accessible_subscribe_sync(
|
||||
db: Session, subscribe_id: int, current_user: User
|
||||
) -> Subscribe | None:
|
||||
"""
|
||||
同步读取当前用户可访问的订阅行。
|
||||
"""
|
||||
subscribe = Subscribe.get(db, subscribe_id)
|
||||
if can_access_subscribe(subscribe, current_user):
|
||||
return subscribe
|
||||
return None
|
||||
|
||||
|
||||
def select_accessible_subscribe(
|
||||
subscribes: List[Subscribe], current_user: User
|
||||
) -> Subscribe | None:
|
||||
subscribes: List[Any], current_user: ApiPrincipal
|
||||
) -> Any:
|
||||
"""
|
||||
从候选订阅中选择当前用户可访问的第一条记录。
|
||||
"""
|
||||
@@ -144,7 +122,7 @@ def select_accessible_subscribe(
|
||||
|
||||
|
||||
def matches_subscribe_music_type(
|
||||
subscribe: Subscribe,
|
||||
subscribe: Any,
|
||||
music_type: Optional[str],
|
||||
) -> bool:
|
||||
"""匹配订阅音乐实体,并把迁移前未标注类型的历史记录兼容为单曲。"""
|
||||
@@ -155,54 +133,30 @@ def matches_subscribe_music_type(
|
||||
or (music_type == MUSIC_ENTITY_RECORDING and subscribe_music_type is None)
|
||||
|
||||
|
||||
async def list_subscribes_by_media_identity(
|
||||
db: AsyncSession,
|
||||
media_source: MediaSource,
|
||||
media_id: str,
|
||||
season: Optional[int] = None,
|
||||
music_type: Optional[str] = None,
|
||||
) -> List[Subscribe]:
|
||||
"""按媒体来源、原生 ID 及音乐实体查询订阅。"""
|
||||
subscribes = list(await Subscribe.async_list_by_media_identity(
|
||||
db,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
music_type=music_type,
|
||||
))
|
||||
unique_subscribes = {
|
||||
subscribe.id: subscribe
|
||||
for subscribe in subscribes
|
||||
if matches_subscribe_music_type(subscribe, music_type)
|
||||
}
|
||||
if season is not None:
|
||||
return [
|
||||
subscribe for subscribe in unique_subscribes.values()
|
||||
if subscribe.season == season
|
||||
]
|
||||
return list(unique_subscribes.values())
|
||||
|
||||
|
||||
@router.get("/", summary="查询所有订阅", response_model=List[_SchemaSubscribe])
|
||||
async def read_subscribes(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询所有订阅
|
||||
"""
|
||||
if not current_user.is_superuser:
|
||||
return await Subscribe.async_list_by_username(db, current_user.name)
|
||||
return await Subscribe.async_list(db)
|
||||
return await query.list_public(current_user.name)
|
||||
return await query.list_public()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/list", summary="查询所有订阅(API_TOKEN)", response_model=List[_SchemaSubscribe]
|
||||
)
|
||||
async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
|
||||
async def list_subscribes(
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
_: Annotated[str, Depends(verify_apitoken)] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
查询所有订阅 API_TOKEN认证(?token=xxx)
|
||||
"""
|
||||
return await Subscribe.async_list()
|
||||
return await query.list_public()
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -213,7 +167,7 @@ async def list_subscribes(_: Annotated[str, Depends(verify_apitoken)]) -> Any:
|
||||
async def create_subscribe(
|
||||
*,
|
||||
subscribe_in: _SchemaSubscribe,
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
新增订阅
|
||||
@@ -274,13 +228,17 @@ async def create_subscribe(
|
||||
async def update_subscribe(
|
||||
*,
|
||||
subscribe_in: _SchemaSubscribe,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
更新订阅信息
|
||||
"""
|
||||
subscribe = await get_accessible_subscribe(db, subscribe_in.id, current_user)
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
subscribe = await mutation.get_accessible(subscribe_in.id, actor)
|
||||
if not subscribe:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
@@ -326,16 +284,21 @@ async def update_subscribe(
|
||||
if total_episode_updated and subscribe_in.total_episode != subscribe.total_episode:
|
||||
subscribe_dict["manual_total_episode"] = 1
|
||||
# 更新到数据库
|
||||
await subscribe.async_update(db, subscribe_dict)
|
||||
# 重新获取更新后的订阅数据
|
||||
updated_subscribe = await Subscribe.async_get(db, subscribe_in.id)
|
||||
change = await mutation.update(
|
||||
subscribe_in.id,
|
||||
subscribe_dict,
|
||||
actor,
|
||||
existing=subscribe,
|
||||
)
|
||||
if not change:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
# 发送订阅调整事件
|
||||
await eventmanager.async_send_event(
|
||||
EventType.SubscribeModified,
|
||||
SubscribeModifiedEventData(
|
||||
subscribe_id=subscribe_in.id,
|
||||
old_subscribe_info=old_subscribe_dict,
|
||||
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
|
||||
old_subscribe_info=change.old,
|
||||
subscribe_info=change.new,
|
||||
scene="update",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -346,29 +309,29 @@ async def update_subscribe(
|
||||
async def update_subscribe_status(
|
||||
subid: int,
|
||||
state: str,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
更新订阅状态
|
||||
"""
|
||||
subscribe = await get_accessible_subscribe(db, subid, current_user)
|
||||
if not subscribe:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
valid_states = ["R", "P", "S"]
|
||||
if state not in valid_states:
|
||||
return _SchemaResponse(success=False, message="无效的订阅状态")
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
await subscribe.async_update(db, {"state": state})
|
||||
# 重新获取更新后的订阅数据
|
||||
updated_subscribe = await Subscribe.async_get(db, subid)
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
change = await mutation.update_status(subid, state, actor)
|
||||
if not change:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
# 发送订阅调整事件
|
||||
await eventmanager.async_send_event(
|
||||
EventType.SubscribeModified,
|
||||
SubscribeModifiedEventData(
|
||||
subscribe_id=subid,
|
||||
old_subscribe_info=old_subscribe_dict,
|
||||
subscribe_info=updated_subscribe.to_dict() if updated_subscribe else {},
|
||||
old_subscribe_info=change.old,
|
||||
subscribe_info=change.new,
|
||||
scene="status",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -382,22 +345,22 @@ async def subscribe_media_identity(
|
||||
season: Optional[int] = None,
|
||||
title: Optional[str] = None,
|
||||
music_type: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据媒体来源和原生 ID 查询订阅。
|
||||
"""
|
||||
subscribes = await list_subscribes_by_media_identity(
|
||||
db, media_source, media_id, season, music_type
|
||||
)
|
||||
subscribes = await query.list_by_media_identity(media_source, media_id, music_type)
|
||||
if season is not None:
|
||||
subscribes = [subscribe for subscribe in subscribes if subscribe.season == season]
|
||||
result = select_accessible_subscribe(subscribes, current_user)
|
||||
return result if result else Subscribe()
|
||||
return result if result else _SchemaSubscribe()
|
||||
|
||||
|
||||
@router.get("/refresh", summary="刷新订阅", response_model=_SchemaResponse[None])
|
||||
def refresh_subscribes(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
@@ -411,44 +374,24 @@ def refresh_subscribes(
|
||||
@router.get("/reset/{subid}", summary="重置订阅", response_model=_SchemaResponse[None])
|
||||
async def reset_subscribes(
|
||||
subid: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
重置订阅
|
||||
"""
|
||||
subscribe = await get_accessible_subscribe(db, subid, current_user)
|
||||
if subscribe:
|
||||
# 在更新之前获取旧数据
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
# 更新订阅
|
||||
await subscribe.async_update(
|
||||
db,
|
||||
{
|
||||
"note": [],
|
||||
"lack_episode": subscribe.total_episode,
|
||||
"current_priority": None,
|
||||
"current_audio_format": None,
|
||||
"current_bitrate": None,
|
||||
"current_bit_depth": None,
|
||||
"current_sample_rate": None,
|
||||
"episode_priority": {},
|
||||
# 重置代表放弃手动总集数,后续订阅检查重新按 TMDB 集数更新。
|
||||
"manual_total_episode": 0,
|
||||
"state": "R",
|
||||
},
|
||||
)
|
||||
# 重新获取更新后的订阅数据
|
||||
updated_subscribe = await Subscribe.async_get(db, subid)
|
||||
# 发送订阅调整事件
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
change = await mutation.reset(subid, actor)
|
||||
if change:
|
||||
await eventmanager.async_send_event(
|
||||
EventType.SubscribeModified,
|
||||
SubscribeModifiedEventData(
|
||||
subscribe_id=subid,
|
||||
old_subscribe_info=old_subscribe_dict,
|
||||
subscribe_info=updated_subscribe.to_dict()
|
||||
if updated_subscribe
|
||||
else {},
|
||||
old_subscribe_info=change.old,
|
||||
subscribe_info=change.new,
|
||||
scene="reset",
|
||||
).to_dict(),
|
||||
)
|
||||
@@ -458,7 +401,7 @@ async def reset_subscribes(
|
||||
|
||||
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=_SchemaResponse[None])
|
||||
def check_subscribes(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
刷新订阅 TMDB 信息
|
||||
@@ -472,7 +415,7 @@ def check_subscribes(
|
||||
@router.get("/search", summary="搜索所有订阅", response_model=_SchemaResponse[None])
|
||||
async def search_subscribes(
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
搜索所有订阅
|
||||
@@ -492,7 +435,7 @@ async def search_subscribes(
|
||||
async def search_subscribe(
|
||||
subscribe_id: int,
|
||||
command: SearchSubscriptionsCommand = Depends(get_search_subscriptions_command),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据订阅编号搜索订阅
|
||||
@@ -518,7 +461,7 @@ async def delete_subscribe_by_media_identity(
|
||||
command: DeleteSubscriptionsByIdentityCommand = Depends(
|
||||
get_delete_subscriptions_by_identity_command
|
||||
),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据任意媒体数据源 ID 删除订阅。
|
||||
@@ -616,28 +559,18 @@ async def subscribe_history(
|
||||
mtype: str,
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询电影、电视剧或音乐订阅历史
|
||||
"""
|
||||
if current_user.is_superuser:
|
||||
histories = await SubscribeHistory.async_list_by_type(
|
||||
db, mtype=mtype, page=page, count=count
|
||||
)
|
||||
else:
|
||||
histories = await SubscribeHistory.async_list_by_type_and_username(
|
||||
db, mtype=mtype, username=current_user.name, page=page, count=count
|
||||
)
|
||||
result = []
|
||||
for history in histories:
|
||||
history_item = _SchemaSubscribe.model_validate(history, from_attributes=True)
|
||||
if history_item.type == MediaType.TV.value:
|
||||
history_item.total_episode = 0
|
||||
history_item.lack_episode = 0
|
||||
result.append(history_item)
|
||||
return result
|
||||
return await query.list_history(
|
||||
mtype,
|
||||
page=page,
|
||||
count=count,
|
||||
username=None if current_user.is_superuser else current_user.name,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -645,15 +578,17 @@ async def subscribe_history(
|
||||
)
|
||||
async def delete_subscribe_history(
|
||||
history_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
删除订阅历史
|
||||
"""
|
||||
history = await SubscribeHistory.async_get(db, history_id)
|
||||
if can_access_subscribe(history, current_user):
|
||||
await SubscribeHistory.async_delete(db, history_id)
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
await mutation.delete_history(history_id, actor)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@@ -721,15 +656,15 @@ async def popular_subscribes(
|
||||
)
|
||||
async def user_subscribes(
|
||||
username: str,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询用户订阅
|
||||
"""
|
||||
if not current_user.is_superuser and username != current_user.name:
|
||||
return []
|
||||
return await Subscribe.async_list_by_username(db, username)
|
||||
return await query.list_public(username)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -739,13 +674,17 @@ async def user_subscribes(
|
||||
)
|
||||
def subscribe_files(
|
||||
subscribe_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_sync_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
订阅相关文件信息
|
||||
"""
|
||||
subscribe = get_accessible_subscribe_sync(db, subscribe_id, current_user)
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
subscribe = mutation.get_accessible_sync(subscribe_id, actor)
|
||||
if subscribe:
|
||||
return SubscribeChain().subscribe_files_info(subscribe)
|
||||
return _SchemaSubscrbieInfo()
|
||||
@@ -754,13 +693,17 @@ def subscribe_files(
|
||||
@router.post("/share", summary="分享订阅", response_model=_SchemaResponse[None])
|
||||
async def subscribe_share(
|
||||
sub: _SchemaSubscribeShare,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
mutation: SubscriptionMutationService = Depends(get_subscription_mutation_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
分享订阅
|
||||
"""
|
||||
subscribe = await get_accessible_subscribe(db, sub.subscribe_id, current_user)
|
||||
actor = SubscriptionActor(
|
||||
name=current_user.name,
|
||||
is_superuser=current_user.is_superuser,
|
||||
)
|
||||
subscribe = await mutation.get_accessible(sub.subscribe_id, actor)
|
||||
if not subscribe:
|
||||
return _SchemaResponse(success=False, message="订阅不存在")
|
||||
state, errmsg = await MoviePilotServerHelper.async_sub_share(
|
||||
@@ -786,7 +729,7 @@ async def subscribe_share_delete(
|
||||
@router.post("/fork", summary="复用订阅", response_model=_SchemaResponse[None])
|
||||
async def subscribe_fork(
|
||||
sub: _SchemaSubscribeShare,
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
复用订阅
|
||||
@@ -809,7 +752,7 @@ async def followed_subscribers(_: _SchemaTokenPayload = Depends(verify_token)) -
|
||||
"""
|
||||
查询已Follow的订阅分享人
|
||||
"""
|
||||
return SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
return get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
|
||||
|
||||
|
||||
@router.post("/follow", summary="Follow订阅分享人", response_model=_SchemaResponse[None])
|
||||
@@ -819,10 +762,10 @@ async def follow_subscriber(
|
||||
"""
|
||||
Follow订阅分享人
|
||||
"""
|
||||
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid not in subscribers:
|
||||
subscribers.append(share_uid)
|
||||
await SystemConfigOper().async_set(
|
||||
await get_configured_system_config().async_set(
|
||||
SystemConfigKey.FollowSubscribers, subscribers
|
||||
)
|
||||
return _SchemaResponse(success=True)
|
||||
@@ -837,10 +780,10 @@ async def unfollow_subscriber(
|
||||
"""
|
||||
取消Follow订阅分享人
|
||||
"""
|
||||
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
subscribers = get_configured_system_config().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid in subscribers:
|
||||
subscribers.remove(share_uid)
|
||||
await SystemConfigOper().async_set(
|
||||
await get_configured_system_config().async_set(
|
||||
SystemConfigKey.FollowSubscribers, subscribers
|
||||
)
|
||||
return _SchemaResponse(success=True)
|
||||
@@ -891,23 +834,27 @@ async def subscribe_share_statistics(
|
||||
@router.get("/{subscribe_id}", summary="订阅详情", response_model=_SchemaSubscribe)
|
||||
async def read_subscribe(
|
||||
subscribe_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
query: SubscriptionQueryService = Depends(get_subscription_query_service),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
根据订阅编号查询订阅信息
|
||||
"""
|
||||
if not subscribe_id:
|
||||
return Subscribe()
|
||||
subscribe = await get_accessible_subscribe(db, subscribe_id, current_user)
|
||||
return subscribe if subscribe else Subscribe()
|
||||
return _SchemaSubscribe()
|
||||
subscribe = await query.get_public(subscribe_id)
|
||||
return (
|
||||
subscribe
|
||||
if subscribe and can_access_subscribe(subscribe, current_user)
|
||||
else _SchemaSubscribe()
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{subscribe_id}", summary="删除订阅", response_model=_SchemaResponse[None])
|
||||
async def delete_subscribe(
|
||||
subscribe_id: int,
|
||||
command: DeleteSubscribeCommand = Depends(get_delete_subscribe_command),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: ApiPrincipal = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
删除订阅信息
|
||||
|
||||
+20
-20
@@ -38,10 +38,10 @@ from app.chain.system import SystemChain
|
||||
from app.runtime.config import global_vars, settings
|
||||
from app.runtime.events import eventmanager
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.application.security.access import verify_apitoken, verify_resource_token, verify_token
|
||||
from app.db.models import User
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.module import ModuleManager
|
||||
from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token
|
||||
from app.api.principal import ApiPrincipal
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import get_current_active_superuser, get_current_active_superuser_async, get_current_active_user_async
|
||||
from app.application.image import ImageHelper
|
||||
from app.runtime.localization import LocaleHelper
|
||||
@@ -57,7 +57,7 @@ from app.application.rules import RuleHelper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
from app.application.scheduling import Scheduler
|
||||
from app.schemas.event import ConfigChangeEventData
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.foundation.crypto import HashUtils
|
||||
@@ -704,7 +704,7 @@ def get_global_setting(token: str):
|
||||
summary="查询用户相关系统设置",
|
||||
response_model=_SchemaResponse[_SchemaJsonObject],
|
||||
)
|
||||
async def get_user_global_setting(_: User = Depends(get_current_active_user_async)):
|
||||
async def get_user_global_setting(_: ApiPrincipal = Depends(get_current_active_user_async)):
|
||||
"""
|
||||
查询用户相关系统设置(登录后获取)
|
||||
包含业务功能相关的配置和用户权限信息
|
||||
@@ -745,7 +745,7 @@ async def get_user_global_setting(_: User = Depends(get_current_active_user_asyn
|
||||
response_model=_SchemaResponse[_SchemaJsonObject],
|
||||
)
|
||||
async def get_env_setting(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
查询系统环境变量,包括当前版本号(仅管理员)
|
||||
@@ -769,7 +769,7 @@ async def get_env_setting(
|
||||
summary="查询安装版本统计报表",
|
||||
response_model=_SchemaResponse[_SchemaJsonObject],
|
||||
)
|
||||
async def usage_statistic(_: User = Depends(get_current_active_user_async)):
|
||||
async def usage_statistic(_: ApiPrincipal = Depends(get_current_active_user_async)):
|
||||
"""
|
||||
查询安装版本统计报表
|
||||
"""
|
||||
@@ -777,7 +777,7 @@ async def usage_statistic(_: User = Depends(get_current_active_user_async)):
|
||||
|
||||
|
||||
@router.get("/ping", summary="服务存活检测", response_model=_SchemaResponse[None])
|
||||
async def ping(_: User = Depends(get_current_active_user_async)) -> _SchemaResponse:
|
||||
async def ping(_: ApiPrincipal = Depends(get_current_active_user_async)) -> _SchemaResponse:
|
||||
"""
|
||||
检测服务是否可用
|
||||
"""
|
||||
@@ -790,7 +790,7 @@ async def ping(_: User = Depends(get_current_active_user_async)) -> _SchemaRespo
|
||||
response_model=_SchemaResponse[_SchemaSystemEnvironmentUpdateData],
|
||||
)
|
||||
async def set_env_setting(
|
||||
env: dict, _: User = Depends(get_current_active_superuser_async)
|
||||
env: dict, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
):
|
||||
"""
|
||||
更新系统环境变量(仅管理员)
|
||||
@@ -870,7 +870,7 @@ async def get_progress(
|
||||
response_model=_SchemaResponse[_SchemaValueData],
|
||||
)
|
||||
async def get_public_setting(
|
||||
key: str, _: User = Depends(get_current_active_user_async)
|
||||
key: str, _: ApiPrincipal = Depends(get_current_active_user_async)
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
查询普通用户可读取的非敏感系统设置
|
||||
@@ -879,7 +879,7 @@ async def get_public_setting(
|
||||
return _SchemaResponse(success=True, data={"value": getattr(settings, key)})
|
||||
if key not in _PUBLIC_SYSTEM_CONFIG_KEYS:
|
||||
raise HTTPException(status_code=404, detail="配置项不存在")
|
||||
value = SystemConfigOper().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key])
|
||||
value = get_configured_system_config().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key])
|
||||
return _SchemaResponse(success=True, data={"value": value})
|
||||
|
||||
|
||||
@@ -890,7 +890,7 @@ async def get_public_setting(
|
||||
)
|
||||
async def sync_plugin_market_from_wiki(
|
||||
request: Optional[_SchemaPluginMarketSyncRequest] = Body(default=None),
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
从 Wiki 插件文档同步插件市场仓库地址。
|
||||
@@ -956,7 +956,7 @@ async def sync_plugin_market_from_wiki(
|
||||
response_model=_SchemaResponse[_SchemaValueData],
|
||||
)
|
||||
async def get_setting(
|
||||
key: str, _: User = Depends(get_current_active_superuser_async)
|
||||
key: str, _: ApiPrincipal = Depends(get_current_active_superuser_async)
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
查询系统设置(仅管理员)
|
||||
@@ -964,7 +964,7 @@ async def get_setting(
|
||||
if hasattr(settings, key):
|
||||
value = getattr(settings, key)
|
||||
else:
|
||||
value = SystemConfigOper().get(key)
|
||||
value = get_configured_system_config().get(key)
|
||||
return _SchemaResponse(success=True, data={"value": value})
|
||||
|
||||
|
||||
@@ -972,7 +972,7 @@ async def get_setting(
|
||||
async def set_setting(
|
||||
key: str,
|
||||
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""
|
||||
更新系统设置(仅管理员)
|
||||
@@ -992,7 +992,7 @@ async def set_setting(
|
||||
if isinstance(value, list):
|
||||
value = list(filter(None, value))
|
||||
value = value if value else None
|
||||
success = await SystemConfigOper().async_set(key, value)
|
||||
success = await get_configured_system_config().async_set(key, value)
|
||||
if success:
|
||||
# 发送配置变更事件
|
||||
await eventmanager.async_send_event(
|
||||
@@ -1446,7 +1446,7 @@ def moduletest(moduleid: str, _: _SchemaTokenPayload = Depends(verify_token)):
|
||||
|
||||
|
||||
@router.get("/restart", summary="重启系统", response_model=_SchemaResponse[None])
|
||||
def restart_system(_: User = Depends(get_current_active_superuser)):
|
||||
def restart_system(_: ApiPrincipal = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
重启系统(仅管理员)
|
||||
"""
|
||||
@@ -1459,7 +1459,7 @@ def restart_system(_: User = Depends(get_current_active_superuser)):
|
||||
@router.post("/upgrade", summary="升级并重启系统", response_model=_SchemaResponse[None])
|
||||
def upgrade_system(
|
||||
mode: Annotated[str | None, Body()] = None,
|
||||
_: User = Depends(get_current_active_superuser),
|
||||
_: ApiPrincipal = Depends(get_current_active_superuser),
|
||||
):
|
||||
"""
|
||||
触发系统升级并重启(仅管理员)
|
||||
@@ -1475,7 +1475,7 @@ def upgrade_system(
|
||||
|
||||
|
||||
@router.get("/runscheduler", summary="运行服务", response_model=_SchemaResponse[None])
|
||||
def run_scheduler(jobid: str, _: User = Depends(get_current_active_superuser)):
|
||||
def run_scheduler(jobid: str, _: ApiPrincipal = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
执行命令(仅管理员)
|
||||
"""
|
||||
|
||||
@@ -12,9 +12,8 @@ from app.schemas.workflow import MediaInfo as _SchemaMediaInfo
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.runtime.config import settings
|
||||
from app.application.security.access import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.adapters.web.security.access import verify_token
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.api.deps import get_current_active_superuser_async
|
||||
from app.schemas.types import MediaType, SystemConfigKey
|
||||
|
||||
@@ -27,7 +26,7 @@ router = ResponseAPIRouter()
|
||||
response_model=_SchemaResponse[_SchemaTmdbRecognitionCacheData],
|
||||
)
|
||||
async def tmdb_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""查询可管理的 TheMovieDb 识别缓存。"""
|
||||
cache_items = TmdbChain().cache_items()
|
||||
@@ -38,7 +37,7 @@ async def tmdb_recognition_cache(
|
||||
"count": len(cache_items),
|
||||
"recognized": recognized_count,
|
||||
"unrecognized": len(cache_items) - recognized_count,
|
||||
"shared_recognized": SystemConfigOper().get(
|
||||
"shared_recognized": get_configured_system_config().get(
|
||||
SystemConfigKey.MediaRecognizeShareCount
|
||||
) or 0,
|
||||
"shared_recognize_enabled": settings.MEDIA_RECOGNIZE_SHARE,
|
||||
@@ -54,7 +53,7 @@ async def tmdb_recognition_cache(
|
||||
)
|
||||
async def delete_tmdb_recognition_cache(
|
||||
cache_key: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""按缓存键删除单条 TheMovieDb 识别缓存。"""
|
||||
deleted_item = TmdbChain().delete_cache(cache_key)
|
||||
@@ -67,7 +66,7 @@ async def delete_tmdb_recognition_cache(
|
||||
"/cache", summary="清空 TheMovieDb 识别缓存", response_model=_SchemaResponse[None]
|
||||
)
|
||||
async def clear_tmdb_recognition_cache(
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
) -> _SchemaResponse:
|
||||
"""清空全部 TheMovieDb 识别缓存。"""
|
||||
TmdbChain().clear_cache()
|
||||
|
||||
@@ -12,7 +12,6 @@ from app.runtime.config import settings
|
||||
from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.db.models import User
|
||||
from app.api.deps import get_current_active_superuser, get_current_active_superuser_async
|
||||
from app.schemas.types import (
|
||||
MUSIC_ENTITY_RECORDING,
|
||||
@@ -32,7 +31,7 @@ router = ResponseAPIRouter()
|
||||
summary="获取种子缓存",
|
||||
response_model=_SchemaResponse[_SchemaTorrentCacheData],
|
||||
)
|
||||
async def torrents_cache(_: User = Depends(get_current_active_superuser_async)):
|
||||
async def torrents_cache(_: object = Depends(get_current_active_superuser_async)):
|
||||
"""
|
||||
获取当前种子缓存数据
|
||||
"""
|
||||
@@ -103,7 +102,7 @@ async def torrents_cache(_: User = Depends(get_current_active_superuser_async)):
|
||||
async def delete_cache(
|
||||
domain: str,
|
||||
torrent_hash: str,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""
|
||||
删除指定的种子缓存
|
||||
@@ -147,7 +146,7 @@ async def delete_cache(
|
||||
|
||||
|
||||
@router.delete("/cache", summary="清理种子缓存", response_model=_SchemaResponse[None])
|
||||
async def clear_cache(_: User = Depends(get_current_active_superuser_async)):
|
||||
async def clear_cache(_: object = Depends(get_current_active_superuser_async)):
|
||||
"""
|
||||
清理所有种子缓存
|
||||
"""
|
||||
@@ -161,7 +160,7 @@ async def clear_cache(_: User = Depends(get_current_active_superuser_async)):
|
||||
|
||||
|
||||
@router.post("/cache/refresh", summary="刷新种子缓存", response_model=_SchemaResponse[None])
|
||||
def refresh_cache(_: User = Depends(get_current_active_superuser)):
|
||||
def refresh_cache(_: object = Depends(get_current_active_superuser)):
|
||||
"""
|
||||
刷新种子缓存
|
||||
"""
|
||||
@@ -195,7 +194,7 @@ async def reidentify_cache(
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
music_type: Optional[MusicTargetEntityType] = None,
|
||||
_: User = Depends(get_current_active_superuser_async),
|
||||
_: object = Depends(get_current_active_superuser_async),
|
||||
):
|
||||
"""
|
||||
重新识别指定的种子
|
||||
|
||||
@@ -2,7 +2,6 @@ from pathlib import Path
|
||||
from typing import Any, List, Annotated, Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.common import NameData as _SchemaNameData
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
@@ -19,12 +18,13 @@ from app.api.response import ResponseAPIRouter
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.runtime.config import settings, global_vars
|
||||
from app.application.security.access import verify_token, verify_apitoken
|
||||
from app.db import get_db
|
||||
from app.db.models import User
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.api.deps import get_current_active_manage_user
|
||||
from app.adapters.web.security.access import verify_token, verify_apitoken
|
||||
from app.api.deps import (
|
||||
get_current_active_manage_user,
|
||||
get_transfer_history_lookup_service,
|
||||
)
|
||||
from app.application.directory import DirectoryHelper
|
||||
from app.application.history import TransferHistoryLookupService
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MediaType
|
||||
from app.schemas.workflow import FileItem
|
||||
@@ -106,7 +106,8 @@ async def remove_queue(
|
||||
|
||||
|
||||
def _resolve_manual_transfer_source_fileitems(
|
||||
transer_item: ManualTransferItem, db: Session
|
||||
transer_item: ManualTransferItem,
|
||||
history_query: TransferHistoryLookupService,
|
||||
) -> tuple[List[FileItem], Optional[str]]:
|
||||
"""
|
||||
从手动整理请求中解析源文件项。
|
||||
@@ -114,7 +115,7 @@ def _resolve_manual_transfer_source_fileitems(
|
||||
if transer_item.logids:
|
||||
fileitems: List[FileItem] = []
|
||||
for logid in transer_item.logids:
|
||||
history: TransferHistory = TransferHistory.get(db, logid)
|
||||
history = history_query.get(logid)
|
||||
if not history:
|
||||
return [], f"整理记录不存在,ID:{logid}"
|
||||
if history.status and ("move" in history.mode):
|
||||
@@ -124,7 +125,7 @@ def _resolve_manual_transfer_source_fileitems(
|
||||
return fileitems, None
|
||||
|
||||
if transer_item.logid:
|
||||
history: TransferHistory = TransferHistory.get(db, transer_item.logid)
|
||||
history = history_query.get(transer_item.logid)
|
||||
if not history:
|
||||
return [], f"整理记录不存在,ID:{transer_item.logid}"
|
||||
if history.status and ("move" in history.mode):
|
||||
@@ -195,19 +196,21 @@ def _get_manual_transfer_target_key(
|
||||
)
|
||||
def match_manual_transfer_target_path(
|
||||
transer_item: ManualTransferItem,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
history_query: TransferHistoryLookupService = Depends(
|
||||
get_transfer_history_lookup_service
|
||||
),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
根据源文件匹配手动整理目的路径。
|
||||
|
||||
:param transer_item: 手工整理项
|
||||
:param db: 数据库
|
||||
:param history_query: 整理历史投影服务
|
||||
:param _: Token校验
|
||||
"""
|
||||
src_fileitems, error_message = _resolve_manual_transfer_source_fileitems(
|
||||
transer_item=transer_item,
|
||||
db=db,
|
||||
history_query=history_query,
|
||||
)
|
||||
if error_message:
|
||||
return _SchemaResponse(success=False, message=error_message)
|
||||
@@ -258,19 +261,21 @@ def match_manual_transfer_target_path(
|
||||
)
|
||||
def query_manual_transfer_history(
|
||||
transer_item: ManualTransferItem,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
history_query: TransferHistoryLookupService = Depends(
|
||||
get_transfer_history_lookup_service
|
||||
),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
查询文件或目录命中的成功整理记录。
|
||||
|
||||
:param transer_item: 手工整理项
|
||||
:param db: 数据库
|
||||
:param history_query: 整理历史投影服务
|
||||
:param _: Token校验
|
||||
"""
|
||||
src_fileitems, error_message = _resolve_manual_transfer_source_fileitems(
|
||||
transer_item=transer_item,
|
||||
db=db,
|
||||
history_query=history_query,
|
||||
)
|
||||
if error_message:
|
||||
return _SchemaResponse(success=False, message=error_message)
|
||||
@@ -293,14 +298,16 @@ def query_manual_transfer_history(
|
||||
def manual_transfer(
|
||||
transer_item: ManualTransferItem,
|
||||
background: Optional[bool] = False,
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
history_query: TransferHistoryLookupService = Depends(
|
||||
get_transfer_history_lookup_service
|
||||
),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
手动转移,文件或历史记录,支持自定义剧集识别格式
|
||||
:param transer_item: 手工整理项
|
||||
:param background: 后台运行
|
||||
:param db: 数据库
|
||||
:param history_query: 整理历史投影服务
|
||||
:param _: Token校验
|
||||
"""
|
||||
force = False
|
||||
@@ -311,7 +318,7 @@ def manual_transfer(
|
||||
target_path = Path(transer_item.target_path) if transer_item.target_path else None
|
||||
if transer_item.logid:
|
||||
# 查询历史记录
|
||||
history: TransferHistory = TransferHistory.get(db, transer_item.logid)
|
||||
history = history_query.get(transer_item.logid)
|
||||
if not history:
|
||||
return _SchemaResponse(
|
||||
success=False, message=f"整理记录不存在,ID:{transer_item.logid}"
|
||||
@@ -592,7 +599,7 @@ def manual_transfer(
|
||||
)
|
||||
def recommend_episode_format(
|
||||
recommend_item: EpisodeFormatRecommendItem,
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: object = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
根据目录样本推荐集数定位模板
|
||||
|
||||
+45
-38
@@ -3,7 +3,6 @@ import re
|
||||
from typing import Annotated, Any, List, Union
|
||||
|
||||
from fastapi import Body, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.common import FileNameData as _SchemaFileNameData
|
||||
from app.schemas.common import ValueData as _SchemaValueData
|
||||
@@ -12,37 +11,41 @@ from app.schemas.user import User as _SchemaUser
|
||||
from app.schemas.user import UserCreate as _SchemaUserCreate
|
||||
from app.schemas.user import UserUpdate as _SchemaUserUpdate
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.security.access import PasswordTooLongError, get_password_hash
|
||||
from app.db import get_async_db
|
||||
from app.db.models.user import User
|
||||
from app.api.deps import get_current_active_superuser_async, get_current_active_user_async, get_current_active_user
|
||||
from app.db.oper.userconfig import UserConfigOper
|
||||
from app.application.security.token import PasswordTooLongError, get_password_hash
|
||||
from app.application.security.user import UserService
|
||||
from app.api.deps import (
|
||||
get_current_active_superuser_async,
|
||||
get_current_active_user_async,
|
||||
get_current_active_user,
|
||||
get_user_service,
|
||||
)
|
||||
from app.application.security.userconfig import get_configured_user_configuration
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@router.get("/", summary="所有用户", response_model=List[_SchemaUser])
|
||||
async def list_users(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: User = Depends(get_current_active_superuser_async),
|
||||
service: UserService = Depends(get_user_service),
|
||||
current_user: Any = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询用户列表
|
||||
"""
|
||||
return await current_user.async_list(db)
|
||||
return await service.list()
|
||||
|
||||
|
||||
@router.post("/", summary="新增用户", response_model=_SchemaResponse[None])
|
||||
async def create_user(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: UserService = Depends(get_user_service),
|
||||
user_in: _SchemaUserCreate,
|
||||
current_user: User = Depends(get_current_active_superuser_async),
|
||||
current_user: Any = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
新增用户
|
||||
"""
|
||||
user = await current_user.async_get_by_name(db, name=user_in.name)
|
||||
user = await service.get_by_name(user_in.name)
|
||||
if user:
|
||||
return _SchemaResponse(success=False, message="用户已存在")
|
||||
user_info = user_in.model_dump()
|
||||
@@ -52,16 +55,16 @@ async def create_user(
|
||||
except PasswordTooLongError as error:
|
||||
return _SchemaResponse(success=False, message=str(error))
|
||||
user_info.pop("password")
|
||||
user = await User(**user_info).async_create(db)
|
||||
user = await service.create(user_info)
|
||||
return _SchemaResponse(success=True if user else False)
|
||||
|
||||
|
||||
@router.put("/", summary="更新用户", response_model=_SchemaResponse[None])
|
||||
async def update_user(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: UserService = Depends(get_user_service),
|
||||
user_in: _SchemaUserUpdate,
|
||||
current_user: User = Depends(get_current_active_superuser_async),
|
||||
current_user: Any = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
更新用户
|
||||
@@ -80,24 +83,24 @@ async def update_user(
|
||||
except PasswordTooLongError as error:
|
||||
return _SchemaResponse(success=False, message=str(error))
|
||||
user_info.pop("password")
|
||||
user = await current_user.async_get_by_id(db, user_id=user_info["id"])
|
||||
user = await service.get_by_id(user_info["id"])
|
||||
user_name = user_info.get("name")
|
||||
if not user_name:
|
||||
return _SchemaResponse(success=False, message="用户名不能为空")
|
||||
# 新用户名去重
|
||||
users = await current_user.async_list(db)
|
||||
users = await service.list()
|
||||
for u in users:
|
||||
if u.name == user_name and u.id != user_info["id"]:
|
||||
return _SchemaResponse(success=False, message="用户名已被使用")
|
||||
if not user:
|
||||
return _SchemaResponse(success=False, message="用户不存在")
|
||||
await user.async_update(db, user_info)
|
||||
await service.update(user_info["id"], user_info)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get("/current", summary="当前登录用户信息", response_model=_SchemaUser)
|
||||
async def read_current_user(
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: Any = Depends(get_current_active_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
当前登录用户信息
|
||||
@@ -112,9 +115,9 @@ async def read_current_user(
|
||||
)
|
||||
async def upload_avatar(
|
||||
user_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: UserService = Depends(get_user_service),
|
||||
file: UploadFile = File(...),
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
current_user: Any = Depends(get_current_active_user_async),
|
||||
) -> _SchemaResponse:
|
||||
"""
|
||||
上传用户头像
|
||||
@@ -125,10 +128,10 @@ async def upload_avatar(
|
||||
# 将文件转换为Base64
|
||||
file_base64 = base64.b64encode(file.file.read())
|
||||
# 更新到用户表
|
||||
user = await User.async_get(db, user_id)
|
||||
user = await service.get_by_id(user_id)
|
||||
if not user:
|
||||
return _SchemaResponse(success=False, message="用户不存在")
|
||||
await user.async_update(db, {"avatar": f"data:image/ico;base64,{file_base64}"})
|
||||
await service.update(user_id, {"avatar": f"data:image/ico;base64,{file_base64}"})
|
||||
return _SchemaResponse(success=True, data={"filename": file.filename})
|
||||
|
||||
|
||||
@@ -137,11 +140,11 @@ async def upload_avatar(
|
||||
summary="查询用户配置",
|
||||
response_model=_SchemaResponse[_SchemaValueData],
|
||||
)
|
||||
def get_config(key: str, current_user: User = Depends(get_current_active_user)):
|
||||
def get_config(key: str, current_user: Any = Depends(get_current_active_user)):
|
||||
"""
|
||||
查询用户配置
|
||||
"""
|
||||
value = UserConfigOper().get(username=current_user.name, key=key)
|
||||
value = get_configured_user_configuration().get(username=current_user.name, key=key)
|
||||
return _SchemaResponse(success=True, data={"value": value})
|
||||
|
||||
|
||||
@@ -149,59 +152,63 @@ def get_config(key: str, current_user: User = Depends(get_current_active_user)):
|
||||
def set_config(
|
||||
key: str,
|
||||
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
current_user: Any = Depends(get_current_active_user),
|
||||
):
|
||||
"""
|
||||
更新用户配置
|
||||
"""
|
||||
UserConfigOper().set(username=current_user.name, key=key, value=value)
|
||||
get_configured_user_configuration().set(
|
||||
username=current_user.name,
|
||||
key=key,
|
||||
value=value,
|
||||
)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.delete("/id/{user_id}", summary="删除用户", response_model=_SchemaResponse[None])
|
||||
async def delete_user_by_id(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: UserService = Depends(get_user_service),
|
||||
user_id: int,
|
||||
current_user: User = Depends(get_current_active_superuser_async),
|
||||
current_user: Any = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
通过唯一ID删除用户
|
||||
"""
|
||||
user = await current_user.async_get_by_id(db, user_id=user_id)
|
||||
user = await service.get_by_id(user_id)
|
||||
if not user:
|
||||
return _SchemaResponse(success=False, message="用户不存在")
|
||||
await current_user.async_delete(db, user_id)
|
||||
await service.delete(user_id)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.delete("/name/{user_name}", summary="删除用户", response_model=_SchemaResponse[None])
|
||||
async def delete_user_by_name(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
service: UserService = Depends(get_user_service),
|
||||
user_name: str,
|
||||
current_user: User = Depends(get_current_active_superuser_async),
|
||||
current_user: Any = Depends(get_current_active_superuser_async),
|
||||
) -> Any:
|
||||
"""
|
||||
通过用户名删除用户
|
||||
"""
|
||||
user = await current_user.async_get_by_name(db, name=user_name)
|
||||
user = await service.get_by_name(user_name)
|
||||
if not user:
|
||||
return _SchemaResponse(success=False, message="用户不存在")
|
||||
await current_user.async_delete(db, user.id)
|
||||
await service.delete(user.id)
|
||||
return _SchemaResponse(success=True)
|
||||
|
||||
|
||||
@router.get("/{username}", summary="用户详情", response_model=_SchemaUser)
|
||||
async def read_user_by_name(
|
||||
username: str,
|
||||
current_user: User = Depends(get_current_active_user_async),
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
current_user: Any = Depends(get_current_active_user_async),
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> Any:
|
||||
"""
|
||||
查询用户详情
|
||||
"""
|
||||
user = await current_user.async_get_by_name(db, name=username)
|
||||
user = await service.get_by_name(username)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
|
||||
@@ -5,7 +5,7 @@ from fastapi import BackgroundTasks, Request, Depends
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.chain.webhook import WebhookChain
|
||||
from app.application.security.access import verify_apitoken
|
||||
from app.adapters.web.security.access import verify_apitoken
|
||||
|
||||
router = ResponseAPIRouter()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import List, Any, Optional
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.schemas.workflow import NameValueOption as _SchemaNameValueOption
|
||||
@@ -10,19 +9,21 @@ from app.schemas.workflow import Workflow as _SchemaWorkflow
|
||||
from app.schemas.workflow import WorkflowActionDefinition as _SchemaWorkflowActionDefinition
|
||||
from app.schemas.workflow import WorkflowShare as _SchemaWorkflowShare
|
||||
from app.api.response import ResponseAPIRouter
|
||||
from app.application.workflow import WorkflowDefinitionCommand, WorkflowMutationCommand
|
||||
from app.application.workflow import (
|
||||
WorkflowDefinitionCommand,
|
||||
WorkflowMutationCommand,
|
||||
WorkflowQueryService,
|
||||
)
|
||||
from app.chain.workflow import WorkflowChain
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.plugin.runtime import get_plugin_manager as PluginManager
|
||||
from app.workflow import WorkFlowManager
|
||||
from app.db import get_async_db
|
||||
from app.db.models import User
|
||||
from app.api.deps import (
|
||||
get_current_active_manage_user,
|
||||
get_current_active_manage_user_async,
|
||||
get_workflow_definition_command,
|
||||
get_workflow_mutation_command,
|
||||
get_workflow_query_service,
|
||||
)
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.schemas.types import EventType, EVENT_TYPE_NAMES
|
||||
|
||||
@@ -30,20 +31,20 @@ router = ResponseAPIRouter()
|
||||
|
||||
@router.get("/", summary="所有工作流", response_model=List[_SchemaWorkflow])
|
||||
async def list_workflows(
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: WorkflowQueryService = Depends(get_workflow_query_service),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
获取工作流列表
|
||||
"""
|
||||
return await WorkflowOper(db).async_list()
|
||||
return await query.list()
|
||||
|
||||
|
||||
@router.post("/", summary="创建工作流", response_model=_SchemaResponse[None])
|
||||
async def create_workflow(
|
||||
workflow: _SchemaWorkflow,
|
||||
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
创建工作流
|
||||
@@ -58,7 +59,7 @@ async def create_workflow(
|
||||
response_model=List[_SchemaPluginWorkflowActionGroup],
|
||||
)
|
||||
def list_plugin_actions(
|
||||
plugin_id: str = None, _: User = Depends(get_current_active_manage_user)
|
||||
plugin_id: str = None, _: Any = Depends(get_current_active_manage_user)
|
||||
) -> Any:
|
||||
"""
|
||||
获取所有动作
|
||||
@@ -71,7 +72,7 @@ def list_plugin_actions(
|
||||
summary="所有动作",
|
||||
response_model=List[_SchemaWorkflowActionDefinition],
|
||||
)
|
||||
async def list_actions(_: User = Depends(get_current_active_manage_user_async)) -> Any:
|
||||
async def list_actions(_: Any = Depends(get_current_active_manage_user_async)) -> Any:
|
||||
"""
|
||||
获取所有动作
|
||||
"""
|
||||
@@ -83,7 +84,7 @@ async def list_actions(_: User = Depends(get_current_active_manage_user_async))
|
||||
summary="获取所有事件类型",
|
||||
response_model=List[_SchemaNameValueOption],
|
||||
)
|
||||
async def get_event_types(_: User = Depends(get_current_active_manage_user_async)) -> Any:
|
||||
async def get_event_types(_: Any = Depends(get_current_active_manage_user_async)) -> Any:
|
||||
"""
|
||||
获取所有事件类型
|
||||
"""
|
||||
@@ -98,7 +99,7 @@ async def get_event_types(_: User = Depends(get_current_active_manage_user_async
|
||||
|
||||
@router.post("/share", summary="分享工作流", response_model=_SchemaResponse[None])
|
||||
async def workflow_share(
|
||||
workflow: _SchemaWorkflowShare, _: User = Depends(get_current_active_manage_user_async)
|
||||
workflow: _SchemaWorkflowShare, _: Any = Depends(get_current_active_manage_user_async)
|
||||
) -> Any:
|
||||
"""
|
||||
分享工作流
|
||||
@@ -119,7 +120,7 @@ async def workflow_share(
|
||||
|
||||
@router.delete("/share/{share_id}", summary="删除分享", response_model=_SchemaResponse[None])
|
||||
async def workflow_share_delete(
|
||||
share_id: int, _: User = Depends(get_current_active_manage_user_async)
|
||||
share_id: int, _: Any = Depends(get_current_active_manage_user_async)
|
||||
) -> Any:
|
||||
"""
|
||||
删除分享
|
||||
@@ -132,7 +133,7 @@ async def workflow_share_delete(
|
||||
async def workflow_fork(
|
||||
workflow: _SchemaWorkflowShare,
|
||||
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
复用工作流
|
||||
@@ -148,7 +149,7 @@ async def workflow_shares(
|
||||
name: Optional[str] = None,
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
查询分享的工作流
|
||||
@@ -162,7 +163,7 @@ async def workflow_shares(
|
||||
def run_workflow(
|
||||
workflow_id: int,
|
||||
from_begin: Optional[bool] = True,
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: Any = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
执行工作流
|
||||
@@ -179,7 +180,7 @@ def run_workflow(
|
||||
def start_workflow(
|
||||
workflow_id: int,
|
||||
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: Any = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
启用工作流
|
||||
@@ -194,7 +195,7 @@ def start_workflow(
|
||||
def pause_workflow(
|
||||
workflow_id: int,
|
||||
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: Any = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
停用工作流
|
||||
@@ -209,7 +210,7 @@ def pause_workflow(
|
||||
async def reset_workflow(
|
||||
workflow_id: int,
|
||||
command: WorkflowDefinitionCommand = Depends(get_workflow_definition_command),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
重置工作流
|
||||
@@ -221,20 +222,20 @@ async def reset_workflow(
|
||||
@router.get("/{workflow_id}", summary="工作流详情", response_model=_SchemaWorkflow)
|
||||
async def get_workflow(
|
||||
workflow_id: int,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
_: User = Depends(get_current_active_manage_user_async),
|
||||
query: WorkflowQueryService = Depends(get_workflow_query_service),
|
||||
_: Any = Depends(get_current_active_manage_user_async),
|
||||
) -> Any:
|
||||
"""
|
||||
获取工作流详情
|
||||
"""
|
||||
return await WorkflowOper(db).async_get(workflow_id)
|
||||
return await query.get(workflow_id)
|
||||
|
||||
|
||||
@router.put("/{workflow_id}", summary="更新工作流", response_model=_SchemaResponse[None])
|
||||
def update_workflow(
|
||||
workflow: _SchemaWorkflow,
|
||||
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: Any = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
更新工作流
|
||||
@@ -247,7 +248,7 @@ def update_workflow(
|
||||
def delete_workflow(
|
||||
workflow_id: int,
|
||||
command: WorkflowMutationCommand = Depends(get_workflow_mutation_command),
|
||||
_: User = Depends(get_current_active_manage_user),
|
||||
_: Any = Depends(get_current_active_manage_user),
|
||||
) -> Any:
|
||||
"""
|
||||
删除工作流
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
"""API 鉴权依赖向端点暴露的最小当前用户契约。"""
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class ApiPrincipal(Protocol):
|
||||
"""隔离端点身份判断与 ORM User 实现。"""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
is_superuser: bool
|
||||
|
||||
|
||||
__all__ = ["ApiPrincipal"]
|
||||
+72
-37
@@ -1,8 +1,6 @@
|
||||
from typing import List, Optional, Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.schemas.response import Response as _SchemaResponse
|
||||
from app.schemas.servarr import RadarrMovie as _SchemaRadarrMovie
|
||||
@@ -19,9 +17,9 @@ from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.tvdb import TvdbChain
|
||||
from app.domain.context import MediaInfo
|
||||
from app.domain.metainfo import MetaInfo
|
||||
from app.application.security.access import verify_apikey
|
||||
from app.db import get_db, get_async_db
|
||||
from app.db.models.subscribe import Subscribe
|
||||
from app.application.servarr import ServarrSubscription, ServarrSubscriptionService
|
||||
from app.adapters.web.security.access import verify_apikey
|
||||
from app.api.deps import get_servarr_subscription_service
|
||||
from app.schemas.servarr import RadarrMovie
|
||||
from app.schemas.servarr import SonarrSeries
|
||||
from app.schemas.types import MediaSource, MediaType
|
||||
@@ -30,7 +28,7 @@ from version import APP_VERSION
|
||||
arr_router = APIRouter(tags=["servarr"], responses=ERROR_RESPONSES)
|
||||
|
||||
|
||||
def _subscribe_tmdb_id(subscribe: Subscribe) -> int | None:
|
||||
def _subscribe_tmdb_id(subscribe: ServarrSubscription) -> int | None:
|
||||
"""将通用订阅身份投影为 Servarr 固定使用的 TMDB ID。"""
|
||||
if (
|
||||
subscribe.media_source == MediaSource.TMDB.value
|
||||
@@ -220,7 +218,11 @@ async def arr_languageprofile(
|
||||
"/movie", summary="所有订阅电影", response_model=List[_SchemaRadarrMovie]
|
||||
)
|
||||
async def arr_movies(
|
||||
_: Annotated[str, Depends(verify_apikey)], db: AsyncSession = Depends(get_async_db)
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> List[_SchemaRadarrMovie]:
|
||||
"""
|
||||
查询Rardar电影
|
||||
@@ -292,7 +294,7 @@ async def arr_movies(
|
||||
"""
|
||||
# 查询所有电影订阅
|
||||
result = []
|
||||
subscribes = await Subscribe.async_list(db)
|
||||
subscribes = await subscriptions.list()
|
||||
for subscribe in subscribes:
|
||||
if subscribe.type != MediaType.MOVIE.value:
|
||||
continue
|
||||
@@ -316,7 +318,12 @@ async def arr_movies(
|
||||
"/movie/lookup", summary="查询电影", response_model=List[_SchemaRadarrMovie]
|
||||
)
|
||||
def arr_movie_lookup(
|
||||
term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)
|
||||
term: str,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> List[_SchemaRadarrMovie]:
|
||||
"""
|
||||
查询Rardar电影 term: `tmdb:${id}`
|
||||
@@ -340,8 +347,9 @@ def arr_movie_lookup(
|
||||
# 文件存在
|
||||
hasfile = True
|
||||
# 查询是否已订阅
|
||||
subscribes = Subscribe.list_by_media_identity(
|
||||
db, MediaSource.TMDB.value, tmdbid
|
||||
subscribes = subscriptions.list_by_media_identity_sync(
|
||||
MediaSource.TMDB,
|
||||
tmdbid,
|
||||
)
|
||||
if subscribes:
|
||||
# 订阅ID
|
||||
@@ -376,12 +384,15 @@ def arr_movie_lookup(
|
||||
async def arr_movie(
|
||||
mid: int,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaRadarrMovie:
|
||||
"""
|
||||
查询Rardar电影订阅
|
||||
"""
|
||||
subscribe = await Subscribe.async_get(db, mid)
|
||||
subscribe = await subscriptions.get(mid)
|
||||
if subscribe:
|
||||
return RadarrMovie(
|
||||
id=subscribe.id,
|
||||
@@ -404,14 +415,18 @@ async def arr_movie(
|
||||
async def arr_add_movie(
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
movie: RadarrMovie,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaServarrIdResponse:
|
||||
"""
|
||||
新增Rardar电影订阅
|
||||
"""
|
||||
# 检查订阅是否已存在
|
||||
subscribes = await Subscribe.async_list_by_media_identity(
|
||||
db, MediaSource.TMDB.value, str(movie.tmdbId)
|
||||
subscribes = await subscriptions.list_by_media_identity(
|
||||
MediaSource.TMDB,
|
||||
str(movie.tmdbId),
|
||||
)
|
||||
if subscribes:
|
||||
return _SchemaServarrIdResponse(id=subscribes[0].id)
|
||||
@@ -436,14 +451,15 @@ async def arr_add_movie(
|
||||
async def arr_remove_movie(
|
||||
mid: int,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaResponse[None]:
|
||||
"""
|
||||
删除Rardar电影订阅
|
||||
"""
|
||||
subscribe = await Subscribe.async_get(db, mid)
|
||||
if subscribe:
|
||||
await subscribe.async_delete(db, mid)
|
||||
if await subscriptions.delete(mid):
|
||||
return _SchemaResponse(success=True)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="未找到该电影!")
|
||||
@@ -453,7 +469,11 @@ async def arr_remove_movie(
|
||||
"/series", summary="所有剧集", response_model=List[_SchemaSonarrSeries]
|
||||
)
|
||||
async def arr_series(
|
||||
_: Annotated[str, Depends(verify_apikey)], db: AsyncSession = Depends(get_async_db)
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> List[_SchemaSonarrSeries]:
|
||||
"""
|
||||
查询Sonarr剧集
|
||||
@@ -562,7 +582,7 @@ async def arr_series(
|
||||
"""
|
||||
# 查询所有电视剧订阅
|
||||
result = []
|
||||
subscribes = await Subscribe.async_list(db)
|
||||
subscribes = await subscriptions.list()
|
||||
for subscribe in subscribes:
|
||||
if subscribe.type != MediaType.TV.value:
|
||||
continue
|
||||
@@ -597,7 +617,12 @@ async def arr_series(
|
||||
response_model=List[_SchemaSonarrSeries],
|
||||
)
|
||||
def arr_series_lookup(
|
||||
term: str, _: Annotated[str, Depends(verify_apikey)], db: Session = Depends(get_db)
|
||||
term: str,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> List[_SchemaSonarrSeries]:
|
||||
"""
|
||||
查询Sonarr剧集 term: `tvdb:${id}` title
|
||||
@@ -651,8 +676,9 @@ def arr_series_lookup(
|
||||
|
||||
# 查询订阅信息
|
||||
seasons: List[dict] = []
|
||||
subscribes = Subscribe.list_by_media_identity(
|
||||
db, MediaSource.TMDB.value, str(mediainfo.tmdb_id)
|
||||
subscribes = subscriptions.list_by_media_identity_sync(
|
||||
MediaSource.TMDB,
|
||||
str(mediainfo.tmdb_id),
|
||||
)
|
||||
if subscribes:
|
||||
# 已监控
|
||||
@@ -711,12 +737,15 @@ def arr_series_lookup(
|
||||
async def arr_serie(
|
||||
tid: int,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaSonarrSeries:
|
||||
"""
|
||||
查询Sonarr剧集
|
||||
"""
|
||||
subscribe = await Subscribe.async_get(db, tid)
|
||||
subscribe = await subscriptions.get(tid)
|
||||
if subscribe:
|
||||
return SonarrSeries(
|
||||
id=subscribe.id,
|
||||
@@ -748,7 +777,10 @@ async def arr_serie(
|
||||
async def arr_add_series(
|
||||
tv: _SchemaSonarrSeries,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaServarrIdResponse:
|
||||
"""
|
||||
新增Sonarr剧集订阅
|
||||
@@ -788,9 +820,8 @@ async def arr_add_series(
|
||||
for season, monitored in seasons:
|
||||
if not monitored:
|
||||
continue
|
||||
subscribe = await Subscribe.async_exists(
|
||||
db,
|
||||
media_source=MediaSource.TMDB.value,
|
||||
subscribe = await subscriptions.exists(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id=str(tv.tmdbId),
|
||||
season=season,
|
||||
)
|
||||
@@ -826,12 +857,15 @@ async def arr_add_series(
|
||||
async def arr_update_series(
|
||||
tv: _SchemaSonarrSeries,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaServarrIdResponse:
|
||||
"""
|
||||
更新Sonarr剧集订阅
|
||||
"""
|
||||
return await arr_add_series(tv=tv, _=_, db=db)
|
||||
return await arr_add_series(tv=tv, _=_, subscriptions=subscriptions)
|
||||
|
||||
|
||||
@arr_router.delete(
|
||||
@@ -840,14 +874,15 @@ async def arr_update_series(
|
||||
async def arr_remove_series(
|
||||
tid: int,
|
||||
_: Annotated[str, Depends(verify_apikey)],
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
subscriptions: Annotated[
|
||||
ServarrSubscriptionService,
|
||||
Depends(get_servarr_subscription_service),
|
||||
],
|
||||
) -> _SchemaResponse[None]:
|
||||
"""
|
||||
删除Sonarr剧集订阅
|
||||
"""
|
||||
subscribe = await Subscribe.async_get(db, tid)
|
||||
if subscribe:
|
||||
await subscribe.async_delete(db, tid)
|
||||
if await subscriptions.delete(tid):
|
||||
return _SchemaResponse(success=True)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="未找到该电视剧!")
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Agent 编排和工具使用的数据端口组合根注册表。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
AgentDataFactory = Callable[[], Any]
|
||||
|
||||
|
||||
class _PortMeta(type):
|
||||
"""支持旧测试按 Oper 方法打桩的端口代理元类。"""
|
||||
|
||||
def __getattr__(cls, name: str) -> Any:
|
||||
"""把类级方法访问转发到当前配置端口。"""
|
||||
return getattr(cls(), name)
|
||||
|
||||
|
||||
class _PortProxy(metaclass=_PortMeta):
|
||||
"""将存量 Oper 调用形态转发到 Agent 数据端口。"""
|
||||
|
||||
port_name: str
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""转发未被测试替换的实例方法。"""
|
||||
ports = get_agent_data_ports()
|
||||
return getattr(getattr(ports, self.port_name)(), name)
|
||||
|
||||
|
||||
class AgentChatPort(_PortProxy):
|
||||
"""Agent 会话数据端口代理。"""
|
||||
|
||||
port_name = "agent_chat"
|
||||
|
||||
|
||||
class AgentTaskPort(_PortProxy):
|
||||
"""Agent 定时任务数据端口代理。"""
|
||||
|
||||
port_name = "agent_task"
|
||||
|
||||
|
||||
class UserPort(_PortProxy):
|
||||
"""用户数据端口代理。"""
|
||||
|
||||
port_name = "user"
|
||||
|
||||
|
||||
class SitePort(_PortProxy):
|
||||
"""站点数据端口代理。"""
|
||||
|
||||
port_name = "site"
|
||||
|
||||
|
||||
class SubscribePort(_PortProxy):
|
||||
"""订阅数据端口代理。"""
|
||||
|
||||
port_name = "subscribe"
|
||||
|
||||
|
||||
class SubscribeHistoryPort(_PortProxy):
|
||||
"""订阅历史数据端口代理。"""
|
||||
|
||||
port_name = "subscribe_history"
|
||||
|
||||
|
||||
class TransferHistoryPort(_PortProxy):
|
||||
"""整理历史数据端口代理。"""
|
||||
|
||||
port_name = "transfer_history"
|
||||
|
||||
|
||||
class DownloadHistoryPort(_PortProxy):
|
||||
"""下载历史数据端口代理。"""
|
||||
|
||||
port_name = "download_history"
|
||||
|
||||
|
||||
class WorkflowPort(_PortProxy):
|
||||
"""工作流数据端口代理。"""
|
||||
|
||||
port_name = "workflow"
|
||||
|
||||
|
||||
class PluginDataPort(_PortProxy):
|
||||
"""插件数据端口代理。"""
|
||||
|
||||
port_name = "plugin_data"
|
||||
|
||||
|
||||
class AgentDataPorts:
|
||||
"""Agent 入口所需的持久化端口集合。"""
|
||||
|
||||
def __init__(self, **factories: AgentDataFactory) -> None:
|
||||
"""保存各数据能力的工厂。"""
|
||||
self.__dict__.update(factories)
|
||||
|
||||
|
||||
_ports: AgentDataPorts | None = None
|
||||
|
||||
|
||||
def configure_agent_data_ports(**factories: AgentDataFactory) -> None:
|
||||
"""由启动组合根登记 Agent 数据端口实现。"""
|
||||
required = {
|
||||
"agent_chat",
|
||||
"agent_task",
|
||||
"user",
|
||||
"site",
|
||||
"subscribe",
|
||||
"subscribe_history",
|
||||
"transfer_history",
|
||||
"download_history",
|
||||
"workflow",
|
||||
"plugin_data",
|
||||
}
|
||||
missing = sorted(required - factories.keys())
|
||||
if missing:
|
||||
raise ValueError(f"Agent 数据端口缺少实现: {', '.join(missing)}")
|
||||
global _ports
|
||||
_ports = AgentDataPorts(**{name: factories[name] for name in required})
|
||||
|
||||
|
||||
def get_agent_data_ports() -> AgentDataPorts:
|
||||
"""返回已登记的 Agent 数据端口。"""
|
||||
if _ports is None:
|
||||
raise RuntimeError("Agent 数据端口尚未配置")
|
||||
return _ports
|
||||
@@ -6,15 +6,11 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.application.messaging.message import MessageHelper, MessageQueueManager
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.runtime.cache import AsyncFileCache, FileCache
|
||||
from app.runtime.events import EventManager
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.plugin_manager import PluginManager
|
||||
from app.application.chain.data import ChainDataPorts
|
||||
|
||||
|
||||
MessageQueueFactory = Callable[[Callable[..., Any]], Any]
|
||||
ModuleDispatcherFactory = Callable[..., Any]
|
||||
ChainRuntimeContextProvider = Callable[[], "ChainRuntimeContext"]
|
||||
|
||||
|
||||
@@ -30,33 +26,24 @@ class ChainRuntimeContext:
|
||||
file_cache: Any
|
||||
async_file_cache: Any
|
||||
message_queue_factory: MessageQueueFactory
|
||||
module_dispatcher_factory: ModuleDispatcherFactory
|
||||
data_ports: Optional[ChainDataPorts] = None
|
||||
|
||||
|
||||
def build_default_chain_runtime_context() -> ChainRuntimeContext:
|
||||
"""按旧构造规则创建上下文,同时复用各管理器既有单例身份。"""
|
||||
return ChainRuntimeContext(
|
||||
module_manager=ModuleManager(),
|
||||
plugin_manager=PluginManager(),
|
||||
event_manager=EventManager(),
|
||||
message_oper=MessageOper(),
|
||||
message_helper=MessageHelper(),
|
||||
file_cache=FileCache(),
|
||||
async_file_cache=AsyncFileCache(),
|
||||
message_queue_factory=lambda callback: MessageQueueManager(
|
||||
send_callback=callback
|
||||
),
|
||||
)
|
||||
def _unconfigured_chain_runtime_context() -> ChainRuntimeContext:
|
||||
"""拒绝在组合根装配前隐式抓取全局管理器。"""
|
||||
raise RuntimeError("Chain 运行上下文尚未由启动组合根配置")
|
||||
|
||||
|
||||
_context_provider: ChainRuntimeContextProvider = build_default_chain_runtime_context
|
||||
_context_provider: ChainRuntimeContextProvider = _unconfigured_chain_runtime_context
|
||||
|
||||
|
||||
def configure_chain_runtime_context_provider(
|
||||
provider: Optional[ChainRuntimeContextProvider],
|
||||
) -> None:
|
||||
"""由组合根替换 Chain 上下文来源;传入空值恢复兼容默认值。"""
|
||||
"""由组合根替换 Chain 上下文来源;传入空值恢复未配置状态。"""
|
||||
global _context_provider
|
||||
_context_provider = provider or build_default_chain_runtime_context
|
||||
_context_provider = provider or _unconfigured_chain_runtime_context
|
||||
|
||||
|
||||
def get_chain_runtime_context() -> ChainRuntimeContext:
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Chain 所需持久化端口的组合根注册表。
|
||||
|
||||
Chain 只依赖本模块声明的工厂,不再直接导入数据库 Oper 或 ORM 模型。
|
||||
具体适配器由 ``app.startup`` 在进程启动时装配,测试也可以登记隔离替身。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
OperFactory = Callable[[], Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ChainDataPorts:
|
||||
"""跨领域 Chain 使用的最小持久化端口工厂集合。"""
|
||||
|
||||
site: OperFactory
|
||||
subscribe: OperFactory
|
||||
workflow: OperFactory
|
||||
download_history: OperFactory
|
||||
transfer_history: OperFactory
|
||||
transfer_pending: OperFactory
|
||||
media_server: OperFactory
|
||||
download_failure: OperFactory
|
||||
user: OperFactory
|
||||
|
||||
|
||||
class _PortProxyMeta(type):
|
||||
"""让迁移期的 Oper 名称支持按方法打桩,同时仍转发到组合根端口。"""
|
||||
|
||||
def __getattr__(cls, name: str) -> Any:
|
||||
"""把类级方法访问转发到一个新的端口实例。"""
|
||||
return getattr(cls(), name)
|
||||
|
||||
|
||||
class _ChainDataPortProxy(metaclass=_PortProxyMeta):
|
||||
"""将旧的 Oper 调用形态转发到 Chain 数据端口的内部代理。"""
|
||||
|
||||
port_name: str
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""转发未被测试替换的数据操作。"""
|
||||
return getattr(getattr(get_chain_data_ports(), self.port_name)(), name)
|
||||
|
||||
|
||||
class SitePortProxy(_ChainDataPortProxy):
|
||||
"""站点数据端口代理。"""
|
||||
|
||||
port_name = "site"
|
||||
|
||||
|
||||
class SubscribePortProxy(_ChainDataPortProxy):
|
||||
"""订阅数据端口代理。"""
|
||||
|
||||
port_name = "subscribe"
|
||||
|
||||
|
||||
class WorkflowPortProxy(_ChainDataPortProxy):
|
||||
"""工作流数据端口代理。"""
|
||||
|
||||
port_name = "workflow"
|
||||
|
||||
|
||||
class DownloadHistoryPortProxy(_ChainDataPortProxy):
|
||||
"""下载历史数据端口代理。"""
|
||||
|
||||
port_name = "download_history"
|
||||
|
||||
|
||||
class TransferHistoryPortProxy(_ChainDataPortProxy):
|
||||
"""整理历史数据端口代理。"""
|
||||
|
||||
port_name = "transfer_history"
|
||||
|
||||
|
||||
class TransferPendingPortProxy(_ChainDataPortProxy):
|
||||
"""待整理数据端口代理。"""
|
||||
|
||||
port_name = "transfer_pending"
|
||||
|
||||
|
||||
class MediaServerPortProxy(_ChainDataPortProxy):
|
||||
"""媒体服务器数据端口代理。"""
|
||||
|
||||
port_name = "media_server"
|
||||
|
||||
|
||||
class DownloadFailurePortProxy(_ChainDataPortProxy):
|
||||
"""下载失败数据端口代理。"""
|
||||
|
||||
port_name = "download_failure"
|
||||
|
||||
|
||||
class UserPortProxy(_ChainDataPortProxy):
|
||||
"""用户数据端口代理。"""
|
||||
|
||||
port_name = "user"
|
||||
|
||||
|
||||
_ports: Optional[ChainDataPorts] = None
|
||||
|
||||
|
||||
def configure_chain_data_ports(**factories: OperFactory) -> None:
|
||||
"""由启动组合根登记 Chain 的数据端口实现。"""
|
||||
required = {
|
||||
"site",
|
||||
"subscribe",
|
||||
"workflow",
|
||||
"download_history",
|
||||
"transfer_history",
|
||||
"transfer_pending",
|
||||
"media_server",
|
||||
"download_failure",
|
||||
"user",
|
||||
}
|
||||
missing = sorted(required - factories.keys())
|
||||
if missing:
|
||||
raise ValueError(f"Chain 数据端口缺少实现: {', '.join(missing)}")
|
||||
global _ports
|
||||
_ports = ChainDataPorts(**{name: factories[name] for name in required})
|
||||
|
||||
|
||||
def get_chain_data_ports() -> ChainDataPorts:
|
||||
"""返回启动阶段登记的 Chain 数据端口。"""
|
||||
if _ports is None:
|
||||
raise RuntimeError("Chain 数据端口尚未配置")
|
||||
return _ports
|
||||
|
||||
|
||||
def get_chain_site_port() -> Any:
|
||||
"""创建站点数据端口实例。"""
|
||||
return get_chain_data_ports().site()
|
||||
|
||||
|
||||
def get_chain_subscribe_port() -> Any:
|
||||
"""创建订阅数据端口实例。"""
|
||||
return get_chain_data_ports().subscribe()
|
||||
|
||||
|
||||
def get_chain_workflow_port() -> Any:
|
||||
"""创建工作流数据端口实例。"""
|
||||
return get_chain_data_ports().workflow()
|
||||
|
||||
|
||||
def get_chain_download_history_port() -> Any:
|
||||
"""创建下载历史数据端口实例。"""
|
||||
return get_chain_data_ports().download_history()
|
||||
|
||||
|
||||
def get_chain_transfer_history_port() -> Any:
|
||||
"""创建整理历史数据端口实例。"""
|
||||
return get_chain_data_ports().transfer_history()
|
||||
|
||||
|
||||
def get_chain_transfer_pending_port() -> Any:
|
||||
"""创建待整理数据端口实例。"""
|
||||
return get_chain_data_ports().transfer_pending()
|
||||
|
||||
|
||||
def get_chain_media_server_port() -> Any:
|
||||
"""创建媒体服务器数据端口实例。"""
|
||||
return get_chain_data_ports().media_server()
|
||||
|
||||
|
||||
def get_chain_download_failure_port() -> Any:
|
||||
"""创建下载失败数据端口实例。"""
|
||||
return get_chain_data_ports().download_failure()
|
||||
|
||||
|
||||
def get_chain_user_port() -> Any:
|
||||
"""创建用户数据端口实例。"""
|
||||
return get_chain_data_ports().user()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""系统配置应用服务与组合根注入点。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
|
||||
class ConfigurationRepository(Protocol):
|
||||
"""配置服务所需的最小持久化端口。"""
|
||||
|
||||
def get(self, key: Any = None) -> Any:
|
||||
"""读取配置。"""
|
||||
|
||||
def set(self, key: Any, value: Any) -> bool | None:
|
||||
"""写入配置。"""
|
||||
|
||||
async def async_get(self, key: Any = None) -> Any:
|
||||
"""异步读取配置。"""
|
||||
|
||||
async def async_set(self, key: Any, value: Any) -> bool | None:
|
||||
"""异步写入配置。"""
|
||||
|
||||
def delete(self, key: Any) -> Any:
|
||||
"""删除配置。"""
|
||||
|
||||
|
||||
class SystemConfigService:
|
||||
"""系统配置读写应用服务。"""
|
||||
|
||||
def __init__(self, repository: ConfigurationRepository) -> None:
|
||||
"""注入配置数据端口。"""
|
||||
self._repository = repository
|
||||
|
||||
def get(self, key: Any = None) -> Any:
|
||||
"""读取配置。"""
|
||||
return self._repository.get(key)
|
||||
|
||||
def set(self, key: Any, value: Any) -> bool | None:
|
||||
"""写入配置。"""
|
||||
return self._repository.set(key, value)
|
||||
|
||||
async def async_get(self, key: Any = None) -> Any:
|
||||
"""异步读取配置。"""
|
||||
return await self._repository.async_get(key)
|
||||
|
||||
async def async_set(self, key: Any, value: Any) -> bool | None:
|
||||
"""异步写入配置。"""
|
||||
return await self._repository.async_set(key, value)
|
||||
|
||||
def delete(self, key: Any) -> Any:
|
||||
"""删除配置。"""
|
||||
return self._repository.delete(key)
|
||||
|
||||
|
||||
_configured_system_config: SystemConfigService | None = None
|
||||
|
||||
|
||||
def configure_system_config(service: SystemConfigService) -> None:
|
||||
"""由启动组合根登记系统配置服务。"""
|
||||
global _configured_system_config
|
||||
_configured_system_config = service
|
||||
|
||||
|
||||
def get_configured_system_config() -> SystemConfigService:
|
||||
"""返回启动阶段登记的系统配置服务。"""
|
||||
if _configured_system_config is None:
|
||||
raise RuntimeError("系统配置服务尚未配置")
|
||||
return _configured_system_config
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Dashboard 统计查询用例。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.schemas.dashboard import Statistic
|
||||
|
||||
|
||||
class TransferHistoryQueryRepository(Protocol):
|
||||
"""Dashboard 所需的整理历史统计端口。"""
|
||||
|
||||
def monthly_media_statistics(self) -> tuple[int, int, int, int]:
|
||||
"""返回本月电影、剧集、单集和音乐数量。"""
|
||||
...
|
||||
|
||||
async def async_statistic(self, days: int = 7) -> list[Any]:
|
||||
"""返回最近若干天的整理趋势。"""
|
||||
...
|
||||
|
||||
|
||||
class DashboardQueryService:
|
||||
"""汇总媒体服务数据与整理历史统计。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
repository: TransferHistoryQueryRepository,
|
||||
media_statistics: Callable[[Optional[str]], Optional[list[Statistic]]],
|
||||
) -> None:
|
||||
"""保存整理历史端口和媒体服务统计提供方。"""
|
||||
self._repository = repository
|
||||
self._media_statistics = media_statistics
|
||||
|
||||
def statistic(self, name: Optional[str] = None) -> Statistic:
|
||||
"""返回媒体服务总量和本月新增量。"""
|
||||
media_statistics = self._media_statistics(name)
|
||||
if media_statistics:
|
||||
result = Statistic()
|
||||
has_episode_count = False
|
||||
for item in media_statistics:
|
||||
result.movie_count += item.movie_count or 0
|
||||
result.tv_count += item.tv_count or 0
|
||||
result.music_count += item.music_count or 0
|
||||
result.user_count += item.user_count or 0
|
||||
if item.episode_count is not None:
|
||||
result.episode_count += item.episode_count or 0
|
||||
has_episode_count = True
|
||||
if not has_episode_count:
|
||||
result.episode_count = None
|
||||
else:
|
||||
result = Statistic()
|
||||
|
||||
(
|
||||
result.movie_count_month,
|
||||
result.tv_count_month,
|
||||
result.episode_count_month,
|
||||
result.music_count_month,
|
||||
) = self._repository.monthly_media_statistics()
|
||||
return result
|
||||
|
||||
async def transfer(self, days: int = 7) -> list[int]:
|
||||
"""返回最近若干天的整理数量序列。"""
|
||||
rows = await self._repository.async_statistic(days)
|
||||
return [row[1] for row in rows]
|
||||
@@ -0,0 +1,37 @@
|
||||
"""数据库连通性应用服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Optional
|
||||
|
||||
|
||||
DatabaseProbe = Callable[[], Optional[str]]
|
||||
|
||||
|
||||
class DatabaseHealthService:
|
||||
"""为模块和诊断入口提供不暴露会话实现的数据库探测能力。"""
|
||||
|
||||
def __init__(self, probe: DatabaseProbe) -> None:
|
||||
"""保存由组合根提供的数据库探测端口。"""
|
||||
self._probe = probe
|
||||
|
||||
def test(self) -> Optional[str]:
|
||||
"""执行数据库探测,成功返回空值,失败返回说明。"""
|
||||
return self._probe()
|
||||
|
||||
|
||||
_configured_database_health: DatabaseHealthService | None = None
|
||||
|
||||
|
||||
def configure_database_health(service: DatabaseHealthService) -> None:
|
||||
"""由启动组合根登记数据库探测服务。"""
|
||||
global _configured_database_health
|
||||
_configured_database_health = service
|
||||
|
||||
|
||||
def get_configured_database_health() -> DatabaseHealthService:
|
||||
"""返回启动阶段登记的数据库探测服务。"""
|
||||
if _configured_database_health is None:
|
||||
raise RuntimeError("数据库探测服务尚未配置")
|
||||
return _configured_database_health
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional, Tuple
|
||||
from app.schemas.file import FileURI as _SchemaFileURI
|
||||
from app.schemas.system import TransferDirectoryConf as _SchemaTransferDirectoryConf
|
||||
from app.domain.context import MediaInfo
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.configuration import get_configured_system_config
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import MediaType, StorageSchema, SystemConfigKey
|
||||
from app.adapters.system.host import SystemUtils
|
||||
@@ -25,7 +25,7 @@ class DirectoryHelper:
|
||||
"""
|
||||
获取所有下载目录
|
||||
"""
|
||||
dir_confs: List[dict] = SystemConfigOper().get(SystemConfigKey.Directories)
|
||||
dir_confs: List[dict] = get_configured_system_config().get(SystemConfigKey.Directories)
|
||||
if not dir_confs:
|
||||
return []
|
||||
return [_SchemaTransferDirectoryConf(**d) for d in dir_confs]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from app.runtime.extensions.service_registry import ServiceBaseHelper
|
||||
from app.application.service import ServiceBaseHelper
|
||||
from app.schemas.system import DownloaderConf
|
||||
from app.schemas.system import ServiceInfo
|
||||
from app.schemas.types import SystemConfigKey, ModuleType
|
||||
|
||||
+282
-15
@@ -6,11 +6,15 @@ from app.domain.context import MediaInfo, MusicInfo
|
||||
from app.schemas.media import resolve_media_identity
|
||||
from app.domain.meta.metabase import MetaBase
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
from app.foundation.text import cut as jieba_cut
|
||||
from app.runtime.cache import TTLCache
|
||||
from app.runtime.config import settings
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.history import (
|
||||
DownloadHistory as DownloadHistoryView,
|
||||
TransferHistory as TransferHistoryView,
|
||||
TransferHistoryPage,
|
||||
)
|
||||
from app.schemas.workflow import FileItem
|
||||
from app.schemas.transfer import TransferInfo
|
||||
from app.schemas.types import MUSIC_ENTITY_RECORDING
|
||||
@@ -28,6 +32,59 @@ FAILED_RETRY_TTL = 24 * 3600
|
||||
_failed_retry_counts = TTLCache(region="transfer_failed_retry", maxsize=5000, ttl=FAILED_RETRY_TTL)
|
||||
|
||||
|
||||
class TransferHistoryRecord(Protocol):
|
||||
"""整理历史用例读取的最小记录投影。"""
|
||||
|
||||
id: int
|
||||
status: bool
|
||||
src: Optional[str]
|
||||
src_storage: Optional[str]
|
||||
src_fileitem: Optional[dict]
|
||||
|
||||
|
||||
class TransferHistoryWriter(Protocol):
|
||||
"""整理历史写入和查重端口。"""
|
||||
|
||||
def get_by_src(self, src: str, storage: Optional[str] = None) -> Optional[TransferHistoryRecord]:
|
||||
"""按源路径读取记录。"""
|
||||
|
||||
def get_success_by_src(self, src: str, storage: Optional[str] = None) -> Optional[TransferHistoryRecord]:
|
||||
"""按源路径读取成功记录。"""
|
||||
|
||||
def add_force(self, **payload: Any) -> Optional[TransferHistoryRecord]:
|
||||
"""强制写入整理历史。"""
|
||||
|
||||
|
||||
_configured_transfer_history_provider: Callable[[], TransferHistoryWriter] | None = None
|
||||
|
||||
|
||||
def configure_transfer_history_provider(
|
||||
provider: Callable[[], TransferHistoryWriter],
|
||||
) -> None:
|
||||
"""由启动组合根登记整理历史数据端口提供器。"""
|
||||
global _configured_transfer_history_provider
|
||||
_configured_transfer_history_provider = provider
|
||||
|
||||
|
||||
def _get_transfer_history_writer(
|
||||
writer: Optional[TransferHistoryWriter],
|
||||
) -> TransferHistoryWriter:
|
||||
"""获取显式传入或组合根登记的整理历史数据端口。"""
|
||||
if writer is not None:
|
||||
return writer
|
||||
if _configured_transfer_history_provider is None:
|
||||
raise RuntimeError("整理历史数据端口尚未配置")
|
||||
return _configured_transfer_history_provider()
|
||||
|
||||
|
||||
class TransferHistoryPort:
|
||||
"""把监控等宿主用例的存量构造形态转发到整理历史端口。"""
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""转发整理历史读写方法,避免上层直接导入数据库操作器。"""
|
||||
return getattr(_get_transfer_history_writer(None), name)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HistoryMutationResult:
|
||||
"""描述历史记录维护操作是否成功及兼容提示。"""
|
||||
@@ -36,6 +93,216 @@ class HistoryMutationResult:
|
||||
message: str = ""
|
||||
|
||||
|
||||
class AsyncDownloadHistoryQueryRepository(Protocol):
|
||||
"""下载历史只读用例需要的最小异步持久化端口。"""
|
||||
|
||||
async def async_list_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
) -> list[Any]:
|
||||
"""按下载时间倒序分页读取历史记录。"""
|
||||
...
|
||||
|
||||
|
||||
class AsyncTransferHistoryQueryRepository(Protocol):
|
||||
"""整理历史列表和详情查询需要的最小异步持久化端口。"""
|
||||
|
||||
async def async_get(self, historyid: int) -> Optional[Any]:
|
||||
"""按主键读取单条整理历史。"""
|
||||
...
|
||||
|
||||
async def async_list_by_title(
|
||||
self,
|
||||
title: str,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: Optional[bool] = None,
|
||||
wildcard: bool = False,
|
||||
) -> list[Any]:
|
||||
"""按标题或路径分页读取整理历史。"""
|
||||
...
|
||||
|
||||
async def async_list_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: Optional[bool] = None,
|
||||
) -> list[Any]:
|
||||
"""按时间倒序分页读取整理历史。"""
|
||||
...
|
||||
|
||||
async def async_count(self, status: Optional[bool] = None) -> Optional[int]:
|
||||
"""统计指定状态的整理历史数量。"""
|
||||
...
|
||||
|
||||
async def async_count_by_title(
|
||||
self,
|
||||
title: str,
|
||||
status: Optional[bool] = None,
|
||||
wildcard: bool = False,
|
||||
) -> Optional[int]:
|
||||
"""统计匹配标题或路径的整理历史数量。"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManualTransferHistory:
|
||||
"""手动整理准备阶段需要的稳定历史投影。"""
|
||||
|
||||
id: int
|
||||
status: bool
|
||||
mode: Optional[str]
|
||||
src_fileitem: Optional[dict]
|
||||
dest_fileitem: Optional[dict]
|
||||
downloader: Optional[str]
|
||||
download_hash: Optional[str]
|
||||
type: Optional[str]
|
||||
media_source: Optional[str]
|
||||
media_id: Optional[str]
|
||||
music_type: Optional[str]
|
||||
seasons: Optional[str]
|
||||
episodes: Optional[str]
|
||||
episode_group: Optional[str]
|
||||
|
||||
|
||||
class TransferHistoryLookupRepository(Protocol):
|
||||
"""手动整理历史投影所需的同步查询端口。"""
|
||||
|
||||
def get(self, history_id: int) -> Optional[Any]:
|
||||
"""按主键读取整理历史。"""
|
||||
...
|
||||
|
||||
|
||||
class TransferHistoryLookupService:
|
||||
"""向同步整理用例提供脱离 ORM 会话的历史投影。"""
|
||||
|
||||
def __init__(self, repository: TransferHistoryLookupRepository) -> None:
|
||||
"""保存整理历史只读端口。"""
|
||||
self._repository = repository
|
||||
|
||||
def get(self, history_id: int) -> Optional[ManualTransferHistory]:
|
||||
"""按主键读取手动整理所需字段。"""
|
||||
record = self._repository.get(history_id)
|
||||
if record is None:
|
||||
return None
|
||||
return ManualTransferHistory(
|
||||
id=record.id,
|
||||
status=bool(record.status),
|
||||
mode=record.mode,
|
||||
src_fileitem=record.src_fileitem,
|
||||
dest_fileitem=record.dest_fileitem,
|
||||
downloader=record.downloader,
|
||||
download_hash=record.download_hash,
|
||||
type=record.type,
|
||||
media_source=record.media_source,
|
||||
media_id=record.media_id,
|
||||
music_type=getattr(record, "music_type", None),
|
||||
seasons=record.seasons,
|
||||
episodes=record.episodes,
|
||||
episode_group=record.episode_group,
|
||||
)
|
||||
|
||||
|
||||
class HistoryQueryService:
|
||||
"""提供历史列表和详情 DTO,隔离 API 与数据库模型。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
download_repository: AsyncDownloadHistoryQueryRepository,
|
||||
transfer_repository: AsyncTransferHistoryQueryRepository,
|
||||
) -> None:
|
||||
"""保存下载历史和整理历史的只读端口。"""
|
||||
self._download_repository = download_repository
|
||||
self._transfer_repository = transfer_repository
|
||||
|
||||
async def list_download(
|
||||
self,
|
||||
*,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
) -> list[DownloadHistoryView]:
|
||||
"""分页读取下载历史并转换为稳定的接口 DTO。"""
|
||||
records = await self._download_repository.async_list_by_page(page, count)
|
||||
return [DownloadHistoryView.model_validate(record) for record in records]
|
||||
|
||||
async def list_transfer(
|
||||
self,
|
||||
*,
|
||||
title: Optional[str] = None,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
status: Optional[bool] = None,
|
||||
) -> TransferHistoryPage:
|
||||
"""应用历史筛选规则并返回整理历史分页 DTO。"""
|
||||
if title == "失败":
|
||||
title = None
|
||||
status = False
|
||||
elif title == "成功":
|
||||
title = None
|
||||
status = True
|
||||
|
||||
if title:
|
||||
wildcard = "*" in title or "?" in title
|
||||
if wildcard:
|
||||
pattern = self._glob_to_like(title)
|
||||
else:
|
||||
pattern = "%".join(jieba_cut(title, HMM=False))
|
||||
total = await self._transfer_repository.async_count_by_title(
|
||||
pattern,
|
||||
status=status,
|
||||
wildcard=wildcard,
|
||||
)
|
||||
records = await self._transfer_repository.async_list_by_title(
|
||||
pattern,
|
||||
page=page,
|
||||
count=count,
|
||||
status=status,
|
||||
wildcard=wildcard,
|
||||
)
|
||||
else:
|
||||
records = await self._transfer_repository.async_list_by_page(
|
||||
page=page,
|
||||
count=count,
|
||||
status=status,
|
||||
)
|
||||
total = await self._transfer_repository.async_count(status=status)
|
||||
|
||||
return TransferHistoryPage(
|
||||
list=[TransferHistoryView.model_validate(record) for record in records],
|
||||
total=int(total or 0),
|
||||
)
|
||||
|
||||
async def get_transfer(self, history_id: int) -> Optional[TransferHistoryView]:
|
||||
"""读取单条整理历史 DTO,不向调用方泄漏 ORM 实例。"""
|
||||
record = await self._transfer_repository.async_get(history_id)
|
||||
if record is None:
|
||||
return None
|
||||
return TransferHistoryView.model_validate(record)
|
||||
|
||||
async def get_transfers(
|
||||
self,
|
||||
history_ids: list[int],
|
||||
) -> tuple[list[TransferHistoryView], list[int]]:
|
||||
"""按输入顺序读取多条整理历史,并同时返回缺失 ID。"""
|
||||
records: list[TransferHistoryView] = []
|
||||
missing_ids: list[int] = []
|
||||
for history_id in history_ids:
|
||||
record = await self.get_transfer(history_id)
|
||||
if record is None:
|
||||
missing_ids.append(history_id)
|
||||
else:
|
||||
records.append(record)
|
||||
return records, missing_ids
|
||||
|
||||
@staticmethod
|
||||
def _glob_to_like(pattern: str) -> str:
|
||||
"""将 glob 通配符转换为使用反斜杠转义的 SQL LIKE 模式。"""
|
||||
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
return result.replace("*", "%").replace("?", "_")
|
||||
|
||||
|
||||
class DownloadHistoryMutationRepository(Protocol):
|
||||
"""下载历史删除用例需要的最小持久化端口。"""
|
||||
|
||||
@@ -447,7 +714,7 @@ def coerce_size(size: Any) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def history_src_size(history: TransferHistory) -> Optional[int]:
|
||||
def history_src_size(history: TransferHistoryRecord) -> Optional[int]:
|
||||
"""
|
||||
读取整理记录中的源文件大小。
|
||||
src_fileitem 是 JSON 列,历史数据可能为空、缺 size 键甚至不是字典,
|
||||
@@ -458,7 +725,7 @@ def history_src_size(history: TransferHistory) -> Optional[int]:
|
||||
return history_src_fingerprint(history).get("size")
|
||||
|
||||
|
||||
def history_src_fingerprint(history: TransferHistory) -> Dict[str, Any]:
|
||||
def history_src_fingerprint(history: TransferHistoryRecord) -> Dict[str, Any]:
|
||||
"""
|
||||
读取整理记录中的源文件版本指纹。
|
||||
:param history: 整理记录
|
||||
@@ -475,8 +742,8 @@ def history_src_fingerprint(history: TransferHistory) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def resolve_history(src_path: str, storage: Optional[str] = None,
|
||||
transfer_history_oper: Optional[TransferHistoryOper] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
transfer_history_oper: Optional[TransferHistoryWriter] = None
|
||||
) -> Optional[TransferHistoryRecord]:
|
||||
"""
|
||||
查询源路径对应的整理记录。
|
||||
|
||||
@@ -488,14 +755,14 @@ def resolve_history(src_path: str, storage: Optional[str] = None,
|
||||
:param transfer_history_oper: 复用的历史操作对象,未传时新建
|
||||
:return: 命中的整理记录,未命中时为 None
|
||||
"""
|
||||
oper = transfer_history_oper or TransferHistoryOper()
|
||||
oper = _get_transfer_history_writer(transfer_history_oper)
|
||||
history = oper.get_by_src(src_path, storage=storage)
|
||||
if history is not None and not history.status:
|
||||
history = oper.get_success_by_src(src_path, storage=storage) or history
|
||||
return history
|
||||
|
||||
|
||||
def evaluate_history_gate(history: Optional[TransferHistory],
|
||||
def evaluate_history_gate(history: Optional[TransferHistoryRecord],
|
||||
file_size: Optional[float] = None,
|
||||
file_modify_time: Optional[float] = None,
|
||||
fileid: Optional[str] = None,
|
||||
@@ -547,7 +814,7 @@ def evaluate_history_gate(history: Optional[TransferHistory],
|
||||
return HistoryGateAction.SKIP
|
||||
|
||||
|
||||
def describe_history_gate(history: Optional[TransferHistory],
|
||||
def describe_history_gate(history: Optional[TransferHistoryRecord],
|
||||
file_size: Optional[float] = None,
|
||||
file_modify_time: Optional[float] = None,
|
||||
fileid: Optional[str] = None) -> str:
|
||||
@@ -609,8 +876,8 @@ def add_transfer_success(fileitem: FileItem, mode: str, meta: MetaBase,
|
||||
mediainfo: Union[MediaInfo, MusicInfo], transferinfo: TransferInfo,
|
||||
downloader: Optional[str] = None,
|
||||
download_hash: Optional[str] = None,
|
||||
transfer_history_oper: Optional[TransferHistoryOper] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
transfer_history_oper: Optional[TransferHistoryWriter] = None
|
||||
) -> Optional[TransferHistoryRecord]:
|
||||
"""
|
||||
新增转移成功历史记录。
|
||||
:param fileitem: 源文件项
|
||||
@@ -623,7 +890,7 @@ def add_transfer_success(fileitem: FileItem, mode: str, meta: MetaBase,
|
||||
:param transfer_history_oper: 复用的历史操作对象,未传时新建
|
||||
:return: 落库后的整理记录
|
||||
"""
|
||||
oper = transfer_history_oper or TransferHistoryOper()
|
||||
oper = _get_transfer_history_writer(transfer_history_oper)
|
||||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||||
return oper.add_force(
|
||||
src=fileitem.path,
|
||||
@@ -661,8 +928,8 @@ def add_transfer_fail(fileitem: FileItem, mode: str, meta: MetaBase,
|
||||
transferinfo: Optional[TransferInfo] = None,
|
||||
downloader: Optional[str] = None,
|
||||
download_hash: Optional[str] = None,
|
||||
transfer_history_oper: Optional[TransferHistoryOper] = None
|
||||
) -> Optional[TransferHistory]:
|
||||
transfer_history_oper: Optional[TransferHistoryWriter] = None
|
||||
) -> Optional[TransferHistoryRecord]:
|
||||
"""
|
||||
新增转移失败历史记录。
|
||||
|
||||
@@ -678,7 +945,7 @@ def add_transfer_fail(fileitem: FileItem, mode: str, meta: MetaBase,
|
||||
:param transfer_history_oper: 复用的历史操作对象,未传时新建
|
||||
:return: 落库后的整理记录
|
||||
"""
|
||||
oper = transfer_history_oper or TransferHistoryOper()
|
||||
oper = _get_transfer_history_writer(transfer_history_oper)
|
||||
if mediainfo and transferinfo:
|
||||
media_source, media_id = resolve_media_identity(media=mediainfo)
|
||||
his = oper.add_force(
|
||||
|
||||
@@ -9,8 +9,6 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, ContextManager, Dict, Optional, Protocol
|
||||
|
||||
from app.db.maintenance import DatabaseCleanupRepository
|
||||
from app.db.session import SessionFactory
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.log import logger
|
||||
|
||||
@@ -327,11 +325,21 @@ def read_cleanup_policy() -> CleanupPolicy:
|
||||
|
||||
|
||||
def build_cleanup_service() -> DataCleanupService:
|
||||
"""在应用边界组装默认数据库适配器,供兼容调度门面触发。"""
|
||||
return DataCleanupService(
|
||||
repository=DatabaseCleanupRepository(session_factory=SessionFactory),
|
||||
policy_reader=read_cleanup_policy,
|
||||
)
|
||||
"""返回启动组合根登记的清理服务。"""
|
||||
if _configured_cleanup_service_factory is None:
|
||||
raise RuntimeError("数据清理服务尚未配置")
|
||||
return _configured_cleanup_service_factory()
|
||||
|
||||
|
||||
_configured_cleanup_service_factory: Callable[[], DataCleanupService] | None = None
|
||||
|
||||
|
||||
def configure_cleanup_service_factory(
|
||||
factory: Callable[[], DataCleanupService],
|
||||
) -> None:
|
||||
"""由启动组合根登记数据清理服务工厂。"""
|
||||
global _configured_cleanup_service_factory
|
||||
_configured_cleanup_service_factory = factory
|
||||
|
||||
|
||||
def _normalize_days(retention_days: Any) -> int:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import re
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.schemas.mediaserver import MediaServerItem as _SchemaMediaServerItem
|
||||
from app.domain.context import MusicInfo
|
||||
from app.schemas.media import normalize_media_source, resolve_media_identity
|
||||
from app.runtime.extensions.service_registry import ServiceBaseHelper
|
||||
from app.application.service import ServiceBaseHelper
|
||||
from app.schemas.system import MediaServerConf
|
||||
from app.schemas.system import ServiceInfo
|
||||
from app.schemas.types import (
|
||||
@@ -16,6 +16,43 @@ from app.schemas.types import (
|
||||
)
|
||||
|
||||
|
||||
class AsyncMediaServerQueryRepository(Protocol):
|
||||
"""媒体服务器本地条目查询所需的异步持久化端口。"""
|
||||
|
||||
async def async_exists(self, **kwargs: Any) -> Any | None:
|
||||
"""按标题或统一媒体身份查找已同步条目。"""
|
||||
...
|
||||
|
||||
|
||||
class MediaServerQueryService:
|
||||
"""封装媒体服务器本地存在性查询与 ORM 投影。"""
|
||||
|
||||
def __init__(self, repository: AsyncMediaServerQueryRepository):
|
||||
"""使用显式媒体服务器查询端口初始化服务。"""
|
||||
self._repository = repository
|
||||
|
||||
async def find_item_id(
|
||||
self,
|
||||
*,
|
||||
title: Optional[str] = None,
|
||||
year: Optional[str] = None,
|
||||
mtype: Optional[str] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
media_id: Optional[str] = None,
|
||||
season: Optional[int] = None,
|
||||
) -> Optional[str]:
|
||||
"""返回匹配条目的服务器 item_id,未命中时返回 None。"""
|
||||
item = await self._repository.async_exists(
|
||||
title=title,
|
||||
year=year,
|
||||
mtype=mtype,
|
||||
media_source=media_source,
|
||||
media_id=media_id,
|
||||
season=season,
|
||||
)
|
||||
return item.item_id if item else None
|
||||
|
||||
|
||||
class MediaServerIdentityHelper:
|
||||
"""将媒体服务器专有 ProviderIds 适配为统一媒体身份。"""
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"""Agent 会话历史的查询、授权与删除应用服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from app.schemas.agent import AgentChatSessionDetail, AgentChatSessionSummary
|
||||
|
||||
|
||||
class AgentChatPrincipal(Protocol):
|
||||
"""会话访问控制所需的最小用户身份。"""
|
||||
|
||||
id: Any
|
||||
name: Optional[str]
|
||||
is_superuser: bool
|
||||
|
||||
|
||||
class AsyncAgentChatRepository(Protocol):
|
||||
"""Agent 会话用例需要的最小异步持久化端口。"""
|
||||
|
||||
async def async_list_by_page(
|
||||
self,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
user_id: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
) -> list[Any]:
|
||||
"""分页读取用户可见的会话。"""
|
||||
...
|
||||
|
||||
async def async_get(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> Optional[Any]:
|
||||
"""按服务端会话 ID 读取记录。"""
|
||||
...
|
||||
|
||||
async def async_delete(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""删除指定服务端会话。"""
|
||||
...
|
||||
|
||||
def get(self, session_id: str, user_id: Optional[str] = None) -> Optional[Any]:
|
||||
"""同步读取服务端会话。"""
|
||||
...
|
||||
|
||||
def save_display_messages(
|
||||
self,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
messages: Optional[list[dict]] = None,
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Any] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> Optional[Any]:
|
||||
"""同步保存用户可见会话消息。"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AgentChatRecord:
|
||||
"""脱离 ORM 会话的 Agent 会话持久化投影。"""
|
||||
|
||||
id: Optional[int]
|
||||
session_id: str
|
||||
client_session_id: Optional[str]
|
||||
title: Optional[str]
|
||||
channel: Optional[str]
|
||||
source: Optional[str]
|
||||
user_id: Optional[str]
|
||||
username: Optional[str]
|
||||
original_chat_id: Optional[str]
|
||||
message_count: int
|
||||
created_at: Any
|
||||
updated_at: Any
|
||||
messages: list[dict]
|
||||
|
||||
|
||||
class AgentChatService:
|
||||
"""统一执行 Agent 会话查询、访问控制和删除。"""
|
||||
|
||||
def __init__(self, repository: AsyncAgentChatRepository) -> None:
|
||||
"""保存异步会话持久化端口。"""
|
||||
self._repository = repository
|
||||
|
||||
async def list(
|
||||
self,
|
||||
principal: AgentChatPrincipal,
|
||||
*,
|
||||
page: int = 1,
|
||||
count: int = 30,
|
||||
) -> list[AgentChatSessionSummary]:
|
||||
"""分页返回当前用户可见的会话摘要。"""
|
||||
user_id = None if principal.is_superuser else str(principal.id)
|
||||
username = None if principal.is_superuser else principal.name
|
||||
records = await self._repository.async_list_by_page(
|
||||
page=page,
|
||||
count=count,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
)
|
||||
return [self.to_summary(self._project(record)) for record in records]
|
||||
|
||||
async def get_accessible(
|
||||
self,
|
||||
session_id: str,
|
||||
principal: AgentChatPrincipal,
|
||||
) -> Optional[AgentChatRecord]:
|
||||
"""读取会话并在应用边界执行访问控制。"""
|
||||
projected = await self.get(session_id)
|
||||
if projected is None:
|
||||
return None
|
||||
if not self.can_access(projected, principal):
|
||||
return None
|
||||
return projected
|
||||
|
||||
async def get(self, session_id: str) -> Optional[AgentChatRecord]:
|
||||
"""读取不附带授权判断的会话投影。"""
|
||||
record = await self._repository.async_get(session_id=session_id)
|
||||
if record is None:
|
||||
return None
|
||||
return self._project(record)
|
||||
|
||||
async def delete(
|
||||
self,
|
||||
session_id: str,
|
||||
principal: AgentChatPrincipal,
|
||||
) -> bool:
|
||||
"""仅在当前用户可访问时删除会话。"""
|
||||
record = await self.get_accessible(session_id, principal)
|
||||
if record is None:
|
||||
return False
|
||||
return await self._repository.async_delete(session_id=session_id)
|
||||
|
||||
def get_sync(self, session_id: str) -> Optional[AgentChatRecord]:
|
||||
"""同步读取会话投影,供同步 Agent 编排路径使用。"""
|
||||
record = self._repository.get(session_id=session_id)
|
||||
return self._project(record) if record is not None else None
|
||||
|
||||
def save_display_sync(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
messages: Optional[list[dict]] = None,
|
||||
username: Optional[str] = None,
|
||||
channel: Optional[Any] = None,
|
||||
source: Optional[str] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
client_session_id: Optional[str] = None,
|
||||
) -> Optional[AgentChatRecord]:
|
||||
"""同步保存用户可见消息并返回最新投影。"""
|
||||
record = self._repository.save_display_messages(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
messages=messages,
|
||||
username=username,
|
||||
channel=channel,
|
||||
source=source,
|
||||
original_chat_id=original_chat_id,
|
||||
client_session_id=client_session_id,
|
||||
)
|
||||
return self._project(record) if record is not None else None
|
||||
|
||||
@staticmethod
|
||||
def can_access(
|
||||
record: AgentChatRecord,
|
||||
principal: AgentChatPrincipal,
|
||||
) -> bool:
|
||||
"""判断用户是否拥有会话访问权。"""
|
||||
if principal.is_superuser:
|
||||
return True
|
||||
user_id = str(principal.id)
|
||||
username = str(principal.name or "")
|
||||
return record.user_id == user_id or (
|
||||
bool(username) and record.username == username
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_summary(record: AgentChatRecord) -> AgentChatSessionSummary:
|
||||
"""把持久化投影转换为会话摘要 DTO。"""
|
||||
return AgentChatSessionSummary(
|
||||
id=record.id,
|
||||
session_id=record.session_id,
|
||||
client_session_id=record.client_session_id,
|
||||
title=record.title,
|
||||
channel=record.channel,
|
||||
source=record.source,
|
||||
user_id=record.user_id,
|
||||
username=record.username,
|
||||
original_chat_id=record.original_chat_id,
|
||||
message_count=record.message_count,
|
||||
created_at=record.created_at,
|
||||
updated_at=record.updated_at,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def to_detail(cls, record: AgentChatRecord) -> AgentChatSessionDetail:
|
||||
"""把持久化投影转换为会话详情 DTO。"""
|
||||
return AgentChatSessionDetail(
|
||||
**cls.to_summary(record).model_dump(),
|
||||
messages=record.messages,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _project(record: Any) -> AgentChatRecord:
|
||||
"""立即复制 ORM 字段,避免对象越过请求级会话边界。"""
|
||||
return AgentChatRecord(
|
||||
id=record.id,
|
||||
session_id=record.session_id,
|
||||
client_session_id=record.client_session_id,
|
||||
title=record.title,
|
||||
channel=record.channel,
|
||||
source=record.source,
|
||||
user_id=record.user_id,
|
||||
username=record.username,
|
||||
original_chat_id=record.original_chat_id,
|
||||
message_count=record.message_count or 0,
|
||||
created_at=record.created_at,
|
||||
updated_at=record.updated_at,
|
||||
messages=list(record.display_messages or []),
|
||||
)
|
||||
|
||||
|
||||
_configured_agent_chat_service: AgentChatService | None = None
|
||||
|
||||
|
||||
def configure_agent_chat_service(service: AgentChatService) -> None:
|
||||
"""由启动组合根登记同步 Agent 会话服务。"""
|
||||
global _configured_agent_chat_service
|
||||
_configured_agent_chat_service = service
|
||||
|
||||
|
||||
def get_configured_agent_chat_service() -> AgentChatService:
|
||||
"""返回启动阶段登记的 Agent 会话服务。"""
|
||||
if _configured_agent_chat_service is None:
|
||||
raise RuntimeError("Agent 会话服务尚未配置")
|
||||
return _configured_agent_chat_service
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user