// ==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 // @downloadURL https://update.greasyfork.icu/scripts/576113/CPA%20to%20sub2api%20%E8%BF%81%E7%A7%BB.user.js // @updateURL https://update.greasyfork.icu/scripts/576113/CPA%20to%20sub2api%20%E8%BF%81%E7%A7%BB.meta.js // ==/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: ', 'Response:', 'HTTP ' + response.status + ' ' + response.statusText, responseText || '' ].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 = [ '
', '

CPA / sub2api 桥接

', '
', '

一键从当前 CPA 导出认证文件和 sub2api 数据,再导入到 sub2api。

', '
', '', '', '
', '当前数据:0 个账号 / 0 个代理', '
', '
完整请求 / 响应详情
', '
', '
配置区
', '', '', '', '', '', '', '', '', '
', '
', '
', '功能区', '
', '', '', '', '', '
', '', '
', '
', '' ].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(); } })();