添加会话管理
This commit is contained in:
parent
b9d7f495ad
commit
00aa1e0d3d
|
|
@ -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生成
|
||||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 // 启用持久化
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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中通过关联关系管理
|
||||
Loading…
Reference in New Issue