fix(web): 多 task 草稿自动保存后继续推进下一 task

useWorkbenchAiApplicationPreviewFlow 在自动保存草稿分支透传 onApplicationActionCompleted,
确保草稿/提交完成后仍能按 remaining tasks 推进,修复多 task 报销场景后续步骤不启动。
更新 application-context-submit/intent-planner-model 测试,补充 bug 日志与开发日志。
This commit is contained in:
caoxiaozhu
2026-06-29 20:17:36 +08:00
parent 6bdaeed6d4
commit 08f023243e
6 changed files with 120 additions and 6 deletions

View File

@@ -28,3 +28,10 @@
- 操作:先补红灯断言,要求工作台入口向申请预览流传 `onApplicationActionCompleted`,且申请预览流在保存/提交成功分支调用该回调;然后按同一条 `steward_continue_next_task` 路由复用原有报销启动逻辑。 - 操作:先补红灯断言,要求工作台入口向申请预览流传 `onApplicationActionCompleted`,且申请预览流在保存/提交成功分支调用该回调;然后按同一条 `steward_continue_next_task` 路由复用原有报销启动逻辑。
- 验证:红灯阶段 `node --test web/tests/workbench-ai-intent-planner-model.test.mjs` 失败在缺少 `onApplicationActionCompleted`;修复后 `node --test web/tests/workbench-ai-intent-planner-model.test.mjs` 17/17 通过,`node --test web/tests/workbench-ai-action-router.test.mjs` 7/7 通过;`git diff --check` 通过;`npm --prefix web run build` 通过。 - 验证:红灯阶段 `node --test web/tests/workbench-ai-intent-planner-model.test.mjs` 失败在缺少 `onApplicationActionCompleted`;修复后 `node --test web/tests/workbench-ai-intent-planner-model.test.mjs` 17/17 通过,`node --test web/tests/workbench-ai-action-router.test.mjs` 7/7 通过;`git diff --check` 通过;`npm --prefix web run build` 通过。
- 影响:复合任务里用户保存第一张出差申请草稿后,系统会立即进入第二个业务招待费报销任务,不再要求用户手动点击“继续处理费用报销”;低置信确认后再保存草稿也保持同样行为。 - 影响:复合任务里用户保存第一张出差申请草稿后,系统会立即进入第二个业务招待费报销任务,不再要求用户手动点击“继续处理费用报销”;低置信确认后再保存草稿也保持同样行为。
- 22:45继续修复同一 bug真实页面已经显示“申请草稿已保存”但保存动作结束后没有继续拉起第二个业务招待费报销 task。
- Git 提交检查:`git fetch --all --prune` 仍因 `LibreSSL SSL_connect: SSL_ERROR_SYSCALL` 失败;`HEAD..@{u}` 为空,未发现可见 upstream 新提交;`@{u}..HEAD` 本地领先 13 个提交:`6bdaeed6 chore: 忽略 .zcode 本地目录并更新规则表与开发日志``d5a8f847 refactor(web): 应用外壳/差旅详情/报销创建视图适配主题与多 task``c4b5fcc0 feat(web): AI 工作台多 task 串行推进与会话适配``5753899e feat(web): 主题皮肤系统与 LLM 设置面板重构``9c3fa80d feat(server): 设置持久化新增 LLM 模型表与主题字段`,以及前面记录过的 8 个本地 ahead 提交。
- 修改:`useWorkbenchAiApplicationPreviewFlow.js` 在保存草稿/提交申请成功分支新增 `actionCompletedHandler`,优先使用本次 `executeInlineApplicationPreviewAction` / `startAiApplicationPreview` options 传入的 `onApplicationActionCompleted`,没有时再回落到 composable 初始化回调;自动保存草稿时同步把 `options.onApplicationActionCompleted` 透传给保存动作。`workbench-ai-application-context-submit.test.mjs` 新增“自动保存申请草稿后继续剩余 steward task”的行为测试`workbench-ai-intent-planner-model.test.mjs` 更新结构断言,锁住 options 回调优先和自动保存透传契约。
- 操作:先用新增测试复现红灯:自动保存时确实只触发初始化回调,`options` 里的续跑回调没有被使用;再做最小修复,让保存动作按本次调用上下文续跑下一任务。
- 验证:红灯阶段 `node --test web/tests/workbench-ai-application-context-submit.test.mjs` 失败在 `fromOptions``undefined`;修复后该测试 2/2 通过;`node --test web/tests/workbench-ai-intent-planner-model.test.mjs` 17/17 通过;`node --test web/tests/workbench-ai-action-router.test.mjs` 7/7 通过;真实 `http://127.0.0.1:5173/api/v1/steward/plans` 采样确认该用户句子仍返回 `expense_application` + `reimbursement` 两个 task`npm --prefix web run build` 通过;`git diff --check` 通过。
- 影响:保存草稿结果消息到达后,前端会使用当前任务链路传下来的续跑回调立即处理剩余 task用户截图里的“申请草稿已保存”不再是终点后续业务招待费报销会自动进入现有报销流程。

View File

@@ -0,0 +1,17 @@
# 2026-06-27 综合工作日志
生成时间2026-06-27 17:43:24 CST
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
## 今日功能点
- 今日未发现功能点文档。
## 今日 Bugs
- 今日未发现 bug 修复记录。
## 综合分析
- 今日目录下暂无功能点或 bug 记录。
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。

View File

@@ -0,0 +1,17 @@
# 2026-06-28 综合工作日志
生成时间2026-06-28 18:30:35 CST
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
## 今日功能点
- 今日未发现功能点文档。
## 今日 Bugs
- 今日未发现 bug 修复记录。
## 综合分析
- 今日目录下暂无功能点或 bug 记录。
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。

View File

@@ -463,9 +463,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
targetMessage.suggestedActions = [] targetMessage.suggestedActions = []
const detailActions = buildInlineApplicationDetailAction(draftPayload) const detailActions = buildInlineApplicationDetailAction(draftPayload)
const nextTaskAction = buildApplicationPreviewNextTaskAction(targetMessage) const nextTaskAction = buildApplicationPreviewNextTaskAction(targetMessage)
const actionCompletedHandler = typeof options.onApplicationActionCompleted === 'function'
? options.onApplicationActionCompleted
: onApplicationActionCompleted
const shouldAutoContinueNextTask = Boolean( const shouldAutoContinueNextTask = Boolean(
nextTaskAction && nextTaskAction &&
typeof onApplicationActionCompleted === 'function' && typeof actionCompletedHandler === 'function' &&
Array.isArray(targetMessage.stewardRemainingTasks) && Array.isArray(targetMessage.stewardRemainingTasks) &&
targetMessage.stewardRemainingTasks.length targetMessage.stewardRemainingTasks.length
) )
@@ -485,7 +488,7 @@ export function useWorkbenchAiApplicationPreviewFlow({
persistCurrentConversation() persistCurrentConversation()
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
if (shouldAutoContinueNextTask) { if (shouldAutoContinueNextTask) {
onApplicationActionCompleted(targetMessage.stewardRemainingTasks, targetMessage) actionCompletedHandler(targetMessage.stewardRemainingTasks, targetMessage)
} }
return true return true
} catch (error) { } catch (error) {
@@ -613,7 +616,8 @@ export function useWorkbenchAiApplicationPreviewFlow({
if (options.autoSaveDraft) { if (options.autoSaveDraft) {
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, previewMessage, { await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, previewMessage, {
skipUserMessage: true, skipUserMessage: true,
userText: options.userMessage || '保存草稿' userText: options.userMessage || '保存草稿',
onApplicationActionCompleted: options.onApplicationActionCompleted
}) })
} else if ( } else if (
typeof options.onPreviewReadyForNextTask === 'function' && typeof options.onPreviewReadyForNextTask === 'function' &&

View File

@@ -19,7 +19,7 @@ function createInlineMessage(role, content, options = {}) {
} }
} }
function buildApplicationPreviewFlowHarness(messages) { function buildApplicationPreviewFlowHarness(messages, options = {}) {
const conversationMessages = createRef(messages) const conversationMessages = createRef(messages)
const applicationSubmitConfirmOpen = createRef(false) const applicationSubmitConfirmOpen = createRef(false)
const applicationSubmitConfirmContext = createRef(null) const applicationSubmitConfirmContext = createRef(null)
@@ -69,7 +69,8 @@ function buildApplicationPreviewFlowHarness(messages) {
resolveLatestInlineUserPrompt: () => '2026-02-20 至 2026-02-23去上海出差交通火车保存草稿', resolveLatestInlineUserPrompt: () => '2026-02-20 至 2026-02-23去上海出差交通火车保存草稿',
scrollInlineConversationToBottom: () => {}, scrollInlineConversationToBottom: () => {},
sending: createRef(false), sending: createRef(false),
toast: () => {} toast: () => {},
onApplicationActionCompleted: options.onApplicationActionCompleted
}) })
return { return {
@@ -80,6 +81,72 @@ function buildApplicationPreviewFlowHarness(messages) {
} }
} }
test('workbench auto-saved application draft continues remaining steward task', async () => {
const originalFetch = globalThis.fetch
const requests = []
globalThis.fetch = async (url, options = {}) => {
const normalizedUrl = String(url)
if (normalizedUrl.includes('/reimbursements/application-preview-action')) {
const body = JSON.parse(String(options.body || '{}'))
requests.push({ url: normalizedUrl, body })
return {
ok: true,
async json() {
return {
status: 'succeeded',
result: {
draft_payload: {
claim_id: 'claim-auto-saved-draft',
claim_no: 'AEW2DDAFL',
status: 'draft'
}
}
}
}
}
}
throw new Error(`unexpected request: ${normalizedUrl}`)
}
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-25',
reason: '业务招待费报销'
}
}]
const harness = buildApplicationPreviewFlowHarness([], {
onApplicationActionCompleted: (tasks, sourceMessage) => {
continuedTasks.push({ tasks, sourceMessage })
}
})
await harness.flow.startAiApplicationPreview('travel', '差旅费', '2月20-23日去上海出差3天服务国网服务器部署并且报销昨天的业务招待费2000元', {
autoSaveDraft: true,
stewardRemainingTasks: remainingTasks,
onApplicationActionCompleted: (tasks, sourceMessage) => {
continuedTasks.push({ tasks, sourceMessage, fromOptions: true })
}
})
assert.equal(requests.length, 1)
assert.equal(continuedTasks.length, 1)
assert.deepEqual(continuedTasks[0].tasks, remainingTasks)
assert.equal(continuedTasks[0].sourceMessage.stewardRemainingTasks, remainingTasks)
assert.equal(continuedTasks[0].fromOptions, true)
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

@@ -351,7 +351,9 @@ test('workbench AI mode asks steward model plan before fallback execution', () =
assert.match(personalWorkbenchAiModeScript, /onApplicationActionCompleted:\s*startModelPlannedNextTask/) assert.match(personalWorkbenchAiModeScript, /onApplicationActionCompleted:\s*startModelPlannedNextTask/)
assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/) assert.match(applicationPreviewFlowScript, /options\.autoSaveDraft/)
assert.match(applicationPreviewFlowScript, /options\.onPreviewReadyForNextTask/) assert.match(applicationPreviewFlowScript, /options\.onPreviewReadyForNextTask/)
assert.match(applicationPreviewFlowScript, /onApplicationActionCompleted\(\s*targetMessage\.stewardRemainingTasks/) assert.match(applicationPreviewFlowScript, /const actionCompletedHandler = typeof options\.onApplicationActionCompleted === 'function'/)
assert.match(applicationPreviewFlowScript, /actionCompletedHandler\(targetMessage\.stewardRemainingTasks/)
assert.match(applicationPreviewFlowScript, /onApplicationActionCompleted:\s*options\.onApplicationActionCompleted/)
assert.doesNotMatch(applicationPreviewFlowScript, /options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit/) assert.doesNotMatch(applicationPreviewFlowScript, /options\.autoSubmit && normalizeApplicationPreview\(preview\)\.readyToSubmit/)
assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/) assert.match(applicationPreviewFlowScript, /ontologyFields:\s*options\.ontologyFields/)
assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/) assert.match(applicationPreviewFlowScript, /executeInlineApplicationPreviewAction\(AI_APPLICATION_ACTION_SAVE_DRAFT/)