初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled

This commit is contained in:
iven
2026-03-01 16:24:24 +08:00
commit 92e5def702
492 changed files with 211343 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
// OpenFang Analytics Page — Full usage analytics with per-model and per-agent breakdowns
// Includes Cost Dashboard with donut chart, bar chart, projections, and provider breakdown.
'use strict';
function analyticsPage() {
return {
tab: 'summary',
summary: {},
byModel: [],
byAgent: [],
loading: true,
loadError: '',
// Cost tab state
dailyCosts: [],
todayCost: 0,
firstEventDate: null,
// Chart colors for providers (stable palette)
_chartColors: [
'#FF5C00', '#3B82F6', '#10B981', '#F59E0B', '#8B5CF6',
'#EC4899', '#06B6D4', '#EF4444', '#84CC16', '#F97316',
'#6366F1', '#14B8A6', '#E11D48', '#A855F7', '#22D3EE'
],
async loadUsage() {
this.loading = true;
this.loadError = '';
try {
await Promise.all([
this.loadSummary(),
this.loadByModel(),
this.loadByAgent(),
this.loadDailyCosts()
]);
} catch(e) {
this.loadError = e.message || 'Could not load usage data.';
}
this.loading = false;
},
async loadData() { return this.loadUsage(); },
async loadSummary() {
try {
this.summary = await OpenFangAPI.get('/api/usage/summary');
} catch(e) {
this.summary = { total_input_tokens: 0, total_output_tokens: 0, total_cost_usd: 0, call_count: 0, total_tool_calls: 0 };
throw e;
}
},
async loadByModel() {
try {
var data = await OpenFangAPI.get('/api/usage/by-model');
this.byModel = data.models || [];
} catch(e) { this.byModel = []; }
},
async loadByAgent() {
try {
var data = await OpenFangAPI.get('/api/usage');
this.byAgent = data.agents || [];
} catch(e) { this.byAgent = []; }
},
async loadDailyCosts() {
try {
var data = await OpenFangAPI.get('/api/usage/daily');
this.dailyCosts = data.days || [];
this.todayCost = data.today_cost_usd || 0;
this.firstEventDate = data.first_event_date || null;
} catch(e) {
this.dailyCosts = [];
this.todayCost = 0;
this.firstEventDate = null;
}
},
formatTokens(n) {
if (!n) return '0';
if (n >= 1000000) return (n / 1000000).toFixed(2) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return String(n);
},
formatCost(c) {
if (!c) return '$0.00';
if (c < 0.01) return '$' + c.toFixed(4);
return '$' + c.toFixed(2);
},
maxTokens() {
var max = 0;
this.byModel.forEach(function(m) {
var t = (m.total_input_tokens || 0) + (m.total_output_tokens || 0);
if (t > max) max = t;
});
return max || 1;
},
barWidth(m) {
var t = (m.total_input_tokens || 0) + (m.total_output_tokens || 0);
return Math.max(2, Math.round((t / this.maxTokens()) * 100)) + '%';
},
// ── Cost tab helpers ──
avgCostPerMessage() {
var count = this.summary.call_count || 0;
if (count === 0) return 0;
return (this.summary.total_cost_usd || 0) / count;
},
projectedMonthlyCost() {
if (!this.firstEventDate || !this.summary.total_cost_usd) return 0;
var first = new Date(this.firstEventDate);
var now = new Date();
var diffMs = now.getTime() - first.getTime();
var diffDays = diffMs / (1000 * 60 * 60 * 24);
if (diffDays < 1) diffDays = 1;
return (this.summary.total_cost_usd / diffDays) * 30;
},
// ── Provider aggregation from byModel data ──
costByProvider() {
var providerMap = {};
var self = this;
this.byModel.forEach(function(m) {
var provider = self._extractProvider(m.model);
if (!providerMap[provider]) {
providerMap[provider] = { provider: provider, cost: 0, tokens: 0, calls: 0 };
}
providerMap[provider].cost += (m.total_cost_usd || 0);
providerMap[provider].tokens += (m.total_input_tokens || 0) + (m.total_output_tokens || 0);
providerMap[provider].calls += (m.call_count || 0);
});
var result = [];
for (var key in providerMap) {
if (providerMap.hasOwnProperty(key)) {
result.push(providerMap[key]);
}
}
result.sort(function(a, b) { return b.cost - a.cost; });
return result;
},
_extractProvider(modelName) {
if (!modelName) return 'Unknown';
var lower = modelName.toLowerCase();
if (lower.indexOf('claude') !== -1 || lower.indexOf('haiku') !== -1 || lower.indexOf('sonnet') !== -1 || lower.indexOf('opus') !== -1) return 'Anthropic';
if (lower.indexOf('gemini') !== -1 || lower.indexOf('gemma') !== -1) return 'Google';
if (lower.indexOf('gpt') !== -1 || lower.indexOf('o1') !== -1 || lower.indexOf('o3') !== -1 || lower.indexOf('o4') !== -1) return 'OpenAI';
if (lower.indexOf('llama') !== -1 || lower.indexOf('mixtral') !== -1 || lower.indexOf('groq') !== -1) return 'Groq';
if (lower.indexOf('deepseek') !== -1) return 'DeepSeek';
if (lower.indexOf('mistral') !== -1) return 'Mistral';
if (lower.indexOf('command') !== -1 || lower.indexOf('cohere') !== -1) return 'Cohere';
if (lower.indexOf('grok') !== -1) return 'xAI';
if (lower.indexOf('jamba') !== -1) return 'AI21';
if (lower.indexOf('qwen') !== -1) return 'Together';
return 'Other';
},
// ── Donut chart (stroke-dasharray on circles) ──
donutSegments() {
var providers = this.costByProvider();
var total = 0;
var colors = this._chartColors;
providers.forEach(function(p) { total += p.cost; });
if (total === 0) return [];
var segments = [];
var offset = 0;
var circumference = 2 * Math.PI * 60; // r=60
for (var i = 0; i < providers.length; i++) {
var pct = providers[i].cost / total;
var dashLen = pct * circumference;
segments.push({
provider: providers[i].provider,
cost: providers[i].cost,
percent: Math.round(pct * 100),
color: colors[i % colors.length],
dasharray: dashLen + ' ' + (circumference - dashLen),
dashoffset: -offset,
circumference: circumference
});
offset += dashLen;
}
return segments;
},
// ── Bar chart (last 7 days) ──
barChartData() {
var days = this.dailyCosts;
if (!days || days.length === 0) return [];
var maxCost = 0;
days.forEach(function(d) { if (d.cost_usd > maxCost) maxCost = d.cost_usd; });
if (maxCost === 0) maxCost = 1;
var dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
var result = [];
for (var i = 0; i < days.length; i++) {
var d = new Date(days[i].date + 'T12:00:00');
var dayName = dayNames[d.getDay()] || '?';
var heightPct = Math.max(2, Math.round((days[i].cost_usd / maxCost) * 120));
result.push({
date: days[i].date,
dayName: dayName,
cost: days[i].cost_usd,
tokens: days[i].tokens,
calls: days[i].calls,
barHeight: heightPct
});
}
return result;
},
// ── Cost by model table (sorted by cost descending) ──
costByModelSorted() {
var models = this.byModel.slice();
models.sort(function(a, b) { return (b.total_cost_usd || 0) - (a.total_cost_usd || 0); });
return models;
},
maxModelCost() {
var max = 0;
this.byModel.forEach(function(m) {
if ((m.total_cost_usd || 0) > max) max = m.total_cost_usd;
});
return max || 1;
},
costBarWidth(m) {
return Math.max(2, Math.round(((m.total_cost_usd || 0) / this.maxModelCost()) * 100)) + '%';
},
modelTier(modelName) {
if (!modelName) return 'unknown';
var lower = modelName.toLowerCase();
if (lower.indexOf('opus') !== -1 || lower.indexOf('o1') !== -1 || lower.indexOf('o3') !== -1 || lower.indexOf('deepseek-r1') !== -1) return 'frontier';
if (lower.indexOf('sonnet') !== -1 || lower.indexOf('gpt-4') !== -1 || lower.indexOf('gemini-2.5') !== -1 || lower.indexOf('gemini-1.5-pro') !== -1) return 'smart';
if (lower.indexOf('haiku') !== -1 || lower.indexOf('gpt-3.5') !== -1 || lower.indexOf('flash') !== -1 || lower.indexOf('mixtral') !== -1) return 'balanced';
if (lower.indexOf('llama') !== -1 || lower.indexOf('groq') !== -1 || lower.indexOf('gemma') !== -1) return 'fast';
return 'balanced';
}
};
}