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:
@@ -35,7 +35,20 @@ import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js'
|
||||
import { useWorkbenchAiMessageExpansion } from './useWorkbenchAiMessageExpansion.js'
|
||||
import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js'
|
||||
import { useWorkbenchAiStewardFlow } from './useWorkbenchAiStewardFlow.js'
|
||||
import { isReimbursementCreationIntent } from './workbenchAiApplicationGateModel.js'
|
||||
import {
|
||||
isReimbursementCreationIntent
|
||||
} from './workbenchAiApplicationGateModel.js'
|
||||
import {
|
||||
buildRuleFallbackWorkbenchAiIntentPlan,
|
||||
normalizeWorkbenchAiIntentPlan,
|
||||
resolveExecutableTravelApplicationPlan,
|
||||
shouldRequestWorkbenchAiIntentPlan
|
||||
} from './workbenchAiIntentPlannerModel.js'
|
||||
import {
|
||||
buildInitialModelPlanningThinkingEvents,
|
||||
buildModelPlanningProgressSchedule,
|
||||
mergeWorkbenchAiThinkingEvents
|
||||
} from './workbenchAiPlanningThinkingModel.js'
|
||||
|
||||
const AI_SEARCH_CONVERSATION_ID = 'ai-search'
|
||||
const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
||||
@@ -251,11 +264,16 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
applicationFlow,
|
||||
assistantDraft,
|
||||
attachmentFlow,
|
||||
conversationMessages,
|
||||
createInlineMessage,
|
||||
emit,
|
||||
expenseFlow,
|
||||
focusAiModeInput,
|
||||
hasInlineAttachmentOcrDetails,
|
||||
persistCurrentConversation,
|
||||
replaceInlineMessage,
|
||||
resolveLatestInlineUserPrompt,
|
||||
scrollInlineConversationToBottom,
|
||||
selectedFiles,
|
||||
startInlineConversation,
|
||||
toast,
|
||||
@@ -560,6 +578,131 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
return false
|
||||
}
|
||||
|
||||
function isModelPlannedReimbursementTask(modelPlan = {}) {
|
||||
const tasks = Array.isArray(modelPlan?.tasks) ? modelPlan.tasks : []
|
||||
return tasks.some((task) => {
|
||||
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
||||
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
|
||||
return taskType === 'reimbursement' || assignedAgent === 'reimbursement_assistant'
|
||||
})
|
||||
}
|
||||
|
||||
function updateModelPlanningThinkingEvent(messageId, event) {
|
||||
const message = conversationMessages.value.find((item) => item.id === messageId)
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
const currentPlan = message.stewardPlan || {}
|
||||
message.stewardPlan = {
|
||||
...currentPlan,
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: mergeWorkbenchAiThinkingEvents(resolveInlineThinkingEvents(message), [event])
|
||||
}
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
}
|
||||
|
||||
function startModelPlanningProgressUpdates(messageId) {
|
||||
const timerIds = buildModelPlanningProgressSchedule().map(({ delayMs, event }) => (
|
||||
globalThis.setTimeout(() => {
|
||||
updateModelPlanningThinkingEvent(messageId, event)
|
||||
}, delayMs)
|
||||
))
|
||||
return () => {
|
||||
timerIds.forEach((timerId) => globalThis.clearTimeout(timerId))
|
||||
}
|
||||
}
|
||||
|
||||
function startModelPlanningConversation(cleanPrompt, entry = {}) {
|
||||
if (conversationId.value === AI_SEARCH_CONVERSATION_ID) {
|
||||
conversationId.value = ''
|
||||
conversationMessages.value = []
|
||||
activeConversationTitle.value = ''
|
||||
}
|
||||
activateInlineConversation({
|
||||
title: entry.label || cleanPrompt.slice(0, 18) || '新对话'
|
||||
})
|
||||
inlineConversationAutoScrollPinned.value = true
|
||||
conversationMessages.value.push(createInlineMessage('user', cleanPrompt))
|
||||
assistantDraft.value = ''
|
||||
removeWorkbenchDateTag()
|
||||
closeWorkbenchDatePicker()
|
||||
filesFlow.clearAiModeFiles()
|
||||
const pendingMessage = createInlineMessage('assistant', '正在识别意图,准备拆解申请、报销和附件任务。', {
|
||||
pending: true,
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: buildInitialModelPlanningThinkingEvents()
|
||||
}
|
||||
})
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
scrollInlineConversationToBottom()
|
||||
persistCurrentConversation()
|
||||
return pendingMessage
|
||||
}
|
||||
|
||||
function startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage = null) {
|
||||
void applicationFlow.startAiApplicationPreview(
|
||||
travelApplicationRequest.expenseType,
|
||||
travelApplicationRequest.expenseTypeLabel,
|
||||
travelApplicationRequest.sourceText,
|
||||
{
|
||||
userMessage: travelApplicationRequest.sourceText,
|
||||
pushUserMessage: !plannerPendingMessage,
|
||||
pendingMessageId: plannerPendingMessage?.id,
|
||||
ontologyFields: travelApplicationRequest.ontologyFields,
|
||||
autoSubmit: travelApplicationRequest.autoSubmit,
|
||||
autoSaveDraft: travelApplicationRequest.autoSaveDraft
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function executeModelPlannedWorkbenchIntent(cleanPrompt, entry = {}, files = []) {
|
||||
let intentPlan = null
|
||||
let modelPlan = null
|
||||
const plannerPendingMessage = startModelPlanningConversation(cleanPrompt, entry)
|
||||
const stopPlanningProgressUpdates = startModelPlanningProgressUpdates(plannerPendingMessage.id)
|
||||
sending.value = true
|
||||
try {
|
||||
modelPlan = await stewardFlow.resolveInlineExecutionPlan(cleanPrompt, entry, files, {
|
||||
pendingMessageId: plannerPendingMessage.id
|
||||
})
|
||||
intentPlan = normalizeWorkbenchAiIntentPlan(modelPlan, { prompt: cleanPrompt })
|
||||
} catch (error) {
|
||||
console.warn('AI mode intent planner failed, using local fallback:', error)
|
||||
const rulePlan = buildRuleFallbackWorkbenchAiIntentPlan(cleanPrompt)
|
||||
const ruleRequest = resolveExecutableTravelApplicationPlan(rulePlan)
|
||||
if (ruleRequest) {
|
||||
sending.value = false
|
||||
startModelPlannedApplicationPreview(ruleRequest, plannerPendingMessage)
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
stopPlanningProgressUpdates()
|
||||
sending.value = false
|
||||
}
|
||||
|
||||
const travelApplicationRequest = resolveExecutableTravelApplicationPlan(intentPlan)
|
||||
if (travelApplicationRequest) {
|
||||
startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (isModelPlannedReimbursementTask(modelPlan) || isReimbursementCreationIntent(cleanPrompt)) {
|
||||
replaceInlineMessage(plannerPendingMessage.id, createInlineMessage('assistant', '已识别为报销任务,正在进入报销流程。', {
|
||||
id: plannerPendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: resolveInlineThinkingEvents(plannerPendingMessage).map((item) => ({ ...item, status: 'completed' }))
|
||||
}
|
||||
}))
|
||||
void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || cleanPrompt)
|
||||
return
|
||||
}
|
||||
|
||||
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files, { pendingMessageId: plannerPendingMessage.id })
|
||||
}
|
||||
|
||||
function handleAiAnswerMarkdownClick(event) {
|
||||
const target = event?.target
|
||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||
@@ -594,6 +737,11 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldRequestWorkbenchAiIntentPlan(cleanPrompt)) {
|
||||
void executeModelPlannedWorkbenchIntent(cleanPrompt, entry, files)
|
||||
return
|
||||
}
|
||||
|
||||
if (isReimbursementCreationIntent(cleanPrompt)) {
|
||||
void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || '我要报销')
|
||||
return
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
extractInlineApplicationDraftPayload
|
||||
} from './workbenchAiApplicationPreviewModel.js'
|
||||
import { AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION } from './workbenchAiMessageModel.js'
|
||||
import {
|
||||
completeWorkbenchAiThinkingEvents,
|
||||
mergeWorkbenchAiThinkingEvents
|
||||
} from './workbenchAiPlanningThinkingModel.js'
|
||||
import {
|
||||
isOrphanInlineApplicationPreviewMessage,
|
||||
resolveInlineApplicationPreviewTextAction,
|
||||
@@ -202,6 +206,26 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
function hasSavedInlineApplicationDraft(message = {}, options = {}) {
|
||||
const draftPayload = message?.draftPayload || options.draftPayload || null
|
||||
if (!draftPayload || typeof draftPayload !== 'object') {
|
||||
return false
|
||||
}
|
||||
const claimId = String(draftPayload.claim_id || draftPayload.claimId || '').trim()
|
||||
const claimNo = String(draftPayload.claim_no || draftPayload.claimNo || '').trim()
|
||||
const status = String(draftPayload.status || '').trim().toLowerCase()
|
||||
return Boolean((claimId || claimNo) && status !== 'submitted')
|
||||
}
|
||||
|
||||
function isContextualInlineApplicationSubmitText(text = '') {
|
||||
const normalized = String(text || '').replace(/\s+/g, '').trim()
|
||||
return Boolean(
|
||||
normalized &&
|
||||
/提交/.test(normalized) &&
|
||||
/(这个|这张|当前|上面|上述|刚才|申请单|单据|草稿|领导|审核|审批)/.test(normalized)
|
||||
)
|
||||
}
|
||||
|
||||
function resolveLatestInlineApplicationPreviewMessage() {
|
||||
return resolveLatestApplicationPreviewMessage(conversationMessages.value)
|
||||
}
|
||||
@@ -334,7 +358,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
return true
|
||||
}
|
||||
|
||||
if (isSubmit && !options.confirmed) {
|
||||
const shouldSubmitSavedDraftDirectly = isSubmit &&
|
||||
!options.confirmed &&
|
||||
hasSavedInlineApplicationDraft(targetMessage, options) &&
|
||||
isContextualInlineApplicationSubmitText(userText)
|
||||
|
||||
if (isSubmit && !options.confirmed && !shouldSubmitSavedDraftDirectly) {
|
||||
requestInlineApplicationSubmitConfirmation(targetMessage, { ...options, userText })
|
||||
return true
|
||||
}
|
||||
@@ -474,11 +503,16 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
pushInlineUserMessage(options.userMessage || '确认发起出差申请')
|
||||
}
|
||||
|
||||
const existingPendingMessage = options.pendingMessageId
|
||||
? conversationMessages.value.find((item) => item.id === options.pendingMessageId)
|
||||
: null
|
||||
const previousThinkingEvents = completeWorkbenchAiThinkingEvents(resolveInlineThinkingEvents(existingPendingMessage))
|
||||
const pendingMessage = createInlineMessage('assistant', '正在生成申请核对表,请稍等...', {
|
||||
id: options.pendingMessageId,
|
||||
pending: true,
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: [
|
||||
thinkingEvents: mergeWorkbenchAiThinkingEvents(previousThinkingEvents, [
|
||||
{
|
||||
eventId: 'application-preview-build',
|
||||
title: '整理申请表字段',
|
||||
@@ -491,19 +525,25 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
content: '正在刷新差旅规则和费用测算,完成后会直接展示核对表。',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
])
|
||||
}
|
||||
})
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
if (options.pendingMessageId) {
|
||||
replaceInlineMessage(options.pendingMessageId, pendingMessage)
|
||||
} else {
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
}
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom()
|
||||
|
||||
try {
|
||||
const preview = await refreshApplicationPreviewEstimate(
|
||||
buildInlineApplicationPreview(expenseTypeLabel || expenseType, previewSourceText, currentUser.value || {})
|
||||
buildInlineApplicationPreview(expenseTypeLabel || expenseType, previewSourceText, currentUser.value || {}, {
|
||||
ontologyFields: options.ontologyFields
|
||||
})
|
||||
)
|
||||
const content = buildLocalApplicationPreviewMessage(preview)
|
||||
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', content, {
|
||||
const previewMessage = createInlineMessage('assistant', content, {
|
||||
id: pendingMessage.id,
|
||||
applicationPreview: preview,
|
||||
suggestedActions: buildInlineApplicationPreviewSuggestedActions(preview),
|
||||
@@ -512,7 +552,20 @@ export function useWorkbenchAiApplicationPreviewFlow({
|
||||
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
||||
},
|
||||
text: content
|
||||
}))
|
||||
})
|
||||
replaceInlineMessage(pendingMessage.id, previewMessage)
|
||||
if (options.autoSubmit && normalizeApplicationPreview(preview).readyToSubmit) {
|
||||
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, previewMessage, {
|
||||
confirmed: true,
|
||||
skipUserMessage: true,
|
||||
userText: options.userMessage || '直接提交'
|
||||
})
|
||||
} else if (options.autoSaveDraft) {
|
||||
await executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SAVE_DRAFT, previewMessage, {
|
||||
skipUserMessage: true,
|
||||
userText: options.userMessage || '保存草稿'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', {
|
||||
id: pendingMessage.id,
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
import {
|
||||
buildInlineAttachmentOcrDetails
|
||||
} from './workbenchAiMessageModel.js'
|
||||
import {
|
||||
completeWorkbenchAiThinkingEvents,
|
||||
mergeWorkbenchAiThinkingEvents
|
||||
} from './workbenchAiPlanningThinkingModel.js'
|
||||
|
||||
function shouldCheckAiRequiredApplicationGate(prompt) {
|
||||
const compact = String(prompt || '').replace(/\s+/g, '')
|
||||
@@ -187,13 +191,16 @@ export function useWorkbenchAiStewardFlow({
|
||||
return planRequest
|
||||
}
|
||||
|
||||
function handleInlineStewardStreamEvent(messageId, event) {
|
||||
function handleInlineStewardStreamEvent(messageId, event, options = {}) {
|
||||
const message = conversationMessages.value.find((item) => item.id === messageId)
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event?.event === 'answer_delta') {
|
||||
if (options.includeAnswerDelta === false) {
|
||||
return
|
||||
}
|
||||
const data = event?.data && typeof event.data === 'object' ? event.data : {}
|
||||
const shouldAutoScroll = inlineConversationAutoScrollPinned.value
|
||||
appendInlineMessageContent(message, data.delta || data.content || data.text || '')
|
||||
@@ -226,16 +233,23 @@ export function useWorkbenchAiStewardFlow({
|
||||
scrollInlineConversationToBottom({ force: shouldAutoScroll })
|
||||
}
|
||||
|
||||
async function fetchInlineStewardPlan(messageId, payload) {
|
||||
async function fetchInlineStewardPlan(messageId, payload, options = {}) {
|
||||
const {
|
||||
includeAnswerDelta = true,
|
||||
idleTimeoutMs = 90000,
|
||||
timeoutMs = 0,
|
||||
timeoutMessage = '小财管家仍在规划任务,已停止等待。您可以稍后继续追问。'
|
||||
} = options
|
||||
try {
|
||||
return await fetchStewardPlanStream(
|
||||
payload,
|
||||
{
|
||||
onEvent: (event) => handleInlineStewardStreamEvent(messageId, event)
|
||||
onEvent: (event) => handleInlineStewardStreamEvent(messageId, event, { includeAnswerDelta })
|
||||
},
|
||||
{
|
||||
idleTimeoutMs: 90000,
|
||||
timeoutMessage: '小财管家仍在规划任务,已停止等待。您可以稍后继续追问。'
|
||||
idleTimeoutMs,
|
||||
timeoutMs,
|
||||
timeoutMessage
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
@@ -249,23 +263,67 @@ export function useWorkbenchAiStewardFlow({
|
||||
}
|
||||
}
|
||||
|
||||
async function requestInlineAssistantReply(prompt, entry = {}, files = []) {
|
||||
async function resolveInlineExecutionPlan(prompt, entry = {}, files = [], options = {}) {
|
||||
const receiptContext = await collectAiModeReceiptContext(files)
|
||||
const planRequest = buildStewardPlanRequest({
|
||||
rawText: prompt,
|
||||
files,
|
||||
currentUser: currentUser.value || {},
|
||||
conversationId: conversationId.value,
|
||||
stewardState: stewardState.value
|
||||
})
|
||||
planRequest.context_json = {
|
||||
...planRequest.context_json,
|
||||
entry_source: 'workbench_ai_inline_execution_plan',
|
||||
source: entry.source || 'workbench',
|
||||
attachment_names: receiptContext.attachmentNames,
|
||||
attachment_count: receiptContext.attachmentCount,
|
||||
ocr_summary: receiptContext.ocrSummary,
|
||||
ocr_documents: receiptContext.ocrDocuments,
|
||||
ocr_source_file_names: receiptContext.ocrSourceFileNames,
|
||||
requested_execution_mode: 'plan_then_execute',
|
||||
...(receiptContext.ocrError ? { ocr_error: receiptContext.ocrError } : {})
|
||||
}
|
||||
await attachAiRequiredApplicationGate(planRequest, prompt)
|
||||
const planningMessageId = String(options.pendingMessageId || '').trim()
|
||||
if (planningMessageId) {
|
||||
return fetchInlineStewardPlan(planningMessageId, planRequest, {
|
||||
includeAnswerDelta: false,
|
||||
idleTimeoutMs: 45000,
|
||||
timeoutMs: 35000,
|
||||
timeoutMessage: '小财管家意图规划超时,已切换到本地保守计划。'
|
||||
})
|
||||
}
|
||||
return fetchStewardPlan(planRequest, {
|
||||
timeoutMs: 35000,
|
||||
timeoutMessage: '小财管家意图规划超时,已切换到本地保守计划。'
|
||||
})
|
||||
}
|
||||
|
||||
async function requestInlineAssistantReply(prompt, entry = {}, files = [], options = {}) {
|
||||
let shouldAutoScrollOnFinish = true
|
||||
const reusablePendingMessageId = String(options.pendingMessageId || '').trim()
|
||||
const reusablePendingMessage = reusablePendingMessageId
|
||||
? conversationMessages.value.find((item) => item.id === reusablePendingMessageId)
|
||||
: null
|
||||
const previousThinkingEvents = resolveInlineThinkingEvents(reusablePendingMessage)
|
||||
const completedPreviousThinkingEvents = completeWorkbenchAiThinkingEvents(previousThinkingEvents)
|
||||
const pendingMessage = createInlineMessage('assistant', '', {
|
||||
id: reusablePendingMessageId || undefined,
|
||||
pending: true,
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: [
|
||||
thinkingEvents: mergeWorkbenchAiThinkingEvents(completedPreviousThinkingEvents, [
|
||||
{
|
||||
eventId: 'init',
|
||||
title: '小财管家正在接入业务流程',
|
||||
content: '正在识别您的意图、上下文和附件信息。',
|
||||
status: 'running'
|
||||
}
|
||||
]
|
||||
])
|
||||
}
|
||||
})
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
reusablePendingMessageId ? replaceInlineMessage(reusablePendingMessageId, pendingMessage) : conversationMessages.value.push(pendingMessage)
|
||||
scrollInlineConversationToBottom()
|
||||
|
||||
try {
|
||||
@@ -303,8 +361,8 @@ export function useWorkbenchAiStewardFlow({
|
||||
})
|
||||
const previousThinkingEvents = resolveInlineThinkingEvents(pendingMessage)
|
||||
const nextThinkingEvents = normalizedPlan.thinkingEvents.length
|
||||
? normalizedPlan.thinkingEvents
|
||||
: previousThinkingEvents.map((item) => ({ ...item, status: 'completed' }))
|
||||
? mergeWorkbenchAiThinkingEvents(previousThinkingEvents, normalizedPlan.thinkingEvents)
|
||||
: completeWorkbenchAiThinkingEvents(previousThinkingEvents)
|
||||
const previousConversationId = conversationId.value
|
||||
const nextConversationId = String(normalizedPlan.conversationId || '').trim()
|
||||
if (nextConversationId) {
|
||||
@@ -374,6 +432,7 @@ export function useWorkbenchAiStewardFlow({
|
||||
buildRequiredApplicationMissingText,
|
||||
buildRequiredApplicationSelectionText,
|
||||
filterRequiredApplicationCandidates,
|
||||
resolveInlineExecutionPlan,
|
||||
requestInlineAssistantReply
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,45 @@ export function isReimbursementCreationIntent(prompt = '') {
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveInlineTravelApplicationRequest(prompt = '') {
|
||||
const sourceText = String(prompt || '').trim()
|
||||
const compact = sourceText.replace(/\s+/g, '')
|
||||
if (!compact) {
|
||||
return null
|
||||
}
|
||||
if (/标准|制度|规则|政策|口径|怎么|如何|能不能|可以吗|查一下|查询|状态|进度|多少钱|多少/.test(compact)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasTravelIntent = /出差|差旅|差旅费|交通/.test(compact)
|
||||
const hasTravelDetail = (
|
||||
/去[\u4e00-\u9fa5A-Za-z0-9]{2,24}/.test(compact) ||
|
||||
/[\u4e00-\u9fa5A-Za-z0-9]{2,24}出差/.test(compact) ||
|
||||
/地点[::]?[\u4e00-\u9fa5A-Za-z0-9]{2,24}/.test(compact) ||
|
||||
/交通(?:方式)?[::]?(火车|高铁|动车|飞机|航班|轮船|轮渡|汽车|网约车|自驾)/.test(compact) ||
|
||||
/(火车|高铁|动车|飞机|航班|轮船|轮渡|汽车|网约车|自驾)/.test(compact)
|
||||
)
|
||||
if (!hasTravelIntent || !hasTravelDetail) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: '差旅费',
|
||||
sourceText,
|
||||
autoSubmit: /直接提交|提交申请|确认提交|提交审批/.test(compact)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveInlineApplicationPreviewTextAction(text = '') {
|
||||
const normalized = String(text || '').replace(/\s+/g, '').trim()
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
if (/^(保存草稿|保存|存草稿|先保存)$/.test(normalized)) {
|
||||
if (/^(保存草稿|保存|存草稿|先保存|保存这个单据|保存这个申请|保存这个申请单|保存这张单据|保存这张申请单|保存当前单据|保存当前申请|保存上面的单据|保存刚才的草稿)$/.test(normalized)) {
|
||||
return AI_APPLICATION_ACTION_SAVE_DRAFT
|
||||
}
|
||||
if (/^(提交|提交申请|确认提交|提交审批|直接提交)$/.test(normalized)) {
|
||||
if (/^(提交|提交申请|确认提交|提交审批|直接提交|提交这个单据|提交这个申请|提交这个申请单|提交这张单据|提交这张申请单|提交当前单据|提交当前申请|提交上面的单据|提交刚才保存的草稿|提交刚才的草稿|把这个单据提交|把这个申请单提交|提交到领导审批|提交给领导审核)$/.test(normalized)) {
|
||||
return AI_APPLICATION_ACTION_SUBMIT
|
||||
}
|
||||
return ''
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
buildApplicationTemplatePreview,
|
||||
buildLocalApplicationPreview,
|
||||
buildModelRefinedApplicationPreview,
|
||||
normalizeApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import {
|
||||
@@ -187,11 +188,74 @@ export function normalizeInlineApplicationTypeLabel(expenseTypeLabel, fallback =
|
||||
return `${label}申请`
|
||||
}
|
||||
|
||||
export function buildInlineApplicationPreview(expenseTypeLabel, sourceText = '', currentUser = {}) {
|
||||
function buildOntologyTimeRangeFromCanonical(value = '') {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw) {
|
||||
return {}
|
||||
}
|
||||
const normalized = raw.replace(/\s+/g, ' ')
|
||||
const [startDate, endDate] = normalized
|
||||
.split(/\s*(?:至|到|~|--|—)\s*/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
if (/^20\d{2}-\d{1,2}-\d{1,2}$/.test(startDate || '')) {
|
||||
return {
|
||||
raw,
|
||||
start_date: startDate,
|
||||
end_date: /^20\d{2}-\d{1,2}-\d{1,2}$/.test(endDate || '') ? endDate : startDate
|
||||
}
|
||||
}
|
||||
return { raw }
|
||||
}
|
||||
|
||||
function buildEntityFromCanonicalField(type, value) {
|
||||
const normalizedValue = String(value || '').trim()
|
||||
if (!type || !normalizedValue) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
type,
|
||||
value: normalizedValue,
|
||||
normalized_value: normalizedValue
|
||||
}
|
||||
}
|
||||
|
||||
function buildApplicationOntologyFromCanonicalFields(fields = {}) {
|
||||
const source = fields && typeof fields === 'object' ? fields : {}
|
||||
const entityKeys = [
|
||||
'expense_type',
|
||||
'location',
|
||||
'reason',
|
||||
'amount',
|
||||
'transport_mode',
|
||||
'customer_name',
|
||||
'merchant_name',
|
||||
'department_name',
|
||||
'employee_name',
|
||||
'employee_no'
|
||||
]
|
||||
return {
|
||||
parse_strategy: 'llm_primary',
|
||||
time_range: buildOntologyTimeRangeFromCanonical(source.time_range),
|
||||
entities: entityKeys
|
||||
.map((key) => buildEntityFromCanonicalField(key, source[key]))
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildInlineApplicationPreview(expenseTypeLabel, sourceText = '', currentUser = {}, options = {}) {
|
||||
const rawText = String(sourceText || '').trim()
|
||||
const preview = rawText
|
||||
const basePreview = rawText
|
||||
? buildLocalApplicationPreview(rawText, currentUser)
|
||||
: buildApplicationTemplatePreview(currentUser)
|
||||
const preview = options.ontologyFields && typeof options.ontologyFields === 'object'
|
||||
? buildModelRefinedApplicationPreview(
|
||||
basePreview,
|
||||
buildApplicationOntologyFromCanonicalFields(options.ontologyFields),
|
||||
rawText,
|
||||
currentUser
|
||||
)
|
||||
: basePreview
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
return normalizeApplicationPreview({
|
||||
...normalized,
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { resolveInlineTravelApplicationRequest } from './workbenchAiApplicationGateModel.js'
|
||||
|
||||
export const WORKBENCH_AI_INTENT_SOURCE_MODEL = 'llm_function_call'
|
||||
export const WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK = 'rule_fallback'
|
||||
|
||||
export const WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW = 'build_application_preview'
|
||||
export const WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS = 'validate_required_fields'
|
||||
export const WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT = 'save_application_draft'
|
||||
export const WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK = 'run_duplicate_precheck'
|
||||
export const WORKBENCH_AI_STEP_SUBMIT_APPLICATION = 'submit_application'
|
||||
|
||||
const TRAVEL_APPLICATION_INTENT = 'create_travel_application'
|
||||
|
||||
function normalizePromptAction(prompt = '') {
|
||||
const compact = String(prompt || '').replace(/\s+/g, '')
|
||||
if (/直接提交|提交申请|确认提交|提交审批/.test(compact)) {
|
||||
return 'submit'
|
||||
}
|
||||
if (/保存草稿|保存|存草稿|先保存/.test(compact)) {
|
||||
return 'save_draft'
|
||||
}
|
||||
return 'preview'
|
||||
}
|
||||
|
||||
function normalizePlannerSource(value = '') {
|
||||
return String(value || '').trim() === WORKBENCH_AI_INTENT_SOURCE_MODEL
|
||||
? WORKBENCH_AI_INTENT_SOURCE_MODEL
|
||||
: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK
|
||||
}
|
||||
|
||||
function normalizeSlotKey(key = '') {
|
||||
const normalized = String(key || '').trim()
|
||||
if (['time_range', 'business_time', 'occurred_date', 'application_time'].includes(normalized)) {
|
||||
return 'timeRange'
|
||||
}
|
||||
if (['transport_mode', 'transportType', 'transport_type', 'trafficMode'].includes(normalized)) {
|
||||
return 'transportMode'
|
||||
}
|
||||
if (['business_reason', 'businessPurpose', 'purpose'].includes(normalized)) {
|
||||
return 'reason'
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeSlots(rawSlots = {}) {
|
||||
if (!rawSlots || typeof rawSlots !== 'object') {
|
||||
return {}
|
||||
}
|
||||
return Object.entries(rawSlots).reduce((slots, [key, value]) => {
|
||||
const normalizedKey = normalizeSlotKey(key)
|
||||
const normalizedValue = String(value || '').trim()
|
||||
if (normalizedKey && normalizedValue) {
|
||||
slots[normalizedKey] = normalizedValue
|
||||
}
|
||||
return slots
|
||||
}, {})
|
||||
}
|
||||
|
||||
const ONTOLOGY_FIELD_ALIASES = {
|
||||
business_time: 'time_range',
|
||||
occurred_date: 'time_range',
|
||||
application_time: 'time_range',
|
||||
transportType: 'transport_mode',
|
||||
transport_type: 'transport_mode',
|
||||
trafficMode: 'transport_mode',
|
||||
business_reason: 'reason',
|
||||
businessPurpose: 'reason',
|
||||
purpose: 'reason'
|
||||
}
|
||||
|
||||
const SUPPORTED_ONTOLOGY_FIELDS = new Set([
|
||||
'expense_type',
|
||||
'time_range',
|
||||
'location',
|
||||
'reason',
|
||||
'amount',
|
||||
'transport_mode',
|
||||
'attachments',
|
||||
'customer_name',
|
||||
'merchant_name',
|
||||
'department_name',
|
||||
'employee_name',
|
||||
'employee_no'
|
||||
])
|
||||
|
||||
function normalizeOntologyFields(rawFields = {}) {
|
||||
if (!rawFields || typeof rawFields !== 'object') {
|
||||
return {}
|
||||
}
|
||||
return Object.entries(rawFields).reduce((fields, [key, value]) => {
|
||||
const normalizedKey = ONTOLOGY_FIELD_ALIASES[String(key || '').trim()] || String(key || '').trim()
|
||||
const normalizedValue = String(value || '').trim()
|
||||
if (SUPPORTED_ONTOLOGY_FIELDS.has(normalizedKey) && normalizedValue) {
|
||||
fields[normalizedKey] = normalizedValue
|
||||
}
|
||||
return fields
|
||||
}, {})
|
||||
}
|
||||
|
||||
function buildApplicationSteps(requestedAction = 'preview') {
|
||||
const steps = [
|
||||
WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW,
|
||||
WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS
|
||||
]
|
||||
if (requestedAction === 'submit') {
|
||||
steps.push(
|
||||
WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK,
|
||||
WORKBENCH_AI_STEP_SUBMIT_APPLICATION
|
||||
)
|
||||
} else if (requestedAction === 'save_draft') {
|
||||
steps.push(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT)
|
||||
}
|
||||
return steps
|
||||
}
|
||||
|
||||
function normalizeServerApplicationSteps(rawSteps = []) {
|
||||
if (!Array.isArray(rawSteps)) {
|
||||
return []
|
||||
}
|
||||
const mappedSteps = rawSteps
|
||||
.map((step) => String(step?.action_type || step?.actionType || '').trim())
|
||||
.map((actionType) => {
|
||||
if (actionType === WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW) {
|
||||
return WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW
|
||||
}
|
||||
if (actionType === WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS) {
|
||||
return WORKBENCH_AI_STEP_VALIDATE_REQUIRED_FIELDS
|
||||
}
|
||||
if (actionType === WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT) {
|
||||
return WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT
|
||||
}
|
||||
if (actionType === WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK) {
|
||||
return WORKBENCH_AI_STEP_RUN_DUPLICATE_PRECHECK
|
||||
}
|
||||
if (actionType === WORKBENCH_AI_STEP_SUBMIT_APPLICATION) {
|
||||
return WORKBENCH_AI_STEP_SUBMIT_APPLICATION
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
return [...new Set(mappedSteps)]
|
||||
}
|
||||
|
||||
function findModelTravelApplicationTask(rawPlan = {}) {
|
||||
const tasks = Array.isArray(rawPlan?.tasks) ? rawPlan.tasks : []
|
||||
return tasks.find((task) => {
|
||||
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
||||
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
|
||||
return taskType === 'expense_application' || assignedAgent === 'application_assistant'
|
||||
}) || null
|
||||
}
|
||||
|
||||
function resolveCandidateFlows(rawPlan = {}) {
|
||||
const pendingFlow = rawPlan?.pending_flow_confirmation || rawPlan?.pendingFlowConfirmation || {}
|
||||
const pendingCandidates = pendingFlow?.candidate_flows || pendingFlow?.candidateFlows
|
||||
const rootCandidates = rawPlan?.candidate_flows || rawPlan?.candidateFlows
|
||||
if (Array.isArray(pendingCandidates)) {
|
||||
return pendingCandidates
|
||||
}
|
||||
return Array.isArray(rootCandidates) ? rootCandidates : []
|
||||
}
|
||||
|
||||
function findSingleApplicationCandidateFlow(rawPlan = {}) {
|
||||
const pendingFlow = rawPlan?.pending_flow_confirmation || rawPlan?.pendingFlowConfirmation || {}
|
||||
if (String(pendingFlow?.status || '').trim() !== 'pending') {
|
||||
return null
|
||||
}
|
||||
const candidateFlows = resolveCandidateFlows(rawPlan)
|
||||
if (candidateFlows.length !== 1) {
|
||||
return null
|
||||
}
|
||||
const [candidate] = candidateFlows
|
||||
const flowId = String(candidate?.flow_id || candidate?.flowId || '').trim()
|
||||
const label = String(candidate?.label || '').trim()
|
||||
if (flowId === 'travel_application' && /先发起出差申请/.test(label)) {
|
||||
return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function normalizeWorkbenchAiIntentPlan(rawPlan = {}, options = {}) {
|
||||
const prompt = String(options.prompt || rawPlan?.sourceText || rawPlan?.source_text || '').trim()
|
||||
const task = findModelTravelApplicationTask(rawPlan)
|
||||
if (!task) {
|
||||
const candidateFlow = findSingleApplicationCandidateFlow(rawPlan)
|
||||
if (!candidateFlow) {
|
||||
return null
|
||||
}
|
||||
const ontologyFields = normalizeOntologyFields(candidateFlow.ontology_fields || candidateFlow.ontologyFields)
|
||||
const requestedAction = normalizePromptAction(prompt)
|
||||
return {
|
||||
source: normalizePlannerSource(rawPlan.planning_source || rawPlan.planningSource),
|
||||
intent: TRAVEL_APPLICATION_INTENT,
|
||||
requestedAction,
|
||||
confidence: Number(candidateFlow.confidence || rawPlan.confidence || 0),
|
||||
sourceText: prompt,
|
||||
ontologyFields,
|
||||
slots: normalizeSlots(ontologyFields),
|
||||
missingFields: Array.isArray(candidateFlow.missing_fields || candidateFlow.missingFields)
|
||||
? candidateFlow.missing_fields || candidateFlow.missingFields
|
||||
: [],
|
||||
steps: buildApplicationSteps(requestedAction)
|
||||
}
|
||||
}
|
||||
|
||||
const rawOntologyFields = task.ontology_fields || task.ontologyFields || rawPlan.slots
|
||||
const ontologyFields = normalizeOntologyFields(rawOntologyFields)
|
||||
const requestedAction = String(
|
||||
task.requested_action ||
|
||||
task.requestedAction ||
|
||||
rawPlan.requested_action ||
|
||||
rawPlan.requestedAction ||
|
||||
''
|
||||
).trim() || normalizePromptAction(prompt)
|
||||
const serverSteps = normalizeServerApplicationSteps(task.action_steps || task.actionSteps)
|
||||
return {
|
||||
source: normalizePlannerSource(rawPlan.planning_source || rawPlan.planningSource),
|
||||
intent: TRAVEL_APPLICATION_INTENT,
|
||||
requestedAction,
|
||||
confidence: Number(task.confidence || rawPlan.confidence || 0),
|
||||
sourceText: prompt,
|
||||
ontologyFields,
|
||||
slots: normalizeSlots(ontologyFields),
|
||||
missingFields: Array.isArray(task.missing_fields || task.missingFields)
|
||||
? task.missing_fields || task.missingFields
|
||||
: [],
|
||||
steps: serverSteps.length ? serverSteps : buildApplicationSteps(requestedAction)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRuleFallbackWorkbenchAiIntentPlan(prompt = '') {
|
||||
const request = resolveInlineTravelApplicationRequest(prompt)
|
||||
if (!request) {
|
||||
return null
|
||||
}
|
||||
const requestedAction = request.autoSubmit ? 'submit' : normalizePromptAction(prompt)
|
||||
return {
|
||||
source: WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK,
|
||||
intent: TRAVEL_APPLICATION_INTENT,
|
||||
requestedAction,
|
||||
confidence: 0.72,
|
||||
sourceText: request.sourceText,
|
||||
ontologyFields: {},
|
||||
slots: {},
|
||||
missingFields: [],
|
||||
steps: buildApplicationSteps(requestedAction)
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRequestWorkbenchAiIntentPlan(prompt = '') {
|
||||
const compact = String(prompt || '').replace(/\s+/g, '')
|
||||
if (!compact) {
|
||||
return false
|
||||
}
|
||||
if (compact.length < 2 || /^[\d\s.,,。::;;!?!?-]+$/.test(compact)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function resolveExecutableTravelApplicationPlan(plan = null) {
|
||||
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(plan.steps) || !plan.steps.includes(WORKBENCH_AI_STEP_BUILD_APPLICATION_PREVIEW)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: '差旅费',
|
||||
sourceText: String(plan.sourceText || '').trim(),
|
||||
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
|
||||
autoSubmit: plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION),
|
||||
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export function mergeWorkbenchAiThinkingEvents(...groups) {
|
||||
const merged = []
|
||||
const indexById = new Map()
|
||||
groups.flat().filter(Boolean).forEach((event) => {
|
||||
const eventId = String(event.eventId || event.event_id || '').trim()
|
||||
if (!eventId || !indexById.has(eventId)) {
|
||||
if (eventId) {
|
||||
indexById.set(eventId, merged.length)
|
||||
}
|
||||
merged.push(event)
|
||||
return
|
||||
}
|
||||
const index = indexById.get(eventId)
|
||||
merged.splice(index, 1, {
|
||||
...merged[index],
|
||||
...event,
|
||||
eventId
|
||||
})
|
||||
})
|
||||
return merged
|
||||
}
|
||||
|
||||
export function completeWorkbenchAiThinkingEvents(events = []) {
|
||||
return events.map((event) => ({
|
||||
...event,
|
||||
status: event.status === 'failed' ? 'failed' : 'completed'
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildInitialModelPlanningThinkingEvents() {
|
||||
return [
|
||||
{
|
||||
eventId: 'model-planning-intent',
|
||||
title: '判断办理意图',
|
||||
content: '正在判断这句话是要办申请、做报销、处理附件,还是普通咨询。',
|
||||
status: 'running'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildModelPlanningProgressSchedule() {
|
||||
return [
|
||||
{
|
||||
delayMs: 900,
|
||||
event: {
|
||||
eventId: 'model-planning-slots',
|
||||
title: '抽取关键信息',
|
||||
content: '正在整理日期、地点、事由、交通方式、附件和“保存/提交”等动作线索。',
|
||||
status: 'running'
|
||||
}
|
||||
},
|
||||
{
|
||||
delayMs: 2200,
|
||||
event: {
|
||||
eventId: 'model-planning-steps',
|
||||
title: '规划执行步骤',
|
||||
content: '正在生成可执行动作序列,例如填充申请表、校验必填项、保存草稿或发起提交。',
|
||||
status: 'running'
|
||||
}
|
||||
},
|
||||
{
|
||||
delayMs: 5200,
|
||||
event: {
|
||||
eventId: 'model-planning-tools',
|
||||
title: '匹配业务工具',
|
||||
content: '正在把模型计划映射到白名单工具,避免未确认的提交或附件关联直接产生副作用。',
|
||||
status: 'running'
|
||||
}
|
||||
},
|
||||
{
|
||||
delayMs: 9000,
|
||||
event: {
|
||||
eventId: 'model-planning-fallback',
|
||||
title: '准备兜底策略',
|
||||
content: '如果模型等待过久或调用失败,会按保守规则继续识别,保证申请、报销和草稿流程还能推进。',
|
||||
status: 'running'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user