import React, { useState } from "react"; import { Play, Wand2, Eye, EyeOff } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; 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 sanitizeNumberInput = (value: string): string => { // 移除所有非数字字符 let cleaned = value.replace(/[^\d]/g, ""); // 移除前导零(除非输入的就是 "0") if (cleaned.length > 1 && cleaned.startsWith("0")) { cleaned = cleaned.replace(/^0+/, ""); } return cleaned; }; // 🔧 失焦时的验证(严格)- 仅确保有效整数 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; } // 约束到 [0, 1440] 范围(最大24小时) 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; }; // 跟踪当前选择的模板类型(用于控制高级配置的显示) // 初始化:如果已有 accessToken 或 userId,说明是 NewAPI 模板 const [selectedTemplate, setSelectedTemplate] = useState( () => { const existingScript = provider.meta?.usage_script; if (existingScript?.accessToken || existingScript?.userId) { return TEMPLATE_KEYS.NEW_API; } return null; }, ); // 控制 API Key 的显示/隐藏 const [showApiKey, setShowApiKey] = useState(false); const [showAccessToken, setShowAccessToken] = useState(false); const handleSave = () => { // 验证脚本格式 if (script.enabled && !script.code.trim()) { toast.error(t("usageScript.scriptEmpty")); return; } // 基本的 JS 语法检查(检查是否包含 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) { // 通用:保留 apiKey 和 baseUrl,清空 NewAPI 字段 setScript({ ...script, code: preset, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_KEYS.NEW_API) { // NewAPI:清空 apiKey(NewAPI 不使用通用的 apiKey) setScript({ ...script, code: preset, apiKey: undefined, }); } setSelectedTemplate(presetName); // 记录选择的模板 } }; // 判断是否应该显示凭证配置区域 const shouldShowCredentialsConfig = selectedTemplate === TEMPLATE_KEYS.GENERAL || selectedTemplate === TEMPLATE_KEYS.NEW_API; return ( !open && onClose()}> {t("usageScript.title")} - {provider.name} {/* Content - Scrollable */}
{/* 启用开关 */}

{t("usageScript.enableUsageQuery")}

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

{t("usageScript.credentialsConfig")}

{/* 通用模板:显示 apiKey + baseUrl */} {selectedTemplate === TEMPLATE_KEYS.GENERAL && ( <>
setScript({ ...script, apiKey: e.target.value }) } placeholder="sk-xxxxx" autoComplete="off" /> {script.apiKey && ( )}
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.example.com" autoComplete="off" />
)} {/* NewAPI 模板:显示 baseUrl + accessToken + userId */} {selectedTemplate === TEMPLATE_KEYS.NEW_API && ( <>
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.newapi.com" autoComplete="off" />
setScript({ ...script, accessToken: e.target.value, }) } placeholder={t( "usageScript.accessTokenPlaceholder", )} autoComplete="off" /> {script.accessToken && ( )}
setScript({ ...script, userId: e.target.value }) } placeholder={t("usageScript.userIdPlaceholder")} autoComplete="off" />
)}
)} {/* 脚本编辑器 */}
setScript({ ...script, code })} height="300px" language="javascript" />

{t("usageScript.variablesHint", { apiKey: "{{apiKey}}", baseUrl: "{{baseUrl}}", })}

{/* 配置选项 */}
{ // 输入时:只清理格式,允许临时为空,避免强制回填默认值 const cleaned = sanitizeNumberInput(e.target.value); setScript((prev) => ({ ...prev, timeout: cleaned === "" ? undefined : parseInt(cleaned, 10), })); }} onBlur={(e) => { // 失焦时:严格验证并约束范围 const validated = validateTimeout(e.target.value); setScript({ ...script, timeout: validated }); }} />

{t("usageScript.timeoutHint") || "范围: 2-30 秒"}

{/* 🆕 自动查询间隔 */}
{ // 输入时:只清理格式,允许临时为空 const cleaned = sanitizeNumberInput(e.target.value); setScript((prev) => ({ ...prev, autoQueryInterval: cleaned === "" ? undefined : parseInt(cleaned, 10), })); }} onBlur={(e) => { // 失焦时:严格验证并约束范围 const validated = validateAndClampInterval( e.target.value, ); setScript({ ...script, autoQueryInterval: validated }); }} />

{t("usageScript.autoQueryIntervalHint")}

{/* 脚本说明 */}

{t("usageScript.scriptHelp")}

{t("usageScript.configFormat")}
                      {`({
  request: {
    url: "{{baseUrl}}/api/usage",
    method: "POST",
    headers: {
      "Authorization": "Bearer {{apiKey}}",
      "User-Agent": "cc-switch/1.0"
    },
    body: JSON.stringify({ key: "value" })  // ${t("usageScript.commentOptional")}
  },
  extractor: function(response) {
    // ${t("usageScript.commentResponseIsJson")}
    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")}
)}
{/* Footer */} {/* Left side - Test and Format buttons */}
{/* Right side - Cancel and Save buttons */}
); }; export default UsageScriptModal;