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}
${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);