feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useNavigation, navItems } from './useNavigation.js'
|
||||
import { useRequests } from './useRequests.js'
|
||||
import { useRequests } from './useRequests.js'
|
||||
import { useSystemState } from './useSystemState.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
|
||||
import { buildDetailAlerts } from '../utils/detailAlerts.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
import { buildWorkbenchSummary } from '../utils/workbenchSummary.js'
|
||||
import { useToast } from './useToast.js'
|
||||
import { fetchOntologyParse } from '../services/ontology.js'
|
||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
|
||||
import { buildDetailAlerts } from '../utils/detailAlerts.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
import {
|
||||
buildWorkbenchIntentOntologyContext,
|
||||
resolveWorkbenchSessionTypeFallback,
|
||||
resolveWorkbenchSessionTypeFromOntology
|
||||
} from '../utils/workbenchAssistantIntent.js'
|
||||
import { buildWorkbenchSummary } from '../utils/workbenchSummary.js'
|
||||
|
||||
const SESSION_TYPE_EXPENSE = 'expense'
|
||||
const SMART_ENTRY_SOURCE_APPLICATION = 'application'
|
||||
@@ -24,10 +30,11 @@ export function useAppShell() {
|
||||
prompt: '',
|
||||
source: 'documents',
|
||||
request: null,
|
||||
files: [],
|
||||
conversation: null,
|
||||
scope: null
|
||||
})
|
||||
files: [],
|
||||
conversation: null,
|
||||
scope: null,
|
||||
sessionType: ''
|
||||
})
|
||||
const smartEntrySessionId = ref(0)
|
||||
const smartEntryRevealToken = ref(0)
|
||||
const smartEntryInvalidatedDraftClaimId = ref('')
|
||||
@@ -48,10 +55,10 @@ export function useAppShell() {
|
||||
ensureLoaded: ensureRequestsLoaded,
|
||||
reload: reloadRequests
|
||||
} = useRequests()
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||
|
||||
const selectedRequest = computed(() => {
|
||||
const requestId = String(route.params.requestId || '')
|
||||
@@ -171,7 +178,8 @@ export function useAppShell() {
|
||||
request: null,
|
||||
files: [],
|
||||
conversation: null,
|
||||
scope: null
|
||||
scope: null,
|
||||
sessionType: ''
|
||||
}
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
@@ -209,6 +217,52 @@ export function useAppShell() {
|
||||
return String(payload.source || '').trim() === 'detail' && Boolean(resolveSmartEntryClaimScope(payload))
|
||||
}
|
||||
|
||||
async function resolveSmartEntrySessionType(payload = {}) {
|
||||
const explicitSessionType = String(payload.sessionType || '').trim()
|
||||
if (explicitSessionType) {
|
||||
return explicitSessionType
|
||||
}
|
||||
|
||||
const source = String(payload.source || 'workbench').trim()
|
||||
if (source !== 'workbench') {
|
||||
return ''
|
||||
}
|
||||
|
||||
const prompt = String(payload.prompt || '').trim()
|
||||
const files = Array.isArray(payload.files) ? payload.files : []
|
||||
const fallbackSessionType = resolveWorkbenchSessionTypeFallback(prompt, {
|
||||
attachmentCount: files.length
|
||||
})
|
||||
if (!prompt) {
|
||||
return fallbackSessionType
|
||||
}
|
||||
|
||||
try {
|
||||
const ontology = await fetchOntologyParse(
|
||||
{
|
||||
query: prompt,
|
||||
user_id: resolveCurrentUserId(),
|
||||
context_json: {
|
||||
...buildWorkbenchIntentOntologyContext({
|
||||
currentUser: currentUser.value,
|
||||
files
|
||||
}),
|
||||
user_input_text: prompt,
|
||||
fallback_session_type: fallbackSessionType
|
||||
}
|
||||
},
|
||||
{
|
||||
timeoutMs: 12000,
|
||||
timeoutMessage: '意图识别超时,已使用本地规则进入助手。'
|
||||
}
|
||||
)
|
||||
return resolveWorkbenchSessionTypeFromOntology(ontology, prompt, fallbackSessionType)
|
||||
} catch (error) {
|
||||
console.warn('Workbench model intent routing failed, fallback to local routing:', error)
|
||||
return fallbackSessionType
|
||||
}
|
||||
}
|
||||
|
||||
function isApplicationDocumentPayload(payload = {}, claimNo = '') {
|
||||
const documentType = String(
|
||||
payload.documentType
|
||||
@@ -227,32 +281,32 @@ export function useAppShell() {
|
||||
|| normalizedClaimNo.startsWith('APP-')
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveSmartEntryConversation(payload = {}) {
|
||||
if (payload.conversation) {
|
||||
return payload.conversation
|
||||
}
|
||||
|
||||
if (isDetailClaimScopedPayload(payload)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!payload.restoreLatestConversation) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
||||
preferRecoverable: true
|
||||
})
|
||||
return latestPayload?.found ? latestPayload.conversation || null : null
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore latest expense conversation for smart entry:', error)
|
||||
toast(error?.message || '恢复最近报销会话失败,请稍后重试。')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function resolveSmartEntryConversation(payload = {}) {
|
||||
if (payload.conversation) {
|
||||
return payload.conversation
|
||||
}
|
||||
|
||||
if (isDetailClaimScopedPayload(payload)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!payload.restoreLatestConversation) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
||||
preferRecoverable: true
|
||||
})
|
||||
return latestPayload?.found ? latestPayload.conversation || null : null
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore latest expense conversation for smart entry:', error)
|
||||
toast(error?.message || '恢复最近报销会话失败,请稍后重试。')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function openSmartEntry(payload = {}) {
|
||||
const shouldReplaceOpenEntry = Boolean(
|
||||
payload?.source === 'budget'
|
||||
@@ -265,38 +319,42 @@ export function useAppShell() {
|
||||
smartEntryRevealToken.value += 1
|
||||
return
|
||||
}
|
||||
const conversation = await resolveSmartEntryConversation(payload)
|
||||
const scope = resolveSmartEntryClaimScope(payload)
|
||||
smartEntryOpen.value = true
|
||||
|
||||
smartEntryContext.value = {
|
||||
prompt: payload.prompt ?? '',
|
||||
source: payload.source ?? 'workbench',
|
||||
request: payload.request ?? selectedRequest.value,
|
||||
files: Array.isArray(payload.files) ? payload.files : [],
|
||||
conversation,
|
||||
scope
|
||||
}
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
function closeSmartEntry() {
|
||||
smartEntryOpen.value = false
|
||||
}
|
||||
|
||||
async function handleDraftSaved(payload = {}) {
|
||||
const [conversation, sessionType] = await Promise.all([
|
||||
resolveSmartEntryConversation(payload),
|
||||
resolveSmartEntrySessionType(payload)
|
||||
])
|
||||
const scope = resolveSmartEntryClaimScope(payload)
|
||||
smartEntryOpen.value = true
|
||||
|
||||
smartEntryContext.value = {
|
||||
prompt: payload.prompt ?? '',
|
||||
source: payload.source ?? 'workbench',
|
||||
request: payload.request ?? selectedRequest.value,
|
||||
files: Array.isArray(payload.files) ? payload.files : [],
|
||||
conversation,
|
||||
scope,
|
||||
sessionType
|
||||
}
|
||||
smartEntrySessionId.value += 1
|
||||
}
|
||||
|
||||
function closeSmartEntry() {
|
||||
smartEntryOpen.value = false
|
||||
}
|
||||
|
||||
async function handleDraftSaved(payload = {}) {
|
||||
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
||||
const status = String(payload.status || payload.claimStatus || '').trim()
|
||||
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
|
||||
const isApplicationDocument = isApplicationDocumentPayload(payload, claimNo)
|
||||
await reloadRequests()
|
||||
if (status === 'submitted') {
|
||||
if (isApplicationDocument) {
|
||||
toast(`${claimNo || '该'}申请单已提交${approvalStage ? `,当前节点:${approvalStage}` : ',等待直属领导审批'}。`)
|
||||
return
|
||||
}
|
||||
smartEntryOpen.value = false
|
||||
toast(
|
||||
isApplicationDocument
|
||||
? `${claimNo || '该'}申请单已提交${approvalStage ? `,当前节点:${approvalStage}` : ',等待直属领导审批'}。`
|
||||
: `${claimNo || '该'}单据已完成 AI预审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`
|
||||
)
|
||||
toast(`${claimNo || '该'}单据已完成 AI预审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`)
|
||||
router.push({ name: 'app-documents' })
|
||||
return
|
||||
}
|
||||
@@ -306,7 +364,7 @@ export function useAppShell() {
|
||||
: `${claimNo || '该'}单据已保存为草稿,可继续上传票据或补充信息。`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function openRequestDetail(request) {
|
||||
selectedRequestSnapshot.value = request || null
|
||||
router.push({
|
||||
@@ -322,57 +380,57 @@ export function useAppShell() {
|
||||
async function handleRequestUpdated() {
|
||||
await reloadRequests()
|
||||
}
|
||||
|
||||
async function handleRequestDeleted(payload = {}) {
|
||||
const deletedClaimId = String(payload.claimId || payload.claim_id || '').trim()
|
||||
if (deletedClaimId) {
|
||||
clearAssistantSessionSnapshotForDraftClaim(resolveCurrentUserId(), deletedClaimId, SESSION_TYPE_EXPENSE)
|
||||
smartEntryInvalidatedDraftClaimId.value = deletedClaimId
|
||||
}
|
||||
|
||||
|
||||
async function handleRequestDeleted(payload = {}) {
|
||||
const deletedClaimId = String(payload.claimId || payload.claim_id || '').trim()
|
||||
if (deletedClaimId) {
|
||||
clearAssistantSessionSnapshotForDraftClaim(resolveCurrentUserId(), deletedClaimId, SESSION_TYPE_EXPENSE)
|
||||
smartEntryInvalidatedDraftClaimId.value = deletedClaimId
|
||||
}
|
||||
|
||||
await reloadRequests()
|
||||
selectedRequestSnapshot.value = null
|
||||
router.push({ name: 'app-documents' })
|
||||
}
|
||||
|
||||
return {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
currentView,
|
||||
|
||||
return {
|
||||
activeRange,
|
||||
activeView,
|
||||
closeRequestDetail,
|
||||
closeSmartEntry,
|
||||
currentView,
|
||||
customRange,
|
||||
detailMode,
|
||||
filteredRequests,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
filters,
|
||||
handleApprove,
|
||||
handleDraftSaved,
|
||||
handleNavigate,
|
||||
handleReject,
|
||||
handleRequestDeleted,
|
||||
handleRequestUpdated,
|
||||
navItems,
|
||||
openExpenseApplicationCreate,
|
||||
openRequestDetail,
|
||||
openSmartEntry,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
workbenchSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
setView,
|
||||
smartEntryContext,
|
||||
openTravelCreate,
|
||||
ranges,
|
||||
requestSummary,
|
||||
workbenchSummary,
|
||||
requestsError,
|
||||
requestsLoading,
|
||||
reloadRequests,
|
||||
requests,
|
||||
search,
|
||||
selectedRequest,
|
||||
setView,
|
||||
smartEntryContext,
|
||||
smartEntryInvalidatedDraftClaimId,
|
||||
smartEntryOpen,
|
||||
smartEntryRevealToken,
|
||||
smartEntrySessionId,
|
||||
detailAlerts,
|
||||
toast,
|
||||
topBarView
|
||||
}
|
||||
}
|
||||
detailAlerts,
|
||||
toast,
|
||||
topBarView
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user