fix: 恢复腾讯云账单同步并完善成本预测

- 接入腾讯云单日账单、全局限频和指数退避

- 修正月末费用预测并增加账期提示和费用归因

- 补充腾讯云限频及账单归因回归测试
This commit is contained in:
Logan.Li committed 2026-07-29 05:44:40 +08:00
1 parent 88c41f0b2b
commit dd3770a66c
15 files changed
+610 -17

No files matched your search

@@ -54,6 +54,7 @@ export function AIPredictiveAnalytics() {
const [billingStats, setBillingStats] = useState<JsonData | null>(null);
const [billingRecords, setBillingRecords] = useState<JsonData[]>([]);
const [billingAlertRules, setBillingAlertRules] = useState<JsonData[]>([]);
const [costAnalysis, setCostAnalysis] = useState<JsonData | null>(null);
useEffect(() => {
void loadData();
@@ -66,12 +67,13 @@ export function AIPredictiveAnalytics() {
start.setDate(start.getDate() - 89);
const startDate = start.toISOString().slice(0, 10);
const [metricsData, statsResp, billStats, billList, billRules] = await Promise.all([
const [metricsData, statsResp, billStats, billList, billRules, analysis] = await Promise.all([
dashboardStatsApi.getMetrics(72),
serverApi.getServerStats(),
billingApi.getStats().catch(() => null),
billingApi.list({ start_date: startDate, page_size: 500 }).catch(() => ({ list: [] })),
billingApi.listAlertRules().catch(() => ({ list: [] })),
billingApi.getCostAnalysis().catch(() => null),
]);
setRawMetrics(Array.isArray(metricsData) ? metricsData : []);
@@ -79,6 +81,7 @@ export function AIPredictiveAnalytics() {
setBillingStats(billStats);
setBillingRecords((billList as JsonData)?.list || []);
setBillingAlertRules((billRules as JsonData)?.list || []);
setCostAnalysis(analysis);
} catch {
// 保持空数据,组件显示空状态
} finally {
@@ -104,8 +107,9 @@ export function AIPredictiveAnalytics() {
billingStats,
billingRecords,
billingAlertRules,
costAnalysis,
}),
[billingStats, billingRecords, billingAlertRules],
[billingStats, billingRecords, billingAlertRules, costAnalysis],
);
const getTrendIcon = (trend: string) => {
@@ -259,4 +263,4 @@ export function AIPredictiveAnalytics() {
</Tabs>
</PageShell>
);
}
}
@@ -38,7 +38,7 @@ export function PredictiveCostTab({
const { t } = useTranslation();
const [insight, setInsight] = useState('');
const [analyzing, setAnalyzing] = useState(false);
const { summary, dimensionForecasts, budgetRisks, billingPattern, recentDailyAvg } = costData;
const { summary, dimensionForecasts, budgetRisks, billingPattern, recentDailyAvg, costAnalysis } = costData;
const hasData = summary.monthCost > 0 || summary.monthTrend.length > 0;
const getTrendIcon = (trend: string) => {
@@ -129,6 +129,17 @@ export function PredictiveCostTab({
</div>
)}
{costAnalysis?.incomplete_period && (
<div className="flex items-start gap-2 text-sm dg-text-secondary">
<AlertTriangle className="h-4 w-4 dg-text-yellow shrink-0 mt-0.5" />
<span>
{t('aiops.costPredict.incompletePeriod', {
date: costAnalysis.data_through || costAnalysis.analysis_end,
})}
</span>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="p-3 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/50">
<p className="text-xs dg-text-muted mb-1">{t('aiops.predictivecost.s7g8h9i0j')}</p>
@@ -172,6 +183,58 @@ export function PredictiveCostTab({
</CardContent>
</Card>
{!loading && costAnalysis && costAnalysis.attributions.length > 0 && (
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-[var(--dg-text-primary)] text-base">
{t('aiops.costPredict.costAttribution')}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
<div>
<p className="text-xs dg-text-muted">{t('aiops.costPredict.baselinePeriod')}</p>
<p className="font-semibold text-[var(--dg-text-primary)]">
{formatCurrency(costAnalysis.baseline_cost)}
</p>
<p className="text-xs dg-text-muted mt-1">{costAnalysis.baseline_period}</p>
</div>
<div>
<p className="text-xs dg-text-muted">{t('aiops.costPredict.comparisonPeriod')}</p>
<p className="font-semibold text-[var(--dg-text-primary)]">
{formatCurrency(costAnalysis.comparison_cost)}
</p>
<p className="text-xs dg-text-muted mt-1">{costAnalysis.comparison_period}</p>
</div>
<div>
<p className="text-xs dg-text-muted">{t('aiops.costPredict.costChange')}</p>
<p className={`font-semibold ${costAnalysis.total_delta > 0 ? 'dg-text-red' : 'dg-text-green'}`}>
{costAnalysis.total_delta > 0 ? '+' : ''}{formatCurrency(costAnalysis.total_delta)}
</p>
</div>
</div>
<div className="space-y-2">
{costAnalysis.attributions.slice(0, 6).map((item) => (
<div key={`${item.dimension}-${item.name}`} className="flex items-center justify-between gap-3 text-sm py-1">
<div className="min-w-0">
<span className="dg-text-muted mr-2">
{t(`aiops.costPredict.dimension.${item.dimension}`, { defaultValue: item.dimension })}
</span>
<span className="text-[var(--dg-text-primary)] break-words">{item.name}</span>
</div>
<div className="shrink-0 text-right">
<span className={item.delta > 0 ? 'dg-text-red' : 'dg-text-green'}>
{item.delta > 0 ? '+' : ''}{formatCurrency(item.delta)}
</span>
<span className="dg-text-muted ml-2">{item.contribution.toFixed(1)}%</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{!loading && dimensionForecasts.length > 0 && (
<Card className="dg-card">
<CardHeader className="pb-3">
@@ -345,4 +408,4 @@ export function PredictiveCostTab({
)}
</div>
);
}
}
+33 -5
View File
@@ -8,6 +8,7 @@ import type {
CostPredictionSummary,
ComputedCostPredictions,
ComputeCostPredictionsInput,
CostAnalysis,
} from './predict-types';
/**
@@ -166,9 +167,18 @@ function forecastMonthEnd(
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;
// 稳态账单逐日累加回归预测值,不能把斜率本身误当成每日费用。 // Logan.Li
const intercept = dailyAvg - slope * ((costs.length - 1) / 2);
const maxDaily = Math.max(dailyAvg, recentDailyAvg, ...costs, 1) * 3;
let extrapolation = 0;
if (billingPattern === 'front_loaded') {
extrapolation = recentDailyAvg * daysLeft;
} else {
for (let day = 0; day < daysLeft; day += 1) {
const projectedDaily = intercept + slope * (costs.length + day);
extrapolation += Math.max(0, Math.min(maxDaily, projectedDaily));
}
}
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);
@@ -192,7 +202,7 @@ function forecastMonthEnd(
* // Logan.Li
*/
export function computeCostPredictions(input: ComputeCostPredictionsInput): ComputedCostPredictions {
const { billingStats, billingRecords, billingAlertRules } = input;
const { billingStats, billingRecords, billingAlertRules, costAnalysis } = input;
const empty: ComputedCostPredictions = {
summary: {
monthCost: 0,
@@ -212,6 +222,7 @@ export function computeCostPredictions(input: ComputeCostPredictionsInput): Comp
recentDailyAvg: 0,
billingPattern: 'sparse',
topServices: [],
costAnalysis: null,
};
if (!billingStats) return empty;
@@ -373,6 +384,7 @@ export function computeCostPredictions(input: ComputeCostPredictionsInput): Comp
recentDailyAvg: overall.recentDailyAvg,
billingPattern: overall.billingPattern,
topServices: topServicesByCost(billingRecords),
costAnalysis: costAnalysis ? (costAnalysis as unknown as CostAnalysis) : null,
};
}
@@ -381,7 +393,7 @@ export function computeCostPredictions(input: ComputeCostPredictionsInput): Comp
* // Logan.Li
*/
export function buildCostInsightPrompt(data: ComputedCostPredictions): string {
const { summary, dimensionForecasts, budgetRisks, recentDailyAvg, billingPattern, topServices } = data;
const { summary, dimensionForecasts, budgetRisks, recentDailyAvg, billingPattern, topServices, costAnalysis } = data;
if (summary.monthCost <= 0 && dimensionForecasts.length === 0) return '';
const patternHint =
@@ -418,6 +430,16 @@ export function buildCostInsightPrompt(data: ComputedCostPredictions): string {
topServices.length > 0
? topServices.map((s) => i18n.t('aiops.costPredict.serviceLine', { service: s.service, cost: s.cost.toFixed(2) }))
: [`- ${i18n.t('aiops.costPredict.noServiceDetail')}`];
const attributionLines = costAnalysis?.attributions?.length
? costAnalysis.attributions.slice(0, 8).map((item) =>
i18n.t('aiops.costPredict.attributionLine', {
dimension: item.dimension,
name: item.name,
delta: item.delta.toFixed(2),
contribution: item.contribution.toFixed(2),
}),
)
: [`- ${i18n.t('aiops.costPredict.noAttribution')}`];
return [
i18n.t('aiops.costPredict.promptIntro'),
@@ -427,10 +449,16 @@ export function buildCostInsightPrompt(data: ComputedCostPredictions): string {
i18n.t('aiops.costPredict.predictedEnd', { amount: summary.predictedMonthEnd.toFixed(2), confidence: summary.confidence }),
i18n.t('aiops.costPredict.dailySummary', { dailyAvg: summary.dailyAvg.toFixed(2), recentAvg: recentDailyAvg.toFixed(2), daysRemaining: summary.daysRemaining }),
i18n.t('aiops.costPredict.billingPatternLabel', { hint: patternHint }),
costAnalysis?.incomplete_period
? i18n.t('aiops.costPredict.incompletePrompt', { date: costAnalysis.data_through })
: '',
'',
i18n.t('aiops.costPredict.sectionTopServices'),
...serviceLines,
'',
i18n.t('aiops.costPredict.sectionAttribution'),
...attributionLines,
'',
i18n.t('aiops.costPredict.sectionDimensionForecast'),
...dimLines,
'',
@@ -77,6 +77,31 @@ export interface CostPredictionSummary {
providerDist: { provider: string; cost: number; percent: number }[];
}
/** 后端确定性费用归因结果。 // Logan.Li */
export interface CostAttribution {
dimension: string;
name: string;
baseline_cost: number;
current_cost: number;
delta: number;
contribution: number;
}
/** 同期成本分析,供页面展示并作为 AI 事实输入。 // Logan.Li */
export interface CostAnalysis {
analysis_start: string;
analysis_end: string;
data_through: string;
currency: string;
incomplete_period: boolean;
baseline_period: string;
comparison_period: string;
baseline_cost: number;
comparison_cost: number;
total_delta: number;
attributions: CostAttribution[];
}
export interface ComputedCostPredictions {
summary: CostPredictionSummary;
dimensionForecasts: CostDimensionForecast[];
@@ -88,6 +113,8 @@ export interface ComputedCostPredictions {
billingPattern: 'steady' | 'front_loaded' | 'sparse';
/** 本月 Top 服务消费 */
topServices: { service: string; cost: number }[];
/** 后端按完整数据集计算的同期归因。 // Logan.Li */
costAnalysis: CostAnalysis | null;
}
// ============ 容量预测入参/出参 ============
@@ -114,6 +141,7 @@ export interface ComputeCostPredictionsInput {
billingStats: JsonData | null;
billingRecords: JsonData[];
billingAlertRules: JsonData[];
costAnalysis?: JsonData | null;
}
// ============ 共享常量 ============