From 570ea60096545d6683ccbf906ed0267cd9e7a852 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Fri, 19 Jun 2026 12:10:39 +0800 Subject: [PATCH] fix(agent): prevent chat history persistence for sessions without a channel --- app/agent/__init__.py | 10 +---- tests/test_agent_background_output.py | 6 +-- tests/test_agent_chat_history.py | 55 ++++++++++++++++----------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 73c2bc33..5b411428 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -57,7 +57,7 @@ from app.log import logger from app.schemas import AgentLLMProviderEventData, AgentTokensUsageEventData, Notification, NotificationType from app.schemas.message import ChannelCapabilityManager, ChannelCapability from app.schemas.types import ChainEventType, EventType, MessageChannel -from app.utils.identity import SYSTEM_INTERNAL_USER_ID, is_internal_user_id +from app.utils.identity import SYSTEM_INTERNAL_USER_ID class AgentChain(ChainBase): @@ -227,7 +227,6 @@ class ReplyMode(str, Enum): CAPTURE_ONLY = "capture_only" -INTERNAL_AGENT_SESSION_PREFIX = "__agent_" HEARTBEAT_SESSION_PREFIX = "__agent_heartbeat_" UNSUPPORTED_IMAGE_INPUT_MESSAGE = "当前模型不支持图片输入,请更换支持图片输入的模型,或在系统设置中关闭图片输入支持后重试。" AGENT_EXECUTION_ERROR_PREFIX = "智能助手执行失败" @@ -318,12 +317,7 @@ class MoviePilotAgent: """ 判断当前 Agent 是否需要写入会话历史表。 """ - if self.is_heartbeat_session: - return False - return not ( - is_internal_user_id(self.user_id) - and self.session_id.startswith(INTERNAL_AGENT_SESSION_PREFIX) - ) + return bool(self.channel and self.source) def _save_display_history_messages(self, messages: List[dict]) -> None: """ diff --git a/tests/test_agent_background_output.py b/tests/test_agent_background_output.py index daea15a3..c428bfc0 100644 --- a/tests/test_agent_background_output.py +++ b/tests/test_agent_background_output.py @@ -81,7 +81,7 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase): await agent._execute_agent([]) agent.send_agent_message.assert_not_awaited() - save_messages.assert_called_once() + save_messages.assert_not_called() self.assertEqual("后台结果", agent._streamed_output) async def test_non_streaming_image_unsupported_error_sends_friendly_notice(self): @@ -213,7 +213,7 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase): agent.send_agent_message.assert_awaited_once_with( "后台结果", title="MoviePilot助手" ) - save_messages.assert_called_once() + save_messages.assert_not_called() self.assertEqual("后台结果", agent._streamed_output) async def test_background_non_streaming_captures_without_sending_when_capture_only(self): @@ -236,7 +236,7 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase): await agent._execute_agent([]) agent.send_agent_message.assert_not_awaited() - save_messages.assert_called_once() + save_messages.assert_not_called() self.assertEqual("后台结果", agent._streamed_output) async def test_heartbeat_check_jobs_captures_final_reply_and_keeps_message_tools(self): diff --git a/tests/test_agent_chat_history.py b/tests/test_agent_chat_history.py index 8a9bfcce..3bcf3505 100644 --- a/tests/test_agent_chat_history.py +++ b/tests/test_agent_chat_history.py @@ -111,63 +111,72 @@ def test_agent_prepare_chat_title_generates_title(monkeypatch): assert chat.source == "web-agent" -def test_agent_prepare_chat_title_skips_internal_background_sessions(monkeypatch): - """内部后台任务和心跳会话不应生成标题或创建历史会话。""" +def test_agent_prepare_chat_title_skips_sessions_without_channel(monkeypatch): + """没有渠道来源的 Agent 会话不应生成标题或创建历史会话。""" async def fake_initialize_llm(self, streaming=False): - """后台会话不应初始化标题模型。""" - raise AssertionError("background title generation should be skipped") + """无渠道会话不应初始化标题模型。""" + raise AssertionError("no-channel title generation should be skipped") monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) - for session_id in ( - "__agent_background_title__", - f"{HEARTBEAT_SESSION_PREFIX}title__", + for session_id, user_id in ( + ("__agent_background_title__", SYSTEM_INTERNAL_USER_ID), + (f"{HEARTBEAT_SESSION_PREFIX}title__", SYSTEM_INTERNAL_USER_ID), + ("mcp-title-session", "mcp"), + ("cli-title-session", "cli"), ): agent = MoviePilotAgent( session_id=session_id, - user_id=SYSTEM_INTERNAL_USER_ID, + user_id=user_id, username="admin", ) asyncio.run(agent.prepare_chat_title("后台任务")) assert AgentChatOper().get( session_id=session_id, - user_id=SYSTEM_INTERNAL_USER_ID, + user_id=user_id, ) is None -def test_agent_prepare_chat_title_keeps_user_cli_sessions(monkeypatch): - """用户显式 CLI 会话即使没有渠道来源也应保留标题生成。""" +def test_agent_prepare_chat_title_keeps_message_channel_sessions(monkeypatch): + """带渠道来源的消息会话应保留标题生成。""" class FakeTitleModel: - """测试用 CLI 标题模型。""" + """测试用消息渠道标题模型。""" async def ainvoke(self, messages): - """返回固定 CLI 标题。""" - return SimpleNamespace(content="CLI 会话排查") + """返回固定消息渠道标题。""" + return SimpleNamespace(content="Telegram 会话排查") async def fake_initialize_llm(self, streaming=False): - """返回测试 CLI 标题模型。""" + """返回测试消息渠道标题模型。""" return FakeTitleModel() monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) agent = MoviePilotAgent( - session_id="cli-title-session", - user_id="cli", + session_id="telegram-title-session", + user_id="telegram-user", + channel="Telegram", + source="telegram-main", username="admin", ) asyncio.run(agent.prepare_chat_title("帮我检查配置")) - chat = AgentChatOper().get(session_id="cli-title-session", user_id="cli") + chat = AgentChatOper().get( + session_id="telegram-title-session", + user_id="telegram-user", + ) - assert chat.title == "CLI 会话排查" + assert chat.title == "Telegram 会话排查" + assert chat.channel == "Telegram" + assert chat.source == "telegram-main" -def test_internal_background_agent_execution_does_not_persist_chat_history(monkeypatch): - """内部后台任务执行完成后不应写入 Agent 会话历史表。""" - session_id = "__agent_background_skip_persist__" - user_id = SYSTEM_INTERNAL_USER_ID +def test_agent_execution_without_channel_does_not_persist_chat_history(monkeypatch): + """没有渠道来源的 Agent 执行完成后不应写入会话历史表。""" + session_id = "mcp-skip-persist" + user_id = "mcp" memory_manager.clear_memory(session_id, user_id) class FakeGraphState: