开一贴,讲讲最新nodejs逆向官网的思路

主要针对上一篇文章中的接口做优化。
把里面用到https://chat-shared.zhile.io/shared.html?v=2的地址改成https://chat.openai.com就好。
目前已知官网对聊天接口增加了哪些参数的前置请求呢


下面主要针对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())
    }
})

主要增加了

  1. WebSocketPool(websocket连接池)
  2. 通过判断聊天接口的返回的 headers[“content-type”],判断是流式响应还是wss响应,兼容text/event-stream。

66 个赞

先赞再看,好像有点硬核

6 个赞

学习

5 个赞

先赞再看发现看不懂

5 个赞

先赞再看。

3 个赞

看不懂

4 个赞

学习 :grinning:

3 个赞

感谢分享整理

1 个赞

学习中!

1 个赞

mark

1 个赞

赞一下,收藏了慢慢看

6 个赞

Mark一下

1 个赞

涨知识


大佬,为什么我找不到这个请求啊,是需要什么插件吗

2 个赞

这里呀


我这里还是没有

这个是这样的,我看到它只会在首次加载的时候刷新一次,没看到是可能使用了旧的wss,没有被F12记录到,保持F12开启然后重新打开页面就有了。

1 个赞

强啊,后排马克

2 个赞

大佬,还真是这样,我刷新后F12,出现了

python的api
先前按照这个帖子搭了api,然后研究了一段时间发现没法用,需要改成wss处理,楼主大佬的思路给了很大启发。。。感谢大佬把wss的接口梳理出来了。。。