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') }