Moonfanz commited on
Commit
6d7b4f0
·
verified ·
1 Parent(s): c6981d9

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +22 -0
  2. index.js +903 -0
  3. package.json +18 -0
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package.json and package-lock.json
6
+ COPY package.json package-lock.json* ./
7
+
8
+ # Install dependencies
9
+ RUN npm ci
10
+
11
+ # Copy source code
12
+ COPY . .
13
+
14
+ # Expose the port
15
+ EXPOSE 7860
16
+
17
+ # Set environment variables (these can be overridden at runtime)
18
+ ENV PORT=7860
19
+ ENV TZ=Asia/Shanghai
20
+
21
+ # Start the application
22
+ CMD ["node", "server.js"]
index.js ADDED
@@ -0,0 +1,903 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // server.js
2
+ const express = require('express');
3
+ const axios = require('axios');
4
+ const crypto = require('crypto');
5
+ const { format } = require('date-fns');
6
+ const stream = require('stream');
7
+ const { promisify } = require('util');
8
+ const Scheduler = require('node-schedule');
9
+
10
+ const app = express();
11
+ app.use(express.json());
12
+
13
+ // Environment configuration
14
+ process.env.TZ = 'Asia/Shanghai';
15
+ const MAX_RETRIES = parseInt(process.env.MaxRetries || '3');
16
+ const MAX_REQUESTS = parseInt(process.env.MaxRequests || '2');
17
+ const LIMIT_WINDOW = parseInt(process.env.LimitWindow || '60');
18
+ const RETRY_DELAY = 1;
19
+ const MAX_RETRY_DELAY = 16;
20
+
21
+ // Initialize logger
22
+ const logger = {
23
+ info: (msg) => console.log(msg),
24
+ warning: (msg) => console.warn(msg),
25
+ error: (msg) => console.error(msg),
26
+ debug: (msg) => console.debug(msg)
27
+ };
28
+
29
+ // Request tracking for rate limiting
30
+ const requestCounts = {};
31
+ const apiKeyBlacklist = new Set();
32
+ const apiKeyBlacklistDuration = 60; // seconds
33
+
34
+ // Safety settings
35
+ const safetySettings = [
36
+ {
37
+ "category": "HARM_CATEGORY_HARASSMENT",
38
+ "threshold": "BLOCK_NONE"
39
+ },
40
+ {
41
+ "category": "HARM_CATEGORY_HATE_SPEECH",
42
+ "threshold": "BLOCK_NONE"
43
+ },
44
+ {
45
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
46
+ "threshold": "BLOCK_NONE"
47
+ },
48
+ {
49
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
50
+ "threshold": "BLOCK_NONE"
51
+ },
52
+ {
53
+ "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
54
+ "threshold": 'BLOCK_NONE'
55
+ }
56
+ ];
57
+
58
+ const safetySettingsG2 = [
59
+ {
60
+ "category": "HARM_CATEGORY_HARASSMENT",
61
+ "threshold": "OFF"
62
+ },
63
+ {
64
+ "category": "HARM_CATEGORY_HATE_SPEECH",
65
+ "threshold": "OFF"
66
+ },
67
+ {
68
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
69
+ "threshold": "OFF"
70
+ },
71
+ {
72
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
73
+ "threshold": "OFF"
74
+ },
75
+ {
76
+ "category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
77
+ "threshold": 'OFF'
78
+ }
79
+ ];
80
+
81
+ // Classes
82
+ class ResponseWrapper {
83
+ constructor(data) {
84
+ this._data = data;
85
+ this._text = this._extractText();
86
+ this._finishReason = this._extractFinishReason();
87
+ this._promptTokenCount = this._extractPromptTokenCount();
88
+ this._candidatesTokenCount = this._extractCandidatesTokenCount();
89
+ this._totalTokenCount = this._extractTotalTokenCount();
90
+ this._thoughts = this._extractThoughts();
91
+ this._jsonDumps = JSON.stringify(this._data, null, 4);
92
+ }
93
+
94
+ _extractThoughts() {
95
+ try {
96
+ for (const part of this._data.candidates[0].content.parts) {
97
+ if ('thought' in part) {
98
+ return part.text;
99
+ }
100
+ }
101
+ return "";
102
+ } catch (error) {
103
+ return "";
104
+ }
105
+ }
106
+
107
+ _extractText() {
108
+ try {
109
+ for (const part of this._data.candidates[0].content.parts) {
110
+ if (!('thought' in part)) {
111
+ return part.text;
112
+ }
113
+ }
114
+ return "";
115
+ } catch (error) {
116
+ return "";
117
+ }
118
+ }
119
+
120
+ _extractFinishReason() {
121
+ try {
122
+ return this._data.candidates[0].finishReason;
123
+ } catch (error) {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ _extractPromptTokenCount() {
129
+ try {
130
+ return this._data.usageMetadata.promptTokenCount;
131
+ } catch (error) {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ _extractCandidatesTokenCount() {
137
+ try {
138
+ return this._data.usageMetadata.candidatesTokenCount;
139
+ } catch (error) {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ _extractTotalTokenCount() {
145
+ try {
146
+ return this._data.usageMetadata.totalTokenCount;
147
+ } catch (error) {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ get text() {
153
+ return this._text;
154
+ }
155
+
156
+ get finishReason() {
157
+ return this._finishReason;
158
+ }
159
+
160
+ get promptTokenCount() {
161
+ return this._promptTokenCount;
162
+ }
163
+
164
+ get candidatesTokenCount() {
165
+ return this._candidatesTokenCount;
166
+ }
167
+
168
+ get totalTokenCount() {
169
+ return this._totalTokenCount;
170
+ }
171
+
172
+ get thoughts() {
173
+ return this._thoughts;
174
+ }
175
+
176
+ get jsonDumps() {
177
+ return this._jsonDumps;
178
+ }
179
+ }
180
+
181
+ class APIKeyManager {
182
+ constructor() {
183
+ this.apiKeys = (process.env.KeyArray || '').match(/AIzaSy[a-zA-Z0-9_-]{33}/g) || [];
184
+ this.currentIndex = Math.floor(Math.random() * this.apiKeys.length);
185
+ }
186
+
187
+ getAvailableKey() {
188
+ const numKeys = this.apiKeys.length;
189
+ for (let i = 0; i < numKeys; i++) {
190
+ if (this.currentIndex >= numKeys) {
191
+ this.currentIndex = 0;
192
+ }
193
+ const currentKey = this.apiKeys[this.currentIndex];
194
+ this.currentIndex++;
195
+
196
+ if (!apiKeyBlacklist.has(currentKey)) {
197
+ return currentKey;
198
+ }
199
+ }
200
+
201
+ logger.error("所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试");
202
+ return null;
203
+ }
204
+
205
+ showAllKeys() {
206
+ logger.info(`当前可用API key个数: ${this.apiKeys.length}`);
207
+ this.apiKeys.forEach((apiKey, i) => {
208
+ logger.info(`API Key${i}: ${apiKey.substring(0, 8)}...${apiKey.slice(-3)}`);
209
+ });
210
+ }
211
+
212
+ blacklistKey(key) {
213
+ logger.warning(`${key.substring(0, 8)} → 暂时禁用 ${apiKeyBlacklistDuration} 秒`);
214
+ apiKeyBlacklist.add(key);
215
+
216
+ setTimeout(() => {
217
+ apiKeyBlacklist.delete(key);
218
+ }, apiKeyBlacklistDuration * 1000);
219
+ }
220
+ }
221
+
222
+ const keyManager = new APIKeyManager();
223
+ keyManager.showAllKeys();
224
+ let currentApiKey = keyManager.getAvailableKey();
225
+
226
+ function switchApiKey() {
227
+ const key = keyManager.getAvailableKey();
228
+ if (key) {
229
+ currentApiKey = key;
230
+ logger.info(`API key 替换为 → ${currentApiKey.substring(0, 8)}...${currentApiKey.slice(-3)}`);
231
+ } else {
232
+ logger.error("API key 替换失败,所有API key都已耗尽或被暂时禁用,请重新配置或稍后重试");
233
+ }
234
+ }
235
+
236
+ logger.info(`当前 API key: ${currentApiKey.substring(0, 8)}...${currentApiKey.slice(-3)}`);
237
+
238
+ // Gemini models
239
+ const GEMINI_MODELS = [
240
+ { "id": "text-embedding-004" },
241
+ { "id": "gemini-1.5-flash-8b-latest" },
242
+ { "id": "gemini-1.5-flash-8b-exp-0924" },
243
+ { "id": "gemini-1.5-flash-latest" },
244
+ { "id": "gemini-1.5-flash-exp-0827" },
245
+ { "id": "gemini-1.5-pro-latest" },
246
+ { "id": "gemini-1.5-pro-exp-0827" },
247
+ { "id": "learnlm-1.5-pro-experimental" },
248
+ { "id": "gemini-exp-1114" },
249
+ { "id": "gemini-exp-1121" },
250
+ { "id": "gemini-exp-1206" },
251
+ { "id": "gemini-2.0-flash-exp" },
252
+ { "id": "gemini-2.0-flash-thinking-exp-1219" },
253
+ { "id": "gemini-2.0-flash-thinking-exp-01-21" },
254
+ { "id": "gemini-2.0-flash" },
255
+ { "id": "gemini-2.0-pro-exp-02-05" }
256
+ ];
257
+
258
+ // Authentication and Message Processing Functions
259
+ function authenticateRequest(req) {
260
+ const authHeader = req.headers.authorization;
261
+
262
+ if (!authHeader) {
263
+ return [false, { error: '缺少Authorization请求头' }, 401];
264
+ }
265
+
266
+ try {
267
+ const [authType, passWord] = authHeader.split(' ', 2);
268
+ if (authType.toLowerCase() !== 'bearer') {
269
+ return [false, { error: 'Authorization类型必须为Bearer' }, 401];
270
+ }
271
+
272
+ if (passWord !== process.env.password) {
273
+ return [false, { error: '未授权' }, 401];
274
+ }
275
+
276
+ return [true, null, null];
277
+ } catch (error) {
278
+ return [false, { error: 'Authorization请求头格式错误' }, 401];
279
+ }
280
+ }
281
+
282
+ function processMessagesForGemini(messages, useSystemPrompt = false) {
283
+ const geminiHistory = [];
284
+ const errors = [];
285
+ let systemInstructionText = "";
286
+ let isSystemPhase = useSystemPrompt;
287
+
288
+ for (const message of messages) {
289
+ const role = message.role;
290
+ const content = message.content;
291
+
292
+ if (typeof content === 'string') {
293
+ if (isSystemPhase && role === 'system') {
294
+ systemInstructionText = systemInstructionText
295
+ ? `${systemInstructionText}\n${content}`
296
+ : content;
297
+ } else {
298
+ isSystemPhase = false;
299
+
300
+ let roleToUse;
301
+ if (role === 'user' || role === 'system') {
302
+ roleToUse = 'user';
303
+ } else if (role === 'assistant') {
304
+ roleToUse = 'model';
305
+ } else {
306
+ errors.push(`Invalid role: ${role}`);
307
+ continue;
308
+ }
309
+
310
+ if (geminiHistory.length > 0 && geminiHistory[geminiHistory.length - 1].role === roleToUse) {
311
+ geminiHistory[geminiHistory.length - 1].parts.push({ text: content });
312
+ } else {
313
+ geminiHistory.push({ role: roleToUse, parts: [{ text: content }] });
314
+ }
315
+ }
316
+ } else if (Array.isArray(content)) {
317
+ const parts = [];
318
+ for (const item of content) {
319
+ if (item.type === 'text') {
320
+ parts.push({ text: item.text });
321
+ } else if (item.type === 'image_url') {
322
+ const imageData = item.image_url?.url || '';
323
+ if (imageData.startsWith('data:image/')) {
324
+ try {
325
+ const mimeType = imageData.split(';')[0].split(':')[1];
326
+ const base64Data = imageData.split(',')[1];
327
+ parts.push({
328
+ inline_data: {
329
+ mime_type: mimeType,
330
+ data: base64Data
331
+ }
332
+ });
333
+ } catch (error) {
334
+ errors.push(`Invalid data URI for image: ${imageData}`);
335
+ }
336
+ } else {
337
+ errors.push(`Invalid image URL format for item: ${JSON.stringify(item)}`);
338
+ }
339
+ } else if (item.type === 'file_url') {
340
+ const fileData = item.file_url?.url || '';
341
+ if (fileData.startsWith('data:')) {
342
+ try {
343
+ const mimeType = fileData.split(';')[0].split(':')[1];
344
+ const base64Data = fileData.split(',')[1];
345
+ parts.push({
346
+ inline_data: {
347
+ mime_type: mimeType,
348
+ data: base64Data
349
+ }
350
+ });
351
+ } catch (error) {
352
+ errors.push(`Invalid data URI for file: ${fileData}`);
353
+ }
354
+ } else {
355
+ errors.push(`Invalid file URL format for item: ${JSON.stringify(item)}`);
356
+ }
357
+ }
358
+ }
359
+
360
+ if (parts.length > 0) {
361
+ let roleToUse;
362
+ if (role === 'user' || role === 'system') {
363
+ roleToUse = 'user';
364
+ } else if (role === 'assistant') {
365
+ roleToUse = 'model';
366
+ } else {
367
+ errors.push(`Invalid role: ${role}`);
368
+ continue;
369
+ }
370
+
371
+ if (geminiHistory.length > 0 && geminiHistory[geminiHistory.length - 1].role === roleToUse) {
372
+ geminiHistory[geminiHistory.length - 1].parts.push(...parts);
373
+ } else {
374
+ geminiHistory.push({ role: roleToUse, parts });
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ if (errors.length > 0) {
381
+ return [geminiHistory, { parts: [{ text: systemInstructionText }] }, { error: errors }];
382
+ } else {
383
+ return [geminiHistory, { parts: [{ text: systemInstructionText }] }, null];
384
+ }
385
+ }
386
+
387
+ // Rate limiting and API error handling
388
+ function isWithinRateLimit(apiKey) {
389
+ const now = new Date();
390
+ if (!requestCounts[apiKey]) {
391
+ requestCounts[apiKey] = [];
392
+ }
393
+
394
+ // Remove expired requests
395
+ while (
396
+ requestCounts[apiKey].length > 0 &&
397
+ (now - requestCounts[apiKey][0]) > LIMIT_WINDOW * 1000
398
+ ) {
399
+ requestCounts[apiKey].shift();
400
+ }
401
+
402
+ if (requestCounts[apiKey].length >= MAX_REQUESTS) {
403
+ const earliestRequestTime = requestCounts[apiKey][0];
404
+ const waitTime = (earliestRequestTime + LIMIT_WINDOW * 1000 - now) / 1000;
405
+ return [false, waitTime];
406
+ } else {
407
+ return [true, 0];
408
+ }
409
+ }
410
+
411
+ function incrementRequestCount(apiKey) {
412
+ const now = new Date();
413
+ if (!requestCounts[apiKey]) {
414
+ requestCounts[apiKey] = [];
415
+ }
416
+ requestCounts[apiKey].push(now);
417
+ }
418
+
419
+ async function handleApiError(error, attempt, currentApiKey) {
420
+ if (attempt > MAX_RETRIES) {
421
+ logger.error(`${MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入`);
422
+ return [0, {
423
+ error: {
424
+ message: `${MAX_RETRIES} 次尝试后仍然失败,请修改预设或输入`,
425
+ type: 'max_retries_exceeded'
426
+ }
427
+ }];
428
+ }
429
+
430
+ if (error.response) {
431
+ const statusCode = error.response.status;
432
+
433
+ if (statusCode === 400) {
434
+ try {
435
+ const errorData = error.response.data;
436
+ if (errorData.error) {
437
+ if (errorData.error.code === "invalid_argument") {
438
+ logger.error(`${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → 无效,可能已过期或被删除`);
439
+ keyManager.blacklistKey(currentApiKey);
440
+ switchApiKey();
441
+ return [0, null];
442
+ }
443
+ const errorMessage = errorData.error.message || 'Bad Request';
444
+ const errorType = errorData.error.type || 'invalid_request_error';
445
+ logger.warning(`400 错误请求: ${errorMessage}`);
446
+ return [2, { error: { message: errorMessage, type: errorType } }];
447
+ }
448
+ } catch (parseError) {
449
+ logger.warning("400 错误请求:响应不是有效的JSON格式");
450
+ return [2, { error: { message: '', type: 'invalid_request_error' } }];
451
+ }
452
+ } else if (statusCode === 429) {
453
+ logger.warning(
454
+ `${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → 429 官方资源耗尽 → 立即重试...`
455
+ );
456
+ keyManager.blacklistKey(currentApiKey);
457
+ switchApiKey();
458
+ return [0, null];
459
+ } else if (statusCode === 403) {
460
+ logger.error(
461
+ `${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → 403 权限被拒绝,该 API KEY 可能已经被官方封禁`
462
+ );
463
+ keyManager.blacklistKey(currentApiKey);
464
+ switchApiKey();
465
+ return [0, null];
466
+ } else if (statusCode === 500) {
467
+ logger.warning(
468
+ `${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → 500 服务器内部错误 → 立即重试...`
469
+ );
470
+ switchApiKey();
471
+ return [0, null];
472
+ } else if (statusCode === 503) {
473
+ logger.warning(
474
+ `${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → 503 服务不可用 → 立即重试...`
475
+ );
476
+ switchApiKey();
477
+ return [0, null];
478
+ } else {
479
+ logger.warning(
480
+ `${currentApiKey.substring(0, 8)} ... ${currentApiKey.slice(-3)} → ${statusCode} 未知错误/模型不可用 → 不重试...`
481
+ );
482
+ switchApiKey();
483
+ return [2, null];
484
+ }
485
+ } else if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
486
+ const delay = Math.min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY);
487
+ logger.warning(`连接错误 → 立即重试...`);
488
+ await new Promise(resolve => setTimeout(resolve, delay * 1000));
489
+ return [0, null];
490
+ } else if (error.code === 'ETIMEDOUT') {
491
+ const delay = Math.min(RETRY_DELAY * (2 ** attempt), MAX_RETRY_DELAY);
492
+ logger.warning(`请求超时 → 立即重试...`);
493
+ await new Promise(resolve => setTimeout(resolve, delay * 1000));
494
+ return [0, null];
495
+ } else {
496
+ logger.error(`发生未知错误: ${error.message || error}`);
497
+ return [0, {
498
+ error: {
499
+ message: `发生未知错误: ${error.message || error}`,
500
+ type: 'unknown_error'
501
+ }
502
+ }];
503
+ }
504
+ }
505
+
506
+ // Route Handlers
507
+ app.get('/', (req, res) => {
508
+ const mainContent = " gemini-rProxy v2.3.5 2025-01-25";
509
+ const htmlTemplate = `
510
+ <!DOCTYPE html>
511
+ <html>
512
+ <head>
513
+ <meta charset="utf-8">
514
+ <script>
515
+ function copyToClipboard(text) {
516
+ var textarea = document.createElement("textarea");
517
+ textarea.textContent = text;
518
+ textarea.style.position = "fixed";
519
+ document.body.appendChild(textarea);
520
+ textarea.select();
521
+ try {
522
+ return document.execCommand("copy");
523
+ } catch (ex) {
524
+ console.warn("Copy to clipboard failed.", ex);
525
+ return false;
526
+ } finally {
527
+ document.body.removeChild(textarea);
528
+ }
529
+ }
530
+ function copyLink(event) {
531
+ event.preventDefault();
532
+ const url = new URL(window.location.href);
533
+ const link = url.protocol + '//' + url.host + '/hf/v1';
534
+ copyToClipboard(link);
535
+ alert('链接已复制: ' + link);
536
+ }
537
+ </script>
538
+ </head>
539
+ <body>
540
+ ${mainContent}<br/><br/>完全开源、免费且禁止商用<br/><br/>点击复制反向代理: <a href="v1" onclick="copyLink(event)">Copy Link</a><br/>聊天来源选择"自定义(兼容 OpenAI)"<br/>将复制的网址填入到自定义端点<br/>将设置password填入自定义API秘钥<br/><br/><br/>
541
+ </body>
542
+ </html>
543
+ `;
544
+ res.send(htmlTemplate);
545
+ });
546
+
547
+ app.post('/hf/v1/chat/completions', async (req, res) => {
548
+ const [isAuthenticated, authError, statusCode] = authenticateRequest(req);
549
+ if (!isAuthenticated) {
550
+ return res.status(statusCode || 401).json(authError || { error: '未授权' });
551
+ }
552
+
553
+ const requestData = req.body;
554
+ const messages = requestData.messages || [];
555
+ const model = requestData.model || 'gemini-2.0-flash-exp';
556
+ const temperature = requestData.temperature || 1;
557
+ const maxTokens = requestData.max_tokens || 8192;
558
+ const showThoughts = requestData.show_thoughts || false;
559
+ const stream = requestData.stream || false;
560
+ const useSystemPrompt = requestData.use_system_prompt || false;
561
+ const hint = stream ? "流式" : "非流";
562
+
563
+ logger.info(`\n${model} [${hint}] → ${currentApiKey.substring(0, 8)}...${currentApiKey.slice(-3)}`);
564
+
565
+ const isThinking = model.includes('thinking');
566
+ const apiVersion = isThinking ? 'v1alpha' : 'v1beta';
567
+ const responseType = stream ? 'streamGenerateContent' : 'generateContent';
568
+ const isSSE = stream ? '&alt=sse' : '';
569
+
570
+ const [contents, systemInstruction, errorResponse] = processMessagesForGemini(messages, useSystemPrompt);
571
+
572
+ if (errorResponse) {
573
+ logger.error(`处理输入消息时出错↙\n ${JSON.stringify(errorResponse)}`);
574
+ return res.status(400).json(errorResponse);
575
+ }
576
+
577
+ async function doRequest(currentApiKey, attempt) {
578
+ const [isOk, timeRemaining] = isWithinRateLimit(currentApiKey);
579
+ if (!isOk) {
580
+ logger.warning(`暂时超过限额,该API key将在 ${timeRemaining} 秒后启用...`);
581
+ switchApiKey();
582
+ return [0, null];
583
+ }
584
+
585
+ incrementRequestCount(currentApiKey);
586
+
587
+ const url = `https://generativelanguage.googleapis.com/${apiVersion}/models/${model}:${responseType}?key=${currentApiKey}${isSSE}`;
588
+ const headers = {
589
+ "Content-Type": "application/json",
590
+ };
591
+
592
+ const data = {
593
+ contents,
594
+ generationConfig: {
595
+ temperature,
596
+ maxOutputTokens: maxTokens,
597
+ },
598
+ safetySettings: model.includes('gemini-2.0-flash-exp') ? safetySettingsG2 : safetySettings,
599
+ };
600
+
601
+ if (systemInstruction.parts[0].text) {
602
+ data.system_instruction = systemInstruction;
603
+ }
604
+
605
+ try {
606
+ const response = await axios({
607
+ method: 'post',
608
+ url,
609
+ headers,
610
+ data,
611
+ responseType: stream ? 'stream' : 'json'
612
+ });
613
+
614
+ if (stream) {
615
+ return [1, response];
616
+ } else {
617
+ return [1, new ResponseWrapper(response.data)];
618
+ }
619
+ } catch (error) {
620
+ return handleApiError(error, attempt, currentApiKey);
621
+ }
622
+ }
623
+
624
+ function generateStream(response) {
625
+ logger.info(`流式开始 →`);
626
+ let buffer = '';
627
+
628
+ response.data.on('data', (chunk) => {
629
+ const lines = chunk.toString().split('\n');
630
+
631
+ for (const line of lines) {
632
+ if (!line.trim()) continue;
633
+
634
+ try {
635
+ let data = line;
636
+ if (data.startsWith('data: ')) {
637
+ data = data.substring(6);
638
+ }
639
+
640
+ buffer += data;
641
+
642
+ try {
643
+ const jsonData = JSON.parse(buffer);
644
+ buffer = '';
645
+
646
+ if (jsonData.candidates && jsonData.candidates.length > 0) {
647
+ const candidate = jsonData.candidates[0];
648
+ if (candidate.content) {
649
+ const content = candidate.content;
650
+ if (content.parts && content.parts.length > 0) {
651
+ let parts = content.parts;
652
+ if (isThinking && !showThoughts) {
653
+ parts = parts.filter(part => !part.thought);
654
+ }
655
+
656
+ if (parts.length > 0) {
657
+ const text = parts[0].text || '';
658
+ const finishReason = candidate.finishReason;
659
+
660
+ if (text) {
661
+ const data = {
662
+ choices: [{
663
+ delta: {
664
+ content: text
665
+ },
666
+ finish_reason: finishReason,
667
+ index: 0
668
+ }],
669
+ object: 'chat.completion.chunk'
670
+ };
671
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
672
+ }
673
+ }
674
+ }
675
+ }
676
+
677
+ if (candidate.finishReason && candidate.finishReason !== "STOP") {
678
+ const errorMessage = {
679
+ error: {
680
+ code: "content_filter",
681
+ message: `模型的响应因违反内容政策而被标记:${candidate.finishReason}`,
682
+ status: candidate.finishReason,
683
+ details: []
684
+ }
685
+ };
686
+ logger.warning(`模型的响应因违反内容政策而被标记: ${candidate.finishReason}`);
687
+ res.write(`data: ${JSON.stringify(errorMessage)}\n\n`);
688
+ break;
689
+ }
690
+
691
+ if (candidate.safetyRatings) {
692
+ let shouldBreak = false;
693
+ for (const rating of candidate.safetyRatings) {
694
+ if (rating.probability === 'HIGH') {
695
+ const errorMessage = {
696
+ error: {
697
+ code: "content_filter",
698
+ message: `模型的响应因高概率被标记为 ${rating.category}`,
699
+ status: "SAFETY_RATING_HIGH",
700
+ details: [rating]
701
+ }
702
+ };
703
+ logger.warning(`模型的响应因高概率被标记为 ${rating.category}`);
704
+ res.write(`data: ${JSON.stringify(errorMessage)}\n\n`);
705
+ shouldBreak = true;
706
+ break;
707
+ }
708
+ }
709
+ if (shouldBreak) break;
710
+ }
711
+ }
712
+ } catch (jsonError) {
713
+ // If JSON parsing fails, just continue collecting data
714
+ continue;
715
+ }
716
+ } catch (e) {
717
+ logger.error(`流式处理期间发生错误: ${e}, 原始数据行↙\n${line}`);
718
+ res.write(`data: ${JSON.stringify({ error: String(e) })}\n\n`);
719
+ }
720
+ }
721
+ });
722
+
723
+ response.data.on('end', () => {
724
+ res.write(`data: ${JSON.stringify({
725
+ choices: [{ delta: {}, finish_reason: 'stop', index: 0 }]
726
+ })}\n\n`);
727
+ logger.info(`流式结束 ←`);
728
+ logger.info(`200!`);
729
+ res.end();
730
+ });
731
+
732
+ response.data.on('error', (err) => {
733
+ logger.error(`流式处理错误↙\n${err}`);
734
+ res.write(`data: ${JSON.stringify({ error: String(err) })}\n\n`);
735
+ res.end();
736
+ });
737
+ }
738
+
739
+ // Main request handler logic
740
+ let success = 0;
741
+ let response = null;
742
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
743
+ logger.info(`第 ${attempt}/${MAX_RETRIES} 次尝试 ...`);
744
+ [success, response] = await doRequest(currentApiKey, attempt);
745
+
746
+ if (success === 0) {
747
+ continue;
748
+ } else if (success === 1 && response === null) {
749
+ continue;
750
+ } else if (success === 1 && stream) {
751
+ res.setHeader('Content-Type', 'text/event-stream');
752
+ res.setHeader('Cache-Control', 'no-cache');
753
+ res.setHeader('Connection', 'keep-alive');
754
+
755
+ generateStream(response);
756
+ return;
757
+ } else if (success === 1 && response instanceof ResponseWrapper) {
758
+ try {
759
+ const textContent = response.text;
760
+ const promptTokens = response.promptTokenCount;
761
+ const completionTokens = response.candidatesTokenCount;
762
+ const totalTokens = response.totalTokenCount;
763
+ const finishReason = response.finishReason;
764
+
765
+ if (textContent === '') {
766
+ let errorMessage = null;
767
+ if (response._data && response._data.error) {
768
+ errorMessage = response._data.error.message;
769
+ }
770
+ if (errorMessage) {
771
+ logger.error(`生成内容失败,API 返回错误: ${errorMessage}`);
772
+ } else {
773
+ logger.error(`生成内容失败: text_content 为空`);
774
+ }
775
+ continue;
776
+ }
777
+
778
+ let finalContent = textContent;
779
+ if (isThinking && showThoughts) {
780
+ finalContent = response.thoughts + '\n' + textContent;
781
+ }
782
+
783
+ const responseData = {
784
+ id: 'chatcmpl-xxxxxxxxxxxx',
785
+ object: 'chat.completion',
786
+ created: Math.floor(Date.now() / 1000),
787
+ model: model,
788
+ choices: [{
789
+ index: 0,
790
+ message: {
791
+ role: 'assistant',
792
+ content: finalContent
793
+ },
794
+ finish_reason: finishReason
795
+ }],
796
+ usage: {
797
+ prompt_tokens: promptTokens,
798
+ completion_tokens: completionTokens,
799
+ total_tokens: totalTokens
800
+ }
801
+ };
802
+
803
+ logger.info(`200!`);
804
+ return res.json(responseData);
805
+ } catch (err) {
806
+ logger.error(`处理响应失败: ${err}`);
807
+ continue;
808
+ }
809
+ } else if (success === 2) {
810
+ logger.error(`${model} 可能暂时不可用,请更换模型或未来一段时间再试`);
811
+ return res.status(503).json({
812
+ error: {
813
+ message: `${model} 可能暂时不可用,请更换模型或未来一段时间再试`,
814
+ type: 'internal_server_error'
815
+ }
816
+ });
817
+ }
818
+ }
819
+
820
+ logger.error(`${MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复`);
821
+ return res.status(response ? 500 : 503).json({
822
+ error: {
823
+ message: `${MAX_RETRIES} 次尝试均失败,请重试或等待官方恢复`,
824
+ type: 'internal_server_error'
825
+ }
826
+ });
827
+ });
828
+
829
+ app.get('/hf/v1/models', (req, res) => {
830
+ const response = { "object": "list", "data": GEMINI_MODELS };
831
+ return res.json(response);
832
+ });
833
+
834
+ app.post('/hf/v1/embeddings', async (req, res) => {
835
+ const data = req.body;
836
+ const modelInput = data.input;
837
+ const model = data.model || "text-embedding-004";
838
+
839
+ if (!modelInput) {
840
+ return res.status(400).json({ error: "没有提供输入" });
841
+ }
842
+
843
+ const inputs = Array.isArray(modelInput) ? modelInput : [modelInput];
844
+
845
+ const geminiRequest = {
846
+ model: `models/${model}`,
847
+ content: {
848
+ parts: inputs.map(text => ({ text }))
849
+ }
850
+ };
851
+
852
+ const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:embedContent?key=${currentApiKey}`;
853
+ const headers = { "Content-Type": "application/json" };
854
+
855
+ try {
856
+ const geminiResponse = await axios.post(geminiUrl, geminiRequest, { headers });
857
+
858
+ const embeddings = [];
859
+ if (geminiResponse.data.embedding) {
860
+ embeddings.push({
861
+ object: "embedding",
862
+ embedding: geminiResponse.data.embedding.values,
863
+ index: 0
864
+ });
865
+ } else if (geminiResponse.data.embeddings) {
866
+ geminiResponse.data.embeddings.forEach((embedding, i) => {
867
+ embeddings.push({
868
+ object: "embedding",
869
+ embedding: embedding.values,
870
+ index: i
871
+ });
872
+ });
873
+ }
874
+
875
+ const clientResponse = {
876
+ object: "list",
877
+ data: embeddings,
878
+ model: model,
879
+ usage: {
880
+ prompt_tokens: 0,
881
+ total_tokens: 0
882
+ }
883
+ };
884
+
885
+ switchApiKey();
886
+ return res.json(clientResponse);
887
+ } catch (e) {
888
+ console.log(`请求Embeddings失败↙\: ${e}`);
889
+ return res.status(500).json({ error: String(e) });
890
+ }
891
+ });
892
+
893
+ // Start the server
894
+ const PORT = process.env.PORT || 7860;
895
+ app.listen(PORT, '0.0.0.0', () => {
896
+ const scheduler = Scheduler;
897
+
898
+ logger.info(`Reminiproxy v2.3.5 启动`);
899
+ logger.info(`最大尝试次数/MaxRetries: ${MAX_RETRIES}`);
900
+ logger.info(`最大请求次数/MaxRequests: ${MAX_REQUESTS}`);
901
+ logger.info(`请求限额窗口/LimitWindow: ${LIMIT_WINDOW} 秒`);
902
+ logger.info(`服务器运行在端口: ${PORT}`);
903
+ });
package.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "hf1sthesh1t",
3
+ "version": "2.3.5",
4
+ "description": "A simple and lightweight Node.js web application for HF1sthesh1t",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "dependencies": {
10
+ "axios": "^1.6.2",
11
+ "date-fns": "^2.30.0",
12
+ "express": "^4.18.2",
13
+ "node-schedule": "^2.1.1"
14
+ },
15
+ "engines": {
16
+ "node": ">=16.0.0"
17
+ }
18
+ }