- 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 测试
257 lines
8.8 KiB
JavaScript
257 lines
8.8 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,
|
||
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
|
||
}
|
||
}
|