Luxx/dashboard/src/utils/useConversations.js

319 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ref, computed, watch, nextTick } from 'vue'
import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from './api.js'
import { streamManager } from './streamManager.js'
import { useStreamStore } from './streamStore.js'
// 对话管理 Composable
export function useConversations() {
const streamStore = useStreamStore()
// 状态
const list = ref([])
const providers = ref([])
const page = ref(1)
const pageSize = 20
const total = ref(0)
const loading = ref(true)
const error = ref('')
const selectedId = ref(null)
const selectedConv = ref(null)
const convMessages = ref([])
const loadingMessages = ref(false)
const enabledTools = ref([])
// 计算属性
const totalPages = computed(() => Math.ceil(total.value / pageSize))
const currentStreamState = computed(() => {
return streamStore.getStreamState(selectedId.value)
})
const sending = computed(() => {
const state = streamStore.getStreamState(selectedId.value)
return state && state.status === 'streaming'
})
// 检查指定会话是否有活跃流
const hasActiveStream = (convId) => {
return streamStore.hasActiveStream(convId)
}
// 加载启用的工具列表
const loadEnabledTools = async () => {
try {
const res = await toolsAPI.list()
if (res.success) {
const tools = res.data?.tools || []
enabledTools.value = tools.map(t => t.function?.name || t.name)
}
} catch (e) {
console.error('Failed to load tools:', e)
}
}
// 加载会话列表
const fetchData = async () => {
loading.value = true
error.value = ''
try {
const [convRes, provRes, toolsRes] = await Promise.allSettled([
conversationsAPI.list({ page: page.value, page_size: pageSize }),
providersAPI.list(),
toolsAPI.list()
])
if (convRes.status === 'fulfilled' && convRes.value.success) {
list.value = convRes.value.data?.items || []
total.value = convRes.value.data?.total || 0
// 默认选中第一个会话
if (list.value.length > 0 && !selectedId.value) {
selectConv(list.value[0])
}
}
if (provRes.status === 'fulfilled' && provRes.value.success) {
providers.value = provRes.value.data?.providers || []
}
if (toolsRes.status === 'fulfilled' && toolsRes.value.success) {
enabledTools.value = (toolsRes.value.data?.tools || []).map(t => t.function?.name || t.name)
}
} catch (e) {
error.value = e.message
}
finally {
loading.value = false
}
}
// 选择会话
const selectConv = async (c) => {
selectedId.value = c.id
selectedConv.value = c
await fetchConvMessages(c.id)
setupStreamWatch()
}
// 获取会话消息
const fetchConvMessages = async (convId) => {
loadingMessages.value = true
convMessages.value = []
try {
const res = await messagesAPI.list(convId)
if (res.success) {
convMessages.value = res.data?.messages || []
// 加载完成后强制滚动到底部(初始加载总是显示最新消息)
nextTick(() => {
if (typeof onInitialScroll === 'function') {
onInitialScroll()
}
})
}
} catch (e) {
console.error('获取消息失败:', e)
} finally {
loadingMessages.value = false
}
}
// 发送消息
const sendMessage = async (content) => {
if (!content.trim() || !selectedConv.value || sending.value) return
const trimmedContent = content.trim()
// 添加用户消息到列表
const userMsgId = 'user-' + Date.now()
const userMsg = {
id: userMsgId,
role: 'user',
content: trimmedContent,
created_at: new Date().toISOString()
}
convMessages.value.push(userMsg)
// 如果还没有标题或标题为默认标题,使用第一条消息作为标题
const currentTitle = selectedConv.value?.title
const isDefaultTitle = !currentTitle || currentTitle === 'New Conversation' || currentTitle.trim() === ''
if (isDefaultTitle) {
const title = trimmedContent.slice(0, 30) + (trimmedContent.length > 30 ? '...' : '')
selectedConv.value.title = title
// 更新列表中的标题
const conv = list.value.find(c => c.id === selectedConv.value.id)
if (conv) conv.title = title
// 调用 API 保存
await conversationsAPI.update(selectedConv.value.id, { title })
}
// 使用 StreamManager 发送流式请求
await streamManager.startStream(
selectedConv.value.id,
{
conversation_id: selectedConv.value.id,
content: trimmedContent,
enabled_tools: enabledTools.value
},
userMsgId
)
}
// 创建会话
const createConv = async (form) => {
const res = await conversationsAPI.create(form)
if (res.success && res.data?.id) {
await fetchData()
const newConv = list.value.find(c => c.id === res.data.id)
if (newConv) {
await selectConv(newConv)
}
return res.data
}
throw new Error(res.message)
}
// 删除会话
const deleteConv = async (c) => {
if (hasActiveStream(c.id)) {
streamManager.cancelStream(c.id)
}
await conversationsAPI.delete(c.id)
if (selectedId.value === c.id) {
selectedId.value = null
selectedConv.value = null
}
await fetchData()
}
// 更新会话标题
const updateConvTitle = async (c, newTitle) => {
c.title = newTitle
// 更新列表中的标题
const conv = list.value.find(item => item.id === c.id)
if (conv) conv.title = newTitle
// 调用 API 保存
await conversationsAPI.update(c.id, { title: newTitle })
}
// 删除消息
const deleteMessage = async (msgId) => {
if (!selectedConv.value) return
try {
await messagesAPI.delete(msgId)
// 从本地列表中移除
convMessages.value = convMessages.value.filter(m => m.id !== msgId)
} catch (e) {
console.error('删除消息失败:', e)
throw e
}
}
// 重新生成消息(删除 assistant 消息并重新发送用户消息)
const regenerateMessage = async (msgId) => {
if (!selectedConv.value || sending.value) return
// 找到要重新生成的消息
const msgIndex = convMessages.value.findIndex(m => m.id === msgId)
if (msgIndex === -1) return
// 找到对应的用户消息assistant 消息的前一条)
const userMsgIndex = msgIndex - 1
if (userMsgIndex < 0 || convMessages.value[userMsgIndex].role !== 'user') return
const userMsg = convMessages.value[userMsgIndex]
// 删除 assistant 消息
convMessages.value = convMessages.value.filter(m => m.id !== msgId)
// 调用 API 删除 assistant 消息
try {
await messagesAPI.delete(selectedConv.value.id, msgId)
} catch (e) {
console.error('删除消息失败:', e)
}
// 重新发送用户消息
await sendMessage(userMsg.content)
}
// 设置流状态监听
let unwatchStream = null
const setupStreamWatch = () => {
if (unwatchStream) {
unwatchStream()
}
unwatchStream = watch(
() => streamStore.getStreamState(selectedId.value),
(state) => {
if (!state) return
if (state.status === 'done') {
const completedMessage = {
id: state.id,
role: 'assistant',
process_steps: state.process_steps,
token_count: state.token_count,
usage: state.usage,
created_at: new Date().toISOString()
}
convMessages.value.push(completedMessage)
streamStore.clearStream(selectedId.value)
} else if (state.status === 'error') {
console.error('Stream error:', state.error)
streamStore.clearStream(selectedId.value)
}
},
{ deep: true }
)
}
// 初始化
const init = async () => {
await fetchData()
}
// 清理
const cleanup = () => {
if (unwatchStream) {
unwatchStream()
}
}
// 初始滚动回调(由外部设置)
let onInitialScroll = null
const setOnInitialScroll = (callback) => {
onInitialScroll = callback
}
return {
// 状态
list,
providers,
page,
totalPages,
loading,
error,
selectedId,
selectedConv,
convMessages,
loadingMessages,
sending,
currentStreamState,
// 方法
hasActiveStream,
fetchData,
selectConv,
fetchConvMessages,
sendMessage,
createConv,
deleteConv,
updateConvTitle,
deleteMessage,
regenerateMessage,
loadEnabledTools,
setOnInitialScroll,
init,
cleanup
}
}