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
|
||||
|
||||
Reference in New Issue
Block a user