This commit is contained in:
LanQian 2024-05-23 15:54:02 +08:00
parent 7f45621bce
commit f8d45dd16b
9 changed files with 47 additions and 72 deletions

View File

@ -6,4 +6,5 @@ ARKOSE_TOKEN_URL=https://arkose.example.com/token
POW_DIFFICULTY=000032
RETRY_TIMES=3
ENABLE_GATEWAY=true
CONVERSATION_ONLY=false
CONVERSATION_ONLY=false
ENABLE_LIMIT=false

View File

@ -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

View File

@ -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_idtrue为不保存且不返回
POW_DIFFICULTY=000032 // 要解决的工作量证明难度字符串越小计算时间越长建议000032
RETRY_TIMES=3 // 出错重试次数
ENABLE_GATEWAY=true // 是否启用网关模式(WEBUI)true为启用
CONVERSATION_ONLY=false // 使用的网关支持在服务端处理pow和arkose时可以开启开启则直接使用对话接口
```
## 部署

15
app.py
View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -7,4 +7,4 @@ websockets
pillow
pybase64
jinja2
APScheduler
APScheduler

View File

@ -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)