From 1197ece12b0d7e5ce352cc7c705cd10c5253e0ca Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:17:21 +0800 Subject: [PATCH] fix(agent): preserve episode lock on unchanged totals (#6271) --- app/agent/tools/impl/query_subscribes.py | 2 + app/agent/tools/impl/update_subscribe.py | 3 +- tests/test_agent_query_subscribes.py | 35 +++++++ tests/test_agent_update_subscribe.py | 116 ++++++++++++++++++++++- 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 tests/test_agent_query_subscribes.py diff --git a/app/agent/tools/impl/query_subscribes.py b/app/agent/tools/impl/query_subscribes.py index da7b193a5..c91987f50 100644 --- a/app/agent/tools/impl/query_subscribes.py +++ b/app/agent/tools/impl/query_subscribes.py @@ -213,6 +213,8 @@ class QuerySubscribesTool(MoviePilotTool): ).model_dump( include=set(QUERY_SUBSCRIBE_OUTPUT_FIELDS), exclude_none=True ) + # 手动总集数是运行锁状态,不属于公共订阅写入 Schema,查询时直接从实体读取。 + payload["manual_total_episode"] = subscribe.manual_total_episode or 0 payload["type"] = media_type_to_agent(payload.get("type")) if payload["type"] == "music" and not payload.get("music_type"): payload["music_type"] = "recording" diff --git a/app/agent/tools/impl/update_subscribe.py b/app/agent/tools/impl/update_subscribe.py index a7e60cbf4..84ee11ecf 100644 --- a/app/agent/tools/impl/update_subscribe.py +++ b/app/agent/tools/impl/update_subscribe.py @@ -221,7 +221,7 @@ class UpdateSubscribeTool(MoviePilotTool): subscribe_dict["season"] = season # 集数相关 - if total_episode is not None: + if total_episode is not None and total_episode != subscribe.total_episode: subscribe_dict["total_episode"] = total_episode # 如果总集数增加,缺失集数也要相应增加 if total_episode > (subscribe.total_episode or 0): @@ -346,6 +346,7 @@ class UpdateSubscribeTool(MoviePilotTool): "season": updated_subscribe.season, "state": updated_subscribe.state, "total_episode": updated_subscribe.total_episode, + "manual_total_episode": updated_subscribe.manual_total_episode, "lack_episode": updated_subscribe.lack_episode, "start_episode": updated_subscribe.start_episode, "quality": updated_subscribe.quality, diff --git a/tests/test_agent_query_subscribes.py b/tests/test_agent_query_subscribes.py new file mode 100644 index 000000000..2bb7fa670 --- /dev/null +++ b/tests/test_agent_query_subscribes.py @@ -0,0 +1,35 @@ +import asyncio +import json +from unittest.mock import patch + +from app.agent.tools.impl.query_subscribes import QuerySubscribesTool +from app.db.models.subscribe import Subscribe +from app.schemas.types import MediaType + + +def test_agent_query_subscribes_returns_manual_total_episode(): + """订阅查询应返回手动总集数标记,供 Agent 判断元数据是否可自动刷新。""" + subscribe = Subscribe( + id=160, + name="测试剧集", + type=MediaType.TV.value, + tmdbid=224839, + season=1, + total_episode=175, + manual_total_episode=1, + state="P", + ) + + with patch( + "app.agent.tools.impl.query_subscribes.SubscribeOper.async_list", + return_value=[subscribe], + ): + result = asyncio.run( + QuerySubscribesTool(session_id="session-1", user_id="10001").run( + tmdb_id=224839, + ) + ) + + _, result_json = result.split("\n\n", maxsplit=1) + payload = json.loads(result_json) + assert payload[0]["manual_total_episode"] == 1 diff --git a/tests/test_agent_update_subscribe.py b/tests/test_agent_update_subscribe.py index 84bbdaecc..2d3ba8ea0 100644 --- a/tests/test_agent_update_subscribe.py +++ b/tests/test_agent_update_subscribe.py @@ -3,7 +3,7 @@ import json from unittest.mock import AsyncMock, patch from app.agent.tools.impl.update_subscribe import UpdateSubscribeTool -from app.schemas.types import EventType +from app.schemas.types import EventType, MediaType def test_agent_update_subscribe_sends_modified_event_payload_with_agent_scene(): @@ -41,6 +41,120 @@ def test_agent_update_subscribe_sends_modified_event_payload_with_agent_scene(): assert event_payload["subscribe_info"]["name"] == "新标题" +def test_agent_update_subscribe_ignores_unchanged_total_episode(): + """Agent 回传相同总集数时,不应产生数据库写入或订阅调整事件。""" + subscribe = _AgentSubscribe( + id=160, + name="测试剧集", + type=MediaType.TV.value, + state="R", + total_episode=175, + lack_episode=0, + manual_total_episode=0, + ) + oper = _SubscribeOperStub(subscribe) + + with patch( + "app.agent.tools.impl.update_subscribe.SubscribeOper", + return_value=oper, + ), patch( + "app.agent.tools.impl.update_subscribe.eventmanager.async_send_event", + new=AsyncMock(), + ) as send_event: + result = asyncio.run( + UpdateSubscribeTool(session_id="session-1", user_id="10001").run( + subscribe_id=160, + total_episode=175, + ) + ) + + payload = json.loads(result) + assert payload == {"success": False, "message": "没有提供要更新的字段"} + assert oper.updates == [] + send_event.assert_not_awaited() + + +def test_agent_update_subscribe_only_updates_other_fields_with_unchanged_total_episode(): + """Agent 同时回传相同总集数和洗版设置时,只更新实际请求的其他字段。""" + subscribe = _AgentSubscribe( + id=160, + name="测试剧集", + type=MediaType.TV.value, + state="R", + total_episode=175, + lack_episode=0, + manual_total_episode=0, + best_version=0, + ) + oper = _SubscribeOperStub(subscribe) + + with patch( + "app.agent.tools.impl.update_subscribe.SubscribeOper", + return_value=oper, + ), patch( + "app.agent.tools.impl.update_subscribe.eventmanager.async_send_event", + new=AsyncMock(), + ) as send_event: + result = asyncio.run( + UpdateSubscribeTool(session_id="session-1", user_id="10001").run( + subscribe_id=160, + total_episode=175, + best_version=1, + ) + ) + + payload = json.loads(result) + assert payload["success"] is True + assert payload["updated_fields"] == ["best_version"] + assert payload["subscribe"]["manual_total_episode"] == 0 + assert oper.updates == [(160, {"best_version": 1})] + send_event.assert_awaited_once() + _, event_payload = send_event.await_args.args + assert event_payload["fields"] == ["best_version"] + + +def test_agent_update_subscribe_marks_changed_total_episode_as_manual(): + """Agent 真正修改总集数时,保持 Web API 的手动总集数语义。""" + subscribe = _AgentSubscribe( + id=160, + name="测试剧集", + type=MediaType.TV.value, + state="R", + total_episode=175, + lack_episode=0, + manual_total_episode=0, + ) + oper = _SubscribeOperStub(subscribe) + + with patch( + "app.agent.tools.impl.update_subscribe.SubscribeOper", + return_value=oper, + ), patch( + "app.agent.tools.impl.update_subscribe.eventmanager.async_send_event", + new=AsyncMock(), + ): + result = asyncio.run( + UpdateSubscribeTool(session_id="session-1", user_id="10001").run( + subscribe_id=160, + total_episode=190, + ) + ) + + payload = json.loads(result) + assert payload["success"] is True + assert payload["subscribe"]["manual_total_episode"] == 1 + assert oper.updates == [ + ( + 160, + { + "total_episode": 190, + "lack_episode": 15, + "manual_total_episode": 1, + }, + ) + ] + + class _AgentSubscribe: """ 最小订阅替身,模拟 Agent 工具依赖的订阅对象接口。