添加会话管理

This commit is contained in:
dengquanzhang 2025-12-21 23:16:49 +08:00
parent b9d7f495ad
commit 00aa1e0d3d
11 changed files with 788 additions and 72 deletions

View File

@ -0,0 +1,408 @@
# 会话管理功能实现计划
## 需求概述
实现一个会话管理功能store为每个会话保存一个memory在本地持久化保存。支持新建会话和切换到历史会话。
## 现有代码分析
1. **Store结构** (`src/stores/`)使用Pinia + pinia-plugin-persistedstate进行持久化
- `index.ts`:配置持久化插件
- `modules/counter.ts`、`user.ts`、`routeCache.ts`示例store
2. **类型定义** (`src/types/chat.ts`):已有`ChatMessage`接口
3. **API定义** (`src/api/agent/index.ts`)`StreamApiParams`包含`memory`对象(`thread`和`resource`字段)
4. **聊天页面** (`src/pages/llm-chat/index.vue`)当前单会话实现未使用memory参数
## 用户需求确认
- **UI交互**:下拉菜单切换会话
- **数据结构**扩展会话结构包含消息列表、memory对象等
- **页面集成**:组合式组件
- **memory管理**前端管理生成thread/resource
- **标题生成**:自动生成(从第一条消息内容)
## 待解决的问题
1. memory中`thread`和`resource`字段的具体生成规则?
2. 会话标题自动生成的详细规则(截取字数、处理空消息等)?
3. 是否需要会话删除、重命名功能?
4. 持久化数据迁移和版本兼容性考虑?
## 详细设计
### 1. 类型定义
创建 `src/types/session.ts` 定义以下接口:
```typescript
export interface SessionMemory {
thread: string; // 会话线程ID前端生成如UUID或会话ID
resource: string; // 资源标识,前端生成(如固定值或动态标识)
}
export interface ChatSession {
id: string; // 会话唯一标识UUID
title: string; // 会话标题(自动生成)
createdAt: Date; // 创建时间
updatedAt: Date; // 最后更新时间
messages: ChatMessage[]; // 消息列表引用现有ChatMessage类型
memory: SessionMemory; // 会话memory
}
// 扩展现有ChatMessage类型可选添加sessionId字段
// 或者在store中通过关联关系管理
```
### 2. 会话管理Store
创建 `src/stores/modules/chatSession.ts`
```typescript
import { defineStore } from 'pinia'
import type { ChatSession, SessionMemory } from '@/types/session'
import type { ChatMessage } from '@/types/chat'
interface ChatSessionState {
sessions: ChatSession[] // 所有会话
currentSessionId: string | null // 当前会话ID
}
// 生成唯一ID和memory的辅助函数
const generateId = () => crypto.randomUUID()
const generateMemory = (sessionId: string): SessionMemory => ({
thread: sessionId,
resource: 'webagent_chat' // 固定资源标识,可根据需要调整
})
const generateTitle = (firstMessage: string): string => {
const maxLength = 20
return firstMessage.length > maxLength
? firstMessage.substring(0, maxLength) + '...'
: firstMessage
}
export const useChatSessionStore = defineStore('chatSession', () => {
const state = reactive<ChatSessionState>({
sessions: [],
currentSessionId: null
})
// 获取当前会话
const currentSession = computed(() =>
state.sessions.find(s => s.id === state.currentSessionId)
)
// 创建新会话
const createSession = (initialMessage?: string) => {
const sessionId = generateId()
const now = new Date()
const session: ChatSession = {
id: sessionId,
title: initialMessage ? generateTitle(initialMessage) : '新会话',
createdAt: now,
updatedAt: now,
messages: [],
memory: generateMemory(sessionId)
}
state.sessions.push(session)
state.currentSessionId = sessionId
return session
}
// 切换到指定会话
const switchSession = (sessionId: string) => {
const session = state.sessions.find(s => s.id === sessionId)
if (session) {
state.currentSessionId = sessionId
}
}
// 更新会话(添加消息、更新标题等)
const updateSession = (sessionId: string, updates: Partial<ChatSession>) => {
const index = state.sessions.findIndex(s => s.id === sessionId)
if (index !== -1) {
state.sessions[index] = {
...state.sessions[index],
...updates,
updatedAt: new Date()
}
}
}
// 添加消息到当前会话
const addMessageToCurrentSession = (message: ChatMessage) => {
const session = currentSession.value
if (session) {
const messages = [...session.messages, message]
updateSession(session.id, { messages })
// 如果是第一条消息且标题为默认值,自动生成标题
if (messages.length === 1 && session.title === '新会话') {
const title = generateTitle(message.content)
updateSession(session.id, { title })
}
}
}
// 删除会话
const deleteSession = (sessionId: string) => {
state.sessions = state.sessions.filter(s => s.id !== sessionId)
if (state.currentSessionId === sessionId) {
state.currentSessionId = state.sessions[0]?.id || null
}
}
return {
// 状态
sessions: computed(() => state.sessions),
currentSessionId: computed(() => state.currentSessionId),
currentSession,
// 方法
createSession,
switchSession,
updateSession,
addMessageToCurrentSession,
deleteSession
}
}, {
persist: true // 启用持久化
})
```
### 3. 组合式组件
创建 `src/composables/useSessionManager.ts`
```typescript
import { useChatSessionStore } from '@/stores/modules/chatSession'
import type { ChatMessage } from '@/types/chat'
export function useSessionManager() {
const sessionStore = useChatSessionStore()
// 初始化:如果没有会话,创建一个默认会话
const initialize = () => {
if (sessionStore.sessions.length === 0) {
sessionStore.createSession()
}
}
// 获取当前会话的memory用于API调用
const getCurrentSessionMemory = () => {
return sessionStore.currentSession?.memory
}
// 处理新消息添加到当前会话返回更新后的memory
const handleNewMessage = (message: ChatMessage) => {
sessionStore.addMessageToCurrentSession(message)
return getCurrentSessionMemory()
}
// 新建会话(带可选初始消息)
const newSession = (initialMessage?: string) => {
return sessionStore.createSession(initialMessage)
}
return {
// 状态
sessions: sessionStore.sessions,
currentSession: sessionStore.currentSession,
currentSessionId: sessionStore.currentSessionId,
// 方法
initialize,
getCurrentSessionMemory,
handleNewMessage,
newSession,
switchSession: sessionStore.switchSession,
deleteSession: sessionStore.deleteSession
}
}
```
### 4. UI组件 - 下拉菜单切换器
创建 `src/components/SessionSelector.vue`
```vue
<template>
<div class="session-selector">
<van-dropdown-menu>
<van-dropdown-item
v-model="currentSessionId"
:options="sessionOptions"
@change="onSessionChange"
>
<template #title>
<span class="session-title">{{ currentSessionTitle }}</span>
</template>
</van-dropdown-item>
</van-dropdown-menu>
<van-button
size="small"
@click="handleNewSession"
class="new-session-btn"
>
新建会话
</van-button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useSessionManager } from '@/composables/useSessionManager'
const sessionManager = useSessionManager()
const sessionOptions = computed(() =>
sessionManager.sessions.map(session => ({
text: session.title,
value: session.id
}))
)
const currentSessionId = computed({
get: () => sessionManager.currentSessionId,
set: (value) => {
if (value && value !== sessionManager.currentSessionId) {
sessionManager.switchSession(value)
}
}
})
const currentSessionTitle = computed(() =>
sessionManager.currentSession?.title || '选择会话'
)
const onSessionChange = (sessionId: string) => {
sessionManager.switchSession(sessionId)
}
const handleNewSession = () => {
sessionManager.newSession()
}
</script>
<style scoped>
.session-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: white;
border-bottom: 1px solid #f0f0f0;
}
.session-title {
font-weight: 500;
}
</style>
```
### 5. 修改现有llm-chat页面
更新 `src/pages/llm-chat/index.vue`
主要修改:
1. 在页面顶部添加SessionSelector组件
2. 初始化会话管理器
3. 修改handleSend函数以使用会话memory
4. 消息存储从本地数组改为会话store
关键修改点:
```typescript
// 在script setup顶部引入
import { useSessionManager } from '@/composables/useSessionManager'
import SessionSelector from '@/components/SessionSelector.vue'
// 替换现有的messages状态
const sessionManager = useSessionManager()
const messages = computed(() => sessionManager.currentSession?.messages || [])
// 修改handleSend函数
function handleSend(text: string) {
if (!text.trim()) return
// 添加用户消息到会话
const userMessage: ChatMessage = {
id: Date.now(),
content: text,
sender: 'user',
timestamp: new Date(),
status: 'sent'
}
const memory = sessionManager.handleNewMessage(userMessage)
// 创建AI消息占位符
const aiMessage: ChatMessage = {
id: Date.now() + 1,
content: '',
sender: 'ai',
timestamp: new Date(),
status: 'sending'
}
sessionManager.handleNewMessage(aiMessage)
// 调用streamApi传递memory参数
streamApi(
{
message: text,
options: {
memory // 传递当前会话的memory
}
},
// ... 其他回调保持不变
)
}
// 在模板中添加SessionSelector
<template>
<div class="bg-gray-50 flex flex-col h-screen dark:bg-gray-950">
<!-- 会话选择器 -->
<SessionSelector />
<!-- 原有的消息容器 -->
<!-- ... -->
</div>
</template>
```
### 6. API调用调整
修改 `src/api/agent/index.ts` 中的streamApi调用
- 确保memory参数正确传递
- 如果memory为undefined不传递options或传递空对象
### 7. 持久化考虑
- 使用现有的pinia-plugin-persistedstate机制
- 会话数据会随store自动持久化到localStorage
- 注意大量消息可能导致localStorage超出限制未来可考虑分页或清理旧消息
## 实施步骤(详细)
1. **创建类型定义** (`src/types/session.ts`)
- 定义SessionMemory和ChatSession接口
2. **实现会话store** (`src/stores/modules/chatSession.ts`)
- 基于现有store模式
- 包含完整的CRUD操作
3. **实现组合式组件** (`src/composables/useSessionManager.ts`)
- 提供高层API给UI组件使用
4. **实现UI组件** (`src/components/SessionSelector.vue`)
- 下拉菜单切换器
- 新建会话按钮
5. **集成到现有页面** (`src/pages/llm-chat/index.vue`)
- 引入SessionSelector组件
- 修改消息处理逻辑
- 初始化会话管理器
6. **测试验证**
- 新建会话功能
- 切换历史会话
- memory数据持久化
- 标题自动生成
7. **优化和调整**
- 根据实际使用反馈调整UI
- 考虑性能优化(大量消息处理)
## 注意事项
1. **数据迁移**:现有单会话用户的平滑迁移
2. **性能**:大量消息时的存储和渲染性能
3. **用户体验**:会话切换的流畅性
4. **错误处理**:网络错误、存储失败等情况
5. **浏览器兼容性**localStorage和UUID生成

View File

@ -1,7 +1,20 @@
const AGENT_BASE_URL = '';
// const AGENT_BASE_URL = 'http://115.190.18.84:7328/api/agent';
const AGENT_BASE_URL = 'http://localhost:7328/api/agent';
import { fetchEventSource } from '@microsoft/fetch-event-source';
export interface StreamApiParams {
message: string;
options?: {
memory?: {
thread?: string;
resource?: string;
};
temperature?: number;
maxTokens?: number;
};
}
export const baseInfoApi = () => {
return fetch(`${AGENT_BASE_URL}`, {
method: 'GET',
@ -10,12 +23,22 @@ export const baseInfoApi = () => {
/**
* JSON {"text":"我来"}
* @param message
* @param params message options
* @param callbacks
* @param options abort signal
* @param signalOptions abort signal
* @example
* // 使用示例:
* streamApi('你好', {
* streamApi({
* message: '你好',
* options: {
* temperature: 0.7,
* maxTokens: 1000,
* memory: {
* thread: 'thread_id',
* resource: 'resource_id'
* }
* }
* }, {
* onopen(response) {
* console.log('连接已建立', response);
* },
@ -32,14 +55,14 @@ export const baseInfoApi = () => {
* }, { signal: abortController.signal });
*/
export const streamApi = (
message: string,
params: StreamApiParams,
callbacks: {
onopen?: (response: Response) => void;
onmessage?: (data: string) => void;
onclose?: () => void;
onerror?: (error: Error) => void;
},
options?: { signal?: AbortSignal }
signalOptions?: { signal?: AbortSignal }
) => {
// 启动 fetchEventSource
fetchEventSource(`${AGENT_BASE_URL}/stream`, {
@ -47,8 +70,8 @@ export const streamApi = (
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
signal: options?.signal,
body: JSON.stringify(params),
signal: signalOptions?.signal,
async onopen(response) {
// 检查响应是否正常
if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) {

View File

@ -1,22 +1,22 @@
<script setup lang="ts">
import type { ChatBubbleProps } from '@/types/chat'
const props = defineProps<ChatBubbleProps>()
const { message, showTimestamp, avatar } = defineProps<ChatBubbleProps>()
const messageClasses = computed(() => {
const base = 'rounded-2xl p-3 max-w-80 break-words'
const sender = props.message.sender === 'user'
const sender = message.sender === 'user'
? 'bg-blue-100 dark:bg-blue-800'
: 'bg-gray-100 dark:bg-gray-700'
return `${base} ${sender}`
})
const formattedTime = computed(() => {
if (!props.message.timestamp)
if (!message.timestamp)
return ''
const date = typeof props.message.timestamp === 'string'
? new Date(props.message.timestamp)
: props.message.timestamp
const date = typeof message.timestamp === 'string'
? new Date(message.timestamp)
: message.timestamp
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
})
</script>

View File

@ -0,0 +1,73 @@
<template>
<div class="session-selector">
<van-dropdown-menu>
<van-dropdown-item
v-model="currentSessionId"
:options="sessionOptions"
@change="onSessionChange"
>
<template #title>
<span class="session-title">{{ currentSessionTitle }}</span>
</template>
</van-dropdown-item>
</van-dropdown-menu>
<van-button
size="small"
@click="handleNewSession"
class="new-session-btn"
>
新建会话
</van-button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useSessionManager } from '@/composables/useSessionManager'
const sessionManager = useSessionManager()
const sessionOptions = computed(() =>
sessionManager.sessions.map(session => ({
text: session.title,
value: session.id
}))
)
const currentSessionId = computed({
get: () => sessionManager.currentSessionId,
set: (value) => {
if (value && value !== sessionManager.currentSessionId) {
sessionManager.switchSession(value)
}
}
})
const currentSessionTitle = computed(() =>
sessionManager.currentSession?.title || '选择会话'
)
const onSessionChange = (sessionId: string) => {
sessionManager.switchSession(sessionId)
}
const handleNewSession = () => {
sessionManager.newSession()
}
</script>
<style scoped>
.session-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: white;
border-bottom: 1px solid #f0f0f0;
}
.session-title {
font-weight: 500;
}
</style>

View File

@ -0,0 +1,47 @@
import { useChatSessionStore } from '@/stores/modules/chatSession'
import type { ChatMessage } from '@/types/chat'
export function useSessionManager() {
const sessionStore = useChatSessionStore()
// 初始化:如果没有会话,创建一个默认会话;确保有当前会话
const initialize = () => {
if (sessionStore.sessions.length === 0) {
sessionStore.createSession()
} else if (!sessionStore.currentSessionId) {
// 如果有会话但没有当前会话ID设置第一个会话为当前会话
sessionStore.switchSession(sessionStore.sessions[0].id)
}
}
// 获取当前会话的memory用于API调用
const getCurrentSessionMemory = () => {
return sessionStore.currentSession?.memory
}
// 处理新消息添加到当前会话返回更新后的memory
const handleNewMessage = (message: ChatMessage) => {
sessionStore.addMessageToCurrentSession(message)
return getCurrentSessionMemory()
}
// 新建会话(带可选初始消息)
const newSession = (initialMessage?: string) => {
return sessionStore.createSession(initialMessage)
}
return {
// 状态
sessions: sessionStore.sessions,
currentSession: sessionStore.currentSession,
currentSessionId: sessionStore.currentSessionId,
// 方法
initialize,
getCurrentSessionMemory,
handleNewMessage,
newSession,
switchSession: sessionStore.switchSession,
deleteSession: sessionStore.deleteSession
}
}

View File

@ -1,12 +1,18 @@
<script setup lang="ts">
import type { ChatMessage } from '@/types/chat'
import { useSessionManager } from '@/composables/useSessionManager'
import SessionSelector from '@/components/SessionSelector.vue'
import { useChatSessionStore } from '@/stores/modules/chatSession'
import { useScroll } from '@vueuse/core'
import { streamApi } from '@/api/agent'
// Sample chat data for demonstration
const messages = ref<ChatMessage[]>([
])
const sessionManager = useSessionManager()
sessionManager.initialize()
const sessionStore = useChatSessionStore()
// Messages from current session - get directly from store
const messages = computed(() => sessionStore.currentSession?.messages || [])
const inputText = ref('')
const isLoading = ref(false)
@ -34,7 +40,7 @@ function handleSend(text: string) {
if (!text.trim())
return
//
//
const userMessage: ChatMessage = {
id: Date.now(),
content: text,
@ -42,7 +48,7 @@ function handleSend(text: string) {
timestamp: new Date(),
status: 'sent',
}
messages.value.push(userMessage)
const memory = sessionManager.handleNewMessage(userMessage)
// AI
const aiMessage: ChatMessage = {
@ -52,8 +58,14 @@ function handleSend(text: string) {
timestamp: new Date(),
status: 'sending',
}
messages.value.push(aiMessage)
currentAiMessage.value = aiMessage
sessionManager.handleNewMessage(aiMessage)
// AI
const currentSession = sessionStore.currentSession
const sessionId = sessionStore.currentSessionId
if (currentSession && currentSession.messages.length > 0) {
currentAiMessage.value = currentSession.messages[currentSession.messages.length - 1]
}
//
inputText.value = ''
@ -62,13 +74,16 @@ function handleSend(text: string) {
isLoading.value = true
isStreaming.value = true
// AbortController
// AbortController
const controller = new AbortController()
streamController.value = controller
// API
// API memory
streamApi(
text,
{
message: text,
options: memory ? { memory } : undefined
},
{
onopen(response) {
console.log('SSE 连接已建立:', response)
@ -76,9 +91,11 @@ function handleSend(text: string) {
onmessage(data) {
try {
const parsed = JSON.parse(data) as { text: string }
if (parsed.text) {
//
typewriterEffect(parsed.text)
if (parsed.text && currentAiMessage.value && sessionId) {
// 使 store
sessionStore.updateMessageContent(sessionId, currentAiMessage.value.id, parsed.text)
//
currentAiMessage.value.content += parsed.text
}
} catch (error) {
console.error('解析 SSE 数据失败:', error)
@ -97,40 +114,20 @@ function handleSend(text: string) {
)
}
//
function typewriterEffect(text: string) {
if (!currentAiMessage.value)
return
const duration = 100 //
const fullText = currentAiMessage.value.content + text
function updateChar(index: number) {
if (!currentAiMessage.value)
return
if (index < fullText.length) {
currentAiMessage.value.content = fullText.slice(0, index + 1)
// Vue
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
if (messageIndex !== -1) {
messages.value[messageIndex] = { ...currentAiMessage.value }
}
setTimeout(() => updateChar(index + 1), duration)
}
}
updateChar(currentAiMessage.value.content.length)
}
//
function completeMessage() {
if (currentAiMessage.value) {
currentAiMessage.value.status = 'sent'
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
if (messageIndex !== -1) {
messages.value[messageIndex] = { ...currentAiMessage.value }
if (currentAiMessage.value && sessionStore.currentSession) {
const sessionId = sessionStore.currentSessionId
const messageId = currentAiMessage.value.id
// 'sent'
const session = sessionStore.currentSession
if (session && sessionId) {
const updatedMessages = session.messages.map(msg =>
msg.id === messageId ? { ...msg, status: 'sent' as const } : msg
)
sessionStore.updateSession(sessionId, { messages: updatedMessages })
}
}
@ -139,12 +136,24 @@ function completeMessage() {
//
function handleStreamError(error: Error) {
if (currentAiMessage.value) {
currentAiMessage.value.status = 'error'
currentAiMessage.value.content += ' (连接错误)'
const messageIndex = messages.value.findIndex(m => m.id === currentAiMessage.value?.id)
if (messageIndex !== -1) {
messages.value[messageIndex] = { ...currentAiMessage.value }
if (currentAiMessage.value && sessionStore.currentSession) {
const sessionId = sessionStore.currentSessionId
const messageId = currentAiMessage.value.id
// 'error'
const session = sessionStore.currentSession
if (session && sessionId) {
const updatedMessages = session.messages.map(msg => {
if (msg.id === messageId) {
return {
...msg,
status: 'error' as const,
content: msg.content + ' (连接错误)'
}
}
return msg
})
sessionStore.updateSession(sessionId, { messages: updatedMessages })
}
}
@ -170,14 +179,13 @@ function handleKeydown(e: KeyboardEvent) {
<template>
<div class="bg-gray-50 flex flex-col h-screen dark:bg-gray-950">
<!-- Session selector -->
<SessionSelector />
<!-- Messages container -->
<van-list
<div
ref="messagesContainer"
class="p-4 pb-24 flex-1"
:finished="true"
:loading="false"
finished-text=""
loading-text=""
class="p-4 pb-24 flex-1 overflow-auto"
>
<van-empty
v-if="messages.length === 0"
@ -193,7 +201,7 @@ function handleKeydown(e: KeyboardEvent) {
:show-timestamp="true"
/>
</div>
</van-list>
</div>
<!-- Input area -->
<div class="bottom-0 left-0 right-0 fixed z-10">

View File

@ -4,9 +4,10 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import useUserStore from './modules/user'
import useCounterStore from './modules/counter'
import useRouteCacheStore from './modules/routeCache'
import { useChatSessionStore } from './modules/chatSession'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export { useUserStore, useCounterStore, useRouteCacheStore }
export { useUserStore, useCounterStore, useRouteCacheStore, useChatSessionStore }
export default pinia

View File

@ -0,0 +1,134 @@
import { defineStore } from 'pinia'
import { reactive, computed } from 'vue'
import type { ChatSession, SessionMemory } from '@/types/session'
import type { ChatMessage } from '@/types/chat'
interface ChatSessionState {
sessions: ChatSession[] // 所有会话
currentSessionId: string | null // 当前会话ID
}
// 生成唯一ID和memory的辅助函数
const generateId = () => crypto.randomUUID()
const generateMemory = (sessionId: string): SessionMemory => ({
thread: sessionId,
resource: 'webagent_chat' // 固定资源标识,可根据需要调整
})
const generateTitle = (firstMessage: string): string => {
const maxLength = 20
const trimmed = firstMessage.trim()
if (!trimmed) return '新会话'
return trimmed.length > maxLength
? trimmed.substring(0, maxLength) + '...'
: trimmed
}
export const useChatSessionStore = defineStore('chatSession', () => {
const state = reactive<ChatSessionState>({
sessions: [],
currentSessionId: null
})
// 获取当前会话
const currentSession = computed(() =>
state.sessions.find(s => s.id === state.currentSessionId)
)
// 创建新会话
const createSession = (initialMessage?: string) => {
const sessionId = generateId()
const now = new Date()
const session: ChatSession = {
id: sessionId,
title: initialMessage ? generateTitle(initialMessage) : '新会话',
createdAt: now,
updatedAt: now,
messages: [],
memory: generateMemory(sessionId)
}
state.sessions.push(session)
state.currentSessionId = sessionId
return session
}
// 切换到指定会话
const switchSession = (sessionId: string) => {
const session = state.sessions.find(s => s.id === sessionId)
if (session) {
state.currentSessionId = sessionId
}
}
// 更新会话(添加消息、更新标题等)
const updateSession = (sessionId: string, updates: Partial<ChatSession>) => {
const index = state.sessions.findIndex(s => s.id === sessionId)
if (index !== -1) {
state.sessions[index] = {
...state.sessions[index],
...updates,
updatedAt: new Date()
}
}
}
// 添加消息到当前会话
const addMessageToCurrentSession = (message: ChatMessage) => {
const session = currentSession.value
if (session) {
const messages = [...session.messages, message]
updateSession(session.id, { messages })
// 如果是第一条消息且标题为默认值,自动生成标题
if (messages.length === 1 && session.title === '新会话') {
const title = generateTitle(message.content)
updateSession(session.id, { title })
}
}
}
// 更新消息内容
const updateMessageContent = (sessionId: string, messageId: string | number, content: string) => {
const sessionIndex = state.sessions.findIndex(s => s.id === sessionId)
if (sessionIndex !== -1) {
const session = state.sessions[sessionIndex]
const messageIndex = session.messages.findIndex(m => m.id === messageId)
if (messageIndex !== -1) {
const updatedMessages = [...session.messages]
updatedMessages[messageIndex] = {
...updatedMessages[messageIndex],
content: updatedMessages[messageIndex].content + content
}
state.sessions[sessionIndex] = {
...session,
messages: updatedMessages,
updatedAt: new Date()
}
}
}
}
// 删除会话
const deleteSession = (sessionId: string) => {
state.sessions = state.sessions.filter(s => s.id !== sessionId)
if (state.currentSessionId === sessionId) {
state.currentSessionId = state.sessions[0]?.id || null
}
}
return {
// 状态
sessions: computed(() => state.sessions),
currentSessionId: computed(() => state.currentSessionId),
currentSession,
// 方法
createSession,
switchSession,
updateSession,
addMessageToCurrentSession,
updateMessageContent,
deleteSession
}
}, {
persist: true // 启用持久化
})

View File

@ -247,6 +247,7 @@ declare global {
const useServerHead: typeof import('@unhead/vue').useServerHead
const useServerHeadSafe: typeof import('@unhead/vue').useServerHeadSafe
const useServerSeoMeta: typeof import('@unhead/vue').useServerSeoMeta
const useSessionManager: typeof import('../composables/useSessionManager').useSessionManager
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSlots: typeof import('vue').useSlots

View File

@ -18,11 +18,14 @@ declare module 'vue' {
NavBar: typeof import('./../components/NavBar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SessionSelector: typeof import('./../components/SessionSelector.vue')['default']
TabBar: typeof import('./../components/TabBar.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanDropdownItem: typeof import('vant/es')['DropdownItem']
VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']

18
src/types/session.ts Normal file
View File

@ -0,0 +1,18 @@
import type { ChatMessage } from './chat'
export interface SessionMemory {
thread: string; // 会话线程ID前端生成如UUID或会话ID
resource: string; // 资源标识,前端生成(如固定值或动态标识)
}
export interface ChatSession {
id: string; // 会话唯一标识UUID
title: string; // 会话标题(自动生成)
createdAt: Date; // 创建时间
updatedAt: Date; // 最后更新时间
messages: ChatMessage[]; // 消息列表引用现有ChatMessage类型
memory: SessionMemory; // 会话memory
}
// 扩展现有ChatMessage类型可选添加sessionId字段
// 或者在store中通过关联关系管理