Files
X-Financial/web/src/utils/employeeProfileViewModel.js

607 lines
18 KiB
JavaScript
Raw Normal View History

const PROFILE_TYPE_LABELS = {
expense: '费用申请',
process_quality: '流程质量',
ai_usage: 'AI 协作',
approval: '审核行为'
}
const STATUS_LABELS = {
succeeded: '已完成',
success: '已完成',
running: '进行中',
blocked: '待确认',
failed: '失败'
}
const STATUS_TONES = {
succeeded: 'success',
success: 'success',
running: 'warning',
blocked: 'warning',
failed: 'danger'
}
const AGENT_LABELS = {
hermes: 'Hermes 数字员工',
user_agent: '智能问答助手',
orchestrator: '智能编排服务',
system: '系统服务'
}
const AGENT_SHORT_LABELS = {
hermes: 'Hermes',
user_agent: '问答助手',
orchestrator: '编排服务',
system: '系统服务'
}
const RADAR_COLORS = [
'#3a7ca5',
'#0f9f8f',
'#f59e0b',
'#7c3aed',
'#dc2626',
'#2563eb',
'#16a34a',
'#db2777'
]
const FINANCIAL_RADAR_CODES = [
'expense_intensity',
'application_rhythm',
'travel_entertainment',
'material_completeness',
'process_pressure'
]
const GOVERNANCE_RADAR_CODES = [
'ai_collaboration',
'approval_efficiency',
'approval_control'
]
export const USER_PROFILE_RADAR_VIEW_OPTIONS = [
{
value: 'financial_risk',
label: '财务风险视角',
shortLabel: '财务风险',
description: '费用、材料和流程相关维度'
},
{
value: 'collaboration_governance',
label: '协作治理视角',
shortLabel: '协作治理',
description: 'AI 协作和审批治理维度'
},
{
value: 'all_behavior',
label: '全部行为视角',
shortLabel: '全部行为',
description: '展示全部可用画像维度'
}
]
const TAG_ACCENT_COUNT = 8
const SOURCE_LABELS = {
user_message: '用户对话',
schedule: '定时任务',
system_event: '系统事件',
workbench: '个人工作台',
detail: '单据详情',
documents_application: '单据中心'
}
const SCENARIO_LABELS = {
knowledge: '知识库问答',
expense: '费用报销',
reimbursement: '费用报销',
expense_application: '费用申请',
application: '费用申请',
budget: '预算查询',
audit: '风险审核',
approval: '审批处理',
policy: '制度问答',
travel: '差旅费用',
entertainment: '业务招待',
accounts_receivable: '应收查询',
accounts_payable: '应付查询'
}
const INTENT_LABELS = {
query: '查询',
explain: '解释',
compare: '对比',
risk_check: '风险检查',
draft: '草稿生成',
operate: '操作办理',
review: '审核',
submit: '提交'
}
const JOB_TYPE_LABELS = {
knowledge_index_sync: '知识库索引同步',
llm_wiki_sync: '知识库归纳同步',
employee_behavior_profile_scan: '用户画像测算',
workbench_on_demand: '工作台画像测算',
global_risk_scan: '财务风险图谱巡检',
weekly_expense_report: '周费用报告'
}
export function buildUserProfileMetricCards(profile, runs = [], currentUser = {}) {
const index = indexProfiles(profile)
const aiMetrics = metricsOf(index.ai_usage)
const userRuns = filterRunsByCurrentUser(runs, currentUser)
const windowedUserRuns = filterRunsByProfileWindow(userRuns, profile)
const durationMs = resolveUsageDurationMs(aiMetrics, windowedUserRuns)
const durationDisplay = formatDurationMetric(durationMs)
const commonAgent = resolveCommonAgent(windowedUserRuns)
const tokenCount = resolveNumber(aiMetrics.exact_token_count) || resolveNumber(aiMetrics.estimated_token_count)
const tokenDisplay = formatTokenCount(tokenCount)
const aiRunCount = resolveNumber(aiMetrics.ai_run_count) || windowedUserRuns.length
return [
{
key: 'usage-duration',
label: '使用时长',
value: durationDisplay.value,
unit: durationDisplay.unit,
hint: resolveUsageDurationHint(aiMetrics, profile),
icon: 'mdi mdi-timer-sand',
tone: 'primary'
},
{
key: 'common-agent',
label: '常用智能体',
value: commonAgent.label,
unit: '',
hint: commonAgent.count ? `${commonAgent.count} 次调用,占比 ${commonAgent.share}` : '暂无智能体调用记录',
icon: 'mdi mdi-account-tie-voice-outline',
tone: 'cyan'
},
{
key: 'ai-usage',
label: 'AI 使用次数',
value: formatNumber(aiRunCount),
unit: '次',
hint: `${resolveWindowDays(profile)}天智能协作记录`,
icon: 'mdi mdi-robot-outline',
tone: 'violet'
},
{
key: 'token-usage',
label: 'Token 消耗',
value: tokenDisplay.value,
unit: tokenDisplay.unit,
hint: resolveTokenHint(aiMetrics),
icon: 'mdi mdi-lightning-bolt-outline',
tone: 'amber'
}
]
}
export function buildUserProfileSummaryMetrics(profile, runs = [], currentUser = {}) {
return buildUserProfileMetricCards(profile, runs, currentUser).slice(0, 4)
}
export function normalizeUserProfileTags(profile, limit = 8) {
return (Array.isArray(profile?.profile_tags) ? profile.profile_tags : [])
.map((tag) => ({
code: normalizeText(tag.code || tag.label),
label: normalizeText(tag.label),
displayLabel: normalizeText(tag.display_label || tag.displayLabel || tag.label),
category: normalizeCode(tag.category),
tone: resolveTagTone(tag),
score: clampScore(tag.score),
reason: normalizeText(tag.reason) || '画像算法已识别该行为特征。',
confidence: resolveNumber(tag.confidence),
radarDimensions: normalizeRadarDimensions(tag)
}))
.filter((tag) => tag.code && tag.displayLabel)
.sort((left, right) => right.score - left.score)
.slice(0, limit)
.map((tag, index) => ({
...tag,
colorIndex: index % TAG_ACCENT_COUNT
}))
}
export function normalizeUserProfileRadarDimensions(profile) {
const dimensions = Array.isArray(profile?.radar?.dimensions) ? profile.radar.dimensions : []
if (dimensions.length) {
return withRadarColors(
dimensions.map((item) => ({
code: normalizeText(item.code || item.label),
label: normalizeText(item.label || item.code),
score: clampScore(item.score)
}))
)
}
return withRadarColors(
(Array.isArray(profile?.profiles) ? profile.profiles : [])
.map((item) => ({
code: normalizeText(item.profile_type),
label: PROFILE_TYPE_LABELS[item.profile_type] || normalizeText(item.profile_label || item.profile_type),
score: clampScore(item.score)
}))
.filter((item) => item.code && item.label)
)
}
export function filterUserProfileRadarDimensions(dimensions, viewKey) {
const items = Array.isArray(dimensions) ? dimensions : []
const codes = resolveRadarViewCodes(viewKey)
if (!codes.length) {
return items
}
const filtered = items.filter((item) => codes.includes(normalizeCode(item?.code)))
return filtered.length ? filtered : items
}
export function filterUserProfileTagsByRadarView(tags, viewKey) {
const items = Array.isArray(tags) ? tags : []
const codes = resolveRadarViewCodes(viewKey)
if (!codes.length) {
return items
}
return items.filter((tag) => {
const dimensions = Array.isArray(tag?.radarDimensions) ? tag.radarDimensions : []
if (dimensions.some((code) => codes.includes(normalizeCode(code)))) {
return true
}
return resolveFallbackTagRadarCodes(tag).some((code) => codes.includes(code))
})
}
export function resolveUserProfileDefaultRadarView(profile) {
const profileTypes = new Set(
(Array.isArray(profile?.profiles) ? profile.profiles : [])
.map((item) => normalizeCode(item?.profile_type))
.filter(Boolean)
)
if (profileTypes.has('expense') || profileTypes.has('process_quality')) {
return 'financial_risk'
}
if (profileTypes.has('ai_usage') || profileTypes.has('approval')) {
return 'collaboration_governance'
}
const dimensions = normalizeUserProfileRadarDimensions(profile)
const financialScore = sumRadarScores(dimensions, FINANCIAL_RADAR_CODES)
const governanceScore = sumRadarScores(dimensions, GOVERNANCE_RADAR_CODES)
if (financialScore > 0 || governanceScore > 0) {
return financialScore >= governanceScore ? 'financial_risk' : 'collaboration_governance'
}
return 'all_behavior'
}
export function buildProfileOperationsFromAgentRuns(runs, currentUser, limit = 5) {
const identities = resolveCurrentUserIdentities(currentUser)
return (Array.isArray(runs) ? runs : [])
.filter((run) => belongsToCurrentUser(run, identities))
.sort((left, right) => Date.parse(right.started_at || 0) - Date.parse(left.started_at || 0))
.slice(0, limit)
.map((run, index) => ({
id: normalizeText(run.run_id || run.id) || `operation-${index + 1}`,
time: formatOperationTime(run.started_at),
action: resolveOperationAction(run),
target: resolveOperationTarget(run),
channel: resolveOperationChannel(run),
status: STATUS_LABELS[normalizeCode(run.status)] || normalizeText(run.status) || '未知',
tone: STATUS_TONES[normalizeCode(run.status)] || 'info'
}))
}
export function resolveCurrentUserProfileError(error) {
return normalizeText(error?.message) || '用户画像读取失败,请稍后重试。'
}
function indexProfiles(profile) {
return Object.fromEntries(
(Array.isArray(profile?.profiles) ? profile.profiles : [])
.map((item) => [normalizeText(item.profile_type), item])
.filter(([key]) => key)
)
}
function metricsOf(profile) {
return profile?.metrics && typeof profile.metrics === 'object' ? profile.metrics : {}
}
function resolveRadarViewCodes(viewKey) {
if (viewKey === 'financial_risk') {
return FINANCIAL_RADAR_CODES
}
if (viewKey === 'collaboration_governance') {
return GOVERNANCE_RADAR_CODES
}
return []
}
function resolveFallbackTagRadarCodes(tag) {
const category = normalizeCode(tag?.category)
if (['expense', 'travel', 'entertainment', 'process'].includes(category)) {
return FINANCIAL_RADAR_CODES
}
if (['ai', 'approval'].includes(category)) {
return GOVERNANCE_RADAR_CODES
}
return []
}
function normalizeRadarDimensions(tag) {
const dimensions = Array.isArray(tag?.radar_dimensions)
? tag.radar_dimensions
: Array.isArray(tag?.radarDimensions)
? tag.radarDimensions
: []
return dimensions.map((item) => normalizeCode(item)).filter(Boolean)
}
function sumRadarScores(dimensions, codes) {
const codeSet = new Set(codes)
return (Array.isArray(dimensions) ? dimensions : [])
.filter((item) => codeSet.has(normalizeCode(item?.code)))
.reduce((total, item) => total + clampScore(item?.score), 0)
}
function hasProfileDurationMetric(metrics) {
return (
Object.prototype.hasOwnProperty.call(metrics || {}, 'usage_duration_ms')
|| Object.prototype.hasOwnProperty.call(metrics || {}, 'online_duration_ms')
|| Object.prototype.hasOwnProperty.call(metrics || {}, 'ai_run_duration_ms')
)
}
function resolveUsageDurationMs(metrics, fallbackRuns) {
if (!hasProfileDurationMetric(metrics)) {
return sumRunDurationMs(fallbackRuns)
}
return resolveNumber(metrics.usage_duration_ms)
|| resolveNumber(metrics.online_duration_ms)
|| resolveNumber(metrics.ai_run_duration_ms)
}
function resolveUsageDurationHint(metrics, profile) {
const days = resolveWindowDays(profile)
if (normalizeCode(metrics?.usage_duration_mode) === 'online_session') {
return `${days}天在线会话累计`
}
if (normalizeCode(metrics?.usage_duration_mode) === 'agent_run_fallback') {
return `${days}天智能体运行累计`
}
return `${days}天使用行为累计`
}
function filterRunsByCurrentUser(runs, currentUser) {
const identities = resolveCurrentUserIdentities(currentUser)
return (Array.isArray(runs) ? runs : []).filter((run) => belongsToCurrentUser(run, identities))
}
function filterRunsByProfileWindow(runs, profile) {
const cutoff = Date.now() - resolveWindowDays(profile) * 24 * 60 * 60 * 1000
return (Array.isArray(runs) ? runs : []).filter((run) => {
const startedAt = Date.parse(run?.started_at || '')
return Number.isFinite(startedAt) && startedAt >= cutoff
})
}
function belongsToCurrentUser(run, identities) {
if (!identities.size) {
return false
}
const userId = normalizeText(run?.user_id).toLowerCase()
return Boolean(userId && identities.has(userId))
}
function resolveCurrentUserIdentities(user = {}) {
return new Set(
[
user.username,
user.email,
user.name,
user.employeeNo,
user.employee_no
]
.map((item) => normalizeText(item).toLowerCase())
.filter(Boolean)
)
}
function resolveCommonAgent(runs) {
const counts = new Map()
for (const run of runs) {
const code = normalizeCode(run?.agent || run?.route_json?.selected_agent || 'system') || 'system'
counts.set(code, (counts.get(code) || 0) + 1)
}
const [code = '', count = 0] = Array.from(counts.entries())
.sort((left, right) => right[1] - left[1])[0] || []
if (!code || !count) {
return { label: '暂无', count: 0, share: '0%' }
}
return {
label: AGENT_SHORT_LABELS[code] || translateKnownValue(code, AGENT_LABELS, '智能体') || '智能体',
count,
share: formatPercent(count / Math.max(1, runs.length))
}
}
function sumRunDurationMs(runs) {
return runs.reduce((total, run) => total + resolveRunDurationMs(run), 0)
}
function resolveRunDurationMs(run) {
const startedAt = Date.parse(run?.started_at || '')
const finishedAt = Date.parse(run?.finished_at || '')
if (Number.isFinite(startedAt) && Number.isFinite(finishedAt) && finishedAt > startedAt) {
return Math.min(finishedAt - startedAt, 24 * 60 * 60 * 1000)
}
return (Array.isArray(run?.tool_calls) ? run.tool_calls : []).reduce(
(total, tool) => total + Math.max(0, resolveNumber(tool?.duration_ms)),
0
)
}
function resolveOperationAction(run) {
const semanticText = normalizeText(run?.semantic_parse?.raw_query)
if (semanticText) {
return `${resolveOperationBusinessLabel(run)}${semanticText}`
}
return translateKnownValue(run?.result_summary, JOB_TYPE_LABELS, '')
|| translateKnownValue(run?.route_json?.job_type, JOB_TYPE_LABELS, '执行系统任务')
|| '执行智能财务任务'
}
function resolveOperationTarget(run) {
return translateKnownValue(run?.route_json?.task_title, JOB_TYPE_LABELS, '')
|| translateKnownValue(run?.route_json?.asset_name, JOB_TYPE_LABELS, '')
|| translateKnownValue(run?.semantic_parse?.scenario, SCENARIO_LABELS, '业务操作')
|| translateKnownValue(run?.task_id, JOB_TYPE_LABELS, '系统任务')
|| '个人工作台'
}
function resolveOperationChannel(run) {
const agent = translateKnownValue(run?.agent, AGENT_LABELS, '智能服务') || 'Hermes 数字员工'
const source = translateKnownValue(run?.source, SOURCE_LABELS, '系统入口')
return source ? `${agent} · ${source}` : agent
}
function resolveOperationBusinessLabel(run) {
const scenario = translateKnownValue(run?.semantic_parse?.scenario, SCENARIO_LABELS, '业务操作')
const intent = translateKnownValue(run?.semantic_parse?.intent, INTENT_LABELS, '')
if (scenario && intent) {
return `${scenario}${intent}`
}
return scenario || intent || '发起'
}
function resolveTagTone(tag) {
const polarity = normalizeText(tag?.polarity || tag?.tone).toLowerCase()
if (['risk', 'danger', 'negative'].includes(polarity)) {
return 'risk'
}
if (['positive', 'success'].includes(polarity)) {
return 'positive'
}
return 'behavior'
}
function resolveWindowDays(profile) {
const days = Number(profile?.window_days || 90)
return Number.isFinite(days) && days > 0 ? Math.round(days) : 90
}
function resolveTokenHint(metrics) {
const mode = normalizeText(metrics.token_count_mode)
return mode === 'estimated_token_count' ? '按运行载荷估算' : '模型调用累计'
}
function formatTokenCount(value) {
const count = resolveNumber(value)
if (count >= 10000) {
return { value: trimNumber(count / 10000, 2), unit: '万' }
}
return { value: formatNumber(count), unit: 'tokens' }
}
function formatDurationMetric(totalMs) {
const seconds = Math.round(Math.max(0, resolveNumber(totalMs)) / 1000)
if (seconds < 60) {
return { value: formatNumber(seconds), unit: '秒' }
}
const minutes = seconds / 60
if (minutes < 60) {
return { value: trimNumber(minutes, minutes >= 10 ? 0 : 1), unit: '分钟' }
}
const hours = minutes / 60
return { value: trimNumber(hours, hours >= 10 ? 0 : 1), unit: '小时' }
}
function withRadarColors(items) {
return items.map((item, index) => ({
...item,
color: item.color || RADAR_COLORS[index % RADAR_COLORS.length]
}))
}
function formatOperationTime(value) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return '时间未知'
}
const now = new Date()
const sameDay = date.toDateString() === now.toDateString()
const yesterday = new Date(now)
yesterday.setDate(now.getDate() - 1)
const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`
if (sameDay) {
return `今天 ${time}`
}
if (date.toDateString() === yesterday.toDateString()) {
return `昨天 ${time}`
}
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${time}`
}
function formatNumber(value) {
return String(Math.round(resolveNumber(value)))
}
function formatPercent(value) {
return `${Math.round(resolveNumber(value) * 100)}%`
}
function formatMoney(value) {
return `¥${trimNumber(resolveNumber(value), 2)}`
}
function trimNumber(value, digits = 0) {
const number = Number(value || 0)
return number.toFixed(digits).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1')
}
function clampScore(value) {
const score = Math.round(resolveNumber(value))
return Math.max(0, Math.min(100, score))
}
function resolveNumber(value) {
const number = Number(value || 0)
return Number.isFinite(number) ? number : 0
}
function normalizeText(value) {
return String(value || '').trim()
}
function normalizeCode(value) {
return normalizeText(value).toLowerCase()
}
function translateKnownValue(value, labels, internalFallback = '') {
const raw = normalizeText(value)
if (!raw) {
return ''
}
const mapped = labels[normalizeCode(raw)]
if (mapped) {
return mapped
}
return isInternalCode(raw) ? internalFallback : raw
}
function isInternalCode(value) {
return /^[a-z][a-z0-9_:-]*$/i.test(normalizeText(value))
}
function pad(value) {
return String(value).padStart(2, '0')
}