Upload 31 files
Browse files- .gitignore +2 -0
- Dockerfile +17 -0
- core/__init__.py +0 -0
- core/__pycache__/__init__.cpython-311.pyc +0 -0
- core/__pycache__/app.cpython-311.pyc +0 -0
- core/__pycache__/auth.cpython-311.pyc +0 -0
- core/__pycache__/config.cpython-311.pyc +0 -0
- core/__pycache__/logger.cpython-311.pyc +0 -0
- core/__pycache__/models.cpython-311.pyc +0 -0
- core/__pycache__/refresh_token.cpython-311.pyc +0 -0
- core/__pycache__/router.cpython-311.pyc +0 -0
- core/__pycache__/scheduler.cpython-311.pyc +0 -0
- core/__pycache__/sqlite_store.cpython-311.pyc +0 -0
- core/__pycache__/utils.cpython-311.pyc +0 -0
- core/app.py +96 -0
- core/auth.py +12 -0
- core/config.py +74 -0
- core/example.db +0 -0
- core/logger.py +20 -0
- core/models.py +16 -0
- core/refresh_token.py +105 -0
- core/router.py +73 -0
- core/scheduler.py +64 -0
- core/sqlite_store.py +174 -0
- core/utils.py +449 -0
- main.py +6 -0
- requirements.txt +12 -0
- task/__init__.py +0 -0
- task/__pycache__/__init__.cpython-311.pyc +0 -0
- task/__pycache__/scheduled_tasks.cpython-311.pyc +0 -0
- task/scheduled_tasks.py +100 -0
.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
.idea
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10.16-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 复制所需文件到容器中
|
| 6 |
+
COPY . .
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
ENV APP_SECRET="sk-123456"
|
| 11 |
+
ENV REQUEST_TIMEOUT=30
|
| 12 |
+
|
| 13 |
+
# Expose port
|
| 14 |
+
EXPOSE 8001
|
| 15 |
+
|
| 16 |
+
# Run the application
|
| 17 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
|
core/__init__.py
ADDED
|
File without changes
|
core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (197 Bytes). View file
|
|
|
core/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (5.95 kB). View file
|
|
|
core/__pycache__/auth.cpython-311.pyc
ADDED
|
Binary file (943 Bytes). View file
|
|
|
core/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (4.01 kB). View file
|
|
|
core/__pycache__/logger.cpython-311.pyc
ADDED
|
Binary file (931 Bytes). View file
|
|
|
core/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (1.25 kB). View file
|
|
|
core/__pycache__/refresh_token.cpython-311.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
core/__pycache__/router.cpython-311.pyc
ADDED
|
Binary file (5.53 kB). View file
|
|
|
core/__pycache__/scheduler.cpython-311.pyc
ADDED
|
Binary file (3.55 kB). View file
|
|
|
core/__pycache__/sqlite_store.cpython-311.pyc
ADDED
|
Binary file (10 kB). View file
|
|
|
core/__pycache__/utils.cpython-311.pyc
ADDED
|
Binary file (17.7 kB). View file
|
|
|
core/app.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI, Request
|
| 4 |
+
from starlette.middleware.cors import CORSMiddleware
|
| 5 |
+
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
| 6 |
+
from fastapi.responses import JSONResponse
|
| 7 |
+
from core.config import get_settings
|
| 8 |
+
from core.logger import setup_logger
|
| 9 |
+
from core.refresh_token import TokenManager
|
| 10 |
+
from core.router import router
|
| 11 |
+
from core.scheduler import SchedulerManager
|
| 12 |
+
|
| 13 |
+
settings = get_settings()
|
| 14 |
+
logger = setup_logger(__name__)
|
| 15 |
+
scheduler_manager = SchedulerManager()
|
| 16 |
+
|
| 17 |
+
# print(settings.SECRET)
|
| 18 |
+
def create_app() -> FastAPI:
|
| 19 |
+
app = FastAPI(
|
| 20 |
+
title=settings.PROJECT_NAME,
|
| 21 |
+
version="0.0.1",
|
| 22 |
+
description=settings.DESCRIPTION,
|
| 23 |
+
)
|
| 24 |
+
# 配置中间件
|
| 25 |
+
app.add_middleware(
|
| 26 |
+
CORSMiddleware,
|
| 27 |
+
allow_origins=["*"],
|
| 28 |
+
allow_credentials=True,
|
| 29 |
+
allow_methods=["*"],
|
| 30 |
+
allow_headers=["*"],
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# # 添加可信主机中间件
|
| 34 |
+
app.add_middleware(
|
| 35 |
+
TrustedHostMiddleware,
|
| 36 |
+
allowed_hosts=["*"] # 在生产环境中应该限制允许的主机
|
| 37 |
+
)
|
| 38 |
+
# API端点用于管理定时任务
|
| 39 |
+
@app.get("/scheduler/jobs")
|
| 40 |
+
async def list_jobs():
|
| 41 |
+
jobs = scheduler_manager.get_jobs()
|
| 42 |
+
return [{"id": job.id, "next_run_time": job.next_run_time} for job in jobs]
|
| 43 |
+
|
| 44 |
+
@app.post("/scheduler/jobs/{job_id}/trigger")
|
| 45 |
+
async def trigger_job(job_id: str):
|
| 46 |
+
job = scheduler_manager.scheduler.get_job(job_id)
|
| 47 |
+
if not job:
|
| 48 |
+
return {"error": "Job not found"}
|
| 49 |
+
await job.func() # 假设任务是异步的
|
| 50 |
+
return {"message": f"Job {job_id} triggered"}
|
| 51 |
+
|
| 52 |
+
@app.delete("/scheduler/jobs/{job_id}")
|
| 53 |
+
async def delete_job(job_id: str):
|
| 54 |
+
try:
|
| 55 |
+
scheduler_manager.remove_job(job_id)
|
| 56 |
+
return {"message": f"Job {job_id} removed"}
|
| 57 |
+
except Exception as e:
|
| 58 |
+
return {"error": str(e)}
|
| 59 |
+
# 添加路由
|
| 60 |
+
app.include_router(router, prefix="/api/v1")
|
| 61 |
+
app.include_router(router, prefix="/v1") # 兼容性路由
|
| 62 |
+
|
| 63 |
+
@app.exception_handler(Exception)
|
| 64 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 65 |
+
logger.error(f"An error occurred: {str(exc)}", exc_info=True)
|
| 66 |
+
return JSONResponse(
|
| 67 |
+
status_code=500,
|
| 68 |
+
content={
|
| 69 |
+
"message": "An internal server error occurred.",
|
| 70 |
+
"detail": str(exc)
|
| 71 |
+
},
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# # 创建 TokenManager 实例
|
| 75 |
+
token_manager = TokenManager()
|
| 76 |
+
|
| 77 |
+
@app.on_event("startup")
|
| 78 |
+
async def startup_event():
|
| 79 |
+
# 在应用启动时创建任务
|
| 80 |
+
app.state.refresh_task = asyncio.create_task(token_manager.start_auto_refresh())
|
| 81 |
+
await scheduler_manager.start()
|
| 82 |
+
|
| 83 |
+
@app.on_event("shutdown")
|
| 84 |
+
async def shutdown_event():
|
| 85 |
+
# 在应用关闭时取消任务
|
| 86 |
+
if hasattr(app.state, 'refresh_task'):
|
| 87 |
+
app.state.refresh_task.cancel()
|
| 88 |
+
try:
|
| 89 |
+
await app.state.refresh_task
|
| 90 |
+
except asyncio.CancelledError:
|
| 91 |
+
pass
|
| 92 |
+
|
| 93 |
+
await scheduler_manager.shutdown()
|
| 94 |
+
return app
|
| 95 |
+
|
| 96 |
+
app = create_app()
|
core/auth.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import Depends, HTTPException
|
| 2 |
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
| 3 |
+
from core.config import get_settings
|
| 4 |
+
|
| 5 |
+
settings = get_settings()
|
| 6 |
+
APP_SECRET = settings.APP_SECRET
|
| 7 |
+
security = HTTPBearer()
|
| 8 |
+
|
| 9 |
+
def verify_app_secret(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
| 10 |
+
if credentials.credentials != APP_SECRET:
|
| 11 |
+
raise HTTPException(status_code=403, detail="Invalid SECRET")
|
| 12 |
+
return credentials.credentials
|
core/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import platform
|
| 3 |
+
import uuid
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
|
| 6 |
+
import httpx
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
from pydantic_settings import BaseSettings
|
| 9 |
+
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Settings(BaseSettings):
|
| 14 |
+
TIMEZONE: str = "Asia/Shanghai"
|
| 15 |
+
READY_REFRESH_TOKEN: str = os.getenv('READY_REFRESH_TOKEN', '')
|
| 16 |
+
READY_TOKEN: str = os.getenv('READY_TOKEN', '')
|
| 17 |
+
|
| 18 |
+
APP_SECRET: str = os.getenv('APP_SECRET', '')
|
| 19 |
+
TOKEN: str = os.getenv('TOKEN', '')
|
| 20 |
+
PROJECT_NAME: str = os.getenv('PROJECT_NAME', 'FastAPI')
|
| 21 |
+
DESCRIPTION: str = os.getenv('DESCRIPTION', 'FastAPI template')
|
| 22 |
+
FIREBASE_API_KEY: str = os.getenv('FIREBASE_API_KEY', '')
|
| 23 |
+
REFRESH_TOKEN: str = os.getenv('REFRESH_TOKEN', '')
|
| 24 |
+
AUTHORIZATION_TOKEN: str = os.getenv('AUTHORIZATION_TOKEN', '')
|
| 25 |
+
|
| 26 |
+
ALLOWED_MODELS: List[Dict[str, str]] = [
|
| 27 |
+
{"id": "o1-mini", "name": "o1-mini[merlin]"},
|
| 28 |
+
{"id": "claude-3.5-sonnet", "name": "Claude 3.5 Sonnet[merlin]"},
|
| 29 |
+
{"id": "gpt-4o-64k-output", "name": "GPT 4o (Longer Output)[merlin]"},
|
| 30 |
+
{"id": "gemini-1.5-pro", "name": "Gemini 1.5 Pro[merlin]"},
|
| 31 |
+
{"id": "gpt-4o", "name": "GPT 4o[merlin]"},
|
| 32 |
+
{"id": "meta-llama/Meta-Llama-3.1-405B-Instruct", "name": "Llama 3.1 405B[merlin]"},
|
| 33 |
+
{"id": "claude-3.5-haiku", "name": "Claude 3.5 Haiku[merlin]"},
|
| 34 |
+
{"id": "claude-3-haiku", "name": "Claude 3 Haiku[merlin]"},
|
| 35 |
+
{"id": "gemini-1.5-flash", "name": "Gemini 1.5 Flash[merlin]"},
|
| 36 |
+
{"id": "gpt-4o-mini", "name": "GPT 4o mini[merlin]"},
|
| 37 |
+
{"id": "deepseek-chat", "name": "DeepSeek V3[merlin]"},
|
| 38 |
+
{"id": "o1", "name": "O1[merlin]"},
|
| 39 |
+
{"id": "o1-preview", "name": "O1 Preview[merlin]"},
|
| 40 |
+
{"id": "mistral-large-latest", "name": "Mistral Large[merlin]"},
|
| 41 |
+
{"id": "claude-3-opus", "name": "Claude 3 Opus[merlin]"},
|
| 42 |
+
{"id": "claude-3-sonnet", "name": "Claude 3 Sonnet[merlin]"},
|
| 43 |
+
{"id": "mistralai/Mixtral-8x7B-Instruct-v0.1", "name": "Mixtral[merlin]"},
|
| 44 |
+
{"id": "gpt-3.5-turbo", "name": "GPT 3.5[merlin]"},
|
| 45 |
+
]
|
| 46 |
+
MODEL_MAPPING: Dict[str, str] = {
|
| 47 |
+
"gpt-4o": "gpt-4o",
|
| 48 |
+
"o1-preview": "o1-preview",
|
| 49 |
+
"claude-3-5-sonnet": "claude-3-5-sonnet",
|
| 50 |
+
"o1-mini": "o1-mini",
|
| 51 |
+
"gemini-1.5-pro": "gemini-1.5-pro",
|
| 52 |
+
"gemini-2.0-flash": "gemini-2.0-flash",
|
| 53 |
+
}
|
| 54 |
+
HEADERS: Dict[str, str] = {
|
| 55 |
+
"authorization": f"Bearer {os.getenv('TOKEN', '')}",
|
| 56 |
+
"accept": "text/event-stream, text/event-stream",
|
| 57 |
+
"content-type": "application/json",
|
| 58 |
+
"x-merlin-version": "web-merlin",
|
| 59 |
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
| 60 |
+
"origin": "https://www.getmerlin.in",
|
| 61 |
+
"referer": "https://www.getmerlin.in/"
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
class Config:
|
| 65 |
+
env_file = '.env'
|
| 66 |
+
case_sensitive = True
|
| 67 |
+
|
| 68 |
+
_settings = None
|
| 69 |
+
|
| 70 |
+
def get_settings():
|
| 71 |
+
global _settings
|
| 72 |
+
if _settings is None:
|
| 73 |
+
_settings = Settings()
|
| 74 |
+
return _settings
|
core/example.db
ADDED
|
Binary file (20.5 kB). View file
|
|
|
core/logger.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
def setup_logger(name):
|
| 4 |
+
logger = logging.getLogger(name)
|
| 5 |
+
if not logger.handlers:
|
| 6 |
+
logger.setLevel(logging.INFO)
|
| 7 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 8 |
+
|
| 9 |
+
# 控制台处理器
|
| 10 |
+
console_handler = logging.StreamHandler()
|
| 11 |
+
console_handler.setFormatter(formatter)
|
| 12 |
+
logger.addHandler(console_handler)
|
| 13 |
+
|
| 14 |
+
# 文件处理器 - 错误级别
|
| 15 |
+
# error_file_handler = logging.FileHandler('error.log')
|
| 16 |
+
# error_file_handler.setFormatter(formatter)
|
| 17 |
+
# error_file_handler.setLevel(logging.ERROR)
|
| 18 |
+
# logger.addHandler(error_file_handler)
|
| 19 |
+
|
| 20 |
+
return logger
|
core/models.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Message(BaseModel):
|
| 6 |
+
role: str
|
| 7 |
+
content: str | list
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ChatRequest(BaseModel):
|
| 11 |
+
model: str
|
| 12 |
+
messages: List[Message]
|
| 13 |
+
stream: Optional[bool] = False
|
| 14 |
+
temperature: Optional[float] = 0.7
|
| 15 |
+
top_p: Optional[float] = 0.9
|
| 16 |
+
max_tokens: Optional[int] = 81920
|
core/refresh_token.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 用于刷新idToken的定时任务
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
from typing import Optional
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from core.utils import sign_in_with_idp, handle_firebase_response, refresh_token_via_rest
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class TokenManager:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.id_token: Optional[str] = None
|
| 13 |
+
self.last_refresh_time: Optional[float] = None
|
| 14 |
+
self.refresh_interval = 30 * 60 # 30分钟,单位:秒
|
| 15 |
+
self.is_running = False
|
| 16 |
+
|
| 17 |
+
async def get_token(self) -> str:
|
| 18 |
+
"""
|
| 19 |
+
获取当前的 idToken,如果不存在或已过期则刷新
|
| 20 |
+
"""
|
| 21 |
+
if not self.id_token or self._should_refresh():
|
| 22 |
+
await self.refresh_token()
|
| 23 |
+
return self.id_token
|
| 24 |
+
|
| 25 |
+
def _should_refresh(self) -> bool:
|
| 26 |
+
"""
|
| 27 |
+
检查是否需要刷新 token
|
| 28 |
+
"""
|
| 29 |
+
if not self.last_refresh_time:
|
| 30 |
+
return True
|
| 31 |
+
return time.time() - self.last_refresh_time >= self.refresh_interval
|
| 32 |
+
|
| 33 |
+
async def refresh_token(self):
|
| 34 |
+
"""
|
| 35 |
+
刷新 idToken
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
if os.getenv("REFRESH_TOKEN", "") == "" or os.getenv("REFRESH_TOKEN", "") == "None":
|
| 39 |
+
response = await sign_in_with_idp()
|
| 40 |
+
result = await handle_firebase_response(response) # idToken 实际就是bearer token
|
| 41 |
+
else:
|
| 42 |
+
result = await refresh_token_via_rest(os.getenv("REFRESH_TOKEN"))
|
| 43 |
+
if result is not None:
|
| 44 |
+
self.id_token = result
|
| 45 |
+
# 修改配置中的 TOKEN
|
| 46 |
+
print(f"Before Token is {os.getenv('TOKEN', '')}")
|
| 47 |
+
os.environ["TOKEN"] = self.id_token
|
| 48 |
+
print(f"Now Token is {os.getenv('TOKEN', '')}")
|
| 49 |
+
self.last_refresh_time = time.time()
|
| 50 |
+
print(f"Token refreshed at {datetime.now()}")
|
| 51 |
+
else:
|
| 52 |
+
print(f"Failed to refresh token: {result['error']}")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Error refreshing token: {str(e)}")
|
| 55 |
+
|
| 56 |
+
async def start_auto_refresh(self):
|
| 57 |
+
"""
|
| 58 |
+
启动自动刷新任务
|
| 59 |
+
"""
|
| 60 |
+
if self.is_running:
|
| 61 |
+
return
|
| 62 |
+
|
| 63 |
+
self.is_running = True
|
| 64 |
+
while self.is_running:
|
| 65 |
+
try:
|
| 66 |
+
await self.refresh_token()
|
| 67 |
+
# 等待到下次刷新时间
|
| 68 |
+
await asyncio.sleep(self.refresh_interval)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"Auto refresh error: {str(e)}")
|
| 71 |
+
# 发生错误时等待短暂时间后重试
|
| 72 |
+
await asyncio.sleep(60)
|
| 73 |
+
|
| 74 |
+
async def stop_auto_refresh(self):
|
| 75 |
+
"""
|
| 76 |
+
停止自动刷新任务
|
| 77 |
+
"""
|
| 78 |
+
self.is_running = False
|
| 79 |
+
|
| 80 |
+
# 使用示例
|
| 81 |
+
# async def main():
|
| 82 |
+
# # 创建 TokenManager 实例
|
| 83 |
+
# token_manager = TokenManager()
|
| 84 |
+
#
|
| 85 |
+
# try:
|
| 86 |
+
# # 启动自动刷新任务
|
| 87 |
+
# refresh_task = asyncio.create_task(token_manager.start_auto_refresh())
|
| 88 |
+
#
|
| 89 |
+
# # 模拟应用运行
|
| 90 |
+
# while True:
|
| 91 |
+
# # 获取当前 token
|
| 92 |
+
# token = await token_manager.get_token()
|
| 93 |
+
# print(f"Current token: {token[:20]}...")
|
| 94 |
+
#
|
| 95 |
+
# # 等待一段时间再次获取
|
| 96 |
+
# await asyncio.sleep(300) # 每5分钟打印一次当前token
|
| 97 |
+
#
|
| 98 |
+
# except KeyboardInterrupt:
|
| 99 |
+
# # 处理 Ctrl+C
|
| 100 |
+
# await token_manager.stop_auto_refresh()
|
| 101 |
+
# await refresh_task
|
| 102 |
+
#
|
| 103 |
+
# # 运行示例
|
| 104 |
+
# if __name__ == "__main__":
|
| 105 |
+
# asyncio.run(main())
|
core/router.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from fastapi import APIRouter, Response, Request, Depends, HTTPException
|
| 5 |
+
from fastapi.responses import StreamingResponse
|
| 6 |
+
from core.auth import verify_app_secret
|
| 7 |
+
from core.config import get_settings
|
| 8 |
+
from core.logger import setup_logger
|
| 9 |
+
from core.models import ChatRequest
|
| 10 |
+
from core.utils import process_streaming_response
|
| 11 |
+
|
| 12 |
+
logger = setup_logger(__name__)
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
ALLOWED_MODELS = get_settings().ALLOWED_MODELS
|
| 15 |
+
|
| 16 |
+
@router.get("/models")
|
| 17 |
+
async def list_models():
|
| 18 |
+
return {"object": "list", "data": ALLOWED_MODELS, "success": True}
|
| 19 |
+
|
| 20 |
+
@router.options("/chat/completions")
|
| 21 |
+
async def chat_completions_options():
|
| 22 |
+
return Response(
|
| 23 |
+
status_code=200,
|
| 24 |
+
headers={
|
| 25 |
+
"Access-Control-Allow-Origin": "*",
|
| 26 |
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
| 27 |
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
| 28 |
+
},
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
@router.post("/chat/completions")
|
| 32 |
+
async def chat_completions(
|
| 33 |
+
request: ChatRequest, app_secret: str = Depends(verify_app_secret)
|
| 34 |
+
):
|
| 35 |
+
logger.info("Entering chat_completions route")
|
| 36 |
+
# logger.info(f"Received request: {request}")
|
| 37 |
+
logger.info(f"Received request json format: {json.dumps(request.dict(), indent=4)}")
|
| 38 |
+
logger.info(f"App secret: {app_secret}")
|
| 39 |
+
logger.info(f"Received chat completion request for model: {request.model}")
|
| 40 |
+
|
| 41 |
+
if request.model not in [model["id"] for model in ALLOWED_MODELS]:
|
| 42 |
+
raise HTTPException(
|
| 43 |
+
status_code=400,
|
| 44 |
+
detail=f"Model {request.model} is not allowed. Allowed models are: {', '.join(model['id'] for model in ALLOWED_MODELS)}",
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
if request.stream:
|
| 48 |
+
logger.info("Streaming response")
|
| 49 |
+
return StreamingResponse(
|
| 50 |
+
process_streaming_response(request, app_secret),
|
| 51 |
+
media_type="text/event-stream",
|
| 52 |
+
headers={
|
| 53 |
+
"Cache-Control": "no-cache",
|
| 54 |
+
"Connection": "keep-alive",
|
| 55 |
+
"Transfer-Encoding": "chunked"
|
| 56 |
+
}
|
| 57 |
+
)
|
| 58 |
+
else:
|
| 59 |
+
logger.info("Non-streaming response")
|
| 60 |
+
# return await process_non_streaming_response(request)
|
| 61 |
+
|
| 62 |
+
@router.route('/')
|
| 63 |
+
@router.route('/healthz')
|
| 64 |
+
@router.route('/ready')
|
| 65 |
+
@router.route('/alive')
|
| 66 |
+
@router.route('/status')
|
| 67 |
+
@router.get("/health")
|
| 68 |
+
async def health_check(request: Request):
|
| 69 |
+
return Response(content=json.dumps({"status": "ok"}), media_type="application/json")
|
| 70 |
+
|
| 71 |
+
@router.post("/env")
|
| 72 |
+
async def environment(app_secret: str = Depends(verify_app_secret)):
|
| 73 |
+
return Response(content=json.dumps({"token": os.getenv("TOKEN", ""), "refresh_token": os.getenv("REFRESH_TOKEN", ""), "key": os.getenv("FIREBASE_API_KEY", "")}), media_type="application/json")
|
core/scheduler.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 2 |
+
from apscheduler.triggers.cron import CronTrigger
|
| 3 |
+
import pytz
|
| 4 |
+
from core.config import get_settings
|
| 5 |
+
|
| 6 |
+
class SchedulerManager:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
self.scheduler = AsyncIOScheduler(timezone=pytz.timezone(get_settings().TIMEZONE))
|
| 9 |
+
self._configure_jobs()
|
| 10 |
+
|
| 11 |
+
def _configure_jobs(self):
|
| 12 |
+
"""配置所有定时任务"""
|
| 13 |
+
from task.scheduled_tasks import (
|
| 14 |
+
daily_task,
|
| 15 |
+
weekly_report_task,
|
| 16 |
+
data_cleanup_task
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# 添加定时任务,注意这些任务都是异步的
|
| 20 |
+
self.scheduler.add_job(
|
| 21 |
+
daily_task,
|
| 22 |
+
trigger=CronTrigger(hour='8', minute='13', timezone=pytz.timezone(get_settings().TIMEZONE)),
|
| 23 |
+
id='daily_task'
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
self.scheduler.add_job(
|
| 27 |
+
daily_task,
|
| 28 |
+
trigger=CronTrigger(hour='9', minute='13', timezone=pytz.timezone(get_settings().TIMEZONE)),
|
| 29 |
+
id='daily_task_1'
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
self.scheduler.add_job(
|
| 33 |
+
weekly_report_task,
|
| 34 |
+
trigger=CronTrigger(day_of_week='mon', hour='9', timezone=pytz.timezone(get_settings().TIMEZONE)),
|
| 35 |
+
id='weekly_report'
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
self.scheduler.add_job(
|
| 39 |
+
data_cleanup_task,
|
| 40 |
+
trigger=CronTrigger(hour='2', timezone=pytz.timezone(get_settings().TIMEZONE)),
|
| 41 |
+
id='data_cleanup'
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
async def start(self):
|
| 45 |
+
"""异步启动调度器"""
|
| 46 |
+
self.scheduler.start()
|
| 47 |
+
|
| 48 |
+
async def shutdown(self):
|
| 49 |
+
"""异步关闭调度器"""
|
| 50 |
+
self.scheduler.shutdown()
|
| 51 |
+
|
| 52 |
+
def get_jobs(self):
|
| 53 |
+
"""获取所有任务"""
|
| 54 |
+
return self.scheduler.get_jobs()
|
| 55 |
+
|
| 56 |
+
def add_job(self, func, trigger, **kwargs):
|
| 57 |
+
"""添加新任务"""
|
| 58 |
+
return self.scheduler.add_job(func, trigger, **kwargs)
|
| 59 |
+
|
| 60 |
+
def remove_job(self, job_id):
|
| 61 |
+
"""移除任务"""
|
| 62 |
+
self.scheduler.remove_job(job_id)
|
| 63 |
+
|
| 64 |
+
|
core/sqlite_store.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sqlite3
|
| 3 |
+
import queue
|
| 4 |
+
import threading
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from contextlib import contextmanager
|
| 7 |
+
|
| 8 |
+
class SQLiteConnectionPool:
|
| 9 |
+
def __init__(self, database, max_connections=5):
|
| 10 |
+
# 展开 ~ 到用户主目录并获取绝对路径
|
| 11 |
+
db_path = os.path.abspath(os.path.expanduser(database))
|
| 12 |
+
# 获取目录路径
|
| 13 |
+
db_dir = os.path.dirname(db_path)
|
| 14 |
+
if db_dir: # 如果有目录部分
|
| 15 |
+
os.makedirs(db_dir, exist_ok=True)
|
| 16 |
+
|
| 17 |
+
self.database = db_path
|
| 18 |
+
self.max_connections = max_connections
|
| 19 |
+
self.connections = queue.Queue(maxsize=max_connections)
|
| 20 |
+
self.lock = threading.Lock()
|
| 21 |
+
|
| 22 |
+
# 初始化连接池
|
| 23 |
+
for _ in range(max_connections):
|
| 24 |
+
conn = sqlite3.connect(self.database, check_same_thread=False)
|
| 25 |
+
# 设置行工厂,返回字典格式的结果
|
| 26 |
+
conn.row_factory = sqlite3.Row
|
| 27 |
+
self.connections.put(conn)
|
| 28 |
+
|
| 29 |
+
@contextmanager
|
| 30 |
+
def get_connection(self):
|
| 31 |
+
connection = self.connections.get()
|
| 32 |
+
try:
|
| 33 |
+
yield connection
|
| 34 |
+
finally:
|
| 35 |
+
self.connections.put(connection)
|
| 36 |
+
|
| 37 |
+
def close_all(self):
|
| 38 |
+
while not self.connections.empty():
|
| 39 |
+
conn = self.connections.get()
|
| 40 |
+
conn.close()
|
| 41 |
+
|
| 42 |
+
# 数据库操作类
|
| 43 |
+
class DatabaseManager:
|
| 44 |
+
def __init__(self, pool):
|
| 45 |
+
self.pool = pool
|
| 46 |
+
self.create_tables()
|
| 47 |
+
|
| 48 |
+
def create_tables(self):
|
| 49 |
+
with self.pool.get_connection() as conn:
|
| 50 |
+
cursor = conn.cursor()
|
| 51 |
+
|
| 52 |
+
# 创建用户表
|
| 53 |
+
cursor.execute('''
|
| 54 |
+
CREATE TABLE IF NOT EXISTS context_records (
|
| 55 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 56 |
+
api_key TEXT NOT NULL,
|
| 57 |
+
chat_id TEXT NOT NULL,
|
| 58 |
+
parent_id TEXT NOT NULL,
|
| 59 |
+
sha256_hash TEXT NOT NULL,
|
| 60 |
+
created_at TIMESTAMP,
|
| 61 |
+
updated_at TIMESTAMP
|
| 62 |
+
)
|
| 63 |
+
''')
|
| 64 |
+
|
| 65 |
+
conn.commit()
|
| 66 |
+
|
| 67 |
+
def insert_context_record(self, api_key, chat_id, parent_id, sha256_hash):
|
| 68 |
+
with self.pool.get_connection() as conn:
|
| 69 |
+
cursor = conn.cursor()
|
| 70 |
+
try:
|
| 71 |
+
cursor.execute(
|
| 72 |
+
'INSERT INTO context_records (api_key, chat_id, parent_id, sha256_hash, created_at) VALUES (?, ?, ?, ?, ?)',
|
| 73 |
+
(api_key, chat_id, parent_id, sha256_hash, datetime.now())
|
| 74 |
+
)
|
| 75 |
+
conn.commit()
|
| 76 |
+
return cursor.lastrowid
|
| 77 |
+
except sqlite3.Error as e:
|
| 78 |
+
print(f"Error inserting context_records: {e}")
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
def update_context_record_by_chat_id(self, api_key, chat_id, parent_id, sha256_hash):
|
| 82 |
+
with self.pool.get_connection() as conn:
|
| 83 |
+
cursor = conn.cursor()
|
| 84 |
+
try:
|
| 85 |
+
cursor.execute(
|
| 86 |
+
'update context_records set parent_id = ?, sha256_hash = ?, updated_at = ? where api_key = ? and chat_id = ?',
|
| 87 |
+
(parent_id, sha256_hash, datetime.now(), api_key, chat_id)
|
| 88 |
+
)
|
| 89 |
+
conn.commit()
|
| 90 |
+
return cursor.lastrowid
|
| 91 |
+
except sqlite3.Error as e:
|
| 92 |
+
print(f"Error inserting context_records: {e}")
|
| 93 |
+
return None
|
| 94 |
+
|
| 95 |
+
def get_context_record_by_sha256_hash(self, sha256_hash):
|
| 96 |
+
with self.pool.get_connection() as conn:
|
| 97 |
+
cursor = conn.cursor()
|
| 98 |
+
cursor.execute('SELECT * FROM context_records WHERE sha256_hash = ?', (sha256_hash,))
|
| 99 |
+
result = cursor.fetchone()
|
| 100 |
+
return dict(result) if result else None
|
| 101 |
+
|
| 102 |
+
# 使用示例
|
| 103 |
+
def main():
|
| 104 |
+
# 创建连接池
|
| 105 |
+
|
| 106 |
+
pool = SQLiteConnectionPool('~/tmp/merlin-sqlite.db', max_connections=5)
|
| 107 |
+
db = DatabaseManager(pool)
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
# 创建表
|
| 111 |
+
db.create_tables()
|
| 112 |
+
|
| 113 |
+
# 模拟多线程操作
|
| 114 |
+
def worker(user_number):
|
| 115 |
+
username = f"user_{user_number}"
|
| 116 |
+
email = f"{username}@example.com"
|
| 117 |
+
|
| 118 |
+
# 插入用户
|
| 119 |
+
user_id = db.insert_user(username, email)
|
| 120 |
+
if user_id:
|
| 121 |
+
# 插入订单
|
| 122 |
+
db.insert_order(user_id, 100.50 * user_number)
|
| 123 |
+
db.insert_order(user_id, 200.75 * user_number)
|
| 124 |
+
|
| 125 |
+
# 查询订单
|
| 126 |
+
orders = db.get_user_orders(username)
|
| 127 |
+
print(f"Orders for {username}:")
|
| 128 |
+
for order in orders:
|
| 129 |
+
print(f"Amount: {order['amount']}, Date: {order['order_date']}")
|
| 130 |
+
|
| 131 |
+
# 创建多个线程
|
| 132 |
+
# threads = []
|
| 133 |
+
# for i in range(3):
|
| 134 |
+
# t = threading.Thread(target=worker, args=(i+1,))
|
| 135 |
+
# threads.append(t)
|
| 136 |
+
# t.start()
|
| 137 |
+
#
|
| 138 |
+
# # 等待所有线程完成
|
| 139 |
+
# for t in threads:
|
| 140 |
+
# t.join()
|
| 141 |
+
|
| 142 |
+
finally:
|
| 143 |
+
# 关闭所有连接
|
| 144 |
+
pool.close_all()
|
| 145 |
+
|
| 146 |
+
# 批量操作示例
|
| 147 |
+
def batch_insert_example(db):
|
| 148 |
+
with db.pool.get_connection() as conn:
|
| 149 |
+
cursor = conn.cursor()
|
| 150 |
+
try:
|
| 151 |
+
# 开始事务
|
| 152 |
+
cursor.execute('BEGIN TRANSACTION')
|
| 153 |
+
|
| 154 |
+
# 准备批量数据
|
| 155 |
+
users_data = [
|
| 156 |
+
('user1', 'user1@example.com', datetime.now()),
|
| 157 |
+
('user2', 'user2@example.com', datetime.now()),
|
| 158 |
+
('user3', 'user3@example.com', datetime.now())
|
| 159 |
+
]
|
| 160 |
+
|
| 161 |
+
# 批量插入
|
| 162 |
+
cursor.executemany(
|
| 163 |
+
'INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)',
|
| 164 |
+
users_data
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# 提交事务
|
| 168 |
+
conn.commit()
|
| 169 |
+
except sqlite3.Error as e:
|
| 170 |
+
print(f"Error in batch insert: {e}")
|
| 171 |
+
conn.rollback()
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
main()
|
core/utils.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import codecs
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
import json
|
| 7 |
+
import uuid
|
| 8 |
+
# from http.client import HTTPException
|
| 9 |
+
from typing import Dict, Any, Optional
|
| 10 |
+
from fastapi import HTTPException
|
| 11 |
+
import ssl
|
| 12 |
+
import httpx
|
| 13 |
+
from httpx import ConnectError, TransportError
|
| 14 |
+
from starlette import status
|
| 15 |
+
|
| 16 |
+
from core.config import get_settings
|
| 17 |
+
from core.logger import setup_logger
|
| 18 |
+
from core.models import ChatRequest
|
| 19 |
+
from core.sqlite_store import SQLiteConnectionPool, DatabaseManager
|
| 20 |
+
|
| 21 |
+
settings = get_settings()
|
| 22 |
+
logger = setup_logger(__name__)
|
| 23 |
+
|
| 24 |
+
def decode_unicode_escape(s):
|
| 25 |
+
# 检查输入是否为字典类型
|
| 26 |
+
if isinstance(s, dict):
|
| 27 |
+
return s
|
| 28 |
+
# 如果需要,将输入转换为字符串
|
| 29 |
+
if not isinstance(s, (str, bytes)):
|
| 30 |
+
s = str(s)
|
| 31 |
+
# 如果是字符串,转换为字节
|
| 32 |
+
if isinstance(s, str):
|
| 33 |
+
s = s.encode('utf-8')
|
| 34 |
+
return codecs.decode(s, 'unicode_escape')
|
| 35 |
+
|
| 36 |
+
FIREBASE_API_KEY = settings.FIREBASE_API_KEY
|
| 37 |
+
async def refresh_token_via_rest(refresh_token):
|
| 38 |
+
# Firebase Auth REST API endpoint
|
| 39 |
+
url = f"https://securetoken.googleapis.com/v1/token?key={FIREBASE_API_KEY}"
|
| 40 |
+
|
| 41 |
+
payload = {
|
| 42 |
+
'grant_type': 'refresh_token',
|
| 43 |
+
'refresh_token': refresh_token
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
async with httpx.AsyncClient() as client:
|
| 48 |
+
response = await client.post(url, json=payload)
|
| 49 |
+
if response.status_code == 200:
|
| 50 |
+
data = response.json()
|
| 51 |
+
print(json.dumps(data, indent=2))
|
| 52 |
+
# return {
|
| 53 |
+
# 'id_token': data['id_token'],
|
| 54 |
+
# 'refresh_token': data.get('refresh_token'),
|
| 55 |
+
# 'expires_in': data['expires_in']
|
| 56 |
+
# }
|
| 57 |
+
return data['id_token']
|
| 58 |
+
else:
|
| 59 |
+
print(f"刷新失败: {response.text}")
|
| 60 |
+
return None
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"请求异常: {e}")
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
async def sign_in_with_idp():
|
| 67 |
+
url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp"
|
| 68 |
+
|
| 69 |
+
# 查询参数
|
| 70 |
+
params = {
|
| 71 |
+
"key": FIREBASE_API_KEY
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
# 请求头
|
| 75 |
+
headers = {
|
| 76 |
+
"X-Client-Version": "Node/JsCore/10.5.2/FirebaseCore-web",
|
| 77 |
+
"X-Firebase-gmpid": "1:252179682924:web:9c80c6a32cb4682cbfaa49",
|
| 78 |
+
"Content-Type": "application/json",
|
| 79 |
+
"User-Agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
# 请求体
|
| 83 |
+
data = {
|
| 84 |
+
"requestUri": "http://localhost",
|
| 85 |
+
"returnSecureToken": True,
|
| 86 |
+
"postBody": f"&id_token={settings.AUTHORIZATION_TOKEN}&providerId=google.com"
|
| 87 |
+
}
|
| 88 |
+
print("Request Headers:", json.dumps(headers, indent=2)) # 格式化打印
|
| 89 |
+
print("Request Body:", json.dumps(data, indent=2)) # 格式化打印
|
| 90 |
+
print("Request params:", json.dumps(params, indent=2)) # 格式化打印
|
| 91 |
+
|
| 92 |
+
async with httpx.AsyncClient() as client:
|
| 93 |
+
response = await client.post(
|
| 94 |
+
url,
|
| 95 |
+
params=params,
|
| 96 |
+
headers=headers,
|
| 97 |
+
json=data
|
| 98 |
+
)
|
| 99 |
+
# 检查状态码
|
| 100 |
+
if response.status_code == 200:
|
| 101 |
+
return response.json()
|
| 102 |
+
else:
|
| 103 |
+
raise Exception(f"Request failed with status code: {response.status_code}")
|
| 104 |
+
|
| 105 |
+
async def handle_firebase_response(response) -> str:
|
| 106 |
+
try:
|
| 107 |
+
# 如果响应是字典(已经解析的 JSON)
|
| 108 |
+
if isinstance(response, dict):
|
| 109 |
+
print(json.dumps(response, indent=2))
|
| 110 |
+
if response.get('error', {}).get('code') == 400:
|
| 111 |
+
print("Invalid id_token in IdP response")
|
| 112 |
+
# 保存refresh_token到配置中
|
| 113 |
+
if 'refreshToken' in response:
|
| 114 |
+
os.environ["REFRESH_TOKEN"] = response['refreshToken']
|
| 115 |
+
if 'idToken' in response:
|
| 116 |
+
return response['idToken']
|
| 117 |
+
else:
|
| 118 |
+
raise ValueError("dict case Response does not contain idToken")
|
| 119 |
+
|
| 120 |
+
# 如果响应是 Response 对象
|
| 121 |
+
elif hasattr(response, 'status_code'):
|
| 122 |
+
if response.status_code == 200:
|
| 123 |
+
data = response.json()
|
| 124 |
+
print(data)
|
| 125 |
+
# 保存refresh_token到配置中
|
| 126 |
+
if 'refreshToken' in data:
|
| 127 |
+
os.environ["REFRESH_TOKEN"] = data['refreshToken']
|
| 128 |
+
if 'idToken' in data:
|
| 129 |
+
return data['idToken']
|
| 130 |
+
else:
|
| 131 |
+
raise ValueError("response case Response does not contain idToken")
|
| 132 |
+
|
| 133 |
+
# 处理其他状态码
|
| 134 |
+
elif response.status_code == 400:
|
| 135 |
+
error_data = response.json()
|
| 136 |
+
raise ValueError(f"Bad Request: {error_data.get('error', {}).get('message', 'Unknown error')}")
|
| 137 |
+
elif response.status_code == 401:
|
| 138 |
+
raise ValueError("Unauthorized: Invalid credentials")
|
| 139 |
+
elif response.status_code == 403:
|
| 140 |
+
raise ValueError("Forbidden: Insufficient permissions")
|
| 141 |
+
elif response.status_code == 404:
|
| 142 |
+
raise ValueError("Not Found: Resource doesn't exist")
|
| 143 |
+
else:
|
| 144 |
+
raise ValueError(f"Unexpected status code: {response.status_code}")
|
| 145 |
+
|
| 146 |
+
else:
|
| 147 |
+
raise ValueError(f"Unexpected response type: {type(response)}")
|
| 148 |
+
|
| 149 |
+
except json.JSONDecodeError:
|
| 150 |
+
raise ValueError("Invalid JSON response")
|
| 151 |
+
except Exception as e:
|
| 152 |
+
raise ValueError(f"Error processing response: {str(e)}")
|
| 153 |
+
|
| 154 |
+
# SHA-256
|
| 155 |
+
def _sha256_hash(text):
|
| 156 |
+
sha256 = hashlib.sha256()
|
| 157 |
+
sha256.update(text.encode('utf-8'))
|
| 158 |
+
return sha256.hexdigest()
|
| 159 |
+
|
| 160 |
+
# 处理字典列表
|
| 161 |
+
def sha256_hash_messages(messages):
|
| 162 |
+
# 只提取 role 为 "user" 的消息的 content 字段
|
| 163 |
+
message_data = [str(msg['content']) for msg in messages if msg['role'] == "user"]
|
| 164 |
+
print("Filtered contents:", message_data) # 调试用
|
| 165 |
+
json_str = json.dumps(message_data, sort_keys=True)
|
| 166 |
+
print("JSON string:", json_str) # 调试用
|
| 167 |
+
return hashlib.sha256(json_str.encode('utf-8')).hexdigest()
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def create_chat_completion_data(
|
| 171 |
+
content: str, model: str, timestamp: int, finish_reason: Optional[str] = None
|
| 172 |
+
) -> Dict[str, Any]:
|
| 173 |
+
return {
|
| 174 |
+
"id": f"chatcmpl-{uuid.uuid4()}",
|
| 175 |
+
"object": "chat.completion.chunk",
|
| 176 |
+
"created": timestamp,
|
| 177 |
+
"model": model,
|
| 178 |
+
"choices": [
|
| 179 |
+
{
|
| 180 |
+
"index": 0,
|
| 181 |
+
"delta": {"content": content, "role": "assistant"},
|
| 182 |
+
"finish_reason": finish_reason,
|
| 183 |
+
}
|
| 184 |
+
],
|
| 185 |
+
"usage": None,
|
| 186 |
+
}
|
| 187 |
+
pool = SQLiteConnectionPool('~/tmp/merlin-sqlite.db', max_connections=5)
|
| 188 |
+
db = DatabaseManager(pool)
|
| 189 |
+
async def process_streaming_response(request: ChatRequest, app_secret: str):
|
| 190 |
+
# 创建自定义 SSL 上下文
|
| 191 |
+
ssl_context = ssl.create_default_context()
|
| 192 |
+
ssl_context.check_hostname = True
|
| 193 |
+
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
| 194 |
+
# 1. 获取消息列表并转换为字典列表
|
| 195 |
+
# previous_messages = request['messages'][:-1]
|
| 196 |
+
# previous_messages = [msg.model_dump() for msg in request.messages[:-1]]
|
| 197 |
+
# previous_messages = [dict(msg) for msg in request.messages[:-1]]
|
| 198 |
+
# 如果用户有系统提示词,则不包含在上下文中 merlin chat
|
| 199 |
+
previous_messages = [
|
| 200 |
+
msg.model_dump(include={'role', 'content'})
|
| 201 |
+
for msg in request.messages[:-1]
|
| 202 |
+
if msg.role == "user"
|
| 203 |
+
]
|
| 204 |
+
# 对话上下文处理
|
| 205 |
+
# sqlite数据存储结构
|
| 206 |
+
# api_key chat_id parent_id sha256_hash # 用于快速查找
|
| 207 |
+
message = [dict(msg) for msg in request.messages]
|
| 208 |
+
if not previous_messages:
|
| 209 |
+
chat_id = str(uuid.uuid4())
|
| 210 |
+
child_id = str(uuid.uuid4())
|
| 211 |
+
parent_id = "root"
|
| 212 |
+
# messages = [msg.model_dump() for msg in request.messages]
|
| 213 |
+
db.insert_context_record(app_secret, chat_id, child_id, sha256_hash_messages(message))
|
| 214 |
+
print("Insert new context record: hash=", sha256_hash_messages(message))
|
| 215 |
+
else:
|
| 216 |
+
sha256_hash = sha256_hash_messages(previous_messages)
|
| 217 |
+
print("func get_context_record_by_sha256_hash SHA-256 hash:", sha256_hash)
|
| 218 |
+
context_record = db.get_context_record_by_sha256_hash(sha256_hash)
|
| 219 |
+
chat_id = context_record['chat_id']
|
| 220 |
+
child_id = str(uuid.uuid4())
|
| 221 |
+
parent_id = context_record['parent_id']
|
| 222 |
+
json_data = {
|
| 223 |
+
"attachments": [],
|
| 224 |
+
"chatId": chat_id,
|
| 225 |
+
"language": "AUTO",
|
| 226 |
+
"message": {
|
| 227 |
+
"childId": child_id,
|
| 228 |
+
"content": request.messages[-1].content,
|
| 229 |
+
"context": "",
|
| 230 |
+
"id": str(uuid.uuid4()),
|
| 231 |
+
"parentId": parent_id
|
| 232 |
+
},
|
| 233 |
+
"metadata": {
|
| 234 |
+
"largeContext": False,
|
| 235 |
+
"merlinMagic": False,
|
| 236 |
+
"proFinderMode": False,
|
| 237 |
+
"webAccess": False
|
| 238 |
+
},
|
| 239 |
+
"mode": "UNIFIED_CHAT",
|
| 240 |
+
# "model": "claude-3.5-sonnet"
|
| 241 |
+
"model": request.model
|
| 242 |
+
}
|
| 243 |
+
async with httpx.AsyncClient(
|
| 244 |
+
verify=ssl_context,
|
| 245 |
+
# timeout=30.0, # 增加超时时间
|
| 246 |
+
# http2=True # 启用 HTTP/2
|
| 247 |
+
) as client:
|
| 248 |
+
try:
|
| 249 |
+
request_headers = {**settings.HEADERS, 'authorization': f"Bearer {os.getenv('TOKEN', '')}"} # 从环境变量中获取新的TOKEN
|
| 250 |
+
|
| 251 |
+
print("Request Headers:", json.dumps(request_headers, indent=2)) # 格式化打印
|
| 252 |
+
print("Request Body:", json.dumps(json_data, indent=2)) # 格式化打印
|
| 253 |
+
async with client.stream(
|
| 254 |
+
"POST",
|
| 255 |
+
f"https://arcane.getmerlin.in/v1/thread/unified",
|
| 256 |
+
headers=request_headers,
|
| 257 |
+
json=json_data,
|
| 258 |
+
timeout=100,
|
| 259 |
+
) as response:
|
| 260 |
+
response.raise_for_status()
|
| 261 |
+
timestamp = int(datetime.now().timestamp())
|
| 262 |
+
async for line in response.aiter_lines():
|
| 263 |
+
# print(f"{type(line)}: {line}")
|
| 264 |
+
if line and line.startswith("data: "):
|
| 265 |
+
try:
|
| 266 |
+
data_str = line[6:] # 去掉 'data: ' 前缀
|
| 267 |
+
# 解析JSON
|
| 268 |
+
json_data = json.loads(data_str)
|
| 269 |
+
# print(json_data)
|
| 270 |
+
# 解码Unicode转义序列
|
| 271 |
+
# human_readable = decode_unicode_escape(decoded_chunk)
|
| 272 |
+
# print(human_readable)
|
| 273 |
+
# 每日用量超限时 返回的数据处理
|
| 274 |
+
# data: {"message":"Daily pro model limit exhausted. Switch to any base model or Go Pro!","type":"USAGE_LIMIT_REACHED_FREE"}
|
| 275 |
+
if 'type' in json_data and json_data['type'] == 'USAGE_LIMIT_REACHED_FREE':
|
| 276 |
+
content = json_data['message']
|
| 277 |
+
print(content, end='', flush=True) # 使用 end='' 来避免额外换行
|
| 278 |
+
yield f"data: {json.dumps(create_chat_completion_data(content, request.model, timestamp))}\n\n"
|
| 279 |
+
# data: {"status":"system","data":{"content":" ","eventType":"DONE"}}
|
| 280 |
+
if 'data' in json_data and 'eventType' in json_data['data'] and json_data['data']['eventType'] == 'DONE':
|
| 281 |
+
await response.aclose()
|
| 282 |
+
break
|
| 283 |
+
# yield f"data: {json.dumps(create_chat_completion_data('', 'gpt-4o-mini', timestamp, 'stop'))}\n\n"
|
| 284 |
+
# yield "data: [DONE]\n\n"
|
| 285 |
+
# await asyncio.sleep(0)
|
| 286 |
+
# return
|
| 287 |
+
# 提取并解码content内容
|
| 288 |
+
# if 'data' in human_readable and 'content' in human_readable['data']:
|
| 289 |
+
# data: {"data":{"content":"xx"}}
|
| 290 |
+
if 'data' in json_data and 'content' in json_data['data'] and json_data['data']['content'] != '':
|
| 291 |
+
content = json_data['data']['content']
|
| 292 |
+
# human_readable_content = decode_unicode_escape(content)
|
| 293 |
+
print(content, end='', flush=True) # 使用 end='' 来避免额外换行
|
| 294 |
+
yield f"data: {json.dumps(create_chat_completion_data(content, request.model, timestamp))}\n\n"
|
| 295 |
+
|
| 296 |
+
except json.JSONDecodeError as e:
|
| 297 |
+
print(f"JSON解析错误: {e}")
|
| 298 |
+
print(f"原始数据: {line}")
|
| 299 |
+
continue
|
| 300 |
+
|
| 301 |
+
yield f"data: {json.dumps(create_chat_completion_data('', request.model, timestamp, 'stop'))}\n\n"
|
| 302 |
+
yield "data: [DONE]\n\n"
|
| 303 |
+
# 更新sha256_hash
|
| 304 |
+
db.update_context_record_by_chat_id(app_secret, chat_id, child_id, sha256_hash_messages(message))
|
| 305 |
+
except ConnectError as e:
|
| 306 |
+
logger.error(f"Connection error details: {str(e)}")
|
| 307 |
+
raise HTTPException(
|
| 308 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 309 |
+
detail="Service temporarily unavailable. Please try again later."
|
| 310 |
+
)
|
| 311 |
+
except TransportError as e:
|
| 312 |
+
logger.error(f"Transport error details: {str(e)}")
|
| 313 |
+
raise HTTPException(
|
| 314 |
+
status_code=status.HTTP_502_BAD_GATEWAY,
|
| 315 |
+
detail="Network transport error occurred."
|
| 316 |
+
)
|
| 317 |
+
except httpx.HTTPStatusError as e:
|
| 318 |
+
# 这里需要处理401错误
|
| 319 |
+
logger.error(f"HTTP error occurred: {e}")
|
| 320 |
+
raise HTTPException(status_code=e.response.status_code, detail=str(e))
|
| 321 |
+
except httpx.RequestError as e:
|
| 322 |
+
logger.error(f"Error occurred during request: {e}")
|
| 323 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
| 324 |
+
finally:
|
| 325 |
+
await response.aclose()
|
| 326 |
+
# except ConnectError as e:
|
| 327 |
+
# logger.error(f"Connection error: {str(e)}")
|
| 328 |
+
# raise HTTPException(
|
| 329 |
+
# status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 330 |
+
# detail="Service temporarily unavailable. Please try again later."
|
| 331 |
+
# )
|
| 332 |
+
# except asyncio.CancelledError:
|
| 333 |
+
# logger.info("Request was cancelled by client")
|
| 334 |
+
# raise # Let the framework handle cancellation
|
| 335 |
+
# except Exception as e:
|
| 336 |
+
# logger.error(f"Unexpected error: {str(e)}", exc_info=True)
|
| 337 |
+
# raise HTTPException(
|
| 338 |
+
# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 339 |
+
# detail="An unexpected error occurred"
|
| 340 |
+
# )
|
| 341 |
+
# finally:
|
| 342 |
+
# # 确保资源被正确清理
|
| 343 |
+
# if 'client' in locals():
|
| 344 |
+
# await client.aclose()
|
| 345 |
+
|
| 346 |
+
# async def process_non_streaming_response(request: ChatRequest):
|
| 347 |
+
# json_data = {
|
| 348 |
+
# "messages": [message_to_dict(msg) for msg in request.messages],
|
| 349 |
+
# "previewToken": None,
|
| 350 |
+
# "userId": None,
|
| 351 |
+
# "codeModelMode": True,
|
| 352 |
+
# "agentMode": {},
|
| 353 |
+
# "trendingAgentMode": {},
|
| 354 |
+
# "isMicMode": False,
|
| 355 |
+
# "userSystemPrompt": None,
|
| 356 |
+
# "maxTokens": request.max_tokens,
|
| 357 |
+
# "playgroundTopP": request.top_p,
|
| 358 |
+
# "playgroundTemperature": request.temperature,
|
| 359 |
+
# "isChromeExt": False,
|
| 360 |
+
# "githubToken": None,
|
| 361 |
+
# "clickedAnswer2": False,
|
| 362 |
+
# "clickedAnswer3": False,
|
| 363 |
+
# "clickedForceWebSearch": False,
|
| 364 |
+
# "visitFromDelta": False,
|
| 365 |
+
# "mobileClient": False,
|
| 366 |
+
# "userSelectedModel": MODEL_MAPPING.get(request.model),
|
| 367 |
+
# "validated": validate.getVid(),
|
| 368 |
+
# "webSearchModePrompt": True if request.model.endswith("-search") else False
|
| 369 |
+
# }
|
| 370 |
+
# full_response = ""
|
| 371 |
+
# async with httpx.AsyncClient() as client:
|
| 372 |
+
# async with client.stream(
|
| 373 |
+
# method="POST", url=f"{BASE_URL}/api/chat", headers=settings.HEADERS, json=json_data
|
| 374 |
+
# ) as response:
|
| 375 |
+
# async for chunk in response.aiter_text():
|
| 376 |
+
# full_response += chunk
|
| 377 |
+
# if "https://www.blackbox.ai" in full_response:
|
| 378 |
+
# validate.getVid(True)
|
| 379 |
+
# full_response = "vid已刷新,重新对话即可"
|
| 380 |
+
# if full_response.startswith("$@$v=undefined-rv1$@$"):
|
| 381 |
+
# full_response = full_response[21:]
|
| 382 |
+
# return {
|
| 383 |
+
# "id": f"chatcmpl-{uuid.uuid4()}",
|
| 384 |
+
# "object": "chat.completion",
|
| 385 |
+
# "created": int(datetime.now().timestamp()),
|
| 386 |
+
# "model": request.model,
|
| 387 |
+
# "choices": [
|
| 388 |
+
# {
|
| 389 |
+
# "index": 0,
|
| 390 |
+
# "message": {"role": "assistant", "content": full_response},
|
| 391 |
+
# "finish_reason": "stop",
|
| 392 |
+
# }
|
| 393 |
+
# ],
|
| 394 |
+
# "usage": None,
|
| 395 |
+
# }
|
| 396 |
+
|
| 397 |
+
# POST /v1/accounts:signInWithIdp?key=AIzaSyCMMynYm5VRHj1NOwkfWinX-HYsFArdUbk HTTP/1.1
|
| 398 |
+
# X-Client-Version: Node/JsCore/10.5.2/FirebaseCore-web
|
| 399 |
+
# X-Firebase-gmpid: 1:252179682924:web:9c80c6a32cb4682cbfaa49
|
| 400 |
+
# Content-Type: application/json
|
| 401 |
+
# Accept: */*
|
| 402 |
+
# Content-Length: 1269
|
| 403 |
+
# User-Agent: node-fetch/1.0 (+https://github.com/bitinn/node-fetch)
|
| 404 |
+
# Accept-Encoding: gzip,deflate
|
| 405 |
+
# Connection: close
|
| 406 |
+
# Host: identitytoolkit.googleapis.com
|
| 407 |
+
#
|
| 408 |
+
# {"requestUri":"http://localhost","returnSecureToken":true,"postBody":"&id_token=aaa.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIyNTIxNzk2ODI5MjQtYWhmcTh2amtyOGEwOGFrMjMxMGFiajZqM29jZ2I5azIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyNTIxNzk2ODI5MjQtYWhmcTh2amtyOGEwOGFrMjMxMGFiajZqM29jZ2I5azIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDI3Mzc4NDA0NjA0MDg3ODQ5NTEiLCJoZCI6ImNoYXRncHQubnljLm1uIiwiZW1haWwiOiJrYmluQGNoYXRncHQubnljLm1uIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJhQ3JLcVlzZ0JzT1JIQnFqQ2k1N0VBIiwibmFtZSI6ImtCaW4gbGVlIiwicGljdHVyZSI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0ktUWJ1WHQzd3UzaUJsMUNkMzVIN1JrQ29xRW5NUmxPLWxwMEcxUVBsYm5ZTGd1dz1zOTYtYyIsImdpdmVuX25hbWUiOiJrQmluIiwiZmFtaWx5X25hbWUiOiJsZWUiLCJpYXQiOjE3MzYwNDAyODMsImV4cCI6MTczNjA0Mzg4M30.hC14X8oJhKzXOU_STfmmgXItPGOT_RYQKNtO_KpisXA6d8NRuCArAV6YOy10pvHVYElTk-Avpe2ymQCl58K2Itw05xpuGQ1EhF-8u_rUFmiBs0w1wtFPt57tnMdzbqyLs_OK7ndUYl-myVoTi-2JVM6P2rMSYqCLkM0jkAeGsiOLFjXw3wQpiOAmUVbiJ8mjztdK2jBMqK91C18vJ9BROkPog_rX5hQYPbTNvrdKPplNJb94NVssXXDuAUShO6ADOCJZ6EXj4IxyD5vyUP_0sHX9tdQs6wJXcmEgNBwgEPJ7DjdcBaEzeG9o-F3v9uMHjCbLCH4ben5KcolAqHVzmw&providerId=google.com"}
|
| 409 |
+
#
|
| 410 |
+
#
|
| 411 |
+
#
|
| 412 |
+
# async def sign_in_with_idp():
|
| 413 |
+
# url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp"
|
| 414 |
+
#
|
| 415 |
+
# # 查询参数
|
| 416 |
+
# params = {
|
| 417 |
+
# "key": "AIzaSyCMMynYm5VRHj1NOwkfWinX-HYsFArdUbk"
|
| 418 |
+
# }
|
| 419 |
+
#
|
| 420 |
+
# # 请求头
|
| 421 |
+
# headers = {
|
| 422 |
+
# "X-Client-Version": "Node/JsCore/10.5.2/FirebaseCore-web",
|
| 423 |
+
# "X-Firebase-gmpid": "1:252179682924:web:9c80c6a32cb4682cbfaa49",
|
| 424 |
+
# "Content-Type": "application/json",
|
| 425 |
+
# "User-Agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)"
|
| 426 |
+
# }
|
| 427 |
+
#
|
| 428 |
+
# # 请求体
|
| 429 |
+
# data = {
|
| 430 |
+
# "requestUri": "http://localhost",
|
| 431 |
+
# "returnSecureToken": True,
|
| 432 |
+
# "postBody": f"&id_token={settings.TOKEN}&providerId=google.com"
|
| 433 |
+
# }
|
| 434 |
+
#
|
| 435 |
+
# async with httpx.AsyncClient() as client:
|
| 436 |
+
# response = await client.post(
|
| 437 |
+
# url,
|
| 438 |
+
# params=params,
|
| 439 |
+
# headers=headers,
|
| 440 |
+
# json=data
|
| 441 |
+
# )
|
| 442 |
+
# return response.json()
|
| 443 |
+
#
|
| 444 |
+
# async def main():
|
| 445 |
+
# result = await sign_in_with_idp()
|
| 446 |
+
# print(result)
|
| 447 |
+
#
|
| 448 |
+
# if __name__ == "__main__":
|
| 449 |
+
# asyncio.run(main())
|
main.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from core.app import app
|
| 2 |
+
import uvicorn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == '__main__':
|
| 6 |
+
uvicorn.run(app, host="0.0.0.0", port=8001, workers=1, loop="asyncio")
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
httpx
|
| 3 |
+
pydantic
|
| 4 |
+
pydantic_settings
|
| 5 |
+
pyinstaller
|
| 6 |
+
python-dotenv
|
| 7 |
+
Requests
|
| 8 |
+
starlette
|
| 9 |
+
uvicorn
|
| 10 |
+
|
| 11 |
+
apscheduler
|
| 12 |
+
pytz
|
task/__init__.py
ADDED
|
File without changes
|
task/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (197 Bytes). View file
|
|
|
task/__pycache__/scheduled_tasks.cpython-311.pyc
ADDED
|
Binary file (4.91 kB). View file
|
|
|
task/scheduled_tasks.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import time
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
import httpx
|
| 8 |
+
import pytz
|
| 9 |
+
|
| 10 |
+
from core.utils import refresh_token_via_rest
|
| 11 |
+
from core.config import get_settings
|
| 12 |
+
|
| 13 |
+
# 2. 使用切片并处理长度不足的情况
|
| 14 |
+
def show_last_20(text: str) -> str:
|
| 15 |
+
return text[-20:] if len(text) > 20 else text
|
| 16 |
+
|
| 17 |
+
async def daily_task():
|
| 18 |
+
|
| 19 |
+
"""异步每日任务"""
|
| 20 |
+
current_time = datetime.now(pytz.timezone(get_settings().TIMEZONE))
|
| 21 |
+
print(f"执行每日任务: {current_time}")
|
| 22 |
+
# 定时发起聊天会话 签到
|
| 23 |
+
url = 'https://arcane.getmerlin.in/v1/thread/unified'
|
| 24 |
+
ready_refresh_tokens = os.getenv("READY_REFRESH_TOKEN")
|
| 25 |
+
if ready_refresh_tokens:
|
| 26 |
+
ready_tokens_array = ready_refresh_tokens.split(',')
|
| 27 |
+
for e in ready_tokens_array:
|
| 28 |
+
id_token = await refresh_token_via_rest(e)
|
| 29 |
+
print(f'{e} 准备签到: {id_token}')
|
| 30 |
+
|
| 31 |
+
request_headers = {**get_settings().HEADERS, 'authorization': f"Bearer {id_token}"} # 从环境变量中获取新的TOKEN
|
| 32 |
+
|
| 33 |
+
json_data = {
|
| 34 |
+
"attachments": [],
|
| 35 |
+
"chatId": str(uuid.uuid4()),
|
| 36 |
+
"language": "AUTO",
|
| 37 |
+
"message": {
|
| 38 |
+
"childId": str(uuid.uuid4()),
|
| 39 |
+
"content": '介绍二叉树',
|
| 40 |
+
"context": "",
|
| 41 |
+
"id": str(uuid.uuid4()),
|
| 42 |
+
"parentId": 'root'
|
| 43 |
+
},
|
| 44 |
+
"metadata": {
|
| 45 |
+
"largeContext": False,
|
| 46 |
+
"merlinMagic": False,
|
| 47 |
+
"proFinderMode": False,
|
| 48 |
+
"webAccess": False
|
| 49 |
+
},
|
| 50 |
+
"mode": "UNIFIED_CHAT",
|
| 51 |
+
# "model": "claude-3.5-sonnet"
|
| 52 |
+
"model": 'gpt-4o-mini'
|
| 53 |
+
}
|
| 54 |
+
# 发送 POST 请求并处理流式响应
|
| 55 |
+
with httpx.Client(timeout=httpx.Timeout(30.0)) as client:
|
| 56 |
+
try:
|
| 57 |
+
with client.stream('POST', url, headers=request_headers, json=json_data) as response:
|
| 58 |
+
if response.status_code == 200:
|
| 59 |
+
current_time = datetime.now(pytz.timezone(get_settings().TIMEZONE))
|
| 60 |
+
# print(f'{id_token}在{current_time} 签到成功')
|
| 61 |
+
print(f'{show_last_20(id_token)}在{current_time} 签到成功')
|
| 62 |
+
|
| 63 |
+
# for line in response.iter_lines():
|
| 64 |
+
# print(line)
|
| 65 |
+
# if line:
|
| 66 |
+
# # 处理每一行数据
|
| 67 |
+
# # 通常需要去掉 "data: " 前缀并解析 JSON
|
| 68 |
+
# if line and line.startswith('data: '):
|
| 69 |
+
# try:
|
| 70 |
+
# json_str = line[6:] # 去掉 "data: " 前缀
|
| 71 |
+
# if json_str.strip() == '[DONE]':
|
| 72 |
+
# break
|
| 73 |
+
# json_data = json.loads(json_str)
|
| 74 |
+
# # 处理 json_data
|
| 75 |
+
# if 'choices' in json_data and len(json_data['choices']) > 0:
|
| 76 |
+
# content = json_data['choices'][0].get('delta', {}).get('content', '')
|
| 77 |
+
# if content:
|
| 78 |
+
# print(content, end='', flush=True)
|
| 79 |
+
# except json.JSONDecodeError:
|
| 80 |
+
# continue
|
| 81 |
+
#
|
| 82 |
+
|
| 83 |
+
except httpx.HTTPStatusError as exc:
|
| 84 |
+
print(f"HTTP 错误发生: {e}")
|
| 85 |
+
raise
|
| 86 |
+
|
| 87 |
+
time.sleep(30)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
async def weekly_report_task():
|
| 91 |
+
"""异步每周报告任务"""
|
| 92 |
+
current_time = datetime.now(pytz.timezone(get_settings().TIMEZONE))
|
| 93 |
+
print(f"生成每周报告: {current_time}")
|
| 94 |
+
# 实现异步周报生成逻辑
|
| 95 |
+
|
| 96 |
+
async def data_cleanup_task():
|
| 97 |
+
"""异步数据清理任务"""
|
| 98 |
+
current_time = datetime.now(pytz.timezone(get_settings().TIMEZONE))
|
| 99 |
+
print(f"执行数据清理: {current_time}")
|
| 100 |
+
# 实现异步数据清理逻辑
|