feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -2,10 +2,10 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ElButton } from 'element-plus/es/components/button/index.mjs'
|
||||
|
||||
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
|
||||
import DocumentDropdownFilter from '../../components/shared/DocumentDropdownFilter.vue'
|
||||
import EnterpriseDetailCard from '../../components/shared/EnterpriseDetailCard.vue'
|
||||
import EnterpriseDetailPage from '../../components/shared/EnterpriseDetailPage.vue'
|
||||
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
|
||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
||||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||
import { fetchEmployeeMeta } from '../../services/employees.js'
|
||||
@@ -47,6 +47,10 @@ function mapOptions(values, suffix = '') {
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveOptionLabel(options, value, fallback) {
|
||||
return (Array.isArray(options) ? options : []).find((option) => option.value === value)?.label || fallback
|
||||
}
|
||||
|
||||
function resolveBudgetUpdatedAt(row) {
|
||||
return row?.updatedAt || row?.submittedAt || row?.archivedAt || '-'
|
||||
}
|
||||
@@ -99,8 +103,8 @@ export default {
|
||||
emits: ['openAssistant', 'detail-open-change', 'detail-topbar-change'],
|
||||
components: {
|
||||
BudgetTrendChart,
|
||||
DocumentDropdownFilter,
|
||||
EnterprisePagination,
|
||||
EnterpriseSelect,
|
||||
EnterpriseDetailCard,
|
||||
EnterpriseDetailPage,
|
||||
TableEmptyState,
|
||||
@@ -116,6 +120,7 @@ export default {
|
||||
const budgetLoading = ref(true)
|
||||
const budgetError = ref('')
|
||||
const selectedBudgetId = ref('')
|
||||
const activeBudgetFilterKey = ref('')
|
||||
const filters = ref({
|
||||
year: '2026',
|
||||
quarter: 'Q1',
|
||||
@@ -158,6 +163,9 @@ export default {
|
||||
() => budgetScopeTabs.value.find((item) => item.value === activeBudgetScope.value)?.label || '预算'
|
||||
)
|
||||
const statusOptions = computed(() => mapOptions(getBudgetStatusOptions(activeBudgetScope.value)))
|
||||
const budgetYearFilterLabel = computed(() => resolveOptionLabel(yearOptions, filters.value.year, '年度'))
|
||||
const budgetQuarterFilterLabel = computed(() => resolveOptionLabel(quarterOptions, filters.value.quarter, '季度'))
|
||||
const budgetStatusFilterLabel = computed(() => resolveOptionLabel(statusOptions.value, filters.value.status, '状态'))
|
||||
|
||||
const filteredBudgetRows = computed(() =>
|
||||
activeScopeRows.value
|
||||
@@ -322,6 +330,15 @@ export default {
|
||||
budgetPage.value = 1
|
||||
}
|
||||
|
||||
function toggleBudgetFilter(key) {
|
||||
activeBudgetFilterKey.value = activeBudgetFilterKey.value === key ? '' : key
|
||||
}
|
||||
|
||||
function selectBudgetFilter(key, value) {
|
||||
filters.value[key] = value
|
||||
activeBudgetFilterKey.value = ''
|
||||
}
|
||||
|
||||
function resolveScopedDepartments(options) {
|
||||
if (!isDepartmentBudgetMonitor.value) return options
|
||||
|
||||
@@ -419,6 +436,7 @@ export default {
|
||||
BUDGET_SCOPE_ALL,
|
||||
BUDGET_SCOPE_ARCHIVE,
|
||||
BUDGET_SCOPE_REVIEW,
|
||||
activeBudgetFilterKey,
|
||||
activeBudgetScope,
|
||||
budgetError,
|
||||
budgetKeyword,
|
||||
@@ -427,6 +445,9 @@ export default {
|
||||
budgetPageSize,
|
||||
budgetPageSizeOptions,
|
||||
budgetScopeTabs,
|
||||
budgetQuarterFilterLabel,
|
||||
budgetStatusFilterLabel,
|
||||
budgetYearFilterLabel,
|
||||
backToList,
|
||||
canAuditBudgetDrafts,
|
||||
canEditBudget,
|
||||
@@ -447,8 +468,10 @@ export default {
|
||||
showEmpty,
|
||||
showTable,
|
||||
statusOptions,
|
||||
selectBudgetFilter,
|
||||
totalBudgetPages,
|
||||
totalBudgetRows,
|
||||
toggleBudgetFilter,
|
||||
visibleBudgetRows,
|
||||
yearOptions
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
||||
import DocumentDropdownFilter from '../../components/shared/DocumentDropdownFilter.vue'
|
||||
import EnterprisePagination from '../../components/shared/EnterprisePagination.vue'
|
||||
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
|
||||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||||
@@ -449,10 +450,18 @@ function buildEmployeeSummary(employees) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapSimpleFilterOptions(values, allLabel) {
|
||||
return [
|
||||
{ label: allLabel, value: '' },
|
||||
...values.map((value) => ({ label: value, value }))
|
||||
]
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'EmployeeManagementView',
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
DocumentDropdownFilter,
|
||||
EnterprisePagination,
|
||||
EnterpriseSelect,
|
||||
TableLoadingState,
|
||||
@@ -559,6 +568,12 @@ export default {
|
||||
)
|
||||
)
|
||||
)
|
||||
const departmentFilterOptions = computed(() => mapSimpleFilterOptions(departmentOptions.value, '全部部门'))
|
||||
const gradeFilterOptions = computed(() => mapSimpleFilterOptions(gradeOptions.value, '全部职级'))
|
||||
const roleDropdownOptions = computed(() => mapSimpleFilterOptions(roleFilterOptions.value, '全部角色'))
|
||||
const departmentFilterLabel = computed(() => selectedDepartment.value || '组织部门')
|
||||
const gradeFilterLabel = computed(() => selectedGrade.value || '职级')
|
||||
const roleFilterLabel = computed(() => selectedRole.value || '系统角色')
|
||||
|
||||
const managerOptions = computed(() => {
|
||||
const currentId = selectedEmployee.value?.id
|
||||
@@ -1440,6 +1455,12 @@ export default {
|
||||
selectedDepartment,
|
||||
selectedGrade,
|
||||
selectedRole,
|
||||
departmentFilterLabel,
|
||||
departmentFilterOptions,
|
||||
gradeFilterLabel,
|
||||
gradeFilterOptions,
|
||||
roleDropdownOptions,
|
||||
roleFilterLabel,
|
||||
activeFilterPopover,
|
||||
currentPage,
|
||||
pageSize,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -203,7 +203,7 @@ export function shouldUseBudgetCompileReport(rawText, options = {}) {
|
||||
const isBudgetContextEditIntent = isBudgetContext && hasBudgetKeyword && hasCompileKeyword
|
||||
|
||||
return Boolean(
|
||||
budgetContext ||
|
||||
(isBudgetContext && budgetContext) ||
|
||||
(
|
||||
text &&
|
||||
(isWholeBudgetCompileIntent || isBudgetContextPeriodIntent || isBudgetContextEditIntent)
|
||||
|
||||
179
web/src/views/scripts/receiptFolderListFilters.js
Normal file
179
web/src/views/scripts/receiptFolderListFilters.js
Normal file
@@ -0,0 +1,179 @@
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export const RECEIPT_FILTER_ALL = 'all'
|
||||
|
||||
const QUALITY_OPTIONS = [
|
||||
{ value: RECEIPT_FILTER_ALL, label: '全部置信度' },
|
||||
{ value: 'high', label: '高置信度' },
|
||||
{ value: 'medium', label: '中等置信度' },
|
||||
{ value: 'low', label: '低置信度' },
|
||||
{ value: 'missing', label: '待确认' }
|
||||
]
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function getFilterValue(filters, key) {
|
||||
return normalizeText(filters?.[key]) || RECEIPT_FILTER_ALL
|
||||
}
|
||||
|
||||
function buildUniqueOptions(rows, valueKey, labelKey, allLabel) {
|
||||
const seen = new Map()
|
||||
for (const row of Array.isArray(rows) ? rows : []) {
|
||||
const value = normalizeText(row?.[valueKey])
|
||||
if (!value || seen.has(value)) continue
|
||||
seen.set(value, normalizeText(row?.[labelKey]) || value)
|
||||
}
|
||||
|
||||
return [
|
||||
{ value: RECEIPT_FILTER_ALL, label: allLabel },
|
||||
...Array.from(seen.entries())
|
||||
.map(([value, label]) => ({ value, label }))
|
||||
.sort((left, right) => left.label.localeCompare(right.label, 'zh-Hans-CN'))
|
||||
]
|
||||
}
|
||||
|
||||
function resolveReceiptMonth(row) {
|
||||
const raw = normalizeText(row?.document_date) || normalizeText(row?.uploaded_at)
|
||||
const match = raw.match(/^(\d{4})[-/年]?(\d{1,2})/)
|
||||
if (!match) return ''
|
||||
return `${match[1]}-${String(match[2]).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function buildMonthOptions(rows) {
|
||||
const months = new Set((Array.isArray(rows) ? rows : []).map(resolveReceiptMonth).filter(Boolean))
|
||||
return [
|
||||
{ value: RECEIPT_FILTER_ALL, label: '全部月份' },
|
||||
...Array.from(months)
|
||||
.sort((left, right) => right.localeCompare(left))
|
||||
.map((value) => ({ value, label: `${value.replace('-', '年')}月` }))
|
||||
]
|
||||
}
|
||||
|
||||
function resolveScore(row) {
|
||||
const score = Number(row?.avg_score || 0)
|
||||
return Number.isFinite(score) ? score : 0
|
||||
}
|
||||
|
||||
function matchesQuality(row, quality) {
|
||||
if (quality === RECEIPT_FILTER_ALL) return true
|
||||
const score = resolveScore(row)
|
||||
if (quality === 'missing') return score <= 0
|
||||
if (quality === 'high') return score >= 0.9
|
||||
if (quality === 'medium') return score >= 0.75 && score < 0.9
|
||||
if (quality === 'low') return score > 0 && score < 0.75
|
||||
return true
|
||||
}
|
||||
|
||||
export function buildReceiptFilterControls(rows, filters) {
|
||||
return [
|
||||
{
|
||||
key: 'documentType',
|
||||
label: '票据类型',
|
||||
options: buildUniqueOptions(rows, 'document_type', 'document_type_label', '全部类型')
|
||||
},
|
||||
{
|
||||
key: 'scene',
|
||||
label: '费用场景',
|
||||
options: buildUniqueOptions(rows, 'scene_code', 'scene_label', '全部场景')
|
||||
},
|
||||
{
|
||||
key: 'month',
|
||||
label: '票据月份',
|
||||
options: buildMonthOptions(rows)
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
label: '置信度',
|
||||
options: QUALITY_OPTIONS
|
||||
}
|
||||
].map((control) => ({
|
||||
...control,
|
||||
value: getFilterValue(filters, control.key)
|
||||
}))
|
||||
}
|
||||
|
||||
export function applyReceiptListFilters(rows, filters) {
|
||||
const documentType = getFilterValue(filters, 'documentType')
|
||||
const scene = getFilterValue(filters, 'scene')
|
||||
const month = getFilterValue(filters, 'month')
|
||||
const quality = getFilterValue(filters, 'quality')
|
||||
|
||||
return (Array.isArray(rows) ? rows : []).filter((row) => (
|
||||
(documentType === RECEIPT_FILTER_ALL || normalizeText(row?.document_type) === documentType)
|
||||
&& (scene === RECEIPT_FILTER_ALL || normalizeText(row?.scene_code) === scene)
|
||||
&& (month === RECEIPT_FILTER_ALL || resolveReceiptMonth(row) === month)
|
||||
&& matchesQuality(row, quality)
|
||||
))
|
||||
}
|
||||
|
||||
export function buildReceiptFilterTokens(controls, filters) {
|
||||
return (Array.isArray(controls) ? controls : [])
|
||||
.map((control) => {
|
||||
const value = getFilterValue(filters, control.key)
|
||||
if (value === RECEIPT_FILTER_ALL) return ''
|
||||
const option = control.options.find((item) => item.value === value)
|
||||
return `${control.label}:${option?.label || value}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function createReceiptFolderListFilterModel({ receipts, activeRows, keyword }) {
|
||||
const openReceiptFilterKey = ref('')
|
||||
const receiptFilters = reactive({
|
||||
documentType: RECEIPT_FILTER_ALL,
|
||||
scene: RECEIPT_FILTER_ALL,
|
||||
month: RECEIPT_FILTER_ALL,
|
||||
quality: RECEIPT_FILTER_ALL
|
||||
})
|
||||
const receiptFilterControls = computed(() => buildReceiptFilterControls(receipts.value, receiptFilters))
|
||||
const hasActiveReceiptFilters = computed(() => Object.values(receiptFilters).some((value) => value !== RECEIPT_FILTER_ALL))
|
||||
const filteredRows = computed(() => {
|
||||
const normalized = keyword.value.trim().toLowerCase()
|
||||
const filtered = applyReceiptListFilters(activeRows.value, receiptFilters)
|
||||
if (!normalized) return filtered
|
||||
return filtered.filter((item) => [
|
||||
item.file_name,
|
||||
item.document_type_label,
|
||||
item.scene_label,
|
||||
item.summary,
|
||||
item.amount,
|
||||
item.document_date,
|
||||
item.linked_claim_no
|
||||
].filter(Boolean).join('').toLowerCase().includes(normalized))
|
||||
})
|
||||
|
||||
function toggleReceiptFilter(key) {
|
||||
openReceiptFilterKey.value = openReceiptFilterKey.value === key ? '' : key
|
||||
}
|
||||
|
||||
function selectReceiptFilter(key, value) {
|
||||
receiptFilters[key] = value
|
||||
openReceiptFilterKey.value = ''
|
||||
}
|
||||
|
||||
function resolveReceiptFilterLabel(control) {
|
||||
return control.options.find((option) => option.value === receiptFilters[control.key])?.label || control.label
|
||||
}
|
||||
|
||||
function clearReceiptFilters() {
|
||||
receiptFilters.documentType = RECEIPT_FILTER_ALL
|
||||
receiptFilters.scene = RECEIPT_FILTER_ALL
|
||||
receiptFilters.month = RECEIPT_FILTER_ALL
|
||||
receiptFilters.quality = RECEIPT_FILTER_ALL
|
||||
openReceiptFilterKey.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
filteredRows,
|
||||
hasActiveReceiptFilters,
|
||||
openReceiptFilterKey,
|
||||
receiptFilterControls,
|
||||
receiptFilters,
|
||||
clearReceiptFilters,
|
||||
resolveReceiptFilterLabel,
|
||||
selectReceiptFilter,
|
||||
toggleReceiptFilter
|
||||
}
|
||||
}
|
||||
131
web/src/views/scripts/stewardFieldCompletionModel.js
Normal file
131
web/src/views/scripts/stewardFieldCompletionModel.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { normalizeApplicationPreview } from '../../utils/expenseApplicationPreview.js'
|
||||
|
||||
const APPLICATION_PREVIEW_FIELD_ONTOLOGY_KEY_MAP = {
|
||||
applicationType: 'expense_type',
|
||||
time: 'time_range',
|
||||
location: 'location',
|
||||
reason: 'reason',
|
||||
amount: 'amount',
|
||||
transportMode: 'transport_mode',
|
||||
department: 'department_name',
|
||||
applicant: 'employee_name',
|
||||
grade: 'employee_grade'
|
||||
}
|
||||
|
||||
const APPLICATION_PREVIEW_FIELD_LABEL_MAP = {
|
||||
applicationType: '申请类型',
|
||||
time: '时间',
|
||||
location: '地点',
|
||||
reason: '事由',
|
||||
amount: '金额',
|
||||
transportMode: '出行方式',
|
||||
department: '所属部门',
|
||||
applicant: '申请人',
|
||||
grade: '职级'
|
||||
}
|
||||
|
||||
function compactValue(value = '') {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function resolveStewardCurrentTask(continuation = null) {
|
||||
const task = continuation?.currentTask || continuation?.current_task || null
|
||||
return task && typeof task === 'object' ? task : null
|
||||
}
|
||||
|
||||
function resolveTaskOntologyFields(task = null) {
|
||||
const fields = task?.ontology_fields || task?.ontologyFields || {}
|
||||
return fields && typeof fields === 'object' ? fields : {}
|
||||
}
|
||||
|
||||
function resolveFieldValue(...candidates) {
|
||||
for (const candidate of candidates) {
|
||||
const value = compactValue(candidate)
|
||||
if (value && !['待补充', '待测算', '未知'].includes(value)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function buildUpdatedTask(task = null, fieldKey = '', value = '') {
|
||||
if (!task || typeof task !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const canonicalKey = APPLICATION_PREVIEW_FIELD_ONTOLOGY_KEY_MAP[fieldKey] || ''
|
||||
if (!canonicalKey) {
|
||||
return { ...task }
|
||||
}
|
||||
|
||||
const ontologyFields = {
|
||||
...resolveTaskOntologyFields(task),
|
||||
[canonicalKey]: value
|
||||
}
|
||||
const sourceMissingFields = Array.isArray(task.missing_fields)
|
||||
? task.missing_fields
|
||||
: Array.isArray(task.missingFields)
|
||||
? task.missingFields
|
||||
: []
|
||||
|
||||
return {
|
||||
...task,
|
||||
ontology_fields: ontologyFields,
|
||||
missing_fields: sourceMissingFields.filter((field) => compactValue(field) !== canonicalKey)
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStewardFieldCompletionContinuation(continuation = null, fieldKey = '', value = '') {
|
||||
const source = continuation && typeof continuation === 'object' ? continuation : {}
|
||||
const currentTask = resolveStewardCurrentTask(source)
|
||||
const updatedTask = buildUpdatedTask(currentTask, fieldKey, value)
|
||||
if (!updatedTask) {
|
||||
return source
|
||||
}
|
||||
|
||||
return {
|
||||
...source,
|
||||
currentTask: updatedTask
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStewardFieldCompletionRawText({
|
||||
preview = {},
|
||||
fieldKey = '',
|
||||
fieldLabel = '',
|
||||
value = '',
|
||||
continuation = null
|
||||
} = {}) {
|
||||
const normalizedPreview = normalizeApplicationPreview(preview)
|
||||
const fields = normalizedPreview.fields || {}
|
||||
const currentTask = resolveStewardCurrentTask(continuation)
|
||||
const ontologyFields = resolveTaskOntologyFields(currentTask)
|
||||
const selectedLabel = compactValue(fieldLabel) || APPLICATION_PREVIEW_FIELD_LABEL_MAP[fieldKey] || '补充项'
|
||||
const selectedValue = compactValue(value)
|
||||
const transportMode = fieldKey === 'transportMode'
|
||||
? selectedValue
|
||||
: resolveFieldValue(fields.transportMode, ontologyFields.transport_mode)
|
||||
|
||||
const knownLines = [
|
||||
['申请类型', resolveFieldValue(fields.applicationType, ontologyFields.expense_type, '差旅费用申请')],
|
||||
['时间', resolveFieldValue(fields.time, ontologyFields.time_range)],
|
||||
['地点', resolveFieldValue(fields.location, ontologyFields.location)],
|
||||
['事由', resolveFieldValue(fields.reason, ontologyFields.reason, currentTask?.summary)],
|
||||
['天数', resolveFieldValue(fields.days)],
|
||||
['出行方式', transportMode]
|
||||
]
|
||||
.filter(([, fieldValue]) => fieldValue)
|
||||
.map(([label, fieldValue]) => `${label}:${fieldValue}`)
|
||||
|
||||
return [
|
||||
'小财管家继续执行申请单字段补齐。',
|
||||
`用户已补充:${selectedLabel}:${selectedValue}。`,
|
||||
currentTask?.summary ? `任务摘要:${currentTask.summary}` : '',
|
||||
'',
|
||||
'已识别信息:',
|
||||
...knownLines,
|
||||
'',
|
||||
'处理要求:请先根据已补齐字段模拟查询交通票据和费用口径,完成系统预估金额测算,再生成申请单核对表。',
|
||||
'如果仍有缺失信息,继续向用户追问;不要替用户默认填写,也不要跳过小财管家的思考过程。'
|
||||
].filter((line) => line !== '').join('\n')
|
||||
}
|
||||
@@ -79,6 +79,25 @@ const FIELD_ALIASES = {
|
||||
application_transport_mode: 'transport_mode'
|
||||
}
|
||||
|
||||
const APPLICATION_NON_BLOCKING_MISSING_FIELDS = new Set([
|
||||
'amount',
|
||||
'attachments',
|
||||
'employee_no',
|
||||
'employee_name',
|
||||
'department_name'
|
||||
])
|
||||
|
||||
const FIELD_VALUE_DISPLAY_CONFIG = {
|
||||
expense_type: {
|
||||
travel: '差旅',
|
||||
business_entertainment: '业务招待',
|
||||
transportation: '交通费',
|
||||
traffic: '交通费',
|
||||
accommodation: '住宿费',
|
||||
meal: '餐饮费'
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStewardPlanRequest({ rawText = '', files = [], currentUser = {} } = {}) {
|
||||
const safeFiles = Array.isArray(files) ? files : []
|
||||
return {
|
||||
@@ -123,9 +142,10 @@ export function normalizeStewardPlan(rawPlan = {}, options = {}) {
|
||||
tasks: Array.isArray(rawPlan.tasks)
|
||||
? rawPlan.tasks.map((item) => {
|
||||
const taskType = String(item.task_type || item.taskType || '')
|
||||
const missingFields = Array.isArray(item.missing_fields || item.missingFields)
|
||||
const rawMissingFields = Array.isArray(item.missing_fields || item.missingFields)
|
||||
? item.missing_fields || item.missingFields
|
||||
: []
|
||||
const missingFields = filterStewardBlockingMissingFields(rawMissingFields, taskType)
|
||||
return {
|
||||
taskId: String(item.task_id || item.taskId || ''),
|
||||
taskType,
|
||||
@@ -188,7 +208,7 @@ export function buildStewardPlanMessageText(plan) {
|
||||
}
|
||||
|
||||
export function buildStewardFieldItems(fields = [], taskType = '') {
|
||||
const safeFields = Array.isArray(fields) ? fields : []
|
||||
const safeFields = filterStewardBlockingMissingFields(fields, taskType)
|
||||
const seen = new Set()
|
||||
return safeFields
|
||||
.map((field) => normalizeFieldKey(field))
|
||||
@@ -202,18 +222,44 @@ export function buildStewardFieldItems(fields = [], taskType = '') {
|
||||
.map((field) => resolveFieldDisplay(field, taskType))
|
||||
}
|
||||
|
||||
export function formatStewardMissingFieldList(fields = [], taskType = '') {
|
||||
export function formatStewardMissingFieldList(fields = [], taskType = '', options = {}) {
|
||||
const includeHints = options.includeHints !== false
|
||||
return buildStewardFieldItems(fields, taskType)
|
||||
.map((item) => item.hint ? `${item.label}(${item.hint})` : item.label)
|
||||
.map((item) => includeHints && item.hint ? `${item.label}(${item.hint})` : item.label)
|
||||
.join('、')
|
||||
}
|
||||
|
||||
export function filterStewardBlockingMissingFields(fields = [], taskType = '') {
|
||||
const safeFields = Array.isArray(fields) ? fields : []
|
||||
const seen = new Set()
|
||||
if (taskType !== 'expense_application') {
|
||||
return safeFields
|
||||
.map((field) => normalizeFieldKey(field))
|
||||
.filter((field) => {
|
||||
if (!field || seen.has(field)) {
|
||||
return false
|
||||
}
|
||||
seen.add(field)
|
||||
return true
|
||||
})
|
||||
}
|
||||
return safeFields
|
||||
.map((field) => normalizeFieldKey(field))
|
||||
.filter((field) => {
|
||||
if (!field || seen.has(field) || APPLICATION_NON_BLOCKING_MISSING_FIELDS.has(field)) {
|
||||
return false
|
||||
}
|
||||
seen.add(field)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function formatStewardOntologyFields(fields = {}, taskType = '') {
|
||||
return Object.entries(fields || {})
|
||||
.filter(([, value]) => String(value || '').trim())
|
||||
.map(([key, value]) => {
|
||||
const field = resolveFieldDisplay(key, taskType)
|
||||
return `${field.label}:${value}`
|
||||
return `${field.label}:${formatStewardFieldDisplayValue(field.key, value)}`
|
||||
})
|
||||
.join(';')
|
||||
}
|
||||
@@ -246,6 +292,7 @@ export function buildStewardSuggestedActions(plan) {
|
||||
steward_confirmation_id: String(action.confirmation_id || action.confirmationId || ''),
|
||||
steward_plan_id: normalized.planId,
|
||||
steward_next_task_id: task?.taskId || '',
|
||||
steward_current_task: buildStewardTaskPayload(task),
|
||||
steward_remaining_task_count: normalized.tasks.filter((item) => item.taskId !== task?.taskId).length,
|
||||
steward_remaining_tasks: buildRemainingTaskPayload(normalized, task?.taskId)
|
||||
}
|
||||
@@ -447,7 +494,11 @@ function buildStewardCarryText(actionType, task, group, normalized = null) {
|
||||
}
|
||||
|
||||
const fields = formatStewardOntologyFields(task.ontologyFields || {}, task.taskType)
|
||||
const missingFields = formatStewardMissingFieldList(task.missingFields || [], task.taskType)
|
||||
const missingFields = formatStewardMissingFieldList(
|
||||
task.missingFields || [],
|
||||
task.taskType,
|
||||
{ includeHints: false }
|
||||
)
|
||||
const lines = [
|
||||
actionType === 'confirm_create_application'
|
||||
? `小财管家已完成意图识别,请先创建申请单:${task.title || task.taskTypeLabel}。`
|
||||
@@ -458,8 +509,12 @@ function buildStewardCarryText(actionType, task, group, normalized = null) {
|
||||
group?.excludedAttachmentNames?.length ? `暂不归集附件:${group.excludedAttachmentNames.join('、')}` : '',
|
||||
missingFields ? `还需要补充:${missingFields}` : '',
|
||||
actionType === 'confirm_create_application'
|
||||
? '请直接生成申请单核对结果;信息足够时生成申请单,但在入库或提交审批前仍需让我确认。'
|
||||
: '请直接生成报销核对结果;需要创建草稿、绑定附件或提交审批前仍需让我确认。'
|
||||
? missingFields
|
||||
? '请先追问上述缺失信息,不要直接生成申请单核对表,也不要替用户默认填写。'
|
||||
: '请直接生成申请单核对结果;信息足够时生成申请单,但在入库或提交审批前仍需让我确认。'
|
||||
: missingFields
|
||||
? '请先追问上述缺失信息,不要直接生成报销核对结果,也不要替用户默认填写。'
|
||||
: '请直接生成报销核对结果;需要创建草稿、绑定附件或提交审批前仍需让我确认。'
|
||||
]
|
||||
const remainingTaskText = normalized ? buildRemainingTaskText(normalized, task.taskId) : ''
|
||||
if (remainingTaskText) {
|
||||
@@ -495,6 +550,12 @@ function resolveFieldDisplay(field, taskType = '') {
|
||||
}
|
||||
}
|
||||
|
||||
function formatStewardFieldDisplayValue(field, value) {
|
||||
const key = normalizeFieldKey(field)
|
||||
const normalizedValue = String(value || '').trim()
|
||||
return FIELD_VALUE_DISPLAY_CONFIG[key]?.[normalizedValue] || normalizedValue
|
||||
}
|
||||
|
||||
function buildRemainingTaskText(normalized, currentTaskId) {
|
||||
const remainingTasks = normalized.tasks.filter((task) => task.taskId !== currentTaskId)
|
||||
if (!remainingTasks.length) {
|
||||
@@ -512,13 +573,20 @@ function buildRemainingTaskText(normalized, currentTaskId) {
|
||||
function buildRemainingTaskPayload(normalized, currentTaskId) {
|
||||
return normalized.tasks
|
||||
.filter((task) => task.taskId !== currentTaskId)
|
||||
.map((task) => ({
|
||||
task_id: task.taskId,
|
||||
task_type: task.taskType,
|
||||
title: task.title,
|
||||
summary: task.summary,
|
||||
assigned_agent: task.assignedAgent,
|
||||
ontology_fields: task.ontologyFields || {},
|
||||
missing_fields: task.missingFields || []
|
||||
}))
|
||||
.map((task) => buildStewardTaskPayload(task))
|
||||
}
|
||||
|
||||
function buildStewardTaskPayload(task) {
|
||||
if (!task) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
task_id: task.taskId || task.task_id || '',
|
||||
task_type: task.taskType || task.task_type || '',
|
||||
title: task.title || '',
|
||||
summary: task.summary || '',
|
||||
assigned_agent: task.assignedAgent || task.assigned_agent || '',
|
||||
ontology_fields: task.ontologyFields || task.ontology_fields || {},
|
||||
missing_fields: task.missingFields || task.missing_fields || []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
} from './stewardPlanModel.js'
|
||||
import { SESSION_TYPE_STEWARD } from './travelReimbursementConversationModel.js'
|
||||
|
||||
const STEWARD_TYPEWRITER_INTERVAL_MS = 18
|
||||
const STEWARD_THINKING_TYPEWRITER_INTERVAL_MS = 14
|
||||
const STEWARD_TYPEWRITER_INTERVAL_MS = 10
|
||||
const STEWARD_THINKING_TYPEWRITER_INTERVAL_MS = 8
|
||||
const STEWARD_TYPEWRITER_CHUNK_SIZE = 4
|
||||
const STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE = 5
|
||||
|
||||
export function useStewardPlanFlow({
|
||||
activeSessionType,
|
||||
@@ -174,7 +176,7 @@ export function useStewardPlanFlow({
|
||||
if (runId !== stewardTypewriterRunId) {
|
||||
return
|
||||
}
|
||||
index += 1
|
||||
index = Math.min(total, index + STEWARD_TYPEWRITER_CHUNK_SIZE)
|
||||
const message = messages.value.find((item) => item.id === messageId)
|
||||
if (!message) {
|
||||
return
|
||||
@@ -269,7 +271,7 @@ export function useStewardPlanFlow({
|
||||
if (runId !== stewardTypewriterRunId) {
|
||||
return
|
||||
}
|
||||
index += 1
|
||||
index = Math.min(chars.length, index + STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE)
|
||||
updateStewardThinkingEvent(messageId, eventId, chars.slice(0, index).join(''), 'running', runId)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
buildLocalApplicationPreview,
|
||||
buildLocalApplicationPreviewMessage,
|
||||
buildModelRefinedApplicationPreview,
|
||||
applicationDateRangesOverlap,
|
||||
normalizeApplicationPreview,
|
||||
normalizeTransportModeOption,
|
||||
resolveApplicationDateRange,
|
||||
shouldUseLocalApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
|
||||
@@ -21,16 +24,275 @@ import { fetchOntologyParse } from '../../services/ontology.js'
|
||||
import { buildExpenseApplicationOntologyContext } from '../../utils/expenseApplicationOntology.js'
|
||||
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
|
||||
import { fetchReceiptFolderItems } from '../../services/receiptFolder.js'
|
||||
import { fetchStewardSlotDecision } from '../../services/steward.js'
|
||||
import {
|
||||
handleBudgetCompileReportSubmit,
|
||||
shouldUseBudgetCompileReport
|
||||
} from './budgetAssistantReportModel.js'
|
||||
|
||||
const STEWARD_ASSISTANT_NAME = '小财管家'
|
||||
const STEWARD_DELEGATED_TYPEWRITER_INTERVAL_MS = 18
|
||||
const STEWARD_DELEGATED_THINKING_INTERVAL_MS = 14
|
||||
const STEWARD_DELEGATED_TYPEWRITER_INTERVAL_MS = 10
|
||||
const STEWARD_DELEGATED_THINKING_INTERVAL_MS = 8
|
||||
const STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE = 4
|
||||
const STEWARD_DELEGATED_THINKING_CHUNK_SIZE = 5
|
||||
const APPLICATION_PREVIEW_FIELD_ACTION_SET = 'set_application_preview_field'
|
||||
|
||||
const APPLICATION_PREVIEW_ONTOLOGY_FIELD_MAP = {
|
||||
applicationType: 'expense_type',
|
||||
time: 'time_range',
|
||||
location: 'location',
|
||||
reason: 'reason',
|
||||
amount: 'amount',
|
||||
transportMode: 'transport_mode',
|
||||
department: 'department_name',
|
||||
applicant: 'employee_name',
|
||||
grade: 'employee_grade'
|
||||
}
|
||||
|
||||
const APPLICATION_MISSING_LABEL_ONTOLOGY_FIELD_MAP = {
|
||||
费用类型: 'expense_type',
|
||||
申请类型: 'expense_type',
|
||||
发生时间: 'time_range',
|
||||
出发时间: 'time_range',
|
||||
申请时间: 'time_range',
|
||||
地点: 'location',
|
||||
事由: 'reason',
|
||||
金额: 'amount',
|
||||
系统预估费用: 'amount',
|
||||
出行方式: 'transport_mode',
|
||||
附件: 'attachments',
|
||||
'附件/凭证': 'attachments',
|
||||
商户: 'merchant_name',
|
||||
'商户/开票方': 'merchant_name',
|
||||
客户: 'customer_name',
|
||||
客户或项目对象: 'customer_name'
|
||||
}
|
||||
|
||||
const ONTOLOGY_FIELD_APPLICATION_PREVIEW_KEY_MAP = {
|
||||
expense_type: 'applicationType',
|
||||
time_range: 'time',
|
||||
location: 'location',
|
||||
reason: 'reason',
|
||||
amount: 'amount',
|
||||
transport_mode: 'transportMode',
|
||||
department_name: 'department',
|
||||
employee_name: 'applicant',
|
||||
employee_grade: 'grade'
|
||||
}
|
||||
|
||||
const ONTOLOGY_FIELD_DISPLAY_LABEL_MAP = {
|
||||
expense_type: '费用类型',
|
||||
time_range: '时间',
|
||||
location: '地点',
|
||||
reason: '事由',
|
||||
amount: '金额',
|
||||
transport_mode: '出行方式',
|
||||
attachments: '附件/凭证',
|
||||
customer_name: '客户或项目对象',
|
||||
merchant_name: '商户/开票方',
|
||||
department_name: '所属部门',
|
||||
employee_name: '申请人',
|
||||
employee_grade: '职级'
|
||||
}
|
||||
|
||||
const APPLICATION_NON_BLOCKING_ONTOLOGY_FIELDS = new Set([
|
||||
'amount',
|
||||
'attachments',
|
||||
'employee_no',
|
||||
'department_name',
|
||||
'employee_name'
|
||||
])
|
||||
|
||||
const APPLICATION_DUPLICATE_IGNORED_STATUSES = new Set([
|
||||
'cancelled',
|
||||
'canceled',
|
||||
'void',
|
||||
'voided',
|
||||
'deleted',
|
||||
'已取消',
|
||||
'已作废',
|
||||
'作废',
|
||||
'已删除'
|
||||
])
|
||||
|
||||
function normalizeClaimListPayload(payload) {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload
|
||||
}
|
||||
return Array.isArray(payload?.items) ? payload.items : []
|
||||
}
|
||||
|
||||
function normalizeClaimRiskFlags(claim) {
|
||||
const flags = claim?.risk_flags_json || claim?.riskFlagsJson || claim?.riskFlags || []
|
||||
if (Array.isArray(flags)) {
|
||||
return flags
|
||||
}
|
||||
return flags && typeof flags === 'object' ? [flags] : []
|
||||
}
|
||||
|
||||
function extractApplicationDetailFromClaim(claim) {
|
||||
return normalizeClaimRiskFlags(claim).reduce((found, item) => {
|
||||
if (found || !item || typeof item !== 'object') {
|
||||
return found
|
||||
}
|
||||
const detail = item.application_detail || item.applicationDetail
|
||||
return detail && typeof detail === 'object' ? detail : null
|
||||
}, null)
|
||||
}
|
||||
|
||||
function isApplicationClaimRecord(claim) {
|
||||
const expenseType = String(claim?.expense_type || claim?.expenseType || '').trim().toLowerCase()
|
||||
const claimNo = String(claim?.claim_no || claim?.claimNo || '').trim().toUpperCase()
|
||||
return (
|
||||
expenseType === 'application' ||
|
||||
expenseType === 'expense_application' ||
|
||||
expenseType.endsWith('_application') ||
|
||||
claimNo.startsWith('AP-') ||
|
||||
claimNo.startsWith('APP-') ||
|
||||
Boolean(extractApplicationDetailFromClaim(claim))
|
||||
)
|
||||
}
|
||||
|
||||
function normalizeApplicationExpenseType(value) {
|
||||
const text = String(value || '').trim().toLowerCase()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
if (text === 'travel_application' || /差旅|出差/.test(text)) {
|
||||
return 'travel_application'
|
||||
}
|
||||
if (text === 'purchase_application' || /采购/.test(text)) {
|
||||
return 'purchase_application'
|
||||
}
|
||||
if (text === 'meeting_application' || /会务|会议/.test(text)) {
|
||||
return 'meeting_application'
|
||||
}
|
||||
if (text === 'expense_application' || text === 'application' || text.endsWith('_application')) {
|
||||
return text === 'application' ? 'expense_application' : text
|
||||
}
|
||||
return 'expense_application'
|
||||
}
|
||||
|
||||
function resolveClaimApplicationExpenseType(claim) {
|
||||
const detail = extractApplicationDetailFromClaim(claim) || {}
|
||||
return normalizeApplicationExpenseType(
|
||||
claim?.expense_type ||
|
||||
claim?.expenseType ||
|
||||
detail.application_type ||
|
||||
detail.applicationType ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
function isIgnoredApplicationDuplicateStatus(status) {
|
||||
return APPLICATION_DUPLICATE_IGNORED_STATUSES.has(String(status || '').trim().toLowerCase())
|
||||
}
|
||||
|
||||
function resolveClaimApplicationDateRange(claim) {
|
||||
const detail = extractApplicationDetailFromClaim(claim) || {}
|
||||
return (
|
||||
resolveApplicationDateRange(
|
||||
detail.time ||
|
||||
detail.time_range ||
|
||||
detail.timeRange ||
|
||||
detail.application_time ||
|
||||
detail.applicationTime ||
|
||||
detail.application_business_time ||
|
||||
detail.applicationBusinessTime ||
|
||||
detail.application_date ||
|
||||
detail.applicationDate,
|
||||
detail.days || detail.application_days || detail.applicationDays
|
||||
) ||
|
||||
resolveApplicationDateRange(claim?.occurred_at || claim?.occurredAt || '')
|
||||
)
|
||||
}
|
||||
|
||||
function formatApplicationDateRangeLabel(range) {
|
||||
if (!range?.startDate) {
|
||||
return '待确认'
|
||||
}
|
||||
return range.startDate === range.endDate ? range.startDate : `${range.startDate} 至 ${range.endDate}`
|
||||
}
|
||||
|
||||
function findOverlappingApplicationClaim(applicationPreview, claimsPayload) {
|
||||
const preview = normalizeApplicationPreview(applicationPreview)
|
||||
const fields = preview.fields || {}
|
||||
const currentRange = resolveApplicationDateRange(fields.time, fields.days)
|
||||
if (!currentRange) {
|
||||
return null
|
||||
}
|
||||
const currentExpenseType = normalizeApplicationExpenseType(fields.applicationType)
|
||||
const claims = normalizeClaimListPayload(claimsPayload)
|
||||
for (const claim of claims) {
|
||||
if (!isApplicationClaimRecord(claim) || isIgnoredApplicationDuplicateStatus(claim?.status)) {
|
||||
continue
|
||||
}
|
||||
const existingExpenseType = resolveClaimApplicationExpenseType(claim)
|
||||
if (currentExpenseType && existingExpenseType && currentExpenseType !== existingExpenseType) {
|
||||
continue
|
||||
}
|
||||
const existingRange = resolveClaimApplicationDateRange(claim)
|
||||
if (!existingRange || !applicationDateRangesOverlap(currentRange, existingRange)) {
|
||||
continue
|
||||
}
|
||||
return {
|
||||
claim,
|
||||
currentRange,
|
||||
existingRange,
|
||||
claimId: String(claim?.id || claim?.claim_id || claim?.claimId || '').trim(),
|
||||
claimNo: String(claim?.claim_no || claim?.claimNo || claim?.id || '').trim(),
|
||||
status: String(claim?.approval_stage || claim?.approvalStage || claim?.status || '').trim(),
|
||||
reason: String(claim?.reason || '').trim(),
|
||||
location: String(claim?.location || '').trim()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function buildApplicationDateConflictMessage(conflict) {
|
||||
const claimNo = conflict?.claimNo || '已有申请'
|
||||
return [
|
||||
'我先检查了你的申请时间,发现同一天或重叠日期已经存在差旅申请,不能重复创建。',
|
||||
'',
|
||||
'已有申请:',
|
||||
`- **单号**:${claimNo}`,
|
||||
`- **申请时间**:${formatApplicationDateRangeLabel(conflict?.existingRange)}`,
|
||||
conflict?.location ? `- **地点**:${conflict.location}` : '',
|
||||
conflict?.reason ? `- **事由**:${conflict.reason}` : '',
|
||||
`- **当前节点**:${conflict?.status || '处理中'}`,
|
||||
'',
|
||||
`本次识别时间:${formatApplicationDateRangeLabel(conflict?.currentRange)}`,
|
||||
'',
|
||||
'请先查看已有申请,或修改本次出差时间后再继续。'
|
||||
].filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
function buildApplicationDateConflictActions(conflict) {
|
||||
const actions = []
|
||||
if (conflict?.claimId) {
|
||||
actions.push({
|
||||
action_type: 'open_application_detail',
|
||||
label: '查看已有申请',
|
||||
description: conflict.claimNo ? `进入 ${conflict.claimNo} 单据详情。` : '进入已有申请单据详情。',
|
||||
icon: 'mdi mdi-file-search-outline',
|
||||
payload: {
|
||||
claim_id: conflict.claimId,
|
||||
claim_no: conflict.claimNo
|
||||
}
|
||||
})
|
||||
}
|
||||
actions.push({
|
||||
action_type: 'prefill_composer',
|
||||
label: '修改出差时间',
|
||||
description: '在输入框中补充新的出差日期后继续。',
|
||||
icon: 'mdi mdi-calendar-edit-outline',
|
||||
payload: {
|
||||
prompt_prefill: '修改出差时间为:'
|
||||
}
|
||||
})
|
||||
return actions
|
||||
}
|
||||
|
||||
export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
const {
|
||||
MAX_ATTACHMENTS,
|
||||
@@ -145,8 +407,21 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
return Array.isArray(normalized.missingFields) ? normalized.missingFields : []
|
||||
}
|
||||
|
||||
function isBlockingApplicationOntologyField(key = '') {
|
||||
const normalizedKey = String(key || '').trim()
|
||||
return Boolean(normalizedKey && !APPLICATION_NON_BLOCKING_ONTOLOGY_FIELDS.has(normalizedKey))
|
||||
}
|
||||
|
||||
function resolveBlockingApplicationMissingFieldsForSteward(preview = {}) {
|
||||
return resolveApplicationPreviewMissingFieldsForSteward(preview).filter((label) => {
|
||||
const ontologyKey = APPLICATION_MISSING_LABEL_ONTOLOGY_FIELD_MAP[String(label || '').trim()] || ''
|
||||
return !ontologyKey || isBlockingApplicationOntologyField(ontologyKey)
|
||||
})
|
||||
}
|
||||
|
||||
function buildStewardApplicationPreviewSuggestedActions(preview = {}) {
|
||||
const missingFields = resolveApplicationPreviewMissingFieldsForSteward(preview)
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const missingFields = resolveApplicationPreviewMissingFieldsForSteward(normalized)
|
||||
if (!missingFields.includes('出行方式')) {
|
||||
return []
|
||||
}
|
||||
@@ -158,77 +433,298 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
return APPLICATION_TRANSPORT_MODE_OPTIONS.map((mode) => ({
|
||||
action_type: APPLICATION_PREVIEW_FIELD_ACTION_SET,
|
||||
label: mode,
|
||||
description: `选择${mode}作为本次出行方式,并同步费用测算。`,
|
||||
description: `选择${mode}后,由小财管家继续查询票价并测算费用。`,
|
||||
icon: iconMap[mode] || 'mdi mdi-map-marker-path',
|
||||
payload: {
|
||||
field_key: 'transportMode',
|
||||
field_label: '出行方式',
|
||||
value: mode
|
||||
value: mode,
|
||||
applicationPreview: normalized,
|
||||
steward_delegated_field_completion: true
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveStewardContinuationCurrentTask(continuation = null) {
|
||||
const task = continuation?.currentTask || continuation?.current_task || null
|
||||
return task && typeof task === 'object' ? task : null
|
||||
}
|
||||
|
||||
function normalizeCanonicalFieldList(fields = []) {
|
||||
const normalized = []
|
||||
if (!Array.isArray(fields)) {
|
||||
return normalized
|
||||
}
|
||||
fields.forEach((field) => {
|
||||
const key = String(field || '').trim()
|
||||
if (key && !normalized.includes(key)) {
|
||||
normalized.push(key)
|
||||
}
|
||||
})
|
||||
return normalized
|
||||
}
|
||||
|
||||
function buildStewardSlotDecisionOntologyFields(preview = {}, continuation = null) {
|
||||
const normalizedPreview = normalizeApplicationPreview(preview)
|
||||
const previewFields = normalizedPreview.fields || {}
|
||||
const task = resolveStewardContinuationCurrentTask(continuation)
|
||||
const taskFields = task?.ontology_fields || task?.ontologyFields || {}
|
||||
const fields = {}
|
||||
Object.entries(taskFields || {}).forEach(([key, value]) => {
|
||||
const normalizedKey = String(key || '').trim()
|
||||
const normalizedValue = String(value || '').trim()
|
||||
if (normalizedKey && normalizedValue) {
|
||||
fields[normalizedKey] = normalizedValue
|
||||
}
|
||||
})
|
||||
Object.entries(APPLICATION_PREVIEW_ONTOLOGY_FIELD_MAP).forEach(([previewKey, ontologyKey]) => {
|
||||
const value = String(previewFields[previewKey] || '').trim()
|
||||
if (value && value !== '待补充' && !fields[ontologyKey]) {
|
||||
fields[ontologyKey] = value
|
||||
}
|
||||
})
|
||||
return fields
|
||||
}
|
||||
|
||||
function buildStewardSlotDecisionMissingFields(preview = {}, continuation = null, ontologyFields = {}) {
|
||||
const task = resolveStewardContinuationCurrentTask(continuation)
|
||||
const taskMissingFields = normalizeCanonicalFieldList(task?.missing_fields || task?.missingFields || [])
|
||||
.filter((key) => isBlockingApplicationOntologyField(key) && !String(ontologyFields[key] || '').trim())
|
||||
if (taskMissingFields.length) {
|
||||
return taskMissingFields
|
||||
}
|
||||
return resolveApplicationPreviewMissingFieldsForSteward(preview)
|
||||
.map((label) => APPLICATION_MISSING_LABEL_ONTOLOGY_FIELD_MAP[String(label || '').trim()] || '')
|
||||
.filter((key, index, list) =>
|
||||
key &&
|
||||
isBlockingApplicationOntologyField(key) &&
|
||||
!String(ontologyFields[key] || '').trim() &&
|
||||
list.indexOf(key) === index
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchStewardApplicationSlotDecision(preview = {}, rawText = '', continuation = null) {
|
||||
const ontologyFields = buildStewardSlotDecisionOntologyFields(preview, continuation)
|
||||
const missingFields = buildStewardSlotDecisionMissingFields(preview, continuation, ontologyFields)
|
||||
try {
|
||||
return await fetchStewardSlotDecision({
|
||||
task_type: 'expense_application',
|
||||
user_message: String(rawText || '').trim(),
|
||||
ontology_fields: ontologyFields,
|
||||
missing_fields: missingFields,
|
||||
task_context: {
|
||||
steward_continuation: continuation || null,
|
||||
application_preview: normalizeApplicationPreview(preview)
|
||||
}
|
||||
}, {
|
||||
timeoutMs: 45000,
|
||||
timeoutMessage: '小财管家字段决策超时,已按当前本体缺口继续追问。'
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('Steward slot decision failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function formatStewardDecisionUserText(text = '') {
|
||||
let formatted = String(text || '').trim()
|
||||
Object.entries(ONTOLOGY_FIELD_DISPLAY_LABEL_MAP).forEach(([fieldKey, label]) => {
|
||||
const escapedKey = fieldKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
formatted = formatted
|
||||
.replace(new RegExp(`(\\s*${escapedKey}\\s*)`, 'g'), '')
|
||||
.replace(new RegExp(`\\(\\s*${escapedKey}\\s*\\)`, 'g'), '')
|
||||
.replace(new RegExp(`\\b${escapedKey}\\b`, 'g'), label)
|
||||
})
|
||||
return formatted.replace(/\s{2,}/g, ' ').trim()
|
||||
}
|
||||
|
||||
function buildStewardSlotDecisionMessage(decision = null, preview = {}, fallbackText = '') {
|
||||
if (!decision || String(decision.next_action || '').trim() !== 'ask_user') {
|
||||
return fallbackText
|
||||
}
|
||||
const question = formatStewardDecisionUserText(decision.question || '')
|
||||
const rationale = formatStewardDecisionUserText(decision.rationale || '')
|
||||
const parts = [
|
||||
'我已经识别出这一步要先处理申请单,但当前还不能直接生成可提交的申请核对表。',
|
||||
'',
|
||||
rationale ? `**原因是:${rationale}**` : '',
|
||||
'',
|
||||
question || buildStewardApplicationPreviewMessage(preview, fallbackText)
|
||||
].filter((item) => item !== '')
|
||||
return parts.join('\n')
|
||||
}
|
||||
|
||||
function buildStewardSlotDecisionSuggestedActions(decision = null, preview = {}) {
|
||||
if (!decision || String(decision.next_action || '').trim() !== 'ask_user') {
|
||||
return []
|
||||
}
|
||||
const normalizedPreview = normalizeApplicationPreview(preview)
|
||||
const iconMap = {
|
||||
火车: 'mdi mdi-train',
|
||||
飞机: 'mdi mdi-airplane',
|
||||
轮船: 'mdi mdi-ferry'
|
||||
}
|
||||
const actions = Array.isArray(decision.options) ? decision.options : []
|
||||
return actions.map((option) => {
|
||||
const canonicalField = String(option?.field_key || option?.fieldKey || '').trim()
|
||||
if (canonicalField && !isBlockingApplicationOntologyField(canonicalField)) {
|
||||
return null
|
||||
}
|
||||
const fieldKey = ONTOLOGY_FIELD_APPLICATION_PREVIEW_KEY_MAP[canonicalField] || canonicalField
|
||||
const value = String(option?.value || option?.label || '').trim()
|
||||
const label = String(option?.label || value).trim()
|
||||
const normalizedValue = fieldKey === 'transportMode'
|
||||
? normalizeTransportModeOption(value || label, '')
|
||||
: value
|
||||
if (!fieldKey || !value || !label) {
|
||||
return null
|
||||
}
|
||||
if (fieldKey === 'transportMode' && !normalizedValue) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
action_type: APPLICATION_PREVIEW_FIELD_ACTION_SET,
|
||||
label,
|
||||
description: String(option?.description || '').trim() || `选择${label}后,由小财管家继续测算并生成核对结果。`,
|
||||
icon: iconMap[label] || iconMap[value] || 'mdi mdi-form-select',
|
||||
payload: {
|
||||
field_key: fieldKey,
|
||||
field_label: ONTOLOGY_FIELD_DISPLAY_LABEL_MAP[canonicalField] || label,
|
||||
value: normalizedValue,
|
||||
applicationPreview: normalizedPreview,
|
||||
steward_delegated_field_completion: true
|
||||
}
|
||||
}
|
||||
}).filter(Boolean)
|
||||
}
|
||||
|
||||
function buildStewardApplicationPreviewMessage(preview = {}, fallbackText = '') {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
const missingFields = resolveApplicationPreviewMissingFieldsForSteward(normalized)
|
||||
const missingFields = resolveBlockingApplicationMissingFieldsForSteward(normalized)
|
||||
if (!missingFields.length) {
|
||||
return fallbackText
|
||||
}
|
||||
|
||||
if (missingFields.includes('出行方式')) {
|
||||
return [
|
||||
'我已经生成这一步的申请单核对结果,但现在还不能继续提交。',
|
||||
'我已经识别出这一步要先处理出差申请,但现在还不能生成可提交的申请核对表。',
|
||||
'',
|
||||
'**原因是:还缺少“出行方式”。**',
|
||||
'',
|
||||
`本次申请是前往${fields.location || '目的地'}的差旅事项,出行方式会影响交通费用口径和系统预估金额。`,
|
||||
'',
|
||||
'请先告诉我你打算怎么出行:**火车、飞机或轮船**。我会根据你的选择更新下方核对表和费用测算,再继续判断是否可以提交申请。'
|
||||
'请先告诉我你打算怎么出行:**火车、飞机或轮船**。我会根据你的选择生成申请核对表并同步费用测算,再继续判断是否可以提交申请。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
return [
|
||||
'我已经生成这一步的申请单核对结果,但现在还不能继续提交。',
|
||||
'我已经识别出这一步要先处理申请单,但现在还不能生成可提交的申请核对表。',
|
||||
'',
|
||||
`**还需要你补充:${missingFields.join('、')}。**`,
|
||||
'',
|
||||
`请先补充 **${missingFields[0]}**。你也可以直接点击下方核对表里的对应行编辑;补齐后我再继续推进下一步。`
|
||||
`请先补充 **${missingFields[0]}**。补齐后我再生成申请核对表并继续推进下一步。`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function shouldPauseStewardApplicationPreview(preview = {}) {
|
||||
return resolveBlockingApplicationMissingFieldsForSteward(preview).length > 0
|
||||
}
|
||||
|
||||
function extractStewardCarryLine(text = '', label = '') {
|
||||
const escapedLabel = String(label || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const match = String(text || '').match(new RegExp(`(?:^|\\n)${escapedLabel}[::]([^\\n]+)`, 'u'))
|
||||
return match ? match[1].trim() : ''
|
||||
}
|
||||
|
||||
function extractStewardDelegatedTaskTitle(text = '', sessionType = '') {
|
||||
const taskMatch = String(text || '').match(/请(?:先)?(?:创建申请单|填写报销单|继续填写报销单)[::]([^。\n]+)/u)
|
||||
if (taskMatch?.[1]) {
|
||||
return taskMatch[1].trim()
|
||||
}
|
||||
return String(sessionType || '').trim() === 'application' ? '本次出差申请' : '本次费用报销'
|
||||
}
|
||||
|
||||
function sanitizeStewardDelegatedTaskSummary(summary = '', sessionType = '') {
|
||||
const text = String(summary || '').trim()
|
||||
if (String(sessionType || '').trim() !== 'application') {
|
||||
return text
|
||||
}
|
||||
return text
|
||||
.replace(/交通方式和(?:预算|预计)?金额待补充/g, '交通方式待补充')
|
||||
.replace(/出行方式和(?:预算|预计)?金额待补充/g, '出行方式待补充')
|
||||
.replace(/交通方式及(?:预估|预计|预算)?费用/g, '交通方式')
|
||||
.replace(/出行方式及(?:预估|预计|预算)?费用/g, '出行方式')
|
||||
.replace(/(?:费用预算|预算费用|出差费用预算)(?:待补充|待确认|待填写|需补充|需要补充|未补充|缺失)?[,,、;;\s]*/g, '')
|
||||
.replace(/[,,、;;\s]*(?:预估|预计|预算)费用(?:待补充|待确认|待填写|需补充|需要补充|需确认|需要确认|未补充|缺失)?/g, '')
|
||||
.replace(/[,,、;;\s]*(?:预算|预计)?金额(?:待补充|待确认|待填写|需补充|需要补充|未补充|缺失)/g, '')
|
||||
.replace(/([,,、;;。])\1+/g, '$1')
|
||||
.replace(/[,,、;;\s]+。/g, '。')
|
||||
.replace(/[,,、;;\s]+$/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function summarizeApplicationPreviewForSteward(preview = {}) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
return [
|
||||
fields.time ? `时间:${fields.time}` : '',
|
||||
fields.location ? `地点:${fields.location}` : '',
|
||||
fields.reason ? `事由:${fields.reason}` : '',
|
||||
fields.applicationType ? `类型:${fields.applicationType}` : ''
|
||||
].filter(Boolean).join(';')
|
||||
}
|
||||
|
||||
function buildStewardDelegatedThinkingEvents(sessionType = '', continuation = null, context = {}) {
|
||||
const actionLabel = resolveStewardDelegatedActionLabel(sessionType)
|
||||
const eventPrefix = `steward-delegated-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const rawText = String(context.rawText || '').trim()
|
||||
const taskTitle = extractStewardDelegatedTaskTitle(rawText, sessionType)
|
||||
const taskSummary = sanitizeStewardDelegatedTaskSummary(
|
||||
extractStewardCarryLine(rawText, '任务摘要'),
|
||||
sessionType
|
||||
)
|
||||
const identifiedInfo = summarizeApplicationPreviewForSteward(context.applicationPreview)
|
||||
|| extractStewardCarryLine(rawText, '已识别信息')
|
||||
const carryMissingInfo = extractStewardCarryLine(rawText, '还需要补充')
|
||||
const applicationMissingFields = context.applicationPreview
|
||||
? resolveBlockingApplicationMissingFieldsForSteward(context.applicationPreview)
|
||||
: []
|
||||
const missingInfo = applicationMissingFields.length
|
||||
? applicationMissingFields.join('、')
|
||||
: carryMissingInfo
|
||||
const events = [
|
||||
{
|
||||
eventId: `${eventPrefix}-confirm`,
|
||||
title: '接收确认',
|
||||
content: '已收到你的确认,小财管家继续推进当前任务。'
|
||||
eventId: `${eventPrefix}-intent`,
|
||||
title: '理解当前任务',
|
||||
content: taskSummary
|
||||
? `你确认先处理“${taskTitle}”。我把这一步理解为:${taskSummary}。`
|
||||
: `你确认先处理“${taskTitle}”,我会先生成${actionLabel}结果。`
|
||||
},
|
||||
{
|
||||
eventId: `${eventPrefix}-coordinate`,
|
||||
title: '协调能力',
|
||||
content: `正在协调${actionLabel}能力,先生成可核对的结果;这个过程仍由小财管家统一呈现。`
|
||||
eventId: `${eventPrefix}-known`,
|
||||
title: '核对已知信息',
|
||||
content: identifiedInfo
|
||||
? `当前已识别到:${identifiedInfo}。`
|
||||
: `当前先围绕“${taskTitle}”生成可核对内容,具体缺口会在核对结果里继续判断。`
|
||||
}
|
||||
]
|
||||
const applicationMissingFields = context.applicationPreview
|
||||
? resolveApplicationPreviewMissingFieldsForSteward(context.applicationPreview)
|
||||
: []
|
||||
if (applicationMissingFields.length) {
|
||||
if (missingInfo) {
|
||||
const transportMissing = /出行方式/.test(missingInfo)
|
||||
events.push({
|
||||
eventId: `${eventPrefix}-gap`,
|
||||
title: '识别缺口',
|
||||
content: `核对结果还缺少${applicationMissingFields.join('、')},我会先向你追问,不直接推进提交。`
|
||||
title: '判断待补充信息',
|
||||
content: transportMissing
|
||||
? '这一步还没有说明出行方式。出行方式会影响交通费用测算,所以我会先问你选择火车、飞机或轮船,不会直接推进提交。'
|
||||
: `这一步还缺少${missingInfo},我会先向你确认这些信息,不直接推进提交。`
|
||||
})
|
||||
} else {
|
||||
events.push({
|
||||
eventId: `${eventPrefix}-ready`,
|
||||
title: '判断下一步动作',
|
||||
content: `这一步的关键业务信息已形成核对结果。我会先让你检查${actionLabel},确认后再继续入库、生成草稿或处理后续任务。`
|
||||
})
|
||||
}
|
||||
events.push(
|
||||
{
|
||||
eventId: `${eventPrefix}-output`,
|
||||
title: '准备输出',
|
||||
content: '结果准备好后,我会先逐字输出正文,再展示核对表、卡片或确认按钮。'
|
||||
}
|
||||
)
|
||||
return events
|
||||
}
|
||||
|
||||
@@ -257,6 +753,9 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
|
||||
async function typeStewardDelegatedMessage(messageId, finalText, finalExtras = {}, context = {}) {
|
||||
const continuation = finalExtras.stewardContinuation || context.stewardContinuation || null
|
||||
const pendingSuggestedActions = Array.isArray(finalExtras.suggestedActions)
|
||||
? finalExtras.suggestedActions
|
||||
: []
|
||||
const message = messages.value.find((item) => item.id === messageId)
|
||||
if (!message) {
|
||||
return
|
||||
@@ -287,11 +786,12 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
nextTick(scrollToBottom)
|
||||
|
||||
const chars = Array.from(String(eventData.content || ''))
|
||||
for (let index = 0; index < chars.length; index += 1) {
|
||||
for (let index = 0; index < chars.length;) {
|
||||
await waitStewardDelegatedTick(STEWARD_DELEGATED_THINKING_INTERVAL_MS)
|
||||
event.content = chars.slice(0, index + 1).join('')
|
||||
index = Math.min(chars.length, index + STEWARD_DELEGATED_THINKING_CHUNK_SIZE)
|
||||
event.content = chars.slice(0, index).join('')
|
||||
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'streaming')
|
||||
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
||||
if (index % STEWARD_DELEGATED_THINKING_CHUNK_SIZE === 0 || index === chars.length) {
|
||||
nextTick(scrollToBottom)
|
||||
}
|
||||
}
|
||||
@@ -304,14 +804,16 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
const text = String(finalText || '')
|
||||
message.text = ''
|
||||
message.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
||||
message.suggestedActions = pendingSuggestedActions
|
||||
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'typing')
|
||||
const chars = Array.from(text)
|
||||
for (let index = 0; index < chars.length; index += 1) {
|
||||
for (let index = 0; index < chars.length;) {
|
||||
await waitStewardDelegatedTick(STEWARD_DELEGATED_TYPEWRITER_INTERVAL_MS)
|
||||
message.text = chars.slice(0, index + 1).join('')
|
||||
index = Math.min(chars.length, index + STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE)
|
||||
message.text = chars.slice(0, index).join('')
|
||||
message.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
||||
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'typing')
|
||||
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
||||
if (index % STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE === 0 || index === chars.length) {
|
||||
nextTick(scrollToBottom)
|
||||
}
|
||||
}
|
||||
@@ -670,7 +1172,12 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
return currentUser.value || user
|
||||
}
|
||||
|
||||
async function buildApplicationPreviewWithModelReview(rawText, businessTimeContext = null, sessionTypeOverride = '') {
|
||||
async function buildApplicationPreviewWithModelReview(
|
||||
rawText,
|
||||
businessTimeContext = null,
|
||||
sessionTypeOverride = '',
|
||||
options = {}
|
||||
) {
|
||||
const user = await resolveApplicationPreviewUser()
|
||||
const localPreview = applyApplicationBusinessTimeContext(
|
||||
buildLocalApplicationPreview(rawText, user),
|
||||
@@ -697,6 +1204,16 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.skipModelReview) {
|
||||
return {
|
||||
applicationPreview: await enrichWithPolicyEstimate({
|
||||
...localPreview,
|
||||
modelReviewStatus: 'skipped'
|
||||
}),
|
||||
meta: ['申请核对预览', '结构化快路径']
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const ontology = await fetchOntologyParse(
|
||||
{
|
||||
@@ -828,7 +1345,7 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
? `新上传 ${fileNames.length} 份票据,请单独建立报销单。`
|
||||
: `我上传了 ${fileNames.length} 份票据,请帮我识别并整理报销建议。`)
|
||||
|
||||
if (shouldUseBudgetCompileReport(rawText, {
|
||||
if (!stewardDelegated && shouldUseBudgetCompileReport(rawText, {
|
||||
sessionType: effectiveSessionType,
|
||||
entrySource: props.entrySource,
|
||||
budgetContext: props.initialBudgetContext
|
||||
@@ -944,9 +1461,62 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
const { applicationPreview, meta } = await buildApplicationPreviewWithModelReview(
|
||||
rawText,
|
||||
selectedBusinessTimeContext,
|
||||
effectiveSessionType
|
||||
effectiveSessionType,
|
||||
{
|
||||
skipModelReview: Boolean(stewardDelegated && options.skipApplicationModelReview)
|
||||
}
|
||||
)
|
||||
const reviewStatus = String(meta?.[1] || '').trim()
|
||||
let applicationDateConflict = null
|
||||
try {
|
||||
const existingClaims = await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
||||
applicationDateConflict = findOverlappingApplicationClaim(applicationPreview, existingClaims)
|
||||
} catch (error) {
|
||||
console.warn('Failed to check overlapping application dates:', error)
|
||||
}
|
||||
|
||||
if (applicationDateConflict) {
|
||||
const conflictText = buildApplicationDateConflictMessage(applicationDateConflict)
|
||||
const conflictActions = buildApplicationDateConflictActions(applicationDateConflict)
|
||||
if (!stewardDelegated) {
|
||||
completeFlowStep('intent', '已识别为费用申请事项', Date.now() - intentStartedAt)
|
||||
completeFlowStep(
|
||||
'application-review-preview',
|
||||
'检测到同日期已有申请,已停止重复创建',
|
||||
Date.now() - reviewStartedAt
|
||||
)
|
||||
replaceMessage(pendingMessage.id, createMessage(
|
||||
'assistant',
|
||||
conflictText,
|
||||
[],
|
||||
{
|
||||
meta: ['申请日期冲突'],
|
||||
suggestedActions: conflictActions,
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
}
|
||||
))
|
||||
} else {
|
||||
await typeStewardDelegatedMessage(
|
||||
pendingMessage.id,
|
||||
conflictText,
|
||||
{
|
||||
meta: [STEWARD_ASSISTANT_NAME, '申请日期冲突'],
|
||||
suggestedActions: conflictActions,
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
},
|
||||
{
|
||||
sessionType: effectiveSessionType,
|
||||
rawText,
|
||||
applicationPreview,
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
}
|
||||
)
|
||||
}
|
||||
persistSessionState()
|
||||
nextTick(scrollToBottom)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!stewardDelegated) {
|
||||
completeFlowStep('intent', '已识别为费用申请事项', Date.now() - intentStartedAt)
|
||||
completeFlowStep(
|
||||
@@ -960,16 +1530,43 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
)
|
||||
}
|
||||
if (stewardDelegated) {
|
||||
const fallbackStewardApplicationText = buildStewardApplicationPreviewMessage(
|
||||
applicationPreview,
|
||||
buildLocalApplicationPreviewMessage(applicationPreview)
|
||||
)
|
||||
const localPauseForMissingFields = shouldPauseStewardApplicationPreview(applicationPreview)
|
||||
const shouldFetchSlotDecision = localPauseForMissingFields && !options.skipStewardSlotDecision
|
||||
const slotDecision = shouldFetchSlotDecision
|
||||
? await fetchStewardApplicationSlotDecision(
|
||||
applicationPreview,
|
||||
rawText,
|
||||
options.stewardContinuation || null
|
||||
)
|
||||
: null
|
||||
const slotDecisionActions = buildStewardSlotDecisionSuggestedActions(slotDecision, applicationPreview)
|
||||
const pauseForMissingFields = slotDecision
|
||||
? String(slotDecision.next_action || '').trim() === 'ask_user'
|
||||
: localPauseForMissingFields
|
||||
const stewardApplicationText = buildStewardSlotDecisionMessage(
|
||||
slotDecision,
|
||||
applicationPreview,
|
||||
fallbackStewardApplicationText
|
||||
)
|
||||
await typeStewardDelegatedMessage(
|
||||
pendingMessage.id,
|
||||
buildLocalApplicationPreviewMessage(applicationPreview),
|
||||
stewardApplicationText,
|
||||
{
|
||||
meta,
|
||||
applicationPreview,
|
||||
applicationPreview: pauseForMissingFields ? null : applicationPreview,
|
||||
suggestedActions: slotDecisionActions.length
|
||||
? slotDecisionActions
|
||||
: buildStewardApplicationPreviewSuggestedActions(applicationPreview),
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
},
|
||||
{
|
||||
sessionType: effectiveSessionType,
|
||||
rawText,
|
||||
applicationPreview,
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
}
|
||||
)
|
||||
@@ -1478,6 +2075,8 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
},
|
||||
{
|
||||
sessionType: effectiveSessionType,
|
||||
rawText,
|
||||
fileNames: effectiveFileNames,
|
||||
stewardContinuation: options.stewardContinuation || null
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user