From f8d45dd16b8884c6ae21b75bc1bf39b7a216325f Mon Sep 17 00:00:00 2001 From: LanQian <5499636+LanQian528@users.noreply.github.com> Date: Thu, 23 May 2024 15:54:02 +0800 Subject: [PATCH] op codes --- .env.example | 3 ++- .github/workflows/build_docker.yml | 2 +- README.md | 31 ++++++++++++++---------------- app.py | 15 --------------- chat2api.py | 20 ++++++++++++------- chatgpt/ChatService.py | 20 ++++++------------- chatgpt/chatLimit.py | 21 ++++++++------------ requirements.txt | 2 +- utils/config.py | 5 ++--- 9 files changed, 47 insertions(+), 72 deletions(-) diff --git a/.env.example b/.env.example index 50a74bb..558de04 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ ARKOSE_TOKEN_URL=https://arkose.example.com/token POW_DIFFICULTY=000032 RETRY_TIMES=3 ENABLE_GATEWAY=true -CONVERSATION_ONLY=false \ No newline at end of file +CONVERSATION_ONLY=false +ENABLE_LIMIT=false \ No newline at end of file diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index 3120444..b6ed5da 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -37,7 +37,7 @@ jobs: images: lanqian528/chat2api tags: | type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=v1.1.4 + type=raw,value=v1.1.5 - name: Build and push uses: docker/build-push-action@v5 diff --git a/README.md b/README.md index a159a7a..a5ebafb 100644 --- a/README.md +++ b/README.md @@ -50,25 +50,22 @@ https://t.me/chat2api ## 环境变量 -每个环境变量都有默认值,如果不懂环境变量的含义,请不要设置,更不要传空值 +每个环境变量都有默认值,如果不懂环境变量的含义,请不要设置,更不要传空值,字符串无需引号 -``` -# 安全相关 -API_PREFIX=your_prefix // API前缀密码,不设置容易被人访问,设置后需请求 /your_prefix/v1/chat/completions -AUTHORIZATION=sk-xxxxxxxx,sk-yyyyyyyy // 先到 /tokens 上传ac或rt,请求时传入 AUTHORIZATION 可多账号轮询 +| 分类 | 变量名 | 示例值 | 描述 | +|------|-------------------|-------------------------------------|-----------------------------------------------------------| +| 安全相关 | API_PREFIX | your_prefix | API前缀密码,不设置容易被人访问,设置后需请求 /your_prefix/v1/chat/completions | +| | AUTHORIZATION | sk-xxxxxxxx, sk-yyyyyyyy | 为使用多账号轮询 Tokens 设置的授权,英文逗号分隔 | +| 请求相关 | CHATGPT_BASE_URL | https://chatgpt.com | ChatGPT网关地址,设置后会改变请求的网站,多个网关用逗号分隔 | +| | PROXY_URL | your_first_proxy, your_second_proxy | 代理URL,多个代理用逗号分隔 | +| | ARKOSE_TOKEN_URL | https://arkose.example.com/token | 获取Arkose token的地址 | +| 功能相关 | HISTORY_DISABLED | true | 是否不保存聊天记录并返回 conversation_id | +| | POW_DIFFICULTY | 00003a | 要解决的工作量证明难度 | +| | RETRY_TIMES | 3 | 出错重试次数 | +| | ENABLE_GATEWAY | true | 是否启用网关模式(WEBUI) | +| | CONVERSATION_ONLY | false | 是否直接使用对话接口 | +| | ENABLE_LIMIT | true | 开启后不尝试突破官方次数限制,尽可能防止封号 | -# 请求相关 -CHATGPT_BASE_URL=https://chatgpt.com // ChatGPT网关地址,设置后会改变请求的网站,多个网关用逗号分隔 -PROXY_URL=your_first_proxy, your_second_proxy // 代理url,多个代理用逗号分隔 -ARKOSE_TOKEN_URL=https://arkose.example.com/token // 获取Arkose token的地址,上文有提供说明 - -# 功能相关 -HISTORY_DISABLED=true // 是否不保存聊天记录并返回 conversation_id,true为不保存且不返回 -POW_DIFFICULTY=000032 // 要解决的工作量证明难度,字符串越小,计算时间越长,建议000032 -RETRY_TIMES=3 // 出错重试次数 -ENABLE_GATEWAY=true // 是否启用网关模式(WEBUI),true为启用 -CONVERSATION_ONLY=false // 使用的网关支持在服务端处理pow和arkose时可以开启,开启则直接使用对话接口 -``` ## 部署 diff --git a/app.py b/app.py index 8f082a1..ce1d13e 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,4 @@ import uvicorn -from apscheduler.schedulers.background import BackgroundScheduler - -from chat2api import app -from chatgpt.chatLimit import clean_dict log_config = uvicorn.config.LOGGING_CONFIG default_format = "%(asctime)s | %(levelname)s | %(message)s" @@ -10,15 +6,4 @@ access_format = r'%(asctime)s | %(levelname)s | %(client_addr)s: %(request_line) log_config["formatters"]["default"]["fmt"] = default_format log_config["formatters"]["access"]["fmt"] = access_format -scheduler = BackgroundScheduler() - - -# 用于自动更新限制access_token的字典 -# 避免过多access_token没有销毁,而堆积 -@app.on_event("startup") -async def app_start(): - scheduler.add_job(id='updateLimit_run', func=clean_dict, trigger='cron', hour=3, minute=0) - scheduler.start() - - uvicorn.run("chat2api:app", host="0.0.0.0", port=5005) diff --git a/chat2api.py b/chat2api.py index 808a2db..d509b1b 100644 --- a/chat2api.py +++ b/chat2api.py @@ -1,6 +1,7 @@ import types import warnings +from apscheduler.schedulers.background import BackgroundScheduler from fastapi import FastAPI, Request, Depends, HTTPException, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse @@ -9,7 +10,7 @@ from fastapi.templating import Jinja2Templates from starlette.background import BackgroundTask from chatgpt.ChatService import ChatService -from chatgpt.chatLimit import handle_request_limit +from chatgpt.chatLimit import handle_request_limit, clean_dict from chatgpt.reverseProxy import chatgpt_reverse_proxy from utils.Logger import logger from utils.authorization import verify_token, token_list @@ -21,7 +22,7 @@ warnings.filterwarnings("ignore") app = FastAPI() templates = Jinja2Templates(directory="templates") - +scheduler = BackgroundScheduler() app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -31,9 +32,15 @@ app.add_middleware( ) +@app.on_event("startup") +async def app_start(): + scheduler.add_job(id='updateLimit_run', func=clean_dict, trigger='cron', hour=3, minute=0) + scheduler.start() + + async def to_send_conversation(request_data, access_token): + chat_service = ChatService(access_token) try: - chat_service = ChatService(access_token) await chat_service.set_dynamic_data(request_data) await chat_service.get_chat_requirements() return chat_service @@ -80,12 +87,11 @@ async def send_conversation(request: Request, token=Depends(verify_token)): @app.get(f"/{api_prefix}/tokens" if api_prefix else "/tokens", response_class=HTMLResponse) async def upload_html(request: Request): tokens_count = len(token_list) - return templates.TemplateResponse("tokens.html", - {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) + return templates.TemplateResponse("tokens.html", {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) @app.post(f"/{api_prefix}/tokens/upload" if api_prefix else "/tokens/upload") -async def upload_post(request: Request, text: str = Form(...)): +async def upload_post(text: str = Form(...)): lines = text.split("\n") for line in lines: if line.strip() and not line.startswith("#"): @@ -98,7 +104,7 @@ async def upload_post(request: Request, text: str = Form(...)): @app.post(f"/{api_prefix}/tokens/clear" if api_prefix else "/tokens/clear") -async def upload_post(request: Request): +async def upload_post(): token_list.clear() with open("data/token.txt", "w", encoding="utf-8") as f: pass diff --git a/chatgpt/ChatService.py b/chatgpt/ChatService.py index 51f2711..34bd43c 100644 --- a/chatgpt/ChatService.py +++ b/chatgpt/ChatService.py @@ -80,8 +80,7 @@ class ChatService: self.base_url = self.host_url + "/backend-anon" await get_dpl(self) - self.s.session.cookies.set("__Secure-next-auth.callback-url", "https%3A%2F%2Fchatgpt.com;", - domain=self.host_url.split("://")[1], secure=True) + self.s.session.cookies.set("__Secure-next-auth.callback-url", "https%3A%2F%2Fchatgpt.com;", domain=self.host_url.split("://")[1], secure=True) async def get_wss_url(self): url = f'{self.base_url}/register-websocket' @@ -126,11 +125,9 @@ class ChatService: if proofofwork_required: proofofwork_diff = proofofwork.get("difficulty") if proofofwork_diff <= pow_difficulty: - raise HTTPException(status_code=403, - detail=f"Proof of work difficulty too high: {proofofwork_diff}") + raise HTTPException(status_code=403, detail=f"Proof of work difficulty too high: {proofofwork_diff}") proofofwork_seed = proofofwork.get("seed") - self.proof_token, solved = await run_in_threadpool(get_answer_token, proofofwork_seed, - proofofwork_diff, config) + self.proof_token, solved = await run_in_threadpool(get_answer_token, proofofwork_seed, proofofwork_diff, config) if not solved: raise HTTPException(status_code=403, detail="Failed to solve proof of work") @@ -246,8 +243,7 @@ class ChatService: raise HTTPException(status_code=e.status_code, detail=str(e)) url = f'{self.base_url}/conversation' stream = self.data.get("stream", False) - r = await self.s.post_stream(url, headers=self.chat_headers, json=self.chat_request, timeout=10, - stream=True) + r = await self.s.post_stream(url, headers=self.chat_headers, json=self.chat_request, timeout=10, stream=True) if r.status_code != 200: rtext = await r.atext() if "application/json" == r.headers.get("Content-Type", ""): @@ -266,9 +262,7 @@ class ChatService: if "text/event-stream" in content_type and stream: return stream_response(self, r.aiter_lines(), self.target_model, self.max_tokens) elif "text/event-stream" in content_type and not stream: - return await format_not_stream_response( - stream_response(self, r.aiter_lines(), self.target_model, self.max_tokens), self.prompt_tokens, - self.max_tokens, self.target_model) + return await format_not_stream_response(stream_response(self, r.aiter_lines(), self.target_model, self.max_tokens), self.prompt_tokens, self.max_tokens, self.target_model) elif "application/json" in content_type: rtext = await r.atext() resp = json.loads(rtext) @@ -283,9 +277,7 @@ class ChatService: if stream and isinstance(wss_r, types.AsyncGeneratorType): return stream_response(self, wss_r, self.target_model, self.max_tokens) else: - return await format_not_stream_response( - stream_response(self, wss_r, self.target_model, self.max_tokens), self.prompt_tokens, - self.max_tokens, self.target_model) + return await format_not_stream_response(stream_response(self, wss_r, self.target_model, self.max_tokens), self.prompt_tokens, self.max_tokens, self.target_model) finally: if not isinstance(wss_r, types.AsyncGeneratorType): await self.ws.close() diff --git a/chatgpt/chatLimit.py b/chatgpt/chatLimit.py index f5995ed..06de916 100644 --- a/chatgpt/chatLimit.py +++ b/chatgpt/chatLimit.py @@ -3,23 +3,20 @@ import time from datetime import datetime from utils.Logger import logger -# 开启线程锁 lock = threading.Lock() -# 对于密钥的限制 limit_access_token = {} def check_isLimit(detail, access_token): if detail.get('clears_in'): - clearTime = time.time() + detail.get('clears_in') - initial_access_list(access_token, clearTime) + clear_time = time.time() + detail.get('clears_in') + initial_access_list(access_token, clear_time) def initial_access_list(key, clear_time): with lock: limit_access_token[key] = clear_time - logger.info( - f"{key[:40]}: Reached 429 limit, will be cleared at {datetime.fromtimestamp(clear_time).replace(second=0, microsecond=0)}") + logger.info(f"{key[:40]}: Reached 429 limit, will be cleared at {datetime.fromtimestamp(clear_time).replace(second=0, microsecond=0)}") def remove_refresh_list(key): @@ -40,20 +37,18 @@ async def handle_request_limit(request_data, access_token): remove_refresh_list(access_token) else: clear_date = datetime.fromtimestamp(limit_time).replace(second=0, microsecond=0) - logger.info(f"Request limit exceeded. " - f"You can continue with the default model now, " - f"or try again after {clear_date}") - return f"Request limit exceeded. You can continue with the default model now, or try again after {clear_date}" + result = f"Request limit exceeded. You can continue with the default model now, or try again after {clear_date}" + logger.info(result) + return result return None except Exception as e: logger.error(e) return None -# 清理函数,用于删除长时间未使用的键值对 def clean_dict(): - logger.info(f"==========================================") - logger.info("开始执行清理过期的limit_access_token......") + logger.info("-" * 50) + logger.info("Start to clean limit_access_token......") current_time = time.time() keys_to_remove = [key for key, clear_time in limit_access_token.items() if clear_time < current_time] for key in keys_to_remove: diff --git a/requirements.txt b/requirements.txt index c77f816..cfcc552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ websockets pillow pybase64 jinja2 -APScheduler +APScheduler \ No newline at end of file diff --git a/utils/config.py b/utils/config.py index cdfeef7..bd26b63 100644 --- a/utils/config.py +++ b/utils/config.py @@ -28,7 +28,7 @@ pow_difficulty = os.getenv('POW_DIFFICULTY', '000032') retry_times = int(os.getenv('RETRY_TIMES', 3)) enable_gateway = is_true(os.getenv('ENABLE_GATEWAY', True)) conversation_only = is_true(os.getenv('CONVERSATION_ONLY', False)) -enable_limit = is_true(os.getenv('ENABLE_LIMIT', False)) +enable_limit = is_true(os.getenv('ENABLE_LIMIT', True)) limit_status_code = os.getenv('LIMIT_STATUS_CODE', 429) authorization_list = authorization.split(',') if authorization else [] @@ -37,7 +37,7 @@ arkose_token_url_list = arkose_token_url.split(',') if arkose_token_url else [] proxy_url_list = proxy_url.split(',') if proxy_url else [] logger.info("-" * 60) -logger.info("Chat2Api v1.1.4 | https://github.com/lanqian528/chat2api") +logger.info("Chat2Api v1.1.5 | https://github.com/lanqian528/chat2api") logger.info("-" * 60) logger.info("Environment variables:") logger.info("API_PREFIX: " + str(api_prefix)) @@ -51,5 +51,4 @@ logger.info("RETRY_TIMES: " + str(retry_times)) logger.info("ENABLE_GATEWAY: " + str(enable_gateway)) logger.info("CONVERSATION_ONLY: " + str(conversation_only)) logger.info("ENABLE_LIMIT: " + str(enable_limit)) -logger.info("LIMIT_STATUS_CODE: " + str(limit_status_code)) logger.info("-" * 60)