${account.is_registered
- ? `
✓ 已注册${account.registered_account_id ? ` #${account.registered_account_id}` : ''}`
+ ? `
✓ 已注册`
: '
未注册'
}
${account.has_oauth ? ' | OAuth' : ''}
@@ -1416,7 +2160,7 @@ async function handleOutlookBatchRegistration() {
const requestData = {
service_ids: selectedIds,
skip_registered: skipRegistered,
- registration_type: elements.registrationType ? elements.registrationType.value : 'none',
+ registration_type: elements.registrationType ? elements.registrationType.value : 'child',
interval_min: intervalMin,
interval_max: intervalMax,
concurrency: Math.min(50, Math.max(1, concurrency)),
@@ -1427,6 +2171,8 @@ async function handleOutlookBatchRegistration() {
sub2api_service_ids: elements.autoUploadSub2api && elements.autoUploadSub2api.checked ? getSelectedServiceIds(elements.sub2apiServiceSelect) : [],
auto_upload_tm: elements.autoUploadTm ? elements.autoUploadTm.checked : false,
tm_service_ids: elements.autoUploadTm && elements.autoUploadTm.checked ? getSelectedServiceIds(elements.tmServiceSelect) : [],
+ auto_upload_new_api: elements.autoUploadNewApi ? elements.autoUploadNewApi.checked : false,
+ new_api_service_ids: elements.autoUploadNewApi && elements.autoUploadNewApi.checked ? getSelectedServiceIds(elements.newApiServiceSelect) : [],
};
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
@@ -1441,7 +2187,7 @@ async function handleOutlookBatchRegistration() {
return;
}
- currentBatch = { batch_id: data.batch_id, ...data };
+ currentBatch = { batch_id: data.batch_id, ...data, pollingMode: 'outlook_batch' };
activeBatchId = data.batch_id; // 保存用于重连
// 持久化到 sessionStorage,跨页面导航后可恢复
sessionStorage.setItem('activeTask', JSON.stringify({ batch_id: data.batch_id, mode: isOutlookBatchMode ? 'outlook_batch' : 'batch', total: data.to_register }));
@@ -1452,7 +2198,7 @@ async function handleOutlookBatchRegistration() {
showBatchStatus({ count: data.to_register });
// 优先使用 WebSocket
- connectBatchWebSocket(data.batch_id, 'outlook_batch');
+ connectBatchWebSocket(data.batch_id);
} catch (error) {
addLog('error', `[错误] 启动失败: ${error.message}`);
@@ -1463,41 +2209,39 @@ async function handleOutlookBatchRegistration() {
// ============== 批量任务 WebSocket 功能 ==============
-function normalizeBatchMode(mode) {
- const text = String(mode || '').trim().toLowerCase();
- if (text === 'outlook_batch') return 'outlook_batch';
- if (text === 'batch') return 'batch';
- return isOutlookBatchMode ? 'outlook_batch' : 'batch';
-}
+// 连接批量任务 WebSocket
+function connectBatchWebSocket(batchId) {
+ activeBatchId = batchId;
-function startCurrentBatchPolling(batchId, mode = null) {
- const normalizedMode = normalizeBatchMode(mode);
- if (normalizedMode === 'outlook_batch') {
- startOutlookBatchPolling(batchId);
+ if (batchWebSocket && [WebSocket.OPEN, WebSocket.CONNECTING].includes(batchWebSocket.readyState)) {
return;
}
- startBatchPolling(batchId);
-}
-// 连接批量任务 WebSocket
-function connectBatchWebSocket(batchId, mode = null) {
- const batchMode = normalizeBatchMode(mode);
- const batchLabel = batchMode === 'outlook_batch' ? 'Outlook 批量任务' : '批量任务';
+ if (batchWsReconnectTimer) {
+ clearTimeout(batchWsReconnectTimer);
+ batchWsReconnectTimer = null;
+ }
+ batchWsManualClose = false;
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/ws/batch/${batchId}`;
try {
- batchWebSocket = new WebSocket(wsUrl);
+ const socket = new WebSocket(wsUrl);
+ batchWebSocket = socket;
- batchWebSocket.onopen = () => {
+ socket.onopen = () => {
+ if (batchWebSocket !== socket) return;
console.log('批量任务 WebSocket 连接成功');
+ clearBatchWebSocketReconnect();
// 停止轮询(如果有)
stopBatchPolling();
// 开始心跳
startBatchWebSocketHeartbeat();
};
- batchWebSocket.onmessage = (event) => {
+ socket.onmessage = (event) => {
+ if (batchWebSocket !== socket) return;
const data = JSON.parse(event.data);
if (data.type === 'log') {
@@ -1530,12 +2274,12 @@ function connectBatchWebSocket(batchId, mode = null) {
if (!toastShown) {
toastShown = true;
if (data.status === 'completed') {
- addLog('success', `[完成] ${batchLabel}完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`);
+ addLog('success', `[完成] Outlook 批量任务完成!成功: ${data.success}, 失败: ${data.failed}, 跳过: ${data.skipped || 0}`);
if (data.success > 0) {
- toast.success(`${batchLabel}完成,成功 ${data.success} 个`);
+ toast.success(`Outlook 批量注册完成,成功 ${data.success} 个`);
loadRecentAccounts();
} else {
- toast.warning(`${batchLabel}完成,但没有成功注册任何账号`);
+ toast.warning('Outlook 批量注册完成,但没有成功注册任何账号');
}
} else if (data.status === 'failed') {
addLog('error', '[错误] 批量任务执行失败');
@@ -1550,40 +2294,49 @@ function connectBatchWebSocket(batchId, mode = null) {
}
};
- batchWebSocket.onclose = (event) => {
+ socket.onclose = (event) => {
+ const isCurrentSocket = batchWebSocket === socket;
+ if (isCurrentSocket) {
+ batchWebSocket = null;
+ stopBatchWebSocketHeartbeat();
+ }
+
console.log('批量任务 WebSocket 连接关闭:', event.code);
- stopBatchWebSocketHeartbeat();
- // 只有在任务未完成且最终状态不是完成状态时才切换到轮询
- // 使用 batchFinalStatus 而不是 currentBatch.status,因为 currentBatch 可能已被重置
- const shouldPoll = !batchCompleted &&
- batchFinalStatus === null; // 如果 batchFinalStatus 有值,说明任务已完成
+ const shouldReconnect = isCurrentSocket &&
+ !batchWsManualClose &&
+ !batchCompleted &&
+ batchFinalStatus === null &&
+ activeBatchId === batchId;
- if (shouldPoll && currentBatch) {
- console.log('切换到轮询模式');
- startCurrentBatchPolling(currentBatch.batch_id, batchMode);
+ if (shouldReconnect) {
+ console.log('批量任务 WebSocket 断开,准备自动重连');
+ startCurrentBatchPolling(batchId);
+ scheduleBatchWebSocketReconnect(batchId);
}
};
- batchWebSocket.onerror = (error) => {
+ socket.onerror = (error) => {
+ if (batchWebSocket !== socket) return;
console.error('批量任务 WebSocket 错误:', error);
- stopBatchWebSocketHeartbeat();
- // 切换到轮询
- startCurrentBatchPolling(batchId, batchMode);
};
} catch (error) {
console.error('批量任务 WebSocket 连接失败:', error);
- startCurrentBatchPolling(batchId, batchMode);
+ startCurrentBatchPolling(batchId);
+ scheduleBatchWebSocketReconnect(batchId);
}
}
// 断开批量任务 WebSocket
function disconnectBatchWebSocket() {
+ batchWsManualClose = true;
+ clearBatchWebSocketReconnect();
stopBatchWebSocketHeartbeat();
if (batchWebSocket) {
- batchWebSocket.close();
+ const socket = batchWebSocket;
batchWebSocket = null;
+ socket.close();
}
}
@@ -1614,22 +2367,13 @@ function cancelBatchViaWebSocket() {
// 开始轮询 Outlook 批量状态(降级方案)
function startOutlookBatchPolling(batchId) {
- stopBatchPolling();
- let lastLogIndex = 0;
- outlookBatchPollingInFlight = false;
+ if (batchPollingInterval) {
+ return;
+ }
+
batchPollingInterval = setInterval(async () => {
- if (outlookBatchPollingInFlight) return;
- outlookBatchPollingInFlight = true;
try {
- const data = await api.get(`/registration/outlook-batch/${batchId}`, {
- timeoutMs: 15000,
- retry: 0,
- requestKey: `app:outlook-batch-status:${batchId}`,
- cancelPrevious: true,
- silentNetworkError: true,
- silentTimeoutError: true,
- priority: 'low',
- });
+ const data = await api.get(`/registration/outlook-batch/${batchId}`);
// 更新进度
updateBatchProgress({
@@ -1641,12 +2385,13 @@ function startOutlookBatchPolling(batchId) {
// 输出日志
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);
}
- lastLogIndex = data.logs.length;
+ batchPollingInterval.lastLogIndex = data.logs.length;
}
// 检查是否完成
@@ -1667,13 +2412,11 @@ function startOutlookBatchPolling(batchId) {
}
}
} catch (error) {
- if (!isIgnorableBackgroundError(error)) {
- console.error('轮询 Outlook 批量状态失败:', error);
- }
- } finally {
- outlookBatchPollingInFlight = false;
+ console.error('轮询 Outlook 批量状态失败:', error);
}
}, 2000);
+
+ batchPollingInterval.lastLogIndex = 0;
}
// ============== 页面可见性重连机制 ==============
@@ -1697,7 +2440,12 @@ function initVisibilityReconnect() {
if (activeBatchId && !batchCompleted && batchWsDisconnected) {
console.log('[重连] 页面重新可见,重连批量任务 WebSocket:', activeBatchId);
addLog('info', '[系统] 页面重新激活,正在重连批量任务监控...');
- connectBatchWebSocket(activeBatchId, isOutlookBatchMode ? 'outlook_batch' : 'batch');
+ connectBatchWebSocket(activeBatchId);
+ }
+
+ if (isAutoMode && !autoMonitorPollingInterval) {
+ addLog('info', '[系统] 页面重新激活,正在恢复自动注册监控...');
+ startAutoRegistrationMonitor();
}
});
}
@@ -1753,7 +2501,7 @@ async function restoreActiveTask() {
return;
}
// 批量任务仍在运行,恢复状态
- currentBatch = { batch_id, ...data };
+ currentBatch = { batch_id, ...data, pollingMode: mode };
activeBatchId = batch_id;
isOutlookBatchMode = (mode === 'outlook_batch');
batchCompleted = false;
@@ -1765,10 +2513,26 @@ async function restoreActiveTask() {
showBatchStatus({ count: total || data.total });
updateBatchProgress(data);
addLog('info', `[系统] 检测到进行中的批量任务,正在重连监控... (${batch_id.substring(0, 8)})`);
- connectBatchWebSocket(batch_id, mode);
+ connectBatchWebSocket(batch_id);
} catch {
sessionStorage.removeItem('activeTask');
}
+ } else if (mode === 'auto') {
+ sessionStorage.removeItem('activeTask');
}
}
+
+async function refreshOutlookRegistrationStatus() {
+ try {
+ const ids = outlookAccounts.map(item => item.id).filter(Boolean);
+ const data = await api.post('/registration/outlook/check-accounts', { service_ids: ids });
+ outlookAccounts = data.accounts || [];
+ renderOutlookAccounts(outlookAccounts);
+ addLog('info', `[??] Outlook ??????? (???: ${data.registered_count}, ???: ${data.unregistered_count})`);
+ toast.success('Outlook ???????');
+ } catch (error) {
+ console.error('?? Outlook ??????:', error);
+ toast.error('?? Outlook ??????: ' + error.message);
+ }
+}
diff --git a/static/js/email_services.js b/static/js/email_services.js
index 1f9ea7d9..be426a16 100644
--- a/static/js/email_services.js
+++ b/static/js/email_services.js
@@ -41,6 +41,11 @@ const elements = {
tempmailApi: document.getElementById('tempmail-api'),
tempmailEnabled: document.getElementById('tempmail-enabled'),
testTempmailBtn: document.getElementById('test-tempmail-btn'),
+ yydsApi: document.getElementById('yyds-api'),
+ yydsApiKey: document.getElementById('yyds-api-key'),
+ yydsDefaultDomain: document.getElementById('yyds-default-domain'),
+ yydsEnabled: document.getElementById('yyds-enabled'),
+ testYydsBtn: document.getElementById('test-yyds-btn'),
// 添加自定义域名模态框
addCustomModal: document.getElementById('add-custom-modal'),
@@ -49,8 +54,11 @@ const elements = {
cancelAddCustom: document.getElementById('cancel-add-custom'),
customSubType: document.getElementById('custom-sub-type'),
addMoemailFields: document.getElementById('add-moemail-fields'),
+ addTempmailBuiltinFields: document.getElementById('add-tempmail-builtin-fields'),
+ addYydsFields: document.getElementById('add-yyds-fields'),
addTempmailFields: document.getElementById('add-tempmail-fields'),
addDuckmailFields: document.getElementById('add-duckmail-fields'),
+ addLuckmailFields: document.getElementById('add-luckmail-fields'),
addFreemailFields: document.getElementById('add-freemail-fields'),
addImapFields: document.getElementById('add-imap-fields'),
@@ -60,8 +68,11 @@ const elements = {
closeEditCustomModal: document.getElementById('close-edit-custom-modal'),
cancelEditCustom: document.getElementById('cancel-edit-custom'),
editMoemailFields: document.getElementById('edit-moemail-fields'),
+ editTempmailBuiltinFields: document.getElementById('edit-tempmail-builtin-fields'),
+ editYydsFields: document.getElementById('edit-yyds-fields'),
editTempmailFields: document.getElementById('edit-tempmail-fields'),
editDuckmailFields: document.getElementById('edit-duckmail-fields'),
+ editLuckmailFields: document.getElementById('edit-luckmail-fields'),
editFreemailFields: document.getElementById('edit-freemail-fields'),
editImapFields: document.getElementById('edit-imap-fields'),
editCustomTypeBadge: document.getElementById('edit-custom-type-badge'),
@@ -76,9 +87,12 @@ const elements = {
const CUSTOM_SUBTYPE_LABELS = {
moemail: '🔗 MoeMail(自定义域名 API)',
+ tempmail_builtin: 'Tempmail.lol(官方渠道)',
+ yyds_mail: 'YYDS Mail(官方渠道)',
tempmail: '📮 TempMail(自部署 Cloudflare Worker)',
cloudmail: '☁️ CloudMail(自部署 Cloudflare Worker)',
duckmail: '🦆 DuckMail(DuckMail API)',
+ luckmail: 'LuckMail(接码平台)',
freemail: 'Freemail(自部署 Cloudflare Worker)',
imap: '📧 IMAP 邮箱(Gmail/QQ/163等)'
};
@@ -160,6 +174,7 @@ function initEventListeners() {
// 临时邮箱配置
elements.tempmailForm.addEventListener('submit', handleSaveTempmail);
elements.testTempmailBtn.addEventListener('click', handleTestTempmail);
+ elements.testYydsBtn.addEventListener('click', handleTestYyds);
// 点击其他地方关闭更多菜单
document.addEventListener('click', () => {
@@ -183,8 +198,11 @@ function closeEmailMoreMenu(el) {
function switchAddSubType(subType) {
elements.customSubType.value = subType;
elements.addMoemailFields.style.display = subType === 'moemail' ? '' : 'none';
+ elements.addTempmailBuiltinFields.style.display = subType === 'tempmail_builtin' ? '' : 'none';
+ elements.addYydsFields.style.display = subType === 'yyds_mail' ? '' : 'none';
elements.addTempmailFields.style.display = (subType === 'tempmail' || subType === 'cloudmail') ? '' : 'none';
elements.addDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
+ elements.addLuckmailFields.style.display = subType === 'luckmail' ? '' : 'none';
elements.addFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
elements.addImapFields.style.display = subType === 'imap' ? '' : 'none';
}
@@ -193,8 +211,11 @@ function switchAddSubType(subType) {
function switchEditSubType(subType) {
elements.editCustomSubTypeHidden.value = subType;
elements.editMoemailFields.style.display = subType === 'moemail' ? '' : 'none';
+ elements.editTempmailBuiltinFields.style.display = subType === 'tempmail_builtin' ? '' : 'none';
+ elements.editYydsFields.style.display = subType === 'yyds_mail' ? '' : 'none';
elements.editTempmailFields.style.display = (subType === 'tempmail' || subType === 'cloudmail') ? '' : 'none';
elements.editDuckmailFields.style.display = subType === 'duckmail' ? '' : 'none';
+ elements.editLuckmailFields.style.display = subType === 'luckmail' ? '' : 'none';
elements.editFreemailFields.style.display = subType === 'freemail' ? '' : 'none';
elements.editImapFields.style.display = subType === 'imap' ? '' : 'none';
elements.editCustomTypeBadge.textContent = CUSTOM_SUBTYPE_LABELS[subType] || CUSTOM_SUBTYPE_LABELS.moemail;
@@ -205,7 +226,7 @@ async function loadStats() {
try {
const data = await api.get('/email-services/stats');
elements.outlookCount.textContent = data.outlook_count || 0;
- elements.customCount.textContent = (data.custom_count || 0) + (data.temp_mail_count || 0) + (data.cloudmail_count || 0) + (data.duck_mail_count || 0) + (data.freemail_count || 0) + (data.imap_mail_count || 0);
+ elements.customCount.textContent = (data.custom_count || 0) + (data.tempmail_builtin_count || 0) + (data.yyds_mail_count || 0) + (data.temp_mail_count || 0) + (data.cloudmail_count || 0) + (data.duck_mail_count || 0) + (data.luckmail_count || 0) + (data.freemail_count || 0) + (data.imap_mail_count || 0);
elements.tempmailStatus.textContent = data.tempmail_available ? '可用' : '不可用';
elements.totalEnabled.textContent = data.enabled_count || 0;
} catch (error) {
@@ -298,6 +319,12 @@ function getCustomServiceTypeBadge(subType) {
if (subType === 'moemail') {
return '
MoeMail';
}
+ if (subType === 'tempmail_builtin') {
+ return '
Tempmail.lol';
+ }
+ if (subType === 'yyds_mail') {
+ return '
YYDS Mail';
+ }
if (subType === 'tempmail') {
return '
TempMail';
}
@@ -307,6 +334,9 @@ function getCustomServiceTypeBadge(subType) {
if (subType === 'duckmail') {
return '
DuckMail';
}
+ if (subType === 'luckmail') {
+ return '
LuckMail';
+ }
if (subType === 'freemail') {
return '
Freemail';
}
@@ -319,6 +349,28 @@ function getCustomServiceAddress(service) {
const emailAddr = service.config?.email || '';
return `${escapeHtml(host)}
${escapeHtml(emailAddr)}
`;
}
+ if (service._subType === 'tempmail_builtin') {
+ const baseUrl = service.config?.base_url || '-';
+ const timeout = service.config?.timeout || 30;
+ return `${escapeHtml(baseUrl)}
超时:${escapeHtml(String(timeout))} 秒
`;
+ }
+ if (service._subType === 'yyds_mail') {
+ const baseUrl = service.config?.base_url || '-';
+ const domain = service.config?.default_domain || '';
+ if (!domain) {
+ return escapeHtml(baseUrl);
+ }
+ return `${escapeHtml(baseUrl)}
默认域名:@${escapeHtml(domain)}
`;
+ }
+ if (service._subType === 'luckmail') {
+ const baseUrl = service.config?.base_url || '-';
+ const projectCode = service.config?.project_code || 'openai';
+ const preferredDomain = service.config?.preferred_domain || '';
+ const detail = preferredDomain
+ ? `项目:${escapeHtml(projectCode)} / 优先域名:${escapeHtml(preferredDomain)}`
+ : `项目:${escapeHtml(projectCode)}`;
+ return `${escapeHtml(baseUrl)}
${detail}
`;
+ }
const baseUrl = service.config?.base_url || '-';
const domain = service.config?.default_domain || service.config?.domain;
if (!domain) {
@@ -330,21 +382,27 @@ function getCustomServiceAddress(service) {
// 加载自定义邮箱服务(moe_mail + temp_mail + cloudmail + duck_mail + freemail + imap_mail 合并)
async function loadCustomServices() {
try {
- const [r1, r2, r3, r4, r5, r6] = await Promise.all([
+ const [r1, r2, r3, r4, r5, r6, r7, r8, r9] = await Promise.all([
api.get('/email-services?service_type=moe_mail'),
+ api.get('/email-services?service_type=tempmail'),
+ api.get('/email-services?service_type=yyds_mail'),
api.get('/email-services?service_type=temp_mail'),
api.get('/email-services?service_type=cloudmail'),
api.get('/email-services?service_type=duck_mail'),
+ api.get('/email-services?service_type=luckmail'),
api.get('/email-services?service_type=freemail'),
api.get('/email-services?service_type=imap_mail')
]);
customServices = [
...(r1.services || []).map(s => ({ ...s, _subType: 'moemail' })),
- ...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail' })),
- ...(r3.services || []).map(s => ({ ...s, _subType: 'cloudmail' })),
- ...(r4.services || []).map(s => ({ ...s, _subType: 'duckmail' })),
- ...(r5.services || []).map(s => ({ ...s, _subType: 'freemail' })),
- ...(r6.services || []).map(s => ({ ...s, _subType: 'imap' }))
+ ...(r2.services || []).map(s => ({ ...s, _subType: 'tempmail_builtin' })),
+ ...(r3.services || []).map(s => ({ ...s, _subType: 'yyds_mail' })),
+ ...(r4.services || []).map(s => ({ ...s, _subType: 'tempmail' })),
+ ...(r5.services || []).map(s => ({ ...s, _subType: 'cloudmail' })),
+ ...(r6.services || []).map(s => ({ ...s, _subType: 'duckmail' })),
+ ...(r7.services || []).map(s => ({ ...s, _subType: 'luckmail' })),
+ ...(r8.services || []).map(s => ({ ...s, _subType: 'freemail' })),
+ ...(r9.services || []).map(s => ({ ...s, _subType: 'imap' }))
];
if (customServices.length === 0) {
@@ -410,6 +468,13 @@ async function loadTempmailConfig() {
elements.tempmailApi.value = settings.tempmail.api_url || '';
elements.tempmailEnabled.checked = settings.tempmail.enabled !== false;
}
+ if (settings.yyds_mail) {
+ elements.yydsApi.value = settings.yyds_mail.api_url || '';
+ elements.yydsDefaultDomain.value = settings.yyds_mail.default_domain || '';
+ elements.yydsEnabled.checked = settings.yyds_mail.enabled === true;
+ elements.yydsApiKey.value = '';
+ elements.yydsApiKey.placeholder = settings.yyds_mail.has_api_key ? '已设置,留空保持不变' : '请输入 YYDS Mail API Key';
+ }
} catch (error) {
// 忽略错误
}
@@ -467,6 +532,22 @@ async function handleAddCustom(e) {
api_key: formData.get('api_key'),
default_domain: formData.get('domain')
};
+ } else if (subType === 'tempmail_builtin') {
+ serviceType = 'tempmail';
+ config = {
+ base_url: formData.get('tpo_base_url'),
+ timeout: parseInt(formData.get('tpo_timeout'), 10) || 30,
+ max_retries: parseInt(formData.get('tpo_max_retries'), 10) || 3
+ };
+ } else if (subType === 'yyds_mail') {
+ serviceType = 'yyds_mail';
+ config = {
+ base_url: formData.get('yyds_base_url'),
+ api_key: formData.get('yyds_api_key'),
+ default_domain: formData.get('yyds_default_domain'),
+ timeout: parseInt(formData.get('yyds_timeout'), 10) || 30,
+ max_retries: parseInt(formData.get('yyds_max_retries'), 10) || 3
+ };
} else if (subType === 'tempmail' || subType === 'cloudmail') {
serviceType = subType === 'cloudmail' ? 'cloudmail' : 'temp_mail';
config = {
@@ -483,6 +564,15 @@ async function handleAddCustom(e) {
default_domain: formData.get('dm_domain'),
password_length: parseInt(formData.get('dm_password_length'), 10) || 12
};
+ } else if (subType === 'luckmail') {
+ serviceType = 'luckmail';
+ config = {
+ base_url: formData.get('lm_base_url'),
+ api_key: formData.get('lm_api_key'),
+ project_code: formData.get('lm_project_code') || 'openai',
+ email_type: formData.get('lm_email_type') || 'ms_graph',
+ preferred_domain: formData.get('lm_preferred_domain')
+ };
} else if (subType === 'freemail') {
serviceType = 'freemail';
config = {
@@ -585,10 +675,18 @@ async function handleBatchDeleteOutlook() {
async function handleSaveTempmail(e) {
e.preventDefault();
try {
- await api.post('/settings/tempmail', {
+ const payload = {
api_url: elements.tempmailApi.value,
- enabled: elements.tempmailEnabled.checked
- });
+ enabled: elements.tempmailEnabled.checked,
+ yyds_api_url: elements.yydsApi.value,
+ yyds_default_domain: elements.yydsDefaultDomain.value,
+ yyds_enabled: elements.yydsEnabled.checked
+ };
+ const yydsApiKey = elements.yydsApiKey.value.trim();
+ if (yydsApiKey) {
+ payload.yyds_api_key = yydsApiKey;
+ }
+ await api.post('/settings/tempmail', payload);
toast.success('配置已保存');
} catch (error) {
toast.error('保存失败: ' + error.message);
@@ -598,7 +696,7 @@ async function handleSaveTempmail(e) {
// 测试临时邮箱
async function handleTestTempmail() {
elements.testTempmailBtn.disabled = true;
- elements.testTempmailBtn.textContent = '测试中...';
+ elements.testTempmailBtn.textContent = '测试 Tempmail.lol 中...';
try {
const result = await api.post('/email-services/test-tempmail', {
api_url: elements.tempmailApi.value
@@ -609,7 +707,30 @@ async function handleTestTempmail() {
toast.error('测试失败: ' + error.message);
} finally {
elements.testTempmailBtn.disabled = false;
- elements.testTempmailBtn.textContent = '🔌 测试连接';
+ elements.testTempmailBtn.textContent = '🔌 测试 Tempmail.lol';
+ }
+}
+
+async function handleTestYyds() {
+ elements.testYydsBtn.disabled = true;
+ elements.testYydsBtn.textContent = '测试 YYDS Mail 中...';
+ try {
+ const payload = {
+ provider: 'yyds_mail',
+ api_url: elements.yydsApi.value
+ };
+ const apiKey = elements.yydsApiKey.value.trim();
+ if (apiKey) {
+ payload.api_key = apiKey;
+ }
+ const result = await api.post('/email-services/test-tempmail', payload);
+ if (result.success) toast.success('YYDS Mail 连接正常');
+ else toast.error('连接失败: ' + (result.error || result.message || '未知错误'));
+ } catch (error) {
+ toast.error('测试失败: ' + error.message);
+ } finally {
+ elements.testYydsBtn.disabled = false;
+ elements.testYydsBtn.textContent = '🔌 测试 YYDS Mail';
}
}
@@ -635,12 +756,18 @@ async function editCustomService(id, subType) {
try {
const service = await api.get(`/email-services/${id}/full`);
const resolvedSubType = subType || (
- service.service_type === 'temp_mail'
+ service.service_type === 'tempmail'
+ ? 'tempmail_builtin'
+ : service.service_type === 'yyds_mail'
+ ? 'yyds_mail'
+ : service.service_type === 'temp_mail'
? 'tempmail'
: service.service_type === 'cloudmail'
? 'cloudmail'
: service.service_type === 'duck_mail'
? 'duckmail'
+ : service.service_type === 'luckmail'
+ ? 'luckmail'
: service.service_type === 'freemail'
? 'freemail'
: service.service_type === 'imap_mail'
@@ -660,6 +787,17 @@ async function editCustomService(id, subType) {
document.getElementById('edit-custom-api-key').value = '';
document.getElementById('edit-custom-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : 'API Key';
document.getElementById('edit-custom-domain').value = service.config?.default_domain || service.config?.domain || '';
+ } else if (resolvedSubType === 'tempmail_builtin') {
+ document.getElementById('edit-tpo-base-url').value = service.config?.base_url || '';
+ document.getElementById('edit-tpo-timeout').value = service.config?.timeout || 30;
+ document.getElementById('edit-tpo-max-retries').value = service.config?.max_retries || 3;
+ } else if (resolvedSubType === 'yyds_mail') {
+ document.getElementById('edit-yyds-base-url').value = service.config?.base_url || '';
+ document.getElementById('edit-yyds-api-key').value = '';
+ document.getElementById('edit-yyds-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : '请输入 API Key';
+ document.getElementById('edit-yyds-default-domain').value = service.config?.default_domain || '';
+ document.getElementById('edit-yyds-timeout').value = service.config?.timeout || 30;
+ document.getElementById('edit-yyds-max-retries').value = service.config?.max_retries || 3;
} else if (resolvedSubType === 'tempmail' || resolvedSubType === 'cloudmail') {
document.getElementById('edit-tm-base-url').value = service.config?.base_url || '';
document.getElementById('edit-tm-admin-password').value = '';
@@ -671,6 +809,13 @@ async function editCustomService(id, subType) {
document.getElementById('edit-dm-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : '请输入 API Key(可选)';
document.getElementById('edit-dm-domain').value = service.config?.default_domain || '';
document.getElementById('edit-dm-password-length').value = service.config?.password_length || 12;
+ } else if (resolvedSubType === 'luckmail') {
+ document.getElementById('edit-lm-base-url').value = service.config?.base_url || '';
+ document.getElementById('edit-lm-api-key').value = '';
+ document.getElementById('edit-lm-api-key').placeholder = service.config?.api_key ? '已设置,留空保持不变' : '请输入 API Key';
+ document.getElementById('edit-lm-project-code').value = service.config?.project_code || 'openai';
+ document.getElementById('edit-lm-email-type').value = service.config?.email_type || 'ms_graph';
+ document.getElementById('edit-lm-preferred-domain').value = service.config?.preferred_domain || '';
} else if (resolvedSubType === 'freemail') {
document.getElementById('edit-fm-base-url').value = service.config?.base_url || '';
document.getElementById('edit-fm-admin-token').value = '';
@@ -706,6 +851,21 @@ async function handleEditCustom(e) {
};
const apiKey = formData.get('api_key');
if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
+ } else if (subType === 'tempmail_builtin') {
+ config = {
+ base_url: formData.get('tpo_base_url'),
+ timeout: parseInt(formData.get('tpo_timeout'), 10) || 30,
+ max_retries: parseInt(formData.get('tpo_max_retries'), 10) || 3
+ };
+ } else if (subType === 'yyds_mail') {
+ config = {
+ base_url: formData.get('yyds_base_url'),
+ default_domain: formData.get('yyds_default_domain'),
+ timeout: parseInt(formData.get('yyds_timeout'), 10) || 30,
+ max_retries: parseInt(formData.get('yyds_max_retries'), 10) || 3
+ };
+ const apiKey = formData.get('yyds_api_key');
+ if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
} else if (subType === 'tempmail' || subType === 'cloudmail') {
config = {
base_url: formData.get('tm_base_url'),
@@ -722,6 +882,15 @@ async function handleEditCustom(e) {
};
const apiKey = formData.get('dm_api_key');
if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
+ } else if (subType === 'luckmail') {
+ config = {
+ base_url: formData.get('lm_base_url'),
+ project_code: formData.get('lm_project_code') || 'openai',
+ email_type: formData.get('lm_email_type') || 'ms_graph',
+ preferred_domain: formData.get('lm_preferred_domain')
+ };
+ const apiKey = formData.get('lm_api_key');
+ if (apiKey && apiKey.trim()) config.api_key = apiKey.trim();
} else if (subType === 'freemail') {
config = {
base_url: formData.get('fm_base_url'),
diff --git a/static/js/settings.js b/static/js/settings.js
index de673a43..95a0e508 100644
--- a/static/js/settings.js
+++ b/static/js/settings.js
@@ -75,6 +75,14 @@ const elements = {
tmServiceForm: document.getElementById('tm-service-form'),
tmServiceModalTitle: document.getElementById('tm-service-modal-title'),
testTmServiceBtn: document.getElementById('test-tm-service-btn'),
+ addNewApiServiceBtn: document.getElementById('add-new-api-service-btn'),
+ newApiServicesTable: document.getElementById('new-api-services-table'),
+ newApiServiceEditModal: document.getElementById('new-api-service-edit-modal'),
+ closeNewApiServiceModal: document.getElementById('close-new-api-service-modal'),
+ cancelNewApiServiceBtn: document.getElementById('cancel-new-api-service-btn'),
+ newApiServiceForm: document.getElementById('new-api-service-form'),
+ newApiServiceModalTitle: document.getElementById('new-api-service-modal-title'),
+ testNewApiServiceBtn: document.getElementById('test-new-api-service-btn'),
// 验证码设置
emailCodeForm: document.getElementById('email-code-form'),
// Outlook 设置
@@ -96,6 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
loadCpaServices();
loadSub2ApiServices();
loadTmServices();
+ loadNewApiServices();
initEventListeners();
});
@@ -304,6 +313,26 @@ function initEventListeners() {
if (elements.testTmServiceBtn) {
elements.testTmServiceBtn.addEventListener('click', handleTestTmService);
}
+ if (elements.addNewApiServiceBtn) {
+ elements.addNewApiServiceBtn.addEventListener('click', () => openNewApiServiceModal());
+ }
+ if (elements.closeNewApiServiceModal) {
+ elements.closeNewApiServiceModal.addEventListener('click', closeNewApiServiceModal);
+ }
+ if (elements.cancelNewApiServiceBtn) {
+ elements.cancelNewApiServiceBtn.addEventListener('click', closeNewApiServiceModal);
+ }
+ if (elements.newApiServiceEditModal) {
+ elements.newApiServiceEditModal.addEventListener('click', (e) => {
+ if (e.target === elements.newApiServiceEditModal) closeNewApiServiceModal();
+ });
+ }
+ if (elements.newApiServiceForm) {
+ elements.newApiServiceForm.addEventListener('submit', handleSaveNewApiService);
+ }
+ if (elements.testNewApiServiceBtn) {
+ elements.testNewApiServiceBtn.addEventListener('click', handleTestNewApiService);
+ }
// CPA 服务管理
if (elements.addCpaServiceBtn) {
@@ -1715,6 +1744,162 @@ async function handleTestSub2ApiService() {
}
}
+async function loadNewApiServices() {
+ if (!elements.newApiServicesTable) return;
+ try {
+ const services = await api.get('/new-api-services');
+ renderNewApiServicesTable(services);
+ } catch (e) {
+ elements.newApiServicesTable.innerHTML = `
| ${e.message} |
`;
+ }
+}
+
+function renderNewApiServicesTable(services) {
+ if (!services || services.length === 0) {
+ elements.newApiServicesTable.innerHTML = '
| 暂无 new-api 服务,点击「添加服务」新增 |
';
+ return;
+ }
+ elements.newApiServicesTable.innerHTML = services.map(s => `
+
+ | ${escapeHtml(s.name)} |
+ ${escapeHtml(s.api_url)} ${escapeHtml(s.username || '')} |
+ ${s.enabled ? '✅' : '⭕'} |
+ ${s.priority} |
+
+
+
+
+ |
+
+ `).join('');
+}
+
+function openNewApiServiceModal(service = null) {
+ document.getElementById('new-api-service-id').value = service ? service.id : '';
+ document.getElementById('new-api-service-name').value = service ? service.name : '';
+ document.getElementById('new-api-service-url').value = service ? service.api_url : '';
+ document.getElementById('new-api-service-username').value = service ? (service.username || '') : '';
+ document.getElementById('new-api-service-password').value = '';
+ document.getElementById('new-api-service-priority').value = service ? service.priority : 0;
+ document.getElementById('new-api-service-enabled').checked = service ? service.enabled : true;
+ document.getElementById('new-api-service-password').placeholder = service && service.has_password ? '已配置,留空保持不变' : '请输入管理员密码';
+ elements.newApiServiceModalTitle.textContent = service ? '编辑 new-api 服务' : '添加 new-api 服务';
+ elements.newApiServiceEditModal.classList.add('active');
+}
+
+function closeNewApiServiceModal() {
+ elements.newApiServiceEditModal.classList.remove('active');
+}
+
+async function editNewApiService(id) {
+ try {
+ const service = await api.get(`/new-api-services/${id}`);
+ openNewApiServiceModal(service);
+ } catch (e) {
+ toast.error('获取服务信息失败: ' + e.message);
+ }
+}
+
+async function handleSaveNewApiService(e) {
+ e.preventDefault();
+ const id = document.getElementById('new-api-service-id').value;
+ const name = document.getElementById('new-api-service-name').value.trim();
+ const apiUrl = document.getElementById('new-api-service-url').value.trim();
+ const username = document.getElementById('new-api-service-username').value.trim();
+ const password = document.getElementById('new-api-service-password').value.trim();
+ const priority = parseInt(document.getElementById('new-api-service-priority').value) || 0;
+ const enabled = document.getElementById('new-api-service-enabled').checked;
+
+ if (!name || !apiUrl || !username) {
+ toast.error('名称、API URL 和管理员用户名不能为空');
+ return;
+ }
+ if (!id && !password) {
+ toast.error('新增服务时管理员密码不能为空');
+ return;
+ }
+
+ try {
+ const payload = { name, api_url: apiUrl, username, priority, enabled };
+ if (password) payload.password = password;
+
+ if (id) {
+ await api.patch(`/new-api-services/${id}`, payload);
+ toast.success('服务已更新');
+ } else {
+ await api.post('/new-api-services', payload);
+ toast.success('服务已添加');
+ }
+ closeNewApiServiceModal();
+ loadNewApiServices();
+ } catch (e) {
+ toast.error('保存失败: ' + e.message);
+ }
+}
+
+async function deleteNewApiService(id, name) {
+ const confirmed = await confirm(`确定要删除 new-api 服务「${name}」吗?`);
+ if (!confirmed) return;
+ try {
+ await api.delete(`/new-api-services/${id}`);
+ toast.success('已删除');
+ loadNewApiServices();
+ } catch (e) {
+ toast.error('删除失败: ' + e.message);
+ }
+}
+
+async function testNewApiServiceById(id) {
+ try {
+ const result = await api.post(`/new-api-services/${id}/test`);
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message);
+ }
+ } catch (e) {
+ toast.error('测试失败: ' + e.message);
+ }
+}
+
+async function handleTestNewApiService() {
+ const apiUrl = document.getElementById('new-api-service-url').value.trim();
+ const username = document.getElementById('new-api-service-username').value.trim();
+ const password = document.getElementById('new-api-service-password').value.trim();
+ const id = document.getElementById('new-api-service-id').value;
+
+ if (!apiUrl || !username) {
+ toast.error('请先填写 API URL 和管理员用户名');
+ return;
+ }
+ if (!id && !password) {
+ toast.error('请先填写管理员密码');
+ return;
+ }
+
+ elements.testNewApiServiceBtn.disabled = true;
+ elements.testNewApiServiceBtn.textContent = '测试中...';
+
+ try {
+ let result;
+ if (id && !password) {
+ result = await api.post(`/new-api-services/${id}/test`);
+ } else {
+ result = await api.post('/new-api-services/test-connection', { api_url: apiUrl, username, password });
+ }
+ if (result.success) {
+ toast.success(result.message);
+ } else {
+ toast.error(result.message);
+ }
+ } catch (e) {
+ toast.error('测试失败: ' + e.message);
+ } finally {
+ elements.testNewApiServiceBtn.disabled = false;
+ elements.testNewApiServiceBtn.textContent = '🔌 测试连接';
+ }
+}
+
function escapeHtml(text) {
if (!text) return '';
const d = document.createElement('div');
diff --git a/templates/accounts.html b/templates/accounts.html
index 803154bd..0ea5dff5 100644
--- a/templates/accounts.html
+++ b/templates/accounts.html
@@ -246,6 +246,7 @@
+ {% include "partials/site_notice.html" %}