From 7055f62da1f93a074cc74fa1066e3c815b9af851 Mon Sep 17 00:00:00 2001 From: Zhu Junhao <2661664053@qq.com> Date: Thu, 3 Sep 2026 11:08:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=8C=E6=AD=A5=20Agent=20Run=20?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目启动、材料确认、规划确认和重试后立即使用接口返回的 modelConfigId 更新页面,避免运行中的任务错误显示为尚未选择模型。 --- web-ui/src/pages/ProjectPage.test.ts | 24 +++++++++++++++- web-ui/src/pages/ProjectPage.vue | 43 +++++++++++++++++++--------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/web-ui/src/pages/ProjectPage.test.ts b/web-ui/src/pages/ProjectPage.test.ts index 1e694cd..b5f8817 100644 --- a/web-ui/src/pages/ProjectPage.test.ts +++ b/web-ui/src/pages/ProjectPage.test.ts @@ -70,6 +70,8 @@ interface ProjectFixtureOptions { projectStatus?: 'MATERIAL_CHECK' | 'PLANNING' | 'WRITING' | 'DELIVERED' | 'FAILED' | 'ARCHIVED' files?: Array> artifacts?: Array> + latestRun?: { status: string; modelConfigId?: string } | null + startedRun?: { status: string; modelConfigId?: string } } /** @@ -90,9 +92,14 @@ function mountProject(runStatus: 'RUNNING' | 'INTERRUPTED', options: ProjectFixt if (url.startsWith('/api/projects/project-1/events')) return [] if (url === '/api/projects/project-1/plan') return null if (url === '/api/projects/project-1/runs/latest') { - return { status: runStatus, modelConfigId: 'model-a' } + return options.latestRun === undefined + ? { status: runStatus, modelConfigId: 'model-a' } + : options.latestRun } if (url === '/api/models') return enabledModels + if (url === '/api/projects/project-1/runs/material-check' && requestOptions?.method === 'POST') { + return options.startedRun || { status: 'RUNNING', modelConfigId: 'model-a' } + } if (requestOptions?.method === 'POST') return { status: 'RUNNING' } throw new Error(`未处理的测试请求:${url}`) }) @@ -183,4 +190,19 @@ describe('ProjectPage 项目工作区', () => { expect(context.text()).toContain('先进级申报书.docx') expect(context.get('a').attributes('href')).toBe('/api/artifacts/artifact-1/download') }) + + it('首次启动材料检查后立即显示本次 Run 实际绑定的模型', async () => { + const wrapper = mountProject('RUNNING', { + projectStatus: 'MATERIAL_CHECK', + latestRun: null, + startedRun: { status: 'RUNNING', modelConfigId: 'model-a' } + }) + await flushPromises() + expect(wrapper.get('.current-model-name').text()).toBe('尚未选择') + + await wrapper.findAll('button').find(button => button.text() === '开始材料检验')!.trigger('click') + await flushPromises() + + expect(wrapper.get('.current-model-name').text()).toBe('当前模型 · model-a') + }) }) diff --git a/web-ui/src/pages/ProjectPage.vue b/web-ui/src/pages/ProjectPage.vue index dca07df..4314013 100644 --- a/web-ui/src/pages/ProjectPage.vue +++ b/web-ui/src/pages/ProjectPage.vue @@ -18,6 +18,11 @@ interface ModelOption { defaultModel: boolean } +interface RunSummary { + status: string + modelConfigId?: string +} + const projectStageDefinitions = [ { key: 'materials', label: '材料检查', description: '核验企业材料' }, { key: 'planning', label: '规划确认', description: '确认建设规划' }, @@ -101,6 +106,17 @@ const updatedAtLabel = computed(() => { return Number.isNaN(value.getTime()) ? '--' : value.toLocaleString('zh-CN', { hour12: false }) }) +/** + * 把创建、恢复或切换 Run 的响应同步到页面状态。 + * 后端返回的 modelConfigId 代表本次 Run 真正绑定的模型,不能继续沿用上一个 Run + * 或页面初次加载时的空值;仅在兼容旧响应时才使用调用方提供的兜底模型 ID。 + */ +function syncRunState(run: RunSummary, fallbackModelConfigId = '') { + runStatus.value = run.status + const modelConfigId = run.modelConfigId || fallbackModelConfigId + if (modelConfigId) currentModelConfigId.value = modelConfigId +} + async function load(id: string) { const version = ++loadVersion historyLoading.value = true @@ -115,7 +131,7 @@ async function load(id: string) { api(`/api/projects/${id}/files`), api(`/api/projects/${id}/artifacts`), api(`/api/projects/${id}/plan`), - api<{ status: string; modelConfigId?: string; pendingInterrupt?: string } | null>(`/api/projects/${id}/runs/latest`), + api<(RunSummary & { pendingInterrupt?: string }) | null>(`/api/projects/${id}/runs/latest`), // 模型摘要只服务于右侧信息栏;加载失败不能阻断项目主体和历史事件恢复。 api('/api/models').catch(() => []) ]) @@ -260,8 +276,8 @@ async function startCheck() { if (loading.value || folderUploading.value) return loading.value = true try { - const run = await api<{ status: string }>(`/api/projects/${projectId.value}/runs/material-check`, { method: 'POST' }) - runStatus.value = run.status + const run = await api(`/api/projects/${projectId.value}/runs/material-check`, { method: 'POST' }) + syncRunState(run) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '启动失败') } finally { @@ -273,8 +289,8 @@ async function retry() { if (plan.value?.status === 'CONFIRMED') { loading.value = true try { - await api(`/api/projects/${projectId.value}/runs/writing`, { method: 'POST' }) - runStatus.value = 'RUNNING' + const run = await api(`/api/projects/${projectId.value}/runs/writing`, { method: 'POST' }) + syncRunState(run) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '重试失败') } finally { @@ -289,8 +305,8 @@ async function stopRun() { if (!running.value || controlLoading.value) return controlLoading.value = true try { - const run = await api<{ status: string }>(`/api/projects/${projectId.value}/runs/stop`, { method: 'POST' }) - runStatus.value = run.status + const run = await api(`/api/projects/${projectId.value}/runs/stop`, { method: 'POST' }) + syncRunState(run) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '停止失败') } finally { @@ -334,12 +350,11 @@ async function confirmModelSelection() { controlLoading.value = true try { const endpoint = modelPickerMode.value === 'switch' ? 'switch-model' : 'resume' - const run = await api<{ status: string; modelConfigId?: string }>(`/api/projects/${projectId.value}/runs/${endpoint}`, { + const run = await api(`/api/projects/${projectId.value}/runs/${endpoint}`, { method: 'POST', body: JSON.stringify({ modelConfigId: selectedModelConfigId.value }) }) - runStatus.value = run.status - currentModelConfigId.value = run.modelConfigId || selectedModelConfigId.value + syncRunState(run, selectedModelConfigId.value) modelPickerVisible.value = false } catch (error) { const fallback = modelPickerMode.value === 'switch' ? '切换失败' : '继续失败' @@ -391,14 +406,14 @@ async function confirmPlan(value: Record) { if (!plan.value || loading.value) return loading.value = true try { - const result = await api<{ plan: PlanView; run: { status: string } }>(`/api/projects/${projectId.value}/plan/confirm`, { + const result = await api<{ plan: PlanView; run: RunSummary }>(`/api/projects/${projectId.value}/plan/confirm`, { method: 'POST', body: JSON.stringify({ planId: plan.value.id, plan: value }) }) localStorage.removeItem(`plan-draft:${plan.value.id}`) plan.value = result.plan pendingAsk.value = null - runStatus.value = result.run.status + syncRunState(result.run) await refreshProject() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '规划确认失败') @@ -411,12 +426,12 @@ async function confirmMaterials(value: Record) { if (loading.value) return loading.value = true try { - const run = await api<{ status: string }>(`/api/projects/${projectId.value}/material/confirm`, { + const run = await api(`/api/projects/${projectId.value}/material/confirm`, { method: 'POST', body: JSON.stringify(value) }) localStorage.removeItem(`material-ask:${String(pendingAsk.value?.interruptId || '')}`) pendingAsk.value = null - runStatus.value = run.status + syncRunState(run) await refreshProject() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '材料确认失败')