diff --git a/app/api/dependencies/__init__.py b/app/api/dependencies/__init__.py new file mode 100644 index 000000000..60de2074d --- /dev/null +++ b/app/api/dependencies/__init__.py @@ -0,0 +1 @@ +"""按业务领域拆分的 FastAPI 依赖工厂。""" diff --git a/app/api/dependencies/agent.py b/app/api/dependencies/agent.py new file mode 100644 index 000000000..d5e8dd2f0 --- /dev/null +++ b/app/api/dependencies/agent.py @@ -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)) diff --git a/app/api/dependencies/auth.py b/app/api/dependencies/auth.py new file mode 100644 index 000000000..5dfc19ea0 --- /dev/null +++ b/app/api/dependencies/auth.py @@ -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 diff --git a/app/api/dependencies/data.py b/app/api/dependencies/data.py new file mode 100644 index 000000000..60e42404d --- /dev/null +++ b/app/api/dependencies/data.py @@ -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) diff --git a/app/api/dependencies/history.py b/app/api/dependencies/history.py new file mode 100644 index 000000000..36bc43d88 --- /dev/null +++ b/app/api/dependencies/history.py @@ -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, + ) diff --git a/app/api/dependencies/plugin.py b/app/api/dependencies/plugin.py new file mode 100644 index 000000000..abc6c4566 --- /dev/null +++ b/app/api/dependencies/plugin.py @@ -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, + ) diff --git a/app/api/dependencies/site.py b/app/api/dependencies/site.py new file mode 100644 index 000000000..0e1bb5293 --- /dev/null +++ b/app/api/dependencies/site.py @@ -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)) diff --git a/app/api/dependencies/subscription.py b/app/api/dependencies/subscription.py new file mode 100644 index 000000000..e7322fa14 --- /dev/null +++ b/app/api/dependencies/subscription.py @@ -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), + ) diff --git a/app/api/dependencies/workflow.py b/app/api/dependencies/workflow.py new file mode 100644 index 000000000..a7b03921e --- /dev/null +++ b/app/api/dependencies/workflow.py @@ -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)) diff --git a/app/api/deps.py b/app/api/deps.py index 90fb04be0..9007667af 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -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", +] diff --git a/app/api/endpoints/agent.py b/app/api/endpoints/agent.py index 38dbc4842..bf6374ff9 100644 --- a/app/api/endpoints/agent.py +++ b/app/api/endpoints/agent.py @@ -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 diff --git a/app/api/endpoints/anthropic.py b/app/api/endpoints/anthropic.py index 67b389c09..b9b72df5d 100644 --- a/app/api/endpoints/anthropic.py +++ b/app/api/endpoints/anthropic.py @@ -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 = [] diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index f17df85ac..7b81c39e3 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -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() diff --git a/app/api/endpoints/dashboard.py b/app/api/endpoints/dashboard.py index 8d2c15006..f983b308f 100644 --- a/app/api/endpoints/dashboard.py +++ b/app/api/endpoints/dashboard.py @@ -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 diff --git a/app/api/endpoints/download.py b/app/api/endpoints/download.py index d2f191c2b..32823e9ab 100644 --- a/app/api/endpoints/download.py +++ b/app/api/endpoints/download.py @@ -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, diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py index 8753c3cbf..debac796b 100644 --- a/app/api/endpoints/history.py +++ b/app/api/endpoints/history.py @@ -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, diff --git a/app/api/endpoints/llm.py b/app/api/endpoints/llm.py index 03942ae4e..6ef82a4ab 100644 --- a/app/api/endpoints/llm.py +++ b/app/api/endpoints/llm.py @@ -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() diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index 769b7368f..2f73f6fe3 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -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 diff --git a/app/api/endpoints/mediaserver.py b/app/api/endpoints/mediaserver.py index 3eb879fcc..bb3bb4774 100644 --- a/app/api/endpoints/mediaserver.py +++ b/app/api/endpoints/mediaserver.py @@ -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 diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index 44dde689d..3c528b522 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -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 diff --git a/app/api/endpoints/mfa.py b/app/api/endpoints/mfa.py index 07ec7bfd8..717999926 100644 --- a/app/api/endpoints/mfa.py +++ b/app/api/endpoints/mfa.py @@ -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, diff --git a/app/api/endpoints/music.py b/app/api/endpoints/music.py index 1a12d7cac..93f98285e 100644 --- a/app/api/endpoints/music.py +++ b/app/api/endpoints/music.py @@ -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, diff --git a/app/api/endpoints/notification.py b/app/api/endpoints/notification.py index f23fbcc6a..a572adbf6 100644 --- a/app/api/endpoints/notification.py +++ b/app/api/endpoints/notification.py @@ -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() diff --git a/app/api/endpoints/openai.py b/app/api/endpoints/openai.py index ea6407ba2..f9cdc03c2 100644 --- a/app/api/endpoints/openai.py +++ b/app/api/endpoints/openai.py @@ -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 = [] diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index ab37c7037..f0af8c86f 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -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 diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index 996734a15..543fdd45c 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -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, diff --git a/app/api/endpoints/storage.py b/app/api/endpoints/storage.py index 8f652684c..370401184 100644 --- a/app/api/endpoints/storage.py +++ b/app/api/endpoints/storage.py @@ -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, ) diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 8c3cfb0f5..af28ca543 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -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, diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 5d934a692..662b4bd38 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -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 ( diff --git a/app/api/endpoints/tmdb.py b/app/api/endpoints/tmdb.py index cfcd46c11..257bb5a48 100644 --- a/app/api/endpoints/tmdb.py +++ b/app/api/endpoints/tmdb.py @@ -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() diff --git a/app/api/endpoints/torrent.py b/app/api/endpoints/torrent.py index 60972bec8..577cdb19a 100644 --- a/app/api/endpoints/torrent.py +++ b/app/api/endpoints/torrent.py @@ -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, diff --git a/app/api/endpoints/transfer.py b/app/api/endpoints/transfer.py index 56397da8b..acf920d92 100644 --- a/app/api/endpoints/transfer.py +++ b/app/api/endpoints/transfer.py @@ -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 diff --git a/app/api/endpoints/user.py b/app/api/endpoints/user.py index f57e9ac70..9f986aba1 100644 --- a/app/api/endpoints/user.py +++ b/app/api/endpoints/user.py @@ -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, diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 23cfda376..459593025 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -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, diff --git a/app/api/presentation/__init__.py b/app/api/presentation/__init__.py new file mode 100644 index 000000000..f6dd77551 --- /dev/null +++ b/app/api/presentation/__init__.py @@ -0,0 +1 @@ +"""API presentation helpers isolated from domain dependency assembly.""" diff --git a/app/api/presentation/sse.py b/app/api/presentation/sse.py new file mode 100644 index 000000000..72576610a --- /dev/null +++ b/app/api/presentation/sse.py @@ -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])) diff --git a/app/api/servarr.py b/app/api/servarr.py index 77641b0b0..f09b299a9 100644 --- a/app/api/servarr.py +++ b/app/api/servarr.py @@ -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 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index ffe24d607..c8597679a 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -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` -> 实施进度:阶段 0(ARCH-201~203)、阶段 1(ARCH-210~212)、阶段 2(ARCH-220~222)与 ARCH-230 已完成,后续任务按 ID 独立提交和回滚 +> 实施进度:阶段 0(ARCH-201~203)、阶段 1(ARCH-210~212)、阶段 2(ARCH-220~222)与 ARCH-230~231 已完成,后续任务按 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 个文件。 diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 889a41e4d..d6e5f1a64 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -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", diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 865ce667a..a32a523fc 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -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 } ]