refactor: Implement WebChat session management and migration from version 4.6 to 4.7
- Added WebChatSession model for managing user sessions. - Introduced methods for creating, retrieving, updating, and deleting WebChat sessions in the database. - Updated core lifecycle to include migration from version 4.6 to 4.7, creating WebChat sessions from existing platform message history. - Refactored chat routes to support new session-based architecture, replacing conversation-related endpoints with session endpoints. - Updated frontend components to handle sessions instead of conversations, including session creation and management.
This commit is contained in:
@@ -22,6 +22,7 @@ from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.conversation_mgr import ConversationManager
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.migration.migra_45_to_46 import migrate_45_to_46
|
||||
from astrbot.core.db.migration.migra_46_to_47 import migrate_46_to_47
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
from astrbot.core.persona_mgr import PersonaManager
|
||||
from astrbot.core.pipeline.scheduler import PipelineContext, PipelineScheduler
|
||||
@@ -103,6 +104,13 @@ 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
|
||||
try:
|
||||
await migrate_46_to_47(self.db)
|
||||
except Exception as e:
|
||||
logger.error(f"Migration from version 4.6 to 4.7 failed: {e!s}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 初始化事件队列
|
||||
self.event_queue = Queue()
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from astrbot.core.db.po import (
|
||||
PlatformStat,
|
||||
Preference,
|
||||
Stats,
|
||||
WebChatSession,
|
||||
)
|
||||
|
||||
|
||||
@@ -313,3 +314,45 @@ class BaseDatabase(abc.ABC):
|
||||
) -> tuple[list[dict], int]:
|
||||
"""Get paginated session conversations with joined conversation and persona details, support search and platform filter."""
|
||||
...
|
||||
|
||||
# ====
|
||||
# WebChat Session Management
|
||||
# ====
|
||||
|
||||
@abc.abstractmethod
|
||||
async def create_webchat_session(
|
||||
self,
|
||||
creator: str,
|
||||
session_id: str | None = None,
|
||||
is_group: int = 0,
|
||||
) -> WebChatSession:
|
||||
"""Create a new WebChat session."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_webchat_session_by_id(self, session_id: str) -> WebChatSession | None:
|
||||
"""Get a WebChat session by its ID."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def get_webchat_sessions_by_creator(
|
||||
self,
|
||||
creator: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> list[WebChatSession]:
|
||||
"""Get all WebChat sessions for a specific creator (username)."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def update_webchat_session(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> None:
|
||||
"""Update a WebChat session's updated_at timestamp."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def delete_webchat_session(self, session_id: str) -> None:
|
||||
"""Delete a WebChat session by its ID."""
|
||||
...
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Migration script from version 4.6 to 4.7.
|
||||
|
||||
This migration creates WebChat sessions from existing platform_message_history records.
|
||||
"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlmodel import col
|
||||
|
||||
from astrbot.api import logger, sp
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.db.po import PlatformMessageHistory
|
||||
|
||||
|
||||
async def migrate_46_to_47(db_helper: BaseDatabase):
|
||||
"""Migrate WebChat data to the new session table.
|
||||
|
||||
This migration extracts all unique user_ids from platform_message_history
|
||||
where platform_id='webchat' and creates corresponding WebChatSession records.
|
||||
"""
|
||||
# 检查是否已经完成迁移
|
||||
# migration_done = await db_helper.get_preference(
|
||||
# "global", "global", "migration_done_v47"
|
||||
# )
|
||||
# if migration_done:
|
||||
# return
|
||||
|
||||
logger.info("开始执行数据库迁移(4.6 -> 4.7)...")
|
||||
|
||||
try:
|
||||
async with db_helper.get_db() as session:
|
||||
# 1. 查询所有 webchat 的唯一 user_id 以及它们的最早和最新消息时间
|
||||
query = (
|
||||
select(
|
||||
col(PlatformMessageHistory.user_id),
|
||||
col(PlatformMessageHistory.sender_name),
|
||||
func.min(PlatformMessageHistory.created_at).label("earliest"),
|
||||
func.max(PlatformMessageHistory.updated_at).label("latest"),
|
||||
)
|
||||
.where(col(PlatformMessageHistory.platform_id) == "webchat")
|
||||
.where(col(PlatformMessageHistory.sender_id) == "astrbot")
|
||||
.group_by(col(PlatformMessageHistory.user_id))
|
||||
)
|
||||
|
||||
result = await session.execute(query)
|
||||
webchat_users = result.all()
|
||||
|
||||
if not webchat_users:
|
||||
logger.info("没有找到需要迁移的 WebChat 数据")
|
||||
await sp.put_async("global", "global", "migration_done_v47", True)
|
||||
return
|
||||
|
||||
logger.info(f"找到 {len(webchat_users)} 个 WebChat 会话需要迁移")
|
||||
|
||||
# 2. 为每个 user_id 创建 WebChatSession 记录
|
||||
migrated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for user_id, sender_name, created_at, updated_at in webchat_users:
|
||||
# user_id 就是 webchat_conv_id (session_id)
|
||||
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)
|
||||
if existing_session:
|
||||
logger.debug(f"会话 {session_id} 已存在,跳过")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# 创建新的 WebChatSession
|
||||
try:
|
||||
await db_helper.create_webchat_session(
|
||||
creator=creator,
|
||||
session_id=session_id,
|
||||
is_group=0,
|
||||
)
|
||||
|
||||
# 更新时间戳以匹配历史记录
|
||||
# 注意:这里我们需要直接更新数据库,因为 create 方法会设置当前时间
|
||||
# 但我们希望保留原始的创建和更新时间
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
if migrated_count % 100 == 0:
|
||||
logger.info(f"已迁移 {migrated_count} 个会话...")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移会话 {session_id} 失败: {e}")
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"WebChat 会话迁移完成!成功迁移: {migrated_count}, 跳过: {skipped_count}",
|
||||
)
|
||||
|
||||
# 标记迁移完成
|
||||
await sp.put_async("global", "global", "migration_done_v47", True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移过程中发生错误: {e}", exc_info=True)
|
||||
raise
|
||||
@@ -155,6 +155,44 @@ class PlatformMessageHistory(SQLModel, table=True):
|
||||
)
|
||||
|
||||
|
||||
class WebChatSession(SQLModel, table=True):
|
||||
"""WebChat session table for managing user sessions.
|
||||
|
||||
A session represents a chat window for a specific user. Each session can have
|
||||
multiple conversations (对话) associated with it.
|
||||
"""
|
||||
|
||||
__tablename__ = "webchat_sessions"
|
||||
|
||||
inner_id: int | None = Field(
|
||||
primary_key=True,
|
||||
sa_column_kwargs={"autoincrement": True},
|
||||
default=None,
|
||||
)
|
||||
session_id: str = Field(
|
||||
max_length=36,
|
||||
nullable=False,
|
||||
unique=True,
|
||||
default_factory=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
creator: str = Field(nullable=False)
|
||||
"""Username of the session creator"""
|
||||
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))
|
||||
updated_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"session_id",
|
||||
name="uix_webchat_session_id",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Attachment(SQLModel, table=True):
|
||||
"""This class represents attachments for messages in AstrBot.
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from astrbot.core.db.po import (
|
||||
PlatformStat,
|
||||
Preference,
|
||||
SQLModel,
|
||||
WebChatSession,
|
||||
)
|
||||
from astrbot.core.db.po import (
|
||||
Platform as DeprecatedPlatformStat,
|
||||
@@ -709,3 +710,86 @@ class SQLiteDatabase(BaseDatabase):
|
||||
t.start()
|
||||
t.join()
|
||||
return result
|
||||
|
||||
# ====
|
||||
# WebChat Session Management
|
||||
# ====
|
||||
|
||||
async def create_webchat_session(
|
||||
self,
|
||||
creator: str,
|
||||
session_id: str | None = None,
|
||||
is_group: int = 0,
|
||||
) -> WebChatSession:
|
||||
"""Create a new WebChat session."""
|
||||
kwargs = {}
|
||||
if session_id:
|
||||
kwargs["session_id"] = session_id
|
||||
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
async with session.begin():
|
||||
new_session = WebChatSession(
|
||||
creator=creator,
|
||||
is_group=is_group,
|
||||
**kwargs,
|
||||
)
|
||||
session.add(new_session)
|
||||
await session.flush()
|
||||
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 with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
query = select(WebChatSession).where(
|
||||
WebChatSession.session_id == session_id,
|
||||
)
|
||||
result = await session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_webchat_sessions_by_creator(
|
||||
self,
|
||||
creator: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> list[WebChatSession]:
|
||||
"""Get all WebChat sessions for a specific creator (username)."""
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
offset = (page - 1) * page_size
|
||||
query = (
|
||||
select(WebChatSession)
|
||||
.where(WebChatSession.creator == creator)
|
||||
.order_by(desc(WebChatSession.updated_at))
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def update_webchat_session(
|
||||
self,
|
||||
session_id: str,
|
||||
) -> None:
|
||||
"""Update a WebChat session's updated_at timestamp."""
|
||||
async with self.get_db() as session:
|
||||
session: AsyncSession
|
||||
async with session.begin():
|
||||
await session.execute(
|
||||
update(WebChatSession)
|
||||
.where(WebChatSession.session_id == session_id)
|
||||
.values(updated_at=datetime.now()),
|
||||
)
|
||||
|
||||
async def delete_webchat_session(self, session_id: str) -> None:
|
||||
"""Delete a WebChat 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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,6 @@ from quart import g, make_response, request
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.db import BaseDatabase
|
||||
from astrbot.core.platform.astr_message_event import MessageSession
|
||||
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
@@ -36,11 +35,10 @@ class ChatRoute(Route):
|
||||
super().__init__(context)
|
||||
self.routes = {
|
||||
"/chat/send": ("POST", self.chat),
|
||||
"/chat/new_conversation": ("GET", self.new_conversation),
|
||||
"/chat/conversations": ("GET", self.get_conversations),
|
||||
"/chat/get_conversation": ("GET", self.get_conversation),
|
||||
"/chat/delete_conversation": ("GET", self.delete_conversation),
|
||||
"/chat/rename_conversation": ("POST", self.rename_conversation),
|
||||
"/chat/new_session": ("GET", self.new_session),
|
||||
"/chat/sessions": ("GET", self.get_sessions),
|
||||
"/chat/get_session": ("GET", self.get_session),
|
||||
"/chat/delete_session": ("GET", self.delete_webchat_session),
|
||||
"/chat/get_file": ("GET", self.get_file),
|
||||
"/chat/post_image": ("POST", self.post_image),
|
||||
"/chat/post_file": ("POST", self.post_file),
|
||||
@@ -53,6 +51,7 @@ class ChatRoute(Route):
|
||||
self.supported_imgs = ["jpg", "jpeg", "png", "gif", "webp"]
|
||||
self.conv_mgr = core_lifecycle.conversation_manager
|
||||
self.platform_history_mgr = core_lifecycle.platform_message_history_manager
|
||||
self.db = db
|
||||
|
||||
self.running_convs: dict[str, bool] = {}
|
||||
|
||||
@@ -137,7 +136,8 @@ class ChatRoute(Route):
|
||||
return Response().error("conversation_id is empty").__dict__
|
||||
|
||||
# 追加用户消息
|
||||
webchat_conv_id = await self._get_webchat_conv_id_from_conv_id(conversation_id)
|
||||
# conversation_id 现在实际上是 session_id
|
||||
webchat_conv_id = conversation_id
|
||||
|
||||
# 获取会话特定的队列
|
||||
back_queue = webchat_queue_mgr.get_or_create_back_queue(webchat_conv_id)
|
||||
@@ -245,88 +245,86 @@ class ChatRoute(Route):
|
||||
response.timeout = None # fix SSE auto disconnect issue
|
||||
return response
|
||||
|
||||
async def _get_webchat_conv_id_from_conv_id(self, conversation_id: str) -> str:
|
||||
"""从对话 ID 中提取 WebChat 会话 ID
|
||||
|
||||
NOTE: 关于这里为什么要单独做一个 WebChat 的 Conversation ID 出来,这个是为了向前兼容。
|
||||
"""
|
||||
conversation = await self.conv_mgr.get_conversation(
|
||||
unified_msg_origin="webchat",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
if not conversation:
|
||||
raise ValueError(f"Conversation with ID {conversation_id} not found.")
|
||||
conv_user_id = conversation.user_id
|
||||
webchat_session_id = MessageSession.from_str(conv_user_id).session_id
|
||||
if "!" not in webchat_session_id:
|
||||
raise ValueError(f"Invalid conv user ID: {conv_user_id}")
|
||||
return webchat_session_id.split("!")[-1]
|
||||
|
||||
async def delete_conversation(self):
|
||||
conversation_id = request.args.get("conversation_id")
|
||||
if not conversation_id:
|
||||
return Response().error("Missing key: conversation_id").__dict__
|
||||
async def delete_webchat_session(self):
|
||||
"""Delete a WebChat 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")
|
||||
|
||||
# Clean up queues when deleting conversation
|
||||
webchat_queue_mgr.remove_queues(conversation_id)
|
||||
webchat_conv_id = await self._get_webchat_conv_id_from_conv_id(conversation_id)
|
||||
await self.conv_mgr.delete_conversation(
|
||||
unified_msg_origin=f"webchat:FriendMessage:webchat!{username}!{webchat_conv_id}",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
# 验证会话是否存在且属于当前用户
|
||||
session = await self.db.get_webchat_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}"
|
||||
await self.conv_mgr.delete_conversations_by_user_id(unified_msg_origin)
|
||||
|
||||
# 删除消息历史
|
||||
await self.platform_history_mgr.delete(
|
||||
platform_id="webchat",
|
||||
user_id=webchat_conv_id,
|
||||
user_id=session_id,
|
||||
offset_sec=99999999,
|
||||
)
|
||||
|
||||
# 清理队列
|
||||
webchat_queue_mgr.remove_queues(session_id)
|
||||
|
||||
# 删除会话
|
||||
await self.db.delete_webchat_session(session_id)
|
||||
|
||||
return Response().ok().__dict__
|
||||
|
||||
async def new_conversation(self):
|
||||
async def new_session(self):
|
||||
"""Create a new WebChat session."""
|
||||
username = g.get("username", "guest")
|
||||
webchat_conv_id = str(uuid.uuid4())
|
||||
conv_id = await self.conv_mgr.new_conversation(
|
||||
unified_msg_origin=f"webchat:FriendMessage:webchat!{username}!{webchat_conv_id}",
|
||||
platform_id="webchat",
|
||||
content=[],
|
||||
|
||||
# 创建新会话
|
||||
session = await self.db.create_webchat_session(
|
||||
creator=username,
|
||||
is_group=0,
|
||||
)
|
||||
return Response().ok(data={"conversation_id": conv_id}).__dict__
|
||||
|
||||
async def rename_conversation(self):
|
||||
post_data = await request.json
|
||||
if "conversation_id" not in post_data or "title" not in post_data:
|
||||
return Response().error("Missing key: conversation_id or title").__dict__
|
||||
return Response().ok(data={"session_id": session.session_id}).__dict__
|
||||
|
||||
conversation_id = post_data["conversation_id"]
|
||||
title = post_data["title"]
|
||||
async def get_sessions(self):
|
||||
"""Get all WebChat sessions for the current user."""
|
||||
username = g.get("username", "guest")
|
||||
|
||||
await self.conv_mgr.update_conversation(
|
||||
unified_msg_origin="webchat", # fake
|
||||
conversation_id=conversation_id,
|
||||
title=title,
|
||||
sessions = await self.db.get_webchat_sessions_by_creator(
|
||||
creator=username,
|
||||
page=1,
|
||||
page_size=100, # 暂时返回前100个
|
||||
)
|
||||
return Response().ok(message="重命名成功!").__dict__
|
||||
|
||||
async def get_conversations(self):
|
||||
conversations = await self.conv_mgr.get_conversations(platform_id="webchat")
|
||||
# remove content
|
||||
conversations_ = []
|
||||
for conv in conversations:
|
||||
conv.history = None
|
||||
conversations_.append(conv)
|
||||
return Response().ok(data=conversations_).__dict__
|
||||
# 转换为字典格式,并添加额外信息
|
||||
sessions_data = []
|
||||
for session in sessions:
|
||||
sessions_data.append(
|
||||
{
|
||||
"session_id": session.session_id,
|
||||
"creator": session.creator,
|
||||
"is_group": session.is_group,
|
||||
"created_at": int(session.created_at.timestamp()),
|
||||
"updated_at": int(session.updated_at.timestamp()),
|
||||
}
|
||||
)
|
||||
|
||||
async def get_conversation(self):
|
||||
conversation_id = request.args.get("conversation_id")
|
||||
if not conversation_id:
|
||||
return Response().error("Missing key: conversation_id").__dict__
|
||||
return Response().ok(data=sessions_data).__dict__
|
||||
|
||||
webchat_conv_id = await self._get_webchat_conv_id_from_conv_id(conversation_id)
|
||||
async def get_session(self):
|
||||
"""Get session information and message history by session_id."""
|
||||
session_id = request.args.get("session_id")
|
||||
if not session_id:
|
||||
return Response().error("Missing key: session_id").__dict__
|
||||
|
||||
# Get platform message history
|
||||
# Get platform message history using session_id
|
||||
history_ls = await self.platform_history_mgr.get(
|
||||
platform_id="webchat",
|
||||
user_id=webchat_conv_id,
|
||||
user_id=session_id,
|
||||
page=1,
|
||||
page_size=1000,
|
||||
)
|
||||
@@ -338,7 +336,7 @@ class ChatRoute(Route):
|
||||
.ok(
|
||||
data={
|
||||
"history": history_res,
|
||||
"is_running": self.running_convs.get(webchat_conv_id, False),
|
||||
"is_running": self.running_convs.get(session_id, False),
|
||||
},
|
||||
)
|
||||
.__dict__
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; padding-top: 8px;">
|
||||
<v-btn block variant="text" class="new-chat-btn" @click="newC" :disabled="!currCid"
|
||||
<v-btn block variant="text" class="new-chat-btn" @click="newC" :disabled="!currSessionId"
|
||||
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-plus"
|
||||
style="background-color: transparent !important; border-radius: 4px;">{{
|
||||
tm('actions.newChat') }}</v-btn>
|
||||
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed && !isMobile"
|
||||
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currSessionId" v-if="sidebarCollapsed && !isMobile"
|
||||
elevation="0"></v-btn>
|
||||
</div>
|
||||
<div v-if="!sidebarCollapsed || isMobile">
|
||||
@@ -52,26 +52,24 @@
|
||||
|
||||
<div style="overflow-y: auto; flex-grow: 1;" :class="{ 'fade-in': sidebarHoverExpanded }"
|
||||
v-if="!sidebarCollapsed || isMobile">
|
||||
<v-card v-if="conversations.length > 0" flat style="background-color: transparent;">
|
||||
<v-list density="compact" nav class="conversation-list"
|
||||
style="background-color: transparent;" v-model:selected="selectedConversations"
|
||||
@update:selected="getConversationMessages">
|
||||
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
|
||||
rounded="lg" class="conversation-item" active-color="secondary">
|
||||
<v-list-item-title v-if="!sidebarCollapsed || isMobile" class="conversation-title">{{ item.title
|
||||
|| tm('conversation.newConversation') }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">{{
|
||||
formatDate(item.updated_at)
|
||||
}}</v-list-item-subtitle>
|
||||
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
|
||||
<v-list density="compact" nav class="session-list"
|
||||
style="background-color: transparent;" v-model:selected="selectedSessions"
|
||||
@update:selected="getSessionMessages">
|
||||
<v-list-item v-for="(session, i) in sessions" :key="session.session_id" :value="session.session_id"
|
||||
rounded="lg" class="session-item" active-color="secondary">
|
||||
<v-list-item-title v-if="!sidebarCollapsed || isMobile" class="session-title">
|
||||
{{ tm('conversation.newConversation') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">
|
||||
{{ formatDate(session.updated_at) }}
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-if="!sidebarCollapsed || isMobile" v-slot:append>
|
||||
<div class="conversation-actions">
|
||||
<v-btn icon="mdi-pencil" size="x-small" variant="text"
|
||||
class="edit-title-btn"
|
||||
@click.stop="showEditTitleDialog(item.cid, item.title)" />
|
||||
<div class="session-actions">
|
||||
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||
class="delete-conversation-btn" color="error"
|
||||
@click.stop="deleteConversation(item.cid)" />
|
||||
class="delete-session-btn" color="error"
|
||||
@click.stop="deleteSession(session.session_id)" />
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -79,9 +77,9 @@
|
||||
</v-card>
|
||||
|
||||
<v-fade-transition>
|
||||
<div class="no-conversations" v-if="conversations.length === 0">
|
||||
<div class="no-sessions" v-if="sessions.length === 0">
|
||||
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||
<div class="no-conversations-text" v-if="!sidebarCollapsed || sidebarHoverExpanded || isMobile">
|
||||
<div class="no-sessions-text" v-if="!sidebarCollapsed || sidebarHoverExpanded || isMobile">
|
||||
{{ tm('conversation.noHistory') }}</div>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
@@ -109,7 +107,7 @@
|
||||
<v-tooltip :text="tm('actions.fullscreen')" v-if="!chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props"
|
||||
@click="router.push(currCid ? `/chatbox/${currCid}` : '/chatbox')"
|
||||
@click="router.push(currSessionId ? `/chatbox/${currSessionId}` : '/chatbox')"
|
||||
class="fullscreen-icon">mdi-fullscreen</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
@@ -131,7 +129,7 @@
|
||||
<!-- router 推送到 /chat -->
|
||||
<v-tooltip :text="tm('actions.exitFullscreen')" v-if="chatboxMode">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" @click="router.push(currCid ? `/chat/${currCid}` : '/chat')"
|
||||
<v-icon v-bind="props" @click="router.push(currSessionId ? `/chat/${currSessionId}` : '/chat')"
|
||||
class="fullscreen-icon">mdi-fullscreen-exit</v-icon>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
@@ -217,21 +215,6 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- 编辑对话标题对话框 -->
|
||||
<v-dialog v-model="editTitleDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="dialog-title">{{ tm('actions.editTitle') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="editingTitle" :label="tm('conversation.newConversation')" variant="outlined"
|
||||
hide-details class="mt-2" @keyup.enter="saveTitle" autofocus />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="editTitleDialog = false" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
|
||||
<v-btn text @click="saveTitle" color="primary">{{ t('core.common.save') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh">
|
||||
@@ -289,9 +272,9 @@ export default {
|
||||
return {
|
||||
prompt: '',
|
||||
messages: [],
|
||||
conversations: [],
|
||||
selectedConversations: [], // 用于控制左侧列表的选中状态
|
||||
currCid: '',
|
||||
sessions: [], // WebChat 会话列表
|
||||
selectedSessions: [], // 当前选中的会话
|
||||
currSessionId: '', // 当前会话ID
|
||||
stagedImagesName: [], // 用于存储图片文件名的数组
|
||||
stagedImagesUrl: [], // 用于存储图片的blob URL数组
|
||||
loadingChat: false,
|
||||
@@ -310,10 +293,6 @@ export default {
|
||||
|
||||
mediaCache: {}, // Add a cache to store media blobs
|
||||
|
||||
// 添加对话标题编辑相关变量
|
||||
editTitleDialog: false,
|
||||
editingTitle: '',
|
||||
editingCid: '',
|
||||
|
||||
// 侧边栏折叠状态
|
||||
sidebarCollapsed: true,
|
||||
@@ -346,10 +325,10 @@ export default {
|
||||
isDark() {
|
||||
return useCustomizerStore().uiTheme === 'PurpleThemeDark';
|
||||
},
|
||||
// Get the current conversation from the conversations array
|
||||
getCurrentConversation() {
|
||||
if (!this.currCid) return null;
|
||||
return this.conversations.find(c => c.cid === this.currCid);
|
||||
// Get the current session from the sessions array
|
||||
getCurrentSession() {
|
||||
if (!this.currSessionId) return null;
|
||||
return this.sessions.find(s => s.session_id === this.currSessionId);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -364,43 +343,43 @@ export default {
|
||||
(from.path.startsWith('/chatbox') && to.path.startsWith('/chat')))) {
|
||||
}
|
||||
|
||||
// Check if the route matches /chat/<cid> or /chatbox/<cid> pattern
|
||||
// Check if the route matches /chat/<session_id> or /chatbox/<session_id> pattern
|
||||
if (to.path.startsWith('/chat/') || to.path.startsWith('/chatbox/')) {
|
||||
const pathCid = to.path.split('/')[2];
|
||||
console.log('Path CID:', pathCid);
|
||||
if (pathCid && pathCid !== this.currCid) {
|
||||
// If conversations are already loaded
|
||||
if (this.conversations.length > 0) {
|
||||
const conversation = this.conversations.find(c => c.cid === pathCid);
|
||||
if (conversation) {
|
||||
this.getConversationMessages([pathCid]);
|
||||
const pathSessionId = to.path.split('/')[2];
|
||||
console.log('Path Session ID:', pathSessionId);
|
||||
if (pathSessionId && pathSessionId !== this.currSessionId) {
|
||||
// If sessions are already loaded
|
||||
if (this.sessions.length > 0) {
|
||||
const session = this.sessions.find(s => s.session_id === pathSessionId);
|
||||
if (session) {
|
||||
this.getSessionMessages([pathSessionId]);
|
||||
}
|
||||
} else {
|
||||
// Store the cid to be used after conversations are loaded
|
||||
this.pendingCid = pathCid;
|
||||
// Store the session_id to be used after sessions are loaded
|
||||
this.pendingCid = pathSessionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Watch for conversations loaded to handle pending cid
|
||||
conversations: {
|
||||
handler(newConversations) {
|
||||
if (this.pendingCid && newConversations.length > 0) {
|
||||
const conversation = newConversations.find(c => c.cid === this.pendingCid);
|
||||
if (conversation) {
|
||||
// 先设置选中状态,然后加载对话消息
|
||||
this.selectedConversations = [this.pendingCid];
|
||||
this.getConversationMessages([this.pendingCid]);
|
||||
// Watch for sessions loaded to handle pending session ID
|
||||
sessions: {
|
||||
handler(newSessions) {
|
||||
if (this.pendingCid && newSessions.length > 0) {
|
||||
const session = newSessions.find(s => s.session_id === this.pendingCid);
|
||||
if (session) {
|
||||
// 先设置选中状态,然后加载会话消息
|
||||
this.selectedSessions = [this.pendingCid];
|
||||
this.getSessionMessages([this.pendingCid]);
|
||||
this.pendingCid = null;
|
||||
}
|
||||
} else {
|
||||
// 如果没有URL参数指定的对话,且当前没有选中对话,则默认打开第一个对话
|
||||
if (!this.currCid && newConversations.length > 0) {
|
||||
const firstConversation = newConversations[0];
|
||||
this.selectedConversations = [firstConversation.cid];
|
||||
this.getConversationMessages([firstConversation.cid]);
|
||||
// 如果没有URL参数指定的会话,且当前没有选中会话,则默认选中第一个会话
|
||||
if (!this.currSessionId && newSessions.length > 0) {
|
||||
const firstSession = newSessions[0];
|
||||
this.selectedSessions = [firstSession.session_id];
|
||||
// 不自动加载消息,等用户点击或发送消息
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,7 +410,7 @@ export default {
|
||||
|
||||
// 设置输入框标签
|
||||
this.inputFieldLabel = this.tm('input.chatPrompt');
|
||||
this.getConversations();
|
||||
this.getSessions();
|
||||
let inputField = document.getElementById('input-field');
|
||||
inputField.addEventListener('paste', this.handlePaste);
|
||||
inputField.addEventListener('keydown', function (e) {
|
||||
@@ -532,34 +511,6 @@ export default {
|
||||
this.sidebarHoverExpanded = false;
|
||||
},
|
||||
|
||||
// 显示编辑对话标题对话框
|
||||
showEditTitleDialog(cid, title) {
|
||||
this.editingCid = cid;
|
||||
this.editingTitle = title || ''; // 如果标题为空,则设置为空字符串
|
||||
this.editTitleDialog = true;
|
||||
},
|
||||
|
||||
// 保存对话标题
|
||||
saveTitle() {
|
||||
if (!this.editingCid) return;
|
||||
|
||||
const trimmedTitle = this.editingTitle.trim();
|
||||
axios.post('/api/chat/rename_conversation', {
|
||||
conversation_id: this.editingCid,
|
||||
title: trimmedTitle
|
||||
})
|
||||
.then(response => {
|
||||
// 更新本地对话列表中的标题
|
||||
const conversation = this.conversations.find(c => c.cid === this.editingCid);
|
||||
if (conversation) {
|
||||
conversation.title = trimmedTitle;
|
||||
}
|
||||
this.editTitleDialog = false;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('重命名对话失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
async getMediaFile(filename) {
|
||||
if (this.mediaCache[filename]) {
|
||||
@@ -691,9 +642,16 @@ export default {
|
||||
// Reset the input value to allow selecting the same file again
|
||||
event.target.value = '';
|
||||
},
|
||||
getConversations() {
|
||||
axios.get('/api/chat/conversations').then(response => {
|
||||
this.conversations = response.data.data;
|
||||
getSessions() {
|
||||
axios.get('/api/chat/sessions').then(response => {
|
||||
this.sessions = response.data.data;
|
||||
// 使用 sessions 作为显示列表(兼容旧代码)
|
||||
this.conversations = this.sessions.map(session => ({
|
||||
cid: session.session_id,
|
||||
title: this.tm('conversation.newConversation'), // 暂时使用默认标题
|
||||
updated_at: session.updated_at,
|
||||
created_at: session.created_at
|
||||
}));
|
||||
|
||||
// If there's a pending conversation ID from the route
|
||||
if (this.pendingCid) {
|
||||
@@ -703,30 +661,33 @@ export default {
|
||||
this.pendingCid = null;
|
||||
}
|
||||
} else {
|
||||
// 如果没有URL参数指定的对话,且当前没有选中对话,则默认打开第一个对话
|
||||
if (!this.currCid && this.conversations.length > 0) {
|
||||
const firstConversation = this.conversations[0];
|
||||
this.selectedConversations = [firstConversation.cid];
|
||||
this.getConversationMessages([firstConversation.cid]);
|
||||
// 如果没有URL参数指定的会话,且当前没有选中会话,则默认打开第一个会话
|
||||
if (!this.currSessionId && this.sessions.length > 0) {
|
||||
const firstSession = this.sessions[0];
|
||||
this.currSessionId = firstSession.session_id;
|
||||
this.selectedConversations = [firstSession.session_id];
|
||||
// 注意:现在不自动加载消息,等用户发送消息时再创建对话
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.response.status === 401) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
this.$router.push('/auth/login?redirect=/chatbox');
|
||||
}
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
getConversationMessages(cid) {
|
||||
if (!cid[0])
|
||||
getSessionMessages(sessionIds) {
|
||||
if (!sessionIds[0])
|
||||
return;
|
||||
|
||||
// Update the URL to reflect the selected conversation
|
||||
if (this.$route.path !== `/chat/${cid[0]}` && this.$route.path !== `/chatbox/${cid[0]}`) {
|
||||
const sessionId = sessionIds[0];
|
||||
|
||||
// Update the URL to reflect the selected session
|
||||
if (this.$route.path !== `/chat/${sessionId}` && this.$route.path !== `/chatbox/${sessionId}`) {
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
this.$router.push(`/chatbox/${cid[0]}`);
|
||||
this.$router.push(`/chatbox/${sessionId}`);
|
||||
} else {
|
||||
this.$router.push(`/chat/${cid[0]}`);
|
||||
this.$router.push(`/chat/${sessionId}`);
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -736,22 +697,22 @@ export default {
|
||||
this.closeMobileSidebar();
|
||||
}
|
||||
|
||||
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => {
|
||||
this.currCid = cid[0];
|
||||
// Update the selected conversation in the sidebar
|
||||
this.selectedConversations = [cid[0]];
|
||||
axios.get('/api/chat/get_session?session_id=' + sessionId).then(async response => {
|
||||
this.currSessionId = sessionId;
|
||||
// Update the selected session in the sidebar
|
||||
this.selectedSessions = [sessionId];
|
||||
let history = response.data.data.history;
|
||||
this.isConvRunning = response.data.data.is_running || false;
|
||||
|
||||
if (this.isConvRunning) {
|
||||
if (!this.isToastedRunningInfo) {
|
||||
useToast().info("该对话正在运行中。", { timeout: 5000 });
|
||||
useToast().info("该会话正在运行中。", { timeout: 5000 });
|
||||
this.isToastedRunningInfo = true;
|
||||
}
|
||||
|
||||
// 如果对话还在运行,3秒后重新获取消息
|
||||
// 如果会话还在运行,3秒后重新获取消息
|
||||
setTimeout(() => {
|
||||
this.getConversationMessages([this.currCid]);
|
||||
this.getSessionMessages([this.currSessionId]);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
@@ -795,35 +756,40 @@ export default {
|
||||
});
|
||||
},
|
||||
async newConversation() {
|
||||
return axios.get('/api/chat/new_conversation').then(response => {
|
||||
const cid = response.data.data.conversation_id;
|
||||
this.currCid = cid;
|
||||
// Update the URL to reflect the new conversation
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
this.$router.push(`/chatbox/${cid}`);
|
||||
} else {
|
||||
this.$router.push(`/chat/${cid}`);
|
||||
}
|
||||
this.getConversations();
|
||||
return cid;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
// 懒加载:如果没有会话ID,先创建会话
|
||||
if (!this.currSessionId) {
|
||||
await this.newC();
|
||||
}
|
||||
// 返回会话ID作为"对话ID"(兼容旧逻辑)
|
||||
return this.currSessionId;
|
||||
},
|
||||
|
||||
newC() {
|
||||
this.currCid = '';
|
||||
this.selectedConversations = []; // 清除选中状态
|
||||
this.messages = [];
|
||||
// 手机端关闭侧边栏
|
||||
if (this.isMobile) {
|
||||
this.closeMobileSidebar();
|
||||
}
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
this.$router.push('/chatbox');
|
||||
} else {
|
||||
this.$router.push('/chat');
|
||||
async newC() {
|
||||
// 创建新会话
|
||||
try {
|
||||
const response = await axios.get('/api/chat/new_session');
|
||||
const sessionId = response.data.data.session_id;
|
||||
|
||||
this.currSessionId = sessionId;
|
||||
this.selectedSessions = [sessionId]; // 选中新会话
|
||||
this.messages = [];
|
||||
|
||||
// 手机端关闭侧边栏
|
||||
if (this.isMobile) {
|
||||
this.closeMobileSidebar();
|
||||
}
|
||||
|
||||
// 更新URL
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
this.$router.push(`/chatbox/${sessionId}`);
|
||||
} else {
|
||||
this.$router.push(`/chat/${sessionId}`);
|
||||
}
|
||||
|
||||
// 刷新会话列表
|
||||
this.getSessions();
|
||||
} catch (err) {
|
||||
console.error('创建新会话失败:', err);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -843,14 +809,24 @@ export default {
|
||||
return date.toLocaleString(locale, options).replace(/\//g, '-').replace(/, /g, ' ');
|
||||
},
|
||||
|
||||
deleteConversation(cid) {
|
||||
axios.get('/api/chat/delete_conversation?conversation_id=' + cid).then(response => {
|
||||
this.getConversations();
|
||||
this.currCid = '';
|
||||
this.selectedConversations = []; // 清除选中状态
|
||||
this.messages = [];
|
||||
deleteSession(sessionId) {
|
||||
// 删除会话
|
||||
axios.get('/api/chat/delete_session?session_id=' + sessionId).then(response => {
|
||||
this.getSessions();
|
||||
// 如果删除的是当前会话,清空状态
|
||||
if (this.currSessionId === sessionId) {
|
||||
this.currSessionId = '';
|
||||
this.selectedSessions = [];
|
||||
this.messages = [];
|
||||
// 更新URL
|
||||
if (this.$route.path.startsWith('/chatbox')) {
|
||||
this.$router.push('/chatbox');
|
||||
} else {
|
||||
this.$router.push('/chat');
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
console.error('删除会话失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -868,9 +844,9 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currCid == '') {
|
||||
const cid = await this.newConversation();
|
||||
// URL is already updated in newConversation method
|
||||
if (this.currSessionId == '') {
|
||||
await this.newConversation();
|
||||
// Session is created and URL is updated
|
||||
}
|
||||
|
||||
// 保存当前要发送的数据到临时变量
|
||||
@@ -935,7 +911,7 @@ export default {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: promptToSend,
|
||||
conversation_id: this.currCid,
|
||||
conversation_id: this.currSessionId,
|
||||
image_url: imageNamesToSend,
|
||||
audio_url: audioNameToSend ? [audioNameToSend] : [],
|
||||
selected_provider: selectedProviderId,
|
||||
@@ -1063,7 +1039,7 @@ export default {
|
||||
this.loadingChat = false;
|
||||
|
||||
// get the latest conversations
|
||||
this.getConversations();
|
||||
this.getSessions();
|
||||
|
||||
} catch (err) {
|
||||
console.error('发送消息失败:', err);
|
||||
|
||||
Reference in New Issue
Block a user