Spaces:
Sleeping
Sleeping
| 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; | |
| } | |
| } | |