feat: 新增预算费控模型与报销审批流引擎
后端新增预算费控服务和报销单审批流模块,引入申请人费用画像 算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常 量和明细同步,更新差旅报销规则电子表格,前端新增预算分析 组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧 边栏和顶栏样式,补充单元测试。
This commit is contained in:
263
web/src/views/scripts/auditViewDigitalEmployeeModel.js
Normal file
263
web/src/views/scripts/auditViewDigitalEmployeeModel.js
Normal file
@@ -0,0 +1,263 @@
|
||||
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
|
||||
|
||||
const TASK_TYPE_LABELS = {
|
||||
daily_risk_scan: '每日风险巡检',
|
||||
global_risk_scan: '全局风险巡检',
|
||||
weekly_ar_summary: '周度应收账龄汇总',
|
||||
weekly_expense_report: '周度费用洞察',
|
||||
rule_review_digest: '规则待审摘要',
|
||||
knowledge_index_sync: '知识库归集',
|
||||
x_financial_callback: '任务回调上报'
|
||||
}
|
||||
|
||||
const CONTENT_LABELS = {
|
||||
task_type: '技能类型',
|
||||
schedule: '执行计划',
|
||||
cron: '调度表达式',
|
||||
folder: '归集范围',
|
||||
changed_only: '仅处理变更',
|
||||
force: '强制重建',
|
||||
index_engine: '索引引擎',
|
||||
callback_type: '回调类型',
|
||||
status: '回写状态',
|
||||
summary: '结果摘要'
|
||||
}
|
||||
|
||||
const HIDDEN_CONTENT_KEYS = new Set([
|
||||
'agent',
|
||||
'target_agent',
|
||||
'callback_token',
|
||||
'token',
|
||||
'api_key',
|
||||
'authorization'
|
||||
])
|
||||
|
||||
export function normalizeDigitalEmployeeText(value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
export function sanitizeDigitalEmployeeText(value, fallback = '') {
|
||||
const text = normalizeDigitalEmployeeText(value)
|
||||
.replace(/hermes/gi, '数字员工')
|
||||
.replace(/赫尔墨斯/g, '数字员工')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
export function sanitizeDigitalEmployeeName(value, fallback = '数字员工技能') {
|
||||
const text = sanitizeDigitalEmployeeText(value, fallback)
|
||||
.replace(/^数字员工[\s·::-]*/i, '')
|
||||
.trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
export function parseDigitalEmployeeContent(value) {
|
||||
if (!value) {
|
||||
return {}
|
||||
}
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeTaskType(source = {}, content = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const raw =
|
||||
normalizeDigitalEmployeeText(content.task_type) ||
|
||||
normalizeDigitalEmployeeText(config.task_type) ||
|
||||
normalizeDigitalEmployeeText(source.task_type) ||
|
||||
normalizeDigitalEmployeeText(source.code).replace(/^task\.hermes\./i, '')
|
||||
return raw.replace(/[-.]/g, '_')
|
||||
}
|
||||
|
||||
export function isDigitalEmployeeAsset(source = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const haystack = [
|
||||
source.asset_type,
|
||||
source.code,
|
||||
source.name,
|
||||
source.description,
|
||||
config.agent,
|
||||
config.target_agent,
|
||||
config.worker,
|
||||
config.runtime_agent
|
||||
]
|
||||
.map((item) => normalizeDigitalEmployeeText(item).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return (
|
||||
normalizeDigitalEmployeeText(source.asset_type) === 'task' &&
|
||||
(haystack.includes(DIGITAL_EMPLOYEE_AGENT) || haystack.includes('task.hermes.'))
|
||||
)
|
||||
}
|
||||
|
||||
export function formatDigitalEmployeeCron(value) {
|
||||
const raw = normalizeDigitalEmployeeText(value)
|
||||
if (!raw) {
|
||||
return '手动触发'
|
||||
}
|
||||
|
||||
const parts = raw.split(/\s+/)
|
||||
if (parts.length < 5) {
|
||||
return sanitizeDigitalEmployeeText(raw)
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
|
||||
const hourNumber = Number(hour)
|
||||
const minuteNumber = Number(minute)
|
||||
const timeLabel =
|
||||
Number.isFinite(hourNumber) && Number.isFinite(minuteNumber)
|
||||
? `${String(hourNumber).padStart(2, '0')}:${String(minuteNumber).padStart(2, '0')}`
|
||||
: `${hour}:${minute}`
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
return `每天 ${timeLabel}`
|
||||
}
|
||||
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
|
||||
const weekdayLabels = {
|
||||
'0': '周日',
|
||||
'1': '周一',
|
||||
'2': '周二',
|
||||
'3': '周三',
|
||||
'4': '周四',
|
||||
'5': '周五',
|
||||
'6': '周六',
|
||||
'7': '周日'
|
||||
}
|
||||
return `每${weekdayLabels[dayOfWeek] || `周${dayOfWeek}`} ${timeLabel}`
|
||||
}
|
||||
|
||||
return sanitizeDigitalEmployeeText(raw)
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeSchedule(source = {}, content = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
const raw =
|
||||
normalizeDigitalEmployeeText(content.schedule) ||
|
||||
normalizeDigitalEmployeeText(config.cron) ||
|
||||
normalizeDigitalEmployeeText(config.schedule) ||
|
||||
normalizeDigitalEmployeeText(config.cron_expression)
|
||||
return {
|
||||
value: raw,
|
||||
label: formatDigitalEmployeeCron(raw)
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeEnabled(source = {}) {
|
||||
const config = source.config_json || source.configJson || {}
|
||||
if (config.enabled === false || config.is_enabled === false) {
|
||||
return false
|
||||
}
|
||||
if (source.enabled === false || source.is_enabled === false) {
|
||||
return false
|
||||
}
|
||||
return normalizeDigitalEmployeeText(source.status || 'active') === 'active'
|
||||
}
|
||||
|
||||
export function resolveDigitalEmployeeDisplayCode(source = {}, content = {}) {
|
||||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||||
return taskType ? `digital.${taskType}` : 'digital.skill'
|
||||
}
|
||||
|
||||
function formatDigitalEmployeeValue(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => sanitizeDigitalEmployeeText(item)).filter(Boolean).join('、') || '-'
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return sanitizeDigitalEmployeeText(JSON.stringify(value, null, 2))
|
||||
}
|
||||
return sanitizeDigitalEmployeeText(value, '-')
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeContentRows(content = {}) {
|
||||
return Object.entries(content)
|
||||
.filter(([key]) => !HIDDEN_CONTENT_KEYS.has(normalizeDigitalEmployeeText(key).toLowerCase()))
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
label: CONTENT_LABELS[key] || key,
|
||||
value: formatDigitalEmployeeValue(value)
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeContentPreview(content = {}) {
|
||||
const visiblePayload = {}
|
||||
for (const [key, value] of Object.entries(content)) {
|
||||
if (HIDDEN_CONTENT_KEYS.has(normalizeDigitalEmployeeText(key).toLowerCase())) {
|
||||
continue
|
||||
}
|
||||
visiblePayload[key] = value
|
||||
}
|
||||
return sanitizeDigitalEmployeeText(JSON.stringify(visiblePayload, null, 2))
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeListMeta(source = {}) {
|
||||
const content = parseDigitalEmployeeContent(source.current_version_content)
|
||||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||||
const schedule = resolveDigitalEmployeeSchedule(source, content)
|
||||
const enabled = resolveDigitalEmployeeEnabled(source)
|
||||
const fallbackName = TASK_TYPE_LABELS[taskType] || '数字员工技能'
|
||||
|
||||
return {
|
||||
name: sanitizeDigitalEmployeeName(source.name, fallbackName),
|
||||
code: resolveDigitalEmployeeDisplayCode(source, content),
|
||||
summary: sanitizeDigitalEmployeeText(source.description, '面向后台自动执行的数字员工技能。'),
|
||||
category: '数字员工',
|
||||
owner: sanitizeDigitalEmployeeText(source.owner, '平台运营'),
|
||||
reviewer: sanitizeDigitalEmployeeText(source.reviewer, '系统'),
|
||||
scope: schedule.label,
|
||||
scheduleLabel: schedule.label,
|
||||
executionMode: schedule.value ? '定时执行' : '手动触发',
|
||||
enabled,
|
||||
enabledLabel: enabled ? '已启动' : '未启动',
|
||||
enabledTone: enabled ? 'success' : 'disabled',
|
||||
taskType
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeDetailMeta(source = {}) {
|
||||
const content = parseDigitalEmployeeContent(source.current_version_content)
|
||||
const listMeta = buildDigitalEmployeeListMeta({
|
||||
...source,
|
||||
current_version_content: content
|
||||
})
|
||||
const schedule = resolveDigitalEmployeeSchedule(source, content)
|
||||
const contentRows = buildDigitalEmployeeContentRows(content)
|
||||
|
||||
return {
|
||||
...listMeta,
|
||||
rawCode: normalizeDigitalEmployeeText(source.code),
|
||||
description: sanitizeDigitalEmployeeText(
|
||||
source.description,
|
||||
'该技能由后台数字员工按计划执行,并把结果沉淀到对应业务资产或运行日志中。'
|
||||
),
|
||||
contentRows,
|
||||
contentPreview: buildDigitalEmployeeContentPreview(content),
|
||||
scheduleRows: [
|
||||
{ label: '执行计划', value: schedule.label },
|
||||
{ label: '调度表达式', value: schedule.value || '手动触发' },
|
||||
{ label: '启动状态', value: listMeta.enabledLabel, tone: listMeta.enabledTone },
|
||||
{ label: '执行方式', value: listMeta.executionMode }
|
||||
],
|
||||
overviewRows: [
|
||||
{ label: '能力编号', value: listMeta.code },
|
||||
{ label: '业务归口', value: listMeta.owner },
|
||||
{ label: '当前版本', value: source.working_version || source.current_version || '-' },
|
||||
{ label: '最近更新', value: source.updated_at || '-' }
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user