const $ = id => document.getElementById(id); let evtSource = null; let accPage = 1; const accPageSize = 20; let wasRunning = false; function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function truncate(value, limit = 90) { const text = String(value || ''); return text.length > limit ? text.slice(0, limit - 1) + '…' : text; } function setState(text, cls = 'neutral') { const el = $('state'); el.textContent = text; el.className = 'chip ' + cls; } function fmtTime(ms) { if (!ms) return ''; const date = new Date(Number(ms)); if (Number.isNaN(date.getTime())) return ''; const pad = n => String(n).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function statusLabel(status) { return { registered: '已注册', paid: '已付款', plus: '已 Plus', cpa_uploaded: '已上传 CPA', cpa_skipped: 'CPA 跳过', failed: '失败', trial: '试用 / 待付款', plus_check_failed: 'Plus 校验失败', cpa_failed: 'CPA 上传失败', }[status] || status || '未知'; } function statusTone(status) { if (['plus', 'cpa_uploaded'].includes(status)) return 'success'; if (['registered', 'paid', 'trial', 'cpa_skipped'].includes(status)) return 'warning'; if (['failed', 'plus_check_failed', 'cpa_failed'].includes(status)) return 'danger'; return 'neutral'; } function renderAccounts(accounts) { const tbody = $('accTable').querySelector('tbody'); tbody.innerHTML = ''; accounts.forEach((account, idx) => { const tr = document.createElement('tr'); tr.innerHTML = `${idx + 1}${escapeHtml(account.email)}${escapeHtml(account.stage)}${escapeHtml(account.planType)}${escapeHtml(account.cpaFile)}${escapeHtml(truncate(account.error, 90))}`; tbody.appendChild(tr); }); } async function refreshStatus() { try { const response = await fetch('/api/status'); const data = await response.json(); $('stageBox').textContent = data.stage || '空闲'; renderAccounts(data.accounts || []); if (data.running) { setState('执行中', 'green'); $('ssoGo').disabled = true; $('ssoStop').disabled = false; } else { $('ssoGo').disabled = false; $('ssoStop').disabled = true; if (data.state === 'done') setState('完成', 'green'); else if (data.state === 'error') setState('异常', 'red'); else if (data.state === 'stopped') setState('已停止', 'red'); else setState('空闲', 'neutral'); } if (wasRunning && !data.running) loadAccounts(); wasRunning = !!data.running; } catch (_) {} } function startLogStream() { if (evtSource) evtSource.close(); evtSource = new EventSource('/api/log'); evtSource.onmessage = event => appendLog(event.data); } function appendLog(line) { if (!line) return; const log = $('log'); log.textContent += line + '\n'; log.scrollTop = log.scrollHeight; updateLogLineCount(); } function updateLogLineCount() { const lines = $('log').textContent.split('\n').filter(Boolean).length; $('logLineCount').textContent = `${lines} 行`; } function showErrorPopover(target, text) { const popover = $('errorPopover'); if (!text) return; popover.textContent = text; popover.hidden = false; const rect = target.getBoundingClientRect(); const top = Math.min(window.innerHeight - 280, rect.bottom + 10); const left = Math.min(window.innerWidth - 540, Math.max(18, rect.left)); popover.style.top = `${Math.max(18, top)}px`; popover.style.left = `${Math.max(18, left)}px`; } function hideErrorPopover() { $('errorPopover').hidden = true; } async function copyTextToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); return; } catch (_) {} } const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } async function loadAccounts(page) { if (page !== undefined) accPage = page; const status = $('accFilter').value || ''; let url = `/api/accounts?page=${accPage}&pageSize=${accPageSize}`; if (status) url += '&status=' + encodeURIComponent(status); try { const response = await fetch(url); const data = await response.json(); const total = data.total || 0; const totalPages = Math.max(1, Math.ceil(total / accPageSize)); if (accPage > totalPages) accPage = totalPages; const list = $('accountList'); const accounts = data.accounts || []; list.innerHTML = accounts.length ? '' : '
当前筛选条件下暂无账号
'; accounts.forEach(account => list.appendChild(renderAccountCard(account))); bindAccountActions(list); renderPager(totalPages, total); } catch (error) { console.error(error); } } function renderAccountCard(account) { const card = document.createElement('article'); const rawEmail = account.email || ''; const email = escapeHtml(rawEmail); const cpa = escapeHtml(account.cpa_file_name || ''); const lastError = String(account.last_error || ''); const status = account.final_status || ''; const updatedAt = fmtTime(account.updated_at || account.created_at) || '暂无'; const detailBtn = ``; const downloadBtn = account.cpa_file_name ? `下载 CPA` : ''; const failedStatuses = ['plus_check_failed','cpa_failed','failed','cpa_skipped','registered','paid']; const recheckBtn = failedStatuses.includes(account.final_status || '') ? `` : ''; const retryPayBtn = account.can_retry_payment ? `` : ''; const trialHtml = account.trial_eligible ? '试用' : ''; const errorHtml = lastError ? `` : ''; card.className = `account-card${lastError ? ' has-error' : ''}`; card.innerHTML = `
${email}
${escapeHtml(statusLabel(status))} ${trialHtml}
PLAN ${escapeHtml(account.plan_type || '暂无')}
更新时间 ${escapeHtml(updatedAt)}
CPA 文件 ${cpa || '未生成'}
${detailBtn}${recheckBtn}${retryPayBtn}${downloadBtn}
${errorHtml ? `
${errorHtml}
` : ''} `; return card; } function bindAccountActions(root) { root.querySelectorAll('button[data-action="detail"]').forEach(btn => btn.addEventListener('click', () => loadAccountDetail(btn.dataset.email))); root.querySelectorAll('button[data-action="recheck"]').forEach(btn => btn.addEventListener('click', () => triggerRecheck(btn))); root.querySelectorAll('button[data-action="retry-pay"]').forEach(btn => btn.addEventListener('click', () => triggerRetryPayment(btn))); root.querySelectorAll('.error-summary').forEach(btn => { btn.addEventListener('mouseenter', () => showErrorPopover(btn, btn.dataset.error)); btn.addEventListener('focus', () => showErrorPopover(btn, btn.dataset.error)); btn.addEventListener('click', event => { event.stopPropagation(); showErrorPopover(btn, btn.dataset.error); }); btn.addEventListener('mouseleave', hideErrorPopover); btn.addEventListener('blur', hideErrorPopover); }); } function renderPager(totalPages, total) { const box = $('dbPager'); box.innerHTML = ''; const info = document.createElement('span'); info.className = 'info'; info.textContent = `共 ${total} 条 · 第 ${accPage}/${totalPages} 页`; box.appendChild(info); const addBtn = (label, page, disabled) => { const btn = document.createElement('button'); btn.textContent = label; btn.disabled = disabled; if (page === accPage) btn.classList.add('active'); if (!disabled) btn.addEventListener('click', () => loadAccounts(page)); box.appendChild(btn); }; addBtn('«', 1, accPage <= 1); addBtn('‹', accPage - 1, accPage <= 1); for (let i = Math.max(1, accPage - 2); i <= Math.min(totalPages, accPage + 2); i++) addBtn(String(i), i, false); addBtn('›', accPage + 1, accPage >= totalPages); addBtn('»', totalPages, accPage >= totalPages); } async function loadAccountDetail(email) { try { const response = await fetch('/api/account/' + encodeURIComponent(email)); $('accDetail').textContent = JSON.stringify(await response.json(), null, 2); } catch (error) { $('accDetail').textContent = String(error); } } async function triggerRecheck(btn) { const email = btn.dataset.email; if (!email) return; const original = btn.textContent; btn.disabled = true; btn.textContent = '校验中...'; try { const response = await fetch('/api/account/' + encodeURIComponent(email) + '/recheck', {method: 'POST'}); const data = await response.json(); btn.textContent = data.ok ? '✓ 已处理' : '✗ 失败'; setTimeout(() => loadAccounts(), 1200); } catch (error) { alert(error.message || String(error)); btn.disabled = false; btn.textContent = original; } } async function triggerRetryPayment(btn) { const email = btn.dataset.email; if (!email) return; if (!confirm(`确认为 ${email} 重新发起付款流程?`)) return; const original = btn.textContent; btn.disabled = true; btn.textContent = '提交中...'; try { const response = await fetch('/api/account/' + encodeURIComponent(email) + '/retry_payment', {method: 'POST'}); const data = await response.json(); btn.textContent = data.ok ? `✓ 已提交 ${data.task_id || ''}` : `✗ ${data.error || '失败'}`; setTimeout(() => loadAccounts(), 1500); } catch (error) { alert(error.message || String(error)); btn.disabled = false; btn.textContent = original; } } function setupNavigation() { document.querySelectorAll('.side-nav a').forEach(link => { link.addEventListener('click', () => { document.querySelectorAll('.side-nav a').forEach(item => item.classList.remove('active')); link.classList.add('active'); }); }); } function setupLogDock() { $('toggleLogDock').addEventListener('click', () => { const dock = $('logDock'); const collapsed = dock.classList.toggle('collapsed'); $('toggleLogDock').textContent = collapsed ? '展开' : '收起'; }); $('copyLog').addEventListener('click', async () => { const text = $('log').textContent || ''; await copyTextToClipboard(text); $('copyLog').textContent = '已复制'; setTimeout(() => $('copyLog').textContent = '复制', 1200); }); $('clearLog').addEventListener('click', () => { $('log').textContent = ''; updateLogLineCount(); }); } function setupActions() { $('ssoSave').addEventListener('click', saveSsoConfig); $('ssoGo').addEventListener('click', startSsoRun); $('ssoStop').addEventListener('click', () => fetch('/api/stop', {method: 'POST'})); $('refreshAccounts').addEventListener('click', () => loadAccounts()); $('accFilter').addEventListener('change', () => loadAccounts(1)); document.addEventListener('click', event => { if (!event.target.closest('.error-summary')) hideErrorPopover(); }); } // --------------------------------------------------------------------------- // SSO 快捷注册 // --------------------------------------------------------------------------- const SSO_CONFIG_KEY = 'ssoConfig'; const SSO_FIELDS = [ ['sso_account_count','account_count','int'], ['sso_mail_domain','sso_mail_domain','str'], ['sso_cpa_url','cpa_url','str'], ['sso_cpa_management_key','cpa_management_key','str'], ['sso_headless','headless','bool'], ['sso_proxy_url','proxy_url','str'], ]; function getDefaultSsoConfig() { return { account_count: 1, sso_mail_domain: 'aef.claudeai.life', cpa_url: '', cpa_management_key: '', headless: false, proxy_url: '', }; } function loadSsoConfigFromStorage() { try { const raw = localStorage.getItem(SSO_CONFIG_KEY); if (raw) return { ...getDefaultSsoConfig(), ...JSON.parse(raw) }; } catch (_) {} return getDefaultSsoConfig(); } function saveSsoConfigToStorage(cfg) { try { localStorage.setItem(SSO_CONFIG_KEY, JSON.stringify(cfg)); } catch (_) {} } function fillSsoForm(cfg) { for (const [domId, key, kind] of SSO_FIELDS) { const el = $(domId); if (!el || cfg[key] === undefined) continue; el.value = kind === 'bool' ? (cfg[key] ? 'true' : 'false') : cfg[key]; } } function readSsoForm() { const out = {}; for (const [domId, key, kind] of SSO_FIELDS) { const el = $(domId); if (!el) continue; let value = el.value; if (kind === 'int') value = Number(value) || 0; else if (kind === 'bool') value = value === 'true' || value === true; out[key] = value; } return out; } function saveSsoConfig() { const cfg = { ...loadSsoConfigFromStorage(), ...readSsoForm() }; saveSsoConfigToStorage(cfg); fillSsoForm(cfg); } async function startSsoRun() { saveSsoConfig(); const cfg = loadSsoConfigFromStorage(); const count = Number(cfg.account_count) || 0; if (count < 1) { alert('模板账号数量必须 >= 1'); return; } if (!cfg.cpa_url || !cfg.cpa_management_key) { alert('CPA 地址和管理密钥为必填项'); return; } $('log').textContent = ''; updateLogLineCount(); $('ssoGo').disabled = true; setState('启动中', 'neutral'); try { const response = await fetch('/api/start-sso', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ account_count: count, sso_mail_domain: cfg.sso_mail_domain, cpa_url: cfg.cpa_url, cpa_management_key: cfg.cpa_management_key, headless: cfg.headless, proxy_url: cfg.proxy_url, }), }); const data = await response.json(); if (data.error) { alert(data.error); $('ssoGo').disabled = false; setState('空闲', 'neutral'); } } catch (error) { alert(error.message || String(error)); $('ssoGo').disabled = false; } } function boot() { setupNavigation(); setupLogDock(); setupActions(); startLogStream(); fillSsoForm(loadSsoConfigFromStorage()); loadAccounts(); refreshStatus(); setInterval(refreshStatus, 1500); } document.addEventListener('DOMContentLoaded', boot);