add: AnySearch-Skill

This commit is contained in:
InfinityPacer
2026-06-21 07:39:39 +08:00
committed by jxxghp
parent e02cebe16c
commit 6b21abd547
18 changed files with 3023 additions and 116 deletions

View File

@@ -0,0 +1,355 @@
import asyncio
from types import SimpleNamespace
from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool
from app.agent.prompt.transfer_redo import build_manual_redo_template_context
def test_delete_transfer_history_tool_removes_old_dest_file_before_history(monkeypatch):
"""AI 重新整理删除整理记录前,应按历史目标文件清理旧媒体库文件。"""
calls = []
history = SimpleNamespace(
id=7,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="link",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
calls.append(("get", history_id))
return history
async def async_delete(self, history_id):
calls.append(("delete_history", history_id))
class FakeStorageChain:
def exists(self, fileitem):
calls.append(("exists_dest", fileitem.path))
return True
def delete_media_file(self, fileitem):
calls.append(("delete_dest", fileitem.path))
return True
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
tool = DeleteTransferHistoryTool(session_id="redo-session", user_id="10001")
result = asyncio.run(tool.run(history_id=7))
assert "已删除整理历史记录" in result
assert calls == [
("get", 7),
("exists_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_history", 7),
]
def test_delete_transfer_history_tool_keeps_history_when_old_dest_delete_fails(monkeypatch):
"""旧媒体库文件删除失败时不得删除整理记录,避免重整链路丢失回滚依据。"""
calls = []
history = SimpleNamespace(
id=8,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="copy",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
calls.append(("get", history_id))
return history
async def async_delete(self, history_id):
calls.append(("delete_history", history_id))
class FakeStorageChain:
def exists(self, fileitem):
calls.append(("exists_dest", fileitem.path))
return True
def delete_media_file(self, fileitem):
calls.append(("delete_dest", fileitem.path))
return False
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
tool = DeleteTransferHistoryTool(session_id="redo-session", user_id="10001")
result = asyncio.run(tool.run(history_id=8))
assert "旧媒体库文件删除失败" in result
assert calls == [
("get", 8),
("exists_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
]
def test_delete_transfer_history_tool_deletes_history_when_old_dest_is_missing(monkeypatch):
"""旧媒体库文件已不存在时应视为已清理,继续删除整理记录。"""
calls = []
history = SimpleNamespace(
id=13,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="link",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
calls.append(("get", history_id))
return history
async def async_delete(self, history_id):
calls.append(("delete_history", history_id))
class FakeStorageChain:
def exists(self, fileitem):
calls.append(("exists_dest", fileitem.path))
return False
def delete_media_file(self, fileitem):
calls.append(("delete_dest", fileitem.path))
return False
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
tool = DeleteTransferHistoryTool(session_id="redo-session", user_id="10001")
result = asyncio.run(tool.run(history_id=13))
assert "已删除整理历史记录" in result
assert "已删除旧媒体库文件" not in result
assert calls == [
("get", 13),
("exists_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_history", 13),
]
def test_delete_transfer_history_tool_keeps_successful_move_dest_as_reorganize_source(monkeypatch):
"""成功 move 记录的目标文件是重新整理输入,不应在删除历史时先删除。"""
calls = []
history = SimpleNamespace(
id=9,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="move",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
calls.append(("get", history_id))
return history
async def async_delete(self, history_id):
calls.append(("delete_history", history_id))
class FakeStorageChain:
def exists(self, fileitem):
calls.append(("exists_dest", fileitem.path))
return True
def delete_media_file(self, fileitem):
calls.append(("delete_dest", fileitem.path))
return True
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
tool = DeleteTransferHistoryTool(session_id="redo-session", user_id="10001")
result = asyncio.run(tool.run(history_id=9))
assert "已删除整理历史记录" in result
assert calls == [
("get", 9),
("delete_history", 9),
]
def test_delete_transfer_history_tool_only_treats_exact_move_as_reorganize_source(monkeypatch):
"""整理方式必须精确等于 move其他模式仍应清理旧目标文件。"""
calls = []
history = SimpleNamespace(
id=11,
title="奔跑吧",
src="/downloads/Keep.Running.mkv",
status=True,
mode="not-move",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
)
class FakeTransferHistoryOper:
async def async_get(self, history_id):
calls.append(("get", history_id))
return history
async def async_delete(self, history_id):
calls.append(("delete_history", history_id))
class FakeStorageChain:
def exists(self, fileitem):
calls.append(("exists_dest", fileitem.path))
return True
def delete_media_file(self, fileitem):
calls.append(("delete_dest", fileitem.path))
return True
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.TransferHistoryOper",
FakeTransferHistoryOper,
)
monkeypatch.setattr(
"app.agent.tools.impl.delete_transfer_history.StorageChain",
FakeStorageChain,
)
tool = DeleteTransferHistoryTool(session_id="redo-session", user_id="10001")
result = asyncio.run(tool.run(history_id=11))
assert "已删除旧媒体库文件" in result
assert calls == [
("get", 11),
("exists_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_dest", "/library/奔跑吧 (2014)/Keep.Running.mkv"),
("delete_history", 11),
]
def test_manual_redo_context_uses_dest_path_for_successful_move_record():
"""成功 move 记录重新整理时,旧目标文件才是可继续整理的输入路径。"""
history = SimpleNamespace(
id=10,
status=True,
title="奔跑吧",
type="电视剧",
category="综艺",
year="2014",
seasons="S01",
episodes="E01",
src="/downloads/Keep.Running.mkv",
src_storage="local",
src_fileitem={
"storage": "local",
"path": "/downloads/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
dest="/library/奔跑吧 (2014)/Keep.Running.mkv",
dest_storage="local",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
mode="move",
tmdbid=100,
doubanid=None,
errmsg=None,
)
context = build_manual_redo_template_context(history)
assert context["source_path"] == "/library/奔跑吧 (2014)/Keep.Running.mkv"
assert context["source_storage"] == "local"
def test_manual_redo_context_only_treats_exact_move_as_dest_source():
"""非 move 整理方式即使名称包含 move也应继续使用原始来源。"""
history = SimpleNamespace(
id=12,
status=True,
title="奔跑吧",
type="电视剧",
category="综艺",
year="2014",
seasons="S01",
episodes="E01",
src="/downloads/Keep.Running.mkv",
src_storage="local",
src_fileitem={
"storage": "local",
"path": "/downloads/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
dest="/library/奔跑吧 (2014)/Keep.Running.mkv",
dest_storage="local",
dest_fileitem={
"storage": "local",
"path": "/library/奔跑吧 (2014)/Keep.Running.mkv",
"name": "Keep.Running.mkv",
"type": "file",
},
mode="not-move",
tmdbid=100,
doubanid=None,
errmsg=None,
)
context = build_manual_redo_template_context(history)
assert context["source_path"] == "/downloads/Keep.Running.mkv"
assert context["source_storage"] == "local"

View File

@@ -1,7 +1,7 @@
from types import SimpleNamespace
from app.agent.prompt import prompt_manager
from app.api.endpoints.history import build_batch_manual_redo_prompt
from app.agent.prompt.transfer_redo import build_batch_manual_redo_prompt
def test_batch_manual_redo_prompt_requires_plain_text_result():
@@ -20,6 +20,7 @@ def test_batch_manual_redo_prompt_requires_plain_text_result():
src_storage="local",
dest="/media/a.mkv",
dest_storage="local",
dest_fileitem=None,
mode="copy",
tmdbid=123,
doubanid=None,

View File

@@ -1,4 +1,5 @@
import unittest
import asyncio
import sys
from types import ModuleType
from types import SimpleNamespace
@@ -89,6 +90,7 @@ class TestTransferFailedRetryButtons(unittest.TestCase):
src_fileitem={"path": "/downloads/Test.Show.S01E01.mkv"},
dest=None,
dest_storage=None,
dest_fileitem=None,
mode="copy",
tmdbid=123,
doubanid=None,
@@ -122,3 +124,72 @@ class TestTransferFailedRetryButtons(unittest.TestCase):
post_message.call_args_list[0].args[0].title,
"已将整理记录 #34 交给智能助手处理",
)
def test_transfer_ai_retry_callback_uses_successful_move_dest_as_source(self):
chain = MessageChain()
captured = {}
history = SimpleNamespace(
id=35,
status=True,
title="Test Show",
type="电视剧",
category=None,
year="2024",
seasons="S01",
episodes="E01",
src="/downloads/Test.Show.S01E01.mkv",
src_storage="local",
src_fileitem={"path": "/downloads/Test.Show.S01E01.mkv"},
dest="/library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
dest_storage="local",
dest_fileitem={
"storage": "local",
"path": "/library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
"name": "Test.Show.S01E01.mkv",
"type": "file",
},
mode="move",
tmdbid=123,
doubanid=None,
errmsg=None,
)
def _run_pending_coro(coro, *args, **kwargs):
asyncio.run(coro)
return SimpleNamespace()
async def fake_run_background_prompt(**kwargs):
captured["message"] = kwargs["message"]
output_callback = kwargs.get("output_callback")
if output_callback:
output_callback("ok")
async def fake_async_post_message(*args, **kwargs):
return None
with patch.object(settings, "AI_AGENT_ENABLE", True):
with patch(
"app.chain.message.TransferHistoryOper"
) as history_oper_cls, patch(
"app.chain.message.agent_manager.run_background_prompt",
side_effect=fake_run_background_prompt,
), patch(
"app.chain.message.asyncio.run_coroutine_threadsafe",
side_effect=_run_pending_coro,
):
history_oper_cls.return_value.get.return_value = history
with patch.object(chain, "post_message"), patch.object(
chain, "async_post_message", side_effect=fake_async_post_message
):
chain._handle_callback(
text="CALLBACK:transfer_ai_retry_35",
channel=MessageChannel.Telegram,
source="telegram-test",
userid="10001",
username="tester",
)
self.assertIn(
"- Source path: /library/Test Show (2024)/Season 1/Test.Show.S01E01.mkv",
captured["message"],
)