我做了第一個ChatGPT .net api聊天庫

2022-12-14 12:00:34

最近這個ChatGPT很火啊,看了B站上很多視訊,自己非常手癢,高低自己得整一個啊,但是讓我很難受的是,翻遍了github前十頁,竟然沒有一個C#的ChatGPT專案,我好難受啊!那能怎麼辦?自己搞一個吧。
但是,等等,現在的ChatGPT專案基本都是網頁逆向獲取幾個token,我不會啊,我都不知道哪些cookie是重要的,那我只能找一個其它語言的ChatGPT API專案,自己造(翻譯)一個c#的ChatGPT API庫了。

ChatGPT是啥?

先了解GTP,摘自官網的一段話「A set of models that can understand and generate natural language」,其實,就是一個自然語言處理的模型。所以ChatGPT顧名思義就是基於GPT3的一個聊天AI。
但是想讓他做苦力幫忙寫程式碼的話所需要的其實不是這個模型,而是Codex這個模型,按照官網所說「Most capable Codex model. Particularly good at translating natural language to code. In addition to completing code, also supports inserting completions within code.」。真的是碉堡了!

準備

第一步:註冊OpenAI賬號

因為對大陸以及中國香港地區不開放,所以我們需要小小的科學一下。
來一個我就是參照著申請賬號的園子的文章:
https://www.cnblogs.com/chatgpt/p/how-to-register-chatgpt-in-china.html
當然手機那裡可以淘寶找,幾塊錢就能幫忙註冊手機,賬號裡面預設還有18美元餘額。

注意點:如果搭建了科學還是提示不對你的國家提供服務的話,嘗試清空瀏覽器快取或者開啟瀏覽器的無痕視窗。Chrome預設在右上角三個點開啟就能找到「開啟新的無痕式視窗」。

萬事具備,直接擼程式碼

1.網頁獲取所需token和cookie

我們需要三個東西:UserAgent,CfClearance,Session_token
我們需要先開啟ChatGPT官方網站:https://chat.openai.com/chat 然後按下F12開啟瀏覽器的開發者模式
UserAgent在網路里(只需要複製UserAgent:後面的值):

CfClearance和Session_token在應用程式->cookie裡面

2.建立一個session用來表示一個對談

public OpenAISession(string session_token,string cfClearance,string userAgent)
{
    Session_token = session_token;
    CfClearance = cfClearance;
    UserAgent = userAgent;
    Headers = new Dictionary<string, string>();
    Cookies = new Dictionary<string, string>();
    Proxies = new Dictionary<string, string>();
}

Session可以重新整理自己的AccessToken和Session_token

public async Task RefreshSessionAsync()
{
    if (string.IsNullOrEmpty(Session_token))
    {
        throw new Exception("No tokens provided");
    }

    // Set cookies
    Cookies.Put("__Secure-next-auth.session-token", Session_token);
    Cookies.Put("cf_clearance", CfClearance);
    string cookiesString = GetCookiesString();
    Dictionary<string, string> map = new Dictionary<string, string>();
    Headers.Put("User-Agent", UserAgent);
    Headers.Put("cookie", cookiesString);
    Headers.Put("Cookie", cookiesString);

    var response = await GetAsync("https://chat.openai.com/api/auth/session");
    if (!response.IsSuccessStatusCode)
    {
        Console.WriteLine("err code: " + response.StatusCode);
        Console.WriteLine("cf_clearance: " + CfClearance);
        Console.WriteLine("token: " + Session_token);
        Console.WriteLine("userAgent: " + UserAgent);
        Console.WriteLine("請檢查以上引數是否正確,是否過期。");

        throw new Exception("無法獲取token!請重試");
    }

    try
    {
        string name = "__Secure-next-auth.session-token=";
        var cookies = response.Headers.GetValues("Set-Cookie");
        var stoken = cookies.FirstOrDefault(x => x.StartsWith(name));
        Session_token = stoken == null ? Session_token : stoken.Substring(name.Length, stoken.IndexOf(";") - name.Length);
        Cookies.Put("__Secure-next-auth.session-token", Session_token);
        var result = await response.Content.ReadAsStringAsync();
        AccessToken = JsonSerializer.Deserialize<Profile>(result, _jsonSerializerOptions)?.AccessToken;
        RefreshHeaders();
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error {nameof(RefreshSessionAsync)}:{ex}");
        throw new Exception($"Error {nameof(RefreshSessionAsync)}", ex);
    }
}

獲取到的最新的AccessToken更新到header裡,Session_token更新到cookie裡

private void RefreshHeaders()
{
    Headers.Put("Host", "chat.openai.com");
    Headers.Put("Accept", "text/event-stream");
    Headers.Put("Authorization", $"Bearer {AccessToken}");
    Headers.Put("User-Agent", UserAgent);
    Headers.Put("X-Openai-Assistant-App-Id", string.Empty);
    Headers.Put("Connection", "close");
    Headers.Put("Accept-Language", "en-US,en;q=0.9");
    Headers.Put("Referer", "https://chat.openai.com/chat");
}

string name = "__Secure-next-auth.session-token=";
var cookies = response.Headers.GetValues("Set-Cookie");
var stoken = cookies.FirstOrDefault(x => x.StartsWith(name));
Session_token = stoken == null ? Session_token : stoken.Substring(name.Length, stoken.IndexOf(";") - name.Length);
Cookies.Put("__Secure-next-auth.session-token", Session_token);

3.建立機器人繫結一個對談

public Chatbot(OpenAISession openAISession)
{
    OpenAISession = openAISession;
    ResetConversation();
}

/// <summary>
/// 重置Conversation,開啟一個新的對談
/// </summary>
public void ResetConversation() 
{
    _conversationId = null;
    _parentMessageId = Guid.NewGuid().ToString();
}

如何保持上下文聊天以及多使用者隔離?
上下文聊天已經內建,會自動與機器人的回覆進行關聯,當需要重新開啟一個對話的時候,可以呼叫ResetConversation,這樣對應的機器人物件發過去就是開啟了一個新的對話。

如何多使用者隔離?比如聯合微信機器人使用,可以將一個人或者一個群對應一個Chatbot物件,並且利用id與機器人做關聯。
當然這個_clientID需要使用者做唯一性管理

//可以作為隔離不同使用者端聊天上下文的憑據,即一個機器人繫結一個使用者端
//使用者端編號可以是,微信id,qq id,飛書id,亦或者自己開發的軟體的使用者id
private readonly string _clientID; 

public Chatbot(OpenAISession openAISession, string clientID)
{
    OpenAISession = openAISession;
    _clientID = clientID == null ? Guid.NewGuid().ToString() : clientID;
    ResetConversation();
}

最後構造對話物件,傳送到對應api

public async Task<Reply> GetChatReplyAsync(string prompt)
{
    var conversation = new Conversation();
    conversation.Conversation_id = _conversationId;
    conversation.Parent_message_id = _parentMessageId;
    conversation.Messages = new Message[]
    {
        new Message()
        {
            Content = new Content
            {
                Parts = new string []{ prompt }
            }
        }
    };

    return await GetChatResponseAsync(conversation);
}

/// <summary>
/// 獲取響應
/// </summary>
/// <param name="conversation"></param>
/// <returns></returns>
/// <exception cref="Exception">伺服器返回非200</exception>
private async Task<Reply> GetChatResponseAsync(Conversation conversation)
{
    using (var client = new HttpClient())
    {
        var response = await OpenAISession.PostAsync(_conversation, JsonSerializer.Serialize(conversation, _jsonSerializerOptions));
        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Faild to request.StatusCode:{response.StatusCode}");
        }
        var msg = await response.Content.ReadAsStringAsync();
        var data = msg.Split("\n")?.ToList().Where(x => !string.IsNullOrEmpty(x) && !x.Contains("data: [DONE]")).LastOrDefault()?.Substring(5);
        var reply = JsonSerializer.Deserialize<Reply>(data, _jsonSerializerOptions);
        _conversationId = reply.Conversation_id;

        return reply;
    }
}

效果截圖(僅測試)

OpenAISession openAIOptions = new OpenAISession(Session_token, CfClearance, UserAgent);
await openAIOptions.RefreshSessionAsync();
Chatbot chatbot = new Chatbot(openAIOptions);
Console.WriteLine("用c++寫個冒泡查詢");
var reply = await chatbot.GetChatReplyAsync("用c++寫個冒泡查詢");
Console.WriteLine(reply.Message.Content.Parts.FirstOrDefault());

原始碼(歡迎star)

https://github.com/BruceQiu1996/NChatGPTRev