hongshi2025 commited on
Commit
c568b1e
·
verified ·
1 Parent(s): 60a2d05

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +1459 -0
main.py ADDED
@@ -0,0 +1,1459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Depends, HTTPException
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from fastapi.responses import StreamingResponse, HTMLResponse, RedirectResponse
4
+ from fastapi.background import BackgroundTasks
5
+ from contextlib import asynccontextmanager
6
+ import requests
7
+ from curl_cffi import requests as cffi_requests
8
+ import uuid
9
+ import json
10
+ import time
11
+ from typing import Optional
12
+ import asyncio
13
+ import base64
14
+ import tempfile
15
+ import os
16
+ import re
17
+ import threading
18
+ import logging
19
+ from dotenv import load_dotenv
20
+ from playwright.sync_api import sync_playwright
21
+ from datetime import datetime, timezone, timedelta
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ import random
24
+
25
+ # 加载环境变量
26
+ load_dotenv(override=True)
27
+
28
+ # 配置日志
29
+ logging.basicConfig(
30
+ level=logging.INFO, # 改为 INFO 级别
31
+ format='%(asctime)s - %(levelname)s - %(message)s'
32
+ )
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # 修改全局数据存储
36
+ global_data = {
37
+ "cookie": None,
38
+ "cookies": None,
39
+ "last_update": 0,
40
+ "cookie_expires": 0, # 添加 cookie 过期时间
41
+ "is_refreshing": False # 添加刷新状态标志
42
+ }
43
+
44
+ @asynccontextmanager
45
+ async def lifespan(app: FastAPI):
46
+ # 启动时获取 cookie
47
+ logger.info("Starting FastAPI application, initializing cookie fetcher...")
48
+
49
+ # 创建并启动线程
50
+ cookie_thread = threading.Thread(target=get_cookie_with_retry)
51
+ cookie_thread.daemon = True # 设置为守护线程
52
+ cookie_thread.start()
53
+
54
+ # 创建并启动自动刷新线程
55
+ refresh_thread = threading.Thread(target=auto_refresh_cookie)
56
+ refresh_thread.daemon = True
57
+ refresh_thread.start()
58
+
59
+ logger.info("Cookie fetcher and auto-refresh threads started")
60
+ yield
61
+
62
+ # 关闭时清理资源
63
+ logger.info("Shutting down FastAPI application")
64
+ global_data["cookie"] = None
65
+ global_data["cookies"] = None
66
+ global_data["last_update"] = 0
67
+ global_data["is_refreshing"] = False
68
+
69
+ def get_cookie_with_retry(max_retries=3, retry_delay=5):
70
+ """带重试机制的获取 cookie 函数"""
71
+ retries = 0
72
+ while retries < max_retries:
73
+ logger.info(f"Cookie fetching attempt {retries + 1}/{max_retries}")
74
+ cookie = get_cookie()
75
+ if cookie:
76
+ logger.info("Successfully retrieved cookie")
77
+ return cookie
78
+
79
+ retries += 1
80
+ if retries < max_retries:
81
+ logger.info(f"Retrying cookie fetch in {retry_delay} seconds...")
82
+ time.sleep(retry_delay)
83
+
84
+ logger.error(f"Failed to fetch cookie after {max_retries} attempts")
85
+ return None
86
+
87
+ app = FastAPI(lifespan=lifespan)
88
+ security = HTTPBearer()
89
+
90
+ # OpenAI API Key 配置,可以通过环境变量覆盖
91
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", None)
92
+ logger.info(f"OPENAI_API_KEY is set: {OPENAI_API_KEY is not None}")
93
+ # logger.info(f"OPENAI_API_KEY value: {OPENAI_API_KEY}")
94
+
95
+ def get_random_browser_fingerprint():
96
+ """生成随机的浏览器指纹"""
97
+ # 随机选择浏览器版本
98
+ chrome_versions = ["120", "121", "122", "123", "124", "125"]
99
+ edge_versions = ["120", "121", "122", "123", "124", "125"]
100
+ selected_version = random.choice(chrome_versions)
101
+ edge_version = random.choice(edge_versions)
102
+
103
+ # 随机选择操作系统
104
+ os_versions = [
105
+ "Windows NT 10.0; Win64; x64",
106
+ "Macintosh; Intel Mac OS X 10_15_7",
107
+ "Macintosh; Intel Mac OS X 11_0_1",
108
+ "Macintosh; Intel Mac OS X 12_0_1"
109
+ ]
110
+ selected_os = random.choice(os_versions)
111
+
112
+ # 随机选择语言偏好
113
+ languages = [
114
+ "en-US,en;q=0.9",
115
+ "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
116
+ "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
117
+ "en-GB,en;q=0.9,en-US;q=0.8"
118
+ ]
119
+ selected_language = random.choice(languages)
120
+
121
+ # 随机选择视口大小
122
+ viewport_sizes = [
123
+ (1920, 1080),
124
+ (1366, 768),
125
+ (1440, 900),
126
+ (1536, 864),
127
+ (1680, 1050)
128
+ ]
129
+ selected_viewport = random.choice(viewport_sizes)
130
+
131
+ # 构建用户代理字符串
132
+ user_agent = f"Mozilla/5.0 ({selected_os}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{selected_version}.0.0.0 Safari/537.36 Edg/{edge_version}.0.0.0"
133
+
134
+ # 构建请求头
135
+ headers = {
136
+ "accept": "*/*",
137
+ "accept-language": selected_language,
138
+ "content-type": "application/json",
139
+ "origin": "https://chat.akash.network",
140
+ "referer": "https://chat.akash.network/",
141
+ "sec-ch-ua": f'"Microsoft Edge";v="{edge_version}", "Not-A.Brand";v="8", "Chromium";v="{selected_version}"',
142
+ "sec-ch-ua-mobile": "?0",
143
+ "sec-ch-ua-platform": '"macOS"',
144
+ "sec-fetch-dest": "empty",
145
+ "sec-fetch-mode": "cors",
146
+ "sec-fetch-site": "same-origin",
147
+ "user-agent": user_agent
148
+ }
149
+
150
+ return {
151
+ "headers": headers,
152
+ "viewport": selected_viewport,
153
+ "user_agent": user_agent
154
+ }
155
+
156
+ def get_cookie():
157
+ """获取 cookie 的函数"""
158
+ browser = None
159
+ context = None
160
+ page = None
161
+
162
+ try:
163
+ logger.info("Starting cookie retrieval process...")
164
+
165
+ # 获取随机浏览器指纹
166
+ fingerprint = get_random_browser_fingerprint()
167
+ logger.info(f"Using browser fingerprint: {fingerprint['user_agent']}")
168
+
169
+ with sync_playwright() as p:
170
+ try:
171
+ # 启动浏览器
172
+ logger.info("Launching browser...")
173
+ browser = p.chromium.launch(
174
+ headless=True,
175
+ args=[
176
+ '--no-sandbox',
177
+ '--disable-dev-shm-usage',
178
+ '--disable-gpu',
179
+ '--disable-software-rasterizer',
180
+ '--disable-extensions',
181
+ '--disable-setuid-sandbox',
182
+ '--no-first-run',
183
+ '--no-zygote',
184
+ '--single-process',
185
+ f'--window-size={fingerprint["viewport"][0]},{fingerprint["viewport"][1]}',
186
+ '--disable-blink-features=AutomationControlled',
187
+ '--disable-features=IsolateOrigins,site-per-process'
188
+ ]
189
+ )
190
+
191
+ logger.info("Browser launched successfully")
192
+
193
+ # 创建上下文,使用随机指纹
194
+ logger.info("Creating browser context...")
195
+ context = browser.new_context(
196
+ viewport={'width': fingerprint["viewport"][0], 'height': fingerprint["viewport"][1]},
197
+ user_agent=fingerprint["user_agent"],
198
+ locale='en-US',
199
+ timezone_id='America/New_York',
200
+ permissions=['geolocation'],
201
+ extra_http_headers=fingerprint["headers"]
202
+ )
203
+
204
+ # 添加脚本以覆盖 navigator.webdriver
205
+ context.add_init_script("""
206
+ Object.defineProperty(navigator, 'webdriver', {
207
+ get: () => false,
208
+ });
209
+ // 更多指纹伪装
210
+ Object.defineProperty(navigator, 'plugins', {
211
+ get: () => [1, 2, 3, 4, 5],
212
+ });
213
+ """)
214
+
215
+ logger.info("Browser context created successfully")
216
+
217
+ # 创建页面
218
+ logger.info("Creating new page...")
219
+ page = context.new_page()
220
+ logger.info("Page created successfully")
221
+
222
+ # 设置页面超时
223
+ page.set_default_timeout(60000)
224
+
225
+ # 访问目标网站,添加重试机制
226
+ max_retries = 3
227
+ retry_delay = 5
228
+
229
+ for attempt in range(max_retries):
230
+ try:
231
+ logger.info(f"Navigating to target website (attempt {attempt + 1}/{max_retries})...")
232
+ page.goto("https://chat.akash.network/", timeout=50000)
233
+ break
234
+ except Exception as e:
235
+ if attempt == max_retries - 1:
236
+ raise
237
+ logger.warning(f"Navigation attempt {attempt + 1} failed: {e}")
238
+ time.sleep(retry_delay)
239
+
240
+ # 等待页面加载
241
+ logger.info("Waiting for page load...")
242
+ try:
243
+ # 首先等待 DOM 加载完成
244
+ page.wait_for_load_state("domcontentloaded", timeout=30000)
245
+ logger.info("DOM content loaded")
246
+
247
+ # 等待一段时间,让 Cloudflare 检查完成
248
+ logger.info("Waiting for Cloudflare check...")
249
+ time.sleep(5)
250
+
251
+ # 尝试点击页面,模拟用户行为
252
+ try:
253
+ page.mouse.move(100, 100)
254
+ page.mouse.click(100, 100)
255
+ logger.info("Simulated user interaction")
256
+
257
+ # 随机滚动页面
258
+ page.mouse.wheel(0, 100)
259
+ time.sleep(0.5)
260
+ page.mouse.wheel(0, -50)
261
+ logger.info("Simulated scrolling")
262
+ except Exception as e:
263
+ logger.warning(f"Failed to simulate user interaction: {e}")
264
+
265
+ # 再次等待一段时间
266
+ time.sleep(5)
267
+
268
+ except Exception as e:
269
+ logger.warning(f"Timeout waiting for load state: {e}")
270
+
271
+ # 等待��长时间确保页面完全加载
272
+ try:
273
+ page.wait_for_load_state("networkidle", timeout=10000)
274
+ logger.info("Network idle reached")
275
+ except Exception as e:
276
+ logger.warning(f"Timeout waiting for network idle: {e}")
277
+
278
+ # 获取 cookies
279
+ logger.info("Getting cookies...")
280
+ cookies = context.cookies()
281
+
282
+ if not cookies:
283
+ logger.error("No cookies found")
284
+ return None
285
+
286
+ # 记录所有 cookie 名称以进行调试
287
+ cookie_names = [cookie['name'] for cookie in cookies]
288
+ logger.info(f"Retrieved cookies: {cookie_names}")
289
+
290
+ # 检查是否有 cf_clearance cookie
291
+ cf_cookie = next((cookie for cookie in cookies if cookie['name'] == 'cf_clearance'), None)
292
+ if not cf_cookie:
293
+ logger.error("cf_clearance cookie not found")
294
+ return None
295
+
296
+ # 检查是否有 session_token cookie
297
+ session_cookie = next((cookie for cookie in cookies if cookie['name'] == 'session_token'), None)
298
+ if not session_cookie:
299
+ logger.error("session_token cookie not found")
300
+ # 继续执行,因为某些情况下可能不需要 session_token
301
+
302
+ # 构建 cookie 字符串
303
+ cookie_str = '; '.join([f"{cookie['name']}={cookie['value']}" for cookie in cookies])
304
+ logger.info(f"Cookie string length: {len(cookie_str)}")
305
+
306
+ global_data["cookie"] = cookie_str
307
+ global_data["cookies"] = cookies # 保存完整的 cookies 列表
308
+ global_data["last_update"] = time.time()
309
+
310
+ # 设置 cookie 过期时间
311
+ if session_cookie and 'expires' in session_cookie and session_cookie['expires'] > 0:
312
+ global_data["cookie_expires"] = session_cookie['expires']
313
+ logger.info(f"Session token expires at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(session_cookie['expires']))}")
314
+ else:
315
+ # 如果没有明确的过期时间,默认设置为30分钟后过期
316
+ global_data["cookie_expires"] = time.time() + 1800 # 30 分钟
317
+ logger.info("No explicit expiration in session_token cookie, setting default 30 minute expiration")
318
+
319
+ logger.info("Successfully retrieved cookies")
320
+ return cookie_str
321
+
322
+ except Exception as e:
323
+ logger.error(f"Error in browser operations: {e}")
324
+ logger.error(f"Error type: {type(e)}")
325
+ import traceback
326
+ logger.error(f"Traceback: {traceback.format_exc()}")
327
+ return None
328
+ finally:
329
+ # 确保资源被正确关闭
330
+ try:
331
+ if page:
332
+ logger.info("Closing page...")
333
+ try:
334
+ page.close()
335
+ logger.info("Page closed successfully")
336
+ except Exception as e:
337
+ logger.error(f"Error closing page: {e}")
338
+ except Exception as e:
339
+ logger.error(f"Error in page cleanup: {e}")
340
+
341
+ try:
342
+ if context:
343
+ logger.info("Closing context...")
344
+ try:
345
+ context.close()
346
+ logger.info("Context closed successfully")
347
+ except Exception as e:
348
+ logger.error(f"Error closing context: {e}")
349
+ except Exception as e:
350
+ logger.error(f"Error in context cleanup: {e}")
351
+
352
+ try:
353
+ if browser:
354
+ logger.info("Closing browser...")
355
+ try:
356
+ browser.close()
357
+ logger.info("Browser closed successfully")
358
+ except Exception as e:
359
+ logger.error(f"Error closing browser: {e}")
360
+ except Exception as e:
361
+ logger.error(f"Error in browser cleanup: {e}")
362
+
363
+ # 确保所有资源都被清理
364
+ page = None
365
+ context = None
366
+ browser = None
367
+
368
+ # 主动触发垃圾回收
369
+ import gc
370
+ gc.collect()
371
+ logger.info("Resource cleanup completed")
372
+
373
+ except Exception as e:
374
+ logger.error(f"Error fetching cookie: {str(e)}")
375
+ logger.error(f"Error type: {type(e)}")
376
+ import traceback
377
+ logger.error(f"Traceback: {traceback.format_exc()}")
378
+
379
+ # 最后再次确保资源被清理
380
+ if page:
381
+ try:
382
+ page.close()
383
+ except:
384
+ pass
385
+ if context:
386
+ try:
387
+ context.close()
388
+ except:
389
+ pass
390
+ if browser:
391
+ try:
392
+ browser.close()
393
+ except:
394
+ pass
395
+
396
+ # 主动触发垃圾回收
397
+ import gc
398
+ gc.collect()
399
+
400
+ return None
401
+
402
+ # 添加刷新 cookie 的函数
403
+ async def refresh_cookie():
404
+ """刷新 cookie 的函数,用于401错误触发"""
405
+ logger.info("Refreshing cookie due to 401 error")
406
+
407
+ # 如果已经在刷新中,等待一段时间
408
+ if global_data["is_refreshing"]:
409
+ logger.info("Cookie refresh already in progress, waiting...")
410
+ # 等待最多10秒
411
+ for _ in range(10):
412
+ await asyncio.sleep(1)
413
+ if not global_data["is_refreshing"]:
414
+ break
415
+
416
+ # 如果仍然在刷新中,强制刷新
417
+ if global_data["is_refreshing"]:
418
+ logger.info("Forcing cookie refresh due to 401 error")
419
+ global_data["is_refreshing"] = False
420
+
421
+ try:
422
+ global_data["is_refreshing"] = True
423
+ # 标记 cookie 为过期
424
+ global_data["cookie_expires"] = 0
425
+ # 调用同步函数进行cookie获取,使用线程池不阻塞事件循环
426
+ executor = ThreadPoolExecutor(max_workers=1)
427
+ loop = asyncio.get_event_loop()
428
+ new_cookie = await loop.run_in_executor(executor, get_cookie_with_retry)
429
+ return new_cookie
430
+ finally:
431
+ global_data["is_refreshing"] = False
432
+
433
+ async def background_refresh_cookie():
434
+ """后台刷新 cookie 的函数,不影响接口调用"""
435
+ if global_data["is_refreshing"]:
436
+ logger.info("Cookie refresh already in progress, skipping")
437
+ return
438
+
439
+ try:
440
+ global_data["is_refreshing"] = True
441
+ logger.info("Starting background cookie refresh")
442
+
443
+ # 使用线程池执行同步函数
444
+ executor = ThreadPoolExecutor(max_workers=1)
445
+ loop = asyncio.get_event_loop()
446
+ new_cookie = await loop.run_in_executor(executor, get_cookie)
447
+
448
+ if new_cookie:
449
+ logger.info("Background cookie refresh successful")
450
+ # 更新 cookie 和过期时间
451
+ global_data["cookie"] = new_cookie
452
+ global_data["last_update"] = time.time()
453
+ # 查找 session_token cookie 的过期时间
454
+ session_cookie = next((cookie for cookie in global_data["cookies"] if cookie['name'] == 'session_token'), None)
455
+ if session_cookie and 'expires' in session_cookie and session_cookie['expires'] > 0:
456
+ global_data["cookie_expires"] = session_cookie['expires']
457
+ logger.info(f"Session token expires at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(session_cookie['expires']))}")
458
+ else:
459
+ # 如果没有明确的过期时间,默认设置为30分钟后过期
460
+ global_data["cookie_expires"] = time.time() + 1800
461
+ logger.info("No explicit expiration in session_token cookie, setting default 30 minute expiration")
462
+ else:
463
+ logger.error("Background cookie refresh failed")
464
+ except Exception as e:
465
+ logger.error(f"Error in background cookie refresh: {e}")
466
+ finally:
467
+ global_data["is_refreshing"] = False
468
+
469
+ async def check_and_update_cookie():
470
+ """检查并更新 cookie"""
471
+ try:
472
+ current_time = time.time()
473
+ # 只在 cookie 不存在或已过期时刷新
474
+ if not global_data["cookie"] or current_time >= global_data["cookie_expires"]:
475
+ logger.info("Cookie expired or not available, starting refresh")
476
+ try:
477
+ # 使用线程池执行同步的 get_cookie 函数
478
+ loop = asyncio.get_event_loop()
479
+ with ThreadPoolExecutor() as executor:
480
+ new_cookie = await loop.run_in_executor(executor, get_cookie)
481
+
482
+ if new_cookie:
483
+ logger.info("Cookie refresh successful")
484
+ else:
485
+ logger.error("Cookie refresh failed")
486
+ except Exception as e:
487
+ logger.error(f"Error during cookie refresh: {e}")
488
+ import traceback
489
+ logger.error(f"Traceback: {traceback.format_exc()}")
490
+ else:
491
+ logger.info("Using existing cookie")
492
+
493
+ except Exception as e:
494
+ logger.error(f"Error in check_and_update_cookie: {e}")
495
+ import traceback
496
+ logger.error(f"Traceback: {traceback.format_exc()}")
497
+
498
+ async def get_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)):
499
+ token = credentials.credentials
500
+ # logger.info(f"Received token: {token}")
501
+
502
+ # 如果设置了 OPENAI_API_KEY,则需要验证
503
+ if OPENAI_API_KEY is not None:
504
+ # 去掉 Bearer 前缀后再比较
505
+ clean_token = token.replace("Bearer ", "") if token.startswith("Bearer ") else token
506
+ # logger.info(f"Clean token: {clean_token}")
507
+ if clean_token != OPENAI_API_KEY:
508
+ logger.error(f"Token mismatch. Expected: {OPENAI_API_KEY}, Got: {clean_token}")
509
+ raise HTTPException(
510
+ status_code=401,
511
+ detail="Invalid API key"
512
+ )
513
+ logger.info("API key validation passed")
514
+
515
+ return True
516
+
517
+ async def validate_cookie(background_tasks: BackgroundTasks):
518
+ # 检查并更新 cookie(如果需要)
519
+ await check_and_update_cookie()
520
+
521
+ # 等待 cookie 初始化完成
522
+ max_wait = 30 # 最大等待时间(秒)
523
+ start_time = time.time()
524
+ while not global_data["cookie"] and time.time() - start_time < max_wait:
525
+ await asyncio.sleep(1)
526
+ logger.info("Waiting for cookie initialization...")
527
+
528
+ # 检查是否有有效的 cookie
529
+ if not global_data["cookie"]:
530
+ logger.error("Cookie not available after waiting")
531
+ raise HTTPException(
532
+ status_code=503,
533
+ detail="Service temporarily unavailable - Cookie not available"
534
+ )
535
+
536
+ logger.info("Cookie validation passed")
537
+ return global_data["cookie"]
538
+
539
+ async def check_image_status(session: requests.Session, job_id: str, headers: dict) -> Optional[str]:
540
+ """检查图片生成状态并获取生成的图片"""
541
+ max_retries = 30
542
+ for attempt in range(max_retries):
543
+ try:
544
+ print(f"\nAttempt {attempt + 1}/{max_retries} for job {job_id}")
545
+ response = session.get(
546
+ f'https://chat.akash.network/api/image-status?ids={job_id}',
547
+ headers=headers
548
+ )
549
+ print(f"Status response code: {response.status_code}")
550
+ status_data = response.json()
551
+
552
+ if status_data and isinstance(status_data, list) and len(status_data) > 0:
553
+ job_info = status_data[0]
554
+ status = job_info.get('status')
555
+ print(f"Job status: {status}")
556
+
557
+ # 只有当状态为 completed 时才处理结果
558
+ if status == "completed":
559
+ result = job_info.get("result")
560
+ if result and not result.startswith("Failed"):
561
+ print("Got valid result, attempting upload...")
562
+ image_url = await upload_to_xinyew(result, job_id)
563
+ if image_url:
564
+ print(f"Successfully uploaded image: {image_url}")
565
+ return image_url
566
+ print("Image upload failed")
567
+ return None
568
+ print("Invalid result received")
569
+ return None
570
+ elif status == "failed":
571
+ print(f"Job {job_id} failed")
572
+ return None
573
+
574
+ # 如果状态是其他(如 pending),继续等待
575
+ await asyncio.sleep(1)
576
+ continue
577
+
578
+ except Exception as e:
579
+ print(f"Error checking status: {e}")
580
+ return None
581
+
582
+ print(f"Timeout waiting for job {job_id}")
583
+ return None
584
+
585
+ @app.get("/", response_class=HTMLResponse)
586
+ async def health_check():
587
+ """健康检查端点,返回服务状态"""
588
+ # 检查 cookie 状态
589
+ cookie_status = "ok" if global_data["cookie"] else "error"
590
+ status_color = "green" if cookie_status == "ok" else "red"
591
+ status_text = "正常" if cookie_status == "ok" else "异常"
592
+
593
+ # 获取当前时间(北京时间)
594
+ current_time = datetime.now(timezone(timedelta(hours=8)))
595
+
596
+ # 格式化 cookie 过期时间(北京时间)
597
+ if global_data["cookie_expires"]:
598
+ expires_time = datetime.fromtimestamp(global_data["cookie_expires"], timezone(timedelta(hours=8)))
599
+ expires_str = expires_time.strftime("%Y-%m-%d %H:%M:%S")
600
+
601
+ # 计算剩余时间
602
+ time_left = global_data["cookie_expires"] - time.time()
603
+ hours_left = int(time_left // 3600)
604
+ minutes_left = int((time_left % 3600) // 60)
605
+
606
+ if hours_left > 0:
607
+ time_left_str = f"{hours_left}小时{minutes_left}分钟"
608
+ else:
609
+ time_left_str = f"{minutes_left}分钟"
610
+ else:
611
+ expires_str = "未知"
612
+ time_left_str = "未知"
613
+
614
+ # 格式化最后更新时间(北京时间)
615
+ if global_data["last_update"]:
616
+ last_update_time = datetime.fromtimestamp(global_data["last_update"], timezone(timedelta(hours=8)))
617
+ last_update_str = last_update_time.strftime("%Y-%m-%d %H:%M:%S")
618
+
619
+ # 计算多久前更新
620
+ time_since_update = time.time() - global_data["last_update"]
621
+ if time_since_update < 60:
622
+ update_ago = f"{int(time_since_update)}秒前"
623
+ elif time_since_update < 3600:
624
+ update_ago = f"{int(time_since_update // 60)}分钟前"
625
+ else:
626
+ update_ago = f"{int(time_since_update // 3600)}小时前"
627
+ else:
628
+ last_update_str = "从未更新"
629
+ update_ago = "未知"
630
+
631
+ status = {
632
+ "status": "ok",
633
+ "cookie_status": {
634
+ "status": cookie_status,
635
+ "status_text": status_text,
636
+ "status_color": status_color,
637
+ "expires": expires_str,
638
+ "time_left": time_left_str,
639
+ "available": bool(global_data["cookie"]),
640
+ "last_update": last_update_str,
641
+ "update_ago": update_ago
642
+ }
643
+ }
644
+
645
+ # 返回 HTML 响应
646
+ return HTMLResponse(content=f"""
647
+ <!DOCTYPE html>
648
+ <html>
649
+ <head>
650
+ <title>Akash API 服务状态</title>
651
+ <meta charset="UTF-8">
652
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
653
+ <script>
654
+ // 每30秒自动刷新页面
655
+ setTimeout(function() {{
656
+ location.reload();
657
+ }}, 30000);
658
+ </script>
659
+ <style>
660
+ body {{
661
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
662
+ margin: 0;
663
+ padding: 20px;
664
+ background-color: #f5f5f5;
665
+ color: #333;
666
+ line-height: 1.6;
667
+ }}
668
+ .container {{
669
+ max-width: 800px;
670
+ margin: 0 auto;
671
+ background-color: white;
672
+ padding: 30px;
673
+ border-radius: 12px;
674
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
675
+ }}
676
+ .header {{
677
+ display: flex;
678
+ align-items: center;
679
+ margin-bottom: 30px;
680
+ border-bottom: 1px solid #eee;
681
+ padding-bottom: 20px;
682
+ }}
683
+ .logo {{
684
+ font-size: 24px;
685
+ font-weight: bold;
686
+ color: #2c3e50;
687
+ margin-right: 15px;
688
+ display: flex;
689
+ align-items: center;
690
+ }}
691
+ .logo-icon {{
692
+ margin-right: 10px;
693
+ font-size: 28px;
694
+ }}
695
+ .status {{
696
+ display: flex;
697
+ align-items: center;
698
+ margin-bottom: 30px;
699
+ }}
700
+ .status-dot {{
701
+ width: 16px;
702
+ height: 16px;
703
+ border-radius: 50%;
704
+ margin-right: 12px;
705
+ box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.2);
706
+ }}
707
+ .status-dot.green {{
708
+ background-color: #4CAF50;
709
+ box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.2);
710
+ }}
711
+ .status-dot.red {{
712
+ background-color: #f44336;
713
+ box-shadow: 0 0 0 4px rgba(244, 67, 54, 0.2);
714
+ }}
715
+ .status-text {{
716
+ font-size: 20px;
717
+ font-weight: 600;
718
+ }}
719
+ .status-text.ok {{
720
+ color: #4CAF50;
721
+ }}
722
+ .status-text.error {{
723
+ color: #f44336;
724
+ }}
725
+ .info-section {{
726
+ background-color: #f9f9f9;
727
+ border-radius: 8px;
728
+ padding: 20px;
729
+ margin-top: 20px;
730
+ }}
731
+ .info-section h3 {{
732
+ margin-top: 0;
733
+ color: #2c3e50;
734
+ font-size: 18px;
735
+ border-bottom: 1px solid #eee;
736
+ padding-bottom: 10px;
737
+ display: flex;
738
+ align-items: center;
739
+ }}
740
+ .info-section h3 i {{
741
+ margin-right: 8px;
742
+ }}
743
+ .info-item {{
744
+ margin: 15px 0;
745
+ display: flex;
746
+ justify-content: space-between;
747
+ align-items: center;
748
+ }}
749
+ .label {{
750
+ color: #666;
751
+ font-weight: 500;
752
+ display: flex;
753
+ align-items: center;
754
+ }}
755
+ .label i {{
756
+ margin-right: 8px;
757
+ font-size: 16px;
758
+ }}
759
+ .value {{
760
+ font-weight: 600;
761
+ padding: 5px 10px;
762
+ border-radius: 4px;
763
+ background-color: #f0f0f0;
764
+ display: flex;
765
+ align-items: center;
766
+ gap: 5px;
767
+ }}
768
+ .value .status-text {{
769
+ font-weight: 600;
770
+ }}
771
+ .value .status-text.ok {{
772
+ color: #4CAF50;
773
+ }}
774
+ .value .status-text.error {{
775
+ color: #f44336;
776
+ }}
777
+ .value.available {{
778
+ color: #4CAF50;
779
+ background-color: rgba(76, 175, 80, 0.1);
780
+ }}
781
+ .value.unavailable {{
782
+ color: #f44336;
783
+ background-color: rgba(244, 67, 54, 0.1);
784
+ }}
785
+ .value i {{
786
+ font-size: 16px;
787
+ }}
788
+ .footer {{
789
+ margin-top: 30px;
790
+ text-align: center;
791
+ color: #999;
792
+ font-size: 14px;
793
+ border-top: 1px solid #eee;
794
+ padding-top: 20px;
795
+ }}
796
+ .refresh-btn {{
797
+ display: inline-block;
798
+ background-color: #3498db;
799
+ color: white;
800
+ padding: 8px 16px;
801
+ border-radius: 4px;
802
+ text-decoration: none;
803
+ margin-top: 20px;
804
+ font-weight: 500;
805
+ transition: background-color 0.3s;
806
+ }}
807
+ .refresh-btn:hover {{
808
+ background-color: #2980b9;
809
+ }}
810
+ .action-buttons {{
811
+ display: flex;
812
+ justify-content: center;
813
+ gap: 15px;
814
+ margin-top: 20px;
815
+ }}
816
+ .action-btn {{
817
+ display: inline-flex;
818
+ align-items: center;
819
+ background-color: #f8f9fa;
820
+ color: #333;
821
+ padding: 8px 16px;
822
+ border-radius: 4px;
823
+ text-decoration: none;
824
+ font-weight: 500;
825
+ transition: all 0.3s;
826
+ border: 1px solid #ddd;
827
+ }}
828
+ .action-btn:hover {{
829
+ background-color: #e9ecef;
830
+ border-color: #ced4da;
831
+ }}
832
+ .action-btn i {{
833
+ margin-right: 8px;
834
+ }}
835
+ .status-badge {{
836
+ display: inline-block;
837
+ padding: 4px 8px;
838
+ border-radius: 4px;
839
+ font-size: 14px;
840
+ font-weight: 500;
841
+ margin-left: 10px;
842
+ }}
843
+ .status-badge.ok {{
844
+ background-color: rgba(76, 175, 80, 0.1);
845
+ color: #4CAF50;
846
+ }}
847
+ .status-badge.error {{
848
+ background-color: rgba(244, 67, 54, 0.1);
849
+ color: #f44336;
850
+ }}
851
+ .time-info {{
852
+ font-size: 14px;
853
+ color: #666;
854
+ margin-top: 5px;
855
+ }}
856
+ .api-info {{
857
+ margin-top: 30px;
858
+ background-color: #f0f7ff;
859
+ border-radius: 8px;
860
+ padding: 20px;
861
+ border-left: 4px solid #3498db;
862
+ }}
863
+ .api-info h3 {{
864
+ margin-top: 0;
865
+ color: #2c3e50;
866
+ font-size: 18px;
867
+ }}
868
+ .api-info p {{
869
+ margin: 10px 0;
870
+ }}
871
+ .api-info code {{
872
+ background-color: #e9ecef;
873
+ padding: 2px 5px;
874
+ border-radius: 3px;
875
+ font-family: monospace;
876
+ }}
877
+ .contact-info {{
878
+ display: flex;
879
+ align-items: center;
880
+ justify-content: center;
881
+ gap: 15px;
882
+ margin: 15px 0;
883
+ }}
884
+ .contact-avatar {{
885
+ width: 40px;
886
+ height: 40px;
887
+ border-radius: 50%;
888
+ object-fit: cover;
889
+ border: 2px solid #eee;
890
+ }}
891
+ .contact-logo {{
892
+ display: flex;
893
+ align-items: center;
894
+ justify-content: center;
895
+ }}
896
+ .contact-name {{
897
+ font-weight: 600;
898
+ color: #3498db;
899
+ transition: color 0.3s;
900
+ text-decoration: none;
901
+ }}
902
+ .contact-name:hover {{
903
+ color: #2980b9;
904
+ text-decoration: underline;
905
+ }}
906
+ .contact-email {{
907
+ color: #666;
908
+ font-size: 14px;
909
+ text-decoration: none;
910
+ transition: color 0.3s;
911
+ }}
912
+ .contact-email:hover {{
913
+ color: #3498db;
914
+ text-decoration: underline;
915
+ }}
916
+ </style>
917
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
918
+ </head>
919
+ <body>
920
+ <div class="container">
921
+ <div class="header">
922
+ <div class="logo">
923
+ <i class="fas fa-robot"></i>
924
+ <span>Akash API</span>
925
+ </div>
926
+ </div>
927
+ <div class="status">
928
+ <div class="status-dot {status["cookie_status"]["status_color"]}"></div>
929
+ <div class="status-text {status["cookie_status"]["status"]}">服务状态: {status["cookie_status"]["status_text"]}</div>
930
+ </div>
931
+ <div class="info-section">
932
+
933
+ <h3><i class="fas fa-cookie"></i> Cookie 信息</h3>
934
+ <div class="info-item">
935
+ <span class="label"><i class="fas fa-clock"></i> 过期时间:</span>
936
+ <span class="value">{status["cookie_status"]["expires"]}</span>
937
+ </div>
938
+ <div class="time-info">剩余时间: {status["cookie_status"]["time_left"]}</div>
939
+ <div class="info-item">
940
+ <span class="label"><i class="fas fa-history"></i> 最后更新:</span>
941
+ <span class="value">{status["cookie_status"]["last_update"]}</span>
942
+ </div>
943
+ <div class="time-info">更新时间: {status["cookie_status"]["update_ago"]}</div>
944
+ </div>
945
+
946
+
947
+ <div class="footer">
948
+ <p>Akash API 服务 - 健康检查页面</p>
949
+ <div class="contact-info">
950
+ <img src="https://gravatar.loli.net/avatar/91af699fa609b1b7730753f1ff96b835?s=50&d=retro" class="contact-avatar" alt="用户头像" />
951
+ <div>
952
+ <p>如遇服务异常,请及时联系:<a href="https://linux.do/u/hzruo" class="contact-name">云胡不喜</a></p>
953
+ </div>
954
+ </div>
955
+ <p>当前时间: {current_time.strftime("%Y-%m-%d %H:%M:%S")} (北京时间)</p>
956
+ </div>
957
+ </div>
958
+ </body>
959
+ </html>
960
+ """)
961
+
962
+ @app.post("/v1/chat/completions")
963
+ async def chat_completions(
964
+ request: Request,
965
+ background_tasks: BackgroundTasks,
966
+ api_key: bool = Depends(get_api_key),
967
+ cookie: str = Depends(validate_cookie)
968
+ ):
969
+ try:
970
+ data = await request.json()
971
+
972
+ # 获取随机浏览器指纹
973
+ fingerprint = get_random_browser_fingerprint()
974
+ logger.info(f"Using browser fingerprint: {fingerprint['user_agent']}")
975
+
976
+ chat_id = str(uuid.uuid4()).replace('-', '')[:16]
977
+
978
+ # 确保系统消息正确处理
979
+ system_message = data.get('system_message') or data.get('system', "You are a helpful assistant.")
980
+
981
+ # 更新请求数据格式,与实际 Akash API 请求保持一致
982
+ akash_data = {
983
+ "id": chat_id,
984
+ "messages": data.get('messages', []),
985
+ "model": data.get('model', "DeepSeek-R1"),
986
+ "system": system_message,
987
+ "temperature": data.get('temperature', 0.6),
988
+ "topP": data.get('top_p', 0.95),
989
+ "context": [] # 添加 context 字段
990
+ }
991
+
992
+ # 记录当前使用的 cookie(部分隐藏)
993
+ cookie_start = cookie[:20]
994
+ cookie_end = cookie[-20:] if len(cookie) > 40 else ""
995
+ logger.info(f"Using cookie: {cookie_start}...{cookie_end}")
996
+
997
+ with requests.Session() as session:
998
+ # 设置 Cookie 使用请求头方式
999
+ session.headers.update(fingerprint["headers"])
1000
+ cookies_dict = {}
1001
+
1002
+ # 解析 cookie 字符串到字典
1003
+ for cookie_item in cookie.split(';'):
1004
+ if '=' in cookie_item:
1005
+ name, value = cookie_item.strip().split('=', 1)
1006
+ cookies_dict[name] = value
1007
+
1008
+ # 使用 cookies 参数而不是 headers['Cookie']
1009
+ response = session.post(
1010
+ 'https://chat.akash.network/api/chat',
1011
+ json=akash_data,
1012
+ cookies=cookies_dict,
1013
+ stream=True
1014
+ )
1015
+
1016
+ # 检查响应状态码,如果是 401 或 403,尝试刷新 cookie 并重试
1017
+ if response.status_code in [401, 403]:
1018
+ logger.info(f"Authentication failed with status {response.status_code}, refreshing cookie...")
1019
+ new_cookie = await refresh_cookie()
1020
+ if new_cookie:
1021
+ logger.info("Successfully refreshed cookie, retrying request")
1022
+ # 解析新 cookie 字符串到字典
1023
+ new_cookies_dict = {}
1024
+ for cookie_item in new_cookie.split(';'):
1025
+ if '=' in cookie_item:
1026
+ name, value = cookie_item.strip().split('=', 1)
1027
+ new_cookies_dict[name] = value
1028
+
1029
+ response = session.post(
1030
+ 'https://chat.akash.network/api/chat',
1031
+ json=akash_data,
1032
+ cookies=new_cookies_dict,
1033
+ stream=True
1034
+ )
1035
+
1036
+ if response.status_code not in [200, 201]:
1037
+ logger.error(f"Akash API error: Status {response.status_code}, Response: {response.text}")
1038
+ raise HTTPException(
1039
+ status_code=response.status_code,
1040
+ detail=f"Akash API error: {response.text}"
1041
+ )
1042
+
1043
+ def generate():
1044
+ content_buffer = ""
1045
+ for line in response.iter_lines():
1046
+ if not line:
1047
+ continue
1048
+
1049
+ try:
1050
+ line_str = line.decode('utf-8')
1051
+ msg_type, msg_data = line_str.split(':', 1)
1052
+
1053
+ if msg_type == '0':
1054
+ if msg_data.startswith('"') and msg_data.endswith('"'):
1055
+ msg_data = msg_data.replace('\\"', '"')
1056
+ msg_data = msg_data[1:-1]
1057
+ msg_data = msg_data.replace("\\n", "\n")
1058
+
1059
+ # 在处理消息时先判断模型类型
1060
+ if data.get('model') == 'AkashGen' and "<image_generation>" in msg_data:
1061
+ # 图片生成模型的特殊处理
1062
+ async def process_and_send():
1063
+ messages = await process_image_generation(msg_data, session, fingerprint["headers"], chat_id)
1064
+ if messages:
1065
+ return messages
1066
+ return None
1067
+
1068
+ # 创建新的事件循环
1069
+ loop = asyncio.new_event_loop()
1070
+ asyncio.set_event_loop(loop)
1071
+ try:
1072
+ result_messages = loop.run_until_complete(process_and_send())
1073
+ finally:
1074
+ loop.close()
1075
+
1076
+ if result_messages:
1077
+ for message in result_messages:
1078
+ yield f"data: {json.dumps(message)}\n\n"
1079
+ continue
1080
+
1081
+ content_buffer += msg_data
1082
+
1083
+ chunk = {
1084
+ "id": f"chatcmpl-{chat_id}",
1085
+ "object": "chat.completion.chunk",
1086
+ "created": int(time.time()),
1087
+ "model": data.get('model'),
1088
+ "choices": [{
1089
+ "delta": {"content": msg_data},
1090
+ "index": 0,
1091
+ "finish_reason": None
1092
+ }]
1093
+ }
1094
+ yield f"data: {json.dumps(chunk)}\n\n"
1095
+
1096
+ elif msg_type in ['e', 'd']:
1097
+ chunk = {
1098
+ "id": f"chatcmpl-{chat_id}",
1099
+ "object": "chat.completion.chunk",
1100
+ "created": int(time.time()),
1101
+ "model": data.get('model'),
1102
+ "choices": [{
1103
+ "delta": {},
1104
+ "index": 0,
1105
+ "finish_reason": "stop"
1106
+ }]
1107
+ }
1108
+ yield f"data: {json.dumps(chunk)}\n\n"
1109
+ yield "data: [DONE]\n\n"
1110
+ break
1111
+
1112
+ except Exception as e:
1113
+ print(f"Error processing line: {e}")
1114
+ continue
1115
+
1116
+ return StreamingResponse(
1117
+ generate(),
1118
+ media_type='text/event-stream',
1119
+ headers={
1120
+ 'Cache-Control': 'no-cache',
1121
+ 'Connection': 'keep-alive',
1122
+ 'Content-Type': 'text/event-stream'
1123
+ }
1124
+ )
1125
+
1126
+ except Exception as e:
1127
+ print(f"Error in chat_completions: {e}")
1128
+ import traceback
1129
+ print(traceback.format_exc())
1130
+ return {"error": str(e)}
1131
+
1132
+ @app.get("/v1/models")
1133
+ async def list_models(
1134
+ background_tasks: BackgroundTasks,
1135
+ cookie: str = Depends(validate_cookie)
1136
+ ):
1137
+ try:
1138
+ # 获取随机浏览器指纹
1139
+ fingerprint = get_random_browser_fingerprint()
1140
+ logger.info(f"Using browser fingerprint: {fingerprint['user_agent']}")
1141
+
1142
+ # 构建更符合实际请求的请求头
1143
+ headers = fingerprint["headers"]
1144
+
1145
+ # 记录当前使用的 cookie(部分隐藏)
1146
+ cookie_start = cookie[:20]
1147
+ cookie_end = cookie[-20:] if len(cookie) > 40 else ""
1148
+ logger.info(f"Using cookie: {cookie_start}...{cookie_end}")
1149
+ logger.info("Sending request to get models...")
1150
+
1151
+ with requests.Session() as session:
1152
+ # 设置会话的默认请求头
1153
+ session.headers.update(headers)
1154
+
1155
+ # 解析 cookie 字符串到字典
1156
+ cookies_dict = {}
1157
+ for cookie_item in cookie.split(';'):
1158
+ if '=' in cookie_item:
1159
+ name, value = cookie_item.strip().split('=', 1)
1160
+ cookies_dict[name] = value
1161
+
1162
+ response = session.get(
1163
+ 'https://chat.akash.network/api/models',
1164
+ cookies=cookies_dict
1165
+ )
1166
+
1167
+ logger.info(f"Models response status: {response.status_code}")
1168
+
1169
+ # 检查响应状态码,如果是 401 或 403,尝试刷新 cookie 并重试
1170
+ if response.status_code in [401, 403]:
1171
+ logger.info(f"Authentication failed with status {response.status_code}, refreshing cookie...")
1172
+ new_cookie = await refresh_cookie()
1173
+ if new_cookie:
1174
+ logger.info("Successfully refreshed cookie, retrying request")
1175
+
1176
+ # 解析新 cookie 字符串到字典
1177
+ new_cookies_dict = {}
1178
+ for cookie_item in new_cookie.split(';'):
1179
+ if '=' in cookie_item:
1180
+ name, value = cookie_item.strip().split('=', 1)
1181
+ new_cookies_dict[name] = value
1182
+
1183
+ response = session.get(
1184
+ 'https://chat.akash.network/api/models',
1185
+ cookies=new_cookies_dict
1186
+ )
1187
+
1188
+ if response.status_code not in [200, 201]:
1189
+ logger.error(f"Akash API error: Status {response.status_code}, Response: {response.text}")
1190
+ return {"error": f"Authentication failed. Status: {response.status_code}"}
1191
+
1192
+ try:
1193
+ akash_response = response.json()
1194
+ logger.info(f"Received models data of type: {type(akash_response)}")
1195
+ except ValueError:
1196
+ logger.error(f"Invalid JSON response: {response.text[:100]}...")
1197
+ return {"error": "Invalid response format"}
1198
+
1199
+ # 检查响应格式并适配
1200
+ models_list = []
1201
+ if isinstance(akash_response, list):
1202
+ # 如果直接是列表
1203
+ models_list = akash_response
1204
+ elif isinstance(akash_response, dict):
1205
+ # 如果是字典格式
1206
+ models_list = akash_response.get("models", [])
1207
+ else:
1208
+ logger.error(f"Unexpected response format: {type(akash_response)}")
1209
+ models_list = []
1210
+
1211
+ # 转换为标准 OpenAI 格式
1212
+ openai_models = {
1213
+ "object": "list",
1214
+ "data": [
1215
+ {
1216
+ "id": model["id"] if isinstance(model, dict) else model,
1217
+ "object": "model",
1218
+ "created": int(time.time()),
1219
+ "owned_by": "akash",
1220
+ "permission": [{
1221
+ "id": f"modelperm-{model['id'] if isinstance(model, dict) else model}",
1222
+ "object": "model_permission",
1223
+ "created": int(time.time()),
1224
+ "allow_create_engine": False,
1225
+ "allow_sampling": True,
1226
+ "allow_logprobs": True,
1227
+ "allow_search_indices": False,
1228
+ "allow_view": True,
1229
+ "allow_fine_tuning": False,
1230
+ "organization": "*",
1231
+ "group": None,
1232
+ "is_blocking": False
1233
+ }]
1234
+ } for model in models_list
1235
+ ]
1236
+ }
1237
+
1238
+ return openai_models
1239
+
1240
+ except Exception as e:
1241
+ logger.error(f"Error in list_models: {e}")
1242
+ import traceback
1243
+ logger.error(f"Traceback: {traceback.format_exc()}")
1244
+ return {"error": str(e)}
1245
+
1246
+ async def process_image_generation(msg_data: str, session: requests.Session, headers: dict, chat_id: str) -> Optional[list]:
1247
+ """处理图片生成的逻辑,返回多个消息块"""
1248
+ # 检查消息中是否包含jobId
1249
+ if "jobId='undefined'" in msg_data or "jobId=''" in msg_data:
1250
+ logger.error("Image generation failed: jobId is undefined or empty")
1251
+ return create_error_messages(chat_id, "Akash官网服务异常,无法生成图片,请稍后再试。")
1252
+
1253
+ match = re.search(r"jobId='([^']+)' prompt='([^']+)' negative='([^']*)'", msg_data)
1254
+ if not match:
1255
+ logger.error(f"Failed to extract job_id from message: {msg_data[:100]}...")
1256
+ return create_error_messages(chat_id, "无法解析图片生成任务。请稍后再试。")
1257
+
1258
+ job_id, prompt, negative = match.groups()
1259
+
1260
+ # 检查job_id是否有效
1261
+ if not job_id or job_id == 'undefined' or job_id == 'null':
1262
+ logger.error(f"Invalid job_id: {job_id}")
1263
+ return create_error_messages(chat_id, "Akash服务异常,无法获取有效的任务ID。请稍后再试。")
1264
+
1265
+ print(f"Starting image generation process for job_id: {job_id}")
1266
+
1267
+ # 记录开始时间
1268
+ start_time = time.time()
1269
+
1270
+ # 发送思考开始的消息
1271
+ think_msg = "<think>\n"
1272
+ think_msg += "🎨 Generating image...\n\n"
1273
+ think_msg += f"Prompt: {prompt}\n"
1274
+
1275
+ try:
1276
+ # 检查图片状态和上传
1277
+ result = await check_image_status(session, job_id, headers)
1278
+
1279
+ # 计算实际花费的时间
1280
+ elapsed_time = time.time() - start_time
1281
+
1282
+ # 完成思考部分
1283
+ think_msg += f"\n🤔 Thinking for {elapsed_time:.1f}s...\n"
1284
+ think_msg += "</think>"
1285
+
1286
+ # 返回两个独立的消息块
1287
+ messages = []
1288
+
1289
+ # 第一个消息块:思考过程
1290
+ messages.append({
1291
+ "id": f"chatcmpl-{chat_id}-think",
1292
+ "object": "chat.completion.chunk",
1293
+ "created": int(time.time()),
1294
+ "model": "AkashGen",
1295
+ "choices": [{
1296
+ "delta": {"content": think_msg},
1297
+ "index": 0,
1298
+ "finish_reason": None
1299
+ }]
1300
+ })
1301
+
1302
+ # 第二个消息块:图片结果
1303
+ if result:
1304
+ image_msg = f"\n\n![Generated Image]({result})"
1305
+ messages.append({
1306
+ "id": f"chatcmpl-{chat_id}-image",
1307
+ "object": "chat.completion.chunk",
1308
+ "created": int(time.time()),
1309
+ "model": "AkashGen",
1310
+ "choices": [{
1311
+ "delta": {"content": image_msg},
1312
+ "index": 0,
1313
+ "finish_reason": None
1314
+ }]
1315
+ })
1316
+ else:
1317
+ fail_msg = "\n\n*Image generation or upload failed.*"
1318
+ messages.append({
1319
+ "id": f"chatcmpl-{chat_id}-fail",
1320
+ "object": "chat.completion.chunk",
1321
+ "created": int(time.time()),
1322
+ "model": "AkashGen",
1323
+ "choices": [{
1324
+ "delta": {"content": fail_msg},
1325
+ "index": 0,
1326
+ "finish_reason": None
1327
+ }]
1328
+ })
1329
+
1330
+ return messages
1331
+ except Exception as e:
1332
+ logger.error(f"Error in image generation process: {e}")
1333
+ import traceback
1334
+ logger.error(f"Traceback: {traceback.format_exc()}")
1335
+ return create_error_messages(chat_id, "图片生成过程中发生错误。请稍后再试。")
1336
+
1337
+ def create_error_messages(chat_id: str, error_message: str) -> list:
1338
+ """创建错误消息块"""
1339
+ return [{
1340
+ "id": f"chatcmpl-{chat_id}-error",
1341
+ "object": "chat.completion.chunk",
1342
+ "created": int(time.time()),
1343
+ "model": "AkashGen",
1344
+ "choices": [{
1345
+ "delta": {"content": f"\n\n**❌ {error_message}**"},
1346
+ "index": 0,
1347
+ "finish_reason": None
1348
+ }]
1349
+ }]
1350
+
1351
+ async def upload_to_xinyew(image_base64: str, job_id: str) -> Optional[str]:
1352
+ """上传图片到新野图床并返回URL"""
1353
+ try:
1354
+ print(f"\n=== Starting image upload for job {job_id} ===")
1355
+ print(f"Base64 data length: {len(image_base64)}")
1356
+
1357
+ # 解码base64图片数据
1358
+ try:
1359
+ image_data = base64.b64decode(image_base64.split(',')[1] if ',' in image_base64 else image_base64)
1360
+ print(f"Decoded image data length: {len(image_data)} bytes")
1361
+ except Exception as e:
1362
+ print(f"Error decoding base64: {e}")
1363
+ print(f"First 100 chars of base64: {image_base64[:100]}...")
1364
+ return None
1365
+
1366
+ # 创建临时文件
1367
+ with tempfile.NamedTemporaryFile(suffix='.jpeg', delete=False) as temp_file:
1368
+ temp_file.write(image_data)
1369
+ temp_file_path = temp_file.name
1370
+
1371
+ try:
1372
+ filename = f"{job_id}.jpeg"
1373
+ print(f"Using filename: {filename}")
1374
+
1375
+ # 准备文件上传
1376
+ files = {
1377
+ 'file': (filename, open(temp_file_path, 'rb'), 'image/jpeg')
1378
+ }
1379
+
1380
+ print("Sending request to xinyew.cn...")
1381
+ response = requests.post(
1382
+ 'https://api.xinyew.cn/api/jdtc',
1383
+ files=files,
1384
+ timeout=30
1385
+ )
1386
+
1387
+ print(f"Upload response status: {response.status_code}")
1388
+ if response.status_code == 200:
1389
+ result = response.json()
1390
+ print(f"Upload response: {result}")
1391
+
1392
+ if result.get('errno') == 0:
1393
+ url = result.get('data', {}).get('url')
1394
+ if url:
1395
+ print(f"Successfully got image URL: {url}")
1396
+ return url
1397
+ print("No URL in response data")
1398
+ else:
1399
+ print(f"Upload failed: {result.get('message')}")
1400
+ else:
1401
+ print(f"Upload failed with status {response.status_code}")
1402
+ print(f"Response content: {response.text}")
1403
+ return None
1404
+
1405
+ finally:
1406
+ # 清理临时文件
1407
+ try:
1408
+ os.unlink(temp_file_path)
1409
+ except Exception as e:
1410
+ print(f"Error removing temp file: {e}")
1411
+
1412
+ except Exception as e:
1413
+ print(f"Error in upload_to_xinyew: {e}")
1414
+ import traceback
1415
+ print(traceback.format_exc())
1416
+ return None
1417
+
1418
+ def auto_refresh_cookie():
1419
+ """自动刷新 cookie 的线程函数"""
1420
+ while True:
1421
+ try:
1422
+ current_time = time.time()
1423
+ # 只在 cookie 不存在或已过期时刷新
1424
+ if (not global_data["cookie"] or
1425
+ current_time >= global_data["cookie_expires"]) and not global_data["is_refreshing"]:
1426
+
1427
+ logger.info(f"Cookie status check: exists={bool(global_data['cookie'])}, expires_in={global_data['cookie_expires'] - current_time if global_data['cookie_expires'] > 0 else 'expired'}")
1428
+ logger.info("Cookie expired or not available, starting refresh")
1429
+
1430
+ try:
1431
+ global_data["is_refreshing"] = True
1432
+ new_cookie = get_cookie()
1433
+ if new_cookie:
1434
+ logger.info("Cookie refresh successful")
1435
+ else:
1436
+ logger.error("Cookie refresh failed, will retry later")
1437
+ except Exception as e:
1438
+ logger.error(f"Error during cookie refresh: {e}")
1439
+ import traceback
1440
+ logger.error(f"Traceback: {traceback.format_exc()}")
1441
+ finally:
1442
+ global_data["is_refreshing"] = False
1443
+ # 强制执行垃圾回收,释放内存
1444
+ import gc
1445
+ gc.collect()
1446
+
1447
+ # 每60秒检查一次
1448
+ time.sleep(60)
1449
+ except Exception as e:
1450
+ logger.error(f"Error in auto-refresh thread: {e}")
1451
+ global_data["is_refreshing"] = False # 确保出错时也重置标志
1452
+ # 强制执行垃圾回收,释放内存
1453
+ import gc
1454
+ gc.collect()
1455
+ time.sleep(60) # 出错后等待60秒再继续
1456
+
1457
+ if __name__ == '__main__':
1458
+ import uvicorn
1459
+ uvicorn.run(app, host='0.0.0.0', port=7860)