Files
X-Financial/web/src/composables/workbenchAiMode/workbenchAiMessageModel.js
caoxiaozhu 59353308a2 feat(web): AI 意图规划置信度阈值与动作策略细化
- workbenchAiIntentPlannerModel 新增 WORKBENCH_AI_INTENT_CONFIDENCE_THRESHOLD 与 isLowConfidenceTravelApplicationPlan,shouldRequestWorkbenchAiIntentPlan 增加业务关键词前置过滤
- resolveExecutableTravelApplicationPlan 区分 requestedSubmit 与提交确认(submitRequiresConfirmation),autoSubmit 不再直接置真
- workbenchIntentActionPolicy 改用 policyDecision 路由(need_confirmation/query_candidates),透传 riskLevel/requiresSelection/requiresConfirmation
- workbenchIntentFrameModel 补充 query 动作识别,usePersonalWorkbenchAiMode/useWorkbenchAiActionRouter/useWorkbenchAiApplicationPreviewFlow 接入低置信度与确认流程
- 更新 intent-planner-model/intent-frame-model/application-gate-model/fast-preview 测试
2026-06-25 10:55:49 +08:00

257 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
requestedSubmit: Boolean(options.requestedSubmit),
submitRequiresConfirmation: Boolean(options.submitRequiresConfirmation),
draftPayload: options.draftPayload || null,
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(options.attachmentAssociationJob || null),
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(options.linkedReimbursementDraftJob || 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,
requestedSubmit: Boolean(message.requestedSubmit),
submitRequiresConfirmation: Boolean(message.submitRequiresConfirmation),
draftPayload: message.draftPayload || null,
attachmentAssociationJob: message.attachmentAssociationJob || null,
linkedReimbursementDraftJob: message.linkedReimbursementDraftJob || 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,
requestedSubmit: Boolean(message.requestedSubmit),
submitRequiresConfirmation: Boolean(message.submitRequiresConfirmation),
draftPayload: message.draftPayload || null,
attachmentAssociationJob: normalizeInlineAttachmentAssociationJob(message.attachmentAssociationJob || null),
linkedReimbursementDraftJob: normalizeInlineLinkedReimbursementDraftJob(message.linkedReimbursementDraftJob || null),
attachmentOcrDetails: message.attachmentOcrDetails || null
}
}
return {
createAiAttachmentAssociationId,
createInlineMessage,
normalizeRuntimeMessage,
serializeRuntimeMessage
}
}
export function normalizeInlineAttachmentAssociationJob(job = null) {
if (!job || typeof job !== 'object') {
return null
}
const jobId = String(job.jobId || job.job_id || '').trim()
if (!jobId) {
return null
}
const status = String(job.status || 'queued').trim() || 'queued'
const receiptIds = (Array.isArray(job.receiptIds) ? job.receiptIds : job.receipt_ids || [])
.map((item) => String(item || '').trim())
.filter(Boolean)
return {
jobId,
status,
message: String(job.message || '').trim(),
receiptIds,
claimId: String(job.claimId || job.claim_id || '').trim(),
claimNo: String(job.claimNo || job.claim_no || '').trim(),
uploadedCount: Number(job.uploadedCount ?? job.uploaded_count ?? 0) || 0,
skippedCount: Number(job.skippedCount ?? job.skipped_count ?? 0) || 0,
error: String(job.error || '').trim()
}
}
export function normalizeInlineLinkedReimbursementDraftJob(job = null) {
if (!job || typeof job !== 'object') {
return null
}
const jobId = String(job.jobId || job.job_id || '').trim()
if (!jobId) {
return null
}
const draftPayload = job.draftPayload && typeof job.draftPayload === 'object'
? job.draftPayload
: job.draft_payload && typeof job.draft_payload === 'object'
? job.draft_payload
: null
return {
jobId,
status: String(job.status || 'queued').trim() || 'queued',
message: String(job.message || '').trim(),
error: String(job.error || '').trim(),
runId: String(job.runId || job.run_id || '').trim(),
applicationClaimNo: String(job.applicationClaimNo || job.application_claim_no || '').trim(),
draftPayload
}
}