devin15 commited on
Commit
3979178
·
verified ·
1 Parent(s): 87d0058

Upload 31 files

Browse files
.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
+ # 实现异步数据清理逻辑