feat(agent): complete music workflow support

This commit is contained in:
jxxghp
2026-08-10 08:11:54 +08:00
parent 1fc255b8a7
commit b3b376f2b1
40 changed files with 2224 additions and 266 deletions
+80 -33
View File
@@ -9,7 +9,8 @@ from app.agent.tools.base import MoviePilotTool
from app.agent.tools.tags import ToolTag
from app.db.subscribehistory_oper import SubscribeHistoryOper
from app.log import logger
from app.schemas.types import media_type_to_agent
from app.schemas.types import MediaType, media_type_to_agent
from ._music_utils import normalize_music_type
PAGE_SIZE = 20
@@ -18,7 +19,11 @@ class QuerySubscribeHistoryInput(BaseModel):
"""查询订阅历史工具的输入参数模型"""
media_type: Optional[str] = Field(
"all", description="Allowed values: movie, tv, all"
"all", description="Allowed values: movie, tv, music, all"
)
music_type: Optional[str] = Field(
None,
description="Optional music history filter: recording or album",
)
name: Optional[str] = Field(
None, description="Filter by media name (partial match, optional)"
@@ -30,6 +35,8 @@ class QuerySubscribeHistoryInput(BaseModel):
class QuerySubscribeHistoryTool(MoviePilotTool):
"""查询已完成的影视、单曲与整张专辑订阅历史。"""
name: str = "query_subscribe_history"
tags: list[str] = [
ToolTag.Read,
@@ -58,38 +65,63 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
async def run(
self,
media_type: Optional[str] = "all",
music_type: Optional[str] = None,
name: Optional[str] = None,
page: Optional[int] = 1,
**kwargs,
) -> str:
"""按规范化数据库类型查询并合并订阅历史。"""
page = max(1, page or 1)
logger.info(
f"执行工具: {self.name}, 参数: media_type={media_type}, name={name}, page={page}"
)
try:
if media_type not in ["all", "movie", "tv"]:
return f"错误:无效的媒体类型 '{media_type}',支持的类型:'movie', 'tv', 'all'"
if media_type == "all":
requested_types = [
MediaType.MOVIE.value,
MediaType.TV.value,
MediaType.MUSIC.value,
]
else:
media_type_enum = MediaType.from_agent(media_type)
if not media_type_enum:
return (
f"错误:无效的媒体类型 '{media_type}'"
"支持的类型:'movie', 'tv', 'music', 'all'"
)
requested_types = [media_type_enum.value]
normalized_music_type = None
if music_type:
normalized_music_type = normalize_music_type(
music_type,
allow_artist=False,
)
if not normalized_music_type:
return (
f"错误:无效的音乐实体类型 '{music_type}'"
"支持的类型:'recording', 'album'"
)
if MediaType.MUSIC.value not in requested_types:
return "错误:music_type 仅能与 media_type='music''all' 一起使用"
subscribe_history_oper = SubscribeHistoryOper()
if name:
# 有名称过滤时,获取足够多的记录在内存中过滤,不分页
fetch_count = 500
if media_type == "all":
movie_history = await subscribe_history_oper.async_list_by_type(
mtype="movie", page=1, count=fetch_count
)
tv_history = await subscribe_history_oper.async_list_by_type(
mtype="tv", page=1, count=fetch_count
)
all_history = list(movie_history) + list(tv_history)
all_history.sort(key=lambda x: x.date or "", reverse=True)
else:
all_history = list(
await subscribe_history_oper.async_list_by_type(
mtype=media_type, page=1, count=fetch_count
)
history_groups = [
await subscribe_history_oper.async_list_by_type(
mtype=requested_type,
page=1,
count=fetch_count,
)
for requested_type in requested_types
]
all_history = [
record for group in history_groups for record in group
]
all_history.sort(key=lambda x: x.date or "", reverse=True)
# 按名称过滤
name_lower = name.lower()
@@ -97,6 +129,7 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
record
for record in all_history
if record.name and name_lower in record.name.lower()
and self._matches_music_type(record, normalized_music_type)
]
if not filtered_history:
@@ -110,22 +143,23 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
return result_json
else:
# 无名称过滤时,直接利用数据库分页
if media_type == "all":
movie_history = await subscribe_history_oper.async_list_by_type(
mtype="movie", page=1, count=page * PAGE_SIZE
)
tv_history = await subscribe_history_oper.async_list_by_type(
mtype="tv", page=1, count=page * PAGE_SIZE
)
all_history = list(movie_history) + list(tv_history)
all_history.sort(key=lambda x: x.date or "", reverse=True)
filtered_history = all_history
else:
filtered_history = list(
await subscribe_history_oper.async_list_by_type(
mtype=media_type, page=1, count=page * PAGE_SIZE
)
history_groups = [
await subscribe_history_oper.async_list_by_type(
mtype=requested_type,
page=1,
count=page * PAGE_SIZE,
)
for requested_type in requested_types
]
all_history = [
record for group in history_groups for record in group
]
all_history.sort(key=lambda x: x.date or "", reverse=True)
filtered_history = [
record
for record in all_history
if self._matches_music_type(record, normalized_music_type)
]
if not filtered_history:
return "未找到相关订阅历史记录"
@@ -156,6 +190,15 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
logger.error(f"查询订阅历史失败: {e}", exc_info=True)
return f"查询订阅历史时发生错误: {str(e)}"
@staticmethod
def _matches_music_type(record, music_type: Optional[str]) -> bool:
"""匹配音乐实体类型,旧空值历史按单曲兼容。"""
if not music_type:
return True
if media_type_to_agent(getattr(record, "type", None)) != "music":
return False
return (getattr(record, "music_type", None) or "recording") == music_type
@staticmethod
def _simplify_records(records) -> list:
"""转换为字典格式,只保留关键信息"""
@@ -173,6 +216,10 @@ class QuerySubscribeHistoryTool(MoviePilotTool):
"anilistid": record.anilistid,
"media_source": record.media_source,
"media_id": record.media_id,
"music_type": getattr(record, "music_type", None) or (
"recording" if media_type_to_agent(record.type) == "music" else None
),
"total_tracks": getattr(record, "total_tracks", None),
"poster": record.poster,
"vote": record.vote,
"total_episode": record.total_episode,