fix: 同步 Agent Run 绑定模型
项目启动、材料确认、规划确认和重试后立即使用接口返回的 modelConfigId 更新页面,避免运行中的任务错误显示为尚未选择模型。
This commit is contained in:
@@ -70,6 +70,8 @@ interface ProjectFixtureOptions {
|
||||
projectStatus?: 'MATERIAL_CHECK' | 'PLANNING' | 'WRITING' | 'DELIVERED' | 'FAILED' | 'ARCHIVED'
|
||||
files?: 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 === '/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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<ProjectFile[]>(`/api/projects/${id}/files`),
|
||||
api<Artifact[]>(`/api/projects/${id}/artifacts`),
|
||||
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(() => [])
|
||||
])
|
||||
@@ -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<RunSummary>(`/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<RunSummary>(`/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<RunSummary>(`/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<RunSummary>(`/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<string, unknown>) {
|
||||
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<string, unknown>) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
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)
|
||||
})
|
||||
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 : '材料确认失败')
|
||||
|
||||
Reference in New Issue
Block a user