/* 账号管理 —— GPT 账号(sub2api/probe) 为主 + PLUS 邮箱(查收/拿RT) 为辅。 GPT 侧移植自 auto public/app.js 的 renderProbeAccounts 等;PLUS 侧沿用先前实现。 */ (function () { 'use strict'; const API_BASE = (document.documentElement.getAttribute('data-api-base') || '').replace(/\/$/, ''); const $ = (sel) => document.querySelector(sel); const state = { activeTab: 'gpt', // GPT / probe probe: [], probeSelected: new Set(), probeShowPw: new Set(), probeAutoRefresh: null, pbPage: 1, pbPageSize: 10, pbOpenedOnly: false, // PLUS plus: [], plSelected: new Set(), plShowPw: new Set(), plCodes: new Map(), plStatusMap: new Map(), plSearch: '', plFilter: 'all', plPage: 1, plPageSize: 10, }; const SUB2API_DEFAULT_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann'; // ---------- 工具 ---------- function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } function domainOf(email) { const m = String(email || '').split('@'); return m[1] || ''; } function fmtTime(t) { if (!t) return ''; const d = new Date(t); return Number.isNaN(d.getTime()) ? String(t) : d.toLocaleString(); } function fmtClock(t) { if (!t) return ''; const d = new Date(t); return Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString(); } let toastTimer = null; function toast(msg, type) { const el = $('#amToast'); el.textContent = msg; el.className = 'am-toast show' + (type === 'ok' ? ' ok' : type === 'err' ? ' err' : ''); clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.className = 'am-toast'; }, 2600); } async function copyText(text, okMsg) { try { await navigator.clipboard.writeText(String(text || '')); toast(okMsg || '已复制', 'ok'); } catch { toast('复制失败', 'err'); } } // 主题化确认弹窗(替代原生 confirm),返回 Promise function confirmDialog(message, opts) { opts = opts || {}; return new Promise((resolve) => { const modal = $('#confirmModal'); const okBtn = $('#confirmOk'); const cancelBtn = $('#confirmCancel'); const closeX = $('#confirmCloseX'); const input = $('#confirmInput'); if (input) input.hidden = true; $('#confirmTitle').textContent = opts.title || '确认操作'; $('#confirmMsg').textContent = String(message == null ? '' : message); okBtn.textContent = opts.okText || '确定'; okBtn.className = opts.danger ? 'danger' : ''; let done = false; const cleanup = (val) => { if (done) return; done = true; modal.hidden = true; okBtn.removeEventListener('click', onOk); cancelBtn.removeEventListener('click', onCancel); closeX.removeEventListener('click', onCancel); modal.removeEventListener('mousedown', onBackdrop); document.removeEventListener('keydown', onKey); resolve(val); }; const onOk = () => cleanup(true); const onCancel = () => cleanup(false); const onBackdrop = (e) => { if (e.target === modal) cleanup(false); }; const onKey = (e) => { if (e.key === 'Escape') cleanup(false); else if (e.key === 'Enter') cleanup(true); }; okBtn.addEventListener('click', onOk); cancelBtn.addEventListener('click', onCancel); closeX.addEventListener('click', onCancel); modal.addEventListener('mousedown', onBackdrop); document.addEventListener('keydown', onKey); modal.hidden = false; okBtn.focus(); }); } function promptDialog(message, opts) { opts = opts || {}; return new Promise((resolve) => { const modal = $('#confirmModal'); const okBtn = $('#confirmOk'); const cancelBtn = $('#confirmCancel'); const closeX = $('#confirmCloseX'); const input = $('#confirmInput'); $('#confirmTitle').textContent = opts.title || '请输入'; $('#confirmMsg').textContent = String(message == null ? '' : message); okBtn.textContent = opts.okText || '确定'; okBtn.className = ''; input.hidden = false; input.value = opts.value || ''; input.placeholder = opts.placeholder || ''; let done = false; const cleanup = (val) => { if (done) return; done = true; modal.hidden = true; input.hidden = true; okBtn.removeEventListener('click', onOk); cancelBtn.removeEventListener('click', onCancel); closeX.removeEventListener('click', onCancel); modal.removeEventListener('mousedown', onBackdrop); document.removeEventListener('keydown', onKey); resolve(val); }; const onOk = () => cleanup(input.value); const onCancel = () => cleanup(null); const onBackdrop = (e) => { if (e.target === modal) cleanup(null); }; const onKey = (e) => { if (e.key === 'Escape') cleanup(null); else if (e.key === 'Enter') cleanup(input.value); }; okBtn.addEventListener('click', onOk); cancelBtn.addEventListener('click', onCancel); closeX.addEventListener('click', onCancel); modal.addEventListener('mousedown', onBackdrop); document.addEventListener('keydown', onKey); modal.hidden = false; setTimeout(() => input.focus(), 0); }); } async function api(method, path, body) { const opts = { method, headers: {} }; if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } const res = await fetch(API_BASE + path, opts); const ct = res.headers.get('content-type') || ''; const data = ct.includes('application/json') ? await res.json() : {}; if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status)); return data; } async function downloadPost(path, body, fallbackName) { const res = await fetch(API_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}), }); if (!res.ok) throw new Error('HTTP ' + res.status); const count = res.headers.get('X-Export-Count'); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fallbackName; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); return count; } function paginate(list, page, size) { const total = list.length; const pages = Math.max(1, Math.ceil(total / size)); const p = Math.min(Math.max(1, page), pages); const startIdx = (p - 1) * size; return { items: list.slice(startIdx, startIdx + size), page: p, pages, total, startIdx }; } // ========================================================================== // Tabs + 统计卡片 // ========================================================================== // 视图切换已下线:拿RT 能力已并入账号管理(工具栏「重拿RT」),页面固定展示 sub2api 账号管理主视图。 // 旧 PLUS 邮箱视图的 DOM 仍保留但隐藏,避免删除后遗留坏引用/JS 报错(后端接口不动)。 function switchTab() { state.activeTab = 'gpt'; const vg = $('#view-gpt'); if (vg) vg.hidden = false; const vp = $('#view-plus'); if (vp) vp.hidden = true; renderStats(); loadProbe(); } function renderStats() { if (state.activeTab === 'gpt') { const okCount = state.probe.filter((a) => a.status === 'ok').length; const renewed = state.probeAutoRefresh && state.probeAutoRefresh.lastResult ? (state.probeAutoRefresh.lastResult.refreshed || 0) : 0; $('#statAL').textContent = '账号总数'; $('#statA').textContent = state.probe.length; $('#statBL').textContent = '已选中'; $('#statB').textContent = state.probeSelected.size; $('#statCL').textContent = '有效 AT'; $('#statC').textContent = okCount; $('#statDL').textContent = '续期数量'; $('#statD').textContent = renewed; updateBatchLabel(); } else { $('#statAL').textContent = 'PLUS 总数'; $('#statA').textContent = state.plus.length; $('#statBL').textContent = '已选中'; $('#statB').textContent = state.plSelected.size; $('#statCL').textContent = 'PLUS数量'; $('#statC').textContent = state.plus.filter((a) => a.isPlus).length; $('#statDL').textContent = 'RT提取数量'; $('#statD').textContent = state.plus.filter((a) => a.codexRt).length; } } // ========================================================================== // GPT 账号管理(probe / sub2api) // ========================================================================== function formatTokenExp(a) { const exp = Number(a.tokenExp) || 0; if (!exp) return '未知'; const main = new Date(exp * 1000).toLocaleString(); const sec = a.tokenExpiresInSec; const expired = a.tokenExpired === true || (typeof sec === 'number' && sec <= 0); let sub = ''; if (expired) sub = '已过期'; else if (typeof sec === 'number') { const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60); sub = d > 0 ? `剩余 ${d} 天 ${h} 小时` : h > 0 ? `剩余 ${h} 小时 ${m} 分` : `剩余 ${m} 分`; } return `
${escapeHtml(main)}
${escapeHtml(sub)}
`; } function statusPill(a) { const s = a.status || 'unknown'; const label = { ok: '有效', expiring: '临期', invalid: '失效', unknown: '未知' }[s] || '未知'; return `${label}`; } function renderAutoRefreshHint() { const ar = state.probeAutoRefresh; const cb = $('#pbAutoRefresh'); const hint = $('#pbAutoRefreshHint'); if (!ar) { if (cb) cb.checked = true; if (hint) hint.textContent = '—'; return; } if (cb) cb.checked = ar.enabled !== false; if (!hint) return; if (ar.running) { hint.textContent = '刷新中…'; return; } if (!ar.enabled) { hint.textContent = '已关闭'; return; } const parts = [`每 ${ar.intervalMin || 15} 分钟`]; if (ar.lastRunAt) parts.push(`上次 ${fmtClock(ar.lastRunAt)}`); if (ar.lastResult) parts.push(`续期 ${ar.lastResult.refreshed || 0}`); hint.textContent = parts.join(' · '); } let pbUnknownKickAt = 0; function kickUnknownIfNeeded() { const ar = state.probeAutoRefresh; if (!ar || ar.enabled === false || ar.running) return; const unknowns = state.probe.filter((a) => (a.status === 'unknown') && a.hasRefresh); if (!unknowns.length) return; const now = Date.now(); if (now - pbUnknownKickAt < 30000) return; pbUnknownKickAt = now; api('POST', '/api/probe/accounts/auto-refresh', { enabled: true, runNow: true }) .then((next) => { state.probeAutoRefresh = next; renderAutoRefreshHint(); setTimeout(() => loadProbe(true), 3000); }) .catch(() => {}); } async function loadProbe(skipKick) { try { const data = await api('GET', '/api/probe/accounts'); state.probe = data.accounts || []; state.probeAutoRefresh = data.autoRefresh || null; renderProbe(); if (!skipKick) kickUnknownIfNeeded(); } catch (e) { toast('加载 GPT 账号失败:' + e.message, 'err'); } } async function pbDetectOne(id, btn) { if (btn) btn.disabled = true; toast('正在通过 AT 检测套餐…', 'info'); try { const r = await api('POST', '/api/probe/accounts/detect', { id }); await loadProbe(true); if (r.ok && r.source === 'live') { toast('实时检测:' + (r.planLabel || '未知') + (r.active ? '(订阅有效)' : '') + (r.refreshed ? '|已刷新TOKEN' : ''), 'ok'); } else if (r.ok) { toast('实时校验失败(' + (r.liveError || '未知') + '),已回退 AT 标记:' + (r.planLabel || '未知'), 'err'); } else { toast(r.error || '检测失败', 'err'); } } catch (e) { toast('检测失败:' + e.message, 'err'); } finally { if (btn) btn.disabled = false; } } function pbFiltered() { return state.pbOpenedOnly ? state.probe.filter((a) => a.opened) : state.probe; } function pbPageItems() { return paginate(pbFiltered(), state.pbPage, state.pbPageSize).items; } function renderProbe() { const tbody = $('#pbRows'); const total = state.probe.length; const list = pbFiltered(); const pg = paginate(list, state.pbPage, state.pbPageSize); state.pbPage = pg.page; tbody.innerHTML = ''; renderAutoRefreshHint(); // 空态与表格互斥:真正 0 账号→导入提示;有账号但筛选无结果→无匹配结果;否则→表格 const emptyEl = $('#pbEmpty'); const tableEl = $('#pbTable'); if (total === 0) { emptyEl.innerHTML = '暂无 GPT 账号点击「导入账号」粘贴 sub2api.json、每行 email----password----clientId----refreshToken,或每行一个 access token。'; emptyEl.hidden = false; tableEl.hidden = true; $('#pbPageInfo').textContent = `第 1 / 1 页,共 0 条`; $('#pbCheckAll').checked = false; renderStats(); return; } if (list.length === 0) { emptyEl.innerHTML = '无匹配结果当前搜索/筛选条件下没有账号。'; emptyEl.hidden = false; tableEl.hidden = true; $('#pbPageInfo').textContent = `第 1 / 1 页,共 0 条`; $('#pbCheckAll').checked = false; renderStats(); return; } emptyEl.hidden = true; tableEl.hidden = false; pg.items.forEach((a, i) => { const sel = state.probeSelected.has(a.id); const showPw = state.probeShowPw.has(a.id); const planKey = String(a.planType || '').toLowerCase(); const planCls = ['free', 'plus', 'pro'].includes(planKey) ? planKey : ''; const atCell = a.hasAccess ? `有 AT ⧉` : ''; const rtCell = a.hasRefresh ? `有 ⧉` : ''; const subCell = a.subOk ? '' : ''; const tr = document.createElement('tr'); tr.innerHTML = ` ${pg.startIdx + i + 1}
${escapeHtml(a.planLabel || '未知')} ${a.opened ? `已开通` : ''}
${a.note ? `
${escapeHtml(a.note)}
` : ''}
${atCell}${a.hasAccess ? formatTokenExp(a) : ''}
${rtCell} ${subCell} ${statusPill(a)}
`; tbody.appendChild(tr); }); $('#pbPageInfo').textContent = `第 ${pg.page} / ${pg.pages} 页,共 ${pg.total} 条`; $('#pbCheckAll').checked = pg.items.length > 0 && pg.items.every((a) => state.probeSelected.has(a.id)); renderStats(); } // 导入 sub2api function pbParseCount(text) { text = String(text || '').trim(); if (!text) return 0; try { const obj = JSON.parse(text); const arr = Array.isArray(obj) ? obj : (obj && Array.isArray(obj.accounts) ? obj.accounts : []); return arr.filter((a) => a && (a.name || a.email || (a.credentials && (a.credentials.access_token || a.credentials.refresh_token)))).length; } catch { /* not JSON — count lines */ } const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; let n = 0; const seen = new Set(); const add = (k) => { if (k && !seen.has(k)) { seen.add(k); n++; } }; for (const raw of text.split(/\r?\n/)) { const line = raw.trim(); if (!line) continue; const delim = line.includes('----') ? '----' : line.includes('\t') ? '\t' : line.includes('|') ? '|' : null; if (delim) { const parts = line.split(delim).map((s) => s.trim()).filter(Boolean); if (parts.length >= 4 && emailRe.test(parts[0])) add('email:' + parts[0].toLowerCase()); continue; } const ws = line.split(/\s+/).filter(Boolean); if (ws.length >= 4 && emailRe.test(ws[0])) add('email:' + ws[0].toLowerCase()); else add('at:' + (ws.length === 1 ? ws[0] : line)); // 裸 access token } return n; } function updatePbParsePreview() { $('#pbParsePreview').textContent = `识别到 ${pbParseCount($('#pbImportText').value)} 个有效账号`; } function openPbImport() { $('#pbImportText').value = ''; updatePbParsePreview(); $('#pbImportModal').hidden = false; } function closePbImport() { $('#pbImportModal').hidden = true; } async function doPbImport() { const text = $('#pbImportText').value.trim(); if (!text) return toast('请粘贴 sub2api.json 或账号行', 'err'); try { const r = await api('POST', '/api/probe/accounts/import', { text }); closePbImport(); toast(`导入完成:新增 ${r.added}|更新 ${r.updated}|共 ${r.total}`, 'ok'); await loadProbe(true); if ((r.added + r.updated) > 0) setTimeout(() => loadProbe(true), 3000); } catch (e) { toast('导入失败:' + e.message, 'err'); } } // 编辑 function openPbEdit(id) { const a = state.probe.find((x) => String(x.id) === String(id)); if (!a) return toast('账号不存在', 'err'); $('#pbEditId').value = a.id; $('#pbEditEmail').value = a.email || ''; $('#pbEditPassword').value = a.password || ''; $('#pbEditTwoFactor').value = a.twoFactorSecret || ''; $('#pbEditAccessToken').value = a.accessToken || ''; $('#pbEditRefreshToken').value = a.refreshToken || ''; $('#pbEditAccountId').value = a.chatgptAccountId || ''; $('#pbEditNote').value = a.note || ''; $('#pbEditModal').hidden = false; } function closePbEdit() { $('#pbEditModal').hidden = true; } async function savePbEdit() { const id = $('#pbEditId').value; if (!id) return; try { await api('PUT', '/api/probe/accounts', { id, email: $('#pbEditEmail').value.trim(), password: $('#pbEditPassword').value.trim(), twoFactorSecret: $('#pbEditTwoFactor').value.trim(), accessToken: $('#pbEditAccessToken').value.trim(), refreshToken: $('#pbEditRefreshToken').value.trim(), chatgptAccountId: $('#pbEditAccountId').value.trim(), note: $('#pbEditNote').value.trim(), }); closePbEdit(); toast('已保存', 'ok'); loadProbe(true); } catch (e) { toast('保存失败:' + e.message, 'err'); } } async function pbRefreshOne(id) { try { toast('正在刷新 TOKEN…'); const r = await api('POST', '/api/probe/accounts/refresh', { id }); if (r.ok) toast('TOKEN 已刷新,状态:有效', 'ok'); else toast(`刷新失败:${r.error || 'unknown'}`, 'err'); loadProbe(true); } catch (e) { toast('刷新失败:' + e.message, 'err'); loadProbe(true); } } async function pbDeleteOne(id) { if (!(await confirmDialog('确认删除该 GPT 账号?', { title: '删除账号', okText: '删除', danger: true }))) return; try { const r = await api('DELETE', '/api/probe/accounts', { ids: [id] }); state.probeSelected.delete(id); toast(`已删除 ${r.removed} 个`, 'ok'); loadProbe(true); } catch (e) { toast('删除失败:' + e.message, 'err'); } } async function pbDeleteSelected() { const ids = Array.from(state.probeSelected); if (!ids.length) return toast('请先勾选要删除的账号', 'err'); if (!(await confirmDialog(`确认删除所选 ${ids.length} 个 GPT 账号?`, { title: '删除所选', okText: '删除', danger: true }))) return; try { const r = await api('DELETE', '/api/probe/accounts', { ids }); state.probeSelected.clear(); toast(`已删除 ${r.removed} 个`, 'ok'); loadProbe(true); } catch (e) { toast('删除失败:' + e.message, 'err'); } } async function pbDeleteAll() { if (!state.probe.length) return toast('没有账号可清空'); if (!(await confirmDialog(`确认清空全部 ${state.probe.length} 个 GPT 账号?`, { title: '清空账号', okText: '清空', danger: true }))) return; try { const r = await api('DELETE', '/api/probe/accounts', { all: true }); state.probeSelected.clear(); toast(`已清空(删除 ${r.removed} 个)`, 'ok'); loadProbe(true); } catch (e) { toast('清空失败:' + e.message, 'err'); } } // 删除失效(status==='invalid')/删除过期(tokenExpired===true):前端按 enriched 字段收集 id,走通用 DELETE async function pbDeleteByFilter(kind) { let ids; const label = kind === 'invalid' ? '失效' : '过期'; if (kind === 'invalid') ids = state.probe.filter((a) => a.status === 'invalid').map((a) => a.id); else ids = state.probe.filter((a) => a.tokenExpired === true).map((a) => a.id); if (!ids.length) return toast('没有符合条件的账号'); if (!(await confirmDialog(`确认删除 ${ids.length} 个${label}账号?`, { title: `删除${label}账号`, okText: '删除', danger: true }))) return; try { const r = await api('DELETE', '/api/probe/accounts', { ids }); ids.forEach((id) => state.probeSelected.delete(id)); toast(`已删除 ${r.removed} 个${label}账号`, 'ok'); loadProbe(true); } catch (e) { toast('删除失败:' + e.message, 'err'); } } // 批量检测:≤5 线程实时套餐校验(选中优先,否则全部) let detectPoll = null; async function pbDetectBatch() { const ids = Array.from(state.probeSelected); const body = ids.length ? { ids } : {}; const startTs = Date.now(); try { const r = await api('POST', '/api/probe/accounts/detect-batch', body); if (!r.ok) return toast(r.error || '无法启动批量检测', 'err'); toast(`已开始批量检测 ${r.total} 个账号`, 'ok'); startDetectPolling(startTs); } catch (e) { toast('启动失败:' + e.message, 'err'); } } function startDetectPolling(startTs) { const btn = $('#btnPbDetectBatch'); const statusEl = $('#pbDetectStatus'); if (btn) btn.disabled = true; if (detectPoll) clearInterval(detectPoll); const tick = async () => { try { const s = await api('GET', '/api/probe/accounts/detect-batch/status'); if (statusEl) { statusEl.hidden = false; statusEl.textContent = `批量检测 ${s.done}/${s.total}(成功 ${s.ok} · 失败 ${s.fail})`; } const finished = !s.running && s.finishedAt && s.finishedAt >= (startTs - 2000); if (finished) { clearInterval(detectPoll); detectPoll = null; if (btn) btn.disabled = false; toast(`批量检测完成:成功 ${s.ok} / 共 ${s.total}`, 'ok'); await loadProbe(true); setTimeout(() => { if (statusEl) { statusEl.hidden = true; } }, 4000); } } catch (e) { /* 轮询期临时错误忽略,继续下次 */ } }; tick(); detectPoll = setInterval(tick, 1200); } // 工具栏下拉:点击切换、外部点击/Esc 关闭 function initDropdowns() { const dds = Array.from(document.querySelectorAll('.am-dropdown')); const closeAll = () => dds.forEach((o) => { o.classList.remove('open'); const t = o.querySelector('.am-dropdown-toggle'); if (t) t.setAttribute('aria-expanded', 'false'); }); dds.forEach((dd) => { const toggle = dd.querySelector('.am-dropdown-toggle'); if (!toggle) return; toggle.addEventListener('click', (e) => { e.stopPropagation(); const willOpen = !dd.classList.contains('open'); closeAll(); if (willOpen) { dd.classList.add('open'); toggle.setAttribute('aria-expanded', 'true'); } }); dd.querySelectorAll('.am-dropdown-menu button').forEach((b) => b.addEventListener('click', closeAll)); }); document.addEventListener('click', closeAll); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAll(); }); } function pbExportIds() { return state.probeSelected.size ? Array.from(state.probeSelected) : null; } async function pbExportAt() { if (!state.probe.length) return toast('没有可导出的账号'); try { const ts = new Date().toISOString().replace(/[:.]/g, '-'); const count = await downloadPost('/api/probe/accounts/export-at', { ids: pbExportIds() }, `probe_at_export_${ts}.txt`); toast(`已导出 ${count || 0} 个 AT(一行一个)`, 'ok'); } catch (e) { toast('导出失败:' + e.message, 'err'); } } async function pbExportSub() { if (!state.probe.length) return toast('没有可导出的账号'); try { const ts = new Date().toISOString().replace(/[:.]/g, '-'); const count = await downloadPost('/api/probe/accounts/export-sub', { ids: pbExportIds() }, `sub2api_export_${ts}.json`); toast(`已导出 ${count || 0} 个账号(SUB2API 格式)`, 'ok'); } catch (e) { toast('导出失败:' + e.message, 'err'); } } async function pbExportPw() { if (!state.probe.length) return toast('没有可导出的账号'); try { const ts = new Date().toISOString().replace(/[:.]/g, '-'); const count = await downloadPost('/api/probe/accounts/export-pw', { ids: pbExportIds() }, `probe_pw_export_${ts}.txt`); toast(`已导出 ${count || 0} 个账密(email----password)`, 'ok'); } catch (e) { toast('导出失败:' + e.message, 'err'); } } function updateBatchLabel() { const btn = $('#ddBatchToggle'); if (!btn) return; const n = state.probeSelected.size; btn.innerHTML = (n ? `批量操作(已选 ${n})` : '批量操作') + ' '; const rb = $('#btnPbRefetch'); if (rb) rb.textContent = n ? `重拿RT(${n})` : '重拿RT'; const bp = $('#btnPbBindPhone'); if (bp) bp.textContent = n ? `批量绑手机拿RT(${n})` : '批量绑手机拿RT'; } async function toggleAutoRefresh(enabled) { try { const ar = await api('POST', '/api/probe/accounts/auto-refresh', { enabled: !!enabled }); state.probeAutoRefresh = ar; renderAutoRefreshHint(); toast(ar.enabled ? '已开启自动刷新 TOKEN' : '已关闭自动刷新 TOKEN', 'ok'); } catch (e) { toast('切换失败:' + e.message, 'err'); renderAutoRefreshHint(); } } // ========================================================================== // 设置弹窗(代理池 + 接码) // ========================================================================== function setSettingsTab(name) { document.querySelectorAll('.am-set-tab').forEach(b => b.classList.toggle('active', b.dataset.setTab === name)); document.querySelectorAll('.am-set-panel').forEach(p => { p.hidden = p.dataset.setPanel !== name; }); } let mbEntries = []; function renderMbList(entries, count, unused) { if (Array.isArray(entries)) mbEntries = entries; const list = $('#mbList'); $('#setMbCount').textContent = `共 ${count != null ? count : mbEntries.length} 个(未使用 ${unused != null ? unused : mbEntries.filter(e => !e.used).length})`; if (!mbEntries.length) { list.innerHTML = '
邮箱池为空,粘贴后点「导入到列表」
'; return; } list.innerHTML = mbEntries.map(e => `
${escapeHtml(e.email)} ${e.used ? '已使用' : '未使用'} ${e.type === 'api' ? 'API' : 'IMAP'}
`).join(''); } async function importMailbox() { const text = $('#setMailbox').value.trim(); if (!text) { toast('请先粘贴 邮箱----凭证', 'err'); return; } try { const r = await api('POST', '/api/mailbox/import', { text }); renderMbList(r.entries, r.count, r.unused); $('#setMailbox').value = ''; $('#mbImportBox').hidden = true; toast(`已导入 ${r.added} 个(解析 ${r.parsed}),共 ${r.count} 个`, 'ok'); } catch (e) { toast('导入失败:' + e.message, 'err'); } } async function generateSplitAliases() { // 防呆:分裂读的是「已入库的邮箱池」,不读粘贴框。若批量导入框里还有未导入文本,用户多半是 // 粘完直接点了分裂——此时分裂会加到旧池、粘贴内容丢失(历史「邮箱加错了」的根因)。先拦下提示。 const pending = ($('#setMailbox').value || '').trim(); if (pending) { await confirmDialog('检测到「批量导入」框里有未导入的内容。请先点『导入到列表』把它们入库,再生成分裂别名。', { title: '请先导入', okText: '知道了', cancelText: '', danger: false, }); // 顺手展开导入框并聚焦,引导用户先导入。 $('#mbImportBox').hidden = false; setTimeout(() => $('#setMailbox').focus(), 0); return; } const n = Math.max(1, Math.min(200, Number($('#mbSplitCount').value) || 10)); try { const r = await api('POST', '/api/mailbox/split-generate', { count: n }); renderMbList(r.entries, r.count, r.unused); toast(`已生成 ${r.added} 个分裂邮箱`, 'ok'); } catch (e) { toast('生成失败:' + e.message, 'err'); } } async function clearAllMailbox() { if (!await confirmDialog('确定清空邮箱池的全部条目?此操作不可恢复。', { title: '清空邮箱池', okText: '清空', danger: true })) return; try { const r = await api('POST', '/api/mailbox/clear', {}); renderMbList(r.entries || [], r.count != null ? r.count : 0, r.unused != null ? r.unused : 0); toast('已清空邮箱池', 'ok'); } catch (e) { toast('清空失败:' + e.message, 'err'); } } let mbEditingEmail = ''; function editMailbox(email) { const e = mbEntries.find(x => (x.email || '').toLowerCase() === (email || '').toLowerCase()); if (!e) { toast('邮箱不存在', 'err'); return; } mbEditingEmail = e.email; $('#mbEditEmail').textContent = e.email; $('#mbEditCred').value = e.credential || ''; $('#mbEditUsed').checked = !!e.used; $('#mbEditModal').hidden = false; setTimeout(() => $('#mbEditCred').focus(), 0); } function closeMbEdit() { $('#mbEditModal').hidden = true; mbEditingEmail = ''; } async function saveMbEdit() { if (!mbEditingEmail) return; const credential = $('#mbEditCred').value.trim(); const used = $('#mbEditUsed').checked; try { await api('POST', '/api/mailbox/update', { email: mbEditingEmail, credential, used }); // 本地同步并重算未使用数 const e = mbEntries.find(x => (x.email || '').toLowerCase() === mbEditingEmail.toLowerCase()); if (e) { if (credential) { e.credential = credential; e.type = /^https?:\/\//i.test(credential) ? 'api' : 'imap'; } e.used = used; } renderMbList(mbEntries); closeMbEdit(); toast('已保存', 'ok'); } catch (e) { toast('更新失败:' + e.message, 'err'); } } async function deleteMailbox(email) { if (!await confirmDialog(`确定删除邮箱「${email}」?`, { title: '删除邮箱', danger: true })) return; try { const r = await api('POST', '/api/mailbox/delete', { email }); renderMbList(r.entries, r.count, r.unused); toast('已删除', 'ok'); } catch (e) { toast('删除失败:' + e.message, 'err'); } } // ========================================================================== // 协议注册(后台任务 + 右下角进度浮层) // ========================================================================== let regTimer = null; // 增量渲染去重用:记录已渲染日志的 key,避免每次轮询整体重绘(那是卡顿主因) const regLogKeys = new Set(); const REG_LOG_MAX_LINES = 500; function regLogKey(l) { return `${l.ts || 0}|${l.level || ''}|${l.account || ''}|${l.msg || ''}`; } function appendRegLogs(logs) { const box = $('#regLogs'); if (!box) return; const atBottom = box.scrollTop + box.clientHeight >= box.scrollHeight - 28; const frag = document.createDocumentFragment(); let added = 0; for (const l of (logs || [])) { const k = regLogKey(l); if (regLogKeys.has(k)) continue; regLogKeys.add(k); const t = new Date(l.ts || Date.now()).toLocaleTimeString('zh-CN', { hour12: false }); const lv = String(l.level || 'info').replace(/[^a-z]/gi, '') || 'info'; const div = document.createElement('div'); div.className = 'reg-log-line lv-' + lv; // 不再渲染账号邮箱(节省空间);后端日志对象仍带 account,仅前端不显示。 div.innerHTML = `${t} ${escapeHtml(l.msg || '')}`; frag.appendChild(div); added++; } if (!added) return; box.appendChild(frag); while (box.childElementCount > REG_LOG_MAX_LINES) box.removeChild(box.firstElementChild); if (atBottom) box.scrollTop = box.scrollHeight; // 用户上滚查看历史时不强制吸底 } async function clearRegLogs() { regLogKeys.clear(); const box = $('#regLogs'); if (box) box.innerHTML = ''; try { await api('POST', '/api/reg/logs/clear', {}); } catch (e) { /* 后端无此接口也无妨 */ } } // 注册方式切换:仅浏览器注册时显示「无头/有头」选择。 function syncRegMode() { const isBrowser = $('#regMode') && $('#regMode').value === 'browser'; if ($('#regHeadlessWrap')) $('#regHeadlessWrap').hidden = !isBrowser; } async function openRegModal() { $('#regUnused').textContent = '…'; syncRegMode(); $('#regModal').hidden = false; try { const s = await api('GET', '/api/reg/status'); $('#regUnused').textContent = s.unused != null ? s.unused : '?'; if (s.unused) $('#regCount').value = Math.min(Number($('#regCount').value) || 10, s.unused) || s.unused; } catch (e) { $('#regUnused').textContent = '?'; } } function closeRegModal() { $('#regModal').hidden = true; } async function startReg() { const count = Math.max(1, Number($('#regCount').value) || 1); const threads = Math.max(1, Math.min(20, Number($('#regThreads').value) || 1)); const mode = ($('#regMode') && $('#regMode').value) || 'protocol'; const headless = !($('#regHeadless') && $('#regHeadless').value === '0'); // 有头=false,其余无头 const label = mode === 'browser' ? '浏览器注册' : '协议注册'; try { const r = await api('POST', '/api/reg/start', { count, threads, mode, headless }); closeRegModal(); toast(`已开始${label}:${r.started} 个`, 'ok'); showRegPanel(); startRegPoll(); } catch (e) { toast('启动失败:' + e.message, 'err'); } } async function stopReg() { try { await api('POST', '/api/reg/stop', {}); toast('已请求停止,将在进行中的账号完成后结束', 'ok'); } catch (e) { toast('停止失败:' + e.message, 'err'); } } // 「重拿RT」:对勾选账号用 codex 客户端重新登录拿 RT(需要手机验证会自动接码),复用注册日志浮层。 async function refetchSelected() { const ids = Array.from(state.probeSelected); if (!ids.length) return toast('请先勾选要重拿 RT 的账号', 'err'); if (!(await confirmDialog(`确认对所选 ${ids.length} 个账号重拿 RT?将用 codex 重新登录(需要时自动接码)。`, { title: '重拿RT', okText: '开始' }))) return; const btn = $('#btnPbRefetch'); if (btn) { btn.disabled = true; } try { const r = await api('POST', '/api/probe/refetch-rt', { ids }); toast(`已开始重拿 RT:${r.started} 个`, 'ok'); showRegPanel(); startRegPoll(); } catch (e) { toast('启动失败:' + e.message, 'err'); } finally { if (btn) { btn.disabled = false; } } } // 「批量绑手机拿RT」:对「跳过手机注册」的待绑手机账号(勾选优先;不选则后端自动挑全部 phonePending) // 补做手机验证并拿 RT。底层与重拿RT同一 codex 登录流程,成功后 SUB=是、清 phonePending。复用注册日志浮层。 async function bindPhoneSelected() { const ids = Array.from(state.probeSelected); const tip = ids.length ? `确认对所选 ${ids.length} 个账号补绑手机并拿 RT?` : '未勾选账号:将对库里全部「待绑手机(无RT)」账号批量补绑手机拿 RT,确认?'; if (!(await confirmDialog(tip + '\n将用邮箱+固定密码重新登录,需要时自动接码(多国轮试)。', { title: '批量绑手机拿RT', okText: '开始' }))) return; const btn = $('#btnPbBindPhone'); if (btn) { btn.disabled = true; } try { const r = await api('POST', '/api/probe/bind-phone-rt', { ids }); toast(`已开始绑手机拿 RT:${r.started} 个`, 'ok'); showRegPanel(); startRegPoll(); } catch (e) { toast('启动失败:' + e.message, 'err'); } finally { if (btn) { btn.disabled = false; } } } function showRegPanel() { $('#regPanel').hidden = false; $('#regPanel').classList.remove('min'); } function renderRegStatus(s) { const total = s.total || 0, done = s.done || 0; const pct = total ? Math.round(done / total * 100) : 0; $('#regProgressBar').style.width = pct + '%'; $('#regPanelStat').textContent = `${done}/${total} · 成功 ${s.ok || 0} · 失败 ${s.fail || 0}${s.running ? '' : ' · 已结束'}`; appendRegLogs(s.logs || []); $('#regPanelStop').hidden = !s.running; $('#regPanelClose').hidden = !!s.running; } function startRegPoll() { if (regTimer) clearInterval(regTimer); const tick = async () => { try { const s = await api('GET', '/api/reg/status'); renderRegStatus(s); if (!s.running) { clearInterval(regTimer); regTimer = null; loadProbe(); } } catch (e) { /* 忽略单次轮询错误 */ } }; tick(); regTimer = setInterval(tick, 1500); } async function resumeRegPanel() { // 页面加载/切换回来时,若后台有任务在跑或刚结束,恢复浮层 try { const s = await api('GET', '/api/reg/status'); if (s.running || (s.total && s.logs && s.logs.length)) { showRegPanel(); if (s.running) startRegPoll(); else renderRegStatus(s); } } catch (e) { /* ignore */ } } let smsAllCfg = {}; function fillSmsFields(v) { v = v || {}; $('#setSmsApiKey').value = v.apiKey || ''; $('#setSmsBaseUrl').value = v.baseUrl || ''; $('#setSmsCountry').value = (v.country === 0 || v.country) ? v.country : ''; $('#setSmsMaxPrice').value = v.maxPrice || ''; } async function testSmsBalance() { const el = $('#smsBalanceResult'); el.className = 'am-sms-bal'; el.textContent = '查询中…'; try { const r = await api('POST', '/api/sms/balance', { provider: $('#setSmsProvider').value, apiKey: $('#setSmsApiKey').value.trim(), baseUrl: $('#setSmsBaseUrl').value.trim(), }); if (r.ok) { el.className = 'am-sms-bal ok'; el.textContent = '余额:' + r.balance; } else { el.className = 'am-sms-bal err'; el.textContent = '失败:' + (r.error || '未知错误'); } } catch (e) { el.className = 'am-sms-bal err'; el.textContent = '失败:' + e.message; } } async function openSettings() { try { const cfg = await api('GET', '/api/account-config'); $('#setProxies').value = (cfg.proxies || []).join('\n'); $('#setProxyCount').textContent = `共 ${cfg.proxyCount || 0} 条`; const sms = cfg.sms || {}; smsAllCfg = cfg.smsAll || {}; $('#setSmsProvider').value = sms.provider || 'smscode'; fillSmsFields(sms); $('#setSmsPollTimeout').value = sms.pollTimeout || 180000; $('#smsBalanceResult').textContent = ''; const mb = cfg.mailbox || {}; $('#setMailbox').value = ''; $('#mbImportBox').hidden = true; $('#setMbSplit').checked = !!mb.split; renderMbList(mb.entries || [], mb.count, mb.unused); $('#mbTestResult').hidden = true; $('#mbTestResult').innerHTML = ''; const cloak = cfg.cloak || {}; if ($('#setCloakKey')) $('#setCloakKey').value = cloak.licenseKey || ''; if ($('#setRegPassword')) $('#setRegPassword').value = cloak.regPassword || ''; setSettingsTab('proxy'); $('#settingsModal').hidden = false; } catch (e) { toast('读取设置失败:' + e.message, 'err'); } } function closeSettings() { $('#settingsModal').hidden = true; } async function saveSettings() { try { const r = await api('POST', '/api/account-config', { proxies: $('#setProxies').value, sms: { provider: $('#setSmsProvider').value, apiKey: $('#setSmsApiKey').value.trim(), baseUrl: $('#setSmsBaseUrl').value.trim(), country: $('#setSmsCountry').value.trim(), maxPrice: $('#setSmsMaxPrice').value.trim(), pollTimeout: Number($('#setSmsPollTimeout').value) || 180000, }, mailbox: { split: $('#setMbSplit').checked, }, cloak: { licenseKey: ($('#setCloakKey') && $('#setCloakKey').value.trim()) || '', regPassword: ($('#setRegPassword') && $('#setRegPassword').value.trim()) || '', }, }); const bad = (r.proxyBad || []).length; toast(`已保存${r.proxySaved ? `:代理 ${r.proxyCount} 条` : ''}${bad ? `(忽略无效 ${bad} 行)` : ''}${r.smsSaved ? ',接码已更新' : ''}${r.mailboxSaved ? ',邮箱池设置已更新' : ''}${r.cloakSaved ? ',浏览器KEY已更新' : ''}`, 'ok'); $('#setProxyCount').textContent = `共 ${r.proxyCount || 0} 条`; closeSettings(); } catch (e) { toast('保存失败:' + e.message, 'err'); } } async function testMailbox(email) { email = (email || '').trim(); if (!email) { toast('请选择一个邮箱', 'err'); return; } const box = $('#mbTestResult'); box.hidden = false; box.innerHTML = '
正在测试收件…
'; try { const r = await api('POST', '/api/mailbox/test', { email }); if (!r.ok) { box.innerHTML = `
测试失败:${escapeHtml(r.error || '未知错误')}
`; return; } const msgs = r.messages || []; const head = `
✓ 连接成功(${escapeHtml(r.type === 'api' ? '取件API' : (r.host || 'IMAP'))}),最新 ${msgs.length} 封
`; const rows = msgs.length ? msgs.map(m => `
${escapeHtml(m.subject || '(无主题)')}
${escapeHtml(m.from || '')} ${m.date ? '· ' + escapeHtml(m.date) : ''}
${m.code ? `
验证码 ${escapeHtml(m.code)}
` : ''}
`).join('') : '
收件箱暂无邮件
'; box.innerHTML = head + rows; } catch (e) { box.innerHTML = `
测试失败:${escapeHtml(e.message)}
`; } } // ========================================================================== // PLUS 邮箱(保留原有:查收 / 拿RT / 删除封号) // ========================================================================== function plMatchFilter(a) { switch (state.plFilter) { case 'rt': return !!a.codexRt; case 'nort': return !a.codexRt && !(a.banned === true || a.codexStatus === 'banned'); case 'banned': return a.banned === true || a.codexStatus === 'banned'; default: return true; } } function plFiltered() { const q = state.plSearch.trim().toLowerCase(); return state.plus.filter((a) => { if (!plMatchFilter(a)) return false; if (!q) return true; return a.email.toLowerCase().includes(q) || domainOf(a.email).toLowerCase().includes(q) || (a.note || '').toLowerCase().includes(q); }); } function plPageItems() { return paginate(plFiltered(), state.plPage, state.plPageSize).items; } async function loadPlus() { try { const { accounts } = await api('GET', '/api/plus'); state.plus = accounts || []; renderPlus(); } catch (e) { toast('加载失败:' + e.message, 'err'); } } function renderPlus() { const list = plFiltered(); const pg = paginate(list, state.plPage, state.plPageSize); state.plPage = pg.page; const tbody = $('#plRows'); tbody.innerHTML = ''; $('#plEmpty').hidden = state.plus.length !== 0; pg.items.forEach((a, i) => { const key = a.email.toLowerCase(); const isSel = state.plSelected.has(key); const showPw = state.plShowPw.has(key); const absIdx = pg.startIdx + i + 1; const rtHtml = a.codexRt ? `✔ 有` : (a.banned === true || a.codexStatus === 'banned') ? '⛔ 已封号' : '— 无'; const tr = document.createElement('tr'); tr.innerHTML = ` ${absIdx}
${escapeHtml(a.email)}
${a.note ? `
${escapeHtml(a.note)}
` : ''}
${showPw ? escapeHtml(a.password) : '••••••••'}
${rtHtml}
`; tbody.appendChild(tr); }); $('#plPageInfo').textContent = `第 ${pg.page} / ${pg.pages} 页,共 ${pg.total} 条`; $('#plCheckAll').checked = plPageItems().length > 0 && plPageItems().every((a) => state.plSelected.has(a.email.toLowerCase())); renderStats(); } function localParsePlus(text) { const out = [], seen = new Set(); for (const raw of String(text).split(/\r?\n/)) { const line = raw.trim(); if (!line) continue; let parts = line.includes('----') ? line.split('----') : line.includes('\t') ? line.split('\t') : line.includes('|') ? line.split('|') : line.split(/\s+/); parts = parts.map((s) => s.trim()).filter(Boolean); if (parts.length < 4) continue; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(parts[0])) continue; const k = parts[0].toLowerCase(); if (seen.has(k)) continue; seen.add(k); out.push(parts[0]); } return out; } function updatePlParsePreview() { $('#plParsePreview').textContent = `识别到 ${localParsePlus($('#plImportText').value).length} 个有效账号`; } function openPlImport() { $('#plImportText').value = ''; updatePlParsePreview(); $('#plImportModal').hidden = false; } function closePlImport() { $('#plImportModal').hidden = true; } async function doPlImport() { const text = $('#plImportText').value; if (!localParsePlus(text).length) return toast('未识别到有效账号', 'err'); try { const r = await api('POST', '/api/plus/import', { text }); closePlImport(); await loadPlus(); toast(`导入完成:新增 ${r.added},更新 ${r.updated}`, 'ok'); } catch (e) { toast('导入失败:' + e.message, 'err'); } } async function plDelete(emails, msg) { if (!emails.length) return; if (!(await confirmDialog(msg || `确认删除选中的 ${emails.length} 个账号?此操作不可恢复。`, { title: '删除账号', okText: '删除', danger: true }))) return; try { const r = await api('DELETE', '/api/plus', { emails }); emails.forEach((e) => { const k = e.toLowerCase(); state.plSelected.delete(k); state.plCodes.delete(k); state.plStatusMap.delete(k); }); await loadPlus(); toast(`已删除 ${r.removed} 个账号`, 'ok'); } catch (e) { toast('删除失败:' + e.message, 'err'); } } function plDeleteBanned() { const banned = state.plus.filter((a) => a.banned === true || a.codexStatus === 'banned'); if (!banned.length) return toast('没有已封号的账号'); plDelete(banned.map((a) => a.email), `检测到 ${banned.length} 个已封号账号,确认全部删除?`); } let rtPoll = null; async function plGetRT() { const targets = state.plSelected.size ? state.plus.filter((a) => state.plSelected.has(a.email.toLowerCase())) : plFiltered(); if (!targets.length) return toast('没有可拿 RT 的账号'); if (!(await confirmDialog(`将对 ${targets.length} 个账号复刻 Codex OAuth 授权流程换取 refresh_token。是否开始?`, { title: '批量拿 RT', okText: '开始' }))) return; try { const r = await api('POST', '/api/plus/get-rt', { emails: targets.map((a) => a.email) }); $('#rtPanel').hidden = false; $('#rtLogs').innerHTML = ''; toast(`已开始拿 RT ${r.started} 个账号`, 'ok'); startRtPolling(); } catch (e) { toast('启动失败:' + e.message, 'err'); } } function startRtPolling() { if (rtPoll) clearInterval(rtPoll); let ticks = 0; const tick = async () => { ticks++; try { const s = await api('GET', '/api/plus/rt-status'); $('#rtSummary').textContent = `进度 ${s.done}/${s.total} · 成功 ${s.ok} · 封号 ${s.banned} · 失败 ${s.fail}`; const box = $('#rtLogs'); box.innerHTML = (s.logs || []).map((l) => `
${escapeHtml(fmtTime(l.ts))} ${l.account ? '[' + escapeHtml(l.account) + '] ' : ''}${escapeHtml(l.msg)}
`).join(''); box.scrollTop = box.scrollHeight; if (state.activeTab === 'plus') await loadPlus(); if (!s.running && ticks > 1) { clearInterval(rtPoll); rtPoll = null; } } catch { /* ignore */ } if (ticks > 300 && rtPoll) { clearInterval(rtPoll); rtPoll = null; } }; tick(); rtPoll = setInterval(tick, 3000); } async function plExport() { const base = state.plSelected.size ? state.plus.filter((a) => state.plSelected.has(a.email.toLowerCase())) : plFiltered(); const withRt = base.filter((a) => typeof a.codexRt === 'string' && a.codexRt); if (!withRt.length) return toast('没有可导出的账号(需先批量拿RT)'); try { const ts = new Date().toISOString().replace(/[:.]/g, '-'); const count = await downloadPost('/api/plus/export', { emails: withRt.map((a) => a.email) }, `sub2api_export_${ts}.json`); toast(`已导出 ${count || withRt.length} 个账号(SUB2API 格式)`, 'ok'); } catch (e) { toast('导出失败:' + e.message, 'err'); } } // 查收 let currentInboxEmail = ''; function openInbox(email) { currentInboxEmail = email; $('#inboxAddr').textContent = email; $('#inboxBody').innerHTML = '
正在查收邮件…
'; $('#inboxDrawer').hidden = false; refreshInbox(); } function closeInbox() { $('#inboxDrawer').hidden = true; currentInboxEmail = ''; } async function refreshInbox() { const email = currentInboxEmail; if (!email) return; $('#inboxBody').innerHTML = '
正在查收邮件…
'; try { const { messages } = await api('POST', '/api/plus/inbox', { email }); renderInbox(messages); const withCode = (messages || []).find((m) => m.extracted_code); if (withCode) state.plCodes.set(email.toLowerCase(), withCode.extracted_code); } catch (e) { $('#inboxBody').innerHTML = `
${escapeHtml(e.message)}
`; toast('查收失败:' + e.message, 'err'); } } function renderInbox(messages) { const body = $('#inboxBody'); if (!messages || !messages.length) { body.innerHTML = '
当前收件箱暂无邮件。
请在目标平台触发验证码后点击「刷新」。
'; return; } body.innerHTML = ''; messages.forEach((m) => { const item = document.createElement('div'); item.className = 'msg-item'; const bodyText = m.body || m.snippet || ''; item.innerHTML = `
${escapeHtml(m.from_addr || '(未知发送者)')}
${escapeHtml(m.subject || '(无主题)')}
${escapeHtml(fmtTime(m.received_at))}
${m.extracted_code ? `
${escapeHtml(m.extracted_code)} ⧉
` : ''} ${bodyText ? `
${escapeHtml(bodyText)}
` : ''}`; body.appendChild(item); }); } // ========================================================================== // 事件绑定 // ========================================================================== function bind() { $('#btnSettings').addEventListener('click', openSettings); $('#settingsClose').addEventListener('click', closeSettings); $('#settingsCancel').addEventListener('click', closeSettings); $('#btnSaveSettings').addEventListener('click', saveSettings); document.querySelectorAll('.am-set-tab').forEach(b => b.addEventListener('click', () => setSettingsTab(b.dataset.setTab))); $('#setSmsProvider').addEventListener('change', (e) => { fillSmsFields(smsAllCfg[e.target.value] || {}); $('#smsBalanceResult').textContent = ''; }); $('#smsBalanceBtn').addEventListener('click', testSmsBalance); $('#mbSplitGen').addEventListener('click', generateSplitAliases); $('#mbToggleImport').addEventListener('click', () => { const box = $('#mbImportBox'); box.hidden = !box.hidden; if (!box.hidden) setTimeout(() => $('#setMailbox').focus(), 0); }); $('#mbImportBtn').addEventListener('click', importMailbox); $('#mbClearAll').addEventListener('click', clearAllMailbox); $('#mbList').addEventListener('click', (e) => { const t = e.target.closest('[data-mb-test],[data-mb-edit],[data-mb-del]'); if (!t) return; if (t.dataset.mbTest) testMailbox(t.dataset.mbTest); else if (t.dataset.mbEdit) editMailbox(t.dataset.mbEdit); else if (t.dataset.mbDel) deleteMailbox(t.dataset.mbDel); }); $('#mbEditSave').addEventListener('click', saveMbEdit); $('#mbEditCancel').addEventListener('click', closeMbEdit); $('#mbEditCloseX').addEventListener('click', closeMbEdit); $('#mbEditModal').addEventListener('mousedown', (e) => { if (e.target === $('#mbEditModal')) closeMbEdit(); }); $('#mbTestResult').addEventListener('click', (e) => { const c = e.target.closest('[data-copy]'); if (c) copyText(c.dataset.copy, '已复制验证码'); }); // GPT toolbar $('#pbAutoRefresh').addEventListener('change', (e) => toggleAutoRefresh(e.target.checked)); $('#pbOpenedOnly').addEventListener('change', (e) => { state.pbOpenedOnly = e.target.checked; state.pbPage = 1; renderProbe(); }); // 协议注册:占位功能,仅提示(不接入任何注册后端) $('#btnPbRefetch').addEventListener('click', refetchSelected); if ($('#btnPbBindPhone')) $('#btnPbBindPhone').addEventListener('click', bindPhoneSelected); $('#btnProtocolReg').addEventListener('click', openRegModal); if ($('#regMode')) $('#regMode').addEventListener('change', syncRegMode); $('#regCloseX').addEventListener('click', closeRegModal); $('#regCancel').addEventListener('click', closeRegModal); $('#regStart').addEventListener('click', startReg); $('#regPanelMin').addEventListener('click', () => $('#regPanel').classList.toggle('min')); $('#regPanelStop').addEventListener('click', stopReg); $('#regPanelClear').addEventListener('click', clearRegLogs); $('#regPanelClose').addEventListener('click', () => { $('#regPanel').hidden = true; }); $('#btnPbImport').addEventListener('click', openPbImport); $('#pbImportText').addEventListener('input', updatePbParsePreview); $('#btnDoPbImport').addEventListener('click', doPbImport); $('#pbImportClose').addEventListener('click', closePbImport); $('#pbImportCancel').addEventListener('click', closePbImport); $('#btnPbDetectBatch').addEventListener('click', pbDetectBatch); $('#btnPbExportAt').addEventListener('click', pbExportAt); $('#btnPbExportSub').addEventListener('click', pbExportSub); $('#btnPbExportPw').addEventListener('click', pbExportPw); $('#btnPbDelSel').addEventListener('click', pbDeleteSelected); $('#btnPbDelAll').addEventListener('click', pbDeleteAll); $('#btnPbDelInvalid').addEventListener('click', () => pbDeleteByFilter('invalid')); $('#btnPbDelExpired').addEventListener('click', () => pbDeleteByFilter('expired')); initDropdowns(); $('#pbPageSize').addEventListener('change', (e) => { state.pbPageSize = Number(e.target.value); state.pbPage = 1; renderProbe(); }); $('#pbPagePrev').addEventListener('click', () => { state.pbPage -= 1; renderProbe(); }); $('#pbPageNext').addEventListener('click', () => { state.pbPage += 1; renderProbe(); }); $('#pbCheckAll').addEventListener('change', (e) => { const items = pbPageItems(); if (e.target.checked) items.forEach((a) => state.probeSelected.add(a.id)); else items.forEach((a) => state.probeSelected.delete(a.id)); renderProbe(); }); $('#pbRows').addEventListener('change', (e) => { const cb = e.target.closest('[data-pb-check]'); if (!cb) return; cb.checked ? state.probeSelected.add(cb.dataset.pbCheck) : state.probeSelected.delete(cb.dataset.pbCheck); renderStats(); $('#pbCheckAll').checked = pbPageItems().every((a) => state.probeSelected.has(a.id)); }); $('#pbRows').addEventListener('click', (e) => { const t = e.target.closest('[data-pb-detect],[data-pb-edit],[data-pb-refresh],[data-pb-del],[data-pb-copy-at],[data-pb-copy-rt],[data-pb-copy-email],[data-pb-toggle-pw],[data-pb-copy-pw]'); if (!t) return; const find = (id) => state.probe.find((x) => String(x.id) === String(id)); if (t.dataset.pbDetect) return pbDetectOne(t.dataset.pbDetect, t); if (t.dataset.pbEdit) return openPbEdit(t.dataset.pbEdit); if (t.dataset.pbRefresh) return pbRefreshOne(t.dataset.pbRefresh); if (t.dataset.pbDel) return pbDeleteOne(t.dataset.pbDel); if (t.dataset.pbCopyAt) return copyText(find(t.dataset.pbCopyAt).accessToken, '已复制 Access Token'); if (t.dataset.pbCopyRt) return copyText(find(t.dataset.pbCopyRt).refreshToken, '已复制 Refresh Token'); if (t.dataset.pbCopyEmail) return copyText(find(t.dataset.pbCopyEmail).email, '已复制邮箱'); if (t.dataset.pbTogglePw) { const id = t.dataset.pbTogglePw; state.probeShowPw.has(id) ? state.probeShowPw.delete(id) : state.probeShowPw.add(id); return renderProbe(); } if (t.dataset.pbCopyPw) return copyText(find(t.dataset.pbCopyPw).password, '已复制密码'); }); $('#pbEditClose').addEventListener('click', closePbEdit); $('#pbEditCancel').addEventListener('click', closePbEdit); $('#btnSavePbEdit').addEventListener('click', savePbEdit); // PLUS toolbar $('#plSearch').addEventListener('input', (e) => { state.plSearch = e.target.value; state.plPage = 1; renderPlus(); }); $('#plFilter').addEventListener('change', (e) => { state.plFilter = e.target.value; state.plPage = 1; renderPlus(); }); $('#plCheckAll').addEventListener('change', (e) => { const items = plPageItems(); if (e.target.checked) items.forEach((a) => state.plSelected.add(a.email.toLowerCase())); else items.forEach((a) => state.plSelected.delete(a.email.toLowerCase())); renderPlus(); }); $('#plPageSize').addEventListener('change', (e) => { state.plPageSize = Number(e.target.value); state.plPage = 1; renderPlus(); }); $('#plPagePrev').addEventListener('click', () => { state.plPage -= 1; renderPlus(); }); $('#plPageNext').addEventListener('click', () => { state.plPage += 1; renderPlus(); }); $('#plRows').addEventListener('change', (e) => { const cb = e.target.closest('[data-pl-check]'); if (!cb) return; const k = cb.dataset.plCheck.toLowerCase(); cb.checked ? state.plSelected.add(k) : state.plSelected.delete(k); renderStats(); }); $('#plRows').addEventListener('click', (e) => { const t = e.target.closest('[data-copy],[data-pl-toggle-pw],[data-pl-copy-pw],[data-pl-copy-codexrt],[data-pl-inbox],[data-pl-del]'); if (!t) return; const find = (email) => state.plus.find((x) => x.email.toLowerCase() === String(email).toLowerCase()); if (t.dataset.copy) return copyText(t.dataset.copy, '已复制邮箱'); if (t.dataset.plTogglePw) { const k = t.dataset.plTogglePw.toLowerCase(); state.plShowPw.has(k) ? state.plShowPw.delete(k) : state.plShowPw.add(k); return renderPlus(); } if (t.dataset.plCopyPw) return copyText(find(t.dataset.plCopyPw).password, '已复制密码'); if (t.dataset.plCopyCodexrt) return copyText(find(t.dataset.plCopyCodexrt).codexRt, '已复制 Codex RT'); if (t.dataset.plInbox) return openInbox(t.dataset.plInbox); if (t.dataset.plDel) return plDelete([t.dataset.plDel]); }); $('#btnPlImport').addEventListener('click', openPlImport); $('#plImportText').addEventListener('input', updatePlParsePreview); $('#btnDoPlImport').addEventListener('click', doPlImport); $('#plImportClose').addEventListener('click', closePlImport); $('#plImportCancel').addEventListener('click', closePlImport); $('#btnPlGetRT').addEventListener('click', plGetRT); $('#btnPlExport').addEventListener('click', plExport); $('#btnPlDelete').addEventListener('click', () => { if (!state.plSelected.size) return toast('请先选择账号'); plDelete(Array.from(state.plSelected)); }); $('#btnPlDeleteBanned').addEventListener('click', plDeleteBanned); $('#inboxClose').addEventListener('click', closeInbox); $('#inboxMask').addEventListener('click', closeInbox); $('#inboxRefresh').addEventListener('click', refreshInbox); $('#inboxBody').addEventListener('click', (e) => { const c = e.target.closest('[data-copy-code]'); if (c) copyText(c.dataset.copyCode, '已复制验证码'); }); $('#rtPanelClose').addEventListener('click', () => { $('#rtPanel').hidden = true; if (rtPoll) { clearInterval(rtPoll); rtPoll = null; } }); } bind(); switchTab('gpt'); resumeRegPanel(); })();