xiaoyukkkk commited on
Commit
bc4a196
·
verified ·
1 Parent(s): a8f435c

Upload 13 files

Browse files
Files changed (9) hide show
  1. .env +69 -0
  2. .env.example +11 -3
  3. .gitignore +3 -2
  4. Dockerfile +4 -0
  5. README.md +27 -20
  6. accounts.json +223 -0
  7. docker-compose.yml +10 -0
  8. main.py +350 -993
  9. 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=your-random-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}/admin` | GET | 管理面板(需ADMIN_KEY) |
257
- | `/{PATH_PREFIX}/admin/accounts` | GET | 获取账户状态(需ADMIN_KEY) |
258
- | `/{PATH_PREFIX}/admin/accounts-config` | GET | 获取账户配置(需ADMIN_KEY) |
259
- | `/{PATH_PREFIX}/admin/accounts-config` | PUT | 更新账户配置(需ADMIN_KEY) |
260
- | `/{PATH_PREFIX}/admin/accounts/{id}` | DELETE | 删除指定账户(需ADMIN_KEY) |
261
- | `/{PATH_PREFIX}/admin/accounts/{id}/disable` | PUT | 禁用指定账户(需ADMIN_KEY) |
262
- | `/{PATH_PREFIX}/admin/accounts/{id}/enable` | PUT | 启用指定账户(需ADMIN_KEY) |
263
- | `/{PATH_PREFIX}/admin/log` | GET | 获取系统日志(需ADMIN_KEY) |
264
- | `/{PATH_PREFIX}/admin/log` | DELETE | 清空系统日志(需ADMIN_KEY) |
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}/admin?key=YOUR_ADMIN_KEY`,点击"编辑配置"按钮:
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, hmac, hashlib, base64, os, asyncio, uuid, ssl, re
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 require_path_prefix, require_admin_auth, require_path_and_admin
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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") or None
99
  TIMEOUT_SECONDS = 600
100
- API_KEY = os.getenv("API_KEY") or None # API 访问密钥(可选)
101
- PATH_PREFIX = os.getenv("PATH_PREFIX") # 路径前缀(必需,用于隐藏端点)
102
- ADMIN_KEY = os.getenv("ADMIN_KEY") # 管理员密钥(必需,用于访问管理端点
103
- BASE_URL = os.getenv("BASE_URL") # 服务器完整URL(可选,用于图片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
- proxies=PROXY,
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
- @dataclass
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
- ACCOUNTS_FILE = "accounts.json"
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
- def reload_accounts():
572
- """重新加载账户配置(清空缓存并重新加载)"""
573
- global multi_account_mgr
574
- multi_account_mgr.global_session_cache.clear()
575
- multi_account_mgr = load_multi_account_config()
576
- logger.info(f"[CONFIG] 配置已重载,当前账户数: {len(multi_account_mgr.accounts)}")
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
- logger.info(f"[SYSTEM] 路径前缀已配置: {PATH_PREFIX[:4]}****")
633
- logger.info(f"[SYSTEM] 用户端点: /{PATH_PREFIX}/v1/chat/completions")
634
- logger.info(f"[SYSTEM] 管理端点: /{PATH_PREFIX}/admin/")
635
- logger.info("[SYSTEM] 公开端点: /public/log/html")
 
 
 
 
 
 
636
  logger.info("[SYSTEM] 系统初始化完成")
637
 
638
  # ---------- JWT 管理 ----------
639
- class JWTManager:
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
- async def create_google_session(account_manager: AccountManager, request_id: str = "") -> str:
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
- def get_conversation_key(messages: List[dict]) -> str:
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
- if HIDE_HOME_PAGE:
1183
  raise HTTPException(404, "Not Found")
1184
-
1185
- # 显示管理页面带隐藏提示
1186
- html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=True)
1187
- return HTMLResponse(content=html_content)
1188
-
1189
- @app.get("/{path_prefix}/admin")
1190
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1191
- async def admin_home(path_prefix: str, request: Request, key: str = None, authorization: str = Header(None)):
1192
- """管理首页 - 显示API信息和错误提醒"""
1193
- # 显示管理页面(显示隐藏提示)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1194
  html_content = templates.generate_admin_html(request, multi_account_mgr, show_hide_tip=False)
1195
  return HTMLResponse(content=html_content)
1196
 
1197
- @app.get("/{path_prefix}/v1/models")
1198
- @require_path_prefix(PATH_PREFIX)
1199
- async def list_models(path_prefix: str, authorization: str = Header(None)):
1200
- # 验证 API Key
1201
- verify_api_key(authorization)
 
 
1202
 
1203
- data = []
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("/{path_prefix}/admin/health")
1224
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1225
- async def admin_health(path_prefix: str, key: str = None, authorization: str = Header(None)):
1226
  return {"status": "ok", "time": datetime.utcnow().isoformat()}
1227
 
1228
- @app.get("/{path_prefix}/admin/accounts")
1229
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1230
- async def admin_get_accounts(path_prefix: str, key: str = None, authorization: str = Header(None)):
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("/{path_prefix}/admin/accounts-config")
1263
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1264
- async def admin_get_config(path_prefix: str, key: str = None, authorization: str = Header(None)):
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("/{path_prefix}/admin/accounts-config")
1274
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1275
- async def admin_update_config(path_prefix: str, accounts_data: list = Body(...), key: str = None, authorization: str = Header(None)):
1276
  """更新整个账户配置"""
 
1277
  try:
1278
- update_accounts_config(accounts_data)
 
 
 
 
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("/{path_prefix}/admin/accounts/{account_id}")
1285
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1286
- async def admin_delete_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1287
  """删除单个账户"""
 
1288
  try:
1289
- delete_account(account_id)
 
 
 
 
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("/{path_prefix}/admin/accounts/{account_id}/disable")
1296
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1297
- async def admin_disable_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1298
  """手动禁用账户"""
 
1299
  try:
1300
- update_account_disabled_status(account_id, True)
 
 
 
 
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("/{path_prefix}/admin/accounts/{account_id}/enable")
1307
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1308
- async def admin_enable_account(path_prefix: str, account_id: str, key: str = None, authorization: str = Header(None)):
1309
  """启用账户"""
 
1310
  try:
1311
- update_account_disabled_status(account_id, False)
 
 
 
 
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("/{path_prefix}/admin/log")
1318
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1319
  async def admin_get_logs(
1320
- path_prefix: str,
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
- "total": len(log_buffer),
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("/{path_prefix}/admin/log")
1402
- @require_path_and_admin(PATH_PREFIX, ADMIN_KEY)
1403
- async def admin_clear_logs(path_prefix: str, confirm: str = None, key: str = None, authorization: str = Header(None)):
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
- return {
1424
- "status": "success",
1425
- "message": "已清空内存日志",
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.admin_logs_html(path_prefix, key, authorization)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1433
 
1434
- @app.post("/{path_prefix}/v1/chat/completions")
1435
- @require_path_prefix(PATH_PREFIX)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1436
  async def chat(
1437
- path_prefix: str,
1438
  req: ChatRequest,
1439
  request: Request,
1440
  authorization: Optional[str] = Header(None)
1441
  ):
1442
- # 1. API Key 验证
1443
- verify_api_key(authorization)
1444
-
1445
- # 1. 生成请求ID(最优先,用于所有日志追踪)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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![生成的图片]({image_url})\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
- global_stats["total_visitors"] = len(global_stats["visitor_ips"])
2136
- await save_stats(global_stats)
 
 
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![生成的图片]({image_url})\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