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
+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,