Files
X-Financial/web/src/views/scripts/auditViewDigitalEmployeeModel.js
caoxiaozhu 8a4a777be7 feat: 新增员工行为画像算法与费用风险标签体系
后端新增员工行为画像算法模块,支持标签规则引擎和评分计算,
完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流
和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费
用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台
样式,优化文档中心和归档中心交互,补充单元测试。
2026-05-28 12:09:49 +08:00

387 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
const TASK_TYPE_LABELS = {
daily_risk_scan: '每日风险巡检',
global_risk_scan: '全局风险巡检',
weekly_ar_summary: '周度应收账龄汇总',
weekly_expense_report: '周度费用洞察',
rule_review_digest: '规则待审摘要',
knowledge_index_sync: '知识库归集',
finance_policy_knowledge_organize: '整理公司财务知识制度',
llm_wiki_rule_formation: '知识库归集',
x_financial_callback: '任务回调上报'
}
const TASK_TYPE_SKILL_CATEGORIES = {
daily_risk_scan: '评估',
global_risk_scan: '评估',
weekly_ar_summary: '整理',
weekly_expense_report: '整理',
rule_review_digest: '升级',
knowledge_index_sync: '积累',
finance_policy_knowledge_organize: '整理',
llm_wiki_rule_formation: '积累',
x_financial_callback: '升级'
}
const CONTENT_LABELS = {
task_type: '任务类型',
skill_category: '技能类型',
skill_category_options: '技能类型范围',
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 sanitizeDigitalEmployeeSource(value, fallback = '') {
const text = normalizeDigitalEmployeeText(value)
.replace(/hermes/gi, '数字员工')
.replace(/赫尔墨斯/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 resolveDigitalEmployeeSkillCategory(source = {}, content = {}) {
const config = source.config_json || source.configJson || {}
const taskType = resolveDigitalEmployeeTaskType(source, content)
const explicitCategory =
normalizeDigitalEmployeeText(config.skill_category) ||
normalizeDigitalEmployeeText(config.skillCategory) ||
normalizeDigitalEmployeeText(content.skill_category) ||
normalizeDigitalEmployeeText(content.skillCategory)
if (DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.includes(explicitCategory)) {
return explicitCategory
}
return TASK_TYPE_SKILL_CATEGORIES[taskType] || '整理'
}
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(config.cron) ||
normalizeDigitalEmployeeText(config.schedule) ||
normalizeDigitalEmployeeText(config.cron_expression) ||
normalizeDigitalEmployeeText(content.schedule)
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))
}
function resolveDigitalEmployeeMarkdownFromContent(content = {}, config = {}) {
const candidates = [
content.skill_markdown,
content.skills_markdown,
content.source_markdown,
content.markdown,
content.skill_source,
config.skill_markdown,
config.skills_markdown,
config.source_markdown,
config.skill_source
]
return candidates.find((item) => normalizeDigitalEmployeeText(item)) || ''
}
function buildDefaultDigitalEmployeeSource(source = {}, listMeta = {}, schedule = {}) {
const name = listMeta.name || '数字员工技能'
const description =
listMeta.summary ||
sanitizeDigitalEmployeeText(source.description, '该技能用于后台自动执行指定任务。')
return [
'---',
`name: ${listMeta.code || 'digital.skill'}`,
`description: ${description}`,
'---',
'',
`# ${name}`,
'',
'## 功能说明',
'',
description,
'',
'## 执行方式',
'',
`- 技能类型:${listMeta.skillCategory || '整理'}`,
`- 可选类型:${DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS.join('、')}`,
`- 执行计划:${schedule.label || '手动触发'}`,
`- 触发方式:${listMeta.executionMode || '手动触发'}`,
'',
'## 操作要求',
'',
'- 按任务参数读取业务数据。',
'- 运行完成后写回业务结果或运行日志。'
].join('\n')
}
export function buildDigitalEmployeeSourceMarkdown(source = {}, content = {}, listMeta = {}) {
const config = source.config_json || source.configJson || {}
if (
normalizeDigitalEmployeeText(source.current_version_content_type) === 'markdown' &&
typeof source.current_version_content === 'string'
) {
return sanitizeDigitalEmployeeSource(source.current_version_content)
}
const schedule = resolveDigitalEmployeeSchedule(source, content)
const sourceMarkdown = resolveDigitalEmployeeMarkdownFromContent(content, config)
return sanitizeDigitalEmployeeSource(
sourceMarkdown,
buildDefaultDigitalEmployeeSource(source, listMeta, schedule)
)
}
function buildDigitalEmployeeBasicRows(source = {}, listMeta = {}, schedule = {}) {
return [
{ label: '技能编号', value: listMeta.code },
{ label: '技能类型', value: listMeta.skillCategory },
{ label: '维护人', value: listMeta.owner },
{ label: '执行计划', value: schedule.label },
{ label: '当前版本', value: source.working_version || source.current_version || '-' },
{ label: '最近更新', value: source.updated_at || '-' }
]
}
export function buildDigitalEmployeeListMeta(source = {}) {
const content = parseDigitalEmployeeContent(source.current_version_content)
const taskType = resolveDigitalEmployeeTaskType(source, content)
const skillCategory = resolveDigitalEmployeeSkillCategory(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: '数字员工',
skillCategory,
skillCategoryOptions: DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
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)
const sourceMarkdown = buildDigitalEmployeeSourceMarkdown(source, content, listMeta)
return {
...listMeta,
rawCode: normalizeDigitalEmployeeText(source.code),
description: sanitizeDigitalEmployeeText(
source.description,
'该技能由后台数字员工按计划执行,并把结果沉淀到对应业务资产或运行日志中。'
),
sourceMarkdown,
basicRows: buildDigitalEmployeeBasicRows(source, listMeta, schedule),
contentRows,
contentPreview: buildDigitalEmployeeContentPreview(content),
scheduleRows: [
{ label: '执行计划', value: schedule.label },
{ label: '调度表达式', value: schedule.value || '手动触发' },
{ label: '启动状态', value: listMeta.enabledLabel, tone: listMeta.enabledTone },
{ label: '执行方式', value: listMeta.executionMode },
{ label: '技能类型', value: listMeta.skillCategory }
],
overviewRows: [
{ label: '能力编号', value: listMeta.code },
{ label: '业务归口', value: listMeta.owner },
{ label: '当前版本', value: source.working_version || source.current_version || '-' },
{ label: '最近更新', value: source.updated_at || '-' }
]
}
}