jiffyt commited on
Commit
8e04ba2
·
verified ·
1 Parent(s): 31014d2

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +32 -0
  2. README.md +12 -0
  3. index.js +945 -0
  4. logger.js +66 -0
  5. package.json +23 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 CHROME_PATH=/usr/bin/chromium
26
+ ENV PORT=7860
27
+
28
+ # 暴露端口
29
+ EXPOSE 7860
30
+
31
+ # 启动应用
32
+ CMD ["npm", "start"]
README.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,945 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_CUSTOM_SSO: process.env.IS_CUSTOM_SSO === 'true',
27
+ BASE_URL: "https://grok.com",
28
+ API_KEY: process.env.API_KEY || "sk-123456",
29
+ SIGNATURE_COOKIE: null,
30
+ TEMP_COOKIE: null,
31
+ PICGO_KEY: process.env.PICGO_KEY || null, //想要流式生图的话需要填入这个PICGO图床的key
32
+ TUMY_KEY: process.env.TUMY_KEY || null //想要流式生图的话需要填入这个TUMY图床的key 两个图床二选一,默认使用PICGO
33
+ },
34
+ SERVER: {
35
+ PORT: process.env.PORT || 3000,
36
+ BODY_LIMIT: '5mb'
37
+ },
38
+ RETRY: {
39
+ MAX_ATTEMPTS: 2//重试次数
40
+ },
41
+ SHOW_THINKING: process.env.SHOW_THINKING === 'true',
42
+ IS_THINKING: false,
43
+ IS_IMG_GEN: false,
44
+ IS_IMG_GEN2: false,
45
+ SSO_INDEX: 0,//sso的索引
46
+ ISSHOW_SEARCH_RESULTS: process.env.ISSHOW_SEARCH_RESULTS === 'true',//是否显示搜索结果
47
+ CHROME_PATH: process.env.CHROME_PATH || "/usr/bin/chromium"//chrome路径
48
+ };
49
+ puppeteer.use(StealthPlugin())
50
+ // 请求头配置
51
+ const DEFAULT_HEADERS = {
52
+ 'accept': '*/*',
53
+ 'accept-language': 'zh-CN,zh;q=0.9',
54
+ 'accept-encoding': 'gzip, deflate, br, zstd',
55
+ 'content-type': 'text/plain;charset=UTF-8',
56
+ 'Connection': 'keep-alive',
57
+ 'origin': 'https://grok.com',
58
+ 'priority': 'u=1, i',
59
+ 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
60
+ 'sec-ch-ua-mobile': '?0',
61
+ 'sec-ch-ua-platform': '"Windows"',
62
+ 'sec-fetch-dest': 'empty',
63
+ 'sec-fetch-mode': 'cors',
64
+ 'sec-fetch-site': 'same-origin',
65
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
66
+ 'baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
67
+ };
68
+
69
+
70
+ async function initialization() {
71
+ if (CONFIG.API.IS_CUSTOM_SSO) {
72
+ await Utils.get_signature()
73
+ return;
74
+ }
75
+ const ssoArray = process.env.SSO.split(',');
76
+ ssoArray.forEach((sso) => {
77
+ tokenManager.addToken(`sso-rw=${sso};sso=${sso}`);
78
+ });
79
+ console.log(JSON.stringify(tokenManager.getActiveTokens(), null, 2));
80
+ await Utils.get_signature()
81
+ Logger.info("初始化完成", 'Server');
82
+ }
83
+
84
+
85
+ class AuthTokenManager {
86
+ constructor() {
87
+ this.activeTokens = [];
88
+ this.expiredTokens = new Map();
89
+ this.tokenModelFrequency = new Map();
90
+ this.modelRateLimit = {
91
+ "grok-3": { RequestFrequency: 20 },
92
+ "grok-3-deepsearch": { RequestFrequency: 5 },
93
+ "grok-3-reasoning": { RequestFrequency: 5 }
94
+ };
95
+ }
96
+
97
+ addToken(token) {
98
+ if (!this.activeTokens.includes(token)) {
99
+ this.activeTokens.push(token);
100
+ this.tokenModelFrequency.set(token, {
101
+ "grok-3": 0,
102
+ "grok-3-deepsearch": 0,
103
+ "grok-3-reasoning": 0
104
+ });
105
+ }
106
+ }
107
+ setToken(token) {
108
+ this.activeTokens = [token];
109
+ this.tokenModelFrequency.set(token, {
110
+ "grok-3": 0,
111
+ "grok-3-deepsearch": 0,
112
+ "grok-3-reasoning": 0
113
+ });
114
+ }
115
+
116
+ getTokenByIndex(index, model) {
117
+ if (this.activeTokens.length === 0) {
118
+ return null;
119
+ }
120
+ const token = this.activeTokens[index];
121
+ this.recordModelRequest(token, model);
122
+ return token;
123
+ }
124
+
125
+ recordModelRequest(token, model) {
126
+ if (model === 'grok-3-search' || model === 'grok-3-imageGen') {
127
+ model = 'grok-3';
128
+ }
129
+
130
+ if (!this.modelRateLimit[model]) return;
131
+ const tokenFrequency = this.tokenModelFrequency.get(token);
132
+ if (tokenFrequency && tokenFrequency[model] !== undefined) {
133
+ tokenFrequency[model]++;
134
+ }
135
+ this.checkAndRemoveTokenIfLimitReached(token);
136
+ }
137
+ setModelLimit(index, model) {
138
+ if (model === 'grok-3-search' || model === 'grok-3-imageGen') {
139
+ model = 'grok-3';
140
+ }
141
+ if (!this.modelRateLimit[model]) return;
142
+ const tokenFrequency = this.tokenModelFrequency.get(this.activeTokens[index]);
143
+ tokenFrequency[model] = 9999;
144
+ }
145
+ isTokenModelLimitReached(index, model) {
146
+ if (model === 'grok-3-search' || model === 'grok-3-imageGen') {
147
+ model = 'grok-3';
148
+ }
149
+ if (!this.modelRateLimit[model]) return;
150
+ const token = this.activeTokens[index];
151
+ const tokenFrequency = this.tokenModelFrequency.get(token);
152
+
153
+ if (!tokenFrequency) {
154
+ return false;
155
+ }
156
+ return tokenFrequency[model] >= this.modelRateLimit[model].RequestFrequency;
157
+ }
158
+ checkAndRemoveTokenIfLimitReached(token) {
159
+ const tokenFrequency = this.tokenModelFrequency.get(token);
160
+ if (!tokenFrequency) return;
161
+
162
+ const isLimitReached = Object.keys(tokenFrequency).every(model =>
163
+ tokenFrequency[model] >= this.modelRateLimit[model].RequestFrequency
164
+ );
165
+
166
+ if (isLimitReached) {
167
+ const tokenIndex = this.activeTokens.indexOf(token);
168
+ if (tokenIndex !== -1) {
169
+ this.removeTokenByIndex(tokenIndex);
170
+ }
171
+ }
172
+ }
173
+
174
+ removeTokenByIndex(index) {
175
+ if (!this.isRecoveryProcess) {
176
+ this.startTokenRecoveryProcess();
177
+ }
178
+ const token = this.activeTokens[index];
179
+ this.expiredTokens.set(token, Date.now());
180
+ this.activeTokens.splice(index, 1);
181
+ this.tokenModelFrequency.delete(token);
182
+ Logger.info(`令牌${token}已达到上限,已移除`, 'TokenManager');
183
+ }
184
+
185
+ startTokenRecoveryProcess() {
186
+ if (CONFIG.API.IS_CUSTOM_SSO) return;
187
+ setInterval(() => {
188
+ const now = Date.now();
189
+ for (const [token, expiredTime] of this.expiredTokens.entries()) {
190
+ if (now - expiredTime >= 2 * 60 * 60 * 1000) {
191
+ this.tokenModelUsage.set(token, {
192
+ "grok-3": 0,
193
+ "grok-3-deepsearch": 0,
194
+ "grok-3-reasoning": 0
195
+ });
196
+ this.activeTokens.push(token);
197
+ this.expiredTokens.delete(token);
198
+ Logger.info(`令牌${token}已恢复,已添加到可用令牌列表`, 'TokenManager');
199
+ }
200
+ }
201
+ }, 2 * 60 * 60 * 1000);
202
+ }
203
+
204
+ getTokenCount() {
205
+ return this.activeTokens.length || 0;
206
+ }
207
+
208
+ getActiveTokens() {
209
+ return [...this.activeTokens];
210
+ }
211
+ }
212
+
213
+ class Utils {
214
+ static async extractGrokHeaders() {
215
+ Logger.info("开始提取头信息", 'Server');
216
+ try {
217
+ const browser = await puppeteer.launch({
218
+ headless: true,
219
+ args: [
220
+ '--no-sandbox',
221
+ '--disable-setuid-sandbox',
222
+ '--disable-dev-shm-usage',
223
+ '--disable-gpu'
224
+ ],
225
+ executablePath: CONFIG.CHROME_PATH
226
+ });
227
+
228
+ const page = await browser.newPage();
229
+ await page.goto('https://grok.com/', { waitUntil: 'domcontentloaded' });
230
+ await page.evaluate(() => {
231
+ return new Promise(resolve => setTimeout(resolve, 5000))
232
+ })
233
+ // 获取所有 Cookies
234
+ const cookies = await page.cookies();
235
+ const targetHeaders = ['x-anonuserid', 'x-challenge', 'x-signature'];
236
+ const extractedHeaders = {};
237
+ for (const cookie of cookies) {
238
+ if (targetHeaders.includes(cookie.name.toLowerCase())) {
239
+ extractedHeaders[cookie.name.toLowerCase()] = cookie.value;
240
+ }
241
+ }
242
+ await browser.close();
243
+ Logger.info('提取的头信息:', JSON.stringify(extractedHeaders, null, 2), 'Server');
244
+ return extractedHeaders;
245
+
246
+ } catch (error) {
247
+ Logger.error('获取头信息出错:', error, 'Server');
248
+ return null;
249
+ }
250
+ }
251
+ static async get_signature() {
252
+ if (CONFIG.API.TEMP_COOKIE) {
253
+ return CONFIG.API.TEMP_COOKIE;
254
+ }
255
+ Logger.info("刷新认证信息", 'Server');
256
+ let retryCount = 0;
257
+ while (!CONFIG.API.TEMP_COOKIE || retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
258
+ let headers = await Utils.extractGrokHeaders();
259
+ if (headers) {
260
+ Logger.info("获取认证信息成功", 'Server');
261
+ CONFIG.API.TEMP_COOKIE = { cookie: `x-anonuserid=${headers["x-anonuserid"]}; x-challenge=${headers["x-challenge"]}; x-signature=${headers["x-signature"]}` };
262
+ return;
263
+ }
264
+ retryCount++;
265
+ if (retryCount >= CONFIG.RETRY.MAX_ATTEMPTS) {
266
+ throw new Error(`获取认证信息失败!`);
267
+ }
268
+ }
269
+ }
270
+ static async organizeSearchResults(searchResults) {
271
+ // 确保传入的是有效的搜索结果对象
272
+ if (!searchResults || !searchResults.results) {
273
+ return '';
274
+ }
275
+
276
+ const results = searchResults.results;
277
+ const formattedResults = results.map((result, index) => {
278
+ // 处理可能为空的字段
279
+ const title = result.title || '未知标题';
280
+ const url = result.url || '#';
281
+ const preview = result.preview || '无预览内容';
282
+
283
+ return `\r\n<details><summary>资料[${index}]: ${title}</summary>\r\n${preview}\r\n\n[Link](${url})\r\n</details>`;
284
+ });
285
+ return formattedResults.join('\n\n');
286
+ }
287
+ static async createAuthHeaders(model) {
288
+ return {
289
+ 'cookie': `${await tokenManager.getTokenByIndex(CONFIG.SSO_INDEX, model)}`
290
+ };
291
+ }
292
+ }
293
+
294
+
295
+ class GrokApiClient {
296
+ constructor(modelId) {
297
+ if (!CONFIG.MODELS[modelId]) {
298
+ throw new Error(`不支持的模型: ${modelId}`);
299
+ }
300
+ this.modelId = CONFIG.MODELS[modelId];
301
+ }
302
+
303
+ processMessageContent(content) {
304
+ if (typeof content === 'string') return content;
305
+ return null;
306
+ }
307
+ // 获取图片类型
308
+ getImageType(base64String) {
309
+ let mimeType = 'image/jpeg';
310
+ if (base64String.includes('data:image')) {
311
+ const matches = base64String.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,/);
312
+ if (matches) {
313
+ mimeType = matches[1];
314
+ }
315
+ }
316
+ const extension = mimeType.split('/')[1];
317
+ const fileName = `image.${extension}`;
318
+
319
+ return {
320
+ mimeType: mimeType,
321
+ fileName: fileName
322
+ };
323
+ }
324
+
325
+ async uploadBase64Image(base64Data, url) {
326
+ try {
327
+ // 处理 base64 数据
328
+ let imageBuffer;
329
+ if (base64Data.includes('data:image')) {
330
+ imageBuffer = base64Data.split(',')[1];
331
+ } else {
332
+ imageBuffer = base64Data
333
+ }
334
+ const { mimeType, fileName } = this.getImageType(base64Data);
335
+ let uploadData = {
336
+ rpc: "uploadFile",
337
+ req: {
338
+ fileName: fileName,
339
+ fileMimeType: mimeType,
340
+ content: imageBuffer
341
+ }
342
+ };
343
+ Logger.info("发送图片请求", 'Server');
344
+ // 发送请求
345
+ const response = await fetch(url, {
346
+ method: 'POST',
347
+ headers: {
348
+ ...CONFIG.DEFAULT_HEADERS,
349
+ ...CONFIG.API.SIGNATURE_COOKIE
350
+ },
351
+ body: JSON.stringify(uploadData)
352
+ });
353
+
354
+ if (!response.ok) {
355
+ Logger.error(`上传图片失败,状态码:${response.status},原因:${response.error}`, 'Server');
356
+ return '';
357
+ }
358
+
359
+ const result = await response.json();
360
+ Logger.info('上传图片成功:', result, 'Server');
361
+ return result.fileMetadataId;
362
+
363
+ } catch (error) {
364
+ Logger.error(error, 'Server');
365
+ return '';
366
+ }
367
+ }
368
+
369
+ async prepareChatRequest(request) {
370
+ if ((request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') && !CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY && request.stream) {
371
+ throw new Error(`该模型流式输出需要配置PICGO或者PICUI图床密钥!`);
372
+ }
373
+
374
+ // 处理画图模型的消息限制
375
+ let todoMessages = request.messages;
376
+ if (request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') {
377
+ const lastMessage = todoMessages[todoMessages.length - 1];
378
+ if (lastMessage.role !== 'user') {
379
+ throw new Error('画图模型的最后一条消息必须是用户消息!');
380
+ }
381
+ todoMessages = [lastMessage];
382
+ }
383
+
384
+ const fileAttachments = [];
385
+ let messages = '';
386
+ let lastRole = null;
387
+ let lastContent = '';
388
+ const search = request.model === 'grok-2-search' || request.model === 'grok-3-search';
389
+
390
+ // 移除<think>标签及其内容和base64图片
391
+ const removeThinkTags = (text) => {
392
+ text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
393
+ text = text.replace(/!\[image\]\(data:.*?base64,.*?\)/g, '[图片]');
394
+ return text;
395
+ };
396
+
397
+ const processImageUrl = async (content) => {
398
+ if (content.type === 'image_url' && content.image_url.url.includes('data:image')) {
399
+ const imageResponse = await this.uploadBase64Image(
400
+ content.image_url.url,
401
+ `${CONFIG.API.BASE_URL}/api/rpc`
402
+ );
403
+ return imageResponse;
404
+ }
405
+ return null;
406
+ };
407
+
408
+ const processContent = async (content) => {
409
+ if (Array.isArray(content)) {
410
+ let textContent = '';
411
+ for (const item of content) {
412
+ if (item.type === 'image_url') {
413
+ textContent += (textContent ? '\n' : '') + "[图片]";
414
+ } else if (item.type === 'text') {
415
+ textContent += (textContent ? '\n' : '') + removeThinkTags(item.text);
416
+ }
417
+ }
418
+ return textContent;
419
+ } else if (typeof content === 'object' && content !== null) {
420
+ if (content.type === 'image_url') {
421
+ return "[图片]";
422
+ } else if (content.type === 'text') {
423
+ return removeThinkTags(content.text);
424
+ }
425
+ }
426
+ return removeThinkTags(this.processMessageContent(content));
427
+ };
428
+
429
+ for (const current of todoMessages) {
430
+ const role = current.role === 'assistant' ? 'assistant' : 'user';
431
+ const isLastMessage = current === todoMessages[todoMessages.length - 1];
432
+
433
+ // 处理图片附件
434
+ if (isLastMessage && current.content) {
435
+ if (Array.isArray(current.content)) {
436
+ for (const item of current.content) {
437
+ if (item.type === 'image_url') {
438
+ const processedImage = await processImageUrl(item);
439
+ if (processedImage) fileAttachments.push(processedImage);
440
+ }
441
+ }
442
+ } else if (current.content.type === 'image_url') {
443
+ const processedImage = await processImageUrl(current.content);
444
+ if (processedImage) fileAttachments.push(processedImage);
445
+ }
446
+ }
447
+
448
+ // 处理文本内容
449
+ const textContent = await processContent(current.content);
450
+
451
+ if (textContent || (isLastMessage && fileAttachments.length > 0)) {
452
+ if (role === lastRole && textContent) {
453
+ lastContent += '\n' + textContent;
454
+ messages = messages.substring(0, messages.lastIndexOf(`${role.toUpperCase()}: `)) +
455
+ `${role.toUpperCase()}: ${lastContent}\n`;
456
+ } else {
457
+ messages += `${role.toUpperCase()}: ${textContent || '[图片]'}\n`;
458
+ lastContent = textContent;
459
+ lastRole = role;
460
+ }
461
+ }
462
+ }
463
+
464
+ return {
465
+ modelName: this.modelId,
466
+ message: messages.trim(),
467
+ fileAttachments: fileAttachments.slice(0, 4),
468
+ imageAttachments: [],
469
+ disableSearch: false,
470
+ enableImageGeneration: true,
471
+ returnImageBytes: false,
472
+ returnRawGrokInXaiRequest: false,
473
+ enableImageStreaming: false,
474
+ imageGenerationCount: 1,
475
+ forceConcise: false,
476
+ toolOverrides: {
477
+ imageGen: request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen',
478
+ webSearch: search,
479
+ xSearch: search,
480
+ xMediaSearch: search,
481
+ trendsSearch: search,
482
+ xPostAnalyze: search
483
+ },
484
+ enableSideBySide: true,
485
+ isPreset: false,
486
+ sendFinalMetadata: true,
487
+ customInstructions: "",
488
+ deepsearchPreset: request.model === 'grok-3-deepsearch' ? "default" : "",
489
+ isReasoning: request.model === 'grok-3-reasoning'
490
+ };
491
+ }
492
+ }
493
+
494
+ class MessageProcessor {
495
+ static createChatResponse(message, model, isStream = false) {
496
+ const baseResponse = {
497
+ id: `chatcmpl-${uuidv4()}`,
498
+ created: Math.floor(Date.now() / 1000),
499
+ model: model
500
+ };
501
+
502
+ if (isStream) {
503
+ return {
504
+ ...baseResponse,
505
+ object: 'chat.completion.chunk',
506
+ choices: [{
507
+ index: 0,
508
+ delta: {
509
+ content: message
510
+ }
511
+ }]
512
+ };
513
+ }
514
+
515
+ return {
516
+ ...baseResponse,
517
+ object: 'chat.completion',
518
+ choices: [{
519
+ index: 0,
520
+ message: {
521
+ role: 'assistant',
522
+ content: message
523
+ },
524
+ finish_reason: 'stop'
525
+ }],
526
+ usage: null
527
+ };
528
+ }
529
+ }
530
+ async function processModelResponse(linejosn, model) {
531
+ let result = { token: null, imageUrl: null }
532
+ if (CONFIG.IS_IMG_GEN) {
533
+ if (linejosn?.cachedImageGenerationResponse && !CONFIG.IS_IMG_GEN2) {
534
+ result.imageUrl = linejosn.cachedImageGenerationResponse.imageUrl;
535
+ }
536
+ return result;
537
+ }
538
+
539
+ //非生图模型的处理
540
+ switch (model) {
541
+ case 'grok-2':
542
+ result.token = linejosn?.token;
543
+ return result;
544
+ case 'grok-2-search':
545
+ case 'grok-3-search':
546
+ if (linejosn?.webSearchResults && CONFIG.ISSHOW_SEARCH_RESULTS) {
547
+ result.token = `\r\n<think>${await Utils.organizeSearchResults(linejosn.webSearchResults)}</think>\r\n`;
548
+ } else {
549
+ result.token = linejosn?.token;
550
+ }
551
+ return result;
552
+ case 'grok-3':
553
+ result.token = linejosn?.token;
554
+ return result;
555
+ case 'grok-3-deepsearch':
556
+ if (linejosn.messageTag === "final") {
557
+ result.token = linejosn?.token;
558
+ }
559
+ return result;
560
+ case 'grok-3-reasoning':
561
+ if (linejosn?.isThinking && !CONFIG.SHOW_THINKING) return result;
562
+
563
+ if (linejosn?.isThinking && !CONFIG.IS_THINKING) {
564
+ result.token = "<think>" + linejosn?.token;
565
+ CONFIG.IS_THINKING = true;
566
+ } else if (!linejosn.isThinking && CONFIG.IS_THINKING) {
567
+ result.token = "</think>" + linejosn?.token;
568
+ CONFIG.IS_THINKING = false;
569
+ } else {
570
+ result.token = linejosn?.token;
571
+ }
572
+ return result;
573
+ }
574
+ return result;
575
+ }
576
+
577
+ async function handleResponse(response, model, res, isStream) {
578
+ try {
579
+ const stream = response.body;
580
+ let buffer = '';
581
+ let fullResponse = '';
582
+ const dataPromises = [];
583
+ if (isStream) {
584
+ res.setHeader('Content-Type', 'text/event-stream');
585
+ res.setHeader('Cache-Control', 'no-cache');
586
+ res.setHeader('Connection', 'keep-alive');
587
+ }
588
+ CONFIG.IS_IMG_GEN = false;
589
+ CONFIG.IS_IMG_GEN2 = false;
590
+ CONFIG.IS_THINKING =false;
591
+ Logger.info("开始处理流式响应", 'Server');
592
+
593
+ return new Promise((resolve, reject) => {
594
+ stream.on('data', async (chunk) => {
595
+ buffer += chunk.toString();
596
+ const lines = buffer.split('\n');
597
+ buffer = lines.pop() || '';
598
+
599
+ for (const line of lines) {
600
+ if (!line.trim()) continue;
601
+ const trimmedLine = line.trim();
602
+ if (trimmedLine.startsWith('data: ')) {
603
+ const data = trimmedLine.substring(6);
604
+ try {
605
+ if (!data.trim()) continue;
606
+ if (data === "[DONE]") continue;
607
+ const linejosn = JSON.parse(data);
608
+ if (linejosn?.error) {
609
+ Logger.error(JSON.stringify(linejosn, null, 2), 'Server');
610
+ if(linejosn.error?.name === "RateLimitError"){
611
+ CONFIG.API.TEMP_COOKIE = null;
612
+ }
613
+ stream.destroy();
614
+ reject(new Error("RateLimitError"));
615
+ return;
616
+ }
617
+ if (linejosn?.doImgGen || linejosn?.imageAttachmentInfo) {
618
+ CONFIG.IS_IMG_GEN = true;
619
+ }
620
+ const processPromise = (async () => {
621
+ const result = await processModelResponse(linejosn, model);
622
+
623
+ if (result.token) {
624
+ if (isStream) {
625
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(result.token, model, true))}\n\n`);
626
+ } else {
627
+ fullResponse += result.token;
628
+ }
629
+ }
630
+ if (result.imageUrl) {
631
+ CONFIG.IS_IMG_GEN2 = true;
632
+ const dataImage = await handleImageResponse(result.imageUrl);
633
+ if (isStream) {
634
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(dataImage, model, true))}\n\n`);
635
+ } else {
636
+ res.json(MessageProcessor.createChatResponse(dataImage, model));
637
+ }
638
+ }
639
+ })();
640
+ dataPromises.push(processPromise);
641
+ } catch (error) {
642
+ Logger.error(error, 'Server');
643
+ continue;
644
+ }
645
+ }
646
+ }
647
+ });
648
+
649
+ stream.on('end', async () => {
650
+ try {
651
+ await Promise.all(dataPromises);
652
+ if (isStream) {
653
+ res.write('data: [DONE]\n\n');
654
+ res.end();
655
+ } else {
656
+ if (!CONFIG.IS_IMG_GEN2) {
657
+ res.json(MessageProcessor.createChatResponse(fullResponse, model));
658
+ }
659
+ }
660
+ resolve();
661
+ } catch (error) {
662
+ Logger.error(error, 'Server');
663
+ reject(error);
664
+ }
665
+ });
666
+
667
+ stream.on('error', (error) => {
668
+ Logger.error(error, 'Server');
669
+ reject(error);
670
+ });
671
+ });
672
+ } catch (error) {
673
+ Logger.error(error, 'Server');
674
+ throw new Error(error);
675
+ }
676
+ }
677
+
678
+ async function handleImageResponse(imageUrl) {
679
+ const MAX_RETRIES = 2;
680
+ let retryCount = 0;
681
+ let imageBase64Response;
682
+
683
+ while (retryCount < MAX_RETRIES) {
684
+ try {
685
+ imageBase64Response = await fetch(`https://assets.grok.com/${imageUrl}`, {
686
+ method: 'GET',
687
+ headers: {
688
+ ...DEFAULT_HEADERS,
689
+ ...CONFIG.API.SIGNATURE_COOKIE
690
+ }
691
+ });
692
+
693
+ if (imageBase64Response.ok) break;
694
+ retryCount++;
695
+ if (retryCount === MAX_RETRIES) {
696
+ throw new Error(`上游服务请求失败! status: ${imageBase64Response.status}`);
697
+ }
698
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
699
+
700
+ } catch (error) {
701
+ Logger.error(error, 'Server');
702
+ retryCount++;
703
+ if (retryCount === MAX_RETRIES) {
704
+ throw error;
705
+ }
706
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
707
+ }
708
+ }
709
+
710
+
711
+ const arrayBuffer = await imageBase64Response.arrayBuffer();
712
+ const imageBuffer = Buffer.from(arrayBuffer);
713
+
714
+ if (!CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY) {
715
+ const base64Image = imageBuffer.toString('base64');
716
+ const imageContentType = imageBase64Response.headers.get('content-type');
717
+ return `![image](data:${imageContentType};base64,${base64Image})`
718
+ }
719
+
720
+ Logger.info("开始请求图床", 'Server');
721
+ const formData = new FormData();
722
+ if(CONFIG.API.PICGO_KEY){
723
+ formData.append('source', imageBuffer, {
724
+ filename: `image-${Date.now()}.jpg`,
725
+ contentType: 'image/jpeg'
726
+ });
727
+ const formDataHeaders = formData.getHeaders();
728
+ const responseURL = await fetch("https://www.picgo.net/api/1/upload", {
729
+ method: "POST",
730
+ headers: {
731
+ ...formDataHeaders,
732
+ "Content-Type": "multipart/form-data",
733
+ "X-API-Key": CONFIG.API.PICGO_KEY
734
+ },
735
+ body: formData
736
+ });
737
+ if (!responseURL.ok) {
738
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
739
+ } else {
740
+ Logger.info("生图成功", 'Server');
741
+ const result = await responseURL.json();
742
+ return `![image](${result.image.url})`
743
+ }
744
+ }else if(CONFIG.API.TUMY_KEY){
745
+ const formData = new FormData();
746
+ formData.append('file', imageBuffer, {
747
+ filename: `image-${Date.now()}.jpg`,
748
+ contentType: 'image/jpeg'
749
+ });
750
+ const formDataHeaders = formData.getHeaders();
751
+ const responseURL = await fetch("https://tu.my/api/v1/upload", {
752
+ method: "POST",
753
+ headers: {
754
+ ...formDataHeaders,
755
+ "Accept": "application/json",
756
+ 'Authorization': `Bearer ${CONFIG.API.TUMY_KEY}`
757
+ },
758
+ body: formData
759
+ });
760
+ if (!responseURL.ok) {
761
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
762
+ } else {
763
+ Logger.info("生图成功", 'Server');
764
+ const result = await responseURL.json();
765
+ try {
766
+ var url = `![image](${result.data.links.url})`
767
+ return url;
768
+ } catch (error) {
769
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
770
+ }
771
+ }
772
+ }
773
+ }
774
+
775
+ const tokenManager = new AuthTokenManager();
776
+ await initialization();
777
+
778
+ // 中间件配置
779
+ const app = express();
780
+ app.use(Logger.requestLogger);
781
+ app.use(express.json({ limit: CONFIG.SERVER.BODY_LIMIT }));
782
+ app.use(express.urlencoded({ extended: true, limit: CONFIG.SERVER.BODY_LIMIT }));
783
+ app.use(cors({
784
+ origin: '*',
785
+ methods: ['GET', 'POST', 'OPTIONS'],
786
+ allowedHeaders: ['Content-Type', 'Authorization']
787
+ }));
788
+
789
+ app.get('/hf/v1/models', (req, res) => {
790
+ res.json({
791
+ object: "list",
792
+ data: Object.keys(CONFIG.MODELS).map((model, index) => ({
793
+ id: model,
794
+ object: "model",
795
+ created: Math.floor(Date.now() / 1000),
796
+ owned_by: "grok",
797
+ }))
798
+ });
799
+ });
800
+
801
+
802
+ app.post('/hf/v1/chat/completions', async (req, res) => {
803
+ try {
804
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
805
+ if (CONFIG.API.IS_CUSTOM_SSO) {
806
+ if (authToken) {
807
+ const result = `sso=${authToken};ssp_rw=${authToken}`;
808
+ tokenManager.setToken(result);
809
+ } else {
810
+ return res.status(401).json({ error: '自定义的SSO令牌缺失' });
811
+ }
812
+ } else if (authToken !== CONFIG.API.API_KEY) {
813
+ return res.status(401).json({ error: 'Unauthorized' });
814
+ }
815
+ let isTempCookie = req.body.model.includes("grok-2");
816
+ let retryCount = 0;
817
+ const grokClient = new GrokApiClient(req.body.model);
818
+ const requestPayload = await grokClient.prepareChatRequest(req.body);
819
+ Logger.info(`请求体: ${JSON.stringify(requestPayload,null,2)}`, 'Server');
820
+
821
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
822
+ retryCount++;
823
+ if (!CONFIG.API.TEMP_COOKIE) {
824
+ await Utils.get_signature();
825
+ }
826
+
827
+ if (isTempCookie) {
828
+ CONFIG.API.SIGNATURE_COOKIE = CONFIG.API.TEMP_COOKIE;
829
+ Logger.info(`已切换为临时令牌`, 'Server');
830
+ } else {
831
+ CONFIG.API.SIGNATURE_COOKIE = await Utils.createAuthHeaders(req.body.model);
832
+ }
833
+ if (!CONFIG.API.SIGNATURE_COOKIE) {
834
+ throw new Error('该模型无可用令牌');
835
+ }
836
+ Logger.info(`当前令牌索引: ${CONFIG.SSO_INDEX}`, 'Server');
837
+ Logger.info(`当前令牌: ${JSON.stringify(CONFIG.API.SIGNATURE_COOKIE,null,2)}`, 'Server');
838
+ const newMessageReq = await fetch(`${CONFIG.API.BASE_URL}/api/rpc`, {
839
+ method: 'POST',
840
+ headers: {
841
+ ...DEFAULT_HEADERS,
842
+ ...CONFIG.API.SIGNATURE_COOKIE
843
+ },
844
+ body: JSON.stringify({
845
+ rpc: "createConversation",
846
+ req: {
847
+ temporary: false
848
+ }
849
+ })
850
+ });
851
+ let conversationId;
852
+ var responseText2 = await newMessageReq.clone().text();
853
+ if (newMessageReq.status === 200) {
854
+ const responseText = await newMessageReq.json();
855
+ conversationId = responseText.conversationId;
856
+ } else {
857
+ Logger.error(`创建会话响应错误: ${responseText2}`, 'Server');
858
+ throw new Error(`创建会话响应错误: ${responseText2}`);
859
+ }
860
+
861
+ const response = await fetch(`${CONFIG.API.BASE_URL}/api/conversations/${conversationId}/responses`, {
862
+ method: 'POST',
863
+ headers: {
864
+ "accept": "text/event-stream",
865
+ "baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
866
+ "content-type": "text/plain;charset=UTF-8",
867
+ "Connection": "keep-alive",
868
+ ...CONFIG.API.SIGNATURE_COOKIE
869
+ },
870
+ body: JSON.stringify(requestPayload)
871
+ });
872
+
873
+ if (response.ok) {
874
+ Logger.info(`请求成功`, 'Server');
875
+ CONFIG.SSO_INDEX = (CONFIG.SSO_INDEX + 1) % tokenManager.getTokenCount();
876
+ Logger.info(`当前剩余可用令牌数: ${tokenManager.getTokenCount()}`, 'Server');
877
+ try {
878
+ await handleResponse(response, req.body.model, res, req.body.stream);
879
+ Logger.info(`请求结束`, 'Server');
880
+ return;
881
+ } catch (error) {
882
+ Logger.error(error, 'Server');
883
+ if (isTempCookie) {
884
+ await Utils.get_signature();
885
+ } else {
886
+ tokenManager.setModelLimit(CONFIG.SSO_INDEX, req.body.model);
887
+ for (let i = 1; i <= tokenManager.getTokenCount(); i++) {
888
+ CONFIG.SSO_INDEX = (CONFIG.SSO_INDEX + 1) % tokenManager.getTokenCount();
889
+ if (!tokenManager.isTokenModelLimitReached(CONFIG.SSO_INDEX, req.body.model)) {
890
+ break;
891
+ } else if (i >= tokenManager.getTokenCount()) {
892
+ throw new Error(`${req.body.model} 次数已达上限,请切换其他模型或者重新对话`);
893
+ }
894
+ }
895
+ }
896
+ }
897
+ } else {
898
+ if (response.status === 429) {
899
+ if (isTempCookie) {
900
+ await Utils.get_signature();
901
+ } else {
902
+ tokenManager.setModelLimit(CONFIG.SSO_INDEX, req.body.model);
903
+ for (let i = 1; i <= tokenManager.getTokenCount(); i++) {
904
+ CONFIG.SSO_INDEX = (CONFIG.SSO_INDEX + 1) % tokenManager.getTokenCount();
905
+ if (!tokenManager.isTokenModelLimitReached(CONFIG.SSO_INDEX, req.body.model)) {
906
+ break;
907
+ } else if (i >= tokenManager.getTokenCount()) {
908
+ throw new Error(`${req.body.model} 次数已达上限,请切换其他模型或者重新对话`);
909
+ }
910
+ }
911
+ }
912
+ } else {
913
+ // 非429错误直接抛出
914
+ if (isTempCookie) {
915
+ await Utils.get_signature();
916
+ } else {
917
+ Logger.error(`令牌异常错误状态!status: ${response.status}, 已移除当前令牌${CONFIG.SSO_INDEX.cookie}`, 'Server');
918
+ tokenManager.removeTokenByIndex(CONFIG.SSO_INDEX);
919
+ Logger.info(`当前剩余可用令牌数: ${tokenManager.getTokenCount()}`, 'Server');
920
+ CONFIG.SSO_INDEX = (CONFIG.SSO_INDEX + 1) % tokenManager.getTokenCount();
921
+ }
922
+ }
923
+ }
924
+ }
925
+ throw new Error('当前模型所有令牌都已耗尽');
926
+ } catch (error) {
927
+ Logger.error(error, 'ChatAPI');
928
+ res.status(500).json({
929
+ error: {
930
+ message: error.message || error,
931
+ type: 'server_error'
932
+ }
933
+ });
934
+ }
935
+ });
936
+
937
+
938
+ app.use((req, res) => {
939
+ res.status(200).send('api运行正常');
940
+ });
941
+
942
+
943
+ app.listen(CONFIG.SERVER.PORT, () => {
944
+ Logger.info(`服务器已启动,监听端口: ${CONFIG.SERVER.PORT}`, 'Server');
945
+ });
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
+ }