fix(web): 多 task 串行推进 task2 不再被预览生成时提前触发

onPreviewReadyForNextTask 在 task1 申请核对表刚生成、用户还没操作时就
提前拉起 task2,与用户后续在 task1 上的保存草稿/提交操作互相打架,导致
task2 完全无反应。移除该提前推进回调,统一由 onApplicationActionCompleted
在 task1 真正完成后再推进 task2。

- useWorkbenchAiApplicationPreviewFlow: 删除预览生成时的提前推进分支
- usePersonalWorkbenchAiMode: startModelPlannedApplicationPreview 不再传 onPreviewReadyForNextTask
- useWorkbenchAiActionRouter: 低置信确认按钮分支同步删除该回调
- 新增时序回归测试:预览生成不提前推进、保存草稿后才推进
- 更新两处源码正则断言为 doesNotMatch
This commit is contained in:
caoxiaozhu
2026-06-30 11:40:31 +08:00
parent 08f023243e
commit 43779f8f2c
6 changed files with 66 additions and 16 deletions

View File

@@ -766,7 +766,6 @@ export function usePersonalWorkbenchAiMode(props, emit) {
requestedSubmit: travelApplicationRequest.requestedSubmit, requestedSubmit: travelApplicationRequest.requestedSubmit,
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation, submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation,
stewardRemainingTasks: travelApplicationRequest.stewardRemainingTasks, stewardRemainingTasks: travelApplicationRequest.stewardRemainingTasks,
onPreviewReadyForNextTask: startModelPlannedNextTask,
onApplicationActionCompleted: startModelPlannedNextTask onApplicationActionCompleted: startModelPlannedNextTask
} }
) )

View File

@@ -96,12 +96,6 @@ export function useWorkbenchAiActionRouter({
requestedSubmit: Boolean(actionPayload.requestedSubmit), requestedSubmit: Boolean(actionPayload.requestedSubmit),
submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation), submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation),
stewardRemainingTasks, stewardRemainingTasks,
onPreviewReadyForNextTask: (remainingTasks = []) => {
const nextTaskAction = buildNextTaskSuggestedAction({ steward_remaining_tasks: remainingTasks })
if (nextTaskAction) {
handleInlineSuggestedAction(nextTaskAction)
}
},
onApplicationActionCompleted: (remainingTasks = []) => { onApplicationActionCompleted: (remainingTasks = []) => {
const nextTaskAction = buildNextTaskSuggestedAction({ steward_remaining_tasks: remainingTasks }) const nextTaskAction = buildNextTaskSuggestedAction({ steward_remaining_tasks: remainingTasks })
if (nextTaskAction) { if (nextTaskAction) {

View File

@@ -619,13 +619,10 @@ export function useWorkbenchAiApplicationPreviewFlow({
userText: options.userMessage || '保存草稿', userText: options.userMessage || '保存草稿',
onApplicationActionCompleted: options.onApplicationActionCompleted onApplicationActionCompleted: options.onApplicationActionCompleted
}) })
} else if (
typeof options.onPreviewReadyForNextTask === 'function' &&
Array.isArray(previewMessage.stewardRemainingTasks) &&
previewMessage.stewardRemainingTasks.length
) {
options.onPreviewReadyForNextTask(previewMessage.stewardRemainingTasks, previewMessage)
} }
// 多 task 串行推进:预览生成后不提前拉起下一个 task(避免和用户在 task1 核对表上的
// 保存草稿/提交操作互相打架,导致 task2 状态错乱)。task2 的推进统一交给
// onApplicationActionCompleted,在 task1 真正完成(保存草稿/提交成功)后再触发。
} catch (error) { } catch (error) {
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', { replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', {
id: pendingMessage.id, id: pendingMessage.id,

View File

@@ -148,7 +148,8 @@ test('workbench low-confidence application confirmation forwards remaining tasks
assert.ok(previewCall, 'startAiApplicationPreview 应被调用') assert.ok(previewCall, 'startAiApplicationPreview 应被调用')
assert.deepEqual(previewCall[3].stewardRemainingTasks, remainingTasks) assert.deepEqual(previewCall[3].stewardRemainingTasks, remainingTasks)
assert.equal(typeof previewCall[3].onPreviewReadyForNextTask, 'function') // 低置信确认按钮只在 task1 完成后推进 task2,不再在预览生成时提前推进。
assert.equal(previewCall[3].onPreviewReadyForNextTask, undefined)
assert.equal(typeof previewCall[3].onApplicationActionCompleted, 'function') assert.equal(typeof previewCall[3].onApplicationActionCompleted, 'function')
}) })

View File

@@ -147,6 +147,63 @@ test('workbench auto-saved application draft continues remaining steward task',
} }
}) })
test('workbench application preview does not continue next task until draft is saved or submitted', async () => {
// 时序回归:task1 申请核对表刚生成、用户还没点保存草稿/提交时,
// 不能提前拉起 task2(会导致两条流程消息和状态互相打架,最终 task2 无反应)。
// task2 的推进必须等 task1 真正完成(onApplicationActionCompleted)后再触发。
const originalFetch = globalThis.fetch
globalThis.fetch = async (url) => {
if (String(url).includes('/reimbursements/application-preview-action')) {
return {
ok: true,
async json() {
return { status: 'succeeded', result: { draft_payload: { claim_id: 'c1', claim_no: 'AEW2', status: 'draft' } } }
}
}
}
throw new Error(`unexpected request: ${url}`)
}
try {
const continuedTasks = []
const remainingTasks = [{
task_id: 'task-reimbursement-2',
task_type: 'reimbursement',
assigned_agent: 'reimbursement_assistant',
summary: '报销昨天的业务招待费 2000 元',
ontology_fields: { expense_type: 'entertainment', amount: '2000元', time_range: '2026-06-29', reason: '业务招待费报销' }
}]
const harness = buildApplicationPreviewFlowHarness([], {
onApplicationActionCompleted: (tasks) => { continuedTasks.push({ tasks, phase: 'module' }) }
})
// 第一步:生成申请核对表(不传 autoSaveDraft,模拟用户需要手动操作 task1)
await harness.flow.startAiApplicationPreview('travel', '差旅费', '2月20-23去上海出差并且报销昨天招待费2000元', {
stewardRemainingTasks: remainingTasks,
onApplicationActionCompleted: (tasks) => { continuedTasks.push({ tasks, phase: 'options' }) }
})
// 预览生成后,task2 不应被提前拉起
assert.equal(continuedTasks.length, 0, '预览生成时不应触发 task2 推进回调')
const previewMessage = harness.conversationMessages.value.find((m) => m.applicationPreview)
assert.equal(previewMessage?.stewardRemainingTasks?.length, 1, 'task2 应仍挂在核对表消息上等待用户完成 task1')
// 第二步:用户手动点击"保存草稿"(走 actionRouter,不传 options.onApplicationActionCompleted),
// 此时回落到模块级 onApplicationActionCompleted 触发 task2,这正是真实运行时的续跑路径。
await harness.flow.executeInlineApplicationPreviewAction('save_draft', previewMessage, {
userText: '保存草稿',
draftPayload: null
})
assert.equal(continuedTasks.length, 1, '保存草稿完成后应推进 task2')
assert.deepEqual(continuedTasks[0].tasks, remainingTasks)
assert.equal(continuedTasks[0].phase, 'module', '手动保存草稿走模块级续跑回调')
assert.doesNotMatch(harness.conversationMessages.value.at(-1).content, /继续处理费用报销/, '自动续跑时不展示重复的继续处理按钮')
} finally {
globalThis.fetch = originalFetch
}
})
test('workbench saved application draft can be submitted by contextual text without re-planning', async () => { test('workbench saved application draft can be submitted by contextual text without re-planning', async () => {
const originalFetch = globalThis.fetch const originalFetch = globalThis.fetch
const requests = [] const requests = []

View File

@@ -347,10 +347,12 @@ test('workbench AI mode asks steward model plan before fallback execution', () =
assert.match(personalWorkbenchAiModeScript, /submitRequiresConfirmation:\s*travelApplicationRequest\.submitRequiresConfirmation/) assert.match(personalWorkbenchAiModeScript, /submitRequiresConfirmation:\s*travelApplicationRequest\.submitRequiresConfirmation/)
assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/) assert.match(personalWorkbenchAiModeScript, /ontologyFields:\s*travelApplicationRequest\.ontologyFields/)
assert.match(personalWorkbenchAiModeScript, /stewardRemainingTasks:\s*travelApplicationRequest\.stewardRemainingTasks/) assert.match(personalWorkbenchAiModeScript, /stewardRemainingTasks:\s*travelApplicationRequest\.stewardRemainingTasks/)
assert.match(personalWorkbenchAiModeScript, /onPreviewReadyForNextTask:\s*startModelPlannedNextTask/)
assert.match(personalWorkbenchAiModeScript, /onApplicationActionCompleted:\s*startModelPlannedNextTask/) assert.match(personalWorkbenchAiModeScript, /onApplicationActionCompleted:\s*startModelPlannedNextTask/)
// 多 task 串行推进:预览生成时不再提前拉起下一个 task(会与用户在 task1 上的操作互相打架),
// 改为只在 task1 完成(保存草稿/提交)后通过 onApplicationActionCompleted 推进 task2。
assert.doesNotMatch(personalWorkbenchAiModeScript, /onPreviewReadyForNextTask/)
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/) assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
assert.match(applicationPreviewFlowScript, /options\.onPreviewReadyForNextTask/) assert.doesNotMatch(applicationPreviewFlowScript, /onPreviewReadyForNextTask/)
assert.match(applicationPreviewFlowScript, /const actionCompletedHandler = typeof options\.onApplicationActionCompleted === 'function'/) assert.match(applicationPreviewFlowScript, /const actionCompletedHandler = typeof options\.onApplicationActionCompleted === 'function'/)
assert.match(applicationPreviewFlowScript, /actionCompletedHandler\(targetMessage\.stewardRemainingTasks/) assert.match(applicationPreviewFlowScript, /actionCompletedHandler\(targetMessage\.stewardRemainingTasks/)
assert.match(applicationPreviewFlowScript, /onApplicationActionCompleted:\s*options\.onApplicationActionCompleted/) assert.match(applicationPreviewFlowScript, /onApplicationActionCompleted:\s*options\.onApplicationActionCompleted/)