feat: 更新后端平台模块、前端组件及构建产物,新增工作计划文档
- 更新 backend 平台 API endpoints 及 platform_store - 更新前端 ComputeNodesView、DataProcessCreateView、FineTuneCreateView 等组件 - 更新前端 API 模块(compute、fineTune) - 重构 frontend/dist 构建产物(新 hash) - 新增 docs/2026-07-24-work-plan.md 工作计划文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,16 @@ export interface ResourceReplica {
|
||||
create_time: string
|
||||
}
|
||||
|
||||
export interface ResourceSyncJob {
|
||||
id: string
|
||||
target_node_id: string
|
||||
resources?: unknown
|
||||
status: string
|
||||
progress: number
|
||||
create_time?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
export const getComputeNodes = () => get<ComputeNode[]>('/compute/nodes')
|
||||
|
||||
export const createComputeNode = (data: ComputeNodePayload) =>
|
||||
@@ -104,7 +114,9 @@ export const checkNodeReplicaDrift = (id: string) =>
|
||||
get<{ node_id: string; items: ResourceReplica[]; drifted: number }>(`/compute/nodes/${id}/replicas/drift`)
|
||||
|
||||
export const repairNodeReplicas = (id: string, replicaIds?: string[]) =>
|
||||
post<{ sync: unknown; replicas: ResourceReplica[]; failed: Array<{ replica_id: string; error: string }> }>(
|
||||
post<{ sync: ResourceSyncJob; replicas: ResourceReplica[]; failed: Array<{ replica_id: string; error: string }>; async?: boolean }>(
|
||||
`/compute/nodes/${id}/replicas/repair`,
|
||||
replicaIds?.length ? { replica_ids: replicaIds } : {},
|
||||
)
|
||||
|
||||
export const getComputeSyncJob = (id: string) => get<ResourceSyncJob>(`/compute/sync-jobs/${id}`)
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import { get, post, put, del } from '../request'
|
||||
import type { FineTuneStartPayload, FineTuneTask, TrainingProgress, LogContent } from '@/types'
|
||||
|
||||
export interface TrainingDiagnostic {
|
||||
level: string
|
||||
title: string
|
||||
suggestion: string
|
||||
}
|
||||
|
||||
export interface FineTunePreflightResult {
|
||||
valid: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
diagnostics?: TrainingDiagnostic[]
|
||||
node?: {
|
||||
id?: string
|
||||
code?: string
|
||||
name?: string
|
||||
api_base_url?: string
|
||||
scheduler_status?: string
|
||||
gpu_count?: number
|
||||
}
|
||||
job_payload?: Record<string, unknown>
|
||||
preview?: {
|
||||
command?: string[]
|
||||
command_text?: string
|
||||
work_dir?: string
|
||||
path_checks?: Array<Record<string, unknown>>
|
||||
accelerator?: Record<string, unknown>
|
||||
runtime_files?: Array<Record<string, unknown>>
|
||||
}
|
||||
sync_results?: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
/** 训练任务列表 */
|
||||
export const getFineTuneList = () => get<FineTuneTask[]>('/fine-tune')
|
||||
|
||||
@@ -18,6 +49,18 @@ export const createFineTune = (data: Partial<FineTuneTask>) =>
|
||||
/** 启动训练(第二步) */
|
||||
export const startFineTune = (data: FineTuneStartPayload) => post('/fine-tune/start', data)
|
||||
|
||||
/** 创建前训练预检,不落库 */
|
||||
export const preflightFineTune = (data: Omit<FineTuneStartPayload, 'task_id'>) =>
|
||||
post<FineTunePreflightResult>('/fine-tune/preflight', data)
|
||||
|
||||
/** 创建前真实命令预览,不落库 */
|
||||
export const previewFineTuneCommand = (data: Omit<FineTuneStartPayload, 'task_id'>) =>
|
||||
post<FineTunePreflightResult>('/fine-tune/command-preview', data)
|
||||
|
||||
/** 训练失败诊断 */
|
||||
export const getFineTuneDiagnostics = (id: string | number) =>
|
||||
get<{ task_id: string | number; status?: string; failure_reason?: string; diagnostics: TrainingDiagnostic[] }>(`/fine-tune/${id}/diagnostics`)
|
||||
|
||||
/** 更新训练任务 */
|
||||
export const updateFineTune = (id: string | number, data: Partial<FineTuneTask>) =>
|
||||
put(`/fine-tune/${id}`, data)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getComputeGpus,
|
||||
getComputeNodes,
|
||||
getComputeQueue,
|
||||
getComputeSyncJob,
|
||||
getNodeReplicas,
|
||||
repairNodeReplicas,
|
||||
testComputeNode,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
type ComputeNode,
|
||||
type ComputeQueueItem,
|
||||
type ResourceReplica,
|
||||
type ResourceSyncJob,
|
||||
} from '@/api/modules/compute'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
@@ -34,6 +36,7 @@ const nodes = ref<ComputeNode[]>([])
|
||||
const gpus = ref<ComputeGpu[]>([])
|
||||
const queue = ref<ComputeQueueItem[]>([])
|
||||
const replicas = ref<ResourceReplica[]>([])
|
||||
const activeSyncJob = ref<ResourceSyncJob | null>(null)
|
||||
const selectedNodeId = ref('')
|
||||
const lastUpdated = ref('')
|
||||
const nodeDialogVisible = ref(false)
|
||||
@@ -91,6 +94,7 @@ async function load(options: { showLoading?: boolean; showButtonLoading?: boolea
|
||||
if ((!selectedNodeId.value || !nodeList.some((item) => item.id === selectedNodeId.value)) && nodeList.length) {
|
||||
selectedNodeId.value = nodeList[0].id
|
||||
}
|
||||
await pollActiveSyncJob()
|
||||
await loadReplicas()
|
||||
lastUpdated.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
} finally {
|
||||
@@ -100,6 +104,19 @@ async function load(options: { showLoading?: boolean; showButtonLoading?: boolea
|
||||
}
|
||||
}
|
||||
|
||||
async function pollActiveSyncJob() {
|
||||
if (!activeSyncJob.value?.id) return
|
||||
try {
|
||||
const sync = await getComputeSyncJob(activeSyncJob.value.id)
|
||||
activeSyncJob.value = sync
|
||||
if (['completed', 'failed', 'stopped', 'cancelled'].includes(sync.status)) {
|
||||
await loadReplicas()
|
||||
}
|
||||
} catch {
|
||||
activeSyncJob.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReplicas() {
|
||||
if (!selectedNodeId.value) {
|
||||
replicas.value = []
|
||||
@@ -150,8 +167,11 @@ async function handleReplicaRepair() {
|
||||
repairingReplicas.value = true
|
||||
try {
|
||||
const result = await repairNodeReplicas(selectedNodeId.value, targets.map((item) => item.id))
|
||||
activeSyncJob.value = result.sync
|
||||
replicas.value = result.replicas || []
|
||||
if (result.failed?.length) {
|
||||
if (result.async) {
|
||||
ElMessage.success('副本修复任务已提交,页面将自动刷新进度')
|
||||
} else if (result.failed?.length) {
|
||||
ElMessage.warning(`修复完成,失败 ${result.failed.length} 个`)
|
||||
} else {
|
||||
ElMessage.success('副本修复完成')
|
||||
@@ -407,6 +427,13 @@ onUnmounted(() => {
|
||||
<el-button :loading="checkingReplicas" @click="handleReplicaDriftCheck">漂移检测</el-button>
|
||||
<el-button type="primary" :loading="repairingReplicas" @click="handleReplicaRepair">修复副本</el-button>
|
||||
</div>
|
||||
<div v-if="activeSyncJob" class="sync-progress">
|
||||
<div>
|
||||
<strong>同步任务 {{ activeSyncJob.id }}</strong>
|
||||
<el-tag :type="statusTagType(activeSyncJob.status)" size="small">{{ statusLabel(activeSyncJob.status) }}</el-tag>
|
||||
</div>
|
||||
<el-progress :percentage="Number(activeSyncJob.progress || 0)" :stroke-width="8" />
|
||||
</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" />
|
||||
@@ -582,6 +609,22 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.sync-progress {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.node-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -233,7 +233,7 @@ function taskPayload() {
|
||||
|
||||
function externalPayload(): DataProcessExternalSourcePayload {
|
||||
return {
|
||||
type: externalSource.type,
|
||||
type: 'postgresql',
|
||||
url: externalSource.url.trim(),
|
||||
auth_mode: externalSource.authMode,
|
||||
username: externalSource.username || undefined,
|
||||
|
||||
@@ -6,9 +6,11 @@ import PageCard from '@/components/PageCard.vue'
|
||||
import ModelSelectDialog from '@/components/ModelSelectDialog.vue'
|
||||
import {
|
||||
createFineTune,
|
||||
preflightFineTune,
|
||||
startFineTune,
|
||||
updateFineTune,
|
||||
checkFineTuneName,
|
||||
type FineTunePreflightResult,
|
||||
} from '@/api/modules/fineTune'
|
||||
import { getModelList } from '@/api/modules/model'
|
||||
import { getDatasetList } from '@/api/modules/dataset'
|
||||
@@ -27,6 +29,8 @@ import type { ModelItem, DatasetItem, GpuInfo } from '@/types'
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const preflightLoading = ref(false)
|
||||
const preflightResult = ref<FineTunePreflightResult | null>(null)
|
||||
|
||||
const models = ref<ModelItem[]>([])
|
||||
const datasets = ref<DatasetItem[]>([])
|
||||
@@ -60,6 +64,12 @@ const modelDialogTitle = computed(() => selectedModel.value?.name || '')
|
||||
/** 训练命令与提交载荷共用同一份表单模型。 */
|
||||
const commandPreview = computed(() => buildFineTuneCommand(form, selectedGpus.value))
|
||||
|
||||
const remoteCommandPreview = computed(() => {
|
||||
const command = preflightResult.value?.preview?.command
|
||||
if (Array.isArray(command) && command.length) return command.join(' ')
|
||||
return preflightResult.value?.preview?.command_text || ''
|
||||
})
|
||||
|
||||
/** GPU 多选切换 */
|
||||
function toggleGpu(index: number) {
|
||||
const idx = selectedGpus.value.indexOf(index)
|
||||
@@ -184,6 +194,11 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
const payload = buildFineTunePayload(form, selectedGpus.value)
|
||||
const preflight = await runPreflight(payload)
|
||||
if (!preflight?.valid) {
|
||||
ElMessage.error('训练预检未通过,请先处理预检问题')
|
||||
return
|
||||
}
|
||||
const createRes = await createFineTune(toCreateFineTunePayload(payload))
|
||||
const taskId = createRes.id
|
||||
|
||||
@@ -203,6 +218,43 @@ async function handleSubmit() {
|
||||
})
|
||||
}
|
||||
|
||||
async function runPreflight(payload = buildFineTunePayload(form, selectedGpus.value)) {
|
||||
preflightLoading.value = true
|
||||
try {
|
||||
const result = await preflightFineTune(payload)
|
||||
preflightResult.value = result
|
||||
if (result.valid) {
|
||||
ElMessage.success('训练预检通过')
|
||||
} else {
|
||||
ElMessage.warning('训练预检未通过')
|
||||
}
|
||||
return result
|
||||
} catch {
|
||||
preflightResult.value = {
|
||||
valid: false,
|
||||
errors: ['预检接口调用失败,请检查后端服务和算力节点连接'],
|
||||
warnings: [],
|
||||
diagnostics: [{ level: 'error', title: '预检调用失败', suggestion: '请确认后端服务可访问 Compute API,并检查浏览器或后端日志。' }],
|
||||
}
|
||||
ElMessage.error('训练预检失败')
|
||||
return preflightResult.value
|
||||
} finally {
|
||||
preflightLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePreflightClick() {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (selectedGpus.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一个 GPU')
|
||||
return
|
||||
}
|
||||
await runPreflight()
|
||||
})
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
router.back()
|
||||
}
|
||||
@@ -437,9 +489,47 @@ onMounted(() => {
|
||||
|
||||
<!-- 训练命令预览 -->
|
||||
<el-divider content-position="left">训练命令预览</el-divider>
|
||||
<div class="preflight-actions">
|
||||
<el-button type="primary" plain :loading="preflightLoading" @click="handlePreflightClick">
|
||||
<i class="fa fa-check-circle-o" style="margin-right: 4px;" /> 预检训练配置
|
||||
</el-button>
|
||||
<span class="preflight-hint">预检会调用后端调度和 Compute validate,提前检查数据格式、模型路径、GPU 条件和真实训练命令。</span>
|
||||
</div>
|
||||
<div class="command-preview-wrapper">
|
||||
<pre class="command-preview">{{ commandPreview }}</pre>
|
||||
</div>
|
||||
<div v-if="preflightResult" class="preflight-panel" :class="{ 'is-valid': preflightResult.valid, 'is-invalid': !preflightResult.valid }">
|
||||
<div class="preflight-header">
|
||||
<strong>{{ preflightResult.valid ? '预检通过' : '预检未通过' }}</strong>
|
||||
<span v-if="preflightResult.node">
|
||||
调度节点:{{ preflightResult.node.code || preflightResult.node.name || preflightResult.node.id }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="remoteCommandPreview" class="preflight-section">
|
||||
<span class="section-label">后端真实命令</span>
|
||||
<pre class="command-preview remote">{{ remoteCommandPreview }}</pre>
|
||||
</div>
|
||||
<div v-if="preflightResult.errors?.length" class="preflight-section">
|
||||
<span class="section-label">错误</span>
|
||||
<ul>
|
||||
<li v-for="item in preflightResult.errors" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="preflightResult.warnings?.length" class="preflight-section">
|
||||
<span class="section-label">警告</span>
|
||||
<ul>
|
||||
<li v-for="item in preflightResult.warnings" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="preflightResult.diagnostics?.length" class="preflight-section">
|
||||
<span class="section-label">诊断建议</span>
|
||||
<ul>
|
||||
<li v-for="item in preflightResult.diagnostics" :key="item.title">
|
||||
<strong>{{ item.title }}:</strong>{{ item.suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions-wrapper">
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">创建并启动训练</el-button>
|
||||
@@ -604,6 +694,67 @@ onMounted(() => {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.preflight-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 0 0 12px 80px;
|
||||
}
|
||||
|
||||
.preflight-hint {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.preflight-panel {
|
||||
margin: -8px 0 24px 80px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
background: #f8fafc;
|
||||
|
||||
&.is-valid {
|
||||
border-color: #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.preflight-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
color: #111827;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.preflight-section {
|
||||
margin-top: 10px;
|
||||
|
||||
ul {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
color: #374151;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.section-label {
|
||||
display: inline-flex;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
@@ -637,6 +788,11 @@ onMounted(() => {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
||||
&.remote {
|
||||
margin-top: 6px;
|
||||
background: #111827;
|
||||
}
|
||||
}
|
||||
|
||||
.model-picker-input {
|
||||
|
||||
@@ -8,7 +8,7 @@ import TrainingTaskOverview from './training-log/TrainingTaskOverview.vue'
|
||||
import { usePolling } from '@/composables/usePolling'
|
||||
import '@/plugins/echarts-training-log'
|
||||
import { useModelsStore } from '@/stores/models'
|
||||
import { getFineTune, getFineTuneLogs } from '@/api/modules/fineTune'
|
||||
import { getFineTune, getFineTuneDiagnostics, getFineTuneLogs, type TrainingDiagnostic } from '@/api/modules/fineTune'
|
||||
import { getTrainingLogFiles, getTrainingLogContent } from '@/api/modules/log'
|
||||
import { getDataset } from '@/api/modules/dataset'
|
||||
import { getSystemInfo } from '@/api/modules/system'
|
||||
@@ -35,6 +35,7 @@ const logContent = ref('')
|
||||
const gpuPool = ref<GpuInfo[]>([])
|
||||
const gpuUpdatedAt = ref<Date | null>(null)
|
||||
const gpuLoadError = ref('')
|
||||
const diagnostics = ref<TrainingDiagnostic[]>([])
|
||||
|
||||
/** 初始加载状态:首次数据返回前显示 loading,避免空白闪烁 */
|
||||
const loading = ref(true)
|
||||
@@ -247,6 +248,19 @@ async function loadLog(currentTask: FineTuneTask) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiagnostics(currentTask: FineTuneTask) {
|
||||
if (currentTask.status !== 'failed') {
|
||||
diagnostics.value = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await getFineTuneDiagnostics(currentTask.id)
|
||||
diagnostics.value = result.diagnostics || []
|
||||
} catch {
|
||||
diagnostics.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
if (refreshInFlight) return
|
||||
refreshInFlight = true
|
||||
@@ -262,7 +276,7 @@ async function refreshAll() {
|
||||
const datasetPromise = currentTask.train_dataset_id
|
||||
? loadDataset(currentTask.train_dataset_id)
|
||||
: Promise.resolve()
|
||||
await Promise.all([datasetPromise, loadLog(currentTask), loadGpuStatus()])
|
||||
await Promise.all([datasetPromise, loadLog(currentTask), loadGpuStatus(), loadDiagnostics(currentTask)])
|
||||
} finally {
|
||||
loading.value = false
|
||||
refreshInFlight = false
|
||||
@@ -535,6 +549,15 @@ onMounted(async () => {
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<PageCard v-if="diagnostics.length" class="diagnostics-card" title="失败诊断" subtitle="根据训练日志和失败原因生成的排查建议">
|
||||
<div class="diagnostics-list">
|
||||
<article v-for="item in diagnostics" :key="item.title" class="diagnostics-item">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<p>{{ item.suggestion }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</PageCard>
|
||||
|
||||
<!-- 原始日志 -->
|
||||
<PageCard class="log-card" title="训练日志" subtitle="查看训练任务的原始运行输出">
|
||||
<template #extra><span class="log-meta">{{ logLineCount }} 行 · 每 5 秒刷新</span></template>
|
||||
@@ -634,6 +657,7 @@ onMounted(async () => {
|
||||
.summary-card,
|
||||
.parameters-card,
|
||||
.metrics-panel,
|
||||
.diagnostics-card,
|
||||
.log-card {
|
||||
margin-bottom: 0;
|
||||
border: 1px solid #e4e7ed !important;
|
||||
@@ -641,6 +665,28 @@ onMounted(async () => {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.diagnostics-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.diagnostics-item {
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
background: #fef2f2;
|
||||
|
||||
strong {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #374151;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user