ZyphrZero commited on
Commit
4803675
·
1 Parent(s): b875088

🌟 feat(core): 功能:实现token池管理,以改进身份验证处理

Browse files

- 添加了 TokenPool 类,用于管理身份验证token,并实现负载均衡和容错。
- 引入了从文件加载token、将token标记为成功或失败以及对token执行健康检查的方法。
- 更新了 Settings 类,使其包含token管理配置,例如token文件路径和健康检查间隔。
- 增强了 chat_completions 函数,使其更有效地处理令牌重试和失败。
- 添加了新的 API 端点,用于检查token池的状态并手动触发健康检查。
- 更新了 Docker Compose 配置,删除了已弃用的环境变量。
- 创建了示例令牌配置文件,以指导用户设置令牌。
- 更新了requirements.txt,使其包含用于异步 HTTP 请求的 httpx

.env.example CHANGED
@@ -11,20 +11,32 @@ AUTH_TOKEN=sk-your-api-key
11
  # 跳过客户端认证(仅开发环境使用)
12
  SKIP_AUTH_TOKEN=false
13
 
14
- # Z.ai 备用访问令牌(当匿名模式失败时使用)
15
- # 注意:这是用于访问 Z.ai 服务的令牌,不是客户端认证密钥
16
- BACKUP_TOKEN=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  # ========== 服务器配置 ==========
19
  # 服务监听端口
20
  LISTEN_PORT=8080
21
 
22
- # 调试日志开关
23
  DEBUG_LOGGING=true
24
 
25
- # 匿名模式开关(推荐启用)
 
26
  # true: 自动从 Z.ai 获取临时访问令牌,避免对话历史共享
27
- # false: 使用固定令牌 BACKUP_TOKEN
28
  ANONYMOUS_MODE=true
29
 
30
  # Function Call 功能开关
 
11
  # 跳过客户端认证(仅开发环境使用)
12
  SKIP_AUTH_TOKEN=false
13
 
14
+ # ========== Token池配置 ==========
15
+ # Token失败阈值(失败多少次后标记为不可用)
16
+ TOKEN_FAILURE_THRESHOLD=3
17
+
18
+ # Token恢复超时时间(秒,失败token在此时间后重新尝试)
19
+ TOKEN_RECOVERY_TIMEOUT=1800
20
+
21
+ # Token健康检查间隔(秒,定期检查token状态)
22
+ TOKEN_HEALTH_CHECK_INTERVAL=300
23
+
24
+ # Z.ai 认证token配置(当匿名模式失败时使用)
25
+ #
26
+ # 使用独立的token文件配置
27
+ # 在项目根目录创建 tokens.txt 文件,每行一个token或逗号分隔
28
+ AUTH_TOKENS_FILE=tokens.txt
29
 
30
  # ========== 服务器配置 ==========
31
  # 服务监听端口
32
  LISTEN_PORT=8080
33
 
34
+ # 调试日志
35
  DEBUG_LOGGING=true
36
 
37
+ # 匿名用户模式
38
+ # false: 使用认证用户令牌
39
  # true: 自动从 Z.ai 获取临时访问令牌,避免对话历史共享
 
40
  ANONYMOUS_MODE=true
41
 
42
  # Function Call 功能开关
README.md CHANGED
@@ -1,30 +1,34 @@
1
  # Z.AI OpenAI API 代理服务
2
 
3
  ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
4
- ![Python: 3.8+](https://img.shields.io/badge/python-3.8+-green.svg)
5
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688.svg)
6
- ![Version: 1.2.0](https://img.shields.io/badge/version-1.2.0-brightgreen.svg)
7
 
8
- 轻量级 OpenAI API 兼容代理服务,通过 Claude Code Router 接入 Z.AI,支持 GLM-4.5 系列模型的完整功能。
 
 
9
 
10
  ## ✨ 核心特性
11
 
12
  - 🔌 **完全兼容 OpenAI API** - 无缝集成现有应用
13
  - 🤖 **Claude Code 支持** - 通过 Claude Code Router 接入 Claude Code (**CCR 工具请升级到 v1.0.47 以上**)
14
  - 🚀 **高性能流式响应** - Server-Sent Events (SSE) 支持
15
- - 🛠️ **增强工具调用** - 改进的 Function Call 实现
16
  - 🧠 **思考模式支持** - 智能处理模型推理过程
17
  - 🔍 **搜索模型集成** - GLM-4.5-Search 网络搜索能力
18
  - 🐳 **Docker 部署** - 一键容器化部署
19
  - 🛡️ **会话隔离** - 匿名模式保护隐私
20
  - 🔧 **灵活配置** - 环境变量灵活配置
21
  - 📊 **多模型映射** - 智能上游模型路由
 
 
22
 
23
  ## 🚀 快速开始
24
 
25
  ### 环境要求
26
 
27
- - Python 3.8+
28
  - pip 或 uv (推荐)
29
 
30
  ### 安装运行
@@ -44,7 +48,9 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
44
  python main.py
45
  ```
46
 
47
- 服务启动后访问:http://localhost:8080/docs
 
 
48
 
49
  ### 基础使用
50
 
@@ -145,18 +151,65 @@ for chunk in response:
145
  | `API_ENDPOINT` | `https://chat.z.ai/api/chat/completions` | 上游 API 地址 |
146
  | `LISTEN_PORT` | `8080` | 服务监听端口 |
147
  | `DEBUG_LOGGING` | `true` | 调试日志开关 |
148
- | `THINKING_PROCESSING` | `think` | 思考内容处理策略 |
149
- | `ANONYMOUS_MODE` | `true` | 匿名模式开关 |
150
  | `TOOL_SUPPORT` | `true` | Function Call 功能开关 |
151
  | `SKIP_AUTH_TOKEN` | `false` | 跳过认证令牌验证 |
152
  | `SCAN_LIMIT` | `200000` | 扫描限制 |
153
- | `BACKUP_TOKEN` | `eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...` | Z.ai 固定访问令牌 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- ### 思考内容处理策略
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- - `think` - 转换为 `<thinking>` 标签(OpenAI 兼容)
158
- - `strip` - 移除思考内容
159
- - `raw` - 保留原始格式
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  ## 🎯 使用场景
162
 
@@ -203,6 +256,12 @@ if response.choices[0].message.tool_calls:
203
  **Q: 如何获取 AUTH_TOKEN?**
204
  A: `AUTH_TOKEN` 为自己自定义的 api key,在环境变量中配置,需要保证客户端与服务端一致。
205
 
 
 
 
 
 
 
206
  **Q: 如何通过 Claude Code 使用本服务?**
207
 
208
  A: 创建 [zai.js](https://gist.githubusercontent.com/musistudio/b35402d6f9c95c64269c7666b8405348/raw/f108d66fa050f308387938f149a2b14a295d29e9/gistfile1.txt) 这个 ccr 插件放在`./.claude-code-router/plugins`目录下,配置 `./.claude-code-router/config.json` 指向本服务地址,使用 `AUTH_TOKEN` 进行认证。
@@ -287,32 +346,25 @@ A: 通过环境变量配置,推荐使用 `.env` 文件。
287
 
288
  要使用完整的多模态功能,需要获取正式的 Z.ai API Token:
289
 
290
- ### 方式 1: 通过 Z.ai 网站
291
-
292
- 1. 访问 [Z.ai 官网](https://chat.z.ai)
293
- 2. 注册账户并登录,进入 [Z.ai API Keys](https://z.ai/manage-apikey/apikey-list) 设置页面,在该页面设置 _**个人 API Token**_
294
- 3. 将 Token 放置在 `BACKUP_TOKEN` 环境变量中
295
-
296
- ### 方式 2: 浏览器开发者工具(临时方案)
297
-
298
  1. 打开 [Z.ai 聊天界面](https://chat.z.ai)
299
  2. 按 F12 打开开发者工具
300
  3. 切换到 "Application" 或 "存储" 标签
301
  4. 查看 Local Storage 中的认证 token
302
  5. 复制 token 值设置为环境变量
303
 
304
- > ⚠️ **注意**: 方式 2 获取的 token 可能有时效性,建议使用方式 1 获取长期有效的 API Token
305
- > ❗ **重要提示**: 多模态模型需要**官方 Z.ai API 非匿名 Token**,匿名 token 不支持多媒体处理。
306
 
307
  ## 🛠️ 技术栈
308
 
309
  | 组件 | 技术 | 版本 | 说明 |
310
  | --------------- | --------------------------------------------------------------------------------- | ------- | ------------------------------------------ |
311
- | **Web 框架** | [FastAPI](https://fastapi.tiangolo.com/) | 0.104.1 | 高性能异步 Web 框架,支持自动 API 文档生成 |
312
  | **ASGI 服务器** | [Granian](https://github.com/emmett-framework/granian) | 2.5.2 | 基于 Rust 的高性能 ASGI 服务器,支持热重载 |
313
- | **HTTP 客户端** | [Requests](https://requests.readthedocs.io/) | 2.32.5 | 简洁易用的 HTTP 库,用于上游 API 调用 |
314
  | **数据验证** | [Pydantic](https://pydantic.dev/) | 2.11.7 | 类型安全的数据验证与序列化 |
315
  | **配置管理** | [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) | 2.10.1 | 基于 Pydantic 的配置管理 |
 
 
316
 
317
  ## 🏗️ 技术架构
318
 
@@ -338,27 +390,27 @@ A: 通过环境变量配置,推荐使用 `.env` 文件。
338
 
339
  ```
340
  z.ai2api_python/
341
- ├── app/
342
- │ ├── core/
343
- │ │ ├── __init__.py
344
- │ │ ├── config.py # 配置管理
345
- │ │ ├── openai.py # OpenAI API 实现
346
- │ └── response_handlers.py # 响应处理器
347
- ├── models/
348
- │ ├── __init__.py
349
- │ └── schemas.py # Pydantic 模型定义
350
- ├── utils/
351
- ├── __init__.py
352
- │ ├── helpers.py # 辅助函数
353
- │ │ ├── tools.py # 增强工具调���处理
354
- │ │ └── sse_parser.py # SSE 流式解析器
355
- └── __init__.py
356
- ├── tests/ # 单元测试
357
- ├── deploy/ # Docker 部署配置
358
- ├── main.py # FastAPI 应用入口
359
- ├── requirements.txt # Python 依赖
360
- ├── .env.example # 环境变量示例
361
- └── README.md # 项目文档
362
  ```
363
 
364
  ## 🤝 贡献指南
 
1
  # Z.AI OpenAI API 代理服务
2
 
3
  ![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)
4
+ ![Python: 3.9-3.12](https://img.shields.io/badge/python-3.9--3.12-green.svg)
5
  ![FastAPI](https://img.shields.io/badge/framework-FastAPI-009688.svg)
6
+ ![Version: 0.1.0](https://img.shields.io/badge/version-0.1.0-brightgreen.svg)
7
 
8
+ > 🎯 **项目愿景**:提供完全兼容 OpenAI API Z.AI 代理服务,让用户无需修改现有代码即可接入 GLM-4.5 系列模型。
9
+
10
+ 轻量级、高性能的 OpenAI API 兼容代理服务,通过 Claude Code Router 接入 Z.AI,支持 GLM-4.5 系列模型的完整功能。
11
 
12
  ## ✨ 核心特性
13
 
14
  - 🔌 **完全兼容 OpenAI API** - 无缝集成现有应用
15
  - 🤖 **Claude Code 支持** - 通过 Claude Code Router 接入 Claude Code (**CCR 工具请升级到 v1.0.47 以上**)
16
  - 🚀 **高性能流式响应** - Server-Sent Events (SSE) 支持
17
+ - 🛠️ **增强工具调用** - 改进的 Function Call 实现,支持复杂工具链
18
  - 🧠 **思考模式支持** - 智能处理模型推理过程
19
  - 🔍 **搜索模型集成** - GLM-4.5-Search 网络搜索能力
20
  - 🐳 **Docker 部署** - 一键容器化部署
21
  - 🛡️ **会话隔离** - 匿名模式保护隐私
22
  - 🔧 **灵活配置** - 环境变量灵活配置
23
  - 📊 **多模型映射** - 智能上游模型路由
24
+ - 🔄 **Token 池管理** - 自动轮询、容错恢复、动态更新
25
+ - 🛡️ **错误处理** - 完善的异常捕获和重试机制
26
 
27
  ## 🚀 快速开始
28
 
29
  ### 环境要求
30
 
31
+ - Python 3.9-3.12
32
  - pip 或 uv (推荐)
33
 
34
  ### 安装运行
 
48
  python main.py
49
  ```
50
 
51
+ > 服务启动后访问接口文档:http://localhost:8080/docs
52
+ > 💡 **提示**:默认端口为 8080,可通过环境变量 `LISTEN_PORT` 修改
53
+ > ⚠️ **注意**:请勿将 `AUTH_TOKEN` 泄露给其他人,请使用 `AUTH_TOKENS` 配置多个认证令牌
54
 
55
  ### 基础使用
56
 
 
151
  | `API_ENDPOINT` | `https://chat.z.ai/api/chat/completions` | 上游 API 地址 |
152
  | `LISTEN_PORT` | `8080` | 服务监听端口 |
153
  | `DEBUG_LOGGING` | `true` | 调试日志开关 |
154
+ | `ANONYMOUS_MODE` | `true` | 匿名用户模式开关 |
 
155
  | `TOOL_SUPPORT` | `true` | Function Call 功能开关 |
156
  | `SKIP_AUTH_TOKEN` | `false` | 跳过认证令牌验证 |
157
  | `SCAN_LIMIT` | `200000` | 扫描限制 |
158
+ | `AUTH_TOKENS_FILE` | `tokens.txt` | 认证token文件路径 |
159
+
160
+ > 💡 详细配置请查看 `.env.example` 文件
161
+
162
+ ## 🔄 Token池机制
163
+
164
+ ### 功能特性
165
+
166
+ - **负载均衡**:轮询使用多个auth token,分散请求负载
167
+ - **自动容错**:token失败时自动切换到下一个可用token
168
+ - **健康监控**:基于Z.AI API的role字段精确验证token类型
169
+ - **自动恢复**:失败token在超时后自动重新尝试
170
+ - **动态管理**:支持运行时更新token池
171
+ - **智能去重**:自动检测和去除重复token
172
+ - **类型验证**:只接受认证用户token (role: "user"),拒绝匿名token (role: "guest")
173
+
174
+ ### Token配置方式
175
 
176
+ 创建 `tokens.txt` 文件,支持两种格式:
177
+
178
+ **格式1:每行一个token**
179
+ ```txt
180
+ # 认证token配置文件
181
+ # 支持注释行(以#开头)和空行
182
+ # 只添加认证用户token (role: "user")
183
+
184
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItMTIzIn0.signature1
185
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNDU2In0.signature2
186
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNzg5In0.signature3
187
+ ```
188
+
189
+ **格式2:逗号分隔**
190
+ ```txt
191
+ # 认证token配置文件
192
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItMTIzIn0.signature1,
193
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNDU2In0.signature2,
194
+ eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNzg5In0.signature3
195
+ ```
196
 
197
+ ## 监控API
198
+
199
+ ```bash
200
+ # 查看token池状态
201
+ curl http://localhost:8080/v1/token-pool/status
202
+
203
+ # 手动健康检查
204
+ curl -X POST http://localhost:8080/v1/token-pool/health-check
205
+
206
+ # 动态更新token池
207
+ curl -X POST http://localhost:8080/v1/token-pool/update \
208
+ -H "Content-Type: application/json" \
209
+ -d '["new_token1", "new_token2"]'
210
+ ```
211
+
212
+ 详细文档请参考:[Token池功能说明](TOKEN_POOL_README.md)
213
 
214
  ## 🎯 使用场景
215
 
 
256
  **Q: 如何获取 AUTH_TOKEN?**
257
  A: `AUTH_TOKEN` 为自己自定义的 api key,在环境变量中配置,需要保证客户端与服务端一致。
258
 
259
+ **Q: 遇到 "Illegal header value b'Bearer '" 错误怎么办?**
260
+ A: 这通常是因为 Token 获取失败导致的。请检查:
261
+ - 匿名模式是否正确配置(`ANONYMOUS_MODE=true`)
262
+ - Token 文件是否存在且格式正确(`tokens.txt`)
263
+ - 网络连接是否正常,能否访问 Z.AI API
264
+
265
  **Q: 如何通过 Claude Code 使用本服务?**
266
 
267
  A: 创建 [zai.js](https://gist.githubusercontent.com/musistudio/b35402d6f9c95c64269c7666b8405348/raw/f108d66fa050f308387938f149a2b14a295d29e9/gistfile1.txt) 这个 ccr 插件放在`./.claude-code-router/plugins`目录下,配置 `./.claude-code-router/config.json` 指向本服务地址,使用 `AUTH_TOKEN` 进行认证。
 
346
 
347
  要使用完整的多模态功能,需要获取正式的 Z.ai API Token:
348
 
 
 
 
 
 
 
 
 
349
  1. 打开 [Z.ai 聊天界面](https://chat.z.ai)
350
  2. 按 F12 打开开发者工具
351
  3. 切换到 "Application" 或 "存储" 标签
352
  4. 查看 Local Storage 中的认证 token
353
  5. 复制 token 值设置为环境变量
354
 
355
+ > **重要提示**: 获取的 token 可能有时效性,多模态模型需要**官方 Z.ai API 非匿名 Token**,匿名 token 不支持多媒体处理
 
356
 
357
  ## 🛠️ 技术栈
358
 
359
  | 组件 | 技术 | 版本 | 说明 |
360
  | --------------- | --------------------------------------------------------------------------------- | ------- | ------------------------------------------ |
361
+ | **Web 框架** | [FastAPI](https://fastapi.tiangolo.com/) | 0.116.1 | 高性能异步 Web 框架,支持自动 API 文档生成 |
362
  | **ASGI 服务器** | [Granian](https://github.com/emmett-framework/granian) | 2.5.2 | 基于 Rust 的高性能 ASGI 服务器,支持热重载 |
363
+ | **HTTP 客户端** | [HTTPX](https://www.python-httpx.org/) / [Requests](https://requests.readthedocs.io/) | 0.27.0 / 2.32.5 | 异步/同步 HTTP 库,用于上游 API 调用 |
364
  | **数据验证** | [Pydantic](https://pydantic.dev/) | 2.11.7 | 类型安全的数据验证与序列化 |
365
  | **配置管理** | [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) | 2.10.1 | 基于 Pydantic 的配置管理 |
366
+ | **日志系统** | [Loguru](https://loguru.readthedocs.io/) | 0.7.3 | 高性能结构化日志库 |
367
+ | **用户代理** | [Fake UserAgent](https://pypi.org/project/fake-useragent/) | 2.2.0 | 动态用户代理生成 |
368
 
369
  ## 🏗️ 技术架构
370
 
 
390
 
391
  ```
392
  z.ai2api_python/
393
+ ├── app/ # 主应用模块
394
+ │ ├── core/ # 核心模块
395
+ │ │ ├── config.py # 配置管理(Pydantic Settings)
396
+ │ │ ├── openai.py # OpenAI API 兼容层
397
+ │ │ └── zai_transformer.py # Z.AI 请求/响应转换器
398
+ ├── models/ # 数据模型
399
+ │ └── schemas.py # Pydantic 数据模型
400
+ └── utils/ # 工具模块
401
+ ├── logger.py # Loguru 日志系统
402
+ ├── reload_config.py # 热重载配置
403
+ ├── sse_tool_handler.py # SSE 工具调用处理器
404
+ └── token_pool.py # Token 池管理
405
+ ├── tests/ # 测试文件
406
+ ├── deploy/ # 部署配置
407
+ ├── Dockerfile # Docker 镜像构建
408
+ │ └── docker-compose.yml # 容器编排
409
+ ├── main.py # FastAPI 应用入口
410
+ ├── requirements.txt # 依赖清单
411
+ ├── pyproject.toml # 项目配置
412
+ ├── tokens.txt.example # Token 配置文件
413
+ └── .env.example # 环境变量示例
414
  ```
415
 
416
  ## 🤝 贡献指南
app/core/config.py CHANGED
@@ -2,8 +2,9 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
- from typing import Dict, Optional
6
  from pydantic_settings import BaseSettings
 
7
 
8
 
9
  class Settings(BaseSettings):
@@ -12,10 +13,102 @@ class Settings(BaseSettings):
12
  # API Configuration
13
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
14
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
15
- BACKUP_TOKEN: str = os.getenv(
16
- "BACKUP_TOKEN",
17
- "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMxNmJjYjQ4LWZmMmYtNGExNS04NTNkLWYyYTI5YjY3ZmYwZiIsImVtYWlsIjoiR3Vlc3QtMTc1NTg0ODU4ODc4OEBndWVzdC5jb20ifQ.PktllDySS3trlyuFpTeIZf-7hl8Qu1qYF3BxjgIul0BrNux2nX9hVzIjthLXKMWAf9V0qM8Vm_iyDqkjPGsaiQ",
18
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  # Model Configuration
21
  PRIMARY_MODEL: str = os.getenv("PRIMARY_MODEL", "GLM-4.5")
 
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
+ from typing import Dict, List, Optional
6
  from pydantic_settings import BaseSettings
7
+ from app.utils.logger import logger
8
 
9
 
10
  class Settings(BaseSettings):
 
13
  # API Configuration
14
  API_ENDPOINT: str = os.getenv("API_ENDPOINT", "https://chat.z.ai/api/chat/completions")
15
  AUTH_TOKEN: str = os.getenv("AUTH_TOKEN", "sk-your-api-key")
16
+
17
+ # 认证token文件路径
18
+ AUTH_TOKENS_FILE: str = os.getenv("AUTH_TOKENS_FILE", "tokens.txt")
19
+
20
+ # Token池配置
21
+ TOKEN_HEALTH_CHECK_INTERVAL: int = int(os.getenv("TOKEN_HEALTH_CHECK_INTERVAL", "300")) # 5分钟
22
+ TOKEN_FAILURE_THRESHOLD: int = int(os.getenv("TOKEN_FAILURE_THRESHOLD", "3")) # 失败3次后标记为不可用
23
+ TOKEN_RECOVERY_TIMEOUT: int = int(os.getenv("TOKEN_RECOVERY_TIMEOUT", "1800")) # 30分钟后重试失败的token
24
+
25
+ def _load_tokens_from_file(self, file_path: str) -> List[str]:
26
+ """
27
+ 从文件加载token列表
28
+
29
+ 支持两种格式:
30
+ 1. 每行一个token(原格式)
31
+ 2. 逗号分隔的token(新格式)
32
+
33
+ 处理规则:
34
+ - 跳过空行和注释行(以#开头)
35
+ - 自动检测并处理逗号分隔格式
36
+ - 去除空格和换行符
37
+ """
38
+ tokens = []
39
+ try:
40
+ if os.path.exists(file_path):
41
+ with open(file_path, 'r', encoding='utf-8') as f:
42
+ content = f.read().strip()
43
+
44
+ if not content:
45
+ logger.debug(f"📄 Token文件为空: {file_path}")
46
+ return tokens
47
+
48
+ # 检查是否包含逗号分隔格式
49
+ if ',' in content:
50
+ # 逗号分隔格式:将整个文件内容按逗号分割
51
+ logger.debug(f"📄 检测到逗号分隔格式: {file_path}")
52
+
53
+ # 移除注释行后再分割
54
+ lines = content.split('\n')
55
+ clean_content = []
56
+ for line in lines:
57
+ line = line.strip()
58
+ if line and not line.startswith('#'):
59
+ clean_content.append(line)
60
+
61
+ # 合并所有非注释内容,然后按逗号分割
62
+ merged_content = ' '.join(clean_content)
63
+ raw_tokens = merged_content.split(',')
64
+
65
+ for token in raw_tokens:
66
+ token = token.strip()
67
+ if token: # 跳过空token
68
+ tokens.append(token)
69
+ else:
70
+ # 每行一个token格式(原格式)
71
+ logger.debug(f"📄 使用每行一个token格式: {file_path}")
72
+ for line in content.split('\n'):
73
+ line = line.strip()
74
+ # 跳过空行和注释行
75
+ if line and not line.startswith('#'):
76
+ tokens.append(line)
77
+
78
+ logger.info(f"📄 从文件加载了 {len(tokens)} 个token: {file_path}")
79
+ else:
80
+ logger.debug(f"📄 Token文件不存在: {file_path}")
81
+ except Exception as e:
82
+ logger.error(f"❌ 读取token文件失败 {file_path}: {e}")
83
+ return tokens
84
+
85
+ @property
86
+ def auth_token_list(self) -> List[str]:
87
+ """
88
+ 解析认证token列表
89
+
90
+ 仅从AUTH_TOKENS_FILE指定的文件加载token
91
+ """
92
+ # 从文件加载token
93
+ tokens = self._load_tokens_from_file(self.AUTH_TOKENS_FILE)
94
+
95
+ # 去重,保持顺序
96
+ if tokens:
97
+ seen = set()
98
+ unique_tokens = []
99
+ for token in tokens:
100
+ if token not in seen:
101
+ unique_tokens.append(token)
102
+ seen.add(token)
103
+
104
+ # 记录去重信息
105
+ duplicate_count = len(tokens) - len(unique_tokens)
106
+ if duplicate_count > 0:
107
+ logger.warning(f"⚠️ 检测到 {duplicate_count} 个重复token,已自动去重")
108
+
109
+ return unique_tokens
110
+
111
+ return []
112
 
113
  # Model Configuration
114
  PRIMARY_MODEL: str = os.getenv("PRIMARY_MODEL", "GLM-4.5")
app/core/openai.py CHANGED
@@ -15,6 +15,7 @@ from app.models.schemas import OpenAIRequest, Message, ModelsResponse, Model
15
  from app.utils.logger import get_logger
16
  from app.core.zai_transformer import ZAITransformer, generate_uuid
17
  from app.utils.sse_tool_handler import SSEToolHandler
 
18
 
19
  logger = get_logger()
20
 
@@ -90,6 +91,7 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
90
  """流式响应生成器(包含重试机制)"""
91
  retry_count = 0
92
  last_error = None
 
93
 
94
  while retry_count <= settings.MAX_RETRIES:
95
  try:
@@ -102,12 +104,19 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
102
  )
103
  await asyncio.sleep(delay)
104
 
105
- # 在匿名模式下,重新获取令牌
106
- if settings.ANONYMOUS_MODE:
107
- logger.info("🔑 重新获取访客令牌用于重试...")
108
- new_token = await transformer.get_token()
109
- transformed["config"]["headers"]["Authorization"] = f"Bearer {new_token}"
110
- logger.debug(f" 新令牌: {new_token[:20] if new_token else 'None'}...")
 
 
 
 
 
 
 
111
 
112
  async with httpx.AsyncClient(timeout=60.0) as client:
113
  # 发送请求到上游
@@ -173,6 +182,10 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
173
  if retry_count > 0:
174
  logger.info(f"✨ 第 {retry_count} 次重试成功")
175
 
 
 
 
 
176
  # 初始化工具处理器(如果需要)
177
  has_tools = transformed["body"].get("tools") is not None
178
  tool_handler = None
@@ -443,6 +456,10 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
443
  import traceback
444
  logger.error(traceback.format_exc())
445
 
 
 
 
 
446
  # 检查是否还可以重试
447
  retry_count += 1
448
  last_error = str(e)
@@ -494,3 +511,113 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
494
 
495
  logger.error(f"错误堆栈: {traceback.format_exc()}")
496
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  from app.utils.logger import get_logger
16
  from app.core.zai_transformer import ZAITransformer, generate_uuid
17
  from app.utils.sse_tool_handler import SSEToolHandler
18
+ from app.utils.token_pool import get_token_pool
19
 
20
  logger = get_logger()
21
 
 
91
  """流式响应生成器(包含重试机制)"""
92
  retry_count = 0
93
  last_error = None
94
+ current_token = transformed.get("token", "") # 获取当前使用的token
95
 
96
  while retry_count <= settings.MAX_RETRIES:
97
  try:
 
104
  )
105
  await asyncio.sleep(delay)
106
 
107
+ # 标记前一个token失败(如果不是匿名模式)
108
+ if current_token and not settings.ANONYMOUS_MODE:
109
+ transformer.mark_token_failure(current_token, Exception(f"Retry {retry_count}: {last_error}"))
110
+
111
+ # 重新获取令牌
112
+ logger.info("🔑 重新获取令牌用于重试...")
113
+ new_token = await transformer.get_token()
114
+ if not new_token:
115
+ logger.error("❌ 重试时无法获取有效的认证令牌")
116
+ raise Exception("重试时无法获取有效的认证令牌")
117
+ transformed["config"]["headers"]["Authorization"] = f"Bearer {new_token}"
118
+ current_token = new_token
119
+ logger.debug(f" 新令牌: {new_token[:20] if new_token else 'None'}...")
120
 
121
  async with httpx.AsyncClient(timeout=60.0) as client:
122
  # 发送请求到上游
 
182
  if retry_count > 0:
183
  logger.info(f"✨ 第 {retry_count} 次重试成功")
184
 
185
+ # 标记token使用成功(如果不是匿名模式)
186
+ if current_token and not settings.ANONYMOUS_MODE:
187
+ transformer.mark_token_success(current_token)
188
+
189
  # 初始化工具处理器(如果需要)
190
  has_tools = transformed["body"].get("tools") is not None
191
  tool_handler = None
 
456
  import traceback
457
  logger.error(traceback.format_exc())
458
 
459
+ # 标记token失败(如果不是匿名模式)
460
+ if current_token and not settings.ANONYMOUS_MODE:
461
+ transformer.mark_token_failure(current_token, e)
462
+
463
  # 检查是否还可以重试
464
  retry_count += 1
465
  last_error = str(e)
 
511
 
512
  logger.error(f"错误堆栈: {traceback.format_exc()}")
513
  raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
514
+
515
+
516
+ @router.get("/v1/token-pool/status")
517
+ async def get_token_pool_status():
518
+ """获取token池状态信息"""
519
+ try:
520
+ token_pool = get_token_pool()
521
+ if not token_pool:
522
+ return {
523
+ "status": "disabled",
524
+ "message": "Token池未初始化,当前仅使用匿名模式",
525
+ "anonymous_mode": settings.ANONYMOUS_MODE,
526
+ "auth_tokens_file": settings.AUTH_TOKENS_FILE,
527
+ "auth_tokens_configured": len(settings.auth_token_list) > 0
528
+ }
529
+
530
+ pool_status = token_pool.get_pool_status()
531
+ return {
532
+ "status": "active",
533
+ "pool_info": pool_status,
534
+ "config": {
535
+ "anonymous_mode": settings.ANONYMOUS_MODE,
536
+ "failure_threshold": settings.TOKEN_FAILURE_THRESHOLD,
537
+ "recovery_timeout": settings.TOKEN_RECOVERY_TIMEOUT,
538
+ "health_check_interval": settings.TOKEN_HEALTH_CHECK_INTERVAL
539
+ }
540
+ }
541
+ except Exception as e:
542
+ logger.error(f"获取token池状态失败: {e}")
543
+ raise HTTPException(status_code=500, detail=f"Failed to get token pool status: {str(e)}")
544
+
545
+
546
+ @router.post("/v1/token-pool/health-check")
547
+ async def trigger_health_check():
548
+ """手动触发token池健康检查"""
549
+ try:
550
+ token_pool = get_token_pool()
551
+ if not token_pool:
552
+ raise HTTPException(status_code=404, detail="Token池未初始化")
553
+
554
+ # 记录开始时间
555
+ import time
556
+ start_time = time.time()
557
+
558
+ logger.info("🔍 API触发Token池健康检查...")
559
+ await token_pool.health_check_all()
560
+
561
+ # 计算耗时
562
+ duration = time.time() - start_time
563
+
564
+ pool_status = token_pool.get_pool_status()
565
+
566
+ # 统计健康检查结果 - 基于实际的健康状态
567
+ total_tokens = pool_status['total_tokens']
568
+ healthy_tokens = sum(1 for token_info in pool_status['tokens'] if token_info['is_healthy'])
569
+ unhealthy_tokens = total_tokens - healthy_tokens
570
+
571
+ # 构建响应
572
+ response = {
573
+ "status": "completed",
574
+ "message": f"健康检查已完成,耗时 {duration:.2f} 秒",
575
+ "summary": {
576
+ "total_tokens": total_tokens,
577
+ "healthy_tokens": healthy_tokens,
578
+ "unhealthy_tokens": unhealthy_tokens,
579
+ "health_rate": f"{(healthy_tokens/total_tokens*100):.1f}%" if total_tokens > 0 else "0%",
580
+ "duration_seconds": round(duration, 2)
581
+ },
582
+ "pool_info": pool_status
583
+ }
584
+
585
+ # 添加建议
586
+ if unhealthy_tokens > 0:
587
+ response["recommendations"] = []
588
+ if unhealthy_tokens == total_tokens:
589
+ response["recommendations"].append("所有token都不健康,请检查token配置和网络连接")
590
+ else:
591
+ response["recommendations"].append(f"有 {unhealthy_tokens} 个token不健康,建议检查这些token的有效性")
592
+
593
+ logger.info(f"✅ API健康检查完成: {healthy_tokens}/{total_tokens} 个token健康")
594
+ return response
595
+ except Exception as e:
596
+ logger.error(f"健康检查失败: {e}")
597
+ raise HTTPException(status_code=500, detail=f"Health check failed: {str(e)}")
598
+
599
+
600
+ @router.post("/v1/token-pool/update")
601
+ async def update_token_pool(tokens: List[str]):
602
+ """动态更新token池"""
603
+ try:
604
+ from app.utils.token_pool import update_token_pool
605
+
606
+ # 过滤空token
607
+ valid_tokens = [token.strip() for token in tokens if token.strip()]
608
+ if not valid_tokens:
609
+ raise HTTPException(status_code=400, detail="至少需要提供一个有效的token")
610
+
611
+ update_token_pool(valid_tokens)
612
+
613
+ token_pool = get_token_pool()
614
+ pool_status = token_pool.get_pool_status() if token_pool else None
615
+
616
+ return {
617
+ "status": "updated",
618
+ "message": f"Token池已更新,共 {len(valid_tokens)} 个token",
619
+ "pool_info": pool_status
620
+ }
621
+ except Exception as e:
622
+ logger.error(f"更新token池失败: {e}")
623
+ raise HTTPException(status_code=500, detail=f"Failed to update token pool: {str(e)}")
app/core/zai_transformer.py CHANGED
@@ -14,6 +14,7 @@ from fake_useragent import UserAgent
14
 
15
  from app.core.config import settings
16
  from app.utils.logger import get_logger
 
17
 
18
  logger = get_logger()
19
 
@@ -116,9 +117,17 @@ def get_auth_token_sync() -> str:
116
  except Exception as e:
117
  logger.warning(f"获取访客令牌失败: {e}")
118
 
119
- # 使用备份令牌
120
- logger.debug("使用备份令牌")
121
- return settings.BACKUP_TOKEN
 
 
 
 
 
 
 
 
122
 
123
 
124
  class ZAITransformer:
@@ -156,9 +165,29 @@ class ZAITransformer:
156
  except Exception as e:
157
  logger.warning(f"异步获取访客令牌失败: {e}")
158
 
159
- # 使用备份令牌
160
- logger.debug("使用备份令牌")
161
- return settings.BACKUP_TOKEN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  async def transform_request_in(self, request: Dict[str, Any]) -> Dict[str, Any]:
164
  """
@@ -171,6 +200,11 @@ class ZAITransformer:
171
  token = await self.get_token()
172
  logger.debug(f" 使用令牌: {token[:20] if token else 'None'}...")
173
 
 
 
 
 
 
174
  # 确定请求的模型特性
175
  requested_model = request.get("model", settings.PRIMARY_MODEL)
176
  is_thinking = requested_model == settings.THINKING_MODEL or request.get("reasoning", False)
@@ -308,8 +342,8 @@ class ZAITransformer:
308
  logger.debug(f" 目标URL: {config['url']}")
309
  logger.debug(f" 请求头数量: {len(config['headers'])}")
310
  logger.debug(f" 消息数: {len(body['messages'])}, 工具数: {len(body.get('tools', [])) if body.get('tools') else 0}")
311
-
312
- return {"body": body, "config": config}
313
 
314
  async def transform_response_out(
315
  self, response_stream: Generator, context: Dict[str, Any]
 
14
 
15
  from app.core.config import settings
16
  from app.utils.logger import get_logger
17
+ from app.utils.token_pool import get_token_pool, initialize_token_pool
18
 
19
  logger = get_logger()
20
 
 
117
  except Exception as e:
118
  logger.warning(f"获取访客令牌失败: {e}")
119
 
120
+ # 使用token池获取备份令牌
121
+ token_pool = get_token_pool()
122
+ if token_pool:
123
+ token = token_pool.get_next_token()
124
+ if token:
125
+ logger.debug(f"从token池获取令牌: {token[:20]}...")
126
+ return token
127
+
128
+ # 没有可用的token
129
+ logger.warning("⚠️ 没有可用的备份token")
130
+ return ""
131
 
132
 
133
  class ZAITransformer:
 
165
  except Exception as e:
166
  logger.warning(f"异步获取访客令牌失败: {e}")
167
 
168
+ # 使用token池获取备份令牌
169
+ token_pool = get_token_pool()
170
+ if token_pool:
171
+ token = token_pool.get_next_token()
172
+ if token:
173
+ logger.debug(f"从token池获取令牌: {token[:20]}...")
174
+ return token
175
+
176
+ # 没有可用的token
177
+ logger.warning("⚠️ 没有可用的备份token")
178
+ return ""
179
+
180
+ def mark_token_success(self, token: str):
181
+ """标记token使用成功"""
182
+ token_pool = get_token_pool()
183
+ if token_pool:
184
+ token_pool.mark_token_success(token)
185
+
186
+ def mark_token_failure(self, token: str, error: Exception = None):
187
+ """标记token使用失败"""
188
+ token_pool = get_token_pool()
189
+ if token_pool:
190
+ token_pool.mark_token_failure(token, error)
191
 
192
  async def transform_request_in(self, request: Dict[str, Any]) -> Dict[str, Any]:
193
  """
 
200
  token = await self.get_token()
201
  logger.debug(f" 使用令牌: {token[:20] if token else 'None'}...")
202
 
203
+ # 检查token是否有效
204
+ if not token:
205
+ logger.error("❌ 无法获取有效的认证令牌")
206
+ raise Exception("无法获取有效的认证令牌,请检查匿名模式配置或token池配置")
207
+
208
  # 确定请求的模型特性
209
  requested_model = request.get("model", settings.PRIMARY_MODEL)
210
  is_thinking = requested_model == settings.THINKING_MODEL or request.get("reasoning", False)
 
342
  logger.debug(f" 目标URL: {config['url']}")
343
  logger.debug(f" 请求头数量: {len(config['headers'])}")
344
  logger.debug(f" 消息数: {len(body['messages'])}, 工具数: {len(body.get('tools', [])) if body.get('tools') else 0}")
345
+
346
+ return {"body": body, "config": config, "token": token}
347
 
348
  async def transform_response_out(
349
  self, response_stream: Generator, context: Dict[str, Any]
app/utils/token_pool.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Token池管理器
6
+ 实现AUTH_TOKEN的轮询机制,提供负载均衡和容错功能
7
+ """
8
+
9
+ import asyncio
10
+ import time
11
+ from typing import Dict, List, Optional, Tuple
12
+ from dataclasses import dataclass, field
13
+ from threading import Lock
14
+ import httpx
15
+ import requests
16
+
17
+ from app.utils.logger import logger
18
+
19
+
20
+ @dataclass
21
+ class TokenStatus:
22
+ """Token状态信息"""
23
+ token: str
24
+ is_available: bool = True
25
+ failure_count: int = 0
26
+ last_failure_time: float = 0.0
27
+ last_success_time: float = 0.0
28
+ total_requests: int = 0
29
+ successful_requests: int = 0
30
+ token_type: str = "unknown" # "user", "guest", "unknown"
31
+
32
+ @property
33
+ def success_rate(self) -> float:
34
+ """成功率"""
35
+ if self.total_requests == 0:
36
+ return 1.0
37
+ return self.successful_requests / self.total_requests
38
+
39
+ @property
40
+ def is_healthy(self) -> bool:
41
+ """
42
+ 是否健康
43
+
44
+ 健康的定义:
45
+ 1. 必须是认证用户token (token_type = "user")
46
+ 2. 当前可用 (is_available = True)
47
+ 3. 成功率 >= 50% 或者总请求数 <= 3(新token容错)
48
+
49
+ 注意:guest token不应该在AUTH_TOKENS中
50
+ """
51
+ # guest token永远不健康
52
+ if self.token_type == "guest":
53
+ return False
54
+
55
+ # 未知类型token不健康
56
+ if self.token_type != "user":
57
+ return False
58
+
59
+ # 不可用的token不健康
60
+ if not self.is_available:
61
+ return False
62
+
63
+ # 对于认证用户token,基于成功率判断
64
+ # 新token或请求数很少时,给予容错
65
+ if self.total_requests <= 3:
66
+ return self.failure_count == 0
67
+
68
+ # 基于成功率判断健康状态
69
+ return self.success_rate >= 0.5
70
+
71
+
72
+ class TokenPool:
73
+ """Token池管理器"""
74
+
75
+ def __init__(self, tokens: List[str], failure_threshold: int = 3, recovery_timeout: int = 1800):
76
+ """
77
+ 初始化Token池
78
+
79
+ Args:
80
+ tokens: token列表
81
+ failure_threshold: 失败阈值,超过此次数将标记为不可用
82
+ recovery_timeout: 恢复超时时间(秒),失败token在此时间后重新尝试
83
+ """
84
+ self.failure_threshold = failure_threshold
85
+ self.recovery_timeout = recovery_timeout
86
+ self._lock = Lock()
87
+ self._current_index = 0
88
+
89
+ # 初始化token状态
90
+ self.token_statuses: Dict[str, TokenStatus] = {}
91
+ original_count = len(tokens)
92
+ unique_tokens = []
93
+
94
+ # 去重处理
95
+ for token in tokens:
96
+ if token and token not in self.token_statuses: # 过滤空token和重复token
97
+ self.token_statuses[token] = TokenStatus(token=token)
98
+ unique_tokens.append(token)
99
+
100
+ duplicate_count = original_count - len(unique_tokens)
101
+ if duplicate_count > 0:
102
+ logger.warning(f"⚠️ 检测到 {duplicate_count} 个重复token,已自动去重")
103
+
104
+ if not self.token_statuses:
105
+ logger.warning("⚠️ Token池为空,将依赖匿名模式")
106
+ else:
107
+ logger.info(f"🔧 初始化Token池,共 {len(self.token_statuses)} 个token")
108
+
109
+ def get_next_token(self) -> Optional[str]:
110
+ """
111
+ 获取下一个可用的token(轮询算法)
112
+
113
+ Returns:
114
+ 可用的token,如果没有可用token则返回None
115
+ """
116
+ with self._lock:
117
+ if not self.token_statuses:
118
+ return None
119
+
120
+ available_tokens = self._get_available_tokens()
121
+ if not available_tokens:
122
+ # 尝试恢复过期的失败token
123
+ self._try_recover_failed_tokens()
124
+ available_tokens = self._get_available_tokens()
125
+
126
+ if not available_tokens:
127
+ logger.warning("⚠️ 没有可用的token")
128
+ return None
129
+
130
+ # 轮询选择token
131
+ token = available_tokens[self._current_index % len(available_tokens)]
132
+ self._current_index = (self._current_index + 1) % len(available_tokens)
133
+
134
+ return token
135
+
136
+ def _get_available_tokens(self) -> List[str]:
137
+ """
138
+ 获取当前可用的认证用户token列表
139
+
140
+ 只返回满足以下条件的token:
141
+ 1. is_available = True (可用状态)
142
+ 2. token_type = "user" (认证用户token)
143
+
144
+ 这确保轮询机制只会选择有效的认证用户token,跳过匿名用户token
145
+ """
146
+ available_user_tokens = [
147
+ status.token for status in self.token_statuses.values()
148
+ if status.is_available and status.token_type == "user"
149
+ ]
150
+
151
+ # 如果没有可用的认证用户token
152
+ if not available_user_tokens and self.token_statuses:
153
+ guest_tokens = [
154
+ status.token for status in self.token_statuses.values()
155
+ if status.token_type == "guest"
156
+ ]
157
+ if guest_tokens:
158
+ logger.warning(f"⚠️ 检测到 {len(guest_tokens)} 个匿名用户token,轮询机制将跳过这些token")
159
+
160
+ return available_user_tokens
161
+
162
+ def _try_recover_failed_tokens(self):
163
+ """尝试恢复失败的token"""
164
+ current_time = time.time()
165
+ recovered_count = 0
166
+
167
+ for status in self.token_statuses.values():
168
+ if (not status.is_available and
169
+ current_time - status.last_failure_time > self.recovery_timeout):
170
+ status.is_available = True
171
+ status.failure_count = 0
172
+ recovered_count += 1
173
+ logger.info(f"🔄 恢复失败token: {status.token[:20]}...")
174
+
175
+ if recovered_count > 0:
176
+ logger.info(f"✅ 恢复了 {recovered_count} 个失败的token")
177
+
178
+ def mark_token_success(self, token: str):
179
+ """标记token使用成功"""
180
+ with self._lock:
181
+ if token in self.token_statuses:
182
+ status = self.token_statuses[token]
183
+ status.total_requests += 1
184
+ status.successful_requests += 1
185
+ status.last_success_time = time.time()
186
+ status.failure_count = 0 # 重置失败计数
187
+
188
+ if not status.is_available:
189
+ status.is_available = True
190
+ logger.info(f"✅ Token恢复可用: {token[:20]}...")
191
+
192
+ def mark_token_failure(self, token: str, error: Exception = None):
193
+ """标记token使用失败"""
194
+ with self._lock:
195
+ if token in self.token_statuses:
196
+ status = self.token_statuses[token]
197
+ status.total_requests += 1
198
+ status.failure_count += 1
199
+ status.last_failure_time = time.time()
200
+
201
+ if status.failure_count >= self.failure_threshold:
202
+ status.is_available = False
203
+ logger.warning(f"🚫 Token已禁用: {token[:20]}... (失败 {status.failure_count} 次)")
204
+
205
+ def get_pool_status(self) -> Dict:
206
+ """获取token池状态信息"""
207
+ with self._lock:
208
+ available_count = len(self._get_available_tokens())
209
+ total_count = len(self.token_statuses)
210
+
211
+ # 统计健康token数量
212
+ healthy_count = sum(1 for status in self.token_statuses.values() if status.is_healthy)
213
+
214
+ status_info = {
215
+ "total_tokens": total_count,
216
+ "available_tokens": available_count,
217
+ "unavailable_tokens": total_count - available_count,
218
+ "healthy_tokens": healthy_count,
219
+ "unhealthy_tokens": total_count - healthy_count,
220
+ "current_index": self._current_index,
221
+ "tokens": []
222
+ }
223
+
224
+ for token, status in self.token_statuses.items():
225
+ status_info["tokens"].append({
226
+ "token": f"{token[:10]}...{token[-10:]}",
227
+ "token_type": status.token_type,
228
+ "is_available": status.is_available,
229
+ "failure_count": status.failure_count,
230
+ "success_count": status.successful_requests,
231
+ "success_rate": f"{status.success_rate:.2%}",
232
+ "total_requests": status.total_requests,
233
+ "is_healthy": status.is_healthy,
234
+ "last_failure_time": status.last_failure_time,
235
+ "last_success_time": status.last_success_time
236
+ })
237
+
238
+ return status_info
239
+
240
+ def update_tokens(self, new_tokens: List[str]):
241
+ """动态更新token列表"""
242
+ with self._lock:
243
+ # 保留现有token的状态信息
244
+ old_statuses = self.token_statuses.copy()
245
+ self.token_statuses.clear()
246
+
247
+ original_count = len(new_tokens)
248
+ unique_tokens = []
249
+
250
+ # 去重并添加新token,保留已存在token的状态
251
+ for token in new_tokens:
252
+ if token and token not in self.token_statuses: # 过滤空token和重复token
253
+ if token in old_statuses:
254
+ self.token_statuses[token] = old_statuses[token]
255
+ else:
256
+ self.token_statuses[token] = TokenStatus(token=token)
257
+ unique_tokens.append(token)
258
+
259
+ # 记录去重信息
260
+ duplicate_count = original_count - len(unique_tokens)
261
+ if duplicate_count > 0:
262
+ logger.warning(f"⚠️ 更新时检测到 {duplicate_count} 个重复token,已自动去重")
263
+
264
+ # 重置索引
265
+ self._current_index = 0
266
+
267
+ logger.info(f"🔄 更新Token池,共 {len(self.token_statuses)} 个token")
268
+
269
+ async def health_check_token(self, token: str, auth_url: str = "https://chat.z.ai/api/v1/auths/") -> bool:
270
+ """
271
+ 异步健康检查单个token
272
+
273
+ 使用Z.AI认证API验证token的有效性,通过检查响应内容判断token是否有效
274
+
275
+ Args:
276
+ token: 要检查的token
277
+ auth_url: 认证URL
278
+
279
+ Returns:
280
+ token是否健康
281
+ """
282
+ try:
283
+ # 构建完整的请求头,模拟真实浏览器请求
284
+ headers = {
285
+ "Accept": "*/*",
286
+ "Accept-Language": "zh-CN,zh;q=0.9",
287
+ "Authorization": f"Bearer {token}",
288
+ "Connection": "keep-alive",
289
+ "Content-Type": "application/json",
290
+ "DNT": "1",
291
+ "Referer": "https://chat.z.ai/",
292
+ "Sec-Fetch-Dest": "empty",
293
+ "Sec-Fetch-Mode": "cors",
294
+ "Sec-Fetch-Site": "same-origin",
295
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
296
+ "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
297
+ "sec-ch-ua-mobile": "?0",
298
+ "sec-ch-ua-platform": "Windows"
299
+ }
300
+
301
+ async with httpx.AsyncClient(timeout=15.0) as client:
302
+ response = await client.get(auth_url, headers=headers)
303
+
304
+ # 验证token有效性并获取类型
305
+ token_type, is_healthy = self._validate_token_response(response)
306
+
307
+ # 更新token类型
308
+ if token in self.token_statuses:
309
+ self.token_statuses[token].token_type = token_type
310
+
311
+ if is_healthy:
312
+ self.mark_token_success(token)
313
+ else:
314
+ # 简化错误信息,只记录关键错误类型
315
+ if token_type == "guest":
316
+ error_msg = "匿名用户token"
317
+ elif response.status_code != 200:
318
+ error_msg = f"HTTP {response.status_code}"
319
+ else:
320
+ error_msg = "认证失败"
321
+
322
+ self.mark_token_failure(token, Exception(error_msg))
323
+
324
+ return is_healthy
325
+
326
+ except (httpx.TimeoutException, httpx.ConnectError, Exception) as e:
327
+ self.mark_token_failure(token, e)
328
+ return False
329
+
330
+ def _validate_token_response(self, response: httpx.Response) -> bool:
331
+ """
332
+ 基于Z.AI API响应中的role字段验证token类型
333
+
334
+ 验证规则:
335
+ - role: "user" = 认证用户token(有效,可用于AUTH_TOKENS)
336
+ - role: "guest" = 匿名用户token(无效,不应在AUTH_TOKENS中)
337
+ - 无role字段或其他值 = 无效token
338
+
339
+ Args:
340
+ response: HTTP响应对象
341
+
342
+ Returns:
343
+ token是否为有效的认证用户token
344
+ """
345
+ # 首先检查HTTP状态码
346
+ if response.status_code != 200:
347
+ return ("unknown", False)
348
+
349
+ try:
350
+ # 尝试解析JSON响应
351
+ response_data = response.json()
352
+
353
+ if not isinstance(response_data, dict):
354
+ return ("unknown", False)
355
+
356
+ # 检查是否包含错误信息
357
+ if "error" in response_data:
358
+ return ("unknown", False)
359
+
360
+ if "message" in response_data and "error" in response_data.get("message", "").lower():
361
+ return ("unknown", False)
362
+
363
+ # 核心验证:检查role字段
364
+ role = response_data.get("role")
365
+
366
+ if role == "user":
367
+ return ("user", True)
368
+ elif role == "guest":
369
+
370
+ if not hasattr(self, '_guest_token_warned'):
371
+ logger.warning("⚠️ 检测到匿名用户token,建议仅在AUTH_TOKENS中配置认证用户token")
372
+ self._guest_token_warned = True
373
+ return ("guest", False)
374
+ else:
375
+ return ("unknown", False)
376
+
377
+ except (ValueError, Exception):
378
+ return ("unknown", False)
379
+
380
+ async def health_check_all(self, auth_url: str = "https://chat.z.ai/api/v1/auths/"):
381
+ """异步健康检查所有token"""
382
+ if not self.token_statuses:
383
+ logger.warning("⚠️ Token池为空,跳过健康检查")
384
+ return
385
+
386
+ total_tokens = len(self.token_statuses)
387
+ logger.info(f"🔍 开始Token池健康检查... (共 {total_tokens} 个token)")
388
+
389
+ # 并发执行所有token的健康检查
390
+ tasks = []
391
+ token_list = list(self.token_statuses.keys())
392
+
393
+ for token in token_list:
394
+ task = self.health_check_token(token, auth_url)
395
+ tasks.append(task)
396
+
397
+ # 执行并收集结果
398
+ results = await asyncio.gather(*tasks, return_exceptions=True)
399
+
400
+ # 统计结果
401
+ healthy_count = 0
402
+ failed_count = 0
403
+ exception_count = 0
404
+
405
+ for i, result in enumerate(results):
406
+ if result is True:
407
+ healthy_count += 1
408
+ elif result is False:
409
+ failed_count += 1
410
+ else:
411
+ # 异常情况
412
+ exception_count += 1
413
+ token = token_list[i]
414
+ logger.error(f"💥 Token {token[:20]}... 健康检查异常: {result}")
415
+
416
+ health_rate = (healthy_count / total_tokens) * 100 if total_tokens > 0 else 0
417
+
418
+ if healthy_count == 0 and total_tokens > 0:
419
+ logger.warning(f"⚠️ 健康检查完成: 0/{total_tokens} 个token健康 - 请检查token配置")
420
+ elif failed_count > 0:
421
+ logger.warning(f"⚠️ 健康检查完成: {healthy_count}/{total_tokens} 个token健康 ({health_rate:.1f}%)")
422
+ else:
423
+ logger.info(f"✅ 健康检查完成: {healthy_count}/{total_tokens} 个token健康")
424
+
425
+ if exception_count > 0:
426
+ logger.error(f"💥 {exception_count} 个token检查异常")
427
+
428
+
429
+ # 全局token池实例
430
+ _token_pool: Optional[TokenPool] = None
431
+ _pool_lock = Lock()
432
+
433
+
434
+ def get_token_pool() -> Optional[TokenPool]:
435
+ """获取全局token池实例"""
436
+ return _token_pool
437
+
438
+
439
+ def initialize_token_pool(tokens: List[str], failure_threshold: int = 3, recovery_timeout: int = 1800) -> TokenPool:
440
+ """初始化全局token池"""
441
+ global _token_pool
442
+ with _pool_lock:
443
+ _token_pool = TokenPool(tokens, failure_threshold, recovery_timeout)
444
+ return _token_pool
445
+
446
+
447
+ def update_token_pool(tokens: List[str]):
448
+ """更新全局token池"""
449
+ global _token_pool
450
+ with _pool_lock:
451
+ if _token_pool:
452
+ _token_pool.update_tokens(tokens)
453
+ else:
454
+ _token_pool = TokenPool(tokens)
deploy/docker-compose.yml CHANGED
@@ -15,8 +15,6 @@ services:
15
  - SKIP_AUTH_TOKEN=false
16
  # Server Configurations
17
  - DEBUG_LOGGING=true
18
- # Feature Configuration
19
- - THINKING_PROCESSING=think
20
  - ANONYMOUS_MODE=true
21
  - TOOL_SUPPORT=true
22
  - SCAN_LIMIT=200000
 
15
  - SKIP_AUTH_TOKEN=false
16
  # Server Configurations
17
  - DEBUG_LOGGING=true
 
 
18
  - ANONYMOUS_MODE=true
19
  - TOOL_SUPPORT=true
20
  - SCAN_LIMIT=200000
main.py CHANGED
@@ -1,13 +1,15 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
 
4
- from fastapi import FastAPI, Request, Response
 
5
  from fastapi.middleware.cors import CORSMiddleware
6
 
7
  from app.core.config import settings
8
  from app.core import openai
9
  from app.utils.reload_config import RELOAD_CONFIG
10
  from app.utils.logger import setup_logger
 
11
 
12
  from granian import Granian
13
 
@@ -15,8 +17,24 @@ from granian import Granian
15
  # Setup logger
16
  logger = setup_logger(log_dir="logs", debug_mode=settings.DEBUG_LOGGING)
17
 
18
- # Create FastAPI app
19
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # Add CORS middleware
22
  app.add_middleware(
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
 
4
+ from contextlib import asynccontextmanager
5
+ from fastapi import FastAPI, Response
6
  from fastapi.middleware.cors import CORSMiddleware
7
 
8
  from app.core.config import settings
9
  from app.core import openai
10
  from app.utils.reload_config import RELOAD_CONFIG
11
  from app.utils.logger import setup_logger
12
+ from app.utils.token_pool import initialize_token_pool
13
 
14
  from granian import Granian
15
 
 
17
  # Setup logger
18
  logger = setup_logger(log_dir="logs", debug_mode=settings.DEBUG_LOGGING)
19
 
20
+
21
+ @asynccontextmanager
22
+ async def lifespan(app: FastAPI):
23
+ token_list = settings.auth_token_list
24
+ if token_list:
25
+ token_pool = initialize_token_pool(
26
+ tokens=token_list,
27
+ failure_threshold=settings.TOKEN_FAILURE_THRESHOLD,
28
+ recovery_timeout=settings.TOKEN_RECOVERY_TIMEOUT
29
+ )
30
+
31
+ yield
32
+
33
+ logger.info("🔄 应用正在关闭...")
34
+
35
+
36
+ # Create FastAPI app with lifespan
37
+ app = FastAPI(lifespan=lifespan)
38
 
39
  # Add CORS middleware
40
  app.add_middleware(
pyproject.toml CHANGED
@@ -33,6 +33,7 @@ dependencies = [
33
  "typing-inspection==0.4.1",
34
  "fake-useragent==2.2.0",
35
  "loguru==0.7.3",
 
36
  ]
37
 
38
  [project.scripts]
 
33
  "typing-inspection==0.4.1",
34
  "fake-useragent==2.2.0",
35
  "loguru==0.7.3",
36
+ "httpx==0.27.0"
37
  ]
38
 
39
  [project.scripts]
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  fastapi==0.116.1
2
  granian[reload]==2.5.2
3
  requests==2.32.5
 
4
  pydantic==2.11.7
5
  pydantic-settings==2.10.1
6
  pydantic-core==2.33.2
 
1
  fastapi==0.116.1
2
  granian[reload]==2.5.2
3
  requests==2.32.5
4
+ httpx==0.27.0
5
  pydantic==2.11.7
6
  pydantic-settings==2.10.1
7
  pydantic-core==2.33.2
tokens.txt.example ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 认证Token配置文件
2
+ #
3
+ # 说明:
4
+ # 1. 支持两种格式:每行一个token 或 逗号分隔的token
5
+ # 2. 只包含认证用户token (role: "user"),不要添加匿名用户token (role: "guest")
6
+ # 3. 系统会自动去重和验证token有效性
7
+ # 4. 修改此文件后无需重启服务,系统会自动重新加载
8
+ # 5. 自动跳过空格、换行符和空token
9
+ #
10
+ # 格式1:每行一个token
11
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItMTIzIn0.signature1
12
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNDU2In0.signature2
13
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNzg5In0.signature3
14
+ #
15
+ # 格式2:逗号分隔(推荐,更紧凑)
16
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItMTIzIn0.signature1,
17
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNDU2In0.signature2,
18
+ # eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InVzZXItNzg5In0.signature3
19
+
20
+ # 请在下方添加您的认证用户token(使用任一格式):
21
+