feat(web): AI 工作台意图规划与规划思考模型

- 新增 workbenchAiIntentPlannerModel,基于 LLM function_call 解析建单/草稿/提交意图,区分 model 与 rule_fallback 来源
- 新增 workbenchAiPlanningThinkingModel 合并规划思考事件流,按 eventId 去重合并
- application gate/preview 模型接入意图规划,usePersonalWorkbenchAiMode/useWorkbenchAiStewardFlow/useWorkbenchAiActionRouter 链路适配,支持上下文提交
- steward 服务与 stewardPlanModel 适配新动作结构,receipt-folder-view 微调样式
- 新增 intent-planner-model/application-context-submit/steward-actions-service 测试,更新 gate-model/action-router/plan-message-copy/fast-preview 测试
This commit is contained in:
caoxiaozhu
2026-06-24 21:58:46 +08:00
parent 5311c99d69
commit bc560145a4
18 changed files with 1914 additions and 38 deletions

View File

@@ -2,6 +2,7 @@ import {
AI_APPLICATION_ACTION_SAVE_DRAFT,
AI_APPLICATION_ACTION_SUBMIT
} from '../../services/aiApplicationPreviewActions.js'
import { executeStewardAction } from '../../services/steward.js'
import { buildAiDocumentDetailRequest } from '../../utils/aiDocumentDetailReference.js'
import {
mergeComposerPrefill,
@@ -25,11 +26,16 @@ export function useWorkbenchAiActionRouter({
applicationFlow,
assistantDraft,
attachmentFlow,
conversationMessages,
createInlineMessage,
emit,
expenseFlow,
focusAiModeInput,
hasInlineAttachmentOcrDetails,
persistCurrentConversation,
replaceInlineMessage,
resolveLatestInlineUserPrompt,
scrollInlineConversationToBottom,
selectedFiles,
startInlineConversation,
toast,
@@ -45,6 +51,9 @@ export function useWorkbenchAiActionRouter({
const actionType = String(action?.action_type || '').trim()
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
if (actionPayload.steward_execute_action) {
return executeInlineStewardAction(action, sourceMessage)
}
if (actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION) {
if (!hasInlineAttachmentOcrDetails(sourceMessage)) {
toast('当前消息没有可查看的附件识别明细。')
@@ -162,6 +171,191 @@ export function useWorkbenchAiActionRouter({
}, action?.payload?.carry_files ? Array.from(selectedFiles.value) : [])
}
async function executeInlineStewardAction(action = {}, sourceMessage = null) {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const actionType = String(actionPayload.steward_action_type || '').trim()
if (!actionType || !actionPayload.steward_current_task) {
toast('当前动作缺少可执行任务快照,请重新发起规划。')
return false
}
if (sourceMessage?.suggestedActionsLocked) {
return true
}
if (sourceMessage) {
sourceMessage.suggestedActionsLocked = true
}
const pendingMessage = appendStewardActionMessage(
`正在执行:${String(action.label || '小财管家动作').trim() || '小财管家动作'}...`,
{ pending: true }
)
try {
let precheckResult = null
if (actionType === 'submit_application') {
precheckResult = await executeStewardAction(
buildStewardActionExecutePayload(action, 'run_duplicate_precheck')
)
if (!isSuccessfulStewardPrecheck(precheckResult)) {
finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(precheckResult), {
suggestedActions: []
})
return true
}
}
const contextJson = precheckResult
? { precheck_result: precheckResult.result_payload || precheckResult.resultPayload || {} }
: {}
const result = await executeStewardAction(
buildStewardActionExecutePayload(action, actionType, contextJson)
)
finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(result), {
suggestedActions: buildStewardActionResultActions(result)
})
return true
} catch (error) {
if (sourceMessage) {
sourceMessage.suggestedActionsLocked = false
}
finalizeStewardActionMessage(
pendingMessage,
`动作执行失败:${String(error?.message || '请稍后重试。').trim()}`
)
return false
}
}
function buildStewardActionExecutePayload(action = {}, actionType = '', contextJson = {}) {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const currentTask = actionPayload.steward_current_task || null
return {
action_type: actionType,
message: resolveStewardActionMessage(action),
plan_id: String(actionPayload.steward_plan_id || '').trim(),
conversation_id: String(actionPayload.conversation_id || actionPayload.conversationId || '').trim() || null,
task: currentTask,
action_step: resolveStewardActionStep(actionPayload, actionType),
confirmed: true,
context_json: contextJson,
client_trace_id: buildStewardActionClientTraceId(actionPayload, actionType)
}
}
function resolveStewardActionMessage(action = {}) {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
return String(
actionPayload.carry_text ||
actionPayload.original_message ||
resolveLatestInlineUserPrompt?.() ||
action.label ||
''
).trim()
}
function resolveStewardActionStep(actionPayload = {}, actionType = '') {
if (String(actionPayload.steward_action_step?.action_type || '').trim() === actionType) {
return actionPayload.steward_action_step
}
const steps = Array.isArray(actionPayload.steward_current_task?.action_steps)
? actionPayload.steward_current_task.action_steps
: []
return steps.find((step) => String(step?.action_type || '').trim() === actionType) || null
}
function buildStewardActionClientTraceId(actionPayload = {}, actionType = '') {
const planId = String(actionPayload.steward_plan_id || 'steward').trim() || 'steward'
const taskId = String(actionPayload.steward_action_task_id || actionPayload.steward_current_task?.task_id || 'task').trim() || 'task'
return `${planId}:${taskId}:${actionType}:${Date.now()}`
}
function isSuccessfulStewardPrecheck(result = {}) {
const status = String(result?.status || '').trim()
const payload = result?.result_payload || result?.resultPayload || {}
return status === 'succeeded' && payload?.blocking !== true
}
function appendStewardActionMessage(content, options = {}) {
if (!conversationMessages?.value || typeof createInlineMessage !== 'function') {
toast(String(content || '').trim())
return null
}
const message = createInlineMessage('assistant', content, options)
conversationMessages.value.push(message)
persistStewardActionConversation()
return message
}
function finalizeStewardActionMessage(pendingMessage, content, options = {}) {
if (!conversationMessages?.value || typeof createInlineMessage !== 'function') {
toast(String(content || '').trim())
return
}
const finalMessage = createInlineMessage('assistant', content, {
...options,
id: pendingMessage?.id
})
if (pendingMessage?.id && typeof replaceInlineMessage === 'function') {
replaceInlineMessage(pendingMessage.id, finalMessage)
} else if (pendingMessage?.id) {
const index = conversationMessages.value.findIndex((item) => item.id === pendingMessage.id)
if (index >= 0) {
conversationMessages.value.splice(index, 1, finalMessage)
} else {
conversationMessages.value.push(finalMessage)
}
} else {
conversationMessages.value.push(finalMessage)
}
persistStewardActionConversation()
}
function persistStewardActionConversation() {
persistCurrentConversation?.()
scrollInlineConversationToBottom?.({ force: true })
}
function buildStewardActionResultText(result = {}) {
const status = String(result?.status || '').trim()
const message = String(result?.message || '').trim()
if (status === 'succeeded') {
return message || '动作已完成。'
}
if (status === 'needs_confirmation') {
return message || '这一步还需要您确认后才能继续。'
}
if (status === 'blocked') {
return ['这一步暂时不能继续。', message].filter(Boolean).join('\n\n')
}
if (status === 'failed') {
return `动作执行失败:${message || '请稍后重试。'}`
}
return message || '动作已返回结果。'
}
function buildStewardActionResultActions(result = {}) {
if (String(result?.status || '').trim() !== 'succeeded') {
return []
}
const resultPayload = result?.result_payload || result?.resultPayload || {}
const draftPayload = resultPayload?.draft_payload || resultPayload?.draftPayload || resultPayload
const claimNo = String(draftPayload?.claim_no || draftPayload?.claimNo || '').trim()
const claimId = String(draftPayload?.claim_id || draftPayload?.claimId || '').trim()
if (!claimNo && !claimId) {
return []
}
return [{
label: claimNo ? `查看单据 ${claimNo}` : '查看单据',
description: '打开刚生成的单据详情。',
icon: 'mdi mdi-open-in-new',
action_type: 'open_application_detail',
payload: {
claim_id: claimId,
claim_no: claimNo
}
}]
}
return {
handleInlineSuggestedAction
}