Files
MoviePilot/app/db/oper/agentchat.py
T
jxxghp 240a4dffe6 refactor(schemas): 统一 message/notification 命名边界,旧名收敛至兼容映射表
- notification 域:渠道能力(MessageChannel→NotificationChannel、ChannelCapability* 迁入 notification.py)
- message 域:消息收发(Notification→Message、NotificationType→MessageType、CommingMessage→IncomingMessage、NotificationHistoryItem→MessageHistoryItem、NotificationClear*→MessageClear*)
- Agent 工具契约:send_notification_message→send_message、notification_callback→message_callback
- 源码不保留旧名物理别名,旧导入经 app/runtime/compat/manifest.py SYMBOL_ALIASES 惰性解析
- API 路径与持久化键冻结不变,前端零改动
- 新增兼容守护测试与 docs/rules/07 命名边界规范
2026-08-16 19:32:20 +08:00

345 lines
11 KiB
Python

import time
from typing import Any, Optional, Union
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from app.db import DbOper
from app.db.models.agentchat import AgentChat
from app.schemas.types import NotificationChannel
DEFAULT_AGENT_CHAT_TITLE = "未命名会话"
class AgentChatOper(DbOper):
"""
Agent 会话历史数据管理。
"""
def __init__(self, db: Optional[Union[Session, AsyncSession]] = None):
super().__init__(db)
@staticmethod
def _now() -> str:
"""返回数据库统一使用的当前时间字符串。"""
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
@staticmethod
def _channel_value(channel: Optional[Union[NotificationChannel, str]]) -> Optional[str]:
"""获取渠道枚举的字符串值。"""
if isinstance(channel, NotificationChannel):
return channel.value
return channel
@staticmethod
def _normalize_messages(messages: Optional[list[dict]]) -> list[dict]:
"""规范化展示消息列表,避免 JSON 字段存入 None。"""
return messages if isinstance(messages, list) else []
@staticmethod
def _normalize_title(value: Optional[str], messages: list[dict]) -> str:
"""生成会话标题。"""
if value and value.strip():
return value.strip()[:120]
for message in messages:
if message.get("role") != "user":
continue
content = str(message.get("content") or "").strip()
if content:
return content.replace("\n", " ")[:120]
attachments = message.get("attachments")
if isinstance(attachments, list) and attachments:
name = attachments[0].get("name") or "附件消息"
return str(name)[:120]
return DEFAULT_AGENT_CHAT_TITLE
@staticmethod
def has_custom_title(value: Optional[str]) -> bool:
"""判断会话是否已有真实标题。"""
return bool(value and value.strip() and value.strip() != DEFAULT_AGENT_CHAT_TITLE)
@staticmethod
def _normalize_preview(messages: list[dict]) -> str:
"""生成会话预览文本。"""
for message in reversed(messages):
content = str(message.get("content") or "").strip()
if content:
return content.replace("\n", " ")[:240]
attachments = message.get("attachments")
if isinstance(attachments, list) and attachments:
name = attachments[0].get("name") or "附件消息"
return str(name)[:240]
return ""
def get(
self, session_id: str, user_id: Optional[str] = None
) -> Optional[AgentChat]:
"""
获取 Agent 会话。
"""
return AgentChat.get_by_session(self._db, session_id, user_id)
async def async_get(
self, session_id: str, user_id: Optional[str] = None
) -> Optional[AgentChat]:
"""
异步获取 Agent 会话。
"""
return await AgentChat.async_get_by_session(self._db, session_id, user_id)
def ensure_session(
self,
session_id: str,
user_id: Optional[str] = None,
username: Optional[str] = None,
channel: Optional[Union[NotificationChannel, str]] = None,
source: Optional[str] = None,
original_chat_id: Optional[str] = None,
client_session_id: Optional[str] = None,
) -> Optional[AgentChat]:
"""
确保 Agent 会话记录存在,并刷新基础渠道信息。
"""
now = self._now()
chat = self.get(session_id=session_id, user_id=user_id)
if not chat:
chat = self.get(session_id=session_id)
payload = {
"user_id": user_id,
"username": username,
"channel": self._channel_value(channel),
"source": source,
"original_chat_id": original_chat_id,
"client_session_id": client_session_id,
"updated_at": now,
}
payload = {key: value for key, value in payload.items() if value is not None}
if chat:
chat.update(self._db, payload)
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
chat = AgentChat(
session_id=session_id,
user_id=user_id,
username=username,
channel=self._channel_value(channel),
source=source,
original_chat_id=original_chat_id,
client_session_id=client_session_id,
title=DEFAULT_AGENT_CHAT_TITLE,
preview="",
agent_messages=[],
display_messages=[],
message_count=0,
created_at=now,
updated_at=now,
)
chat.create(self._db)
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
def save_agent_messages(
self,
session_id: str,
user_id: Optional[str],
messages: list[dict],
) -> None:
"""
保存可恢复 Agent 上下文的原始消息。
"""
chat = self.get(session_id=session_id, user_id=user_id)
if not chat:
chat = self.get(session_id=session_id)
if not chat:
chat = self.ensure_session(session_id=session_id, user_id=user_id)
if not chat:
return
chat.update(
self._db,
{
"agent_messages": messages or [],
"updated_at": self._now(),
},
)
def update_title_if_empty(
self,
session_id: str,
user_id: Optional[str],
title: Optional[str],
username: Optional[str] = None,
channel: Optional[Union[NotificationChannel, str]] = None,
source: Optional[str] = None,
original_chat_id: Optional[str] = None,
client_session_id: Optional[str] = None,
) -> None:
"""
在会话尚未生成标题时写入标题。
"""
normalized_title = self._normalize_title(title, [])
if normalized_title == DEFAULT_AGENT_CHAT_TITLE:
return
chat = self.ensure_session(
session_id=session_id,
user_id=user_id,
username=username,
channel=channel,
source=source,
original_chat_id=original_chat_id,
client_session_id=client_session_id,
)
if not chat:
return
if self.has_custom_title(chat.title):
return
chat.update(
self._db,
{
"title": normalized_title,
"updated_at": self._now(),
},
)
def save_display_messages(
self,
session_id: str,
user_id: Optional[str] = None,
messages: Optional[list[dict]] = None,
username: Optional[str] = None,
channel: Optional[Union[NotificationChannel, str]] = None,
source: Optional[str] = None,
original_chat_id: Optional[str] = None,
client_session_id: Optional[str] = None,
title: Optional[str] = None,
) -> Optional[AgentChat]:
"""
保存用户可见的 Agent 会话消息。
"""
normalized_messages = self._normalize_messages(messages)
chat = self.ensure_session(
session_id=session_id,
user_id=user_id,
username=username,
channel=channel,
source=source,
original_chat_id=original_chat_id,
client_session_id=client_session_id,
)
if not chat:
return None
normalized_title = (
chat.title
if self.has_custom_title(chat.title)
else self._normalize_title(title, normalized_messages)
)
chat.update(
self._db,
{
"title": normalized_title,
"preview": self._normalize_preview(normalized_messages),
"display_messages": normalized_messages,
"message_count": len(normalized_messages),
"updated_at": self._now(),
},
)
return self.get(session_id=session_id, user_id=user_id) or self.get(session_id=session_id)
def append_display_messages(
self,
session_id: str,
user_id: Optional[str] = None,
messages: Optional[list[dict]] = None,
username: Optional[str] = None,
channel: Optional[Union[NotificationChannel, str]] = None,
source: Optional[str] = None,
original_chat_id: Optional[str] = None,
client_session_id: Optional[str] = None,
) -> Optional[AgentChat]:
"""
追加一组用户可见的 Agent 会话消息。
"""
chat = self.ensure_session(
session_id=session_id,
user_id=user_id,
username=username,
channel=channel,
source=source,
original_chat_id=original_chat_id,
client_session_id=client_session_id,
)
if not chat:
return None
display_messages = self._normalize_messages(chat.display_messages)
display_messages.extend(self._normalize_messages(messages))
title = chat.title if self.has_custom_title(chat.title) else None
return self.save_display_messages(
session_id=session_id,
user_id=user_id,
messages=display_messages,
username=username or chat.username,
channel=channel or chat.channel,
source=source or chat.source,
original_chat_id=original_chat_id or chat.original_chat_id,
client_session_id=client_session_id or chat.client_session_id,
title=title,
)
async def async_list_by_page(
self,
page: int = 1,
count: int = 30,
user_id: Optional[str] = None,
username: Optional[str] = None,
) -> list[AgentChat]:
"""
异步分页获取 Agent 会话历史。
"""
return await AgentChat.async_list_by_page(
self._db,
page=page,
count=count,
user_id=user_id,
username=username,
)
async def async_delete(
self, session_id: str, user_id: Optional[str] = None
) -> bool:
"""
异步删除 Agent 会话历史。
"""
chat = await self.async_get(session_id=session_id, user_id=user_id)
if not chat:
return False
await AgentChat.async_delete(self._db, chat.id)
return True
@staticmethod
def to_summary(chat: AgentChat) -> dict[str, Any]:
"""
转换为历史会话摘要。
"""
return {
"id": chat.id,
"session_id": chat.session_id,
"client_session_id": chat.client_session_id,
"title": chat.title,
"channel": chat.channel,
"source": chat.source,
"user_id": chat.user_id,
"username": chat.username,
"original_chat_id": chat.original_chat_id,
"message_count": chat.message_count or 0,
"created_at": chat.created_at,
"updated_at": chat.updated_at,
}
@classmethod
def to_detail(cls, chat: AgentChat) -> dict[str, Any]:
"""
转换为历史会话详情。
"""
data = cls.to_summary(chat)
data["messages"] = chat.display_messages or []
return data