Greasy Fork

Greasy Fork is available in English.

CPA to sub2api 迁移

在 CPA 和 sub2api 页面提供手动 JSON 中间态导入导出工具

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         CPA to sub2api 迁移
// @namespace    cpa-sub2api-local
// @version      1.3.2
// @description  在 CPA 和 sub2api 页面提供手动 JSON 中间态导入导出工具
// @match        *://*/*
// @license MIT
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
  'use strict';

  var STORAGE_KEY = 'cpa_sub2api_bridge_settings_v1';
  var EXPORT_CACHE_KEY = 'last_sub2api_export_text';
  var CPA_AUTH_DOWNLOAD_BATCH_SIZE = 50;
  var state = {
    outputText: '',
    cpaFiles: []
  };

  function defaultCpaBaseUrl() {
    return window.location.origin;
  }

  function defaultSub2apiBaseUrl() {
    return window.location.origin + '/api/v1';
  }

  function normalizeBaseUrl(value) {
    return String(value || '').trim().replace(/\/+$/, '');
  }

  function normalizeCpaBaseUrl(value) {
    var base = normalizeBaseUrl(value);
    if (!base) return defaultCpaBaseUrl();
    return base.replace(/\/?v0\/management\/?$/i, '');
  }

  function cpaManagementUrl(settings, path) {
    return normalizeCpaBaseUrl(settings.cpaBaseUrl) + '/v0/management' + path;
  }

  function normalizeSavedSettings(saved) {
    var settings = Object.assign({
      cpaBaseUrl: defaultCpaBaseUrl(),
      cpaManagementKey: '',
      sub2apiBaseUrl: defaultSub2apiBaseUrl(),
      sub2apiToken: localStorage.getItem('auth_token') || '',
      skipDefaultGroupBind: true
    }, saved || {});

    if (!settings.cpaBaseUrl || settings.cpaBaseUrl === 'http://127.0.0.1:8317/v0/management') {
      settings.cpaBaseUrl = defaultCpaBaseUrl();
    } else {
      settings.cpaBaseUrl = normalizeCpaBaseUrl(settings.cpaBaseUrl);
    }
    if (!settings.sub2apiBaseUrl) {
      settings.sub2apiBaseUrl = defaultSub2apiBaseUrl();
    }
    return settings;
  }

  function loadSettings() {
    try {
      return normalizeSavedSettings(JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'));
    } catch (error) {
      return normalizeSavedSettings({});
    }
  }

  function saveSettings(settings) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
  }

  function getSettingsFromForm() {
    var settings = {
      cpaBaseUrl: normalizeCpaBaseUrl(document.getElementById('cpa-sub2api-cpa-base').value),
      cpaManagementKey: document.getElementById('cpa-sub2api-cpa-key').value.trim(),
      sub2apiBaseUrl: document.getElementById('cpa-sub2api-sub-base').value.trim().replace(/\/+$/, ''),
      sub2apiToken: document.getElementById('cpa-sub2api-sub-token').value.trim(),
      skipDefaultGroupBind: document.getElementById('cpa-sub2api-skip-group').checked
    };
    saveSettings(settings);
    return settings;
  }

  function applySettingsToForm() {
    var settings = loadSettings();
    document.getElementById('cpa-sub2api-cpa-base').value = settings.cpaBaseUrl;
    document.getElementById('cpa-sub2api-cpa-key').value = settings.cpaManagementKey;
    document.getElementById('cpa-sub2api-sub-base').value = settings.sub2apiBaseUrl;
    document.getElementById('cpa-sub2api-sub-token').value = settings.sub2apiToken || localStorage.getItem('auth_token') || '';
    document.getElementById('cpa-sub2api-skip-group').checked = settings.skipDefaultGroupBind !== false;
  }

  function decodeJwtPayload(token) {
    try {
      var parts = String(token || '').split('.');
      if (parts.length !== 3) return {};
      var payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
      var padding = payload.length % 4;
      if (padding) payload += '='.repeat(4 - padding);
      var jsonText = decodeURIComponent(
        atob(payload)
          .split('')
          .map(function (char) {
            return '%' + char.charCodeAt(0).toString(16).padStart(2, '0');
          })
          .join('')
      );
      return JSON.parse(jsonText);
    } catch (error) {
      return {};
    }
  }

  function parseExpiredTime(expiredString) {
    try {
      if (!expiredString) return 0;
      var date = String(expiredString).includes('+')
        ? new Date(expiredString)
        : new Date(String(expiredString).replace('Z', '+00:00'));
      var timestamp = Math.floor(date.getTime() / 1000);
      return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : 0;
    } catch (error) {
      return 0;
    }
  }

  function buildAccount(sourceData, index) {
    var accessPayload = decodeJwtPayload(sourceData.access_token || '');
    var authInfo = accessPayload['https://api.openai.com/auth'] || {};
    var idTokenPayload = decodeJwtPayload(sourceData.id_token || '');
    var idTokenAuth = idTokenPayload['https://api.openai.com/auth'] || {};
    var organizations = Array.isArray(idTokenAuth.organizations) ? idTokenAuth.organizations : [];
    var expiresAt = parseExpiredTime(sourceData.expired || '') || Number(accessPayload.exp || 0);
    var accountType = sourceData.type || 'unknown';

    return {
      name: accountType + '-普号-' + String(index).padStart(4, '0'),
      platform: 'openai',
      type: 'oauth',
      credentials: {
        access_token: sourceData.access_token || '',
        chatgpt_account_id: sourceData.account_id || '',
        chatgpt_user_id: authInfo.chatgpt_user_id || '',
        expires_at: expiresAt,
        expires_in: 864000,
        organization_id: organizations.length ? organizations[0].id || '' : '',
        refresh_token: sourceData.refresh_token || ''
      },
      extra: {
        email: sourceData.email || ''
      },
      concurrency: 10,
      priority: 1,
      rate_multiplier: 1,
      auto_pause_on_expired: true
    };
  }

  function normalizeCpaInput(data) {
    if (Array.isArray(data)) return data;
    if (data && Array.isArray(data.accounts)) return data.accounts;
    if (data && typeof data === 'object') return [data];
    return [];
  }

  function convertBatch(items) {
    var accounts = [];
    var issues = [];

    normalizeCpaInput(items).forEach(function (sourceData, index) {
      if (!sourceData || typeof sourceData !== 'object' || Array.isArray(sourceData)) {
        issues.push('第 ' + (index + 1) + ' 项不是对象');
        return;
      }

      if (!sourceData.access_token || !sourceData.account_id) {
        issues.push('第 ' + (index + 1) + ' 项缺少 access_token 或 account_id');
        return;
      }

      accounts.push(buildAccount(sourceData, accounts.length + 1));
    });

    return {
      output: {
        exported_at: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
        proxies: [],
        accounts: accounts
      },
      issues: issues
    };
  }

  function parseJson(text) {
    var trimmed = String(text || '').trim();
    if (!trimmed) throw new Error('请输入或导入 JSON');
    return JSON.parse(trimmed);
  }

  function formatJson(data) {
    return JSON.stringify(data, null, 2);
  }

  function isSub2apiPayload(data) {
    return Boolean(
      data &&
        typeof data === 'object' &&
        !Array.isArray(data) &&
        Array.isArray(data.accounts) &&
        Array.isArray(data.proxies)
    );
  }

  function extractSub2apiPayload(data) {
    if (isSub2apiPayload(data)) return data;
    if (data && typeof data === 'object' && data.code === 0 && isSub2apiPayload(data.data)) return data.data;
    if (data && typeof data === 'object' && data.data && isSub2apiPayload(data.data.data)) return data.data.data;
    if (data && typeof data === 'object' && isSub2apiPayload(data.data)) return data.data;
    return null;
  }

  function persistExportText(text) {
    try {
      localStorage.setItem(EXPORT_CACHE_KEY, text);
    } catch (error) {}
    try {
      if (typeof GM_setValue === 'function') GM_setValue(EXPORT_CACHE_KEY, text);
    } catch (error) {}
  }

  function readPersistedExportText() {
    try {
      if (typeof GM_getValue === 'function') {
        var gmValue = GM_getValue(EXPORT_CACHE_KEY, '');
        if (gmValue) return Promise.resolve(gmValue);
      }
    } catch (error) {}
    try {
      return Promise.resolve(localStorage.getItem(EXPORT_CACHE_KEY) || '');
    } catch (error) {
      return Promise.resolve('');
    }
  }

  function gmFetch(url, options) {
    return new Promise(function (resolve, reject) {
      var requestOptions = options || {};
      var headers = requestOptions.headers || {};
      GM_xmlhttpRequest({
        method: requestOptions.method || 'GET',
        url: url,
        headers: headers,
        data: requestOptions.body,
        responseType: 'text',
        onload: function (result) {
          resolve({
            ok: result.status >= 200 && result.status < 300,
            status: result.status,
            statusText: result.statusText || '',
            text: function () { return Promise.resolve(result.responseText || ''); }
          });
        },
        onerror: function () {
          reject(new Error('网络请求失败'));
        },
        ontimeout: function () {
          reject(new Error('网络请求超时'));
        }
      });
    });
  }

  function smartFetch(url, options) {
    var nativeFetch = window.fetch ? window.fetch.bind(window) : fetch;
    return nativeFetch(url, options).catch(function (error) {
      if (typeof GM_xmlhttpRequest === 'function') return gmFetch(url, options);
      throw error;
    });
  }

  function normalizeApiResponse(payload) {
    if (payload && typeof payload === 'object' && payload.code === 0 && 'data' in payload) {
      return payload.data;
    }
    return payload;
  }

  function maskHeaderValue(key, value) {
    if (!value) return value;
    if (/authorization|management-key/i.test(key)) {
      var text = String(value);
      return text.length > 18 ? text.slice(0, 12) + '...' + text.slice(-4) : '***';
    }
    return value;
  }

  function buildRequestDebug(method, url, options, response, responseText) {
    var headers = options && options.headers ? options.headers : {};
    var safeHeaders = {};
    Object.keys(headers).forEach(function (key) {
      safeHeaders[key] = maskHeaderValue(key, headers[key]);
    });
    return [
      'Request:',
      method + ' ' + url,
      'Headers: ' + JSON.stringify(safeHeaders),
      options && options.body
        ? 'Body: ' + (typeof FormData !== 'undefined' && options.body instanceof FormData ? '[FormData] ' + Array.from(options.body.keys()).join(', ') : String(options.body).slice(0, 4000))
        : 'Body: <empty>',
      'Response:',
      'HTTP ' + response.status + ' ' + response.statusText,
      responseText || '<empty>'
    ].join('\n');
  }

  function attachDebug(error, debugText) {
    error.debugText = debugText;
    return error;
  }

  function requestJson(url, options) {
    var requestOptions = options || {};
    var method = requestOptions.method || 'GET';
    return smartFetch(url, requestOptions).then(function (response) {
      return response.text().then(function (text) {
        var data = null;
        try {
          data = text ? JSON.parse(text) : null;
        } catch (error) {
          data = null;
        }
        if (!response.ok) {
          var message = data && (data.message || data.error || data.detail);
          var detail = message || text || response.statusText || '请求失败';
          throw attachDebug(
            new Error('HTTP ' + response.status + ' ' + detail),
            buildRequestDebug(method, url, requestOptions, response, text)
          );
        }
        return normalizeApiResponse(data);
      });
    });
  }

  function getCpaHeaders(settings) {
    var headers = {};
    if (settings.cpaManagementKey) {
      headers.Authorization = 'Bearer ' + settings.cpaManagementKey;
      headers['X-Management-Key'] = settings.cpaManagementKey;
    }
    return headers;
  }

  function getSub2apiHeaders(settings, includeJson) {
    var headers = {};
    if (includeJson) headers['Content-Type'] = 'application/json';
    if (settings.sub2apiToken) headers.Authorization = 'Bearer ' + settings.sub2apiToken;
    return headers;
  }

  function downloadText(filename, text) {
    var blob = new Blob([text], { type: 'application/json;charset=utf-8' });
    openBlob(filename, blob);
  }

  function openBlob(filename, blob) {
    var url = URL.createObjectURL(blob);
    var opened = window.open(url, '_blank', 'noopener,noreferrer');
    if (!opened) {
      var anchor = document.createElement('a');
      anchor.href = url;
      anchor.download = filename;
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
    }
    setTimeout(function () { URL.revokeObjectURL(url); }, 60000);
  }

  function copyText(text) {
    if (typeof GM_setClipboard === 'function') {
      GM_setClipboard(text, 'text');
      return Promise.resolve();
    }
    return navigator.clipboard.writeText(text);
  }

  function crc32(bytes) {
    var crc = -1;
    for (var i = 0; i < bytes.length; i += 1) {
      crc ^= bytes[i];
      for (var j = 0; j < 8; j += 1) {
        crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
      }
    }
    return (crc ^ -1) >>> 0;
  }

  function uint16(value) {
    return [value & 255, (value >>> 8) & 255];
  }

  function uint32(value) {
    return [value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255];
  }

  function dosDateTime(date) {
    var year = Math.max(1980, date.getFullYear());
    return {
      time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
      date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()
    };
  }

  function concatUint8(parts) {
    var total = parts.reduce(function (sum, part) { return sum + part.length; }, 0);
    var out = new Uint8Array(total);
    var offset = 0;
    parts.forEach(function (part) {
      out.set(part, offset);
      offset += part.length;
    });
    return out;
  }

  function bytesFromArray(values) {
    return new Uint8Array(values);
  }

  function createZip(files) {
    var encoder = new TextEncoder();
    var now = dosDateTime(new Date());
    var localParts = [];
    var centralParts = [];
    var offset = 0;

    files.forEach(function (file) {
      var nameBytes = encoder.encode(file.name);
      var dataBytes = typeof file.content === 'string' ? encoder.encode(file.content) : file.content;
      var checksum = crc32(dataBytes);
      var localHeader = bytesFromArray([].concat(
        uint32(0x04034b50), uint16(20), uint16(0x0800), uint16(0), uint16(now.time), uint16(now.date),
        uint32(checksum), uint32(dataBytes.length), uint32(dataBytes.length), uint16(nameBytes.length), uint16(0)
      ));
      localParts.push(localHeader, nameBytes, dataBytes);

      var centralHeader = bytesFromArray([].concat(
        uint32(0x02014b50), uint16(20), uint16(20), uint16(0x0800), uint16(0), uint16(now.time), uint16(now.date),
        uint32(checksum), uint32(dataBytes.length), uint32(dataBytes.length), uint16(nameBytes.length), uint16(0), uint16(0),
        uint16(0), uint16(0), uint32(0), uint32(offset)
      ));
      centralParts.push(centralHeader, nameBytes);
      offset += localHeader.length + nameBytes.length + dataBytes.length;
    });

    var centralSize = centralParts.reduce(function (sum, part) { return sum + part.length; }, 0);
    var endRecord = bytesFromArray([].concat(
      uint32(0x06054b50), uint16(0), uint16(0), uint16(files.length), uint16(files.length),
      uint32(centralSize), uint32(offset), uint16(0)
    ));
    return new Blob([concatUint8(localParts.concat(centralParts, [endRecord]))], { type: 'application/zip' });
  }

  function setStatus(message, type, debugText) {
    var status = document.getElementById('cpa-sub2api-status');
    if (status) {
      status.textContent = message || '';
      status.className = type || '';
      status.id = 'cpa-sub2api-status';
    }
    var detail = document.getElementById('cpa-sub2api-error-detail');
    if (detail) {
      detail.textContent = type === 'error' && message ? '调用错误:' + message : '';
      detail.className = type === 'error' && message ? 'error' : '';
    }
    var debugBox = document.getElementById('cpa-sub2api-debug-block');
    var debugPre = document.getElementById('cpa-sub2api-debug-text');
    if (debugBox && debugPre) {
      if (type === 'error' && debugText) {
        debugBox.style.display = 'block';
        debugBox.open = false;
        debugPre.textContent = debugText;
      } else {
        debugBox.style.display = 'none';
        debugPre.textContent = '';
      }
    }
  }

  function errorMessage(error, fallback) {
    return error instanceof Error ? error.message : fallback;
  }

  function errorDebug(error) {
    return error && typeof error === 'object' && error.debugText ? error.debugText : '';
  }

  function updateDataBadge(data) {
    var badge = document.getElementById('cpa-sub2api-data-badge');
    if (!badge) return;
    var payload = data;
    if (typeof payload === 'string') {
      try {
        payload = JSON.parse(payload);
      } catch (error) {
        payload = null;
      }
    }
    payload = extractSub2apiPayload(payload) || payload;
    var accounts = payload && Array.isArray(payload.accounts) ? payload.accounts.length : 0;
    var proxies = payload && Array.isArray(payload.proxies) ? payload.proxies.length : 0;
    badge.textContent = '当前数据:' + accounts + ' 个账号 / ' + proxies + ' 个代理';
  }

  function updateExportProgressBadge(done, total) {
    var badge = document.getElementById('cpa-sub2api-data-badge');
    if (!badge) return;
    badge.textContent = '当前数据:正在导出 ' + done + ' / ' + total + ' 个账号 / 0 个代理';
  }

  function setOutput(data, message) {
    state.outputText = typeof data === 'string' ? data : formatJson(data);
    var output = document.getElementById('cpa-sub2api-output');
    if (output) output.value = state.outputText;
    updateDataBadge(data);
    setStatus(message || '已生成 sub2api 数据', 'success');
  }

  function renderCpaFileOptions(files) {
    state.cpaFiles = Array.isArray(files) ? files : [];
  }

  function handleConvert() {
    try {
      var input = parseJson(document.getElementById('cpa-sub2api-input').value);
      var payload = extractSub2apiPayload(input);
      if (payload) {
        setOutput(payload, '已识别为 sub2api 数据');
        return;
      }

      var result = convertBatch(input);
      setOutput(result.output, '转换完成:' + result.output.accounts.length + ' 个账号,' + result.issues.length + ' 个跳过');
      if (result.issues.length) {
        setStatus('转换完成,但有跳过项:' + result.issues.join(';'), 'error');
      }
    } catch (error) {
      setStatus(error instanceof Error ? error.message : 'JSON 处理失败', 'error');
    }
  }

  function handleLoadIntermediate() {
    try {
      var input = parseJson(document.getElementById('cpa-sub2api-input').value);
      var payload = extractSub2apiPayload(input);
      if (!payload) {
        throw new Error('这不是 sub2api 数据:需要包含 accounts 和 proxies 数组');
      }
      setOutput(payload, 'sub2api 数据已载入并格式化');
    } catch (error) {
      setStatus(error instanceof Error ? error.message : '载入失败', 'error');
    }
  }

  function handleCopy() {
    var text = state.outputText || document.getElementById('cpa-sub2api-output').value;
    if (!text.trim()) {
      setStatus('没有可复制的输出内容', 'error');
      return;
    }
    copyText(text).then(
      function () { setStatus('已复制到剪贴板', 'success'); },
      function () { setStatus('复制失败,请手动选择输出框内容复制', 'error'); }
    );
  }

  function handleDownload() {
    var text = state.outputText || document.getElementById('cpa-sub2api-output').value;
    if (!text.trim()) {
      setStatus('没有可下载的输出内容', 'error');
      return;
    }
    var stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
    downloadText('sub2api-bridge-' + stamp + '.json', text);
    setStatus('已开始下载 JSON', 'success');
  }

  function handleFile(event) {
    var file = event.target.files && event.target.files[0];
    if (!file) return;
    var reader = new FileReader();
    reader.onload = function () {
      document.getElementById('cpa-sub2api-input').value = String(reader.result || '');
      setStatus('已导入:' + file.name, 'success');
    };
    reader.onerror = function () {
      setStatus('文件读取失败', 'error');
    };
    reader.readAsText(file, 'utf-8');
  }

  function copyExportToClipboard(text) {
    persistExportText(text);
    return copyText(text).then(
      function () {
        setStatus('导出完成,已自动复制到剪贴板', 'success');
      },
      function () {
        setStatus('导出完成,但浏览器阻止自动复制;请点击“复制输出”手动复制', 'error');
      }
    );
  }

  function handleQuickExport() {
    var settings = getSettingsFromForm();
    if (!settings.cpaBaseUrl) {
      setStatus('请填写 CPA API Base', 'error');
      return;
    }
    setStatus('正在导出 CPA auth-files...', '');
    requestJson(cpaManagementUrl(settings, '/auth-files'), {
      method: 'GET',
      headers: getCpaHeaders(settings)
    }).then(function (data) {
      var files = data && Array.isArray(data.files) ? data.files : [];
      var names = files.map(function (file) { return file && file.name; }).filter(Boolean);
      renderCpaFileOptions(files);
      if (!names.length) throw new Error('CPA 没有可导出的 auth JSON');
      var downloadedCount = 0;
      updateExportProgressBadge(downloadedCount, names.length);
      return mapInBatches(names, CPA_AUTH_DOWNLOAD_BATCH_SIZE, function (name) {
        return downloadCpaAuthFile(settings, name).then(function (json) {
          downloadedCount += 1;
          updateExportProgressBadge(downloadedCount, names.length);
          return json;
        });
      });
    }).then(function (items) {
      var inputBox = document.getElementById('cpa-sub2api-input');
      if (inputBox) inputBox.value = formatJson(items);
      var result = convertBatch(items);
      setOutput(result.output, '导出完成:' + result.output.accounts.length + ' 个账号');
      copyExportToClipboard(state.outputText);
      if (result.issues.length) setStatus('导出完成,但有跳过项:' + result.issues.join(';'), 'error');
    }).catch(function (error) {
      setStatus(errorMessage(error, '导出失败'), 'error', errorDebug(error));
    });
  }

  function handleListCpaFiles() {
    var settings = getSettingsFromForm();
    if (!settings.cpaBaseUrl) {
      setStatus('请填写 CPA 管理 API 地址', 'error');
      return;
    }
    setStatus('正在读取 CPA auth-files...', '');
    requestJson(cpaManagementUrl(settings, '/auth-files'), {
      method: 'GET',
      headers: getCpaHeaders(settings)
    }).then(function (data) {
      var files = data && Array.isArray(data.files) ? data.files : [];
      renderCpaFileOptions(files);
      setStatus('已读取 ' + files.length + ' 个 CPA auth 文件', 'success');
    }).catch(function (error) {
      setStatus(errorMessage(error, '读取 CPA auth-files 失败'), 'error', errorDebug(error));
    });
  }

  function downloadCpaAuthFile(settings, name) {
    var url = cpaManagementUrl(settings, '/auth-files/download?name=') + encodeURIComponent(name);
    var options = {
      method: 'GET',
      headers: getCpaHeaders(settings)
    };
    return smartFetch(url, options).then(function (response) {
      return response.text().then(function (text) {
        if (!response.ok) {
          throw attachDebug(
            new Error(text || '下载 ' + name + ' 失败:HTTP ' + response.status),
            buildRequestDebug(options.method, url, options, response, text)
          );
        }
        return JSON.parse(text);
      });
    });
  }

  function mapInBatches(items, batchSize, mapper) {
    var results = new Array(items.length);
    var nextIndex = 0;
    var activeCount = 0;

    return new Promise(function (resolve, reject) {
      function runNext() {
        if (nextIndex >= items.length && activeCount === 0) {
          resolve(results);
          return;
        }
        while (activeCount < batchSize && nextIndex < items.length) {
          (function (currentIndex) {
            nextIndex += 1;
            activeCount += 1;
            Promise.resolve(mapper(items[currentIndex], currentIndex)).then(function (result) {
              results[currentIndex] = result;
              activeCount -= 1;
              runNext();
            }, reject);
          })(nextIndex);
        }
      }
      runNext();
    });
  }

  function handleDownloadAllCpa() {
    var settings = getSettingsFromForm();
    setStatus('正在下载全部 CPA 认证文件并生成 sub2api 数据...', '');
    requestJson(cpaManagementUrl(settings, '/auth-files'), {
      method: 'GET',
      headers: getCpaHeaders(settings)
    }).then(function (data) {
      var files = data && Array.isArray(data.files) ? data.files : [];
      var names = files.map(function (file) { return file && file.name; }).filter(Boolean);
      if (!names.length) throw new Error('CPA 没有可下载的 auth JSON');
      var downloadedCount = 0;
      updateExportProgressBadge(downloadedCount, names.length);
      return mapInBatches(names, CPA_AUTH_DOWNLOAD_BATCH_SIZE, function (name) {
        return downloadCpaAuthFile(settings, name).then(function (json) {
          downloadedCount += 1;
          updateExportProgressBadge(downloadedCount, names.length);
          return { name: name, json: json };
        });
      });
    }).then(function (items) {
      var cpaItems = items.map(function (item) { return item.json; });
      var inputBox = document.getElementById('cpa-sub2api-input');
      if (inputBox) inputBox.value = formatJson(cpaItems);
      var result = convertBatch(cpaItems);
      setOutput(result.output, '全部下载完成:' + result.output.accounts.length + ' 个账号');
      var zipFiles = items.map(function (item) {
        return { name: 'cpa-auth/' + item.name, content: formatJson(item.json) };
      });
      zipFiles.push({ name: 'sub2api/sub2api-data.json', content: state.outputText });
      var stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
      openBlob('cpa-sub2api-export-' + stamp + '.zip', createZip(zipFiles));
    }).catch(function (error) {
      setStatus(errorMessage(error, '全部下载失败'), 'error', errorDebug(error));
    });
  }

  function handleUploadCpaAuth() {
    var settings = getSettingsFromForm();
    var fileInput = document.getElementById('cpa-sub2api-cpa-upload');
    var files = Array.from(fileInput.files || []);
    if (!files.length) {
      setStatus('请选择要上传到 CPA 的 JSON 文件', 'error');
      return;
    }
    var formData = new FormData();
    files.forEach(function (file) { formData.append('file', file, file.name); });
    setStatus('正在上传 CPA auth 文件...', '');
    var uploadUrl = cpaManagementUrl(settings, '/auth-files');
    var uploadOptions = {
      method: 'POST',
      headers: getCpaHeaders(settings),
      body: formData
    };
    smartFetch(uploadUrl, uploadOptions).then(function (response) {
      return response.text().then(function (text) {
        var data = {};
        try {
          data = text ? JSON.parse(text) : {};
        } catch (error) {
          data = {};
        }
        if (!response.ok) {
          throw attachDebug(
            new Error(data.error || data.message || '上传失败:HTTP ' + response.status),
            buildRequestDebug(uploadOptions.method, uploadUrl, uploadOptions, response, text)
          );
        }
        setStatus('CPA auth 文件上传完成', 'success');
        handleListCpaFiles();
      });
    }).catch(function (error) {
      setStatus(errorMessage(error, '上传 CPA auth 文件失败'), 'error', errorDebug(error));
    });
  }

  function resolveImportText() {
    var fallbackText = function () {
      return state.outputText || (document.getElementById('cpa-sub2api-output') || {}).value || '';
    };
    var fromClipboard = navigator.clipboard && navigator.clipboard.readText
      ? navigator.clipboard.readText().catch(function () { return ''; })
      : Promise.resolve('');
    return fromClipboard.then(function (text) {
      var trimmed = String(text || '').trim();
      if (trimmed) return trimmed;
      return readPersistedExportText().then(function (cachedText) {
        return String(cachedText || '').trim() || fallbackText();
      });
    });
  }

  function importToSub2apiWithText(settings, text) {
    var payload;
    try {
      payload = parseJson(text);
      var extractedPayload = extractSub2apiPayload(payload);
      if (extractedPayload) {
        payload = extractedPayload;
      } else {
        var result = convertBatch(payload);
        payload = result.output;
      }
    } catch (error) {
      setStatus(error instanceof Error ? error.message : '导入前 JSON 解析失败', 'error');
      return;
    }
    if (!settings.sub2apiBaseUrl) {
      setStatus('请填写 sub2api API 地址', 'error');
      return;
    }
    setStatus('正在导入到 sub2api...', '');
    requestJson(settings.sub2apiBaseUrl + '/admin/accounts/data', {
      method: 'POST',
      headers: getSub2apiHeaders(settings, true),
      body: JSON.stringify({
        data: payload,
        skip_default_group_bind: settings.skipDefaultGroupBind
      })
    }).then(function (data) {
      updateDataBadge(payload);
      setStatus('sub2api 导入完成:' + formatImportResult(data), 'success');
    }).catch(function (error) {
      setStatus(errorMessage(error, 'sub2api 导入失败'), 'error', errorDebug(error));
    });
  }

  function handleImportToSub2api() {
    var settings = getSettingsFromForm();
    resolveImportText().then(function (text) {
      importToSub2apiWithText(settings, text);
    });
  }

  function formatImportResult(data) {
    if (!data || typeof data !== 'object') return '已提交';
    return [
      '账号创建 ' + Number(data.account_created || 0),
      '账号失败 ' + Number(data.account_failed || 0),
      '代理创建 ' + Number(data.proxy_created || 0),
      '代理复用 ' + Number(data.proxy_reused || 0),
      '代理失败 ' + Number(data.proxy_failed || 0)
    ].join(',');
  }

  function createStyle() {
    var style = document.createElement('style');
    style.textContent = [
      '#cpa-sub2api-bridge{position:fixed;right:18px;bottom:18px;z-index:2147483647;font-family:Arial,"Microsoft YaHei",sans-serif;color:#172033}',
      '#cpa-sub2api-bridge *{box-sizing:border-box}',
      '#cpa-sub2api-toggle{border:0;border-radius:999px;background:#2454ff;color:#fff;padding:10px 14px;font-size:13px;font-weight:700;box-shadow:0 10px 28px rgba(36,84,255,.35);cursor:pointer}',
      '#cpa-sub2api-panel{display:none;width:520px;max-width:calc(100vw - 36px);max-height:calc(100vh - 90px);overflow:auto;margin-bottom:10px;padding:14px;border:1px solid #d8deea;border-radius:16px;background:#fff;box-shadow:0 18px 48px rgba(15,23,42,.22)}',
      '#cpa-sub2api-bridge.open #cpa-sub2api-panel{display:block}',
      '#cpa-sub2api-panel h2{margin:0 0 6px;font-size:16px}',
      '#cpa-sub2api-panel h3{margin:12px 0 8px;font-size:13px}',
      '#cpa-sub2api-panel p{margin:0 0 10px;color:#5c667a;font-size:12px;line-height:1.5}',
      '#cpa-sub2api-status{margin:8px 0 10px;padding:8px 10px;border-radius:10px;background:#f8fafc;color:#475569;font-size:13px;line-height:1.45;min-height:18px}',
      '#cpa-sub2api-status.error{background:#fef2f2;color:#dc2626}',
      '#cpa-sub2api-status.success{background:#f0fdf4;color:#15803d}',
      '#cpa-sub2api-primary-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:12px 0}',
      '#cpa-sub2api-primary-actions button{font-size:18px!important;padding:16px 12px!important;border-radius:14px!important}',
      '#cpa-sub2api-data-badge{display:block;margin:8px 0 6px;padding:8px 10px;border-radius:10px;background:#eef2ff;color:#26336f;font-size:13px;font-weight:700;text-align:center}',
      '#cpa-sub2api-error-detail{display:block;min-height:18px;margin:0 0 6px;color:#64748b;font-size:12px;line-height:1.45;word-break:break-all}',
      '#cpa-sub2api-error-detail.error{padding:7px 9px;border-radius:9px;background:#fef2f2;color:#dc2626}',
      '#cpa-sub2api-debug-block{display:none;margin:0 0 12px}',
      '#cpa-sub2api-debug-block summary{padding:7px 9px;border-radius:9px;background:#fff7ed;color:#9a3412;font-size:12px;font-weight:700}',
      '#cpa-sub2api-debug-text{max-height:180px;overflow:auto;margin:6px 0 0;padding:8px;border:1px solid #fed7aa;border-radius:9px;background:#fff7ed;color:#7c2d12;font:11px/1.45 Consolas,monospace;white-space:pre-wrap;word-break:break-all}',
      '.cpa-sub2api-separator{height:1px;background:#e2e8f0;margin:14px 0}',
      '.cpa-sub2api-section-title{margin:12px 0 8px;font-size:13px;font-weight:800;color:#334155}',
      '#cpa-sub2api-panel details{margin-top:8px}',
      '#cpa-sub2api-panel summary{cursor:pointer;user-select:none;list-style:none}',
      '#cpa-sub2api-panel summary::-webkit-details-marker{display:none}',
      '#cpa-sub2api-panel input,#cpa-sub2api-panel select,#cpa-sub2api-panel textarea{width:100%;border:1px solid #cad2e1;border-radius:10px;padding:8px;font:12px/1.45 Arial,"Microsoft YaHei",sans-serif;color:#172033;background:#f8fafc}',
      '#cpa-sub2api-panel textarea{min-height:118px;resize:vertical;font-family:Consolas,monospace}',
      '#cpa-sub2api-panel select{height:86px}',
      '#cpa-sub2api-panel label{display:block;margin:8px 0 5px;font-size:12px;font-weight:700}',
      '#cpa-sub2api-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}',
      '#cpa-sub2api-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:10px 0}',
      '#cpa-sub2api-panel button,#cpa-sub2api-file-label,#cpa-sub2api-cpa-upload-label{border:1px solid #cbd5e1;border-radius:10px;background:#f8fafc;color:#172033;padding:8px 10px;font-size:12px;font-weight:700;text-align:center;cursor:pointer}',
      '#cpa-sub2api-panel button.primary{background:#2454ff;border-color:#2454ff;color:#fff}',
      '#cpa-sub2api-file,#cpa-sub2api-cpa-upload{display:none}',
      '#cpa-sub2api-status.error{color:#dc2626}',
      '#cpa-sub2api-status.success{color:#15803d}',
      '.cpa-sub2api-row{display:flex;align-items:center;gap:8px;margin:8px 0;font-size:12px}',
      '.cpa-sub2api-row input[type="checkbox"]{width:auto}'
    ].join('');
    document.head.appendChild(style);
  }

  function createPanel() {
    var root = document.createElement('div');
    root.id = 'cpa-sub2api-bridge';
    root.innerHTML = [
      '<div id="cpa-sub2api-panel">',
      '<h2>CPA / sub2api 桥接</h2>',
      '<div id="cpa-sub2api-status"></div>',
      '<p>一键从当前 CPA 导出认证文件和 sub2api 数据,再导入到 sub2api。</p>',
      '<div id="cpa-sub2api-primary-actions">',
      '<button class="primary" type="button" id="cpa-sub2api-quick-export">导出</button>',
      '<button class="primary" type="button" id="cpa-sub2api-import-sub">导入</button>',
      '</div>',
      '<span id="cpa-sub2api-data-badge">当前数据:0 个账号 / 0 个代理</span>',
      '<div id="cpa-sub2api-error-detail"></div>',
      '<details id="cpa-sub2api-debug-block"><summary>完整请求 / 响应详情</summary><pre id="cpa-sub2api-debug-text"></pre></details>',
      '<div class="cpa-sub2api-separator"></div>',
      '<div class="cpa-sub2api-section-title">配置区</div>',
      '<label for="cpa-sub2api-cpa-base">CPA API Base</label>',
      '<input id="cpa-sub2api-cpa-base" placeholder="当前页面 origin,例如 https://cpa.example.com">',
      '<label for="cpa-sub2api-cpa-key">CPA Management Key</label>',
      '<input id="cpa-sub2api-cpa-key" type="password" placeholder="Authorization Bearer / X-Management-Key">',
      '<label for="cpa-sub2api-sub-base">sub2api API Base</label>',
      '<input id="cpa-sub2api-sub-base" placeholder="https://你的-sub2api/api/v1">',
      '<label for="cpa-sub2api-sub-token">sub2api JWT Token</label>',
      '<input id="cpa-sub2api-sub-token" type="password" placeholder="默认读取 localStorage.auth_token">',
      '<div class="cpa-sub2api-row"><input id="cpa-sub2api-skip-group" type="checkbox"><label for="cpa-sub2api-skip-group">导入时跳过默认分组绑定</label></div>',
      '<div class="cpa-sub2api-separator"></div>',
      '<details id="cpa-sub2api-advanced">',
      '<summary class="cpa-sub2api-section-title">功能区</summary>',
      '<div id="cpa-sub2api-actions">',
      '<button type="button" id="cpa-sub2api-download-cpa" class="primary">下载</button>',
      '<label id="cpa-sub2api-cpa-upload-label" for="cpa-sub2api-cpa-upload">选择文件上传</label>',
      '<input id="cpa-sub2api-cpa-upload" type="file" multiple accept=".json,application/json,.zip,application/zip">',
      '<button type="button" id="cpa-sub2api-upload-cpa">上传</button>',
      '</div>',
      '<textarea id="cpa-sub2api-output" readonly placeholder="sub2api 数据预览" style="display:none"></textarea>',
      '</details>',
      '</div>',
      '<button id="cpa-sub2api-toggle" type="button">CPA ⇄ sub2api</button>'
    ].join('');

    document.body.appendChild(root);
    applySettingsToForm();
    document.getElementById('cpa-sub2api-toggle').addEventListener('click', function () {
      root.classList.toggle('open');
    });
    document.getElementById('cpa-sub2api-quick-export').addEventListener('click', handleQuickExport);
    document.getElementById('cpa-sub2api-download-cpa').addEventListener('click', handleDownloadAllCpa);
    document.getElementById('cpa-sub2api-upload-cpa').addEventListener('click', handleUploadCpaAuth);
    document.getElementById('cpa-sub2api-import-sub').addEventListener('click', handleImportToSub2api);
  }

  if (document.body) {
    createStyle();
    createPanel();
  }
})();