commons-proxy 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Filters Module
|
|
3
|
+
* 职责:管理图表筛选器的状态和持久化
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* 1. 时间范围筛选(1h/6h/24h/7d/all)
|
|
7
|
+
* 2. 显示模式切换(按家族/按模型)
|
|
8
|
+
* 3. 模型/家族多选筛选
|
|
9
|
+
* 4. 筛选器状态持久化到 localStorage
|
|
10
|
+
*
|
|
11
|
+
* 调用时机:
|
|
12
|
+
* - 组件初始化时加载用户偏好
|
|
13
|
+
* - 筛选器变化时保存并触发图表更新
|
|
14
|
+
*
|
|
15
|
+
* 持久化键:
|
|
16
|
+
* - localStorage['dashboard_chart_prefs']
|
|
17
|
+
*
|
|
18
|
+
* @module DashboardFilters
|
|
19
|
+
*/
|
|
20
|
+
window.DashboardFilters = window.DashboardFilters || {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get initial filter state
|
|
24
|
+
* @returns {object} Initial state for filter properties
|
|
25
|
+
*/
|
|
26
|
+
window.DashboardFilters.getInitialState = function() {
|
|
27
|
+
return {
|
|
28
|
+
timeRange: '24h', // '1h', '6h', '24h', '7d', 'all'
|
|
29
|
+
displayMode: 'model',
|
|
30
|
+
selectedFamilies: [],
|
|
31
|
+
selectedModels: {},
|
|
32
|
+
showModelFilter: false,
|
|
33
|
+
showTimeRangeDropdown: false,
|
|
34
|
+
showDisplayModeDropdown: false
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load filter preferences from localStorage
|
|
40
|
+
* @param {object} component - Dashboard component instance
|
|
41
|
+
*/
|
|
42
|
+
window.DashboardFilters.loadPreferences = function(component) {
|
|
43
|
+
try {
|
|
44
|
+
const saved = localStorage.getItem('dashboard_chart_prefs');
|
|
45
|
+
if (saved) {
|
|
46
|
+
const prefs = JSON.parse(saved);
|
|
47
|
+
component.timeRange = prefs.timeRange || '24h';
|
|
48
|
+
component.displayMode = prefs.displayMode || 'model';
|
|
49
|
+
component.selectedFamilies = prefs.selectedFamilies || [];
|
|
50
|
+
component.selectedModels = prefs.selectedModels || {};
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (window.UILogger) window.UILogger.debug('Failed to load dashboard preferences:', e.message);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save filter preferences to localStorage
|
|
59
|
+
* @param {object} component - Dashboard component instance
|
|
60
|
+
*/
|
|
61
|
+
window.DashboardFilters.savePreferences = function(component) {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem('dashboard_chart_prefs', JSON.stringify({
|
|
64
|
+
timeRange: component.timeRange,
|
|
65
|
+
displayMode: component.displayMode,
|
|
66
|
+
selectedFamilies: component.selectedFamilies,
|
|
67
|
+
selectedModels: component.selectedModels
|
|
68
|
+
}));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (window.UILogger) window.UILogger.debug('Failed to save dashboard preferences:', e.message);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set display mode (family or model)
|
|
76
|
+
* @param {object} component - Dashboard component instance
|
|
77
|
+
* @param {string} mode - 'family' or 'model'
|
|
78
|
+
*/
|
|
79
|
+
window.DashboardFilters.setDisplayMode = function(component, mode) {
|
|
80
|
+
component.displayMode = mode;
|
|
81
|
+
component.showDisplayModeDropdown = false;
|
|
82
|
+
window.DashboardFilters.savePreferences(component);
|
|
83
|
+
// updateTrendChart uses debounce internally, call directly
|
|
84
|
+
component.updateTrendChart();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set time range filter
|
|
89
|
+
* @param {object} component - Dashboard component instance
|
|
90
|
+
* @param {string} range - '1h', '6h', '24h', '7d', 'all'
|
|
91
|
+
*/
|
|
92
|
+
window.DashboardFilters.setTimeRange = function(component, range) {
|
|
93
|
+
component.timeRange = range;
|
|
94
|
+
component.showTimeRangeDropdown = false;
|
|
95
|
+
window.DashboardFilters.savePreferences(component);
|
|
96
|
+
component.updateTrendChart();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get time range cutoff timestamp
|
|
101
|
+
* @param {string} range - Time range code
|
|
102
|
+
* @returns {number|null} Cutoff timestamp or null for 'all'
|
|
103
|
+
*/
|
|
104
|
+
window.DashboardFilters.getTimeRangeCutoff = function(range) {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
switch (range) {
|
|
107
|
+
case '1h': return now - 1 * 60 * 60 * 1000;
|
|
108
|
+
case '6h': return now - 6 * 60 * 60 * 1000;
|
|
109
|
+
case '24h': return now - 24 * 60 * 60 * 1000;
|
|
110
|
+
case '7d': return now - 7 * 24 * 60 * 60 * 1000;
|
|
111
|
+
default: return null; // 'all'
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get filtered history data based on time range
|
|
117
|
+
* @param {object} component - Dashboard component instance
|
|
118
|
+
* @returns {object} Filtered history data
|
|
119
|
+
*/
|
|
120
|
+
window.DashboardFilters.getFilteredHistoryData = function(component) {
|
|
121
|
+
const history = component.historyData;
|
|
122
|
+
if (!history || Object.keys(history).length === 0) return {};
|
|
123
|
+
|
|
124
|
+
const cutoff = window.DashboardFilters.getTimeRangeCutoff(component.timeRange);
|
|
125
|
+
if (!cutoff) return history; // 'all' - return everything
|
|
126
|
+
|
|
127
|
+
const filtered = {};
|
|
128
|
+
Object.entries(history).forEach(([iso, data]) => {
|
|
129
|
+
const timestamp = new Date(iso).getTime();
|
|
130
|
+
if (timestamp >= cutoff) {
|
|
131
|
+
filtered[iso] = data;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return filtered;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get time range label for display
|
|
139
|
+
* @param {object} component - Dashboard component instance
|
|
140
|
+
* @returns {string} Translated label
|
|
141
|
+
*/
|
|
142
|
+
window.DashboardFilters.getTimeRangeLabel = function(component) {
|
|
143
|
+
const store = Alpine.store('global');
|
|
144
|
+
switch (component.timeRange) {
|
|
145
|
+
case '1h': return store.t('last1Hour');
|
|
146
|
+
case '6h': return store.t('last6Hours');
|
|
147
|
+
case '24h': return store.t('last24Hours');
|
|
148
|
+
case '7d': return store.t('last7Days');
|
|
149
|
+
default: return store.t('allTime');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Toggle family selection
|
|
155
|
+
* @param {object} component - Dashboard component instance
|
|
156
|
+
* @param {string} family - Family name (e.g., 'claude', 'gemini')
|
|
157
|
+
*/
|
|
158
|
+
window.DashboardFilters.toggleFamily = function(component, family) {
|
|
159
|
+
const index = component.selectedFamilies.indexOf(family);
|
|
160
|
+
if (index > -1) {
|
|
161
|
+
component.selectedFamilies.splice(index, 1);
|
|
162
|
+
} else {
|
|
163
|
+
component.selectedFamilies.push(family);
|
|
164
|
+
}
|
|
165
|
+
window.DashboardFilters.savePreferences(component);
|
|
166
|
+
// updateTrendChart uses debounce internally, call directly
|
|
167
|
+
component.updateTrendChart();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Toggle model selection within a family
|
|
172
|
+
* @param {object} component - Dashboard component instance
|
|
173
|
+
* @param {string} family - Family name
|
|
174
|
+
* @param {string} model - Model name
|
|
175
|
+
*/
|
|
176
|
+
window.DashboardFilters.toggleModel = function(component, family, model) {
|
|
177
|
+
if (!component.selectedModels[family]) {
|
|
178
|
+
component.selectedModels[family] = [];
|
|
179
|
+
}
|
|
180
|
+
const index = component.selectedModels[family].indexOf(model);
|
|
181
|
+
if (index > -1) {
|
|
182
|
+
component.selectedModels[family].splice(index, 1);
|
|
183
|
+
} else {
|
|
184
|
+
component.selectedModels[family].push(model);
|
|
185
|
+
}
|
|
186
|
+
window.DashboardFilters.savePreferences(component);
|
|
187
|
+
// updateTrendChart uses debounce internally, call directly
|
|
188
|
+
component.updateTrendChart();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if family is selected
|
|
193
|
+
* @param {object} component - Dashboard component instance
|
|
194
|
+
* @param {string} family - Family name
|
|
195
|
+
* @returns {boolean}
|
|
196
|
+
*/
|
|
197
|
+
window.DashboardFilters.isFamilySelected = function(component, family) {
|
|
198
|
+
return component.selectedFamilies.includes(family);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if model is selected
|
|
203
|
+
* @param {object} component - Dashboard component instance
|
|
204
|
+
* @param {string} family - Family name
|
|
205
|
+
* @param {string} model - Model name
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
window.DashboardFilters.isModelSelected = function(component, family, model) {
|
|
209
|
+
return component.selectedModels[family]?.includes(model) || false;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Select all families and models
|
|
214
|
+
* @param {object} component - Dashboard component instance
|
|
215
|
+
*/
|
|
216
|
+
window.DashboardFilters.selectAll = function(component) {
|
|
217
|
+
component.selectedFamilies = [...component.families];
|
|
218
|
+
component.families.forEach(family => {
|
|
219
|
+
component.selectedModels[family] = [...(component.modelTree[family] || [])];
|
|
220
|
+
});
|
|
221
|
+
window.DashboardFilters.savePreferences(component);
|
|
222
|
+
// updateTrendChart uses debounce internally, call directly
|
|
223
|
+
component.updateTrendChart();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Deselect all families and models
|
|
228
|
+
* @param {object} component - Dashboard component instance
|
|
229
|
+
*/
|
|
230
|
+
window.DashboardFilters.deselectAll = function(component) {
|
|
231
|
+
component.selectedFamilies = [];
|
|
232
|
+
component.selectedModels = {};
|
|
233
|
+
window.DashboardFilters.savePreferences(component);
|
|
234
|
+
// updateTrendChart uses debounce internally, call directly
|
|
235
|
+
component.updateTrendChart();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get color for a family
|
|
240
|
+
* @param {string} family - Family name
|
|
241
|
+
* @returns {string} Color value
|
|
242
|
+
*/
|
|
243
|
+
window.DashboardFilters.getFamilyColor = function(family) {
|
|
244
|
+
const FAMILY_COLORS = window.DashboardConstants?.FAMILY_COLORS || {};
|
|
245
|
+
return FAMILY_COLORS[family] || FAMILY_COLORS.other;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get color for a model (with index for variation within family)
|
|
250
|
+
* @param {string} family - Family name
|
|
251
|
+
* @param {number} modelIndex - Index of model within family
|
|
252
|
+
* @returns {string} Color value
|
|
253
|
+
*/
|
|
254
|
+
window.DashboardFilters.getModelColor = function(family, modelIndex) {
|
|
255
|
+
const MODEL_COLORS = window.DashboardConstants?.MODEL_COLORS || [];
|
|
256
|
+
const baseIndex = family === 'claude' ? 0 : (family === 'gemini' ? 4 : (family === 'gpt' ? 8 : (family === 'o1' ? 12 : 8)));
|
|
257
|
+
return MODEL_COLORS[(baseIndex + modelIndex) % MODEL_COLORS.length];
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get count of selected items for display
|
|
262
|
+
* @param {object} component - Dashboard component instance
|
|
263
|
+
* @returns {string} Selected count string (e.g., "3/5")
|
|
264
|
+
*/
|
|
265
|
+
window.DashboardFilters.getSelectedCount = function(component) {
|
|
266
|
+
if (component.displayMode === 'family') {
|
|
267
|
+
return `${component.selectedFamilies.length}/${component.families.length}`;
|
|
268
|
+
}
|
|
269
|
+
let selected = 0, total = 0;
|
|
270
|
+
component.families.forEach(family => {
|
|
271
|
+
const models = component.modelTree[family] || [];
|
|
272
|
+
total += models.length;
|
|
273
|
+
selected += (component.selectedModels[family] || []).length;
|
|
274
|
+
});
|
|
275
|
+
return `${selected}/${total}`;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Auto-select new families/models that haven't been configured
|
|
280
|
+
* @param {object} component - Dashboard component instance
|
|
281
|
+
*/
|
|
282
|
+
window.DashboardFilters.autoSelectNew = function(component) {
|
|
283
|
+
// If no preferences saved, select all
|
|
284
|
+
if (component.selectedFamilies.length === 0 && Object.keys(component.selectedModels).length === 0) {
|
|
285
|
+
component.selectedFamilies = [...component.families];
|
|
286
|
+
component.families.forEach(family => {
|
|
287
|
+
component.selectedModels[family] = [...(component.modelTree[family] || [])];
|
|
288
|
+
});
|
|
289
|
+
window.DashboardFilters.savePreferences(component);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Add new families/models that appeared
|
|
294
|
+
component.families.forEach(family => {
|
|
295
|
+
if (!component.selectedFamilies.includes(family)) {
|
|
296
|
+
component.selectedFamilies.push(family);
|
|
297
|
+
}
|
|
298
|
+
if (!component.selectedModels[family]) {
|
|
299
|
+
component.selectedModels[family] = [];
|
|
300
|
+
}
|
|
301
|
+
(component.modelTree[family] || []).forEach(model => {
|
|
302
|
+
if (!component.selectedModels[family].includes(model)) {
|
|
303
|
+
component.selectedModels[family].push(model);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Auto-select top N models by usage (past 24 hours)
|
|
311
|
+
* @param {object} component - Dashboard component instance
|
|
312
|
+
* @param {number} n - Number of models to select (default: 5)
|
|
313
|
+
*/
|
|
314
|
+
window.DashboardFilters.autoSelectTopN = function(component, n = 5) {
|
|
315
|
+
// Calculate usage for each model over past 24 hours
|
|
316
|
+
const usage = {};
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const dayAgo = now - 24 * 60 * 60 * 1000;
|
|
319
|
+
|
|
320
|
+
Object.entries(component.historyData).forEach(([iso, hourData]) => {
|
|
321
|
+
const timestamp = new Date(iso).getTime();
|
|
322
|
+
if (timestamp < dayAgo) return;
|
|
323
|
+
|
|
324
|
+
Object.entries(hourData).forEach(([family, familyData]) => {
|
|
325
|
+
if (typeof familyData === 'object' && family !== '_total') {
|
|
326
|
+
Object.entries(familyData).forEach(([model, count]) => {
|
|
327
|
+
if (model !== '_subtotal') {
|
|
328
|
+
const key = `${family}:${model}`;
|
|
329
|
+
usage[key] = (usage[key] || 0) + count;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Sort by usage and take top N
|
|
337
|
+
const sorted = Object.entries(usage)
|
|
338
|
+
.sort((a, b) => b[1] - a[1])
|
|
339
|
+
.slice(0, n);
|
|
340
|
+
|
|
341
|
+
// Clear current selection
|
|
342
|
+
component.selectedFamilies = [];
|
|
343
|
+
component.selectedModels = {};
|
|
344
|
+
|
|
345
|
+
// Select top models and their families
|
|
346
|
+
sorted.forEach(([key, _]) => {
|
|
347
|
+
const [family, model] = key.split(':');
|
|
348
|
+
if (!component.selectedFamilies.includes(family)) {
|
|
349
|
+
component.selectedFamilies.push(family);
|
|
350
|
+
}
|
|
351
|
+
if (!component.selectedModels[family]) {
|
|
352
|
+
component.selectedModels[family] = [];
|
|
353
|
+
}
|
|
354
|
+
if (!component.selectedModels[family].includes(model)) {
|
|
355
|
+
component.selectedModels[family].push(model);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
window.DashboardFilters.savePreferences(component);
|
|
360
|
+
// updateTrendChart uses debounce internally, call directly
|
|
361
|
+
component.updateTrendChart();
|
|
362
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Stats Module
|
|
3
|
+
* 职责:根据 Alpine.store('data') 计算账号统计数据
|
|
4
|
+
*
|
|
5
|
+
* 调用时机:
|
|
6
|
+
* - dashboard 组件 init() 时
|
|
7
|
+
* - $store.data 更新时(通过 $watch 监听)
|
|
8
|
+
*
|
|
9
|
+
* 统计维度:
|
|
10
|
+
* - total: 启用账号总数(排除禁用账号)
|
|
11
|
+
* - active: 有可用配额的账号数
|
|
12
|
+
* - limited: 配额受限或失效的账号数
|
|
13
|
+
* - subscription: 按订阅级别分类(ultra/pro/free)
|
|
14
|
+
*
|
|
15
|
+
* @module DashboardStats
|
|
16
|
+
*/
|
|
17
|
+
window.DashboardStats = window.DashboardStats || {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 更新账号统计数据
|
|
21
|
+
*
|
|
22
|
+
* 统计逻辑:
|
|
23
|
+
* 1. 仅统计启用的账号(enabled !== false)
|
|
24
|
+
* 2. 检查账号下所有追踪模型的配额
|
|
25
|
+
* 3. 如果任一追踪模型配额 <= 5%,则标记为 limited (Rate Limited Cooldown)
|
|
26
|
+
* 4. 如果所有追踪模型配额 > 5%,则标记为 active
|
|
27
|
+
* 5. 状态非 'ok' 的账号归为 limited
|
|
28
|
+
*
|
|
29
|
+
* @param {object} component - Dashboard 组件实例(Alpine.js 上下文)
|
|
30
|
+
* @param {object} component.stats - 统计数据对象(会被修改)
|
|
31
|
+
* @param {number} component.stats.total - 启用账号总数
|
|
32
|
+
* @param {number} component.stats.active - 活跃账号数
|
|
33
|
+
* @param {number} component.stats.limited - 受限账号数
|
|
34
|
+
* @param {object} component.stats.subscription - 订阅级别分布
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
window.DashboardStats.updateStats = function(component) {
|
|
38
|
+
const accounts = Alpine.store('data').accounts;
|
|
39
|
+
let active = 0, limited = 0;
|
|
40
|
+
|
|
41
|
+
// Only count enabled accounts in statistics
|
|
42
|
+
const enabledAccounts = accounts.filter(acc => acc.enabled !== false);
|
|
43
|
+
|
|
44
|
+
enabledAccounts.forEach(acc => {
|
|
45
|
+
if (acc.status === 'ok') {
|
|
46
|
+
const limits = Object.entries(acc.limits || {});
|
|
47
|
+
|
|
48
|
+
if (limits.length === 0) {
|
|
49
|
+
// No limit data available, consider limited to be safe
|
|
50
|
+
limited++;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if ANY tracked model is rate limited (<= 5%)
|
|
55
|
+
// We consider all models in the limits object as "tracked"
|
|
56
|
+
const hasRateLimitedModel = limits.some(([_, l]) => {
|
|
57
|
+
// Treat null/undefined fraction as 0 (limited)
|
|
58
|
+
if (!l || l.remainingFraction === null || l.remainingFraction === undefined) return true;
|
|
59
|
+
return l.remainingFraction <= 0.05;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (hasRateLimitedModel) {
|
|
63
|
+
limited++;
|
|
64
|
+
} else {
|
|
65
|
+
active++;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
limited++;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// TOTAL shows only enabled accounts
|
|
73
|
+
// Disabled accounts are excluded from all statistics
|
|
74
|
+
component.stats.total = enabledAccounts.length;
|
|
75
|
+
component.stats.active = active;
|
|
76
|
+
component.stats.limited = limited;
|
|
77
|
+
|
|
78
|
+
// Calculate model usage for rate limit details
|
|
79
|
+
let totalLimitedModels = 0;
|
|
80
|
+
let totalTrackedModels = 0;
|
|
81
|
+
|
|
82
|
+
enabledAccounts.forEach(acc => {
|
|
83
|
+
const limits = Object.entries(acc.limits || {});
|
|
84
|
+
limits.forEach(([id, l]) => {
|
|
85
|
+
totalTrackedModels++;
|
|
86
|
+
if (!l || l.remainingFraction == null || l.remainingFraction <= 0.05) {
|
|
87
|
+
totalLimitedModels++;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
component.stats.modelUsage = {
|
|
93
|
+
limited: totalLimitedModels,
|
|
94
|
+
total: totalTrackedModels
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Calculate subscription tier distribution
|
|
98
|
+
const subscription = { ultra: 0, pro: 0, free: 0 };
|
|
99
|
+
enabledAccounts.forEach(acc => {
|
|
100
|
+
const tier = acc.subscription?.tier || 'free';
|
|
101
|
+
if (tier === 'ultra') {
|
|
102
|
+
subscription.ultra++;
|
|
103
|
+
} else if (tier === 'pro') {
|
|
104
|
+
subscription.pro++;
|
|
105
|
+
} else {
|
|
106
|
+
subscription.free++;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
component.stats.subscription = subscription;
|
|
110
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Component (Refactored)
|
|
3
|
+
* Orchestrates stats, charts, and filters modules
|
|
4
|
+
* Registers itself to window.Components for Alpine.js to consume
|
|
5
|
+
*/
|
|
6
|
+
window.Components = window.Components || {};
|
|
7
|
+
|
|
8
|
+
window.Components.dashboard = () => ({
|
|
9
|
+
// Core state
|
|
10
|
+
stats: { total: 0, active: 0, limited: 0, overallHealth: 0, hasTrendData: false },
|
|
11
|
+
hasFilteredTrendData: true,
|
|
12
|
+
charts: { quotaDistribution: null, usageTrend: null },
|
|
13
|
+
usageStats: { total: 0, today: 0, thisHour: 0 },
|
|
14
|
+
historyData: {},
|
|
15
|
+
modelTree: {},
|
|
16
|
+
families: [],
|
|
17
|
+
|
|
18
|
+
// Filter state (from module)
|
|
19
|
+
...window.DashboardFilters.getInitialState(),
|
|
20
|
+
|
|
21
|
+
// Debounced chart update to prevent rapid successive updates
|
|
22
|
+
_debouncedUpdateTrendChart: null,
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
// Create debounced version of updateTrendChart (300ms delay for stability)
|
|
26
|
+
this._debouncedUpdateTrendChart = window.utils.debounce(() => {
|
|
27
|
+
window.DashboardCharts.updateTrendChart(this);
|
|
28
|
+
}, 300);
|
|
29
|
+
|
|
30
|
+
// Load saved preferences from localStorage
|
|
31
|
+
window.DashboardFilters.loadPreferences(this);
|
|
32
|
+
|
|
33
|
+
// Update stats when dashboard becomes active (skip initial trigger)
|
|
34
|
+
this.$watch('$store.global.activeTab', (val, oldVal) => {
|
|
35
|
+
if (val === 'dashboard' && oldVal !== undefined) {
|
|
36
|
+
this.$nextTick(() => {
|
|
37
|
+
this.updateStats();
|
|
38
|
+
this.updateCharts();
|
|
39
|
+
this.updateTrendChart();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Watch for data changes
|
|
45
|
+
this.$watch('$store.data.accounts', () => {
|
|
46
|
+
if (this.$store.global.activeTab === 'dashboard') {
|
|
47
|
+
this.updateStats();
|
|
48
|
+
// Debounce chart updates to prevent rapid flickering
|
|
49
|
+
if (this._debouncedUpdateCharts) {
|
|
50
|
+
this._debouncedUpdateCharts();
|
|
51
|
+
} else {
|
|
52
|
+
this._debouncedUpdateCharts = window.utils.debounce(() => this.updateCharts(), 100);
|
|
53
|
+
this._debouncedUpdateCharts();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Watch for history updates from data-store (automatically loaded with account data)
|
|
59
|
+
this.$watch('$store.data.usageHistory', (newHistory) => {
|
|
60
|
+
if (this.$store.global.activeTab === 'dashboard' && newHistory && Object.keys(newHistory).length > 0) {
|
|
61
|
+
// Optimization: Skip if data hasn't changed (prevents double render on load)
|
|
62
|
+
if (this.historyData && JSON.stringify(newHistory) === JSON.stringify(this.historyData)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.historyData = newHistory;
|
|
67
|
+
this.processHistory(newHistory);
|
|
68
|
+
this.stats.hasTrendData = true;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Initial update if already on dashboard
|
|
73
|
+
// Note: Alpine.store('data') may already have data from cache if initialized before this component
|
|
74
|
+
if (this.$store.global.activeTab === 'dashboard') {
|
|
75
|
+
this.$nextTick(() => {
|
|
76
|
+
this.updateStats();
|
|
77
|
+
this.updateCharts();
|
|
78
|
+
|
|
79
|
+
// Optimization: Only process history if it hasn't been processed yet
|
|
80
|
+
// The usageHistory watcher above will handle updates if data changes
|
|
81
|
+
const history = Alpine.store('data').usageHistory;
|
|
82
|
+
if (history && Object.keys(history).length > 0) {
|
|
83
|
+
// Check if we already have this data to avoid redundant chart update
|
|
84
|
+
if (!this.historyData || JSON.stringify(history) !== JSON.stringify(this.historyData)) {
|
|
85
|
+
this.historyData = history;
|
|
86
|
+
this.processHistory(history);
|
|
87
|
+
this.stats.hasTrendData = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
processHistory(history) {
|
|
95
|
+
// Build model tree from hierarchical data
|
|
96
|
+
const tree = {};
|
|
97
|
+
let total = 0, today = 0, thisHour = 0;
|
|
98
|
+
|
|
99
|
+
const now = new Date();
|
|
100
|
+
const todayStart = new Date(now);
|
|
101
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
102
|
+
const currentHour = new Date(now);
|
|
103
|
+
currentHour.setMinutes(0, 0, 0);
|
|
104
|
+
|
|
105
|
+
Object.entries(history).forEach(([iso, hourData]) => {
|
|
106
|
+
const timestamp = new Date(iso);
|
|
107
|
+
|
|
108
|
+
// Process each family in the hour data
|
|
109
|
+
Object.entries(hourData).forEach(([key, value]) => {
|
|
110
|
+
// Skip metadata keys
|
|
111
|
+
if (key === '_total' || key === 'total') return;
|
|
112
|
+
|
|
113
|
+
// Handle hierarchical format: { claude: { "opus-4-5": 10, "_subtotal": 10 } }
|
|
114
|
+
if (typeof value === 'object' && value !== null) {
|
|
115
|
+
if (!tree[key]) tree[key] = new Set();
|
|
116
|
+
|
|
117
|
+
Object.keys(value).forEach(modelName => {
|
|
118
|
+
if (modelName !== '_subtotal') {
|
|
119
|
+
tree[key].add(modelName);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Calculate totals
|
|
126
|
+
const hourTotal = hourData._total || hourData.total || 0;
|
|
127
|
+
total += hourTotal;
|
|
128
|
+
|
|
129
|
+
if (timestamp >= todayStart) {
|
|
130
|
+
today += hourTotal;
|
|
131
|
+
}
|
|
132
|
+
if (timestamp.getTime() === currentHour.getTime()) {
|
|
133
|
+
thisHour = hourTotal;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.usageStats = { total, today, thisHour };
|
|
138
|
+
|
|
139
|
+
// Convert Sets to sorted arrays
|
|
140
|
+
this.modelTree = {};
|
|
141
|
+
Object.entries(tree).forEach(([family, models]) => {
|
|
142
|
+
this.modelTree[family] = Array.from(models).sort();
|
|
143
|
+
});
|
|
144
|
+
this.families = Object.keys(this.modelTree).sort();
|
|
145
|
+
|
|
146
|
+
// Auto-select new families/models that haven't been configured
|
|
147
|
+
this.autoSelectNew();
|
|
148
|
+
|
|
149
|
+
this.updateTrendChart();
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Delegation methods for stats
|
|
153
|
+
updateStats() {
|
|
154
|
+
window.DashboardStats.updateStats(this);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Delegation methods for charts
|
|
158
|
+
updateCharts() {
|
|
159
|
+
window.DashboardCharts.updateCharts(this);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
updateTrendChart() {
|
|
163
|
+
// Use debounced version to prevent rapid successive updates
|
|
164
|
+
if (this._debouncedUpdateTrendChart) {
|
|
165
|
+
this._debouncedUpdateTrendChart();
|
|
166
|
+
} else {
|
|
167
|
+
// Fallback if debounced version not initialized
|
|
168
|
+
window.DashboardCharts.updateTrendChart(this);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// Delegation methods for filters
|
|
173
|
+
loadPreferences() {
|
|
174
|
+
window.DashboardFilters.loadPreferences(this);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
savePreferences() {
|
|
178
|
+
window.DashboardFilters.savePreferences(this);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
setDisplayMode(mode) {
|
|
182
|
+
window.DashboardFilters.setDisplayMode(this, mode);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
setTimeRange(range) {
|
|
186
|
+
window.DashboardFilters.setTimeRange(this, range);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
getTimeRangeLabel() {
|
|
190
|
+
return window.DashboardFilters.getTimeRangeLabel(this);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
toggleFamily(family) {
|
|
194
|
+
window.DashboardFilters.toggleFamily(this, family);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
toggleModel(family, model) {
|
|
198
|
+
window.DashboardFilters.toggleModel(this, family, model);
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
isFamilySelected(family) {
|
|
202
|
+
return window.DashboardFilters.isFamilySelected(this, family);
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
isModelSelected(family, model) {
|
|
206
|
+
return window.DashboardFilters.isModelSelected(this, family, model);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
selectAll() {
|
|
210
|
+
window.DashboardFilters.selectAll(this);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
deselectAll() {
|
|
214
|
+
window.DashboardFilters.deselectAll(this);
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
getFamilyColor(family) {
|
|
218
|
+
return window.DashboardFilters.getFamilyColor(family);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
getModelColor(family, modelIndex) {
|
|
222
|
+
return window.DashboardFilters.getModelColor(family, modelIndex);
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
getSelectedCount() {
|
|
226
|
+
return window.DashboardFilters.getSelectedCount(this);
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
autoSelectNew() {
|
|
230
|
+
window.DashboardFilters.autoSelectNew(this);
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
autoSelectTopN(n = 5) {
|
|
234
|
+
window.DashboardFilters.autoSelectTopN(this, n);
|
|
235
|
+
}
|
|
236
|
+
});
|