mirror of
https://github.com/lanqian528/chat2api.git
synced 2026-06-13 21:02:46 +08:00
v1.1.12 add auth_key, fix bugs
This commit is contained in:
parent
550068ddec
commit
69cef746ee
2
.github/workflows/build_docker.yml
vendored
2
.github/workflows/build_docker.yml
vendored
@ -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
|
||||
|
||||
35
README.md
35
README.md
@ -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内容,但是会增加请求时延,且并不能解决降智问题 |
|
||||
|
||||
## 部署
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user