2026-07-10 16:45:55 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import DataTablePage from '@/components/DataTablePage.vue'
|
2026-07-16 11:03:25 +08:00
|
|
|
|
import { usePolling } from '@/composables/usePolling'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getCompareList,
|
|
|
|
|
|
deleteCompare,
|
|
|
|
|
|
getCompare,
|
|
|
|
|
|
loadCompare,
|
|
|
|
|
|
unloadCompare,
|
|
|
|
|
|
stopModelByPid,
|
|
|
|
|
|
} from '@/api/modules/compare'
|
|
|
|
|
|
import type { CompareTask, LoadedModel } from '@/types'
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const dataList = ref<CompareTask[]>([])
|
2026-07-16 11:03:25 +08:00
|
|
|
|
let delayedRefreshTimer: ReturnType<typeof setTimeout> | null = null
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
async function loadData(silent = false) {
|
|
|
|
|
|
if (!silent) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
dataList.value = (await getCompareList()) || []
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (!silent) {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 解析 load_status 判断就绪状态 */
|
|
|
|
|
|
function isStarting(row: any): boolean {
|
|
|
|
|
|
const models = parseLoadedModels(row)
|
|
|
|
|
|
return models.length > 0 && models.some((m) => m.status === 'starting')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isReady(row: any): boolean {
|
|
|
|
|
|
const models = parseLoadedModels(row)
|
|
|
|
|
|
if (models.length === 0) return row.status === 'loaded' || row.status === 'ready'
|
|
|
|
|
|
return models.every((m) => m.status === 'ready' || m.status === 'running') && !isStarting(row)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseLoadedModels(row: any): LoadedModel[] {
|
|
|
|
|
|
if (!row.load_status) return []
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed =
|
|
|
|
|
|
typeof row.load_status === 'string' ? JSON.parse(row.load_status) : row.load_status
|
|
|
|
|
|
return parsed.loaded_models || []
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 解析相关模型字段,返回模型名称列表 */
|
|
|
|
|
|
function parseModelNames(row: any): string[] {
|
|
|
|
|
|
if (!row.models) return []
|
|
|
|
|
|
// 数组结构直接取
|
|
|
|
|
|
if (Array.isArray(row.models)) {
|
|
|
|
|
|
return row.models.map((m: any) => m.model_name || m.name).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 字符串结构需先 JSON.parse
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(row.models)
|
|
|
|
|
|
if (Array.isArray(parsed)) {
|
|
|
|
|
|
return parsed.map((m: any) => m.model_name || m.name).filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 非法 JSON,忽略
|
|
|
|
|
|
}
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 加载推理任务 */
|
|
|
|
|
|
async function handleLoad(row: any) {
|
|
|
|
|
|
await loadCompare(row.id)
|
|
|
|
|
|
ElMessage.info('正在加载模型,请稍候...')
|
2026-07-16 11:03:25 +08:00
|
|
|
|
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
|
|
|
|
|
|
delayedRefreshTimer = setTimeout(loadData, 1000)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 卸载推理任务 */
|
|
|
|
|
|
async function handleUnload(row: any) {
|
|
|
|
|
|
await ElMessageBox.confirm('确定要停止模型服务吗?', '确认停止', { type: 'warning' })
|
|
|
|
|
|
await unloadCompare(row.id)
|
|
|
|
|
|
ElMessage.success('已停止模型服务')
|
|
|
|
|
|
loadData()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 删除(先停止进程) */
|
|
|
|
|
|
async function handleDelete(row: any) {
|
|
|
|
|
|
// 先尝试停止已加载的模型进程
|
|
|
|
|
|
const task = await getCompare(row.id).catch(() => null)
|
|
|
|
|
|
if (task?.load_status) {
|
|
|
|
|
|
const models = parseLoadedModels(task as CompareTask)
|
|
|
|
|
|
for (const m of models) {
|
|
|
|
|
|
if (m.pid) {
|
|
|
|
|
|
await stopModelByPid(m.pid).catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await deleteCompare(row.id)
|
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 开始对话 */
|
|
|
|
|
|
function startChat(row: any) {
|
|
|
|
|
|
router.push(`/model-inference/chat/${row.id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-16 11:03:25 +08:00
|
|
|
|
const { start: startPolling } = usePolling(() => loadData(true), 3000, { immediate: false })
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadData()
|
|
|
|
|
|
startPolling()
|
2026-07-10 16:45:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-07-16 11:03:25 +08:00
|
|
|
|
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
|
2026-07-10 16:45:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<DataTablePage
|
|
|
|
|
|
title="模型推理"
|
|
|
|
|
|
:data="dataList"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
searchable
|
|
|
|
|
|
:search-fields="['name', 'model_name', 'description']"
|
|
|
|
|
|
create-text="新建推理"
|
|
|
|
|
|
create-to="/model-inference/create"
|
|
|
|
|
|
:delete-fn="handleDelete"
|
|
|
|
|
|
row-key="id"
|
|
|
|
|
|
@refresh="loadData"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #columns>
|
|
|
|
|
|
<el-table-column label="推理名称" align="center">
|
|
|
|
|
|
<template #default="{ row }">{{ row.model_name || row.name || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="描述" align="center" show-overflow-tooltip>
|
|
|
|
|
|
<template #default="{ row }">{{ row.description || '-' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" align="center" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag v-if="isStarting(row)" type="warning" size="small">启动中</el-tag>
|
|
|
|
|
|
<el-tag v-else-if="isReady(row)" type="success" size="small">已就绪</el-tag>
|
|
|
|
|
|
<el-tag v-else type="info" size="small">{{ row.status || '未启动' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="相关模型" align="center" show-overflow-tooltip>
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ parseModelNames(row).join(',') || '-' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="创建时间" align="center" width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ row.create_time ? new Date(row.create_time).toLocaleString('zh-CN') : '-' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<template #actions="{ row }">
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
|
<template v-if="isStarting(row)">
|
|
|
|
|
|
<el-button type="warning" link size="small" :loading="true" disabled>
|
|
|
|
|
|
加载中
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="isReady(row)">
|
|
|
|
|
|
<el-button type="success" link size="small" @click="startChat(row)">
|
|
|
|
|
|
<i class="fa fa-comments-o" style="margin-right: 4px" />对话
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="warning" link size="small" @click="handleUnload(row)">
|
|
|
|
|
|
<i class="fa fa-stop-circle-o" style="margin-right: 4px" />停止
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<el-button type="primary" link size="small" @click="handleLoad(row)">
|
|
|
|
|
|
<i class="fa fa-play-circle-o" style="margin-right: 4px" />加载
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
|
|
|
|
|
<i class="fa fa-trash-o" style="margin-right: 4px" />删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</DataTablePage>
|
|
|
|
|
|
</template>
|