refactor: update orchestrator service and travel form view

- services/orchestrator.py: update orchestrator service
- views/TravelReimbursementCreateView.vue: update travel form view
- views/scripts/TravelReimbursementCreateView.js: update travel form script
This commit is contained in:
caoxiaozhu
2026-05-13 15:40:41 +00:00
parent f804a23239
commit edb484e2f6
3 changed files with 353 additions and 3 deletions

View File

@@ -120,6 +120,100 @@
</div>
</div>
<div
v-if="message.role === 'assistant' && !message.reviewPayload && message.queryPayload?.resultType === 'expense_claim_list'"
class="message-detail-block expense-query-block"
>
<strong>{{ message.queryPayload.recentWindowApplied ? '近 10 日单据' : '单据明细' }}</strong>
<p v-if="buildExpenseQueryWindowLabel(message.queryPayload)" class="expense-query-window-label">
{{ buildExpenseQueryWindowLabel(message.queryPayload) }}
</p>
<div v-if="message.queryPayload.statusGroups?.length" class="expense-query-summary-row">
<span
v-for="item in message.queryPayload.statusGroups"
:key="`${message.id}-${item.key}`"
class="expense-query-summary-chip"
:class="item.key"
>
{{ item.label }} {{ item.count }}
</span>
</div>
<div v-if="message.queryPayload.records?.length" class="expense-query-record-list compact">
<button
v-for="record in getExpenseQueryVisibleRecords(message.queryPayload)"
:key="`${message.id}-${record.claimId}`"
type="button"
class="expense-query-record-card"
@click="openExpenseQueryRecord(record)"
>
<div class="expense-query-record-main">
<div class="expense-query-record-top">
<strong>{{ record.claimNo }}</strong>
<span class="expense-query-record-status" :class="record.statusGroup || 'other'">
{{ record.statusLabel }}
</span>
</div>
<p>{{ record.summary }}</p>
<div class="expense-query-record-meta">
<span>{{ record.expenseTypeLabel }}</span>
<span>{{ record.dateDisplay }}</span>
<span>{{ record.amountDisplay }}</span>
</div>
</div>
<i class="mdi mdi-chevron-right"></i>
</button>
<div
v-if="getExpenseQueryTotalPages(message.queryPayload) > 1"
class="expense-query-pager"
>
<button
type="button"
class="expense-query-pager-btn"
:disabled="getExpenseQueryActivePage(message.queryPayload) === 1"
aria-label="上一页"
@click="shiftExpenseQueryPage(message, -1)"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<div class="expense-query-pager-dots" aria-label="单据分页">
<button
v-for="page in getExpenseQueryTotalPages(message.queryPayload)"
:key="`${message.id}-query-page-${page}`"
type="button"
class="expense-query-pager-dot"
:class="{ active: getExpenseQueryActivePage(message.queryPayload) === page }"
:aria-label="` ${page} `"
@click="setExpenseQueryPage(message, page)"
></button>
</div>
<button
type="button"
class="expense-query-pager-btn"
:disabled="getExpenseQueryActivePage(message.queryPayload) === getExpenseQueryTotalPages(message.queryPayload)"
aria-label="下一页"
@click="shiftExpenseQueryPage(message, 1)"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
</div>
<div v-else class="expense-query-empty">
<i class="mdi mdi-file-search-outline"></i>
<span>当前没有可直接展开的近期待办单据</span>
</div>
<p v-if="buildExpenseQueryHint(message.queryPayload)" class="expense-query-hint">
{{ buildExpenseQueryHint(message.queryPayload) }}
</p>
</div>
<div v-if="message.role === 'assistant' && message.reviewPayload" class="message-detail-block review-message-block">
<div class="review-card-shell">
<div class="review-card-head">

View File

@@ -1,4 +1,5 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import { useSystemState } from '../../composables/useSystemState.js'
@@ -151,6 +152,7 @@ const MAX_ATTACHMENTS = 10
const MAX_OCR_DOCUMENTS = 10
const VISIBLE_ATTACHMENT_CHIPS = 2
const COMPOSER_MAX_ROWS = 5
const EXPENSE_QUERY_PAGE_SIZE = 5
const SESSION_TYPE_EXPENSE = 'expense'
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const HOT_KNOWLEDGE_QUESTIONS = [
@@ -199,6 +201,7 @@ function createMessage(role, text, attachments = [], extras = {}) {
meta: [],
citations: [],
suggestedActions: [],
queryPayload: null,
draftPayload: null,
reviewPayload: null,
riskFlags: [],
@@ -625,8 +628,50 @@ function resolveKnowledgeRankTone(index) {
return 'default'
}
function parseConversationMessageSequence(message) {
const messageJson = message?.message_json || message?.messageJson || {}
const sequence = Number.parseInt(messageJson?.sequence, 10)
return Number.isFinite(sequence) && sequence > 0 ? sequence : null
}
function parseConversationMessageTime(message) {
const rawValue = message?.created_at || message?.createdAt || ''
const timestamp = new Date(rawValue).getTime()
return Number.isFinite(timestamp) ? timestamp : Number.MAX_SAFE_INTEGER
}
function resolveConversationMessageRolePriority(message) {
return String(message?.role || '').trim() === 'user' ? 0 : 1
}
function sortConversationMessages(messages) {
return [...(Array.isArray(messages) ? messages : [])].sort((left, right) => {
const leftSequence = parseConversationMessageSequence(left)
const rightSequence = parseConversationMessageSequence(right)
if (leftSequence !== null && rightSequence !== null && leftSequence !== rightSequence) {
return leftSequence - rightSequence
}
const timeDiff = parseConversationMessageTime(left) - parseConversationMessageTime(right)
if (timeDiff !== 0) {
return timeDiff
}
const leftRunId = String(left?.run_id || left?.runId || '').trim()
const rightRunId = String(right?.run_id || right?.runId || '').trim()
if (leftRunId && rightRunId && leftRunId === rightRunId) {
const roleDiff = resolveConversationMessageRolePriority(left) - resolveConversationMessageRolePriority(right)
if (roleDiff !== 0) {
return roleDiff
}
}
return String(left?.id || '').localeCompare(String(right?.id || ''))
})
}
function normalizeInitialConversationMessages(conversation) {
const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : []
const rawMessages = sortConversationMessages(conversation?.messages)
return rawMessages.map((item) => {
const messageJson = item?.message_json || item?.messageJson || {}
@@ -645,6 +690,7 @@ function normalizeInitialConversationMessages(conversation) {
item.role === 'assistant' && Array.isArray(result?.suggested_actions)
? result.suggested_actions
: [],
queryPayload: item.role === 'assistant' ? normalizeExpenseQueryPayload(result?.query_payload) : null,
draftPayload: item.role === 'assistant' ? result?.draft_payload || messageJson?.draft_payload || null : null,
reviewPayload: item.role === 'assistant' ? result?.review_payload || null : null,
riskFlags: item.role === 'assistant' && Array.isArray(result?.risk_flags) ? result.risk_flags : []
@@ -909,6 +955,159 @@ function formatAmountDisplay(value) {
}).format(amount)
}
function normalizeExpenseQueryStatusGroup(item) {
if (!item || typeof item !== 'object') {
return null
}
const rawCount = Number(item.count || 0)
return {
key: String(item.key || 'other').trim() || 'other',
label: String(item.label || '其他状态').trim() || '其他状态',
count: Number.isFinite(rawCount) ? Math.max(0, rawCount) : 0
}
}
function normalizeExpenseQueryRecord(item) {
if (!item || typeof item !== 'object') {
return null
}
const amount = Number(item.amount || 0)
const amountValue = Number.isFinite(amount) ? amount : 0
const expenseTypeLabel = String(item.expense_type_label || item.expense_type || '报销').trim() || '报销'
const reason = String(item.reason || '').trim()
const documentDate = String(item.document_date || '').trim()
const occurredAt = String(item.occurred_at || '').trim()
return {
claimId: String(item.claim_id || '').trim(),
claimNo: String(item.claim_no || '').trim() || '未编号',
employeeName: String(item.employee_name || '').trim(),
expenseType: String(item.expense_type || '').trim(),
expenseTypeLabel,
amount: amountValue,
amountDisplay: formatAmountDisplay(amountValue),
status: String(item.status || '').trim(),
statusLabel: String(item.status_label || '处理中').trim() || '处理中',
statusGroup: String(item.status_group || 'other').trim() || 'other',
statusGroupLabel: String(item.status_group_label || '其他状态').trim() || '其他状态',
approvalStage: String(item.approval_stage || '').trim(),
documentDate,
occurredAt,
reason,
location: String(item.location || '').trim(),
summary: reason || `${expenseTypeLabel}报销`,
dateDisplay: documentDate || occurredAt || '待补充日期'
}
}
function normalizeExpenseQueryPayload(payload) {
if (!payload || typeof payload !== 'object') {
return null
}
const resultType = String(payload.result_type || '').trim()
if (resultType && resultType !== 'expense_claim_list') {
return null
}
const records = (Array.isArray(payload.records) ? payload.records : [])
.map(normalizeExpenseQueryRecord)
.filter(Boolean)
const statusGroups = (Array.isArray(payload.status_groups) ? payload.status_groups : [])
.map(normalizeExpenseQueryStatusGroup)
.filter(Boolean)
const rawRecordCount = Number(payload.record_count || 0)
const rawPreviewCount = Number(payload.preview_count || records.length)
const rawOlderRecordCount = Number(payload.older_record_count || 0)
const totalAmount = Number(payload.total_amount || 0)
const rawWindowDays = Number(payload.window_days || 0)
const windowStartDate = String(payload.window_start_date || '').trim()
const windowEndDate = String(payload.window_end_date || '').trim()
return {
resultType: 'expense_claim_list',
scopeLabel: String(payload.scope_label || '报销单').trim() || '报销单',
recentWindowApplied: Boolean(payload.recent_window_applied),
windowDays:
payload.window_days === null || payload.window_days === undefined || payload.window_days === ''
? null
: (Number.isFinite(rawWindowDays) ? Math.max(1, rawWindowDays) : null),
windowStartDate: windowStartDate || '',
windowEndDate: windowEndDate || '',
recordCount: Number.isFinite(rawRecordCount) ? Math.max(0, rawRecordCount) : 0,
previewCount: Number.isFinite(rawPreviewCount) ? Math.max(0, rawPreviewCount) : records.length,
olderRecordCount: Number.isFinite(rawOlderRecordCount) ? Math.max(0, rawOlderRecordCount) : 0,
hasMoreInWindow: Boolean(payload.has_more_in_window || payload.has_more),
totalAmount: Number.isFinite(totalAmount) ? totalAmount : 0,
statusGroups,
records,
currentPage: 1
}
}
function buildExpenseQueryWindowLabel(queryPayload) {
if (!queryPayload) {
return ''
}
if (queryPayload.windowStartDate && queryPayload.windowEndDate) {
return `${queryPayload.windowStartDate}${queryPayload.windowEndDate}`
}
if (queryPayload.recentWindowApplied && queryPayload.windowDays) {
return `${queryPayload.windowDays} 日内`
}
return '当前条件下'
}
function getExpenseQueryTotalPages(queryPayload) {
const recordCount = Array.isArray(queryPayload?.records) ? queryPayload.records.length : 0
return Math.max(1, Math.ceil(recordCount / EXPENSE_QUERY_PAGE_SIZE))
}
function getExpenseQueryActivePage(queryPayload) {
const totalPages = getExpenseQueryTotalPages(queryPayload)
const rawPage = Number(queryPayload?.currentPage || 1)
if (!Number.isFinite(rawPage)) {
return 1
}
return Math.min(Math.max(1, Math.round(rawPage)), totalPages)
}
function getExpenseQueryVisibleRecords(queryPayload) {
const records = Array.isArray(queryPayload?.records) ? queryPayload.records : []
const activePage = getExpenseQueryActivePage(queryPayload)
const start = (activePage - 1) * EXPENSE_QUERY_PAGE_SIZE
return records.slice(start, start + EXPENSE_QUERY_PAGE_SIZE)
}
function buildExpenseQueryHint(queryPayload) {
if (!queryPayload) {
return ''
}
const parts = []
const windowText = buildExpenseQueryWindowLabel(queryPayload)
if (Array.isArray(queryPayload.records) && queryPayload.records.length > EXPENSE_QUERY_PAGE_SIZE) {
parts.push(`当前共整理 ${queryPayload.records.length} 笔单据,可左右切换查看`)
}
if (queryPayload.hasMoreInWindow && queryPayload.previewCount < queryPayload.recordCount) {
parts.push(`${windowText}${queryPayload.recordCount} 笔,当前先整理最近 ${queryPayload.previewCount}`)
}
if (queryPayload.olderRecordCount > 0 && queryPayload.windowDays) {
parts.push(`另有 ${queryPayload.olderRecordCount} 笔超过 ${queryPayload.windowDays} 日的单据,请前往个人报销中心查看`)
}
return parts.join('。')
}
function countReviewPendingItems(reviewPayload) {
return resolveReviewMissingSlotCards(reviewPayload).length
}
@@ -1661,6 +1860,7 @@ function buildErrorInsight(error, fileNames = []) {
fileNames,
citations: [],
suggestedActions: [],
queryPayload: null,
draftPayload: null,
reviewPayload: null,
riskFlags: [],
@@ -1699,6 +1899,7 @@ function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
fileNames,
citations: Array.isArray(result?.citations) ? result.citations : [],
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
queryPayload: normalizeExpenseQueryPayload(result?.query_payload),
draftPayload: result?.draft_payload || null,
reviewPayload: result?.review_payload || null,
riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [],
@@ -1743,6 +1944,7 @@ export default {
},
emits: ['close', 'draft-saved'],
setup(props, { emit }) {
const router = useRouter()
const { currentUser } = useSystemState()
const { toast } = useToast()
@@ -1816,7 +2018,7 @@ export default {
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
}
return '例如:查一下本周报销金额、解释酒店超标风险,或根据附件生成报销草稿。'
return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件生成报销草稿。'
})
const currentIntentLabel = computed(() => {
if (isKnowledgeSession.value && currentInsight.value.intent === 'welcome') {
@@ -2395,6 +2597,37 @@ export default {
emit('close')
}
function openExpenseQueryRecord(record) {
const claimId = String(record?.claimId || '').trim()
if (!claimId) {
return
}
router.push({
name: 'app-request-detail',
params: { requestId: claimId }
})
emit('close')
}
function setExpenseQueryPage(message, page) {
if (!message?.queryPayload) {
return
}
const totalPages = getExpenseQueryTotalPages(message.queryPayload)
const nextPage = Math.min(Math.max(1, Number(page || 1)), totalPages)
message.queryPayload.currentPage = nextPage
}
function shiftExpenseQueryPage(message, delta) {
if (!message?.queryPayload) {
return
}
setExpenseQueryPage(message, getExpenseQueryActivePage(message.queryPayload) + Number(delta || 0))
}
function openDeleteSessionDialog() {
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || sessionSwitchBusy.value) {
return
@@ -2637,6 +2870,7 @@ export default {
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
? payload.result.suggested_actions
: [],
queryPayload: normalizeExpenseQueryPayload(payload?.result?.query_payload),
draftPayload: payload?.result?.draft_payload || null,
reviewPayload: payload?.result?.review_payload || null,
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
@@ -2886,6 +3120,11 @@ export default {
buildReviewRiskHint,
buildReviewActionHint,
buildReviewStatusTag,
buildExpenseQueryWindowLabel,
buildExpenseQueryHint,
getExpenseQueryActivePage,
getExpenseQueryTotalPages,
getExpenseQueryVisibleRecords,
resolveDocumentPreview,
triggerFileUpload,
handleFilesChange,
@@ -2899,6 +3138,9 @@ export default {
removeAttachedFile,
clearAttachedFiles,
requestCloseWorkbench,
openExpenseQueryRecord,
setExpenseQueryPage,
shiftExpenseQueryPage,
openDeleteSessionDialog,
closeDeleteSessionDialog,
confirmDeleteCurrentSession,