iflow-local-proxy 1.0.0

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.
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Anthropic Messages API <-> OpenAI Chat Completions API 格式转换适配器
3
+ * 支持: 文本消息、system prompt、tool_use / tool_result、图片(base64)、流式/非流式
4
+ */
5
+
6
+ import { randomBytes } from 'crypto';
7
+
8
+ // ========================================
9
+ // Anthropic 请求 → OpenAI 请求
10
+ // ========================================
11
+
12
+ export function anthropicToOpenAI(req) {
13
+ const openai = {
14
+ model: req.model,
15
+ max_tokens: req.max_tokens,
16
+ stream: req.stream || false,
17
+ messages: [],
18
+ };
19
+
20
+ // system prompt
21
+ if (req.system) {
22
+ if (typeof req.system === 'string') {
23
+ openai.messages.push({ role: 'system', content: req.system });
24
+ } else if (Array.isArray(req.system)) {
25
+ const text = req.system.filter(b => b.type === 'text').map(b => b.text).join('\n');
26
+ if (text) openai.messages.push({ role: 'system', content: text });
27
+ }
28
+ }
29
+
30
+ // messages
31
+ for (const msg of req.messages || []) {
32
+ if (msg.role === 'assistant') {
33
+ const converted = convertAssistantMsg(msg);
34
+ if (Array.isArray(converted)) {
35
+ openai.messages.push(...converted);
36
+ } else {
37
+ openai.messages.push(converted);
38
+ }
39
+ } else if (msg.role === 'user') {
40
+ const expanded = expandUserMsg(msg);
41
+ openai.messages.push(...expanded);
42
+ }
43
+ }
44
+
45
+ // optional params
46
+ if (req.temperature !== undefined) openai.temperature = req.temperature;
47
+ if (req.top_p !== undefined) openai.top_p = req.top_p;
48
+ if (req.stop_sequences) openai.stop = req.stop_sequences;
49
+
50
+ // tools
51
+ if (req.tools?.length) {
52
+ openai.tools = req.tools.map(t => ({
53
+ type: 'function',
54
+ function: {
55
+ name: t.name,
56
+ description: t.description || '',
57
+ parameters: t.input_schema || { type: 'object' },
58
+ },
59
+ }));
60
+ }
61
+
62
+ // tool_choice
63
+ if (req.tool_choice) {
64
+ const tc = req.tool_choice;
65
+ if (tc.type === 'auto') openai.tool_choice = 'auto';
66
+ else if (tc.type === 'any') openai.tool_choice = 'required';
67
+ else if (tc.type === 'tool') openai.tool_choice = { type: 'function', function: { name: tc.name } };
68
+ else if (tc.type === 'none') openai.tool_choice = 'none';
69
+ }
70
+
71
+ return openai;
72
+ }
73
+
74
+ function convertAssistantMsg(msg) {
75
+ if (typeof msg.content === 'string') {
76
+ return { role: 'assistant', content: msg.content };
77
+ }
78
+
79
+ const textParts = [];
80
+ const toolCalls = [];
81
+
82
+ for (const block of msg.content || []) {
83
+ if (block.type === 'text') {
84
+ textParts.push(block.text);
85
+ } else if (block.type === 'thinking') {
86
+ if (block.thinking) textParts.push(block.thinking);
87
+ } else if (block.type === 'tool_use') {
88
+ toolCalls.push({
89
+ id: block.id,
90
+ type: 'function',
91
+ function: {
92
+ name: block.name,
93
+ arguments: JSON.stringify(block.input),
94
+ },
95
+ });
96
+ }
97
+ }
98
+
99
+ const result = { role: 'assistant', content: textParts.join('\n') || null };
100
+ if (toolCalls.length) result.tool_calls = toolCalls;
101
+ return result;
102
+ }
103
+
104
+ function expandUserMsg(msg) {
105
+ if (typeof msg.content === 'string') {
106
+ return [{ role: 'user', content: msg.content }];
107
+ }
108
+
109
+ const results = [];
110
+ const contentParts = [];
111
+ let hasImage = false;
112
+
113
+ for (const block of msg.content || []) {
114
+ if (block.type === 'tool_result') {
115
+ results.push({
116
+ role: 'tool',
117
+ tool_call_id: block.tool_use_id,
118
+ content: extractTextContent(block.content),
119
+ });
120
+ } else if (block.type === 'text') {
121
+ contentParts.push({ type: 'text', text: block.text });
122
+ } else if (block.type === 'image') {
123
+ if (block.source?.type === 'base64') {
124
+ hasImage = true;
125
+ contentParts.push({
126
+ type: 'image_url',
127
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` },
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ if (contentParts.length) {
134
+ if (hasImage) {
135
+ results.push({ role: 'user', content: contentParts });
136
+ } else {
137
+ results.push({ role: 'user', content: contentParts.map(p => p.text).join('\n') });
138
+ }
139
+ }
140
+
141
+ return results.length ? results : [{ role: 'user', content: '' }];
142
+ }
143
+
144
+ function extractTextContent(content) {
145
+ if (!content) return '';
146
+ if (typeof content === 'string') return content;
147
+ if (Array.isArray(content)) {
148
+ return content.filter(b => b.type === 'text').map(b => b.text).join('\n');
149
+ }
150
+ return JSON.stringify(content);
151
+ }
152
+
153
+ // ========================================
154
+ // OpenAI 响应 → Anthropic 响应
155
+ // ========================================
156
+
157
+ export function openAIToAnthropic(resp, model) {
158
+ const content = [];
159
+
160
+ for (const choice of resp.choices || []) {
161
+ if (choice?.message?.content) {
162
+ content.push({ type: 'text', text: choice.message.content });
163
+ }
164
+ if (choice?.message?.tool_calls) {
165
+ for (const tc of choice.message.tool_calls) {
166
+ content.push({
167
+ type: 'tool_use',
168
+ id: tc.id || `toolu_${randomBytes(12).toString('hex')}`,
169
+ name: tc.function.name,
170
+ input: safeJsonParse(tc.function.arguments),
171
+ });
172
+ }
173
+ }
174
+ }
175
+
176
+ if (!content.length) content.push({ type: 'text', text: '' });
177
+
178
+ const firstChoice = resp.choices?.[0];
179
+ const cachedTokens = resp.usage?.prompt_tokens_details?.cached_tokens;
180
+ const usage = {
181
+ input_tokens: (resp.usage?.prompt_tokens || 0) - (cachedTokens || 0),
182
+ output_tokens: resp.usage?.completion_tokens || 0,
183
+ };
184
+ if (cachedTokens !== undefined) {
185
+ usage.cache_read_input_tokens = cachedTokens;
186
+ }
187
+
188
+ return {
189
+ id: `msg_${randomBytes(12).toString('hex')}`,
190
+ type: 'message',
191
+ role: 'assistant',
192
+ content,
193
+ model: model || resp.model,
194
+ stop_reason: mapStopReason(firstChoice?.finish_reason),
195
+ stop_sequence: null,
196
+ usage,
197
+ };
198
+ }
199
+
200
+ function mapStopReason(reason) {
201
+ switch (reason) {
202
+ case 'stop': return 'end_turn';
203
+ case 'length': return 'max_tokens';
204
+ case 'tool_calls': return 'tool_use';
205
+ case 'tool_call': return 'tool_use';
206
+ case 'content_filter': return 'end_turn';
207
+ default: return 'end_turn';
208
+ }
209
+ }
210
+
211
+ function safeJsonParse(str) {
212
+ try { return JSON.parse(str); } catch { return {}; }
213
+ }
214
+
215
+ // ========================================
216
+ // Anthropic 格式错误响应
217
+ // ========================================
218
+
219
+ export function anthropicError(status, message) {
220
+ const typeMap = {
221
+ 400: 'invalid_request_error',
222
+ 401: 'authentication_error',
223
+ 403: 'permission_error',
224
+ 404: 'not_found_error',
225
+ 429: 'rate_limit_error',
226
+ 500: 'api_error',
227
+ 529: 'overloaded_error',
228
+ };
229
+ return {
230
+ type: 'error',
231
+ error: {
232
+ type: typeMap[status] || 'api_error',
233
+ message,
234
+ },
235
+ };
236
+ }
237
+
238
+ // ========================================
239
+ // 流式适配器: OpenAI SSE → Anthropic SSE
240
+ // ========================================
241
+
242
+ export class AnthropicStreamAdapter {
243
+ constructor(res, model) {
244
+ this.res = res;
245
+ this.model = model;
246
+ this.msgId = `msg_${randomBytes(12).toString('hex')}`;
247
+
248
+ this.started = false;
249
+ this.contentBlockOpen = false;
250
+ this.finished = false;
251
+ this.blockIndex = 0;
252
+ this.inputTokens = 0;
253
+ this.outputTokens = 0;
254
+ this.cachedTokens = undefined;
255
+ this.lastFinishReason = null;
256
+
257
+ this.toolBlockMap = new Map();
258
+ this.textBlockOpen = false;
259
+ this.buffer = '';
260
+ }
261
+
262
+ _emit(event, data) {
263
+ this.res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
264
+ }
265
+
266
+ _ensureStarted() {
267
+ if (this.started) return;
268
+ this.started = true;
269
+ this._emit('message_start', {
270
+ type: 'message_start',
271
+ message: {
272
+ id: this.msgId, type: 'message', role: 'assistant',
273
+ content: [], model: this.model,
274
+ stop_reason: null, stop_sequence: null,
275
+ usage: { input_tokens: this.inputTokens, output_tokens: 0 },
276
+ },
277
+ });
278
+ this._emit('ping', { type: 'ping' });
279
+ }
280
+
281
+ _openTextBlock() {
282
+ if (this.textBlockOpen) return;
283
+ this._ensureStarted();
284
+ this.textBlockOpen = true;
285
+ this._emit('content_block_start', {
286
+ type: 'content_block_start',
287
+ index: this.blockIndex,
288
+ content_block: { type: 'text', text: '' },
289
+ });
290
+ }
291
+
292
+ _closeTextBlock() {
293
+ if (this.textBlockOpen) {
294
+ this._emit('content_block_stop', { type: 'content_block_stop', index: this.blockIndex });
295
+ this.blockIndex++;
296
+ this.textBlockOpen = false;
297
+ this.contentBlockOpen = false;
298
+ }
299
+ }
300
+
301
+ _closeToolBlock(openaiIdx) {
302
+ const info = this.toolBlockMap.get(openaiIdx);
303
+ if (info && info.open) {
304
+ this._emit('content_block_stop', { type: 'content_block_stop', index: info.anthropicBlockIndex });
305
+ info.open = false;
306
+ this.contentBlockOpen = false;
307
+ }
308
+ }
309
+
310
+ _closeAllOpenBlocks() {
311
+ this._closeTextBlock();
312
+ for (const [idx] of this.toolBlockMap) {
313
+ this._closeToolBlock(idx);
314
+ }
315
+ }
316
+
317
+ feed(rawData) {
318
+ this.buffer += rawData;
319
+ const lines = this.buffer.split('\n');
320
+ this.buffer = lines.pop() || '';
321
+
322
+ for (const line of lines) {
323
+ const trimmed = line.trim();
324
+ if (trimmed.startsWith('data:')) {
325
+ this._processDataLine(trimmed);
326
+ }
327
+ }
328
+ }
329
+
330
+ _processDataLine(line) {
331
+ let payload;
332
+ if (line.startsWith('data: ')) {
333
+ payload = line.slice(6).trim();
334
+ } else if (line.startsWith('data:')) {
335
+ payload = line.slice(5).trim();
336
+ } else {
337
+ return;
338
+ }
339
+ if (payload === '[DONE]') {
340
+ this._finalize();
341
+ return;
342
+ }
343
+
344
+ let chunk;
345
+ try { chunk = JSON.parse(payload); } catch { return; }
346
+
347
+ const choice = chunk.choices?.[0];
348
+ if (!choice) return;
349
+
350
+ const delta = choice.delta || {};
351
+ this._ensureStarted();
352
+
353
+ // usage
354
+ if (chunk.usage) {
355
+ this.inputTokens = chunk.usage.prompt_tokens || 0;
356
+ this.outputTokens = chunk.usage.completion_tokens || 0;
357
+ if (chunk.usage.prompt_tokens_details?.cached_tokens !== undefined) {
358
+ this.cachedTokens = chunk.usage.prompt_tokens_details.cached_tokens;
359
+ }
360
+ }
361
+
362
+ // text
363
+ if (delta.content) {
364
+ for (const [idx] of this.toolBlockMap) {
365
+ this._closeToolBlock(idx);
366
+ }
367
+ this._openTextBlock();
368
+ this._emit('content_block_delta', {
369
+ type: 'content_block_delta',
370
+ index: this.blockIndex,
371
+ delta: { type: 'text_delta', text: delta.content },
372
+ });
373
+ }
374
+
375
+ // tool_calls
376
+ if (delta.tool_calls) {
377
+ for (const tc of delta.tool_calls) {
378
+ const idx = tc.index ?? 0;
379
+ const existingInfo = this.toolBlockMap.get(idx);
380
+
381
+ if (tc.id && !existingInfo) {
382
+ this._closeAllOpenBlocks();
383
+ const bi = this.blockIndex;
384
+ this.toolBlockMap.set(idx, { anthropicBlockIndex: bi, open: true, id: tc.id });
385
+ this._emit('content_block_start', {
386
+ type: 'content_block_start',
387
+ index: bi,
388
+ content_block: { type: 'tool_use', id: tc.id, name: tc.function?.name || '', input: {} },
389
+ });
390
+ this.contentBlockOpen = true;
391
+ this.blockIndex++;
392
+
393
+ if (tc.function?.arguments) {
394
+ this._emit('content_block_delta', {
395
+ type: 'content_block_delta',
396
+ index: bi,
397
+ delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
398
+ });
399
+ }
400
+ } else if (tc.function?.arguments) {
401
+ const info = existingInfo || this.toolBlockMap.get(idx);
402
+ const bi = info ? info.anthropicBlockIndex : (this.blockIndex - 1);
403
+ this._emit('content_block_delta', {
404
+ type: 'content_block_delta',
405
+ index: bi,
406
+ delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
407
+ });
408
+ }
409
+ }
410
+ }
411
+
412
+ // finish_reason
413
+ if (choice.finish_reason && choice.finish_reason !== '') {
414
+ const reason = choice.finish_reason;
415
+ if (reason === 'tool_calls' || reason === 'tool_call') {
416
+ this.lastFinishReason = reason;
417
+ } else if (!this.lastFinishReason || (this.lastFinishReason !== 'tool_calls' && this.lastFinishReason !== 'tool_call')) {
418
+ this.lastFinishReason = reason;
419
+ }
420
+ }
421
+ }
422
+
423
+ flush() {
424
+ if (this.buffer.trim()) {
425
+ const trimmed = this.buffer.trim();
426
+ this.buffer = '';
427
+ if (trimmed.startsWith('data:')) {
428
+ this._processDataLine(trimmed);
429
+ }
430
+ }
431
+ this._finalize();
432
+ }
433
+
434
+ _finalize() {
435
+ if (this.finished) return;
436
+ this._ensureStarted();
437
+ this._closeAllOpenBlocks();
438
+ this.toolBlockMap.clear();
439
+
440
+ const stopReason = mapStopReason(this.lastFinishReason) || 'end_turn';
441
+ const usage = { output_tokens: this.outputTokens };
442
+ if (this.cachedTokens !== undefined) {
443
+ usage.input_tokens = this.inputTokens - this.cachedTokens;
444
+ usage.cache_read_input_tokens = this.cachedTokens;
445
+ } else {
446
+ usage.input_tokens = this.inputTokens;
447
+ }
448
+
449
+ this._emit('message_delta', {
450
+ type: 'message_delta',
451
+ delta: { stop_reason: stopReason, stop_sequence: null },
452
+ usage,
453
+ });
454
+ this._emit('message_stop', { type: 'message_stop' });
455
+ this.finished = true;
456
+ }
457
+ }
@@ -0,0 +1,239 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * iFlow 凭据管理 - 加载、保存、刷新
7
+ */
8
+
9
+ export function resolveAuthDir(authDir) {
10
+ if (authDir.startsWith('~')) {
11
+ return path.join(os.homedir(), authDir.slice(1));
12
+ }
13
+ return path.resolve(authDir);
14
+ }
15
+
16
+ /**
17
+ * 加载所有 iFlow 凭据文件(跳过 disabled)
18
+ */
19
+ export function loadCredentials(authDir) {
20
+ const dir = resolveAuthDir(authDir);
21
+
22
+ if (!fs.existsSync(dir)) {
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ return [];
25
+ }
26
+
27
+ const files = fs.readdirSync(dir).filter(f => f.startsWith('iflow-') && f.endsWith('.json'));
28
+ const credentials = [];
29
+
30
+ for (const filename of files) {
31
+ try {
32
+ const content = fs.readFileSync(path.join(dir, filename), 'utf-8');
33
+ const data = JSON.parse(content);
34
+ if (!data.disabled) {
35
+ credentials.push({ filename, data });
36
+ }
37
+ } catch (err) {
38
+ console.warn(`[warn] 加载凭据失败: ${filename}`, err.message);
39
+ }
40
+ }
41
+
42
+ return credentials;
43
+ }
44
+
45
+ /**
46
+ * 加载单个凭据(单账号模式,取第一个非 disabled 的)
47
+ */
48
+ export function loadCredential(authDir) {
49
+ const creds = loadCredentials(authDir);
50
+ return creds.length > 0 ? creds[0] : null;
51
+ }
52
+
53
+ /**
54
+ * 加载全部凭据(含 disabled)
55
+ */
56
+ export function loadAllCredentials(authDir) {
57
+ const dir = resolveAuthDir(authDir);
58
+ if (!fs.existsSync(dir)) return [];
59
+ const files = fs.readdirSync(dir).filter(f => f.startsWith('iflow-') && f.endsWith('.json'));
60
+ return files.map(filename => {
61
+ try {
62
+ const content = fs.readFileSync(path.join(dir, filename), 'utf-8');
63
+ return { filename, data: JSON.parse(content) };
64
+ } catch { return null; }
65
+ }).filter(Boolean);
66
+ }
67
+
68
+ /**
69
+ * 保存凭据
70
+ */
71
+ export function saveCredential(authDir, filename, data) {
72
+ const dir = resolveAuthDir(authDir);
73
+ if (!fs.existsSync(dir)) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+ fs.writeFileSync(path.join(dir, filename), JSON.stringify(data, null, 2), 'utf-8');
77
+ }
78
+
79
+ // ========================
80
+ // OAuth Token 刷新
81
+ // ========================
82
+
83
+ const OAUTH_TOKEN_URL = 'https://iflow.cn/oauth/token';
84
+ const CLIENT_ID = '10009311001';
85
+ const CLIENT_SECRET = '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW';
86
+ const USER_INFO_URL = 'https://iflow.cn/api/oauth/getUserInfo';
87
+
88
+ /**
89
+ * 使用 refresh_token 刷新 OAuth 凭据
90
+ */
91
+ export async function refreshOAuthToken(credential, authDir) {
92
+ const { data, filename } = credential;
93
+ if (!data.refresh_token) return false;
94
+
95
+ try {
96
+ const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
97
+
98
+ const tokenResp = await fetch(OAUTH_TOKEN_URL, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/x-www-form-urlencoded',
102
+ 'Authorization': `Basic ${basicAuth}`,
103
+ },
104
+ body: new URLSearchParams({
105
+ grant_type: 'refresh_token',
106
+ refresh_token: data.refresh_token,
107
+ }),
108
+ });
109
+
110
+ if (!tokenResp.ok) {
111
+ console.error(`[error] OAuth 刷新失败 [${filename}]: HTTP ${tokenResp.status}`);
112
+ return false;
113
+ }
114
+
115
+ const tokenData = await tokenResp.json();
116
+
117
+ if (!tokenData.access_token) {
118
+ console.error(`[error] OAuth 刷新返回空 access_token [${filename}]`);
119
+ return false;
120
+ }
121
+
122
+ // 获取用户信息验证
123
+ const userResp = await fetch(`${USER_INFO_URL}?accessToken=${encodeURIComponent(tokenData.access_token)}`, {
124
+ headers: { 'Accept': 'application/json' },
125
+ });
126
+
127
+ let apiKey = data.api_key;
128
+
129
+ if (userResp.ok) {
130
+ const userInfo = await userResp.json();
131
+ if (userInfo.success === false || userInfo.code === 'INVALID_TOKEN') {
132
+ console.error(`[error] 刷新获得的 access_token 无效 [${filename}]`);
133
+ data.refresh_failed_at = new Date().toISOString();
134
+ saveCredential(authDir, filename, data);
135
+ return false;
136
+ }
137
+ if (userInfo.data?.apiKey) {
138
+ apiKey = userInfo.data.apiKey;
139
+ }
140
+ if (data.email === 'unknown') {
141
+ const newEmail = (userInfo.data?.email || userInfo.data?.phone || '').trim();
142
+ if (newEmail) data.email = newEmail;
143
+ }
144
+ } else {
145
+ console.error(`[error] getUserInfo HTTP 失败 [${filename}]: ${userResp.status}`);
146
+ return false;
147
+ }
148
+
149
+ // 更新凭据
150
+ data.access_token = tokenData.access_token;
151
+ if (tokenData.refresh_token) data.refresh_token = tokenData.refresh_token;
152
+ data.api_key = apiKey;
153
+ data.last_refresh = new Date().toISOString();
154
+ delete data.refresh_failed_at;
155
+ if (tokenData.expires_in) {
156
+ data.expired = new Date(Date.now() + tokenData.expires_in * 1000).toISOString();
157
+ }
158
+
159
+ saveCredential(authDir, filename, data);
160
+ console.log(`[info] OAuth 刷新成功: ${filename} (到期: ${data.expired})`);
161
+ return true;
162
+ } catch (err) {
163
+ console.error(`[error] OAuth 刷新异常 [${filename}]:`, err.message);
164
+ return false;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * 使用 Cookie 刷新 API Key
170
+ */
171
+ export async function refreshCookieApiKey(credential, authDir) {
172
+ const { data, filename } = credential;
173
+ if (!data.cookie) return false;
174
+
175
+ try {
176
+ const resp = await fetch('https://platform.iflow.cn/api/openapi/apikey', {
177
+ method: 'POST',
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ 'Cookie': data.cookie,
181
+ },
182
+ body: JSON.stringify({ name: data.email || 'iflow-proxy' }),
183
+ });
184
+
185
+ if (!resp.ok) {
186
+ console.error(`[error] Cookie 刷新失败 [${filename}]: ${resp.status}`);
187
+ return false;
188
+ }
189
+
190
+ const result = await resp.json();
191
+ if (result.data?.apiKey) {
192
+ data.api_key = result.data.apiKey;
193
+ data.last_refresh = new Date().toISOString();
194
+ data.expired = new Date(Date.now() + 48 * 3600 * 1000).toISOString();
195
+ saveCredential(authDir, filename, data);
196
+ console.log(`[info] Cookie 刷新成功: ${filename}`);
197
+ return true;
198
+ }
199
+
200
+ return false;
201
+ } catch (err) {
202
+ console.error(`[error] Cookie 刷新异常 [${filename}]:`, err.message);
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * 刷新即将过期的凭据
209
+ */
210
+ export async function refreshExpiring(credentials, authDir) {
211
+ const leadTime = 47 * 3600 * 1000;
212
+ const failedAccounts = [];
213
+
214
+ for (const cred of credentials) {
215
+ if (cred.data.disabled) continue;
216
+
217
+ const expired = new Date(cred.data.expired).getTime();
218
+ if (isNaN(expired)) continue;
219
+
220
+ if (Date.now() + leadTime > expired) {
221
+ const hoursLeft = Math.round((expired - Date.now()) / 3600000);
222
+ console.log(`[info] 凭据将在 ${hoursLeft}h 后过期,正在刷新: ${cred.filename}`);
223
+
224
+ let success = false;
225
+ if (cred.data.cookie) {
226
+ success = await refreshCookieApiKey(cred, authDir);
227
+ } else if (cred.data.refresh_token) {
228
+ success = await refreshOAuthToken(cred, authDir);
229
+ }
230
+
231
+ if (!success) {
232
+ failedAccounts.push({ filename: cred.filename, email: cred.data.email });
233
+ console.error(`[ALERT] 凭据刷新失败: ${cred.filename} (${cred.data.email})`);
234
+ }
235
+ }
236
+ }
237
+
238
+ return failedAccounts;
239
+ }