- applicationApproval 新增按日期/时间/审批角色拆分格式化,buildLeaderApprovalEvents 补充 dateLabel/timeLabel/roleLabel 字段 - TravelRequestDetailView 领导意见事件改为日期+时间+审批人结构化展示,travel-request-detail-view.css 重构对应样式 - travelReimbursementAttachmentModel 微调附件标识,同步更新 application-approval-info、travel-request-detail-leader-approval、attachment-association-confirmation 测试 - 更新公司通信费报销规则表
675 lines
21 KiB
JavaScript
675 lines
21 KiB
JavaScript
import {
|
||
buildReviewSlotMap,
|
||
resolveDocumentTypeLabel,
|
||
resolveExpenseTypeCode
|
||
} from './travelReimbursementReviewModel.js'
|
||
import { normalizeExpenseQueryPayload } from './travelReimbursementExpenseQueryModel.js'
|
||
|
||
const SCENARIO_LABELS = {
|
||
expense: '报销',
|
||
accounts_receivable: '应收',
|
||
accounts_payable: '应付',
|
||
knowledge: '知识',
|
||
unknown: '通用'
|
||
}
|
||
|
||
const INTENT_LABELS = {
|
||
query: '查询',
|
||
explain: '解释',
|
||
compare: '对比',
|
||
risk_check: '风险检查',
|
||
draft: '信息核对',
|
||
operate: '动作请求'
|
||
}
|
||
|
||
function resolveStatusLabel(status) {
|
||
if (status === 'succeeded') return '已完成'
|
||
if (status === 'blocked') return '已阻断'
|
||
return '处理中'
|
||
}
|
||
|
||
function resolveStatusTone(status) {
|
||
if (status === 'succeeded') return 'success'
|
||
if (status === 'blocked') return 'warning'
|
||
return 'note'
|
||
}
|
||
|
||
export const MAX_ATTACHMENTS = 10
|
||
export const MAX_OCR_DOCUMENTS = 10
|
||
export const VISIBLE_ATTACHMENT_CHIPS = 2
|
||
export const ATTACHMENT_ASSOCIATION_CONFIRM_HREF = '#confirm-attachment-association'
|
||
|
||
export function buildUnsavedDraftAttachmentConfirmationMessage({ fileNames = [] } = {}) {
|
||
const names = (Array.isArray(fileNames) ? fileNames : [])
|
||
.map((item) => String(item || '').trim())
|
||
.filter(Boolean)
|
||
const attachmentLine = names.length
|
||
? `本次待归集附件:${names.length} 份(${names.join('、')})`
|
||
: '本次待归集附件:待识别'
|
||
|
||
return [
|
||
'当前这笔报销信息还没有保存为草稿。',
|
||
'',
|
||
'如果继续上传票据,我需要先把当前已识别的信息保存成一张草稿单据,再识别并归集本次附件。',
|
||
'',
|
||
attachmentLine,
|
||
'',
|
||
'',
|
||
`如果 **[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})**,我会先保存这笔未保存单据,再把此次上传的附件归集到该单据。`
|
||
].join('\n').trim()
|
||
}
|
||
|
||
export function normalizeOcrDocuments(payload) {
|
||
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => ({
|
||
filename: item.filename,
|
||
summary: item.summary,
|
||
text: String(item.text || ''),
|
||
avg_score: Number(item.avg_score || 0),
|
||
line_count: Number(item.line_count || 0),
|
||
document_type: String(item.document_type || 'other').trim() || 'other',
|
||
document_type_label: String(item.document_type_label || '').trim(),
|
||
scene_code: String(item.scene_code || 'other').trim() || 'other',
|
||
scene_label: String(item.scene_label || '').trim(),
|
||
preview_kind: String(item.preview_kind || '').trim(),
|
||
preview_data_url: String(item.preview_data_url || '').trim(),
|
||
preview_url: String(item.preview_url || '').trim(),
|
||
receipt_id: String(item.receipt_id || item.receiptId || '').trim(),
|
||
receipt_status: String(item.receipt_status || item.receiptStatus || '').trim(),
|
||
receipt_preview_url: String(item.receipt_preview_url || item.receiptPreviewUrl || '').trim(),
|
||
receipt_source_url: String(item.receipt_source_url || item.receiptSourceUrl || '').trim(),
|
||
document_fields: Array.isArray(item.document_fields)
|
||
? item.document_fields
|
||
.map((field) => ({
|
||
key: String(field?.key || '').trim(),
|
||
label: String(field?.label || '').trim(),
|
||
value: String(field?.value || '').trim()
|
||
}))
|
||
.filter((field) => field.key && field.label && field.value)
|
||
: [],
|
||
warnings: Array.isArray(item.warnings) ? item.warnings : []
|
||
}))
|
||
}
|
||
|
||
function defineFileReceiptId(file, receiptId) {
|
||
const normalizedReceiptId = String(receiptId || '').trim()
|
||
if (!file || !normalizedReceiptId) {
|
||
return false
|
||
}
|
||
|
||
try {
|
||
Object.defineProperty(file, 'receiptId', {
|
||
value: normalizedReceiptId,
|
||
enumerable: false,
|
||
configurable: true
|
||
})
|
||
return true
|
||
} catch {
|
||
try {
|
||
file.receiptId = normalizedReceiptId
|
||
return String(file.receiptId || '').trim() === normalizedReceiptId
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
export function attachReceiptFolderIdsToFiles(files = [], payload = null) {
|
||
const safeFiles = Array.isArray(files) ? files : []
|
||
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||
let attachedCount = 0
|
||
|
||
safeFiles.slice(0, documents.length).forEach((file, index) => {
|
||
const document = documents[index] || {}
|
||
const receiptId = String(document.receipt_id || document.receiptId || '').trim()
|
||
if (receiptId && defineFileReceiptId(file, receiptId)) {
|
||
attachedCount += 1
|
||
}
|
||
})
|
||
|
||
return attachedCount
|
||
}
|
||
|
||
export async function collectReceiptFiles({
|
||
files = [],
|
||
recognizedAttachmentData = null,
|
||
recognizeOcrFiles,
|
||
timeoutMs = 90000,
|
||
timeoutMessage = '票据 OCR 识别超时,已继续使用附件名称处理。'
|
||
} = {}) {
|
||
const safeFiles = Array.isArray(files) ? files : []
|
||
const reusedData = recognizedAttachmentData && typeof recognizedAttachmentData === 'object'
|
||
? recognizedAttachmentData
|
||
: null
|
||
|
||
if (reusedData) {
|
||
const ocrDocuments = Array.isArray(reusedData.ocrDocuments) ? [...reusedData.ocrDocuments] : []
|
||
const ocrPayload = reusedData.ocrPayload || { documents: ocrDocuments }
|
||
attachReceiptFolderIdsToFiles(safeFiles, ocrPayload)
|
||
return {
|
||
ocrPayload,
|
||
ocrSummary: String(reusedData.ocrSummary || '').trim() || buildOcrSummaryFromDocuments(ocrDocuments),
|
||
ocrDocuments,
|
||
ocrFilePreviews: Array.isArray(reusedData.ocrFilePreviews) ? [...reusedData.ocrFilePreviews] : []
|
||
}
|
||
}
|
||
|
||
if (typeof recognizeOcrFiles !== 'function') {
|
||
throw new Error('票据采集服务未配置。')
|
||
}
|
||
|
||
const ocrPayload = await recognizeOcrFiles(safeFiles, {
|
||
timeoutMs,
|
||
timeoutMessage
|
||
})
|
||
attachReceiptFolderIdsToFiles(safeFiles, ocrPayload)
|
||
|
||
return {
|
||
ocrPayload,
|
||
ocrSummary: buildOcrSummary(ocrPayload),
|
||
ocrDocuments: normalizeOcrDocuments(ocrPayload),
|
||
ocrFilePreviews: buildOcrFilePreviews(ocrPayload)
|
||
}
|
||
}
|
||
|
||
export function buildOcrSummary(payload) {
|
||
return buildOcrSummaryFromDocuments(normalizeOcrDocuments(payload))
|
||
}
|
||
|
||
export function buildOcrSummaryFromDocuments(documents) {
|
||
return (Array.isArray(documents) ? documents : [])
|
||
.slice(0, MAX_OCR_DOCUMENTS)
|
||
.map((item) => {
|
||
const filename = String(item?.filename || '').trim()
|
||
const summary = String(item?.summary || item?.text || '').trim()
|
||
if (filename && summary) {
|
||
return `${filename}:${summary}`
|
||
}
|
||
return filename || summary
|
||
})
|
||
.filter(Boolean)
|
||
.join(';')
|
||
}
|
||
|
||
function resolveAssociationDocumentTypeLabel(document) {
|
||
const explicitLabel = String(document?.document_type_label || '').trim()
|
||
if (explicitLabel) {
|
||
return explicitLabel
|
||
}
|
||
|
||
const sceneLabel = String(document?.scene_label || '').trim()
|
||
if (sceneLabel) {
|
||
return sceneLabel
|
||
}
|
||
|
||
const typeLabel = resolveDocumentTypeLabel(document?.document_type)
|
||
return String(typeLabel || '').trim() || '其他票据'
|
||
}
|
||
|
||
function buildAssociationDocumentContentLines(document) {
|
||
const fields = Array.isArray(document?.document_fields) ? document.document_fields : []
|
||
const fieldLines = fields
|
||
.map((field) => {
|
||
const label = String(field?.label || '').trim()
|
||
const value = String(field?.value || '').trim()
|
||
return label && value ? `- ${label}:${value}` : ''
|
||
})
|
||
.filter(Boolean)
|
||
|
||
if (fieldLines.length) {
|
||
return fieldLines.slice(0, 8)
|
||
}
|
||
|
||
const summary = String(document?.summary || document?.text || '').trim()
|
||
if (summary) {
|
||
return [`- 识别内容:${summary}`]
|
||
}
|
||
|
||
return ['- 识别内容:暂未提取到结构化字段,请以票据原件为准。']
|
||
}
|
||
|
||
function buildAssociationDocumentCard(lines) {
|
||
return (Array.isArray(lines) ? lines : [])
|
||
.map((line) => String(line || '').trim() ? `> ${line}` : '>')
|
||
.join('\n')
|
||
}
|
||
|
||
export function buildAttachmentAssociationConfirmationMessage({
|
||
claimNo = '',
|
||
claimTitle = '',
|
||
fileNames = [],
|
||
ocrDocuments = []
|
||
} = {}) {
|
||
const documents = Array.isArray(ocrDocuments) && ocrDocuments.length
|
||
? ocrDocuments
|
||
: (Array.isArray(fileNames) ? fileNames : [])
|
||
.map((filename) => ({ filename }))
|
||
.filter((item) => String(item.filename || '').trim())
|
||
const targetLines = [
|
||
claimNo ? `- 草稿单号:${claimNo}` : '',
|
||
claimTitle ? `- 单据说明:${claimTitle}` : '',
|
||
`- 本次待归集附件:${documents.length || fileNames.length || 0} 份`
|
||
].filter(Boolean)
|
||
|
||
const documentBlocks = documents.map((document, index) => {
|
||
const filename = String(document?.filename || '').trim() || `附件 ${index + 1}`
|
||
const typeLabel = resolveAssociationDocumentTypeLabel(document)
|
||
const contentLines = buildAssociationDocumentContentLines(document)
|
||
.map((line) => String(line || '').replace(/^-\s*/, ''))
|
||
return buildAssociationDocumentCard([
|
||
`**附件 ${index + 1}:${filename}**`,
|
||
'',
|
||
`附件类型:${typeLabel}`,
|
||
'',
|
||
...contentLines
|
||
])
|
||
})
|
||
|
||
return [
|
||
'已识别附件信息:',
|
||
'',
|
||
documentBlocks.join('\n\n'),
|
||
'',
|
||
'',
|
||
'请问是否确定将票据信息归集到单据:',
|
||
'',
|
||
targetLines.join('\n'),
|
||
'',
|
||
'',
|
||
`如果 **[确认](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})** 该信息,我将直接将票据进行归集。`
|
||
]
|
||
.join('\n')
|
||
.replace(/\n{4,}/g, '\n\n\n')
|
||
.trim()
|
||
}
|
||
|
||
export function normalizeReviewDocumentFieldKey(label) {
|
||
const compact = String(label || '').replace(/\s+/g, '').toLowerCase()
|
||
if (!compact) return ''
|
||
if (
|
||
['金额', '价税合计', '合计', '总额', '总计', '票价', '支付金额', '实付金额', '实收金额'].some((token) =>
|
||
compact.includes(token.toLowerCase())
|
||
)
|
||
) {
|
||
return 'amount'
|
||
}
|
||
if (['日期', '时间', '开票日期', '发生时间'].some((token) => compact.includes(token.toLowerCase()))) {
|
||
return 'date'
|
||
}
|
||
if (['商户', '酒店', '销售方', '开票方', '收款方'].some((token) => compact.includes(token.toLowerCase()))) {
|
||
return 'merchant_name'
|
||
}
|
||
if (['票据号码', '发票号码', '票号', '单号', '订单号'].some((token) => compact.includes(token.toLowerCase()))) {
|
||
return 'invoice_number'
|
||
}
|
||
if (compact.includes('发票代码')) {
|
||
return 'invoice_code'
|
||
}
|
||
if (compact.includes('车次') || compact.includes('航班')) {
|
||
return 'trip_no'
|
||
}
|
||
if (compact.includes('行程') || compact.includes('路线')) {
|
||
return 'route'
|
||
}
|
||
return compact
|
||
}
|
||
|
||
export function buildOcrDocumentsFromReviewPayload(reviewPayload) {
|
||
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => {
|
||
const fields = Array.isArray(item?.fields)
|
||
? item.fields
|
||
.map((field) => {
|
||
const label = String(field?.label || '').trim()
|
||
const value = String(field?.value || '').trim()
|
||
if (!label || !value) {
|
||
return null
|
||
}
|
||
return {
|
||
key: normalizeReviewDocumentFieldKey(label),
|
||
label,
|
||
value
|
||
}
|
||
})
|
||
.filter(Boolean)
|
||
: []
|
||
|
||
return {
|
||
filename: String(item?.filename || '').trim(),
|
||
summary: String(item?.summary || '').trim(),
|
||
text: [
|
||
String(item?.scene_label || '').trim(),
|
||
String(item?.summary || '').trim(),
|
||
...fields.map((field) => `${field.label}:${field.value}`)
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.slice(0, 240),
|
||
avg_score: Number(item?.avg_score || 0),
|
||
document_type: String(item?.document_type || 'other').trim() || 'other',
|
||
document_type_label: resolveDocumentTypeLabel(item?.document_type),
|
||
scene_code: resolveExpenseTypeCode(item?.suggested_expense_type),
|
||
scene_label: String(item?.scene_label || '').trim(),
|
||
preview_kind: String(item?.preview_kind || '').trim(),
|
||
preview_data_url: String(item?.preview_data_url || '').trim(),
|
||
preview_url: String(item?.preview_url || '').trim(),
|
||
document_fields: fields,
|
||
warnings: Array.isArray(item?.warnings) ? item.warnings : []
|
||
}
|
||
}).filter((item) => item.filename)
|
||
}
|
||
|
||
export function mergeUploadAttachmentNames(existingNames, incomingNames) {
|
||
const merged = []
|
||
const seen = new Set()
|
||
|
||
for (const value of [...(existingNames || []), ...(incomingNames || [])]) {
|
||
const normalized = String(value || '').trim()
|
||
if (!normalized || seen.has(normalized)) continue
|
||
seen.add(normalized)
|
||
merged.push(normalized)
|
||
if (merged.length >= MAX_ATTACHMENTS) {
|
||
break
|
||
}
|
||
}
|
||
|
||
return merged
|
||
}
|
||
|
||
export function mergeUploadOcrDocuments(existingDocuments, incomingDocuments) {
|
||
const merged = []
|
||
const seen = new Set()
|
||
|
||
for (const item of [...(existingDocuments || []), ...(incomingDocuments || [])]) {
|
||
const filename = String(item?.filename || '').trim()
|
||
if (!filename || seen.has(filename)) continue
|
||
seen.add(filename)
|
||
merged.push(item)
|
||
if (merged.length >= MAX_OCR_DOCUMENTS) {
|
||
break
|
||
}
|
||
}
|
||
|
||
return merged
|
||
}
|
||
|
||
export function inferPreviewKind(file) {
|
||
const mediaType = String(file?.type || '').toLowerCase()
|
||
const filename = String(file?.name || '').toLowerCase()
|
||
if (mediaType.startsWith('image/') || /\.(png|jpg|jpeg|webp|bmp)$/i.test(filename)) {
|
||
return 'image'
|
||
}
|
||
if (mediaType.includes('pdf') || /\.pdf$/i.test(filename)) {
|
||
return 'pdf'
|
||
}
|
||
return 'file'
|
||
}
|
||
|
||
export function buildFilePreviews(files, previewRegistry) {
|
||
return files.map((file) => {
|
||
const kind = inferPreviewKind(file)
|
||
if (!['image', 'pdf'].includes(kind)) {
|
||
return {
|
||
filename: file.name,
|
||
kind
|
||
}
|
||
}
|
||
|
||
const url = URL.createObjectURL(file)
|
||
previewRegistry.push(url)
|
||
return {
|
||
filename: file.name,
|
||
kind,
|
||
url
|
||
}
|
||
})
|
||
}
|
||
|
||
export function resolveDocumentPreview(filePreviews, filename) {
|
||
if (!Array.isArray(filePreviews)) return null
|
||
const matches = filePreviews.filter((item) => item.filename === filename)
|
||
if (!matches.length) {
|
||
return null
|
||
}
|
||
return (
|
||
matches.find((item) => item.kind === 'image' && item.url) ||
|
||
matches.find((item) => item.url) ||
|
||
matches[0]
|
||
)
|
||
}
|
||
|
||
export function isTemporaryPreviewUrl(url) {
|
||
return String(url || '').trim().toLowerCase().startsWith('blob:')
|
||
}
|
||
|
||
export function buildFileIdentity(file) {
|
||
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
|
||
}
|
||
|
||
export function mergeFilesWithLimit(existingFiles, incomingFiles, limit = MAX_ATTACHMENTS) {
|
||
const nextFiles = []
|
||
const seen = new Set()
|
||
|
||
for (const file of Array.isArray(existingFiles) ? existingFiles : []) {
|
||
const key = buildFileIdentity(file)
|
||
if (seen.has(key)) continue
|
||
seen.add(key)
|
||
nextFiles.push(file)
|
||
}
|
||
|
||
let duplicateCount = 0
|
||
let overflowCount = 0
|
||
|
||
for (const file of Array.isArray(incomingFiles) ? incomingFiles : []) {
|
||
const key = buildFileIdentity(file)
|
||
if (seen.has(key)) {
|
||
duplicateCount += 1
|
||
continue
|
||
}
|
||
if (nextFiles.length >= limit) {
|
||
overflowCount += 1
|
||
continue
|
||
}
|
||
seen.add(key)
|
||
nextFiles.push(file)
|
||
}
|
||
|
||
return {
|
||
files: nextFiles,
|
||
duplicateCount,
|
||
overflowCount
|
||
}
|
||
}
|
||
|
||
export function mergeFilePreviews(existingPreviews, incomingPreviews) {
|
||
const result = []
|
||
const indexByKey = new Map()
|
||
|
||
for (const preview of [...(existingPreviews || []), ...(incomingPreviews || [])]) {
|
||
const key = [preview?.filename, preview?.kind].join('__')
|
||
if (!preview?.filename) continue
|
||
|
||
const existingIndex = indexByKey.get(key)
|
||
if (existingIndex === undefined) {
|
||
indexByKey.set(key, result.length)
|
||
result.push(preview)
|
||
continue
|
||
}
|
||
|
||
const existingPreview = result[existingIndex]
|
||
const nextUrl = String(preview?.url || '').trim()
|
||
const existingUrl = String(existingPreview?.url || '').trim()
|
||
if (nextUrl && (!existingUrl || isTemporaryPreviewUrl(existingUrl) || nextUrl !== existingUrl)) {
|
||
result[existingIndex] = preview
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
export function filterPersistableFilePreviews(filePreviews) {
|
||
return (Array.isArray(filePreviews) ? filePreviews : [])
|
||
.filter((preview) => {
|
||
const filename = String(preview?.filename || '').trim()
|
||
const url = String(preview?.url || '').trim()
|
||
return filename && !isTemporaryPreviewUrl(url)
|
||
})
|
||
}
|
||
|
||
function inferPreviewKindFromUrl(url) {
|
||
const normalized = String(url || '').trim().toLowerCase()
|
||
if (!normalized) return ''
|
||
if (normalized.startsWith('data:image/') || /\.(png|jpg|jpeg|webp|bmp)(?:[?#].*)?$/i.test(normalized)) {
|
||
return 'image'
|
||
}
|
||
if (normalized.startsWith('data:application/pdf') || /\.pdf(?:[?#].*)?$/i.test(normalized)) {
|
||
return 'pdf'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function resolveDocumentPreviewKind(item) {
|
||
const explicit = String(item?.preview_kind || '').trim()
|
||
if (explicit) {
|
||
return explicit
|
||
}
|
||
return inferPreviewKindFromUrl(String(item?.preview_url || item?.preview_data_url || '').trim())
|
||
}
|
||
|
||
export function buildOcrFilePreviews(payload) {
|
||
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||
return documents
|
||
.map((item) => ({
|
||
filename: String(item?.filename || '').trim(),
|
||
kind: resolveDocumentPreviewKind(item),
|
||
url: String(item?.preview_url || item?.preview_data_url || '').trim()
|
||
}))
|
||
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
||
}
|
||
|
||
export function buildReviewFilePreviewsFromReviewPayload(reviewPayload) {
|
||
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||
return documents
|
||
.map((item) => ({
|
||
filename: String(item?.filename || '').trim(),
|
||
kind: resolveDocumentPreviewKind(item),
|
||
url: String(item?.preview_url || item?.preview_data_url || '').trim()
|
||
}))
|
||
.filter((item) => item.filename && item.kind === 'image' && item.url)
|
||
}
|
||
|
||
export function buildReviewFilePreviewsFromMessages(messages) {
|
||
const previews = []
|
||
for (const message of Array.isArray(messages) ? messages : []) {
|
||
previews.push(...buildReviewFilePreviewsFromReviewPayload(message?.reviewPayload))
|
||
}
|
||
return mergeFilePreviews([], previews)
|
||
}
|
||
|
||
export function resolveAttachmentPreviewKind(metadata) {
|
||
const explicitKind = String(metadata?.preview_kind || '').trim()
|
||
if (explicitKind) {
|
||
return explicitKind
|
||
}
|
||
|
||
const mediaType = String(metadata?.media_type || '').trim().toLowerCase()
|
||
if (mediaType.startsWith('image/')) {
|
||
return 'image'
|
||
}
|
||
if (mediaType === 'application/pdf') {
|
||
return 'pdf'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
export function extractReviewAttachmentNames(reviewPayload) {
|
||
const documentNames = Array.isArray(reviewPayload?.document_cards)
|
||
? reviewPayload.document_cards.map((item) => String(item?.filename || '').trim()).filter(Boolean)
|
||
: []
|
||
if (documentNames.length) {
|
||
return documentNames
|
||
}
|
||
|
||
const slotMap = buildReviewSlotMap(reviewPayload)
|
||
const attachmentValue = String(slotMap.attachments?.value || '').trim()
|
||
if (!attachmentValue) {
|
||
return []
|
||
}
|
||
|
||
return attachmentValue.split(/[、,,]/).map((item) => item.trim()).filter(Boolean)
|
||
}
|
||
|
||
export function buildErrorInsight(error, fileNames = []) {
|
||
return {
|
||
intent: 'agent',
|
||
metricLabel: '运行状态',
|
||
metricValue: '失败',
|
||
title: '智能体调用失败',
|
||
summary: error?.message || '无法连接后端 Orchestrator。',
|
||
agent: {
|
||
runId: '未生成',
|
||
selectedAgent: 'orchestrator',
|
||
scenario: '未知',
|
||
intent: '未知',
|
||
permissionLevel: 'unknown',
|
||
routeReason: 'request_failed',
|
||
requiresConfirmation: false,
|
||
degraded: false,
|
||
fileNames,
|
||
citations: [],
|
||
suggestedActions: [],
|
||
queryPayload: null,
|
||
draftPayload: null,
|
||
reviewPayload: null,
|
||
riskFlags: [],
|
||
toolCount: 0,
|
||
failedToolCount: 0,
|
||
selectedCapabilityCodes: [],
|
||
filePreviews: [],
|
||
statusLabel: '失败',
|
||
statusTone: 'note'
|
||
}
|
||
}
|
||
}
|
||
|
||
export function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
|
||
const trace = payload?.trace_summary || {}
|
||
const result = payload?.result || {}
|
||
const statusLabel = resolveStatusLabel(payload?.status)
|
||
|
||
return {
|
||
intent: 'agent',
|
||
metricLabel: '运行状态',
|
||
metricValue: statusLabel,
|
||
title:
|
||
result?.draft_payload?.title ||
|
||
`${SCENARIO_LABELS[trace?.scenario] || '通用'}${INTENT_LABELS[trace?.intent] || '处理'}结果`,
|
||
summary: result?.answer || result?.message || '智能体已完成处理。',
|
||
agent: {
|
||
runId: payload?.run_id || '未生成',
|
||
selectedAgent: payload?.selected_agent || 'orchestrator',
|
||
scenario: SCENARIO_LABELS[trace?.scenario] || trace?.scenario || '未知',
|
||
intent: INTENT_LABELS[trace?.intent] || trace?.intent || '未知',
|
||
permissionLevel: payload?.permission_level || 'unknown',
|
||
routeReason: payload?.route_reason || 'unknown',
|
||
requiresConfirmation: Boolean(payload?.requires_confirmation),
|
||
degraded: Boolean(trace?.degraded),
|
||
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 : [],
|
||
toolCount: Number(trace?.tool_count || 0),
|
||
failedToolCount: Number(trace?.failed_tool_count || 0),
|
||
selectedCapabilityCodes: Array.isArray(trace?.selected_capability_codes)
|
||
? trace.selected_capability_codes
|
||
: [],
|
||
filePreviews,
|
||
statusLabel,
|
||
statusTone: resolveStatusTone(payload?.status)
|
||
}
|
||
}
|
||
}
|