feat: sync upstream models with real-time streaming updates
This commit is contained in:
+119
-10
@@ -947,8 +947,25 @@
|
||||
title="一键同步上游模型结果"
|
||||
width="850px"
|
||||
destroy-on-close
|
||||
:show-close="!syncModelsExecuting"
|
||||
:close-on-click-modal="!syncModelsExecuting"
|
||||
:close-on-press-escape="!syncModelsExecuting"
|
||||
:before-close="handleSyncModelsDialogBeforeClose"
|
||||
>
|
||||
<div v-loading="syncModelsExecuting">
|
||||
<div>
|
||||
<div style="margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; font-size: 14px;">
|
||||
<div>
|
||||
<span style="margin-right: 15px;">已处理: <strong style="color: var(--el-color-primary);">{{ syncModelsProcessedCount }}/{{ syncModelsTotal }}</strong></span>
|
||||
<span style="margin-right: 15px;">成功: <strong style="color: var(--el-color-success);">{{ syncModelsSuccessCount }}</strong></span>
|
||||
<span style="margin-right: 15px;">失败: <strong style="color: var(--el-color-danger);">{{ syncModelsFailedCount }}</strong></span>
|
||||
<span>跳过: <strong style="color: var(--el-color-warning);">{{ syncModelsSkippedCount }}</strong></span>
|
||||
</div>
|
||||
<div v-if="syncModelsExecuting" style="color: var(--el-color-info); display: flex; align-items: center;">
|
||||
<span class="is-loading" style="margin-right: 5px; display: inline-flex;"><el-icon><Refresh /></el-icon></span>
|
||||
正在同步模型...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="syncModelsMessage" style="margin-bottom: 15px; font-weight: bold; color: var(--el-color-primary);">
|
||||
{{ syncModelsMessage }}
|
||||
</div>
|
||||
@@ -1005,6 +1022,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowDown, Delete, Edit, Plus, Minus, Grid, Connection, Link, Upload, Key, Refresh, Sort, WarningFilled, Search, Close } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
upstreamsApi,
|
||||
websitesApi,
|
||||
@@ -1189,6 +1207,18 @@ const syncModelsDialog = ref(false)
|
||||
const syncModelsExecuting = ref(false)
|
||||
const syncModelsMessage = ref('')
|
||||
const syncModelsResults = ref<SyncUpstreamModelsItem[]>([])
|
||||
const syncModelsTotal = ref(0)
|
||||
const syncModelsSuccessCount = computed(() => syncModelsResults.value.filter(r => r.status === 'success').length)
|
||||
const syncModelsFailedCount = computed(() => syncModelsResults.value.filter(r => r.status === 'failed').length)
|
||||
const syncModelsSkippedCount = computed(() => syncModelsResults.value.filter(r => r.status === 'skipped').length)
|
||||
const syncModelsProcessedCount = computed(() => syncModelsResults.value.length)
|
||||
|
||||
function handleSyncModelsDialogBeforeClose(done: () => void) {
|
||||
if (syncModelsExecuting.value) {
|
||||
return
|
||||
}
|
||||
done()
|
||||
}
|
||||
|
||||
const upstreamGroupOptions = computed(() => {
|
||||
const rows: Array<{ key: string; label: string; rate: string | number; source: BindingSourceGroup }> = []
|
||||
@@ -1951,21 +1981,100 @@ async function triggerSyncUpstreamModels() {
|
||||
|
||||
syncModelsResults.value = []
|
||||
syncModelsMessage.value = ''
|
||||
syncModelsTotal.value = 0
|
||||
syncModelsExecuting.value = true
|
||||
syncModelsDialog.value = true
|
||||
|
||||
let hasProcessedStart = false
|
||||
|
||||
try {
|
||||
const res = await websitesApi.syncUpstreamModels(selectedWebsite.value.id)
|
||||
syncModelsMessage.value = res.data.message
|
||||
syncModelsResults.value = res.data.items
|
||||
if (res.data.success) {
|
||||
ElMessage.success('同步完成')
|
||||
} else {
|
||||
ElMessage.warning(res.data.message || '部分账号同步模型失败')
|
||||
const authStore = useAuthStore()
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (authStore.token) {
|
||||
headers['Authorization'] = `Bearer ${authStore.token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/websites/${selectedWebsite.value.id}/accounts/sync-upstream-models/stream`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
let errMsg = `请求失败 (${response.status})`
|
||||
try {
|
||||
const errJson = await response.json()
|
||||
if (errJson?.detail) errMsg = errJson.detail
|
||||
} catch {}
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error('无法读取响应流')
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue
|
||||
const eventObj = JSON.parse(line)
|
||||
const { event, data } = eventObj
|
||||
|
||||
if (event === 'start') {
|
||||
syncModelsTotal.value = data.total_accounts || 0
|
||||
hasProcessedStart = true
|
||||
} else if (event === 'item') {
|
||||
syncModelsResults.value.push(data)
|
||||
} else if (event === 'complete') {
|
||||
syncModelsMessage.value = data.message
|
||||
if (data.success) {
|
||||
ElMessage.success('同步完成')
|
||||
} else {
|
||||
ElMessage.warning(data.message || '部分账号同步模型失败')
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
throw new Error(data.message || '流式同步出错')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
const eventObj = JSON.parse(buffer)
|
||||
const { event, data } = eventObj
|
||||
if (event === 'start') {
|
||||
syncModelsTotal.value = data.total_accounts || 0
|
||||
hasProcessedStart = true
|
||||
} else if (event === 'item') {
|
||||
syncModelsResults.value.push(data)
|
||||
} else if (event === 'complete') {
|
||||
syncModelsMessage.value = data.message
|
||||
if (data.success) {
|
||||
ElMessage.success('同步完成')
|
||||
} else {
|
||||
ElMessage.warning(data.message || '部分账号同步模型失败')
|
||||
}
|
||||
} else if (event === 'error') {
|
||||
throw new Error(data.message || '流式同步出错')
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '同步上游模型失败')
|
||||
syncModelsDialog.value = false
|
||||
ElMessage.error(e.message || '同步上游模型失败')
|
||||
if (!hasProcessedStart || syncModelsResults.value.length === 0) {
|
||||
syncModelsDialog.value = false
|
||||
}
|
||||
} finally {
|
||||
syncModelsExecuting.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user