196 lines
6.2 KiB
JavaScript
196 lines
6.2 KiB
JavaScript
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|