Spaces:
Sleeping
Sleeping
Upload 13 files
Browse files- .env +69 -0
- .env.example +11 -3
- .gitignore +3 -2
- Dockerfile +4 -0
- README.md +27 -20
- accounts.json +223 -0
- docker-compose.yml +10 -0
- main.py +350 -993
- requirements.txt +3 -1
.env
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================
|
| 2 |
+
# Gemini Business2API 配置示例
|
| 3 |
+
# ============================================
|
| 4 |
+
|
| 5 |
+
# 代理设置(可选)
|
| 6 |
+
# PROXY=http://127.0.0.1:7890
|
| 7 |
+
|
| 8 |
+
# API 访问密钥(可选,用于保护 API 端点)
|
| 9 |
+
# API_KEY=your-secret-api-key-here
|
| 10 |
+
|
| 11 |
+
# 路径前缀(可选,用于隐藏端点路径)
|
| 12 |
+
# 如果设置了,端点将为: /{PATH_PREFIX}/, /{PATH_PREFIX}/v1/
|
| 13 |
+
# 如果未设置,端点将为: /, /v1/
|
| 14 |
+
PATH_PREFIX=your-random-path-prefix
|
| 15 |
+
# 管理员密钥(必需,用于登录管理面板)
|
| 16 |
+
ADMIN_KEY=your-admin-secret-key
|
| 17 |
+
|
| 18 |
+
# Session加密密钥(可选,自动生成)
|
| 19 |
+
# SESSION_SECRET_KEY=your-session-secret-key
|
| 20 |
+
|
| 21 |
+
# Session过期时间(可选,单位:小时,默认24小时)
|
| 22 |
+
# SESSION_EXPIRE_HOURS=24
|
| 23 |
+
|
| 24 |
+
# 服务器完整 URL(可选,用于反代公开日志和图片 URL)
|
| 25 |
+
# BASE_URL=https://your-domain.com
|
| 26 |
+
|
| 27 |
+
# ============================================
|
| 28 |
+
# 高级配置(可选,使用默认值)
|
| 29 |
+
# ============================================
|
| 30 |
+
|
| 31 |
+
# 新会话创建最多尝试账户数(默认:5)
|
| 32 |
+
# MAX_NEW_SESSION_TRIES=5
|
| 33 |
+
|
| 34 |
+
# 请求失败最多重试次数(默认:3)
|
| 35 |
+
# MAX_REQUEST_RETRIES=3
|
| 36 |
+
|
| 37 |
+
# 每次重试找账户的最大尝试次数(默认:5)
|
| 38 |
+
# MAX_ACCOUNT_SWITCH_TRIES=5
|
| 39 |
+
|
| 40 |
+
# 账户连续失败阈值(默认:3次)
|
| 41 |
+
# ACCOUNT_FAILURE_THRESHOLD=3
|
| 42 |
+
|
| 43 |
+
# 429限流错误冷却时间,单位秒(默认:600秒=10分钟)
|
| 44 |
+
# RATE_LIMIT_COOLDOWN_SECONDS=600
|
| 45 |
+
|
| 46 |
+
# 会话缓存过期时间,单位秒(默认:3600秒=1小时)
|
| 47 |
+
# SESSION_CACHE_TTL_SECONDS=3600
|
| 48 |
+
|
| 49 |
+
# ============================================
|
| 50 |
+
# 公开展示配置(可选)
|
| 51 |
+
# ============================================
|
| 52 |
+
# Logo URL(公开,为空则不显示)
|
| 53 |
+
# LOGO_URL=https://your-domain.com/logo.png
|
| 54 |
+
|
| 55 |
+
# 开始对话链接(公开,为空则不显示)
|
| 56 |
+
# CHAT_URL=https://your-chat-app.com
|
| 57 |
+
|
| 58 |
+
# 模型名称(公开,默认:gemini-business)
|
| 59 |
+
# MODEL_NAME=gemini-business
|
| 60 |
+
|
| 61 |
+
# ============================================
|
| 62 |
+
# 多账户配置(必需)
|
| 63 |
+
# ============================================
|
| 64 |
+
# 使用 JSON 数组格式配置多个 Gemini 账户
|
| 65 |
+
# 必需字段:secure_c_ses, csesidx, config_id
|
| 66 |
+
# 可选字段:id, host_c_oses, proxy, expires_at
|
| 67 |
+
# 详细配置请参考 accounts_config.example.json
|
| 68 |
+
# 账号配置直接在 accounts_config.json文件配置即可,注意过期时间为北京时间
|
| 69 |
+
ACCOUNTS_CONFIG=[{"secure_c_ses":"your-cookie-here","csesidx":"your-idx","config_id":"your-config","expires_at": "2026-01-08 21:04:39"}]
|
.env.example
CHANGED
|
@@ -8,12 +8,20 @@
|
|
| 8 |
# API 访问密钥(可选,用于保护 API 端点)
|
| 9 |
# API_KEY=your-secret-api-key-here
|
| 10 |
|
| 11 |
-
# 路径前缀(
|
| 12 |
-
PATH_PREFIX
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
# 管理员密钥(必需,用于
|
| 15 |
ADMIN_KEY=your-admin-secret-key
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# 服务器完整 URL(可选,用于反代公开日志和图片 URL)
|
| 18 |
# BASE_URL=https://your-domain.com
|
| 19 |
|
|
|
|
| 8 |
# API 访问密钥(可选,用于保护 API 端点)
|
| 9 |
# API_KEY=your-secret-api-key-here
|
| 10 |
|
| 11 |
+
# 路径前缀(可选,用于隐藏端点路径)
|
| 12 |
+
# 如果设置了,端点将为: /{PATH_PREFIX}/, /{PATH_PREFIX}/v1/
|
| 13 |
+
# 如果未设置,端点将为: /admin/, /v1/
|
| 14 |
+
# PATH_PREFIX=your-random-path-prefix
|
| 15 |
|
| 16 |
+
# 管理员密钥(必需,用于登录管理面板)
|
| 17 |
ADMIN_KEY=your-admin-secret-key
|
| 18 |
|
| 19 |
+
# Session加密密钥(可选,自动生成)
|
| 20 |
+
# SESSION_SECRET_KEY=your-session-secret-key
|
| 21 |
+
|
| 22 |
+
# Session过期时间(可选,单位:小时,默认24小时)
|
| 23 |
+
# SESSION_EXPIRE_HOURS=24
|
| 24 |
+
|
| 25 |
# 服务器完整 URL(可选,用于反代公开日志和图片 URL)
|
| 26 |
# BASE_URL=https://your-domain.com
|
| 27 |
|
.gitignore
CHANGED
|
@@ -35,10 +35,11 @@ ENV/
|
|
| 35 |
# Project specific
|
| 36 |
.env
|
| 37 |
*.log
|
| 38 |
-
stats.json
|
|
|
|
| 39 |
|
| 40 |
# Generated files
|
| 41 |
-
images/
|
| 42 |
static/
|
| 43 |
logs/
|
| 44 |
|
|
|
|
| 35 |
# Project specific
|
| 36 |
.env
|
| 37 |
*.log
|
| 38 |
+
data/stats.json
|
| 39 |
+
accounts.json
|
| 40 |
|
| 41 |
# Generated files
|
| 42 |
+
data/images/
|
| 43 |
static/
|
| 44 |
logs/
|
| 45 |
|
Dockerfile
CHANGED
|
@@ -14,4 +14,8 @@ COPY uptime_tracker.py .
|
|
| 14 |
COPY core ./core
|
| 15 |
# 复制 util 目录
|
| 16 |
COPY util ./util
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
CMD ["python", "-u", "main.py"]
|
|
|
|
| 14 |
COPY core ./core
|
| 15 |
# 复制 util 目录
|
| 16 |
COPY util ./util
|
| 17 |
+
# 创建数据目录
|
| 18 |
+
RUN mkdir -p ./data/images
|
| 19 |
+
# 声明数据卷(运行时需要 -v 挂载才能持久化)
|
| 20 |
+
VOLUME ["/app/data"]
|
| 21 |
CMD ["python", "-u", "main.py"]
|
README.md
CHANGED
|
@@ -253,15 +253,15 @@ ACCOUNTS_CONFIG='[
|
|
| 253 |
| ---------------------------------------- | ------ | --------------------------- |
|
| 254 |
| `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
|
| 255 |
| `/{PATH_PREFIX}/v1/chat/completions` | POST | 聊天接口(需API_KEY) |
|
| 256 |
-
| `/{PATH_PREFIX}
|
| 257 |
-
| `/{PATH_PREFIX}/
|
| 258 |
-
| `/{PATH_PREFIX}/
|
| 259 |
-
| `/{PATH_PREFIX}/
|
| 260 |
-
| `/{PATH_PREFIX}/
|
| 261 |
-
| `/{PATH_PREFIX}/
|
| 262 |
-
| `/{PATH_PREFIX}/
|
| 263 |
-
| `/{PATH_PREFIX}/
|
| 264 |
-
| `/{PATH_PREFIX}/
|
| 265 |
| `/public/log/html` | GET | 公开日志页面(无需认证) |
|
| 266 |
| `/public/stats` | GET | 公开统计信息(无需认证) |
|
| 267 |
| `/public/stats/html` | GET | 实时状态监控页面(无需认证)|
|
|
@@ -295,7 +295,7 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 295 |
|
| 296 |
### 多模态输入(支持 100+ 种文件类型)
|
| 297 |
|
| 298 |
-
本项目支持图片、PDF、Office 文档、音频、视频、代码等 100+ 种文件类型。详细列表请查看 [支持的文件类型清单](SUPPORTED_FILE_TYPES.md)。
|
| 299 |
|
| 300 |
#### 图片输入
|
| 301 |
|
|
@@ -448,7 +448,7 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 448 |
- 🔧 **配置文件** - 5 种格式(YAML, TOML, INI, ENV, Properties)
|
| 449 |
- 📚 **电子书** - 2 种格式(EPUB, MOBI)
|
| 450 |
|
| 451 |
-
完整列表和使用示例请查看 [支持的文件类型清单](SUPPORTED_FILE_TYPES.md)
|
| 452 |
|
| 453 |
### 图片生成
|
| 454 |
|
|
@@ -488,7 +488,7 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 488 |
|
| 489 |
### 1. 如何在线编辑账户配置?
|
| 490 |
|
| 491 |
-
访问管理面板 `/{PATH_PREFIX}
|
| 492 |
- ✅ 实时编辑 JSON 格式配置
|
| 493 |
- ✅ 保存后立即生效,无需重启
|
| 494 |
- ✅ 配置保存到 `accounts.json` 文件
|
|
@@ -559,9 +559,9 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 559 |
#### 📊 **统计说明**
|
| 560 |
|
| 561 |
- **自动计数**:每次聊天请求成功后自动 +1
|
| 562 |
-
- **持久化保存**:保存到 `stats.json` 文件,重启不丢失
|
| 563 |
- **实时显示**:管理面板账户卡片实时显示累计次数
|
| 564 |
-
- **数据位置**:`stats.json` → `account_conversations` 字段
|
| 565 |
|
| 566 |
#### 📈 **显示位置**
|
| 567 |
|
|
@@ -577,11 +577,11 @@ curl -X POST http://localhost:7860/v1/v1/chat/completions \
|
|
| 577 |
- 统计范围:仅统计成功的对话请求
|
| 578 |
- 失败请求:不计入累计次数
|
| 579 |
- 数据格式:`{"account_id": conversation_count}`
|
| 580 |
-
- 重置方式:目前需要手动编辑 `stats.json` 文件
|
| 581 |
|
| 582 |
### 5. 图片生成后在哪里找到文件?
|
| 583 |
|
| 584 |
-
- **临时存储**: 图片保存在 `./images/`,可通过 URL 访问
|
| 585 |
- **重启后会丢失**,建议使用持久化存储
|
| 586 |
|
| 587 |
### 6. 如何设置 BASE_URL?
|
|
@@ -669,20 +669,27 @@ gemini-business2api/
|
|
| 669 |
│ └── templates.py # HTML模板生成
|
| 670 |
├── util/ # 工具模块
|
| 671 |
│ └── streaming_parser.py # 流式JSON解析器
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
├── requirements.txt # Python依赖
|
| 673 |
├── Dockerfile # Docker构建文件
|
| 674 |
├── README.md # 项目文档
|
| 675 |
-
├── SUPPORTED_FILE_TYPES.md # 支持的文件类型清单
|
| 676 |
├── .env.example # 环境变量配置示例
|
| 677 |
└── accounts_config.example.json # 多账户配置示例
|
| 678 |
```
|
| 679 |
|
| 680 |
**运行时生成的文件和目录**:
|
| 681 |
- `accounts.json` - 账户配置持久化文件(Web编辑后保存)
|
| 682 |
-
- `stats.json` - 统计数据(访问量、请求数等)
|
| 683 |
-
- `images/` - 生成的图片存储目录
|
| 684 |
- HF Pro: `/data/images`(持久化,重启不丢失)
|
| 685 |
-
- 其他环境: `./images`(临时存储,重启会丢失)
|
| 686 |
|
| 687 |
**日志系统**:
|
| 688 |
- 内存日志缓冲区:最多保存 3000 条日志
|
|
|
|
| 253 |
| ---------------------------------------- | ------ | --------------------------- |
|
| 254 |
| `/{PATH_PREFIX}/v1/models` | GET | 获取模型列表 |
|
| 255 |
| `/{PATH_PREFIX}/v1/chat/completions` | POST | 聊天接口(需API_KEY) |
|
| 256 |
+
| `/{PATH_PREFIX}` | GET | 管理面板(需ADMIN_KEY) |
|
| 257 |
+
| `/{PATH_PREFIX}/accounts` | GET | 获取账户状态(需ADMIN_KEY) |
|
| 258 |
+
| `/{PATH_PREFIX}/accounts-config` | GET | 获取账户配置(需ADMIN_KEY) |
|
| 259 |
+
| `/{PATH_PREFIX}/accounts-config` | PUT | 更新账户配置(需ADMIN_KEY) |
|
| 260 |
+
| `/{PATH_PREFIX}/accounts/{id}` | DELETE | 删除指定账户(需ADMIN_KEY) |
|
| 261 |
+
| `/{PATH_PREFIX}/accounts/{id}/disable` | PUT | 禁用指定账户(需ADMIN_KEY) |
|
| 262 |
+
| `/{PATH_PREFIX}/accounts/{id}/enable` | PUT | 启用指定账户(需ADMIN_KEY) |
|
| 263 |
+
| `/{PATH_PREFIX}/log` | GET | 获取系统日志(需ADMIN_KEY) |
|
| 264 |
+
| `/{PATH_PREFIX}/log` | DELETE | 清空系统日志(需ADMIN_KEY) |
|
| 265 |
| `/public/log/html` | GET | 公开日志页面(无需认证) |
|
| 266 |
| `/public/stats` | GET | 公开统计信息(无需认证) |
|
| 267 |
| `/public/stats/html` | GET | 实时状态监控页面(无需认证)|
|
|
|
|
| 295 |
|
| 296 |
### 多模态输入(支持 100+ 种文件类型)
|
| 297 |
|
| 298 |
+
本项目支持图片、PDF、Office 文档、音频、视频、代码等 100+ 种文件类型。详细列表请查看 [支持的文件类型清单](docs/SUPPORTED_FILE_TYPES.md)。
|
| 299 |
|
| 300 |
#### 图片输入
|
| 301 |
|
|
|
|
| 448 |
- 🔧 **配置文件** - 5 种格式(YAML, TOML, INI, ENV, Properties)
|
| 449 |
- 📚 **电子书** - 2 种格式(EPUB, MOBI)
|
| 450 |
|
| 451 |
+
完整列表和使用示例请查看 [支持的文件类型清单](docs/SUPPORTED_FILE_TYPES.md)
|
| 452 |
|
| 453 |
### 图片生成
|
| 454 |
|
|
|
|
| 488 |
|
| 489 |
### 1. 如何在线编辑账户配置?
|
| 490 |
|
| 491 |
+
访问管理面板 `/{PATH_PREFIX}?key=YOUR_ADMIN_KEY`,点击"编辑配置"按钮:
|
| 492 |
- ✅ 实时编辑 JSON 格式配置
|
| 493 |
- ✅ 保存后立即生效,无需重启
|
| 494 |
- ✅ 配置保存到 `accounts.json` 文件
|
|
|
|
| 559 |
#### 📊 **统计说明**
|
| 560 |
|
| 561 |
- **自动计数**:每次聊天请求成功后自动 +1
|
| 562 |
+
- **持久化保存**:保存到 `data/stats.json` 文件,重启不丢失
|
| 563 |
- **实时显示**:管理面板账户卡片实时显示累计次数
|
| 564 |
+
- **数据位置**:`data/stats.json` → `account_conversations` 字段
|
| 565 |
|
| 566 |
#### 📈 **显示位置**
|
| 567 |
|
|
|
|
| 577 |
- 统计范围:仅统计成功的对话请求
|
| 578 |
- 失败请求:不计入累计次数
|
| 579 |
- 数据格式:`{"account_id": conversation_count}`
|
| 580 |
+
- 重置方式:目前需要手动编辑 `data/stats.json` 文件
|
| 581 |
|
| 582 |
### 5. 图片生成后在哪里找到文件?
|
| 583 |
|
| 584 |
+
- **临时存储**: 图片保存在 `./data/images/`,可通过 URL 访问
|
| 585 |
- **重启后会丢失**,建议使用持久化存储
|
| 586 |
|
| 587 |
### 6. 如何设置 BASE_URL?
|
|
|
|
| 669 |
│ └── templates.py # HTML模板生成
|
| 670 |
├── util/ # 工具模块
|
| 671 |
│ └── streaming_parser.py # 流式JSON解析器
|
| 672 |
+
├── docs/ # 文档目录
|
| 673 |
+
│ └── SUPPORTED_FILE_TYPES.md # 支持的文件类型清单
|
| 674 |
+
├── data/ # 运行时数据目录
|
| 675 |
+
│ ├── stats.json # 统计数据(gitignore)
|
| 676 |
+
│ └── images/ # 生成的图片(gitignore)
|
| 677 |
+
├── script/ # 辅助脚本
|
| 678 |
+
│ ├── copy-config.js # 油猴脚本:复制配置到剪贴板
|
| 679 |
+
│ └── download-config.js # 油猴脚本:下载配置文件
|
| 680 |
├── requirements.txt # Python依赖
|
| 681 |
├── Dockerfile # Docker构建文件
|
| 682 |
├── README.md # 项目文档
|
|
|
|
| 683 |
├── .env.example # 环境变量配置示例
|
| 684 |
└── accounts_config.example.json # 多账户配置示例
|
| 685 |
```
|
| 686 |
|
| 687 |
**运行时生成的文件和目录**:
|
| 688 |
- `accounts.json` - 账户配置持久化文件(Web编辑后保存)
|
| 689 |
+
- `data/stats.json` - 统计数据(访问量、请求数等)
|
| 690 |
+
- `data/images/` - 生成的图片存储目录
|
| 691 |
- HF Pro: `/data/images`(持久化,重启不丢失)
|
| 692 |
+
- 其他环境: `./data/images`(临时存储,重启会丢失)
|
| 693 |
|
| 694 |
**日志系统**:
|
| 695 |
- 内存日志缓冲区:最多保存 3000 条日志
|
accounts.json
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "qvzikjjtqaksdinimiadtl@gmail.com",
|
| 4 |
+
"csesidx": "2077462123",
|
| 5 |
+
"config_id": "ea70232b-450e-4d19-8d36-92eb1e899f2e",
|
| 6 |
+
"secure_c_ses": "CSE.AdwtfTAbNjb_CSIyaCu1lPxXETzGwu4sTDHVjAzDtzh3QCRTqdZmXEZMqEHqBPKV6tP-NfSXGy4rLZqCEb3HSmxOk55k0Gy0-0NAyw6wFk7Fb6bMiCesPA8KeNWXabgH7hQvZ4_rQVsmHDxKrpK7PCHbZrpK1wzORrgOaoObFmL2wUXyrXNKZvt7gdAJsAkU9ek8YLTX0Eo8c8jZ1Q8eAKYVM0Iz82OANH-Pj6XoN6_TQUYGNSTUShWOsDBw9EVvzbAqHUA3ldvFZhqFrwF250huTtC_jcfGrF2DkEeTLbDWpcWxXXeW5O6e8X2je3KrYKmsxsCG5ZGv7PgeC9WocDYJIIWAqE-abKj3KBiAozg9AyyBZ2W1VtUa-6l-LxFyb74cLwTRPWwUC5_y7k5A_varlyILzkcQuU5ud38kNjJgpKPNXw8DTXf10UZ-nDRISwICfMywQv93PA",
|
| 7 |
+
"host_c_oses": "COS.AfQtEyC5kScHV6Ldz1Xoce0jEEqHz00extvVRBOXpOuGIQWUgMNgmjpZkQLWnVvDMsFowuKxy5flm0Zw9hAj0b7tuewyOR61ceg2EFO7iPaqN0ZM3kmpVvWFdhsaewRsmX1zr0o-A-l8ZB8N",
|
| 8 |
+
"expires_at": "2026-01-08 21:04:39"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"id": "kanghyunwoo65@gmail.com",
|
| 12 |
+
"csesidx": "668833036",
|
| 13 |
+
"config_id": "06ea7902-019a-46ae-8a39-c322f4719482",
|
| 14 |
+
"secure_c_ses": "CSE.AdwtfTDhUiB-ww2GWQKV_npY8EM5ZS5zZrLX4MORFxIiHxZTyyDqkNSS1Kl99pnnfukXpu-rP_kYdD5W0xyZee496g0d5qLkJShZP96N9dli26WcgBRSfdyVeQbeqBFYW8SVzZFd3kmDXiorlwfHdsvSkxQzlP0-EYb8MMmumb_LNJD3R0QPJzWfrNNXq8-HgPAJMV9UvncuNQCt37T_yp0lxF6nUDMsFcImfRaWVzNFhNJvkJLJWtx3Zd6-OlDJflXQ1NtC-rhCYFKMHQ9YB8PuNd9hjGqUiPhZEGCZPIhNIlXmBDyFKNOVGuyAUBu4M2GIEiacyrfBIOx_X6ImXy1pf4YaPmfHWVJrdm6A5P4vQpX64yZci-zr4HB2YYRJU9vypPNXcrgJkEQ3clxJYFO54423uQnJdHH4GrD9npYTe9szQGMbItzVYaLBn8VjPFIuiedACU7PZw",
|
| 15 |
+
"host_c_oses": "COS.AfQtEyDF2RzTrB696_QKwYL8OumNSv5sOeDCTtV_pu0n2wewFb5OG9fpZR_mwd9BGV1E1QU6HGlDftp3Uq6PQtoChtgITTDD0Mdy0DXlb4m0q1zuMUTgEvhXkvME_dGhayCVVsB3DJ62KPGD",
|
| 16 |
+
"expires_at": "2026-01-08 21:03:46"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"id": "ryxdhdipkqvdasx@gmail.com",
|
| 20 |
+
"csesidx": "2061181307",
|
| 21 |
+
"config_id": "5fc0c329-6e08-48fe-9360-ace12d2d20fd",
|
| 22 |
+
"secure_c_ses": "CSE.AdwtfTC-PTLDnEDOm_8D12FbLoDaN9VGSW4SylqL8934hXY7YsOYo_S6WecPyiuG-5knaznDe7IQC64A_eBWunnVYp8SZRAg7RJW28CGWM5EaoN_-NSKqtZgH4e66JzTe3mZ3YraOP8gE1YFEjB-1leFsPbqPHjVWISxeSzrovt6wjIgmt5b_WT1J4-AtXuZ4zT2JsEN3u7fn4jGQF7NYhZ8WOS2g4TFGcPhHmTTeJ9dRfxmDkLDRZMm7a93VOYASYfdWY8OycXWSMUs17dypIv5YiA5jtoiklkz3UsGgQQdcUKpUK-wS9-k-owNPXgxZIZTQv8w0Z04E1hJx5_hyXvi_obZu0GPFZt5PbDl48BtA-v_4murOaVqo1dWdaxHp4sr-nDiGlhbbU8Z0_5Ou87WYqfuy1mWWDKgiu2iUSt7lpafqjJ6oX7TMpsigI3GV7ETy0uLHkPCZQ",
|
| 23 |
+
"host_c_oses": "COS.AfQtEyD3loYX6XjCmwcrUFJYw9IJJ6TbOjsynUkuzY07-51nnl0f69j0go_VOMOUPKPV3gn0l4y42ytTJHtCrVuRuPSZjTE0G5EDxJLg_mQMs6BQz7RgLmmf4Z2Na6WjxVHVcOS5oXi2yt9k",
|
| 24 |
+
"expires_at": "2026-01-08 21:05:23"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "fytkzpff@gmail.com",
|
| 28 |
+
"csesidx": "1397021800",
|
| 29 |
+
"config_id": "0a178e94-0593-415f-92ca-190c64791798",
|
| 30 |
+
"secure_c_ses": "CSE.AdwtfTCqP_lDymleaWGP8WnHPvWxNexTBNdK-R6P5R6v0GnrSe5mM66FhreggoeukPBCwkf3IZLXFLrGWwyUjnGpYs2OTguXJuL7oJ6_oNBfN-iV7zYs0uWaCjuYxChL7z9dos0DsTjFZ1SpUvs1m7UMmv2siddkaK8wSVLFb4tycUPCgKZST6K6gm7JfPwxhE6yOl7yhI5DrAMQ65ebVG2IO_phZzyxdTI1c59Qj5clXPK0nOKXRi2IuuPRZDzf2Gjd0nuLYHaW2jgB-M26PWIrj72u_eUuHfjjbmHHKlrfxqIBgbqNFpHqsx3-erWV-H0pVc5-1-9cPQrtlA0_cj3PQ4LBPsFgJndPQwhFu650O9DqExGbWYZjTaBw1pHDBX_CXGdX3IhwIyIKh3HHn-YQCsTzRGFMSmp8bE5iw_CB6Q0x7YiNx8BSuo7noMtIb4uX7Y7-Pkdm7g",
|
| 31 |
+
"host_c_oses": "COS.AfQtEyAp-AyZPSaCK-dNa1QPky5Y2O5ueevch4f044Zow7EXOQoJHW2xgQEJq7_BPdHR3k3IQhfIBH86Pzi1Tc0TBzLZ7vP6w7EAOvxUIHazFHPJPtJtKXpO9mIvjpoY3V5J9WSh1WnU_hvq",
|
| 32 |
+
"expires_at": "2026-01-08 21:03:31"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "vpetrovichivanovichdmitrymikha@gmail.com",
|
| 36 |
+
"csesidx": "1842103773",
|
| 37 |
+
"config_id": "ccd05b39-9107-4364-8b60-628dc78256fe",
|
| 38 |
+
"secure_c_ses": "CSE.AdwtfTCxjhQRubOjg0C_z2lZ7rC_aTsOfsEiydiw8p5yhetZKymHqkrm_DeIfUXo7A0f4B5T2WCrV-3WtAtIv-niPZDBEl80rjkoF3QhN07W4jKN3kAbXvXJHPcc9DkwVeUPUausA4xrNTYOjwAnBHnqIvB_dvc6hej2PVKZcs3DDinHwjVzXxIhxDH9x1GnkL2BS1G149YGH10CnJL9mVjc0j1PXceuE3r_soN1Hr2QzhNgANirRtXIVIMLTK1Rw5rPShgUsun0DgHyBxCRhWaVeB6A96R9dvb3-1a1qzIqXE70G_oypCAneCs3TSHw8tFgWUcXQPTmuiuZNGRg-hsa4tTRpBhCc-_pmJ-HtzhKpR10EPURkn2AOjc5iKw4MOeH186H-VGrnHyKt2NCYcSGeAzTImOIJRre63OoZfFUughfaLCtzHNTCtnpQJ0be-pkqLq_ZDOQwg",
|
| 39 |
+
"host_c_oses": "COS.AfQtEyCK75zN0utPKF95zDlDZZAbdg0AO-2mZGEHoiuSpE-K5wbGc5gDodwO6Pvgawk2xl7sYrjw1fK0gs7QEgak4Xut6j7dfj03hYWvQiHVNyyT2LG5IqFUxaldoNSSLFQ3Kimy4NZulIJK",
|
| 40 |
+
"expires_at": "2026-01-08 21:06:50"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "qgsvslizip3z@gmail.com",
|
| 44 |
+
"csesidx": "147279001",
|
| 45 |
+
"config_id": "45f124ac-2083-4a3d-b2c8-ebe623c4cf0d",
|
| 46 |
+
"secure_c_ses": "CSE.AdwtfTDCzrTHEwjafu04wFI2HQtmNygTeATs1ZGHxKzCtoTJVTHkRBitIIv-PFbmrOTBxSYZYsuPfC4sO3V19qKuhkIWVzG8iRa2fjprII0f6o6mdv02Glk2TEqeIqSbeMZc9DeSJclbCIpB6_7h61WgLjad3FJQSgNMpXWWNJiCbr24sXTA7rMN-w8wG_qiBLRx2u5eBdNo7ouHNNEzt2JIm9RwYWzQnzK2hWA5i-HbM5UbBJCFUkrAGc0SrW1mhoq9qU4fAgZDY-w8B1aeQhdsny0gGjvYHvENzg6g2IypOotm5WZ4nCj8_BuV3PfLPtj4k4Q7c9kpUdGK6bWzYWZa-JrHwww2l1zmuzjhZC4mrUgnUXAbjTxGVtH0w3bqkudaWrlDlvzYSp2vefBhZzE0tujUYJ7Ug8NtYZU5KZlVchyLh6iw8-TssGijY7kXdxeI6n026MUR",
|
| 47 |
+
"host_c_oses": "COS.AfQtEyDuBfBkPnOXH0KvoduBEpXe4zC0y6G-WozeQd0mpBzRub37DRm9VALpEEB60DOMv1-l7-Nyl3OyVKztSEu1HZZ2tsYHaVXx8Art4dlRiCmnqwxE7QB-vlGiKIFtxVrTTFoe2FENRti6",
|
| 48 |
+
"expires_at": "2026-01-08 21:07:07"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"id": "66wp79dlpo29@gmail.com",
|
| 52 |
+
"csesidx": "758206494",
|
| 53 |
+
"config_id": "1fa5f2de-dc02-42d3-a07d-3bd5df9744c7",
|
| 54 |
+
"secure_c_ses": "CSE.AdwtfTCsxgi4Ah0AbGCjLT859NYb6E8E0uvYxFOYu7cF6Pr71eXBPFFfitRMAD23bkH6Diz0f-gsVI6-D46Yukj2rnzmHNLInVeHv-Zp8267NJsyLhhZEBfkPYoEeJk5dZPEahDKvL4XHvI4LiheQX1CoH1G8vrhTKRGdgkK2PsA-itowKvaTjH6L1kTQpRW1RAW4X1hhEWwDruoxKCpsZ-JOnU9N77Ql-nVSGOouDeWocORsivWpnQsIznEUvVmhWYYH3vfVZ3F_RthhC6u0y9xmgwsvQpbWbi3iTy39XlPDQnELWg6N39uX59JYiWbC4F5i6RyloQMk2TtVA2S9otXGguoaeymEuvOGbmQrjFoe0gzoNCn4ai1JFQ6KcBQlVucO5CzQY4ts6aV2H4Z0hbpF1xNKUVvtWPlTi6bf4-160dTQKqQNmtONL8Ttlv71kl5T6m1L-yo7Q",
|
| 55 |
+
"host_c_oses": "COS.AfQtEyAEnwAjUVsVsHhrxABbcNykgHdZsNPwyX4FAJuVa3mA--pGZPwqR_JBRMi-mtwnLmSeO5HwxGkMoSXd_2B-BVF6ei7PRNh_6sE42rETfK0iKreHoYR29xwuUF-PClTdgzNb8ndtwoiC",
|
| 56 |
+
"expires_at": "2026-01-08 21:07:20"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "dewimayadianpermana@gmail.com",
|
| 60 |
+
"csesidx": "1510317426",
|
| 61 |
+
"config_id": "f1c9585c-2bf4-48d8-96b8-b1b3ae045074",
|
| 62 |
+
"secure_c_ses": "CSE.AdwtfTBfGlDyT0GT6J2n1NJ28VlqUQMbkcYfTFrHg0KFQJt4yxjUtTsLQaYGd-Db8WlqLaRneAFP51zsZOZMNvRw15d_1nhRTdZa6Thjv0Tq3f_et2xY0KhdaXlCbohAacpPQwccQK8V1E2k9oZ4ZQH8ZT_P9AZwhwWOGz7WXuUFAaJsNeV29tHI7gHO4xxymbO3ZmHCb9aL9NvIYeeO_nm-n2Z0JzJJKrV1p26b8pv0_NtDQN516LD1mTj-d-KRBGtgGM5jU4Jquva9JPF0Gowxp_3Mne2TLYYgW6Po1piXUPgp0MfxLIMW7nOFZT8R-hXVwLrWKDFlfGDUTPxWQAi7-ac6AYNsisHJzunuL-v9dKaD0mavMHrlw1HJAgYpxhnDRkeZ667kK5juSafP6rh_zSEFXSLthp8RwCyYMZiSD0lpN4ju6sXUwAka3NZOyY7YMxTia_5nbQ",
|
| 63 |
+
"host_c_oses": "COS.AfQtEyD8DDz8By-E4Xt4zHwdsKy770NeJVV0kmTXR7aRqTSn2pz4yk71hewpEFyB4D3Y9MZ5JOO6h6BNz7m_RMGTs6xI0_5SqqjvJL8ZAwX2sl1xU-KnIp24wFoB7VgqG0r8rXFqtWUlGl3Y",
|
| 64 |
+
"expires_at": "2026-01-08 21:07:40"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"id": "setiawanagustridianfitri@gmail.com",
|
| 68 |
+
"csesidx": "1460987002",
|
| 69 |
+
"config_id": "2ea88373-32b2-492e-8709-c157867e2b3f",
|
| 70 |
+
"secure_c_ses": "CSE.AdwtfTBnhMuHm0G-Bl8qsymZgz5ddKnJ3MhseYG5es91uZ3iXApE7EpWjclAUOaVBHJe1Em13kMO6yOhQEafFlABY0vaURNmlQEszRobc4H5kK-Bz_EauztqYNL1oAW3ZSkPN9Hy-btQFkgSmYs0V0ywND6KiOdXsvTjf90EvJ-b2N7uTuLLY32h65eLBV6-5HbGk477sAtAVMHio2SrjC5G8Pm3ldFlzZGXvIxIOVxFJ-ENRu3_-tQNNRp6S7LaFSxGTh7b-yOz6n6DrWiPZuvqGN5llw2WCwBkfezfzo4O2ntLwjQSkcfXkeZstWq-_Ig5wkS3BbZAe1sEMZO7O9tgoVKkLRzOPUMrNtnLYhgE1VN6nqq00eLJiQ2Ui2Ymbco4krFmKKvdDR4daGR0GxKw8qJ-v8lvPMbLGnU3zdtwoKgNngzTux-TwPlHA6RnqHEKcQ_XAgpkJg",
|
| 71 |
+
"host_c_oses": "COS.AfQtEyC-3yCgrU0kiBYj4h1jUxvtZUzUYkICkBYh3GG8Z_aX9tX_rnPbn8Q1MXaFSh_emchLMFwkbl2ow1BUA6886pooFG8vnYb81dtZvZeyxMqwJHIRXwNeeepYZ-ZXvLZWsXPPemfyCFfZ",
|
| 72 |
+
"expires_at": "2026-01-09 09:29:35",
|
| 73 |
+
"disabled": false
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"id": "zhvreejcalezmsf@gmail.com",
|
| 77 |
+
"csesidx": "56883269",
|
| 78 |
+
"config_id": "8a3dfa35-6d47-4941-8cf2-223d6b22fc64",
|
| 79 |
+
"secure_c_ses": "CSE.AdwtfTDpFdHVxAM7bK45pTvtqwTF7zgIoH69wq4Gpu-Ko7jAK-lYq1McP0UnDRqMLNnwKF0EBXf6wnuBl2Rc70NO6gzQFTiRHKnNZVxTnUXLYPuv2jQPWKX0lBixiQCDjuBFnFwh5rG75r6O03tDCmt0RSp_ie9awzb9HMfEFWo1B4xx9NBG5Wm8hKcvhtyEGbkQf4SpZn8ztFUAGDsr9OYMeu-lZS2zZvxW80sedEUJBVBVRczzLULbQch9qwwzHyTuWJ1l3todVNGid19bEgaXh9EJPhaKZ93AdW0F4_ofac1Pttd8qIyjs2tfFprEpGtWSO8eQI6A2EGLsG8CYNWz3UbHfqJOvlxPBMwlq3_mHm624Js41ck2cqTMvlQjnZtbP_lQXaTSXcm8ozDkjRoqaXI5IYO3a3ye3zaPf85UbWocDz0dcBKxfNx82fvgIvUibb0Su6B-",
|
| 80 |
+
"host_c_oses": "COS.AfQtEyAuIXg4vEmxuRKn20NBkNIyUet2e7qUkubUvp7cIEgv5-_KVssaB5l6ZkPFgAJaeePotByEey6VPE8rfu_BvX0wrq9V2yuPVIRbO6e2BMUvu1k8ZLwYgT4c9HY1A73p1-yBS3wJVIA7",
|
| 81 |
+
"expires_at": "2026-01-09 09:29:12",
|
| 82 |
+
"disabled": false
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"id": "ludwigjohannannafischer@gmail.com",
|
| 86 |
+
"csesidx": "568055810",
|
| 87 |
+
"config_id": "7d977232-eb5b-4907-9f6c-2889c58b016e",
|
| 88 |
+
"secure_c_ses": "CSE.AdwtfTBvZ-tDItdMo4UvlggbPfrx9kOzsHKg9pDRqHn8B2Ofu8EnIq4rvQ9twd9mSP8_IzzAH6EQNnLDSJoaxtoeZpgcDfN_YcsXhNQQGJEznWrHh3K0fBELrcgENa7WnVqXefUIlxeAk_BP5NOKQKd7ty5Vou56PSDn4PMSc7zh6KnXFoyhWonqkuVxfba5Z6B68aunyEr50OmY7HoFcS60vb5eX5m7lVXTDK-B07hen75QC-Cfs73efEPRHxCHR8dQubecZmin7nyGWVgvNRIxElYsVwJOIfd6sdbVZWZimswIqCDuzsD7is1glZfPEbICuTM2mOerdJkOs73dUAl4aXz9cWK6cOG_rVozqM2Mu-WNJPhmOI9bMa0vL90se1pSNSG1b30rDPNAbflTzq6AFQDokhZiNply4IvEUIaFatWSDBedBIbEgAzUYXHXwzj-poJAHwnxrQ",
|
| 89 |
+
"host_c_oses": "COS.AfQtEyB0-ggLgckKQA12cDnG9XfIQhN2j8SNtC7XmMQuSOs2TyWf70EiCeeoOvQfEeYnjTlwCQ6u0StdVAteQlZ9Xz69Qn8ilqeFCyALRHPIugH5imf_Y1ByrMHUrS_9XmaJ8G428zLkk3CN",
|
| 90 |
+
"expires_at": "2026-01-09 09:28:45",
|
| 91 |
+
"disabled": false
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"id": "cahyadewisari5@gmail.com",
|
| 95 |
+
"csesidx": "61801455",
|
| 96 |
+
"config_id": "138dd0d0-ec6e-4c6c-be55-9c23bfe52d68",
|
| 97 |
+
"secure_c_ses": "CSE.AdwtfTBmTTxvAR1l627V7DuVT4bVazBn7fcv6p9vcSuO7MK9iQ9X1Y4aFdhDbMVoiaKIsiGnepJbw0KHD33hJxZCKi8ob9i_Rwi06fEsmlWkdPSvww18lRpjhzF23MlZe0Ah2bBmvtzlvXgw1CDZr4q249qZ2_BUXVRxLGMee8n0QbfsrgiTZgwU7fzdnxwLou5Lxcwako_Mk8fHhcTa8ODsS_7y8I3DhjQAv5jNZOK9apSuPH9JMMcn0Fez0wLwDm5Z0U_5ECcN6HR6VkVfSSVqRzfOJNQNaQDxKOzA-gqz-BVZC4ghN-HylKAn1JAGmBE0Mudpv4f8wr2ZtlG2eDrr7UKtA1CrLBTgzKVqkK9blAqwPTZ8d_TM6tneMdd5wBUPdEoOgZOkHgr9tMsG9H-hGkBzNZvqrRcWlt7_xdUhTL1p-aCLBuhe-c3P1AJjk90TiPhA4lXL",
|
| 98 |
+
"host_c_oses": "COS.AfQtEyBC-XesaeyF0KjTqQOUOzzZtchiEPXDOXAiSTPmEkakqhTMRU7d_6lss8NcuNq6TZP1eBw6lecv-Ks3NHLlPpUwy-BZKhai6AsAvwjR23j6Txf7JpsUweQHk7poQAQOAEMdUsQTDTYb",
|
| 99 |
+
"expires_at": "2026-01-09 09:26:39",
|
| 100 |
+
"disabled": false
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"id": "robertsislaamelia957@gmail.com",
|
| 104 |
+
"csesidx": "124360125",
|
| 105 |
+
"config_id": "bbfaba1c-8cda-4c5b-bd54-90fc74095abc",
|
| 106 |
+
"secure_c_ses": "CSE.AdwtfTDCF2_ThPC274-icqtHLCm1F9t90K5LN3AxlA06o8GMQFBdavwE_Wwi3kyGFC916CqJ2rBJL2f-Tbg532SnSIihsCn8FYBcEjKQ6gX8_B71hSbz6iGPI3Vj49ZGxnfY_YJm63-iq9tPrnjemnhr9wYeiccpEMH2kjKjYeHtrcawmpuMA546CJqFqMlGKFJwJvpQ4DpXiJWBz3FrSYte_NfX4peOt0lkcULYba-s5VS9tjZotL5M5-WSL4X2xZv3R4L3JaW6Im-gPoopiNwLvP2oCsYwBZA3EYufP5gy92ZY9U14aelbrHpEutZ2aXlZk3MEwdRWxqfH2LL5JpXfFMNZ8-EZkljqcGt65lrHDgbLlOuxbE1TalIN8IpCdHXZvkcSaFJ8Xn39N1UyRtyf3EgxmogTtYMojemznU51_hGC1gIXyJb8lvz82cjysbg4oMq2bhs9",
|
| 107 |
+
"host_c_oses": "COS.AfQtEyBzEW7vuiZZ5Uj9kNqlNqTMFCD-YL9wTEe4NCXEjTDi3cnSh7wURlVr5f2BSGB5T48xM0ZY7zZBLiPJHiLdFBYMWstEuWwbVBV9pzIzv6dMPpIpwpIYMr-2ku1I14WYpGAwKIRolb5J",
|
| 108 |
+
"expires_at": "2026-01-09 09:25:58",
|
| 109 |
+
"disabled": false
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"id": "thimaivanbinhho@gmail.com",
|
| 113 |
+
"csesidx": "2102457917",
|
| 114 |
+
"config_id": "72d7429e-8dfc-4cb9-9985-b877dcb3ffca",
|
| 115 |
+
"secure_c_ses": "CSE.AdwtfTBDeZzxbcU4Ag1yJH00muvfgy3WizZJ7AHhevS-ZF4QklJZW_em_BvY-B-OXn42SmTjTzcDF7IVGeYNPnsA7JeoBOHsg8aMxPGzbe_cxwrIG6bIVWcdjiw7u-ZbswaWnm0GzcuDVY75Ut5LwXC7HBx4uJhEshLsZqbP5DVRtmTL9ak7M3fr70M-aasmc9HdCqLycWSZmpYYK6CvmgD3kJzhCXNOPHB5AOpPlQr0VthfHtj2HqSh0JoB1Lv41AhFDIcL0owFWNoIC1zpwCh5agtPMe2gFsJN_9xBGVabj6RGzjjk4MfWg51XRtiGdaloPu6jfwqf8QOqw2eGqIVSm8usdwV3GYOJXK03qKQcHnsDIL5RaB5KpOEZQCp55_JE8Wjomiv5pl1C2HuLd7aGVnXFB__ha30UgFyHBqTdcbCR81Ik5fllvKlHNmvbysJlsRmBeMMB-w",
|
| 116 |
+
"host_c_oses": "COS.AfQtEyCQzAfRXFoDwb7JqGaI7aZCk4Vxhze80_H_rALLuRH7ZIe9T1-Ev5t0-XWpWq_L08IlbpLznnsUYagbiogQay_vuQ6ARGvJh-Fsb2zjrxJBjvNhOBRWFSFR6pn5QgjA9nfPQeuTYiwe",
|
| 117 |
+
"expires_at": "2026-01-09 09:25:40"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"id": "kuznetsovmariamikhail@gmail.com",
|
| 121 |
+
"csesidx": "516526177",
|
| 122 |
+
"config_id": "f44bd49b-275d-43c5-8db6-7cb9da618a64",
|
| 123 |
+
"secure_c_ses": "CSE.AdwtfTC9yumNS5mPk9TE_jyrEji0Ufr6MUZrNkemz3jE_uvQT6AK7GaktzrrRhq4NH4_wK98JhKxbghsVeQFvtPPKezRZiIYBF8KAZggogPWszPQu-I_bOWeETFQLXO7bOZ3C93qznHuLPfFwM69P-JuTDCCJk8Q6xWhJWX5b6gbJJMDTH9uWWZNVVLiR1wUNN8C9qrEwkdKMM4ZeXH28c5-ybAy62zQyhhF_Sa2X7iKET2sNCMx91sphDKiZQXr8n_fEo5CLLNMPXsQKbnc0r_0O8yvpdiFwos7uCUp7wQitKLqoqNP0BEKF6bHgvN2S7LW-kB_jvNBO5jmTFTBCteb76n2m5bwQqJvXSur2HktGVs4PWSlMsKpJplhk91Dq71k2G6qx092AIJkzLjX7Ora_MfSBLZOPNqAvcGq5AotRm9zrCDH0vmiY6IP86KizGaeu4IJPG82_Q",
|
| 124 |
+
"host_c_oses": "COS.AfQtEyDc37SdDWnkZvQL6z1RfOt5UBZeIRhIOMjl8T_pHXSSB961F7uiJFhuHEpJE29mFtnFGNfoo0GNCG10Td_W_ooEfi0E6_Uf5bW0md7Hcxym2TPp8OoZuONlXIy6zig0DAiIXaHyEp0_",
|
| 125 |
+
"expires_at": "2026-01-09 09:25:19"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"id": "zbdlhrqjejtqqjaistbye@gmail.com",
|
| 129 |
+
"csesidx": "613137890",
|
| 130 |
+
"config_id": "7dff83f6-9756-4e52-aa5b-1b1a7747594d",
|
| 131 |
+
"secure_c_ses": "CSE.AdwtfTAxX23Bbi5voc4UKYVeNlCgCr7fPajOWgvgMU6tGVucBdTrftHdNvSnnHjcxE0c1NNWV1-MsN_Ra0EgWf6QE0NLmBZiDQEeJF1PLs-K9Oxx4L0xRJ6x1OT12Il0TgHL8Ge4oravsmZitpaxE3lcX_exxraJfrhck3xVQxxE7rmrJO6qXzcqkHTYKslbTUi_zTD6rL52cpe-ELCZwV3f0OMMwDHSg44iPRaWxgbJ1ye8XXCHylScZjU_8pjpt_22abA-vHsfHElGFGUypuuvAfFVLYlkRcejy97vruB_BKB_aKHzYwZ6PZFVAE3WQUKp_kd_xIeJmbmBFwSDNcM1VIGMyGYO0O8H_eepZZb1qdcIuhGrJIvO1jxo6WwcbDbdTjbrBDrVLpA5bMy8c0vdNcJpvIeUJGYg94sBAbBw5xsQreRRpklAB-snyVdZmzYuElVPdfdjOQ",
|
| 132 |
+
"host_c_oses": "COS.AfQtEyARr2Yz2PRP_Jge3x4J_SdXSplDdZYeAFpcOkcSpO9J65tnjlagDeJG4msHa8k8sUXn70e8Iw5KOu5z1EMBeRzB1PhgiZ26QlybVOaBChbwQTxqWVaj2pze77jmGHO44s362jmm6DRa",
|
| 133 |
+
"expires_at": "2026-01-09 09:23:52"
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "zhangsy.dreamer2@gmail.com",
|
| 137 |
+
"csesidx": "1834748654",
|
| 138 |
+
"config_id": "782e9083-0012-4fb7-848b-097790e165c6",
|
| 139 |
+
"secure_c_ses": "CSE.AdwtfTBDBOsvXcd2uWKiG3R_6nnOuik6GK90cmOlglqNhUp1FB6vSbAtrAb1_f_sQnnSYpWHSAI44jQbs0oM0SibgFxCWC2yv8i4GuXktROE1Jr6PDkbvFjxp0iNFu8DcwHvDDn435KrxcEx_aULfUUgZyAtZAOT1Jt8t8cvqqYCWQyzWl5UgahXpImnjuZIObTW4kW5Ih7pq4O06_6kRTYrDdnfz_og4DVN94bBAzS665-ejECp_WRYTBcefICtDI6MmkwSL1rKofhjU5rNopI_tXTdEBe1WO1PoYTg2H1TrhlXmkhb9EuLznH5i-KlNwSGu5vpjw7Fkd29WpVdAIGofNex4Ifr_EWYg_mmIOY9ZNrliwatBJV2wHNCyvFI0Eb-bMWixYPe2gNwkWP4YeZDN1oDRl0SkrJ99eeIvZjrhrCITH1dTZglfe2T8zN4VnUCkCLg67jCdg",
|
| 140 |
+
"host_c_oses": "COS.AfQtEyCy4U9UOl941eP5i9nHt-bxOON9kYXVaUFk3bN9c-uhoU5S9oNV4Ty0jMo8dT6Nk0lLAsKoLfLxTqUxMtr5VBLQbjFKB2gtInYodPgWHQB4QwAtLv8QMKBSCeN98dLWQNUHC56XYd7v",
|
| 141 |
+
"expires_at": "2026-01-08 21:12:51"
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"id": "95d4r6wxl1s3@gmail.com",
|
| 145 |
+
"csesidx": "382938560",
|
| 146 |
+
"config_id": "60301a41-da8d-444d-b6c4-d5403cf2ac7a",
|
| 147 |
+
"secure_c_ses": "CSE.AdwtfTCgUh7DA_FwniHbez07GQ04ExF0ALs5BixnXtfnZMu9_giGTUK-6shncxnVCXjIfgewfCcipzCDBNxzSaaWv4Ylwr8dqKahkYe6yg5cOfstBQl7wm0YNYvxzkKLNQsT3K3BJWwDv_o3R1XsFojRC_rPKD6zT4VW_AFKTvHkMtCPVvYWSWsHwXh5vdf3lFIAidxbkP58JCDTipXEfE5uG_7Fvr54hNJ7B0rNB2O8ZYYMjbMFhG-T2eMZ1ItKDxxmOEYkX3j27pk68R_ioD8yDocfp-dBM9jRRlaFwjQbGmIUh-2bfKABLFxX8Sk5IDb3UdnSn7X00Dy03BdpWFzNYUp5wYfByCRz7TNL3JTSuZ-7drZEIHU3A8NRCxOt_rvffU_9tUEdF48yyJiqCkP8VIKzsgE2WUXzHAL-SUG2LJcO87KrBuj0Zbn-PQusZM3DhRdq-_2_ug",
|
| 148 |
+
"host_c_oses": "COS.AfQtEyAYPCVFiuk-KAAD-S5MzX2vCPETXo05J8_PF9Xyy3CfLCa0yPmSPtCGq5eyHeFg-itwXYtM4s_CKCUE_diqPeWtLXSWhXecmvc4abxMmT63FCsdgiumI_7u_kQAEwLW-VfTVyXnwa6w",
|
| 149 |
+
"expires_at": "2026-01-08 21:12:35"
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"id": "zhangsy.dreamer@gmail.com",
|
| 153 |
+
"csesidx": "787806297",
|
| 154 |
+
"config_id": "92e1ef11-0965-40f6-8cfb-eae19dd9b624",
|
| 155 |
+
"secure_c_ses": "CSE.AdwtfTDiVWQJ0VYblIB8Xx3UWYlx5Goa8oT2u3ZW_v9lrPu2Gy1uN094Om_BKdvKHwl4PBQBlPbbd3Os9iBAS2vnZjjMoJNTvvYa9BzihS3xTtJ8lARH3XNxri80jW1mtvmn3adMwQa3A32KhKCRAvBaL5_NHg78iXNErobOM1q1vUPgwnhmb8dQtNvwieA2BDVeYLAic3O1Rbh4gvhbakPMbfg08ogWHFLomcSuai5OWJW7Sr4-ySTtgtxb7_FZ8owjjtBWsLhdr-Kdy4mbEGsNBmF2rhBpPOy-OPrU8JtygjzPLkxROvRG6F8_abx2oLzzW4f2QAEbUSFNDmYif3OZwPL89jppXa1MjbA7AAD6ClV-EkKx5vkeiJZCkEk14mRyZOKATzjzJLoA5wyE9Hkhzl5SNsjB5oAg1RsMGXYf2vclkz3GrpThUtslPt5aWTYHDxhp_3PZLA",
|
| 156 |
+
"host_c_oses": "COS.AfQtEyDHp_smdmvO4NCpetOK7jLsrm7wKWltNeYeube4IPiK034RgkUdlb7Kfkl3MbOkt5qQh-gCS6M7MutcyuqNOnBrV2_2nzMcvK8hsw5fcMK22r9Z55S01WS0sJqvgz6vAW0ETZwzAOIZ",
|
| 157 |
+
"expires_at": "2026-01-08 21:13:43"
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"id": "vuqumo@gmail.com",
|
| 161 |
+
"csesidx": "355439776",
|
| 162 |
+
"config_id": "96962f36-79c3-4f33-81e6-66d38a48c231",
|
| 163 |
+
"secure_c_ses": "CSE.AdwtfTA9DXjXh7mBmoFbGqVNsll-NNcr7D4jv5MjhAkMFJfL3T6bZtJB0Y4X2GkvVbgat_kzJTkdPJsFKL0S_U8J6i_rbu_yYWre0a1mPX80LtojKh40LYMeeCbvSosZVhuctQ9p2nE5Hbty4_jX6zXdqs46bEnLT4EvukfjJLTaZHPSjYihVJQ78ug0zH6i_jhabrwpadZbDe0_jaZpvb04zvggs13ErnubD95k6Ao7vX0Mi2Jf4qKg7sPv8M-tgYXkflUO7H00blibbytAiXF-xJhCeCINAtFwM0ejisnm8M83j0DkOQ-iVxh2xTdSOjFqYHrToBPyvKPQySE_tD45gpv0Le5iG41UqkjVkP58aNhKKKRfy8gfJaktA3KyyjBPuQFrdfUiAcSwHGhYUpHW5w9_K_QpK6gFtmXa3nlFzMEvS_eGuw4FFJKQdFvF7lzmm6oEISlJSQ",
|
| 164 |
+
"host_c_oses": "COS.AfQtEyBlj1M1ra8vg8MMCGYUmYqW7AUsubY1aP8SBF8r86TkJlHcWPCvJXKVyVd6COakigc5mIgMz9UucxsZSmIS7avKqOZdF9oAR7_u4zJHt7fsRRzVPdials1g41CshiNALzsay0bxkgyq",
|
| 165 |
+
"expires_at": "2026-01-08 21:13:21"
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"id": "baipiaoyouhui001@gmail.com",
|
| 169 |
+
"csesidx": "2064418629",
|
| 170 |
+
"config_id": "0cd70003-1ed9-4059-8f31-90364ee3f000",
|
| 171 |
+
"secure_c_ses": "CSE.AdwtfTCQip7_nUoII86jjLGjO4iT-9BekDpVFqcDY9eRyWAiDE9j6ZkcQNI0Ido2xeHU9RsVjZWFfRilcmlRakU4jRZRNIiGZxQGC7ptb2aVxNRL5QSjwZ6mV41iEbDeLnmiECqkdQOH188kK6rB6vfrcGJUw94lYosUYBsxu6VsdsNeq_joLrd_4nmA0diwYkQAo-rehLtYWboBw-MOjvzYU8-1_ONNVpjk6kOj95ANzCvs8-vorkmI8ugc8N9e6SGUm1KBagSNa2kjioN5jHJJ1FdcbQXrgRMKGVQyTgUKPeFUqiOZ8s2dwG3xsSZJIZIuv6Okg3E5FdYWnPNaOpSzPYslyNLV45vyCCQL_CHTEau1yl3FXzV5giFKraGCDq5GihzvgFJbPTgkgbNlTt9lHtgkQzUKsr4ZMGgiF80LlfNeVFsFV1IZ70GcCLwdob1upRVL-p2AJQ",
|
| 172 |
+
"host_c_oses": "COS.AfQtEyBcoy24Ng2ArWmvAQth8DnCiUmlnrgp1qw3HcH69Tgqr7ANGDk1tihgZ9aAcyz8TbT01iRjerOxoWKP7LkqD8_ICK91H-OeUMhwdimPGDn5U_67iBJthA1FWUoFGBnx52fz1qyCX0s9",
|
| 173 |
+
"expires_at": "2026-01-08 21:12:15"
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"id": "zhangsy404@gmail.com",
|
| 177 |
+
"csesidx": "1725184524",
|
| 178 |
+
"config_id": "c638af83-a43f-495e-985e-8f33146d53b6",
|
| 179 |
+
"secure_c_ses": "CSE.AdwtfTBvXrGFGcN30_uT_cDFQJfctcTqOojz5bKTzzq7DxNGKLDSTAYB9_UpJyyyWYxL3paKtGUgcaWo5QwrH4oxkkLKbm1ZZixamSflRDvWR7gIvFopVtznFRX9GkV3jZwflYNTJ9VHwh5cyEpWVabGZDr6Aa6UPPDzuSB5IH46I6Vq7aa5IhqFChJ5IRnXOnbkb-Gkx9rItlzKNk_E3cKKNkQD_dftCchpd-knvawq7Mvt_ydGfpYPJGOtw-ec5BJy9yQ7yEAaXv3AmxPv8WrgBazi_1okXN-TQqxtYntOoX7OfEUuthXzCf_wDfNrokrxr7V4jrWR8pyXWvsV5Z3Yfcborf61e6R-BEYJIi-rrU0-utS1us74s8pL-WiaWuVVFgYLWlp32Mkxrb56fSlKHjS5_WFbD9-jplly0S5sf-feCK9MdDiBN22eEl1jqGHtUP0Z24eiug",
|
| 180 |
+
"host_c_oses": "COS.AfQtEyDuSTIf-otgTfEk7WxjYQAad5hqS3K5fewAO9aCrN2pKcSKssbBSAUForw02w8lMGfSRnAe0vyRkxKjr8TVmLf67bCUNMRclAPx1YGN7H3m6OzezI6hlsCnyAJRcyr_xNASZ00cQtNc",
|
| 181 |
+
"expires_at": "2026-01-08 21:11:47"
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"id": "vovanlan152@gmail.com",
|
| 185 |
+
"csesidx": "1880271056",
|
| 186 |
+
"config_id": "4fbf6a48-6353-4c1e-8294-b5cd6f1f35ba",
|
| 187 |
+
"secure_c_ses": "CSE.AdwtfTABqBtO4KEigR5xbVrTNiEhlmSGqI7lSZ8hzFkINozVQVOZw8Zq6yF6Gg4pMce1zxwJKndgo4zMHCi1lgEqrFz719vzNQzg3gooVcRY8S7Bd3Jd3AhWcy5Vzr66t5XTnsAsN8d3x-59FOLdl7L1GSayrbQTmBeVpBE6Vby437E_rAqg6GGStfZGj3WoYTjoKBE4-cj11GN-CJqLD3XRJUEQewgYDTAWBD7YytkfZsYIhY1QSOUCdyFjtPGPXsE5YwP0EU1jsnt8y1O6qd96L1coRZFFSmDzoQWpR7U2H4-VSgruDXKSzAPPd_q8hg2ioNbtXcBiLUTbI_1bDqPvpPjs1nDbwgEeYBvCDVxZTmLRSdlN_mLFGxV82WgMEoBgmG8_WUVhN9GeEgeZ_ldagh7ktUJYLQBP11MHeB-VsDOp1nuTqxFe3wfbrYN7_fK1Zxvh0fnGgA",
|
| 188 |
+
"host_c_oses": "COS.AfQtEyDgr2RrhUdYEmGP4S7i8CZlC7P-j1r__PVfLc7bNwuNYPLoNfHXTzpllxbByxAhMsWvKpGcw7WDm6XT4Y6xKTPiUPZ6n4HrVzs0z_Enp_rgqAlsmxc2kXfhHUZfRE5ijRJ5PnRAS0hA",
|
| 189 |
+
"expires_at": "2026-01-08 21:11:20"
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"id": "kxhwyxjlhykpxbnqdefhpqkm@gmail.com",
|
| 193 |
+
"csesidx": "1551335924",
|
| 194 |
+
"config_id": "17342b42-2daf-410c-9c4c-4000bc274123",
|
| 195 |
+
"secure_c_ses": "CSE.AdwtfTAPiexnyfi929pvHPhRzbU-rLzIkUpB2AVGQWPnHErk4Z6FlNyZZIVgTLzp6IZBHXQ0K6ewZue-sUfzd58UqO2awSrV2urF1hTAQ9fTAcC2Y9uf57fTYTDh6FMDFjWsdEyp9-gy-IghS8Bd-3Ymu0j0QBkS4ism4HYbUaqlARZT1syU-FX1-1bqXYdbTpsw47AY31I9qCpo5gqDasUb5ItTKkfa8H3IDMIybP0Dhl_5VmeFrGJcNJep8DsAcwRRHNFhbuKvrqZrYpPUqzcDSvUHu_qrgd0YB1Vo5Jy6Ug7fyvI8Yz2bgvxhbcWHLxGNc_Qdwcz45awXfYGb3vnunUIQw492wzaQN3ltRUFPziigSDn2xAGFa1kv2rL2qno9MwpV7nTZSHbEVQnDqfUh8AHXeKrgO7dzFwJjcL215maFOS3iIz4t8bMyCiz7ouJpMcvm-QMhIg",
|
| 196 |
+
"host_c_oses": "COS.AfQtEyCnmkhZXlRiVMwY3sALY9lC4eXZZ_cmTU-B8jSHemyHcA4RzK8H5d5_of1KhZpor_Nj-f68Wn63OSZSmPKKtqwkyB7UADH9qhOdyk_MnAldWX1pYEQ_XbZ1n4cSlVIQYzoweU08lCU6",
|
| 197 |
+
"expires_at": "2026-01-08 21:10:14"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "khqeuulrjibqsnob@gmail.com",
|
| 201 |
+
"csesidx": "1282154517",
|
| 202 |
+
"config_id": "e7d694c4-01a3-4fff-85cb-503538035af5",
|
| 203 |
+
"secure_c_ses": "CSE.AdwtfTAW4KbOsm405yTMqcGKCUDSl5M0SF28mKC6fnQn47kLJ49xctHSL9mwLToh7bQKJ3REgmc5xYI1fRnXLZ9xyGhfevZnufCuS4ZCUYlu23BAV9DJuoBlOi-1nEA9LxRUeu-Jr9nPtu_UKwqMa3hrdidi60y6-7NjoLnjfjTKFBUX_mMGsfLEUQbJvSw3wIO9vlzyfpgxEWFuAQowYQW3jzsgQOr7S2NsXcSuC5_6tsR_2VrqKzSzckrLHvOqvjBWWbkPIc05o7sP-2IE4QWz9-mwYz09-cZsJzNmW_2gyg1CXK4zwD9ya-_b_bZCav5Eatv0swf0q30RV0CHtVGHoqKzX3UNAhOaUUtP9CxXmeIRtNu-P7Imxt_1ahaX2dc5lZvY0XT5ZNkq5mpoeZLBNHt1JjrEefjJDLptW5Icd8sDl-B7osDYBwKP3ugueTnPWSiBi9l3ow",
|
| 204 |
+
"host_c_oses": "COS.AfQtEyBwij1hmkfIfDRuLlVAWshpMzJuZSCnQvlw-l-slJVG-7dNURzy0OnBhhI9Bf43BU73njMNHpERR_H90x5TgSvA_oA9-f9QwCIz8x9NPFH4M1eh79iSgqFJwbEkPVy-fa-Bai6I0LUW",
|
| 205 |
+
"expires_at": "2026-01-08 21:09:49"
|
| 206 |
+
},
|
| 207 |
+
{
|
| 208 |
+
"id": "jwdrhceoqsompvnwy@gmail.com",
|
| 209 |
+
"csesidx": "1657726537",
|
| 210 |
+
"config_id": "4d6632ea-956e-454b-8ddf-0ca4b41b6608",
|
| 211 |
+
"secure_c_ses": "CSE.AdwtfTDKUhjSqqEgXYCMC8FNFePn7MZkh1Fq0cfFjvVI6VKRka_JqyLuG_ZIzAcskKMJxPK9Bd34QDJhSJqdSlN2LYtVXWPzl2kU7IFynQ42CVd2Be73y1EVmAmJfogbJadwQ_MN5x3QK0I3vnlPWi6qnejNCFFBKs_HNQFSRnQkOwBn3GWpSWkzmEKNsU5BBXw0cik6c6JD5UHcDVB8jVnrlrRO3LOgGt8KVYaDFpZFFP-A42zKZLooDuFKGBXAfWA9jd35zQiCZUKVcvKLipGS70A4pZEj-y4iuyZFGpqO0CfgjxGa218jyPKUnhFk5Z94udcJPvpO-LcDsM57WhDetBJDNtiKkIAft2odP2WiIu1XAH26Tr2Sf4ytyO2v37HhYS7nxVQhiYvklu2d0c4GoRxY3USGwkRoTHcX9PTGmwtxPqniBCEUZVNBdzTtLM2BD1nKSh7rKA",
|
| 212 |
+
"host_c_oses": "COS.AfQtEyAD-BW9EMI-fQk4I2ijz2p84Yh-jW7FdlbQ1zRbqwVA4nil3T2oit2--_HkPcg3Zn32cSuY1mj2yDW6bYjodrBrLpMzX6ppAbF5pt7d8n_V__VIChvrgAGYIi2RpXwSRB5MdvTDVYAa",
|
| 213 |
+
"expires_at": "2026-01-08 21:09:21"
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"id": "smariamariepaul@gmail.com",
|
| 217 |
+
"csesidx": "1012844174",
|
| 218 |
+
"config_id": "fa083019-0c5f-46fe-8338-7e94ffa36b5b",
|
| 219 |
+
"secure_c_ses": "CSE.AdwtfTCIqwUpajcOSccv6vATc_Dh3MGEnYqHPoMuXeFcx6V3AX6pdbKLvv3bHH8qtk_he_qnzCSrmYJx1Kx3tLWJWTBPSpIOK3TcFsrphWP4ieotH2l1cM3RVm01tnJaTEEFxckPT5Htf6M4QYjKE6_sEhvytDo-71V-pU4-XcxPY8o_bzT-VsYmyEF2pQVnR2AqK5Dm5i07SXivLfYzvdGRK4Mgk5twLdrh0YtyE9lrTi4UewQr86042V9W2GIg2hIxyeY2JrBDq1qkVFdcdqANIDiGPshPfdO_N1BcbKuwOMzugZKmSOhRj-GZd8bnHvRZyljcJ1gB6nHDS5U2-KMi-aLPle6kXarZG1BLaQmXw0ZW6z-hPULPH_n3Ua1NhEn_d7RBMWTZZl1yb84TgOHayoOKKtpnBOAKvVjoBk3nrwY4Xa95-7y5lGZocYWS89mAv3hq21zHqQ",
|
| 220 |
+
"host_c_oses": "COS.AfQtEyA8iRkEZykLBMt4oTtqW8mlQqOQtY7QP3lwHhnZwU3HqdA7PGVMtWYhOucRTOd4F7W-mNkUxXHOIVnQscpL_6yF9xvEYTv8E9JeLPLlBs-gXOr9zEs0B4BBZRdqktRPVPehETo6txzy",
|
| 221 |
+
"expires_at": "2026-01-08 21:08:55"
|
| 222 |
+
}
|
| 223 |
+
]
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
gemini-api:
|
| 3 |
+
build: .
|
| 4 |
+
ports:
|
| 5 |
+
- "7860:7860"
|
| 6 |
+
volumes:
|
| 7 |
+
- ./data:/app/data
|
| 8 |
+
env_file:
|
| 9 |
+
- .env
|
| 10 |
+
restart: unless-stopped
|
main.py
CHANGED
|
@@ -1,23 +1,47 @@
|
|
| 1 |
-
import json, time,
|
| 2 |
from datetime import datetime, timezone, timedelta
|
| 3 |
from typing import List, Optional, Union, Dict, Any
|
| 4 |
-
from dataclasses import dataclass
|
| 5 |
import logging
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
|
| 8 |
import httpx
|
| 9 |
import aiofiles
|
| 10 |
-
from fastapi import FastAPI, HTTPException, Header, Request, Body
|
| 11 |
-
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
from pydantic import BaseModel
|
| 14 |
from util.streaming_parser import parse_json_array_stream_async
|
| 15 |
from collections import deque
|
| 16 |
from threading import Lock
|
| 17 |
-
from functools import wraps
|
| 18 |
|
| 19 |
-
# 导入认证
|
| 20 |
-
from core.auth import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# 导入 Uptime 追踪器
|
| 23 |
import uptime_tracker
|
|
@@ -29,7 +53,7 @@ log_buffer = deque(maxlen=3000)
|
|
| 29 |
log_lock = Lock()
|
| 30 |
|
| 31 |
# 统计数据持久化
|
| 32 |
-
STATS_FILE = "stats.json"
|
| 33 |
stats_lock = asyncio.Lock() # 改为异步锁
|
| 34 |
|
| 35 |
async def load_stats():
|
|
@@ -95,25 +119,25 @@ logger.addHandler(memory_handler)
|
|
| 95 |
|
| 96 |
load_dotenv()
|
| 97 |
# ---------- 配置 ----------
|
| 98 |
-
PROXY = os.getenv("PROXY"
|
| 99 |
TIMEOUT_SECONDS = 600
|
| 100 |
-
API_KEY = os.getenv("API_KEY"
|
| 101 |
-
PATH_PREFIX = os.getenv("PATH_PREFIX")
|
| 102 |
-
ADMIN_KEY = os.getenv("ADMIN_KEY")
|
| 103 |
-
BASE_URL = os.getenv("BASE_URL")
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# ---------- 公开展示配置 ----------
|
| 106 |
LOGO_URL = os.getenv("LOGO_URL", "") # Logo URL(公开,为空则不显示)
|
| 107 |
CHAT_URL = os.getenv("CHAT_URL", "") # 开始对话链接(公开,为空则不显示)
|
| 108 |
MODEL_NAME = os.getenv("MODEL_NAME", "gemini-business") # 模型名称(公开)
|
| 109 |
-
HIDE_HOME_PAGE = os.getenv("HIDE_HOME_PAGE", "").lower() == "true" # 是否隐藏首页(默认不隐藏)
|
| 110 |
|
| 111 |
# ---------- 图片存储配置 ----------
|
| 112 |
-
# 自动检测存储路径:优先使用持久化存储,否则使用临时存储
|
| 113 |
if os.path.exists("/data"):
|
| 114 |
-
IMAGE_DIR = "/data/images" # HF Pro持久化存储
|
| 115 |
else:
|
| 116 |
-
IMAGE_DIR = "./images" #
|
| 117 |
|
| 118 |
# ---------- 重试配置 ----------
|
| 119 |
MAX_NEW_SESSION_TRIES = int(os.getenv("MAX_NEW_SESSION_TRIES", "5")) # 新会话创建最多尝试账户数(默认5)
|
|
@@ -134,7 +158,7 @@ MODEL_MAPPING = {
|
|
| 134 |
|
| 135 |
# ---------- HTTP 客户端 ----------
|
| 136 |
http_client = httpx.AsyncClient(
|
| 137 |
-
|
| 138 |
verify=False,
|
| 139 |
http2=False,
|
| 140 |
timeout=httpx.Timeout(TIMEOUT_SECONDS, connect=60.0),
|
|
@@ -160,689 +184,63 @@ def get_base_url(request: Request) -> str:
|
|
| 160 |
# ---------- 常量定义 ----------
|
| 161 |
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"
|
| 162 |
|
| 163 |
-
def get_common_headers(jwt: str) -> dict:
|
| 164 |
-
return {
|
| 165 |
-
"accept": "*/*",
|
| 166 |
-
"accept-encoding": "gzip, deflate, br, zstd",
|
| 167 |
-
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
| 168 |
-
"authorization": f"Bearer {jwt}",
|
| 169 |
-
"content-type": "application/json",
|
| 170 |
-
"origin": "https://business.gemini.google",
|
| 171 |
-
"referer": "https://business.gemini.google/",
|
| 172 |
-
"user-agent": USER_AGENT,
|
| 173 |
-
"x-server-timeout": "1800",
|
| 174 |
-
"sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
| 175 |
-
"sec-ch-ua-mobile": "?0",
|
| 176 |
-
"sec-ch-ua-platform": '"Windows"',
|
| 177 |
-
"sec-fetch-dest": "empty",
|
| 178 |
-
"sec-fetch-mode": "cors",
|
| 179 |
-
"sec-fetch-site": "cross-site",
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
def urlsafe_b64encode(data: bytes) -> str:
|
| 183 |
-
return base64.urlsafe_b64encode(data).decode().rstrip("=")
|
| 184 |
-
|
| 185 |
-
def kq_encode(s: str) -> str:
|
| 186 |
-
b = bytearray()
|
| 187 |
-
for ch in s:
|
| 188 |
-
v = ord(ch)
|
| 189 |
-
if v > 255:
|
| 190 |
-
b.append(v & 255)
|
| 191 |
-
b.append(v >> 8)
|
| 192 |
-
else:
|
| 193 |
-
b.append(v)
|
| 194 |
-
return urlsafe_b64encode(bytes(b))
|
| 195 |
-
|
| 196 |
-
def create_jwt(key_bytes: bytes, key_id: str, csesidx: str) -> str:
|
| 197 |
-
now = int(time.time())
|
| 198 |
-
header = {"alg": "HS256", "typ": "JWT", "kid": key_id}
|
| 199 |
-
payload = {
|
| 200 |
-
"iss": "https://business.gemini.google",
|
| 201 |
-
"aud": "https://biz-discoveryengine.googleapis.com",
|
| 202 |
-
"sub": f"csesidx/{csesidx}",
|
| 203 |
-
"iat": now,
|
| 204 |
-
"exp": now + 300,
|
| 205 |
-
"nbf": now,
|
| 206 |
-
}
|
| 207 |
-
header_b64 = kq_encode(json.dumps(header, separators=(",", ":")))
|
| 208 |
-
payload_b64 = kq_encode(json.dumps(payload, separators=(",", ":")))
|
| 209 |
-
message = f"{header_b64}.{payload_b64}"
|
| 210 |
-
sig = hmac.new(key_bytes, message.encode(), hashlib.sha256).digest()
|
| 211 |
-
return f"{message}.{urlsafe_b64encode(sig)}"
|
| 212 |
-
|
| 213 |
# ---------- 多账户支持 ----------
|
| 214 |
-
|
| 215 |
-
class AccountConfig:
|
| 216 |
-
"""单个账户配置"""
|
| 217 |
-
account_id: str
|
| 218 |
-
secure_c_ses: str
|
| 219 |
-
host_c_oses: Optional[str]
|
| 220 |
-
csesidx: str
|
| 221 |
-
config_id: str
|
| 222 |
-
expires_at: Optional[str] = None # 账户过期时间 (格式: "2025-12-23 10:59:21")
|
| 223 |
-
disabled: bool = False # 手动禁用状态
|
| 224 |
-
|
| 225 |
-
def get_remaining_hours(self) -> Optional[float]:
|
| 226 |
-
"""计算账户剩余小时数"""
|
| 227 |
-
if not self.expires_at:
|
| 228 |
-
return None
|
| 229 |
-
try:
|
| 230 |
-
# 解析过期时间(假设为北京时间)
|
| 231 |
-
beijing_tz = timezone(timedelta(hours=8))
|
| 232 |
-
expire_time = datetime.strptime(self.expires_at, "%Y-%m-%d %H:%M:%S")
|
| 233 |
-
expire_time = expire_time.replace(tzinfo=beijing_tz)
|
| 234 |
-
|
| 235 |
-
# 当前时间(北京时间)
|
| 236 |
-
now = datetime.now(beijing_tz)
|
| 237 |
-
|
| 238 |
-
# 计算剩余时间
|
| 239 |
-
remaining = (expire_time - now).total_seconds() / 3600
|
| 240 |
-
return remaining
|
| 241 |
-
except Exception:
|
| 242 |
-
return None
|
| 243 |
-
|
| 244 |
-
def is_expired(self) -> bool:
|
| 245 |
-
"""检查账户是否已过期"""
|
| 246 |
-
remaining = self.get_remaining_hours()
|
| 247 |
-
if remaining is None:
|
| 248 |
-
return False # 未设置过期时间,默认不过期
|
| 249 |
-
return remaining <= 0
|
| 250 |
-
|
| 251 |
-
def format_account_expiration(remaining_hours: Optional[float]) -> tuple:
|
| 252 |
-
"""
|
| 253 |
-
格式化账户过期时间显示(基于12小时过期周期)
|
| 254 |
-
|
| 255 |
-
Args:
|
| 256 |
-
remaining_hours: 剩余小时数(None表示未设置过期时间)
|
| 257 |
-
|
| 258 |
-
Returns:
|
| 259 |
-
(status, status_color, expire_display) 元组
|
| 260 |
-
"""
|
| 261 |
-
if remaining_hours is None:
|
| 262 |
-
# 未设置过期时间时显示为"未设置"
|
| 263 |
-
return ("未设置", "#9e9e9e", "未设置")
|
| 264 |
-
elif remaining_hours <= 0:
|
| 265 |
-
return ("已过期", "#f44336", "已过期")
|
| 266 |
-
elif remaining_hours < 3: # 少于3小时
|
| 267 |
-
return ("即将过期", "#ff9800", f"{remaining_hours:.1f} 小时")
|
| 268 |
-
else: # 3小时及以上,统一显示小时
|
| 269 |
-
return ("正常", "#4caf50", f"{remaining_hours:.1f} 小时")
|
| 270 |
-
|
| 271 |
-
class AccountManager:
|
| 272 |
-
"""单个账户管理器"""
|
| 273 |
-
def __init__(self, config: AccountConfig):
|
| 274 |
-
self.config = config
|
| 275 |
-
self.jwt_manager: Optional['JWTManager'] = None # 延迟初始化
|
| 276 |
-
self.is_available = True
|
| 277 |
-
self.last_error_time = 0.0
|
| 278 |
-
self.last_429_time = 0.0 # 429错误专属时间戳
|
| 279 |
-
self.error_count = 0
|
| 280 |
-
self.conversation_count = 0 # 累计对话次数
|
| 281 |
-
|
| 282 |
-
async def get_jwt(self, request_id: str = "") -> str:
|
| 283 |
-
"""获取 JWT token (带错误处理)"""
|
| 284 |
-
# 检查账户是否过期
|
| 285 |
-
if self.config.is_expired():
|
| 286 |
-
self.is_available = False
|
| 287 |
-
logger.warning(f"[ACCOUNT] [{self.config.account_id}] 账户已过期,已自动禁用")
|
| 288 |
-
raise HTTPException(403, f"Account {self.config.account_id} has expired")
|
| 289 |
-
|
| 290 |
-
try:
|
| 291 |
-
if self.jwt_manager is None:
|
| 292 |
-
# 延迟初始化 JWTManager (避免循环依赖)
|
| 293 |
-
self.jwt_manager = JWTManager(self.config)
|
| 294 |
-
jwt = await self.jwt_manager.get(request_id)
|
| 295 |
-
self.is_available = True
|
| 296 |
-
self.error_count = 0
|
| 297 |
-
return jwt
|
| 298 |
-
except Exception as e:
|
| 299 |
-
self.last_error_time = time.time()
|
| 300 |
-
self.error_count += 1
|
| 301 |
-
# 使用配置的失败阈值
|
| 302 |
-
if self.error_count >= ACCOUNT_FAILURE_THRESHOLD:
|
| 303 |
-
self.is_available = False
|
| 304 |
-
logger.error(f"[ACCOUNT] [{self.config.account_id}] JWT获取连续失败{self.error_count}次,账户已永久禁用")
|
| 305 |
-
else:
|
| 306 |
-
# 安全:只记录异常类型,不记录详细信息
|
| 307 |
-
logger.warning(f"[ACCOUNT] [{self.config.account_id}] JWT获取失败({self.error_count}/{ACCOUNT_FAILURE_THRESHOLD}): {type(e).__name__}")
|
| 308 |
-
raise
|
| 309 |
-
|
| 310 |
-
def should_retry(self) -> bool:
|
| 311 |
-
"""检查账户是否可重试(429错误10分钟后恢复,普通错误永久禁用)"""
|
| 312 |
-
if self.is_available:
|
| 313 |
-
return True
|
| 314 |
-
|
| 315 |
-
current_time = time.time()
|
| 316 |
-
|
| 317 |
-
# 检查429冷却期(10分钟后自动恢复)
|
| 318 |
-
if self.last_429_time > 0:
|
| 319 |
-
if current_time - self.last_429_time > RATE_LIMIT_COOLDOWN_SECONDS:
|
| 320 |
-
return True # 冷却期已过,可以重试
|
| 321 |
-
return False # 仍在冷却期
|
| 322 |
-
|
| 323 |
-
# 普通错误永久禁用
|
| 324 |
-
return False
|
| 325 |
-
|
| 326 |
-
def get_cooldown_info(self) -> tuple[int, str | None]:
|
| 327 |
-
"""
|
| 328 |
-
获取账户冷却信息
|
| 329 |
-
|
| 330 |
-
Returns:
|
| 331 |
-
(cooldown_seconds, cooldown_reason) 元组
|
| 332 |
-
- cooldown_seconds: 剩余冷却秒数,0表示无冷却,-1表示永久禁用
|
| 333 |
-
- cooldown_reason: 冷却原因,None表示无冷却
|
| 334 |
-
"""
|
| 335 |
-
current_time = time.time()
|
| 336 |
-
|
| 337 |
-
# 优先检查429冷却期(无论账户是否可用)
|
| 338 |
-
if self.last_429_time > 0:
|
| 339 |
-
remaining_429 = RATE_LIMIT_COOLDOWN_SECONDS - (current_time - self.last_429_time)
|
| 340 |
-
if remaining_429 > 0:
|
| 341 |
-
return (int(remaining_429), "429限流")
|
| 342 |
-
# 429冷却期已过
|
| 343 |
-
|
| 344 |
-
# 如果账户可用且没有429冷却,返回正常状态
|
| 345 |
-
if self.is_available:
|
| 346 |
-
return (0, None)
|
| 347 |
-
|
| 348 |
-
# 普通错误永久禁用
|
| 349 |
-
return (-1, "错误禁用")
|
| 350 |
-
|
| 351 |
-
class MultiAccountManager:
|
| 352 |
-
"""多账户协调器"""
|
| 353 |
-
def __init__(self):
|
| 354 |
-
self.accounts: Dict[str, AccountManager] = {}
|
| 355 |
-
self.account_list: List[str] = [] # 账户ID列表 (用于轮询)
|
| 356 |
-
self.current_index = 0
|
| 357 |
-
self._cache_lock = asyncio.Lock() # 缓存操作专用锁
|
| 358 |
-
self._index_lock = asyncio.Lock() # 索引更新专用锁
|
| 359 |
-
# 全局会话缓存:{conv_key: {"account_id": str, "session_id": str, "updated_at": float}}
|
| 360 |
-
self.global_session_cache: Dict[str, dict] = {}
|
| 361 |
-
self.cache_max_size = 1000 # 最大缓存条目数
|
| 362 |
-
self.cache_ttl = SESSION_CACHE_TTL_SECONDS # 缓存过期时间(秒)
|
| 363 |
-
# Session级别锁:防止同一对话的并发请求冲突
|
| 364 |
-
self._session_locks: Dict[str, asyncio.Lock] = {}
|
| 365 |
-
self._session_locks_lock = asyncio.Lock() # 保护锁字典的锁
|
| 366 |
-
self._session_locks_max_size = 2000 # 最大锁数量
|
| 367 |
-
|
| 368 |
-
def _clean_expired_cache(self):
|
| 369 |
-
"""清理过期的缓存条目"""
|
| 370 |
-
current_time = time.time()
|
| 371 |
-
expired_keys = [
|
| 372 |
-
key for key, value in self.global_session_cache.items()
|
| 373 |
-
if current_time - value["updated_at"] > self.cache_ttl
|
| 374 |
-
]
|
| 375 |
-
for key in expired_keys:
|
| 376 |
-
del self.global_session_cache[key]
|
| 377 |
-
if expired_keys:
|
| 378 |
-
logger.info(f"[CACHE] 清理 {len(expired_keys)} 个过期会话缓存")
|
| 379 |
-
|
| 380 |
-
def _ensure_cache_size(self):
|
| 381 |
-
"""确保缓存不超过最大大小(LRU策略)"""
|
| 382 |
-
if len(self.global_session_cache) > self.cache_max_size:
|
| 383 |
-
# 按更新时间排序,删除最旧的20%
|
| 384 |
-
sorted_items = sorted(
|
| 385 |
-
self.global_session_cache.items(),
|
| 386 |
-
key=lambda x: x[1]["updated_at"]
|
| 387 |
-
)
|
| 388 |
-
remove_count = len(sorted_items) - int(self.cache_max_size * 0.8)
|
| 389 |
-
for key, _ in sorted_items[:remove_count]:
|
| 390 |
-
del self.global_session_cache[key]
|
| 391 |
-
logger.info(f"[CACHE] LRU清理 {remove_count} 个最旧会话缓存")
|
| 392 |
-
|
| 393 |
-
async def start_background_cleanup(self):
|
| 394 |
-
"""启动后台缓存清理任务(每5分钟执行一次)"""
|
| 395 |
-
try:
|
| 396 |
-
while True:
|
| 397 |
-
await asyncio.sleep(300) # 5分钟
|
| 398 |
-
async with self._cache_lock:
|
| 399 |
-
self._clean_expired_cache()
|
| 400 |
-
self._ensure_cache_size()
|
| 401 |
-
except asyncio.CancelledError:
|
| 402 |
-
logger.info("[CACHE] 后台清理任务已停止")
|
| 403 |
-
except Exception as e:
|
| 404 |
-
logger.error(f"[CACHE] 后台清理任务异常: {e}")
|
| 405 |
-
|
| 406 |
-
async def set_session_cache(self, conv_key: str, account_id: str, session_id: str):
|
| 407 |
-
"""线程安全地设置���话缓存"""
|
| 408 |
-
async with self._cache_lock:
|
| 409 |
-
self.global_session_cache[conv_key] = {
|
| 410 |
-
"account_id": account_id,
|
| 411 |
-
"session_id": session_id,
|
| 412 |
-
"updated_at": time.time()
|
| 413 |
-
}
|
| 414 |
-
# 检查缓存大小
|
| 415 |
-
self._ensure_cache_size()
|
| 416 |
-
|
| 417 |
-
async def update_session_time(self, conv_key: str):
|
| 418 |
-
"""线程安全地更新会话时间戳"""
|
| 419 |
-
async with self._cache_lock:
|
| 420 |
-
if conv_key in self.global_session_cache:
|
| 421 |
-
self.global_session_cache[conv_key]["updated_at"] = time.time()
|
| 422 |
-
|
| 423 |
-
async def acquire_session_lock(self, conv_key: str) -> asyncio.Lock:
|
| 424 |
-
"""获取指定对话的锁(用于防止同一对话的并发请求冲突)"""
|
| 425 |
-
async with self._session_locks_lock:
|
| 426 |
-
# 清理过多的锁(LRU策略:删除不在缓存中的锁)
|
| 427 |
-
if len(self._session_locks) > self._session_locks_max_size:
|
| 428 |
-
# 只保留当前缓存中存在的锁
|
| 429 |
-
valid_keys = set(self.global_session_cache.keys())
|
| 430 |
-
keys_to_remove = [k for k in self._session_locks if k not in valid_keys]
|
| 431 |
-
for k in keys_to_remove[:len(keys_to_remove)//2]: # 删除一半无效锁
|
| 432 |
-
del self._session_locks[k]
|
| 433 |
-
|
| 434 |
-
if conv_key not in self._session_locks:
|
| 435 |
-
self._session_locks[conv_key] = asyncio.Lock()
|
| 436 |
-
return self._session_locks[conv_key]
|
| 437 |
-
|
| 438 |
-
def add_account(self, config: AccountConfig):
|
| 439 |
-
"""添加账户"""
|
| 440 |
-
manager = AccountManager(config)
|
| 441 |
-
# 从统计数据加载对话次数
|
| 442 |
-
if "account_conversations" in global_stats:
|
| 443 |
-
manager.conversation_count = global_stats["account_conversations"].get(config.account_id, 0)
|
| 444 |
-
self.accounts[config.account_id] = manager
|
| 445 |
-
self.account_list.append(config.account_id)
|
| 446 |
-
logger.info(f"[MULTI] [ACCOUNT] 添加账户: {config.account_id}")
|
| 447 |
-
|
| 448 |
-
async def get_account(self, account_id: Optional[str] = None, request_id: str = "") -> AccountManager:
|
| 449 |
-
"""获取账户 (轮询或指定) - 优化锁粒度,减少竞争"""
|
| 450 |
-
req_tag = f"[req_{request_id}] " if request_id else ""
|
| 451 |
-
|
| 452 |
-
# 如果指定了账户ID(无需锁)
|
| 453 |
-
if account_id:
|
| 454 |
-
if account_id not in self.accounts:
|
| 455 |
-
raise HTTPException(404, f"Account {account_id} not found")
|
| 456 |
-
account = self.accounts[account_id]
|
| 457 |
-
if not account.should_retry():
|
| 458 |
-
raise HTTPException(503, f"Account {account_id} temporarily unavailable")
|
| 459 |
-
return account
|
| 460 |
-
|
| 461 |
-
# 轮询选择可用账户(无锁读取账户列表)
|
| 462 |
-
available_accounts = [
|
| 463 |
-
acc_id for acc_id in self.account_list
|
| 464 |
-
if self.accounts[acc_id].should_retry()
|
| 465 |
-
and not self.accounts[acc_id].config.is_expired()
|
| 466 |
-
and not self.accounts[acc_id].config.disabled
|
| 467 |
-
]
|
| 468 |
-
|
| 469 |
-
if not available_accounts:
|
| 470 |
-
raise HTTPException(503, "No available accounts")
|
| 471 |
-
|
| 472 |
-
# 只在更新索引时加锁(最小化锁持有时间)
|
| 473 |
-
async with self._index_lock:
|
| 474 |
-
if not hasattr(self, '_available_index'):
|
| 475 |
-
self._available_index = 0
|
| 476 |
-
|
| 477 |
-
account_id = available_accounts[self._available_index % len(available_accounts)]
|
| 478 |
-
self._available_index = (self._available_index + 1) % len(available_accounts)
|
| 479 |
-
|
| 480 |
-
account = self.accounts[account_id]
|
| 481 |
-
logger.info(f"[MULTI] [ACCOUNT] {req_tag}选择账户: {account_id}")
|
| 482 |
-
return account
|
| 483 |
|
| 484 |
# ---------- 配置文件管理 ----------
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
def save_accounts_to_file(accounts_data: list):
|
| 488 |
-
"""保存账户配置到文件"""
|
| 489 |
-
with open(ACCOUNTS_FILE, 'w', encoding='utf-8') as f:
|
| 490 |
-
json.dump(accounts_data, f, ensure_ascii=False, indent=2)
|
| 491 |
-
logger.info(f"[CONFIG] 配置已保存到 {ACCOUNTS_FILE}")
|
| 492 |
-
|
| 493 |
-
def load_accounts_from_source() -> list:
|
| 494 |
-
"""优先从文件加载,否则从环境变量加载"""
|
| 495 |
-
# 优先从文件加载
|
| 496 |
-
if os.path.exists(ACCOUNTS_FILE):
|
| 497 |
-
try:
|
| 498 |
-
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
| 499 |
-
accounts_data = json.load(f)
|
| 500 |
-
logger.info(f"[CONFIG] 从文件加载配置: {ACCOUNTS_FILE}")
|
| 501 |
-
return accounts_data
|
| 502 |
-
except Exception as e:
|
| 503 |
-
logger.warning(f"[CONFIG] 文件加载失败,尝试环境变量: {str(e)}")
|
| 504 |
-
|
| 505 |
-
# 从环境变量加载
|
| 506 |
-
accounts_json = os.getenv("ACCOUNTS_CONFIG")
|
| 507 |
-
if not accounts_json:
|
| 508 |
-
raise ValueError(
|
| 509 |
-
"未找到配置文件或 ACCOUNTS_CONFIG 环境变量。\n"
|
| 510 |
-
"请在环境变量中配置 JSON 格式的账户列表,格式示例:\n"
|
| 511 |
-
'[{"id":"account_1","csesidx":"xxx","config_id":"yyy","secure_c_ses":"zzz","host_c_oses":null,"expires_at":"2025-12-23 10:59:21"}]'
|
| 512 |
-
)
|
| 513 |
-
|
| 514 |
-
try:
|
| 515 |
-
accounts_data = json.loads(accounts_json)
|
| 516 |
-
if not isinstance(accounts_data, list):
|
| 517 |
-
raise ValueError("ACCOUNTS_CONFIG 必须是 JSON 数组格式")
|
| 518 |
-
# 首次从环境变量加载后,保存到文件
|
| 519 |
-
save_accounts_to_file(accounts_data)
|
| 520 |
-
logger.info(f"[CONFIG] 从环境变量加载配置并保存到文件")
|
| 521 |
-
return accounts_data
|
| 522 |
-
except json.JSONDecodeError as e:
|
| 523 |
-
logger.error(f"[CONFIG] ACCOUNTS_CONFIG JSON 解析失败: {str(e)}")
|
| 524 |
-
raise ValueError(f"ACCOUNTS_CONFIG 格式错误: {str(e)}")
|
| 525 |
-
|
| 526 |
-
def get_account_id(acc: dict, index: int) -> str:
|
| 527 |
-
"""获取账户ID(有显式ID则使用,否则生成默认ID)"""
|
| 528 |
-
return acc.get("id", f"account_{index}")
|
| 529 |
-
|
| 530 |
-
# ---------- 多账户配置加载 ----------
|
| 531 |
-
def load_multi_account_config() -> MultiAccountManager:
|
| 532 |
-
"""从文件或环境变量加载多账户配置"""
|
| 533 |
-
manager = MultiAccountManager()
|
| 534 |
-
|
| 535 |
-
accounts_data = load_accounts_from_source()
|
| 536 |
-
|
| 537 |
-
for i, acc in enumerate(accounts_data, 1):
|
| 538 |
-
# 验证必需字段
|
| 539 |
-
required_fields = ["secure_c_ses", "csesidx", "config_id"]
|
| 540 |
-
missing_fields = [f for f in required_fields if f not in acc]
|
| 541 |
-
if missing_fields:
|
| 542 |
-
raise ValueError(f"账户 {i} 缺少必需字段: {', '.join(missing_fields)}")
|
| 543 |
-
|
| 544 |
-
config = AccountConfig(
|
| 545 |
-
account_id=get_account_id(acc, i),
|
| 546 |
-
secure_c_ses=acc["secure_c_ses"],
|
| 547 |
-
host_c_oses=acc.get("host_c_oses"),
|
| 548 |
-
csesidx=acc["csesidx"],
|
| 549 |
-
config_id=acc["config_id"],
|
| 550 |
-
expires_at=acc.get("expires_at"),
|
| 551 |
-
disabled=acc.get("disabled", False) # 读取手动禁用状态,默认为 False
|
| 552 |
-
)
|
| 553 |
-
|
| 554 |
-
# 检查账户是否已过期
|
| 555 |
-
if config.is_expired():
|
| 556 |
-
logger.warning(f"[CONFIG] 账户 {config.account_id} 已过期,跳过加载")
|
| 557 |
-
continue
|
| 558 |
-
|
| 559 |
-
manager.add_account(config)
|
| 560 |
-
|
| 561 |
-
if not manager.accounts:
|
| 562 |
-
raise ValueError("没有有效的账户配置(可能全部已过期)")
|
| 563 |
-
|
| 564 |
-
logger.info(f"[CONFIG] 成功加载 {len(manager.accounts)} 个账户")
|
| 565 |
-
return manager
|
| 566 |
-
|
| 567 |
|
| 568 |
# 初始化多账户管理器
|
| 569 |
-
multi_account_mgr = load_multi_account_config(
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
def update_accounts_config(accounts_data: list):
|
| 579 |
-
"""更新账户配置(保存到文件并重新加载)"""
|
| 580 |
-
save_accounts_to_file(accounts_data)
|
| 581 |
-
reload_accounts()
|
| 582 |
-
|
| 583 |
-
def delete_account(account_id: str):
|
| 584 |
-
"""删除单个账户"""
|
| 585 |
-
accounts_data = load_accounts_from_source()
|
| 586 |
-
|
| 587 |
-
# 过滤掉要删除的账户
|
| 588 |
-
filtered = [
|
| 589 |
-
acc for i, acc in enumerate(accounts_data, 1)
|
| 590 |
-
if get_account_id(acc, i) != account_id
|
| 591 |
-
]
|
| 592 |
-
|
| 593 |
-
if len(filtered) == len(accounts_data):
|
| 594 |
-
raise ValueError(f"账户 {account_id} 不存在")
|
| 595 |
-
|
| 596 |
-
save_accounts_to_file(filtered)
|
| 597 |
-
reload_accounts()
|
| 598 |
-
|
| 599 |
-
def update_account_disabled_status(account_id: str, disabled: bool):
|
| 600 |
-
"""更新账户的禁用状态"""
|
| 601 |
-
accounts_data = load_accounts_from_source()
|
| 602 |
-
|
| 603 |
-
# 查找并更新账户
|
| 604 |
-
found = False
|
| 605 |
-
for i, acc in enumerate(accounts_data, 1):
|
| 606 |
-
if get_account_id(acc, i) == account_id:
|
| 607 |
-
acc["disabled"] = disabled
|
| 608 |
-
found = True
|
| 609 |
-
break
|
| 610 |
-
|
| 611 |
-
if not found:
|
| 612 |
-
raise ValueError(f"账户 {account_id} 不存在")
|
| 613 |
-
|
| 614 |
-
save_accounts_to_file(accounts_data)
|
| 615 |
-
reload_accounts()
|
| 616 |
-
|
| 617 |
-
status_text = "已禁用" if disabled else "已启用"
|
| 618 |
-
logger.info(f"[CONFIG] 账户 {account_id} {status_text}")
|
| 619 |
|
| 620 |
# 验证必需的环境变量
|
| 621 |
-
if not PATH_PREFIX:
|
| 622 |
-
logger.error("[SYSTEM] 未配置 PATH_PREFIX 环境变量,请设置后重启")
|
| 623 |
-
import sys
|
| 624 |
-
sys.exit(1)
|
| 625 |
-
|
| 626 |
if not ADMIN_KEY:
|
| 627 |
logger.error("[SYSTEM] 未配置 ADMIN_KEY 环境变量,请设置后重启")
|
| 628 |
import sys
|
| 629 |
sys.exit(1)
|
| 630 |
|
| 631 |
# 启动日志
|
| 632 |
-
|
| 633 |
-
logger.info(f"[SYSTEM]
|
| 634 |
-
logger.info(f"[SYSTEM]
|
| 635 |
-
logger.info("[SYSTEM]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
logger.info("[SYSTEM] 系统初始化完成")
|
| 637 |
|
| 638 |
# ---------- JWT 管理 ----------
|
| 639 |
-
|
| 640 |
-
def __init__(self, config: AccountConfig) -> None:
|
| 641 |
-
self.config = config
|
| 642 |
-
self.jwt: str = ""
|
| 643 |
-
self.expires: float = 0
|
| 644 |
-
self._lock = asyncio.Lock()
|
| 645 |
-
|
| 646 |
-
async def get(self, request_id: str = "") -> str:
|
| 647 |
-
async with self._lock:
|
| 648 |
-
if time.time() > self.expires:
|
| 649 |
-
await self._refresh(request_id)
|
| 650 |
-
return self.jwt
|
| 651 |
-
|
| 652 |
-
async def _refresh(self, request_id: str = "") -> None:
|
| 653 |
-
cookie = f"__Secure-C_SES={self.config.secure_c_ses}"
|
| 654 |
-
if self.config.host_c_oses:
|
| 655 |
-
cookie += f"; __Host-C_OSES={self.config.host_c_oses}"
|
| 656 |
-
|
| 657 |
-
req_tag = f"[req_{request_id}] " if request_id else ""
|
| 658 |
-
r = await http_client.get(
|
| 659 |
-
"https://business.gemini.google/auth/getoxsrf",
|
| 660 |
-
params={"csesidx": self.config.csesidx},
|
| 661 |
-
headers={
|
| 662 |
-
"cookie": cookie,
|
| 663 |
-
"user-agent": USER_AGENT,
|
| 664 |
-
"referer": "https://business.gemini.google/"
|
| 665 |
-
},
|
| 666 |
-
)
|
| 667 |
-
if r.status_code != 200:
|
| 668 |
-
logger.error(f"[AUTH] [{self.config.account_id}] {req_tag}JWT 刷新失败: {r.status_code}")
|
| 669 |
-
raise HTTPException(r.status_code, "getoxsrf failed")
|
| 670 |
-
|
| 671 |
-
txt = r.text[4:] if r.text.startswith(")]}'") else r.text
|
| 672 |
-
data = json.loads(txt)
|
| 673 |
-
|
| 674 |
-
key_bytes = base64.urlsafe_b64decode(data["xsrfToken"] + "==")
|
| 675 |
-
self.jwt = create_jwt(key_bytes, data["keyId"], self.config.csesidx)
|
| 676 |
-
self.expires = time.time() + 270
|
| 677 |
-
logger.info(f"[AUTH] [{self.config.account_id}] {req_tag}JWT 刷新成功")
|
| 678 |
|
| 679 |
# ---------- Session & File 管理 ----------
|
| 680 |
-
|
| 681 |
-
jwt = await account_manager.get_jwt(request_id)
|
| 682 |
-
headers = get_common_headers(jwt)
|
| 683 |
-
body = {
|
| 684 |
-
"configId": account_manager.config.config_id,
|
| 685 |
-
"additionalParams": {"token": "-"},
|
| 686 |
-
"createSessionRequest": {
|
| 687 |
-
"session": {"name": "", "displayName": ""}
|
| 688 |
-
}
|
| 689 |
-
}
|
| 690 |
-
|
| 691 |
-
req_tag = f"[req_{request_id}] " if request_id else ""
|
| 692 |
-
r = await http_client.post(
|
| 693 |
-
"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetCreateSession",
|
| 694 |
-
headers=headers,
|
| 695 |
-
json=body,
|
| 696 |
-
)
|
| 697 |
-
if r.status_code != 200:
|
| 698 |
-
logger.error(f"[SESSION] [{account_manager.config.account_id}] {req_tag}Session 创建失败: {r.status_code}")
|
| 699 |
-
raise HTTPException(r.status_code, "createSession failed")
|
| 700 |
-
sess_name = r.json()["session"]["name"]
|
| 701 |
-
logger.info(f"[SESSION] [{account_manager.config.account_id}] {req_tag}创建成功: {sess_name[-12:]}")
|
| 702 |
-
return sess_name
|
| 703 |
-
|
| 704 |
-
async def upload_context_file(session_name: str, mime_type: str, base64_content: str, account_manager: AccountManager, request_id: str = "") -> str:
|
| 705 |
-
"""上传文件到指定 Session,返回 fileId"""
|
| 706 |
-
jwt = await account_manager.get_jwt(request_id)
|
| 707 |
-
headers = get_common_headers(jwt)
|
| 708 |
-
|
| 709 |
-
# 生成随机文件名
|
| 710 |
-
ext = mime_type.split('/')[-1] if '/' in mime_type else "bin"
|
| 711 |
-
file_name = f"upload_{int(time.time())}_{uuid.uuid4().hex[:6]}.{ext}"
|
| 712 |
-
|
| 713 |
-
body = {
|
| 714 |
-
"configId": account_manager.config.config_id,
|
| 715 |
-
"additionalParams": {"token": "-"},
|
| 716 |
-
"addContextFileRequest": {
|
| 717 |
-
"name": session_name,
|
| 718 |
-
"fileName": file_name,
|
| 719 |
-
"mimeType": mime_type,
|
| 720 |
-
"fileContents": base64_content
|
| 721 |
-
}
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
r = await http_client.post(
|
| 725 |
-
"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetAddContextFile",
|
| 726 |
-
headers=headers,
|
| 727 |
-
json=body,
|
| 728 |
-
)
|
| 729 |
-
|
| 730 |
-
req_tag = f"[req_{request_id}] " if request_id else ""
|
| 731 |
-
if r.status_code != 200:
|
| 732 |
-
logger.error(f"[FILE] [{account_manager.config.account_id}] {req_tag}文件上传失败: {r.status_code}")
|
| 733 |
-
raise HTTPException(r.status_code, f"Upload failed: {r.text}")
|
| 734 |
-
|
| 735 |
-
data = r.json()
|
| 736 |
-
file_id = data.get("addContextFileResponse", {}).get("fileId")
|
| 737 |
-
logger.info(f"[FILE] [{account_manager.config.account_id}] {req_tag}文件上传成功: {mime_type}")
|
| 738 |
-
return file_id
|
| 739 |
|
| 740 |
# ---------- 消息处理逻辑 ----------
|
| 741 |
-
|
| 742 |
-
"""
|
| 743 |
-
生成对话指纹(使用前3条消息,平衡唯一性和Session复用)
|
| 744 |
-
|
| 745 |
-
策略:
|
| 746 |
-
1. 使用前3条消息生成指纹(而非仅第1条)
|
| 747 |
-
2. 大幅降低不同用户共享Session的概率
|
| 748 |
-
3. 保持Session复用能力(后续消息仍能找到同一Session)
|
| 749 |
-
"""
|
| 750 |
-
if not messages:
|
| 751 |
-
return "empty"
|
| 752 |
-
|
| 753 |
-
# 提取前3条消息的关键信息(角色+内容)
|
| 754 |
-
message_fingerprints = []
|
| 755 |
-
for msg in messages[:3]: # 只取前3条
|
| 756 |
-
role = msg.get("role", "")
|
| 757 |
-
content = msg.get("content", "")
|
| 758 |
-
|
| 759 |
-
# 统一处理内容格式(字符串或数组)
|
| 760 |
-
if isinstance(content, list):
|
| 761 |
-
# 多模态消息:只提取文本部分
|
| 762 |
-
text = "".join([x.get("text", "") for x in content if x.get("type") == "text"])
|
| 763 |
-
else:
|
| 764 |
-
text = str(content)
|
| 765 |
-
|
| 766 |
-
# 标准化:去除首尾空白,转小写
|
| 767 |
-
text = text.strip().lower()
|
| 768 |
-
|
| 769 |
-
# 组合角色和内容
|
| 770 |
-
message_fingerprints.append(f"{role}:{text}")
|
| 771 |
-
|
| 772 |
-
# 使用前3条消息生成指纹
|
| 773 |
-
conversation_prefix = "|".join(message_fingerprints)
|
| 774 |
-
return hashlib.md5(conversation_prefix.encode()).hexdigest()
|
| 775 |
-
|
| 776 |
-
async def parse_last_message(messages: List['Message'], request_id: str = ""):
|
| 777 |
-
"""解析最后一条消息,分离文本和文件(支持图片、PDF、文档等,base64 和 URL)"""
|
| 778 |
-
if not messages:
|
| 779 |
-
return "", []
|
| 780 |
-
|
| 781 |
-
last_msg = messages[-1]
|
| 782 |
-
content = last_msg.content
|
| 783 |
-
|
| 784 |
-
text_content = ""
|
| 785 |
-
images = [] # List of {"mime": str, "data": str_base64} - 兼容变量名,实际支持所有文件
|
| 786 |
-
image_urls = [] # 需要下载的 URL - 兼容变量名,实际支持所有文件
|
| 787 |
-
|
| 788 |
-
if isinstance(content, str):
|
| 789 |
-
text_content = content
|
| 790 |
-
elif isinstance(content, list):
|
| 791 |
-
for part in content:
|
| 792 |
-
if part.get("type") == "text":
|
| 793 |
-
text_content += part.get("text", "")
|
| 794 |
-
elif part.get("type") == "image_url":
|
| 795 |
-
url = part.get("image_url", {}).get("url", "")
|
| 796 |
-
# 解析 Data URI: data:mime/type;base64,xxxxxx (支持所有 MIME 类型)
|
| 797 |
-
match = re.match(r"data:([^;]+);base64,(.+)", url)
|
| 798 |
-
if match:
|
| 799 |
-
images.append({"mime": match.group(1), "data": match.group(2)})
|
| 800 |
-
elif url.startswith(("http://", "https://")):
|
| 801 |
-
image_urls.append(url)
|
| 802 |
-
else:
|
| 803 |
-
logger.warning(f"[FILE] [req_{request_id}] 不支持的文件格式: {url[:30]}...")
|
| 804 |
-
|
| 805 |
-
# 并行下载所有 URL 文件(支持图片、PDF、文档等)
|
| 806 |
-
if image_urls:
|
| 807 |
-
async def download_url(url: str):
|
| 808 |
-
try:
|
| 809 |
-
resp = await http_client.get(url, timeout=30, follow_redirects=True)
|
| 810 |
-
resp.raise_for_status()
|
| 811 |
-
content_type = resp.headers.get("content-type", "application/octet-stream").split(";")[0]
|
| 812 |
-
# 移除图片类型限制,支持所有文件类型
|
| 813 |
-
b64 = base64.b64encode(resp.content).decode()
|
| 814 |
-
logger.info(f"[FILE] [req_{request_id}] URL文件下载成功: {url[:50]}... ({len(resp.content)} bytes, {content_type})")
|
| 815 |
-
return {"mime": content_type, "data": b64}
|
| 816 |
-
except Exception as e:
|
| 817 |
-
logger.warning(f"[FILE] [req_{request_id}] URL文件下载失败: {url[:50]}... - {e}")
|
| 818 |
-
return None
|
| 819 |
-
|
| 820 |
-
results = await asyncio.gather(*[download_url(u) for u in image_urls])
|
| 821 |
-
images.extend([r for r in results if r])
|
| 822 |
-
|
| 823 |
-
return text_content, images
|
| 824 |
-
|
| 825 |
-
def build_full_context_text(messages: List['Message']) -> str:
|
| 826 |
-
"""仅拼接历史文本,图片只处理当次请求的"""
|
| 827 |
-
prompt = ""
|
| 828 |
-
for msg in messages:
|
| 829 |
-
role = "User" if msg.role in ["user", "system"] else "Assistant"
|
| 830 |
-
content_str = ""
|
| 831 |
-
if isinstance(msg.content, str):
|
| 832 |
-
content_str = msg.content
|
| 833 |
-
elif isinstance(msg.content, list):
|
| 834 |
-
for part in msg.content:
|
| 835 |
-
if part.get("type") == "text":
|
| 836 |
-
content_str += part.get("text", "")
|
| 837 |
-
elif part.get("type") == "image_url":
|
| 838 |
-
content_str += "[图片]"
|
| 839 |
-
|
| 840 |
-
prompt += f"{role}: {content_str}\n\n"
|
| 841 |
-
return prompt
|
| 842 |
|
| 843 |
# ---------- OpenAI 兼容接口 ----------
|
| 844 |
app = FastAPI(title="Gemini-Business OpenAI Gateway")
|
| 845 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
# ---------- Uptime 追踪中间件 ----------
|
| 847 |
@app.middleware("http")
|
| 848 |
async def track_uptime_middleware(request: Request, call_next):
|
|
@@ -887,9 +285,9 @@ async def track_uptime_middleware(request: Request, call_next):
|
|
| 887 |
os.makedirs(IMAGE_DIR, exist_ok=True)
|
| 888 |
app.mount("/images", StaticFiles(directory=IMAGE_DIR), name="images")
|
| 889 |
if IMAGE_DIR == "/data/images":
|
| 890 |
-
logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (持久化
|
| 891 |
else:
|
| 892 |
-
logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (
|
| 893 |
|
| 894 |
# ---------- 后台任务启动 ----------
|
| 895 |
@app.on_event("startup")
|
|
@@ -1145,99 +543,106 @@ def create_chunk(id: str, created: int, model: str, delta: dict, finish_reason:
|
|
| 1145 |
}
|
| 1146 |
return json.dumps(chunk)
|
| 1147 |
|
| 1148 |
-
# ---------- API Key 验证 ----------
|
| 1149 |
-
def verify_api_key(authorization: str = None):
|
| 1150 |
-
"""验证 API Key(如果配置了 API_KEY)"""
|
| 1151 |
-
# 如果未配置 API_KEY,则跳过验证
|
| 1152 |
-
if API_KEY is None:
|
| 1153 |
-
return True
|
| 1154 |
-
|
| 1155 |
-
# 检查 Authorization header
|
| 1156 |
-
if not authorization:
|
| 1157 |
-
raise HTTPException(
|
| 1158 |
-
status_code=401,
|
| 1159 |
-
detail="Missing Authorization header"
|
| 1160 |
-
)
|
| 1161 |
-
|
| 1162 |
-
# 支持两种格式:
|
| 1163 |
-
# 1. Bearer YOUR_API_KEY
|
| 1164 |
-
# 2. YOUR_API_KEY
|
| 1165 |
-
token = authorization
|
| 1166 |
-
if authorization.startswith("Bearer "):
|
| 1167 |
-
token = authorization[7:]
|
| 1168 |
-
|
| 1169 |
-
if token != API_KEY:
|
| 1170 |
-
logger.warning(f"[AUTH] API Key 验证失败")
|
| 1171 |
-
raise HTTPException(
|
| 1172 |
-
status_code=401,
|
| 1173 |
-
detail="Invalid API Key"
|
| 1174 |
-
)
|
| 1175 |
-
|
| 1176 |
-
return True
|
| 1177 |
-
|
| 1178 |
@app.get("/")
|
| 1179 |
async def home(request: Request):
|
| 1180 |
-
"""首页 -
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
raise HTTPException(404, "Not Found")
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1194 |
html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=False)
|
| 1195 |
return HTMLResponse(content=html_content)
|
| 1196 |
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
|
|
|
|
|
|
| 1202 |
|
| 1203 |
-
|
| 1204 |
-
now = int(time.time())
|
| 1205 |
-
for m in MODEL_MAPPING.keys():
|
| 1206 |
-
data.append({
|
| 1207 |
-
"id": m,
|
| 1208 |
-
"object": "model",
|
| 1209 |
-
"created": now,
|
| 1210 |
-
"owned_by": "google",
|
| 1211 |
-
"permission": []
|
| 1212 |
-
})
|
| 1213 |
-
return {"object": "list", "data": data}
|
| 1214 |
-
|
| 1215 |
-
@app.get("/{path_prefix}/v1/models/{model_id}")
|
| 1216 |
-
@require_path_prefix(PATH_PREFIX)
|
| 1217 |
-
async def get_model(path_prefix: str, model_id: str, authorization: str = Header(None)):
|
| 1218 |
-
# 验证 API Key
|
| 1219 |
-
verify_api_key(authorization)
|
| 1220 |
-
|
| 1221 |
-
return {"id": model_id, "object": "model"}
|
| 1222 |
|
| 1223 |
-
@app.get("/
|
| 1224 |
-
@
|
| 1225 |
-
async def admin_health(
|
| 1226 |
return {"status": "ok", "time": datetime.utcnow().isoformat()}
|
| 1227 |
|
| 1228 |
-
@app.get("/
|
| 1229 |
-
@
|
| 1230 |
-
async def admin_get_accounts(
|
| 1231 |
"""获取所有账户的状态信息"""
|
| 1232 |
accounts_info = []
|
| 1233 |
for account_id, account_manager in multi_account_mgr.accounts.items():
|
| 1234 |
config = account_manager.config
|
| 1235 |
remaining_hours = config.get_remaining_hours()
|
| 1236 |
-
|
| 1237 |
-
# 使用统一的格式化函数
|
| 1238 |
status, status_color, remaining_display = format_account_expiration(remaining_hours)
|
| 1239 |
-
|
| 1240 |
-
# 使用AccountManager的方法获取冷却信息
|
| 1241 |
cooldown_seconds, cooldown_reason = account_manager.get_cooldown_info()
|
| 1242 |
|
| 1243 |
accounts_info.append({
|
|
@@ -1248,20 +653,17 @@ async def admin_get_accounts(path_prefix: str, key: str = None, authorization: s
|
|
| 1248 |
"remaining_display": remaining_display,
|
| 1249 |
"is_available": account_manager.is_available,
|
| 1250 |
"error_count": account_manager.error_count,
|
| 1251 |
-
"disabled": config.disabled,
|
| 1252 |
-
"cooldown_seconds": cooldown_seconds,
|
| 1253 |
-
"cooldown_reason": cooldown_reason,
|
| 1254 |
-
"conversation_count": account_manager.conversation_count
|
| 1255 |
})
|
| 1256 |
|
| 1257 |
-
return {
|
| 1258 |
-
"total": len(accounts_info),
|
| 1259 |
-
"accounts": accounts_info
|
| 1260 |
-
}
|
| 1261 |
|
| 1262 |
-
@app.get("/
|
| 1263 |
-
@
|
| 1264 |
-
async def admin_get_config(
|
| 1265 |
"""获取完整账户配置"""
|
| 1266 |
try:
|
| 1267 |
accounts_data = load_accounts_from_source()
|
|
@@ -1270,181 +672,259 @@ async def admin_get_config(path_prefix: str, key: str = None, authorization: str
|
|
| 1270 |
logger.error(f"[CONFIG] 获取配置失败: {str(e)}")
|
| 1271 |
raise HTTPException(500, f"获取失败: {str(e)}")
|
| 1272 |
|
| 1273 |
-
@app.put("/
|
| 1274 |
-
@
|
| 1275 |
-
async def admin_update_config(
|
| 1276 |
"""更新整个账户配置"""
|
|
|
|
| 1277 |
try:
|
| 1278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1279 |
return {"status": "success", "message": "配置已更新", "account_count": len(multi_account_mgr.accounts)}
|
| 1280 |
except Exception as e:
|
| 1281 |
logger.error(f"[CONFIG] 更新配置失败: {str(e)}")
|
| 1282 |
raise HTTPException(500, f"更新失败: {str(e)}")
|
| 1283 |
|
| 1284 |
-
@app.delete("/
|
| 1285 |
-
@
|
| 1286 |
-
async def admin_delete_account(
|
| 1287 |
"""删除单个账户"""
|
|
|
|
| 1288 |
try:
|
| 1289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1290 |
return {"status": "success", "message": f"账户 {account_id} 已删除", "account_count": len(multi_account_mgr.accounts)}
|
| 1291 |
except Exception as e:
|
| 1292 |
logger.error(f"[CONFIG] 删除账户失败: {str(e)}")
|
| 1293 |
raise HTTPException(500, f"删除失败: {str(e)}")
|
| 1294 |
|
| 1295 |
-
@app.put("/
|
| 1296 |
-
@
|
| 1297 |
-
async def admin_disable_account(
|
| 1298 |
"""手动禁用账户"""
|
|
|
|
| 1299 |
try:
|
| 1300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1301 |
return {"status": "success", "message": f"账户 {account_id} 已禁用", "account_count": len(multi_account_mgr.accounts)}
|
| 1302 |
except Exception as e:
|
| 1303 |
logger.error(f"[CONFIG] 禁用账户失败: {str(e)}")
|
| 1304 |
raise HTTPException(500, f"禁用失败: {str(e)}")
|
| 1305 |
|
| 1306 |
-
@app.put("/
|
| 1307 |
-
@
|
| 1308 |
-
async def admin_enable_account(
|
| 1309 |
"""启用账户"""
|
|
|
|
| 1310 |
try:
|
| 1311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1312 |
return {"status": "success", "message": f"账户 {account_id} 已启用", "account_count": len(multi_account_mgr.accounts)}
|
| 1313 |
except Exception as e:
|
| 1314 |
logger.error(f"[CONFIG] 启用账户失败: {str(e)}")
|
| 1315 |
raise HTTPException(500, f"启用失败: {str(e)}")
|
| 1316 |
|
| 1317 |
-
@app.get("/
|
| 1318 |
-
@
|
| 1319 |
async def admin_get_logs(
|
| 1320 |
-
|
| 1321 |
limit: int = 1500,
|
| 1322 |
-
key: str = None,
|
| 1323 |
-
authorization: str = Header(None),
|
| 1324 |
level: str = None,
|
| 1325 |
search: str = None,
|
| 1326 |
start_time: str = None,
|
| 1327 |
end_time: str = None
|
| 1328 |
):
|
| 1329 |
-
"""
|
| 1330 |
-
获取系统日志(包含统计信息)
|
| 1331 |
-
|
| 1332 |
-
参数:
|
| 1333 |
-
- limit: 返回最近 N 条日志 (默认 1500, 最大 3000)
|
| 1334 |
-
- level: 过滤日志级别 (INFO, WARNING, ERROR, DEBUG)
|
| 1335 |
-
- search: 搜索关键词(在消息中搜索)
|
| 1336 |
-
- start_time: 开始时间 (格式: 2025-12-17 10:00:00)
|
| 1337 |
-
- end_time: 结束时间 (格式: 2025-12-17 11:00:00)
|
| 1338 |
-
"""
|
| 1339 |
with log_lock:
|
| 1340 |
logs = list(log_buffer)
|
| 1341 |
|
| 1342 |
-
# 计算统计信息(在过滤前)
|
| 1343 |
stats_by_level = {}
|
| 1344 |
error_logs = []
|
| 1345 |
chat_count = 0
|
| 1346 |
for log in logs:
|
| 1347 |
level_name = log.get("level", "INFO")
|
| 1348 |
stats_by_level[level_name] = stats_by_level.get(level_name, 0) + 1
|
| 1349 |
-
|
| 1350 |
-
# 收集错误日志
|
| 1351 |
if level_name in ["ERROR", "CRITICAL"]:
|
| 1352 |
error_logs.append(log)
|
| 1353 |
-
|
| 1354 |
-
# 统计对话次数(匹配包含"收到请求"的日志)
|
| 1355 |
if "收到请求" in log.get("message", ""):
|
| 1356 |
chat_count += 1
|
| 1357 |
|
| 1358 |
-
# 按级别过滤
|
| 1359 |
if level:
|
| 1360 |
level = level.upper()
|
| 1361 |
logs = [log for log in logs if log["level"] == level]
|
| 1362 |
-
|
| 1363 |
-
# 按关键词搜索
|
| 1364 |
if search:
|
| 1365 |
logs = [log for log in logs if search.lower() in log["message"].lower()]
|
| 1366 |
-
|
| 1367 |
-
# 按时间范围过滤
|
| 1368 |
if start_time:
|
| 1369 |
logs = [log for log in logs if log["time"] >= start_time]
|
| 1370 |
if end_time:
|
| 1371 |
logs = [log for log in logs if log["time"] <= end_time]
|
| 1372 |
|
| 1373 |
-
# 限制数量(返回最近的)
|
| 1374 |
limit = min(limit, 3000)
|
| 1375 |
filtered_logs = logs[-limit:]
|
| 1376 |
|
| 1377 |
return {
|
| 1378 |
"total": len(filtered_logs),
|
| 1379 |
"limit": limit,
|
| 1380 |
-
"filters": {
|
| 1381 |
-
"level": level,
|
| 1382 |
-
"search": search,
|
| 1383 |
-
"start_time": start_time,
|
| 1384 |
-
"end_time": end_time
|
| 1385 |
-
},
|
| 1386 |
"logs": filtered_logs,
|
| 1387 |
"stats": {
|
| 1388 |
-
"memory": {
|
| 1389 |
-
|
| 1390 |
-
"by_level": stats_by_level,
|
| 1391 |
-
"capacity": log_buffer.maxlen
|
| 1392 |
-
},
|
| 1393 |
-
"errors": {
|
| 1394 |
-
"count": len(error_logs),
|
| 1395 |
-
"recent": error_logs[-10:] # 最近10条错误
|
| 1396 |
-
},
|
| 1397 |
"chat_count": chat_count
|
| 1398 |
}
|
| 1399 |
}
|
| 1400 |
|
| 1401 |
-
@app.delete("/
|
| 1402 |
-
@
|
| 1403 |
-
async def admin_clear_logs(
|
| 1404 |
-
"""
|
| 1405 |
-
清空所有日志(内存缓冲 + 文件)
|
| 1406 |
-
|
| 1407 |
-
参数:
|
| 1408 |
-
- confirm: 必须传入 "yes" 才能清空
|
| 1409 |
-
"""
|
| 1410 |
if confirm != "yes":
|
| 1411 |
-
raise HTTPException(
|
| 1412 |
-
status_code=400,
|
| 1413 |
-
detail="需要 confirm=yes 参数确认清空操作"
|
| 1414 |
-
)
|
| 1415 |
-
|
| 1416 |
-
# 清空内存缓冲
|
| 1417 |
with log_lock:
|
| 1418 |
cleared_count = len(log_buffer)
|
| 1419 |
log_buffer.clear()
|
| 1420 |
-
|
| 1421 |
logger.info("[LOG] 日志已清空")
|
|
|
|
| 1422 |
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
"cleared_count": cleared_count
|
| 1427 |
-
}
|
| 1428 |
-
|
| 1429 |
-
@app.get("/{path_prefix}/admin/log/html")
|
| 1430 |
-
async def admin_logs_html_route(path_prefix: str, key: str = None, authorization: str = Header(None)):
|
| 1431 |
"""返回美化的 HTML 日志查看界面"""
|
| 1432 |
-
return await templates.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1433 |
|
| 1434 |
-
@app.
|
| 1435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1436 |
async def chat(
|
| 1437 |
-
path_prefix: str,
|
| 1438 |
req: ChatRequest,
|
| 1439 |
request: Request,
|
| 1440 |
authorization: Optional[str] = Header(None)
|
| 1441 |
):
|
| 1442 |
-
#
|
| 1443 |
-
verify_api_key(authorization)
|
| 1444 |
-
|
| 1445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1446 |
request_id = str(uuid.uuid4())[:6]
|
| 1447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1448 |
# 记录请求统计
|
| 1449 |
async with stats_lock:
|
| 1450 |
global_stats["total_requests"] += 1
|
|
@@ -1463,7 +943,7 @@ async def chat(
|
|
| 1463 |
request.state.model = req.model
|
| 1464 |
|
| 1465 |
# 3. 生成会话指纹,获取Session锁(防止同一对话的并发请求冲突)
|
| 1466 |
-
conv_key = get_conversation_key([m.dict() for m in req.messages])
|
| 1467 |
session_lock = await multi_account_mgr.acquire_session_lock(conv_key)
|
| 1468 |
|
| 1469 |
# 4. 在锁的保护下检查缓存和处理Session(保证同一对话的请求串行化)
|
|
@@ -1485,7 +965,7 @@ async def chat(
|
|
| 1485 |
for attempt in range(max_account_tries):
|
| 1486 |
try:
|
| 1487 |
account_manager = await multi_account_mgr.get_account(None, request_id)
|
| 1488 |
-
google_session = await create_google_session(account_manager, request_id)
|
| 1489 |
# 线程安全地绑定账户到此对话
|
| 1490 |
await multi_account_mgr.set_session_cache(
|
| 1491 |
conv_key,
|
|
@@ -1531,7 +1011,7 @@ async def chat(
|
|
| 1531 |
logger.info(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 用户消息: {preview}")
|
| 1532 |
|
| 1533 |
# 3. 解析请求内容
|
| 1534 |
-
last_text, current_images = await parse_last_message(req.messages, request_id)
|
| 1535 |
|
| 1536 |
# 4. 准备文本内容
|
| 1537 |
if is_new_conversation:
|
|
@@ -1571,7 +1051,7 @@ async def chat(
|
|
| 1571 |
cached = multi_account_mgr.global_session_cache.get(conv_key)
|
| 1572 |
if not cached:
|
| 1573 |
logger.warning(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 缓存已清理,重建Session")
|
| 1574 |
-
new_sess = await create_google_session(account_manager, request_id)
|
| 1575 |
await multi_account_mgr.set_session_cache(
|
| 1576 |
conv_key,
|
| 1577 |
account_manager.config.account_id,
|
|
@@ -1587,7 +1067,7 @@ async def chat(
|
|
| 1587 |
# 注意:每次重试如果是新 Session,都需要重新上传图片
|
| 1588 |
if current_images and not current_file_ids:
|
| 1589 |
for img in current_images:
|
| 1590 |
-
fid = await upload_context_file(current_session, img["mime"], img["data"], account_manager, request_id)
|
| 1591 |
current_file_ids.append(fid)
|
| 1592 |
|
| 1593 |
# B. 准备文本 (重试模式下发全文)
|
|
@@ -1687,7 +1167,7 @@ async def chat(
|
|
| 1687 |
logger.info(f"[CHAT] [req_{request_id}] 切换账户: {account_manager.config.account_id} -> {new_account.config.account_id}")
|
| 1688 |
|
| 1689 |
# 创建新 Session
|
| 1690 |
-
new_sess = await create_google_session(new_account, request_id)
|
| 1691 |
|
| 1692 |
# 更新缓存绑定到新账户
|
| 1693 |
await multi_account_mgr.set_session_cache(
|
|
@@ -1794,131 +1274,6 @@ def parse_images_from_response(data_list: list) -> tuple[list, str]:
|
|
| 1794 |
return file_ids, session_name
|
| 1795 |
|
| 1796 |
|
| 1797 |
-
async def get_session_file_metadata(account_mgr: AccountManager, session_name: str, request_id: str = "") -> dict:
|
| 1798 |
-
"""获取session中的文件元数据,包括正确的session路径"""
|
| 1799 |
-
jwt = await account_mgr.get_jwt(request_id)
|
| 1800 |
-
headers = get_common_headers(jwt)
|
| 1801 |
-
body = {
|
| 1802 |
-
"configId": account_mgr.config.config_id,
|
| 1803 |
-
"additionalParams": {"token": "-"},
|
| 1804 |
-
"listSessionFileMetadataRequest": {
|
| 1805 |
-
"name": session_name,
|
| 1806 |
-
"filter": "file_origin_type = AI_GENERATED"
|
| 1807 |
-
}
|
| 1808 |
-
}
|
| 1809 |
-
|
| 1810 |
-
resp = await http_client.post(
|
| 1811 |
-
"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetListSessionFileMetadata",
|
| 1812 |
-
headers=headers,
|
| 1813 |
-
json=body
|
| 1814 |
-
)
|
| 1815 |
-
|
| 1816 |
-
if resp.status_code == 401:
|
| 1817 |
-
# JWT过期,刷新后重试
|
| 1818 |
-
jwt = await account_mgr.get_jwt(request_id)
|
| 1819 |
-
headers = get_common_headers(jwt)
|
| 1820 |
-
resp = await http_client.post(
|
| 1821 |
-
"https://biz-discoveryengine.googleapis.com/v1alpha/locations/global/widgetListSessionFileMetadata",
|
| 1822 |
-
headers=headers,
|
| 1823 |
-
json=body
|
| 1824 |
-
)
|
| 1825 |
-
|
| 1826 |
-
if resp.status_code != 200:
|
| 1827 |
-
logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 获取文件元数据失败: {resp.status_code}")
|
| 1828 |
-
return {}
|
| 1829 |
-
|
| 1830 |
-
data = resp.json()
|
| 1831 |
-
result = {}
|
| 1832 |
-
file_metadata_list = data.get("listSessionFileMetadataResponse", {}).get("fileMetadata", [])
|
| 1833 |
-
for fm in file_metadata_list:
|
| 1834 |
-
fid = fm.get("fileId")
|
| 1835 |
-
if fid:
|
| 1836 |
-
result[fid] = fm
|
| 1837 |
-
|
| 1838 |
-
return result
|
| 1839 |
-
|
| 1840 |
-
|
| 1841 |
-
def build_image_download_url(session_name: str, file_id: str) -> str:
|
| 1842 |
-
"""构造图片下载URL"""
|
| 1843 |
-
return f"https://biz-discoveryengine.googleapis.com/v1alpha/{session_name}:downloadFile?fileId={file_id}&alt=media"
|
| 1844 |
-
|
| 1845 |
-
|
| 1846 |
-
async def download_image_with_jwt(account_mgr: AccountManager, session_name: str, file_id: str, request_id: str = "", max_retries: int = 3) -> bytes:
|
| 1847 |
-
"""
|
| 1848 |
-
使用JWT认证下载图片(带超时和重试机制)
|
| 1849 |
-
|
| 1850 |
-
Args:
|
| 1851 |
-
account_mgr: 账户管理器
|
| 1852 |
-
session_name: Session名称
|
| 1853 |
-
file_id: 文件ID
|
| 1854 |
-
request_id: 请求ID
|
| 1855 |
-
max_retries: 最大重试次数(默认3次)
|
| 1856 |
-
|
| 1857 |
-
Returns:
|
| 1858 |
-
图片字节数据
|
| 1859 |
-
|
| 1860 |
-
Raises:
|
| 1861 |
-
HTTPException: 下载失败
|
| 1862 |
-
asyncio.TimeoutError: 超时
|
| 1863 |
-
"""
|
| 1864 |
-
url = build_image_download_url(session_name, file_id)
|
| 1865 |
-
logger.info(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 开始下载图片: {file_id[:8]}...")
|
| 1866 |
-
|
| 1867 |
-
for attempt in range(max_retries):
|
| 1868 |
-
try:
|
| 1869 |
-
# 3分钟超时(180秒)
|
| 1870 |
-
async with asyncio.timeout(180):
|
| 1871 |
-
jwt = await account_mgr.get_jwt(request_id)
|
| 1872 |
-
headers = get_common_headers(jwt)
|
| 1873 |
-
|
| 1874 |
-
# 复用全局http_client
|
| 1875 |
-
resp = await http_client.get(url, headers=headers, follow_redirects=True)
|
| 1876 |
-
|
| 1877 |
-
if resp.status_code == 401:
|
| 1878 |
-
# JWT过期,刷新后重试
|
| 1879 |
-
jwt = await account_mgr.get_jwt(request_id)
|
| 1880 |
-
headers = get_common_headers(jwt)
|
| 1881 |
-
resp = await http_client.get(url, headers=headers, follow_redirects=True)
|
| 1882 |
-
|
| 1883 |
-
resp.raise_for_status()
|
| 1884 |
-
logger.info(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载成功: {file_id[:8]}... ({len(resp.content)} bytes)")
|
| 1885 |
-
return resp.content
|
| 1886 |
-
|
| 1887 |
-
except asyncio.TimeoutError:
|
| 1888 |
-
logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载超时 (尝试 {attempt + 1}/{max_retries}): {file_id[:8]}...")
|
| 1889 |
-
if attempt == max_retries - 1:
|
| 1890 |
-
raise HTTPException(504, f"Image download timeout after {max_retries} attempts")
|
| 1891 |
-
await asyncio.sleep(2 ** attempt) # 指数退避:2s, 4s, 8s
|
| 1892 |
-
|
| 1893 |
-
except httpx.HTTPError as e:
|
| 1894 |
-
logger.warning(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载失败 (尝试 {attempt + 1}/{max_retries}): {type(e).__name__}")
|
| 1895 |
-
if attempt == max_retries - 1:
|
| 1896 |
-
raise HTTPException(500, f"Image download failed: {str(e)[:100]}")
|
| 1897 |
-
await asyncio.sleep(2 ** attempt) # 指数退避
|
| 1898 |
-
|
| 1899 |
-
except Exception as e:
|
| 1900 |
-
logger.error(f"[IMAGE] [{account_mgr.config.account_id}] [req_{request_id}] 图片下载异常: {type(e).__name__}: {str(e)[:100]}")
|
| 1901 |
-
raise
|
| 1902 |
-
|
| 1903 |
-
# 不应该到达这里
|
| 1904 |
-
raise HTTPException(500, "Image download failed unexpectedly")
|
| 1905 |
-
|
| 1906 |
-
|
| 1907 |
-
|
| 1908 |
-
def save_image_to_hf(image_data: bytes, chat_id: str, file_id: str, mime_type: str, base_url: str) -> str:
|
| 1909 |
-
"""保存图片到持久化存储,返回完整的公开URL"""
|
| 1910 |
-
ext_map = {"image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp"}
|
| 1911 |
-
ext = ext_map.get(mime_type, ".png")
|
| 1912 |
-
|
| 1913 |
-
filename = f"{chat_id}_{file_id}{ext}"
|
| 1914 |
-
save_path = os.path.join(IMAGE_DIR, filename)
|
| 1915 |
-
|
| 1916 |
-
# 目录已在启动时创建(Line 635),无需重复创建
|
| 1917 |
-
with open(save_path, "wb") as f:
|
| 1918 |
-
f.write(image_data)
|
| 1919 |
-
|
| 1920 |
-
return f"{base_url}/images/{filename}"
|
| 1921 |
-
|
| 1922 |
async def stream_chat_generator(session: str, text_content: str, file_ids: List[str], model_name: str, chat_id: str, created_time: int, account_manager: AccountManager, is_stream: bool = True, request_id: str = "", request: Request = None):
|
| 1923 |
start_time = time.time()
|
| 1924 |
|
|
@@ -1929,7 +1284,7 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
|
|
| 1929 |
logger.info(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 附带文件: {len(file_ids)}个")
|
| 1930 |
|
| 1931 |
jwt = await account_manager.get_jwt(request_id)
|
| 1932 |
-
headers = get_common_headers(jwt)
|
| 1933 |
|
| 1934 |
body = {
|
| 1935 |
"configId": account_manager.config.config_id,
|
|
@@ -2006,7 +1361,7 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
|
|
| 2006 |
|
| 2007 |
try:
|
| 2008 |
base_url = get_base_url(request) if request else ""
|
| 2009 |
-
file_metadata = await get_session_file_metadata(account_manager, session_name, request_id)
|
| 2010 |
|
| 2011 |
# 并行下载所有图片
|
| 2012 |
download_tasks = []
|
|
@@ -2015,7 +1370,7 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
|
|
| 2015 |
mime = file_info["mimeType"]
|
| 2016 |
meta = file_metadata.get(fid, {})
|
| 2017 |
correct_session = meta.get("session") or session_name
|
| 2018 |
-
task = download_image_with_jwt(account_manager, correct_session, fid, request_id)
|
| 2019 |
download_tasks.append((fid, mime, task))
|
| 2020 |
|
| 2021 |
results = await asyncio.gather(*[task for _, _, task in download_tasks], return_exceptions=True)
|
|
@@ -2026,7 +1381,7 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
|
|
| 2026 |
logger.error(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}下载失败: {type(result).__name__}")
|
| 2027 |
continue
|
| 2028 |
|
| 2029 |
-
image_url = save_image_to_hf(result, chat_id, fid, mime, base_url)
|
| 2030 |
logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}已保存: {image_url}")
|
| 2031 |
|
| 2032 |
markdown = f"\n\n\n\n"
|
|
@@ -2132,8 +1487,10 @@ async def get_public_logs(request: Request, limit: int = 100):
|
|
| 2132 |
# 记录新访问(24小时内同一IP只计数一次)
|
| 2133 |
if client_ip not in global_stats["visitor_ips"]:
|
| 2134 |
global_stats["visitor_ips"][client_ip] = current_time
|
| 2135 |
-
|
| 2136 |
-
|
|
|
|
|
|
|
| 2137 |
|
| 2138 |
sanitized_logs = get_sanitized_logs(limit=min(limit, 1000))
|
| 2139 |
return {
|
|
|
|
| 1 |
+
import json, time, os, asyncio, uuid, ssl, re
|
| 2 |
from datetime import datetime, timezone, timedelta
|
| 3 |
from typing import List, Optional, Union, Dict, Any
|
|
|
|
| 4 |
import logging
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
|
| 7 |
import httpx
|
| 8 |
import aiofiles
|
| 9 |
+
from fastapi import FastAPI, HTTPException, Header, Request, Body, Form
|
| 10 |
+
from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse, RedirectResponse
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from pydantic import BaseModel
|
| 13 |
from util.streaming_parser import parse_json_array_stream_async
|
| 14 |
from collections import deque
|
| 15 |
from threading import Lock
|
|
|
|
| 16 |
|
| 17 |
+
# 导入认证模块
|
| 18 |
+
from core.auth import verify_api_key
|
| 19 |
+
from core.session_auth import is_logged_in, login_user, logout_user, require_login, generate_session_secret
|
| 20 |
+
|
| 21 |
+
# 导入核心模块
|
| 22 |
+
from core.message import (
|
| 23 |
+
get_conversation_key,
|
| 24 |
+
parse_last_message,
|
| 25 |
+
build_full_context_text
|
| 26 |
+
)
|
| 27 |
+
from core.google_api import (
|
| 28 |
+
get_common_headers,
|
| 29 |
+
create_google_session,
|
| 30 |
+
upload_context_file,
|
| 31 |
+
get_session_file_metadata,
|
| 32 |
+
download_image_with_jwt,
|
| 33 |
+
save_image_to_hf
|
| 34 |
+
)
|
| 35 |
+
from core.account import (
|
| 36 |
+
AccountManager,
|
| 37 |
+
MultiAccountManager,
|
| 38 |
+
format_account_expiration,
|
| 39 |
+
load_multi_account_config,
|
| 40 |
+
load_accounts_from_source,
|
| 41 |
+
update_accounts_config as _update_accounts_config,
|
| 42 |
+
delete_account as _delete_account,
|
| 43 |
+
update_account_disabled_status as _update_account_disabled_status
|
| 44 |
+
)
|
| 45 |
|
| 46 |
# 导入 Uptime 追踪器
|
| 47 |
import uptime_tracker
|
|
|
|
| 53 |
log_lock = Lock()
|
| 54 |
|
| 55 |
# 统计数据持久化
|
| 56 |
+
STATS_FILE = "data/stats.json"
|
| 57 |
stats_lock = asyncio.Lock() # 改为异步锁
|
| 58 |
|
| 59 |
async def load_stats():
|
|
|
|
| 119 |
|
| 120 |
load_dotenv()
|
| 121 |
# ---------- 配置 ----------
|
| 122 |
+
PROXY = os.getenv("PROXY", "")
|
| 123 |
TIMEOUT_SECONDS = 600
|
| 124 |
+
API_KEY = os.getenv("API_KEY", "") # API 访问密钥(可选,用于保护API端点)
|
| 125 |
+
PATH_PREFIX = os.getenv("PATH_PREFIX", "") # 路径前缀(可选,用于隐藏端点路径)
|
| 126 |
+
ADMIN_KEY = os.getenv("ADMIN_KEY", "") # 管理员密钥(必需,用于登录)
|
| 127 |
+
BASE_URL = os.getenv("BASE_URL", "") # 服务器完整URL(可选,用于图片URL生成)
|
| 128 |
+
SESSION_SECRET_KEY = os.getenv("SESSION_SECRET_KEY", generate_session_secret()) # Session加密密钥(自动生成)
|
| 129 |
+
SESSION_EXPIRE_HOURS = int(os.getenv("SESSION_EXPIRE_HOURS", "24")) # Session过期时间(默认24小时)
|
| 130 |
|
| 131 |
# ---------- 公开展示配置 ----------
|
| 132 |
LOGO_URL = os.getenv("LOGO_URL", "") # Logo URL(公开,为空则不显示)
|
| 133 |
CHAT_URL = os.getenv("CHAT_URL", "") # 开始对话链接(公开,为空则不显示)
|
| 134 |
MODEL_NAME = os.getenv("MODEL_NAME", "gemini-business") # 模型名称(公开)
|
|
|
|
| 135 |
|
| 136 |
# ---------- 图片存储配置 ----------
|
|
|
|
| 137 |
if os.path.exists("/data"):
|
| 138 |
+
IMAGE_DIR = "/data/images" # HF Pro持久化存储
|
| 139 |
else:
|
| 140 |
+
IMAGE_DIR = "./data/images" # 本地持久化存储
|
| 141 |
|
| 142 |
# ---------- 重试配置 ----------
|
| 143 |
MAX_NEW_SESSION_TRIES = int(os.getenv("MAX_NEW_SESSION_TRIES", "5")) # 新会话创建最多尝试账户数(默认5)
|
|
|
|
| 158 |
|
| 159 |
# ---------- HTTP 客户端 ----------
|
| 160 |
http_client = httpx.AsyncClient(
|
| 161 |
+
proxy=PROXY or None,
|
| 162 |
verify=False,
|
| 163 |
http2=False,
|
| 164 |
timeout=httpx.Timeout(TIMEOUT_SECONDS, connect=60.0),
|
|
|
|
| 184 |
# ---------- 常量定义 ----------
|
| 185 |
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"
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
# ---------- 多账户支持 ----------
|
| 188 |
+
# (AccountConfig, AccountManager, MultiAccountManager 已移至 core/account.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
# ---------- 配置文件管理 ----------
|
| 191 |
+
# (配置管理函数已移至 core/account.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
# 初始化多账户管理器
|
| 194 |
+
multi_account_mgr = load_multi_account_config(
|
| 195 |
+
http_client,
|
| 196 |
+
USER_AGENT,
|
| 197 |
+
ACCOUNT_FAILURE_THRESHOLD,
|
| 198 |
+
RATE_LIMIT_COOLDOWN_SECONDS,
|
| 199 |
+
SESSION_CACHE_TTL_SECONDS,
|
| 200 |
+
global_stats
|
| 201 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
# 验证必需的环境变量
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
if not ADMIN_KEY:
|
| 205 |
logger.error("[SYSTEM] 未配置 ADMIN_KEY 环境变量,请设置后重启")
|
| 206 |
import sys
|
| 207 |
sys.exit(1)
|
| 208 |
|
| 209 |
# 启动日志
|
| 210 |
+
if PATH_PREFIX:
|
| 211 |
+
logger.info(f"[SYSTEM] 路径前缀已配置: {PATH_PREFIX[:4]}****")
|
| 212 |
+
logger.info(f"[SYSTEM] API端点: /{PATH_PREFIX}/v1/chat/completions")
|
| 213 |
+
logger.info(f"[SYSTEM] 管理端点: /{PATH_PREFIX}/")
|
| 214 |
+
else:
|
| 215 |
+
logger.info("[SYSTEM] 未配置路径前缀,使用默认路径")
|
| 216 |
+
logger.info("[SYSTEM] API端点: /v1/chat/completions")
|
| 217 |
+
logger.info("[SYSTEM] 管理端点: /admin/")
|
| 218 |
+
logger.info("[SYSTEM] 公开端点: /public/log/html, /public/stats, /public/uptime/html")
|
| 219 |
+
logger.info(f"[SYSTEM] Session过期时间: {SESSION_EXPIRE_HOURS}小时")
|
| 220 |
logger.info("[SYSTEM] 系统初始化完成")
|
| 221 |
|
| 222 |
# ---------- JWT 管理 ----------
|
| 223 |
+
# (JWTManager已移至 core/jwt.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
# ---------- Session & File 管理 ----------
|
| 226 |
+
# (Google API函数已移至 core/google_api.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
# ---------- 消息处理逻辑 ----------
|
| 229 |
+
# (消息处理函数已移至 core/message.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
# ---------- OpenAI 兼容接口 ----------
|
| 232 |
app = FastAPI(title="Gemini-Business OpenAI Gateway")
|
| 233 |
|
| 234 |
+
# ---------- Session 中间件配置 ----------
|
| 235 |
+
from starlette.middleware.sessions import SessionMiddleware
|
| 236 |
+
app.add_middleware(
|
| 237 |
+
SessionMiddleware,
|
| 238 |
+
secret_key=SESSION_SECRET_KEY,
|
| 239 |
+
max_age=SESSION_EXPIRE_HOURS * 3600, # 转换为秒
|
| 240 |
+
same_site="lax",
|
| 241 |
+
https_only=False # 本地开发可设为False,生产环境建议True
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
# ---------- Uptime 追踪中间件 ----------
|
| 245 |
@app.middleware("http")
|
| 246 |
async def track_uptime_middleware(request: Request, call_next):
|
|
|
|
| 285 |
os.makedirs(IMAGE_DIR, exist_ok=True)
|
| 286 |
app.mount("/images", StaticFiles(directory=IMAGE_DIR), name="images")
|
| 287 |
if IMAGE_DIR == "/data/images":
|
| 288 |
+
logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (HF Pro持久化)")
|
| 289 |
else:
|
| 290 |
+
logger.info(f"[SYSTEM] 图片静态服务已启用: /images/ -> {IMAGE_DIR} (本地持久化)")
|
| 291 |
|
| 292 |
# ---------- 后台任务启动 ----------
|
| 293 |
@app.on_event("startup")
|
|
|
|
| 543 |
}
|
| 544 |
return json.dumps(chunk)
|
| 545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
@app.get("/")
|
| 547 |
async def home(request: Request):
|
| 548 |
+
"""首页 - 根据PATH_PREFIX配置决定行为"""
|
| 549 |
+
if PATH_PREFIX:
|
| 550 |
+
# 如果设置了PATH_PREFIX(隐藏模式),首页返回404,不暴露任何信息
|
| 551 |
raise HTTPException(404, "Not Found")
|
| 552 |
+
else:
|
| 553 |
+
# 未设置PATH_PREFIX(公开模式),根据登录状态重定向
|
| 554 |
+
if is_logged_in(request):
|
| 555 |
+
return RedirectResponse(url="/admin", status_code=302)
|
| 556 |
+
else:
|
| 557 |
+
return RedirectResponse(url="/admin/login", status_code=302)
|
| 558 |
+
|
| 559 |
+
# ---------- 登录/登出端点(支持可选PATH_PREFIX) ----------
|
| 560 |
+
|
| 561 |
+
# 不带PATH_PREFIX的登录端点
|
| 562 |
+
@app.get("/admin/login")
|
| 563 |
+
async def admin_login_get(request: Request, error: str = None):
|
| 564 |
+
"""登录页面"""
|
| 565 |
+
return await templates.get_login_html(request, error)
|
| 566 |
+
|
| 567 |
+
@app.post("/admin/login")
|
| 568 |
+
async def admin_login_post(request: Request, admin_key: str = Form(...)):
|
| 569 |
+
"""处理登录表单提交"""
|
| 570 |
+
if admin_key == ADMIN_KEY:
|
| 571 |
+
login_user(request)
|
| 572 |
+
logger.info(f"[AUTH] 管理员登录成功")
|
| 573 |
+
return RedirectResponse(url="/admin", status_code=302)
|
| 574 |
+
else:
|
| 575 |
+
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 576 |
+
return await templates.get_login_html(request, error="密钥错误,请重试")
|
| 577 |
+
|
| 578 |
+
@app.post("/admin/logout")
|
| 579 |
+
@require_login(redirect_to_login=False)
|
| 580 |
+
async def admin_logout(request: Request):
|
| 581 |
+
"""登出"""
|
| 582 |
+
logout_user(request)
|
| 583 |
+
logger.info(f"[AUTH] 管理员已登出")
|
| 584 |
+
return RedirectResponse(url="/admin/login", status_code=302)
|
| 585 |
+
|
| 586 |
+
# 带PATH_PREFIX的登录端点(如果配置了PATH_PREFIX)
|
| 587 |
+
if PATH_PREFIX:
|
| 588 |
+
@app.get(f"/{PATH_PREFIX}/login")
|
| 589 |
+
async def admin_login_get_prefixed(request: Request, error: str = None):
|
| 590 |
+
"""登录页面(带前缀)"""
|
| 591 |
+
return await templates.get_login_html(request, error)
|
| 592 |
+
|
| 593 |
+
@app.post(f"/{PATH_PREFIX}/login")
|
| 594 |
+
async def admin_login_post_prefixed(request: Request, admin_key: str = Form(...)):
|
| 595 |
+
"""处理登录表单提交(带前缀)"""
|
| 596 |
+
if admin_key == ADMIN_KEY:
|
| 597 |
+
login_user(request)
|
| 598 |
+
logger.info(f"[AUTH] 管理员登录成功")
|
| 599 |
+
return RedirectResponse(url=f"/{PATH_PREFIX}", status_code=302)
|
| 600 |
+
else:
|
| 601 |
+
logger.warning(f"[AUTH] 登录失败 - 密钥错误")
|
| 602 |
+
return await templates.get_login_html(request, error="密钥错误,请重试")
|
| 603 |
+
|
| 604 |
+
@app.post(f"/{PATH_PREFIX}/logout")
|
| 605 |
+
@require_login(redirect_to_login=False)
|
| 606 |
+
async def admin_logout_prefixed(request: Request):
|
| 607 |
+
"""登出(带前缀)"""
|
| 608 |
+
logout_user(request)
|
| 609 |
+
logger.info(f"[AUTH] 管理员已登出")
|
| 610 |
+
return RedirectResponse(url=f"/{PATH_PREFIX}/login", status_code=302)
|
| 611 |
+
|
| 612 |
+
# ---------- 管理端点(需要登录) ----------
|
| 613 |
+
|
| 614 |
+
# 不带PATH_PREFIX的管理端点
|
| 615 |
+
@app.get("/admin")
|
| 616 |
+
@require_login()
|
| 617 |
+
async def admin_home_no_prefix(request: Request):
|
| 618 |
+
"""管理首页"""
|
| 619 |
html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=False)
|
| 620 |
return HTMLResponse(content=html_content)
|
| 621 |
|
| 622 |
+
# 带PATH_PREFIX的管理端点(如果配置了PATH_PREFIX)
|
| 623 |
+
if PATH_PREFIX:
|
| 624 |
+
@app.get(f"/{PATH_PREFIX}")
|
| 625 |
+
@require_login()
|
| 626 |
+
async def admin_home_prefixed(request: Request):
|
| 627 |
+
"""管理首页(带前缀)"""
|
| 628 |
+
return await admin_home_no_prefix(request=request)
|
| 629 |
|
| 630 |
+
# ---------- 管理API端点(需要登录) ----------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
+
@app.get("/admin/health")
|
| 633 |
+
@require_login()
|
| 634 |
+
async def admin_health(request: Request):
|
| 635 |
return {"status": "ok", "time": datetime.utcnow().isoformat()}
|
| 636 |
|
| 637 |
+
@app.get("/admin/accounts")
|
| 638 |
+
@require_login()
|
| 639 |
+
async def admin_get_accounts(request: Request):
|
| 640 |
"""获取所有账户的状态信息"""
|
| 641 |
accounts_info = []
|
| 642 |
for account_id, account_manager in multi_account_mgr.accounts.items():
|
| 643 |
config = account_manager.config
|
| 644 |
remaining_hours = config.get_remaining_hours()
|
|
|
|
|
|
|
| 645 |
status, status_color, remaining_display = format_account_expiration(remaining_hours)
|
|
|
|
|
|
|
| 646 |
cooldown_seconds, cooldown_reason = account_manager.get_cooldown_info()
|
| 647 |
|
| 648 |
accounts_info.append({
|
|
|
|
| 653 |
"remaining_display": remaining_display,
|
| 654 |
"is_available": account_manager.is_available,
|
| 655 |
"error_count": account_manager.error_count,
|
| 656 |
+
"disabled": config.disabled,
|
| 657 |
+
"cooldown_seconds": cooldown_seconds,
|
| 658 |
+
"cooldown_reason": cooldown_reason,
|
| 659 |
+
"conversation_count": account_manager.conversation_count
|
| 660 |
})
|
| 661 |
|
| 662 |
+
return {"total": len(accounts_info), "accounts": accounts_info}
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
+
@app.get("/admin/accounts-config")
|
| 665 |
+
@require_login()
|
| 666 |
+
async def admin_get_config(request: Request):
|
| 667 |
"""获取完整账户配置"""
|
| 668 |
try:
|
| 669 |
accounts_data = load_accounts_from_source()
|
|
|
|
| 672 |
logger.error(f"[CONFIG] 获取配置失败: {str(e)}")
|
| 673 |
raise HTTPException(500, f"获取失败: {str(e)}")
|
| 674 |
|
| 675 |
+
@app.put("/admin/accounts-config")
|
| 676 |
+
@require_login()
|
| 677 |
+
async def admin_update_config(request: Request, accounts_data: list = Body(...)):
|
| 678 |
"""更新整个账户配置"""
|
| 679 |
+
global multi_account_mgr
|
| 680 |
try:
|
| 681 |
+
multi_account_mgr = _update_accounts_config(
|
| 682 |
+
accounts_data, multi_account_mgr, http_client, USER_AGENT,
|
| 683 |
+
ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS,
|
| 684 |
+
SESSION_CACHE_TTL_SECONDS, global_stats
|
| 685 |
+
)
|
| 686 |
return {"status": "success", "message": "配置已更新", "account_count": len(multi_account_mgr.accounts)}
|
| 687 |
except Exception as e:
|
| 688 |
logger.error(f"[CONFIG] 更新配置失败: {str(e)}")
|
| 689 |
raise HTTPException(500, f"更新失败: {str(e)}")
|
| 690 |
|
| 691 |
+
@app.delete("/admin/accounts/{account_id}")
|
| 692 |
+
@require_login()
|
| 693 |
+
async def admin_delete_account(request: Request, account_id: str):
|
| 694 |
"""删除单个账户"""
|
| 695 |
+
global multi_account_mgr
|
| 696 |
try:
|
| 697 |
+
multi_account_mgr = _delete_account(
|
| 698 |
+
account_id, multi_account_mgr, http_client, USER_AGENT,
|
| 699 |
+
ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS,
|
| 700 |
+
SESSION_CACHE_TTL_SECONDS, global_stats
|
| 701 |
+
)
|
| 702 |
return {"status": "success", "message": f"账户 {account_id} 已删除", "account_count": len(multi_account_mgr.accounts)}
|
| 703 |
except Exception as e:
|
| 704 |
logger.error(f"[CONFIG] 删除账户失败: {str(e)}")
|
| 705 |
raise HTTPException(500, f"删除失败: {str(e)}")
|
| 706 |
|
| 707 |
+
@app.put("/admin/accounts/{account_id}/disable")
|
| 708 |
+
@require_login()
|
| 709 |
+
async def admin_disable_account(request: Request, account_id: str):
|
| 710 |
"""手动禁用账户"""
|
| 711 |
+
global multi_account_mgr
|
| 712 |
try:
|
| 713 |
+
multi_account_mgr = _update_account_disabled_status(
|
| 714 |
+
account_id, True, multi_account_mgr, http_client, USER_AGENT,
|
| 715 |
+
ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS,
|
| 716 |
+
SESSION_CACHE_TTL_SECONDS, global_stats
|
| 717 |
+
)
|
| 718 |
return {"status": "success", "message": f"账户 {account_id} 已禁用", "account_count": len(multi_account_mgr.accounts)}
|
| 719 |
except Exception as e:
|
| 720 |
logger.error(f"[CONFIG] 禁用账户失败: {str(e)}")
|
| 721 |
raise HTTPException(500, f"禁用失败: {str(e)}")
|
| 722 |
|
| 723 |
+
@app.put("/admin/accounts/{account_id}/enable")
|
| 724 |
+
@require_login()
|
| 725 |
+
async def admin_enable_account(request: Request, account_id: str):
|
| 726 |
"""启用账户"""
|
| 727 |
+
global multi_account_mgr
|
| 728 |
try:
|
| 729 |
+
multi_account_mgr = _update_account_disabled_status(
|
| 730 |
+
account_id, False, multi_account_mgr, http_client, USER_AGENT,
|
| 731 |
+
ACCOUNT_FAILURE_THRESHOLD, RATE_LIMIT_COOLDOWN_SECONDS,
|
| 732 |
+
SESSION_CACHE_TTL_SECONDS, global_stats
|
| 733 |
+
)
|
| 734 |
return {"status": "success", "message": f"账户 {account_id} 已启用", "account_count": len(multi_account_mgr.accounts)}
|
| 735 |
except Exception as e:
|
| 736 |
logger.error(f"[CONFIG] 启用账户失败: {str(e)}")
|
| 737 |
raise HTTPException(500, f"启用失败: {str(e)}")
|
| 738 |
|
| 739 |
+
@app.get("/admin/log")
|
| 740 |
+
@require_login()
|
| 741 |
async def admin_get_logs(
|
| 742 |
+
request: Request,
|
| 743 |
limit: int = 1500,
|
|
|
|
|
|
|
| 744 |
level: str = None,
|
| 745 |
search: str = None,
|
| 746 |
start_time: str = None,
|
| 747 |
end_time: str = None
|
| 748 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
with log_lock:
|
| 750 |
logs = list(log_buffer)
|
| 751 |
|
|
|
|
| 752 |
stats_by_level = {}
|
| 753 |
error_logs = []
|
| 754 |
chat_count = 0
|
| 755 |
for log in logs:
|
| 756 |
level_name = log.get("level", "INFO")
|
| 757 |
stats_by_level[level_name] = stats_by_level.get(level_name, 0) + 1
|
|
|
|
|
|
|
| 758 |
if level_name in ["ERROR", "CRITICAL"]:
|
| 759 |
error_logs.append(log)
|
|
|
|
|
|
|
| 760 |
if "收到请求" in log.get("message", ""):
|
| 761 |
chat_count += 1
|
| 762 |
|
|
|
|
| 763 |
if level:
|
| 764 |
level = level.upper()
|
| 765 |
logs = [log for log in logs if log["level"] == level]
|
|
|
|
|
|
|
| 766 |
if search:
|
| 767 |
logs = [log for log in logs if search.lower() in log["message"].lower()]
|
|
|
|
|
|
|
| 768 |
if start_time:
|
| 769 |
logs = [log for log in logs if log["time"] >= start_time]
|
| 770 |
if end_time:
|
| 771 |
logs = [log for log in logs if log["time"] <= end_time]
|
| 772 |
|
|
|
|
| 773 |
limit = min(limit, 3000)
|
| 774 |
filtered_logs = logs[-limit:]
|
| 775 |
|
| 776 |
return {
|
| 777 |
"total": len(filtered_logs),
|
| 778 |
"limit": limit,
|
| 779 |
+
"filters": {"level": level, "search": search, "start_time": start_time, "end_time": end_time},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 780 |
"logs": filtered_logs,
|
| 781 |
"stats": {
|
| 782 |
+
"memory": {"total": len(log_buffer), "by_level": stats_by_level, "capacity": log_buffer.maxlen},
|
| 783 |
+
"errors": {"count": len(error_logs), "recent": error_logs[-10:]},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
"chat_count": chat_count
|
| 785 |
}
|
| 786 |
}
|
| 787 |
|
| 788 |
+
@app.delete("/admin/log")
|
| 789 |
+
@require_login()
|
| 790 |
+
async def admin_clear_logs(request: Request, confirm: str = None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
if confirm != "yes":
|
| 792 |
+
raise HTTPException(400, "需要 confirm=yes 参数确认清空操作")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
with log_lock:
|
| 794 |
cleared_count = len(log_buffer)
|
| 795 |
log_buffer.clear()
|
|
|
|
| 796 |
logger.info("[LOG] 日志已清空")
|
| 797 |
+
return {"status": "success", "message": "已清空内存日志", "cleared_count": cleared_count}
|
| 798 |
|
| 799 |
+
@app.get("/admin/log/html")
|
| 800 |
+
@require_login()
|
| 801 |
+
async def admin_logs_html_route(request: Request):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
"""返回美化的 HTML 日志查看界面"""
|
| 803 |
+
return await templates.admin_logs_html_no_auth(request)
|
| 804 |
+
|
| 805 |
+
# 带PATH_PREFIX的管理API端点(如果配置了PATH_PREFIX)
|
| 806 |
+
if PATH_PREFIX:
|
| 807 |
+
@app.get(f"/{PATH_PREFIX}/health")
|
| 808 |
+
@require_login()
|
| 809 |
+
async def admin_health_prefixed(request: Request):
|
| 810 |
+
return await admin_health(request=request)
|
| 811 |
+
|
| 812 |
+
@app.get(f"/{PATH_PREFIX}/accounts")
|
| 813 |
+
@require_login()
|
| 814 |
+
async def admin_get_accounts_prefixed(request: Request):
|
| 815 |
+
return await admin_get_accounts(request=request)
|
| 816 |
+
|
| 817 |
+
@app.get(f"/{PATH_PREFIX}/accounts-config")
|
| 818 |
+
@require_login()
|
| 819 |
+
async def admin_get_config_prefixed(request: Request):
|
| 820 |
+
return await admin_get_config(request=request)
|
| 821 |
+
|
| 822 |
+
@app.put(f"/{PATH_PREFIX}/accounts-config")
|
| 823 |
+
@require_login()
|
| 824 |
+
async def admin_update_config_prefixed(request: Request, accounts_data: list = Body(...)):
|
| 825 |
+
return await admin_update_config(request=request, accounts_data=accounts_data)
|
| 826 |
+
|
| 827 |
+
@app.delete(f"/{PATH_PREFIX}/accounts/{{account_id}}")
|
| 828 |
+
@require_login()
|
| 829 |
+
async def admin_delete_account_prefixed(request: Request, account_id: str):
|
| 830 |
+
return await admin_delete_account(request=request, account_id=account_id)
|
| 831 |
+
|
| 832 |
+
@app.put(f"/{PATH_PREFIX}/accounts/{{account_id}}/disable")
|
| 833 |
+
@require_login()
|
| 834 |
+
async def admin_disable_account_prefixed(request: Request, account_id: str):
|
| 835 |
+
return await admin_disable_account(request=request, account_id=account_id)
|
| 836 |
+
|
| 837 |
+
@app.put(f"/{PATH_PREFIX}/accounts/{{account_id}}/enable")
|
| 838 |
+
@require_login()
|
| 839 |
+
async def admin_enable_account_prefixed(request: Request, account_id: str):
|
| 840 |
+
return await admin_enable_account(request=request, account_id=account_id)
|
| 841 |
+
|
| 842 |
+
@app.get(f"/{PATH_PREFIX}/log")
|
| 843 |
+
@require_login()
|
| 844 |
+
async def admin_get_logs_prefixed(
|
| 845 |
+
request: Request,
|
| 846 |
+
limit: int = 1500,
|
| 847 |
+
level: str = None,
|
| 848 |
+
search: str = None,
|
| 849 |
+
start_time: str = None,
|
| 850 |
+
end_time: str = None
|
| 851 |
+
):
|
| 852 |
+
return await admin_get_logs(request=request, limit=limit, level=level, search=search, start_time=start_time, end_time=end_time)
|
| 853 |
+
|
| 854 |
+
@app.delete(f"/{PATH_PREFIX}/log")
|
| 855 |
+
@require_login()
|
| 856 |
+
async def admin_clear_logs_prefixed(request: Request, confirm: str = None):
|
| 857 |
+
return await admin_clear_logs(request=request, confirm=confirm)
|
| 858 |
+
|
| 859 |
+
@app.get(f"/{PATH_PREFIX}/log/html")
|
| 860 |
+
@require_login()
|
| 861 |
+
async def admin_logs_html_route_prefixed(request: Request):
|
| 862 |
+
return await admin_logs_html_route(request=request)
|
| 863 |
+
|
| 864 |
+
# ---------- API端点(API Key认证) ----------
|
| 865 |
+
|
| 866 |
+
@app.get("/v1/models")
|
| 867 |
+
async def list_models(authorization: str = Header(None)):
|
| 868 |
+
verify_api_key(API_KEY, authorization)
|
| 869 |
+
data = []
|
| 870 |
+
now = int(time.time())
|
| 871 |
+
for m in MODEL_MAPPING.keys():
|
| 872 |
+
data.append({"id": m, "object": "model", "created": now, "owned_by": "google", "permission": []})
|
| 873 |
+
return {"object": "list", "data": data}
|
| 874 |
|
| 875 |
+
@app.get("/v1/models/{model_id}")
|
| 876 |
+
async def get_model(model_id: str, authorization: str = Header(None)):
|
| 877 |
+
verify_api_key(API_KEY, authorization)
|
| 878 |
+
return {"id": model_id, "object": "model"}
|
| 879 |
+
|
| 880 |
+
# 带PATH_PREFIX的API端点(如果配置了PATH_PREFIX)
|
| 881 |
+
if PATH_PREFIX:
|
| 882 |
+
@app.get(f"/{PATH_PREFIX}/v1/models")
|
| 883 |
+
async def list_models_prefixed(authorization: str = Header(None)):
|
| 884 |
+
return await list_models(authorization)
|
| 885 |
+
|
| 886 |
+
@app.get(f"/{PATH_PREFIX}/v1/models/{{model_id}}")
|
| 887 |
+
async def get_model_prefixed(model_id: str, authorization: str = Header(None)):
|
| 888 |
+
return await get_model(model_id, authorization)
|
| 889 |
+
|
| 890 |
+
# ---------- 聊天API端点 ----------
|
| 891 |
+
|
| 892 |
+
@app.post("/v1/chat/completions")
|
| 893 |
async def chat(
|
|
|
|
| 894 |
req: ChatRequest,
|
| 895 |
request: Request,
|
| 896 |
authorization: Optional[str] = Header(None)
|
| 897 |
):
|
| 898 |
+
# API Key 验证
|
| 899 |
+
verify_api_key(API_KEY, authorization)
|
| 900 |
+
# ... (保留原有的chat逻辑)
|
| 901 |
+
return await chat_impl(req, request, authorization)
|
| 902 |
+
|
| 903 |
+
if PATH_PREFIX:
|
| 904 |
+
@app.post(f"/{PATH_PREFIX}/v1/chat/completions")
|
| 905 |
+
async def chat_prefixed(
|
| 906 |
+
req: ChatRequest,
|
| 907 |
+
request: Request,
|
| 908 |
+
authorization: Optional[str] = Header(None)
|
| 909 |
+
):
|
| 910 |
+
return await chat(req, request, authorization)
|
| 911 |
+
|
| 912 |
+
# chat实现函数
|
| 913 |
+
async def chat_impl(
|
| 914 |
+
req: ChatRequest,
|
| 915 |
+
request: Request,
|
| 916 |
+
authorization: Optional[str]
|
| 917 |
+
):
|
| 918 |
+
# 生成请求ID(最优先,用于所有日志追踪)
|
| 919 |
request_id = str(uuid.uuid4())[:6]
|
| 920 |
|
| 921 |
+
# 获取客户端IP(用于会话隔离)
|
| 922 |
+
client_ip = request.headers.get("x-forwarded-for")
|
| 923 |
+
if client_ip:
|
| 924 |
+
client_ip = client_ip.split(",")[0].strip()
|
| 925 |
+
else:
|
| 926 |
+
client_ip = request.client.host if request.client else "unknown"
|
| 927 |
+
|
| 928 |
# 记录请求统计
|
| 929 |
async with stats_lock:
|
| 930 |
global_stats["total_requests"] += 1
|
|
|
|
| 943 |
request.state.model = req.model
|
| 944 |
|
| 945 |
# 3. 生成会话指纹,获取Session锁(防止同一对话的并发请求冲突)
|
| 946 |
+
conv_key = get_conversation_key([m.dict() for m in req.messages], client_ip)
|
| 947 |
session_lock = await multi_account_mgr.acquire_session_lock(conv_key)
|
| 948 |
|
| 949 |
# 4. 在锁的保护下检查缓存和处理Session(保证同一对话的请求串行化)
|
|
|
|
| 965 |
for attempt in range(max_account_tries):
|
| 966 |
try:
|
| 967 |
account_manager = await multi_account_mgr.get_account(None, request_id)
|
| 968 |
+
google_session = await create_google_session(account_manager, http_client, USER_AGENT, request_id)
|
| 969 |
# 线程安全地绑定账户到此对话
|
| 970 |
await multi_account_mgr.set_session_cache(
|
| 971 |
conv_key,
|
|
|
|
| 1011 |
logger.info(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 用户消息: {preview}")
|
| 1012 |
|
| 1013 |
# 3. 解析请求内容
|
| 1014 |
+
last_text, current_images = await parse_last_message(req.messages, http_client, request_id)
|
| 1015 |
|
| 1016 |
# 4. 准备文本内容
|
| 1017 |
if is_new_conversation:
|
|
|
|
| 1051 |
cached = multi_account_mgr.global_session_cache.get(conv_key)
|
| 1052 |
if not cached:
|
| 1053 |
logger.warning(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 缓存已清理,重建Session")
|
| 1054 |
+
new_sess = await create_google_session(account_manager, http_client, USER_AGENT, request_id)
|
| 1055 |
await multi_account_mgr.set_session_cache(
|
| 1056 |
conv_key,
|
| 1057 |
account_manager.config.account_id,
|
|
|
|
| 1067 |
# 注意:每次重试如果是新 Session,都需要重新上传图片
|
| 1068 |
if current_images and not current_file_ids:
|
| 1069 |
for img in current_images:
|
| 1070 |
+
fid = await upload_context_file(current_session, img["mime"], img["data"], account_manager, http_client, USER_AGENT, request_id)
|
| 1071 |
current_file_ids.append(fid)
|
| 1072 |
|
| 1073 |
# B. 准备文本 (重试模式下发全文)
|
|
|
|
| 1167 |
logger.info(f"[CHAT] [req_{request_id}] 切换账户: {account_manager.config.account_id} -> {new_account.config.account_id}")
|
| 1168 |
|
| 1169 |
# 创建新 Session
|
| 1170 |
+
new_sess = await create_google_session(new_account, http_client, USER_AGENT, request_id)
|
| 1171 |
|
| 1172 |
# 更新缓存绑定到新账户
|
| 1173 |
await multi_account_mgr.set_session_cache(
|
|
|
|
| 1274 |
return file_ids, session_name
|
| 1275 |
|
| 1276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1277 |
async def stream_chat_generator(session: str, text_content: str, file_ids: List[str], model_name: str, chat_id: str, created_time: int, account_manager: AccountManager, is_stream: bool = True, request_id: str = "", request: Request = None):
|
| 1278 |
start_time = time.time()
|
| 1279 |
|
|
|
|
| 1284 |
logger.info(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 附带文件: {len(file_ids)}个")
|
| 1285 |
|
| 1286 |
jwt = await account_manager.get_jwt(request_id)
|
| 1287 |
+
headers = get_common_headers(jwt, USER_AGENT)
|
| 1288 |
|
| 1289 |
body = {
|
| 1290 |
"configId": account_manager.config.config_id,
|
|
|
|
| 1361 |
|
| 1362 |
try:
|
| 1363 |
base_url = get_base_url(request) if request else ""
|
| 1364 |
+
file_metadata = await get_session_file_metadata(account_manager, session_name, http_client, USER_AGENT, request_id)
|
| 1365 |
|
| 1366 |
# 并行下载所有图片
|
| 1367 |
download_tasks = []
|
|
|
|
| 1370 |
mime = file_info["mimeType"]
|
| 1371 |
meta = file_metadata.get(fid, {})
|
| 1372 |
correct_session = meta.get("session") or session_name
|
| 1373 |
+
task = download_image_with_jwt(account_manager, correct_session, fid, http_client, USER_AGENT, request_id)
|
| 1374 |
download_tasks.append((fid, mime, task))
|
| 1375 |
|
| 1376 |
results = await asyncio.gather(*[task for _, _, task in download_tasks], return_exceptions=True)
|
|
|
|
| 1381 |
logger.error(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}下载失败: {type(result).__name__}")
|
| 1382 |
continue
|
| 1383 |
|
| 1384 |
+
image_url = save_image_to_hf(result, chat_id, fid, mime, base_url, IMAGE_DIR)
|
| 1385 |
logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}已保存: {image_url}")
|
| 1386 |
|
| 1387 |
markdown = f"\n\n\n\n"
|
|
|
|
| 1487 |
# 记录新访问(24小时内同一IP只计数一次)
|
| 1488 |
if client_ip not in global_stats["visitor_ips"]:
|
| 1489 |
global_stats["visitor_ips"][client_ip] = current_time
|
| 1490 |
+
|
| 1491 |
+
# 同步访问者计数(清理后的实际数量)
|
| 1492 |
+
global_stats["total_visitors"] = len(global_stats["visitor_ips"])
|
| 1493 |
+
await save_stats(global_stats)
|
| 1494 |
|
| 1495 |
sanitized_logs = get_sanitized_logs(limit=min(limit, 1000))
|
| 1496 |
return {
|
requirements.txt
CHANGED
|
@@ -3,4 +3,6 @@ uvicorn[standard]==0.29.0
|
|
| 3 |
httpx==0.27.0
|
| 4 |
pydantic==2.7.0
|
| 5 |
aiofiles==24.1.0
|
| 6 |
-
python-dotenv==1.0.1
|
|
|
|
|
|
|
|
|
| 3 |
httpx==0.27.0
|
| 4 |
pydantic==2.7.0
|
| 5 |
aiofiles==24.1.0
|
| 6 |
+
python-dotenv==1.0.1
|
| 7 |
+
itsdangerous==2.1.2
|
| 8 |
+
python-multipart==0.0.6
|