refactor: split api dependencies by domain

This commit is contained in:
jxxghp
2026-08-21 21:09:57 +08:00
parent 773ea8cb9b
commit 9df599f502
40 changed files with 1044 additions and 735 deletions
+1
View File
@@ -0,0 +1 @@
"""按业务领域拆分的 FastAPI 依赖工厂。"""
+29
View File
@@ -0,0 +1,29 @@
"""Agent 与消息查询依赖。"""
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.context import get_agent_chat_repository, get_agent_chat_transaction
from app.api.data import get_async_db
from app.api.dependencies.data import repository
from app.application.messaging.chat import (
AgentChatService,
AsyncAgentChatRepository,
AsyncUnitOfWork,
)
from app.application.messaging.message import MessageQueryService
def get_agent_chat_service(
chat_repository: AsyncAgentChatRepository = Depends(get_agent_chat_repository),
unit_of_work: AsyncUnitOfWork = Depends(get_agent_chat_transaction),
) -> AgentChatService:
"""组装类型化 Agent 会话历史查询和删除服务。"""
return AgentChatService(chat_repository, unit_of_work)
def get_message_query_service(
db: AsyncSession = Depends(get_async_db),
) -> MessageQueryService:
"""组装消息历史异步查询服务。"""
return MessageQueryService(repository=repository("message", db))
+116
View File
@@ -0,0 +1,116 @@
"""用户身份、授权与认证服务依赖。"""
from typing import Any
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.web.security.access import verify_token
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, standalone_repository
from app.application.security.auth import AuthService
from app.application.security.passkeys import PasskeyService
from app.application.security.user import UserService
from app.schemas.token import TokenPayload as _SchemaTokenPayload
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_current_user(
db: Session = Depends(get_db),
token_data: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""读取令牌对应用户,不存在时返回 403。"""
user = repository("user", db).get_by_id(token_data.sub)
if not user:
raise HTTPException(status_code=403, detail="用户不存在")
return user
async def get_current_user_async(
db: AsyncSession = Depends(get_async_db),
token_data: _SchemaTokenPayload = Depends(verify_token),
) -> Any:
"""异步读取令牌对应用户,不存在时返回 403。"""
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: Any = Depends(get_current_user),
) -> Any:
"""校验并返回当前激活用户。"""
if not current_user.is_active:
raise HTTPException(status_code=403, detail="用户未激活")
return current_user
async def get_current_active_user_async(
current_user: Any = Depends(get_current_user_async),
) -> Any:
"""异步校验并返回当前激活用户。"""
if not current_user.is_active:
raise HTTPException(status_code=403, detail="用户未激活")
return current_user
def _ensure_manage_user(current_user: Any) -> Any:
"""校验用户具备全局管理权限。"""
permissions = current_user.permissions or {}
if not current_user.is_superuser and not bool(permissions.get("manage")):
raise HTTPException(status_code=400, detail="用户权限不足")
return current_user
def get_current_active_manage_user(
current_user: Any = Depends(get_current_active_user),
) -> Any:
"""返回当前拥有管理权限的激活用户。"""
return _ensure_manage_user(current_user)
async def get_current_active_manage_user_async(
current_user: Any = Depends(get_current_active_user_async),
) -> Any:
"""异步返回当前拥有管理权限的激活用户。"""
return _ensure_manage_user(current_user)
def get_current_active_superuser(
current_user: Any = Depends(get_current_user),
) -> Any:
"""校验并返回当前激活超级管理员。"""
if not current_user.is_superuser:
raise HTTPException(status_code=400, detail="用户权限不足")
return current_user
async def get_current_active_superuser_async(
current_user: Any = Depends(get_current_user_async),
) -> Any:
"""异步校验并返回当前激活超级管理员。"""
if not current_user.is_superuser:
raise HTTPException(status_code=400, detail="用户权限不足")
return current_user
+20
View File
@@ -0,0 +1,20 @@
"""未迁移领域共用的 API 数据兼容 Facade。"""
from typing import Any
from app.api.data import get_api_data_ports
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)
+86
View File
@@ -0,0 +1,86 @@
"""历史、媒体服务器与 Dashboard 查询依赖。"""
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.application.dashboard import DashboardQueryService
from app.application.history import (
DownloadHistoryMutationCommand,
HistoryQueryService,
TransferHistoryLookupService,
TransferHistoryMutationCommand,
clear_transfer_failures,
)
from app.application.mediaserver import MediaServerQueryService
from app.chain.storage import StorageChain
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.schemas.workflow import FileItem as _SchemaFileItem
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=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=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(
EventType.DownloadFileDeleted,
payload,
),
clear_failures=clear_transfer_failures,
)
+43
View File
@@ -0,0 +1,43 @@
"""插件配置与运行态刷新依赖。"""
from app.application.commands import init_commands
from app.application.plugin.config import PluginConfigCommand
from app.application.plugin.routes import register_plugin_api
from app.application.plugin.runtime import get_plugin_manager
from app.application.scheduling import update_plugin_job
from app.runtime.events import eventmanager
from app.schemas.event import PluginDataResetEventData
from app.schemas.types import ChainEventType
def get_plugin_config_command() -> PluginConfigCommand:
"""组装插件配置更新与重置用例,隔离 API 对运行时写操作的编排。"""
manager = get_plugin_manager()
def publish_reset(plugin_id: str) -> None:
"""在清理持久化数据前通知目标插件执行补偿。"""
eventmanager.send_event(
ChainEventType.PluginDataReset,
PluginDataResetEventData(
plugin_id=plugin_id,
reset_config=True,
reset_data=True,
),
)
def refresh_registrations(plugin_id: str) -> None:
"""按服务、命令、动态路由顺序刷新插件宿主注册。"""
update_plugin_job(plugin_id)
init_commands(plugin_id)
register_plugin_api(plugin_id)
return PluginConfigCommand(
save_config=manager.save_plugin_config,
initialize=manager.init_plugin,
stop=manager.stop,
delete_config=manager.delete_plugin_config,
delete_data=manager.delete_plugin_data,
reload_runtime=manager.reload_plugin,
publish_reset=publish_reset,
refresh_registrations=refresh_registrations,
)
+62
View File
@@ -0,0 +1,62 @@
"""站点领域的请求级 command/query 依赖。"""
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.application.site.mutation import SiteMutationCommand
from app.application.site.query import SiteQueryService
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.runtime.events import eventmanager
from app.schemas.types import EventType
async def _publish_site_updated(payload: dict) -> None:
"""发布已提交的站点更新事件。"""
await eventmanager.async_send_event(EventType.SiteUpdated, payload)
async def _publish_site_deleted(payload: dict) -> None:
"""发布已提交的站点删除事件。"""
await eventmanager.async_send_event(EventType.SiteDeleted, payload)
def get_site_mutation_command(
db: AsyncSession = Depends(get_async_db),
) -> SiteMutationCommand:
"""组装请求级站点写用例及其事务和外部目录依赖。"""
sites_helper = SitesHelper()
def normalize_url(value: str) -> str:
"""沿用站点接口的 scheme/netloc 规范化格式。"""
scheme, netloc = url_tools.split_netloc(value)
return f"{scheme}://{netloc}/"
return SiteMutationCommand(
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,
url_normalizer=normalize_url,
publish_updated=_publish_site_updated,
publish_deleted=_publish_site_deleted,
)
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))
+125
View File
@@ -0,0 +1,125 @@
"""订阅领域的请求级 command/query 依赖。"""
from fastapi import BackgroundTasks, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, transaction
from app.application.scheduling import Scheduler
from app.application.servarr import ServarrSubscriptionService
from app.application.subscription.delete import DeleteSubscribeCommand
from app.application.subscription.identity import DeleteSubscriptionsByIdentityCommand
from app.application.subscription.mutation import SubscriptionMutationService
from app.application.subscription.query import SubscriptionQueryService
from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager
from app.runtime.log import logger
from app.schemas.types import EventType
async def _publish_subscribe_deleted(
subscribe_id: int,
subscribe_info: dict,
) -> None:
"""通过宿主事件总线发布已提交的订阅删除事件。"""
await eventmanager.async_send_event(
EventType.SubscribeDeleted,
{"subscribe_id": subscribe_id, "subscribe_info": subscribe_info},
)
def get_delete_subscribe_command(
db: AsyncSession = Depends(get_async_db),
) -> DeleteSubscribeCommand:
"""组装请求级订阅删除用例及其具体适配器。"""
return DeleteSubscribeCommand(
repository=repository("subscribe", db),
unit_of_work=transaction("async", db),
publish_deleted=_publish_subscribe_deleted,
report_deleted=MoviePilotServerHelper.sub_done_async,
)
def _log_subscribe_deleted_event_error(
subscribe_id: int,
error: Exception,
) -> None:
"""记录按媒体身份删除时的单条事件失败并允许后续事件继续。"""
logger.error(
f"发送订阅删除事件失败:{subscribe_id} - {error}",
exc_info=True,
)
def get_delete_subscriptions_by_identity_command(
db: AsyncSession = Depends(get_async_db),
) -> DeleteSubscriptionsByIdentityCommand:
"""组装请求级按媒体身份删除订阅用例。"""
return DeleteSubscriptionsByIdentityCommand(
repository=repository("subscribe", db),
unit_of_work=transaction("async", db),
publish_deleted=_publish_subscribe_deleted,
handle_event_error=_log_subscribe_deleted_event_error,
)
def get_search_subscriptions_command(
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_async_db),
) -> SearchSubscriptionsCommand:
"""组装手工订阅搜索用例,并把调度延迟到响应后的后台任务。"""
def schedule_search(subscribe_id: int | None, state: str | None) -> None:
"""按历史参数提交订阅搜索调度任务。"""
background_tasks.add_task(
Scheduler().start,
job_id="subscribe_search",
sid=subscribe_id,
state=state,
manual=True,
)
return SearchSubscriptionsCommand(
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_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),
)
+60
View File
@@ -0,0 +1,60 @@
"""工作流领域的请求级 command/query 依赖。"""
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.adapters.external.server import MoviePilotServerHelper
from app.api.data import get_async_db, get_db
from app.api.dependencies.data import repository, standalone_repository, transaction
from app.application.scheduling import Scheduler
from app.application.workflow import (
WorkflowDefinitionCommand,
WorkflowMutationCommand,
WorkflowQueryService,
)
from app.runtime.config import global_vars
from app.workflow import WorkFlowManager
def get_workflow_mutation_command(
db: Session = Depends(get_db),
) -> WorkflowMutationCommand:
"""组装请求级工作流写用例和提交后的调度副作用。"""
scheduler = Scheduler()
workflow_manager = WorkFlowManager()
return WorkflowMutationCommand(
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: standalone_repository(
"system_config"
).delete(f"WorkflowCache-{workflow_id}"),
)
def get_workflow_definition_command(
db: AsyncSession = Depends(get_async_db),
) -> WorkflowDefinitionCommand:
"""组装工作流创建、复用和重置的异步写用例。"""
return WorkflowDefinitionCommand(
repository=repository("workflow", db),
unit_of_work=transaction("async", db),
stop_running=global_vars.stop_workflow,
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))
+81 -517
View File
@@ -1,524 +1,88 @@
"""FastAPI 依赖兼容聚合入口。
新代码按领域从 ``app.api.dependencies`` 导入;本模块保留全部历史名字,避免端点、测试和
旧 SDK 在依赖拆分期间同步改动导入路径。
"""
API 层的公共依赖。
这些是 FastAPI 的路由依赖:从令牌解出用户、校验激活状态与权限,失败一律以
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
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.application.subscription.delete import DeleteSubscribeCommand
from app.application.subscription.identity import (
DeleteSubscriptionsByIdentityCommand,
from app.api.dependencies.agent import (
get_agent_chat_service,
get_message_query_service,
)
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.api.dependencies.auth import (
get_auth_service,
get_current_active_manage_user,
get_current_active_manage_user_async,
get_current_active_superuser,
get_current_active_superuser_async,
get_current_active_user,
get_current_active_user_async,
get_current_user,
get_current_user_async,
get_passkey_service,
get_user_service,
)
from app.application.messaging.message import MessageQueryService
from app.application.messaging.chat import AgentChatService
from app.api.context import (
get_agent_chat_repository,
get_agent_chat_transaction,
from app.api.dependencies.history import (
get_dashboard_query_service,
get_download_history_mutation_command,
get_history_query_service,
get_mediaserver_query_service,
get_transfer_history_lookup_service,
get_transfer_history_mutation_command,
)
from app.application.messaging.chat import (
AsyncAgentChatRepository,
AsyncUnitOfWork as AgentChatUnitOfWork,
from app.api.dependencies.plugin import get_plugin_config_command
from app.api.dependencies.site import (
get_site_mutation_command,
get_site_query_service,
get_site_sync_query_service,
)
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,
from app.api.dependencies.subscription import (
get_delete_subscribe_command,
get_delete_subscriptions_by_identity_command,
get_search_subscriptions_command,
get_servarr_subscription_service,
get_subscription_mutation_service,
get_subscription_query_service,
get_subscription_sync_mutation_service,
)
from app.application.plugin.config import PluginConfigCommand
from app.application.commands import init_commands
from app.application.plugin.routes import register_plugin_api
from app.application.scheduling import update_plugin_job
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.api.data import get_api_data_ports, get_async_db, get_db
from app.runtime.events import eventmanager
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.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.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,
) -> None:
"""通过宿主事件总线发布已提交的订阅删除事件。"""
await eventmanager.async_send_event(
EventType.SubscribeDeleted,
{"subscribe_id": subscribe_id, "subscribe_info": subscribe_info},
)
def get_delete_subscribe_command(
db: AsyncSession = Depends(get_async_db),
) -> DeleteSubscribeCommand:
"""组装请求级订阅删除用例及其具体适配器。"""
return DeleteSubscribeCommand(
repository=_repository("subscribe", db),
unit_of_work=_transaction("async", db),
publish_deleted=_publish_subscribe_deleted,
report_deleted=MoviePilotServerHelper.sub_done_async,
)
def _log_subscribe_deleted_event_error(
subscribe_id: int,
error: Exception,
) -> None:
"""记录按媒体身份删除时的单条事件失败并允许后续事件继续。"""
logger.error(
f"发送订阅删除事件失败:{subscribe_id} - {error}",
exc_info=True,
)
def get_delete_subscriptions_by_identity_command(
db: AsyncSession = Depends(get_async_db),
) -> DeleteSubscriptionsByIdentityCommand:
"""组装请求级按媒体身份删除订阅用例。"""
return DeleteSubscriptionsByIdentityCommand(
repository=_repository("subscribe", db),
unit_of_work=_transaction("async", db),
publish_deleted=_publish_subscribe_deleted,
handle_event_error=_log_subscribe_deleted_event_error,
)
def get_search_subscriptions_command(
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_async_db),
) -> SearchSubscriptionsCommand:
"""组装手工订阅搜索用例,并把调度延迟到响应后的后台任务。"""
def schedule_search(subscribe_id: int | None, state: str | None) -> None:
"""按历史参数提交订阅搜索调度任务。"""
background_tasks.add_task(
Scheduler().start,
job_id="subscribe_search",
sid=subscribe_id,
state=state,
manual=True,
)
return SearchSubscriptionsCommand(
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)
async def _publish_site_deleted(payload: dict) -> None:
"""发布已提交的站点删除事件。"""
await eventmanager.async_send_event(EventType.SiteDeleted, payload)
def get_site_mutation_command(
db: AsyncSession = Depends(get_async_db),
) -> SiteMutationCommand:
"""组装请求级站点写用例及其事务和外部目录依赖。"""
sites_helper = SitesHelper()
def normalize_url(value: str) -> str:
"""沿用站点接口的 scheme/netloc 规范化格式。"""
scheme, netloc = url_tools.split_netloc(value)
return f"{scheme}://{netloc}/"
return SiteMutationCommand(
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,
url_normalizer=normalize_url,
publish_updated=_publish_site_updated,
publish_deleted=_publish_site_deleted,
)
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:
"""组装请求级工作流写用例和提交后的调度副作用。"""
scheduler = Scheduler()
workflow_manager = WorkFlowManager()
return WorkflowMutationCommand(
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: _standalone_repository("system_config").delete(
f"WorkflowCache-{workflow_id}"
),
)
def get_workflow_definition_command(
db: AsyncSession = Depends(get_async_db),
) -> WorkflowDefinitionCommand:
"""组装工作流创建、复用和重置的异步写用例。"""
return WorkflowDefinitionCommand(
repository=_repository("workflow", db),
unit_of_work=_transaction("async", db),
stop_running=global_vars.stop_workflow,
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(
repository: AsyncAgentChatRepository = Depends(get_agent_chat_repository),
unit_of_work: AgentChatUnitOfWork = Depends(get_agent_chat_transaction),
) -> AgentChatService:
"""组装 Agent 会话历史查询和删除服务。"""
return AgentChatService(
repository=repository,
unit_of_work=unit_of_work,
)
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=_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=_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(
EventType.DownloadFileDeleted,
payload,
),
clear_failures=clear_transfer_failures,
)
def get_plugin_config_command() -> PluginConfigCommand:
"""组装插件配置更新与重置用例,隔离 API 对运行时写操作的编排。"""
manager = PluginManager()
def publish_reset(plugin_id: str) -> None:
"""在清理持久化数据前通知目标插件执行补偿。"""
eventmanager.send_event(
ChainEventType.PluginDataReset,
PluginDataResetEventData(
plugin_id=plugin_id,
reset_config=True,
reset_data=True,
),
)
def refresh_registrations(plugin_id: str) -> None:
"""按服务、命令、动态路由顺序刷新插件宿主注册。"""
update_plugin_job(plugin_id)
init_commands(plugin_id)
register_plugin_api(plugin_id)
return PluginConfigCommand(
save_config=manager.save_plugin_config,
initialize=manager.init_plugin,
stop=manager.stop,
delete_config=manager.delete_plugin_config,
delete_data=manager.delete_plugin_data,
reload_runtime=manager.reload_plugin,
publish_reset=publish_reset,
refresh_registrations=refresh_registrations,
)
def get_current_user(
db: Session = Depends(get_db),
token_data: _SchemaTokenPayload = Depends(verify_token)
) -> Any:
"""
获取当前用户
"""
user = _repository("user", db).get_by_id(token_data.sub)
if not user:
raise HTTPException(status_code=403, detail="用户不存在")
return user
async def get_current_user_async(
db: AsyncSession = Depends(get_async_db),
token_data: _SchemaTokenPayload = Depends(verify_token)
) -> Any:
"""
异步获取当前用户
"""
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: Any = Depends(get_current_user),
) -> Any:
"""
获取当前激活用户
"""
if not current_user.is_active:
raise HTTPException(status_code=403, detail="用户未激活")
return current_user
async def get_current_active_user_async(
current_user: Any = Depends(get_current_user_async),
) -> Any:
"""
异步获取当前激活用户
"""
if not current_user.is_active:
raise HTTPException(status_code=403, detail="用户未激活")
return current_user
def _ensure_manage_user(current_user: Any) -> Any:
"""
校验用户具备全局管理权限。
"""
permissions = current_user.permissions or {}
if not current_user.is_superuser and not bool(permissions.get("manage")):
raise HTTPException(
status_code=400, detail="用户权限不足"
)
return current_user
def get_current_active_manage_user(
current_user: Any = Depends(get_current_active_user),
) -> Any:
"""
获取当前拥有管理权限的激活用户。
"""
return _ensure_manage_user(current_user)
async def get_current_active_manage_user_async(
current_user: Any = Depends(get_current_active_user_async),
) -> Any:
"""
异步获取当前拥有管理权限的激活用户。
"""
return _ensure_manage_user(current_user)
def get_current_active_superuser(
current_user: Any = Depends(get_current_user),
) -> Any:
"""
获取当前激活超级管理员
"""
if not current_user.is_superuser:
raise HTTPException(
status_code=400, detail="用户权限不足"
)
return current_user
async def get_current_active_superuser_async(
current_user: Any = Depends(get_current_user_async),
) -> Any:
"""
异步获取当前激活超级管理员
"""
if not current_user.is_superuser:
raise HTTPException(
status_code=400, detail="用户权限不足"
)
return current_user
from app.api.dependencies.workflow import (
get_workflow_definition_command,
get_workflow_mutation_command,
get_workflow_query_service,
)
# 兼容聚合入口只显式列出既有 FastAPI 依赖,不向插件制造新的动态导出规则。
__all__ = [
"get_agent_chat_service",
"get_auth_service",
"get_current_active_manage_user",
"get_current_active_manage_user_async",
"get_current_active_superuser",
"get_current_active_superuser_async",
"get_current_active_user",
"get_current_active_user_async",
"get_current_user",
"get_current_user_async",
"get_dashboard_query_service",
"get_delete_subscribe_command",
"get_delete_subscriptions_by_identity_command",
"get_download_history_mutation_command",
"get_history_query_service",
"get_mediaserver_query_service",
"get_message_query_service",
"get_passkey_service",
"get_plugin_config_command",
"get_search_subscriptions_command",
"get_servarr_subscription_service",
"get_site_mutation_command",
"get_site_query_service",
"get_site_sync_query_service",
"get_subscription_mutation_service",
"get_subscription_query_service",
"get_subscription_sync_mutation_service",
"get_transfer_history_lookup_service",
"get_transfer_history_mutation_command",
"get_user_service",
"get_workflow_definition_command",
"get_workflow_mutation_command",
"get_workflow_query_service",
]
+41 -78
View File
@@ -33,6 +33,7 @@ from app.schemas.message import AgentWebChoiceRequest as _SchemaAgentWebChoiceRe
from app.schemas.message import Message as _SchemaMessage
from app.schemas.response import Response as _SchemaResponse
from app.api.response import ResponseAPIRouter
from app.api.presentation.sse import build_sse_error_response, build_sse_response
from app.agent.contracts import ReplyMode, build_display_message
from app.agent.llm.capability import AgentCapabilityManager
from app.agent.mcp import agent_mcp_manager
@@ -45,7 +46,8 @@ from app.command import Command
from app.runtime.config import global_vars, settings
from app.runtime.events import Event, EventManager
from app.api.principal import ApiPrincipal
from app.api.deps import get_agent_chat_service, get_current_active_user
from app.api.dependencies.agent import get_agent_chat_service
from app.api.dependencies.auth import get_current_active_user
from app.application.messaging.chat import (
AgentChatRecord,
AgentChatService,
@@ -694,6 +696,21 @@ def _build_web_agent_sse(
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
def _build_web_agent_error_response(
message: str,
*,
locale: Optional[str],
) -> StreamingResponse:
"""Map a rejected WebAgent request to one terminal SSE error event."""
return build_sse_error_response(
_build_web_agent_sse(
"error",
{"message": message},
locale=locale,
)
)
def _sanitize_web_agent_upload_name(
filename: Optional[str], mime_type: Optional[str] = None
) -> str:
@@ -1965,15 +1982,9 @@ async def web_agent_stream(
getattr(request, "headers", {}).get("X-MoviePilot-Agent-Interaction") == "1"
)
if is_secret_confirmation_control and not protected_transport_supported:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "当前客户端不支持安全交付敏感设置,未执行操作。"},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
"当前客户端不支持安全交付敏感设置,未执行操作。",
locale=locale,
)
is_traditional_message = (
_is_web_agent_traditional_message(prompt)
@@ -1982,27 +1993,15 @@ async def web_agent_stream(
if is_traditional_message:
denied_message = _ensure_web_agent_command_allowed(current_user)
if denied_message:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": denied_message},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
denied_message,
locale=locale,
)
unknown_command_message = _get_web_agent_unknown_command_message(prompt)
if unknown_command_message:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": unknown_command_message},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
unknown_command_message,
locale=locale,
)
user_attachments = _build_web_agent_input_attachments(
@@ -2089,39 +2088,19 @@ async def web_agent_stream(
return
yield _build_web_agent_sse("done", {}, locale=locale)
return StreamingResponse(
traditional_event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
return build_sse_response(traditional_event_generator())
if not settings.AI_AGENT_ENABLE:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "智能助手未启用,请先在系统设置中开启。"},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
"智能助手未启用,请先在系统设置中开启。",
locale=locale,
)
manager = get_running_agent_manager()
if manager is None:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "智能助手服务尚未就绪,请稍后重试。"},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
"智能助手服务尚未就绪,请稍后重试。",
locale=locale,
)
transcript = _transcribe_web_agent_audio_refs(payload.audio_refs or [])
@@ -2129,26 +2108,14 @@ async def web_agent_stream(
display_prompt = _merge_web_agent_prompt_with_transcript(display_prompt, transcript)
has_audio_input = bool(transcript)
if not prompt and payload.audio_refs and not payload.images and not payload.files:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "语音识别失败,请稍后重试。"},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
"语音识别失败,请稍后重试。",
locale=locale,
)
if not prompt and not payload.images and not payload.files and not payload.audio_refs:
return StreamingResponse(
iter([
_build_web_agent_sse(
"error",
{"message": "请输入要发送给智能助手的内容或选择附件。"},
locale=locale,
)
]),
media_type="text/event-stream",
return _build_web_agent_error_response(
"请输入要发送给智能助手的内容或选择附件。",
locale=locale,
)
MessageChain().bind_user_session(str(current_user.id), session_id)
@@ -2314,13 +2281,9 @@ async def web_agent_stream(
await event_publisher.aclose()
# 客户端断线后保留 Agent 继续执行;发布器关闭后不再接受受保护结果。
return StreamingResponse(
return build_sse_response(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
**(
{"X-MoviePilot-Agent-Control": "secret-confirmation"}
if is_secret_confirmation_control
+58 -19
View File
@@ -1,10 +1,9 @@
import asyncio
import json
import uuid
from typing import AsyncIterator, List, Optional
from fastapi import APIRouter, Header, Security
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import JSONResponse
from app.schemas.openai import AnthropicErrorDetail as _SchemaAnthropicErrorDetail
from app.schemas.openai import AnthropicErrorResponse as _SchemaAnthropicErrorResponse
@@ -21,6 +20,7 @@ from app.api.openai_utils import (
build_prompt,
build_session_id,
)
from app.api.presentation.sse import build_sse_response, encode_named_event
from app.agent.runtime_loader import get_running_agent_manager
from app.runtime.config import settings
from app.adapters.web.security.access import anthropic_api_key_header
@@ -97,26 +97,71 @@ async def _stream_anthropic_response(
task = asyncio.create_task(_run_agent())
try:
yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': {'id': message_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': MODEL_ID, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}}, ensure_ascii=False)}\n\n"
yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}}, ensure_ascii=False)}\n\n"
yield encode_named_event(
"message_start",
{
"type": "message_start",
"message": {
"id": message_id,
"type": "message",
"role": "assistant",
"content": [],
"model": MODEL_ID,
"stop_reason": None,
"stop_sequence": None,
"usage": {"input_tokens": 0, "output_tokens": 0},
},
},
)
yield encode_named_event(
"content_block_start",
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "text", "text": ""},
},
)
while True:
item = await event_queue.get()
if item is None:
break
if isinstance(item, dict) and item.get("error"):
yield (
"event: error\n"
f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': str(item['error'])}}, ensure_ascii=False)}\n\n"
yield encode_named_event(
"error",
{
"type": "error",
"error": {
"type": "api_error",
"message": str(item["error"]),
},
},
)
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'}, ensure_ascii=False)}\n\n"
yield encode_named_event("message_stop", {"type": "message_stop"})
return
text = str(item or "")
if not text:
continue
yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': text}}, ensure_ascii=False)}\n\n"
yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0}, ensure_ascii=False)}\n\n"
yield f"event: message_delta\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': 0}}, ensure_ascii=False)}\n\n"
yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'}, ensure_ascii=False)}\n\n"
yield encode_named_event(
"content_block_delta",
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": text},
},
)
yield encode_named_event(
"content_block_stop",
{"type": "content_block_stop", "index": 0},
)
yield encode_named_event(
"message_delta",
{
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {"output_tokens": 0},
},
)
yield encode_named_event("message_stop", {"type": "message_stop"})
finally:
await manager.clear_session(session_id=session_id, user_id=user_id)
if not task.done():
@@ -172,7 +217,7 @@ async def messages(
session_seed = anthropic_version or "anthropic"
session_id = build_session_id(f"{session_seed}:{uuid.uuid4().hex}", SESSION_PREFIX)
if payload.stream:
return StreamingResponse(
return build_sse_response(
_stream_anthropic_response(
manager=manager,
session_id=session_id,
@@ -180,12 +225,6 @@ async def messages(
prompt=prompt,
images=images,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
collected_messages = []
+1 -1
View File
@@ -8,7 +8,7 @@ from app.schemas.user import AuthProviderInfo as _SchemaAuthProviderInfo
from app.api.response import RAW_RESPONSE_OPENAPI_KEY, ResponseAPIRouter
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
from app.api.dependencies.auth import get_auth_service
router = ResponseAPIRouter()
+2 -1
View File
@@ -18,7 +18,8 @@ from app.chain.dashboard import DashboardChain
from app.chain.storage import StorageChain
from app.runtime.config import settings
from app.adapters.web.security.access import verify_apitoken
from app.api.deps import get_current_active_superuser, get_dashboard_query_service
from app.api.dependencies.auth import get_current_active_superuser
from app.api.dependencies.history import get_dashboard_query_service
from app.application.dashboard import DashboardQueryService
from app.schemas.types import StorageAction
from app.application.directory import DirectoryHelper
+2 -1
View File
@@ -28,7 +28,8 @@ 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.api.dependencies.auth import get_current_active_user
from app.api.dependencies.site import get_site_sync_query_service
from app.application.directory import DirectoryHelper
from app.schemas.types import (
MUSIC_ENTITY_RECORDING,
+3 -1
View File
@@ -21,9 +21,11 @@ from app.agent.prompt.transfer_redo import (
)
from app.runtime.config import settings, global_vars
from app.adapters.web.security.access import verify_token
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_manage_user,
get_current_active_superuser,
)
from app.api.dependencies.history import (
get_download_history_mutation_command,
get_history_query_service,
get_transfer_history_mutation_command,
+1 -1
View File
@@ -6,7 +6,7 @@ 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.api.deps import get_current_active_superuser_async
from app.api.dependencies.auth import get_current_active_superuser_async
router = ResponseAPIRouter()
+4 -1
View File
@@ -26,7 +26,10 @@ from app.domain.meta.metabase import MetaBase
from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo, MetaInfoPath
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.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_user,
)
from app.schemas.category import CategoryConfig
from app.schemas.event import MediaSourceInfo as _SchemaMediaSourceInfo
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource, MediaType
+1 -1
View File
@@ -21,7 +21,7 @@ from app.domain.metainfo import MetaInfo
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.api.dependencies.history 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
+2 -1
View File
@@ -22,7 +22,8 @@ from app.runtime.config import settings, global_vars
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.api.dependencies.agent import get_message_query_service
from app.api.dependencies.auth import get_current_active_superuser
from app.application.messaging.message import MessageQueryService
from app.runtime.extensions.service_config import ServiceConfigHelper
from app.runtime.log import logger
+1 -1
View File
@@ -30,7 +30,7 @@ from app.application.security.passkeys import (
PasskeyService,
)
from app.api.principal import ApiPrincipal
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_user,
get_current_active_user_async,
get_user_service,
+1 -1
View File
@@ -15,7 +15,7 @@ from app.chain.recommend import RecommendChain
from app.schemas.types import MediaSource, MediaType
from app.domain.context import MusicAlbumInfo, MusicArtistInfo, MusicInfo
from app.adapters.web.security.access import verify_token
from app.api.deps import get_current_active_superuser_async
from app.api.dependencies.auth import get_current_active_superuser_async
from app.chain.listenbrainz import (
LISTENBRAINZ_CHART_RANGES,
LISTENBRAINZ_FRESH_MAX_DAYS,
+1 -1
View File
@@ -6,7 +6,7 @@ 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.api.deps import get_current_active_superuser
from app.api.dependencies.auth import get_current_active_superuser
router = ResponseAPIRouter()
+5 -10
View File
@@ -1,12 +1,11 @@
import asyncio
import json
import time
import uuid
from threading import Lock
from typing import AsyncIterator, List, Optional, Tuple
from fastapi import APIRouter, Request, Security
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import JSONResponse
from fastapi.security import HTTPAuthorizationCredentials
from app.schemas.openai import OpenAIChatCompletionResponse as _SchemaOpenAIChatCompletionResponse
@@ -26,6 +25,7 @@ from app.api.openai_utils import (
build_responses_input,
build_session_id,
)
from app.api.presentation.sse import build_sse_response, encode_data_event
from app.agent.runtime_loader import (
get_moviepilot_agent_type,
get_running_agent_manager,
@@ -216,7 +216,8 @@ def _get_collecting_agent_type() -> type:
def _sse_payload(data: dict) -> str:
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
"""保留旧测试入口并委托独立 OpenAI SSE wire mapper。"""
return encode_data_event(data)
async def _stream_response(
@@ -519,7 +520,7 @@ async def chat_completions(
session_id = build_session_id(session_key, SESSION_PREFIX)
username = str(payload.user or "openai-client")
if payload.stream:
return StreamingResponse(
return build_sse_response(
_stream_response(
manager=manager,
session_id=session_id,
@@ -529,12 +530,6 @@ async def chat_completions(
images=images,
cleanup_session=not use_server_session,
),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
collected_messages = []
+3 -1
View File
@@ -46,9 +46,11 @@ from app.adapters.web.security.access import (
)
from app.api.principal import ApiPrincipal
from app.application.configuration import get_configured_system_config
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.api.dependencies.plugin import (
get_plugin_config_command,
)
from app.adapters.external.server import MoviePilotServerHelper
+3 -1
View File
@@ -26,11 +26,13 @@ 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 (
from app.api.dependencies.auth import (
get_current_active_manage_user,
get_current_active_manage_user_async,
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.api.dependencies.site import (
get_site_mutation_command,
get_site_query_service,
get_site_sync_query_service,
+1 -1
View File
@@ -16,7 +16,7 @@ from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.runtime.config import settings
from app.api.principal import ApiPrincipal
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_manage_user,
get_current_active_superuser,
)
+3 -1
View File
@@ -36,9 +36,11 @@ from app.application.subscription.mutation import (
SubscriptionMutationService,
)
from app.application.configuration import get_configured_system_config
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_user,
get_current_active_user_async,
)
from app.api.dependencies.subscription import (
get_delete_subscribe_command,
get_delete_subscriptions_by_identity_command,
get_search_subscriptions_command,
+5 -1
View File
@@ -42,7 +42,11 @@ 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.api.dependencies.auth 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
from app.adapters.external.market import (
+1 -1
View File
@@ -14,7 +14,7 @@ from app.chain.tmdb import TmdbChain
from app.runtime.config import settings
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.api.dependencies.auth import get_current_active_superuser_async
from app.schemas.types import MediaType, SystemConfigKey
router = ResponseAPIRouter()
+4 -1
View File
@@ -12,7 +12,10 @@ 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.api.deps import get_current_active_superuser, get_current_active_superuser_async
from app.api.dependencies.auth import (
get_current_active_superuser,
get_current_active_superuser_async,
)
from app.schemas.types import (
MUSIC_ENTITY_RECORDING,
MediaSource,
+17 -5
View File
@@ -19,10 +19,8 @@ from app.chain.media import MediaChain
from app.chain.transfer import TransferChain
from app.runtime.config import settings, global_vars
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.api.dependencies.auth import get_current_active_manage_user
from app.api.dependencies.history import get_transfer_history_lookup_service
from app.application.directory import DirectoryHelper
from app.application.history import TransferHistoryLookupService
from app.runtime.log import logger
@@ -304,12 +302,26 @@ def manual_transfer(
_: object = Depends(get_current_active_manage_user),
) -> Any:
"""
手动转移文件或历史记录支持自定义剧集识别格式
解析手动整理 HTTP 请求并委托兼容用例处理器
:param transer_item: 手工整理项
:param background: 后台运行
:param history_query: 整理历史投影服务
:param _: Token校验
"""
return _execute_manual_transfer(
transer_item=transer_item,
background=background,
history_query=history_query,
)
def _execute_manual_transfer(
transer_item: ManualTransferItem,
background: Optional[bool],
history_query: TransferHistoryLookupService,
) -> Any:
"""执行历史恢复、批量预览与 TransferChain 兼容编排。"""
force = False
downloader = None
download_hash = None
+1 -1
View File
@@ -13,7 +13,7 @@ from app.schemas.user import UserUpdate as _SchemaUserUpdate
from app.api.response import ResponseAPIRouter
from app.application.security.token import PasswordTooLongError, get_password_hash
from app.application.security.user import UserService
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_superuser_async,
get_current_active_user_async,
get_current_active_user,
+3 -1
View File
@@ -17,9 +17,11 @@ from app.application.workflow import (
from app.chain.workflow import WorkflowChain
from app.application.plugin.runtime import get_plugin_manager as PluginManager
from app.workflow import WorkFlowManager
from app.api.deps import (
from app.api.dependencies.auth import (
get_current_active_manage_user,
get_current_active_manage_user_async,
)
from app.api.dependencies.workflow import (
get_workflow_definition_command,
get_workflow_mutation_command,
get_workflow_query_service,
+1
View File
@@ -0,0 +1 @@
"""API presentation helpers isolated from domain dependency assembly."""
+45
View File
@@ -0,0 +1,45 @@
"""SSE wire mapping and response lifecycle helpers."""
import json
from collections.abc import AsyncIterable, Iterable
from typing import Any
from fastapi.responses import StreamingResponse
SSE_HEADERS = {
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
}
def encode_data_event(payload: dict[str, Any]) -> str:
"""Encode an OpenAI-style unnamed SSE data event."""
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
def encode_named_event(event: str, payload: dict[str, Any]) -> str:
"""Encode a named SSE event without applying a response envelope."""
return (
f"event: {event}\n"
f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
)
def build_sse_response(
content: AsyncIterable[str] | Iterable[str],
*,
headers: dict[str, str] | None = None,
) -> StreamingResponse:
"""Build a non-buffered SSE response and merge protocol-specific headers."""
return StreamingResponse(
content,
media_type="text/event-stream",
headers={**SSE_HEADERS, **(headers or {})},
)
def build_sse_error_response(payload: str) -> StreamingResponse:
"""Build a one-event SSE error response using the common transport policy."""
return build_sse_response(iter([payload]))
+1 -1
View File
@@ -19,7 +19,7 @@ from app.domain.context import MediaInfo
from app.domain.metainfo import MetaInfo
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.api.dependencies.subscription import get_servarr_subscription_service
from app.schemas.servarr import RadarrMovie
from app.schemas.servarr import SonarrSeries
from app.schemas.types import MediaSource, MediaType
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md``docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md``docs/refactor/backend-architecture-governance.md``docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0ARCH-201203)、阶段 1ARCH-210212)、阶段 2ARCH-220222)与 ARCH-230 已完成,后续任务按 ID 独立提交和回滚
> 实施进度:阶段 0ARCH-201203)、阶段 1ARCH-210212)、阶段 2ARCH-220222)与 ARCH-230231 已完成,后续任务按 ID 独立提交和回滚
## 1. 结论先行
@@ -473,6 +473,22 @@ app/api/dependencies/ # 按领域拆分依赖工厂
**优先切片**`manual_transfer()``web_agent_stream()`、OpenAI/Anthropic streaming adapter。
**实施记录(2026-08-21**
- `app/api/deps.py` 已由 524 行集中装配点收敛为 88 行兼容聚合入口;认证、Agent、订阅、站点、
工作流、历史和插件依赖分别由 `app/api/dependencies/` 下的领域模块拥有。宿主 API 端点全部改为
直接导入领域依赖,旧入口只为外部兼容消费者保留。
- 新增 `app/api/presentation/sse.py`,统一 non-buffered SSE transport 策略,并分别提供 unnamed data
与 named event wire mapper。WebAgent、OpenAI 和 Anthropic 继续保留各自协议 payload 与错误结构,
不进入通用 `Response` 包装。
- `manual_transfer()` 已缩为 HTTP/鉴权/依赖入口,历史恢复、批量预览和旧 `TransferChain` 参数兼容
由内部处理器承接;WebAgent 的拒绝响应、stream headers 与协议映射已从主控制流抽离。长生命周期
generator 仍保留在端点模块,因为它直接拥有 request disconnect、后台 task cancel 与敏感结果关闭时序,
后续只能在保持现有取消测试的前提下继续下沉。
- 125 个鉴权、手动整理、WebAgent、OpenAI/Anthropic 生命周期、API 响应和 typed runtime 专项测试通过;
61 个架构/基线 CLI 测试通过。依赖基线变化只反映集中边拆为领域边,runtime contract 变化只反映
dependency callable 的新模块路径;禁止边与插件 raw API 均未变化。
#### ARCH-232:配置快照与窄配置端口
**目标**:阻止 `settings``SystemConfigOper()` 继续扩散,不要求一次清除 180 个文件。
+188 -79
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6105,
"edge_sha256": "47fd6792530be771c9854d3f2951097d3b110cd46b1cab11643a070f3b11299c",
"edge_count": 6203,
"edge_sha256": "75ac7a20854abb707ea6c851326a183873a64e93f67053ffcd79ca5b01a105f6",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -1480,60 +1480,119 @@
"app.api.context -> app.application.messaging.chat",
"app.api.context -> app.startup",
"app.api.context -> app.startup.context",
"app.api.deps -> app.adapters",
"app.api.deps -> app.adapters.external",
"app.api.deps -> app.adapters.external.server",
"app.api.deps -> app.adapters.web",
"app.api.deps -> app.adapters.web.security",
"app.api.deps -> app.adapters.web.security.access",
"app.api.dependencies.agent -> app.api",
"app.api.dependencies.agent -> app.api.context",
"app.api.dependencies.agent -> app.api.data",
"app.api.dependencies.agent -> app.api.dependencies",
"app.api.dependencies.agent -> app.api.dependencies.data",
"app.api.dependencies.agent -> app.application",
"app.api.dependencies.agent -> app.application.messaging",
"app.api.dependencies.agent -> app.application.messaging.chat",
"app.api.dependencies.agent -> app.application.messaging.message",
"app.api.dependencies.auth -> app.adapters",
"app.api.dependencies.auth -> app.adapters.web",
"app.api.dependencies.auth -> app.adapters.web.security",
"app.api.dependencies.auth -> app.adapters.web.security.access",
"app.api.dependencies.auth -> app.api",
"app.api.dependencies.auth -> app.api.data",
"app.api.dependencies.auth -> app.api.dependencies",
"app.api.dependencies.auth -> app.api.dependencies.data",
"app.api.dependencies.auth -> app.application",
"app.api.dependencies.auth -> app.application.security",
"app.api.dependencies.auth -> app.application.security.auth",
"app.api.dependencies.auth -> app.application.security.passkeys",
"app.api.dependencies.auth -> app.application.security.user",
"app.api.dependencies.auth -> app.schemas",
"app.api.dependencies.auth -> app.schemas.token",
"app.api.dependencies.data -> app.api",
"app.api.dependencies.data -> app.api.data",
"app.api.dependencies.history -> app.api",
"app.api.dependencies.history -> app.api.data",
"app.api.dependencies.history -> app.api.dependencies",
"app.api.dependencies.history -> app.api.dependencies.data",
"app.api.dependencies.history -> app.application",
"app.api.dependencies.history -> app.application.dashboard",
"app.api.dependencies.history -> app.application.history",
"app.api.dependencies.history -> app.application.mediaserver",
"app.api.dependencies.history -> app.chain",
"app.api.dependencies.history -> app.chain.dashboard",
"app.api.dependencies.history -> app.chain.storage",
"app.api.dependencies.history -> app.runtime",
"app.api.dependencies.history -> app.runtime.events",
"app.api.dependencies.history -> app.schemas",
"app.api.dependencies.history -> app.schemas.types",
"app.api.dependencies.history -> app.schemas.workflow",
"app.api.dependencies.plugin -> app.application",
"app.api.dependencies.plugin -> app.application.commands",
"app.api.dependencies.plugin -> app.application.plugin",
"app.api.dependencies.plugin -> app.application.plugin.config",
"app.api.dependencies.plugin -> app.application.plugin.routes",
"app.api.dependencies.plugin -> app.application.plugin.runtime",
"app.api.dependencies.plugin -> app.application.scheduling",
"app.api.dependencies.plugin -> app.runtime",
"app.api.dependencies.plugin -> app.runtime.events",
"app.api.dependencies.plugin -> app.schemas",
"app.api.dependencies.plugin -> app.schemas.event",
"app.api.dependencies.plugin -> app.schemas.types",
"app.api.dependencies.site -> app.api",
"app.api.dependencies.site -> app.api.data",
"app.api.dependencies.site -> app.api.dependencies",
"app.api.dependencies.site -> app.api.dependencies.data",
"app.api.dependencies.site -> app.application",
"app.api.dependencies.site -> app.application.site",
"app.api.dependencies.site -> app.application.site.mutation",
"app.api.dependencies.site -> app.application.site.query",
"app.api.dependencies.site -> app.domain",
"app.api.dependencies.site -> app.domain.site",
"app.api.dependencies.site -> app.foundation",
"app.api.dependencies.site -> app.foundation.url",
"app.api.dependencies.site -> app.runtime",
"app.api.dependencies.site -> app.runtime.events",
"app.api.dependencies.site -> app.schemas",
"app.api.dependencies.site -> app.schemas.types",
"app.api.dependencies.subscription -> app.adapters",
"app.api.dependencies.subscription -> app.adapters.external",
"app.api.dependencies.subscription -> app.adapters.external.server",
"app.api.dependencies.subscription -> app.api",
"app.api.dependencies.subscription -> app.api.data",
"app.api.dependencies.subscription -> app.api.dependencies",
"app.api.dependencies.subscription -> app.api.dependencies.data",
"app.api.dependencies.subscription -> app.application",
"app.api.dependencies.subscription -> app.application.scheduling",
"app.api.dependencies.subscription -> app.application.servarr",
"app.api.dependencies.subscription -> app.application.subscription",
"app.api.dependencies.subscription -> app.application.subscription.delete",
"app.api.dependencies.subscription -> app.application.subscription.identity",
"app.api.dependencies.subscription -> app.application.subscription.mutation",
"app.api.dependencies.subscription -> app.application.subscription.query",
"app.api.dependencies.subscription -> app.application.subscription.search",
"app.api.dependencies.subscription -> app.runtime",
"app.api.dependencies.subscription -> app.runtime.events",
"app.api.dependencies.subscription -> app.runtime.log",
"app.api.dependencies.subscription -> app.schemas",
"app.api.dependencies.subscription -> app.schemas.types",
"app.api.dependencies.workflow -> app.adapters",
"app.api.dependencies.workflow -> app.adapters.external",
"app.api.dependencies.workflow -> app.adapters.external.server",
"app.api.dependencies.workflow -> app.api",
"app.api.dependencies.workflow -> app.api.data",
"app.api.dependencies.workflow -> app.api.dependencies",
"app.api.dependencies.workflow -> app.api.dependencies.data",
"app.api.dependencies.workflow -> app.application",
"app.api.dependencies.workflow -> app.application.scheduling",
"app.api.dependencies.workflow -> app.application.workflow",
"app.api.dependencies.workflow -> app.runtime",
"app.api.dependencies.workflow -> app.runtime.config",
"app.api.dependencies.workflow -> app.workflow",
"app.api.deps -> app.api",
"app.api.deps -> app.api.context",
"app.api.deps -> app.api.data",
"app.api.deps -> app.application",
"app.api.deps -> app.application.commands",
"app.api.deps -> app.application.dashboard",
"app.api.deps -> app.application.history",
"app.api.deps -> app.application.mediaserver",
"app.api.deps -> app.application.messaging",
"app.api.deps -> app.application.messaging.chat",
"app.api.deps -> app.application.messaging.message",
"app.api.deps -> app.application.plugin",
"app.api.deps -> app.application.plugin.config",
"app.api.deps -> app.application.plugin.routes",
"app.api.deps -> app.application.plugin.runtime",
"app.api.deps -> app.application.scheduling",
"app.api.deps -> app.application.security",
"app.api.deps -> app.application.security.auth",
"app.api.deps -> app.application.security.passkeys",
"app.api.deps -> app.application.security.user",
"app.api.deps -> app.application.servarr",
"app.api.deps -> app.application.site",
"app.api.deps -> app.application.site.mutation",
"app.api.deps -> app.application.site.query",
"app.api.deps -> app.application.subscription",
"app.api.deps -> app.application.subscription.delete",
"app.api.deps -> app.application.subscription.identity",
"app.api.deps -> app.application.subscription.mutation",
"app.api.deps -> app.application.subscription.query",
"app.api.deps -> app.application.subscription.search",
"app.api.deps -> app.application.workflow",
"app.api.deps -> app.chain",
"app.api.deps -> app.chain.dashboard",
"app.api.deps -> app.chain.storage",
"app.api.deps -> app.domain",
"app.api.deps -> app.domain.site",
"app.api.deps -> app.foundation",
"app.api.deps -> app.foundation.url",
"app.api.deps -> app.runtime",
"app.api.deps -> app.runtime.config",
"app.api.deps -> app.runtime.events",
"app.api.deps -> app.runtime.log",
"app.api.deps -> app.schemas",
"app.api.deps -> app.schemas.event",
"app.api.deps -> app.schemas.token",
"app.api.deps -> app.schemas.types",
"app.api.deps -> app.schemas.workflow",
"app.api.deps -> app.workflow",
"app.api.deps -> app.api.dependencies",
"app.api.deps -> app.api.dependencies.agent",
"app.api.deps -> app.api.dependencies.auth",
"app.api.deps -> app.api.dependencies.history",
"app.api.deps -> app.api.dependencies.plugin",
"app.api.deps -> app.api.dependencies.site",
"app.api.deps -> app.api.dependencies.subscription",
"app.api.deps -> app.api.dependencies.workflow",
"app.api.endpoints.agent -> app.agent",
"app.api.endpoints.agent -> app.agent.callback",
"app.api.endpoints.agent -> app.agent.contracts",
@@ -1542,7 +1601,11 @@
"app.api.endpoints.agent -> app.agent.mcp",
"app.api.endpoints.agent -> app.agent.runtime_loader",
"app.api.endpoints.agent -> app.api",
"app.api.endpoints.agent -> app.api.deps",
"app.api.endpoints.agent -> app.api.dependencies",
"app.api.endpoints.agent -> app.api.dependencies.agent",
"app.api.endpoints.agent -> app.api.dependencies.auth",
"app.api.endpoints.agent -> app.api.presentation",
"app.api.endpoints.agent -> app.api.presentation.sse",
"app.api.endpoints.agent -> app.api.principal",
"app.api.endpoints.agent -> app.api.response",
"app.api.endpoints.agent -> app.application",
@@ -1589,12 +1652,15 @@
"app.api.endpoints.anthropic -> app.api.endpoints",
"app.api.endpoints.anthropic -> app.api.endpoints.openai",
"app.api.endpoints.anthropic -> app.api.openai_utils",
"app.api.endpoints.anthropic -> app.api.presentation",
"app.api.endpoints.anthropic -> app.api.presentation.sse",
"app.api.endpoints.anthropic -> app.runtime",
"app.api.endpoints.anthropic -> app.runtime.config",
"app.api.endpoints.anthropic -> app.schemas",
"app.api.endpoints.anthropic -> app.schemas.openai",
"app.api.endpoints.auth -> app.api",
"app.api.endpoints.auth -> app.api.deps",
"app.api.endpoints.auth -> app.api.dependencies",
"app.api.endpoints.auth -> app.api.dependencies.auth",
"app.api.endpoints.auth -> app.api.response",
"app.api.endpoints.auth -> app.application",
"app.api.endpoints.auth -> app.application.plugin",
@@ -1625,7 +1691,9 @@
"app.api.endpoints.dashboard -> app.adapters.web.security",
"app.api.endpoints.dashboard -> app.adapters.web.security.access",
"app.api.endpoints.dashboard -> app.api",
"app.api.endpoints.dashboard -> app.api.deps",
"app.api.endpoints.dashboard -> app.api.dependencies",
"app.api.endpoints.dashboard -> app.api.dependencies.auth",
"app.api.endpoints.dashboard -> app.api.dependencies.history",
"app.api.endpoints.dashboard -> app.api.response",
"app.api.endpoints.dashboard -> app.application",
"app.api.endpoints.dashboard -> app.application.dashboard",
@@ -1677,7 +1745,9 @@
"app.api.endpoints.download -> app.adapters.web.security",
"app.api.endpoints.download -> app.adapters.web.security.access",
"app.api.endpoints.download -> app.api",
"app.api.endpoints.download -> app.api.deps",
"app.api.endpoints.download -> app.api.dependencies",
"app.api.endpoints.download -> app.api.dependencies.auth",
"app.api.endpoints.download -> app.api.dependencies.site",
"app.api.endpoints.download -> app.api.principal",
"app.api.endpoints.download -> app.api.response",
"app.api.endpoints.download -> app.application",
@@ -1718,7 +1788,9 @@
"app.api.endpoints.history -> app.agent.prompt.transfer_redo",
"app.api.endpoints.history -> app.agent.runtime_loader",
"app.api.endpoints.history -> app.api",
"app.api.endpoints.history -> app.api.deps",
"app.api.endpoints.history -> app.api.dependencies",
"app.api.endpoints.history -> app.api.dependencies.auth",
"app.api.endpoints.history -> app.api.dependencies.history",
"app.api.endpoints.history -> app.api.response",
"app.api.endpoints.history -> app.application",
"app.api.endpoints.history -> app.application.history",
@@ -1735,7 +1807,8 @@
"app.api.endpoints.llm -> app.agent.llm",
"app.api.endpoints.llm -> app.agent.llm.provider",
"app.api.endpoints.llm -> app.api",
"app.api.endpoints.llm -> app.api.deps",
"app.api.endpoints.llm -> app.api.dependencies",
"app.api.endpoints.llm -> app.api.dependencies.auth",
"app.api.endpoints.llm -> app.api.response",
"app.api.endpoints.llm -> app.schemas",
"app.api.endpoints.llm -> app.schemas.common",
@@ -1779,7 +1852,8 @@
"app.api.endpoints.media -> app.adapters.web.security",
"app.api.endpoints.media -> app.adapters.web.security.access",
"app.api.endpoints.media -> app.api",
"app.api.endpoints.media -> app.api.deps",
"app.api.endpoints.media -> app.api.dependencies",
"app.api.endpoints.media -> app.api.dependencies.auth",
"app.api.endpoints.media -> app.api.response",
"app.api.endpoints.media -> app.application",
"app.api.endpoints.media -> app.application.plugin",
@@ -1811,7 +1885,8 @@
"app.api.endpoints.mediaserver -> app.adapters.web.security",
"app.api.endpoints.mediaserver -> app.adapters.web.security.access",
"app.api.endpoints.mediaserver -> app.api",
"app.api.endpoints.mediaserver -> app.api.deps",
"app.api.endpoints.mediaserver -> app.api.dependencies",
"app.api.endpoints.mediaserver -> app.api.dependencies.history",
"app.api.endpoints.mediaserver -> app.api.response",
"app.api.endpoints.mediaserver -> app.application",
"app.api.endpoints.mediaserver -> app.application.configuration",
@@ -1837,7 +1912,9 @@
"app.api.endpoints.message -> app.adapters.web.security",
"app.api.endpoints.message -> app.adapters.web.security.access",
"app.api.endpoints.message -> app.api",
"app.api.endpoints.message -> app.api.deps",
"app.api.endpoints.message -> app.api.dependencies",
"app.api.endpoints.message -> app.api.dependencies.agent",
"app.api.endpoints.message -> app.api.dependencies.auth",
"app.api.endpoints.message -> app.api.principal",
"app.api.endpoints.message -> app.api.response",
"app.api.endpoints.message -> app.application",
@@ -1861,7 +1938,8 @@
"app.api.endpoints.mfa -> app.adapters.web.security",
"app.api.endpoints.mfa -> app.adapters.web.security.access",
"app.api.endpoints.mfa -> app.api",
"app.api.endpoints.mfa -> app.api.deps",
"app.api.endpoints.mfa -> app.api.dependencies",
"app.api.endpoints.mfa -> app.api.dependencies.auth",
"app.api.endpoints.mfa -> app.api.principal",
"app.api.endpoints.mfa -> app.api.response",
"app.api.endpoints.mfa -> app.application",
@@ -1884,7 +1962,8 @@
"app.api.endpoints.music -> app.adapters.web.security",
"app.api.endpoints.music -> app.adapters.web.security.access",
"app.api.endpoints.music -> app.api",
"app.api.endpoints.music -> app.api.deps",
"app.api.endpoints.music -> app.api.dependencies",
"app.api.endpoints.music -> app.api.dependencies.auth",
"app.api.endpoints.music -> app.api.response",
"app.api.endpoints.music -> app.chain",
"app.api.endpoints.music -> app.chain.listenbrainz",
@@ -1900,7 +1979,8 @@
"app.api.endpoints.music -> app.schemas.transfer",
"app.api.endpoints.music -> app.schemas.types",
"app.api.endpoints.notification -> app.api",
"app.api.endpoints.notification -> app.api.deps",
"app.api.endpoints.notification -> app.api.dependencies",
"app.api.endpoints.notification -> app.api.dependencies.auth",
"app.api.endpoints.notification -> app.api.response",
"app.api.endpoints.notification -> app.chain",
"app.api.endpoints.notification -> app.chain.notification",
@@ -1917,6 +1997,8 @@
"app.api.endpoints.openai -> app.agent.runtime_loader",
"app.api.endpoints.openai -> app.api",
"app.api.endpoints.openai -> app.api.openai_utils",
"app.api.endpoints.openai -> app.api.presentation",
"app.api.endpoints.openai -> app.api.presentation.sse",
"app.api.endpoints.openai -> app.runtime",
"app.api.endpoints.openai -> app.runtime.config",
"app.api.endpoints.openai -> app.schemas",
@@ -1933,7 +2015,9 @@
"app.api.endpoints.plugin -> app.adapters.web.security",
"app.api.endpoints.plugin -> app.adapters.web.security.access",
"app.api.endpoints.plugin -> app.api",
"app.api.endpoints.plugin -> app.api.deps",
"app.api.endpoints.plugin -> app.api.dependencies",
"app.api.endpoints.plugin -> app.api.dependencies.auth",
"app.api.endpoints.plugin -> app.api.dependencies.plugin",
"app.api.endpoints.plugin -> app.api.principal",
"app.api.endpoints.plugin -> app.api.response",
"app.api.endpoints.plugin -> app.application",
@@ -2005,7 +2089,9 @@
"app.api.endpoints.site -> app.adapters.web.security",
"app.api.endpoints.site -> app.adapters.web.security.access",
"app.api.endpoints.site -> app.api",
"app.api.endpoints.site -> app.api.deps",
"app.api.endpoints.site -> app.api.dependencies",
"app.api.endpoints.site -> app.api.dependencies.auth",
"app.api.endpoints.site -> app.api.dependencies.site",
"app.api.endpoints.site -> app.api.endpoints",
"app.api.endpoints.site -> app.api.endpoints.plugin",
"app.api.endpoints.site -> app.api.principal",
@@ -2035,7 +2121,8 @@
"app.api.endpoints.site -> app.schemas.types",
"app.api.endpoints.site -> app.schemas.workflow",
"app.api.endpoints.storage -> app.api",
"app.api.endpoints.storage -> app.api.deps",
"app.api.endpoints.storage -> app.api.dependencies",
"app.api.endpoints.storage -> app.api.dependencies.auth",
"app.api.endpoints.storage -> app.api.principal",
"app.api.endpoints.storage -> app.api.response",
"app.api.endpoints.storage -> app.chain",
@@ -2059,7 +2146,9 @@
"app.api.endpoints.subscribe -> app.adapters.web.security",
"app.api.endpoints.subscribe -> app.adapters.web.security.access",
"app.api.endpoints.subscribe -> app.api",
"app.api.endpoints.subscribe -> app.api.deps",
"app.api.endpoints.subscribe -> app.api.dependencies",
"app.api.endpoints.subscribe -> app.api.dependencies.auth",
"app.api.endpoints.subscribe -> app.api.dependencies.subscription",
"app.api.endpoints.subscribe -> app.api.principal",
"app.api.endpoints.subscribe -> app.api.response",
"app.api.endpoints.subscribe -> app.application",
@@ -2103,7 +2192,8 @@
"app.api.endpoints.system -> app.agent.llm",
"app.api.endpoints.system -> app.agent.llm.server_tools",
"app.api.endpoints.system -> app.api",
"app.api.endpoints.system -> app.api.deps",
"app.api.endpoints.system -> app.api.dependencies",
"app.api.endpoints.system -> app.api.dependencies.auth",
"app.api.endpoints.system -> app.api.principal",
"app.api.endpoints.system -> app.api.response",
"app.api.endpoints.system -> app.application",
@@ -2147,7 +2237,8 @@
"app.api.endpoints.tmdb -> app.adapters.web.security",
"app.api.endpoints.tmdb -> app.adapters.web.security.access",
"app.api.endpoints.tmdb -> app.api",
"app.api.endpoints.tmdb -> app.api.deps",
"app.api.endpoints.tmdb -> app.api.dependencies",
"app.api.endpoints.tmdb -> app.api.dependencies.auth",
"app.api.endpoints.tmdb -> app.api.response",
"app.api.endpoints.tmdb -> app.application",
"app.api.endpoints.tmdb -> app.application.configuration",
@@ -2163,7 +2254,8 @@
"app.api.endpoints.tmdb -> app.schemas.types",
"app.api.endpoints.tmdb -> app.schemas.workflow",
"app.api.endpoints.torrent -> app.api",
"app.api.endpoints.torrent -> app.api.deps",
"app.api.endpoints.torrent -> app.api.dependencies",
"app.api.endpoints.torrent -> app.api.dependencies.auth",
"app.api.endpoints.torrent -> app.api.response",
"app.api.endpoints.torrent -> app.chain",
"app.api.endpoints.torrent -> app.chain.media",
@@ -2188,7 +2280,9 @@
"app.api.endpoints.transfer -> app.adapters.web.security",
"app.api.endpoints.transfer -> app.adapters.web.security.access",
"app.api.endpoints.transfer -> app.api",
"app.api.endpoints.transfer -> app.api.deps",
"app.api.endpoints.transfer -> app.api.dependencies",
"app.api.endpoints.transfer -> app.api.dependencies.auth",
"app.api.endpoints.transfer -> app.api.dependencies.history",
"app.api.endpoints.transfer -> app.api.response",
"app.api.endpoints.transfer -> app.application",
"app.api.endpoints.transfer -> app.application.directory",
@@ -2208,7 +2302,8 @@
"app.api.endpoints.transfer -> app.schemas.types",
"app.api.endpoints.transfer -> app.schemas.workflow",
"app.api.endpoints.user -> app.api",
"app.api.endpoints.user -> app.api.deps",
"app.api.endpoints.user -> app.api.dependencies",
"app.api.endpoints.user -> app.api.dependencies.auth",
"app.api.endpoints.user -> app.api.response",
"app.api.endpoints.user -> app.application",
"app.api.endpoints.user -> app.application.security",
@@ -2233,7 +2328,9 @@
"app.api.endpoints.workflow -> app.adapters.external",
"app.api.endpoints.workflow -> app.adapters.external.server",
"app.api.endpoints.workflow -> app.api",
"app.api.endpoints.workflow -> app.api.deps",
"app.api.endpoints.workflow -> app.api.dependencies",
"app.api.endpoints.workflow -> app.api.dependencies.auth",
"app.api.endpoints.workflow -> app.api.dependencies.workflow",
"app.api.endpoints.workflow -> app.api.response",
"app.api.endpoints.workflow -> app.application",
"app.api.endpoints.workflow -> app.application.plugin",
@@ -2289,7 +2386,8 @@
"app.api.servarr -> app.adapters.web.security",
"app.api.servarr -> app.adapters.web.security.access",
"app.api.servarr -> app.api",
"app.api.servarr -> app.api.deps",
"app.api.servarr -> app.api.dependencies",
"app.api.servarr -> app.api.dependencies.subscription",
"app.api.servarr -> app.api.response",
"app.api.servarr -> app.application",
"app.api.servarr -> app.application.servarr",
@@ -6122,7 +6220,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
"module_count": 760,
"module_count": 771,
"modules": [
"app",
"app.adapters",
@@ -6312,6 +6410,15 @@
"app.api.apiv1",
"app.api.context",
"app.api.data",
"app.api.dependencies",
"app.api.dependencies.agent",
"app.api.dependencies.auth",
"app.api.dependencies.data",
"app.api.dependencies.history",
"app.api.dependencies.plugin",
"app.api.dependencies.site",
"app.api.dependencies.subscription",
"app.api.dependencies.workflow",
"app.api.deps",
"app.api.endpoints",
"app.api.endpoints.agent",
@@ -6348,6 +6455,8 @@
"app.api.endpoints.webhook",
"app.api.endpoints.workflow",
"app.api.openai_utils",
"app.api.presentation",
"app.api.presentation.sse",
"app.api.principal",
"app.api.response",
"app.api.router_specs",
+5 -5
View File
@@ -1317,7 +1317,7 @@
"consumers": [],
"producers": [
{
"caller": "app.api.deps",
"caller": "app.api.dependencies.plugin",
"count": 1
}
]
@@ -1514,7 +1514,7 @@
],
"producers": [
{
"caller": "app.api.deps",
"caller": "app.api.dependencies.history",
"count": 1
}
]
@@ -1598,7 +1598,7 @@
],
"producers": [
{
"caller": "app.api.deps",
"caller": "app.api.dependencies.site",
"count": 1
}
]
@@ -1625,7 +1625,7 @@
"count": 1
},
{
"caller": "app.api.deps",
"caller": "app.api.dependencies.site",
"count": 1
},
{
@@ -1660,7 +1660,7 @@
"count": 1
},
{
"caller": "app.api.deps",
"caller": "app.api.dependencies.subscription",
"count": 1
}
]