Files
X-Financial/web/src/composables/useChat.js

208 lines
6.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.
import { nextTick, ref } from 'vue'
import { useSystemState } from './useSystemState.js'
import { runOrchestrator } from '../services/orchestrator.js'
const initialMessages = [
{
id: 1,
role: 'agent',
text: '我已读取当前报销、发票、行程和制度命中情况。当前建议:优先处理即将超时与高风险单据。'
},
{
id: 2,
role: 'user',
text: '请列出今天最需要关注的风险。'
},
{
id: 3,
role: 'agent',
text: '主要风险包括3 笔单据将在 30 分钟内超时,市场部存在 2 笔高风险差旅报销,另有 1 笔报销缺少酒店入住清单。'
}
]
export const prompts = ['生成审批意见', '列出补件清单', '解释风险原因', '生成运营简报']
export function useChat(activeView) {
const { currentUser } = useSystemState()
const messages = ref([...initialMessages])
const draft = ref('')
const uploadedFiles = ref([])
const messageList = ref(null)
const activeCase = ref(null)
const sending = ref(false)
function agentReply(text) {
const c = activeCase.value
if (text.includes('超时') || text.includes('SLA')) {
return '当前最紧急的是 3 笔即将超时单据,建议先按剩余处理时长排序,并把缺附件单据转给申请人补齐。'
}
if (text.includes('高风险') || text.includes('风险')) {
return '高风险主要集中在市场部差旅报销,风险点包括住宿超标、重复发票疑似命中、行程说明缺失。建议人工复核后再通过。'
}
if (text.includes('部门')) {
return '从待处理金额看,销售部与研发中心占比最高;从异常占比看,市场部更需要优先关注。'
}
if (text.includes('简报')) {
return '运营简报:今日待审批 12 单,高风险 4 单,平均审批 5.6hSLA 达成率 96%。建议优先处理差旅报销和即将超时单据。'
}
if (text.includes('补件') || text.includes('附件')) {
return '建议补件清单:酒店入住水单、完整行程单、发票原件或验真结果、直属经理确认记录。'
}
if (text.includes('审批意见')) {
return c
? `${c.id} 建议审批意见:费用归属与预算中心基本匹配,请补充必要说明后通过。`
: '建议审批意见:当前单据存在待确认项,请先完成风险核查和附件补齐后再审批。'
}
return '收到。我可以继续帮你拆解异常原因、比较部门趋势、生成审批意见,或整理一份今日报销运营简报。'
}
function scrollToBottom() {
nextTick(() => messageList.value?.scrollTo({ top: messageList.value.scrollHeight, behavior: 'smooth' }))
}
function replaceMessage(id, nextMessage) {
const index = messages.value.findIndex((item) => item.id === id)
if (index === -1) {
messages.value.push(nextMessage)
return
}
messages.value.splice(index, 1, nextMessage)
}
function buildOrchestratorMeta(payload) {
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}`)
}
return items
}
function buildAssistantMessage(payload, fallbackText) {
const result = payload?.result || {}
return {
text: result.answer || result.message || fallbackText,
meta: buildOrchestratorMeta(payload),
citations: Array.isArray(result.citations) ? result.citations : [],
suggestedActions: Array.isArray(result.suggested_actions) ? result.suggested_actions : [],
draftPayload: result.draft_payload || null,
riskFlags: Array.isArray(result.risk_flags) ? result.risk_flags : []
}
}
async function sendMessage() {
const text = draft.value.trim()
if (!text || sending.value) return false
const userMessageId = Date.now()
const pendingMessageId = userMessageId + 1
messages.value.push({ id: userMessageId, role: 'user', text })
draft.value = ''
sending.value = true
messages.value.push({
id: pendingMessageId,
role: 'agent',
text: 'Orchestrator 正在处理中...',
meta: ['运行中']
})
scrollToBottom()
const user = currentUser.value || {}
try {
const payload = await runOrchestrator({
source: 'user_message',
user_id: user.username || user.name || 'anonymous',
message: text,
context_json: {
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
is_admin: Boolean(user.isAdmin),
name: user.name || '',
role: user.role || '',
position: user.position || '',
grade: user.grade || '',
active_case_id: activeCase.value?.id || ''
}
})
const assistantMessage = buildAssistantMessage(payload, 'Orchestrator 已完成处理。')
replaceMessage(pendingMessageId, {
id: pendingMessageId,
role: 'agent',
...assistantMessage
})
} catch (error) {
replaceMessage(pendingMessageId, {
id: pendingMessageId,
role: 'agent',
text: `后端暂时不可用,已切换为本地占位回复:${agentReply(text)}`,
meta: [error?.message || '后端调用失败'],
citations: [],
suggestedActions: [],
draftPayload: null,
riskFlags: []
})
} finally {
sending.value = false
scrollToBottom()
}
return true
}
function handleUpload(event) {
uploadedFiles.value = Array.from(event.target.files ?? []).map((file) => ({
name: file.name,
size: file.size
}))
if (uploadedFiles.value.length) {
const names = uploadedFiles.value.map((file) => file.name).join('、')
messages.value.push({
id: Date.now(),
role: 'agent',
text: `已接收 ${uploadedFiles.value.length} 个附件:${names}。我会优先核对发票验真、费用标准、预算归属和必要审批材料。`
})
scrollToBottom()
}
}
function openChat(request) {
activeCase.value = request
activeView.value = 'chat'
nextTick(() => messageList.value?.scrollTo({ top: messageList.value.scrollHeight }))
}
function openNewChat() {
activeCase.value = null
activeView.value = 'chat'
}
return {
messages, draft, uploadedFiles, messageList, activeCase, prompts, sending,
sendMessage, handleUpload, openChat, openNewChat, scrollToBottom
}
}