import type { VersionWithContext } from './derive'; import type { XiaobaoRiskInsight } from './xiaobao-risk-cache'; import type { XiaobaoVersionRisk } from './xiaobao-risk'; import { buildRiskInsightDisplaySignature, normalizeRiskInsightDisplaySignature } from './xiaobao-risk-ai'; import { WORK_HOURS } from './work-hours'; const UNFINISHED_VERSION_STATUSES = new Set(['planned', 'developing', 'paused']); const HIGH_RISK_LEVELS = new Set(['at_risk', 'likely_delayed', 'blocked']); const PAGE_REFRESH_ADVICE_PATTERNS = [ /(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i, /(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i, /(手动|主动).*(触发|刷新).*(更新|预警|分析)/i, /(manual|manually).*(refresh|reload|trigger)/i, /(refresh|reload).*(page|browser|xiaobao|warning)/i, ]; export interface XiaobaoWarningVersionFilter { canManage: boolean; userName?: string; } export type XiaobaoWarningRiskFilter = 'all' | 'attention' | 'high'; export interface XiaobaoWarningRiskListFilter { riskFilter?: XiaobaoWarningRiskFilter; productId?: string; projectId?: string; } export type RiskScoreTone = 'danger' | 'warn' | 'ok'; export interface XiaobaoWarningReadState { userId: string; versionId: string; signature: string; readAt: string; } export function filterXiaobaoWarningVersions( versions: VersionWithContext[], filter: XiaobaoWarningVersionFilter, ): VersionWithContext[] { return versions.filter((version) => { if (!UNFINISHED_VERSION_STATUSES.has(version.status)) return false; if (filter.canManage) return true; if (!filter.userName) return false; return (version.members ?? []).some((member) => member.name === filter.userName); }); } export function filterXiaobaoRiskWarnings( risks: XiaobaoVersionRisk[], filter: XiaobaoWarningRiskListFilter = {}, ): XiaobaoVersionRisk[] { return risks.filter((risk) => { if (risk.riskLevel === 'on_track') return false; if (filter.productId && risk.productId !== filter.productId) return false; if (filter.projectId && risk.projectId !== filter.projectId) return false; if (filter.riskFilter === 'attention') return true; if (filter.riskFilter === 'high') return HIGH_RISK_LEVELS.has(risk.riskLevel); return true; }); } export function getXiaobaoWarningRiskCount(risks: XiaobaoVersionRisk[]): number { return filterXiaobaoRiskWarnings(risks).length; } export function getXiaobaoWarningUpdateSignature(risk: XiaobaoVersionRisk): string { return buildRiskInsightDisplaySignature(risk); } export function isXiaobaoWarningUpdated( risk: XiaobaoVersionRisk, readStates: XiaobaoWarningReadState[], userId?: string, ): boolean { if (!userId) return false; if (risk.aiInsightUpdating) return false; const currentSignature = getXiaobaoWarningUpdateSignature(risk); const readState = readStates.find((item) => item.userId === userId && item.versionId === risk.versionId); const readSignature = readState?.signature ? normalizeRiskInsightDisplaySignature(readState.signature) ?? readState.signature : undefined; return readSignature !== currentSignature; } export function getXiaobaoWarningUnreadUpdateCount( risks: XiaobaoVersionRisk[], readStates: XiaobaoWarningReadState[], userId?: string, ): number { return risks.filter((risk) => isXiaobaoWarningUpdated(risk, readStates, userId)).length; } export function markXiaobaoWarningRead( readStates: XiaobaoWarningReadState[], userId: string, risk: XiaobaoVersionRisk, readAt: string, ): XiaobaoWarningReadState[] { if (risk.aiInsightUpdating) return readStates; const item: XiaobaoWarningReadState = { userId, versionId: risk.versionId, signature: getXiaobaoWarningUpdateSignature(risk), readAt, }; return [ item, ...readStates.filter((row) => row.userId !== userId || row.versionId !== risk.versionId), ]; } export function getRiskScoreTone(score: number): RiskScoreTone { if (score >= 75) return 'danger'; if (score >= 55) return 'warn'; return 'ok'; } export function formatRemainingWork(hours: number): string { const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 0; const days = safeHours / WORK_HOURS.hoursPerDay; return `${formatNumber(safeHours)}h / ${formatNumber(days)}天`; } export function sanitizeRiskInsight(insight: XiaobaoRiskInsight): XiaobaoRiskInsight { return { ...insight, suggestedActions: insight.suggestedActions.filter((action) => !isPageRefreshAdvice(action)), ownerHints: insight.ownerHints.filter((hint) => !isPageRefreshAdvice(hint)), }; } function formatNumber(value: number): string { if (Number.isInteger(value)) return String(value); return value.toFixed(1).replace(/\.0$/, ''); } function isPageRefreshAdvice(text: string): boolean { return PAGE_REFRESH_ADVICE_PATTERNS.some((pattern) => pattern.test(text)); }