Files
droid2api/add-account.js
empty dab863fcfe feat: 添加多账号 OAuth 支持
- 新增 add-account.js OAuth 批量授权辅助工具
- 新增 account-manager.js 多账号管理模块(加权轮询、自动刷新、健康度统计)
- 新增 accounts.json.example 配置示例
- 修改 auth.js 支持多账号模式(优先检测 accounts.json)
2025-12-27 12:23:41 +08:00

357 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);