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:
wuyongtao
2026-07-23 19:32:42 +08:00
parent f04dc479bb
commit b28cfbc6fa
193 changed files with 2647 additions and 424 deletions

View File

@@ -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 {