Skip to content

Chn student #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
handle_path /api/* {
reverse_proxy localhost:8000
}

handle_path /cdn/* {
rewrite * /api/arkose/p/cdn{path}
reverse_proxy localhost:8000
}

handle_path /fc/* {
rewrite * /api/arkose/p/fc{path}
reverse_proxy localhost:8000
}

handle /* {
file_server
root * /app/dist
Expand Down
120 changes: 101 additions & 19 deletions backend/api/routers/arkose.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from urllib.parse import urlparse
import re
from urllib.parse import parse_qs, urlencode, urlparse

import httpx
import json
from fastapi import APIRouter, Depends, Response, Request

from api.conf import Config
from api.conf import Config, Credentials
from api.exceptions import ResourceNotFoundException, ArkoseForwardException, InvalidRequestException
from api.models.db import User
from api.response import handle_arkose_forward_exception
Expand All @@ -14,6 +15,9 @@
config = Config()
router = APIRouter()
openai_web_manager = OpenaiWebChatManager()
credentials = Credentials()
blob_data = None
blob_object = None


def extract_origin(referer):
Expand All @@ -32,13 +36,26 @@ def extract_origin(referer):


def modify_challenge_url_cdn(content: bytes):
global blob_data
global blob_object
try:
data = json.loads(content)
# 检查是否同时存在object和data字段
if "object" in data and "data" in data:
blob_data = None
blob_data = data["data"] # 提取data字段的值
blob_object = None
blob_object = data["object"]
if "challenge_url_cdn" in data:
data["challenge_url_cdn"] = "/api/arkose/p" + data["challenge_url_cdn"]
return json.dumps(data).encode()
modified_content = json.dumps(data).encode() # 更新content
else:
modified_content = content # 保持content不变
except json.JSONDecodeError:
return content
modified_content = content # 如果解析失败,保持content不变

# 返回修改后的content和提取的data值
return modified_content


def modify_fc_gt2_url(content: bytes):
Expand All @@ -52,34 +69,59 @@ def modify_fc_gt2_url(content: bytes):
return content


async def forward_arkose_request(request: Request, path: str, _user: User = Depends(current_active_user)):
async def forward_arkose_request(request: Request, path: str):
"""
TODO:/fc/a/?callback=
将 /arkose/p/ 和 /api/arkose/p/ 请求转发至 ninja
"""
global blob_data
global blob_object
method = request.method
headers = {
"accept": request.headers.get("accept"),
"content-type": request.headers.get("content-type"),
"user-agent": request.headers.get("user-agent"),
}
# 复制原请求headers,排除掉一些不应该透传的headers
headers = {key: value for key, value in request.headers.items() if key.lower() not in ['host', 'content-length']}

referer = request.headers.get("referer")
origin = request.headers.get("origin")

if referer and "/arkose/p/" in referer:
referer_path = referer.split("/arkose/p/", maxsplit=1)[1]
referer = f"{config.openai_web.arkose_endpoint_base}{referer_path}"
origin = extract_origin(referer)
# if referer and "/arkose/p/" in referer:
# referer_path = referer.split("/arkose/p/", maxsplit=1)[1]
# referer = f"{config.openai_web.arkose_endpoint_base}{referer_path}"
# origin = extract_origin(referer)
if not referer:
referer = f"{config.openai_web.arkose_endpoint_base}"

# 检查是否有cookie,如果有,则处理并添加到headers中
cookie = request.headers.get("cookie")
if cookie:
# 将cookie字符串分解为一个cookie字典
cookies = dict(item.split("=", 1) for item in cookie.split("; "))
# 移除名为 cws_user_auth 的cookie
cookies.pop("cws_user_auth", None)
# 重新构建cookie字符串
modified_cookie = "; ".join([f"{key}={value}" for key, value in cookies.items()])
# 如果修改后的cookie字符串不为空,则添加到headers中
if modified_cookie:
headers["cookie"] = modified_cookie

headers["referer"] = referer
if origin:
headers["origin"] = origin

headers = {k: v for k, v in headers.items() if v is not None}
data_bytes = await request.body()
modified_data_bytes = data_bytes
if re.match(r'fc/gt2/public_key/.*', path) and blob_data is not None:
# 解析原始请求体
body_data = parse_qs(data_bytes.decode('utf-8'))
# 在原始数据中添加data值
body_data['data[blob]'] = blob_data.encode('utf-8')
# 将修改后的数据编码回x-www-form-urlencoded格式
modified_data_bytes = urlencode(body_data, doseq=True).encode('utf-8')
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
blob_data = None
blob_object = None
try:
request_to_send = httpx.Request(method, f"{config.openai_web.arkose_endpoint_base}{path}", headers=headers,
content=data_bytes, params=dict(request.query_params))
content=modified_data_bytes, params=dict(request.query_params))
async with httpx.AsyncClient() as client:
resp = await client.send(request_to_send)
resp.raise_for_status()
Expand All @@ -92,20 +134,60 @@ async def forward_arkose_request(request: Request, path: str, _user: User = Depe
content = resp.content
if resp_content_type and resp_content_type == "application/json":
content = modify_challenge_url_cdn(resp.content)
# 处理 /fc/a/?callback=, /fc/a/?callback 的 jsonp 不需要包装, 下面这部分就不要了
# elif resp_content_type and resp_content_type == "application/javascript":
# content = modify_fc_gt2_url(resp.content)
# callback_name = request.query_params.get('callback')
# if callback_name:
# content = f'{callback_name}({content.decode("utf-8")});'.encode('utf-8')
# 设置正确的内容类型
# headers['Content-Type'] = 'application/javascript'
# 这部分应该不需要了,由前端加载 fc_gc2_url,不需要重写
# content = modify_fc_gt2_url(resp.content)
return Response(content=content, headers=headers, status_code=200)
except httpx.HTTPStatusError as e:
e = ArkoseForwardException(code=e.response.status_code, message=e.response.text)
return handle_arkose_forward_exception(e)


router.add_api_route("/arkose/p/{path:path}", forward_arkose_request, methods=["GET", "POST"])
# 一些资源需要加载不然404
router.add_api_route("/api/arkose/p/{path:path}", forward_arkose_request, methods=["GET", "POST"])


@router.get("/arkose/info", tags=["arkose"])
async def get_arkose_info(_user: User = Depends(current_active_user)):
async def get_arkose_info(request: Request, _user: User = Depends(current_active_user)):
global blob_data
global blob_object
# 复制原请求headers,排除掉一些不应该透传的headers
headers = {key: value for key, value in request.headers.items() if key.lower() not in ['host', 'content-length']}
referer = request.headers.get("referer")
origin = request.headers.get("origin")

headers["referer"] = referer
if origin:
headers["origin"] = origin
headers["Authorization"] = f"Bearer {credentials.openai_web_access_token}"

# 删除不必要的头部信息
headers.pop('accept-encoding', None)
headers.pop('content-length', None)

headers = {k: v for k, v in headers.items() if v is not None}

async with httpx.AsyncClient() as client:
response = await client.post(f"{config.openai_web.arkose_endpoint_base}backend-api/sentinel/arkose/dx", headers=headers)
# print(response.json())
# 检查请求是否成功
if response.status_code == 200:
response_data = response.json()
# 尝试获取data和object,如果不存在则保持为空字符串
blob_data = response_data.get("data", "")
blob_object = response_data.get("object", "")

# 返回包含data和object的信息
return {
"enabled": config.openai_web.enable_arkose_endpoint,
"url": "/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js"
"url": "/arkose/p/v2/35536E1E-65B4-4D96-9D97-6ADB7EFF8147/api.js",
"data": blob_data,
"object": blob_object
}
16 changes: 16 additions & 0 deletions frontend/src/api/arkose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,19 @@ import ApiUrl from './url';
export function getArkoseInfo() {
return axios.get<{ enabled: boolean, url: string }>(ApiUrl.ArkoseInfo);
}
export function getCurrentUrlWithApiPath(): string {
// 获取当前URL的组成部分
const protocol = window.location.protocol; // 协议 (例如, 'http:' 或 'https:')
const hostname = window.location.hostname; // 主机名
const port = window.location.port; // 端口号
const pathname = "/api"; // 设定的API路径

// 判断是否需要包含端口号
let url = `${protocol}//${hostname}`;
if (port) {
url += `:${port}`;
}
url += pathname;

return url;
}
2 changes: 1 addition & 1 deletion frontend/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@
"file_upload_strategy": "配置使用 code interpreter 模型时文件如何上传,详见文档",
"max_completion_concurrency": "决定同时能有多少用户进行对话。如果设置为一个很大的值,相当于禁用排队对话功能。",
"enable_arkose_endpoint": "实验性功能,启用前请仔细阅读文档说明。启用后务必填写下方的 arkose_endpoint_base。如果启用,将会在前端取得 Arkose token,用户可能需要进行手动验证。",
"arkose_endpoint_base": "Arkose 代理的地址前缀,必须以 /v2/ 结尾,如:http://ninja/v2/"
"arkose_endpoint_base": "Arkose 代理的地址前缀,必须以 / 结尾,如:http://ninja/"
}
},
"dialog": {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/views/admin/pages/config_manager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import { NDynamicTags } from 'naive-ui';
import { computed, h, ref } from 'vue';
import { useI18n } from 'vue-i18n';

import { getArkoseInfo } from '@/api/arkose';
import { getArkoseInfo, getCurrentUrlWithApiPath } from '@/api/arkose';
import {
getSystemConfig,
getSystemCredentials,
Expand Down Expand Up @@ -286,6 +286,7 @@ const checkChatgptAccount = () => {
const testArkose = async () => {
const { data } = await getArkoseInfo();
const { enabled, url } = data;
const baseUrl = getCurrentUrlWithApiPath();

if (!enabled) {
Message.error('Please set enable_arkose_endpoint to true in the config');
Expand All @@ -295,7 +296,8 @@ const testArkose = async () => {
testArkoseLoading.value = true;

try {
const arkoseToken = await getArkoseToken(url);
const arkose_endpoint_url = baseUrl + url;
const arkoseToken = await getArkoseToken(arkose_endpoint_url);
Message.success(t('tips.success') + ': ' + arkoseToken);
console.log('Get arkose token', arkoseToken);
} catch (err: any) {
Expand Down
40 changes: 23 additions & 17 deletions frontend/src/views/conversation/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ import { NButton, NIcon, useThemeVars } from 'naive-ui';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';

import { getArkoseInfo } from '@/api/arkose';
import { getArkoseInfo,getCurrentUrlWithApiPath } from '@/api/arkose';
import { getAskWebsocketApiUrl } from '@/api/chat';
import { generateConversationTitleApi } from '@/api/conv';
import { useAppStore, useConversationStore, useFileStore, useUserStore } from '@/store';
Expand Down Expand Up @@ -289,9 +289,6 @@ const sendMsg = async () => {
isAborted.value = false;
let hasGotReply = false;

// 唤起 arkose
const { data: arkoseInfo } = await getArkoseInfo();

// 处理附件
let attachments = null as OpenaiWebChatMessageMetadataAttachment[] | null;
if (uploadMode.value !== null && fileStore.uploadedFileInfos.length > 0) {
Expand Down Expand Up @@ -354,22 +351,31 @@ const sendMsg = async () => {
];
}

// 获取 Arkose 相关信息
// const { data: arkoseInfo } = await getArkoseInfo();
const baseUrl = getCurrentUrlWithApiPath();

let arkoseToken = null as string | null;
if (arkoseInfo.enabled) {
const url = arkoseInfo.url;
try {
arkoseToken = await getArkoseToken(url);
console.log('Get arkose token', arkoseToken);
} catch (err: any) {
console.error('Failed to get Arkose token', err);
Dialog.error({
title: t('errors.arkoseError'),
content: t('errors.arkoseTokenError'),
});
return;

// 判断是否满足特定条件, TODO: 3.5 API 模型 418 错误需要尝试带 arkose token
if (currentConversation.value!.source! === 'openai_web' && currentConversation.value!.current_model! === 'gpt_4') {
const { data: arkoseInfo } = await getArkoseInfo(); // 异步获取arkoseInfo
if (arkoseInfo && arkoseInfo.enabled) {
const url = baseUrl + arkoseInfo.url;
try {
arkoseToken = await getArkoseToken(url);
console.log('Get arkose token', arkoseToken);
// 使用arkoseToken进行接下来的操作...
} catch (err: any) {
console.error('Failed to get Arkose token', err);
Dialog.error({
title: t('errors.arkoseError'),
content: t('errors.arkoseTokenError'),
});
return;
}
}
}

const askRequest: AskRequest = {
new_conversation: isCurrentNewConversation.value,
source: currentConversation.value!.source,
Expand Down