from fastapi import FastAPI, Request, Depends, HTTPException from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.responses import StreamingResponse, HTMLResponse from fastapi.background import BackgroundTasks from contextlib import asynccontextmanager import requests from curl_cffi import requests as cffi_requests import uuid import json import time from typing import Optional import asyncio import base64 import tempfile import os import re import threading from DrissionPage import ChromiumPage, ChromiumOptions import logging from dotenv import load_dotenv # 加载环境变量 load_dotenv(override=True) # 配置日志 logging.basicConfig( level=logging.INFO, # 改为 INFO 级别 format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # 修改全局数据存储 global_data = { "cookie": None, "cookies": None, "last_update": 0, "cookie_expires": 0 # 添加 cookie 过期时间 } @asynccontextmanager async def lifespan(app: FastAPI): # 启动时获取 cookie threading.Thread(target=get_cookie).start() yield # 关闭时清理资源 global_data["cookie"] = None global_data["cookies"] = None global_data["last_update"] = 0 app = FastAPI(lifespan=lifespan) security = HTTPBearer() # OpenAI API Key 配置,可以通过环境变量覆盖 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", None) logger.info(f"OPENAI_API_KEY is set: {OPENAI_API_KEY is not None}") logger.info(f"OPENAI_API_KEY value: {OPENAI_API_KEY}") def get_cookie(): try: options = ChromiumOptions().headless() page = ChromiumPage(addr_or_opts=options) page.set.window.size(1920, 1080) page.set.user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" page.get("https://chat.akash.network/") time.sleep(10) cookies = page.cookies() if not cookies: page.quit() return None cookie_dict = {cookie['name']: cookie['value'] for cookie in cookies} if 'cf_clearance' not in cookie_dict: page.quit() return None cookie_str = '; '.join([f"{cookie['name']}={cookie['value']}" for cookie in cookies]) global_data["cookie"] = cookie_str global_data["last_update"] = time.time() expires = min([cookie.get('expires', float('inf')) for cookie in cookies]) if expires != float('inf'): global_data["cookie_expires"] = expires else: global_data["cookie_expires"] = time.time() + 3600 page.quit() return cookie_str except Exception as e: logger.error(f"Error fetching cookie: {e}") return None # 添加刷新 cookie 的函数 async def refresh_cookie(): logger.info("Refreshing cookie due to 401 error") # 标记 cookie 为过期 global_data["cookie_expires"] = 0 # 获取新的 cookie return get_cookie() async def check_and_update_cookie(background_tasks: BackgroundTasks): # 如果 cookie 不存在或已过期,则更新 current_time = time.time() if not global_data["cookie"] or current_time >= global_data["cookie_expires"]: logger.info("Cookie expired or not available, refreshing...") background_tasks.add_task(get_cookie) else: logger.info("Using existing cookie") async def get_api_key(credentials: HTTPAuthorizationCredentials = Depends(security)): token = credentials.credentials logger.info(f"Received token: {token}") # 如果设置了 OPENAI_API_KEY,则需要验证 if OPENAI_API_KEY is not None: # 去掉 Bearer 前缀后再比较 clean_token = token.replace("Bearer ", "") if token.startswith("Bearer ") else token logger.info(f"Clean token: {clean_token}") if clean_token != OPENAI_API_KEY: logger.error(f"Token mismatch. Expected: {OPENAI_API_KEY}, Got: {clean_token}") raise HTTPException( status_code=401, detail="Invalid API key" ) logger.info("API key validation passed") return True async def validate_cookie(background_tasks: BackgroundTasks): # 检查并更新 cookie(如果需要) await check_and_update_cookie(background_tasks) # 等待 cookie 初始化完成 max_wait = 30 # 最大等待时间(秒) start_time = time.time() while not global_data["cookie"] and time.time() - start_time < max_wait: await asyncio.sleep(1) logger.info("Waiting for cookie initialization...") # 检查是否有有效的 cookie if not global_data["cookie"]: logger.error("Cookie not available after waiting") raise HTTPException( status_code=503, detail="Service temporarily unavailable - Cookie not available" ) logger.info("Cookie validation passed") return global_data["cookie"] async def check_image_status(session: requests.Session, job_id: str, headers: dict) -> Optional[str]: """检查图片生成状态并获取生成的图片""" max_retries = 30 for attempt in range(max_retries): try: print(f"\nAttempt {attempt + 1}/{max_retries} for job {job_id}") response = session.get( f'https://chat.akash.network/api/image-status?ids={job_id}', headers=headers ) print(f"Status response code: {response.status_code}") status_data = response.json() if status_data and isinstance(status_data, list) and len(status_data) > 0: job_info = status_data[0] status = job_info.get('status') print(f"Job status: {status}") # 只有当状态为 completed 时才处理结果 if status == "completed": result = job_info.get("result") if result and not result.startswith("Failed"): print("Got valid result, attempting upload...") image_url = await upload_to_xinyew(result, job_id) if image_url: print(f"Successfully uploaded image: {image_url}") return image_url print("Image upload failed") return None print("Invalid result received") return None elif status == "failed": print(f"Job {job_id} failed") return None # 如果状态是其他(如 pending),继续等待 await asyncio.sleep(1) continue except Exception as e: print(f"Error checking status: {e}") return None print(f"Timeout waiting for job {job_id}") return None @app.get("/", response_class=HTMLResponse) async def health_check(): """Health check endpoint""" status = { "status": "ok", "version": "1.0.0", "cookie_status": { "available": global_data["cookie"] is not None, "last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(global_data["last_update"])) if global_data["last_update"] > 0 else None, "expires": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(global_data["cookie_expires"])) if global_data["cookie_expires"] > 0 else None } } html = f""" Akash API Status

Akash API Status {status["status"]}

Version: {status["version"]}
""" return html @app.post("/v1/chat/completions") async def chat_completions( request: Request, background_tasks: BackgroundTasks, api_key: bool = Depends(get_api_key), cookie: str = Depends(validate_cookie) ): try: data = await request.json() chat_id = str(uuid.uuid4()).replace('-', '')[:16] akash_data = { "id": chat_id, "messages": data.get('messages', []), "model": data.get('model', "DeepSeek-R1"), "system": data.get('system_message', "You are a helpful assistant."), "temperature": data.get('temperature', 0.6), "topP": data.get('top_p', 0.95) } # 构建请求头 headers = { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Origin": "https://chat.akash.network", "Referer": "https://chat.akash.network/", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Connection": "keep-alive" } # 设置 Cookie headers["Cookie"] = cookie with requests.Session() as session: response = session.post( 'https://chat.akash.network/api/chat', json=akash_data, headers=headers, stream=True ) # 检查响应状态码,如果是 401,尝试刷新 cookie 并重试 if response.status_code == 401: logger.info("Cookie expired, refreshing...") new_cookie = await refresh_cookie() if new_cookie: headers["Cookie"] = new_cookie response = session.post( 'https://chat.akash.network/api/chat', json=akash_data, headers=headers, stream=True ) if response.status_code != 200: logger.error(f"Akash API error: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"Akash API error: {response.text}" ) def generate(): content_buffer = "" for line in response.iter_lines(): if not line: continue try: line_str = line.decode('utf-8') msg_type, msg_data = line_str.split(':', 1) if msg_type == '0': if msg_data.startswith('"') and msg_data.endswith('"'): msg_data = msg_data.replace('\\"', '"') msg_data = msg_data[1:-1] msg_data = msg_data.replace("\\n", "\n") # 在处理消息时先判断模型类型 if data.get('model') == 'AkashGen' and "" in msg_data: # 图片生成模型的特殊处理 async def process_and_send(): messages = await process_image_generation(msg_data, session, headers, chat_id) if messages: return messages return None # 创建新的事件循环 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: result_messages = loop.run_until_complete(process_and_send()) finally: loop.close() if result_messages: for message in result_messages: yield f"data: {json.dumps(message)}\n\n" continue content_buffer += msg_data chunk = { "id": f"chatcmpl-{chat_id}", "object": "chat.completion.chunk", "created": int(time.time()), "model": data.get('model'), "choices": [{ "delta": {"content": msg_data}, "index": 0, "finish_reason": None }] } yield f"data: {json.dumps(chunk)}\n\n" elif msg_type in ['e', 'd']: chunk = { "id": f"chatcmpl-{chat_id}", "object": "chat.completion.chunk", "created": int(time.time()), "model": data.get('model'), "choices": [{ "delta": {}, "index": 0, "finish_reason": "stop" }] } yield f"data: {json.dumps(chunk)}\n\n" yield "data: [DONE]\n\n" break except Exception as e: print(f"Error processing line: {e}") continue return StreamingResponse( generate(), media_type='text/event-stream', headers={ 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Content-Type': 'text/event-stream' } ) except Exception as e: print(f"Error in chat_completions: {e}") import traceback print(traceback.format_exc()) return {"error": str(e)} @app.get("/v1/models") async def list_models( background_tasks: BackgroundTasks, cookie: str = Depends(validate_cookie) ): try: headers = { "accept": "application/json", "accept-language": "en-US,en;q=0.9", "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"macOS"', "sec-fetch-dest": "document", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "referer": "https://chat.akash.network/" } # 设置 Cookie headers["Cookie"] = cookie print(f"Using cookie: {headers.get('Cookie', 'None')}") print("Sending request to get models...") response = requests.get( 'https://chat.akash.network/api/models', headers=headers ) print(f"Models response status: {response.status_code}") print(f"Models response headers: {response.headers}") if response.status_code == 401: print("Authentication failed. Please check your API key.") return {"error": "Authentication failed. Please check your API key."} akash_response = response.json() # 添加错误处理和调试信息 print(f"Akash API response: {akash_response}") # 检查响应格式并适配 models_list = [] if isinstance(akash_response, list): # 如果直接是列表 models_list = akash_response elif isinstance(akash_response, dict): # 如果是字典格式 models_list = akash_response.get("models", []) else: print(f"Unexpected response format: {type(akash_response)}") models_list = [] # 转换为标准 OpenAI 格式 openai_models = { "object": "list", "data": [ { "id": model["id"] if isinstance(model, dict) else model, "object": "model", "created": int(time.time()), "owned_by": "akash", "permission": [{ "id": f"modelperm-{model['id'] if isinstance(model, dict) else model}", "object": "model_permission", "created": int(time.time()), "allow_create_engine": False, "allow_sampling": True, "allow_logprobs": True, "allow_search_indices": False, "allow_view": True, "allow_fine_tuning": False, "organization": "*", "group": None, "is_blocking": False }] } for model in models_list ] } return openai_models except Exception as e: print(f"Error in list_models: {e}") import traceback print(traceback.format_exc()) return {"error": str(e)} async def process_image_generation(msg_data: str, session: requests.Session, headers: dict, chat_id: str) -> Optional[list]: """处理图片生成的逻辑,返回多个消息块""" match = re.search(r"jobId='([^']+)' prompt='([^']+)' negative='([^']*)'", msg_data) if match: job_id, prompt, negative = match.groups() print(f"Starting image generation process for job_id: {job_id}") # 记录开始时间 start_time = time.time() # 发送思考开始的消息 think_msg = "\n" think_msg += "🎨 Generating image...\n\n" think_msg += f"Prompt: {prompt}\n" # 检查图片状态和上传 result = await check_image_status(session, job_id, headers) # 计算实际花费的时间 elapsed_time = time.time() - start_time # 完成思考部分 think_msg += f"\n🤔 Thinking for {elapsed_time:.1f}s...\n" think_msg += "" # 返回两个独立的消息块 messages = [] # 第一个消息块:思考过程 messages.append({ "id": f"chatcmpl-{chat_id}-think", "object": "chat.completion.chunk", "created": int(time.time()), "model": "AkashGen", "choices": [{ "delta": {"content": think_msg}, "index": 0, "finish_reason": None }] }) # 第二个消息块:图片结果 if result: image_msg = f"\n\n![Generated Image]({result})" messages.append({ "id": f"chatcmpl-{chat_id}-image", "object": "chat.completion.chunk", "created": int(time.time()), "model": "AkashGen", "choices": [{ "delta": {"content": image_msg}, "index": 0, "finish_reason": None }] }) else: fail_msg = "\n\n*Image generation or upload failed.*" messages.append({ "id": f"chatcmpl-{chat_id}-fail", "object": "chat.completion.chunk", "created": int(time.time()), "model": "AkashGen", "choices": [{ "delta": {"content": fail_msg}, "index": 0, "finish_reason": None }] }) return messages return None async def upload_to_xinyew(image_base64: str, job_id: str) -> Optional[str]: """上传图片到新野图床并返回URL""" try: print(f"\n=== Starting image upload for job {job_id} ===") print(f"Base64 data length: {len(image_base64)}") # 解码base64图片数据 try: image_data = base64.b64decode(image_base64.split(',')[1] if ',' in image_base64 else image_base64) print(f"Decoded image data length: {len(image_data)} bytes") except Exception as e: print(f"Error decoding base64: {e}") print(f"First 100 chars of base64: {image_base64[:100]}...") return None # 创建临时文件 with tempfile.NamedTemporaryFile(suffix='.jpeg', delete=False) as temp_file: temp_file.write(image_data) temp_file_path = temp_file.name try: filename = f"{job_id}.jpeg" print(f"Using filename: {filename}") # 准备文件上传 files = { 'file': (filename, open(temp_file_path, 'rb'), 'image/jpeg') } print("Sending request to xinyew.cn...") response = requests.post( 'https://api.xinyew.cn/api/jdtc', files=files, timeout=30 ) print(f"Upload response status: {response.status_code}") if response.status_code == 200: result = response.json() print(f"Upload response: {result}") if result.get('errno') == 0: url = result.get('data', {}).get('url') if url: print(f"Successfully got image URL: {url}") return url print("No URL in response data") else: print(f"Upload failed: {result.get('message')}") else: print(f"Upload failed with status {response.status_code}") print(f"Response content: {response.text}") return None finally: # 清理临时文件 try: os.unlink(temp_file_path) except Exception as e: print(f"Error removing temp file: {e}") except Exception as e: print(f"Error in upload_to_xinyew: {e}") import traceback print(traceback.format_exc()) return None if __name__ == '__main__': import uvicorn uvicorn.run(app, host='0.0.0.0', port=9000)