feat: 增强规则资产管理与审计页面运行时调试

后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险
规则生成模板执行和员工数据模型字段,知识库 RAG 增强本
地回退和文档提取能力,清理旧风险规则文件统一由生成引擎
管理,前端审计页面增加运行时调试面板和规则资产编辑交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-24 21:44:17 +08:00
parent 575f093c74
commit 50b1c3f9a9
113 changed files with 13896 additions and 5044 deletions

View File

@@ -6,29 +6,30 @@ import { useNavigation, navItems } from './useNavigation.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'
const SESSION_TYPE_EXPENSE = 'expense'
export function useAppShell() {
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'
const SESSION_TYPE_EXPENSE = 'expense'
export function useAppShell() {
const route = useRoute()
const router = useRouter()
const smartEntryOpen = ref(false)
const smartEntryContext = ref({
prompt: '',
source: 'requests',
request: null,
files: [],
conversation: null,
scope: null
})
const smartEntryContext = ref({
prompt: '',
source: 'requests',
request: null,
files: [],
conversation: null,
scope: null
})
const smartEntrySessionId = ref(0)
const smartEntryInvalidatedDraftClaimId = ref('')
const selectedRequestSnapshot = ref(null)
const { activeView, currentView, setView } = useNavigation()
const {
@@ -60,14 +61,32 @@ export function useAppShell() {
const rawRequest = requests.value.find(
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
)
return normalizeRequestForUi(rawRequest)
const normalizedRequest = normalizeRequestForUi(rawRequest)
if (normalizedRequest) {
return normalizedRequest
}
const snapshot = normalizeRequestForUi(selectedRequestSnapshot.value)
if (
snapshot
&& (
String(snapshot.claimId || '').trim() === requestId
|| String(snapshot.id || '').trim() === requestId
|| String(snapshot.documentNo || '').trim() === requestId
)
) {
return snapshot
}
return null
})
const detailMode = computed(() => route.name === 'app-request-detail')
const detailMode = computed(() => ['app-request-detail', 'app-document-detail'].includes(route.name))
const logDetailMode = computed(() => route.name === 'app-log-detail')
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
const requestsListActive = computed(() => activeView.value === 'requests' && !detailMode.value)
const documentsListActive = computed(() => activeView.value === 'documents' && !detailMode.value)
const workbenchActive = computed(() => activeView.value === 'workbench')
watch(requestsListActive, (isActive, wasActive) => {
@@ -76,6 +95,12 @@ export function useAppShell() {
}
})
watch(documentsListActive, (isActive, wasActive) => {
if (isActive && !wasActive) {
void reloadRequests()
}
})
watch(workbenchActive, (isActive, wasActive) => {
if (isActive && !wasActive) {
void reloadRequests()
@@ -145,56 +170,56 @@ export function useAppShell() {
setView(view)
}
function openTravelCreate() {
smartEntryOpen.value = true
smartEntryContext.value = {
prompt: '',
source: 'topbar',
request: null,
files: [],
conversation: null,
scope: null
}
smartEntrySessionId.value += 1
}
function openTravelCreate() {
smartEntryOpen.value = true
smartEntryContext.value = {
prompt: '',
source: 'topbar',
request: null,
files: [],
conversation: null,
scope: null
}
smartEntrySessionId.value += 1
}
function resolveCurrentUserId() {
const user = currentUser.value || {}
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
function resolveSmartEntryClaimScope(payload = {}) {
const request = payload.request && typeof payload.request === 'object' ? payload.request : null
const payloadScope = payload.scope && typeof payload.scope === 'object' ? payload.scope : null
const claimId = String(
payloadScope?.claimId ||
payloadScope?.claim_id ||
request?.claimId ||
request?.claim_id ||
''
).trim()
if (!claimId) {
return null
}
return { type: 'claim', claimId }
}
function isDetailClaimScopedPayload(payload = {}) {
return String(payload.source || '').trim() === 'detail' && Boolean(resolveSmartEntryClaimScope(payload))
}
async function resolveSmartEntryConversation(payload = {}) {
if (payload.conversation) {
return payload.conversation
}
if (isDetailClaimScopedPayload(payload)) {
return null
}
if (!payload.restoreLatestConversation) {
return null
}
function resolveCurrentUserId() {
const user = currentUser.value || {}
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
function resolveSmartEntryClaimScope(payload = {}) {
const request = payload.request && typeof payload.request === 'object' ? payload.request : null
const payloadScope = payload.scope && typeof payload.scope === 'object' ? payload.scope : null
const claimId = String(
payloadScope?.claimId ||
payloadScope?.claim_id ||
request?.claimId ||
request?.claim_id ||
''
).trim()
if (!claimId) {
return null
}
return { type: 'claim', claimId }
}
function isDetailClaimScopedPayload(payload = {}) {
return String(payload.source || '').trim() === 'detail' && Boolean(resolveSmartEntryClaimScope(payload))
}
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, {
@@ -208,19 +233,19 @@ export function useAppShell() {
}
}
async function openSmartEntry(payload = {}) {
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
}
async function openSmartEntry(payload = {}) {
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
}
@@ -237,21 +262,23 @@ export function useAppShell() {
smartEntryOpen.value = false
void refreshApprovalInbox()
toast(`${claimNo || '该'}单据已完成 AI预审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}`)
router.push({ name: 'app-requests' })
router.push({ name: activeView.value === 'documents' ? 'app-documents' : 'app-requests' })
return
}
toast(`${claimNo || '该'}单据已保存为草稿,可继续上传票据或补充信息。`)
}
function openRequestDetail(request) {
selectedRequestSnapshot.value = request || null
const routeName = activeView.value === 'documents' ? 'app-document-detail' : 'app-request-detail'
router.push({
name: 'app-request-detail',
name: routeName,
params: { requestId: request.claimId || request.id }
})
}
function closeRequestDetail() {
router.push({ name: 'app-requests' })
router.push({ name: activeView.value === 'documents' ? 'app-documents' : 'app-requests' })
}
async function handleRequestUpdated() {
@@ -268,7 +295,8 @@ export function useAppShell() {
await reloadRequests()
void refreshApprovalInbox()
router.push({ name: 'app-requests' })
selectedRequestSnapshot.value = null
router.push({ name: activeView.value === 'documents' ? 'app-documents' : 'app-requests' })
}
return {