import React, { useState } from "react"; import { Play, Wand2, Eye, EyeOff, Save } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { Provider, UsageScript } from "@/types"; import { usageApi, type AppId } from "@/lib/api"; import JsonEditor from "./JsonEditor"; import * as prettier from "prettier/standalone"; import * as parserBabel from "prettier/parser-babel"; import * as pluginEstree from "prettier/plugins/estree"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import { cn } from "@/lib/utils"; interface UsageScriptModalProps { provider: Provider; appId: AppId; isOpen: boolean; onClose: () => void; onSave: (script: UsageScript) => void; } // 预设模板键名(用于国际化) const TEMPLATE_KEYS = { CUSTOM: "custom", GENERAL: "general", NEW_API: "newapi", } as const; // 生成预设模板的函数(支持国际化) const generatePresetTemplates = ( t: (key: string) => string, ): Record => ({ [TEMPLATE_KEYS.CUSTOM]: `({ request: { url: "", method: "GET", headers: {} }, extractor: function(response) { return { remaining: 0, unit: "USD" }; } })`, [TEMPLATE_KEYS.GENERAL]: `({ request: { url: "{{baseUrl}}/user/balance", method: "GET", headers: { "Authorization": "Bearer {{apiKey}}", "User-Agent": "cc-switch/1.0" } }, extractor: function(response) { return { isValid: response.is_active || true, remaining: response.balance, unit: "USD" }; } })`, [TEMPLATE_KEYS.NEW_API]: `({ request: { url: "{{baseUrl}}/api/user/self", method: "GET", headers: { "Content-Type": "application/json", "Authorization": "Bearer {{accessToken}}", "New-Api-User": "{{userId}}" }, }, extractor: function (response) { if (response.success && response.data) { return { planName: response.data.group || "${t("usageScript.defaultPlan")}", remaining: response.data.quota / 500000, used: response.data.used_quota / 500000, total: (response.data.quota + response.data.used_quota) / 500000, unit: "USD", }; } return { isValid: false, invalidMessage: response.message || "${t("usageScript.queryFailedMessage")}" }; }, })`, }); // 模板名称国际化键映射 const TEMPLATE_NAME_KEYS: Record = { [TEMPLATE_KEYS.CUSTOM]: "usageScript.templateCustom", [TEMPLATE_KEYS.GENERAL]: "usageScript.templateGeneral", [TEMPLATE_KEYS.NEW_API]: "usageScript.templateNewAPI", }; const UsageScriptModal: React.FC = ({ provider, appId, isOpen, onClose, onSave, }) => { const { t } = useTranslation(); // 生成带国际化的预设模板 const PRESET_TEMPLATES = generatePresetTemplates(t); const [script, setScript] = useState(() => { return ( provider.meta?.usage_script || { enabled: false, language: "javascript", code: PRESET_TEMPLATES[TEMPLATE_KEYS.GENERAL], timeout: 10, } ); }); const [testing, setTesting] = useState(false); // 🔧 失焦时的验证(严格)- 仅确保有效整数 const validateTimeout = (value: string): number => { const num = Number(value); if (isNaN(num) || value.trim() === "") { return 10; } if (!Number.isInteger(num)) { toast.warning( t("usageScript.timeoutMustBeInteger") || "超时时间必须为整数", ); } if (num < 0) { toast.error( t("usageScript.timeoutCannotBeNegative") || "超时时间不能为负数", ); return 10; } return Math.floor(num); }; // 🔧 失焦时的验证(严格)- 自动查询间隔 const validateAndClampInterval = (value: string): number => { const num = Number(value); if (isNaN(num) || value.trim() === "") { return 0; } if (!Number.isInteger(num)) { toast.warning( t("usageScript.intervalMustBeInteger") || "自动查询间隔必须为整数", ); } if (num < 0) { toast.error( t("usageScript.intervalCannotBeNegative") || "自动查询间隔不能为负数", ); return 0; } const clamped = Math.max(0, Math.min(1440, Math.floor(num))); if (clamped !== num && num > 0) { toast.info( t("usageScript.intervalAdjusted", { value: clamped }) || `自动查询间隔已调整为 ${clamped} 分钟`, ); } return clamped; }; const [selectedTemplate, setSelectedTemplate] = useState( () => { const existingScript = provider.meta?.usage_script; if (existingScript?.accessToken || existingScript?.userId) { return TEMPLATE_KEYS.NEW_API; } return null; }, ); const [showApiKey, setShowApiKey] = useState(false); const [showAccessToken, setShowAccessToken] = useState(false); const handleSave = () => { if (script.enabled && !script.code.trim()) { toast.error(t("usageScript.scriptEmpty")); return; } if (script.enabled && !script.code.includes("return")) { toast.error(t("usageScript.mustHaveReturn"), { duration: 5000 }); return; } onSave(script); onClose(); }; const handleTest = async () => { setTesting(true); try { const result = await usageApi.testScript( provider.id, appId, script.code, script.timeout, script.apiKey, script.baseUrl, script.accessToken, script.userId, ); if (result.success && result.data && result.data.length > 0) { const summary = result.data .map((plan) => { const planInfo = plan.planName ? `[${plan.planName}]` : ""; return `${planInfo} ${t("usage.remaining")} ${plan.remaining} ${plan.unit}`; }) .join(", "); toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000, }); } else { toast.error( `${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`, { duration: 5000, }, ); } } catch (error: any) { toast.error( `${t("usageScript.testFailed")}: ${error?.message || t("common.unknown")}`, { duration: 5000, }, ); } finally { setTesting(false); } }; const handleFormat = async () => { try { const formatted = await prettier.format(script.code, { parser: "babel", plugins: [parserBabel as any, pluginEstree as any], semi: true, singleQuote: false, tabWidth: 2, printWidth: 80, }); setScript({ ...script, code: formatted.trim() }); toast.success(t("usageScript.formatSuccess"), { duration: 1000 }); } catch (error: any) { toast.error( `${t("usageScript.formatFailed")}: ${error?.message || t("jsonEditor.invalidJson")}`, { duration: 3000, }, ); } }; const handleUsePreset = (presetName: string) => { const preset = PRESET_TEMPLATES[presetName]; if (preset) { if (presetName === TEMPLATE_KEYS.CUSTOM) { setScript({ ...script, code: preset, apiKey: undefined, baseUrl: undefined, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_KEYS.GENERAL) { setScript({ ...script, code: preset, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_KEYS.NEW_API) { setScript({ ...script, code: preset, apiKey: undefined, }); } setSelectedTemplate(presetName); } }; const shouldShowCredentialsConfig = selectedTemplate === TEMPLATE_KEYS.GENERAL || selectedTemplate === TEMPLATE_KEYS.NEW_API; const footer = ( <>
); return (

{t("usageScript.enableUsageQuery")}

{t("usageScript.autoQueryIntervalHint")}

setScript({ ...script, enabled: checked }) } aria-label={t("usageScript.enableUsageQuery")} />
{script.enabled && (
{/* 预设模板选择 */}
{t("usageScript.variablesHint")}
{Object.keys(PRESET_TEMPLATES).map((name) => { const isSelected = selectedTemplate === name; return ( ); })}
{/* 凭证配置 */} {shouldShowCredentialsConfig && (

{t("usageScript.credentialsConfig")}

{selectedTemplate === TEMPLATE_KEYS.GENERAL && ( <>
setScript({ ...script, apiKey: e.target.value }) } placeholder="sk-xxxxx" autoComplete="off" className="bg-card border-border-default" /> {script.apiKey && ( )}
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.example.com" autoComplete="off" className="bg-card border-border-default" />
)} {selectedTemplate === TEMPLATE_KEYS.NEW_API && ( <>
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.newapi.com" autoComplete="off" className="bg-card border-border-default" />
setScript({ ...script, accessToken: e.target.value, }) } placeholder={t( "usageScript.accessTokenPlaceholder", )} autoComplete="off" className="bg-card border-border-default" /> {script.accessToken && ( )}
setScript({ ...script, userId: e.target.value }) } placeholder={t("usageScript.userIdPlaceholder")} autoComplete="off" className="bg-card border-border-default" />
)}
)}
{/* 脚本配置 */}

{t("usageScript.scriptConfig")}

{t("usageScript.variablesHint")}

{ setScript({ ...script, request: { ...script.request, url: e.target.value }, }); }} placeholder={t("usageScript.requestUrlPlaceholder")} className="bg-card border-border-default" />
{ setScript({ ...script, request: { ...script.request, method: e.target.value.toUpperCase(), }, }); }} placeholder="GET / POST" className="bg-card border-border-default" />
setScript({ ...script, timeout: validateTimeout(e.target.value), }) } onBlur={(e) => setScript({ ...script, timeout: validateTimeout(e.target.value), }) } className="bg-card border-border-default" />
{ try { const parsed = JSON.parse(value || "{}"); setScript({ ...script, request: { ...script.request, headers: parsed }, }); } catch (error) { console.error("Invalid headers JSON", error); } }} height={180} />
{ try { const parsed = value?.trim() === "" ? undefined : JSON.parse(value); setScript({ ...script, request: { ...script.request, body: parsed }, }); } catch (error) { toast.error( t("usageScript.invalidJson") || "Body 必须是合法 JSON", ); } }} height={220} />
setScript({ ...script, autoIntervalMinutes: validateAndClampInterval( e.target.value, ), }) } onBlur={(e) => setScript({ ...script, autoIntervalMinutes: validateAndClampInterval( e.target.value, ), }) } className="bg-card border-border-default" />

{t("usageScript.autoQueryIntervalHint")}

{/* 提取器代码 */}
{t("usageScript.extractorHint")}
setScript({ ...script, code: value })} height={480} language="javascript" showMinimap={false} />
{/* 帮助信息 */}

{t("usageScript.scriptHelp")}

{t("usageScript.configFormat")}
                  {`({
  request: {
    url: "{{baseUrl}}/api/usage",
    method: "POST",
    headers: {
      "Authorization": "Bearer {{apiKey}}",
      "User-Agent": "cc-switch/1.0"
    }
  },
  extractor: function(response) {
    return {
      isValid: !response.error,
      remaining: response.balance,
      unit: "USD"
    };
  }
})`}
                
{t("usageScript.extractorFormat")}
  • {t("usageScript.fieldIsValid")}
  • {t("usageScript.fieldInvalidMessage")}
  • {t("usageScript.fieldRemaining")}
  • {t("usageScript.fieldUnit")}
  • {t("usageScript.fieldPlanName")}
  • {t("usageScript.fieldTotal")}
  • {t("usageScript.fieldUsed")}
  • {t("usageScript.fieldExtra")}
{t("usageScript.tips")}
  • {t("usageScript.tip1", { apiKey: "{{apiKey}}", baseUrl: "{{baseUrl}}", })}
  • {t("usageScript.tip2")}
  • {t("usageScript.tip3")}
)}
); }; export default UsageScriptModal;