pan0022 commited on
Commit
1e6f5d0
·
verified ·
1 Parent(s): 3c546d7

Upload 12 files

Browse files
Files changed (12) hide show
  1. .dockerignore +12 -0
  2. Dockerfile +14 -0
  3. README.md +189 -10
  4. app.py +1419 -0
  5. docker-compose.yml +18 -0
  6. index.js +1319 -0
  7. logger.js +66 -0
  8. package.json +23 -0
  9. start.bat +73 -0
  10. start.sh +83 -0
  11. templates/login.html +77 -0
  12. templates/manager.html +1081 -0
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"]
README.md CHANGED
@@ -1,10 +1,189 @@
1
- ---
2
- title: Grok2api
3
- emoji: ⚡
4
- colorFrom: red
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
app.py ADDED
@@ -0,0 +1,1419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
24
+ "<level>{level: <8}</level> | "
25
+ "<cyan>{extra[filename]}</cyan>:<cyan>{extra[function]}</cyan>:<cyan>{extra[lineno]}</cyan> | "
26
+ "<level>{message}</level>"
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<details><summary>资料[{index}]: {title}</summary>\r\n{preview}\r\n\n[Link]({url})\r\n</details>"
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
+ # 移除<think>标签及其内容和base64图片
717
+ def remove_think_tags(text):
718
+ import re
719
+ text = re.sub(r'<think>[\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<think>{Utils.organize_search_results(response['webSearchResults'])}</think>\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"] = "<think>" + 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"] = "</think>" + 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"] = "<think>" + response.get("token", "")
886
+ CONFIG["IS_THINKING"] = True
887
+ elif not response.get("isThinking") and CONFIG["IS_THINKING"]:
888
+ result["token"] = "</think>" + 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"] = "<think>" + 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"] = "</think>" + 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"] = "<think>" + 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"] = "</think>" + 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('/<path:path>')
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
+ )
docker-compose.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+ services:
3
+ grok2api_python:
4
+ image: yxmiler/grok2api_python:latest
5
+ container_name: grok2api_python
6
+ ports:
7
+ - "3000:3000"
8
+ environment:
9
+ - API_KEY=your_api_key
10
+ - TUMY_KEY=你的图床key,和PICGO_KEY 二选一
11
+ - PICGO_KEY=你的图床key,和TUMY_KEY二选一
12
+ - IS_TEMP_CONVERSATION=true
13
+ - IS_CUSTOM_SSO=false
14
+ - ISSHOW_SEARCH_RESULTS=false
15
+ - PORT=3000
16
+ - SHOW_THINKING=true
17
+ - SSO=your_sso
18
+ restart: unless-stopped
index.js ADDED
@@ -0,0 +1,1319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import fetch from 'node-fetch';
3
+ import FormData from 'form-data';
4
+ import dotenv from 'dotenv';
5
+ import cors from 'cors';
6
+ import puppeteer from 'puppeteer-extra'
7
+ import StealthPlugin from 'puppeteer-extra-plugin-stealth'
8
+ import { v4 as uuidv4 } from 'uuid';
9
+ import Logger from './logger.js';
10
+
11
+ dotenv.config();
12
+
13
+ // 配置常量
14
+ const CONFIG = {
15
+ MODELS: {
16
+ 'grok-2': 'grok-latest',
17
+ 'grok-2-imageGen': 'grok-latest',
18
+ 'grok-2-search': 'grok-latest',
19
+ "grok-3": "grok-3",
20
+ "grok-3-search": "grok-3",
21
+ "grok-3-imageGen": "grok-3",
22
+ "grok-3-deepsearch": "grok-3",
23
+ "grok-3-reasoning": "grok-3"
24
+ },
25
+ API: {
26
+ IS_TEMP_CONVERSATION: process.env.IS_TEMP_CONVERSATION == undefined ? false : process.env.IS_TEMP_CONVERSATION == 'true',
27
+ IS_TEMP_GROK2: process.env.IS_TEMP_GROK2 == undefined ? true : process.env.IS_TEMP_GROK2 == 'true',
28
+ GROK2_CONCURRENCY_LEVEL: process.env.GROK2_CONCURRENCY_LEVEL || 4,
29
+ IS_CUSTOM_SSO: process.env.IS_CUSTOM_SSO == undefined ? false : process.env.IS_CUSTOM_SSO == 'true',
30
+ BASE_URL: "https://grok.com",
31
+ API_KEY: process.env.API_KEY || "sk-123456",
32
+ SIGNATURE_COOKIE: null,
33
+ TEMP_COOKIE: null,
34
+ PICGO_KEY: process.env.PICGO_KEY || null, //想要流式生图的话需要填入这个PICGO图床的key
35
+ TUMY_KEY: process.env.TUMY_KEY || null //想要流式生图的话需要填入这个TUMY图床的key 两个图床二选一,默认使用PICGO
36
+ },
37
+ SERVER: {
38
+ PORT: process.env.PORT || 3000,
39
+ BODY_LIMIT: '5mb'
40
+ },
41
+ RETRY: {
42
+ MAX_ATTEMPTS: 2//重试次数
43
+ },
44
+ SHOW_THINKING: process.env.SHOW_THINKING == undefined ? true : process.env.SHOW_THINKING == 'true',
45
+ IS_THINKING: false,
46
+ IS_IMG_GEN: false,
47
+ IS_IMG_GEN2: false,
48
+ TEMP_COOKIE_INDEX: 0,//临时cookie的下标
49
+ ISSHOW_SEARCH_RESULTS: process.env.ISSHOW_SEARCH_RESULTS == undefined ? true : process.env.ISSHOW_SEARCH_RESULTS == 'true',//是否显示搜索结果
50
+ CHROME_PATH: process.env.CHROME_PATH || null
51
+ };
52
+ puppeteer.use(StealthPlugin())
53
+
54
+ // 请求头配置
55
+ const DEFAULT_HEADERS = {
56
+ 'accept': '*/*',
57
+ 'accept-language': 'zh-CN,zh;q=0.9',
58
+ 'accept-encoding': 'gzip, deflate, br, zstd',
59
+ 'content-type': 'text/plain;charset=UTF-8',
60
+ 'Connection': 'keep-alive',
61
+ 'origin': 'https://grok.com',
62
+ 'priority': 'u=1, i',
63
+ 'sec-ch-ua': '"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"',
64
+ 'sec-ch-ua-mobile': '?0',
65
+ 'sec-ch-ua-platform': '"Windows"',
66
+ 'sec-fetch-dest': 'empty',
67
+ 'sec-fetch-mode': 'cors',
68
+ 'sec-fetch-site': 'same-origin',
69
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
70
+ 'baggage': 'sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c'
71
+ };
72
+
73
+
74
+ async function initialization() {
75
+ if (CONFIG.CHROME_PATH == null) {
76
+ try {
77
+ CONFIG.CHROME_PATH = puppeteer.executablePath();
78
+ } catch (error) {
79
+ CONFIG.CHROME_PATH = "/usr/bin/chromium";
80
+ }
81
+ }
82
+ Logger.info(`CHROME_PATH: ${CONFIG.CHROME_PATH}`, 'Server');
83
+ if (CONFIG.API.IS_CUSTOM_SSO) {
84
+ if (CONFIG.API.IS_TEMP_GROK2) {
85
+ await tempCookieManager.ensureCookies();
86
+ }
87
+ return;
88
+ }
89
+ const ssoArray = process.env.SSO.split(',');
90
+ const concurrencyLimit = 3;
91
+ for (let i = 0; i < ssoArray.length; i += concurrencyLimit) {
92
+ const batch = ssoArray.slice(i, i + concurrencyLimit);
93
+ const batchPromises = batch.map(sso =>
94
+ tokenManager.addToken(`sso-rw=${sso};sso=${sso}`)
95
+ );
96
+
97
+ await Promise.all(batchPromises);
98
+ Logger.info(`已加载令牌: ${i} 个`, 'Server');
99
+ await new Promise(resolve => setTimeout(resolve, 1000));
100
+ }
101
+ Logger.info(`令牌加载完成: ${JSON.stringify(tokenManager.getAllTokens(), null, 2)}`, 'Server');
102
+ Logger.info(`共加载: ${tokenManager.getAllTokens().length}个令牌`, 'Server');
103
+ if (CONFIG.API.IS_TEMP_GROK2) {
104
+ await tempCookieManager.ensureCookies();
105
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
106
+ }
107
+ Logger.info("初始化完成", 'Server');
108
+ }
109
+
110
+ class AuthTokenManager {
111
+ constructor() {
112
+ this.tokenModelMap = {};
113
+ this.expiredTokens = new Set();
114
+ this.tokenStatusMap = {};
115
+
116
+ // 定义模型请求频率限制和过期时间
117
+ this.modelConfig = {
118
+ "grok-2": {
119
+ RequestFrequency: 30,
120
+ ExpirationTime: 1 * 60 * 60 * 1000 // 1小时
121
+ },
122
+ "grok-3": {
123
+ RequestFrequency: 20,
124
+ ExpirationTime: 2 * 60 * 60 * 1000 // 2小时
125
+ },
126
+ "grok-3-deepsearch": {
127
+ RequestFrequency: 10,
128
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小时
129
+ },
130
+ "grok-3-reasoning": {
131
+ RequestFrequency: 10,
132
+ ExpirationTime: 24 * 60 * 60 * 1000 // 24小时
133
+ }
134
+ };
135
+ this.tokenResetSwitch = false;
136
+ this.tokenResetTimer = null;
137
+ }
138
+ async fetchGrokStats(token, modelName) {
139
+ let requestKind = 'DEFAULT';
140
+ if (modelName == 'grok-2' || modelName == 'grok-3') {
141
+ requestKind = 'DEFAULT';
142
+ } else if (modelName == 'grok-3-deepsearch') {
143
+ requestKind = 'DEEPSEARCH';
144
+ } else if (modelName == 'grok-3-reasoning') {
145
+ requestKind = 'REASONING';
146
+ }
147
+ const response = await fetch('https://grok.com/rest/rate-limits', {
148
+ method: 'POST',
149
+ headers: {
150
+ 'content-type': 'application/json',
151
+ 'Cookie': token,
152
+ },
153
+ body: JSON.stringify({
154
+ "requestKind": requestKind,
155
+ "modelName": modelName == 'grok-2' ? 'grok-latest' : "grok-3"
156
+ })
157
+ });
158
+
159
+ if (response.status != 200) {
160
+ return 0;
161
+ }
162
+ const data = await response.json();
163
+ return data.remainingQueries;
164
+ }
165
+ async addToken(token) {
166
+ const sso = token.split("sso=")[1].split(";")[0];
167
+
168
+ for (const model of Object.keys(this.modelConfig)) {
169
+ if (!this.tokenModelMap[model]) {
170
+ this.tokenModelMap[model] = [];
171
+ }
172
+ if (!this.tokenStatusMap[sso]) {
173
+ this.tokenStatusMap[sso] = {};
174
+ }
175
+ const existingTokenEntry = this.tokenModelMap[model].find(entry => entry.token === token);
176
+
177
+ if (!existingTokenEntry) {
178
+ try {
179
+ const remainingQueries = await this.fetchGrokStats(token, model);
180
+
181
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
182
+ const usedRequestCount = modelRequestFrequency - remainingQueries;
183
+
184
+ if (usedRequestCount === modelRequestFrequency) {
185
+ this.expiredTokens.add({
186
+ token: token,
187
+ model: model,
188
+ expiredTime: Date.now()
189
+ });
190
+
191
+ if (!this.tokenStatusMap[sso][model]) {
192
+ this.tokenStatusMap[sso][model] = {
193
+ isValid: false,
194
+ invalidatedTime: Date.now(),
195
+ totalRequestCount: Math.max(0, usedRequestCount)
196
+ };
197
+ }
198
+
199
+ if (!this.tokenResetSwitch) {
200
+ this.startTokenResetProcess();
201
+ this.tokenResetSwitch = true;
202
+ }
203
+ } else {
204
+ this.tokenModelMap[model].push({
205
+ token: token,
206
+ RequestCount: Math.max(0, usedRequestCount),
207
+ AddedTime: Date.now(),
208
+ StartCallTime: null
209
+ });
210
+
211
+ if (!this.tokenStatusMap[sso][model]) {
212
+ this.tokenStatusMap[sso][model] = {
213
+ isValid: true,
214
+ invalidatedTime: null,
215
+ totalRequestCount: Math.max(0, usedRequestCount)
216
+ };
217
+ }
218
+ }
219
+ } catch (error) {
220
+ this.tokenModelMap[model].push({
221
+ token: token,
222
+ RequestCount: 0,
223
+ AddedTime: Date.now(),
224
+ StartCallTime: null
225
+ });
226
+
227
+ if (!this.tokenStatusMap[sso][model]) {
228
+ this.tokenStatusMap[sso][model] = {
229
+ isValid: true,
230
+ invalidatedTime: null,
231
+ totalRequestCount: 0
232
+ };
233
+ }
234
+
235
+ Logger.error(`获取模型 ${model} 的统计信息失败: ${error}`, 'TokenManager');
236
+ }
237
+ await Utils.delay(200);
238
+ }
239
+ }
240
+ }
241
+
242
+ setToken(token) {
243
+ const models = Object.keys(this.modelConfig);
244
+ this.tokenModelMap = models.reduce((map, model) => {
245
+ map[model] = [{
246
+ token,
247
+ RequestCount: 0,
248
+ AddedTime: Date.now(),
249
+ StartCallTime: null
250
+ }];
251
+ return map;
252
+ }, {});
253
+ const sso = token.split("sso=")[1].split(";")[0];
254
+ this.tokenStatusMap[sso] = models.reduce((statusMap, model) => {
255
+ statusMap[model] = {
256
+ isValid: true,
257
+ invalidatedTime: null,
258
+ totalRequestCount: 0
259
+ };
260
+ return statusMap;
261
+ }, {});
262
+ }
263
+
264
+ async deleteToken(token) {
265
+ try {
266
+ const sso = token.split("sso=")[1].split(";")[0];
267
+ await Promise.all([
268
+ new Promise((resolve) => {
269
+ this.tokenModelMap = Object.fromEntries(
270
+ Object.entries(this.tokenModelMap).map(([model, entries]) => [
271
+ model,
272
+ entries.filter(entry => entry.token !== token)
273
+ ])
274
+ );
275
+ resolve();
276
+ }),
277
+
278
+ new Promise((resolve) => {
279
+ delete this.tokenStatusMap[sso];
280
+ resolve();
281
+ }),
282
+ ]);
283
+ Logger.info(`令牌已成功移除: ${token}`, 'TokenManager');
284
+ return true;
285
+ } catch (error) {
286
+ Logger.error('令牌删除失败:', error);
287
+ return false;
288
+ }
289
+ }
290
+ getNextTokenForModel(modelId) {
291
+ const normalizedModel = this.normalizeModelName(modelId);
292
+
293
+ if (!this.tokenModelMap[normalizedModel] || this.tokenModelMap[normalizedModel].length === 0) {
294
+ return null;
295
+ }
296
+ const tokenEntry = this.tokenModelMap[normalizedModel][0];
297
+
298
+ if (tokenEntry) {
299
+ if (tokenEntry.StartCallTime === null || tokenEntry.StartCallTime === undefined) {
300
+ tokenEntry.StartCallTime = Date.now();
301
+ }
302
+ if (!this.tokenResetSwitch) {
303
+ this.startTokenResetProcess();
304
+ this.tokenResetSwitch = true;
305
+ }
306
+ tokenEntry.RequestCount++;
307
+
308
+ if (tokenEntry.RequestCount > this.modelConfig[normalizedModel].RequestFrequency) {
309
+ this.removeTokenFromModel(normalizedModel, tokenEntry.token);
310
+ const nextTokenEntry = this.tokenModelMap[normalizedModel][0];
311
+ return nextTokenEntry ? nextTokenEntry.token : null;
312
+ }
313
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
314
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][normalizedModel]) {
315
+ if (tokenEntry.RequestCount === this.modelConfig[normalizedModel].RequestFrequency) {
316
+ this.tokenStatusMap[sso][normalizedModel].isValid = false;
317
+ this.tokenStatusMap[sso][normalizedModel].invalidatedTime = Date.now();
318
+ }
319
+ this.tokenStatusMap[sso][normalizedModel].totalRequestCount++;
320
+ }
321
+ return tokenEntry.token;
322
+ }
323
+
324
+ return null;
325
+ }
326
+
327
+ removeTokenFromModel(modelId, token) {
328
+ const normalizedModel = this.normalizeModelName(modelId);
329
+
330
+ if (!this.tokenModelMap[normalizedModel]) {
331
+ Logger.error(`模型 ${normalizedModel} 不存在`, 'TokenManager');
332
+ return false;
333
+ }
334
+
335
+ const modelTokens = this.tokenModelMap[normalizedModel];
336
+ const tokenIndex = modelTokens.findIndex(entry => entry.token === token);
337
+
338
+ if (tokenIndex !== -1) {
339
+ const removedTokenEntry = modelTokens.splice(tokenIndex, 1)[0];
340
+ this.expiredTokens.add({
341
+ token: removedTokenEntry.token,
342
+ model: normalizedModel,
343
+ expiredTime: Date.now()
344
+ });
345
+
346
+ if (!this.tokenResetSwitch) {
347
+ this.startTokenResetProcess();
348
+ this.tokenResetSwitch = true;
349
+ }
350
+ Logger.info(`模型${modelId}的令牌已失效,已成功移除令牌: ${token}`, 'TokenManager');
351
+ return true;
352
+ }
353
+
354
+ Logger.error(`在模型 ${normalizedModel} 中未找到 token: ${token}`, 'TokenManager');
355
+ return false;
356
+ }
357
+
358
+ getExpiredTokens() {
359
+ return Array.from(this.expiredTokens);
360
+ }
361
+
362
+ normalizeModelName(model) {
363
+ if (model.startsWith('grok-') && !model.includes('deepsearch') && !model.includes('reasoning')) {
364
+ return model.split('-').slice(0, 2).join('-');
365
+ }
366
+ return model;
367
+ }
368
+
369
+ getTokenCountForModel(modelId) {
370
+ const normalizedModel = this.normalizeModelName(modelId);
371
+ return this.tokenModelMap[normalizedModel]?.length || 0;
372
+ }
373
+
374
+ getRemainingTokenRequestCapacity() {
375
+ const remainingCapacityMap = {};
376
+
377
+ Object.keys(this.modelConfig).forEach(model => {
378
+ const modelTokens = this.tokenModelMap[model] || [];
379
+
380
+ const modelRequestFrequency = this.modelConfig[model].RequestFrequency;
381
+
382
+ const totalUsedRequests = modelTokens.reduce((sum, tokenEntry) => {
383
+ return sum + (tokenEntry.RequestCount || 0);
384
+ }, 0);
385
+
386
+ // 计算剩余可用请求数量
387
+ const remainingCapacity = (modelTokens.length * modelRequestFrequency) - totalUsedRequests;
388
+ remainingCapacityMap[model] = Math.max(0, remainingCapacity);
389
+ });
390
+
391
+ return remainingCapacityMap;
392
+ }
393
+
394
+ getTokenArrayForModel(modelId) {
395
+ const normalizedModel = this.normalizeModelName(modelId);
396
+ return this.tokenModelMap[normalizedModel] || [];
397
+ }
398
+
399
+ startTokenResetProcess() {
400
+ if (this.tokenResetTimer) {
401
+ clearInterval(this.tokenResetTimer);
402
+ }
403
+
404
+ this.tokenResetTimer = setInterval(() => {
405
+ const now = Date.now();
406
+
407
+ this.expiredTokens.forEach(expiredTokenInfo => {
408
+ const { token, model, expiredTime } = expiredTokenInfo;
409
+ const expirationTime = this.modelConfig[model].ExpirationTime;
410
+ if (now - expiredTime >= expirationTime) {
411
+ if (!this.tokenModelMap[model].some(entry => entry.token === token)) {
412
+ this.tokenModelMap[model].push({
413
+ token: token,
414
+ RequestCount: 0,
415
+ AddedTime: now,
416
+ StartCallTime: null
417
+ });
418
+ }
419
+ const sso = token.split("sso=")[1].split(";")[0];
420
+
421
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
422
+ this.tokenStatusMap[sso][model].isValid = true;
423
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
424
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
425
+ }
426
+
427
+ this.expiredTokens.delete(expiredTokenInfo);
428
+ }
429
+ });
430
+
431
+ Object.keys(this.modelConfig).forEach(model => {
432
+ if (!this.tokenModelMap[model]) return;
433
+
434
+ const processedTokens = this.tokenModelMap[model].map(tokenEntry => {
435
+ if (!tokenEntry.StartCallTime) return tokenEntry;
436
+
437
+ const expirationTime = this.modelConfig[model].ExpirationTime;
438
+ if (now - tokenEntry.StartCallTime >= expirationTime) {
439
+ const sso = tokenEntry.token.split("sso=")[1].split(";")[0];
440
+ if (this.tokenStatusMap[sso] && this.tokenStatusMap[sso][model]) {
441
+ this.tokenStatusMap[sso][model].isValid = true;
442
+ this.tokenStatusMap[sso][model].invalidatedTime = null;
443
+ this.tokenStatusMap[sso][model].totalRequestCount = 0;
444
+ }
445
+
446
+ return {
447
+ ...tokenEntry,
448
+ RequestCount: 0,
449
+ StartCallTime: null
450
+ };
451
+ }
452
+
453
+ return tokenEntry;
454
+ });
455
+
456
+ this.tokenModelMap[model] = processedTokens;
457
+ });
458
+ }, 1 * 60 * 60 * 1000);
459
+ }
460
+
461
+ getAllTokens() {
462
+ const allTokens = new Set();
463
+ Object.values(this.tokenModelMap).forEach(modelTokens => {
464
+ modelTokens.forEach(entry => allTokens.add(entry.token));
465
+ });
466
+ return Array.from(allTokens);
467
+ }
468
+
469
+ getTokenStatusMap() {
470
+ return this.tokenStatusMap;
471
+ }
472
+ }
473
+
474
+
475
+ class Utils {
476
+ static delay(time) {
477
+ return new Promise(function (resolve) {
478
+ setTimeout(resolve, time)
479
+ });
480
+ }
481
+ static async organizeSearchResults(searchResults) {
482
+ // 确保传入的是有效的搜索结果对象
483
+ if (!searchResults || !searchResults.results) {
484
+ return '';
485
+ }
486
+
487
+ const results = searchResults.results;
488
+ const formattedResults = results.map((result, index) => {
489
+ // 处理可能为空的字段
490
+ const title = result.title || '未知标题';
491
+ const url = result.url || '#';
492
+ const preview = result.preview || '无预览内容';
493
+
494
+ return `\r\n<details><summary>资料[${index}]: ${title}</summary>\r\n${preview}\r\n\n[Link](${url})\r\n</details>`;
495
+ });
496
+ return formattedResults.join('\n\n');
497
+ }
498
+ static async createAuthHeaders(model) {
499
+ return await tokenManager.getNextTokenForModel(model);
500
+ }
501
+ }
502
+ class GrokTempCookieManager {
503
+ constructor() {
504
+ this.cookies = [];
505
+ this.currentIndex = 0;
506
+ this.isRefreshing = false;
507
+ this.initialCookieCount = CONFIG.API.GROK2_CONCURRENCY_LEVEL;
508
+ this.extractCount = 0;
509
+ }
510
+
511
+ async ensureCookies() {
512
+ // 如果 cookies 数量不足,则重新获取
513
+ if (this.cookies.length < this.initialCookieCount) {
514
+ await this.refreshCookies();
515
+ }
516
+ }
517
+ async extractGrokHeaders(browser) {
518
+ Logger.info("开始提取头信息", 'Server');
519
+ try {
520
+ const page = await browser.newPage();
521
+ await page.goto('https://grok.com/', { waitUntil: 'domcontentloaded' });
522
+ let waitTime = 0;
523
+ const targetHeaders = ['x-anonuserid', 'x-challenge', 'x-signature'];
524
+
525
+ while (true) {
526
+ const cookies = await page.cookies();
527
+ const extractedHeaders = cookies
528
+ .filter(cookie => targetHeaders.includes(cookie.name.toLowerCase()))
529
+ .map(cookie => `${cookie.name}=${cookie.value}`);
530
+
531
+ if (targetHeaders.every(header =>
532
+ extractedHeaders.some(cookie => cookie && cookie.startsWith(header + '='))
533
+ )) {
534
+ await browser.close();
535
+ Logger.info('提取的头信息:', JSON.stringify(extractedHeaders, null, 2), 'Server');
536
+ this.cookies.push(extractedHeaders.join(';'));
537
+ this.extractCount++;
538
+ return true;
539
+ }
540
+
541
+ await Utils.delay(500);
542
+ waitTime += 500;
543
+ if (waitTime >= 10000) {
544
+ await browser.close();
545
+ return null;
546
+ }
547
+ }
548
+ } catch (error) {
549
+ Logger.error('获取头信息出错:', error, 'Server');
550
+ return null;
551
+ }
552
+ }
553
+ async initializeTempCookies(count = 1) {
554
+ Logger.info(`开始初始化 ${count} 个临时账号认证信息`, 'Server');
555
+ const browserOptions = {
556
+ headless: true,
557
+ args: [
558
+ '--no-sandbox',
559
+ '--disable-setuid-sandbox',
560
+ '--disable-dev-shm-usage',
561
+ '--disable-gpu'
562
+ ],
563
+ executablePath: CONFIG.CHROME_PATH
564
+ };
565
+
566
+ const browsers = await Promise.all(
567
+ Array.from({ length: count }, () => puppeteer.launch(browserOptions))
568
+ );
569
+
570
+ const cookiePromises = browsers.map(browser => this.extractGrokHeaders(browser));
571
+ return Promise.all(cookiePromises);
572
+ }
573
+ async refreshCookies() {
574
+ if (this.isRefreshing) return;
575
+ this.isRefreshing = true;
576
+ this.extractCount = 0;
577
+ try {
578
+ // 获取新的 cookies
579
+ let retryCount = 0;
580
+ let remainingCount = this.initialCookieCount - this.cookies.length;
581
+
582
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
583
+ await this.initializeTempCookies(remainingCount);
584
+ if (this.extractCount != remainingCount) {
585
+ if (this.extractCount == 0) {
586
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
587
+ } else if (this.extractCount < remainingCount) {
588
+ remainingCount -= this.extractCount;
589
+ this.extractCount = 0;
590
+ retryCount++;
591
+ await Utils.delay(1000 * retryCount);
592
+ } else {
593
+ break;
594
+ }
595
+ } else {
596
+ break;
597
+ }
598
+ }
599
+ if (this.currentIndex >= this.cookies.length) {
600
+ this.currentIndex = 0;
601
+ }
602
+
603
+ if (this.cookies.length < this.initialCookieCount) {
604
+ if (this.cookies.length !== 0) {
605
+ // 如果已经获取到一些 TempCookies,则只提示警告错误
606
+ Logger.error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
607
+ } else {
608
+ // 如果未获取到任何 TempCookies,则抛出错误
609
+ throw new Error(`无法获取足够的有效 TempCookies,可能网络存在问题,当前数量:${this.cookies.length}`);
610
+ }
611
+ }
612
+ } catch (error) {
613
+ Logger.error('刷新 cookies 失败:', error);
614
+ } finally {
615
+ Logger.info(`已提取${this.cookies.length}个TempCookies`, 'Server');
616
+ Logger.info(`提取的TempCookies为${JSON.stringify(this.cookies, null, 2)}`, 'Server');
617
+ this.isRefreshing = false;
618
+ }
619
+ }
620
+ }
621
+
622
+ class GrokApiClient {
623
+ constructor(modelId) {
624
+ if (!CONFIG.MODELS[modelId]) {
625
+ throw new Error(`不支持的模型: ${modelId}`);
626
+ }
627
+ this.modelId = CONFIG.MODELS[modelId];
628
+ }
629
+
630
+ processMessageContent(content) {
631
+ if (typeof content === 'string') return content;
632
+ return null;
633
+ }
634
+ // 获取图片类型
635
+ getImageType(base64String) {
636
+ let mimeType = 'image/jpeg';
637
+ if (base64String.includes('data:image')) {
638
+ const matches = base64String.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,/);
639
+ if (matches) {
640
+ mimeType = matches[1];
641
+ }
642
+ }
643
+ const extension = mimeType.split('/')[1];
644
+ const fileName = `image.${extension}`;
645
+
646
+ return {
647
+ mimeType: mimeType,
648
+ fileName: fileName
649
+ };
650
+ }
651
+
652
+ async uploadBase64Image(base64Data, url) {
653
+ try {
654
+ // 处理 base64 数据
655
+ let imageBuffer;
656
+ if (base64Data.includes('data:image')) {
657
+ imageBuffer = base64Data.split(',')[1];
658
+ } else {
659
+ imageBuffer = base64Data
660
+ }
661
+ const { mimeType, fileName } = this.getImageType(base64Data);
662
+ let uploadData = {
663
+ rpc: "uploadFile",
664
+ req: {
665
+ fileName: fileName,
666
+ fileMimeType: mimeType,
667
+ content: imageBuffer
668
+ }
669
+ };
670
+ Logger.info("发送图片请求", 'Server');
671
+ // 发送请求
672
+ const response = await fetch(url, {
673
+ method: 'POST',
674
+ headers: {
675
+ ...CONFIG.DEFAULT_HEADERS,
676
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
677
+ },
678
+ body: JSON.stringify(uploadData)
679
+ });
680
+
681
+ if (!response.ok) {
682
+ Logger.error(`上传图片失败,状态码:${response.status},原因:${response.error}`, 'Server');
683
+ return '';
684
+ }
685
+
686
+ const result = await response.json();
687
+ Logger.info('上传图片成功:', result, 'Server');
688
+ return result.fileMetadataId;
689
+
690
+ } catch (error) {
691
+ Logger.error(error, 'Server');
692
+ return '';
693
+ }
694
+ }
695
+
696
+ async prepareChatRequest(request) {
697
+ if ((request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') && !CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY && request.stream) {
698
+ throw new Error(`该模型流式输出需要配置PICGO或者TUMY图床密钥!`);
699
+ }
700
+
701
+ // 处理画图模型的消息限制
702
+ let todoMessages = request.messages;
703
+ if (request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen') {
704
+ const lastMessage = todoMessages[todoMessages.length - 1];
705
+ if (lastMessage.role !== 'user') {
706
+ throw new Error('画图模型的最后一条消息必须是用户消息!');
707
+ }
708
+ todoMessages = [lastMessage];
709
+ }
710
+
711
+ const fileAttachments = [];
712
+ let messages = '';
713
+ let lastRole = null;
714
+ let lastContent = '';
715
+ const search = request.model === 'grok-2-search' || request.model === 'grok-3-search';
716
+
717
+ // 移除<think>标签及其内容和base64图片
718
+ const removeThinkTags = (text) => {
719
+ text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
720
+ text = text.replace(/!\[image\]\(data:.*?base64,.*?\)/g, '[图片]');
721
+ return text;
722
+ };
723
+
724
+ const processImageUrl = async (content) => {
725
+ if (content.type === 'image_url' && content.image_url.url.includes('data:image')) {
726
+ const imageResponse = await this.uploadBase64Image(
727
+ content.image_url.url,
728
+ `${CONFIG.API.BASE_URL}/api/rpc`
729
+ );
730
+ return imageResponse;
731
+ }
732
+ return null;
733
+ };
734
+
735
+ const processContent = async (content) => {
736
+ if (Array.isArray(content)) {
737
+ let textContent = '';
738
+ for (const item of content) {
739
+ if (item.type === 'image_url') {
740
+ textContent += (textContent ? '\n' : '') + "[图片]";
741
+ } else if (item.type === 'text') {
742
+ textContent += (textContent ? '\n' : '') + removeThinkTags(item.text);
743
+ }
744
+ }
745
+ return textContent;
746
+ } else if (typeof content === 'object' && content !== null) {
747
+ if (content.type === 'image_url') {
748
+ return "[图片]";
749
+ } else if (content.type === 'text') {
750
+ return removeThinkTags(content.text);
751
+ }
752
+ }
753
+ return removeThinkTags(this.processMessageContent(content));
754
+ };
755
+
756
+ for (const current of todoMessages) {
757
+ const role = current.role === 'assistant' ? 'assistant' : 'user';
758
+ const isLastMessage = current === todoMessages[todoMessages.length - 1];
759
+
760
+ // 处理图片附件
761
+ if (isLastMessage && current.content) {
762
+ if (Array.isArray(current.content)) {
763
+ for (const item of current.content) {
764
+ if (item.type === 'image_url') {
765
+ const processedImage = await processImageUrl(item);
766
+ if (processedImage) fileAttachments.push(processedImage);
767
+ }
768
+ }
769
+ } else if (current.content.type === 'image_url') {
770
+ const processedImage = await processImageUrl(current.content);
771
+ if (processedImage) fileAttachments.push(processedImage);
772
+ }
773
+ }
774
+
775
+ // 处理文本内容
776
+ const textContent = await processContent(current.content);
777
+
778
+ if (textContent || (isLastMessage && fileAttachments.length > 0)) {
779
+ if (role === lastRole && textContent) {
780
+ lastContent += '\n' + textContent;
781
+ messages = messages.substring(0, messages.lastIndexOf(`${role.toUpperCase()}: `)) +
782
+ `${role.toUpperCase()}: ${lastContent}\n`;
783
+ } else {
784
+ messages += `${role.toUpperCase()}: ${textContent || '[图片]'}\n`;
785
+ lastContent = textContent;
786
+ lastRole = role;
787
+ }
788
+ }
789
+ }
790
+
791
+ return {
792
+ temporary: CONFIG.API.IS_TEMP_CONVERSATION,
793
+ modelName: this.modelId,
794
+ message: messages.trim(),
795
+ fileAttachments: fileAttachments.slice(0, 4),
796
+ imageAttachments: [],
797
+ disableSearch: false,
798
+ enableImageGeneration: true,
799
+ returnImageBytes: false,
800
+ returnRawGrokInXaiRequest: false,
801
+ enableImageStreaming: false,
802
+ imageGenerationCount: 1,
803
+ forceConcise: false,
804
+ toolOverrides: {
805
+ imageGen: request.model === 'grok-2-imageGen' || request.model === 'grok-3-imageGen',
806
+ webSearch: search,
807
+ xSearch: search,
808
+ xMediaSearch: search,
809
+ trendsSearch: search,
810
+ xPostAnalyze: search
811
+ },
812
+ enableSideBySide: true,
813
+ isPreset: false,
814
+ sendFinalMetadata: true,
815
+ customInstructions: "",
816
+ deepsearchPreset: request.model === 'grok-3-deepsearch' ? "default" : "",
817
+ isReasoning: request.model === 'grok-3-reasoning'
818
+ };
819
+ }
820
+ }
821
+
822
+ class MessageProcessor {
823
+ static createChatResponse(message, model, isStream = false) {
824
+ const baseResponse = {
825
+ id: `chatcmpl-${uuidv4()}`,
826
+ created: Math.floor(Date.now() / 1000),
827
+ model: model
828
+ };
829
+
830
+ if (isStream) {
831
+ return {
832
+ ...baseResponse,
833
+ object: 'chat.completion.chunk',
834
+ choices: [{
835
+ index: 0,
836
+ delta: {
837
+ content: message
838
+ }
839
+ }]
840
+ };
841
+ }
842
+
843
+ return {
844
+ ...baseResponse,
845
+ object: 'chat.completion',
846
+ choices: [{
847
+ index: 0,
848
+ message: {
849
+ role: 'assistant',
850
+ content: message
851
+ },
852
+ finish_reason: 'stop'
853
+ }],
854
+ usage: null
855
+ };
856
+ }
857
+ }
858
+ async function processModelResponse(response, model) {
859
+ let result = { token: null, imageUrl: null }
860
+ if (CONFIG.IS_IMG_GEN) {
861
+ if (response?.cachedImageGenerationResponse && !CONFIG.IS_IMG_GEN2) {
862
+ result.imageUrl = response.cachedImageGenerationResponse.imageUrl;
863
+ }
864
+ return result;
865
+ }
866
+
867
+ //非生图模型的处理
868
+ switch (model) {
869
+ case 'grok-2':
870
+ result.token = response?.token;
871
+ return result;
872
+ case 'grok-2-search':
873
+ case 'grok-3-search':
874
+ if (response?.webSearchResults && CONFIG.ISSHOW_SEARCH_RESULTS) {
875
+ result.token = `\r\n<think>${await Utils.organizeSearchResults(response.webSearchResults)}</think>\r\n`;
876
+ } else {
877
+ result.token = response?.token;
878
+ }
879
+ return result;
880
+ case 'grok-3':
881
+ result.token = response?.token;
882
+ return result;
883
+ case 'grok-3-deepsearch':
884
+ if (response?.messageTag === "final") {
885
+ result.token = response?.token;
886
+ }
887
+ return result;
888
+ case 'grok-3-reasoning':
889
+ if (response?.isThinking && !CONFIG.SHOW_THINKING) return result;
890
+
891
+ if (response?.isThinking && !CONFIG.IS_THINKING) {
892
+ result.token = "<think>" + response?.token;
893
+ CONFIG.IS_THINKING = true;
894
+ } else if (!response.isThinking && CONFIG.IS_THINKING) {
895
+ result.token = "</think>" + response?.token;
896
+ CONFIG.IS_THINKING = false;
897
+ } else {
898
+ result.token = response?.token;
899
+ }
900
+ return result;
901
+ }
902
+ return result;
903
+ }
904
+
905
+ async function handleResponse(response, model, res, isStream) {
906
+ try {
907
+ const stream = response.body;
908
+ let buffer = '';
909
+ let fullResponse = '';
910
+ const dataPromises = [];
911
+ if (isStream) {
912
+ res.setHeader('Content-Type', 'text/event-stream');
913
+ res.setHeader('Cache-Control', 'no-cache');
914
+ res.setHeader('Connection', 'keep-alive');
915
+ }
916
+ CONFIG.IS_THINKING = false;
917
+ CONFIG.IS_IMG_GEN = false;
918
+ CONFIG.IS_IMG_GEN2 = false;
919
+ Logger.info("开始处理流式响应", 'Server');
920
+
921
+ return new Promise((resolve, reject) => {
922
+ stream.on('data', async (chunk) => {
923
+ buffer += chunk.toString();
924
+ const lines = buffer.split('\n');
925
+ buffer = lines.pop() || '';
926
+
927
+ for (const line of lines) {
928
+ if (!line.trim()) continue;
929
+ try {
930
+ const linejosn = JSON.parse(line.trim());
931
+ if (linejosn?.error) {
932
+ Logger.error(JSON.stringify(linejosn, null, 2), 'Server');
933
+ if (linejosn.error?.name === "RateLimitError") {
934
+ CONFIG.API.TEMP_COOKIE = null;
935
+ }
936
+ stream.destroy();
937
+ reject(new Error("RateLimitError"));
938
+ return;
939
+ }
940
+ let response = linejosn?.result?.response;
941
+ if (!response) continue;
942
+ if (response?.doImgGen || response?.imageAttachmentInfo) {
943
+ CONFIG.IS_IMG_GEN = true;
944
+ }
945
+ const processPromise = (async () => {
946
+ const result = await processModelResponse(response, model);
947
+
948
+ if (result.token) {
949
+ if (isStream) {
950
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(result.token, model, true))}\n\n`);
951
+ } else {
952
+ fullResponse += result.token;
953
+ }
954
+ }
955
+ if (result.imageUrl) {
956
+ CONFIG.IS_IMG_GEN2 = true;
957
+ const dataImage = await handleImageResponse(result.imageUrl);
958
+ if (isStream) {
959
+ res.write(`data: ${JSON.stringify(MessageProcessor.createChatResponse(dataImage, model, true))}\n\n`);
960
+ } else {
961
+ res.json(MessageProcessor.createChatResponse(dataImage, model));
962
+ }
963
+ }
964
+ })();
965
+ dataPromises.push(processPromise);
966
+ } catch (error) {
967
+ Logger.error(error, 'Server');
968
+ continue;
969
+ }
970
+ }
971
+ });
972
+
973
+ stream.on('end', async () => {
974
+ try {
975
+ await Promise.all(dataPromises);
976
+ if (isStream) {
977
+ res.write('data: [DONE]\n\n');
978
+ res.end();
979
+ } else {
980
+ if (!CONFIG.IS_IMG_GEN2) {
981
+ res.json(MessageProcessor.createChatResponse(fullResponse, model));
982
+ }
983
+ }
984
+ resolve();
985
+ } catch (error) {
986
+ Logger.error(error, 'Server');
987
+ reject(error);
988
+ }
989
+ });
990
+
991
+ stream.on('error', (error) => {
992
+ Logger.error(error, 'Server');
993
+ reject(error);
994
+ });
995
+ });
996
+ } catch (error) {
997
+ Logger.error(error, 'Server');
998
+ throw new Error(error);
999
+ }
1000
+ }
1001
+
1002
+ async function handleImageResponse(imageUrl) {
1003
+ const MAX_RETRIES = 2;
1004
+ let retryCount = 0;
1005
+ let imageBase64Response;
1006
+
1007
+ while (retryCount < MAX_RETRIES) {
1008
+ try {
1009
+ imageBase64Response = await fetch(`https://assets.grok.com/${imageUrl}`, {
1010
+ method: 'GET',
1011
+ headers: {
1012
+ ...DEFAULT_HEADERS,
1013
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
1014
+ }
1015
+ });
1016
+
1017
+ if (imageBase64Response.ok) break;
1018
+ retryCount++;
1019
+ if (retryCount === MAX_RETRIES) {
1020
+ throw new Error(`上游服务请求失败! status: ${imageBase64Response.status}`);
1021
+ }
1022
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1023
+
1024
+ } catch (error) {
1025
+ Logger.error(error, 'Server');
1026
+ retryCount++;
1027
+ if (retryCount === MAX_RETRIES) {
1028
+ throw error;
1029
+ }
1030
+ await new Promise(resolve => setTimeout(resolve, CONFIG.API.RETRY_TIME * retryCount));
1031
+ }
1032
+ }
1033
+
1034
+
1035
+ const arrayBuffer = await imageBase64Response.arrayBuffer();
1036
+ const imageBuffer = Buffer.from(arrayBuffer);
1037
+
1038
+ if (!CONFIG.API.PICGO_KEY && !CONFIG.API.TUMY_KEY) {
1039
+ const base64Image = imageBuffer.toString('base64');
1040
+ const imageContentType = imageBase64Response.headers.get('content-type');
1041
+ return `![image](data:${imageContentType};base64,${base64Image})`
1042
+ }
1043
+
1044
+ Logger.info("开始上传图床", 'Server');
1045
+ const formData = new FormData();
1046
+ if (CONFIG.API.PICGO_KEY) {
1047
+ formData.append('source', imageBuffer, {
1048
+ filename: `image-${Date.now()}.jpg`,
1049
+ contentType: 'image/jpeg'
1050
+ });
1051
+ const formDataHeaders = formData.getHeaders();
1052
+ const responseURL = await fetch("https://www.picgo.net/api/1/upload", {
1053
+ method: "POST",
1054
+ headers: {
1055
+ ...formDataHeaders,
1056
+ "Content-Type": "multipart/form-data",
1057
+ "X-API-Key": CONFIG.API.PICGO_KEY
1058
+ },
1059
+ body: formData
1060
+ });
1061
+ if (!responseURL.ok) {
1062
+ return "生图失败,请查看PICGO图床密钥是否设置正确"
1063
+ } else {
1064
+ Logger.info("生图成功", 'Server');
1065
+ const result = await responseURL.json();
1066
+ return `![image](${result.image.url})`
1067
+ }
1068
+ } else if (CONFIG.API.TUMY_KEY) {
1069
+ const formData = new FormData();
1070
+ formData.append('file', imageBuffer, {
1071
+ filename: `image-${Date.now()}.jpg`,
1072
+ contentType: 'image/jpeg'
1073
+ });
1074
+ const formDataHeaders = formData.getHeaders();
1075
+ const responseURL = await fetch("https://tu.my/api/v1/upload", {
1076
+ method: "POST",
1077
+ headers: {
1078
+ ...formDataHeaders,
1079
+ "Accept": "application/json",
1080
+ 'Authorization': `Bearer ${CONFIG.API.TUMY_KEY}`
1081
+ },
1082
+ body: formData
1083
+ });
1084
+ if (!responseURL.ok) {
1085
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1086
+ } else {
1087
+ try {
1088
+ const result = await responseURL.json();
1089
+ Logger.info("生图成功", 'Server');
1090
+ return `![image](${result.data.links.url})`
1091
+ } catch (error) {
1092
+ Logger.error(error, 'Server');
1093
+ return "生图失败,请查看TUMY图床密钥是否设置正确"
1094
+ }
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ const tokenManager = new AuthTokenManager();
1100
+ const tempCookieManager = new GrokTempCookieManager();
1101
+ await initialization();
1102
+
1103
+ // 中间件配置
1104
+ const app = express();
1105
+ app.use(Logger.requestLogger);
1106
+ app.use(express.json({ limit: CONFIG.SERVER.BODY_LIMIT }));
1107
+ app.use(express.urlencoded({ extended: true, limit: CONFIG.SERVER.BODY_LIMIT }));
1108
+ app.use(cors({
1109
+ origin: '*',
1110
+ methods: ['GET', 'POST', 'OPTIONS'],
1111
+ allowedHeaders: ['Content-Type', 'Authorization']
1112
+ }));
1113
+
1114
+
1115
+ app.get('/get/tokens', (req, res) => {
1116
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1117
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1118
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法获取轮询sso令牌状态' });
1119
+ } else if (authToken !== CONFIG.API.API_KEY) {
1120
+ return res.status(401).json({ error: 'Unauthorized' });
1121
+ }
1122
+ res.json(tokenManager.getTokenStatusMap());
1123
+ });
1124
+ app.post('/add/token', async (req, res) => {
1125
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1126
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1127
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法添加sso令牌' });
1128
+ } else if (authToken !== CONFIG.API.API_KEY) {
1129
+ return res.status(401).json({ error: 'Unauthorized' });
1130
+ }
1131
+ try {
1132
+ const sso = req.body.sso;
1133
+ await tokenManager.addToken(`sso-rw=${sso};sso=${sso}`);
1134
+ res.status(200).json(tokenManager.getTokenStatusMap()[sso]);
1135
+ } catch (error) {
1136
+ Logger.error(error, 'Server');
1137
+ res.status(500).json({ error: '添加sso令牌失败' });
1138
+ }
1139
+ });
1140
+ app.post('/delete/token', async (req, res) => {
1141
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1142
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1143
+ return res.status(403).json({ error: '自定义的SSO令牌模式无法删除sso令牌' });
1144
+ } else if (authToken !== CONFIG.API.API_KEY) {
1145
+ return res.status(401).json({ error: 'Unauthorized' });
1146
+ }
1147
+ try {
1148
+ const sso = req.body.sso;
1149
+ await tokenManager.deleteToken(`sso-rw=${sso};sso=${sso}`);
1150
+ res.status(200).json({ message: '删除sso令牌成功' });
1151
+ } catch (error) {
1152
+ Logger.error(error, 'Server');
1153
+ res.status(500).json({ error: '删除sso令牌失败' });
1154
+ }
1155
+ });
1156
+
1157
+ app.get('/v1/models', (req, res) => {
1158
+ res.json({
1159
+ object: "list",
1160
+ data: Object.keys(tokenManager.tokenModelMap).map((model, index) => ({
1161
+ id: model,
1162
+ object: "model",
1163
+ created: Math.floor(Date.now() / 1000),
1164
+ owned_by: "grok",
1165
+ }))
1166
+ });
1167
+ });
1168
+
1169
+
1170
+ app.post('/v1/chat/completions', async (req, res) => {
1171
+ try {
1172
+ const authToken = req.headers.authorization?.replace('Bearer ', '');
1173
+ if (CONFIG.API.IS_CUSTOM_SSO) {
1174
+ if (authToken) {
1175
+ const result = `sso=${authToken};ssp_rw=${authToken}`;
1176
+ tokenManager.setToken(result);
1177
+ } else {
1178
+ return res.status(401).json({ error: '自定义的SSO令牌缺失' });
1179
+ }
1180
+ } else if (authToken !== CONFIG.API.API_KEY) {
1181
+ return res.status(401).json({ error: 'Unauthorized' });
1182
+ }
1183
+ const { model, stream } = req.body;
1184
+ let isTempCookie = model.includes("grok-2") && CONFIG.API.IS_TEMP_GROK2;
1185
+ let retryCount = 0;
1186
+ const grokClient = new GrokApiClient(model);
1187
+ const requestPayload = await grokClient.prepareChatRequest(req.body);
1188
+ //Logger.info(`请求体: ${JSON.stringify(requestPayload, null, 2)}`, 'Server');
1189
+
1190
+ while (retryCount < CONFIG.RETRY.MAX_ATTEMPTS) {
1191
+ retryCount++;
1192
+ if (isTempCookie) {
1193
+ CONFIG.API.SIGNATURE_COOKIE = CONFIG.API.TEMP_COOKIE;
1194
+ Logger.info(`已切换为临时令牌`, 'Server');
1195
+ } else {
1196
+ CONFIG.API.SIGNATURE_COOKIE = await Utils.createAuthHeaders(model);
1197
+ }
1198
+ if (!CONFIG.API.SIGNATURE_COOKIE) {
1199
+ throw new Error('该模型无可用令牌');
1200
+ }
1201
+ Logger.info(`当前令牌: ${JSON.stringify(CONFIG.API.SIGNATURE_COOKIE, null, 2)}`, 'Server');
1202
+ Logger.info(`当前可用模型的全部可用数量: ${JSON.stringify(tokenManager.getRemainingTokenRequestCapacity(), null, 2)}`, 'Server');
1203
+ const response = await fetch(`${CONFIG.API.BASE_URL}/rest/app-chat/conversations/new`, {
1204
+ method: 'POST',
1205
+ headers: {
1206
+ "accept": "text/event-stream",
1207
+ "baggage": "sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c",
1208
+ "content-type": "text/plain;charset=UTF-8",
1209
+ "Connection": "keep-alive",
1210
+ "cookie": CONFIG.API.SIGNATURE_COOKIE
1211
+ },
1212
+ body: JSON.stringify(requestPayload)
1213
+ });
1214
+
1215
+ if (response.ok) {
1216
+ Logger.info(`请求成功`, 'Server');
1217
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1218
+ try {
1219
+ await handleResponse(response, model, res, stream);
1220
+ Logger.info(`请求结束`, 'Server');
1221
+ return;
1222
+ } catch (error) {
1223
+ Logger.error(error, 'Server');
1224
+ if (isTempCookie) {
1225
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1226
+ if (tempCookieManager.cookies.length != 0) {
1227
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1228
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1229
+ tempCookieManager.ensureCookies()
1230
+ } else {
1231
+ try {
1232
+ await tempCookieManager.ensureCookies();
1233
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1234
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1235
+ } catch (error) {
1236
+ throw error;
1237
+ }
1238
+ }
1239
+ } else {
1240
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1241
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1242
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1243
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1244
+ }
1245
+ }
1246
+ }
1247
+ } else {
1248
+ if (response.status === 429) {
1249
+ if (isTempCookie) {
1250
+ // 移除当前失效的 cookie
1251
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1252
+ if (tempCookieManager.cookies.length != 0) {
1253
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1254
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1255
+ tempCookieManager.ensureCookies()
1256
+ } else {
1257
+ try {
1258
+ await tempCookieManager.ensureCookies();
1259
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1260
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1261
+ } catch (error) {
1262
+ throw error;
1263
+ }
1264
+ }
1265
+ } else {
1266
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1267
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1268
+ if (tokenManager.getTokenCountForModel(model) === 0) {
1269
+ throw new Error(`${model} 次数已达上限,请切换其他模型或者重新对话`);
1270
+ }
1271
+ }
1272
+ } else {
1273
+ // 非429错误直接抛出
1274
+ if (isTempCookie) {
1275
+ // 移除当前失效的 cookie
1276
+ tempCookieManager.cookies.splice(tempCookieManager.currentIndex, 1);
1277
+ if (tempCookieManager.cookies.length != 0) {
1278
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1279
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1280
+ tempCookieManager.ensureCookies()
1281
+ } else {
1282
+ try {
1283
+ await tempCookieManager.ensureCookies();
1284
+ tempCookieManager.currentIndex = tempCookieManager.currentIndex % tempCookieManager.cookies.length;
1285
+ CONFIG.API.TEMP_COOKIE = tempCookieManager.cookies[tempCookieManager.currentIndex];
1286
+ } catch (error) {
1287
+ throw error;
1288
+ }
1289
+ }
1290
+ } else {
1291
+ if (CONFIG.API.IS_CUSTOM_SSO) throw new Error(`自定义SSO令牌当前模型${model}的请求次数已失效`);
1292
+ Logger.error(`令牌异常错误状态!status: ${response.status}`, 'Server');
1293
+ tokenManager.removeTokenFromModel(model, CONFIG.API.SIGNATURE_COOKIE.cookie);
1294
+ Logger.info(`当前${model}剩余可用令牌数: ${tokenManager.getTokenCountForModel(model)}`, 'Server');
1295
+ }
1296
+ }
1297
+ }
1298
+ }
1299
+ throw new Error('当前模型所有令牌都已耗尽');
1300
+ } catch (error) {
1301
+ Logger.error(error, 'ChatAPI');
1302
+ res.status(500).json({
1303
+ error: {
1304
+ message: error.message || error,
1305
+ type: 'server_error'
1306
+ }
1307
+ });
1308
+ }
1309
+ });
1310
+
1311
+
1312
+ app.use((req, res) => {
1313
+ res.status(200).send('api运行正常');
1314
+ });
1315
+
1316
+
1317
+ app.listen(CONFIG.SERVER.PORT, () => {
1318
+ Logger.info(`服务器已启动,监听端口: ${CONFIG.SERVER.PORT}`, 'Server');
1319
+ });
logger.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chalk from 'chalk';
2
+ import moment from 'moment';
3
+
4
+ const LogLevel = {
5
+ INFO: 'INFO',
6
+ WARN: 'WARN',
7
+ ERROR: 'ERROR',
8
+ DEBUG: 'DEBUG'
9
+ };
10
+
11
+ class Logger {
12
+ static formatMessage(level, message) {
13
+ const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
14
+
15
+ switch(level) {
16
+ case LogLevel.INFO:
17
+ return chalk.blue(`[${timestamp}] [${level}] ${message}`);
18
+ case LogLevel.WARN:
19
+ return chalk.yellow(`[${timestamp}] [${level}] ${message}`);
20
+ case LogLevel.ERROR:
21
+ return chalk.red(`[${timestamp}] [${level}] ${message}`);
22
+ case LogLevel.DEBUG:
23
+ return chalk.gray(`[${timestamp}] [${level}] ${message}`);
24
+ default:
25
+ return message;
26
+ }
27
+ }
28
+
29
+ static info(message, context) {
30
+ console.log(this.formatMessage(LogLevel.INFO, context ? `[${context}] ${message}` : message));
31
+ }
32
+
33
+ static warn(message, context) {
34
+ console.warn(this.formatMessage(LogLevel.WARN, context ? `[${context}] ${message}` : message));
35
+ }
36
+
37
+ static error(message, context, error = null) {
38
+ const errorMessage = error ? ` - ${error.message}` : '';
39
+ console.error(this.formatMessage(LogLevel.ERROR, `${context ? `[${context}] ` : ''}${message}${errorMessage}`));
40
+ }
41
+
42
+ static debug(message, context) {
43
+ if (process.env.NODE_ENV === 'development') {
44
+ console.debug(this.formatMessage(LogLevel.DEBUG, context ? `[${context}] ${message}` : message));
45
+ }
46
+ }
47
+
48
+ static requestLogger(req, res, next) {
49
+ const startTime = Date.now();
50
+
51
+ res.on('finish', () => {
52
+ const duration = Date.now() - startTime;
53
+ const logMessage = `${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`;
54
+
55
+ if (res.statusCode >= 400) {
56
+ Logger.error(logMessage, undefined, 'HTTP');
57
+ } else {
58
+ Logger.info(logMessage, 'HTTP');
59
+ }
60
+ });
61
+
62
+ next();
63
+ }
64
+ }
65
+
66
+ export default Logger;
package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "grok2api",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "node index.js"
8
+ },
9
+ "author": "yxmiler",
10
+ "dependencies": {
11
+ "express": "^4.18.2",
12
+ "node-fetch": "^3.3.2",
13
+ "dotenv": "^16.3.1",
14
+ "cors": "^2.8.5",
15
+ "form-data": "^4.0.0",
16
+ "puppeteer": "^22.8.2",
17
+ "puppeteer-extra": "^3.3.6",
18
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
19
+ "moment": "^2.30.1",
20
+ "chalk": "^5.4.1",
21
+ "uuid": "^9.0.0"
22
+ }
23
+ }
start.bat ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
templates/login.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>登录</title>
6
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
7
+ <style>
8
+ :root {
9
+ --bg-primary: #f4f6f9;
10
+ --card-bg: #ffffff;
11
+ --text-primary: #2c3e50;
12
+ --text-secondary: #6c757d;
13
+ --border-color: #e2e8f0;
14
+ --primary-color: #3498db;
15
+ }
16
+ * { box-sizing: border-box; margin: 0; padding: 0; }
17
+ body {
18
+ font-family: 'Inter', sans-serif;
19
+ background-color: var(--bg-primary);
20
+ color: var(--text-primary);
21
+ display: flex;
22
+ justify-content: center;
23
+ align-items: center;
24
+ height: 100vh;
25
+ }
26
+ .login-form {
27
+ background: var(--card-bg);
28
+ padding: 40px;
29
+ border-radius: 16px;
30
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
31
+ width: 100%;
32
+ max-width: 400px;
33
+ }
34
+ h2 {
35
+ text-align: center;
36
+ margin-bottom: 20px;
37
+ color: var(--text-primary);
38
+ }
39
+ input {
40
+ width: 100%;
41
+ padding: 12px 15px;
42
+ margin: 10px 0;
43
+ border: 1px solid var(--border-color);
44
+ border-radius: 8px;
45
+ font-size: 16px;
46
+ }
47
+ button {
48
+ width: 100%;
49
+ padding: 12px;
50
+ background-color: var(--primary-color);
51
+ color: white;
52
+ border: none;
53
+ border-radius: 8px;
54
+ cursor: pointer;
55
+ transition: background-color 0.3s;
56
+ }
57
+ button:hover { background-color: #2980b9; }
58
+ </style>
59
+ </head>
60
+ <body>
61
+ <div class="login-form">
62
+ <h2>管理员登录</h2>
63
+ <form action="/manager/login" method="post">
64
+ <input type="password" name="password" placeholder="输入管理员密码" required>
65
+ <button type="submit">登录</button>
66
+ </form>
67
+ </div>
68
+ {% if error %}
69
+ <div id="notification" style="position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background-color: #f44336; color: white; padding: 10px 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: block; z-index: 1000;">密码错误</div>
70
+ <script>
71
+ setTimeout(() => {
72
+ document.getElementById('notification').style.display = 'none';
73
+ }, 2000);
74
+ </script>
75
+ {% endif %}
76
+ </body>
77
+ </html>
templates/manager.html ADDED
@@ -0,0 +1,1081 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GrokAPI管理面板</title>
7
+ <style type="text/css">@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:300;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/latin-ext/wght/normal.woff2);unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:Inter;font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.0.16/greek/wght/normal.woff2);unicode-range:U+0370-03FF;font-display:swap;}</style>
8
+ <style>
9
+ :root {
10
+ --primary: #3498db;
11
+ --primary-hover: #1575b5;
12
+ --secondary: #64748B;
13
+ --success: #10B981;
14
+ --danger: #EF4444;
15
+ --warning: #F59E0B;
16
+ --bg-light: #F8FAFC;
17
+ --bg-white: #FFFFFF;
18
+ --text-dark: #1F2937;
19
+ --text-muted: #64748B;
20
+ --border: #E2E8F0;
21
+ --shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
22
+ --card-border: #E2E8F0;
23
+ --progress-bg: #E2E8F0;
24
+ --status-active-bg: rgba(16, 185, 129, 0.1);
25
+ --status-expired-bg: rgba(239, 68, 68, 0.1);
26
+ --progress-fill-success: #10B981;
27
+ --progress-fill-warning: #F59E0B;
28
+ --progress-fill-danger: #EF4444;
29
+ }
30
+
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body { font-family: 'Inter', sans-serif; background: var(--bg-light); color: var(--text-dark); line-height: 1.6; min-height: 100vh; padding-top: 5rem; }
33
+ .container { max-width: 1280px; margin: 0 auto; padding: 0 1.5rem; }
34
+ .card { background: var(--bg-white); border-radius: 0.75rem; box-shadow: var(--shadow); padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid var(--card-border); }
35
+
36
+ .search-section {
37
+ position: fixed;
38
+ top: 0;
39
+ left: 0;
40
+ right: 0;
41
+ z-index: 100;
42
+ background: var(--bg-white);
43
+ box-shadow: var(--shadow);
44
+ padding: 1rem 1.5rem;
45
+ margin: 0;
46
+ }
47
+ .search-section .card {
48
+ padding: 0;
49
+ margin: 0;
50
+ box-shadow: none;
51
+ border: none;
52
+ }
53
+ .search-input {
54
+ width: 100%;
55
+ padding: 0.75rem 1rem 0.75rem 2.5rem;
56
+ border: 1px solid #CBD5E1;
57
+ border-radius: 0.5rem;
58
+ font-size: 0.9rem;
59
+ background: #F9FAFB url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="%2364748B" stroke-width="2"><circle cx="7" cy="7" r="5"/><path d="M11 11l4 4"/></svg>') no-repeat 0.75rem center;
60
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
61
+ }
62
+ .search-input:focus {
63
+ border-color: #0f5fc3;
64
+ outline: none;
65
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2);
66
+ }
67
+ .btn-base { padding: 0.5rem 1rem; border-radius: 0.5rem; border: none; cursor: pointer; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: transform 0.2s ease, background 0.3s ease, box-shadow 0.2s ease; }
68
+ .btn { background: linear-gradient(135deg, var(--primary) 0%, #0f5fc3 100%); color: white; }
69
+ .btn:hover { background: linear-gradient(135deg, var(--primary-hover) 0%, #0f5fc3 100%); transform: translateY(-2px); box-shadow: 0 4px 15px rgba(107, 70, 193, 0.3); }
70
+ .btn-secondary { background: linear-gradient(135deg, var(--primary) 0%, #0f5fc3 100%); color: white; width: 2rem; height: 2rem; padding: 0; justify-content: center; }
71
+ .btn-secondary:hover { background: linear-gradient(135deg, var(--primary-hover) 0%, #0f5fc3 100%); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(107, 70, 193, 0.3); }
72
+ .btn-danger { background: var(--danger); width: 2rem; height: 2rem; padding: 0; justify-content: center; }
73
+ .btn-danger:hover { background: #DC2626; }
74
+ .token-manage-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; margin-top: 1rem; }
75
+ .input-group { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
76
+ .input-field { flex: 1; min-width: 0; padding: 0.75rem; border: 1px solid #CBD5E1; border-radius: 0.5rem; font-size: 0.9rem; background: #F9FAFB; transition: border-color 0.2s ease, box-shadow 0.2s ease; }
77
+ .input-field:focus { border-color: #0f5fc3; outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2); }
78
+ #statusFilter, #modelSelect { background: #F9FAFB; border: 1px solid #CBD5E1; color: var(--text-dark); }
79
+ #statusFilter:focus, #modelSelect:focus { border-color: #0f5fc3; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.2); }
80
+ .token-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
81
+ .token-card { background: var(--bg-white); border: 1px solid var(--card-border); border-radius: 0.75rem; box-shadow: var(--shadow); padding: 1rem; display: flex; flex-direction: column; gap: 1rem; transition: transform 0.2s ease; max-width: 100%; overflow: hidden; }
82
+ .token-card:hover { transform: translateY(-4px); }
83
+ .token-checkbox { display: none; }
84
+ .token-checkbox.show { display: block; position: absolute; top: 1rem; left: 1rem; }
85
+ .token-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.75rem; }
86
+ .token-title { font-weight: 600; font-size: 0.9rem; color: var(--text-dark); flex: 1 1 auto; max-width: calc(100% - 80px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
87
+ .token-actions { display: flex; gap: 0.5rem; flex-shrink: 0; }
88
+ .model-list { display: grid; gap: 0.75rem; }
89
+ .model-item { display: grid; grid-template-columns: 1fr 2fr 80px; gap: 0.5rem; align-items: center; padding: 0.5rem; background: rgba(248, 250, 252, 0.5); border-radius: 0.5rem; }
90
+ .model-name { font-size: 0.85rem; font-weight: 500; color: var(--text-dark); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
91
+ .progress-container { display: flex; align-items: center; gap: 0.5rem; }
92
+ .progress-bar { flex: 1; height: 0.375rem; background: var(--progress-bg); border-radius: 1rem; overflow: hidden; }
93
+ .progress-fill { height: 100%; border-radius: 1rem; transition: width 0.3s ease; }
94
+ .progress-text { font-size: 0.75rem; color: var(--text-muted); min-width: 40px; }
95
+ .status { font-size: 0.75rem; padding: 0.25rem 0.75rem; border-radius: 1rem; text-align: center; font-weight: 500; }
96
+ .status-active { background: var(--status-active-bg); color: var(--success); }
97
+ .status-expired { background: var(--status-expired-bg); color: var(--danger); position: relative; }
98
+ .status-expired .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--text-dark); color: white; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s; margin-bottom: 0.5rem; }
99
+ .status-expired:hover .tooltip { opacity: 1; visibility: visible; }
100
+ .notification { position: fixed; top: 5rem; left: 50%; transform: translateX(-50%); background: var(--primary); color: white; padding: 0.75rem 1.5rem; border-radius: 0.5rem; box-shadow: var(--shadow); z-index: 1000; display: none; animation: slideIn 0.3s ease; }
101
+ @keyframes slideIn { from { transform: translateX(-50%) translateY(-100%); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } }
102
+
103
+ .overview-section { background: var(--bg-white); border-radius: 1rem; padding: 1.5rem; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06); position: relative; overflow: hidden; }
104
+ .overview-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
105
+ .overview-title { font-size: 1.5rem; font-weight: 700; color: var(--text-dark); }
106
+ .overview-actions { display: flex; gap: 0.75rem; }
107
+ .overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
108
+ .overview-card { background: linear-gradient(135deg, #F9FAFB 0%, #F1F5F9 100%); border-radius: 0.75rem; padding: 1rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); transition: transform 0.2s ease, box-shadow 0.2s ease; display: flex; align-items: center; gap: 1rem; position: relative; border: 1px solid rgba(107, 70, 193, 0.1); }
109
+ .overview-card:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(107, 70, 193, 0.15); }
110
+ .overview-icon { width: 2.5rem; height: 2.5rem; background: linear-gradient(135deg, rgba(107, 70, 193, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center; color: var(--primary); flex-shrink: 0; }
111
+ .overview-content { display: flex; flex-direction: column; gap: 0.25rem; }
112
+ .overview-label { font-size: 0.9rem; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05px; }
113
+ .overview-value { font-size: 1.75rem; font-weight: 700; color: var(--primary); }
114
+ .overview-card::after { content: attr(data-tooltip); position: absolute; top: -2.5rem; left: 50%; transform: translateX(-50%); background: rgba(31, 41, 55, 0.9); color: white; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.85rem; white-space: nowrap; opacity: 0; visibility: hidden; transition: opacity 0.2s ease; pointer-events: none; }
115
+ .overview-card:hover::after { opacity: 1; visibility: visible; }
116
+
117
+ .pagination { display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1.5rem; }
118
+ .pagination-btn { padding: 0.5rem 1rem; border-radius: 0.5rem; border: 1px solid var(--border); background: var(--bg-white); cursor: pointer; font-size: 0.9rem; transition: background 0.2s ease; }
119
+ .pagination-btn:hover { background: var(--bg-light); }
120
+ .pagination-btn:disabled { opacity: 0.5; cursor: not-allowed; }
121
+ .pagination-select { padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.5rem; font-size: 0.9rem; }
122
+
123
+ @media (max-width: 768px) {
124
+ .overview-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.75rem; }
125
+ .overview-card { padding: 0.75rem; }
126
+ .overview-icon { width: 2rem; height: 2rem; }
127
+ .overview-value { font-size: 1.5rem; }
128
+ .overview-label { font-size: 0.85rem; }
129
+ .token-grid { grid-template-columns: 1fr; gap: 1rem; }
130
+ .token-card { padding: 0.75rem; }
131
+ }
132
+
133
+ @media (min-width: 768px) {
134
+ .token-grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
135
+ .token-card { min-height: 240px; }
136
+ .token-manage-grid { grid-template-columns: repeat(2, 1fr); }
137
+ }
138
+
139
+ @media (max-width: 480px) {
140
+ body { padding: 5rem 1rem 1rem; }
141
+ .card { padding: 1rem; }
142
+ .overview-section { padding: 1rem; }
143
+ .overview-header { flex-direction: column; align-items: flex-start; gap: 1rem; }
144
+ .overview-actions { width: 100%; justify-content: flex-end; }
145
+ .overview-grid { grid-template-columns: 1fr; gap: 0.75rem; }
146
+ .overview-card { padding: 1rem; }
147
+ .overview-value { font-size: 1.5rem; }
148
+ .search-section { padding: 0.5rem 1rem; }
149
+ .search-section .input-group { flex-direction: column; gap: 0.5rem; }
150
+ .search-input, #statusFilter { width: 100%; }
151
+ .pagination { flex-wrap: wrap; gap: 0.75rem; }
152
+ .input-group label { width: 100%; margin-bottom: 0.5rem; }
153
+ .input-field { width: 100%; }
154
+ }
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <div class="search-section">
159
+ <div class="card">
160
+ <div class="input-group">
161
+ <input type="text" class="search-input" id="searchInput" placeholder="搜索 Token..." aria-label="搜索 Token" style="flex: 1;">
162
+ <select class="input-field" id="statusFilter" style="width: 120px;" aria-label="筛选 Token 状态">
163
+ <option value="all">全部</option>
164
+ <option value="active">活跃</option>
165
+ <option value="expired">失效</option>
166
+ </select>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <div class="container">
172
+ <div class="overview-section card">
173
+ <div class="overview-header">
174
+ <h2 class="overview-title">概览</h2>
175
+ <div class="overview-actions">
176
+ <button class="btn btn-base" id="batchDeleteTokens" aria-label="批量删除选中的 Token">
177
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
+ <path d="M3 6h18M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M10 11v6M14 11v6M4 6v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6"/>
179
+ </svg>
180
+ 批量删除
181
+ </button>
182
+ <button class="btn btn-base" id="refreshTokens" aria-label="刷新 Token 列表">
183
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
184
+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
185
+ <path d="M21 3v5h-5"/>
186
+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
187
+ <path d="M3 21v-5h5"/>
188
+ </svg>
189
+ 刷新
190
+ </button>
191
+ </div>
192
+ </div>
193
+ <div class="overview-grid">
194
+ <div class="overview-card" data-tooltip="总共管理的Token数量">
195
+ <div class="overview-icon">
196
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
197
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
198
+ <rect x="8" y="2" width="8" height="4" rx="1"/>
199
+ </svg>
200
+ </div>
201
+ <div class="overview-content">
202
+ <span class="overview-label">Token 总数</span>
203
+ <span class="overview-value" id="totalTokens">0</span>
204
+ </div>
205
+ </div>
206
+ <div class="overview-card" data-tooltip="grok-2 模型剩余可用次数">
207
+ <div class="overview-icon">
208
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
209
+ <path d="M12 2v20M2 12h20"/>
210
+ </svg>
211
+ </div>
212
+ <div class="overview-content">
213
+ <span class="overview-label">grok-2</span>
214
+ <span class="overview-value" id="grok-2-count">0</span>
215
+ </div>
216
+ </div>
217
+ <div class="overview-card" data-tooltip="grok-3 模型剩余可用次数">
218
+ <div class="overview-icon">
219
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
+ <circle cx="12" cy="12" r="10"/>
221
+ <path d="M12 2v4M12 18v4"/>
222
+ </svg>
223
+ </div>
224
+ <div class="overview-content">
225
+ <span class="overview-label">grok-3</span>
226
+ <span class="overview-value" id="grok-3-count">0</span>
227
+ </div>
228
+ </div>
229
+ <div class="overview-card" data-tooltip="grok-3-deepsearch 模型剩余可用次数">
230
+ <div class="overview-icon">
231
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
232
+ <circle cx="11" cy="11" r="8"/>
233
+ <path d="M21 21l-4.35-4.35"/>
234
+ </svg>
235
+ </div>
236
+ <div class="overview-content">
237
+ <span class="overview-label">grok-3-deepsearch</span>
238
+ <span class="overview-value" id="grok-3-deepsearch-count">0</span>
239
+ </div>
240
+ </div>
241
+ <div class="overview-card" data-tooltip="grok-3-deepersearch 模型剩余可用次数">
242
+ <div class="overview-icon">
243
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
244
+ <circle cx="11" cy="11" r="8"/>
245
+ <path d="M21 21l-4.35-4.35"/>
246
+ </svg>
247
+ </div>
248
+ <div class="overview-content">
249
+ <span class="overview-label">grok-3-deepersearch</span>
250
+ <span class="overview-value" id="grok-3-deepersearch-count">0</span>
251
+ </div>
252
+ </div>
253
+ <div class="overview-card" data-tooltip="grok-3-reasoning 模型剩余可用次数">
254
+ <div class="overview-icon">
255
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
256
+ <path d="M12 2a10 10 0 0 0-10 10c0 5 8 13 10 13s10-8 10-13a10 10 0 0 0-10-10z"/>
257
+ </svg>
258
+ </div>
259
+ <div class="overview-content">
260
+ <span class="overview-label">grok-3-reasoning</span>
261
+ <span class="overview-value" id="grok-3-reasoning-count">0</span>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+
267
+ <div class="card">
268
+ <h3>Token 管理</h3>
269
+ <div class="token-manage-grid">
270
+ <div>
271
+ <h4 style="margin-bottom: 0.75rem;">添加单个 SSO Token</h4>
272
+ <div class="input-group">
273
+ <input type="text" class="input-field" id="singleTokenInput" placeholder="输入单个 SSO Token" aria-label="输入单个 SSO Token">
274
+ <button class="btn btn-base" id="addSingleTokenBtn" aria-label="添加单个 SSO Token">添加</button>
275
+ </div>
276
+ </div>
277
+ <div>
278
+ <h4 style="margin-bottom: 0.75rem;">设置 CF Clearance</h4>
279
+ <div class="input-group">
280
+ <input type="text" class="input-field" id="cfInput" placeholder="输入 CF Clearance" aria-label="输入 CF Clearance">
281
+ <button class="btn btn-base" id="setCfBtn" aria-label="设置 CF Clearance">设置</button>
282
+ </div>
283
+ </div>
284
+ <div>
285
+ <h4 style="margin-bottom: 0.75rem;">批量添加 SSO Token</h4>
286
+ <div class="input-group">
287
+ <input type="text" class="input-field" id="batchTokenInput" placeholder="输入多个 SSO Token(用逗号隔开,如 ey1,ey2)" aria-label="输入多个 SSO Token">
288
+ <button class="btn btn-base" id="addBatchTokenBtn" aria-label="批量添加 SSO Token">添加</button>
289
+ </div>
290
+ </div>
291
+ <div>
292
+ <h4 style="margin-bottom: 0.75rem;">检测模型可用性</h4>
293
+ <div class="input-group">
294
+ <select class="input-field" id="modelSelect" style="width: 150px;" aria-label="选择要检测的模型">
295
+ <option value="grok-2">grok-2</option>
296
+ <option value="grok-3" selected>grok-3</option>
297
+ <option value="grok-3-reasoning">grok-3-reasoning</option>
298
+ <option value="grok-3-deepsearch">grok-3-deepsearch</option>
299
+ </select>
300
+ <button class="btn btn-base" id="testAvailabilityBtn" aria-label="检测模型可用性">检测</button>
301
+ </div>
302
+ </div>
303
+ <div>
304
+ <h4 style="margin-bottom: 0.75rem;">Base URL</h4>
305
+ <div class="input-group">
306
+ <input type="text" class="input-field" id="baseUrl" readonly aria-label="Base URL" value="">
307
+ <button class="btn btn-base" id="copyBaseUrlBtn" aria-label="复制 Base URL">
308
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
309
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
310
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
311
+ </svg>
312
+ 复制
313
+ </button>
314
+ </div>
315
+ </div>
316
+ <div>
317
+ <h4 style="margin-bottom: 0.75rem;">API Key</h4>
318
+ <div class="input-group">
319
+ <input type="text" class="input-field" id="apiKey" aria-label="API Key">
320
+ <button class="btn btn-base" id="copyApiKeyBtn" aria-label="复制 API Key">
321
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
322
+ <rect x="9" y="9" width="13" height="13" rx="2"/>
323
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
324
+ </svg>
325
+ 复制
326
+ </button>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="token-grid" id="tokenGrid"></div>
333
+ <div class="pagination" id="pagination">
334
+ <button class="pagination-btn" id="prevPage" aria-label="上一页">上一页</button>
335
+ <select class="pagination-select" id="pageSelect" aria-label="选择页面"></select>
336
+ <button class="pagination-btn" id="nextPage" aria-label="下一页">下一页</button>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="notification" id="notification"></div>
341
+
342
+ <script>
343
+ var modelConfig = {
344
+ "grok-3": { RequestFrequency: 20, ExpirationTime: 7200000 },
345
+ "grok-3-deepsearch": { RequestFrequency: 10, ExpirationTime: 86400000 },
346
+ "grok-3-deepersearch": { RequestFrequency: 3, ExpirationTime: 86400000 },
347
+ "grok-3-reasoning": { RequestFrequency: 10, ExpirationTime: 86400000 },
348
+ "grok-4": { RequestFrequency: 20, ExpirationTime: 7200000 }
349
+ };
350
+
351
+ let tokenMap = {};
352
+ let batchDeleteMode = false;
353
+ let lastUpdateTime = 0;
354
+ let currentPage = 1;
355
+ const itemsPerPage = 30;
356
+
357
+ function getProgressColor(percentage, isValid) {
358
+ if (!isValid) return '#94A3B8';
359
+ if (percentage > 70) return 'var(--progress-fill-danger)';
360
+ if (percentage > 30) return 'var(--progress-fill-warning)';
361
+ return 'var(--progress-fill-success)';
362
+ }
363
+
364
+ function calculateModelRemaining() {
365
+ const modelRemaining = {};
366
+ Object.keys(modelConfig).forEach(modelName => {
367
+ var maxRequests = modelConfig[modelName].RequestFrequency;
368
+ modelRemaining[modelName] = 0;
369
+ Object.values(tokenMap).forEach(tokenData => {
370
+ const modelData = tokenData[modelName];
371
+ if(modelData){
372
+ if (!modelData.isSuper) {
373
+ modelRemaining[modelName] = 0;
374
+ }
375
+ else if (modelData.isValid) {
376
+ modelRemaining[modelName] += maxRequests - modelData.totalRequestCount;
377
+ }
378
+ }
379
+ });
380
+ });
381
+ return modelRemaining;
382
+ }
383
+
384
+ function updateTokenCounters() {
385
+ const totalTokensElement = document.getElementById('totalTokens');
386
+ if (totalTokensElement) {
387
+ totalTokensElement.textContent = Object.keys(tokenMap).length;
388
+ } else {
389
+ console.warn('Element with ID "totalTokens" not found.');
390
+ }
391
+
392
+ const modelRemaining = calculateModelRemaining();
393
+ const modelIds = ['grok-3', 'grok-3-deepsearch', 'grok-3-deepersearch', 'grok-3-reasoning','grok-4'];
394
+ modelIds.forEach(modelName => {
395
+ const countElement = document.getElementById(`${modelName}-count`);
396
+ if (countElement) {
397
+ countElement.textContent = modelRemaining[modelName] || 0;
398
+ } else {
399
+ console.warn(`Element with ID "${modelName}-count" not found.`);
400
+ }
401
+ });
402
+ }
403
+
404
+ async function updateExpiredTokenTimers() {
405
+ const currentTime = Date.now();
406
+ if (currentTime - lastUpdateTime < 5000) return;
407
+
408
+ let shouldRefresh = false;
409
+ Object.values(tokenMap).forEach(tokenData => {
410
+ Object.entries(tokenData).forEach(([modelName, modelData]) => {
411
+ if (!modelData.isValid) {
412
+ if(modelData.isSuper){
413
+ ExpirationTime = 3 * 60 * 60 * 1000;
414
+ }else{
415
+ ExpirationTime = modelConfig[modelName].ExpirationTime;
416
+ }
417
+ const recoveryTime = modelData.invalidatedTime + ExpirationTime;
418
+ if (recoveryTime <= currentTime) {
419
+ shouldRefresh = true;
420
+ }
421
+ }
422
+ });
423
+ });
424
+
425
+ if (shouldRefresh) {
426
+ lastUpdateTime = currentTime;
427
+ await fetchTokenMap();
428
+ showNotification('Token 状态已自动更新');
429
+ } else {
430
+ renderTokenDiff(tokenMap);
431
+ }
432
+ }
433
+
434
+ function getTooltipText(invalidatedTime, expirationTime) {
435
+ const currentTime = Date.now();
436
+ const recoveryTime = invalidatedTime + expirationTime;
437
+ const remainingTime = recoveryTime - currentTime;
438
+ if (remainingTime > 0) {
439
+ const minutes = Math.floor(remainingTime / 60000);
440
+ const seconds = Math.floor((remainingTime % 60000) / 1000);
441
+ return `${minutes}分${seconds}秒后恢复`;
442
+ }
443
+ return '已可恢复';
444
+ }
445
+
446
+ function createTokenCard(token, tokenData) {
447
+ const tokenCard = document.createElement('div');
448
+ tokenCard.className = 'token-card';
449
+ tokenCard.setAttribute('data-token', token);
450
+
451
+ const checkbox = document.createElement('input');
452
+ checkbox.type = 'checkbox';
453
+ checkbox.className = `token-checkbox ${batchDeleteMode ? 'show' : ''}`;
454
+ checkbox.value = token;
455
+
456
+ const tokenHeader = document.createElement('div');
457
+ tokenHeader.className = 'token-header';
458
+
459
+ const tokenTitle = document.createElement('div');
460
+ tokenTitle.className = 'token-title';
461
+ tokenTitle.textContent = token;
462
+ tokenTitle.title = token;
463
+
464
+ const tokenActions = document.createElement('div');
465
+ tokenActions.className = 'token-actions';
466
+
467
+ const copyBtn = document.createElement('button');
468
+ copyBtn.className = 'btn btn-secondary btn-base';
469
+ copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
470
+ copyBtn.setAttribute('aria-label', '复制 Token 到剪贴板');
471
+ copyBtn.addEventListener('click', async () => {
472
+ try {
473
+ await navigator.clipboard.writeText(token);
474
+ showNotification('Token 已复制到剪贴板');
475
+ } catch (err) {
476
+ showNotification('复制失败,请重试');
477
+ }
478
+ });
479
+
480
+ const deleteBtn = document.createElement('button');
481
+ deleteBtn.className = 'btn btn-danger btn-base';
482
+ deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M10 11v6M14 11v6M4 6v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6"/></svg>';
483
+ deleteBtn.setAttribute('aria-label', `删除 Token ${token}`);
484
+ deleteBtn.addEventListener('click', async () => {
485
+ if (confirm(`确认删除 token: ${token}?`)) {
486
+ try {
487
+ const baseUrl = document.getElementById('baseUrl').value;
488
+ const response = await fetch(`${baseUrl}/manager/api/delete`, {
489
+ method: 'POST',
490
+ headers: { 'Content-Type': 'application/json' },
491
+ body: JSON.stringify({ sso: token })
492
+ });
493
+ if (response.ok) {
494
+ await fetchTokenMap();
495
+ showNotification('Token 删除成功');
496
+ } else {
497
+ showNotification('删除 Token 失败');
498
+ }
499
+ } catch (error) {
500
+ showNotification('删除 Token 出错');
501
+ }
502
+ }
503
+ });
504
+
505
+ tokenActions.appendChild(copyBtn);
506
+ tokenActions.appendChild(deleteBtn);
507
+ tokenHeader.appendChild(tokenTitle);
508
+ tokenHeader.appendChild(tokenActions);
509
+
510
+ const modelList = document.createElement('div');
511
+ modelList.className = 'model-list';
512
+
513
+ Object.entries(modelConfig).forEach(([modelName, config]) => {
514
+ const modelItem = document.createElement('div');
515
+ modelItem.className = 'model-item';
516
+
517
+ const modelNameSpan = document.createElement('div');
518
+ modelNameSpan.className = 'model-name';
519
+ modelNameSpan.textContent = modelName;
520
+
521
+ const progressContainer = document.createElement('div');
522
+ progressContainer.className = 'progress-container';
523
+
524
+ const progressBar = document.createElement('div');
525
+ progressBar.className = 'progress-bar';
526
+ const progressFill = document.createElement('div');
527
+ progressFill.className = 'progress-fill';
528
+
529
+ const modelData = tokenData[modelName];
530
+ if(!modelData) return;
531
+ const isSuper = modelData.isSuper;
532
+ let requestCount = modelData.totalRequestCount;
533
+ let maxRequests = config.RequestFrequency;
534
+ if (isSuper) {
535
+ switch (modelName) {
536
+ case "grok-3":
537
+ maxRequests = 100;
538
+ break;
539
+ case "grok-3-deepsearch":
540
+ maxRequests = 30;
541
+ break;
542
+ case "grok-3-deepersearch":
543
+ maxRequests = 10;
544
+ break;
545
+ case "grok-3-reasoning":
546
+ maxRequests = 30;
547
+ break;
548
+ case "grok-4":
549
+ maxRequests = 20;
550
+ }
551
+ }
552
+ const percentage = Math.min((requestCount / maxRequests) * 100, 100);
553
+
554
+ progressFill.style.width = `${percentage}%`;
555
+ progressFill.style.backgroundColor = getProgressColor(percentage, modelData.isValid);
556
+ progressBar.appendChild(progressFill);
557
+
558
+ const progressText = document.createElement('div');
559
+ progressText.className = 'progress-text';
560
+ progressText.textContent = `${requestCount}/${maxRequests}`;
561
+
562
+ const status = document.createElement('div');
563
+ status.className = 'status';
564
+ if (!modelData.isValid) {
565
+ if(modelData.isSuper){
566
+ ExpirationTime = 3 * 60 * 60 * 1000;
567
+ }
568
+ else{
569
+ ExpirationTime = config.ExpirationTime;
570
+ }
571
+ status.classList.add('status-expired');
572
+ status.textContent = '失效';
573
+ status.setAttribute('data-invalidated-time', modelData.invalidatedTime);
574
+ status.setAttribute('data-expiration-time', ExpirationTime);
575
+ const tooltip = document.createElement('div');
576
+ tooltip.className = 'tooltip';
577
+ tooltip.textContent = getTooltipText(modelData.invalidatedTime, ExpirationTime);
578
+ status.appendChild(tooltip);
579
+ } else {
580
+ status.classList.add('status-active');
581
+ status.textContent = '活跃';
582
+ }
583
+
584
+ progressContainer.appendChild(progressBar);
585
+ progressContainer.appendChild(progressText);
586
+
587
+ modelItem.appendChild(modelNameSpan);
588
+ modelItem.appendChild(progressContainer);
589
+ modelItem.appendChild(status);
590
+ modelList.appendChild(modelItem);
591
+ });
592
+
593
+ tokenCard.appendChild(checkbox);
594
+ tokenCard.appendChild(tokenHeader);
595
+ tokenCard.appendChild(modelList);
596
+ return tokenCard;
597
+ }
598
+
599
+ function updateTokenCard(token, tokenData) {
600
+ const tokenCard = document.querySelector(`[data-token="${token}"]`);
601
+ const modelItems = tokenCard.querySelectorAll('.model-item');
602
+ let index = 0;
603
+ Object.entries(modelConfig).forEach(([modelName, config]) => {
604
+ const modelItem = modelItems[index++];
605
+ const modelData = tokenData[modelName];
606
+ if(!modelData) return;
607
+ const isSuper = modelData.isSuper;
608
+ let requestCount = modelData.totalRequestCount;
609
+ let maxRequests = config.RequestFrequency;
610
+ if (isSuper) {
611
+ switch (modelName) {
612
+ case "grok-3":
613
+ maxRequests = 100;
614
+ break;
615
+ case "grok-3-deepsearch":
616
+ maxRequests = 30;
617
+ break;
618
+ case "grok-3-deepersearch":
619
+ maxRequests = 10;
620
+ break;
621
+ case "grok-3-reasoning":
622
+ maxRequests = 30;
623
+ break;
624
+ case "grok-4":
625
+ maxRequests = 20;
626
+ }
627
+ }
628
+ const percentage = Math.min((requestCount / maxRequests) * 100, 100);
629
+
630
+ const progressFill = modelItem.querySelector('.progress-fill');
631
+ progressFill.style.width = `${percentage}%`;
632
+ progressFill.style.backgroundColor = getProgressColor(percentage, modelData.isValid);
633
+
634
+ const progressText = modelItem.querySelector('.progress-text');
635
+ progressText.textContent = `${requestCount}/${maxRequests}`;
636
+
637
+ const status = modelItem.querySelector('.status');
638
+ status.className = 'status';
639
+ if (!modelData.isValid) {
640
+ if(modelData.isSuper){
641
+ ExpirationTime = 3 * 60 * 60 * 1000;
642
+ }
643
+ else{
644
+ ExpirationTime = config.ExpirationTime;
645
+ }
646
+ status.classList.add('status-expired');
647
+ status.textContent = '失效';
648
+ status.setAttribute('data-invalidated-time', modelData.invalidatedTime);
649
+ status.setAttribute('data-expiration-time', ExpirationTime);
650
+ const tooltip = status.querySelector('.tooltip') || document.createElement('div');
651
+ tooltip.className = 'tooltip';
652
+ tooltip.textContent = getTooltipText(modelData.invalidatedTime, ExpirationTime);
653
+ if (!status.contains(tooltip)) status.appendChild(tooltip);
654
+ } else {
655
+ status.classList.add('status-active');
656
+ status.textContent = '活跃';
657
+ const tooltip = status.querySelector('.tooltip');
658
+ if (tooltip) tooltip.remove();
659
+ }
660
+ });
661
+ const checkbox = tokenCard.querySelector('.token-checkbox');
662
+ checkbox.className = `token-checkbox ${batchDeleteMode ? 'show' : ''}`;
663
+ }
664
+
665
+ function renderTokenDiff(newTokenMap) {
666
+ const tokenGrid = document.getElementById('tokenGrid');
667
+ if (!tokenGrid) {
668
+ console.error('Token grid element not found.');
669
+ return;
670
+ }
671
+ const existingTokens = new Set(Array.from(tokenGrid.children).map(card => card.getAttribute('data-token')));
672
+ const filteredTokens = filterTokensArray(Object.entries(newTokenMap));
673
+ const totalItems = filteredTokens.length;
674
+ const totalPages = Math.ceil(totalItems / itemsPerPage);
675
+ currentPage = Math.min(currentPage, totalPages) || 1;
676
+
677
+ const startIndex = (currentPage - 1) * itemsPerPage;
678
+ const endIndex = Math.min(startIndex + itemsPerPage, totalItems);
679
+ const tokensToRender = filteredTokens.slice(startIndex, endIndex);
680
+
681
+ const newTokens = new Set(tokensToRender.map(([token]) => token));
682
+
683
+ existingTokens.forEach(token => {
684
+ if (!newTokens.has(token)) {
685
+ tokenGrid.querySelector(`[data-token="${token}"]`).remove();
686
+ }
687
+ });
688
+
689
+ tokensToRender.forEach(([token, tokenData]) => {
690
+ if (!existingTokens.has(token)) {
691
+ const tokenCard = createTokenCard(token, tokenData);
692
+ tokenGrid.appendChild(tokenCard);
693
+ } else {
694
+ updateTokenCard(token, tokenData);
695
+ }
696
+ });
697
+
698
+ updateTokenCounters();
699
+ renderPagination(totalPages);
700
+ }
701
+
702
+ function filterTokensArray(tokenEntries) {
703
+ const searchInput = document.getElementById('searchInput');
704
+ const statusFilter = document.getElementById('statusFilter');
705
+ if (!searchInput || !statusFilter) {
706
+ console.error('Search input or status filter element not found.');
707
+ return tokenEntries;
708
+ }
709
+ const searchTerm = searchInput.value.toLowerCase();
710
+ const statusFilterValue = statusFilter.value;
711
+
712
+ return tokenEntries.filter(([token, tokenData]) => {
713
+ const hasActive = Object.values(tokenData).some(data => data.isValid);
714
+ const hasExpired = Object.values(tokenData).some(data => !data.isValid);
715
+
716
+ let display = true;
717
+ if (!token.toLowerCase().includes(searchTerm)) display = false;
718
+ if (statusFilterValue === 'active' && !hasActive) display = false;
719
+ if (statusFilterValue === 'expired' && !hasExpired) display = false;
720
+
721
+ return display;
722
+ });
723
+ }
724
+
725
+ function renderPagination(totalPages) {
726
+ const pageSelect = document.getElementById('pageSelect');
727
+ if (!pageSelect) {
728
+ console.error('Page select element not found.');
729
+ return;
730
+ }
731
+ pageSelect.innerHTML = '';
732
+ for (let i = 1; i <= totalPages; i++) {
733
+ const option = document.createElement('option');
734
+ option.value = i;
735
+ option.textContent = `第 ${i} 页`;
736
+ if (i === currentPage) option.selected = true;
737
+ pageSelect.appendChild(option);
738
+ }
739
+
740
+ const prevPage = document.getElementById('prevPage');
741
+ const nextPage = document.getElementById('nextPage');
742
+ if (prevPage) prevPage.disabled = currentPage === 1;
743
+ if (nextPage) nextPage.disabled = currentPage === totalPages || totalPages === 0;
744
+ }
745
+
746
+ async function fetchTokenMap() {
747
+ console.log('开始获取 tokenMap');
748
+ try {
749
+ const baseUrlElement = document.getElementById('baseUrl');
750
+ if (!baseUrlElement) {
751
+ throw new Error('Base URL 元素未找到');
752
+ }
753
+ const baseUrl = baseUrlElement.value;
754
+ console.log('请求 URL:', `${baseUrl}/manager/api/get`);
755
+ const response = await fetch(`${baseUrl}/manager/api/get`);
756
+ if (!response.ok) {
757
+ const errorText = await response.text();
758
+ throw new Error(`获取 Token 失败: ${response.status} - ${errorText}`);
759
+ }
760
+ const data = await response.json();
761
+ console.log('获取到的数据:', data);
762
+ if (!data || typeof data !== 'object') {
763
+ throw new Error('返回的数据不是有效的 Token Map');
764
+ }
765
+ tokenMap = data;
766
+ renderTokenDiff(tokenMap);
767
+ console.log('tokenMap 更新成功');
768
+ } catch (error) {
769
+ console.error('获取 Token 出错:', error);
770
+ showNotification(`获取 Token 出错: ${error.message}`);
771
+ }
772
+ }
773
+
774
+ document.addEventListener('DOMContentLoaded', () => {
775
+ const baseUrlInput = document.getElementById('baseUrl');
776
+ if (baseUrlInput) {
777
+ baseUrlInput.value = window.location.origin;
778
+ } else {
779
+ console.error('Base URL input element not found.');
780
+ }
781
+
782
+ const apiKeyInput = document.getElementById('apiKey');
783
+ if (apiKeyInput) {
784
+ const savedApiKey = localStorage.getItem('apiKey');
785
+ apiKeyInput.value = savedApiKey || 'sk-1234567';
786
+ apiKeyInput.addEventListener('change', () => {
787
+ const newApiKey = apiKeyInput.value.trim();
788
+ if (newApiKey) {
789
+ localStorage.setItem('apiKey', newApiKey);
790
+ showNotification(`API Key 已更新为: ${newApiKey}`);
791
+ } else {
792
+ apiKeyInput.value = localStorage.getItem('apiKey') || 'sk-1234567';
793
+ showNotification('API Key 不能为空,已恢复为上次保存的值');
794
+ }
795
+ });
796
+ } else {
797
+ console.error('API Key input element not found.');
798
+ }
799
+
800
+ const copyBaseUrlBtn = document.getElementById('copyBaseUrlBtn');
801
+ if (copyBaseUrlBtn) {
802
+ copyBaseUrlBtn.addEventListener('click', async () => {
803
+ try {
804
+ await navigator.clipboard.writeText(baseUrlInput.value);
805
+ showNotification('Base URL 已复制到剪贴板');
806
+ } catch (err) {
807
+ showNotification('复制失败,请重试');
808
+ }
809
+ });
810
+ }
811
+
812
+ const copyApiKeyBtn = document.getElementById('copyApiKeyBtn');
813
+ if (copyApiKeyBtn) {
814
+ copyApiKeyBtn.addEventListener('click', async () => {
815
+ try {
816
+ await navigator.clipboard.writeText(apiKeyInput.value);
817
+ showNotification('API Key 已复制到剪贴板');
818
+ } catch (err) {
819
+ showNotification('复制失败,请重试');
820
+ }
821
+ });
822
+ }
823
+
824
+ const addSingleTokenBtn = document.getElementById('addSingleTokenBtn');
825
+ if (addSingleTokenBtn) {
826
+ addSingleTokenBtn.addEventListener('click', async () => {
827
+ const tokenInput = document.getElementById('singleTokenInput');
828
+ const tokenText = tokenInput.value.trim();
829
+ if (tokenText) {
830
+ try {
831
+ const baseUrl = document.getElementById('baseUrl').value;
832
+ const response = await fetch(`${baseUrl}/manager/api/add`, {
833
+ method: 'POST',
834
+ headers: { 'Content-Type': 'application/json' },
835
+ body: JSON.stringify({ sso: tokenText })
836
+ });
837
+ if (response.ok) {
838
+ tokenInput.value = '';
839
+ await fetchTokenMap();
840
+ showNotification('Token 添加成功');
841
+ } else {
842
+ showNotification('添加 Token 失败');
843
+ }
844
+ } catch (error) {
845
+ showNotification('添加 Token 出错');
846
+ }
847
+ } else {
848
+ showNotification('请输入 Token');
849
+ }
850
+ });
851
+ }
852
+
853
+ const addBatchTokenBtn = document.getElementById('addBatchTokenBtn');
854
+ if (addBatchTokenBtn) {
855
+ addBatchTokenBtn.addEventListener('click', async () => {
856
+ const tokenInput = document.getElementById('batchTokenInput');
857
+ const tokenText = tokenInput.value.trim();
858
+ if (tokenText) {
859
+ const tokens = tokenText.split(',').map(t => t.trim()).filter(t => t.length > 0);
860
+ if (tokens.length === 0) {
861
+ showNotification('请输入至少一个有效的 Token');
862
+ return;
863
+ }
864
+ try {
865
+ const baseUrl = document.getElementById('baseUrl').value;
866
+ const successes = [];
867
+ const failures = [];
868
+ for (const token of tokens) {
869
+ const response = await fetch(`${baseUrl}/manager/api/add`, {
870
+ method: 'POST',
871
+ headers: { 'Content-Type': 'application/json' },
872
+ body: JSON.stringify({ sso: token })
873
+ });
874
+ if (response.ok) {
875
+ successes.push(token);
876
+ } else {
877
+ failures.push(token);
878
+ }
879
+ }
880
+ tokenInput.value = '';
881
+ await fetchTokenMap();
882
+ if (successes.length > 0 && failures.length === 0) {
883
+ showNotification(`成功添加 ${successes.length} 个 Token`);
884
+ } else if (successes.length > 0 && failures.length > 0) {
885
+ showNotification(`成功添加 ${successes.length} 个 Token,失败 ${failures.length} 个`);
886
+ } else {
887
+ showNotification('所有 Token 添加失败');
888
+ }
889
+ } catch (error) {
890
+ showNotification('添加 Token 时出错');
891
+ }
892
+ } else {
893
+ showNotification('请输入 Token');
894
+ }
895
+ });
896
+ }
897
+
898
+ const setCfBtn = document.getElementById('setCfBtn');
899
+ if (setCfBtn) {
900
+ setCfBtn.addEventListener('click', async () => {
901
+ const cfInput = document.getElementById('cfInput');
902
+ const newCf = cfInput.value.trim();
903
+ if (newCf) {
904
+ try {
905
+ const baseUrl = document.getElementById('baseUrl').value;
906
+ const response = await fetch(`${baseUrl}/manager/api/cf_clearance`, {
907
+ method: 'POST',
908
+ headers: { 'Content-Type': 'application/json' },
909
+ body: JSON.stringify({ cf_clearance: newCf })
910
+ });
911
+ if (response.ok) {
912
+ cfInput.value = '';
913
+ showNotification('CF Clearance 设置成功');
914
+ } else {
915
+ showNotification('设置 CF Clearance 失败');
916
+ }
917
+ } catch (error) {
918
+ showNotification('设置 CF Clearance 出错');
919
+ }
920
+ }
921
+ });
922
+ }
923
+
924
+ const testAvailabilityBtn = document.getElementById('testAvailabilityBtn');
925
+ if (testAvailabilityBtn) {
926
+ testAvailabilityBtn.addEventListener('click', async () => {
927
+ const selectedModel = document.getElementById('modelSelect').value;
928
+ const baseUrl = document.getElementById('baseUrl').value;
929
+ const apiKey = document.getElementById('apiKey').value.trim();
930
+ const apiUrl = `${baseUrl}/v1/chat/completions`;
931
+ try {
932
+ const response = await fetch(apiUrl, {
933
+ method: 'POST',
934
+ headers: {
935
+ 'Content-Type': 'application/json',
936
+ 'Authorization': `Bearer ${apiKey}`
937
+ },
938
+ body: JSON.stringify({
939
+ model: selectedModel,
940
+ messages: [{ role: 'user', content: 'Hello' }],
941
+ max_tokens: 10
942
+ })
943
+ });
944
+ if (response.ok) {
945
+ const data = await response.json();
946
+ if (data.choices && data.choices.length > 0) {
947
+ showNotification(`${selectedModel} 模型可用`);
948
+ } else {
949
+ showNotification(`${selectedModel} 模型不可用`);
950
+ }
951
+ } else {
952
+ const errorText = await response.text();
953
+ showNotification(`${selectedModel} 模型不可用(错误: ${response.status} - ${errorText})`);
954
+ }
955
+ } catch (error) {
956
+ console.error('检测模型出错:', error);
957
+ showNotification(`检测 ${selectedModel} 失败,请检查网络或接口`);
958
+ }
959
+ });
960
+ }
961
+
962
+ const batchDeleteTokens = document.getElementById('batchDeleteTokens');
963
+ if (batchDeleteTokens) {
964
+ batchDeleteTokens.addEventListener('click', async () => {
965
+ if (!batchDeleteMode) {
966
+ batchDeleteMode = true;
967
+ renderTokenDiff(tokenMap);
968
+ showNotification('请选择要删除的 Token');
969
+ } else {
970
+ const selectedTokens = Array.from(document.querySelectorAll('.token-checkbox:checked')).map(cb => cb.value);
971
+ if (selectedTokens.length === 0) {
972
+ showNotification('请至少选择一个 Token');
973
+ return;
974
+ }
975
+ if (confirm(`确认删除 ${selectedTokens.length} 个 Token?`)) {
976
+ try {
977
+ const baseUrl = document.getElementById('baseUrl').value;
978
+ const successes = [];
979
+ const failures = [];
980
+ for (const token of selectedTokens) {
981
+ const response = await fetch(`${baseUrl}/manager/api/delete`, {
982
+ method: 'POST',
983
+ headers: { 'Content-Type': 'application/json' },
984
+ body: JSON.stringify({ sso: token })
985
+ });
986
+ if (response.ok) {
987
+ successes.push(token);
988
+ } else {
989
+ failures.push(token);
990
+ }
991
+ }
992
+ batchDeleteMode = false;
993
+ await fetchTokenMap();
994
+ if (successes.length > 0 && failures.length === 0) {
995
+ showNotification(`成功删除 ${successes.length} 个 Token`);
996
+ } else if (successes.length > 0 && failures.length > 0) {
997
+ showNotification(`成功删除 ${successes.length} 个 Token,失败 ${failures.length} 个`);
998
+ } else {
999
+ showNotification('所有 Token 删除失败');
1000
+ }
1001
+ } catch (error) {
1002
+ showNotification('删除 Token 时出错');
1003
+ }
1004
+ }
1005
+ }
1006
+ });
1007
+ }
1008
+
1009
+ const searchInput = document.getElementById('searchInput');
1010
+ if (searchInput) searchInput.addEventListener('input', () => renderTokenDiff(tokenMap));
1011
+
1012
+ const statusFilter = document.getElementById('statusFilter');
1013
+ if (statusFilter) statusFilter.addEventListener('change', () => renderTokenDiff(tokenMap));
1014
+
1015
+ const refreshTokens = document.getElementById('refreshTokens');
1016
+ if (refreshTokens) {
1017
+ refreshTokens.addEventListener('click', async () => {
1018
+ await fetchTokenMap();
1019
+ showNotification('Token 列表已刷新');
1020
+ });
1021
+ } else {
1022
+ console.error('Refresh Tokens button not found.');
1023
+ }
1024
+
1025
+ const prevPage = document.getElementById('prevPage');
1026
+ if (prevPage) {
1027
+ prevPage.addEventListener('click', () => {
1028
+ if (currentPage > 1) {
1029
+ currentPage--;
1030
+ renderTokenDiff(tokenMap);
1031
+ }
1032
+ });
1033
+ }
1034
+
1035
+ const nextPage = document.getElementById('nextPage');
1036
+ if (nextPage) {
1037
+ nextPage.addEventListener('click', () => {
1038
+ const totalPages = Math.ceil(filterTokensArray(Object.entries(tokenMap)).length / itemsPerPage);
1039
+ if (currentPage < totalPages) {
1040
+ currentPage++;
1041
+ renderTokenDiff(tokenMap);
1042
+ }
1043
+ });
1044
+ }
1045
+
1046
+ const pageSelect = document.getElementById('pageSelect');
1047
+ if (pageSelect) {
1048
+ pageSelect.addEventListener('change', (e) => {
1049
+ currentPage = parseInt(e.target.value, 10);
1050
+ renderTokenDiff(tokenMap);
1051
+ });
1052
+ }
1053
+
1054
+ fetchTokenMap(); // 页面加载时获取 Token
1055
+
1056
+ let timer = setInterval(updateExpiredTokenTimers, 60000);
1057
+ document.addEventListener('visibilitychange', () => {
1058
+ if (document.hidden) {
1059
+ clearInterval(timer);
1060
+ } else {
1061
+ timer = setInterval(updateExpiredTokenTimers, 60000);
1062
+ }
1063
+ });
1064
+ });
1065
+
1066
+ function showNotification(message) {
1067
+ const notification = document.getElementById('notification');
1068
+ if (notification) {
1069
+ notification.textContent = message;
1070
+ notification.style.display = 'block';
1071
+ const duration = Math.max(2000, message.length * 100);
1072
+ setTimeout(() => {
1073
+ notification.style.display = 'none';
1074
+ }, duration);
1075
+ } else {
1076
+ console.warn('Notification element not found.');
1077
+ }
1078
+ }
1079
+ </script>
1080
+ </body>
1081
+ </html>