├── .dockerignore ├── Dockerfile ├── docker-compose.yml ├── package.json ├── .github └── workflows │ └── docker-deploy.yml ├── start.bat ├── start.sh ├── logger.js ├── templates ├── login.html └── manager.html ├── README.md ├── index.js └── app.py /.dockerignore: -------------------------------------------------------------------------------- 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 11 | start.bat 12 | start.sh 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 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 | VOLUME ["/data"] 8 | 9 | COPY . . 10 | 11 | ENV PORT=3000 12 | EXPOSE 3000 13 | 14 | CMD ["python", "app.py"] 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 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 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 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 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/docker-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Docker Multiarch CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - 'README.md' # 忽略README.md修改触发 8 | - '.dockerignore' # 忽略.dockerignore修改触发 9 | pull_request: 10 | branches: [ "main" ] 11 | paths-ignore: 12 | - 'README.md' # 忽略README.md修改触发 13 | - '.dockerignore' # 忽略.dockerignore修改触发 14 | 15 | jobs: 16 | build-and-push: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Login to Docker Hub 30 | uses: docker/login-action@v2 31 | with: 32 | username: ${{ secrets.DOCKERHUB_USERNAME }} 33 | password: ${{ secrets.DOCKERHUB_TOKEN }} 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v3 37 | with: 38 | context: . 39 | platforms: linux/amd64,linux/arm64 40 | push: true 41 | tags: | 42 | ${{ secrets.DOCKERHUB_USERNAME }}/grok2api_python:latest 43 | ${{ secrets.DOCKERHUB_USERNAME }}/grok2api_python:${{ github.sha }} 44 | cache-from: type=gha 45 | cache-to: type=gha,mode=max 46 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @chcp 65001 >nul 2 | @echo off 3 | setlocal enabledelayedexpansion 4 | 5 | set "PYTHONIOENCODING=utf-8" 6 | 7 | set "GREEN=[92m" 8 | set "RED=[91m" 9 | set "YELLOW=[93m" 10 | set "RESET=[0m" 11 | 12 | set "VENV_NAME=myenv" 13 | 14 | :check_and_create_env 15 | if not exist ".env" ( 16 | echo %YELLOW%未找到 .env 文件,正在自动创建...%RESET% 17 | 18 | ( 19 | echo # 系统配置文件 20 | echo IS_TEMP_CONVERSATION=true 21 | echo IS_CUSTOM_SSO=false 22 | echo API_KEY=your_api_key 23 | echo PICGO_KEY=your_picgo_key 24 | echo TUMY_KEY=your_tumy_key 25 | echo PROXY=http://127.0.0.1:5200 26 | echo MANAGER_SWITCH=false 27 | echo ADMINPASSWORD=admin123 28 | echo CF_CLEARANCE=your_cloudflare_clearance 29 | echo PORT=5200 30 | echo SHOW_THINKING=true 31 | echo ISSHOW_SEARCH_RESULTS=true 32 | echo SSO=ssoCookie1;ssoCookie2;ssoCookie3 33 | ) > .env 34 | 35 | echo %GREEN%.env 文件已创建%RESET% 36 | echo %YELLOW%请手动编辑 .env 文件并配置您的密钥和设置%RESET% 37 | pause 38 | exit /b 0 39 | ) 40 | 41 | for /f "tokens=2 delims=." %%a in ('python --version 2^>^&1 ^| findstr /R "^Python [0-9]"') do set "PYTHON_VERSION=%%a" 42 | 43 | if %PYTHON_VERSION% LSS 8 ( 44 | echo %RED%错误:需要 Python 3.8 或更高版本%RESET% 45 | pause 46 | exit /b 1 47 | ) 48 | 49 | if not exist "%VENV_NAME%" ( 50 | echo %GREEN%创建虚拟环境...%RESET% 51 | python -m venv %VENV_NAME% 52 | ) 53 | 54 | call %VENV_NAME%\Scripts\activate 55 | 56 | python -m pip install --upgrade pip 57 | 58 | echo %GREEN%安装依赖...%RESET% 59 | pip install --no-cache-dir flask flask_cors requests curl_cffi werkzeug datetime python-dotenv loguru 60 | 61 | if %ERRORLEVEL% NEQ 0 ( 62 | echo %RED%依赖安装失败%RESET% 63 | deactivate 64 | pause 65 | exit /b 1 66 | ) 67 | 68 | echo %GREEN%启动应用...%RESET% 69 | python app.py 70 | 71 | deactivate 72 | 73 | pause -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | GREEN='\033[0;32m' 4 | RED='\033[0;31m' 5 | YELLOW='\033[0;33m' 6 | RESET='\033[0m' 7 | 8 | VENV_NAME="myenv" 9 | 10 | check_python_version() { 11 | python_version=$(python3 --version 2>&1 | awk '{print $2}' | cut -d. -f1-2) 12 | 13 | if [[ "$(printf '%s\n' "3.8" "$python_version" | sort -V | head -n1)" != "3.8" ]]; then 14 | echo -e "${RED}错误:需要 Python 3.8 或更高版本${RESET}" 15 | exit 1 16 | fi 17 | } 18 | 19 | create_env_file() { 20 | if [ ! -f ".env" ]; then 21 | echo -e "${YELLOW}未找到 .env 文件,正在自动创建...${RESET}" 22 | 23 | cat > .env << EOL 24 | # 系统配置文件 25 | IS_TEMP_CONVERSATION=true 26 | IS_CUSTOM_SSO=false 27 | API_KEY=your_api_key 28 | PICGO_KEY=your_picgo_key 29 | TUMY_KEY=your_tumy_key 30 | PROXY=http://127.0.0.1:5200 31 | MANAGER_SWITCH=false 32 | ADMINPASSWORD=admin123 33 | CF_CLEARANCE=your_cloudflare_clearance 34 | PORT=5200 35 | SHOW_THINKING=true 36 | ISSHOW_SEARCH_RESULTS=true 37 | SSO=ssoCookie1;ssoCookie2;ssoCookie3 38 | EOL 39 | 40 | echo -e "${GREEN}.env 文件已创建${RESET}" 41 | echo -e "${YELLOW}请手动编辑 .env 文件并配置您的密钥和设置${RESET}" 42 | exit 0 43 | fi 44 | } 45 | 46 | create_venv() { 47 | if [ ! -d "$VENV_NAME" ]; then 48 | echo -e "${GREEN}创建虚拟环境...${RESET}" 49 | python3 -m venv "$VENV_NAME" 50 | fi 51 | } 52 | 53 | main() { 54 | create_env_file 55 | 56 | check_python_version 57 | 58 | create_venv 59 | 60 | source "$VENV_NAME/bin/activate" 61 | 62 | python3 -m pip install --upgrade pip 63 | 64 | echo -e "${GREEN}安装依赖...${RESET}" 65 | pip install --no-cache-dir \ 66 | flask flask_cors requests curl_cffi \ 67 | werkzeug datetime python-dotenv loguru 68 | 69 | if [ $? -ne 0 ]; then 70 | echo -e "${RED}依赖安装失败${RESET}" 71 | deactivate 72 | exit 1 73 | fi 74 | 75 | echo -e "${GREEN}启动应用...${RESET}" 76 | python3 app.py 77 | 78 | deactivate 79 | } 80 | 81 | chmod +x "$0" 82 | 83 | main -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 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; -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 登录 6 | 7 | 59 | 60 | 61 |
62 |

管理员登录

63 |
64 | 65 | 66 |
67 |
68 | {% if error %} 69 |
密码错误
70 | 75 | {% endif %} 76 | 77 | 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # grok2API 接入指南:基于 python 的实现 3 | 4 | ## 项目简介 5 | 本项目提供了一种简单、高效的方式通过 Docker 部署 使用openAI的格式转换调用grok官网,进行api处理。 6 | 7 | >支持自动过cf屏蔽盾,需要自己ip没有被风控。如果被风控,将会升级为5秒盾,无法绕过。 8 | 9 | ## 如何检测ip是否被风控? 10 | 1. 打开无痕浏览器,输入https://grok.com 11 | 2. 直接进入则没有被风控,如果出现下图所示画面,则表示已经被风控,该ip无法使用本项目 12 | ![image](https://github.com/user-attachments/assets/0466aa57-9a31-4f7c-bd07-fece11f27646) 13 | 14 | 4. 如果风控后,过了5秒盾,会给与一个一年有效期的cf_clearance,可以将这个填入环境变量CF_CLEARANCE,这个cf_clearance和你的ip是绑定的,如果更换ip需要重新获取,可以提高破盾的稳定性(大概)。 15 | 5. 如果ip没有风控,不要加cf_clearance,加了可能反而因为校验问题出盾 16 | 17 | ### 功能特点 18 | 实现的功能: 19 | 1. 已支持文字生成图,使用grok-3-imageGen和grok-4-imageGen模型。 20 | 2. 已支持全部模型识图和传图,只会识别存储用户消息最新的一个图,历史记录图全部为占位符替代。 21 | 3. 已支持搜索功能,使用grok-3-search,可以选择是否关闭搜索结果 22 | 4. 已支持深度搜索功能,使用grok-3-deepsearch,grok-4-deepsearch,深度搜索支持think过程显示 23 | 5. 已支持推理模型功能,使用grok-3-reasoning 24 | 6. 已支持真流式,上面全部功能都可以在流式情况调用 25 | 7. 支持多账号轮询,在环境变量中配置 26 | 8. 可以选择是否移除思考模型的思考过程。 27 | 9. 支持自行设置轮询和负载均衡,而不依靠项目代码 28 | 10. 自动过CF屏蔽盾 29 | 11. 可自定义http和Socks5代理 30 | 12. 上下文40k时自动转换为文件以提高上下文限制 31 | 13. 已转换为openai格式。 32 | 14. 支持super会员账号token单独导入,暂不支持浏览器面板方式导入 33 | 34 | ## API 接口文档 35 | 36 | ### 模型管理 37 | | 接口 | 方法 | 路径 | 描述 | 38 | |------|------|------|------| 39 | | 模型列表 | GET | `/v1/models` | 获取可用模型列表 | 40 | | 对话 | POST | `/v1/chat/completions` | 发起对话请求 | 41 | 42 | ### SSO令牌管理与安全设置 43 | | 接口 | 方法 | 路径 | 请求体 | 描述 | 44 | |------|------|------|--------|------| 45 | | 添加SSO令牌 | POST | `/add/token` | `{sso: "eyXXXXXXXX"}` | 添加SSO认证令牌 | 46 | | 删除SSO令牌 | POST | `/delete/token` | `{sso: "eyXXXXXXXX"}` | 删除SSO认证令牌 | 47 | | 获取SSO令牌状态 | GET | `/get/tokens` | - | 查询所有SSO令牌状态 | 48 | | 修改cf_clearance | POST | `/set/cf_clearance` | `{cf_clearance: "cf_clearance=XXXXXXXX"}` | 更新cf_clearance Cookie | 49 | 50 | ### TOKEN管理界面 51 | 使用如下接口:http://127.0.0.1:3000/manager 52 | 53 | ![image](https://github.com/user-attachments/assets/9caedf30-5075-4edb-b5c4-96852647a43d) 54 | 55 | 56 | ### 环境变量具体配置 57 | 58 | |变量 | 说明 | 构建时是否必填 |示例| 59 | |--- | --- | ---| ---| 60 | |`MANAGER_SWITCH` | 是否开启管理界面 | (可以不填,默认是false) | `true/false`| 61 | |`ADMINPASSWORD` | 管理界面的管理员密码,请区别于API_KEY,并且设置高强度密码 | (MANAGER_SWITCH没有开启时可以不填,默认是无) | `OjB6*BLlT&nV2M$x`| 62 | |`IS_TEMP_CONVERSATION` | 是否开启临时会话,开启后会话历史记录不会保留在网页 | (可以不填,默认是false) | `true/false`| 63 | |`CF_CLEARANCE` | cf的5秒盾后的值,随便一个号过盾后的都可以,这个cf_clearance和你的ip是绑定的,如果更换ip需要重新获取。通用,可以提高破盾的稳定性 | (可以不填,默认无) | `cf_clearance=xxxxxx`| 64 | |`API_KEY` | 自定义认证鉴权密钥 | (可以不填,默认是sk-123456) | `sk-123456`| 65 | |`PROXY` | 代理设置,支持https和Socks5 | 可不填,默认无 | -| 66 | |`PICGO_KEY` | PicGo图床密钥,两个图床二选一 | 不填无法流式生图 | -| 67 | |`TUMY_KEY` | TUMY图床密钥,两个图床二选一 | 不填无法流式生图 | -| 68 | |`ISSHOW_SEARCH_RESULTS` | 是否显示搜索结果 | (可不填,默认关闭) | `true/false`| 69 | |`SSO` | Grok官网SSO Cookie,可以设置多个使用英文 , 分隔,我的代码里会对不同账号的SSO自动轮询和均衡 | (除非开启IS_CUSTOM_SSO否则和SSO_SUPER二选一) | `sso,sso`| 70 | |`SSO_SUPER` | Grok官网的会员账号的SSO Cookie,可以设置多个使用英文 , 分隔,我的代码里会对不同账号的SSO自动轮询和均衡 | (除非开启IS_CUSTOM_SSO否则否则和SSO二选一) | `sso,sso`| 71 | |`PORT` | 服务部署端口 | (可不填,默认3000) | `3000`| 72 | |`IS_CUSTOM_SSO` | 这是如果你想自己来自定义号池来轮询均衡,而不是通过我代码里已经内置的号池逻辑系统来为你轮询均衡启动的开关。开启后 API_KEY 需要设置为请求认证用的 sso cookie,同时SSO环境变量失效。一个apikey每次只能传入一个sso cookie 值,不支持一个请求里的apikey填入多个sso。想自动使用多个sso请关闭 IS_CUSTOM_SSO 这个环境变量,然后按照SSO环境变量要求在sso环境变量里填入多个sso,由我的代码里内置的号池系统来为你自动轮询 | (可不填,默认关闭) | `true/false`| 73 | |`SHOW_THINKING` | 是否显示思考模型的思考过程 | (可不填,默认关闭) | `true/false`| 74 | 75 | **注意事项**: 76 | - 所有POST请求需要在请求体中携带相应的认证信息 77 | - SSO令牌和cf_clearance是敏感信息,请妥善保管 78 | 79 | ## 方法一:Docker部署 80 | 81 | ### 1. 获取项目 82 | 克隆我的仓库:[grok2api](https://github.com/xLmiler/grok2api) 83 | ### 2. 部署选项 84 | 85 | #### 方式A:直接使用Docker镜像 86 | ```bash 87 | docker run -it -d --name grok2api_python \ 88 | -p 3000:3000 \ 89 | -v $(pwd)/data:/data \ 90 | -e IS_TEMP_CONVERSATION=false \ 91 | -e API_KEY=your_api_key \ 92 | -e TUMY_KEY=你的图床key,和PICGO_KEY 二选一 \ 93 | -e PICGO_KEY=你的图床key,和TUMY_KEY二选一 \ 94 | -e IS_CUSTOM_SSO=false \ 95 | -e ISSHOW_SEARCH_RESULTS=false \ 96 | -e PORT=3000 \ 97 | -e SHOW_THINKING=true \ 98 | -e SSO=your_sso \ 99 | yxmiler/grok2api_python:latest 100 | ``` 101 | 102 | #### 方式B:使用Docker Compose 103 | ````artifact 104 | version: '3.8' 105 | services: 106 | grok2api_python: 107 | image: yxmiler/grok2api_python:latest 108 | container_name: grok2api_python 109 | ports: 110 | - "3000:3000" 111 | volumes: 112 | - ./data:/data 113 | environment: 114 | - API_KEY=your_api_key 115 | - IS_TEMP_CONVERSATION=true 116 | - IS_CUSTOM_SSO=false 117 | - ISSHOW_SEARCH_RESULTS=false 118 | - PORT=3000 119 | - SHOW_THINKING=true 120 | - SSO=your_sso 121 | restart: unless-stopped 122 | ```` 123 | 124 | #### 方式C:自行构建 125 | 1. 克隆仓库 126 | 2. 构建镜像 127 | ```bash 128 | docker build -t yourusername/grok2api . 129 | ``` 130 | 3. 运行容器 131 | ```bash 132 | docker run -it -d --name grok2api \ 133 | -p 3000:3000 \ 134 | -v $(pwd)/data:/data \ 135 | -e IS_TEMP_CONVERSATION=false \ 136 | -e API_KEY=your_api_key \ 137 | -e IS_CUSTOM_SSO=false \ 138 | -e ISSHOW_SEARCH_RESULTS=false \ 139 | -e PORT=3000 \ 140 | -e SHOW_THINKING=true \ 141 | -e SSO=your_sso \ 142 | yourusername/grok2api:latest 143 | ``` 144 | 145 | ## 方法二:Hugging Face部署 146 | 147 | ### 部署地址 148 | [GrokPythonService](https://huggingface.co/spaces/yxmiler/GrokPythonService) 149 | 150 | ### 可用模型列表 151 | - `grok-4` 152 | - `grok-4-reasoning` 153 | - `grok-4-imageGen` 154 | - `grok-3` 155 | - `grok-3-search` 156 | - `grok-3-imageGen` 157 | - `grok-3-deepsearch` 158 | - `grok-3-deepersearch` 159 | - `grok-3-reasoning` 160 | 161 | ### 模型可用次数参考 162 | - grok-4所有模型 合计一共:30次 每3小时刷新 仅grok会员可用 163 | - grok-3,grok-3-search,grok-3-imageGen 合计:20次 每3小时刷新 164 | - grok-3-deepsearch:8次 每24小时刷新 165 | - grok-3-deepersearch:3次 每24小时刷新 166 | - grok-3-reasoning:10次 每24小时刷新 167 | 168 | ### cookie的获取办法: 169 | 1. 打开[grok官网](https://grok.com/) 170 | 2. 复制如下的SSO的cookie的值填入SSO变量即可 171 | ![9EA{{UY6 PU~PENQHYO5JS7](https://github.com/user-attachments/assets/539d4a53-9352-49fd-8657-e942a94f44e9) 172 | 173 | ### cf_clearance的获取办法: 174 | 1. 随便登录一个账号打开[grok官网](https://grok.com/) 175 | 2. 复制如下的cf_clearance的cookie的值填入CF_CLEARANCE变量即可,只需要填入一个,不可以多个,格式cf_clearance=xxxxx 176 | ![W1F8FTBT`~17(TFP5LS173Q](https://github.com/user-attachments/assets/f5603267-316a-4126-8c77-a84a91ee6344) 177 | 178 | 179 | ## 备注 180 | - 消息基于用户的伪造连续对话 181 | - 可能存在一定程度的降智 182 | - 生图模型不支持历史对话,仅支持生图。 183 | ## 补充说明 184 | - 如需使用流式生图的图像功能,需在[PicGo图床](https://www.picgo.net/)或者[tumy图床](https://tu.my/)申请API Key,前者似乎无法注册了,没有前面图床账号的可以选择后一个图床。 185 | - 自动移除历史消息里的think过程,同时如果历史消息里包含里base64图片文本,而不是通过文件上传的方式上传,则自动转换为[图片]占用符。 186 | 187 | ## 注意事项 188 | ⚠️ 本项目仅供学习和研究目的,请遵守相关使用条款。 189 | 190 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 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
资料[${index}]: ${title}\r\n${preview}\r\n\n[Link](${url})\r\n
`; 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 | // 移除标签及其内容和base64图片 718 | const removeThinkTags = (text) => { 719 | text = text.replace(/[\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${await Utils.organizeSearchResults(response.webSearchResults)}\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 = "" + response?.token; 893 | CONFIG.IS_THINKING = true; 894 | } else if (!response.isThinking && CONFIG.IS_THINKING) { 895 | result.token = "" + 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 | }); 1320 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import uuid 4 | import time 5 | import base64 6 | import sys 7 | import inspect 8 | import secrets 9 | from loguru import logger 10 | from pathlib import Path 11 | 12 | import requests 13 | from flask import Flask, request, Response, jsonify, stream_with_context, render_template, redirect, session 14 | from curl_cffi import requests as curl_requests 15 | from werkzeug.middleware.proxy_fix import ProxyFix 16 | 17 | class Logger: 18 | def __init__(self, level="INFO", colorize=True, format=None): 19 | logger.remove() 20 | 21 | if format is None: 22 | format = ( 23 | "{time:YYYY-MM-DD HH:mm:ss} | " 24 | "{level: <8} | " 25 | "{extra[filename]}:{extra[function]}:{extra[lineno]} | " 26 | "{message}" 27 | ) 28 | 29 | logger.add( 30 | sys.stderr, 31 | level=level, 32 | format=format, 33 | colorize=colorize, 34 | backtrace=True, 35 | diagnose=True 36 | ) 37 | 38 | self.logger = logger 39 | 40 | def _get_caller_info(self): 41 | frame = inspect.currentframe() 42 | try: 43 | caller_frame = frame.f_back.f_back 44 | full_path = caller_frame.f_code.co_filename 45 | function = caller_frame.f_code.co_name 46 | lineno = caller_frame.f_lineno 47 | 48 | filename = os.path.basename(full_path) 49 | 50 | return { 51 | 'filename': filename, 52 | 'function': function, 53 | 'lineno': lineno 54 | } 55 | finally: 56 | del frame 57 | 58 | def info(self, message, source="API"): 59 | caller_info = self._get_caller_info() 60 | self.logger.bind(**caller_info).info(f"[{source}] {message}") 61 | 62 | def error(self, message, source="API"): 63 | caller_info = self._get_caller_info() 64 | 65 | if isinstance(message, Exception): 66 | self.logger.bind(**caller_info).exception(f"[{source}] {str(message)}") 67 | else: 68 | self.logger.bind(**caller_info).error(f"[{source}] {message}") 69 | 70 | def warning(self, message, source="API"): 71 | caller_info = self._get_caller_info() 72 | self.logger.bind(**caller_info).warning(f"[{source}] {message}") 73 | 74 | def debug(self, message, source="API"): 75 | caller_info = self._get_caller_info() 76 | self.logger.bind(**caller_info).debug(f"[{source}] {message}") 77 | 78 | async def request_logger(self, request): 79 | caller_info = self._get_caller_info() 80 | self.logger.bind(**caller_info).info(f"请求: {request.method} {request.path}", "Request") 81 | 82 | logger = Logger(level="INFO") 83 | DATA_DIR = Path("/data") 84 | 85 | if not DATA_DIR.exists(): 86 | DATA_DIR.mkdir(parents=True, exist_ok=True) 87 | CONFIG = { 88 | "MODELS": { 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-deepersearch": "grok-3", 94 | "grok-3-reasoning": "grok-3", 95 | 'grok-4': 'grok-4', 96 | 'grok-4-reasoning': 'grok-4', 97 | 'grok-4-imageGen': 'grok-4', 98 | 'grok-4-deepsearch': 'grok-4' 99 | }, 100 | "API": { 101 | "IS_TEMP_CONVERSATION": os.environ.get("IS_TEMP_CONVERSATION", "true").lower() == "true", 102 | "IS_CUSTOM_SSO": os.environ.get("IS_CUSTOM_SSO", "false").lower() == "true", 103 | "BASE_URL": "https://grok.com", 104 | "API_KEY": os.environ.get("API_KEY", "sk-123456"), 105 | "SIGNATURE_COOKIE": None, 106 | "PICGO_KEY": os.environ.get("PICGO_KEY") or None, 107 | "TUMY_KEY": os.environ.get("TUMY_KEY") or None, 108 | "RETRY_TIME": 1000, 109 | "PROXY": os.environ.get("PROXY") or None 110 | }, 111 | "ADMIN": { 112 | "MANAGER_SWITCH": os.environ.get("MANAGER_SWITCH") or None, 113 | "PASSWORD": os.environ.get("ADMINPASSWORD") or None 114 | }, 115 | "SERVER": { 116 | "COOKIE": None, 117 | "CF_CLEARANCE":os.environ.get("CF_CLEARANCE") or None, 118 | "PORT": int(os.environ.get("PORT", 5200)) 119 | }, 120 | "RETRY": { 121 | "RETRYSWITCH": False, 122 | "MAX_ATTEMPTS": 2 123 | }, 124 | "TOKEN_STATUS_FILE": str(DATA_DIR / "token_status.json"), 125 | "SHOW_THINKING": os.environ.get("SHOW_THINKING").lower() == "true", 126 | "IS_THINKING": False, 127 | "IS_IMG_GEN": False, 128 | "IS_IMG_GEN2": False, 129 | "ISSHOW_SEARCH_RESULTS": os.environ.get("ISSHOW_SEARCH_RESULTS", "true").lower() == "true", 130 | "IS_SUPER_GROK": os.environ.get("IS_SUPER_GROK", "false").lower() == "true" 131 | } 132 | 133 | 134 | DEFAULT_HEADERS = { 135 | 'Accept': '*/*', 136 | 'Accept-Language': 'zh-CN,zh;q=0.9', 137 | 'Accept-Encoding': 'gzip, deflate, br, zstd', 138 | 'Content-Type': 'text/plain;charset=UTF-8', 139 | 'Connection': 'keep-alive', 140 | 'Origin': 'https://grok.com', 141 | 'Priority': 'u=1, i', 142 | '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', 143 | 'Sec-Ch-Ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', 144 | 'Sec-Ch-Ua-Mobile': '?0', 145 | 'Sec-Ch-Ua-Platform': '"macOS"', 146 | 'Sec-Fetch-Dest': 'empty', 147 | 'Sec-Fetch-Mode': 'cors', 148 | 'Sec-Fetch-Site': 'same-origin', 149 | 'Baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c', 150 | 'x-statsig-id': 'ZTpUeXBlRXJyb3I6IENhbm5vdCByZWFkIHByb3BlcnRpZXMgb2YgdW5kZWZpbmVkIChyZWFkaW5nICdjaGlsZE5vZGVzJyk=' 151 | } 152 | 153 | class AuthTokenManager: 154 | def __init__(self): 155 | self.token_model_map = {} 156 | self.expired_tokens = set() 157 | self.token_status_map = {} 158 | self.model_super_config = { 159 | "grok-3": { 160 | "RequestFrequency": 100, 161 | "ExpirationTime": 3 * 60 * 60 * 1000 # 3小时 162 | }, 163 | "grok-3-deepsearch": { 164 | "RequestFrequency": 30, 165 | "ExpirationTime": 24 * 60 * 60 * 1000 # 3小时 166 | }, 167 | "grok-3-deepersearch": { 168 | "RequestFrequency": 10, 169 | "ExpirationTime": 3 * 60 * 60 * 1000 # 23小时 170 | }, 171 | "grok-3-reasoning": { 172 | "RequestFrequency": 30, 173 | "ExpirationTime": 3 * 60 * 60 * 1000 # 3小时 174 | }, 175 | "grok-4": { 176 | "RequestFrequency": 20, 177 | "ExpirationTime": 3 * 60 * 60 * 1000 # 3小时 178 | } 179 | } 180 | self.model_normal_config = { 181 | "grok-3": { 182 | "RequestFrequency": 20, 183 | "ExpirationTime": 3 * 60 * 60 * 1000 # 3小时 184 | }, 185 | "grok-3-deepsearch": { 186 | "RequestFrequency": 10, 187 | "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时 188 | }, 189 | "grok-3-deepersearch": { 190 | "RequestFrequency": 3, 191 | "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时 192 | }, 193 | "grok-3-reasoning": { 194 | "RequestFrequency": 8, 195 | "ExpirationTime": 24 * 60 * 60 * 1000 # 24小时 196 | } 197 | } 198 | self.model_config = self.model_normal_config 199 | self.token_reset_switch = False 200 | self.token_reset_timer = None 201 | def save_token_status(self): 202 | try: 203 | with open(CONFIG["TOKEN_STATUS_FILE"], 'w', encoding='utf-8') as f: 204 | json.dump(self.token_status_map, f, indent=2, ensure_ascii=False) 205 | logger.info("令牌状态已保存到配置文件", "TokenManager") 206 | except Exception as error: 207 | logger.error(f"保存令牌状态失败: {str(error)}", "TokenManager") 208 | 209 | def load_token_status(self): 210 | try: 211 | token_status_file = Path(CONFIG["TOKEN_STATUS_FILE"]) 212 | if token_status_file.exists(): 213 | with open(token_status_file, 'r', encoding='utf-8') as f: 214 | self.token_status_map = json.load(f) 215 | logger.info("已从配置文件加载令牌状态", "TokenManager") 216 | except Exception as error: 217 | logger.error(f"加载令牌状态失败: {str(error)}", "TokenManager") 218 | def add_token(self, tokens, isinitialization=False): 219 | tokenType = tokens.get("type") 220 | tokenSso = tokens.get("token") 221 | if tokenType == "normal": 222 | self.model_config = self.model_normal_config 223 | else: 224 | self.model_config = self.model_super_config 225 | sso = tokenSso.split("sso=")[1].split(";")[0] 226 | 227 | for model in self.model_config.keys(): 228 | if model not in self.token_model_map: 229 | self.token_model_map[model] = [] 230 | if sso not in self.token_status_map: 231 | self.token_status_map[sso] = {} 232 | 233 | existing_token_entry = next((entry for entry in self.token_model_map[model] if entry["token"] == tokenSso), None) 234 | 235 | if not existing_token_entry: 236 | self.token_model_map[model].append({ 237 | "token": tokenSso, 238 | "MaxRequestCount": self.model_config[model]["RequestFrequency"], 239 | "RequestCount": 0, 240 | "AddedTime": int(time.time() * 1000), 241 | "StartCallTime": None, 242 | "type": tokenType 243 | }) 244 | 245 | if model not in self.token_status_map[sso]: 246 | self.token_status_map[sso][model] = { 247 | "isValid": True, 248 | "invalidatedTime": None, 249 | "totalRequestCount": 0, 250 | "isSuper":tokenType == "super" 251 | } 252 | if not isinitialization: 253 | self.save_token_status() 254 | 255 | def set_token(self, tokens): 256 | tokenType = tokens.get("type") 257 | tokenSso = tokens.get("token") 258 | if tokenType == "normal": 259 | self.model_config = self.model_normal_config 260 | else: 261 | self.model_config = self.model_super_config 262 | 263 | models = list(self.model_config.keys()) 264 | self.token_model_map = {model: [{ 265 | "token": tokenSso, 266 | "MaxRequestCount": self.model_config[model]["RequestFrequency"], 267 | "RequestCount": 0, 268 | "AddedTime": int(time.time() * 1000), 269 | "StartCallTime": None, 270 | "type": tokenType 271 | }] for model in models} 272 | 273 | sso = tokenSso.split("sso=")[1].split(";")[0] 274 | self.token_status_map[sso] = {model: { 275 | "isValid": True, 276 | "invalidatedTime": None, 277 | "totalRequestCount": 0, 278 | "isSuper":tokenType == "super" 279 | } for model in models} 280 | 281 | def delete_token(self, token): 282 | try: 283 | sso = token.split("sso=")[1].split(";")[0] 284 | for model in self.token_model_map: 285 | self.token_model_map[model] = [entry for entry in self.token_model_map[model] if entry["token"] != token] 286 | 287 | if sso in self.token_status_map: 288 | del self.token_status_map[sso] 289 | 290 | self.save_token_status() 291 | 292 | logger.info(f"令牌已成功移除: {token}", "TokenManager") 293 | return True 294 | except Exception as error: 295 | logger.error(f"令牌删除失败: {str(error)}") 296 | return False 297 | def reduce_token_request_count(self, model_id, count): 298 | try: 299 | normalized_model = self.normalize_model_name(model_id) 300 | 301 | if normalized_model not in self.token_model_map: 302 | logger.error(f"模型 {normalized_model} 不存在", "TokenManager") 303 | return False 304 | 305 | if not self.token_model_map[normalized_model]: 306 | logger.error(f"模型 {normalized_model} 没有可用的token", "TokenManager") 307 | return False 308 | 309 | token_entry = self.token_model_map[normalized_model][0] 310 | 311 | # 确保RequestCount不会小于0 312 | new_count = max(0, token_entry["RequestCount"] - count) 313 | reduction = token_entry["RequestCount"] - new_count 314 | 315 | token_entry["RequestCount"] = new_count 316 | 317 | # 更新token状态 318 | if token_entry["token"]: 319 | sso = token_entry["token"].split("sso=")[1].split(";")[0] 320 | if sso in self.token_status_map and normalized_model in self.token_status_map[sso]: 321 | self.token_status_map[sso][normalized_model]["totalRequestCount"] = max( 322 | 0, 323 | self.token_status_map[sso][normalized_model]["totalRequestCount"] - reduction 324 | ) 325 | return True 326 | 327 | except Exception as error: 328 | logger.error(f"重置校对token请求次数时发生错误: {str(error)}", "TokenManager") 329 | return False 330 | def get_next_token_for_model(self, model_id, is_return=False): 331 | normalized_model = self.normalize_model_name(model_id) 332 | 333 | if normalized_model not in self.token_model_map or not self.token_model_map[normalized_model]: 334 | return None 335 | 336 | token_entry = self.token_model_map[normalized_model][0] 337 | logger.info(f"token_entry: {token_entry}", "TokenManager") 338 | if is_return: 339 | return token_entry["token"] 340 | 341 | if token_entry: 342 | if token_entry["type"] == "super": 343 | self.model_config = self.model_super_config 344 | else: 345 | self.model_config = self.model_normal_config 346 | if token_entry["StartCallTime"] is None: 347 | token_entry["StartCallTime"] = int(time.time() * 1000) 348 | 349 | if not self.token_reset_switch: 350 | self.start_token_reset_process() 351 | self.token_reset_switch = True 352 | 353 | token_entry["RequestCount"] += 1 354 | 355 | if token_entry["RequestCount"] > token_entry["MaxRequestCount"]: 356 | self.remove_token_from_model(normalized_model, token_entry["token"]) 357 | next_token_entry = self.token_model_map[normalized_model][0] if self.token_model_map[normalized_model] else None 358 | return next_token_entry["token"] if next_token_entry else None 359 | 360 | sso = token_entry["token"].split("sso=")[1].split(";")[0] 361 | 362 | if sso in self.token_status_map and normalized_model in self.token_status_map[sso]: 363 | if token_entry["RequestCount"] == self.model_config[normalized_model]["RequestFrequency"]: 364 | self.token_status_map[sso][normalized_model]["isValid"] = False 365 | self.token_status_map[sso][normalized_model]["invalidatedTime"] = int(time.time() * 1000) 366 | self.token_status_map[sso][normalized_model]["totalRequestCount"] += 1 367 | 368 | 369 | 370 | self.save_token_status() 371 | 372 | return token_entry["token"] 373 | 374 | return None 375 | 376 | def remove_token_from_model(self, model_id, token): 377 | normalized_model = self.normalize_model_name(model_id) 378 | 379 | if normalized_model not in self.token_model_map: 380 | logger.error(f"模型 {normalized_model} 不存在", "TokenManager") 381 | return False 382 | 383 | model_tokens = self.token_model_map[normalized_model] 384 | token_index = next((i for i, entry in enumerate(model_tokens) if entry["token"] == token), -1) 385 | 386 | if token_index != -1: 387 | removed_token_entry = model_tokens.pop(token_index) 388 | self.expired_tokens.add(( 389 | removed_token_entry["token"], 390 | normalized_model, 391 | int(time.time() * 1000), 392 | removed_token_entry["type"] 393 | )) 394 | 395 | if not self.token_reset_switch: 396 | self.start_token_reset_process() 397 | self.token_reset_switch = True 398 | 399 | logger.info(f"模型{model_id}的令牌已失效,已成功移除令牌: {token}", "TokenManager") 400 | return True 401 | 402 | logger.error(f"在模型 {normalized_model} 中未找到 token: {token}", "TokenManager") 403 | return False 404 | 405 | def get_expired_tokens(self): 406 | return list(self.expired_tokens) 407 | 408 | def normalize_model_name(self, model): 409 | if model.startswith('grok-') and not any(keyword in model for keyword in ['deepsearch','deepersearch','reasoning']): 410 | return '-'.join(model.split('-')[:2]) 411 | return model 412 | 413 | def get_token_count_for_model(self, model_id): 414 | normalized_model = self.normalize_model_name(model_id) 415 | return len(self.token_model_map.get(normalized_model, [])) 416 | 417 | def get_remaining_token_request_capacity(self): 418 | remaining_capacity_map = {} 419 | 420 | for model in self.model_config.keys(): 421 | model_tokens = self.token_model_map.get(model, []) 422 | 423 | model_request_frequency = sum(token_entry.get("MaxRequestCount", 0) for token_entry in model_tokens) 424 | total_used_requests = sum(token_entry.get("RequestCount", 0) for token_entry in model_tokens) 425 | 426 | remaining_capacity = (len(model_tokens) * model_request_frequency) - total_used_requests 427 | remaining_capacity_map[model] = max(0, remaining_capacity) 428 | 429 | return remaining_capacity_map 430 | 431 | def get_token_array_for_model(self, model_id): 432 | normalized_model = self.normalize_model_name(model_id) 433 | return self.token_model_map.get(normalized_model, []) 434 | 435 | def start_token_reset_process(self): 436 | def reset_expired_tokens(): 437 | now = int(time.time() * 1000) 438 | 439 | model_config = self.model_normal_config 440 | tokens_to_remove = set() 441 | for token_info in self.expired_tokens: 442 | token, model, expired_time ,type = token_info 443 | if type == "super": 444 | model_config = self.model_super_config 445 | expiration_time = model_config[model]["ExpirationTime"] 446 | 447 | if now - expired_time >= expiration_time: 448 | if not any(entry["token"] == token for entry in self.token_model_map.get(model, [])): 449 | if model not in self.token_model_map: 450 | self.token_model_map[model] = [] 451 | 452 | self.token_model_map[model].append({ 453 | "token": token, 454 | "MaxRequestCount": model_config[model]["RequestFrequency"], 455 | "RequestCount": 0, 456 | "AddedTime": now, 457 | "StartCallTime": None, 458 | "type": type 459 | }) 460 | 461 | sso = token.split("sso=")[1].split(";")[0] 462 | if sso in self.token_status_map and model in self.token_status_map[sso]: 463 | self.token_status_map[sso][model]["isValid"] = True 464 | self.token_status_map[sso][model]["invalidatedTime"] = None 465 | self.token_status_map[sso][model]["totalRequestCount"] = 0 466 | self.token_status_map[sso][model]["isSuper"] = type == "super" 467 | 468 | tokens_to_remove.add(token_info) 469 | 470 | self.expired_tokens -= tokens_to_remove 471 | 472 | for model in model_config.keys(): 473 | if model not in self.token_model_map: 474 | continue 475 | 476 | for token_entry in self.token_model_map[model]: 477 | if not token_entry.get("StartCallTime"): 478 | continue 479 | 480 | expiration_time = model_config[model]["ExpirationTime"] 481 | if now - token_entry["StartCallTime"] >= expiration_time: 482 | sso = token_entry["token"].split("sso=")[1].split(";")[0] 483 | if sso in self.token_status_map and model in self.token_status_map[sso]: 484 | self.token_status_map[sso][model]["isValid"] = True 485 | self.token_status_map[sso][model]["invalidatedTime"] = None 486 | self.token_status_map[sso][model]["totalRequestCount"] = 0 487 | self.token_status_map[sso][model]["isSuper"] = token_entry["type"] == "super" 488 | 489 | token_entry["RequestCount"] = 0 490 | token_entry["StartCallTime"] = None 491 | 492 | import threading 493 | # 启动一个线程执行定时任务,每小时执行一次 494 | def run_timer(): 495 | while True: 496 | reset_expired_tokens() 497 | time.sleep(3600) 498 | 499 | timer_thread = threading.Thread(target=run_timer) 500 | timer_thread.daemon = True 501 | timer_thread.start() 502 | 503 | def get_all_tokens(self): 504 | all_tokens = set() 505 | for model_tokens in self.token_model_map.values(): 506 | for entry in model_tokens: 507 | all_tokens.add(entry["token"]) 508 | return list(all_tokens) 509 | def get_current_token(self, model_id): 510 | normalized_model = self.normalize_model_name(model_id) 511 | 512 | if normalized_model not in self.token_model_map or not self.token_model_map[normalized_model]: 513 | return None 514 | 515 | token_entry = self.token_model_map[normalized_model][0] 516 | return token_entry["token"] 517 | 518 | def get_token_status_map(self): 519 | return self.token_status_map 520 | 521 | class Utils: 522 | @staticmethod 523 | def organize_search_results(search_results): 524 | if not search_results or 'results' not in search_results: 525 | return '' 526 | 527 | results = search_results['results'] 528 | formatted_results = [] 529 | 530 | for index, result in enumerate(results): 531 | title = result.get('title', '未知标题') 532 | url = result.get('url', '#') 533 | preview = result.get('preview', '无预览内容') 534 | 535 | formatted_result = f"\r\n
资料[{index}]: {title}\r\n{preview}\r\n\n[Link]({url})\r\n
" 536 | formatted_results.append(formatted_result) 537 | 538 | return '\n\n'.join(formatted_results) 539 | 540 | @staticmethod 541 | def create_auth_headers(model, is_return=False): 542 | return token_manager.get_next_token_for_model(model, is_return) 543 | 544 | @staticmethod 545 | def get_proxy_options(): 546 | proxy = CONFIG["API"]["PROXY"] 547 | proxy_options = {} 548 | 549 | if proxy: 550 | logger.info(f"使用代理: {proxy}", "Server") 551 | 552 | if proxy.startswith("socks5://"): 553 | proxy_options["proxy"] = proxy 554 | 555 | if '@' in proxy: 556 | auth_part = proxy.split('@')[0].split('://')[1] 557 | if ':' in auth_part: 558 | username, password = auth_part.split(':') 559 | proxy_options["proxy_auth"] = (username, password) 560 | else: 561 | proxy_options["proxies"] = {"https": proxy, "http": proxy} 562 | return proxy_options 563 | 564 | class GrokApiClient: 565 | def __init__(self, model_id): 566 | if model_id not in CONFIG["MODELS"]: 567 | raise ValueError(f"不支持的模型: {model_id}") 568 | self.model_id = CONFIG["MODELS"][model_id] 569 | 570 | def process_message_content(self, content): 571 | if isinstance(content, str): 572 | return content 573 | return None 574 | 575 | def get_image_type(self, base64_string): 576 | mime_type = 'image/jpeg' 577 | if 'data:image' in base64_string: 578 | import re 579 | matches = re.search(r'data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,', base64_string) 580 | if matches: 581 | mime_type = matches.group(1) 582 | 583 | extension = mime_type.split('/')[1] 584 | file_name = f"image.{extension}" 585 | 586 | return { 587 | "mimeType": mime_type, 588 | "fileName": file_name 589 | } 590 | def upload_base64_file(self, message, model): 591 | try: 592 | message_base64 = base64.b64encode(message.encode('utf-8')).decode('utf-8') 593 | upload_data = { 594 | "fileName": "message.txt", 595 | "fileMimeType": "text/plain", 596 | "content": message_base64 597 | } 598 | 599 | logger.info("发送文字文件请求", "Server") 600 | cookie = f"{Utils.create_auth_headers(model, True)};{CONFIG['SERVER']['CF_CLEARANCE']}" 601 | proxy_options = Utils.get_proxy_options() 602 | response = curl_requests.post( 603 | "https://grok.com/rest/app-chat/upload-file", 604 | headers={ 605 | **DEFAULT_HEADERS, 606 | "Cookie":cookie 607 | }, 608 | json=upload_data, 609 | impersonate="chrome133a", 610 | **proxy_options 611 | ) 612 | 613 | if response.status_code != 200: 614 | logger.error(f"上传文件失败,状态码:{response.status_code}", "Server") 615 | raise Exception(f"上传文件失败,状态码:{response.status_code}") 616 | 617 | result = response.json() 618 | logger.info(f"上传文件成功: {result}", "Server") 619 | return result.get("fileMetadataId", "") 620 | 621 | except Exception as error: 622 | logger.error(str(error), "Server") 623 | raise Exception(f"上传文件失败,状态码:{response.status_code}") 624 | def upload_base64_image(self, base64_data, url): 625 | try: 626 | if 'data:image' in base64_data: 627 | image_buffer = base64_data.split(',')[1] 628 | else: 629 | image_buffer = base64_data 630 | 631 | image_info = self.get_image_type(base64_data) 632 | mime_type = image_info["mimeType"] 633 | file_name = image_info["fileName"] 634 | 635 | upload_data = { 636 | "rpc": "uploadFile", 637 | "req": { 638 | "fileName": file_name, 639 | "fileMimeType": mime_type, 640 | "content": image_buffer 641 | } 642 | } 643 | 644 | logger.info("发送图片请求", "Server") 645 | 646 | proxy_options = Utils.get_proxy_options() 647 | response = curl_requests.post( 648 | url, 649 | headers={ 650 | **DEFAULT_HEADERS, 651 | "Cookie":CONFIG["SERVER"]['COOKIE'] 652 | }, 653 | json=upload_data, 654 | impersonate="chrome133a", 655 | **proxy_options 656 | ) 657 | 658 | if response.status_code != 200: 659 | logger.error(f"上传图片失败,状态码:{response.status_code}", "Server") 660 | return '' 661 | 662 | result = response.json() 663 | logger.info(f"上传图片成功: {result}", "Server") 664 | return result.get("fileMetadataId", "") 665 | 666 | except Exception as error: 667 | logger.error(str(error), "Server") 668 | return '' 669 | # def convert_system_messages(self, messages): 670 | # try: 671 | # system_prompt = [] 672 | # i = 0 673 | # while i < len(messages): 674 | # if messages[i].get('role') != 'system': 675 | # break 676 | 677 | # system_prompt.append(self.process_message_content(messages[i].get('content'))) 678 | # i += 1 679 | 680 | # messages = messages[i:] 681 | # system_prompt = '\n'.join(system_prompt) 682 | 683 | # if not messages: 684 | # raise ValueError("没有找到用户或者AI消息") 685 | # return {"system_prompt":system_prompt,"messages":messages} 686 | # except Exception as error: 687 | # logger.error(str(error), "Server") 688 | # raise ValueError(error) 689 | def prepare_chat_request(self, request): 690 | if ((request["model"] == 'grok-4-imageGen' or request["model"] == 'grok-3-imageGen') and 691 | not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"] and 692 | request.get("stream", False)): 693 | raise ValueError("该模型流式输出需要配置PICGO或者TUMY图床密钥!") 694 | 695 | # system_message, todo_messages = self.convert_system_messages(request["messages"]).values() 696 | todo_messages = request["messages"] 697 | if request["model"] in ['grok-4-imageGen', 'grok-3-imageGen', 'grok-3-deepsearch']: 698 | last_message = todo_messages[-1] 699 | if last_message["role"] != 'user': 700 | raise ValueError('此模型最后一条消息必须是用户消息!') 701 | todo_messages = [last_message] 702 | file_attachments = [] 703 | messages = '' 704 | last_role = None 705 | last_content = '' 706 | message_length = 0 707 | convert_to_file = False 708 | last_message_content = '' 709 | search = request["model"] in ['grok-4-deepsearch', 'grok-3-search'] 710 | deepsearchPreset = '' 711 | if request["model"] == 'grok-3-deepsearch': 712 | deepsearchPreset = 'default' 713 | elif request["model"] == 'grok-3-deepersearch': 714 | deepsearchPreset = 'deeper' 715 | 716 | # 移除标签及其内容和base64图片 717 | def remove_think_tags(text): 718 | import re 719 | text = re.sub(r'[\s\S]*?<\/think>', '', text).strip() 720 | text = re.sub(r'!\[image\]\(data:.*?base64,.*?\)', '[图片]', text) 721 | return text 722 | 723 | def process_content(content): 724 | if isinstance(content, list): 725 | text_content = '' 726 | for item in content: 727 | if item["type"] == 'image_url': 728 | text_content += ("[图片]" if not text_content else '\n[图片]') 729 | elif item["type"] == 'text': 730 | text_content += (remove_think_tags(item["text"]) if not text_content else '\n' + remove_think_tags(item["text"])) 731 | return text_content 732 | elif isinstance(content, dict) and content is not None: 733 | if content["type"] == 'image_url': 734 | return "[图片]" 735 | elif content["type"] == 'text': 736 | return remove_think_tags(content["text"]) 737 | return remove_think_tags(self.process_message_content(content)) 738 | for current in todo_messages: 739 | role = 'assistant' if current["role"] == 'assistant' else 'user' 740 | is_last_message = current == todo_messages[-1] 741 | 742 | if is_last_message and "content" in current: 743 | if isinstance(current["content"], list): 744 | for item in current["content"]: 745 | if item["type"] == 'image_url': 746 | processed_image = self.upload_base64_image( 747 | item["image_url"]["url"], 748 | f"{CONFIG['API']['BASE_URL']}/api/rpc" 749 | ) 750 | if processed_image: 751 | file_attachments.append(processed_image) 752 | elif isinstance(current["content"], dict) and current["content"].get("type") == 'image_url': 753 | processed_image = self.upload_base64_image( 754 | current["content"]["image_url"]["url"], 755 | f"{CONFIG['API']['BASE_URL']}/api/rpc" 756 | ) 757 | if processed_image: 758 | file_attachments.append(processed_image) 759 | 760 | 761 | text_content = process_content(current.get("content", "")) 762 | if is_last_message and convert_to_file: 763 | last_message_content = f"{role.upper()}: {text_content or '[图片]'}\n" 764 | continue 765 | if text_content or (is_last_message and file_attachments): 766 | if role == last_role and text_content: 767 | last_content += '\n' + text_content 768 | messages = messages[:messages.rindex(f"{role.upper()}: ")] + f"{role.upper()}: {last_content}\n" 769 | else: 770 | messages += f"{role.upper()}: {text_content or '[图片]'}\n" 771 | last_content = text_content 772 | last_role = role 773 | message_length += len(messages) 774 | if message_length >= 40000: 775 | convert_to_file = True 776 | 777 | if convert_to_file: 778 | file_id = self.upload_base64_file(messages, request["model"]) 779 | if file_id: 780 | file_attachments.insert(0, file_id) 781 | messages = last_message_content.strip() 782 | if messages.strip() == '': 783 | if convert_to_file: 784 | messages = '基于txt文件内容进行回复:' 785 | else: 786 | raise ValueError('消息内容为空!') 787 | return { 788 | "temporary": CONFIG["API"].get("IS_TEMP_CONVERSATION", False), 789 | "modelName": self.model_id, 790 | "message": messages.strip(), 791 | "fileAttachments": file_attachments[:4], 792 | "imageAttachments": [], 793 | "disableSearch": False, 794 | "enableImageGeneration": True, 795 | "returnImageBytes": False, 796 | "returnRawGrokInXaiRequest": False, 797 | "enableImageStreaming": False, 798 | "imageGenerationCount": 1, 799 | "forceConcise": False, 800 | "toolOverrides": { 801 | "imageGen": request["model"] in ['grok-4-imageGen', 'grok-3-imageGen'], 802 | "webSearch": search, 803 | "xSearch": search, 804 | "xMediaSearch": search, 805 | "trendsSearch": search, 806 | "xPostAnalyze": search 807 | }, 808 | "enableSideBySide": True, 809 | "sendFinalMetadata": True, 810 | "customPersonality": "", 811 | "deepsearchPreset": deepsearchPreset, 812 | "isReasoning": request["model"] == 'grok-3-reasoning', 813 | "disableTextFollowUps": True 814 | } 815 | 816 | class MessageProcessor: 817 | @staticmethod 818 | def create_chat_response(message, model, is_stream=False): 819 | base_response = { 820 | "id": f"chatcmpl-{uuid.uuid4()}", 821 | "created": int(time.time()), 822 | "model": model 823 | } 824 | 825 | if is_stream: 826 | return { 827 | **base_response, 828 | "object": "chat.completion.chunk", 829 | "choices": [{ 830 | "index": 0, 831 | "delta": { 832 | "content": message 833 | } 834 | }] 835 | } 836 | 837 | return { 838 | **base_response, 839 | "object": "chat.completion", 840 | "choices": [{ 841 | "index": 0, 842 | "message": { 843 | "role": "assistant", 844 | "content": message 845 | }, 846 | "finish_reason": "stop" 847 | }], 848 | "usage": None 849 | } 850 | 851 | def process_model_response(response, model): 852 | result = {"token": None, "imageUrl": None} 853 | 854 | if CONFIG["IS_IMG_GEN"]: 855 | if response.get("cachedImageGenerationResponse") and not CONFIG["IS_IMG_GEN2"]: 856 | result["imageUrl"] = response["cachedImageGenerationResponse"]["imageUrl"] 857 | return result 858 | if model == 'grok-3': 859 | result["token"] = response.get("token") 860 | elif model in ['grok-3-search']: 861 | if response.get("webSearchResults") and CONFIG["ISSHOW_SEARCH_RESULTS"]: 862 | result["token"] = f"\r\n{Utils.organize_search_results(response['webSearchResults'])}\r\n" 863 | else: 864 | result["token"] = response.get("token") 865 | elif model in ['grok-3-deepsearch', 'grok-3-deepersearch','grok-4-deepsearch']: 866 | if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]: 867 | return result 868 | if response.get("messageStepId") and not CONFIG["IS_THINKING"]: 869 | result["token"] = "" + response.get("token", "") 870 | CONFIG["IS_THINKING"] = True 871 | elif not response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "final": 872 | result["token"] = "" + response.get("token", "") 873 | CONFIG["IS_THINKING"] = False 874 | elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final": 875 | result["token"] = response.get("token","") 876 | elif (CONFIG["IS_THINKING"] and response.get("token","").get("action","") == "webSearch"): 877 | result["token"] = response.get("token","").get("action_input","").get("query","") 878 | elif (CONFIG["IS_THINKING"] and response.get("webSearchResults")): 879 | result["token"] = Utils.organize_search_results(response['webSearchResults']) 880 | elif model == 'grok-3-reasoning': 881 | if response.get("isThinking") and not CONFIG["SHOW_THINKING"]: 882 | return result 883 | 884 | if response.get("isThinking") and not CONFIG["IS_THINKING"]: 885 | result["token"] = "" + response.get("token", "") 886 | CONFIG["IS_THINKING"] = True 887 | elif not response.get("isThinking") and CONFIG["IS_THINKING"]: 888 | result["token"] = "" + response.get("token", "") 889 | CONFIG["IS_THINKING"] = False 890 | else: 891 | result["token"] = response.get("token") 892 | 893 | elif model == 'grok-4': 894 | if response.get("isThinking"): 895 | return result 896 | result["token"] = response.get("token") 897 | elif model == 'grok-4-reasoning': 898 | if response.get("isThinking") and not CONFIG["SHOW_THINKING"]: 899 | return result 900 | if response.get("isThinking") and not CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant": 901 | result["token"] = "" + response.get("token", "") 902 | CONFIG["IS_THINKING"] = True 903 | elif not response.get("isThinking") and CONFIG["IS_THINKING"] and response.get("messageTag") == "final": 904 | result["token"] = "" + response.get("token", "") 905 | CONFIG["IS_THINKING"] = False 906 | else: 907 | result["token"] = response.get("token") 908 | elif model in ['grok-4-deepsearch']: 909 | if response.get("messageStepId") and not CONFIG["SHOW_THINKING"]: 910 | return result 911 | if response.get("messageStepId") and not CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant": 912 | result["token"] = "" + response.get("token", "") 913 | CONFIG["IS_THINKING"] = True 914 | elif not response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "final": 915 | result["token"] = "" + response.get("token", "") 916 | CONFIG["IS_THINKING"] = False 917 | elif (response.get("messageStepId") and CONFIG["IS_THINKING"] and response.get("messageTag") == "assistant") or response.get("messageTag") == "final": 918 | result["token"] = response.get("token","") 919 | elif (CONFIG["IS_THINKING"] and response.get("token","").get("action","") == "webSearch"): 920 | result["token"] = response.get("token","").get("action_input","").get("query","") 921 | elif (CONFIG["IS_THINKING"] and response.get("webSearchResults")): 922 | result["token"] = Utils.organize_search_results(response['webSearchResults']) 923 | 924 | return result 925 | 926 | def handle_image_response(image_url): 927 | max_retries = 2 928 | retry_count = 0 929 | image_base64_response = None 930 | 931 | while retry_count < max_retries: 932 | try: 933 | proxy_options = Utils.get_proxy_options() 934 | image_base64_response = curl_requests.get( 935 | f"https://assets.grok.com/{image_url}", 936 | headers={ 937 | **DEFAULT_HEADERS, 938 | "Cookie":CONFIG["SERVER"]['COOKIE'] 939 | }, 940 | impersonate="chrome133a", 941 | **proxy_options 942 | ) 943 | 944 | if image_base64_response.status_code == 200: 945 | break 946 | 947 | retry_count += 1 948 | if retry_count == max_retries: 949 | raise Exception(f"上游服务请求失败! status: {image_base64_response.status_code}") 950 | 951 | time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count) 952 | 953 | except Exception as error: 954 | logger.error(str(error), "Server") 955 | retry_count += 1 956 | if retry_count == max_retries: 957 | raise 958 | 959 | time.sleep(CONFIG["API"]["RETRY_TIME"] / 1000 * retry_count) 960 | 961 | image_buffer = image_base64_response.content 962 | 963 | if not CONFIG["API"]["PICGO_KEY"] and not CONFIG["API"]["TUMY_KEY"]: 964 | base64_image = base64.b64encode(image_buffer).decode('utf-8') 965 | image_content_type = image_base64_response.headers.get('content-type', 'image/jpeg') 966 | return f"![image](data:{image_content_type};base64,{base64_image})" 967 | 968 | logger.info("开始上传图床", "Server") 969 | 970 | if CONFIG["API"]["PICGO_KEY"]: 971 | files = {'source': ('image.jpg', image_buffer, 'image/jpeg')} 972 | headers = { 973 | "X-API-Key": CONFIG["API"]["PICGO_KEY"] 974 | } 975 | 976 | response_url = requests.post( 977 | "https://www.picgo.net/api/1/upload", 978 | files=files, 979 | headers=headers 980 | ) 981 | 982 | if response_url.status_code != 200: 983 | return "生图失败,请查看PICGO图床密钥是否设置正确" 984 | else: 985 | logger.info("生图成功", "Server") 986 | result = response_url.json() 987 | return f"![image]({result['image']['url']})" 988 | 989 | 990 | elif CONFIG["API"]["TUMY_KEY"]: 991 | files = {'file': ('image.jpg', image_buffer, 'image/jpeg')} 992 | headers = { 993 | "Accept": "application/json", 994 | 'Authorization': f"Bearer {CONFIG['API']['TUMY_KEY']}" 995 | } 996 | 997 | response_url = requests.post( 998 | "https://tu.my/api/v1/upload", 999 | files=files, 1000 | headers=headers 1001 | ) 1002 | 1003 | if response_url.status_code != 200: 1004 | return "生图失败,请查看TUMY图床密钥是否设置正确" 1005 | else: 1006 | try: 1007 | result = response_url.json() 1008 | logger.info("生图成功", "Server") 1009 | return f"![image]({result['data']['links']['url']})" 1010 | except Exception as error: 1011 | logger.error(str(error), "Server") 1012 | return "生图失败,请查看TUMY图床密钥是否设置正确" 1013 | 1014 | def handle_non_stream_response(response, model): 1015 | try: 1016 | logger.info("开始处理非流式响应", "Server") 1017 | 1018 | stream = response.iter_lines() 1019 | full_response = "" 1020 | 1021 | CONFIG["IS_THINKING"] = False 1022 | CONFIG["IS_IMG_GEN"] = False 1023 | CONFIG["IS_IMG_GEN2"] = False 1024 | 1025 | for chunk in stream: 1026 | if not chunk: 1027 | continue 1028 | try: 1029 | line_json = json.loads(chunk.decode("utf-8").strip()) 1030 | if line_json.get("error"): 1031 | logger.error(json.dumps(line_json, indent=2), "Server") 1032 | return json.dumps({"error": "RateLimitError"}) + "\n\n" 1033 | 1034 | response_data = line_json.get("result", {}).get("response") 1035 | if not response_data: 1036 | continue 1037 | 1038 | if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"): 1039 | CONFIG["IS_IMG_GEN"] = True 1040 | 1041 | result = process_model_response(response_data, model) 1042 | 1043 | if result["token"]: 1044 | full_response += result["token"] 1045 | 1046 | if result["imageUrl"]: 1047 | CONFIG["IS_IMG_GEN2"] = True 1048 | return handle_image_response(result["imageUrl"]) 1049 | 1050 | except json.JSONDecodeError: 1051 | continue 1052 | except Exception as e: 1053 | logger.error(f"处理流式响应行时出错: {str(e)}", "Server") 1054 | continue 1055 | 1056 | return full_response 1057 | except Exception as error: 1058 | logger.error(str(error), "Server") 1059 | raise 1060 | def handle_stream_response(response, model): 1061 | def generate(): 1062 | logger.info("开始处理流式响应", "Server") 1063 | 1064 | stream = response.iter_lines() 1065 | CONFIG["IS_THINKING"] = False 1066 | CONFIG["IS_IMG_GEN"] = False 1067 | CONFIG["IS_IMG_GEN2"] = False 1068 | 1069 | for chunk in stream: 1070 | if not chunk: 1071 | continue 1072 | try: 1073 | line_json = json.loads(chunk.decode("utf-8").strip()) 1074 | print(line_json) 1075 | if line_json.get("error"): 1076 | logger.error(json.dumps(line_json, indent=2), "Server") 1077 | yield json.dumps({"error": "RateLimitError"}) + "\n\n" 1078 | return 1079 | 1080 | response_data = line_json.get("result", {}).get("response") 1081 | if not response_data: 1082 | continue 1083 | 1084 | if response_data.get("doImgGen") or response_data.get("imageAttachmentInfo"): 1085 | CONFIG["IS_IMG_GEN"] = True 1086 | 1087 | result = process_model_response(response_data, model) 1088 | 1089 | if result["token"]: 1090 | yield f"data: {json.dumps(MessageProcessor.create_chat_response(result['token'], model, True))}\n\n" 1091 | 1092 | if result["imageUrl"]: 1093 | CONFIG["IS_IMG_GEN2"] = True 1094 | image_data = handle_image_response(result["imageUrl"]) 1095 | yield f"data: {json.dumps(MessageProcessor.create_chat_response(image_data, model, True))}\n\n" 1096 | 1097 | except json.JSONDecodeError: 1098 | continue 1099 | except Exception as e: 1100 | logger.error(f"处理流式响应行时出错: {str(e)}", "Server") 1101 | continue 1102 | 1103 | yield "data: [DONE]\n\n" 1104 | return generate() 1105 | 1106 | def initialization(): 1107 | sso_array = os.environ.get("SSO", "").split(',') 1108 | sso_array_super = os.environ.get("SSO_SUPER", "").split(',') 1109 | 1110 | combined_dict = [] 1111 | for value in sso_array_super: 1112 | combined_dict.append({ 1113 | "token": f"sso-rw={value};sso={value}", 1114 | "type": "super" 1115 | }) 1116 | for value in sso_array: 1117 | combined_dict.append({ 1118 | "token": f"sso-rw={value};sso={value}", 1119 | "type": "normal" 1120 | }) 1121 | 1122 | 1123 | logger.info("开始加载令牌", "Server") 1124 | token_manager.load_token_status() 1125 | for tokens in combined_dict: 1126 | if tokens: 1127 | token_manager.add_token(tokens,True) 1128 | token_manager.save_token_status() 1129 | 1130 | logger.info(f"成功加载令牌: {json.dumps(token_manager.get_all_tokens(), indent=2)}", "Server") 1131 | logger.info(f"令牌加载完成,共加载: {len(sso_array)+len(sso_array_super)}个令牌", "Server") 1132 | logger.info(f"其中共加载: {len(sso_array_super)}个super会员令牌", "Server") 1133 | 1134 | if CONFIG["API"]["PROXY"]: 1135 | logger.info(f"代理已设置: {CONFIG['API']['PROXY']}", "Server") 1136 | 1137 | logger.info("初始化完成", "Server") 1138 | 1139 | 1140 | app = Flask(__name__) 1141 | app.wsgi_app = ProxyFix(app.wsgi_app) 1142 | app.secret_key = os.environ.get('FLASK_SECRET_KEY') or secrets.token_hex(16) 1143 | app.json.sort_keys = False 1144 | 1145 | @app.route('/manager/login', methods=['GET', 'POST']) 1146 | def manager_login(): 1147 | if CONFIG["ADMIN"]["MANAGER_SWITCH"]: 1148 | if request.method == 'POST': 1149 | password = request.form.get('password') 1150 | if password == CONFIG["ADMIN"]["PASSWORD"]: 1151 | session['is_logged_in'] = True 1152 | return redirect('/manager') 1153 | return render_template('login.html', error=True) 1154 | return render_template('login.html', error=False) 1155 | else: 1156 | return redirect('/') 1157 | 1158 | def check_auth(): 1159 | return session.get('is_logged_in', False) 1160 | 1161 | @app.route('/manager') 1162 | def manager(): 1163 | if not check_auth(): 1164 | return redirect('/manager/login') 1165 | return render_template('manager.html') 1166 | 1167 | @app.route('/manager/api/get') 1168 | def get_manager_tokens(): 1169 | if not check_auth(): 1170 | return jsonify({"error": "Unauthorized"}), 401 1171 | return jsonify(token_manager.get_token_status_map()) 1172 | 1173 | @app.route('/manager/api/add', methods=['POST']) 1174 | def add_manager_token(): 1175 | if not check_auth(): 1176 | return jsonify({"error": "Unauthorized"}), 401 1177 | try: 1178 | sso = request.json.get('sso') 1179 | if not sso: 1180 | return jsonify({"error": "SSO token is required"}), 400 1181 | token_manager.add_token({"token":f"sso-rw={sso};sso={sso}","type":"normal"}) 1182 | return jsonify({"success": True}) 1183 | except Exception as e: 1184 | return jsonify({"error": str(e)}), 500 1185 | 1186 | @app.route('/manager/api/delete', methods=['POST']) 1187 | def delete_manager_token(): 1188 | if not check_auth(): 1189 | return jsonify({"error": "Unauthorized"}), 401 1190 | try: 1191 | sso = request.json.get('sso') 1192 | if not sso: 1193 | return jsonify({"error": "SSO token is required"}), 400 1194 | token_manager.delete_token(f"sso-rw={sso};sso={sso}") 1195 | return jsonify({"success": True}) 1196 | except Exception as e: 1197 | return jsonify({"error": str(e)}), 500 1198 | 1199 | @app.route('/manager/api/cf_clearance', methods=['POST']) 1200 | def setCf_Manager_clearance(): 1201 | if not check_auth(): 1202 | return jsonify({"error": "Unauthorized"}), 401 1203 | try: 1204 | cf_clearance = request.json.get('cf_clearance') 1205 | if not cf_clearance: 1206 | return jsonify({"error": "cf_clearance is required"}), 400 1207 | CONFIG["SERVER"]['CF_CLEARANCE'] = cf_clearance 1208 | return jsonify({"success": True}) 1209 | except Exception as e: 1210 | return jsonify({"error": str(e)}), 500 1211 | 1212 | 1213 | @app.route('/get/tokens', methods=['GET']) 1214 | def get_tokens(): 1215 | auth_token = request.headers.get('Authorization', '').replace('Bearer ', '') 1216 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1217 | return jsonify({"error": '自定义的SSO令牌模式无法获取轮询sso令牌状态'}), 403 1218 | elif auth_token != CONFIG["API"]["API_KEY"]: 1219 | return jsonify({"error": 'Unauthorized'}), 401 1220 | return jsonify(token_manager.get_token_status_map()) 1221 | 1222 | @app.route('/add/token', methods=['POST']) 1223 | def add_token(): 1224 | auth_token = request.headers.get('Authorization', '').replace('Bearer ', '') 1225 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1226 | return jsonify({"error": '自定义的SSO令牌模式无法添加sso令牌'}), 403 1227 | elif auth_token != CONFIG["API"]["API_KEY"]: 1228 | return jsonify({"error": 'Unauthorized'}), 401 1229 | 1230 | try: 1231 | sso = request.json.get('sso') 1232 | token_manager.add_token({"token":f"sso-rw={sso};sso={sso}","type":"normal"}) 1233 | return jsonify(token_manager.get_token_status_map().get(sso, {})), 200 1234 | except Exception as error: 1235 | logger.error(str(error), "Server") 1236 | return jsonify({"error": '添加sso令牌失败'}), 500 1237 | 1238 | @app.route('/set/cf_clearance', methods=['POST']) 1239 | def setCf_clearance(): 1240 | auth_token = request.headers.get('Authorization', '').replace('Bearer ', '') 1241 | if auth_token != CONFIG["API"]["API_KEY"]: 1242 | return jsonify({"error": 'Unauthorized'}), 401 1243 | try: 1244 | cf_clearance = request.json.get('cf_clearance') 1245 | CONFIG["SERVER"]['CF_CLEARANCE'] = cf_clearance 1246 | return jsonify({"message": '设置cf_clearance成功'}), 200 1247 | except Exception as error: 1248 | logger.error(str(error), "Server") 1249 | return jsonify({"error": '设置cf_clearance失败'}), 500 1250 | 1251 | @app.route('/delete/token', methods=['POST']) 1252 | def delete_token(): 1253 | auth_token = request.headers.get('Authorization', '').replace('Bearer ', '') 1254 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1255 | return jsonify({"error": '自定义的SSO令牌模式无法删除sso令牌'}), 403 1256 | elif auth_token != CONFIG["API"]["API_KEY"]: 1257 | return jsonify({"error": 'Unauthorized'}), 401 1258 | 1259 | try: 1260 | sso = request.json.get('sso') 1261 | token_manager.delete_token(f"sso-rw={sso};sso={sso}") 1262 | return jsonify({"message": '删除sso令牌成功'}), 200 1263 | except Exception as error: 1264 | logger.error(str(error), "Server") 1265 | return jsonify({"error": '删除sso令牌失败'}), 500 1266 | 1267 | @app.route('/v1/models', methods=['GET']) 1268 | def get_models(): 1269 | return jsonify({ 1270 | "object": "list", 1271 | "data": [ 1272 | { 1273 | "id": model, 1274 | "object": "model", 1275 | "created": int(time.time()), 1276 | "owned_by": "grok" 1277 | } 1278 | for model in CONFIG["MODELS"].keys() 1279 | ] 1280 | }) 1281 | 1282 | @app.route('/v1/chat/completions', methods=['POST']) 1283 | def chat_completions(): 1284 | response_status_code = 500 1285 | try: 1286 | auth_token = request.headers.get('Authorization', 1287 | '').replace('Bearer ', '') 1288 | if auth_token: 1289 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1290 | result = f"sso={auth_token};sso-rw={auth_token}" 1291 | token_manager.set_token(result) 1292 | elif auth_token != CONFIG["API"]["API_KEY"]: 1293 | return jsonify({"error": 'Unauthorized'}), 401 1294 | else: 1295 | return jsonify({"error": 'API_KEY缺失'}), 401 1296 | 1297 | data = request.json 1298 | model = data.get("model") 1299 | stream = data.get("stream", False) 1300 | 1301 | retry_count = 0 1302 | grok_client = GrokApiClient(model) 1303 | request_payload = grok_client.prepare_chat_request(data) 1304 | 1305 | logger.info(json.dumps(request_payload,indent=2)) 1306 | 1307 | while retry_count < CONFIG["RETRY"]["MAX_ATTEMPTS"]: 1308 | retry_count += 1 1309 | CONFIG["API"]["SIGNATURE_COOKIE"] = Utils.create_auth_headers(model) 1310 | 1311 | if not CONFIG["API"]["SIGNATURE_COOKIE"]: 1312 | raise ValueError('该模型无可用令牌') 1313 | 1314 | logger.info( 1315 | f"当前令牌: {json.dumps(CONFIG['API']['SIGNATURE_COOKIE'], indent=2)}","Server") 1316 | logger.info( 1317 | f"当前可用模型的全部可用数量: {json.dumps(token_manager.get_remaining_token_request_capacity(), indent=2)}","Server") 1318 | 1319 | if CONFIG['SERVER']['CF_CLEARANCE']: 1320 | CONFIG["SERVER"]['COOKIE'] = f"{CONFIG['API']['SIGNATURE_COOKIE']};{CONFIG['SERVER']['CF_CLEARANCE']}" 1321 | else: 1322 | CONFIG["SERVER"]['COOKIE'] = CONFIG['API']['SIGNATURE_COOKIE'] 1323 | logger.info(json.dumps(request_payload,indent=2),"Server") 1324 | try: 1325 | proxy_options = Utils.get_proxy_options() 1326 | response = curl_requests.post( 1327 | f"{CONFIG['API']['BASE_URL']}/rest/app-chat/conversations/new", 1328 | headers={ 1329 | **DEFAULT_HEADERS, 1330 | "Cookie":CONFIG["SERVER"]['COOKIE'] 1331 | }, 1332 | data=json.dumps(request_payload), 1333 | impersonate="chrome133a", 1334 | stream=True, 1335 | **proxy_options) 1336 | logger.info(CONFIG["SERVER"]['COOKIE'],"Server") 1337 | if response.status_code == 200: 1338 | response_status_code = 200 1339 | logger.info("请求成功", "Server") 1340 | logger.info(f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}","Server") 1341 | 1342 | try: 1343 | if stream: 1344 | return Response(stream_with_context( 1345 | handle_stream_response(response, model)),content_type='text/event-stream') 1346 | else: 1347 | content = handle_non_stream_response(response, model) 1348 | return jsonify( 1349 | MessageProcessor.create_chat_response(content, model)) 1350 | 1351 | except Exception as error: 1352 | logger.error(str(error), "Server") 1353 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1354 | raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效") 1355 | token_manager.remove_token_from_model(model, CONFIG["API"]["SIGNATURE_COOKIE"]) 1356 | if token_manager.get_token_count_for_model(model) == 0: 1357 | raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话") 1358 | elif response.status_code == 403: 1359 | response_status_code = 403 1360 | token_manager.reduce_token_request_count(model,1)#重置去除当前因为错误未成功请求的次数,确保不会因为错误未成功请求的次数导致次数上限 1361 | if token_manager.get_token_count_for_model(model) == 0: 1362 | raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话") 1363 | print("状态码:", response.status_code) 1364 | print("响应头:", response.headers) 1365 | print("响应内容:", response.text) 1366 | raise ValueError(f"IP暂时被封无法破盾,请稍后重试或者更换ip") 1367 | elif response.status_code == 429: 1368 | response_status_code = 429 1369 | token_manager.reduce_token_request_count(model,1) 1370 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1371 | raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效") 1372 | 1373 | token_manager.remove_token_from_model( 1374 | model, CONFIG["API"]["SIGNATURE_COOKIE"]) 1375 | if token_manager.get_token_count_for_model(model) == 0: 1376 | raise ValueError(f"{model} 次数已达上限,请切换其他模型或者重新对话") 1377 | 1378 | else: 1379 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1380 | raise ValueError(f"自定义SSO令牌当前模型{model}的请求次数已失效") 1381 | 1382 | logger.error(f"令牌异常错误状态!status: {response.status_code}","Server") 1383 | token_manager.remove_token_from_model(model, CONFIG["API"]["SIGNATURE_COOKIE"]) 1384 | logger.info( 1385 | f"当前{model}剩余可用令牌数: {token_manager.get_token_count_for_model(model)}", 1386 | "Server") 1387 | 1388 | except Exception as e: 1389 | logger.error(f"请求处理异常: {str(e)}", "Server") 1390 | if CONFIG["API"]["IS_CUSTOM_SSO"]: 1391 | raise 1392 | continue 1393 | if response_status_code == 403: 1394 | raise ValueError('IP暂时被封无法破盾,请稍后重试或者更换ip') 1395 | elif response_status_code == 500: 1396 | raise ValueError('当前模型所有令牌暂无可用,请稍后重试') 1397 | 1398 | except Exception as error: 1399 | logger.error(str(error), "ChatAPI") 1400 | return jsonify( 1401 | {"error": { 1402 | "message": str(error), 1403 | "type": "server_error" 1404 | }}), response_status_code 1405 | 1406 | @app.route('/', defaults={'path': ''}) 1407 | @app.route('/') 1408 | def catch_all(path): 1409 | return 'api运行正常', 200 1410 | 1411 | if __name__ == '__main__': 1412 | token_manager = AuthTokenManager() 1413 | initialization() 1414 | 1415 | app.run( 1416 | host='0.0.0.0', 1417 | port=CONFIG["SERVER"]["PORT"], 1418 | debug=False 1419 | ) 1420 | -------------------------------------------------------------------------------- /templates/manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GrokAPI管理面板 7 | 8 | 156 | 157 | 158 |
159 |
160 |
161 | 162 | 167 |
168 |
169 |
170 | 171 |
172 |
173 |
174 |

概览

175 |
176 | 182 | 191 |
192 |
193 |
194 |
195 |
196 | 197 | 198 | 199 | 200 |
201 |
202 | Token 总数 203 | 0 204 |
205 |
206 |
207 |
208 | 209 | 210 | 211 |
212 |
213 | grok-2 214 | 0 215 |
216 |
217 |
218 |
219 | 220 | 221 | 222 | 223 |
224 |
225 | grok-3 226 | 0 227 |
228 |
229 |
230 |
231 | 232 | 233 | 234 | 235 |
236 |
237 | grok-3-deepsearch 238 | 0 239 |
240 |
241 |
242 |
243 | 244 | 245 | 246 | 247 |
248 |
249 | grok-3-deepersearch 250 | 0 251 |
252 |
253 |
254 |
255 | 256 | 257 | 258 |
259 |
260 | grok-3-reasoning 261 | 0 262 |
263 |
264 |
265 |
266 | 267 |
268 |

Token 管理

269 |
270 |
271 |

添加单个 SSO Token

272 |
273 | 274 | 275 |
276 |
277 |
278 |

设置 CF Clearance

279 |
280 | 281 | 282 |
283 |
284 |
285 |

批量添加 SSO Token

286 |
287 | 288 | 289 |
290 |
291 |
292 |

检测模型可用性

293 |
294 | 300 | 301 |
302 |
303 |
304 |

Base URL

305 |
306 | 307 | 314 |
315 |
316 |
317 |

API Key

318 |
319 | 320 | 327 |
328 |
329 |
330 |
331 | 332 |
333 | 338 |
339 | 340 |
341 | 342 | 1080 | 1081 | 1082 | --------------------------------------------------------------------------------