Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 27 additions & 70 deletions apps/web/src/components/ai/chat-box/AIAssistantPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Textarea } from '@/components/ui/textarea'
import { buildAIHeaders, resolveEndpointUrl, useAIFetch } from '@/composables/useAIFetch'
import useAIConfigStore from '@/stores/aiConfig'
import { useEditorStore } from '@/stores/editor'
import { useQuickCommands } from '@/stores/quickCommands'
Expand Down Expand Up @@ -57,8 +58,7 @@ const inputHistory = ref<string[]>([])
const historyIndex = ref<number | null>(null)

const configVisible = ref(false)
const loading = ref(false)
const fetchController = ref<AbortController | null>(null)
const { loading, abort: abortFetch, fetchSSE } = useAIFetch()
const copiedIndex = ref<number | null>(null)
const insertedIndex = ref<number | null>(null)
const memoryKey = `ai_memory_context`
Expand Down Expand Up @@ -273,10 +273,7 @@ function insertToDocument(text: string, index: number) {
}

async function resetMessages() {
if (fetchController.value) {
fetchController.value.abort()
fetchController.value = null
}
abortFetch()

if (currentConversationId.value) {
conversationList.value = conversationList.value.filter(c => c.id !== currentConversationId.value)
Expand All @@ -292,11 +289,7 @@ async function resetMessages() {
}

function pauseStreaming() {
if (fetchController.value) {
fetchController.value.abort()
fetchController.value = null
}
loading.value = false
abortFetch()
const last = messages.value[messages.value.length - 1]
if (last?.role === `assistant`)
last.done = true
Expand Down Expand Up @@ -386,78 +379,42 @@ async function streamResponse(replyMessageProxy: ChatMessage) {
max_tokens: maxToken.value,
stream: true,
}
const headers: Record<string, string> = { 'Content-Type': `application/json` }
if (apiKey.value && type.value !== `default`)
headers.Authorization = `Bearer ${apiKey.value}`

fetchController.value = new AbortController()
const signal = fetchController.value.signal
const headers = buildAIHeaders(apiKey.value, type.value)
const url = resolveEndpointUrl(endpoint.value, `chat`)

try {
const url = new URL(endpoint.value)
if (!url.pathname.endsWith(`/chat/completions`))
url.pathname = url.pathname.replace(/\/?$/, `/chat/completions`)

const res = await window.fetch(url.toString(), {
method: `POST`,
headers,
body: JSON.stringify(payload),
signal,
})
if (!res.ok || !res.body)
throw new Error(`响应错误:${res.status} ${res.statusText}`)

const reader = res.body.getReader()
const decoder = new TextDecoder(`utf-8`)
let buffer = ``

while (true) {
const { value, done } = await reader.read()
if (done) {
await fetchSSE(url, headers, payload, {
onDelta(content) {
const last = messages.value[messages.value.length - 1]
if (last !== replyMessageProxy)
return
last.content += content
scrollToBottom()
},
onReasoningDelta(reasoning) {
const last = messages.value[messages.value.length - 1]
if (last !== replyMessageProxy)
return
last.reasoning = (last.reasoning || ``) + reasoning
scrollToBottom()
},
onDone() {
const last = messages.value[messages.value.length - 1]
if (last.role === `assistant`) {
last.done = true
await scrollToBottom(true)
}
break
}

buffer += decoder.decode(value, { stream: true })
const lines = buffer.split(`\n`)
buffer = lines.pop() || ``

for (const line of lines) {
if (!line.trim() || line.trim() === `data: [DONE]`)
continue
try {
const json = JSON.parse(line.replace(/^data: /, ``))
const delta = json.choices?.[0]?.delta || {}
const last = messages.value[messages.value.length - 1]
if (last !== replyMessageProxy)
return
if (delta.content)
last.content += delta.content
else if (delta.reasoning_content)
last.reasoning = (last.reasoning || ``) + delta.reasoning_content
await scrollToBottom()
}
catch {
scrollToBottom(true)
}
}
}
},
})
}
catch (e) {
if ((e as Error).name !== `AbortError`) {
messages.value[messages.value.length - 1].content
= `❌ 请求失败: ${(e as Error).message}`
}
messages.value[messages.value.length - 1].content
= `❌ 请求失败: ${(e as Error).message}`
await scrollToBottom(true)
}
finally {
await store.setJSON(memoryKey, messages.value)
await autoSaveCurrentConversation()
loading.value = false
fetchController.value = null
}
}

Expand Down
22 changes: 7 additions & 15 deletions apps/web/src/components/ai/chat-box/AIConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { serviceOptions } from '@md/shared/configs'
import { DEFAULT_SERVICE_TYPE } from '@md/shared/constants'
import { Info } from 'lucide-vue-next'
import { PasswordInput } from '@/components/ui/password-input'
import { buildAIHeaders, resolveEndpointUrl, useAIFetch } from '@/composables/useAIFetch'
import useAIConfigStore from '@/stores/aiConfig'

/* -------------------------- 基础数据 -------------------------- */
Expand All @@ -13,7 +14,7 @@ const AIConfigStore = useAIConfigStore()
const { type, endpoint, model, apiKey, temperature, maxToken } = storeToRefs(AIConfigStore)

/** UI 状态 */
const loading = ref(false)
const { loading, fetchJSON } = useAIFetch()
const testResult = ref(``)

/** 当前服务信息 */
Expand Down Expand Up @@ -51,14 +52,10 @@ async function testConnection() {
testResult.value = ``
loading.value = true

const headers: Record<string, string> = { 'Content-Type': `application/json` }
if (apiKey.value && type.value !== DEFAULT_SERVICE_TYPE)
headers.Authorization = `Bearer ${apiKey.value}`
const headers = buildAIHeaders(apiKey.value, type.value)

try {
const url = new URL(endpoint.value)
if (!url.pathname.endsWith(`/chat/completions`))
url.pathname = url.pathname.replace(/\/?$/, `/chat/completions`)
const url = resolveEndpointUrl(endpoint.value, `chat`)

const payload = {
model: model.value,
Expand All @@ -68,20 +65,15 @@ async function testConnection() {
stream: false,
}

const res = await window.fetch(url.toString(), {
method: `POST`,
headers,
body: JSON.stringify(payload),
})
const res = await fetchJSON(url, headers, payload)

if (res.ok) {
testResult.value = `✅ 测试成功,/chat/completions 可用`
saveConfig(false)
}
else {
const text = await res.text()
try {
const { error } = JSON.parse(text)
const { error } = JSON.parse(res.errorText)
if (
res.status === 404
&& (error?.code === `ModelNotOpen`
Expand All @@ -93,7 +85,7 @@ async function testConnection() {
}
}
catch {}
testResult.value = `❌ 测试失败:${res.status} ${res.statusText},${text}`
testResult.value = `❌ 测试失败:${res.status} ${res.statusText},${res.errorText}`
}
}
catch (err) {
Expand Down
21 changes: 6 additions & 15 deletions apps/web/src/components/ai/image-generator/AIImageConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { buildAIHeaders, resolveEndpointUrl, useAIFetch } from '@/composables/useAIFetch'
import useAIImageConfigStore from '@/stores/aiImageConfig'

/* -------------------------- 基础数据 -------------------------- */
Expand All @@ -22,7 +23,7 @@ const AIImageConfigStore = useAIImageConfigStore()
const { type, endpoint, model, apiKey, size, quality, style } = storeToRefs(AIImageConfigStore)

/** UI 状态 */
const loading = ref(false)
const { loading, fetchJSON } = useAIFetch()
const testResult = ref(``)

/** 当前服务信息 */
Expand Down Expand Up @@ -82,15 +83,10 @@ async function testConnection() {
testResult.value = ``
loading.value = true

const headers: Record<string, string> = { 'Content-Type': `application/json` }
if (apiKey.value && type.value !== DEFAULT_SERVICE_TYPE)
headers.Authorization = `Bearer ${apiKey.value}`
const headers = buildAIHeaders(apiKey.value, type.value)

try {
const url = new URL(endpoint.value)
if (!url.pathname.includes(`/images/`) && !url.pathname.endsWith(`/images/generations`)) {
url.pathname = url.pathname.replace(/\/?$/, `/images/generations`)
}
const url = resolveEndpointUrl(endpoint.value, `image`)

const payload = {
model: model.value,
Expand All @@ -101,18 +97,13 @@ async function testConnection() {
n: 1,
}

const res = await window.fetch(url.toString(), {
method: `POST`,
headers,
body: JSON.stringify(payload),
})
const res = await fetchJSON(url, headers, payload)

if (res.ok) {
testResult.value = `✅ 连接成功`
}
else {
const errorText = await res.text()
testResult.value = `❌ 连接失败:${res.status} ${errorText}`
testResult.value = `❌ 连接失败:${res.status} ${res.errorText}`
}
}
catch (error) {
Expand Down
Loading
Loading