Files
X-Financial/web/src/composables/workbenchAiMode/workbenchAiIntentPlannerModel.js
caoxiaozhu c4b5fcc067 feat(web): AI 工作台多 task 串行推进与会话适配
- useWorkbenchAiApplicationPreviewFlow/useWorkbenchAiActionRouter/useWorkbenchAiCommandIntents 支持 task1 完成后自动推进 task2,确认按钮直接拉起申请预览,草稿/提交成功后继续推进下一 task
- workbenchAiIntentPlannerModel/workbenchAiMessageModel/workbenchAiCommandIntentModel 适配多 task 意图规划与消息结构
- aiApplicationPreviewActions/aiApplicationPrecheckModel/aiExpenseDraftModel/aiWorkbenchConversationStore 草稿与会话存储适配
- PersonalWorkbenchAiMode 与样式适配,更新 preview-actions/expense-draft/conversation-store/fast-preview/action-router/command-intent/intent-planner 测试
2026-06-26 22:42:23 +08:00

336 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_INTENT_CONFIDENCE_THRESHOLD = 0.6
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 resolveModelTasks(rawPlan = {}) {
return Array.isArray(rawPlan?.tasks) ? rawPlan.tasks : []
}
function isModelTravelApplicationTask(task = {}) {
if (!task || typeof task !== 'object') {
return false
}
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'
}
function findModelTravelApplicationTask(rawPlan = {}) {
return resolveModelTasks(rawPlan).find(isModelTravelApplicationTask) || null
}
function resolveModelRemainingTasks(rawPlan = {}, selectedTask = null) {
const tasks = resolveModelTasks(rawPlan)
const selectedIndex = tasks.findIndex((task) => task === selectedTask)
if (selectedIndex < 0) {
return []
}
return tasks.slice(selectedIndex + 1).filter((task) => {
if (!task || typeof task !== 'object') {
return false
}
const taskType = String(task?.task_type || task?.taskType || '').trim()
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
return Boolean(taskType || assignedAgent)
})
}
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),
stewardRemainingTasks: resolveModelRemainingTasks(rawPlan, task)
}
}
export function buildRuleFallbackWorkbenchAiIntentPlan(prompt = '') {
const request = resolveInlineTravelApplicationRequest(prompt)
if (!request) {
return null
}
const requestedAction = request.requestedSubmit ? '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
}
if (!WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN.test(compact)) {
return false
}
return true
}
const WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN = (
/报销|报账|出差|差旅|申请|审批|审核|报销单|申请单|草稿|删除|提交|保存|查|看|找|列出|发起|新建|创建|驳回|退回|通过|多少|标准|制度|规则|政策/
)
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
}
const requestedSubmit = plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION)
const request = {
expenseType: 'travel',
expenseTypeLabel: '差旅费',
sourceText: String(plan.sourceText || '').trim(),
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
autoSubmit: false,
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT),
requestedSubmit,
submitRequiresConfirmation: requestedSubmit
}
const stewardRemainingTasks = Array.isArray(plan.stewardRemainingTasks) ? plan.stewardRemainingTasks : []
if (stewardRemainingTasks.length) {
request.stewardRemainingTasks = stewardRemainingTasks
}
return request
}
export function isLowConfidenceTravelApplicationPlan(plan = null) {
if (!plan || plan.intent !== TRAVEL_APPLICATION_INTENT) {
return false
}
if (plan.source === WORKBENCH_AI_INTENT_SOURCE_RULE_FALLBACK) {
return false
}
if (plan.requestedAction === 'submit' || plan.requestedAction === 'save_draft') {
return false
}
const confidence = Number(plan.confidence)
if (!Number.isFinite(confidence)) {
return false
}
return confidence < WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD
}