305 lines
8.2 KiB
JavaScript
305 lines
8.2 KiB
JavaScript
import { ref, computed, watch } 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 || []
|
||
}
|
||
} 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()
|
||
}
|
||
}
|
||
|
||
return {
|
||
// 状态
|
||
list,
|
||
providers,
|
||
page,
|
||
totalPages,
|
||
loading,
|
||
error,
|
||
selectedId,
|
||
selectedConv,
|
||
convMessages,
|
||
loadingMessages,
|
||
sending,
|
||
currentStreamState,
|
||
|
||
// 方法
|
||
hasActiveStream,
|
||
fetchData,
|
||
selectConv,
|
||
fetchConvMessages,
|
||
sendMessage,
|
||
createConv,
|
||
deleteConv,
|
||
updateConvTitle,
|
||
deleteMessage,
|
||
regenerateMessage,
|
||
loadEnabledTools,
|
||
init,
|
||
cleanup
|
||
}
|
||
}
|