diff --git a/.dockerignore b/.dockerignore index 35879fc..1706cd7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,5 @@ /.git/ /.idea/ /docs/ -/tmp/ \ No newline at end of file +/tmp/ +/data/ \ No newline at end of file diff --git a/.github/workflows/build_docker.yml b/.github/workflows/build_docker.yml index e4ba9d4..a9e190b 100644 --- a/.github/workflows/build_docker.yml +++ b/.github/workflows/build_docker.yml @@ -35,7 +35,7 @@ jobs: images: lanqian528/chat2api tags: | type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=v1.1.0 + type=raw,value=v1.1.2 - name: Build and push uses: docker/build-push-action@v5 diff --git a/.gitignore b/.gitignore index 60092de..d8f1ffe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.pyc /.git/ /.idea/ -/tmp/ \ No newline at end of file +/tmp/ +/data/ \ No newline at end of file diff --git a/README.md b/README.md index 122d68e..0ec957a 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ curl --location 'http://127.0.0.1:5005/v1/chat/completions' \ ``` # 安全相关 API_PREFIX=your_prefix // API前缀密码,不设置容易被人访问,设置后需请求 /your_prefix/v1/chat/completions -AUTHORIZATION=sk-xxxxxxxx,sk-yyyyyyyy // 填写data文件夹下的token.txt后,请求时传入AUTHORIZATION可多账号轮询 +AUTHORIZATION=sk-xxxxxxxx,sk-yyyyyyyy // 先到 /tokens 上传ac或rt,请求时传入 AUTHORIZATION 可多账号轮询 # 请求相关 CHATGPT_BASE_URL=https://chatgpt.com // ChatGPT网关地址,设置后会改变请求的网站,多个网关用逗号分隔 diff --git a/chat2api.py b/chat2api.py index 7d4aaae..e7c5e7f 100644 --- a/chat2api.py +++ b/chat2api.py @@ -1,23 +1,24 @@ -import os import types +import warnings -from fastapi import FastAPI, Request, Depends, HTTPException +from fastapi import FastAPI, Request, Depends, HTTPException, Form from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse from starlette.background import BackgroundTask from chatgpt.ChatService import ChatService from chatgpt.reverseProxy import chatgpt_reverse_proxy from utils.Logger import logger -from utils.authorization import verify_token +from utils.authorization import verify_token, token_list from utils.config import api_prefix from utils.retry import async_retry -import warnings - warnings.filterwarnings("ignore") app = FastAPI() +templates = Jinja2Templates(directory="templates") app.add_middleware( CORSMiddleware, @@ -65,6 +66,35 @@ async def send_conversation(request: Request, token=Depends(verify_token)): raise HTTPException(status_code=500, detail="Server error") +@app.get(f"/{api_prefix}/tokens" if api_prefix else "/upload", 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}) + + +@app.post(f"/{api_prefix}/tokens/upload" if api_prefix else "/tokens/upload") +async def upload_post(request: Request, text: str = Form(...)): + lines = text.split("\n") + for line in lines: + if line.strip() and not line.startswith("#"): + token_list.append(line.strip()) + with open("data/token.txt", "a", encoding="utf-8") as f: + f.write(line.strip() + "\n") + logger.info(f"Token list count: {len(token_list)}") + tokens_count = len(token_list) + return templates.TemplateResponse("tokens.html", {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) + + +@app.post(f"/{api_prefix}/tokens/clear" if api_prefix else "/tokens/clear") +async def upload_post(request: Request): + token_list.clear() + with open("data/token.txt", "w", encoding="utf-8") as f: + pass + logger.info(f"Token list count: {len(token_list)}") + tokens_count = len(token_list) + return templates.TemplateResponse("tokens.html", {"request": request, "api_prefix": api_prefix, "tokens_count": tokens_count}) + + @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]) async def reverse_proxy(request: Request, path: str): - return await chatgpt_reverse_proxy(request, path) \ No newline at end of file + return await chatgpt_reverse_proxy(request, path) diff --git a/chatgpt/refreshToken.py b/chatgpt/refreshToken.py index c257b74..0d2dc50 100644 --- a/chatgpt/refreshToken.py +++ b/chatgpt/refreshToken.py @@ -1,5 +1,8 @@ +import json +import os import random import time +from functools import cache from fastapi import HTTPException @@ -7,11 +10,30 @@ from utils.Client import Client from utils.Logger import logger from utils.config import proxy_url_list -refresh_map = {} -fake_map = {} + +DATA_FOLDER = "data" +REFRESH_MAP_FILE = os.path.join(DATA_FOLDER, "refresh_map.json") + +if not os.path.exists(DATA_FOLDER): + os.makedirs(DATA_FOLDER) + + +@cache +def load_refresh_map(): + if os.path.exists(REFRESH_MAP_FILE): + with open(REFRESH_MAP_FILE, "r") as file: + return json.load(file) + else: + return {} + + +def save_refresh_map(refresh_map): + with open(REFRESH_MAP_FILE, "w") as file: + json.dump(refresh_map, file) async def rt2ac(refresh_token): + refresh_map = load_refresh_map() if refresh_token in refresh_map and int(time.time()) - refresh_map.get(refresh_token, {}).get("timestamp", 0) < 24 * 60 * 60: access_token = refresh_map[refresh_token]["token"] logger.info(f"refresh_token -> access_token from cache") @@ -19,6 +41,7 @@ async def rt2ac(refresh_token): else: access_token = await chat_refresh(refresh_token) refresh_map[refresh_token] = {"token": access_token, "timestamp": int(time.time())} + save_refresh_map(refresh_map) logger.info(f"refresh_token -> access_token with openai: {access_token}") return access_token diff --git a/docker-compose.yml b/docker-compose.yml index 3690d48..be97d76 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: ports: - '5005:5005' volumes: - - ./data:/app/data # 挂载tokens的目录 + - ./data:/app/data # 挂载一些需要保存的数据 environment: - ARKOSE_TOKEN_URL=http://arkose:5006/token diff --git a/requirements.txt b/requirements.txt index a8957b4..d1b1ff6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ tiktoken python-dotenv websockets pillow -pybase64 \ No newline at end of file +pybase64 +jinja2 \ No newline at end of file diff --git a/templates/tokens.html b/templates/tokens.html new file mode 100644 index 0000000..e408520 --- /dev/null +++ b/templates/tokens.html @@ -0,0 +1,39 @@ + + + + + + Tokens 管理 + + + + +
+

Tokens 管理

+

当前可用 Tokens 数量:{{ tokens_count }}

+
+ +

注:使用docker时如果挂载了data文件夹则重启后不需要再次上传

+ +
+
+ +
+

点击清空,将会清空所有已保存的 Tokens

+
+ + \ No newline at end of file diff --git a/utils/authorization.py b/utils/authorization.py index bf1a077..f49de5f 100644 --- a/utils/authorization.py +++ b/utils/authorization.py @@ -12,6 +12,11 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) count = 0 token_list = [] +DATA_FOLDER = "data" +TOKENS_FILE = os.path.join(DATA_FOLDER, "token.txt") +if not os.path.exists(DATA_FOLDER): + os.makedirs(DATA_FOLDER) + if os.path.exists("data/token.txt"): with open("data/token.txt", "r", encoding="utf-8") as f: for line in f: diff --git a/utils/config.py b/utils/config.py index 05a3fd3..cdb9f90 100644 --- a/utils/config.py +++ b/utils/config.py @@ -35,7 +35,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.0 | https://github.com/lanqian528/chat2api") +logger.info("Chat2Api v1.1.2 | https://github.com/lanqian528/chat2api") logger.info("-" * 60) logger.info("Environment variables:") logger.info("API_PREFIX: " + str(api_prefix))