feat(usage): add auto-refresh interval for usage queries

New Features:
- Users can configure auto-query interval in "Configure Usage Query" dialog
- Interval in minutes (0 = disabled, recommend 5-60 minutes)
- Auto-query only enabled for currently active provider
- Display last query timestamp in relative time format (e.g., "5 min ago")
- Execute first query immediately when enabled, then repeat at intervals

Technical Implementation:
- Backend: Add auto_query_interval field to UsageScript struct
- Frontend: Create useAutoUsageQuery Hook to manage timers and query state
- UI: Add auto-query interval input field in UsageScriptModal
- Integration: Display auto-query results and timestamp in UsageFooter
- i18n: Add Chinese and English translations

UX Improvements:
- Minimum interval protection (1 minute) to prevent API abuse
- Auto-cleanup timers on component unmount
- Silent failure handling for auto-queries, non-intrusive to users
- Prioritize auto-query results, fallback to manual query results
- Timestamp display positioned next to refresh button for better clarity
This commit is contained in:
Jason
2025-11-05 15:48:19 +08:00
parent 254896e5eb
commit 21d29b9c2d
8 changed files with 224 additions and 14 deletions

View File

@@ -0,0 +1,118 @@
import { useEffect, useRef, useState } from "react";
import { usageApi, type AppId } from "@/lib/api";
import type { Provider, UsageResult } from "@/types";
export interface AutoQueryState {
result: UsageResult | null;
lastQueriedAt: number | null;
isQuerying: boolean;
error: string | null;
}
/**
* 自动用量查询 Hook
* @param provider 供应商对象
* @param appId 应用 IDclaude 或 codex
* @param enabled 是否启用(通常只对当前激活的供应商启用)
* @returns 自动查询状态
*/
export function useAutoUsageQuery(
provider: Provider,
appId: AppId,
enabled: boolean
): AutoQueryState {
const [state, setState] = useState<AutoQueryState>({
result: null,
lastQueriedAt: null,
isQuerying: false,
error: null,
});
const timerRef = useRef<NodeJS.Timeout | null>(null);
const isMountedRef = useRef(true);
// 跟踪组件挂载状态
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
useEffect(() => {
// 清理旧定时器
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
// 重置状态(切换供应商或禁用时)
if (!enabled) {
setState({
result: null,
lastQueriedAt: null,
isQuerying: false,
error: null,
});
return;
}
// 检查是否启用自动查询
const usageScript = provider.meta?.usage_script;
if (!usageScript?.enabled) {
return;
}
const interval = usageScript.autoQueryInterval || 0;
if (interval === 0) {
return; // 间隔为 0不启用自动查询
}
// 限制最小间隔为 1 分钟,避免过于频繁
const actualInterval = Math.max(interval, 1);
// 执行查询的函数
const executeQuery = async () => {
if (!isMountedRef.current) return;
setState((prev) => ({ ...prev, isQuerying: true, error: null }));
try {
const result = await usageApi.query(provider.id, appId);
if (isMountedRef.current) {
setState({
result,
lastQueriedAt: Date.now(),
isQuerying: false,
error: result.success ? null : result.error || "Unknown error",
});
}
} catch (error: any) {
if (isMountedRef.current) {
setState((prev) => ({
...prev,
isQuerying: false,
error: error?.message || "Query failed",
}));
}
console.error("[AutoQuery] Failed:", error);
}
};
// 立即执行一次查询
executeQuery();
// 设置定时器(间隔单位:分钟)
timerRef.current = setInterval(executeQuery, actualInterval * 60 * 1000);
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
}, [provider.id, provider.meta?.usage_script, appId, enabled]);
return state;
}