import fs from 'fs/promises'; import AdmZip from 'adm-zip'; import path from 'path'; import { spawn } from 'child_process'; import logger from '../utils/logger.js'; const ACCOUNTS_FILE = path.join(process.cwd(), 'data', 'accounts.json'); // 读取所有账号 export async function loadAccounts() { try { const data = await fs.readFile(ACCOUNTS_FILE, 'utf-8'); return JSON.parse(data); } catch (error) { if (error.code === 'ENOENT') { return []; } throw error; } } // 保存账号 async function saveAccounts(accounts) { const dir = path.dirname(ACCOUNTS_FILE); try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); } await fs.writeFile(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), 'utf-8'); } // 删除账号 export async function deleteAccount(index) { const accounts = await loadAccounts(); if (index < 0 || index >= accounts.length) { throw new Error('无效的账号索引'); } accounts.splice(index, 1); await saveAccounts(accounts); logger.info(`账号 ${index} 已删除`); return true; } // 启用/禁用账号 export async function toggleAccount(index, enable) { const accounts = await loadAccounts(); if (index < 0 || index >= accounts.length) { throw new Error('无效的账号索引'); } accounts[index].enable = enable; await saveAccounts(accounts); logger.info(`账号 ${index} 已${enable ? '启用' : '禁用'}`); return true; } // 触发登录流程 export async function triggerLogin() { return new Promise((resolve, reject) => { logger.info('启动登录流程...'); const loginScript = path.join(process.cwd(), 'scripts', 'oauth-server.js'); const child = spawn('node', [loginScript], { stdio: 'pipe', shell: true }); let authUrl = ''; let output = ''; child.stdout.on('data', (data) => { const text = data.toString(); output += text; // 提取授权 URL const urlMatch = text.match(/(https:\/\/accounts\.google\.com\/o\/oauth2\/v2\/auth\?[^\s]+)/); if (urlMatch) { authUrl = urlMatch[1]; } logger.info(text.trim()); }); child.stderr.on('data', (data) => { logger.error(data.toString().trim()); }); child.on('close', (code) => { if (code === 0) { logger.info('登录流程完成'); resolve({ success: true, authUrl, message: '登录成功' }); } else { reject(new Error('登录流程失败')); } }); // 5 秒后返回授权 URL,不等待完成 setTimeout(() => { if (authUrl) { resolve({ success: true, authUrl, message: '请在浏览器中完成授权' }); } }, 5000); child.on('error', (error) => { reject(error); }); }); } // 获取账号统计信息 export async function getAccountStats() { const accounts = await loadAccounts(); return { total: accounts.length, enabled: accounts.filter(a => a.enable !== false).length, disabled: accounts.filter(a => a.enable === false).length }; } // 从回调链接手动添加 Token import https from 'https'; const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'; const CLIENT_SECRET = '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'; // 获取 Google 账号信息 export async function getAccountName(accessToken) { return new Promise((resolve, reject) => { const options = { hostname: 'www.googleapis.com', path: '/oauth2/v2/userinfo', method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }; const req = https.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode === 200) { const data = JSON.parse(body); resolve({ email: data.email, name: data.name || data.email }); } else { resolve({ email: 'Unknown', name: 'Unknown' }); } }); }); req.on('error', () => resolve({ email: 'Unknown', name: 'Unknown' })); req.end(); }); } export async function addTokenFromCallback(callbackUrl) { // 解析回调链接 const url = new URL(callbackUrl); const code = url.searchParams.get('code'); const port = url.port || '80'; if (!code) { throw new Error('回调链接中没有找到授权码 (code)'); } logger.info(`正在使用授权码换取 Token...`); // 使用授权码换取 Token const tokenData = await exchangeCodeForToken(code, port, url.origin); // 保存账号 const account = { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, expires_in: tokenData.expires_in, timestamp: Date.now(), enable: true }; const accounts = await loadAccounts(); accounts.push(account); await saveAccounts(accounts); logger.info('Token 已成功保存'); return { success: true, message: 'Token 已成功添加' }; } function exchangeCodeForToken(code, port, origin) { return new Promise((resolve, reject) => { const redirectUri = `${origin}/oauth-callback`; const postData = new URLSearchParams({ code: code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: redirectUri, grant_type: 'authorization_code' }).toString(); const options = { hostname: 'oauth2.googleapis.com', path: '/token', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { if (res.statusCode === 200) { resolve(JSON.parse(body)); } else { logger.error(`Token 交换失败: ${body}`); reject(new Error(`Token 交换失败: ${res.statusCode} - ${body}`)); } }); }); req.on('error', reject); req.write(postData); req.end(); }); } // 批量导入 Token export async function importTokens(filePath) { try { logger.info('开始导入 Token...'); // 检查是否是 ZIP 文件 if (filePath.endsWith('.zip') || true) { const zip = new AdmZip(filePath); const zipEntries = zip.getEntries(); // 查找 tokens.json const tokensEntry = zipEntries.find(entry => entry.entryName === 'tokens.json'); if (!tokensEntry) { throw new Error('ZIP 文件中没有找到 tokens.json'); } const tokensContent = tokensEntry.getData().toString('utf8'); const importedTokens = JSON.parse(tokensContent); // 验证数据格式 if (!Array.isArray(importedTokens)) { throw new Error('tokens.json 格式错误:应该是一个数组'); } // 加载现有账号 const accounts = await loadAccounts(); // 添加新账号 let addedCount = 0; for (const token of importedTokens) { // 检查是否已存在 const exists = accounts.some(acc => acc.access_token === token.access_token); if (!exists) { accounts.push({ access_token: token.access_token, refresh_token: token.refresh_token, expires_in: token.expires_in, timestamp: token.timestamp || Date.now(), enable: token.enable !== false }); addedCount++; } } // 保存账号 await saveAccounts(accounts); // 清理上传的文件 try { await fs.unlink(filePath); } catch (e) { logger.warn('清理上传文件失败:', e); } logger.info(`成功导入 ${addedCount} 个 Token 账号`); return { success: true, count: addedCount, total: importedTokens.length, skipped: importedTokens.length - addedCount, message: `成功导入 ${addedCount} 个 Token 账号${importedTokens.length - addedCount > 0 ? `,跳过 ${importedTokens.length - addedCount} 个重复账号` : ''}` }; } } catch (error) { logger.error('导入 Token 失败:', error); // 清理上传的文件 try { await fs.unlink(filePath); } catch (e) {} throw error; } }