import React, { useState } from "react"; import { Play, Wand2, Eye, EyeOff, Save } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { Provider, UsageScript, UsageData, createUsageScript } from "@/types"; import { usageApi, settingsApi, type AppId } from "@/lib/api"; import { copilotGetUsage, copilotGetUsageForAccount } from "@/lib/api/copilot"; import { useSettingsQuery } from "@/lib/query"; import { resolveManagedAccountId } from "@/lib/authBinding"; import { extractCodexBaseUrl, extractCodexExperimentalBearerToken, } from "@/utils/providerConfigUtils"; 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 { ConfirmDialog } from "@/components/ConfirmDialog"; import { cn } from "@/lib/utils"; import { TEMPLATE_TYPES, PROVIDER_TYPES } from "@/config/constants"; import { CODING_PLAN_PROVIDERS, detectCodingPlanProvider, } from "@/config/codingPlanProviders"; import { formatUsageDataSummary } from "@/utils/usageDisplay"; interface UsageScriptModalProps { provider: Provider; appId: AppId; isOpen: boolean; onClose: () => void; onSave: (script: UsageScript) => void; } // 生成预设模板的函数(支持国际化) const generatePresetTemplates = ( t: (key: string) => string, ): Record => ({ [TEMPLATE_TYPES.CUSTOM]: `({ request: { url: "", method: "GET", headers: {} }, extractor: function(response) { return { remaining: 0, unit: "USD" }; } })`, [TEMPLATE_TYPES.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_TYPES.NEW_API]: `({ request: { url: "{{baseUrl}}/api/user/self", method: "GET", headers: { "Content-Type": "application/json", "Authorization": "Bearer {{accessToken}}", "User-Agent": "cc-switch/1.0", "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")}" }; }, })`, // GitHub Copilot 模板不需要脚本,使用专用 API [TEMPLATE_TYPES.GITHUB_COPILOT]: "", // Coding Plan 模板不需要脚本,使用专用 Rust 查询 [TEMPLATE_TYPES.TOKEN_PLAN]: "", // 官方余额查询模板不需要脚本,使用专用 Rust 查询 [TEMPLATE_TYPES.BALANCE]: "", }); // 模板名称国际化键映射 const TEMPLATE_NAME_KEYS: Record = { [TEMPLATE_TYPES.CUSTOM]: "usageScript.templateCustom", [TEMPLATE_TYPES.GENERAL]: "usageScript.templateGeneral", [TEMPLATE_TYPES.NEW_API]: "usageScript.templateNewAPI", [TEMPLATE_TYPES.GITHUB_COPILOT]: "usageScript.templateCopilot", [TEMPLATE_TYPES.TOKEN_PLAN]: "usageScript.templateTokenPlan", [TEMPLATE_TYPES.BALANCE]: "usageScript.templateBalance", }; /** 官方余额查询供应商检测 */ const BALANCE_PROVIDERS = [ { id: "deepseek", label: "DeepSeek", pattern: /api\.deepseek\.com/i }, { id: "stepfun", label: "StepFun", pattern: /api\.stepfun\.(ai|com)/i }, { id: "siliconflow", label: "SiliconFlow", pattern: /api\.siliconflow\.(cn|com)/i, }, { id: "openrouter", label: "OpenRouter", pattern: /openrouter\.ai/i }, { id: "novita", label: "Novita AI", pattern: /api\.novita\.ai/i }, ] as const; /** 根据 Base URL 自动检测余额查询供应商 */ function detectBalanceProvider(baseUrl: string | undefined): boolean { if (!baseUrl) return false; return BALANCE_PROVIDERS.some((bp) => bp.pattern.test(baseUrl)); } const UsageScriptModal: React.FC = ({ provider, appId, isOpen, onClose, onSave, }) => { const { t } = useTranslation(); const queryClient = useQueryClient(); const { data: settingsData } = useSettingsQuery(); const [showUsageConfirm, setShowUsageConfirm] = useState(false); // 生成带国际化的预设模板 const PRESET_TEMPLATES = generatePresetTemplates(t); // 从 provider 的 settingsConfig 中提取 API Key 和 Base URL const getProviderCredentials = (): { apiKey: string | undefined; baseUrl: string | undefined; } => { const trimTrailingSlash = (url: string | undefined) => typeof url === "string" ? url.replace(/\/+$/, "") : url; const raw = ((): { apiKey: string | undefined; baseUrl: string | undefined; } => { try { const config = provider.settingsConfig; if (!config) return { apiKey: undefined, baseUrl: undefined }; // 处理不同应用的配置格式 if (appId === "claude" || appId === "claude-desktop") { // Claude / Claude Desktop: { env: { ANTHROPIC_AUTH_TOKEN | ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL } } // Key fallbacks mirror the backend resolver (Provider::resolve_usage_credentials). const env = (config as any).env || {}; return { apiKey: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || env.OPENROUTER_API_KEY || env.GOOGLE_API_KEY, baseUrl: env.ANTHROPIC_BASE_URL, }; } else if (appId === "codex") { // Codex: { auth: { OPENAI_API_KEY }, config: TOML string with base_url } const auth = (config as any).auth || {}; const configToml = (config as any).config || ""; const apiKey = typeof auth.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY.trim() ? auth.OPENAI_API_KEY : extractCodexExperimentalBearerToken(configToml); return { apiKey, baseUrl: extractCodexBaseUrl(configToml), }; } else if (appId === "gemini") { // Gemini: { env: { GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL } } // Key fallback mirrors the backend resolver (Provider::resolve_usage_credentials). const env = (config as any).env || {}; return { apiKey: env.GEMINI_API_KEY || env.GOOGLE_API_KEY, baseUrl: env.GOOGLE_GEMINI_BASE_URL, }; } else if (appId === "hermes") { // Hermes: settingsConfig 顶层扁平(snake_case,对应 config.yaml) return { apiKey: (config as any).api_key, baseUrl: (config as any).base_url, }; } else if (appId === "openclaw") { // OpenClaw: settingsConfig 顶层扁平(camelCase,对应 openclaw.json) return { apiKey: (config as any).apiKey, baseUrl: (config as any).baseUrl, }; } else if (appId === "opencode") { // OpenCode (OMO): 凭据嵌在 options.{baseURL, apiKey}(SDK options 对象) const options = (config as any).options || {}; return { apiKey: options.apiKey, baseUrl: options.baseURL, }; } return { apiKey: undefined, baseUrl: undefined }; } catch (error) { console.error("Failed to extract provider credentials:", error); return { apiKey: undefined, baseUrl: undefined }; } })(); // Trim the trailing slash to mirror the backend resolver // (Provider::resolve_usage_credentials), so `{{baseUrl}}/path` never // produces a double slash regardless of which path runs the query. return { apiKey: raw.apiKey, baseUrl: trimTrailingSlash(raw.baseUrl) }; }; const providerCredentials = getProviderCredentials(); const [script, setScript] = useState(() => { const savedScript = provider.meta?.usage_script; if (savedScript) { // 已有配置:如果是 coding_plan 但没有 codingPlanProvider,自动检测填充 if ( savedScript.templateType === TEMPLATE_TYPES.TOKEN_PLAN && !savedScript.codingPlanProvider ) { return { ...savedScript, codingPlanProvider: detectCodingPlanProvider(providerCredentials.baseUrl) || "kimi", }; } return savedScript; } const autoDetected = detectCodingPlanProvider(providerCredentials.baseUrl); if (autoDetected) { return createUsageScript({ codingPlanProvider: autoDetected }); } if (detectBalanceProvider(providerCredentials.baseUrl)) { return createUsageScript(); } return createUsageScript({ code: PRESET_TEMPLATES[TEMPLATE_TYPES.GENERAL], }); }); 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; // Copilot 供应商默认使用 Copilot 模板 if (provider.meta?.providerType === PROVIDER_TYPES.GITHUB_COPILOT) { return TEMPLATE_TYPES.GITHUB_COPILOT; } // 优先使用保存的 templateType if (existingScript?.templateType) { return existingScript.templateType as string; } // 向后兼容:根据字段推断模板类型 // 检测 NEW_API 模板(有 accessToken 或 userId) if (existingScript?.accessToken || existingScript?.userId) { return TEMPLATE_TYPES.NEW_API; } // 检测 GENERAL 模板(有 apiKey 或 baseUrl) if (existingScript?.apiKey || existingScript?.baseUrl) { return TEMPLATE_TYPES.GENERAL; } // 新配置:如果 URL 匹配 Coding Plan 供应商,自动选择 Coding Plan 模板 if (detectCodingPlanProvider(providerCredentials.baseUrl)) { return TEMPLATE_TYPES.TOKEN_PLAN; } // 新配置:如果 URL 匹配官方余额查询供应商,自动选择 Balance 模板 if (detectBalanceProvider(providerCredentials.baseUrl)) { return TEMPLATE_TYPES.BALANCE; } // 默认使用 GENERAL(与默认代码模板一致) return TEMPLATE_TYPES.GENERAL; }, ); const [showApiKey, setShowApiKey] = useState(false); const [showAccessToken, setShowAccessToken] = useState(false); const handleEnableToggle = (checked: boolean) => { if (checked && !settingsData?.usageConfirmed) { setShowUsageConfirm(true); } else { setScript({ ...script, enabled: checked }); } }; const handleUsageConfirm = async () => { setShowUsageConfirm(false); try { if (settingsData) { const { webdavSync: _, ...rest } = settingsData; await settingsApi.save({ ...rest, usageConfirmed: true }); await queryClient.invalidateQueries({ queryKey: ["settings"] }); } } catch (error) { console.error("Failed to save usage confirmed:", error); } setScript({ ...script, enabled: true }); }; const handleSave = () => { // Copilot、Coding Plan、Balance 模板不需要脚本验证 if ( selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT && selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN && selectedTemplate !== TEMPLATE_TYPES.BALANCE ) { 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; } } // 保存时记录当前选择的模板类型 const scriptWithTemplate = { ...script, templateType: selectedTemplate as | "custom" | "general" | "newapi" | "github_copilot" | "token_plan" | "balance" | undefined, }; onSave(scriptWithTemplate); onClose(); }; const handleTest = async () => { setTesting(true); try { // 官方余额查询模板使用专用 API if (selectedTemplate === TEMPLATE_TYPES.BALANCE) { const baseUrl = providerCredentials.baseUrl ?? ""; const apiKey = providerCredentials.apiKey ?? ""; const { subscriptionApi } = await import("@/lib/api/subscription"); const result = await subscriptionApi.getBalance(baseUrl, apiKey); if (result.success && result.data && result.data.length > 0) { const summary = result.data .map((d) => formatUsageDataSummary(d, { invalid: t("usage.invalid"), remaining: t("usage.remaining"), used: t("usage.used"), }), ) .join(", "); toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000, closeButton: true, }); queryClient.setQueryData(["usage", provider.id, appId], result); } else { toast.error( `${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`, { duration: 5000 }, ); } return; } // Coding Plan 模板使用专用 API if (selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN) { // ZenMux 使用用户在脚本配置中手动填入的 API Key 和 Base URL const isZenMux = script.codingPlanProvider === "zenmux"; const baseUrl = isZenMux ? (script.baseUrl ?? "") : (providerCredentials.baseUrl ?? ""); const apiKey = isZenMux ? (script.apiKey ?? "") : (providerCredentials.apiKey ?? ""); const { subscriptionApi } = await import("@/lib/api/subscription"); const quota = await subscriptionApi.getCodingPlanQuota(baseUrl, apiKey); if (quota.success && quota.tiers.length > 0) { const summary = quota.tiers .map((tier) => `${tier.name}: ${Math.round(tier.utilization)}%`) .join(", "); toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000, closeButton: true, }); // 将结果转换为 UsageResult 格式更新缓存 const usageData = quota.tiers.map((tier) => ({ planName: tier.name, remaining: 100 - tier.utilization, total: 100, used: tier.utilization, unit: "%", })); queryClient.setQueryData(["usage", provider.id, appId], { success: true, data: usageData, }); } else { toast.error( `${t("usageScript.testFailed")}: ${quota.error || t("endpointTest.noResult")}`, { duration: 5000 }, ); } return; } // Copilot 模板使用专用 API if (selectedTemplate === TEMPLATE_TYPES.GITHUB_COPILOT) { const accountId = resolveManagedAccountId( provider.meta, PROVIDER_TYPES.GITHUB_COPILOT, ); const usage = accountId ? await copilotGetUsageForAccount(accountId) : await copilotGetUsage(); const premium = usage.quota_snapshots.premium_interactions; const used = premium.entitlement - premium.remaining; const summary = `[${usage.copilot_plan}] ${t("usage.remaining")} ${premium.remaining}/${premium.entitlement} (${t("usageScript.resetDate")}: ${usage.quota_reset_date})`; toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000, closeButton: true, }); // 更新缓存 queryClient.setQueryData(["usage", provider.id, appId], { success: true, data: [ { planName: usage.copilot_plan, remaining: premium.remaining, total: premium.entitlement, used: used, unit: t("usageScript.premiumRequests"), }, ], }); return; } const result = await usageApi.testScript( provider.id, appId, script.code, script.timeout, script.apiKey, script.baseUrl, script.accessToken, script.userId, selectedTemplate as "custom" | "general" | "newapi" | undefined, ); if (result.success && result.data && result.data.length > 0) { const summary = result.data .map((plan: UsageData) => formatUsageDataSummary(plan, { invalid: t("usage.invalid"), remaining: t("usage.remaining"), used: t("usage.used"), }), ) .join(", "); toast.success(`${t("usageScript.testSuccess")}${summary}`, { duration: 3000, closeButton: true, }); // 🔧 测试成功后,更新主界面列表的用量查询缓存 queryClient.setQueryData(["usage", provider.id, appId], result); } 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, closeButton: true, }); } 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 !== undefined) { if (presetName === TEMPLATE_TYPES.CUSTOM) { // 🔧 自定义模式:用户应该在脚本中直接写完整 URL 和凭证,而不是依赖变量替换 // 这样可以避免同源检查导致的问题 // 如果用户想使用变量,需要手动在配置中设置 baseUrl/apiKey setScript({ ...script, code: preset, // 清除凭证,用户可选择手动输入或保持空 apiKey: undefined, baseUrl: undefined, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_TYPES.GENERAL) { setScript({ ...script, code: preset, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_TYPES.NEW_API) { setScript({ ...script, code: preset, apiKey: undefined, }); } else if (presetName === TEMPLATE_TYPES.GITHUB_COPILOT) { // Copilot 模板不需要脚本和凭证,使用专用 API setScript({ ...script, code: "", apiKey: undefined, baseUrl: undefined, accessToken: undefined, userId: undefined, }); } else if (presetName === TEMPLATE_TYPES.TOKEN_PLAN) { // Coding Plan 模板不需要脚本,使用 Rust 原生查询 const autoDetected = detectCodingPlanProvider( providerCredentials.baseUrl, ); const provider = script.codingPlanProvider || autoDetected || "kimi"; // ZenMux 允许手动填写 API Key 和 Base URL,不清除 const isZenMux = provider === "zenmux"; setScript({ ...script, code: "", apiKey: isZenMux ? script.apiKey : undefined, baseUrl: isZenMux ? script.baseUrl : undefined, accessToken: undefined, userId: undefined, codingPlanProvider: provider, }); } else if (presetName === TEMPLATE_TYPES.BALANCE) { // 官方余额查询模板不需要脚本,使用 Rust 原生查询 setScript({ ...script, code: "", apiKey: undefined, baseUrl: undefined, accessToken: undefined, userId: undefined, }); } setSelectedTemplate(presetName); } }; const shouldShowCredentialsConfig = selectedTemplate === TEMPLATE_TYPES.GENERAL || selectedTemplate === TEMPLATE_TYPES.NEW_API || (selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN && script.codingPlanProvider === "zenmux"); const footer = ( <>
); return (

{t("usageScript.enableUsageQuery")}

{script.enabled && (
{/* 预设模板选择 */}
{Object.keys(PRESET_TEMPLATES) .filter((name) => { const isCopilotProvider = provider.meta?.providerType === "github_copilot"; // Copilot 供应商只显示 copilot 模板 if (isCopilotProvider) { return name === TEMPLATE_TYPES.GITHUB_COPILOT; } // 非 Copilot 供应商不显示 copilot 模板 return name !== TEMPLATE_TYPES.GITHUB_COPILOT; }) .map((name) => { const isSelected = selectedTemplate === name; return ( ); })}
{/* 自定义模式:变量提示和具体值 */} {selectedTemplate === TEMPLATE_TYPES.CUSTOM && (

{t("usageScript.supportedVariables")}

{/* baseUrl */}
{"{{baseUrl}}"} = {providerCredentials.baseUrl ? ( {providerCredentials.baseUrl} ) : ( {t("common.notSet") || "未设置"} )}
{/* apiKey */}
{"{{apiKey}}"} = {providerCredentials.apiKey ? ( <> {showApiKey ? ( {providerCredentials.apiKey} ) : ( •••••••• )} ) : ( {t("common.notSet") || "未设置"} )}
)} {/* Copilot 模式:自动认证提示 */} {selectedTemplate === TEMPLATE_TYPES.GITHUB_COPILOT && (

{t("usageScript.copilotAutoAuth")}

)} {/* 官方余额查询模式:自动提示 */} {selectedTemplate === TEMPLATE_TYPES.BALANCE && (

{t("usageScript.balanceHint")}

{BALANCE_PROVIDERS.filter((bp) => bp.pattern.test(providerCredentials.baseUrl || ""), ).map((bp) => ( {bp.label} ))}
)} {/* Coding Plan 模式:供应商选择 */} {selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN && (

{t("usageScript.tokenPlanHint")}

{CODING_PLAN_PROVIDERS.map((cp) => ( ))}
)} {/* 凭证配置 */} {shouldShowCredentialsConfig && (

{t("usageScript.credentialsConfig")}

{t("usageScript.credentialsHint")}

{selectedTemplate === TEMPLATE_TYPES.GENERAL && ( <>
setScript({ ...script, apiKey: e.target.value }) } placeholder={t("usageScript.apiKeyPlaceholder")} autoComplete="off" className="border-white/10" /> {script.apiKey && ( )}
setScript({ ...script, baseUrl: e.target.value }) } placeholder={t("usageScript.baseUrlPlaceholder")} autoComplete="off" className="border-white/10" />
)} {selectedTemplate === TEMPLATE_TYPES.NEW_API && ( <>
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.newapi.com" autoComplete="off" className="border-white/10" />
setScript({ ...script, accessToken: e.target.value, }) } placeholder={t( "usageScript.accessTokenPlaceholder", )} autoComplete="off" className="border-white/10" /> {script.accessToken && ( )}
setScript({ ...script, userId: e.target.value }) } placeholder={t("usageScript.userIdPlaceholder")} autoComplete="off" className="border-white/10" />
)} {selectedTemplate === TEMPLATE_TYPES.TOKEN_PLAN && script.codingPlanProvider === "zenmux" && ( <>
setScript({ ...script, baseUrl: e.target.value }) } placeholder="https://api.zenmux.com/v1/..." autoComplete="off" className="border-white/10" />
setScript({ ...script, apiKey: e.target.value, }) } placeholder="sk-..." autoComplete="off" className="border-white/10" /> {script.apiKey && ( )}
)}
)} {/* 通用配置(始终显示) */}
{/* 超时时间 */}
setScript({ ...script, timeout: e.target.value === "" ? ("" as unknown as number) : Number(e.target.value), }) } onBlur={(e) => setScript({ ...script, timeout: validateTimeout(e.target.value), }) } className="border-white/10" />
{/* 自动查询间隔 */}
setScript({ ...script, autoQueryInterval: e.target.value === "" ? ("" as unknown as number) : Number(e.target.value), }) } onBlur={(e) => setScript({ ...script, autoQueryInterval: validateAndClampInterval( e.target.value, ), }) } className="border-white/10" />
{/* 提取器代码 - Copilot 模板不需要 */} {selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT && selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN && (
{t("usageScript.extractorHint")}
setScript((prev) => ({ ...prev, code: value })) } height={480} language="javascript" showMinimap={false} />
)} {/* 帮助信息 - Copilot 模板不需要 */} {selectedTemplate !== TEMPLATE_TYPES.GITHUB_COPILOT && selectedTemplate !== TEMPLATE_TYPES.TOKEN_PLAN && (

{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")}
)}
)} void handleUsageConfirm()} onCancel={() => setShowUsageConfirm(false)} />
); }; export default UsageScriptModal;