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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user