- 新增数字员工财务报告生成、邮件投递与渲染调度器 - 引入员工画像扫描调度与定时提醒任务 - 完善财务看板快照、排行口径与部门人员占比计算 - 优化数字员工工作看板仪表盘与技能目录 - 增强前端总览页图表、工作台摘要与顶部导航栏交互 - 新增差旅申请规划推动提醒与报销创建会话状态管理 - 补充财务报告、看板调度、数字员工工作记录测试覆盖
419 lines
14 KiB
JavaScript
419 lines
14 KiB
JavaScript
const DIGITAL_EMPLOYEE_AGENT = 'hermes'
|
||
export const DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS = ['积累', '升级', '整理', '评估']
|
||
|
||
export const DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES = new Set([
|
||
'finance_dashboard_snapshot',
|
||
'digital_employee_reminder_scan',
|
||
'employee_behavior_profile_scan',
|
||
'department_expense_baseline_accumulate',
|
||
'budget_overrun_precontrol_evaluate',
|
||
'multi_evidence_consistency_evaluate',
|
||
'travel_spatiotemporal_consistency_evaluate',
|
||
'global_risk_scan',
|
||
'finance_policy_knowledge_organize'
|
||
])
|
||
|
||
const TASK_TYPE_LABELS = {
|
||
finance_dashboard_snapshot: '财务经营快照沉淀',
|
||
digital_employee_reminder_scan: '定时提醒与待办扫描',
|
||
daily_risk_scan: '每日风险巡检',
|
||
global_risk_scan: '财务风险图谱巡检',
|
||
employee_behavior_profile_scan: '员工行为画像巡检',
|
||
department_expense_baseline_accumulate: '部门费用基线沉淀',
|
||
budget_overrun_precontrol_evaluate: '预算占用与超标预警',
|
||
multi_evidence_consistency_evaluate: '单据多凭证一致性评估',
|
||
travel_spatiotemporal_consistency_evaluate: '差旅时空一致性评估',
|
||
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 = {
|
||
finance_dashboard_snapshot: '整理',
|
||
digital_employee_reminder_scan: '升级',
|
||
daily_risk_scan: '评估',
|
||
global_risk_scan: '评估',
|
||
employee_behavior_profile_scan: '积累',
|
||
department_expense_baseline_accumulate: '积累',
|
||
budget_overrun_precontrol_evaluate: '评估',
|
||
multi_evidence_consistency_evaluate: '评估',
|
||
travel_spatiotemporal_consistency_evaluate: '评估',
|
||
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 shouldDisplayDigitalEmployeeAsset(source = {}) {
|
||
const content = parseDigitalEmployeeContent(source.current_version_content)
|
||
const taskType = resolveDigitalEmployeeTaskType(source, content)
|
||
return DIGITAL_EMPLOYEE_VISIBLE_TASK_TYPES.has(taskType)
|
||
}
|
||
|
||
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 || '-' }
|
||
]
|
||
}
|
||
}
|