cpa批量打包导出codex认证文件

2026-04-11 11:291阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

没找到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在这个文件夹下。
我说的是后台管理面板。可以像我上面说的那样操作。