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 MessageChannel DEFAULT_AGENT_CHAT_TITLE = "未命名会话" class AgentChatOper(DbOper): """ Agent 会话历史数据管理。 """ def __init__(self, db: 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[MessageChannel, str]]) -> Optional[str]: """获取渠道枚举的字符串值。""" if isinstance(channel, MessageChannel): 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[MessageChannel, str]] = None, source: Optional[str] = None, original_chat_id: Optional[str] = None, client_session_id: Optional[str] = None, ) -> 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) 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[MessageChannel, 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 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[MessageChannel, str]] = None, source: Optional[str] = None, original_chat_id: Optional[str] = None, client_session_id: Optional[str] = None, title: Optional[str] = None, ) -> 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, ) 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[MessageChannel, str]] = None, source: Optional[str] = None, original_chat_id: Optional[str] = None, client_session_id: Optional[str] = None, ) -> 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, ) 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: Optional[int] = 1, count: Optional[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