feat: 增强规则资产管理与审计页面运行时调试
后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险 规则生成模板执行和员工数据模型字段,知识库 RAG 增强本 地回退和文档提取能力,清理旧风险规则文件统一由生成引擎 管理,前端审计页面增加运行时调试面板和规则资产编辑交互, 补充单元测试覆盖。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
export const DEFAULT_APP_VIEW_ORDER = [
|
||||
'overview',
|
||||
'workbench',
|
||||
'documents',
|
||||
'requests',
|
||||
'approval',
|
||||
'archive',
|
||||
@@ -11,7 +12,7 @@ export const DEFAULT_APP_VIEW_ORDER = [
|
||||
'settings'
|
||||
]
|
||||
|
||||
const ALWAYS_VISIBLE_VIEWS = new Set(['workbench', 'requests', 'policies'])
|
||||
const ALWAYS_VISIBLE_VIEWS = new Set(['workbench', 'documents', 'requests', 'policies'])
|
||||
const VIEW_ROLE_RULES = {
|
||||
overview: ['finance', 'executive'],
|
||||
approval: ['approver', 'finance', 'executive'],
|
||||
|
||||
76
web/src/utils/documentCenterTime.js
Normal file
76
web/src/utils/documentCenterTime.js
Normal file
@@ -0,0 +1,76 @@
|
||||
function toDate(value) {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextDate = new Date(value)
|
||||
return Number.isNaN(nextDate.getTime()) ? null : nextDate
|
||||
}
|
||||
|
||||
export function extractDateText(value) {
|
||||
const matched = String(value || '').match(/\d{4}-\d{2}-\d{2}/)
|
||||
return matched ? matched[0] : ''
|
||||
}
|
||||
|
||||
export function formatDocumentListTime(value) {
|
||||
const raw = String(value || '').trim()
|
||||
if (!raw || raw === '待补充') {
|
||||
return '待补充'
|
||||
}
|
||||
|
||||
const date = toDate(raw)
|
||||
if (date) {
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${month}-${day} ${hour}:${minute}`
|
||||
}
|
||||
|
||||
return raw.replace(/^\d{4}-/, '').slice(0, 11)
|
||||
}
|
||||
|
||||
export function resolveDocumentSortTime(value) {
|
||||
const date = toDate(value)
|
||||
return date ? date.getTime() : 0
|
||||
}
|
||||
|
||||
export function formatDocumentDurationSince(value, now = Date.now()) {
|
||||
const startAt = toDate(value)
|
||||
if (!startAt) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const diffMs = Math.max(0, Number(now) - startAt.getTime())
|
||||
const totalMinutes = Math.floor(diffMs / (60 * 1000))
|
||||
if (totalMinutes < 1) {
|
||||
return '刚刚'
|
||||
}
|
||||
|
||||
const days = Math.floor(totalMinutes / (24 * 60))
|
||||
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
|
||||
const minutes = totalMinutes % 60
|
||||
|
||||
if (days > 0) {
|
||||
return hours > 0 ? `${days}天${hours}小时` : `${days}天`
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时`
|
||||
}
|
||||
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
export function resolveDocumentStayTimeDisplay(row, now = Date.now()) {
|
||||
const currentStep = Array.isArray(row?.progressSteps)
|
||||
? row.progressSteps.find((step) => step?.current)
|
||||
: null
|
||||
const stepTime = String(currentStep?.time || '').trim()
|
||||
if (stepTime.startsWith('停留')) {
|
||||
return stepTime.replace(/^停留\s*/, '') || '待计算'
|
||||
}
|
||||
|
||||
const startedAt = row?.updatedAt || row?.submittedAt || row?.createdAt || row?.applyTime
|
||||
return formatDocumentDurationSince(startedAt, now) || '待计算'
|
||||
}
|
||||
158
web/src/utils/expenseApplicationOntology.js
Normal file
158
web/src/utils/expenseApplicationOntology.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const EXPENSE_TYPE_LABELS = {
|
||||
travel: '差旅费',
|
||||
hotel: '住宿费',
|
||||
transport: '交通费',
|
||||
meal: '业务招待费',
|
||||
entertainment: '业务招待费',
|
||||
meeting: '会务费',
|
||||
office: '办公用品费',
|
||||
training: '培训费',
|
||||
communication: '通讯费',
|
||||
welfare: '福利费',
|
||||
other: '其他费用'
|
||||
}
|
||||
|
||||
const SLOT_LABELS = {
|
||||
expense_type: '费用场景',
|
||||
amount: '申请金额',
|
||||
time_range: '业务时间',
|
||||
reason: '申请事由',
|
||||
attachments: '附件说明',
|
||||
customer_name: '客户名称',
|
||||
participants: '参与人员'
|
||||
}
|
||||
|
||||
const PRE_APPROVAL_TYPES = new Set(['travel', 'meeting', 'office', 'training'])
|
||||
const ATTACHMENT_REQUIRED_TYPES = new Set(['meeting', 'training'])
|
||||
|
||||
export const APPLICATION_EXAMPLES = [
|
||||
'申请下周去北京做客户现场验收,差旅预算18000元',
|
||||
'申请上海产品发布会会务费32000元,需要场地和物料',
|
||||
'申请部门集中采购办公用品4800元,用于新员工入职'
|
||||
]
|
||||
|
||||
export function buildExpenseApplicationOntologyContext(currentUser = {}) {
|
||||
return {
|
||||
document_type: 'expense_application',
|
||||
application_stage: 'pre_approval',
|
||||
conversation_scenario: 'expense',
|
||||
entry_source: 'documents_application',
|
||||
role_codes: Array.isArray(currentUser.roleCodes) ? currentUser.roleCodes : [],
|
||||
is_admin: Boolean(currentUser.isAdmin),
|
||||
name: currentUser.name || '',
|
||||
role: currentUser.role || '',
|
||||
department: currentUser.department || currentUser.departmentName || '',
|
||||
department_name: currentUser.department || currentUser.departmentName || '',
|
||||
position: currentUser.position || '',
|
||||
grade: currentUser.grade || '',
|
||||
employee_no: currentUser.employeeNo || currentUser.employee_no || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveEntity(ontology, type) {
|
||||
const entities = Array.isArray(ontology?.entities) ? ontology.entities : []
|
||||
return entities.find((item) => item?.type === type) || null
|
||||
}
|
||||
|
||||
export function resolveConstraint(ontology, field) {
|
||||
const constraints = Array.isArray(ontology?.constraints) ? ontology.constraints : []
|
||||
return constraints.find((item) => item?.field === field) || null
|
||||
}
|
||||
|
||||
export function resolveExpenseTypeCode(ontology) {
|
||||
const entity = resolveEntity(ontology, 'expense_type')
|
||||
return String(entity?.normalized_value || entity?.value || 'other').trim() || 'other'
|
||||
}
|
||||
|
||||
export function resolveExpenseTypeLabel(code) {
|
||||
return EXPENSE_TYPE_LABELS[String(code || '').trim()] || EXPENSE_TYPE_LABELS.other
|
||||
}
|
||||
|
||||
export function resolveApplicationAmount(ontology) {
|
||||
const amountEntity = resolveEntity(ontology, 'amount')
|
||||
const amountConstraint = resolveConstraint(ontology, 'amount')
|
||||
const rawValue = amountEntity?.normalized_value || amountEntity?.value || amountConstraint?.value || ''
|
||||
const numericValue = Number(String(rawValue).replace(/[^\d.]/g, ''))
|
||||
return {
|
||||
raw: String(rawValue || '').trim(),
|
||||
value: Number.isFinite(numericValue) ? numericValue : 0
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveTimeRangeText(ontology) {
|
||||
const range = ontology?.time_range || {}
|
||||
if (range.start_date && range.end_date) {
|
||||
return range.start_date === range.end_date
|
||||
? range.start_date
|
||||
: `${range.start_date} 至 ${range.end_date}`
|
||||
}
|
||||
return String(range.raw || '').trim()
|
||||
}
|
||||
|
||||
export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
|
||||
const code = String(expenseTypeCode || '').trim()
|
||||
if (ATTACHMENT_REQUIRED_TYPES.has(code)) {
|
||||
return {
|
||||
level: 'required',
|
||||
label: '必须提交',
|
||||
description: code === 'meeting'
|
||||
? '需补充会议通知、议程、参会范围或预算说明。'
|
||||
: '需补充培训通知、课程说明、报价或审批依据。'
|
||||
}
|
||||
}
|
||||
if (code === 'office' && amount >= 5000) {
|
||||
return {
|
||||
level: 'required',
|
||||
label: '必须提交',
|
||||
description: '办公采购金额较高,需补充采购清单、报价或预算说明。'
|
||||
}
|
||||
}
|
||||
if (code === 'travel') {
|
||||
return {
|
||||
level: 'optional',
|
||||
label: '说明可选',
|
||||
description: '可先提交出差目的、时间和预算;行程或邀请材料可作为补充说明。'
|
||||
}
|
||||
}
|
||||
return {
|
||||
level: 'none',
|
||||
label: '无需附件',
|
||||
description: '当前申请事项可先不提交附件,后续报销阶段再按票据要求补充。'
|
||||
}
|
||||
}
|
||||
|
||||
export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser = {}) {
|
||||
const expenseTypeCode = resolveExpenseTypeCode(ontology)
|
||||
const amount = resolveApplicationAmount(ontology)
|
||||
const locationEntity = resolveEntity(ontology, 'location')
|
||||
const documentTypeEntity = resolveEntity(ontology, 'document_type')
|
||||
const workflowStageEntity = resolveEntity(ontology, 'workflow_stage')
|
||||
const attachmentPolicy = resolveAttachmentPolicy(expenseTypeCode, amount.value)
|
||||
|
||||
return {
|
||||
documentType: documentTypeEntity?.normalized_value || 'expense_application',
|
||||
documentTypeLabel: documentTypeEntity?.value || '费用申请',
|
||||
workflowStage: workflowStageEntity?.normalized_value || 'pre_approval',
|
||||
workflowStageLabel: workflowStageEntity?.value || '前置申请',
|
||||
expenseTypeCode,
|
||||
expenseTypeLabel: resolveExpenseTypeLabel(expenseTypeCode),
|
||||
amount: amount.value,
|
||||
amountDisplay: amount.value ? `¥${amount.value.toLocaleString('zh-CN')}` : '待补充',
|
||||
timeRange: resolveTimeRangeText(ontology) || '待补充',
|
||||
location: locationEntity?.normalized_value || locationEntity?.value || '待补充',
|
||||
reason: String(prompt || '').trim() || '待补充',
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充',
|
||||
preApprovalRequired: PRE_APPROVAL_TYPES.has(expenseTypeCode),
|
||||
attachmentPolicy,
|
||||
missingSlots: normalizeMissingSlots(ontology?.missing_slots || [])
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeMissingSlots(slots = []) {
|
||||
const normalized = Array.isArray(slots) ? slots : []
|
||||
return normalized.map((item) => ({
|
||||
key: String(item || '').trim(),
|
||||
label: SLOT_LABELS[String(item || '').trim()] || String(item || '').trim()
|
||||
})).filter((item) => item.key)
|
||||
}
|
||||
@@ -1,69 +1,26 @@
|
||||
/** 数字员工设置:面向管理员的简明任务列表(频率固定,仅可调执行时间) */
|
||||
export const HERMES_SIMPLE_TASKS = [
|
||||
{
|
||||
id: 'knowledgeAggregation',
|
||||
label: '知识库同步',
|
||||
hint: '同步制度文档与知识索引',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'ruleReviewDigest',
|
||||
label: '规则待审提醒',
|
||||
hint: '汇总待审规则并推送管理员',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'riskSummary',
|
||||
id: 'global_risk_scan',
|
||||
label: '风险每日巡检',
|
||||
hint: '扫描报销、付款等风险信号',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'archiveDigest',
|
||||
label: '归档周报',
|
||||
hint: '汇总已归档报销单',
|
||||
id: 'weekly_expense_report',
|
||||
label: '费控洞察周报',
|
||||
hint: '聚合生成财务总结简报',
|
||||
frequency: 'weekly',
|
||||
frequencyLabel: '每周一',
|
||||
weekday: 1
|
||||
},
|
||||
{
|
||||
id: 'dailyStats',
|
||||
label: '日报统计',
|
||||
hint: '生成昨日报销与审批数据',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'monthlyStats',
|
||||
label: '月报统计',
|
||||
hint: '每月 1 号生成上月汇总',
|
||||
frequency: 'monthly',
|
||||
frequencyLabel: '每月 1 日',
|
||||
monthDay: 1
|
||||
},
|
||||
{
|
||||
id: 'yearlyStats',
|
||||
label: '年报统计',
|
||||
hint: '每年 1 月 1 号生成上年汇总',
|
||||
frequency: 'yearly',
|
||||
frequencyLabel: '每年 1 月 1 日',
|
||||
month: 1,
|
||||
monthDay: 1
|
||||
}
|
||||
]
|
||||
|
||||
function buildDefaultSchedules() {
|
||||
const defaults = {
|
||||
knowledgeAggregation: { enabled: true, frequency: 'daily', time: '00:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
ruleReviewDigest: { enabled: true, frequency: 'daily', time: '18:00', weekday: 5, monthDay: 1, month: 1 },
|
||||
riskSummary: { enabled: true, frequency: 'daily', time: '09:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
archiveDigest: { enabled: false, frequency: 'weekly', time: '10:30', weekday: 1, monthDay: 1, month: 1 },
|
||||
dailyStats: { enabled: true, frequency: 'daily', time: '08:30', weekday: 1, monthDay: 1, month: 1 },
|
||||
monthlyStats: { enabled: true, frequency: 'monthly', time: '09:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
yearlyStats: { enabled: false, frequency: 'yearly', time: '10:00', weekday: 1, monthDay: 1, month: 1 }
|
||||
global_risk_scan: { enabled: true, frequency: 'daily', time: '09:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
weekly_expense_report: { enabled: false, frequency: 'weekly', time: '10:30', weekday: 1, monthDay: 1, month: 1 }
|
||||
}
|
||||
|
||||
for (const task of HERMES_SIMPLE_TASKS) {
|
||||
@@ -91,13 +48,8 @@ export function buildDefaultHermesEmployeeForm() {
|
||||
masterEnabled: true,
|
||||
notifyOnFailure: true,
|
||||
capabilities: {
|
||||
knowledgeAggregation: true,
|
||||
ruleReviewDigest: true,
|
||||
riskSummary: true,
|
||||
archiveDigest: false,
|
||||
dailyStats: true,
|
||||
monthlyStats: true,
|
||||
yearlyStats: false
|
||||
global_risk_scan: true,
|
||||
weekly_expense_report: false
|
||||
},
|
||||
schedules: buildDefaultSchedules()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SECTION_DEFINITIONS = [
|
||||
id: 'hermes',
|
||||
label: '数字员工设置',
|
||||
title: '数字员工设置',
|
||||
desc: 'Hermes 自动任务',
|
||||
desc: '自动任务',
|
||||
longDesc: '选择需要自动执行的任务,并设置每天的执行时间。无需了解 Cron 或复杂调度规则。',
|
||||
actionLabel: '保存数字员工设置'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user