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:
Soulter
2025-11-18 22:04:26 +08:00
parent b984bb2513
commit 31ef3d1084
7 changed files with 484 additions and 234 deletions
+8
View File
@@ -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()
+43
View File
@@ -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."""
...
+103
View File
@@ -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
+38
View File
@@ -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.
+84
View File
@@ -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,
),
)
+68 -70
View File
@@ -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__
+140 -164
View File
@@ -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);