xiaoyukkkk commited on
Commit
30c9975
·
verified ·
1 Parent(s): bcd318a

Upload 9 files

Browse files
Files changed (5) hide show
  1. .env.example +4 -4
  2. .gitignore +1 -4
  3. Dockerfile +23 -9
  4. README.md +82 -686
  5. main.py +435 -296
.env.example CHANGED
@@ -3,15 +3,15 @@
3
  # ============================================
4
 
5
  # 管理员密钥(必需,用于登录管理面板)
 
6
  ADMIN_KEY=your-admin-secret-key
 
7
 
8
  # API密钥(可选,用于API端点认证,优先级:环境变量 > settings.yaml)
9
  # API_KEY=your-api-key
10
 
11
- # 路径前缀(可选,用于隐藏端点路径,提高安全性
12
- # 如果设置了,端点将为: /{PATH_PREFIX}/, /{PATH_PREFIX}/v1/
13
- # 如果未设置,端点将为: /, /v1/
14
- # PATH_PREFIX=your-random-path-prefix
15
 
16
  # ============================================
17
  # 其他配置请在管理面板的"系统设置"中配置
 
3
  # ============================================
4
 
5
  # 管理员密钥(必需,用于登录管理面板)
6
+ # 明文示例:
7
  ADMIN_KEY=your-admin-secret-key
8
+ # Hash 示例(SHA256):ADMIN_KEY=sha256:你的32字节hex
9
 
10
  # API密钥(可选,用于API端点认证,优先级:环境变量 > settings.yaml)
11
  # API_KEY=your-api-key
12
 
13
+ # 服务端口(可选,默认 7860
14
+ # PORT=7860
 
 
15
 
16
  # ============================================
17
  # 其他配置请在管理面板的"系统设置"中配置
.gitignore CHANGED
@@ -35,12 +35,9 @@ ENV/
35
  # Project specific
36
  .env
37
  *.log
38
- data/stats.json
39
- data/settings.yaml
40
- accounts.json
41
 
42
  # Generated files
43
- data/images/
44
  logs/
45
 
46
  # OS
 
35
  # Project specific
36
  .env
37
  *.log
 
 
 
38
 
39
  # Generated files
40
+ data/
41
  logs/
42
 
43
  # OS
Dockerfile CHANGED
@@ -1,5 +1,16 @@
 
 
 
 
 
 
 
 
 
1
  FROM python:3.11-slim
2
  WORKDIR /app
 
 
3
  COPY requirements.txt .
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
  gcc \
@@ -7,17 +18,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
7
  && apt-get purge -y gcc \
8
  && apt-get autoremove -y \
9
  && rm -rf /var/lib/apt/lists/*
 
 
10
  COPY main.py .
11
- # 复制 core 模块
12
  COPY core ./core
13
- # 复制 util 目录
14
  COPY util ./util
15
- # 复制 templates 目录
16
- COPY templates ./templates
17
- # 复制 static 目录
18
- COPY static ./static
19
- # 创建数据目录
20
- RUN mkdir -p ./data/images
21
- # 声明数据卷(运行时需要 -v 挂载才能持久化)
 
22
  VOLUME ["/app/data"]
 
 
23
  CMD ["python", "-u", "main.py"]
 
1
+ # 多阶段构建:前端构建
2
+ FROM node:20-alpine AS frontend-builder
3
+ WORKDIR /frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm ci
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ # 后端运行环境
10
  FROM python:3.11-slim
11
  WORKDIR /app
12
+
13
+ # 安装 Python 依赖
14
  COPY requirements.txt .
15
  RUN apt-get update && apt-get install -y --no-install-recommends \
16
  gcc \
 
18
  && apt-get purge -y gcc \
19
  && apt-get autoremove -y \
20
  && rm -rf /var/lib/apt/lists/*
21
+
22
+ # 复制后端代码
23
  COPY main.py .
 
24
  COPY core ./core
 
25
  COPY util ./util
26
+
27
+ # 复制前端构建产物
28
+ COPY --from=frontend-builder /frontend/dist ./static
29
+
30
+ # 创建数据目录(支持本地和 HF Spaces Pro)
31
+ RUN mkdir -p ./data
32
+
33
+ # 声明数据卷
34
  VOLUME ["/app/data"]
35
+
36
+ # 启动服务
37
  CMD ["python", "-u", "main.py"]
README.md CHANGED
@@ -1,729 +1,125 @@
1
- ---
2
- title: Gemini Business2API
3
- emoji: 💎
4
- colorFrom: pink
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
 
 
 
10
 
11
- # Gemini Business2API
12
 
13
- Google Gemini Business API 转换为 OpenAI 兼容接口,支持多账户负载均衡。
14
- 感谢Claude老师!
15
 
16
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
17
- [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
18
 
19
- **快速部署到 HuggingFace Spaces:**
 
 
 
20
 
21
- [![Deploy to Spaces](https://huggingface.co/datasets/huggingface/badges/resolve/main/deploy-to-spaces-md.svg)](https://huggingface.co/spaces/xiaoyukkkk/gemini-business2api?duplicate=true)
22
 
23
  ## ✨ 功能特性
24
 
25
- ### 核心功能
26
- - ✅ **OpenAI API 完全兼容** - 无缝对接现有工具
27
- - ✅ **流式响应支持** - 实时输出
28
- - ✅ **多模态支持** - 支持 100+ 文件类型(图片、PDF、Office 文档、音频、视频、代码等)
29
- - ✅ **图片生成 & 图生图** - 支持 `gemini-3-pro-preview` 模型
30
- - ✅ **智能文件处理** - 自动识别文件类型,支持 URL Base64 格式
31
-
32
- ### 多账户管
33
- - ✅ **多账户负载均衡** - 支持多户轮询,故障自动转移
34
- - ✅ **智能熔断机制** - 账户连续失败自动熔断,429限流10分钟后自动恢复
35
- - **三层重试策略** - 新会话重试、请求重试、账户切换
36
- - ✅ **智能会话复用** - 自动管理对话历史,缓存过期自动清理
37
- - **在线配置管理** - Web界面编辑账户配置,实时
38
- - **账户过期自动禁用** - 设置过期时间,过期后自动禁用不可选用
39
- - ✅ **手动禁用/启用** - 管理面板一键禁用/启用账户
40
- - ✅ **错误永久禁用** - 普通错误触发熔断后永久禁用,需手动启用恢复
41
-
42
- ### 系统功能
43
- - ✅ **JWT自动管理** - 无需手动刷新令牌
44
- - 📊 **可视化管理面板** - 实时监控账户状态、过期时间、失败计数、累计对话次数
45
- - 📈 **账户使用统计** - 自动统计每个账户累计对话次数,持久化保存
46
- - 📝 **公开日志系统** - 实时查看服务运行状态(内存最多3000条,自动淘汰)
47
- - 🔐 **双重认证保护** - API_KEY 保护聊天接口,ADMIN_KEY 保护管理面板
48
- - 📡 **实时状态监控** - 公开统计接口,实时查看服务状态和请求统计
49
-
50
- ### 性能优化
51
- - ⚡ **异步文件 I/O** - 避免阻塞事件循环,提升并发性能
52
- - ⚡ **HTTP 连接池优化** - 提升高并发场景下的稳定性
53
- - ⚡ **图片并行下载** - 多图场景下显著提升响应速度
54
- - ⚡ **智能锁优化** - 减少锁竞争,提升账户选择效率
55
- - ⚡ **会话并发控制** - Session 级别锁,避免对话冲突
56
-
57
- ## 📸 功能展示
58
-
59
- ### 图片生成效果
60
-
61
- <table>
62
- <tr>
63
- <td><img src="https://github.com/user-attachments/assets/d6837897-63f2-4a17-ba4a-f59030e37018" alt="图片生成示例1" width="800"/></td>
64
- <td><img src="https://github.com/user-attachments/assets/dc597631-b00b-4307-bba1-c0ed21db0e1b" alt="图片生成示例2" width="800"/></td>
65
- </tr>
66
- <tr>
67
- <td><img src="https://github.com/user-attachments/assets/4e3a1ffa-dea9-4207-ac9b-bb32f8e83c6f" alt="图片生成示例3" width="800"/></td>
68
- <td><img src="https://github.com/user-attachments/assets/53a30edd-c2ec-4cd3-a0bd-ccf68884472a" alt="图片生成示例4" width="800"/></td>
69
- </tr>
70
- </table>
71
-
72
- ### 管理面板
73
-
74
- <table>
75
- <tr>
76
- <td><img src="https://github.com/user-attachments/assets/d0548b2b-b57e-4857-8ed0-b48b4daef34f" alt="管理面板1" width="800"/></td>
77
- <td><img src="https://github.com/user-attachments/assets/6b2aff95-e48f-412f-9e6e-2e893595b6dd" alt="管理面板2" width="800"/></td>
78
- </tr>
79
- </table>
80
-
81
- ### 日志系统
82
-
83
- <table>
84
- <tr>
85
- <td><img src="https://github.com/user-attachments/assets/4c9c38c4-6322-4057-b5f0-a10f8b82b6ae" alt="日志系统1" width="800"/></td>
86
- <td><img src="https://github.com/user-attachments/assets/095b86d7-3924-4258-954a-85bda9e8478e" alt="日志系统2" width="800"/></td>
87
- </tr>
88
- </table>
89
 
90
  ## 🚀 快速开始
91
 
92
- ### 方: HuggingFace Spaces 部署(推荐)
93
-
94
- 1. Fork 本项目到你的 GitHub 账户
95
- 2. 在 [HuggingFace Spaces](https://huggingface.co/spaces) 创建新 Space
96
- 3. 选择 Docker SDK,关联你的 GitHub 仓库
97
- 4. 配置环境变量(Settings → Variables and secrets):
98
- ```bash
99
- ACCOUNTS_CONFIG='[{"secure_c_ses":"your_cookie","csesidx":"your_idx","config_id":"your_config"}]'
100
- PATH_PREFIX=path_prefix
101
- ADMIN_KEY=your_admin_key
102
- API_KEY=your_api_key
103
- LOGO_URL=https://your-domain.com/logo.png
104
- CHAT_URL=https://your-chat-app.com
105
- ```
106
- 5. 等待构建完成(约 2-3 分钟)
107
- 6. 访问你的 Space URL 开始使用
108
-
109
- ### 方法二: Docker 部署
110
-
111
- ```bash
112
- # 1. 克隆项目
113
- git clone https://github.com/YOUR_USERNAME/gemini-business2api.git
114
- cd gemini-business2api
115
-
116
- # 2. 构建并运行
117
- docker build -t gemini-business2api .
118
- docker run -d \
119
- -p 7860:7860 \
120
- -e ACCOUNTS_CONFIG='[{"secure_c_ses":"your_cookie","csesidx":"your_idx","config_id":"your_config"}]' \
121
- -e PATH_PREFIX=path_prefix \
122
- -e ADMIN_KEY=your_admin_key \
123
- -e API_KEY=your_api_key \
124
- -e LOGO_URL=https://your-domain.com/logo.png \
125
- -e CHAT_URL=https://your-chat-app.com \
126
- gemini-business2api
127
- ```
128
-
129
- ### 方法三: 本地运行
130
 
131
  ```bash
132
- # 1. 安装依赖
133
  pip install -r requirements.txt
134
-
135
- # 2. 配置环境变量
136
  cp .env.example .env
137
- # 编辑 .env 文件,填入实际配
138
-
139
- # 3. 启动服务
140
  python main.py
141
  ```
142
 
143
- 服务将在 `http://localhost:7860` 启动
144
-
145
- ## ⚙️ 配置说明
146
-
147
- ### 必需的环境变量
148
-
149
- ```bash
150
- # 账户配置(必需)
151
- ACCOUNTS_CONFIG='[{"secure_c_ses":"your_cookie","csesidx":"your_idx","config_id":"your_config"}]'
152
-
153
- # 路径前缀(必需)
154
- PATH_PREFIX=path_prefix
155
-
156
- # 管理员密钥(必需)
157
- ADMIN_KEY=your_admin_key
158
-
159
- # API访问密钥(可选,推荐设置)
160
- API_KEY=your_api_key
161
-
162
- # 图片URL生成(可选,推荐设置)
163
- BASE_URL=https://your-domain.com
164
-
165
- # 全局代理(可选)
166
- PROXY=http://127.0.0.1:7890
167
-
168
- # 公开展示配置(可选)
169
- LOGO_URL=https://your-domain.com/logo.png
170
- CHAT_URL=https://your-chat-app.com
171
- MODEL_NAME=gemini-business
172
-
173
- # 重试配置(可选)
174
- MAX_NEW_SESSION_TRIES=5 # 新会话尝试账户数(默认5)
175
- MAX_REQUEST_RETRIES=3 # 请求失败重试次数(默认3)
176
- MAX_ACCOUNT_SWITCH_TRIES=5 # 每次重试查找账户次数(默认5)
177
- ACCOUNT_FAILURE_THRESHOLD=3 # 账户失败阈值,达到后熔断(默认3)
178
- RATE_LIMIT_COOLDOWN_SECONDS=600 # 429限流冷却时间,秒(默认600=10分钟)
179
- SESSION_CACHE_TTL_SECONDS=3600 # 会话缓存过期时间,秒(默认3600=1小时)
180
- ```
181
-
182
- ### 重试机制说明
183
-
184
- 系统提供三层重试保护:
185
-
186
- 1. **新会话创建重试**:创建新对话时,如果账户失败,自动切换到其他账户(最多尝试5个)
187
- 2. **请求失败重试**:对话过程中出错,自动重试并切换账户(最多重试3次)
188
- 3. **智能熔断机制**:
189
- - 账户连续失败3次 → 自动标记为不可用
190
- - **429限流错误**:冷却10分钟后自动��复
191
- - **普通错误**:永久禁用,需手动启用
192
- - JWT失败和请求失败都会触发熔断
193
- ```
194
-
195
- ### 多账户配置示例
196
-
197
- ```bash
198
- ACCOUNTS_CONFIG='[
199
- {
200
- "id": "account_1",
201
- "secure_c_ses": "CSE.Ad...",
202
- "csesidx": "498...",
203
- "config_id": "0cd...",
204
- "host_c_oses": "COS.Af...",
205
- "expires_at": "2025-12-23 23:03:20"
206
- },
207
- {
208
- "id": "account_2",
209
- "secure_c_ses": "CSE.Ad...",
210
- "csesidx": "208...",
211
- "config_id": "782..."
212
- }
213
- ]'
214
- ```
215
-
216
- **配置字段说明**:
217
- - `secure_c_ses` (必需): `__Secure-C_SES` Cookie 值
218
- - `csesidx` (必需): 会话索引
219
- - `config_id` (必需): 配置 ID
220
- - `id` (可选): 账户标识
221
- - `host_c_oses` (可选): `__Host-C_OSES` Cookie 值
222
- - `expires_at` (可选): 过期时间,格式 `YYYY-MM-DD HH:MM:SS`
223
-
224
- **提示**: 参考项目根目录的 `.env.example` 和 `accounts_config.example.json` 文件
225
-
226
- ## 🔧 获取配置参数
227
-
228
- 1. 访问 [Google Gemini Business](https://business.gemini.google)
229
- 2. 打开浏览器开发者工具 (F12)
230
- 3. 切换到 **Application** → **Cookies**,找到:
231
- - `__Secure-C_SES` → `secure_c_ses`
232
- - `__Host-C_OSES` → `host_c_oses` (可选)
233
- 4. 切换到 **Network** 标签,刷新页面
234
- 5. 找到 `streamGenerate` 请求,查看 Payload:
235
- - `csesidx` → `csesidx`
236
- - `configId` → `config_id`
237
-
238
- ## 📖 API 使用
239
-
240
- ### 支持的模型
241
-
242
- | 模型名称 | 说明 | 图片生成 |
243
- | ------------------------ | ---------------------- | -------- |
244
- | `gemini-auto` | 自动选择最佳模型(默认) | ❌ |
245
- | `gemini-2.5-flash` | Flash 2.5 - 快速响应 | ❌ |
246
- | `gemini-2.5-pro` | Pro 2.5 - 高质量输出 | ❌ |
247
- | `gemini-3-flash-preview` | Flash 3 预览版 | ❌ |
248
- | `gemini-3-pro-preview` | Pro 3 预览版 | ✅ |
249
-
250
- ### 访问端点
251
-
252
- | 端点 | 方法 | 说明 |
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 | 实时状态监控页面(无需认证)|
268
-
269
- **访问示例**:
270
-
271
- 假设你的配置为:
272
- - Space URL: `https://your-space.hf.space`
273
- - PATH_PREFIX: `my_prefix`
274
- - ADMIN_KEY: `my_admin_key`
275
-
276
- 则访问地址为:
277
- - **管理面板**: `https://your-space.hf.space/my_prefix?key=my_admin_key`
278
- - **公开日志**: `https://your-space.hf.space/public/log/html`
279
- - **API 端点**: `https://your-space.hf.space/my_prefix/v1/chat/completions`
280
-
281
- ### 基本对话
282
-
283
- ```bash
284
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
285
- -H "Content-Type: application/json" \
286
- -H "Authorization: Bearer your_api_key" \
287
- -d '{
288
- "model": "gemini-2.5-flash",
289
- "messages": [
290
- {"role": "user", "content": "Hello!"}
291
- ],
292
- "stream": true
293
- }'
294
- ```
295
-
296
- ### 多模态输入(支持 100+ 种文件类型)
297
-
298
- 本项目支持图片、PDF、Office 文档、音频、视频、代码等 100+ 种文件类型。详细列表请查看 [支持的文件类型清单](docs/SUPPORTED_FILE_TYPES.md)。
299
-
300
- #### 图片输入
301
-
302
- ```bash
303
- # Base64 格式
304
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
305
- -H "Content-Type: application/json" \
306
- -H "Authorization: Bearer your_api_key" \
307
- -d '{
308
- "model": "gemini-2.5-pro",
309
- "messages": [
310
- {
311
- "role": "user",
312
- "content": [
313
- {"type": "text", "text": "这张图片里有什么?"},
314
- {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,<base64_encoded_image>"}}
315
- ]
316
- }
317
- ]
318
- }'
319
-
320
- # URL 格式(自动下载并转换)
321
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
322
- -H "Content-Type: application/json" \
323
- -H "Authorization: Bearer your_api_key" \
324
- -d '{
325
- "model": "gemini-2.5-pro",
326
- "messages": [
327
- {
328
- "role": "user",
329
- "content": [
330
- {"type": "text", "text": "分析这张图片"},
331
- {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
332
- ]
333
- }
334
- ]
335
- }'
336
- ```
337
-
338
- #### PDF 文档
339
-
340
- ```bash
341
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
342
- -H "Content-Type: application/json" \
343
- -H "Authorization: Bearer your_api_key" \
344
- -d '{
345
- "model": "gemini-2.5-pro",
346
- "messages": [
347
- {
348
- "role": "user",
349
- "content": [
350
- {"type": "text", "text": "总结这个PDF的内容"},
351
- {"type": "image_url", "image_url": {"url": "https://example.com/document.pdf"}}
352
- ]
353
- }
354
- ]
355
- }'
356
- ```
357
-
358
- #### Office 文档(Word、Excel、PowerPoint)
359
-
360
- ```bash
361
- # Word 文档
362
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
363
- -H "Content-Type: application/json" \
364
- -H "Authorization: Bearer your_api_key" \
365
- -d '{
366
- "model": "gemini-2.5-pro",
367
- "messages": [
368
- {
369
- "role": "user",
370
- "content": [
371
- {"type": "text", "text": "总结这个Word文档"},
372
- {"type": "image_url", "image_url": {"url": "https://example.com/document.docx"}}
373
- ]
374
- }
375
- ]
376
- }'
377
-
378
- # Excel 表格
379
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
380
- -H "Content-Type: application/json" \
381
- -H "Authorization: Bearer your_api_key" \
382
- -d '{
383
- "model": "gemini-2.5-pro",
384
- "messages": [
385
- {
386
- "role": "user",
387
- "content": [
388
- {"type": "text", "text": "分析这个Excel数据"},
389
- {"type": "image_url", "image_url": {"url": "https://example.com/data.xlsx"}}
390
- ]
391
- }
392
- ]
393
- }'
394
- ```
395
-
396
- #### 音频文件(语音转录)
397
-
398
- ```bash
399
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
400
- -H "Content-Type: application/json" \
401
- -H "Authorization: Bearer your_api_key" \
402
- -d '{
403
- "model": "gemini-2.5-pro",
404
- "messages": [
405
- {
406
- "role": "user",
407
- "content": [
408
- {"type": "text", "text": "转录这段音频"},
409
- {"type": "image_url", "image_url": {"url": "https://example.com/audio.mp3"}}
410
- ]
411
- }
412
- ]
413
- }'
414
- ```
415
-
416
- #### 视频文件(场景分析)
417
-
418
- ```bash
419
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
420
- -H "Content-Type: application/json" \
421
- -H "Authorization: Bearer your_api_key" \
422
- -d '{
423
- "model": "gemini-2.5-pro",
424
- "messages": [
425
- {
426
- "role": "user",
427
- "content": [
428
- {"type": "text", "text": "描述这个视频的内容"},
429
- {"type": "image_url", "image_url": {"url": "https://example.com/video.mp4"}}
430
- ]
431
- }
432
- ]
433
- }'
434
- ```
435
-
436
- **支持的文件类型**(12 个分类,100+ 种格式):
437
-
438
- - 🖼️ **图片文件** - 11 种格式(PNG, JPEG, WebP, GIF, BMP, TIFF, SVG, ICO, HEIC, HEIF, AVIF)
439
- - 📄 **文档文件** - 9 种格式(PDF, TXT, Markdown, HTML, XML, CSV, TSV, RTF, LaTeX)
440
- - 📊 **Microsoft Office** - 6 种格式(.docx, .doc, .xlsx, .xls, .pptx, .ppt)
441
- - 📝 **Google Workspace** - 3 种格式(Docs, Sheets, Slides)
442
- - 💻 **代码文件** - 19 种语言(Python, JavaScript, TypeScript, Java, C/C++, Go, Rust, PHP, Ruby, Swift, Kotlin, Scala, Shell, PowerShell, SQL, R, MATLAB 等)
443
- - 🎨 **Web 开发** - 8 种格式(CSS, SCSS, LESS, JSON, YAML, TOML, Vue, Svelte)
444
- - 🎵 **音频文件** - 10 种格式(MP3, WAV, AAC, M4A, OGG, FLAC, AIFF, WMA, OPUS, AMR)
445
- - 🎬 **视频文件** - 10 种格式(MP4, MOV, AVI, MPEG, WebM, FLV, WMV, MKV, 3GPP, M4V)
446
- - 📦 **数据文件** - 6 种格式(JSON, JSONL, CSV, TSV, Parquet, Avro)
447
- - 🗜️ **压缩文件** - 5 种格式(ZIP, RAR, 7Z, TAR, GZ)
448
- - 🔧 **配置文件** - 5 种格式(YAML, TOML, INI, ENV, Properties)
449
- - 📚 **电子书** - 2 种格式(EPUB, MOBI)
450
-
451
- 完整列表和使用示例请查看 [支持的文件类型清单](docs/SUPPORTED_FILE_TYPES.md)
452
-
453
- ### 图片生成
454
-
455
- ```bash
456
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
457
- -H "Content-Type: application/json" \
458
- -H "Authorization: Bearer your_api_key" \
459
- -d '{
460
- "model": "gemini-3-pro-preview",
461
- "messages": [
462
- {"role": "user", "content": "画一只可爱的猫咪"}
463
- ]
464
- }'
465
- ```
466
-
467
- ### 图生图(Image-to-Image)
468
 
469
  ```bash
470
- curl -X POST http://localhost:7860/v1/v1/chat/completions \
471
- -H "Content-Type: application/json" \
472
- -H "Authorization: Bearer your_api_key" \
473
- -d '{
474
- "model": "gemini-3-pro-preview",
475
- "messages": [
476
- {
477
- "role": "user",
478
- "content": [
479
- {"type": "text", "text": "将这张图片改成水彩画风格"},
480
- {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,<base64_encoded_image>"}}
481
- ]
482
- }
483
- ]
484
- }'
485
- ```
486
-
487
- ## ❓ 常见问题
488
-
489
- ### 1. 如何在线编辑账户配置?
490
-
491
- 访问管理面板 `/{PATH_PREFIX}?key=YOUR_ADMIN_KEY`,点击"编辑配置"按钮:
492
- - ✅ 实时编辑 JSON 格式配置
493
- - ✅ 保存后立即生效,无需重启
494
- - ✅ 配置保存到 `accounts.json` 文件
495
- - ⚠️ 重启后从环境变量 `ACCOUNTS_CONFIG` 重新加载
496
-
497
- **建议**:在线修改后,同步更新环境变量 `ACCOUNTS_CONFIG`,避免重启后配置丢失。
498
-
499
- ### 2. 账户熔断后如何恢复?
500
-
501
- 账户连续失败3次后会自动熔断(标记为不可用):
502
- - ⏰ **429限流错误**:冷却10分钟后自动恢复(可通过 `RATE_LIMIT_COOLDOWN_SECONDS` 配置)
503
- - 🔄 **普通错误**:永久禁用,需在管理面板手动点击"启用"按钮恢复
504
- - ✅ **成功后**:失败计数重置为0,账户恢复正常
505
-
506
- 可在管理面板实时查看账户状态和失败计数。
507
-
508
- ### 3. 账户禁用功能有哪些?
509
-
510
- 管理面板提供完整的账户禁用管理功能,不同禁用状态有不同的恢复方式:
511
-
512
- #### 📋 **账户状态说明**
513
-
514
- | 状态 | 显示 | 颜色 | 自动恢复 | 恢复方式 | 倒计时 |
515
- |------|------|------|---------|---------|--------|
516
- | **正常** | 正常/即将过期 | 绿色/橙色 | - | - | ❌ |
517
- | **过期禁用** | 过期禁用 | 灰色 | ❌ | 修改过期时间 | ❌ |
518
- | **手动禁用** | 手动禁用 | 灰色 | ❌ | 点击"启用"按钮 | ❌ |
519
- | **错误禁用** | 错误禁用 | 红色 | ❌ | 点击"启用"按钮 | ❌ |
520
- | **429限流** | 429限流 | 橙色 | ✅ 10分钟 | 自动恢复或点击"启用" | ✅ |
521
-
522
- #### ⚙️ **功能说明**
523
-
524
- 1. **账户过期自动禁用**
525
- - 在账户配置中设置 `expires_at` 字段(格式:`YYYY-MM-DD HH:MM:SS`)
526
- - 过期后账户自动禁用,不参与轮询选择
527
- - 页面显示灰色半透明卡片,仅保留"删除"按钮
528
- - 需要修改过期时间才能重新启用
529
-
530
- 2. **手动禁用/启用**
531
- - 管理面板每个账户卡片都有"禁用"按钮
532
- - 点击后立即禁用,不参与轮询选择
533
- - 显示灰色半透明卡片,提供"启用"+"删除"按钮
534
- - 点击"启用"按钮即可恢复
535
-
536
- 3. **错误自动禁用(永久)**
537
- - 账户连续失败3次触发(非429错误)
538
- - 自动标记为不可用,永久禁用
539
- - 显示红色半透明卡片,提供"启用"+"删除"按钮
540
- - 需要手动点击"启用"按钮恢复
541
-
542
- 4. **429限流自动禁用(临时)**
543
- - 账户连续遇到429错误3次触发
544
- - 自动冷却10分钟(可配置 `RATE_LIMIT_COOLDOWN_SECONDS`)
545
- - 显示橙色卡片,带倒计时显示(如:`567秒 (429限流)`)
546
- - 冷却期过后自动恢复,或手动点击"启用"立即恢复
547
-
548
- #### 💡 **使用建议**
549
-
550
- - **临时维护**:使用"手动禁用"功能暂时停用账户
551
- - **账户轮换**:设置过期时间,到期自动禁用
552
- - **故障排查**:错误禁用的账户需检查后再手动启用
553
- - **429限流**:耐心等待10分钟自动恢复,或检查请求频率
554
-
555
- ### 4. 账户对话次数统计如何工作?
556
-
557
- 系统自动统计每个账户的累计对话次数,无需手动操作。
558
-
559
- #### 📊 **统计说明**
560
-
561
- - **自动计数**:每次聊天请求成功后自动 +1
562
- - **持久化保存**:保存到 `data/stats.json` 文件,重启不丢失
563
- - **实时显示**:管理面板账户卡片实时显示累计次数
564
- - **数据位置**:`data/stats.json` → `account_conversations` 字段
565
-
566
- #### 📈 **显示位置**
567
-
568
- 管理面板账户卡片中,"剩余时长"行下方:
569
- ```
570
- 过期时间: 2025-12-31 23:59:59
571
- 剩余时长: 72.5 小时
572
- 累计对话: 123 次 ← 蓝色加粗显示
573
- ```
574
-
575
- #### 💡 **数据说明**
576
-
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?
588
-
589
- **自动检测**(推荐):
590
- - 不设置 `BASE_URL` 环境变量
591
- - 系统自动从请求头检测域名
592
-
593
- **手动设置**:
594
- ```bash
595
- BASE_URL=https://your-domain.com
596
- ```
597
-
598
- **使用反向代理**:
599
-
600
- 如果你使用自己的域名反向代理到 HuggingFace Space,可以通过以下方式配置:
601
-
602
- **Nginx 配置示例**:
603
- ```nginx
604
- location / {
605
- proxy_pass https://your-username-space-name.hf.space;
606
- proxy_set_header Host your-username-space-name.hf.space;
607
- proxy_ssl_server_name on;
608
- }
609
- ```
610
-
611
- **Deno Deploy 配置示例**:
612
- ```typescript
613
- async function handler(request: Request): Promise<Response> {
614
- const url = new URL(request.url);
615
- url.host = 'your-username-space-name.hf.space';
616
- return fetch(new Request(url, request));
617
- }
618
-
619
- Deno.serve(handler);
620
  ```
621
 
622
- 配置反向代理后,将 `BASE_URL` 设置为你的自定义域名即可。
623
-
624
- ### 7. API_KEY 和 ADMIN_KEY 的区别?
625
 
626
- - **API_KEY**: 保护聊天接口 (`/v1/chat/completions`)
627
- - **ADMIN_KEY**: 保护管理面板 (`/` 或 `/{PATH_PREFIX}`)
628
 
629
- 可以设相同的值,也可以分开
630
 
631
- ### 8. 如何查看日志?
 
632
 
633
- - **公开日志**: 访问 `/public/log/html` (无需密钥)
634
- - **管理面板**: 访问 `/?key=YOUR_ADMIN_KEY` 或 `/{PATH_PREFIX}?key=YOUR_ADMIN_KEY`
635
 
636
- 日志系统说明
637
- - 内存存储最多 3000 条日志
638
- - 超过上限自动删除最旧的日志
639
- - 重启后清空(内存存储)
640
- - 可通过 API 手动清空日志
641
 
642
- ## 🔧 油猴脚本使用说明
643
-
644
- 本项目提供油猴脚本辅助获取配置参数,使用前需要配置 TamperMonkey:
645
-
646
- ### TamperMonkey 设置
647
-
648
- 1. **配置模式**:改为 `高级`
649
- 2. **安全设置**:允许脚本访问 Cookie 改为 `All`
650
-
651
- ### Google Chrome 额外设置
652
-
653
- 1. 打开油猴扩展设置
654
- 2. 启用 **允许运行用户脚本**
655
- 3. 设置 **有权访问的网站** 权限
656
-
657
- 配置完成后即可使用脚本自动获取 `secure_c_ses`、`csesidx`、`config_id` 等参数。
658
-
659
- ---
660
-
661
- ## 📁 项目结构
662
-
663
- ```
664
- gemini-business2api/
665
- ├── main.py # 主程序入口
666
- ├── core/ # 核心模块
667
- │ ├── __init__.py
668
- │ ├── auth.py # 认证装饰器
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 条日志
696
- - 自动淘汰机制:超过上限自动删除最旧的日志(FIFO)
697
- - 重启后清空:日志存储在内存中,重启后丢失
698
- - 内存占用:约 450KB - 750KB(非常小,不会爆炸)
699
-
700
- ## 🛠️ 技术栈
701
 
702
- - **Python 3.11+**
703
- - **FastAPI** - 现代Web框架
704
- - **Uvicorn** - ASGI服务器
705
- - **httpx** - HTTP客户端
706
- - **Docker** - 容器化部署
707
 
708
- ## 📝 License
 
 
 
 
 
 
 
 
 
 
 
 
 
709
 
710
- MIT License - 查看 [LICENSE](LICENSE) 文件了解详情
711
 
712
- ---
 
 
 
 
 
 
 
 
 
713
 
714
  ## 🙏 致谢
715
 
716
  * 源项目:[F佬 Linux.do 讨论](https://linux.do/t/topic/1225645)
717
  * 源项目:[heixxin/gemini](https://huggingface.co/spaces/heixxin/gemini/tree/main) | [Linux.do 讨论](https://linux.do/t/topic/1226413)
718
  * 绘图参考:[Gemini-Link-System](https://github.com/qxd-ljy/Gemini-Link-System) | [Linux.do 讨论](https://linux.do/t/topic/1234363)
719
- * Gemini Business 2API Helper 参考:[Linux.do 讨论](https://linux.do/t/topic/1231008)
720
-
721
- ---
722
 
723
  ## ⭐ Star History
724
 
725
  [![Star History Chart](https://api.star-history.com/svg?repos=Dreamy-rain/gemini-business2api&type=date&legend=top-left)](https://www.star-history.com/#Dreamy-rain/gemini-business2api&type=date&legend=top-left)
726
 
727
- ---
728
-
729
- **如果这个项目对你有帮助,请给个 ⭐ Star!**
 
1
+ <p align="center">
2
+ <img src="docs/logo.svg" width="120" alt="Gemini Business2API logo" />
3
+ </p>
4
+ <h1 align="center">Gemini Business2API</h1>
5
+ <p align="center">赋予硅基生物以灵魂</p>
6
+ <p align="center">当时明月在 · 曾照彩云归</p>
7
+ <p align="center">
8
+ <strong>简体中文</strong> | <a href="docs/README_EN.md">English</a>
9
+ </p>
10
+ <p align="center"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" /> <img src="https://img.shields.io/badge/Python-3.11+-3776AB?logo=python&logoColor=white" /> <img src="https://img.shields.io/badge/FastAPI-0.110-009688?logo=fastapi&logoColor=white" /> <img src="https://img.shields.io/badge/Vue-3-4FC08D?logo=vue.js&logoColor=white" /> <img src="https://img.shields.io/badge/Vite-7-646CFF?logo=vite&logoColor=white" /> <img src="https://img.shields.io/badge/Docker-ready-2496ED?logo=docker&logoColor=white" /></p>
11
+
12
+ <p align="center">将 Gemini Business 转换为 OpenAI 兼容接口,支持多账号负载均衡、图像生成、多模态能力与内置管理面板。</p>
13
 
14
+ ---
15
 
16
+ ## 📜 开源协议与声明
 
17
 
18
+ **开源协议**: MIT License - 查看 [LICENSE](LICENSE) 文件了解详情
 
19
 
20
+ **使用声明**:
21
+ - ⚠️ **本项目仅限学习与研究用途,禁止用于商业用途**
22
+ - 📝 **使用时请保留本声明、原作者信息与开源来源**
23
+ - 🔗 **项目地址**: [github.com/Dreamy-rain/gemini-business2api](https://github.com/Dreamy-rain/gemini-business2api)
24
 
25
+ ---
26
 
27
  ## ✨ 功能特性
28
 
29
+ - ✅ OpenAI API 完全兼容 - 无缝对接现有工具
30
+ - ✅ 多账号负载均衡 - 轮询与故障自动切换
31
+ - ✅ 流式输出 - 实时响应
32
+ - ✅ 多模态输入 - 100+ 文件类型(图片、PDF、Office 文档、音频、视频、代码等)
33
+ - ✅ 图片生成 & 图生图 - 模型可配置,Base64 或 URL 返回
34
+ - ✅ 智能文件处理 - 自动识别文件类型,支持 URL Base64
35
+ - ✅ 日志与监控 - 实时状态与统计信息
36
+ - ✅ 代支持 - 通过 PROXY 配置
37
+ - ✅ 内置管理面板 - 在线配置与号管理
38
+
39
+ ## 🤖 模型功能
40
+
41
+ | 模型ID | 识图 | 香蕉绘图 | 原联网 | 文件多模态 |
42
+ | ------------------------ | ---- | -------- | -------- | ---------- |
43
+ | `gemini-auto` | | 可选 | ✅ | ✅ |
44
+ | `gemini-2.5-flash` | | 可选 | ✅ | ✅ |
45
+ | `gemini-2.5-pro` | ✅ | 可选 | ✅ | ✅ |
46
+ | `gemini-3-flash-preview` | ✅ | 可选 | ✅ | ✅ |
47
+ | `gemini-3-pro-preview` | | 可选 | ✅ | ✅ |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  ## 🚀 快速开始
50
 
51
+ ### 方:本地运行(推荐)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  ```bash
 
54
  pip install -r requirements.txt
 
 
55
  cp .env.example .env
56
+ # 编辑 .env ADMIN_KEY
 
 
57
  python main.py
58
  ```
59
 
60
+ ### 方式二:Docker
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  ```bash
63
+ docker build -t gemini-business2api .
64
+ docker run -d -p 7860:7860 \
65
+ -e ADMIN_KEY=your_admin_key \
66
+ gemini-business2api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  ```
68
 
69
+ ### 访问方式
 
 
70
 
71
+ - 管理面板:`http://localhost:7860/`(使用 `ADMIN_KEY` 登录)
72
+ - OpenAI 兼容接口:`http://localhost:7860/v1/chat/completions`
73
 
74
+ ### 配提示
75
 
76
+ - 账号配置优先读取 `ACCOUNTS_CONFIG`,也可在管理面板中录入并保存至 `data/accounts.json`。
77
+ - 如需鉴权,可设置 `API_KEY` 保护 `/v1/chat/completions`。
78
 
79
+ ### 更多文档
 
80
 
81
+ - 支持的文件类型`docs/SUPPORTED_FILE_TYPES.md`
 
 
 
 
82
 
83
+ ## 📸 功能展示
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ ### 管理系统
 
 
 
 
86
 
87
+ <table>
88
+ <tr>
89
+ <td><img src="docs/1.png" alt="管理系统 1" /></td>
90
+ <td><img src="docs/2.png" alt="管理系统 2" /></td>
91
+ </tr>
92
+ <tr>
93
+ <td><img src="docs/3.png" alt="管理系统 3" /></td>
94
+ <td><img src="docs/4.png" alt="管理系统 4" /></td>
95
+ </tr>
96
+ <tr>
97
+ <td><img src="docs/5.png" alt="管理系统 5" /></td>
98
+ <td><img src="docs/6.png" alt="管理系统 6" /></td>
99
+ </tr>
100
+ </table>
101
 
102
+ ### 图片效果
103
 
104
+ <table>
105
+ <tr>
106
+ <td><img src="docs/img_1.png" alt="图片效果 1" /></td>
107
+ <td><img src="docs/img_2.png" alt="图片效果 2" /></td>
108
+ </tr>
109
+ <tr>
110
+ <td><img src="docs/img_3.png" alt="图片效果 3" /></td>
111
+ <td><img src="docs/img_4.png" alt="图片效果 4" /></td>
112
+ </tr>
113
+ </table>
114
 
115
  ## 🙏 致谢
116
 
117
  * 源项目:[F佬 Linux.do 讨论](https://linux.do/t/topic/1225645)
118
  * 源项目:[heixxin/gemini](https://huggingface.co/spaces/heixxin/gemini/tree/main) | [Linux.do 讨论](https://linux.do/t/topic/1226413)
119
  * 绘图参考:[Gemini-Link-System](https://github.com/qxd-ljy/Gemini-Link-System) | [Linux.do 讨论](https://linux.do/t/topic/1234363)
 
 
 
120
 
121
  ## ⭐ Star History
122
 
123
  [![Star History Chart](https://api.star-history.com/svg?repos=Dreamy-rain/gemini-business2api&type=date&legend=top-left)](https://www.star-history.com/#Dreamy-rain/gemini-business2api&type=date&legend=top-left)
124
 
125
+ **如果这个项目对你有帮助,请给个 ⭐ Star!**
 
 
main.py CHANGED
@@ -1,4 +1,4 @@
1
- import json, time, os, asyncio, uuid, ssl, re, yaml, shutil
2
  from datetime import datetime, timezone, timedelta
3
  from typing import List, Optional, Union, Dict, Any
4
  from pathlib import Path
@@ -8,7 +8,8 @@ from dotenv import load_dotenv
8
  import httpx
9
  import aiofiles
10
  from fastapi import FastAPI, HTTPException, Header, Request, Body, Form
11
- from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse, RedirectResponse
 
12
  from fastapi.staticfiles import StaticFiles
13
  from pydantic import BaseModel
14
  from util.streaming_parser import parse_json_array_stream_async
@@ -69,9 +70,7 @@ from core.account import (
69
  from core import uptime as uptime_tracker
70
 
71
  # 导入配置管理和模板系统
72
- from fastapi.templating import Jinja2Templates
73
  from core.config import config_manager, config
74
- from util.template_helpers import prepare_admin_template_data
75
 
76
  # ---------- 日志配置 ----------
77
 
@@ -83,7 +82,7 @@ log_lock = Lock()
83
  stats_lock = asyncio.Lock() # 改为异步锁
84
 
85
  async def load_stats():
86
- """加载统计数据(异步)"""
87
  try:
88
  if os.path.exists(STATS_FILE):
89
  async with aiofiles.open(STATS_FILE, 'r', encoding='utf-8') as f:
@@ -94,9 +93,13 @@ async def load_stats():
94
  return {
95
  "total_visitors": 0,
96
  "total_requests": 0,
97
- "request_timestamps": [], # 最近1小时的请求时间戳
98
- "visitor_ips": {}, # {ip: timestamp} 记录访问IP和时间
99
- "account_conversations": {} # {account_id: conversation_count} 账户对话次数
 
 
 
 
100
  }
101
 
102
  async def save_stats(stats):
@@ -112,10 +115,85 @@ global_stats = {
112
  "total_visitors": 0,
113
  "total_requests": 0,
114
  "request_timestamps": [],
 
 
 
115
  "visitor_ips": {},
116
- "account_conversations": {}
 
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  class MemoryLogHandler(logging.Handler):
120
  """自定义日志处理器,将日志写入内存缓冲区"""
121
  def emit(self, record):
@@ -147,7 +225,6 @@ logger.addHandler(memory_handler)
147
  # 所有配置通过 config_manager 访问,优先级:环境变量 > YAML > 默认值
148
  TIMEOUT_SECONDS = 600
149
  API_KEY = config.basic.api_key
150
- PATH_PREFIX = config.security.path_prefix
151
  ADMIN_KEY = config.security.admin_key
152
  PROXY = config.basic.proxy
153
  BASE_URL = config.basic.base_url
@@ -204,6 +281,8 @@ def get_base_url(request: Request) -> str:
204
 
205
  return f"{forwarded_proto}://{forwarded_host}"
206
 
 
 
207
  # ---------- 常量定义 ----------
208
  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"
209
 
@@ -230,15 +309,9 @@ if not ADMIN_KEY:
230
  sys.exit(1)
231
 
232
  # 启动日志
233
- if PATH_PREFIX:
234
- logger.info(f"[SYSTEM] 路径前缀已配置: {PATH_PREFIX[:4]}****")
235
- logger.info(f"[SYSTEM] API端点: /{PATH_PREFIX}/v1/chat/completions")
236
- logger.info(f"[SYSTEM] 管理端点: /{PATH_PREFIX}/")
237
- else:
238
- logger.info("[SYSTEM] 未配置路径前缀,使用默认路径")
239
- logger.info("[SYSTEM] API端点: /v1/chat/completions")
240
- logger.info("[SYSTEM] 管理端点: /admin/")
241
- logger.info("[SYSTEM] 公开端点: /public/log/html, /public/stats, /public/uptime/html")
242
  logger.info(f"[SYSTEM] Session过期时间: {SESSION_EXPIRE_HOURS}小时")
243
  logger.info("[SYSTEM] 系统初始化完成")
244
 
@@ -254,16 +327,44 @@ logger.info("[SYSTEM] 系统初始化完成")
254
  # ---------- OpenAI 兼容接口 ----------
255
  app = FastAPI(title="Gemini-Business OpenAI Gateway")
256
 
257
- # ---------- 模板系统配置 ----------
258
- templates = Jinja2Templates(directory="templates")
259
-
260
- # 开发模式:支持热更新
261
- if os.getenv("ENV") == "development":
262
- templates.env.auto_reload = True
263
- logger.info("[SYSTEM] 模板热更新已启用(开发模式)")
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- # 挂载静态文件
266
  app.mount("/static", StaticFiles(directory="static"), name="static")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  # ---------- Session 中间件配置 ----------
269
  from starlette.middleware.sessions import SessionMiddleware
@@ -278,43 +379,30 @@ app.add_middleware(
278
  # ---------- Uptime 追踪中间件 ----------
279
  @app.middleware("http")
280
  async def track_uptime_middleware(request: Request, call_next):
281
- """追踪每个请求的成功/失败状态,用于 Uptime 监控"""
282
- # 只追踪 API 请求(排除静态文件、管理端点等)
283
  path = request.url.path
284
- if path.startswith("/images/") or path.startswith("/public/") or path.startswith("/favicon"):
 
 
 
 
 
285
  return await call_next(request)
286
 
287
  start_time = time.time()
288
- success = False
289
- model = None
290
 
291
  try:
292
  response = await call_next(request)
 
293
  success = response.status_code < 400
294
-
295
- # 尝试从请求中提取模型信息
296
- if hasattr(request.state, "model"):
297
- model = request.state.model
298
-
299
- # 记录 API 主服务状态
300
- uptime_tracker.record_request("api_service", success)
301
-
302
- # 如果有模型信息,记录模型状态
303
- if model and model in uptime_tracker.SUPPORTED_MODELS:
304
- uptime_tracker.record_request(model, success)
305
-
306
  return response
307
 
308
- except Exception as e:
309
- # 请求失败 - 尝试提取模型信息(可能在异常前已设置)
310
- if hasattr(request.state, "model"):
311
- model = request.state.model
312
-
313
  uptime_tracker.record_request("api_service", False)
314
- if model and model in uptime_tracker.SUPPORTED_MODELS:
315
- uptime_tracker.record_request(model, False)
316
  raise
317
 
 
318
  # ---------- 图片静态服务初始化 ----------
319
  os.makedirs(IMAGE_DIR, exist_ok=True)
320
  app.mount("/images", StaticFiles(directory=IMAGE_DIR), name="images")
@@ -340,16 +428,19 @@ async def startup_event():
340
 
341
  # 加载统计数据
342
  global_stats = await load_stats()
 
 
 
 
 
 
 
343
  logger.info(f"[SYSTEM] 统计数据已加载: {global_stats['total_requests']} 次请求, {global_stats['total_visitors']} 位访客")
344
 
345
  # 启动缓存清理任务
346
  asyncio.create_task(multi_account_mgr.start_background_cleanup())
347
  logger.info("[SYSTEM] 后台缓存清理任务已启动(间隔: 5分钟)")
348
 
349
- # 启动 Uptime 数据聚合任务
350
- asyncio.create_task(uptime_tracker.uptime_aggregation_task())
351
- logger.info("[SYSTEM] Uptime 数据聚合任务已启动(间隔: 240秒)")
352
-
353
  # ---------- 日志脱敏函数 ----------
354
  def get_sanitized_logs(limit: int = 100) -> list:
355
  """获取脱敏后的日志列表,按请求ID分组并提取关键事件"""
@@ -581,116 +672,129 @@ def create_chunk(id: str, created: int, model: str, delta: dict, finish_reason:
581
  "system_fingerprint": None # OpenAI 标准字段(可选)
582
  }
583
  return json.dumps(chunk)
584
-
585
- # ---------- 辅助函数 ----------
586
-
587
- def get_admin_template_data(request: Request):
588
- """获取管理页面模板数据(避免重复代码)"""
589
- return prepare_admin_template_data(
590
- request, multi_account_mgr, log_buffer, log_lock,
591
- api_key=API_KEY, base_url=BASE_URL, proxy=PROXY,
592
- logo_url=LOGO_URL, chat_url=CHAT_URL, path_prefix=PATH_PREFIX,
593
- max_new_session_tries=MAX_NEW_SESSION_TRIES,
594
- max_request_retries=MAX_REQUEST_RETRIES,
595
- max_account_switch_tries=MAX_ACCOUNT_SWITCH_TRIES,
596
- account_failure_threshold=ACCOUNT_FAILURE_THRESHOLD,
597
- rate_limit_cooldown_seconds=RATE_LIMIT_COOLDOWN_SECONDS,
598
- session_cache_ttl_seconds=SESSION_CACHE_TTL_SECONDS
599
- )
600
-
601
- # ---------- 路由定义 ----------
602
-
603
- @app.get("/")
604
- async def home(request: Request):
605
- """首页 - 根据PATH_PREFIX配置决定行为"""
606
- if PATH_PREFIX:
607
- # 如果设置了PATH_PREFIX(隐藏模式),首页返回404,不暴露任何信息
608
- raise HTTPException(404, "Not Found")
609
- else:
610
- # 未设置PATH_PREFIX(公开模式),根据登录状态重定向
611
- if is_logged_in(request):
612
- template_data = get_admin_template_data(request)
613
- return templates.TemplateResponse("admin/index.html", template_data)
614
- else:
615
- return RedirectResponse(url="/login", status_code=302)
616
-
617
- # ---------- 登录/登出端点(支持可选PATH_PREFIX) ----------
618
-
619
- # 不带PATH_PREFIX的登录端点
620
- @app.get("/login")
621
- async def admin_login_get(request: Request, error: str = None):
622
- """登录页面"""
623
- return templates.TemplateResponse("auth/login.html", {"request": request, "error": error})
624
 
625
  @app.post("/login")
626
  async def admin_login_post(request: Request, admin_key: str = Form(...)):
627
- """处理登录表单提交"""
628
  if admin_key == ADMIN_KEY:
629
  login_user(request)
630
- logger.info(f"[AUTH] 管理员登录成功")
631
- return RedirectResponse(url="/", status_code=302)
632
- else:
633
- logger.warning(f"[AUTH] 登录失败 - 密钥错误")
634
- return templates.TemplateResponse("auth/login.html", {"request": request, "error": "密钥错误,请重试"})
635
 
636
  @app.post("/logout")
637
  @require_login(redirect_to_login=False)
638
  async def admin_logout(request: Request):
639
- """登出"""
640
  logout_user(request)
641
- logger.info(f"[AUTH] 管理员已登出")
642
- return RedirectResponse(url="/login", status_code=302)
643
-
644
- # 带PATH_PREFIX的登录端点(如果配置了PATH_PREFIX)
645
- if PATH_PREFIX:
646
- @app.get(f"/{PATH_PREFIX}/login")
647
- async def admin_login_get_prefixed(request: Request, error: str = None):
648
- """登录页面(带前缀)"""
649
- return templates.TemplateResponse("auth/login.html", {"request": request, "error": error})
650
-
651
- @app.post(f"/{PATH_PREFIX}/login")
652
- async def admin_login_post_prefixed(request: Request, admin_key: str = Form(...)):
653
- """处理登录表单提交(带前缀)"""
654
- if admin_key == ADMIN_KEY:
655
- login_user(request)
656
- logger.info(f"[AUTH] 管理员登录成功")
657
- return RedirectResponse(url=f"/{PATH_PREFIX}", status_code=302)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  else:
659
- logger.warning(f"[AUTH] 登录失败 - 密钥错误")
660
- return templates.TemplateResponse("auth/login.html", {"request": request, "error": "密钥错误,请重试"})
661
 
662
- @app.post(f"/{PATH_PREFIX}/logout")
663
- @require_login(redirect_to_login=False)
664
- async def admin_logout_prefixed(request: Request):
665
- """登出(带前缀)"""
666
- logout_user(request)
667
- logger.info(f"[AUTH] 管理员已登出")
668
- return RedirectResponse(url=f"/{PATH_PREFIX}/login", status_code=302)
669
 
670
- # ---------- 管理端点(需要登录) ----------
 
 
 
 
671
 
672
- # 不带PATH_PREFIX的管理端点
673
- @app.get("/admin")
674
- @require_login()
675
- async def admin_home_no_prefix(request: Request):
676
- """管理首页"""
677
- template_data = get_admin_template_data(request)
678
- return templates.TemplateResponse("admin/index.html", template_data)
679
 
680
- # 带PATH_PREFIX的管理端点(如果配置了PATH_PREFIX)
681
- if PATH_PREFIX:
682
- @app.get(f"/{PATH_PREFIX}")
683
- @require_login()
684
- async def admin_home_prefixed(request: Request):
685
- """管理首页(带前缀)"""
686
- return await admin_home_no_prefix(request=request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
- # ---------- 管理API端点(需要登录) ----------
689
 
690
- @app.get("/admin/health")
691
- @require_login()
692
- async def admin_health(request: Request):
693
- return {"status": "ok", "time": datetime.utcnow().isoformat()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
  @app.get("/admin/accounts")
696
  @require_login()
@@ -803,7 +907,7 @@ async def admin_enable_account(request: Request, account_id: str):
803
  logger.error(f"[CONFIG] 启用账户失败: {str(e)}")
804
  raise HTTPException(500, f"启用失败: {str(e)}")
805
 
806
- # ---------- 系统设置 API ----------
807
  @app.get("/admin/settings")
808
  @require_login()
809
  async def admin_get_settings(request: Request):
@@ -975,86 +1079,10 @@ async def admin_clear_logs(request: Request, confirm: str = None):
975
  logger.info("[LOG] 日志已清空")
976
  return {"status": "success", "message": "已清空内存日志", "cleared_count": cleared_count}
977
 
978
- @app.get("/admin/log/html")
979
- @require_login()
980
- async def admin_logs_html_route(request: Request):
981
- """返回美化的 HTML 日志查看界面"""
982
- return templates.TemplateResponse("admin/logs.html", {"request": request})
983
-
984
- # 带PATH_PREFIX的管理API端点(如果配置了PATH_PREFIX)
985
- if PATH_PREFIX:
986
- @app.get(f"/{PATH_PREFIX}/health")
987
- @require_login()
988
- async def admin_health_prefixed(request: Request):
989
- return await admin_health(request=request)
990
-
991
- @app.get(f"/{PATH_PREFIX}/accounts")
992
- @require_login()
993
- async def admin_get_accounts_prefixed(request: Request):
994
- return await admin_get_accounts(request=request)
995
-
996
- @app.get(f"/{PATH_PREFIX}/accounts-config")
997
- @require_login()
998
- async def admin_get_config_prefixed(request: Request):
999
- return await admin_get_config(request=request)
1000
-
1001
- @app.put(f"/{PATH_PREFIX}/accounts-config")
1002
- @require_login()
1003
- async def admin_update_config_prefixed(request: Request, accounts_data: list = Body(...)):
1004
- return await admin_update_config(request=request, accounts_data=accounts_data)
1005
-
1006
- @app.delete(f"/{PATH_PREFIX}/accounts/{{account_id}}")
1007
- @require_login()
1008
- async def admin_delete_account_prefixed(request: Request, account_id: str):
1009
- return await admin_delete_account(request=request, account_id=account_id)
1010
-
1011
- @app.put(f"/{PATH_PREFIX}/accounts/{{account_id}}/disable")
1012
- @require_login()
1013
- async def admin_disable_account_prefixed(request: Request, account_id: str):
1014
- return await admin_disable_account(request=request, account_id=account_id)
1015
-
1016
- @app.put(f"/{PATH_PREFIX}/accounts/{{account_id}}/enable")
1017
- @require_login()
1018
- async def admin_enable_account_prefixed(request: Request, account_id: str):
1019
- return await admin_enable_account(request=request, account_id=account_id)
1020
-
1021
- @app.get(f"/{PATH_PREFIX}/log")
1022
- @require_login()
1023
- async def admin_get_logs_prefixed(
1024
- request: Request,
1025
- limit: int = 1500,
1026
- level: str = None,
1027
- search: str = None,
1028
- start_time: str = None,
1029
- end_time: str = None
1030
- ):
1031
- return await admin_get_logs(request=request, limit=limit, level=level, search=search, start_time=start_time, end_time=end_time)
1032
-
1033
- @app.delete(f"/{PATH_PREFIX}/log")
1034
- @require_login()
1035
- async def admin_clear_logs_prefixed(request: Request, confirm: str = None):
1036
- return await admin_clear_logs(request=request, confirm=confirm)
1037
-
1038
- @app.get(f"/{PATH_PREFIX}/log/html")
1039
- @require_login()
1040
- async def admin_logs_html_route_prefixed(request: Request):
1041
- return await admin_logs_html_route(request=request)
1042
-
1043
- @app.get(f"/{PATH_PREFIX}/settings")
1044
- @require_login()
1045
- async def admin_get_settings_prefixed(request: Request):
1046
- return await admin_get_settings(request=request)
1047
-
1048
- @app.put(f"/{PATH_PREFIX}/settings")
1049
- @require_login()
1050
- async def admin_update_settings_prefixed(request: Request, new_settings: dict = Body(...)):
1051
- return await admin_update_settings(request=request, new_settings=new_settings)
1052
-
1053
- # ---------- API端点(API Key认证) ----------
1054
 
1055
  @app.get("/v1/models")
1056
  async def list_models(authorization: str = Header(None)):
1057
- verify_api_key(API_KEY, authorization)
1058
  data = []
1059
  now = int(time.time())
1060
  for m in MODEL_MAPPING.keys():
@@ -1063,20 +1091,9 @@ async def list_models(authorization: str = Header(None)):
1063
 
1064
  @app.get("/v1/models/{model_id}")
1065
  async def get_model(model_id: str, authorization: str = Header(None)):
1066
- verify_api_key(API_KEY, authorization)
1067
  return {"id": model_id, "object": "model"}
1068
 
1069
- # 带PATH_PREFIX的API端点(如果配置了PATH_PREFIX)
1070
- if PATH_PREFIX:
1071
- @app.get(f"/{PATH_PREFIX}/v1/models")
1072
- async def list_models_prefixed(authorization: str = Header(None)):
1073
- return await list_models(authorization)
1074
-
1075
- @app.get(f"/{PATH_PREFIX}/v1/models/{{model_id}}")
1076
- async def get_model_prefixed(model_id: str, authorization: str = Header(None)):
1077
- return await get_model(model_id, authorization)
1078
-
1079
- # ---------- 聊天API端点 ----------
1080
 
1081
  @app.post("/v1/chat/completions")
1082
  async def chat(
@@ -1089,15 +1106,6 @@ async def chat(
1089
  # ... (保留原有的chat逻辑)
1090
  return await chat_impl(req, request, authorization)
1091
 
1092
- if PATH_PREFIX:
1093
- @app.post(f"/{PATH_PREFIX}/v1/chat/completions")
1094
- async def chat_prefixed(
1095
- req: ChatRequest,
1096
- request: Request,
1097
- authorization: Optional[str] = Header(None)
1098
- ):
1099
- return await chat(req, request, authorization)
1100
-
1101
  # chat实现函数
1102
  async def chat_impl(
1103
  req: ChatRequest,
@@ -1107,6 +1115,62 @@ async def chat_impl(
1107
  # 生成请求ID(最优先,用于所有日志追踪)
1108
  request_id = str(uuid.uuid4())[:6]
1109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1110
  # 获取客户端IP(用于会话隔离)
1111
  client_ip = request.headers.get("x-forwarded-for")
1112
  if client_ip:
@@ -1116,13 +1180,17 @@ async def chat_impl(
1116
 
1117
  # 记录请求统计
1118
  async with stats_lock:
 
1119
  global_stats["total_requests"] += 1
1120
- global_stats["request_timestamps"].append(time.time())
 
 
1121
  await save_stats(global_stats)
1122
 
1123
  # 2. 模型校验
1124
  if req.model not in MODEL_MAPPING:
1125
  logger.error(f"[CHAT] [req_{request_id}] 不支持的模型: {req.model}")
 
1126
  raise HTTPException(
1127
  status_code=404,
1128
  detail=f"Model '{req.model}' not found. Available models: {list(MODEL_MAPPING.keys())}"
@@ -1173,9 +1241,12 @@ async def chat_impl(
1173
  account_id = account_manager.config.account_id if 'account_manager' in locals() and account_manager else 'unknown'
1174
  logger.error(f"[CHAT] [req_{request_id}] 账户 {account_id} 创建会话���败 (尝试 {attempt + 1}/{max_account_tries}) - {error_type}: {str(e)}")
1175
  # 记录账号池状态(单个账户失败)
1176
- uptime_tracker.record_request("account_pool", False)
 
1177
  if attempt == max_account_tries - 1:
1178
  logger.error(f"[CHAT] [req_{request_id}] 所有账户均不可用")
 
 
1179
  raise HTTPException(503, f"All accounts unavailable: {str(last_error)[:100]}")
1180
  # 继续尝试下一个账户
1181
 
@@ -1200,7 +1271,16 @@ async def chat_impl(
1200
  logger.info(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 用户消息: {preview}")
1201
 
1202
  # 3. 解析请求内容
1203
- last_text, current_images = await parse_last_message(req.messages, http_client, request_id)
 
 
 
 
 
 
 
 
 
1204
 
1205
  # 4. 准备文本内容
1206
  if is_new_conversation:
@@ -1293,14 +1373,24 @@ async def chat_impl(
1293
  global_stats["account_conversations"][account_manager.config.account_id] = account_manager.conversation_count
1294
  await save_stats(global_stats)
1295
 
 
 
1296
  break
1297
 
1298
  except (httpx.HTTPError, ssl.SSLError, HTTPException) as e:
 
 
 
 
 
 
1299
  # 记录当前失败的账户
1300
  failed_accounts.add(account_manager.config.account_id)
1301
 
1302
  # 记录账号池状态(请求失败)
1303
- uptime_tracker.record_request("account_pool", False)
 
 
1304
 
1305
  # 检查是否为429错误(Rate Limit)
1306
  is_rate_limit = isinstance(e, HTTPException) and e.status_code == 429
@@ -1349,7 +1439,8 @@ async def chat_impl(
1349
  break
1350
 
1351
  if not new_account:
1352
- logger.error(f"[CHAT] [req_{request_id}] 所有账户均已失败,无可用账户")
 
1353
  if req.stream: yield f"data: {json.dumps({'error': {'message': 'All Accounts Failed'}})}\n\n"
1354
  return
1355
 
@@ -1376,12 +1467,20 @@ async def chat_impl(
1376
  error_type = type(create_err).__name__
1377
  logger.error(f"[CHAT] [req_{request_id}] 账户切换失败 ({error_type}): {str(create_err)}")
1378
  # 记录账号池状态(账户切换失败)
1379
- uptime_tracker.record_request("account_pool", False)
 
 
 
 
 
 
1380
  if req.stream: yield f"data: {json.dumps({'error': {'message': 'Account Failover Failed'}})}\n\n"
1381
  return
1382
  else:
1383
  # 已达到最大重试次数
1384
  logger.error(f"[CHAT] [req_{request_id}] 已达到最大重试次数 ({max_retries}),请求失败")
 
 
1385
  if req.stream: yield f"data: {json.dumps({'error': {'message': f'Max retries ({max_retries}) exceeded: {e}'}})}\n\n"
1386
  return
1387
 
@@ -1465,6 +1564,8 @@ def parse_images_from_response(data_list: list) -> tuple[list, str]:
1465
 
1466
  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):
1467
  start_time = time.time()
 
 
1468
 
1469
  # 记录发送给API的内容
1470
  text_preview = text_content[:500] + "...(已截断)" if len(text_content) > 500 else text_content
@@ -1523,6 +1624,7 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
1523
  ) as r:
1524
  if r.status_code != 200:
1525
  error_text = await r.aread()
 
1526
  raise HTTPException(status_code=r.status_code, detail=f"Upstream Error {error_text.decode()}")
1527
 
1528
  # 使用异步解析器处理 JSON 数组流
@@ -1544,7 +1646,10 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
1544
  chunk = create_chunk(chat_id, created_time, model_name, {"reasoning_content": text}, None)
1545
  yield f"data: {chunk}\n\n"
1546
  else:
 
 
1547
  # 正常内容使用 content 字段
 
1548
  chunk = create_chunk(chat_id, created_time, model_name, {"content": text}, None)
1549
  yield f"data: {chunk}\n\n"
1550
 
@@ -1556,9 +1661,11 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
1556
  logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 检测到{len(file_ids)}张生成图片")
1557
 
1558
  except ValueError as e:
 
1559
  logger.error(f"[API] [{account_manager.config.account_id}] [req_{request_id}] JSON解析失败: {str(e)}")
1560
  except Exception as e:
1561
  error_type = type(e).__name__
 
1562
  logger.error(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 流处理错误 ({error_type}): {str(e)}")
1563
  raise
1564
 
@@ -1593,16 +1700,26 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
1593
  continue
1594
 
1595
  try:
1596
- image_url = save_image_to_hf(result, chat_id, fid, mime, base_url, IMAGE_DIR)
1597
- logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}已保存: {image_url}")
1598
- success_count += 1
 
 
 
 
 
 
 
 
 
 
1599
 
1600
- markdown = f"\n\n![生成的图片]({image_url})\n\n"
1601
  chunk = create_chunk(chat_id, created_time, model_name, {"content": markdown}, None)
1602
  yield f"data: {chunk}\n\n"
1603
  except Exception as save_error:
1604
- logger.error(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}保存失败: {str(save_error)[:100]}")
1605
- error_msg = f"\n\n⚠️ 图片 {idx} 保存失败\n\n"
1606
  chunk = create_chunk(chat_id, created_time, model_name, {"content": error_msg}, None)
1607
  yield f"data: {chunk}\n\n"
1608
 
@@ -1615,6 +1732,16 @@ async def stream_chat_generator(session: str, text_content: str, file_ids: List[
1615
  chunk = create_chunk(chat_id, created_time, model_name, {"content": error_msg}, None)
1616
  yield f"data: {chunk}\n\n"
1617
 
 
 
 
 
 
 
 
 
 
 
1618
  total_time = time.time() - start_time
1619
  logger.info(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 响应完成: {total_time:.2f}秒")
1620
 
@@ -1631,10 +1758,6 @@ async def get_public_uptime(days: int = 90):
1631
  days = 90
1632
  return await uptime_tracker.get_uptime_summary(days)
1633
 
1634
- @app.get("/public/uptime/html")
1635
- async def get_public_uptime_html(request: Request):
1636
- """Uptime 监控页面(类似 status.openai.com)"""
1637
- return templates.TemplateResponse("public/uptime.html", {"request": request})
1638
 
1639
  @app.get("/public/stats")
1640
  async def get_public_stats():
@@ -1642,14 +1765,14 @@ async def get_public_stats():
1642
  async with stats_lock:
1643
  # 清理1小时前的请求时间戳
1644
  current_time = time.time()
1645
- global_stats["request_timestamps"] = [
1646
  ts for ts in global_stats["request_timestamps"]
1647
  if current_time - ts < 3600
1648
  ]
1649
 
1650
  # 计算每分钟请求数
1651
  recent_minute = [
1652
- ts for ts in global_stats["request_timestamps"]
1653
  if current_time - ts < 60
1654
  ]
1655
  requests_per_minute = len(recent_minute)
@@ -1673,59 +1796,74 @@ async def get_public_stats():
1673
  "load_color": load_color
1674
  }
1675
 
 
 
 
 
 
 
 
 
1676
  @app.get("/public/log")
1677
  async def get_public_logs(request: Request, limit: int = 100):
1678
- """获取脱敏后的日志(JSON格式)"""
1679
  try:
1680
  # 基于IP的访问统计(24小时内去重)
1681
- # 优先从 X-Forwarded-For 获取真实IP(处理代理情况)
1682
- client_ip = request.headers.get("x-forwarded-for")
1683
- if client_ip:
1684
- # X-Forwarded-For 可能包含多个IP,取第一个
1685
- client_ip = client_ip.split(",")[0].strip()
1686
- else:
1687
- # 没有代理时使用直连IP
1688
- client_ip = request.client.host if request.client else "unknown"
1689
-
1690
  current_time = time.time()
1691
 
1692
  async with stats_lock:
1693
  # 清理24小时前的IP记录
1694
  if "visitor_ips" not in global_stats:
1695
  global_stats["visitor_ips"] = {}
1696
-
1697
- expired_ips = [
1698
- ip for ip, timestamp in global_stats["visitor_ips"].items()
1699
- if current_time - timestamp > 86400 # 24小时
1700
- ]
1701
- for ip in expired_ips:
1702
- del global_stats["visitor_ips"][ip]
1703
 
1704
  # 记录新访问(24小时内同一IP只计数一次)
1705
  if client_ip not in global_stats["visitor_ips"]:
1706
  global_stats["visitor_ips"][client_ip] = current_time
 
1707
 
1708
- # 同步访问者计数(清理后的实际数量)
1709
- global_stats["total_visitors"] = len(global_stats["visitor_ips"])
1710
  await save_stats(global_stats)
1711
 
 
 
1712
  sanitized_logs = get_sanitized_logs(limit=min(limit, 1000))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1713
  return {
1714
- "total": len(sanitized_logs),
1715
- "logs": sanitized_logs
1716
  }
1717
  except Exception as e:
1718
  logger.error(f"[LOG] 获取公开日志失败: {e}")
1719
  return {"total": 0, "logs": [], "error": str(e)}
1720
-
1721
- @app.get("/public/log/html")
1722
- async def get_public_logs_html(request: Request):
1723
- """公开的脱敏日志查看器"""
1724
- return templates.TemplateResponse("public/logs.html", {
1725
- "request": request,
1726
- "logo_url": LOGO_URL,
1727
- "chat_url": CHAT_URL
1728
- })
1729
 
1730
  # ---------- 全局 404 处理(必须在最后) ----------
1731
 
@@ -1739,4 +1877,5 @@ async def not_found_handler(request: Request, exc: HTTPException):
1739
 
1740
  if __name__ == "__main__":
1741
  import uvicorn
1742
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
1
+ import json, time, os, asyncio, uuid, ssl, re, yaml, shutil, base64
2
  from datetime import datetime, timezone, timedelta
3
  from typing import List, Optional, Union, Dict, Any
4
  from pathlib import Path
 
8
  import httpx
9
  import aiofiles
10
  from fastapi import FastAPI, HTTPException, Header, Request, Body, Form
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
13
  from fastapi.staticfiles import StaticFiles
14
  from pydantic import BaseModel
15
  from util.streaming_parser import parse_json_array_stream_async
 
70
  from core import uptime as uptime_tracker
71
 
72
  # 导入配置管理和模板系统
 
73
  from core.config import config_manager, config
 
74
 
75
  # ---------- 日志配置 ----------
76
 
 
82
  stats_lock = asyncio.Lock() # 改为异步锁
83
 
84
  async def load_stats():
85
+ """加载统计数据(异步)"""
86
  try:
87
  if os.path.exists(STATS_FILE):
88
  async with aiofiles.open(STATS_FILE, 'r', encoding='utf-8') as f:
 
93
  return {
94
  "total_visitors": 0,
95
  "total_requests": 0,
96
+ "request_timestamps": [],
97
+ "model_request_timestamps": {},
98
+ "failure_timestamps": [],
99
+ "rate_limit_timestamps": [],
100
+ "visitor_ips": {},
101
+ "account_conversations": {},
102
+ "recent_conversations": []
103
  }
104
 
105
  async def save_stats(stats):
 
115
  "total_visitors": 0,
116
  "total_requests": 0,
117
  "request_timestamps": [],
118
+ "model_request_timestamps": {},
119
+ "failure_timestamps": [],
120
+ "rate_limit_timestamps": [],
121
  "visitor_ips": {},
122
+ "account_conversations": {},
123
+ "recent_conversations": []
124
  }
125
 
126
+
127
+ def get_beijing_time_str(ts: Optional[float] = None) -> str:
128
+ tz = timezone(timedelta(hours=8))
129
+ current = datetime.fromtimestamp(ts or time.time(), tz=tz)
130
+ return current.strftime("%Y-%m-%d %H:%M:%S")
131
+
132
+
133
+ def build_recent_conversation_entry(
134
+ request_id: str,
135
+ model: Optional[str],
136
+ message_count: Optional[int],
137
+ start_ts: float,
138
+ status: str,
139
+ duration_s: Optional[float] = None,
140
+ error_detail: Optional[str] = None,
141
+ ) -> dict:
142
+ start_time = get_beijing_time_str(start_ts)
143
+ if model:
144
+ start_content = f"{model}"
145
+ if message_count:
146
+ start_content = f"{model} | {message_count}条消息"
147
+ else:
148
+ start_content = "请求处理中"
149
+
150
+ events = [{
151
+ "time": start_time,
152
+ "type": "start",
153
+ "content": start_content,
154
+ }]
155
+
156
+ end_time = get_beijing_time_str(start_ts + duration_s) if duration_s is not None else get_beijing_time_str()
157
+
158
+ if status == "success":
159
+ if duration_s is not None:
160
+ events.append({
161
+ "time": end_time,
162
+ "type": "complete",
163
+ "status": "success",
164
+ "content": f"响应完成 | 耗时{duration_s:.2f}s",
165
+ })
166
+ else:
167
+ events.append({
168
+ "time": end_time,
169
+ "type": "complete",
170
+ "status": "success",
171
+ "content": "响应完成",
172
+ })
173
+ elif status == "timeout":
174
+ events.append({
175
+ "time": end_time,
176
+ "type": "complete",
177
+ "status": "timeout",
178
+ "content": "请求超时",
179
+ })
180
+ else:
181
+ detail = error_detail or "请求失败"
182
+ events.append({
183
+ "time": end_time,
184
+ "type": "complete",
185
+ "status": "error",
186
+ "content": detail[:120],
187
+ })
188
+
189
+ return {
190
+ "request_id": request_id,
191
+ "start_time": start_time,
192
+ "start_ts": start_ts,
193
+ "status": status,
194
+ "events": events,
195
+ }
196
+
197
  class MemoryLogHandler(logging.Handler):
198
  """自定义日志处理器,将日志写入内存缓冲区"""
199
  def emit(self, record):
 
225
  # 所有配置通过 config_manager 访问,优先级:环境变量 > YAML > 默认值
226
  TIMEOUT_SECONDS = 600
227
  API_KEY = config.basic.api_key
 
228
  ADMIN_KEY = config.security.admin_key
229
  PROXY = config.basic.proxy
230
  BASE_URL = config.basic.base_url
 
281
 
282
  return f"{forwarded_proto}://{forwarded_host}"
283
 
284
+
285
+
286
  # ---------- 常量定义 ----------
287
  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"
288
 
 
309
  sys.exit(1)
310
 
311
  # 启动日志
312
+ logger.info("[SYSTEM] API端点: /v1/chat/completions")
313
+ logger.info("[SYSTEM] Admin API endpoints: /admin/*")
314
+ logger.info("[SYSTEM] Public endpoints: /public/log, /public/stats, /public/uptime")
 
 
 
 
 
 
315
  logger.info(f"[SYSTEM] Session过期时间: {SESSION_EXPIRE_HOURS}小时")
316
  logger.info("[SYSTEM] 系统初始化完成")
317
 
 
327
  # ---------- OpenAI 兼容接口 ----------
328
  app = FastAPI(title="Gemini-Business OpenAI Gateway")
329
 
330
+ frontend_origin = os.getenv("FRONTEND_ORIGIN", "").strip()
331
+ allow_all_origins = os.getenv("ALLOW_ALL_ORIGINS", "0") == "1"
332
+ if allow_all_origins and not frontend_origin:
333
+ app.add_middleware(
334
+ CORSMiddleware,
335
+ allow_origins=["*"],
336
+ allow_credentials=False,
337
+ allow_methods=["*"],
338
+ allow_headers=["*"],
339
+ )
340
+ elif frontend_origin:
341
+ app.add_middleware(
342
+ CORSMiddleware,
343
+ allow_origins=[frontend_origin],
344
+ allow_credentials=True,
345
+ allow_methods=["*"],
346
+ allow_headers=["*"],
347
+ )
348
 
 
349
  app.mount("/static", StaticFiles(directory="static"), name="static")
350
+ if os.path.exists(os.path.join("static", "assets")):
351
+ app.mount("/assets", StaticFiles(directory=os.path.join("static", "assets")), name="assets")
352
+ if os.path.exists(os.path.join("static", "vendor")):
353
+ app.mount("/vendor", StaticFiles(directory=os.path.join("static", "vendor")), name="vendor")
354
+
355
+ @app.get("/")
356
+ async def serve_frontend_index():
357
+ index_path = os.path.join("static", "index.html")
358
+ if os.path.exists(index_path):
359
+ return FileResponse(index_path)
360
+ raise HTTPException(404, "Not Found")
361
+
362
+ @app.get("/logo.svg")
363
+ async def serve_logo():
364
+ logo_path = os.path.join("static", "logo.svg")
365
+ if os.path.exists(logo_path):
366
+ return FileResponse(logo_path)
367
+ raise HTTPException(404, "Not Found")
368
 
369
  # ---------- Session 中间件配置 ----------
370
  from starlette.middleware.sessions import SessionMiddleware
 
379
  # ---------- Uptime 追踪中间件 ----------
380
  @app.middleware("http")
381
  async def track_uptime_middleware(request: Request, call_next):
382
+ """Uptime 监控:跟踪非对话接口的请求结果。"""
 
383
  path = request.url.path
384
+ if (
385
+ path.startswith("/images/")
386
+ or path.startswith("/public/")
387
+ or path.startswith("/favicon")
388
+ or path.endswith("/v1/chat/completions")
389
+ ):
390
  return await call_next(request)
391
 
392
  start_time = time.time()
 
 
393
 
394
  try:
395
  response = await call_next(request)
396
+ latency_ms = int((time.time() - start_time) * 1000)
397
  success = response.status_code < 400
398
+ uptime_tracker.record_request("api_service", success, latency_ms, response.status_code)
 
 
 
 
 
 
 
 
 
 
 
399
  return response
400
 
401
+ except Exception:
 
 
 
 
402
  uptime_tracker.record_request("api_service", False)
 
 
403
  raise
404
 
405
+
406
  # ---------- 图片静态服务初始化 ----------
407
  os.makedirs(IMAGE_DIR, exist_ok=True)
408
  app.mount("/images", StaticFiles(directory=IMAGE_DIR), name="images")
 
428
 
429
  # 加载统计数据
430
  global_stats = await load_stats()
431
+ global_stats.setdefault("request_timestamps", [])
432
+ global_stats.setdefault("model_request_timestamps", {})
433
+ global_stats.setdefault("failure_timestamps", [])
434
+ global_stats.setdefault("rate_limit_timestamps", [])
435
+ global_stats.setdefault("recent_conversations", [])
436
+ uptime_tracker.configure_storage(os.path.join(DATA_DIR, "uptime.json"))
437
+ uptime_tracker.load_heartbeats()
438
  logger.info(f"[SYSTEM] 统计数据已加载: {global_stats['total_requests']} 次请求, {global_stats['total_visitors']} 位访客")
439
 
440
  # 启动缓存清理任务
441
  asyncio.create_task(multi_account_mgr.start_background_cleanup())
442
  logger.info("[SYSTEM] 后台缓存清理任务已启动(间隔: 5分钟)")
443
 
 
 
 
 
444
  # ---------- 日志脱敏函数 ----------
445
  def get_sanitized_logs(limit: int = 100) -> list:
446
  """获取脱敏后的日志列表,按请求ID分组并提取关键事件"""
 
672
  "system_fingerprint": None # OpenAI 标准字段(可选)
673
  }
674
  return json.dumps(chunk)
675
+ # ---------- Auth endpoints (API) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676
 
677
  @app.post("/login")
678
  async def admin_login_post(request: Request, admin_key: str = Form(...)):
679
+ """Admin login (API)"""
680
  if admin_key == ADMIN_KEY:
681
  login_user(request)
682
+ logger.info("[AUTH] Admin login success")
683
+ return {"success": True}
684
+ logger.warning("[AUTH] Login failed - invalid key")
685
+ raise HTTPException(401, "Invalid key")
686
+
687
 
688
  @app.post("/logout")
689
  @require_login(redirect_to_login=False)
690
  async def admin_logout(request: Request):
691
+ """Admin logout (API)"""
692
  logout_user(request)
693
+ logger.info("[AUTH] Admin logout")
694
+ return {"success": True}
695
+
696
+ @app.get("/admin/health")
697
+ @require_login()
698
+ async def admin_health(request: Request):
699
+ return {"status": "ok", "time": datetime.utcnow().isoformat()}
700
+
701
+ @app.get("/admin/stats")
702
+ @require_login()
703
+ async def admin_stats(request: Request):
704
+ now = time.time()
705
+ window_seconds = 12 * 3600
706
+
707
+ active_accounts = 0
708
+ failed_accounts = 0
709
+ rate_limited_accounts = 0
710
+ idle_accounts = 0
711
+
712
+ for account_manager in multi_account_mgr.accounts.values():
713
+ config = account_manager.config
714
+ cooldown_seconds, cooldown_reason = account_manager.get_cooldown_info()
715
+ is_rate_limited = cooldown_seconds > 0 and cooldown_reason and "429" in cooldown_reason
716
+ is_expired = config.is_expired()
717
+ is_auto_disabled = (not account_manager.is_available) and (not config.disabled)
718
+ is_failed = is_auto_disabled or is_expired or cooldown_reason == "错误禁用"
719
+ is_active = (not is_failed) and (not config.disabled) and (not is_rate_limited)
720
+
721
+ if is_rate_limited:
722
+ rate_limited_accounts += 1
723
+ elif is_failed:
724
+ failed_accounts += 1
725
+ elif is_active:
726
+ active_accounts += 1
727
  else:
728
+ idle_accounts += 1
 
729
 
730
+ total_accounts = len(multi_account_mgr.accounts)
 
 
 
 
 
 
731
 
732
+ beijing_tz = timezone(timedelta(hours=8))
733
+ now_dt = datetime.now(beijing_tz)
734
+ start_dt = (now_dt - timedelta(hours=11)).replace(minute=0, second=0, microsecond=0)
735
+ start_ts = start_dt.timestamp()
736
+ labels = [(start_dt + timedelta(hours=i)).strftime("%H:00") for i in range(12)]
737
 
738
+ def bucketize(timestamps: list) -> list:
739
+ buckets = [0] * 12
740
+ for ts in timestamps:
741
+ idx = int((ts - start_ts) // 3600)
742
+ if 0 <= idx < 12:
743
+ buckets[idx] += 1
744
+ return buckets
745
 
746
+ async with stats_lock:
747
+ global_stats.setdefault("request_timestamps", [])
748
+ global_stats.setdefault("failure_timestamps", [])
749
+ global_stats.setdefault("rate_limit_timestamps", [])
750
+ global_stats.setdefault("model_request_timestamps", {})
751
+ global_stats["request_timestamps"] = [
752
+ ts for ts in global_stats["request_timestamps"]
753
+ if now - ts < window_seconds
754
+ ]
755
+ global_stats["failure_timestamps"] = [
756
+ ts for ts in global_stats["failure_timestamps"]
757
+ if now - ts < window_seconds
758
+ ]
759
+ global_stats["rate_limit_timestamps"] = [
760
+ ts for ts in global_stats["rate_limit_timestamps"]
761
+ if now - ts < window_seconds
762
+ ]
763
+ model_request_timestamps = {}
764
+ for model, timestamps in global_stats["model_request_timestamps"].items():
765
+ model_request_timestamps[model] = [
766
+ ts for ts in timestamps
767
+ if now - ts < window_seconds
768
+ ]
769
+ global_stats["model_request_timestamps"] = model_request_timestamps
770
 
771
+ await save_stats(global_stats)
772
 
773
+ request_timestamps = list(global_stats["request_timestamps"])
774
+ failure_timestamps = list(global_stats["failure_timestamps"])
775
+ rate_limit_timestamps = list(global_stats["rate_limit_timestamps"])
776
+ model_request_timestamps = global_stats.get("model_request_timestamps", {})
777
+ model_requests = {}
778
+ for model in MODEL_MAPPING.keys():
779
+ model_requests[model] = bucketize(model_request_timestamps.get(model, []))
780
+ for model, timestamps in model_request_timestamps.items():
781
+ if model not in model_requests:
782
+ model_requests[model] = bucketize(timestamps)
783
+
784
+ return {
785
+ "total_accounts": total_accounts,
786
+ "active_accounts": active_accounts,
787
+ "failed_accounts": failed_accounts,
788
+ "rate_limited_accounts": rate_limited_accounts,
789
+ "idle_accounts": idle_accounts,
790
+ "trend": {
791
+ "labels": labels,
792
+ "total_requests": bucketize(request_timestamps),
793
+ "failed_requests": bucketize(failure_timestamps),
794
+ "rate_limited_requests": bucketize(rate_limit_timestamps),
795
+ "model_requests": model_requests,
796
+ }
797
+ }
798
 
799
  @app.get("/admin/accounts")
800
  @require_login()
 
907
  logger.error(f"[CONFIG] 启用账户失败: {str(e)}")
908
  raise HTTPException(500, f"启用失败: {str(e)}")
909
 
910
+ # ---------- Auth endpoints (API) ----------
911
  @app.get("/admin/settings")
912
  @require_login()
913
  async def admin_get_settings(request: Request):
 
1079
  logger.info("[LOG] 日志已清空")
1080
  return {"status": "success", "message": "已清空内存日志", "cleared_count": cleared_count}
1081
 
1082
+ # ---------- Auth endpoints (API) ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1083
 
1084
  @app.get("/v1/models")
1085
  async def list_models(authorization: str = Header(None)):
 
1086
  data = []
1087
  now = int(time.time())
1088
  for m in MODEL_MAPPING.keys():
 
1091
 
1092
  @app.get("/v1/models/{model_id}")
1093
  async def get_model(model_id: str, authorization: str = Header(None)):
 
1094
  return {"id": model_id, "object": "model"}
1095
 
1096
+ # ---------- Auth endpoints (API) ----------
 
 
 
 
 
 
 
 
 
 
1097
 
1098
  @app.post("/v1/chat/completions")
1099
  async def chat(
 
1106
  # ... (保留原有的chat逻辑)
1107
  return await chat_impl(req, request, authorization)
1108
 
 
 
 
 
 
 
 
 
 
1109
  # chat实现函数
1110
  async def chat_impl(
1111
  req: ChatRequest,
 
1115
  # 生成请求ID(最优先,用于所有日志追踪)
1116
  request_id = str(uuid.uuid4())[:6]
1117
 
1118
+ start_ts = time.time()
1119
+ request.state.first_response_time = None
1120
+ message_count = len(req.messages)
1121
+
1122
+ monitor_recorded = False
1123
+
1124
+ async def finalize_result(
1125
+ status: str,
1126
+ status_code: Optional[int] = None,
1127
+ error_detail: Optional[str] = None
1128
+ ) -> None:
1129
+ nonlocal monitor_recorded
1130
+ if monitor_recorded:
1131
+ return
1132
+ monitor_recorded = True
1133
+ duration_s = time.time() - start_ts
1134
+ latency_ms = None
1135
+ first_response_time = getattr(request.state, "first_response_time", None)
1136
+ if first_response_time:
1137
+ latency_ms = int((first_response_time - start_ts) * 1000)
1138
+ else:
1139
+ latency_ms = int(duration_s * 1000)
1140
+
1141
+ uptime_tracker.record_request("api_service", status == "success", latency_ms, status_code)
1142
+
1143
+ entry = build_recent_conversation_entry(
1144
+ request_id=request_id,
1145
+ model=req.model if req else None,
1146
+ message_count=message_count,
1147
+ start_ts=start_ts,
1148
+ status=status,
1149
+ duration_s=duration_s if status == "success" else None,
1150
+ error_detail=error_detail,
1151
+ )
1152
+
1153
+ async with stats_lock:
1154
+ global_stats.setdefault("failure_timestamps", [])
1155
+ global_stats.setdefault("rate_limit_timestamps", [])
1156
+ global_stats.setdefault("recent_conversations", [])
1157
+ if status != "success":
1158
+ if status_code == 429:
1159
+ global_stats["rate_limit_timestamps"].append(time.time())
1160
+ else:
1161
+ global_stats["failure_timestamps"].append(time.time())
1162
+ global_stats["recent_conversations"].append(entry)
1163
+ global_stats["recent_conversations"] = global_stats["recent_conversations"][-60:]
1164
+ await save_stats(global_stats)
1165
+
1166
+ def classify_error_status(status_code: Optional[int], error: Exception) -> str:
1167
+ if status_code == 504:
1168
+ return "timeout"
1169
+ if isinstance(error, (asyncio.TimeoutError, httpx.TimeoutException)):
1170
+ return "timeout"
1171
+ return "error"
1172
+
1173
+
1174
  # 获取客户端IP(用于会话隔离)
1175
  client_ip = request.headers.get("x-forwarded-for")
1176
  if client_ip:
 
1180
 
1181
  # 记录请求统计
1182
  async with stats_lock:
1183
+ timestamp = time.time()
1184
  global_stats["total_requests"] += 1
1185
+ global_stats["request_timestamps"].append(timestamp)
1186
+ global_stats.setdefault("model_request_timestamps", {})
1187
+ global_stats["model_request_timestamps"].setdefault(req.model, []).append(timestamp)
1188
  await save_stats(global_stats)
1189
 
1190
  # 2. 模型校验
1191
  if req.model not in MODEL_MAPPING:
1192
  logger.error(f"[CHAT] [req_{request_id}] 不支持的模型: {req.model}")
1193
+ await finalize_result("error", 404, f"HTTP 404: Model '{req.model}' not found")
1194
  raise HTTPException(
1195
  status_code=404,
1196
  detail=f"Model '{req.model}' not found. Available models: {list(MODEL_MAPPING.keys())}"
 
1241
  account_id = account_manager.config.account_id if 'account_manager' in locals() and account_manager else 'unknown'
1242
  logger.error(f"[CHAT] [req_{request_id}] 账户 {account_id} 创建会话���败 (尝试 {attempt + 1}/{max_account_tries}) - {error_type}: {str(e)}")
1243
  # 记录账号池状态(单个账户失败)
1244
+ status_code = e.status_code if isinstance(e, HTTPException) else None
1245
+ uptime_tracker.record_request("account_pool", False, status_code=status_code)
1246
  if attempt == max_account_tries - 1:
1247
  logger.error(f"[CHAT] [req_{request_id}] 所有账户均不可用")
1248
+ status = classify_error_status(503, last_error if isinstance(last_error, Exception) else Exception("account_pool_unavailable"))
1249
+ await finalize_result(status, 503, f"All accounts unavailable: {str(last_error)[:100]}")
1250
  raise HTTPException(503, f"All accounts unavailable: {str(last_error)[:100]}")
1251
  # 继续尝试下一个账户
1252
 
 
1271
  logger.info(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] 用户消息: {preview}")
1272
 
1273
  # 3. 解析请求内容
1274
+ try:
1275
+ last_text, current_images = await parse_last_message(req.messages, http_client, request_id)
1276
+ except HTTPException as e:
1277
+ status = classify_error_status(e.status_code, e)
1278
+ await finalize_result(status, e.status_code, f"HTTP {e.status_code}: {e.detail}")
1279
+ raise
1280
+ except Exception as e:
1281
+ status = classify_error_status(None, e)
1282
+ await finalize_result(status, 500, f"{type(e).__name__}: {str(e)[:200]}")
1283
+ raise
1284
 
1285
  # 4. 准备文本内容
1286
  if is_new_conversation:
 
1373
  global_stats["account_conversations"][account_manager.config.account_id] = account_manager.conversation_count
1374
  await save_stats(global_stats)
1375
 
1376
+ await finalize_result("success", 200, None)
1377
+
1378
  break
1379
 
1380
  except (httpx.HTTPError, ssl.SSLError, HTTPException) as e:
1381
+ status_code = e.status_code if isinstance(e, HTTPException) else None
1382
+ error_detail = (
1383
+ f"HTTP {e.status_code}: {e.detail}"
1384
+ if isinstance(e, HTTPException)
1385
+ else f"{type(e).__name__}: {str(e)[:200]}"
1386
+ )
1387
  # 记录当前失败的账户
1388
  failed_accounts.add(account_manager.config.account_id)
1389
 
1390
  # 记录账号池状态(请求失败)
1391
+ status_code = e.status_code if isinstance(e, HTTPException) else None
1392
+
1393
+ uptime_tracker.record_request("account_pool", False, status_code=status_code)
1394
 
1395
  # 检查是否为429错误(Rate Limit)
1396
  is_rate_limit = isinstance(e, HTTPException) and e.status_code == 429
 
1439
  break
1440
 
1441
  if not new_account:
1442
+ logger.error(f"[CHAT] [req_{request_id}] All accounts failed, no available account")
1443
+ await finalize_result("error", 503, "All Accounts Failed")
1444
  if req.stream: yield f"data: {json.dumps({'error': {'message': 'All Accounts Failed'}})}\n\n"
1445
  return
1446
 
 
1467
  error_type = type(create_err).__name__
1468
  logger.error(f"[CHAT] [req_{request_id}] 账户切换失败 ({error_type}): {str(create_err)}")
1469
  # 记录账号池状态(账户切换失败)
1470
+ status_code = create_err.status_code if isinstance(create_err, HTTPException) else None
1471
+
1472
+ uptime_tracker.record_request("account_pool", False, status_code=status_code)
1473
+
1474
+ status = classify_error_status(status_code, create_err)
1475
+
1476
+ await finalize_result(status, status_code, f"Account Failover Failed: {str(create_err)[:200]}")
1477
  if req.stream: yield f"data: {json.dumps({'error': {'message': 'Account Failover Failed'}})}\n\n"
1478
  return
1479
  else:
1480
  # 已达到最大重试次数
1481
  logger.error(f"[CHAT] [req_{request_id}] 已达到最大重试次数 ({max_retries}),请求失败")
1482
+ status = classify_error_status(status_code, e)
1483
+ await finalize_result(status, status_code, error_detail)
1484
  if req.stream: yield f"data: {json.dumps({'error': {'message': f'Max retries ({max_retries}) exceeded: {e}'}})}\n\n"
1485
  return
1486
 
 
1564
 
1565
  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):
1566
  start_time = time.time()
1567
+ full_content = ""
1568
+ first_response_time = None
1569
 
1570
  # 记录发送给API的内容
1571
  text_preview = text_content[:500] + "...(已截断)" if len(text_content) > 500 else text_content
 
1624
  ) as r:
1625
  if r.status_code != 200:
1626
  error_text = await r.aread()
1627
+ uptime_tracker.record_request(model_name, False, status_code=r.status_code)
1628
  raise HTTPException(status_code=r.status_code, detail=f"Upstream Error {error_text.decode()}")
1629
 
1630
  # 使用异步解析器处理 JSON 数组流
 
1646
  chunk = create_chunk(chat_id, created_time, model_name, {"reasoning_content": text}, None)
1647
  yield f"data: {chunk}\n\n"
1648
  else:
1649
+ if first_response_time is None:
1650
+ first_response_time = time.time()
1651
  # 正常内容使用 content 字段
1652
+ full_content += text
1653
  chunk = create_chunk(chat_id, created_time, model_name, {"content": text}, None)
1654
  yield f"data: {chunk}\n\n"
1655
 
 
1661
  logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 检测到{len(file_ids)}张生成图片")
1662
 
1663
  except ValueError as e:
1664
+ uptime_tracker.record_request(model_name, False)
1665
  logger.error(f"[API] [{account_manager.config.account_id}] [req_{request_id}] JSON解析失败: {str(e)}")
1666
  except Exception as e:
1667
  error_type = type(e).__name__
1668
+ uptime_tracker.record_request(model_name, False)
1669
  logger.error(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 流处理错误 ({error_type}): {str(e)}")
1670
  raise
1671
 
 
1700
  continue
1701
 
1702
  try:
1703
+ # 根据配置选择输出格式
1704
+ output_format = config_manager.image_output_format
1705
+
1706
+ if output_format == "base64":
1707
+ # Base64 模式:直接返回 base64 编码
1708
+ b64 = base64.b64encode(result).decode()
1709
+ markdown = f"\n\n![生成的图片](data:{mime};base64,{b64})\n\n"
1710
+ logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}已编码为base64")
1711
+ else:
1712
+ # URL 模式:保存到本地并返回 URL
1713
+ image_url = save_image_to_hf(result, chat_id, fid, mime, base_url, IMAGE_DIR)
1714
+ markdown = f"\n\n![生成的图片]({image_url})\n\n"
1715
+ logger.info(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}已保存: {image_url}")
1716
 
1717
+ success_count += 1
1718
  chunk = create_chunk(chat_id, created_time, model_name, {"content": markdown}, None)
1719
  yield f"data: {chunk}\n\n"
1720
  except Exception as save_error:
1721
+ logger.error(f"[IMAGE] [{account_manager.config.account_id}] [req_{request_id}] 图片{idx}处理失败: {str(save_error)[:100]}")
1722
+ error_msg = f"\n\n⚠️ 图片 {idx} 处理失败\n\n"
1723
  chunk = create_chunk(chat_id, created_time, model_name, {"content": error_msg}, None)
1724
  yield f"data: {chunk}\n\n"
1725
 
 
1732
  chunk = create_chunk(chat_id, created_time, model_name, {"content": error_msg}, None)
1733
  yield f"data: {chunk}\n\n"
1734
 
1735
+ if full_content:
1736
+ response_preview = full_content[:500] + "...(已截断)" if len(full_content) > 500 else full_content
1737
+ logger.info(f"[CHAT] [{account_manager.config.account_id}] [req_{request_id}] AI响应: {response_preview}")
1738
+
1739
+ if first_response_time:
1740
+ latency_ms = int((first_response_time - start_time) * 1000)
1741
+ uptime_tracker.record_request(model_name, True, latency_ms)
1742
+ else:
1743
+ uptime_tracker.record_request(model_name, True)
1744
+
1745
  total_time = time.time() - start_time
1746
  logger.info(f"[API] [{account_manager.config.account_id}] [req_{request_id}] 响应完成: {total_time:.2f}秒")
1747
 
 
1758
  days = 90
1759
  return await uptime_tracker.get_uptime_summary(days)
1760
 
 
 
 
 
1761
 
1762
  @app.get("/public/stats")
1763
  async def get_public_stats():
 
1765
  async with stats_lock:
1766
  # 清理1小时前的请求时间戳
1767
  current_time = time.time()
1768
+ recent_requests = [
1769
  ts for ts in global_stats["request_timestamps"]
1770
  if current_time - ts < 3600
1771
  ]
1772
 
1773
  # 计算每分钟请求数
1774
  recent_minute = [
1775
+ ts for ts in recent_requests
1776
  if current_time - ts < 60
1777
  ]
1778
  requests_per_minute = len(recent_minute)
 
1796
  "load_color": load_color
1797
  }
1798
 
1799
+ @app.get("/public/display")
1800
+ async def get_public_display():
1801
+ """获取公开展示信息"""
1802
+ return {
1803
+ "logo_url": LOGO_URL,
1804
+ "chat_url": CHAT_URL
1805
+ }
1806
+
1807
  @app.get("/public/log")
1808
  async def get_public_logs(request: Request, limit: int = 100):
 
1809
  try:
1810
  # 基于IP的访问统计(24小时内去重)
1811
+ client_ip = request.client.host
 
 
 
 
 
 
 
 
1812
  current_time = time.time()
1813
 
1814
  async with stats_lock:
1815
  # 清理24小时前的IP记录
1816
  if "visitor_ips" not in global_stats:
1817
  global_stats["visitor_ips"] = {}
1818
+ global_stats["visitor_ips"] = {
1819
+ ip: timestamp for ip, timestamp in global_stats["visitor_ips"].items()
1820
+ if current_time - timestamp <= 86400
1821
+ }
 
 
 
1822
 
1823
  # 记录新访问(24小时内同一IP只计数一次)
1824
  if client_ip not in global_stats["visitor_ips"]:
1825
  global_stats["visitor_ips"][client_ip] = current_time
1826
+ global_stats["total_visitors"] = global_stats.get("total_visitors", 0) + 1
1827
 
1828
+ global_stats.setdefault("recent_conversations", [])
 
1829
  await save_stats(global_stats)
1830
 
1831
+ stored_logs = list(global_stats.get("recent_conversations", []))
1832
+
1833
  sanitized_logs = get_sanitized_logs(limit=min(limit, 1000))
1834
+
1835
+ log_map = {log.get("request_id"): log for log in sanitized_logs}
1836
+ for log in stored_logs:
1837
+ request_id = log.get("request_id")
1838
+ if request_id and request_id not in log_map:
1839
+ log_map[request_id] = log
1840
+
1841
+ def get_log_ts(item: dict) -> float:
1842
+ if "start_ts" in item:
1843
+ return float(item["start_ts"])
1844
+ try:
1845
+ return datetime.strptime(item.get("start_time", ""), "%Y-%m-%d %H:%M:%S").timestamp()
1846
+ except Exception:
1847
+ return 0.0
1848
+
1849
+ merged_logs = sorted(log_map.values(), key=get_log_ts, reverse=True)[:min(limit, 1000)]
1850
+ output_logs = []
1851
+ for log in merged_logs:
1852
+ if "start_ts" in log:
1853
+ log = dict(log)
1854
+ log.pop("start_ts", None)
1855
+ output_logs.append(log)
1856
+
1857
  return {
1858
+ "total": len(output_logs),
1859
+ "logs": output_logs
1860
  }
1861
  except Exception as e:
1862
  logger.error(f"[LOG] 获取公开日志失败: {e}")
1863
  return {"total": 0, "logs": [], "error": str(e)}
1864
+ except Exception as e:
1865
+ logger.error(f"[LOG] 获取公开日志失败: {e}")
1866
+ return {"total": 0, "logs": [], "error": str(e)}
 
 
 
 
 
 
1867
 
1868
  # ---------- 全局 404 处理(必须在最后) ----------
1869
 
 
1877
 
1878
  if __name__ == "__main__":
1879
  import uvicorn
1880
+ port = int(os.getenv("PORT", "7860"))
1881
+ uvicorn.run(app, host="0.0.0.0", port=port)