#!/usr/bin/env node /** * add-account.js - OAuth 账号授权辅助工具 * * 用途:简化多账号 OAuth refresh_token 的获取流程 * * 使用方式: * node add-account.js # 交互式添加账号 * node add-account.js --loop # 连续添加多个账号 * * 流程: * 1. 向 WorkOS 请求设备授权码 * 2. 自动打开浏览器(或显示链接) * 3. 用户在浏览器完成登录 * 4. 脚本轮询获取 access_token + refresh_token * 5. 自动保存到 accounts.json */ import fs from 'fs'; import path from 'path'; import fetch from 'node-fetch'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // WorkOS 配置(从 Factory CLI 逆向获取) const WORKOS_CLIENT_ID = 'client_01HNM792M5G5G1A2THWPXKFMXB'; const WORKOS_AUTHORIZE_URL = 'https://api.workos.com/user_management/authorize/device'; const WORKOS_TOKEN_URL = 'https://api.workos.com/user_management/authenticate'; // 配置 const ACCOUNTS_FILE = path.join(process.cwd(), 'accounts.json'); const POLL_INTERVAL_MS = 2000; // 轮询间隔 2 秒 const POLL_TIMEOUT_MS = 300000; // 轮询超时 5 分钟 /** * 请求设备授权码 */ async function requestDeviceCode() { console.log('\n🔐 正在请求设备授权码...'); const response = await fetch(WORKOS_AUTHORIZE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: WORKOS_CLIENT_ID }) }); if (!response.ok) { const error = await response.text(); throw new Error(`请求设备授权码失败: ${response.status} ${error}`); } const data = await response.json(); return data; } /** * 轮询获取 token */ async function pollForToken(deviceCode) { const startTime = Date.now(); while (Date.now() - startTime < POLL_TIMEOUT_MS) { try { const formData = new URLSearchParams(); formData.append('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'); formData.append('device_code', deviceCode); formData.append('client_id', WORKOS_CLIENT_ID); const response = await fetch(WORKOS_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString() }); if (response.ok) { const data = await response.json(); return data; } const errorData = await response.json().catch(() => ({})); if (errorData.error === 'authorization_pending') { // 用户还未授权,继续轮询 process.stdout.write('.'); } else if (errorData.error === 'slow_down') { // 需要降低轮询速度 await sleep(POLL_INTERVAL_MS * 2); continue; } else if (errorData.error === 'expired_token') { throw new Error('授权码已过期,请重新开始'); } else if (errorData.error === 'access_denied') { throw new Error('授权被拒绝'); } } catch (err) { if (err.message.includes('授权')) { throw err; } // 网络错误,继续轮询 } await sleep(POLL_INTERVAL_MS); } throw new Error('授权超时(5分钟),请重新开始'); } /** * 保存账号到配置文件 */ function saveAccount(tokenData) { let accounts = { accounts: [], settings: {} }; // 读取现有配置 if (fs.existsSync(ACCOUNTS_FILE)) { try { accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8')); } catch (e) { console.warn('⚠️ 无法读取现有配置,将创建新文件'); } } // 确保 accounts 数组存在 if (!accounts.accounts) { accounts.accounts = []; } // 提取用户信息 const user = tokenData.user || {}; const email = user.email || 'unknown'; // 检查是否已存在此账号 const existingIndex = accounts.accounts.findIndex(acc => acc.email === email); if (existingIndex >= 0) { // 更新现有账号 accounts.accounts[existingIndex] = { ...accounts.accounts[existingIndex], refresh_token: tokenData.refresh_token, access_token: tokenData.access_token, last_refresh: new Date().toISOString(), status: 'active' }; console.log(`\n✅ 账号已更新: ${email}`); } else { // 添加新账号 const newAccount = { id: `account_${accounts.accounts.length + 1}`, name: user.first_name ? `${user.first_name} ${user.last_name}` : email, email: email, refresh_token: tokenData.refresh_token, access_token: tokenData.access_token, last_refresh: new Date().toISOString(), status: 'active', stats: { success: 0, fail: 0 } }; accounts.accounts.push(newAccount); console.log(`\n✅ 新账号已添加: ${email}`); } // 设置默认配置 if (!accounts.settings || Object.keys(accounts.settings).length === 0) { accounts.settings = { algorithm: 'weighted', refresh_interval_hours: 6, disable_on_401: true, disable_on_402: true }; } // 保存配置 accounts.last_updated = new Date().toISOString(); fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), 'utf-8'); console.log(`📁 配置已保存到: ${ACCOUNTS_FILE}`); return accounts; } /** * 尝试自动打开浏览器 */ async function openBrowser(url) { const { exec } = await import('child_process'); const platform = process.platform; return new Promise((resolve) => { let command; if (platform === 'darwin') { command = `open "${url}"`; } else if (platform === 'win32') { command = `start "" "${url}"`; } else { command = `xdg-open "${url}"`; } exec(command, (error) => { if (error) { console.log('\n⚠️ 无法自动打开浏览器,请手动打开以下链接'); resolve(false); } else { resolve(true); } }); }); } /** * 睡眠函数 */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 显示当前账号状态 */ function showAccountStatus() { if (!fs.existsSync(ACCOUNTS_FILE)) { console.log('\n📋 当前没有已配置的账号'); return; } try { const accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8')); console.log('\n📋 当前已配置的账号:'); console.log('─'.repeat(50)); if (!accounts.accounts || accounts.accounts.length === 0) { console.log(' (无)'); } else { accounts.accounts.forEach((acc, i) => { const status = acc.status === 'active' ? '🟢' : '🔴'; console.log(` ${i + 1}. ${status} ${acc.email || acc.name} [${acc.id}]`); }); } console.log('─'.repeat(50)); } catch (e) { console.log('\n⚠️ 无法读取账号配置'); } } /** * 读取用户输入 */ function readline(question) { return new Promise((resolve) => { const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout }); rl.question(question, (answer) => { rl.close(); resolve(answer); }); }); } /** * 主流程 */ async function addAccount() { console.log('═'.repeat(50)); console.log(' 🔐 OAuth 账号授权工具'); console.log('═'.repeat(50)); try { // 1. 请求设备授权码 const deviceAuth = await requestDeviceCode(); console.log('\n📱 请在浏览器中完成授权:'); console.log('─'.repeat(50)); console.log(` 验证码: ${deviceAuth.user_code}`); console.log(` 链接: ${deviceAuth.verification_uri_complete || deviceAuth.verification_uri}`); console.log('─'.repeat(50)); // 2. 尝试自动打开浏览器 const browserOpened = await openBrowser( deviceAuth.verification_uri_complete || deviceAuth.verification_uri ); if (browserOpened) { console.log('\n🌐 已自动打开浏览器,请完成登录...'); } else { console.log('\n👆 请复制上面的链接到浏览器打开'); } // 3. 轮询获取 token console.log('\n⏳ 等待授权中'); const tokenData = await pollForToken(deviceAuth.device_code); // 4. 保存账号 const accounts = saveAccount(tokenData); console.log(`\n🎉 授权成功!当前共有 ${accounts.accounts.length} 个账号`); return true; } catch (error) { console.error(`\n❌ 错误: ${error.message}`); return false; } } /** * 入口函数 */ async function main() { const args = process.argv.slice(2); const loopMode = args.includes('--loop') || args.includes('-l'); // 显示当前状态 showAccountStatus(); do { const success = await addAccount(); if (loopMode && success) { console.log('\n━'.repeat(50)); console.log('按 Enter 继续添加下一个账号,或输入 q 退出'); // 等待用户输入 const readlineModule = await import('readline'); const rl = readlineModule.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise(resolve => { rl.question('> ', (ans) => { rl.close(); resolve(ans); }); }); if (answer.toLowerCase() === 'q') { break; } } else if (!loopMode) { break; } } while (loopMode); // 显示最终状态 showAccountStatus(); console.log('\n✨ 完成!'); } // 运行 main().catch(console.error);