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:
attacker committed 2026-07-10 12:06:58 +08:00
1 parent 4283fb93c1
commit a2edf6aa9f
5 files changed
+809 -707

No files matched your search

+1
View File
@@ -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,
};
+40 -707
View File
@@ -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';