feat: 新增风险规则生成引擎与知识图谱可视化

后端新增风险规则自动生成和模板执行服务,支持从规则资产
批量生成并持久化风险规则文件;知识库入库日志增强图谱
查询和本地 RAG 回退,前端审计页面增加风险规则模型和流
程图组件,知识入库面板拆分为图谱可视化子组件,报销创
建页面增加引导式流程模型,更新知识库索引数据。
This commit is contained in:
caoxiaozhu
2026-05-23 19:54:42 +08:00
parent 5b388d08c0
commit 575f093c74
63 changed files with 35497 additions and 1517 deletions

View File

@@ -0,0 +1,195 @@
<template>
<section class="ingest-run-info">
<div class="info-title">
<div>
<span>基本信息</span>
<h4>{{ model.folder || '未指定知识目录' }}</h4>
</div>
<strong :class="model.statusTone">{{ model.statusLabel }}</strong>
</div>
<div class="info-grid">
<div v-for="item in infoItems" :key="item.label" class="info-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
</div>
</section>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
run: {
type: Object,
required: true
},
model: {
type: Object,
required: true
}
})
const infoItems = computed(() => {
const routeJson = props.run?.route_json || {}
const currentDocument = props.model.documents?.find(
(item) => item.documentId === props.model.currentDocumentId
)
return [
{ label: 'Trace ID', value: props.run?.run_id || '-' },
{ label: '任务类型', value: resolveJobType(routeJson.job_type) },
{ label: '触发来源', value: resolveSource(props.run?.source) },
{ label: '当前阶段', value: props.model.phaseLabel || '-' },
{ label: '开始时间', value: formatDateTime(props.run?.started_at) },
{ label: '结束时间', value: formatDateTime(props.run?.finished_at) },
{ label: '执行耗时', value: formatElapsed(props.run?.started_at, props.run?.finished_at) },
{ label: '当前文件', value: currentDocument?.name || '-' }
]
})
function resolveJobType(value) {
const jobType = String(value || '').trim()
if (jobType === 'knowledge_index_sync') return 'LightRAG 知识归纳'
if (jobType === 'llm_wiki_sync') return 'LLM Wiki 知识归纳'
return jobType || '-'
}
function resolveSource(value) {
const source = String(value || '').trim()
if (source === 'manual') return '手动触发'
if (source === 'scheduled') return '定时任务'
if (source === 'system') return '系统任务'
return source || '-'
}
function formatDateTime(value) {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return String(value)
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
}
function formatElapsed(startedAt, finishedAt) {
const started = new Date(startedAt || '')
const finished = finishedAt ? new Date(finishedAt) : new Date()
if (Number.isNaN(started.getTime()) || Number.isNaN(finished.getTime())) return '-'
const seconds = Math.max(0, Math.round((finished.getTime() - started.getTime()) / 1000))
if (seconds < 60) return `${seconds}s`
const minutes = Math.floor(seconds / 60)
const restSeconds = seconds % 60
if (minutes < 60) return `${minutes}m ${restSeconds}s`
const hours = Math.floor(minutes / 60)
return `${hours}h ${minutes % 60}m`
}
</script>
<style scoped>
.ingest-run-info {
display: grid;
gap: 12px;
padding: 14px;
border: 1px solid #dbe6ef;
border-radius: 8px;
background: #fff;
}
.info-title {
display: flex;
justify-content: space-between;
gap: 16px;
}
.info-title span {
color: #0f766e;
font-size: 12px;
font-weight: 850;
}
.info-title h4 {
margin: 4px 0 0;
color: #0f172a;
font-size: 16px;
}
.info-title > strong {
align-self: flex-start;
min-height: 26px;
display: inline-flex;
align-items: center;
padding: 0 9px;
border-radius: 999px;
font-size: 12px;
}
.info-title > strong.success {
background: #dcfce7;
color: #166534;
}
.info-title > strong.warning {
background: #fef3c7;
color: #92400e;
}
.info-title > strong.danger {
background: #fee2e2;
color: #991b1b;
}
.info-title > strong.muted {
background: #eef2f7;
color: #475569;
}
.info-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.info-item {
min-width: 0;
display: grid;
gap: 5px;
padding: 10px;
border-radius: 8px;
background: #f8fafc;
}
.info-item span {
color: #64748b;
font-size: 12px;
}
.info-item strong {
min-width: 0;
overflow: hidden;
color: #0f172a;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1100px) {
.info-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 620px) {
.info-title {
flex-direction: column;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>