diff --git a/src-tauri/src/commands/provider.rs b/src-tauri/src/commands/provider.rs index 6732c6d..a55814a 100644 --- a/src-tauri/src/commands/provider.rs +++ b/src-tauri/src/commands/provider.rs @@ -108,8 +108,9 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result< } /// 查询供应商用量 +#[allow(non_snake_case)] #[tauri::command] -pub async fn query_provider_usage( +pub async fn queryProviderUsage( state: State<'_, AppState>, #[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端 @@ -122,8 +123,9 @@ pub async fn query_provider_usage( } /// 测试用量脚本(使用当前编辑器中的脚本,不保存) +#[allow(non_snake_case)] #[tauri::command] -pub async fn test_usage_script( +pub async fn testUsageScript( state: State<'_, AppState>, #[allow(non_snake_case)] providerId: String, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a58f06..859ea98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -539,8 +539,8 @@ pub fn run() { commands::delete_claude_mcp_server, commands::validate_mcp_command, // usage query - commands::query_provider_usage, - commands::test_usage_script, + commands::queryProviderUsage, + commands::testUsageScript, // New MCP via config.json (SSOT) commands::get_mcp_config, commands::upsert_mcp_server_in_config, diff --git a/src/components/UsageFooter.tsx b/src/components/UsageFooter.tsx index 9851976..fda2811 100644 --- a/src/components/UsageFooter.tsx +++ b/src/components/UsageFooter.tsx @@ -3,8 +3,7 @@ import { RefreshCw, AlertCircle, Clock } from "lucide-react"; import { useTranslation } from "react-i18next"; import { type AppId } from "@/lib/api"; import { useUsageQuery } from "@/lib/query/queries"; -import { useAutoUsageQuery } from "@/hooks/useAutoUsageQuery"; -import { UsageData, Provider } from "../types"; +import { UsageData, Provider } from "@/types"; interface UsageFooterProps { provider: Provider; @@ -23,20 +22,34 @@ const UsageFooter: React.FC = ({ }) => { const { t } = useTranslation(); - // 手动查询(点击刷新按钮时使用) + // 统一的用量查询(自动查询仅对当前激活的供应商启用) + const autoQueryInterval = isCurrent + ? provider.meta?.usage_script?.autoQueryInterval || 0 + : 0; + const { - data: manualUsage, + data: usage, isFetching: loading, + lastQueriedAt, refetch, - } = useUsageQuery(providerId, appId, usageEnabled); + } = useUsageQuery(providerId, appId, { + enabled: usageEnabled, + autoQueryInterval, + }); - // 自动查询(仅对当前激活的供应商启用) - const autoQuery = useAutoUsageQuery(provider, appId, isCurrent && usageEnabled); + // 🆕 定期更新当前时间,用于刷新相对时间显示 + const [now, setNow] = React.useState(Date.now()); - // 优先使用自动查询结果,如果没有则使用手动查询结果 - const usage = autoQuery.result || manualUsage; - const isAutoQuerying = autoQuery.isQuerying; - const lastQueriedAt = autoQuery.lastQueriedAt; + React.useEffect(() => { + if (!lastQueriedAt) return; + + // 每30秒更新一次当前时间,触发相对时间显示的刷新 + const interval = setInterval(() => { + setNow(Date.now()); + }, 30000); // 30秒 + + return () => clearInterval(interval); + }, [lastQueriedAt]); // 只在启用用量查询且有数据时显示 if (!usageEnabled || !usage) return null; @@ -82,18 +95,18 @@ const UsageFooter: React.FC = ({ {lastQueriedAt && ( - {formatRelativeTime(lastQueriedAt, t)} + {formatRelativeTime(lastQueriedAt, now, t)} )} @@ -227,9 +240,9 @@ const UsagePlanItem: React.FC<{ data: UsageData }> = ({ data }) => { // 格式化相对时间 function formatRelativeTime( timestamp: number, + now: number, t: (key: string, options?: { count?: number }) => string ): string { - const now = Date.now(); const diff = Math.floor((now - timestamp) / 1000); // 秒 if (diff < 60) { diff --git a/src/components/UsageScriptModal.tsx b/src/components/UsageScriptModal.tsx index 5b080e5..4662b0b 100644 --- a/src/components/UsageScriptModal.tsx +++ b/src/components/UsageScriptModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Play, Wand2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; -import { Provider, UsageScript } from "../types"; +import { Provider, UsageScript } from "@/types"; import { usageApi, type AppId } from "@/lib/api"; import JsonEditor from "./JsonEditor"; import * as prettier from "prettier/standalone"; diff --git a/src/hooks/useAutoUsageQuery.ts b/src/hooks/useAutoUsageQuery.ts deleted file mode 100644 index e549b6a..0000000 --- a/src/hooks/useAutoUsageQuery.ts +++ /dev/null @@ -1,118 +0,0 @@ -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 应用 ID(claude 或 codex) - * @param enabled 是否启用(通常只对当前激活的供应商启用) - * @returns 自动查询状态 - */ -export function useAutoUsageQuery( - provider: Provider, - appId: AppId, - enabled: boolean -): AutoQueryState { - const [state, setState] = useState({ - result: null, - lastQueriedAt: null, - isQuerying: false, - error: null, - }); - - const timerRef = useRef(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; -} diff --git a/src/lib/api/usage.ts b/src/lib/api/usage.ts index 4751b68..cb145ac 100644 --- a/src/lib/api/usage.ts +++ b/src/lib/api/usage.ts @@ -6,7 +6,7 @@ import i18n from "@/i18n"; export const usageApi = { async query(providerId: string, appId: AppId): Promise { try { - return await invoke("query_provider_usage", { + return await invoke("queryProviderUsage", { providerId: providerId, app: appId, }); @@ -36,7 +36,7 @@ export const usageApi = { userId?: string ): Promise { try { - return await invoke("test_usage_script", { + return await invoke("testUsageScript", { providerId: providerId, app: appId, scriptCode: scriptCode, @@ -59,4 +59,3 @@ export const usageApi = { } }, }; - diff --git a/src/lib/query/queries.ts b/src/lib/query/queries.ts index 8f03fa0..c76e7c5 100644 --- a/src/lib/query/queries.ts +++ b/src/lib/query/queries.ts @@ -83,17 +83,33 @@ export const useSettingsQuery = (): UseQueryResult => { }); }; +export interface UseUsageQueryOptions { + enabled?: boolean; + autoQueryInterval?: number; // 自动查询间隔(分钟),0 表示禁用 +} + export const useUsageQuery = ( providerId: string, appId: AppId, - enabled: boolean = true, -): UseQueryResult => { - return useQuery({ + options?: UseUsageQueryOptions, +) => { + const { enabled = true, autoQueryInterval = 0 } = options || {}; + + const query = useQuery({ queryKey: ["usage", providerId, appId], queryFn: async () => usageApi.query(providerId, appId), enabled: enabled && !!providerId, + refetchInterval: + autoQueryInterval > 0 + ? Math.max(autoQueryInterval, 1) * 60 * 1000 // 最小1分钟 + : false, refetchOnWindowFocus: false, retry: false, staleTime: 5 * 60 * 1000, // 5分钟 }); + + return { + ...query, + lastQueriedAt: query.dataUpdatedAt || null, + }; };