@pikoloo/codex-proxy 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/cli.js +118 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +289 -0
- package/docs/ARCHITECTURE.md +129 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +85 -0
- package/docs/OPENCLAW.md +34 -0
- package/docs/legal.md +11 -0
- package/images/dashboard-screenshot.png +0 -0
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +61 -0
- package/public/css/style.css +1502 -0
- package/public/index.html +827 -0
- package/public/js/app.js +601 -0
- package/src/account-manager.js +528 -0
- package/src/account-rotation/index.js +93 -0
- package/src/account-rotation/rate-limits.js +293 -0
- package/src/account-rotation/strategies/base-strategy.js +48 -0
- package/src/account-rotation/strategies/index.js +31 -0
- package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
- package/src/account-rotation/strategies/sticky-strategy.js +97 -0
- package/src/claude-config.js +153 -0
- package/src/cli/accounts.js +557 -0
- package/src/direct-api.js +164 -0
- package/src/format-converter.js +420 -0
- package/src/index.js +46 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +285 -0
- package/src/kilo-models.js +103 -0
- package/src/kilo-streamer.js +243 -0
- package/src/middleware/credentials.js +116 -0
- package/src/middleware/sse.js +96 -0
- package/src/model-api.js +189 -0
- package/src/model-mapper.js +157 -0
- package/src/oauth.js +666 -0
- package/src/response-streamer.js +409 -0
- package/src/routes/accounts-route.js +332 -0
- package/src/routes/api-routes.js +98 -0
- package/src/routes/chat-route.js +229 -0
- package/src/routes/claude-config-route.js +121 -0
- package/src/routes/logs-route.js +43 -0
- package/src/routes/messages-route.js +203 -0
- package/src/routes/models-route.js +119 -0
- package/src/routes/settings-route.js +143 -0
- package/src/security.js +142 -0
- package/src/server-settings.js +56 -0
- package/src/server.js +58 -0
- package/src/signature-cache.js +106 -0
- package/src/thinking-utils.js +312 -0
- package/src/utils/logger.js +156 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Manager
|
|
3
|
+
* Manages multiple ChatGPT accounts with manual switching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { refreshAccessToken, extractAccountInfo } from './oauth.js';
|
|
10
|
+
import { getAccountQuota as fetchQuota } from './model-api.js';
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = join(homedir(), '.codex-claude-proxy');
|
|
13
|
+
const ACCOUNTS_FILE = join(CONFIG_DIR, 'accounts.json');
|
|
14
|
+
const ACCOUNTS_DIR = join(CONFIG_DIR, 'accounts');
|
|
15
|
+
|
|
16
|
+
const TOKEN_REFRESH_INTERVAL_MS = 55 * 60 * 1000;
|
|
17
|
+
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ACCOUNTS = {
|
|
20
|
+
accounts: [],
|
|
21
|
+
activeAccount: null,
|
|
22
|
+
version: 1
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let autoRefreshIntervalId = null;
|
|
26
|
+
const tokenCache = new Map();
|
|
27
|
+
let accountsData = null;
|
|
28
|
+
|
|
29
|
+
function ensureConfigDir() {
|
|
30
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
31
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
32
|
+
}
|
|
33
|
+
if (!existsSync(ACCOUNTS_DIR)) {
|
|
34
|
+
mkdirSync(ACCOUNTS_DIR, { recursive: true, mode: 0o700 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sanitizeEmailForPath(email) {
|
|
39
|
+
return email.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getAccountDir(email) {
|
|
43
|
+
const safeEmail = sanitizeEmailForPath(email);
|
|
44
|
+
return join(ACCOUNTS_DIR, safeEmail);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getAccountAuthFile(email) {
|
|
48
|
+
return join(getAccountDir(email), 'auth.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadAccounts() {
|
|
52
|
+
if (accountsData !== null) {
|
|
53
|
+
return accountsData;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ensureConfigDir();
|
|
57
|
+
|
|
58
|
+
if (!existsSync(ACCOUNTS_FILE)) {
|
|
59
|
+
accountsData = { ...DEFAULT_ACCOUNTS };
|
|
60
|
+
return accountsData;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(readFileSync(ACCOUNTS_FILE, 'utf8'));
|
|
65
|
+
accountsData = { ...DEFAULT_ACCOUNTS, ...data };
|
|
66
|
+
return accountsData;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error('[AccountManager] Error loading accounts:', e.message);
|
|
69
|
+
accountsData = { ...DEFAULT_ACCOUNTS };
|
|
70
|
+
return accountsData;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function saveAccounts(data) {
|
|
75
|
+
ensureConfigDir();
|
|
76
|
+
accountsData = data;
|
|
77
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function save() {
|
|
81
|
+
if (accountsData === null) {
|
|
82
|
+
loadAccounts();
|
|
83
|
+
}
|
|
84
|
+
ensureConfigDir();
|
|
85
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(accountsData, null, 2), { mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getAccount(email) {
|
|
89
|
+
const data = loadAccounts();
|
|
90
|
+
return data.accounts.find(a => a.email === email) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getActiveAccount() {
|
|
94
|
+
const data = loadAccounts();
|
|
95
|
+
if (!data.activeAccount) return null;
|
|
96
|
+
return data.accounts.find(a => a.email === data.activeAccount) || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function updateAccountAuth(account) {
|
|
100
|
+
if (!account) return;
|
|
101
|
+
|
|
102
|
+
const accountDir = getAccountDir(account.email);
|
|
103
|
+
const authFile = getAccountAuthFile(account.email);
|
|
104
|
+
|
|
105
|
+
if (!existsSync(accountDir)) {
|
|
106
|
+
mkdirSync(accountDir, { recursive: true, mode: 0o700 });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const authData = {
|
|
110
|
+
auth_mode: 'chatgpt',
|
|
111
|
+
OPENAI_API_KEY: null,
|
|
112
|
+
tokens: {
|
|
113
|
+
id_token: account.idToken,
|
|
114
|
+
access_token: account.accessToken,
|
|
115
|
+
refresh_token: account.refreshToken,
|
|
116
|
+
account_id: account.accountId
|
|
117
|
+
},
|
|
118
|
+
last_refresh: new Date().toISOString()
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(authFile, JSON.stringify(authData, null, 2), { mode: 0o600 });
|
|
123
|
+
console.log(`[AccountManager] Updated auth for: ${account.email}`);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error('[AccountManager] Failed to update auth:', e.message);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function setActiveAccount(email) {
|
|
130
|
+
const data = loadAccounts();
|
|
131
|
+
const account = data.accounts.find(a => a.email === email);
|
|
132
|
+
|
|
133
|
+
if (!account) {
|
|
134
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
data.activeAccount = email;
|
|
138
|
+
saveAccounts(data);
|
|
139
|
+
|
|
140
|
+
updateAccountAuth(account);
|
|
141
|
+
|
|
142
|
+
return { success: true, message: `Switched to account: ${email}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function removeAccount(email) {
|
|
146
|
+
const data = loadAccounts();
|
|
147
|
+
const index = data.accounts.findIndex(a => a.email === email);
|
|
148
|
+
|
|
149
|
+
if (index < 0) {
|
|
150
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const accountDir = getAccountDir(email);
|
|
154
|
+
try {
|
|
155
|
+
if (existsSync(accountDir)) {
|
|
156
|
+
rmSync(accountDir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error('[AccountManager] Failed to remove account directory:', e.message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
data.accounts.splice(index, 1);
|
|
163
|
+
|
|
164
|
+
if (data.activeAccount === email) {
|
|
165
|
+
data.activeAccount = data.accounts[0]?.email || null;
|
|
166
|
+
|
|
167
|
+
if (data.activeAccount) {
|
|
168
|
+
const newActive = data.accounts.find(a => a.email === data.activeAccount);
|
|
169
|
+
updateAccountAuth(newActive);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
saveAccounts(data);
|
|
174
|
+
|
|
175
|
+
return { success: true, message: `Account removed: ${email}` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function listAccounts() {
|
|
179
|
+
const data = loadAccounts();
|
|
180
|
+
|
|
181
|
+
const accounts = data.accounts.map(account => {
|
|
182
|
+
const info = extractAccountInfo(account.accessToken);
|
|
183
|
+
return {
|
|
184
|
+
email: account.email,
|
|
185
|
+
accountId: account.accountId,
|
|
186
|
+
planType: info?.planType || account.planType || 'unknown',
|
|
187
|
+
addedAt: account.addedAt,
|
|
188
|
+
lastUsed: account.lastUsed,
|
|
189
|
+
isActive: account.email === data.activeAccount,
|
|
190
|
+
tokenExpired: info?.expiresAt ? info.expiresAt < Date.now() : false,
|
|
191
|
+
quota: account.quota || null
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
accounts,
|
|
197
|
+
activeAccount: data.activeAccount,
|
|
198
|
+
total: accounts.length
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function updateAccountQuota(email, quotaData) {
|
|
203
|
+
const data = loadAccounts();
|
|
204
|
+
const account = data.accounts.find(a => a.email === email);
|
|
205
|
+
|
|
206
|
+
if (!account) {
|
|
207
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
account.quota = {
|
|
211
|
+
...quotaData,
|
|
212
|
+
lastChecked: new Date().toISOString()
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
saveAccounts(data);
|
|
216
|
+
return { success: true, message: `Quota updated for: ${email}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getAccountQuota(email) {
|
|
220
|
+
const data = loadAccounts();
|
|
221
|
+
const account = data.accounts.find(a => a.email === email);
|
|
222
|
+
|
|
223
|
+
if (!account) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return account.quota || null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isTokenExpiredOrExpiringSoon(account) {
|
|
231
|
+
if (!account.expiresAt) return true;
|
|
232
|
+
return Date.now() >= (account.expiresAt - TOKEN_EXPIRY_BUFFER_MS);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function refreshAccountToken(email) {
|
|
236
|
+
const data = loadAccounts();
|
|
237
|
+
const account = data.accounts.find(a => a.email === email);
|
|
238
|
+
|
|
239
|
+
if (!account) {
|
|
240
|
+
return { success: false, message: `Account not found: ${email}` };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!account.refreshToken) {
|
|
244
|
+
return { success: false, message: 'No refresh token available' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
249
|
+
const accountInfo = extractAccountInfo(tokens.accessToken);
|
|
250
|
+
|
|
251
|
+
const index = data.accounts.findIndex(a => a.email === email);
|
|
252
|
+
if (index >= 0) {
|
|
253
|
+
data.accounts[index].accessToken = tokens.accessToken;
|
|
254
|
+
data.accounts[index].refreshToken = tokens.refreshToken || data.accounts[index].refreshToken;
|
|
255
|
+
data.accounts[index].idToken = tokens.idToken || data.accounts[index].idToken;
|
|
256
|
+
data.accounts[index].expiresAt = accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000);
|
|
257
|
+
if (accountInfo?.planType) {
|
|
258
|
+
data.accounts[index].planType = accountInfo.planType;
|
|
259
|
+
}
|
|
260
|
+
saveAccounts(data);
|
|
261
|
+
|
|
262
|
+
tokenCache.set(email, {
|
|
263
|
+
token: tokens.accessToken,
|
|
264
|
+
extractedAt: Date.now()
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (data.activeAccount === email) {
|
|
268
|
+
updateAccountAuth(data.accounts[index]);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`[AccountManager] Token refreshed for: ${email}`);
|
|
273
|
+
|
|
274
|
+
// Auto-fetch quota after refresh
|
|
275
|
+
try {
|
|
276
|
+
const quotaData = await fetchQuota(tokens.accessToken, accountInfo.accountId);
|
|
277
|
+
updateAccountQuota(email, quotaData);
|
|
278
|
+
console.log(`[AccountManager] Quota refreshed for: ${email}`);
|
|
279
|
+
} catch (qErr) {
|
|
280
|
+
console.warn(`[AccountManager] Failed to auto-fetch quota for ${email}: ${qErr.message}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { success: true, message: `Token refreshed for: ${email}` };
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(`[AccountManager] Token refresh failed for ${email}:`, error.message);
|
|
286
|
+
return { success: false, message: `Token refresh failed: ${error.message}` };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function refreshAllAccounts() {
|
|
291
|
+
const data = loadAccounts();
|
|
292
|
+
const results = [];
|
|
293
|
+
|
|
294
|
+
for (const account of data.accounts) {
|
|
295
|
+
if (account.refreshToken) {
|
|
296
|
+
const result = await refreshAccountToken(account.email);
|
|
297
|
+
results.push({ email: account.email, ...result });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function startAutoRefresh() {
|
|
305
|
+
if (autoRefreshIntervalId) {
|
|
306
|
+
clearInterval(autoRefreshIntervalId);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const startupRefreshTimeout = setTimeout(async () => {
|
|
310
|
+
console.log('[AccountManager] Startup: refreshing all account tokens...');
|
|
311
|
+
const data = loadAccounts();
|
|
312
|
+
for (const account of data.accounts) {
|
|
313
|
+
if (account.refreshToken) {
|
|
314
|
+
console.log(`[AccountManager] Startup refresh for ${account.email}`);
|
|
315
|
+
await refreshAccountToken(account.email);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}, 2000);
|
|
319
|
+
startupRefreshTimeout.unref?.();
|
|
320
|
+
|
|
321
|
+
autoRefreshIntervalId = setInterval(async () => {
|
|
322
|
+
const data = loadAccounts();
|
|
323
|
+
|
|
324
|
+
for (const account of data.accounts) {
|
|
325
|
+
if (account.refreshToken) {
|
|
326
|
+
console.log(`[AccountManager] Periodic refresh for ${account.email}`);
|
|
327
|
+
await refreshAccountToken(account.email);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}, TOKEN_REFRESH_INTERVAL_MS);
|
|
331
|
+
autoRefreshIntervalId.unref?.();
|
|
332
|
+
|
|
333
|
+
console.log('[AccountManager] Auto-refresh started (every 55 minutes)');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function stopAutoRefresh() {
|
|
337
|
+
if (autoRefreshIntervalId) {
|
|
338
|
+
clearInterval(autoRefreshIntervalId);
|
|
339
|
+
autoRefreshIntervalId = null;
|
|
340
|
+
console.log('[AccountManager] Auto-refresh stopped');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getCachedToken(email) {
|
|
345
|
+
const cached = tokenCache.get(email);
|
|
346
|
+
if (cached && (Date.now() - cached.extractedAt) < TOKEN_REFRESH_INTERVAL_MS) {
|
|
347
|
+
return cached.token;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setCachedToken(email, token) {
|
|
353
|
+
tokenCache.set(email, { token, extractedAt: Date.now() });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function refreshActiveAccount() {
|
|
357
|
+
const account = getActiveAccount();
|
|
358
|
+
if (!account) {
|
|
359
|
+
return { success: false, message: 'No active account' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!account.refreshToken) {
|
|
363
|
+
return { success: false, message: 'No refresh token available' };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const tokens = await refreshAccessToken(account.refreshToken);
|
|
368
|
+
const accountInfo = extractAccountInfo(tokens.accessToken);
|
|
369
|
+
|
|
370
|
+
const data = loadAccounts();
|
|
371
|
+
const index = data.accounts.findIndex(a => a.email === account.email);
|
|
372
|
+
|
|
373
|
+
if (index >= 0) {
|
|
374
|
+
data.accounts[index].accessToken = tokens.accessToken;
|
|
375
|
+
data.accounts[index].refreshToken = tokens.refreshToken || data.accounts[index].refreshToken;
|
|
376
|
+
data.accounts[index].idToken = tokens.idToken || data.accounts[index].idToken;
|
|
377
|
+
data.accounts[index].expiresAt = accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000);
|
|
378
|
+
if (accountInfo?.planType) {
|
|
379
|
+
data.accounts[index].planType = accountInfo.planType;
|
|
380
|
+
}
|
|
381
|
+
saveAccounts(data);
|
|
382
|
+
|
|
383
|
+
updateAccountAuth(data.accounts[index]);
|
|
384
|
+
console.log(`[AccountManager] Active account token refreshed: ${account.email}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { success: true, message: `Token refreshed for: ${account.email}` };
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`[AccountManager] Token refresh failed for ${account.email}:`, error.message);
|
|
390
|
+
return { success: false, message: `Token refresh failed: ${error.message}` };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function importFromCodex() {
|
|
395
|
+
const codeAuthFile = join(homedir(), '.codex', 'auth.json');
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
if (!existsSync(codeAuthFile)) {
|
|
399
|
+
return { success: false, message: 'No Codex auth.json found' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const codexAuth = JSON.parse(readFileSync(codeAuthFile, 'utf8'));
|
|
403
|
+
|
|
404
|
+
if (!codexAuth.tokens?.access_token) {
|
|
405
|
+
return { success: false, message: 'No valid tokens in Codex auth.json' };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const info = extractAccountInfo(codexAuth.tokens.access_token);
|
|
409
|
+
|
|
410
|
+
const newAccount = {
|
|
411
|
+
email: info?.email || 'imported@unknown.com',
|
|
412
|
+
accountId: codexAuth.tokens.account_id,
|
|
413
|
+
planType: info?.planType || 'unknown',
|
|
414
|
+
accessToken: codexAuth.tokens.access_token,
|
|
415
|
+
refreshToken: codexAuth.tokens.refresh_token,
|
|
416
|
+
idToken: codexAuth.tokens.id_token,
|
|
417
|
+
expiresAt: info?.expiresAt,
|
|
418
|
+
addedAt: new Date().toISOString(),
|
|
419
|
+
lastUsed: new Date().toISOString(),
|
|
420
|
+
source: 'imported'
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const data = loadAccounts();
|
|
424
|
+
|
|
425
|
+
const existingIndex = data.accounts.findIndex(a => a.email === newAccount.email);
|
|
426
|
+
if (existingIndex >= 0) {
|
|
427
|
+
data.accounts[existingIndex] = newAccount;
|
|
428
|
+
} else {
|
|
429
|
+
data.accounts.push(newAccount);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!data.activeAccount) {
|
|
433
|
+
data.activeAccount = newAccount.email;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
saveAccounts(data);
|
|
437
|
+
updateAccountAuth(newAccount);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
message: `Imported account: ${newAccount.email} (${newAccount.planType})`
|
|
442
|
+
};
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return { success: false, message: `Import failed: ${error.message}` };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getStatus() {
|
|
449
|
+
const data = loadAccounts();
|
|
450
|
+
const accounts = data.accounts.map(a => {
|
|
451
|
+
const info = extractAccountInfo(a.accessToken);
|
|
452
|
+
return {
|
|
453
|
+
email: a.email,
|
|
454
|
+
planType: a.planType,
|
|
455
|
+
isActive: a.email === data.activeAccount,
|
|
456
|
+
quota: a.quota || null,
|
|
457
|
+
tokenExpired: info?.expiresAt ? info.expiresAt < Date.now() : false,
|
|
458
|
+
lastUsed: a.lastUsed
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
total: data.accounts.length,
|
|
464
|
+
active: data.activeAccount,
|
|
465
|
+
accounts
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function ensureAccountsPersist() {
|
|
470
|
+
const data = loadAccounts();
|
|
471
|
+
if (data.accounts.length > 0 && data.activeAccount) {
|
|
472
|
+
const active = data.accounts.find(a => a.email === data.activeAccount);
|
|
473
|
+
if (active) {
|
|
474
|
+
updateAccountAuth(active);
|
|
475
|
+
console.log(`[AccountManager] Restored active account: ${active.email}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export {
|
|
481
|
+
loadAccounts,
|
|
482
|
+
saveAccounts,
|
|
483
|
+
save,
|
|
484
|
+
getAccount,
|
|
485
|
+
getActiveAccount,
|
|
486
|
+
setActiveAccount,
|
|
487
|
+
removeAccount,
|
|
488
|
+
listAccounts,
|
|
489
|
+
refreshActiveAccount,
|
|
490
|
+
refreshAccountToken,
|
|
491
|
+
refreshAllAccounts,
|
|
492
|
+
importFromCodex,
|
|
493
|
+
getStatus,
|
|
494
|
+
updateAccountAuth,
|
|
495
|
+
ensureAccountsPersist,
|
|
496
|
+
updateAccountQuota,
|
|
497
|
+
getAccountQuota,
|
|
498
|
+
startAutoRefresh,
|
|
499
|
+
stopAutoRefresh,
|
|
500
|
+
isTokenExpiredOrExpiringSoon,
|
|
501
|
+
getCachedToken,
|
|
502
|
+
setCachedToken,
|
|
503
|
+
TOKEN_REFRESH_INTERVAL_MS,
|
|
504
|
+
ACCOUNTS_FILE,
|
|
505
|
+
CONFIG_DIR
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
export default {
|
|
509
|
+
getActiveAccount,
|
|
510
|
+
setActiveAccount,
|
|
511
|
+
removeAccount,
|
|
512
|
+
listAccounts,
|
|
513
|
+
refreshActiveAccount,
|
|
514
|
+
refreshAccountToken,
|
|
515
|
+
refreshAllAccounts,
|
|
516
|
+
importFromCodex,
|
|
517
|
+
getStatus,
|
|
518
|
+
ensureAccountsPersist,
|
|
519
|
+
updateAccountQuota,
|
|
520
|
+
getAccountQuota,
|
|
521
|
+
startAutoRefresh,
|
|
522
|
+
stopAutoRefresh,
|
|
523
|
+
isTokenExpiredOrExpiringSoon,
|
|
524
|
+
getCachedToken,
|
|
525
|
+
setCachedToken,
|
|
526
|
+
save,
|
|
527
|
+
getAccount
|
|
528
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
markRateLimited,
|
|
3
|
+
markInvalid,
|
|
4
|
+
clearInvalid,
|
|
5
|
+
isAllRateLimited,
|
|
6
|
+
getMinWaitTimeMs,
|
|
7
|
+
clearExpiredLimits
|
|
8
|
+
} from './rate-limits.js';
|
|
9
|
+
|
|
10
|
+
import { createStrategy, STRATEGIES } from './strategies/index.js';
|
|
11
|
+
|
|
12
|
+
export class AccountRotator {
|
|
13
|
+
constructor(accountManager, strategyName = 'sticky') {
|
|
14
|
+
this.accountManager = accountManager;
|
|
15
|
+
this.strategy = createStrategy(strategyName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
selectAccount(modelId, options = {}) {
|
|
19
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
20
|
+
return this.strategy.selectAccount(accounts, modelId, options);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
markRateLimited(email, resetMs, modelId) {
|
|
24
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
25
|
+
markRateLimited(accounts, email, resetMs, modelId);
|
|
26
|
+
this.accountManager.save();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
markInvalid(email, reason) {
|
|
30
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
31
|
+
markInvalid(accounts, email, reason);
|
|
32
|
+
this.accountManager.save();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clearInvalid(email) {
|
|
36
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
37
|
+
clearInvalid(accounts, email);
|
|
38
|
+
this.accountManager.save();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isAllRateLimited(modelId) {
|
|
42
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
43
|
+
return isAllRateLimited(accounts, modelId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getMinWaitTimeMs(modelId) {
|
|
47
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
48
|
+
return getMinWaitTimeMs(accounts, modelId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
notifySuccess(account, modelId) {
|
|
52
|
+
if (this.strategy.notifySuccess) {
|
|
53
|
+
this.strategy.notifySuccess(account, modelId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
notifyRateLimit(account, modelId) {
|
|
58
|
+
if (this.strategy.notifyRateLimit) {
|
|
59
|
+
this.strategy.notifyRateLimit(account, modelId);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
notifyFailure(account, modelId) {
|
|
64
|
+
if (this.strategy.notifyFailure) {
|
|
65
|
+
this.strategy.notifyFailure(account, modelId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clearExpiredLimits() {
|
|
70
|
+
const { accounts } = this.accountManager.listAccounts();
|
|
71
|
+
clearExpiredLimits(accounts);
|
|
72
|
+
this.accountManager.save();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getStrategyName() {
|
|
76
|
+
return this.strategy.name;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getStrategyLabel() {
|
|
80
|
+
return this.strategy.label;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
createStrategy,
|
|
86
|
+
STRATEGIES,
|
|
87
|
+
markRateLimited,
|
|
88
|
+
markInvalid,
|
|
89
|
+
clearInvalid,
|
|
90
|
+
isAllRateLimited,
|
|
91
|
+
getMinWaitTimeMs,
|
|
92
|
+
clearExpiredLimits
|
|
93
|
+
};
|