JXJBing commited on
Commit
3e9d0d5
·
verified ·
1 Parent(s): d7279ff

Delete sora-api-master

Browse files
Files changed (36) hide show
  1. sora-api-master/.gitignore +0 -53
  2. sora-api-master/Dockerfile +0 -29
  3. sora-api-master/README.md +0 -485
  4. sora-api-master/api_keys.json.example +0 -12
  5. sora-api-master/docs/image_localization.md +0 -1
  6. sora-api-master/requirements.txt +0 -28
  7. sora-api-master/run.py +0 -44
  8. sora-api-master/src/__init__.py +0 -2
  9. sora-api-master/src/api/__init__.py +0 -29
  10. sora-api-master/src/api/admin.py +0 -463
  11. sora-api-master/src/api/auth.py +0 -96
  12. sora-api-master/src/api/chat.py +0 -168
  13. sora-api-master/src/api/dependencies.py +0 -131
  14. sora-api-master/src/api/generation.py +0 -159
  15. sora-api-master/src/api/health.py +0 -57
  16. sora-api-master/src/app.py +0 -171
  17. sora-api-master/src/app.py.backup +0 -1159
  18. sora-api-master/src/app.py.optimized +0 -129
  19. sora-api-master/src/config.py +0 -179
  20. sora-api-master/src/key_manager.py +0 -777
  21. sora-api-master/src/main.py +0 -44
  22. sora-api-master/src/models/__init__.py +0 -1
  23. sora-api-master/src/models/schemas.py +0 -86
  24. sora-api-master/src/services/__init__.py +0 -1
  25. sora-api-master/src/services/image_service.py +0 -235
  26. sora-api-master/src/services/streaming.py +0 -231
  27. sora-api-master/src/sora_generator.py +0 -1145
  28. sora-api-master/src/sora_integration.py +0 -114
  29. sora-api-master/src/static/admin/config.html +0 -99
  30. sora-api-master/src/static/admin/css/style.css +0 -424
  31. sora-api-master/src/static/admin/index.html +0 -512
  32. sora-api-master/src/static/admin/js/config.js +0 -121
  33. sora-api-master/src/static/admin/js/main.js +0 -1631
  34. sora-api-master/src/utils.py +0 -208
  35. sora-api-master/test_client.py +0 -180
  36. sora-api-master/test_image_download.py +0 -131
sora-api-master/.gitignore DELETED
@@ -1,53 +0,0 @@
1
- # 确保文本文件在仓库中使用 LF
2
- * text=auto eol=lf
3
- # API密钥配置
4
- api_keys.json
5
- admin_config.json
6
- *.key
7
-
8
- # 环境变量和配置
9
- .env
10
- *.env
11
- config.local.json
12
-
13
- # 生成的图像文件
14
- src/static/images/*
15
- !src/static/images/.gitkeep
16
-
17
- # Python
18
- __pycache__/
19
- *.py[cod]
20
- *$py.class
21
- *.so
22
- .Python
23
- env/
24
- build/
25
- develop-eggs/
26
- dist/
27
- downloads/
28
- eggs/
29
- .eggs/
30
- lib/
31
- lib64/
32
- parts/
33
- sdist/
34
- var/
35
- *.egg-info/
36
- .installed.cfg
37
- *.egg
38
- venv/
39
- .venv/
40
-
41
- # 日志文件
42
- *.log
43
- logs/
44
-
45
- # IDE配置
46
- .idea/
47
- .vscode/
48
- *.swp
49
- *.swo
50
- .DS_Store
51
-
52
- # Docker
53
- .dockerignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/Dockerfile DELETED
@@ -1,29 +0,0 @@
1
- FROM python:3.10-slim
2
-
3
- WORKDIR /app
4
-
5
- # 设置Python环境
6
- ENV PYTHONDONTWRITEBYTECODE=1
7
- ENV PYTHONUNBUFFERED=1
8
- ENV PYTHONIOENCODING=utf-8
9
-
10
- # 安装依赖
11
- COPY requirements.txt .
12
- RUN pip install --no-cache-dir -r requirements.txt
13
-
14
- # 复制应用代码
15
- COPY . .
16
-
17
- # 创建必要的目录
18
- RUN mkdir -p /app/src/static/images && \
19
- chmod -R 777 /app/src/static
20
-
21
- # 暴露端口
22
- EXPOSE 8890
23
-
24
- # 设置健康检查
25
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
26
- CMD curl -f http://localhost:8890/health || exit 1
27
-
28
- # 启动应用
29
- CMD ["python", "run.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/README.md DELETED
@@ -1,485 +0,0 @@
1
- # OpenAI兼容的Sora API服务
2
-
3
- 这是一个为Sora提供OpenAI兼容接口的API服务。该服务使用cloudscraper绕过Cloudflare验证,支持多key轮询、并发处理和标准的OpenAI接口格式。
4
-
5
- ## 功能特点
6
-
7
- - **OpenAI兼容接口**:完全兼容OpenAI的`/v1/chat/completions`接口
8
- - **CF验证绕过**:使用cloudscraper库成功绕过Cloudflare验证
9
- - **多key轮询**:支持多个Sora认证token,根据权重和速率限制智能选择
10
- - **并发处理**:支持多个并发请求
11
- - **流式响应**:支持SSE格式的流式响应
12
- - **图像处理**:支持文本到图像生成和图像到图像生成(Remix)
13
- - **异步处理**:支持异步生成图像,返回立即响应,防止请求超时
14
- - **状态查询**:提供API端点查询异步任务的状态和结果
15
- - **优化性能**:经过代码优化,提高请求处理速度和资源利用率
16
- - **健康检查**:支持容器健康检查功能
17
-
18
- ## 环境要求
19
-
20
- - Python 3.8+
21
- - FastAPI 0.95.0+
22
- - cloudscraper 1.2.71+
23
- - 其他依赖见requirements.txt
24
-
25
- ## 快速部署指南
26
-
27
-
28
- 你可以直接运行不指定任何环境变量,所有环境变量都可以在面板里面配置
29
-
30
-
31
- 管理员登录密钥默认:sk-123456
32
-
33
-
34
- api请求密钥不设置默认和管理员登录密钥相同
35
-
36
-
37
- docker 一键运行
38
- ```bash
39
- docker run -d -p 8890:8890 --name sora-api 1hei1/sora-api:latest
40
- ```
41
-
42
-
43
-
44
- ### 方法一:直接运行
45
-
46
- 1. 克隆仓库
47
- ```bash
48
- git clone https://github.com/1hei1/sora-api.git
49
- cd sora-api
50
- ```
51
-
52
- 2. 安装依赖
53
- ```bash
54
- pip install -r requirements.txt
55
- ```
56
-
57
- 3. 配置API Keys(两种方式)
58
- - **方式1**: 创建api_keys.json文件
59
- ```json
60
- [
61
- {"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60},
62
- {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}
63
- ]
64
- ```
65
- - **方式2**: 设置环境变量
66
- ```bash
67
- # Linux/macOS
68
- export API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}, {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}]' 
69
-
70
- # Windows (PowerShell)
71
- $env:API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}, {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}]'
72
-
73
- # Windows (CMD)
74
- set API_KEYS=[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}, {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}] 
75
- ```
76
-
77
- 4. 配置代理(可选,如果需要)
78
- ```bash
79
- # Linux/macOS - 基本代理
80
- export PROXY_HOST=127.0.0.1
81
- export PROXY_PORT=7890
82
-
83
- # Linux/macOS - 带认证的代理
84
- export PROXY_HOST=127.0.0.1
85
- export PROXY_PORT=7890
86
- export PROXY_USER=username
87
- export PROXY_PASS=password
88
-
89
- # Windows (PowerShell) - 基本代理
90
- $env:PROXY_HOST="127.0.0.1"
91
- $env:PROXY_PORT="7890"
92
-
93
- # Windows (PowerShell) - 带认证的代理
94
- $env:PROXY_HOST="127.0.0.1"
95
- $env:PROXY_PORT="7890"
96
- $env:PROXY_USER="username"
97
- $env:PROXY_PASS="password"
98
-
99
- # Windows (CMD) - 基本代理
100
- set PROXY_HOST=127.0.0.1
101
- set PROXY_PORT=7890
102
-
103
- # Windows (CMD) - 带认证的代理
104
- set PROXY_HOST=127.0.0.1
105
- set PROXY_PORT=7890
106
- set PROXY_USER=username
107
- set PROXY_PASS=password
108
- ```
109
-
110
- 5. 启动服务
111
- ```bash
112
- python run.py
113
- ```
114
-
115
- 6. 访问服务
116
- - API服务地址: http://localhost:8890
117
- - 后台管理面板: http://localhost:8890/admin
118
-
119
- ### 方法二:Docker部署
120
-
121
- 1. 构建Docker镜像
122
- ```bash
123
- docker build -t sora-api .
124
- ```
125
-
126
- 2. 运行Docker容器(不同配置选项)
127
-
128
- **基本运行方式**:
129
- ```bash
130
- docker run -d -p 8890:8890 --name sora-api sora-api
131
- ```
132
-
133
- **使用预打包镜像**:
134
- ```bash
135
- docker run -d -p 8890:8890 --name sora-api 1hei1/sora-api:latest
136
- ```
137
-
138
- **使用预打包镜像并配置API密钥**:
139
- ```bash
140
- docker run -d -p 8890:8890 \
141
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}, {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}]' \
142
- --name sora-api \
143
- 1hei1/sora-api:v0.1
144
- ```
145
-
146
- **使用预打包镜像并配置代理**:
147
- ```bash
148
- docker run -d -p 8890:8890 \
149
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}]' \
150
- -e PROXY_HOST=host.docker.internal \
151
- -e PROXY_PORT=7890 \
152
- --name sora-api \
153
- 1hei1/sora-api:v0.1
154
- ```
155
-
156
- **带API密钥配置**:
157
- ```bash
158
- docker run -d -p 8890:8890 \
159
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}, {"key": "Bearer your-sora-token-2", "weight": 2, "max_rpm": 60}]' \
160
- --name sora-api \
161
- sora-api
162
- ```
163
-
164
- **带基本代理配置**:
165
- ```bash
166
- docker run -d -p 8890:8890 \
167
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}]' \
168
- -e PROXY_HOST=host.docker.internal \
169
- -e PROXY_PORT=7890 \
170
- --name sora-api \
171
- sora-api
172
- ```
173
-
174
- **带认证代理配置**:
175
- ```bash
176
- docker run -d -p 8890:8890 \
177
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}]' \
178
- -e PROXY_HOST=host.docker.internal \
179
- -e PROXY_PORT=7890 \
180
- -e PROXY_USER=username \
181
- -e PROXY_PASS=password \
182
- --name sora-api \
183
- sora-api
184
- ```
185
-
186
- **使用外部配置文件**:
187
- ```bash
188
- # 首先确保api_keys.json文件已正确配置
189
- docker run -d -p 8890:8890 \
190
- -v $(pwd)/api_keys.json:/app/api_keys.json \
191
- -e PROXY_HOST=host.docker.internal \
192
- -e PROXY_PORT=7890 \
193
- --name sora-api \
194
- sora-api
195
- ```
196
-
197
- **挂载本地目录保存图片**:
198
- ```bash
199
- docker run -d -p 8890:8890 \
200
- -v /your/local/path:/app/src/static/images \
201
- --name sora-api \
202
- sora-api
203
- ```
204
-
205
- **启用详细日志**:
206
- ```bash
207
- docker run -d -p 8890:8890 \
208
- -e API_KEYS='[{"key": "Bearer your-sora-token-1", "weight": 1, "max_rpm": 60}]' \
209
- -e VERBOSE_LOGGING=true \
210
- --name sora-api \
211
- sora-api
212
- ```
213
-
214
- **注意**: 在Docker中使用宿主机代理时,请使用`host.docker.internal`而不是`127.0.0.1`作为代理主机地址。
215
-
216
- 3. 检查容器状态
217
- ```bash
218
- docker ps
219
- docker logs sora-api
220
- ```
221
-
222
- 4. 停止和移除容器
223
- ```bash
224
- docker stop sora-api
225
- docker rm sora-api
226
- ```
227
-
228
- ## 环境变量说明
229
-
230
- | 环境变量 | 描述 | 默认值 | 示例 |
231
- |---------|------|--------|------|
232
- | `API_HOST` | API服务监听地址 | `0.0.0.0` | `127.0.0.1` |
233
- | `API_PORT` | API服务端口 | `8890` | `9000` |
234
- | `BASE_URL` | API基础URL(生成图片的时候需要用到) | `http://0.0.0.0:8890` | `https://api.example.com` | 
235
- | `PROXY_HOST` | HTTP代理主机 | 空(不使用代理) | `127.0.0.1` |
236
- | `PROXY_PORT` | HTTP代理端口 | 空(不使用代理) | `7890` |
237
- | `PROXY_USER` | HTTP代理用户名 | 空(不使用认证) | `username` |
238
- | `PROXY_PASS` | HTTP代理密码 | 空(不使用认证) | `password` |
239
- | `IMAGE_SAVE_DIR` | 图片保存目录 | `src/static/images` | `/data/images` |
240
- | `IMAGE_LOCALIZATION` | 是否启用图片本地化 | `False` | `True` |
241
- | `ADMIN_KEY` | 管理员API密钥(登录界面输入的密码) | `sk-123456` | `sk-youradminkey` |
242
- | `API_AUTH_TOKEN` | API认证令牌(使用api服务传入的key) | 空 | `your-auth-token` |
243
- | `VERBOSE_LOGGING` | 是否启用详细日志 | `False` | `True` |
244
-
245
- ## API密钥配置说明
246
-
247
- API密钥配置采用JSON格式,每个密钥包含以下属性:
248
-
249
- - `key`: Sora认证令牌(必须包含Bearer前缀)
250
- - `weight`: 轮询权重,数字越大被选中概率越高
251
- - `max_rpm`: 每分钟最大请求数(速率限制)
252
-
253
- 示例:
254
- ```json
255
- [
256
- {
257
- "key": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9...",
258
- "weight": 1,
259
- "max_rpm": 60
260
- },
261
- {
262
- "key": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9...",
263
- "weight": 2,
264
- "max_rpm": 60
265
- }
266
- ]
267
- ```
268
-
269
- ## 使用示例
270
-
271
- ### 使用curl发送请求
272
-
273
- ```bash
274
- # 文本到图像请求(非流式)
275
- curl -X POST http://localhost:8890/v1/chat/completions \
276
- -H "Content-Type: application/json" \
277
- -H "Authorization: Bearer your-api-key" \
278
- -d '{
279
- "model": "sora-1.0",
280
- "messages": [
281
- {"role": "user", "content": "生成一只在草地上奔跑的金毛犬"}
282
- ],
283
- "n": 1,
284
- "stream": false
285
- }'
286
-
287
- # 文本到图像请求(流式)
288
- curl -X POST http://localhost:8890/v1/chat/completions \
289
- -H "Content-Type: application/json" \
290
- -H "Authorization: Bearer your-api-key" \
291
- -d '{
292
- "model": "sora-1.0",
293
- "messages": [
294
- {"role": "user", "content": "生成一只在草地上奔跑的金毛犬"}
295
- ],
296
- "n": 1,
297
- "stream": true
298
- }'
299
-
300
- # 查询异步任务状态
301
- curl -X GET http://localhost:8890/v1/generation/chatcmpl-123456789abcdef \
302
- -H "Authorization: Bearer your-api-key"
303
- ```
304
-
305
- ## 常见问题排查
306
-
307
- 1. **连接超时或无法连接**
308
- - 检查代理配置是否正确
309
- - 如使用代理认证,确认用户名密码正确
310
- - 确认Sora服务器是否可用
311
- - 检查本地网络连接
312
-
313
- 2. **API密钥加载失败**
314
- - 确认api_keys.json格式正确
315
- - 检查环境变量API_KEYS是否正确设置
316
- - 查看日志中的错误信息
317
-
318
- 3. **图片生成失败**
319
- - 确认Sora令牌有效性
320
- - 查看日志中的错误信息
321
- - 检查是否超出账户额度限制
322
-
323
- 4. **Docker容器启动失败**
324
- - 检查端口是否被占用
325
- - 确认环境变量设置正确
326
- - 查看Docker日志中的错误信息
327
-
328
- 5. **环境变量API_AUTH_TOKEN和ADMIN_KEY的区别**
329
- - 环境变量 API_AUTH_TOKEN代表你在cheery studio 或者newapi里调用填写的令牌
330
- - 环境变量 ADMIN_KEY 代表你管理面板的登录密码
331
- - 当不设置API_AUTH_TOKE时 其值默认等于ADMIN_KEY的值
332
-
333
- 6. **环境变量BASE_URL的作用**
334
- - 这个是图��本地化时设置的,比如生成了一张图片,获取到图片原始url,由于有的客户端不能访问sora,所以要本地化,这个base_url就是指定本地化图片的url前缀。
335
-
336
- 6. **token无效**
337
- - 首次使用的token需要设置用户名,可以使用下面的脚本批量设置用户名:
338
- ```python
339
- import random
340
- import string
341
- import logging
342
- import cloudscraper
343
-
344
- # Configure logging
345
- tlogging = logging.getLogger(__name__)
346
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
347
-
348
- # --- Configuration ---
349
- PROXY = {
350
- "http": "http://127.0.0.1:7890",
351
- "https": "http://127.0.0.1:7890"
352
- }
353
- PROFILE_API = "https://sora.chatgpt.com/backend/me"
354
- TOKENS_FILE = "tokens.txt" # 每行一个 Bearer token
355
- RESULTS_FILE = "update_results.txt" # 保存更新结果
356
- USERNAME_LENGTH = 8 # 随机用户名长度
357
-
358
- # --- Utilities ---
359
- def random_username(length: int = USERNAME_LENGTH) -> str:
360
- """生成全小写随机用户名"""
361
- return ''.join(random.choices(string.ascii_lowercase, k=length))
362
-
363
-
364
- def sanitize_headers(headers: dict) -> dict:
365
- """
366
- 移除所有非 Latin-1 字符,确保 headers 可以被底层 HTTP 库正确编码。
367
- """
368
- new = {}
369
- for k, v in headers.items():
370
- if isinstance(v, str):
371
- new[k] = v.encode('latin-1', 'ignore').decode('latin-1')
372
- else:
373
- new[k] = v
374
- return new
375
-
376
-
377
- class SoraBatchUpdater:
378
- def __init__(self, proxy: dict = None):
379
- self.proxy = proxy or {}
380
-
381
- def update_username_for_token(self, token: str) -> tuple[bool, str]:
382
- """
383
- 针对单个 Bearer token,生成随机用户名并发送更新请求。
384
- 返回 (success, message)。
385
- """
386
- scraper = cloudscraper.create_scraper()
387
- if self.proxy:
388
- scraper.proxies = self.proxy
389
-
390
- headers = {
391
- "Accept": "*/*",
392
- "Accept-Language": "zh-CN,zh;q=0.9",
393
- "Content-Type": "application/json",
394
- "Authorization": f"Bearer {token}",
395
- "sec-ch-ua": '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"',
396
- "sec-ch-ua-mobile": "?0",
397
- "sec-ch-ua-platform": '"Windows"',
398
- "sec-fetch-dest": "empty",
399
- "sec-fetch-mode": "cors",
400
- "sec-fetch-site": "same-origin",
401
- }
402
- headers = sanitize_headers(headers)
403
-
404
- new_username = random_username()
405
- payload = {"username": new_username}
406
-
407
- try:
408
- resp = scraper.post(
409
- PROFILE_API,
410
- headers=headers,
411
- json=payload,
412
- allow_redirects=False,
413
- timeout=15
414
- )
415
- status = resp.status_code
416
- if resp.ok:
417
- msg = f"OK ({new_username})"
418
- logging.info("Token %s: updated to %s", token[:6], new_username)
419
- return True, msg
420
- else:
421
- text = resp.text.replace('\n', '')
422
- msg = f"Failed {status}: {text}" # 简要错误信息
423
- logging.warning("Token %s: %s", token[:6], msg)
424
- return False, msg
425
- except Exception as e:
426
- msg = str(e)
427
- logging.error("Token %s exception: %s", token[:6], msg)
428
- return False, msg
429
-
430
- def batch_update(self, tokens: list[str]) -> None:
431
- """
432
- 对一组 Bearer token 批量更新用户名,并将结果写入 RESULTS_FILE。
433
- """
434
- results = []
435
- for token in tokens:
436
- success, message = self.update_username_for_token(token)
437
- results.append((token, success, message))
438
-
439
- # 写入结果文件
440
- with open(RESULTS_FILE, 'w', encoding='utf-8') as f:
441
- for token, success, msg in results:
442
- status = 'SUCCESS' if success else 'ERROR'
443
- f.write(f"{token} ---- {status} ---- {msg}\n")
444
- logging.info("Batch update complete. Results saved to %s", RESULTS_FILE)
445
-
446
-
447
- def load_tokens(filepath: str) -> list[str]:
448
- """从文件加载每行一个 token 的列表"""
449
- try:
450
- with open(filepath, 'r', encoding='utf-8') as f:
451
- return [line.strip() for line in f if line.strip()]
452
- except FileNotFoundError:
453
- logging.error("Tokens file not found: %s", filepath)
454
- return []
455
-
456
-
457
- if __name__ == '__main__':
458
- tokens = load_tokens(TOKENS_FILE)
459
- if not tokens:
460
- logging.error("No tokens to update. Exiting.")
461
- else:
462
- updater = SoraBatchUpdater(proxy=PROXY)
463
- updater.batch_update(tokens)
464
-
465
- ```
466
-
467
-
468
- ## 性能优化
469
-
470
- 最新版本包含以下性能优化:
471
-
472
- 1. **代码重构**:简化了代码结构,提高可读性和可维护性
473
- 2. **内存优化**:减少不必要的内存使用,优化大型图像处理
474
- 3. **异步处理**:全面使用异步处理提高并发性能
475
- 4. **错误处理**:改进了错误处理和日志记录
476
- 5. **密钥管理**:优化了密钥轮询算法,提高了可靠性
477
- 6. **容器优化**:增强了Docker容器配置,支持健康检查
478
-
479
- ## 贡献
480
-
481
- 欢迎提交问题报告和改进建议!
482
-
483
- ## 许可证
484
-
485
- MIT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/api_keys.json.example DELETED
@@ -1,12 +0,0 @@
1
- [
2
- {
3
- "key": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9...",
4
- "weight": 1,
5
- "max_rpm": 60
6
- },
7
- {
8
- "key": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9...",
9
- "weight": 2,
10
- "max_rpm": 60
11
- }
12
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/docs/image_localization.md DELETED
@@ -1 +0,0 @@
1
-
 
 
sora-api-master/requirements.txt DELETED
@@ -1,28 +0,0 @@
1
- # Web框架
2
- fastapi>=0.101.1
3
- uvicorn>=0.23.2
4
- pydantic>=2.3.0
5
- python-multipart==0.0.6
6
-
7
- # HTTP请求
8
- cloudscraper==1.2.71
9
- aiohttp>=3.8.5
10
- aiofiles==0.8.0
11
-
12
- # 工具
13
- python-dotenv>=1.0.0
14
- requests==2.30.0
15
- pillow==10.0.0
16
- cryptography==41.0.3
17
- psutil==5.9.5
18
- tzdata>=2023.3
19
- pytz>=2023.3
20
- retry>=0.9.2
21
- fuzzywuzzy>=0.18.0
22
- python-Levenshtein>=0.21.1
23
- tzlocal>=5.0.1
24
- humanize>=4.7.0
25
- prettytable>=3.8.0
26
- natsort>=8.4.0
27
- tenacity>=8.2.3
28
- PyJWT>=2.6.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/run.py DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env python
2
- import sys
3
- import os
4
- import logging
5
-
6
- # 设置日志
7
- logging.basicConfig(
8
- level=logging.INFO,
9
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
10
- )
11
- logger = logging.getLogger("sora-api")
12
-
13
- # 设置控制台输出编码为UTF-8
14
- if sys.platform.startswith('win'):
15
- os.system("chcp 65001")
16
- sys.stdout.reconfigure(encoding='utf-8')
17
- elif sys.stdout.encoding != 'utf-8':
18
- sys.stdout.reconfigure(encoding='utf-8')
19
-
20
- # 确保src目录在路径中
21
- sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
22
-
23
- # 提前加载app和key_manager,确保API密钥在启动前已加载
24
- from src.app import app, key_manager
25
- from src.main import init_app
26
- from src.config import Config
27
- import uvicorn
28
-
29
- if __name__ == "__main__":
30
- # 设置环境变量确保正确处理UTF-8
31
- os.environ["PYTHONIOENCODING"] = "utf-8"
32
-
33
- # 初始化应用
34
- init_app()
35
-
36
- # 启动服务
37
- logger.info(f"启动OpenAI兼容的Sora API服务: {Config.HOST}:{Config.PORT}")
38
- logger.info(f"已加载 {len(key_manager.keys)} 个API密钥")
39
- uvicorn.run(
40
- "src.app:app",
41
- host=Config.HOST,
42
- port=Config.PORT,
43
- reload=False # 生产环境关闭自动重载
44
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- # Sora API服务包
2
- # 包含对Sora的API调用封装,提供OpenAI兼容接口
 
 
 
sora-api-master/src/api/__init__.py DELETED
@@ -1,29 +0,0 @@
1
- import logging
2
- from fastapi import APIRouter
3
- from .admin import router as admin_router
4
- from .generation import router as generation_router
5
- from .chat import router as chat_router
6
- from .health import router as health_router
7
- # 添加auth路由
8
- from .auth import router as auth_router
9
-
10
- # 设置日志
11
- logger = logging.getLogger("sora-api.api")
12
-
13
- # 创建v1版本路由(兼容OpenAI API)
14
- v1_router = APIRouter(prefix="/v1")
15
-
16
- # 创建主路由
17
- main_router = APIRouter()
18
-
19
- # 向v1路由注册相关功能路由
20
- v1_router.include_router(generation_router)
21
- v1_router.include_router(chat_router)
22
-
23
- # 注册所有路由
24
- main_router.include_router(v1_router) # 保持与OpenAI API兼容的v1前缀路由
25
- main_router.include_router(admin_router, tags=["admin"])
26
- main_router.include_router(health_router, tags=["health"])
27
- main_router.include_router(auth_router, tags=["auth"])
28
-
29
- logger.info("API路由已初始化")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/admin.py DELETED
@@ -1,463 +0,0 @@
1
- import os
2
- import logging
3
- import dotenv
4
- from typing import Dict, Any, List
5
- from fastapi import APIRouter, Depends, HTTPException
6
- from fastapi.responses import FileResponse
7
-
8
- from ..models.schemas import ApiKeyCreate, ApiKeyUpdate, ConfigUpdate, LogLevelUpdate, BatchOperation, BatchImportOperation
9
- from ..api.dependencies import verify_admin, verify_admin_jwt
10
- from ..config import Config
11
- from ..key_manager import key_manager
12
- from ..sora_integration import SoraClient
13
-
14
- # 设置日志
15
- logger = logging.getLogger("sora-api.admin")
16
-
17
- # 日志系统配置
18
- class LogConfig:
19
- LEVEL = os.getenv("LOG_LEVEL", "WARNING").upper()
20
- FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
21
-
22
- # 创建路由
23
- router = APIRouter(prefix="/api")
24
-
25
- # 密钥管理API
26
- @router.get("/keys")
27
- async def get_all_keys(admin_token = Depends(verify_admin_jwt)):
28
- """获取所有API密钥"""
29
- return key_manager.get_all_keys()
30
-
31
- @router.get("/keys/{key_id}")
32
- async def get_key(key_id: str, admin_token = Depends(verify_admin_jwt)):
33
- """获取单个API密钥详情"""
34
- key = key_manager.get_key_by_id(key_id)
35
- if not key:
36
- raise HTTPException(status_code=404, detail="密钥不存在")
37
- return key
38
-
39
- @router.post("/keys")
40
- async def create_key(key_data: ApiKeyCreate, admin_token = Depends(verify_admin_jwt)):
41
- """创建新API密钥"""
42
- try:
43
- # 确保密钥值包含 Bearer 前缀
44
- key_value = key_data.key_value
45
- if not key_value.startswith("Bearer "):
46
- key_value = f"Bearer {key_value}"
47
-
48
- new_key = key_manager.add_key(
49
- key_value,
50
- name=key_data.name,
51
- weight=key_data.weight,
52
- rate_limit=key_data.rate_limit,
53
- is_enabled=key_data.is_enabled,
54
- notes=key_data.notes
55
- )
56
-
57
- # 通过Config永久保存所有密钥
58
- Config.save_api_keys(key_manager.keys)
59
-
60
- return new_key
61
- except Exception as e:
62
- logger.error(f"创建密钥失败: {str(e)}", exc_info=True)
63
- raise HTTPException(status_code=400, detail=str(e))
64
-
65
- @router.put("/keys/{key_id}")
66
- async def update_key(key_id: str, key_data: ApiKeyUpdate, admin_token = Depends(verify_admin_jwt)):
67
- """更新API密钥信息"""
68
- try:
69
- # 如果提供了新的密钥值,确保包含Bearer前缀
70
- key_value = key_data.key_value
71
- if key_value and not key_value.startswith("Bearer "):
72
- key_value = f"Bearer {key_value}"
73
- key_data.key_value = key_value
74
-
75
- updated_key = key_manager.update_key(
76
- key_id,
77
- key_value=key_data.key_value,
78
- name=key_data.name,
79
- weight=key_data.weight,
80
- rate_limit=key_data.rate_limit,
81
- is_enabled=key_data.is_enabled,
82
- notes=key_data.notes
83
- )
84
- if not updated_key:
85
- raise HTTPException(status_code=404, detail="密钥不存在")
86
-
87
- # 通过Config永久保存所有密钥
88
- Config.save_api_keys(key_manager.keys)
89
-
90
- return updated_key
91
- except HTTPException:
92
- raise
93
- except Exception as e:
94
- logger.error(f"更新密钥失败: {str(e)}", exc_info=True)
95
- raise HTTPException(status_code=400, detail=str(e))
96
-
97
- @router.delete("/keys/{key_id}")
98
- async def delete_key(key_id: str, admin_token = Depends(verify_admin_jwt)):
99
- """删除API密钥"""
100
- success = key_manager.delete_key(key_id)
101
- if not success:
102
- raise HTTPException(status_code=404, detail="密钥不存在")
103
-
104
- # 通过Config永久保存所有密钥
105
- Config.save_api_keys(key_manager.keys)
106
-
107
- return {"status": "success", "message": "密钥已删除"}
108
-
109
- @router.get("/stats")
110
- async def get_usage_stats(admin_token = Depends(verify_admin_jwt)):
111
- """获取API使用统计"""
112
- stats = key_manager.get_usage_stats()
113
-
114
- # 处理daily_usage数据,确保前端能够正确显示
115
- daily_usage = {}
116
- keys_usage = {}
117
-
118
- # 从past_7_days数据转换为daily_usage格式
119
- for date, counts in stats.get("past_7_days", {}).items():
120
- daily_usage[date] = counts.get("successful", 0) + counts.get("failed", 0)
121
-
122
- # 获取每个密钥的使用情况
123
- for key in key_manager.keys:
124
- key_id = key.get("id")
125
- key_name = key.get("name") or f"密钥_{key_id[:6]}"
126
-
127
- # 获取该密钥的使用统计
128
- if key_id in key_manager.usage_stats:
129
- key_stats = key_manager.usage_stats[key_id]
130
- total_requests = key_stats.get("total_requests", 0)
131
-
132
- if total_requests > 0:
133
- keys_usage[key_name] = total_requests
134
-
135
- # 添加到返回数据中
136
- stats["daily_usage"] = daily_usage
137
- stats["keys_usage"] = keys_usage
138
-
139
- return stats
140
-
141
- @router.post("/keys/test")
142
- async def test_key(key_data: ApiKeyCreate, admin_token = Depends(verify_admin_jwt)):
143
- """测试API密钥是否有效"""
144
- try:
145
- # 获取密钥值
146
- key_value = key_data.key_value.strip()
147
-
148
- # 确保密钥格式正确
149
- if not key_value.startswith("Bearer "):
150
- key_value = f"Bearer {key_value}"
151
-
152
- # 获取代理配置
153
- proxy_host = Config.PROXY_HOST if Config.PROXY_HOST and Config.PROXY_HOST.strip() else None
154
- proxy_port = Config.PROXY_PORT if Config.PROXY_PORT and Config.PROXY_PORT.strip() else None
155
- proxy_user = Config.PROXY_USER if Config.PROXY_USER and Config.PROXY_USER.strip() else None
156
- proxy_pass = Config.PROXY_PASS if Config.PROXY_PASS and Config.PROXY_PASS.strip() else None
157
-
158
- test_client = SoraClient(
159
- proxy_host=proxy_host,
160
- proxy_port=proxy_port,
161
- proxy_user=proxy_user,
162
- proxy_pass=proxy_pass,
163
- auth_token=key_value
164
- )
165
-
166
- # 执行简单API调用测试连接
167
- test_result = await test_client.test_connection()
168
- logger.info(f"API密钥测试结果: {test_result}")
169
-
170
- # 检查底层测试结果的状态
171
- if test_result.get("status") == "success":
172
- # API连接测试成功
173
- return {
174
- "status": "success",
175
- "message": "API密钥测试成功",
176
- "details": test_result,
177
- "success": True
178
- }
179
- else:
180
- # API连接测试失败
181
- return {
182
- "status": "error",
183
- "message": f"API密钥测试失败: {test_result.get('message', '连接失败')}",
184
- "details": test_result,
185
- "success": False
186
- }
187
- except Exception as e:
188
- logger.error(f"测试密钥失败: {str(e)}", exc_info=True)
189
- return {
190
- "status": "error",
191
- "message": f"API密钥测试失败: {str(e)}",
192
- "success": False
193
- }
194
-
195
- @router.post("/keys/batch")
196
- async def batch_operation(operation: Dict[str, Any], admin_token = Depends(verify_admin_jwt)):
197
- """批量操作API密钥"""
198
- try:
199
- action = operation.get("action")
200
- logger.info(f"接收到批量操作请求: {action}")
201
-
202
- if not action:
203
- logger.warning("批量操作缺少action参数")
204
- raise HTTPException(status_code=400, detail="缺少必要参数: action")
205
-
206
- logger.info(f"批量操作类型: {action}")
207
-
208
- if action == "import":
209
- # 批量导入API密钥
210
- keys_data = operation.get("keys", [])
211
- if not keys_data:
212
- logger.warning("批量导入缺少keys数据")
213
- raise HTTPException(status_code=400, detail="未提供密钥数据")
214
-
215
- logger.info(f"准备导入 {len(keys_data)} 个密钥")
216
-
217
- # 对每个密钥处理Bearer前缀
218
- for key_data in keys_data:
219
- if isinstance(key_data, dict):
220
- key_value = key_data.get("key", "").strip()
221
- if key_value and not key_value.startswith("Bearer "):
222
- key_data["key"] = f"Bearer {key_value}"
223
-
224
- # 执行批量导入
225
- try:
226
- result = key_manager.batch_import_keys(keys_data)
227
- logger.info(f"导入结果: 成功={result['imported']}, 跳过={result['skipped']}")
228
-
229
- # 通过Config永久保存所有密钥
230
- Config.save_api_keys(key_manager.keys)
231
-
232
- return {
233
- "success": True,
234
- "message": f"成功导入 {result['imported']} 个密钥,跳过 {result['skipped']} 个重复密钥",
235
- "imported": result["imported"],
236
- "skipped": result["skipped"]
237
- }
238
- except Exception as e:
239
- logger.error(f"批量导入密钥错误: {str(e)}", exc_info=True)
240
- raise HTTPException(status_code=500, detail=f"导入密钥失败: {str(e)}")
241
-
242
- elif action not in ["enable", "disable", "delete"]:
243
- logger.warning(f"不支持的批量操作: {action}")
244
- raise HTTPException(status_code=400, detail=f"不支持的操作: {action}")
245
-
246
- # 对于非导入操作,需要提供key_ids
247
- key_ids = operation.get("key_ids", [])
248
- if not key_ids:
249
- logger.warning(f"{action}操作缺少key_ids参数")
250
- raise HTTPException(status_code=400, detail="缺少必要参数: key_ids")
251
-
252
- # 确保key_ids是一个列表
253
- if isinstance(key_ids, str):
254
- key_ids = [key_ids]
255
-
256
- logger.info(f"批量{action}操作 {len(key_ids)} 个密钥")
257
-
258
- if action == "enable":
259
- # 批量启用
260
- success_count = 0
261
- for key_id in key_ids:
262
- updated = key_manager.update_key(key_id, is_enabled=True)
263
- if updated:
264
- success_count += 1
265
-
266
- # 通过Config永久保存所有密钥
267
- Config.save_api_keys(key_manager.keys)
268
-
269
- logger.info(f"成功启用 {success_count} 个密钥")
270
- return {
271
- "success": True,
272
- "message": f"已成功启用 {success_count} 个密钥",
273
- "affected": success_count
274
- }
275
- elif action == "disable":
276
- # 批量禁用
277
- success_count = 0
278
- for key_id in key_ids:
279
- updated = key_manager.update_key(key_id, is_enabled=False)
280
- if updated:
281
- success_count += 1
282
-
283
- # 通过Config永久保存所有密钥
284
- Config.save_api_keys(key_manager.keys)
285
-
286
- logger.info(f"成功禁用 {success_count} 个密钥")
287
- return {
288
- "success": True,
289
- "message": f"已成功禁用 {success_count} 个密钥",
290
- "affected": success_count
291
- }
292
- elif action == "delete":
293
- # 批量删除
294
- success_count = 0
295
- for key_id in key_ids:
296
- if key_manager.delete_key(key_id):
297
- success_count += 1
298
-
299
- # 通过Config永久保存所有密钥
300
- Config.save_api_keys(key_manager.keys)
301
-
302
- logger.info(f"成功删除 {success_count} 个密钥")
303
- return {
304
- "success": True,
305
- "message": f"已成功删除 {success_count} 个密钥",
306
- "affected": success_count
307
- }
308
- except HTTPException:
309
- raise
310
- except Exception as e:
311
- logger.error(f"批量操作失败: {str(e)}", exc_info=True)
312
- raise HTTPException(status_code=500, detail=str(e))
313
-
314
- # 配置管理API
315
- @router.get("/config")
316
- async def get_config(admin_token = Depends(verify_admin_jwt)):
317
- """获取当前系统配置"""
318
- return {
319
- "HOST": Config.HOST,
320
- "PORT": Config.PORT,
321
- "BASE_URL": Config.BASE_URL,
322
- "PROXY_HOST": Config.PROXY_HOST,
323
- "PROXY_PORT": Config.PROXY_PORT,
324
- "PROXY_USER": Config.PROXY_USER,
325
- "PROXY_PASS": "******" if Config.PROXY_PASS else "",
326
- "IMAGE_LOCALIZATION": Config.IMAGE_LOCALIZATION,
327
- "IMAGE_SAVE_DIR": Config.IMAGE_SAVE_DIR,
328
- "API_AUTH_TOKEN": bool(Config.API_AUTH_TOKEN) # 只返回是否设置,不返回实际值
329
- }
330
-
331
- @router.post("/config")
332
- async def update_config(config_data: ConfigUpdate, admin_token = Depends(verify_admin_jwt)):
333
- """更新系统配置"""
334
- try:
335
- changes = []
336
-
337
- # 更新代理设置
338
- if config_data.PROXY_HOST is not None:
339
- Config.PROXY_HOST = config_data.PROXY_HOST
340
- changes.append("PROXY_HOST")
341
- # 更新环境变量
342
- os.environ["PROXY_HOST"] = config_data.PROXY_HOST
343
-
344
- if config_data.PROXY_PORT is not None:
345
- Config.PROXY_PORT = config_data.PROXY_PORT
346
- changes.append("PROXY_PORT")
347
- # 更新环境变量
348
- os.environ["PROXY_PORT"] = config_data.PROXY_PORT
349
-
350
- # 更新代理认证设置
351
- if config_data.PROXY_USER is not None:
352
- Config.PROXY_USER = config_data.PROXY_USER
353
- changes.append("PROXY_USER")
354
- # 更新环境变量
355
- os.environ["PROXY_USER"] = config_data.PROXY_USER
356
-
357
- if config_data.PROXY_PASS is not None:
358
- Config.PROXY_PASS = config_data.PROXY_PASS
359
- changes.append("PROXY_PASS")
360
- # 更新环境变量
361
- os.environ["PROXY_PASS"] = config_data.PROXY_PASS
362
-
363
- # 更新基础URL设置
364
- if config_data.BASE_URL is not None:
365
- Config.BASE_URL = config_data.BASE_URL
366
- changes.append("BASE_URL")
367
- # 更新环境变量
368
- os.environ["BASE_URL"] = config_data.BASE_URL
369
-
370
- # 更新图片本地化设置
371
- if config_data.IMAGE_LOCALIZATION is not None:
372
- Config.IMAGE_LOCALIZATION = config_data.IMAGE_LOCALIZATION
373
- changes.append("IMAGE_LOCALIZATION")
374
- # 更新环境变量
375
- os.environ["IMAGE_LOCALIZATION"] = str(config_data.IMAGE_LOCALIZATION)
376
-
377
- if config_data.IMAGE_SAVE_DIR is not None:
378
- Config.IMAGE_SAVE_DIR = config_data.IMAGE_SAVE_DIR
379
- changes.append("IMAGE_SAVE_DIR")
380
- # 更新环境变量
381
- os.environ["IMAGE_SAVE_DIR"] = config_data.IMAGE_SAVE_DIR
382
- # 确保目录存在
383
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
384
-
385
- # 保存到.env文件
386
- if config_data.save_to_env and changes:
387
- env_file = os.path.join(Config.BASE_DIR, '.env')
388
- env_data = {}
389
-
390
- # 先读取现有的.env文件
391
- if os.path.exists(env_file):
392
- env_data = dotenv.dotenv_values(env_file)
393
-
394
- # 更新环境变量
395
- for field in changes:
396
- value = getattr(Config, field)
397
- env_data[field] = str(value)
398
-
399
- # 写入.env文件
400
- with open(env_file, 'w') as f:
401
- for key, value in env_data.items():
402
- f.write(f"{key}={value}\n")
403
-
404
- logger.info(f"已将配置保存到.env文件: {changes}")
405
-
406
- return {
407
- "status": "success",
408
- "message": f"配置已更新: {', '.join(changes) if changes else '无变更'}"
409
- }
410
- except Exception as e:
411
- logger.error(f"更新配置失败: {str(e)}", exc_info=True)
412
- raise HTTPException(status_code=400, detail=f"更新配置失败: {str(e)}")
413
-
414
- @router.post("/logs/level")
415
- async def update_log_level(data: LogLevelUpdate, admin_token = Depends(verify_admin_jwt)):
416
- """更新日志级别"""
417
- try:
418
- # 验证日志级别
419
- level = data.level.upper()
420
- valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
421
-
422
- if level not in valid_levels:
423
- raise HTTPException(status_code=400, detail=f"无效的日志级别: {level}")
424
-
425
- # 更新根日志记录器的级别
426
- root_logger = logging.getLogger()
427
- root_logger.setLevel(getattr(logging, level))
428
-
429
- # 同时更新sora-api模块的日志级别
430
- sora_logger = logging.getLogger("sora-api")
431
- sora_logger.setLevel(getattr(logging, level))
432
-
433
- # 记录日志级别变更
434
- logger.info(f"日志级别已更新为: {level}")
435
-
436
- # 如果需要,保存到环境变量
437
- if data.save_to_env:
438
- env_file = os.path.join(Config.BASE_DIR, '.env')
439
- env_data = {}
440
-
441
- # 先读取现有的.env文件
442
- if os.path.exists(env_file):
443
- env_data = dotenv.dotenv_values(env_file)
444
-
445
- # 更新LOG_LEVEL环境变量
446
- env_data["LOG_LEVEL"] = level
447
-
448
- # 写入.env文件
449
- with open(env_file, 'w') as f:
450
- for key, value in env_data.items():
451
- f.write(f"{key}={value}\n")
452
-
453
- # 记录配置保存
454
- logger.info(f"已将日志级别保存到.env文件: LOG_LEVEL={level}")
455
-
456
- return {"status": "success", "message": f"日志级别已更新为: {level}"}
457
- except HTTPException:
458
- raise
459
- except Exception as e:
460
- logger.error(f"更新日志级别失败: {str(e)}", exc_info=True)
461
- raise HTTPException(status_code=400, detail=f"更新日志级别失败: {str(e)}")
462
-
463
- # 管理员密钥API - 已移至app.py中
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/auth.py DELETED
@@ -1,96 +0,0 @@
1
- import jwt
2
- import time
3
- import uuid
4
- import logging
5
- from typing import Dict, Any
6
- from fastapi import APIRouter, HTTPException, Depends
7
- from pydantic import BaseModel
8
-
9
- from ..config import Config
10
-
11
- # 设置日志
12
- logger = logging.getLogger("sora-api.auth")
13
-
14
- # 创建路由
15
- router = APIRouter(prefix="/api/auth")
16
-
17
- # JWT配置
18
- JWT_SECRET = Config.ADMIN_KEY # 使用管理员密钥作为JWT秘钥
19
- JWT_ALGORITHM = "HS256"
20
- JWT_EXPIRATION = 3600 # 令牌有效期1小时
21
-
22
- # 认证请求模型
23
- class LoginRequest(BaseModel):
24
- admin_key: str
25
-
26
- # 令牌响应模型
27
- class TokenResponse(BaseModel):
28
- token: str
29
- expires_in: int
30
- token_type: str = "bearer"
31
-
32
- # 创建JWT令牌
33
- def create_jwt_token(data: Dict[str, Any], expires_delta: int = JWT_EXPIRATION) -> str:
34
- payload = data.copy()
35
- issued_at = int(time.time())
36
- expiration = issued_at + expires_delta
37
- payload.update({
38
- "iat": issued_at,
39
- "exp": expiration,
40
- "jti": str(uuid.uuid4())
41
- })
42
-
43
- token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
44
- return token
45
-
46
- # 验证JWT令牌
47
- def verify_jwt_token(token: str) -> Dict[str, Any]:
48
- try:
49
- payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
50
- # 检查token是否已过期
51
- if payload.get("exp", 0) < time.time():
52
- raise HTTPException(status_code=401, detail="令牌已过期")
53
- return payload
54
- except jwt.PyJWTError as e:
55
- logger.warning(f"JWT验证失败: {str(e)}")
56
- raise HTTPException(status_code=401, detail="无效的令牌")
57
-
58
- # 登录API
59
- @router.post("/login", response_model=TokenResponse)
60
- async def login(request: LoginRequest):
61
- """管理员登录,返回JWT令牌"""
62
- # 验证管理员密钥
63
- if request.admin_key != Config.ADMIN_KEY:
64
- logger.warning(f"尝试使用无效的管理员密钥登录")
65
- # 使用固定延迟以防止计时攻击
66
- time.sleep(1)
67
- raise HTTPException(status_code=401, detail="管理员密钥错误")
68
-
69
- # 创建令牌
70
- token_data = {
71
- "sub": "admin",
72
- "role": "admin",
73
- "name": "管理员"
74
- }
75
-
76
- token = create_jwt_token(token_data)
77
- logger.info(f"管理员登录成功,生成新令牌")
78
-
79
- return TokenResponse(token=token, expires_in=JWT_EXPIRATION)
80
-
81
- # 刷新令牌API
82
- @router.post("/refresh", response_model=TokenResponse)
83
- async def refresh_token(token: str = Depends(verify_jwt_token)):
84
- """刷新JWT令牌"""
85
- # 创建新令牌,保持相同的sub和role
86
- token_data = {
87
- "sub": token.get("sub"),
88
- "role": token.get("role", "admin"),
89
- "name": token.get("name", "管理员"),
90
- "refresh_count": token.get("refresh_count", 0) + 1
91
- }
92
-
93
- new_token = create_jwt_token(token_data)
94
- logger.info(f"令牌刷新成功,生成新令牌")
95
-
96
- return TokenResponse(token=new_token, expires_in=JWT_EXPIRATION)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/chat.py DELETED
@@ -1,168 +0,0 @@
1
- import uuid
2
- import time
3
- import re
4
- import logging
5
- from typing import Dict, Any, List, Optional
6
- from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException
7
- from fastapi.responses import StreamingResponse, JSONResponse
8
-
9
- from ..models.schemas import ChatCompletionRequest
10
- from ..api.dependencies import verify_api_key, get_sora_client_dep
11
- from ..services.image_service import process_image_task, format_think_block
12
- from ..services.streaming import generate_streaming_response, generate_streaming_remix_response
13
- from ..key_manager import key_manager
14
-
15
- # 设置日志
16
- logger = logging.getLogger("sora-api.chat")
17
-
18
- # 创建路由
19
- router = APIRouter()
20
-
21
- @router.post("/chat/completions")
22
- async def chat_completions(
23
- request: ChatCompletionRequest,
24
- background_tasks: BackgroundTasks,
25
- client_info = Depends(get_sora_client_dep()),
26
- api_key: str = Depends(verify_api_key)
27
- ):
28
- """
29
- 聊天完成端点 - 处理文本到图像和图像到图像的请求
30
- 兼容OpenAI API格式
31
- """
32
- # 解析客户端信息
33
- sora_client, sora_auth_token = client_info
34
-
35
- # 记录开始时间
36
- start_time = time.time()
37
- success = False
38
-
39
- try:
40
- # 分析用户消息
41
- user_messages = [m for m in request.messages if m.role == "user"]
42
- if not user_messages:
43
- raise HTTPException(status_code=400, detail="至少需要一条用户消息")
44
-
45
- last_user_message = user_messages[-1]
46
- prompt = ""
47
- image_data = None
48
-
49
- # 提取提示词和图片数据
50
- if isinstance(last_user_message.content, str):
51
- # 简单的字符串内容
52
- prompt = last_user_message.content
53
-
54
- # 检查是否包含内嵌的base64图片
55
- pattern = r'data:image\/[^;]+;base64,([^"]+)'
56
- match = re.search(pattern, prompt)
57
- if match:
58
- image_data = match.group(1)
59
- # 从提示词中删除base64数据
60
- prompt = re.sub(pattern, "[已上传图片]", prompt)
61
- else:
62
- # 多模态内容,提取文本和图片
63
- content_items = last_user_message.content
64
- text_parts = []
65
-
66
- for item in content_items:
67
- if item.type == "text" and item.text:
68
- text_parts.append(item.text)
69
- elif item.type == "image_url" and item.image_url:
70
- # 如果有图片URL包含base64数据
71
- url = item.image_url.get("url", "")
72
- if url.startswith("data:image/"):
73
- pattern = r'data:image\/[^;]+;base64,([^"]+)'
74
- match = re.search(pattern, url)
75
- if match:
76
- image_data = match.group(1)
77
- text_parts.append("[已上传图片]")
78
-
79
- prompt = " ".join(text_parts)
80
-
81
- # 检查是否为流式响应
82
- if request.stream:
83
- # 流式响应处理
84
- if image_data:
85
- response = StreamingResponse(
86
- generate_streaming_remix_response(sora_client, prompt, image_data, request.n),
87
- media_type="text/event-stream"
88
- )
89
- else:
90
- response = StreamingResponse(
91
- generate_streaming_response(sora_client, prompt, request.n),
92
- media_type="text/event-stream"
93
- )
94
- success = True
95
-
96
- # 记录请求结果
97
- response_time = time.time() - start_time
98
- key_manager.record_request_result(sora_auth_token, success, response_time)
99
-
100
- return response
101
- else:
102
- # 非流式响应 - 返回一个即时响应,表示任务已接收
103
- request_id = f"chatcmpl-{uuid.uuid4().hex}"
104
-
105
- # 创建后台任务
106
- if image_data:
107
- background_tasks.add_task(
108
- process_image_task,
109
- request_id,
110
- sora_client,
111
- "remix",
112
- prompt,
113
- image_data=image_data,
114
- num_images=request.n
115
- )
116
- else:
117
- background_tasks.add_task(
118
- process_image_task,
119
- request_id,
120
- sora_client,
121
- "generation",
122
- prompt,
123
- num_images=request.n,
124
- width=720,
125
- height=480
126
- )
127
-
128
- # 返回正在处理的响应
129
- processing_message = "正在准备生成任务,请稍候..."
130
- response = {
131
- "id": request_id,
132
- "object": "chat.completion",
133
- "created": int(time.time()),
134
- "model": "sora-1.0",
135
- "choices": [
136
- {
137
- "index": 0,
138
- "message": {
139
- "role": "assistant",
140
- "content": format_think_block(processing_message)
141
- },
142
- "finish_reason": "processing"
143
- }
144
- ],
145
- "usage": {
146
- "prompt_tokens": len(prompt) // 4,
147
- "completion_tokens": 10,
148
- "total_tokens": len(prompt) // 4 + 10
149
- }
150
- }
151
-
152
- success = True
153
-
154
- # 记录请求结果
155
- response_time = time.time() - start_time
156
- key_manager.record_request_result(sora_auth_token, success, response_time)
157
-
158
- return JSONResponse(content=response)
159
-
160
- except Exception as e:
161
- success = False
162
- logger.error(f"处理聊天完成请求失败: {str(e)}", exc_info=True)
163
-
164
- # 记录请求结果
165
- response_time = time.time() - start_time
166
- key_manager.record_request_result(sora_auth_token, success, response_time)
167
-
168
- raise HTTPException(status_code=500, detail=f"图像生成失败: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/dependencies.py DELETED
@@ -1,131 +0,0 @@
1
- from typing import Optional, Tuple, Dict, Any
2
- from fastapi import Request, HTTPException, Depends, Header
3
- import aiohttp
4
- import logging
5
- from ..config import Config
6
- from .auth import verify_jwt_token
7
-
8
- logger = logging.getLogger("sora-api.dependencies")
9
-
10
- # 全局会话池
11
- session_pool: Optional[aiohttp.ClientSession] = None
12
-
13
- # 获取Sora客户端
14
- def get_sora_client(auth_token: str):
15
- from ..sora_integration import SoraClient
16
-
17
- # 使用字典缓存客户端实例
18
- if not hasattr(get_sora_client, "clients"):
19
- get_sora_client.clients = {}
20
-
21
- if auth_token not in get_sora_client.clients:
22
- proxy_host = Config.PROXY_HOST if Config.PROXY_HOST and Config.PROXY_HOST.strip() else None
23
- proxy_port = Config.PROXY_PORT if Config.PROXY_PORT and Config.PROXY_PORT.strip() else None
24
- proxy_user = Config.PROXY_USER if Config.PROXY_USER and Config.PROXY_USER.strip() else None
25
- proxy_pass = Config.PROXY_PASS if Config.PROXY_PASS and Config.PROXY_PASS.strip() else None
26
-
27
- get_sora_client.clients[auth_token] = SoraClient(
28
- proxy_host=proxy_host,
29
- proxy_port=proxy_port,
30
- proxy_user=proxy_user,
31
- proxy_pass=proxy_pass,
32
- auth_token=auth_token
33
- )
34
-
35
- return get_sora_client.clients[auth_token]
36
-
37
- # 从请求头中获取并验证认证令牌
38
- async def get_token_from_header(authorization: Optional[str] = Header(None)) -> str:
39
- """从请求头中获取认证令牌"""
40
- if not authorization:
41
- raise HTTPException(status_code=401, detail="缺少认证头")
42
-
43
- if not authorization.startswith("Bearer "):
44
- raise HTTPException(status_code=401, detail="无效的认证头格式")
45
-
46
- return authorization.replace("Bearer ", "")
47
-
48
- # 验证API key
49
- async def verify_api_key(request: Request):
50
- """检查请求头中的API密钥"""
51
- auth_header = request.headers.get("Authorization")
52
- if not auth_header or not auth_header.startswith("Bearer "):
53
- raise HTTPException(status_code=401, detail="缺少或无效的API key")
54
-
55
- api_key = auth_header.replace("Bearer ", "")
56
-
57
- # 验证API认证令牌
58
- if Config.API_AUTH_TOKEN:
59
- # 如果设置了API_AUTH_TOKEN环境变量,则进行验证
60
- if api_key != Config.API_AUTH_TOKEN:
61
- logger.warning(f"API认证失败: 提供的令牌不匹配")
62
- raise HTTPException(status_code=401, detail="API认证失败,令牌无效")
63
- else:
64
- # 如果未设置API_AUTH_TOKEN,则验证是否为管理面板的key
65
- from ..key_manager import key_manager
66
- valid_keys = [k.get("key") for k in key_manager.get_all_keys() if k.get("is_enabled", False)]
67
- if api_key not in valid_keys and api_key != Config.ADMIN_KEY:
68
- logger.warning(f"API认证失败: 提供的key不在有效列表中")
69
- raise HTTPException(status_code=401, detail="API认证失败,key无效")
70
-
71
- return api_key
72
-
73
- # 获取Sora客户端依赖
74
- def get_sora_client_dep(specific_key=None):
75
- """返回一个依赖函数,用于获取Sora客户端
76
-
77
- Args:
78
- specific_key: 指定使用的API密钥,如果不为None,则优先使用此密钥
79
- """
80
- async def _get_client(auth_token: str = Depends(verify_api_key)):
81
- from ..key_manager import key_manager
82
-
83
- # 如果提供了特定密钥,则使用该密钥
84
- if specific_key:
85
- sora_auth_token = specific_key
86
- else:
87
- # 使用密钥管理器获取可用的API密钥
88
- sora_auth_token = key_manager.get_key()
89
- if not sora_auth_token:
90
- raise HTTPException(status_code=429, detail="所有API key都已达到速率限制")
91
-
92
- # 获取Sora客户端
93
- return get_sora_client(sora_auth_token), sora_auth_token
94
-
95
- return _get_client
96
-
97
- # 验证JWT令牌并验证管理员权限
98
- async def verify_admin_jwt(token: str = Depends(get_token_from_header)) -> Dict[str, Any]:
99
- """验证JWT令牌并确认管理员权限"""
100
- # 验证JWT令牌
101
- payload = verify_jwt_token(token)
102
-
103
- # 验证是否为管理员角色
104
- if payload.get("role") != "admin":
105
- raise HTTPException(status_code=403, detail="没有管理员权限")
106
-
107
- return payload
108
-
109
- # 验证管理员权限(传统方法,保留向后兼容性)
110
- async def verify_admin(request: Request):
111
- """验证管理员权限"""
112
- auth_header = request.headers.get("Authorization")
113
- if not auth_header or not auth_header.startswith("Bearer "):
114
- raise HTTPException(status_code=401, detail="未授权")
115
-
116
- token = auth_header.replace("Bearer ", "")
117
-
118
- # 尝试JWT验证
119
- try:
120
- payload = verify_jwt_token(token)
121
- if payload.get("role") == "admin":
122
- return token
123
- except HTTPException:
124
- # JWT验证失败,尝试传统验证
125
- pass
126
-
127
- # 传统验证(直接验证管理员密钥)
128
- if token != Config.ADMIN_KEY:
129
- raise HTTPException(status_code=403, detail="没有管理员权限")
130
-
131
- return token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/generation.py DELETED
@@ -1,159 +0,0 @@
1
- import time
2
- import logging
3
- from typing import Dict, Any
4
- from fastapi import APIRouter, Depends, HTTPException
5
- from fastapi.responses import JSONResponse
6
-
7
- from ..api.dependencies import verify_api_key, get_sora_client_dep
8
- from ..services.image_service import get_generation_result, get_task_api_key
9
- from ..key_manager import key_manager
10
-
11
- # 设置日志
12
- logger = logging.getLogger("sora-api.generation")
13
-
14
- # 创建路由
15
- router = APIRouter()
16
-
17
- @router.get("/generation/{request_id}")
18
- async def check_generation_status(
19
- request_id: str,
20
- client_info = Depends(get_sora_client_dep()),
21
- api_key: str = Depends(verify_api_key)
22
- ):
23
- """
24
- 检查图像生成任务的状态
25
-
26
- Args:
27
- request_id: 要查询的请求ID
28
- client_info: Sora客户端信息(由依赖提供)
29
- api_key: API密钥(由依赖提供)
30
-
31
- Returns:
32
- 包含任务状态和结果的JSON响应
33
- """
34
- # 获取任务对应的原始API密钥
35
- task_api_key = get_task_api_key(request_id)
36
-
37
- # 如果找到任务对应的API密钥,则使用该密钥获取客户端
38
- if task_api_key:
39
- # 获取使用特定API密钥的客户端
40
- specific_client_dep = get_sora_client_dep(specific_key=task_api_key)
41
- client_info = await specific_client_dep(api_key)
42
-
43
- # 解析客户端信息
44
- _, sora_auth_token = client_info
45
-
46
- # 记录开始时间
47
- start_time = time.time()
48
- success = False
49
-
50
- try:
51
- # 获取任务结果
52
- result = get_generation_result(request_id)
53
-
54
- if result.get("status") == "not_found":
55
- raise HTTPException(status_code=404, detail=f"找不到生成任务: {request_id}")
56
-
57
- if result.get("status") == "completed":
58
- # 任务已完成,返回结果
59
- image_urls = result.get("image_urls", [])
60
-
61
- # 构建OpenAI兼容的响应
62
- response = {
63
- "id": request_id,
64
- "object": "chat.completion",
65
- "created": result.get("timestamp", int(time.time())),
66
- "model": "sora-1.0",
67
- "choices": [
68
- {
69
- "index": i,
70
- "message": {
71
- "role": "assistant",
72
- "content": f"![Generated Image]({url})"
73
- },
74
- "finish_reason": "stop"
75
- }
76
- for i, url in enumerate(image_urls)
77
- ],
78
- "usage": {
79
- "prompt_tokens": 0,
80
- "completion_tokens": 20,
81
- "total_tokens": 20
82
- }
83
- }
84
- success = True
85
-
86
- elif result.get("status") == "failed":
87
- # 任务失败
88
- message = result.get("message", f"```think\n生成失败: {result.get('error', '未知错误')}\n```")
89
-
90
- response = {
91
- "id": request_id,
92
- "object": "chat.completion",
93
- "created": result.get("timestamp", int(time.time())),
94
- "model": "sora-1.0",
95
- "choices": [
96
- {
97
- "index": 0,
98
- "message": {
99
- "role": "assistant",
100
- "content": message
101
- },
102
- "finish_reason": "error"
103
- }
104
- ],
105
- "usage": {
106
- "prompt_tokens": 0,
107
- "completion_tokens": 10,
108
- "total_tokens": 10
109
- }
110
- }
111
- success = False
112
-
113
- else: # 处理中
114
- # 任务仍在处理中
115
- message = result.get("message", "```think\n正在生成图像,请稍候...\n```")
116
-
117
- response = {
118
- "id": request_id,
119
- "object": "chat.completion",
120
- "created": result.get("timestamp", int(time.time())),
121
- "model": "sora-1.0",
122
- "choices": [
123
- {
124
- "index": 0,
125
- "message": {
126
- "role": "assistant",
127
- "content": message
128
- },
129
- "finish_reason": "processing"
130
- }
131
- ],
132
- "usage": {
133
- "prompt_tokens": 0,
134
- "completion_tokens": 10,
135
- "total_tokens": 10
136
- }
137
- }
138
- success = True
139
-
140
- # 记录请求结果
141
- response_time = time.time() - start_time
142
- key_manager.record_request_result(sora_auth_token, success, response_time)
143
-
144
- # 返回响应
145
- return JSONResponse(content=response)
146
-
147
- except HTTPException:
148
- # 直接重新抛出HTTP异常
149
- raise
150
- except Exception as e:
151
- # 处理其他异常
152
- success = False
153
- logger.error(f"检查任务状态失败: {str(e)}", exc_info=True)
154
-
155
- # 记录请求结果
156
- response_time = time.time() - start_time
157
- key_manager.record_request_result(sora_auth_token, success, response_time)
158
-
159
- raise HTTPException(status_code=500, detail=f"检查任务状态失败: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/api/health.py DELETED
@@ -1,57 +0,0 @@
1
- import time
2
- import platform
3
- import os
4
- import sys
5
- import psutil
6
- from fastapi import APIRouter, Depends
7
- from ..key_manager import key_manager
8
-
9
- # 创建路由
10
- router = APIRouter()
11
-
12
- @router.get("/health")
13
- async def health_check():
14
- """
15
- API健康检查端点
16
-
17
- 返回:
18
- 基本的健康状态信息
19
- """
20
- return {
21
- "status": "ok",
22
- "timestamp": time.time(),
23
- "version": "2.0.0"
24
- }
25
-
26
- @router.get("/health/extended")
27
- async def extended_health_check():
28
- """
29
- 扩展的健康检查端点
30
-
31
- 返回:
32
- 详细的系统和服务状态信息
33
- """
34
- # 获取系统信息
35
- system_info = {
36
- "platform": platform.platform(),
37
- "python_version": sys.version,
38
- "cpu_count": psutil.cpu_count(),
39
- "memory_total": psutil.virtual_memory().total,
40
- "memory_available": psutil.virtual_memory().available,
41
- "disk_usage": psutil.disk_usage('/').percent
42
- }
43
-
44
- # 获取服务信息
45
- service_info = {
46
- "uptime": time.time() - psutil.Process(os.getpid()).create_time(),
47
- "active_keys": sum(1 for k in key_manager.keys if k.get("is_enabled", False)),
48
- "total_keys": len(key_manager.keys),
49
- }
50
-
51
- return {
52
- "status": "ok",
53
- "timestamp": time.time(),
54
- "version": "2.0.0",
55
- "system": system_info,
56
- "service": service_info,
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/app.py DELETED
@@ -1,171 +0,0 @@
1
- import os
2
- import aiohttp
3
- import logging
4
- from fastapi import FastAPI, Request, HTTPException
5
- from fastapi.responses import JSONResponse, FileResponse
6
- from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.staticfiles import StaticFiles
8
- from fastapi.exceptions import RequestValidationError
9
-
10
- from .config import Config
11
- from .key_manager import key_manager
12
- from .api import main_router
13
- from .api.dependencies import session_pool
14
-
15
- # 配置日志
16
- logging.basicConfig(
17
- level=getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper()),
18
- format="%(asctime)s [%(levelname)s] %(message)s",
19
- datefmt="%Y-%m-%d %H:%M:%S"
20
- )
21
-
22
- logger = logging.getLogger("sora-api")
23
-
24
- # 创建FastAPI应用
25
- app = FastAPI(
26
- title="OpenAI Compatible Sora API",
27
- description="为Sora提供OpenAI兼容接口的API服务",
28
- version="2.0.0",
29
- docs_url="/docs",
30
- redoc_url="/redoc",
31
- )
32
-
33
- # 配置CORS中间件
34
- origins = [
35
- "http://localhost",
36
- "http://localhost:8890",
37
- f"http://{Config.HOST}:{Config.PORT}",
38
- Config.BASE_URL,
39
- ]
40
-
41
- app.add_middleware(
42
- CORSMiddleware,
43
- allow_origins=origins,
44
- allow_credentials=True,
45
- allow_methods=["GET", "POST", "PUT", "DELETE"],
46
- allow_headers=["Authorization", "Content-Type"],
47
- )
48
-
49
- # 异常处理
50
- @app.exception_handler(RequestValidationError)
51
- async def validation_exception_handler(request: Request, exc: RequestValidationError):
52
- """处理请求验证错误"""
53
- return JSONResponse(
54
- status_code=422,
55
- content={"detail": f"请求验证错误: {str(exc)}"}
56
- )
57
-
58
- @app.exception_handler(Exception)
59
- async def global_exception_handler(request: Request, exc: Exception):
60
- """全局异常处理器"""
61
- # 记录错误
62
- logger.error(f"全局异常: {str(exc)}", exc_info=True)
63
-
64
- # 如果是已知的HTTPException,保持原始状态码和详情
65
- if isinstance(exc, HTTPException):
66
- return JSONResponse(
67
- status_code=exc.status_code,
68
- content={"detail": exc.detail}
69
- )
70
-
71
- # 其他异常返回500状态码
72
- return JSONResponse(
73
- status_code=500,
74
- content={"detail": f"服务器内部错误: {str(exc)}"}
75
- )
76
-
77
- # 应用启动和关闭事件
78
- @app.on_event("startup")
79
- async def startup_event():
80
- """应用启动时执行的操作"""
81
- global session_pool
82
- # 创建共享会话池
83
- session_pool = aiohttp.ClientSession()
84
- logger.info("应用已启动,创建了全局会话池")
85
-
86
- # 初始化时保存管理员密钥
87
- Config.save_admin_key()
88
-
89
- # 确保静态文件目录存在
90
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin"), exist_ok=True)
91
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/js"), exist_ok=True)
92
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/css"), exist_ok=True)
93
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
94
-
95
- # 输出图片保存目录的信息
96
- logger.info(f"图片保存目录: {Config.IMAGE_SAVE_DIR}")
97
-
98
- # 图片访问URL
99
- base_url = Config.BASE_URL.rstrip('/')
100
- if Config.STATIC_PATH_PREFIX:
101
- logger.info(f"图片将通过 {base_url}{Config.STATIC_PATH_PREFIX}/images/<filename> 访问")
102
- else:
103
- logger.info(f"图片将通过 {base_url}/images/<filename> 访问")
104
-
105
- # 打印配置信息
106
- Config.print_config()
107
-
108
- @app.on_event("shutdown")
109
- async def shutdown_event():
110
- """应用关闭时执行的操作"""
111
- # 关闭会话池
112
- if session_pool:
113
- await session_pool.close()
114
- logger.info("应用已关闭,清理了全局会话池")
115
-
116
- # 添加根路径路由
117
- @app.get("/")
118
- async def root():
119
- """根路径,返回系统状态信息"""
120
- return {
121
- "status": "OK",
122
- "message": "系统运行正常",
123
- "version": app.version,
124
- "name": app.title
125
- }
126
-
127
- # 挂载静态文件目录
128
- app.mount("/static", StaticFiles(directory=Config.STATIC_DIR), name="static")
129
-
130
- # 通用图片访问路由 - 支持多种路径格式
131
- @app.get("/images/{filename}")
132
- @app.get("/static/images/{filename}")
133
- async def get_image(filename: str):
134
- """处理图片请求 - 无论保存在哪里"""
135
- # 直接从IMAGE_SAVE_DIR获取图片
136
- file_path = os.path.join(Config.IMAGE_SAVE_DIR, filename)
137
- if os.path.exists(file_path):
138
- return FileResponse(file_path)
139
- else:
140
- logger.warning(f"请求的图片不存在: {file_path}")
141
- raise HTTPException(status_code=404, detail="图片不存在")
142
-
143
- # 添加静态文件路径前缀的兼容路由
144
- if Config.STATIC_PATH_PREFIX:
145
- prefix_path = Config.STATIC_PATH_PREFIX.lstrip("/")
146
-
147
- @app.get(f"/{prefix_path}/images/{{filename}}")
148
- async def get_prefixed_image(filename: str):
149
- """处理带前缀的图片请求"""
150
- return await get_image(filename)
151
-
152
- # 管理界面路由
153
- @app.get("/admin")
154
- async def admin_panel():
155
- """返回管理面板HTML页面"""
156
- return FileResponse(os.path.join(Config.STATIC_DIR, "admin/index.html"))
157
-
158
- # 现在使用JWT��证,不再需要直接暴露管理员密钥
159
-
160
- # 注册所有API路由
161
- app.include_router(main_router)
162
-
163
- # 应用入口点(供uvicorn直接调用)
164
- if __name__ == "__main__":
165
- import uvicorn
166
- uvicorn.run(
167
- "app:app",
168
- host=Config.HOST,
169
- port=Config.PORT,
170
- reload=Config.VERBOSE_LOGGING
171
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/app.py.backup DELETED
@@ -1,1159 +0,0 @@
1
- import asyncio
2
- import json
3
- import time
4
- import uuid
5
- import base64
6
- import os
7
- import tempfile
8
- import threading
9
- import dotenv
10
- import logging
11
- from typing import List, Dict, Any, Optional, Union
12
- from fastapi import FastAPI, HTTPException, Depends, Request, BackgroundTasks, File, UploadFile, Form
13
- from fastapi.responses import StreamingResponse, JSONResponse, FileResponse, HTMLResponse
14
- from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.staticfiles import StaticFiles
16
- from pydantic import BaseModel, Field
17
- import uvicorn
18
- import re
19
-
20
- from .key_manager import KeyManager
21
- from .sora_integration import SoraClient
22
- from .config import Config
23
- from .utils import localize_image_urls # 导入新增的图片本地化功能
24
-
25
- # 日志系统配置
26
- class LogConfig:
27
- LEVEL = os.getenv("LOG_LEVEL", "WARNING").upper()
28
- FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
29
-
30
- # 初始化日志
31
- logging.basicConfig(
32
- level=getattr(logging, LogConfig.LEVEL),
33
- format=LogConfig.FORMAT,
34
- datefmt="%Y-%m-%d %H:%M:%S"
35
- )
36
- logger = logging.getLogger("sora-api")
37
-
38
- # 打印日志级别信息
39
- logger.info(f"日志级别设置为: {LogConfig.LEVEL}")
40
- logger.info(f"要调整日志级别,请设置环境变量 LOG_LEVEL=DEBUG|INFO|WARNING|ERROR")
41
-
42
- # 创建FastAPI应用
43
- app = FastAPI(title="OpenAI Compatible Sora API")
44
-
45
- # 添加CORS支持
46
- app.add_middleware(
47
- CORSMiddleware,
48
- allow_origins=["*"],
49
- allow_credentials=True,
50
- allow_methods=["*"],
51
- allow_headers=["*"],
52
- )
53
-
54
- # 确保静态文件目录存在
55
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin"), exist_ok=True)
56
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/js"), exist_ok=True)
57
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/css"), exist_ok=True)
58
- os.makedirs(os.path.join(Config.STATIC_DIR, "images"), exist_ok=True) # 确保图片目录存在
59
-
60
- # 打印配置信息
61
- Config.print_config()
62
-
63
- # 挂载静态文件目录
64
- app.mount("/static", StaticFiles(directory=Config.STATIC_DIR), name="static")
65
-
66
- # 初始化Key管理器
67
- key_manager = KeyManager(storage_file=Config.KEYS_STORAGE_FILE)
68
-
69
- # 初始化时保存管理员密钥
70
- Config.save_admin_key()
71
-
72
- # 创建Sora客户端池
73
- sora_clients = {}
74
-
75
- # 存储生成结果的全局字典
76
- generation_results = {}
77
-
78
- # 请求模型
79
- class ContentItem(BaseModel):
80
- type: str
81
- text: Optional[str] = None
82
- image_url: Optional[Dict[str, str]] = None
83
-
84
- class ChatMessage(BaseModel):
85
- role: str
86
- content: Union[str, List[ContentItem]]
87
-
88
- class ChatCompletionRequest(BaseModel):
89
- model: str
90
- messages: List[ChatMessage]
91
- temperature: Optional[float] = 1.0
92
- top_p: Optional[float] = 1.0
93
- n: Optional[int] = 1
94
- stream: Optional[bool] = False
95
- max_tokens: Optional[int] = None
96
- presence_penalty: Optional[float] = 0
97
- frequency_penalty: Optional[float] = 0
98
-
99
- # API密钥管理模型
100
- class ApiKeyCreate(BaseModel):
101
- name: str = Field(..., description="密钥名称")
102
- key_value: str = Field(..., description="Bearer Token值")
103
- weight: int = Field(default=1, ge=1, le=10, description="权重值")
104
- rate_limit: int = Field(default=60, description="每分钟最大请求数")
105
- is_enabled: bool = Field(default=True, description="是否启用")
106
- notes: Optional[str] = Field(default=None, description="备注信息")
107
-
108
- class ApiKeyUpdate(BaseModel):
109
- name: Optional[str] = None
110
- key_value: Optional[str] = None
111
- weight: Optional[int] = None
112
- rate_limit: Optional[int] = None
113
- is_enabled: Optional[bool] = None
114
- notes: Optional[str] = None
115
-
116
- # 获取Sora客户端
117
- def get_sora_client(auth_token: str) -> SoraClient:
118
- if auth_token not in sora_clients:
119
- proxy_host = Config.PROXY_HOST if Config.PROXY_HOST and Config.PROXY_HOST.strip() else None
120
- proxy_port = Config.PROXY_PORT if Config.PROXY_PORT and Config.PROXY_PORT.strip() else None
121
-
122
- sora_clients[auth_token] = SoraClient(
123
- proxy_host=proxy_host,
124
- proxy_port=proxy_port,
125
- auth_token=auth_token
126
- )
127
- return sora_clients[auth_token]
128
-
129
- # 验证API key
130
- async def verify_api_key(request: Request):
131
- auth_header = request.headers.get("Authorization")
132
- if not auth_header or not auth_header.startswith("Bearer "):
133
- raise HTTPException(status_code=401, detail="缺少或无效的API key")
134
-
135
- api_key = auth_header.replace("Bearer ", "")
136
- # 在实际应用中,这里应该验证key的有效性
137
- # 这里简化处理,假设所有key都有效
138
- return api_key
139
-
140
- # 验证管理员权限
141
- async def verify_admin(request: Request):
142
- auth_header = request.headers.get("Authorization")
143
- if not auth_header or not auth_header.startswith("Bearer "):
144
- raise HTTPException(status_code=401, detail="未授权")
145
-
146
- admin_key = auth_header.replace("Bearer ", "")
147
- # 这里应该检查是否为管理员密钥
148
- # 简化处理,假设admin_key是预设的管理员密钥
149
- if admin_key != Config.ADMIN_KEY:
150
- raise HTTPException(status_code=403, detail="没有管理员权限")
151
- return admin_key
152
-
153
- # 将处理中状态消息格式化为think代码块
154
- def format_think_block(message):
155
- """将消息放入```think代码块中"""
156
- return f"```think\n{message}\n```"
157
-
158
- # 后台任务处理函数 - 文本生成图像
159
- async def process_image_generation(
160
- request_id: str,
161
- sora_client: SoraClient,
162
- prompt: str,
163
- num_images: int = 1,
164
- width: int = 720,
165
- height: int = 480
166
- ):
167
- try:
168
- # 更新状态为生成中
169
- generation_results[request_id] = {
170
- "status": "processing",
171
- "message": format_think_block("正在生成图像中,请耐心等待..."),
172
- "timestamp": int(time.time())
173
- }
174
-
175
- # 生成图像
176
- logger.info(f"[{request_id}] 开始生成图像, 提示词: {prompt}")
177
- image_urls = await sora_client.generate_image(
178
- prompt=prompt,
179
- num_images=num_images,
180
- width=width,
181
- height=height
182
- )
183
-
184
- # 验证生成结果
185
- if isinstance(image_urls, str):
186
- logger.warning(f"[{request_id}] 图像生成失败或返回了错误信息: {image_urls}")
187
- generation_results[request_id] = {
188
- "status": "failed",
189
- "error": image_urls,
190
- "message": format_think_block(f"图像生成失败: {image_urls}"),
191
- "timestamp": int(time.time())
192
- }
193
- return
194
-
195
- if not image_urls:
196
- logger.warning(f"[{request_id}] 图像生成返回了空列表")
197
- generation_results[request_id] = {
198
- "status": "failed",
199
- "error": "图像生成返回了空结果",
200
- "message": format_think_block("图像生成失败: 服务器返回了空结果"),
201
- "timestamp": int(time.time())
202
- }
203
- return
204
-
205
- logger.info(f"[{request_id}] 成功生成 {len(image_urls)} 张图片")
206
- if logger.isEnabledFor(logging.DEBUG):
207
- for i, url in enumerate(image_urls):
208
- logger.debug(f"[{request_id}] 图片 {i+1}: {url}")
209
-
210
- # 本地化图片URL
211
- if Config.IMAGE_LOCALIZATION:
212
- logger.info(f"[{request_id}] 准备进行图片本地化处理")
213
- logger.debug(f"[{request_id}] 图片本地化配置: 启用={Config.IMAGE_LOCALIZATION}, 保存目录={Config.IMAGE_SAVE_DIR}")
214
- try:
215
- localized_urls = await localize_image_urls(image_urls)
216
- logger.info(f"[{request_id}] 图片本地化处理完成")
217
-
218
- # 检查本地化结果
219
- if not localized_urls:
220
- logger.warning(f"[{request_id}] 本地化处理返回了空列表,将使用原始URL")
221
- localized_urls = image_urls
222
-
223
- # 检查是否所有URL都被正确本地化
224
- local_count = sum(1 for url in localized_urls if url.startswith("/static/") or "/static/" in url)
225
- logger.info(f"[{request_id}] 本地化结果: 总计 {len(localized_urls)} 张图片,成功本地化 {local_count} 张")
226
-
227
- if local_count == 0:
228
- logger.warning(f"[{request_id}] 警告:没有一个URL被成功本地化,将使用原始URL")
229
- localized_urls = image_urls
230
-
231
- # 打印结果对比
232
- if logger.isEnabledFor(logging.DEBUG):
233
- for i, (orig, local) in enumerate(zip(image_urls, localized_urls)):
234
- logger.debug(f"[{request_id}] 图片 {i+1} 本地化结果: {orig} -> {local}")
235
-
236
- image_urls = localized_urls
237
- except Exception as e:
238
- logger.error(f"[{request_id}] 图片本地化过程中发生错误: {str(e)}")
239
- if logger.isEnabledFor(logging.DEBUG):
240
- import traceback
241
- logger.debug(traceback.format_exc())
242
- logger.info(f"[{request_id}] 由于错误,将使用原始URL")
243
- else:
244
- logger.info(f"[{request_id}] 图片本地化功能未启用,使用原始URL")
245
-
246
- # 存储结果
247
- generation_results[request_id] = {
248
- "status": "completed",
249
- "image_urls": image_urls,
250
- "timestamp": int(time.time())
251
- }
252
-
253
- # 30分钟后自动清理结果
254
- threading.Timer(1800, lambda: generation_results.pop(request_id, None)).start()
255
-
256
- except Exception as e:
257
- error_message = f"图像生成失败: {str(e)}"
258
- generation_results[request_id] = {
259
- "status": "failed",
260
- "error": error_message,
261
- "message": format_think_block(error_message),
262
- "timestamp": int(time.time())
263
- }
264
- logger.error(f"图像生成失败 (ID: {request_id}): {str(e)}")
265
- if logger.isEnabledFor(logging.DEBUG):
266
- import traceback
267
- logger.debug(traceback.format_exc())
268
-
269
- # 后台任务处理函数 - 带图片的remix
270
- async def process_image_remix(
271
- request_id: str,
272
- sora_client: SoraClient,
273
- prompt: str,
274
- image_data: str,
275
- num_images: int = 1
276
- ):
277
- try:
278
- # 更新状态为处理中
279
- generation_results[request_id] = {
280
- "status": "processing",
281
- "message": format_think_block("正在处理上传的图片..."),
282
- "timestamp": int(time.time())
283
- }
284
-
285
- # 保存base64图片到临时文件
286
- temp_dir = tempfile.mkdtemp()
287
- temp_image_path = os.path.join(temp_dir, f"upload_{uuid.uuid4()}.png")
288
-
289
- try:
290
- # 解码并保存图片
291
- with open(temp_image_path, "wb") as f:
292
- f.write(base64.b64decode(image_data))
293
-
294
- # 更新状态为上传中
295
- generation_results[request_id] = {
296
- "status": "processing",
297
- "message": format_think_block("正在上传图片到Sora服务..."),
298
- "timestamp": int(time.time())
299
- }
300
-
301
- # 上传图片
302
- upload_result = await sora_client.upload_image(temp_image_path)
303
- media_id = upload_result['id']
304
-
305
- # 更新状态为生成中
306
- generation_results[request_id] = {
307
- "status": "processing",
308
- "message": format_think_block("正在基于图片生成新图像..."),
309
- "timestamp": int(time.time())
310
- }
311
-
312
- # 执行remix生成
313
- logger.info(f"[{request_id}] 开始生成Remix图像, 提示词: {prompt}")
314
- image_urls = await sora_client.generate_image_remix(
315
- prompt=prompt,
316
- media_id=media_id,
317
- num_images=num_images
318
- )
319
-
320
- # 本地化图片URL
321
- if Config.IMAGE_LOCALIZATION:
322
- logger.info(f"[{request_id}] 准备进行图片本地化处理")
323
- localized_urls = await localize_image_urls(image_urls)
324
- image_urls = localized_urls
325
- logger.info(f"[{request_id}] Remix图片本地化处理完成")
326
-
327
- # 存储结果
328
- generation_results[request_id] = {
329
- "status": "completed",
330
- "image_urls": image_urls,
331
- "timestamp": int(time.time())
332
- }
333
-
334
- # 30分钟后自动清理结果
335
- threading.Timer(1800, lambda: generation_results.pop(request_id, None)).start()
336
-
337
- finally:
338
- # 清理临时文件
339
- if os.path.exists(temp_image_path):
340
- os.remove(temp_image_path)
341
- if os.path.exists(temp_dir):
342
- os.rmdir(temp_dir)
343
-
344
- except Exception as e:
345
- error_message = f"图像Remix失败: {str(e)}"
346
- generation_results[request_id] = {
347
- "status": "failed",
348
- "error": error_message,
349
- "message": format_think_block(error_message),
350
- "timestamp": int(time.time())
351
- }
352
- logger.error(f"图像Remix失败 (ID: {request_id}): {str(e)}")
353
-
354
- # 添加一个新端点用于检查生成状态
355
- @app.get("/v1/generation/{request_id}")
356
- async def check_generation_status(request_id: str, api_key: str = Depends(verify_api_key)):
357
- """
358
- 检查图像生成任务的状态
359
- """
360
- # 获取一个可用的key并记录开始时间
361
- sora_auth_token = key_manager.get_key()
362
- if not sora_auth_token:
363
- raise HTTPException(status_code=429, detail="所有API key都已达到速率限制")
364
-
365
- start_time = time.time()
366
- success = False
367
-
368
- try:
369
- if request_id not in generation_results:
370
- raise HTTPException(status_code=404, detail=f"找不到生成任务: {request_id}")
371
-
372
- result = generation_results[request_id]
373
-
374
- if result["status"] == "completed":
375
- image_urls = result["image_urls"]
376
-
377
- # 构建OpenAI兼容的响应
378
- response = {
379
- "id": request_id,
380
- "object": "chat.completion",
381
- "created": result["timestamp"],
382
- "model": "sora-1.0",
383
- "choices": [
384
- {
385
- "index": i,
386
- "message": {
387
- "role": "assistant",
388
- "content": f"![Generated Image]({url})"
389
- },
390
- "finish_reason": "stop"
391
- }
392
- for i, url in enumerate(image_urls)
393
- ],
394
- "usage": {
395
- "prompt_tokens": 0, # 简化的令牌计算
396
- "completion_tokens": 20,
397
- "total_tokens": 20
398
- }
399
- }
400
- success = True
401
- return JSONResponse(content=response)
402
-
403
- elif result["status"] == "failed":
404
- if "message" in result:
405
- # 返回带有格式化错误消息的响应
406
- response = {
407
- "id": request_id,
408
- "object": "chat.completion",
409
- "created": result["timestamp"],
410
- "model": "sora-1.0",
411
- "choices": [
412
- {
413
- "index": 0,
414
- "message": {
415
- "role": "assistant",
416
- "content": result["message"]
417
- },
418
- "finish_reason": "error"
419
- }
420
- ],
421
- "usage": {
422
- "prompt_tokens": 0,
423
- "completion_tokens": 10,
424
- "total_tokens": 10
425
- }
426
- }
427
- success = False
428
- return JSONResponse(content=response)
429
- else:
430
- # 向后兼容,使用老的方式
431
- raise HTTPException(status_code=500, detail=f"生成失败: {result['error']}")
432
-
433
- else: # 处理中
434
- message = result.get("message", "```think\n正在生成图像,请稍候...\n```")
435
- response = {
436
- "id": request_id,
437
- "object": "chat.completion",
438
- "created": result["timestamp"],
439
- "model": "sora-1.0",
440
- "choices": [
441
- {
442
- "index": 0,
443
- "message": {
444
- "role": "assistant",
445
- "content": message
446
- },
447
- "finish_reason": "processing"
448
- }
449
- ],
450
- "usage": {
451
- "prompt_tokens": 0,
452
- "completion_tokens": 10,
453
- "total_tokens": 10
454
- }
455
- }
456
- success = True
457
- return JSONResponse(content=response)
458
- except Exception as e:
459
- success = False
460
- raise HTTPException(status_code=500, detail=f"检查任务状态失败: {str(e)}")
461
- finally:
462
- # 记录请求结果
463
- response_time = time.time() - start_time
464
- key_manager.record_request_result(sora_auth_token, success, response_time)
465
-
466
- # 聊天完成端点
467
- @app.post("/v1/chat/completions")
468
- async def chat_completions(
469
- request: ChatCompletionRequest,
470
- api_key: str = Depends(verify_api_key),
471
- background_tasks: BackgroundTasks = None
472
- ):
473
- # 获取一个可用的key
474
- sora_auth_token = key_manager.get_key()
475
- if not sora_auth_token:
476
- raise HTTPException(status_code=429, detail="所有API key都已达到速率限制")
477
-
478
- # 获取Sora客户端
479
- sora_client = get_sora_client(sora_auth_token)
480
-
481
- # 分析最后一条用户消息以提取内容
482
- user_messages = [m for m in request.messages if m.role == "user"]
483
- if not user_messages:
484
- raise HTTPException(status_code=400, detail="至少需要一条用户消息")
485
-
486
- last_user_message = user_messages[-1]
487
- prompt = ""
488
- image_data = None
489
-
490
- # 提取提示词和图片数据
491
- if isinstance(last_user_message.content, str):
492
- # 简单的字符串内容
493
- prompt = last_user_message.content
494
-
495
- # 检查是否包含内嵌的base64图片
496
- pattern = r'data:image\/[^;]+;base64,([^"]+)'
497
- match = re.search(pattern, prompt)
498
- if match:
499
- image_data = match.group(1)
500
- # 从提示词中删除base64数据,以保持提示词的可读性
501
- prompt = re.sub(pattern, "[已上传图片]", prompt)
502
- else:
503
- # 多模态内容,提取文本和图片
504
- content_items = last_user_message.content
505
- text_parts = []
506
-
507
- for item in content_items:
508
- if item.type == "text" and item.text:
509
- text_parts.append(item.text)
510
- elif item.type == "image_url" and item.image_url:
511
- # 如果有图片URL包含base64数据
512
- url = item.image_url.get("url", "")
513
- if url.startswith("data:image/"):
514
- pattern = r'data:image\/[^;]+;base64,([^"]+)'
515
- match = re.search(pattern, url)
516
- if match:
517
- image_data = match.group(1)
518
- text_parts.append("[已上传图片]")
519
-
520
- prompt = " ".join(text_parts)
521
-
522
- # 记录开始时间
523
- start_time = time.time()
524
- success = False
525
-
526
- # 处理图片生成
527
- try:
528
- # 检查是否为流式响应
529
- if request.stream:
530
- # 流式响应特殊处理文本+图片的情况
531
- if image_data:
532
- response = StreamingResponse(
533
- generate_streaming_remix_response(sora_client, prompt, image_data, request.n),
534
- media_type="text/event-stream"
535
- )
536
- else:
537
- response = StreamingResponse(
538
- generate_streaming_response(sora_client, prompt, request.n),
539
- media_type="text/event-stream"
540
- )
541
- success = True
542
-
543
- # 记录请求结果(流式响应立即记录)
544
- response_time = time.time() - start_time
545
- key_manager.record_request_result(sora_auth_token, success, response_time)
546
-
547
- return response
548
- else:
549
- # 对于非流式响应,返回一个即时响应,表示任务已接收
550
- # 创建一个唯一ID
551
- request_id = f"chatcmpl-{uuid.uuid4().hex}"
552
-
553
- # 在结果字典中创建初始状态
554
- processing_message = "正在准备生成任务,请稍候..."
555
- generation_results[request_id] = {
556
- "status": "processing",
557
- "message": format_think_block(processing_message),
558
- "timestamp": int(time.time())
559
- }
560
-
561
- # 添加后台任务
562
- if image_data:
563
- background_tasks.add_task(
564
- process_image_remix,
565
- request_id,
566
- sora_client,
567
- prompt,
568
- image_data,
569
- request.n
570
- )
571
- else:
572
- background_tasks.add_task(
573
- process_image_generation,
574
- request_id,
575
- sora_client,
576
- prompt,
577
- request.n,
578
- 720, # width
579
- 480 # height
580
- )
581
-
582
- # 立即返回一个"正在处理中"的响应
583
- response = {
584
- "id": request_id,
585
- "object": "chat.completion",
586
- "created": int(time.time()),
587
- "model": "sora-1.0",
588
- "choices": [
589
- {
590
- "index": 0,
591
- "message": {
592
- "role": "assistant",
593
- "content": format_think_block(processing_message)
594
- },
595
- "finish_reason": "processing"
596
- }
597
- ],
598
- "usage": {
599
- "prompt_tokens": len(prompt) // 4,
600
- "completion_tokens": 10,
601
- "total_tokens": len(prompt) // 4 + 10
602
- }
603
- }
604
-
605
- success = True
606
-
607
- # 记录请求结果(非流式响应立即记录)
608
- response_time = time.time() - start_time
609
- key_manager.record_request_result(sora_auth_token, success, response_time)
610
-
611
- return JSONResponse(content=response)
612
- except Exception as e:
613
- success = False
614
-
615
- # 记录请求结果(异常情况也记录)
616
- response_time = time.time() - start_time
617
- key_manager.record_request_result(sora_auth_token, success, response_time)
618
-
619
- raise HTTPException(status_code=500, detail=f"图像生成失败: {str(e)}")
620
-
621
- # 流式响应生成器 - 普通文本到图像
622
- async def generate_streaming_response(
623
- sora_client: SoraClient,
624
- prompt: str,
625
- n_images: int = 1
626
- ):
627
- request_id = f"chatcmpl-{uuid.uuid4().hex}"
628
-
629
- # 发送开始事件
630
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]})}\n\n"
631
-
632
- # 发送处理中的消息(放在代码块中)
633
- start_msg = "```think\n正在生成图像,请稍候...\n"
634
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': start_msg}, 'finish_reason': None}]})}\n\n"
635
-
636
- # 创建一个后台任务来生成图像
637
- logger.info(f"[流式响应 {request_id}] 开始生成图像, 提示词: {prompt}")
638
- generation_task = asyncio.create_task(sora_client.generate_image(
639
- prompt=prompt,
640
- num_images=n_images,
641
- width=720,
642
- height=480
643
- ))
644
-
645
- # 每5秒发送一条"仍在生成中"的消息,防止连接超时
646
- progress_messages = [
647
- "正在处理您的请求...",
648
- "仍在生成图像中,请继续等待...",
649
- "Sora正在创作您的图像作品...",
650
- "图像生成需要一点时间,感谢您的耐心等待...",
651
- "我们正在努力为您创作高质量图像..."
652
- ]
653
-
654
- i = 0
655
- while not generation_task.done():
656
- # 每5秒发送一次进度消息
657
- await asyncio.sleep(5)
658
- progress_msg = progress_messages[i % len(progress_messages)]
659
- i += 1
660
- content = "\n" + progress_msg + "\n"
661
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})}\n\n"
662
-
663
- try:
664
- # 获取生成结果
665
- image_urls = await generation_task
666
- logger.info(f"[流式响应 {request_id}] 图像生成完成,获取到 {len(image_urls) if isinstance(image_urls, list) else '非列表'} 个URL")
667
-
668
- # 本地化图片URL
669
- if Config.IMAGE_LOCALIZATION and isinstance(image_urls, list) and image_urls:
670
- logger.info(f"[流式响应 {request_id}] 准备进行图片本地化处理")
671
- try:
672
- localized_urls = await localize_image_urls(image_urls)
673
- logger.info(f"[流式响应 {request_id}] 图片本地化处理完成")
674
-
675
- # 检查本地化结果
676
- if not localized_urls:
677
- logger.warning(f"[流式响应 {request_id}] 本地化处理返回了空列表,将使用原始URL")
678
- localized_urls = image_urls
679
-
680
- # 检查是否所有URL都被正确本地化
681
- local_count = sum(1 for url in localized_urls if url.startswith("/static/") or "/static/" in url)
682
- if local_count == 0:
683
- logger.warning(f"[流式响应 {request_id}] 警告:没有一个URL被成功本地化,将使用原始URL")
684
- localized_urls = image_urls
685
- else:
686
- logger.info(f"[流式响应 {request_id}] 成功本地化 {local_count}/{len(localized_urls)} 张图片")
687
-
688
- # 打印本地化对比结果
689
- if logger.isEnabledFor(logging.DEBUG):
690
- for i, (orig, local) in enumerate(zip(image_urls, localized_urls)):
691
- logger.debug(f"[流式响应 {request_id}] 图片 {i+1}: {orig} -> {local}")
692
-
693
- image_urls = localized_urls
694
- except Exception as e:
695
- logger.error(f"[流式响应 {request_id}] 图片本地化过程中发生错误: {str(e)}")
696
- if logger.isEnabledFor(logging.DEBUG):
697
- import traceback
698
- logger.debug(traceback.format_exc())
699
- logger.info(f"[流式响应 {request_id}] 由于错误,将使用原始URL")
700
- elif not Config.IMAGE_LOCALIZATION:
701
- logger.info(f"[流式响应 {request_id}] 图片本地化功能未启用")
702
- elif not isinstance(image_urls, list) or not image_urls:
703
- logger.warning(f"[流式响应 {request_id}] 无法进行本地化: 图像结果不是有效的URL列表")
704
-
705
- # 结束代码块
706
- content_str = "\n```\n\n"
707
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
708
-
709
- # 添加生成的图片URLs
710
- for i, url in enumerate(image_urls):
711
- if i > 0:
712
- content_str = "\n\n"
713
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
714
-
715
- image_markdown = f"![Generated Image]({url})"
716
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': image_markdown}, 'finish_reason': None}]})}\n\n"
717
-
718
- # 发送完成事件
719
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
720
-
721
- # 发送结束标志
722
- yield "data: [DONE]\n\n"
723
- except Exception as e:
724
- error_msg = f"图像生成失败: {str(e)}"
725
- logger.error(f"[流式响应 {request_id}] 错误: {error_msg}")
726
- if logger.isEnabledFor(logging.DEBUG):
727
- import traceback
728
- logger.debug(traceback.format_exc())
729
- error_content = f"\n{error_msg}\n```"
730
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': error_content}, 'finish_reason': 'error'}]})}\n\n"
731
- yield "data: [DONE]\n\n"
732
-
733
- # 流式响应生成器 - 带图片的remix
734
- async def generate_streaming_remix_response(
735
- sora_client: SoraClient,
736
- prompt: str,
737
- image_data: str,
738
- n_images: int = 1
739
- ):
740
- request_id = f"chatcmpl-{uuid.uuid4().hex}"
741
-
742
- # 发送开始事件
743
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]})}\n\n"
744
-
745
- try:
746
- # 保存base64图片到临时文件
747
- temp_dir = tempfile.mkdtemp()
748
- temp_image_path = os.path.join(temp_dir, f"upload_{uuid.uuid4()}.png")
749
-
750
- try:
751
- # 解码并保存图片
752
- with open(temp_image_path, "wb") as f:
753
- f.write(base64.b64decode(image_data))
754
-
755
- # 上传图片
756
- upload_msg = "```think\n上传图片中...\n"
757
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': upload_msg}, 'finish_reason': None}]})}\n\n"
758
-
759
- logger.info(f"[流式响应Remix {request_id}] 上传图片中")
760
- upload_result = await sora_client.upload_image(temp_image_path)
761
- media_id = upload_result['id']
762
-
763
- # 发送生成中消息
764
- generate_msg = "\n基于图片生成新图像中...\n"
765
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': generate_msg}, 'finish_reason': None}]})}\n\n"
766
-
767
- # 创建一个后台任务来生成图像
768
- logger.info(f"[流式响应Remix {request_id}] 开始生成图像,提示词: {prompt}")
769
- generation_task = asyncio.create_task(sora_client.generate_image_remix(
770
- prompt=prompt,
771
- media_id=media_id,
772
- num_images=n_images
773
- ))
774
-
775
- # 每5秒发送一条"仍在生成中"的消息,防止连接超时
776
- progress_messages = [
777
- "正在处理您的请求...",
778
- "仍在生成图像中,请继续等待...",
779
- "Sora正在基于您的图片创作新作品...",
780
- "图像生成需要一点时间,感谢您的耐心等待...",
781
- "正在努力融合您的风格和提示词,打造专属图像..."
782
- ]
783
-
784
- i = 0
785
- while not generation_task.done():
786
- # 每5秒发送一次进度消息
787
- await asyncio.sleep(5)
788
- progress_msg = progress_messages[i % len(progress_messages)]
789
- i += 1
790
- content = "\n" + progress_msg + "\n"
791
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})}\n\n"
792
-
793
- # 获取生成结果
794
- image_urls = await generation_task
795
- logger.info(f"[流式响应Remix {request_id}] 图像生成完成")
796
-
797
- # 本地化图片URL
798
- if Config.IMAGE_LOCALIZATION:
799
- logger.info(f"[流式响应Remix {request_id}] 进行图片本地化处理")
800
- localized_urls = await localize_image_urls(image_urls)
801
- image_urls = localized_urls
802
- logger.info(f"[流式响应Remix {request_id}] 图片本地化处理完成")
803
- else:
804
- logger.info(f"[流式响应Remix {request_id}] 图片本地化功能未启用")
805
-
806
- # 结束代码块
807
- content_str = "\n```\n\n"
808
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
809
-
810
- # 发送图片URL作为Markdown
811
- for i, url in enumerate(image_urls):
812
- if i > 0:
813
- newline_str = "\n\n"
814
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': newline_str}, 'finish_reason': None}]})}\n\n"
815
-
816
- image_markdown = f"![Generated Image]({url})"
817
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': image_markdown}, 'finish_reason': None}]})}\n\n"
818
-
819
- # 发送完成事件
820
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
821
-
822
- # 发送结束标志
823
- yield "data: [DONE]\n\n"
824
-
825
- finally:
826
- # 清理临时文件
827
- if os.path.exists(temp_image_path):
828
- os.remove(temp_image_path)
829
- if os.path.exists(temp_dir):
830
- os.rmdir(temp_dir)
831
-
832
- except Exception as e:
833
- error_msg = f"图像Remix失败: {str(e)}"
834
- logger.error(f"[流式响应Remix {request_id}] 错误: {error_msg}")
835
- if logger.isEnabledFor(logging.DEBUG):
836
- import traceback
837
- logger.debug(traceback.format_exc())
838
- error_content = f"\n{error_msg}\n```"
839
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': error_content}, 'finish_reason': 'error'}]})}\n\n"
840
-
841
- # 结束流
842
- yield "data: [DONE]\n\n"
843
-
844
- # API密钥管理端点
845
- @app.get("/api/keys")
846
- async def get_all_keys(admin_key: str = Depends(verify_admin)):
847
- """获取所有API密钥"""
848
- return key_manager.get_all_keys()
849
-
850
- @app.get("/api/keys/{key_id}")
851
- async def get_key(key_id: str, admin_key: str = Depends(verify_admin)):
852
- """获取单个API密钥详情"""
853
- key = key_manager.get_key_by_id(key_id)
854
- if not key:
855
- raise HTTPException(status_code=404, detail="密钥不存在")
856
- return key
857
-
858
- @app.post("/api/keys")
859
- async def create_key(key_data: ApiKeyCreate, admin_key: str = Depends(verify_admin)):
860
- """创建新API密钥"""
861
- try:
862
- # 确保密钥值包含 Bearer 前缀
863
- key_value = key_data.key_value
864
- if not key_value.startswith("Bearer "):
865
- key_value = f"Bearer {key_value}"
866
-
867
- new_key = key_manager.add_key(
868
- key_value,
869
- name=key_data.name,
870
- weight=key_data.weight,
871
- rate_limit=key_data.rate_limit,
872
- is_enabled=key_data.is_enabled,
873
- notes=key_data.notes
874
- )
875
-
876
- # 通过Config永久保存所有密钥
877
- Config.save_api_keys(key_manager.keys)
878
-
879
- return new_key
880
- except Exception as e:
881
- raise HTTPException(status_code=400, detail=str(e))
882
-
883
- @app.put("/api/keys/{key_id}")
884
- async def update_key(key_id: str, key_data: ApiKeyUpdate, admin_key: str = Depends(verify_admin)):
885
- """更新API密钥信息"""
886
- try:
887
- # 如果提供了新的密钥值,确保包含Bearer前缀
888
- key_value = key_data.key_value
889
- if key_value and not key_value.startswith("Bearer "):
890
- key_value = f"Bearer {key_value}"
891
- key_data.key_value = key_value
892
-
893
- updated_key = key_manager.update_key(
894
- key_id,
895
- key_value=key_data.key_value,
896
- name=key_data.name,
897
- weight=key_data.weight,
898
- rate_limit=key_data.rate_limit,
899
- is_enabled=key_data.is_enabled,
900
- notes=key_data.notes
901
- )
902
- if not updated_key:
903
- raise HTTPException(status_code=404, detail="密钥不存在")
904
-
905
- # 通过Config永久保存所有密钥
906
- Config.save_api_keys(key_manager.keys)
907
-
908
- return updated_key
909
- except Exception as e:
910
- raise HTTPException(status_code=400, detail=str(e))
911
-
912
- @app.delete("/api/keys/{key_id}")
913
- async def delete_key(key_id: str, admin_key: str = Depends(verify_admin)):
914
- """删除API密钥"""
915
- success = key_manager.delete_key(key_id)
916
- if not success:
917
- raise HTTPException(status_code=404, detail="密钥不存在")
918
-
919
- # 通过Config永久保存所有密钥
920
- Config.save_api_keys(key_manager.keys)
921
-
922
- return {"status": "success", "message": "密钥已删除"}
923
-
924
- @app.get("/api/stats")
925
- async def get_usage_stats(admin_key: str = Depends(verify_admin)):
926
- """获取API使用统计"""
927
- return key_manager.get_usage_stats()
928
-
929
- @app.post("/api/keys/test")
930
- async def test_key(key_data: ApiKeyCreate, admin_key: str = Depends(verify_admin)):
931
- """测试API密钥是否有效"""
932
- try:
933
- # 确保密钥值包含 Bearer 前缀
934
- key_value = key_data.key_value
935
- if not key_value.startswith("Bearer "):
936
- key_value = f"Bearer {key_value}"
937
-
938
- # 获取代理配置
939
- proxy_host = Config.PROXY_HOST if Config.PROXY_HOST and Config.PROXY_HOST.strip() else None
940
- proxy_port = Config.PROXY_PORT if Config.PROXY_PORT and Config.PROXY_PORT.strip() else None
941
-
942
- # 创建临时客户端测试连接
943
- test_client = SoraClient(
944
- proxy_host=proxy_host,
945
- proxy_port=proxy_port,
946
- auth_token=key_value
947
- )
948
- # 执行简单API调用测试连接
949
- test_result = await test_client.test_connection()
950
- return {"status": "success", "message": "API密钥测试成功", "details": test_result}
951
- except Exception as e:
952
- return {"status": "error", "message": f"API密钥测试失败: {str(e)}"}
953
-
954
- @app.post("/api/keys/batch")
955
- async def batch_operation(operation: Dict[str, Any], admin_key: str = Depends(verify_admin)):
956
- """批量操作API密钥"""
957
- action = operation.get("action")
958
- key_ids = operation.get("key_ids", [])
959
-
960
- if not action or not key_ids:
961
- raise HTTPException(status_code=400, detail="无效的请求参数")
962
-
963
- # 确保key_ids是一个列表
964
- if isinstance(key_ids, str):
965
- key_ids = [key_ids]
966
-
967
- results = {}
968
-
969
- if action == "enable":
970
- for key_id in key_ids:
971
- success = key_manager.update_key(key_id, is_enabled=True)
972
- results[key_id] = "success" if success else "failed"
973
- elif action == "disable":
974
- for key_id in key_ids:
975
- success = key_manager.update_key(key_id, is_enabled=False)
976
- results[key_id] = "success" if success else "failed"
977
- elif action == "delete":
978
- for key_id in key_ids:
979
- success = key_manager.delete_key(key_id)
980
- results[key_id] = "success" if success else "failed"
981
- else:
982
- raise HTTPException(status_code=400, detail="不支持的操作类型")
983
-
984
- # 通过Config永久保存所有密钥
985
- Config.save_api_keys(key_manager.keys)
986
-
987
- return {"status": "success", "results": results}
988
-
989
- # 健康检查端点
990
- @app.get("/health")
991
- async def health_check():
992
- return {"status": "ok", "timestamp": time.time()}
993
-
994
- # 管理界面路由
995
- @app.get("/admin")
996
- async def admin_panel():
997
- return FileResponse(os.path.join(Config.STATIC_DIR, "admin/index.html"))
998
-
999
- # 管理员密钥API
1000
- @app.get("/admin/key")
1001
- async def admin_key():
1002
- return {"admin_key": Config.ADMIN_KEY}
1003
-
1004
- # 挂载静态文件
1005
- app.mount("/admin", StaticFiles(directory=os.path.join(Config.STATIC_DIR, "admin"), html=True), name="admin")
1006
-
1007
- # 配置管理模型
1008
- class ConfigUpdate(BaseModel):
1009
- IMAGE_LOCALIZATION: Optional[bool] = None
1010
- IMAGE_SAVE_DIR: Optional[str] = None
1011
- LOG_LEVEL: Optional[str] = None
1012
-
1013
- # 配置管理页面
1014
- @app.get("/admin/config")
1015
- async def config_panel():
1016
- return FileResponse("src/static/admin/config.html")
1017
-
1018
- # 获取当前配置
1019
- @app.get("/api/config")
1020
- async def get_config(admin_key: str = Depends(verify_admin)):
1021
- """获取当前系统配置"""
1022
- return {
1023
- "IMAGE_LOCALIZATION": Config.IMAGE_LOCALIZATION,
1024
- "IMAGE_SAVE_DIR": Config.IMAGE_SAVE_DIR,
1025
- "LOG_LEVEL": LogConfig.LEVEL
1026
- }
1027
-
1028
- # 更新配置
1029
- @app.post("/api/config")
1030
- async def update_config(config_data: ConfigUpdate, admin_key: str = Depends(verify_admin)):
1031
- """更新系统配置"""
1032
- changes = {}
1033
-
1034
- if config_data.IMAGE_LOCALIZATION is not None:
1035
- old_value = Config.IMAGE_LOCALIZATION
1036
- Config.IMAGE_LOCALIZATION = config_data.IMAGE_LOCALIZATION
1037
- os.environ["IMAGE_LOCALIZATION"] = str(config_data.IMAGE_LOCALIZATION)
1038
- changes["IMAGE_LOCALIZATION"] = {
1039
- "old": old_value,
1040
- "new": Config.IMAGE_LOCALIZATION
1041
- }
1042
-
1043
- if config_data.IMAGE_SAVE_DIR is not None:
1044
- old_value = Config.IMAGE_SAVE_DIR
1045
- Config.IMAGE_SAVE_DIR = config_data.IMAGE_SAVE_DIR
1046
- os.environ["IMAGE_SAVE_DIR"] = config_data.IMAGE_SAVE_DIR
1047
-
1048
- # 确保目录存在
1049
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
1050
-
1051
- changes["IMAGE_SAVE_DIR"] = {
1052
- "old": old_value,
1053
- "new": Config.IMAGE_SAVE_DIR
1054
- }
1055
-
1056
- if config_data.LOG_LEVEL is not None:
1057
- old_value = LogConfig.LEVEL
1058
- level = config_data.LOG_LEVEL.upper()
1059
- # 验证日志级别是否有效
1060
- valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
1061
- if level not in valid_levels:
1062
- raise HTTPException(status_code=400, detail=f"无效的日志级别,有效值:{', '.join(valid_levels)}")
1063
-
1064
- # 更新日志级别
1065
- LogConfig.LEVEL = level
1066
- logging.getLogger("sora-api").setLevel(getattr(logging, level))
1067
- os.environ["LOG_LEVEL"] = level
1068
-
1069
- changes["LOG_LEVEL"] = {
1070
- "old": old_value,
1071
- "new": level
1072
- }
1073
-
1074
- logger.info(f"日志级别已更改为: {level}")
1075
-
1076
- # 保存到.env文件以持久化配置
1077
- try:
1078
- dotenv_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
1079
-
1080
- # 读取现有.env文件
1081
- env_vars = {}
1082
- if os.path.exists(dotenv_file):
1083
- with open(dotenv_file, "r") as f:
1084
- for line in f:
1085
- if line.strip() and not line.startswith("#"):
1086
- key, value = line.strip().split("=", 1)
1087
- env_vars[key] = value
1088
-
1089
- # 更新值
1090
- if config_data.IMAGE_LOCALIZATION is not None:
1091
- env_vars["IMAGE_LOCALIZATION"] = str(config_data.IMAGE_LOCALIZATION)
1092
- if config_data.IMAGE_SAVE_DIR is not None:
1093
- env_vars["IMAGE_SAVE_DIR"] = config_data.IMAGE_SAVE_DIR
1094
- if config_data.LOG_LEVEL is not None:
1095
- env_vars["LOG_LEVEL"] = config_data.LOG_LEVEL.upper()
1096
-
1097
- # 写回文件
1098
- with open(dotenv_file, "w") as f:
1099
- for key, value in env_vars.items():
1100
- f.write(f"{key}={value}\n")
1101
- except Exception as e:
1102
- logger.error(f"保存配置到.env文件失败: {str(e)}")
1103
-
1104
- return {
1105
- "success": True,
1106
- "message": "配置已更新",
1107
- "changes": changes
1108
- }
1109
-
1110
- # 日志级别控制
1111
- class LogLevelUpdate(BaseModel):
1112
- level: str = Field(..., description="日志级别")
1113
-
1114
- @app.post("/api/logs/level")
1115
- async def update_log_level(data: LogLevelUpdate, admin_key: str = Depends(verify_admin)):
1116
- """更新系统日志级别"""
1117
- level = data.level.upper()
1118
-
1119
- # 验证日志级别是否有效
1120
- valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
1121
- if level not in valid_levels:
1122
- raise HTTPException(status_code=400, detail=f"无效的日志级别,有效值:{', '.join(valid_levels)}")
1123
-
1124
- # 更新日志级别
1125
- old_level = LogConfig.LEVEL
1126
- LogConfig.LEVEL = level
1127
- logging.getLogger("sora-api").setLevel(getattr(logging, level))
1128
- os.environ["LOG_LEVEL"] = level
1129
-
1130
- # 记录变更
1131
- logger.info(f"日志级别已更改为: {level}")
1132
-
1133
- # 更新.env文件
1134
- try:
1135
- dotenv_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
1136
-
1137
- # 读取现有.env文件
1138
- env_vars = {}
1139
- if os.path.exists(dotenv_file):
1140
- with open(dotenv_file, "r") as f:
1141
- for line in f:
1142
- if line.strip() and not line.startswith("#"):
1143
- key, value = line.strip().split("=", 1)
1144
- env_vars[key] = value
1145
-
1146
- # 更新日志级别
1147
- env_vars["LOG_LEVEL"] = level
1148
-
1149
- # 写回文件
1150
- with open(dotenv_file, "w") as f:
1151
- for key, value in env_vars.items():
1152
- f.write(f"{key}={value}\n")
1153
- except Exception as e:
1154
- logger.warning(f"保存日志级别到.env文件失败: {str(e)}")
1155
-
1156
- return {
1157
- "success": True,
1158
- "message": f"日志级别已更改: {old_level} -> {level}"
1159
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/app.py.optimized DELETED
@@ -1,129 +0,0 @@
1
- import os
2
- import aiohttp
3
- import logging
4
- from fastapi import FastAPI, Request, HTTPException
5
- from fastapi.responses import JSONResponse, FileResponse
6
- from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.staticfiles import StaticFiles
8
- from fastapi.exceptions import RequestValidationError
9
-
10
- from .config import Config
11
- from .key_manager import KeyManager
12
- from .api import main_router
13
- from .api.dependencies import session_pool
14
-
15
- # 配置日志
16
- logging.basicConfig(
17
- level=getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper()),
18
- format="%(asctime)s [%(levelname)s] %(message)s",
19
- datefmt="%Y-%m-%d %H:%M:%S"
20
- )
21
-
22
- logger = logging.getLogger("sora-api")
23
-
24
- # 全局键管理器
25
- key_manager = KeyManager(storage_file=Config.KEYS_STORAGE_FILE)
26
-
27
- # 创建FastAPI应用
28
- app = FastAPI(
29
- title="OpenAI Compatible Sora API",
30
- description="为Sora提供OpenAI兼容接口的API服务",
31
- version="2.0.0",
32
- docs_url="/docs",
33
- redoc_url="/redoc",
34
- )
35
-
36
- # 配置CORS中间件
37
- origins = [
38
- "http://localhost",
39
- "http://localhost:8890",
40
- f"http://{Config.HOST}:{Config.PORT}",
41
- Config.BASE_URL,
42
- ]
43
-
44
- app.add_middleware(
45
- CORSMiddleware,
46
- allow_origins=origins,
47
- allow_credentials=True,
48
- allow_methods=["GET", "POST", "PUT", "DELETE"],
49
- allow_headers=["Authorization", "Content-Type"],
50
- )
51
-
52
- # 异常处理
53
- @app.exception_handler(RequestValidationError)
54
- async def validation_exception_handler(request: Request, exc: RequestValidationError):
55
- """处理请求验证错误"""
56
- return JSONResponse(
57
- status_code=422,
58
- content={"detail": f"请求验证错误: {str(exc)}"}
59
- )
60
-
61
- @app.exception_handler(Exception)
62
- async def global_exception_handler(request: Request, exc: Exception):
63
- """全局异常处理器"""
64
- # 记录错误
65
- logger.error(f"全局异常: {str(exc)}", exc_info=True)
66
-
67
- # 如果是已知的HTTPException,保持原始状态码和详情
68
- if isinstance(exc, HTTPException):
69
- return JSONResponse(
70
- status_code=exc.status_code,
71
- content={"detail": exc.detail}
72
- )
73
-
74
- # 其他异常返回500状态码
75
- return JSONResponse(
76
- status_code=500,
77
- content={"detail": f"服务器内部错误: {str(exc)}"}
78
- )
79
-
80
- # 应用启动和关闭事件
81
- @app.on_event("startup")
82
- async def startup_event():
83
- """应用启动时执行的操作"""
84
- global session_pool
85
- # 创建共享会话池
86
- session_pool = aiohttp.ClientSession()
87
- logger.info("应用已启动,创建了全局会话池")
88
-
89
- # 初始化时保存管理员密钥
90
- Config.save_admin_key()
91
-
92
- # 确保静态文件目录存在
93
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin"), exist_ok=True)
94
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/js"), exist_ok=True)
95
- os.makedirs(os.path.join(Config.STATIC_DIR, "admin/css"), exist_ok=True)
96
- os.makedirs(os.path.join(Config.STATIC_DIR, "images"), exist_ok=True)
97
-
98
- # 打印配置信息
99
- Config.print_config()
100
-
101
- @app.on_event("shutdown")
102
- async def shutdown_event():
103
- """应用关闭时执行的操作"""
104
- # 关闭会话池
105
- if session_pool:
106
- await session_pool.close()
107
- logger.info("应用已关闭,清理了全局会话池")
108
-
109
- # 挂载静态文件目录
110
- app.mount("/static", StaticFiles(directory=Config.STATIC_DIR), name="static")
111
-
112
- # 管理界面路由
113
- @app.get("/admin")
114
- async def admin_panel():
115
- """返回管理面板HTML页面"""
116
- return FileResponse(os.path.join(Config.STATIC_DIR, "admin/index.html"))
117
-
118
- # 注册所有API路由
119
- app.include_router(main_router)
120
-
121
- # 应用入口点(供uvicorn直接调用)
122
- if __name__ == "__main__":
123
- import uvicorn
124
- uvicorn.run(
125
- "app:app",
126
- host=Config.HOST,
127
- port=Config.PORT,
128
- reload=Config.VERBOSE_LOGGING
129
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/config.py DELETED
@@ -1,179 +0,0 @@
1
- import os
2
- import json
3
- import uuid
4
- from typing import List, Dict
5
- import logging
6
-
7
- logger = logging.getLogger("sora-api.config")
8
-
9
- class Config:
10
- # API服务配置
11
- HOST = os.getenv("API_HOST", "0.0.0.0")
12
- PORT = int(os.getenv("API_PORT", "8890"))
13
-
14
- # 基础URL配置
15
- BASE_URL = os.getenv("BASE_URL", f"http://0.0.0.0:{PORT}")
16
-
17
- # 静态文件路径前缀,用于处理应用部署在子路径的情况
18
- # 例如: /sora-api 表示应用部署在 /sora-api 下
19
- STATIC_PATH_PREFIX = os.getenv("STATIC_PATH_PREFIX", "")
20
-
21
- # 代理配置
22
- PROXY_HOST = os.getenv("PROXY_HOST", "")
23
- PROXY_PORT = os.getenv("PROXY_PORT", "")
24
- PROXY_USER = os.getenv("PROXY_USER", "")
25
- PROXY_PASS = os.getenv("PROXY_PASS", "")
26
-
27
- # 目录配置
28
- ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
29
- BASE_DIR = ROOT_DIR
30
- STATIC_DIR = os.getenv("STATIC_DIR", os.path.join(BASE_DIR, "src/static"))
31
-
32
- # 图片保存目录 - 用户只需设置这一项
33
- IMAGE_SAVE_DIR = os.getenv("IMAGE_SAVE_DIR", os.path.join(STATIC_DIR, "images"))
34
-
35
- # 图片本地化配置
36
- IMAGE_LOCALIZATION = os.getenv("IMAGE_LOCALIZATION", "False").lower() in ("true", "1", "yes")
37
-
38
- # 当外部访问地址与服务器地址不同时,可通过BASE_URL覆盖图片访问地址
39
- # 例如:当服务器在内网,但通过反向代理从外网访问时
40
-
41
- # API Keys配置
42
- API_KEYS = []
43
-
44
- # 管理员配置
45
- ADMIN_KEY = os.getenv("ADMIN_KEY", "sk-123456")
46
- KEYS_STORAGE_FILE = os.getenv("KEYS_STORAGE_FILE", "api_keys.json")
47
-
48
- # API认证令牌
49
- API_AUTH_TOKEN = os.getenv("API_AUTH_TOKEN", "")
50
-
51
- # 日志配置
52
- VERBOSE_LOGGING = os.getenv("VERBOSE_LOGGING", "False").lower() in ("true", "1", "yes")
53
-
54
- @classmethod
55
- def print_config(cls):
56
- """打印当前配置信息"""
57
- print("\n==== Sora API 配置信息 ====")
58
- print(f"基础目录: {cls.BASE_DIR}")
59
- print(f"API服务地址: {cls.HOST}:{cls.PORT}")
60
- print(f"基础URL: {cls.BASE_URL}")
61
-
62
- # API认证信息
63
- if cls.API_AUTH_TOKEN:
64
- print(f"API认证令牌: 已设置 (长度: {len(cls.API_AUTH_TOKEN)})")
65
- else:
66
- print(f"API认证令牌: 未设置 (将使用管理面板的key)")
67
-
68
- # 详细日志
69
- if cls.VERBOSE_LOGGING:
70
- print(f"静态文件目录: {cls.STATIC_DIR}")
71
- print(f"图片保存目录: {cls.IMAGE_SAVE_DIR}")
72
- print(f"图片本地化: {'启用' if cls.IMAGE_LOCALIZATION else '禁用'}")
73
-
74
- # 代理配置信息
75
- if cls.PROXY_HOST:
76
- proxy_info = f"{cls.PROXY_HOST}:{cls.PROXY_PORT}"
77
- if cls.PROXY_USER:
78
- proxy_info = f"{cls.PROXY_USER}:****@{proxy_info}"
79
- print(f"代理配置: {proxy_info}")
80
- else:
81
- print(f"代理配置: (未配置)")
82
-
83
- # 确保必要目录存在
84
- cls._ensure_directories()
85
-
86
- @classmethod
87
- def _ensure_directories(cls):
88
- """确保必要的目录存在"""
89
- # 检查静态文件目录
90
- if not os.path.exists(cls.STATIC_DIR):
91
- print(f"⚠️ 警告: 静态文件目录不存在: {cls.STATIC_DIR}")
92
- elif cls.VERBOSE_LOGGING:
93
- print(f"✅ 静态文件目录存在")
94
-
95
- # 检查并创建图片保存目录
96
- if not os.path.exists(cls.IMAGE_SAVE_DIR):
97
- try:
98
- os.makedirs(cls.IMAGE_SAVE_DIR, exist_ok=True)
99
- if cls.VERBOSE_LOGGING:
100
- print(f"✅ 已创建图片保存目录: {cls.IMAGE_SAVE_DIR}")
101
- except Exception as e:
102
- print(f"❌ 创建图片保存目录失败: {str(e)}")
103
- elif cls.VERBOSE_LOGGING:
104
- print(f"✅ 图片保存目录存在")
105
-
106
- # 测试写入权限
107
- try:
108
- test_file = os.path.join(cls.IMAGE_SAVE_DIR, '.test_write')
109
- with open(test_file, 'w') as f:
110
- f.write('test')
111
- os.remove(test_file)
112
- print(f"✅ 图片保存目录有写入权限")
113
- except Exception as e:
114
- print(f"❌ 图片保存目录没有写入权限: {str(e)}")
115
-
116
- @classmethod
117
- def load_api_keys(cls):
118
- """加载API密钥"""
119
- # 先从环境变量加载
120
- api_keys_str = os.getenv("API_KEYS", "")
121
- if api_keys_str:
122
- try:
123
- cls.API_KEYS = json.loads(api_keys_str)
124
- if cls.VERBOSE_LOGGING:
125
- logger.info(f"已从环境变量加载 {len(cls.API_KEYS)} 个API keys")
126
- return
127
- except json.JSONDecodeError as e:
128
- logger.error(f"解析环境变量API keys失败: {e}")
129
-
130
- # 再从文件加载
131
- try:
132
- if os.path.exists(cls.KEYS_STORAGE_FILE):
133
- with open(cls.KEYS_STORAGE_FILE, "r", encoding="utf-8") as f:
134
- keys_data = json.load(f)
135
- if isinstance(keys_data, dict) and "keys" in keys_data:
136
- cls.API_KEYS = [k for k in keys_data["keys"] if k.get("key")]
137
- else:
138
- cls.API_KEYS = keys_data
139
-
140
- if cls.VERBOSE_LOGGING:
141
- logger.info(f"已从文件加载 {len(cls.API_KEYS)} 个API keys")
142
- except Exception as e:
143
- logger.error(f"从文件加载API keys失败: {e}")
144
-
145
- @classmethod
146
- def save_api_keys(cls, keys_data):
147
- """保存API密钥到文件"""
148
- try:
149
- keys_storage_file = os.path.join(cls.BASE_DIR, cls.KEYS_STORAGE_FILE)
150
-
151
- with open(keys_storage_file, "w", encoding="utf-8") as f:
152
- if isinstance(keys_data, list):
153
- json.dump({"keys": keys_data}, f, ensure_ascii=False, indent=2)
154
- else:
155
- json.dump(keys_data, f, ensure_ascii=False, indent=2)
156
-
157
- # 更新内存中的keys
158
- if isinstance(keys_data, dict) and "keys" in keys_data:
159
- cls.API_KEYS = [k for k in keys_data["keys"] if k.get("key")]
160
- else:
161
- cls.API_KEYS = keys_data
162
-
163
- if cls.VERBOSE_LOGGING:
164
- logger.info(f"API keys已保存至 {keys_storage_file}")
165
- except Exception as e:
166
- logger.error(f"保存API keys失败: {e}")
167
-
168
- @classmethod
169
- def save_admin_key(cls):
170
- """保存管理员密钥到文件"""
171
- try:
172
- admin_config_file = os.path.join(cls.BASE_DIR, "admin_config.json")
173
- with open(admin_config_file, "w", encoding="utf-8") as f:
174
- json.dump({"admin_key": cls.ADMIN_KEY}, f, indent=2)
175
-
176
- if cls.VERBOSE_LOGGING:
177
- logger.info(f"管理员密钥已保存至 {admin_config_file}")
178
- except Exception as e:
179
- logger.error(f"保存管理员密钥失败: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/key_manager.py DELETED
@@ -1,777 +0,0 @@
1
- import time
2
- import random
3
- import uuid
4
- import json
5
- import os
6
- import logging
7
- import threading
8
- from typing import Dict, List, Optional, Any, Union, Tuple, Callable
9
-
10
- # 初始化日志
11
- logger = logging.getLogger("sora-api.key_manager")
12
-
13
- class KeyManager:
14
- def __init__(self, storage_file: str = "api_keys.json"):
15
- """
16
- 初始化密钥管理器
17
-
18
- Args:
19
- storage_file: 密钥存储文件路径
20
- """
21
- self.keys = [] # 密钥列表
22
- self.storage_file = storage_file
23
- self.usage_stats = {} # 使用统计
24
- self._lock = threading.RLock() # 添加可重入锁以支持并发访问
25
- self._working_keys = {} # 新增:记录正在工作中的密钥 {key_value: task_id}
26
- self._load_keys()
27
-
28
- def _load_keys(self) -> None:
29
- """从环境变量或文件加载密钥"""
30
- keys_loaded = False
31
-
32
- # 先尝试从环境变量加载
33
- api_keys_str = os.getenv("API_KEYS", "")
34
- if api_keys_str:
35
- try:
36
- env_data = json.loads(api_keys_str)
37
- self._process_keys_data(env_data)
38
- if len(self.keys) > 0:
39
- logger.info(f"已从环境变量加载 {len(self.keys)} 个密钥")
40
- keys_loaded = True
41
- else:
42
- logger.warning("环境变量API_KEYS存在但未包含有效密钥")
43
- except json.JSONDecodeError as e:
44
- logger.error(f"解析环境变量API keys失败: {str(e)}")
45
-
46
- # 如果环境变量未设置、解析失败或未加载到密钥,从文件加载
47
- if not keys_loaded:
48
- try:
49
- if os.path.exists(self.storage_file):
50
- logger.info(f"尝试从文件加载密钥: {self.storage_file}")
51
- with open(self.storage_file, 'r', encoding='utf-8') as f:
52
- data = json.load(f)
53
- keys_before = len(self.keys)
54
- self._process_keys_data(data)
55
- keys_loaded = len(self.keys) > keys_before
56
- logger.info(f"已从文件加载 {len(self.keys) - keys_before} 个密钥")
57
- else:
58
- logger.warning(f"密钥文件不存在: {self.storage_file}")
59
- except Exception as e:
60
- logger.error(f"加载密钥失败: {str(e)}")
61
-
62
- if len(self.keys) == 0:
63
- logger.warning("未能从环境变量或文件加载任何密钥")
64
-
65
-
66
- def _process_keys_data(self, data):
67
- """处理不同格式的密钥数据"""
68
- # 处理不同的数据格式
69
- if isinstance(data, list):
70
- # 旧版格式:直接是密钥列表
71
- raw_keys = data
72
- self.keys = []
73
- self.usage_stats = {}
74
-
75
- # 为每个密钥创建完整的记录
76
- for key_info in raw_keys:
77
- if isinstance(key_info, dict):
78
- key_value = key_info.get("key")
79
- if not key_value:
80
- logger.warning(f"忽略无效密钥配置: {key_info}")
81
- continue
82
-
83
- # 确保有ID
84
- key_id = key_info.get("id") or str(uuid.uuid4())
85
-
86
- # 构建完整的密钥记录
87
- key_record = {
88
- "id": key_id,
89
- "name": key_info.get("name", ""),
90
- "key": key_value,
91
- "weight": key_info.get("weight", 1),
92
- "max_rpm": key_info.get("max_rpm", 60),
93
- "requests": 0,
94
- "last_reset": time.time(),
95
- "available": key_info.get("is_enabled", True),
96
- "is_enabled": key_info.get("is_enabled", True),
97
- "created_at": key_info.get("created_at", time.time()),
98
- "last_used": key_info.get("last_used"),
99
- "notes": key_info.get("notes")
100
- }
101
-
102
- self.keys.append(key_record)
103
-
104
- # 初始化使用统计
105
- self.usage_stats[key_id] = {
106
- "total_requests": 0,
107
- "successful_requests": 0,
108
- "failed_requests": 0,
109
- "daily_usage": {},
110
- "average_response_time": 0
111
- }
112
- elif isinstance(key_info, str):
113
- # 如果是字符串,直接作为密钥值
114
- key_id = str(uuid.uuid4())
115
- self.keys.append({
116
- "id": key_id,
117
- "name": "",
118
- "key": key_info,
119
- "weight": 1,
120
- "max_rpm": 60,
121
- "requests": 0,
122
- "last_reset": time.time(),
123
- "available": True,
124
- "is_enabled": True,
125
- "created_at": time.time(),
126
- "last_used": None,
127
- "notes": None
128
- })
129
-
130
- # 初始化使用统计
131
- self.usage_stats[key_id] = {
132
- "total_requests": 0,
133
- "successful_requests": 0,
134
- "failed_requests": 0,
135
- "daily_usage": {},
136
- "average_response_time": 0
137
- }
138
- else:
139
- # 新版格式:包含keys和usage_stats的字典
140
- self.keys = data.get('keys', [])
141
- self.usage_stats = data.get('usage_stats', {})
142
-
143
- def _save_keys(self) -> None:
144
- """保存密钥到文件"""
145
- try:
146
- with open(self.storage_file, 'w', encoding='utf-8') as f:
147
- json.dump({
148
- 'keys': self.keys,
149
- 'usage_stats': self.usage_stats
150
- }, f, ensure_ascii=False, indent=2)
151
-
152
- # 同时更新Config中的API_KEYS
153
- try:
154
- from .config import Config
155
- Config.API_KEYS = self.keys
156
- except (ImportError, AttributeError):
157
- logger.debug("无法更新Config中的API_KEYS")
158
- except Exception as e:
159
- logger.error(f"保存密钥失败: {str(e)}")
160
-
161
- def add_key(self, key_value: str, name: str = "", weight: int = 1,
162
- rate_limit: int = 60, is_enabled: bool = True, notes: str = None) -> Dict[str, Any]:
163
- """
164
- 添加密钥
165
-
166
- Args:
167
- key_value: 密钥值
168
- name: 密钥名称
169
- weight: 权重
170
- rate_limit: 速率限制(每分钟请求数)
171
- is_enabled: 是否启用
172
- notes: 备注
173
-
174
- Returns:
175
- 添加的密钥信息
176
- """
177
- with self._lock: # 使用锁保护添加过程
178
- # 检查密钥是否已存在
179
- for key in self.keys:
180
- if key.get("key") == key_value:
181
- return key
182
-
183
- key_id = str(uuid.uuid4())
184
- new_key = {
185
- "id": key_id,
186
- "name": name,
187
- "key": key_value,
188
- "weight": weight,
189
- "max_rpm": rate_limit,
190
- "requests": 0,
191
- "last_reset": time.time(),
192
- "available": is_enabled,
193
- "is_enabled": is_enabled,
194
- "created_at": time.time(),
195
- "last_used": None,
196
- "notes": notes
197
- }
198
- self.keys.append(new_key)
199
-
200
- # 初始化使用统计
201
- self.usage_stats[key_id] = {
202
- "total_requests": 0,
203
- "successful_requests": 0,
204
- "failed_requests": 0,
205
- "daily_usage": {},
206
- "average_response_time": 0
207
- }
208
-
209
- self._save_keys()
210
- logger.info(f"已添加密钥: {name or key_id}")
211
- return new_key
212
-
213
- def get_all_keys(self) -> List[Dict[str, Any]]:
214
- """获取所有密钥信息(已隐藏完整密钥值)"""
215
- with self._lock: # 使用锁保护读取过程
216
- result = []
217
- for key in self.keys:
218
- key_copy = key.copy()
219
- if "key" in key_copy:
220
- # 只显示密钥前6位和后4位
221
- full_key = key_copy["key"]
222
- if len(full_key) > 10:
223
- key_copy["key"] = full_key[:6] + "..." + full_key[-4:]
224
-
225
- # 增加临时禁用信息的处理
226
- if key_copy.get("temp_disabled_until"):
227
- temp_disabled_until = key_copy["temp_disabled_until"]
228
- # 确保temp_disabled_until是时间戳格式
229
- if isinstance(temp_disabled_until, (int, float)):
230
- # 转换为可读格式,但保留原始时间戳,让前端可以自行处理
231
- disabled_until_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(temp_disabled_until))
232
- key_copy["temp_disabled_until_formatted"] = disabled_until_date
233
- key_copy["temp_disabled_remaining"] = int(temp_disabled_until - time.time())
234
-
235
- result.append(key_copy)
236
- return result
237
-
238
- def get_key_by_id(self, key_id: str) -> Optional[Dict[str, Any]]:
239
- """根据ID获取密钥信息"""
240
- with self._lock: # 使用锁保护读取过程
241
- for key in self.keys:
242
- if key.get("id") == key_id:
243
- return key
244
- return None
245
-
246
- def update_key(self, key_id: str, **kwargs) -> Optional[Dict[str, Any]]:
247
- """
248
- 更新密钥信息
249
-
250
- Args:
251
- key_id: 密钥ID
252
- **kwargs: 要更新的字段
253
-
254
- Returns:
255
- 更新后的密钥信息,未找到则返回None
256
- """
257
- with self._lock: # 使用锁保护更新过程
258
- for key in self.keys:
259
- if key.get("id") == key_id:
260
- # 更新提供的字段
261
- for field, value in kwargs.items():
262
- if value is not None:
263
- if field == "is_enabled":
264
- key["available"] = value # 同步更新available字段
265
- key[field] = value
266
-
267
- self._save_keys()
268
- logger.info(f"已更新密钥: {key.get('name') or key_id}")
269
- return key
270
-
271
- logger.warning(f"未找到密钥: {key_id}")
272
- return None
273
-
274
- def delete_key(self, key_id: str) -> bool:
275
- """
276
- 删除密钥
277
-
278
- Args:
279
- key_id: 密钥ID
280
-
281
- Returns:
282
- 是否成功删除
283
- """
284
- with self._lock: # 使用锁保护删除过程
285
- original_length = len(self.keys)
286
- self.keys = [key for key in self.keys if key.get("id") != key_id]
287
-
288
- # 如果成功删除,保存密钥
289
- if len(self.keys) < original_length:
290
- self._save_keys()
291
- return True
292
-
293
- return False
294
-
295
- def batch_import_keys(self, keys_data: List[Dict[str, Any]]) -> Dict[str, int]:
296
- """
297
- 批量导入密钥
298
-
299
- Args:
300
- keys_data: 密钥数据列表,每个元素为包含密钥信息的字典
301
-
302
- Returns:
303
- 导入结果统计
304
- """
305
- with self._lock: # 使用锁保护导入过程
306
- imported_count = 0
307
- skipped_count = 0
308
-
309
- # 获取现有密钥值
310
- existing_keys = {key.get("key") for key in self.keys}
311
-
312
- for key_data in keys_data:
313
- key_value = key_data.get("key")
314
- if not key_value:
315
- continue
316
-
317
- # 检查密钥是否已存在
318
- if key_value in existing_keys:
319
- skipped_count += 1
320
- continue
321
-
322
- # 添加新密钥
323
- key_id = str(uuid.uuid4())
324
- new_key = {
325
- "id": key_id,
326
- "name": key_data.get("name", ""),
327
- "key": key_value,
328
- "weight": key_data.get("weight", 1),
329
- "max_rpm": key_data.get("rate_limit", 60),
330
- "requests": 0,
331
- "last_reset": time.time(),
332
- "available": key_data.get("enabled", True),
333
- "is_enabled": key_data.get("enabled", True),
334
- "created_at": time.time(),
335
- "last_used": None,
336
- "notes": key_data.get("notes")
337
- }
338
- self.keys.append(new_key)
339
- existing_keys.add(key_value) # 添加到已存在集合中
340
-
341
- # 初始化使用统计
342
- self.usage_stats[key_id] = {
343
- "total_requests": 0,
344
- "successful_requests": 0,
345
- "failed_requests": 0,
346
- "daily_usage": {},
347
- "average_response_time": 0
348
- }
349
-
350
- imported_count += 1
351
-
352
- # 保存密钥
353
- if imported_count > 0:
354
- self._save_keys()
355
-
356
- return {
357
- "imported": imported_count,
358
- "skipped": skipped_count
359
- }
360
-
361
- def get_key(self) -> Optional[str]:
362
- """获取下一个可用的密钥"""
363
- with self._lock: # 使用锁保护整个获取密钥过程
364
- if not self.keys:
365
- logger.warning("没有可用的密钥")
366
- return None
367
-
368
- # 重置计数器(如果需要)
369
- current_time = time.time()
370
- temporary_disabled_updated = False
371
-
372
- for key in self.keys:
373
- # 检查是否有被临时禁用的密钥需要重新启用
374
- if key.get("temp_disabled_until") and current_time > key.get("temp_disabled_until"):
375
- key["is_enabled"] = True
376
- key["available"] = True
377
- key["temp_disabled_until"] = None
378
- temporary_disabled_updated = True
379
- logger.info(f"密钥 {key.get('name') or key.get('id')} 的临时禁用已��除")
380
-
381
- if current_time - key["last_reset"] >= 60:
382
- key["requests"] = 0
383
- key["last_reset"] = current_time
384
- if not key.get("temp_disabled_until"): # 只有未被临时禁用的密钥才会被重新激活
385
- key["available"] = key.get("is_enabled", True)
386
-
387
- # 如果有任何临时禁用的密钥被更新,保存变更
388
- if temporary_disabled_updated:
389
- self._save_keys()
390
-
391
- # 筛选可用的密钥,排除工作中的密钥
392
- available_keys = []
393
- for k in self.keys:
394
- key_value = k.get("key", "")
395
- clean_key = key_value.replace("Bearer ", "") if key_value.startswith("Bearer ") else key_value
396
-
397
- # 检查此密钥是否在工作中
398
- is_working = clean_key in self._working_keys
399
-
400
- if k.get("available", False) and not is_working:
401
- available_keys.append(k)
402
-
403
- if not available_keys:
404
- logger.warning("没有可用的密钥(所有密钥都达到速率限制、被禁用或正在工作中)")
405
- return None
406
-
407
- # 根据权重选择密钥
408
- weights = [k.get("weight", 1) for k in available_keys]
409
- selected_idx = random.choices(range(len(available_keys)), weights=weights, k=1)[0]
410
- selected_key = available_keys[selected_idx]
411
-
412
- # 更新使用统计
413
- selected_key["requests"] += 1
414
- selected_key["last_used"] = current_time
415
-
416
- # 检查是否达到速率限制
417
- if selected_key["requests"] >= selected_key.get("max_rpm", 60):
418
- selected_key["available"] = False
419
-
420
- # 保存数据 - 并发环境下调整为每次都保存,避免状态不一致
421
- # 原来是随机保存(10%的概率)
422
- self._save_keys()
423
-
424
- # 确保返回的密钥包含"Bearer "前缀
425
- key_value = selected_key["key"]
426
- if not key_value.startswith("Bearer "):
427
- key_value = f"Bearer {key_value}"
428
-
429
- return key_value
430
-
431
- def record_request_result(self, key: str, success: bool, response_time: float = 0) -> None:
432
- """
433
- 记录请求结果
434
-
435
- Args:
436
- key: 密钥值
437
- success: 请求是否成功
438
- response_time: 响应时间(秒)
439
- """
440
- if not key:
441
- logger.warning("记录请求结果失败:密钥为空")
442
- return
443
-
444
- with self._lock: # 使用锁保护记录过程
445
- # 去掉可能的Bearer前缀
446
- key_for_search = key.replace("Bearer ", "") if key.startswith("Bearer ") else key
447
-
448
- # 查找对应的密钥ID
449
- key_id = None
450
- key_info = None
451
- for k in self.keys:
452
- stored_key = k.get("key", "").replace("Bearer ", "") if k.get("key", "").startswith("Bearer ") else k.get("key", "")
453
- if stored_key == key_for_search:
454
- key_id = k.get("id")
455
- key_info = k
456
- break
457
-
458
- if not key_id:
459
- logger.warning(f"记录请求结果失败:未找到密钥 {key_for_search[:6]}...")
460
- return
461
-
462
- # 初始化usage_stats如果该密钥还没有统计数据
463
- if key_id not in self.usage_stats:
464
- self.usage_stats[key_id] = {
465
- "total_requests": 0,
466
- "successful_requests": 0,
467
- "failed_requests": 0,
468
- "daily_usage": {},
469
- "average_response_time": 0
470
- }
471
-
472
- # 记录请求结果
473
- stats = self.usage_stats[key_id]
474
- stats["total_requests"] += 1
475
-
476
- if success:
477
- stats["successful_requests"] += 1
478
- else:
479
- stats["failed_requests"] += 1
480
-
481
- # 记录响应时间
482
- if response_time > 0:
483
- if stats["average_response_time"] == 0:
484
- stats["average_response_time"] = response_time
485
- else:
486
- # 使用加权平均
487
- old_avg = stats["average_response_time"]
488
- total = stats["total_requests"]
489
- # 避免 total 为0或1时产生问题,尽管前面 total_requests 已经增加了
490
- if total > 0:
491
- stats["average_response_time"] = ((old_avg * (total - 1)) + response_time) / total
492
- else: # 理论上不应该发生,因为 total_requests 已经增加了
493
- stats["average_response_time"] = response_time
494
-
495
- # 记录每日使用情况
496
- today = time.strftime("%Y-%m-%d")
497
- if today not in stats["daily_usage"]:
498
- stats["daily_usage"][today] = {"successful": 0, "failed": 0} # 初始化每日统计
499
-
500
- # 根据成功与否更新每日统计
501
- if success:
502
- stats["daily_usage"][today]["successful"] += 1
503
- else:
504
- stats["daily_usage"][today]["failed"] += 1
505
-
506
- # 保留最近30天的数据
507
- if len(stats["daily_usage"]) > 30:
508
- # 获取所有日期并排序,然后删除最早的
509
- sorted_dates = sorted(stats["daily_usage"].keys())
510
- if sorted_dates: # 确保列表不为空
511
- oldest_date = sorted_dates[0]
512
- del stats["daily_usage"][oldest_date]
513
-
514
- # 更新密钥的最后使用时间
515
- if key_info and "last_used" in key_info:
516
- key_info["last_used"] = time.time()
517
-
518
- # 并发环境下每次都保存,确保统计准确性
519
- self._save_keys()
520
-
521
- def get_usage_stats(self) -> Dict[str, Any]:
522
- """获取使用统计信息"""
523
- with self._lock: # 使用锁保护读取过程
524
- total_keys = len(self.keys)
525
- active_keys = sum(1 for k in self.keys if k.get("is_enabled", False))
526
- available_keys = sum(1 for k in self.keys if k.get("available", False))
527
-
528
- total_requests = sum(stats.get("total_requests", 0) for stats in self.usage_stats.values())
529
- successful_requests = sum(stats.get("successful_requests", 0) for stats in self.usage_stats.values())
530
-
531
- # 计算成功率
532
- success_rate = (successful_requests / total_requests * 100) if total_requests > 0 else 0
533
-
534
- # 计算每个密钥的平均响应时间
535
- avg_response_times = [stats.get("average_response_time", 0) for stats in self.usage_stats.values() if stats.get("average_response_time", 0) > 0]
536
- overall_avg_response_time = sum(avg_response_times) / len(avg_response_times) if avg_response_times else 0
537
-
538
- # 获取过去7天的使用情况
539
- past_7_days = {}
540
- for key_id, stats in self.usage_stats.items():
541
- daily_usage = stats.get("daily_usage", {})
542
- for date, count_data in daily_usage.items():
543
- if date not in past_7_days:
544
- past_7_days[date] = {"successful": 0, "failed": 0}
545
- # 正确处理字典类型的count_data
546
- past_7_days[date]["successful"] += count_data.get("successful", 0)
547
- past_7_days[date]["failed"] += count_data.get("failed", 0)
548
-
549
- # 只保留最近7天
550
- dates = sorted(past_7_days.keys(), reverse=True)[:7]
551
- past_7_days = {date: past_7_days[date] for date in dates}
552
-
553
- return {
554
- "total_keys": total_keys,
555
- "active_keys": active_keys,
556
- "available_keys": available_keys,
557
- "total_requests": total_requests,
558
- "successful_requests": successful_requests,
559
- "failed_requests": total_requests - successful_requests,
560
- "success_rate": success_rate,
561
- "average_response_time": overall_avg_response_time,
562
- "past_7_days": past_7_days
563
- }
564
-
565
- def mark_key_as_working(self, key: str, task_id: str) -> None:
566
- """
567
- 将密钥标记为工作中状态
568
-
569
- Args:
570
- key: API密钥值(可能包含Bearer前缀)
571
- task_id: 关联的任务ID
572
- """
573
- with self._lock:
574
- clean_key = key.replace("Bearer ", "") if key.startswith("Bearer ") else key
575
- self._working_keys[clean_key] = task_id
576
- logger.debug(f"密钥已标记为工作中,关联任务ID: {task_id}")
577
-
578
- def release_key(self, key: str) -> None:
579
- """
580
- 释放工作中的密钥
581
-
582
- Args:
583
- key: API密钥值(可能包含Bearer前缀)
584
- """
585
- with self._lock:
586
- clean_key = key.replace("Bearer ", "") if key.startswith("Bearer ") else key
587
- if clean_key in self._working_keys:
588
- del self._working_keys[clean_key]
589
- logger.debug(f"密钥已释放")
590
-
591
- def is_key_working(self, key: str) -> bool:
592
- """
593
- 检查密钥是否正在工作中
594
-
595
- Args:
596
- key: API密钥值(可能包含Bearer前缀)
597
-
598
- Returns:
599
- bool: 是否在工作中
600
- """
601
- with self._lock:
602
- clean_key = key.replace("Bearer ", "") if key.startswith("Bearer ") else key
603
- return clean_key in self._working_keys
604
-
605
- def mark_key_invalid(self, key: str) -> Optional[str]:
606
- """
607
- 将指定的密钥标记为无效(临时禁用而不是永久禁用),并返回一个新的可用密钥
608
-
609
- Args:
610
- key: API密钥值(可能包含Bearer前缀)
611
-
612
- Returns:
613
- Optional[str]: 新的可用密钥,如果没有可用密钥则返回None
614
- """
615
- # 调用临时禁用方法,设置24小时禁用时间
616
- return self.mark_key_temp_disabled(key, hours=24.0)
617
-
618
- def mark_key_temp_disabled(self, key: str, hours: float = 12.0) -> Optional[str]:
619
- """
620
- 将指定的密钥临时禁用指定小时数,并返回一个新的可用密钥
621
-
622
- Args:
623
- key: API密钥值(可能包含Bearer前缀)
624
- hours: 禁用小时数
625
-
626
- Returns:
627
- Optional[str]: 新的可用密钥,如果没有可用密钥则返回None
628
- """
629
- with self._lock: # 使用锁保护临时禁用过程
630
- # 去掉可能的Bearer前缀
631
- key_for_search = key.replace("Bearer ", "") if key.startswith("Bearer ") else key
632
-
633
- # 检查是否是因为密钥在工作中导致的错误
634
- if key_for_search in self._working_keys:
635
- logger.warning(f"尝试禁用正在工作中的密钥(任务ID: {self._working_keys[key_for_search]}),跳过禁用操作")
636
- # 获取一个新密钥返回,但不禁用当前密钥
637
- new_key = self.get_key()
638
- if new_key:
639
- logger.info(f"已返回新密钥,但未禁用工作中的密钥")
640
- return new_key
641
- else:
642
- logger.warning("没有可用的备用密钥")
643
- return None
644
-
645
- # 查找对应的密钥
646
- key_found = False
647
- disabled_key_id = None
648
- for key_info in self.keys:
649
- stored_key = key_info.get("key", "").replace("Bearer ", "") if key_info.get("key", "").startswith("Bearer ") else key_info.get("key", "")
650
- if stored_key == key_for_search:
651
- # 标记密钥为临时禁用
652
- disabled_until = time.time() + (hours * 3600) # 当前时间加上禁用小时数
653
- key_info["available"] = False
654
- key_info["temp_disabled_until"] = disabled_until
655
- key_info["notes"] = (key_info.get("notes") or "") + f"\n[自动] 在 {time.strftime('%Y-%m-%d %H:%M:%S')} 被临时禁用{hours}小时"
656
- key_found = True
657
- disabled_key_id = key_info.get("id")
658
- logger.warning(f"密钥 {key_info.get('name') or key_info.get('id')} 被临时禁用{hours}小时")
659
- break
660
-
661
- if key_found:
662
- # 保存更改
663
- self._save_keys()
664
-
665
- # 获取新的密钥,排除已禁用的
666
- new_key = self.get_key()
667
- if new_key:
668
- logger.info(f"已自动切换到新的密钥")
669
- return new_key
670
- else:
671
- logger.warning("没有可用的备用密钥")
672
- return None
673
- else:
674
- logger.warning(f"未找到要临时禁用的密钥")
675
- return None
676
-
677
- def retry_request(self, original_key: str, request_func: Callable, max_retries: int = 1,
678
- max_key_switches: int = 3) -> Tuple[bool, Any, str]:
679
- """
680
- 出错时自动重试请求,并在需要时切换密钥
681
-
682
- Args:
683
- original_key: 原始API密钥(可能包含Bearer前缀)
684
- request_func: 执行请求的函数,接受一个参数(密钥)并返回(成功标志, 结果)
685
- max_retries: 使用同一密钥的最大重试次数
686
- max_key_switches: 最大密钥切换次数
687
-
688
- Returns:
689
- Tuple[bool, Any, str]: (是否成功, 请求结果, 使用的密钥)
690
- """
691
- current_key = original_key
692
- current_key_switches = 0
693
-
694
- # 首先用原始密钥尝试
695
- for attempt in range(max_retries + 1): # +1是因为第一次不算重试
696
- try:
697
- success, result = request_func(current_key)
698
- # 成功的请求不应该导致密钥被禁用
699
- if success:
700
- # 记录请求成功,避免不必要的密钥禁用
701
- with self._lock:
702
- self.record_request_result(current_key, True)
703
- return True, result, current_key
704
- logger.warning(f"请求失败(尝试 {attempt+1}/{max_retries+1}): {result}")
705
- except Exception as e:
706
- logger.error(f"请求异常(尝试 {attempt+1}/{max_retries+1}): {str(e)}")
707
-
708
- # 如果这不是最后一次尝试,等���一秒后重试
709
- if attempt < max_retries:
710
- time.sleep(1)
711
-
712
- # 如果原始密钥的所有重试都失败,尝试切换密钥
713
- tried_keys = set([current_key.replace("Bearer ", "") if current_key.startswith("Bearer ") else current_key])
714
-
715
- while current_key_switches < max_key_switches:
716
- # 获取新的密钥
717
- with self._lock:
718
- new_key = self.get_key()
719
- if not new_key:
720
- logger.warning("没有更多可用的密钥")
721
- break
722
-
723
- # 确保不使用已经尝试过的密钥
724
- clean_new_key = new_key.replace("Bearer ", "") if new_key.startswith("Bearer ") else new_key
725
- if clean_new_key in tried_keys:
726
- continue
727
-
728
- tried_keys.add(clean_new_key)
729
- current_key = new_key
730
- current_key_switches += 1
731
-
732
- logger.info(f"切换到新密钥 (切换 {current_key_switches}/{max_key_switches})")
733
-
734
- # 用新密钥尝试
735
- for attempt in range(max_retries + 1):
736
- try:
737
- success, result = request_func(current_key)
738
- if success:
739
- # 记录请求成功
740
- with self._lock:
741
- self.record_request_result(current_key, True)
742
- return True, result, current_key
743
- logger.warning(f"使用新密钥请求失败(尝试 {attempt+1}/{max_retries+1}): {result}")
744
- except Exception as e:
745
- logger.error(f"使用新密钥请求异常(尝试 {attempt+1}/{max_retries+1}): {str(e)}")
746
-
747
- # 如果这不是最后一次尝试,等待一秒后重试
748
- if attempt < max_retries:
749
- time.sleep(1)
750
-
751
- # 所有尝试都失败,临时禁用原始密钥
752
- # 但是在并发环境下,这可能是因为网络或服务问题,而非密钥问题
753
- # 增加额外检查以减少不必要的密钥禁用
754
- should_disable = True
755
-
756
- # 在临时禁用前,确认是否是密钥问题而非服务或网络问题
757
- # 此处可以添加额外逻辑来判断是否应该禁用密钥
758
-
759
- if should_disable:
760
- logger.error(f"所有重试和密钥切换尝试都失败,临时禁用原始密钥")
761
- with self._lock:
762
- self.mark_key_temp_disabled(original_key, hours=6.0) # 减少禁用时间,避免资源浪费
763
- else:
764
- logger.warning(f"所有重试和密钥切换尝试都失败,但可能是服务问题而非密钥问题,不禁用密钥")
765
-
766
- # 返回最后一次尝试的结果
767
- return False, result, current_key
768
-
769
- # 创建全局密钥管理器实例
770
- storage_file = os.getenv("KEYS_STORAGE_FILE", "api_keys.json")
771
- # 如果提供了绝对路径则直接使用,否则使用相对路径
772
- if not os.path.isabs(storage_file):
773
- base_dir = os.getenv("BASE_DIR", os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
774
- storage_file = os.path.join(base_dir, storage_file)
775
-
776
- key_manager = KeyManager(storage_file=storage_file)
777
- logger.info(f"初始化全局密钥管理器,存储文件: {storage_file}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/main.py DELETED
@@ -1,44 +0,0 @@
1
- import uvicorn
2
- import logging
3
- from .app import app, key_manager
4
- from .config import Config
5
-
6
- # 获取日志记录器
7
- logger = logging.getLogger("sora-api.main")
8
-
9
- def init_app():
10
- """初始化应用程序"""
11
- try:
12
- # 密钥管理器已在app.py中初始化并加载完成
13
- # 检查是否有可用的密钥
14
- if not key_manager.keys:
15
- logger.warning("未配置API key,将使用测试密钥")
16
- key_manager.add_key(
17
- key_value="Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9...",
18
- name="默认测试密钥"
19
- )
20
-
21
- logger.info(f"API服务初始化完成,已加载 {len(key_manager.keys)} 个API key")
22
- except Exception as e:
23
- logger.error(f"API服务初始化失败: {str(e)}")
24
- raise
25
-
26
- def start():
27
- """启动API服务"""
28
- # 初始化应用
29
- init_app()
30
-
31
- # 打印配置信息
32
- Config.print_config()
33
-
34
- # 启动服务
35
- logger.info(f"启动服务: {Config.HOST}:{Config.PORT}")
36
- uvicorn.run(
37
- "src.app:app",
38
- host=Config.HOST,
39
- port=Config.PORT,
40
- reload=Config.VERBOSE_LOGGING # 仅在调试模式下开启自动重载
41
- )
42
-
43
- if __name__ == "__main__":
44
- start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/models/__init__.py DELETED
@@ -1 +0,0 @@
1
-
 
 
sora-api-master/src/models/schemas.py DELETED
@@ -1,86 +0,0 @@
1
- from typing import List, Dict, Any, Optional, Union
2
- from pydantic import BaseModel, Field
3
-
4
- # 聊天完成请求模型
5
- class ContentItem(BaseModel):
6
- type: str
7
- text: Optional[str] = None
8
- image_url: Optional[Dict[str, str]] = None
9
-
10
- class ChatMessage(BaseModel):
11
- role: str
12
- content: Union[str, List[ContentItem]]
13
-
14
- class ChatCompletionRequest(BaseModel):
15
- model: str
16
- messages: List[ChatMessage]
17
- temperature: Optional[float] = 1.0
18
- top_p: Optional[float] = 1.0
19
- n: Optional[int] = 1
20
- stream: Optional[bool] = False
21
- max_tokens: Optional[int] = None
22
- presence_penalty: Optional[float] = 0
23
- frequency_penalty: Optional[float] = 0
24
-
25
- # API密钥创建模型
26
- class ApiKeyCreate(BaseModel):
27
- name: str = Field(..., description="密钥名称")
28
- key_value: str = Field(..., description="密钥值")
29
- weight: int = Field(1, description="权重")
30
- rate_limit: int = Field(60, description="速率限制(每分钟请求数)")
31
- is_enabled: bool = Field(True, description="是否启用")
32
- notes: Optional[str] = Field(None, description="备注")
33
-
34
- # API密钥更新模型
35
- class ApiKeyUpdate(BaseModel):
36
- name: Optional[str] = Field(None, description="密钥名称")
37
- key_value: Optional[str] = Field(None, description="密钥值")
38
- weight: Optional[int] = Field(None, description="权重")
39
- rate_limit: Optional[int] = Field(None, description="速率限制(每分钟请求数)")
40
- is_enabled: Optional[bool] = Field(None, description="是否启用")
41
- notes: Optional[str] = Field(None, description="备注")
42
-
43
- # 批量操作基础模型
44
- class BatchOperation(BaseModel):
45
- action: str = Field(..., description="操作类型:import, enable, disable, delete")
46
- key_ids: Optional[List[str]] = Field(None, description="要操作的密钥ID列表")
47
-
48
- # 批量导入的密钥项模型
49
- class ImportKeyItem(BaseModel):
50
- name: str = Field(..., description="密钥名称")
51
- key: str = Field(..., description="密钥值")
52
- weight: int = Field(1, description="权重")
53
- rate_limit: int = Field(60, description="速率限制(每分钟请求数)")
54
- enabled: bool = Field(True, description="是否启用")
55
- notes: Optional[str] = Field(None, description="备注")
56
-
57
- # 批量导入模型
58
- class BatchImportOperation(BatchOperation):
59
- keys: List[ImportKeyItem] = Field(..., description="要导入的密钥列表")
60
- key_ids: Optional[List[str]] = None
61
-
62
- # 系统配置更新模型
63
- class ConfigUpdate(BaseModel):
64
- PROXY_HOST: Optional[str] = Field(None, description="代理服务器主机")
65
- PROXY_PORT: Optional[str] = Field(None, description="代理服务器端口")
66
- PROXY_USER: Optional[str] = Field(None, description="代理服务器用户名")
67
- PROXY_PASS: Optional[str] = Field(None, description="代理服务器密码")
68
- BASE_URL: Optional[str] = Field(None, description="基础URL,用于图片访问地址")
69
- IMAGE_LOCALIZATION: Optional[bool] = Field(None, description="是否启用图片本地化存储")
70
- IMAGE_SAVE_DIR: Optional[str] = Field(None, description="图片保存目录")
71
- save_to_env: bool = Field(True, description="是否保存到环境变量文件")
72
-
73
- # 日志级别更新模型
74
- class LogLevelUpdate(BaseModel):
75
- level: str = Field(..., description="日志级别: DEBUG, INFO, WARNING, ERROR, CRITICAL")
76
- save_to_env: bool = Field(True, description="是否保存到环境变量文件")
77
-
78
- # JWT认证请求模型
79
- class LoginRequest(BaseModel):
80
- admin_key: str = Field(..., description="管理员密钥")
81
-
82
- # JWT令牌响应模型
83
- class TokenResponse(BaseModel):
84
- token: str = Field(..., description="JWT令牌")
85
- expires_in: int = Field(..., description="有效期(秒)")
86
- token_type: str = Field("bearer", description="令牌类型")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/services/__init__.py DELETED
@@ -1 +0,0 @@
1
-
 
 
sora-api-master/src/services/image_service.py DELETED
@@ -1,235 +0,0 @@
1
- import asyncio
2
- import base64
3
- import os
4
- import tempfile
5
- import time
6
- import uuid
7
- import logging
8
- import threading
9
- from typing import List, Dict, Any, Optional, Union, Tuple
10
-
11
- from ..sora_integration import SoraClient
12
- from ..config import Config
13
- from ..utils import localize_image_urls
14
-
15
- logger = logging.getLogger("sora-api.image_service")
16
-
17
- # 存储生成结果的全局字典
18
- generation_results = {}
19
-
20
- # 存储任务与API密钥的映射关系
21
- task_to_api_key = {}
22
-
23
- # 将处理中状态消息格式化为think代码块
24
- def format_think_block(message: str) -> str:
25
- """将消息放入```think代码块中"""
26
- return f"```think\n{message}\n```"
27
-
28
- async def process_image_task(
29
- request_id: str,
30
- sora_client: SoraClient,
31
- task_type: str,
32
- prompt: str,
33
- **kwargs
34
- ) -> None:
35
- """
36
- 统一的图像处理任务函数
37
-
38
- Args:
39
- request_id: 请求ID
40
- sora_client: Sora客户端实例
41
- task_type: 任务类型 ("generation" 或 "remix")
42
- prompt: 提示词
43
- **kwargs: 其他参数,取决于任务类型
44
- """
45
- try:
46
- # 保存当前任务使用的API密钥,以便后续使用同一密钥进行操作
47
- current_api_key = sora_client.auth_token
48
- task_to_api_key[request_id] = current_api_key
49
-
50
- # 更新状态为处理中
51
- generation_results[request_id] = {
52
- "status": "processing",
53
- "message": format_think_block("正在准备生成任务,请稍候..."),
54
- "timestamp": int(time.time()),
55
- "api_key": current_api_key # 记录使用的API密钥
56
- }
57
-
58
- # 根据任务类型执行不同操作
59
- if task_type == "generation":
60
- # 文本到图像生成
61
- num_images = kwargs.get("num_images", 1)
62
- width = kwargs.get("width", 720)
63
- height = kwargs.get("height", 480)
64
-
65
- # 更新状态
66
- generation_results[request_id] = {
67
- "status": "processing",
68
- "message": format_think_block("正在生成图像,请耐心等待..."),
69
- "timestamp": int(time.time()),
70
- "api_key": current_api_key
71
- }
72
-
73
- # 生成图像
74
- logger.info(f"[{request_id}] 开始生成图像, 提示词: {prompt}")
75
- image_urls = await sora_client.generate_image(
76
- prompt=prompt,
77
- num_images=num_images,
78
- width=width,
79
- height=height
80
- )
81
-
82
- elif task_type == "remix":
83
- # 图像到图像生成(Remix)
84
- image_data = kwargs.get("image_data")
85
- num_images = kwargs.get("num_images", 1)
86
-
87
- if not image_data:
88
- raise ValueError("缺少图像数据")
89
-
90
- # 更新状态
91
- generation_results[request_id] = {
92
- "status": "processing",
93
- "message": format_think_block("正在处理上传的图片..."),
94
- "timestamp": int(time.time()),
95
- "api_key": current_api_key
96
- }
97
-
98
- # 保存base64图片到临时文件
99
- temp_dir = tempfile.mkdtemp()
100
- temp_image_path = os.path.join(temp_dir, f"upload_{uuid.uuid4()}.png")
101
-
102
- try:
103
- # 解码并保存图片
104
- with open(temp_image_path, "wb") as f:
105
- f.write(base64.b64decode(image_data))
106
-
107
- # 更新状态
108
- generation_results[request_id] = {
109
- "status": "processing",
110
- "message": format_think_block("正在上传图片到Sora服务..."),
111
- "timestamp": int(time.time()),
112
- "api_key": current_api_key
113
- }
114
-
115
- # 上传图片 - 确保使用与初始请求相同的API密钥
116
- upload_result = await sora_client.upload_image(temp_image_path)
117
- media_id = upload_result['id']
118
-
119
- # 更新状态
120
- generation_results[request_id] = {
121
- "status": "processing",
122
- "message": format_think_block("正在基于图片生成新图像..."),
123
- "timestamp": int(time.time()),
124
- "api_key": current_api_key
125
- }
126
-
127
- # 执行remix生成
128
- logger.info(f"[{request_id}] 开始生成Remix图像, 提示词: {prompt}")
129
- image_urls = await sora_client.generate_image_remix(
130
- prompt=prompt,
131
- media_id=media_id,
132
- num_images=num_images
133
- )
134
-
135
- finally:
136
- # 清理临时文件
137
- if os.path.exists(temp_image_path):
138
- os.remove(temp_image_path)
139
- if os.path.exists(temp_dir):
140
- os.rmdir(temp_dir)
141
- else:
142
- raise ValueError(f"未知的任务类型: {task_type}")
143
-
144
- # 验证生成结果
145
- if isinstance(image_urls, str):
146
- logger.warning(f"[{request_id}] 图像生成失败或返回了错误信息: {image_urls}")
147
- generation_results[request_id] = {
148
- "status": "failed",
149
- "error": image_urls,
150
- "message": format_think_block(f"图像生成失败: {image_urls}"),
151
- "timestamp": int(time.time()),
152
- "api_key": current_api_key
153
- }
154
- return
155
-
156
- if not image_urls:
157
- logger.warning(f"[{request_id}] 图像生成返回了空列表")
158
- generation_results[request_id] = {
159
- "status": "failed",
160
- "error": "图像生成返回了空结果",
161
- "message": format_think_block("图像生成失败: 服务器返回了空结果"),
162
- "timestamp": int(time.time()),
163
- "api_key": current_api_key
164
- }
165
- return
166
-
167
- logger.info(f"[{request_id}] 成功生成 {len(image_urls)} 张图片")
168
-
169
- # 本地化图片URL
170
- if Config.IMAGE_LOCALIZATION:
171
- logger.info(f"[{request_id}] 准备进行图片本地化处理")
172
- try:
173
- localized_urls = await localize_image_urls(image_urls)
174
- logger.info(f"[{request_id}] 图片本地化处理完成")
175
-
176
- # 检查本地化结果
177
- if not localized_urls:
178
- logger.warning(f"[{request_id}] 本地化处理返回了空列表,将使用原始URL")
179
- localized_urls = image_urls
180
-
181
- # 检查是否所有URL都被正确本地化
182
- local_count = sum(1 for url in localized_urls if url.startswith("/static/") or "/static/" in url)
183
- logger.info(f"[{request_id}] 本地化结果: 总计 {len(localized_urls)} 张图片,成功本地化 {local_count} 张")
184
-
185
- if local_count == 0:
186
- logger.warning(f"[{request_id}] 警告:没有一个URL被成功本地化,将使用原始URL")
187
- localized_urls = image_urls
188
-
189
- image_urls = localized_urls
190
- except Exception as e:
191
- logger.error(f"[{request_id}] 图片本地化过程中发生错误: {str(e)}", exc_info=True)
192
- logger.info(f"[{request_id}] 由于错误,将使用原始URL")
193
- else:
194
- logger.info(f"[{request_id}] 图片本地化功能未启用,使用原始URL")
195
-
196
- # 存储结果
197
- generation_results[request_id] = {
198
- "status": "completed",
199
- "image_urls": image_urls,
200
- "timestamp": int(time.time()),
201
- "api_key": current_api_key
202
- }
203
-
204
- # 30分钟后自动清理结果
205
- def cleanup_task():
206
- generation_results.pop(request_id, None)
207
- task_to_api_key.pop(request_id, None)
208
-
209
- threading.Timer(1800, cleanup_task).start()
210
-
211
- except Exception as e:
212
- error_message = f"图像生成失败: {str(e)}"
213
- generation_results[request_id] = {
214
- "status": "failed",
215
- "error": error_message,
216
- "message": format_think_block(error_message),
217
- "timestamp": int(time.time()),
218
- "api_key": sora_client.auth_token # 记录当前API密钥
219
- }
220
- logger.error(f"图像生成失败 (ID: {request_id}): {str(e)}", exc_info=True)
221
-
222
- def get_generation_result(request_id: str) -> Dict[str, Any]:
223
- """获取生成结果"""
224
- if request_id not in generation_results:
225
- return {
226
- "status": "not_found",
227
- "error": f"找不到生成任务: {request_id}",
228
- "timestamp": int(time.time())
229
- }
230
-
231
- return generation_results[request_id]
232
-
233
- def get_task_api_key(request_id: str) -> Optional[str]:
234
- """获取任务对应的API密钥"""
235
- return task_to_api_key.get(request_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/services/streaming.py DELETED
@@ -1,231 +0,0 @@
1
- import json
2
- import time
3
- import asyncio
4
- import logging
5
- from typing import AsyncGenerator, List, Dict, Any
6
-
7
- from ..sora_integration import SoraClient
8
- from ..config import Config
9
- from ..utils import localize_image_urls
10
- from .image_service import format_think_block
11
-
12
- logger = logging.getLogger("sora-api.streaming")
13
-
14
- async def generate_streaming_response(
15
- sora_client: SoraClient,
16
- prompt: str,
17
- n_images: int = 1
18
- ) -> AsyncGenerator[str, None]:
19
- """
20
- 文本到图像的流式响应生成器
21
-
22
- Args:
23
- sora_client: Sora客户端
24
- prompt: 提示词
25
- n_images: 生成图像数量
26
-
27
- Yields:
28
- SSE格式的响应数据
29
- """
30
- request_id = f"chatcmpl-stream-{time.time()}-{hash(prompt) % 10000}"
31
-
32
- # 发送开始事件
33
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]})}\n\n"
34
-
35
- # 发送处理中的消息(放在代码块中)
36
- start_msg = "```think\n正在生成图像,请稍候...\n"
37
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': start_msg}, 'finish_reason': None}]})}\n\n"
38
-
39
- # 创建一个后台任务来生成图像
40
- logger.info(f"[流式响应 {request_id}] 开始生成图像, 提示词: {prompt}")
41
- generation_task = asyncio.create_task(sora_client.generate_image(
42
- prompt=prompt,
43
- num_images=n_images,
44
- width=720,
45
- height=480
46
- ))
47
-
48
- # 每5秒发送一条"仍在生成中"的消息,防止连接超时
49
- progress_messages = [
50
- "正在处理您的请求...",
51
- "仍在生成图像中,请继续等待...",
52
- "Sora正在创作您的图像作品...",
53
- "图像生成需要一点时间,感谢您的耐心等待...",
54
- "我们正在努力为您创作高质量图像..."
55
- ]
56
-
57
- i = 0
58
- while not generation_task.done():
59
- # 每5秒发送一次进度消息
60
- await asyncio.sleep(5)
61
- progress_msg = progress_messages[i % len(progress_messages)]
62
- i += 1
63
- content = "\n" + progress_msg + "\n"
64
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})}\n\n"
65
-
66
- try:
67
- # 获取生成结果
68
- image_urls = await generation_task
69
- logger.info(f"[流式响应 {request_id}] 图像生成完成,获取到 {len(image_urls) if isinstance(image_urls, list) else '非列表'} 个URL")
70
-
71
- # 本地化图片URL
72
- if Config.IMAGE_LOCALIZATION and isinstance(image_urls, list) and image_urls:
73
- logger.info(f"[流式响应 {request_id}] 准备进行图片本地化处理")
74
- try:
75
- localized_urls = await localize_image_urls(image_urls)
76
- image_urls = localized_urls
77
- logger.info(f"[流式响应 {request_id}] 图片本地化处理完成")
78
- except Exception as e:
79
- logger.error(f"[流式响应 {request_id}] 图片本地化过程中发生错误: {str(e)}", exc_info=True)
80
- logger.info(f"[流式响应 {request_id}] 由于错误,将使用原始URL")
81
- elif not Config.IMAGE_LOCALIZATION:
82
- logger.info(f"[流式响应 {request_id}] 图片本地化功能未启用")
83
-
84
- # 结束代码块
85
- content_str = "\n```\n\n"
86
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
87
-
88
- # 添加生成的图片URLs
89
- for i, url in enumerate(image_urls):
90
- if i > 0:
91
- content_str = "\n\n"
92
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
93
-
94
- image_markdown = f"![Generated Image]({url})"
95
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': image_markdown}, 'finish_reason': None}]})}\n\n"
96
-
97
- # 发送完成事件
98
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
99
-
100
- # 发送结束标志
101
- yield "data: [DONE]\n\n"
102
-
103
- except Exception as e:
104
- error_msg = f"图像生成失败: {str(e)}"
105
- logger.error(f"[流式响应 {request_id}] 错误: {error_msg}", exc_info=True)
106
- error_content = f"\n{error_msg}\n```"
107
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': error_content}, 'finish_reason': 'error'}]})}\n\n"
108
- yield "data: [DONE]\n\n"
109
-
110
- async def generate_streaming_remix_response(
111
- sora_client: SoraClient,
112
- prompt: str,
113
- image_data: str,
114
- n_images: int = 1
115
- ) -> AsyncGenerator[str, None]:
116
- """
117
- 图像到图像的流式响应生成器
118
-
119
- Args:
120
- sora_client: Sora客户端
121
- prompt: 提示词
122
- image_data: Base64编码的图像数据
123
- n_images: 生成图像数量
124
-
125
- Yields:
126
- SSE格式的响应数据
127
- """
128
- import os
129
- import tempfile
130
- import uuid
131
- import base64
132
-
133
- request_id = f"chatcmpl-stream-remix-{time.time()}-{hash(prompt) % 10000}"
134
-
135
- # 发送开始事件
136
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'role': 'assistant'}, 'finish_reason': None}]})}\n\n"
137
-
138
- try:
139
- # 保存base64图片到临时文件
140
- temp_dir = tempfile.mkdtemp()
141
- temp_image_path = os.path.join(temp_dir, f"upload_{uuid.uuid4()}.png")
142
-
143
- try:
144
- # 解码并保存图片
145
- with open(temp_image_path, "wb") as f:
146
- f.write(base64.b64decode(image_data))
147
-
148
- # 上传图片
149
- upload_msg = "```think\n上传图片中...\n"
150
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': upload_msg}, 'finish_reason': None}]})}\n\n"
151
-
152
- logger.info(f"[流式响应Remix {request_id}] 上传图片中")
153
- upload_result = await sora_client.upload_image(temp_image_path)
154
- media_id = upload_result['id']
155
-
156
- # 发送生成中消息
157
- generate_msg = "\n基于图片生成新图像中...\n"
158
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': generate_msg}, 'finish_reason': None}]})}\n\n"
159
-
160
- # 创建后台任务生成图像
161
- logger.info(f"[流式响应Remix {request_id}] 开始生成图像,提示词: {prompt}")
162
- generation_task = asyncio.create_task(sora_client.generate_image_remix(
163
- prompt=prompt,
164
- media_id=media_id,
165
- num_images=n_images
166
- ))
167
-
168
- # 每5秒发送一条"仍在生成中"的消息
169
- progress_messages = [
170
- "正在处理您的请求...",
171
- "仍在生成图像中,请继续等待...",
172
- "Sora正在基于您的图片创作新作品...",
173
- "图像生成需要一点时间,感谢您的耐心等待...",
174
- "正在融合您的风格和提示词,打造专属图像..."
175
- ]
176
-
177
- i = 0
178
- while not generation_task.done():
179
- await asyncio.sleep(5)
180
- progress_msg = progress_messages[i % len(progress_messages)]
181
- i += 1
182
- content = "\n" + progress_msg + "\n"
183
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content}, 'finish_reason': None}]})}\n\n"
184
-
185
- # 获取生成结果
186
- image_urls = await generation_task
187
- logger.info(f"[流式响应Remix {request_id}] 图像生成完成")
188
-
189
- # 本地化图片URL
190
- if Config.IMAGE_LOCALIZATION:
191
- logger.info(f"[流式响应Remix {request_id}] 进行图片本地化处理")
192
- localized_urls = await localize_image_urls(image_urls)
193
- image_urls = localized_urls
194
- logger.info(f"[流式响应Remix {request_id}] 图片本地化处理完成")
195
- else:
196
- logger.info(f"[流式响应Remix {request_id}] 图片本地化功能未启用")
197
-
198
- # 结束代码块
199
- content_str = "\n```\n\n"
200
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': content_str}, 'finish_reason': None}]})}\n\n"
201
-
202
- # 发送图片URL作为Markdown
203
- for i, url in enumerate(image_urls):
204
- if i > 0:
205
- newline_str = "\n\n"
206
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': newline_str}, 'finish_reason': None}]})}\n\n"
207
-
208
- image_markdown = f"![Generated Image]({url})"
209
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': image_markdown}, 'finish_reason': None}]})}\n\n"
210
-
211
- # 发送完成事件
212
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}]})}\n\n"
213
-
214
- # 发送结束标志
215
- yield "data: [DONE]\n\n"
216
-
217
- finally:
218
- # 清理临时文件
219
- if os.path.exists(temp_image_path):
220
- os.remove(temp_image_path)
221
- if os.path.exists(temp_dir):
222
- os.rmdir(temp_dir)
223
-
224
- except Exception as e:
225
- error_msg = f"图像Remix失败: {str(e)}"
226
- logger.error(f"[流式响应Remix {request_id}] 错误: {error_msg}", exc_info=True)
227
- error_content = f"\n{error_msg}\n```"
228
- yield f"data: {json.dumps({'id': request_id, 'object': 'chat.completion.chunk', 'created': int(time.time()), 'model': 'sora-1.0', 'choices': [{'index': 0, 'delta': {'content': error_content}, 'finish_reason': 'error'}]})}\n\n"
229
-
230
- # 结束流
231
- yield "data: [DONE]\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/sora_generator.py DELETED
@@ -1,1145 +0,0 @@
1
- import cloudscraper
2
- import time
3
- import json
4
- import random
5
- import string
6
- import os
7
- import mimetypes # To guess file mime type
8
- import argparse # For better command-line arguments
9
- import asyncio
10
- from .utils import localize_image_urls
11
- from .config import Config
12
-
13
- class SoraImageGenerator:
14
- def __init__(self, proxy_host=None, proxy_port=None, proxy_user=None, proxy_pass=None, auth_token=None):
15
- # 使用Config.VERBOSE_LOGGING替代直接从环境变量读取SORA_DEBUG
16
- self.DEBUG = Config.VERBOSE_LOGGING
17
-
18
- # 设置代理
19
- if proxy_host and proxy_port:
20
- # 如果有认证信息,添加到代理URL中
21
- if proxy_user and proxy_pass:
22
- proxy_auth = f"{proxy_user}:{proxy_pass}@"
23
- self.proxies = {
24
- "http": f"http://{proxy_auth}{proxy_host}:{proxy_port}",
25
- "https": f"http://{proxy_auth}{proxy_host}:{proxy_port}"
26
- }
27
- if self.DEBUG:
28
- print(f"已配置带认证的代理: {proxy_user}:****@{proxy_host}:{proxy_port}")
29
- else:
30
- self.proxies = {
31
- "http": f"http://{proxy_host}:{proxy_port}",
32
- "https": f"http://{proxy_host}:{proxy_port}"
33
- }
34
- if self.DEBUG:
35
- print(f"已配置代理: {proxy_host}:{proxy_port}")
36
- else:
37
- self.proxies = None
38
- if self.DEBUG:
39
- print("代理未配置。请求将直接发送。")
40
- # 创建 cloudscraper 实例
41
- self.scraper = cloudscraper.create_scraper(
42
- browser={
43
- 'browser': 'chrome',
44
- 'platform': 'windows',
45
- 'mobile': False
46
- }
47
- )
48
- # 设置通用请求头 - 移除 Content-Type 和 openai-sentinel-token (会动态添加)
49
- self.base_headers = {
50
- "accept": "*/*",
51
- "accept-language": "zh-CN,zh;q=0.9",
52
- "cache-control": "no-cache",
53
- "pragma": "no-cache",
54
- "priority": "u=1, i",
55
- "sec-ch-ua": "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"",
56
- "sec-ch-ua-mobile": "?0",
57
- "sec-ch-ua-platform": "\"Windows\"",
58
- "sec-fetch-dest": "empty",
59
- "sec-fetch-mode": "cors",
60
- "sec-fetch-site": "same-origin",
61
- # Referer 会根据操作不同设置
62
- }
63
- # 设置认证Token (从外部传入或硬编码)
64
- self.auth_token = auth_token or "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc0Nzk3MDExMSwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InVzZXJfaWQiOiJ1c2VyLWdNeGM0QmVoVXhmTW1iTDdpeUtqengxYiJ9LCJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiIzajVtOTFud3VtckBmcmVlLnViby5lZHUuZ24iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0sImlhdCI6MTc0NzEwNjExMSwiaXNzIjoiaHR0cHM6Ly9hdXRoLm9wZW5haS5jb20iLCJqdGkiOiIzMGM4ZDJhOS0yNzkxLTRhNjQtODI2OS0yMzU3OGFhMmI0MTEiLCJuYmYiOjE3NDcxMDYxMTEsInB3ZF9hdXRoX3RpbWUiOjE3NDcxMDYxMDkxMDksInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiLCJvZmZsaW5lX2FjY2VzcyIsIm1vZGVsLnJlcXVlc3QiLCJtb2RlbC5yZWFkIiwib3JnYW5pemF0aW9uLnJlYWQiLCJvcmdhbml6YXRpb24ud3JpdGUiXSwic2Vzc2lvbl9pZCI6ImF1dGhzZXNzX21yWFRwZlVENU51TDFsV05xNUhSOW9lYiIsInN1YiI6ImF1dGgwfDY4MjFkYWYyNjhiYjgxMzFkMDRkYTAwNCJ9.V4ZqYJuf_f7F_DrMMRrt-ymul5HUrqENVkiFyEwfYmzMFWthEGS6Ryia100QRlprw8jjGscHZXlUFaOcRNIarcBig8fBY6n_AB3J34MlcBv6peS-3_EJlIiH_N7j_mu-8lNpJbxk9lSlFaGpKU1IOO7kBuaAmLH-iErM-wqBfSlnnAq8h4iqBDxi4CMTcAhVm2-qG7u7f0Ho1TCGa7wrdchWtZxyfHIqNWkC88qBlUwTH5g2vRL419_zIKEWKyAtV2WNI68vpyBLrRVhtnpDh0jcrm2WqCj2X2LQqNFkFKoui3wCdG9Vskpl39l9sV54HuV7w6stQIausR1F4Y9NbjsBAyLIimZOllCwYAefTC2BOpIHfOA3_D58G3SEiRADVK7pK7ip6QsEI__GteoeCuRvZA9b5jLmhVS0SUlDYSOoNwlJ_ejfEpPJcmHUchFa7bUkS-XVrEUgr1yP5FxPwWUyn7UWrW_dZ3lVW1EU4Bp6Kp6JuwyOFf2Mj-V3_9tc8qJRClI8WHUf6In0hiO_pGbFCI2opkF3XusAQKmTB12nPBsmSlwewigTPhAj3nf-8Ze3O-etnBrV5pz_woIwQsQ54T-wgEdrLWDE6dSqNDulfpldF6Cok62212kW8w3SY3V7VSq5Tr1KRyWXJEH-haVb6qmAE2ldDjeHvJossWg" # 替换成你的有效Token或从环境变量读取
65
- if not self.auth_token or not self.auth_token.startswith("Bearer "):
66
- raise ValueError("无效或缺失的认证Token (应以 'Bearer ' 开头)")
67
- self.gen_url = "https://sora.chatgpt.com/backend/video_gen"
68
- self.check_url = "https://sora.chatgpt.com/backend/video_gen"
69
- self.upload_url = "https://sora.chatgpt.com/backend/uploads"
70
-
71
- # 显示是否启用了图片本地化功能
72
- if Config.IMAGE_LOCALIZATION:
73
- if self.DEBUG:
74
- print(f"图片本地化功能已启用,图片将保存到:{Config.IMAGE_SAVE_DIR}")
75
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
76
-
77
- def _get_dynamic_headers(self, content_type="application/json", referer="https://sora.chatgpt.com/explore"):
78
- """为每个请求生成包含动态sentinel token的headers"""
79
- headers = self.base_headers.copy()
80
- headers["authorization"] = self.auth_token
81
- headers["openai-sentinel-token"] = self._generate_sentinel_token()
82
- headers["referer"] = referer
83
- if content_type: # multipart/form-data 时 Content-Type 由 requests 自动设置
84
- headers["content-type"] = content_type
85
- return headers
86
-
87
- def _generate_sentinel_token(self):
88
- """生成一个随机修改的sentinel-token"""
89
- base_token = {
90
- "p": "gAAAAABWzIxMjksIk1vbiBNYXkgMTIgMjAyNSAxODo1ODowOSBHTVQrMDgwMCAo5Lit5Zu95qCH5YeG5pe26Ze0KSIsMjI0ODE0Njk0NCw5LCJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM2LjAuMC4wIFNhZmFyaS81MzcuMzYiLCJodHRwczovL3NvcmEtY2RuLm9haXN0YXRpYy5jb20vX25leHQvc3RhdGljL2NodW5rcy93ZWJwYWNrLWU1MDllMDk0N2RlZWM0Y2YuanMiLG51bGwsInpoLUNOIiwiemgtQ04semgiLDEwLCJwcm9kdWN0U3Vi4oiSMjAwMzAxMDciLCJfcmVhY3RMaXN0ZW5pbmd2cXAzdWtpNmh6IiwiX19zZW50aW5lbF9pbml0X3BlbmRpbmciLDEyMDczMzEuNSwiN2M4OTMxM2EtZTY0Mi00MjI4LTk5ZDQtNTRlZDRjYzQ3MGZiIiwiIiwxNl0=",
91
- "t": "SBYdBBoGFhQRdVtbdxEYEQEaHQAAFhQRYwN2T2IEUH5pRHJyYx51YnJybFBhWHZkYgRyUHgDclZzH2l5a1gXUWVlcRMRGBEBHR0HBBYUEXpnfVt9YFAJDB8WAAIAAgYRDgxpYltjSl55SWJEZV9mXx4KFh8WGAAaBxYUEXVydW9ydXJ1b3J1cnVvcnVydW9ydXJ1b3J1b3J1b3J1b3J1DgkMHxYBAgALAxEOHh0BBwIXCwwEBx8FAwADGwAAHxYcBxoFDQwJFnFDEw4WHxYXBxoCBwwJFmB2dGRxYgBqdHVLf2hUX3VySWpVcVNNa3ZiYW13dgtyb3J5cHxvam12YWB7YgN2THVcYnxsSwR7fH9mfHJlZ1F3VARtcVwLcGpiV2pwaEdmZFhgdGZLbWRxXAdQbHJ5dHNvanlwQ29Xd1QEZnFmXFJoWFBRbEZ2e3JTWVF8YnFvd2ZUd2xiR3Vzb1BxcWV3UXxLbmxrYV9Wf3FxfHNJclVydU1qdXJtaHJmUH9sYnF1fElcdXtMdH5sdnZmZAR+ZmpUcXZzVgN2cUN3UHZyZWhyXAd8bHJ5cnVGdW1lWGRScWIAamBlDgkMHxYKBgADDREODHZjcWx/AnIBZkJLV2FTRWZWZX5Pa2JHVWsDbmRie1xgYXFoUWxfbmlhBQJ4f3FmUGFJBnNkWEphZ3VET2VYcntpA25kYntDVWRYf2Z3ZXZ9Vl9hXXZ1RGZseENzZnJ3Z3YCdn1WX2FddnVEZmx4Q3NmcndkFh8WFwAaBAUMCRZrcnh7fF92T2Z1eXx2dW5pZ1YKXGIFRnFhX3ZAZmJLUXwAfmtgeGlxYQVoeFdYdU1iYgJjfFsBAVJ7YUxrB1ZDVUR+fGthcXN4ZkRrbH9lWmtsG0FiAkdIZHFXZnxffnRiVgZsa3JeUmVlQEhhQ31ybFsNZ2VCS2NmWEZFZlhMWWQEcWJ2ZWZdZx5XdWRYXnllcVxJanMKWnQARElXdgoDUHx4WlZ0UEBWB0tNTQBuQW4dAlNXWXRUV11cXWkHQwx0XW5BUW9QbWVyRnFhZWJIVwR1UHtfRGdSeGl1YQUfcmJ2V0h3Zgd4bFR9cWdCUHFmZU1qd199amVDeQx5X3JRYB51ZWRYXnlSZHJcdAVHdXtmcWlWaXFwV1hGXlB0Q2hlX1dQf2V+fWB7cndqcl5YUF92TXRxV3V3AkBXY0JpbWRfSlJ8AgVLZGFHdnwCRHBnHFdzZlheV2wCYmZmbAZjfF9iUWxoBmJrYkp/UAJiZmVleXB4ZWZhdR9HYmJ2Qn5iX3JKdGFbV3xfdmF1HnVmZWF0f2V1THlmdXlTd3t+VFJ2Q05RfEZbV3tiAVIHAlpLAXICVXZpRmpzbEVXa1wcUgZXBwxO",
92
- "c": "gAAAAABoIdRAZEk6qAnDRqdimKxPhXtA_xBkiDXhKF4LUmlNY6CZmfNxZiabjPHk_DCEUnnyq-y6JPj-D46YPk-6r7zR6qS64hEwGYt9Hh_8vUIod-7PLh9qPKqdYl4TBCVUgtrbhTWfse7s6NHCSy1T0Nzj2C6vAUPhzAx4LAMIrl2YbElkUVPgwELyYF_inh3zliwZL-zp4zR3LOABcrGqlrLoP7_kNrwcIZwlVD1RNlnHy9TEFsRzYOMQo_DbagZAK1h87arrMonZHBi9ukfiGuvCQP-y76j61b4qaQPMA19EoURLwnotVBWUIBpHEEoH9vmPb817sGwQ2R8XHoAVR4dwYs_7EoS8H8kAlUVZDjAKGq5x48nvZrarLBjYJXXsfJLuxhibNYXG1hKNbOdi1w-Xl1NgqSPAb-MuwnyDLPGE5MeLkwM2Dl3jD7G6B2Z2F993cvW7mOOs0OebZ6NMgIrZnTG4mMI7PirPY95JWmztDfeuFLJ_V_kyaSP--BZCIIAB4074RBVitIrJEwceWVW3zXOOHWJoDax7E0nfa5abvLGCjdEeJfNx4Fcp7iYFN_E8iR0f797DLlFh4uFLv1DPhipYtQFpPPUlbxKu9H9W4IDr7Hv_LgfvFo0VwLzV6ANZPGmdza67dAsKXrWtXlCrVNfqFoVO4wI4n-zrE3lcUPzI_ZJebF2HGlzerTvqgU5R0i4fzUGY4-UqpWlVurP-rCJY2ARcSMSyPXnPGetl5Z2m-f9k7K3n7txrfv2293jyyRTVAZLC9aLrBOFWHD4cf0aiwisgJ9IKnhhnoJ-WF8NuvFS7l1Z0d2zTnndjuryb0M36Og4b_Ku3aJKS0_Eqbns8_bUXdEPh5_15T_92_1yf5jy-amrgolgcO_7yJqX5aU9-PUUiaP3WzeyidMSH4Vtls63tQ5evUlDkEHfNKoyCYaSxpzA_FsNmPbMmcv3g1wNKg_W8V0Yh7ZrtW1L8229SpZFWc96Sg3CRPplk-dnVzV93lP7cN5o5ubGWZMkkz5UASjE5XLn8h5dx6neuOemKVHAj29QxOWmdGEehNvmeMec0k8uL9X-7yYBJSTnI066OR38JUUTFqTH3RSXI9M4ggals32P56bwUmawvZ-bu02qc3kCVI3oK9bnP8oTk0xTK5_bMrlevGYKG0qdPXamgdDfVg7hlNA2OTCnw6iRP6DJiPm_zKVdQa4z6SPJsdt_Q",
93
- "id": self._generate_random_id(),
94
- "flow": "sora_create_task" # This might need changing for uploads? Let's try keeping it.
95
- }
96
- # Simple randomization (can be improved if needed)
97
- p_chars = list(base_token["p"])
98
- for _ in range(random.randint(3, 7)):
99
- pos = random.randint(10, len(p_chars) - 10)
100
- p_chars[pos] = random.choice(string.ascii_letters + string.digits + "+/=")
101
- base_token["p"] = "".join(p_chars)
102
- c_chars = list(base_token["c"])
103
- for _ in range(random.randint(3, 7)):
104
- pos = random.randint(10, len(c_chars) - 10)
105
- c_chars[pos] = random.choice(string.ascii_letters + string.digits + "+/=_-")
106
- base_token["c"] = "".join(c_chars)
107
- return json.dumps(base_token)
108
-
109
- def _generate_random_id(self):
110
- """生成一个随机ID,格式类似于UUID"""
111
- return f"{self._random_hex(8)}-{self._random_hex(4)}-{self._random_hex(4)}-{self._random_hex(4)}-{self._random_hex(12)}"
112
-
113
- def _random_hex(self, length):
114
- """生成指定长度的随机十六进制字符串"""
115
- return ''.join(random.choice(string.hexdigits.lower()) for _ in range(length))
116
-
117
- def generate_image(self, prompt, num_images=1, width=720, height=480):
118
- """
119
- 生成一张或多张图片并返回图片URL列表
120
- 参数:
121
- prompt (str): 图片生成提示词
122
- num_images (int): 要生成的图片数量 (对应 n_variants)
123
- width (int): 图片宽度
124
- height (int): 图片高度
125
- 返回:
126
- list[str] or str: 成功时返回包含图片URL的列表,失败时返回错误信息字符串
127
- """
128
- # 确保提示词是正确的UTF-8格式
129
- if isinstance(prompt, bytes):
130
- prompt = prompt.decode('utf-8')
131
-
132
- # 打印提示词,并处理可能的编码问题
133
- try:
134
- if self.DEBUG:
135
- print(f"开始生成 {num_images} 张图片,提示词: '{prompt}'")
136
- except UnicodeEncodeError:
137
- if self.DEBUG:
138
- print(f"开始生成 {num_images} 张图片,提示词: [编码显示问题,但数据正确]")
139
-
140
- payload = {
141
- "type": "image_gen",
142
- "operation": "simple_compose",
143
- "prompt": prompt,
144
- "n_variants": num_images, # 使用传入的数量
145
- "width": width,
146
- "height": height,
147
- "n_frames": 1,
148
- "inpaint_items": []
149
- }
150
- try:
151
- task_id = self._submit_task(payload, referer="https://sora.chatgpt.com/explore")
152
- if not task_id:
153
- # 任务提交失败,尝试切换密钥后重试
154
- if self.DEBUG:
155
- print(f"任务提交失败,尝试切换API密钥后重试")
156
-
157
- try:
158
- # 导入在这里进行以避免循环导入
159
- from .key_manager import key_manager
160
- # 标记当前密钥为无效并获取新密钥
161
- new_key = key_manager.mark_key_invalid(self.auth_token)
162
- if new_key:
163
- self.auth_token = new_key
164
- if self.DEBUG:
165
- print(f"已切换到新的API密钥,重试任务")
166
- # 使用新密钥重试提交任务
167
- task_id = self._submit_task(payload, referer="https://sora.chatgpt.com/explore")
168
- if not task_id:
169
- return "任务提交失败(已尝试切换密钥)"
170
- else:
171
- return "任务提交失败(无可用的备用密钥)"
172
- except ImportError:
173
- if self.DEBUG:
174
- print(f"无法导入key_manager,无法自动切换密钥")
175
- return "任务提交失败"
176
- except Exception as e:
177
- if self.DEBUG:
178
- print(f"切换API密钥时发生错误: {str(e)}")
179
- return f"任务提交失败: {str(e)}"
180
-
181
- if self.DEBUG:
182
- print(f"任务已提交,ID: {task_id}")
183
- # 轮询检查任务状态,直到完成
184
- image_urls = self._poll_task_status(task_id)
185
-
186
- # 如果轮询返回错误信息,也尝试切换密钥重试
187
- if isinstance(image_urls, str) and ("失败" in image_urls or "错误" in image_urls or "error" in image_urls.lower()):
188
- if self.DEBUG:
189
- print(f"任务执行失败,尝试切换API密钥后重试")
190
-
191
- try:
192
- from .key_manager import key_manager
193
- new_key = key_manager.mark_key_invalid(self.auth_token)
194
- if new_key:
195
- self.auth_token = new_key
196
- if self.DEBUG:
197
- print(f"已切换到新的API密钥,重试整个生成过程")
198
- # 使用新密钥重试整个生成过程
199
- return self.generate_image(prompt, num_images, width, height)
200
- except (ImportError, Exception) as e:
201
- if self.DEBUG:
202
- print(f"切换API密钥失败或重试失败: {str(e)}")
203
-
204
- # 图片本地化处理
205
- if Config.IMAGE_LOCALIZATION and isinstance(image_urls, list) and image_urls:
206
- if self.DEBUG:
207
- print(f"\n================================")
208
- print(f"开始图片本地化处理")
209
- print(f"图片本地化配置: 启用={Config.IMAGE_LOCALIZATION}, 保存目录={Config.IMAGE_SAVE_DIR}")
210
- print(f"需要本地化的图片数量: {len(image_urls)}")
211
- print(f"原始图片URLs: {image_urls}")
212
- print(f"================================\n")
213
-
214
- try:
215
- # 创建事件循环并运行图片本地化
216
- if self.DEBUG:
217
- print(f"创建异步事件循环处理图片下载...")
218
- loop = asyncio.new_event_loop()
219
- asyncio.set_event_loop(loop)
220
-
221
- if self.DEBUG:
222
- print(f"调用localize_image_urls函数...")
223
- localized_urls = loop.run_until_complete(localize_image_urls(image_urls))
224
- loop.close()
225
- if self.DEBUG:
226
- print(f"异步事件循环已关闭")
227
-
228
- if self.DEBUG:
229
- print(f"\n================================")
230
- print(f"图片本地化完成")
231
- print(f"原始URLs: {image_urls}")
232
- print(f"本地化后的URLs: {localized_urls}")
233
- print(f"================================\n")
234
-
235
- # 检查结果是否有效
236
- if not localized_urls:
237
- if self.DEBUG:
238
- print(f"❌ 警告:本地化后的URL列表为空,将使用原始URL")
239
- return image_urls
240
-
241
- # 检查是否所有URL都被正确本地化
242
- local_count = sum(1 for url in localized_urls if url.startswith("/static/") or "/static/" in url)
243
- if local_count == 0:
244
- if self.DEBUG:
245
- print(f"❌ 警告:没有一个URL被成功本地化,将使用原始URL")
246
- return image_urls
247
- elif local_count < len(localized_urls):
248
- if self.DEBUG:
249
- print(f"⚠️ 注意:部分URL未成功本地化 ({local_count}/{len(localized_urls)})")
250
-
251
- return localized_urls
252
- except Exception as e:
253
- if self.DEBUG:
254
- print(f"❌ 图片本地化过程中发生错误: {str(e)}")
255
- import traceback
256
- traceback.print_exc()
257
- if self.DEBUG:
258
- print(f"由于错误,将返回原始URL")
259
- return image_urls
260
- elif not Config.IMAGE_LOCALIZATION:
261
- if self.DEBUG:
262
- print(f"图片本地化功能未启用,返回原始URLs")
263
- elif not isinstance(image_urls, list):
264
- if self.DEBUG:
265
- print(f"图片生成返回了非列表结果: {image_urls},无法进行本地化")
266
- elif not image_urls:
267
- if self.DEBUG:
268
- print(f"图片生成返回了空列表,没有可本地化的内容")
269
-
270
- return image_urls
271
- except Exception as e:
272
- if self.DEBUG:
273
- print(f"❌ 生成图片时出错: {str(e)}")
274
- import traceback
275
- traceback.print_exc()
276
- return f"生成图片时出错: {str(e)}"
277
-
278
- def upload_image(self, file_path):
279
- """
280
- 上传本地图片文件到Sora后端
281
- 参数:
282
- file_path (str): 本地图片文件的路径
283
- 返回:
284
- dict or str: 成功时返回包含上传信息的字典,失败时返回错误信息字符串
285
- """
286
- if not os.path.exists(file_path):
287
- return f"错误:文件未找到 '{file_path}'"
288
- file_name = os.path.basename(file_path)
289
- mime_type, _ = mimetypes.guess_type(file_path)
290
- if not mime_type or not mime_type.startswith('image/'):
291
- return f"错误:无法确定文件类型或文件不是图片 '{file_path}' (Mime: {mime_type})"
292
- if self.DEBUG:
293
- print(f"开始上传图片: {file_name} (Type: {mime_type})")
294
- # multipart/form-data 请求不需要手动设置 Content-Type header
295
- # requests 会根据 files 参数自动处理 boundary 和 Content-Type
296
- headers = self._get_dynamic_headers(content_type=None, referer="https://sora.chatgpt.com/library") # Referer from example
297
-
298
- # 尝试上传
299
- return self._try_upload_with_retry(file_path, file_name, mime_type, headers)
300
-
301
- def _try_upload_with_retry(self, file_path, file_name, mime_type, headers, is_retry=False):
302
- """尝试上传图片,失败时可能尝试切换密钥重试"""
303
- # 保存当前的API密钥,确保整个上传过程使用相同的密钥
304
- current_auth_token = self.auth_token
305
-
306
- files = {
307
- 'file': (file_name, open(file_path, 'rb'), mime_type),
308
- 'file_name': (None, file_name) # 第二个字段是文件名
309
- }
310
- try:
311
- response = self.scraper.post(
312
- self.upload_url,
313
- headers=headers,
314
- files=files, # 使用 files 参数上传文件
315
- proxies=self.proxies,
316
- timeout=60 # 上传可能需要更长时间
317
- )
318
- if response.status_code == 200:
319
- result = response.json()
320
- if self.DEBUG:
321
- print(f"图片上传成功! Media ID: {result.get('id')}")
322
- # 确保返回的结果中包含上传时使用的API密钥信息
323
- result['used_auth_token'] = current_auth_token
324
- # print(f"上传响应: {json.dumps(result, indent=2)}") # 可选:打印完整响应
325
- return result # 返回包含id, url等信息的字典
326
- else:
327
- error_msg = f"上传图片失败,状态码: {response.status_code}, 响应: {response.text}"
328
- if self.DEBUG:
329
- print(error_msg)
330
-
331
- # 如果不是已经在重试,且响应表明可能是API密钥问题,尝试切换密钥
332
- if not is_retry and (response.status_code in [401, 403] or "auth" in response.text.lower() or "token" in response.text.lower()):
333
- if self.DEBUG:
334
- print(f"上传失败可能与API密钥有关,尝试切换密钥后重试")
335
-
336
- try:
337
- from .key_manager import key_manager
338
- new_key = key_manager.mark_key_invalid(self.auth_token)
339
- if new_key:
340
- self.auth_token = new_key
341
- if self.DEBUG:
342
- print(f"已切换到新的API密钥,重试上传")
343
- # 更新头部信息并重试
344
- new_headers = self._get_dynamic_headers(content_type=None, referer="https://sora.chatgpt.com/library")
345
- return self._try_upload_with_retry(file_path, file_name, mime_type, new_headers, is_retry=True)
346
- except (ImportError, Exception) as e:
347
- if self.DEBUG:
348
- print(f"切换API密钥失败: {str(e)}")
349
-
350
- return error_msg
351
- except Exception as e:
352
- error_msg = f"上传图片时出错: {str(e)}"
353
- if self.DEBUG:
354
- print(error_msg)
355
-
356
- # 如果不是已经在重试,尝试切换密钥后重试
357
- if not is_retry:
358
- if self.DEBUG:
359
- print(f"上传过程中发生异常,尝试切换API密钥后重试")
360
-
361
- try:
362
- from .key_manager import key_manager
363
- new_key = key_manager.mark_key_invalid(self.auth_token)
364
- if new_key:
365
- self.auth_token = new_key
366
- if self.DEBUG:
367
- print(f"已切换到新的API密钥,重试上传")
368
- # 更新头部信息并重试
369
- new_headers = self._get_dynamic_headers(content_type=None, referer="https://sora.chatgpt.com/library")
370
- return self._try_upload_with_retry(file_path, file_name, mime_type, new_headers, is_retry=True)
371
- except (ImportError, Exception) as err:
372
- if self.DEBUG:
373
- print(f"切换API密钥失败: {str(err)}")
374
-
375
- return error_msg
376
- finally:
377
- # 确保文件句柄被关闭
378
- if 'file' in files and files['file'][1]:
379
- files['file'][1].close()
380
-
381
- def generate_image_remix(self, prompt, uploaded_media_id, num_images=1, width=None, height=None):
382
- """
383
- 基于已上传的图片进行重混(Remix)生成新图片
384
- 参数:
385
- prompt (str): 图片生成提示词
386
- uploaded_media_id (str): 通过 upload_image 获取的媒体ID (例如 "media_...")
387
- num_images (int): 要生成的图片数量
388
- width (int, optional): 输出图片宽度。如果为None,可能由API决定。
389
- height (int, optional): 输出图片高度。如果为None,可能由API决定。
390
- 返回:
391
- list[str] or str: 成功时返回包含图片URL的列表,失败时返回错误信息字符串
392
- """
393
- if self.DEBUG:
394
- print(f"开始 Remix 图片 (ID: {uploaded_media_id}),提示词: '{prompt}'")
395
-
396
- # 如果上传图片时使用了特定API密钥,则确保使用同一个密钥进行remix
397
- # 在upload_image的返回结果中可能包含used_auth_token
398
- if isinstance(uploaded_media_id, dict) and 'id' in uploaded_media_id:
399
- if 'used_auth_token' in uploaded_media_id and uploaded_media_id['used_auth_token'] != self.auth_token:
400
- if self.DEBUG:
401
- print(f"检测到上传图片时使用了不同的API密钥,切换到匹配的密钥进行Remix操作")
402
- self.auth_token = uploaded_media_id['used_auth_token']
403
- uploaded_media_id = uploaded_media_id['id']
404
-
405
- # 获取上传图片的信息,特别是原始尺寸,如果未指定输出尺寸则可能需要
406
- # (这里简化,假设API能处理尺寸或���们强制指定)
407
- # 实际应用中可能需要先查询 media_id 的详情
408
- payload = {
409
- "prompt": prompt,
410
- "n_variants": num_images,
411
- "inpaint_items": [
412
- {
413
- "type": "image",
414
- "frame_index": 0,
415
- "preset_id": None,
416
- "generation_id": None,
417
- "upload_media_id": uploaded_media_id, # 关键:引用上传的图片
418
- "source_start_frame": 0,
419
- "source_end_frame": 0,
420
- "crop_bounds": None
421
- }
422
- ],
423
- "operation": "remix", # 关键:操作类型
424
- "type": "image_gen",
425
- "n_frames": 1,
426
- "width": 720, # 可选,如果为None,API可能会基于输入调整
427
- "height": 480 # 可选
428
- }
429
- # 只有当width和height都提供了值时才添加到payload中
430
- if width is not None:
431
- payload["width"] = width
432
- if height is not None:
433
- payload["height"] = height
434
- try:
435
- # 提交任务时使用 library 作为 referer,与示例一致
436
- task_id = self._submit_task(payload, referer="https://sora.chatgpt.com/library")
437
- if not task_id:
438
- # 任务提交失败,尝试切换密钥后重试
439
- if self.DEBUG:
440
- print(f"Remix任务提交失败,尝试切换API密钥后重试")
441
-
442
- try:
443
- # 导入在这里进行以避免循环导入
444
- from .key_manager import key_manager
445
- # 标记当前密钥为无效并获取新密钥
446
- new_key = key_manager.mark_key_invalid(self.auth_token)
447
- if new_key:
448
- self.auth_token = new_key
449
- if self.DEBUG:
450
- print(f"已切换到新的API密钥,重试Remix任务")
451
- # 使用新密钥重试提交任务
452
- task_id = self._submit_task(payload, referer="https://sora.chatgpt.com/library")
453
- if not task_id:
454
- return "Remix任务提交失败(已尝试切换密钥)"
455
- else:
456
- return "Remix任务提交失败(无可用的备用密钥)"
457
- except ImportError:
458
- if self.DEBUG:
459
- print(f"无法导入key_manager,无法自动切换密钥")
460
- return "Remix任务提交失败"
461
- except Exception as e:
462
- if self.DEBUG:
463
- print(f"切换API密钥时发生错误: {str(e)}")
464
- return f"Remix任务提交失败: {str(e)}"
465
-
466
- if self.DEBUG:
467
- print(f"Remix 任务已提交,ID: {task_id}")
468
- # 轮询检查任务状态
469
- image_urls = self._poll_task_status(task_id)
470
-
471
- # 如果轮询返回错误信息,也尝试切换密钥重试
472
- if isinstance(image_urls, str) and ("失败" in image_urls or "错误" in image_urls or "error" in image_urls.lower()):
473
- if self.DEBUG:
474
- print(f"Remix任务执行失败,尝试切换API密钥后重试")
475
-
476
- try:
477
- from .key_manager import key_manager
478
- new_key = key_manager.mark_key_invalid(self.auth_token)
479
- if new_key:
480
- self.auth_token = new_key
481
- if self.DEBUG:
482
- print(f"已切换到新的API密钥,重试整个Remix过程")
483
- # 使用新密钥重试整个生成过程
484
- return self.generate_image_remix(prompt, uploaded_media_id, num_images, width, height)
485
- except (ImportError, Exception) as e:
486
- if self.DEBUG:
487
- print(f"切换API密钥失败或重试失败: {str(e)}")
488
-
489
- # 增加图片本地化支持
490
- if image_urls and isinstance(image_urls, list):
491
- if Config.IMAGE_LOCALIZATION:
492
- if self.DEBUG:
493
- print(f"正在本地化 Remix 生成的 {len(image_urls)} 张图片...")
494
- # 创建事件循环并运行图片本地化
495
- loop = asyncio.new_event_loop()
496
- asyncio.set_event_loop(loop)
497
- localized_urls = loop.run_until_complete(localize_image_urls(image_urls))
498
- loop.close()
499
-
500
- if self.DEBUG:
501
- print(f"Remix 图片本地化完成")
502
- return localized_urls
503
-
504
- return image_urls
505
- except Exception as e:
506
- # 如果发生异常,也尝试切换密钥重试
507
- if self.DEBUG:
508
- print(f"Remix生成过程中发生异常: {str(e)},尝试切换API密钥")
509
-
510
- try:
511
- from .key_manager import key_manager
512
- new_key = key_manager.mark_key_invalid(self.auth_token)
513
- if new_key:
514
- self.auth_token = new_key
515
- if self.DEBUG:
516
- print(f"已切换到新的API密钥,重试整个Remix过程")
517
- # 使用新密钥重试整个生成过程
518
- return self.generate_image_remix(prompt, uploaded_media_id, num_images, width, height)
519
- except (ImportError, Exception) as err:
520
- if self.DEBUG:
521
- print(f"切换API密钥失败或重试失败: {str(err)}")
522
-
523
- return f"Remix 生成图片时出错: {str(e)}"
524
-
525
- def _submit_task(self, payload, referer="https://sora.chatgpt.com/explore"):
526
- """提交生成任务 (通用,接受payload字典)"""
527
- headers = self._get_dynamic_headers(content_type="application/json", referer=referer)
528
-
529
- # 获取当前的auth_token用于最后可能的释放
530
- current_auth_token = self.auth_token
531
-
532
- try:
533
- # 检查是否可以导入key_manager并标记密钥为工作中
534
- try:
535
- from .key_manager import key_manager
536
- # 生成一个临时任务ID,用于标记密钥工作状态
537
- temp_task_id = f"pending_task_{self._generate_random_id()}"
538
- key_manager.mark_key_as_working(self.auth_token, temp_task_id)
539
- except ImportError:
540
- if self.DEBUG:
541
- print(f"无法导入key_manager,无法标记密钥工作状态")
542
- except Exception as e:
543
- if self.DEBUG:
544
- print(f"标记密钥工作状态时发生错误: {str(e)}")
545
-
546
- response = self.scraper.post(
547
- self.gen_url,
548
- headers=headers,
549
- json=payload,
550
- proxies=self.proxies,
551
- timeout=20 # 增加超时时间
552
- )
553
- if response.status_code == 200:
554
- try:
555
- result = response.json()
556
- task_id = result.get("id")
557
- if task_id:
558
- # 更新任务ID为实际分配的ID
559
- try:
560
- from .key_manager import key_manager
561
- # 更新工作中状态的任务ID
562
- key_manager.release_key(self.auth_token) # 先释放临时ID
563
- key_manager.mark_key_as_working(self.auth_token, task_id) # 用真实ID重新标记
564
- if self.DEBUG:
565
- print(f"已将密钥标记为工作中,任务ID: {task_id}")
566
- except (ImportError, Exception) as e:
567
- if self.DEBUG:
568
- print(f"更新密钥工作状态时发生错误: {str(e)}")
569
-
570
- return task_id
571
- else:
572
- # 任务提交成功但未返回ID,释放密钥
573
- try:
574
- from .key_manager import key_manager
575
- key_manager.release_key(self.auth_token)
576
- if self.DEBUG:
577
- print(f"任务提交未返回ID,已释放密钥")
578
- except (ImportError, Exception) as e:
579
- if self.DEBUG:
580
- print(f"释放密钥时发生错误: {str(e)}")
581
-
582
- # 检查响应中是否有可能表明API密钥问题的信息
583
- response_text = response.text.lower()
584
- is_auth_issue = False
585
- auth_keywords = ["authorization", "auth", "token", "permission", "unauthorized", "credentials", "login"]
586
-
587
- for keyword in auth_keywords:
588
- if keyword in response_text:
589
- is_auth_issue = True
590
- break
591
-
592
- if is_auth_issue:
593
- if self.DEBUG:
594
- print(f"API响应内容表明可能存在认证问题,尝试切换密钥")
595
-
596
- try:
597
- from .key_manager import key_manager
598
- new_key = key_manager.mark_key_invalid(self.auth_token)
599
- if new_key:
600
- self.auth_token = new_key
601
- if self.DEBUG:
602
- print(f"已切换到新的API密钥,重试请求")
603
- # 使用新密钥重试请求
604
- return self._submit_task(payload, referer)
605
- except ImportError:
606
- if self.DEBUG:
607
- print(f"无法导入key_manager,无法自动切换密钥")
608
- except Exception as e:
609
- if self.DEBUG:
610
- print(f"切换API密钥时发生错误: {str(e)}")
611
-
612
- if self.DEBUG:
613
- print(f"提交任务成功,但响应中未找到任务ID。响应: {response.text}")
614
- return None
615
- except json.JSONDecodeError:
616
- # 释放密钥
617
- try:
618
- from .key_manager import key_manager
619
- key_manager.release_key(self.auth_token)
620
- if self.DEBUG:
621
- print(f"JSON解析失败,已释放密钥")
622
- except (ImportError, Exception) as e:
623
- if self.DEBUG:
624
- print(f"释放密钥时发生错误: {str(e)}")
625
-
626
- if self.DEBUG:
627
- print(f"提交任务成功,但无法解析响应JSON。状态码: {response.status_code}, 响应: {response.text}")
628
- return None
629
- else:
630
- # 释放密钥
631
- try:
632
- from .key_manager import key_manager
633
- key_manager.release_key(self.auth_token)
634
- if self.DEBUG:
635
- print(f"请求失败,已释放密钥")
636
- except (ImportError, Exception) as e:
637
- if self.DEBUG:
638
- print(f"释放密钥时发生错误: {str(e)}")
639
-
640
- if self.DEBUG:
641
- print(f"提交任务失败,状态码: {response.status_code}")
642
- if self.DEBUG:
643
- print(f"请求Payload: {json.dumps(payload)}")
644
- if self.DEBUG:
645
- print(f"响应内容: {response.text}")
646
-
647
- # 特殊处理429错误(太多并发任务)
648
- if response.status_code == 429:
649
- response_text = response.text.lower()
650
- is_concurrent_issue = (
651
- "concurrent" in response_text or
652
- "too many" in response_text or
653
- "wait" in response_text or
654
- "progress" in response_text
655
- )
656
-
657
- if is_concurrent_issue:
658
- if self.DEBUG:
659
- print(f"检测到并发限制错误,当前密钥正在处理其他任务,获取新密钥但不禁用当前密钥")
660
- try:
661
- from .key_manager import key_manager
662
- # 不标记为无效,但获取一个新的可用密钥
663
- new_key = key_manager.get_key()
664
- if new_key:
665
- self.auth_token = new_key
666
- if self.DEBUG:
667
- print(f"已获取新的API密钥,重试请求")
668
- # 使用新密钥重试请求
669
- return self._submit_task(payload, referer)
670
- else:
671
- if self.DEBUG:
672
- print(f"没有可用的备用密钥")
673
- except (ImportError, Exception) as e:
674
- if self.DEBUG:
675
- print(f"获取新密钥时发生错误: {str(e)}")
676
- return None
677
-
678
- # 检查是否是认证失败(401/403)或其他可能表明API密钥失效的情况
679
- response_text = response.text.lower()
680
- is_auth_issue = (
681
- response.status_code in [401, 403] or
682
- "authorization" in response_text or
683
- "auth" in response_text or
684
- "token" in response_text or
685
- "permission" in response_text or
686
- "unauthorized" in response_text or
687
- "credentials" in response_text or
688
- "login" in response_text or
689
- "invalid" in response_text
690
- ) and not (
691
- # 排除并发限制导致的错误
692
- "concurrent" in response_text or
693
- "too many" in response_text or
694
- "wait" in response_text or
695
- "progress" in response_text
696
- )
697
-
698
- if is_auth_issue:
699
- if self.DEBUG:
700
- print(f"API密钥可能已失效,尝试切换密钥")
701
-
702
- try:
703
- # 导入在这里进行以避免循环导入
704
- from .key_manager import key_manager
705
- # 标记当前密钥为无效并获取新密钥
706
- new_key = key_manager.mark_key_invalid(self.auth_token)
707
- if new_key:
708
- self.auth_token = new_key
709
- if self.DEBUG:
710
- print(f"已切换到新的API密钥,重试请求")
711
- # 使用新密钥重试请求
712
- return self._submit_task(payload, referer)
713
- except ImportError:
714
- if self.DEBUG:
715
- print(f"无法导入key_manager,无法自动切换密钥")
716
- except Exception as e:
717
- if self.DEBUG:
718
- print(f"切换API密钥时发生错误: {str(e)}")
719
-
720
- return None
721
- except Exception as e:
722
- # 确保释放密钥
723
- try:
724
- from .key_manager import key_manager
725
- key_manager.release_key(current_auth_token)
726
- if self.DEBUG:
727
- print(f"发生异常,已释放密钥")
728
- except (ImportError, Exception) as release_err:
729
- if self.DEBUG:
730
- print(f"释放密钥时发生错误: {str(release_err)}")
731
-
732
- if self.DEBUG:
733
- print(f"提交任务时出错: {str(e)}")
734
-
735
- # 检查异常信息中是否包含可能的认证问题
736
- error_str = str(e).lower()
737
- is_auth_issue = (
738
- "authorization" in error_str or
739
- "auth" in error_str or
740
- "token" in error_str or
741
- "permission" in error_str or
742
- "unauthorized" in error_str or
743
- "credentials" in error_str or
744
- "login" in error_str
745
- )
746
-
747
- if is_auth_issue:
748
- if self.DEBUG:
749
- print(f"异常信息表明可能存在API密钥问题,尝试切换密钥")
750
-
751
- try:
752
- from .key_manager import key_manager
753
- new_key = key_manager.mark_key_invalid(self.auth_token)
754
- if new_key:
755
- self.auth_token = new_key
756
- if self.DEBUG:
757
- print(f"已切换到新的API密钥,重试请求")
758
- # 使用新密钥重试请求
759
- return self._submit_task(payload, referer)
760
- except (ImportError, Exception) as err:
761
- if self.DEBUG:
762
- print(f"切换API密钥失败: {str(err)}")
763
-
764
- return None
765
-
766
- def _poll_task_status(self, task_id, max_attempts=40, interval=5):
767
- """
768
- 轮询检查任务状态,直到完成,返回所有生成的图片URL列表
769
- """
770
- # 保存当前使用的密钥,确保最后可以正确释放
771
- current_auth_token = self.auth_token
772
-
773
- if self.DEBUG:
774
- print(f"开始轮询任务 {task_id} 的状态...")
775
- try:
776
- for attempt in range(max_attempts):
777
- try:
778
- headers = self._get_dynamic_headers(referer="https://sora.chatgpt.com/library") # Polling often happens from library view
779
- query_url = f"{self.check_url}?limit=10" # 获取最近的任务,减少数据量
780
- response = self.scraper.get(
781
- query_url,
782
- headers=headers,
783
- proxies=self.proxies,
784
- timeout=15
785
- )
786
- if response.status_code == 200:
787
- try:
788
- result = response.json()
789
- task_responses = result.get("task_responses", [])
790
- # 查找对应的任务
791
- for task in task_responses:
792
- if task.get("id") == task_id:
793
- status = task.get("status")
794
- if self.DEBUG:
795
- print(f" 任务 {task_id} 状态: {status} (尝试 {attempt+1}/{max_attempts})")
796
- if status == "succeeded":
797
- # 任务成功完成,释放密钥
798
- try:
799
- from .key_manager import key_manager
800
- key_manager.release_key(current_auth_token)
801
- if self.DEBUG:
802
- print(f"任务成功完成,已释放密钥")
803
- except (ImportError, Exception) as e:
804
- if self.DEBUG:
805
- print(f"释放密钥时发生错误: {str(e)}")
806
-
807
- generations = task.get("generations", [])
808
- image_urls = []
809
- if generations:
810
- for gen in generations:
811
- url = gen.get("url")
812
- if url:
813
- image_urls.append(url)
814
- if image_urls:
815
- if self.DEBUG:
816
- print(f"任务 {task_id} 成功完成!找到 {len(image_urls)} 张图片。")
817
- return image_urls
818
- else:
819
- if self.DEBUG:
820
- print(f"任务 {task_id} 状态为 succeeded,但在响应中未找到有效的图片URL。")
821
- if self.DEBUG:
822
- print(f"任务详情: {json.dumps(task, indent=2)}")
823
- return "任务成功但未找到图片URL"
824
- elif status == "failed":
825
- # 任务失败,释放密钥
826
- try:
827
- from .key_manager import key_manager
828
- key_manager.release_key(current_auth_token)
829
- if self.DEBUG:
830
- print(f"任务失败,已释放密钥")
831
- except (ImportError, Exception) as e:
832
- if self.DEBUG:
833
- print(f"释放密钥时发生错误: {str(e)}")
834
-
835
- failure_reason = task.get("failure_reason", "未知原因")
836
- if self.DEBUG:
837
- print(f"任务 {task_id} 失败: {failure_reason}")
838
- # 检查是否是因为API密钥问题导致的失败
839
- failure_reason_lower = failure_reason.lower()
840
- auth_keywords = [
841
- "authorization", "auth", "token", "permission",
842
- "unauthorized", "credentials", "login", "invalid"
843
- ]
844
- is_auth_issue = any(keyword in failure_reason_lower for keyword in auth_keywords)
845
-
846
- if is_auth_issue:
847
- if self.DEBUG:
848
- print(f"检测到API密钥可能失效,尝试切换密钥")
849
- try:
850
- # 导入在这里进行以避免循环导入
851
- from .key_manager import key_manager
852
- # 标记当前密钥为无效并获取新密钥
853
- new_key = key_manager.mark_key_invalid(self.auth_token)
854
- if new_key:
855
- self.auth_token = new_key
856
- if self.DEBUG:
857
- print(f"已切换到新的API密钥")
858
- except ImportError:
859
- if self.DEBUG:
860
- print(f"无法导入key_manager,无法自动切换密钥")
861
- except Exception as e:
862
- if self.DEBUG:
863
- print(f"切换API密钥时发生错误: {str(e)}")
864
- return f"任务失败: {failure_reason}"
865
- elif status in ["rejected", "needs_user_review"]:
866
- # 任务被拒绝,释放密钥
867
- try:
868
- from .key_manager import key_manager
869
- key_manager.release_key(current_auth_token)
870
- if self.DEBUG:
871
- print(f"任务被拒绝,已释放密钥")
872
- except (ImportError, Exception) as e:
873
- if self.DEBUG:
874
- print(f"释放密钥时发生错误: {str(e)}")
875
-
876
- if self.DEBUG:
877
- print(f"任务 {task_id} 被拒绝或需要审查: {status}")
878
- return f"任务被拒绝或需审查: {status}"
879
- # else status is pending, processing, etc. - continue polling
880
- break # Found the task, no need to check others in this response
881
- else:
882
- # Task ID not found in the recent list, maybe it's older or just submitted
883
- if self.DEBUG:
884
- print(f" 未在最近任务列表中找到 {task_id},继续等待... (尝试 {attempt+1}/{max_attempts})")
885
- except json.JSONDecodeError:
886
- if self.DEBUG:
887
- print(f"检查任务状态时无法解析响应JSON。状态码: {response.status_code}, 响应: {response.text}")
888
- else:
889
- if self.DEBUG:
890
- print(f"检查任务状态失败,状态码: {response.status_code}, 响应: {response.text}")
891
-
892
- # 检查是否是认证失败或其他可能表明API密钥失效的情况
893
- response_text = response.text.lower()
894
- is_auth_issue = (
895
- response.status_code in [401, 403, 429] or
896
- "authorization" in response_text or
897
- "auth" in response_text or
898
- "token" in response_text or
899
- "permission" in response_text or
900
- "unauthorized" in response_text or
901
- "credentials" in response_text or
902
- "login" in response_text or
903
- "invalid" in response_text
904
- )
905
-
906
- if is_auth_issue:
907
- if self.DEBUG:
908
- print(f"API密钥可能已失效,尝试切换密钥")
909
-
910
- try:
911
- # 导入在这里进行以避免循环导入
912
- from .key_manager import key_manager
913
- # 标记当前密钥为无效并获取新密钥
914
- new_key = key_manager.mark_key_invalid(self.auth_token)
915
- if new_key:
916
- self.auth_token = new_key
917
- if self.DEBUG:
918
- print(f"已切换到新的API密钥,重试请求")
919
- # 使用新密钥继续轮询
920
- continue
921
- except ImportError:
922
- if self.DEBUG:
923
- print(f"无法导入key_manager,无法自动切换密钥")
924
- except Exception as e:
925
- if self.DEBUG:
926
- print(f"切换API密钥时发生错误: {str(e)}")
927
-
928
- time.sleep(interval) # 等待一段时间后再次检查
929
- except Exception as e:
930
- if self.DEBUG:
931
- print(f"检查任务状态时出错: {str(e)}")
932
-
933
- # 检查异常信息中是否包含可能的认证问题
934
- error_str = str(e).lower()
935
- is_auth_issue = (
936
- "authorization" in error_str or
937
- "auth" in error_str or
938
- "token" in error_str or
939
- "permission" in error_str or
940
- "unauthorized" in error_str or
941
- "credentials" in error_str or
942
- "login" in error_str
943
- )
944
-
945
- if is_auth_issue:
946
- if self.DEBUG:
947
- print(f"异常信息表明可能存在API密钥问题,尝试切换密钥")
948
-
949
- try:
950
- from .key_manager import key_manager
951
- new_key = key_manager.mark_key_invalid(self.auth_token)
952
- if new_key:
953
- self.auth_token = new_key
954
- if self.DEBUG:
955
- print(f"已切换到新的API密钥,重试请求")
956
- # 重置当前尝试次数,继续轮询
957
- continue
958
- except (ImportError, Exception) as err:
959
- if self.DEBUG:
960
- print(f"切换API密钥失败: {str(err)}")
961
-
962
- # Add a slightly longer delay on error to avoid hammering the server
963
- time.sleep(interval * 1.5)
964
-
965
- # 如果达到最大尝试次数,释放密钥
966
- try:
967
- from .key_manager import key_manager
968
- key_manager.release_key(current_auth_token)
969
- if self.DEBUG:
970
- print(f"轮询超时,已释放密钥")
971
- except (ImportError, Exception) as e:
972
- if self.DEBUG:
973
- print(f"释放密钥时发生错误: {str(e)}")
974
-
975
- return f"任务 {task_id} 超时 ({max_attempts * interval}秒),未能获取最终状态"
976
- except Exception as e:
977
- # 确保在异常情况下也释放密钥
978
- try:
979
- from .key_manager import key_manager
980
- key_manager.release_key(current_auth_token)
981
- if self.DEBUG:
982
- print(f"轮询过程发生异常,已释放密钥")
983
- except (ImportError, Exception) as release_err:
984
- if self.DEBUG:
985
- print(f"释放密钥时发生错误: {str(release_err)}")
986
-
987
- if self.DEBUG:
988
- print(f"轮询任务状态时发生未处理的异常: {str(e)}")
989
- import traceback
990
- traceback.print_exc()
991
- return f"轮询任务状态时出错: {str(e)}"
992
-
993
- def test_connection(self):
994
- """
995
- 测试API连接是否有效,仅发送一个轻量级请求
996
- 返回:
997
- dict: 包含连接状态信息的字典
998
- """
999
- start_time = time.time() # 记录开始时间,用于计算响应时间
1000
- success = False # 初始化请求结果标识
1001
-
1002
- try:
1003
- # 使用简单的GET请求来验证连接和认证
1004
- headers = self._get_dynamic_headers(referer="https://sora.chatgpt.com/explore")
1005
- response = self.scraper.get(
1006
- "https://sora.chatgpt.com/backend/parameters",
1007
- headers=headers,
1008
- proxies=self.proxies,
1009
- timeout=10
1010
- )
1011
-
1012
- if response.status_code == 200:
1013
- result = response.json()
1014
- # 检查返回的数据中是否含有关键字段,确认API确实有效
1015
- api_valid = result.get("can_create_images") is not None or "limits_for_images" in result
1016
-
1017
- if api_valid:
1018
- success = True # 标记请求成功
1019
- # 记录请求结果(成功)
1020
- try:
1021
- from .key_manager import key_manager
1022
- response_time = time.time() - start_time
1023
- key_manager.record_request_result(self.auth_token, success, response_time)
1024
- except (ImportError, Exception) as e:
1025
- if self.DEBUG:
1026
- print(f"记录请求结果失败: {str(e)}")
1027
-
1028
- return {
1029
- "status": "success",
1030
- "message": "API连接测试成功",
1031
- "data": result
1032
- }
1033
- else:
1034
- # API返回200但数据不符合预期
1035
- success = False # 标记请求失败
1036
-
1037
- # 记录请求结果(失败)
1038
- try:
1039
- from .key_manager import key_manager
1040
- response_time = time.time() - start_time
1041
- key_manager.record_request_result(self.auth_token, success, response_time)
1042
- except (ImportError, Exception) as e:
1043
- if self.DEBUG:
1044
- print(f"记录请求结果失败: {str(e)}")
1045
-
1046
- return {
1047
- "status": "error",
1048
- "message": "API连接测试失败:返回数据格式不符合预期",
1049
- "response": result
1050
- }
1051
- else:
1052
- # 检查是否是认证失败或其他可能表明API密钥失效的情况
1053
- response_text = response.text.lower()
1054
- is_auth_issue = (
1055
- response.status_code in [401, 403, 429] or
1056
- "authorization" in response_text or
1057
- "auth" in response_text or
1058
- "token" in response_text or
1059
- "permission" in response_text or
1060
- "unauthorized" in response_text or
1061
- "credentials" in response_text or
1062
- "login" in response_text or
1063
- "invalid" in response_text
1064
- )
1065
-
1066
- success = False # 标记请求失败
1067
-
1068
- # 记录请求结果(失败)
1069
- try:
1070
- from .key_manager import key_manager
1071
- response_time = time.time() - start_time
1072
- key_manager.record_request_result(self.auth_token, success, response_time)
1073
- except (ImportError, Exception) as e:
1074
- if self.DEBUG:
1075
- print(f"记录请求结果失败: {str(e)}")
1076
-
1077
- if is_auth_issue:
1078
- if self.DEBUG:
1079
- print(f"API密钥可能已失效,尝试切换密钥")
1080
-
1081
- try:
1082
- # 导入在这里进行以避免循环导入
1083
- from .key_manager import key_manager
1084
- # 标记当前密钥为无效并获取新密钥
1085
- new_key = key_manager.mark_key_invalid(self.auth_token)
1086
- if new_key:
1087
- self.auth_token = new_key
1088
- if self.DEBUG:
1089
- print(f"已切换到新的API密钥,重试连接测试")
1090
- # 使用新密钥重试
1091
- return self.test_connection()
1092
- except ImportError:
1093
- if self.DEBUG:
1094
- print(f"无法导入key_manager,无法自动切换密钥")
1095
- except Exception as e:
1096
- if self.DEBUG:
1097
- print(f"切换API密钥时发生错误: {str(e)}")
1098
-
1099
- return {
1100
- "status": "error",
1101
- "message": f"API连接测试失败,状态码: {response.status_code}",
1102
- "response": response.text
1103
- }
1104
- except Exception as e:
1105
- success = False # 标记请求失败
1106
-
1107
- # 记录请求结果(异常失败)
1108
- try:
1109
- from .key_manager import key_manager
1110
- response_time = time.time() - start_time
1111
- key_manager.record_request_result(self.auth_token, success, response_time)
1112
- except (ImportError, Exception) as err:
1113
- if self.DEBUG:
1114
- print(f"记录请求结果失败: {str(err)}")
1115
-
1116
- # 检查异常信息中是否包含可能的认证问题
1117
- error_str = str(e).lower()
1118
- is_auth_issue = (
1119
- "authorization" in error_str or
1120
- "auth" in error_str or
1121
- "token" in error_str or
1122
- "permission" in error_str or
1123
- "unauthorized" in error_str or
1124
- "credentials" in error_str or
1125
- "login" in error_str
1126
- )
1127
-
1128
- if is_auth_issue:
1129
- if self.DEBUG:
1130
- print(f"异常信息表明可能存在API密钥问题,尝试切换密钥")
1131
-
1132
- try:
1133
- from .key_manager import key_manager
1134
- new_key = key_manager.mark_key_invalid(self.auth_token)
1135
- if new_key:
1136
- self.auth_token = new_key
1137
- if self.DEBUG:
1138
- print(f"已切换到新的API密钥,重试连接测试")
1139
- # 使用新密钥重试
1140
- return self.test_connection()
1141
- except (ImportError, Exception) as err:
1142
- if self.DEBUG:
1143
- print(f"切换API密钥失败: {str(err)}")
1144
-
1145
- raise Exception(f"API连接测试失败: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/sora_integration.py DELETED
@@ -1,114 +0,0 @@
1
- import asyncio
2
- import concurrent.futures
3
- from typing import List, Dict, Any, Optional, Union
4
- import json
5
- import os
6
- import base64
7
- import tempfile
8
- import uuid
9
-
10
- # 导入原有的SoraImageGenerator类
11
- from .sora_generator import SoraImageGenerator
12
-
13
- class SoraClient:
14
- def __init__(self, proxy_host=None, proxy_port=None, proxy_user=None, proxy_pass=None, auth_token=None):
15
- """初始化Sora客户端,使用cloudscraper绕过CF验证"""
16
- self.generator = SoraImageGenerator(
17
- proxy_host=proxy_host,
18
- proxy_port=proxy_port,
19
- proxy_user=proxy_user,
20
- proxy_pass=proxy_pass,
21
- auth_token=auth_token
22
- )
23
- # 保存原始的auth_token,用于检测是否已更新
24
- self.auth_token = auth_token
25
- # 创建线程池执行器
26
- self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
27
-
28
- async def generate_image(self, prompt: str, num_images: int = 1,
29
- width: int = 720, height: int = 480) -> List[str]:
30
- """异步包装SoraImageGenerator.generate_image方法"""
31
- loop = asyncio.get_running_loop()
32
- # 使用线程池执行同步方法(因为cloudscraper不是异步的)
33
- result = await loop.run_in_executor(
34
- self.executor,
35
- lambda: self.generator.generate_image(prompt, num_images, width, height)
36
- )
37
-
38
- # 检查generator中的auth_token是否已经被更新(由自动切换密钥机制)
39
- if self.generator.auth_token != self.auth_token:
40
- self.auth_token = self.generator.auth_token
41
-
42
- if isinstance(result, list):
43
- return result
44
- else:
45
- raise Exception(f"图像生成失败: {result}")
46
-
47
- async def upload_image(self, image_path: str) -> Dict:
48
- """异步包装上传图片方法"""
49
- loop = asyncio.get_running_loop()
50
- result = await loop.run_in_executor(
51
- self.executor,
52
- lambda: self.generator.upload_image(image_path)
53
- )
54
-
55
- # 检查generator中的auth_token是否已经被更新
56
- if self.generator.auth_token != self.auth_token:
57
- self.auth_token = self.generator.auth_token
58
-
59
- if isinstance(result, dict) and 'id' in result:
60
- return result
61
- else:
62
- raise Exception(f"图片上传失败: {result}")
63
-
64
- async def generate_image_remix(self, prompt: str, media_id: str,
65
- num_images: int = 1) -> List[str]:
66
- """异步包装remix方法"""
67
- loop = asyncio.get_running_loop()
68
-
69
- # 处理可能包含API密钥信息的media_id对象
70
- if isinstance(media_id, dict) and 'id' in media_id:
71
- # 如果上传时使用的密钥与当前不同,则先切换密钥
72
- if 'used_auth_token' in media_id and media_id['used_auth_token'] != self.auth_token:
73
- self.auth_token = media_id['used_auth_token']
74
- # 同步更新generator的auth_token
75
- self.generator.auth_token = self.auth_token
76
- # 提取实际的media_id
77
- media_id = media_id['id']
78
-
79
- result = await loop.run_in_executor(
80
- self.executor,
81
- lambda: self.generator.generate_image_remix(prompt, media_id, num_images)
82
- )
83
-
84
- # 检查generator中的auth_token是否已经被更新
85
- if self.generator.auth_token != self.auth_token:
86
- self.auth_token = self.generator.auth_token
87
-
88
- if isinstance(result, list):
89
- return result
90
- else:
91
- raise Exception(f"Remix生成失败: {result}")
92
-
93
- async def test_connection(self) -> Dict:
94
- """测试API连接是否有效"""
95
- try:
96
- # 简单测试上传功能,这个方法会调用API但不会真正上传文件
97
- loop = asyncio.get_running_loop()
98
- result = await loop.run_in_executor(
99
- self.executor,
100
- lambda: self.generator.test_connection()
101
- )
102
-
103
- # 检查generator中的auth_token是否已经被更新
104
- if self.generator.auth_token != self.auth_token:
105
- self.auth_token = self.generator.auth_token
106
-
107
- # 直接返回generator.test_connection的结果,保留所有信息
108
- return result
109
- except Exception as e:
110
- return {"status": "error", "message": f"API连接测试失败: {str(e)}"}
111
-
112
- def close(self):
113
- """关闭线程池"""
114
- self.executor.shutdown(wait=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/static/admin/config.html DELETED
@@ -1,99 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Sora API - 配置管理</title>
7
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
- <link href="css/admin.css" rel="stylesheet">
9
- </head>
10
- <body>
11
- <div class="container">
12
- <h1 class="my-4">Sora API - 配置管理</h1>
13
-
14
- <div id="messages" class="mb-4"></div>
15
-
16
- <div class="card mb-4">
17
- <div class="card-header">
18
- <h5 class="mb-0">系统配置</h5>
19
- </div>
20
- <div class="card-body">
21
- <form id="configForm">
22
- <div class="mb-3">
23
- <label class="form-label fw-bold">HTTP代理设置</label>
24
- <div class="row g-3">
25
- <div class="col-md-6">
26
- <label for="proxy_host" class="form-label">代理主机</label>
27
- <input type="text" class="form-control" id="proxy_host" placeholder="例如: 127.0.0.1">
28
- </div>
29
- <div class="col-md-6">
30
- <label for="proxy_port" class="form-label">代理端口</label>
31
- <input type="text" class="form-control" id="proxy_port" placeholder="例如: 7890">
32
- </div>
33
- </div>
34
- <div class="mt-3">
35
- <label class="form-label">代理认证 (可选)</label>
36
- <div class="row g-3">
37
- <div class="col-md-6">
38
- <label for="proxy_user" class="form-label">用户名</label>
39
- <input type="text" class="form-control" id="proxy_user" placeholder="代理用户名">
40
- </div>
41
- <div class="col-md-6">
42
- <label for="proxy_pass" class="form-label">密码</label>
43
- <input type="password" class="form-control" id="proxy_pass" placeholder="代理密码">
44
- </div>
45
- </div>
46
- </div>
47
- <div class="form-text text-muted mt-2">如需使用代理访问Sora API,请填写以上信息,留空表示不使用代理</div>
48
- </div>
49
-
50
- <hr>
51
-
52
- <div class="mb-3">
53
- <label class="form-label fw-bold">图片本地化设置</label>
54
- <div class="form-check form-switch">
55
- <input class="form-check-input" type="checkbox" id="image_localization">
56
- <label class="form-check-label" for="image_localization">启用图片本地化</label>
57
- </div>
58
- <div class="form-text text-muted">启用后,Sora生成的图片将被下载并保存到本地服务器,避免客户端无法访问外部链接的问题</div>
59
- </div>
60
-
61
- <div class="mb-3">
62
- <label for="image_save_dir" class="form-label">图片保存目录</label>
63
- <input type="text" class="form-control" id="image_save_dir" placeholder="src/static/images">
64
- <div class="form-text text-muted">相对于工作目录的路径,必须确保目录存在且有写入权限</div>
65
- </div>
66
-
67
- <button type="button" id="saveConfig" class="btn btn-primary">保存配置</button>
68
- </form>
69
- </div>
70
- </div>
71
-
72
- <div class="card mb-4">
73
- <div class="card-header">
74
- <h5 class="mb-0">帮助说明</h5>
75
- </div>
76
- <div class="card-body">
77
- <p><strong>HTTP代理设置:</strong></p>
78
- <ul>
79
- <li>如果您的服务器无法直接访问Sora API,可以配置HTTP代理</li>
80
- <li>代理认证是可选的,如果您的代理服务器不需要认证,请留空</li>
81
- <li>在Docker环境中,代理主机通常设置为<code>host.docker.internal</code>而不是<code>127.0.0.1</code></li>
82
- </ul>
83
-
84
- <p><strong>图片本地化功能说明:</strong></p>
85
- <ul>
86
- <li>本功能解决Sora图片URL无法访问的问题,将生成的图片保存到本地服务器</li>
87
- <li>启用后,系统会自动下载Sora返回的图片并存储到指定目录</li>
88
- <li>API返回的图片链接将替换为本地URL地址</li>
89
- <li>确保���置的保存目录有足够的磁盘空间和访问权限</li>
90
- </ul>
91
- <p class="text-warning"><strong>注意:</strong> 配置变更会立即生效,但不会影响已经生成的图片</p>
92
- </div>
93
- </div>
94
- </div>
95
-
96
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
97
- <script src="js/config.js"></script>
98
- </body>
99
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/static/admin/css/style.css DELETED
@@ -1,424 +0,0 @@
1
- /* 全局样式 */
2
- :root {
3
- --sidebar-width: 250px;
4
- --sidebar-bg: #343a40;
5
- --sidebar-color: #fff;
6
- --primary-color: #0d6efd;
7
- --success-color: #198754;
8
- --warning-color: #ffc107;
9
- --danger-color: #dc3545;
10
- --info-color: #0dcaf0;
11
- --transition-speed: 0.3s;
12
- }
13
-
14
- body {
15
- font-family: 'Segoe UI', 'Microsoft YaHei', 'Arial', sans-serif;
16
- background-color: #f8f9fa;
17
- overflow-x: hidden;
18
- margin: 0;
19
- padding: 0;
20
- min-height: 100vh;
21
- }
22
-
23
- /* 登录页面样式 */
24
- #login-container, .login-container {
25
- background-color: #f8f9fa;
26
- position: fixed;
27
- top: 0;
28
- left: 0;
29
- width: 100%;
30
- height: 100vh;
31
- z-index: 1050;
32
- align-items: center;
33
- justify-content: center;
34
- }
35
-
36
- #login-container .card {
37
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
38
- border-radius: 15px;
39
- overflow: hidden;
40
- width: 400px;
41
- max-width: 90%;
42
- }
43
-
44
- #login-container .card-header {
45
- background-color: var(--primary-color);
46
- padding: 1.5rem 1rem;
47
- }
48
-
49
- #login-container .bi-shield-lock {
50
- color: var(--primary-color);
51
- }
52
-
53
- #login-form .btn-primary {
54
- background-color: var(--primary-color);
55
- border-color: var(--primary-color);
56
- padding: 0.6rem 1.2rem;
57
- font-weight: 500;
58
- }
59
-
60
- #login-form .btn-primary:hover {
61
- background-color: #0b5ed7;
62
- border-color: #0a58ca;
63
- }
64
-
65
- #login-error {
66
- border-radius: 8px;
67
- }
68
-
69
- /* 页面布局 */
70
- .wrapper {
71
- display: flex;
72
- width: 100%;
73
- min-height: 100vh;
74
- align-items: stretch;
75
- position: relative;
76
- }
77
-
78
- /* 侧边栏样式 */
79
- #sidebar {
80
- min-width: 250px;
81
- max-width: 250px;
82
- background: var(--sidebar-bg);
83
- color: var(--sidebar-color);
84
- transition: all var(--transition-speed);
85
- height: 100vh;
86
- position: fixed;
87
- z-index: 999;
88
- box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
89
- top: 0;
90
- left: 0;
91
- }
92
-
93
- #sidebar.active {
94
- margin-left: -250px;
95
- }
96
-
97
- #sidebar .sidebar-header {
98
- padding: 20px;
99
- background: #212529;
100
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
101
- }
102
-
103
- #sidebar ul.components {
104
- padding: 20px 0;
105
- height: calc(100vh - 140px);
106
- overflow-y: auto;
107
- }
108
-
109
- #sidebar ul li a {
110
- padding: 12px 20px;
111
- font-size: 1.1em;
112
- display: block;
113
- color: #fff;
114
- text-decoration: none;
115
- transition: all 0.3s;
116
- border-left: 3px solid transparent;
117
- }
118
-
119
- #sidebar ul li a:hover {
120
- background: #495057;
121
- border-left: 3px solid var(--primary-color);
122
- }
123
-
124
- #sidebar ul li.active > a {
125
- background: var(--primary-color);
126
- color: white;
127
- border-left: 3px solid #fff;
128
- }
129
-
130
- #sidebar ul li.sidebar-footer {
131
- margin-top: auto;
132
- border-top: 1px solid rgba(255, 255, 255, 0.1);
133
- position: absolute;
134
- bottom: 0;
135
- width: 100%;
136
- }
137
-
138
- #sidebar ul li.sidebar-footer a {
139
- color: rgba(255, 255, 255, 0.8);
140
- }
141
-
142
- #sidebar ul li.sidebar-footer a:hover {
143
- background: rgba(220, 53, 69, 0.6);
144
- color: white;
145
- }
146
-
147
- /* 主内容区样式 */
148
- #content {
149
- width: calc(100% - 250px);
150
- padding: 0;
151
- min-height: 100vh;
152
- transition: all 0.3s;
153
- position: absolute;
154
- right: 0;
155
- overflow-x: hidden;
156
- }
157
-
158
- #content.active {
159
- width: 100%;
160
- }
161
-
162
- /* 内容页面样式 */
163
- .content-page {
164
- display: none;
165
- padding: 0 20px 30px;
166
- }
167
-
168
- .content-page.active {
169
- display: block;
170
- animation: fadeIn 0.3s;
171
- }
172
-
173
- /* 表格样式 */
174
- .table th {
175
- font-weight: 600;
176
- border-top: none;
177
- white-space: nowrap;
178
- }
179
-
180
- .table td {
181
- vertical-align: middle;
182
- }
183
-
184
- /* 状态标签样式 */
185
- .status-badge {
186
- display: inline-block;
187
- padding: 0.25em 0.6em;
188
- font-size: 85%;
189
- font-weight: 600;
190
- line-height: 1;
191
- text-align: center;
192
- white-space: nowrap;
193
- vertical-align: baseline;
194
- border-radius: 0.25rem;
195
- cursor: help;
196
- }
197
-
198
- .status-enabled {
199
- background-color: var(--success-color);
200
- color: white;
201
- }
202
-
203
- .status-disabled {
204
- background-color: var(--danger-color);
205
- color: white;
206
- }
207
-
208
- .status-temp-disabled {
209
- background-color: var(--warning-color);
210
- color: black;
211
- position: relative;
212
- }
213
-
214
- /* 为临时禁用状态的提示信息添加样式 */
215
- .small.text-muted {
216
- font-size: 0.75rem;
217
- display: block;
218
- margin-top: 3px;
219
- }
220
-
221
- /* 让状态标签支持鼠标悬停提示 */
222
- .status-badge:hover {
223
- background-color: #ffd700;
224
- color: black;
225
- }
226
-
227
- /* 响应式调整 */
228
- @media (max-width: 768px) {
229
- #sidebar {
230
- margin-left: -250px;
231
- }
232
- #sidebar.active {
233
- margin-left: 0;
234
- }
235
- #content {
236
- width: 100%;
237
- }
238
- #content.active {
239
- width: calc(100% - 250px);
240
- }
241
- #sidebarCollapse {
242
- display: block;
243
- }
244
- .navbar .container-fluid {
245
- padding-left: 10px;
246
- padding-right: 10px;
247
- }
248
- }
249
-
250
- /* 卡片样式美化 */
251
- .card {
252
- border: none;
253
- border-radius: 10px;
254
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
255
- margin-bottom: 20px;
256
- overflow: hidden;
257
- }
258
-
259
- .card-header {
260
- background-color: #fff;
261
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
262
- padding: 15px 20px;
263
- font-weight: 500;
264
- }
265
-
266
- .card-body {
267
- padding: 20px;
268
- }
269
-
270
- /* 按钮样式 */
271
- .btn-primary {
272
- background-color: var(--primary-color);
273
- border-color: var(--primary-color);
274
- }
275
-
276
- .btn-primary:hover {
277
- background-color: #0b5ed7;
278
- border-color: #0a58ca;
279
- }
280
-
281
- .btn-success {
282
- background-color: var(--success-color);
283
- border-color: var(--success-color);
284
- }
285
-
286
- .btn-danger {
287
- background-color: var(--danger-color);
288
- border-color: var(--danger-color);
289
- }
290
-
291
- /* 表单元素样式 */
292
- .form-control:focus {
293
- border-color: var(--primary-color);
294
- box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
295
- }
296
-
297
- /* 操作按钮样式 */
298
- .action-btn {
299
- margin-right: 5px;
300
- display: inline-flex;
301
- align-items: center;
302
- justify-content: center;
303
- width: 2.2rem;
304
- height: 2.2rem;
305
- padding: 0;
306
- border-radius: 50%;
307
- }
308
-
309
- .action-btn i {
310
- font-size: 0.9rem;
311
- }
312
-
313
- /* 密钥值样式 */
314
- .key-value {
315
- max-width: 180px;
316
- overflow: hidden;
317
- text-overflow: ellipsis;
318
- white-space: nowrap;
319
- font-family: monospace;
320
- font-size: 0.9rem;
321
- }
322
-
323
- /* 统计卡片样式 */
324
- .card.bg-primary, .card.bg-success, .card.bg-danger, .card.bg-info {
325
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
326
- transition: transform 0.3s ease;
327
- }
328
-
329
- .card.bg-primary:hover, .card.bg-success:hover, .card.bg-danger:hover, .card.bg-info:hover {
330
- transform: translateY(-5px);
331
- }
332
-
333
- .card.bg-primary .card-title, .card.bg-success .card-title,
334
- .card.bg-danger .card-title, .card.bg-info .card-title {
335
- font-size: 1rem;
336
- opacity: 0.9;
337
- }
338
-
339
- .card.bg-primary .card-text, .card.bg-success .card-text,
340
- .card.bg-danger .card-text, .card.bg-info .card-text {
341
- font-size: 1.8rem;
342
- font-weight: 600;
343
- }
344
-
345
- /* 图表容器 */
346
- canvas {
347
- width: 100% !important;
348
- height: 300px !important;
349
- }
350
-
351
- /* 导航栏样式 */
352
- .navbar {
353
- box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
354
- background-color: #fff !important;
355
- }
356
-
357
- /* 管理员密钥显示 */
358
- #admin-key-display {
359
- font-size: 0.8rem;
360
- font-family: monospace;
361
- }
362
-
363
- /* Toast通知样式 */
364
- .toast {
365
- border-radius: 8px;
366
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
367
- }
368
-
369
- /* 动画效果 */
370
- @keyframes fadeIn {
371
- from { opacity: 0; }
372
- to { opacity: 1; }
373
- }
374
-
375
- /* 最近的API密钥列表样式 */
376
- #recent-keys-list .list-group-item {
377
- padding: 0.75rem 1rem;
378
- border-left: none;
379
- border-right: none;
380
- }
381
-
382
- #recent-keys-list .list-group-item:hover {
383
- background-color: rgba(13, 110, 253, 0.05);
384
- }
385
-
386
- /* 模态框样式 */
387
- .modal-content {
388
- border-radius: 12px;
389
- border: none;
390
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
391
- overflow: hidden;
392
- }
393
-
394
- .modal-header {
395
- border-bottom: 1px solid rgba(0, 0, 0, 0.05);
396
- }
397
-
398
- .modal-footer {
399
- border-top: 1px solid rgba(0, 0, 0, 0.05);
400
- }
401
-
402
- /* 分页样式 */
403
- .pagination {
404
- margin-bottom: 0;
405
- }
406
-
407
- .page-link {
408
- color: var(--primary-color);
409
- border-radius: 4px;
410
- margin: 0 2px;
411
- }
412
-
413
- .page-item.active .page-link {
414
- background-color: var(--primary-color);
415
- border-color: var(--primary-color);
416
- }
417
-
418
- /* 搜索框样式 */
419
- #search-keys {
420
- min-width: 250px;
421
- border-radius: 20px;
422
- padding-left: 15px;
423
- padding-right: 15px;
424
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/static/admin/index.html DELETED
@@ -1,512 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Sora API - 管理控制台</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
9
- <link rel="stylesheet" href="/static/admin/css/style.css">
10
- </head>
11
- <body>
12
- <!-- 登录页面 -->
13
- <div id="login-container" style="display: flex;" class="login-container">
14
- <div class="card shadow-lg">
15
- <div class="card-header bg-primary text-white text-center py-3">
16
- <h4>Sora API 管理控制台</h4>
17
- </div>
18
- <div class="card-body p-4">
19
- <div class="text-center mb-4">
20
- <i class="bi bi-shield-lock" style="font-size: 3rem;"></i>
21
- <h5 class="mt-2">管理员登录</h5>
22
- </div>
23
- <form id="login-form">
24
- <div class="mb-3">
25
- <label for="admin-key-input" class="form-label">管理员密钥</label>
26
- <input type="password" class="form-control" id="admin-key-input" required>
27
- <div class="form-text">请输入管理员密钥以访问管理控制台</div>
28
- </div>
29
- <div class="d-grid gap-2">
30
- <button type="submit" class="btn btn-primary">登录</button>
31
- </div>
32
- </form>
33
- <div id="login-error" class="alert alert-danger mt-3" style="display: none;"></div>
34
- </div>
35
- </div>
36
- </div>
37
-
38
- <!-- 管理控制台 -->
39
- <div id="admin-panel" style="display: none;" class="wrapper">
40
- <!-- 侧边栏导航 -->
41
- <nav id="sidebar">
42
- <div class="sidebar-header d-flex align-items-center">
43
- <h3 class="mb-0">Sora API</h3>
44
- </div>
45
-
46
- <ul class="list-unstyled components">
47
- <li class="active">
48
- <a href="#" data-page="dashboard"><i class="bi bi-speedometer2 me-2"></i>仪表盘</a>
49
- </li>
50
- <li>
51
- <a href="#" data-page="keys"><i class="bi bi-key-fill me-2"></i>API密钥管理</a>
52
- </li>
53
- <li>
54
- <a href="#" data-page="stats"><i class="bi bi-graph-up me-2"></i>使用统计</a>
55
- </li>
56
- <li>
57
- <a href="#" data-page="settings"><i class="bi bi-gear-fill me-2"></i>系统设置</a>
58
- </li>
59
- <li class="sidebar-footer">
60
- <a href="#" id="logout-btn"><i class="bi bi-box-arrow-right me-2"></i>退出登录</a>
61
- </li>
62
- </ul>
63
- </nav>
64
-
65
- <!-- 页面内容区 -->
66
- <div id="content">
67
- <!-- 顶部导航栏 -->
68
- <nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm mb-4">
69
- <div class="container-fluid">
70
- <button type="button" id="sidebarCollapse" class="btn btn-primary">
71
- <i class="bi bi-list"></i>
72
- </button>
73
- <div class="ms-3 d-none d-md-block">
74
- <h5 class="mb-0" id="current-page-title">仪表盘</h5>
75
- </div>
76
- <div class="ms-auto d-flex align-items-center">
77
- <span class="badge bg-primary me-3" id="admin-key-display"></span>
78
- <span class="text-muted d-none d-md-block">欢迎回来,管理员</span>
79
- </div>
80
- </div>
81
- </nav>
82
-
83
- <!-- 仪表盘页面 -->
84
- <div id="dashboard-page" class="content-page active container-fluid">
85
- <div class="row mb-4">
86
- <div class="col-12">
87
- <h4 class="mb-4">系统概览</h4>
88
- </div>
89
- <div class="col-md-6 col-lg-3 mb-4">
90
- <div class="card bg-primary text-white h-100">
91
- <div class="card-body">
92
- <h5 class="card-title">总API密钥</h5>
93
- <h3 id="total-keys" class="card-text">--</h3>
94
- </div>
95
- </div>
96
- </div>
97
- <div class="col-md-6 col-lg-3 mb-4">
98
- <div class="card bg-success text-white h-100">
99
- <div class="card-body">
100
- <h5 class="card-title">激活密钥</h5>
101
- <h3 id="active-keys" class="card-text">--</h3>
102
- </div>
103
- </div>
104
- </div>
105
- <div class="col-md-6 col-lg-3 mb-4">
106
- <div class="card bg-danger text-white h-100">
107
- <div class="card-body">
108
- <h5 class="card-title">禁用密钥</h5>
109
- <h3 id="disabled-keys" class="card-text">--</h3>
110
- </div>
111
- </div>
112
- </div>
113
- <div class="col-md-6 col-lg-3 mb-4">
114
- <div class="card bg-info text-white h-100">
115
- <div class="card-body">
116
- <h5 class="card-title">今日请求</h5>
117
- <h3 id="today-requests" class="card-text">--</h3>
118
- </div>
119
- </div>
120
- </div>
121
- </div>
122
-
123
- <div class="row">
124
- <div class="col-lg-8 mb-4">
125
- <div class="card h-100">
126
- <div class="card-header">
127
- <h5 class="card-title mb-0">近期请求趋势</h5>
128
- </div>
129
- <div class="card-body">
130
- <canvas id="dashboard-requests-chart"></canvas>
131
- </div>
132
- </div>
133
- </div>
134
- <div class="col-lg-4 mb-4">
135
- <div class="card h-100">
136
- <div class="card-header">
137
- <h5 class="card-title mb-0">最新API密钥</h5>
138
- </div>
139
- <div class="card-body p-0">
140
- <div class="list-group list-group-flush" id="recent-keys-list">
141
- <!-- JS动态加载 -->
142
- </div>
143
- </div>
144
- <div class="card-footer text-end">
145
- <a href="#" class="btn btn-sm btn-primary" data-page="keys">查看全部</a>
146
- </div>
147
- </div>
148
- </div>
149
- </div>
150
- </div>
151
-
152
- <!-- API密钥管理页面 -->
153
- <div id="keys-page" class="content-page container-fluid">
154
- <div class="row mb-4">
155
- <div class="col-12">
156
- <div class="card">
157
- <div class="card-body">
158
- <div class="d-flex justify-content-between flex-wrap mb-3">
159
- <div class="mb-2">
160
- <button id="add-key-btn" class="btn btn-primary me-2">
161
- <i class="bi bi-plus-circle me-1"></i> 添加密钥
162
- </button>
163
- <div class="btn-group">
164
- <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
165
- 批量操作
166
- </button>
167
- <ul class="dropdown-menu">
168
- <li><a class="dropdown-item batch-action" data-action="enable" href="#">启用选中</a></li>
169
- <li><a class="dropdown-item batch-action" data-action="disable" href="#">禁用选中</a></li>
170
- <li><hr class="dropdown-divider"></li>
171
- <li><a class="dropdown-item batch-action text-danger" data-action="delete" href="#">删除选中</a></li>
172
- </ul>
173
- </div>
174
- <button id="import-keys-btn" class="btn btn-outline-primary ms-2">
175
- <i class="bi bi-upload me-1"></i> 批量导入
176
- </button>
177
- </div>
178
- <div class="mb-2">
179
- <input type="text" id="search-keys" class="form-control" placeholder="搜索密钥...">
180
- </div>
181
- </div>
182
-
183
- <div class="table-responsive">
184
- <table class="table table-hover">
185
- <thead>
186
- <tr>
187
- <th><input type="checkbox" id="select-all"></th>
188
- <th>名称</th>
189
- <th>密钥值</th>
190
- <th>状��</th>
191
- <th>权重</th>
192
- <th>速率限制</th>
193
- <th>创建时间</th>
194
- <th>最后使用</th>
195
- <th>操作</th>
196
- </tr>
197
- </thead>
198
- <tbody id="keys-table-body">
199
- <!-- JS动态加载 -->
200
- </tbody>
201
- </table>
202
- </div>
203
-
204
- <div id="pagination" class="d-flex justify-content-center mt-4">
205
- <!-- JS动态加载 -->
206
- </div>
207
- </div>
208
- </div>
209
- </div>
210
- </div>
211
- </div>
212
-
213
- <!-- 使用统计页面 -->
214
- <div id="stats-page" class="content-page container-fluid">
215
- <div class="row">
216
- <div class="col-12">
217
- <h4 class="mb-4">使用统计</h4>
218
- </div>
219
- <div class="col-md-6 col-lg-3 mb-4">
220
- <div class="card bg-primary text-white">
221
- <div class="card-body">
222
- <h5 class="card-title">总请求次数</h5>
223
- <h3 id="total-requests" class="card-text">--</h3>
224
- </div>
225
- </div>
226
- </div>
227
- <div class="col-md-6 col-lg-3 mb-4">
228
- <div class="card bg-success text-white">
229
- <div class="card-body">
230
- <h5 class="card-title">成功请求</h5>
231
- <h3 id="successful-requests" class="card-text">--</h3>
232
- </div>
233
- </div>
234
- </div>
235
- <div class="col-md-6 col-lg-3 mb-4">
236
- <div class="card bg-danger text-white">
237
- <div class="card-body">
238
- <h5 class="card-title">失败请求</h5>
239
- <h3 id="failed-requests" class="card-text">--</h3>
240
- </div>
241
- </div>
242
- </div>
243
- <div class="col-md-6 col-lg-3 mb-4">
244
- <div class="card bg-info text-white">
245
- <div class="card-body">
246
- <h5 class="card-title">成功率</h5>
247
- <h3 id="success-rate" class="card-text">--</h3>
248
- </div>
249
- </div>
250
- </div>
251
- </div>
252
-
253
- <div class="row">
254
- <!-- 请求趋势图 -->
255
- <div class="col-lg-8 mb-4">
256
- <div class="card">
257
- <div class="card-header">
258
- <h5 class="card-title mb-0">请求趋势</h5>
259
- </div>
260
- <div class="card-body">
261
- <canvas id="requests-chart"></canvas>
262
- </div>
263
- </div>
264
- </div>
265
-
266
- <!-- 密钥使用分布 -->
267
- <div class="col-lg-4 mb-4">
268
- <div class="card">
269
- <div class="card-header">
270
- <h5 class="card-title mb-0">密钥使用分布</h5>
271
- </div>
272
- <div class="card-body">
273
- <canvas id="keys-usage-chart"></canvas>
274
- </div>
275
- </div>
276
- </div>
277
- </div>
278
- </div>
279
-
280
- <!-- 设置页面 -->
281
- <div id="settings-page" class="content-page container-fluid">
282
- <div class="row">
283
- <div class="col-12">
284
- <h4 class="mb-4">系统设置</h4>
285
- </div>
286
- <div class="col-md-6 mb-4">
287
- <div class="card">
288
- <div class="card-header">
289
- <h5 class="card-title mb-0">管理员密钥</h5>
290
- </div>
291
- <div class="card-body">
292
- <div class="mb-3">
293
- <label for="admin-key" class="form-label">管理员密钥</label>
294
- <div class="input-group">
295
- <input type="password" class="form-control" id="admin-key" readonly>
296
- <button class="btn btn-outline-secondary" type="button" id="show-admin-key">
297
- <i class="bi bi-eye"></i>
298
- </button>
299
- <button class="btn btn-outline-secondary" type="button" id="copy-admin-key">
300
- <i class="bi bi-clipboard"></i>
301
- </button>
302
- </div>
303
- <div class="form-text">此密钥用于访问管理界面,请妥善保管</div>
304
- </div>
305
- </div>
306
- </div>
307
- </div>
308
-
309
- <div class="col-md-6 mb-4">
310
- <div class="card">
311
- <div class="card-header">
312
- <h5 class="card-title mb-0">系统配置</h5>
313
- </div>
314
- <div class="card-body">
315
- <form id="settings-form">
316
- <div class="mb-3">
317
- <label for="proxy-host" class="form-label">代理主机</label>
318
- <input type="text" class="form-control" id="proxy-host">
319
- </div>
320
- <div class="mb-3">
321
- <label for="proxy-port" class="form-label">代理端口</label>
322
- <input type="text" class="form-control" id="proxy-port">
323
- </div>
324
-
325
- <div class="mb-3">
326
- <label for="proxy-user" class="form-label">代理用户名</label>
327
- <input type="text" class="form-control" id="proxy-user">
328
- </div>
329
-
330
- <div class="mb-3">
331
- <label for="proxy-pass" class="form-label">代理密码</label>
332
- <input type="password" class="form-control" id="proxy-pass">
333
- <div class="form-text">如果代理服务器需要认证,请填写用户名和密码</div>
334
- </div>
335
-
336
- <hr>
337
- <h6 class="mb-3">图片本地化设置</h6>
338
-
339
- <div class="mb-3 form-check">
340
- <input type="checkbox" class="form-check-input" id="image-localization">
341
- <label class="form-check-label" for="image-localization">启用图片本地化</label>
342
- <div class="form-text">启用后,Sora生成的图片将被下载并保存到本地服务器,避免客户端无法访问外部链接</div>
343
- </div>
344
-
345
- <div class="mb-3">
346
- <label for="base-url" class="form-label">Base URL</label>
347
- <input type="text" class="form-control" id="base-url" placeholder="http://example.com">
348
- <div class="form-text">设置访问本地化图片的基础URL,例如:http://example.com 或 https://your-domain.com</div>
349
- </div>
350
-
351
- <div class="mb-3">
352
- <label for="image-save-dir" class="form-label">图片保存目录</label>
353
- <input type="text" class="form-control" id="image-save-dir" placeholder="src/static/images">
354
- <div class="form-text">相对于工作目录的路径,必须确保目录存在且有写入权限</div>
355
- </div>
356
-
357
- <button type="submit" class="btn btn-primary">保存设置</button>
358
- </form>
359
- </div>
360
- </div>
361
- </div>
362
- </div>
363
- </div>
364
- </div>
365
- </div>
366
-
367
- <!-- 添加/编辑密钥对话框 -->
368
- <div class="modal fade" id="keyModal" tabindex="-1">
369
- <div class="modal-dialog">
370
- <div class="modal-content">
371
- <div class="modal-header">
372
- <h5 class="modal-title" id="keyModalLabel">添加新密钥</h5>
373
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
374
- </div>
375
- <div class="modal-body">
376
- <form id="key-form">
377
- <input type="hidden" id="key-id">
378
- <div class="mb-3">
379
- <label for="key-name" class="form-label">密钥名称</label>
380
- <input type="text" class="form-control" id="key-name" required>
381
- </div>
382
- <div class="mb-3">
383
- <label for="key-value" class="form-label">密钥值 (Bearer Token)</label>
384
- <input type="text" class="form-control" id="key-value" required>
385
- <div class="form-text">输入OpenAI格式的API密钥,无需添加"Bearer "前缀,系统会自动处理</div>
386
- </div>
387
- <div class="mb-3">
388
- <label for="key-weight" class="form-label">权重 (1-10)</label>
389
- <input type="number" class="form-control" id="key-weight" min="1" max="10" value="1">
390
- <div class="form-text">权重越高,被选中的概率越大</div>
391
- </div>
392
- <div class="mb-3">
393
- <label for="key-rate-limit" class="form-label">速率限制 (每分钟请求数)</label>
394
- <input type="number" class="form-control" id="key-rate-limit" min="1" value="60">
395
- </div>
396
- <div class="mb-3 form-check">
397
- <input type="checkbox" class="form-check-input" id="key-enabled" checked>
398
- <label class="form-check-label" for="key-enabled">启用</label>
399
- </div>
400
- <div class="mb-3">
401
- <label for="key-notes" class="form-label">备注</label>
402
- <textarea class="form-control" id="key-notes" rows="2"></textarea>
403
- </div>
404
- </form>
405
- </div>
406
- <div class="modal-footer">
407
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
408
- <button type="button" class="btn btn-primary" id="test-key-btn">测试连接</button>
409
- <button type="button" class="btn btn-success" id="save-key-btn">保存</button>
410
- </div>
411
- </div>
412
- </div>
413
- </div>
414
-
415
- <!-- 确认对话框 -->
416
- <div class="modal fade" id="confirmModal" tabindex="-1">
417
- <div class="modal-dialog">
418
- <div class="modal-content">
419
- <div class="modal-header">
420
- <h5 class="modal-title" id="confirmModalLabel">确认操作</h5>
421
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
422
- </div>
423
- <div class="modal-body" id="confirm-message">
424
- 确定要执行此操作吗?
425
- </div>
426
- <div class="modal-footer">
427
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
428
- <button type="button" class="btn btn-danger" id="confirm-btn">确认</button>
429
- </div>
430
- </div>
431
- </div>
432
- </div>
433
-
434
- <!-- 批量导入密钥对话框 -->
435
- <div class="modal fade" id="importKeysModal" tabindex="-1">
436
- <div class="modal-dialog modal-lg">
437
- <div class="modal-content">
438
- <div class="modal-header">
439
- <h5 class="modal-title">批量导入API密钥</h5>
440
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
441
- </div>
442
- <div class="modal-body">
443
- <ul class="nav nav-tabs" id="importTabs" role="tablist">
444
- <li class="nav-item" role="presentation">
445
- <button class="nav-link active" id="paste-tab" data-bs-toggle="tab" data-bs-target="#paste-content" type="button" role="tab">粘贴内容</button>
446
- </li>
447
- <li class="nav-item" role="presentation">
448
- <button class="nav-link" id="file-tab" data-bs-toggle="tab" data-bs-target="#file-upload" type="button" role="tab">上传文件</button>
449
- </li>
450
- </ul>
451
- <div class="tab-content mt-3" id="importTabsContent">
452
- <div class="tab-pane fade show active" id="paste-content" role="tabpanel">
453
- <div class="mb-3">
454
- <label for="keys-text" class="form-label">API密钥列表</label>
455
- <textarea class="form-control" id="keys-text" rows="10" placeholder="支持两种格式:&#10;1. 每行一个密钥&#10;2. 每行格式:[名称],[API密钥],[权重(可选)],[速率限制(可选)]&#10;&#10;例如:&#10;sk-abcdefg123456&#10;我的密钥2,sk-hijklmn789012,5,100"></textarea>
456
- </div>
457
- </div>
458
- <div class="tab-pane fade" id="file-upload" role="tabpanel">
459
- <div class="mb-3">
460
- <label for="keys-file" class="form-label">选择文件</label>
461
- <input class="form-control" type="file" id="keys-file" accept=".txt,.csv">
462
- <div class="form-text">支持TXT或CSV文件,每行格式同上</div>
463
- </div>
464
- </div>
465
- </div>
466
- <div class="form-check mt-3">
467
- <input class="form-check-input" type="checkbox" id="auto-enable-keys" checked>
468
- <label class="form-check-label" for="auto-enable-keys">
469
- 自动启用导入的密钥
470
- </label>
471
- </div>
472
- <div class="alert alert-info mt-3">
473
- <i class="bi bi-info-circle me-2"></i> 批量导入将跳过重复的密钥值。如果名称重复但密钥值不同,将作为新密钥添加。
474
- </div>
475
- <div id="import-preview" class="mt-3" style="display: none;">
476
- <h6>导入预览 (<span id="preview-count">0</span>个密钥)</h6>
477
- <div class="table-responsive">
478
- <table class="table table-sm">
479
- <thead>
480
- <tr>
481
- <th>名称</th>
482
- <th>密钥</th>
483
- <th>权重</th>
484
- <th>速率限制</th>
485
- </tr>
486
- </thead>
487
- <tbody id="preview-table-body">
488
- </tbody>
489
- </table>
490
- </div>
491
- </div>
492
- </div>
493
- <div class="modal-footer">
494
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
495
- <button type="button" class="btn btn-primary" id="preview-import-btn">预览</button>
496
- <button type="button" class="btn btn-success" id="confirm-import-btn" disabled>导入</button>
497
- </div>
498
- </div>
499
- </div>
500
- </div>
501
-
502
- <!-- Toast通知 -->
503
- <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 5">
504
- <div id="toast-container"></div>
505
- </div>
506
-
507
- <!-- JavaScript库 -->
508
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
509
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
510
- <script src="/static/admin/js/main.js"></script>
511
- </body>
512
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/static/admin/js/config.js DELETED
@@ -1,121 +0,0 @@
1
- document.addEventListener('DOMContentLoaded', function() {
2
- // 获取当前配置
3
- fetchConfig();
4
-
5
- // 保存配置按钮事件
6
- document.getElementById('saveConfig').addEventListener('click', saveConfig);
7
- });
8
-
9
- // 获取当前配置
10
- function fetchConfig() {
11
- fetch('/api/config', {
12
- method: 'GET',
13
- headers: {
14
- 'Authorization': 'Bearer ' + getAdminKey()
15
- }
16
- })
17
- .then(response => {
18
- if (!response.ok) {
19
- throw new Error('获取配置失败,请检查管理员密钥是否正确');
20
- }
21
- return response.json();
22
- })
23
- .then(data => {
24
- // 代理设置
25
- document.getElementById('proxy_host').value = data.PROXY_HOST || '';
26
- document.getElementById('proxy_port').value = data.PROXY_PORT || '';
27
- document.getElementById('proxy_user').value = data.PROXY_USER || '';
28
-
29
- // 代理密码字段
30
- const proxyPassField = document.getElementById('proxy_pass');
31
- if (data.PROXY_PASS && data.PROXY_PASS !== '') {
32
- // 设置代理密码占位符,防止用户修改
33
- proxyPassField.setAttribute('placeholder', '已设置代理密码 (请保留空)');
34
- } else {
35
- proxyPassField.setAttribute('placeholder', '代理密码');
36
- }
37
-
38
- // 图像本地化设置
39
- document.getElementById('image_localization').checked = data.IMAGE_LOCALIZATION || false;
40
- document.getElementById('image_save_dir').value = data.IMAGE_SAVE_DIR || 'src/static/images';
41
- })
42
- .catch(error => {
43
- showMessage('错误', error.message, 'danger');
44
- });
45
- }
46
-
47
- // 保存配置
48
- function saveConfig() {
49
- // 获取代理设置
50
- const proxyHost = document.getElementById('proxy_host').value.trim();
51
- const proxyPort = document.getElementById('proxy_port').value.trim();
52
- const proxyUser = document.getElementById('proxy_user').value.trim();
53
- const proxyPass = document.getElementById('proxy_pass').value.trim();
54
-
55
- // 获取图像本地化设置
56
- const imageLocalization = document.getElementById('image_localization').checked;
57
- const imageSaveDir = document.getElementById('image_save_dir').value.trim();
58
-
59
- // 创建配置对象
60
- const config = {
61
- PROXY_HOST: proxyHost,
62
- PROXY_PORT: proxyPort,
63
- PROXY_USER: proxyUser,
64
- IMAGE_LOCALIZATION: imageLocalization,
65
- IMAGE_SAVE_DIR: imageSaveDir,
66
- save_to_env: true
67
- };
68
-
69
- // 如果代理密码存在,则添加到配置对象中
70
- if (proxyPass) {
71
- config.PROXY_PASS = proxyPass;
72
- }
73
-
74
- fetch('/api/config', {
75
- method: 'POST',
76
- headers: {
77
- 'Content-Type': 'application/json',
78
- 'Authorization': 'Bearer ' + getAdminKey()
79
- },
80
- body: JSON.stringify(config)
81
- })
82
- .then(response => {
83
- if (!response.ok) {
84
- throw new Error('保存配置失败,请检查管理员密钥是否正确');
85
- }
86
- return response.json();
87
- })
88
- .then(data => {
89
- showMessage('成功', '配置已保存', 'success');
90
-
91
- // 成功后,立即重新获取配置以更新显示
92
- setTimeout(fetchConfig, 1000);
93
- })
94
- .catch(error => {
95
- showMessage('错误', error.message, 'danger');
96
- });
97
- }
98
-
99
- // 获取管理员密钥
100
- function getAdminKey() {
101
- return localStorage.getItem('adminKey') || '';
102
- }
103
-
104
- // 显示消息
105
- function showMessage(title, message, type) {
106
- const alertDiv = document.createElement('div');
107
- alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
108
- alertDiv.innerHTML = `
109
- <strong>${title}:</strong> ${message}
110
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
111
- `;
112
-
113
- const messagesContainer = document.getElementById('messages');
114
- messagesContainer.appendChild(alertDiv);
115
-
116
- // 5秒后自动关闭
117
- setTimeout(() => {
118
- alertDiv.classList.remove('show');
119
- setTimeout(() => alertDiv.remove(), 150);
120
- }, 5000);
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/static/admin/js/main.js DELETED
@@ -1,1631 +0,0 @@
1
- // 全局变量
2
- let adminKey = '';
3
- let keysData = [];
4
- let currentPage = 1;
5
- const keysPerPage = 10;
6
- let statsData = null;
7
- let dashboardRequestsChart = null;
8
- let requestsChart = null;
9
- let keysUsageChart = null;
10
- let systemSettings = {
11
- proxyHost: '',
12
- proxyPort: '',
13
- proxyUser: '',
14
- proxyPass: ''
15
- };
16
- // token过期时间
17
- let tokenExpiry = null;
18
- // JWT配置
19
- const JWT_EXPIRATION = 3600; // 令牌有效期1小时(秒)
20
- // 导入预览数据
21
- let importPreviewData = [];
22
-
23
- // DOM 加载完成后执行
24
- document.addEventListener('DOMContentLoaded', () => {
25
- // 检查是否已登录
26
- checkAuth();
27
-
28
- // 绑定登录表单提交事件
29
- document.getElementById('login-form').addEventListener('submit', async (e) => {
30
- e.preventDefault();
31
- await handleLogin();
32
- });
33
-
34
- // 绑定退出登录事件
35
- document.getElementById('logout-btn').addEventListener('click', (e) => {
36
- e.preventDefault();
37
- handleLogout();
38
- });
39
-
40
- // 侧边栏切换
41
- document.getElementById('sidebarCollapse').addEventListener('click', () => {
42
- const sidebar = document.getElementById('sidebar');
43
- const content = document.getElementById('content');
44
-
45
- sidebar.classList.toggle('active');
46
- content.classList.toggle('active');
47
- });
48
-
49
- // 页面切换
50
- document.querySelectorAll('[data-page]').forEach(link => {
51
- link.addEventListener('click', (e) => {
52
- e.preventDefault();
53
- const targetPage = link.getAttribute('data-page');
54
-
55
- // 验证token是否有效
56
- if (!isTokenValid()) {
57
- handleLogout();
58
- showToast('登录已过期,请重新登录', 'warning');
59
- return;
60
- }
61
-
62
- // 更新导航链接激活状态
63
- document.querySelectorAll('#sidebar li').forEach(li => li.classList.remove('active'));
64
- link.closest('li').classList.add('active');
65
-
66
- // 更新页面标题
67
- document.getElementById('current-page-title').textContent = link.textContent.trim();
68
-
69
- // 隐藏所有页面,只显示目标页面
70
- document.querySelectorAll('.content-page').forEach(page => {
71
- page.classList.remove('active');
72
- page.style.display = 'none';
73
- });
74
- document.getElementById(`${targetPage}-page`).classList.add('active');
75
- document.getElementById(`${targetPage}-page`).style.display = 'block';
76
-
77
- // 加载相应页面的数据
78
- switch (targetPage) {
79
- case 'dashboard':
80
- loadDashboard();
81
- break;
82
- case 'keys':
83
- loadKeys();
84
- break;
85
- case 'stats':
86
- loadStats();
87
- break;
88
- case 'settings':
89
- loadSettings();
90
- break;
91
- }
92
- });
93
- });
94
-
95
- // 仪表盘页面的"查看全部"按钮
96
- document.querySelectorAll('#dashboard-page [data-page]').forEach(link => {
97
- link.addEventListener('click', (e) => {
98
- e.preventDefault();
99
- const targetPage = link.getAttribute('data-page');
100
- // 触发相应导航的点击事件
101
- document.querySelector(`#sidebar a[data-page="${targetPage}"]`).click();
102
- });
103
- });
104
-
105
- // 添加密钥按钮事件
106
- document.getElementById('add-key-btn').addEventListener('click', () => {
107
- // 重置表单
108
- document.getElementById('key-form').reset();
109
- document.getElementById('key-id').value = '';
110
- document.getElementById('keyModalLabel').textContent = '添加新密钥';
111
-
112
- // 显示模态框
113
- const keyModal = new bootstrap.Modal(document.getElementById('keyModal'));
114
- keyModal.show();
115
- });
116
-
117
- // 保存密钥按钮事件
118
- document.getElementById('save-key-btn').addEventListener('click', saveKey);
119
-
120
- // 测试密钥按钮事件
121
- document.getElementById('test-key-btn').addEventListener('click', testKey);
122
-
123
- // 全选/取消全选
124
- document.getElementById('select-all').addEventListener('change', (e) => {
125
- const checkboxes = document.querySelectorAll('#keys-table-body input[type="checkbox"]');
126
- checkboxes.forEach(checkbox => checkbox.checked = e.target.checked);
127
- });
128
-
129
- // 批量操作事件
130
- document.querySelectorAll('.batch-action').forEach(action => {
131
- action.addEventListener('click', (e) => {
132
- e.preventDefault();
133
- const actionType = action.getAttribute('data-action');
134
- const selectedIds = getSelectedKeyIds();
135
-
136
- if (selectedIds.length === 0) {
137
- showToast('请至少选择一个密钥', 'warning');
138
- return;
139
- }
140
-
141
- // 显示确认对话框
142
- let message = '';
143
- if (actionType === 'enable') {
144
- message = `确定要启用选中的 ${selectedIds.length} 个密钥吗?`;
145
- } else if (actionType === 'disable') {
146
- message = `确定要禁用选中的 ${selectedIds.length} 个密钥吗?`;
147
- } else if (actionType === 'delete') {
148
- message = `确定要删除选中的 ${selectedIds.length} 个密钥吗?此操作不可恢复!`;
149
- }
150
-
151
- showConfirmDialog(message, () => {
152
- batchOperation(actionType, selectedIds);
153
- });
154
- });
155
- });
156
-
157
- // 搜索框事件
158
- document.getElementById('search-keys').addEventListener('input', (e) => {
159
- const searchTerm = e.target.value.toLowerCase();
160
- if (searchTerm) {
161
- const filteredKeys = keysData.filter(key =>
162
- key.name.toLowerCase().includes(searchTerm) ||
163
- key.key.toLowerCase().includes(searchTerm)
164
- );
165
- renderKeysTable(filteredKeys);
166
- } else {
167
- renderKeysTable(keysData);
168
- }
169
- });
170
-
171
- // 管理员密钥显示/隐藏
172
- document.getElementById('show-admin-key').addEventListener('click', () => {
173
- const adminKeyInput = document.getElementById('admin-key');
174
- if (adminKeyInput.type === 'password') {
175
- adminKeyInput.type = 'text';
176
- document.getElementById('show-admin-key').innerHTML = '<i class="bi bi-eye-slash"></i>';
177
- } else {
178
- adminKeyInput.type = 'password';
179
- document.getElementById('show-admin-key').innerHTML = '<i class="bi bi-eye"></i>';
180
- }
181
- });
182
-
183
- // 复制管理员密钥
184
- document.getElementById('copy-admin-key').addEventListener('click', () => {
185
- const adminKeyInput = document.getElementById('admin-key');
186
- adminKeyInput.type = 'text';
187
- adminKeyInput.select();
188
- document.execCommand('copy');
189
- adminKeyInput.type = 'password';
190
- showToast('管理员密钥已复制到剪贴板', 'success');
191
- });
192
-
193
- // 设置表单提交
194
- document.getElementById('settings-form').addEventListener('submit', async (e) => {
195
- e.preventDefault();
196
- await saveSettings();
197
- });
198
-
199
- // 批量导入密钥按钮事件
200
- document.getElementById('import-keys-btn').addEventListener('click', () => {
201
- // 重置导入表单和预览
202
- document.getElementById('keys-text').value = '';
203
- document.getElementById('keys-file').value = '';
204
- document.getElementById('auto-enable-keys').checked = true;
205
- document.getElementById('import-preview').style.display = 'none';
206
- document.getElementById('confirm-import-btn').disabled = true;
207
- importPreviewData = [];
208
-
209
- // 显示导入模态框
210
- const importModal = new bootstrap.Modal(document.getElementById('importKeysModal'));
211
- importModal.show();
212
- });
213
-
214
- // 预览导入按钮事件
215
- document.getElementById('preview-import-btn').addEventListener('click', previewImport);
216
-
217
- // 确认导入按钮事件
218
- document.getElementById('confirm-import-btn').addEventListener('click', confirmImport);
219
-
220
- // 文件选择事件
221
- document.getElementById('keys-file').addEventListener('change', async (e) => {
222
- if (e.target.files.length > 0) {
223
- const file = e.target.files[0];
224
- const reader = new FileReader();
225
-
226
- reader.onload = function(event) {
227
- document.getElementById('keys-text').value = event.target.result;
228
- };
229
-
230
- reader.readAsText(file);
231
- }
232
- });
233
- });
234
-
235
- // 检查是否已登录
236
- function checkAuth() {
237
- const savedAdminKey = localStorage.getItem('adminKey');
238
- const savedTokenExpiry = localStorage.getItem('tokenExpiry');
239
-
240
- if (savedAdminKey && savedTokenExpiry) {
241
- adminKey = savedAdminKey;
242
- tokenExpiry = parseInt(savedTokenExpiry);
243
-
244
- // 检查token是否已过期
245
- if (isTokenValid()) {
246
- showAdminPanel();
247
- // 初始化其他数据
248
- displayAdminKey();
249
- loadDashboard(); // 初始加载仪表盘页面
250
-
251
- // 如果token即将过期(小于5分钟),则刷新
252
- const fiveMinutesInMs = 5 * 60 * 1000;
253
- if (tokenExpiry - new Date().getTime() < fiveMinutesInMs) {
254
- refreshToken();
255
- }
256
- } else {
257
- // token已过期,清除存储并显示登录面板
258
- handleLogout();
259
- showToast('登录已过期,请重新登录', 'warning');
260
- }
261
- } else {
262
- showLoginPanel();
263
- }
264
- }
265
-
266
- // 处理登录
267
- async function handleLogin() {
268
- const inputKey = document.getElementById('admin-key-input').value.trim();
269
- if (!inputKey) {
270
- showLoginError('请输入管理员密钥');
271
- return;
272
- }
273
-
274
- try {
275
- // 显示加载状态
276
- document.querySelector('#login-form button').disabled = true;
277
- document.querySelector('#login-form button').innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 登录中...';
278
-
279
- // 使用fetch API进行登录请求
280
- const response = await fetch('/api/auth/login', {
281
- method: 'POST',
282
- headers: {
283
- 'Content-Type': 'application/json',
284
- },
285
- body: JSON.stringify({ admin_key: inputKey })
286
- });
287
-
288
- // 恢复按钮状态
289
- document.querySelector('#login-form button').disabled = false;
290
- document.querySelector('#login-form button').innerHTML = '登录';
291
-
292
- if (response.ok) {
293
- const data = await response.json();
294
- adminKey = data.token;
295
- tokenExpiry = new Date().getTime() + (data.expires_in * 1000);
296
-
297
- // 保存至本地存储
298
- localStorage.setItem('adminKey', adminKey);
299
- localStorage.setItem('tokenExpiry', tokenExpiry.toString());
300
-
301
- // 隐藏错误信息
302
- document.getElementById('login-error').style.display = 'none';
303
-
304
- // 显示管理面板
305
- showAdminPanel();
306
- displayAdminKey();
307
- loadDashboard();
308
- } else {
309
- // 登录失败
310
- try {
311
- const error = await response.json();
312
- showLoginError(error.detail || '登录失败,请检查管理员密钥');
313
- } catch {
314
- showLoginError('登录失败,请检查管理员密钥');
315
- }
316
- }
317
- } catch (error) {
318
- // 处理网络错误等异常
319
- document.querySelector('#login-form button').disabled = false;
320
- document.querySelector('#login-form button').innerHTML = '登录';
321
- console.error('验证管理员密钥失败:', error);
322
- showLoginError('验证失败,请稍后再试');
323
- }
324
- }
325
-
326
- // 显示登录错误
327
- function showLoginError(message) {
328
- const errorElement = document.getElementById('login-error');
329
- errorElement.textContent = message;
330
- errorElement.style.display = 'block';
331
-
332
- // 3秒后自动隐藏
333
- setTimeout(() => {
334
- errorElement.style.display = 'none';
335
- }, 3000);
336
- }
337
-
338
- // 处理登出
339
- function handleLogout() {
340
- localStorage.removeItem('adminKey');
341
- localStorage.removeItem('tokenExpiry');
342
- adminKey = '';
343
- tokenExpiry = null;
344
- showLoginPanel();
345
- document.getElementById('admin-key-input').value = '';
346
- }
347
-
348
- // 检查token是否有效
349
- function isTokenValid() {
350
- if (!tokenExpiry) return false;
351
- // 检查token是否过期(留10秒缓冲)
352
- return new Date().getTime() < tokenExpiry - 10000;
353
- }
354
-
355
- // 刷新token
356
- async function refreshToken() {
357
- try {
358
- const response = await fetch('/api/auth/refresh', {
359
- method: 'POST',
360
- headers: {
361
- 'Authorization': `Bearer ${adminKey}`,
362
- 'Content-Type': 'application/json'
363
- }
364
- });
365
-
366
- if (response.ok) {
367
- const data = await response.json();
368
- adminKey = data.token;
369
- tokenExpiry = new Date().getTime() + (data.expires_in * 1000);
370
- localStorage.setItem('adminKey', adminKey);
371
- localStorage.setItem('tokenExpiry', tokenExpiry.toString());
372
- return true;
373
- } else {
374
- return false;
375
- }
376
- } catch (error) {
377
- console.error('刷新token失败:', error);
378
- return false;
379
- }
380
- }
381
-
382
- // 显示登录面板
383
- function showLoginPanel() {
384
- document.getElementById('login-container').style.display = 'flex';
385
- document.getElementById('admin-panel').style.display = 'none';
386
- // 清除可能存在的错误信息
387
- document.getElementById('login-error').style.display = 'none';
388
- }
389
-
390
- // 显示管理面板
391
- function showAdminPanel() {
392
- document.getElementById('login-container').style.display = 'none';
393
- document.getElementById('admin-panel').style.display = 'flex';
394
-
395
- // 确保侧边栏正常显示
396
- document.getElementById('sidebar').classList.remove('active');
397
- document.getElementById('content').classList.remove('active');
398
-
399
- // 初始化显示仪表盘页面,隐藏其他页面
400
- document.querySelectorAll('.content-page').forEach(page => {
401
- page.classList.remove('active');
402
- page.style.display = 'none';
403
- });
404
- document.getElementById('dashboard-page').classList.add('active');
405
- document.getElementById('dashboard-page').style.display = 'block';
406
-
407
- // 更新导航栏状态
408
- document.querySelectorAll('#sidebar li').forEach(li => li.classList.remove('active'));
409
- document.querySelector('#sidebar li:first-child').classList.add('active');
410
-
411
- // 更新页面标题
412
- document.getElementById('current-page-title').textContent = '仪���盘';
413
- }
414
-
415
- // 显示管理员密钥信息
416
- function displayAdminKey() {
417
- // 设置到隐藏输入框
418
- document.getElementById('admin-key').value = adminKey;
419
-
420
- // 显示密钥部分信息
421
- const keyDisplay = adminKey.substring(0, 6) + '...' + adminKey.substring(adminKey.length - 4);
422
- document.getElementById('admin-key-display').textContent = '管理员: ' + keyDisplay;
423
- }
424
-
425
- // API请求包装函数,处理token刷新逻辑
426
- async function apiRequest(url, options = {}) {
427
- // 检查token是否即将到期(剩余5分钟以内)
428
- const fiveMinutesInMs = 5 * 60 * 1000;
429
- if (tokenExpiry && (tokenExpiry - new Date().getTime() < fiveMinutesInMs)) {
430
- // 刷新token
431
- const refreshSuccess = await refreshToken();
432
- if (!refreshSuccess) {
433
- // token刷新失败,需要重新登录
434
- handleLogout();
435
- showToast('登录已过期,请重新登录', 'warning');
436
- return null;
437
- }
438
- }
439
-
440
- // 确保options中包含正确的headers
441
- options.headers = options.headers || {};
442
- options.headers['Authorization'] = `Bearer ${adminKey}`;
443
-
444
- // 如果是POST/PUT请求且有body,确保设置正确的Content-Type
445
- if ((options.method === 'POST' || options.method === 'PUT') && options.body) {
446
- // 确保Content-Type正确设置
447
- if (!options.headers['Content-Type']) {
448
- options.headers['Content-Type'] = 'application/json';
449
- }
450
- }
451
-
452
- // 发送请求
453
- try {
454
- const response = await fetch(url, options);
455
-
456
- // 处理401/403错误(未授权)
457
- if (response.status === 401 || response.status === 403) {
458
- // 尝试刷新token
459
- if (await refreshToken()) {
460
- // 刷新成功,使用新token重试请求
461
- options.headers['Authorization'] = `Bearer ${adminKey}`;
462
- const retryResponse = await fetch(url, options);
463
- if (retryResponse.ok) {
464
- return await retryResponse.json();
465
- }
466
- }
467
-
468
- // 刷新失败或重试失败,需要重新登录
469
- handleLogout();
470
- showToast('登录已过期,请重新登录', 'warning');
471
- return null;
472
- }
473
-
474
- // 其他错误
475
- if (!response.ok) {
476
- // 尝试解析错误消息
477
- try {
478
- const errorData = await response.json();
479
- throw new Error(`请求失败: ${response.status} - ${errorData.detail || errorData.message || response.statusText}`);
480
- } catch (e) {
481
- throw new Error(`请求失败: ${response.status} ${response.statusText}`);
482
- }
483
- }
484
-
485
- return await response.json();
486
- } catch (error) {
487
- console.error('API请求错误:', error);
488
- showToast(`请求失败: ${error.message}`, 'error');
489
- return null;
490
- }
491
- }
492
-
493
- // 加载密钥列表
494
- async function loadKeys() {
495
- try {
496
- // 显示加载中
497
- document.getElementById('keys-table-body').innerHTML = '<tr><td colspan="9" class="text-center">加载中...</td></tr>';
498
-
499
- const data = await apiRequest('/api/keys');
500
- if (data) {
501
- keysData = data;
502
- renderKeysTable(keysData);
503
- }
504
- } catch (error) {
505
- console.error('加载密钥失败:', error);
506
- document.getElementById('keys-table-body').innerHTML =
507
- `<tr><td colspan="9" class="text-center text-danger">加载失败: ${error.message}</td></tr>`;
508
- }
509
- }
510
-
511
- // 渲染密钥表格
512
- function renderKeysTable(keys) {
513
- const tableBody = document.getElementById('keys-table-body');
514
- tableBody.innerHTML = '';
515
-
516
- if (keys.length === 0) {
517
- tableBody.innerHTML = '<tr><td colspan="9" class="text-center">暂无密钥数据</td></tr>';
518
- return;
519
- }
520
-
521
- // 分页处理
522
- const startIndex = (currentPage - 1) * keysPerPage;
523
- const endIndex = startIndex + keysPerPage;
524
- const keysToShow = keys.slice(startIndex, endIndex);
525
-
526
- // 渲染表格行
527
- keysToShow.forEach(key => {
528
- const row = document.createElement('tr');
529
-
530
- // 创建时间和最后使用时间格式化
531
- const createDate = key.created_at ? new Date(key.created_at * 1000).toLocaleString() : '未知';
532
- const lastUsedDate = key.last_used ? new Date(key.last_used * 1000).toLocaleString() : '从未使用';
533
-
534
- // 确定密钥状态
535
- let statusText = '';
536
- let statusClass = '';
537
- let statusTitle = '';
538
- let remainingTimeText = '';
539
-
540
- if (key.temp_disabled_until) {
541
- // 临时禁用 - 使用格式化后的时间
542
- statusText = '临时禁用';
543
- statusClass = 'status-temp-disabled';
544
-
545
- // 优先使用服务器返回的格式化时间
546
- const enableTimeText = key.temp_disabled_until_formatted ||
547
- new Date(key.temp_disabled_until * 1000).toLocaleString();
548
-
549
- statusTitle = `将于 ${enableTimeText} 恢复`;
550
-
551
- // 如果有剩余时间信息,添加可读性更强的显示
552
- if (key.temp_disabled_remaining !== undefined) {
553
- const remainingSecs = key.temp_disabled_remaining;
554
- if (remainingSecs > 0) {
555
- // 转换为 小时:分钟:秒 格式
556
- const hours = Math.floor(remainingSecs / 3600);
557
- const minutes = Math.floor((remainingSecs % 3600) / 60);
558
- const seconds = remainingSecs % 60;
559
-
560
- remainingTimeText = `剩余 ${hours}小时${minutes}分钟`;
561
- } else {
562
- remainingTimeText = '即将恢复';
563
- }
564
- }
565
- } else if (key.is_enabled) {
566
- // 启用
567
- statusText = '启用';
568
- statusClass = 'status-enabled';
569
- } else {
570
- // 永久禁用
571
- statusText = '禁用';
572
- statusClass = 'status-disabled';
573
- }
574
-
575
- row.innerHTML = `
576
- <td><input type="checkbox" name="key-checkbox" class="key-checkbox" value="${key.id}" data-id="${key.id}"></td>
577
- <td>${key.name || '未命名'}</td>
578
- <td class="key-value">${key.key}</td>
579
- <td>
580
- <span class="status-badge ${statusClass}" title="${statusTitle}">
581
- ${statusText}
582
- </span>
583
- ${key.temp_disabled_until ?
584
- `<div class="small text-muted">
585
- 启用于: ${key.temp_disabled_until_formatted || new Date(key.temp_disabled_until * 1000).toLocaleString()}
586
- ${remainingTimeText ? `<br>${remainingTimeText}` : ''}
587
- </div>` : ''}
588
- </td>
589
- <td>${key.weight || 1}</td>
590
- <td>${key.max_rpm || 60}/分钟</td>
591
- <td>${createDate}</td>
592
- <td>${lastUsedDate}</td>
593
- <td>
594
- <button class="btn btn-sm btn-primary action-btn edit-key" data-id="${key.id}" title="编辑">
595
- <i class="bi bi-pencil"></i>
596
- </button>
597
- <button class="btn btn-sm btn-danger action-btn delete-key" data-id="${key.id}" title="删除">
598
- <i class="bi bi-trash"></i>
599
- </button>
600
- <button class="btn btn-sm btn-secondary action-btn copy-key" data-key="${key.key}" title="复制密钥">
601
- <i class="bi bi-clipboard"></i>
602
- </button>
603
- </td>
604
- `;
605
-
606
- tableBody.appendChild(row);
607
- });
608
-
609
- // 绑定操作按钮事件
610
- bindTableEvents();
611
-
612
- // 更新分页
613
- renderPagination(keys.length);
614
- }
615
-
616
- // 绑定表格操作事件
617
- function bindTableEvents() {
618
- // 编辑按钮
619
- document.querySelectorAll('.edit-key').forEach(btn => {
620
- btn.addEventListener('click', async () => {
621
- const keyId = btn.getAttribute('data-id');
622
- await loadKeyDetails(keyId);
623
-
624
- // 显示模态框
625
- document.getElementById('keyModalLabel').textContent = '编辑密钥';
626
- const keyModal = new bootstrap.Modal(document.getElementById('keyModal'));
627
- keyModal.show();
628
- });
629
- });
630
-
631
- // 删除按钮
632
- document.querySelectorAll('.delete-key').forEach(btn => {
633
- btn.addEventListener('click', () => {
634
- const keyId = btn.getAttribute('data-id');
635
- const keyName = btn.closest('tr').children[1].textContent;
636
-
637
- showConfirmDialog(`确定要删除密钥 "${keyName}" 吗?此操作不可恢复!`, () => {
638
- deleteKey(keyId);
639
- });
640
- });
641
- });
642
-
643
- // 复制按钮
644
- document.querySelectorAll('.copy-key').forEach(btn => {
645
- btn.addEventListener('click', () => {
646
- const keyValue = btn.getAttribute('data-key');
647
- navigator.clipboard.writeText(keyValue)
648
- .then(() => showToast('密钥已复制到剪贴板', 'success'))
649
- .catch(err => showToast('复制失败: ' + err, 'error'));
650
- });
651
- });
652
- }
653
-
654
- // 渲染分页
655
- function renderPagination(totalKeys) {
656
- const pagination = document.getElementById('pagination');
657
- pagination.innerHTML = '';
658
-
659
- if (totalKeys <= keysPerPage) {
660
- return;
661
- }
662
-
663
- const totalPages = Math.ceil(totalKeys / keysPerPage);
664
- const ul = document.createElement('ul');
665
- ul.className = 'pagination';
666
-
667
- // 上一页
668
- const prevLi = document.createElement('li');
669
- prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
670
- prevLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage - 1}">上一页</a>`;
671
- ul.appendChild(prevLi);
672
-
673
- // 页码
674
- for (let i = 1; i <= totalPages; i++) {
675
- const li = document.createElement('li');
676
- li.className = `page-item ${currentPage === i ? 'active' : ''}`;
677
- li.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
678
- ul.appendChild(li);
679
- }
680
-
681
- // 下一页
682
- const nextLi = document.createElement('li');
683
- nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
684
- nextLi.innerHTML = `<a class="page-link" href="#" data-page="${currentPage + 1}">下一页</a>`;
685
- ul.appendChild(nextLi);
686
-
687
- pagination.appendChild(ul);
688
-
689
- // 绑定页码点击事件
690
- document.querySelectorAll('.page-link').forEach(link => {
691
- link.addEventListener('click', (e) => {
692
- e.preventDefault();
693
- if (link.parentElement.classList.contains('disabled')) {
694
- return;
695
- }
696
- currentPage = parseInt(link.getAttribute('data-page'));
697
- renderKeysTable(keysData);
698
- });
699
- });
700
- }
701
-
702
- // 加载单个密钥详情
703
- async function loadKeyDetails(keyId) {
704
- try {
705
- const keyData = await apiRequest(`/api/keys/${keyId}`);
706
- if (!keyData) return;
707
-
708
- // 填充表单
709
- document.getElementById('key-id').value = keyData.id;
710
- document.getElementById('key-name').value = keyData.name || '';
711
- document.getElementById('key-value').value = keyData.key || '';
712
- document.getElementById('key-weight').value = keyData.weight || 1;
713
- document.getElementById('key-rate-limit').value = keyData.max_rpm || 60;
714
- document.getElementById('key-enabled').checked = keyData.is_enabled;
715
- document.getElementById('key-notes').value = keyData.notes || '';
716
- } catch (error) {
717
- console.error('加载密钥详情失败:', error);
718
- showToast('加载密钥详情失败: ' + error.message, 'error');
719
- }
720
- }
721
-
722
- // 保存密钥
723
- async function saveKey() {
724
- try {
725
- const keyId = document.getElementById('key-id').value;
726
- const keyData = {
727
- name: document.getElementById('key-name').value,
728
- key_value: document.getElementById('key-value').value,
729
- weight: parseInt(document.getElementById('key-weight').value),
730
- rate_limit: parseInt(document.getElementById('key-rate-limit').value),
731
- is_enabled: document.getElementById('key-enabled').checked,
732
- notes: document.getElementById('key-notes').value
733
- };
734
-
735
- if (!keyData.name || !keyData.key_value) {
736
- showToast('密钥名称和值不能为空', 'warning');
737
- return;
738
- }
739
-
740
- let url, method;
741
-
742
- if (keyId) {
743
- // 更新现有密钥
744
- url = `/api/keys/${keyId}`;
745
- method = 'PUT';
746
- } else {
747
- // 创建新密钥
748
- url = '/api/keys';
749
- method = 'POST';
750
- }
751
-
752
- const options = {
753
- method,
754
- headers: {
755
- 'Content-Type': 'application/json'
756
- },
757
- body: JSON.stringify(keyData)
758
- };
759
-
760
- const result = await apiRequest(url, options);
761
- if (!result) return;
762
-
763
- // 关闭模态框
764
- const keyModal = bootstrap.Modal.getInstance(document.getElementById('keyModal'));
765
- keyModal.hide();
766
-
767
- // 重新加载密钥列表
768
- await loadKeys();
769
-
770
- // 如果是仪表盘页面,也更新仪表盘数据
771
- if (document.getElementById('dashboard-page').classList.contains('active')) {
772
- await loadDashboard();
773
- }
774
-
775
- // 显示成功消息
776
- showToast(keyId ? '密钥更新成功' : '密钥添加成功', 'success');
777
- } catch (error) {
778
- console.error('保存密钥失败:', error);
779
- showToast('保存密钥失败: ' + error.message, 'error');
780
- }
781
- }
782
-
783
- // 测试密钥连接
784
- async function testKey() {
785
- try {
786
- const keyValue = document.getElementById('key-value').value;
787
-
788
- if (!keyValue) {
789
- showToast('请输入有效的密钥值', 'warning');
790
- return;
791
- }
792
-
793
- const keyName = document.getElementById('key-name').value || '新密钥';
794
-
795
- // 显示测试中状态
796
- const testButton = document.getElementById('test-key-btn');
797
- const originalText = testButton.innerHTML;
798
- testButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> 测试中...';
799
- testButton.disabled = true;
800
-
801
- const testData = {
802
- name: keyName,
803
- key_value: keyValue
804
- };
805
-
806
- const result = await apiRequest('/api/keys/test', {
807
- method: 'POST',
808
- headers: {
809
- 'Content-Type': 'application/json'
810
- },
811
- body: JSON.stringify(testData)
812
- });
813
-
814
- // 恢复按钮状态
815
- testButton.innerHTML = originalText;
816
- testButton.disabled = false;
817
-
818
- if (!result) return;
819
-
820
- if (result.status === "success") {
821
- showToast(`测试成功: ${result.message || '密钥可用'}`, 'success');
822
- } else {
823
- showToast(`测试失败: ${result.message || '密钥无法连接'}`, 'warning');
824
- }
825
- } catch (error) {
826
- console.error('测试密钥失败:', error);
827
- showToast('测试密钥失败: ' + error.message, 'error');
828
-
829
- // 恢复按钮状态
830
- const testButton = document.getElementById('test-key-btn');
831
- testButton.innerHTML = '<i class="bi bi-shield-check me-1"></i> 测试连接';
832
- testButton.disabled = false;
833
- }
834
- }
835
-
836
- // 删除密钥
837
- async function deleteKey(keyId) {
838
- try {
839
- const result = await apiRequest(`/api/keys/${keyId}`, {
840
- method: 'DELETE'
841
- });
842
-
843
- if (!result) return;
844
-
845
- // 重新加载密钥列表
846
- await loadKeys();
847
-
848
- // 如果是仪表盘页面,也更新仪表盘数据
849
- if (document.getElementById('dashboard-page').classList.contains('active')) {
850
- await loadDashboard();
851
- }
852
-
853
- // 显示成功消息
854
- showToast('密钥删除成功', 'success');
855
- } catch (error) {
856
- console.error('删除密钥失败:', error);
857
- showToast('删除密钥失败: ' + error.message, 'error');
858
- }
859
- }
860
-
861
- // 批量操作
862
- async function batchOperation(action, keyIds) {
863
- try {
864
- const operationData = {
865
- action: action,
866
- key_ids: keyIds
867
- };
868
-
869
- const result = await apiRequest('/api/keys/batch', {
870
- method: 'POST',
871
- headers: {
872
- 'Content-Type': 'application/json'
873
- },
874
- body: JSON.stringify(operationData)
875
- });
876
-
877
- if (!result) return;
878
-
879
- // 重新加载密钥列表
880
- await loadKeys();
881
-
882
- // 如果是仪表盘页面,也更新仪表盘数据
883
- if (document.getElementById('dashboard-page').classList.contains('active')) {
884
- await loadDashboard();
885
- }
886
-
887
- // 显示成功消息
888
- let message = '';
889
- if (action === 'enable') {
890
- message = '批量启用成功';
891
- } else if (action === 'disable') {
892
- message = '批量禁用成功';
893
- } else if (action === 'delete') {
894
- message = '批量删除成功';
895
- }
896
-
897
- showToast(message, 'success');
898
- } catch (error) {
899
- console.error('批量操作失败:', error);
900
- showToast('批量操作失败: ' + error.message, 'error');
901
- }
902
- }
903
-
904
- // 获取所有选中的密钥ID
905
- function getSelectedKeyIds() {
906
- const checkboxes = document.querySelectorAll('#keys-table-body input[type="checkbox"]:checked');
907
- return Array.from(checkboxes).map(checkbox => checkbox.getAttribute('data-id'));
908
- }
909
-
910
- // 加载统计数据
911
- async function loadStats() {
912
- try {
913
- // 显示加载中状态
914
- document.getElementById('total-requests').textContent = '加载中...';
915
- document.getElementById('successful-requests').textContent = '加载中...';
916
- document.getElementById('failed-requests').textContent = '加载中...';
917
- document.getElementById('success-rate').textContent = '加载中...';
918
-
919
- const data = await apiRequest('/api/stats');
920
- if (data) {
921
- statsData = data;
922
-
923
- // 渲染统计卡片
924
- renderStats();
925
-
926
- // 渲染图表
927
- renderRequestsChart(statsData.daily_usage);
928
- renderKeysUsageChart(statsData.keys_usage);
929
- }
930
- } catch (error) {
931
- console.error('加载统计数据失败:', error);
932
-
933
- // 创建模拟数据
934
- statsData = {
935
- total_requests: Math.floor(Math.random() * 5000) + 1000,
936
- successful_requests: Math.floor(Math.random() * 4000) + 800,
937
- daily_usage: generateMockDailyUsage(),
938
- keys_usage: generateMockKeysUsage()
939
- };
940
-
941
- // 渲染模拟数据
942
- renderStats();
943
- renderRequestsChart(statsData.daily_usage);
944
- renderKeysUsageChart(statsData.keys_usage);
945
- }
946
- }
947
-
948
- // 渲染统计卡片
949
- function renderStats() {
950
- if (!statsData) {
951
- // 如果没有统计数据,使用默认值
952
- statsData = {
953
- total_requests: 0,
954
- successful_requests: 0
955
- };
956
- }
957
-
958
- const totalRequests = statsData.total_requests || 0;
959
- const successfulRequests = statsData.successful_requests || 0;
960
- const failedRequests = totalRequests - successfulRequests;
961
- const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) + '%' : '0%';
962
-
963
- // 更新统计卡片
964
- document.getElementById('total-requests').textContent = totalRequests;
965
- document.getElementById('successful-requests').textContent = successfulRequests;
966
- document.getElementById('failed-requests').textContent = failedRequests;
967
- document.getElementById('success-rate').textContent = successRate;
968
- }
969
-
970
- // 渲染请求趋势图
971
- function renderRequestsChart(dailyUsage) {
972
- // 准备数据
973
- const dates = Object.keys(dailyUsage || {}).sort();
974
- const lastDates = dates.slice(-30); // 最近30天
975
-
976
- const chartData = {
977
- labels: lastDates.map(date => date.substring(5)), // 只显示月-日
978
- datasets: [{
979
- label: '请求数',
980
- data: lastDates.map(date => dailyUsage[date] || 0),
981
- backgroundColor: 'rgba(13, 110, 253, 0.4)',
982
- borderColor: 'rgba(13, 110, 253, 1)',
983
- borderWidth: 2
984
- }]
985
- };
986
-
987
- // 销毁现有图表
988
- if (requestsChart) {
989
- requestsChart.destroy();
990
- }
991
-
992
- // 创建新图表
993
- const ctx = document.getElementById('requests-chart').getContext('2d');
994
- requestsChart = new Chart(ctx, {
995
- type: 'bar',
996
- data: chartData,
997
- options: {
998
- responsive: true,
999
- maintainAspectRatio: false,
1000
- plugins: {
1001
- legend: {
1002
- display: false
1003
- },
1004
- tooltip: {
1005
- mode: 'index',
1006
- intersect: false
1007
- }
1008
- },
1009
- scales: {
1010
- x: {
1011
- grid: {
1012
- display: false
1013
- }
1014
- },
1015
- y: {
1016
- beginAtZero: true,
1017
- ticks: {
1018
- precision: 0
1019
- }
1020
- }
1021
- }
1022
- }
1023
- });
1024
- }
1025
-
1026
- // 渲染密钥使用分布图
1027
- function renderKeysUsageChart(keysUsage) {
1028
- // 准备数据
1029
- const keys = Object.keys(keysUsage || {});
1030
- const values = keys.map(key => keysUsage[key]);
1031
-
1032
- // 只显示前8个密钥,其余归为"其他"
1033
- let displayKeys = keys;
1034
- let displayValues = values;
1035
-
1036
- if (keys.length > 8) {
1037
- displayKeys = keys.slice(0, 7);
1038
- displayValues = values.slice(0, 7);
1039
-
1040
- // 计算"其他"的总和
1041
- const othersSum = values.slice(7).reduce((sum, value) => sum + value, 0);
1042
- displayKeys.push('其他');
1043
- displayValues.push(othersSum);
1044
- }
1045
-
1046
- // 生成颜色
1047
- const backgroundColors = [
1048
- 'rgba(13, 110, 253, 0.7)', // 主蓝色
1049
- 'rgba(220, 53, 69, 0.7)', // 红色
1050
- 'rgba(25, 135, 84, 0.7)', // 绿色
1051
- 'rgba(255, 193, 7, 0.7)', // 黄色
1052
- 'rgba(111, 66, 193, 0.7)', // 紫色
1053
- 'rgba(23, 162, 184, 0.7)', // 青色
1054
- 'rgba(102, 16, 242, 0.7)', // 靛蓝色
1055
- 'rgba(108, 117, 125, 0.7)' // 灰色
1056
- ];
1057
-
1058
- const chartData = {
1059
- labels: displayKeys,
1060
- datasets: [{
1061
- data: displayValues,
1062
- backgroundColor: backgroundColors,
1063
- borderColor: backgroundColors.map(color => color.replace('0.7', '1')),
1064
- borderWidth: 1
1065
- }]
1066
- };
1067
-
1068
- // 销毁现有图表
1069
- if (keysUsageChart) {
1070
- keysUsageChart.destroy();
1071
- }
1072
-
1073
- // 创建新图表
1074
- const ctx = document.getElementById('keys-usage-chart').getContext('2d');
1075
- keysUsageChart = new Chart(ctx, {
1076
- type: 'pie',
1077
- data: chartData,
1078
- options: {
1079
- responsive: true,
1080
- maintainAspectRatio: false,
1081
- plugins: {
1082
- legend: {
1083
- position: 'right'
1084
- }
1085
- }
1086
- }
1087
- });
1088
- }
1089
-
1090
- // 加载系统设置
1091
- async function loadSettings() {
1092
- try {
1093
- const data = await apiRequest('/api/config');
1094
- if (!data) return;
1095
-
1096
- // 设置现有的代理设置
1097
- document.getElementById('proxy-host').value = data.PROXY_HOST || '';
1098
- document.getElementById('proxy-port').value = data.PROXY_PORT || '';
1099
- document.getElementById('proxy-user').value = data.PROXY_USER || '';
1100
- document.getElementById('proxy-pass').value = '';
1101
-
1102
- // 设置基础URL
1103
- document.getElementById('base-url').value = data.BASE_URL || '';
1104
-
1105
- // 设置图片本地化配置
1106
- document.getElementById('image-localization').checked = data.IMAGE_LOCALIZATION || false;
1107
- document.getElementById('image-save-dir').value = data.IMAGE_SAVE_DIR || 'src/static/images';
1108
- } catch (error) {
1109
- console.error('加载设置失败:', error);
1110
- showToast('获取设置失败: ' + error.message, 'error');
1111
- }
1112
- }
1113
-
1114
- // 保存系统设置
1115
- async function saveSettings() {
1116
- try {
1117
- // 获取表单数据
1118
- const config = {
1119
- PROXY_HOST: document.getElementById('proxy-host').value,
1120
- PROXY_PORT: document.getElementById('proxy-port').value,
1121
- PROXY_USER: document.getElementById('proxy-user').value,
1122
- PROXY_PASS: document.getElementById('proxy-pass').value,
1123
- BASE_URL: document.getElementById('base-url').value,
1124
- IMAGE_LOCALIZATION: document.getElementById('image-localization').checked,
1125
- IMAGE_SAVE_DIR: document.getElementById('image-save-dir').value || 'src/static/images',
1126
- save_to_env: true
1127
- };
1128
-
1129
- // 调用API保存到服务器
1130
- const result = await apiRequest('/api/config', {
1131
- method: 'POST',
1132
- headers: {
1133
- 'Content-Type': 'application/json'
1134
- },
1135
- body: JSON.stringify(config)
1136
- });
1137
-
1138
- if (!result) return;
1139
-
1140
- showToast('设置已保存', 'success');
1141
- } catch (error) {
1142
- console.error('保存设置失败:', error);
1143
- showToast('保存设置失败: ' + error.message, 'error');
1144
- }
1145
- }
1146
-
1147
- // 显示确认对话框
1148
- function showConfirmDialog(message, callback) {
1149
- document.getElementById('confirm-message').textContent = message;
1150
- const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal'));
1151
-
1152
- // 确认按钮事件
1153
- document.getElementById('confirm-btn').onclick = () => {
1154
- confirmModal.hide();
1155
- if (typeof callback === 'function') {
1156
- callback();
1157
- }
1158
- };
1159
-
1160
- confirmModal.show();
1161
- }
1162
-
1163
- // 显示提示消息
1164
- function showToast(message, type = 'info') {
1165
- // 创建Toast元素
1166
- const toastId = 'toast-' + Date.now();
1167
- const toastEl = document.createElement('div');
1168
- toastEl.className = `toast align-items-center text-white bg-${type}`;
1169
- toastEl.id = toastId;
1170
- toastEl.setAttribute('role', 'alert');
1171
- toastEl.setAttribute('aria-live', 'assertive');
1172
- toastEl.setAttribute('aria-atomic', 'true');
1173
-
1174
- toastEl.innerHTML = `
1175
- <div class="d-flex">
1176
- <div class="toast-body">
1177
- ${message}
1178
- </div>
1179
- <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
1180
- </div>
1181
- `;
1182
-
1183
- // 添加到容器
1184
- document.getElementById('toast-container').appendChild(toastEl);
1185
-
1186
- // 显示Toast
1187
- const toast = new bootstrap.Toast(toastEl, {
1188
- autohide: true,
1189
- delay: 3000
1190
- });
1191
- toast.show();
1192
-
1193
- // 监听隐藏事件,删除元素
1194
- toastEl.addEventListener('hidden.bs.toast', () => {
1195
- toastEl.remove();
1196
- });
1197
- }
1198
-
1199
- // 加载仪表盘数据
1200
- async function loadDashboard() {
1201
- try {
1202
- // 先加载密钥统计
1203
- await loadKeyStats();
1204
-
1205
- // 再加载使用统计
1206
- await loadUsageStats();
1207
-
1208
- // 加载最近的密钥
1209
- await loadRecentKeys();
1210
- } catch (error) {
1211
- console.error('加载仪表盘数据失败:', error);
1212
- showToast('仪表盘数据加载失败', 'error');
1213
- }
1214
- }
1215
-
1216
- // 加载密钥统计
1217
- async function loadKeyStats() {
1218
- try {
1219
- const data = await apiRequest('/api/keys');
1220
- if (!data) return;
1221
-
1222
- keysData = data;
1223
-
1224
- // 计算统计数据
1225
- const totalKeys = keysData.length;
1226
- const activeKeys = keysData.filter(key => key.is_enabled).length;
1227
- const disabledKeys = totalKeys - activeKeys;
1228
-
1229
- // 更新统计卡片
1230
- document.getElementById('total-keys').textContent = totalKeys;
1231
- document.getElementById('active-keys').textContent = activeKeys;
1232
- document.getElementById('disabled-keys').textContent = disabledKeys;
1233
- } catch (error) {
1234
- console.error('加载密钥统计失败:', error);
1235
- }
1236
- }
1237
-
1238
- // 加载使用统计
1239
- async function loadUsageStats() {
1240
- try {
1241
- const data = await apiRequest('/api/stats');
1242
- if (!data) return;
1243
-
1244
- // 更新仪表盘统计数据
1245
- const totalRequests = data.total_requests || 0;
1246
- const successfulRequests = data.successful_requests || 0;
1247
- const failedRequests = data.failed_requests || 0;
1248
-
1249
- // 更新统计卡片
1250
- document.getElementById('total-requests').textContent = totalRequests;
1251
- document.getElementById('successful-requests').textContent = successfulRequests;
1252
- document.getElementById('failed-requests').textContent = failedRequests;
1253
-
1254
- // 计算成功率
1255
- const successRate = totalRequests > 0 ? Math.round((successfulRequests / totalRequests) * 100) : 0;
1256
- document.getElementById('success-rate').textContent = `${successRate}%`;
1257
-
1258
- // 获取今日使用量
1259
- const today = new Date().toISOString().split('T')[0];
1260
- const todayRequests = (data.daily_usage && data.daily_usage[today]) || 0;
1261
- document.getElementById('today-requests').textContent = todayRequests;
1262
-
1263
- // 设置图表数据
1264
- statsData = data;
1265
-
1266
- // 更新图表
1267
- renderDashboardRequestsChart(data.daily_usage || {});
1268
-
1269
- // 渲染请求趋势图和密钥使用分布图
1270
- renderRequestsChart(data.daily_usage || {});
1271
- renderKeysUsageChart(data.keys_usage || {});
1272
-
1273
- console.log("图表数据已更新:", {
1274
- daily_usage: data.daily_usage,
1275
- keys_usage: data.keys_usage
1276
- });
1277
- } catch (error) {
1278
- console.error('加载使用统计失败:', error);
1279
- }
1280
- }
1281
-
1282
- // 加载最近的密钥
1283
- async function loadRecentKeys() {
1284
- try {
1285
- if (!keysData || keysData.length === 0) {
1286
- const data = await apiRequest('/api/keys');
1287
- if (!data) return;
1288
- keysData = data;
1289
- }
1290
-
1291
- // 按创建时间排序,获取最近的5个
1292
- const recentKeys = [...keysData]
1293
- .sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
1294
- .slice(0, 5);
1295
-
1296
- const recentKeysList = document.getElementById('recent-keys-list');
1297
- recentKeysList.innerHTML = '';
1298
-
1299
- if (recentKeys.length === 0) {
1300
- recentKeysList.innerHTML = '<div class="list-group-item text-center text-muted">暂无密钥数据</div>';
1301
- return;
1302
- }
1303
-
1304
- recentKeys.forEach(key => {
1305
- const createDate = key.created_at ? new Date(key.created_at * 1000).toLocaleDateString() : '未知';
1306
- const li = document.createElement('a');
1307
- li.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
1308
- li.href = '#';
1309
- li.innerHTML = `
1310
- <div>
1311
- <strong>${key.name || '未命名'}</strong>
1312
- <div class="text-muted small">${key.key.substring(0, 10)}...</div>
1313
- </div>
1314
- <div>
1315
- <span class="badge ${key.is_enabled ? 'bg-success' : 'bg-danger'} rounded-pill">
1316
- ${key.is_enabled ? '启用' : '禁用'}
1317
- </span>
1318
- <small class="text-muted ms-2">${createDate}</small>
1319
- </div>
1320
- `;
1321
- recentKeysList.appendChild(li);
1322
-
1323
- // 点击跳转到密钥管理页面
1324
- li.addEventListener('click', (e) => {
1325
- e.preventDefault();
1326
- document.querySelector('#sidebar a[data-page="keys"]').click();
1327
- });
1328
- });
1329
- } catch (error) {
1330
- console.error('加载最近密钥失败:', error);
1331
- }
1332
- }
1333
-
1334
- // 渲染仪表盘请求趋势图
1335
- function renderDashboardRequestsChart(dailyUsage) {
1336
- // 获取最近7天的日期
1337
- const dates = [];
1338
- const now = new Date();
1339
- for (let i = 6; i >= 0; i--) {
1340
- const date = new Date(now);
1341
- date.setDate(now.getDate() - i);
1342
- dates.push(date.toISOString().split('T')[0]);
1343
- }
1344
-
1345
- // 准备图表数据
1346
- const chartData = {
1347
- labels: dates.map(date => date.substring(5)), // 只显示月-日
1348
- datasets: [{
1349
- label: '每日请求数',
1350
- data: dates.map(date => (dailyUsage && dailyUsage[date]) || 0),
1351
- borderColor: '#0d6efd',
1352
- backgroundColor: 'rgba(13, 110, 253, 0.1)',
1353
- borderWidth: 2,
1354
- fill: true,
1355
- tension: 0.3
1356
- }]
1357
- };
1358
-
1359
- // 销毁现有图表
1360
- if (dashboardRequestsChart) {
1361
- dashboardRequestsChart.destroy();
1362
- }
1363
-
1364
- // 创建新图表
1365
- const ctx = document.getElementById('dashboard-requests-chart').getContext('2d');
1366
- dashboardRequestsChart = new Chart(ctx, {
1367
- type: 'line',
1368
- data: chartData,
1369
- options: {
1370
- responsive: true,
1371
- maintainAspectRatio: false,
1372
- plugins: {
1373
- legend: {
1374
- display: false
1375
- },
1376
- tooltip: {
1377
- mode: 'index',
1378
- intersect: false
1379
- }
1380
- },
1381
- scales: {
1382
- x: {
1383
- grid: {
1384
- display: false
1385
- }
1386
- },
1387
- y: {
1388
- beginAtZero: true,
1389
- ticks: {
1390
- precision: 0
1391
- }
1392
- }
1393
- }
1394
- }
1395
- });
1396
- }
1397
-
1398
- // 批量导入密钥预览
1399
- async function previewImport() {
1400
- const keysText = document.getElementById('keys-text').value.trim();
1401
-
1402
- if (!keysText) {
1403
- showToast('请输入或上传密钥数据', 'warning');
1404
- return;
1405
- }
1406
-
1407
- try {
1408
- // 解析密钥数据
1409
- const lines = keysText.split('\n').filter(line => line.trim());
1410
- importPreviewData = [];
1411
-
1412
- console.log(`开始处理 ${lines.length} 行数据`);
1413
-
1414
- for (let i = 0; i < lines.length; i++) {
1415
- try {
1416
- const line = lines[i];
1417
- // 安全获取子字符串
1418
- const safeSubstring = (str, start, end) => {
1419
- if (!str) return '';
1420
- return str.substring(start, Math.min(end, str.length));
1421
- };
1422
-
1423
- console.log(`处理第 ${i+1} 行: ${safeSubstring(line, 0, 10)}...`);
1424
-
1425
- // 检查是否是单行密钥格式(不包含逗号)
1426
- if (!line.includes(',')) {
1427
- const keyValue = line.trim();
1428
- console.log(` 单行格式,密钥值: ${safeSubstring(keyValue, 0, 5)}...`);
1429
-
1430
- // 几乎不做验证 - 只要不是空字符串或太短就接受
1431
- if (!keyValue || keyValue.length < 5) {
1432
- console.log(` 密钥太短,跳过`);
1433
- continue; // 跳过太短的密钥
1434
- }
1435
-
1436
- // 为密钥自动生成名称(使用前5位)
1437
- const keyName = `密钥_${safeSubstring(keyValue, 0, 5)}`;
1438
-
1439
- importPreviewData.push({
1440
- name: keyName,
1441
- key: keyValue,
1442
- weight: 1,
1443
- rate_limit: 60,
1444
- enabled: document.getElementById('auto-enable-keys').checked
1445
- });
1446
- console.log(` 添加到预览数据,当前共 ${importPreviewData.length} 个`);
1447
- continue;
1448
- }
1449
-
1450
- // 处理标准格式(带逗号分隔)
1451
- const parts = line.split(',').map(part => part.trim());
1452
- console.log(` 标准格式,分割后有 ${parts.length} 部分`);
1453
-
1454
- if (parts.length < 2) {
1455
- console.log(` 部分数量不足2,跳过`);
1456
- continue; // 跳过格式不正确的行
1457
- }
1458
-
1459
- const keyName = parts[0] || `密钥_未命名_${i+1}`;
1460
- const keyValue = parts[1] || '';
1461
- const weight = parts.length > 2 ? parseInt(parts[2]) || 1 : 1;
1462
- const rateLimit = parts.length > 3 ? parseInt(parts[3]) || 60 : 60;
1463
-
1464
- console.log(` 名称: ${keyName}, 密钥: ${safeSubstring(keyValue, 0, 5)}..., 权重: ${weight}, 速率: ${rateLimit}`);
1465
-
1466
- // 几乎不做验证 - 只要不是空字符串或太短就接受
1467
- if (!keyValue || keyValue.length < 5) {
1468
- console.log(` 密钥太短,跳过`);
1469
- continue; // 跳过太短的密钥
1470
- }
1471
-
1472
- importPreviewData.push({
1473
- name: keyName,
1474
- key: keyValue,
1475
- weight: weight,
1476
- rate_limit: rateLimit,
1477
- enabled: document.getElementById('auto-enable-keys').checked
1478
- });
1479
- console.log(` 添加到预览数据,当前共 ${importPreviewData.length} 个`);
1480
- } catch (lineError) {
1481
- console.error(`处理第 ${i+1} 行时出错:`, lineError);
1482
- // 继续处理下一行
1483
- continue;
1484
- }
1485
- }
1486
-
1487
- console.log(`处理完成,共有 ${importPreviewData.length} 个有效密钥`);
1488
-
1489
- // 显示预览
1490
- if (importPreviewData.length > 0) {
1491
- renderImportPreview();
1492
- document.getElementById('preview-count').textContent = importPreviewData.length;
1493
- document.getElementById('import-preview').style.display = 'block';
1494
- document.getElementById('confirm-import-btn').disabled = false;
1495
- console.log('预览渲染完成,启用导入按钮');
1496
- } else {
1497
- showToast('未找到有效的密钥数据', 'warning');
1498
- document.getElementById('import-preview').style.display = 'none';
1499
- document.getElementById('confirm-import-btn').disabled = true;
1500
- console.log('未找到有效密钥,禁用导入按钮');
1501
- }
1502
- } catch (error) {
1503
- console.error('预览导入失败:', error);
1504
- showToast('预览失败: ' + error.message, 'danger');
1505
- }
1506
- }
1507
-
1508
- // 渲染导入预览表格
1509
- function renderImportPreview() {
1510
- const tableBody = document.getElementById('preview-table-body');
1511
- tableBody.innerHTML = '';
1512
-
1513
- // 限制预览最多显示10行
1514
- const displayData = importPreviewData.slice(0, 10);
1515
-
1516
- displayData.forEach(key => {
1517
- const row = document.createElement('tr');
1518
-
1519
- // 名称
1520
- const nameCell = document.createElement('td');
1521
- nameCell.textContent = key.name;
1522
- row.appendChild(nameCell);
1523
-
1524
- // 密钥(部分隐藏)
1525
- const keyCell = document.createElement('td');
1526
- const maskedKey = key.key.substring(0, 5) + '...' + key.key.substring(key.key.length - 4);
1527
- keyCell.textContent = maskedKey;
1528
- row.appendChild(keyCell);
1529
-
1530
- // 权重
1531
- const weightCell = document.createElement('td');
1532
- weightCell.textContent = key.weight;
1533
- row.appendChild(weightCell);
1534
-
1535
- // 速率限制
1536
- const rateLimitCell = document.createElement('td');
1537
- rateLimitCell.textContent = key.rate_limit;
1538
- row.appendChild(rateLimitCell);
1539
-
1540
- tableBody.appendChild(row);
1541
- });
1542
-
1543
- // 如果有更多未显示的行
1544
- if (importPreviewData.length > 10) {
1545
- const moreRow = document.createElement('tr');
1546
- const moreCell = document.createElement('td');
1547
- moreCell.colSpan = 4;
1548
- moreCell.textContent = `... 另外 ${importPreviewData.length - 10} 个密钥未在预览中显示`;
1549
- moreCell.className = 'text-center text-muted';
1550
- moreRow.appendChild(moreCell);
1551
- tableBody.appendChild(moreRow);
1552
- }
1553
- }
1554
-
1555
- // 确认导入密钥
1556
- async function confirmImport() {
1557
- if (importPreviewData.length === 0) {
1558
- showToast('没有要导入的密钥', 'warning');
1559
- return;
1560
- }
1561
-
1562
- try {
1563
- // 显示加载状态
1564
- const importBtn = document.getElementById('confirm-import-btn');
1565
- const originalText = importBtn.textContent;
1566
- importBtn.disabled = true;
1567
- importBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 导入中...';
1568
-
1569
- console.log('准备发送批量导入请求,数据预览:', importPreviewData.slice(0, 2));
1570
-
1571
- // 简化数据处理
1572
- const requestData = {
1573
- action: "import",
1574
- keys: []
1575
- };
1576
-
1577
- // 手动将每个importPreviewData项转换为普通对象
1578
- for (const item of importPreviewData) {
1579
- requestData.keys.push({
1580
- name: item.name || "",
1581
- key: item.key || "",
1582
- weight: item.weight || 1,
1583
- rate_limit: item.rate_limit || 60,
1584
- enabled: item.enabled !== undefined ? item.enabled : true
1585
- });
1586
- }
1587
-
1588
- console.log('发送请求到 /api/keys/batch,请求数据:', requestData);
1589
-
1590
- // 发送请求
1591
- const response = await apiRequest('/api/keys/batch', {
1592
- method: 'POST',
1593
- headers: {
1594
- 'Content-Type': 'application/json'
1595
- },
1596
- body: JSON.stringify(requestData)
1597
- });
1598
-
1599
- console.log('收到批量导入响应:', response);
1600
-
1601
- if (response && response.success) {
1602
- // 隐藏模态框
1603
- bootstrap.Modal.getInstance(document.getElementById('importKeysModal')).hide();
1604
-
1605
- // 刷新密钥列表
1606
- await loadKeys();
1607
-
1608
- // 显示成功消息
1609
- const successCount = response.imported || importPreviewData.length;
1610
- const skippedCount = response.skipped || 0;
1611
- let message = `成功导入 ${successCount} 个密钥`;
1612
- if (skippedCount > 0) {
1613
- message += `,${skippedCount} 个重复密钥已跳过`;
1614
- }
1615
- showToast(message, 'success');
1616
- console.log('导入成功完成');
1617
- } else {
1618
- const errorMsg = (response && response.message) ? response.message : '未知错误';
1619
- showToast('导入失败: ' + errorMsg, 'danger');
1620
- console.error('导入失败,服务器响应:', response);
1621
- }
1622
- } catch (error) {
1623
- console.error('导入过程中发生异常:', error);
1624
- showToast('导入失败: ' + error.message, 'danger');
1625
- } finally {
1626
- // 恢复按钮状态
1627
- const importBtn = document.getElementById('confirm-import-btn');
1628
- importBtn.disabled = false;
1629
- importBtn.textContent = '导入';
1630
- }
1631
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/src/utils.py DELETED
@@ -1,208 +0,0 @@
1
- import os
2
- import uuid
3
- import aiohttp
4
- import aiofiles
5
- import logging
6
- import ssl
7
- from urllib.parse import urlparse
8
- from .config import Config
9
-
10
- # 初始化日志
11
- logger = logging.getLogger("sora-api.utils")
12
-
13
- # 图片本地化调试开关
14
- IMAGE_DEBUG = os.getenv("IMAGE_DEBUG", "").lower() in ("true", "1", "yes")
15
-
16
- # 修复Python 3.11之前版本中HTTPS代理处理HTTPS请求的问题
17
- # 参考: https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support
18
- try:
19
- import aiohttp.connector
20
- orig_create_connection = aiohttp.connector.TCPConnector._create_connection
21
-
22
- async def patched_create_connection(self, req, traces, timeout):
23
- if req.ssl and req.proxy and req.proxy.scheme == 'https':
24
- # 为代理连接创建SSL上下文
25
- proxy_ssl = ssl.create_default_context()
26
- req.proxy_ssl = proxy_ssl
27
-
28
- if IMAGE_DEBUG:
29
- logger.debug("已应用HTTPS代理补丁")
30
-
31
- return await orig_create_connection(self, req, traces, timeout)
32
-
33
- # 应用猴子补丁
34
- aiohttp.connector.TCPConnector._create_connection = patched_create_connection
35
-
36
- if IMAGE_DEBUG:
37
- logger.debug("已启用aiohttp HTTPS代理支持补丁")
38
- except Exception as e:
39
- logger.warning(f"应用HTTPS代理补丁失败: {e}")
40
-
41
- async def download_and_save_image(image_url: str) -> str:
42
- """
43
- 下载图片并保存到本地
44
-
45
- Args:
46
- image_url: 图片URL
47
-
48
- Returns:
49
- 本地化后的图片URL
50
- """
51
- # 如果未启用本地化或URL已经是本地路径,直接返回
52
- if not Config.IMAGE_LOCALIZATION:
53
- if IMAGE_DEBUG:
54
- logger.debug(f"图片本地化未启用,返回原始URL: {image_url}")
55
- return image_url
56
-
57
- # 检查是否已经是本地URL
58
- # 准备URL检测所需的信息
59
- parsed_base_url = urlparse(Config.BASE_URL)
60
- base_path = parsed_base_url.path.rstrip('/')
61
-
62
- # 直接检查常见的图片URL模式
63
- local_url_patterns = [
64
- "/images/",
65
- "/static/images/",
66
- f"{base_path}/images/",
67
- f"{base_path}/static/images/"
68
- ]
69
-
70
- # 检查是否有自定义前缀
71
- if Config.STATIC_PATH_PREFIX:
72
- prefix = Config.STATIC_PATH_PREFIX
73
- if not prefix.startswith('/'):
74
- prefix = f"/{prefix}"
75
- local_url_patterns.append(f"{prefix}/images/")
76
- local_url_patterns.append(f"{base_path}{prefix}/images/")
77
-
78
- # 检查是否符合任何本地URL模式
79
- is_local_url = any(pattern in image_url for pattern in local_url_patterns)
80
-
81
- if is_local_url:
82
- if IMAGE_DEBUG:
83
- logger.debug(f"URL已是本地图片路径: {image_url}")
84
-
85
- # 如果是相对路径,补充完整的URL
86
- if image_url.startswith("/"):
87
- return f"{parsed_base_url.scheme}://{parsed_base_url.netloc}{image_url}"
88
- return image_url
89
-
90
- try:
91
- # 生成文件名和保存路径
92
- parsed_url = urlparse(image_url)
93
- file_extension = os.path.splitext(parsed_url.path)[1] or ".png"
94
- filename = f"{uuid.uuid4()}{file_extension}"
95
- save_path = os.path.join(Config.IMAGE_SAVE_DIR, filename)
96
-
97
- # 确保保存目录存在
98
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
99
-
100
- if IMAGE_DEBUG:
101
- logger.debug(f"下载图片: {image_url} -> {save_path}")
102
-
103
- # 配置代理
104
- proxy = None
105
- if Config.PROXY_HOST and Config.PROXY_PORT:
106
- proxy_auth = None
107
- if Config.PROXY_USER and Config.PROXY_PASS:
108
- proxy_auth = aiohttp.BasicAuth(Config.PROXY_USER, Config.PROXY_PASS)
109
-
110
- proxy_url = f"http://{Config.PROXY_HOST}:{Config.PROXY_PORT}"
111
- if IMAGE_DEBUG:
112
- auth_info = f" (使用认证)" if proxy_auth else ""
113
- logger.debug(f"使用代理: {proxy_url}{auth_info}")
114
- proxy = proxy_url
115
-
116
- # 下载图片
117
- timeout = aiohttp.ClientTimeout(total=60)
118
- async with aiohttp.ClientSession(timeout=timeout) as session:
119
- # 创建请求参数
120
- request_kwargs = {"timeout": 30}
121
- if proxy:
122
- request_kwargs["proxy"] = proxy
123
- if Config.PROXY_USER and Config.PROXY_PASS:
124
- request_kwargs["proxy_auth"] = aiohttp.BasicAuth(Config.PROXY_USER, Config.PROXY_PASS)
125
-
126
- async with session.get(image_url, **request_kwargs) as response:
127
- if response.status != 200:
128
- logger.warning(f"下载失败,状态码: {response.status}, URL: {image_url}")
129
- return image_url
130
-
131
- content = await response.read()
132
- if not content:
133
- logger.warning("下载内容为空")
134
- return image_url
135
-
136
- # 保存图片
137
- async with aiofiles.open(save_path, "wb") as f:
138
- await f.write(content)
139
-
140
- # 检查文件是否成功保存
141
- if not os.path.exists(save_path) or os.path.getsize(save_path) == 0:
142
- logger.warning(f"图片保存失败: {save_path}")
143
- if os.path.exists(save_path):
144
- os.remove(save_path)
145
- return image_url
146
-
147
- # 返回本地URL
148
- # 获取文件名
149
- filename = os.path.basename(save_path)
150
-
151
- # 解析基础URL
152
- parsed_base_url = urlparse(Config.BASE_URL)
153
- base_path = parsed_base_url.path.rstrip('/')
154
-
155
- # 使用固定的图片URL格式
156
- relative_url = f"/images/{filename}"
157
-
158
- # 如果设置了STATIC_PATH_PREFIX,优先使用该前缀
159
- if Config.STATIC_PATH_PREFIX:
160
- prefix = Config.STATIC_PATH_PREFIX
161
- if not prefix.startswith('/'):
162
- prefix = f"/{prefix}"
163
- relative_url = f"{prefix}/images/{filename}"
164
-
165
- # 如果BASE_URL有子路径,添加到相对路径前
166
- if base_path:
167
- relative_url = f"{base_path}{relative_url}"
168
-
169
- # 处理重复斜杠
170
- while "//" in relative_url:
171
- relative_url = relative_url.replace("//", "/")
172
-
173
- # 生成完整URL
174
- full_url = f"{parsed_base_url.scheme}://{parsed_base_url.netloc}{relative_url}"
175
-
176
- if IMAGE_DEBUG:
177
- logger.debug(f"图片保存成功: {full_url}")
178
- logger.debug(f"图片保存路径: {save_path}")
179
-
180
- return full_url
181
- except Exception as e:
182
- logger.error(f"图片下载失败: {str(e)}", exc_info=IMAGE_DEBUG)
183
- return image_url
184
-
185
- async def localize_image_urls(image_urls: list) -> list:
186
- """
187
- 批量将图片URL本地化
188
-
189
- Args:
190
- image_urls: 图片URL列表
191
-
192
- Returns:
193
- 本地化后的URL列表
194
- """
195
- if not Config.IMAGE_LOCALIZATION or not image_urls:
196
- return image_urls
197
-
198
- if IMAGE_DEBUG:
199
- logger.debug(f"本地化 {len(image_urls)} 个URL: {image_urls}")
200
- else:
201
- logger.info(f"本地化 {len(image_urls)} 个图片")
202
-
203
- localized_urls = []
204
- for url in image_urls:
205
- localized_url = await download_and_save_image(url)
206
- localized_urls.append(localized_url)
207
-
208
- return localized_urls
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/test_client.py DELETED
@@ -1,180 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
- import requests
4
- import json
5
- import time
6
- import sys
7
- import base64
8
- import os
9
-
10
- # 设置UTF-8编码
11
- if sys.platform.startswith('win'):
12
- os.system("chcp 65001")
13
- if hasattr(sys.stdout, 'reconfigure'):
14
- sys.stdout.reconfigure(encoding='utf-8')
15
-
16
- API_URL = "http://127.0.0.1:8890/v1/chat/completions"
17
- API_KEY = "sk-123456" # 替换为实际的API key
18
-
19
- def test_text_to_image(prompt="生成一只可爱的猫咪", stream=False):
20
- """测试文本到图像生成"""
21
- print(f"\n===== 测试文本到图像生成 =====")
22
- try:
23
- print(f"提示词: '{prompt}'")
24
- except UnicodeEncodeError:
25
- print(f"提示词: [包含非ASCII字符]")
26
- print(f"流式响应: {stream}")
27
-
28
- headers = {
29
- "Content-Type": "application/json",
30
- "Authorization": f"Bearer {API_KEY}"
31
- }
32
-
33
- payload = {
34
- "model": "sora-1.0",
35
- "messages": [
36
- {"role": "user", "content": prompt}
37
- ],
38
- "n": 1,
39
- "stream": stream
40
- }
41
-
42
- start_time = time.time()
43
- response = requests.post(
44
- API_URL,
45
- headers=headers,
46
- json=payload,
47
- stream=stream
48
- )
49
-
50
- if response.status_code != 200:
51
- print(f"错误: 状态码 {response.status_code}")
52
- print(response.text)
53
- return
54
-
55
- if stream:
56
- # 处理流式响应
57
- print("流式响应内容:")
58
- for line in response.iter_lines():
59
- if line:
60
- line = line.decode('utf-8')
61
- if line.startswith("data: "):
62
- data = line[6:]
63
- if data == "[DONE]":
64
- print("[完成]")
65
- else:
66
- try:
67
- json_data = json.loads(data)
68
- if 'choices' in json_data and json_data['choices'] and 'delta' in json_data['choices'][0]:
69
- delta = json_data['choices'][0]['delta']
70
- if 'content' in delta:
71
- print(f"接收内容: {delta['content']}")
72
- except Exception as e:
73
- print(f"解析响应时出错: {e}")
74
- else:
75
- # 处理普通响应
76
- try:
77
- data = response.json()
78
- print(f"响应内容:")
79
- print(json.dumps(data, indent=2, ensure_ascii=False))
80
-
81
- if 'choices' in data and data['choices']:
82
- image_url = None
83
- content = data['choices'][0]['message']['content']
84
- if "![Generated Image](" in content:
85
- image_url = content.split("![Generated Image](")[1].split(")")[0]
86
- print(f"\n生成的图片URL: {image_url}")
87
- except Exception as e:
88
- print(f"解析响应时出错: {e}")
89
-
90
- elapsed = time.time() - start_time
91
- print(f"请求耗时: {elapsed:.2f}秒")
92
-
93
- def test_image_to_image(image_path, prompt="将这张图片变成动漫风格"):
94
- """测试图像到图像生成(Remix)"""
95
- print(f"\n===== 测试图像到图像生成 =====")
96
- print(f"图片路径: '{image_path}'")
97
- print(f"提示词: '{prompt}'")
98
-
99
- # 读取并转换图片为base64
100
- try:
101
- with open(image_path, "rb") as image_file:
102
- base64_image = base64.b64encode(image_file.read()).decode('utf-8')
103
- except Exception as e:
104
- print(f"读取图片失败: {e}")
105
- return
106
-
107
- # 构建请求
108
- headers = {
109
- "Content-Type": "application/json",
110
- "Authorization": f"Bearer {API_KEY}"
111
- }
112
-
113
- payload = {
114
- "model": "sora-1.0",
115
- "messages": [
116
- {"role": "user", "content": f"data:image/jpeg;base64,{base64_image}\n{prompt}"}
117
- ],
118
- "n": 1,
119
- "stream": False
120
- }
121
-
122
- start_time = time.time()
123
- response = requests.post(
124
- API_URL,
125
- headers=headers,
126
- json=payload
127
- )
128
-
129
- if response.status_code != 200:
130
- print(f"错误: 状态码 {response.status_code}")
131
- print(response.text)
132
- return
133
-
134
- # 处理响应
135
- try:
136
- data = response.json()
137
- print(f"响应内容:")
138
- print(json.dumps(data, indent=2, ensure_ascii=False))
139
-
140
- if 'choices' in data and data['choices']:
141
- image_url = None
142
- content = data['choices'][0]['message']['content']
143
- if "![Generated Image](" in content:
144
- image_url = content.split("![Generated Image](")[1].split(")")[0]
145
- print(f"\n生成的图片URL: {image_url}")
146
- except Exception as e:
147
- print(f"解析响应时出错: {e}")
148
-
149
- elapsed = time.time() - start_time
150
- print(f"请求耗时: {elapsed:.2f}秒")
151
-
152
- def main():
153
- """主函数"""
154
- if len(sys.argv) < 2:
155
- print("用法: python test_client.py <测试类型> [参数...]")
156
- print("测试类型:")
157
- print(" text2img <提示词> [stream=true/false]")
158
- print(" img2img <图片路径> <提示词>")
159
- return
160
-
161
- test_type = sys.argv[1].lower()
162
-
163
- if test_type == "text2img":
164
- prompt = sys.argv[2] if len(sys.argv) > 2 else "生成一只可爱的猫咪"
165
- stream = False
166
- if len(sys.argv) > 3 and sys.argv[3].lower() == "stream=true":
167
- stream = True
168
- test_text_to_image(prompt, stream)
169
- elif test_type == "img2img":
170
- if len(sys.argv) < 3:
171
- print("错误: 需要图片路径")
172
- return
173
- image_path = sys.argv[2]
174
- prompt = sys.argv[3] if len(sys.argv) > 3 else "将这张图片变成动漫风格"
175
- test_image_to_image(image_path, prompt)
176
- else:
177
- print(f"错误: 未知的测试类型 '{test_type}'")
178
-
179
- if __name__ == "__main__":
180
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sora-api-master/test_image_download.py DELETED
@@ -1,131 +0,0 @@
1
- import asyncio
2
- import os
3
- import sys
4
- import logging
5
-
6
- # 配置日志
7
- logging.basicConfig(
8
- level=logging.DEBUG,
9
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
10
- )
11
-
12
- # 添加src目录到Python路径
13
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
14
-
15
- from src.utils import download_and_save_image, localize_image_urls
16
- from src.config import Config
17
-
18
- # 设置为True启用本地化
19
- Config.IMAGE_LOCALIZATION = True
20
- # 打开调试日志
21
- os.environ["IMAGE_DEBUG"] = "true"
22
- # 确保目录存在
23
- os.makedirs(Config.IMAGE_SAVE_DIR, exist_ok=True)
24
-
25
- async def test_single_download():
26
- """测试单个图片下载"""
27
- # 使用一个可靠的图片URL进行测试
28
- test_url = "https://pic.baidu.com/feed/b90e7bec54e736d1f9ddbe94c2691f254d4ade13.jpeg"
29
- print(f"测试单个图片下载: {test_url}")
30
-
31
- local_url = await download_and_save_image(test_url)
32
- print(f"下载结果: {local_url}")
33
-
34
- # 检查文件是否真的下载了
35
- if local_url.startswith("http"):
36
- # 处理完整URL的情况
37
- url_path = local_url.split(Config.BASE_URL, 1)[-1]
38
- if url_path.startswith("/static/"):
39
- relative_path = url_path[len("/static/"):]
40
- file_path = os.path.join(Config.STATIC_DIR, relative_path)
41
- print(f"完整URL转换为文件路径: {file_path}")
42
-
43
- if os.path.exists(file_path):
44
- print(f"文件存在: {file_path}, 大小: {os.path.getsize(file_path)} 字节")
45
- else:
46
- print(f"文件不存在: {file_path}")
47
- # 尝试查找可能的文件
48
- dir_path = os.path.dirname(file_path)
49
- if os.path.exists(dir_path):
50
- print(f"目录存在: {dir_path}")
51
- print(f"目录内容: {os.listdir(dir_path)}")
52
- else:
53
- print(f"目录不存在: {dir_path}")
54
- else:
55
- print(f"URL格式异常: {local_url}")
56
- elif local_url.startswith("/static/"):
57
- # 从URL恢复实际的文件路径
58
- relative_path = local_url[len("/static/"):]
59
- file_path = os.path.join(Config.STATIC_DIR, relative_path)
60
- print(f"相对URL转换为文件路径: {file_path}")
61
-
62
- if os.path.exists(file_path):
63
- print(f"文件存在: {file_path}, 大小: {os.path.getsize(file_path)} 字节")
64
- else:
65
- print(f"文件不存在: {file_path}")
66
- else:
67
- print("下载失败,返回了原始URL")
68
-
69
- async def test_multiple_downloads():
70
- """测试多个图片下载"""
71
- test_urls = [
72
- "https://pic.baidu.com/feed/b90e7bec54e736d1f9ddbe94c2691f254d4ade13.jpeg",
73
- "https://pic1.zhimg.com/v2-b78b719d8782ad5146851b87bbd3a9fb_r.jpg"
74
- ]
75
- print(f"\n测试多个图片下载: {test_urls}")
76
-
77
- local_urls = await localize_image_urls(test_urls)
78
-
79
- print(f"本地化结果: {local_urls}")
80
-
81
- # 验证所有文件是否下载成功
82
- for i, url in enumerate(local_urls):
83
- print(f"\n检查文件 {i+1}:")
84
- if url.startswith("http"):
85
- # 处理完整URL的情况
86
- if url.startswith(Config.BASE_URL):
87
- url_path = url.split(Config.BASE_URL, 1)[-1]
88
- if url_path.startswith("/static/"):
89
- relative_path = url_path[len("/static/"):]
90
- file_path = os.path.join(Config.STATIC_DIR, relative_path)
91
- print(f"完整URL转换为文件路径: {file_path}")
92
-
93
- if os.path.exists(file_path):
94
- print(f"文件 {i+1} 存在: {file_path}, 大小: {os.path.getsize(file_path)} 字节")
95
- else:
96
- print(f"文件 {i+1} 不存在: {file_path}")
97
- # 尝试查找可能的文件
98
- dir_path = os.path.dirname(file_path)
99
- if os.path.exists(dir_path):
100
- print(f"目录存在: {dir_path}")
101
- print(f"目录内容: {os.listdir(dir_path)}")
102
- else:
103
- print(f"目录不存在: {dir_path}")
104
- else:
105
- print(f"URL格式异常: {url}")
106
- else:
107
- print(f"文件 {i+1} 下载失败,返回了原始URL: {url}")
108
- elif url.startswith("/static/"):
109
- # 从URL恢复实际的文件路径
110
- relative_path = url[len("/static/"):]
111
- file_path = os.path.join(Config.STATIC_DIR, relative_path)
112
- print(f"相对URL转换为文件路径: {file_path}")
113
-
114
- if os.path.exists(file_path):
115
- print(f"文件 {i+1} 存在: {file_path}, 大小: {os.path.getsize(file_path)} 字节")
116
- else:
117
- print(f"文件 {i+1} 不存在: {file_path}")
118
- else:
119
- print(f"文件 {i+1} 下载失败,返回了原始URL: {url}")
120
-
121
- async def main():
122
- print(f"配置信息:")
123
- print(f"IMAGE_LOCALIZATION: {Config.IMAGE_LOCALIZATION}")
124
- print(f"STATIC_DIR: {Config.STATIC_DIR}")
125
- print(f"IMAGE_SAVE_DIR: {Config.IMAGE_SAVE_DIR}")
126
-
127
- await test_single_download()
128
- await test_multiple_downloads()
129
-
130
- if __name__ == "__main__":
131
- asyncio.run(main())