feat: sync upstream models with real-time streaming updates

This commit is contained in:
SmartUp Developer
2026-07-02 18:11:55 +08:00
parent 852bf84246
commit ece11d66e6
3 changed files with 421 additions and 263 deletions
+119 -10
View File
@@ -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
}