feat: 添加多账号 OAuth 支持

- 新增 add-account.js OAuth 批量授权辅助工具
- 新增 account-manager.js 多账号管理模块(加权轮询、自动刷新、健康度统计)
- 新增 accounts.json.example 配置示例
- 修改 auth.js 支持多账号模式(优先检测 accounts.json)
This commit is contained in:
empty
2025-12-27 12:23:41 +08:00
parent eb1096ce54
commit dab863fcfe
4 changed files with 890 additions and 17 deletions

356
add-account.js Normal file
View File

@@ -0,0 +1,356 @@
#!/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);