diff --git a/ROADMAP.md b/ROADMAP.md index 2db92b4..419f4a0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -507,6 +507,7 @@ services/api.ts (1324L) | Q33 | 前端巨型组件拆分(DashboardRecent 518L→25L (-95%),提取 dashboard-recent/ 7 个文件 (types 48L + shared-components(StatusDot/SeverityBadge) 35L + AlertMessagesCard 72L + CertificateExpiryCard 99L + ReleaseHistoryCard 99L + OpsOverviewCard 77L + KubernetesClusterCard 97L),主文件改为 barrel re-export 保持向后兼容,tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** | | Q34 | 前端巨型组件拆分(AIOpsAssistant 505L→288L (-43%),提取 aiops-assistant/ 5 个文件 (types 39L Message/QuickAction/defaultConfig/fmtTime + context-loaders.ts 69L loadAlerts/loadMetrics/loadReport 3 个平台数据加载纯函数 + quick-actions.tsx 17L + ChatMessage 47L 消息气泡(含 streaming/thinking/error 状态) + AssistantSidebar 76L 快捷操作+能力卡片),tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** | | Q35 | 前端巨型组件拆分(AgentCLI 521L→399L (-23%),提取 agent-cli/ 2 个文件 (event-handler.ts 149L createWsEventHandler 工厂函数封装 8 种 WS 事件分发+高危命令确认 + ChatInputArea.tsx 44L 受控输入区组件),导出 ConfirmOptions 接口供跨模块引用,tsc -b=0,npm build✓,ESLint=0/66/0,go build=0,17/17 测试通过。最后一个 ≥500L 业务组件拆分完成,剩余最大业务组件 UnifiedQueryPanel 529L(已从 682L 拆分)) | P0 | **done** | +| Q36 | 前端巨型工具文件拆分(predictUtils.ts 716L→49L barrel re-export (-93%),提取 predict-types.ts 140L 类型+常量 + capacity-predict.ts 194L 容量预测函数(linearRegression+computePredictions+buildCapacityInsightPrompt) + cost-predict.ts 434L 云成本预测函数(computeCostPredictions+buildCostInsightPrompt),3 个消费者 import 路径无需修改,tsc -b=0,npm build✓ (17.26s),ESLint=0/66/0,go build=0,17/17 测试通过) | P0 | **done** | --- diff --git a/frontend/src/components/aiops/capacity-predict.ts b/frontend/src/components/aiops/capacity-predict.ts new file mode 100644 index 0000000..3169bf8 --- /dev/null +++ b/frontend/src/components/aiops/capacity-predict.ts @@ -0,0 +1,194 @@ +import type { JsonData } from '@/types/common'; +import i18n from '@/i18n'; +import { + METRIC_DEFS, + TIME_RANGE_LABELS, + TIME_RANGE_HOURS, + type PredictionMetric, + type CapacityForecast, + type ComputePredictionsInput, + type ComputedPredictions, +} from './predict-types'; + +/** + * 容量预测 — 线性回归 + 指标预测 + 容量规划 + AI 解读 prompt + * + * 从 predictUtils.ts (716L) 提取。 + * 所有函数均为纯函数(输入 → 输出,无副作用),便于单元测试和复用。 + * // Logan.Li + */ + +// ============ 纯函数工具 ============ + +/** 简单最小二乘线性回归,返回斜率和 R² */ +export function linearRegression(values: number[]): { slope: number; r2: number } { + const n = values.length; + if (n < 2) return { slope: 0, r2: 0 }; + const xs = values.map((_, i) => i); + const meanX = xs.reduce((a, b) => a + b, 0) / n; + const meanY = values.reduce((a, b) => a + b, 0) / n; + let num = 0, + den = 0, + ssTot = 0, + ssRes = 0; + for (let i = 0; i < n; i++) { + num += (xs[i] - meanX) * (values[i] - meanY); + den += (xs[i] - meanX) ** 2; + ssTot += (values[i] - meanY) ** 2; + } + const slope = den === 0 ? 0 : num / den; + const intercept = meanY - slope * meanX; + for (let i = 0; i < n; i++) { + const pred = intercept + slope * xs[i]; + ssRes += (values[i] - pred) ** 2; + } + const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot); + return { slope, r2 }; +} + +/** 从时序指标中提取按时间排序的数值序列 */ +export function extractSeries(metrics: JsonData[], name: string): number[] { + return metrics + .filter((m) => m.name === name) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) + .map((m) => Number(m.value) || 0); +} + +/** 容量规划建议 */ +export function capacityRecommendation(metricKey: string, days: number): string { + const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal'; + const recs: Record> = { + urgent: { + cpu: i18n.t('aiops.predictutilsts.s4d0bd643'), + memory: i18n.t('aiops.predictutilsts.sb4593f3a'), + disk: i18n.t('aiops.predictutilsts.s6c25383f'), + }, + warning: { + cpu: i18n.t('aiops.predictutilsts.s8ea826f4'), + memory: i18n.t('aiops.predictutilsts.sb08bd5fc'), + disk: i18n.t('aiops.predictutilsts.s4cdda5dc'), + }, + normal: { + cpu: i18n.t('aiops.predictutilsts.sfa295c44'), + memory: i18n.t('aiops.predictutilsts.s7156a4c0'), + disk: i18n.t('aiops.predictutilsts.s0073f3a3'), + }, + }; + return recs[level]?.[metricKey] || i18n.t('aiops.predictutilsts.sfeb368e6'); +} + +// ============ 综合预测推算 ============ + +/** + * 综合预测推算:基于时序指标计算指标预测与容量规划。 + * 主组件 AIPredictiveAnalytics.tsx 直接调用此函数获取展示数据。 + * // Logan.Li + */ +export function computePredictions(input: ComputePredictionsInput): ComputedPredictions { + const { rawMetrics, serverStats, selectedTimeRange } = input; + const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24; + const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || i18n.t('aiops.predictutilsts.sfc78646e'); + + // ---- 指标预测 ---- + const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => { + const series = extractSeries(rawMetrics, def.key); + const { slope, r2 } = linearRegression(series); + const avgFallback = + def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; + const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; + let predicted = current + slope * horizonHours; + predicted = Math.max(0, Math.min(100, predicted)); + const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable'; + const confidence = Math.round(60 + r2 * 35); + return { + name: def.label, + current: Math.round(current), + predicted: Math.round(predicted), + unit: '%', + trend, + confidence, + timeRange: timeRangeLabel, + }; + }); + + // ---- 容量规划 ---- + const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => { + const series = extractSeries(rawMetrics, def.key); + const { slope } = linearRegression(series); + const avgFallback = + def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; + const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0); + const slopePerDay = slope * 24; + const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7)); + const threshold = def.threshold; + let daysUntilThreshold = 999; + if (slopePerDay > 0.01) { + daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay); + daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold; + } + const resourceNames: Record = { + cpu: '服务器CPU算力', + memory: '服务器内存', + disk: '存储磁盘空间', + }; + return { + resource: resourceNames[def.key] || def.label, + metricKey: def.key, + currentUsage, + predictedUsage, + threshold, + daysUntilThreshold, + recommendation: capacityRecommendation(def.key, daysUntilThreshold), + }; + }).filter((f) => f.currentUsage > 0); + + // ---- 概览统计 ---- + const avgR2 = METRIC_DEFS.length > 0 + ? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) / + METRIC_DEFS.length + : 0; + const accuracy = Math.round(78 + avgR2 * 17); + const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length; + + return { + predictions, + capacityForecasts, + accuracy, + capacityWarningCount, + }; +} + +/** 趋势图标颜色工具,供主组件 render 用 */ +export function trendColorClass(trend: string): string { + switch (trend) { + case 'up': + return 'dg-text-red'; + case 'down': + return 'dg-text-green'; + default: + return 'dg-text-secondary'; + } +} + +/** + * 构建容量预测「AI 深度解读」的 prompt —— 把硬数据序列化为结构化上下文喂给 LLM。 // Logan.Li + * 纯函数,供 PredictiveCapacityTab 调 aiApi.chat 使用。无数据返回空串(调用方据此禁用按钮)。 + */ +export function buildCapacityInsightPrompt(forecasts: CapacityForecast[]): string { + if (forecasts.length === 0) return ''; + const lines = forecasts.map((f) => { + const risk = f.daysUntilThreshold >= 999 ? i18n.t('aiops.predictutilsts.s179fd5a9') : i18n.t('aiops.predictutilsts.daysUntilThreshold', { days: f.daysUntilThreshold }); + return `- ${f.resource}:当前 ${f.currentUsage}% → 7天后预测 ${f.predictedUsage}%(阈值 ${f.threshold}%),${risk}`; + }); + return [ + i18n.t('aiops.predictutilsts.s3b46e5a0'), + '', + i18n.t('aiops.predictutilsts.s586bac5d'), + ...lines, + '', + i18n.t('aiops.predictutilsts.sdf6ebee3'), + i18n.t('aiops.predictutilsts.s534b6893'), + i18n.t('aiops.predictutilsts.saf173e7a'), + i18n.t('aiops.predictutilsts.s4ed1d035'), + ].join('\n'); +} diff --git a/frontend/src/components/aiops/cost-predict.ts b/frontend/src/components/aiops/cost-predict.ts new file mode 100644 index 0000000..23c9a2f --- /dev/null +++ b/frontend/src/components/aiops/cost-predict.ts @@ -0,0 +1,434 @@ +import type { JsonData } from '@/types/common'; +import i18n from '@/i18n'; +import { linearRegression } from './capacity-predict'; +import type { + DailyCostPoint, + CostDimensionForecast, + BudgetRiskForecast, + CostPredictionSummary, + ComputedCostPredictions, + ComputeCostPredictionsInput, +} from './predict-types'; + +/** + * 云成本预测 — 月末费用预测 + 预算触线风险 + AI 解读 prompt + * + * 从 predictUtils.ts (716L) 提取。 + * 所有函数均为纯函数(输入 → 输出,无副作用),便于单元测试和复用。 + * // Logan.Li + */ + +// ============ 私有常量 ============ + +const PROVIDER_LABELS: Record = { + aliyun: i18n.t('aiops.predictutilsts.providerAliyun'), + tencent: i18n.t('aiops.predictutilsts.providerTencent'), + aws: 'AWS', + huawei: i18n.t('aiops.predictutilsts.providerHuawei'), + azure: 'Azure', +}; + +// ============ 日期工具 ============ + +/** 本月第一天 YYYY-MM-DD */ +export function monthStartDate(ref = new Date()): string { + return `${ref.getFullYear()}-${String(ref.getMonth() + 1).padStart(2, '0')}-01`; +} + +/** 本月剩余天数(不含今天) */ +export function daysRemainingInMonth(ref = new Date()): number { + const lastDay = new Date(ref.getFullYear(), ref.getMonth() + 1, 0).getDate(); + return Math.max(0, lastDay - ref.getDate()); +} + +// ============ 账单聚合 ============ + +/** 账单明细按日聚合 */ +export function aggregateDailyCosts( + records: JsonData[], + filter?: { startDate?: string; provider?: string }, +): DailyCostPoint[] { + const map = new Map(); + for (const r of records) { + const date = String(r.billing_date || ''); + if (!date) continue; + if (filter?.startDate && date < filter.startDate) continue; + if (filter?.provider && r.provider !== filter.provider) continue; + map.set(date, (map.get(date) || 0) + (Number(r.cost) || 0)); + } + return Array.from(map.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([date, cost]) => ({ date, cost })); +} + +/** 本月累计费用 */ +export function sumMonthCost(records: JsonData[], provider?: string): number { + const start = monthStartDate(); + return records.reduce((sum, r) => { + const date = String(r.billing_date || ''); + if (date < start) return sum; + if (provider && r.provider !== provider) return sum; + return sum + (Number(r.cost) || 0); + }, 0); +} + +// ============ 私有工具 ============ + +function costTrendFromSlope(slope: number): 'up' | 'down' | 'stable' { + if (slope > 1) return 'up'; + if (slope < -1) return 'down'; + return 'stable'; +} + +/** 成本优化建议 */ +export function costRecommendation(trend: 'up' | 'down' | 'stable', ratio: number): string { + if (trend === 'up' && ratio >= 0.85) { + return i18n.t('aiops.predictutilsts.sa7d4e3c1'); + } + if (trend === 'up') { + return i18n.t('aiops.predictutilsts.sb3f5a2d8'); + } + if (trend === 'down') { + return i18n.t('aiops.predictutilsts.sc1a9b4e7'); + } + return i18n.t('aiops.predictutilsts.sd8c6f2a3'); +} + +/** 预算风险建议 */ +function budgetRiskAction(metric: 'daily' | 'monthly', provider: string): string { + const scope = provider && provider !== i18n.t('aiops.predictutilsts.sg5a3d8e1') ? `${provider} ` : ''; + if (metric === 'daily') { + return i18n.t('aiops.predictutilsts.se4b7a5d2', { scope }); + } + return i18n.t('aiops.predictutilsts.sf2d8c6b4', { scope }); +} + +interface MonthEndForecast { + currentMonth: number; + predictedMonthEnd: number; + dailyAvg: number; + recentDailyAvg: number; + billingPattern: 'steady' | 'front_loaded' | 'sparse'; + trend: 'up' | 'down' | 'stable'; + confidence: number; + series: DailyCostPoint[]; +} + +/** 识别账单形态(预付费常表现为月初大额、后续近零) // Logan.Li */ +function detectBillingPattern(series: DailyCostPoint[]): 'steady' | 'front_loaded' | 'sparse' { + const active = series.filter((d) => d.cost > 0); + if (active.length < 2) return 'sparse'; + const total = series.reduce((s, d) => s + d.cost, 0); + const recent = series.slice(-Math.min(7, series.length)); + const recentAvg = recent.reduce((s, d) => s + d.cost, 0) / recent.length; + const overallAvg = total / series.length; + if (overallAvg > 0 && recentAvg < overallAvg * 0.25) return 'front_loaded'; + return 'steady'; +} + +function recentRunRate(series: DailyCostPoint[], days = 7): number { + const recent = series.slice(-Math.min(days, series.length)); + if (recent.length === 0) return 0; + return recent.reduce((s, d) => s + d.cost, 0) / recent.length; +} + +/** 本月 Top 服务消费 */ +function topServicesByCost(records: JsonData[], limit = 5): { service: string; cost: number }[] { + const start = monthStartDate(); + const map = new Map(); + records.forEach((r) => { + const date = String(r.billing_date || ''); + if (date < start) return; + const svc = String(r.service || i18n.t('aiops.predictutilsts.sh7c2b9d6')); + map.set(svc, (map.get(svc) || 0) + (Number(r.cost) || 0)); + }); + return [...map.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([service, cost]) => ({ service, cost: Math.round(cost * 100) / 100 })); +} + +/** 基于日费用序列预测月末总费用 */ +function forecastMonthEnd( + records: JsonData[], + statsMonthCost: number, + fallbackTrend: DailyCostPoint[], + provider?: string, +): MonthEndForecast { + const start = monthStartDate(); + const monthSeries = aggregateDailyCosts(records, { startDate: start, provider }); + const series = monthSeries.length >= 2 ? monthSeries : fallbackTrend; + const costs = series.map((d) => d.cost); + const { slope, r2 } = linearRegression(costs); + const currentMonth = + monthSeries.length > 0 ? monthSeries.reduce((s, d) => s + d.cost, 0) : statsMonthCost; + const daysLeft = daysRemainingInMonth(); + const dailyAvg = costs.length > 0 ? costs.reduce((a, b) => a + b, 0) / costs.length : 0; + const recentDailyAvg = recentRunRate(series); + const billingPattern = detectBillingPattern(series); + // 预付费/前高后平:用近 7 日运行费率外推,避免线性回归高估或矛盾 // Logan.Li + const extrapolation = + billingPattern === 'front_loaded' ? recentDailyAvg * daysLeft : slope * daysLeft; + const predictedMonthEnd = Math.max(0, Math.round((currentMonth + extrapolation) * 100) / 100); + let confidence = Math.round(55 + r2 * 40); + if (billingPattern === 'front_loaded') confidence = Math.max(45, confidence - 15); + if (billingPattern === 'sparse') confidence = Math.min(confidence, 50); + return { + currentMonth, + predictedMonthEnd, + dailyAvg: Math.round(dailyAvg * 100) / 100, + recentDailyAvg: Math.round(recentDailyAvg * 100) / 100, + billingPattern, + trend: costTrendFromSlope(billingPattern === 'front_loaded' ? recentDailyAvg - dailyAvg : slope), + confidence, + series: monthSeries.length > 0 ? monthSeries : fallbackTrend, + }; +} + +// ============ 综合成本预测 ============ + +/** + * 云成本预测:基于账单日粒度时序 + 预算预警规则,推算月末费用与触线风险。 + * // Logan.Li + */ +export function computeCostPredictions(input: ComputeCostPredictionsInput): ComputedCostPredictions { + const { billingStats, billingRecords, billingAlertRules } = input; + const empty: ComputedCostPredictions = { + summary: { + monthCost: 0, + predictedMonthEnd: 0, + todayCost: 0, + yesterdayCost: 0, + dailyAvg: 0, + daysRemaining: daysRemainingInMonth(), + trend: 'stable', + confidence: 0, + monthTrend: [], + providerDist: [], + }, + dimensionForecasts: [], + budgetRisks: [], + budgetRiskCount: 0, + recentDailyAvg: 0, + billingPattern: 'sparse', + topServices: [], + }; + + if (!billingStats) return empty; + + const monthTrend: DailyCostPoint[] = Array.isArray(billingStats.month_trend) + ? billingStats.month_trend.map((d: JsonData) => ({ + date: String(d.date || ''), + cost: Number(d.cost) || 0, + })) + : []; + const providerDist: { provider: string; cost: number; percent: number }[] = Array.isArray( + billingStats.provider_dist, + ) + ? billingStats.provider_dist.map((p: JsonData) => ({ + provider: String(p.provider || ''), + cost: Number(p.cost) || 0, + percent: Number(p.percent) || 0, + })) + : []; + + const overall = forecastMonthEnd( + billingRecords, + Number(billingStats.month_cost) || 0, + monthTrend, + ); + + const summary: CostPredictionSummary = { + monthCost: Number(billingStats.month_cost) || overall.currentMonth, + predictedMonthEnd: overall.predictedMonthEnd, + todayCost: Number(billingStats.today_cost) || 0, + yesterdayCost: Number(billingStats.yesterday_cost) || 0, + dailyAvg: overall.dailyAvg, + daysRemaining: daysRemainingInMonth(), + trend: overall.trend, + confidence: overall.confidence, + monthTrend: overall.series.length > 0 ? overall.series : monthTrend, + providerDist, + }; + + // 全部 + 各云厂商维度预测 + const dimensionForecasts: CostDimensionForecast[] = []; + const allRatio = + summary.predictedMonthEnd > 0 ? summary.monthCost / summary.predictedMonthEnd : 0; + dimensionForecasts.push({ + dimension: i18n.t('aiops.predictutilsts.sa1b2c3d4'), + currentMonth: summary.monthCost, + predictedMonthEnd: summary.predictedMonthEnd, + dailyAvg: summary.dailyAvg, + trend: summary.trend, + confidence: summary.confidence, + recommendation: costRecommendation(summary.trend, allRatio), + }); + + const providers = new Set(); + providerDist.forEach((p) => providers.add(p.provider)); + billingRecords.forEach((r) => { + if (r.provider) providers.add(String(r.provider)); + }); + + for (const provider of providers) { + if (!provider) continue; + const provForecast = forecastMonthEnd(billingRecords, 0, monthTrend, provider); + const provMonth = + provForecast.currentMonth > 0 ? provForecast.currentMonth : sumMonthCost(billingRecords, provider); + const ratio = provForecast.predictedMonthEnd > 0 ? provMonth / provForecast.predictedMonthEnd : 0; + dimensionForecasts.push({ + dimension: PROVIDER_LABELS[provider] || provider, + provider, + currentMonth: provMonth, + predictedMonthEnd: provForecast.predictedMonthEnd, + dailyAvg: provForecast.dailyAvg, + trend: provForecast.trend, + confidence: provForecast.confidence, + recommendation: costRecommendation(provForecast.trend, ratio), + }); + } + + dimensionForecasts.sort((a, b) => b.predictedMonthEnd - a.predictedMonthEnd); + + // 预算触线风险 + const budgetRisks: BudgetRiskForecast[] = []; + const activeAlerts: JsonData[] = Array.isArray(billingStats.alerts) ? billingStats.alerts : []; + + for (const rule of billingAlertRules) { + if (!rule.enabled) continue; + const threshold = Number(rule.threshold) || 0; + if (threshold <= 0) continue; + + const metric = (rule.metric === 'daily' ? 'daily' : 'monthly') as 'daily' | 'monthly'; + const ruleProvider = String(rule.provider || ''); + const providerLabel = ruleProvider ? PROVIDER_LABELS[ruleProvider] || ruleProvider : i18n.t('aiops.predictutilsts.sb2c3d4e5'); + + let current = 0; + let predicted = 0; + let slope = 0; + + if (metric === 'daily') { + current = Number(billingStats.today_cost) || 0; + if (ruleProvider) { + const today = new Date().toISOString().slice(0, 10); + current = billingRecords + .filter((r) => r.billing_date === today && r.provider === ruleProvider) + .reduce((s, r) => s + (Number(r.cost) || 0), 0); + } + const recent = aggregateDailyCosts(billingRecords, ruleProvider ? { provider: ruleProvider } : undefined); + const costs = recent.slice(-7).map((d) => d.cost); + slope = linearRegression(costs).slope; + predicted = Math.max(0, current + slope); + } else { + const provForecast = ruleProvider + ? forecastMonthEnd(billingRecords, 0, monthTrend, ruleProvider) + : overall; + current = ruleProvider ? sumMonthCost(billingRecords, ruleProvider) : summary.monthCost; + predicted = provForecast.predictedMonthEnd; + const costs = provForecast.series.map((d) => d.cost); + slope = linearRegression(costs).slope; + } + + const active = activeAlerts.find((a) => a.rule_id === rule.id || a.rule_name === rule.name); + if (active) current = Number(active.current) || current; + + const ratio = threshold > 0 ? predicted / threshold : 0; + let probability = Math.round(ratio * 50); + if (slope > 0.5) probability += 15; + if (current >= threshold) probability += 25; + probability = Math.max(5, Math.min(92, probability)); + + let timeframe = metric === 'daily' ? i18n.t('aiops.predictutilsts.sc3d4e5f6') : i18n.t('aiops.predictutilsts.sd4e5f6g7'); + if (slope > 0.01 && threshold > current) { + const days = (threshold - current) / slope; + if (days < 1) timeframe = metric === 'daily' ? i18n.t('aiops.predictutilsts.se5f6g7h8') : i18n.t('aiops.predictutilsts.sf6g7h8i9'); + else if (days <= 7) timeframe = i18n.t('aiops.predictutilsts.sg7h8i9j0', { days: Math.ceil(days) }); + else timeframe = i18n.t('aiops.predictutilsts.sh8i9j0k1', { days: Math.ceil(days) }); + } + + if (probability >= 30 || current >= threshold * 0.7) { + budgetRisks.push({ + ruleName: String(rule.name || i18n.t('aiops.predictutilsts.si9j0k1l2')), + provider: providerLabel, + metric, + threshold, + current: Math.round(current * 100) / 100, + predicted: Math.round(predicted * 100) / 100, + probability, + timeframe, + suggestedAction: budgetRiskAction(metric, providerLabel), + }); + } + } + + budgetRisks.sort((a, b) => b.probability - a.probability); + const budgetRiskCount = budgetRisks.filter((r) => r.probability >= 50).length; + + return { + summary, + dimensionForecasts, + budgetRisks, + budgetRiskCount, + recentDailyAvg: overall.recentDailyAvg, + billingPattern: overall.billingPattern, + topServices: topServicesByCost(billingRecords), + }; +} + +/** + * 构建成本预测「AI 深度解读」prompt。 + * // Logan.Li + */ +export function buildCostInsightPrompt(data: ComputedCostPredictions): string { + const { summary, dimensionForecasts, budgetRisks, recentDailyAvg, billingPattern, topServices } = data; + if (summary.monthCost <= 0 && dimensionForecasts.length === 0) return ''; + + const patternHint = + billingPattern === 'front_loaded' + ? i18n.t('aiops.predictutilsts.sj0k1l2m3') + : billingPattern === 'sparse' + ? i18n.t('aiops.predictutilsts.sk1l2m3n4') + : i18n.t('aiops.predictutilsts.sl2m3n4o5'); + + const dimLines = dimensionForecasts.slice(0, 6).map( + (d) => + `- ${d.dimension}:本月 ${d.currentMonth.toFixed(2)} 元 → 预测月末 ${d.predictedMonthEnd.toFixed(2)} 元(趋势 ${d.trend})`, + ); + const riskLines = + budgetRisks.length > 0 + ? budgetRisks.slice(0, 5).map( + (r) => + `- ${r.ruleName}(${r.provider}):当前 ${r.current} / 阈值 ${r.threshold},预测 ${r.predicted},触线概率 ${r.probability}%`, + ) + : [`- ${i18n.t('aiops.predictutilsts.sm3n4o5p6')}`]; + const serviceLines = + topServices.length > 0 + ? topServices.map((s) => `- ${s.service}:${s.cost.toFixed(2)} 元`) + : [`- ${i18n.t('aiops.predictutilsts.sn4o5p6q7')}`]; + + return [ + i18n.t('aiops.predictutilsts.so5p6q7r8'), + '', + i18n.t('aiops.predictutilsts.sp6q7r8s9'), + `- 本月已花:${summary.monthCost.toFixed(2)} 元`, + `- 预测月末:${summary.predictedMonthEnd.toFixed(2)} 元(置信 ${summary.confidence}%)`, + `- 全月日均:${summary.dailyAvg.toFixed(2)} 元;近7日日均:${recentDailyAvg.toFixed(2)} 元;剩余 ${summary.daysRemaining} 天`, + `- 账单形态:${patternHint}`, + '', + i18n.t('aiops.predictutilsts.sq7r8s9t0'), + ...serviceLines, + '', + i18n.t('aiops.predictutilsts.sr8s9t0u1'), + ...dimLines, + '', + i18n.t('aiops.predictutilsts.ss9t0u1v2'), + ...riskLines, + '', + i18n.t('aiops.predictutilsts.st0u1v2w3'), + i18n.t('aiops.predictutilsts.su1v2w3x4'), + i18n.t('aiops.predictutilsts.sv2w3x4y5'), + i18n.t('aiops.predictutilsts.sw3x4y5z6'), + i18n.t('aiops.predictutilsts.sx4y5z6a7'), + ].join('\n'); +} diff --git a/frontend/src/components/aiops/predict-types.ts b/frontend/src/components/aiops/predict-types.ts new file mode 100644 index 0000000..96cf34f --- /dev/null +++ b/frontend/src/components/aiops/predict-types.ts @@ -0,0 +1,140 @@ +import type { JsonData } from '@/types/common'; +import i18n from '@/i18n'; + +/** + * AI 预测分析模块 — 共享类型定义 + 常量 + * + * 从 predictUtils.ts (716L) 提取。 + * // Logan.Li + */ + +// ============ 容量预测类型 ============ + +export interface PredictionMetric { + name: string; + current: number; + predicted: number; + unit: string; + trend: 'up' | 'down' | 'stable'; + confidence: number; + timeRange: string; +} + +export interface CapacityForecast { + resource: string; + metricKey: string; + currentUsage: number; + predictedUsage: number; + threshold: number; + daysUntilThreshold: number; + recommendation: string; +} + +// ============ 云成本预测类型 ============ + +/** 日费用数据点 */ +export interface DailyCostPoint { + date: string; + cost: number; +} + +/** 单维度(全部 / 云厂商)成本预测 */ +export interface CostDimensionForecast { + dimension: string; + provider?: string; + currentMonth: number; + predictedMonthEnd: number; + dailyAvg: number; + trend: 'up' | 'down' | 'stable'; + confidence: number; + recommendation: string; +} + +/** 预算触线风险(对接 BillingAlertRule) */ +export interface BudgetRiskForecast { + ruleName: string; + provider: string; + metric: 'daily' | 'monthly'; + threshold: number; + current: number; + predicted: number; + probability: number; + timeframe: string; + suggestedAction: string; +} + +/** 成本预测汇总 */ +export interface CostPredictionSummary { + monthCost: number; + predictedMonthEnd: number; + todayCost: number; + yesterdayCost: number; + dailyAvg: number; + daysRemaining: number; + trend: 'up' | 'down' | 'stable'; + confidence: number; + monthTrend: DailyCostPoint[]; + providerDist: { provider: string; cost: number; percent: number }[]; +} + +export interface ComputedCostPredictions { + summary: CostPredictionSummary; + dimensionForecasts: CostDimensionForecast[]; + budgetRisks: BudgetRiskForecast[]; + budgetRiskCount: number; + /** 近 7 日日均(用于识别预付费后按量趋零) // Logan.Li */ + recentDailyAvg: number; + /** 账单形态:steady 平稳 / front_loaded 月初集中 / sparse 样本不足 */ + billingPattern: 'steady' | 'front_loaded' | 'sparse'; + /** 本月 Top 服务消费 */ + topServices: { service: string; cost: number }[]; +} + +// ============ 容量预测入参/出参 ============ + +/** computePredictions 的输入参数 */ +export interface ComputePredictionsInput { + rawMetrics: JsonData[]; + serverStats?: JsonData; + selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d' +} + +/** computePredictions 的聚合输出 */ +export interface ComputedPredictions { + predictions: PredictionMetric[]; + capacityForecasts: CapacityForecast[]; + accuracy: number; + capacityWarningCount: number; +} + +// ============ 成本预测入参/出参 ============ + +/** computeCostPredictions 输入 */ +export interface ComputeCostPredictionsInput { + billingStats: JsonData | null; + billingRecords: JsonData[]; + billingAlertRules: JsonData[]; +} + +// ============ 共享常量 ============ + +/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */ +export const METRIC_DEFS = [ + { key: 'cpu', label: i18n.t('aiops.predictutilsts.s7054bc34'), exprKey: 'cpu_usage', threshold: 85 }, + { key: 'memory', label: i18n.t('aiops.predictutilsts.s82c0d1b0'), exprKey: 'memory_usage', threshold: 85 }, + { key: 'disk', label: i18n.t('aiops.predictutilsts.s4e2d8f91'), exprKey: 'disk_usage', threshold: 90 }, +] as const; + +export const TIME_RANGE_LABELS: Record = { + '1h': i18n.t('aiops.predictutilsts.s75719bb8'), + '24h': i18n.t('aiops.predictutilsts.sfc78646e'), + '7d': i18n.t('aiops.predictutilsts.s5b5a820c'), + '30d': i18n.t('aiops.predictutilsts.s933ab0a6'), +}; + +export const TIME_RANGE_HOURS: Record = { + '1h': 1, + '24h': 24, + '7d': 168, + '30d': 720, +}; diff --git a/frontend/src/components/aiops/predictUtils.ts b/frontend/src/components/aiops/predictUtils.ts index fa15f6b..5d5bc45 100644 --- a/frontend/src/components/aiops/predictUtils.ts +++ b/frontend/src/components/aiops/predictUtils.ts @@ -1,716 +1,49 @@ -import type { JsonData } from "@/types/common"; -import i18n from '@/i18n'; /** - * AI 预测分析模块的纯计算工具 — 类型定义、常量、线性回归、预测推算 + * AI 预测分析模块 — Barrel re-export(向后兼容) * - * 从 AIPredictiveAnalytics.tsx(670行) 提取,避免主组件超 700 行硬上限。 - * 所有函数均为纯函数(输入 → 输出,无副作用),便于单元测试和复用。 + * 原 716L 文件已按域拆分为 3 个文件: + * - predict-types.ts — 类型定义 + 共享常量 (METRIC_DEFS / TIME_RANGE_*) + * - capacity-predict.ts — 容量预测(线性回归 + computePredictions + 容量规划 + AI prompt) + * - cost-predict.ts — 云成本预测(月末预测 + 预算触线风险 + AI prompt) + * + * 现有 3 个消费者的 import 路径 (`from './predictUtils'`) 无需修改。 * // Logan.Li */ -// ============ 类型定义 ============ - -export interface PredictionMetric { - name: string; - current: number; - predicted: number; - unit: string; - trend: 'up' | 'down' | 'stable'; - confidence: number; - timeRange: string; -} - -export interface CapacityForecast { - resource: string; - metricKey: string; - currentUsage: number; - predictedUsage: number; - threshold: number; - daysUntilThreshold: number; - recommendation: string; -} - -/** 日费用数据点 */ -export interface DailyCostPoint { - date: string; - cost: number; -} - -/** 单维度(全部 / 云厂商)成本预测 */ -export interface CostDimensionForecast { - dimension: string; - provider?: string; - currentMonth: number; - predictedMonthEnd: number; - dailyAvg: number; - trend: 'up' | 'down' | 'stable'; - confidence: number; - recommendation: string; -} - -/** 预算触线风险(对接 BillingAlertRule) */ -export interface BudgetRiskForecast { - ruleName: string; - provider: string; - metric: 'daily' | 'monthly'; - threshold: number; - current: number; - predicted: number; - probability: number; - timeframe: string; - suggestedAction: string; -} - -/** 成本预测汇总 */ -export interface CostPredictionSummary { - monthCost: number; - predictedMonthEnd: number; - todayCost: number; - yesterdayCost: number; - dailyAvg: number; - daysRemaining: number; - trend: 'up' | 'down' | 'stable'; - confidence: number; - monthTrend: DailyCostPoint[]; - providerDist: { provider: string; cost: number; percent: number }[]; -} - -export interface ComputedCostPredictions { - summary: CostPredictionSummary; - dimensionForecasts: CostDimensionForecast[]; - budgetRisks: BudgetRiskForecast[]; - budgetRiskCount: number; - /** 近 7 日日均(用于识别预付费后按量趋零) // Logan.Li */ - recentDailyAvg: number; - /** 账单形态:steady 平稳 / front_loaded 月初集中 / sparse 样本不足 */ - billingPattern: 'steady' | 'front_loaded' | 'sparse'; - /** 本月 Top 服务消费 */ - topServices: { service: string; cost: number }[]; -} +// ============ 类型 ============ +export type { + PredictionMetric, + CapacityForecast, + DailyCostPoint, + CostDimensionForecast, + BudgetRiskForecast, + CostPredictionSummary, + ComputedCostPredictions, + ComputePredictionsInput, + ComputedPredictions, + ComputeCostPredictionsInput, +} from './predict-types'; // ============ 常量 ============ +export { METRIC_DEFS, TIME_RANGE_LABELS, TIME_RANGE_HOURS } from './predict-types'; -/** 指标定义:后端 metric name → 展示标签 / 告警表达式前缀 / 阈值 */ -export const METRIC_DEFS = [ - { key: 'cpu', label: i18n.t('aiops.predictutilsts.s7054bc34'), exprKey: 'cpu_usage', threshold: 85 }, - { key: 'memory', label: i18n.t('aiops.predictutilsts.s82c0d1b0'), exprKey: 'memory_usage', threshold: 85 }, - { key: 'disk', label: i18n.t('aiops.predictutilsts.s4e2d8f91'), exprKey: 'disk_usage', threshold: 90 }, -] as const; +// ============ 容量预测函数 ============ +export { + linearRegression, + extractSeries, + capacityRecommendation, + computePredictions, + trendColorClass, + buildCapacityInsightPrompt, +} from './capacity-predict'; -export const TIME_RANGE_LABELS: Record = { - '1h': i18n.t('aiops.predictutilsts.s75719bb8'), - '24h': i18n.t('aiops.predictutilsts.sfc78646e'), - '7d': i18n.t('aiops.predictutilsts.s5b5a820c'), - '30d': i18n.t('aiops.predictutilsts.s933ab0a6'), -}; - -export const TIME_RANGE_HOURS: Record = { - '1h': 1, - '24h': 24, - '7d': 168, - '30d': 720, -}; - -// ============ 纯函数工具 ============ - -/** 简单最小二乘线性回归,返回斜率和 R² */ -export function linearRegression(values: number[]): { slope: number; r2: number } { - const n = values.length; - if (n < 2) return { slope: 0, r2: 0 }; - const xs = values.map((_, i) => i); - const meanX = xs.reduce((a, b) => a + b, 0) / n; - const meanY = values.reduce((a, b) => a + b, 0) / n; - let num = 0, - den = 0, - ssTot = 0, - ssRes = 0; - for (let i = 0; i < n; i++) { - num += (xs[i] - meanX) * (values[i] - meanY); - den += (xs[i] - meanX) ** 2; - ssTot += (values[i] - meanY) ** 2; - } - const slope = den === 0 ? 0 : num / den; - const intercept = meanY - slope * meanX; - for (let i = 0; i < n; i++) { - const pred = intercept + slope * xs[i]; - ssRes += (values[i] - pred) ** 2; - } - const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot); - return { slope, r2 }; -} - -/** 从时序指标中提取按时间排序的数值序列 */ -export function extractSeries( metrics: JsonData[], name: string): number[] { - return metrics - .filter((m) => m.name === name) - .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) - .map((m) => Number(m.value) || 0); -} - -/** 容量规划建议 */ -export function capacityRecommendation(metricKey: string, days: number): string { - const level = days <= 7 ? 'urgent' : days <= 14 ? 'warning' : 'normal'; - const recs: Record> = { - urgent: { - cpu: i18n.t('aiops.predictutilsts.s4d0bd643'), - memory: i18n.t('aiops.predictutilsts.sb4593f3a'), - disk: i18n.t('aiops.predictutilsts.s6c25383f'), - }, - warning: { - cpu: i18n.t('aiops.predictutilsts.s8ea826f4'), - memory: i18n.t('aiops.predictutilsts.sb08bd5fc'), - disk: i18n.t('aiops.predictutilsts.s4cdda5dc'), - }, - normal: { - cpu: i18n.t('aiops.predictutilsts.sfa295c44'), - memory: i18n.t('aiops.predictutilsts.s7156a4c0'), - disk: i18n.t('aiops.predictutilsts.s0073f3a3'), - }, - }; - return recs[level]?.[metricKey] || i18n.t('aiops.predictutilsts.sfeb368e6'); -} - -// ============ 综合预测推算 ============ - -/** computePredictions 的输入参数 */ -export interface ComputePredictionsInput { - rawMetrics: JsonData[]; - serverStats?: JsonData; - selectedTimeRange: string; // '1h' | '24h' | '7d' | '30d' -} - -/** computePredictions 的聚合输出 */ -export interface ComputedPredictions { - predictions: PredictionMetric[]; - capacityForecasts: CapacityForecast[]; - accuracy: number; - capacityWarningCount: number; -} - -/** - * 综合预测推算:基于时序指标计算指标预测与容量规划。 - * 主组件 AIPredictiveAnalytics.tsx 直接调用此函数获取展示数据。 - * // Logan.Li - */ -export function computePredictions(input: ComputePredictionsInput): ComputedPredictions { - const { rawMetrics, serverStats, selectedTimeRange } = input; - const horizonHours = TIME_RANGE_HOURS[selectedTimeRange] || 24; - const timeRangeLabel = TIME_RANGE_LABELS[selectedTimeRange] || i18n.t('aiops.predictutilsts.sfc78646e'); - - // ---- 指标预测 ---- - const predictions: PredictionMetric[] = METRIC_DEFS.map((def) => { - const series = extractSeries(rawMetrics, def.key); - const { slope, r2 } = linearRegression(series); - const avgFallback = - def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; - const current = series.length > 0 ? series[series.length - 1] : avgFallback || 0; - let predicted = current + slope * horizonHours; - predicted = Math.max(0, Math.min(100, predicted)); - const trend: 'up' | 'down' | 'stable' = slope > 0.05 ? 'up' : slope < -0.05 ? 'down' : 'stable'; - const confidence = Math.round(60 + r2 * 35); - return { - name: def.label, - current: Math.round(current), - predicted: Math.round(predicted), - unit: '%', - trend, - confidence, - timeRange: timeRangeLabel, - }; - }); - - // ---- 容量规划 ---- - const capacityForecasts: CapacityForecast[] = METRIC_DEFS.map((def) => { - const series = extractSeries(rawMetrics, def.key); - const { slope } = linearRegression(series); - const avgFallback = - def.key === 'cpu' ? serverStats.avg_cpu : def.key === 'memory' ? serverStats.avg_memory : serverStats.avg_disk; - const currentUsage = Math.round(series.length > 0 ? series[series.length - 1] : avgFallback || 0); - const slopePerDay = slope * 24; - const predictedUsage = Math.round(Math.min(100, currentUsage + slopePerDay * 7)); - const threshold = def.threshold; - let daysUntilThreshold = 999; - if (slopePerDay > 0.01) { - daysUntilThreshold = Math.round((threshold - currentUsage) / slopePerDay); - daysUntilThreshold = daysUntilThreshold < 1 ? 1 : daysUntilThreshold; - } - const resourceNames: Record = { - cpu: '服务器CPU算力', - memory: '服务器内存', - disk: '存储磁盘空间', - }; - return { - resource: resourceNames[def.key] || def.label, - metricKey: def.key, - currentUsage, - predictedUsage, - threshold, - daysUntilThreshold, - recommendation: capacityRecommendation(def.key, daysUntilThreshold), - }; - }).filter((f) => f.currentUsage > 0); - - // ---- 概览统计 ---- - const avgR2 = METRIC_DEFS.length > 0 - ? METRIC_DEFS.reduce((sum, def) => sum + linearRegression(extractSeries(rawMetrics, def.key)).r2, 0) / - METRIC_DEFS.length - : 0; - const accuracy = Math.round(78 + avgR2 * 17); - const capacityWarningCount = capacityForecasts.filter((f) => f.daysUntilThreshold <= 14).length; - - return { - predictions, - capacityForecasts, - accuracy, - capacityWarningCount, - }; -} - -/** 趋势图标颜色工具,供主组件 render 用 */ -export function trendColorClass(trend: string): string { - switch (trend) { - case 'up': - return 'dg-text-red'; - case 'down': - return 'dg-text-green'; - default: - return 'dg-text-secondary'; - } -} - -/** - * 构建容量预测「AI 深度解读」的 prompt —— 把硬数据序列化为结构化上下文喂给 LLM。 // Logan.Li - * 纯函数,供 PredictiveCapacityTab 调 aiApi.chat 使用。无数据返回空串(调用方据此禁用按钮)。 - */ -export function buildCapacityInsightPrompt(forecasts: CapacityForecast[]): string { - if (forecasts.length === 0) return ''; - const lines = forecasts.map((f) => { - const risk = f.daysUntilThreshold >= 999 ? i18n.t('aiops.predictutilsts.s179fd5a9') : i18n.t('aiops.predictutilsts.daysUntilThreshold', { days: f.daysUntilThreshold }); - return `- ${f.resource}:当前 ${f.currentUsage}% → 7天后预测 ${f.predictedUsage}%(阈值 ${f.threshold}%),${risk}`; - }); - return [ - i18n.t('aiops.predictutilsts.s3b46e5a0'), - '', - i18n.t('aiops.predictutilsts.s586bac5d'), - ...lines, - '', - i18n.t('aiops.predictutilsts.sdf6ebee3'), - i18n.t('aiops.predictutilsts.s534b6893'), - i18n.t('aiops.predictutilsts.saf173e7a'), - i18n.t('aiops.predictutilsts.s4ed1d035'), - ].join('\n'); -} - -// ============ 云成本预测 ============ - -const PROVIDER_LABELS: Record = { - aliyun: i18n.t('aiops.predictutilsts.providerAliyun'), - tencent: i18n.t('aiops.predictutilsts.providerTencent'), - aws: 'AWS', - huawei: i18n.t('aiops.predictutilsts.providerHuawei'), - azure: 'Azure', -}; - -/** 本月第一天 YYYY-MM-DD */ -export function monthStartDate(ref = new Date()): string { - return `${ref.getFullYear()}-${String(ref.getMonth() + 1).padStart(2, '0')}-01`; -} - -/** 本月剩余天数(不含今天) */ -export function daysRemainingInMonth(ref = new Date()): number { - const lastDay = new Date(ref.getFullYear(), ref.getMonth() + 1, 0).getDate(); - return Math.max(0, lastDay - ref.getDate()); -} - -/** 账单明细按日聚合 */ -export function aggregateDailyCosts( - records: JsonData[], - filter?: { startDate?: string; provider?: string }, -): DailyCostPoint[] { - const map = new Map(); - for (const r of records) { - const date = String(r.billing_date || ''); - if (!date) continue; - if (filter?.startDate && date < filter.startDate) continue; - if (filter?.provider && r.provider !== filter.provider) continue; - map.set(date, (map.get(date) || 0) + (Number(r.cost) || 0)); - } - return Array.from(map.entries()) - .sort((a, b) => a[0].localeCompare(b[0])) - .map(([date, cost]) => ({ date, cost })); -} - -/** 本月累计费用 */ -export function sumMonthCost(records: JsonData[], provider?: string): number { - const start = monthStartDate(); - return records.reduce((sum, r) => { - const date = String(r.billing_date || ''); - if (date < start) return sum; - if (provider && r.provider !== provider) return sum; - return sum + (Number(r.cost) || 0); - }, 0); -} - -function costTrendFromSlope(slope: number): 'up' | 'down' | 'stable' { - if (slope > 1) return 'up'; - if (slope < -1) return 'down'; - return 'stable'; -} - -/** 成本优化建议 */ -export function costRecommendation(trend: 'up' | 'down' | 'stable', ratio: number): string { - if (trend === 'up' && ratio >= 0.85) { - return i18n.t('aiops.predictutilsts.sa7d4e3c1'); - } - if (trend === 'up') { - return i18n.t('aiops.predictutilsts.sb3f5a2d8'); - } - if (trend === 'down') { - return i18n.t('aiops.predictutilsts.sc1a9b4e7'); - } - return i18n.t('aiops.predictutilsts.sd8c6f2a3'); -} - -/** 预算风险建议 */ -function budgetRiskAction(metric: 'daily' | 'monthly', provider: string): string { - const scope = provider && provider !== i18n.t('aiops.predictutilsts.sg5a3d8e1') ? `${provider} ` : ''; - if (metric === 'daily') { - return i18n.t('aiops.predictutilsts.se4b7a5d2', { scope }); - } - return i18n.t('aiops.predictutilsts.sf2d8c6b4', { scope }); -} - -interface MonthEndForecast { - currentMonth: number; - predictedMonthEnd: number; - dailyAvg: number; - recentDailyAvg: number; - billingPattern: 'steady' | 'front_loaded' | 'sparse'; - trend: 'up' | 'down' | 'stable'; - confidence: number; - series: DailyCostPoint[]; -} - -/** 识别账单形态(预付费常表现为月初大额、后续近零) // Logan.Li */ -function detectBillingPattern(series: DailyCostPoint[]): 'steady' | 'front_loaded' | 'sparse' { - const active = series.filter((d) => d.cost > 0); - if (active.length < 2) return 'sparse'; - const total = series.reduce((s, d) => s + d.cost, 0); - const recent = series.slice(-Math.min(7, series.length)); - const recentAvg = recent.reduce((s, d) => s + d.cost, 0) / recent.length; - const overallAvg = total / series.length; - if (overallAvg > 0 && recentAvg < overallAvg * 0.25) return 'front_loaded'; - return 'steady'; -} - -function recentRunRate(series: DailyCostPoint[], days = 7): number { - const recent = series.slice(-Math.min(days, series.length)); - if (recent.length === 0) return 0; - return recent.reduce((s, d) => s + d.cost, 0) / recent.length; -} - -/** 本月 Top 服务消费 */ -function topServicesByCost(records: JsonData[], limit = 5): { service: string; cost: number }[] { - const start = monthStartDate(); - const map = new Map(); - records.forEach((r) => { - const date = String(r.billing_date || ''); - if (date < start) return; - const svc = String(r.service || i18n.t('aiops.predictutilsts.sh7c2b9d6')); - map.set(svc, (map.get(svc) || 0) + (Number(r.cost) || 0)); - }); - return [...map.entries()] - .sort((a, b) => b[1] - a[1]) - .slice(0, limit) - .map(([service, cost]) => ({ service, cost: Math.round(cost * 100) / 100 })); -} - -/** 基于日费用序列预测月末总费用 */ -function forecastMonthEnd( - records: JsonData[], - statsMonthCost: number, - fallbackTrend: DailyCostPoint[], - provider?: string, -): MonthEndForecast { - const start = monthStartDate(); - const monthSeries = aggregateDailyCosts(records, { startDate: start, provider }); - const series = monthSeries.length >= 2 ? monthSeries : fallbackTrend; - const costs = series.map((d) => d.cost); - const { slope, r2 } = linearRegression(costs); - const currentMonth = - monthSeries.length > 0 ? monthSeries.reduce((s, d) => s + d.cost, 0) : statsMonthCost; - const daysLeft = daysRemainingInMonth(); - const dailyAvg = costs.length > 0 ? costs.reduce((a, b) => a + b, 0) / costs.length : 0; - const recentDailyAvg = recentRunRate(series); - const billingPattern = detectBillingPattern(series); - // 预付费/前高后平:用近 7 日运行费率外推,避免线性回归高估或矛盾 // Logan.Li - const extrapolation = - billingPattern === 'front_loaded' ? recentDailyAvg * daysLeft : slope * daysLeft; - const predictedMonthEnd = Math.max(0, Math.round((currentMonth + extrapolation) * 100) / 100); - let confidence = Math.round(55 + r2 * 40); - if (billingPattern === 'front_loaded') confidence = Math.max(45, confidence - 15); - if (billingPattern === 'sparse') confidence = Math.min(confidence, 50); - return { - currentMonth, - predictedMonthEnd, - dailyAvg: Math.round(dailyAvg * 100) / 100, - recentDailyAvg: Math.round(recentDailyAvg * 100) / 100, - billingPattern, - trend: costTrendFromSlope(billingPattern === 'front_loaded' ? recentDailyAvg - dailyAvg : slope), - confidence, - series: monthSeries.length > 0 ? monthSeries : fallbackTrend, - }; -} - -/** computeCostPredictions 输入 */ -export interface ComputeCostPredictionsInput { - billingStats: JsonData | null; - billingRecords: JsonData[]; - billingAlertRules: JsonData[]; -} - -/** - * 云成本预测:基于账单日粒度时序 + 预算预警规则,推算月末费用与触线风险。 - * // Logan.Li - */ -export function computeCostPredictions(input: ComputeCostPredictionsInput): ComputedCostPredictions { - const { billingStats, billingRecords, billingAlertRules } = input; - const empty: ComputedCostPredictions = { - summary: { - monthCost: 0, - predictedMonthEnd: 0, - todayCost: 0, - yesterdayCost: 0, - dailyAvg: 0, - daysRemaining: daysRemainingInMonth(), - trend: 'stable', - confidence: 0, - monthTrend: [], - providerDist: [], - }, - dimensionForecasts: [], - budgetRisks: [], - budgetRiskCount: 0, - recentDailyAvg: 0, - billingPattern: 'sparse', - topServices: [], - }; - - if (!billingStats) return empty; - - const monthTrend: DailyCostPoint[] = Array.isArray(billingStats.month_trend) - ? billingStats.month_trend.map((d: JsonData) => ({ - date: String(d.date || ''), - cost: Number(d.cost) || 0, - })) - : []; - const providerDist: { provider: string; cost: number; percent: number }[] = Array.isArray( - billingStats.provider_dist, - ) - ? billingStats.provider_dist.map((p: JsonData) => ({ - provider: String(p.provider || ''), - cost: Number(p.cost) || 0, - percent: Number(p.percent) || 0, - })) - : []; - - const overall = forecastMonthEnd( - billingRecords, - Number(billingStats.month_cost) || 0, - monthTrend, - ); - - const summary: CostPredictionSummary = { - monthCost: Number(billingStats.month_cost) || overall.currentMonth, - predictedMonthEnd: overall.predictedMonthEnd, - todayCost: Number(billingStats.today_cost) || 0, - yesterdayCost: Number(billingStats.yesterday_cost) || 0, - dailyAvg: overall.dailyAvg, - daysRemaining: daysRemainingInMonth(), - trend: overall.trend, - confidence: overall.confidence, - monthTrend: overall.series.length > 0 ? overall.series : monthTrend, - providerDist, - }; - - // 全部 + 各云厂商维度预测 - const dimensionForecasts: CostDimensionForecast[] = []; - const allRatio = - summary.predictedMonthEnd > 0 ? summary.monthCost / summary.predictedMonthEnd : 0; - dimensionForecasts.push({ - dimension: i18n.t('aiops.predictutilsts.sa1b2c3d4'), - currentMonth: summary.monthCost, - predictedMonthEnd: summary.predictedMonthEnd, - dailyAvg: summary.dailyAvg, - trend: summary.trend, - confidence: summary.confidence, - recommendation: costRecommendation(summary.trend, allRatio), - }); - - const providers = new Set(); - providerDist.forEach((p) => providers.add(p.provider)); - billingRecords.forEach((r) => { - if (r.provider) providers.add(String(r.provider)); - }); - - for (const provider of providers) { - if (!provider) continue; - const provForecast = forecastMonthEnd(billingRecords, 0, monthTrend, provider); - const provMonth = - provForecast.currentMonth > 0 ? provForecast.currentMonth : sumMonthCost(billingRecords, provider); - const ratio = provForecast.predictedMonthEnd > 0 ? provMonth / provForecast.predictedMonthEnd : 0; - dimensionForecasts.push({ - dimension: PROVIDER_LABELS[provider] || provider, - provider, - currentMonth: provMonth, - predictedMonthEnd: provForecast.predictedMonthEnd, - dailyAvg: provForecast.dailyAvg, - trend: provForecast.trend, - confidence: provForecast.confidence, - recommendation: costRecommendation(provForecast.trend, ratio), - }); - } - - dimensionForecasts.sort((a, b) => b.predictedMonthEnd - a.predictedMonthEnd); - - // 预算触线风险 - const budgetRisks: BudgetRiskForecast[] = []; - const activeAlerts: JsonData[] = Array.isArray(billingStats.alerts) ? billingStats.alerts : []; - - for (const rule of billingAlertRules) { - if (!rule.enabled) continue; - const threshold = Number(rule.threshold) || 0; - if (threshold <= 0) continue; - - const metric = (rule.metric === 'daily' ? 'daily' : 'monthly') as 'daily' | 'monthly'; - const ruleProvider = String(rule.provider || ''); - const providerLabel = ruleProvider ? PROVIDER_LABELS[ruleProvider] || ruleProvider : i18n.t('aiops.predictutilsts.sb2c3d4e5'); - - let current = 0; - let predicted = 0; - let slope = 0; - - if (metric === 'daily') { - current = Number(billingStats.today_cost) || 0; - if (ruleProvider) { - const today = new Date().toISOString().slice(0, 10); - current = billingRecords - .filter((r) => r.billing_date === today && r.provider === ruleProvider) - .reduce((s, r) => s + (Number(r.cost) || 0), 0); - } - const recent = aggregateDailyCosts(billingRecords, ruleProvider ? { provider: ruleProvider } : undefined); - const costs = recent.slice(-7).map((d) => d.cost); - slope = linearRegression(costs).slope; - predicted = Math.max(0, current + slope); - } else { - const provForecast = ruleProvider - ? forecastMonthEnd(billingRecords, 0, monthTrend, ruleProvider) - : overall; - current = ruleProvider ? sumMonthCost(billingRecords, ruleProvider) : summary.monthCost; - predicted = provForecast.predictedMonthEnd; - const costs = provForecast.series.map((d) => d.cost); - slope = linearRegression(costs).slope; - } - - const active = activeAlerts.find((a) => a.rule_id === rule.id || a.rule_name === rule.name); - if (active) current = Number(active.current) || current; - - const ratio = threshold > 0 ? predicted / threshold : 0; - let probability = Math.round(ratio * 50); - if (slope > 0.5) probability += 15; - if (current >= threshold) probability += 25; - probability = Math.max(5, Math.min(92, probability)); - - let timeframe = metric === 'daily' ? i18n.t('aiops.predictutilsts.sc3d4e5f6') : i18n.t('aiops.predictutilsts.sd4e5f6g7'); - if (slope > 0.01 && threshold > current) { - const days = (threshold - current) / slope; - if (days < 1) timeframe = metric === 'daily' ? i18n.t('aiops.predictutilsts.se5f6g7h8') : i18n.t('aiops.predictutilsts.sf6g7h8i9'); - else if (days <= 7) timeframe = i18n.t('aiops.predictutilsts.sg7h8i9j0', { days: Math.ceil(days) }); - else timeframe = i18n.t('aiops.predictutilsts.sh8i9j0k1', { days: Math.ceil(days) }); - } - - if (probability >= 30 || current >= threshold * 0.7) { - budgetRisks.push({ - ruleName: String(rule.name || i18n.t('aiops.predictutilsts.si9j0k1l2')), - provider: providerLabel, - metric, - threshold, - current: Math.round(current * 100) / 100, - predicted: Math.round(predicted * 100) / 100, - probability, - timeframe, - suggestedAction: budgetRiskAction(metric, providerLabel), - }); - } - } - - budgetRisks.sort((a, b) => b.probability - a.probability); - const budgetRiskCount = budgetRisks.filter((r) => r.probability >= 50).length; - - return { - summary, - dimensionForecasts, - budgetRisks, - budgetRiskCount, - recentDailyAvg: overall.recentDailyAvg, - billingPattern: overall.billingPattern, - topServices: topServicesByCost(billingRecords), - }; -} - -/** - * 构建成本预测「AI 深度解读」prompt。 - * // Logan.Li - */ -export function buildCostInsightPrompt(data: ComputedCostPredictions): string { - const { summary, dimensionForecasts, budgetRisks, recentDailyAvg, billingPattern, topServices } = data; - if (summary.monthCost <= 0 && dimensionForecasts.length === 0) return ''; - - const patternHint = - billingPattern === 'front_loaded' - ? i18n.t('aiops.predictutilsts.sj0k1l2m3') - : billingPattern === 'sparse' - ? i18n.t('aiops.predictutilsts.sk1l2m3n4') - : i18n.t('aiops.predictutilsts.sl2m3n4o5'); - - const dimLines = dimensionForecasts.slice(0, 6).map( - (d) => - `- ${d.dimension}:本月 ${d.currentMonth.toFixed(2)} 元 → 预测月末 ${d.predictedMonthEnd.toFixed(2)} 元(趋势 ${d.trend})`, - ); - const riskLines = - budgetRisks.length > 0 - ? budgetRisks.slice(0, 5).map( - (r) => - `- ${r.ruleName}(${r.provider}):当前 ${r.current} / 阈值 ${r.threshold},预测 ${r.predicted},触线概率 ${r.probability}%`, - ) - : [`- ${i18n.t('aiops.predictutilsts.sm3n4o5p6')}`]; - const serviceLines = - topServices.length > 0 - ? topServices.map((s) => `- ${s.service}:${s.cost.toFixed(2)} 元`) - : [`- ${i18n.t('aiops.predictutilsts.sn4o5p6q7')}`]; - - return [ - i18n.t('aiops.predictutilsts.so5p6q7r8'), - '', - i18n.t('aiops.predictutilsts.sp6q7r8s9'), - `- 本月已花:${summary.monthCost.toFixed(2)} 元`, - `- 预测月末:${summary.predictedMonthEnd.toFixed(2)} 元(置信 ${summary.confidence}%)`, - `- 全月日均:${summary.dailyAvg.toFixed(2)} 元;近7日日均:${recentDailyAvg.toFixed(2)} 元;剩余 ${summary.daysRemaining} 天`, - `- 账单形态:${patternHint}`, - '', - i18n.t('aiops.predictutilsts.sq7r8s9t0'), - ...serviceLines, - '', - i18n.t('aiops.predictutilsts.sr8s9t0u1'), - ...dimLines, - '', - i18n.t('aiops.predictutilsts.ss9t0u1v2'), - ...riskLines, - '', - i18n.t('aiops.predictutilsts.st0u1v2w3'), - i18n.t('aiops.predictutilsts.su1v2w3x4'), - i18n.t('aiops.predictutilsts.sv2w3x4y5'), - i18n.t('aiops.predictutilsts.sw3x4y5z6'), - i18n.t('aiops.predictutilsts.sx4y5z6a7'), - ].join('\n'); -} +// ============ 云成本预测函数 ============ +export { + monthStartDate, + daysRemainingInMonth, + aggregateDailyCosts, + sumMonthCost, + costRecommendation, + computeCostPredictions, + buildCostInsightPrompt, +} from './cost-predict';