a842623 commited on
Commit
b623360
·
0 Parent(s):

Add grok2api_python files

Browse files
Files changed (10) hide show
  1. .dockerignore +10 -0
  2. .github/workflows/docker-deploy.yml +43 -0
  3. Dockerfile +12 -0
  4. README.md +144 -0
  5. app.py +1113 -0
  6. docker-compose.yml +18 -0
  7. grok2api_python +1 -0
  8. index.js +1319 -0
  9. logger.js +66 -0
  10. package.json +23 -0
.dockerignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .env
5
+ .dockerignore
6
+ Dockerfile
7
+ index.js
8
+ package.json
9
+ logger.js
10
+ docker-compose.yml
.github/workflows/docker-deploy.yml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Multiarch CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ paths-ignore:
7
+ - 'README.md' # 忽略README.md修改触发
8
+ pull_request:
9
+ branches: [ "main" ]
10
+ paths-ignore:
11
+ - 'README.md' # 忽略README.md修改触发
12
+
13
+ jobs:
14
+ build-and-push:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v3
20
+
21
+ - name: Set up QEMU
22
+ uses: docker/setup-qemu-action@v2
23
+
24
+ - name: Set up Docker Buildx
25
+ uses: docker/setup-buildx-action@v2
26
+
27
+ - name: Login to Docker Hub
28
+ uses: docker/login-action@v2
29
+ with:
30
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
31
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
32
+
33
+ - name: Build and push
34
+ uses: docker/build-push-action@v3
35
+ with:
36
+ context: .
37
+ platforms: linux/amd64,linux/arm64
38
+ push: true
39
+ tags: |
40
+ ${{ secrets.DOCKERHUB_USERNAME }}/grok2api_python:latest
41
+ ${{ secrets.DOCKERHUB_USERNAME }}/grok2api_python:${{ github.sha }}
42
+ cache-from: type=gha
43
+ cache-to: type=gha,mode=max
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN pip install --no-cache-dir flask requests curl_cffi werkzeug loguru
6
+
7
+ COPY . .
8
+
9
+ ENV PORT=3000
10
+ EXPOSE 3000
11
+
12
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # grok2API 接入指南:基于 python 的实现
3
+
4
+ ## 项目简介
5
+ 本项目提供了一种简单、高效的方式通过 Docker 部署 使用openAI的格式转换调用grok官网,进行api处理。
6
+
7
+ >支持自动过cf屏蔽盾,需要自己ip没有被屏蔽。
8
+
9
+ ## 方法一:Docker部署
10
+
11
+ ### 1. 获取项目
12
+ 克隆我的仓库:[grok2api](https://github.com/xLmiler/grok2api)
13
+ ### 2. 部署选项
14
+
15
+ #### 方式A:直接使用Docker镜像
16
+ ```bash
17
+ docker run -it -d --name grok2api_python \
18
+ -p 3000:3000 \
19
+ -e IS_TEMP_CONVERSATION=false \
20
+ -e API_KEY=your_api_key \
21
+ -e TUMY_KEY=你的图床key,和PICGO_KEY 二选一 \
22
+ -e PICGO_KEY=你的图床key,和TUMY_KEY二选一 \
23
+ -e IS_CUSTOM_SSO=false \
24
+ -e ISSHOW_SEARCH_RESULTS=false \
25
+ -e PORT=3000 \
26
+ -e SHOW_THINKING=true \
27
+ -e SSO=your_sso \
28
+ yxmiler/grok2api_python:latest
29
+ ```
30
+
31
+ #### 方式B:使用Docker Compose
32
+ ````artifact
33
+ version: '3.8'
34
+ services:
35
+ grok2api_python:
36
+ image: yxmiler/grok2api_python:latest
37
+ container_name: grok2api_python
38
+ ports:
39
+ - "3000:3000"
40
+ environment:
41
+ - API_KEY=your_api_key
42
+ - IS_TEMP_CONVERSATION=true
43
+ - IS_CUSTOM_SSO=false
44
+ - ISSHOW_SEARCH_RESULTS=false
45
+ - PORT=3000
46
+ - SHOW_THINKING=true
47
+ - SSO=your_sso
48
+ restart: unless-stopped
49
+ ````
50
+
51
+ #### 方式C:自行构建
52
+ 1. 克隆仓库
53
+ 2. 构建镜像
54
+ ```bash
55
+ docker build -t yourusername/grok2api .
56
+ ```
57
+ 3. 运行容器
58
+ ```bash
59
+ docker run -it -d --name grok2api \
60
+ -p 3000:3000 \
61
+ -e IS_TEMP_CONVERSATION=false \
62
+ -e API_KEY=your_api_key \
63
+ -e IS_CUSTOM_SSO=false \
64
+ -e ISSHOW_SEARCH_RESULTS=false \
65
+ -e PORT=3000 \
66
+ -e SHOW_THINKING=true \
67
+ -e SSO=your_sso \
68
+ yourusername/grok2api:latest
69
+ ```
70
+
71
+ ### 3. 环境变量具体配置
72
+
73
+ |变量 | 说明 | 构建时是否必填 |示例|
74
+ |--- | --- | ---| ---|
75
+ |`IS_TEMP_CONVERSATION` | 是否开启临时会话,开启后会话历史记录不会保留在网页 | (可以不填,默认是false) | `true/false`|
76
+ |`API_KEY` | 自定义认证鉴权密钥 | (可以不填,默认是sk-123456) | `sk-123456`|
77
+ |`PROXY` | 代理设置,支持https和Socks5 | 可不填,默认无 | -|
78
+ |`PICGO_KEY` | PicGo图床密钥,两个图床二选一 | 不填无法流式生图 | -|
79
+ |`TUMY_KEY` | TUMY图床密钥,两个图床二选一 | 不填无法流式生图 | -|
80
+ |`ISSHOW_SEARCH_RESULTS` | 是否显示搜索结果 | (可不填,默认关闭) | `true/false`|
81
+ |`SSO` | Grok官网SSO Cookie,可以设置多个使用英文 , 分隔,我的代码里会对不同账号的SSO自动轮询和均衡 | (除非开启IS_CUSTOM_SSO否则必填) | `sso,sso`|
82
+ |`PORT` | 服务部署端口 | (可不填,默认3000) | `3000`|
83
+ |`IS_CUSTOM_SSO` | 这是如果你想自己来自定义号池来轮询均衡,而不是通过我代码里已经内置的号池逻辑系统来为你轮询均衡启动的开关。开启后 API_KEY 需要设置为请求认证用的 sso cookie,同时SSO环境变量失效。一个apikey每次只能传入一个sso cookie 值,不支持一个请求里的apikey填入多个sso。想自动使用多个sso请关闭 IS_CUSTOM_SSO 这个环境变量,然后按照SSO环境变量要求在sso环境变量里填入多个sso,由我的代码里内置的号池系统来为你自动轮询 | (可不填,默认关闭) | `true/false`|
84
+ |`SHOW_THINKING` | 是否显示思考模型的思考过程 | (可不填,默认关闭) | `true/false`|
85
+
86
+ ## 方法二:Hugging Face部署
87
+
88
+ ### 部署地址
89
+ [GrokPythonService](https://huggingface.co/spaces/yxmiler/GrokPythonService)
90
+
91
+ ### 功能特点
92
+ 实现的功能:
93
+ 1. 已支持文字生成图,使用grok-2-imageGen和grok-3-imageGen模型。
94
+ 2. 已支持全部模型识图和传图,只会识别存储用户消息最新的一个图,历史记录图全部为占位符替代。
95
+ 3. 已支持搜索功能,使用grok-2-search或者grok-3-search模型,可以选择是否关闭搜索结果
96
+ 4. 已支持深度搜索功能,使用grok-3-deepsearch,深度搜索支持think过程显示
97
+ 5. 已支持推理模型功能,使用grok-3-reasoning
98
+ 6. 已支持真流式,上面全部功能都可以在流式情况调用
99
+ 7. 支持多账号轮询,在环境变量中配置
100
+ 8. 可以选择是否移除思考模型的思考过程。
101
+ 9. 支持自行设置轮询和负载均衡,而不依靠项目代码
102
+ 10. 自动过CF屏蔽盾
103
+ 11. 可自定义http和Socks5代理
104
+ 12. 已转换为openai格式。
105
+
106
+ ### 可用模型列表
107
+ - `grok-2`
108
+ - `grok-2-imageGen`
109
+ - `grok-2-search`
110
+ - `grok-3`
111
+ - `grok-3-search`
112
+ - `grok-3-imageGen`
113
+ - `grok-3-deepsearch`
114
+ - `grok-3-reasoning`
115
+
116
+ ### 模型可用次数参考
117
+ - grok-2,grok-2-imageGen,grok-2-search 合计:30次 每1小时刷新
118
+ - grok-3,grok-3-search,grok-3-imageGen 合计:20次 每2小时刷新
119
+ - grok-3-deepsearch:10次 每24小时刷新
120
+ - grok-3-reasoning:10次 每24小时刷新
121
+
122
+ ### cookie的获取办法:
123
+ 1. 打开[grok官网](https://grok.com/)
124
+ 2. 复制如下的SSO的cookie的值填入SSO变量即可
125
+ ![9EA{{UY6 PU~PENQHYO5JS7](https://github.com/user-attachments/assets/539d4a53-9352-49fd-8657-e942a94f44e9)
126
+
127
+
128
+ ### API调用
129
+ - 模型列表:`/v1/models`
130
+ - 对话:`/v1/chat/completions`
131
+ - 添加令牌:`/add/token`
132
+ - 获取全部令牌状态:`/get/tokens`
133
+
134
+ ## 备注
135
+ - 消息基于用户的伪造连续对话
136
+ - 可能存在一定程度的降智
137
+ - 生图模型不支持历史对话,仅支持生图。
138
+ ## 补充说明
139
+ - 如需使用流式生图的图像功能,需在[PicGo图床](https://www.picgo.net/)或者[tumy图床](https://tu.my/)申请API Key,前者似乎无法注册了,没有前面图床账号的可以选择后一个图床。
140
+ - 自动移除历史消息里的think过程,同时如果历史消息里包含里base64图片文本,而不是通过文件上传的方式上传,则自动转换为[图片]占用符。
141
+
142
+ ## 注意事项
143
+ ⚠️ 本项目仅供学习和研究目的,请遵守相关使用条款。
144
+
app.py ADDED
@@ -0,0 +1,1113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import time
5
+ import base64
6
+ import sys
7
+ import inspect
8
+ from loguru import logger
9
+
10
+ import requests
11
+ from flask import Flask, request, Response, jsonify, stream_with_context
12
+ from curl_cffi import requests as curl_requests
13
+ from werkzeug.middleware.proxy_fix import ProxyFix
14
+
15
+
16
+ class Logger:
17
+ def __init__(self, level="INFO", colorize=True, format=None):
18
+ logger.remove()
19
+
20
+ if format is None:
21
+ format = (
22
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
23
+ "<level>{level: <8}</level> | "
24
+ "<cyan>{extra[filename]}</cyan>:<cyan>{extra[function]}</cyan>:<cyan>{extra[lineno]}</cyan> | "
25
+ "<level>{message}</level>"
26
+ )
27
+
28
+ logger.add(
29
+ sys.stderr,
30
+ level=level,
31
+ format=format,
32
+ colorize=colorize,
33
+ backtrace=True,
34
+ diagnose=True
35
+ )
36
+
37
+ self.logger = logger
38
+
39
+ def _get_caller_info(self):
40
+ frame = inspect.currentframe()
41
+ try:
42
+ caller_frame = frame.f_back.f_back
43
+ full_path = caller_frame.f_code.co_filename
44
+ function = caller_frame.f_code.co_name
45
+ lineno = caller_frame.f_lineno
46
+
47
+ filename = os.path.basename(full_path)
48
+
49
+ return {
50
+ 'filename': filename,
51
+ 'function': function,
52
+ 'lineno': lineno
53
+ }
54
+ finally:
55
+ del frame
56
+
57
+ def info(self, message, source="API"):
58
+ caller_info = self._get_caller_info()
59
+ self.logger.bind(**caller_info).info(f"[{source}] {message}")
60
+
61
+ def error(self, message, source="API"):
62
+ caller_info = self._get_caller_info()
63
+
64
+ if isinstance(message, Exception):
65
+ self.logger.bind(**caller_info).exception(f"[{source}] {str(message)}")
66
+ else:
67
+ self.logger.bind(**caller_info).error(f"[{source}] {message}")
68
+
69
+ def warning(self, message, source="API"):
70
+ caller_info = self._get_caller_info()
71
+ self.logger.bind(**caller_info).warning(f"[{source}] {message}")
72
+
73
+ def debug(self, message, source="API"):
74
+ caller_info = self._get_caller_info()
75
+ self.logger.bind(**caller_info).debug(f"[{source}] {message}")
76
+
77
+ async def request_logger(self, request):
78
+ caller_info = self._get_caller_info()
79
+ self.logger.bind(**caller_info).info(f"请求: {request.method} {request.path}", "Request")
80
+
81
+ logger = Logger(level="INFO")
82
+
83
+
84
+ CONFIG = {
85
+ "MODELS": {
86
+ 'grok-2': 'grok-latest',
87
+ 'grok-2-imageGen': 'grok-latest',
88
+ 'grok-2-search': 'grok-latest',
89
+ "grok-3": "grok-3",
90
+ "grok-3-search": "grok-3",
91
+ "grok-3-imageGen": "grok-3",
92
+ "grok-3-deepsearch": "grok-3",
93
+ "grok-3-reasoning": "grok-3"
94
+ },
95
+ "API": {
96
+ "IS_TEMP_CONVERSATION": os.environ.get("IS_TEMP_CONVERSATION", "true").lower() == "true",
97
+ "IS_CUSTOM_SSO": os.environ.get("IS_CUSTOM_SSO", "false").lower() == "true",
98
+ "BASE_URL": "https://grok.com",
99
+ "API_KEY": os.environ.get("API_KEY", "sk-123456"),
100
+ "SIGNATURE_COOKIE": None,
101
+ "PICGO_KEY": os.environ.get("PICGO_KEY") or None,
102
+ "TUMY_KEY": os.environ.get("TUMY_KEY") or None,
103
+ "RETRY_TIME": 1000,
104
+ "PROXY": os.environ.get("PROXY") or None
105
+ },
106
+ "SERVER": {
107
+ "PORT": int(os.environ.get("PORT", 5200))
108
+ },
109
+ "RETRY": {
110
+ "MAX_ATTEMPTS": 2
111
+ },
112
+ "SHOW_THINKING": os.environ.get("SHOW_THINKING") == "true",
113
+ "IS_THINKING": False,
114
+ "IS_IMG_GEN": False,
115
+ "IS_IMG_GEN2": False,
116
+ "ISSHOW_SEARCH_RESULTS": os.environ.get("ISSHOW_SEARCH_RESULTS", "true").lower() == "true"
117
+ }
118
+
119
+
120
+ DEFAULT_HEADERS = {
121
+ 'Accept': '*/*',
122
+ 'Accept-Language': 'zh-CN,zh;q=0.9',
123
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
124
+ 'Content-Type': 'text/plain;charset=UTF-8',
125
+ 'Connection': 'keep-alive',
126
+ 'Origin': 'https://grok.com',
127
+ 'Priority': 'u=1, i',
128
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
129
+ 'Sec-Ch-Ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
130
+ 'Sec-Ch-Ua-Mobile': '?0',
131
+ 'Sec-Ch-Ua-Platform': '"macOS"',
132
+ 'Sec-Fetch-Dest': 'empty',
133
+ 'Sec-Fetch-Mode': 'cors',
134
+ 'Sec-Fetch-Site': 'same-origin',
135
+ 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
136
+ }
137
+
138
+ class AuthTokenManager:
139
+ def __init__(self):
140
+ self.token_model_map = {}
141
+ self.expired_tokens = set()
142
+ self.token_status_map = {}
143
+
144
+ self.model_config = {
145
+ "grok-2": {
146
+ "RequestFrequency": 30,
147
+ "ExpirationTime": 1 * 60 * 60 * 1000 # 1小时
148
+ },
149
+ "grok-3": {
150
+ "RequestFrequency": 20,
151
+ "ExpirationTime": 2 * 60 * 60 * 1000 # 2小时
152
+ },
153
+ "grok-3-deepsearch": {
154
+ "RequestFrequency": 10,
155
+ "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
156
+ },
157
+ "grok-3-reasoning": {
158
+ "RequestFrequency": 10,
159
+ "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时
160
+ }
161
+ }
162
+ self.token_reset_switch = False
163
+ self.token_reset_timer = None
164
+
165
+ def add_token(self, token):
166
+ sso = token.split("sso=")[1].split(";")[0]
167
+ for model in self.model_config.keys():
168
+ if model not in self.token_model_map:
169
+ self.token_model_map[model] = []
170
+ if sso not in self.token_status_map:
171
+ self.token_status_map[sso] = {}
172
+
173
+ existing_token_entry = next((entry for entry in self.token_model_map[model] if entry["token"] == token), None)
174
+
175
+ if not existing_token_entry:
176
+ self.token_model_map[model].append({
177
+ "token": token,
178
+ "RequestCount": 0,
179
+ "AddedTime": int(time.time() * 1000),
180
+ "StartCallTime": None
181
+ })
182
+
183
+ if model not in self.token_status_map[sso]:
184
+ self.token_status_map[sso][model] = {
185
+ "isValid": True,
186
+ "invalidatedTime": None,
187
+ "totalRequestCount": 0
188
+ }
189
+
190
+ def set_token(self, token):
191
+ models = list(self.model_config.keys())
192
+ self.token_model_map = {model: [{
193
+ "token": token,
194
+ "RequestCount": 0,
195
+ "AddedTime": int(time.time() * 1000),
196
+ "StartCallTime": None
197
+ }] for model in models}
198
+
199
+ sso = token.split("sso=")[1].split(";")[0]
200
+ self.token_status_map[sso] = {model: {
201
+ "isValid": True,
202
+ "invalidatedTime": None,
203
+ "totalRequestCount": 0
204
+ } for model in models}
205
+
206
+ def delete_token(self, token):
207
+ try:
208
+ sso = token.split("sso=")[1].split(";")[0]
209
+ for model in self.token_model_map:
210
+ self.token_model_map[model] = [entry for entry in self.token_model_map[model] if entry["token"] != token]
211
+
212
+ if sso in self.token_status_map:
213
+ del self.token_status_map[sso]
214
+
215
+ logger.info(f"令牌已成功移除: {token}", "TokenManager")
216
+ return True
217
+ except Exception as error:
218
+ logger.error(f"令牌删除失败: {str(error)}")
219
+ return False
220
+ def reduce_token_request_count(self, model_id, count):
221
+ try:
222
+ normalized_model = self.normalize_model_name(model_id)
223
+
224
+ if normalized_model not in self.token_model_map:
225
+ logger.error(f"模型 {normalized_model} 不存在", "TokenManager")
226
+ return False
227
+
228
+ if not self.token_model_map[normalized_model]:
229
+ logger.error(f"模型 {normalized_model} 没有可用的token", "TokenManager")
230
+ return False
231
+
232
+ token_entry = self.token_model_map[normalized_model][0]
233
+
234
+ # 确保RequestCount不会小于0
235
+ new_count = max(0, token_entry["RequestCount"] - count)
236
+ reduction = token_entry["RequestCount"] - new_count
237
+
238
+ token_entry["RequestCount"] = new_count
239
+
240
+ # 更新token状态
241
+ if token_entry["token"]:
242
+ sso = token_entry["token"].split("sso=")[1].split(";")[0]
243
+ if sso in self.token_status_map and normalized_model in self.token_status_map[sso]:
244
+ self.token_status_map[sso][normalized_model]["totalRequestCount"] = max(
245
+ 0,
246
+ self.token_status_map[sso][normalized_model]["totalRequestCount"] - reduction
247
+ )
248
+ return True
249
+
250
+ except Exception as error:
251
+ logger.error(f"重置校对token请求次数时发生错误: {str(error)}", "TokenManager")
252
+ return False
253
+ def get_next_token_for_model(self, model_id):
254
+ normalized_model = self.normalize_model_name(model_id)
255
+
256
+ if normalized_model not in self.token_model_map or not self.token_model_map[normalized_model]:
257
+ return None
258
+
259
+ token_entry = self.token_model_map[normalized_model][0]
260
+
261
+ if token_entry:
262
+ if token_entry["StartCallTime"] is None:
263
+ token_entry["StartCallTime"] = int(time.time() * 1000)
264
+
265
+ if not self.token_reset_switch:
266
+ self.start_token_reset_process()
267
+ self.token_reset_switch = True
268
+
269
+ token_entry["RequestCount"] += 1
270
+
271
+ if token_entry["RequestCount"] > self.model_config[normalized_model]["RequestFrequency"]:
272
+ self.remove_token_from_model(normalized_model, token_entry["token"])
273
+ next_token_entry = self.token_model_map[normalized_model][0] if self.token_model_map[normalized_model] else None
274
+ return next_token_entry["token"] if next_token_entry else None
275
+
276
+ sso = token_entry["token"].split("sso=")[1].split(";")[0]
277
+ if sso in self.token_status_map and normalized_model in self.token_status_map[sso]:
278
+ if token_entry["RequestCount"] == self.model_config[normalized_model]["RequestFrequency"]:
279
+ self.token_status_map[sso][normalized_model]["isValid"] = False
280
+ self.token_status_map[sso][normalized_model]["invalidatedTime"] = int(time.time() * 1000)
281
+ self.token_status_map[sso][normalized_model]["totalRequestCount"] += 1
282
+
283
+ return token_entry["token"]
284
+
285
+ return None
286
+
287
+ def remove_token_from_model(self, model_id, token):
288
+ normalized_model = self.normalize_model_name(model_id)
289
+
290
+ if normalized_model not in self.token_model_map:
291
+ logger.error(f"模型 {normalized_model} 不存在", "TokenManager")
292
+ return False
293
+
294
+ model_tokens = self.token_model_map[normalized_model]
295
+ token_index = next((i for i, entry in enumerate(model_tokens) if entry["token"] == token), -1)
296
+
297
+ if token_index != -1:
298
+ removed_token_entry = model_tokens.pop(token_index)
299
+ self.expired_tokens.add((
300
+ removed_token_entry["token"],
301
+ normalized_model,
302
+ int(time.time() * 1000)
303
+ ))
304
+
305
+ if not self.token_reset_switch:
306
+ self.start_token_reset_process()
307
+ self.token_reset_switch = True
308
+
309
+ logger.info(f"模型{model_id}的令牌已失效,已成功移除令牌: {token}", "TokenManager")
310
+ return True
311
+
312
+ logger.error(f"在模型 {normalized_model} 中未找到 token: {token}", "TokenManager")
313
+ return False
314
+
315
+ def get_expired_tokens(self):
316
+ return list(self.expired_tokens)
317
+
318
+ def normalize_model_name(self, model):
319
+ if model.startswith('grok-') and 'deepsearch' not in model and 'reasoning' not in model:
320
+ return '-'.join(model.split('-')[:2])
321
+ return model
322
+
323
+ def get_token_count_for_model(self, model_id):
324
+ normalized_model = self.normalize_model_name(model_id)
325
+ return len(self.token_model_map.get(normalized_model, []))
326
+
327
+ def get_remaining_token_request_capacity(self):
328
+ remaining_capacity_map = {}
329
+
330
+ for model in self.model_config.keys():
331
+ model_tokens = self.token_model_map.get(model, [])
332
+ model_request_frequency = self.model_config[model]["RequestFrequency"]
333
+
334
+ total_used_requests = sum(token_entry.get("RequestCount", 0) for token_entry in model_tokens)
335
+
336
+ remaining_capacity = (len(model_tokens) * model_request_frequency) - total_used_requests
337
+ remaining_capacity_map[model] = max(0, remaining_capacity)
338
+
339
+ return remaining_capacity_map
340
+
341
+ def get_token_array_for_model(self, model_id):
342
+ normalized_model = self.normalize_model_name(model_id)
343
+ return self.token_model_map.get(normalized_model, [])
344
+
345
+ def start_token_reset_process(self):
346
+ def reset_expired_tokens():
347
+ now = int(time.time() * 1000)
348
+
349
+ tokens_to_remove = set()
350
+ for token_info in self.expired_tokens:
351
+ token, model, expired_time = token_info
352
+ expiration_time = self.model_config[model]["ExpirationTime"]
353
+
354
+ if now - expired_time >= expiration_time:
355
+ if not any(entry["token"] == token for entry in self.token_model_map.get(model, [])):
356
+ if model not in self.token_model_map:
357
+ self.token_model_map[model] = []
358
+
359
+ self.token_model_map[model].append({
360
+ "token": token,
361
+ "RequestCount": 0,
362
+ "AddedTime": now,
363
+ "StartCallTime": None
364
+ })
365
+
366
+ sso = token.split("sso=")[1].split(";")[0]
367
+ if sso in self.token_status_map and model in self.token_status_map[sso]:
368
+ self.token_status_map[sso][model]["isValid"] = True
369
+ self.token_status_map[sso][model]["invalidatedTime"] = None
370
+ self.token_status_map[sso][model]["totalRequestCount"] = 0
371
+
372
+ tokens_to_remove.add(token_info)
373
+
374
+ self.expired_tokens -= tokens_to_remove
375
+
376
+ for model in self.model_config.keys():
377
+ if model not in self.token_model_map:
378
+ continue
379
+
380
+ for token_entry in self.token_model_map[model]:
381
+ if not token_entry.get("StartCallTime"):
382
+ continue
383
+
384
+ expiration_time = self.model_config[model]["ExpirationTime"]
385
+ if now - token_entry["StartCallTime"] >= expiration_time:
386
+ sso = token_entry["token"].split("sso=")[1].split(";")[0]
387
+ if sso in self.token_status_map and model in self.token_status_map[sso]:
388
+ self.token_status_map[sso][model]["isValid"] = True
389
+ self.token_status_map[sso][model]["invalidatedTime"] = None
390
+ self.token_status_map[sso][model]["totalRequestCount"] = 0
391
+
392
+ token_entry["RequestCount"] = 0
393
+ token_entry["StartCallTime"] = None
394
+
395
+ import threading
396
+ # 启动一个线程执行定时任务,每小时执行一次
397
+ def run_timer():
398
+ while True:
399
+ reset_expired_tokens()
400
+ time.sleep(3600)
401
+
402
+ timer_thread = threading.Thread(target=run_timer)
403
+ timer_thread.daemon = True
404
+ timer_thread.start()
405
+
406
+ def get_all_tokens(self):
407
+ all_tokens = set()
408
+ for model_tokens in self.token_model_map.values():
409
+ for entry in model_tokens:
410
+ all_tokens.add(entry["token"])
411
+ return list(all_tokens)
412
+
413
+ def get_token_status_map(self):
414
+ return self.token_status_map
415
+
416
+ class Utils:
417
+ @staticmethod
418
+ def organize_search_results(search_results):
419
+ if not search_results or 'results' not in search_results:
420
+ return ''
421
+
422
+ results = search_results['results']
423
+ formatted_results = []
424
+
425
+ for index, result in enumerate(results):
426
+ title = result.get('title', '未知标题')
427
+ url = result.get('url', '#')
428
+ preview = result.get('preview', '无预览内容')
429
+
430
+ formatted_result = f"\r\n<details><summary>资料[{index}]: {title}</summary>\r\n{preview}\r\n\n[Link]({url})\r\n</details>"
431
+ formatted_results.append(formatted_result)
432
+
433
+ return '\n\n'.join(formatted_results)
434
+
435
+ @staticmethod
436
+ def create_auth_headers(model):
437
+ return token_manager.get_next_token_for_model(model)
438
+
439
+ @staticmethod
440
+ def get_proxy_options():
441
+ proxy = CONFIG["API"]["PROXY"]
442
+ proxy_options = {}
443
+
444
+ if proxy:
445
+ logger.info(f"使用代理: {proxy}", "Server")
446
+ if proxy.startswith("socks5://"):
447
+ proxy_options["proxy"] = proxy
448
+
449
+ if '@' in proxy:
450
+ auth_part = proxy.split('@')[0].split('://')[1]
451
+ if ':' in auth_part:
452
+ username, password = auth_part.split(':')
453
+ proxy_options["proxy_auth"] = (username, password)
454
+ else:
455
+ proxy_options["proxies"] = {"https": proxy, "http": proxy}
456
+ print(proxy_options)
457
+ return proxy_options
458
+
459
+ class GrokApiClient:
460
+ def __init__(self, model_id):
461
+ if model_id not in CONFIG["MODELS"]:
462
+ raise ValueError(f"不支持的模型: {model_id}")
463
+ self.model_id = CONFIG["MODELS"][model_id]
464
+
465
+ def process_message_content(self, content):
466
+ if isinstance(content, str):
467
+ return content
468
+ return None
469
+
470
+ def get_image_type(self, base64_string):
471
+ mime_type = 'image/jpeg'
472
+ if 'data:image' in base64_string:
473
+ import re
474
+ matches = re.search(r'data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,', base64_string)
475
+ if matches:
476
+ mime_type = matches.group(1)
477
+
478
+ extension = mime_type.split('/')[1]
479
+ file_name = f"image.{extension}"
480
+
481
+ return {
482
+ "mimeType": mime_type,
483
+ "fileName": file_name
484
+ }
485
+
486
+ def upload_base64_image(self, base64_data, url):
487
+ try:
488
+ if 'data:image' in base64_data:
489
+ image_buffer = base64_data.split(',')[1]
490
+ else:
491
+ image_buffer = base64_data
492
+
493
+ image_info = self.get_image_type(base64_data)
494
+ mime_type = image_info["mimeType"]
495
+ file_name = image_info["fileName"]
496
+
497
+ upload_data = {
498
+ "rpc": "uploadFile",
499
+ "req": {
500
+ "fileName": file_name,
501
+ "fileMimeType": mime_type,
502
+ "content": image_buffer
503
+ }
504
+ }
505
+
506
+ logger.info("发送图片请求", "Server")
507
+
508
+ proxy_options = Utils.get_proxy_options()
509
+ response = curl_requests.post(
510
+ url,
511
+ headers={
512
+ **DEFAULT_HEADERS,
513
+ "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
514
+ },
515
+ json=upload_data,
516
+ impersonate="chrome133a",
517
+ **proxy_options
518
+ )
519
+
520
+ if response.status_code != 200:
521
+ logger.error(f"上传图片失败,状态码:{response.status_code}", "Server")
522
+ return ''
523
+
524
+ result = response.json()
525
+ logger.info(f"上传图片成功: {result}", "Server")
526
+ return result.get("fileMetadataId", "")
527
+
528
+ except Exception as error:
529
+ logger.error(str(error), "Server")
530
+ return ''
531
+
532
+ def prepare_chat_request(self, request):
533
+ if ((request["model"] == 'grok-2-imageGen' or request["model"] == 'grok-3-imageGen') and
534
+ not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"] and
535
+ request.get("stream", False)):
536
+ raise ValueError("该模型流式输出需要配置PICGO或者TUMY图床密钥!")
537
+
538
+ todo_messages = request["messages"]
539
+ if request["model"] in ['grok-2-imageGen', 'grok-3-imageGen', 'grok-3-deepsearch']:
540
+ last_message = todo_messages[-1]
541
+ if last_message["role"] != 'user':
542
+ raise ValueError('此模型最后一条消息必须是用户消息!')
543
+ todo_messages = [last_message]
544
+
545
+ file_attachments = []
546
+ messages = ''
547
+ last_role = None
548
+ last_content = ''
549
+ search = request["model"] in ['grok-2-search', 'grok-3-search']
550
+
551
+ # 移除<think>标签及其内容和base64图片
552
+ def remove_think_tags(text):
553
+ import re
554
+ text = re.sub(r'<think>[\s\S]*?<\/think>', '', text).strip()
555
+ text = re.sub(r'!\[image\]\(data:.*?base64,.*?\)', '[图片]', text)
556
+ return text
557
+
558
+ def process_content(content):
559
+ if isinstance(content, list):
560
+ text_content = ''
561
+ for item in content:
562
+ if item["type"] == 'image_url':
563
+ text_content += ("[图片]" if not text_content else '\n[图片]')
564
+ elif item["type"] == 'text':
565
+ text_content += (remove_think_tags(item["text"]) if not text_content else '\n' + remove_think_tags(item["text"]))
566
+ return text_content
567
+ elif isinstance(content, dict) and content is not None:
568
+ if content["type"] == 'image_url':
569
+ return "[图片]"
570
+ elif content["type"] == 'text':
571
+ return remove_think_tags(content["text"])
572
+ return remove_think_tags(self.process_message_content(content))
573
+
574
+ for current in todo_messages:
575
+ role = 'assistant' if current["role"] == 'assistant' else 'user'
576
+ is_last_message = current == todo_messages[-1]
577
+
578
+ if is_last_message and "content" in current:
579
+ if isinstance(current["content"], list):
580
+ for item in current["content"]:
581
+ if item["type"] == 'image_url':
582
+ processed_image = self.upload_base64_image(
583
+ item["image_url"]["url"],
584
+ f"{CONFIG['API']['BASE_URL']}/api/rpc"
585
+ )
586
+ if processed_image:
587
+ file_attachments.append(processed_image)
588
+ elif isinstance(current["content"], dict) and current["content"].get("type") == 'image_url':
589
+ processed_image = self.upload_base64_image(
590
+ current["content"]["image_url"]["url"],
591
+ f"{CONFIG['API']['BASE_URL']}/api/rpc"
592
+ )
593
+ if processed_image:
594
+ file_attachments.append(processed_image)
595
+
596
+
597
+ text_content = process_content(current.get("content", ""))
598
+
599
+ if text_content or (is_last_message and file_attachments):
600
+ if role == last_role and text_content:
601
+ last_content += '\n' + text_content
602
+ messages = messages[:messages.rindex(f"{role.upper()}: ")] + f"{role.upper()}: {last_content}\n"
603
+ else:
604
+ messages += f"{role.upper()}: {text_content or '[图片]'}\n"
605
+ last_content = text_content
606
+ last_role = role
607
+
608
+ return {
609
+ "temporary": CONFIG["API"].get("IS_TEMP_CONVERSATION", False),
610
+ "modelName": self.model_id,
611
+ "message": messages.strip(),
612
+ "fileAttachments": file_attachments[:4],
613
+ "imageAttachments": [],
614
+ "disableSearch": False,
615
+ "enableImageGeneration": True,
616
+ "returnImageBytes": False,
617
+ "returnRawGrokInXaiRequest": False,
618
+ "enableImageStreaming": False,
619
+ "imageGenerationCount": 1,
620
+ "forceConcise": False,
621
+ "toolOverrides": {
622
+ "imageGen": request["model"] in ['grok-2-imageGen', 'grok-3-imageGen'],
623
+ "webSearch": search,
624
+ "xSearch": search,
625
+ "xMediaSearch": search,
626
+ "trendsSearch": search,
627
+ "xPostAnalyze": search
628
+ },
629
+ "enableSideBySide": True,
630
+ "isPreset": False,
631
+ "sendFinalMetadata": True,
632
+ "customInstructions": "",
633
+ "deepsearchPreset": "default" if request["model"] == 'grok-3-deepsearch' else "",
634
+ "isReasoning": request["model"] == 'grok-3-reasoning'
635
+ }
636
+
637
+ class MessageProcessor:
638
+ @staticmethod
639
+ def create_chat_response(message, model, is_stream=False):
640
+ base_response = {
641
+ "id": f"chatcmpl-{uuid.uuid4()}",
642
+ "created": int(time.time()),
643
+ "model": model
644
+ }
645
+
646
+ if is_stream:
647
+ return {
648
+ **base_response,
649
+ "object": "chat.completion.chunk",
650
+ "choices": [{
651
+ "index": 0,
652
+ "delta": {
653
+ "content": message
654
+ }
655
+ }]
656
+ }
657
+
658
+ return {
659
+ **base_response,
660
+ "object": "chat.completion",
661
+ "choices": [{
662
+ "index": 0,
663
+ "message": {
664
+ "role": "assistant",
665
+ "content": message
666
+ },
667
+ "finish_reason": "stop"
668
+ }],
669
+ "usage": None
670
+ }
671
+
672
+ def process_model_response(response, model):
673
+ result = {"token": None, "imageUrl": None}
674
+
675
+ if CONFIG["IS_IMG_GEN"]:
676
+ if response.get("cachedImageGenerationResponse") and not CONFIG["IS_IMG_GEN2"]:
677
+ result["imageUrl"] = response["cachedImageGenerationResponse"]["imageUrl"]
678
+ return result
679
+
680
+ if model == 'grok-2':
681
+ result["token"] = response.get("token")
682
+ elif model in ['grok-2-search', 'grok-3-search']:
683
+ if response.get("webSearchResults") and CONFIG["ISSHOW_SEARCH_RESULTS"]:
684
+ result["token"] = f"\r\n<think>{Utils.organize_search_results(response['webSearchResults'])}</think>\r\n"
685
+ else:
686
+ result["token"] = response.get("token")
687
+ elif model == 'grok-3':
688
+ result["token"] = response.get("token")
689
+ elif model == 'grok-3-deepsearch':
690
+ if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]:
691
+ return result
692
+ if response.get("messageStepId") and not CONFIG["IS_THINKING"]:
693
+ result["token"] = "<think>" + response.get("token", "")
694
+ CONFIG["IS_THINKING"] = True
695
+ elif not response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "final":
696
+ result["token"] = "</think>" + response.get("token", "")
697
+ CONFIG["IS_THINKING"] = False
698
+ elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final":
699
+ result["token"] = response.get("token")
700
+ elif model == 'grok-3-reasoning':
701
+ if response.get("isThinking") and not CONFIG["SHOW_THINKING"]:
702
+ return result
703
+
704
+ if response.get("isThinking") and not CONFIG["IS_THINKING"]:
705
+ result["token"] = "<think>" + response.get("token", "")
706
+ CONFIG["IS_THINKING"] = True
707
+ elif not response.get("isThinking") and CONFIG["IS_THINKING"]:
708
+ result["token"] = "</think>" + response.get("token", "")
709
+ CONFIG["IS_THINKING"] = False
710
+ else:
711
+ result["token"] = response.get("token")
712
+
713
+ return result
714
+
715
+ def handle_image_response(image_url):
716
+ max_retries = 2
717
+ retry_count = 0
718
+ image_base64_response = None
719
+
720
+ while retry_count < max_retries:
721
+ try:
722
+ proxy_options = Utils.get_proxy_options()
723
+ image_base64_response = curl_requests.get(
724
+ f"https://assets.grok.com/{image_url}",
725
+ headers={
726
+ **DEFAULT_HEADERS,
727
+ "Cookie": CONFIG["API"]["SIGNATURE_COOKIE"]
728
+ },
729
+ impersonate="chrome120",
730
+ **proxy_options
731
+ )
732
+
733
+ if image_base64_response.status_code == 200:
734
+ break
735
+
736
+ retry_count += 1
737
+ if retry_count == max_retries:
738
+ raise Exception(f"上游服务请求失败! status: {image_base64_response.status_code}")
739
+
740
+ time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count)
741
+
742
+ except Exception as error:
743
+ logger.error(str(error), "Server")
744
+ retry_count += 1
745
+ if retry_count == max_retries:
746
+ raise
747
+
748
+ time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count)
749
+
750
+ image_buffer = image_base64_response.content
751
+
752
+ if not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"]:
753
+ base64_image = base64.b64encode(image_buffer).decode('utf-8')
754
+ image_content_type = image_base64_response.headers.get('content-type', 'image/jpeg')
755
+ return f"![image](data:{image_content_type};base64,{base64_image})"
756
+
757
+ logger.info("开始上传图床", "Server")
758
+
759
+ if CONFIG["API"]["PICGO_KEY"]:
760
+ files = {'source': ('image.jpg', image_buffer, 'image/jpeg')}
761
+ headers = {
762
+ "X-API-Key": CONFIG["API"]["PICGO_KEY"]
763
+ }
764
+
765
+ response_url = requests.post(
766
+ "https://www.picgo.net/api/1/upload",
767
+ files=files,
768
+ headers=headers
769
+ )
770
+
771
+ if response_url.status_code != 200:
772
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
773
+ else:
774
+ logger.info("生图成功", "Server")
775
+ result = response_url.json()
776
+ return f"![image]({result['image']['url']})"
777
+
778
+
779
+ elif CONFIG["API"]["TUMY_KEY"]:
780
+ files = {'file': ('image.jpg', image_buffer, 'image/jpeg')}
781
+ headers = {
782
+ "Accept": "application/json",
783
+ 'Authorization': f"Bearer {CONFIG['API']['TUMY_KEY']}"
784
+ }
785
+
786
+ response_url = requests.post(
787
+ "https://tu.my/api/v1/upload",
788
+ files=files,
789
+ headers=headers
790
+ )
791
+
792
+ if response_url.status_code != 200:
793
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
794
+ else:
795
+ try:
796
+ result = response_url.json()
797
+ logger.info("生图成功", "Server")
798
+ return f"![image]({result['data']['links']['url']})"
799
+ except Exception as error:
800
+ logger.error(str(error), "Server")
801
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
802
+
803
+ def handle_non_stream_response(response, model):
804
+ try:
805
+ logger.info("开始处理非流式响应", "Server")
806
+
807
+ stream = response.iter_lines()
808
+ full_response = ""
809
+
810
+ CONFIG["IS_THINKING"] = False
811
+ CONFIG["IS_IMG_GEN"] = False
812
+ CONFIG["IS_IMG_GEN2"] = False
813
+
814
+ for chunk in stream:
815
+ if not chunk:
816
+ continue
817
+ try:
818
+ line_json = json.loads(chunk.decode("utf-8").strip())
819
+ if line_json.get("error"):
820
+ logger.error(json.dumps(line_json, indent=2), "Server")
821
+ return json.dumps({"error": "RateLimitError"}) + "\n\n"
822
+
823
+ response_data = line_json.get("result", {}).get("response")
824
+ if not response_data:
825
+ continue
826
+
827
+ if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
828
+ CONFIG["IS_IMG_GEN"] = True
829
+
830
+ result = process_model_response(response_data, model)
831
+
832
+ if result["token"]:
833
+ full_response += result["token"]
834
+
835
+ if result["imageUrl"]:
836
+ CONFIG["IS_IMG_GEN2"] = True
837
+ return handle_image_response(result["imageUrl"])
838
+
839
+ except json.JSONDecodeError:
840
+ continue
841
+ except Exception as e:
842
+ logger.error(f"处理流式响应行时出错: {str(e)}", "Server")
843
+ continue
844
+
845
+ return full_response
846
+ except Exception as error:
847
+ logger.error(str(error), "Server")
848
+ raise
849
+ def handle_stream_response(response, model):
850
+ def generate():
851
+ logger.info("开始处理流式响应", "Server")
852
+
853
+ stream = response.iter_lines()
854
+ CONFIG["IS_THINKING"] = False
855
+ CONFIG["IS_IMG_GEN"] = False
856
+ CONFIG["IS_IMG_GEN2"] = False
857
+
858
+ for chunk in stream:
859
+ if not chunk:
860
+ continue
861
+ try:
862
+ line_json = json.loads(chunk.decode("utf-8").strip())
863
+ if line_json.get("error"):
864
+ logger.error(json.dumps(line_json, indent=2), "Server")
865
+ yield json.dumps({"error": "RateLimitError"}) + "\n\n"
866
+ return
867
+
868
+ response_data = line_json.get("result", {}).get("response")
869
+ if not response_data:
870
+ continue
871
+
872
+ if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"):
873
+ CONFIG["IS_IMG_GEN"] = True
874
+
875
+ result = process_model_response(response_data, model)
876
+
877
+ if result["token"]:
878
+ yield f"data: {json.dumps(MessageProcessor.create_chat_response(result['token'], model, True))}\n\n"
879
+
880
+ if result["imageUrl"]:
881
+ CONFIG["IS_IMG_GEN2"] = True
882
+ image_data = handle_image_response(result["imageUrl"])
883
+ yield f"data: {json.dumps(MessageProcessor.create_chat_response(image_data, model, True))}\n\n"
884
+
885
+ except json.JSONDecodeError:
886
+ continue
887
+ except Exception as e:
888
+ logger.error(f"处理流式响应行时出错: {str(e)}", "Server")
889
+ continue
890
+
891
+ yield "data: [DONE]\n\n"
892
+ return generate()
893
+
894
+ def initialization():
895
+ sso_array = os.environ.get("SSO", "").split(',')
896
+ logger.info("开始加载令牌", "Server")
897
+ for sso in sso_array:
898
+ if sso:
899
+ token_manager.add_token(f"sso-rw={sso};sso={sso}")
900
+
901
+ logger.info(f"成功加载令牌: {json.dumps(token_manager.get_all_tokens(), indent=2)}", "Server")
902
+ logger.info(f"令牌加载完成,共加载: {len(token_manager.get_all_tokens())}个令牌", "Server")
903
+
904
+ if CONFIG["API"]["PROXY"]:
905
+ logger.info(f"代理已设置: {CONFIG['API']['PROXY']}", "Server")
906
+
907
+ logger.info("初始化完成", "Server")
908
+
909
+
910
+ app = Flask(__name__)
911
+ app.wsgi_app = ProxyFix(app.wsgi_app)
912
+
913
+
914
+ @app.before_request
915
+ def log_request_info():
916
+ logger.info(f"{request.method} {request.path}", "Request")
917
+
918
+ @app.route('/get/tokens', methods=['GET'])
919
+ def get_tokens():
920
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
921
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
922
+ return jsonify({"error": '自定义的SSO令牌模式无法获取轮询sso令牌状态'}), 403
923
+ elif auth_token != CONFIG["API"]["API_KEY"]:
924
+ return jsonify({"error": 'Unauthorized'}), 401
925
+
926
+ return jsonify(token_manager.get_token_status_map())
927
+
928
+ @app.route('/add/token', methods=['POST'])
929
+ def add_token():
930
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
931
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
932
+ return jsonify({"error": '自定义的SSO令牌模式无法添加sso令牌'}), 403
933
+ elif auth_token != CONFIG["API"]["API_KEY"]:
934
+ return jsonify({"error": 'Unauthorized'}), 401
935
+
936
+ try:
937
+ sso = request.json.get('sso')
938
+ token_manager.add_token(f"sso-rw={sso};sso={sso}")
939
+ return jsonify(token_manager.get_token_status_map().get(sso, {})), 200
940
+ except Exception as error:
941
+ logger.error(str(error), "Server")
942
+ return jsonify({"error": '添加sso令牌失败'}), 500
943
+
944
+ @app.route('/delete/token', methods=['POST'])
945
+ def delete_token():
946
+ auth_token = request.headers.get('Authorization', '').replace('Bearer ', '')
947
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
948
+ return jsonify({"error": '自定义的SSO令牌模式无法删除sso令牌'}), 403
949
+ elif auth_token != CONFIG["API"]["API_KEY"]:
950
+ return jsonify({"error": 'Unauthorized'}), 401
951
+
952
+ try:
953
+ sso = request.json.get('sso')
954
+ token_manager.delete_token(f"sso-rw={sso};sso={sso}")
955
+ return jsonify({"message": '删除sso令牌成功'}), 200
956
+ except Exception as error:
957
+ logger.error(str(error), "Server")
958
+ return jsonify({"error": '删除sso令牌失败'}), 500
959
+
960
+ @app.route('/v1/models', methods=['GET'])
961
+ def get_models():
962
+ return jsonify({
963
+ "object": "list",
964
+ "data": [
965
+ {
966
+ "id": model,
967
+ "object": "model",
968
+ "created": int(time.time()),
969
+ "owned_by": "grok"
970
+ }
971
+ for model in CONFIG["MODELS"].keys()
972
+ ]
973
+ })
974
+
975
+ @app.route('/v1/chat/completions', methods=['POST'])
976
+ def chat_completions():
977
+ try:
978
+ auth_token = request.headers.get('Authorization',
979
+ '').replace('Bearer ', '')
980
+ if auth_token:
981
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
982
+ result = f"sso={auth_token};sso-rw={auth_token}"
983
+ token_manager.set_token(result)
984
+ elif auth_token != CONFIG["API"]["API_KEY"]:
985
+ return jsonify({"error": 'Unauthorized'}), 401
986
+ else:
987
+ return jsonify({"error": 'API_KEY缺失'}), 401
988
+
989
+ data = request.json
990
+ model = data.get("model")
991
+ stream = data.get("stream", False)
992
+
993
+ retry_count = 0
994
+ grok_client = GrokApiClient(model)
995
+ request_payload = grok_client.prepare_chat_request(data)
996
+ response_status_code = 500
997
+
998
+ while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]:
999
+ retry_count += 1
1000
+ CONFIG["API"]["SIGNATURE_COOKIE"] = Utils.create_auth_headers(
1001
+ model)
1002
+
1003
+ if not CONFIG["API"]["SIGNATURE_COOKIE"]:
1004
+ raise ValueError('该模型无可用令牌')
1005
+
1006
+ logger.info(
1007
+ f"当前令牌: {json.dumps(CONFIG['API']['SIGNATURE_COOKIE'], indent=2)}",
1008
+ "Server")
1009
+ logger.info(
1010
+ f"当前可用模型的全部可用数量: {json.dumps(token_manager.get_remaining_token_request_capacity(), indent=2)}",
1011
+ "Server")
1012
+
1013
+ try:
1014
+ proxy_options = Utils.get_proxy_options()
1015
+ response = curl_requests.post(
1016
+ f"{CONFIG['API']['BASE_URL']}/rest/app-chat/conversations/new",
1017
+ headers={
1018
+ **DEFAULT_HEADERS, "Cookie":
1019
+ CONFIG["API"]["SIGNATURE_COOKIE"]
1020
+ },
1021
+ data=json.dumps(request_payload),
1022
+ impersonate="chrome133a",
1023
+ stream=True,
1024
+ **proxy_options)
1025
+ if response.status_code == 200:
1026
+ response_status_code = 200
1027
+ logger.info("请求成功", "Server")
1028
+ logger.info(
1029
+ f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}",
1030
+ "Server")
1031
+
1032
+ try:
1033
+ if stream:
1034
+ return Response(stream_with_context(
1035
+ handle_stream_response(response, model)),
1036
+ content_type='text/event-stream')
1037
+ else:
1038
+ content = handle_non_stream_response(
1039
+ response, model)
1040
+ return jsonify(
1041
+ MessageProcessor.create_chat_response(
1042
+ content, model))
1043
+
1044
+ except Exception as error:
1045
+ logger.error(str(error), "Server")
1046
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1047
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
1048
+
1049
+ token_manager.remove_token_from_model(
1050
+ model, CONFIG["API"]["SIGNATURE_COOKIE"])
1051
+ if token_manager.get_token_count_for_model(model) == 0:
1052
+ raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1053
+ elif response.status_code == 403:
1054
+ response_status_code = 403
1055
+ token_manager.reduce_token_request_count(model,1)#重置去除当前因为错误未成功请求的次数,确保不会因为错误未成功请求的次数导致次数上限
1056
+ if token_manager.get_token_count_for_model(model) == 0:
1057
+ raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1058
+ raise ValueError(f"IP暂时被封黑无法破盾,请稍后重试或者更换ip")
1059
+ elif response.status_code == 429:
1060
+ response_status_code = 429
1061
+ token_manager.reduce_token_request_count(model,1)
1062
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1063
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
1064
+
1065
+ token_manager.remove_token_from_model(
1066
+ model, CONFIG["API"]["SIGNATURE_COOKIE"])
1067
+ if token_manager.get_token_count_for_model(model) == 0:
1068
+ raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话")
1069
+
1070
+ else:
1071
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1072
+ raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效")
1073
+
1074
+ logger.error(f"令牌异常错误状态!status: {response.status_code}",
1075
+ "Server")
1076
+ token_manager.remove_token_from_model(
1077
+ model, CONFIG["API"]["SIGNATURE_COOKIE"])
1078
+ logger.info(
1079
+ f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}",
1080
+ "Server")
1081
+
1082
+ except Exception as e:
1083
+ logger.error(f"请求处理异常: {str(e)}", "Server")
1084
+ if CONFIG["API"]["IS_CUSTOM_SSO"]:
1085
+ raise
1086
+ continue
1087
+ if response_status_code == 403:
1088
+ raise ValueError('IP暂时被封黑无法破盾,请稍后重试或者更换ip')
1089
+ elif response_status_code == 500:
1090
+ raise ValueError('当前模型所有令牌暂无可用,请稍后重试')
1091
+
1092
+ except Exception as error:
1093
+ logger.error(str(error), "ChatAPI")
1094
+ return jsonify(
1095
+ {"error": {
1096
+ "message": str(error),
1097
+ "type": "server_error"
1098
+ }}), response_status_code
1099
+
1100
+ @app.route('/', defaults={'path': ''})
1101
+ @app.route('/<path:path>')
1102
+ def catch_all(path):
1103
+ return 'api运行正常', 200
1104
+
1105
+ if __name__ == '__main__':
1106
+ token_manager = AuthTokenManager()
1107
+ initialization()
1108
+
1109
+ app.run(
1110
+ host='0.0.0.0',
1111
+ port=CONFIG["SERVER"]["PORT"],
1112
+ debug=False
1113
+ )
docker-compose.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+ services:
3
+ grok2api_python:
4
+ image: yxmiler/grok2api_python:latest
5
+ container_name: grok2api_python
6
+ ports:
7
+ - "3000:3000"
8
+ environment:
9
+ - API_KEY=your_api_key
10
+ - TUMY_KEY=你的图床key,和PICGO_KEY 二选一
11
+ - PICGO_KEY=你的图床key,和TUMY_KEY二选一
12
+ - IS_TEMP_CONVERSATION=true
13
+ - IS_CUSTOM_SSO=false
14
+ - ISSHOW_SEARCH_RESULTS=false
15
+ - PORT=3000
16
+ - SHOW_THINKING=true
17
+ - SSO=your_sso
18
+ restart: unless-stopped
grok2api_python ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit c88b44a807bd8cc04067fd1a8b47195a51cca206
index.js ADDED
@@ -0,0 +1,1319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ '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',
70
+ 'baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
71
+ };
72
+
73
+
74
+ async function initialization() {
75
+ if (CONFIG.CHROME_PATH == null) {
76
+ try {
77
+ CONFIG.CHROME_PATH = puppeteer.executablePath();
78
+ } catch (error) {
79
+ CONFIG.CHROME_PATH = "/usr/bin/chromium";
80
+ }
81
+ }
82
+ Logger.info(`CHROME_PATH: ${CONFIG.CHROME_PATH}`, 'Server');
83
+ if (CONFIG.API.IS_CUSTOM_SSO) {
84
+ if (CONFIG.API.IS_TEMP_GROK2) {
85
+ await tempCookieManager.ensureCookies();
86
+ }
87
+ return;
88
+ }
89
+ const ssoArray = process.env.SSO.split(',');
90
+ const concurrencyLimit = 3;
91
+ for (let i = 0; i < ssoArray.length; i += concurrencyLimit) {
92
+ const batch = ssoArray.slice(i, i + concurrencyLimit);
93
+ const batchPromises = batch.map(sso =>
94
+ tokenManager.addToken(`sso-rw=${sso};sso=${sso}`)
95
+ );
96
+
97
+ await Promise.all(batchPromises);
98
+ Logger.info(`已加载令牌: ${i} 个`, 'Server');
99
+ await new Promise(resolve => setTimeout(resolve, 1000));
100
+ }
101
+ Logger.info(`令牌加载完成: ${JSON.stringify(tokenManager.getAllTokens(), null, 2)}`, 'Server');
102
+ Logger.info(`共加载: ${tokenManager.getAllTokens().length}个令牌`, 'Server');
103
+ if (CONFIG.API.IS_TEMP_GROK2) {
104
+ await tempCookieManager.ensureCookies();
105
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
106
+ }
107
+ Logger.info("初始化完成", 'Server');
108
+ }
109
+
110
+ class AuthTokenManager {
111
+ constructor() {
112
+ this.tokenModelMap = {};
113
+ this.expiredTokens = new Set();
114
+ this.tokenStatusMap = {};
115
+
116
+ // 定义模型请求频率限制和过期时间
117
+ this.modelConfig = {
118
+ "grok-2": {
119
+ RequestFrequency: 30,
120
+ ExpirationTime: 1 * 60 * 60 * 1000 // 1小时
121
+ },
122
+ "grok-3": {
123
+ RequestFrequency: 20,
124
+ ExpirationTime: 2 * 60 * 60 * 1000 // 2小时
125
+ },
126
+ "grok-3-deepsearch": {
127
+ RequestFrequency: 10,
128
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小时
129
+ },
130
+ "grok-3-reasoning": {
131
+ RequestFrequency: 10,
132
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小时
133
+ }
134
+ };
135
+ this.tokenResetSwitch = false;
136
+ this.tokenResetTimer = null;
137
+ }
138
+ async fetchGrokStats(token, modelName) {
139
+ let requestKind = 'DEFAULT';
140
+ if (modelName == 'grok-2' || modelName == 'grok-3') {
141
+ requestKind = 'DEFAULT';
142
+ } else if (modelName == 'grok-3-deepsearch') {
143
+ requestKind = 'DEEPSEARCH';
144
+ } else if (modelName == 'grok-3-reasoning') {
145
+ requestKind = 'REASONING';
146
+ }
147
+ const response = await fetch('https://grok.com/rest/rate-limits', {
148
+ method: 'POST',
149
+ headers: {
150
+ 'content-type': 'application/json',
151
+ 'Cookie': token,
152
+ },
153
+ body: JSON.stringify({
154
+ "requestKind": requestKind,
155
+ "modelName": modelName == 'grok-2' ? 'grok-latest' : "grok-3"
156
+ })
157
+ });
158
+
159
+ if (response.status != 200) {
160
+ return 0;
161
+ }
162
+ const data = await response.json();
163
+ return data.remainingQueries;
164
+ }
165
+ async addToken(token) {
166
+ const sso = token.split("sso=")[1].split(";")[0];
167
+
168
+ for (const model of Object.keys(this.modelConfig)) {
169
+ if (!this.tokenModelMap[model]) {
170
+ this.tokenModelMap[model] = [];
171
+ }
172
+ if (!this.tokenStatusMap[sso]) {
173
+ this.tokenStatusMap[sso] = {};
174
+ }
175
+ const existingTokenEntry = this.tokenModelMap[model].find(entry => entry.token === token);
176
+
177
+ if (!existingTokenEntry) {
178
+ try {
179
+ const remainingQueries = await this.fetchGrokStats(token, model);
180
+
181
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
182
+ const usedRequestCount = modelRequestFrequency - remainingQueries;
183
+
184
+ if (usedRequestCount === modelRequestFrequency) {
185
+ this.expiredTokens.add({
186
+ token: token,
187
+ model: model,
188
+ expiredTime: Date.now()
189
+ });
190
+
191
+ if (!this.tokenStatusMap[sso][model]) {
192
+ this.tokenStatusMap[sso][model] = {
193
+ isValid: false,
194
+ invalidatedTime: Date.now(),
195
+ totalRequestCount: Math.max(0, usedRequestCount)
196
+ };
197
+ }
198
+
199
+ if (!this.tokenResetSwitch) {
200
+ this.startTokenResetProcess();
201
+ this.tokenResetSwitch = true;
202
+ }
203
+ } else {
204
+ this.tokenModelMap[model].push({
205
+ token: token,
206
+ RequestCount: Math.max(0, usedRequestCount),
207
+ AddedTime: Date.now(),
208
+ StartCallTime: null
209
+ });
210
+
211
+ if (!this.tokenStatusMap[sso][model]) {
212
+ this.tokenStatusMap[sso][model] = {
213
+ isValid: true,
214
+ invalidatedTime: null,
215
+ totalRequestCount: Math.max(0, usedRequestCount)
216
+ };
217
+ }
218
+ }
219
+ } catch (error) {
220
+ this.tokenModelMap[model].push({
221
+ token: token,
222
+ RequestCount: 0,
223
+ AddedTime: Date.now(),
224
+ StartCallTime: null
225
+ });
226
+
227
+ if (!this.tokenStatusMap[sso][model]) {
228
+ this.tokenStatusMap[sso][model] = {
229
+ isValid: true,
230
+ invalidatedTime: null,
231
+ totalRequestCount: 0
232
+ };
233
+ }
234
+
235
+ Logger.error(`获取模型 ${model} 的统计信息失败: ${error}`, 'TokenManager');
236
+ }
237
+ await Utils.delay(200);
238
+ }
239
+ }
240
+ }
241
+
242
+ setToken(token) {
243
+ const models = Object.keys(this.modelConfig);
244
+ this.tokenModelMap = models.reduce((map, model) => {
245
+ map[model] = [{
246
+ token,
247
+ RequestCount: 0,
248
+ AddedTime: Date.now(),
249
+ StartCallTime: null
250
+ }];
251
+ return map;
252
+ }, {});
253
+ const sso = token.split("sso=")[1].split(";")[0];
254
+ this.tokenStatusMap[sso] = models.reduce((statusMap, model) => {
255
+ statusMap[model] = {
256
+ isValid: true,
257
+ invalidatedTime: null,
258
+ totalRequestCount: 0
259
+ };
260
+ return statusMap;
261
+ }, {});
262
+ }
263
+
264
+ async deleteToken(token) {
265
+ try {
266
+ const sso = token.split("sso=")[1].split(";")[0];
267
+ await Promise.all([
268
+ new Promise((resolve) => {
269
+ this.tokenModelMap = Object.fromEntries(
270
+ Object.entries(this.tokenModelMap).map(([model, entries]) => [
271
+ model,
272
+ entries.filter(entry => entry.token !== token)
273
+ ])
274
+ );
275
+ resolve();
276
+ }),
277
+
278
+ new Promise((resolve) => {
279
+ delete this.tokenStatusMap[sso];
280
+ resolve();
281
+ }),
282
+ ]);
283
+ Logger.info(`令牌已成功移除: ${token}`, 'TokenManager');
284
+ return true;
285
+ } catch (error) {
286
+ Logger.error('令牌删除失败:', error);
287
+ return false;
288
+ }
289
+ }
290
+ getNextTokenForModel(modelId) {
291
+ const normalizedModel = this.normalizeModelName(modelId);
292
+
293
+ if (!this.tokenModelMap[normalizedModel] || this.tokenModelMap[normalizedModel].length === 0) {
294
+ return null;
295
+ }
296
+ const tokenEntry = this.tokenModelMap[normalizedModel][0];
297
+
298
+ if (tokenEntry) {
299
+ if (tokenEntry.StartCallTime === null || tokenEntry.StartCallTime === undefined) {
300
+ tokenEntry.StartCallTime = Date.now();
301
+ }
302
+ if (!this.tokenResetSwitch) {
303
+ this.startTokenResetProcess();
304
+ this.tokenResetSwitch = true;
305
+ }
306
+ tokenEntry.RequestCount++;
307
+
308
+ if (tokenEntry.RequestCount > this.modelConfig[normalizedModel].RequestFrequency) {
309
+ this.removeTokenFromModel(normalizedModel, tokenEntry.token);
310
+ const nextTokenEntry = this.tokenModelMap[normalizedModel][0];
311
+ return nextTokenEntry ? nextTokenEntry.token : null;
312
+ }
313
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
314
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][normalizedModel]) {
315
+ if (tokenEntry.RequestCount === this.modelConfig[normalizedModel].RequestFrequency) {
316
+ this.tokenStatusMap[sso][normalizedModel].isValid = false;
317
+ this.tokenStatusMap[sso][normalizedModel].invalidatedTime = Date.now();
318
+ }
319
+ this.tokenStatusMap[sso][normalizedModel].totalRequestCount++;
320
+ }
321
+ return tokenEntry.token;
322
+ }
323
+
324
+ return null;
325
+ }
326
+
327
+ removeTokenFromModel(modelId, token) {
328
+ const normalizedModel = this.normalizeModelName(modelId);
329
+
330
+ if (!this.tokenModelMap[normalizedModel]) {
331
+ Logger.error(`模型 ${normalizedModel} 不存在`, 'TokenManager');
332
+ return false;
333
+ }
334
+
335
+ const modelTokens = this.tokenModelMap[normalizedModel];
336
+ const tokenIndex = modelTokens.findIndex(entry => entry.token === token);
337
+
338
+ if (tokenIndex !== -1) {
339
+ const removedTokenEntry = modelTokens.splice(tokenIndex, 1)[0];
340
+ this.expiredTokens.add({
341
+ token: removedTokenEntry.token,
342
+ model: normalizedModel,
343
+ expiredTime: Date.now()
344
+ });
345
+
346
+ if (!this.tokenResetSwitch) {
347
+ this.startTokenResetProcess();
348
+ this.tokenResetSwitch = true;
349
+ }
350
+ Logger.info(`模型${modelId}的令牌已失效,已成功移除令牌: ${token}`, 'TokenManager');
351
+ return true;
352
+ }
353
+
354
+ Logger.error(`在模型 ${normalizedModel} 中未找到 token: ${token}`, 'TokenManager');
355
+ return false;
356
+ }
357
+
358
+ getExpiredTokens() {
359
+ return Array.from(this.expiredTokens);
360
+ }
361
+
362
+ normalizeModelName(model) {
363
+ if (model.startsWith('grok-') && !model.includes('deepsearch') && !model.includes('reasoning')) {
364
+ return model.split('-').slice(0, 2).join('-');
365
+ }
366
+ return model;
367
+ }
368
+
369
+ getTokenCountForModel(modelId) {
370
+ const normalizedModel = this.normalizeModelName(modelId);
371
+ return this.tokenModelMap[normalizedModel]?.length || 0;
372
+ }
373
+
374
+ getRemainingTokenRequestCapacity() {
375
+ const remainingCapacityMap = {};
376
+
377
+ Object.keys(this.modelConfig).forEach(model => {
378
+ const modelTokens = this.tokenModelMap[model] || [];
379
+
380
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
381
+
382
+ const totalUsedRequests = modelTokens.reduce((sum, tokenEntry) => {
383
+ return sum + (tokenEntry.RequestCount || 0);
384
+ }, 0);
385
+
386
+ // 计算剩余可用请求数量
387
+ const remainingCapacity = (modelTokens.length * modelRequestFrequency) - totalUsedRequests;
388
+ remainingCapacityMap[model] = Math.max(0, remainingCapacity);
389
+ });
390
+
391
+ return remainingCapacityMap;
392
+ }
393
+
394
+ getTokenArrayForModel(modelId) {
395
+ const normalizedModel = this.normalizeModelName(modelId);
396
+ return this.tokenModelMap[normalizedModel] || [];
397
+ }
398
+
399
+ startTokenResetProcess() {
400
+ if (this.tokenResetTimer) {
401
+ clearInterval(this.tokenResetTimer);
402
+ }
403
+
404
+ this.tokenResetTimer = setInterval(() => {
405
+ const now = Date.now();
406
+
407
+ this.expiredTokens.forEach(expiredTokenInfo => {
408
+ const { token, model, expiredTime } = expiredTokenInfo;
409
+ const expirationTime = this.modelConfig[model].ExpirationTime;
410
+ if (now - expiredTime >= expirationTime) {
411
+ if (!this.tokenModelMap[model].some(entry => entry.token === token)) {
412
+ this.tokenModelMap[model].push({
413
+ token: token,
414
+ RequestCount: 0,
415
+ AddedTime: now,
416
+ StartCallTime: null
417
+ });
418
+ }
419
+ const sso = token.split("sso=")[1].split(";")[0];
420
+
421
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
422
+ this.tokenStatusMap[sso][model].isValid = true;
423
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
424
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
425
+ }
426
+
427
+ this.expiredTokens.delete(expiredTokenInfo);
428
+ }
429
+ });
430
+
431
+ Object.keys(this.modelConfig).forEach(model => {
432
+ if (!this.tokenModelMap[model]) return;
433
+
434
+ const processedTokens = this.tokenModelMap[model].map(tokenEntry => {
435
+ if (!tokenEntry.StartCallTime) return tokenEntry;
436
+
437
+ const expirationTime = this.modelConfig[model].ExpirationTime;
438
+ if (now - tokenEntry.StartCallTime >= expirationTime) {
439
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
440
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
441
+ this.tokenStatusMap[sso][model].isValid = true;
442
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
443
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
444
+ }
445
+
446
+ return {
447
+ ...tokenEntry,
448
+ RequestCount: 0,
449
+ StartCallTime: null
450
+ };
451
+ }
452
+
453
+ return tokenEntry;
454
+ });
455
+
456
+ this.tokenModelMap[model] = processedTokens;
457
+ });
458
+ }, 1 * 60 * 60 * 1000);
459
+ }
460
+
461
+ getAllTokens() {
462
+ const allTokens = new Set();
463
+ Object.values(this.tokenModelMap).forEach(modelTokens => {
464
+ modelTokens.forEach(entry => allTokens.add(entry.token));
465
+ });
466
+ return Array.from(allTokens);
467
+ }
468
+
469
+ getTokenStatusMap() {
470
+ return this.tokenStatusMap;
471
+ }
472
+ }
473
+
474
+
475
+ class Utils {
476
+ static delay(time) {
477
+ return new Promise(function (resolve) {
478
+ setTimeout(resolve, time)
479
+ });
480
+ }
481
+ static async organizeSearchResults(searchResults) {
482
+ // 确保传入的是有效的搜索结果对象
483
+ if (!searchResults || !searchResults.results) {
484
+ return '';
485
+ }
486
+
487
+ const results = searchResults.results;
488
+ const formattedResults = results.map((result, index) => {
489
+ // 处理可能为空的字段
490
+ const title = result.title || '未知标题';
491
+ const url = result.url || '#';
492
+ const preview = result.preview || '无预览内容';
493
+
494
+ return `\r\n<details><summary>资料[${index}]: ${title}</summary>\r\n${preview}\r\n\n[Link](${url})\r\n</details>`;
495
+ });
496
+ return formattedResults.join('\n\n');
497
+ }
498
+ static async createAuthHeaders(model) {
499
+ return await tokenManager.getNextTokenForModel(model);
500
+ }
501
+ }
502
+ class GrokTempCookieManager {
503
+ constructor() {
504
+ this.cookies = [];
505
+ this.currentIndex = 0;
506
+ this.isRefreshing = false;
507
+ this.initialCookieCount = CONFIG.API.GROK2_CONCURRENCY_LEVEL;
508
+ this.extractCount = 0;
509
+ }
510
+
511
+ async ensureCookies() {
512
+ // 如果 cookies 数量不足,则重新获取
513
+ if (this.cookies.length < this.initialCookieCount) {
514
+ await this.refreshCookies();
515
+ }
516
+ }
517
+ async extractGrokHeaders(browser) {
518
+ Logger.info("开始提取头信息", 'Server');
519
+ try {
520
+ const page = await browser.newPage();
521
+ await page.goto('https://grok.com/', { waitUntil: 'domcontentloaded' });
522
+ let waitTime = 0;
523
+ const targetHeaders = ['x-anonuserid', 'x-challenge', 'x-signature'];
524
+
525
+ while (true) {
526
+ const cookies = await page.cookies();
527
+ const extractedHeaders = cookies
528
+ .filter(cookie => targetHeaders.includes(cookie.name.toLowerCase()))
529
+ .map(cookie => `${cookie.name}=${cookie.value}`);
530
+
531
+ if (targetHeaders.every(header =>
532
+ extractedHeaders.some(cookie => cookie && cookie.startsWith(header + '='))
533
+ )) {
534
+ await browser.close();
535
+ Logger.info('提取的头信息:', JSON.stringify(extractedHeaders, null, 2), 'Server');
536
+ this.cookies.push(extractedHeaders.join(';'));
537
+ this.extractCount++;
538
+ return true;
539
+ }
540
+
541
+ await Utils.delay(500);
542
+ waitTime += 500;
543
+ if (waitTime >= 10000) {
544
+ await browser.close();
545
+ return null;
546
+ }
547
+ }
548
+ } catch (error) {
549
+ Logger.error('获取头信息出错:', error, 'Server');
550
+ return null;
551
+ }
552
+ }
553
+ async initializeTempCookies(count = 1) {
554
+ Logger.info(`开始初始化 ${count} 个临时账号认证信息`, 'Server');
555
+ const browserOptions = {
556
+ headless: true,
557
+ args: [
558
+ '--no-sandbox',
559
+ '--disable-setuid-sandbox',
560
+ '--disable-dev-shm-usage',
561
+ '--disable-gpu'
562
+ ],
563
+ executablePath: CONFIG.CHROME_PATH
564
+ };
565
+
566
+ const browsers = await Promise.all(
567
+ Array.from({ length: count }, () => puppeteer.launch(browserOptions))
568
+ );
569
+
570
+ const cookiePromises = browsers.map(browser => this.extractGrokHeaders(browser));
571
+ return Promise.all(cookiePromises);
572
+ }
573
+ async refreshCookies() {
574
+ if (this.isRefreshing) return;
575
+ this.isRefreshing = true;
576
+ this.extractCount = 0;
577
+ try {
578
+ // 获取新的 cookies
579
+ let retryCount = 0;
580
+ let remainingCount = this.initialCookieCount - this.cookies.length;
581
+
582
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
583
+ await this.initializeTempCookies(remainingCount);
584
+ if (this.extractCount != remainingCount) {
585
+ if (this.extractCount == 0) {
586
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
587
+ } else if (this.extractCount < remainingCount) {
588
+ remainingCount -= this.extractCount;
589
+ this.extractCount = 0;
590
+ retryCount++;
591
+ await Utils.delay(1000 * retryCount);
592
+ } else {
593
+ break;
594
+ }
595
+ } else {
596
+ break;
597
+ }
598
+ }
599
+ if (this.currentIndex >= this.cookies.length) {
600
+ this.currentIndex = 0;
601
+ }
602
+
603
+ if (this.cookies.length < this.initialCookieCount) {
604
+ if (this.cookies.length !== 0) {
605
+ // 如果已经获取到一些 TempCookies,则只提示警告错误
606
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
607
+ } else {
608
+ // 如果未获取到任何 TempCookies,则抛出错误
609
+ throw new Error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
610
+ }
611
+ }
612
+ } catch (error) {
613
+ Logger.error('刷新 cookies 失败:', error);
614
+ } finally {
615
+ Logger.info(`已提取${this.cookies.length}个TempCookies`, 'Server');
616
+ Logger.info(`提取的TempCookies为${JSON.stringify(this.cookies, null, 2)}`, 'Server');
617
+ this.isRefreshing = false;
618
+ }
619
+ }
620
+ }
621
+
622
+ class GrokApiClient {
623
+ constructor(modelId) {
624
+ if (!CONFIG.MODELS[modelId]) {
625
+ throw new Error(`不支持的模型: ${modelId}`);
626
+ }
627
+ this.modelId = CONFIG.MODELS[modelId];
628
+ }
629
+
630
+ processMessageContent(content) {
631
+ if (typeof content === 'string') return content;
632
+ return null;
633
+ }
634
+ // 获取图片类型
635
+ getImageType(base64String) {
636
+ let mimeType = 'image/jpeg';
637
+ if (base64String.includes('data:image')) {
638
+ const matches = base64String.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,/);
639
+ if (matches) {
640
+ mimeType = matches[1];
641
+ }
642
+ }
643
+ const extension = mimeType.split('/')[1];
644
+ const fileName = `image.${extension}`;
645
+
646
+ return {
647
+ mimeType: mimeType,
648
+ fileName: fileName
649
+ };
650
+ }
651
+
652
+ async uploadBase64Image(base64Data, url) {
653
+ try {
654
+ // 处理 base64 数据
655
+ let imageBuffer;
656
+ if (base64Data.includes('data:image')) {
657
+ imageBuffer = base64Data.split(',')[1];
658
+ } else {
659
+ imageBuffer = base64Data
660
+ }
661
+ const { mimeType, fileName } = this.getImageType(base64Data);
662
+ let uploadData = {
663
+ rpc: "uploadFile",
664
+ req: {
665
+ fileName: fileName,
666
+ fileMimeType: mimeType,
667
+ content: imageBuffer
668
+ }
669
+ };
670
+ Logger.info("发送图片请求", 'Server');
671
+ // 发送请求
672
+ const response = await fetch(url, {
673
+ method: 'POST',
674
+ headers: {
675
+ ...CONFIG.DEFAULT_HEADERS,
676
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
677
+ },
678
+ body: JSON.stringify(uploadData)
679
+ });
680
+
681
+ if (!response.ok) {
682
+ Logger.error(`上传图片失败,状态码:${response.status},原因:${response.error}`, 'Server');
683
+ return '';
684
+ }
685
+
686
+ const result = await response.json();
687
+ Logger.info('上传图片成功:', result, 'Server');
688
+ return result.fileMetadataId;
689
+
690
+ } catch (error) {
691
+ Logger.error(error, 'Server');
692
+ return '';
693
+ }
694
+ }
695
+
696
+ async prepareChatRequest(request) {
697
+ if ((request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') && !CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY && request.stream) {
698
+ throw new Error(`该模型流式输出需要配置PICGO或者TUMY图床密钥!`);
699
+ }
700
+
701
+ // 处理画图模型的消息限制
702
+ let todoMessages = request.messages;
703
+ if (request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') {
704
+ const lastMessage = todoMessages[todoMessages.length - 1];
705
+ if (lastMessage.role !== 'user') {
706
+ throw new Error('画图模型的最后一条消息必须是用户消息!');
707
+ }
708
+ todoMessages = [lastMessage];
709
+ }
710
+
711
+ const fileAttachments = [];
712
+ let messages = '';
713
+ let lastRole = null;
714
+ let lastContent = '';
715
+ const search = request.model === 'grok-2-search' || request.model === 'grok-3-search';
716
+
717
+ // 移除<think>标签及其内容和base64图片
718
+ const removeThinkTags = (text) => {
719
+ text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
720
+ text = text.replace(/!\[image\]\(data:.*?base64,.*?\)/g, '[图片]');
721
+ return text;
722
+ };
723
+
724
+ const processImageUrl = async (content) => {
725
+ if (content.type === 'image_url' && content.image_url.url.includes('data:image')) {
726
+ const imageResponse = await this.uploadBase64Image(
727
+ content.image_url.url,
728
+ `${CONFIG.API.BASE_URL}/api/rpc`
729
+ );
730
+ return imageResponse;
731
+ }
732
+ return null;
733
+ };
734
+
735
+ const processContent = async (content) => {
736
+ if (Array.isArray(content)) {
737
+ let textContent = '';
738
+ for (const item of content) {
739
+ if (item.type === 'image_url') {
740
+ textContent += (textContent ? '\n' : '') + "[图片]";
741
+ } else if (item.type === 'text') {
742
+ textContent += (textContent ? '\n' : '') + removeThinkTags(item.text);
743
+ }
744
+ }
745
+ return textContent;
746
+ } else if (typeof content === 'object' && content !== null) {
747
+ if (content.type === 'image_url') {
748
+ return "[图片]";
749
+ } else if (content.type === 'text') {
750
+ return removeThinkTags(content.text);
751
+ }
752
+ }
753
+ return removeThinkTags(this.processMessageContent(content));
754
+ };
755
+
756
+ for (const current of todoMessages) {
757
+ const role = current.role === 'assistant' ? 'assistant' : 'user';
758
+ const isLastMessage = current === todoMessages[todoMessages.length - 1];
759
+
760
+ // 处理图片附件
761
+ if (isLastMessage && current.content) {
762
+ if (Array.isArray(current.content)) {
763
+ for (const item of current.content) {
764
+ if (item.type === 'image_url') {
765
+ const processedImage = await processImageUrl(item);
766
+ if (processedImage) fileAttachments.push(processedImage);
767
+ }
768
+ }
769
+ } else if (current.content.type === 'image_url') {
770
+ const processedImage = await processImageUrl(current.content);
771
+ if (processedImage) fileAttachments.push(processedImage);
772
+ }
773
+ }
774
+
775
+ // 处理文本内容
776
+ const textContent = await processContent(current.content);
777
+
778
+ if (textContent || (isLastMessage && fileAttachments.length > 0)) {
779
+ if (role === lastRole && textContent) {
780
+ lastContent += '\n' + textContent;
781
+ messages = messages.substring(0, messages.lastIndexOf(`${role.toUpperCase()}: `)) +
782
+ `${role.toUpperCase()}: ${lastContent}\n`;
783
+ } else {
784
+ messages += `${role.toUpperCase()}: ${textContent || '[图片]'}\n`;
785
+ lastContent = textContent;
786
+ lastRole = role;
787
+ }
788
+ }
789
+ }
790
+
791
+ return {
792
+ temporary: CONFIG.API.IS_TEMP_CONVERSATION,
793
+ modelName: this.modelId,
794
+ message: messages.trim(),
795
+ fileAttachments: fileAttachments.slice(0, 4),
796
+ imageAttachments: [],
797
+ disableSearch: false,
798
+ enableImageGeneration: true,
799
+ returnImageBytes: false,
800
+ returnRawGrokInXaiRequest: false,
801
+ enableImageStreaming: false,
802
+ imageGenerationCount: 1,
803
+ forceConcise: false,
804
+ toolOverrides: {
805
+ imageGen: request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen',
806
+ webSearch: search,
807
+ xSearch: search,
808
+ xMediaSearch: search,
809
+ trendsSearch: search,
810
+ xPostAnalyze: search
811
+ },
812
+ enableSideBySide: true,
813
+ isPreset: false,
814
+ sendFinalMetadata: true,
815
+ customInstructions: "",
816
+ deepsearchPreset: request.model === 'grok-3-deepsearch' ? "default" : "",
817
+ isReasoning: request.model === 'grok-3-reasoning'
818
+ };
819
+ }
820
+ }
821
+
822
+ class MessageProcessor {
823
+ static createChatResponse(message, model, isStream = false) {
824
+ const baseResponse = {
825
+ id: `chatcmpl-${uuidv4()}`,
826
+ created: Math.floor(Date.now() / 1000),
827
+ model: model
828
+ };
829
+
830
+ if (isStream) {
831
+ return {
832
+ ...baseResponse,
833
+ object: 'chat.completion.chunk',
834
+ choices: [{
835
+ index: 0,
836
+ delta: {
837
+ content: message
838
+ }
839
+ }]
840
+ };
841
+ }
842
+
843
+ return {
844
+ ...baseResponse,
845
+ object: 'chat.completion',
846
+ choices: [{
847
+ index: 0,
848
+ message: {
849
+ role: 'assistant',
850
+ content: message
851
+ },
852
+ finish_reason: 'stop'
853
+ }],
854
+ usage: null
855
+ };
856
+ }
857
+ }
858
+ async function processModelResponse(response, model) {
859
+ let result = { token: null, imageUrl: null }
860
+ if (CONFIG.IS_IMG_GEN) {
861
+ if (response?.cachedImageGenerationResponse && !CONFIG.IS_IMG_GEN2) {
862
+ result.imageUrl = response.cachedImageGenerationResponse.imageUrl;
863
+ }
864
+ return result;
865
+ }
866
+
867
+ //非生图模型的处理
868
+ switch (model) {
869
+ case 'grok-2':
870
+ result.token = response?.token;
871
+ return result;
872
+ case 'grok-2-search':
873
+ case 'grok-3-search':
874
+ if (response?.webSearchResults && CONFIG.ISSHOW_SEARCH_RESULTS) {
875
+ result.token = `\r\n<think>${await Utils.organizeSearchResults(response.webSearchResults)}</think>\r\n`;
876
+ } else {
877
+ result.token = response?.token;
878
+ }
879
+ return result;
880
+ case 'grok-3':
881
+ result.token = response?.token;
882
+ return result;
883
+ case 'grok-3-deepsearch':
884
+ if (response?.messageTag === "final") {
885
+ result.token = response?.token;
886
+ }
887
+ return result;
888
+ case 'grok-3-reasoning':
889
+ if (response?.isThinking && !CONFIG.SHOW_THINKING) return result;
890
+
891
+ if (response?.isThinking && !CONFIG.IS_THINKING) {
892
+ result.token = "<think>" + response?.token;
893
+ CONFIG.IS_THINKING = true;
894
+ } else if (!response.isThinking && CONFIG.IS_THINKING) {
895
+ result.token = "</think>" + response?.token;
896
+ CONFIG.IS_THINKING = false;
897
+ } else {
898
+ result.token = response?.token;
899
+ }
900
+ return result;
901
+ }
902
+ return result;
903
+ }
904
+
905
+ async function handleResponse(response, model, res, isStream) {
906
+ try {
907
+ const stream = response.body;
908
+ let buffer = '';
909
+ let fullResponse = '';
910
+ const dataPromises = [];
911
+ if (isStream) {
912
+ res.setHeader('Content-Type', 'text/event-stream');
913
+ res.setHeader('Cache-Control', 'no-cache');
914
+ res.setHeader('Connection', 'keep-alive');
915
+ }
916
+ CONFIG.IS_THINKING = false;
917
+ CONFIG.IS_IMG_GEN = false;
918
+ CONFIG.IS_IMG_GEN2 = false;
919
+ Logger.info("开始处理流式响应", 'Server');
920
+
921
+ return new Promise((resolve, reject) => {
922
+ stream.on('data', async (chunk) => {
923
+ buffer += chunk.toString();
924
+ const lines = buffer.split('\n');
925
+ buffer = lines.pop() || '';
926
+
927
+ for (const line of lines) {
928
+ if (!line.trim()) continue;
929
+ try {
930
+ const linejosn = JSON.parse(line.trim());
931
+ if (linejosn?.error) {
932
+ Logger.error(JSON.stringify(linejosn, null, 2), 'Server');
933
+ if (linejosn.error?.name === "RateLimitError") {
934
+ CONFIG.API.TEMP_COOKIE = null;
935
+ }
936
+ stream.destroy();
937
+ reject(new Error("RateLimitError"));
938
+ return;
939
+ }
940
+ let response = linejosn?.result?.response;
941
+ if (!response) continue;
942
+ if (response?.doImgGen || response?.imageAttachmentInfo) {
943
+ CONFIG.IS_IMG_GEN = true;
944
+ }
945
+ const processPromise = (async () => {
946
+ const result = await processModelResponse(response, model);
947
+
948
+ if (result.token) {
949
+ if (isStream) {
950
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(result.token, model, true))}\n\n`);
951
+ } else {
952
+ fullResponse += result.token;
953
+ }
954
+ }
955
+ if (result.imageUrl) {
956
+ CONFIG.IS_IMG_GEN2 = true;
957
+ const dataImage = await handleImageResponse(result.imageUrl);
958
+ if (isStream) {
959
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(dataImage, model, true))}\n\n`);
960
+ } else {
961
+ res.json(MessageProcessor.createChatResponse(dataImage, model));
962
+ }
963
+ }
964
+ })();
965
+ dataPromises.push(processPromise);
966
+ } catch (error) {
967
+ Logger.error(error, 'Server');
968
+ continue;
969
+ }
970
+ }
971
+ });
972
+
973
+ stream.on('end', async () => {
974
+ try {
975
+ await Promise.all(dataPromises);
976
+ if (isStream) {
977
+ res.write('data: [DONE]\n\n');
978
+ res.end();
979
+ } else {
980
+ if (!CONFIG.IS_IMG_GEN2) {
981
+ res.json(MessageProcessor.createChatResponse(fullResponse, model));
982
+ }
983
+ }
984
+ resolve();
985
+ } catch (error) {
986
+ Logger.error(error, 'Server');
987
+ reject(error);
988
+ }
989
+ });
990
+
991
+ stream.on('error', (error) => {
992
+ Logger.error(error, 'Server');
993
+ reject(error);
994
+ });
995
+ });
996
+ } catch (error) {
997
+ Logger.error(error, 'Server');
998
+ throw new Error(error);
999
+ }
1000
+ }
1001
+
1002
+ async function handleImageResponse(imageUrl) {
1003
+ const MAX_RETRIES = 2;
1004
+ let retryCount = 0;
1005
+ let imageBase64Response;
1006
+
1007
+ while (retryCount < MAX_RETRIES) {
1008
+ try {
1009
+ imageBase64Response = await fetch(`https://assets.grok.com/${imageUrl}`, {
1010
+ method: 'GET',
1011
+ headers: {
1012
+ ...DEFAULT_HEADERS,
1013
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
1014
+ }
1015
+ });
1016
+
1017
+ if (imageBase64Response.ok) break;
1018
+ retryCount++;
1019
+ if (retryCount === MAX_RETRIES) {
1020
+ throw new Error(`上游服务请求失败! status: ${imageBase64Response.status}`);
1021
+ }
1022
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1023
+
1024
+ } catch (error) {
1025
+ Logger.error(error, 'Server');
1026
+ retryCount++;
1027
+ if (retryCount === MAX_RETRIES) {
1028
+ throw error;
1029
+ }
1030
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1031
+ }
1032
+ }
1033
+
1034
+
1035
+ const arrayBuffer = await imageBase64Response.arrayBuffer();
1036
+ const imageBuffer = Buffer.from(arrayBuffer);
1037
+
1038
+ if (!CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY) {
1039
+ const base64Image = imageBuffer.toString('base64');
1040
+ const imageContentType = imageBase64Response.headers.get('content-type');
1041
+ return `![image](data:${imageContentType};base64,${base64Image})`
1042
+ }
1043
+
1044
+ Logger.info("开始上传图床", 'Server');
1045
+ const formData = new FormData();
1046
+ if (CONFIG.API.PICGO_KEY) {
1047
+ formData.append('source', imageBuffer, {
1048
+ filename: `image-${Date.now()}.jpg`,
1049
+ contentType: 'image/jpeg'
1050
+ });
1051
+ const formDataHeaders = formData.getHeaders();
1052
+ const responseURL = await fetch("https://www.picgo.net/api/1/upload", {
1053
+ method: "POST",
1054
+ headers: {
1055
+ ...formDataHeaders,
1056
+ "Content-Type": "multipart/form-data",
1057
+ "X-API-Key": CONFIG.API.PICGO_KEY
1058
+ },
1059
+ body: formData
1060
+ });
1061
+ if (!responseURL.ok) {
1062
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
1063
+ } else {
1064
+ Logger.info("生图成功", 'Server');
1065
+ const result = await responseURL.json();
1066
+ return `![image](${result.image.url})`
1067
+ }
1068
+ } else if (CONFIG.API.TUMY_KEY) {
1069
+ const formData = new FormData();
1070
+ formData.append('file', imageBuffer, {
1071
+ filename: `image-${Date.now()}.jpg`,
1072
+ contentType: 'image/jpeg'
1073
+ });
1074
+ const formDataHeaders = formData.getHeaders();
1075
+ const responseURL = await fetch("https://tu.my/api/v1/upload", {
1076
+ method: "POST",
1077
+ headers: {
1078
+ ...formDataHeaders,
1079
+ "Accept": "application/json",
1080
+ 'Authorization': `Bearer ${CONFIG.API.TUMY_KEY}`
1081
+ },
1082
+ body: formData
1083
+ });
1084
+ if (!responseURL.ok) {
1085
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1086
+ } else {
1087
+ try {
1088
+ const result = await responseURL.json();
1089
+ Logger.info("生图成功", 'Server');
1090
+ return `![image](${result.data.links.url})`
1091
+ } catch (error) {
1092
+ Logger.error(error, 'Server');
1093
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1094
+ }
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ const tokenManager = new AuthTokenManager();
1100
+ const tempCookieManager = new GrokTempCookieManager();
1101
+ await initialization();
1102
+
1103
+ // 中间件配置
1104
+ const app = express();
1105
+ app.use(Logger.requestLogger);
1106
+ app.use(express.json({ limit: CONFIG.SERVER.BODY_LIMIT }));
1107
+ app.use(express.urlencoded({ extended: true, limit: CONFIG.SERVER.BODY_LIMIT }));
1108
+ app.use(cors({
1109
+ origin: '*',
1110
+ methods: ['GET', 'POST', 'OPTIONS'],
1111
+ allowedHeaders: ['Content-Type', 'Authorization']
1112
+ }));
1113
+
1114
+
1115
+ app.get('/get/tokens', (req, res) => {
1116
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1117
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1118
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法获取轮询sso令牌状态' });
1119
+ } else if (authToken !== CONFIG.API.API_KEY) {
1120
+ return res.status(401).json({ error: 'Unauthorized' });
1121
+ }
1122
+ res.json(tokenManager.getTokenStatusMap());
1123
+ });
1124
+ app.post('/add/token', async (req, res) => {
1125
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1126
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1127
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法添加sso令牌' });
1128
+ } else if (authToken !== CONFIG.API.API_KEY) {
1129
+ return res.status(401).json({ error: 'Unauthorized' });
1130
+ }
1131
+ try {
1132
+ const sso = req.body.sso;
1133
+ await tokenManager.addToken(`sso-rw=${sso};sso=${sso}`);
1134
+ res.status(200).json(tokenManager.getTokenStatusMap()[sso]);
1135
+ } catch (error) {
1136
+ Logger.error(error, 'Server');
1137
+ res.status(500).json({ error: '添加sso令牌失败' });
1138
+ }
1139
+ });
1140
+ app.post('/delete/token', async (req, res) => {
1141
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1142
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1143
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法删除sso令牌' });
1144
+ } else if (authToken !== CONFIG.API.API_KEY) {
1145
+ return res.status(401).json({ error: 'Unauthorized' });
1146
+ }
1147
+ try {
1148
+ const sso = req.body.sso;
1149
+ await tokenManager.deleteToken(`sso-rw=${sso};sso=${sso}`);
1150
+ res.status(200).json({ message: '删除sso令牌成功' });
1151
+ } catch (error) {
1152
+ Logger.error(error, 'Server');
1153
+ res.status(500).json({ error: '删除sso令牌失败' });
1154
+ }
1155
+ });
1156
+
1157
+ app.get('/v1/models', (req, res) => {
1158
+ res.json({
1159
+ object: "list",
1160
+ data: Object.keys(tokenManager.tokenModelMap).map((model, index) => ({
1161
+ id: model,
1162
+ object: "model",
1163
+ created: Math.floor(Date.now() / 1000),
1164
+ owned_by: "grok",
1165
+ }))
1166
+ });
1167
+ });
1168
+
1169
+
1170
+ app.post('/v1/chat/completions', async (req, res) => {
1171
+ try {
1172
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1173
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1174
+ if (authToken) {
1175
+ const result = `sso=${authToken};ssp_rw=${authToken}`;
1176
+ tokenManager.setToken(result);
1177
+ } else {
1178
+ return res.status(401).json({ error: '自定义的SSO令牌缺失' });
1179
+ }
1180
+ } else if (authToken !== CONFIG.API.API_KEY) {
1181
+ return res.status(401).json({ error: 'Unauthorized' });
1182
+ }
1183
+ const { model, stream } = req.body;
1184
+ let isTempCookie = model.includes("grok-2") && CONFIG.API.IS_TEMP_GROK2;
1185
+ let retryCount = 0;
1186
+ const grokClient = new GrokApiClient(model);
1187
+ const requestPayload = await grokClient.prepareChatRequest(req.body);
1188
+ //Logger.info(`请求体: ${JSON.stringify(requestPayload, null, 2)}`, 'Server');
1189
+
1190
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
1191
+ retryCount++;
1192
+ if (isTempCookie) {
1193
+ CONFIG.API.SIGNATURE_COOKIE = CONFIG.API.TEMP_COOKIE;
1194
+ Logger.info(`已切换为临时令牌`, 'Server');
1195
+ } else {
1196
+ CONFIG.API.SIGNATURE_COOKIE = await Utils.createAuthHeaders(model);
1197
+ }
1198
+ if (!CONFIG.API.SIGNATURE_COOKIE) {
1199
+ throw new Error('该模型无可用令牌');
1200
+ }
1201
+ Logger.info(`当前令牌: ${JSON.stringify(CONFIG.API.SIGNATURE_COOKIE, null, 2)}`, 'Server');
1202
+ Logger.info(`当前可用模型的全部可用数量: ${JSON.stringify(tokenManager.getRemainingTokenRequestCapacity(), null, 2)}`, 'Server');
1203
+ const response = await fetch(`${CONFIG.API.BASE_URL}/rest/app-chat/conversations/new`, {
1204
+ method: 'POST',
1205
+ headers: {
1206
+ "accept": "text/event-stream",
1207
+ "baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
1208
+ "content-type": "text/plain;charset=UTF-8",
1209
+ "Connection": "keep-alive",
1210
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
1211
+ },
1212
+ body: JSON.stringify(requestPayload)
1213
+ });
1214
+
1215
+ if (response.ok) {
1216
+ Logger.info(`请求成功`, 'Server');
1217
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1218
+ try {
1219
+ await handleResponse(response, model, res, stream);
1220
+ Logger.info(`请求结束`, 'Server');
1221
+ return;
1222
+ } catch (error) {
1223
+ Logger.error(error, 'Server');
1224
+ if (isTempCookie) {
1225
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1226
+ if (tempCookieManager.cookies.length != 0) {
1227
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1228
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1229
+ tempCookieManager.ensureCookies()
1230
+ } else {
1231
+ try {
1232
+ await tempCookieManager.ensureCookies();
1233
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1234
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1235
+ } catch (error) {
1236
+ throw error;
1237
+ }
1238
+ }
1239
+ } else {
1240
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1241
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1242
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1243
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1244
+ }
1245
+ }
1246
+ }
1247
+ } else {
1248
+ if (response.status === 429) {
1249
+ if (isTempCookie) {
1250
+ // 移除当前失效的 cookie
1251
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1252
+ if (tempCookieManager.cookies.length != 0) {
1253
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1254
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1255
+ tempCookieManager.ensureCookies()
1256
+ } else {
1257
+ try {
1258
+ await tempCookieManager.ensureCookies();
1259
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1260
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1261
+ } catch (error) {
1262
+ throw error;
1263
+ }
1264
+ }
1265
+ } else {
1266
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1267
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1268
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1269
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1270
+ }
1271
+ }
1272
+ } else {
1273
+ // 非429错误直接抛出
1274
+ if (isTempCookie) {
1275
+ // 移除当前失效的 cookie
1276
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1277
+ if (tempCookieManager.cookies.length != 0) {
1278
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1279
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1280
+ tempCookieManager.ensureCookies()
1281
+ } else {
1282
+ try {
1283
+ await tempCookieManager.ensureCookies();
1284
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1285
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1286
+ } catch (error) {
1287
+ throw error;
1288
+ }
1289
+ }
1290
+ } else {
1291
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1292
+ Logger.error(`令牌异常错误状态!status: ${response.status}`, 'Server');
1293
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1294
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1295
+ }
1296
+ }
1297
+ }
1298
+ }
1299
+ throw new Error('当前模型所有令牌都已耗尽');
1300
+ } catch (error) {
1301
+ Logger.error(error, 'ChatAPI');
1302
+ res.status(500).json({
1303
+ error: {
1304
+ message: error.message || error,
1305
+ type: 'server_error'
1306
+ }
1307
+ });
1308
+ }
1309
+ });
1310
+
1311
+
1312
+ app.use((req, res) => {
1313
+ res.status(200).send('api运行正常');
1314
+ });
1315
+
1316
+
1317
+ app.listen(CONFIG.SERVER.PORT, () => {
1318
+ Logger.info(`服务器已启动,监听端口: ${CONFIG.SERVER.PORT}`, 'Server');
1319
+ });
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
+ }