feat(web): add OCR service and update travel reimbursement view
- web/src/services/ocr.js: add OCR service API client - web/src/views/scripts/TravelReimbursementCreateView.js: update travel form script with OCR integration
This commit is contained in:
14
web/src/services/ocr.js
Normal file
14
web/src/services/ocr.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { apiRequest } from './api.js'
|
||||||
|
|
||||||
|
export function recognizeOcrFiles(files) {
|
||||||
|
const formData = new FormData()
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', file)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRequest('/ocr/recognize', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
contentType: null
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useSystemState } from '../../composables/useSystemState.js'
|
import { useSystemState } from '../../composables/useSystemState.js'
|
||||||
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
||||||
import { runOrchestrator } from '../../services/orchestrator.js'
|
import { runOrchestrator } from '../../services/orchestrator.js'
|
||||||
|
|
||||||
const DEFAULT_REQUEST = {
|
const DEFAULT_REQUEST = {
|
||||||
@@ -128,6 +129,26 @@ function buildMessageMeta(payload, fileNames = []) {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOcrDocuments(payload) {
|
||||||
|
const documents = Array.isArray(payload?.documents) ? payload.documents : []
|
||||||
|
return documents.slice(0, 5).map((item) => ({
|
||||||
|
filename: item.filename,
|
||||||
|
summary: item.summary,
|
||||||
|
text: String(item.text || '').slice(0, 240),
|
||||||
|
avg_score: Number(item.avg_score || 0),
|
||||||
|
line_count: Number(item.line_count || 0),
|
||||||
|
warnings: Array.isArray(item.warnings) ? item.warnings : []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOcrSummary(payload) {
|
||||||
|
const parts = normalizeOcrDocuments(payload)
|
||||||
|
.map((item) => `${item.filename}:${item.summary || item.text}`)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return parts.join(';')
|
||||||
|
}
|
||||||
|
|
||||||
function buildWelcomeInsight(entrySource, linkedRequest) {
|
function buildWelcomeInsight(entrySource, linkedRequest) {
|
||||||
return {
|
return {
|
||||||
intent: 'welcome',
|
intent: 'welcome',
|
||||||
@@ -360,7 +381,7 @@ export default {
|
|||||||
submitComposer()
|
submitComposer()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBackendMessage(rawText, fileNames) {
|
function buildBackendMessage(rawText, fileNames, ocrSummary = '') {
|
||||||
const parts = []
|
const parts = []
|
||||||
const normalizedText = String(rawText || '').trim()
|
const normalizedText = String(rawText || '').trim()
|
||||||
|
|
||||||
@@ -374,6 +395,10 @@ export default {
|
|||||||
parts.push(`附件名称:${fileNames.join('、')}`)
|
parts.push(`附件名称:${fileNames.join('、')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ocrSummary) {
|
||||||
|
parts.push(`OCR摘要:${ocrSummary}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (props.entrySource === 'detail') {
|
if (props.entrySource === 'detail') {
|
||||||
parts.push(`关联单号:${linkedRequest.value.id}`)
|
parts.push(`关联单号:${linkedRequest.value.id}`)
|
||||||
}
|
}
|
||||||
@@ -389,7 +414,6 @@ export default {
|
|||||||
const fileNames = files.map((file) => file.name)
|
const fileNames = files.map((file) => file.name)
|
||||||
const userText =
|
const userText =
|
||||||
rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
|
rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
|
||||||
const backendMessage = buildBackendMessage(rawText, fileNames)
|
|
||||||
|
|
||||||
messages.value.push(createMessage('user', userText, fileNames))
|
messages.value.push(createMessage('user', userText, fileNames))
|
||||||
|
|
||||||
@@ -409,6 +433,21 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const user = currentUser.value || {}
|
const user = currentUser.value || {}
|
||||||
|
let ocrPayload = null
|
||||||
|
let ocrSummary = ''
|
||||||
|
let ocrDocuments = []
|
||||||
|
|
||||||
|
if (files.length) {
|
||||||
|
try {
|
||||||
|
ocrPayload = await recognizeOcrFiles(files)
|
||||||
|
ocrSummary = buildOcrSummary(ocrPayload)
|
||||||
|
ocrDocuments = normalizeOcrDocuments(ocrPayload)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('OCR request failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendMessage = buildBackendMessage(rawText, fileNames, ocrSummary)
|
||||||
const payload = await runOrchestrator({
|
const payload = await runOrchestrator({
|
||||||
source: 'user_message',
|
source: 'user_message',
|
||||||
user_id: user.username || user.name || 'anonymous',
|
user_id: user.username || user.name || 'anonymous',
|
||||||
@@ -421,7 +460,9 @@ export default {
|
|||||||
entry_source: props.entrySource,
|
entry_source: props.entrySource,
|
||||||
request_context: linkedRequest.value,
|
request_context: linkedRequest.value,
|
||||||
attachment_names: fileNames,
|
attachment_names: fileNames,
|
||||||
attachment_count: fileNames.length
|
attachment_count: fileNames.length,
|
||||||
|
ocr_summary: ocrSummary,
|
||||||
|
ocr_documents: ocrDocuments
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user