Files
codex-register/static/js/app.js
237899745 0f9948ffc3
Some checks are pending
Docker Image CI / build-and-push-image (push) Waiting to run
feat: codex-register with Sub2API增强 + Playwright引擎
2026-03-22 00:24:16 +08:00

1674 lines
65 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 注册页面 JavaScript
* 使用 utils.js 中的工具库
*/
// 状态
let currentTask = null;
let currentBatch = null;
let logPollingInterval = null;
let batchPollingInterval = null;
let accountsPollingInterval = null;
let isBatchMode = false;
let isOutlookBatchMode = false;
let outlookAccounts = [];
let taskCompleted = false; // 标记任务是否已完成
let batchCompleted = false; // 标记批量任务是否已完成
let taskFinalStatus = null; // 保存任务的最终状态
let batchFinalStatus = null; // 保存批量任务的最终状态
let displayedLogs = new Set(); // 用于日志去重
let toastShown = false; // 标记是否已显示过 toast
let availableServices = {
tempmail: { available: true, services: [] },
outlook: { available: false, services: [] },
moe_mail: { available: false, services: [] },
temp_mail: { available: false, services: [] },
duck_mail: { available: false, services: [] },
freemail: { available: false, services: [] }
};
// WebSocket 相关变量
let webSocket = null;
let batchWebSocket = null; // 批量任务 WebSocket
let useWebSocket = true; // 是否使用 WebSocket
let wsHeartbeatInterval = null; // 心跳定时器
let batchWsHeartbeatInterval = null; // 批量任务心跳定时器
let activeTaskUuid = null; // 当前活跃的单任务 UUID用于页面重新可见时重连
let activeBatchId = null; // 当前活跃的批量任务 ID用于页面重新可见时重连
// DOM 元素
const elements = {
form: document.getElementById('registration-form'),
emailService: document.getElementById('email-service'),
regMode: document.getElementById('reg-mode'),
regModeGroup: document.getElementById('reg-mode-group'),
batchCountGroup: document.getElementById('batch-count-group'),
batchCount: document.getElementById('batch-count'),
batchOptions: document.getElementById('batch-options'),
intervalMin: document.getElementById('interval-min'),
intervalMax: document.getElementById('interval-max'),
startBtn: document.getElementById('start-btn'),
cancelBtn: document.getElementById('cancel-btn'),
taskStatusRow: document.getElementById('task-status-row'),
batchProgressSection: document.getElementById('batch-progress-section'),
consoleLog: document.getElementById('console-log'),
clearLogBtn: document.getElementById('clear-log-btn'),
// 任务状态
taskId: document.getElementById('task-id'),
taskEmail: document.getElementById('task-email'),
taskStatus: document.getElementById('task-status'),
taskService: document.getElementById('task-service'),
taskStatusBadge: document.getElementById('task-status-badge'),
// 批量状态
batchProgressText: document.getElementById('batch-progress-text'),
batchProgressPercent: document.getElementById('batch-progress-percent'),
progressBar: document.getElementById('progress-bar'),
batchSuccess: document.getElementById('batch-success'),
batchFailed: document.getElementById('batch-failed'),
batchRemaining: document.getElementById('batch-remaining'),
// 已注册账号
recentAccountsTable: document.getElementById('recent-accounts-table'),
refreshAccountsBtn: document.getElementById('refresh-accounts-btn'),
// Outlook 批量注册
outlookBatchSection: document.getElementById('outlook-batch-section'),
outlookAccountsContainer: document.getElementById('outlook-accounts-container'),
outlookIntervalMin: document.getElementById('outlook-interval-min'),
outlookIntervalMax: document.getElementById('outlook-interval-max'),
outlookSkipRegistered: document.getElementById('outlook-skip-registered'),
outlookConcurrencyMode: document.getElementById('outlook-concurrency-mode'),
outlookConcurrencyCount: document.getElementById('outlook-concurrency-count'),
outlookConcurrencyHint: document.getElementById('outlook-concurrency-hint'),
outlookIntervalGroup: document.getElementById('outlook-interval-group'),
// 批量并发控件
concurrencyMode: document.getElementById('concurrency-mode'),
concurrencyCount: document.getElementById('concurrency-count'),
concurrencyHint: document.getElementById('concurrency-hint'),
intervalGroup: document.getElementById('interval-group'),
// 注册后自动操作
autoUploadCpa: document.getElementById('auto-upload-cpa'),
cpaServiceSelectGroup: document.getElementById('cpa-service-select-group'),
cpaServiceSelect: document.getElementById('cpa-service-select'),
autoUploadSub2api: document.getElementById('auto-upload-sub2api'),
sub2apiServiceSelectGroup: document.getElementById('sub2api-service-select-group'),
sub2apiServiceSelect: document.getElementById('sub2api-service-select'),
// Sub2API 高级配置
sub2apiAdvancedGroup: document.getElementById('sub2api-advanced-group'),
sub2apiGroupSelect: document.getElementById('sub2api-group-select'),
sub2apiProxySelect: document.getElementById('sub2api-proxy-select'),
sub2apiModelCheckboxes: document.getElementById('sub2api-model-checkboxes'),
sub2apiCustomModels: document.getElementById('sub2api-custom-models'),
sub2apiAddModelBtn: document.getElementById('sub2api-add-model-btn'),
sub2apiCustomModelTags: document.getElementById('sub2api-custom-model-tags'),
autoUploadTm: document.getElementById('auto-upload-tm'),
tmServiceSelectGroup: document.getElementById('tm-service-select-group'),
tmServiceSelect: document.getElementById('tm-service-select'),
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
loadAvailableServices();
loadRecentAccounts();
startAccountsPolling();
initVisibilityReconnect();
restoreActiveTask();
initAutoUploadOptions();
initSub2apiAdvancedOptions();
});
// 初始化注册后自动操作选项CPA / Sub2API / TM
async function initAutoUploadOptions() {
await Promise.all([
loadServiceSelect('/cpa-services?enabled=true', elements.cpaServiceSelect, elements.autoUploadCpa, elements.cpaServiceSelectGroup),
loadServiceSelect('/sub2api-services?enabled=true', elements.sub2apiServiceSelect, elements.autoUploadSub2api, elements.sub2apiServiceSelectGroup),
loadServiceSelect('/tm-services?enabled=true', elements.tmServiceSelect, elements.autoUploadTm, elements.tmServiceSelectGroup),
]);
}
// Sub2API 默认模型列表
const SUB2API_DEFAULT_MODELS = [
'gpt-5.1', 'gpt-5.1-codex', 'gpt-5.1-codex-max', 'gpt-5.1-codex-mini',
'gpt-5.2', 'gpt-5.2-codex', 'gpt-5.3', 'gpt-5.3-codex', 'gpt-5.4'
];
// 初始化 Sub2API 高级配置(分组、代理、模型)
function initSub2apiAdvancedOptions() {
if (!elements.autoUploadSub2api) return;
// 渲染默认模型复选框
if (elements.sub2apiModelCheckboxes) {
elements.sub2apiModelCheckboxes.innerHTML = SUB2API_DEFAULT_MODELS.map(m =>
'<label style="display:flex;align-items:center;gap:3px;font-size:0.82em;cursor:pointer;">' +
'<input type="checkbox" class="sub2api-model-cb" value="' + m + '" checked>' +
'<span>' + m + '</span>' +
'</label>'
).join('');
}
// 监听 Sub2API 勾选:控制高级配置区显示
elements.autoUploadSub2api.addEventListener('change', () => {
const show = elements.autoUploadSub2api.checked;
if (elements.sub2apiAdvancedGroup) elements.sub2apiAdvancedGroup.style.display = show ? 'block' : 'none';
if (show) loadSub2apiRemoteOptions();
});
// "添加"按钮事件:将自定义模型添加为标签
if (elements.sub2apiAddModelBtn) {
elements.sub2apiAddModelBtn.addEventListener('click', function() {
addCustomSub2apiModel();
});
}
// 输入框回车也可添加
if (elements.sub2apiCustomModels) {
elements.sub2apiCustomModels.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); addCustomSub2apiModel(); }
});
}
// 监听服务多选下拉中 checkbox 变化
if (elements.sub2apiServiceSelect) {
elements.sub2apiServiceSelect.addEventListener('change', (e) => {
if (e.target.matches('.msd-item input') && elements.autoUploadSub2api.checked) {
loadSub2apiRemoteOptions();
}
});
}
}
// 加载 Sub2API 远程分组和代理列表
async function loadSub2apiRemoteOptions() {
const serviceIds = getSelectedServiceIds(elements.sub2apiServiceSelect);
const serviceId = serviceIds.length > 0 ? serviceIds[0] : null;
if (!serviceId) {
if (elements.sub2apiGroupSelect) elements.sub2apiGroupSelect.innerHTML = '<option value="" disabled>请先选择 Sub2API 服务</option>';
if (elements.sub2apiProxySelect) elements.sub2apiProxySelect.innerHTML = '<option value="">请先选择 Sub2API 服务</option>';
return;
}
// 并行加载分组和代理
try {
const [groups, proxies] = await Promise.all([
api.get('/sub2api-services/' + serviceId + '/groups').catch(() => []),
api.get('/sub2api-services/' + serviceId + '/proxies').catch(() => []),
]);
// 渲染分组选择
if (elements.sub2apiGroupSelect) {
if (groups.length === 0) {
elements.sub2apiGroupSelect.innerHTML = '<option value="" disabled>无可用分组</option>';
} else {
elements.sub2apiGroupSelect.innerHTML = groups.map(g =>
'<option value="' + g.id + '">' + escapeHtml(g.name) + ' (' + (g.account_count || 0) + ' 个账号)</option>'
).join('');
}
}
// 渲染代理选择
if (elements.sub2apiProxySelect) {
var opts = '<option value="">不指定(使用默认)</option>';
proxies.forEach(function(p) {
var info = [p.protocol, p.country, p.ip_address].filter(Boolean).join(' | ');
opts += '<option value="' + p.id + '">' + escapeHtml(p.name) + ' (' + info + ', ' + (p.account_count || 0) + ' 个账号)</option>';
});
elements.sub2apiProxySelect.innerHTML = opts;
}
} catch (e) {
console.error('加载 Sub2API 远程配置失败:', e);
}
}
// 添加自定义模型标签
function addCustomSub2apiModel() {
var input = elements.sub2apiCustomModels;
if (!input) return;
var names = input.value.trim().split(',').map(function(s) { return s.trim(); }).filter(Boolean);
if (names.length === 0) return;
names.forEach(function(name) {
// 检查是否已存在于预置复选框中
var existing = document.querySelector('.sub2api-model-cb[value="' + name + '"]');
if (existing) { existing.checked = true; return; }
// 检查是否已在自定义标签中
if (document.querySelector('.sub2api-custom-tag[data-model="' + name + '"]')) return;
// 创建标签
var tag = document.createElement('span');
tag.className = 'sub2api-custom-tag';
tag.setAttribute('data-model', name);
tag.style.cssText = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;background:var(--primary-bg,#e8f0fe);border:1px solid var(--primary-color,#4a90d9);border-radius:12px;font-size:0.8em;color:var(--primary-color,#4a90d9);';
tag.innerHTML = name + ' <span style="cursor:pointer;font-weight:bold;margin-left:2px;" onclick="removeCustomSub2apiModel(this)">&times;</span>';
elements.sub2apiCustomModelTags.appendChild(tag);
});
input.value = '';
}
// 移除自定义模型标签
function removeCustomSub2apiModel(closeBtn) {
var tag = closeBtn.parentElement;
if (tag) tag.remove();
}
// 获取 Sub2API 高级配置
function getSub2apiAdvancedConfig() {
var config = {};
// 分组 IDs
if (elements.sub2apiGroupSelect) {
var selected = Array.from(elements.sub2apiGroupSelect.selectedOptions).map(function(o) { return parseInt(o.value); }).filter(function(v) { return !isNaN(v); });
if (selected.length > 0) config.sub2api_group_ids = selected;
}
// 代理节点 ID
if (elements.sub2apiProxySelect && elements.sub2apiProxySelect.value) {
config.sub2api_proxy_id = parseInt(elements.sub2apiProxySelect.value);
}
// 模型映射
var checkedModels = Array.from(document.querySelectorAll('.sub2api-model-cb:checked')).map(function(cb) { return cb.value; });
// 从自定义标签收集模型
var customModels = [];
if (elements.sub2apiCustomModelTags) {
var tags = elements.sub2apiCustomModelTags.querySelectorAll('.sub2api-custom-tag');
tags.forEach(function(tag) { customModels.push(tag.getAttribute('data-model')); });
}
var allModels = [];
var seen = {};
checkedModels.concat(customModels).forEach(function(m) {
if (!seen[m]) { seen[m] = true; allModels.push(m); }
});
if (allModels.length > 0) {
var mapping = {};
allModels.forEach(function(m) { mapping[m] = m; });
config.sub2api_model_mapping = mapping;
}
return config;
}
// 通用:构建自定义多选下拉组件并处理联动
async function loadServiceSelect(apiPath, container, checkbox, selectGroup) {
if (!checkbox || !container) return;
let services = [];
try {
services = await api.get(apiPath);
} catch (e) {}
if (!services || services.length === 0) {
checkbox.disabled = true;
checkbox.title = '请先在设置中添加对应服务';
const label = checkbox.closest('label');
if (label) label.style.opacity = '0.5';
container.innerHTML = '<div class="msd-empty">暂无可用服务</div>';
} else {
const items = services.map(s =>
`<label class="msd-item">
<input type="checkbox" value="${s.id}" checked>
<span>${escapeHtml(s.name)}</span>
</label>`
).join('');
container.innerHTML = `
<div class="msd-dropdown" id="${container.id}-dd">
<div class="msd-trigger" onclick="toggleMsd('${container.id}-dd')">
<span class="msd-label">全部 (${services.length})</span>
<span class="msd-arrow">▼</span>
</div>
<div class="msd-list">${items}</div>
</div>`;
// 监听 checkbox 变化,更新触发器文字
container.querySelectorAll('.msd-item input').forEach(cb => {
cb.addEventListener('change', () => updateMsdLabel(container.id + '-dd'));
});
// 点击外部关闭
document.addEventListener('click', (e) => {
const dd = document.getElementById(container.id + '-dd');
if (dd && !dd.contains(e.target)) dd.classList.remove('open');
}, true);
}
// 联动显示/隐藏服务选择区
checkbox.addEventListener('change', () => {
if (selectGroup) selectGroup.style.display = checkbox.checked ? 'block' : 'none';
});
}
function toggleMsd(ddId) {
const dd = document.getElementById(ddId);
if (dd) dd.classList.toggle('open');
}
function updateMsdLabel(ddId) {
const dd = document.getElementById(ddId);
if (!dd) return;
const all = dd.querySelectorAll('.msd-item input');
const checked = dd.querySelectorAll('.msd-item input:checked');
const label = dd.querySelector('.msd-label');
if (!label) return;
if (checked.length === 0) label.textContent = '未选择';
else if (checked.length === all.length) label.textContent = `全部 (${all.length})`;
else label.textContent = Array.from(checked).map(c => c.nextElementSibling.textContent).join(', ');
}
// 获取自定义多选下拉中选中的服务 ID 列表
function getSelectedServiceIds(container) {
if (!container) return [];
return Array.from(container.querySelectorAll('.msd-item input:checked')).map(cb => parseInt(cb.value));
}
// 事件监听
function initEventListeners() {
// 注册表单提交
elements.form.addEventListener('submit', handleStartRegistration);
// 注册模式切换
elements.regMode.addEventListener('change', handleModeChange);
// 邮箱服务切换
elements.emailService.addEventListener('change', handleServiceChange);
// 取消按钮
elements.cancelBtn.addEventListener('click', handleCancelTask);
// 清空日志
elements.clearLogBtn.addEventListener('click', () => {
elements.consoleLog.innerHTML = '<div class="log-line info">[系统] 日志已清空</div>';
displayedLogs.clear(); // 清空日志去重集合
});
// 刷新账号列表
elements.refreshAccountsBtn.addEventListener('click', () => {
loadRecentAccounts();
toast.info('已刷新');
});
// 并发模式切换
elements.concurrencyMode.addEventListener('change', () => {
handleConcurrencyModeChange(elements.concurrencyMode, elements.concurrencyHint, elements.intervalGroup);
});
elements.outlookConcurrencyMode.addEventListener('change', () => {
handleConcurrencyModeChange(elements.outlookConcurrencyMode, elements.outlookConcurrencyHint, elements.outlookIntervalGroup);
});
}
// 加载可用的邮箱服务
async function loadAvailableServices() {
try {
const data = await api.get('/registration/available-services');
availableServices = data;
// 更新邮箱服务选择框
updateEmailServiceOptions();
addLog('info', '[系统] 邮箱服务列表已加载');
} catch (error) {
console.error('加载邮箱服务列表失败:', error);
addLog('warning', '[警告] 加载邮箱服务列表失败');
}
}
// 更新邮箱服务选择框
function updateEmailServiceOptions() {
const select = elements.emailService;
select.innerHTML = '';
// Tempmail
if (availableServices.tempmail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = '🌐 临时邮箱';
availableServices.tempmail.services.forEach(service => {
const option = document.createElement('option');
option.value = `tempmail:${service.id || 'default'}`;
option.textContent = service.name;
option.dataset.type = 'tempmail';
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
// Outlook
if (availableServices.outlook.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `📧 Outlook (${availableServices.outlook.count} 个账户)`;
availableServices.outlook.services.forEach(service => {
const option = document.createElement('option');
option.value = `outlook:${service.id}`;
option.textContent = service.name + (service.has_oauth ? ' (OAuth)' : '');
option.dataset.type = 'outlook';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
// Outlook 批量注册选项
const batchOption = document.createElement('option');
batchOption.value = 'outlook_batch:all';
batchOption.textContent = `📋 Outlook 批量注册 (${availableServices.outlook.count} 个账户)`;
batchOption.dataset.type = 'outlook_batch';
optgroup.appendChild(batchOption);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '📧 Outlook (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在邮箱服务页面导入账户';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
// 自定义域名
if (availableServices.moe_mail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `🔗 自定义域名 (${availableServices.moe_mail.count} 个服务)`;
availableServices.moe_mail.services.forEach(service => {
const option = document.createElement('option');
option.value = `moe_mail:${service.id || 'default'}`;
option.textContent = service.name + (service.default_domain ? ` (@${service.default_domain})` : '');
option.dataset.type = 'moe_mail';
if (service.id) {
option.dataset.serviceId = service.id;
}
optgroup.appendChild(option);
});
select.appendChild(optgroup);
} else {
const optgroup = document.createElement('optgroup');
optgroup.label = '🔗 自定义域名 (未配置)';
const option = document.createElement('option');
option.value = '';
option.textContent = '请先在邮箱服务页面添加服务';
option.disabled = true;
optgroup.appendChild(option);
select.appendChild(optgroup);
}
// Temp-Mail自部署
if (availableServices.temp_mail && availableServices.temp_mail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `📮 Temp-Mail 自部署 (${availableServices.temp_mail.count} 个服务)`;
availableServices.temp_mail.services.forEach(service => {
const option = document.createElement('option');
option.value = `temp_mail:${service.id}`;
option.textContent = service.name + (service.domain ? ` (@${service.domain})` : '');
option.dataset.type = 'temp_mail';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
// DuckMail
if (availableServices.duck_mail && availableServices.duck_mail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `🦆 DuckMail (${availableServices.duck_mail.count} 个服务)`;
availableServices.duck_mail.services.forEach(service => {
const option = document.createElement('option');
option.value = `duck_mail:${service.id}`;
option.textContent = service.name + (service.default_domain ? ` (@${service.default_domain})` : '');
option.dataset.type = 'duck_mail';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
// Freemail
if (availableServices.freemail && availableServices.freemail.available) {
const optgroup = document.createElement('optgroup');
optgroup.label = `📧 Freemail (${availableServices.freemail.count} 个服务)`;
availableServices.freemail.services.forEach(service => {
const option = document.createElement('option');
option.value = `freemail:${service.id}`;
option.textContent = service.name + (service.domain ? ` (@${service.domain})` : '');
option.dataset.type = 'freemail';
option.dataset.serviceId = service.id;
optgroup.appendChild(option);
});
select.appendChild(optgroup);
}
}
// 处理邮箱服务切换
function handleServiceChange(e) {
const value = e.target.value;
if (!value) return;
const [type, id] = value.split(':');
// 处理 Outlook 批量注册模式
if (type === 'outlook_batch') {
isOutlookBatchMode = true;
elements.outlookBatchSection.style.display = 'block';
elements.regModeGroup.style.display = 'none';
elements.batchCountGroup.style.display = 'none';
elements.batchOptions.style.display = 'none';
loadOutlookAccounts();
addLog('info', '[系统] 已切换到 Outlook 批量注册模式');
return;
} else {
isOutlookBatchMode = false;
elements.outlookBatchSection.style.display = 'none';
elements.regModeGroup.style.display = 'block';
}
// 显示服务信息
if (type === 'outlook') {
const service = availableServices.outlook.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 Outlook 账户: ${service.name}`);
}
} else if (type === 'moe_mail') {
const service = availableServices.moe_mail.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择自定义域名服务: ${service.name}`);
}
} else if (type === 'temp_mail') {
const service = availableServices.temp_mail.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 Temp-Mail 自部署服务: ${service.name}`);
}
} else if (type === 'duck_mail') {
const service = availableServices.duck_mail.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 DuckMail 服务: ${service.name}`);
}
} else if (type === 'freemail') {
const service = availableServices.freemail.services.find(s => s.id == id);
if (service) {
addLog('info', `[系统] 已选择 Freemail 服务: ${service.name}`);
}
}
}
// 模式切换
function handleModeChange(e) {
const mode = e.target.value;
isBatchMode = mode === 'batch';
elements.batchCountGroup.style.display = isBatchMode ? 'block' : 'none';
elements.batchOptions.style.display = isBatchMode ? 'block' : 'none';
}
// 并发模式切换(批量)
function handleConcurrencyModeChange(selectEl, hintEl, intervalGroupEl) {
const mode = selectEl.value;
if (mode === 'parallel') {
hintEl.textContent = '所有任务分成 N 个并发批次同时执行';
intervalGroupEl.style.display = 'none';
} else {
hintEl.textContent = '同时最多运行 N 个任务,每隔 interval 秒启动新任务';
intervalGroupEl.style.display = 'block';
}
}
// 开始注册
async function handleStartRegistration(e) {
e.preventDefault();
const selectedValue = elements.emailService.value;
if (!selectedValue) {
toast.error('请选择一个邮箱服务');
return;
}
// 处理 Outlook 批量注册模式
if (isOutlookBatchMode) {
await handleOutlookBatchRegistration();
return;
}
const [emailServiceType, serviceId] = selectedValue.split(':');
// 禁用开始按钮
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
// 清空日志
elements.consoleLog.innerHTML = '';
// 构建请求数据(代理从设置中自动获取)
const requestData = {
email_service_type: emailServiceType,
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false,
cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getSelectedServiceIds(elements.cpaServiceSelect) : [],
auto_upload_sub2api: elements.autoUploadSub2api ? elements.autoUploadSub2api.checked : false,
sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
...(elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSub2apiAdvancedConfig() : {}),
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
};
// 如果选择了数据库中的服务,传递 service_id
if (serviceId && serviceId !== 'default') {
requestData.email_service_id = parseInt(serviceId);
}
if (isBatchMode) {
await handleBatchRegistration(requestData);
} else {
await handleSingleRegistration(requestData);
}
}
// 单次注册
async function handleSingleRegistration(requestData) {
// 重置任务状态
taskCompleted = false;
taskFinalStatus = null;
displayedLogs.clear(); // 清空日志去重集合
toastShown = false; // 重置 toast 标志
addLog('info', '[系统] 正在启动注册任务...');
try {
const data = await api.post('/registration/start', requestData);
currentTask = data;
activeTaskUuid = data.task_uuid; // 保存用于重连
// 持久化到 sessionStorage跨页面导航后可恢复
sessionStorage.setItem('activeTask', JSON.stringify({ task_uuid: data.task_uuid, mode: 'single' }));
addLog('info', `[系统] 任务已创建: ${data.task_uuid}`);
showTaskStatus(data);
updateTaskStatus('running');
// 优先使用 WebSocket
connectWebSocket(data.task_uuid);
} catch (error) {
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
// ============== WebSocket 功能 ==============
// 连接 WebSocket
function connectWebSocket(taskUuid) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws/task/${taskUuid}`;
try {
webSocket = new WebSocket(wsUrl);
webSocket.onopen = () => {
console.log('WebSocket 连接成功');
useWebSocket = true;
// 停止轮询(如果有)
stopLogPolling();
// 开始心跳
startWebSocketHeartbeat();
};
webSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log') {
const logType = getLogType(data.message);
addLog(logType, data.message);
} else if (data.type === 'status') {
updateTaskStatus(data.status);
// 检查是否完成
if (['completed', 'failed', 'cancelled', 'cancelling'].includes(data.status)) {
// 保存最终状态,用于 onclose 判断
taskFinalStatus = data.status;
taskCompleted = true;
// 断开 WebSocket异步操作
disconnectWebSocket();
// 任务完成后再重置按钮
resetButtons();
// 只显示一次 toast
if (!toastShown) {
toastShown = true;
if (data.status === 'completed') {
addLog('success', '[成功] 注册成功!');
toast.success('注册成功!');
// 刷新账号列表
loadRecentAccounts();
} else if (data.status === 'failed') {
addLog('error', '[错误] 注册失败');
toast.error('注册失败');
} else if (data.status === 'cancelled' || data.status === 'cancelling') {
addLog('warning', '[警告] 任务已取消');
}
}
}
} else if (data.type === 'pong') {
// 心跳响应,忽略
}
};
webSocket.onclose = (event) => {
console.log('WebSocket 连接关闭:', event.code);
stopWebSocketHeartbeat();
// 只有在任务未完成且最终状态不是完成状态时才切换到轮询
// 使用 taskFinalStatus 而不是 currentTask.status因为 currentTask 可能已被重置
const shouldPoll = !taskCompleted &&
taskFinalStatus === null; // 如果 taskFinalStatus 有值,说明任务已完成
if (shouldPoll && currentTask) {
console.log('切换到轮询模式');
useWebSocket = false;
startLogPolling(currentTask.task_uuid);
}
};
webSocket.onerror = (error) => {
console.error('WebSocket 错误:', error);
// 切换到轮询
useWebSocket = false;
stopWebSocketHeartbeat();
startLogPolling(taskUuid);
};
} catch (error) {
console.error('WebSocket 连接失败:', error);
useWebSocket = false;
startLogPolling(taskUuid);
}
}
// 断开 WebSocket
function disconnectWebSocket() {
stopWebSocketHeartbeat();
if (webSocket) {
webSocket.close();
webSocket = null;
}
}
// 开始心跳
function startWebSocketHeartbeat() {
stopWebSocketHeartbeat();
wsHeartbeatInterval = setInterval(() => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({ type: 'ping' }));
}
}, 25000); // 每 25 秒发送一次心跳
}
// 停止心跳
function stopWebSocketHeartbeat() {
if (wsHeartbeatInterval) {
clearInterval(wsHeartbeatInterval);
wsHeartbeatInterval = null;
}
}
// 发送取消请求
function cancelViaWebSocket() {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({ type: 'cancel' }));
}
}
// 批量注册
async function handleBatchRegistration(requestData) {
// 重置批量任务状态
batchCompleted = false;
batchFinalStatus = null;
displayedLogs.clear(); // 清空日志去重集合
toastShown = false; // 重置 toast 标志
const count = parseInt(elements.batchCount.value) || 5;
const intervalMin = parseInt(elements.intervalMin.value) || 5;
const intervalMax = parseInt(elements.intervalMax.value) || 30;
const concurrency = parseInt(elements.concurrencyCount.value) || 3;
const mode = elements.concurrencyMode.value || 'pipeline';
requestData.count = count;
requestData.interval_min = intervalMin;
requestData.interval_max = intervalMax;
requestData.concurrency = Math.min(50, Math.max(1, concurrency));
requestData.mode = mode;
addLog('info', `[系统] 正在启动批量注册任务 (数量: ${count})...`);
try {
const data = await api.post('/registration/batch', requestData);
currentBatch = data;
activeBatchId = data.batch_id; // 保存用于重连
// 持久化到 sessionStorage跨页面导航后可恢复
sessionStorage.setItem('activeTask', JSON.stringify({ batch_id: data.batch_id, mode: 'batch', total: data.count }));
addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[系统] 共 ${data.count} 个任务已加入队列`);
showBatchStatus(data);
// 优先使用 WebSocket
connectBatchWebSocket(data.batch_id);
} catch (error) {
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
// 取消任务
async function handleCancelTask() {
// 禁用取消按钮,防止重复点击
elements.cancelBtn.disabled = true;
addLog('info', '[系统] 正在提交取消请求...');
try {
// 批量任务取消(包括普通批量模式和 Outlook 批量模式)
if (currentBatch && (isBatchMode || isOutlookBatchMode)) {
// 优先通过 WebSocket 取消
if (batchWebSocket && batchWebSocket.readyState === WebSocket.OPEN) {
batchWebSocket.send(JSON.stringify({ type: 'cancel' }));
addLog('warning', '[警告] 批量任务取消请求已提交');
toast.info('任务取消请求已提交');
} else {
// 降级到 REST API
const endpoint = isOutlookBatchMode
? `/registration/outlook-batch/${currentBatch.batch_id}/cancel`
: `/registration/batch/${currentBatch.batch_id}/cancel`;
await api.post(endpoint);
addLog('warning', '[警告] 批量任务取消请求已提交');
toast.info('任务取消请求已提交');
stopBatchPolling();
resetButtons();
}
}
// 单次任务取消
else if (currentTask) {
// 优先通过 WebSocket 取消
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({ type: 'cancel' }));
addLog('warning', '[警告] 任务取消请求已提交');
toast.info('任务取消请求已提交');
} else {
// 降级到 REST API
await api.post(`/registration/tasks/${currentTask.task_uuid}/cancel`);
addLog('warning', '[警告] 任务已取消');
toast.info('任务已取消');
stopLogPolling();
resetButtons();
}
}
// 没有活动任务
else {
addLog('warning', '[警告] 没有活动的任务可以取消');
toast.warning('没有活动的任务');
resetButtons();
}
} catch (error) {
addLog('error', `[错误] 取消失败: ${error.message}`);
toast.error(error.message);
// 恢复取消按钮,允许重试
elements.cancelBtn.disabled = false;
}
}
// 开始轮询日志
function startLogPolling(taskUuid) {
let lastLogIndex = 0;
logPollingInterval = setInterval(async () => {
try {
const data = await api.get(`/registration/tasks/${taskUuid}/logs`);
// 更新任务状态
updateTaskStatus(data.status);
// 更新邮箱信息
if (data.email) {
elements.taskEmail.textContent = data.email;
}
if (data.email_service) {
elements.taskService.textContent = getServiceTypeText(data.email_service);
}
// 添加新日志
const logs = data.logs || [];
for (let i = lastLogIndex; i < logs.length; i++) {
const log = logs[i];
const logType = getLogType(log);
addLog(logType, log);
}
lastLogIndex = logs.length;
// 检查任务是否完成
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
stopLogPolling();
resetButtons();
// 只显示一次 toast
if (!toastShown) {
toastShown = true;
if (data.status === 'completed') {
addLog('success', '[成功] 注册成功!');
toast.success('注册成功!');
// 刷新账号列表
loadRecentAccounts();
} else if (data.status === 'failed') {
addLog('error', '[错误] 注册失败');
toast.error('注册失败');
} else if (data.status === 'cancelled') {
addLog('warning', '[警告] 任务已取消');
}
}
}
} catch (error) {
console.error('轮询日志失败:', error);
}
}, 1000);
}
// 停止轮询日志
function stopLogPolling() {
if (logPollingInterval) {
clearInterval(logPollingInterval);
logPollingInterval = null;
}
}
// 开始轮询批量状态
function startBatchPolling(batchId) {
batchPollingInterval = setInterval(async () => {
try {
const data = await api.get(`/registration/batch/${batchId}`);
updateBatchProgress(data);
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
// 只显示一次 toast
if (!toastShown) {
toastShown = true;
addLog('info', `[完成] 批量任务完成!成功: ${data.success}, 失败: ${data.failed}`);
if (data.success > 0) {
toast.success(`批量注册完成,成功 ${data.success}`);
// 刷新账号列表
loadRecentAccounts();
} else {
toast.warning('批量注册完成,但没有成功注册任何账号');
}
}
}
} catch (error) {
console.error('轮询批量状态失败:', error);
}
}, 2000);
}
// 停止轮询批量状态
function stopBatchPolling() {
if (batchPollingInterval) {
clearInterval(batchPollingInterval);
batchPollingInterval = null;
}
}
// 显示任务状态
function showTaskStatus(task) {
elements.taskStatusRow.style.display = 'grid';
elements.batchProgressSection.style.display = 'none';
elements.taskStatusBadge.style.display = 'inline-flex';
elements.taskId.textContent = task.task_uuid.substring(0, 8) + '...';
elements.taskEmail.textContent = '-';
elements.taskService.textContent = '-';
}
// 更新任务状态
function updateTaskStatus(status) {
const statusInfo = {
pending: { text: '等待中', class: 'pending' },
running: { text: '运行中', class: 'running' },
completed: { text: '已完成', class: 'completed' },
failed: { text: '失败', class: 'failed' },
cancelled: { text: '已取消', class: 'disabled' }
};
const info = statusInfo[status] || { text: status, class: '' };
elements.taskStatusBadge.textContent = info.text;
elements.taskStatusBadge.className = `status-badge ${info.class}`;
elements.taskStatus.textContent = info.text;
}
// 显示批量状态
function showBatchStatus(batch) {
elements.batchProgressSection.style.display = 'block';
elements.taskStatusRow.style.display = 'none';
elements.taskStatusBadge.style.display = 'none';
elements.batchProgressText.textContent = `0/${batch.count}`;
elements.batchProgressPercent.textContent = '0%';
elements.progressBar.style.width = '0%';
elements.batchSuccess.textContent = '0';
elements.batchFailed.textContent = '0';
elements.batchRemaining.textContent = batch.count;
// 重置计数器
elements.batchSuccess.dataset.last = '0';
elements.batchFailed.dataset.last = '0';
}
// 更新批量进度
function updateBatchProgress(data) {
const progress = ((data.completed / data.total) * 100).toFixed(0);
elements.batchProgressText.textContent = `${data.completed}/${data.total}`;
elements.batchProgressPercent.textContent = `${progress}%`;
elements.progressBar.style.width = `${progress}%`;
elements.batchSuccess.textContent = data.success;
elements.batchFailed.textContent = data.failed;
elements.batchRemaining.textContent = data.total - data.completed;
// 记录日志(避免重复)
if (data.completed > 0) {
const lastSuccess = parseInt(elements.batchSuccess.dataset.last || '0');
const lastFailed = parseInt(elements.batchFailed.dataset.last || '0');
if (data.success > lastSuccess) {
addLog('success', `[成功] 第 ${data.success} 个账号注册成功`);
}
if (data.failed > lastFailed) {
addLog('error', `[失败] 第 ${data.failed} 个账号注册失败`);
}
elements.batchSuccess.dataset.last = data.success;
elements.batchFailed.dataset.last = data.failed;
}
}
// 加载最近注册的账号
async function loadRecentAccounts() {
try {
const data = await api.get('/accounts?page=1&page_size=10');
if (data.accounts.length === 0) {
elements.recentAccountsTable.innerHTML = `
<tr>
<td colspan="5">
<div class="empty-state" style="padding: var(--spacing-md);">
<div class="empty-state-icon">📭</div>
<div class="empty-state-title">暂无已注册账号</div>
</div>
</td>
</tr>
`;
return;
}
elements.recentAccountsTable.innerHTML = data.accounts.map(account => `
<tr data-id="${account.id}">
<td>${account.id}</td>
<td>
<span style="display:inline-flex;align-items:center;gap:4px;">
<span title="${escapeHtml(account.email)}">${escapeHtml(account.email)}</span>
<button class="btn-copy-icon copy-email-btn" data-email="${escapeHtml(account.email)}" title="复制邮箱">📋</button>
</span>
</td>
<td class="password-cell">
${account.password
? `<span style="display:inline-flex;align-items:center;gap:4px;">
<span class="password-hidden" title="点击查看">${escapeHtml(account.password.substring(0, 8))}...</span>
<button class="btn-copy-icon copy-pwd-btn" data-pwd="${escapeHtml(account.password)}" title="复制密码">📋</button>
</span>`
: '-'}
</td>
<td>
${getStatusIcon(account.status)}
</td>
</tr>
`).join('');
// 绑定复制按钮事件
elements.recentAccountsTable.querySelectorAll('.copy-email-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.stopPropagation(); copyToClipboard(btn.dataset.email); });
});
elements.recentAccountsTable.querySelectorAll('.copy-pwd-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.stopPropagation(); copyToClipboard(btn.dataset.pwd); });
});
} catch (error) {
console.error('加载账号列表失败:', error);
}
}
// 开始账号列表轮询
function startAccountsPolling() {
// 每30秒刷新一次账号列表
accountsPollingInterval = setInterval(() => {
loadRecentAccounts();
}, 30000);
}
// 添加日志
function addLog(type, message) {
// 日志去重:使用消息内容的 hash 作为键
const logKey = `${type}:${message}`;
if (displayedLogs.has(logKey)) {
return; // 已经显示过,跳过
}
displayedLogs.add(logKey);
// 限制去重集合大小,避免内存泄漏
if (displayedLogs.size > 1000) {
// 清空一半的记录
const keys = Array.from(displayedLogs);
keys.slice(0, 500).forEach(k => displayedLogs.delete(k));
}
const line = document.createElement('div');
line.className = `log-line ${type}`;
// 添加时间戳
const timestamp = new Date().toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
line.innerHTML = `<span class="timestamp">[${timestamp}]</span>${escapeHtml(message)}`;
elements.consoleLog.appendChild(line);
// 自动滚动到底部
elements.consoleLog.scrollTop = elements.consoleLog.scrollHeight;
// 限制日志行数
const lines = elements.consoleLog.querySelectorAll('.log-line');
if (lines.length > 500) {
lines[0].remove();
}
}
// 获取日志类型
function getLogType(log) {
if (typeof log !== 'string') return 'info';
const lowerLog = log.toLowerCase();
if (lowerLog.includes('error') || lowerLog.includes('失败') || lowerLog.includes('错误')) {
return 'error';
}
if (lowerLog.includes('warning') || lowerLog.includes('警告')) {
return 'warning';
}
if (lowerLog.includes('success') || lowerLog.includes('成功') || lowerLog.includes('完成')) {
return 'success';
}
return 'info';
}
// 重置按钮状态
function resetButtons() {
elements.startBtn.disabled = false;
elements.cancelBtn.disabled = true;
currentTask = null;
currentBatch = null;
isBatchMode = false;
// 重置完成标志
taskCompleted = false;
batchCompleted = false;
// 重置最终状态标志
taskFinalStatus = null;
batchFinalStatus = null;
// 清除活跃任务标识
activeTaskUuid = null;
activeBatchId = null;
// 清除 sessionStorage 持久化状态
sessionStorage.removeItem('activeTask');
// 断开 WebSocket
disconnectWebSocket();
disconnectBatchWebSocket();
// 注意:不重置 isOutlookBatchMode因为用户可能想继续使用 Outlook 批量模式
}
// HTML 转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============== Outlook 批量注册功能 ==============
// 加载 Outlook 账户列表
async function loadOutlookAccounts() {
try {
elements.outlookAccountsContainer.innerHTML = '<div class="loading-placeholder" style="text-align: center; padding: var(--spacing-md); color: var(--text-muted);">加载中...</div>';
const data = await api.get('/registration/outlook-accounts');
outlookAccounts = data.accounts || [];
renderOutlookAccountsList();
addLog('info', `[系统] 已加载 ${data.total} 个 Outlook 账户 (已注册: ${data.registered_count}, 未注册: ${data.unregistered_count})`);
} catch (error) {
console.error('加载 Outlook 账户列表失败:', error);
elements.outlookAccountsContainer.innerHTML = `<div style="text-align: center; padding: var(--spacing-md); color: var(--text-muted);">加载失败: ${error.message}</div>`;
addLog('error', `[错误] 加载 Outlook 账户列表失败: ${error.message}`);
}
}
// 渲染 Outlook 账户列表
function renderOutlookAccountsList() {
if (outlookAccounts.length === 0) {
elements.outlookAccountsContainer.innerHTML = '<div style="text-align: center; padding: var(--spacing-md); color: var(--text-muted);">没有可用的 Outlook 账户</div>';
return;
}
const html = outlookAccounts.map(account => `
<label class="outlook-account-item" style="display: flex; align-items: center; padding: var(--spacing-sm); border-bottom: 1px solid var(--border-light); cursor: pointer; ${account.is_registered ? 'opacity: 0.6;' : ''}" data-id="${account.id}" data-registered="${account.is_registered}">
<input type="checkbox" class="outlook-account-checkbox" value="${account.id}" ${account.is_registered ? '' : 'checked'} style="margin-right: var(--spacing-sm);">
<div style="flex: 1;">
<div style="font-weight: 500;">${escapeHtml(account.email)}</div>
<div style="font-size: 0.75rem; color: var(--text-muted);">
${account.is_registered
? `<span style="color: var(--success-color);">✓ 已注册</span>`
: '<span style="color: var(--primary-color);">未注册</span>'
}
${account.has_oauth ? ' | OAuth' : ''}
</div>
</div>
</label>
`).join('');
elements.outlookAccountsContainer.innerHTML = html;
}
// 全选
function selectAllOutlookAccounts() {
const checkboxes = document.querySelectorAll('.outlook-account-checkbox');
checkboxes.forEach(cb => cb.checked = true);
}
// 只选未注册
function selectUnregisteredOutlook() {
const items = document.querySelectorAll('.outlook-account-item');
items.forEach(item => {
const checkbox = item.querySelector('.outlook-account-checkbox');
const isRegistered = item.dataset.registered === 'true';
checkbox.checked = !isRegistered;
});
}
// 取消全选
function deselectAllOutlookAccounts() {
const checkboxes = document.querySelectorAll('.outlook-account-checkbox');
checkboxes.forEach(cb => cb.checked = false);
}
// 处理 Outlook 批量注册
async function handleOutlookBatchRegistration() {
// 重置批量任务状态
batchCompleted = false;
batchFinalStatus = null;
displayedLogs.clear(); // 清空日志去重集合
toastShown = false; // 重置 toast 标志
// 获取选中的账户
const selectedIds = [];
document.querySelectorAll('.outlook-account-checkbox:checked').forEach(cb => {
selectedIds.push(parseInt(cb.value));
});
if (selectedIds.length === 0) {
toast.error('请选择至少一个 Outlook 账户');
return;
}
const intervalMin = parseInt(elements.outlookIntervalMin.value) || 5;
const intervalMax = parseInt(elements.outlookIntervalMax.value) || 30;
const skipRegistered = elements.outlookSkipRegistered.checked;
const concurrency = parseInt(elements.outlookConcurrencyCount.value) || 3;
const mode = elements.outlookConcurrencyMode.value || 'pipeline';
// 禁用开始按钮
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
// 清空日志
elements.consoleLog.innerHTML = '';
const requestData = {
service_ids: selectedIds,
skip_registered: skipRegistered,
interval_min: intervalMin,
interval_max: intervalMax,
concurrency: Math.min(50, Math.max(1, concurrency)),
mode: mode,
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false,
cpa_service_ids: elements.autoUploadCpa && elements.autoUploadCpa.checked ? getSelectedServiceIds(elements.cpaServiceSelect) : [],
auto_upload_sub2api: elements.autoUploadSub2api ? elements.autoUploadSub2api.checked : false,
sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
...(elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSub2apiAdvancedConfig() : {}),
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
};
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
try {
const data = await api.post('/registration/outlook-batch', requestData);
if (data.to_register === 0) {
addLog('warning', '[警告] 所有选中的邮箱都已注册,无需重复注册');
toast.warning('所有选中的邮箱都已注册');
resetButtons();
return;
}
currentBatch = { batch_id: data.batch_id, ...data };
activeBatchId = data.batch_id; // 保存用于重连
// 持久化到 sessionStorage跨页面导航后可恢复
sessionStorage.setItem('activeTask', JSON.stringify({ batch_id: data.batch_id, mode: isOutlookBatchMode ? 'outlook_batch' : 'batch', total: data.to_register }));
addLog('info', `[系统] 批量任务已创建: ${data.batch_id}`);
addLog('info', `[系统] 总数: ${data.total}, 跳过已注册: ${data.skipped}, 待注册: ${data.to_register}`);
// 初始化批量状态显示
showBatchStatus({ count: data.to_register });
// 优先使用 WebSocket
connectBatchWebSocket(data.batch_id);
} catch (error) {
addLog('error', `[错误] 启动失败: ${error.message}`);
toast.error(error.message);
resetButtons();
}
}
// ============== 批量任务 WebSocket 功能 ==============
// 连接批量任务 WebSocket
function connectBatchWebSocket(batchId) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws/batch/${batchId}`;
try {
batchWebSocket = new WebSocket(wsUrl);
batchWebSocket.onopen = () => {
console.log('批量任务 WebSocket 连接成功');
// 停止轮询(如果有)
stopBatchPolling();
// 开始心跳
startBatchWebSocketHeartbeat();
};
batchWebSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'log') {
const logType = getLogType(data.message);
addLog(logType, data.message);
} else if (data.type === 'status') {
// 更新进度
if (data.total !== undefined) {
updateBatchProgress({
total: data.total,
completed: data.completed || 0,
success: data.success || 0,
failed: data.failed || 0
});
}
// 检查是否完成
if (['completed', 'failed', 'cancelled', 'cancelling'].includes(data.status)) {
// 保存最终状态,用于 onclose 判断
batchFinalStatus = data.status;
batchCompleted = true;
// 断开 WebSocket异步操作
disconnectBatchWebSocket();
// 任务完成后再重置按钮
resetButtons();
// 只显示一次 toast
if (!toastShown) {
toastShown = true;
if (data.status === 'completed') {
addLog('success', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`);
if (data.success > 0) {
toast.success(`Outlook 批量注册完成,成功 ${data.success}`);
loadRecentAccounts();
} else {
toast.warning('Outlook 批量注册完成,但没有成功注册任何账号');
}
} else if (data.status === 'failed') {
addLog('error', '[错误] 批量任务执行失败');
toast.error('批量任务执行失败');
} else if (data.status === 'cancelled' || data.status === 'cancelling') {
addLog('warning', '[警告] 批量任务已取消');
}
}
}
} else if (data.type === 'pong') {
// 心跳响应,忽略
}
};
batchWebSocket.onclose = (event) => {
console.log('批量任务 WebSocket 连接关闭:', event.code);
stopBatchWebSocketHeartbeat();
// 只有在任务未完成且最终状态不是完成状态时才切换到轮询
// 使用 batchFinalStatus 而不是 currentBatch.status因为 currentBatch 可能已被重置
const shouldPoll = !batchCompleted &&
batchFinalStatus === null; // 如果 batchFinalStatus 有值,说明任务已完成
if (shouldPoll && currentBatch) {
console.log('切换到轮询模式');
startOutlookBatchPolling(currentBatch.batch_id);
}
};
batchWebSocket.onerror = (error) => {
console.error('批量任务 WebSocket 错误:', error);
stopBatchWebSocketHeartbeat();
// 切换到轮询
startOutlookBatchPolling(batchId);
};
} catch (error) {
console.error('批量任务 WebSocket 连接失败:', error);
startOutlookBatchPolling(batchId);
}
}
// 断开批量任务 WebSocket
function disconnectBatchWebSocket() {
stopBatchWebSocketHeartbeat();
if (batchWebSocket) {
batchWebSocket.close();
batchWebSocket = null;
}
}
// 开始批量任务心跳
function startBatchWebSocketHeartbeat() {
stopBatchWebSocketHeartbeat();
batchWsHeartbeatInterval = setInterval(() => {
if (batchWebSocket && batchWebSocket.readyState === WebSocket.OPEN) {
batchWebSocket.send(JSON.stringify({ type: 'ping' }));
}
}, 25000); // 每 25 秒发送一次心跳
}
// 停止批量任务心跳
function stopBatchWebSocketHeartbeat() {
if (batchWsHeartbeatInterval) {
clearInterval(batchWsHeartbeatInterval);
batchWsHeartbeatInterval = null;
}
}
// 发送批量任务取消请求
function cancelBatchViaWebSocket() {
if (batchWebSocket && batchWebSocket.readyState === WebSocket.OPEN) {
batchWebSocket.send(JSON.stringify({ type: 'cancel' }));
}
}
// 开始轮询 Outlook 批量状态(降级方案)
function startOutlookBatchPolling(batchId) {
batchPollingInterval = setInterval(async () => {
try {
const data = await api.get(`/registration/outlook-batch/${batchId}`);
// 更新进度
updateBatchProgress({
total: data.total,
completed: data.completed,
success: data.success,
failed: data.failed
});
// 输出日志
if (data.logs && data.logs.length > 0) {
const lastLogIndex = batchPollingInterval.lastLogIndex || 0;
for (let i = lastLogIndex; i < data.logs.length; i++) {
const log = data.logs[i];
const logType = getLogType(log);
addLog(logType, log);
}
batchPollingInterval.lastLogIndex = data.logs.length;
}
// 检查是否完成
if (data.finished) {
stopBatchPolling();
resetButtons();
// 只显示一次 toast
if (!toastShown) {
toastShown = true;
addLog('info', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`);
if (data.success > 0) {
toast.success(`Outlook 批量注册完成,成功 ${data.success}`);
loadRecentAccounts();
} else {
toast.warning('Outlook 批量注册完成,但没有成功注册任何账号');
}
}
}
} catch (error) {
console.error('轮询 Outlook 批量状态失败:', error);
}
}, 2000);
batchPollingInterval.lastLogIndex = 0;
}
// ============== 页面可见性重连机制 ==============
function initVisibilityReconnect() {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return;
// 页面重新可见时,检查是否需要重连(针对同页面标签切换场景)
const wsDisconnected = !webSocket || webSocket.readyState === WebSocket.CLOSED;
const batchWsDisconnected = !batchWebSocket || batchWebSocket.readyState === WebSocket.CLOSED;
// 单任务重连
if (activeTaskUuid && !taskCompleted && wsDisconnected) {
console.log('[重连] 页面重新可见,重连单任务 WebSocket:', activeTaskUuid);
addLog('info', '[系统] 页面重新激活,正在重连任务监控...');
connectWebSocket(activeTaskUuid);
}
// 批量任务重连
if (activeBatchId && !batchCompleted && batchWsDisconnected) {
console.log('[重连] 页面重新可见,重连批量任务 WebSocket:', activeBatchId);
addLog('info', '[系统] 页面重新激活,正在重连批量任务监控...');
connectBatchWebSocket(activeBatchId);
}
});
}
// 页面加载时恢复进行中的任务(处理跨页面导航后回到注册页的情况)
async function restoreActiveTask() {
const saved = sessionStorage.getItem('activeTask');
if (!saved) return;
let state;
try {
state = JSON.parse(saved);
} catch {
sessionStorage.removeItem('activeTask');
return;
}
const { mode, task_uuid, batch_id, total } = state;
if (mode === 'single' && task_uuid) {
// 查询任务是否仍在运行
try {
const data = await api.get(`/registration/tasks/${task_uuid}`);
if (['completed', 'failed', 'cancelled'].includes(data.status)) {
sessionStorage.removeItem('activeTask');
return;
}
// 任务仍在运行,恢复状态
currentTask = data;
activeTaskUuid = task_uuid;
taskCompleted = false;
taskFinalStatus = null;
toastShown = false;
displayedLogs.clear();
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
showTaskStatus(data);
updateTaskStatus(data.status);
addLog('info', `[系统] 检测到进行中的任务,正在重连监控... (${task_uuid.substring(0, 8)})`);
connectWebSocket(task_uuid);
} catch {
sessionStorage.removeItem('activeTask');
}
} else if ((mode === 'batch' || mode === 'outlook_batch') && batch_id) {
// 查询批量任务是否仍在运行
const endpoint = mode === 'outlook_batch'
? `/registration/outlook-batch/${batch_id}`
: `/registration/batch/${batch_id}`;
try {
const data = await api.get(endpoint);
if (data.finished) {
sessionStorage.removeItem('activeTask');
return;
}
// 批量任务仍在运行,恢复状态
currentBatch = { batch_id, ...data };
activeBatchId = batch_id;
isOutlookBatchMode = (mode === 'outlook_batch');
batchCompleted = false;
batchFinalStatus = null;
toastShown = false;
displayedLogs.clear();
elements.startBtn.disabled = true;
elements.cancelBtn.disabled = false;
showBatchStatus({ count: total || data.total });
updateBatchProgress(data);
addLog('info', `[系统] 检测到进行中的批量任务,正在重连监控... (${batch_id.substring(0, 8)})`);
connectBatchWebSocket(batch_id);
} catch {
sessionStorage.removeItem('activeTask');
}
}
}