Skip to content

Commit 5813ae3

Browse files
committed
feat: add 5H/weekly quota switcher and 7-day reset smart warmup
1 parent a2e3c45 commit 5813ae3

14 files changed

Lines changed: 363 additions & 332 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -362,10 +362,9 @@ pub fn run() {
362362

363363
info!("Headless proxy service is running.");
364364

365-
// [DISABLED] Start smart scheduler (Automatic warmup disabled as per user request)
366-
// modules::scheduler::start_scheduler(None, proxy_state.clone());
367-
info!("Smart scheduler (Automatic Warmup) is DISABLED.");
368-
info!("Smart scheduler started in headless mode.");
365+
// Start smart scheduler for 7-day weekly reset warmup
366+
modules::scheduler::start_scheduler(None, proxy_state.clone());
367+
info!("Smart scheduler (7-Day Weekly Reset Warmup) started in headless mode.");
369368
}
370369
Err(e) => {
371370
error!("Failed to load config for headless mode: {}", e);
@@ -488,10 +487,10 @@ pub fn run() {
488487
}
489488
});
490489

491-
// [DISABLED] Start smart scheduler (Automatic warmup disabled as per user request)
492-
// let scheduler_state = app.handle().state::<commands::proxy::ProxyServiceState>();
493-
// modules::scheduler::start_scheduler(Some(app.handle().clone()), scheduler_state.inner().clone());
494-
info!("Smart scheduler (Automatic Warmup) is DISABLED.");
490+
// Start smart scheduler for 7-day weekly reset warmup
491+
let scheduler_state = app.handle().state::<commands::proxy::ProxyServiceState>();
492+
modules::scheduler::start_scheduler(Some(app.handle().clone()), scheduler_state.inner().clone());
493+
info!("Smart scheduler (7-Day Weekly Reset Warmup) initialized.");
495494

496495
// [PHASE 1] 已整合至 Axum 端口 (8045),不再单独启动 19527 端口
497496
info!("Management API integrated into main proxy server (port 8045)");

src-tauri/src/modules/account.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,11 +2057,10 @@ pub async fn refresh_all_quotas_logic() -> Result<RefreshStats, String> {
20572057
elapsed.as_millis()
20582058
));
20592059

2060-
// After quota refresh, immediately check and trigger warmup for recovered models
2061-
// [Disabled] Automatic warmup is temporarily disabled
2062-
// tokio::spawn(async {
2063-
// check_and_trigger_warmup_for_recovered_models().await;
2064-
// });
2060+
// After quota refresh, immediately check and trigger warmup for weekly recovered models
2061+
tokio::spawn(async {
2062+
check_and_trigger_warmup_for_recovered_models().await;
2063+
});
20652064

20662065
Ok(RefreshStats {
20672066
total,

src-tauri/src/modules/scheduler.rs

Lines changed: 174 additions & 252 deletions
Large diffs are not rendered by default.

src-tauri/src/proxy/handlers/warmup.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,6 @@ pub async fn handle_warmup(
4747
) -> Response {
4848
let start_time = std::time::Instant::now();
4949

50-
// ===== 前置检查:跳过 gemini-2.5-* 家族模型 =====
51-
let model_lower = req.model.to_lowercase();
52-
if model_lower.contains("2.5-") || model_lower.contains("2-5-") {
53-
info!(
54-
"[Warmup-API] SKIP: gemini-2.5-* model not supported for warmup: {} / {}",
55-
req.email, req.model
56-
);
57-
return (
58-
StatusCode::OK,
59-
Json(WarmupResponse {
60-
success: true,
61-
message: format!(
62-
"Skipped warmup for {} (2.5 models not supported)",
63-
req.model
64-
),
65-
error: None,
66-
}),
67-
)
68-
.into_response();
69-
}
70-
7150
info!(
7251
"[Warmup-API] ========== START: email={}, model={} ==========",
7352
req.email, req.model

src-tauri/src/proxy/middleware/service_status.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pub async fn service_status_middleware(
1313
) -> Response {
1414
let path = request.uri().path();
1515

16-
// Always allow Admin API and Auth callback
17-
if path.starts_with("/api/") || path == "/auth/callback" || path == "/health" {
16+
// Always allow Admin API, internal endpoints and Auth callback
17+
if path.starts_with("/api/") || path.starts_with("/internal/") || path == "/auth/callback" || path == "/health" {
1818
return next.run(request).await;
1919
}
2020

src/components/accounts/AccountCard.tsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface AccountCardProps {
2626
onWarmup?: () => void;
2727
onUpdateLabel?: (label: string) => void;
2828
onViewError: () => void;
29+
quotaWindow?: '5h' | 'weekly';
2930
}
3031

3132
// 使用统一的模型配置
@@ -36,7 +37,7 @@ const DEFAULT_MODELS = Object.entries(MODEL_CONFIG).map(([id, config]) => ({
3637
Icon: config.Icon
3738
}));
3839

39-
function AccountCard({ account, selected, onSelect, isCurrent: propIsCurrent, isRefreshing, isSwitching = false, onSwitch, onRefresh, onViewDetails, onExport, onDelete, onToggleProxy, onViewDevice, onWarmup, onUpdateLabel, onViewError }: AccountCardProps) {
40+
function AccountCard({ account, selected, onSelect, isCurrent: propIsCurrent, isRefreshing, isSwitching = false, onSwitch, onRefresh, onViewDetails, onExport, onDelete, onToggleProxy, onViewDevice, onWarmup, onUpdateLabel, onViewError, quotaWindow }: AccountCardProps) {
4041
const { t } = useTranslation();
4142
const { config, showAllQuotas } = useConfigStore();
4243
const isDisabled = Boolean(account.disabled);
@@ -129,6 +130,27 @@ function AccountCard({ account, selected, onSelect, isCurrent: propIsCurrent, is
129130
return sortModels(models).filter(m => m.id !== 'claude-sonnet-4-6-thinking' && m.id !== 'claude-sonnet-4-5-thinking' && m.id !== 'claude-opus-4-5-thinking');
130131
}, [config, account, showAllQuotas]);
131132

133+
// 解析周配额项 (当处于 weekly 视图时)
134+
const weeklyItems = useMemo(() => {
135+
if (quotaWindow !== 'weekly') return [];
136+
return (account.quota?.quota_groups || []).flatMap(group => {
137+
return group.buckets
138+
.filter(b => b.window.toLowerCase().includes('week') || b.bucket_id.toLowerCase().includes('week'))
139+
.map(b => {
140+
const shortGroupName = group.display_name
141+
.replace(/ models?$/i, '')
142+
.replace(/Claude and GPT/i, 'Claude/GPT');
143+
return {
144+
id: `${group.display_name}-${b.bucket_id}`,
145+
label: b.display_name ? `${shortGroupName} (${b.display_name})` : `${shortGroupName} (周)`,
146+
percentage: Math.round((b.remaining_fraction || 0) * 100),
147+
resetTime: b.reset_time,
148+
Icon: shortGroupName.toLowerCase().includes('claude') ? Sparkles : Bot,
149+
};
150+
});
151+
});
152+
}, [quotaWindow, account.quota?.quota_groups]);
153+
132154
const isModelProtected = (key?: string) => {
133155
if (!key) return false;
134156
return account.protected_models?.includes(key);
@@ -262,17 +284,29 @@ function AccountCard({ account, selected, onSelect, isCurrent: propIsCurrent, is
262284
</div>
263285
) : (
264286
<div className="grid grid-cols-1 gap-2 content-start">
265-
{displayModels.map((model) => (
266-
<QuotaItem
267-
key={model.id}
268-
label={model.label}
269-
percentage={model.data?.percentage || 0}
270-
resetTime={model.data?.reset_time}
271-
isProtected={isModelProtected(model.protectedKey)}
272-
liveLimit={getLiveLimitForModel(account, model.id, model.protectedKey)}
273-
Icon={model.Icon}
274-
/>
275-
))}
287+
{quotaWindow === 'weekly' && weeklyItems.length > 0 ? (
288+
weeklyItems.map((item) => (
289+
<QuotaItem
290+
key={item.id}
291+
label={item.label}
292+
percentage={item.percentage}
293+
resetTime={item.resetTime}
294+
Icon={item.Icon}
295+
/>
296+
))
297+
) : (
298+
displayModels.map((model) => (
299+
<QuotaItem
300+
key={model.id}
301+
label={model.label}
302+
percentage={model.data?.percentage || 0}
303+
resetTime={model.data?.reset_time}
304+
isProtected={isModelProtected(model.protectedKey)}
305+
liveLimit={getLiveLimitForModel(account, model.id, model.protectedKey)}
306+
Icon={model.Icon}
307+
/>
308+
))
309+
)}
276310
</div>
277311
)}
278312
</div>

src/components/accounts/AccountGrid.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ interface AccountGridProps {
1919
onWarmup?: (accountId: string) => void;
2020
onUpdateLabel?: (accountId: string, label: string) => void;
2121
onViewError: (accountId: string) => void;
22+
quotaWindow?: '5h' | 'weekly';
2223
}
2324

2425

25-
function AccountGrid({ accounts, selectedIds, refreshingIds, onToggleSelect, currentAccountId, switchingAccountId, onSwitch, onRefresh, onViewDetails, onExport, onDelete, onToggleProxy, onViewDevice, onWarmup, onUpdateLabel, onViewError }: AccountGridProps) {
26+
function AccountGrid({ accounts, selectedIds, refreshingIds, onToggleSelect, currentAccountId, switchingAccountId, onSwitch, onRefresh, onViewDetails, onExport, onDelete, onToggleProxy, onViewDevice, onWarmup, onUpdateLabel, onViewError, quotaWindow }: AccountGridProps) {
2627
const { t } = useTranslation();
2728
if (accounts.length === 0) {
2829
return (
@@ -54,6 +55,7 @@ function AccountGrid({ accounts, selectedIds, refreshingIds, onToggleSelect, cur
5455
onWarmup={onWarmup ? () => onWarmup(account.id) : undefined}
5556
onUpdateLabel={onUpdateLabel ? (label: string) => onUpdateLabel(account.id, label) : undefined}
5657
onViewError={() => onViewError(account.id)}
58+
quotaWindow={quotaWindow}
5759
/>
5860
))}
5961
</div>

src/components/accounts/AccountTable.tsx

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ interface AccountTableProps {
8181
/** 拖拽排序回调,当用户完成拖拽时触发 */
8282
onReorder?: (accountIds: string[]) => void;
8383
onViewError: (accountId: string) => void;
84+
quotaWindow?: '5h' | 'weekly';
8485
}
8586

8687
interface SortableRowProps {
@@ -101,6 +102,7 @@ interface SortableRowProps {
101102
onWarmup?: () => void;
102103
onUpdateLabel?: (label: string) => void;
103104
onViewError: () => void;
105+
quotaWindow?: '5h' | 'weekly';
104106
}
105107

106108
interface AccountRowContentProps {
@@ -119,6 +121,7 @@ interface AccountRowContentProps {
119121
onWarmup?: () => void;
120122
onUpdateLabel?: (label: string) => void;
121123
onViewError: () => void;
124+
quotaWindow?: '5h' | 'weekly';
122125
}
123126

124127
// ============================================================================
@@ -177,6 +180,7 @@ function SortableAccountRow({
177180
onWarmup,
178181
onUpdateLabel,
179182
onViewError,
183+
quotaWindow,
180184
}: SortableRowProps) {
181185
const { t } = useTranslation();
182186
const {
@@ -221,10 +225,10 @@ function SortableAccountRow({
221225
<td className="px-2 py-1 w-10 align-middle">
222226
<input
223227
type="checkbox"
224-
className="checkbox checkbox-xs rounded border-2 border-gray-400 dark:border-gray-500 checked:border-blue-600 checked:bg-blue-600 [--chkbg:theme(colors.blue.600)] [--chkfg:white]"
228+
className="checkbox checkbox-sm rounded border-2 border-gray-400 dark:border-gray-500 checked:border-blue-600 checked:bg-blue-600 [--chkbg:theme(colors.blue.600)] [--chkfg:white]"
225229
checked={selected}
226230
onChange={onSelect}
227-
onClick={(e) => e.stopPropagation()}
231+
disabled={isRefreshing}
228232
/>
229233
</td>
230234
<AccountRowContent
@@ -243,6 +247,7 @@ function SortableAccountRow({
243247
onWarmup={onWarmup}
244248
onUpdateLabel={onUpdateLabel}
245249
onViewError={onViewError}
250+
quotaWindow={quotaWindow}
246251
/>
247252
</tr>
248253
);
@@ -268,6 +273,7 @@ function AccountRowContent({
268273
onWarmup,
269274
onUpdateLabel,
270275
onViewError,
276+
quotaWindow,
271277
}: AccountRowContentProps) {
272278
const { t } = useTranslation();
273279
const { config, showAllQuotas } = useConfigStore();
@@ -297,7 +303,26 @@ function AccountRowContent({
297303
}
298304
};
299305

300-
// 使用统一的模型配置
306+
// 解析周配额项 (当处于 weekly 视图时)
307+
const weeklyItems = useMemo(() => {
308+
if (quotaWindow !== 'weekly') return [];
309+
return (account.quota?.quota_groups || []).flatMap(group => {
310+
return group.buckets
311+
.filter(b => b.window.toLowerCase().includes('week') || b.bucket_id.toLowerCase().includes('week'))
312+
.map(b => {
313+
const shortGroupName = group.display_name
314+
.replace(/ models?$/i, '')
315+
.replace(/Claude and GPT/i, 'Claude/GPT');
316+
return {
317+
id: `${group.display_name}-${b.bucket_id}`,
318+
label: b.display_name ? `${shortGroupName} (${b.display_name})` : `${shortGroupName} (周)`,
319+
percentage: Math.round((b.remaining_fraction || 0) * 100),
320+
resetTime: b.reset_time,
321+
Icon: shortGroupName.toLowerCase().includes('claude') ? Sparkles : Bot,
322+
};
323+
});
324+
});
325+
}, [quotaWindow, account.quota?.quota_groups]);
301326

302327
// 获取要显示的模型列表
303328
const pinnedModels = ensurePinnedImageSelector(
@@ -509,23 +534,37 @@ function AccountRowContent({
509534
) : (
510535
<div className={cn(
511536
"grid gap-x-2 gap-y-1 py-0",
512-
displayModels.length === 1 ? "grid-cols-1" : "grid-cols-2"
537+
(quotaWindow === 'weekly' && weeklyItems.length > 0)
538+
? (weeklyItems.length === 1 ? "grid-cols-1" : "grid-cols-2")
539+
: (displayModels.length === 1 ? "grid-cols-1" : "grid-cols-2")
513540
)}>
514-
{displayModels.map((model) => {
515-
const modelData = model.data;
516-
517-
return (
541+
{quotaWindow === 'weekly' && weeklyItems.length > 0 ? (
542+
weeklyItems.map((item) => (
518543
<QuotaItem
519-
key={model.id}
520-
label={model.label}
521-
percentage={modelData?.percentage || 0}
522-
resetTime={modelData?.reset_time}
523-
isProtected={isModelProtected(account.protected_models, model.protectedKey)}
524-
liveLimit={getLiveLimitForModel(account, model.id, model.protectedKey)}
525-
Icon={MODEL_CONFIG[model.id]?.Icon || Bot}
544+
key={item.id}
545+
label={item.label}
546+
percentage={item.percentage}
547+
resetTime={item.resetTime}
548+
Icon={item.Icon}
526549
/>
527-
);
528-
})}
550+
))
551+
) : (
552+
displayModels.map((model) => {
553+
const modelData = model.data;
554+
555+
return (
556+
<QuotaItem
557+
key={model.id}
558+
label={model.label}
559+
percentage={modelData?.percentage || 0}
560+
resetTime={modelData?.reset_time}
561+
isProtected={isModelProtected(account.protected_models, model.protectedKey)}
562+
liveLimit={getLiveLimitForModel(account, model.id, model.protectedKey)}
563+
Icon={MODEL_CONFIG[model.id]?.Icon || Bot}
564+
/>
565+
);
566+
})
567+
)}
529568
</div>
530569
)}
531570
</td>
@@ -686,6 +725,7 @@ function AccountTable({
686725
onWarmup,
687726
onUpdateLabel,
688727
onViewError,
728+
quotaWindow,
689729
}: AccountTableProps) {
690730
const { t } = useTranslation();
691731

@@ -756,7 +796,7 @@ function AccountTable({
756796
</th>
757797
<th className="px-2 py-1 text-left rtl:text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[300px] whitespace-nowrap">{t('accounts.table.email')}</th>
758798
<th className="px-2 py-1 text-left rtl:text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider min-w-[340px] whitespace-nowrap">
759-
{t('accounts.table.quota')}
799+
{quotaWindow === 'weekly' ? t('accounts.table.weekly_quota', '周配额') : t('accounts.table.quota')}
760800
</th>
761801
<th className="px-2 py-1 text-left rtl:text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-[90px] whitespace-nowrap">{t('accounts.table.last_used')}</th>
762802
<th className="px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider whitespace-nowrap sticky right-0 w-[220px] bg-gray-50 dark:bg-base-200 z-20 shadow-[-12px_0_12px_-12px_rgba(0,0,0,0.1)] dark:shadow-[-12px_0_12px_-12px_rgba(255,255,255,0.05)] text-center">{t('accounts.table.actions')}</th>
@@ -784,6 +824,7 @@ function AccountTable({
784824
onWarmup={onWarmup ? () => onWarmup(account.id) : undefined}
785825
onUpdateLabel={onUpdateLabel ? (label: string) => onUpdateLabel(account.id, label) : undefined}
786826
onViewError={() => onViewError(account.id)}
827+
quotaWindow={quotaWindow}
787828
/>
788829
))}
789830
</tbody>
@@ -825,6 +866,7 @@ function AccountTable({
825866
onToggleProxy={() => { }}
826867
isDisabled={Boolean(activeAccount.disabled)}
827868
onViewError={() => { }}
869+
quotaWindow={quotaWindow}
828870
/>
829871
</tr>
830872
</tbody>

0 commit comments

Comments
 (0)