2026-06-24 21:58:46 +08:00
|
|
|
|
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'
|
|
|
|
|
|
|
2026-06-25 10:55:49 +08:00
|
|
|
|
export const WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD = 0.6
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-06-25 10:55:49 +08:00
|
|
|
|
const requestedAction = request.requestedSubmit ? 'submit' : normalizePromptAction(prompt)
|
2026-06-24 21:58:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-06-25 10:55:49 +08:00
|
|
|
|
if (!WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN.test(compact)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-06-24 21:58:46 +08:00
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 10:55:49 +08:00
|
|
|
|
const WORKBENCH_AI_BUSINESS_KEYWORD_PATTERN = (
|
|
|
|
|
|
/报销|报账|出差|差旅|申请|审批|审核|报销单|申请单|草稿|删除|提交|保存|查|看|找|列出|发起|新建|创建|驳回|退回|通过|多少|标准|制度|规则|政策/
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-06-25 10:55:49 +08:00
|
|
|
|
const requestedSubmit = plan.steps.includes(WORKBENCH_AI_STEP_SUBMIT_APPLICATION)
|
2026-06-24 21:58:46 +08:00
|
|
|
|
return {
|
|
|
|
|
|
expenseType: 'travel',
|
|
|
|
|
|
expenseTypeLabel: '差旅费',
|
|
|
|
|
|
sourceText: String(plan.sourceText || '').trim(),
|
|
|
|
|
|
ontologyFields: normalizeOntologyFields(plan.ontologyFields || {}),
|
2026-06-25 10:55:49 +08:00
|
|
|
|
autoSubmit: false,
|
|
|
|
|
|
autoSaveDraft: plan.steps.includes(WORKBENCH_AI_STEP_SAVE_APPLICATION_DRAFT),
|
|
|
|
|
|
requestedSubmit,
|
|
|
|
|
|
submitRequiresConfirmation: requestedSubmit
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-06-24 21:58:46 +08:00
|
|
|
|
}
|
2026-06-25 10:55:49 +08:00
|
|
|
|
return confidence < WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD
|
2026-06-24 21:58:46 +08:00
|
|
|
|
}
|