import asyncio import time from typing import List, Any, Optional from fastapi import Depends from app.schemas.common import BatchProgressKeyData as _SchemaBatchProgressKeyData from app.schemas.common import ProgressKeyData as _SchemaProgressKeyData from app.schemas.history import BatchTransferHistoryRedoRequest as _SchemaBatchTransferHistoryRedoRequest from app.schemas.history import TransferHistory as _SchemaTransferHistory from app.schemas.history import TransferHistoryPage as _SchemaTransferHistoryPage from app.schemas.response import Response as _SchemaResponse from app.schemas.token import TokenPayload as _SchemaTokenPayload from app.schemas.history import DownloadHistory as _SchemaDownloadHistory from app.api.response import ResponseAPIRouter from app.agent.contracts import ReplyMode from app.agent.runtime_loader import get_running_agent_manager from app.agent.prompt.transfer_redo import ( build_batch_manual_redo_prompt, build_manual_redo_prompt, ) from app.runtime.config import global_vars from app.api.context import ( get_api_runtime_config, get_background_task_registry, resolve_api_runtime_config, resolve_background_task_registry, ) from app.application.configuration import ApiRuntimeConfig from app.adapters.web.security.access import verify_token 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, ) from app.runtime.progress import AsyncProgressHelper from app.application.history import ( DownloadHistoryMutationCommand, HistoryQueryService, TransferHistoryMutationCommand, ) from app.runtime.log import logger from app.runtime.tasks import TaskRegistry router = ResponseAPIRouter() def normalize_history_ids(history_ids: list[int]) -> list[int]: """对输入的历史记录 ID 列表进行规范化处理,去除重复项并保持原有顺序。""" normalized_ids: list[int] = [] for history_id in history_ids: if history_id not in normalized_ids: normalized_ids.append(history_id) return normalized_ids def _start_ai_redo_task( history_id: int, prompt: str, progress_key: str, task_registry: TaskRegistry | None = None, ) -> None: """在后台任务中启动单条 AI 重新整理任务,并通过异步进度辅助类实时更新进度。""" progress = AsyncProgressHelper(progress_key) def update_output(text: str): # 输出回调由 agent 在事件循环上同步调用,不能直接 await; # 提交到全局事件循环非阻塞执行,避免同步缓存后端阻塞事件循环。 asyncio.run_coroutine_threadsafe( progress.update(text=text, data={"history_id": history_id}), global_vars.loop, ) async def runner(): try: await progress.start() await progress.update( text=f"智能助手正在准备整理记录 #{history_id} ...", data={"history_id": history_id, "success": True}, ) manager = get_running_agent_manager() if manager is None: logger.warning("智能助手服务未运行,跳过单条整理历史 AI 重做") raise RuntimeError("智能助手服务未运行") await manager.run_background_prompt( message=prompt, session_prefix=f"__agent_manual_redo_{history_id}", output_callback=update_output, reply_mode=ReplyMode.CAPTURE_ONLY, allow_message_tools=False, ) await progress.update( text="智能助手整理完成", data={"history_id": history_id, "success": True, "completed": True}, ) except Exception as e: await progress.update( text=f"智能助手整理失败:{str(e)}", data={ "history_id": history_id, "success": False, "completed": True, "error": str(e), }, ) finally: await progress.end() registry = resolve_background_task_registry(task_registry) registry.create(runner(), owner="api.history.ai_redo") def _start_batch_ai_redo_task( history_ids: list[int], prompt: str, progress_key: str, task_registry: TaskRegistry | None = None, ) -> None: """在后台任务中启动批量 AI 重新整理任务,并通过异步进度辅助类实时更新进度。""" progress = AsyncProgressHelper(progress_key) def update_output(text: str): # 输出回调由 agent 在事件循环上同步调用,不能直接 await; # 提交到全局事件循环非阻塞执行,避免同步缓存后端阻塞事件循环。 asyncio.run_coroutine_threadsafe( progress.update(text=text, data={"history_ids": history_ids}), global_vars.loop, ) async def runner(): try: await progress.start() await progress.update( text=f"智能助手正在准备批量整理 {len(history_ids)} 条记录 ...", data={"history_ids": history_ids, "success": True}, ) manager = get_running_agent_manager() if manager is None: logger.warning("智能助手服务未运行,跳过批量整理历史 AI 重做") raise RuntimeError("智能助手服务未运行") await manager.run_background_prompt( message=prompt, session_prefix="__agent_manual_redo_batch", output_callback=update_output, reply_mode=ReplyMode.CAPTURE_ONLY, allow_message_tools=False, ) await progress.update( text="智能助手批量整理完成", data={"history_ids": history_ids, "success": True, "completed": True}, ) except Exception as e: await progress.update( text=f"智能助手批量整理失败:{str(e)}", data={ "history_ids": history_ids, "success": False, "completed": True, "error": str(e), }, ) finally: await progress.end() registry = resolve_background_task_registry(task_registry) registry.create(runner(), owner="api.history.ai_redo_batch") @router.get( "/download", summary="查询下载历史记录", response_model=List[_SchemaDownloadHistory], ) async def download_history( page: Optional[int] = 1, count: Optional[int] = 30, query: HistoryQueryService = Depends(get_history_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 按下载时间倒序查询下载历史记录 """ return await query.list_download(page=page, count=count) @router.delete( "/download", summary="删除下载历史记录", response_model=_SchemaResponse[None], ) def delete_download_history( history_in: _SchemaDownloadHistory, command: DownloadHistoryMutationCommand = Depends( get_download_history_mutation_command ), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 删除下载历史记录 """ result = command.delete(history_in.id) return _SchemaResponse(success=result.success, message=result.message) @router.get( "/transfer", summary="查询整理记录", response_model=_SchemaResponse[_SchemaTransferHistoryPage], ) async def transfer_history( title: Optional[str] = None, page: Optional[int] = 1, count: Optional[int] = 30, status: Optional[bool] = None, query: HistoryQueryService = Depends(get_history_query_service), _: _SchemaTokenPayload = Depends(verify_token), ) -> Any: """ 查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*) """ result = await query.list_transfer( title=title, page=page, count=count, status=status, ) return _SchemaResponse(success=True, data=result) @router.delete("/transfer", summary="删除整理记录", response_model=_SchemaResponse[None]) def delete_transfer_history( history_in: _SchemaTransferHistory, deletesrc: Optional[bool] = False, deletedest: Optional[bool] = False, command: TransferHistoryMutationCommand = Depends( get_transfer_history_mutation_command ), _: object = Depends(get_current_active_manage_user), ) -> Any: """ 删除整理记录。 """ result = command.delete( history_in.id, delete_source=bool(deletesrc), delete_destination=bool(deletedest), ) return _SchemaResponse(success=result.success, message=result.message) @router.post( "/transfer/{history_id}/ai-redo", summary="智能助手重新整理", response_model=_SchemaResponse[_SchemaProgressKeyData], ) async def ai_redo_transfer_history( history_id: int, query: HistoryQueryService = Depends(get_history_query_service), runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config), _: object = Depends(get_current_active_manage_user), task_registry: TaskRegistry = Depends(get_background_task_registry), ) -> Any: """ 手动触发单条历史记录的 AI 重新整理,并返回进度键。 """ runtime_config = resolve_api_runtime_config(runtime_config) if not runtime_config.ai_agent_enable: return _SchemaResponse(success=False, message="MoviePilot智能助手未启用") history = await query.get_transfer(history_id) if not history: return _SchemaResponse(success=False, message="整理记录不存在") prompt = build_manual_redo_prompt(history) progress_key = f"ai_redo_transfer_{history_id}_{int(time.time() * 1000)}" _start_ai_redo_task( history_id=history_id, prompt=prompt, progress_key=progress_key, task_registry=task_registry, ) return _SchemaResponse(success=True, data={"progress_key": progress_key}) @router.post( "/transfer/ai-redo", summary="智能助手批量重新整理", response_model=_SchemaResponse[_SchemaBatchProgressKeyData], ) async def batch_ai_redo_transfer_history( payload: _SchemaBatchTransferHistoryRedoRequest, query: HistoryQueryService = Depends(get_history_query_service), runtime_config: ApiRuntimeConfig = Depends(get_api_runtime_config), _: object = Depends(get_current_active_manage_user), task_registry: TaskRegistry = Depends(get_background_task_registry), ) -> Any: """ 手动触发多条历史记录的 AI 批量重新整理,并返回进度键。 """ runtime_config = resolve_api_runtime_config(runtime_config) if not runtime_config.ai_agent_enable: return _SchemaResponse(success=False, message="MoviePilot智能助手未启用") history_ids = normalize_history_ids(payload.history_ids) if not history_ids: return _SchemaResponse(success=False, message="未提供有效的整理记录") histories, missing_ids = await query.get_transfers(history_ids) if missing_ids: return _SchemaResponse( success=False, message="整理记录不存在: " + ", ".join(str(history_id) for history_id in missing_ids), ) prompt = build_batch_manual_redo_prompt(histories) progress_key = f"ai_redo_transfer_batch_{int(time.time() * 1000)}" _start_batch_ai_redo_task( history_ids=history_ids, prompt=prompt, progress_key=progress_key, task_registry=task_registry, ) return _SchemaResponse( success=True, data={"progress_key": progress_key, "history_ids": history_ids}, ) @router.get( "/empty/transfer", summary="清空整理记录", response_model=_SchemaResponse[None], ) def empty_transfer_history( command: TransferHistoryMutationCommand = Depends( get_transfer_history_mutation_command ), _: object = Depends(get_current_active_superuser), ) -> Any: """ 清空整理记录 """ result = command.truncate() return _SchemaResponse(success=result.success, message=result.message)