feat: 更新后端平台模块、Compute引擎、前端组件及构建产物
- 更新 backend 平台 API、platform_store、compute_gateway sync - 更新 compute agent/engine/adapter 及 API - 更新 Docker 部署配置(app/compute) - 新增 frontend/src/utils/ 工具模块 - 新增 scripts/ops_diagnostics.py 运维诊断脚本 - 新增 docs/2026-07-23-development-summary.md 开发总结 - 重构 frontend/dist 构建产物(新 hash) - 更新前端多个视图组件及 API 模块 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
checkNodeReplicaDrift,
|
||||
createComputeNode,
|
||||
disableComputeNode,
|
||||
drainComputeNode,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
getComputeNodes,
|
||||
getComputeQueue,
|
||||
getNodeReplicas,
|
||||
repairNodeReplicas,
|
||||
testComputeNode,
|
||||
updateComputeNode,
|
||||
type ComputeGpu,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
type ComputeQueueItem,
|
||||
type ResourceReplica,
|
||||
} from '@/api/modules/compute'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -25,6 +28,8 @@ const router = useRouter()
|
||||
const activeTab = ref(String(route.query.tab || 'nodes'))
|
||||
const loading = ref(true)
|
||||
const buttonRefreshing = ref(false)
|
||||
const checkingReplicas = ref(false)
|
||||
const repairingReplicas = ref(false)
|
||||
const nodes = ref<ComputeNode[]>([])
|
||||
const gpus = ref<ComputeGpu[]>([])
|
||||
const queue = ref<ComputeQueueItem[]>([])
|
||||
@@ -58,6 +63,10 @@ const enabledNodes = computed(() => nodes.value.filter((item) => item.enabled).l
|
||||
const busyGpus = computed(() => gpus.value.filter((item) => item.status === 'busy' || item.status === 'reserved').length)
|
||||
const totalRunningJobs = computed(() => nodes.value.reduce((sum, item) => sum + item.current_running_jobs, 0))
|
||||
|
||||
function asComputeNode(row: unknown): ComputeNode {
|
||||
return row as ComputeNode
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(tab) => {
|
||||
@@ -68,10 +77,8 @@ watch(
|
||||
async function load(options: { showLoading?: boolean; showButtonLoading?: boolean } = {}) {
|
||||
if (refreshInFlight) return
|
||||
refreshInFlight = true
|
||||
const showLoading = options.showLoading ?? false
|
||||
const showButtonLoading = options.showButtonLoading ?? false
|
||||
if (showLoading) loading.value = true
|
||||
if (showButtonLoading) buttonRefreshing.value = true
|
||||
if (options.showLoading) loading.value = true
|
||||
if (options.showButtonLoading) buttonRefreshing.value = true
|
||||
try {
|
||||
const [nodeList, gpuList, queueList] = await Promise.all([
|
||||
getComputeNodes(),
|
||||
@@ -87,8 +94,8 @@ async function load(options: { showLoading?: boolean; showButtonLoading?: boolea
|
||||
await loadReplicas()
|
||||
lastUpdated.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
} finally {
|
||||
if (showLoading) loading.value = false
|
||||
if (showButtonLoading) buttonRefreshing.value = false
|
||||
if (options.showLoading) loading.value = false
|
||||
if (options.showButtonLoading) buttonRefreshing.value = false
|
||||
refreshInFlight = false
|
||||
}
|
||||
}
|
||||
@@ -105,7 +112,7 @@ async function changeTab(name: string | number) {
|
||||
await router.replace({ path: '/compute', query: { tab: String(name) } })
|
||||
}
|
||||
|
||||
async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test', node: any) {
|
||||
async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test', node: ComputeNode) {
|
||||
const nodeId = String(node.id)
|
||||
if (action === 'enable') await enableComputeNode(nodeId)
|
||||
if (action === 'disable') await disableComputeNode(nodeId)
|
||||
@@ -121,6 +128,40 @@ async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test',
|
||||
await load()
|
||||
}
|
||||
|
||||
async function handleReplicaDriftCheck() {
|
||||
if (!selectedNodeId.value) return
|
||||
checkingReplicas.value = true
|
||||
try {
|
||||
const result = await checkNodeReplicaDrift(selectedNodeId.value)
|
||||
replicas.value = result.items || []
|
||||
ElMessage.success(result.drifted ? `检测完成,发现 ${result.drifted} 个异常副本` : '检测完成,副本状态正常')
|
||||
} finally {
|
||||
checkingReplicas.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReplicaRepair() {
|
||||
if (!selectedNodeId.value) return
|
||||
const targets = replicas.value.filter((item) => ['drifted', 'failed', 'repair_pending'].includes(item.sync_status))
|
||||
if (!targets.length) {
|
||||
ElMessage.info('暂无需要修复的副本')
|
||||
return
|
||||
}
|
||||
repairingReplicas.value = true
|
||||
try {
|
||||
const result = await repairNodeReplicas(selectedNodeId.value, targets.map((item) => item.id))
|
||||
replicas.value = result.replicas || []
|
||||
if (result.failed?.length) {
|
||||
ElMessage.warning(`修复完成,失败 ${result.failed.length} 个`)
|
||||
} else {
|
||||
ElMessage.success('副本修复完成')
|
||||
}
|
||||
await loadReplicas()
|
||||
} finally {
|
||||
repairingReplicas.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNodeForm() {
|
||||
Object.assign(nodeForm, {
|
||||
id: '',
|
||||
@@ -146,7 +187,7 @@ function openCreateNodeDialog() {
|
||||
nodeDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditNodeDialog(node: any) {
|
||||
function openEditNodeDialog(node: ComputeNode) {
|
||||
Object.assign(nodeForm, {
|
||||
id: node.id,
|
||||
code: node.code,
|
||||
@@ -212,23 +253,17 @@ async function saveNode() {
|
||||
}
|
||||
}
|
||||
|
||||
function nodeStatusType(status: string) {
|
||||
if (status === 'online') return 'success'
|
||||
if (status === 'draining' || status === 'maintenance') return 'warning'
|
||||
return 'info'
|
||||
function formatBytes(value?: number) {
|
||||
const size = Number(value || 0)
|
||||
if (!size) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
function taskStatusType(status: string) {
|
||||
if (status === 'running') return 'success'
|
||||
if (status === 'syncing' || status === 'queued') return 'warning'
|
||||
if (status === 'failed') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function gpuStatusType(status: string) {
|
||||
if (status === 'busy') return 'success'
|
||||
if (status === 'reserved') return 'warning'
|
||||
return 'info'
|
||||
function formatTime(value?: string) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -256,22 +291,10 @@ onUnmounted(() => {
|
||||
</header>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-tile">
|
||||
<span>在线节点</span>
|
||||
<strong>{{ enabledNodes }} / {{ nodes.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>GPU 占用</span>
|
||||
<strong>{{ busyGpus }} / {{ gpus.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>运行任务</span>
|
||||
<strong>{{ totalRunningJobs }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>队列任务</span>
|
||||
<strong>{{ queue.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile"><span>启用节点</span><strong>{{ enabledNodes }} / {{ nodes.length }}</strong></div>
|
||||
<div class="summary-tile"><span>GPU 占用</span><strong>{{ busyGpus }} / {{ gpus.length }}</strong></div>
|
||||
<div class="summary-tile"><span>运行任务</span><strong>{{ totalRunningJobs }}</strong></div>
|
||||
<div class="summary-tile"><span>队列任务</span><strong>{{ queue.length }}</strong></div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="compute-tabs" @tab-change="changeTab">
|
||||
@@ -281,7 +304,7 @@ onUnmounted(() => {
|
||||
<el-table-column prop="name" label="节点名称" min-width="150" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="nodeStatusType(row.scheduler_status)">{{ row.scheduler_status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.scheduler_status)">{{ statusLabel(row.scheduler_status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="90">
|
||||
@@ -308,11 +331,11 @@ onUnmounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="330" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEditNodeDialog(row)">编辑</el-button>
|
||||
<el-button size="small" @click="handleNodeAction('test', row)">测试</el-button>
|
||||
<el-button v-if="row.enabled" size="small" @click="handleNodeAction('disable', row)">停用</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="handleNodeAction('enable', row)">启用</el-button>
|
||||
<el-button size="small" type="warning" plain @click="handleNodeAction('drain', row)">维护</el-button>
|
||||
<el-button size="small" @click="openEditNodeDialog(asComputeNode(row))">编辑</el-button>
|
||||
<el-button size="small" @click="handleNodeAction('test', asComputeNode(row))">测试</el-button>
|
||||
<el-button v-if="row.enabled" size="small" @click="handleNodeAction('disable', asComputeNode(row))">停用</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="handleNodeAction('enable', asComputeNode(row))">启用</el-button>
|
||||
<el-button size="small" type="warning" plain @click="handleNodeAction('drain', asComputeNode(row))">维护</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -328,13 +351,11 @@ onUnmounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="gpuStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="利用率" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.gpu_percent" :stroke-width="8" />
|
||||
</template>
|
||||
<template #default="{ row }"><el-progress :percentage="row.gpu_percent" :stroke-width="8" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显存" min-width="190">
|
||||
<template #default="{ row }">
|
||||
@@ -361,19 +382,19 @@ onUnmounted(() => {
|
||||
<el-table-column prop="name" label="任务名称" min-width="200" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="taskStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress" :stroke-width="8" />
|
||||
</template>
|
||||
<template #default="{ row }"><el-progress :percentage="row.progress" :stroke-width="8" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="compute_node_id" label="节点" width="140" />
|
||||
<el-table-column label="GPU" width="120">
|
||||
<template #default="{ row }">{{ row.gpus?.join(', ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="190" />
|
||||
<el-table-column label="创建时间" width="190">
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -383,14 +404,29 @@ onUnmounted(() => {
|
||||
<el-option v-for="node in nodes" :key="node.id" :label="`${node.code} - ${node.name}`" :value="node.id" />
|
||||
</el-select>
|
||||
<span v-if="selectedNode" class="muted">当前节点 {{ selectedNode.api_base_url }}</span>
|
||||
<el-button :loading="checkingReplicas" @click="handleReplicaDriftCheck">漂移检测</el-button>
|
||||
<el-button type="primary" :loading="repairingReplicas" @click="handleReplicaRepair">修复副本</el-button>
|
||||
</div>
|
||||
<el-table :data="replicas" height="100%">
|
||||
<el-table-column prop="resource_type" label="资源类型" width="110" />
|
||||
<el-table-column prop="resource_id" label="资源 ID" min-width="180" />
|
||||
<el-table-column prop="local_path" label="本地路径" min-width="300" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="sync_status" label="同步状态" width="120" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="190" />
|
||||
<el-table-column prop="local_path" label="本地路径" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="同步状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.sync_status)" size="small">{{ statusLabel(row.sync_status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大小" width="110">
|
||||
<template #default="{ row }">{{ formatBytes(row.byte_size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后检测" width="190">
|
||||
<template #default="{ row }">{{ formatTime(row.last_checked_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -432,21 +468,11 @@ onUnmounted(() => {
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="标签">
|
||||
<el-input v-model="nodeForm.tags_text" placeholder="a800, lora, beijing" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据根目录">
|
||||
<el-input v-model="nodeForm.data_root" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型根目录">
|
||||
<el-input v-model="nodeForm.model_root" />
|
||||
</el-form-item>
|
||||
<el-form-item label="训练日志目录">
|
||||
<el-input v-model="nodeForm.log_root" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="nodeForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="标签"><el-input v-model="nodeForm.tags_text" placeholder="a800, lora, beijing" /></el-form-item>
|
||||
<el-form-item label="数据根目录"><el-input v-model="nodeForm.data_root" /></el-form-item>
|
||||
<el-form-item label="模型根目录"><el-input v-model="nodeForm.model_root" /></el-form-item>
|
||||
<el-form-item label="训练日志目录"><el-input v-model="nodeForm.log_root" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="nodeForm.description" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nodeDialogVisible = false">取消</el-button>
|
||||
@@ -486,7 +512,8 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.header-actions,
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -499,7 +526,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
display: inline-block;
|
||||
min-width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -549,9 +575,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-select {
|
||||
|
||||
Reference in New Issue
Block a user