fix(agent): prevent chat history persistence for sessions without a channel

This commit is contained in:
jxxghp
2026-06-19 12:10:39 +08:00
parent e02650cce9
commit 570ea60096
3 changed files with 37 additions and 34 deletions
+2 -8
View File
@@ -57,7 +57,7 @@ from app.log import logger
from app.schemas import AgentLLMProviderEventData, AgentTokensUsageEventData, Notification, NotificationType from app.schemas import AgentLLMProviderEventData, AgentTokensUsageEventData, Notification, NotificationType
from app.schemas.message import ChannelCapabilityManager, ChannelCapability from app.schemas.message import ChannelCapabilityManager, ChannelCapability
from app.schemas.types import ChainEventType, EventType, MessageChannel 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): class AgentChain(ChainBase):
@@ -227,7 +227,6 @@ class ReplyMode(str, Enum):
CAPTURE_ONLY = "capture_only" CAPTURE_ONLY = "capture_only"
INTERNAL_AGENT_SESSION_PREFIX = "__agent_"
HEARTBEAT_SESSION_PREFIX = "__agent_heartbeat_" HEARTBEAT_SESSION_PREFIX = "__agent_heartbeat_"
UNSUPPORTED_IMAGE_INPUT_MESSAGE = "当前模型不支持图片输入,请更换支持图片输入的模型,或在系统设置中关闭图片输入支持后重试。" UNSUPPORTED_IMAGE_INPUT_MESSAGE = "当前模型不支持图片输入,请更换支持图片输入的模型,或在系统设置中关闭图片输入支持后重试。"
AGENT_EXECUTION_ERROR_PREFIX = "智能助手执行失败" AGENT_EXECUTION_ERROR_PREFIX = "智能助手执行失败"
@@ -318,12 +317,7 @@ class MoviePilotAgent:
""" """
判断当前 Agent 是否需要写入会话历史表。 判断当前 Agent 是否需要写入会话历史表。
""" """
if self.is_heartbeat_session: return bool(self.channel and self.source)
return False
return not (
is_internal_user_id(self.user_id)
and self.session_id.startswith(INTERNAL_AGENT_SESSION_PREFIX)
)
def _save_display_history_messages(self, messages: List[dict]) -> None: def _save_display_history_messages(self, messages: List[dict]) -> None:
""" """
+3 -3
View File
@@ -81,7 +81,7 @@ class AgentBackgroundOutputTest(unittest.IsolatedAsyncioTestCase):
await agent._execute_agent([]) await agent._execute_agent([])
agent.send_agent_message.assert_not_awaited() agent.send_agent_message.assert_not_awaited()
save_messages.assert_called_once() save_messages.assert_not_called()
self.assertEqual("后台结果", agent._streamed_output) self.assertEqual("后台结果", agent._streamed_output)
async def test_non_streaming_image_unsupported_error_sends_friendly_notice(self): 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( agent.send_agent_message.assert_awaited_once_with(
"后台结果", title="MoviePilot助手" "后台结果", title="MoviePilot助手"
) )
save_messages.assert_called_once() save_messages.assert_not_called()
self.assertEqual("后台结果", agent._streamed_output) self.assertEqual("后台结果", agent._streamed_output)
async def test_background_non_streaming_captures_without_sending_when_capture_only(self): 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([]) await agent._execute_agent([])
agent.send_agent_message.assert_not_awaited() agent.send_agent_message.assert_not_awaited()
save_messages.assert_called_once() save_messages.assert_not_called()
self.assertEqual("后台结果", agent._streamed_output) self.assertEqual("后台结果", agent._streamed_output)
async def test_heartbeat_check_jobs_captures_final_reply_and_keeps_message_tools(self): async def test_heartbeat_check_jobs_captures_final_reply_and_keeps_message_tools(self):
+32 -23
View File
@@ -111,63 +111,72 @@ def test_agent_prepare_chat_title_generates_title(monkeypatch):
assert chat.source == "web-agent" 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): 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) monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm)
for session_id in ( for session_id, user_id in (
"__agent_background_title__", ("__agent_background_title__", SYSTEM_INTERNAL_USER_ID),
f"{HEARTBEAT_SESSION_PREFIX}title__", (f"{HEARTBEAT_SESSION_PREFIX}title__", SYSTEM_INTERNAL_USER_ID),
("mcp-title-session", "mcp"),
("cli-title-session", "cli"),
): ):
agent = MoviePilotAgent( agent = MoviePilotAgent(
session_id=session_id, session_id=session_id,
user_id=SYSTEM_INTERNAL_USER_ID, user_id=user_id,
username="admin", username="admin",
) )
asyncio.run(agent.prepare_chat_title("后台任务")) asyncio.run(agent.prepare_chat_title("后台任务"))
assert AgentChatOper().get( assert AgentChatOper().get(
session_id=session_id, session_id=session_id,
user_id=SYSTEM_INTERNAL_USER_ID, user_id=user_id,
) is None ) is None
def test_agent_prepare_chat_title_keeps_user_cli_sessions(monkeypatch): def test_agent_prepare_chat_title_keeps_message_channel_sessions(monkeypatch):
"""用户显式 CLI 会话即使没有渠道来源也应保留标题生成。""" """带渠道来源的消息会话应保留标题生成。"""
class FakeTitleModel: class FakeTitleModel:
"""测试用 CLI 标题模型。""" """测试用消息渠道标题模型。"""
async def ainvoke(self, messages): async def ainvoke(self, messages):
"""返回固定 CLI 标题。""" """返回固定消息渠道标题。"""
return SimpleNamespace(content="CLI 会话排查") return SimpleNamespace(content="Telegram 会话排查")
async def fake_initialize_llm(self, streaming=False): async def fake_initialize_llm(self, streaming=False):
"""返回测试 CLI 标题模型。""" """返回测试消息渠道标题模型。"""
return FakeTitleModel() return FakeTitleModel()
monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm) monkeypatch.setattr(MoviePilotAgent, "_initialize_llm", fake_initialize_llm)
agent = MoviePilotAgent( agent = MoviePilotAgent(
session_id="cli-title-session", session_id="telegram-title-session",
user_id="cli", user_id="telegram-user",
channel="Telegram",
source="telegram-main",
username="admin", username="admin",
) )
asyncio.run(agent.prepare_chat_title("帮我检查配置")) 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): def test_agent_execution_without_channel_does_not_persist_chat_history(monkeypatch):
"""内部后台任务执行完成后不应写入 Agent 会话历史表。""" """没有渠道来源的 Agent 执行完成后不应写入会话历史表。"""
session_id = "__agent_background_skip_persist__" session_id = "mcp-skip-persist"
user_id = SYSTEM_INTERNAL_USER_ID user_id = "mcp"
memory_manager.clear_memory(session_id, user_id) memory_manager.clear_memory(session_id, user_id)
class FakeGraphState: class FakeGraphState: