mirror of
https://github.com/cnlimiter/codex-register.git
synced 2026-05-11 10:00:11 +08:00
feat(newapi): 添加 NEWAPI 上传功能及服务管理接口
- 新增 `newapi_upload.py` 文件,包含上传到 NEWAPI 的功能。 - 在数据库模型中添加 `NewapiService` 表及相关字段。 - 更新 CRUD 操作以支持 NEWAPI 服务的创建、更新、查询和删除。 - 添加新的 API 路由以管理 NEWAPI 服务。 - 前端实现批量上传和单个账号上传到 NEWAPI 的功能。 - 更新相关页面以支持 NEWAPI 服务的选择和管理。
This commit is contained in:
@@ -107,6 +107,7 @@ function initEventListeners() {
|
||||
document.getElementById('batch-upload-cpa-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadCpa(); });
|
||||
document.getElementById('batch-upload-sub2api-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadSub2Api(); });
|
||||
document.getElementById('batch-upload-tm-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadTm(); });
|
||||
document.getElementById('batch-upload-newapi-item').addEventListener('click', (e) => { e.preventDefault(); uploadMenu.classList.remove('active'); handleBatchUploadNewapi(); });
|
||||
|
||||
// 批量删除
|
||||
elements.batchDeleteBtn.addEventListener('click', handleBatchDelete);
|
||||
@@ -314,6 +315,13 @@ function renderAccounts(accounts) {
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cpa-status">
|
||||
${account.newapi_uploaded
|
||||
? `<span class="badge uploaded" title="已上传于 ${format.date(account.newapi_uploaded_at)}">✓</span>`
|
||||
: `<span class="badge pending">-</span>`}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cpa-status">
|
||||
${account.subscription_type
|
||||
@@ -841,6 +849,7 @@ async function uploadAccount(id) {
|
||||
{ label: '☁️ 上传到 CPA', value: 'cpa' },
|
||||
{ label: '🔗 上传到 Sub2API', value: 'sub2api' },
|
||||
{ label: '🚀 上传到 Team Manager', value: 'tm' },
|
||||
{ label: '🧩 上传到 NEWAPI', value: 'newapi' },
|
||||
];
|
||||
|
||||
const choice = await new Promise((resolve) => {
|
||||
@@ -870,6 +879,7 @@ async function uploadAccount(id) {
|
||||
if (choice === 'cpa') return uploadToCpa(id);
|
||||
if (choice === 'sub2api') return uploadToSub2Api(id);
|
||||
if (choice === 'tm') return uploadToTm(id);
|
||||
if (choice === 'newapi') return uploadToNewapi(id);
|
||||
}
|
||||
|
||||
// 上传单个账号到CPA
|
||||
@@ -1209,6 +1219,120 @@ async function handleBatchUploadTm() {
|
||||
}
|
||||
}
|
||||
|
||||
// ============== NEWAPI 上传 ==============
|
||||
|
||||
function selectNewapiService() {
|
||||
return new Promise(async (resolve) => {
|
||||
const modal = document.getElementById('newapi-service-modal');
|
||||
const listEl = document.getElementById('newapi-service-list');
|
||||
const closeBtn = document.getElementById('close-newapi-modal');
|
||||
const cancelBtn = document.getElementById('cancel-newapi-modal-btn');
|
||||
const autoBtn = document.getElementById('newapi-use-auto-btn');
|
||||
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted)">加载中...</div>';
|
||||
modal.classList.add('active');
|
||||
|
||||
let services = [];
|
||||
try {
|
||||
services = await api.get('/newapi-services?enabled=true');
|
||||
} catch (e) {
|
||||
services = [];
|
||||
}
|
||||
|
||||
if (services.length === 0) {
|
||||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);padding:12px;">暂无已启用的 NEWAPI 服务,将自动选择第一个</div>';
|
||||
} else {
|
||||
listEl.innerHTML = services.map(s => `
|
||||
<div class="newapi-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(--primary);color:#fff;font-size:0.7rem;padding:2px 8px;border-radius:10px;">选择</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.newapi-service-item').forEach(item => {
|
||||
item.addEventListener('mouseenter', () => item.style.background = 'var(--surface-hover)');
|
||||
item.addEventListener('mouseleave', () => item.style.background = '');
|
||||
item.addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve({ service_id: parseInt(item.dataset.id) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
modal.classList.remove('active');
|
||||
closeBtn.removeEventListener('click', onCancel);
|
||||
cancelBtn.removeEventListener('click', onCancel);
|
||||
autoBtn.removeEventListener('click', onAuto);
|
||||
}
|
||||
function onCancel() { cleanup(); resolve(null); }
|
||||
function onAuto() { cleanup(); resolve({ service_id: null }); }
|
||||
|
||||
closeBtn.addEventListener('click', onCancel);
|
||||
cancelBtn.addEventListener('click', onCancel);
|
||||
autoBtn.addEventListener('click', onAuto);
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadToNewapi(id) {
|
||||
const choice = await selectNewapiService();
|
||||
if (choice === null) return;
|
||||
try {
|
||||
toast.info('正在上传到 NEWAPI...');
|
||||
const payload = {};
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
const result = await api.post(`/accounts/${id}/upload-newapi`, payload);
|
||||
if (result.success) {
|
||||
toast.success('上传成功');
|
||||
} else {
|
||||
toast.error('上传失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('上传失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchUploadNewapi() {
|
||||
const count = getEffectiveCount();
|
||||
if (count === 0) return;
|
||||
|
||||
const choice = await selectNewapiService();
|
||||
if (choice === null) return;
|
||||
|
||||
const confirmed = await confirm(`确定要将选中的 ${count} 个账号上传到 NEWAPI 吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
elements.batchUploadBtn.disabled = true;
|
||||
elements.batchUploadBtn.textContent = '上传中...';
|
||||
|
||||
try {
|
||||
const payload = buildBatchPayload();
|
||||
if (choice.service_id != null) payload.service_id = choice.service_id;
|
||||
const result = await api.post('/accounts/batch-upload-newapi', payload);
|
||||
let message = `成功: ${result.success_count}`;
|
||||
if (result.failed_count > 0) message += `, 失败: ${result.failed_count}`;
|
||||
if (result.skipped_count > 0) message += `, 跳过: ${result.skipped_count}`;
|
||||
toast.success(message);
|
||||
loadAccounts();
|
||||
} catch (e) {
|
||||
toast.error('批量上传失败: ' + e.message);
|
||||
} finally {
|
||||
updateBatchButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// 更多菜单切换
|
||||
function toggleMoreMenu(btn) {
|
||||
const menu = btn.nextElementSibling;
|
||||
|
||||
@@ -94,6 +94,9 @@ const elements = {
|
||||
autoUploadTm: document.getElementById('auto-upload-tm'),
|
||||
tmServiceSelectGroup: document.getElementById('tm-service-select-group'),
|
||||
tmServiceSelect: document.getElementById('tm-service-select'),
|
||||
autoUploadNewapi: document.getElementById('auto-upload-newapi'),
|
||||
newapiServiceSelectGroup: document.getElementById('newapi-service-select-group'),
|
||||
newapiServiceSelect: document.getElementById('newapi-service-select'),
|
||||
};
|
||||
|
||||
// 初始化
|
||||
@@ -113,6 +116,7 @@ async function initAutoUploadOptions() {
|
||||
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),
|
||||
loadServiceSelect('/newapi-services?enabled=true', elements.newapiServiceSelect, elements.autoUploadNewapi, elements.newapiServiceSelectGroup),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -480,6 +484,8 @@ async function handleStartRegistration(e) {
|
||||
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_newapi: elements.autoUploadNewapi ? elements.autoUploadNewapi.checked : false,
|
||||
newapi_service_ids: elements.autoUploadNewapi && elements.autoUploadNewapi.checked ? getSelectedServiceIds(elements.newapiServiceSelect) : [],
|
||||
};
|
||||
|
||||
// 如果选择了数据库中的服务,传递 service_id
|
||||
@@ -1196,6 +1202,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_newapi: elements.autoUploadNewapi ? elements.autoUploadNewapi.checked : false,
|
||||
newapi_service_ids: elements.autoUploadNewapi && elements.autoUploadNewapi.checked ? getSelectedServiceIds(elements.newapiServiceSelect) : [],
|
||||
};
|
||||
|
||||
addLog('info', `[系统] 正在启动 Outlook 批量注册 (${selectedIds.length} 个账户)...`);
|
||||
|
||||
@@ -66,6 +66,13 @@ 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-newapi-service-btn'),
|
||||
newapiServicesTable: document.getElementById('newapi-services-table'),
|
||||
newapiServiceEditModal: document.getElementById('newapi-service-edit-modal'),
|
||||
closeNewapiServiceModal: document.getElementById('close-newapi-service-modal'),
|
||||
cancelNewapiServiceBtn: document.getElementById('cancel-newapi-service-btn'),
|
||||
newapiServiceForm: document.getElementById('newapi-service-form'),
|
||||
newapiServiceModalTitle: document.getElementById('newapi-service-modal-title'),
|
||||
// 验证码设置
|
||||
emailCodeForm: document.getElementById('email-code-form'),
|
||||
// Outlook 设置
|
||||
@@ -87,6 +94,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
loadCpaServices();
|
||||
loadSub2ApiServices();
|
||||
loadTmServices();
|
||||
loadNewapiServices();
|
||||
initEventListeners();
|
||||
});
|
||||
|
||||
@@ -269,6 +277,23 @@ function initEventListeners() {
|
||||
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);
|
||||
}
|
||||
// CPA 服务管理
|
||||
if (elements.addCpaServiceBtn) {
|
||||
elements.addCpaServiceBtn.addEventListener('click', () => openCpaServiceModal());
|
||||
@@ -1215,6 +1240,114 @@ async function handleTestTmService() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNewapiServices() {
|
||||
if (!elements.newapiServicesTable) return;
|
||||
try {
|
||||
const services = await api.get('/newapi-services');
|
||||
renderNewapiServicesTable(services);
|
||||
} catch (e) {
|
||||
elements.newapiServicesTable.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--danger-color);">${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNewapiServicesTable(services) {
|
||||
if (!services || services.length === 0) {
|
||||
elements.newapiServicesTable.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:20px;">暂无 NEWAPI 服务,点击「添加服务」新增</td></tr>';
|
||||
return;
|
||||
}
|
||||
elements.newapiServicesTable.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 style="text-align:center;" title="${s.enabled ? '已启用' : '已禁用'}">${s.enabled ? '✅' : '⭕'}</td>
|
||||
<td style="text-align:center;">${s.priority}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="editNewapiService(${s.id})">编辑</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteNewapiService(${s.id}, '${escapeHtml(s.name)}')">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openNewapiServiceModal(service = null) {
|
||||
document.getElementById('newapi-service-id').value = service ? service.id : '';
|
||||
document.getElementById('newapi-service-name').value = service ? service.name : '';
|
||||
document.getElementById('newapi-service-url').value = service ? service.api_url : '';
|
||||
document.getElementById('newapi-service-key').value = '';
|
||||
document.getElementById('newapi-service-priority').value = service ? service.priority : 0;
|
||||
document.getElementById('newapi-service-enabled').checked = service ? service.enabled : true;
|
||||
if (service) {
|
||||
document.getElementById('newapi-service-key').placeholder = service.has_key ? '已配置,留空保持不变' : '请输入 Root Token / API Key';
|
||||
} else {
|
||||
document.getElementById('newapi-service-key').placeholder = '请输入 Root Token / API Key';
|
||||
}
|
||||
elements.newapiServiceModalTitle.textContent = service ? '编辑 NEWAPI 服务' : '添加 NEWAPI 服务';
|
||||
elements.newapiServiceEditModal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeNewapiServiceModal() {
|
||||
elements.newapiServiceEditModal.classList.remove('active');
|
||||
}
|
||||
|
||||
async function editNewapiService(id) {
|
||||
try {
|
||||
const service = await api.get(`/newapi-services/${id}`);
|
||||
openNewapiServiceModal(service);
|
||||
} catch (e) {
|
||||
toast.error('获取服务信息失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveNewapiService(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('newapi-service-id').value;
|
||||
const name = document.getElementById('newapi-service-name').value.trim();
|
||||
const apiUrl = document.getElementById('newapi-service-url').value.trim();
|
||||
const apiKey = document.getElementById('newapi-service-key').value.trim();
|
||||
const priority = parseInt(document.getElementById('newapi-service-priority').value) || 0;
|
||||
const enabled = document.getElementById('newapi-service-enabled').checked;
|
||||
|
||||
if (!name || !apiUrl) {
|
||||
toast.error('名称和 API URL 不能为空');
|
||||
return;
|
||||
}
|
||||
if (!id && !apiKey) {
|
||||
toast.error('新增服务时 Root Token / API Key 不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = { name, api_url: apiUrl, priority, enabled };
|
||||
if (apiKey) payload.api_key = apiKey;
|
||||
|
||||
if (id) {
|
||||
await api.patch(`/newapi-services/${id}`, payload);
|
||||
toast.success('服务已更新');
|
||||
} else {
|
||||
payload.api_key = apiKey;
|
||||
await api.post('/newapi-services', payload);
|
||||
toast.success('服务已添加');
|
||||
}
|
||||
closeNewapiServiceModal();
|
||||
loadNewapiServices();
|
||||
} catch (e) {
|
||||
toast.error('保存失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNewapiService(id, name) {
|
||||
const confirmed = await confirm(`确定要删除 NEWAPI 服务「${name}」吗?`);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await api.delete(`/newapi-services/${id}`);
|
||||
toast.success('已删除');
|
||||
loadNewapiServices();
|
||||
} catch (e) {
|
||||
toast.error('删除失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============== CPA 服务管理 ==============
|
||||
|
||||
|
||||
Reference in New Issue
Block a user