Files
droid2api/account-manager.js
2025-12-27 15:07:54 +08:00

421 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
import fs from 'fs';
import path from 'path';
import { logInfo, logDebug, logError } from './logger.js';
import { getNextProxyAgent } from './proxy-manager.js';
import { getRefreshConfig, requestRefreshToken } from './refresh-client.js';
/**
* Account Manager - 管理多个 OAuth 账号的选择、刷新和统计
*
* 设计思路:
* 1. 支持多个 refresh_token 账号池
* 2. 基于健康度加权轮询选择账号
* 3. 自动刷新 access_token
* 4. 401/402 时自动禁用异常账号
*/
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
const REFRESH_INTERVAL_HOURS = 6;
const CLIENT_ID = 'client_01HNM792M5G5G1A2THWPXKFMXB';
class AccountManager {
constructor() {
this.accounts = [];
this.refreshLocks = new Map(); // 刷新锁,避免同一账号并发刷新
this.settings = {
algorithm: 'weighted', // 'weighted' or 'simple'
refresh_interval_hours: REFRESH_INTERVAL_HOURS,
disable_on_401: true,
disable_on_402: true
};
this.simpleIndex = 0;
this.configPath = null;
this.endpointStats = {}; // 端点统计
}
/**
* 从配置文件加载账号
*/
async loadAccounts(configPath = null) {
// 优先检查环境变量中的多账号配置
const envAccounts = process.env.OAUTH_ACCOUNTS;
if (envAccounts) {
try {
const parsed = JSON.parse(envAccounts);
this.accounts = this._normalizeAccounts(parsed);
logInfo(`AccountManager: 从环境变量加载了 ${this.accounts.length} 个账号`);
return true;
} catch (e) {
logError('AccountManager: 解析 OAUTH_ACCOUNTS 环境变量失败', e);
}
}
// 检查配置文件
const possiblePaths = [
configPath,
path.join(process.cwd(), 'accounts.json'),
path.join(process.cwd(), 'oauth_accounts.json')
].filter(Boolean);
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
try {
const content = fs.readFileSync(p, 'utf-8');
const data = JSON.parse(content);
this.accounts = this._normalizeAccounts(data.accounts || data);
if (data.settings) {
this.settings = { ...this.settings, ...data.settings };
}
this.configPath = p;
logInfo(`AccountManager: 从 ${p} 加载了 ${this.accounts.length} 个账号`);
return true;
} catch (e) {
logError(`AccountManager: 读取配置文件 ${p} 失败`, e);
}
}
}
logInfo('AccountManager: 未找到多账号配置,将回退到单账号模式');
return false;
}
/**
* 标准化账号数据结构
*/
_normalizeAccounts(accounts) {
if (!Array.isArray(accounts)) {
accounts = [accounts];
}
return accounts.map((acc, index) => ({
id: acc.id || `account_${index + 1}`,
name: acc.name || `账号${index + 1}`,
refresh_token: acc.refresh_token,
access_token: acc.access_token || null,
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).getTime() : null,
status: acc.status || 'active', // 'active', 'disabled', 'rate_limited'
disable_reason: acc.disable_reason || null,
stats: acc.stats || { success: 0, fail: 0 }
}));
}
/**
* 获取活跃账号列表
*/
getActiveAccounts() {
return this.accounts.filter(acc => acc.status === 'active');
}
/**
* 选择一个账号(基于配置的算法)
*/
selectAccount() {
const activeAccounts = this.getActiveAccounts();
if (activeAccounts.length === 0) {
throw new Error('AccountManager: 没有可用账号 - 所有账号已被禁用');
}
if (activeAccounts.length === 1) {
return activeAccounts[0];
}
if (this.settings.algorithm === 'simple') {
return this._simpleSelect(activeAccounts);
} else {
return this._weightedSelect(activeAccounts);
}
}
/**
* 简单轮询算法
*/
_simpleSelect(accounts) {
const account = accounts[this.simpleIndex % accounts.length];
this.simpleIndex = (this.simpleIndex + 1) % accounts.length;
logDebug(`AccountManager: 简单轮询选择账号 ${account.id}`);
return account;
}
/**
* 基于健康度的加权选择算法
*/
_weightedSelect(accounts) {
const weights = accounts.map(acc => {
const total = acc.stats.success + acc.stats.fail;
if (total === 0) {
return 1; // 新账号默认权重
}
// 成功率 + 0.1(确保失败账号也有机会恢复)
return (acc.stats.success / total) + 0.1;
});
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
let random = Math.random() * totalWeight;
for (let i = 0; i < weights.length; i++) {
random -= weights[i];
if (random <= 0) {
const acc = accounts[i];
logDebug(`AccountManager: 加权选择账号 ${acc.id}, 健康度: ${((weights[i] - 0.1) * 100).toFixed(1)}%`);
return acc;
}
}
return accounts[accounts.length - 1];
}
/**
* 获取账号的 access_token自动刷新
*/
async getAccessToken(account) {
// 检查是否需要刷新
const needsRefresh = !account.access_token || this._shouldRefresh(account);
if (needsRefresh) {
await this._refreshTokenWithLock(account);
}
return account.access_token;
}
/**
* 带锁刷新,避免同一账号并发刷新
*/
async _refreshTokenWithLock(account) {
const existing = this.refreshLocks.get(account.id);
if (existing) {
return existing;
}
const refreshPromise = this._refreshToken(account)
.finally(() => {
this.refreshLocks.delete(account.id);
});
this.refreshLocks.set(account.id, refreshPromise);
return refreshPromise;
}
/**
* 检查是否需要刷新 token
*/
_shouldRefresh(account) {
if (!account.last_refresh) {
return true;
}
const hoursSinceRefresh = (Date.now() - account.last_refresh) / (1000 * 60 * 60);
return hoursSinceRefresh >= this.settings.refresh_interval_hours;
}
/**
* 刷新账号的 access_token
*/
async _refreshToken(account) {
logInfo(`AccountManager: 刷新账号 ${account.id} 的 token...`);
try {
const proxyAgentInfo = getNextProxyAgent(REFRESH_URL);
const refreshConfig = getRefreshConfig();
const data = await requestRefreshToken({
refreshUrl: REFRESH_URL,
refreshToken: account.refresh_token,
clientId: CLIENT_ID,
proxyAgentInfo,
...refreshConfig
});
// 更新账号信息
account.access_token = data.access_token;
account.refresh_token = data.refresh_token; // 更新 refresh_token
account.last_refresh = Date.now();
// 记录用户信息
if (data.user) {
account.email = data.user.email;
logInfo(`AccountManager: 账号 ${account.id} 刷新成功 - ${data.user.email}`);
}
// 保存配置
this._saveConfig();
return data.access_token;
} catch (error) {
logError(`AccountManager: 账号 ${account.id} 刷新失败`, error);
// 刷新失败可能是 refresh_token 失效,禁用账号
if (error.message.includes('401') || error.message.includes('invalid')) {
this.disableAccount(account.id, 'refresh_token_invalid');
}
throw error;
}
}
/**
* 记录请求结果
*/
recordResult(accountId, endpoint, success, statusCode) {
const account = this.accounts.find(acc => acc.id === accountId);
if (!account) return;
if (success) {
account.stats.success++;
} else {
account.stats.fail++;
// 检查是否需要禁用账号
if (statusCode === 401 && this.settings.disable_on_401) {
this.disableAccount(accountId, '401_unauthorized');
} else if (statusCode === 402 && this.settings.disable_on_402) {
this.disableAccount(accountId, '402_payment_required');
}
}
// 更新端点统计
if (!this.endpointStats[endpoint]) {
this.endpointStats[endpoint] = { success: 0, fail: 0 };
}
if (success) {
this.endpointStats[endpoint].success++;
} else {
this.endpointStats[endpoint].fail++;
}
}
/**
* 禁用账号
*/
disableAccount(accountId, reason) {
const account = this.accounts.find(acc => acc.id === accountId);
if (!account) return;
account.status = 'disabled';
account.disable_reason = reason;
account.disabled_at = new Date().toISOString();
logError(`AccountManager: 账号 ${accountId} 已禁用,原因: ${reason}`);
// 保存配置
this._saveConfig();
// 记录到废弃账号文件
this._logDeprecatedAccount(account);
}
/**
* 记录废弃账号到文件
*/
_logDeprecatedAccount(account) {
try {
const logPath = path.join(process.cwd(), 'deprecated_accounts.txt');
const logEntry = `[${new Date().toISOString()}] ${account.id} (${account.email || 'unknown'}) - ${account.disable_reason}\n`;
fs.appendFileSync(logPath, logEntry, 'utf-8');
} catch (e) {
logError('AccountManager: 记录废弃账号失败', e);
}
}
/**
* 保存配置到文件
*/
_saveConfig() {
if (!this.configPath) return;
try {
const data = {
accounts: this.accounts.map(acc => ({
id: acc.id,
name: acc.name,
refresh_token: acc.refresh_token,
access_token: acc.access_token,
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).toISOString() : null,
status: acc.status,
disable_reason: acc.disable_reason,
email: acc.email,
stats: acc.stats
})),
settings: this.settings,
last_updated: new Date().toISOString()
};
fs.writeFileSync(this.configPath, JSON.stringify(data, null, 2), 'utf-8');
logDebug(`AccountManager: 配置已保存到 ${this.configPath}`);
} catch (e) {
logError('AccountManager: 保存配置失败', e);
}
}
/**
* 获取状态信息(用于 /status 页面)
*/
getStatus() {
return {
total_accounts: this.accounts.length,
active_accounts: this.getActiveAccounts().length,
algorithm: this.settings.algorithm,
accounts: this.accounts.map(acc => ({
id: acc.id,
name: acc.name,
email: acc.email || 'unknown',
status: acc.status,
disable_reason: acc.disable_reason,
stats: acc.stats,
health: this._calculateHealth(acc),
last_refresh: acc.last_refresh ? new Date(acc.last_refresh).toISOString() : null
})),
endpoint_stats: this.endpointStats
};
}
/**
* 计算账号健康度
*/
_calculateHealth(account) {
const total = account.stats.success + account.stats.fail;
if (total === 0) return 100;
return Math.round((account.stats.success / total) * 100);
}
/**
* 检查是否有多账号配置
*/
hasMultipleAccounts() {
return this.accounts.length > 1;
}
/**
* 获取账号数量
*/
getAccountCount() {
return this.accounts.length;
}
}
// 单例实例
let accountManagerInstance = null;
/**
* 获取 AccountManager 实例
*/
export function getAccountManager() {
if (!accountManagerInstance) {
accountManagerInstance = new AccountManager();
}
return accountManagerInstance;
}
/**
* 初始化 AccountManager
*/
export async function initializeAccountManager() {
const manager = getAccountManager();
const loaded = await manager.loadAccounts();
return loaded;
}
export default AccountManager;