diff --git a/astrbot/core/core_lifecycle.py b/astrbot/core/core_lifecycle.py index 676e50384..fdf757116 100644 --- a/astrbot/core/core_lifecycle.py +++ b/astrbot/core/core_lifecycle.py @@ -104,7 +104,7 @@ class AstrBotCoreLifecycle: logger.error(f"Migration from version 4.5 to 4.6 failed: {e!s}") logger.error(traceback.format_exc()) - # 4.6 to 4.7 migration for webchat sessions and group feature + # 4.6 to 4.7 migration for platform sessions and group feature try: await migrate_46_to_47(self.db) except Exception as e: diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 456682bd2..48ccb6801 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -13,10 +13,10 @@ from astrbot.core.db.po import ( ConversationV2, Persona, PlatformMessageHistory, + PlatformSession, PlatformStat, Preference, Stats, - WebChatSession, ) @@ -316,43 +316,49 @@ class BaseDatabase(abc.ABC): ... # ==== - # WebChat Session Management + # Platform Session Management # ==== @abc.abstractmethod - async def create_webchat_session( + async def create_platform_session( self, creator: str, + platform_id: str = "webchat", session_id: str | None = None, + display_name: str | None = None, is_group: int = 0, - ) -> WebChatSession: - """Create a new WebChat session.""" + ) -> PlatformSession: + """Create a new Platform session.""" ... @abc.abstractmethod - async def get_webchat_session_by_id(self, session_id: str) -> WebChatSession | None: - """Get a WebChat session by its ID.""" + async def get_platform_session_by_id( + self, session_id: str + ) -> PlatformSession | None: + """Get a Platform session by its ID.""" ... @abc.abstractmethod - async def get_webchat_sessions_by_creator( + async def get_platform_sessions_by_creator( self, creator: str, + platform_id: str | None = None, page: int = 1, page_size: int = 20, - ) -> list[WebChatSession]: - """Get all WebChat sessions for a specific creator (username).""" + ) -> list[PlatformSession]: + """Get all Platform sessions for a specific creator (username) and optionally platform.""" ... @abc.abstractmethod - async def update_webchat_session( + async def update_platform_session( self, session_id: str, + display_name: str | None = None, ) -> None: - """Update a WebChat session's updated_at timestamp.""" + """Update a Platform session's updated_at timestamp and optionally display_name.""" ... @abc.abstractmethod - async def delete_webchat_session(self, session_id: str) -> None: - """Delete a WebChat session by its ID.""" + async def delete_platform_session(self, session_id: str) -> None: + """Delete a Platform session by its ID.""" ... diff --git a/astrbot/core/db/migration/migra_46_to_47.py b/astrbot/core/db/migration/migra_46_to_47.py index 407a840d8..e523551f6 100644 --- a/astrbot/core/db/migration/migra_46_to_47.py +++ b/astrbot/core/db/migration/migra_46_to_47.py @@ -1,6 +1,12 @@ """Migration script from version 4.6 to 4.7. -This migration creates WebChat sessions from existing platform_message_history records. +This migration creates PlatformSession from existing platform_message_history records. + +Changes: +- Creates platform_sessions table +- Adds platform_id field (default: 'webchat') +- Adds display_name field +- Session_id format: {platform_id}_{uuid} """ from sqlalchemy import func, select @@ -12,10 +18,10 @@ from astrbot.core.db.po import PlatformMessageHistory async def migrate_46_to_47(db_helper: BaseDatabase): - """Migrate WebChat data to the new session table. + """Create PlatformSession records from platform_message_history. This migration extracts all unique user_ids from platform_message_history - where platform_id='webchat' and creates corresponding WebChatSession records. + where platform_id='webchat' and creates corresponding PlatformSession records. """ # 检查是否已经完成迁移 migration_done = await db_helper.get_preference( @@ -28,7 +34,7 @@ async def migrate_46_to_47(db_helper: BaseDatabase): try: async with db_helper.get_db() as session: - # 1. 查询所有 webchat 的唯一 user_id 以及它们的最早和最新消息时间 + # 从 platform_message_history 创建 PlatformSession query = ( select( col(PlatformMessageHistory.user_id), @@ -51,7 +57,7 @@ async def migrate_46_to_47(db_helper: BaseDatabase): logger.info(f"找到 {len(webchat_users)} 个 WebChat 会话需要迁移") - # 2. 为每个 user_id 创建 WebChatSession 记录 + # 为每个 user_id 创建 PlatformSession 记录 migrated_count = 0 skipped_count = 0 @@ -60,28 +66,26 @@ async def migrate_46_to_47(db_helper: BaseDatabase): session_id = user_id # sender_name 通常是 username,但可能为 None - # 从第一条消息中提取 creator creator = sender_name if sender_name else "guest" # 检查是否已经存在该会话 - existing_session = await db_helper.get_webchat_session_by_id(session_id) + existing_session = await db_helper.get_platform_session_by_id( + session_id + ) if existing_session: logger.debug(f"会话 {session_id} 已存在,跳过") skipped_count += 1 continue - # 创建新的 WebChatSession + # 创建新的 PlatformSession try: - await db_helper.create_webchat_session( + await db_helper.create_platform_session( creator=creator, session_id=session_id, + platform_id="webchat", is_group=0, ) - # 更新时间戳以匹配历史记录 - # 注意:这里我们需要直接更新数据库,因为 create 方法会设置当前时间 - # 但我们希望保留原始的创建和更新时间 - migrated_count += 1 if migrated_count % 100 == 0: diff --git a/astrbot/core/db/po.py b/astrbot/core/db/po.py index eee4c9dc6..9fc871d08 100644 --- a/astrbot/core/db/po.py +++ b/astrbot/core/db/po.py @@ -161,14 +161,14 @@ class PlatformMessageHistory(SQLModel, table=True): ) -class WebChatSession(SQLModel, table=True): - """WebChat session table for managing user sessions. +class PlatformSession(SQLModel, table=True): + """Platform session table for managing user sessions across different platforms. - A session represents a chat window for a specific user. Each session can have - multiple conversations (对话) associated with it. + A session represents a chat window for a specific user on a specific platform. + Each session can have multiple conversations (对话) associated with it. """ - __tablename__ = "webchat_sessions" + __tablename__ = "platform_sessions" inner_id: int | None = Field( primary_key=True, @@ -176,13 +176,17 @@ class WebChatSession(SQLModel, table=True): default=None, ) session_id: str = Field( - max_length=36, + max_length=100, nullable=False, unique=True, - default_factory=lambda: str(uuid.uuid4()), + default_factory=lambda: f"webchat_{uuid.uuid4()}", ) + platform_id: str = Field(default="webchat", nullable=False) + """Platform identifier (e.g., 'webchat', 'qq', 'discord')""" creator: str = Field(nullable=False) """Username of the session creator""" + display_name: str | None = Field(default=None, max_length=255) + """Display name for the session""" is_group: int = Field(default=0, nullable=False) """0 for private chat, 1 for group chat (not implemented yet)""" created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ -194,7 +198,7 @@ class WebChatSession(SQLModel, table=True): __table_args__ = ( UniqueConstraint( "session_id", - name="uix_webchat_session_id", + name="uix_platform_session_id", ), ) diff --git a/astrbot/core/db/sqlite.py b/astrbot/core/db/sqlite.py index b96a2d3ff..202c9d892 100644 --- a/astrbot/core/db/sqlite.py +++ b/astrbot/core/db/sqlite.py @@ -1,6 +1,7 @@ import asyncio import threading import typing as T +import uuid from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession @@ -12,10 +13,10 @@ from astrbot.core.db.po import ( ConversationV2, Persona, PlatformMessageHistory, + PlatformSession, PlatformStat, Preference, SQLModel, - WebChatSession, ) from astrbot.core.db.po import ( Platform as DeprecatedPlatformStat, @@ -712,25 +713,32 @@ class SQLiteDatabase(BaseDatabase): return result # ==== - # WebChat Session Management + # Platform Session Management # ==== - async def create_webchat_session( + async def create_platform_session( self, creator: str, + platform_id: str = "webchat", session_id: str | None = None, + display_name: str | None = None, is_group: int = 0, - ) -> WebChatSession: - """Create a new WebChat session.""" + ) -> PlatformSession: + """Create a new Platform session.""" kwargs = {} if session_id: kwargs["session_id"] = session_id + else: + # Auto-generate session_id with platform_id prefix + kwargs["session_id"] = f"{platform_id}_{uuid.uuid4()}" async with self.get_db() as session: session: AsyncSession async with session.begin(): - new_session = WebChatSession( + new_session = PlatformSession( creator=creator, + platform_id=platform_id, + display_name=display_name, is_group=is_group, **kwargs, ) @@ -739,57 +747,68 @@ class SQLiteDatabase(BaseDatabase): await session.refresh(new_session) return new_session - async def get_webchat_session_by_id(self, session_id: str) -> WebChatSession | None: - """Get a WebChat session by its ID.""" + async def get_platform_session_by_id( + self, session_id: str + ) -> PlatformSession | None: + """Get a Platform session by its ID.""" async with self.get_db() as session: session: AsyncSession - query = select(WebChatSession).where( - WebChatSession.session_id == session_id, + query = select(PlatformSession).where( + PlatformSession.session_id == session_id, ) result = await session.execute(query) return result.scalar_one_or_none() - async def get_webchat_sessions_by_creator( + async def get_platform_sessions_by_creator( self, creator: str, + platform_id: str | None = None, page: int = 1, page_size: int = 20, - ) -> list[WebChatSession]: - """Get all WebChat sessions for a specific creator (username).""" + ) -> list[PlatformSession]: + """Get all Platform sessions for a specific creator (username) and optionally platform.""" async with self.get_db() as session: session: AsyncSession offset = (page - 1) * page_size + query = select(PlatformSession).where(PlatformSession.creator == creator) + + if platform_id: + query = query.where(PlatformSession.platform_id == platform_id) + query = ( - select(WebChatSession) - .where(WebChatSession.creator == creator) - .order_by(desc(WebChatSession.updated_at)) + query.order_by(desc(PlatformSession.updated_at)) .offset(offset) .limit(page_size) ) result = await session.execute(query) return list(result.scalars().all()) - async def update_webchat_session( + async def update_platform_session( self, session_id: str, + display_name: str | None = None, ) -> None: - """Update a WebChat session's updated_at timestamp.""" + """Update a Platform session's updated_at timestamp and optionally display_name.""" async with self.get_db() as session: session: AsyncSession async with session.begin(): + values = {"updated_at": datetime.now()} + if display_name is not None: + values["display_name"] = display_name + await session.execute( - update(WebChatSession) - .where(WebChatSession.session_id == session_id) - .values(updated_at=datetime.now()), + update(PlatformSession) + .where(PlatformSession.session_id == session_id) + .values(**values), ) - async def delete_webchat_session(self, session_id: str) -> None: - """Delete a WebChat session by its ID.""" + async def delete_platform_session(self, session_id: str) -> None: + """Delete a Platform session by its ID.""" async with self.get_db() as session: session: AsyncSession async with session.begin(): await session.execute( - delete(WebChatSession).where( - WebChatSession.session_id == session_id, + delete(PlatformSession).where( + PlatformSession.session_id == session_id, ), ) diff --git a/astrbot/dashboard/routes/chat.py b/astrbot/dashboard/routes/chat.py index 8eacc5c4f..70e52e778 100644 --- a/astrbot/dashboard/routes/chat.py +++ b/astrbot/dashboard/routes/chat.py @@ -39,6 +39,10 @@ class ChatRoute(Route): "/chat/sessions": ("GET", self.get_sessions), "/chat/get_session": ("GET", self.get_session), "/chat/delete_session": ("GET", self.delete_webchat_session), + "/chat/update_session_display_name": ( + "POST", + self.update_session_display_name, + ), "/chat/get_file": ("GET", self.get_file), "/chat/post_image": ("POST", self.post_image), "/chat/post_file": ("POST", self.post_file), @@ -246,56 +250,74 @@ class ChatRoute(Route): return response async def delete_webchat_session(self): - """Delete a WebChat session and all its related data.""" + """Delete a Platform session and all its related data.""" session_id = request.args.get("session_id") if not session_id: return Response().error("Missing key: session_id").__dict__ username = g.get("username", "guest") # 验证会话是否存在且属于当前用户 - session = await self.db.get_webchat_session_by_id(session_id) + session = await self.db.get_platform_session_by_id(session_id) if not session: return Response().error(f"Session {session_id} not found").__dict__ if session.creator != username: return Response().error("Permission denied").__dict__ # 删除该会话下的所有对话 - unified_msg_origin = f"webchat:FriendMessage:webchat!{username}!{session_id}" + unified_msg_origin = f"{session.platform_id}:FriendMessage:{session.platform_id}!{username}!{session_id}" await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin) # 删除消息历史 await self.platform_history_mgr.delete( - platform_id="webchat", + platform_id=session.platform_id, user_id=session_id, offset_sec=99999999, ) - # 清理队列 - webchat_queue_mgr.remove_queues(session_id) + # 清理队列(仅对 webchat) + if session.platform_id == "webchat": + webchat_queue_mgr.remove_queues(session_id) # 删除会话 - await self.db.delete_webchat_session(session_id) + await self.db.delete_platform_session(session_id) return Response().ok().__dict__ async def new_session(self): - """Create a new WebChat session.""" + """Create a new Platform session (default: webchat).""" username = g.get("username", "guest") + # 获取可选的 platform_id 参数,默认为 webchat + platform_id = request.args.get("platform_id", "webchat") + # 创建新会话 - session = await self.db.create_webchat_session( + session = await self.db.create_platform_session( creator=username, + platform_id=platform_id, is_group=0, ) - return Response().ok(data={"session_id": session.session_id}).__dict__ + return ( + Response() + .ok( + data={ + "session_id": session.session_id, + "platform_id": session.platform_id, + } + ) + .__dict__ + ) async def get_sessions(self): - """Get all WebChat sessions for the current user.""" + """Get all Platform sessions for the current user.""" username = g.get("username", "guest") - sessions = await self.db.get_webchat_sessions_by_creator( + # 获取可选的 platform_id 参数 + platform_id = request.args.get("platform_id") + + sessions = await self.db.get_platform_sessions_by_creator( creator=username, + platform_id=platform_id, page=1, page_size=100, # 暂时返回前100个 ) @@ -306,7 +328,9 @@ class ChatRoute(Route): sessions_data.append( { "session_id": session.session_id, + "platform_id": session.platform_id, "creator": session.creator, + "display_name": session.display_name, "is_group": session.is_group, "created_at": int(session.created_at.timestamp()), "updated_at": int(session.updated_at.timestamp()), @@ -321,9 +345,13 @@ class ChatRoute(Route): if not session_id: return Response().error("Missing key: session_id").__dict__ + # 获取会话信息以确定 platform_id + session = await self.db.get_platform_session_by_id(session_id) + platform_id = session.platform_id if session else "webchat" + # Get platform message history using session_id history_ls = await self.platform_history_mgr.get( - platform_id="webchat", + platform_id=platform_id, user_id=session_id, page=1, page_size=1000, @@ -341,3 +369,32 @@ class ChatRoute(Route): ) .__dict__ ) + + async def update_session_display_name(self): + """Update a Platform session's display name.""" + post_data = await request.json + + session_id = post_data.get("session_id") + display_name = post_data.get("display_name") + + if not session_id: + return Response().error("Missing key: session_id").__dict__ + if display_name is None: + return Response().error("Missing key: display_name").__dict__ + + username = g.get("username", "guest") + + # 验证会话是否存在且属于当前用户 + session = await self.db.get_platform_session_by_id(session_id) + if not session: + return Response().error(f"Session {session_id} not found").__dict__ + if session.creator != username: + return Response().error("Permission denied").__dict__ + + # 更新 display_name + await self.db.update_platform_session( + session_id=session_id, + display_name=display_name, + ) + + return Response().ok().__dict__ diff --git a/dashboard/src/components/chat/Chat.vue b/dashboard/src/components/chat/Chat.vue index 69cb5d6a8..e1a912032 100644 --- a/dashboard/src/components/chat/Chat.vue +++ b/dashboard/src/components/chat/Chat.vue @@ -59,7 +59,7 @@ - {{ tm('conversation.newConversation') }} + {{ session.display_name || tm('conversation.newConversation') }} {{ formatDate(session.updated_at) }} @@ -67,6 +67,9 @@ @@ -1368,14 +1435,31 @@ export default { transition: all 0.2s ease; } +.session-item:hover .session-actions { + opacity: 1; + visibility: visible; +} + +.session-actions { + display: flex; + gap: 4px; + opacity: 0; + visibility: hidden; + transition: all 0.2s ease; +} + .edit-title-btn, -.delete-conversation-btn { +.delete-conversation-btn, +.edit-session-btn, +.delete-session-btn { opacity: 0.7; transition: opacity 0.2s ease; } .edit-title-btn:hover, -.delete-conversation-btn:hover { +.delete-conversation-btn:hover, +.edit-session-btn:hover, +.delete-session-btn:hover { opacity: 1; } diff --git a/dashboard/src/i18n/locales/en-US/features/chat.json b/dashboard/src/i18n/locales/en-US/features/chat.json index bce552aff..bc83787ff 100644 --- a/dashboard/src/i18n/locales/en-US/features/chat.json +++ b/dashboard/src/i18n/locales/en-US/features/chat.json @@ -47,7 +47,11 @@ "noHistory": "No conversation history", "systemStatus": "System Status", "llmService": "LLM Service", - "speechToText": "Speech to Text" + "speechToText": "Speech to Text", + "editDisplayName": "Edit Session Name", + "displayName": "Session Name", + "displayNameUpdated": "Session name updated", + "displayNameUpdateFailed": "Failed to update session name" }, "modes": { "darkMode": "Switch to Dark Mode", diff --git a/dashboard/src/i18n/locales/zh-CN/features/chat.json b/dashboard/src/i18n/locales/zh-CN/features/chat.json index 37bacd408..8130ecd8b 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/chat.json +++ b/dashboard/src/i18n/locales/zh-CN/features/chat.json @@ -47,7 +47,11 @@ "noHistory": "暂无对话历史", "systemStatus": "系统状态", "llmService": "LLM 服务", - "speechToText": "语音转文本" + "speechToText": "语音转文本", + "editDisplayName": "编辑会话名称", + "displayName": "会话名称", + "displayNameUpdated": "会话名称已更新", + "displayNameUpdateFailed": "更新会话名称失败" }, "modes": { "darkMode": "切换到夜间模式",