Files
X-Financial/web/src/views/scripts/travelReimbursementGuidedFlowModel.js
caoxiaozhu 34457f9c3e feat: 本体字段治理与风险规则模板执行器重构
- 新增本体字段注册表与字段治理审计脚本
- 重构风险规则模板执行器、DSL 验证与清单分类器
- 完善票据夹服务与差旅请求详情页交互
- 优化趋势图表与总览页数据展示
- 增强报销平台风险分级与模拟公司筛选
- 补充本体字段、风险规则生成与票据夹服务测试覆盖
2026-06-03 15:46:56 +08:00

644 lines
28 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.
export const GUIDED_FLOW_MODE_NONE = ''
export const GUIDED_FLOW_MODE_REIMBURSEMENT = 'reimbursement_guide'
export const GUIDED_FLOW_MODE_STATUS_QUERY = 'status_query_guide'
export const GUIDED_ACTION_START_REIMBURSEMENT = 'start_guided_reimbursement'
export const GUIDED_ACTION_START_APPLICATION = 'start_guided_application'
export const GUIDED_ACTION_START_STATUS_QUERY = 'start_guided_status_query'
export const GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR = 'open_travel_calculator'
export const GUIDED_ACTION_SELECT_EXPENSE_TYPE = 'guided_select_expense_type'
export const GUIDED_ACTION_SELECT_REQUIRED_APPLICATION = 'guided_select_required_application'
export const GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW = 'guided_confirm_reimbursement_review'
export const GUIDED_ACTION_CONTINUE_FILLING = 'guided_continue_filling'
export const GUIDED_ACTION_PROCESS_INTERRUPTION = 'guided_process_interruption'
export const GUIDED_ACTION_SELECT_QUERY_MODE = 'guided_select_query_mode'
export const GUIDED_ACTION_SELECT_QUERY_STATUS = 'guided_select_query_status'
export const GUIDED_EXPENSE_TYPES = [
{ key: 'travel', label: '差旅费', description: '出差、跨城交通、住宿和补贴', icon: 'mdi mdi-bag-suitcase-outline' },
{ key: 'transport', label: '交通费', description: '市内交通、打车、停车和通行费', icon: 'mdi mdi-car-outline' },
{ key: 'hotel', label: '住宿费', description: '单独住宿或酒店票据', icon: 'mdi mdi-bed-outline' },
{ key: 'meal', label: '业务招待费', description: '客户接待、餐饮和工作招待', icon: 'mdi mdi-food-fork-drink' },
{ key: 'office', label: '办公用品费', description: '办公用品、文具和低值易耗品', icon: 'mdi mdi-briefcase-outline' },
{ key: 'other', label: '其他费用', description: '暂不属于以上类型的费用', icon: 'mdi mdi-dots-horizontal-circle-outline' }
]
const GUIDED_REIMBURSEMENT_STEPS = {
travel: [
{ key: 'reason', summaryLabel: '事由', prompt: '请先告诉我本次出差事由,例如:去上海支持项目部署。' },
{ key: 'location', summaryLabel: '出差地点', prompt: '本次出差地点是哪里?可以回复城市或具体客户地点。' },
{ key: 'time_range', summaryLabel: '出差时间/天数', prompt: '请补充出差时间或天数例如2026-05-20 至 2026-05-23出差 3 天。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充本次预计或实际报销金额。如果还没有汇总,可以回复“待核算”。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '票据可以现在上传,也可以回复“稍后上传”。上传后我会在生成核对信息时一起处理。' }
],
transport: [
{ key: 'reason', summaryLabel: '出行事由', prompt: '请说明本次交通费事由,例如:送客户去机场。' },
{ key: 'time_range', summaryLabel: '出行时间', prompt: '请补充出行时间例如2026-05-20 下午。' },
{ key: 'location', summaryLabel: '路线/地点', prompt: '请补充出行路线或地点,例如:公司至机场。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充交通费金额。如果票据里再识别金额,可以回复“以票据为准”。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '请上传出租车、网约车、停车或通行费等票据;也可以回复“稍后上传”。' }
],
hotel: [
{ key: 'reason', summaryLabel: '住宿事由', prompt: '请说明住宿事由,例如:项目现场支持期间住宿。' },
{ key: 'location', summaryLabel: '城市/酒店地点', prompt: '住宿城市或酒店地点是哪里?' },
{ key: 'time_range', summaryLabel: '入住离店时间', prompt: '请补充入住和离店时间例如2026-05-20 至 2026-05-23。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充住宿金额。如果还没有汇总,可以回复“待核算”。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '请上传酒店发票或住宿水单;也可以回复“稍后上传”。' }
],
meal: [
{ key: 'customer_name', summaryLabel: '客户单位', prompt: '请补充客户单位或接待对象。' },
{ key: 'participants', summaryLabel: '参与人员', prompt: '请补充参与人员,例如:客户 2 人,我方 1 人。' },
{ key: 'time_range', summaryLabel: '招待时间', prompt: '请补充招待时间例如2026-05-20 晚。' },
{ key: 'location', summaryLabel: '招待地点', prompt: '请补充招待地点或商户名称。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充招待金额。' },
{ key: 'reason', summaryLabel: '事由', prompt: '请补充招待事由,例如:项目沟通或客户接待。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '请上传餐饮发票或相关凭证;也可以回复“稍后上传”。' }
],
office: [
{ key: 'reason', summaryLabel: '采购用途', prompt: '请说明采购用途,例如:项目现场临时采购办公用品。' },
{ key: 'location', summaryLabel: '商户/采购地点', prompt: '请补充商户或采购地点。' },
{ key: 'time_range', summaryLabel: '发生时间', prompt: '请补充费用发生时间。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充办公用品金额。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '请上传办公用品发票或购物凭证;也可以回复“稍后上传”。' }
],
other: [
{ key: 'reason', summaryLabel: '费用说明', prompt: '请说明这笔费用的具体内容和用途。' },
{ key: 'time_range', summaryLabel: '发生时间', prompt: '请补充费用发生时间。' },
{ key: 'location', summaryLabel: '地点/对象', prompt: '请补充费用发生地点或关联对象。' },
{ key: 'amount', summaryLabel: '金额', prompt: '请补充费用金额。' },
{ key: 'attachments', summaryLabel: '票据', prompt: '请上传相关票据;也可以回复“稍后上传”。' }
]
}
export const GUIDED_QUERY_MODES = [
{ key: 'claim_no', label: '按单号', description: '输入报销单号精准查询', icon: 'mdi mdi-pound' },
{ key: 'status', label: '按状态', description: '查询草稿、审批中或已归档单据', icon: 'mdi mdi-list-status' },
{ key: 'time_range', label: '按时间范围', description: '例如上周、去年、2026-05', icon: 'mdi mdi-calendar-search-outline' },
{ key: 'keyword', label: '按地点/事由', description: '例如北京、上海电力、服务器部署', icon: 'mdi mdi-map-search-outline' }
]
export const GUIDED_QUERY_STATUS_OPTIONS = [
{ key: 'draft', label: '草稿', description: '还没有正式提交的单据' },
{ key: 'pending', label: '审批中', description: '正在流转审批的单据' },
{ key: 'returned', label: '已退回', description: '需要补充或修改的单据' },
{ key: 'archived', label: '已归档', description: '已完成归档的单据' },
{ key: 'completed', label: '已完成', description: '已审核完成或已付款的单据' }
]
const NO_ATTACHMENT_TEXT_PATTERN = /^(稍后|暂不|不用|没有|待上传|后面|后续|先不|以票据为准)/u
const INTERRUPTION_PATTERN = /(查一下|查询|状态|报销了吗|报销了么|多少|总额|标准|制度|规则|为什么|怎么|可以吗|能不能|差旅计算器|计算一下|解释|风险|打开|跳转|查看|审批|归档|入账|[?])/u
function uniqueValues(values) {
return Array.from(new Set((Array.isArray(values) ? values : []).map((item) => String(item || '').trim()).filter(Boolean)))
}
function normalizeText(value) {
return String(value || '').trim()
}
function normalizeValues(values) {
if (!values || typeof values !== 'object') {
return {}
}
return Object.entries(values).reduce((result, [key, value]) => {
if (key === 'attachment_names') {
result[key] = uniqueValues(value)
return result
}
result[key] = normalizeText(value)
return result
}, {})
}
function hasLinkedApplication(values) {
return Boolean(normalizeText(values?.application_claim_id) || normalizeText(values?.application_claim_no))
}
function buildApplicationSummaryParts(values) {
return [
normalizeText(values?.application_claim_no),
normalizeText(values?.application_reason),
normalizeText(values?.application_business_time),
normalizeText(values?.application_location),
normalizeText(values?.application_amount_label || values?.application_amount)
].filter(Boolean)
}
function normalizeApplicationCandidates(applications) {
if (!Array.isArray(applications)) {
return []
}
return applications
.map((item) => (item && typeof item === 'object' ? item : null))
.filter(Boolean)
.map((item) => ({
id: normalizeText(item.id || item.application_claim_id),
claim_no: normalizeText(item.claim_no || item.application_claim_no),
expense_type: normalizeText(item.expense_type || item.application_expense_type),
reason: normalizeText(item.reason || item.application_reason),
location: normalizeText(item.location || item.application_location),
amount: normalizeText(item.amount || item.application_amount),
amount_label: normalizeText(item.amount_label || item.application_amount_label),
business_time: normalizeText(item.business_time || item.application_business_time),
days: normalizeText(item.days || item.application_days),
transport_mode: normalizeText(item.transport_mode || item.application_transport_mode),
lodging_daily_cap: normalizeText(item.lodging_daily_cap || item.application_lodging_daily_cap),
subsidy_daily_cap: normalizeText(item.subsidy_daily_cap || item.application_subsidy_daily_cap),
transport_policy: normalizeText(item.transport_policy || item.application_transport_policy),
policy_estimate: normalizeText(item.policy_estimate || item.application_policy_estimate),
rule_name: normalizeText(item.rule_name || item.application_rule_name),
rule_version: normalizeText(item.rule_version || item.application_rule_version),
status: normalizeText(item.status || item.application_status),
status_label: normalizeText(item.status_label || item.application_status_label),
application_date: normalizeText(item.application_date)
}))
.filter((item) => item.id || item.claim_no)
}
export function createEmptyGuidedFlowState() {
return {
mode: GUIDED_FLOW_MODE_NONE,
stepKey: '',
expenseType: '',
values: {},
pendingInterruptionText: '',
applicationCandidates: []
}
}
export function normalizeGuidedFlowState(state) {
const source = state && typeof state === 'object' ? state : {}
const mode = normalizeText(source.mode)
const supportedMode = [GUIDED_FLOW_MODE_REIMBURSEMENT, GUIDED_FLOW_MODE_STATUS_QUERY].includes(mode)
? mode
: GUIDED_FLOW_MODE_NONE
if (!supportedMode) {
return createEmptyGuidedFlowState()
}
return {
mode: supportedMode,
stepKey: normalizeText(source.stepKey),
expenseType: normalizeText(source.expenseType),
values: normalizeValues(source.values),
pendingInterruptionText: normalizeText(source.pendingInterruptionText),
applicationCandidates: normalizeApplicationCandidates(source.applicationCandidates)
}
}
export function isGuidedFlowActive(state) {
return Boolean(normalizeGuidedFlowState(state).mode)
}
export function getGuidedExpenseType(expenseType) {
const key = normalizeText(expenseType)
return GUIDED_EXPENSE_TYPES.find((item) => item.key === key) || null
}
export function getGuidedExpenseTypeLabel(expenseType) {
return getGuidedExpenseType(expenseType)?.label || ''
}
export function buildGuidedExpenseTypeActions() {
return GUIDED_EXPENSE_TYPES.map((option) => ({
label: option.label,
description: option.description,
icon: option.icon,
action_type: GUIDED_ACTION_SELECT_EXPENSE_TYPE,
payload: {
expense_type: option.key,
expense_type_label: option.label
}
}))
}
export function buildGuidedReimbursementStartText() {
return [
'请问你要报销的类型?',
'',
'先选一个最贴近的费用场景,我会按对应流程逐项询问。这个过程只做本地引导,不会自动创建草稿。'
].join('\n')
}
export function createGuidedReimbursementState() {
return {
...createEmptyGuidedFlowState(),
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
stepKey: 'expense_type'
}
}
export function selectGuidedExpenseType(state, expenseType) {
const type = getGuidedExpenseType(expenseType)
if (!type) {
return normalizeGuidedFlowState(state)
}
const steps = GUIDED_REIMBURSEMENT_STEPS[type.key] || []
return {
...normalizeGuidedFlowState(state),
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
expenseType: type.key,
stepKey: steps[0]?.key || 'summary',
pendingInterruptionText: '',
applicationCandidates: []
}
}
export function waitForGuidedApplicationSelection(state, expenseType, applications = []) {
const type = getGuidedExpenseType(expenseType)
if (!type) {
return normalizeGuidedFlowState(state)
}
return {
...normalizeGuidedFlowState(state),
mode: GUIDED_FLOW_MODE_REIMBURSEMENT,
expenseType: type.key,
stepKey: 'application_selection',
pendingInterruptionText: '',
applicationCandidates: normalizeApplicationCandidates(applications)
}
}
export function selectGuidedRequiredApplication(state, application = {}) {
const current = normalizeGuidedFlowState(state)
return {
...current,
values: normalizeValues({
...current.values,
application_claim_id: application.application_claim_id || application.id || '',
application_claim_no: application.application_claim_no || application.claim_no || '',
application_reason: application.application_reason || application.reason || '',
application_location: application.application_location || application.location || '',
application_amount: application.application_amount || application.amount || '',
application_amount_label: application.application_amount_label || application.amount_label || '',
application_business_time: application.application_business_time || application.business_time || '',
application_days: application.application_days || application.days || '',
application_transport_mode: application.application_transport_mode || application.transport_mode || '',
application_lodging_daily_cap: application.application_lodging_daily_cap || application.lodging_daily_cap || '',
application_subsidy_daily_cap: application.application_subsidy_daily_cap || application.subsidy_daily_cap || '',
application_transport_policy: application.application_transport_policy || application.transport_policy || '',
application_policy_estimate: application.application_policy_estimate || application.policy_estimate || '',
application_rule_name: application.application_rule_name || application.rule_name || '',
application_rule_version: application.application_rule_version || application.rule_version || '',
application_status_label: application.application_status_label || application.status_label || '',
application_date: application.application_date || ''
}),
stepKey: 'summary',
pendingInterruptionText: '',
applicationCandidates: []
}
}
export function getGuidedReimbursementSteps(expenseType) {
const key = normalizeText(expenseType)
return GUIDED_REIMBURSEMENT_STEPS[key] || []
}
export function getCurrentGuidedStep(state) {
const current = normalizeGuidedFlowState(state)
if (current.mode !== GUIDED_FLOW_MODE_REIMBURSEMENT || !current.expenseType) {
return null
}
return getGuidedReimbursementSteps(current.expenseType).find((step) => step.key === current.stepKey) || null
}
export function buildGuidedStepPromptText(state) {
const current = normalizeGuidedFlowState(state)
const step = getCurrentGuidedStep(current)
const typeLabel = getGuidedExpenseTypeLabel(current.expenseType)
if (!step || !typeLabel) {
return buildGuidedReimbursementStartText()
}
const steps = getGuidedReimbursementSteps(current.expenseType)
const stepIndex = Math.max(0, steps.findIndex((item) => item.key === step.key))
return [
`已选择“${typeLabel}”。`,
'',
`${stepIndex + 1} 步:${step.summaryLabel}`,
step.prompt,
'',
'直接回复这一项即可。'
].join('\n')
}
export function resolveGuidedExpenseTypeFromText(text) {
const normalized = normalizeText(text)
if (!normalized) {
return ''
}
const exact = GUIDED_EXPENSE_TYPES.find((item) => normalized === item.label || normalized === item.key)
if (exact) {
return exact.key
}
const matched = GUIDED_EXPENSE_TYPES.find((item) => normalized.includes(item.label))
return matched?.key || ''
}
export function applyGuidedReimbursementAnswer(state, answerText, attachmentNames = []) {
const current = normalizeGuidedFlowState(state)
const step = getCurrentGuidedStep(current)
if (!step) {
return current
}
const answer = normalizeText(answerText)
const nextValues = { ...current.values }
if (step.key === 'attachments') {
const nextAttachmentNames = uniqueValues([
...(Array.isArray(nextValues.attachment_names) ? nextValues.attachment_names : []),
...attachmentNames
])
if (nextAttachmentNames.length) {
nextValues.attachment_names = nextAttachmentNames
}
nextValues.attachments = answer || (nextAttachmentNames.length ? `已选择 ${nextAttachmentNames.length} 份附件` : '稍后上传')
} else {
nextValues[step.key] = answer
}
const steps = getGuidedReimbursementSteps(current.expenseType)
const currentIndex = steps.findIndex((item) => item.key === step.key)
const nextStep = steps[currentIndex + 1]
return {
...current,
values: normalizeValues(nextValues),
stepKey: nextStep?.key || 'summary',
pendingInterruptionText: ''
}
}
export function isGuidedReimbursementReadyForReview(state) {
const current = normalizeGuidedFlowState(state)
return current.mode === GUIDED_FLOW_MODE_REIMBURSEMENT
&& Boolean(current.expenseType)
&& current.stepKey === 'summary'
}
export function buildGuidedReimbursementSummaryText(state) {
const current = normalizeGuidedFlowState(state)
const typeLabel = getGuidedExpenseTypeLabel(current.expenseType) || '报销'
const steps = getGuidedReimbursementSteps(current.expenseType)
const linkedApplication = hasLinkedApplication(current.values)
const lines = [
`已完成“${typeLabel}”的引导填写。`,
'',
'请核查下面的关键信息:'
]
if (linkedApplication) {
const applicationParts = buildApplicationSummaryParts(current.values)
lines.push(`- 关联申请单:${applicationParts.join(' / ')}`)
lines.push('- 报销票据:可先生成草稿,随后在草稿详情中上传对应票据。')
} else {
steps.forEach((step) => {
const value = step.key === 'attachments'
? (current.values.attachment_names?.length
? current.values.attachment_names.join('、')
: current.values.attachments || '稍后上传')
: current.values[step.key]
lines.push(`- ${step.summaryLabel}${value || '待补充'}`)
})
}
lines.push('')
lines.push(
linkedApplication
? '如果关联信息无误,我可以直接生成报销草稿;后续由你在草稿详情中上传和归集票据。'
: '如果这些信息无误,我可以继续生成报销草稿;草稿生成后可继续上传票据或补充信息。'
)
return lines.join('\n')
}
export function buildGuidedReviewConfirmationActions() {
return [{
label: '生成报销草稿',
description: '使用当前信息生成草稿,票据可在草稿详情继续上传',
icon: 'mdi mdi-clipboard-check-outline',
action_type: GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW
}]
}
export function buildGuidedReviewSubmitOptions(state, files = []) {
const current = normalizeGuidedFlowState(state)
const type = getGuidedExpenseType(current.expenseType)
const values = current.values || {}
const typeLabel = type?.label || '其他费用'
const linkedApplication = hasLinkedApplication(values)
const applicationReason = values.application_reason || ''
const applicationLocation = values.application_location || ''
const applicationAmount = values.application_amount || values.application_amount_label || ''
const applicationBusinessTime = values.application_business_time || ''
const applicationTransportMode = values.application_transport_mode || ''
const fieldLines = []
if (linkedApplication) {
const applicationParts = buildApplicationSummaryParts(values)
fieldLines.push(`关联申请单:${applicationParts.join(' / ')}`)
fieldLines.push('报销票据:草稿生成后在详情中上传')
} else {
getGuidedReimbursementSteps(current.expenseType).forEach((step) => {
const value = step.key === 'attachments'
? (values.attachment_names?.length ? values.attachment_names.join('、') : values.attachments || '稍后上传')
: values[step.key]
fieldLines.push(`${step.summaryLabel}${value || '待补充'}`)
})
}
const rawText = [
`报销类型:${typeLabel}`,
...fieldLines
].join('\n')
const reviewFormValues = {
expense_type: typeLabel,
reason: values.reason || applicationReason || values.customer_name || '',
customer_name: values.customer_name || '',
participants: values.participants || '',
location: values.location || applicationLocation || '',
time_range: values.time_range || applicationBusinessTime || '',
transport_mode: values.transport_mode || applicationTransportMode || '',
amount: linkedApplication ? (values.amount || '') : (values.amount || applicationAmount || ''),
attachments: Array.isArray(values.attachment_names) ? values.attachment_names.join('、') : '',
application_claim_id: values.application_claim_id || '',
application_claim_no: values.application_claim_no || '',
application_reason: values.application_reason || '',
application_location: values.application_location || '',
application_amount: values.application_amount || '',
application_amount_label: values.application_amount_label || '',
application_business_time: values.application_business_time || '',
application_days: values.application_days || '',
application_transport_mode: values.application_transport_mode || '',
application_lodging_daily_cap: values.application_lodging_daily_cap || '',
application_subsidy_daily_cap: values.application_subsidy_daily_cap || '',
application_transport_policy: values.application_transport_policy || '',
application_policy_estimate: values.application_policy_estimate || '',
application_rule_name: values.application_rule_name || '',
application_rule_version: values.application_rule_version || '',
application_date: values.application_date || ''
}
return {
rawText,
userText: '生成报销草稿',
pendingText: '正在生成报销草稿...',
systemGenerated: true,
files,
extraContext: {
draft_claim_id: '',
review_action: 'save_draft',
user_input_text: rawText,
expense_scene_selection: {
expense_type: type?.key || current.expenseType || 'other',
expense_type_label: typeLabel,
original_message: rawText,
application_claim_id: values.application_claim_id || '',
application_claim_no: values.application_claim_no || ''
},
review_form_values: reviewFormValues
}
}
}
export function shouldConfirmGuidedInterruption(text, state) {
const current = normalizeGuidedFlowState(state)
if (!current.mode || current.pendingInterruptionText) {
return false
}
const normalized = normalizeText(text)
if (!normalized || NO_ATTACHMENT_TEXT_PATTERN.test(normalized)) {
return false
}
return INTERRUPTION_PATTERN.test(normalized)
}
export function buildGuidedInterruptionText(text) {
return [
`我看到你刚才输入的是:“${normalizeText(text)}”。`,
'',
'这看起来像一个新的问题。你想继续填写当前引导,还是先暂停当前引导并处理这个问题?'
].join('\n')
}
export function buildGuidedInterruptionActions() {
return [
{
label: '继续填写',
description: '保留当前引导,继续回答这一项',
icon: 'mdi mdi-pencil-outline',
action_type: GUIDED_ACTION_CONTINUE_FILLING
},
{
label: '暂停当前引导并处理这个问题',
description: '暂停引导,把刚才输入交给财务助手处理',
icon: 'mdi mdi-chat-processing-outline',
action_type: GUIDED_ACTION_PROCESS_INTERRUPTION
}
]
}
export function createGuidedStatusQueryState() {
return {
...createEmptyGuidedFlowState(),
mode: GUIDED_FLOW_MODE_STATUS_QUERY,
stepKey: 'query_mode'
}
}
export function buildGuidedStatusQueryStartText() {
return [
'你想按什么条件查询单据状态?',
'',
'先选查询方式,我再向你收集对应条件。'
].join('\n')
}
export function buildGuidedQueryModeActions() {
return GUIDED_QUERY_MODES.map((option) => ({
label: option.label,
description: option.description,
icon: option.icon,
action_type: GUIDED_ACTION_SELECT_QUERY_MODE,
payload: {
query_mode: option.key,
query_mode_label: option.label
}
}))
}
export function buildGuidedQueryStatusActions() {
return GUIDED_QUERY_STATUS_OPTIONS.map((option) => ({
label: option.label,
description: option.description,
icon: 'mdi mdi-checkbox-marked-circle-outline',
action_type: GUIDED_ACTION_SELECT_QUERY_STATUS,
payload: {
query_status: option.key,
query_status_label: option.label
}
}))
}
export function resolveGuidedQueryModeFromText(text) {
const normalized = normalizeText(text)
if (!normalized) return ''
const exact = GUIDED_QUERY_MODES.find((item) => normalized === item.label || normalized === item.key)
if (exact) return exact.key
if (/单号|编号|EXP-|APP-|AP-|RE-|AD-/i.test(normalized)) return 'claim_no'
if (/状态|草稿|审批|退回|归档|完成/.test(normalized)) return 'status'
if (/上周|本周|去年|今年|月份|时间|日期|[0-9]{4}-[0-9]{2}/.test(normalized)) return 'time_range'
return 'keyword'
}
export function selectGuidedQueryMode(state, queryMode) {
const current = normalizeGuidedFlowState(state)
const mode = GUIDED_QUERY_MODES.find((item) => item.key === normalizeText(queryMode))
if (!mode) {
return current
}
return {
...current,
mode: GUIDED_FLOW_MODE_STATUS_QUERY,
stepKey: mode.key === 'status' ? 'status_value' : 'query_value',
values: {
...current.values,
query_mode: mode.key,
query_mode_label: mode.label
},
pendingInterruptionText: ''
}
}
export function buildGuidedQueryPromptText(state) {
const current = normalizeGuidedFlowState(state)
const mode = normalizeText(current.values.query_mode)
if (!mode) {
return buildGuidedStatusQueryStartText()
}
if (mode === 'status') {
return [
'请选择要查询的单据状态。',
'',
'我会按所选状态筛选最近的报销单据。'
].join('\n')
}
const prompts = {
claim_no: '请输入单据编号,例如 RE-20260525103045-ABCDEFGH。',
time_range: '请输入查询时间范围,例如:上周、今年 5 月、2025 年全年。',
keyword: '请输入地点、客户或事由关键词,例如:上海电力、北京、服务器部署。'
}
return prompts[mode] || '请补充查询条件。'
}
export function buildGuidedStatusQueryText(state, valueText) {
const current = normalizeGuidedFlowState(state)
const mode = normalizeText(current.values.query_mode)
const value = normalizeText(valueText)
if (mode === 'claim_no') {
return `帮我查询单号 ${value} 的报销单状态`
}
if (mode === 'status') {
return `帮我查询${value}的报销单据,筛选最近的 5 条记录`
}
if (mode === 'time_range') {
return `帮我查询${value}提交或发生的报销单据状态,筛选最近的 5 条记录`
}
return `帮我查询地点或事由包含“${value}”的报销单据状态,筛选最近的 5 条记录`
}