Greasy Fork is available in English.
在 CPA 和 sub2api 页面提供手动 JSON 中间态导入导出工具
// ==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();
}
})();