mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-06-26 01:31:47 +08:00
feat(cpa): 支持多cpa服务
This commit is contained in:
@@ -740,11 +740,86 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ============== CPA 服务选择 ==============
|
||||
|
||||
// 弹出 CPA 服务选择框,返回 Promise<{cpa_service_id: number|null}|null>
|
||||
// null 表示用户取消,{cpa_service_id: null} 表示使用全局配置
|
||||
function selectCpaService() {
|
||||
return new Promise(async (resolve) => {
|
||||
const modal = document.getElementById('cpa-service-modal');
|
||||
const listEl = document.getElementById('cpa-service-list');
|
||||
const closeBtn = document.getElementById('close-cpa-modal');
|
||||
const cancelBtn = document.getElementById('cancel-cpa-modal-btn');
|
||||
const globalBtn = document.getElementById('cpa-use-global-btn');
|
||||
|
||||
// 加载服务列表
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted)">加载中...</div>';
|
||||
modal.classList.add('active');
|
||||
|
||||
let services = [];
|
||||
try {
|
||||
services = await api.get('/cpa-services?enabled=true');
|
||||
} catch (e) {
|
||||
services = [];
|
||||
}
|
||||
|
||||
if (services.length === 0) {
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:12px;">暂无已启用的 CPA 服务,将使用全局配置</div>';
|
||||
} else {
|
||||
listEl.innerHTML = services.map(s => `
|
||||
<div class="cpa-service-item" data-id="${s.id}" style="
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
">
|
||||
<div>
|
||||
<div style="font-weight:500;">${escapeHtml(s.name)}</div>
|
||||
<div style="font-size:0.8rem;color:var(--text-muted);">${escapeHtml(s.api_url)}</div>
|
||||
</div>
|
||||
<span class="badge" style="background:var(--success-color);color:#fff;font-size:0.7rem;padding:2px 8px;border-radius:10px;">选择</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.cpa-service-item').forEach(item => {
|
||||
item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)');
|
||||
item.addEventListener('mouseleave', () => item.style.background = '');
|
||||
item.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve({ cpa_service_id: parseInt(item.dataset.id) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
modal.classList.remove('active');
|
||||
closeBtn.removeEventListener('click', onCancel);
|
||||
cancelBtn.removeEventListener('click', onCancel);
|
||||
globalBtn.removeEventListener('click', onGlobal);
|
||||
}
|
||||
function onCancel() { cleanup(); resolve(null); }
|
||||
function onGlobal() { cleanup(); resolve({ cpa_service_id: null }); }
|
||||
|
||||
closeBtn.addEventListener('click', onCancel);
|
||||
cancelBtn.addEventListener('click', onCancel);
|
||||
globalBtn.addEventListener('click', onGlobal);
|
||||
});
|
||||
}
|
||||
|
||||
// 上传单个账号到CPA
|
||||
async function uploadToCpa(id) {
|
||||
const choice = await selectCpaService();
|
||||
if (choice === null) return; // 用户取消
|
||||
|
||||
try {
|
||||
toast.info('正在上传到CPA...');
|
||||
const result = await api.post(`/accounts/${id}/upload-cpa`);
|
||||
const payload = {};
|
||||
if (choice.cpa_service_id != null) payload.cpa_service_id = choice.cpa_service_id;
|
||||
const result = await api.post(`/accounts/${id}/upload-cpa`, payload);
|
||||
|
||||
if (result.success) {
|
||||
toast.success('上传成功');
|
||||
@@ -762,6 +837,9 @@ async function handleBatchUploadCpa() {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const choice = await selectCpaService();
|
||||
if (choice === null) return; // 用户取消
|
||||
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到CPA吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -769,15 +847,13 @@ async function handleBatchUploadCpa() {
|
||||
elements.batchUploadCpaBtn.textContent = '上传中...';
|
||||
|
||||
try {
|
||||
const result = await api.post('/accounts/batch-upload-cpa', buildBatchPayload());
|
||||
const payload = buildBatchPayload();
|
||||
if (choice.cpa_service_id != null) payload.cpa_service_id = choice.cpa_service_id;
|
||||
const result = await api.post('/accounts/batch-upload-cpa', payload);
|
||||
|
||||
let message = `成功: ${result.success_count}`;
|
||||
if (result.failed_count > 0) {
|
||||
message += `, 失败: ${result.failed_count}`;
|
||||
}
|
||||
if (result.skipped_count > 0) {
|
||||
message += `, 跳过: ${result.skipped_count}`;
|
||||
}
|
||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;
|
||||
|
||||
toast.success(message);
|
||||
loadAccounts();
|
||||
|
||||
@@ -83,7 +83,9 @@ const elements = {
|
||||
concurrencyHint: document.getElementById('concurrency-hint'),
|
||||
intervalGroup: document.getElementById('interval-group'),
|
||||
// 注册后自动操作
|
||||
autoUploadCpa: document.getElementById('auto-upload-cpa')
|
||||
autoUploadCpa: document.getElementById('auto-upload-cpa'),
|
||||
cpaServiceSelectGroup: document.getElementById('cpa-service-select-group'),
|
||||
cpaServiceSelect: document.getElementById('cpa-service-select')
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -97,7 +99,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
checkCpaEnabled();
|
||||
});
|
||||
|
||||
// 检查 CPA 是否启用,未启用则禁用复选框
|
||||
// 检查 CPA 是否启用,未启用则禁用复选框;同时加载 CPA 服务列表
|
||||
async function checkCpaEnabled() {
|
||||
if (!elements.autoUploadCpa) return;
|
||||
try {
|
||||
@@ -111,6 +113,32 @@ async function checkCpaEnabled() {
|
||||
} catch (e) {
|
||||
elements.autoUploadCpa.disabled = true;
|
||||
}
|
||||
// 加载 CPA 服务列表
|
||||
await loadCpaServiceOptions();
|
||||
// 复选框联动显示/隐藏服务选择器
|
||||
if (elements.autoUploadCpa) {
|
||||
elements.autoUploadCpa.addEventListener('change', () => {
|
||||
if (elements.cpaServiceSelectGroup) {
|
||||
elements.cpaServiceSelectGroup.style.display =
|
||||
elements.autoUploadCpa.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCpaServiceOptions() {
|
||||
if (!elements.cpaServiceSelect) return;
|
||||
try {
|
||||
const services = await api.get('/cpa-services?enabled=true');
|
||||
// 保留「使用全局配置」选项
|
||||
const defaultOpt = '<option value="">使用全局配置</option>';
|
||||
const opts = services.map(s =>
|
||||
`<option value="${s.id}">${s.name.replace(/</g,'<')}</option>`
|
||||
).join('');
|
||||
elements.cpaServiceSelect.innerHTML = defaultOpt + opts;
|
||||
} catch (e) {
|
||||
// 加载失败静默处理,保持默认选项
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听
|
||||
@@ -357,6 +385,10 @@ async function handleStartRegistration(e) {
|
||||
email_service_type: emailServiceType,
|
||||
auto_upload_cpa: elements.autoUploadCpa ? elements.autoUploadCpa.checked : false
|
||||
};
|
||||
// 带上指定 CPA 服务
|
||||
if (requestData.auto_upload_cpa && elements.cpaServiceSelect && elements.cpaServiceSelect.value) {
|
||||
requestData.cpa_service_id = parseInt(elements.cpaServiceSelect.value);
|
||||
}
|
||||
|
||||
// 如果选择了数据库中的服务,传递 service_id
|
||||
if (serviceId && serviceId !== 'default') {
|
||||
|
||||
@@ -44,6 +44,15 @@ const elements = {
|
||||
// CPA 设置
|
||||
cpaForm: document.getElementById('cpa-form'),
|
||||
testCpaBtn: document.getElementById('test-cpa-btn'),
|
||||
// CPA 服务管理
|
||||
addCpaServiceBtn: document.getElementById('add-cpa-service-btn'),
|
||||
cpaServicesTable: document.getElementById('cpa-services-table'),
|
||||
cpaServiceEditModal: document.getElementById('cpa-service-edit-modal'),
|
||||
closeCpaServiceModal: document.getElementById('close-cpa-service-modal'),
|
||||
cancelCpaServiceBtn: document.getElementById('cancel-cpa-service-btn'),
|
||||
cpaServiceForm: document.getElementById('cpa-service-form'),
|
||||
cpaServiceModalTitle: document.getElementById('cpa-service-modal-title'),
|
||||
testCpaServiceBtn: document.getElementById('test-cpa-service-btn'),
|
||||
// Team Manager 设置
|
||||
tmForm: document.getElementById('tm-form'),
|
||||
testTmBtn: document.getElementById('test-tm-btn'),
|
||||
@@ -65,6 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadEmailServices();
|
||||
loadDatabaseInfo();
|
||||
loadProxies();
|
||||
loadCpaServices();
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
@@ -247,6 +257,28 @@ function initEventListeners() {
|
||||
if (elements.testTmBtn) {
|
||||
elements.testTmBtn.addEventListener('click', handleTestTm);
|
||||
}
|
||||
|
||||
// CPA 服务管理
|
||||
if (elements.addCpaServiceBtn) {
|
||||
elements.addCpaServiceBtn.addEventListener('click', () => openCpaServiceModal());
|
||||
}
|
||||
if (elements.closeCpaServiceModal) {
|
||||
elements.closeCpaServiceModal.addEventListener('click', closeCpaServiceModal);
|
||||
}
|
||||
if (elements.cancelCpaServiceBtn) {
|
||||
elements.cancelCpaServiceBtn.addEventListener('click', closeCpaServiceModal);
|
||||
}
|
||||
if (elements.cpaServiceEditModal) {
|
||||
elements.cpaServiceEditModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.cpaServiceEditModal) closeCpaServiceModal();
|
||||
});
|
||||
}
|
||||
if (elements.cpaServiceForm) {
|
||||
elements.cpaServiceForm.addEventListener('submit', handleSaveCpaService);
|
||||
}
|
||||
if (elements.testCpaServiceBtn) {
|
||||
elements.testCpaServiceBtn.addEventListener('click', handleTestCpaService);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置
|
||||
@@ -1182,3 +1214,172 @@ async function handleTestTm() {
|
||||
elements.testTmBtn.textContent = '🔌 测试连接';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============== CPA 服务管理 ==============
|
||||
|
||||
async function loadCpaServices() {
|
||||
if (!elements.cpaServicesTable) return;
|
||||
try {
|
||||
const services = await api.get('/cpa-services');
|
||||
renderCpaServicesTable(services);
|
||||
} catch (e) {
|
||||
elements.cpaServicesTable.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--danger-color);">${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCpaServicesTable(services) {
|
||||
if (!services || services.length === 0) {
|
||||
elements.cpaServicesTable.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:20px;">暂无 CPA 服务,点击「添加服务」新增</td></tr>';
|
||||
return;
|
||||
}
|
||||
elements.cpaServicesTable.innerHTML = services.map(s => `
|
||||
<tr>
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td style="font-size:0.85rem;color:var(--text-muted);">${escapeHtml(s.api_url)}</td>
|
||||
<td>
|
||||
<span class="badge" style="background:${s.enabled ? 'var(--success-color)' : 'var(--border)'};color:${s.enabled ? '#fff' : 'var(--text-muted)'};font-size:0.75rem;padding:2px 8px;border-radius:10px;">
|
||||
${s.enabled ? '启用' : '禁用'}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align:center;">${s.priority}</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-sm" onclick="editCpaService(${s.id})">编辑</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="testCpaServiceById(${s.id})">测试</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteCpaService(${s.id}, '${escapeHtml(s.name)}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openCpaServiceModal(service = null) {
|
||||
document.getElementById('cpa-service-id').value = service ? service.id : '';
|
||||
document.getElementById('cpa-service-name').value = service ? service.name : '';
|
||||
document.getElementById('cpa-service-url').value = service ? service.api_url : '';
|
||||
document.getElementById('cpa-service-token').value = '';
|
||||
document.getElementById('cpa-service-priority').value = service ? service.priority : 0;
|
||||
document.getElementById('cpa-service-enabled').checked = service ? service.enabled : true;
|
||||
elements.cpaServiceModalTitle.textContent = service ? '编辑 CPA 服务' : '添加 CPA 服务';
|
||||
elements.cpaServiceEditModal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeCpaServiceModal() {
|
||||
elements.cpaServiceEditModal.classList.remove('active');
|
||||
}
|
||||
|
||||
async function editCpaService(id) {
|
||||
try {
|
||||
const service = await api.get(`/cpa-services/${id}`);
|
||||
openCpaServiceModal(service);
|
||||
} catch (e) {
|
||||
toast.error('获取服务信息失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveCpaService(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('cpa-service-id').value;
|
||||
const name = document.getElementById('cpa-service-name').value.trim();
|
||||
const apiUrl = document.getElementById('cpa-service-url').value.trim();
|
||||
const apiToken = document.getElementById('cpa-service-token').value.trim();
|
||||
const priority = parseInt(document.getElementById('cpa-service-priority').value) || 0;
|
||||
const enabled = document.getElementById('cpa-service-enabled').checked;
|
||||
|
||||
if (!name || !apiUrl) {
|
||||
toast.error('名称和 API URL 不能为空');
|
||||
return;
|
||||
}
|
||||
if (!id && !apiToken) {
|
||||
toast.error('新增服务时 API Token 不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = { name, api_url: apiUrl, priority, enabled };
|
||||
if (apiToken) payload.api_token = apiToken;
|
||||
|
||||
if (id) {
|
||||
await api.patch(`/cpa-services/${id}`, payload);
|
||||
toast.success('服务已更新');
|
||||
} else {
|
||||
payload.api_token = apiToken;
|
||||
await api.post('/cpa-services', payload);
|
||||
toast.success('服务已添加');
|
||||
}
|
||||
closeCpaServiceModal();
|
||||
loadCpaServices();
|
||||
} catch (e) {
|
||||
toast.error('保存失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCpaService(id, name) {
|
||||
const confirmed = await confirm(`确定要删除 CPA 服务「${name}」吗?`);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await api.delete(`/cpa-services/${id}`);
|
||||
toast.success('已删除');
|
||||
loadCpaServices();
|
||||
} catch (e) {
|
||||
toast.error('删除失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testCpaServiceById(id) {
|
||||
try {
|
||||
const result = await api.post(`/cpa-services/${id}/test`);
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('测试失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestCpaService() {
|
||||
const apiUrl = document.getElementById('cpa-service-url').value.trim();
|
||||
const apiToken = document.getElementById('cpa-service-token').value.trim();
|
||||
const id = document.getElementById('cpa-service-id').value;
|
||||
|
||||
if (!apiUrl) {
|
||||
toast.error('请先填写 API URL');
|
||||
return;
|
||||
}
|
||||
// 新增时必须有 token,编辑时 token 可为空(用已保存的)
|
||||
if (!id && !apiToken) {
|
||||
toast.error('请先填写 API Token');
|
||||
return;
|
||||
}
|
||||
|
||||
elements.testCpaServiceBtn.disabled = true;
|
||||
elements.testCpaServiceBtn.textContent = '测试中...';
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (id && !apiToken) {
|
||||
// 编辑时未填 token,直接测试已保存的服务
|
||||
result = await api.post(`/cpa-services/${id}/test`);
|
||||
} else {
|
||||
result = await api.post('/cpa-services/test-connection', { api_url: apiUrl, api_token: apiToken });
|
||||
}
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('测试失败: ' + e.message);
|
||||
} finally {
|
||||
elements.testCpaServiceBtn.disabled = false;
|
||||
elements.testCpaServiceBtn.textContent = '🔌 测试连接';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = text;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user