yang11edf commited on
Commit
d3c9e2d
·
verified ·
1 Parent(s): 2bc78c1

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile (1) +31 -0
  2. README (1).md +12 -0
  3. index.js +1315 -0
  4. logger.js +66 -0
  5. package.json +23 -0
Dockerfile (1) ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-slim
2
+
3
+ # 安装 Chrome 依赖
4
+ RUN apt-get update && apt-get install -y \
5
+ wget \
6
+ gnupg \
7
+ ca-certificates \
8
+ procps \
9
+ chromium \
10
+ chromium-sandbox
11
+
12
+ # 设置工作目录
13
+ WORKDIR /app
14
+
15
+ # 复制 package.json 和 package-lock.json
16
+ COPY package*.json ./
17
+
18
+ # 安装依赖
19
+ RUN npm install
20
+
21
+ # 复制源代码
22
+ COPY . .
23
+
24
+ # 设置环境变量
25
+ ENV PORT=7860
26
+
27
+ # 暴露端口
28
+ EXPOSE 7860
29
+
30
+ # 启动应用
31
+ CMD ["npm", "start"]
README (1).md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Grok API Service
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ ---
10
+
11
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
12
+
index.js ADDED
@@ -0,0 +1,1315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import fetch from 'node-fetch';
3
+ import FormData from 'form-data';
4
+ import dotenv from 'dotenv';
5
+ import cors from 'cors';
6
+ import puppeteer from 'puppeteer-extra'
7
+ import StealthPlugin from 'puppeteer-extra-plugin-stealth'
8
+ import { v4 as uuidv4 } from 'uuid';
9
+ import Logger from './logger.js';
10
+
11
+ dotenv.config();
12
+
13
+ // 配置常量
14
+ const CONFIG = {
15
+ MODELS: {
16
+ 'grok-2': 'grok-latest',
17
+ 'grok-2-imageGen': 'grok-latest',
18
+ 'grok-2-search': 'grok-latest',
19
+ "grok-3": "grok-3",
20
+ "grok-3-search": "grok-3",
21
+ "grok-3-imageGen": "grok-3",
22
+ "grok-3-deepsearch": "grok-3",
23
+ "grok-3-reasoning": "grok-3"
24
+ },
25
+ API: {
26
+ IS_TEMP_CONVERSATION: process.env.IS_TEMP_CONVERSATION == undefined ? false : process.env.IS_TEMP_CONVERSATION == 'true',
27
+ IS_TEMP_GROK2: process.env.IS_TEMP_GROK2 == undefined ? true : process.env.IS_TEMP_GROK2 == 'true',
28
+ GROK2_CONCURRENCY_LEVEL: process.env.GROK2_CONCURRENCY_LEVEL || 4,
29
+ IS_CUSTOM_SSO: process.env.IS_CUSTOM_SSO == undefined ? false : process.env.IS_CUSTOM_SSO == 'true',
30
+ BASE_URL: "https://grok.com",
31
+ API_KEY: process.env.API_KEY || "sk-123456",
32
+ SIGNATURE_COOKIE: null,
33
+ TEMP_COOKIE: null,
34
+ PICGO_KEY: process.env.PICGO_KEY || null, //想要流式生图的话需要填入这个PICGO图床的key
35
+ TUMY_KEY: process.env.TUMY_KEY || null //想要流式生图的话需要填入这个TUMY图床的key 两个图床二选一,默认使用PICGO
36
+ },
37
+ SERVER: {
38
+ PORT: process.env.PORT || 3000,
39
+ BODY_LIMIT: '5mb'
40
+ },
41
+ RETRY: {
42
+ MAX_ATTEMPTS: 2//重试次数
43
+ },
44
+ SHOW_THINKING: process.env.SHOW_THINKING == undefined ? true : process.env.SHOW_THINKING == 'true',
45
+ IS_THINKING: false,
46
+ IS_IMG_GEN: false,
47
+ IS_IMG_GEN2: false,
48
+ TEMP_COOKIE_INDEX: 0,//临时cookie的下标
49
+ ISSHOW_SEARCH_RESULTS: process.env.ISSHOW_SEARCH_RESULTS == undefined ? true : process.env.ISSHOW_SEARCH_RESULTS == 'true',//是否显示搜索结果
50
+ CHROME_PATH: process.env.CHROME_PATH || null
51
+ };
52
+ puppeteer.use(StealthPlugin())
53
+
54
+ // 请求头配置
55
+ const DEFAULT_HEADERS = {
56
+ 'Accept': '*/*',
57
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
58
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
59
+ 'Content-Type': 'text/plain;charset=UTF-8',
60
+ 'Connection': 'keep-alive',
61
+ 'Origin': 'https://grok.com',
62
+ 'Priority': 'u=1, i',
63
+ 'Sec-Ch-Ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
64
+ 'Sec-Ch-Ua-Mobile': '?0',
65
+ 'Sec-Ch-Ua-Platform': '"Windows"',
66
+ 'Sec-Fetch-Dest': 'empty',
67
+ 'Sec-Fetch-Mode': 'cors',
68
+ 'Sec-Fetch-Site': 'same-origin',
69
+ 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
70
+ };
71
+
72
+
73
+ async function initialization() {
74
+ if (CONFIG.CHROME_PATH == null) {
75
+ try {
76
+ CONFIG.CHROME_PATH = puppeteer.executablePath();
77
+ } catch (error) {
78
+ CONFIG.CHROME_PATH = "/usr/bin/chromium";
79
+ }
80
+ }
81
+ Logger.info(`CHROME_PATH: ${CONFIG.CHROME_PATH}`, 'Server');
82
+ if (CONFIG.API.IS_CUSTOM_SSO) {
83
+ if (CONFIG.API.IS_TEMP_GROK2) {
84
+ await tempCookieManager.ensureCookies();
85
+ }
86
+ return;
87
+ }
88
+ const ssoArray = process.env.SSO.split(',');
89
+ const concurrencyLimit = 4;
90
+ for (let i = 0; i < ssoArray.length; i += concurrencyLimit) {
91
+ const batch = ssoArray.slice(i, i + concurrencyLimit);
92
+ const batchPromises = batch.map(sso =>
93
+ tokenManager.addToken(`sso-rw=${sso};sso=${sso}`)
94
+ );
95
+
96
+ await Promise.all(batchPromises);
97
+ Logger.info(`已加载令牌: ${i+1} 个`, 'Server');
98
+ await new Promise(resolve => setTimeout(resolve, 1000));
99
+ }
100
+ Logger.info(`令牌加载完成: ${JSON.stringify(tokenManager.getAllTokens(), null, 2)}`, 'Server');
101
+ Logger.info(`共加载: ${tokenManager.getAllTokens().length}个令牌`, 'Server');
102
+ if (CONFIG.API.IS_TEMP_GROK2) {
103
+ await tempCookieManager.ensureCookies();
104
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
105
+ }
106
+ Logger.info("初始化完成", 'Server');
107
+ }
108
+
109
+ class AuthTokenManager {
110
+ constructor() {
111
+ this.tokenModelMap = {};
112
+ this.expiredTokens = new Set();
113
+ this.tokenStatusMap = {};
114
+
115
+ // 定义模型请求频率限制和过期时间
116
+ this.modelConfig = {
117
+ "grok-2": {
118
+ RequestFrequency: 30,
119
+ ExpirationTime: 1 * 60 * 60 * 1000 // 1小时
120
+ },
121
+ "grok-3": {
122
+ RequestFrequency: 20,
123
+ ExpirationTime: 2 * 60 * 60 * 1000 // 2小时
124
+ },
125
+ "grok-3-deepsearch": {
126
+ RequestFrequency: 10,
127
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小时
128
+ },
129
+ "grok-3-reasoning": {
130
+ RequestFrequency: 10,
131
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小���
132
+ }
133
+ };
134
+ this.tokenResetSwitch = false;
135
+ this.tokenResetTimer = null;
136
+ }
137
+ async fetchGrokStats(token, modelName) {
138
+ let requestKind = 'DEFAULT';
139
+ if (modelName == 'grok-2' || modelName == 'grok-3') {
140
+ requestKind = 'DEFAULT';
141
+ } else if (modelName == 'grok-3-deepsearch') {
142
+ requestKind = 'DEEPSEARCH';
143
+ } else if (modelName == 'grok-3-reasoning') {
144
+ requestKind = 'REASONING';
145
+ }
146
+ const response = await fetch('https://grok.com/rest/rate-limits', {
147
+ method: 'POST',
148
+ headers: {
149
+ 'content-type': 'application/json',
150
+ 'Cookie': token,
151
+ },
152
+ body: JSON.stringify({
153
+ "requestKind": requestKind,
154
+ "modelName": modelName == 'grok-2' ? 'grok-latest' : "grok-3"
155
+ })
156
+ });
157
+
158
+ if (response.status != 200) {
159
+ return 0;
160
+ }
161
+ const data = await response.json();
162
+ return data.remainingQueries;
163
+ }
164
+ async addToken(token) {
165
+ const sso = token.split("sso=")[1].split(";")[0];
166
+
167
+ for (const model of Object.keys(this.modelConfig)) {
168
+ if (!this.tokenModelMap[model]) {
169
+ this.tokenModelMap[model] = [];
170
+ }
171
+ if (!this.tokenStatusMap[sso]) {
172
+ this.tokenStatusMap[sso] = {};
173
+ }
174
+ const existingTokenEntry = this.tokenModelMap[model].find(entry => entry.token === token);
175
+
176
+ if (!existingTokenEntry) {
177
+ try {
178
+ const remainingQueries = await this.fetchGrokStats(token, model);
179
+
180
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
181
+ const usedRequestCount = modelRequestFrequency - remainingQueries;
182
+
183
+ if (usedRequestCount === modelRequestFrequency) {
184
+ this.expiredTokens.add({
185
+ token: token,
186
+ model: model,
187
+ expiredTime: Date.now()
188
+ });
189
+
190
+ if (!this.tokenStatusMap[sso][model]) {
191
+ this.tokenStatusMap[sso][model] = {
192
+ isValid: false,
193
+ invalidatedTime: Date.now(),
194
+ totalRequestCount: Math.max(0, usedRequestCount)
195
+ };
196
+ }
197
+
198
+ if (!this.tokenResetSwitch) {
199
+ this.startTokenResetProcess();
200
+ this.tokenResetSwitch = true;
201
+ }
202
+ } else {
203
+ this.tokenModelMap[model].push({
204
+ token: token,
205
+ RequestCount: Math.max(0, usedRequestCount),
206
+ AddedTime: Date.now(),
207
+ StartCallTime: null
208
+ });
209
+
210
+ if (!this.tokenStatusMap[sso][model]) {
211
+ this.tokenStatusMap[sso][model] = {
212
+ isValid: true,
213
+ invalidatedTime: null,
214
+ totalRequestCount: Math.max(0, usedRequestCount)
215
+ };
216
+ }
217
+ }
218
+ } catch (error) {
219
+ this.tokenModelMap[model].push({
220
+ token: token,
221
+ RequestCount: 0,
222
+ AddedTime: Date.now(),
223
+ StartCallTime: null
224
+ });
225
+
226
+ if (!this.tokenStatusMap[sso][model]) {
227
+ this.tokenStatusMap[sso][model] = {
228
+ isValid: true,
229
+ invalidatedTime: null,
230
+ totalRequestCount: 0
231
+ };
232
+ }
233
+
234
+ Logger.error(`获取模型 ${model} 的统计信息失败: ${error}`, 'TokenManager');
235
+ }
236
+ await Utils.delay(200);
237
+ }
238
+ }
239
+ }
240
+
241
+ setToken(token) {
242
+ const models = Object.keys(this.modelConfig);
243
+ this.tokenModelMap = models.reduce((map, model) => {
244
+ map[model] = [{
245
+ token,
246
+ RequestCount: 0,
247
+ AddedTime: Date.now(),
248
+ StartCallTime: null
249
+ }];
250
+ return map;
251
+ }, {});
252
+ const sso = token.split("sso=")[1].split(";")[0];
253
+ this.tokenStatusMap[sso] = models.reduce((statusMap, model) => {
254
+ statusMap[model] = {
255
+ isValid: true,
256
+ invalidatedTime: null,
257
+ totalRequestCount: 0
258
+ };
259
+ return statusMap;
260
+ }, {});
261
+ }
262
+
263
+ async deleteToken(token) {
264
+ try {
265
+ const sso = token.split("sso=")[1].split(";")[0];
266
+ await Promise.all([
267
+ new Promise((resolve) => {
268
+ this.tokenModelMap = Object.fromEntries(
269
+ Object.entries(this.tokenModelMap).map(([model, entries]) => [
270
+ model,
271
+ entries.filter(entry => entry.token !== token)
272
+ ])
273
+ );
274
+ resolve();
275
+ }),
276
+
277
+ new Promise((resolve) => {
278
+ delete this.tokenStatusMap[sso];
279
+ resolve();
280
+ }),
281
+ ]);
282
+ Logger.info(`令牌已成功移除: ${token}`, 'TokenManager');
283
+ return true;
284
+ } catch (error) {
285
+ Logger.error('令牌删除失败:', error);
286
+ return false;
287
+ }
288
+ }
289
+ getNextTokenForModel(modelId) {
290
+ const normalizedModel = this.normalizeModelName(modelId);
291
+
292
+ if (!this.tokenModelMap[normalizedModel] || this.tokenModelMap[normalizedModel].length === 0) {
293
+ return null;
294
+ }
295
+ const tokenEntry = this.tokenModelMap[normalizedModel][0];
296
+
297
+ if (tokenEntry) {
298
+ if (tokenEntry.StartCallTime === null || tokenEntry.StartCallTime === undefined) {
299
+ tokenEntry.StartCallTime = Date.now();
300
+ }
301
+ if (!this.tokenResetSwitch) {
302
+ this.startTokenResetProcess();
303
+ this.tokenResetSwitch = true;
304
+ }
305
+ tokenEntry.RequestCount++;
306
+
307
+ if (tokenEntry.RequestCount > this.modelConfig[normalizedModel].RequestFrequency) {
308
+ this.removeTokenFromModel(normalizedModel, tokenEntry.token);
309
+ const nextTokenEntry = this.tokenModelMap[normalizedModel][0];
310
+ return nextTokenEntry ? nextTokenEntry.token : null;
311
+ }
312
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
313
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][normalizedModel]) {
314
+ if (tokenEntry.RequestCount === this.modelConfig[normalizedModel].RequestFrequency) {
315
+ this.tokenStatusMap[sso][normalizedModel].isValid = false;
316
+ this.tokenStatusMap[sso][normalizedModel].invalidatedTime = Date.now();
317
+ }
318
+ this.tokenStatusMap[sso][normalizedModel].totalRequestCount++;
319
+ }
320
+ return tokenEntry.token;
321
+ }
322
+
323
+ return null;
324
+ }
325
+
326
+ removeTokenFromModel(modelId, token) {
327
+ const normalizedModel = this.normalizeModelName(modelId);
328
+
329
+ if (!this.tokenModelMap[normalizedModel]) {
330
+ Logger.error(`模型 ${normalizedModel} 不存在`, 'TokenManager');
331
+ return false;
332
+ }
333
+
334
+ const modelTokens = this.tokenModelMap[normalizedModel];
335
+ const tokenIndex = modelTokens.findIndex(entry => entry.token === token);
336
+
337
+ if (tokenIndex !== -1) {
338
+ const removedTokenEntry = modelTokens.splice(tokenIndex, 1)[0];
339
+ this.expiredTokens.add({
340
+ token: removedTokenEntry.token,
341
+ model: normalizedModel,
342
+ expiredTime: Date.now()
343
+ });
344
+
345
+ if (!this.tokenResetSwitch) {
346
+ this.startTokenResetProcess();
347
+ this.tokenResetSwitch = true;
348
+ }
349
+ Logger.info(`模型${modelId}的令牌已失效,已成功移除令牌: ${token}`, 'TokenManager');
350
+ return true;
351
+ }
352
+
353
+ Logger.error(`在模型 ${normalizedModel} 中未找到 token: ${token}`, 'TokenManager');
354
+ return false;
355
+ }
356
+
357
+ getExpiredTokens() {
358
+ return Array.from(this.expiredTokens);
359
+ }
360
+
361
+ normalizeModelName(model) {
362
+ if (model.startsWith('grok-') && !model.includes('deepsearch') && !model.includes('reasoning')) {
363
+ return model.split('-').slice(0, 2).join('-');
364
+ }
365
+ return model;
366
+ }
367
+
368
+ getTokenCountForModel(modelId) {
369
+ const normalizedModel = this.normalizeModelName(modelId);
370
+ return this.tokenModelMap[normalizedModel]?.length || 0;
371
+ }
372
+
373
+ getRemainingTokenRequestCapacity() {
374
+ const remainingCapacityMap = {};
375
+
376
+ Object.keys(this.modelConfig).forEach(model => {
377
+ const modelTokens = this.tokenModelMap[model] || [];
378
+
379
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
380
+
381
+ const totalUsedRequests = modelTokens.reduce((sum, tokenEntry) => {
382
+ return sum + (tokenEntry.RequestCount || 0);
383
+ }, 0);
384
+
385
+ // 计算剩余可用请求数量
386
+ const remainingCapacity = (modelTokens.length * modelRequestFrequency) - totalUsedRequests;
387
+ remainingCapacityMap[model] = Math.max(0, remainingCapacity);
388
+ });
389
+
390
+ return remainingCapacityMap;
391
+ }
392
+
393
+ getTokenArrayForModel(modelId) {
394
+ const normalizedModel = this.normalizeModelName(modelId);
395
+ return this.tokenModelMap[normalizedModel] || [];
396
+ }
397
+
398
+ startTokenResetProcess() {
399
+ if (this.tokenResetTimer) {
400
+ clearInterval(this.tokenResetTimer);
401
+ }
402
+
403
+ this.tokenResetTimer = setInterval(() => {
404
+ const now = Date.now();
405
+
406
+ this.expiredTokens.forEach(expiredTokenInfo => {
407
+ const { token, model, expiredTime } = expiredTokenInfo;
408
+ const expirationTime = this.modelConfig[model].ExpirationTime;
409
+ if (now - expiredTime >= expirationTime) {
410
+ if (!this.tokenModelMap[model].some(entry => entry.token === token)) {
411
+ this.tokenModelMap[model].push({
412
+ token: token,
413
+ RequestCount: 0,
414
+ AddedTime: now,
415
+ StartCallTime: null
416
+ });
417
+ }
418
+ const sso = token.split("sso=")[1].split(";")[0];
419
+
420
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
421
+ this.tokenStatusMap[sso][model].isValid = true;
422
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
423
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
424
+ }
425
+
426
+ this.expiredTokens.delete(expiredTokenInfo);
427
+ }
428
+ });
429
+
430
+ Object.keys(this.modelConfig).forEach(model => {
431
+ if (!this.tokenModelMap[model]) return;
432
+
433
+ const processedTokens = this.tokenModelMap[model].map(tokenEntry => {
434
+ if (!tokenEntry.StartCallTime) return tokenEntry;
435
+
436
+ const expirationTime = this.modelConfig[model].ExpirationTime;
437
+ if (now - tokenEntry.StartCallTime >= expirationTime) {
438
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
439
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
440
+ this.tokenStatusMap[sso][model].isValid = true;
441
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
442
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
443
+ }
444
+
445
+ return {
446
+ ...tokenEntry,
447
+ RequestCount: 0,
448
+ StartCallTime: null
449
+ };
450
+ }
451
+
452
+ return tokenEntry;
453
+ });
454
+
455
+ this.tokenModelMap[model] = processedTokens;
456
+ });
457
+ }, 1 * 60 * 60 * 1000);
458
+ }
459
+
460
+ getAllTokens() {
461
+ const allTokens = new Set();
462
+ Object.values(this.tokenModelMap).forEach(modelTokens => {
463
+ modelTokens.forEach(entry => allTokens.add(entry.token));
464
+ });
465
+ return Array.from(allTokens);
466
+ }
467
+
468
+ getTokenStatusMap() {
469
+ return this.tokenStatusMap;
470
+ }
471
+ }
472
+
473
+
474
+ class Utils {
475
+ static delay(time) {
476
+ return new Promise(function (resolve) {
477
+ setTimeout(resolve, time)
478
+ });
479
+ }
480
+ static async organizeSearchResults(searchResults) {
481
+ // 确保传入的是有效的搜索结果对象
482
+ if (!searchResults || !searchResults.results) {
483
+ return '';
484
+ }
485
+
486
+ const results = searchResults.results;
487
+ const formattedResults = results.map((result, index) => {
488
+ // 处理可能为空的字段
489
+ const title = result.title || '未知标题';
490
+ const url = result.url || '#';
491
+ const preview = result.preview || '无预览内容';
492
+
493
+ return `\r\n<details><summary>资料[${index}]: ${title}</summary>\r\n${preview}\r\n\n[Link](${url})\r\n</details>`;
494
+ });
495
+ return formattedResults.join('\n\n');
496
+ }
497
+ static async createAuthHeaders(model) {
498
+ return await tokenManager.getNextTokenForModel(model);
499
+ }
500
+ }
501
+ class GrokTempCookieManager {
502
+ constructor() {
503
+ this.cookies = [];
504
+ this.currentIndex = 0;
505
+ this.isRefreshing = false;
506
+ this.initialCookieCount = CONFIG.API.GROK2_CONCURRENCY_LEVEL;
507
+ this.extractCount = 0;
508
+ }
509
+
510
+ async ensureCookies() {
511
+ // 如果 cookies 数量不足,则重新获取
512
+ if (this.cookies.length < this.initialCookieCount) {
513
+ await this.refreshCookies();
514
+ }
515
+ }
516
+ async extractGrokHeaders(browser) {
517
+ Logger.info("开始提取头信息", 'Server');
518
+ try {
519
+ const page = await browser.newPage();
520
+ await page.goto('https://grok.com/', { waitUntil: 'domcontentloaded' });
521
+ let waitTime = 0;
522
+ const targetHeaders = ['x-anonuserid', 'x-challenge', 'x-signature'];
523
+
524
+ while (true) {
525
+ const cookies = await page.cookies();
526
+ const extractedHeaders = cookies
527
+ .filter(cookie => targetHeaders.includes(cookie.name.toLowerCase()))
528
+ .map(cookie => `${cookie.name}=${cookie.value}`);
529
+
530
+ if (targetHeaders.every(header =>
531
+ extractedHeaders.some(cookie => cookie && cookie.startsWith(header + '='))
532
+ )) {
533
+ await browser.close();
534
+ Logger.info('提取的头信息:', JSON.stringify(extractedHeaders, null, 2), 'Server');
535
+ this.cookies.push(extractedHeaders.join(';'));
536
+ this.extractCount++;
537
+ return true;
538
+ }
539
+
540
+ await Utils.delay(500);
541
+ waitTime += 500;
542
+ if (waitTime >= 10000) {
543
+ await browser.close();
544
+ return null;
545
+ }
546
+ }
547
+ } catch (error) {
548
+ Logger.error('获取头信息出错:', error, 'Server');
549
+ return null;
550
+ }
551
+ }
552
+ async initializeTempCookies(count = 1) {
553
+ Logger.info(`开始初始化 ${count} 个临时账号认证信息`, 'Server');
554
+ const browserOptions = {
555
+ headless: true,
556
+ args: [
557
+ '--no-sandbox',
558
+ '--disable-setuid-sandbox',
559
+ '--disable-dev-shm-usage',
560
+ '--disable-gpu'
561
+ ],
562
+ executablePath: CONFIG.CHROME_PATH
563
+ };
564
+
565
+ const browsers = await Promise.all(
566
+ Array.from({ length: count }, () => puppeteer.launch(browserOptions))
567
+ );
568
+
569
+ const cookiePromises = browsers.map(browser => this.extractGrokHeaders(browser));
570
+ return Promise.all(cookiePromises);
571
+ }
572
+ async refreshCookies() {
573
+ if (this.isRefreshing) return;
574
+ this.isRefreshing = true;
575
+ this.extractCount = 0;
576
+ try {
577
+ // 获取新的 cookies
578
+ let retryCount = 0;
579
+ let remainingCount = this.initialCookieCount - this.cookies.length;
580
+
581
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
582
+ await this.initializeTempCookies(remainingCount);
583
+ if (this.extractCount != remainingCount) {
584
+ if (this.extractCount == 0) {
585
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
586
+ } else if (this.extractCount < remainingCount) {
587
+ remainingCount -= this.extractCount;
588
+ this.extractCount = 0;
589
+ retryCount++;
590
+ await Utils.delay(1000 * retryCount);
591
+ } else {
592
+ break;
593
+ }
594
+ } else {
595
+ break;
596
+ }
597
+ }
598
+ if (this.currentIndex >= this.cookies.length) {
599
+ this.currentIndex = 0;
600
+ }
601
+
602
+ if (this.cookies.length < this.initialCookieCount) {
603
+ if (this.cookies.length !== 0) {
604
+ // 如果已经获取到一些 TempCookies,则只提示警告错误
605
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
606
+ } else {
607
+ // 如果未获取到任何 TempCookies,则抛出错误
608
+ throw new Error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
609
+ }
610
+ }
611
+ } catch (error) {
612
+ Logger.error('刷新 cookies 失败:', error);
613
+ } finally {
614
+ Logger.info(`已提取${this.cookies.length}个TempCookies`, 'Server');
615
+ Logger.info(`提取的TempCookies为${JSON.stringify(this.cookies, null, 2)}`, 'Server');
616
+ this.isRefreshing = false;
617
+ }
618
+ }
619
+ }
620
+
621
+ class GrokApiClient {
622
+ constructor(modelId) {
623
+ if (!CONFIG.MODELS[modelId]) {
624
+ throw new Error(`不支持的模型: ${modelId}`);
625
+ }
626
+ this.modelId = CONFIG.MODELS[modelId];
627
+ }
628
+
629
+ processMessageContent(content) {
630
+ if (typeof content === 'string') return content;
631
+ return null;
632
+ }
633
+ // 获取图片类型
634
+ getImageType(base64String) {
635
+ let mimeType = 'image/jpeg';
636
+ if (base64String.includes('data:image')) {
637
+ const matches = base64String.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,/);
638
+ if (matches) {
639
+ mimeType = matches[1];
640
+ }
641
+ }
642
+ const extension = mimeType.split('/')[1];
643
+ const fileName = `image.${extension}`;
644
+
645
+ return {
646
+ mimeType: mimeType,
647
+ fileName: fileName
648
+ };
649
+ }
650
+
651
+ async uploadBase64Image(base64Data, url) {
652
+ try {
653
+ // 处理 base64 数据
654
+ let imageBuffer;
655
+ if (base64Data.includes('data:image')) {
656
+ imageBuffer = base64Data.split(',')[1];
657
+ } else {
658
+ imageBuffer = base64Data
659
+ }
660
+ const { mimeType, fileName } = this.getImageType(base64Data);
661
+ let uploadData = {
662
+ rpc: "uploadFile",
663
+ req: {
664
+ fileName: fileName,
665
+ fileMimeType: mimeType,
666
+ content: imageBuffer
667
+ }
668
+ };
669
+ Logger.info("发送图片请求", 'Server');
670
+ // 发送请求
671
+ const response = await fetch(url, {
672
+ method: 'POST',
673
+ headers: {
674
+ ...CONFIG.DEFAULT_HEADERS,
675
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
676
+ },
677
+ body: JSON.stringify(uploadData)
678
+ });
679
+
680
+ if (!response.ok) {
681
+ Logger.error(`上传图片失败,状态码:${response.status},原因:${response.error}`, 'Server');
682
+ return '';
683
+ }
684
+
685
+ const result = await response.json();
686
+ Logger.info('上传图片成功:', result, 'Server');
687
+ return result.fileMetadataId;
688
+
689
+ } catch (error) {
690
+ Logger.error(error, 'Server');
691
+ return '';
692
+ }
693
+ }
694
+
695
+ async prepareChatRequest(request) {
696
+ if ((request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') && !CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY && request.stream) {
697
+ throw new Error(`该模型流式输出需要配置PICGO或者TUMY图床密钥!`);
698
+ }
699
+
700
+ // 处理画图模型的消息限制
701
+ let todoMessages = request.messages;
702
+ if (request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') {
703
+ const lastMessage = todoMessages[todoMessages.length - 1];
704
+ if (lastMessage.role !== 'user') {
705
+ throw new Error('画图模型的最后一条消息必须是用户消息!');
706
+ }
707
+ todoMessages = [lastMessage];
708
+ }
709
+
710
+ const fileAttachments = [];
711
+ let messages = '';
712
+ let lastRole = null;
713
+ let lastContent = '';
714
+ const search = request.model === 'grok-2-search' || request.model === 'grok-3-search';
715
+
716
+ // 移除<think>标签及其内容和base64图片
717
+ const removeThinkTags = (text) => {
718
+ text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
719
+ text = text.replace(/!\[image\]\(data:.*?base64,.*?\)/g, '[图片]');
720
+ return text;
721
+ };
722
+
723
+ const processImageUrl = async (content) => {
724
+ if (content.type === 'image_url' && content.image_url.url.includes('data:image')) {
725
+ const imageResponse = await this.uploadBase64Image(
726
+ content.image_url.url,
727
+ `${CONFIG.API.BASE_URL}/api/rpc`
728
+ );
729
+ return imageResponse;
730
+ }
731
+ return null;
732
+ };
733
+
734
+ const processContent = async (content) => {
735
+ if (Array.isArray(content)) {
736
+ let textContent = '';
737
+ for (const item of content) {
738
+ if (item.type === 'image_url') {
739
+ textContent += (textContent ? '\n' : '') + "[图片]";
740
+ } else if (item.type === 'text') {
741
+ textContent += (textContent ? '\n' : '') + removeThinkTags(item.text);
742
+ }
743
+ }
744
+ return textContent;
745
+ } else if (typeof content === 'object' && content !== null) {
746
+ if (content.type === 'image_url') {
747
+ return "[图片]";
748
+ } else if (content.type === 'text') {
749
+ return removeThinkTags(content.text);
750
+ }
751
+ }
752
+ return removeThinkTags(this.processMessageContent(content));
753
+ };
754
+
755
+ for (const current of todoMessages) {
756
+ const role = current.role === 'assistant' ? 'assistant' : 'user';
757
+ const isLastMessage = current === todoMessages[todoMessages.length - 1];
758
+
759
+ // 处理图片附件
760
+ if (isLastMessage && current.content) {
761
+ if (Array.isArray(current.content)) {
762
+ for (const item of current.content) {
763
+ if (item.type === 'image_url') {
764
+ const processedImage = await processImageUrl(item);
765
+ if (processedImage) fileAttachments.push(processedImage);
766
+ }
767
+ }
768
+ } else if (current.content.type === 'image_url') {
769
+ const processedImage = await processImageUrl(current.content);
770
+ if (processedImage) fileAttachments.push(processedImage);
771
+ }
772
+ }
773
+
774
+ // 处理文本内容
775
+ const textContent = await processContent(current.content);
776
+
777
+ if (textContent || (isLastMessage && fileAttachments.length > 0)) {
778
+ if (role === lastRole && textContent) {
779
+ lastContent += '\n' + textContent;
780
+ messages = messages.substring(0, messages.lastIndexOf(`${role.toUpperCase()}: `)) +
781
+ `${role.toUpperCase()}: ${lastContent}\n`;
782
+ } else {
783
+ messages += `${role.toUpperCase()}: ${textContent || '[图片]'}\n`;
784
+ lastContent = textContent;
785
+ lastRole = role;
786
+ }
787
+ }
788
+ }
789
+
790
+ return {
791
+ temporary: CONFIG.API.IS_TEMP_CONVERSATION,
792
+ modelName: this.modelId,
793
+ message: messages.trim(),
794
+ fileAttachments: fileAttachments.slice(0, 4),
795
+ imageAttachments: [],
796
+ disableSearch: false,
797
+ enableImageGeneration: true,
798
+ returnImageBytes: false,
799
+ returnRawGrokInXaiRequest: false,
800
+ enableImageStreaming: false,
801
+ imageGenerationCount: 1,
802
+ forceConcise: false,
803
+ toolOverrides: {
804
+ imageGen: request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen',
805
+ webSearch: search,
806
+ xSearch: search,
807
+ xMediaSearch: search,
808
+ trendsSearch: search,
809
+ xPostAnalyze: search
810
+ },
811
+ enableSideBySide: true,
812
+ isPreset: false,
813
+ sendFinalMetadata: true,
814
+ customInstructions: "",
815
+ deepsearchPreset: request.model === 'grok-3-deepsearch' ? "default" : "",
816
+ isReasoning: request.model === 'grok-3-reasoning'
817
+ };
818
+ }
819
+ }
820
+
821
+ class MessageProcessor {
822
+ static createChatResponse(message, model, isStream = false) {
823
+ const baseResponse = {
824
+ id: `chatcmpl-${uuidv4()}`,
825
+ created: Math.floor(Date.now() / 1000),
826
+ model: model
827
+ };
828
+
829
+ if (isStream) {
830
+ return {
831
+ ...baseResponse,
832
+ object: 'chat.completion.chunk',
833
+ choices: [{
834
+ index: 0,
835
+ delta: {
836
+ content: message
837
+ }
838
+ }]
839
+ };
840
+ }
841
+
842
+ return {
843
+ ...baseResponse,
844
+ object: 'chat.completion',
845
+ choices: [{
846
+ index: 0,
847
+ message: {
848
+ role: 'assistant',
849
+ content: message
850
+ },
851
+ finish_reason: 'stop'
852
+ }],
853
+ usage: null
854
+ };
855
+ }
856
+ }
857
+ async function processModelResponse(response, model) {
858
+ let result = { token: null, imageUrl: null }
859
+ if (CONFIG.IS_IMG_GEN) {
860
+ if (response?.cachedImageGenerationResponse && !CONFIG.IS_IMG_GEN2) {
861
+ result.imageUrl = response.cachedImageGenerationResponse.imageUrl;
862
+ }
863
+ return result;
864
+ }
865
+
866
+ //非生图模型的处理
867
+ switch (model) {
868
+ case 'grok-2':
869
+ result.token = response?.token;
870
+ return result;
871
+ case 'grok-2-search':
872
+ case 'grok-3-search':
873
+ if (response?.webSearchResults && CONFIG.ISSHOW_SEARCH_RESULTS) {
874
+ result.token = `\r\n<think>${await Utils.organizeSearchResults(response.webSearchResults)}</think>\r\n`;
875
+ } else {
876
+ result.token = response?.token;
877
+ }
878
+ return result;
879
+ case 'grok-3':
880
+ result.token = response?.token;
881
+ return result;
882
+ case 'grok-3-deepsearch':
883
+ if (response?.messageTag === "final") {
884
+ result.token = response?.token;
885
+ }
886
+ return result;
887
+ case 'grok-3-reasoning':
888
+ if (response?.isThinking && !CONFIG.SHOW_THINKING) return result;
889
+
890
+ if (response?.isThinking && !CONFIG.IS_THINKING) {
891
+ result.token = "<think>" + response?.token;
892
+ CONFIG.IS_THINKING = true;
893
+ } else if (!response.isThinking && CONFIG.IS_THINKING) {
894
+ result.token = "</think>" + response?.token;
895
+ CONFIG.IS_THINKING = false;
896
+ } else {
897
+ result.token = response?.token;
898
+ }
899
+ return result;
900
+ }
901
+ return result;
902
+ }
903
+
904
+ async function handleResponse(response, model, res, isStream) {
905
+ try {
906
+ const stream = response.body;
907
+ let buffer = '';
908
+ let fullResponse = '';
909
+ const dataPromises = [];
910
+ if (isStream) {
911
+ res.setHeader('Content-Type', 'text/event-stream');
912
+ res.setHeader('Cache-Control', 'no-cache');
913
+ res.setHeader('Connection', 'keep-alive');
914
+ }
915
+ CONFIG.IS_THINKING = false;
916
+ CONFIG.IS_IMG_GEN = false;
917
+ CONFIG.IS_IMG_GEN2 = false;
918
+ Logger.info("开始处理流式响应", 'Server');
919
+
920
+ return new Promise((resolve, reject) => {
921
+ stream.on('data', async (chunk) => {
922
+ buffer += chunk.toString();
923
+ const lines = buffer.split('\n');
924
+ buffer = lines.pop() || '';
925
+
926
+ for (const line of lines) {
927
+ if (!line.trim()) continue;
928
+ try {
929
+ const linejosn = JSON.parse(line.trim());
930
+ if (linejosn?.error) {
931
+ Logger.error(JSON.stringify(linejosn, null, 2), 'Server');
932
+ if (linejosn.error?.name === "RateLimitError") {
933
+ CONFIG.API.TEMP_COOKIE = null;
934
+ }
935
+ stream.destroy();
936
+ reject(new Error("RateLimitError"));
937
+ return;
938
+ }
939
+ let response = linejosn?.result?.response;
940
+ if (!response) continue;
941
+ if (response?.doImgGen || response?.imageAttachmentInfo) {
942
+ CONFIG.IS_IMG_GEN = true;
943
+ }
944
+ const processPromise = (async () => {
945
+ const result = await processModelResponse(response, model);
946
+
947
+ if (result.token) {
948
+ if (isStream) {
949
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(result.token, model, true))}\n\n`);
950
+ } else {
951
+ fullResponse += result.token;
952
+ }
953
+ }
954
+ if (result.imageUrl) {
955
+ CONFIG.IS_IMG_GEN2 = true;
956
+ const dataImage = await handleImageResponse(result.imageUrl);
957
+ if (isStream) {
958
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(dataImage, model, true))}\n\n`);
959
+ } else {
960
+ res.json(MessageProcessor.createChatResponse(dataImage, model));
961
+ }
962
+ }
963
+ })();
964
+ dataPromises.push(processPromise);
965
+ } catch (error) {
966
+ Logger.error(error, 'Server');
967
+ continue;
968
+ }
969
+ }
970
+ });
971
+
972
+ stream.on('end', async () => {
973
+ try {
974
+ await Promise.all(dataPromises);
975
+ if (isStream) {
976
+ res.write('data: [DONE]\n\n');
977
+ res.end();
978
+ } else {
979
+ if (!CONFIG.IS_IMG_GEN2) {
980
+ res.json(MessageProcessor.createChatResponse(fullResponse, model));
981
+ }
982
+ }
983
+ resolve();
984
+ } catch (error) {
985
+ Logger.error(error, 'Server');
986
+ reject(error);
987
+ }
988
+ });
989
+
990
+ stream.on('error', (error) => {
991
+ Logger.error(error, 'Server');
992
+ reject(error);
993
+ });
994
+ });
995
+ } catch (error) {
996
+ Logger.error(error, 'Server');
997
+ throw new Error(error);
998
+ }
999
+ }
1000
+
1001
+ async function handleImageResponse(imageUrl) {
1002
+ const MAX_RETRIES = 2;
1003
+ let retryCount = 0;
1004
+ let imageBase64Response;
1005
+
1006
+ while (retryCount < MAX_RETRIES) {
1007
+ try {
1008
+ imageBase64Response = await fetch(`https://assets.grok.com/${imageUrl}`, {
1009
+ method: 'GET',
1010
+ headers: {
1011
+ ...DEFAULT_HEADERS,
1012
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
1013
+ }
1014
+ });
1015
+
1016
+ if (imageBase64Response.ok) break;
1017
+ retryCount++;
1018
+ if (retryCount === MAX_RETRIES) {
1019
+ throw new Error(`上游服务请求失败! status: ${imageBase64Response.status}`);
1020
+ }
1021
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1022
+
1023
+ } catch (error) {
1024
+ Logger.error(error, 'Server');
1025
+ retryCount++;
1026
+ if (retryCount === MAX_RETRIES) {
1027
+ throw error;
1028
+ }
1029
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1030
+ }
1031
+ }
1032
+
1033
+
1034
+ const arrayBuffer = await imageBase64Response.arrayBuffer();
1035
+ const imageBuffer = Buffer.from(arrayBuffer);
1036
+
1037
+ if (!CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY) {
1038
+ const base64Image = imageBuffer.toString('base64');
1039
+ const imageContentType = imageBase64Response.headers.get('content-type');
1040
+ return `![image](data:${imageContentType};base64,${base64Image})`
1041
+ }
1042
+
1043
+ Logger.info("开始上传图床", 'Server');
1044
+ const formData = new FormData();
1045
+ if (CONFIG.API.PICGO_KEY) {
1046
+ formData.append('source', imageBuffer, {
1047
+ filename: `image-${Date.now()}.jpg`,
1048
+ contentType: 'image/jpeg'
1049
+ });
1050
+ const formDataHeaders = formData.getHeaders();
1051
+ const responseURL = await fetch("https://www.picgo.net/api/1/upload", {
1052
+ method: "POST",
1053
+ headers: {
1054
+ ...formDataHeaders,
1055
+ "Content-Type": "multipart/form-data",
1056
+ "X-API-Key": CONFIG.API.PICGO_KEY
1057
+ },
1058
+ body: formData
1059
+ });
1060
+ if (!responseURL.ok) {
1061
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
1062
+ } else {
1063
+ Logger.info("生图成功", 'Server');
1064
+ const result = await responseURL.json();
1065
+ return `![image](${result.image.url})`
1066
+ }
1067
+ } else if (CONFIG.API.TUMY_KEY) {
1068
+ const formData = new FormData();
1069
+ formData.append('file', imageBuffer, {
1070
+ filename: `image-${Date.now()}.jpg`,
1071
+ contentType: 'image/jpeg'
1072
+ });
1073
+ const formDataHeaders = formData.getHeaders();
1074
+ const responseURL = await fetch("https://tu.my/api/v1/upload", {
1075
+ method: "POST",
1076
+ headers: {
1077
+ ...formDataHeaders,
1078
+ "Accept": "application/json",
1079
+ 'Authorization': `Bearer ${CONFIG.API.TUMY_KEY}`
1080
+ },
1081
+ body: formData
1082
+ });
1083
+ if (!responseURL.ok) {
1084
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1085
+ } else {
1086
+ try {
1087
+ const result = await responseURL.json();
1088
+ Logger.info("生图成功", 'Server');
1089
+ return `![image](${result.data.links.url})`
1090
+ } catch (error) {
1091
+ Logger.error(error, 'Server');
1092
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ const tokenManager = new AuthTokenManager();
1099
+ const tempCookieManager = new GrokTempCookieManager();
1100
+ await initialization();
1101
+
1102
+ // 中间件配置
1103
+ const app = express();
1104
+ app.use(Logger.requestLogger);
1105
+ app.use(express.json({ limit: CONFIG.SERVER.BODY_LIMIT }));
1106
+ app.use(express.urlencoded({ extended: true, limit: CONFIG.SERVER.BODY_LIMIT }));
1107
+ app.use(cors({
1108
+ origin: '*',
1109
+ methods: ['GET', 'POST', 'OPTIONS'],
1110
+ allowedHeaders: ['Content-Type', 'Authorization']
1111
+ }));
1112
+
1113
+
1114
+ app.get('/hf/get/tokens', (req, res) => {
1115
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1116
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1117
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法获取轮询sso令牌状态' });
1118
+ } else if (authToken !== CONFIG.API.API_KEY) {
1119
+ return res.status(401).json({ error: 'Unauthorized' });
1120
+ }
1121
+ res.json(tokenManager.getTokenStatusMap());
1122
+ });
1123
+ app.post('/hf/add/token', async (req, res) => {
1124
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1125
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1126
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法添加sso令牌' });
1127
+ } else if (authToken !== CONFIG.API.API_KEY) {
1128
+ return res.status(401).json({ error: 'Unauthorized' });
1129
+ }
1130
+ try {
1131
+ const sso = req.body.sso;
1132
+ await tokenManager.addToken(`sso-rw=${sso};sso=${sso}`);
1133
+ res.status(200).json(tokenManager.getTokenStatusMap()[sso]);
1134
+ } catch (error) {
1135
+ Logger.error(error, 'Server');
1136
+ res.status(500).json({ error: '添加sso令牌失败' });
1137
+ }
1138
+ });
1139
+ app.post('/hf/delete/token', async (req, res) => {
1140
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1141
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1142
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法删除sso令牌' });
1143
+ } else if (authToken !== CONFIG.API.API_KEY) {
1144
+ return res.status(401).json({ error: 'Unauthorized' });
1145
+ }
1146
+ try {
1147
+ const sso = req.body.sso;
1148
+ await tokenManager.deleteToken(`sso-rw=${sso};sso=${sso}`);
1149
+ res.status(200).json({ message: '删除sso令牌成功' });
1150
+ } catch (error) {
1151
+ Logger.error(error, 'Server');
1152
+ res.status(500).json({ error: '删除sso令牌失败' });
1153
+ }
1154
+ });
1155
+
1156
+ app.get('/hf/v1/models', (req, res) => {
1157
+ res.json({
1158
+ object: "list",
1159
+ data: Object.keys(CONFIG.MODELS).map((model, index) => ({
1160
+ id: model,
1161
+ object: "model",
1162
+ created: Math.floor(Date.now() / 1000),
1163
+ owned_by: "grok",
1164
+ }))
1165
+ });
1166
+ });
1167
+
1168
+
1169
+ app.post('/hf/v1/chat/completions', async (req, res) => {
1170
+ try {
1171
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1172
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1173
+ if (authToken) {
1174
+ const result = `sso=${authToken};ssp_rw=${authToken}`;
1175
+ tokenManager.setToken(result);
1176
+ } else {
1177
+ return res.status(401).json({ error: '自定义的SSO令牌缺失' });
1178
+ }
1179
+ } else if (authToken !== CONFIG.API.API_KEY) {
1180
+ return res.status(401).json({ error: 'Unauthorized' });
1181
+ }
1182
+ const { model, stream } = req.body;
1183
+ let isTempCookie = model.includes("grok-2") && CONFIG.API.IS_TEMP_GROK2;
1184
+ let retryCount = 0;
1185
+ const grokClient = new GrokApiClient(model);
1186
+ const requestPayload = await grokClient.prepareChatRequest(req.body);
1187
+ //Logger.info(`请求体: ${JSON.stringify(requestPayload, null, 2)}`, 'Server');
1188
+
1189
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
1190
+ retryCount++;
1191
+ if (isTempCookie) {
1192
+ CONFIG.API.SIGNATURE_COOKIE = CONFIG.API.TEMP_COOKIE;
1193
+ Logger.info(`已切换为临时令牌`, 'Server');
1194
+ } else {
1195
+ CONFIG.API.SIGNATURE_COOKIE = await Utils.createAuthHeaders(model);
1196
+ }
1197
+ if (!CONFIG.API.SIGNATURE_COOKIE) {
1198
+ throw new Error('该模型无可用令牌');
1199
+ }
1200
+ Logger.info(`当前令牌: ${JSON.stringify(CONFIG.API.SIGNATURE_COOKIE, null, 2)}`, 'Server');
1201
+ Logger.info(`当前可用模型的全部可用数量: ${JSON.stringify(tokenManager.getRemainingTokenRequestCapacity(), null, 2)}`, 'Server');
1202
+ const response = await fetch(`${CONFIG.API.BASE_URL}/rest/app-chat/conversations/new`, {
1203
+ method: 'POST',
1204
+ headers: {
1205
+ ...DEFAULT_HEADERS,
1206
+ "Cookie": CONFIG.API.SIGNATURE_COOKIE
1207
+ },
1208
+ body: JSON.stringify(requestPayload)
1209
+ });
1210
+
1211
+ if (response.ok) {
1212
+ Logger.info(`请求成功`, 'Server');
1213
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1214
+ try {
1215
+ await handleResponse(response, model, res, stream);
1216
+ Logger.info(`请求结束`, 'Server');
1217
+ return;
1218
+ } catch (error) {
1219
+ Logger.error(error, 'Server');
1220
+ if (isTempCookie) {
1221
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1222
+ if (tempCookieManager.cookies.length != 0) {
1223
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1224
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1225
+ tempCookieManager.ensureCookies()
1226
+ } else {
1227
+ try {
1228
+ await tempCookieManager.ensureCookies();
1229
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1230
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1231
+ } catch (error) {
1232
+ throw error;
1233
+ }
1234
+ }
1235
+ } else {
1236
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1237
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1238
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1239
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1240
+ }
1241
+ }
1242
+ }
1243
+ } else {
1244
+ if (response.status === 429) {
1245
+ if (isTempCookie) {
1246
+ // 移除当前失效的 cookie
1247
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1248
+ if (tempCookieManager.cookies.length != 0) {
1249
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1250
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1251
+ tempCookieManager.ensureCookies()
1252
+ } else {
1253
+ try {
1254
+ await tempCookieManager.ensureCookies();
1255
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1256
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1257
+ } catch (error) {
1258
+ throw error;
1259
+ }
1260
+ }
1261
+ } else {
1262
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1263
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1264
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1265
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1266
+ }
1267
+ }
1268
+ } else {
1269
+ // 非429错误直接抛出
1270
+ if (isTempCookie) {
1271
+ // 移除当前失效的 cookie
1272
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1273
+ if (tempCookieManager.cookies.length != 0) {
1274
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1275
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1276
+ tempCookieManager.ensureCookies()
1277
+ } else {
1278
+ try {
1279
+ await tempCookieManager.ensureCookies();
1280
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1281
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1282
+ } catch (error) {
1283
+ throw error;
1284
+ }
1285
+ }
1286
+ } else {
1287
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1288
+ Logger.error(`令牌异常错误状态!status: ${response.status}`, 'Server');
1289
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1290
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+ throw new Error('当前模型所有令牌都已耗尽');
1296
+ } catch (error) {
1297
+ Logger.error(error, 'ChatAPI');
1298
+ res.status(500).json({
1299
+ error: {
1300
+ message: error.message || error,
1301
+ type: 'server_error'
1302
+ }
1303
+ });
1304
+ }
1305
+ });
1306
+
1307
+
1308
+ app.use((req, res) => {
1309
+ res.status(200).send('api运行正常');
1310
+ });
1311
+
1312
+
1313
+ app.listen(CONFIG.SERVER.PORT, () => {
1314
+ Logger.info(`服务器已启动,监听端口: ${CONFIG.SERVER.PORT}`, 'Server');
1315
+ });
logger.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chalk from 'chalk';
2
+ import moment from 'moment';
3
+
4
+ const LogLevel = {
5
+ INFO: 'INFO',
6
+ WARN: 'WARN',
7
+ ERROR: 'ERROR',
8
+ DEBUG: 'DEBUG'
9
+ };
10
+
11
+ class Logger {
12
+ static formatMessage(level, message) {
13
+ const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
14
+
15
+ switch(level) {
16
+ case LogLevel.INFO:
17
+ return chalk.blue(`[${timestamp}] [${level}] ${message}`);
18
+ case LogLevel.WARN:
19
+ return chalk.yellow(`[${timestamp}] [${level}] ${message}`);
20
+ case LogLevel.ERROR:
21
+ return chalk.red(`[${timestamp}] [${level}] ${message}`);
22
+ case LogLevel.DEBUG:
23
+ return chalk.gray(`[${timestamp}] [${level}] ${message}`);
24
+ default:
25
+ return message;
26
+ }
27
+ }
28
+
29
+ static info(message, context) {
30
+ console.log(this.formatMessage(LogLevel.INFO, context ? `[${context}] ${message}` : message));
31
+ }
32
+
33
+ static warn(message, context) {
34
+ console.warn(this.formatMessage(LogLevel.WARN, context ? `[${context}] ${message}` : message));
35
+ }
36
+
37
+ static error(message, context, error = null) {
38
+ const errorMessage = error ? ` - ${error.message}` : '';
39
+ console.error(this.formatMessage(LogLevel.ERROR, `${context ? `[${context}] ` : ''}${message}${errorMessage}`));
40
+ }
41
+
42
+ static debug(message, context) {
43
+ if (process.env.NODE_ENV === 'development') {
44
+ console.debug(this.formatMessage(LogLevel.DEBUG, context ? `[${context}] ${message}` : message));
45
+ }
46
+ }
47
+
48
+ static requestLogger(req, res, next) {
49
+ const startTime = Date.now();
50
+
51
+ res.on('finish', () => {
52
+ const duration = Date.now() - startTime;
53
+ const logMessage = `${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`;
54
+
55
+ if (res.statusCode >= 400) {
56
+ Logger.error(logMessage, undefined, 'HTTP');
57
+ } else {
58
+ Logger.info(logMessage, 'HTTP');
59
+ }
60
+ });
61
+
62
+ next();
63
+ }
64
+ }
65
+
66
+ export default Logger;
package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "grok2api",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "author": "yxmiler",
10
+ "dependencies": {
11
+ "express": "^4.18.2",
12
+ "node-fetch": "^3.3.2",
13
+ "dotenv": "^16.3.1",
14
+ "cors": "^2.8.5",
15
+ "form-data": "^4.0.0",
16
+ "puppeteer": "^22.8.2",
17
+ "puppeteer-extra": "^3.3.6",
18
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
19
+ "moment": "^2.30.1",
20
+ "chalk": "^5.4.1",
21
+ "uuid": "^9.0.0"
22
+ }
23
+ }