2026-05-12 06:40:19 +00:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
2026-05-12 01:27:49 +00:00
|
|
|
|
|
2026-05-12 07:23:29 +00:00
|
|
|
|
import aiAvatar from '../../assets/header.svg?no-inline'
|
|
|
|
|
|
import userAvatar from '../../assets/person.svg?no-inline'
|
2026-05-12 01:27:49 +00:00
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
2026-05-12 03:05:51 +00:00
|
|
|
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
2026-05-12 01:27:49 +00:00
|
|
|
|
import { runOrchestrator } from '../../services/orchestrator.js'
|
|
|
|
|
|
|
|
|
|
|
|
const SOURCE_LABELS = {
|
|
|
|
|
|
workbench: '来自个人工作台',
|
|
|
|
|
|
topbar: '来自发起报销',
|
|
|
|
|
|
detail: '来自智能录入',
|
|
|
|
|
|
upload: '来自附件上传',
|
|
|
|
|
|
requests: '来自报销列表'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const SCENARIO_LABELS = {
|
|
|
|
|
|
expense: '报销',
|
|
|
|
|
|
accounts_receivable: '应收',
|
|
|
|
|
|
accounts_payable: '应付',
|
|
|
|
|
|
knowledge: '知识',
|
|
|
|
|
|
unknown: '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const INTENT_LABELS = {
|
|
|
|
|
|
query: '查询',
|
|
|
|
|
|
explain: '解释',
|
|
|
|
|
|
compare: '对比',
|
|
|
|
|
|
risk_check: '风险检查',
|
|
|
|
|
|
draft: '草稿生成',
|
|
|
|
|
|
operate: '动作请求'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let messageSeed = 0
|
|
|
|
|
|
|
|
|
|
|
|
function nowTime() {
|
|
|
|
|
|
return new Date().toLocaleTimeString('zh-CN', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createMessage(role, text, attachments = [], extras = {}) {
|
|
|
|
|
|
messageSeed += 1
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: `msg-${messageSeed}`,
|
|
|
|
|
|
role,
|
|
|
|
|
|
text,
|
|
|
|
|
|
attachments,
|
|
|
|
|
|
time: nowTime(),
|
|
|
|
|
|
meta: [],
|
|
|
|
|
|
citations: [],
|
|
|
|
|
|
suggestedActions: [],
|
|
|
|
|
|
draftPayload: null,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
reviewPayload: null,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
riskFlags: [],
|
|
|
|
|
|
...extras
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
function formatMessageTime(value) {
|
|
|
|
|
|
if (!value) {
|
|
|
|
|
|
return nowTime()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parsed = new Date(value)
|
|
|
|
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
|
|
|
|
return nowTime()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parsed.toLocaleTimeString('zh-CN', {
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
|
hour12: false
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function sanitizeRequest(request) {
|
2026-05-12 07:22:11 +00:00
|
|
|
|
if (!request || typeof request !== 'object') return null
|
|
|
|
|
|
|
|
|
|
|
|
const normalized = {
|
|
|
|
|
|
id: String(request.id || '').trim(),
|
|
|
|
|
|
reason: String(request.reason || request.title || '').trim(),
|
|
|
|
|
|
city: String(request.city || request.location || '').trim(),
|
|
|
|
|
|
period: String(request.period || '').trim(),
|
|
|
|
|
|
applyTime: String(request.applyTime || request.occurredAt || '').trim(),
|
|
|
|
|
|
amount: String(request.amount || '').trim(),
|
|
|
|
|
|
node: String(request.node || '').trim(),
|
|
|
|
|
|
approval: String(request.approval || '').trim(),
|
|
|
|
|
|
travel: String(request.travel || '').trim()
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
2026-05-12 07:22:11 +00:00
|
|
|
|
|
|
|
|
|
|
return Object.values(normalized).some(Boolean) ? normalized : null
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStatusLabel(status) {
|
|
|
|
|
|
if (status === 'succeeded') return '已完成'
|
|
|
|
|
|
if (status === 'blocked') return '已阻断'
|
|
|
|
|
|
return '失败'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStatusTone(status) {
|
|
|
|
|
|
if (status === 'succeeded') return 'success'
|
|
|
|
|
|
if (status === 'blocked') return 'warning'
|
|
|
|
|
|
return 'note'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageMeta(payload, fileNames = []) {
|
|
|
|
|
|
const items = []
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.selected_agent) {
|
|
|
|
|
|
items.push(`Agent: ${payload.selected_agent}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.permission_level) {
|
|
|
|
|
|
items.push(`权限: ${payload.permission_level}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.trace_summary?.tool_count) {
|
|
|
|
|
|
items.push(`工具: ${payload.trace_summary.tool_count}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.trace_summary?.degraded) {
|
|
|
|
|
|
items.push('已降级')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.requires_confirmation) {
|
|
|
|
|
|
items.push('待确认')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (payload?.run_id) {
|
|
|
|
|
|
items.push(`Run: ${payload.run_id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fileNames.length) {
|
|
|
|
|
|
items.push(`附件: ${fileNames.length}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
function buildStoredMessageMeta(messageJson, attachmentNames = []) {
|
|
|
|
|
|
const payload = messageJson?.orchestrator_payload
|
|
|
|
|
|
if (payload) {
|
|
|
|
|
|
return buildMessageMeta(payload, attachmentNames)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const items = []
|
|
|
|
|
|
if (messageJson?.status) {
|
|
|
|
|
|
items.push(`状态: ${messageJson.status}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (attachmentNames.length) {
|
|
|
|
|
|
items.push(`附件: ${attachmentNames.length}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
return items
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 03:05:51 +00:00
|
|
|
|
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(';')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
function inferPreviewKind(file) {
|
|
|
|
|
|
const mediaType = String(file?.type || '').toLowerCase()
|
|
|
|
|
|
const filename = String(file?.name || '').toLowerCase()
|
|
|
|
|
|
if (mediaType.startsWith('image/') || /\.(png|jpg|jpeg|webp|bmp)$/i.test(filename)) {
|
|
|
|
|
|
return 'image'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mediaType.includes('pdf') || /\.pdf$/i.test(filename)) {
|
|
|
|
|
|
return 'pdf'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'file'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFilePreviews(files, previewRegistry) {
|
|
|
|
|
|
return files.map((file) => {
|
|
|
|
|
|
const kind = inferPreviewKind(file)
|
|
|
|
|
|
if (kind !== 'image') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
filename: file.name,
|
|
|
|
|
|
kind
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const url = URL.createObjectURL(file)
|
|
|
|
|
|
previewRegistry.push(url)
|
|
|
|
|
|
return {
|
|
|
|
|
|
filename: file.name,
|
|
|
|
|
|
kind,
|
|
|
|
|
|
url
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDocumentPreview(filePreviews, filename) {
|
|
|
|
|
|
if (!Array.isArray(filePreviews)) return null
|
|
|
|
|
|
return filePreviews.find((item) => item.filename === filename) ?? null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function buildWelcomeInsight(entrySource, linkedRequest) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
intent: 'welcome',
|
2026-05-12 07:22:11 +00:00
|
|
|
|
metricLabel: '当前状态',
|
|
|
|
|
|
metricValue: '待识别',
|
|
|
|
|
|
title: entrySource === 'detail' && linkedRequest?.id ? `已关联 ${linkedRequest.id}` : '等待识别内容',
|
2026-05-12 01:27:49 +00:00
|
|
|
|
summary:
|
2026-05-12 07:22:11 +00:00
|
|
|
|
entrySource === 'detail' && linkedRequest?.id
|
|
|
|
|
|
? '发送消息后会直接结合当前单据上下文识别报销语义,并在右侧展示可核对字段。'
|
|
|
|
|
|
: '请输入费用场景或上传票据,右侧会展示识别出的报销类型、时间、金额和待补字段。',
|
2026-05-12 01:27:49 +00:00
|
|
|
|
agent: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
function buildInitialInsightFromConversation(conversation) {
|
|
|
|
|
|
const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : []
|
|
|
|
|
|
for (let index = rawMessages.length - 1; index >= 0; index -= 1) {
|
|
|
|
|
|
const item = rawMessages[index]
|
|
|
|
|
|
const messageJson = item?.message_json || item?.messageJson || {}
|
|
|
|
|
|
const orchestratorPayload = messageJson?.orchestrator_payload || null
|
|
|
|
|
|
if (!orchestratorPayload) continue
|
|
|
|
|
|
const attachmentNames = Array.isArray(messageJson?.attachment_names)
|
|
|
|
|
|
? messageJson.attachment_names.filter(Boolean)
|
|
|
|
|
|
: []
|
|
|
|
|
|
return buildAgentInsight(orchestratorPayload, attachmentNames, [])
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
function resolveInitialConversationId(conversation) {
|
|
|
|
|
|
return String(conversation?.conversation_id || conversation?.conversationId || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveInitialDraftClaimId(conversation) {
|
|
|
|
|
|
return String(conversation?.draft_claim_id || conversation?.draftClaimId || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeInitialConversationMessages(conversation) {
|
|
|
|
|
|
const rawMessages = Array.isArray(conversation?.messages) ? conversation.messages : []
|
|
|
|
|
|
|
|
|
|
|
|
return rawMessages.map((item) => {
|
|
|
|
|
|
const messageJson = item?.message_json || item?.messageJson || {}
|
|
|
|
|
|
const attachmentNames = Array.isArray(messageJson?.attachment_names)
|
|
|
|
|
|
? messageJson.attachment_names.filter(Boolean)
|
|
|
|
|
|
: []
|
|
|
|
|
|
const orchestratorPayload = messageJson?.orchestrator_payload || null
|
|
|
|
|
|
const result = orchestratorPayload?.result || {}
|
|
|
|
|
|
|
|
|
|
|
|
return createMessage(item.role, item.content, attachmentNames, {
|
|
|
|
|
|
id: `restored-${item.id || ++messageSeed}`,
|
|
|
|
|
|
time: formatMessageTime(item.created_at || item.createdAt),
|
|
|
|
|
|
meta: item.role === 'assistant' ? buildStoredMessageMeta(messageJson, attachmentNames) : [],
|
|
|
|
|
|
citations: item.role === 'assistant' && Array.isArray(result?.citations) ? result.citations : [],
|
|
|
|
|
|
suggestedActions:
|
|
|
|
|
|
item.role === 'assistant' && Array.isArray(result?.suggested_actions)
|
|
|
|
|
|
? result.suggested_actions
|
|
|
|
|
|
: [],
|
|
|
|
|
|
draftPayload: item.role === 'assistant' ? result?.draft_payload || messageJson?.draft_payload || null : null,
|
|
|
|
|
|
reviewPayload: item.role === 'assistant' ? result?.review_payload || null : null,
|
|
|
|
|
|
riskFlags: item.role === 'assistant' && Array.isArray(result?.risk_flags) ? result.risk_flags : []
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
function cloneReviewEditFields(fields) {
|
|
|
|
|
|
const items = Array.isArray(fields) ? fields : []
|
|
|
|
|
|
return items.map((item) => ({
|
|
|
|
|
|
key: String(item?.key || '').trim(),
|
|
|
|
|
|
label: String(item?.label || '').trim(),
|
|
|
|
|
|
value: String(item?.value || ''),
|
|
|
|
|
|
placeholder: String(item?.placeholder || ''),
|
|
|
|
|
|
required: Boolean(item?.required),
|
|
|
|
|
|
field_type: String(item?.field_type || item?.fieldType || 'text').trim() || 'text',
|
|
|
|
|
|
group: String(item?.group || 'basic').trim() || 'basic'
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewFormValues(fields) {
|
|
|
|
|
|
return cloneReviewEditFields(fields).reduce((result, item) => {
|
|
|
|
|
|
if (!item.key) {
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
result[item.key] = String(item.value || '').trim()
|
|
|
|
|
|
return result
|
|
|
|
|
|
}, {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewCorrectionMessage(fields) {
|
|
|
|
|
|
const lines = ['请按以下核对后的报销信息更新当前识别结果:']
|
|
|
|
|
|
for (const item of cloneReviewEditFields(fields)) {
|
|
|
|
|
|
if (!item.label || (!item.value && !item.required)) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(`${item.label}:${String(item.value || '').trim() || '待补充'}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function buildErrorInsight(error, fileNames = []) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
intent: 'agent',
|
|
|
|
|
|
metricLabel: '运行状态',
|
|
|
|
|
|
metricValue: '失败',
|
|
|
|
|
|
title: '智能体调用失败',
|
|
|
|
|
|
summary: error?.message || '无法连接后端 Orchestrator。',
|
|
|
|
|
|
agent: {
|
|
|
|
|
|
runId: '未生成',
|
|
|
|
|
|
selectedAgent: 'orchestrator',
|
|
|
|
|
|
scenario: '未知',
|
|
|
|
|
|
intent: '未知',
|
|
|
|
|
|
permissionLevel: 'unknown',
|
|
|
|
|
|
routeReason: 'request_failed',
|
|
|
|
|
|
requiresConfirmation: false,
|
|
|
|
|
|
degraded: false,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
citations: [],
|
|
|
|
|
|
suggestedActions: [],
|
|
|
|
|
|
draftPayload: null,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
reviewPayload: null,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
riskFlags: [],
|
|
|
|
|
|
toolCount: 0,
|
|
|
|
|
|
failedToolCount: 0,
|
|
|
|
|
|
selectedCapabilityCodes: [],
|
2026-05-12 06:40:19 +00:00
|
|
|
|
filePreviews: [],
|
2026-05-12 01:27:49 +00:00
|
|
|
|
statusLabel: '失败',
|
|
|
|
|
|
statusTone: 'note'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const trace = payload?.trace_summary || {}
|
|
|
|
|
|
const result = payload?.result || {}
|
|
|
|
|
|
const statusLabel = resolveStatusLabel(payload?.status)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
intent: 'agent',
|
|
|
|
|
|
metricLabel: '运行状态',
|
|
|
|
|
|
metricValue: statusLabel,
|
|
|
|
|
|
title:
|
|
|
|
|
|
result?.draft_payload?.title ||
|
|
|
|
|
|
`${SCENARIO_LABELS[trace?.scenario] || '通用'}${INTENT_LABELS[trace?.intent] || '处理'}结果`,
|
|
|
|
|
|
summary: result?.answer || result?.message || '智能体已完成处理。',
|
|
|
|
|
|
agent: {
|
|
|
|
|
|
runId: payload?.run_id || '未生成',
|
|
|
|
|
|
selectedAgent: payload?.selected_agent || 'orchestrator',
|
|
|
|
|
|
scenario: SCENARIO_LABELS[trace?.scenario] || trace?.scenario || '未知',
|
|
|
|
|
|
intent: INTENT_LABELS[trace?.intent] || trace?.intent || '未知',
|
|
|
|
|
|
permissionLevel: payload?.permission_level || 'unknown',
|
|
|
|
|
|
routeReason: payload?.route_reason || 'unknown',
|
|
|
|
|
|
requiresConfirmation: Boolean(payload?.requires_confirmation),
|
|
|
|
|
|
degraded: Boolean(trace?.degraded),
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
citations: Array.isArray(result?.citations) ? result.citations : [],
|
|
|
|
|
|
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
|
|
|
|
|
|
draftPayload: result?.draft_payload || null,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
reviewPayload: result?.review_payload || null,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
riskFlags: Array.isArray(result?.risk_flags) ? result.risk_flags : [],
|
|
|
|
|
|
toolCount: Number(trace?.tool_count || 0),
|
|
|
|
|
|
failedToolCount: Number(trace?.failed_tool_count || 0),
|
|
|
|
|
|
selectedCapabilityCodes: Array.isArray(trace?.selected_capability_codes)
|
|
|
|
|
|
? trace.selected_capability_codes
|
|
|
|
|
|
: [],
|
2026-05-12 06:40:19 +00:00
|
|
|
|
filePreviews,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
statusLabel,
|
|
|
|
|
|
statusTone: resolveStatusTone(payload?.status)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TravelReimbursementCreateView',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
initialPrompt: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
initialFiles: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
2026-05-12 06:40:19 +00:00
|
|
|
|
initialConversation: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
2026-05-12 01:27:49 +00:00
|
|
|
|
entrySource: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'requests'
|
|
|
|
|
|
},
|
|
|
|
|
|
requestContext: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
emits: ['close'],
|
|
|
|
|
|
setup(props, { emit }) {
|
|
|
|
|
|
const { currentUser } = useSystemState()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
const fileInputRef = ref(null)
|
|
|
|
|
|
const messageListRef = ref(null)
|
|
|
|
|
|
const composerDraft = ref('')
|
|
|
|
|
|
const attachedFiles = ref([])
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const submitting = ref(false)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
|
2026-05-12 06:40:19 +00:00
|
|
|
|
const restoredMessages = normalizeInitialConversationMessages(props.initialConversation)
|
2026-05-12 07:22:11 +00:00
|
|
|
|
const initialInsight = buildInitialInsightFromConversation(props.initialConversation)
|
2026-05-12 06:40:19 +00:00
|
|
|
|
const messages = ref(
|
|
|
|
|
|
restoredMessages.length
|
|
|
|
|
|
? restoredMessages
|
|
|
|
|
|
: [
|
|
|
|
|
|
createMessage(
|
|
|
|
|
|
'assistant',
|
2026-05-12 07:22:11 +00:00
|
|
|
|
props.entrySource === 'detail' && linkedRequest.value?.id
|
|
|
|
|
|
? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。请描述费用场景或补充票据。`
|
2026-05-12 06:40:19 +00:00
|
|
|
|
: '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
const conversationId = ref(resolveInitialConversationId(props.initialConversation))
|
|
|
|
|
|
const draftClaimId = ref(resolveInitialDraftClaimId(props.initialConversation))
|
|
|
|
|
|
const previewRegistry = []
|
2026-05-12 01:27:49 +00:00
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
const currentInsight = ref(initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value))
|
|
|
|
|
|
const reviewCancelDialogOpen = ref(false)
|
|
|
|
|
|
const reviewEditDialogOpen = ref(false)
|
|
|
|
|
|
const reviewActionBusy = ref(false)
|
|
|
|
|
|
const reviewEditFields = ref([])
|
|
|
|
|
|
const reviewActionMessageId = ref('')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const sourceLabel = computed(() => SOURCE_LABELS[props.entrySource] ?? '来自 AI 工作台')
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const canSubmit = computed(
|
|
|
|
|
|
() => !submitting.value && Boolean(composerDraft.value.trim() || attachedFiles.value.length)
|
|
|
|
|
|
)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const showInsightPanel = computed(() => currentInsight.value.intent !== 'welcome')
|
|
|
|
|
|
const composerPlaceholder = computed(() => {
|
2026-05-12 07:22:11 +00:00
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
return '例如:查一下本周报销金额、解释酒店超标风险,或根据附件生成报销草稿。'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
const currentIntentLabel = computed(() => {
|
|
|
|
|
|
const labels = {
|
|
|
|
|
|
welcome: '等待输入',
|
2026-05-12 01:27:49 +00:00
|
|
|
|
agent: '真实智能体'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
|
|
|
|
|
})
|
2026-05-12 07:22:11 +00:00
|
|
|
|
const latestReviewMessage = computed(() =>
|
|
|
|
|
|
[...messages.value].reverse().find((item) => item.role === 'assistant' && item.reviewPayload) ?? null
|
|
|
|
|
|
)
|
|
|
|
|
|
const activeReviewPayload = computed(
|
|
|
|
|
|
() => currentInsight.value.agent?.reviewPayload || latestReviewMessage.value?.reviewPayload || null
|
|
|
|
|
|
)
|
|
|
|
|
|
const activeReviewFilePreviews = computed(
|
|
|
|
|
|
() => currentInsight.value.agent?.filePreviews || []
|
|
|
|
|
|
)
|
|
|
|
|
|
const recognizedSlotCards = computed(() =>
|
|
|
|
|
|
Array.isArray(activeReviewPayload.value?.slot_cards)
|
|
|
|
|
|
? activeReviewPayload.value.slot_cards.filter((item) => item.status !== 'missing')
|
|
|
|
|
|
: []
|
|
|
|
|
|
)
|
|
|
|
|
|
const missingSlotCards = computed(() =>
|
|
|
|
|
|
Array.isArray(activeReviewPayload.value?.slot_cards)
|
|
|
|
|
|
? activeReviewPayload.value.slot_cards.filter((item) => item.status === 'missing')
|
|
|
|
|
|
: []
|
|
|
|
|
|
)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const shortcuts = computed(() => {
|
2026-05-12 07:22:11 +00:00
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '解释风险原因',
|
|
|
|
|
|
icon: 'mdi mdi-shield-alert-outline',
|
|
|
|
|
|
prompt: `解释一下 ${linkedRequest.value.id} 为什么会被拦截`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '生成处理意见',
|
|
|
|
|
|
icon: 'mdi mdi-file-document-edit-outline',
|
|
|
|
|
|
prompt: `帮我给 ${linkedRequest.value.id} 生成处理意见草稿`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '列出补件清单',
|
|
|
|
|
|
icon: 'mdi mdi-format-list-checks',
|
|
|
|
|
|
prompt: `帮我列出 ${linkedRequest.value.id} 还需要补哪些附件`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '引用相关制度',
|
|
|
|
|
|
icon: 'mdi mdi-book-open-variant-outline',
|
|
|
|
|
|
prompt: `解释一下 ${linkedRequest.value.id} 相关的报销制度依据`
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '查本周报销金额',
|
|
|
|
|
|
icon: 'mdi mdi-cash-multiple',
|
|
|
|
|
|
prompt: '查一下本周报销金额'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '解释报销风险',
|
|
|
|
|
|
icon: 'mdi mdi-shield-alert-outline',
|
|
|
|
|
|
prompt: '为什么酒店超标报销不能直接通过'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '生成报销草稿',
|
|
|
|
|
|
icon: 'mdi mdi-file-document-edit-outline',
|
|
|
|
|
|
prompt: '帮我生成一份差旅报销草稿'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '查待付款金额',
|
|
|
|
|
|
icon: 'mdi mdi-bank-transfer-out',
|
|
|
|
|
|
prompt: '供应商B待付款多少'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
})
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-05-12 07:22:11 +00:00
|
|
|
|
currentInsight.value = initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value)
|
2026-05-12 01:27:49 +00:00
|
|
|
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
2026-05-06 11:00:38 +08:00
|
|
|
|
composerDraft.value = props.initialPrompt.trim()
|
2026-05-12 01:27:49 +00:00
|
|
|
|
attachedFiles.value = Array.from(props.initialFiles)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
submitComposer()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
for (const url of previewRegistry) {
|
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function scrollToBottom() {
|
|
|
|
|
|
if (!messageListRef.value) return
|
|
|
|
|
|
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
function replaceMessage(messageId, nextMessage) {
|
|
|
|
|
|
const index = messages.value.findIndex((item) => item.id === messageId)
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
messages.value.push(nextMessage)
|
|
|
|
|
|
return
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
2026-05-12 01:27:49 +00:00
|
|
|
|
messages.value.splice(index, 1, nextMessage)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function triggerFileUpload() {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
if (submitting.value) return
|
2026-05-06 11:00:38 +08:00
|
|
|
|
fileInputRef.value?.click()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleFilesChange(event) {
|
|
|
|
|
|
attachedFiles.value = Array.from(event.target.files ?? [])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function runShortcut(prompt) {
|
|
|
|
|
|
composerDraft.value = prompt
|
|
|
|
|
|
submitComposer()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 03:05:51 +00:00
|
|
|
|
function buildBackendMessage(rawText, fileNames, ocrSummary = '') {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const parts = []
|
|
|
|
|
|
const normalizedText = String(rawText || '').trim()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
if (normalizedText) {
|
|
|
|
|
|
parts.push(normalizedText)
|
|
|
|
|
|
} else if (fileNames.length) {
|
|
|
|
|
|
parts.push(`我上传了 ${fileNames.length} 份票据,请结合附件名称给出报销建议并尽量生成草稿。`)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
if (fileNames.length) {
|
|
|
|
|
|
parts.push(`附件名称:${fileNames.join('、')}`)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 03:05:51 +00:00
|
|
|
|
if (ocrSummary) {
|
|
|
|
|
|
parts.push(`OCR摘要:${ocrSummary}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
2026-05-12 01:27:49 +00:00
|
|
|
|
parts.push(`关联单号:${linkedRequest.value.id}`)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
return parts.join('\n')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
async function submitComposer(options = {}) {
|
|
|
|
|
|
const rawText = String(options.rawText ?? composerDraft.value).trim()
|
|
|
|
|
|
const files = Array.from(options.files ?? attachedFiles.value)
|
|
|
|
|
|
if (!rawText && !files.length) return
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const fileNames = files.map((file) => file.name)
|
2026-05-12 06:40:19 +00:00
|
|
|
|
const filePreviews = buildFilePreviews(files, previewRegistry)
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const userText =
|
2026-05-12 07:22:11 +00:00
|
|
|
|
String(options.userText || '').trim() ||
|
|
|
|
|
|
rawText ||
|
|
|
|
|
|
`我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
|
|
|
|
|
|
const extraContext = options.extraContext && typeof options.extraContext === 'object'
|
|
|
|
|
|
? options.extraContext
|
|
|
|
|
|
: {}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
const pendingMessage = createMessage('assistant', options.pendingText || '正在识别并更新右侧核对信息...', [], {
|
|
|
|
|
|
meta: ['处理中']
|
2026-05-12 01:27:49 +00:00
|
|
|
|
})
|
|
|
|
|
|
messages.value.push(pendingMessage)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
attachedFiles.value = []
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
submitting.value = true
|
|
|
|
|
|
nextTick(scrollToBottom)
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const user = currentUser.value || {}
|
2026-05-12 03:05:51 +00:00
|
|
|
|
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)
|
2026-05-12 01:27:49 +00:00
|
|
|
|
const payload = await runOrchestrator({
|
|
|
|
|
|
source: 'user_message',
|
|
|
|
|
|
user_id: user.username || user.name || 'anonymous',
|
2026-05-12 06:40:19 +00:00
|
|
|
|
conversation_id: conversationId.value || null,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
message: backendMessage,
|
|
|
|
|
|
context_json: {
|
|
|
|
|
|
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
|
|
|
|
|
is_admin: Boolean(user.isAdmin),
|
|
|
|
|
|
name: user.name || '',
|
|
|
|
|
|
role: user.role || '',
|
|
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
attachment_names: fileNames,
|
2026-05-12 03:05:51 +00:00
|
|
|
|
attachment_count: fileNames.length,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
draft_claim_id: draftClaimId.value || undefined,
|
2026-05-12 03:05:51 +00:00
|
|
|
|
ocr_summary: ocrSummary,
|
2026-05-12 07:22:11 +00:00
|
|
|
|
ocr_documents: ocrDocuments,
|
|
|
|
|
|
...(linkedRequest.value ? { request_context: linkedRequest.value } : {}),
|
|
|
|
|
|
...extraContext
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-12 06:40:19 +00:00
|
|
|
|
conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
|
|
|
|
|
|
draftClaimId.value =
|
|
|
|
|
|
String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
|
|
|
|
|
|
2026-05-12 01:27:49 +00:00
|
|
|
|
replaceMessage(
|
|
|
|
|
|
pendingMessage.id,
|
|
|
|
|
|
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
|
|
|
|
|
|
meta: buildMessageMeta(payload, fileNames),
|
|
|
|
|
|
citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [],
|
|
|
|
|
|
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
|
|
|
|
|
|
? payload.result.suggested_actions
|
|
|
|
|
|
: [],
|
|
|
|
|
|
draftPayload: payload?.result?.draft_payload || null,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
reviewPayload: payload?.result?.review_payload || null,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
2026-05-12 06:40:19 +00:00
|
|
|
|
currentInsight.value = buildAgentInsight(payload, fileNames, filePreviews)
|
2026-05-12 01:27:49 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
replaceMessage(
|
|
|
|
|
|
pendingMessage.id,
|
|
|
|
|
|
createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
error?.message || '无法连接后端 Orchestrator,请稍后重试。',
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: ['调用失败']
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
currentInsight.value = buildErrorInsight(error, fileNames)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 07:22:11 +00:00
|
|
|
|
function openCancelReviewDialog(message) {
|
|
|
|
|
|
reviewActionMessageId.value = String(message?.id || '')
|
|
|
|
|
|
reviewCancelDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeCancelReviewDialog() {
|
|
|
|
|
|
if (reviewActionBusy.value) return
|
|
|
|
|
|
reviewCancelDialogOpen.value = false
|
|
|
|
|
|
reviewActionMessageId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function confirmCancelReview() {
|
|
|
|
|
|
if (reviewActionBusy.value) return
|
|
|
|
|
|
reviewCancelDialogOpen.value = false
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openEditReviewDialog(message) {
|
|
|
|
|
|
reviewEditFields.value = cloneReviewEditFields(message?.reviewPayload?.edit_fields)
|
|
|
|
|
|
reviewActionMessageId.value = String(message?.id || '')
|
|
|
|
|
|
reviewEditDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeEditReviewDialog() {
|
|
|
|
|
|
if (reviewActionBusy.value) return
|
|
|
|
|
|
reviewEditDialogOpen.value = false
|
|
|
|
|
|
reviewEditFields.value = []
|
|
|
|
|
|
reviewActionMessageId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function applyEditedReview() {
|
|
|
|
|
|
if (reviewActionBusy.value) return
|
|
|
|
|
|
|
|
|
|
|
|
reviewActionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fields = cloneReviewEditFields(reviewEditFields.value)
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: buildReviewCorrectionMessage(fields),
|
|
|
|
|
|
userText: '我已修改识别信息,请按最新内容更新。',
|
|
|
|
|
|
pendingText: '正在根据修改内容重新识别...',
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
review_action: 'edit_review',
|
|
|
|
|
|
review_form_values: buildReviewFormValues(fields)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewActionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
closeEditReviewDialog()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleReviewAction(message, action) {
|
|
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
|
|
|
|
if (!actionType || reviewActionBusy.value) return
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'cancel_review') {
|
|
|
|
|
|
openCancelReviewDialog(message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'edit_review') {
|
|
|
|
|
|
openEditReviewDialog(message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!['save_draft', 'next_step'].includes(actionType)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reviewActionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fields = cloneReviewEditFields(message?.reviewPayload?.edit_fields)
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText:
|
|
|
|
|
|
actionType === 'save_draft'
|
|
|
|
|
|
? '请按当前已识别信息先保存草稿,缺失字段后续再补。'
|
|
|
|
|
|
: '我已核对右侧识别结果,请进入下一步。',
|
|
|
|
|
|
userText: actionType === 'save_draft' ? '我先按当前信息保存草稿。' : '我确认当前识别结果,继续下一步。',
|
|
|
|
|
|
pendingText: actionType === 'save_draft' ? '正在保存当前草稿...' : '正在进入下一步...',
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
review_action: actionType,
|
|
|
|
|
|
review_form_values: buildReviewFormValues(fields)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewActionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
|
return {
|
|
|
|
|
|
emit,
|
2026-05-12 07:22:11 +00:00
|
|
|
|
aiAvatar,
|
|
|
|
|
|
userAvatar,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
fileInputRef,
|
|
|
|
|
|
messageListRef,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
attachedFiles,
|
2026-05-12 01:27:49 +00:00
|
|
|
|
submitting,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
messages,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
sourceLabel,
|
|
|
|
|
|
canSubmit,
|
|
|
|
|
|
showInsightPanel,
|
|
|
|
|
|
composerPlaceholder,
|
|
|
|
|
|
currentIntentLabel,
|
2026-05-12 07:22:11 +00:00
|
|
|
|
latestReviewMessage,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
activeReviewFilePreviews,
|
|
|
|
|
|
recognizedSlotCards,
|
|
|
|
|
|
missingSlotCards,
|
|
|
|
|
|
reviewCancelDialogOpen,
|
|
|
|
|
|
reviewEditDialogOpen,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
reviewEditFields,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
shortcuts,
|
2026-05-12 06:40:19 +00:00
|
|
|
|
resolveDocumentPreview,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
triggerFileUpload,
|
|
|
|
|
|
handleFilesChange,
|
|
|
|
|
|
runShortcut,
|
2026-05-12 07:22:11 +00:00
|
|
|
|
submitComposer,
|
|
|
|
|
|
handleReviewAction,
|
|
|
|
|
|
closeCancelReviewDialog,
|
|
|
|
|
|
confirmCancelReview,
|
|
|
|
|
|
closeEditReviewDialog,
|
|
|
|
|
|
applyEditedReview
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|