fix: 同步 Agent Run 绑定模型

项目启动、材料确认、规划确认和重试后立即使用接口返回的 modelConfigId 更新页面,避免运行中的任务错误显示为尚未选择模型。
This commit is contained in:
Zhu Junhao
2026-09-03 11:08:43 +08:00
parent 4759759d02
commit 7055f62da1
2 changed files with 52 additions and 15 deletions

View File

@@ -70,6 +70,8 @@ interface ProjectFixtureOptions {
projectStatus?: 'MATERIAL_CHECK' | 'PLANNING' | 'WRITING' | 'DELIVERED' | 'FAILED' | 'ARCHIVED' projectStatus?: 'MATERIAL_CHECK' | 'PLANNING' | 'WRITING' | 'DELIVERED' | 'FAILED' | 'ARCHIVED'
files?: Array<Record<string, unknown>> files?: Array<Record<string, unknown>>
artifacts?: Array<Record<string, unknown>> artifacts?: Array<Record<string, unknown>>
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.startsWith('/api/projects/project-1/events')) return []
if (url === '/api/projects/project-1/plan') return null if (url === '/api/projects/project-1/plan') return null
if (url === '/api/projects/project-1/runs/latest') { 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/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' } if (requestOptions?.method === 'POST') return { status: 'RUNNING' }
throw new Error(`未处理的测试请求:${url}`) throw new Error(`未处理的测试请求:${url}`)
}) })
@@ -183,4 +190,19 @@ describe('ProjectPage 项目工作区', () => {
expect(context.text()).toContain('先进级申报书.docx') expect(context.text()).toContain('先进级申报书.docx')
expect(context.get('a').attributes('href')).toBe('/api/artifacts/artifact-1/download') 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')
})
}) })

View File

@@ -18,6 +18,11 @@ interface ModelOption {
defaultModel: boolean defaultModel: boolean
} }
interface RunSummary {
status: string
modelConfigId?: string
}
const projectStageDefinitions = [ const projectStageDefinitions = [
{ key: 'materials', label: '材料检查', description: '核验企业材料' }, { key: 'materials', label: '材料检查', description: '核验企业材料' },
{ key: 'planning', label: '规划确认', description: '确认建设规划' }, { key: 'planning', label: '规划确认', description: '确认建设规划' },
@@ -101,6 +106,17 @@ const updatedAtLabel = computed(() => {
return Number.isNaN(value.getTime()) ? '--' : value.toLocaleString('zh-CN', { hour12: false }) 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) { async function load(id: string) {
const version = ++loadVersion const version = ++loadVersion
historyLoading.value = true historyLoading.value = true
@@ -115,7 +131,7 @@ async function load(id: string) {
api<ProjectFile[]>(`/api/projects/${id}/files`), api<ProjectFile[]>(`/api/projects/${id}/files`),
api<Artifact[]>(`/api/projects/${id}/artifacts`), api<Artifact[]>(`/api/projects/${id}/artifacts`),
api<PlanView | null>(`/api/projects/${id}/plan`), api<PlanView | null>(`/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<ModelOption[]>('/api/models').catch(() => []) api<ModelOption[]>('/api/models').catch(() => [])
]) ])
@@ -260,8 +276,8 @@ async function startCheck() {
if (loading.value || folderUploading.value) return if (loading.value || folderUploading.value) return
loading.value = true loading.value = true
try { try {
const run = await api<{ status: string }>(`/api/projects/${projectId.value}/runs/material-check`, { method: 'POST' }) const run = await api<RunSummary>(`/api/projects/${projectId.value}/runs/material-check`, { method: 'POST' })
runStatus.value = run.status syncRunState(run)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '启动失败') ElMessage.error(error instanceof Error ? error.message : '启动失败')
} finally { } finally {
@@ -273,8 +289,8 @@ async function retry() {
if (plan.value?.status === 'CONFIRMED') { if (plan.value?.status === 'CONFIRMED') {
loading.value = true loading.value = true
try { try {
await api(`/api/projects/${projectId.value}/runs/writing`, { method: 'POST' }) const run = await api<RunSummary>(`/api/projects/${projectId.value}/runs/writing`, { method: 'POST' })
runStatus.value = 'RUNNING' syncRunState(run)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '重试失败') ElMessage.error(error instanceof Error ? error.message : '重试失败')
} finally { } finally {
@@ -289,8 +305,8 @@ async function stopRun() {
if (!running.value || controlLoading.value) return if (!running.value || controlLoading.value) return
controlLoading.value = true controlLoading.value = true
try { try {
const run = await api<{ status: string }>(`/api/projects/${projectId.value}/runs/stop`, { method: 'POST' }) const run = await api<RunSummary>(`/api/projects/${projectId.value}/runs/stop`, { method: 'POST' })
runStatus.value = run.status syncRunState(run)
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '停止失败') ElMessage.error(error instanceof Error ? error.message : '停止失败')
} finally { } finally {
@@ -334,12 +350,11 @@ async function confirmModelSelection() {
controlLoading.value = true controlLoading.value = true
try { try {
const endpoint = modelPickerMode.value === 'switch' ? 'switch-model' : 'resume' 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<RunSummary>(`/api/projects/${projectId.value}/runs/${endpoint}`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ modelConfigId: selectedModelConfigId.value }) body: JSON.stringify({ modelConfigId: selectedModelConfigId.value })
}) })
runStatus.value = run.status syncRunState(run, selectedModelConfigId.value)
currentModelConfigId.value = run.modelConfigId || selectedModelConfigId.value
modelPickerVisible.value = false modelPickerVisible.value = false
} catch (error) { } catch (error) {
const fallback = modelPickerMode.value === 'switch' ? '切换失败' : '继续失败' const fallback = modelPickerMode.value === 'switch' ? '切换失败' : '继续失败'
@@ -391,14 +406,14 @@ async function confirmPlan(value: Record<string, unknown>) {
if (!plan.value || loading.value) return if (!plan.value || loading.value) return
loading.value = true loading.value = true
try { 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', method: 'POST',
body: JSON.stringify({ planId: plan.value.id, plan: value }) body: JSON.stringify({ planId: plan.value.id, plan: value })
}) })
localStorage.removeItem(`plan-draft:${plan.value.id}`) localStorage.removeItem(`plan-draft:${plan.value.id}`)
plan.value = result.plan plan.value = result.plan
pendingAsk.value = null pendingAsk.value = null
runStatus.value = result.run.status syncRunState(result.run)
await refreshProject() await refreshProject()
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '规划确认失败') ElMessage.error(error instanceof Error ? error.message : '规划确认失败')
@@ -411,12 +426,12 @@ async function confirmMaterials(value: Record<string, unknown>) {
if (loading.value) return if (loading.value) return
loading.value = true loading.value = true
try { try {
const run = await api<{ status: string }>(`/api/projects/${projectId.value}/material/confirm`, { const run = await api<RunSummary>(`/api/projects/${projectId.value}/material/confirm`, {
method: 'POST', body: JSON.stringify(value) method: 'POST', body: JSON.stringify(value)
}) })
localStorage.removeItem(`material-ask:${String(pendingAsk.value?.interruptId || '')}`) localStorage.removeItem(`material-ask:${String(pendingAsk.value?.interruptId || '')}`)
pendingAsk.value = null pendingAsk.value = null
runStatus.value = run.status syncRunState(run)
await refreshProject() await refreshProject()
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '材料确认失败') ElMessage.error(error instanceof Error ? error.message : '材料确认失败')