import ontologyBusinessContract from '../../../shared/ontology_business_contract.json' with { type: 'json' } export const ASSISTANT_SCOPE_ACTION_SWITCH = 'switch_assistant_session' export const ASSISTANT_SCOPE_ACTION_UNSUPPORTED = 'unsupported_business_intent' export const ASSISTANT_SCOPE_SESSION_APPLICATION = 'application' export const ASSISTANT_SCOPE_SESSION_EXPENSE = 'expense' export const ASSISTANT_SCOPE_SESSION_APPROVAL = 'approval' export const ASSISTANT_SCOPE_SESSION_KNOWLEDGE = 'knowledge' export const ASSISTANT_SCOPE_SESSION_STEWARD = 'steward' const FALLBACK_SESSION_SCOPE_CONFIG = { [ASSISTANT_SCOPE_SESSION_STEWARD]: { label: '小财管家', icon: 'mdi mdi-account-tie-outline', scope: '多任务拆解、附件归集、申请助手和报销助手统一调度' }, [ASSISTANT_SCOPE_SESSION_APPLICATION]: { label: '申请助手', icon: 'mdi mdi-file-plus-outline', scope: '费用申请、事前审批、申请材料清单、申请单状态查询' }, [ASSISTANT_SCOPE_SESSION_EXPENSE]: { label: '报销助手', icon: 'mdi mdi-receipt-text-plus-outline', scope: '发起报销、票据识别、草稿归集、报销单状态查询和报销信息核对' }, [ASSISTANT_SCOPE_SESSION_APPROVAL]: { label: '审核助手', icon: 'mdi mdi-clipboard-check-outline', scope: '待审单据查询、审批动作、风险解释和审核意见草稿' }, [ASSISTANT_SCOPE_SESSION_KNOWLEDGE]: { label: '财务知识助手', icon: 'mdi mdi-book-open-page-variant-outline', scope: '财务制度、报销标准、票据要求、流程规则和政策口径解释' } } const ONTOLOGY_BUSINESS_CONTRACT = ontologyBusinessContract || {} const SESSION_SCOPE_CONFIG = ONTOLOGY_BUSINESS_CONTRACT.sessions || FALLBACK_SESSION_SCOPE_CONFIG const SESSION_SCOPE_TYPES = Object.keys(SESSION_SCOPE_CONFIG) const BUSINESS_SIGNAL_GROUPS = ONTOLOGY_BUSINESS_CONTRACT.businessSignals || {} const APPLICATION_PATTERN = /费用申请|发起申请|申请单|事前申请|事前审批|前置审批|出差申请|申请出差|差旅申请|申请差旅|采购申请|用款申请|预算申请|申请材料|材料清单|先申请|立项申请/ const APPLICATION_PLANNING_PATTERN = /计划|安排|准备|需要|打算|预计|申请|发起|提交|提出|先走|先办|要去|将要|下周|下月|明天|后天|近期|月底|去|到|赴|前往|参加/ const APPLICATION_BUSINESS_PATTERN = /出差|差旅|客户现场|现场|客户|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|会务|驻场|上线|验收|采购|购置|用款|立项/ const APPLICATION_FUTURE_OR_DURATION_PATTERN = /明天|后天|下周|下月|近期|月底|预计|计划|安排|准备|将要|[0-9]+天|[一二两三四五六七八九十]+天/ const APPLICATION_ROUTE_PATTERN = /(?:去|到|赴|前往)[^,,。;;!??!\n]{0,24}(?:出差|差旅|客户|现场|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|驻场|上线|验收)|(?:出差|差旅)[^,,。;;!??!\n]{0,24}(?:[0-9]+天|[一二两三四五六七八九十]+天|客户|现场|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|驻场|上线|验收)/ const COMPLETED_EXPENSE_PATTERN = /已经|已|昨天|前天|上周|上月|去年|花了|花销|消费|垫付|支付|付了|买了|采购了|招待了|发生了/ const EXPENSE_PATTERN = /报销|报销单|票据|发票|火车票|高铁票|机票|飞机票|的士票|出租车|网约车|酒店票|住宿票|住宿单据|保存草稿|草稿|费用明细|归集|上传.*票|关联单据|继续下一步/ const APPROVAL_PATTERN = /待我审核|待审|审核|审批|审核意见|审批意见|审批通过|审批驳回|驳回|退回|审核中心|审批中心|领导审批|财务审核|处理意见/ const KNOWLEDGE_PATTERN = /制度|政策|标准|规则|规定|流程|口径|依据|上限|额度|补贴|住宿标准|差旅标准|报销标准|票据要求|可不可以|能不能|怎么规定|如何计算|怎么算/ const EXPENSE_OPERATION_PATTERN = /发起报销|报销单|票据|发票|火车票|高铁票|机票|的士票|草稿|归集|上传|关联单据|继续下一步/ const CURRENT_CLAIM_RISK_PATTERN = /这张|当前|本单|该单|单据|风险|超标|异常|重复|待补/ const FINANCE_OPERATING_PATTERN = buildKeywordPattern([ ...(BUSINESS_SIGNAL_GROUPS.budget || []), ...(BUSINESS_SIGNAL_GROUPS.accounts_receivable || []), ...(BUSINESS_SIGNAL_GROUPS.accounts_payable || []) ]) const CONTEXTUAL_FOLLOW_UP_PATTERN = buildExactKeywordPattern(ONTOLOGY_BUSINESS_CONTRACT.contextualFollowUps || []) export const SUPPORTED_BUSINESS_SCOPE_TEXT = Array.isArray(ONTOLOGY_BUSINESS_CONTRACT.supportedBusinessScopes) ? ONTOLOGY_BUSINESS_CONTRACT.supportedBusinessScopes : [ '费用申请/事前审批', '报销与票据识别', '审批审核与风险解释', '财务制度、报销标准和流程规则问答', '预算、应收、应付等财务经营查询', '小财管家多任务拆解和附件归集' ] function escapeRegExp(value) { return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function buildKeywordPattern(keywords = []) { const source = keywords .map((keyword) => String(keyword || '').trim()) .filter(Boolean) .map(escapeRegExp) .join('|') return source ? new RegExp(source) : /$a/ } function buildExactKeywordPattern(keywords = []) { const source = keywords .map((keyword) => String(keyword || '').trim()) .filter(Boolean) .map(escapeRegExp) .join('|') return source ? new RegExp(`^(${source})$`) : /$a/ } function normalizeSessionType(sessionType) { const normalized = String(sessionType || '').trim() return SESSION_SCOPE_TYPES.includes(normalized) ? normalized : ASSISTANT_SCOPE_SESSION_EXPENSE } function normalizeText(rawText) { return String(rawText || '') .replace(/\s+/g, '') .toLowerCase() } export function hasReimbursementIntentSignal(rawText) { return EXPENSE_PATTERN.test(normalizeText(rawText)) } export function hasExpenseApplicationIntentSignal(rawText) { const text = normalizeText(rawText) if (!text) { return false } if (APPLICATION_PATTERN.test(text)) { return true } if (hasReimbursementIntentSignal(text) || COMPLETED_EXPENSE_PATTERN.test(text)) { return false } if (KNOWLEDGE_PATTERN.test(text) && !EXPENSE_OPERATION_PATTERN.test(text)) { return false } const hasBusinessSignal = APPLICATION_BUSINESS_PATTERN.test(text) const planningScore = APPLICATION_PLANNING_PATTERN.test(text) ? 1 : 0 const timingScore = APPLICATION_FUTURE_OR_DURATION_PATTERN.test(text) ? 1 : 0 const routeScore = APPLICATION_ROUTE_PATTERN.test(text) ? 2 : 0 return hasBusinessSignal && planningScore + timingScore + routeScore >= 2 } function resolveScopeConfig(sessionType) { return SESSION_SCOPE_CONFIG[normalizeSessionType(sessionType)] || SESSION_SCOPE_CONFIG[ASSISTANT_SCOPE_SESSION_EXPENSE] } export function inferAssistantScopeTarget(rawText, options = {}) { const text = normalizeText(rawText) if (!text) { return '' } const applicationMatched = hasExpenseApplicationIntentSignal(text) const expenseMatched = EXPENSE_PATTERN.test(text) const approvalMatched = APPROVAL_PATTERN.test(text) const knowledgeMatched = KNOWLEDGE_PATTERN.test(text) if (applicationMatched && expenseMatched) { return ASSISTANT_SCOPE_SESSION_STEWARD } if (approvalMatched && /(待我审核|待审|审核意见|审批意见|审批通过|审批驳回|驳回|退回|审核中心|审批中心|处理意见)/.test(text)) { return ASSISTANT_SCOPE_SESSION_APPROVAL } if (knowledgeMatched && !options.hasActiveReviewPayload && !EXPENSE_OPERATION_PATTERN.test(text)) { return ASSISTANT_SCOPE_SESSION_KNOWLEDGE } if (expenseMatched && !applicationMatched) { return ASSISTANT_SCOPE_SESSION_EXPENSE } if (applicationMatched && !expenseMatched) { return ASSISTANT_SCOPE_SESSION_APPLICATION } if (FINANCE_OPERATING_PATTERN.test(text)) { return ASSISTANT_SCOPE_SESSION_STEWARD } if (knowledgeMatched && !expenseMatched && !approvalMatched && !applicationMatched) { return ASSISTANT_SCOPE_SESSION_KNOWLEDGE } if (knowledgeMatched && !options.hasActiveReviewPayload) { return ASSISTANT_SCOPE_SESSION_KNOWLEDGE } if (approvalMatched) { return ASSISTANT_SCOPE_SESSION_APPROVAL } if (expenseMatched) { return ASSISTANT_SCOPE_SESSION_EXPENSE } if (applicationMatched) { return ASSISTANT_SCOPE_SESSION_APPLICATION } return '' } function shouldAllowCurrentExpensePolicyQuestion(rawText, currentSessionType, targetSessionType, options = {}) { if ( normalizeSessionType(currentSessionType) !== ASSISTANT_SCOPE_SESSION_EXPENSE || targetSessionType !== ASSISTANT_SCOPE_SESSION_KNOWLEDGE || !options.hasActiveReviewPayload ) { return false } return CURRENT_CLAIM_RISK_PATTERN.test(normalizeText(rawText)) } function buildScopeSwitchAction(targetSessionType, rawText, options = {}) { const target = resolveScopeConfig(targetSessionType) const carryText = String(rawText || '').trim() return { label: `切换到${target.label}`, description: `带着这条内容进入${target.label}继续处理`, icon: target.icon, action_type: ASSISTANT_SCOPE_ACTION_SWITCH, payload: { session_type: normalizeSessionType(targetSessionType), carry_text: carryText, carry_files: Boolean(options.attachmentCount) } } } function buildScopeBoundaryText(currentSessionType, targetSessionType) { const current = resolveScopeConfig(currentSessionType) const target = resolveScopeConfig(targetSessionType) return [ `我先暂停在「${current.label}」里继续处理这条消息。`, '', `当前助手的业务范围是:${current.scope}。`, '', `您这条内容更适合交给「${target.label}」处理;它的业务范围是:${target.scope}。`, '', `建议切换到「${target.label}」后继续,我会尽量把这条内容带过去,避免在错误的会话里把流程跑偏。` ].join('\n') } function shouldAllowContextualFollowUp(rawText, currentSessionType, options = {}) { const text = normalizeText(rawText) if (options.hasActiveReviewPayload && CURRENT_CLAIM_RISK_PATTERN.test(text)) { return true } if (!text || !CONTEXTUAL_FOLLOW_UP_PATTERN.test(text)) { return false } return Boolean( options.hasActiveReviewPayload || options.hasPendingApplicationPreview || options.reviewAction || currentSessionType ) } function buildUnsupportedBusinessScopeText() { const message = ONTOLOGY_BUSINESS_CONTRACT.unsupportedIntentMessage || {} return [ message.title || '此意图系统不支持。', '', `当前系统支持的业务范围:${SUPPORTED_BUSINESS_SCOPE_TEXT.join('、')}。`, '', message.body || '你这条内容没有识别到相关财务业务意图,系统暂不支持处理。', '', message.retryHint || '请重新描述你的财务业务要求,例如“申请下周去上海出差”“查询我的报销单进度”或“解释差旅住宿标准”。' ].join('\n') } function buildUnsupportedBusinessScopeGuard() { return { targetSessionType: '', targetLabel: '不支持的意图', blocked: true, text: buildUnsupportedBusinessScopeText(), meta: ['意图不支持'], suggestedActions: [], actionType: ASSISTANT_SCOPE_ACTION_UNSUPPORTED } } export function resolveAssistantScopeGuard(rawText, currentSessionType, options = {}) { const normalizedCurrent = normalizeSessionType(currentSessionType) const targetSessionType = inferAssistantScopeTarget(rawText, options) if (!targetSessionType) { if (shouldAllowContextualFollowUp(rawText, normalizedCurrent, options)) { return null } return normalizeText(rawText) ? buildUnsupportedBusinessScopeGuard() : null } if (targetSessionType === normalizedCurrent) { return null } if (shouldAllowCurrentExpensePolicyQuestion(rawText, normalizedCurrent, targetSessionType, options)) { return null } const target = resolveScopeConfig(targetSessionType) return { targetSessionType, targetLabel: target.label, text: buildScopeBoundaryText(normalizedCurrent, targetSessionType), meta: [`建议切换至${target.label}`], suggestedActions: [buildScopeSwitchAction(targetSessionType, rawText, options)] } }