Files
X-Financial/web/src/composables/workbenchAiMode/workbenchAiMessageModel.js

196 lines
6.2 KiB
JavaScript
Raw Normal View History

export const AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION = 'confirm_ai_attachment_association'
export const AI_ATTACHMENT_OCR_DETAIL_ACTION = 'show_ai_attachment_ocr_details'
function normalizeParagraphs(content) {
return String(content || '')
.split(/\n{2,}|\n/)
.map((item) => item.trim())
.filter(Boolean)
}
function stripInlineAssociationMarkdown(value = '') {
return String(value || '')
.replace(/\*\*/g, '')
.replace(/`/g, '')
.trim()
}
export function resolveLegacyAiAttachmentAssociationPayload(content = '') {
const text = String(content || '')
if (!/我已先识别票据,并(?:匹配到最可能的报销单|找到一张可能关联的报销单)/.test(text)) {
return null
}
const claimNo = stripInlineAssociationMarkdown(
text.match(/推荐关联[:]\s*([^\n]+)/u)?.[1] || ''
)
if (!claimNo) {
return null
}
return {
claim_no: claimNo,
document_type: 'expense'
}
}
export function hydrateInlineAttachmentAssociationSuggestedActions(actions = [], content = '') {
const safeActions = Array.isArray(actions) ? actions : []
const hasConfirmAction = safeActions.some(
(action) => String(action?.action_type || '').trim() === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION
)
if (hasConfirmAction) {
return safeActions
}
const payload = resolveLegacyAiAttachmentAssociationPayload(content)
if (!payload) {
return safeActions
}
return [
{
label: '确认自动关联',
description: '将本次票据自动归集到推荐单据。',
icon: 'mdi mdi-link-variant',
action_type: AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION,
payload
},
...safeActions
]
}
function normalizeInlineAttachmentOcrField(field = {}) {
if (!field || typeof field !== 'object') {
return null
}
const value = String(field.value ?? field.text ?? '').trim()
if (!value) {
return null
}
return {
label: String(field.label || field.key || field.name || '识别字段').trim() || '识别字段',
value
}
}
function normalizeInlineAttachmentOcrDocument(document = {}, index = 0) {
const fields = (Array.isArray(document?.document_fields) ? document.document_fields : document?.fields || [])
.map((field) => normalizeInlineAttachmentOcrField(field))
.filter(Boolean)
.slice(0, 12)
const summary = String(document?.summary || document?.text || '').replace(/\s+/g, ' ').trim()
const filename = String(document?.filename || document?.name || '').trim()
if (!filename && !summary && !fields.length) {
return null
}
return {
filename: filename || `附件 ${index + 1}`,
summary,
fields
}
}
export function normalizeInlineAttachmentOcrDetails(details = null) {
if (!details || typeof details !== 'object') {
return null
}
const documents = (Array.isArray(details.documents) ? details.documents : details.ocrDocuments || [])
.map((document, index) => normalizeInlineAttachmentOcrDocument(document, index))
.filter(Boolean)
const fileNames = (Array.isArray(details.fileNames) ? details.fileNames : [])
.map((name) => String(name || '').trim())
.filter(Boolean)
if (!documents.length && !fileNames.length) {
return null
}
return {
fileNames,
documents
}
}
export function buildInlineAttachmentOcrDetails(collected = {}, files = []) {
return normalizeInlineAttachmentOcrDetails({
fileNames: files.map((file) => file?.name || '').filter(Boolean),
documents: collected?.ocrDocuments || []
})
}
export function formatMessageTime(timestamp) {
if (!timestamp) return ''
return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
export function createWorkbenchAiMessageRuntime() {
let messageSeq = 0
function nextMessageId() {
messageSeq += 1
return `${Date.now()}-${messageSeq}`
}
function createAiAttachmentAssociationId() {
messageSeq += 1
return `ai-attachment-${Date.now()}-${messageSeq}`
}
function createInlineMessage(role, content, options = {}) {
const normalizedContent = String(content || '').trim()
const suggestedActions = Array.isArray(options.suggestedActions) ? options.suggestedActions : []
return {
id: options.id || nextMessageId(),
role,
content: normalizedContent,
paragraphs: normalizeParagraphs(normalizedContent),
pending: Boolean(options.pending),
feedback: String(options.feedback || ''),
stewardPlan: options.stewardPlan || null,
suggestedActions: role === 'assistant'
? hydrateInlineAttachmentAssociationSuggestedActions(suggestedActions, normalizedContent)
: suggestedActions,
applicationPreview: options.applicationPreview || null,
draftPayload: options.draftPayload || null,
attachmentOcrDetails: normalizeInlineAttachmentOcrDetails(options.attachmentOcrDetails || null),
text: options.text || normalizedContent,
createdAt: options.createdAt || Date.now()
}
}
function normalizeRuntimeMessage(message = {}) {
return createInlineMessage(message.role || 'assistant', message.content || '', {
id: message.id,
pending: false,
feedback: message.feedback || '',
stewardPlan: message.stewardPlan || null,
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
applicationPreview: message.applicationPreview || null,
draftPayload: message.draftPayload || null,
attachmentOcrDetails: message.attachmentOcrDetails || null,
text: message.text || message.content || ''
})
}
function serializeRuntimeMessage(message = {}) {
return {
id: message.id,
role: message.role,
content: message.content,
text: message.text || message.content || '',
feedback: message.feedback || '',
stewardPlan: message.stewardPlan || null,
suggestedActions: Array.isArray(message.suggestedActions) ? message.suggestedActions : [],
applicationPreview: message.applicationPreview || null,
draftPayload: message.draftPayload || null,
attachmentOcrDetails: message.attachmentOcrDetails || null
}
}
return {
createAiAttachmentAssociationId,
createInlineMessage,
normalizeRuntimeMessage,
serializeRuntimeMessage
}
}