feat(web): update views and services

Views:
- AppShellRouteView.vue: update route view
- SettingsView.vue: update settings view
- TravelReimbursementCreateView.vue: update travel form view
- scripts/SettingsView.js: update settings view logic
- scripts/TravelReimbursementCreateView.js: update travel form logic

Services:
- services/orchestrator.js: update orchestrator service client
This commit is contained in:
caoxiaozhu
2026-05-12 06:40:19 +00:00
parent f6a5eeb620
commit c263fc9752
6 changed files with 561 additions and 89 deletions

View File

@@ -1,4 +1,4 @@
import { computed, nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { recognizeOcrFiles } from '../../services/ocr.js'
@@ -63,11 +63,29 @@ function createMessage(role, text, attachments = [], extras = {}) {
citations: [],
suggestedActions: [],
draftPayload: null,
reviewPayload: null,
riskFlags: [],
...extras
}
}
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
})
}
function sanitizeRequest(request) {
if (!request) return { ...DEFAULT_REQUEST }
return {
@@ -129,6 +147,22 @@ function buildMessageMeta(payload, fileNames = []) {
return items
}
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
}
function normalizeOcrDocuments(payload) {
const documents = Array.isArray(payload?.documents) ? payload.documents : []
return documents.slice(0, 5).map((item) => ({
@@ -149,6 +183,43 @@ function buildOcrSummary(payload) {
return parts.join('')
}
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
}
function buildWelcomeInsight(entrySource, linkedRequest) {
return {
intent: 'welcome',
@@ -163,6 +234,41 @@ function buildWelcomeInsight(entrySource, linkedRequest) {
}
}
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 : []
})
})
}
function buildErrorInsight(error, fileNames = []) {
return {
intent: 'agent',
@@ -183,17 +289,19 @@ function buildErrorInsight(error, fileNames = []) {
citations: [],
suggestedActions: [],
draftPayload: null,
reviewPayload: null,
riskFlags: [],
toolCount: 0,
failedToolCount: 0,
selectedCapabilityCodes: [],
filePreviews: [],
statusLabel: '失败',
statusTone: 'note'
}
}
}
function buildAgentInsight(payload, fileNames = []) {
function buildAgentInsight(payload, fileNames = [], filePreviews = []) {
const trace = payload?.trace_summary || {}
const result = payload?.result || {}
const statusLabel = resolveStatusLabel(payload?.status)
@@ -219,12 +327,14 @@ function buildAgentInsight(payload, fileNames = []) {
citations: Array.isArray(result?.citations) ? result.citations : [],
suggestedActions: Array.isArray(result?.suggested_actions) ? result.suggested_actions : [],
draftPayload: result?.draft_payload || null,
reviewPayload: result?.review_payload || null,
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
: [],
filePreviews,
statusLabel,
statusTone: resolveStatusTone(payload?.status)
}
@@ -242,6 +352,10 @@ export default {
type: Array,
default: () => []
},
initialConversation: {
type: Object,
default: null
},
entrySource: {
type: String,
default: 'requests'
@@ -260,8 +374,23 @@ export default {
const composerDraft = ref('')
const attachedFiles = ref([])
const submitting = ref(false)
const messages = ref([])
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
const restoredMessages = normalizeInitialConversationMessages(props.initialConversation)
const messages = ref(
restoredMessages.length
? restoredMessages
: [
createMessage(
'assistant',
props.entrySource === 'detail'
? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。你的提问会直接进入真实智能体链路。`
: '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
)
]
)
const conversationId = ref(resolveInitialConversationId(props.initialConversation))
const draftClaimId = ref(resolveInitialDraftClaimId(props.initialConversation))
const previewRegistry = []
const currentInsight = ref(buildWelcomeInsight(props.entrySource, linkedRequest.value))
const sourceLabel = computed(() => SOURCE_LABELS[props.entrySource] ?? '来自 AI 工作台')
@@ -333,15 +462,6 @@ export default {
]
})
messages.value = [
createMessage(
'assistant',
props.entrySource === 'detail'
? `已进入统一对话工作台,当前关联单据 ${linkedRequest.value.id}。你的提问会直接进入真实智能体链路。`
: '这里是统一对话入口。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
)
]
onMounted(() => {
currentInsight.value = buildWelcomeInsight(props.entrySource, linkedRequest.value)
if (props.initialPrompt?.trim() || props.initialFiles.length) {
@@ -353,6 +473,12 @@ export default {
}
})
onBeforeUnmount(() => {
for (const url of previewRegistry) {
URL.revokeObjectURL(url)
}
})
function scrollToBottom() {
if (!messageListRef.value) return
messageListRef.value.scrollTop = messageListRef.value.scrollHeight
@@ -412,6 +538,7 @@ export default {
const rawText = composerDraft.value.trim()
const files = Array.from(attachedFiles.value)
const fileNames = files.map((file) => file.name)
const filePreviews = buildFilePreviews(files, previewRegistry)
const userText =
rawText || `我上传了 ${fileNames.length} 份票据,请帮我识别并给出报销建议。`
@@ -451,6 +578,7 @@ export default {
const payload = await runOrchestrator({
source: 'user_message',
user_id: user.username || user.name || 'anonymous',
conversation_id: conversationId.value || null,
message: backendMessage,
context_json: {
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
@@ -461,11 +589,16 @@ export default {
request_context: linkedRequest.value,
attachment_names: fileNames,
attachment_count: fileNames.length,
draft_claim_id: draftClaimId.value || undefined,
ocr_summary: ocrSummary,
ocr_documents: ocrDocuments
}
})
conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
draftClaimId.value =
String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
replaceMessage(
pendingMessage.id,
createMessage('assistant', payload?.result?.answer || payload?.result?.message || '智能体已完成处理。', [], {
@@ -475,10 +608,11 @@ export default {
? payload.result.suggested_actions
: [],
draftPayload: payload?.result?.draft_payload || null,
reviewPayload: payload?.result?.review_payload || null,
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
})
)
currentInsight.value = buildAgentInsight(payload, fileNames)
currentInsight.value = buildAgentInsight(payload, fileNames, filePreviews)
} catch (error) {
replaceMessage(
pendingMessage.id,
@@ -514,6 +648,7 @@ export default {
composerPlaceholder,
currentIntentLabel,
shortcuts,
resolveDocumentPreview,
triggerFileUpload,
handleFilesChange,
runShortcut,