主要针对上一篇文章中的接口做优化。
把里面用到https://chat-shared.zhile.io/shared.html?v=2
的地址改成https://chat.openai.com
就好。
目前已知官网对聊天接口增加了哪些参数的前置请求呢
- https://chat.openai.com/api/auth/session (拿access_token)
- https://chat.openai.com/backend-api/sentinel/chat-requirements (用来拿turnstile和聊天arkose的blob),也可以判断是否需要账号是否需要打码
- https://chat.openai.com/backend-api/accounts/check/v4-2023-04-27 (用来拿_account) plus或team需要
- https://chat.openai.com/backend-api/register-websocket (获取账号聊天的wss连接)
下面主要针对3.5的聊天开始。
首先针对https://chat.openai.com/backend-api/conversation
接口来说。
这是以前的
这是现在的
区别很明显,以前是直接返回text/event-stream,现在是返回application/json,加多了一个websocket。
现在要对之前实现的接口进行改造。
已知要改造的是添加一个websocket连接,和兼容旧的text/event-stream的返回响应
需要用到一个ws库
npm install ws
npm install @types/ws --save-dev
创建一个测试websocket的脚本 test/websocket.ts
import Websocket from "ws";
import {Transform} from 'stream';
const ws = new Websocket("wss://chatgpt-async-webps-prod-southcentralus-27.webpubsub.azure.com/client/hubs/conversations?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL2NoYXRncHQtYXN5bmMtd2VicHMtcHJvZC1zb3V0aGNlbnRyYWx1cy0yNy53ZWJwdWJzdWIuYXp1cmUuY29tL2NsaWVudC9odWJzL2NvbnZlcnNhdGlvbnMiLCJpYXQiOjE3MTA2MTgzMjYsImV4cCI6MTcxMDYyMTkyNiwic3ViIjoidXNlci1mUWN2RE13aXJibGNoUzVNWUJHbjhiZnEiLCJyb2xlIjpbIndlYnB1YnN1Yi5qb2luTGVhdmVHcm91cC51c2VyLWZRY3ZETXdpcmJsY2hTNU1ZQkduOGJmcSJdLCJ3ZWJwdWJzdWIuZ3JvdXAiOlsidXNlci1mUWN2RE13aXJibGNoUzVNWUJHbjhiZnEiXX0.zfZYKCzl_T9xFL5x5Vor3TTcLVhwCFXLSxTcLnqakWs", ["json.reliable.webpubsub.azure.v1"], {
perMessageDeflate: false,
headers: {
"Sec-Websocket-Protocol": "json.reliable.webpubsub.azure.v1",
"Origin": "https://chat.openai.com",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
},
});
//{"type":"system","event":"connected","userId":"user-fQcvDMwirblchS5MYBGn8bfq","connectionId":"TTBs9KzqVcmU6M9atKu5JAGNm7ZQD02","reconnectionToken":"eyJhbGciOiJIUzI1NiIsImtpZCI6IjM1NjAyNzgxIiwidHlwIjoiSldUIn0.eyJuYmYiOjE3MTA2MTg5OTMsImV4cCI6MTcxMTIyMzc5MywiaWF0IjoxNzEwNjE4OTkzLCJhdWQiOiJUVEJzOUt6cVZjbVU2TTlhdEt1NUpBR05tN1pRRDAyIn0.6cVpnpXB0Sks2distK2XPVWJUJzS0zZlPRilig_6iZE"}
interface ISystem {
type: string
event: string
userId: string
connectionId: string
reconnectionToken: string
}
// {"sequenceId":30,"type":"message","from":"group","fromUserId":"server_e276f690-01e5-440d-b13f-abf98e7564c0","group":"user-fQcvDMwirblchS5MYBGn8bfq","dataType":"json","data": {"type": "http.response.body", "body": "ZGF0YTogW0RPTkVdCgo=", "more_body": true, "response_id": "865742c788df3158-ICN", "websocket_request_id": "b22baed5-fae0-40b5-b37e-75882849eb5b", "conversation_id": "af15fef2-1b0f-42dd-b62e-ad4509141382"}}
interface IMessage {
sequenceId: number
type: string
from: string
fromUserId: string
group: string
dataType: string
data: {
type: string
body: string
more_body: boolean
response_id: string
websocket_request_id: string
conversation_id: string
}
}
// 将WebSocket消息写入流中
ws.on("message", (data: Buffer) => {
transformStream.write(data);
});
// 创建一个转换流,用于处理WebSocket消息
const transformStream = new Transform({
transform(chunk, encoding, callback) {
let res_data = chunk.toString();
let resp: ISystem | IMessage = JSON.parse(res_data);
if (resp.type == "system") {
const system = resp as ISystem;
this.push(system.reconnectionToken);
} else if (resp.type == "message") {
const message = resp as IMessage;
this.push(Buffer.from(message.data.body, 'base64').toString());
}
callback();
}
});
transformStream.on("data", function (data) {
console.log(data.toString());
});
通过创建一个可读可写的流,将ws中接收的数据写入到这个流中。
现在分析写这个数据包。
// 初始连接时会返回一个reconnectionToken
// 接收信息是通过data.body中的response_id和conversation_id来确认数据是否数据当前聊天的数据
下面正式修改接口。
注意:我们发起聊天请求前,需要先拿到wss,最主要的原因是:如果服务端响应太快,在我们连接wss的时候消息已经过去了,就会造成ws拿不到对应response_id和conversation_id的消息。
在src/types/chatgpt/response.ts
增加几个接口
export interface RegisterWebsocketResponse {
wss_url: string
expires_at: string
}
export interface ChatGPTConversationResponse {
conversation_id: string,
response_id: string,
}
export interface ChatGPTWsResponseSyStem {
type: string
event: string
userId: string
connectionId: string
reconnectionToken: string
}
export interface ChatGPTWsResponseMessage {
sequenceId: number
type: string
from: string
fromUserId: string
group: string
dataType: string
data: WsResponseMessageData
}
export interface WsResponseMessageData {
type: string
body: string
more_body: boolean
response_id: string
websocket_request_id: string
conversation_id: string
}
在src/chagpt/handler.ts
中增加初始化连接InitWebSocket方法
export const GetRegisterWebsocket = async (token: string) => {
const headers = {
'authority': 'chat.openai.com',
'accept': '*/*',
'accept-language': 'en-US',
'authorization': `Bearer ${token}`,
'content-type': 'application/json',
'origin': 'https://chat.openai.com',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
return requests.post<RegisterWebsocketResponse>("backend-api/register-websocket", null, {headers})
}
export const InitWebSocket = async (token: string) => {
try {
const {data} = await GetRegisterWebsocket(token)
return new WebSocket(data.wss_url, ["json.reliable.webpubsub.azure.v1"], {
perMessageDeflate: false,
headers: {
"Sec-Websocket-Protocol": "json.reliable.webpubsub.azure.v1",
"Origin": "https://chat.openai.com",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
},
agent: new SocksProxyAgent("socks5://127.0.0.1:7990")
});
} catch (e: any) {
return errorResponse(e.response.status, e.response.statusText)
}
}
src/utils/response.ts
export interface CustomErrorResponse {
message: string
code: number
}
export const errorResponse = ( code: number,message: string): CustomErrorResponse => {
return {
message,
code
}
}
修改后的接口。
const WebSocketPool = new Map<string, WebSocket>();
router.post("/chat/completions", async (req: RequestWithToken, res: Response) => {
const authorizationHeader = req.headers.authorization;
let token: string = process.env.OPENAI_API_KEY!
if (authorizationHeader && authorizationHeader.startsWith('Bearer ')) {
// 提取 Bearer 令牌
const result_token = authorizationHeader.split(' ')[1];
if (result_token != '') {
token = result_token
}
if (token == '') {
// 将解析后的信息存储在 req 对象中
return res.status(401).json({message: 'No token provided'});
}
}
const schema = Joi.object<APIRequest>({
messages: Joi.array<api_message>().required(),
stream: Joi.boolean().default(false),
model: Joi.string().required(),
});
const {error, value} = schema.validate(req.body);
if (error) {
return res.status(400).json({error: error.details[0].message});
}
if (!WebSocketPool.has(token) || WebSocketPool.get(token)!.readyState !== WebSocket.OPEN) {
const ws = await InitWebSocket(token)
if (!(ws instanceof WebSocket)) {
return res.status(ws.code).json(ws);
}
WebSocketPool.set(token, ws as WebSocket)
}
const ws = WebSocketPool.get(token)
if (!ws) {
return res.status(500).json({error: 'WebSocket connection failed'});
}
const convert = ConvertAPIRequest(value)
try {
const {data, headers} = await POSTConversation(convert, token)
let chat_msg: SSETransformer | ChatCompletionTransformer
const type = headers["content-type"];
if (!type.includes("text/event-stream")) {
let resp: ChatGPTConversationResponse
data.on("data", (chunk: Buffer) => {
resp = JSON.parse(chunk.toString()) as ChatGPTConversationResponse;
})
const transformStream = new Transform({
transform(chunk, encoding, callback) {
let resp: ChatGPTWsResponseSyStem | ChatGPTWsResponseMessage = JSON.parse(chunk);
if (resp.type == "message") {
const message = resp as ChatGPTWsResponseMessage;
this.push(Buffer.from(message.data.body, 'base64').toString());
}
callback();
}
});
ws!.on("message", (data: Buffer) => {
if (data.includes(resp.conversation_id || resp.response_id)) {
transformStream.write(data);
if (data.includes("ZGF0YTogW0RPTkVdCgo=")) {
transformStream.end();
// ws.close();
}
}
})
chat_msg = Handler(transformStream, value.stream, value.model)
} else {
chat_msg = Handler(data, value.stream, value.model)
}
if (value.stream) {
res.setHeader("Content-Type", "text/event-stream")
chat_msg.on("data", chunk => {
res.write(chunk)
if (chunk.includes("data: [DONE]")) {
res.end()
}
})
return;
}
chat_msg.on("data", (chunk: ChatCompletion) => {
res.json(chunk)
})
return
} catch (e: any) {
return res.status(e.response.status).json(e.response.data.json())
}
})
主要增加了
- WebSocketPool(websocket连接池)
- 通过判断聊天接口的返回的 headers[“content-type”],判断是流式响应还是wss响应,兼容text/event-stream。