mirror of
https://wget.la/https://github.com/432539/gpt
synced 2026-08-17 03:26:59 +08:00
只开源「GPT 账号注册机 / 管理」(account-manager):账号列表/导入导出、检测/批量检测、 拿 RT、协议/浏览器注册等注册机能力。批量直开 / 协议支付 / 提取链接 / 直卡直开 等其它功能 不含实现代码,仅通过 iframe 内嵌外链访问。真实机密均由 .gitignore 忽略,仅提供脱敏示例配置。
1242 lines
64 KiB
JavaScript
1242 lines
64 KiB
JavaScript
/* 账号管理 —— 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<boolean>
|
||
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 '<span class="muted">未知</span>';
|
||
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 `<div class="pb-exp-cell${expired ? ' expired' : ''}"><div class="pb-exp-main">${escapeHtml(main)}</div><div class="pb-exp-sub">${escapeHtml(sub)}</div></div>`;
|
||
}
|
||
function statusPill(a) {
|
||
const s = a.status || 'unknown';
|
||
const label = { ok: '有效', expiring: '临期', invalid: '失效', unknown: '未知' }[s] || '未知';
|
||
return `<span class="pb-status ${s}">${label}</span>`;
|
||
}
|
||
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 = '<b>暂无 GPT 账号</b><span>点击「导入账号」粘贴 sub2api.json、每行 email----password----clientId----refreshToken,或每行一个 access token。</span>';
|
||
emptyEl.hidden = false;
|
||
tableEl.hidden = true;
|
||
$('#pbPageInfo').textContent = `第 1 / 1 页,共 0 条`;
|
||
$('#pbCheckAll').checked = false;
|
||
renderStats();
|
||
return;
|
||
}
|
||
if (list.length === 0) {
|
||
emptyEl.innerHTML = '<b>无匹配结果</b><span>当前搜索/筛选条件下没有账号。</span>';
|
||
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
|
||
? `<span class="at-chip has" data-pb-copy-at="${escapeHtml(a.id)}" title="点击复制 Access Token">有 AT ⧉</span>`
|
||
: '<span class="at-chip none">无</span>';
|
||
const rtCell = a.hasRefresh
|
||
? `<span class="rt-chip has" data-pb-copy-rt="${escapeHtml(a.id)}" title="点击复制 Refresh Token">有 ⧉</span>`
|
||
: '<span class="rt-chip none">无</span>';
|
||
const subCell = a.subOk
|
||
? '<span class="sub-chip yes" title="第二阶段 SUB / sub2api 授权成功">是</span>'
|
||
: '<span class="sub-chip no" title="未取得 SUB 授权">否</span>';
|
||
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td class="col-check"><input type="checkbox" data-pb-check="${escapeHtml(a.id)}" ${sel ? 'checked' : ''}></td>
|
||
<td class="col-idx">${pg.startIdx + i + 1}</td>
|
||
<td>
|
||
<div class="email-cell">
|
||
<span class="text-mono am-email" title="${escapeHtml(a.email || '(无邮箱)')}">${escapeHtml(a.email || '(无邮箱)')}</span>
|
||
<span class="pb-plan-tag ${planCls}">${escapeHtml(a.planLabel || '未知')}</span>
|
||
${a.opened ? `<span class="pb-opened-tag" title="批量直开已开通${a.activatedAt ? ':' + fmtTime(a.activatedAt) : ''}">已开通</span>` : ''}
|
||
</div>
|
||
${a.note ? `<div class="muted" style="font-size:12px">${escapeHtml(a.note)}</div>` : ''}
|
||
</td>
|
||
<td><div class="at-cell">${atCell}${a.hasAccess ? formatTokenExp(a) : ''}</div></td>
|
||
<td>${rtCell}</td>
|
||
<td class="col-sub">${subCell}</td>
|
||
<td>${statusPill(a)}</td>
|
||
<td class="col-ops">
|
||
<div class="row-ops">
|
||
<button class="op-link detect" data-pb-detect="${escapeHtml(a.id)}" title="通过 AT 检测真实套餐">检测</button>
|
||
<button class="op-link" data-pb-edit="${escapeHtml(a.id)}">编辑</button>
|
||
<button class="op-link" data-pb-refresh="${escapeHtml(a.id)}">刷新TOKEN</button>
|
||
<button class="op-link danger" data-pb-del="${escapeHtml(a.id)}">删除</button>
|
||
</div>
|
||
</td>`;
|
||
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})` : '批量操作') + ' <span class="am-caret">▾</span>';
|
||
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 = '<div class="am-mb-empty">邮箱池为空,粘贴后点「导入到列表」</div>'; return; }
|
||
list.innerHTML = mbEntries.map(e => `
|
||
<div class="am-mb-row" data-email="${escapeHtml(e.email)}">
|
||
<span class="am-mb-addr" title="${escapeHtml(e.email)}">${escapeHtml(e.email)}</span>
|
||
<span class="am-mb-badge ${e.used ? 'used' : 'free'}">${e.used ? '已使用' : '未使用'}</span>
|
||
<span class="am-mb-tag">${e.type === 'api' ? 'API' : 'IMAP'}</span>
|
||
<span class="am-mb-ops">
|
||
<button class="op-link" data-mb-test="${escapeHtml(e.email)}">测试</button>
|
||
<button class="op-link" data-mb-edit="${escapeHtml(e.email)}">编辑</button>
|
||
<button class="op-link danger" data-mb-del="${escapeHtml(e.email)}">删除</button>
|
||
</span>
|
||
</div>`).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 = `<span class="reg-log-ts">${t}</span> <span class="reg-log-msg">${escapeHtml(l.msg || '')}</span>`;
|
||
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 = '<div class="am-mb-loading">正在测试收件…</div>';
|
||
try {
|
||
const r = await api('POST', '/api/mailbox/test', { email });
|
||
if (!r.ok) { box.innerHTML = `<div class="am-mb-empty">测试失败:${escapeHtml(r.error || '未知错误')}</div>`; return; }
|
||
const msgs = r.messages || [];
|
||
const head = `<div class="am-mb-head">✓ 连接成功(${escapeHtml(r.type === 'api' ? '取件API' : (r.host || 'IMAP'))}),最新 ${msgs.length} 封</div>`;
|
||
const rows = msgs.length ? msgs.map(m => `
|
||
<div class="am-mb-item">
|
||
<div class="am-mb-subj">${escapeHtml(m.subject || '(无主题)')}</div>
|
||
<div class="am-mb-meta">${escapeHtml(m.from || '')} ${m.date ? '· ' + escapeHtml(m.date) : ''}</div>
|
||
${m.code ? `<div class="am-mb-code">验证码 <b>${escapeHtml(m.code)}</b> <button class="op-link" data-copy="${escapeHtml(m.code)}">复制</button></div>` : ''}
|
||
</div>`).join('') : '<div class="am-mb-empty">收件箱暂无邮件</div>';
|
||
box.innerHTML = head + rows;
|
||
} catch (e) { box.innerHTML = `<div class="am-mb-empty">测试失败:${escapeHtml(e.message)}</div>`; }
|
||
}
|
||
|
||
// ==========================================================================
|
||
// 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
|
||
? `<span class="rt-chip has" data-pl-copy-codexrt="${escapeHtml(a.email)}" title="点击复制 Codex Refresh Token">✔ 有</span>`
|
||
: (a.banned === true || a.codexStatus === 'banned')
|
||
? '<span class="pb-status invalid">⛔ 已封号</span>'
|
||
: '<span class="rt-chip none">— 无</span>';
|
||
const tr = document.createElement('tr');
|
||
tr.innerHTML = `
|
||
<td class="col-check"><input type="checkbox" data-pl-check="${escapeHtml(a.email)}" ${isSel ? 'checked' : ''}></td>
|
||
<td class="col-idx">${absIdx}</td>
|
||
<td>
|
||
<div class="email-cell"><span class="text-mono">${escapeHtml(a.email)}</span>
|
||
<button class="icon-btn" data-copy="${escapeHtml(a.email)}" title="复制邮箱">⧉</button></div>
|
||
${a.note ? `<div class="muted" style="font-size:12px">${escapeHtml(a.note)}</div>` : ''}
|
||
</td>
|
||
<td>
|
||
<div class="pw-cell">
|
||
<span class="${showPw ? 'pw-text' : 'pw-mask'}">${showPw ? escapeHtml(a.password) : '••••••••'}</span>
|
||
<button class="icon-btn" data-pl-toggle-pw="${escapeHtml(a.email)}" title="显示/隐藏">${showPw ? '🙈' : '👁'}</button>
|
||
<button class="icon-btn" data-pl-copy-pw="${escapeHtml(a.email)}" title="复制密码">⧉</button>
|
||
</div>
|
||
</td>
|
||
<td><span class="muted">—</span></td>
|
||
<td>${rtHtml}</td>
|
||
<td class="col-ops">
|
||
<div class="row-ops">
|
||
<button class="op-link" data-pl-inbox="${escapeHtml(a.email)}">查收</button>
|
||
<button class="op-link danger" data-pl-del="${escapeHtml(a.email)}">删除</button>
|
||
</div>
|
||
</td>`;
|
||
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) => `<div class="am-rt-line ${escapeHtml(l.level)}">${escapeHtml(fmtTime(l.ts))} ${l.account ? '[' + escapeHtml(l.account) + '] ' : ''}${escapeHtml(l.msg)}</div>`).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 = '<div class="drawer-loading">正在查收邮件…</div>'; $('#inboxDrawer').hidden = false; refreshInbox(); }
|
||
function closeInbox() { $('#inboxDrawer').hidden = true; currentInboxEmail = ''; }
|
||
async function refreshInbox() {
|
||
const email = currentInboxEmail; if (!email) return;
|
||
$('#inboxBody').innerHTML = '<div class="drawer-loading">正在查收邮件…</div>';
|
||
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 = `<div class="drawer-empty">${escapeHtml(e.message)}</div>`; toast('查收失败:' + e.message, 'err'); }
|
||
}
|
||
function renderInbox(messages) {
|
||
const body = $('#inboxBody');
|
||
if (!messages || !messages.length) { body.innerHTML = '<div class="drawer-empty">当前收件箱暂无邮件。<br>请在目标平台触发验证码后点击「刷新」。</div>'; return; }
|
||
body.innerHTML = '';
|
||
messages.forEach((m) => {
|
||
const item = document.createElement('div'); item.className = 'msg-item';
|
||
const bodyText = m.body || m.snippet || '';
|
||
item.innerHTML = `
|
||
<div class="msg-from">${escapeHtml(m.from_addr || '(未知发送者)')}</div>
|
||
<div class="msg-subject">${escapeHtml(m.subject || '(无主题)')}</div>
|
||
<div class="msg-time">${escapeHtml(fmtTime(m.received_at))}</div>
|
||
${m.extracted_code ? `<div class="msg-code"><span class="code-chip" data-copy-code="${escapeHtml(m.extracted_code)}">${escapeHtml(m.extracted_code)} ⧉</span></div>` : ''}
|
||
${bodyText ? `<div class="msg-body">${escapeHtml(bodyText)}</div>` : ''}`;
|
||
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();
|
||
})();
|