refactor(quality): Q36 predictUtils.ts 巨型工具文件拆分 — 716L→49L barrel re-export (-93%)
提取 3 个域文件: - predict-types.ts 140L: 类型定义 + 共享常量 (METRIC_DEFS/TIME_RANGE_*) - capacity-predict.ts 194L: 容量预测 (linearRegression + extractSeries + computePredictions + capacityRecommendation + trendColorClass + buildCapacityInsightPrompt) - cost-predict.ts 434L: 云成本预测 (monthStartDate + daysRemainingInMonth + aggregateDailyCosts + sumMonthCost + costRecommendation + computeCostPredictions + buildCostInsightPrompt + 私有函数 forecastMonthEnd/detectBillingPattern/recentRunRate/topServicesByCost/budgetRiskAction) predictUtils.ts 改为 barrel re-export 保持向后兼容, 3 个消费者 (AIPredictiveAnalytics/PredictiveCostTab/PredictiveCapacityTab) import 路径无需修改。 零逻辑变更, tsc -b=0, npm build✓ (17.26s), ESLint errors=0/warnings=66/any=0, go build=0, 17/17 集成测试通过
This commit is contained in:
1 parent
4283fb93c1
commit
a2edf6aa9f
5 files changed
+809
-707
No files matched your search
@@ -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** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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<string, Record<string, string>> = {
|
||||
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<string, string> = {
|
||||
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');
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
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<string, number>();
|
||||
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<string, number>();
|
||||
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<string>();
|
||||
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');
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
'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<string, number> = {
|
||||
'1h': 1,
|
||||
'24h': 24,
|
||||
'7d': 168,
|
||||
'30d': 720,
|
||||
};
|
||||
@@ -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<string, string> = {
|
||||
'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<string, number> = {
|
||||
'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<string, Record<string, string>> = {
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, number>();
|
||||
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<string, number>();
|
||||
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<string>();
|
||||
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';
|
||||
Reference in new issue
Block a user