From a25c709927fe298d1b143a9a6fc0e19c71a24602 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 27 Mar 2026 11:50:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eagent=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agent/tools/factory.py | 2 + .../tools/impl/delete_download_history.py | 43 +++ app/db/models/downloadhistory.py | 262 +++++++++++++----- 3 files changed, 239 insertions(+), 68 deletions(-) create mode 100644 app/agent/tools/impl/delete_download_history.py diff --git a/app/agent/tools/factory.py b/app/agent/tools/factory.py index 52d10bf6..bc034f6a 100644 --- a/app/agent/tools/factory.py +++ b/app/agent/tools/factory.py @@ -36,6 +36,7 @@ from app.agent.tools.impl.query_workflows import QueryWorkflowsTool from app.agent.tools.impl.run_workflow import RunWorkflowTool from app.agent.tools.impl.update_site_cookie import UpdateSiteCookieTool from app.agent.tools.impl.delete_download import DeleteDownloadTool +from app.agent.tools.impl.delete_download_history import DeleteDownloadHistoryTool from app.agent.tools.impl.modify_download import ModifyDownloadTool from app.agent.tools.impl.query_directory_settings import QueryDirectorySettingsTool from app.agent.tools.impl.list_directory import ListDirectoryTool @@ -95,6 +96,7 @@ class MoviePilotToolFactory: DeleteSubscribeTool, QueryDownloadTasksTool, DeleteDownloadTool, + DeleteDownloadHistoryTool, ModifyDownloadTool, QueryDownloadersTool, QuerySitesTool, diff --git a/app/agent/tools/impl/delete_download_history.py b/app/agent/tools/impl/delete_download_history.py new file mode 100644 index 00000000..bfe0c88f --- /dev/null +++ b/app/agent/tools/impl/delete_download_history.py @@ -0,0 +1,43 @@ +"""删除下载历史记录工具""" + +from typing import Optional, Type + +from pydantic import BaseModel, Field + +from app.agent.tools.base import MoviePilotTool +from app.db import AsyncSessionFactory +from app.db.models.downloadhistory import DownloadHistory +from app.log import logger + + +class DeleteDownloadHistoryInput(BaseModel): + """删除下载历史记录工具的输入参数模型""" + + explanation: str = Field( + ..., + description="Clear explanation of why this tool is being used in the current context", + ) + history_id: int = Field( + ..., description="The ID of the download history record to delete" + ) + + +class DeleteDownloadHistoryTool(MoviePilotTool): + name: str = "delete_download_history" + description: str = "Delete a download history record by ID. This only removes the record from the database, does not delete any actual files." + args_schema: Type[BaseModel] = DeleteDownloadHistoryInput + + def get_tool_message(self, **kwargs) -> Optional[str]: + history_id = kwargs.get("history_id") + return f"正在删除下载历史记录 ID: {history_id}" + + async def run(self, history_id: int, **kwargs) -> str: + logger.info(f"执行工具: {self.name}, 参数: history_id={history_id}") + + try: + async with AsyncSessionFactory() as db: + await DownloadHistory.async_delete(db, history_id) + return f"下载历史记录 ID: {history_id} 已成功删除" + except Exception as e: + logger.error(f"删除下载历史记录失败: {e}", exc_info=True) + return f"删除下载历史记录时发生错误: {str(e)}" diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index d1653fdb..516ec1d0 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -12,6 +12,7 @@ class DownloadHistory(Base): """ 下载历史记录 """ + id = get_id_column() # 保存路径 path = Column(String, nullable=False, index=True) @@ -61,32 +62,73 @@ class DownloadHistory(Base): @classmethod @db_query def get_by_hash(cls, db: Session, download_hash: str): - return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).order_by( - DownloadHistory.date.desc() - ).first() + return ( + db.query(DownloadHistory) + .filter(DownloadHistory.download_hash == download_hash) + .order_by(DownloadHistory.date.desc()) + .first() + ) @classmethod @db_query def get_by_mediaid(cls, db: Session, tmdbid: int, doubanid: str): if tmdbid: - return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).all() + return ( + db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid).all() + ) elif doubanid: - return db.query(DownloadHistory).filter(DownloadHistory.doubanid == doubanid).all() + return ( + db.query(DownloadHistory) + .filter(DownloadHistory.doubanid == doubanid) + .all() + ) return [] @classmethod @db_query - def list_by_page(cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30): + def list_by_page( + cls, db: Session, page: Optional[int] = 1, count: Optional[int] = 30 + ): return db.query(DownloadHistory).offset((page - 1) * count).limit(count).all() @classmethod @async_db_query - async def async_list_by_page(cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30): - result = await db.execute( - select(cls).offset((page - 1) * count).limit(count) - ) + async def async_list_by_page( + cls, db: AsyncSession, page: Optional[int] = 1, count: Optional[int] = 30 + ): + result = await db.execute(select(cls).offset((page - 1) * count).limit(count)) return result.scalars().all() + @classmethod + @async_db_query + async def async_list_by_title( + cls, + db: AsyncSession, + title: str, + page: Optional[int] = 1, + count: Optional[int] = 30, + ): + query = ( + select(cls).filter(cls.title.like(f"%{title}%")).order_by(cls.date.desc()) + ) + query = query.offset((page - 1) * count).limit(count) + result = await db.execute(query) + return result.scalars().all() + + @classmethod + @async_db_query + async def async_count(cls, db: AsyncSession): + result = await db.execute(select(func.count(cls.id))) + return result.scalar() + + @classmethod + @async_db_query + async def async_count_by_title(cls, db: AsyncSession, title: str): + result = await db.execute( + select(func.count(cls.id)).filter(cls.title.like(f"%{title}%")) + ) + return result.scalar() + @classmethod @db_query def get_by_path(cls, db: Session, path: str): @@ -94,9 +136,16 @@ class DownloadHistory(Base): @classmethod @db_query - def get_last_by(cls, db: Session, mtype: Optional[str] = None, title: Optional[str] = None, - year: Optional[str] = None, season: Optional[str] = None, - episode: Optional[str] = None, tmdbid: Optional[int] = None): + def get_last_by( + cls, + db: Session, + mtype: Optional[str] = None, + title: Optional[str] = None, + year: Optional[str] = None, + season: Optional[str] = None, + episode: Optional[str] = None, + tmdbid: Optional[int] = None, + ): """ 据tmdbid、season、season_episode查询下载记录 tmdbid + mtype 或 title + year @@ -105,42 +154,76 @@ class DownloadHistory(Base): if tmdbid and mtype: # 电视剧某季某集 if season is not None and episode: - return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, - DownloadHistory.type == mtype, - DownloadHistory.seasons == season, - DownloadHistory.episodes == episode).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.tmdbid == tmdbid, + DownloadHistory.type == mtype, + DownloadHistory.seasons == season, + DownloadHistory.episodes == episode, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) # 电视剧某季 elif season is not None: - return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, - DownloadHistory.type == mtype, - DownloadHistory.seasons == season).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.tmdbid == tmdbid, + DownloadHistory.type == mtype, + DownloadHistory.seasons == season, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) else: # 电视剧所有季集/电影 - return db.query(DownloadHistory).filter(DownloadHistory.tmdbid == tmdbid, - DownloadHistory.type == mtype).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.tmdbid == tmdbid, DownloadHistory.type == mtype + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) # 标题 + 年份 elif title and year: # 电视剧某季某集 if season is not None and episode: - return db.query(DownloadHistory).filter(DownloadHistory.title == title, - DownloadHistory.year == year, - DownloadHistory.seasons == season, - DownloadHistory.episodes == episode).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.title == title, + DownloadHistory.year == year, + DownloadHistory.seasons == season, + DownloadHistory.episodes == episode, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) # 电视剧某季 elif season is not None: - return db.query(DownloadHistory).filter(DownloadHistory.title == title, - DownloadHistory.year == year, - DownloadHistory.seasons == season).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.title == title, + DownloadHistory.year == year, + DownloadHistory.seasons == season, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) else: # 电视剧所有季集/电影 - return db.query(DownloadHistory).filter(DownloadHistory.title == title, - DownloadHistory.year == year).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.title == title, DownloadHistory.year == year + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) return [] @@ -151,45 +234,80 @@ class DownloadHistory(Base): 查询某用户某时间之后的下载历史 """ if username: - return db.query(DownloadHistory).filter(DownloadHistory.date < date, - DownloadHistory.username == username).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.date < date, DownloadHistory.username == username + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) else: - return db.query(DownloadHistory).filter(DownloadHistory.date < date).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter(DownloadHistory.date < date) + .order_by(DownloadHistory.id.desc()) + .all() + ) @classmethod @db_query - def list_by_date(cls, db: Session, date: str, type: str, tmdbid: str, seasons: Optional[str] = None): + def list_by_date( + cls, + db: Session, + date: str, + type: str, + tmdbid: str, + seasons: Optional[str] = None, + ): """ 查询某时间之后的下载历史 """ if seasons: - return db.query(DownloadHistory).filter(DownloadHistory.date > date, - DownloadHistory.type == type, - DownloadHistory.tmdbid == tmdbid, - DownloadHistory.seasons == seasons).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.date > date, + DownloadHistory.type == type, + DownloadHistory.tmdbid == tmdbid, + DownloadHistory.seasons == seasons, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) else: - return db.query(DownloadHistory).filter(DownloadHistory.date > date, - DownloadHistory.type == type, - DownloadHistory.tmdbid == tmdbid).order_by( - DownloadHistory.id.desc()).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.date > date, + DownloadHistory.type == type, + DownloadHistory.tmdbid == tmdbid, + ) + .order_by(DownloadHistory.id.desc()) + .all() + ) @classmethod @db_query def list_by_type(cls, db: Session, mtype: str, days: int): - return db.query(DownloadHistory) \ - .filter(DownloadHistory.type == mtype, - DownloadHistory.date >= time.strftime("%Y-%m-%d %H:%M:%S", - time.localtime(time.time() - 86400 * int(days))) - ).all() + return ( + db.query(DownloadHistory) + .filter( + DownloadHistory.type == mtype, + DownloadHistory.date + >= time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(time.time() - 86400 * int(days)) + ), + ) + .all() + ) class DownloadFiles(Base): """ 下载文件记录 """ + id = get_id_column() # 下载器 downloader = Column(String) @@ -210,8 +328,11 @@ class DownloadFiles(Base): @db_query def get_by_hash(cls, db: Session, download_hash: str, state: Optional[int] = None): if state is not None: - return db.query(cls).filter(cls.download_hash == download_hash, - cls.state == state).all() + return ( + db.query(cls) + .filter(cls.download_hash == download_hash, cls.state == state) + .all() + ) else: return db.query(cls).filter(cls.download_hash == download_hash).all() @@ -219,11 +340,19 @@ class DownloadFiles(Base): @db_query def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False): if not all_files: - return db.query(cls).filter(cls.fullpath == fullpath).order_by( - cls.id.desc()).first() + return ( + db.query(cls) + .filter(cls.fullpath == fullpath) + .order_by(cls.id.desc()) + .first() + ) else: - return db.query(cls).filter(cls.fullpath == fullpath).order_by( - cls.id.desc()).all() + return ( + db.query(cls) + .filter(cls.fullpath == fullpath) + .order_by(cls.id.desc()) + .all() + ) @classmethod @db_query @@ -233,9 +362,6 @@ class DownloadFiles(Base): @classmethod @db_update def delete_by_fullpath(cls, db: Session, fullpath: str): - db.query(cls).filter(cls.fullpath == fullpath, - cls.state == 1).update( - { - "state": 0 - } + db.query(cls).filter(cls.fullpath == fullpath, cls.state == 1).update( + {"state": 0} )