cpa批量打包导出codex认证文件
- 内容介绍
- 文章标签
- 相关推荐
没找到cpa批量导出的按钮,只能用codex胡一个脚本了
(async () => {
/* =========================
* 可修改配置
* ========================= */
const CONCURRENCY = 5; // 并发数,默认 5
const EXPORT_TYPE = 'codex'; // 仅导出 type=codex
const PLAN_TYPE = ''; // '', 'all', 'free', 'team', 'plus', 'pro', 'enterprise', 'edu'
const IS_OUTPUT_ONLY_ACTIVE_JSON = false; // true 时,仅导出 status=active
const ZIP_NAME = 'cpa-codex-jsons.zip';
const REQUEST_TIMEOUT_MS = 30000; // XHR 超时毫秒数
const admin_password = ''; // cpa管理后台登录密码
/* =========================
* 固定配置
* ========================= */
const LIST_URL = new URL('/v0/management/auth-files', location.origin).toString();
const DOWNLOAD_URL = new URL('/v0/management/auth-files/download', location.origin).toString();
const SAFE_CONCURRENCY =
Number.isFinite(CONCURRENCY) && CONCURRENCY > 0 ? Math.floor(CONCURRENCY) : 5;
const ALLOWED_PLAN_TYPES = new Set([
'all',
'free',
'team',
'plus',
'pro',
'enterprise',
'edu'
]);
function normalizeBearerToken(value = '') {
return String(value ?? '').trim().replace(/^bearer\s+/i, '').trim();
}
function normalizePlanType(value = '') {
return String(value ?? '').trim().toLowerCase();
}
function getItemPlanType(item) {
const rawIdToken = item?.id_token;
if (!rawIdToken) return '';
if (typeof rawIdToken === 'object') {
return normalizePlanType(rawIdToken?.plan_type);
}
if (typeof rawIdToken === 'string') {
const text = rawIdToken.trim();
if (!text) return '';
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
return normalizePlanType(parsed?.plan_type);
}
} catch (_) {
// 忽略非 JSON 字符串
}
}
return '';
}
const NORMALIZED_ADMIN_PASSWORD = normalizeBearerToken(admin_password);
const AUTHORIZATION_HEADER = NORMALIZED_ADMIN_PASSWORD
? `Bearer ${NORMALIZED_ADMIN_PASSWORD}`
: '';
const NORMALIZED_PLAN_TYPE = normalizePlanType(PLAN_TYPE);
const PLAN_FILTER_ENABLED =
NORMALIZED_PLAN_TYPE !== '' && NORMALIZED_PLAN_TYPE !== 'all';
/* =========================
* 日志样式
* ========================= */
const LABEL_FMT = '%c[ cpa%c-codex%c-json%c导出脚本%c ] %c%s';
const LABEL_STYLES = [
'background:#67e8f9;color:#083344;padding:2px 0 2px 6px;font-weight:700;border-radius:4px 0 0 4px;',
'background:#2dd4bf;color:#083344;padding:2px 0;font-weight:700;',
'background:#34d399;color:#064e3b;padding:2px 0;font-weight:700;',
'background:#a3e635;color:#365314;padding:2px 0;font-weight:700;',
'background:#fde047;color:#713f12;padding:2px 6px 2px 0;font-weight:700;border-radius:0 4px 4px 0;'
];
function print(type, msg, extra) {
const msgStyle =
type === 'error'
? 'color:#dc2626;font-weight:700;'
: type === 'warn'
? 'color:#d97706;font-weight:700;'
: type === 'success'
? 'color:#059669;font-weight:700;'
: 'color:#111827;font-weight:600;';
if (typeof extra !== 'undefined') {
console.log(LABEL_FMT, ...LABEL_STYLES, msgStyle, msg, extra);
} else {
console.log(LABEL_FMT, ...LABEL_STYLES, msgStyle, msg);
}
}
const log = (msg, extra) => print('info', msg, extra);
const ok = (msg, extra) => print('success', msg, extra);
const warn = (msg, extra) => print('warn', msg, extra);
const err = (msg, extra) => print('error', msg, extra);
function isActiveStatus(status) {
return String(status || '').trim().toLowerCase() === 'active';
}
function ensureJsonFileName(name, index, usedNames) {
let fileName = String(name || `auth-file-${index}.json`).trim();
fileName = fileName.split('/').pop().split('\\').pop();
fileName = fileName.replace(/[<>:"/\\|?*\u0000-\u001F]+/g, '_');
if (!fileName) fileName = `auth-file-${index}.json`;
if (!/\.json$/i.test(fileName)) fileName += '.json';
const dotIndex = fileName.lastIndexOf('.');
const base = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
const ext = dotIndex > 0 ? fileName.slice(dotIndex) : '';
let candidate = fileName;
let count = 1;
while (usedNames.has(candidate.toLowerCase())) {
count += 1;
candidate = `${base} (${count})${ext}`;
}
usedNames.add(candidate.toLowerCase());
return candidate;
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes < 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
const units = ['KB', 'MB', 'GB', 'TB'];
let value = bytes / 1024;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
}
function formatDuration(ms) {
if (!Number.isFinite(ms) || ms < 0) return '0 ms';
if (ms < 1000) return `${Math.round(ms)} ms`;
const sec = ms / 1000;
if (sec < 60) return `${sec.toFixed(2)} s`;
const min = Math.floor(sec / 60);
const rest = sec % 60;
return `${min} min ${rest.toFixed(1)} s`;
}
function buildSummaryItems({
totalFiles = 0,
matchedTypeCodex = 0,
matchedPlanType = 0,
targetExportCount = 0,
success = 0,
failed = 0,
zipSize = '0 B',
durationMs = 0
}) {
return [
['压缩包文件名', ZIP_NAME],
['导出类型', EXPORT_TYPE],
['订阅类型筛选', PLAN_FILTER_ENABLED ? NORMALIZED_PLAN_TYPE : '全部'],
['仅导出 active', IS_OUTPUT_ONLY_ACTIVE_JSON ? '是' : '否'],
['列表总数', totalFiles],
['匹配 codex 数量', matchedTypeCodex],
['匹配订阅类型数量', matchedPlanType],
['待导出数量', targetExportCount],
['成功数量', success],
['失败数量', failed],
['压缩包大小', zipSize],
['耗时', formatDuration(durationMs)]
];
}
function printSummary(summary) {
log('导出统计汇总如下:');
for (const [label, value] of buildSummaryItems(summary)) {
log(`${label}:${value}`);
}
}
function getDosDateTime(date = new Date()) {
let year = date.getFullYear();
if (year < 1980) year = 1980;
const dosTime =
((date.getHours() & 0x1f) << 11) |
((date.getMinutes() & 0x3f) << 5) |
((Math.floor(date.getSeconds() / 2)) & 0x1f);
const dosDate =
(((year - 1980) & 0x7f) << 9) |
(((date.getMonth() + 1) & 0x0f) << 5) |
(date.getDate() & 0x1f);
return { dosTime, dosDate };
}
const CRC32_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
table[i] = c >>> 0;
}
return table;
})();
function crc32(bytes) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < bytes.length; i++) {
crc = CRC32_TABLE[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
function createZip(entries) {
const encoder = new TextEncoder();
const chunks = [];
const centralChunks = [];
let offset = 0;
const { dosTime, dosDate } = getDosDateTime(new Date());
for (const entry of entries) {
const nameBytes = encoder.encode(entry.name);
const dataBytes =
typeof entry.data === 'string' ? encoder.encode(entry.data) : entry.data;
const checksum = crc32(dataBytes);
const localHeader = new Uint8Array(30 + nameBytes.length);
const lv = new DataView(localHeader.buffer);
lv.setUint32(0, 0x04034b50, true);
lv.setUint16(4, 20, true);
lv.setUint16(6, 0x0800, true);
lv.setUint16(8, 0, true);
lv.setUint16(10, dosTime, true);
lv.setUint16(12, dosDate, true);
lv.setUint32(14, checksum, true);
lv.setUint32(18, dataBytes.length, true);
lv.setUint32(22, dataBytes.length, true);
lv.setUint16(26, nameBytes.length, true);
lv.setUint16(28, 0, true);
localHeader.set(nameBytes, 30);
chunks.push(localHeader, dataBytes);
const centralHeader = new Uint8Array(46 + nameBytes.length);
const cv = new DataView(centralHeader.buffer);
cv.setUint32(0, 0x02014b50, true);
cv.setUint16(4, 20, true);
cv.setUint16(6, 20, true);
cv.setUint16(8, 0x0800, true);
cv.setUint16(10, 0, true);
cv.setUint16(12, dosTime, true);
cv.setUint16(14, dosDate, true);
cv.setUint32(16, checksum, true);
cv.setUint32(20, dataBytes.length, true);
cv.setUint32(24, dataBytes.length, true);
cv.setUint16(28, nameBytes.length, true);
cv.setUint16(30, 0, true);
cv.setUint16(32, 0, true);
cv.setUint16(34, 0, true);
cv.setUint16(36, 0, true);
cv.setUint32(38, 0, true);
cv.setUint32(42, offset, true);
centralHeader.set(nameBytes, 46);
centralChunks.push(centralHeader);
offset += localHeader.length + dataBytes.length;
}
const centralOffset = offset;
let centralSize = 0;
for (const chunk of centralChunks) {
chunks.push(chunk);
centralSize += chunk.length;
}
const eocd = new Uint8Array(22);
const ev = new DataView(eocd.buffer);
ev.setUint32(0, 0x06054b50, true);
ev.setUint16(4, 0, true);
ev.setUint16(6, 0, true);
ev.setUint16(8, entries.length, true);
ev.setUint16(10, entries.length, true);
ev.setUint32(12, centralSize, true);
ev.setUint32(16, centralOffset, true);
ev.setUint16(20, 0, true);
chunks.push(eocd);
return new Blob(chunks, { type: 'application/zip' });
}
function triggerDownload(blob, fileName) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
function xhrRequestText(url, options = {}) {
const {
method = 'GET',
headers = {},
timeout = REQUEST_TIMEOUT_MS,
body = null
} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.withCredentials = true;
xhr.timeout = timeout;
xhr.responseType = 'text';
for (const [key, value] of Object.entries(headers)) {
if (value !== undefined && value !== null) {
xhr.setRequestHeader(key, value);
}
}
xhr.onload = () => {
if (xhr.status === 0) {
reject(
new Error('XHR 请求返回 status=0,可能是网络异常、CORS 限制或被浏览器拦截')
);
return;
}
resolve({
status: xhr.status,
statusText: xhr.statusText,
text: xhr.responseText || '',
contentType: (xhr.getResponseHeader('content-type') || '').toLowerCase(),
responseURL: xhr.responseURL || url
});
};
xhr.onerror = () => {
reject(new Error('XHR 请求失败,可能是网络异常、CORS 限制或网关拦截'));
};
xhr.ontimeout = () => {
reject(new Error(`XHR 请求超时(${timeout} ms)`));
};
xhr.send(body);
});
}
async function requestText(url, options = {}) {
const headers = {
Accept: 'application/json, text/plain, application/octet-stream, */*',
'X-Requested-With': 'XMLHttpRequest',
...(AUTHORIZATION_HEADER ? { Authorization: AUTHORIZATION_HEADER } : {}),
...(options.headers || {})
};
return xhrRequestText(url, {
...options,
headers
});
}
async function fetchAuthFileList() {
const { status, text } = await requestText(LIST_URL, {
headers: {
Accept: 'application/json, text/plain, */*'
}
});
if (status === 401 || status === 403) {
throw new Error('导出过程中接口返回 401/403;请配置正确的管理后台密码!');
}
if (status < 200 || status >= 300) {
throw new Error(`获取授权文件列表失败:HTTP ${status}`);
}
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error('授权文件列表接口返回的不是有效 JSON');
}
if (!data || !Array.isArray(data.files)) {
throw new Error('接口返回格式异常,未找到 files 数组');
}
return data.files;
}
async function downloadAuthFile(name) {
const url = new URL(DOWNLOAD_URL);
url.searchParams.set('name', name);
const { status, text } = await requestText(url.toString(), {
headers: {
Accept: 'application/json, text/plain, application/octet-stream, */*'
}
});
if (status === 401 || status === 403) {
throw new Error('导出过程中接口返回 401/403;请配置正确的管理后台密码!');
}
if (status < 200 || status >= 300) {
throw new Error(`下载失败:HTTP ${status}`);
}
return text;
}
async function runWithConcurrency(items, limit, worker) {
const results = new Array(items.length);
let nextIndex = 0;
async function runner() {
while (true) {
const current = nextIndex++;
if (current >= items.length) break;
try {
results[current] = await worker(items[current], current);
} catch (e) {
results[current] = {
ok: false,
reason: e?.message || String(e)
};
}
}
}
const workers = Array.from(
{ length: Math.min(limit, items.length || 0) },
() => runner()
);
await Promise.all(workers);
return results;
}
const startedAt = performance.now();
try {
if (NORMALIZED_PLAN_TYPE && !ALLOWED_PLAN_TYPES.has(NORMALIZED_PLAN_TYPE)) {
throw new Error(
`PLAN_TYPE 配置无效:${PLAN_TYPE},可选值为 all/free/team/plus/pro/enterprise/edu,留空或 all 表示不筛选`
);
}
log(`开始读取授权文件列表... 当前并发数:${SAFE_CONCURRENCY}`);
const allFiles = await fetchAuthFileList();
const codexFiles = allFiles.filter(
(item) =>
String(item?.type || '').trim().toLowerCase() ===
String(EXPORT_TYPE).trim().toLowerCase()
);
const skippedNonCodex = allFiles.length - codexFiles.length;
const planFilteredFiles = PLAN_FILTER_ENABLED
? codexFiles.filter((item) => getItemPlanType(item) === NORMALIZED_PLAN_TYPE)
: codexFiles;
const matchedPlanType = planFilteredFiles.length;
const skippedNonPlanType = codexFiles.length - planFilteredFiles.length;
const finalFiles = IS_OUTPUT_ONLY_ACTIVE_JSON
? planFilteredFiles.filter((item) => isActiveStatus(item?.status))
: planFilteredFiles;
const skippedNonActive = planFilteredFiles.length - finalFiles.length;
log(
`列表总数:${allFiles.length},type=${EXPORT_TYPE}:${codexFiles.length},跳过非 ${EXPORT_TYPE}:${skippedNonCodex},` +
(PLAN_FILTER_ENABLED
? `plan_type=${NORMALIZED_PLAN_TYPE}:${matchedPlanType},跳过非 ${NORMALIZED_PLAN_TYPE}:${skippedNonPlanType},`
: '未开启订阅类型过滤,') +
(IS_OUTPUT_ONLY_ACTIVE_JSON
? `已开启 active 过滤,跳过非 active:${skippedNonActive}`
: '未开启 active 过滤')
);
if (allFiles.length === 0) {
warn('已登录,但当前没有任何授权文件');
printSummary({
totalFiles: 0,
matchedTypeCodex: 0,
matchedPlanType: 0,
targetExportCount: 0,
success: 0,
failed: 0,
zipSize: '0 B',
durationMs: performance.now() - startedAt
});
return;
}
if (finalFiles.length === 0) {
let emptyMsg = `当前没有可导出的 type=${EXPORT_TYPE}`;
if (PLAN_FILTER_ENABLED) {
emptyMsg += ` 且 plan_type=${NORMALIZED_PLAN_TYPE}`;
}
if (IS_OUTPUT_ONLY_ACTIVE_JSON) {
emptyMsg += ' 且 status=active';
}
emptyMsg += ' 文件';
warn(emptyMsg);
printSummary({
totalFiles: allFiles.length,
matchedTypeCodex: codexFiles.length,
matchedPlanType,
targetExportCount: 0,
success: 0,
failed: 0,
zipSize: '0 B',
durationMs: performance.now() - startedAt
});
return;
}
const usedNames = new Set();
const tasks = finalFiles.map((item, index) => {
const sourceName = item?.name || item?.id || `auth-file-${index + 1}.json`;
const outputName = ensureJsonFileName(sourceName, index + 1, usedNames);
return {
index: index + 1,
sourceName,
outputName,
meta: item,
planType: getItemPlanType(item)
};
});
log(
`开始并发下载并打包,共 ${tasks.length} 个待导出文件... 过滤条件:type=${EXPORT_TYPE}` +
(PLAN_FILTER_ENABLED ? ` 且 plan_type=${NORMALIZED_PLAN_TYPE}` : '') +
(IS_OUTPUT_ONLY_ACTIVE_JSON ? ' 且 status=active' : '')
);
let finishedCount = 0;
const results = await runWithConcurrency(
tasks,
SAFE_CONCURRENCY,
async (task, index) => {
log(`开始下载 (${index + 1}/${tasks.length}):${task.outputName}`);
try {
const text = await downloadAuthFile(task.sourceName);
finishedCount += 1;
ok(`下载成功 (${finishedCount}/${tasks.length}):${task.outputName}`);
return {
ok: true,
name: task.outputName,
sourceName: task.sourceName,
data: text,
type: task.meta?.type || '',
status: task.meta?.status || '',
label: task.meta?.label || '',
account: task.meta?.account || task.meta?.email || '',
planType: task.planType
};
} catch (e) {
finishedCount += 1;
err(`下载失败 (${finishedCount}/${tasks.length}):${task.outputName} -> ${e?.message || e}`);
return {
ok: false,
name: task.outputName,
sourceName: task.sourceName,
reason: e?.message || String(e),
type: task.meta?.type || '',
status: task.meta?.status || '',
label: task.meta?.label || '',
account: task.meta?.account || task.meta?.email || '',
planType: task.planType
};
}
}
);
const successList = results.filter((x) => x && x.ok);
const failedList = results.filter((x) => x && !x.ok);
if (successList.length === 0) {
throw new Error('没有任何符合条件的文件导出成功,已取消打包下载');
}
const zipEntries = successList.map((item) => ({
name: item.name,
data: item.data
}));
log(`正在生成 ZIP 压缩包,共 ${zipEntries.length} 个文件...`);
const zipBlob = createZip(zipEntries);
triggerDownload(zipBlob, ZIP_NAME);
const duration = performance.now() - startedAt;
if (failedList.length > 0) {
warn(`导出完成,部分文件失败。已自动触发下载:${ZIP_NAME}`);
} else {
ok(`全部导出完成,已自动触发下载:${ZIP_NAME}`);
}
printSummary({
totalFiles: allFiles.length,
matchedTypeCodex: codexFiles.length,
matchedPlanType,
targetExportCount: finalFiles.length,
success: successList.length,
failed: failedList.length,
zipSize: formatBytes(zipBlob.size),
durationMs: duration
});
if (failedList.length > 0) {
warn('失败文件明细:');
console.table(
failedList.map((item, i) => ({
序号: i + 1,
文件名: item.name,
源文件名: item.sourceName,
类型: item.type,
订阅类型: item.planType,
状态: item.status,
账号: item.account,
失败原因: item.reason
}))
);
}
} catch (e) {
err(e?.message || String(e));
}
})();
使用方法:
1、填写admin_password配置
2、打开自己的cpa页面,F12,切换到控制台,复制粘贴代码,回车运行即可
网友解答:--【壹】--:
现在已经支持批量导出了
--【贰】--:
我就是想到这,好不容易,古法注册了几十个账号,赶紧备份,后面需要导入到新的cpa 或者 codex2api,或者 转格式到sub2api 都行
--【叁】--:
啊?cpa不是有批量导出吗
--【肆】--:
zb 部署的有福了
--【伍】--:
那还好,我这不是白忙活,等待cpa完善~
--【陆】--:
管理面板的还不太完善
--【柒】--:
IMG_20260406_1945301072×586 44.7 KB
Screenshot_2026-04-06-19-43-48-446_com.microsoft.emmx_1775475899484edit1200×2517 197 KB
--【捌】--:
你这样下载是.zip文件吗?还是我的版本问题
--【玖】--:
首先,去认证文件里,先点击全部,然后随便选中一个框,然后你就可以看到全选,把它下载下来就可以。
--【拾】--:
嗨,我在发布了主题才看到需要选择一个后才在底部弹出。
我试了一下,是一个一个json的下载,不是打包
--【拾壹】--:
自己的CPA直接去服务器的配置目录复制不就好了吗
--【拾贰】--:
比如:部署到云容器麻烦啊。多一种方法,多一点方便。
这脚本还可以筛选。
--【拾叁】--:
就只是json
--【拾肆】--:
感谢分享,生怕zeabur哪天用不了了
--【拾伍】--:
我看到只能一个个下载,没有打包
--【拾陆】--:
我也是这么做的,直接打包下载就好了
--【拾柒】--:
auth目录不是有吗,再说了。。。在官方的Webui的认证文件不也有批量导出吗。。。我们用的是同一个cpa吗
--【拾捌】--:
认证文件是在cpa的auth 目录下吧。
--【拾玖】--:
如果你说的是在服务器上./cli-proxy-cli在这个文件夹下。
我说的是后台管理面板。可以像我上面说的那样操作。
没找到cpa批量导出的按钮,只能用codex胡一个脚本了
(async () => {
/* =========================
* 可修改配置
* ========================= */
const CONCURRENCY = 5; // 并发数,默认 5
const EXPORT_TYPE = 'codex'; // 仅导出 type=codex
const PLAN_TYPE = ''; // '', 'all', 'free', 'team', 'plus', 'pro', 'enterprise', 'edu'
const IS_OUTPUT_ONLY_ACTIVE_JSON = false; // true 时,仅导出 status=active
const ZIP_NAME = 'cpa-codex-jsons.zip';
const REQUEST_TIMEOUT_MS = 30000; // XHR 超时毫秒数
const admin_password = ''; // cpa管理后台登录密码
/* =========================
* 固定配置
* ========================= */
const LIST_URL = new URL('/v0/management/auth-files', location.origin).toString();
const DOWNLOAD_URL = new URL('/v0/management/auth-files/download', location.origin).toString();
const SAFE_CONCURRENCY =
Number.isFinite(CONCURRENCY) && CONCURRENCY > 0 ? Math.floor(CONCURRENCY) : 5;
const ALLOWED_PLAN_TYPES = new Set([
'all',
'free',
'team',
'plus',
'pro',
'enterprise',
'edu'
]);
function normalizeBearerToken(value = '') {
return String(value ?? '').trim().replace(/^bearer\s+/i, '').trim();
}
function normalizePlanType(value = '') {
return String(value ?? '').trim().toLowerCase();
}
function getItemPlanType(item) {
const rawIdToken = item?.id_token;
if (!rawIdToken) return '';
if (typeof rawIdToken === 'object') {
return normalizePlanType(rawIdToken?.plan_type);
}
if (typeof rawIdToken === 'string') {
const text = rawIdToken.trim();
if (!text) return '';
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
return normalizePlanType(parsed?.plan_type);
}
} catch (_) {
// 忽略非 JSON 字符串
}
}
return '';
}
const NORMALIZED_ADMIN_PASSWORD = normalizeBearerToken(admin_password);
const AUTHORIZATION_HEADER = NORMALIZED_ADMIN_PASSWORD
? `Bearer ${NORMALIZED_ADMIN_PASSWORD}`
: '';
const NORMALIZED_PLAN_TYPE = normalizePlanType(PLAN_TYPE);
const PLAN_FILTER_ENABLED =
NORMALIZED_PLAN_TYPE !== '' && NORMALIZED_PLAN_TYPE !== 'all';
/* =========================
* 日志样式
* ========================= */
const LABEL_FMT = '%c[ cpa%c-codex%c-json%c导出脚本%c ] %c%s';
const LABEL_STYLES = [
'background:#67e8f9;color:#083344;padding:2px 0 2px 6px;font-weight:700;border-radius:4px 0 0 4px;',
'background:#2dd4bf;color:#083344;padding:2px 0;font-weight:700;',
'background:#34d399;color:#064e3b;padding:2px 0;font-weight:700;',
'background:#a3e635;color:#365314;padding:2px 0;font-weight:700;',
'background:#fde047;color:#713f12;padding:2px 6px 2px 0;font-weight:700;border-radius:0 4px 4px 0;'
];
function print(type, msg, extra) {
const msgStyle =
type === 'error'
? 'color:#dc2626;font-weight:700;'
: type === 'warn'
? 'color:#d97706;font-weight:700;'
: type === 'success'
? 'color:#059669;font-weight:700;'
: 'color:#111827;font-weight:600;';
if (typeof extra !== 'undefined') {
console.log(LABEL_FMT, ...LABEL_STYLES, msgStyle, msg, extra);
} else {
console.log(LABEL_FMT, ...LABEL_STYLES, msgStyle, msg);
}
}
const log = (msg, extra) => print('info', msg, extra);
const ok = (msg, extra) => print('success', msg, extra);
const warn = (msg, extra) => print('warn', msg, extra);
const err = (msg, extra) => print('error', msg, extra);
function isActiveStatus(status) {
return String(status || '').trim().toLowerCase() === 'active';
}
function ensureJsonFileName(name, index, usedNames) {
let fileName = String(name || `auth-file-${index}.json`).trim();
fileName = fileName.split('/').pop().split('\\').pop();
fileName = fileName.replace(/[<>:"/\\|?*\u0000-\u001F]+/g, '_');
if (!fileName) fileName = `auth-file-${index}.json`;
if (!/\.json$/i.test(fileName)) fileName += '.json';
const dotIndex = fileName.lastIndexOf('.');
const base = dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
const ext = dotIndex > 0 ? fileName.slice(dotIndex) : '';
let candidate = fileName;
let count = 1;
while (usedNames.has(candidate.toLowerCase())) {
count += 1;
candidate = `${base} (${count})${ext}`;
}
usedNames.add(candidate.toLowerCase());
return candidate;
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes) || bytes < 0) return '0 B';
if (bytes < 1024) return `${bytes} B`;
const units = ['KB', 'MB', 'GB', 'TB'];
let value = bytes / 1024;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[unitIndex]}`;
}
function formatDuration(ms) {
if (!Number.isFinite(ms) || ms < 0) return '0 ms';
if (ms < 1000) return `${Math.round(ms)} ms`;
const sec = ms / 1000;
if (sec < 60) return `${sec.toFixed(2)} s`;
const min = Math.floor(sec / 60);
const rest = sec % 60;
return `${min} min ${rest.toFixed(1)} s`;
}
function buildSummaryItems({
totalFiles = 0,
matchedTypeCodex = 0,
matchedPlanType = 0,
targetExportCount = 0,
success = 0,
failed = 0,
zipSize = '0 B',
durationMs = 0
}) {
return [
['压缩包文件名', ZIP_NAME],
['导出类型', EXPORT_TYPE],
['订阅类型筛选', PLAN_FILTER_ENABLED ? NORMALIZED_PLAN_TYPE : '全部'],
['仅导出 active', IS_OUTPUT_ONLY_ACTIVE_JSON ? '是' : '否'],
['列表总数', totalFiles],
['匹配 codex 数量', matchedTypeCodex],
['匹配订阅类型数量', matchedPlanType],
['待导出数量', targetExportCount],
['成功数量', success],
['失败数量', failed],
['压缩包大小', zipSize],
['耗时', formatDuration(durationMs)]
];
}
function printSummary(summary) {
log('导出统计汇总如下:');
for (const [label, value] of buildSummaryItems(summary)) {
log(`${label}:${value}`);
}
}
function getDosDateTime(date = new Date()) {
let year = date.getFullYear();
if (year < 1980) year = 1980;
const dosTime =
((date.getHours() & 0x1f) << 11) |
((date.getMinutes() & 0x3f) << 5) |
((Math.floor(date.getSeconds() / 2)) & 0x1f);
const dosDate =
(((year - 1980) & 0x7f) << 9) |
(((date.getMonth() + 1) & 0x0f) << 5) |
(date.getDate() & 0x1f);
return { dosTime, dosDate };
}
const CRC32_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
table[i] = c >>> 0;
}
return table;
})();
function crc32(bytes) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < bytes.length; i++) {
crc = CRC32_TABLE[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
function createZip(entries) {
const encoder = new TextEncoder();
const chunks = [];
const centralChunks = [];
let offset = 0;
const { dosTime, dosDate } = getDosDateTime(new Date());
for (const entry of entries) {
const nameBytes = encoder.encode(entry.name);
const dataBytes =
typeof entry.data === 'string' ? encoder.encode(entry.data) : entry.data;
const checksum = crc32(dataBytes);
const localHeader = new Uint8Array(30 + nameBytes.length);
const lv = new DataView(localHeader.buffer);
lv.setUint32(0, 0x04034b50, true);
lv.setUint16(4, 20, true);
lv.setUint16(6, 0x0800, true);
lv.setUint16(8, 0, true);
lv.setUint16(10, dosTime, true);
lv.setUint16(12, dosDate, true);
lv.setUint32(14, checksum, true);
lv.setUint32(18, dataBytes.length, true);
lv.setUint32(22, dataBytes.length, true);
lv.setUint16(26, nameBytes.length, true);
lv.setUint16(28, 0, true);
localHeader.set(nameBytes, 30);
chunks.push(localHeader, dataBytes);
const centralHeader = new Uint8Array(46 + nameBytes.length);
const cv = new DataView(centralHeader.buffer);
cv.setUint32(0, 0x02014b50, true);
cv.setUint16(4, 20, true);
cv.setUint16(6, 20, true);
cv.setUint16(8, 0x0800, true);
cv.setUint16(10, 0, true);
cv.setUint16(12, dosTime, true);
cv.setUint16(14, dosDate, true);
cv.setUint32(16, checksum, true);
cv.setUint32(20, dataBytes.length, true);
cv.setUint32(24, dataBytes.length, true);
cv.setUint16(28, nameBytes.length, true);
cv.setUint16(30, 0, true);
cv.setUint16(32, 0, true);
cv.setUint16(34, 0, true);
cv.setUint16(36, 0, true);
cv.setUint32(38, 0, true);
cv.setUint32(42, offset, true);
centralHeader.set(nameBytes, 46);
centralChunks.push(centralHeader);
offset += localHeader.length + dataBytes.length;
}
const centralOffset = offset;
let centralSize = 0;
for (const chunk of centralChunks) {
chunks.push(chunk);
centralSize += chunk.length;
}
const eocd = new Uint8Array(22);
const ev = new DataView(eocd.buffer);
ev.setUint32(0, 0x06054b50, true);
ev.setUint16(4, 0, true);
ev.setUint16(6, 0, true);
ev.setUint16(8, entries.length, true);
ev.setUint16(10, entries.length, true);
ev.setUint32(12, centralSize, true);
ev.setUint32(16, centralOffset, true);
ev.setUint16(20, 0, true);
chunks.push(eocd);
return new Blob(chunks, { type: 'application/zip' });
}
function triggerDownload(blob, fileName) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
function xhrRequestText(url, options = {}) {
const {
method = 'GET',
headers = {},
timeout = REQUEST_TIMEOUT_MS,
body = null
} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.withCredentials = true;
xhr.timeout = timeout;
xhr.responseType = 'text';
for (const [key, value] of Object.entries(headers)) {
if (value !== undefined && value !== null) {
xhr.setRequestHeader(key, value);
}
}
xhr.onload = () => {
if (xhr.status === 0) {
reject(
new Error('XHR 请求返回 status=0,可能是网络异常、CORS 限制或被浏览器拦截')
);
return;
}
resolve({
status: xhr.status,
statusText: xhr.statusText,
text: xhr.responseText || '',
contentType: (xhr.getResponseHeader('content-type') || '').toLowerCase(),
responseURL: xhr.responseURL || url
});
};
xhr.onerror = () => {
reject(new Error('XHR 请求失败,可能是网络异常、CORS 限制或网关拦截'));
};
xhr.ontimeout = () => {
reject(new Error(`XHR 请求超时(${timeout} ms)`));
};
xhr.send(body);
});
}
async function requestText(url, options = {}) {
const headers = {
Accept: 'application/json, text/plain, application/octet-stream, */*',
'X-Requested-With': 'XMLHttpRequest',
...(AUTHORIZATION_HEADER ? { Authorization: AUTHORIZATION_HEADER } : {}),
...(options.headers || {})
};
return xhrRequestText(url, {
...options,
headers
});
}
async function fetchAuthFileList() {
const { status, text } = await requestText(LIST_URL, {
headers: {
Accept: 'application/json, text/plain, */*'
}
});
if (status === 401 || status === 403) {
throw new Error('导出过程中接口返回 401/403;请配置正确的管理后台密码!');
}
if (status < 200 || status >= 300) {
throw new Error(`获取授权文件列表失败:HTTP ${status}`);
}
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error('授权文件列表接口返回的不是有效 JSON');
}
if (!data || !Array.isArray(data.files)) {
throw new Error('接口返回格式异常,未找到 files 数组');
}
return data.files;
}
async function downloadAuthFile(name) {
const url = new URL(DOWNLOAD_URL);
url.searchParams.set('name', name);
const { status, text } = await requestText(url.toString(), {
headers: {
Accept: 'application/json, text/plain, application/octet-stream, */*'
}
});
if (status === 401 || status === 403) {
throw new Error('导出过程中接口返回 401/403;请配置正确的管理后台密码!');
}
if (status < 200 || status >= 300) {
throw new Error(`下载失败:HTTP ${status}`);
}
return text;
}
async function runWithConcurrency(items, limit, worker) {
const results = new Array(items.length);
let nextIndex = 0;
async function runner() {
while (true) {
const current = nextIndex++;
if (current >= items.length) break;
try {
results[current] = await worker(items[current], current);
} catch (e) {
results[current] = {
ok: false,
reason: e?.message || String(e)
};
}
}
}
const workers = Array.from(
{ length: Math.min(limit, items.length || 0) },
() => runner()
);
await Promise.all(workers);
return results;
}
const startedAt = performance.now();
try {
if (NORMALIZED_PLAN_TYPE && !ALLOWED_PLAN_TYPES.has(NORMALIZED_PLAN_TYPE)) {
throw new Error(
`PLAN_TYPE 配置无效:${PLAN_TYPE},可选值为 all/free/team/plus/pro/enterprise/edu,留空或 all 表示不筛选`
);
}
log(`开始读取授权文件列表... 当前并发数:${SAFE_CONCURRENCY}`);
const allFiles = await fetchAuthFileList();
const codexFiles = allFiles.filter(
(item) =>
String(item?.type || '').trim().toLowerCase() ===
String(EXPORT_TYPE).trim().toLowerCase()
);
const skippedNonCodex = allFiles.length - codexFiles.length;
const planFilteredFiles = PLAN_FILTER_ENABLED
? codexFiles.filter((item) => getItemPlanType(item) === NORMALIZED_PLAN_TYPE)
: codexFiles;
const matchedPlanType = planFilteredFiles.length;
const skippedNonPlanType = codexFiles.length - planFilteredFiles.length;
const finalFiles = IS_OUTPUT_ONLY_ACTIVE_JSON
? planFilteredFiles.filter((item) => isActiveStatus(item?.status))
: planFilteredFiles;
const skippedNonActive = planFilteredFiles.length - finalFiles.length;
log(
`列表总数:${allFiles.length},type=${EXPORT_TYPE}:${codexFiles.length},跳过非 ${EXPORT_TYPE}:${skippedNonCodex},` +
(PLAN_FILTER_ENABLED
? `plan_type=${NORMALIZED_PLAN_TYPE}:${matchedPlanType},跳过非 ${NORMALIZED_PLAN_TYPE}:${skippedNonPlanType},`
: '未开启订阅类型过滤,') +
(IS_OUTPUT_ONLY_ACTIVE_JSON
? `已开启 active 过滤,跳过非 active:${skippedNonActive}`
: '未开启 active 过滤')
);
if (allFiles.length === 0) {
warn('已登录,但当前没有任何授权文件');
printSummary({
totalFiles: 0,
matchedTypeCodex: 0,
matchedPlanType: 0,
targetExportCount: 0,
success: 0,
failed: 0,
zipSize: '0 B',
durationMs: performance.now() - startedAt
});
return;
}
if (finalFiles.length === 0) {
let emptyMsg = `当前没有可导出的 type=${EXPORT_TYPE}`;
if (PLAN_FILTER_ENABLED) {
emptyMsg += ` 且 plan_type=${NORMALIZED_PLAN_TYPE}`;
}
if (IS_OUTPUT_ONLY_ACTIVE_JSON) {
emptyMsg += ' 且 status=active';
}
emptyMsg += ' 文件';
warn(emptyMsg);
printSummary({
totalFiles: allFiles.length,
matchedTypeCodex: codexFiles.length,
matchedPlanType,
targetExportCount: 0,
success: 0,
failed: 0,
zipSize: '0 B',
durationMs: performance.now() - startedAt
});
return;
}
const usedNames = new Set();
const tasks = finalFiles.map((item, index) => {
const sourceName = item?.name || item?.id || `auth-file-${index + 1}.json`;
const outputName = ensureJsonFileName(sourceName, index + 1, usedNames);
return {
index: index + 1,
sourceName,
outputName,
meta: item,
planType: getItemPlanType(item)
};
});
log(
`开始并发下载并打包,共 ${tasks.length} 个待导出文件... 过滤条件:type=${EXPORT_TYPE}` +
(PLAN_FILTER_ENABLED ? ` 且 plan_type=${NORMALIZED_PLAN_TYPE}` : '') +
(IS_OUTPUT_ONLY_ACTIVE_JSON ? ' 且 status=active' : '')
);
let finishedCount = 0;
const results = await runWithConcurrency(
tasks,
SAFE_CONCURRENCY,
async (task, index) => {
log(`开始下载 (${index + 1}/${tasks.length}):${task.outputName}`);
try {
const text = await downloadAuthFile(task.sourceName);
finishedCount += 1;
ok(`下载成功 (${finishedCount}/${tasks.length}):${task.outputName}`);
return {
ok: true,
name: task.outputName,
sourceName: task.sourceName,
data: text,
type: task.meta?.type || '',
status: task.meta?.status || '',
label: task.meta?.label || '',
account: task.meta?.account || task.meta?.email || '',
planType: task.planType
};
} catch (e) {
finishedCount += 1;
err(`下载失败 (${finishedCount}/${tasks.length}):${task.outputName} -> ${e?.message || e}`);
return {
ok: false,
name: task.outputName,
sourceName: task.sourceName,
reason: e?.message || String(e),
type: task.meta?.type || '',
status: task.meta?.status || '',
label: task.meta?.label || '',
account: task.meta?.account || task.meta?.email || '',
planType: task.planType
};
}
}
);
const successList = results.filter((x) => x && x.ok);
const failedList = results.filter((x) => x && !x.ok);
if (successList.length === 0) {
throw new Error('没有任何符合条件的文件导出成功,已取消打包下载');
}
const zipEntries = successList.map((item) => ({
name: item.name,
data: item.data
}));
log(`正在生成 ZIP 压缩包,共 ${zipEntries.length} 个文件...`);
const zipBlob = createZip(zipEntries);
triggerDownload(zipBlob, ZIP_NAME);
const duration = performance.now() - startedAt;
if (failedList.length > 0) {
warn(`导出完成,部分文件失败。已自动触发下载:${ZIP_NAME}`);
} else {
ok(`全部导出完成,已自动触发下载:${ZIP_NAME}`);
}
printSummary({
totalFiles: allFiles.length,
matchedTypeCodex: codexFiles.length,
matchedPlanType,
targetExportCount: finalFiles.length,
success: successList.length,
failed: failedList.length,
zipSize: formatBytes(zipBlob.size),
durationMs: duration
});
if (failedList.length > 0) {
warn('失败文件明细:');
console.table(
failedList.map((item, i) => ({
序号: i + 1,
文件名: item.name,
源文件名: item.sourceName,
类型: item.type,
订阅类型: item.planType,
状态: item.status,
账号: item.account,
失败原因: item.reason
}))
);
}
} catch (e) {
err(e?.message || String(e));
}
})();
使用方法:
1、填写admin_password配置
2、打开自己的cpa页面,F12,切换到控制台,复制粘贴代码,回车运行即可
网友解答:--【壹】--:
现在已经支持批量导出了
--【贰】--:
我就是想到这,好不容易,古法注册了几十个账号,赶紧备份,后面需要导入到新的cpa 或者 codex2api,或者 转格式到sub2api 都行
--【叁】--:
啊?cpa不是有批量导出吗
--【肆】--:
zb 部署的有福了
--【伍】--:
那还好,我这不是白忙活,等待cpa完善~
--【陆】--:
管理面板的还不太完善
--【柒】--:
IMG_20260406_1945301072×586 44.7 KB
Screenshot_2026-04-06-19-43-48-446_com.microsoft.emmx_1775475899484edit1200×2517 197 KB
--【捌】--:
你这样下载是.zip文件吗?还是我的版本问题
--【玖】--:
首先,去认证文件里,先点击全部,然后随便选中一个框,然后你就可以看到全选,把它下载下来就可以。
--【拾】--:
嗨,我在发布了主题才看到需要选择一个后才在底部弹出。
我试了一下,是一个一个json的下载,不是打包
--【拾壹】--:
自己的CPA直接去服务器的配置目录复制不就好了吗
--【拾贰】--:
比如:部署到云容器麻烦啊。多一种方法,多一点方便。
这脚本还可以筛选。
--【拾叁】--:
就只是json
--【拾肆】--:
感谢分享,生怕zeabur哪天用不了了
--【拾伍】--:
我看到只能一个个下载,没有打包
--【拾陆】--:
我也是这么做的,直接打包下载就好了
--【拾柒】--:
auth目录不是有吗,再说了。。。在官方的Webui的认证文件不也有批量导出吗。。。我们用的是同一个cpa吗
--【拾捌】--:
认证文件是在cpa的auth 目录下吧。
--【拾玖】--:
如果你说的是在服务器上./cli-proxy-cli在这个文件夹下。
我说的是后台管理面板。可以像我上面说的那样操作。

