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

111
auth.js
View File

@@ -4,15 +4,18 @@ import os from 'os';
import fetch from 'node-fetch';
import { logDebug, logError, logInfo } from './logger.js';
import { getNextProxyAgent } from './proxy-manager.js';
import { getAccountManager, initializeAccountManager } from './account-manager.js';
// State management for API key and refresh
let currentApiKey = null;
let currentRefreshToken = null;
let lastRefreshTime = null;
let clientId = null;
let authSource = null; // 'env' or 'file' or 'factory_key' or 'client'
let authSource = null; // 'env' or 'file' or 'factory_key' or 'client' or 'multi_account'
let authFilePath = null;
let factoryApiKey = null; // From FACTORY_API_KEY environment variable
let multiAccountMode = false; // 是否启用多账号模式
let lastSelectedAccountId = null; // 记录最后选择的账号ID用于结果回调
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
@@ -27,10 +30,10 @@ const TOKEN_VALID_HOURS = 8; // Token valid for 8 hours
function generateULID() {
// Crockford's Base32 alphabet (no I, L, O, U to avoid confusion)
const ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
// Get timestamp in milliseconds
const timestamp = Date.now();
// Encode timestamp to 10 characters
let time = '';
let ts = timestamp;
@@ -39,14 +42,14 @@ function generateULID() {
time = ENCODING[mod] + time;
ts = Math.floor(ts / 32);
}
// Generate 16 random characters
let randomPart = '';
for (let i = 0; i < 16; i++) {
const rand = Math.floor(Math.random() * 32);
randomPart += ENCODING[rand];
}
return time + randomPart;
}
@@ -60,10 +63,31 @@ function generateClientId() {
/**
* Load auth configuration with priority system
* Priority: FACTORY_API_KEY > refresh token mechanism > client authorization
* Priority: accounts.json (multi-account) > FACTORY_API_KEY > refresh token mechanism > client authorization
*/
function loadAuthConfig() {
// 1. Check FACTORY_API_KEY environment variable (highest priority)
// 0. Check accounts.json for multi-account mode (highest priority)
const accountsPath = path.join(process.cwd(), 'accounts.json');
try {
if (fs.existsSync(accountsPath)) {
const accountsContent = fs.readFileSync(accountsPath, 'utf-8');
const accountsData = JSON.parse(accountsContent);
if (accountsData.accounts && accountsData.accounts.length > 0) {
const activeAccounts = accountsData.accounts.filter(acc => acc.status === 'active');
if (activeAccounts.length > 0) {
logInfo(`Found accounts.json with ${activeAccounts.length} active account(s), enabling multi-account mode`);
multiAccountMode = true;
authSource = 'multi_account';
return { type: 'multi_account', value: accountsPath };
}
}
}
} catch (error) {
logError('Error reading accounts.json', error);
}
// 1. Check FACTORY_API_KEY environment variable
const factoryKey = process.env.FACTORY_API_KEY;
if (factoryKey && factoryKey.trim() !== '') {
logInfo('Using fixed API key from FACTORY_API_KEY environment variable');
@@ -84,22 +108,22 @@ function loadAuthConfig() {
// 3. Check ~/.factory/auth.json
const homeDir = os.homedir();
const factoryAuthPath = path.join(homeDir, '.factory', 'auth.json');
try {
if (fs.existsSync(factoryAuthPath)) {
const authContent = fs.readFileSync(factoryAuthPath, 'utf-8');
const authData = JSON.parse(authContent);
if (authData.refresh_token && authData.refresh_token.trim() !== '') {
logInfo('Using refresh token from ~/.factory/auth.json');
authSource = 'file';
authFilePath = factoryAuthPath;
// Also load access_token if available
if (authData.access_token) {
currentApiKey = authData.access_token.trim();
}
return { type: 'refresh', value: authData.refresh_token.trim() };
}
}
@@ -156,7 +180,7 @@ async function refreshApiKey() {
}
const data = await response.json();
// Update tokens
currentApiKey = data.access_token;
currentRefreshToken = data.refresh_token;
@@ -240,7 +264,18 @@ export async function initializeAuth() {
try {
const authConfig = loadAuthConfig();
if (authConfig.type === 'factory_key') {
if (authConfig.type === 'multi_account') {
// Multi-account mode - initialize AccountManager
const loaded = await initializeAccountManager();
if (loaded) {
const manager = getAccountManager();
logInfo(`Auth system initialized with multi-account mode (${manager.getAccountCount()} accounts)`);
} else {
logError('Failed to initialize AccountManager, falling back to client authorization');
authSource = 'client';
multiAccountMode = false;
}
} else if (authConfig.type === 'factory_key') {
// Using fixed FACTORY_API_KEY, no refresh needed
logInfo('Auth system initialized with fixed API key');
} else if (authConfig.type === 'refresh') {
@@ -275,11 +310,21 @@ export async function initializeAuth() {
* @param {string} clientAuthorization - Authorization header from client request (optional)
*/
export async function getApiKey(clientAuthorization = null) {
// Priority 0: Multi-account mode
if (authSource === 'multi_account' && multiAccountMode) {
const manager = getAccountManager();
const account = manager.selectAccount();
lastSelectedAccountId = account.id;
const accessToken = await manager.getAccessToken(account);
return `Bearer ${accessToken}`;
}
// Priority 1: FACTORY_API_KEY environment variable
if (authSource === 'factory_key' && factoryApiKey) {
return `Bearer ${factoryApiKey}`;
}
// Priority 2: Refresh token mechanism
if (authSource === 'env' || authSource === 'file') {
// Check if we need to refresh
@@ -294,13 +339,45 @@ export async function getApiKey(clientAuthorization = null) {
return `Bearer ${currentApiKey}`;
}
// Priority 3: Client authorization header
if (clientAuthorization) {
logDebug('Using client authorization header');
return clientAuthorization;
}
// No authorization available
throw new Error('No authorization available. Please configure FACTORY_API_KEY, refresh token, or provide client authorization.');
throw new Error('No authorization available. Please configure accounts.json, FACTORY_API_KEY, refresh token, or provide client authorization.');
}
/**
* Record authentication result for multi-account mode
* @param {string} endpoint - The API endpoint called
* @param {boolean} success - Whether the request was successful
* @param {number} statusCode - HTTP status code
*/
export function recordAuthResult(endpoint, success, statusCode) {
if (authSource === 'multi_account' && multiAccountMode && lastSelectedAccountId) {
const manager = getAccountManager();
manager.recordResult(lastSelectedAccountId, endpoint, success, statusCode);
}
}
/**
* Get auth status for /status endpoint
*/
export function getAuthStatus() {
if (authSource === 'multi_account' && multiAccountMode) {
const manager = getAccountManager();
return {
mode: 'multi_account',
...manager.getStatus()
};
}
return {
mode: authSource,
multiAccountMode: false
};
}