You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

128 lines
3.2 KiB

import { processDataStream } from 'ai'
export interface LlmChatMessage {
id: string
role: 'user' | 'assistant'
content: string
reasoning?: string
}
export interface UseLlmChatOptions {
modelId: () => number | null
apiEndpoint?: string
systemPrompt?: () => string
enableThinking?: () => boolean
}
export function useLlmChat(options: UseLlmChatOptions) {
const {
modelId,
apiEndpoint = '/api/llm/chat',
systemPrompt,
enableThinking,
} = options
const messages = ref<LlmChatMessage[]>([])
const isLoading = ref(false)
const errorMessage = ref('')
let abortController: AbortController | null = null
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2)
}
async function sendMessage(text: string) {
const trimmed = text.trim()
const mid = modelId()
if (!trimmed || mid === null || isLoading.value) return
errorMessage.value = ''
const userMsg: LlmChatMessage = { id: generateId(), role: 'user', content: trimmed }
messages.value.push(userMsg)
const assistantMsg: LlmChatMessage = { id: generateId(), role: 'assistant', content: '' }
messages.value.push(assistantMsg)
const assistantIdx = messages.value.length - 1
isLoading.value = true
abortController = new AbortController()
try {
const res = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
modelId: mid,
messages: [
...(systemPrompt?.() ? [{ role: 'system' as const, content: systemPrompt() }] : []),
...messages.value
.filter(m => m.content)
.map(m => ({ role: m.role, content: m.content })),
],
enableThinking: enableThinking?.() ?? false,
}),
signal: abortController.signal,
})
if (!res.ok) {
const errText = await res.text()
throw new Error(errText || `请求失败 (${res.status})`)
}
if (!res.body) {
throw new Error('响应体为空')
}
await processDataStream({
stream: res.body,
onReasoningPart: (text) => {
const msg = messages.value[assistantIdx]
if (msg) msg.reasoning = (msg.reasoning ?? '') + text
},
onTextPart: (text) => {
const msg = messages.value[assistantIdx]
if (msg) msg.content += text
},
onErrorPart: (error) => {
errorMessage.value = error || '流式响应出错'
},
})
} catch (err: any) {
if (err.name === 'AbortError') {
// user stopped
} else {
errorMessage.value = err.message || '请求失败'
const msg = messages.value[assistantIdx]
if (msg && !msg.content) {
messages.value.splice(assistantIdx, 1)
}
}
} finally {
isLoading.value = false
abortController = null
}
}
function stopGeneration() {
if (abortController) {
abortController.abort()
abortController = null
}
}
function clearChat() {
messages.value = []
errorMessage.value = ''
}
return {
messages,
isLoading,
errorMessage,
sendMessage,
stopGeneration,
clearChat,
}
}