v1.1.12 add auth_key, fix bugs

This commit is contained in:
LanQian 2024-05-27 15:48:07 +08:00
parent 550068ddec
commit 69cef746ee
11 changed files with 86 additions and 61 deletions

View File

@ -37,7 +37,7 @@ jobs:
images: lanqian528/chat2api
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=v1.1.11
type=raw,value=v1.1.12
- name: Build and push
uses: docker/build-push-action@v5

View File

@ -21,7 +21,8 @@
3. 接口返回的状态码和响应体
## 功能
### 最新版 v1.1.11
### 最新版 v1.1.12
> 已完成
> - [x] 流式、非流式传输
@ -55,22 +56,22 @@
每个环境变量都有默认值,如果不懂环境变量的含义,请不要设置,更不要传空值,字符串无需引号。
| 分类 | 变量名 | 示例值 | 描述 |
|------|-------------------|-------------------------------------|--------------------------------------------------------------|
| 安全相关 | 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 | 出错重试次数,使用 AUTHORIZATION 会自动轮询下一个账号 |
| | ENABLE_GATEWAY | true | 是否启用网关模式WEBUI |
| | CONVERSATION_ONLY | false | 是否直接使用对话接口如果你用的网关支持自动解决pow和arkose才启用 |
| | ENABLE_LIMIT | true | 开启后不尝试突破官方次数限制,尽可能防止封号 |
| | UPLOAD_BY_URL | false | 开启后按照 `URL+空格+正文` 进行对话,自动解析 URL 内容并上传,多个 URL 用空格分隔 |
| | CHECK_MODEL | false | 检查账号是否支持传入模型开启后可以稍微避免4o返回3.5内容,但是会增加请求时延,且并不能解决降智问题 |
| 分类 | 变量名 | 示例值 | 默认值 | 描述 |
|------|-------------------|-------------------------------------------------------------|-----------------------|--------------------------------------------------------------|
| 安全相关 | API_PREFIX | `your_prefix` | `None` | API 前缀密码,不设置容易被人访问,设置后需请求 `/your_prefix/v1/chat/completions` |
| | AUTHORIZATION | `your_first_authorization`,<br/>`your_second_authorization` | `[]` | 你自己为使用多账号轮询 Tokens 设置的授权,英文逗号分隔 |
| | AUTH_KEY | `your_auth_key` | `None` | 私人网关需要加`auth_key`请求头才设置该项 |
| 请求相关 | CHATGPT_BASE_URL | `https://chatgpt.com` | `https://chatgpt.com` | ChatGPT 网关地址,设置后会改变请求的网站,多个网关用逗号分隔 |
| | PROXY_URL | `http://ip:port`,<br/>`http://username:password@ip:port` | `[]` | 代理 URL多个代理用逗号分隔 |
| | ARKOSE_TOKEN_URL | `https://example.com/token` | `[]` | 获取 Arkose token 的地址 |
| 功能相关 | HISTORY_DISABLED | `true` | `true` | 是否不保存聊天记录并返回 conversation_id |
| | POW_DIFFICULTY | `00003a` | `00003a` | 要解决的工作量证明难度,不懂别设置 |
| | RETRY_TIMES | `3` | `3` | 出错重试次数,使用 AUTHORIZATION 会自动轮询下一个账号 |
| | ENABLE_GATEWAY | `true` | `true` | 是否启用网关模式WEBUI |
| | CONVERSATION_ONLY | `false` | `false` | 是否直接使用对话接口如果你用的网关支持自动解决pow和arkose才启用 |
| | ENABLE_LIMIT | `true` | `true` | 开启后不尝试突破官方次数限制,尽可能防止封号 |
| | UPLOAD_BY_URL | `false` | `false` | 开启后按照 `URL+空格+正文` 进行对话,自动解析 URL 内容并上传,多个 URL 用空格分隔 |
| | CHECK_MODEL | `false` | `false` | 检查账号是否支持传入模型开启后可以稍微避免4o返回3.5内容,但是会增加请求时延,且并不能解决降智问题 |
## 部署

View File

@ -87,7 +87,8 @@ async def send_conversation(request: Request, req_token: str = Depends(oauth2_sc
@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")

View File

@ -17,7 +17,7 @@ from utils.Client import Client
from utils.Logger import logger
from utils.authorization import verify_token
from utils.config import proxy_url_list, chatgpt_base_url_list, arkose_token_url_list, history_disabled, pow_difficulty, \
conversation_only, enable_limit, limit_status_code, upload_by_url, check_model
conversation_only, enable_limit, limit_status_code, upload_by_url, check_model, auth_key
class ChatService:
@ -61,9 +61,20 @@ class ChatService:
self.history_disabled = data.get('history_disabled', history_disabled)
self.data = data
self.origin_model = self.data.get("model", "gpt-3.5-turbo-0125")
self.resp_model = model_proxy.get(self.origin_model, self.origin_model)
self.req_model = None
if "gpt-4o" in self.origin_model:
self.req_model = "gpt-4o"
elif "gpt-4-mobile" in self.origin_model:
self.req_model = "gpt-4-mobile"
elif "gpt-4-gizmo" in self.origin_model:
self.req_model = "gpt-4o"
elif "gpt-4" in self.origin_model:
self.req_model = "gpt-4"
else:
self.req_model = "text-davinci-002-render-sha"
self.api_messages = self.data.get("messages", [])
self.prompt_tokens = 0
self.max_tokens = self.data.get("max_tokens", 2147483647)
@ -99,8 +110,12 @@ class ChatService:
else:
self.base_url = self.host_url + "/backend-anon"
if auth_key:
self.base_headers['authkey'] = auth_key
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'
@ -129,17 +144,6 @@ class ChatService:
if r.status_code == 200:
resp = r.json()
if "gpt-4o" in self.origin_model:
self.req_model = "gpt-4o"
elif "gpt-4-mobile" in self.origin_model:
self.req_model = "gpt-4-mobile"
elif "gpt-4-gizmo" in self.origin_model:
self.req_model = "gpt-4o"
elif "gpt-4" in self.origin_model:
self.req_model = "gpt-4"
else:
self.req_model = "text-davinci-002-render-sha"
if check_model:
r = await self.s.get(f'{self.base_url}/models', headers=headers, timeout=5)
if r.status_code == 200:
@ -174,9 +178,11 @@ 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")
@ -285,7 +291,8 @@ 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", ""):
@ -304,7 +311,9 @@ class ChatService:
if "text/event-stream" in content_type and stream:
return stream_response(self, r.aiter_lines(), self.resp_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.resp_model, self.max_tokens), self.prompt_tokens, self.max_tokens, self.resp_model)
return await format_not_stream_response(
stream_response(self, r.aiter_lines(), self.resp_model, self.max_tokens), self.prompt_tokens,
self.max_tokens, self.resp_model)
elif "application/json" in content_type:
rtext = await r.atext()
resp = json.loads(rtext)
@ -319,7 +328,9 @@ class ChatService:
if stream and isinstance(wss_r, types.AsyncGeneratorType):
return stream_response(self, wss_r, self.resp_model, self.max_tokens)
else:
return await format_not_stream_response(stream_response(self, wss_r, self.resp_model, self.max_tokens), self.prompt_tokens, self.max_tokens, self.resp_model)
return await format_not_stream_response(
stream_response(self, wss_r, self.resp_model, self.max_tokens), self.prompt_tokens,
self.max_tokens, self.resp_model)
finally:
if not isinstance(wss_r, types.AsyncGeneratorType):
await self.ws.close()

View File

@ -248,7 +248,9 @@ async def stream_response(service, response, model, max_tokens):
def get_url_from_content(content):
if isinstance(content, str) and content.startswith('http'):
try:
url = re.match(r'(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))', content.split(' ')[0])[0]
url = re.match(
r'(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))',
content.split(' ')[0])[0]
content = content.replace(url, '').strip()
return url, content
except Exception:

View File

@ -1,6 +1,7 @@
import threading
import time
from datetime import datetime
from utils.Logger import logger
lock = threading.Lock()
@ -16,7 +17,8 @@ def check_isLimit(detail, access_token):
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):

View File

@ -9,7 +9,6 @@ from utils.Client import Client
from utils.Logger import logger
from utils.config import proxy_url_list
DATA_FOLDER = "data"
REFRESH_MAP_FILE = os.path.join(DATA_FOLDER, "refresh_map.json")
@ -29,7 +28,8 @@ def save_refresh_map(refresh_map):
async def rt2ac(refresh_token):
if refresh_token in refresh_map and int(time.time()) - refresh_map.get(refresh_token, {}).get("timestamp", 0) < 2 * 24 * 60 * 60:
if refresh_token in refresh_map and int(time.time()) - refresh_map.get(refresh_token, {}).get("timestamp",
0) < 2 * 24 * 60 * 60:
access_token = refresh_map[refresh_token]["token"]
logger.info(f"refresh_token -> access_token from cache")
return access_token

View File

@ -113,10 +113,11 @@ async def chatgpt_reverse_proxy(request: Request, path: str):
if "oai-dm=1" not in r.headers.get("Location"):
return Response(status_code=307, headers={
"Location": r.headers.get("Location").replace("chat.openai.com", origin_host)
.replace("chatgpt.com", origin_host)
.replace("https", petrol) + "?oai-dm=1"}, background=background)
.replace("chatgpt.com", origin_host)
.replace("https", petrol) + "?oai-dm=1"}, background=background)
else:
return Response(status_code=307, headers={"Location": r.headers.get("Location")}, background=background)
return Response(status_code=307, headers={"Location": r.headers.get("Location")},
background=background)
elif r.status_code == 302:
return Response(status_code=302,
headers={"Location": r.headers.get("Location").replace("chatgpt.com", origin_host)

View File

@ -20,4 +20,4 @@ async def ac2wss(access_token):
async def set_wss(access_token, wss_url):
wss_map[access_token] = {"timestamp": int(time.time()), "wss_url": wss_url}
return True
return True

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>Tokens 管理</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
@ -22,18 +22,23 @@
</script>
</head>
<body class="bg-gradient-to-r from-blue-200 via-purple-200 to-pink-200 flex justify-center items-center min-h-screen">
<div class="bg-white p-10 rounded-lg shadow-2xl w-128 text-center">
<h1 class="text-4xl font-extrabold text-gray-900 mb-6">Tokens 管理</h1>
<p class="text-gray-600 mb-6">当前可用 Tokens 数量:<span class="text-blue-600">{{ tokens_count }}</span></p>
<form id="uploadForm" method="post" class="mb-6">
<textarea name="text" rows="10" placeholder="一行一个Token可以是 AccessToken 或 RefreshToken" class="w-full p-4 mb-4 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-400 resize-none"></textarea>
<p class="text-gray-600 mb-4">使用docker时如果挂载了data文件夹则重启后不需要再次上传</p>
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-md hover:bg-blue-700 transition duration-300 mb-4">上传</button>
</form>
<form id="clearForm" method="post">
<button type="submit" class="w-full bg-red-600 text-white py-3 rounded-md hover:bg-red-700 transition duration-300">清空Tokens</button>
</form>
<p class="text-gray-600 mt-6">点击清空,将会清空所有已保存的 Tokens</p>
</div>
<div class="bg-white p-10 rounded-lg shadow-2xl w-128 text-center">
<h1 class="text-4xl font-extrabold text-gray-900 mb-6">Tokens 管理</h1>
<p class="text-gray-600 mb-6">当前可用 Tokens 数量:<span class="text-blue-600">{{ tokens_count }}</span></p>
<form class="mb-6" id="uploadForm" method="post">
<textarea class="w-full p-4 mb-4 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-400 resize-none" name="text" placeholder="一行一个Token可以是 AccessToken 或 RefreshToken"
rows="10"></textarea>
<p class="text-gray-600 mb-4">使用docker时如果挂载了data文件夹则重启后不需要再次上传</p>
<button class="w-full bg-blue-600 text-white py-3 rounded-md hover:bg-blue-700 transition duration-300 mb-4"
type="submit">上传
</button>
</form>
<form id="clearForm" method="post">
<button class="w-full bg-red-600 text-white py-3 rounded-md hover:bg-red-700 transition duration-300"
type="submit">清空Tokens
</button>
</form>
<p class="text-gray-600 mt-6">点击清空,将会清空所有已保存的 Tokens</p>
</div>
</body>
</html>

View File

@ -21,6 +21,7 @@ def is_true(x):
api_prefix = os.getenv('API_PREFIX', None)
authorization = os.getenv('AUTHORIZATION', '').replace(' ', '')
chatgpt_base_url = os.getenv('CHATGPT_BASE_URL', 'https://chatgpt.com').replace(' ', '')
auth_key = os.getenv('AUTH_KEY', None)
arkose_token_url = os.getenv('ARKOSE_TOKEN_URL', '').replace(' ', '')
proxy_url = os.getenv('PROXY_URL', '').replace(' ', '')
history_disabled = is_true(os.getenv('HISTORY_DISABLED', True))
@ -39,12 +40,13 @@ 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.11 | https://github.com/lanqian528/chat2api")
logger.info("Chat2Api v1.1.12 | https://github.com/lanqian528/chat2api")
logger.info("-" * 60)
logger.info("Environment variables:")
logger.info("API_PREFIX: " + str(api_prefix))
logger.info("AUTHORIZATION: " + str(authorization_list))
logger.info("CHATGPT_BASE_URL: " + str(chatgpt_base_url_list))
logger.info("AUTH_KEY: " + str(auth_key))
logger.info("ARKOSE_TOKEN_URL: " + str(arkose_token_url_list))
logger.info("PROXY_URL: " + str(proxy_url_list))
logger.info("HISTORY_DISABLED: " + str(history_disabled))